Download presentation
Presentation is loading. Please wait.
Published bySuryadi Oesman Modified 6年之前
1
数据结构 本 章 说 明 10.1 概 述 10.2 插入排序 10.3 快速排序 10.4 堆 排 序 10.5 归并排序
10.1 概 述 10.2 插入排序 10.3 快速排序 10.4 堆 排 序 10.5 归并排序 10.6 基数排序 10.7 各种方法比较 本 章 小 结 返回主目录
2
本章说明 学习目标 理解排序的定义和各种排序方法的特点,并能加以灵活应用。排序方法有不同的分类方法,基于“关键字间的比较”进行排序的方法可以按排序过程所依据的不同原则分为插入排序、交换排序、选择排序、归并排序和计数排序等五类 掌握各种排序方法的时间复杂度的分析方法。能从"关键字间的比较次数"分析排序算法的平均情况和最坏情况的时间性能。按平均时间复杂度划分,内部排序可分为三类:O (n2) 的简单排序方法,O (n·logn) 的高效排序方法和O (d·n)的基数排序方法。 从学习利用高级语言编制程序开始,数组是大家惯用的存储批量数据的工具,前几章讨论的线性结构的顺序存储结构也都是利用数组来描述的,那么数组本身又是怎么实现的呢?因此本章的学习目的主要是了解数组类型的特点以及在高级编程语言中的实现方法。对于本章讨论的随机稀疏矩阵运算的各个算法主要是理解问题的处理方法。
3
理解排序方法"稳定"或"不稳定"的含义,弄清楚在什么情况下要求应用的排序方法必须是稳定的。
本章说明 理解排序方法"稳定"或"不稳定"的含义,弄清楚在什么情况下要求应用的排序方法必须是稳定的。 重点和难点 希尔排序、快速排序、堆排序和归并排序等高效方法是本章的学习重点和难点 知识点 排序、直接插入排序、折半插入排序、表插入排序、希尔排序、起泡排序、快速排序、简单选择排序、堆排序、2-路归并排序、基数排序、排序方法的综合比较 从学习利用高级语言编制程序开始,数组是大家惯用的存储批量数据的工具,前几章讨论的线性结构的顺序存储结构也都是利用数组来描述的,那么数组本身又是怎么实现的呢?因此本章的学习目的主要是了解数组类型的特点以及在高级编程语言中的实现方法。对于本章讨论的随机稀疏矩阵运算的各个算法主要是理解问题的处理方法。
4
本章说明 学习指南 本章学习的要点主要是了解各种排序方法实现时所依据的原则以及它们的主要操作("关键字间的比较"和"记录的移动")的时间分析。学习中应注意掌握各种排序方法实现的要点,可通过对基础知识题中算法的手工执行和比较分析,切实掌握各种排序过程的排序特点所在,注意同一排序方法在不同的教科书上可以有不同书写形式描述的算法。在学习本章过程中需练习的算法设计题为:10.23,10.25,10.32,10.34,10.38 和 10.42。 从学习利用高级语言编制程序开始,数组是大家惯用的存储批量数据的工具,前几章讨论的线性结构的顺序存储结构也都是利用数组来描述的,那么数组本身又是怎么实现的呢?因此本章的学习目的主要是了解数组类型的特点以及在高级编程语言中的实现方法。对于本章讨论的随机稀疏矩阵运算的各个算法主要是理解问题的处理方法。 目录
5
10.1 概 述 排序的定义 内部排序和外部排序 内部排序方法的分类 目录
6
10.1 概 述 一、什么是排序? 排序是计算机内经常进行的一种操作,其目的是将一组“无序”的记录序列调整为“有序”的记录序列。 例如:将下列关键字序列 52, 49, 80, 36, 14, 58, 61, 23, 97, 75 调整为 14, 23, 36, 49, 52, 58, 61 ,75, 80, 97
7
一般情况下, 假设含n个记录的序列为{ R1, R2, …, Rn } 其相应的关键字序列为 { K1, K2, …,Kn }
10.1 概 述 一般情况下, 假设含n个记录的序列为{ R1, R2, …, Rn } 其相应的关键字序列为 { K1, K2, …,Kn } 这些关键字相互之间可以进行比较,即在 它们之间存在着这样一个关系 Kp1≤Kp2≤…≤Kpn 按此固有关系将上式记录序列重新排列为 { Rp1, Rp2, …,Rpn } 的操作称作排序。
8
二、内部排序和外部排序 反之,若参加排序的记录数量很大,整个序列的排序过程不可能在内存中完成,则称此类排序问题为外部排序。
10.1 概 述 二、内部排序和外部排序 若整个排序过程不需要访问外存便能完成,则称此类排序问题为内部排序 反之,若参加排序的记录数量很大,整个序列的排序过程不可能在内存中完成,则称此类排序问题为外部排序。
9
三、内部排序的方法 内部排序的过程是一个逐步扩大 记录的有序序列长度的过程。 有序序列区 无 序 序 列 区 经过一趟排序 有序序列区
10.1 概 述 三、内部排序的方法 内部排序的过程是一个逐步扩大 记录的有序序列长度的过程。 有序序列区 无 序 序 列 区 经过一趟排序 有序序列区 无 序 序 列 区
10
10.1 概 述 基于不同的“扩大” 有序序列长度的方法,内部排序方法大致可分下列几种类型: 插入类 交换类 选择类 归并类 其它方法
11
10.1 概 述 1. 插入类 将无序子序列中的一个或几个记录“插入”到有序序列中,从而增加记录的有序子序列的长度。
12
2. 交换类 通过“交换”无序序列中的记录从而得到其中关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列的长度。
10.1 概 述 2. 交换类 通过“交换”无序序列中的记录从而得到其中关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列的长度。
13
3. 选择类 从记录的无序子序列中“选择”关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列的长度。
10.1 概 述 3. 选择类 从记录的无序子序列中“选择”关键字最小或最大的记录,并将它加入到有序子序列中,以此方法增加记录的有序子序列的长度。
14
10.1 概 述 4. 归并类 通过“归并”两个或两个以上的记录有序子序列,逐步增加记录有序序列的长度。 5. 其它方法
15
一趟直接插入排序的基本思想: 有序序列R[1..i-1] 无序序列 R[i..n] R[i] 有序序列R[1..i]
10.2 插入排序 一趟直接插入排序的基本思想: 有序序列R[1..i-1] 无序序列 R[i..n] R[i] 有序序列R[1..i] 无序序列 R[i+1..n]
16
1.在R[1..i-1]中查找R[i]的插入位置 j ;
10.2 插入排序 实现“一趟插入排序”可分三步进行: 1.在R[1..i-1]中查找R[i]的插入位置 j ; R[1..j].key R[i].key < R[j+1..i-1].key 2.将R[j+1..i-1]中的所有记录均后移 一个位置; 3.将R[i] 插入(复制)到R[j+1]的位置上。
17
直接插入排序(基于顺序查找) 折半插入排序(基于折半查找) 表插入排序(基于链表存储) 希尔排序(基于逐趟缩小增量)
10.2 插入排序 不同的具体实现方法导致不同的算法描述 直接插入排序(基于顺序查找) 折半插入排序(基于折半查找) 表插入排序(基于链表存储) 希尔排序(基于逐趟缩小增量) 目录
18
10.2 插入排序 一、直接插入排序 利用 “顺序查找”实现 “在R[1..i-1]中查找R[i]的插入位置” 算法的实现要点:
19
从R[i-1]起向前进行顺序查找,监视哨设置在R[0];
10.2 插入排序 从R[i-1]起向前进行顺序查找,监视哨设置在R[0]; R[0] R[i] j j=i-1 插入位置 R[0] = R[i]; // 设置“哨兵” for (j=i-1; R[0].key<R[j].key; --j); // 从后往前找 循环结束表明R[i]的插入位置为 j +1
20
for (j=i-1; R[0].key<R[j].key; --j); R[j+1] = R[j]
10.2 插入排序 对于在查找过程中找到的那些关键字不小于R[i].key的记录,并在查找的同时实现记录向后移动; for (j=i-1; R[0].key<R[j].key; --j); R[j+1] = R[j] R[0] R[i] j j= i-1 插入位置 上述循环结束后可以直接进行“插入”
21
if (R[i].key<R[i-1].key) { 在 R[1..i-1]中查找R[i]的插入位置; 插入R[i] ; }
10.2 插入排序 令 i = 2,3,…, n, 实现整个序列的排序。 for ( i=2; i<=n; ++i ) if (R[i].key<R[i-1].key) { 在 R[1..i-1]中查找R[i]的插入位置; 插入R[i] ; }
22
for ( j=i-2; L.r[0].key < L.r[j].key; -- j )
10.2 插入排序 void InsertionSort ( SqList &L ) { // 对顺序表 L 作直接插入排序。 for ( i=2; i<=L.length; ++i ) if (L.r[i].key < L.r[i-1].key) { }//if } // InsertSort L.r[0] = L.r[i]; // 复制为监视哨 L.r[i] = L.r[i-1]; for ( j=i-2; L.r[0].key < L.r[j].key; -- j ) L.r[j+1] = L.r[j]; // 记录后移 L.r[j+1] = L.r[0]; // 插入到正确位置
23
10.2 插入排序 内部排序的时间分析: 实现内部排序的基本操作有两个: (1)“比较”序列中两个关键字的 大小; (2)“移动”记录。
24
对于直接插入排序: “比较”的次数: “移动”的次数: “比较”的次数: “移动”的次数: 最好的情况(关键字在记录序列中顺序有序):
10.2 插入排序 对于直接插入排序: 最好的情况(关键字在记录序列中顺序有序): “比较”的次数: “移动”的次数: 最坏的情况(关键字在记录序列中逆序有序): “比较”的次数: “移动”的次数: 2
25
10.2 插入排序 二、折半插入排序 因为 R[1..i-1] 是一个按关键字有序的有序序列,则可以利用折半查找实现“在R[1..i-1]中查找R[i]的插入位置”,如此实现的插入排序为折半插入排序。
26
void BiInsertionSort ( SqList &L ) {
10.2 插入排序 void BiInsertionSort ( SqList &L ) { } // BInsertSort for ( i=2; i<=L.length; ++i ) { } // for L.r[0] = L.r[i]; // 将 L.r[i] 暂存到 L.r[0] 在 L.r[1..i-1]中折半查找插入位置; for ( j=i-1; j>=low; --j ) L.r[j+1] = L.r[j]; // 记录后移 L.r[low] = L.r[0]; // 插入
27
while (low<=high) {
10.2 插入排序 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; // 插入点在高半区
28
10.2 插入排序 插入 位置 例如: i L.r low high low low high m m 插入 位置 m 再如: i L.r low high low high high m m m
29
设一n个记录的辅助数组d,将d看成循环向量 将d[1]=L.r[1],然后若L.r[i].key<d[1],插入在其前,否则插入在其后
10.2 插入排序 2-路插入排序 减少排序中移动记录的次数 设一n个记录的辅助数组d,将d看成循环向量 将d[1]=L.r[1],然后若L.r[i].key<d[1],插入在其前,否则插入在其后
30
10.2 插入排序 三、表插入排序 为了减少在排序过程中进行的“移动”记录的操作,必须改变排序过程中采用的存储结构。利用静态链表进行排序,并在排序完成之后,一次性地调整各个记录相互之间的位置,即将每个记录都调整到它们所应该在的位置上。
31
void LInsertionSort (Elem SL[ ], int n){ // 对记录序列SL[1..n]作表插入排序。
10.2 插入排序 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
32
如何在排序之后调整记录序列? 算法中使用了三个指针: 其中:p 指示第 i 个记录的当前位置; i 指示第 i 个记录应在的位置;
10.2 插入排序 如何在排序之后调整记录序列? 算法中使用了三个指针: 其中:p 指示第 i 个记录的当前位置; i 指示第 i 个记录应在的位置; q 指示第 i+1 个记录的当前位置。
33
void Arrange ( Elem SL[ ], int n ) { p = SL[0].next; // p指示第一个记录的当前位置
10.2 插入排序 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; // 指向被移走的记录, }//if p = q; // p指示尚未调整的表尾,准备找第i+1个记录 }//for } // Arrange
34
基本思想:对待排记录序列先作“宏观”调整,再作“微观”调整。
10.2 插入排序 四、希尔排序(又称缩小增量排序) 基本思想:对待排记录序列先作“宏观”调整,再作“微观”调整。 所谓“宏观”调整,指的是,“跳跃式” 的插入排序。 具体做法为:
35
其中,d 称为增量,它的值在排序过程中从大到小逐渐缩小,直至最后一趟排序减为 1。
10.2 插入排序 将记录序列分成若干子序列,分别对每个子序列进行插入排序。 例如:将 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] } 其中,d 称为增量,它的值在排序过程中从大到小逐渐缩小,直至最后一趟排序减为 1。
36
10.2 插入排序 第一趟希尔排序,设增量 d =5 第二趟希尔排序,设增量 d = 3 第三趟希尔排序,设增量 d = 1
37
void ShellInsert ( SqList &L, int dk ) { for ( i=dk+1; i<=n; ++i )
10.2 插入排序 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[i]插入有序子 L.r[0] = L.r[i]; // 暂存在L.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
38
void ShellSort (SqList &L, int dlta[], int t) { // 增量为dlta[]的希尔排序
10.2 插入排序 void ShellSort (SqList &L, int dlta[], int t) { // 增量为dlta[]的希尔排序 for (k=0; k<t; ++t) ShellInsert(L, dlta[k]); //一趟增量为dlta[k]的插入排序 } // ShellSort
39
10.3 快速排序 起泡排序 一趟快速排序 快速排序 时间分析 目录
40
假设在排序过程中,记录序列R[1..n]的状态为:
10.3 快速排序 一、起泡排序 假设在排序过程中,记录序列R[1..n]的状态为: n-i+1 无序序列R[1..n-i+1] 有序序列 R[n-i+2..n] 比较相邻记录,将关键字最大的记录交换到 n-i+1 的位置上 第 i 趟起泡排序 无序序列R[1..n-i] 有序序列 R[n-i+1..n]
41
void BubbleSort(Elem R[ ], int n) { while (i >1) {
10.3 快速排序 void BubbleSort(Elem R[ ], int n) { while (i >1) { } // while } // BubbleSort i = n; lastExchangeIndex = 1; if (R[j+1].key < R[j].key) { Swap(R[j], R[j+1]); } //if for (j = 1; j < i; j++) lastExchangeIndex = j; //记下进行交换的记录位置 i = lastExchangeIndex; // 本趟进行过交换的 // 最后一个记录的位置
42
2. 一般情况下,每经过一趟“起泡”,“i 减一”,但并不是每趟都如此。
10.3 快速排序 注意: 1. 起泡排序的结束条件为, 最后一趟没有进行“交换记录”。 2. 一般情况下,每经过一趟“起泡”,“i 减一”,但并不是每趟都如此。 例如: 2 1 3 5 5 1 3 5 7 9 9 8 j j j j i=2 j j j j i=6 i=7 for (j = 1; j < i; j++) if (R[j+1].key < R[j].key) …
43
n-1 时间分析: “比较”的次数: “移动”的次数: “比较”的次数: “移动”的次数: 最好的情况(关键字在记录序列中顺序有序):
10.3 快速排序 时间分析: 最好的情况(关键字在记录序列中顺序有序): 只需进行一趟起泡 “比较”的次数: “移动”的次数: n-1 最坏的情况(关键字在记录序列中逆序有序): 需进行n-1趟起泡 “比较”的次数: “移动”的次数:
44
10.3 快速排序 二、一趟快速排序(一次划分) 目标:找一个记录,以它的关键字作为“枢轴”,凡其关键字小于枢轴的记录均移动至该记录之前,反之,凡关键字大于枢轴的记录均移动至该记录之后。 致使一趟排序之后,记录的无序序列R[s..t]将分割成两部分:R[s..i-1]和R[i+1..t], 且 R[j].key≤ R[i].key ≤ R[j].key (s≤j≤i-1) 枢轴 (i+1≤j≤t)
45
将 R[high].key 和 枢轴的关键字进行比较,要求R[high].key ≥ 枢轴的关键字
10.3 快速排序 例如 s R[0] 52 t 23 14 52 80 low low low low low high high high high high high 设 R[s]=52 为枢轴暂存在R[0]的位置上 将 R[high].key 和 枢轴的关键字进行比较,要求R[high].key ≥ 枢轴的关键字 将 R[low].key 和 枢轴的关键字进行比较,要求R[low].key ≤ 枢轴的关键字
46
在调整过程中,设立了两个指针: low 和high,它们的初值分别为: s 和 t,
10.3 快速排序 可见,经过“一次划分” ,将关键字序列 52, 49, 80, 36, 14, 58, 61, 97, 23, 75 调整为: 23, 49, 14, 36, (52) 58, 61, 97, 80, 75 在调整过程中,设立了两个指针: low 和high,它们的初值分别为: s 和 t, 之后逐渐减小 high,增加 low,并保证 R[high].key≥52,和 R[low].key≤52,否则进行记录的“交换”。
47
int Partition (RedType& R[], int low, int high) {
10.3 快速排序 int Partition (RedType& R[], int low, int high) { pivotkey = R[low].key; while (low<high) { while (low<high && R[high].key>=pivotkey) --high; R[low]←→R[high]; while (low<high && R[low].key<=pivotkey) ++low; } return low; // 返回枢轴所在位置 } // Partition
48
int Partition (RedType R[], int low, int high) {
10.3 快速排序 int Partition (RedType R[], int low, int high) { }// Partition R[0] = R[low]; pivotkey = R[low].key; // 枢轴 while (low<high) { } while(low<high&& R[high].key>=pivotkey) -- high; // 从右向左搜索 R[low] = R[high]; while (low<high && R[low].key<=pivotkey) ++ low; // 从左向右搜索 R[high] = R[low]; R[low] = R[0]; return low;
49
三、快速排序 首先对无序的记录序列进行“一次划分”,之后分别对分割所得两个子序列“递归”进行快速排序。 分别进行快速排序 一次划分
10.3 快速排序 三、快速排序 首先对无序的记录序列进行“一次划分”,之后分别对分割所得两个子序列“递归”进行快速排序。 无 序 的 记 录 序 列 一次划分 无序记录子序列(1) 枢轴 无序子序列(2) 分别进行快速排序
50
void QSort (RedType& R[], int s, int t ) { // 对记录序列R[s..t]进行快速排序
10.3 快速排序 void QSort (RedType& R[], int s, int t ) { // 对记录序列R[s..t]进行快速排序 if (s < t) { // 长度大于1 } } // QSort pivotloc = Partition(R, s, t); // 对 R[s..t] 进行一次划分 QSort(R, s, pivotloc-1); // 对低子序列递归排序,pivotloc是枢轴位置 QSort(R, pivotloc+1, t); // 对高子序列递归排序
51
void QuickSort( SqList & L) { // 对顺序表进行快速排序 QSort(L.r, 1, L.length);
10.3 快速排序 第一次调用函数 Qsort 时,待排序记录序列的上、下界分别为 1 和 L.length。 void QuickSort( SqList & L) { // 对顺序表进行快速排序 QSort(L.r, 1, L.length); } // QuickSort
52
四、快速排序的时间分析 假设一次划分所得枢轴位置 i=k,则对n 个记录进行快排所需时间
10.3 快速排序 四、快速排序的时间分析 假设一次划分所得枢轴位置 i=k,则对n 个记录进行快排所需时间 T(n) = Tpass(n) + T(k-1) + T(n-k) 其中 Tpass(n)为对 n 个记录进行一次划分所需时间, 若待排序列中记录的关键字是随机分布的,则 k 取 1 至 n 中任意一值的可能性相同。
53
结论: 快速排序的时间复杂度为O(nlogn)
10.3 快速排序 由此可得快速排序所需时间的平均值为: 设 Tavg(1)≤b 则可得结果: 结论: 快速排序的时间复杂度为O(nlogn)
54
若待排记录的初始状态为按关键字有序时,快速排序将蜕化为起泡排序,其时间复杂度为O(n2)。
10.3 快速排序 若待排记录的初始状态为按关键字有序时,快速排序将蜕化为起泡排序,其时间复杂度为O(n2)。 为避免出现这种情况,需在进行一次划分之前,进行“予处理”,即: 先对 R(s).key, R(t).key 和 R[(s+t)/2.key,进行相互比较,然后取关键字为 “三者之中”的记录为枢轴记录。
55
10.4 堆排序 简单选择排序 堆 排 序 目录
56
一、简单选择排序 假设排序过程中,待排记录序列的状态为: 有序序列R[1..i-1] 无序序列 R[i..n] 第 i 趟 简单选择排序
10.4 堆排序 一、简单选择排序 假设排序过程中,待排记录序列的状态为: 有序序列R[1..i-1] 无序序列 R[i..n] 从中选出 关键字最小的记录 第 i 趟 简单选择排序 有序序列R[1..i] 无序序列 R[i+1..n]
57
void SelectSort (Elem R[], int n ) { // 对记录序列R[1..n]作简单选择排序。
10.4 堆排序 简单选择排序的算法描述如下: void SelectSort (Elem R[], int n ) { // 对记录序列R[1..n]作简单选择排序。 for (i=1; i<n; ++i) { // 选择第 i 小的记录,并交换到位 } } // SelectSort j = SelectMinKey(R, i); // 在 R[i..n] 中选择关键字最小的记录 if (i!=j) R[i]←→R[j]; // 与第 i 个记录交换
58
移动记录的次数,最小值为 0, 最大值为3(n-1)
10.4 堆排序 时间性能分析 对 n 个记录进行简单选择排序,所需进行的 关键字间的比较次数 总计为 移动记录的次数,最小值为 0, 最大值为3(n-1)
59
二、堆排序 堆的定义: 堆是满足下列性质的数列{r1, r2, …,rn}: 或 例如: 是小顶堆 不是堆 (小顶堆) (大顶堆)
10.4 堆排序 二、堆排序 堆的定义: 堆是满足下列性质的数列{r1, r2, …,rn}: (小顶堆) 或 (大顶堆) 例如: {12, 36, 27, 65, 40, 34, 98, 81, 73, 55, 49} 是小顶堆 {12, 36, 27, 65, 40, 14, 98, 81, 73, 55, 49} 不是堆
60
不 ri r2i r2i+1 若将该数列视作完全二叉树, 则 r2i 是 ri 的左孩子; r2i+1 是 ri 的右孩子。 例如: 是堆
10.4 堆排序 若将该数列视作完全二叉树, 则 r2i 是 ri 的左孩子; r2i+1 是 ri 的右孩子。 ri r2i r2i+1 例如: 12 36 27 65 49 81 73 55 40 34 98 14 不 是堆
61
堆排序即是利用堆的特性对记录序列进行排序的一种排序方法。
10.4 堆排序 堆排序即是利用堆的特性对记录序列进行排序的一种排序方法。 例如: { 40, 55, 49, 73, 12, 27, 98, 81, 64, 36 } 建大顶堆 { 98, 81, 49, 73, 36, 27, 40, 55, 64, 12 } 交换 98 和 12 { 12, 81, 49, 73, 36, 27, 40, 55, 64, 98 } 经过筛选 重新调整为大顶堆 { 81, 73, 49, 64, 36, 27, 40, 55, 12, 98 }
62
void HeapSort ( HeapType &H ) { // 对顺序表 H 进行堆排序。
10.4 堆排序 void HeapSort ( HeapType &H ) { // 对顺序表 H 进行堆排序。 } // HeapSort for ( i=H.length/2; i>0; --i ) HeapAdjust ( H.r, i, H.length ); // 建大顶堆 for ( i=H.length; i>1; --i ) { H.r[1]←→H.r[i]; // 将堆顶记录和当前未经排序子序列 // H.r[1..i]中最后一个记录相互交换 HeapAdjust(H.r, 1, i-1); // 对 H.r[1] 进行筛选 }
63
两个问题: 如何“建堆”? 如何“筛选”? 定义堆类型为: typedef SqList HeapType; // 堆采用顺序表表示之
10.4 堆排序 定义堆类型为: typedef SqList HeapType; // 堆采用顺序表表示之 两个问题: 如何“建堆”? 如何“筛选”?
64
10.4 堆排序 所谓“筛选”指的是,对一棵左/右子树 均为堆的完全二叉树,“调整”根结点 使整个二叉树也成为一个堆。 筛选 堆 堆
65
因此,需要对它进行“筛选”(堆顶到叶子)
10.4 堆排序 例如: 12 交换 98 81 49 73 55 64 12 36 27 40 81 12 比较 73 64 12 98 98 是大顶堆 但在 98 和 12 进行互换之后,它就不是堆了 因此,需要对它进行“筛选”(堆顶到叶子)
66
void HeapAdjust (RcdType &R[], int s, int m)
10.4 堆排序 void HeapAdjust (RcdType &R[], int s, int m) { // 已知 R[s..m]中记录的关键字除 R[s] 之外均 // 满足堆的特征,本函数自上而下调整 R[s] 的 // 关键字,使 R[s..m] 也成为一个大顶堆。 } // HeapAdjust rc = R[s]; // 暂存 R[s] for ( j=2*s; j<=m; j*=2 ) { // j 初值指向左孩子 自上而下的筛选过程; } R[s] = rc; // 将调整前的堆顶记录插入到 s 位置
67
if ( j<m && R[j].key<R[j+1].key ) ++j;
10.4 堆排序 if ( j<m && R[j].key<R[j+1].key ) ++j; // 左/右“子树根”之间先进行相互比较 // 令 j 指示关键字较大记录的位置 if ( rc.key >= R[j].key ) break; // 再作“根”和“子树根”之间的比较, // 若“>=”成立,则说明已找到 rc 的插 // 入位置 s ,不需要继续往下调整 R[s] = R[j]; s = j; // 否则记录上移,尚需继续往下调整
68
现在,左/右子树都已经调整为堆,最后只要调整根结点,使整个二叉树是个“堆”即可。
10.4 堆排序 建堆是一个从下往上进行“筛选”的过程。 例如: 排序之前的关键字序列为 40 55 49 73 81 64 36 12 27 98 98 81 49 98 81 73 36 36 27 40 49 55 73 64 12 12 现在,左/右子树都已经调整为堆,最后只要调整根结点,使整个二叉树是个“堆”即可。
69
因此,堆排序的时间复杂度为O(nlogn)
10.4 堆排序 堆排序的时间复杂度分析: 1. 对深度为 k 的堆,“筛选”所需进行的关键字 比较的次数至多为2(k-1); 2. 对 n 个关键字,建成深度为 h (=log2n+1) 的堆, 所需进行的关键字比较的次数至多 4n; 3. 调整“堆顶” n-1 次,总共进行的关键 字比较的次数不超过 2 (log2(n-1)+ log2(n-2)+ …+log22) < 2n(log2n) 因此,堆排序的时间复杂度为O(nlogn)
70
将两个或两个以上的有序子序列 “归并” 为一个有序序列。
10.5 归并排序 归并排序的过程基于下列基本思想进行: 将两个或两个以上的有序子序列 “归并” 为一个有序序列。
71
在内部排序中,通常采用的是2-路归并排序。即:将两个位置相邻的记录有序子序列
10.5 归并排序 在内部排序中,通常采用的是2-路归并排序。即:将两个位置相邻的记录有序子序列 有序子序列 R[l..m] 有序子序列 R[m+1..n] 归并为一个记录的有序序列。 有 序 序 列 R[l..n] 这个操作对顺序表而言,是轻而易举的。
72
… … void Merge (RcdType SR[], RcdType &TR[], int i, int m, int n) {
10.5 归并排序 void Merge (RcdType SR[], RcdType &TR[], int i, int m, int n) { // 将有序的记录序列 SR[i..m] 和 SR[m+1..n] // 归并为有序的记录序列 TR[i..n], sf10.12,一趟 } // Merge for (j=m+1, k=i; i<=m && j<=n; ++k) { // 将SR中记录由小到大地并入TR if (SR[i].key<=SR[j].key) TR[k] = SR[i++]; else TR[k] = SR[j++]; } … …
73
if (i<=m) TR[k..n] = SR[i..m]; // 将剩余的 SR[i..m] 复制到 TR
10.5 归并排序 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
74
归并排序的算法 如果记录无序序列 R[s..t] 的两部分 R[s..(s+t)/2] 和 R[(s+t)/2+1..t]
10.5 归并排序 归并排序的算法 如果记录无序序列 R[s..t] 的两部分 R[s..(s+t)/2] 和 R[(s+t)/2+1..t] 分别按关键字有序, 则利用上述归并算法很容易将它们归并成整个记录序列是一个有序序列。 由此,应该先分别对这两部分进行 2-路归并排序。
75
10.5 归并排序 例如: 52, 23, 80, , 68, (s=1, t=6) [ 52, 23, 80] [36, 68, 14] [ 52, 23] [80] [36, 68] [14] [ 52] [23] [36] [68] [ 52] [23] [80] [36] [68] [14] [ 23, 52] [36, 68] [ 23, 52, 80] [14, 36, 68] [ 14, 23, 36, 52, 68, 80 ]
76
… … void Msort ( RcdType SR[], RcdType &TR1[], int s, int t ) {
10.5 归并排序 void Msort ( RcdType SR[], RcdType &TR1[], int s, int t ) { // 将SR[s..t] 归并排序为 TR1[s..t], sf10.13 if (s= =t) TR1[s]=SR[s]; else { } } // Msort … …
77
// 将SR[s..t]平分为SR[s..m]和SR[m+1..t]
10.5 归并排序 m = (s+t)/2; // 将SR[s..t]平分为SR[s..m]和SR[m+1..t] Msort (SR, TR2, s, m); // 递归地将SR[s..m]归并为有序的TR2[s..m] Msort (SR, TR2, m+1, t); //递归地SR[m+1..t]归并为有序的TR2[m+1..t] Merge (TR2, TR1, s, m, t); // 将TR2[s..m]和TR2[m+1..t]归并到TR1[s..t]
78
void MergeSort (SqList &L) {//sf10.14 // 对顺序表 L 作2-路归并排序。
10.5 归并排序 void MergeSort (SqList &L) {//sf10.14 // 对顺序表 L 作2-路归并排序。 MSort(L.r, L.r, 1, L.length); } // MergeSort 容易看出,对 n 个记录进行归并排序的时间复杂度为Ο(nlogn)。即: 每一趟归并的时间复杂度为 O(n), 总共需进行 log2n 趟。 目录
79
10.6 基数排序 基数排序是一种借助“多关键字排序”的思想来实现“单关键字排序”的内部排序算法。 多关键字的排序 链式基数排序 目录
80
一、多关键字的排序 n 个记录的序列 { R1, R2, …,Rn} 对关键字 (Ki0, Ki1,…,Kid-1) 有序是指:
10.6 基数排序 一、多关键字的排序 n 个记录的序列 { R1, R2, …,Rn} 对关键字 (Ki0, Ki1,…,Kid-1) 有序是指: 对于序列中任意两个记录 Ri 和 Rj (1≤i<j≤n) 都满足下列(词典)有序关系: (Ki0, Ki1, …,Kid-1) < (Kj0, Kj1, …,Kjd-1) 其中: K0 被称为 “最主”位关键字, Kd-1 被称为 “最次”位关键字。
81
10.6 基数排序 实现多关键字排序通常有两种作法: 最高位优先MSD法: 最低位优先LSD法:
82
10.6 基数排序 先对K0进行排序,并按 K0 的不同值将记录序列分成若干子序列之后,分别对 K1 进行排序,...…, 依次类推,直至最后对最次位关键字排序完成为止。
83
先对 Kd-1 进行排序,然后对 Kd-2 进行排序,依次类推,直至对最主位关键字 K0 排序完成为止。
10.6 基数排序 先对 Kd-1 进行排序,然后对 Kd-2 进行排序,依次类推,直至对最主位关键字 K0 排序完成为止。 排序过程中不需要根据 “前一个” 关键字的排序结果,将记录序列分割成若干个(“前一个”关键字不同的)子序列。
84
系别、班号和班内的序列号,其中以系别为“最主” 位关键字。
10.6 基数排序 例如:学生记录含三个关键字: 系别、班号和班内的序列号,其中以系别为“最主” 位关键字。 LSD的排序过程如下: 无序序列 3,2,30 1,2,15 3,1,20 2,3,18 2,1,20 对K2排序 1,2,15 2,3,18 3,1,20 2,1,20 3,2,30 对K1排序 3,1,20 2,1,20 1,2,15 3,2,30 2,3,18 对K0排序 1,2,15 2,1,20 2,3,18 3,1,20 3,2,30
85
10.6 基数排序 二、链式基数排序 假如多关键字的记录序列中,每个关键字的取值范围相同,则按LSD法进行排序时,可以采用“分配-收集”的方法,其好处是不需要进行关键字间的比较。 对于数字型或字符型的单关键字,可以看成是由多个数位或多个字符构成的多关键字,此时可以采用这种“分配-收集”的办法进行排序,称作基数排序法。
86
10.6 基数排序 例如:对下列这组关键字 {209, 386, 768, 185, 247, 606, 230, 834, 539 } 首先按其 “个位数” 取值分别为 0, 1, …, 9 “分配” 成 10 组,之后按从 0 至 9 的顺序将 它们 “收集” 在一起; 然后按其 “十位数” 取值分别为 0, 1, …, 9 “分配” 成 10 组,之后再按从 0 至 9 的顺序将它们 “收集” 在一起; 最后按其“百位数”重复一遍上述操作。
87
2.“分配” 时,按当前“关键字位”所取值,将记录分配到不同的 “链队列” 中,每个队列中记录的 “关键字位” 相同;
10.6 基数排序 在计算机上实现基数排序时,为减少所需辅助存储空间,应采用链表作存储结构,即链式基数排序,具体作法为: 1.待排序记录以指针相链,构成一个链表; 2.“分配” 时,按当前“关键字位”所取值,将记录分配到不同的 “链队列” 中,每个队列中记录的 “关键字位” 相同; 3.“收集”时,按当前关键字位取值从小到大将各队列首尾相链成一个链表; 4.对每个关键字位均重复 2) 和 3) 两步。
88
例如: 进行第一次分配 f[0] r[0] →230← f[7] r[7] →367 ← →167 →237 f[8] r[8] →138←
10.6 基数排序 例如: p→369→367→167→239→237→138→230→139 进行第一次分配 f[0] r[0] →230← f[7] r[7] → ← →167 →237 f[8] r[8] →138← f[9] r[9] → ← →239 →139 进行第一次收集 p→230 →367→167→237 →138 →368→239→139
89
进行第二次分配 f[3] r[3] →230 ← →237 →138 →239 →139 f[6] r[6] →367 ← →167
10.6 基数排序 p→230→367→167→237→138→368→239→139 进行第二次分配 f[3] r[3] → ← →237 →138 →239 →139 f[6] r[6] → ← →167 →368 进行第二次收集 p→230→237→138→239→139 →367→167→368
90
进行第三次分配 进行第三次收集之后便得到记录的有序序列 f[1] r[1] →138 ← →139 →167 f[2] r[2]
10.6 基数排序 p→230→237→138→239→139→367→167→368 进行第三次分配 f[1] r[1] → ← →139 →167 f[2] r[2] → ← →237 →239 f[3] r[3] → ← →368 进行第三次收集之后便得到记录的有序序列 p→138→139→167 →230→237→239 →367→368
91
提醒注意: 1.“分配”和“收集”的实际操作仅为修改链表中的指针和设置队列的头、尾指针;
10.6 基数排序 提醒注意: 1.“分配”和“收集”的实际操作仅为修改链表中的指针和设置队列的头、尾指针; 2.为查找使用,该链表尚需应用算法Arrange(sf10.3) 将它调整为有序表。
92
其中,分配为O(n); 收集为O(rd)(rd为“基”), d为“分配-收集”的趟数。 算法: 基数排序的时间复杂度为O(d(n+rd))
10.6 基数排序 算法: 基数排序的时间复杂度为O(d(n+rd)) 其中,分配为O(n); 收集为O(rd)(rd为“基”), d为“分配-收集”的趟数。 目录
93
一、时间性能 1. 平均的时间性能 时间复杂度为 O(nlogn): 快速排序、堆排序和归并排序 时间复杂度为 O(n2):
10.7 各种方法比较 一、时间性能 1. 平均的时间性能 时间复杂度为 O(nlogn): 快速排序、堆排序和归并排序 时间复杂度为 O(n2): 直接插入排序、起泡排序和 简单选择排序 时间复杂度为 O(n): 基数排序
94
直接插入排序和起泡排序能达到O(n)的时间复杂度;
10.7 各种方法比较 2. 当待排记录序列按关键字顺序有序时 直接插入排序和起泡排序能达到O(n)的时间复杂度; 快速排序的时间性能蜕化为O(n2) 3. 简单选择排序、堆排序和归并排序的时间性能不随记录序列中关键字的分布而改变。
95
2. 快速排序为O(logn),为递归程序执行过程中,栈所需的辅助空间;
10.7 各种方法比较 二、空间性能 指的是排序过程中所需的辅助空间大小 1. 所有的简单排序方法(包括:直接插入、 起泡和简单选择) 和堆排序的空间复杂度为O(1); 2. 快速排序为O(logn),为递归程序执行过程中,栈所需的辅助空间;
96
3. 归并排序所需辅助空间最多,其空间复杂度为 O(n);
10.7 各种方法比较 3. 归并排序所需辅助空间最多,其空间复杂度为 O(n); 4. 链式基数排序需附设队列首尾指针,则空间复杂度为 O(rd)。
97
2. 当对多关键字的记录序列进行LSD方法排序时,必须采用稳定的排序方法。
10.7 各种方法比较 三、排序方法的稳定性能 1. 稳定的排序方法指的是,对于两个关键字相等的记录,它们在序列中的相对位置,在排序之前和经过排序之后,没有改变。 排序之前 : { · · · · · Ri(K) · · · · · Rj(K) · · · · · } 排序之后 : { · · · · · Ri(K) Rj(K) · · · · ·· · · · · } 2. 当对多关键字的记录序列进行LSD方法排序时,必须采用稳定的排序方法。
98
10.7 各种方法比较 例如: 排序前 ( 56, 34, 47, 23, 66, 18, 82, 47 ) 若排序后得到结果 ( 18, 23, 34, 47, 47, 56, 66, 82 ) 则称该排序方法是稳定的; 若排序后得到结果 ( 18, 23, 34, 47, 47, 56, 66, 82 ) 则称该排序方法是不稳定的;
99
2. 对于不稳定的排序方法,只要能举出一个实例说明即可。
10.7 各种方法比较 2. 对于不稳定的排序方法,只要能举出一个实例说明即可。 例如 : 对 { 4, 3, 4, 2 } 进行快速排序, 得到 { 2, 3, 4, 4 } 3. 快速排序、堆排序和希尔排序是不稳定的排序方法。
100
本章讨论的各种排序方法,除基数排序外,其它方法都是基于“比较关键字”进行排序的排序方法。
10.7 各种方法比较 四、关于“排序方法的时间复杂度的下限” 本章讨论的各种排序方法,除基数排序外,其它方法都是基于“比较关键字”进行排序的排序方法。 可以证明, 这类排序法可能达到的最快的时间复杂度为O(nlogn)。 (基数排序不是基于“比较关键字”的排序方法,所以它不受这个限制)
101
10.7 各种方法比较 排序方法 平均时间 最坏情况 辅助存储 稳定排序 简单排序 O(n2) O(1) 希尔排序 O(n3/2) ×
快速排序 O(nlogn) O(logn) 堆 排 序 归并排序 O(n) 基数排序 O(d(n+rd)) O(rd) 目录
102
本章小结 1. 了解排序的定义和各种排序方法的特点。熟悉各种方法的排序过程及其依据的原则。基于“关键字间的比较”进行排序的方法可以按排序过程所依据的不同原则分为插入排序、交换排序、选择排序、归并排序和基数排序等五类。 二维数组可定义为"其数据元素为一维数组的线性表"。可表示为: A(2) = ( A(1)0,A(1)1,……,A(1)m-1 ) 其中每个数据元素是一个一维数组 A(1)i = ( ai,0,ai,1,……,ai,n-1 ) i = 0,1,……,m-1 类似地,N维数组是N-1维数组的线性表 A(N) = ( A(N-1)0,A(N-1)1,……,A(N-1)s-1 )
103
2. 掌握各种排序方法的时间复杂度。能从“关键字间的比较次数”分析排序算法的平均情况和最坏情况的时间性能。
本章小结 2. 掌握各种排序方法的时间复杂度。能从“关键字间的比较次数”分析排序算法的平均情况和最坏情况的时间性能。 按平均时间复杂度划分,内部排序可分为三类:O(n2)的简单排序方法,O(nlogn)的高效排序方法 和 O(dn)的基数排序方法。 二维数组可定义为"其数据元素为一维数组的线性表"。可表示为: A(2) = ( A(1)0,A(1)1,……,A(1)m-1 ) 其中每个数据元素是一个一维数组 A(1)i = ( ai,0,ai,1,……,ai,n-1 ) i = 0,1,……,m-1 类似地,N维数组是N-1维数组的线性表 A(N) = ( A(N-1)0,A(N-1)1,……,A(N-1)s-1 )
104
3.理解排序方法“稳定”或“不稳定”的含义,弄清楚在什么情况下要求应用的排序方法必须是稳定的。
本章小结 3.理解排序方法“稳定”或“不稳定”的含义,弄清楚在什么情况下要求应用的排序方法必须是稳定的。 二维数组可定义为"其数据元素为一维数组的线性表"。可表示为: A(2) = ( A(1)0,A(1)1,……,A(1)m-1 ) 其中每个数据元素是一个一维数组 A(1)i = ( ai,0,ai,1,……,ai,n-1 ) i = 0,1,……,m-1 类似地,N维数组是N-1维数组的线性表 A(N) = ( A(N-1)0,A(N-1)1,……,A(N-1)s-1 )
Similar presentations