文章目录
总结
时间复杂度
【平均情况下】
- 快速、希尔、归并和堆排序的时间复杂度为O(nlog2n),其他都是O(n2)
- 其他都是O(n2)
- 一个特殊的是基数排序,时间复杂度为O(d(n+rr))
其中d为关键字位数,r为一位的关键字取值范围,n为数量
【最坏情况下】
- 快速排序的时间复杂度为O(n2)
- 其他都和平均情况相等
【最好的情况下】直接插容易插变成O(n),起泡起的好变成O(n)
“容易插”、“起的好”都是指初始序列已经有序
【助记】教官说:“快(快速排序)些(希尔排序)以nlog2n的速度归(归并排序)队(堆排序)”
【其他结论】借助于“比较”进行排序的算法,在最坏的情况下能达到最好的时间复杂度为O(nlog2n)
空间复杂度
- 快速排序O(log2n)
- 归并排序O(n)
- 基数排序O(rd)
- 其他都是O(1)
稳定性
【助记】考研复习痛苦啊,心情不稳定(不稳定的算法),快(快速排序)些(希尔排序)选(简单选择排序)一堆(堆排序)好友来聊天吧
这4种不是稳定的,其他自然都是稳定的
排序原理
- 经过一趟排序,就能保证一个元素到达最终位置:起泡、快速(交换类的两种),简单选择、堆(选择类的两种)
- 排序方法的元素比较次数和原始序列无关:简单选择排序、折半插入排序
- 排序方法的排序趟数和原始序列有关:交换类的排序
插入排序
直接插入排序
void InsertSort(int arr[], int n) {
int tmp, i, j;
for (i=1; i<n; ++i) {
tmp = arr[i];
j=i-1;
while (j>=0 && tmp<arr[j] ) {
arr[j+1] = arr[j];
--j;
}
arr[j+1] = temp;
}
}
【时间复杂度】
- 最坏的情况:O(n^2)
两层循环,外层循环i=5的话,内层循环4次;因此执行次数n(n-1)/2 - 最好的情况:O(n)
两层循环,最好的情况,内层循环不执行–>O(n)
【空间复杂度】O(1),额外的空间只有一个tmp
【缺点】考虑每一个元素时,该元素插入、其他元素移动后(一趟下来),元素不一定会移到最终的位置
如:插入排序考虑到1、2、3、4、5、0时,这时候要将0插入到第一位,其他元素都要往后位移,所以1、2、3、4、5在前面的操作中都没有移动到最后的位置
折半插入排序
【折半插入排序】采用折半查找来寻找插入位置的,然后在插入
【与直接插入排序比较】折半插入排序适合记录较多的场景
- 折半插入排序只是在寻找插入位置上面所花的时间减少
- 记录在移动次数上还是和直接插入排序一样–>所以时间复杂度也是一样
- 折半插入排序的记录比较次数与初始序列无关
因为每趟排序折半寻找插入位置时,折半次数是一定的(都是在low>high时结束),折半一次就要比较一次,所以比较次数是一定的
【时间复杂度】
- 最好情况O(nlog2n)
- 最差情况O(n^2)
- 平均情况为O(n^2)
【空间复杂度】O(1)
2-路插入排序
2-路插入排序是在折半插入排序的基础上改进的
- 目的:减少排序过程中移动记录的次数,但为此需要n个记录的辅助空间
- 缺点:2-路插入排序只能减少移动记录的次数,而不能绝对避免移动记录
- 缺点:当arr[1]是待排序记录中关键字最小或最大的记录时,2-路插入排序就完全失去了它的优越性
- 改进:若希望在排序过程中不移动记录,只有改变存储结构,进行表插入排序
【具体做法】
- 另设一个数组d[]
- 首先将arr[1]赋值给d[1],并将d[1]看成是排好序的序列中处于中间位置的记录
- 然后从arr第2个记录起依次插入到d[1]之间或之后的有序序列中
【实现】可以将d看成是一个循环向量,并设两个指针first和final分别指示排序过程中得到的有序序列中的第一个记录和最后一个记录在d的位置
表插入排序
【表插入排序】用静态链表的方式作为待排记录序列的存储结构
【优点】避免了插入排序中,元素的移动
#define SIZE 100 //静态链表容量
typedef struct{
int rc;//记录项
int next;//指针项
}SLNode; //表结点类型
typedef struct{
SLNode r[SIZE]; //0号单元为表头结点
int length; //链表当前长度
}SLinkListType; //静态链表类型
void Arrange(SLinikListType &SL){
//根据静态链表SL中各结点的指针调整记录位置,使得SL中记录按关键字非递减有序顺序排列
int p = SL.r[0].next; // p指示第一个记录的当前位置
for(int i=1;i<SL.length;++i){
while(p<i) p=SL.r[p].next;
q = SL.r[p].next;
if(p!=i){
SL.r[p]←→SL.r[i];
SL.r[i].next = p;
}
p = q;
}
}//Arrange
希尔排序
【希尔排序】又叫做缩小增量排序
【增量】
- 增量序列的最后一个值一定是1
- 增量序列中的值没有除1之外的公因子(尽量不要有公因子)
如8、4、2、1这样的增量序列,因为8、4、2有公因子2
【为什么尽量不要有公因子呢?】有了这个约束条件,使得每一趟排序更有可能保持前一趟排序已排好的效果。希尔排序最初以N/2为间隔的低效性就是因为它没有遵守这个准则
【时间复杂度】平均情况O(nlog2n)
void ShellSort(int arr[], int n) {
int temp,i,j;
int gap;
for (gap=n/2; gap>0; gap/=2) {
for (i=gap; i<n; ++i) {
temp = arr[i];
int j;
for (j=i; j>=gap && arr[j-gap]>temp; j-=gap) {
arr[j] = arr[j-gap];
}
arr[j] = temp;
}
}
}
选择排序
简单选择排序
【简单选择排序】选择一个最小记录,和第一个交换
【时间复杂度】两层循环O(n)
执行次数为(n-1+1)(n-1)/2 = n(n-1)/2
【空间复杂度】额外空间只有一个tmp,O(1)
void SelectSort(int arr[], int n) {
int i,j,k;
int temp;
for (i=0; i<n; i++) {
k =i;
for (j=i+1; j<n; ++j) {
if (arr[k] > arr[j])
k =j;
}
temp = arr[i];
arr[i] = arr[k];
arr[k] = temp;
}
}
树形选择排序
【树形选择排序(Tree Selection Sort),又称锦标赛排序(Tournament Sort)】是一种按照锦标赛的思想进行选择排序的方法
【步骤】
- 对n个记录关键字进行两两比较
- 在其中⌈n/2⌉个较小者之间再进行两两比较
- 重复1、2步,直至选出最小关键字记录为止,得到下图的a
【排序过程】
- a选出最小关键字13,选出后将13删除,树部分重新进行比赛
- b选出次小关键字27
- 选出居第三的关键字为38
【时间复杂度】O(nlogn)
【评价】这种排序方法辅助存储空间较多、和“最大值”进行多余的比较等缺点。在此基础上改进诞生了堆排序
堆排序
用堆来选择最大最小的关键字,再插入
链接:https://blog.csdn.net/summer_dew/article/details/83075906
交换排序
冒泡排序
【冒泡排序】每一次把最大的关键字移到了最右边,像气泡从水底冒到了水面一样
【时间复杂度】
- 最坏情况:O(n^2)
基本操作执行的次数n-i,i的取值为1~n-1 --> 基本操作总的执行次数为(n-1+1)(n-1)/2=n(n-1)/2 - 最好情况:O(n)
待排序列即有序,第二层循环不进行,O(n)
【空间复杂度】额外辅助空间只有tmp一个,O(1)
void BubleSort(int arr[], int n) {
int i,j,flag;
int tmp;
for (i=n-1; i>=1; --i) {
flag = 0;
for (j=1; j<=i; ++j) {
if (arr[j-1] > arr[j] ) {
tmp = arr[j];
arr[j] = arr[j-1];
arr[j-1] = temp;
flag = 1;
}
}
if (flag ==0 ) return ;
}
}
快速排序
【快速排序】教官说:“第一个同学出列,其他人以他为中心,比他矮的到他的左边,比他高的到他的右边”
【一趟过程】一个序列
- 把一个序列的第一个人取出来,放到tmp
- 定义一个i、j。i是序列的第一个位置,j是序列的最后一个位置(i从前面往后面走,j从后面往前面走)
- j从后面往前走,直到arr[j]的身高比tmp小,把arr[j]赋值到i的位置上
- i从前面往后走,直到arr[i]的身高比tmp大,把arr[i]赋值到j的位置上
- 重复3、4两个步骤。直到i==j时,把tmp放入
- 一趟结束。这一趟将序列以第一个人为哨兵,分成了两部分,哨兵的左边比他都矮,哨兵的右边比他都高
- 递归左边的序列、右边的序列,重复1-6步
【时间复杂度】快速排序的排序趟数和初始序列有关
- 最好的情况:O(nlog2n) 待排序列越接近无序,本算法效率越高
- 最坏情况:O(n^2)待排序列越接近有序,本算法效率越低
- 平均时间复杂度:O(nlog2n)。就平均时间而言,快速排序是所有排序算法中最好的
【空间复杂度】O(log2n) - 递归算法,空间消耗大,需要用到系统栈
//low,high 当前子序列的范围
void QuickSort(int arr[], int low, int high) {
int temp;
int i = low, j=high;
if (low<high) {
temp = arr[low];
while (i<j) {
//满足条件的情况下,将j往前移动
while (j>i && arr[j]>=temp) --j;
//把比tmp小的关键字赋值到i
if (i<j) {
arr[i] = arr[j];
++i;
}
//满足条件的情况下,将i后移
while (i<j && arr[i]<temp) ++i;
//把 比tmp大的i 赋值到j上
if (i<j) {
arr[j] = arr[i];
--j;
}
}
arr[i] = tmp;
QuickSort(arr, low, i-1);
QuickSort(arr, i+1, high);
}
}
归并排序Merging Sort
二路归并排序
【步骤】
- 将原始序列看成好几个序列,一个序列只有一个元素,显然这些序列都是有序的
- 序列之间进行两两归并,形成几个有序的二元组
- 重复第二步,直到只剩下一个序列时,排序结束
【时间复杂度】O(nlog2n)
- 归并排序中可选用merge()函数作为“归并操作”的基本操作,merge()的作用是将两个有序序列归并成一个整体有序的序列
- 归并操作即为将待归并表中的元素复制到一个存储归并结果的表中的过程
- 在顺序表中,merge()函数的归并操作执行次数为要归并的两个子序列中元素个数之和
- 归并操作如下:
- 第1趟归并需要执行2(n/2)=n次基本操作(其中2为两子序列元素个数之和,n/2为要归并的子序列对的个数;每个子序列对执行一次merge()函数,也就是2次基本操作)
- 第2趟归并需要执行4(n/4)=n次基本操作
- 第3趟归并需要执行8(n/8)=n次基本操作
- 第k趟归并需要执行2k *n/2k = n次基本操作
- 当n/2k=1时,即需要归并两个子序列长度均为原序列的一般,只需要执行一次merge()函数归并排序即可结束。此时,k=log2n,即总共需要进行log2n趟排序,每趟排序执行n次基本操作
- 整个归并排序总的基本操作执行次数为nlog2n,时间复杂度O(nlog2n)
【空间复杂度】归并排序需要转存整个待排序序列,因此空间复杂度为O(n)
【递归形式的代码】
// 归并操作
void Merge(int SR[],int &TR[], int i, int m, int n ) {
// 将有序的SR[i..m]和SR[m+1...n]归并为有序的TR[i...n]
for (j=m+1, k=i; i<=m && j<=n; ++k) {
if (SR[i]<SR[j]) TR[k] = SR[i++];
else TR[k] = SR[j++];
}
if (i<=m) TR[k...n] = SR[i..m]; //将剩余的SR[i...m]复制到TR
if (j<=n) TR[k...n] = SR[j..n]; //将剩余的SR[j...n]复制到TR
}
void MSort(int SR[], int &TR1[], int s, int t) {
//将SR[s..t]归并排序为TR1[s..t]
if (s==t) TR1[s] = SR[s];
else {
m = (s+t)/2; //将SR[s..t]平分为SR[s..m]和SR[m+1..t]
MSort(SR, TR2, s, m); //递归的将SR[s..m]归并为有序的TR2[s..m]
MSort(SR, TR2, m+1, t); //递归地将SR[m+1...t]归并为有序的TR2[m+1..t]
Merge(TR2, TR1, s, m, t); //将TR2[s..m]和TR2[m+1..t]归并到TR1[s..t]
}
}
void MergeSort(SqList &L) {
//对顺序表L作归并排序
MSort(L.r, L.r, 1, L.length);
}
基数排序
【思想】“多关键字排序”
【两种实现方法】
- 最高位优先:先按最高位排成若干个子序列,再对每个子序列按次高位排序
例子:扑克牌,先按花色排成4个子序列,再对每种花色13张牌进行排序 - 最低位优先:这种方式不必分成子序列,每次排序全体元素都参与。可以不通过比较,而通过“分配”和“收集”来进行排序
例子:扑克牌,按数字将牌分配到13个桶中,然后从第一个桶开始依次收集;再将收集好的牌按花色分配到4个桶中,然后还是从第一个桶开始依次收集。经过两次“分配”和“收集”操作,最终使牌有序
【时间复杂度】平均和最坏情况下都是O(d(n+rd))
【空间复杂度】O(rd)
- n为序列中元素的个数
- d为元素的关键字位数(如930,有三位数,d=3)
- rd为关键字每一位的取值范围(如关键字为930,每一位的取值都0-9,所以rd=10)
【评价】
- 技术排序适合的场景是序列中的元素树很多,但组成元素的关键字的取值范围较小,如数字0-9是可以接受的
- 如果关键字的取值范围也很大,如26个字母,并且序列中大多数元素的最高位关键字都不相同,那么这时可以考虑使用“最高位优先法”。先根据最高位排成若干子序列,然后分别对这些子序列进行直接插入排序