840 likes | 1k Views
数 据 结 构. 计算机系教师 段恩泽. 办公室: C7101 电话: 82878095 Email:duanenze@126.com. 第 7 章 排序. 基本概念 简单排序方法 快速排序 堆排序 归并排序 基数排序 各种排序方法的比较与讨论 C# 中的排序方法. 排序( Sorting ) 是计算机程序设计中的一种重要操作,也是日常生活中经常遇到的问题。 字典中的单词是以字母的顺序排列,否则,使用起来非常困难。. 7.1 基本概念. 排序是把一个 记录 (在排序中把数据元素称为记录)集合或序列重新排列成按记录的某个数据项值递增(或递减)的序列。
E N D
数 据 结 构 计算机系教师 段恩泽 办公室:C7101 电话:82878095 Email:duanenze@126.com
第7章 排序 基本概念 简单排序方法 快速排序 堆排序 归并排序 基数排序 各种排序方法的比较与讨论 C#中的排序方法
排序(Sorting)是计算机程序设计中的一种重要操作,也是日常生活中经常遇到的问题。排序(Sorting)是计算机程序设计中的一种重要操作,也是日常生活中经常遇到的问题。 • 字典中的单词是以字母的顺序排列,否则,使用起来非常困难。
7.1基本概念 排序是把一个记录(在排序中把数据元素称为记录)集合或序列重新排列成按记录的某个数据项值递增(或递减)的序列。 表7-1是一个学生成绩表,其中某个学生记录包括学号、姓名及计算机文化基础、C语言、数据结构等课程的成绩和总成绩等数据项。在排序时,如果用总成绩来排序,则会得到一个有序序列;如果以数据结构成绩进行排序,则会得到另一个有序序列。
作为排序依据的数据项称为“排序项”,也称为记录的关键码。作为排序依据的数据项称为“排序项”,也称为记录的关键码。 • 关键码分为主关键码和次关键码。 • 若关键码是主关键码,则对于任意待排序的序列,经排序后得到的结果是唯一的; • 若关键码是次关键码,排序的结果不一定唯一。 • 它们之间的位置关系与排序前不一定保持一致。 • 相同关键码值的记录之间的位置关系与排序前一致,则称此排序方法是稳定的; • 如果不一致,则称此排序方法是不稳定的 注意:关键码与数据库中表中的域的关系,主关键码与主键的关系。
一个记录的关键码序列为(31,2,15,7,91,7*)。一个记录的关键码序列为(31,2,15,7,91,7*)。 • 若结果序列为(2,7,7*,15,31,91),则该排序方法是稳定的; • 若得到的结果序列为(1,7*,7,15,31,91),则这种排序方法是不稳定的。
内部排序:记录全部存放在计算机的内存中,在内存中调整记录之间的相对位置,没有内、外存的数据交换。内部排序:记录全部存放在计算机的内存中,在内存中调整记录之间的相对位置,没有内、外存的数据交换。 • 外部排序:记录的主要部分存放在外存中,借助于内存逐步调整记录之间的相对位置。不断地在内、外存之间交换数据。
排序问题的记录采用线性结构。 • 排序算法基本上是基于顺序表设计。
7.2 简单排序方法 7.2.1 直接插入排序 直接插入排序的基本思想是:顺序地将待排序的记录按其关键码的大小插入到已排序的记录子序列的适当位置。子序列的记录个数从1开始逐渐增大,当子序列的记录个数与顺序表中的记录个数相同时排序完毕。 注意:排序的范围由小到大。
设顺序表sqList中有n个记录,初始时子序列中只有sqList[0]。设顺序表sqList中有n个记录,初始时子序列中只有sqList[0]。 • 第一次排序,比较sqList[0]和sqList[1]的大小,若sqList[0]≤sqList[1],说明序列已有序,否则将sqList[1]插入到sqList[0]的前面,这样子序列的大小增大为2。 • 第二次排序时,比较sqList[2] 和sqList[1]以确定是否需要把sqList[2]插入到sqList[1]之前。如果sqList[2]插入到sqList[1]之前,再比较sqList[2]和sqList[0]以确定是否需要把sqList[2]插入到sqList[0]之前。
直接插入排序的算法: public void InsertSort(SeqList<int> sqList) { for (int i = 1; i < sqList.Last; ++i) { if (sqList[i] < sqList[i - 1]) { int tmp = sqList[i]; int j = 0;
for (j = i - 1; j >= 0&&tmp<sqList[j]; --j) { sqList[j + 1] = sqList[j]; } sqList[j + 1] = tmp; } } }
分最好、最坏和随机三种情况: • 最好的情况是顺序表中的记录已全部排好序。外层循环的次数为n-1,内层循环的次数为0,外层循环中每次记录的比较次数为1,最好情况下的时间复杂度为O(n)。 • 最坏情况是顺序表中记录是反序的。内层循环的循环系数每次均为i。这样,整个外层循环的比较次数为n-1,最坏情况下的时间复杂度为O(n2)。 • 如果顺序表中的记录的排列是随机的,则记录的期望比较次数为n2/4。在一般情况下的时间复杂度为O(n2)。 可以证明,顺序表中的记录越接近于有序,直接插入排序算法的时间效率越高,在O(n)到O(n2)之间。 • 直接插入排序算法的空间复杂度为O(1) ,是一种稳定的排序算法。
0 1 2 3 4 5 6 7 初始: [42] 20 17 27 13 8 17* 48 i=1 [20 42] i=2 [17 20 42] i=3 [17 20 27 42] i=4 [13 17 20 27 42] i=5 [8 13 17 20 27 42] i=6 [8 13 17 17* 20 27 42] i=7 [8 13 17 17* 20 27 42 48] 【例7-1】关键码序列为(42,20,17,27,13,8,17*,48),用直接插入排序算法进行排序。
7.2.2 冒泡排序 • 冒泡排序的基本思想是:将相邻的记录的关键码进行比较,若前面记录的关键码大于后面记录的关键码,则将它们交换,否则不交换。 • 设待排序的顺序表sqList中有n个记录,冒泡排序要进行n-1趟,每趟循环都是从最后两个记录开始。 • 第1趟循环到第2个记录的关键码与第1个记录的关键码比较后终止; • 第2躺循环到第3个记录的关键码与第2个记录的关键码比较结束后终止; • 第i趟循环到第i+1个记录的关键码与第i个记录的关键码比较后终止; • 第n-1趟循环到第n个记录的关键码与第n-1个记录的关键码比较后终止。 注意:排序的范围由大到小。
冒泡排序算法的实现: public void BubbleSort(SeqList<int> sqList) { int tmp; for (int i = 0; i < sqList.Last; ++i) { for (int j = sqList.Last - 1; j >= i; --j) { if (sqList[j + 1] < sqList[j]) { tmp = sqList[j + 1]; sqList[j + 1] = sqList[j]; sqList[j] = tmp; } } } }
最好情况是记录已全部排好序,这时,循环n-1次,最好情况下的时间复杂度为O(n)。最好情况是记录已全部排好序,这时,循环n-1次,最好情况下的时间复杂度为O(n)。 • 最坏情况是记录全部逆序存放,循环n-1次,总比较次数为: • 总的移动次数为比较次数的3倍,在最坏情况下的时间复杂度为O(n2)。 • 冒泡排序算法是一种稳定的排序方法。
0 1 2 3 4 5 6 7 初始:42 20 17 27 13 8 17* 48 第1趟 8 42 20 17 27 13 17* 48 第2趟 8 13 42 20 17 27 17* 48 第3趟 8 13 17 42 20 17* 27 48 第4趟 8 13 17 17* 42 20 27 48 第5趟 8 13 17 17* 20 42 27 48 第6趟 8 13 17 17* 20 27 42 48 第7趟 8 13 17 17* 20 27 42 48 【例7-2】关键码序列为(42,20,17,27,13,8,17*,48),用冒泡排序算法进行排序。
7.2.3 简单选择排序 简单选择排序算法的基本思想是: • 从待排序的记录序列中选择关键码最小(或最大)的记录与第一个记录交换位置; • 然后从除第一个记录的序列中选择关键码最小(或最大)的记录与第二个记录交换位置; • 如此重复,直到序列中只剩下一个记录为止。 注意:选择的范围从大到小。
简单选择排序要进行n-1趟: • 第1趟从n个记录选择关键码最小(或最大)的记录与第1个记录交换位置; • 第2趟从n-1个记录中选择关键码最小(或最大)的记录与第2个记录交换位置。 • 第i趟从n-i+1个记录中选择关键码最小(或最大)的记录与第i个记录交换位置; • 第n-1趟在最后两个记录中选择关键码最小(或最大)的记录与第n-1个记录交换位置。
public void SimpleSelectSort(SeqList<int> sqList) { int tmp = 0; int t = 0; for (int i = 0; i < sqList.Last; ++i) { t = i; for (int j = i + 1; j <= sqList.Last; ++j) {
if (sqList[t] > sqList[j]) { t = j; } } tmp = sqList[i]; sqList[i] = sqList[t]; sqList[t] = tmp; } }
第一次排序进行n-1次比较,第二次排序进行n-2次比较,…,第n-1排序进行1次比较,总的比较次数为:第一次排序进行n-1次比较,第二次排序进行n-2次比较,…,第n-1排序进行1次比较,总的比较次数为: • 移动次数最好0次,最坏为3次,时间复杂度为O(n2)。 简单选择排序算法是一种稳定的排序方法。
0 1 2 3 4 5 6 7 初始:42 20 17 27 13 8 17* 48 第1趟 8 20 17 27 13 42 17* 48 第2趟 8 13 17 27 20 42 17* 48 第3趟 8 13 17 27 20 42 17* 48 第4趟 8 13 17 17* 20 42 27 48 第5趟 8 13 17 17* 20 42 27 48 第6趟 8 13 17 17* 20 27 42 48 第7趟 8 13 17 17* 20 27 42 48
7.3 快速排序 快速排序的基本思想是:不断比较关键码,以某个记录为界将待排序列分成两部分。其中,一部分记录的关键码都大于或等于支点记录的关键码,另一部分记录的关键码都小于支点记录的关键码,称为一次划分。对各部分不断划分,直到整个序列按关键码有序为止。 注意:快速排序的原理是缩小排序的范围,也叫二分排序。
待排序的顺序表sqList中有n个记录: • 第一次划分把第一个记录作为支点; • 把支点复制到临时存储空间,low指向顺序表的低端,high指向顺序表的高端。 • 从high开始,将记录的关键码与支点进行比较: • high的关键码大于支点,high向低端移动一个位置; • 将high记录复制到low的存储空间中。 • 从low开始,将记录的关键码与临时存储空间进行比较: • low的关键码小于临时存储空间的关键码,low向高端移动一个位置; • 将low记录复制到high的存储空间中。 • 重复,直到low和high指向同一个记录,将临时空间的记录赋给low存储空间,第一次划分结束。
快速排序的算法实现如下所示: public void QuickSort(SeqList<int> sqList, int low, int high) { int i = low; int j = high; int tmp = sqList[low];
while (low < high) { while ((low < high) && (sqList[high] >= tmp)) { --high; } sqList[low] = sqList[high];
while ((low < high) && (sqList[low] <= tmp)) { ++low; } sqList[high] = sqList[low]; } sqList[low] = tmp;
if (i < low-1) { QuickSort(sqList, i, low-1); } if (low+1 < j) { QuickSort(sqList, low+1, j); } } }
【例7-4】关键码序列为(42,20,17,27,13,8,17*,48),写出用快速排序算法进行排序的过程。【例7-4】关键码序列为(42,20,17,27,13,8,17*,48),写出用快速排序算法进行排序的过程。 0 1 2 3 4 5 6 7 初始: 42 20 17 27 13 8 17* 48 low high 第一次划分: 第1次查找交换,得到如下结果: 17* 20 17 27 13 8 17* 48 low high 第2次查找交换,得到如下结果,第一次划分结束 17* 20 17 27 13 8 42 48 low high
第二次划分: 17* 20 17 27 13 8 42 48 low high 第1次查找交换,得到如下结果: 8 20 17 27 13 8 42 48 low high 第2次查找交换,得到如下结果: 8 20 17 27 13 20 42 48 low high 第3次查找交换,得到如下结果: 8 13 17 27 13 20 42 48 low high 第4次查找交换,得到如下结果: 8 13 17 27 27 20 42 48 low high 第5次查找交换,得到如下结果,第二次划分结束。 8 13 17 17* 27 20 42 48 low high
第三次划分: 8 13 17 17* 27 20 42 48 low high 第1次查找交换,得到如下结果 ,第三次划分结束。 8 13 17 17* 27 20 42 48 low high
第四次划分: 8 13 17 17* 27 20 42 48 low high 第1次查找交换,得到如下结果 8 13 17 17* 20 27 42 48 low high 第2次查找交换,得到如下结果,第四次划分结束。 8 13 17 17* 20 27 42 48 low high
最好情况下快速排序,每次选取的记录都能均分成两个相等的子序列,这样的快速排序过程是一棵完全二叉树结构,分解次数等于完全二叉树的深度log2n,全部的比较次数都接近于n-1次,所以,时间复杂度为O(nlog2n)。最好情况下快速排序,每次选取的记录都能均分成两个相等的子序列,这样的快速排序过程是一棵完全二叉树结构,分解次数等于完全二叉树的深度log2n,全部的比较次数都接近于n-1次,所以,时间复杂度为O(nlog2n)。 • 最坏情况是记录已全部有序,n个记录待排序列的根结点的分解次数就构成了一棵单右支二叉树,间复杂度为O(n2)。 • 一般情况下,记录的分布是随机的,序列的分解次数构成一棵二叉树,这样二叉树的深度接近于log2n,时间复杂度为O(nlog2n)。 快速排序算法是一种不稳定的排序的方法。
7.4 堆排序 堆排序的基本思想:顺序表是一个线性结构,从有n个记录中选择出一个最小的记录需要比较n-1次。如果把n个记录构成一个完全二叉树,则每次选择出一个最大(或最小)的记录比较的次数就是完全二叉树的高度,即log2n次,算法的时间复杂度就是O(nlog2n)。
堆分为最大堆和最小堆两种 最大堆的定义: 设顺序表sqList中存放了n个记录,对于任意的i(0≤i≤n-1),如果2i+1<n时有sqList[i]的关键码不小于sqList[2i+1]的关键码;如果2i+2<n时有sqList[i] 的关键码不小于sqList[2i+2] 的关键码,则这样的堆为最大堆。
如果把这n个记录看作是一棵完全二叉树的结点,则sqList[0]对应完全二叉树的根,sqList[1]对应树根的左孩子结点,sqList[2]对应树根的右孩子结点,如果把这n个记录看作是一棵完全二叉树的结点,则sqList[0]对应完全二叉树的根,sqList[1]对应树根的左孩子结点,sqList[2]对应树根的右孩子结点, • sqList[3]对应sqList[1]的左孩子结点,sqList[4]对应sqList[2]的右孩子结点,如此等等。在此基础上,只需调整所有非叶子结点满足:sqList[i] 的关键码不小于sqList[2i+1] 的关键码和sqList[i] 的关键码不小于sqList[2i+2] 的关键码,则这样的完全二叉树就是一个最大堆。
42 48 20 17 42 17 27 13 8 17* 27 13 8 17* 48 20 (a) 完全二叉树 (b) 最大堆
最小堆的定义: • 设顺序表sqList中存放了n个记录,对于任意的i(0≤i≤n-1),如果2i+1<n时有sqList[i] 的关键码不大于sqList[2i+1] 的关键码;如果2i+2<n时有sqList[i] 的关键码不大于sqList[2i+2] 的关键码,则这样的堆为最小堆。 • 如果把这n个记录看作是一棵完全二叉树的结点,则sqList[0]对应完全二叉树的根,sqList[1]对应树根的左孩子结点,sqList[2]对应树根的右孩子结点, • sqList[3]对应sqList[1]的左孩子结点,sqList[4]对应sqList[2]的右孩子结点,如此等等。在此基础上,只需调整所有非叶子结点满足:sqList[i] 的关键码不大于sqList[2i+1] 的关键码和sqList[i] 的关键码不大于sqList[2i+2] 的关键码,则这样的完全二叉树就是一个最小堆。
42 8 20 17 13 17* 27 13 8 17* 27 20 17 42 48 48 (b) 最小堆 (a) 完全二叉树
堆的两个性质: • 最大堆的根结点是堆中关键码最大的结点,最小堆的根结点是堆中关键码最小的结点,我们称堆的根结点记录为堆顶记录。 • 对于最大堆,从根结点到每个叶子结点的路径上,结点组成的序列都是递减有序的;对于最小堆,从根结点到每个叶子结点的路径上,结点组成的序列都是递增有序的。
堆排序的过程: • 首先将这n个记录按关键码建成堆,将堆顶记录输出,得到n个记录中关键码最大(或最小)的记录。 • 再把剩下的n-1个记录,输出堆顶记录,得到n个记录中关键码次大(或次小)的记录。 • 如此反复,便可得到一个按关键码有序的序列。
需解决两个问题: • 如何将n个记录的序列按关键码建成堆; • 输出堆顶记录后,怎样调整剩下的n-1个记录,使其按关键码成为一个新堆。
所有的叶子结点都满足最大堆的定义。 • 对于第1个非叶子结点sqList[i](i=(n-1)/2),左孩子sqList[2i+1]和右孩子sqList[2i+2] 是最大堆,找出其中关键码较大者与sqList[i]结点的关键码进行比较: • sqList[i] 大于或等于较大结点的关键码,则以sqList[i]结点为根结点的完全二叉树已满足最大堆的定义; • 否则,对换sqList[i]结点和关键码较大的结点,对换后以sqList[i]结点为根结点的完全二叉树满足最大堆的定义 • 再调整第2个非叶子结点sqList[i-1] (i=(n-1)/2),第3个非叶子结点sqList[i-2],…,直到根结点。 • 当根结点调整完后,则这棵完全二叉树就是一个最大堆了。
42 42 20 17 20 17 27 13 8 17* 48 13 8 17* 48 27 42 20 17 48 13 8 17* 27 42 20 17 27 13 8 17* 48 顺序表 顺序表 (b) (a) 42 42 20 17 48 17 48 13 8 17* 27 13 8 17* 27 20 42 20 17 48 13 8 17* 27 42 48 17 27 13 8 17* 20 顺序表 顺序表 (d) (c) 48 42 17 27 13 8 17* 20 48 42 17 27 13 8 17* 20 (e) 顺序表
第一步:从i=(n-1)/2=(7-1)/2=3开始,sqList[3]的关键码27小于sqList[7]的关键码48,所以,sqList[3]与sqList[7]交换,这样,以sqList[3]为根结点的完全二叉树是一个最大堆,如图 b所示。 • 第二步:当i=2时,由于sqList[2] 的关键码17不小于sqList[5]的关键码8和sqList[6]的关键码17*,所以不需要调整,如图 c所示。 • 第三步:当i=1时,由于sqList[1]的关键码20小于sqList[3]的关键码48,所以将sqList[1]与sqList[3]交换,这样导致sqList[3]的关键码20小于sqList[7]的关键码27,所以将sqList[3]与sqList[7]交换,这样,以sqList[1]为根结点的完全二叉树是一个最大堆,如图 d)所示。 • 第四步:当i=0时,对堆顶结点记录进行调整,由于sqList[0] 的关键码42小于sqList[1] 的关键码48,所以将sqList[0]与sqList[1]交换,这样,以sqList[0]为根结点的完全二叉树是一个最大堆,如图 (e)所示,整个堆建立的过程完成。
建堆的算法如下所示: public void CreateHeap(SeqList<int> sqList, int low, int high) { if ((low < high) && (high <= sqList.Last)) { int j = 0; int tmp = 0; int k = 0;