排序
Sorting Algorithm Complexities| 算法种类 | 最好情况 | 平均情况 | 最坏情况 | 空间复杂度 | 是否稳定 |
|---|
| 直接插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | 是 |
| 冒泡排序 | O(n) | O(n^2) | O(n^2) | O(1) | 是 |
| 简单选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 否 |
| 希尔排序 | | 取决于间隔序列 | | O(1) | 否 |
| 快速排序 | O(nlogn) | O(nlogn) | O(n^2) | O(logn) | 否 |
| 堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 否 |
| 归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 是 |
| 基数排序 | O(d(n + r)) | O(d(n + r)) | O(d(n + r)) | O(n + r) | 是 |
插入排序
算法思想
- 算法思想: 每次将一个待排序的记录按其关键字大小插入到前面已排好的子序列中,直到全部记录插入完成。
算法实现
1 2 3 4 5 6 7 8 9 10 11
| void InsertSort(int A[],int n){ int i,j,temp; for(i=1;i<n;i++) if(A[i]<A[i-1]){ temp=A[i]; for(j=i-1;j>=0&&A[j]>temp;--j) A[j+1]=A[j]; A[j+1]=temp; } }
|
1 2 3 4 5 6 7 8 9 10 11 12
| void InsertSort(int A[],int n){ int i,j; for(i=2;i<=n;i++) if(A[i]<A[i-1]){ A[0]=A[i]; for(j=i-1;A[0]<A[j];--j) A[j+1]=A[j]; A[j+1]=A[0]; } }
|

- 空间复杂度:O(1)
- 时间复杂度: 主要来自对比关键字,移动元素。
- 最好时间复杂度:
- 最坏时间复杂度:
平均时间复杂度:O(n2) (最好和最坏加和取半)- 算法稳定性:
希尔排序
思想
- 希尔排序(Shell Sort): 先追求表中元素部分有序,再逐渐逼近全局有序。
希尔排序: 先将待排序表分割成若干形如L[i,i+d,i+2d,...,i+kd]的特殊子表,对各个子表分别进行直接插入排序。缩小增量d,重复上述过程,直到d=1为止。

提示: 一般是缩小一半(但是考研五花八门)
算法实现
1 2 3 4 5 6 7 8 9 10 11 12 13
| void ShellSort(int A[],int n){ int d,i,j; for(d=n/2;d>=1;d=d/2) for(i=d+1;i<=n;++i) if(A[i]<A[i-d]){ A[0]=A[i]; for(j=i-d;j>0&&A[0]<A[j];j-=d) A[j+d]=A[j]; A[j+d]=A[0]; } }
|
算法性能分析
时间复杂度: 和增量序列d1,d2,d3,...的选择有关,目前无法用数学手段证明确切的时间复杂度,最坏时间复杂度为O(n2),当n在某个范围内时,可达O(n1.3)
冒泡排序
原理
算法实现
1 2 3 4 5 6
| void swap(int &a,int &b){ int temp = a; a = b; b = temp; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| void BubbleSort(int A[],int n){ for(int i=0;i<n-1;i++){ bool flag=false; for(int j=n-1;j>i;j--) if(A[j-1]>A[j]){ swap(A[j-1],A[j]); flag=true; } if(flag==false) return; } }
|

算法性能分析
- 空间复杂度:O(1)
- 冒泡排序是稳定的

快速排序
算法思想
- 算法思想: 在待排序表L[1...n]中任取一个元素pivot作为枢轴(或基准,通常取首元素),通过一趟排序将待排序表划分为独立的两部分L[1...k−1]和L[k+1...n],使得L[1...k−1]中的所有元素小于pivot,l[k+1...n]中的所有元素大于等于pivit,则pivot放在了最终未知L(k)上,这个过程称为一次划分。然后分别递归地对两个子表重复上述过程,直至每部分内只有一个元素或空为止,即所有元素放在其最终位置上。
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| int Partition(int A[],int low,int high){ int pivot=A[low]; while(low<high){ while(low<high&&A[high]>=pivot) --high; A[low]=A[high]; while(low<high&&A[low]<=pivot) ++low; A[high]=A[low]; } A[low]=pivot; return low; }
|
1 2 3 4 5 6 7 8
| void QuickSort(int A[],int low,int high){ if(low<high){ int pivotpos = Partition(A,low,high); QuickSort(A,low,pivotpos-1); QuickSort(A,pivotpos+1,high); } }
|
算法效率分析


时间复杂度:
最好时间复杂度=O(nlog2n)最坏时间复杂度=O(n2)
空间复杂度:
最好空间复杂度=O(log2n)最坏空间复杂度=O(n)
排序情况:
- 最好: 每次选的枢轴元素都能将序列划分成均匀的两部分
- 最坏: 若序列原本就有逆序,则时间,空间复杂度最高
稳定性: 快速排序是不稳定的
注:
- 408原题中说,对所有尚未确定最终位置的所有元素进行一遍处理称为一趟排序,因此一次划分= 一趟排序。
- 一次划分可以确定一个元素的最终位置,而一趟排序也许可以确定多个元素的最终位置。
注意: 快速排序是所有内部排序算法平均性能最优的排序算法
简单选择排序
算法思想
- 选择排序: 每一趟在待排序元素中选取关键字最小(或最大)的元素加入有序子序列。
算法实现
1 2 3 4 5 6 7 8 9 10 11
| void SelectSort(int A[],int n){ for(int i=0;i<n-1;i++){ int min=i; for(int j=i+1;j<n;j++) if(A[j]<A[min]) min=j; if(min!=i) swap(A[i],A[min]); } }
|
1 2 3 4 5 6
| void swap(int &a,int &b){ int temp = a; a = b; b = temp; }
|
算法性能

- 稳定性: 不稳定
- 适用性: 既可以用顺序表,也可用链表
堆排序
算法思想
- 若n个关键字序列L[1...n]满足下面某一条性质,则称为
堆(Heap):- 若满足:L(i)≥L(2i)且L(i)≥L(2i+1),(1≤i≤2n)——大根堆(大顶堆)
- 若满足:L(i)≤L(2i)且L(i)≤L(2i+1),(1≤i≤2n)——小根堆(小顶堆)



- 选择排序: 每一趟在待排序元素中选取关键字最小(或最大)的元素加入有序子序列。
- 建立大根堆:
- 思路:
- 把所有非终端结点都检查一遍,是否满足大根堆的要求,如果不满足,则进行调整。
- 操作:
- 检查当前结点是否满足:根≥左右孩子,若不满足,将当前结点与更大的一个孩子互换。
- i的左孩子—— 2i
- i的右孩子—— 2i+1
- i的父结点——⌊2i⌋
- 若元素互换破坏了下一级的堆,则采用相同的方法继续往下调整(小元素不断下坠)
代码实现
1 2 3 4 5
| void BuildMaxHeap(int A[],int len){ for(int i=len/2;i>0;i--) HeadAdjust(A,i,len); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| void HeadAdjust(int A[],int k,int len){ A[0]=A[k]; for(int i=2*k;i<=len;i*=2){ if(i<len&&A[i]<A[i+1]) i++; if(A[0]>=A[i]) break; else{ A[k]=A[i]; k=i; } } A[k]=A[0]; }
|
1 2 3 4 5 6 7 8
| void HeapSort(int A[],int len){ BuildMaxHeap(A,len); for(int i=len;i>1;i--){ swap(A[i],A[1]); HeadAdjust(A,1,i-1); } }
|
算法效率分析
- 结论:
- 一个结点每下坠一层,最多需对比关键字2次
- 若树高为h,某结点在第i层,则将这个结点向下调整最多只需要下坠h-i层,关键字对比次数不超过2(h-i)
- n个结点的完全二叉树树高h=⌊log2n⌋+1
- 推导:
- 第i层最多有2i−1个结点,而只有第1∼(h−1)层的结点才有可能需要下坠调整
- 将整棵树调整为大根堆,关键字对比次数不超过:
i=h−1∑12i−12(h−i)=i=h−1∑12i(h−i)=j=1∑h−12h−jj≤2nj=1∑h−12jj≤4n
结论: 建堆的过程,关键字对比次数不超过4n,建堆时间复杂度O(n)

- 稳定性: 堆排序是不稳定的
- 特点:
- 基于大根堆的堆排序得到递增序列
- 基于小根堆的堆排序得到递减序列
堆的插入删除
在堆中插入元素:

在堆中删除元素:

归并排序
算法思想
在内部排序中一般采用2路归并:

核心操作: 把数组内的两个有序序列归并为一个
代码实现
1
| int *B(int *)malloc(n*sizeof(int));
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| void Merge(int A[],int low,int mid,int high){ int i,j,k; for(k=low;k<=high;k++) B[k]=A[k]; for(i=low,j=mid+1,k=i;i<=mid&&j<=high;k++){ if(B[i]<=B[j]) A[k]=B[i++]; else A[k]=B[j++]; } while(i<=mid) A[k++]=B[i++]; while(j<=high) A[k++]=B[j++]; }
|
1 2 3 4 5 6 7 8 9
| void MergeSort(int A[],int low,int high){ if(low<high){ int mid=(low+high)/2; MergeSort(A,low,mid); MergeSort(A,mid+1,high); Merage(A,low,mid,high); } }
|
算法效率分析
特点分析:
- 2路归并的
归并树—— 形态上就是一棵倒立的二叉树 - 二叉树的第h层最多有2h−1个结点,若树高为h,则应满足n≤2h−1,即h−1=⌈log2n⌉
结论: n个元素进行2路归并排序,归并趟数=⌈log2n⌉
时间复杂度:O(nlog2n)
空间复杂度:O(n),来自与辅助数组B
- 递归所调用的空间不会超过log2n
基数排序
算法思想
假设长度为n的线性表中每个结点aj的关键字由d元组(kjd−1,kjd−2,kjd−3,...,kj1,kj0)组成,其中,0≤kji≤r−1(0≤j<n,0≤i≤d−1),r称为基数
基数排序得到递减序列的过程如下:
初始化:设置r个空队列,Qr−1,Qr−2,...,Q0- 按照各个关键字权重递增的次序(个,十,百),对d个关键字位分别做分配和收集
分配:顺序扫描各个元素,若当前处理的关键字位=x,则将元素插入Qx队尾收集:把Qr−1,Qr−2,...,Q0各个队列中的结点依次出队并链接
注意: 基数排序不是基于比较的排序算法



- 基数排序具体操作:

算法效率分析

- 基数排序稳定性:

应用
- 基数排序擅长解决的问题:
- 数据元素的关键字可以方便地拆分为d组,且d较小
- 每组关键字的取值范围不大,即r较小
- 数据元素个数n较大
