8.1 排序算法的下界
8.1-1
在一棵比较排序算法的决策树中,一个叶结点可能的最小深度是n。
8.1-2
易得:。
- 证明
:
。
- 证明
:
。
因此:。所以,
的渐进紧确界为
。
8.1-3
证明:对于某种输入,如果存在能达到线性运行时间的比较排序算法,则它对应于决策树中的叶结点的深度为n,这样的叶结点的数目不多于。假设对n!种长度为n的输入中的至少一半,存在能达到线性运行时间的比较排序算法,则有
,这只对很小的n才成立。如果只要求对1/n的输入达到线性时间,则有
,这也只对很小的n才成立。
时则有
,这同样只对很小的n才成立。
8.1-4
有一个包含n个元素的待排序序列,该序列有n/k个子序列组成,每个子序列包含k个元素,因此每个子序列有k!种可能的排列,整个序列有种可能的排列。考虑一棵高度为h、具有l个可达叶结点的决策树,它对应一个对此序列所做的比较排序。因为输入数据的
种可能的排列都是叶结点,所以有
。由于在一棵高为h的二叉树中,叶结点的数目不多于
,得到:
。所以,这个排序问题中所需比较次数的下界是
。
8.2 计数排序
8.2-1
COUNTING-SORT在数组A=<6,0,2,0,1,3,4,6,1,3,2>上的操作过程。
8.2-2
证明:因为提供临时存储空间的数组C中存储的是每类元素在排好序的最终数组中的最大下标,所以在COUNTING-SORT的最后一个循环中,同一类元素在存放排序输出的数组B中,是从大下标往小下标填入的。又因为最后一个循环是逆序遍历输入数组A,即从大下标往小下标遍历输入数组A,与往存放排序输出的数组B中填入元素的方式相同,所以在输入数组A中相同的多个元素,有更大下标的元素在存放排序输出的数组B中也有更大的下标。因此,COUNTING-SORT是稳定的。
8.2-3
证明:在COUNTING-SORT的第10行循环的开始部分,将代码改写为:
for j = 1 to A.length
因为提供临时存储空间的数组C中存储的是每类元素在排好序的最终数组中的最大下标,所以在遍历输入数组A时,每个元素都能在存放排序输出的数组B中找到正确的填入位置。因此,该算法仍然是正确的。但它不再稳定了。
8.2-4
设计一个算法,它能够对于任何给定的介于0到k之间的n个整数先进行预处理,然后在时间内回答输入的n个整数中有多少个落在区间[a..b]内。设计的算法的预处理时间为
。
PREPROCESS(A, n, k)
let B[0..k] be a new array
for i = 0 to k
B[i] = 0
for j = 1 to A.length
B[A[j]] = B[A[j]] + 1
// B[i] now contains the number of elements equal to i.
for i = 1 to k
B[i] = B[i] + B[i-1]
// B[i] now contains the number of elements less than or equal to i.
return B
COUNT(A, n, k, a, b)
B = PREPROCESS(A, n, k)
return B[b] - B[a-1]
8.3 基数排序
8.3-1
RADIX-SORT在下列英文单词上的操作过程:COW,DOG,SEA,RUG,ROW,MOB,BOX,TAB,BAR,EAR,TAR,DIG,BIG,TEA,NOW,FOX。
8.3-2
稳定的排序算法:插入排序、归并排序;不稳定的排序算法:堆排序、快速排序。能使任何排序算法都稳定的方法:给每个元素新增一个数据项存放元素在输入数组中的下标,并在最后根据此数据项对相同的元素进行最终排序。此方法的额外时间开销是,额外空间开销是
。
8.3-3
证明:
- 当数组中的元素只有1位时,使用了一次排序算法后得到的数组即是有序的。因此,基数排序是正确的。
- 假设当数组中的元素有k位时基数排序是正确的,现在考虑数组中的元素有k+1位时的情况。基数排序在运行了k轮排序算法的输出结果上,再运行了1轮排序算法。此时数组中的元素在最高位是有序的,当元素的最高位相同时,因为所用的底层排序算法是稳定的,所以在运行了k轮排序算法的输出结果中排在更前面的元素,再运行了1轮排序算法后仍然排在更前面,也即得到的数组是有序的。因此,基数排序是正确的。
综上,基数排序是正确的。在证明中,在归纳假设的第二步需要假设所用的底层排序算法是稳定的。
8.3-4
在时间内,对0到
区间内的n个整数进行排序:
- 把这n个整数全部转换成n进制;
- 用基数排序对这些n进制的n个整数进行排序。
在0到区间内的n个整数转换成n进制后最多有3位,根据引理8.3,可以在
时间内将这些数排好序。
8.4 桶排序
8.4-1
BUCKET-SORT在数组A=<0.79,0.13,0.16,0.64,0.39,0.20,0.89,0.53,0.71,0.42>上的操作过程:
8.4-2
在最坏情况下,桶排序的n个输入数都被放到1个桶中,在第8行对此桶中的元素用插入排序进行排序的时间代价是。除第8行以外,所有其他各行时间代价都是
。因此,桶排序在最坏情况下运行时间是
。
通过把桶排序第8行中对各个桶中的元素进行排序的排序算法改成归并排序或者堆排序,可以使其在保持平均情况为线性时间代价的同时,最坏情况下时间代价为。
8.4-3
0 | 1 | 2 | |
0 | 1 | 4 | |
1/4 | 1/2 | 1/4 |
,
。
思考题
8-1 比较排序的概率下界
a.证明:对于一个确定的比较排序算法A,其决策树为,
的每个叶结点都对应着给定的随机输入情况经过特定的比较操作序列最终有序的过程。每种随机输入情况都只对应着一种比较操作序列,同时每种比较操作序列也只对应着一种随机输入情况,因为A的输入总共有n!种排列情况,所以
中有n!个结点是可达的,又因为每一种排列情况都是等可能的,所以到达这n!个结点中的任何一个的概率都是1/n!。因此,假设
的每个叶结点都标有在给定的随机输入情况下到达该结点的概率,恰有n!个叶结点标有1/n!,其他的叶结点标记为0。
b.证明:因为LT和RT的叶结点的个数和等于T的叶结点个数k,又因为LT和RT的每个叶结点的深度都比该叶结点在T中的深度少1,所以。
c.证明:定义d(k)为所有具有k>1个叶结点的决策树T的最小D(T)值。考虑一棵能够取得该最小值的、有k个叶结点的决策树T。设是LT中的叶结点数,
是RT中的叶结点数,根据b小问,
。因此,
。
d.证明:
- 设
,则
,易得
在
内单调递增。当
时,
,
单调递减;当
时,
,
单调递增。因此,
在
处取得最小值,即函数
在
处取得最小值。
- 对某个恰当选出的常数c>0,假定
对所有正数m<n都成立,特别是对于
,有
,
。将其代入递归式,得到
。其中,只要
,最后一步都会成立。
e.证明:
- 因为决策树
的叶结点数大于等于n!,所以
。
- 比较排序算法A的输入有n!种排列情况,在平均情况下,排序n个元素的时间代价为
。
f.证明:对任何随机化比较排序算法B,其在排序过程中,在所有“随机化”结点处选择子结点的结果,最后都会与普通的决策树中某一条到达叶结点的路径相对应,也即与一个确定的比较排序算法A相对应。因为A的决策路径与B相同,所以A期望的比较次数不多于B的比较次数。
8-2 线性时间原址排序
假设有一个包含n个待排序数据记录的数组,且每条记录的关键字的值为0或1。对这样一组记录进行排序的算法可能具备如下三种特性中的一部分:
- 算法的时间代价是
。
- 算法是稳定的。
- 算法是原址排序,除了输入数组之外,算法只需要固定的额外存储空间。
a.一个满足上述条件1和条件2的算法。
LINEAR-STABLE-SORT(A, n)
let B[1..n] be a new array
for i = 1 to n
if A[i].key == 0
B[i] = A[i]
for i = 1 to n
if A[i].key == 1
B[i] = A[i]
return B
b.一个满足上述条件1和条件3的算法。
LINEAR-IN-PLACE-SORT(A, n)
index = 1
for i = 1 to n
if A[i].key == 0
SWAP(A[i], A[index])
index = index + 1
c.一个满足上述条件2和条件3的算法。
STABLE-IN-PLACE(A, n)
zero_begin = 0
one_begin = 0
one_length = 0
for i = 1 to n
if A[i].key == 0
if one_length != 0
index = one_begin
time = 1
while time ≤ one_length
SWAP(A[i], A[index])
index = index + 1
time = time + 1
one_begin = one_begin + 1
else
if one_begin != 0
one_begin = i
one_length = one_length + 1
d.设计的算法(a)~(c)中的(a)可以用于RADIX-SORT的第2行作为基础排序方法,从而使RADIX-SORT在排序有b位关键字的n条记录时的时间代价是。只需从最低位到最高位依次调用算法(a)进行排序即可。
e.假设有n条记录,其中所有关键字的值都在1到k的区间内。修改计数排序,使得它可以在时间内完成对n条记录的原址排序。除输入数组外,可以
使用大小的额外存储空间。
COUNTING-SORT'(A, B, k)
let C[1..k] be a new array
for i = 1 to k
C[i] = 0
for j = 1 to A.length
C[A[j]] = C[A[j]] + 1
// C[i] now contains the number of elements equal to i.
for i = 2 to k
C[i] = C[i] + C[i-1]
// C[i] now contains the number of elements less than or equal to i.
for j = A.length downto 1
B[C[A[j]]] = A[j]
C[A[j]] = C[A[j]] - 1
该算法是稳定的。
8-3 变长数据项的排序
a.给定一个整数数组,其中不同的整数所包含的数字的位数可能不同,但该数组中,所有整数中包含的总数字位数为n。设计一个算法,使其可以在时间内对该数组进行排序。
SORT-ITEMS-BY-LENGTH(A, B)
min_length = +∞
max_length = -∞
for i = 1 to A.length
min_length = MIN(min_length, A[i].length)
max_length = MAX(max_length, A[i].length)
let C[0..max_length-min_length] be a new array
for i = 0 to max_length-min_length
C[i] = 0
for j = 1 to A.length
C[A[j].length-min_length] = C[A[j].length-min_length] + 1
// C[i] now contains the number of elements whose length equal to i+min_length.
for i = 1 to max_length-min_length
C[i] = C[i] + C[i-1]
// C[i] now contains the number of elements whose length less than or equal to i+min_length.
groups = C
for j = A.length downto 1
B[C[A[j].length-min_length]] = A[j]
C[A[j].length-min_length] = C[A[j].length-min_length] - 1
return groups, min_length
GROUP-INTEGERS-SORT(A, begin, end, length)
if begin ≥ end
return
for i = 1 to length
use a stable sort to sort array A with subscripts between begin and end on digit i
VARIABLE-LENGTH-INTEGERS-SORT(A)
let B[1..A.length] be a new array
groups, min_length = SORT-ITEMS-BY-LENGTH(A, B)
k = groups.length - 1
groups[-1] = 0
for i = 0 to k
GROUP-ITEMS-SORT(B, groups[i-1]+1, groups[i], i+min_length)
b.给定一个字符串数组,其中不同的字符串所包含的字符数可能不同,但所有字符串中的总字符个数为n。设计一个算法,其可以在时间内对该数组进行排序。(注意:此处的顺序是指标准的字典序,例如a<ab<b。)
GROUP-STRINGS-SORT(A, begin, end, length)
if begin ≥ end
return
let B[0..end-begin] be a new array
for i = begin to end
B[i-begin] = A[i]
let C[1..52] be a new array
for i = 1 to 52
C[i] = 0
for j = 0 to end-begin
C[B[j][length]] = C[B[j][length]] + 1
// C[i] now contains the number of elements whose lengthth letter equal to i.
for i = 2 to 52
C[i] = C[i-1] + 1
// C[i] now contains the number of elements whose lengthth less than or equal to i.
for j = end-begin downto 0
A[C[B[j][length]]+begin-1] = B[j]
VARIABLE-LENGTH-STRINGS-SORT(A)
let B[1..A.length] be a new array
groups, min_length = SORT-ITEMS-BY-LENGTH(A, B)
k = groups.length - 1
groups[-1] = 0
for i = k to 0
GROUP-STRINGS-SORT(B, groups[i-1]+1, groups[k], i+min_length)
8-4 水壶
a.设计一个确定性算法,它能够用次比较来完成所有水壶的配对:
先挑出一个红色水壶,将其与所有蓝色水壶进行比较,找出与其对应的蓝色水壶。再挑出下一个红色水壶,将其与剩下蓝色水壶进行比较,找出与其对应的蓝色水壶。重复此步骤,直至所有水壶完成配对。
b.证明:在这一问题中,对于给定的形状和尺寸都各不相同的n个红色的水壶和n个蓝色的水壶,分析对它们进行配对的算法A,其决策树为。此决策树中每个结点都有三个子结点,分别对应一次比较的三种情况。假设此决策树高度为h、具有l个可达叶结点。因为两组水壶的
种可能的配对都是叶结点,所以有
。由于在一棵高为h的三叉树中,叶结点的数目不多于
,得到:
。因此,解决该问题算法的比较次数下界为
。
c.设计一个随机算法,其期望的比较次数为。
JUG-PARTITION(RED, BLUE, p, r)
i = RANDOM(p, r)
exchange RED[r] with RED[i]
for j = p to r
if BLUE[j] == RED[r]
break
exchange BLUE[r] with BLUE[j]
pivot = p
for j = p to r-1
if BLUE[j] < RED[r]
exchange BLUE[pivot] with BLUE[j]
pivot = pivot + 1
exchange BLUE[pivot] with BLUE[r]
temp = p
for j = p to r-1
if RED[j] < BLUE[pivot]
exchange RED[temp] with RED[j]
temp = temp + 1
exchange RED[temp] with RED[r]
return pivot
JUG-PAIR(RED, BLUE, p, r)
if p < r
q = JUG-PARTITION(RED, BLUE, p, r)
JUG-PAIR(RED, BLUE, p, q-1)
JUG-PAIR(RED, BLUE, q+1, r)
此算法的时间复杂度和快速排序类似,所以其期望的比较次数为。对此算法来说,最坏情况下的比较次数是
。
8-5 平均排序
a.一个数组是1排序的,说明这个数组是完全有序的。
b.对数字1,2,...,10的一个排列<1,3,2,4,6,5,7,9,8,10>,它是2排序的,但不是完全有序的。
c.证明:
- 充分性:当一个包含n个元素的数组对所有的i=1,2,...,n-k,有A[i]≤A[i+k],则有
,即这个数组是k排序的。
- 必要性:一个包含n个元素的数组是k排序的,则有
,即这个数组对所有的i=1,2,...,n-k,有A[i]≤A[i+k]。
因此,一个包含n个元素的数组是k排序的,当且仅当对所有的i=1,2,...,n-k,有A[i]≤A[i+k]。
d.设计一个算法,它能在时间内对一个包含n个元素的数组进行k排序。
K-SORT(A, n, k)
for i = 1 to k
length = ⌊(n-i)/k⌋ + 1
let B[1..length] be a new array
for j = 1 to length
B[j] = A[i + (j-1)*k]
QUICKSORT(B, 1, length)
for j = 1 to length
A[i + (j-1)*k] = B[j]
当k是一个常数时,也可以给出k排序算法的下界。
e.证明:因为在一个长度为n的k排序数组中,对所有的i=1,2,...,n-k,有A[i]≤A[i+k],所以可以将数组中的元素分成k组,每组元素都是完全有序的,根据练习6.5-9的结果,可以在的时间内对其进行全排序。
f.证明:对包含n个元素的数组进行k排序可以划分成对k个小数组进行全排序,每个小数组全排序需要的时间,k个小数组全排序需要
的时间。当k是一个常数时,可得对此数组进行k排序需要
的时间。
8-6 合并有序列表的下界
a.给定2n个数,共有种可能的方式将它们划分成两个有序的列表,其中每个列表都包含n个数。
b.证明:考虑一棵高度为h、具有l个可达叶结点的决策树,它对应一个将2n个数划分成两个有序的列表所做的比较。因为2n个数的种可能的划分方式都是叶结点,所以有
。由于在一棵高度为h的二叉树中,叶结点的数目不多于
,得到:
。根据C.1-13的结论,
,则有
。对该式两边取对数,有
。因此,任何能够正确合并两个有序列表的算法都至少要进行2n-o(n)次比较。
c.如果两个元素在有序序列中是连续的,且它们分别来自不同的列表,如果它们不进行比较,就不知道哪个元素应该排在前面。因此,它们必须进行比较。
d.根据上一部分的答案,如果两个元素在有序序列中是连续的,且它们分别来自不同的列表,则它们必须进行比较。在一个包含2n个元素的数组中,总共含有2n-1个相邻对,当这些相邻对都来自不同的列表时,比较次数达到下界。因此,合并两个有序列表时的比较次数下界为2n-1。
8-7 0-1排序引理和列排序
a.因为A[p]≤A[p],所以B[p]=0。因为A[q]>A[p],所以B[q]=1。
b.证明:因为A[p]是算法X未能将其放到正确位置的最小的元素,而A[q]是被算法X放在A[p]原本应该在的位置上的元素。可见A[p]和A[q]都被算法X放错位置了,原本应该在A[q]的位置上的元素是被放错的最小的元素,所以q<p。根据上一部分的结论,B[p]=0和B[q]=1。因此,算法X不能对数组B正确地排序。
c.因为列排序只按照事先定义好的操作执行,不用管奇数步对每一列单独排序的算法,因此,可以把列排序看做一种遗忘比较交换算法。
d.证明:
- 经过第1步之后,在数组的每一列中,0都在顶部,1都在底部。
- 经过第2步之后,之前数组中的每一列都转换成了r/s行的各个区域,得到的数组的每一行都是全部有序的,所以如果某一行是0结尾的,则这行只包含全0,如果某一行是1开头的,则这行只包含全1。在每个r/s行的区域中,如果某一行是0开头的,则这行之前的所有行只包含全0,如果某一行是1结尾的,则这行之后的所有行只包含全1,所以每个r/s行的区域中最多只有一行包含的是0和1的混合。因为数组有s列,所以总共能得到s个r/s行的区域,最多有s行包含的是0和1的混合。
- 经过第3步后,数组的每一列都是全部有序的。
因此,经过第1~3步,数组由三部分组成,顶部一些由全0组成的干净行,底部一些由全1组成的干净行,以及中间最多s行脏的行。
e.证明:
- 根据上一部分的结论,经过第1~3步,数组由三部分组成:顶部一些由全0组成的干净行,底部一些由全1组成的干净行,以及中间最多s行脏的行。
- 经过第4步之后,之前顶部那些由全0组成的干净行转换成了前端的若干列,之前底部那些由全1组成的干净行转换成了后端的若干列,之前中间最多s行脏的行转换成了中间的1或2列。因为之前中间最多有s行脏的行,也即最多有
个元素组成的脏的区域。
因此,经过第4步之后,如果按照列优先原则读取数组,先读到的是全0的干净区域,最后是全1的干净区域,中间是由最多个元素组成的脏的区域。
f.证明:
- 根据上一部分的结论,经过第4步之后,如果按照列优先原则读取数组,先读到的是全0的干净区域,最后是全1的干净区域,中间是由最多个
元素组成的脏的区域。因为
,所以脏的区域最多存在两列中。
- 经过第5步之后,中间最多两列的脏的区域全部有序,且因为
,如果脏的区域有两列,第一列的上半部分只包含全0,第二列的下半部分只包含全1;如果脏的区域只有一列,此列或者上半部分只包含全0,或者下半部分只包含全1,即有半列是干净的,半列是脏的,此时已经产生一个全排序的0~1输出。
- 经过第6步之后,如果之前脏的区域有两列,此时脏的区域被合并到一列中;如果之前脏的区域只有一列,因为之前此列中只有半列是脏的,此时脏的区域仍然只有一列。
- 经过第7步之后,一列脏的区域全部有序。
- 经过第8步之后,对数组中元素的顺序并不产生影响。
因此,第5~8步产生一个全排序的0~1输出。根据0-1排序引理,列排序可以正确地对任意输入值排序。
g.证明:经过第1~2步之后,除了第d小问分析得到的每个行的区域有的一行脏行,在每两个
行的区域相交的那一行也是脏行,这样脏行总共有s-1行。因此,经过第1~3步,数组的顶部有一些全0的干净行,底部有一些全1的干净行,中间是最多2s-1行脏行。
与s相比,在s不能被r整除时,才能保证列排序的正确性。
h.对第1步做一个简单修改:对奇数列进行升序排序,对偶数列进行降序排序。这使得可以在s不能被r整除且时,也保证列排序仍然正确。
证明:对第1步做了修改后,经过第1~2步之后,除了第d小问分析得到的每个行的区域有的一行脏行,在每两个
行的区域相交的那一行不再是脏行。所以,经过第1~3步,数组由三部分组成,顶部一些由全0组成的干净行,底部一些由全1组成的干净行,以及中间最多s行脏行。再根据第e和第f小问的证明,可以得到最终会产生一个全排序的0~1输出。因此,在这一修改后,列排序仍然正确。