输入:
n
n
n 个记录
R
1
,
R
2
,
⋯
,
R
n
R_1,R_2, \cdots ,R_n
R1,R2,⋯,Rn,对应的关键字为
k
1
,
k
2
,
⋯
,
k
n
k_1, k_2, \cdots ,k_n
k1,k2,⋯,kn
输出:输入序列的一个重排
R
1
′
,
R
2
′
,
⋯
,
R
n
′
R_1',R_2', \cdots ,R_n'
R1′,R2′,⋯,Rn′,使得
k
1
′
≤
k
2
′
≤
⋯
≤
k
n
′
k_1' \leq k_2' \leq \cdots \leq k_n'
k1′≤k2′≤⋯≤kn′(其中 “
≤
\leq
≤” 可以换成其他的比较大小的符号)
算法的稳定性
若待排序表中有两个元素
R
i
R_i
Ri 和
R
j
R_j
Rj,其对应的关键字相同即
k
e
y
i
=
k
e
y
j
key_i = key_j
keyi=keyj,且在排序前
R
i
R_i
Ri 在
R
j
R_j
Rj 的前面,若使用某一排序算法排序后,
R
i
R_i
Ri 仍然在
R
j
R_j
Rj 的前面,则称这个排序算法是稳定的,否则称排序算法是不稳定的
算法是否具有稳定性并不能衡量一个算法的优劣,它主要是对算法的性质进行描述
如果待排序表中的关键字不允许重复,则排序结果是唯一的,那么选择排序算法时的稳定与否就无关紧要
在排序过程中,根据数据元素是否完全在内存中,可将排序算法分为两类:
① 内部排序,是指在排序期间元素全部存放在内存中的排序
② 外部排序,是指在排序期间元素无法全部同时存放在内存中,必须在排序的过程中根据要求不断地在内、外存之间移动的排序
时间效率:折半插入排序仅减少了比较元素的次数,约为
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2n),该比较次数与待排序表的初始状态无关,仅取决于表中的元素个数
n
n
n;而元素的移动次数并未改变,它依赖于待排序表的初始状态。因此,折半查找排序的时间复杂度仍为
O
(
n
2
)
O(n^2)
O(n2),但对于数据量不很大的排序表,折半插入排序往往能表现出很好的性能
稳定性:折半插入排序是一种稳定的排序方法
适用性:折半插入排序因为使用了折半查找,所以仅适用于顺序存储的线性表
8.2.3 希尔排序
从前面的分析可知,直接插入排序算法的时间复杂度为
O
(
n
2
)
O(n^2)
O(n2),但若待排序列为“正序”时,期时间复杂度可提高至
O
(
n
)
O(n)
O(n),由此可见它更适用于基本有序的排序表和数据量不打的排序表。希尔排序正是基于这两点分析对直接插入排序进行改进而得来的,又称缩小增量排序
希尔排序的基本思想是:
先将待排序表分割成若干刑辱
L
[
i
,
i
+
d
,
i
+
2
d
,
⋯
,
i
+
k
d
]
L[i, i+d, i+2d, \cdots , i+kd]
L[i,i+d,i+2d,⋯,i+kd] 的“特殊”子表,即把像个某个“增量”的记录组成一个子表,对各个子表分别进行直接插入排序,当整个表中的元素已呈“基本有序”时,再对全体记录进行一次直接插入排序
希尔排序的过程如下:
先取一个小于
n
n
n 的步长
d
1
d_1
d1,把表中的全部记录分成
d
1
d_1
d1 组,所有距离为
d
1
d1
d1 的倍数的记录放在同一组,在各组内进行直接插入排序
然后取第二个步长
d
2
<
d
1
d_2 < d_1
d2<d1,重复上述过程
直到所取得
d
t
=
1
d_t = 1
dt=1,即所有记录已放在同一组中,再进行直接插入排序,由于此时已经具有较好的局部有序性,故可以很快得到最终结果
到目前为止,尚未求得一个最好的增量序列,希尔提出的方法是
d
1
=
n
/
2
d_1 = n/2
d1=n/2,
d
i
+
1
=
⌊
d
i
/
2
⌋
d_{i+1} = \lfloor d_i/2 \rfloor
di+1=⌊di/2⌋,并且最后一个增量等于
1
1
1
时间效率:由于希尔排序的时间复杂度依赖于增量序列的函数,这涉及数学上尚未解决的难题,所以其时间复杂度分析比较困难。当
n
n
n 在某个特定范围时,希尔排序的时间复杂度约为
O
(
n
1.3
)
O(n^{1.3})
O(n1.3)。在最坏情况下,希尔排序的时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)
voidBubbleSort(ElemType A[],int n){for(int i =0; i < n-1; i++){
bool flag = false;//表示本趟冒泡是否发生交换的标志for(j = n -1; j > i; j--){//一趟冒泡过程if(A[j-1]> A[j]){//若为逆序swap(A[j-1], A[j]);//交换
flag = true;}}if(flag == false)return;//本趟遍历后没有发生交换,说明表已经有序}}
冒泡排序的性能分析如下:
空间效率:仅使用了常数个辅助单元,因而空间复杂度为
O
(
1
)
O(1)
O(1)
时间效率:
当初始序列有序时,显然第一趟冒泡后 flag依然为false(本趟冒泡没有元素交换),从而直接跳出循环,比较次数为
n
−
1
n-1
n−1,移动次数为
0
0
0,从而最好情况下的时间复杂度为
O
(
n
)
O(n)
O(n)
当初始序列为逆序时,需要进行
n
−
1
n-1
n−1 趟排序,第
i
i
i 趟排序要进行
n
−
i
n-i
n−i 次关键字的比较,而且每次比较前后都必须移动元素
3
3
3 次来交换元素位置。这种情况下,比较次数
=
∑
i
=
1
n
−
1
=
n
(
n
−
1
)
2
= \sum _{i=1}^{n-1} = \frac{n(n-1)}{2}
=∑i=1n−1=2n(n−1),移动次数
=
∑
i
=
1
n
−
1
3
(
n
−
i
)
=
3
n
(
n
−
1
)
2
= \sum_{i=1}^{n-1}3(n-i) = \frac{3n(n-1)}{2}
=∑i=1n−13(n−i)=23n(n−1),从而,最坏情况下的时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)
在待排序的
L
[
1...
n
]
L[1...n]
L[1...n] 中任取一个元素
p
i
v
o
t
pivot
pivot 作为枢轴(或基准,通常取首元素),通过一趟排序将待排序表划分为独立的两部分
L
[
1..
k
−
1
]
L[1..k-1]
L[1..k−1] 和
L
[
k
+
1...
n
]
L[k+1...n]
L[k+1...n],使得
L
[
1..
k
−
1
]
L[1..k-1]
L[1..k−1] 中的所有元素小于
p
i
v
o
t
pivot
pivot,
L
[
k
+
1...
n
]
L[k+1...n]
L[k+1...n] 中的所有元素大于等于
p
i
v
o
t
pivot
pivot,则
p
i
v
o
t
pivot
pivot 放在了其最终位置
L
(
k
)
L(k)
L(k) 上,这个过程称为一趟快速排序(或一次划分)
在理想状态下,即
P
a
r
t
i
t
i
o
n
(
)
Partition()
Partition() 可能做到最平衡的划分,得到的两个子问题的大小都不能可能大于
n
/
2
n/2
n/2,在这种情况下,快速排序的运行速度将大大提升,此时,最好情况下的时间复杂度为
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2n)
快速排序平均情况下的运行时间与其最爱情况下的运行时间很接近,因此,平均情况下的时间复杂度为
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2n)
每一趟(如第
i
i
i 趟)在后面
n
−
i
+
1
(
i
=
1
,
2
,
⋯
,
n
−
1
)
n-i+1(i=1,2,\cdots ,n-1)
n−i+1(i=1,2,⋯,n−1) 个待排序元素中选取关键字最小的元素,作为有序子序列的第
i
i
i 个元素,直到第
n
−
1
n-1
n−1 趟做完,带排序元素只剩下
1
1
1 个,就不用再选了
一趟排序会将一个元素放置在最终位置上
8.4.1 简单选择排序
简单选择排序算法的思想:
假设排序表为
L
[
1...
n
]
L[1...n]
L[1...n],第
i
i
i 趟排序即从
L
[
1...
n
]
L[1...n]
L[1...n] 中选择关键字最小的元素与
L
(
i
)
L(i)
L(i) 交换,没一趟排序可以确定一个元素的最总位置,这样经过
n
−
1
n-1
n−1 趟排序就可使得整个排序表有序
voidSelectSort(ElemType A[],int n){for(int i =0; i < n -1; i++){//一共进行n-1趟int minpos = i;//记录最小元素位置for(int j = i +1; j < n -1; j++){//在A[i...n]中选择最小的元素if(A[j]< A[minpos])
minpos = j;//更新最小元素位置}if(minpos != i)swap(A[i], A[minpos]);//封装的swap()函数共移动元素3次}}
简单选择排序算法的性能分析如下:
空间效率:仅使用常数个辅助单元,故空间复杂度为
O
(
1
)
O(1)
O(1)
时间效率:在简单选择排序过程中,元素移动的操作次数很少,不会超过
3
(
n
−
1
)
3(n-1)
3(n−1) 次,最好情况是移动
0
0
0 次,此时对应的表已经有序;但元素间比较的次数与序列的初始状态无关,始终是
n
(
(
n
−
1
)
2
n((n-1)2
n((n−1)2 次,因此时间复杂度始终是
O
(
n
2
)
O(n^2)
O(n2)
稳定性:在第
i
i
i 趟找到最小元素后,和第
i
i
i 个元素交换,可能会导致第
i
i
i 个元素与其含有相同关键字的元素的相对位置发生改变,因此,简单选择排序是一种不稳定的排序方法
适用性:适用于顺序存储和链式存储的线性表
8.4.2 堆排序
堆的定义如下:
n
n
n 个关键字序列
L
[
1...
n
]
L[1...n]
L[1...n] 称为堆,当且仅当该序列满足:
①
L
(
i
)
≥
L
(
2
i
)
L(i) \geq L(2i)
L(i)≥L(2i) 且
L
(
i
)
≥
L
(
2
i
+
1
)
L(i) \geq L(2i+1)
L(i)≥L(2i+1) 或
②
L
(
i
)
≤
L
(
2
i
)
L(i) \leq L(2i)
L(i)≤L(2i) 且
L
(
i
)
≤
L
(
2
i
+
1
)
L(i) \leq L(2i+1)
L(i)≤L(2i+1)
(
1
≤
i
≤
⌊
n
/
2
⌋
)
(1 \leq i \leq \lfloor n/2 \rfloor)
(1≤i≤⌊n/2⌋)
n
n
n 个结点的完全二叉树,最后一个结点是第
⌊
n
/
2
⌋
\lfloor n/2 \rfloor
⌊n/2⌋ 个结点的孩子。对第
⌊
n
/
2
⌋
\lfloor n/2 \rfloor
⌊n/2⌋ 个结点为根的子树进行筛选(对于大根堆,若根结点的关键字小于左右孩子中关键字较大者,则交换),使其子树称为堆
voidBuildMaxHeap(ElemType A[],int len){for(int i = len /2; i >0; i--)//从i=[n/2]~1,反复调整堆HeadAdjust(A, i, len);}voidHeadAdjust(ElemType A[],int k,int len){//函数HeadAjust将元素k为根的子树进行调整
A[0]= A[k];//A[0]暂存子树的根结点for(int i =2* k; i <= len; i *=2){//沿key较大的子节点向下筛选if(i < len && A[i]< A[i+1])
i++;//取key较大的子结点的下标if(A[0]>= A[i])break;//筛选结束else{
A[k]= A[i];//将A[i]调整到双亲结点上
k = i;//修改k值,以便继续向下筛选}}
A[k]= A[0];//被筛选结点的值放入最终位置}
调整的时间与树高有关,为
O
(
h
)
O(h)
O(h);在建含有
n
n
n 个元素的堆时,关键字的比较总次数不超过
4
n
4n
4n,时间复杂度为
O
(
n
)
O(n)
O(n),这说明可以在线性时间内将一个无序数组简称一个堆
设
h
=
h
i
g
h
−
l
o
w
+
1
h = high-low+1
h=high−low+1,则
M
e
r
g
e
(
)
Merge()
Merge() 算法的时间复杂度为
O
(
h
)
O(h)
O(h)
一趟归并排序的操作是,调用
⌈
n
/
2
h
⌉
\lceil n/2h \rceil
⌈n/2h⌉ 次算法
M
e
r
g
e
(
)
Merge()
Merge(),将
L
[
1...
n
]
L[1...n]
L[1...n] 中前后相邻且长度为
h
h
h 的有序段进行两两归并,得到前后相邻、长度为
2
h
2h
2h 的有序段,整个归并排序需要进行
⌈
l
o
g
2
n
⌉
\lceil log_2n \rceil
⌈log2n⌉ 趟
递归形式的 2 路归并排序算法是基于分治的,其过程如下:
分解:将含有
n
n
n 个元素的待排序表分成各含
n
/
2
n/2
n/2 元素的子表,采用 2 路归并排序算法对两个子表递归地进行排序
空间效率:
M
e
r
g
e
(
)
Merge()
Merge() 操作中,辅助空间刚好为
n
n
n 个单元,所以算法的空间复杂度为 O(n)
时间效率:每趟归并的时间复杂度为
O
(
n
)
O(n)
O(n),共需进行
⌈
l
o
g
2
n
⌉
\lceil log_2n \rceil
⌈log2n⌉ 趟归并,所以算法的时间复杂度为
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2n)
稳定性:由于
M
e
r
g
e
(
)
Merge()
Merge() 操作不会改变相同关键字记录的相对次序,所以 2 路归并排序算法是一种稳定的算法
适用性:适用于顺序存储和链式存储的线性表
注意:
一般而言,对于
N
N
N 个元素进行
k
k
k 路归并排序时,排序的趟数
m
m
m 满足
k
m
=
N
k^m=N
km=N,从而
m
=
l
o
g
k
N
m=log_kN
m=logkN,又考虑到
m
m
m 为整数,所以
m
=
⌈
l
o
g
k
N
⌉
m=\lceil log_kN \rceil
m=⌈logkN⌉,这与前面的 2 路归并排序时一致的
8.5.2 基数排序
基数排序是一种特别的排序方法,它不基于比较和移动进行排序,而是基于关键字各位的大小进行排序
基数排序是一种借助多关键字排序的思想对单逻辑关键字进行排序的方法
假设长度为
n
n
n 的线性表中每个结点
a
j
a_j
aj 的关注兼职由
d
d
d 元组(
k
j
d
−
1
,
k
j
d
−
2
,
⋯
,
k
j
1
,
k
j
0
k_j^{d-1}, k_j^{d-2}, \cdots , k_j^1, k_j^0
kjd−1,kjd−2,⋯,kj1,kj0)组成,满足
0
≤
k
j
i
≤
r
−
1
(
0
≤
j
<
n
,
0
≤
i
≤
d
−
1
)
0 \leq k_j^i \leq r-1(0 \leq j <n, 0 \leq i \leq d-1)
0≤kji≤r−1(0≤j<n,0≤i≤d−1),其中
k
j
d
−
1
k_j^{d-1}
kjd−1 为最主位关键字,
k
j
0
k_j^0
kj0 为最次位关键字
① 若 n 较小,可采用直接插入排序或简单选择排序。由于直接插入排序所需的记录移动次数较简单选择排序的多,因而当记录本身信息量较大时,用简单选择排序较好
② 若文件的初始状态已按关键字基本有序,则选用直接插入或冒泡排序为宜
③ 若 n 较大,则应采用时间复杂度为
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2n) 的排序算法:快速排序、堆排序或归并排序。快速排序被认为是目前基于比较的内部排序算法中最好的算法,当待排序的关键字随机分布时,快速排序的平均时间最短。堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。这两种排序都是不稳定的,若要求排序稳定且时间复杂度为
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2n),则可选用归并排序。但从单个记录起进行的两两归并的排序算法并不值得提倡,通常可以将它和直接插入排序结合在一起使用。先利用直接插入排序求得较长的有序子文件,然后两两归并。直接插入排序时稳定的,因此改进后的归并排序仍是稳定的
④ 在基于比较的排序方法中,每次比较两个关键字的大小之后,仅出现两种可能的转移,因此可以用一棵二叉树来描述比较判定过程,由此可以证明:当文件的 n 个关键字随机分布时,任何借助于“比较”的内部排序算法,至少需要
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2n) 的时间
t
E
S
=
r
∗
t
I
S
+
d
∗
t
I
O
+
S
(
n
−
1
)
t
m
g
t_{ES} = r * t_{IS} + d* t_{IO} + S(n-1)t_{mg}
tES=r∗tIS+d∗tIO+S(n−1)tmg
其中,
t
E
S
t_{ES}
tES 为外部排序时间,
r
r
r 为初始划分的归并段的个数,
t
I
S
t_{IS}
tIS 为每个归并段的内部排序时间,
d
d
d 为进行磁盘 I/O 的次数,
t
I
O
t_{IO}
tIO 为一个归并段一次读/写的时间,
S
S
S 内部归并比较的趟数,
n
−
1
n-1
n−1 为每趟需要比较的次数,
t
m
g
t_{mg}
tmg 为一个记录取得一个最小关键字所需要的时间
例:20000 个记录,初始归并段 5000 个记录,则
t
E
S
=
4
∗
t
I
S
+
3
∗
(
4
+
4
)
∗
t
I
O
+
2
∗
20000
∗
t
m
g
t_{ES} = 4*t_{IS} + 3*(4+4)*t_{IO}+2*20000*t_{mg}
tES=4∗tIS+3∗(4+4)∗tIO+2∗20000∗tmg
显然,外存信息读写的时间远大于内部排序和内部归并的时间,因此应着力减少 I/O 次数
一般地,对
r
r
r 个初始归并段,做
k
k
k 路平衡归并,归并树可用严格
k
k
k 叉树(即只有度为
k
k
k 与度为
0
0
0 的结点的
k
k
k 叉树)来表示。第一趟可将
r
r
r 个出使归并段归并为
⌈
r
/
k
⌉
\lceil r/k \rceil
⌈r/k⌉ 个归并段,以后每
m
m
m 趟归并将
m
m
m 个归并段归并成
⌈
m
/
k
⌉
\lceil m/k \rceil
⌈m/k⌉ 个归并段,直至最后形成一个大的归并段为止。树的高度
=
⌈
l
o
g
k
r
⌉
=
= \lceil log_kr \rceil =
=⌈logkr⌉= 归并趟数
S
S
S。可见,只要增大归并路数
k
k
k,或减少初始归并段个数
r
r
r 都能减少归并趟数
S
S
S,进而减少读写磁盘的次数,达到提高外部排序速度的目的
8.7.3 多路平衡归并与败者树
从8.7.2的讨论可知,增加归并路数
k
k
k 能减少归并趟数
S
S
S,进而减少 I/O 次数。然而,增加归并路数
k
k
k 时,内部归并的渐渐将增加。做内部归并时,在
k
k
k 个元素中选择关键字最小的记录需要比较
k
−
1
k-1
k−1 次。每趟归并
n
n
n 个元素需要做
(
n
−
1
)
(
k
−
1
)
(n-1)(k-1)
(n−1)(k−1) 次比较,
S
S
S 趟归并总共需要的比较次数为
S
(
n
−
1
)
(
k
−
1
)
=
⌈
l
o
g
k
r
⌉
(
n
−
1
)
(
k
−
1
)
=
⌈
l
o
g
2
r
⌉
(
n
−
1
)
(
k
−
1
)
/
⌈
l
o
g
2
k
⌉
S(n-1)(k-1) = \lceil log_kr \rceil (n-1)(k-1) = \lceil log_2r \rceil (n-1)(k-1) / \lceil log_2k \rceil
S(n−1)(k−1)=⌈logkr⌉(n−1)(k−1)=⌈log2r⌉(n−1)(k−1)/⌈log2k⌉
式中,
(
k
−
1
)
/
⌈
l
o
g
2
k
⌉
(k-1) / \lceil log_2k \rceil
(k−1)/⌈log2k⌉ 随
k
k
k 增长而曾昭,因此内部归并时间亦随
k
k
k 的增长而增长。这将抵消由于增大
k
k
k 而减少外存访问次数所得到的效益。因此,不能使用普通的内部归并排序算法
为了使内部归并并不受
k
k
k 的增大的影响,引入了败者树:
败者树是树形选择排序的一种变体,可视为一棵完全二叉树
k
k
k 个叶结点分别存放
k
k
k 个归并段在归并过程中当前参加比较的记录,内部结点用来记忆左右子树中的“失败者”,而让胜者继续往上进行比较,一直到根结点
若比较两个数,大的为失败者、小的为胜利者,则根结点指向的树为最小数
因为
k
k
k 路归并的败者树深度为
⌈
l
o
g
2
k
⌉
\lceil log_2k \rceil
⌈log2k⌉,因此
k
k
k 个记录中选择最小的关键字,最多需要
⌈
l
o
g
2
k
⌉
\lceil log_2k \rceil
⌈log2k⌉ 次比较。所以总的比较次数为
S
(
n
−
1
)
⌈
l
o
g
2
k
⌉
=
⌈
l
o
g
k
r
⌉
(
n
−
1
)
⌈
l
o
g
2
k
⌉
=
(
n
−
1
)
⌈
l
o
g
2
r
⌉
S(n-1)\lceil log_2k \rceil = \lceil log_kr \rceil (n-1) \lceil log_2k \rceil = (n-1) \lceil log_2r \rceil
S(n−1)⌈log2k⌉=⌈logkr⌉(n−1)⌈log2k⌉=(n−1)⌈log2r⌉
可见,使用败者树后,内部归并的比较次数与
k
k
k 无关了。因此,只要内存空间允许,增大归并路数
k
k
k 将有效地减少归并书的高度,从而减少 I/O 次数,提高外部排序的速度
值得说明的是,归并路数
k
k
k 并不是越大越好。归并路数
k
k
k 增大时,相应地需要增加输入缓冲区的个数。若可供使用的内部空间不变,势必要减少每个输入缓冲区的容量,使得内存、外存交换数据的次数增大。当
k
k
k 值过大时,虽然归并趟数会减少,但读写外存的次数仍会增加
8.7.4 置换-选择排序(生成初始归并段)
从8.7.2的讨论可知,减少初始归并段个数
r
r
r 也可以减少归并趟数
S
S
S。若总的记录个数为
n
n
n,每个归并段的长度为
l
l
l,则归并段的个数
r
=
⌈
n
/
l
⌉
r = \lceil n/l \rceil
r=⌈n/l⌉。采用内部排序方法得到的各个初始归并段长度都相同(除最后一段外),它依赖于内部排序书可用内存工作区的大小。因此,必须探索新的方法,来产生更长的初始归并段,这就是这里要介绍的置换-选择算法
设初始待排文件为 FI,初始归并段输出文件为 FO,内存工作区为 WA,FO 和 WA 的初始状态为空,WA 可容纳
w
w
w 个记录。置换-选择算法步骤如下:
1)从 FI 输入
w
w
w 个记录到工作区 WA
2)从 WA 中选出其中关键字取最小的记录,记为 MINIMAX 记录
3)将 MINIMAX 记录输出到 FO 中去
4)若 FI 不空,则从 FI 输入下一个记录到 WA 中
5)从 WA 中所有关键字比 MINIMAX 记录的关键字大的记录中选出最小关键字记录,作为新的 MINIMAX 记录
6)重复 3)~5),直至在 WA 中选不出新的 MINIMAX 记录为止,由此得到一个初始归并段,输出一个归并段的结束标志到 FO 中区
图中,各叶结点表示一个初始归并段,上面的权值表示该归并段的长度,叶结点到根的路径长度表示其参加归并的趟数,各非叶结点代表归并成的新归并段,根结点表示最终生成的归并段。树的带权路径长度
W
P
L
WPL
WPL 为归并过程中的总读记录,故 I/O 次数
=
2
×
W
P
L
=
484
= 2 \times WPL = 484
=2×WPL=484
显然,归并方案不同,所得归并树亦不同,输的带权路径长度(I/O 次数)亦不同。为了优化归并树的
W
P
L
WPL
WPL,可将哈弗曼树的思想推广到
m
m
m 叉树的情形,在归并树中,让记录数少的初始归并段最先归并,记录数多得初始归并段最晚归并,就可以建立总的 I/O 次数最少的最佳归并树
当叶子结点不够时,即初始归并段不足以构成一棵严格
k
k
k 叉树时,需添加长度为
0
0
0 的“虚段”,按照哈弗曼树的原则,权为
0
0
0 的叶子应离树根最远
如何判断添加虚段的数目?——设度为
0
0
0 的结点有
n
0
n_0
n0 个,度为
k
k
k 的结点有
n
k
n_k
nk 个,则对严格
k
k
k 叉树有
n
0
=
(
k
−
1
)
n
k
+
1
n_0 = (k-1)n_k + 1
n0=(k−1)nk+1,由此可得
n
k
=
(
n
0
−
1
)
/
(
k
−
1
)
n_k = (n_0-1)/(k-1)
nk=(n0−1)/(k−1)
若
(
n
0
−
1
)
%
(
k
−
1
)
=
0
(n_0-1) \% (k-1) = 0
(n0−1)%(k−1)=0,则说明这
n
0
n_0
n0 个叶结点(初始归并段)正好可以构造
k
k
k 叉归并树。此时,内结点有
n
k
n_k
nk 个
若
(
n
0
−
1
)
%
(
k
−
1
)
=
u
≠
0
(n_0-1) \% (k-1) = u \neq 0
(n0−1)%(k−1)=u=0,则说明对于这
n
0
n_0
n0 个叶结点,其中有
u
u
u 个多余,不能包含在
k
k
k 叉归并树中。为构造包含所有
n
0
n_0
n0 个初始归并段的
k
k
k 叉归并树,应在原有
n
k
n_k
nk 个内结点的基础上再增加
1
1
1 个内结点。它在归并树中代替了一个叶结点的位置,被代替的叶结点加上刚才多出的
u
u
u 个叶结点,即再加上
k
−
u
−
1
k-u-1
k−u−1 个空归并段,就可以建立归并树