内部排序
1.插入排序
基本思想:每步将一个待排序的对象,按其关键码大小,插入到前面以及排好序的一组对象的适当位置,直到对象全部插入。即边插入边排序,保证子序列中随时都是排好序的。
直接插入排序
算法思想
每一趟将一个无序区的对象在有序区中从后往前扫描,反复将已排序元素向后挪位,在找到合适位置后插入新元素。
动态图示
算法实现
void InsertSort(SqList &L){
int i,j;
for (i=2;i<=L.length;++i){
if(L.r[i].key<L.r[i-1].key){//若“<”,需将L.r[i]插入有序表
L.r[0]=L.r[i]; //复制为哨兵
for(j=i-1;L.r[0].key<L.r[j].key;--j)
L.r[j+1]=L.r[j]; //记录后移
L.r[j+1]=L.r[0]; //插入到正确位置
}//if
}//for
}//InsertSort
算法分析
- 最好情况(关键字在序列中顺序有序)
-
比较次数: ∑ i = 2 n 1 = n − 1 \sum\limits_{i=2}^{n}1=n-1 i=2∑n1=n−1
-
移动次数: 0 0 0
- 最差情况(关键字在序列中逆序有序)
-
比较次数: ∑ i = 2 n i = ( n + 2 ) ( n − 1 ) 2 \sum\limits_{i=2}^{n}i=\cfrac{(n+2)(n-1)}{2} i=2∑ni=2(n+2)(n−1)
-
移动次数: ∑ i = 2 n ( i + 1 ) = ( n + 4 ) ( n − 1 ) 2 \sum\limits_{i=2}^{n}(i+1)=\cfrac{(n+4)(n-1)}{2} i=2∑n(i+1)=2(n+4)(n−1)
- 平均情况
-
比较次数: ∑ i = 1 n − 1 i + 1 2 = ( n + 2 ) ( n − 1 ) 4 \sum\limits_{i=1}^{n-1}\cfrac{i+1}{2}=\cfrac{(n+2)(n-1)}{4} i=1∑n−12i+1=4(n+2)(n−1)
-
移动次数: ∑ i = 1 n − 1 ( i + 1 2 + 1 ) = ( n + 6 ) ( n − 1 ) 4 \sum\limits_{i=1}^{n-1}(\cfrac{i+1}{2}+1)=\cfrac{(n+6)(n-1)}{4} i=1∑n−1(2i+1+1)=4(n+6)(n−1)
-
时间复杂度结论
- 原始数据越接近有序,排序速度越快
- 最坏情况下, T w ( n ) = O ( n 2 ) T_w(n)=O(n^2) Tw(n)=O(n2)
- 平均情况下,耗时差不多是最坏情况的一半, T e ( n ) = O ( n 2 ) T_e(n)=O(n^2) Te(n)=O(n2)
- 要提高查找速度
- 减少元素的比较次数
- 减少元素的移动次数
二分插入排序(折半插入排序)
算法实现
void BInsertSort(SqList &L){
for(i=2;i<=L.length;++i){//依次插入第2~n个元素
L.r[0]=L.r[i]; //当前插入元素存入哨兵
low=1;high=i-1; //采用二分法查找插入位置
while(low<=high){
mid=(low+high)/2;
if(L.r[0].key<L.r[mid].key)
high=mid-1;
else
low=mid+1;
}//while,结束循环,high+1则为插入位置
for(j=i-1;j>=high+1;--j)
L.r[j+1]=L.r[j];//移动元素
L.r[high+1]=L.r[0];//插入到正确位置
}//for
}//BInsertSort
算法分析
- 在插入第i个元素,需要经过
⌊
l
o
g
2
i
⌋
+
1
\lfloor log_2i \rfloor+1
⌊log2i⌋+1次比较,才能确定应插入的位置
- 在n较大时,总比较次数比直接插入排序的最坏情况好得多,但比最好情况要差
- 在关键字序列初始排列已经按关键码排好序或接近有序时,直接插入排序比折半插入排序执行的关键码比较次数要少
- 减少了比较次数,没有减少移动次数
- 平均性能优于直接插入排序
- 时间复杂度为 O ( n 2 ) O(n^2) O(n2)
- 空间复杂度为 O ( 1 ) O(1) O(1)
希尔排序
算法思想
先将整个待排记录序列分割为若干子序列,分别进行直接插入排序,待整个序列中的记录“基本有序”时,在对全体记录进行一次直接插入排序
- 缩小增量
- 多遍插入排序
动态图示
算法实现
void ShellSort(SqList &L,int dlta[],int t){//按增量序列dlta[0...t-1]对顺序表作希尔排序
for(k=0;k<t;++k)
ShellInsert(L,dlat[k]);//一趟增量为dlta[k]的插入排序
}//ShellSort
void ShellInsert(SqList &L,int dk){//对顺序表L进行一趟增量为dk的Shell排序,dk为步长因子
for(i=dk+1;i<L.length;++i){
L.r[0]=L.r[i];
for(j=i-dk;j>0&&(L.r[0].key<L.r[j].key);j=j-dk)
L.r[j+dk]=L.r[j];
L.r[j+dk]=r[0];
}//for
}//ShellInsert
算法分析
- 算法效率与增量序列取值有关
- 时间复杂度是n和d的函数: O ( n 1.25 ) O(n^{1.25}) O(n1.25)~ O ( 1.6 n 1.25 ) O(1.6n^{1.25}) O(1.6n1.25)(经验公式)
- 空间复杂度是 O ( 1 ) O(1) O(1)
- 如何选择最佳d序列,目前尚未解决
- 最后一个增量值必须为1,无出了1以外的公因子
- 不宜在链式存储结构上实现
2.交换排序
基本思想:两两比较,如果发生逆序则交换,直到所有记录都排好序为止。
冒泡排序
算法思想
每趟不断将记录两两比较,按"前小后大"/"前大后小"规则交换
动态图示
算法实现
void BubbleSort(SqList &L){
int i,j;
for(i=1;i<L.length-1;i++){//总共需要m趟
for(j=1;j<L.length-i;j++)
if(L.r[j].key>L.r[j+1].key){//发生逆序,进行交换
Swap(L.r[j],L.r[j+1]);
}//if
}//for
}//BubbleSort
void BubbleSort(Sqlist &L){//改进的冒泡排序
int i,j,
bool flag=true;//flag作为是否交换的标记
for(i=0;i<L.length-1&&flag;i++){
flag=false;
for(j=L.length-1;j>i;j--)
if(L.r[j].key<L.r[j-1].key){//发生逆序
Swap(L.r[j],L.r[j-1]);
flag=true;;//发生交换,flag置为true,若本趟没有发生交换,flag保持为false
}//if
}//for
}//BubbleSort
算法分析
- 优点:每趟结束后,不仅能挤出一个最大值到最后面,还能同时部分理清其他元素
- 最好情况(正序)
- 比较次数: n − 1 n-1 n−1
- 移动次数: 0 0 0
- 最坏情况(逆序)
- 比较次数: ∑ i = 1 n − 1 ( n − i ) = n 2 − n 2 \sum\limits_{i=1}^{n-1}(n-i)=\cfrac{n^2-n}{2} i=1∑n−1(n−i)=2n2−n
- 移动次数: 3 ∑ i = 1 n − 1 ( n − i ) = 3 ( n 2 − n ) 2 3\sum\limits_{i=1}^{n-1}(n-i)=\cfrac{3(n^2-n)}{2} 3i=1∑n−1(n−i)=23(n2−n)
- 时间复杂度
- 最好情况: O ( n ) O(n) O(n)
- 最坏情况: O ( n 2 ) O(n^2) O(n2)
- 平均情况: O ( n 2 ) O(n^2) O(n2)
- 空间复杂度: O ( 1 ) O(1) O(1)
快速排序
算法思想
任取一个元素(如第一个)为中心(pivot),所有比它小的元素一律前放,比它大的元素一律后放,形成两个左右子表,对各子表重新选择中心元素并依此规律调整,直到子表的元素只剩一个。
通过一趟排序,将待排序记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录进行排序,以达到整个序列有序
动态图示
算法实现
void QSort(SqList &L,int low,int high){
if(low<high){//长度大于1
pivotloc=Partition(L,low,high);//将L.r[]一分为二,pivotloc为枢轴元素排好序的位置
QSort(L,low,pivotloc-1);//对低子表递归排序
QSort(L,pivotloc+1,high);//对高子表递归排序
}//if
}//QSort
int Partition(SqList &L,int left,int right){
L.r[0]=L.r[left];
pivotkey=L.r[left].key;
while(left<right){
while(left<right&&L.r[right].key>=pivotkey)
--right;
L.r[left]=L.r[right];
while(left<right&&L.r[right].key<=pivotkey)
++left;
L.r[right]=L.r[left];
}//while
L.r[left]=L.r[0];
return left;
}//Partition
算法分析
- 时间复杂度
- 平均计算时间是 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
- QSort(): O ( l o g 2 n ) O(log_2n) O(log2n)
- Partition(): O ( n ) O(n) O(n)
- 最坏情况下: O ( n 2 ) O(n^2) O(n2)
- 空间复杂度
- 平均情况下,需要 O ( l o g 2 n ) O(log_2n) O(log2n)的栈空间
- 最坏情况下,需要 O ( n ) O(n) O(n)的栈空间
- 快速排序不适合于对原本有序或基本有序的记录序列进行排序
3.选择排序
简单选择排序
算法思想
在待排序的数据中选出最大/最小的元素放在其最终位置
动态图示
算法实现
void SelectSort(SqList &L){
for(i=1;i<L.length;++i){
k=i;
for(j=i+1;j<=L.length;j++)
if(L.r[j].key<L.r[k].key)
k=j;//记录最小值位置
if(k!=i)
Swap(L.r[i],L.r[k])//交换
}//for
}//SelectSort
算法分析
- 时间复杂度
- 移动次数
- 最好情况: 0 0 0
- 最坏情况: 3 ( n − 1 ) 3(n-1) 3(n−1)
- 比较次数无论哪种情况都相同
- 移动次数
堆排序
算法思想
在输出堆顶的最小值/最大值后,使得剩余n-1个元素的序列重新又建成一个堆,则得到次小值/次大值,如此反复,得到一个有序序列
- 从最后一个非叶子节点开始,以此向前调整:
- 调整从第n/2个元素开始,将以该元素为根的二叉树调整为堆
- 将以序号为n/2-1的结点为根的二叉树调整为堆
- 将以序号为n/2-2的结点为根的二叉树调整为堆
- 将以序号为n/2-3的结点为根的二叉树调整为堆
- ······
动态图示
算法实现
void HeapAdjust(HeapType &H,int s,int m){//调整为大根堆
rc=H.r[s];
for(j=2*s;j<m;j*=2){//沿key较大的孩子结点向下筛选
if(j<m&&H.r[j].key<H.r[j+1].key)
++j;//j为key较大的记录的下标
if(rc.key>=H.r[j].key)
break;
H.r[s]=H.r[j];
s=j;//rc应插在位置s上
}//for
H.r[s]=rc;//插入
}//HeapAdjust
void HeapSort(HeapType &H){
int i;
for(i=H.length/2;i>0;--i)
HeapAdjust(H,i,H.length);
for(i=H.length;i>1;--i){
Swap(H.r[1],H.r[i]);
HeapAdjust(H,1,i-1);
}//for
}//HeapSort
算法分析
- 初始化堆需要时间不超过 O ( n ) O(n) O(n)
- 排序阶段(不包含初始化堆)
- 一次重新堆化所需时间不超过 O ( l o g 2 n ) O(log_2n) O(log2n)
- n-1次循环所需时间不超过 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
- T w ( n ) = O ( n ) + O ( n l o g 2 n ) = o ( n l o g 2 n ) T_w(n)=O(n)+O(nlog_2n)=o(nlog_2n) Tw(n)=O(n)+O(nlog2n)=o(nlog2n)
- 无论待排序序列中记录是什么情况,都不会使堆排序处于“最好”或“最坏”的状态
- 不适用于待排序记录个数n较少的情况,但对于n较大的文件还是很有效的
4.归并排序
2-路归并排序
算法思想
将两个或两个以上的有序子序列“归并”为一个有序序列
- 内部排序中,通常采用2-路归并排序
- 核心操作是将一维数组的前后相邻的两个有序序列归并为一个有序序列
动态图示
算法实现
void Merge(RcdType SR[],RcdType TR[],int low,int m,int high){//
for(j=m+1,k=low;low<=m&&j<=n;++k){
if(SR[low].key<=SR[j].key)
TR[k]=SR[low++];
else
TR[k]=SR[j++];
}//for
if(i<=m)
TR[k..high]=SR[low..m];//将剩余SR[low..m]复制到TR
if(j<=n)
TR[k..high]=SR[j..high];//将剩余SR[j..high]复制到TR
}//Merge
void MSort(RcdType SR[],RcdType TR1[],int s,int t){//将SR[s..t]归并排序为TR1[s..t]
if(s==t)
TR1[s]==SR[s];
else{
m=(s+t)/2;
MSort(SR,TR2,s,m);
MSort(SR,TR2,m+1,t);
Merge(TR2,TR1,s,m,t);
}
}//MSort
void MergeSort(Sqlist &L){
MSort(L.r,L.r,1,L.length);
}//MergeSort
算法分析
- 整个归并排序仅需 ⌈ l o g 2 n ⌉ \lceil log_2n \rceil ⌈log2n⌉趟
- 时间效率: O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
- 空间效率: O ( n ) O(n) O(n)
5.基数排序
基数排序
算法思想
分配+收集,
- 桶排序或箱排序:设置若干个箱子,将关键字为k的记录放入第k个箱子,然后按序号将非空的连接
- 基数排序:数字是有范围的,均由0~9组成,则只需要设置10个箱子,相继按照个、十、百、千···进行排序
动态图示
算法分析
- 时间效率:
O
(
k
∗
(
n
+
m
)
)
O(k*(n+m))
O(k∗(n+m))
- k:关键字个数(待排元素的维数)
- m:关键字取值范围为m个值(基数的个数)
- 空间效率: O ( n + m ) O(n+m) O(n+m)
各排序算法比较
排序算法 | 类别 | 最好情况 | 最差情况 | 平均时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|---|
直接插入排序 | 插入排序 | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 稳定 |
希尔排序 | 插入排序 | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | ∼ O ( n 1.3 ) \sim O(n^{1.3}) ∼O(n1.3) | O ( 1 ) O(1) O(1) | 不稳定 |
冒泡排序 | 交换排序 | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 稳定 |
快速排序 | 交换排序 | O ( n l o g 2 n ) O(nlog{_2}n) O(nlog2n) | O ( n 2 ) O(n^2) O(n2) | O ( n l o g 2 n ) O(nlog{_2}n) O(nlog2n) | O ( n l o g 2 n ) O(nlog{_2}n) O(nlog2n) | 不稳定 |
简单选择排序 | 选择排序 | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 不稳定 |
堆排序 | 选择排序 | O ( n l o g 2 n ) O(nlog{_2}n) O(nlog2n) | O ( n l o g 2 n ) O(nlog{_2}n) O(nlog2n) | O ( n l o g 2 n ) O(nlog{_2}n) O(nlog2n) | O ( n 2 ) O(n^2) O(n2) | 不稳定 |
归并排序 | 归并排序 | O ( n l o g 2 n ) O(nlog{_2}n) O(nlog2n) | O ( n l o g 2 n ) O(nlog{_2}n) O(nlog2n) | O ( n l o g 2 n ) O(nlog{_2}n) O(nlog2n) | O ( n ) O(n) O(n) | 稳定 |
基数排序 | 基数排序 | O ( n + m ) O(n+m) O(n+m) | O ( k ∗ ( n + m ) ) O(k*(n+m)) O(k∗(n+m)) | O ( k ∗ ( n + m ) ) O(k*(n+m)) O(k∗(n+m)) | O ( n + m ) O(n+m) O(n+m) | 稳定 |
时间性能
- 按平均的时间性能来分,有三类排序方法
- 时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)的方法:快速排序(Best)、堆排序、归并排序
- 时间复杂度为 O ( n 2 ) O(n^2) O(n2)的方法:直接插入排序(Best)、冒泡排序、简单选择排序
- 时间复杂度为 O ( n ) O(n) O(n)的方法:基数排序
- 当待排记录序列按关键字顺序有序时
- 直接插入排序、冒泡时间排序能达到 O ( n ) O(n) O(n)
- 快速排序将进入最坏情况,时间性能退化为 O ( n 2 ) O(n^2) O(n2),应该尽量避免
- 不随记录序列中关键字的分布而改变
- 简单选择排序、堆排序、归并排序
空间性能
指的是排序过程中所需的辅助空间大小
- 所有的简单排序方法(直接插入、冒泡、简单选择)和堆排序的空间复杂度为 O ( 1 ) O(1) O(1)
- 快速排序为 O ( l o g 2 n ) O(log_2n) O(log2n),为栈所需要的辅助空间
- 归并排序为 O ( n ) O(n) O(n),所需的辅助空间最多
- 链式基数排序为 O ( r d ) O(rd) O(rd),其需附设队列首尾指针
排序方法的稳定性
- 稳定的排序方法指的是,对于两个关键字相等的记录,它们在序列中的相对位置,在排序之前和经过排序之后,没有改变
- 当对多关键字的记录序列进行LSD方法排序时,必须采用稳定的排序方法
- 对于不稳定的排序方法,只要能举出一个实例说明即可
- 快速排序和堆排序是不稳定的排序方法
关于“排序方法的时间复杂度的下限”
- 除基数排序外,其他方法都是基于“比较关键字”进行排序的排序方法,可以证明,这类排序法可能达到的最快的时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
- 可以用一颗判定树来描述这类基于“比较关键字”进行排序的排序方法
注:本文GIF图片为网络获取,侵删!