第10章 内排序
1、排序的基本概念
- 排序:假设一个文件由 \(n\) 个记录 \(R_1,R_2,\cdots,R_n\) 组成,以记录中某个(或几个)字段值不减(或不增)的次序把这 \(n\) 个记录从新排列,称该字段为排序码
- 关键码:能惟一标识一个记录的字段,关键码能够做为排序码,但排序码不必定要是关键码
- 稳定排序算法:任意两个排序码相同的记录 \(R_i\) 和 \(R_j\),若是排序前 \(R_i\) 在 \(R_j\) 的前面,排序后 \(R_i\) 还在在 \(R_j\) 的前面,则该算法是稳定的排序算法,不然是不稳定的排序算法
- 注:如下讨论的排序,都是以记录中某个字段值不减的次序把这 \(n\) 个记录从新排列,且排序码数据类型为整型
1.1 排序算法的记录存储结构定义
#define MAXSIZE 100 // 文件中记录个数的最大值
typedef int keytype; // 定义排序码类型为整数类型
typedef struct {
keytype key;
// int other; // 此处还能够定义记录中除排序码外的其它域
} recordtype; // 记录类型的定义
typedef struct {
recordtype r[MAXSIZE + 1];
int length; // 待排序文件中记录的个数
} table; // 待排序文件类型
2、插入排序
2.1 直接插入排序
-
算法步骤:算法
- 初始认为文件中的第 \(1\) 个记录已排好序
- 而后把第 \(2\) 个到第 \(n\) 个记录依次插入到已排序的记录组成的文件中
- 对第 \(i\) 个记录 \(R_i\) 进行插入时,\(R_1,R_2,\cdots,R_{i-1}\) 已排序
- 把记录 \(R_i\) 的排序码 \(key_i\) 和已经排好序的排序码从右向左依次比较,找到 \(R_i\) 应插入的位置
- 把该位置之后直到 \(R_{i-1}\) 的记录顺序后移,空出位置让 \(R_i\) 插入
- 注:当待插入排序小于全部已排序的排序码时,可能会出现下标越界,所以能够在排序文件的第一个排序码前插入一个值做为哨兵
-
图10-2-直接插入排序算法的执行过程示意图:
shell
2.1.1 直接插入排序算法(算法)
void insertsort(table *tab) {
int i, j;
// 依次插入从第2个开始的全部元素
for (i = 2; i <= tab->length; i++) {
j = i - 1;
tab->r[0].key = tab->r[i].key; // 设置哨兵,准备找插入位置
// 找插入位置并后移
while (tab->r[0].key < tab->r[j].key) {
tab->r[j + 1].key = tab->r[j].key; // 后移
j = j - 1; // 继续向前(左)查找
}
tab->r[j + 1].key = tab->r[0].key; // 插入第i个元素的副本,即前面设置的哨兵
}
}
时间复杂度:\(O(n^2)\)数组
2.2 二分法(折半)插入排序
-
算法步骤:spa
- 注:依据直接插入排序的思想,进行扩充
- 在找第 \(i\) 个记录的插入位置时,因为前 \(i-1\) 个记录已排好序,所以在插入过程当中使用二分法肯定 \(key[i]\) 的插入位置
-
算法代码:略设计
2.3 表插入排序(大纲未规定)
- 略
2.4 希尔(Shell)插入排序
- 算法步骤:
- 对 \(n\) 个记录进行排序,首先取一个整数 \(d<n\),把这个 \(n\) 个记录分红 \(d\) 组
- 全部位置相差为 \(d\) 的倍数的记录分在同一组
- 在每组中使用直接插入排序进行组内排序
- 缩小 \(d\) 的值
- 重复进行分组和组内排序
- 知道 \(d=1\) 结束排序
- 算法代码:略
- 图10-5-Shell插入排序示意图:

3、选择排序
- 选择排序基本思想:每次从待排序文件中选择出排序码最小的记录,把该记录放入已排序文件的最后一个位置
3.1 直接选择排序
- 算法步骤:
- 首先从 \(n\) 个记录中选择排序码最小的记录,让该记录和第 \(1\) 个记录交换
- 再从剩下的 \(n-1\) 个记录选出排序码最小的记录,让该记录和第 \(2\) 个记录交换
- 重复这样的操做直到剩下最后两个记录
- 从最后两个记录中选出最小的记录和第 \(n-1\) 个记录交换
- 结束
- 图10-6-直接选择排序算法执行过程:

3.1.1 直接选择排序算法(算法)
void simpleselectsort(table *tab) {
int i, j, k;
for (i = 1; i <= tab->length - 1; i++) {
k = i; // 记下当前最小元素的位置
// 向右查找更小的元素
for (j = i + 1; j <= tab->length; j++)
if (tab->r[j].key < tab->r[k].key) k = j; // 修改当前最小元素的位置
// 若是第i次选到的最小元素位置k不等于i,则将第k、i个元素交换
if (k != i) {
tab->r[0].key = tab->r[k].key; // 以第0个元素做为中间单元进行交换
tab->r[k].key = tab->r[i].key;
tab->r[i].key = tab->r[0].key;
}
}
}
时间复杂度:\(O(n^2)\)3d
3.2 树型选择排序(大纲未规定)
3.3 堆排序
-
堆排序解决的问题:保存中间比较结果,减小后面的比较次数,又不占用大量的附加存储空间指针
-
堆:一个序列 \(\{k_1,k_2,\cdots,k_n\}\),其中 \(k_i\leq{k_{2i}}\) 且 \(k_i\leq{k_{2i+1}}\),当 \(i=1,2,\cdots,n/2\) 且 \(2i+1\leq{n}\)code
- 注:当采用顺序存储这个序列,就能够把这个序列的每个元素 \(k_i\) 当作是一颗有 \(n\) 个结点的彻底二叉树的第 \(i\) 个结点,其中 \(k_1\) 是该二叉树的根结点
-
堆的特征:把堆对应的一维数组看做一颗彻底二叉树的顺序存储,则该彻底二叉树中任意分支结点的值都小于或等于它的左右儿子结点的值,且根结点的值是全部元素中值最小的blog
-
最小堆和最大堆:当父节点的值老是大于等于任何一个子节点的值时为最大堆; 当父节点的值老是小于等于任何一个子节点的值时为最小堆排序
-
堆排序使用条件:当对记录 \(n\) 较大的文件,堆排序很好,当 \(n\) 较小时,并不提倡使用,由于初始建堆和调整建新堆时须要进行反复的筛选
-
图10-8-一个序列 Arr = {5, 1, 13, 3, 16, 7, 10, 14, 6, 9}
和相应的彻底二叉树:
-
构造最大堆步骤(经过已有彻底二叉树构建堆):
- 主要思想:判断有子女结点的子树是否知足最大(小)堆的要求
- 首先咱们须要找到最后一个结点的父结点如图 \((a)\),咱们找到的结点是 \(16\),而后找出该结点的最大子节点与本身比较,若该子节点比自身大,则将两个结点交换。图 \((a)\) 中,\(16\) 是最大的结点,不须要交换
- 咱们移动到第下一个父结点 \(3\),如图 \((b)\) 所示。同理作第一步的操做,交换了 \(3\) 和 \(14\),结果如图 \((c)\) 所示
- 移动结点到下一个父结点 \(13\),如图 \((d)\) 所示,发现不须要作任何操做
- 移动到下个父结点 \(1\),如图 \((e)\) 所示,而后交换 \(1\) 和 \(16\),如图 \((f)\) 所示,此时咱们发现交换后,\(1\) 的子节点并非最大的,咱们接着在交换,如图 \((g)\)所示
- 移动到父结点到 \(5\),一次重复上述步骤,交换 \(5\) 和 \(16\),在交换 \(14\) 和 \(5\),在交换 \(5\) 和 \(6\) 全部节点交换完毕,最大堆构建完成
-
图10-9-建堆过程示意图:
-
堆排序步骤:
- 构建一个筛选算法,该算法能够把一个任意的排序码序列建成一个堆,堆的第一个元素就是排序码中最小的
- 把选出的最小排序码从堆中删除,对剩余的部分从新建堆,继续选出其中的最小者
- 直到剩余 \(1\) 个元素时,排序结束
4、交换排序
- 交换排序:对待排序记录两两进行排序码比较,若不知足排序顺序,则交换这对排序码
4.1 冒泡排序
- 冒泡排序算法步骤:
- 对全部记录从左到右每相邻两个记录的排序码进行比较,若是这两个记录的排序码不符合排序要求,则进行交换,这样一趟作完,将排序码最大者放在最后一个位置
- 对剩下的 \(n-1\) 个待排序记录重复上述过程,又将一个排序码放于最终位置,反复进行 \(n-1\) 次,可将 \(n-1\) 个排序码对应的记录放至最终位置,剩下的即为排序码最小的记录,它在第 \(1\) 的位置处
- 注:若是在某一趟中,没有发生交换,则说明此时全部记录已经按排序要求排列完毕,排序结束
- 图10-10-冒泡排序算法示意图:

4.2 快速排序
- 快速排序算法步骤(主要经过首尾指针的移动来划分大于 \(x\) 和小于 \(x\) 的值):
- 从 \(n\) 个待排序的记录中任取一个记录(不妨取第 \(1\) 个记录)
- 设法将该记录放置于排序后它最终应该放的位置,使它前面的记录排序码都不大于它的排序码,然后面的记录排序码都大于它的排序码
- 而后对前、后两部分待排序记录重复上述过程,能够将全部记录放于排序成功后的相应位置,排序即告完成
- 图10-11-一次划分的过程:

5、归并排序
-
归并排序算法基本思路:一个待排序记录构成的文件,能够看做是有多个有序子文件组成的,对有序子文件经过若干次使用归并的方法,获得一个有序文件
-
归并:把两个(或多个)有序子表合并成一个有序表的过程
-
一次归并:把一个数组中两个相邻的有序数组归并为一个有序数组段,其结果存储于另外一个数组
-
一趟归并:\(n\) 个元素的数组中,从第 \(1\) 个元素开始,每连续 \(len\) 个元素组成的数组段是有序的,从第一个数组段开始,每相邻的两个长度相等的有序数组段进行一次归并,一直到剩余元素个数小于 \(2*len\)时结束。若是剩余元素个数大于 \(len\),能够对一个长度为 \(len\),另外一个长度小于 \(len\) 的两个有序数组实行一次归并;若是其个数小于 \(len\),则把这些元素直接拷贝到目标数组中
-
图10-13-一趟归并的图示:
-
二路归并排序算法步骤:
- 对任意一个待排序的文件,初始时它的有序段的长度为 \(1\)
- 经过不断调用一趟归并算法,使有序段的长度不断增长
- 直到有序段的长度不小于待排序文件的长度,排序结束
-
图10-14-二路归并排序过程示意图:
6、基数排序
- 基数排序(又称分配排序):不对排序码比较,而是借助于多排序码排序的思想进行单排序吗排序的方法
6.1 多排序码的排序
- 注: 每一张牌有两个 “排序码”:花色(梅花<方块<红心<黑桃)和面值(\(2<3<\ldots<A\)),且花色的地位高于面值,即面值相等的两张牌,以花色大的为大,即在比较两张牌的牌面大小时,先比较花色,若花色相同,则再比较面值
- 分配:把 \(52\) 张扑克牌按面值分红 \(13\) 堆,再按照不一样的花色分红 \(4\) 堆,分红若干堆的过程称为分配
- 收集:第一次分配时,把面值不一样的 \(13\) 堆牌自小至大叠在一块儿,把不一样花色的 \(4\) 堆牌自小至大的次序合在一块儿,从若干堆自小到大叠在一块儿的过程称为收集
6.2 静态链式基数排序
- 静态链式基数排序步骤:
- 先用静态链表存储待排序文件中的 \(n\) 个记录,表中每个结点对应一个记录
- 第一趟对低位排序码(个位数)进行分配,将 \(n\) 个结点分配到序号分别为 \(0-9\) 的 \(10\) 个链式队列中,用 \(f[i]\) 和 \(e[i]\) 分别做为第 \(i\) 个队列的队首和队尾指针
- 第一趟收集过程当中把这个 \(10\) 个队列中非空的队列依次合并在一块儿产生一个新的静态单链表
- 对这个新的静态单链表按十位数进行分配和采集,而后再依次对百位数、千位数直到最高位数反复进行这样的分配和收集操做,排序结束
- 图10-15-静态链式技术排序示意图:

7、各类内部排序算法的比较(书中未给出)
排序方法 |
比较次数(最好) |
比较次数(最差) |
移动次数(最好) |
移动次数(最差) |
稳定性 |
直接插入排序 |
\(n\) |
\(n^2\) |
\(0\) |
$$n^2$$ |
\(√\) |
折半插入排序 |
\(nlog_2n\) |
\(nlog_2n\) |
\(0\) |
\(n^2\) |
\(√\) |
冒泡排序 |
\(n\) |
\(n^2\) |
\(0\) |
\(n^2\) |
\(√\) |
快速排序 |
\(nlog_2n\) |
\(n^2\) |
\(nlog_2n\) |
\(n^2\) |
\(×\) |
简单选择排序 |
\(n^2\) |
\(n^2\) |
\(0\) |
\(n\) |
\(×\) |
堆排序 |
\(nlog_2n\) |
\(nlog_2n\) |
$$nlog_2n$$ |
\(nlog_2n\) |
\(×\) |
归并排序 |
\(nlog_2n\) |
\(nlog_2n\) |
\(nlog_2n\) |
\(nlog_2n\) |
\(√\) |
基数排序 |
\(O(b(n+rd))\) |
\(O(b(n+rd))\) |
|
|
\(√\) |
上表基数排序中:\(rd\) 是收集过程和各位排序码的取值范围中值的个数(十进制为 \(10\));静态链式基数排序要进行 \(b\) 唐收集和分配
8、内部排序算法的应用(书中未给出)
- 略
9、算法设计题
- 略
10、错题集
- 下列说法错误的是:基数排序适合实型数据的排序
- 插入排序算法在最后一趟开始以前,全部的元素可能都不在其最终的位置上(好比最后一个须要插入的元素是最小的元素)
- 一个序列中有 \(10000\) 个元素,若只想获得其中前 \(10\) 个最小元素,最好采用堆排序算法
- 排序的趟数和待排序元素的原始状态有关的排序算法是冒泡排序
- 快速排序算法和冒泡排序算法都会出现起泡的现象