Upload
amber-lopez
View
68
Download
8
Embed Size (px)
DESCRIPTION
第 10 章 排序. 要求: 本章要理解各种排序算法的思想(层次 1 )、稳定性和时空性能; 插入排序:直接插入排序、折半插入排序、*希尔排序; 交换排序:冒泡、快速排序;快速排序算法要求灵活应用(算法层次 3 ); 选择排序:直接选择排序、堆排序(堆概念,筛选、建堆、堆排序); 归并排序: 2- 路归并排序. 第 10 章 排序. 10.1 概述 排序定义 —— 将一个数据元素(或记录)的任意序列,重新排列成一个按关键字有序的序列叫 ~ 排序分类 按待排序记录所在位置 内部排序:待排序记录存放在内存 外部排序:排序过程中需对外存进行访问的排序 - PowerPoint PPT Presentation
Citation preview
1
第 10 章 排序要求:•本章要理解各种排序算法的思想(层次 1 )、稳定性和时空性能;•插入排序:直接插入排序、折半插入排序、 * 希尔排序;•交换排序:冒泡、快速排序;快速排序算法要求灵活应用(算法层次 3 );•选择排序:直接选择排序、堆排序(堆概念,筛选、建堆、堆排序);•归并排序: 2- 路归并排序
2
第 10 章 排序10.1 概述
– 排序定义——将一个数据元素(或记录)的任意序列,重新排列成一个按关键字有序的序列叫 ~
– 排序分类• 按待排序记录所在位置
– 内部排序:待排序记录存放在内存– 外部排序:排序过程中需对外存进行访问的排序
• 按排序依据原则– 插入排序:直接插入排序、折半插入排序、 * 希尔排序– 交换排序:冒泡排序、快速排序– 选择排序:简单选择排序、堆排序– 归并排序: 2- 路归并排序– * 基数排序
3
• 按排序所需工作量– 简单的排序方法: T(n)=O(n²)– 先进的排序方法: T(n)=O(logn)– * 基数排序: T(n)=O(d.n)
– 排序基本操作• 比较两个关键字大小• 将记录从一个位置移动到另一个位置
4
排序的稳定性 当待排序记录的关键字均不相同时,排序结果是唯一的,否则排序结果不唯一。
在待排序的文件中,若存在多个关键字相同的记录,经过排序后这些具有相同关键字的记录之间的相对次序保持不变,该排序方法是稳定的;若具有相同关键字的记录之间的相对次序发生变化,则称这种排序方法是不稳定的。
注意:
排序算法的稳定性是针对所有输入实例而言的。即在所有可能的输入实例中,只要有一个实例使得算法不满足稳定性要求,则该排序算法就是不稳定的。
5
10.2 插入排序10.2.1 直接插入排序
– 排序过程:整个排序过程为 n-1 趟插入,即先将序列中第 1 个记录看成是一个有序子序列,然后从第 2 个记录开始,逐个进行插入,直至整个序列有序
例 49 38 65 97 76 13 27 49
i=2 38 (38 49) 65 97 76 13 27 49
i=3 65 (38 49 65) 97 76 13 27 49
i=4 97 (38 49 65 97) 76 13 27 49
i=5 76 (38 49 65 76 97) 13 27 49
i=6 13 (13 38 49 65 76 97) 27 49
i=1 ( )
i=7 (13 38 49 65 76 97 ) 27 4927
jj j jjj
977665493827
(13 27 38 49 49 65 76 97)排序结果:
• 算法描述 算法 10.1 P265
(13 27 38 49 65 76 97 ) 49
void InsertSort(SqList &L) { // 算法 10.1
// 对顺序表 L 作直接插入排序。 for (i=2; i<=L.length; ++i)
if (LT(L.r[i].key, L.r[i-1].key)) {
// "<" 时,需将 L.r[i] 插入有序子表 L.r[0] = L.r[i]; // 复制为哨兵 for (j=i-1; LT(L.r[0].key, L.r[j].key); --j)
L.r[j+1] = L.r[j]; // 记录后移 L.r[j+1] = L.r[0]; // 插入到正确位置 }
} // InsertSort
– 算法评价• 时间复杂度
– 若待排序记录按关键字从小到大排列 ( 正序 )关键字比较次数: 11
2
nn
i
记录移动次数: 0 或 )1(222
nn
i
– 若待排序记录按关键字从大到小排列 ( 逆序 )关键字比较次数:
2
)1)(2(
2
nni
n
i
记录移动次数:2
)1)(4()1(
2
nni
n
i
– 若待排序记录是随机的,取平均值关键字比较次数:
4
2n
记录移动次数:4
2nT(n)=O(n²)
• 空间复杂度: S(n)=O(1)
9
10.2.2 折半插入排序– 排序过程:用折半查找方法确定插入位置的排序叫 ~
例 i=1 (30) 13 70 85 39 42 6 20
i=2 13 (13 30) 70 85 39 42 6 20
i=7 6 (6 13 30 39 42 70 85 ) 20
….
..
i=8 20 (6 13 30 39 42 70 85 ) 20s jm
i=8 20 (6 13 30 39 42 70 85 ) 20s jm
i=8 20 (6 13 30 39 42 70 85 ) 20s jm
i=8 20 (6 13 30 39 42 70 85 ) 20sj
i=8 20 (6 13 20 30 39 42 70 85 )
算法描述 算法 10.2 P267void BInsertSort(SqList &L) {// 对顺序表 L 作折半插入排序。 for (i=2; i<=L.length; ++i) { L.r[0] = L.r[i]; // 将 L.r[i] 暂存到 L.r[0] low = 1; high = i-1; while (low<=high) {// 在 r[low..high] 中折半查找有序插入的位置 m = (low+high)/2; // 折半 if (LT(L.r[0].key, L.r[m].key)) high = m-1; // 插入点在低半区 else low = m+1; // 插入点在高半区 } for (j=i-1; j>=high+1; --j) L.r[j+1] = L.r[j]; // 记录后移 L.r[high+1] = L.r[0]; // 插入 }} // BInsertSort
– 算法评价• 时间复杂度: T(n)=O(n²)• 空间复杂度: S(n)=O(1)
11
10.2.3 * 希尔排序 ( 缩小增量法 )– 排序过程:先取一个正整数 d1<n ,把所有相隔
d1 的记录放一组,组内进行直接插入排序;然后取 d2<d1 ,重复上述分组和排序操作;直至 di=1 ,即所有记录放进一个组中排序为止
取 d3=1三趟分组:13 27 48 55 4 49 38 65 97 76
三趟排序:4 13 27 38 48 49 55 65 76 97
例 初始: 49 38 65 97 76 13 27 48 55 4
一趟排序:13 27 48 55 4 49 38 65 97 76
二趟排序:13 4 48 38 27 49 55 65 97 76
取 d1=5一趟分组:49 38 65 97 76 13 27 48 55 4
取 d2=3二趟分组:13 27 48 55 4 49 38 65 97 76
13
– 算法描述 算法 10.5 P272
例 49 38 65 97 76 13 27 48 55 4
#define T 3int d[]={5,3,1};
j i
4913 3827
一趟排序:13 27 48 55 4 49 38 65 97 76
j ij i
274
j ij i
55
j i
38
j ij ij i
二趟排序:13 4 48 38 27 49 55 65 97 76
j ij i
6548
j i
9755
j i
764
14
• 希尔排序特点– 子序列的构成不是简单的“逐段分割”,而是将
相隔某个增量的记录组成一个子序列– 希尔排序可提高排序速度,因为
• 分组后 n 值减小, n² 更小,而 T(n)=O(n²), 所以 T(n) 从总体上看是减小了
• 关键字较小的记录跳跃式前移,在进行最后一趟增量为 1 的插入排序时,序列已基本有序
– 增量序列取法• 无除 1 以外的公因子• 最后一个增量值必须为 1
15
10.3 交换排序10.3.1 冒泡排序
– 排序过程将第一个记录的关键字与第二个记录的关键字进行比
较,若为逆序 r[1].key>r[2].key ,则交换;然后比较第二个记录与第三个记录;依次类推,直至第 n-1 个记录和第 n 个记录比较为止——第一趟冒泡排序,结果关键字最大的记录被安置在最后一个记录上
对前 n-1 个记录进行第二趟冒泡排序,结果使关键字次大的记录被安置在第 n-1 个记录位置
重复上述过程,直到“在一趟排序过程中没有进行过交换记录的操作”为止
例 49 38 65 97 76 13 27 30
初始关键
字
38 49 65 76 13 27 30 97
第一
趟
38 49 65 13 27 30 76
第二
趟
38 49 13 27 30 65
第三
趟38 13 27 30 49
第四
趟
13 27 30 38
第五
趟
13 27 30
第六
趟
38
49
76
9713
9727
9730
97
13
76
76
7627
30
13
6527
6530
65
13
13
49
4930
4927
3827
3830
38
17
– 算法描述 C 语言
– 算法评价• 时间复杂度
– 最好情况(正序)比较次数: n-1移动次数: 0
– 最坏情况(逆序)比较次数: )(
2
1)( 2
1
1
nninn
i
移动次数: )(2
3)(3 2
1
nninn
i
• 空间复杂度: S(n)=O(1)
T(n)=O(n²)
10.3.2 快速排序– 基本思想:通过一趟排序,将待排序记录分割成独立
的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录进行排序,以达到整个序列有序
– 排序过程:对 r[s……t] 中记录进行一趟快速排序,附设两个指针 i 和 j ,设枢轴记录 rp=r[s] , x=rp.key
T. Hoare partition初始时令 i=s , j=t首先从 j 所指位置向前搜索第一个关键字小于 x 的记录,并
和 rp 交换再从 i 所指位置起向后搜索,找到第一个关键字大于 x 的记
录,和 rp 交换重复上述两步,直至 i==j 为止再分别对两个子序列进行快速排序,直到每个子序列只含有
一个记录为止
例 初始关键字: 49 38 65 97 76 13 27 50
i j
x
ji
完成一趟排序: ( 27 38 13) 49 (76 97 65 50)
分别进行快速排序: ( 13) 27 (38) 49 (50 65) 76 (97)
快速排序结束: 13 27 38 49 50 65 76 97
4927
i ji ji j
49 65
ji
13 49
i j
49 97
i j
int Partition(SqList &L, int low, int high) { // 算法 10.6(a)
// 交换顺序表 L 中子序列 L.r[low..high] 的记录,使枢轴记录到位, // 并返回其所在位置,此时,在它之前(后)的记录均不大(小)于它 KeyType pivotkey;
RedType temp;
pivotkey = L.r[low].key; // 用子表的第一个记录作枢轴记录 while (low<high) { // 从表的两端交替地向中间扫描 while (low<high && L.r[high].key>=pivotkey) --high;
temp=L.r[low]; L.r[low]=L.r[high]; L.r[high]=temp;
// 将比枢轴记录小的记录交换到低端 while (low<high && L.r[low].key<=pivotkey) ++low;
temp=L.r[low]; L.r[low]=L.r[high]; L.r[high]=temp;
// 将比枢轴记录大的记录交换到高端 }
return low; // 返回枢轴所在位置} // Partition
int Partition(SqList &L, int low, int high) { // 算法 10.6(b)
// 交换顺序表 L 中子序列 L.r[low..high] 的记录,使枢轴记录到位, // 并返回其所在位置,此时,在它之前(后)的记录均不大(小)于它 KeyType pivotkey;
L.r[0] = L.r[low]; // 用子表的第一个记录作枢轴记录 pivotkey = L.r[low].key; // 枢轴记录关键字 while (low<high) { // 从表的两端交替地向中间扫描 while (low<high && L.r[high].key>=pivotkey) --high;
L.r[low] = L.r[high]; // 将比枢轴记录小的记录移到低端 while (low<high && L.r[low].key<=pivotkey) ++low;
L.r[high] = L.r[low]; // 将比枢轴记录大的记录移到高端 }
L.r[low] = L.r[0]; // 枢轴记录到位 return low; // 返回枢轴位置} // Partition
void QSort(SqList &L, int low, int high) { // 算法 10.7
// 对顺序表 L 中的子序列 L.r[low..high] 进行快速排序 int pivotloc;
if (low < high) { // 长度大于 1
pivotloc = Partition(L, low, high);
// 将 L.r[low..high] 一分为二 QSort(L, low, pivotloc-1);
// 对低子表递归排序, pivotloc 是枢轴位置 QSort(L, pivotloc+1, high); // 对高子表递归排序 }
} // Qsort
void QuickSort(SqList &L) { // 算法 10.8
// 对顺序表 L 进行快速排序 QSort(L, 1, L.length);
} // QuickSort
– 算法评价• 时间复杂度
– 最好情况(每次总是选到中间值作枢轴) T(n)=O(nlog2n)
– 最坏情况(每次总是选到最小或最大元素作枢轴) T(n)=O(n²)
• 空间复杂度:需栈空间以实现递归– 最坏情况: S(n)=O(n)
– 一般情况: S(n)=O(log2n)
T(n)=O(n²)
思考: 5 3 3 4 3 8 9 10 11
24
10.4 选择排序10.4.1 简单选择排序
– 排序过程• 首先通过 n-1 次关键字比较,从 n 个记录中找出关键字最小的记录,将它与第一个记录交换
• 再通过 n-2 次比较,从剩余的 n-1 个记录中找出关键字次小的记录,将它与第二个记录交换
• 重复上述操作,共进行 n-1 趟排序后,排序结束
例 初始: [ 49 38 65 97 76 13 27 ]
k
j j j j j j
k k
i=1 13 49
一趟: 13 [38 65 97 76 49 27 ]i=2
k k
j j j jj
27 38
二趟: 13 27 [65 97 76 49 38 ]
三趟: 13 27 38 [97 76 49 65 ]
四趟: 13 27 38 49 [76 97 65 ]
五趟: 13 27 38 49 65 [97 76 ]
六趟: 13 27 38 49 65 76 [97 ]
排序结束: 13 27 38 49 65 76 97
void SelectSort(SqList &L) { // 算法 10.9 // 对顺序表 L 作简单选择排序。 for (i=1; i<L.length; ++i) { // 选择第 i 小的记录,并交换到位 j = SelectMinKey(L, i); // 在 L.r[i..L.length] 中选择 key 最小的记录 if (i!=j) L.r[i]←→L.r[j]; // 与第 i 个记录交换 }} // SelectSortint SelectMinKey(SqList L, int i){ min = i; for (j = i+1; j < =L.length; j++) if (L.r[j] <L.r[min]) min = j; return min;}
– 算法评价• 时间复杂度
– 记录移动次数最好情况: 0最坏情况: 3(n-1)– 比较次数: )(
2
1)( 2
1
1
nninn
i
• 空间复杂度: S(n)=O(1)
T(n)=O(n²)
思考: 5 8 5 2 9
28
10.4.2 堆排序– 堆的定义: n 个元素的序列 (k1,k2,……kn) ,当且仅当满足下列关系时,称之为堆
或 (i=1,2,…...n/2)kik2i
kik2i+1
kik2i
kik2i+1
例 ( 96 , 83 , 27 , 38 , 11 , 9 ) 例 ( 13 , 38 , 27 , 50 , 76 , 65 ,49 , 97 )96
27
91138
83
13
2738
49657650
97可将堆序列看成完全二叉树,则堆顶元素(完全二叉树的根)必为序列中n 个元素的最小值或最大值
– 堆排序:将无序序列建成一个堆,得到关键字最小(或最大)的记录;输出堆顶的最小(大)值后,使剩余的 n-1 个元素重又建成一个堆,则可得到 n 个元素的次小值;重复执行,得到一个有序序列,这个过程叫 ~
– 堆排序需解决的两个问题:1.如何由一个无序序列建成一个堆?2.如何在输出堆顶元素之后,调整剩余元素,使之成为
一个新的堆?– 第 2 个问题解决方法——筛选
• 输出堆顶元素之后,以堆中最后一个元素替代之;然后将根结点值与左、右子树的根结点值进行比较,并与其中小者进行交换;重复上述操作,直至叶子结点,将得到新的堆,称这个从堆顶至叶子的调整过程为“筛选”
例13
2738
49657649
97
97
2738
49657649
13
输出: 13
27
4938
97657649
13
输出: 13
97
4938
27657649
13
输出: 13 27
38
4949
27657697
13
输出: 13 27
65
4949
27387697
13
输出: 13 27 38
49
6549
27387697
13
输出: 13 27 38
76
6549
27384997
13
输出: 13 27 38 49
49
6576
27384997
13
输出: 13 27 38 49
97
6576
27384949
13
输出: 13 27 38 49 49
65
9776
27384949
13
输出: 13 27 38 49 49
97
6576
27384949
13
输出: 13 27 38 49 49 65
76
6597
27384949
13
输出: 13 27 38 49 49 65
97
6576
27384949
13
输出: 13 27 38 49 49 65 76
97
6576
27384949
13
输出: 13 27 38 49 49 65 76 97
P282 筛选算法 10.10Type SqList HeapType;Void HeapAdjust(HeapType &H, int s, int m){ //H.r[s+1…m] 为大顶堆 rc = H.r[s]; for (j=2*s; j<=m; j*=2) {// 沿key较大的孩子结点向下筛选 if (j<m && LT(H.r[j].key, H.r[j+1].key)) ++j; if (!LT(rc.key, H.r[j].key)) break; H.r[s] = H.r[j]; // 大值上移到父结点 s = j; } H.r[s] = rc; }
– 第 1 个问题解决方法• 从无序序列的第 n/2 个元素(即此无序序列对应的完全二叉树的最后一个非终端结点)起,至第一个元素止,进行反复筛选
例 含 8 个元素的无序序列( 49 , 38 , 65 , 97 , 76 , 13 , 27 , 49 )49
6538
27137697
49
49
6538
27137649
97
49
1338
27657649
97
49
1338
27657649
97
13
2738
49657649
97
堆排序 算法描述 10.11
Void HeapSort (HeapType &H)
{
for (i = H.length/2; i>0; --i) // 建成大顶堆 HeapAdjust(H, i, H.length);
for (i = H.length; i>1; --i) {
temp = H.r[1]; H.r[1] = H.r[i]; H.r[i] = temp;
HeapAdjust(H, 1, i-1); //调整为大顶堆 }
}
– 算法评价• 时间复杂度:最坏情况下 T(n)=O(nlogn)• 空间复杂度: S(n)=O(1)
36
10.5 归并排序• 归并——将两个或两个以上的有序表组合成一
个新的有序表,叫 ~• 2- 路归并排序 排序过程
– 设初始序列含有 n 个记录,则可看成 n 个有序的子序列,每个子序列长度为 1
– 两两合并,得到 n/2 个长度为 2 或 1 的有序子序列
– 再两两合并,……如此重复,直至得到一个长度为n 的有序序列为止
例 初始关键字: [49] [38] [65] [97] [76] [13] [27]
一趟归并后: [38 49] [65 97] [13 76] [27]
二趟归并后: [38 49 65 97] [13 27 76]
三趟归并后: [13 27 38 49 65 76 97]
算法描述 10.12
Void Merge(RcdType SR[]. RcdType &TR[], int i, int m, int n)
{ // SR[i..m] 和 SR[m+1..n] 分别有序 , 归并到 TR[i..n]
for (j = m+1, k=i; i<=m && j<=n; ++k) {
if LQ(SR[i].key, SR[j].key) TR[k] = SR[i++];
else TR[k] = SR[j++];
} // 将 SR 中记录由小到大地并入 TR
if (i <=m) TR[k..n] = SR[i..m];
if (j <=n ) TR[k..n] = SR[j..n];
}
算法 10.13
void MSort( RcdType SR[], RcdType &TR1[], int s, int t)
{
if ( s==t) TR1[s] = SR[s];
else {
m=(s+t)/2; // 将 SR[s..t]平分为 SR[s..m] 和 SR[m+1..t]
MSort(SR, TR2, s, m); // 递归 MSort(SR, TR2, m+1, t); // 递归 Merge(TR2, TR1, s, m, t); // 归并 }
}
算法 10.14
void MergeSort(SqList &L)
{
MSort(L.r, L.r, 1, L.length);
}
• 算法评价– 时间复杂度: T(n)
=O(nlog2n)
– 空间复杂度: S(n)=O(n)
各种内部排序方法的比较和选择 简单排序中直接插入最好,快速排序最快,当文件为正序时,直接插入和冒泡均最佳。
影响排序效果的因素
因为不同的排序方法适应不同的应用环境和要求,所以选择合适的排序方法应综合考虑下列因素: ①待排序的记录数目n; ②记录的大小 ( 规模 ); ③关键字的结构及其初始状态; ④对稳定性的要求; ⑤语言工具的条件; ⑥存储结构; ⑦时间和辅助空间复杂度等。
不同条件下,排序方法的选择
(1)若 n较小 ( 如 n≤50),可采用直接插入或直接选择排序。
当记录规模较小时,直接插入排序较好;否则因为直接选择移动的记录数少于直接插人,应选直接选择排序为宜。
(2)若文件初始状态基本有序 (指正序 ),则应选用直接插人、冒泡或随机的快速排序为宜;
(3)若 n较大,则应采用时间复杂度为 O(nlgn)的排序方法:快速排序、堆排序或归并排序。
快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。这两种排序和选择排序、希尔排序都是不稳定的。
若要求排序稳定,则可选用归并排序、冒泡排序。但本章介绍的从单个记录起进行两两归并的排序算法并不值得提倡,通常可以将它和直接插入排序结合在一起使用。先利用直接插入排序求得较长的有序子文件,然后再两两归并之。因为直接插入排序是稳定的,所以改进后的归并排序仍是稳定的。
43
作业
10.1
10.2
10.3
10.4