数据结构·排序

1. 排序的概念及运用

1.1 排序的概念

        排序:排序是将一组“无序”的记录序列,按照某个或某些关键字的大小,递增或递减归零调整为“有序”的记录序列的操作

        稳定性:假定在待排序的记录序列中,存在多个具有相同关键字的记录,若经过排序,这些记录的相对次序保持不变。及在原序列中, r[i] = r[j],且 r[i] 在 r[j] 之前,而在排序后的序列中,r[i] 仍在 r[j] 之前,则称这种排序算法是稳定的;否则称为不稳定的

        内部排序:数据元素全部放在内存中的排序

        外部排序:数据元素太多,不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序 

        

1.2 常见的排序算法

        插入排序:直接插入排序,希尔排序

        选择排序:选择排序,堆排序

        交换排序:冒泡排序,快速排序

        归并排序:归并排序

2. 常见排序算法的实现

2.1 插入排序 

        基本思想:

        把待排序的记录按其关键码值的大小逐个插入到一个已经排好的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列,这个过程就像我们在整理扑克牌一样

2.1.1 直接插入排序 O(N^2)

        当插入第 i (i >= 1) 个元素时,前面的 array[0],array[1] …… array[i-1] 已经排好序,此时用 array[i] 的排序码与 array[i-1],array[i-2],…… 的排序码顺序进行比较,找到插入为止及将 array[i-1] 插入,原来位置上的元素顺序后移

         实现:

        

        直接插入排序的特性总结

                1. 元素集合越接近有序,直接插入算法的时间效率越高

                2. 时间复杂度 0(N^2)

                3. 空间复杂度 O(1)

                4. 稳定的排序算法

        

2.1.2 希尔排序 O(N^1.3)

        在学希尔排序之前,我们先对比一下同为 O(N^2) 量级的两个排序,插入和冒泡。

        他俩的结构很像,都是将数据们分成:已经排好的区域,和还没排好的区域。但是在每轮排序过程的处理方案时出现了分歧,冒泡排序是一定会再次遍历一遍那块没排好的区域,然后将一个数据落入已经排好的区域,但是插入排序有可能不用完全遍历那块已经排好的区域,甚至只用访问一次那块区域 (tmp >= a[end]),就可以从没排好的区域中落一个数据进入已经排好的区域。换句话说插入排序有很强的适应性,它几乎每轮排序都不是最坏的情况,甚至都还不错,这是冒泡排序达不到的,冒泡排序只会把每轮排序都当作最坏的情况处理。

        正是因为这个原因,在处理大量数据的绝大多数情况下,插入排序的速度要比冒泡排序块很多。或者说得益于插入排序的第一个特性,元素集合越接近有序,直接插入算法的时间效率越高,在绝大多数任务中插入排序的效果明显好于冒泡排序。

        那么我们该如何放大这个优势,优化插入排序,让其排序速度更快。这时聪明的希尔就有了这样一个思路,将排序过程分成两步。第一步是预排序:将数据们都整理的越来越有序,第二步是执行一次插入排序,经过这两步操作数据集合就有序了。

        那么有的同学这时候就开始怀疑了,这也没看出来什么神奇的思路,这个希尔排序怎么就好了,那么我展示一下用我们学过的四种排序处理 100,000 个随机数据的用时(ms),1,000,000个数据冒泡实在是跑不出来我们就看前3个,10,000,000个数据插入也跑不动了我们看剩下两个

                100,000个随机数据                                                        1,000,000个随机数据

                                                

                                                        10,000,000个随机数据

        看到了吗,希尔排序对于插入排序产生了巨大的优化,甚至在数据量巨大的情况下,其速度竟然超过了堆排序。

        这个测试就是开了几块空间往里头填相同的随机值,让这几种排序方法分别给这几块空间进行排序,记录了各自排序的时长,这个测试时长的代码挺简单的,这里我就不写了,如果想要可以评论区说一下

2.1.2.1 预排序

        预排序的思路就是选定一个整数n,把待排序集合中的所有数据分成n组,所有距离为n的数据分在同一组内,并对每一组内的数据进行排序。这样做的好处就是让大的数尽快到后面,小的数尽快到前面,选定的整数n越大,数字移动的越快,但是数据集合越不接近有序

        这里我们选定整数为3,因此所有数据被分成了3组 (黑、红、蓝)

                

        接下来就是给这三组数据分别排序,我们一步一步的展示如何写出一个排序

        第一步,是写出一次排序的代码

                        

        这里我们就能发现其实这跟插入排序的一轮排序是一样的,只是将自增自减的地方改成了加gap减gap

        第二步,将一组排序

                

        到这里我们将黑色组完成了排序,其实就是加了一层循环让每次排序跑起来

        第三步,将每组都排序

        首先我们能想到的就是再套一层循环控制排完一组再去排下一组

                

        但是我们还可以这样操作:控制完这组的第一个再控制下一组的第一个,每组第一个控制完之后,控制每组的第二个,这样并不会有负面影响,就相当于将每组的排序拆开来进行,大家可以体会一下这个过程,这样就只用一层循环就能解决问题

                

        但是他俩的效率是一样的,只是这种方法代码量小了一点,还有就是这种方法看起来相当的天才

        到这里预排序就完成了,现在的数据集合已经变成了一个比较有顺序的集合了

2.1.2.2 控制gap完成排序

        我们前面提到过gap越大,数据跳的越快,大的数据更快到后面,小的数据更快到前面,但是越不接近有序。gap越小数据跳的越慢,但是越接近有序,gup == 1时就是插入排序,数据集合直接就有序了。

        所以说我们要靠控制合适的gap值来达到时间效率的提升,这时有人想到了用这样的方法

                

        让gap每次除2,让数据元素处理的很有调理,还保证了最后一个值一定是1 ,因为一个大于1的数除2商一定为1。

        后来还有人提 gap = gap / 3 + 1 的方案,总之到现在为止还没有哪一种主张得到了充分的证明。

        对于希尔排序的时间复杂度计算到现在也还没有定论,有人利用大量的实验统计出当数据集合容量n很大时,时间复杂度大概在 O(N^1.25) 到 O(1.6*N^1.25) 之间,为了好记我就认为是 O(N^1.3)

        最后,希尔排序是不稳定的排序算法

2.2 选择排序

        基本思想:

        每一次从待排序的元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到所有待排序的元素都处理完

2.2.1 直接选择排序 O(N^2)

        这里我们每次找出最大和最小两个数据分别弄到数组尾部和头部,这样能节省一半的循环操作。因为我们每次排序都是交换两次数据,那就有可能刚交换完一次数据之后第二次又交换回去了,这样排序就出错了,这点要特别注意一下

        

        直接选择排序是不稳定排序算法,因为可能把很前面的一个大数据换到很后面,打乱了原本的顺序

2.2.2 堆排序 O(N*logN)

        堆排序我们刚讲完,这里就展示一下代码

数据结构·二叉树(一)-CSDN博客文章浏览阅读978次,点赞24次,收藏23次。本节介绍了二叉树的概念,基于完全二叉树引出了堆的概念,并着手实现了一个小根堆,最后我们介绍了堆应用中的堆排序和TOP-K问题,堆排序的时间复杂度是跟快排一个量级的,很有意思https://blog.csdn.net/atlanteep/article/details/136437696?spm=1001.2014.3001.5501        

        

        堆排序是不稳定排序算法

2.3 交换排序

        基本思想:

        根据元素集合中两个元素的大小比较结果来对换两个元素的位置,交换排序的特点是将较大的值向集合尾部移动,较小的值向集合首部移动

2.3.1 冒泡排序 O(N^2)

        这个太简单了,直接上代码

                

        冒泡排序是稳定的排序算法

2.3.2 快速排序 O(N*logN)

        快速排序是霍尔于1962年提出的一中二叉树结构的交换排序方式,其基本思想为:任取待排序元素序列中的某元素值作为基准值,利用基准值将元素集合分割成左子集合、基准值、右子集合。小于基准值的元素都放到左子集合,大于基准值的元素都放到右子集合。然后左右子集合重复该过程,直到所有元素都到达自己相应的位置上。

        下面我们解释一下,当元素右边都是小于它的值,左边都是大于它的值,那么我们是不是就可以认为这个元素已经到达了它相应的位置上。之后我们再将基准值的左右子集合中找到一个数放到它相应的位置上,不停的递归这一过程,最后元素集合不就有序了吗,这是快速排序的核心思想,大家可以理解一下。

        这里我直接展示一次排序的代码,然后咱们根据这个代码讲解一下

        ​​​​​​​        

        这里我们搞一组数据,一般都是将第一个数据设成基准值,或者说关键值

        然后我们让right指针先走,当找到小于基准值的数据时就停下来,然后再让left指针走,找到大于基准值的数据停下来,然后交换left指针和right指针指向的两个元素

        就这样一直让左右指针找下去,直到他俩相遇,此时把它们同时指向的元素和基准值交换,因为要交换基准值,所以我们记录基准值时并不是记录它的值,而是记录它所在的下标keyi,否则就根形参一样不会正确的交换。

        我知道,此时一定有同学会怀疑,为啥最后一步可以就这么直接交换呢,万一俩指针同时指向的值比key大怎么办。

        其实,结论是俩指针同时指向的值是一定比key小,大家可以思考一下。

        只要left和right之间有比key小的值,那么经过交换后,这个比key小的值一定会到left指针底下,此时如果left和right之间没有比key小的值了,那么下一次移动right就会停到left指针上面,此时他俩同时指向的值因为上一步的筛选一定比key小,这就是让right指针先走的特性。

        那如果从一开始left和right之间就没有比key小的值,那么right指针就会直接走到left指针上,也就是key上,此时交换一俩指针同时指向的值和key就相当于没交换,还是正确的。

        所以说经过这一次排序之后数组就变成了这样。

        此时6到达了它相应的位置,之后的排序就是去递归处理6左边的区域和右边区域即可,注意左右区域是不包括key的

        那么递归的最小子问题就是没有区域或者区域里只有一个值。

           

        快排是不稳定的排序算法

        

2.3.2.1 快速排序优化

        我们先跑一下快排,看看排1,000,000个数据和10,000,000个数据的用时

                ​​​​​​​                             

                        1,000,000个数据                                            10,000,000个数据

        我们发现在数据完全随机的情况下,快排的效率还是很不错的,同样是二叉结构它比堆排块的原因是堆排的建堆是有些消耗的。

        我们思考一下快速排序的时间复杂度到底是多少,因为它的思路于二叉树相同,所以说如果每次选出的key值都可以到区域最中间的情况是最好的,因为这种情况完全符合二叉结构,那么此时的时间复杂度就是 O(N*logN)。但是如果情况较坏,就是说数据集合是有序或者接近有序的情况下,时间复杂度就会来到 O(N^2)。

        那么我们该如何优化这一缺陷,事实上我们可以在left和right控制的区间中随机选一个数,与left指向的数据交换位置,就像这样,我们成这种方案叫随机选key

     ​​​​​​​        ​​​​​​​

        那么此时可能有人就认为这种随机的方法还是不靠谱,还是有几率出现最坏的情况,所以就又有人提出另一种解决方案叫三数取中

        ​​​​​​​

        这段函数让我们从left到right区间取出了一个既不可能是这段区域中的最大值也不可能是最小值的数据的下标,下一步到了快排函数中让这个下标指向的数据与a[left]交换就行了,后面的代码的处理方案与随机选key是一样的

        这个子函数的作用就是保证了取出来的值一定不是最大值也不是最小值,也就是说这样一来就一定不会出现最坏情况了,同时如果给出的任务集合是有序或接近有序的情况下,一下子就遍成了三数取中方案的最好情况

        其实随机选key三数取中两个方案,还是三数取中效率上好一点,但是随机选key的代码较短,最后还是根据实际情况自行取舍吧

        最后还有一种叫小区间优化的方案,这个优化方案与随机选key和三数取中优化的地方是不一样的,并且这个方案的优化效果不明显。

        我们知道二叉递归,如果是一个满二叉树的话,最后一层的递归次数占总次数的50%,倒数第二层的递归次数占总次数的25%,也就是说后三层递归节点占了整颗树的87.5%。因此有人提出也许我们不需要递归的那么深,就是说不用非得让区间中的数据只剩一个或者没有再返回,可以让区间中还剩10个值的时候,我们给这10个值插入排序就行了。为什么选择插入排序,因为数据量不大,不用使用堆排序或希尔排序,抛去他俩肯定就插入最好了。

        ​​​​​​​        

2.3.2.2 双指针快排法

        这种双指针的方法是后人在希尔大佬的思想下延伸出来的另一种快排写法,这种写法的代码量被大大缩减了,同时时间效率相同

        一样的,还是先选好一个关键值key,然后创建prev指针和cur指针

        当cur指向的值大于等于key cur指针向后移动一位

        当cur指向的值小于key prev指针向后移动一位,同时交换 prev 和 cur 指向的两个值,然后再将cur指针向后移动一位

        最后当cur遍历完整个数组之后就变成了这样,然后我们将key与prev指向的数据交换

        此时的效果就是key到达了它对应的位置了。

        这个放方法的关键就是保证了prev指针前面的元素都小于key(key除外),最后当cur指针遍历完数组的时候数组中所有小于key的数据都会出现在prev指针之前(key除外),并且prev指向的元素一定小于key,或者压根就是key。大家可以多走读几遍,理解一下这里面的逻辑 。

        我们直接上代码

        ​​​​​​​     

2.3.2.3 非递归快排法

        如何绕开递归来实现快排呢,我们需要借助到栈。首先将需要排序的左右区间压入栈中,处理完后将这对控制区间的数字出栈,此时又出现了两个区间,我们先将右区间压栈,再将左区间压栈。然后取栈顶元素,取到了左区间进行处理并出栈,此时又出现两个区间······如此操作下去,如果区间中只有一个或没有值就不入栈,最后栈为空就说明都处理完了,记得销毁栈。

        这里我们用双指针的方式处理一次排序

        这里我给出的数据集合案例不太好,大家能明白是什么意思就行,其实这就是用栈后进先出的特点实现了类似递归的功能。

        再实际敲代码的时候我们要不要把栈中元素的类型改成一个二元数组来表示区域呢?其实不必,我们只要保证:先压右边界再压左边界对应先取左边界再取右边界的顺序就好。进两个区域的时候也同理,一直保证先压右区域再压左区域,相当于后处理右区域,先处理左区域。

        处理就是单趟排序,将key放到它对应的位置

        ​​​​​​​        ​​​​​​​        

        这个用栈模拟递归的思路还是很巧妙的,那我们为什么要有这种非递归的需求呢?是因为函数栈帧(函数调用所需空间)是建立在操作系统的栈(这个栈与我们自己写的栈是完全两回事)上的,这个操作系统的栈比较小,如果我们的递归深度太深就会导致栈溢出,从而崩掉。而我们自己写的栈是建立在堆区上的,这块区域相对来说就比较大了,不会出现类似栈溢出的问题。

        事实上非递归还可以用队列实现,队列拥有先进先出的特点,那么在实现的时候我们就能看到队列是以层序遍历的结构在进行处理的,先处理左区域,然后把左区域中的左右区域入队列,pop掉左区域,然后取队头就是右区域,处理完之后同样把右区域中的左右队列入进来,pop掉右区域。此时一层就处理完了,然后接下来去处理下一层。只是这种处理结构与递归时的结构思路不同,我就不重点讲解了。

2.4 归并排序 O(N*logN)

        归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已经有序的子序列合并,得到完全有序的数列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

        单趟归并中,我们拿到两个有序的数组之后,用4个指针控制着向后走,哪个begin指针指示的数据小就入到新开辟的空间tmp中,直到有一个数组遍历完,再把另一个数组剩下的数据粘到新开辟的空间tmp中

        归并排序我们还是要用分治的思想借助递归完成,当分开的区域中就剩一个数据的时候就可以开始返回了

        ​​​​​​​        

        归并排序的思路很简单,但是其中的一些控制细节需要用点心

        归并排序是稳定的排序算法

2.4.1 非递归归并

        如果像快排那样用栈实现的话,我们想一下就会发现不现实,因为快排单趟排完之后就不用那块区间了,或者说快排的排序发生在逻辑树展开的过程中,等逻辑树到达了所有叶节点(区间大小<=1)之后排序就结束了。但是归并的排序是发生在逻辑树回归的过程当中,将区域从1开始慢慢扩大,每扩大一次就实现一次单趟归并,我们都将上一块区域的界限pop掉了,找不到上一级的区域界限那就归不动了。

        所以我们要换一种思路,将相邻数据区间进行归并,这样做的时间复杂度不变,但是思考方案确实与递归不同了

        ​​​​​​​        ​​​​​​​        

        ​​​​​​​        

        非递归的主要难点不是思路,而是越界处理的细节控制

2.5 非比较排序

        前面我们讲到的排序都是比较排序,核心都是通过比较大小进行排序的。而非比较排序的核心不在比较,常见的非比较排序有:计数排序、基数排序、桶排序,非比较排序都是小众且局限的排序方案,这里我们仅讲解计数排序

2.5.1 计数排序 O(N)

        计数排序的思路很简单,就是再弄一个计数数组,下标对应任务数组的值,然后扫描任务数组,比如任务数组出现了一个6,那就在计数数组下标为6的位置加1。如此做的意义就是任务数组中一个值出现了几次,都会被计数数组在相应的映射位置上统计下来。

        这种方案听起来是不是特别简单,而且时间复杂度就是O(N),空间复杂度是O(N)。但是这个方案有两个局限性:第一是只适合排整形,而实际应用中很少有这种需求;第二是只适合任务数组的值较为集中的情况,否则计数数组要开很大。

        ​​​​​​​        

        这里我还是简单说一下基数排序和桶排序吧,它们的原理是一样的,都是先排个位,再排十位,再排百位,这样往上排。区别就是基数排序直接用变量存储每次排序的结果,而桶排序是用链表存储的,比如个位是0的用链表串成一串挂起来,个位是1的用链表串成一串挂起来这样。这两种排序方案效率既低,消耗又大,同时还没有像冒泡排序那样的教学意义,总之就是很鸡肋。

3. 本节完整代码

Sort.h

#pragma once

#include"Stack.h"
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<string.h>

//时间计算
void TestOp();

//打印数组
void PrintArry(int* a, int n);

//交换
void Swap(int* p1, int* p2);

//插入排序
void InsertSort(int* a, int n);

//希尔排序
void ShellSort(int* a, int n);

//直接选择排序
void SelectSort(int* a, int n);

//堆排序
void HeapSort(int* a, int n);

//快排
void QuickSort(int* a, int left, int right);

//双指针快排
void QuickSort2(int* a, int left, int right);

//快排非递归
void QuickSort3(int* a, int left, int right);

//归并排序
void MergeSort(int* a, int n);

//非递归归并排序
void MergeSort2(int* a, int n);

//计数排序
void CountSort(int* a, int n);

//冒泡
void BubbleSort(int* a, int n);

Sort.c

#include"sort.h"

//打印数组
void PrintArry(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}


void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}




//插入排序 O(N^2)
void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		//变量end指示有序数列的最后一个位置
		int end = i;
		int tmp = a[end + 1];
		while (end >= 0)
		{
			if (tmp < a[end])
			{
				a[end + 1] = a[end];
				end--;
			}
			else
				break;
		}
		a[end + 1] = tmp;
	}
}





//希尔排序 O(N^1.3)
void ShellSort(int* a, int n)
{
	int gap = n;
	while (gap > 1)
	{
		gap /= 2;

		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
					break;
			}
			a[end + gap] = tmp;
		}
	}
}





//直接选择排序
void SelectSort(int* a, int n)
{
	int begin = 0, end = n - 1;

	while (begin < end)
	{
		//选出最小值和最大值的位置
		int mini = begin, maxi = begin;
		for (size_t i = begin + 1; i <= end; i++)
		{
			if (a[i] < a[mini])
				mini = i;
			if (a[i] > a[maxi])
				maxi = i;
		}
		Swap(&a[begin], &a[mini]);
		//防止maxi和begin重叠,从而交换两次的错误
		if (maxi == begin)
			maxi = mini;

		Swap(&a[end], &a[maxi]);
		begin++;
		end--;
	}
}




//堆排序 O(N^logN)
void AdjustDown(int* a, int n, int parent)
{
	//假设法,选出大的孩子
	int child = parent * 2 + 1;//假设左孩子大
	while (child < n)//当child>=size时出数组了说明是叶子
	{
		if (child + 1 < n && a[child + 1] > a[child])
		{
			//找到小的那个孩子
			child++;
		}

		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
void HeapSort(int* a, int n)
{
	//数组直接建大堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
		AdjustDown(a, n, i);

	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}





//快速排序

//三数取中 left mid right
int GetMidi(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
			return mid;
		else if (a[left] > a[right])//a[right] a[left] a[mid]
			return left;
		else//a[left] a[right] a[mid]
			return right;
	}
	else //a[left] >= a[mid]
	{
		if (a[mid] > a[right])
			return mid;
		else if (a[left] < a[right])
			return left;
		else
			return right;
	}
}


void QuickSort(int* a, int left, int right)
{
	//区间只有一个值或者没有值就结束
	if (left >= right)
		return;

	//小区间优化
	//if (right - left + 1 <= 10)
	//{
	//	InsertSort(&a[left], right - left + 1);
	//}
	else
	{
		int begin = left, end = right;

		在left和right之间选一个随机值做key
		//int randi = rand() % (right - left);
		//randi += left;
		//Swap(&a[left], &a[randi]);

		//三数取中
		int midi = GetMidi(a, left, right);
		Swap(&a[left], &a[midi]);

		int keyi = left;
		while (left < right)
		{
			//让right先走,找小
			while (left < right && a[right] >= a[keyi])
				right--;

			//left再走,找大
			while (left < right && a[left] <= a[keyi])
				left++;

			Swap(&a[left], &a[right]);
		}
		//此时left right相遇
		Swap(&a[left], &a[keyi]);

		keyi = left;
		//[begin, keyi-1] keyi [keyi+1, end]
		QuickSort(a, begin, keyi - 1);
		QuickSort(a, keyi + 1, end);
	}
}


//双指针快排
void QuickSort2(int* a, int left, int right)
{
	if (left >= right)
		return;


	int keyi = left;
	int prev = left;
	int cur = left + 1;

	while (cur <= right)
	{
		if (a[cur] < a[keyi])
		{
			prev++;
			Swap(&a[prev], &a[cur]);
		}
		cur++;
	}
	Swap(&a[keyi], &a[prev]);
	keyi = prev;
	//[left, keyi-1] keyi [keyi+1, right]
	QuickSort2(a, left, keyi - 1);
	QuickSort2(a, keyi + 1, right);
}

//快排非递归
void QuickSort3(int* a, int left, int right)
{
	ST st;
	STInit(&st);
	STPush(&st, right);
	STPush(&st, left);

	//栈不为空就说明还没处理完
	while (!STEmpty(&st))
	{
		int begin = STTop(&st);
		STPop(&st);
		int end = STTop(&st);
		STPop(&st);

		//走单趟排序
		int keyi = begin;
		int prev = begin;
		int cur = begin + 1;

		while (cur <= end)
		{
			if (a[cur] < a[keyi])
			{
				prev++;
				Swap(&a[prev], &a[cur]);
			}
			cur++;
		}
		Swap(&a[keyi], &a[prev]);
		keyi = prev;

		//[begin, keyi-1] keyi [keyi+1, end]
		//先进右再进左
		if (keyi + 1 < end)
		{
			//进右
			STPush(&st, end);
			STPush(&st, keyi + 1);
		}
		if (begin < keyi - 1)
		{
			//进左
			STPush(&st, keyi - 1);
			STPush(&st, begin);
		}
	}
	STDestory(&st);
}






//归并排序
void _MergeSort(int* a, int begin, int end, int* tmp)
{
	//当区间中只剩一个值的时候就返回
	if (begin == end)
		return;

	int mid = (begin + end) / 2;
	//[begin,mid] [mid+1,end]
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid + 1, end, tmp);

	//归并
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	int i = begin;
	//依次比较,取小的尾插到tmp数组
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] <= a[begin2])
			tmp[i++] = a[begin1++];
		else
			tmp[i++] = a[begin2++];
	}

	while (begin1 <= end1)
		tmp[i++] = a[begin1++];
	while (begin2 <= end2)
		tmp[i++] = a[begin2++];

	memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}

void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(n * sizeof(int));
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}

	_MergeSort(a, 0, n - 1, tmp);
	free(tmp);
	tmp = NULL;
}



//非递归归并排序
void MergeSort2(int* a, int n)
{
	int* tmp = (int*)malloc(n * sizeof(int));
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}

	int gap = 1;

	while (gap < n)
	{
		for (int j = 0; j < n; j += 2 * gap)
		{
			int begin1 = j, end1 = begin1 + gap - 1;
			int begin2 = end1 + 1, end2 = begin2 + gap - 1;

			//越界处理
			if (end1 >= n || begin2 >= n)
				break;
			if (end2 >= n)
				end2 = n - 1;

			int i = j;
			//依次比较,取小的尾插到tmp数组
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
					tmp[i++] = a[begin1++];
				else
					tmp[i++] = a[begin2++];
			}

			while (begin1 <= end1)
				tmp[i++] = a[begin1++];
			while (begin2 <= end2)
				tmp[i++] = a[begin2++];

			memcpy(a + j, tmp + j, sizeof(int) * (end2 - j + 1));
		}
		gap *= 2;
	}

	free(tmp);
	tmp = NULL;
}


//计数排序
void CountSort(int* a, int n)
{
	int min = a[0], max = a[0];
	for (int i = 0; i < n; i++)
	{
		if (a[i] > max)
			max = a[i];
		if (a[i] < min)
			min = a[i];
	}

	int range = max - min + 1;
	int* count = (int*)malloc(sizeof(int) * range);
	if (count == NULL)
	{
		perror("malloc fail");
		return;
	}
	memset(count, 0, sizeof(int) * range);

	//统计次数
	for (int i = 0; i < n; i++)
	{
		count[a[i] - min]++;
	}

	//排序
	int j = 0;
	for (int i = 0; i < range; i++)
	{
		while (count[i]--)
		{
			a[j++] = i + min;
		}
	}
}



//冒泡 O(N^2)
void BubbleSort(int* a, int n)
{
	for (int j = 0; j < n - 1; j++)
	{
		int flag = 0;
		for (int i = 1; i < n - j; i++)
		{
			if (a[i - 1] > a[i])
			{
				Swap(&a[i - 1], &a[i]);
				flag = 1;
			}
		}
		//当一趟冒泡结束后发现没有进行数据交换
		//说明已经有序了
		if (flag == 0)
			break;
	}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/485210.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

[Java基础揉碎]单例模式

目录 什么是设计模式 什么是单例模式 饿汉式与懒汉式 饿汉式vs懒汉式 懒汉式存在线程安全问题 什么是设计模式 1.静态方法和属性的经典使用 2.设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、 以及解决问题的思考方式。设计模式就像是经典的棋谱&am…

Docker进阶:Docker-cpmpose 实现服务弹性伸缩

Docker进阶&#xff1a;Docker-cpmpose 实现服务弹性伸缩 一、Docker Compose基础概念1.1 Docker Compose简介1.2 Docker Compose文件结构 二、弹性伸缩的原理和实现步骤2.1 弹性伸缩原理2.2 实现步骤 三、技术实践案例3.1 场景描述3.2 配置Docker Compose文件3.3 使用 docker-…

Spark Map 和 FlatMap 的比较

Spark Map 和 FlatMap 的比较 本节将介绍Spark中map(func)和flatMap(func)两个函数的区别和基本使用。 函数原型 map(func) 将原数据的每个元素传给函数func进行格式化&#xff0c;返回一个新的分布式数据集。 flatMap(func) 跟map(func)类似&#xff0c;但是每个输入项和…

基于51单片机数控直流电压源proteus仿真LCD显示+程序+设计报告+讲解视频

基于51单片机数控直流电压源proteus仿真LCD显示( proteus仿真程序设计报告讲解视频&#xff09; 仿真图proteus7.8及以上 程序编译器&#xff1a;keil 4/keil 5 编程语言&#xff1a;C语言 设计编号&#xff1a;S0072 讲解视频 基于51单片机数控直流电压源proteus仿真程序…

37、Linux中Xsync数据同步备份工具

37、Linux中Xsync数据同步备份工具 一、介绍二、配置集群hostname三、修改xsync文件四、赋权五、安装Rsync六、验证一七、配置免密登录1、生成rsa密钥2、copy机器自身公钥到目标机器3、.ssh/文件目录赋权 八、验证二 ⚠️ 注&#xff1a;本文全程在普通用户下操作&#xff0c;…

基于spring boot的个人博客系统的设计与实现(带源码)

随着国内市场经济这几十年来的蓬勃发展&#xff0c;突然遇到了从国外传入国内的互联网技术&#xff0c;互联网产业从开始的群众不信任&#xff0c;到现在的离不开&#xff0c;中间经历了很多挫折。本次开发的个人博客系统&#xff0c;有管理员&#xff0c;用户&#xff0c;博主…

QT----基于QT的人脸考勤系统ubuntu系统运行,编译开发板

目录 1 Ubantu编译opencv和seetaface库1.1 Ubantu编译opencv1.2 Ubuntu编译seetaface1.3 安装qt 2 更改代码2.1 直接运行报错/usr/bin/ld: cannot find -lGL: No such file or directory2.2 遇到报错摄像头打不开2.3 修改部分代码2.4 解决中文语音输出问题 3 尝试交叉编译rk358…

QTabWidget的tabbar不同方向显示 文字方向设置 图标跟随变化 实现方式 qt控件绘制原理

先来看结果图&#xff1a;&#xff08;参考博客&#xff1a;QTabWidget中tab页文本水平或垂直设置_pyqt tab_widget.settabposition(qtabwidget.west) 字体-CSDN博客&#xff09; 从图中可知&#xff0c;"普通"是qt自己的样式&#xff0c;但是很明显&#xff0c;在垂…

SpringBoot Starter解析

conditional注解解析 介绍 基于条件的注解作用: 根据是否满足某一个特定条件决定是否创建某个特定的bean意义: Springboot实现自动配置的关键基础能力 常见的conditional注解 ConditionalOnBean: 当容器中存在某个Bean才会生效ConditionalOnMissingBean: 不存在某个Bean才会…

JavaEE企业级分布式高级架构师课程

教程介绍 本课程主要面向1-5年及以上工作经验的Java工程师&#xff0c;大纲由IT界知名大牛 — 廖雪峰老师亲自打造&#xff0c;由来自一线大型互联网公司架构师、技术总监授课&#xff0c;内容涵盖深入spring5设计模式/高级web MVC开发/高级数据库设计与开发/高级响应式web开发…

【ESP32S3 Sense接入百度在线语音识别】

视频地址&#xff1a; ESP32S3 Sense接入百度在线语音识别 1. 前言 使用Seeed XIAO ESP32S3 Sense开发板接入百度智能云实现在线语音识别。自带麦克风模块用做语音输入&#xff0c;通过串口发送字符“1”来控制数据的采集和上传。 步骤概括    (1) 在百度云控制端选择“语音…

【从零开始学习Redis | 第七篇】认识Redis底层数据结构

目录 前言&#xff1a; 动态字符串SDS&#xff1a; SDS的优势&#xff1a; IntSet&#xff1a; IntSet的特点&#xff1a; Dict&#xff1a; Dict的扩容&#xff1a; ​编辑 Dict的收缩&#xff1a; Rehash&#xff1a; Dict的特点&#xff1a; 总结&#xff1…

uniapp-Form示例(uviewPlus)

示例说明 Vue版本&#xff1a;vue3 组件&#xff1a;uviewPlus&#xff08;Form 表单 | uview-plus 3.0 - 全面兼容nvue的uni-app生态框架 - uni-app UI框架&#xff09; 说明&#xff1a;表单组建、表单验证、提交验证等&#xff1b; 截图&#xff1a; 示例代码 <templat…

C++入门:类和对象(上)

类和对象重点解析 1.类的定义1.类的访问限定符及封装1.C实现封装的方式2.访问限定符注意 3.封装 2.类对象模型2.1类对象存储方式2.2类对象的大小2.2.1结构体内存对齐原则2.2.2为什么要内存对齐 3.this指针3.1this指针的引出3.2this指针的特性3.3this指针的存储3.4this指针可以为…

Three.js 中的 OrbitControls 是一个用于控制相机围绕目标旋转以及缩放、平移等操作的控制器。

demo案例 Three.js 中的 OrbitControls 是一个用于控制相机围绕目标旋转以及缩放、平移等操作的控制器。下面是它的详细讲解&#xff1a; 构造函数: OrbitControls(object: Camera, domElement?: HTMLElement)object&#xff1a;THREE.Camera 实例&#xff0c;控制器将围绕…

Kafka快速入门及使用

入门 官网 简介 Kafka是一个分布式的流媒体平台应用&#xff1a; 消息系统日志收集用户行为追踪流式处理 特点 高吞吐量消息持久化高可靠性高扩展性 常用术语 Broker&#xff1a;集群中的服务器Zookeeper&#xff1a;服务管理Topic&#xff1a;主题&#xff0c;Kafka发…

Attention Is All You Need若如爱因斯坦的相对论,Transformer模型则堪称E=MC^2之等量公式

Transformer模型已经成为当前所有自然语言处理NLP的标配&#xff0c;如GPT&#xff0c;Bert&#xff0c;Sora&#xff0c;LLama&#xff0c;Grok等。假如《Attention Is All You Need》类比为爱因斯坦的侠义相对论&#xff0c;Transformer模型则堪称EMC^2之等量公式。 看过论文…

IDEA Android新建项目基础

title: IDEA Android基础开发 search: 2024-03-16 tags: “#JavaAndroid开发” 一、构建基本项目 在使用 IDEA 进行基础的Android 开发时&#xff0c;我们可以通过IDEA自带的新建项目功能进行Android应用开发基础架构的搭建&#xff0c;可以直接找到 File --> New --> …

数据库管理开发工具Navicat for MySQL Mac版下载

Navicat for MySQL&#xff08;Mac版&#xff09;是一款强大的数据库管理开发工具&#xff0c;专为MySQL设计。它提供直观的用户界面&#xff0c;支持数据建模、查询构建、数据传输等功能&#xff0c;帮助用户轻松管理数据库。其特点包括高效的数据处理能力、安全的数据传输机制…

【算法与数据结构】 C语言实现单链表队列详解

文章目录 &#x1f4dd;队列&#x1f320; 数据结构设计&#x1f309;初始化队列函数 &#x1f320;销毁队列函数&#x1f309;入队函数 &#x1f320;出队函数&#x1f309;获取队首元素函数 &#x1f320;获取队尾元素函数&#x1f309; 判断队列是否为空函数&#x1f309;获…