10.1、基本概念 10.2、插入排序 10.3、快速排序 10.4、选择排序 10.5、归并排序...
DESCRIPTION
第十章 内部排序. 10.1、基本概念 10.2、插入排序 10.3、快速排序 10.4、选择排序 10.5、归并排序 10.6、基数排序 10.7、讨论. 8.1 基本概念. 术语 记录 —— 结点;文件 —— 线性表 关键码:能够唯一确定结点的一个或若干域。 排序码:作为排序运算依据的一个或若干域。 组合排序码,(主关键码,次关键码) 例 :(总分数,数据结构分数,大学英语分数) 排序 :将一个数据元素 ( 或记录 ) 的任意序列,重排成一个按排序码有序的序列。即排序码域的值具有不减 ( 或不增 ) 的顺序。. - PowerPoint PPT PresentationTRANSCRIPT
1
10.1 、基本概念10.2 、插入排序10.3 、快速排序10.4 、选择排序10.5 、归并排序10.6 、基数排序10.7 、讨论
第十章 内部排序
2
•术语
记录——结点;文件——线性表
关键码:能够唯一确定结点的一个或若干域。
排序码:作为排序运算依据的一个或若干域。
组合排序码, ( 主关键码,次关键码 )
例: ( 总分数,数据结构分数,大学英语分数 )
•排序:将一个数据元素 ( 或记录 ) 的任意序列,重排成一个按排序码有序的序列。即排序码域的值具有不减 ( 或不增 ) 的顺序。
8.1 基本概念
•排序问题:给定一组记录 r1, r2, …rn ,其排序码分别为 k1, k2, … , kn ,将这些记录排成顺序为rs1,rs2,…rsn 的一个序列 S ,满足条件 ks1≤ks2≤…≤ksn 。
•稳定与不稳定排序:若记录序列中的任意两个记录 Rx 、 Ry 的关键字 Kx = Ky ;如果在 排序之前和排序之后,它们的相对位置保持不变,则这种排序方法是稳定的排序,否则是不稳定的排序。•分类:内部排序:全部记录都可以同时调入内存进行的排序外部排序:文件中的记录太大,无法全部将其同时调入内存进行的排序。
4
待排序数据的存储结构
#define MAXSIZE 1000 // 待排顺序表最大长度typedef int KeyType; // 关键字类型为整数类型typedef struct { KeyType key; // 关键字项 InfoType otherinfo; // 其它数据项} RcdType; // 记录类型typedef struct { RcdType r[MAXSIZE+1]; // r[0] 闲置 int length; // 顺序表长度} SqList; // 顺序表类型
5
8.2 插入排序
有序序列 R[1..i-1]
R[i]
无序序列 R[i..n]
插入排序的基本思想:共做 n 趟排序,第 i 趟排序的过程如下
有序序列 R[1..i] 无序序列 R[i+1..n]
6
1. 直接插入排序(基于顺序查找)
3. 表插入排序(基于链表存储)
2. 折半插入排序(基于折半查找)
4. 希尔排序(基于逐趟缩小增量)
8.2 插入排序
7
1 、直接插入排序
8.2 插入排序
利用 “顺序查找”实现“在 R[1..i-1] 中查找R[i] 的插入位置”
插入过程分为两步,
1 、在已排好序的表中查找插入位置
2 、插入
不同的插入算法的区别在于第一步的不同
8
从 R[i-1] 起向前进行顺序查找,监视哨设置在R[0] ;
R[0] = R[i]; // 设置“哨兵”
循环结束表明 R[i] 的插入位置为 j +1
R[0]
j
R[i]
for (j=i-1; R[0].key<R[j].key; --j);
// 从后往前找
j=i-1
插入位置
8.2 插入排序
9
对于在查找过程中找到的那些关键字不小于 [i].key
的记录,并在查找的同时实现记录向后移动;for (j=i-1; R[0].key<R[j].key; --j); R[j+1] = R[j]R[0]
j
R[i]
j= i-1
上述循环结束后可以直接进行“插入”
插入位置
8.2 插入排序
10
void InsertionSort ( SqList &L ) { // 对顺序表 L 作直接插入排序。 for ( i=2; i<=L.length; ++i ) if (L.r[i].key < L.r[i-1].key) {
}} // InsertSort
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]; // 插入到正确位置
8.2 插入排序
例 2 :关键字序列 T= ( 21 , 25 , 49 , 25* , 16 , 08 ),请写出直接插入排序的具体实现过程。
* 表示后一个 25
解:假设该序列已存入一维数组 V[7] 中,将 V[0] 作为缓冲或暂存单元( Temp )。则程序执行过程为:
ii=1=1
2121 2525 4949 25*25* 1616 0808
0 1 2 3 4 5 6
暂暂存存 2121
ii=2=2 ii=3=3 ii=5=5ii=4=4 ii=6=6
25252525 2525 49494949 4949 25*25*25*25* 4949 16161616 25*25*0808080849492121 2525 494925*25*2121
初态:1616 494925*25*2525212116160808
完成完成 !!
12
最好的情况(关键字在记录序列中顺序有序):“ 比较”的次数:
最坏的情况(关键字在记录序列中逆序有序):“ 比较”的次数:
112
nn
i
0
2
)1()1(
2
nni
n
i
“ 移动”的次数:
“ 移动”的次数:
2
)1()1(
2
nni
n
i
空间效率: OO (( 11 ))——因为仅占用 1 个缓冲单元算法的稳定性:稳定稳定
直接插入排序算法分析:
13
平均的情况:若待排序对象序列中出现各种可能排
列的概率相同,则可取上述最好情况和最坏情况的平
均情况。在平均情况下的关键码比较次数和对象移动
次数约为 n2/4 。因此,直接插入排序的时间复杂度
为 O(n2) 。
直接插入排序是一种稳定的排序方法。
空间效率: OO (( 11 ))——因为仅占用 1 个缓冲单元
14
因为 R[1..i-1] 是一个按关键字有序的有序序列,则可以利用折半查找实现“在 R[1..i-1]
中查找 R[i] 的插入位置”,如此实现的插入排序为折半插入排序。
二、折半插入排序
15
void BiInsertionSort ( SqList &L ) {
} // BInsertSort
在 L.r[1..i-1] 中折半查找插入位置;
for ( i=2; i<=L.length; ++i ) {
} // for
L.r[0] = L.r[i]; // 将 L.r[i] 暂存到 L.r[0]
for ( j=i-1; j>=high+1; --j )
L.r[j+1] = L.r[j]; // 记录后移L.r[high+1] = L.r[0]; // 插入
16
low = 1; high = i-1;
while (low<=high) {
}
m = (low+high)/2; // 折半if (L.r[0].key < L.r[m].key)
high = m-1; // 插入点在低半区else low = m+1; // 插入点在高半区
17
14 36 49 52 80 58 61 23 97 75
i
low highm m
lowlow
m
high
14 36 49 52 58 61 80 23 97 75
i
low highm
high
m
high
m
low
例如 :
再如 :
插入位置
插入位置
L.r
L.r
18
时间效率:在插入第 i 个对象时,需要经过 log2i +1 次关键码比较,才能确定它应插入的位置。因此,将 n 个对象用折半插入排序所进行的关键码比较次数为: n*log2n
但是移动次数并未减少,所以排序效率仍为 O(n2) 。空间效率: O ( 1 )稳定性:稳定 讨论:若记录是链表结构,用直接插入排序行否?
折半插入排序呢?答:直接插入不仅可行,而且还无需移动元素,时
间效率更高!
折半插入排序的算法分析
但链表无法“折半”!但链表无法“折半”!
19
三、表插入排序(地址排序)
为了减少在排序过程中进行的“移动”记录的操作,必须改变排序过程中采用的存储结构。利用静态链表进行排序,并在排序完成之后,一次性地调整各个记录相互之间的位置,即将每个记录都调整到它们所应该在的位置上。
20
静态链表的存储结构
#define SIZE 100
Type struct{
RcdType rc;
int next;
}SLNode
Type struct{
SLNode r[SIZE];
int length;
}SLinkListType
例:关键字序列 T=(21 , 25 , 49 , 25* , 16 , 08 ), 请写出表插入排序的具体实现过程。假设该序列(结构类型 ) 已存入一维数组 V[7] 中,将 V[0]
作为表头结点。则算法执行过程为:
1
i 关键字 V[i].key 指针 V[i].link
0 MaxNum
1 21
2 25
3 49
4 25*
5 16
6 08
初态初态i=1i=1
i=2i=2
i=3i=3
i=4i=4
i=5i=5
i=6i=6
0344
5566
55
0
33
11
02
22
void LInsertionSort (Elem SL[ ] , int n){
// 对记录序列 SL[1..n] 作表插入排序。 SL[0].key = MAXINT ;
SL[0].next = 1; SL[1].next = 0;
for ( i=2; i<=n; ++i )
for ( j=0, k = SL[0].next;SL[k].key<=
SL[i].key ; j=k, k=SL[k].next )
{ SL[j].next = i; SL[i].next = k; }
// 结点 i 插入在结点 j 和结点 k 之间}// LinsertionSort
23
算法中使用了三个指针 :
其中: p 指示第 i 个记录的当前位置; i 指示第 i 个记录应在的位置; q 指示第 i+1 个记录的当前位置
如何在排序之后调整记录序列?
排序的结果只是求得一个有序链表,但只能进行顺序查找,不能进行随机查找,为了实现折半查找,还需要对纪录进行重新排列。
24
void Arrange ( Elem SL[ ], int n ) { p = SL[0].next; // p 指示第一个记录的当前位置 for ( i=1; i<n; ++i ) { while (p<i) p = SL[p].next; q = SL[p].next; // q 指示尚未调整的表尾 if ( p!= i ) { SL[p]←→SL[i]; // 交换记录,使第 i 个记录到位 SL[i].next = p; // 指向被移走的记录, } p = q; // p 指示尚未调整的表尾, // 为找第 i+1 个记录作准备 }} // Arrange
25
表插入排序算法分析:
① 时间开销:无需移动记录,只需修改 2n 次指针值。但由于比较次数没有减少,故时间效率仍为O(n2) 。
② 空间开销:空间效率肯定低,因为增开了指针分量(但在运算过程中没有用到更多的辅助单元)。
③ 稳定性: 25 和 25* 排序前后次序未变,稳定。
26
四、希尔 (shell) 排序(又称缩小增量排序)
基本思想:对待排记录序列先作“宏观”调整,再作“微观”调整。所谓“宏观”调整,指的是,“跳跃式”的插入排序。
技巧:子序列的构成不是简单地“逐段分割”,而是将相隔某个增量 dk 的记录组成一个子序列 , 让增量dk 逐趟缩短(例如依次取 5,3,1 ),直到 dk = 1 为止。优点:让关键字值小的元素能很快前移,且序列若基本有序时,再用直接插入排序处理,时间效率会高很多。
27
将记录序列分成若干子序列,分别对每个子序列进行插入排序。
其中, d 称为增量,它的值在排序过程中从大到小逐渐缩小,直至最后一趟排序减为 1 。
例如:将 n 个记录分成 d 个子序列: { R[1] , R[1+d] , R[1+2d] …, , R[1+kd] }
{ R[2] , R[2+d] , R[2+2d] …, , R[2+kd] }
…
{ R[d] , R[2d] , R[3d] …, , R[kd] , R[(k+1)d] }
例:关键字序列 T=(49 , 38 , 65 , 97, 76, 13, 27, 49* , 55, 04 ),请写出希尔排序的具体实现过程。
算法分析:开始时 dk 的值较大,子序列中的对象较少,排序速度较快;随着排序进展, dk 值逐渐变小,子序列中对象个数逐渐变多,由于前面工作的基础,大多数对象已基本有序,所以排序速度仍然很快。
38
0 1 2 3 4 5 6 7 8 9 10
49 38 65 97 76 13 27 49* 55 04初态:
第 1 趟 (dk=5)
第 2 趟 (dk=3)
第 3 趟 (dk=1)
49 1313 4938 2765 49*97 5576 0427 38 65 49* 9755
13 55
7604
5513 27 042704 4949* 4949* 7638 76 65 65 9797
5513 2704 4949* 38 76 65 9713 27
04 49* 76
97
r[i]r[i]
29
void ShellInsert ( SqList &L, int dk ) { for ( i=dk+1; i<=n; ++i ) if ( L.r[i].key< L.r[i-dk].key) { L.r[0] = L.r[i]; // 暂存在 R[0] for (j=i-dk; j>0&&(L.r[0].key<L.r[j].key); j-=dk) L.r[j+dk] = L.r[j]; // 记录后移,查找插入位置 L.r[j+dk] = L.r[0]; // 插入 } // if} // ShellInsert
30
void ShellSort (SqList &L, int dlta[ ], int t)
{ // 增量为 dlta[] 的希尔排序 for (k=0; k<t; ++t)
ShellInsert(L, dlta[k]);
// 一趟增量为 dlta[k] 的插入排序} // ShellSort
31
时间效率:
空间效率: OO (( 11 ))——因为仅占用 1 个缓冲单元算法的稳定性:不稳定不稳定——因为 49* 排序后却到了
49的前面
O(O(nn1.251.25 )~)~ OO (( 1.61.6nn1.251.25 ))——经验公式
算法分析:开始时 dk 的值较大,子序列中的对象较少,排序速度较快;随着排序进展, dk 值逐渐变小,子序列中对象个数逐渐变多,由于前面工作的基础,大多数对象已基本有序,所以排序速度仍然很快。
32
对特定的待排序对象序列,可以准确地估算关键码的比较次数和对象移动次数。但想要弄清关键码比较次数和对象移动次数与增量选择之间的依赖关系,并给出完整的数学分析,还没有人能够做到。
Knuth 利用大量的实验统计资料得出,当 n 很大时,关键码平均比较次数和对象平均移动次数大约在 n1.25 到 1.6n1.25 的范围内。这是在利用直接插入排序作为子序列排序方法的情况下得到的。
33
10.3 快速排序
1) 冒泡排序基本思路:每趟不断将记录两两比较,并按“前小
后大”(或“前大后小”)规则交换。优点:每趟结束时,不仅能挤出一个最大值到最后
面位置,还能同时部分理顺其他元素;一旦下趟没有交换发生,还可以提前结束排序。
前提:顺序存储结构
void BubbleSort(Elem R[ ], int n) { while (i >1) {
} // while} // BubbleSort
i = n;
i = lastExchangeIndex; // 本趟进行过交换的 // 最后一个记录的位置
if (R[j+1].key < R[j].key) { Swap(R[j], R[j+1]); lastExchangeIndex = j; // 记下进行交换的记录位置
} //if
for (j = 1; j < i; j++)
lastExchangeIndex = 1;
35
注意 :
2. 一般情况下,每经过一趟“起泡”,“ i 减一”,但并不是每趟都如此。
例如 :
5 2 3 1 9 7 82 5 53 1 5 7 98 9
i=7i=6for (j = 1; j < i; j++) if (R[j+1].key < R[j].key) …
1 3
i=2
1. 起泡排序的结束条件为, 最后一趟没有进行“交换记录”。
36
时间分析 :
最好的情况(关键字在记录序列中顺序有序): 只需进行一趟起泡
最坏的情况:初始排列逆序,算法要执行 n-1 趟起泡,第 i趟 (1 i n) 做了 n- i 次关键码比较,执行了n-i 次对象交换。
“ 比较”的次数:
0
“ 移动”的次数:
2
)1()1(
2
nni
ni 2
)1(3)1(3
2
nni
ni
“ 比较”的次数: “ 移动”的次数:n-1
37
2 ) 快速排序
从待排序列中任取一个元素 ( 例如取第一个 ) 作为中心,所有比它小的元素一律前放,所有比它大的元素一律后放,形成左右两个子表;然后再对各子表重新选择中心元素并依此规则调整,直到每个子表的元素只剩一个。此时便为有序序列了。
基本思想:
优点:因为每趟可以确定不止一个元素的位置,而且呈指数增加,所以特别快!
前提:顺序存储结构
例:关键字序列 T=(21 , 25 , 49 , 25* , 16 , 08 ),快速排序的算法步骤 :
讨论:1. 这种不断划分子表的过程,计算机如何自动实现?
2. “ 快速排序”是否真的比任何排序算法都快?
( ) ,
21 , 25 , 49 , 25* , 16 , 08
初态:
第 1趟:
第 2趟:
第 3趟:
08 , 16 , 21 , 25 , 25* ,(49)
2116 , 08 , ( )25 , 25* , 49
(08) , 16 , 21 ,25 , (25* , 49)
39
1. 这种不断划分子表的过程,计算机如何自动实现?编程时:①每一趟的子表的形成是采用从两头向中间交替式逼近法;②由于每趟中对各子表的操作都相似,主程序可采用递归算法。
int PartitionPartition(SqList &L,int low,int high){ //一趟快排// 交换子表 r[low…high]的记录,使支点(枢轴)记录到位,并返回其位置。返回时,在支点之前的记录均不大于它,支点之后的记录均不小于它。
r[0]=r[low]; //以子表的首记录作为支点记录,放入 r[0]单元
(续下页)
一趟快速排序算法(针对一个子表的操作)
pivotkey=r[low].key; // 取 支 点 的 关 键 码 存 入pivotkey变量while(low < high){{ //从表的两端交替地向中间扫描
while(low<high && r[high].key>=pivotkey ) - -high;- -high;
r[low]=r[high]; //将比支点小的记录交换到低端;
while(low<high && r[low].key<=pivotkey) + + +low;+low;
r[high]=r[low]; //将比支点大的记录交换到高端;
}}
r[low]=r[0]; //支点记录到位;
return low; //返回支点记录所在位置。
}//Partition
Low=high=3 ,本趟停止,将支点定位并返回位置信息
例 2 :关键字序列 T=(21 , 25 , 49 , 25* , 16 , 08 ),请写出快速排序算法的一趟实现过程。
r[i] 0 1 2 3 4 5 6
初态 21 25 49 25* 16 08
第 1 趟
high
low
21 0825 1649 25*
3
21
pivotkey=21
08 2516 49
( 08 , 16 ) 21 ( 25* , 49 , 25 )
25* 跑到了前面,不稳定!
42
j 从高端扫描寻找小于pivot 的元素
i 从低端扫描寻找大于pivot 的元素
i=low; j=high;r[0]=r[low]; pivot=r[low].key;
i < j
i < j &&r[j].key>=pivot
--j;
r[i] = r[j];
i < j &&r[i].key<=pivot
--i;
r[j] = r[i];
r[i] = r[0];
return ok;YY
YY
YY
NN
NN
NN
一趟快速排序算法流程图一趟快速排序算法流程图
43
void QSort ( SqList &L, int low, int high ) {
if ( low < high) {
pivotpivot = Partition ( L, low, high );
QSort ( L, low, pivot-1);
QSort ( L, pivot+1, high );
}
}
整个快速排序的递归算法:
// 长度 >1
//QSort
void void QQuickuickSort Sort ( ( SqList SqList &L) {&L) {
QSort QSort (L,(L, 1, L.length ););
}}
对顺序表 L进行快速排序的操作函数为:
例:以关键字序列( 256 , 301 , 751 , 129, 937 , 863 , 742 , 694 , 076 , 438 )为例,写出执行快速算法的各趟排序结束时,关键字序列的状态。
原始序列: 256 , 301 , 751 , 129, 937 , 863 , 742 , 694 ,076 , 438
第 1 趟
第 2 趟
第 3 趟
第 4 趟
256 , 301 , 751 , 129, 937 , 863 , 742 , 694 , 076 , 438
076076 , 129, 256256 , 751751 , 937 , 863 , 742 , 694 , 301 , 438
256256
076 301
129 751256256
076076 , 129, 256256 , 438 , 301 , 694 , 742 , 694 ,863 , 937
751751
076076 , 129129, 256256 , 438438 , 301 , 694 , 742 , 751751 ,863863 , 937076076 , 129129, 256256 , 301 , 301 , 694 , 742 , 751751 ,863863 , 937
434388
076076 , 129129, 256256 , 301301 , 438438 , 694694 , 742 , 751751 ,863863 , 937937
45
快速排序算法分析:•快速排序是递归的,需要有一个栈存放每层递归调用时的指针和参数(新的 low和 high)。
•可以证明,函数 quicksort 的平均计算时间也是 O(nlog2n) 。实验结果表明:就平均计算时间而言,快速排序是我们所讨论的所有内排序方法中最好的一个。
•最大递归调用层次数与递归树的深度一致,理想情况为 log2(n+1) 。因此,要求存储开销为 o(log2n) 。
•如果每次划分对一个对象定位后,该对象的左侧子序列与右侧子序列的长度相同,则下一步将是对两个长度减半的子序列进行排序,这是最理想的情况。此时,快速排序的趟数最少。
稳 定 性: 不稳定 —因为可选任一元素为支点。
46
讨论 2. “ 快速排序”是否真的比任何排序算法都快?
设每个子表的支点都在中间(比较均衡),则:第 1趟比较,可以确定 1个元素的位置;第 2趟比较( 2个子表),可以再确定 2个元素的位置;第 3趟比较( 4个子表),可以再确定 4个元素的位置;第 4趟比较( 8个子表),可以再确定 8个元素的位置; ……只需 log2n + 1 趟便可排好序。
—— 基本上是!因为每趟可以确定的数据元素是呈指数增加的!
而且,每趟需要比较和移动的元素也呈指数下降,加上编程时使用了交替逼近技巧,更进一步减少了移动次数,所以速度特别快。
47
10.4 选择排序
选择排序有多种具体实现算法:
1) 简单选择排序
2) 锦标赛排序
3) 堆排序
基本思想:每一趟在后面 n-i 个待排记录中选取关键字最小的记录作为有序序列中的第 i 个记录。
48
1 )简单选择排序思路:每经过一趟比较就找出一个最小值,与待排序列最前面的位置互换即可。——首先,在 n 个记录中选择最小者放到 r[1] 位置;然后,从剩余的 n-1 个记录中选择最小者放到r[2] 位置;…如此进行下去,直到全部有序为止。
优点:实现简单
缺点:每趟只能确定一个元素,表长为 n 时需要 n-1趟
前提:顺序存储结构
49
例:关键字序列 T= ( 21 , 25 , 49 , 25* , 16 , 08 ),请给出简单
选择排序的具体实现过程。原始序列: 21 , 25 , 49 , 25* , 16 ,08
第 1趟
第 2趟
第 3趟
第 4趟
第 5趟
08 , 25 , 49 , 25* , 16 ,21
08 , 16, 49 , 25* , 25 ,21
08 , 16, 21 , 25* , 25 ,49
08 , 16, 21 , 25* , 25 ,49
08 , 16, 21 , 25* , 25 ,49
时间效率: O(nO(n22))——虽移动次数较少,但比较次数仍多。 空间效率: OO (( 11 ))——无需任何附加单元!算法的稳定性:不稳定不稳定——因为排序时, 25* 到了 25 的前面。
50
简单选择排序的算法如下:
Void SelectSort(SqList &L ) {
for (i=1; i<L.length; ++i){
j = SelectMinKey(L,i);
if( i!=j ) r[i] r[j]; } } //SelectSort
// 对顺序表 L 作简单选择排序
//选择第 i 小的记录,并交换到位
// 在 r[i…L.length] 中选择 key最小的记录//与第 i 个记录交
换
讨论:能否利用(或记忆)首趟的 n-1 次比较所得信息,从而尽量减少后续比较次数呢?
能!——锦标赛排序和堆排序!
51
2 ) 锦标赛排序 (又称树形选择排序 )基本思想:与体育比赛时的淘汰赛类似。 首先对 n 个记录的关键字进行两两比较,得到 n/2 个优胜者 ( 关键字小者 ) ,作为第一步比较的结果保留下来。然后在这 n/2 个较小者之间再进行两两比较,…,如此重复,直到选出最小关键字的记录为止。优点:减少比较次数,加快排序速度缺点:空间效率低
例:关键字序列 T= ( 21 , 25 , 49 , 25* , 16 , 08 , 63 ),请给出锦标赛排序的具体实现过程。
52
08080808
Winner Winner (( 胜者胜者 ))
21212121 08080808
08080808 6363636325*25*25*25*21212121
21212121 25252525 49494949 25*25*25*25* 16161616 08080808 63636363
rr[1][1]
注:为便于自动处理,建议每个记录多开两个特殊分量:key otherinfo Index( 结点位置编号) Tag (是否参加比较)
初态:初态: 补足补足 22kk( k=( k=loglog22nn )) 个叶子结个叶子结点点
第一趟:第一趟:
53
第二趟:第二趟:
08080808
21212121 08080808
08080808 6363636325*25*25*25*21212121
21212121 25252525 49494949 25*25*25*25* 16161616 08080808 63636363
16161616
16161616
16161616 rr[2][2]
Winner Winner (( 胜者胜者 ))
求次小值 16时 , 只需比较22次,即只比较 loglog22nn - -11 次。次。
令其 Tag = 0,不参与比较
54
令其 Tag = 0,不参与比较
第三趟:第三趟:
16161616
21212121 16161616
16161616 6363636325*25*25*25*21212121
21212121 25252525 49494949 25*25*25*25* 16161616 08080808 63636363
rr[3][3]
Winner Winner (( 胜者胜者 ))
63636363
21212121
55
08080808
21212121 08080808
08080808 6363636325*25*25*25*21212121
21212121 25252525 49494949 25*25*25*25* 16161616 08080808 63636363
63636363
21212121
第四趟:第四趟:
rr[4][4]
Winner Winner (( 胜者胜者 ))
25252525
25252525
25252525
56
08080808
21212121 08080808
08080808 6363636325*25*25*25*21212121
21212121 25252525 49494949 25*25*25*25* 16161616 08080808 63636363
16161616
16161616
16161616
63636363
21212121
25252525
25252525
25252525
第五趟:第五趟:
rr[5][5]
Winner Winner (( 胜者胜者 ))
25*25*25*25*
25*25*25*25*
57
08080808
21212121 08080808
08080808 6363636325*25*25*25*21212121
21212121 25252525 49494949 25*25*25*25* 16161616 08080808 63636363
16161616
16161616
16161616
63636363
21212121
25252525
25252525
25252525
25*25*25*25*
25*25*25*25*
第六趟:第六趟:
rr[6][6]
Winner Winner (( 胜者胜者 ))
49494949
49494949
49494949
58
08080808
21212121 08080808
08080808 6363636325*25*25*25*21212121
21212121 25252525 49494949 25*25*25*25* 16161616 08080808 63636363
16161616
16161616
16161616
63636363
21212121
25252525
25252525
25252525
25*25*25*25*
25*25*25*25*
49494949
49494949
49494949
第七趟:第七趟: rr[7][7]Winner Winner (( 胜者胜者 ))
63636363
59
算法分析:
•锦标赛排序构成的树是满 (完全)二叉树,其深度为 log2n +1 ,其中 n 为待排序元素个数。
• 时间复杂度 O(nlog2n)—n 个记录各自比较约log2n 次
• 空间效率 O(n )—胜者树的附加内结点共有 n’-1个!
• 稳定性:稳定 —左右结点相同者左为先
60
3 ) 堆排序
1. 什么是堆?
堆的定义:设有 n 个元素的序列 k1 , k2 ,…, kn ,当且仅当满足下述关系之一时,称之为堆。
kki i ≤≤ k k2i2i
kki i ≤≤ k k2i+12i+1
kki i ≥≥ kk2i2i
kki i ≥≥ k k2i+12i+1
或者或者 i=1, 2,… n/2i=1, 2,… n/2
2. 怎样建堆? 3. 怎样堆排序?
61
如果让满足以上条件的元素序列 (如果让满足以上条件的元素序列 ( kk11 ,, kk22 ,,
…,…, kknn ))顺次排成一棵顺次排成一棵完全二叉树完全二叉树,,则此树的特点则此树的特点是:是: 树中所有结点的值均大于(或小于)其左右孩子,树中所有结点的值均大于(或小于)其左右孩子,此树的根结点(此树的根结点(即堆顶即堆顶))必最大(或最小)。必最大(或最小)。
0808
2525
4646
4949
5858 6767
2 3
4 5 6
1
(大根堆)
9191
8585
6666
7676
5858 6767
2 3
4 5 6
1
55557
例 有序列 T1= ( 08, 25, 49, 46, 58, 67 )和序列T2= ( 91, 85, 76, 66, 58, 67, 55 ) , 判断它们是否 “堆”?
√ (小根堆) √(小顶堆) (最小堆)
(大顶堆)(最大堆)
63
步骤:从最后一个非终端结点开始往前逐步调整,让每个双亲大于(或小于)子女,直到根结点为止。
2. 怎样建堆?
注:终端结点(即叶子)没有任何子女,无需单独调整。
64
2121
2525
25*25*
4949
1616 0808
1
2 3
4 5 6
例:关键字序列 T= (21 , 25 , 49 , 25* , 16 , 08 ),建大根堆。为便于理解,先将原始序列画成完全二叉树的形式:
完全二叉树的第一个非终端结点编号必为 n/2 !(性质 5)
2121i=3:i=3: 49 大于 08 ,不必调整;i=2:i=2: 25 大于 25* 和 16 ,也不必调整;i=1:i=1: 21 小于 25 和 49 ,要调整!
4949
而且 21 还应当向下比较!!
65
void HeapSort (HeapType &H ) {
//H//H 是顺序表,含有是顺序表,含有 H.rH.r[ ][ ] 和和 H.lengthH.length 两个分量两个分量 for ( i = length / 2; i >0; - - i ) //// 把把 rr[[1…length1…length]] 建成大根建成大根堆堆 HeapAdjust(r, i, length ); //// 使使 rr[[i…lengthi…length]] 成为大根堆成为大根堆 ……
}
建堆算法 (堆排序算法中的第一步)
这是针对结点 i 的堆调整函数,其含义是:从结点 i开始到堆尾为止,自上向下比较,如果子女的值大于双亲结点的值,则互相交换,即把局部调整为大根堆。
针对结点 i 的堆调整函数 HeapAdjust 如下:
HeapAdjust(r, i, m ){
current=i; child=2*i; temp=r[i];
while(child<=m){
if ( child<m && r[child].key<r[child+1].key )
child= child+1;
if(temp.key>=r[child].key)breack;
else { r[current]=r[child];
current= child; child=2* child; }
}
r[current]=temp;
} // HeapAdjust
//temp 是根, child 是其左孩子 // 检查是否到达当前堆尾
// 让 child 指向两子女中的大者 // 根大则不必调整,结束整个函数
// 否则子女中的大者上移
// 并继续向下整理!并继续向下整理! // 直到自下而上都满足堆定义,再安置老根
——从结点 i 开始到当前堆尾 m 为止,自上向下比较,如果子女的值大于双亲结点的值,则互相交换,即把局部调整为大根堆。
// 将根下降到子女位置
67
关键:将堆的当前顶点输出后,如何将剩余序列重新调整为堆?
方法:将当前顶点与堆尾记录交换,然后仿建堆动作重新调整,如此反复直至排序结束。
3. 怎样进行堆排序?
68
08 25 21 25* 16 08 25 21 25* 16 494908 25 21 25* 16 08 25 21 25* 16 4949
交换 交换 11 号与 号与 6 6 号记录号记录
4949
2525
25*25*
2121
1616 0808
1
2 3
4 5 6
49 25 21 25* 16 0849 25 21 25* 16 0849 25 21 25* 16 0849 25 21 25* 16 08
初始最大堆初始最大堆
2525
25*25* 1616
2121
1
3
654
2
0808
4949
例:对刚才建好的大根堆进行排序:
69
16 25* 21 08 16 25* 21 08 25 4925 4916 25* 21 08 16 25* 21 08 25 4925 49
交换 交换 11 号与 号与 5 5 号记录号记录
0808 25 2125 21 25* 16 25* 16 49490808 25 2125 21 25* 16 25* 16 4949
从 从 1 1 号到 号到 5 5 号 重新号 重新调整为最大堆调整为最大堆
0808
2525
25*25*
2121
1616 4949
1
2 3
4 5 6
1616
25*25*
0808 2525
2121
4949
1
3
654
20808
2525
25*25*
25 25 0808 21 21 25* 16 25* 16 494925 25 0808 21 21 25* 16 25* 16 4949
0808
25 25 25* 25* 2121 08 08 16 16 494925 25 25* 25* 2121 08 08 16 16 4949
70
08 16 21 08 16 21 25* 25 4925* 25 4908 16 21 08 16 21 25* 25 4925* 25 49
交换 交换 1 1 号与 号与 4 4 号记录号记录
25* 16 21 08 25* 16 21 08 25 4925 4925* 16 21 08 25* 16 21 08 25 4925 49
从 从 11 号到 号到 44 号 重新号 重新调整为最大堆调整为最大堆
25*25*
1616
0808
2121
2525 4949
1
2 3
4 5 6
0808
1616
25*25* 2525
2121
4949
1
3
654
2
71
08 16 08 16 21 25* 25 4921 25* 25 4908 16 08 16 21 25* 25 4921 25* 25 49
交换 交换 11 号与号与 3 3 号记录号记录
21 16 08 21 16 08 25* 25 4925* 25 4921 16 08 21 16 08 25* 25 4925* 25 49
从 从 1 1 号到 号到 33 号 重新号 重新调整为最大堆调整为最大堆
2121
1616
25*25*
0808
2525 4949
1
2 3
4 5 6
0808
1616
25*25* 2525
2121
4949
1
3
654
2
72
08 08 16 21 25* 25 4916 21 25* 25 4908 08 16 21 25* 25 4916 21 25* 25 49
交换 交换 1 1 号与号与 2 2 号记录号记录
16 08 16 08 2121 25* 25 4925* 25 4916 08 16 08 2121 25* 25 4925* 25 49
从 从 1 1 号到 号到 2 2 号 重新号 重新调整为最大堆调整为最大堆
1616
0808
25*25*
2121
2525 4949
1
2 3
4 5 6
0808
1616
25*25* 2525
2121
4949
1
3
654
2
73
void HeapSort (HeapType &H ) {
// 对顺序表 H 进行堆排序 for ( i = H.length / 2; i >0; -- i )
HeapAdjust(H,i, H.length ); // 初始堆 for ( i = H.length; i > 1; --i) {
H.r[1] H.r[i]; // 交换,要借用 temp
HeapAdjust( H, 1,i-1 ); // 重建最大堆 }
}
堆排序的算法
这是针对结点 i 的堆调整函数(也是建堆函数),每次调用耗时 O(log2n)
74
基于初始堆进行堆排序的算法步骤:
堆的第一个对象堆的第一个对象 VV[0][0] 具有最大的关键码,将具有最大的关键码,将VV[0][0] 与与 VV[[nn]] 对调,把具有最大关键码的对象交对调,把具有最大关键码的对象交换到最后,再对前面的换到最后,再对前面的 nn-1-1 个对象,使用堆的调个对象,使用堆的调整算法,重新建立堆。结果具有次最大关键码的对整算法,重新建立堆。结果具有次最大关键码的对象又上浮到堆顶,即象又上浮到堆顶,即 VV[0][0] 位置。位置。
•再对调再对调 VV[0][0] 和和 VV[[n-n-1]1] ,,调用对前调用对前 nn-2-2 个对象个对象
重新调整,…如此反复,最后得到全部排序好的对重新调整,…如此反复,最后得到全部排序好的对
象序列。象序列。
75
堆排序算法分析:堆排序算法分析:
• 时间效率:时间效率: O(O(nnloglog22nn)) 。。因为整个排序过程中需因为整个排序过程中需要调用要调用 nn-1-1 次次 HeapAdjust( ) 算法,而算法本身算法,而算法本身耗时为耗时为 loglog22nn ;;
• 空间效率:空间效率: O(1)O(1) 。。仅在第二个仅在第二个 forfor 循环中交换循环中交换记录时用到一个临时变量记录时用到一个临时变量 temptemp 。。
• 稳定性:稳定性: 不稳定。不稳定。• 优点:优点:对小文件效果不明显,但对大文件有效。对小文件效果不明显,但对大文件有效。
76
10.5 归并排序基本思想:将两个(或以上)的有序表组成新的有序表。
更实际的意义:可以把一个长度为 n 的无序序列看成是 n 个长度为 1 的有序子序列 ,首先做两两归并,得到 n/2 个长度为 2 的子序列;再做两两归并,…,如此重复,直到最后得到一个长度为 n 的有序序列。
例:例:关键字序列 T= ( 21 , 25 , 49 , 25* , 93 , 62 , 72 , 08 , 37 , 16 , 54 ),请给出归并排序的具体实现过程。
77
2121 2525 25*25* 9393 6262 7272 0808 3737 1616 54544949
2121 2525 25*25* 4949 6262 9393 0808 7272 1616 3737 5454
1616 3737 5454
2121 2525 25*25* 4949 0808 6262 7272 9393
0808 2121 2525 25*25* 4949 6262 7272 9393
0808 1616 2121 2525 25*25* 3737 4949 5454 6262 7272 9393
lenlen=1=1
lenlen=2=2
lenlen=4=4
lenlen=8=8
lenlen=16=16
1616 3737 5454
整个归并排序仅需整个归并排序仅需 loglog22n n 趟趟
78
一趟归并排序算法 : ( 两路有序并为一路 )
void Merge Merge (SR , &TR , i, m, n) { // 将有序的 SR[i…m] 和 SR[m+1…n] 归并为有序的TR[i…n] for(k=i , j=m+1; i<=m && j<=n; ++k ) {if ( SR[i]<= SR[j] )TR[k]=SR[i++]; else TR[k]=SR[j++]; // 将 SR 中记录由小到大地并入 TR
} if (i<=m) TR[k…n]=SR[i…m]; // 将剩余的 SR[i…m] 复制到 TR if (j<=n) TR[k…n]=SR[j…n]; // 将剩余的 SR[j…n] 复制到 TR} // Merge
79
void MSort (SR , &TR1 , s, t) { // 将无序的 SR[s…t] 归并排序为TR1[s…t] if ( s==t )TR1[s]=SR[s]; // 当 len=1 时返回 else { m=(s+t)/2; // 将 SR [s…t] 平分为 SR [s…m] 和 SR [m+1…t] MSort (SR , &TR2 , s, m); // 将 SR 一分为二 , 2 分为 4… // 递归地将 SR [s…m] 归并为有序的 TR2[s…m] MSort (SR , &TR2 , m+1, t ); // 递归地将 SR [m+1…t] 归并为有序的TR2[m+1…t] MergeMerge(TR2 , TR1 , s, m, t ); // 将 TR2 [s…m] 和 TR2 [m+1…t] 归并到 TR1 [s…t] } } // MSort
递归形式的两路归并排序算法 : ( 一路无序变为有序 )
简言之,先由“长”无序变成“短”有序, 再从“短”有序归并为“长”有序。
初次调用时为( L, L, 1, length )
80
归并排序算法分析:
• 时间效率:时间效率: O(O(nnloglog22nn))在递归的归并排序算法中,函数 Merge( ) 做一趟两路归 并 排 序 , 需 要 调 用 merge ( ) 函数 n/(2*len) O(n/len) 次,函数 Merge( ) 调用 Merge( )正好 log2n 次,而每次 merge( )要执行比较 O(len) 次,所以算法总的时间复杂度为O(nlog2n) 。
• 空间效率:空间效率: O(O(nn) ) 因为需要一个与原始序列同样大小的辅助序列( TR )。这正是此算法的缺点。• 稳定性:稳定性:稳定稳定
81
10.6 基数排序 ((Radix Sort)Radix Sort)
问题:1. 什么是“多关键字”排序?实现方法?2. 单逻辑关键字怎样“按位值”排序?
基本思想:
借助多关键字排序的思想对单逻辑关键字进行排序。即:用关键字不同的位值进行排序。
82
1. 什么是“多关键字”排序?实现方法?
例 1 :对一副扑克牌该如何排序? 若规定花色和面值的顺序关系为: 花色: 面值: 2 < 3 < 4 < 5 < 6 < 7 < 8 < 9 < 10 < J < Q < K < A 则可以先按花色排序,花色相同者再按面值排序 也可以先按面值排序,面值相同者再按花色排序。
83
例 2 :职工分房该如何排序? 内大规定:先以总分排序(职称分+工龄分);总分相同者,再按配偶总分排序,其次按配偶职称、工龄、人口……等等排序。例 3 :学生评奖学金如何排序?
多关键字排序的实现方法通常有两种:• 最高位优先法 MSD(Most Significant Digit
first)• 最低位优先法 LSD(Least Significant Digit first)
84
例:对一副扑克牌该如何排序?
若规定花色为第一关键字(高位),面值为第二关键字(低位),则使用 MSD 和 LSD 方法都可以达到排序目的。
MSD 方法的思路:先设立 4 个花色“箱”,将全部牌按花色分别归入 4 个箱内(每个箱中有 13张牌);然后对每个箱中的牌按面值进行插入排序(或其它稳定算法)。
LSD 方法的思路:先按面值分成 13堆(每堆 4张牌),然后对每堆中的牌按花色进行排序(用插入排序等稳定的算法)。
用哪种方法更快些用哪种方法更快些??
85
2. 单逻辑关键字怎样“按位值”排序?
设 n 个记录的序列为: {V0, V1, …, Vn-1 } ,可以把每个记录 Vi 的单关键码 Ki
看成是一个d 元组( Ki
1, Ki2, …, Ki
d ),则其中的每一个分量 Ki
j ( 1 j d ) 也可看成是一个关键字。
注 1 : Ki1=最高位, Ki
d=最低位; Ki 共有 d 位,可看成 d 元组;注 2 : 每个分量 Ki
j (1 j d ) 有 radix 种取值,则称 radix 为基数。
思路:
86
44
2626
33
1010例 1 :关键码 984 可以看成是 元组;基数
radix 为 。
例 2 :关键码关键码 diandian 可以看成是可以看成是 元组;基元组;基
数数 radixradix 为为 。。
87
因为有分组,故此算法需递归实现。
讨论:是借用 MSD 方式来排序呢,还是借用 LSD 方式?例:初始关键字序列 T= ( 32, 13, 27, 32*, 19 , 33 ),请
分别用 MSD 和 LSD 进行排序,并讨论其优缺点。
法 1 ( MSD ):原始序列: 32, 13, 27, 32*, 19 , 33 先按高位 KKii
11 排序:( 13, 19 ) , 27, ( 32, 32* ,33 ) 再按低位 KKii
2 2 排序 : 13, 19 , 27, 32, 32*, 33法 2 ( LSD ): 原始序列: 32, 13, 27, 32*, 19 ,33 先按低位 KKii
22 排序: 32, 32*, 13, 33, 27, 19 再按高位 KKii
11 排序: 13, 19 , 27, 32, 32*, 33
无需分组,易编程实现!
例:例:T=T= (( 0202 ,, 7777 ,, 7070 ,, 5454 ,, 6464 ,, 2121 ,, 5555 ,, 1111 ),),用用 LSDLSD 排序。排序。①①各关键字可视为各关键字可视为 22 元组元组;②每位的取值范围是:;②每位的取值范围是: 0-90-9;即;即基数基数 radixradix == 10 10 。。因此,特设置因此,特设置 1010 个队列,并编号为个队列,并编号为 0-0-99。。
11
55
21
64
54
70
77
02
原始序列原始序列
1122334455667788
先对低位扫描先对低位扫描 出队出队
00112233445566778899
1010 个队列个队列
计算机怎样实现计算机怎样实现 LSDLSD 算法?算法?
分配分配过程过程
收集收集过程过程
下一步下一步
77
55
64
54
02
11
21
701122334455667788
出队后序列出队后序列
77
55
54 , 64
21 , 11
70
02
又称散列过程!
89
00112233445566778899
再次入队再次入队
再次出队再次出队再对高位扫描再对高位扫描
小结:小结:排序时经过了反复的“分配”和“收集”过程。当对关键字所有的位进行扫描排序后,整个序列便从无序变为有序了。
77
55
64
54
02
11
21
701122334455667788
出队后序列出队后序列
70 , 77
64
54 , 55
21
11
02
再次再次分配分配 再次再次
收集收集 77
70
64
55
54
21
11
02
再次出队后序列再次出队后序列
这种这种 LSDLSD 排序方法称为排序方法称为 ::基数排基数排序序
90
用链队列来实现基数排序用链队列来实现基数排序—— 链式基数排序链式基数排序
实现思路: 针对 d 元组中的每一位分量,把原始链表中的所有记录 , 按 Ki
j 的取值 , 让 j = d, d-1, …, 1 ,
① 先“分配”到 radix 个链队列中去;② 然后再按各链队列的顺序,依次把记录从链队列中
“收集”起来;③ 分别用这种“分配”、“收集”的运算逐趟进行排
序;④ 在最后一趟“分配”、“收集” 完成后,所有记录就按其关键码的值从小到大排好序了。
实现以下关键字序列的实现以下关键字序列的链式基数排序链式基数排序::T=T= (( 614614 ,, 738738 ,, 921921 ,, 485485 ,, 637637 , ,
101101 ,, 215215 ,, 530530 ,, 790790 ,, 306306 ))
例例
614614614614 921921921921 485485485485 637637637637738738738738 101101101101 215215215215 530530530530 790790790790 306306306306
第一趟分配第一趟分配e[0] e[1] e[2] e[3] e[4] e[5] e[6] e[7] e[8] e[9]
614614 738738921921 485485 637637
101101 215215
530530
790790
306306
f[0] f[1] f[2] f[3] f[4] f[5] f[6] f[7] f[8] f[9]
原始序列链表:原始序列链表:r[0]→
(从最低位 (从最低位 i i = 3= 3 开始排序,开始排序, f[ ] 是队首指针,是队首指针, e[ ] 为队尾指针)为队尾指针)
第一趟收集第一趟收集(让队尾指针(让队尾指针 e[i] 链接到下一非空队首指针链接到下一非空队首指针 f[i+1 ] 即即可)可) 530530 790790 921921 101101 614614 485485 215215 306306 637637 738738r[0]→
92
第一趟收集的结果:第一趟收集的结果:
e[0] e[1] e[2] e[3] e[4] e[5] e[6] e[7] e[8] e[9]
614614
738738
921921 485485637637
101101
215215
530530 790790
306306
f[0] f[1] f[2] f[3] f[4] f[5] f[6] f[7] f[8] f[9]
第二趟分配第二趟分配(按次低位 (按次低位 ii = 2 = 2 ))
530530 790790 921921 101101 614614 485485 215215 306306 637637 738738
第二趟收集第二趟收集(让队尾指针(让队尾指针 e[i] 链接到下一非空队首指针链接到下一非空队首指针 f[i+1 ] ))
530530 790790921921101101 614614 485485215215306306 637637 738738
r[0]→
r[0]→
93
第二趟收集的结果:第二趟收集的结果:530530 790790921921101101 614614 485485215215306306 637637 738738
e[0] e[1] e[2] e[3] e[4] e[5] e[6] e[7] e[8] e[9]
614614 738738 921921485485
637637
101101 215215 530530
790790
306306
f[0] f[1] f[2] f[3] f[4] f[5] f[6] f[7] f[8] f[9]
第三趟分配第三趟分配(按最高位 (按最高位 ii = 1 = 1 ))
第三趟收集第三趟收集(让队尾指针(让队尾指针 e[i] 链接到下一非空队首指针链接到下一非空队首指针 f[i+1 ] ))
530530 790790 921921101101 614614485485215215 306306 637637 738738
r[0]→
r[0]→
排序结束!排序结束!
94
1. 排序前后的 n 个记录都用顺序表 r[ ] 存储,但建议增开 n 个指针分量(转为静态链表形式);这样在记录重排时不必移动数据,只需修改各记录的链接指针即可。
2. 在 radix 个队列中,每个队列都要设置两 个指针:
int f [radix] 指示队头 ( f[ j ]初始为空); int e [radix] 指向队尾( e[ j ] 不必单独初始化 ) ;分配到同一队列的关键码要用链接指针链接起来。 (以上一共增开了 n+2 radix 个附加指针分量)
如何编程实现? 先作若干说明:
95
3. 待排序记录存入 r[ ] 中, r[0] 作为头结点;每个记录都包含 key 分量、 othernifo 分量和指针域 intint next 分量。 另外,为能单独表示单关键码 key 的各位,将key改用向量 key[0…d-1]来表示之,这样: 第 p 个记录的关键码的第 i位可表示为:r[p].key[i]; 第 p 个记录的指针分量可表示为: r[p].next
96
void RadixSort (&list, int d, int radix ) { int f[radix], e[radix]; for ( int i = 1; i < n; i++ ) r[i].next=i+1; r[n].next=0; // 静态链表初始化,将各记录顺次链接。 int p= 1; //p 是链表元素的地址指针 for ( i = d-1; i >= 0; i-- ) { // 开始做 d 趟分配 / 收集 , i 是 key 的第 i 位 for ( int j = 0; j < radix; j++ ) f[j] = 0; // 初态=各队列清空 while ( p!= 0 ) { // 开始将 n 个记录分配到 radix 个队列 int k = r[p]. key[i]; // 取当前记录之 key 分量的第 i 位 if ( f[k] == 0) f [k] =p; // 若第 k 个队列空 , 此记录成为队首;
else r[e[k]].next=p; // 若队列不空 , 链入原队尾元素之后 e[k] =p; // 修改队尾指针,该记录成为新的队尾 p= r[p].next; } / / 选下一关键字,直到本趟分配完
链表基数排序的算法:
97
j = 0; // 开始从 0 号队列(总共 radix 个队)开始收集 while ( f [j] == 0 ) j++; // 若是空队列则跳过 r[0].next=p = f [j]; // 建立本趟收集链表的头指针 int last = e[j]; // 建立本趟收集链表的尾指针 for ( k = j+1; k < radix; k++) // 逐个队列链接(收集) if ( f [k] ) { // 若队列非空 r[last].next=f [k]; last = e[k]; // 队尾指针链接 } r[last].next=0; // 本趟收集链表之尾部应为 0 }} // RadixSort
在此算法中,数组 r[n]被反复使用,用来存放原始序列和每趟收集的结果。记录从未移动,仅指针改变。
98
基数排序算法分析
• 假设有 n 个记录 , 每个记录的关键字有 d 位,每个关键字的取值有 radix 个 , 则需要 radix 个队列 , 进行 d 趟“分配”与“收集”。因此时间复杂度:O ( d ( n+radix ) ) 。
• 基数排序需要增加 n+2radix 个附加链接指针,空间效率低
• 空间复杂度: O ( radix ) .• 稳定性:稳定。 ( 一直前后有序 ) 。用途:若基数 radix 相同,对于记录个数较多而关键
码位数较少的情况,使用链式基数排序较好。
特点:不用比较和移动,改用分配和收集,时间效率高!
99
排序方法 排序方法 最好情况 最好情况 平均时间 平均时间 最坏情况最坏情况 辅助存储辅助存储 稳定性 稳定性 简单排序 O(n) O(n2) O(n2) O(1 ) 稳定 快速排序 O(nlgn ) O(nlgn) O(n2) O(lgn) 不稳定 堆排序 O(nlgn ) O(nlgn ) O(nlgn) O(1 ) 不稳定 归并排序 O(nlgn ) O(nlgn ) O(nlgn) O(n ) 稳定基数排序 O(d(n+rd)) O(d(n+rd)) O(d(n+rd)) O(rd) 稳定
简单选择 O(n2) O(n2) O(n2) O(1 ) 不稳定 直接插入 O(n) O(n2) O(n2) O(1 ) 稳定 折半插入 O(nlgn ) O(nlgn ) O(nlgn ) O(1 ) 稳定冒泡 O(n) O(n2) O(n2) O(1 ) 稳定
10.7 、各种内部排序方法的比较讨论
100
讨论:若初始记录基本无序,则选用哪些排序方法比较适合?若初始记录基本无序,则最好选用哪些排序方法?
对基本有序的情况,可选用堆排序、冒泡排序、归并排序等方法; 在基本无序的情况下,最好选用快速排序、希尔排序。
想一想:能选用折半排序么?
10.7 、各种内部排序方法的比较讨论
101
1 、 各种方法的时、空优劣
• 判定树模型:如下图所示,说明对 a,b,c 进行分类的一棵判定树。当判断条件成立时, 转向左,否则转向右。
2 、 比较法分类的下界: O( n logn )
a < b
a < b < c a < c b < c
a < cb < c
a < c < b c < a < b c < b < a
b < a < c
b < c < a
no yes
yes
yes
yes
yes
no
no
no
no
• 注意:树高代表比较的代价。因此只要知道了树高和结点数 n 的关系,就可以求出 用比较法进行排序时的时间代价。另外, n 个结点的分类序列,其叶
子结点 共有 n! 片。
10.7 、各种内部排序方法的比较讨论
102
• 定理:高为 H 的二叉树,最多具有 2(H-1)片叶子。
证明: H = 1 时,最多 1 片叶子,定理成立。
H = 2 时,最多 2 片叶子,定理成立。
设 H = n-1 时公式成立。则当 H =n 时,
根结点有具有叶子结点最多的高为 n-1 的左 右子树;叶子结点总数为:
2(n-2)×2 = 2(n-1) 公式成立。
推论:具有 N 片叶子的二叉树,高最少为 logN + 1 。
或比较次数最少为 logN 。
2 、 比较法分类的下界: O( n logn )
高为 n-1
具有 2(n-2)
片叶子
高为 n-1
具有 2(n-2)
片叶子
H = 1
H = 2
H = n
• 定理:把 n 个不同的结点进行分类的任一判定树的高 度至少为 log(n!) + 1 。
证明: n 个元素的排序序列的总数共有 n! 种,因此在 相应的判定树上至少有 n! 片叶子。根据上述定 理,可证。
推论:用比较的方法对 n 个元素的进行分类。在最坏 情况下至少需要 cnlogn(log(n!)) 次比较
, 或 Ω(nlogn)
10.7 、各种内部排序方法的比较讨论
103
2 、 比较法分类的下界: Ω( n logn )
• 定理:把 n 个不同的结点进行分类的任一判定树的高 度至少为 log(n!) + 1 。
证明: n 个元素的排序序列的总数共有 n! 种,因此在 相应的判定树上至少有 n! 片叶子。根据上述定 理,可证。
推论:用比较的方法对 n 个元素的进行分类。在最坏 情况下至少需要 cnlogn 次比较, 或
Ω(nlogn)
证明:注意到:
n! >= n(n-1) (n-2) …… ( n/2 ) >= (n/2)n/2
∴ log(n!) >= (n/2 ) log(n/2) >=
(n/4)logn
∴ 在最坏情况下至少需要 cnlogn 次比较,即 下界为 Ω(nlogn)
即借助于“比较”进行排序,在最坏的情况下 能达到的最好的时间复杂度为O(nlogn).
3 、 注意: 2介绍的是在比较的前提的下界,并不意味着, 在任何情况下,下界都为 Ω( n logn )
高为 n-1
具有 2(n-2)
片叶子
高为 n-1
具有 2(n-2)
片叶子
H = 1
H = 2
H = n
10.7 、各种内部排序方法的比较讨论
104
本章小结
总的要求:深刻理解排序的定义和各种排序方法的特点,并能加以灵活应用;了解各种方法的排序过程极其依据的原则;掌握各种排序方法的时间复杂度的分析方法;理解排序方法“稳定”或“不稳定”的含义;
本章重点:希尔排序、快速排序、堆排序和归并排序等高效方法的排序。
难点:希尔排序、快速排序、堆排序、归并排序和基数排序的排序。
105
本章作业
书面作业 :
p61: 10.1 题、 p62: 10.7 题
p65:10.31 题
p63: 10.12 题
上机作业 :
1. 实现快速排序
2. 实现堆排序