每次将一个待排序的记录,按其关键字大小插入到前面已经排好序的子表中的适当位置,直到全部记录插入完成为止。
整个排序过程为n-1趟插入,即先将序列中第1个记录看成是一个有序子序列,然后从第2个记录开始,逐个进行插入,直至整个序列有序。
void InsertSort(SqList &L)
{
int i,j;
for(i=2;i<=L.length; i++)
{
//将L.R[i]插入有序子表
if( L.R[i].key<L.R[i-1].key)
{
L.R[0]=L.R[i]; // 复制为哨兵
j = i-1;
do{
L.R[j+1]=L.R[j]; // 记录后移
j--;
}while(L.R[0].key>=L.R[j].key))
L.R[j+1]=L.R[0]; //插入到正确位置
}
}
}
最好的情况(关键字在记录序列中正序
):
“比较”的次数: ∑ i = 1 n − 1 1 = n − 1 \sum_{i=1}^{n-1} 1=n-1 ∑i=1n−11=n−1
“移动”的次数:0
最坏的情况(关键字在记录序列中逆序
有序):
“比较”的次数: ∑ i = 1 n − 1 i = n ( n − 1 ) / 2 \sum_{i=1}^{n-1} i=n(n-1)/2 ∑i=1n−1i=n(n−1)/2
“移动”的次数: ∑ i = 1 n − 1 ( i + 2 ) = ( n + 4 ) ( n − 1 ) / 2 \sum_{i=1}^{n-1} (i+2)=(n+4)(n-1)/2 ∑i=1n−1(i+2)=(n+4)(n−1)/2
先将整个待排记录序列分割成若干子序列,各个子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。
将相距d个位置的记录分为一组, n 个记录被分成 d 个子序列,d 称为增量,增量d的值在排序过程中从大到小逐渐缩小,直至最后一趟排序减为 1。所以希尔排序也称为缩小增量排序。
时间复杂度平均情况: O ( n 1.3 ) O(n^{1.3}) O(n1.3)
如何选择最佳d序列,目前尚未解决;但最后一个增量值必须为1
不宜在链式存储结构上实现
void Bubble-sort(SqList &L)
{
int i, j, swap; // 当swap为0则停止排序
for ( i=1; i<L.length; i++) // i 表示趟数,最多n-1趟
{
swap=0; // 开始时元素未交换
for ( j=1; j<=L.length-i; j++)
if (L.R[j].key>L.R[j+1].key) // 发生逆序
{
L.R[0]=L.R[j]; L.R[j]=L.R[j+1]; L.R[j+1]=L.R[0];
swap=1;
} // 交换,并标记发生了交换
if(swap==0) break;
}
}
最好的情况(关键字在记录序列中正序
):
“比较”的次数: ∑ i = 1 n − 1 1 = n − 1 \sum_{i=1}^{n-1} 1=n-1 ∑i=1n−11=n−1
“移动”的次数:0
最坏的情况(关键字在记录序列中逆序
有序):
“比较”的次数: ∑ i = 1 n − 1 i = n ( n − 1 ) / 2 \sum_{i=1}^{n-1} i=n(n-1)/2 ∑i=1n−1i=n(n−1)/2
“移动”的次数: ∑ i = 1 n − 2 3 ( n − i − 1 ) = 3 n ( n − 1 ) / 2 \sum_{i=1}^{n-2}3 (n-i-1)=3n(n-1)/2 ∑i=1n−23(n−i−1)=3n(n−1)/2
改进:在冒泡排序中,记录的比较和移动是在相邻单元中进行的,记录每次交换只能上移或下移一个单元,因而总的比较次数和移动次数较多。
void Quick_Sort(SqList &L,int s,int t) /* 对R[s]到R[t]的元素进行排序 */
{
if (s<t) //至少有两个元素
{
int i=Partition(L,s,t);
Quick_Sort(L,s,i-1);
Quick_Sort(L,i+1,t);
}
}
int Partition(SqList &L,int low,int high)
{
L.R[0]= L.R[low]; /* 暂存基准值元素到R[0]中*/
while(low<high) /* 从表的两端交替地向中间扫描 */
{
while( low<high&&L.R[high].key>=L.R[0].key )high--;
if(low<high) {
L.R[low]= L.R[high]; low++; }
while( low<high&&L.R[low].key<L.R[0].key ) low++;
if (low<high) {
L.R[high]= L.R[low]; high--; }
}
L.R[low]= L.R[0]; /* 将基准值元素放到其最终位置 */
return low; /* 返回基准值元素所在的位置*/
}
最好情况时间复杂度为 O ( n log 2 n ) O(n\log_2n) O(nlog2n),空间复杂度为 O ( l o g 2 n ) O(log_2n) O(log2n)。
最坏情况时间复杂度为 O ( n 2 ) O(n^2) O(n2),空间复杂度为 O ( n ) O(n) O(n)。
平均时间复杂度为 O ( n log 2 n ) O(n\log_2n) O(nlog2n)。
稳定性:不稳定。
每一趟在后面 n-i+1个中选出关键码最小的对象, 作为有序序列的第 i 个记录。
最好情况时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
最坏情况时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
空间复杂度为 O ( 1 ) O(1) O(1)。
简单选择排序慢的原因?
用直接选择排序从n个记录中选出关键字值最小的记录要做n-1次比较,然后从其余n-1个记录中选出最小者要作n-2次比较。显然,相邻两趟中某些比较是重复的。
树形选择排序:首先对n个关键字进行两两比较,然后在其⌈/2⌉个较小者之间再进行两两比较,如此重复,直至选出最小关键字为止。这个过程可用一棵有n个叶结点的完全二叉树表示。
n个元素的序列 { k 1 , k 2 , … … , k n } \{ k_1, k_2 ,…… , k_n \} { k1,k2,……,kn},当且仅当满足下面条件(以大根堆为例)称之为堆。
{ k i ≥ k 2 i k i ≥ k 2 i + 1 \left\{\begin{array}{l} k _{ i } \geq k _{2 i } \\ k _{ i } \geq k _{2 i +1}\end{array}\right. { ki≥k2iki≥k2i+1
( i = 1 , 2 , … , ⌊ n / 2 ⌋ ) (i=1,2, \ldots,\lfloor n / 2\rfloor) (i=1,2,…,⌊n/2⌋)
若将此关键字序列按顺序组成一棵完全二叉树,则堆可以如下定义:
或每个结点的值都大于或等于其左右孩子结点的值(称为大根堆或大顶堆)。
对一棵左右子树均为堆的完全二叉树,“调整”根结点使整个二叉树也成为一个堆。
void HeapAdjust(SqList &L,int s,int m)
{
//假设R[s+1..m]已经是堆,将R[s..m]调整为以R[s]为根的大根堆
RecType tmp=L.R[s];
for(int i=2*s ; i<=m ; i*=2) //沿key较大的孩子结点向下筛选
{
if(i<m && L.R[i].key<L.R[i+1].key) i++; //i为key较大的记录的下标
if( tmp.key>=L.R[i].key )
break;//双亲大:不再调整,temp应插在位置s上
L.R[s]=L.R[i];//将R[j]调整到双亲结点位置上
s=i; //修改s值,以便继续向下筛选
}
L.R[s]=tmp; //插入
}
for (i=n/2;i>=1;i--)
HeapAdjust(L.R,i,n);
void HeapSort(SqList &L)
{
int i; RecType tmp;
for (i=n/2;i>=1;i--) //循环建立初始堆
HeapAdjust(L.R,i,n);
for (i=n; i>=2; i--) //进行n-1次循环,完成堆排序
{
temp=L.R[1]; //堆顶归位,R[1] R[i]
L.R[1]=L.R[i];
L.R[i]=tmp;
HeapAdjust(L.R,1,i-1);//调整剩余记录,筛选R[1]结点,得到i-1个结点的堆
}
}
设有n个记录的初始序列对应的完全二叉树的深度为 h = ⌊ log 2 n ⌋ + 1 h =\left\lfloor\log _{2} n\right\rfloor+1 h=⌊log2n⌋+1,每个非终端结点都要自上而下进行“筛选”。由于第i层上的结点数小于等于 2 i − 1 2^{i-1} 2i−1,且第i层结点最大下移的深度为h-i,每下移一层要做两次比较,所以建初堆时关键字总的比较次数为
∑ i = h − 1 1 2 i − 1 ⋅ 2 ( h − i ) ≤ 4 n \sum_{i=h-1}^{1} 2^{i-1} \cdot 2(h-i) \leq 4 n ∑i=h−112i−1⋅2(h−i)≤4n
调整“堆顶”要做n-1 次“筛选”,每次“筛选”都要将根结点下移到合适的位置,比较2(h-1)次。n 个关键字的完全二叉树的深度为 ⌊ log 2 n ⌋ + 1 \lfloor \log_2n\rfloor+1 ⌊log2n⌋+1,则重建堆时关键字总的比较次数不超过:
2 ( log 2 ( n − 1 ) ⌋ + ⌊ log 2 ( n − 2 ) ⌋ + … + log 2 2 ) < 2 n ( log 2 n ⌋ ) \left.\left.2\left(\log _{2}( n -1)\right\rfloor+\left\lfloor\log _{2}( n -2)\right\rfloor+\ldots+\log _{2} 2\right)<2 n \left(\log _{2} n \right\rfloor\right) 2(log2(n−1)⌋+⌊log2(n−2)⌋+…+log22)<2n(log2n⌋)
因此,堆排序的时间复杂度为 O ( n log 2 n ) O(n\log_2n) O(nlog2n)。
空间复杂度为 O ( 1 ) O(1) O(1)。
稳定性:不稳定。
void Merge( SqList &L,int low,int mid,int high)
{
SqList L1; L1.length = high-low+1;
int i=low, j=mid+1, k=0;//k是L1.R的下标,i、j分别为第1、2路的下标
while ( i<=mid && j<=high )
if (L.R[i].key<=L.R[j].key) //将关键字值小的记录放入L1中
{
L1.R[k]=L.R[i]; i++;k++; }
else {
L1.R[k]=L.R[j]; j++;k++; }
while (i<=mid) //如果第1路还有剩余记录,将其余下部分复制到L1
{
L1.R[k]=L.R[i]; i++;k++; }
while (j<=high) //如果第2路还有剩余记录,将其余下部分复制到L1
{
L1.R[k]=L.R[j]; j++;k++; }
for (k=0,i=low;i<=high; k++,i++)
L.R[i]=L1.R[k];//将归并后的记录复制回L中
}
void MergePass(SqList &L,int m)
{
for (int i=0;i+2*m<L.length;i=i+2*m) //归并长为m的两相邻子表
Merge(L,i,i+m-1,i+2*m-1);
if (i+m-1<L.length) //还剩下两个子表,第1段长度为m,第2段长度小于m
Merge(L,i,i+m-1,L.length-1); //归并剩余的这两个子表
}
最好情况时间复杂度为 O ( n log 2 n ) O(n\log_2n) O(nlog2n)。
最坏情况时间复杂度为 O ( n log 2 n ) O(n\log_2n) O(nlog2n)。
空间复杂度为 O ( n ) O(n) O(n)。
稳定性:稳定。
为避免顺序存储时大量移动记录的时间开销,可考虑用链表作为存储结构:直接插入排序、归并排序、基数排序
不宜采用链表作为存储结构:折半插入排序、希尔排序、快速排序、堆排序