9.1 基本概念和分类
-
核心思想:比较和移动
-
基本概念
假设含有n个记录的序列为 { r 1 , r 2 , . . . . . . , r n } \{ r_1,r_2,......,r_n\} {r1,r2,......,rn} ,其相应的关键字分别为 { k 1 , k 2 , . . . . . . , k n } \{k_1,k_2,......,k_n\} {k1,k2,......,kn} ,需确定 1,2,…,n的一种排列 p 1 , p 2 , . . . . . . , p n p_1,p_2,......,p_n p1,p2,......,pn,使其相应的关键字满足 k p 1 ⩽ k p 2 ⩽ . . . . . . ⩽ k p n k_{p1} \leqslant k_{p2} \leqslant ...... \leqslant k_{pn} kp1⩽kp2⩽......⩽kpn (非递减或非递增)关系,即使得序列成为一个按关键字有序的序列 { r p 1 , r p 2 , . . . . . . , r p n } \{r_{p1},r_{p2},......,r_{pn}\} {rp1,rp2,......,rpn}这样的操作就称为排序。
- 排序的稳定性
假设 k i = k j ( 1 ⩽ i ⩽ , 1 ⩽ j ⩽ n , i ≠ j ) k_i=k_j\space (1\leqslant i\leqslant ,1\leqslant j \leqslant n,i\neq j) ki=kj (1⩽i⩽,1⩽j⩽n,i=j),且在排序前的序列中ri领先rj。如果排序后ri仍领先于rj,则称所用的排序方法是稳定的;反之,若可能使得排序后的序列中rj领先ri,则称所用的排序方法是不稳定的。
- 内排序和外排序
内排序:在排序整个过程中,待排序的所有记录全部被放置在内存中。
外排序:由于排序的记录个数太多,不能同时放置在内存,整个排序过程需要在内外存之间多次交换数据才能进行。
-
影响排序性能的3个方面:
- 时间性能:比较和移动次数尽可能少。
- 辅助空间:执行算法所需的除存储待排序记录空间之外的其他存储空间尽可能小。
- 算法的复杂度:算法本身的复杂度(非时间复杂度)尽可能小。
-
排序结构:顺序表结构
#define MAXSIZE10 /*用于要排序数组个数最大值,可根据需要修改 */
typedef struct
{
int r[MAXSIZE+1]; /*用于存储要排序数组,r[0]用作哨兵或临时变量 */
int length; /*用于记录顺序表的长度 */
}Sqlist;
- 交换函数
/*交换L中数组r的下标为i和了的值 */
void swap(sqList *L,int i,int j)
{
int temp=L->r[i];
L->r[i]=L->r[j];
I->r[j]=temp;
}
9.2 冒泡算法
9.2.1 最简单的排序实现
/* 对顺序表工作交换排序(冒泡排序初级版) */
void BubbleSorto(SqList *L)
{
int i,j;
for(i=1;i<L->length;i++)
{
for (j=i+1;j<=L->length;j++)
{
if(L->r[i] > L->r[j])
{
swap(L,i,j); /*交换L->r[i]与L->r[j]的值*/
}
}
}
}
- 上述方法,严格不算冒泡算法。该方法好写易读,但是因关注当前i的最值,对于其余关键字排序毫无帮助,效率很低。比如上图中3和2交换后,3换至队尾。
9.2.2 冒泡排序算法
/*对顺序表L作冒泡排序 */
void BubbleSort(SqList *L)
{
int i,j;
for(i=l;i<L->length;1++)
{
for(j=L->length-1;j>=i;j--) /*注意j是从后往前循环 */
{
if(L->r[j]>L->r[j+1) /*若前者大于后者(注意这里与上一算法差异)*/
{
swap(L,j,j+1) /*交换L->r[j]与L->r[j+1]的值*/
}
}
}
}
9.2.3 冒泡排序优化
- 冒泡排序待改进的问题:如果序列为{2,1,3,4,5,6,7,8,9},当i=1 时,交换了2和1的位置;后面i=2到9的每个循环都是多余的。
/*对顺序表L作改进冒泡算法*/
void BubbleSort2(SqList *L)
{
int i,j;
Status flag-TRUE; /*tlag用来作为标记*/
for(i=1;i<L->length && flag;i++) /*若flag为False则退出循环*/
{
flag=FALSE; /*初始为false*/
for(j=L->length-1;j>=i;j--)
{
if(L->r[j] > L->r[j+1])
{
swap(L,j,j+1);/* 交换L->r[j]与L->r[+1]的值*/
flag = TRUE;/*如果有数据交换,则flag为true*/
}
}
}
}
- 改进后的代码,针对排序{2,1,3,4,5,6,7,8,9},i=2时,j循环完后没有数据交换,flag为Fasle,退出循环。
9.2.4 复杂度分析
最差情况:数组逆序,复杂度为: ∑ i = 2 n ( i − 1 ) = 1 + 2 + 3 + . . . + ( n − 1 ) = n ( n − 1 ) 2 = O [ n 2 ] \sum_{i=2}^n (i-1)=1+2+3+...+(n-1)=\frac{n(n-1)}{2}=O[n^2] i=2∑n(i−1)=1+2+3+...+(n−1)=2n(n−1)=O[n2]
9.3 简单选择排序
9.3.1 算法
- 多次对比,一次交换:对于序列为{9,1,5,8,3,7,4,6,2}的关键字排序,进行了(n-1)!=8!=36次比较、(n-1)=8次交换。
/*对顺序表L作简单选择排序 */
void Selectsort(SqList *L)
{
int i,j,min;
for(i=1;i<L->length;i++)
{
min=i; /*将当前下标定义为最小值下标 */
for(j=i+1;j<=L->length;j++)/* 循环之后的数据*/
{
if (L->r[min]>L->r[j]) /*如果有小于当前最小值的关键宇 */
min=j; /*将此关键字的下标默值给min*/
}
if(i!=min) /*若min 不等于,说明找到最小值,交换*/
swap(L,i,min); /*交换L->r[i]与L->r[min]的值*/
}
}
9.3.2 复杂度分析
比较次数为:(n-1)! = n(n-1)/2=O[n2]
9.4 直接插入排序
- 直接插入排序:将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表。
9.4.1 算法
/*对顺序表L作直接插入排序 */
void InsertSort(SqList *L)
{
int i,j;
for(i=2;i<=L->length;i++)
{
if(L->r[i]<L->r[i-1])/*需将L->r[i]插入有序子表 */
{
L->r[0]=L->r[i]; /*设置哨兵 */
for(j=i-1;L->r[j]>L->r[0];j--)
L->r[j+1]=L->r[j]; /* 记录后移*/
L->r[j+1]=L->r[0]; /*插入到正确位置*/
}
}
}
- 代码说明
1. i从2开始循环,假设r[1]已经放置好位置;
2. 假如L顺序表中r[i]小于r[i-1],即当前值小于它前面的值,那么需要将当前值与前面的值换位;
3. 将当前值r[i]赋值给r[0],设置哨兵,目的是在后面的for循环中,设置退出条件:只要r[j]<=r[0]就退出循环;
4. for循环体的作用:只要r[j]>r[0],就将r[j]值后移;
5. 退出循环后的j,为j--后因r[j]<=r[0]退出循环后的key下标。此时r[0]赋值给r[j+1]
9.4.2 复杂度分析
比较最大次数为: ∑ i = 2 n i = 2 + 3 + . . . + n = ( n + 2 ) ( n − 1 ) 2 = O [ n 2 ] \sum_{i=2}^n i=2+3+...+n=\frac{(n+2)(n-1)}{2}=O[n^2] i=2∑ni=2+3+...+n=2(n+2)(n−1)=O[n2]
移动最大次数为:
∑
i
=
2
n
(
i
+
1
)
=
(
n
+
4
)
(
n
−
1
)
2
=
O
[
n
2
]
\sum_{i=2}^n (i+1)=\frac{(n+4)(n-1)}{2}=O[n^2]
i=2∑n(i+1)=2(n+4)(n−1)=O[n2]
所以时间复杂度为O[n2]。
9.5 希尔排序(直接插入排序改进版)
- 突破O[n2]慢速排序的第一批算法
- 通过分割,先让序列基本有序,再直接插入排序
- 跳跃分割策略:将相距某个’增量‘的记录组成一个子序列,这样才能保证在子序列内分别进行直接插入排序后得到的结果基本有序而不是局部有序。
9.5.1 算法
- 该算法的精华为:关键字较小的记录不是一步一步地往前挪动,而是跳跃式地向前移动,从而使每完成一次循环后,整个序列就朝着有序坚实地迈进一步。
/*对顺序表L作希尔排序*/
void ShellSort (SqList *L)
{
int i,j;
int increment=L->length;
do /*do...while循环,先执行循环体,再判断条件*/
{
难 increment=increment/3+1;/*增量序列*/
for(i=increment+1;i <= L->length;i++)
{ /*该循环体就完成一件事:如果前大后小则交换*/
if (L->r[i] < L->r[i-increment])
{ /*需将L->r[i]插入有序增量子表*/
L->r[0]=L->r[i]; /*暂存在L-[0]*/
for(j=i-increment;j>0 && L->r[0]<L->r[j];j-=increment)
L->r[j+increment]=L->r[j]; /*记录后移,查找插入位置*/
L->r[j+increment]=L->r[0]; /*插入*/
}
}
}
while(increment>1);
}
9.5.2 复杂度分析
- 该算法的关键在于’增量‘的选取,上述代码中选用increment=increment/3+1的方式确定增量。
- 确定最好的增量目前还是一个世界难题。
- 大量研究表明,当增量序列为 d l t a [ k ] = 2 t − k + 1 ( 0 ⩽ k ⩽ ⌊ l o g 2 ( n + 1 ) ⌋ ) dlta[k]= 2^{t-k+1}(0 \leqslant k \leqslant \lfloor log_2(n+1) \rfloor) dlta[k]=2t−k+1(0⩽k⩽⌊log2(n+1)⌋)时,其时间复杂度为 O [ n 3 / 2 ] O[n^{3/2}] O[n3/2]。
9.6 堆排序
-
堆的定义:
- 特殊的完全二叉树;
- 每个结点都大于等于它的左右孩子或者小于等于它的左右孩子;
- 大于等于时为大顶堆,小于等于时为小顶堆
- 如果按照层序遍历的方式给结点从1开始编号,结点之间满足以下关系: { k i ⩾ k 2 i k i ⩾ k 2 i + 1 或 { k i ⩽ k 2 i k i ⩽ k 2 i + 1 1 ⩽ i ⩽ ⌊ n 2 ⌋ \left \{ \begin{array} {c} k_i \geqslant k_{2i} \\ k_i \geqslant k_{2i+1} \end{array} \right. 或 \left \{ \begin{array} {c} k_i \leqslant k_{2i} \\ k_i \leqslant k_{2i+1} \end{array}\right. \space\space 1\leqslant i \leqslant \lfloor \frac{n}{2} \rfloor {ki⩾k2iki⩾k2i+1或{ki⩽k2iki⩽k2i+1 1⩽i⩽⌊2n⌋
- 上面关系中 i ⩽ ⌊ n 2 ⌋ i \leqslant \lfloor \frac{n}{2} \rfloor i⩽⌊2n⌋的原因:要保证 k 2 i , k 2 i + 1 k_{2i},k_{2i+1} k2i,k2i+1 两个元素存在。
-
堆是一种不稳定的排序方法(跳跃比较和切换)。
-
不适合待排序序列个数较少的情况。
9.6.1 算法
/*对顺序表L进行堆排序 */
void HeapSort (SqList *L )
{
int i;
for(i=L->length/2;i>0;i--) /*把L中的r构建成一个大顶堆 */
HeapAdjust(L,i,L->length);
for(i=L->length;i>1;1--)
{
swap(L,1,i);/*将堆顶记录和当前未经排序子序列的最后一个记录交换*/
HeapAdjust(L,1,i-1); /*将L->r[1...i-1]重新调整为大顶堆 */
}
}
/*已知L->r[s..m]中记录的关键字除 L->r[s]之外均满足堆的定义 */
/*本函数调整L->r[s]的关键字,使L->r[s..m]成为一个大顶堆*/
void HeapAdjust(SqList *L,int s,int m)
{
int temp,j;
temp=L->r[s];
for(j=2*s;j<=m;j*=2)/*沿关健宇较大的孩子结点向下筛选 */
{
if(j<m && L->r[j]<L->r[j+1])
++j; /*j为关键宇中校大的记录的下标 */
if(temp>=L->[j])
break; /*rc应插入在位置s上*/
L->r[s]=L->r[j];
s=j;
}
L->r[s]=temp; /*插入*/
}
/*交换L中数组r的下标为i和j的值 */
void swap(sqList *L,int i,int j)
{
int temp=L->r[i];
L->r[i]=L->r[j];
I->r[j]=temp;
}
-
代码执行过程如下:
-
构建大顶堆
-
排序(收尾交换,恢复大顶堆)
-
-
代码理解难点:
HeapAdjust()函数中’s=j’:当’temp>=L->[j]'成立时,退出for循环,s还是原来的s,通过’L->r[s]=temp’将原值赋予原变量,即结点值没变;当’temp>=L->[j]'不成立时,‘L->r[s]=L->r[j]; s=j’,根结点值替换为子结点值,s赋值为当前j值,当退出for循环后再将子结点值替换为temp值,实现两结点换值。
9.6.2 复杂度分析
- 构建大顶堆过程:每个非终端结点最多进行2次比较和互换操作,这个过程时间复杂度为O[n]
- 排序过程:第i次取堆顶记录重建堆需要
O[logi]
的时间(完全二叉树的某个结点到根结点的距离为 ⌊ l o g 2 i ⌋ + 1 \lfloor log_2i \rfloor+1 ⌊log2i⌋+1),并且需要取n-1次,所以重建堆的时间复杂度O[nlongn]