文章目录
- 排序的概念及其运用
- 1.1排序的概念
- 1.2排序的运用
- 1.3常见的排序算法
- 常见排序算法的实现
- 2.插入排序
- 2.1 基本思想
- 2.2 ✨直接插入排序
- 2.3 ✨希尔排序( 缩小增量排序 )
- 3. 选择排序
- 3.1 基本思想
- 3.2✨直接选择排序
- 3.3✨堆排序
- 4. 交换排序
- 4.1✨冒泡排序
- 4.2 ✨快速排序
- 4.2.1快速排序hoare版本
- 4.2.2快速排序挖坑法版本
- 4.2.3快速排序前后指针版本
- 4.2.4快速排序优化
- 4.2.5快速排序非递归
- 5.✨归并排序
- 5.1递归版本
- 5.2 非递归版本
- 6.✨非比较排序
- 总结
排序的概念及其运用
1.1排序的概念
- 排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作
- 稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次
序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算 法是稳定的;否则称为不稳定的。 - 内部排序:数据元素全部放在内存中的排序。
- 外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序
1.2排序的运用
排序在实现生活上可谓是不可缺少的一部分,比如我们网上购物,会按照某一个属性进行排序访问,
再比如平时查看大学排名等等都需要排序
目的就是方便我们筛选,选择
1.3常见的排序算法
常见排序算法的实现
2.插入排序
2.1 基本思想
直接插入排序是一种简单的插入排序法,其基本思想是:
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为
止,得到一个新的有序序列
实际中我们玩扑克牌时,就用了插入排序的思想
2.2 ✨直接插入排序
当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与
array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移
代码实现:
//插入排序void InsertSort(int* a, int n){for (int i = 0; i < n - 1; ++i){int end = i;int tem = a[end + 1];//保存后一个值while (end >= 0){if (tem < a[end])//后一个值小,前面的值往后移{a[end + 1] = a[end];end--;}else{break;}}a[end + 1] = tem;//把小的值插到属于它的位置}}
直接插入排序的特性总结:
- 元素集合越接近有序,直接插入排序算法的时间效率越高
- 时间复杂度:O(N^2)
- 空间复杂度:O(1),它是一种稳定的排序算法
- 稳定性:稳定
直接插入排序在时间复杂度为O(N^2)的排序上算得上是佼佼者了,因为它最好的情况上排序的时间复杂度为O(N),就是当它排一个有序的数据时,它是比较高效率的,甚至比快速排序还快(当然前提是数据有序)
2.3 ✨希尔排序( 缩小增量排序 )
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数gap,把待排序文件中所有记录分成gap个
组,所有距离为gap的记录分在同一组内,并对每一组内的记录进行排序。然后分别进行插入排序,重复上述分组和排序的工
作。当到达gap=1时,所有记录在统一组内排好序。
选定的那个整数,就是gap,是希尔排序的核心,至于怎么选,都是有技巧的!
一些动画的演示:
《数据结构-用面相对象方法与C++描述》— 殷人昆
// 希尔排序void ShellSort(int* a, int n){int gap = n;while (gap > 1){gap = gap / 3 + 1;for (int i = 0; i < n - gap;i++){int end = i;int tem = a[end + gap];//保存第后gap的值while (end >= 0){if (tem < a[end])//后gap的值小,前面的值往后移{a[end + gap] = a[end];end-= gap;}else{break;}}a[end + gap] = tem;//把小的值插到属于它的位置}}}
希尔排序的特性总结:
- 希尔排序是对直接插入排序的优化。
- 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就
会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。 - 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些书中给出的
希尔排序的时间复杂度都不固定
《数据结构(C语言版)》— 严蔚敏
《数据结构-用面相对象方法与C++描述》— 殷人昆
- 稳定性:不稳定
你是否有过这样的疑惑?
既然希尔排序是插入排序的一种优化,那这优化力度有多大呢,感觉也就那样!
其实并非这样,希尔排序的优化,可谓是给插入排序如虎添翼了,实践是检验真理的唯一标准,下面我们用实际数据来检验一下它的优化之牛
我们用十万个随机数分别让插入排序和希尔排序分别排序,计算它们运行的时间,可以发现它们两的差距还是很大的,说明希尔排序的优化还是很牛的(单位是毫秒)
3. 选择排序
3.1 基本思想
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的
数据元素排完
3.2✨直接选择排序
- 在元素集合array[i]–array[n-1]中选择关键码最大(小)的数据元素
- 若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
- 在剩余的array[i]–array[n-2](array[i+1]–array[n-1])集合中,重复上述步骤,直到集合剩余1个元素
选择排序一趟选最小的放前面,我们可以稍微优化一下,在一趟中同时选出最大和最小值,分别放在前面和后面
代码实现:
// 选择排序void SelectSort(int* a, int n){int begin = 0, end = n-1 ;while (begin < end){int maxi = begin, mini = begin;//选数,同时选出最大的和最小的for (int i = begin; i <= end; i++){if (a[maxi] < a[i])//选出大的那个下标maxi = i;if (a[mini] > a[i])//选出小的那个下标mini = i;}Swap(&a[mini], &a[begin]);//如果刚好最大值在begin位置,则要更新maxiif (maxi == begin)maxi = mini;Swap(&a[maxi], &a[end]);begin++;end--;}}
直接选择排序的特性总结
- 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:不稳定
3.3✨堆排序
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是
通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
✨【二叉树】数中的特殊结构->堆✨
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
- 建堆
- 升序:建大堆
- 降序:建小堆
- 利用堆删除思想来进行排序
void Swap(int* e1, int* e2){int tem = *e1;*e1 = *e2;*e2 = tem;}//向下调整算法void AdjustDwon(int* a, int n, int parent){int child = parent * 2 + 1;while (child < n){if (child + 1 < n && a[child] < a[child + 1])//升序建大堆{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 - 2) / 2; i >= 0; i--){AdjustDwon(a, n, i);}//排序for (int i = 0; i < n; i++){Swap(&a[0], &a[n-i-1]);//把大的放到后面AdjustDwon(a, n - i-1, 0);//重新调整堆}}
堆排序的特性总结:
- 堆排序使用堆来选数,效率就高了很多。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(1)
- 稳定性:不稳定
4. 交换排序
基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排
序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
4.1✨冒泡排序
冒泡排序的原理是:从左到右,相邻元素进行比较。每次比较一轮,就会找到序列中最大的一个或最小的一个。这个数就会从序列的最右边冒出来。
以从小到大排序为例,第一轮比较后,所有数中最大的那个数就会浮到最右边;第二轮比较后,所有数中第二大的那个数就会浮到倒数第二个位置……就这样一轮一轮地比较,最后实现从小到大排序。
// 冒泡排序void BubbleSort(int* a, int n){for (int i = 0; i < n - 1; i++){int flag = 0;for (int j = 0; j < n - i - 1; j++){if (a[j] > a[j + 1]){Swap(&a[j], &a[j + 1]);flag = 1;//标志本趟有交换元素}}//在一趟比较中,如果没有发生交换元素,说明已经有序,不需要再排序if (flag == 0)break;}}
冒泡排序的特性总结:
- 冒泡排序是一种非常容易理解的排序
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
4.2 ✨快速排序
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中
的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右
子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
// 假设按照升序对array数组中[left, right)区间中的元素进行排序void QuickSort(int array[], int left, int right){if(right - left <= 1)return;// 按照基准值对array数组的 [left, right)区间中的元素进行划分int div = partion(array, left, right);// 划分成功后以div为边界形成了左右两部分 [left, div) 和 [div+1, right)// 递归排[left, div)QuickSort(array, left, div);// 递归排[div+1, right)QuickSort(array, div+1, right);}
上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,同学们在写递归框架时可想想二叉
树前序遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式即可
将区间按照基准值划分为左右两半部分的常见方式有:
- hoare版本
- 挖坑法版本
- 前后指针版本
4.2.1快速排序hoare版本
一趟的排序动画演示
// 快速排序hoare版本int PartSort1(int* a, int left, int right){int keyi = left;while (left < right){//找小while (left < right && a[right] >= a[keyi]){right--;}//找大while (left < right && a[left] <= a[keyi]){left++;}if (left < right){//交换Swap(&a[left], &a[right]);}}Swap(&a[left], &a[keyi]);return left;}void QuickSort(int* a, int left, int right){if (left >= right)return;int pivot=PartSort1(a, left, right);//排完了一趟后分成了两个区域 [left,pivot] pivot [pivot+1,right];QuickSort(a, left, pivot - 1);//递归左子区间QuickSort(a, pivot+1, right);//递归右子区间}
4.2.2快速排序挖坑法版本
一趟的排序动画演示
// 快速排序挖坑法int PartSort2(int* a, int left, int right){int key = a[left];//保存基准值int hole = left;//坑位while (left < right){//找小while (left < right && a[right] >= key){right--;}//放坑位if (left < right){a[hole] = a[right];hole = right;//更新坑位}//找大while (left < right && a[left] <= key){left++;}if (left < right){a[hole] = a[left];hole = left;}}a[hole] = key;return hole;}void QuickSort(int* a, int left, int right){if (left >= right)return;int pivot=PartSort2(a, left, right);//排完了一趟后分成了两个区域 [left,pivot] pivot [pivot+1,right];QuickSort(a, left, pivot - 1);//递归左子区间QuickSort(a, pivot+1, right);//递归右子区间}
4.2.3快速排序前后指针版本
// 快速排序前后指针法int PartSort3(int* a, int left, int right){int keyi = left;int prev = left;int cur = left+1;while (cur <= right){//cur从左往右找小跟prev换if (a[cur] < a[keyi]&& ++prev!=cur){Swap(&a[prev], &a[cur]);}cur++;}Swap(&a[prev], &a[keyi]);return prev;}void QuickSort(int* a, int left, int right){if (left >= right)return;int pivot=PartSort3(a, left, right);//排完了一趟后分成了两个区域 [left,pivot] pivot [pivot+1,right];QuickSort(a, left, pivot - 1);//递归左子区间QuickSort(a, pivot+1, right);//递归右子区间}
4.2.4快速排序优化
- 三数取中法选key
快速排序为什么还需要优化呢?
快速排序在选key的时候,最理想,效率最高的情况就是每次都能选到中间值
但是,快速排序在没有优化前,对数据有序的情况进行排序,那么它每次选key的值都在最左边或最右边,效率就会大大降低,
下面是我用十万个有序数据对快速排序优化前后测出来的数据对比(单位ms)
三数取中讲究的就是让我们每次选key的时候都能保证它大概中分区间,不至于每次都选到最大或最小。
代码实现:
//三数取中int GetMidIndex(int* a, int left, int right){int mid = left + (right - left) / 2;if (a[left] > a[mid]){if (a[mid] > a[right])return mid;else if (a[right] > a[left])return left;elsereturn right;}else//a[left] <= a[mid]{if (a[left] > a[right])return left;else if (a[right] > a[mid])return mid;elsereturn right;}}
用三数取中优化后的各个版本的快排
// 快速排序hoare版本int PartSort1(int* a, int left, int right){int mid = GetMidIndex(a, left, right);//取相对中间的值Swap(&a[mid], &a[left]);//跟第一个值进行交换,下面逻辑就不用变化太大,下面版本也类似int keyi = left;while (left < right){//找小while (left < right && a[right] >= a[keyi]){right--;}//找大while (left < right && a[left] <= a[keyi]){left++;}if (left < right){//交换Swap(&a[left], &a[right]);}}Swap(&a[left], &a[keyi]);return left;}// 快速排序挖坑法int PartSort2(int* a, int left, int right){int mid = GetMidIndex(a, left, right);Swap(&a[mid], &a[left]);int key = a[left];//保存基准值int hole = left;//坑位while (left < right){//找小while (left < right && a[right] >= key){right--;}//放坑位if (left < right){a[hole] = a[right];hole = right;//更新坑位}//找大while (left < right && a[left] <= key){left++;}if (left < right){a[hole] = a[left];hole = left;}}a[hole] = key;return hole;}// 快速排序前后指针法int PartSort3(int* a, int left, int right){int mid = GetMidIndex(a, left, right);Swap(&a[mid], &a[left]);int keyi = left;int prev = left;int cur = left+1;while (cur <= right){//cur从左往右找小跟prev换if (a[cur] < a[keyi]&& ++prev!=cur){Swap(&a[prev], &a[cur]);}cur++;}Swap(&a[prev], &a[keyi]);return prev;}void QuickSort(int* a, int left, int right){if (left >= right)return;int pivot=PartSort2(a, left, right);//分成了两个区域 [left,pivot] pivot [pivot+1,right];QuickSort(a, left, pivot - 1);QuickSort(a, pivot+1, right);}
- 递归到小的子区间时,可以考虑使用插入排序
其实快速排序经过上面的三数优化之后,大体上已经没有很多问题,也不会出现最坏的情况,但是还可以进行优化
.
因为快排在实现排序的时候,当区间已经变小了,那么它的数据就相对已经有序了很多,此时我们就可以使用插入排序对它的小区间进行排序,插入排序对相对有序的数据进行排序效率是比较高的。
void QuickSort(int* a, int left, int right){if (left >= right)return;int pivot=PartSort2(a, left, right);//分成了两个区域 [left,pivot] pivot [pivot+1,right];//小区间优化if (right - left < 8)//当区间比较小的时候就可以使用插入排序{InsertSort(a + left, right - left + 1);}else{QuickSort(a, left, pivot - 1);QuickSort(a, pivot + 1, right);}}
4.2.5快速排序非递归
递归改循环,有两种情况
- 比较简单的递归(如斐波那契数列),可以直接改循环
- 比较复杂的递归(如快排),需要借助其他工具模拟实现
快速排序的非递归需要借助数据结构的栈去模拟递归的实现
// 快速排序 非递归实现void QuickSortNonR(int* a, int left, int right){Stack st;StackInit(&st);StackPush(&st, right);StackPush(&st, left);while (!StackEmpty(&st)){int begin = StackTop(&st);StackPop(&st);int end = StackTop(&st);StackPop(&st);int pivot = PartSort2(a, begin, end);//[begin, pivot-1] pivot [pivot+1,end]//当区间有数才入栈if (end - pivot > 1){StackPush(&st, end);StackPush(&st, pivot + 1);}if (pivot - begin > 1){StackPush(&st, pivot - 1);StackPush(&st, begin);}}}//小区间优化版本void QuickSortNonR(int* a, int left, int right){Stack st;StackInit(&st);StackPush(&st, right);StackPush(&st, left);while (!StackEmpty(&st)){int begin = StackTop(&st);StackPop(&st);int end = StackTop(&st);StackPop(&st);//小区间优化if (end - begin < 8){InsertSort(a + begin, end - begin + 1);continue;}int pivot = PartSort2(a, begin, end);//分出的区间:[begin, pivot-1] pivot [pivot+1,end]//当区间有数才入栈if (end - pivot > 1){StackPush(&st, end);StackPush(&st, pivot + 1);}if (pivot - begin > 1){StackPush(&st, pivot - 1);StackPush(&st, begin);}}}
可以得出: 递归和非递归的效率差不多
非递归的好处:
- 避免栈溢出
快速排序的特性总结:
快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
时间复杂度:O(N*logN)
空间复杂度:O(logN)
稳定性:不稳定
5.✨归并排序
基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤
5.1递归版本
void _MergeSort(int* a, int begin, int end, int* tmp){if (begin >= end)return;//取中间数int mid = begin + (end - begin) / 2;//递归子问题_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;//取小的先放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, (end - begin + 1) * sizeof(int));}// 归并排序递归实现void MergeSort(int* a, int n){//开辟辅助空间int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){perror("malloc fail\n");return;}_MergeSort(a, 0, n - 1, tmp);free(tmp);tmp = NULL;}
5.2 非递归版本
归并排序的非递归可以直接改循环处理
但是在边界处理的时候需要及其小心
// 归并排序非递归实现void MergeSortNonR(int* a, int n){int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){perror("malloc fail\n");return;}//gap控制每组有多少个元素进行归并int gap = 1;while (gap < n){for (int j = 0; j < n; j += 2 * gap){//合并int begin1 = j, end1 = j+gap-1;int begin2 = j+gap, end2 = j+2*gap-1;int i = j;//end1越界if (end1 >= n){break;}//begin2 这组全部越界if (begin2 >= n){break;}//end2越界if (end2 >= n){end2 = n - 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 + j, tmp + j, (end2 - j + 1) * sizeof(int));}gap *= 2;}}
归并排序的特性总结:
- 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N)
- 稳定性:稳定
6.✨非比较排序
思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:
- 统计相同元素出现次数
- 根据统计的结果将序列回收到原来的序列中
// 计数排序void CountSort(int* a, int n){int max, min;max = min = a[0];//找出最大最小值for (int i = 0; i < n; ++i){if (max < a[i])max = a[i];if (min > a[i])min = a[i];}//开辟对应大小的空间int* count = (int*)malloc(sizeof(int) * (max - min + 1));if (count == NULL){perror("malloc fail\n");return;}//将辅助数组中的值初始化成0memset(count, 0, sizeof(int) * (max - min + 1));//统计每个数出现的次数for (int i = 0; i < n; ++i){count[a[i] - min]++;}//开始拷贝回原数组int i = 0, j = 0;while (j < n){if (count[i] == 0){i++;continue;}a[j++] = i + min;count[i]--;}}free(count);
计数排序的特性总结:
- 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
- 时间复杂度:O(MAX(N,范围))
- 空间复杂度:O(范围)
- 稳定性:稳定
总结
排序算法有很多,比如还有基数排序,桶排序…但是我们掌握上面这八种足矣!
毕竟算得上经典的也就这么多,比较常用的也包括在里面了!
下面是一些排序的实测数据结果(冒泡,非比较排序不在里面)单位ms
一共是一百万个随机数
可以看出仅仅一百万个数据插入排序和选择排序的速度是非常缓慢的。