八大基本排序(详解)
前言
博主将在本文讲述选择,插入,希尔,快速,冒泡,堆排,归并排序,记数排序.
文章目录
- 排序相关概念
- 1.插入排序
- 1.1 直接插入排序
- 思路:
- 代码实现
- 复杂度
- 1.2希尔排序(缩小增量排序)
- 动图演示:
- 画图讲解:
- 代码
- 2.选择排序
- 2.1.1单向选择排序
- 思路
- 动图演示:
- 代码:
- 2.1.2 双向选择排序(对单向选择排序的优化)
- 代码
- 复杂度
- 2.2 堆排序
- 3.交换排序
- 3.1冒泡排序
- 思路:
- 代码:
- 3.2 快速排序
- 3.2.1 hoare版本
- 思路
- 动图
- 代码:
- 3.2.2 挖坑法
- 思路
- 动图
- 代码
- 3.2.3 前后指针法
- 思路
- 动图
- 代码
- 快速排序优化
- 优化1:
- 代码
- 优化2
- 代码
- 快速排序非递归版
- 思路:
- 代码
- 4.归并排序
- 思路
- 动图
- 4.1归并排序递归法
- 4.2归并排序非递归版(1)
- 4.3归并排序非递归版(2)
- 5.记数排序
- 思想
- 5.1记数排序
- 总结
排序相关概念
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递
减的排列起来的操作。稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
1.插入排序
1.1 直接插入排序
动图演示:
思路:
就像我们打牌时,一个一个插入一样
具体思想就是,我们在一个有序的数组中插入一个数,那么我们是将这个数在有序数组中依次比较,那么我们怎么将这个数据插入其中呢? 我们采用覆盖的方法,就是将后一个数等于前一个数这样依次覆盖,这样就会给我们要插入的数据留下一个空位,然后插入即可,可能大家会有疑问,一个无序的数组我们怎么插入呢??? 其实我们将第一个数看成一个数组,那么他不就是有序的吗,然后将第二个数插入,再插入第三个数. . . . . . .,这样就完成了排序
代码实现
void InsertSort(int* a, int n)
{for (int i = 0; i < n-1; i++)//可以分为n-1次插入,第一次就认为是有序,{int end = i;//记录每次插入的有序数组最后一个位置int tmp = a[end + 1];//记录插入的数据while (end >= 0)//一直比较到0下标位置{if (tmp< a[end]){a[end + 1] = a[end];//往后覆盖--end;}else{break;}a[end + 1] = tmp;//插入,不能放到else中,因为可能是else中break出来 也有可能是end>=0为假出来,也就是说tmp是在有序数组中是最小的数} // 放这也行 a[end + 1] = tmp;//插入,不能放到else中,因为可能是else中break出来 也有可能是end>=0为假出来,也就是说tmp是在有序数组中是最小的数}
}
复杂度
空间复杂度:
O(1),未开辟额外空间
时间复杂度:
最好的情况:已经有序的情况下,只需遍历一次,即O(N)
最坏的情况:每插入一个数都要遍历,即F(N)=1+2+3+4+…+N=N(N-1)/2=O(N^2).
1.2希尔排序(缩小增量排序)
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。
动图演示:
画图讲解:
从图中可知gap越大,分成的小数组越多,希尔排序就是通过这种小区间预排,让数组似有序化,从而减轻最后一次直接插入排序(gap=1)的遍历次数,从而提升速度!
代码
void ShellSort(int* a, int n)
{int gap = n;while (gap > 1){gap = gap / 3 + 1;//避免gap=0,导致最后一次不为1,所以加上1,当为1的时候就是插入排序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;}}}
}
希尔排序的特性总结:
- 希尔排序是对直接插入排序的优化。
- 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
- 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算.但是通过大量实验证明O(N^1.3)左右
2.选择排序
2.1.1单向选择排序
思路
就是再数组中找出最值,然后放入最左边,依次排放
动图演示:
代码:
void SelectSort(int* a, int n)
{for (int i = 0; i < n; i++){int index = i;for (int j = i + 1; j < n; j++)//找到最值的下标{if (a[index] < a[j]){index = j;}}Swap(&a[index], &a[i]);}
}
2.1.2 双向选择排序(对单向选择排序的优化)
代码
void SelectSort2(int* a, int n)
{int begin = 0;int end = n-1;while(begin<end){int maxi = begin;int mini = begin;for (int j = begin; j <= end; j++){if (a[maxi] < a[j]){maxi = j;}if (a[mini] > a[j]){mini = j;}}Swap(&a[maxi], &a[begin]);if (mini == begin)//因为上面的交换,maxi和begin交换了,当mini==begin时,说明begin是最小值,但是和maxi交换了,所以maxi现在是最小值{mini = maxi; }int temp = 0;Swap(&a[mini], &a[end]);begin++;end--;}
}
复杂度
时间复杂度:O(N^2)
空间复杂度:O(1)
2.2 堆排序
看我这篇文章:二叉堆的实现(含堆排序讲解),点击直达
3.交换排序
3.1冒泡排序
思路:
相邻数据依次比较交换
代码:
void BubbleSort(int* a, int n)
{for (int j = 0; j < n - 1; ++j){int exchange = 0;for (int i = 1; i < n - j; ++i){if (a[i - 1] > a[i]){Swap(&a[i - 1], &a[i]);exchange = 1;}}if (exchange == 0)//exchange==0说明数据已经有序{break;}}
}
3.2 快速排序
思想;
任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
重点解决,单趟key的位置
代码模板:
void QuickSort(int* a, int begin, int end)
{while (begin >= end)return;int keyi = PartSort(a, begin, end);QuickSort(a, begin, keyi - 1);QuickSort(a, keyi + 1, end);}
和二叉树的前序遍历很像!就是将数组分成最小单元,然后最小单元有序,那么上一层key值两边均有序,以此类推,完成排序!!
3.2.1 hoare版本
思路
先从key的反方向找小,假设key在左边,我们先让右边的right找到比key小的暂停,然后让左边的left找大,找到之后和right交换,然后right继续找,left然后也找,找到就交换,知道相遇之后,相遇位置的左边就是都比key小的,右边都是比key大的,我们让key的值和相遇点交换,然后key的位置到相遇点,这就完成了单趟交换,然后以key的左边,右边分别为一个数组,再完成交换!!
动图
代码:
int PartSort(int* a, int begin, int end)//hoare版本 这种就像是先走完再找key位置
{int left = begin;int keyi = left;int right = end;while (left < right){while (left < right && a[right] >= a[keyi])//找小//这里为什么从keyi的反方向开始呢?? 因为在left和right相遇的时候,相遇的值的比a[keyi]小或者相等,即使right一路畅通无阻跑到keyi位置也没关系//这就是因为right就是找小的,最极端也就是相等,因为找小的先走的,所以相遇的时候一定是小的,如果从keyi的正方向走的话,相遇一定是大的啊,大的和a[keyi]交换肯定不对啊//我们就是需要找小的交换, 你找个大的算怎么回事呢?{right--;}while (left < right && a[left] <= a[keyi])//找大{left++;}Swap(&a[right], &a[left]);}Swap(&a[keyi], &a[left]);keyi = left;return keyi;
}
3.2.2 挖坑法
思路
将key的位置当做一个坑,然后right指针开始找小,找到小的就放入坑中(就是交换),然后left找大,找到就放入坑中,直到两个指针相遇停止,那么相遇的这个位置一定是坑,将key原始数据放入坑中,key的位置变成改坑的位置,和hoare版本很像,但是更容易理解!这就完成了单趟排序!
动图
代码
int PartSort(int* a, int begin, int end)//挖坑法,有效的帮助你理解 这种就像边走边找key位置
{int key = a[begin];int piti = begin;//坑位while (begin < end){//从左找大,找到之后把它放坑位里,然后这里成为新的坑位while (begin < end && a[end] >= key){end--;}a[piti] = a[end];//将大值放入坑位piti = end;//坑位到原来大值的位置//从左找小,找到之后把它放坑位里,然后这里成为新的坑位while (begin < end && a[begin] <= key){begin++;}a[piti] = a[begin];//将小值放入坑位piti = begin;//坑位到原来小值的位置}a[piti] = key;return piti;//最后的坑位就是我们的key值
}
3.2.3 前后指针法
思路
前指针cur,后指针prev,前指针在前面依次找比key值小的,找到后就和prev交换位置,然后prev+1,重复执行,最后prev的位置就是key的分界点,所以让那个key和prev交换
动图
代码
int PartSort3(int* a, int begin, int end)//前后指针法 代码最简洁
{int cur = begin+1;int prev = begin;int keyi = begin;for (int i = begin; i < end; i++){if (a[cur] <= a[keyi]&& prev++!=cur)//找大,找到最后prev往后腾一个位置和cur交换位置,就是cur去找大值,找到就往prev那边放,放完prev就跑一个{Swap(&a[prev], &a[cur]);}cur++;}Swap(&a[prev], &a[keyi]);//将keyi对应的数值和prev交换,让keyi变成分界线!keyi = prev;return keyi;
}
快速排序优化
优化1:
如果我们学过二叉树的话,我们应该key在数组中间的话,这个遍历的深度就越小,而且如果每次选到的key是这组数的最大值或者最小值的话,快排的效率就会变得非常低,几乎接近于O(N²),还可能因为递归深度太深导致栈溢出。,所以我们采取三数取中来进行优化!
代码
int GetMidIndex(int* a, int begin, int end)//三值取中
{int mid = (begin + end) / 2;if (a[begin] < a[mid]){if (a[mid] < a[end])return mid;else if (a[begin] < a[end])return end;elsereturn begin;}else //(a[begin] >= a[mid]){if (a[mid] > a[end])return mid;else if (a[begin] < a[end])return begin;elsereturn end;}
}
优化2
要知道,快排算法是不断递归来排一个数的左右两边,所以当快排算法越接近与结束的时候,左右两边数组越接近有序。
而快排算法对于接近有序的序列是没有任何效率上的提高的,但是我们知道插入排序时,如果一段序列越接近于有序,插入排序的效率就越高。
所以我们可以采用插入排序进行优化!!
代码
void QuickSort(int* a, int begin, int end)
{while (begin >= end)return;while (end - begin > 20)//优化,因为越小递归次数越多,就得不偿失了{int keyi = PartSort3(a, begin, end);QuickSort(a, begin, keyi - 1);QuickSort(a, keyi + 1, end);}InsertSort(a, end - begin+1);//数组下标差+1等于数组个数}
快速排序非递归版
思路:
在设计单趟排序的时候,我们要知道改数组的头和尾,所以我们通过栈来实现非递归,将头和尾依次压入栈中,然后在单趟排序之前出栈,得到一个keyi值,这样我们又知道左右子区间的头和尾,继续进行压栈,这类似于二叉树的层序遍历
代码
void QuickSortNonR(int* a, int begin, int end)//快排非递归方法
{Stack q;StackInit(&q);StackPush(&q, end);StackPush(&q, begin);while (!StackEmpty(&q)){int left = StackTop(&q);StackPop(&q);int right = StackTop(&q);StackPop(&q);int keyi = PartSort3(a, left, right);//区间[keyi-1,left] keyi [keyi+1,right]if (keyi + 1 < right){StackPush(&q, right);//因为先top的是left,所以left边最后放StackPush(&q, keyi+1);}if (keyi - 1 > left){StackPush(&q, keyi-1);//因为先top的是left,所以left边最后放StackPush(&q, left);}}StackDestroy(&q);
}
4.归并排序
思路
归并排序的思路就是先将所有元素分成最小单元(一个),然后再通过指针在每个有序单元数组中挑选元素比较,将较小的元素放到辅助数组中即可,递归版就是通过递归得到最小元素单元(类似后序遍历),非递归就是直接找最小元素进行比较放入辅助数组
动图
4.1归并排序递归法
void _MargeSort(int* a, int begin, int end, int* tmp)//递归法
{if (begin >= end)//截止条件return;int mid = (begin + end) / 2;_MargeSort(a, begin, mid,tmp);_MargeSort(a, mid + 1, end, tmp);//左右区间[begin,mid] [mid+1,end]int begin1 = begin, end1 = mid;int begin2 = mid + 1, end2 = end;int i = begin1;//记录归并的两个数组的头下标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 MargeSort(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){printf("malloc fail!");exit(-1);}_MargeSort(a, 0, n - 1, tmp);//MargeSortNone1(a, n, tmp);//MargeSortNone2(a, n, tmp);free(tmp);
}
4.2归并排序非递归版(1)
void MargeSortNone1(int* a, int n,int* tmp)//非递归法
{int gap = 1;//gap是设置归并数组的长度while (gap <= n)//gap>n的时候就已经排序好了{for (int i = 0; i < n; i += gap * 2)//gap*2的意思是每次加上两个数组长度,因为归并是两个数组之间的{//左右数组的范围[i,i+gap-1] [i+gap,i+gap*2-1] i是归并的两个数组初始位置,数组的第一个下标int begin1 = i, end1 = i + gap - 1;int begin2 = i + gap, end2 = i + 2 * gap - 1;int j = begin1;//这里是修正边界,因为我们通过非递归法,边界一定是2的次方倍,所以会导致end1越界,或者begin2越界,或者end2越界if (end1 >= n)//end1越界{end1 = n - 1;//修正边界//将右区间变成一个不存在的区间,这就不会进入下面的第一个循环begin2 = n;end2 = n - 1;}else if (begin2 >= n){//将右区间变成一个不存在的区间,这就不会进入下面的第一个循环begin2 = n;end2 = n - 1;}else if (end2 >= n){end2 = n - 1;//修正边界}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, tmp, sizeof(int) * n);//每次进行一个完整的归并结束后将结果cpy到原数组中gap *= 2;//gap数组(归并数组)长度翻倍}
}
void MargeSort(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){printf("malloc fail!");exit(-1);}//_MargeSort(a, 0, n - 1, tmp);MargeSortNone1(a, n, tmp);//MargeSortNone2(a, n, tmp);free(tmp);
}
方法一就是通过修正边界和设置一个不存在的右区间
然后每次完整的归并结束复制的原数组
4.3归并排序非递归版(2)
void MargeSortNone2(int* a, int n, int* tmp)
{int gap = 1;while (gap <= n){for (int i = 0; i < n; i += gap * 2){int begin1 = i, end1 = i + gap - 1;int begin2 = i + gap, end2 = i + 2 * gap - 1;int j = begin1;if (end1 >= n || begin2 >= n)//和1的区别就是这里不修正边界,这里是越界的区间直接不归并{break;}else if (end2 >= n)//右区间越界就修正一下{end2 = n - 1;}int m = end2 - begin1 + 1;//这里是计算cpy到原数组的长度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) * m);//每归并比较一次就cpy到原数组,最容易出错的地方}gap *= 2;}
}
void MargeSort(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){printf("malloc fail!");exit(-1);}//_MargeSort(a, 0, n - 1, tmp);//MargeSortNone1(a, n, tmp);MargeSortNone2(a, n, tmp);free(tmp);
}
方法二是之间让越界的部分直接不去归并
但是这里就得每一次小归并就得复制到原数组
5.记数排序
思想
源于哈希的思想,通过数据的波动差值创建一个数组count,然后在原数组中找到最大值和最小值,将每个数组元素-min(最小值)存到对应的count数组下标中,然后在遍历count取的排序的数组,取出的元素+min,因为我们存的时候就是下标=元素-min,所以我们取的下标元素下标=元素-min,所以元素=下标+min
5.1记数排序
不适用于上下波动较大的数组
只是用整型排序
void CountSort(int* a, int n)
{int max = a[0], 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 range = max - min + 1;//求出数据的波动范围int* count = (int*)malloc(sizeof(int) * range);if (count == NULL){printf("malloc fail");exit(-1);}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) | O(N) | O(N^2) | O(1) | 稳定 |
希尔排序 | O(N*logN)-O(N^2) | O(N^1.3) | O(N^2) | O(1) | 不稳定 |
选择法排序 | O(N^2) | O(N^2) | O(N^2) | O(1) | 不稳定 |
堆排序 | O(N*logN) | O(N*logN) | O(N*logN) | O(1) | 不稳定 |
冒泡排序 | O(N^2) | O(N) | O(N^2) | O(1) | 稳定 |
快排 | O(N*logN) | O(N*logN) | O(N^2) | O(1) | 不稳定 |
归并排序 | O(N*logN) | O(N*logN) | O(N*logN) | O(N) | 稳定 |
计数排序 | O(N) | O(N) | O(N) | O(countsize) | 不稳定 |
八大基本排序(详解)相关推荐
- 八大排序 详解(下)——指向函数的指针 的使用
<八大排序 详解(上)> <八大排序 详解(中)> 紧接前两篇博文,我们这篇博文所要讲解的是我们前两篇博文编写的所有函数的使用.生成随机数组的函数的讲解以及一种及其凶悍的调用方 ...
- pandas dataframe中的列进行重新排序、倒排、正排、自定义排序详解及实践
pandas dataframe中的列进行重新排序,pandas dataframe列重排.倒排.正排.自定义排序详解及实践 实施数据构建: import pandas as pd import nu ...
- 希尔排序基础java代码_java 算法之希尔排序详解及实现代码
摘要:这篇Java开发技术栏目下的"java 算法之希尔排序详解及实现代码",介绍的技术点是"希尔排序详解.实现代码.希尔排序.Java.实现.代码",希望对大 ...
- Linux 中使用 sort 指令分组排序详解
Linux 中使用 sort 指令分组排序详解 sort 中进行分组排序主要用到的选项为 -k,此文,我们着重于该选项的使用方式,用到的其它选项不做解释,有兴趣的同学可以查看帮助文档 1. 数据准备 ...
- Java八大基本数据类型-详解
Java八大基本数据类型-详解 3.5 八大基本数据类型 Java中的八大基本数据类型在我们学习Java编程中非常重要,可以说是Java编程的起步.这节会为大家非常细致地介绍如何使用这八大基本数据类型 ...
- c语言排序常用问题,【更新中】【排序详解】解决排序问题(以C语言为例)
[更新中][排序详解]解决排序问题(以C语言为例) [更新中][排序详解]解决排序问题(以C语言为例) 文章目录 排序的相关概念 简单排序 一.插入排序: (一)插入排序基本思想 (二)插入排序基本操 ...
- 【排序】什么都能排的C语言qsort排序详解【超详细的宝藏级别教程】深度理解qsort排序
[排序]什么都能排的C语言qsort排序详解[超详细的宝藏级别教程]深度理解qsort排序 作者: @小小Programmer 这是我的主页:@小小Programmer 在食用这篇博客之前,博主在这里 ...
- shell编程数组与冒泡算法排序详解
shell编程数组与冒泡算法排序详解 一 数组的四种表达方式 二 数组的增删改查操作 三 数组传参 3.1 主程序向函数传数组参数 3.2 从函数返回数组到主程序 四 冒泡算法排序 一 数组的四种表达 ...
- C++中的结构体vector排序详解
C++中的结构体vector排序详解 使用sort函数对一个vector很常用,前提是通文件中必须包含#include ,但是针对结构体vector排序则需要进行一定的改动.具体事例如下所示: // ...
最新文章
- 自学web前端的方法都有哪些?新手怎么学HTML5
- Go 导入当前项目下的包
- 设计模式——Template Method模板方法
- 2048java课程设计报告_软件工程——Java版2048游戏学习报告
- python下面的代码_求下面python代码的差别。
- Ubuntu上安装jdk出现的错误
- 数据结构 队列Queue
- htons htonl ntohl ntohs 的区别和作用
- 提高网页效率的14条准则
- 全新卡盟系统PHP版 集成易支付_2020易支付系统/聚合支付系统源码/免签约聚合支付系统/集成易支付相互对接...
- 考勤系统与服务器链接,考勤机怎么连接服务器
- MATLAB全局变量
- MySQL的子查询(二十)
- 常喝酸奶,远离糖尿病
- cf_global_round7
- 护卫神6588端口提权
- 百度关键词推广选词技巧,你了解多少?
- 六级单词--词根词缀篇
- reading 摘录二
- 浏览器内核学习笔记二
热门文章
- Ubuntu9.04配置命令宝典
- UVALive 6555 Playing Fair with Cryptography(细节处理)
- “COMSOL Multiphysics多物理场仿真技术与应用” 电化学专题
- Ubuntu 13.04下安装WPS for Linux
- Linux各目录的作用
- Python中如何编写接口,以及如何请求外部接口
- pycharm删除的文件找回步骤
- 网络分析仪测试线损_求e5071c网络分析仪校50欧姆阻抗与线损方法,标准...
- Adam优化算法中的指数移动平均
- 高光谱图像分析:分类 II