前言:
  正如标题所言,本篇博客是数据结构初阶的最终章节.但不是数据结构的最终章节!事实上,诸如AVL 树,红黑树这样高阶复杂的数据结构使用C语言非常麻烦,这些数据结构我会放在后续的C++的博客中去讲解!今天我们讲解的是八大经典的排序算法。因为排序真的是太太太重要了!!!不仅是是在生活中我们经常需要排序,更因为排序更是面试中的必考题!!!,所以接下来请跟进我的脚步,我来带你走进面试常问的八大排序算法。
本文重点

1.冒泡排序
2.直接插入排序
3.选择排序
4.希尔排序
5.堆排序
6.快速排序(多种方法实现)
7.归并排序
8.计数排序

我们接下来的所有排序都是默认升序。
1.冒泡排序
  作为我们大学学习C语言的第一个接触的排序,这个排序的思想非常简单:两两比较,如果前面大则交换,就这样大的数最终就会到后面,小的数就会到前面,如同水里的气泡一样,因此得名冒泡排序,接下来我们来看一看一趟冒泡排序的动图演示:


接下来,我们写出代码。

void Swap(int* pa, int* pb)
{int tmp = *pa;*pa = *pb;*pb = tmp;
}
//冒泡排序
void BubbleSort(int* a, int n)
{  //i是冒泡的趟数,确定的也是冒泡最后落到的数字区间for (int i = 0; i < n; ++i){     //标杆变量int exchange = 0;for (int j = 1; j <n-i ; ++j){if (a[j - 1] > a[j]){Swap(&a[j - 1], &a[j]);exchange = 1;}}if (exchange == 0)break;}
}

这里我们做了一点小优化,我们设置了一个标杆变量exchange,当发生交换的时候,exchange置成1,而如果在交换的过程中没有发生交换,那么就说明已经是有序了,不用再进行比较,就可以break了。

最好的时间复杂度:O(N):当数据有序的时候,冒泡排序的时间复杂度最低
最坏的时间复杂度:O(N^2):当数据是逆序的时候,总的需要比较交换的次数的和是一个等差数列的求和.

所以当数据量非常大,并且是逆序的时候,使用冒泡排序就会非常慢!但是冒泡排序也有优点,那就是思想易于理解。适合初学者学习。
2.插入排序
  插入排序的思想也比较好理解,举个例子:大家应该都玩过斗地主把,斗地主抽完牌以后,你整理扑克的过程就是插入排序的思想。

[0,end]是一个有序的区间,那么这时候我们拿了一个数x,我们需要把x插入这个区间使得这个区间仍然有序,那么我们就可以这么做
1.如果当前的数大于end,那么把元素后挪。
2.找到了大于当前元素的元素,那么就把这个元素插入

具体我们可以通过这样的一个动图来体会:

具体的每一步就是找到合适位置插入有序区间,使得新区间仍然有序!所以我们写出如下的代码:

//插入排序
void InsertSort(int* a, int n)
{for (int i = 0; i < n - 1; ++i){int end=i;int x = a[end + 1];while (end >= 0){if (a[end] >x){a[end + 1] = a[end];--end;}else{break;}}a[end + 1] = x;}
}

注意这里最后一个数字落到的是n-1!,这里注意的是,无论是找到了合适的位置还是end走到了–1,我们都要放入数字。所以找到合适位置我们便可以结束循环提前插入,做到代码不冗余。接下来,我们来分析一下插入排序的时间复杂度:

1.最好时间复杂度:O(N):同样是序列有序的情况下,直接插入排序的时间复杂度是最优秀的。
2.最坏的时间复杂度:O(N^2):在面对同样是逆序的情况下,直接插入排序的时间复杂度也是一个等差数列的求和,是O(N*N)

  那么这时候你可能就会疑问?插入排序和冒泡排序的最好和最坏时间复杂度都是相同的,是不是意味着这两个排序是差不多的呢?答案是否定的!接下来我们给定一组测试用例来分析,看看再数据接近有序或部分有序的情况下,插入排序会更加优秀!从思想上来看,冒泡只要前大于后就要进行交换,但是插入排序在序列接近有序的时候只要插入数据就可以了!如果能够让序列接近有序,那么插入排序将会非常高效!
3.选择排序
  选择 排序的思想也是相对来说比较好理解的,每一去遍历选出最小的数放到数组的最开头,然后接下来把这个数字剔除,从剩下的数据里面 继续重复先前的动作即可。那么接下来我们做一点小的优化,我们一次遍历选出最小和最大,然后把最小交换到左边,最大交换到右边,接下来剔除这两个数缩减区间继续重复相同的步骤。

//选择排序
void SelectSort(int* a, int n)
{int left = 0, right = n - 1;while (left < right){ //mini和maxi分别表示最小、最大数的下标int mini = left, maxi = left;for (int i = left + 1; i <= right;++i){if (a[i] < a[mini]){mini = i;}if (a[i] > a[maxi]){maxi = i;}}Swap(&a[left], &a[mini]);Swap(&a[right], &a[maxi]);++left;--right;}
}

但是实际上这段代码是有问题的,给定如下的测试用例:

int a[]={6,5,4,3,2,1};

我们按照代码逻辑执行完第一遍选数以后结果如下:

接下来我们把mini和left位置的数交换,同时再把right和maxi位置的数交换,发现6的位置又会被放到left的位置,就没有做到选出最大的数放到右边的效果!也就是如果遇到left和maxi重叠的情况代码没办法很好的处理!。解决 的方案是当left和maxi重叠,就修正maxi为mini即可!

//选择排序
void SelectSort(int* a, int n)
{int left = 0, right = n - 1;while (left < right){ //mini和maxi分别表示最小、最大数的下标int mini = left, maxi = left;for (int i = left + 1; i <= right;++i){if (a[i] < a[mini]){mini = i;}if (a[i] > a[maxi]){maxi = i;}}Swap(&a[left], &a[mini]);//如果maxi和left重叠,修正maxi即可if (left == maxi){maxi = mini;}Swap(&a[right], &a[maxi]);++left;--right;}
}

接下来我们从选择排序的思想上来分析它的时间复杂度:

时间复杂度:O(N^2):由于无论序列是否有序,选择排序每一都要遍历选数,也正因为这样,对于选择排序而言没有什么最优的情况。所以读者可以认为选择排序是相对而言效率非常低的排序(冒泡:我也有翻身的一天!)

讲完了冒泡,直接插入和选择排序,我们不难可以看出:在序列接近有序的情况下,插入排序算法的效率最高而当序列完全有序的情况下,冒泡排序的效率也相当不错,而对于选择排序几乎都是O(N*N)!
那么聪明的你可能就会想,假设给我一个随机序列,如果使用直接插入排序效率会非常低,而如果我先把这个序列进行整理,让整个序列接近有序那么再使用插入排序就更高效!那么希尔排序就是基于这个思想的排序,下面我们正式来介绍希尔排序
4.希尔排序:
  听名字就知道这个算法最早是一个叫做希尔的人提出的(膜拜大佬),希尔排序的核心思想就是:就是选定一个间距gap,以gap为间距对序列进行分组,对每一组序列进行预排序,使得每组序列接近有序,最后当gap==1的时候就相当于是对一个接近有序的序列进行直接插入排序,接下来我们通过图片来看一看希尔排序的整个过程:

那么从这张图片我们不难得出,当gap越大,数据越不接近有序,但是大的数字越快能够到达后面。而当gap越小,gap=1的时候就是直接插入排序!
所以我们可以写出如下单趟希尔排序的代码:

for (int i = 0; i < n - gap; ++i){int end = i;//预排序,保存a[end+gap]位置的值int tmp = a[end + gap];while (end >= 0){if (a[end] > tmp){a[end + gap] = a[end];end -= gap;}else{break;}}a[end + gap] = tmp;}

那么这里最关键的就是这个间距怎么给比较合适,给固定值肯定是不行的!那么根据具体的问题的规模给定即可,一般没有特别的规定,大致有如下两种给法:

方法1:gap=gap/3+1;//官方的给法,注意最后一次一定要取到1才可以进行插入排序。
方法2:gap=gap/2;//这种给定的方法是一定能够取到1的

所以最终的实现代码如下:

//希尔排序
//进行预排序,使数据接近有序,最后使用插入排序
void ShellSort(int* a, int n)
{int gap=n;while (gap > 1){   //一定要保证最后一次gap取到1,进行插入排序!//不是一次性预排序,而是一次预排一组一部分,下一次预排另一组一部分,多组预排for (int i = 0; i < n - gap; ++i){int end = i;//预排序,保存a[end+gap]位置的值int tmp = a[end + gap];while (end >= 0){if (a[end] > tmp){a[end + gap] = a[end];end -= gap;}else{break;}}a[end + gap] = tmp;}}
}

预排序的时间复杂度是O(logN),而接近有序序列后最后一次插入排序的时间复杂度是O(N),那么希尔排序的时间复杂度就是O(N*log(N)),也有的地方计算出希尔排序的平均时间复杂度是O(N^1.3),至于为什么是这个结果,我也不知道,大家记个结论就可以了

3.堆排序
前面我们讲了堆这个数据结构,实际上也有一个排序基于这个数据结构,那就是堆排序,我们可以建立一个小堆,然后每次取堆顶的数据,然后删除堆顶元素,循环重复一直到堆为空,这样数据就被排序了!所以这个思路的代码如下:

//建立小堆取堆顶元素建堆
void HeapSort(HPDataType* a, size_t size)
{ Heap hp;HeapInit(&hp);//把数据整理成堆for (int i = 0; i < size; ++i){HeapPush(&hp, a[i]);}int index = 0;//取出堆顶元素返回原数组while (!HeapEmpty(&hp)){printf("%d ", HeapTop(&hp));HeapPop(&hp);}HeapDestroy(&hp);
}

这是我们利用堆这个数据结构进行堆排序,然而这么做开销太大,为了排序我们特意去实现这么一个数据结构,在先前的堆的博客中我们也提到了直接利用数组进行建堆的方法(不了解的可以点击这个传送门:二叉树顺序存储之堆结构)那么问题来了,如果直接在原来的数组里建堆,我们要还是建立小堆吗?
答案是否定的!在原数组建堆排升序我们恰恰要建立大堆!建立小堆虽然确定了最小的,但是取走堆顶以后,所有的节点的逻辑关系全都乱了,还要重新建堆!而建堆的时间复杂度是O(N)(稍后证明)!所以我们要建大堆来排升序。


//向下调整算法
void AdjustDown(int* a, size_t n, size_t root)
{   size_t parent = root;size_t child = 2 * parent + 1;while (child < n){   //去左右孩子大的那一个if (child + 1 < n && a[child + 1] > a[child]){++child;}//调整if (a[child] > a[parent]){Swap(&a[parent], &a[child]);parent = child;child = 2 * parent + 1;}else{break;}}
}//堆排序
void HeapSort(int* a, int n)
{  //从最后一个非叶子节点开始向下调整for (int i = (n - 1 - 1) / 2; i >= 0; --i){AdjustDown(a, n, i);}//头尾交换,不把这个节点看作堆里的节点进行调整size_t end = n - 1;while (end > 0){Swap(&a[0], &a[end]);AdjustDown(a, end, 0);--end;}
}

接下来我们来分析一下,堆排序的算法时间复杂度。显然,堆排序的时间复杂度是由向上调整算法或者向下调整算法决定的,那么我们来看一看这两个算法具体的时间复杂度是如何计算的。

由于时间复杂度是一个悲观的指标量,所以这里我们就以满二叉树的情况来分析这两个算法的时间复杂度:

向上调整算法:

所以向上调整算法的时间复杂的是O(nlogn)接下来我们对向下调整算法进行分析:

  从这里不难可以看出,向下调整算法的时间复杂度优于向上调整算法,也就是可以这样认为:建堆的时间复杂的是O(N),而每次选完数以后的调整的次数是O(logN),所以堆排序的时间复杂度是O(NlogN)
最会整花活的快速排序
  正如标题所言,快速排序可以说是到目前为止最能整事的排序了。不仅因为它很快,而且快速排序的玩法非常多,hoare法,挖坑法,前后指针法,并且这些方法里面的细节非常多!另外,快排也是利用了我们先前二叉树里面的的分治思想,也就意味着快排需要和递归挂钩。但是由于递归存在溢出的风险,所以在特定的情境下我们还要把快排实现成非递归的版本!另外还要对快排进行一些小的优化等等!所以说快排是一个很能“整事”

快排的递归版本:
快速排序的思想是基于二叉树而来,每次都选出一个数字做关键字(key),那么将小于key的数字都划分在key的左边,把大于key的数字都划分到key的右边,最后key的位置就不变了。接着左半区间有序,右半区间有序,整个序列就整体有序了!

那么我们接下来介绍3种单趟排序的方法:

法一:hoare法:使用两个指针left,right,key=a[left],右指针先出发,向左寻找比key小的值,如果找到就停下,换。左指针开始向前寻找,同时左指针出发找到大于key的就和右指针交换,一直重复知道二者相遇!

注意这里的key我采取的是关键字的下标,在最后left,right相遇的时候我们交换key和left的下标即可
通过一张动图来体会hoare单趟快排的过程:

接下来我们写出单趟的排序:

//版本一:hoare
//单趟排序
int PartionByleft(int* a, int left, int right)
{int keyi = left;//选左边做为key,那么右边先走才能相遇!while (left < right){  //第一个防止找不到而越界,第二个则是防止当遇到值和key相同的数死循环!while (left < right && a[right] >= a[keyi]){--right;}while (left < right && a[left] <= a[keyi]){++left;}Swap(&a[left], &a[right]);}Swap(&a[left], &a[keyi]);return left;
}

经过这么一趟排序,左边的数据都比key小,右边的数据都比key大,那么key最终确定就在这个位置。接下来只要key的左半区间有序,右半区间有序,那么这个序列就整体有序了。如何做才能让左右区间有序呢?->分治递归左半区间,递归右半区间即可。

//快速排序
void QuickSort(int* a, int begin, int end)
{   if (begin >= end)return;//hoareint keyi = PartionByleft(a, begin, end);QuickSort(a, begin, keyi - 1);QuickSort(a, keyi+1, end);
}

如果不能很好理解这个分治递归的童鞋,可以自己动手尝试画出递归展开图。
到了这里你可能还是不太理解hoare法为什么就能把一个数排到最终确定得位置,那么接下来我就再给你介绍另一个方法------>挖坑法

法二:挖坑法:
这个方法是基于hoare法做了一个改进,定义了一个坑(pivot),假如我们选左边做key,那么最开始的pivot就是key的下标,接下来右边开始出发找小于key的元素,找到以后把这个元素的值赋给当前pivot指向的元素,然后它成为新的pivot。这时候再左边开始寻找大于key的元素,找到后把值给给pivot指向的元素,然后变成新的坑。重复知道left和right相遇(一定相遇在pivot),最后把key放入就可以了。

同样通过一张动图来体会这个单趟排序的过程:

那么接下来我们就可以动手写出挖坑法的单趟排序:

int PartionByPiv(int* a, int left, int right)
{    //定义坑位int pivot = left;int key = a[left];while (left < right){  //左边做key,右边先走while (left < right && a[right] >= key)--right;//找到小的数放置,挖新坑a[pivot] = a[right];pivot = right;//左边找大数while (left < right && a[left] <= key)++left;a[pivot] = a[left];pivot = left;}a[pivot] = key;return pivot;
}

和hoare法一样,整体有序只要左区间有序,右区间有序,整体就有序了。所以也是分治递归。
介绍了hoare法,挖坑法,最后再来一个前后指针法:
前后指针法:
  相对于前面的两个方法,前后双指针方法不太好理解,所以接下来我们通过一张动态图片来分析一下前后指针法怎么个原理:

从这个动图可以看出:

当cur一直遇到小于key的值,那么prev和cur就永远是紧挨着一个距离的。
而当cur遇到大的值的时候,就会逐步拉开和prev的距离

所以我们可以写出如下的代码:

int PartionByPrevCur(int* a, int left, int right)
{   int prev = left, cur = left + 1;int keyi = left;while (cur <= right){//cur找到小并且交换有意义才进行交换if (a[cur] < a[keyi] && a[++prev] != a[cur]){Swap(&a[prev], &a[cur]);}++cur;}Swap(&a[prev], &a[keyi]);return prev;
}

接下来也是和之前一样,对选出来的keyi的左右区间分治递归即可。
介绍完了这3种方法,我们来分析一下这个算法的时间复杂度:

首先我们先来看单趟遍历:不难看出,我们需要选keyi是要遍历整个区间的,所以单趟排序的时间复杂度是O(N),那么接下来递归分治成2个区间去处理,分治logN次,所以快排的平均时间复杂度是O(n*logn)

同时因为递归的原因,快排还存在O(logn)的空间复杂度
正因为递归存在栈溢出的问题,面试官就又会向你提出要求:小伙子实现以下非递归的快速排序吧。虽然心里有万般的不愿意,但是没办法。快排的非递归我们同样需要掌握!
首先我们仔细想一想递归的时候发生的事情:创建栈帧,保存begin,end区间。假如我们能够保存begin和end区间,那么就不需要递归也可以选出keyi,这里我们就可以利用数据结构的栈进行操作来模拟递归的动作:

//快速排序非递归实现
//每次快排的递归存储的都是分治的区间,使用一个栈来模拟
void QuickSortNonR(int* a, int begin, int end)
{Stack st;StackInit(&st);StackPush(&st, begin);StackPush(&st, end);while (!StackEmpty(&st)){int right = StackTop(&st);StackPop(&st);int left = StackTop(&st);StackPop(&st);//单趟排序int keyi = PartionByPiv(a, left, right);//符合条件的区间才入if (left < keyi - 1){StackPush(&st, left);StackPush(&st, keyi - 1);}if (keyi+1 < right){StackPush(&st, keyi+1);StackPush(&st, right);}}StackDestroy(&st);
}

  这时候你就会很好奇,快排真的什么时候都是最快速的吗?答案显然是否定的仔细想一想,快排的思想有点类似于二叉树,如果这个二叉树是一个单边的树,即每次选到的keyi都是端点值得画,那么快排也退化成了一个O(N*N)的算法!为了处理这种极端情况,官方还提供了一种三数取中选keyi的方法,具体的代码如下:

//三数取中优化
int GetMidIndex(int* a, int left, int right)
{int midi = ((right - left) >> 1) + left;if (a[left] < a[midi]){if (a[midi] < a[right]){return midi;}//a[midi]>a[right] a[left]和a[right]比else if (a[left]<a[right]){return right;}else{return left;}}//a[left]>a[midi]else{if (a[midi] > a[right]){return midi;}//a[midi]<a[right] 比 l和relse if (a[left]<a[right]){return left;}else{return right;}}
}

  有了三数取中,就可以很好避免出现上面单边树那种分治的极端情况,这时候的快排可以说是在绝大多数的情况下都很快了。
  对于快速排序,官方还进行了一个小区间优化。我们知道,快速排序要递归,那么就要占用栈空间,为了能够解放部分的栈空间,减少不必要的递归,那么在区间长度小于10的时候,调用直接插入排序是一个不错的选择。

//快速排序
void QuickSort(int* a, int begin, int end)
{   if (begin >= end)return;if (end - begin + 1 <= 10){InsertSort(a, end - begin + 1);}else{   int keyi = PartionByleft(a, begin, end);QuickSort(a, begin, keyi - 1);QuickSort(a, keyi + 1, end);}
}

归并排序:
  在链表那一个章节,我们曾经做过一道叫做合并两个有序链表的OJ题,那道题目我们的思路就是:两个链表有序,那么每次从两个链表里取小插入新链表得到最后的序列依然有序。其实这就是我们接下来要讲得归并排序的思路:接下来我们通过一张图片来体会一下归并排序的流程:
不难可以看出,整个归并排序的流程也是和快排类似,递归分割区间然后知道不可分割就开始有序的合并区间!具体我们需要开辟一个额外的临时空间,在这个区间里归并,然后把临时数组里的内容拷贝到原来的空间。

void _MergeSort(int* a, int* tmp, int begin, int end)
{int mid = ((end - begin) >> 1) + begin;if (begin >= end)return;_MergeSort(a, tmp, begin, mid);_MergeSort(a, tmp, mid + 1, end);//走到这里,递归结束开始归并int index = begin;int begin1 = begin, end1 = mid, begin2 = mid + 1, end2 = end;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]){tmp[index++] = a[begin1++];}else{tmp[index++] = a[begin2++];}}while (begin1 <= end1){tmp[index++] = a[begin1++];}while (begin2 <= end2){tmp[index++] = a[begin2++];}memcpy(a + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}//归并排序
void MergeSort(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);assert(tmp);_MergeSort(a,tmp ,0, n - 1);free(tmp);
}

接下来我们画出递归展开图来帮助分析和理解

这里的区间的划分如果是[begin,mid-1][mid,end]那么在特定的情况下会出现死递归!

比如在递归分治(0,1)区间,可得到mid=0,那么分治的区间是(0,-1)和(0,1)(0,1)这里就出现了死递归,最终的结果就是栈溢出!

  递归版本的归并排序还算比较人性化,但是由于递归存在栈溢出的问题,那么我们还要实现归并排序的非递归版本
首先我们来通过图解体会一下非递归的归并排序:

每次控制两个区间[i,i+dis-1],[i+dis,i+2*dis-1]区间进行单趟归并,接下来再用dis来控制归并区间的操作次数,所以最终的代码如下:

  int* tmp = (int*)malloc(sizeof(int) * n);assert(tmp);int dis = 1;while (dis < n){for (int i = 0; i < n; i += 2 * dis ){int begin1 = i, end1 = i + dis - 1;int begin2 = i + dis , end2 = i + 2 * dis - 1;int index = i;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]){tmp[index++] = a[begin1++];}else{tmp[index++] = a[begin2++];}}while (begin1 <= end1){tmp[index++] = a[begin1++];}while (begin2 <= end2){tmp[index++] = a[begin2++];}}dis *= 2;memcpy(a, tmp, sizeof(int) * n);}

测试用例:int a[]={10,6,7,1,3,9,4,2};

运行结果如下:

程序运行出了正确的结果,但是我们的代码就真的是正确的吗?多加一个数字11,看看结果如何
运行结果如下:

我们发现程序报了个内存错误,通常这种错误都是在free的时候发现的,说明说我们的代码存在越界问题。除了调试,我们还可以借助打印日志来分析错误:

//归并排序非递归版本
void MergeSortNoneR(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);assert(tmp);int gap = 1;while (gap < n){for (int i = 0; i < n; i += 2 * gap){int begin1 = i, end1 = i + gap - 1;int begin2 = i + gap, end2 = i + 2 * gap - 1;int index = i;printf("归并[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]){tmp[index++] = a[begin1++];}else{tmp[index++] = a[begin2++];}}while (begin1 <= end1){tmp[index++] = a[begin1++];}while (begin2 <= end2){tmp[index++] = a[begin2++];}}gap *= 2;memcpy(a, tmp, sizeof(int) * n);}free(tmp);
}

打印日志如下:

这里很明显看到,end1,begin2,end2发生了越界访问!原因就是区间长度不是2的倍数就很可能出现越界问题! 那么接下来我们就要对end1,begin2,end2进行修正!

修正方式1:只要越界就修正成n-1

//归并排序非递归版本
void MergeSortNoneR(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);assert(tmp);int gap = 1;while (gap < n){for (int i = 0; i < n; i += 2 * gap){int begin1 = i, end1 = i + gap - 1;int begin2 = i + gap, end2 = i + 2 * gap - 1;int index = i;printf("归并[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);if (end1 >= n)end1 = n - 1;if (begin2 >= n)begin2 = n - 1;if (end2 >= n)end2 = n - 1;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]){tmp[index++] = a[begin1++];}else{tmp[index++] = a[begin2++];}}while (begin1 <= end1){tmp[index++] = a[begin1++];}while (begin2 <= end2){tmp[index++] = a[begin2++];}}gap *= 2;memcpy(a, tmp, sizeof(int) * n);}free(tmp);
}

实际上这样修正依然没有完整处理越界问题!,假设最后出现了这种情况:其中一个待归并的区间是另外一个区间的子区间这时候就会出现数据重复写入index越界的情况!
所以正确的修正情况要分如下几种情况:

情况1:end1越界,修正end1即可
情况2:begin2越界,那么把第二个区间修正成一个不存在的区间
情况3:begin2正常,end2越界,那么修正end2就可以了

所以最后的排序代码如下:

//归并排序非递归版本
void MergeSortNoneR(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);assert(tmp);int dis= 1;while (dis< n){for (int i = 0; i < n; i += 2 * dis){int begin1 = i, end1 = i + dis- 1;int begin2 = i + dis, end2 = i + 2 * dis- 1;int index = i;printf("归并[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);end1越界修正即可if (end1 >=n){end1 = n - 1;}//begin2越界,设置成不存在的区间if (begin2 >=n){begin2 = n+1;end2 = 2;}//如果begin2有效,end2越界,修正end2if (begin2<n && end2>=n){end2 = n - 1;}while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]){tmp[index++] = a[begin1++];}else{tmp[index++] = a[begin2++];}}while (begin1 <= end1){tmp[index++] = a[begin1++];}while (begin2 <= end2){tmp[index++] = a[begin2++];}}dis*= 2;memcpy(a, tmp, sizeof(int) * n);}free(tmp);
}

  最后我们来分析一下归并排序的时间复杂度:
和快排不一样,归并排序是绝对的二分,所以说归并排序是绝对的O(N*log(N))
  介绍完了前面的这些比较排序,最后再介绍一个“巧妙”的排序---->计数排序

开辟一个数组,里面的元素全是0,如果碰到对应位置的数就++,然后在取的时候把对应位置的数的下标取出,然后把个数–,最后得到的数据就是有序的。

代码如下:

// 计数排序
void CountSort(int* a, int n)
{int min = INT_MAX, max = INT_MIN;for (int i = 0; i < n; ++i){if (a[i] < min){min = a[i];}if (a[i] > max){max = a[i];}}int range = max - min + 1;int * countArray = (int*)calloc(range, sizeof(int));assert(countArray);for (int i = 0; i < n; ++i){countArray[a[i] - min]++;}//写数据int index = 0;for (int i = 0; i < range; ++i){while (countArray[i]--){a[index++] = i + min;}}free(countArray);
}

这个代码使用了相对映射,也就是根据数据的最大范围来分配空间,然后再取出的时候加上min即可.
时间复杂度:O(range+n)
空间复杂度:O(range)
这个排序的局限性很大:因为这个排序只能排int类型的数据,对于其它的数据类型并不能排序.
7.排序的稳定性:
  一直以来,可能你们对排序的稳定性一直有误解,稳定性并不是说排序的性能够不够好,而是对数据的处理够不够好。举个例子,有的时候在分数相同的前提下,要把数学成绩作为第二关键字来进行排序,如果能够做到这么一件事。我们就说这个排序是稳定的,反之就是不稳定的。
  下表是各种排序的稳定性

排序 稳定性
冒泡排序 稳定
插入排序 稳定
希尔排序 不稳定
直接选择 不稳定
堆排序 不稳定
快速排序 不稳定
归并排序 稳定
计数排序 不稳定

最终测试排序稳定性:
  如果读者想要测试排序的性能,可以使用系统里的clock()函数,然后用作差的方式来求出排序消耗的时间,具体的测试代码如下:

void TestOP()
{const int N = 10000000;int* a1 = (int*)malloc(sizeof(int) * N);int* a2 = (int*)malloc(sizeof(int) * N);int* a3 = (int*)malloc(sizeof(int) * N);int* a4 = (int*)malloc(sizeof(int) * N);assert(a1);assert(a2);assert(a3);assert(a4);srand((unsigned)time(NULL));for (int i = 0; i < N; ++i){a1[i] = rand();a2[i] = a1[i];a3[i] = a2[i];a4[i] = a3[i];}int begin1 = clock();QuickSort(a1, 0,N-1);int end1 = clock();printf("QuickSort: %d \n", end1 - begin1);int begin2 = clock();InsertSort(a2, N);int end2 = clock();printf("InsertSort: %d \n", end2 - begin2);int begin3 = clock();ShellSort(a3, N);int end3 = clock();printf("ShellSort: %d \n", end3 - begin3);int begin4= clock();HeapSort(a4, N);int end4 = clock();printf("HeapSort: %d \n", end4 - begin4);free(a1);free(a2);free(a3);free(a4);
}

以上就是文章所有的内容,如果有错误之处,还望读者可以指出,制作不易,希望各位百忙之中的客观能够抽空为作者的作品点一个赞,留下积极的评论。这就是你对作者最大的肯定。到这里,数据结构初阶就结束了,下一篇博客正式进入C++的大门。

``

数据结构初阶最终章------>经典八大排序(C语言实现)相关推荐

  1. 数据结构初阶(4)(OJ练习【判断链表中是否有环、返回链表入口点、删除链表中的所有重复出现的元素】、双向链表LinkedList【注意事项、构造方法、常用方法、模拟实现、遍历方法、顺序表和链表的区别)

    接上次博客:数据结构初阶(3)(链表:链表的基本概念.链表的类型.单向不带头非循环链表的实现.链表的相关OJ练习.链表的优缺点 )_di-Dora的博客-CSDN博客 目录 OJ练习 双向链表--Li ...

  2. 【数据结构初阶】链表(下)——带头双向循环链表的实现

    目录 带头双向循环链表的实现 1.带头双向循环链表的节点类型 2.创建带头双向循环链表的节点 3.向带头双向循环链表中插入数据 <3.1>从链表尾部插入数据 <3.2>从链表头 ...

  3. 二叉树前中后序遍历+刷题【中】【数据结构/初阶/C语言实现】

    文章目录 1. 二叉树基础操作 1.1 二叉树遍历 1.1.1 前序遍历 前序遍历(Pre-Order Traversal) 1.1.2 中序遍历 中序遍历(In-Order Traversal) 1 ...

  4. 【数据结构初阶】八大排序算法+时空复杂度

    学会控制自己是人生的必修课 文章目录 一.插入排序 1.直接插入排序 2.希尔排序 二.选择排序 1.直接选择排序 2.堆排序(已经建好堆的基础之上) 三.交换排序(Swap) 1.冒泡排序(大学牲最 ...

  5. 数据结构初阶:八大排序

    文章目录 1 排序的概念 2 插入排序 2.1直接插入排序 2.1.1 基本思想 2.1.2代码实现 2.1.3 复杂度分析 2.2 希尔排序 2.2.1 基本思想 2.2.2 代码实现 2.2.3 ...

  6. 数据结构初阶:二叉树

    二叉树 链表和数组都是线性结构,而树是非线性的结构. 树是依靠分支关系定义出的一种层次结构.社会亲缘关系和组织结构图都可以用树来形象地表示. 1. 树 1.1 树的定义 树是 n ( n ≥ 0 ) ...

  7. 树与二叉树(二叉树前传、数据结构初阶、C语言)

    文章目录 前言 一.树的概念及结构 (一).树的概念 (二).树的相关概念 (三).树的表示 二.二叉树的概念及结构 (一).二叉树的概念 (二).满二叉树和完全二叉树 (三).二叉树的性质 (四). ...

  8. 数据结构初阶——第三节-- 单链表

    链表 1. 链表的概念及结构 1.1 链表的概念及结构 1.2 链表的实现(无头+单向+非循环链表增删查改实现) 1. 动态申请一个节点 2. 单链表打印 3. 单链表尾插 1. 找尾用 tail - ...

  9. 数据结构初阶——链式二叉树

    目录 树概念及结构 树的概念 树的表示 二叉树概念及结构 概念 特殊二叉树 二叉树的性质 二叉树链式结构及实现 二叉树的简单创建 二叉树的前序遍历 二叉树中序遍历与二叉树后序遍历 求二叉树节点个数 求 ...

最新文章

  1. 软硬兼施极限轻量BERT!能比ALBERT再轻13倍?!
  2. 解决TextView排版混乱或者自动换行的问题
  3. IE6中Form.submit不提交的问题
  4. 从零开始搭建spring-cloud(3) ----feign
  5. 深入理解Delete(JavaScript)
  6. 蓝桥杯第八届省赛JAVA真题----油漆面积
  7. ​如何判断公司是否靠谱?
  8. 用DOM方式读取xml
  9. idea配置Lua环境
  10. linux下启动tomcat出现“This file is needed to run this program ”
  11. urllib实现请求发送(python3)
  12. html调用ckplayer说明,CKplayer功能配置(示例代码)
  13. oracle数据库的使用
  14. win10 高分屏显示模糊的解决办法
  15. 中国大学数据科学与大数据技术专业排名!2021软科排名
  16. android app分享到微信让应用来源显示qq浏览器或者是其他应用
  17. 项目管理基础案例分析答案
  18. iOS Core Bluetooth_2 基础知识
  19. Linux环境下程序的多核CPU占用率高的问题分析和解决
  20. 微信小程序长按图片发送给好友

热门文章

  1. 年轻代,老年代,永久代
  2. 【面试必过系列】程序员简历就该这样写,美的Java面试题
  3. 大脑皮层是如何工作的 《人工智能的未来》(On intelligence)读书笔记
  4. 商务智能与第三方物流企业管理
  5. 十分有趣的逻辑推理题
  6. 计算机动画的 优缺点,浅谈计算机动画画面的视觉特征
  7. Beanstalk(内存队列)
  8. 自监督对比学习系列论文(二):有引导对比学习--SCCL,SwAV,PCL,SupervisedCon
  9. js-onbeforepaste详解
  10. dcat-admin 配置富文本参数 TinyMCE配置