排序&查找
查找
基本概念
-
查找: 在数据集合中寻找满足某种条件的数据元素的过程称为查找 -
查找表(查找结构):用于查找的数据集合称为查找表,它由同一类型的数据元素(或记录)组成 -
关键字: 数据元素中唯一标识该元素的某个数据项的值,使用基于关键字的查找,查找结果应该是唯一的。


对查找表的常见操作
查找符合条件的数据元素
-
静态查找表
-
仅关注查找速度即可
插入、删除某个数据元素
- 动态查找表
- 除了查找速度,也要关注插/删操作是否方便实现

查找算法的评价指标
查找长度:在查找运算中,需要对比关键字的次数称为查找长度
平均查找长度(ASL, Average Search Length ):所有查找过程中进行关键字的比较次数的平均值

ASL的数量级反应了查找算法的时间复杂度
评价一个查找算法的效率时,通常考虑查找成功、查找失败两种情况的ASL



顺序查找
定义
顺序查找,又叫“线性查找”,通常用于线性表。
算法思想:从头到jio依次往后找(反过来也OK)
顺序查找的实现

顺序查找的实现(哨兵)

优点:无需判断是否越界,效率更高
顺序查找效率分析
\(\mathbf{A S L}=\sum_{i=1}^{n} P_{i} C_{i}\)
\(\mathbf{A S L}_{\text {成功 }}=\frac{1+2+3+\ldots+n}{n}=\frac{n+1}{2}\)
\(\mathbf{A S L}_{\text {失败 }}=\mathrm{n}+1\)
顺序查找的优化(对有序表)

用查找判定树分析
ASL
- 一个成功结点的查找长度 = 自身所在层数
- 一个失败结点的查找长度 = 其父结点所在层数
- 默认情况下,各种失败情况或成功情况都等概率发生

顺序查找的优化(被查概率不相等)


折半查找
算法思想
折半查找,又称“二分查找”,仅适用于有序的顺序表。

代码实现

查找效率分析

折半查找判定树的构造

如果当前low和high之间有偶数个元素,则 mid 分隔后,左半部分比右半部分少一个元素
如果当前low和high之间有奇数个元素,则 mid 分隔后,左右两部分元素个数相等
折半查找的判定树中,mid = [(low + high)/2]则对于任何一个结点,必有:右树结点数-左子树结点数=0或1
折半查找的判定树一定是平衡二叉树
折半查找的判定树中,只有最下面一层是不满的因此,元素个数为n时树高\(h=\left\lceil\log _{2}(n+1)\right\rceil\)(该树高不包含失败节点)
判定树结点关键字:左<中<右,满足二叉排序树的定义
失败结点:n+1个(等于成功结点的空链域数量)
折半查找的时间复杂度:\(O(log_2n)\)

分块查找
算法思想

特点:块内无序、块间有序
分块查找,又称索引顺序查找,算法过程如下:
-
在索引表中确定待查记录所属的分块(可顺序、可折半)
-
在块内顺序查找
用折半查找查索引
查找目标与索引表值相同

查找目标与索引值不同

查找失败

查找效率分析



二叉排序树
定义

二叉排序树查找

| 非递归实现 | 递归实现 |
|---|---|
![]() |
![]() |
二叉排序树插入

二叉排序树构造
![]() |
![]() |
二叉排序树删除
先搜索找到目标结点:
- 若被删除结点z是叶结点,则直接删除,不会破坏二叉排序树的性质
- 若结点z只有一棵左子树或右子树,则让z的子树成为z父结点的子树,替代z的位置。
- 若结点z有左、右两棵子树,则令z的直接后继 (或直接前驱)替代z,然后从二叉排序树中删去这 个直接后继(或直接前驱),这样就转换成了第一或第二种情况。

查找效率分析


| 查找成功的平均查找长度ASL | 查找失败的平均查找长度ASL |
|---|---|
最好情况:\(ASL = (1*1 + 2*2 + 3*4 + 4*1)/8 = 2.625\) 最坏情况: \(ASL=(1*1+2*2 +3*1 +4*1 +5*1 +6*1+ 7*1)/8 =3.75\) |
最好情况:\(ASL = (3*7 + 4*2)/9 = 3.22\) 最坏情况:\(ASL = (2*3 +3+4+5+6+7*2)/9 = 4.22\) |

平衡二叉树
定义

平衡二叉树
![]() |
![]() |
调整平衡二叉树★★★

调整最小不平衡子树LL&RR
![]() |
![]() |

调整最小不平衡子树LR&RL
![]() |
1 |
![]() |
![]() |
![]() |
![]() |

平衡二叉树插入&删除
平衡二又树的
插入操作:
插入新结点后,要保持二叉排序树的特性不变(左<中<右)- 若插入新结点导致
不平衡,则需要调整平衡
平衡二叉树的
删除操作:
删除新结点后,要保持二叉排序树的特性不变(左<中<右)- 若
删除新结点导致不平衡,则需要调整平衡
平衡二叉树的
删除操作具体步骤:
- 删除结点(方法同“二叉排序树”)
若删除的结点是叶子,直接删。
若删除的结点只有一个子树,用子树顶替删除位置
若删除的结点有两棵子树,用前驱(或后继)结点顶替,并转换为对前驱(或后继)结点的删除 一路向北找到最小不平衡子树,找不到就完结撒花- 找最小不平衡子树下,
“个头”最高的儿子、孙子 - 根据孙子的位置,调整平衡(LL/RR/LR/RL)
孙子在LL:儿子右单旋
孙子在RR:儿子左单旋
孙子在LR:孙子先左旋,再右旋
孙子在RL:孙子先右旋,再左旋 - 如果不平衡向上传导,继续2.
对最小不平衡子树的旋转可能导致树变矮,从而导致上层祖先不平衡 (不平衡向上传递)
平衡二叉树删除操作
时间复杂度\(=O\left(\log _{2} n\right)\)
红黑树

定义
平衡二叉树AVL:插入/删除很容易破坏“平衡”特性,需要频繁调整树的形态。如:插入操作导致不平衡,则需要先计算平衡因子,找到最小不平衡子树(时间开销大),再进行LL/RR/LR/RL调整
| 违反“不红红” | 违反“根叶黑” |
|---|---|
![]() |
![]() |
| 违反“黑路同” | 违反“左根右” |
|---|---|
![]() |
![]() |
性质1: 从根节点到叶结点的最长路径不大于最短路径的2倍
性质2: 有 \({\mathrm{n}}\) 个内部节点的红黑树高度 \({\mathrm{h} \leq 2 \log _{2}(n+1)}\)
红黑树查找操作时间复杂度 \(=O\left(\log _{2} n\right)\)——(查找效率与AVL树同等数量级)
红黑树查找
与 BST、AVL 相同,从根出发,左小右大,若查找到一个空叶节点,则查找失败
红黑树的插入
从一棵空的红黑树开始,插入:20,10,5,30,40,57,3,2,4,35,25,18,22,23,24,19,18


性质1证明:任何一条查找失败路径上黑结点数量都相同,而路径上不能连续出现两个红结点,即红结点只能穿插在各个黑结点中间
性质2证明: 若红黑树总高度 =h 则根节点黑高 \(\geq h / 2\)n , 因此内部结点数\(n\geq 2^{h / 2}-1\), 由此推出 $ h\leq 2 \log _{2}(n+1)$

结点的黑高 bh一一从某结点出发(不含该结点)到达任一叶结点的路径上黑结点总数
思考:根节点黑高为h的红黑树,内部结点数(关键字)至多有多少个?
回答:内部结点数最多的情况一一h层黑结点,每一层黑结点下面都铺满一层红结点。共2h层的满树形态
结论:若根节点黑高为h,内部结点数(关键字)最多有\(2^{2h}-1\)个
红黑树的删除

B树
m叉查找树:
-
实际上就是二叉查找树的拓展,结点多少有多少个分叉就是多少叉查找树
-
每个结点关键字的查找可以用顺序查找也可以用折半查找

如何保证查找效率:
-
若每个结点内关键字太少,导致树变高,要查更多层结点,效率低
-
策略:m叉查找树中,规定
除了根结点外,任何结点至少有⌈m/2⌉个分叉,即至少含有⌈m/2⌉ − 1 个关键字 -
为什么不能保证根结点至少有⌈m/2⌉个分叉:如果整个树只有1个元素,根结点只能有两个分叉
-
-
不够“平衡”,树会很高,要查很多层结点
- 策略:m叉查找树中,规定对于
任何一个结点,其所有子树的高度都要相同。
- 策略:m叉查找树中,规定对于
B树的定义

B树,又称多路平衡查找树,B树中所有结点的孩子个数的最大值称为B树的阶,通常用m表示。一棵m阶B树或为空树,或为满足如下特性的m叉树:
-
树中每个结点至多有m棵子树,即至多含有m-1个关键字。
-
若根结点不是终端结点,则至少有两棵子树。
-
除根结点外的所有非叶结点至少有⌈m/2⌉棵子树,即至少含有⌈m/2⌉-1个关键字。 -
所有非叶结点的结构如下:
-

-
其中,Ki(i = 1, 2,…, n)为结点的关键字,且满足
K1 < K2 <…< Kn;Pi(i = 0, 1,…, n)为指向子树根结点的指针,且指针Pi-1所指子树中所有结点的关键字均小于Ki,Pi所指子树中所有结点的关键字均大于Ki,n(⌈m/2⌉- 1≤n≤m - 1)为结点中关键字的个数。
-
-
所有的叶结点都出现在同一层次上,并且不带信息(可以视为外部结点或类似于折半查找判定树的查找失败结点,实际上这些结点不存在,指向这些结点的指针为空)。
m阶B树核心特性
- 根节点的子树数 \({\in[2, \mathrm{m}]}\), 关键字数 \({\in[1, m-1]}\) 。 其他结点的子树数 \({\in[[m / 2], \mathrm{m}]}\); 关键字数 \({\in[[m / 2]-1, \mathrm{m}-1]}\) (尽可能“满”)
- 对任一结点, 其所有子树高度都相同 (尽可能“平衡”)
- 关键字的值: 子树 \({0<}\) 关键字 \({1<子}\) 树 \({1<}\) 关键字 \({2<子}\) 树 \({2<\ldots}\) (类比二叉查找树 左<中<右)
B树的高度

B树的插入删除
B树的插入:
-
新元素一定是插入到最底层“终端结点”,用“查找”来确定插入位置
-
在插入key后,若导致原结点关键字数超过上限,则从中间位置(⌈m/2⌉)将其中的关键字分为两部分
-
左部分包含的关键字放在原结点中,右部分包含的关键字放到新结点中,中间位置(⌈m/2⌉)的结点插入原结点的父结点
-
若此时导致其父结点的关键字个数也超过了上限,则继续进行这种分裂操作,直至这个过程传到根结点为止,进而导致B树高度增1。
B树的删除
-
若被删除关键字在终端结点,则直接删除该关键字(要注意结点关键字个数是否低于下限 ⌈m/2⌉ − 1)
-
若被删除关键字在非终端结点,则用直接前驱或直接后继来替代被删除的关键字
-
直接前驱:当前关键字左侧指针所指子树中“最右下”的元素
-
直接后继:当前关键字右侧指针所指子树中“最左下”的元素
-
-
若被删除关键字所在结点删除前的关键字个数低于下限,且与此结点右(或左)兄弟结点的关键字个数还很宽裕,则需要调整该结点、右(或左)兄弟结点及其双亲结点(父子换位法)
-
当右兄弟很宽裕时,用当前结点的后继(比当前结点大一位)、后继的后继(比当前结点大两位)来填补空缺
-
当左兄弟很宽裕时,用当前结点的前驱(比当前结点小一位)、前驱的前驱(比当前结点小两位)来填补空缺
-
-
若被删除关键字所在结点删除前的关键字个数低于下限,且此时与该结点相邻的左、右兄弟结点的关键字个数均=⌈m/2⌉ − 1,则将关键字删除后与左(或右)兄弟结点及双亲结点中的关键字进行合并

B+树

定义和性质
一棵m阶的B+树需满足下列条件:
-
每个分支结点最多有m棵子树(孩子结点)。
-
非叶根结点至少有两棵子树,其他每个分支结点至少有⌈m/2⌉棵子树。
- (可以理解为:要追求“绝对平衡”,即所有子树高度要相同)
-
结点的子树个数与关键字个数相等。 -
所有
叶结点包含全部关键字及指向相应记录的指针,叶结点中将关键字按大小顺序排列,并且相邻叶结点按大小顺序相互链接起来(支持顺序查找)。 -
所有
分支结点中仅包含它的各个子结点中关键字的最大值及指向其子结点的指针。
B+树的查找:
B+树中,无论查找成功与否,最终一定都要走到最下面一层结点,因为只有叶子结点才有关键字对应的记录
B+树和B树区别:
| B树 | B+树 |
|---|---|
![]() |
![]() |
-
典型应用:关系型数据库的“索引”(如MySQL)
-
在B+树中,
非叶结点不含有该关键字对应记录的存储地址。 -
可以使一个磁盘块可以包含更多个关键字,使得B+树的阶更大,
树高更矮,读磁盘次数更少,查找更快

散列(哈希)查找

定义
-
散列表(Hash Table),又称哈希表 -
是一种数据结构,特点是:数据元素的关键字与其存储地址直接相关
-
通过哈希函数建立关键字和存储地址的联系
-
若不同的关键字通过散列函数映射到同一个值,则称它们为“
同义词” -
通过散列函数确定的位置已经存放了其他元素,则称这种情况为“
冲突”
处理冲突的方法——拉链法

| 查找27 | 查找20 |
|---|---|
![]() |
![]() |
-
用
拉链法(又称链接法、链地址法)处理“冲突”:把所有“同义词”存储在一个链表中 -
最理想情况:散列查找时间复杂度可到达\(O(1)\)
-
“冲突”越多,查找效率越低
-
实际上就是顺序表和链表的结合结合两者优点,比如顺序表的随机存取,然后用链表的解决冲突问题,又规避了顺序表的一系列缺点
-
在插入新元素时,保持关键字有序,可微微提高查找效率
常见的散列函数
散列函数的设计目的:
-
让不同关键字的冲突尽可能的少
-
散列查找是典型的“用空间换时间”的算法,只要散列函数设计的合理,则散列表越长,冲突的概率越低。
除留余数法:\(H(key) = key % p\)
- 散列表表长为m,取一个不大于m但最接近或等于m的质数p
- 质数又称素数。指除了1和此整数自身外,不能被其他自然数整除的数

直接定址法 : H(key) = key 或 H(key) = a*key + b
- 其中,a和b是常数。这种方法计算最简单,且不会产生冲突。它适合关键字的分布基本连续的情况,若关键字分布不连续,空位较多,则会造成存储空间的浪费。

数字分析法:选取数码分布较为均匀的若干位作为散列地址
-
设关键字是r进制数(如十进制数),而r个数码在各位上出现的频率不一定相同,可能在某些位上分布均匀一些,每种数码出现的机会均等;
-
而在某些位上分布不均匀,只有某几种数码经常出现,此时可选取数码分布较为均匀的若干位作为散列地址。这种方法适合于已知的关键字集合,若更换了关键字,则需要重新构造新的散列函数。

平方取中法:取关键字的平方值的中间几位作为散列地址。
- 具体取多少位要视实际情况而定。
- 这种方法得到的散列地址与关键字的每位都有关系,因此使得散列地址分布比较均匀,适用于关键字的每位取值都不够均匀或均小于散列地址所需的位数。
-
\[\begin{array}{l} 1310^{2}=1,716,100 \\ 1110^{2}=1,232,100 \\ 1300^{2}=1,690,000 \\ 1210^{2}=1,464,100 \\ 1200^{2}=1,440,000 \end{array} \]


处理冲突的方法——开放定址法

线性探测法
-
di = 0, 1, 2, 3, …, m-1;即发生冲突时,每次往后探测相邻的下一个单元是否为空,为空则可以插入数值
-
查找也是类似,先通过公式得到Hi再依次往后查找,若遇到空的位置则说明查找失败,所以越早遇到空位置,就可以越早确定查找失败
-
删除结点不能简单地将被删结点的空间置为空,否则将截断在它之后填入散列表的同义词结点的查找路径,可以做一个“删除标记”,进行逻辑删除
-
线性探测法由于冲突后再探测一定是放在某个连续的位置,很容易造成同义词、非同义词的“聚集(堆积)”现象,严重影响查找效率
平方探测法
-
当\(di = 0^2, 1^2, -1^2, 2^2, -2^2, …, k^2, -k^2\)时,称为平方探测法,又称
二次探测法,其中\(k≤m/2\) -
比起线性探测法更不易产生“聚集(堆积)”问题
-
注意:负数的模运算,(-3)%27 = 24,而不是3
-
模运算的规则:
a MOD m == (a+km) MOD m, 其中,k为任意整数 -
散列表长度m必须是一个可以表示成4j + 3的素数,才能探测到所有位置
伪随机序列法
- \(di\) 是一个伪随机序列,如 \(di= 0, 5, 24, 11, …\)
处理冲突的方法——再散列法:
-
除了原始的散列函数 H(key) 之外,多准备几个散列函数
-
当散列函数冲突时,用下一个散列函数计算一个新地址,直到不冲突为止:
-
\(Hi = RHi(Key)\)
-
\(i=1,2,3….,k\)

排序
基本概念
定义
-
排序(Sort),就是重新排列表中的元素,使表中的元素满足按关键字有序的过程。 -
输入:n个记录\(R_1, R_2,…, R_n\),对应的关键字为\(k_1, k_2,…, k_n\)。
-
输出:输入序列的一个重排\(R_1ʹ, R_2ʹ,…, R_nʹ\),使得有\(k_1ʹ≤k_2ʹ≤…≤k_nʹ\)(也可递减)

排序算法的评价指标:
-
时间复杂度
-
空间复杂度
-
算法的稳定性

排序算法的分类:
-
内部排序:数据都在内存中,
关注如何使算法时、空复杂度更低 -
外部排序:数据太多,无法全部放入内存,
还要关注如何使读/写磁盘次数更少


https://www.cs.usfca.edu/~galles/visualization/Algorithms.html
插入排序
算法思想

每次将一个待排序的记录按其关键字大小插入到前面已排好序的子序列中,直到全部记录插入完成。
代码实现
| 不带哨兵 | 带哨兵 |
|---|---|
![]() |
![]() |
算法效率分析:
-
空间复杂度:\(O(1)\)
-
时间复杂度:主要来自对比关键字、移动元素,若有 n 个元素,则需要 n-1 趟处理
-
最好时间复杂度:\(O(n)\) 共n-1趟处理,每一趟只需要对比关键字1次,不用移动元素
-
最坏时间复杂度:\(O(n^2)\) 共n-1趟处理,每一趟都需要从尾移到到头(全部逆序)
-
-
算法稳定性:
稳定
优化——折半插入排序
思路
-
先用折半查找找到应该插入的位置,再移动元素
-
当 low>high 时折半查找停止,应将 [low, i-1] 内的元素全部右移,并将 A[0] 复制到 low 所指位置
-
当 A[mid]==A[0] 时,为了保证算法的“稳定性”,应继续在 mid 所指位置右边寻找插入位置
代码实现

比起“直接插入排序”,比较关键字的次数减少了,但是移动元素的次数没变,整体来看时间复杂度依然是\(O(n^2)\)
对链表进行插入排序
-
使用链表不需要对序列进行依次右移,移动元素的次数变少了
-
但是关键字对比的次数依然是\(O(n^2)\) 数量级,整体来看时间复杂度依然是\(O(n^2)\)

希尔排序
算法思想

-
先追求表中元素部分有序,再逐渐逼近全局有序
-
先将待排序表分割成若干形如 \(L[i, i + d, i + 2d,…, i + kd]\) 的“特殊”子表,对各个子表分别进行直接插入排序。缩小
增量d,重复上述过程,直到d=1为止。

代码实现

算法性能分析

空间复杂度:O(1)
时间复杂度:
-
和增量序列 \(d_1, d_2, d_3…\) 的选择有关,目前无法用数学手段证明确切的时间复杂度
-
最坏时间复杂度为 \(O(n^2)\),当n在某个范围内时,可达\(O(n^{1.3})\)
稳定性:不稳定
适用性:仅适用于顺序表,不适用于链表
冒泡排序
算法思想

-
从后往前(或从前往后)两两比较相邻元素的值,若为逆序(即A[i-1]>A[i]),则交换它们,直到序列比较完
-
这样过程称为“
一趟”冒泡排序 -
第n趟结束后,最小的n个元素会“冒”到最前边
-
若某一趟排序没有发生“交换”,说明此时已经整体有序。
-
可以从后往前冒泡,也可以从前往后冒泡
代码实现

算法性能分析
-
空间复杂度:\(O(1)\)
-
时间复杂度:
-
最好情况(有序):\(O(n)\)
-
最坏情况(逆序):\(O(n^2)\)
-
平均情况:\(O(n^2)\)
-
-
稳定性:稳定
-
适用性:顺序表、链表都可以

快速排序
算法思想

-
在待排序表L[1…n]中任取一个元素pivot作为枢轴(或基准,通常取⾸元素)
-
通过一趟排序将待排序表划分为独立的两部分L[1…k-1]和L[k+1…n]
-
使得L[1…k-1]中的所有元素小于pivot,L[k+1…n]中的所有元素大于等于pivot
-
则
pivot放在了其最终位置L(k)上,这个过程称为一次“划分” -
然后分别递归地对两个子表重复上述过程,直至每部分内只有一个元素或空为止,即所有元素放在了其最终位置上
代码实现

算法性能分析

n个结点的二叉树
-
最小高度$ = ⌊log_2n⌋ + 1$
-
最大高度$ = n$
时间复杂度\(=O(n*递归层数)\)
-
最好时间复杂度\(=O(nlog_2n)\)- 每次选的枢轴元素都能将序列划分成均匀的两部分
-
最坏时间复杂度\(=O(n^2)\) -
若序列原本就有序或逆序,则时、空复杂度最高(可优化,尽量选择可以把数据中分的枢轴元素。)
空间复杂度=O(递归层数)
-
最好空间复杂度\(=O(log_2n)\)
-
最坏空间复杂度\(=O(n)\)
若每一次选中的“枢轴”将待排序序列划分为很不均匀的两个部分,则会导致递归深度增加,算法效率变低
若初始序列有序或逆序,则快速排序的性能最差(因为每次选择的都是最靠边的元素)
快速排序算法优化思路:尽量选择可以把数据中分的枢轴元素。
-
选头、中、尾三个位置的元素,取中间值作为枢轴元素;
-
随机选一个元素作为枢轴元素
快速排序是所有内部排序算法中平均性能最优的排序算法
稳定性:不稳定

简单选择排序
算法思想
每一趟在待排序元素中选取关键字最小(或最大)的元素(每一趟待排序序列长度-1)加入有序子序列(每一趟有序序列长度+1)

代码实现

算法性能分析
-
无论有序、逆序、还是乱序,一定需要 \(n-1\) 趟处理
-
总共需要对比关键字\((n-1)+(n-2)+\ldots+1=\frac{n(n-1)}{2}\)次
-
元素交换次数 \(< n-1\)
-
空间复杂度:\(O(1)\)
-
时间复杂度\(=O(n^2)\)
-
稳定性:不稳定
-
适用性:既可以用于顺序表,也可用于链表

堆排序


堆的定义
若n个关键字序列L[1…n] 满足下面某一条性质,则称为堆(Heap):
-
若满足:\(L(i)≥L(2i)\)且$L(i)≥L(2i+1) $$(1 ≤ i ≤n/2 )$则为
大根堆(大顶堆)- 即完全二叉树中,任意根≥左、右
-
若满足:\(L(i)≤L(2i)\)且\(L(i)≤L(2i+1)\) \((1 ≤ i ≤n/2 )\)则为
小根堆(小顶堆)- 即完全二叉树中,任意根≤左、右

建立大根堆
把所有非终端结点都检查一遍,是否满足大根堆的要求,如果不满足,则进行调整
-
在顺序存储的完全二叉树中,非终端结点编号 i≤⌊n/2⌋
-
检查当前结点是否满足:
根\(≥\)左、右,若不满足,将当前结点与更大的一个孩子互换-
i的左孩子:\(2i\)
-
i的右孩子:\(2i+1\)
-
i的父结点:\([i/2]\)
-
-
若元素互换破坏了下一级的堆,则采用相同的方法继续往下调整(小元素不断“下坠”)
建立大根堆(代码):

基于大根堆进行排序
选择排序:每一趟在待排序元素中选取关键字最大的元素加入有序子序列
堆排序每一趟完成以下工作:
-
将堆顶元素(就是最大的元素)加入有序子序列(与待排序序列中的最后一个元素交换)
-
并将
待排序元素序列再次调整为大根堆(小元素不断“下坠”)
注意:大根堆获得的排序序列是递增序列,小跟堆相反,获得的是递减序列
代码实现(大根堆排序)
堆排序的效率分析
-
建堆的过程,关键字对比次数不超过4n,建堆时间复杂度=O(n)
-
堆排序的时间复杂度 \(=O(n) + O(nlog_2n) = O(nlog_2n)\)
-
堆排序的空间复杂度 \(=O(1)\)
-
稳定性:不稳定

堆的插入删除
基本操作
-
i的左孩子:2i
-
i的右孩子:2i+1
-
i的父结点:[i/2]
在堆中插入新元素
-
对于小根堆,新元素放到表尾,与父节点对比,若新元素比父节点更小,则将二者互换。
-
新元素就这样一路“上升”,直到无法继续上升为止
在堆中删除元素
-
被删除的元素用堆底元素替代
-
然后让该元素不断“下坠”,直到无法下坠为止
关键字对比次数
-
每次“上升”调整只需对比关键字1次
-
每次“下坠”调整可能需要对比关键字2次,也可能只需对比1次

归并排序
算法思想


-
把两个或多个已经有序的序列合并成一个
-
对于两个有序序列,将i、j指针指向序列的表头,选择更小的一个放入k所指的位置
-
k++,i/j指向更小元素的指针++
-
只剩一个子表未合并时,可以将该表的剩余元素全部加到总表
-
m路归并:每选出一个小的元素,需要对比关键字m-1次
-
核心操作:把数组内的两个有序序列归并为一个
代码实现

-
low是数组中第一个有序序列的开始
-
mid是数组中第一个有序序列的结尾
-
high是数组中第二个有序序列的结尾
-
辅助数组B临时存放这两段有序序列
算法效率分析:
-
2路归并的“归并树”形态上就是一棵倒立的二叉树
-
二叉树的第h层最多有\(2 ^ {h-1}\)个结点,若树高为h,则应满足\(n <= 2 ^ {h-1}\),即\(h − 1 = ⌈log_2n⌉\)
-
n个元素进行2路归并排序,归并趟数\(=⌈log_2n⌉\)
-
每趟归并时间复杂度为\(O(n)\),则算法时间复杂度\(O(nlog_2n)\)
-
空间复杂度\(=O(n)\),来自于辅助数组B
-
稳定性:稳定
基数排序
算法思想


基数排序得到递增序列的过程如下:
-
初始化: 设置 r 个空队列,\(Q_0, Q_1,…, Q_{r−1}\)
-
按照各个 关键字位 权重递增的次序(个、十、百),对 d 个关键字位分别做“分配”和“收集”
-
分配:顺序扫描各个元素,若当前处理的关键字位=x,则将元素插入 \(Q_x\) 队尾
-
收集:把 \(Q_0, Q_1,…, Q_{r−1}\) 各个队列中的结点依次出队并链接


算法效率分析

- 稳定性:稳定
基数排序的应用

基数排序擅长解决的问题:
-
数据元素的关键字可以方便地拆分为 d 组,且 d 较小
-
每组关键字的取值范围不大,即 r 较小
-
数据元素个数 n 较大

外部排序
外存、内存的数据交换
外存:
-
操作系统以“块”为单位对磁盘存储空间进行管理
-
如:每块大小1KB各个磁盘块内存放着各种各样的数据
内存:
-
磁盘的读/写以“
块”为单位 -
数据读入内存后才能被修改
-
修改完了还要写回磁盘

外部排序原理
-
数据元素太多,无法一次全部读入内存进行排序
-
使用“归并排序”的方法,最少只需在内存或只能怪分配3块大小的缓冲区即可对任意一个大文件进行排序
-
”归并排序“要求各个子序列有序,每次读入两个块的内容,进行内部排序后写回磁盘
构造初始“归并段”:

若有N个记录,内存工作区可以容纳L个记录,则初始归并段数量=r=N/L
第一趟归并:

第二趟归并:

第三趟归并:

时间开销分析

外部排序时间开销=读写外存的时间+内部排序所需时间+内部归并所需时间
优化思路
多路归并

采用多路归并可以减少归并趟数,从而减少磁盘I/O(读写)次数(重要结论)

多路归并带来的负面影响:
-
k路归并时,需要开辟k个输入缓冲区,内存开销增加 -
每挑选一个关键字需要对比关键字(
k-1)次,内部归并所需时间增加(可使用败者树优化)
减少初始归并段数量

- 生成初始归并段的“内存工作区”越大,初始归并段越长

败者树
问题引入(多路平衡带来的问题)

定义
-
可视为一棵完全二叉树(多了一个头头)
-
k个叶结点分别是当前参加比较的元素,非叶子结点用来记忆左右子树中的“失败者”,而让胜者往上继续进行比较,一直到根结点。
-
即失败者留在这一回合,胜利者进入下一回合比拼

败者树在多路平衡归并中的应用
![]() |
![]() |
败者树的存储结构

对于k路归并,第一次构造败者树需要对比关键字\(k-1\)次
有了败者树,选出最小元素,只需对比关键字\([log_2k]\)次
置换-选择排序

使用置换-选择排序,可以让每个初始归并段的长度超过内存工作区大小的限制


设初始待排文件为FI,初始归并段输出文件为FO,内存工作区为WA,FO和WA的初始状态为空,WA可容纳w个记录。
置换-选择算法的步骤如下
-
从FI输入w个记录到工作区WA。
-
从WA中选出其中关键字取最小值的记录,记为MINIMAX记录。
-
将MINIMAX记录输出到FO中去。
-
若FI不空,则从FI输入下一个记录到WA中。
-
从WA中所有关键字比MINIMAX记录的关键字大的记录中选出最小关键字记录,作为新的MINIMAX记录。
-
重复3.~5.,直至在WA中选不出新的MINIMAX记录为止,由此得到一个初始归并段,输出一个归并段的结束标志到FO中去。
-
重复2.~6.,直至WA为空。由此得到全部初始归并段。
最佳归并树
| 归并树神秘性质 | |
|---|---|
![]() |
![]() |
-
归并过程中
磁盘I/O次数=归并树的WPL*2(重要结论) -
因此要让磁盘I/O次数最小,就要使归并树的WPL最小即构建一个
哈夫曼树
m叉最佳归并树的构造
| 2路归并最佳归并树 | 3路归并最佳归并树 |
|---|---|
![]() |
![]() |

-
注意:对于k叉归并,若初始归并段的数量无法构成严格的k叉归并树
-
则需要补充几个长度为0的“虚段”,再进行k叉哈夫曼树的构造

增加虚段的数量


总结
| 排序方法 | 时间复杂度(平均) |
时间复杂度(最坏) |
时间复杂度(最好) |
空间复杂度 |
稳定性 |
|---|---|---|---|---|---|
| 直接插入排序 | \(O(n^2)\) | \(O(n^2)\) | \(O(n)\) | \(O(1)\) | 稳定 |
| 折半插人排序 | \(O(nlog_2n)\) | \(O(n^2)\) | \(O(n^2)\) | \(O(1)\) | 稳定 |
| 希尔排序 | \(O(n^{1.3})\) | \(O(n)\) | \(O(n)\) | \(O(1)\) | 不稳定 |
| 选择排序 | \(O(n^2)\) | \(O(n^2)\) | \(O(n^2)\) | \(O(1)\) | 不稳定 |
| 冒泡排序 | \(O(n^2)\) | \(O(n)\) | \(O(n)\) | \(O(1)\) | 稳定 |
| 快速排序 | \(O(nlog_2n)\) | \(O(n^2)\) | \(O(nlog_2n)\) | \(O(nlog_2n)\) | 不稳定 |
| 堆排序 | \(O(nlog_2n)\) | \(O(nlog_2n)\) | \(O(nlog_2n)\) | \(O(1)\) | 不稳定 |
| 归并排序 | \(O(nlog_2n)\) | \(O(nlog_2n)\) | \(O(nlog_2n)\) | \(O(n)\) | 稳定 |
| 基数排序 | \(O(d(n + rd))\) | \(O(d(n + rd))\) | \(O(d(n+ rd))\) | \(O(n + rd)\) | 稳定 |










1



















浙公网安备 33010602011771号