数据结构|排序算法(七大经典排序算法)

【数据结构|排序算法(七大经典排序算法)】排序算法是一种在日常生活中应用很广泛的算法,所以我们应该很好的掌握他。然而最熟悉的往往是最容易忽略的。“工欲善其事,必先利其器”,将对下面常见算法逐个介绍。
本文所有排序分析及代码实现都是升序情况 ,降序与此相反。
常见排序算法:
数据结构|排序算法(七大经典排序算法)
文章图片

1.插入排序算法
1)直接插入排序
思路分析:
①在长度为N的数组,将数组中第i【1~(N-1)】个元素,插入到数组【0~i】适当的位置上。
②在排序的过程中当前元素之前的数组元素已经是有序的了。
③在插入的过程中,有序的数组元素,需要向右移动为更小的元素腾出空间,直到为当前元素找到合适的位置。
过程动态图展示:
数据结构|排序算法(七大经典排序算法)
文章图片

代码实现:

void InsertSort(DataType *a,size_t n) { size_t i = 0; assert(a); for (i=0; i=0 && a[end] > tmp) { a[end+1] = a[end]; end--; }a[end+1] = tmp; } }

总结:时间复杂度为O(n^2),数据量小时使用。并且大部分已经被排序。
2)希尔排序
希尔排序是对插入排序最坏情况的改进。主要改进思路是减少数据移动次数,增加算法的效率。
思路分析
先比较距离远的元素,而不是像简单交换排序算法那样先比较相邻的元素,这样可以快速减少大量的无序情况,从而减轻后续的工作。被比较的元素之间的距离逐步减少,直到减少为1,这时的排序变成了相邻元素的互换。
代码实现:
void ShellSort(DataType *a,size_t n) { int gap = n; size_t i = 0; while (gap>1) { gap = gap/3 + 1; for (i=0; i= 0 && a[end] > tmp) { a[end+gap] = a[end]; end-= gap; }a[end+gap] = tmp; } } }

总结:希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。这样可以让一个元素可以一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序,算法的最后一步就是普通的插入排序,但是到了这步,需排序的数据几乎是已排好的了(此时插入排序较快)。
步长的选择是希尔排序的重要部分。只要最终步长为1任何步长序列都可以工作(且步长要小于数组长度)。算法最开始以一定的步长进行排序。然后会继续以一定步长进行排序,最终算法以步长为1进行排序。当步长为1时,算法变为插入排序,这就保证了数据一定会被排序。
2.选择排序算法
1)选择排序
思路分析:第一趟从n个元素的数据序列中选出关键字最小/大的元素并放在最前/后位置,下一趟从n-1个元素中选出最小/大的元素并放在最前/后位置。以此类推,经过n-1趟完成排序
过程动态图
数据结构|排序算法(七大经典排序算法)
文章图片

代码实现(此处代码对直接排序进行了有优化,遍历一次同时选出最大的和最小的,最大的放在最右边,最小的放在最左边,排序范围缩减)
void SelectSort(DataType *a,size_t n) { size_t left = 0; size_t right = n-1; assert(a); while (left a[i]) min = i; if (a[max] < a[i]) max = i; } Swap(&a[left],&a[min]); if (max == left) //最大值在最左边 max = min; Swap(&a[right],&a[max]); left++; right--; } }

总结: 直接选择排序的最好时间复杂度和最差时间复杂度都是O(n2),因为即使数组一开始就是正序的,也需要将两重循环进行完,平均时间复杂度也是O(n2)。空间复杂度为O(1),因为不占用多余的空间。直接选择排序是一种原地排序(In-place sort)并且稳定(stable sort)的排序算法,优点是实现简单,占用空间小,缺点是效率低,时间复杂度高,对于大规模的数据耗时长。
2)堆排序
思路分析
①将长度为n的待排序的数组进行堆有序化构造成一个大顶堆
②将根节点与尾节点交换并输出此时的尾节点
③将剩余的n -1个节点重新进行堆有序化
④重复步骤2,步骤3直至构造成一个有序序列
(升序构建小堆,降序构建大堆)
过程动态图
数据结构|排序算法(七大经典排序算法)
文章图片

代码实现:
void AdjustDown(DataType *a,size_t n,size_t root)//向下调整算法 { size_t parent = root; size_t child = parent *2+1; while (child>1; i>0; --i) AdjustDown(a,n,i); while (end) { Swap(&a[0],&a[end]); AdjustDown(a,end,0); end--; } }

总结: 堆排序的最好和最差情况时间复杂度都为O(nlogn),平均时间复杂度也为O(nlogn),空间复杂度为O(1),无需使用多余的空间帮助排序。优点是占用空间小,时间复杂度低,达到了基于比较的排序的最低时间复杂度,缺点是实现较为复杂,并且当待排序序列发生改动时,哪怕是小改动,都需要调整整个堆来维护堆的性质,维护开销大。
3.交换排序
1)冒泡排序
思路分析:
①比较相邻的元素。如果第一个比第二个大,就交换他们两个。
②对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
③针对所有的元素重复以上的步骤,除了最后一个。
④持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
过程动态图:
数据结构|排序算法(七大经典排序算法)
文章图片

代码实现:
void BubbleSort(DataType *a,size_t n) { size_t i,j,flag = 0; assert(a); for (i=0; i a[j+1]) { Swap(&a[j],&a[j+1]); flag = -1; } } if (!flag) break; } }

总结:冒泡排序算法最坏情况和平均复杂度是O(n2),空间复杂度为O(1)。
2)快速排序
本质上快速排序把数据划分成几份,所以快速排序通过选取一个关键数据,再根据它的大小,把原数组分成两个子数组:第一个数组里的数都比这个主元数据小或等于,而另一个数组里的数都比这个主元数据要大或等于。
快排有三种方法①左右指针法②挖坑法③前后指针法
#快排之所有快,是因为利用了二分的思想
void QSort(DataType *a,int left, int right) { /****************************【快排思路】****************************** *快排之所以快,是利用了二分的思想 ************************************************************************/ assert(a); if (left<=right) { int mid = PartSort3(a,left,right); QSort(a,left,mid-1); QSort(a,mid+1,right); } }

①左右指针法
int PartSort1(DataType *a,int left,int right) {/* 左右指针法*/ /************************************************************************ *选取key(key随意选择,选左边或者右边方便设计算法), *大的值放在key的右边,小的值放在key的左边。 *最后当bigin和end相交时,交换交点值和key。 *返回交点的下标,等待下一次递归使用 /************************************************************************/ int key = a[right]; int begin = left; int end = right; while (begin= key && begin < end) end--; Swap(&a[begin],&a[end]); } Swap(&a[begin],&a[right]); return end; }

②挖坑法
int PartSort2(DataType *a,int left,int right) { /******************************【挖坑法】******************************** *选取初始化坑,(选取最左边或者最右边的值),大的值放在坑的右边,小的值 *放在坑的左边,当左右相遇时,把原始坑的值放在这里。此时左边所有值比坑大 *右边所有值比坑小,把数组分成两半,依次递归。 ************************************************************************/ int begin = left; int end = right; DataType key = a[right]; while (begin < end) { while(a[begin] <= key && begin< end) begin++; a[right] = a[begin]; while(a[end] >= key && begin < end) end--; a[begin] = a[end]; } a[end] = key; return end; }

③前后指针法
int PartSort3(DataType *a,int left,int right) { /****************************【前后指针法】****************************** *定义两个指针,如果前面的a[cur]小于key,prev就跟着走,遇到a[cur]大于key时 *prev停下来,cur继续走,当a[cur]等于key时,一趟结束,交换key和a[cur],平分 *数组,依次递归。 ************************************************************************/ int prev = left-1; int cur = left; while (cur < right) { if (a[cur] < a[right] && a[cur] != a[++prev]) Swap(&a[cur],&a[prev]); cur++; } Swap(&a[++prev],&a[right]); return prev; }

3)快排优化
①三数取中法
当我们每次选取key时,如果key恰好是最大或者最小值,此时快排效率会很低,为了避免这种情况,我们对快排选取key值进行优化。
优化思路:依旧选取最右边的值作为key,但是在选取前,我们把数组中最左边,中间,最右边位置的三个数取出来。找到这三个数中值排在中间的一个。把该值与最右边位置的值进行交换。此时key的值不可能是最大值或者最小值。
②随机值法。
num = rand()%N 把num位置的数与最右边的值交换,key依旧去最右边的值。这种方法也可以,但是太随机了,特殊场景会导致不可控的结果。
③小区间优化
快排是利用递归栈帧完成的,如果递归深度太深会影响效率。切割区间时,当区间内元素数量比较少时就不用切割区间了,这时候就直接对这个区间采用直接插入法,可以进一步提高算法效率。
3)归并排序
思路分析:
?是利用归并的思想实现的排序方法,该算法采用经典的分治策略,分治法将问题分成一些小的问题然后递归求解,而治的阶段则将分的阶段得到的各答案”修补”在一起,即分而治之。
题解思路:
数据结构|排序算法(七大经典排序算法)
文章图片

数据结构|排序算法(七大经典排序算法)
文章图片

代码实现:
void Merge(DataType* arr,DataType* ret,size_t left,size_t mid,size_t right) //排序并合并 { size_t i = left; size_t j = mid+1; size_t k = left; while (i!= mid+1 && j!= right+1) { if (arr[i]>arr[j]) ret[k++] = arr[j++]; else ret[k++] = arr[i++]; } while (i!= mid+1) ret[k++] = arr[i++]; while (j!=right+1) ret[k++] = arr[j++]; for (size_t i = left; i<=right; i++) arr[i] = ret[i]; }void MergeSort(DataType* arr,DataType*ret, size_t left,size_t right)//归并排序 { if(left

归并排序是稳定排序,它也是一种十分高效的排序。
排序算法性能比较
名称 时间复杂度 空间复杂度 稳定性
直接插入排序 O(N^2) O(1) 稳定
希尔排序 O(N)~O(N^2)之间 O(1) 不稳定
选择排序 O(N^2) O(1) 不稳定
堆排序 O(NlogN) O(1) 不稳定
冒泡排序 O(NlogN) O(1) 稳定
快排 O(NlogN) O(N) 不稳定
归并排序 O(Nlog(N)) O(N) 稳定

    推荐阅读