声明:此总结较简练,对于每种算法未给出排序例子,适合有一定基础进行复习了解具体实现性能分析使用,建议初学者选择更详细的教程
框架:
内部排序:
一、插入排序
直接插入排序
将元素L[i]插入已有序的子序列L[1...i-1]
1)找出L[i]在子序列中的插入位置k
2)将L[k...i-1]的元素后移一位
3)将L[i]复制到L[k]
void insertsort(element a[i], 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] = a[0];
}
}
空间复杂度:O(1)
时间复杂度:总共进行n-1趟插入,插入分为比较和移动
最好情况:已有序,每趟比较一次,不移动,O(n)
最坏情况:逆序,O(n^2)
算法稳定,适用于顺序存储的线性表
折半插入
比较元素时使用折半查找,可以减少元素比较的次数,时间变为O(nlog2(n)),但元素移动次数不变,因此总的时间复杂度仍为O(n^2),其余特性与直接插入一致
void insertsort(element a[i], int n) {
int i, j, low, high, mid;
for (i = 2; i <= n; i++) {
a[0] = a[i];
low = 1, high = i - 1;
while (low <= high) {
mid = low + high >> 1;
if (a[mid] > a[0])high = mid - 1;
else low = mid + 1;
}
for (j = i - 1; j >= high + 1; --j)
a[j + 1] = a[j];
a[j] = a[0];
}
}
希尔排序(减少增量排序)
将原数组分割为如L[i,i+d,i+2d,...,i+kd]的子表,对每个子表进行直接插入排序,然后取d2<d,重复此过程,直至d为1
void shellsort(element a[], int n) {
int d, i, j;
for(d=n/2;d>0;d=d/2)
for(i=d+1;i<=n;i++)
if (a[i] < a[i - d]) {
a[0] = a[i];
for (j = i - d; a[j] > a[0] && j > 0; j -= d)
a[j + d] = a[j];
a[j] = a[0];
}
}
空间复杂度依然为O(1)
时间复杂度在特定范围内普遍认为在O(n^1.3),最坏情况为O(n^2)
算法不稳定,适用于顺序存储的线性表
二、交换排序
冒泡排序
两两比较相邻元素的值,若逆序则交换
每趟排序可使最小(或最大)元素交换到第一个(或最后一个位置)
void bubblesort(element a[], int n) {
for (int i = 0; i < n; 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) return;
}
}
(此伪代码为排递增序列,每次选一个最小值)
空间复杂度:O(1)
时间复杂度:最好情况:本身有序,仅比较n次,无交换,O(n)
平均和最坏情况:O(n^2)
最坏情况:逆序,n-1躺排序,第i躺比较n-i次,每次移动需要交换3个元素(swap实质就是开一个辅助变量交换一下)
算法稳定,适用于顺序存储和链式存储的线性表
快速排序
有多种实现方法,考研主体使用递归的挖坑法(分治思想)
在排序序列中任取一个元素作为枢轴,通过一趟排序将序列划分为两个独立部分L[1..k-1],L[k+1...n]
左边各元素均小于枢轴L[k],右边皆大于L[k],然后在两个子表中重复此过程
注意:每次不一定能划分为2个部分,如第一次选择枢轴若最终位置在首部或尾部,则下次也只选择一个枢轴,而若第一次选择的枢轴最终位置在中间部分,下次在两个子表中各选一个新枢轴
int partion(element a[], int low, int high) {
element 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;
}
void quiksort(element a[], int low, int high) {
if (low < high) {
int pivoitpos = partion(a, low, high);
quiksort(a, low, pivoitpos);
quiksort(a, pivoitpos + 1, high);
}
}
空间复杂度:因使用递归,所以取决于递归调用的最大层数,最好为O(log2(n)),最坏为O(n) 【想象一下二叉排序树,枢轴为根节点】
时间复杂度:最坏情况:基本有序(逆序),则深度可达n,为O(n^2)
算法不稳定,适用于顺序存储的线性表
三、选择排序
简单选择排序
每趟选一个剩余序列中最大(小)的元素
元素的移动次数最大不会超过3*(n-1)次
元素的比较次数于初始状态无关,必为n*(n-1)/2
void selectsort(element a[], int n) {
for (int i = 0; i < n; 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]);
}
}
(此伪代码为每次选最小元素)
空间复杂度:O(1)
时间复杂度:因为比较次数恒定,时间复杂度也恒定为O(n^2)
算法不稳定,适用于顺序存储和链式存储的线性表及关键字较少的情况
关于为什么不稳定大家可以试一下(2,2,1)这个序列就明白了
堆排序
可将堆视为完全二叉树
大根堆:任一非根结点的值小于其双亲结点值
小根堆:与大根堆相反
创建初始堆:
从n/2-1到1进行调整,
调整过程:看要调整结点的值是否大于左右子结点的值,若不大于,与较大者交换,交换后下面的堆可能出现问题,需要继续往下调整
堆排序(仅适用于顺序存储的线性表)
输出堆顶元素,将堆顶元素与最后一个元素互换,将堆从1开始进行调整(堆元素-1,原堆顶元素不参与调整,后序同理)
(建立大根堆)
void buildheap(element a[], int n) {
for (int i = n / 2; i > 0; i--)
headadjust(a, i, n);
}
void headadjust(element a[], int k, int n) {
a[0] = a[k];
for (int i = 2 * k; i <= n; i *= 2) {
if (i < n && a[i] < a[i + 1])i++;
if (a[0] >= a[i])break;
a[k] = a[i];
k = i;
}
a[k] = a[i];
}
(堆排序)
void heapsort(element a[], int n) {
buildheap(a, n);
for (int i = n; i > 1; i--) {
swap(a[i], a[1]);
headadjust(a, 1, i - 1);
}
}
空间复杂度为O(1)
时间复杂度:建堆时关键字比较次数不超过4n,时间间为O(n),排序时间为O(n*log2(n))
所以总时间复杂度为O(n*log2(n))
算法不稳定,适用于顺序存储的线性表,蛇关键字多的情况
四、归并排序
以二路归并进行分析:
将两个有序表合并成一个新的有序表,n个元素,可视为n个元素为1的有序表,逐渐合并直到合并成一个长度为n的有序表为止
element* b=new element[n]
void merge(element 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; i <= mid && j <= high; k++)
a[k] = min(a[i], a[j]);
while (i <= mid)a[k++] = b[i++];
while (j <= high)a[k++] = b[j++];
}
void mergesort(element a[], int low, int high) {
if (low < high) {
int mid = low + high >> 1;
mergesort(a, low, mid);
mergesort(a, mid+1, high);
merge(a, low, mid, high);
}
}
空间复杂度:递归调用堆栈为O(log2(n)),辅助空间为O(n) ,总体为O(n)
时间复杂度:每次归并为O(n),共cell(log2(n))次,因此为O(nlog2(n))
算法稳定,适用于顺序存储和链式存储的线性表
五、基数排序(桶排序)
不基于比较和移动进行排序
MSD(最高位优先) LSD(最低位优先)
采用r个链式队列存储和排序(r为进制即基数)
空间复杂度:O(r)
时间复杂度:需要进行d趟分配和收集,每趟分配需要遍历所有关键字O(n),每趟收集需要合并r个队列O(r),因此总时间复杂度为O(d*(n+r)),与序列初始状态无关
算法稳定,适用于顺序存储和链式存储的线性表
六、计数排序
不基于比较进行排序,空间换时间
思想:对每个待排序元素x,统计小于x的元素个数
需要两个辅助数组,b用于存放输出的排序序列,c用于存储计数值
void countsort(element a[], element b[], int n, int k) {
int i, c[k];
for (i = 0; i < k; i++)
c[i] = 0;
for (i = 0; i < n; i++)
c[a[i]]++;
for (i = 1; i < n; i++)
c[i] += c[i - 1];
for (i = n - 1; i >= 0; i--) {
c[a[i]]--;
b[c[a[i]]] = a[i];
}
}
空间复杂度:O(n+k)
时间辅助度:根据for循环可得为O(n+k),k=O(n)时,复杂度为O(n),当k>O(n*logn)时,性能明显下降
算法稳定,适用于序列中元素为整数且元素范围不能太大
外部排序
通常采用归并算法
1)根据内存缓冲区大小,将外存上的文件分成若干长度为l的子文件,依次读入内存并利用内部排序算法对它们进行排序,并将排序后的有序子文件(归并段)重新写回外存
2)对归并段进行逐趟归并,使归并段逐渐由小到大
外部排序的总时间=内部排序的时间+外存读写时间+内部归并时间
每趟读次数=每趟写次数=总块数
初始归并段数量=总容量/内存分配大小
内存缓冲分配如下:
无论几路归并,至少需要一个输出缓冲区,
若想实现输入/内部归并/输出的并行处理
需在原有缓冲区数量基础上*2
未优化的归并:
(k路)在k个元素中选择最小的元素需要k-1次比较,每趟归并n个元素需要(n-1)(k-1)次比较,
共s趟归并,总比较次数为:S(n-1)(k-1)
败者树(减少比较次数):仅需比较cell(log2(k))次
置换-选择排序(减少初始归并段数)
由置换-选择排序生成的初始归并段的长度可能是不同的,此时需要最佳归并数
所谓最佳归并数其实就是哈夫曼树,但必须保证可以进行k路归并
设度为0的结点有n0个,度为k的结点有nk个,总结点n个
n=nk+n0
n=nk*k+1
对于严格的k叉树
有n0=(k-1)*nk-1,nk=(n0-1)/(k-1)
若(n0-1)%(k-1)=u!=0,则需要增加k-u-1个空归并段
归纳(重点):
1、稳定的算法有直接插入排序,折半插入,冒泡,基数,计数,归并
不稳定的算法有希尔,简单选择,快速,堆
2、直接插入,冒泡,简单选择,折半插入的平均时间复杂度都是O(n^2),最好情况下,插入和冒泡的时间复杂度为O(n),简单选择不变,它们主要用于数据较小的情况(n<10000)
3、快速排序,堆排序,二路归并排序的平均时间复杂度都是O(nlog2(n)),最坏情况下快速排序时间复杂度变为O(n^2),空间复杂度变为O(n),堆排序,归并不变
4、比较次数与序列初始状态无关的有简单选择排序,基数排序
5、不基于比较的排序有基数排序和计数排序
手敲不易,感谢点赞