数据结构——八大排序
一.排序的概念和其应用
1.1排序的概念
- 排序:排列或排序是将一组数据按照一定的规则或顺序重新组织的过程,数据既可以被组织成递增顺序(升序),或者递减顺序(降序)。
- 稳定性:假定在待排序的数据序列中,存在多个相同的数据,若经过排序,这些数据的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排 序算法是稳定的;否则称为不稳定的。
- 内部排序:待排序的数据全都存放在内存中。
- 外部排序:外排的数据太多,内存无法同时存储这么多的数据,需要不停从外存中提取数据到内存中进行排序。
1.2排序的应用
排序与我们的生活息息相关,当我们想买一部性价比较高的手机时,我们打开购物网站可以根据综合排序来找到最近热门的手机型号。
当然我们还可以根据销量、评论数等等来进行选择,这里面就是排序的过程。该网站获取所有手机的销量,然后排序,再展示给使用者。这就是排序在生活中的作用。
还比如我们到了一个地方旅游不知道吃什么,我们就可以根据大众点评上面的餐饮排序来选择。
所以,排序是我们日常生活中不可或缺的。而在数据结构与算法中,排序大致分为:插入排序、选择排序、交换排序和归并排序。而这几种下面又具体分化出不同的排序方法。
下面我们来了解一下这几种常见的排序方法。
二.冒泡排序
冒泡排序是我们刚开始借助C语言所学的一种排序方式,其排序的基本思路是遍历待排序的数据,两两相邻的数据之间进行比较,与要求的排列顺序进行对比,如果顺序不正确就要进行交换。直到将所有的数据都交换至有序为止。动图如下:
假设我们要排升序,那我们就从头开始进行比较,如果前一个大于后一个就交换,这样就可以将该数据中最大的一个数据放到最后一个位置,这是一次排序,然后我们要从头开始继续比较,将次大的数放到倒数第二个的位置上。重复该操作我们就可以将该数据变为有序。
//交换
void Swap(int* a, int* b)
{int tmp = *a;*a = *b;*b = tmp;
}//冒泡排序
void BubbleSort(int* a, int n)
{//打印为排序的数据Print(a, n);int j = 0;for (j = 0; j < n; j++){//一趟交换int i = 0;int flag = 0;//标记,判断一趟中是否发生交换for (i = 1; i < n - j; i++){if (a[i - 1] > a[i]){flag = 1;Swap(&a[i - 1], &a[i]);//交换}}//序列已经有序,不必在进行排序if (flag == 0){break;}}//打印排序好的数据Print(a, n);
}
我们根据冒泡排序的执行逻辑可以推出其时间复杂度为:O(N^2).冒泡排序的一些细节:
三.插入排序
插入排序是一种非常清晰明了的一种排序方式,通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。我们所熟知的扑克牌中就暗藏我们的插入排序。我们先将某一张牌看作有序,然后将后面无序的牌与前面的牌进行比较,然后插入到合适的位置。
动图演示:
插入排序中的直接插入排序就很好的利用了这一算法逻辑,实现了插入排序。
3.1直接插入排序
直接插入排序的逻辑就是插入排序的逻辑,我们根据动图分析,我们先将第一个数3看作为有序,然后将后面的数与其进行比较,如果小于则3往后挪,将后面的数插到前面,否则就不动,此时这两个数就有序了。然后插入第三个数据,插入的地方一定是大于等于前一个数并且小于等于后一个数。
直接插入排序的特性总结:
- 元素集合越接近有序,直接插入排序算法的时间效率越高
- 时间复杂度:O(N^2)
- 空间复杂度:O(1),它是一种稳定的排序算法
- 稳定性:稳定
然而我们现在只是完成了单次的交换,那么怎样实现插入排序的全过程呢? 我们知道了end是有序队列的最后一个的下标,所以我们就可以控制end来实现插入排序的实现。我们使end从0开始,到n-1(n表示数据个数),就可以实现对n个数据的排序。
//插入排序
void InsertSort(int* a, int n)
{int i = 0;for (i = 0; i < n - 1; i++){int end = i;int tmp = a[end + 1];//待插入的数while (end >= 0){if (a[end] > tmp){a[end + 1] = a[end];//将大数往后放end--;}else{break;}}a[end + 1] = tmp;}
}
我们i从0~n-2,就可以对n个数进行排序。为什么i<n-1呢?因为i = end,而待排序的数的下标是end+1,如果i<n的话,i最大就是n-1,end+1 = n,而n不是数组的下标,就越界了。我们通过i的位置,就可以一直往后找到待排序的数。当i = n-2的时候,此时就只剩一个数没有被排了。
3.2希尔排序
希尔排序利用的基本方法也是插入排序,但是希尔排序在排序前会先进行预排序,使序列更接近有序。然后再进行一次插入排序使序列有序。
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数gap,把待排序文件中所有记录分成gap组,所有距离为gap的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工 作。当到达=1时,所有记录在统一组内排好序。
什么意思呢?我们借助图来进行分析。
知道了思路,那如何实现希尔排序呢?
我们进行一次预排序是不能使队列更接近有序的,所以我们需要进行多组预排序,使我们的数据更可能的接近有序。
那么再次进行预排序的时候gap还等于3有用么?
当然没有用了,因为我们根据gap等于3已经对其进行了预排序,其分为3组的情况已经有序了。所以我们要改变gap的值后再进行预排序。
那么现在gap的值是应该变大还是变小呢?
答案是变小,因为我们在预排序之后还要进行一次插入排序,而我们可以观察到当gap等于1时,希尔排序和插入排序是一样的。
这样不仅可以解决要进行多次预排序的问题,还可以解决最后一次插入排序的问题。 那么gap要怎么取呢?gap的取法有很多种,没有一个固定的要求,但是目前大多数都已gap/3+1的方式来取gap。这样可以保证最后一次gap一定等于1.
// 希尔排序
void ShellSort(int* a, int n)
{int gap = n;while (gap > 1){gap = gap / 3 + 1;int i = 0;for (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;}}
}
3.2.1希尔排序的时间复杂度
希尔排序的时间复杂度非常难计算,因为gap取的不同,算出的时间复杂度也不同。但是目前都比较认可的时间复杂度为:O(N^1.3)。
我们从图中可分析出,gap的变化会导致交换次数的变化,而gap的取法又是多种多样的,所以希尔排序的时间复杂度非常难计算。
四.选择排序
选择排序也是我们刚开始学习C语言就接触到的一种排序方法。其排序思想是:遍历待排数据,从中选出最小的与第一个数据进行交换,然后重新遍历数据找到次小的数据放到第二个数据的位置。直到所有数据有序。我们从分析中可以得知其时间复杂度为O(N^2).
直接选择排序的特性总结:
- 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:不稳定
//选择排序
void SelectSort(int* a, int n)
{int i = 0;for (i = 0; i < n; i++){int mini = i;int j = 0;for (j = i; j < n; j++){if (a[mini] > a[j]){mini = j;}}Swap(&a[i], &a[mini]);}
}
我们在遍历的时候记录下当前数据中最小数据的下标,如果遇到更小的就更新下标,遍历完之后,与第一个数据交换。此时我们第一个数据就是最小的数据,我们直接从第二个开始遍历,如果从第一个开始的话结果会错误。以此类推,第三次从第三个开始遍历。
这种选择排序的效率太低了,实践中没有什么意义。所以我们可以对其进行优化。
我们可以从左右两边同时开始遍历,左边找最小的放,右边找最大的放。
//优化选择排序
void OptimizeSelectSort(int* a, int n)
{int begin = 0;int end = n - 1;while (begin < end){int mini = begin;//记录最小数据的下标int maxi = end;//记录最大数据的下标int j = 0;for (j = begin; j <= end; j++){if (a[mini] > a[j]){mini = j;}if (a[maxi] < a[j]){maxi = j;}}if (mini + maxi == n - 1){Swap(&a[mini], &a[maxi]);}else if (maxi == begin){Swap(&a[end], &a[maxi]);Swap(&a[begin], &a[mini]);}else if (mini == end){Swap(&a[begin], &a[mini]);Swap(&a[end], &a[maxi]);}begin++;end--;}
}
五.堆排序
堆排序是数据结构堆的一种应用,我之前已经介绍过了,大家可以看这篇文章来了解堆排序。小伙饿坏了,赶紧点了一份爱学的二叉树——堆,狼吞虎咽学完很过瘾-CSDN博客
六.快速排序
6.1hoare快排
hoare快排的基本思想是:选左边的数为关键字key,然后R先从右边开始遍历序列找到第一个比key小的数就停下,然后L开始从左边开始遍历,找到第一个比key大的数停下来,然后交换R和L的值,然后R继续找,找到后L找,然后交换,当R与L相遇的时候,与key交换。这就完成了一次排序,使key换到了自己有序的位置上,左边的数都小于key右边的数都大于key。然后key会将区间分成左右两个区间,然后分别对这两个区间采用相同的操作就可以使数据有序。
下面我们来实现一下单趟排序的逻辑:
首先选取区间左端点为key,L从左端点开始,R从右端点开始
然后R先向左走,找到第一个比key小的数,然后停下来
然后L开始向右走,找到第一个比key大的数停下来
然后交换L和R对应位置上的数据
此时L和R还没有相遇,R继续向左走找比key小的数据
找到后,L向右走找比key大的数
L和R还没有相遇,交换L和R对应的值
然后重复刚才找小,和找大的过程 判断有没有相遇,没有相遇L再向右走找大
当L和R相遇之后,交换key位置的数据和L、R位置上的数据
然后,原来key对应的数6,此刻就已经有序了,因为它的左边都是小于它的数,右边都是大于它的数 。接着想要让该序列有序的话,只需要让6的左右区间分别有序就可以了。而且让其有序的方法和上述方法一样,所以我们这里需要用到递归的思想。
然后下面的左右区间也会使一个数有序,然后又将区间分成左右两个,然后再利用递归使其有序。
理解了思想,我们先来实现快排单趟如何实现,也就是如果先使一个数据有序?
我们单趟完成之后,只需要采用递归的思想,对左右两个区间分别进行该操作即可。 但是我们递归就必须得有递归地结束条件。每完成一次就会有一个数有序,分出来左右区间,那么当分出来的区间只有一个数时或者区间不存在时,还需要排序么?
所以递归地结束条件就是当区间只有一个数或者区间不存在。
//hoare快排
void hoareSort(int* a, int left, int right)
{//区间只有一个数或者没有数时不需要排序if (left >= right){return;}int begin = left;int end = right;int keyi = left;while (begin < end){//end先往左走,找小while (a[end] > a[keyi] && begin < end){end--;}//begin往右走,找大while (a[begin] < a[keyi] && begin < end){begin++;}//begin和end在一个位置时交换没必要if (begin != end){//交换end和begin处的数据Swap(&a[begin], &a[end]);}}//此时begin和end相遇,交换其和keyi的值Swap(&a[begin], &a[keyi]);keyi = begin;//[left,keyi-1] keyi [keyi+1,right]QuickSort(a, left, keyi - 1);QuickSort(a, keyi + 1, right);
}
这就是hoare快排的基本形式,但是该代码有一定的缺陷。可能会造成栈溢出的风险。我们分析排序过程之后可以发现其递归图就好像二叉树的样子,每一个区间就类似于二叉树的节点,其高度也就是logN,但是当待排序的序列是有序的时候,即最小的数据一直在左边的话,那么就会出现下面这种情况:
每次都将最左边的排好,将区间分为左边和右边,每次相当于左区间为空,右区间比原区间少一个数,所以现在的高度就成了N^2这个量级。 这样递归的深度就增加了,可能会造成栈溢出的封信。
那么有什么办法可以避免该问题么?
采用三数取中法
我们可以在进行排序之前先取出区间左右端点的值和区间中点的值,然后取三个数中中间的数作为key,这样就可以避免key是序列中最小值的情况了。
//三数取中法
int GetMidData(int* a,int left,int right)
{int begin = left;int end = right;int mid = (left + right) / 2;if (a[begin] > a[mid]){if (a[mid] > a[end]){//begin > mid > endreturn mid;}else{//mid是最小的,返回end和begin中较小的那个return a[begin] > a[end] ? end : begin;}}else//begin < mid{if (a[mid] < a[end]){//begin < mid < endreturn mid;}else{//mid是最大的,返回begin和end中较大的那个return a[begin] > a[end] ? begin : end;}}
}
我们将第二大的数的下标返回,然后在快排函数中接收该下标,与区间左端点值进行交换,使左端点left对应的值为第二大的值。
那么快排的时间复杂度是多少呢?
最坏的情况就是数据已经有序的情况下,递归深度增加,其时间复杂度为O(N^2)。但是我们采用了三数取中的方式对快排进行了优化,就不可能出现递归太深的情况,往往都是哪种类似二叉树的情况,它的高度为logN,每一层都要遍历区间,所以是N,所以它的时间复杂度为O(N*logN)。
到了这里,我们还可以对快排在进行一次优化——小区间优化。
我们刚才介绍,快排展开就像是一棵二叉树一样,而对于二叉树来说,其最后一层上的节点数就占了总结点数的一半。那对于快排来说,最后一层上的区间数就是总区间数的一半,而每一个区间都要进行递归,所以快排最后一层的递归次数占了整个排序过程中递归次数的一半。
而对于后面几层的区间来说,这些已经是被分的很小的区间了。所以我们这里提出一个小区间优化,对区间元素个数小于10个的,直接对其使用插入排序,这样就可以减少快排的大部分递归次数。
hoare快排最后一个问题就是,当L和R相遇之后,key与其交换,相遇处的值是否一定小于key值?答案是:是
我们在这里先给出结论:区间左端点为key,R起始为右端点,L为左端点,R先走找小,L后走找大,当其相遇的时候,相遇位置的值一定小于key位置的值。
在走的过程中,结束的情况有两种:
- L遇见R:L遇见R说明R已经停下来了,此时该L走了,那么它们相遇的位置就是R停下的位置,R是找小,说明其遇到了比key小的数才停了下来。此时相遇值小于key。
- R遇见L:R遇见L说明已经完成了一次交换,该R先走,走到了L之前停的位置,又因为上一次L已经和R发生了交换,此时L位置上的数一定小于key,所以该相遇处的值还是小于key。
当然,key也可以取右端点,那就是L先走,R再走。对于上面的情况,如果想要排降序,那就R找大,L找下。
6.2挖坑法
hoare快排是最先提出来的快排方式,但后来还出现来好几种实现快排的方式,但其本质上都是一样。这里我们了解一下挖坑法实现快排。
这里依旧选区间左端点为key,我们记录其值,然后该位置的数就可以被覆盖,把这记作一个坑。然后R从右端点开始找小,找到之后将数据填入坑中,然后在该位置形成一个新的坑。然后L开始找大,找到之后填入坑中,再形成新的坑。当L与R相遇时,将key的值,填入坑中,就完成了一次单趟快排。然后跟hoare一样,再进行递归实现其他区间排序。
下面给出单趟排序的过程图:
先记录区间左端点的值,然后在该位置形成坑
R开始找小,找到之后将值填入坑中,然后形成新的坑
L开始找大,找到之后填入坑中,形成新坑
然后重复上面的过程,直到L和R相遇
R走
L走
R走
L走 ,但是L走的过程中与R相遇,此时将key填入坑中。这样,6就已经有序了,然后我们在对6左右两个区间分别采用递归的方式使序列有序。
//挖坑法
int DigHoleSort(int* a, int left, int right)
{//三数取中法int mid = GetMidData(a, left, right);//将中间数的值与left的值进行交换,left的值就一定不是最小的了Swap(&a[mid], &a[left]);int begin = left;int end = right;int key = a[left];//关键字,R和L比较的标准int hole = left;//在挖坑法中hole就是坑位的下标while (begin < end){while (a[end] > key && begin < end){end--;}//找到小于key的数之后填入坑中a[hole] = a[end];//更新坑位hole = end;while (a[begin] < key && begin < end){begin++;}//找到大于key的数之后填入坑中a[hole] = a[begin];//更新坑位hole = begin;}//相遇后,将key填入坑中a[hole] = key;//hole下标对应的数据就已经有序了return hole;//返回的是分左右区间的依据
}
我们对快排进行了拆分,将单趟的排序分割出来,使快排函数内部显得更为简单一点。我们只需要调用单趟排序即可。下面是快排的逻辑。
//快排
void QuickSort(int* a, int left, int right)//left为起始下标,right为终止下标
{//区间只有一个数或者没有数时不需要排序if (left >= right){return;}//小区间优化if ((right - left + 1) < 10){InsertSort(a+left, right - left + 1);}else {//int keyi = hoareSort(a, left, right);//hoare快排int keyi = DigHoleSort(a, left, right);//挖坑法//一个单趟走完,此时keyi处的数据已经有序,将还需排序的数据分成了两个区间,再对下面区间数据采用相同逻辑进行排序//[left,keyi-1] keyi [keyi+1,right]QuickSort(a, left, keyi - 1);QuickSort(a, keyi + 1, right);}
}
我们根据上面两端代码的逻辑可以清楚的理解挖坑法为什么要将最后一个坑位返回给快排函数,因为单趟之后最后一个坑的数已经有序了,所以要借助来分割区间。
6.3前后指针法
前后指针法也是一种递归方式的快排,与hoare和挖坑有些许不同。
该方法就是key指向左端点值,prev也指向左端点,cur指向prev的下一个位置,然后cur开始向后遍历,遇到比key小的数后,prev往前走一步,然后交换cur和prev,然后cur继续走,当cur指向序列之外时,此时prev指向的数据就已经有序了,返回该值作为区分区间的依据。然后在进行递归排序。
下面给出一个单趟交换的过程:
//前后指针法
int PrevCurSort(int* a, int left, int right)
{//三数取中法int mid = GetMidData(a, left, right);//将中间数的值与left的值进行交换,left的值就一定不是最小的了Swap(&a[mid], &a[left]);int prev = left;int cur = prev + 1;int keyi = left;while (cur <= right){ //cur小于key时,prev向后走一步,然后交换if (a[cur] < a[keyi] && ++prev != cur){Swap(&a[cur], &a[prev]);}cur++;}Swap(&a[prev], &a[keyi]);keyi = prev;return keyi;
}
6.4非递归实现快排
6.4.1栈实现快排
我们在上面介绍了三种借助递归实现快排的方法。我们在这里了解一下如何用非递归实现快排。
我们仔细观察上面递归实现快排的过程中,每一次不同的地方在于函数的区间不同,所以如果想用非递归实现快排,我们只需要将每一个需要排序的区间记录下来。然后根据区间,调用单趟快排即可。
在这里我们可以借助我们先前了解过的数据结构栈来实现。不了解栈的可以看这篇文章重生之我在学数据结构——栈-CSDN博客。
栈遵循的是后进先出的逻辑,所以如果我们想要实现跟上面递归一样的逻辑时,我们可以先储存右区间,在储存左区间。在储存区间时,先插入区间右端点,再插入区间左端点。当我们要排序的时候先取出区间,然后进行单趟排序,然后将其产生的左右区间再次插入到栈中。当栈为空时,此时序列已经有序。
在这里我们采用上面的哪一个单趟都是可以的。
这就是借助栈来实现快排的思路。接下来我们实现代码:
//非递归快排
void QuickSortNonR(int* a, int left, int right)
{//创建栈Stack s = { 0 };StackInit(&s);//先将区间端点压入栈中//先压右端点后压左端点StackPush(&s, right);StackPush(&s, left);while (!StackEmpty(&s)){int begin = GetStackTop(&s);StackPop(&s);int end = GetStackTop(&s);StackPop(&s);int keyi = PrevCurSort(a, begin, end);//右区间if (end > keyi + 1){StackPush(&s, end);StackPush(&s, keyi + 1);}//左区间if (begin < keyi - 1){StackPush(&s, keyi - 1);StackPush(&s, begin);}}StackDestory(&s);
}
我们再将区间压入栈时要进行判断,如果区间只有一个数那就没有必要入栈去走单趟了。区间不存在时也不能入栈。
6.4.2队列实现快排
我们也可以借助队列来实现快排。基本思路是相同的储存区间即可。但是因为队列遵循的是先进先出,所以在存储区间的时候有所不同。我们要先入左区间再入右区间;入区间端点时,先入左区间,再入右区间。
//借助队列实现非递归快排
void QuickSortNonRByQueue(int* a, int left, int right)
{//创建队列Queue q = { 0 };QueueInit(&q);QueuePush(&q, left);QueuePush(&q, right);while (!QueueEmpty(&q)){//获取区间左右端点int begin = GetQueueFront(&q);QueuePop(&q);int end = GetQueueFront(&q);QueuePop(&q);int keyi = PrevCurSort(a, begin, end);//先入左区间if (begin < keyi - 1){//先入左端点QueuePush(&q, begin);QueuePush(&q, keyi - 1);}if (end > keyi + 1){QueuePush(&q, keyi + 1);QueuePush(&q, end);}}QueueDestroy(&q);
}
七.归并排序
7.1递归实现归并
归并排序也是一种常用的排序方法,其基本逻辑是:将一段区间分为左右区间,如果左右区间有序,那就进行归并。归并的操作就是同时遍历左右区间进行比较,将小的数拷贝到一个新数组中,然后拷贝完后在将数据拷贝到原数组,这样就实现了排序的功能。
所以我们的问题就转变成了使区间分成的左右区间有序,那么如何使分成的左右区间有序呢?
我们可以多次进行分割,当区间只有一个数据时或者区间不存在时,那就说明此时这个区间就已经有序了,然后进行归并操作,这时候两个数据就有序了,然后继续归并,直到将所有数据归并完。我们可以看到这里面用到了递归地思想。
我们给出下面一组数据进行归并排序的过程分析:
假设下面就是待排序的数据
为了使这组数据有序,那就得将他分成左右两个区间,让左右区间分别有序然后进行归并操作
但是这两个区间并不有序,那我们就得继续分割,直到其区间中的数据有序
我们将其分为一个区间只有一个数据时,此时每一个区间就都是有序的了,然后我们在进行归并操作。使左右区间的上一级区间有序。
然后再继续归并,使其上一层区间有序
在执行同样的归并操作
当其没有上一层区间时,就说明数据已经有序了。
void _MergeSort(int* a, int* tmp, int left, int right)
{if (left >= right){return;}//分割区间//[left,mid][mid+1,right]int mid = (left + right) / 2;_MergeSort(a, tmp, left, mid);_MergeSort(a, tmp, mid + 1, right);//归并int i = left;//左区间int begin1 = left;int end1 = mid;//右区间int begin2 = mid + 1;int end2 = right;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 + left, tmp + left, sizeof(int) * (end2 - left + 1));
}//归并排序——递归
void MergeSort(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){perror("MergeSort()::malloc()");return;}_MergeSort(a,tmp,0,n-1);free(tmp);tmp = NULL;
}
那么归并排序的时间复杂度是多少呢?
归并排序最后会将数据分割成一个一个的区间,其分割的过程是一种折半分割,类似于二叉树,所以其高度为logN,然后其每一层都要进行归并,就要对每一个数据进行比较,访问到每一个数据,其消耗是N,所以其时间复杂度为:O(N*logN)
7.2非递归实现归并
我们也可以使用非递归来实现归并排序。但是我们不再借助栈来实现,我们直接通过循环来控制每一次归并的区间,使其每进行一次归并就使区间有序。
我们同样对相同的数据进行操作,下面进行过程的分析:
我们不再像上面那种分割区间,将区间分成只有一个数据时,然后在进行归并。我们这里直接进行归并,但是归并的要求是左右区间是有序的,而在当前情况下,只有一个数据是一个区间时才是有序的,所以我们可以进行11归并,即每组只有一个值,相邻两组进行归并。
我们看到此时就产生了有序的区间了,而区间的值也有1变为2,所以我们这里来在进行22归并,使相邻的区间合并成一个有序区间
此时上面相邻的左右区间就合并成了一个新的有序,然后又作为左区间和右区间进行44归并
当只剩一个区间时,就说明已经排序完成了。 我们看到我们再归并的过程中,控制了每一次归并数据的数据个数,由一个变成2个、4个,这就像是我们递归的逆过程。我们用一个gap变量来表示当前归并的每组数据的数据个数。
//归并排序——非递归
void MergeSortNonR(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){perror("MergeSort()::malloc()");return;}int gap = 1;while (gap < n){int i = 0;for (i = 0; i < n; i += 2*gap){int begin1 = i;int end1 = i + gap - 1;int begin2 = i + gap;int end2 = i + 2 * gap - 1;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 + i, tmp + i, sizeof(int) * (end2 - i));}gap *= 2;}free(tmp);tmp = NULL;
}
代码写完了,但是我们写的是有问题的,我们的i是每次归并的开始位置,只有每一次归并结束后才改变,但是我们再归并操作中用它做了,导致它进行了自增,对后面其他组的递归造成影响。
所以我们利用j来作为下标使用,防止意外改变了i。
还有一个问题就是拷贝数据的时候,我们拷贝的数据个数是右区间的右端点-左区间的左端再加上1,因为下标是个数-1,所以求个数时要加上1.
修改完成后,我们看结果:
但是当我们的数据不是2的倍数时就会产生错误,会发生越界访问。因为我们的下标都是按照2的倍数计算的,当数据不是时,就会导致下标大于最后一个数据的下标,导致越界访问。
我们来打印一下每一次归并的区间,来判断那个端点越界了:
我们只有10个数据,当下标超过9时,就说明越界了。而根据我们分出的区间我们可以判断出是谁越界了。
这样分类是因为,当end1和begin2都越界时,它们就不再需要进行归并了,说明此时只有一组数据,两组才能归并,所以我们将这两种归为一类。
第二类就是end2越界,说明此时begin2没有越界,是有两组数据的,所以还需要进行归并,此时我们就要对end2进行修改。
再进行测试:
我们已经解决了非递归中出现的问题,代码已经完全:
//归并排序——非递归
void MergeSortNonR(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){perror("MergeSort()::malloc()");return;}int gap = 1;while (gap < n){int i = 0;for (i = 0; i < n; i += 2*gap){int begin1 = i;int end1 = i + gap - 1;int begin2 = i + gap;int end2 = i + 2 * gap - 1;//printf("[%d,%d] [%d,%d]", begin1, end1, begin2, end2);//begin2或者end1越界,都不需要归并,直接跳出循环if (begin2 >= n){break;}//只有end2越界,修改end2的下标,使其指向数据的最后一个元素的下标if (end2 >= n){end2 = n - 1;}int j = begin1;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]){tmp[j++] = a[begin1++];}else{tmp[j++] = a[begin2++];}}//一个区间结束放完了,还有一个区间没完while (begin1 <= end1){tmp[j++] = a[begin1++];}while (begin2 <= end2){tmp[j++] = a[begin2++];}//将数据复制到原数组中memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));}gap *= 2;}free(tmp);tmp = NULL;
}
八.计数排序
计数排序是一种不需要进行比较就可以实现排序的一种的算法,有点像哈希表。其具体操作步骤为:1.统计相同数据出现的个数;2.根据个数将对应数据放到原数组中。
下面借助一组数据来分析计数排序的执行过程:
假设有一组数据都属于0~10这个范围,我们来对其进行排序
第一步就是统计数据中每一种元素出现的数据个数,记录到一个count数组中 ,count数组中每一个数据都初始化为0
我们进行统计之后count数组就发生了变化:
然后我们根据count数组中每一个下标对应元素的个数,开始打印下标。
0下标对应的元素是0,那就不打印0,1下标对应的元素是3,那就打印3次1,一次类推,在遍历count数组时,将数据输出到原数组,此时数据就变得有序了。
这到底是怎么实现的呢?
其实我们是将待排序的数作为了count数组的下标了,而count数组的下标本来就是有序的。所以我们只需要知道每一个下标出现了几次,然后拷贝到原数组就行了。
那该方法遇到数据不是0~10的怎么办呢?比如数据是100~109呢?
其实我们还是可以做到一一对应的关系。我们计算出数据所在的区间大小range,来判断我们需要开多大的空间,然后我们将原数组中每一个数据减去最小值再作为count数组的下标来进行统计。
假如我们的数据是上面的数据,难道我们要开一个空间为110的count数组来存储嘛?
如果这样的话,我们前面的空间都被浪费掉了,因为那里并没有值,所以肯定不可以这样来申请空间。
我们用每一个值减去最小值来看一下:
当前去最小值之后,是不是就可以和下面的下标对应起来, 0就相当于是100,1相当于是101。这样就避免了多开空间,造成空间浪费的问题。而我们这个逻辑转化为代码就是:
我们接下来实现代码:
//计数排序
void CountSort(int* a, int n)
{//找出最小值和最大值,求出数据的范围int i = 0;int min = a[0];int max = a[0];for (i = 0; i < n; i++){if (min > a[i]){min = a[i];}if (max < a[i]){max = a[i];}}int range = max - min + 1;//创建count数组int* count = (int*)calloc(range, sizeof(int));if (count == NULL){perror("CountSort()::calloc()");return;}//统计for (i = 0; i < n; i++){count[a[i] - min]++;}//将数据拷贝回原数组i = 0;int j = 0;for (j=0; j<range; j++){while (count[j]--){a[i++] = j + min;}}
九.排序的稳定性
排序的稳定性就是说对两个相同的数据,进行排序之后,它们两个的相对位置不发生变化,就说明该排序是稳定的,否则就是不稳定的。我们对上面介绍过的排序进行稳定性分析。
冒泡排序是稳定的,冒泡排序是相邻之间进行交换,如果两个数据相等时就不再交换了,所以其不会改变两个相等元素的相对位置。
插入排序是稳定的,如果该元素小于前面的元素才会向前插入,如果与前面元素相等就会插入到该元素的后面,所以相对位置也不会改变。
希尔排序是不稳定的,因为希尔排序要进行多组的预排序,有可能相等的元素不在同一组,可能会造成数据位置的交换。
选择排序是不稳定的,选择排序会遍历数组选出最小的放到第一个位置,但是如果是下面这种情况就会交换数据位置:
虽然两个1的相对位置没变,但是6的相对位置变了
堆排序是不稳定的,如果数组建堆之后的跟和左右儿子位置的值相等,此时将根换到最后一个位置时,相对位置发生了改变。
快速排序是不稳定的,快速排序中会跳跃交换,这种就很有可能造成相对位置的变化:
归并排序是稳定的,如果归并时两组的数据相同,就会先归并第一个,在归并第二个,但是他们的相对位置却没有变化。
相关文章:
数据结构——八大排序
一.排序的概念和其应用 1.1排序的概念 排序:排列或排序是将一组数据按照一定的规则或顺序重新组织的过程,数据既可以被组织成递增顺序(升序),或者递减顺序(降序)。稳定性:假定在待…...
【Unity】RPG2D龙城纷争(十九)流程与UI界面(终章)
更新日期:2024年8月1日。 项目源码:第五章发布(正式开始游戏逻辑的章节) 索引 简介一、游戏流程1.初始化流程2.开始流程3.关卡流程4.关卡结束流程5.启用所有流程二、UI界面逻辑1.开始界面2.存档界面3.关卡界面DataRegion 数据显示逻辑区域RoundRegion 回合逻辑区域RoleMenu…...
C#类和结构体的区别
1、类class是引用类型,多个引用类型变量的值会互相影响。存储在堆(heap)上 2、结构体struct是值类型,多个值类型变量的值不会互相影响。存储在栈(stack)上 类结构关键字classstruct类型引用类型值类型存储…...
【RabbitMQ】RabbitMQ持久化
一、简介 RabbitMQ的持久化机制是一种确保数据在RabbitMQ服务重启或异常情况下不会丢失的重要特性。RabbitMQ的持久化主要包括三个方面的内容:交换器的持久化、队列的持久化、消息的持久化。 二、交换器的持久化 1、实现方式 在RabbitMQ中,实现交换器…...
算法刷题笔记 Kruskal算法求最小生成树(详细算法介绍,详细注释C++代码实现)
文章目录 题目描述基本思路实现代码 题目描述 给定一个n个点m条边的无向图,图中可能存在重边和自环,边权可能为负数。求最小生成树的树边权重之和,如果最小生成树不存在则输出impossible。 最小生成树的概念:给定一张边带权的无向…...
5年经验的软件测试人员,碰到这样的面试题居然会心虚......
我们这边最近的面试机会比较多,但是根据他们的反馈,结束后大部分都没音信了,因为现在企业面试问的非常多,范围非常广,而且开放性的问题很多,很多人即便面试前刷了成百上千道面试题,也很难碰到一…...
C#进阶-轻量级ORM框架Dapper的使用教程与原理详解
本文详细介绍了Dapper在C#中的使用方法,包括Dapper的基本概念、与其他持久层框架的比较、基本语法和高级语法的使用,并通过实例讲解了如何在项目中集成和使用Dapper。Dapper以其高效的性能和简洁的API受到开发者的青睐,适用于各种数据库操作需…...
Windows图形界面(GUI)-MFC-C/C++ - 编辑框(Edit Control) - CEdit
公开视频 -> 链接点击跳转公开课程博客首页 -> 链接点击跳转博客主页 目录 编辑框(Edit Control) - CEdit 基本概念 成员函数 示例代码 编辑框(Edit Control) - CEdit 基本概念 编辑框(Edit Control)是一个允许用户输入和编辑文本的窗…...
网络安全防御【IPsec VPN搭建】
目录 一、实验拓扑图 二、实验要求 三、实验思路 四、实验步骤: 修改双机热备的为主备模式: 2、配置交换机LSW6新增的配置: 3、防火墙(FW4)做相关的基础配置: 4、搭建IPsec VPN通道 (1…...
java环境配置与tomcat的配置
1、java环境配置 一、JDK下载 访问Oracle官网: 前往Oracle官网(Oracle | Cloud Applications and Cloud Platform),在首页的顶部菜单中选择“Resources” > “Downloads” > “Java” > “JDK”。注意:Orac…...
OD C卷 - 来自异国的客人/幸运数字
来自异国的客人/幸运数字(100) 输入描述: 输入k,n,m k表示物品价值(十进制) k>0 n表示幸运数字, n > 0 m表示异国采用的进制;m > 1 n < m 输出描述: 输出幸运数字的个数࿰…...
C++ | 动态内存管理 new、delete (用法、底层)详解
目录 简单回顾C语言动态内存管理 new、delete的用法 内置类型 new delete 自定义类型 new、delete底层讲解(重要) operator new 与 operator delete 定位 new 结语 简单回顾C语言动态内存管理 在C语言的学习阶段 我们接触到了三个能在堆上开辟…...
【C语言】结构体内存布局解析——字节对齐
🦄个人主页:小米里的大麦-CSDN博客 🎏所属专栏:https://blog.csdn.net/huangcancan666/category_12718530.html 🎁代码托管:黄灿灿 (huang-cancan-xbc) - Gitee.com ⚙️操作环境:Visual Studio 2022 目录 一、引言 二、什么是字节对齐&…...
模型表达方式
目录 一、模型表达概述 二、模型精确表达 2.1 几何表示 (Geometrical Representation) 三、模型非精确表达 3.1 网格表示 (Mesh Representation) 3.2 体素表示 (Voxel Representation) 一、模型表达概述 模型的表达方式多种多样,选择适合的表达方式取决于具体应用场景和…...
校园课程助手【4】-使用Elasticsearch实现课程检索
本节将介绍本项目的查询模块,使用Elasticsearch又不是查询接口,具体流程如图所示(如果不了解Elasticsearch可以使用sql语句进行查询): 这里是两种方法的异同点: Mysql:擅长事务类型操作&#…...
经典运维面试题
1、Linux常见的日志文件都有哪些,各自的用途?日志轮询配置文件在哪里?欢迎界面配置文件在哪里? /var/log/messages #内核及公共消息日志/var/log/cron #计划任务日志/var/log/dmesg #系统引导日志/var/log/malilog #邮件系…...
别再盲目推广了!Xinstall助你开启App线下推广新篇章
在这个数字化飞速发展的时代,App已经成为我们生活中不可或缺的一部分。然而,App市场的竞争也日益激烈,如何让你的App在众多竞争者中脱颖而出,成为每个推广者必须面对的问题。今天,就让我们一起探讨一下App线下推广的痛…...
大厂linux面试题攻略五之数据库管理
一、数据库管理-MySQL语句 0.MySQL基本语句: 1.SQL语句-增 创建xxx用户: mysql>create user xxx % indentified by 123456; xxx表示用户名 %b表示该用户用来连接数据库的方式(远程或本地连接) indentified by 123456设置密码…...
【pytorch】模型集成
在集成学习中,我们会训练多个模型(通常称为「弱学习器」)解决相同的问题,并将它们结合起来以获得更好的结果。最重要的假设是:当弱模型被正确组合时,我们可以得到更精确和/或更鲁棒的模型。 常用的模型集成…...
初识集合和数据结构
目录 初识集合框架数据结构基本概念和术语数据数据元素数据项数据对象前四者的关系数据结构逻辑结构和物理结构逻辑结构物理结构 算法算法设计要求 初识集合框架 Java的集合框架也可被称为容器,是定义在Java.util包下的一些接口和实现类。其就是将多个元素存储到一…...
cocos creator 3.x中动态加载 resources 文件夹下的图片时提示找不到
文件目录如下 类型为spriteFrame 代码案例 图片设置为 sprite-frame、texture 或其他图片类型后,将会在 资源管理器 中生成一个对应类型的资源。但如果直接加载 equipments/testea,得到的类型将会是 ImageAsset,必须指定路径到具体的子资源…...
第九十八周周报
学习时间: 2024.7.27-204.8.3 学习产出: 这周主要在按照审稿人的意见修改论文,由于有个模型保存的文件找不到了,所以重新训练花了点时间,目前已经把修改后的论文和cover letter发给杨老师了。...
程序员找工作之数据结构面试题总结分析
文章目录 1. 数据结构的基本概念与分类2. 数据结构的存储与表示3. 数据元素的存储与关系4. 存储结构的选择与考量5. 特定数据结构的定义与特性6. 数据结构操作与应用7. 数组与存储8. 特定数据结构的存储与访问 程序员在找工作面试中,数据结构方面可能会被问到的问题…...
设置provider解决maven找不到JUnit 5测试样例
问题描述 尝试复现一个用大模型生成测试样例的工作,但使用maven生成的JUnit 5测试样例死活不执行。又不想用命令行运行,因此进行排查 基本知识 <dependencies> junit-jupiter-api JUnit 5写代码时调用的库 junit-jupyter-engine 运行JUnit 5测…...
php反序列化靶机serial实战
扫描ip,找到靶机ip后进入 他说这是cookie的测试网页,我们抓个包,得到cookie值 base64解码 扫描一下靶机ip的目录 发现http://192.168.88.153/backup/,访问 下载一下发现是他的网页源码 通过代码审计,发现 通过代码审计得知&…...
类型推断技术及仓颉语言实践
史磊 仓颉语言类型推断技术专家 一、一种看待类型系统的方式 一门编程语言一定得包含类型系统吗? 这个问题今天看来可能显而易见,一个程序没有类型的话还能算是个完整、正确的程序吗?但是其实关于类型系统的作用,一直是存在两种…...
职场生存秘籍:16条黄金法则
作者简介:一名计算机萌新、前来进行学习VUE,让我们一起进步吧。 座右铭:低头赶路,敬事如仪 个人主页:我叫于豆豆吖的主页 写在前面 在这个瞬息万变的时代,职场不仅是实现个人价值与梦想的舞台,更是一…...
Flask 介绍
Flask 介绍 为什么要学 Flask框架对比设计哲学功能特点适用场景学习曲线总结 Flask 的特点Flask 常用扩展包Flask 的基本组件Flask 的应用场景官方文档官方文档链接文档内容概述学习建议 Flask 是一个使用 Python 编写的轻量级 Web 应用框架。它旨在让 Web 开发变得快速、简单且…...
JAVA基础知识点3 (String 和 StringBuffer 以及 StringBuilder 的特点以及区别)
1,String 和 StringBuffer 以及 StringBuilder 的特点 (1)String的特点:String是final修饰的字符序列是不可改变的, 是字符串常量,一旦初始化就不可以被更改,因此是线程安全的 因为是常量每次对其操作都会…...
2024年8月AI内容生成技术的现状与未来:从文生文到跨模态交互的全景分析
2024年8月AI内容生成技术的现状与未来:从文生文到跨模态交互的全景分析 大家好,我是猫头虎!🚀 随着AI在内容生成领域的爆发式发展,从2022年末开始,AI生成技术已经走过了文生文(AIGC)…...
pw网站更换域名/seo对网店推广的作用有哪些
四、队列的使用(基于内存 和 基于数据库) 2018年10月23日 10:34:13 齐大圣2012 阅读数:169 转载自:https://blog.csdn.net/yang5726685/article/details/54234569 今天跟大家来看看如何在项目中使用队列。首先我们要知道使用队…...
男人和女人做性网站/网络推广渠道
今天学习了下文件格式处理一般常用的三个命令。sed处理行,ack处理行内的段,printf格式化打印。1.printf:格式化打印printf和C语言里面的printf差不多。格式:printf 打印格式 打印内容参数:\f:清楚屏幕\n&am…...
做网站上面图片的软件/如何推广自己产品
2019独角兽企业重金招聘Python工程师标准>>> 现代web开发是如此复杂。完成同一个任务有许多方式,至于使用哪种方式完全取决于开发人员自己,但他们却容易犯错。各种开发平台、开发模式和开发实践以及易出现的问题有时已经超越了web开发人员的能…...
花瓣网素材/站长之家seo查询官方网站
传送门 可以离线,把询问拆成四个,然后把所有的按\(x\)坐标排序,这样就只要考虑\(y\)坐标了。然后把\(y\)坐标离散化,用树状数组统计即可 记得开longlong //minamoto #include<bits/stdc.h> #define R register #define int …...
免费网站建设怎样/旺道seo优化
/etc/sysconfig/network 包括主机基本网络信息,用于系统启动 /etc/sysconfig/network-script/ 此目录下是系统启动最初始化网络的信息 /etc/sysconfig/network-script/ifcfg-eth0 网络配置信息 /etc/xinetd.conf 定义了由超级进程XINETD启动的网络服务 /etc/protoco…...
网站制作 技术/搜索引擎的优化方法
添加方法:菜单component->Install Packets按Add按钮,选择delphi目录里的bin目录下的dclsockets70.bpl(delphi2010是dclsockets140.bpl),然后TClientSocket和TServerSocket控件就会出现在Internet页上了。转载于:https://www.cnblogs.com/d…...