(变长数据项的排序)
a. 给定一个整数数组,其中不同的整数所包含的数字的位数可能不同,但该数组中,所有整数中包含的总数字位数为
n
n
n。设计一个算法,使其可以在
O
(
n
)
O(n)
O(n)时间内对该数组进行排序。
b. 给定一个字符串数组,其中不同的字符串所包含的字符数可能不同,但所有字符串中的总字符个数为
n
n
n。设计一个算法,使其可以在
O
(
n
)
O(n)
O(n)时间内对该数组进行排序。(注意:此处的顺度是指标准的字典序,例如
a
<
a
b
<
b
a < ab < b
a<ab<b。)
解
a.
假设数组一共有
m
m
m个元素,各元素的位数分别为
d
1
,
d
2
,
…
,
d
m
d_1, d_2, …, d_m
d1,d2,…,dm,显然有
∑
i
=
1
m
d
i
=
n
\sum_{i=1}^m{d_i} =n
∑i=1mdi=n。如果采用基数排序,运行时间为
O
(
d
m
a
x
m
)
O(d_{max}m)
O(dmaxm),其中
d
m
a
x
=
max
1
≤
i
≤
m
d
i
d_{max}=\max_{1≤i≤m}{d_i}
dmax=max1≤i≤mdi,这与题目要求的
O
(
n
)
O(n)
O(n)运行时间不符。例如,假设元素个数
m
=
n
/
2
+
1
m = n/2 + 1
m=n/2+1,有一个元素的位数为
n
/
2
n/2
n/2,其他
n
/
2
n/2
n/2个元素的位数都为
1
1
1,这种情况下基数排序的运行时间为
O
(
n
2
)
O(n^2)
O(n2)。
为了可以在
O
(
n
)
O(n)
O(n)时间内完成排序,考虑先按照元素的位数对元素进行分组,相同位数的元素分为一组,然后采用基数排序分别对每组元素进行排序,然后按照位数从小到大依次输出每组元素即可完成排序。
该算法的时间复杂度取决于两部分,一部分是分组的时间,一部分是对各组元素进行排序的时间。分组的时间取决于确定各元素的位数所花费的时间,对于一个有
d
i
d_i
di位的元素,需要
Θ
(
d
i
)
Θ(d_i)
Θ(di)时间来确定它的位数。于是,确定所有元素的位数所花费的总时间为
∑
i
=
1
m
Θ
(
d
i
)
=
Θ
(
n
)
\sum_{i=1}^m{Θ(d_i)}=Θ(n)
∑i=1mΘ(di)=Θ(n)。对各组元素的排序采用基数排序。对于第
j
j
j组来说,假设其中有
m
j
m_j
mj个元素,该组元素都包含
j
j
j个数位,基数排序的时间为
Θ
(
j
•
m
j
)
Θ(j•m_j)
Θ(j•mj)。于是,对各组元素进行基数排序的总时间为
∑
j
=
1
n
Θ
(
j
∙
m
j
)
=
Θ
(
n
)
\sum_{j=1}^n{Θ(j∙m_j)}=Θ(n)
∑j=1nΘ(j∙mj)=Θ(n)。根据以上分析,该算法的时间复杂度为
Θ
(
n
)
Θ(n)
Θ(n)。
b.
同样考虑借用基数排序的思想。在对数字的基数排序中,排序从低位开始,再到高位。与此不同,在对字符串的基数排序中,排序先从最左边的字符开始,再到右边的字符。下面给出一个例子。
注意,第一轮排序对所有字符串的最左边的字符进行排序。在第一轮排序过后,所有字符串按照最左边的字符进行分组(如上图所示,第一轮排序过后,at和a为一组,box和bat为一组,fix、fox和fit为一组,I为一组)。因为要求按字典序来排序,所以可以断言,在最终排好序的序列中,第一组字符串肯定排在第二组字符串之前,第二组字符串也肯定排在第三组字符串之前……。于是在第二轮排序中,分别对第一轮分组后的各组元素进行组内排序,而不改变组与组之间的顺序。第二轮排序之后,又可以按各字符串的第二个字符进行分组,第三轮排序又针对第二轮的分组进行组内排序,如此递归下去,最后可以完成整个字符串序列的排序。
假如所有字符串都是C风格字符串(即字符串以’\0’结尾),假设第
i
i
i个字符串的长度为
l
i
l_i
li,那么在第
l
i
+
1
l_i+1
li+1轮排序中,该字符串以’\0’参与排序,它一定会排在所在分组的前列,因为字符’\0’的值小于其他任何字符的值,而所有长度大于
l
i
l_i
li的字符串都排在这个长度为
l
i
l_i
li字符串的后面,这符合字典序。所以在第
l
i
+
2
l_i+2
li+2轮排序中,这个长度为
l
i
l_i
li字符串字符串可以不用参与排序,只需要让所有长度大于
l
i
l_i
li的字符串参与排序。
根据以上分析,一个长度为
l
i
l_i
li的字符串会参与
l
i
+
1
l_i+1
li+1轮排序。如果每一轮排序都采用线性时间的计数排序,那么一个长度为
l
i
l_i
li的字符串在每一轮排序中贡献
O
(
1
)
O(1)
O(1)时间,在所有
l
i
+
1
l_i+1
li+1轮排序中贡献
O
(
l
i
+
1
)
O(l_i+1)
O(li+1)时间。于是,对所有字符串进行排序的总时间为
∑
i
=
1
m
O
(
l
i
+
1
)
=
O
(
∑
i
=
1
m
(
l
i
+
1
)
)
=
O
(
n
+
m
)
=
O
(
n
)
\sum\limits_{i=1}^m{O(l_i+1)} =O(\sum\limits_{i=1}^m{(l_i+1)})=O(n+m)=O(n)
i=1∑mO(li+1)=O(i=1∑m(li+1))=O(n+m)=O(n)
在上式中,
m
m
m为字符串的个数。假如没有空字符串,即所有字符串的长度至少为
1
1
1,则肯定有
m
≤
n
m ≤ n
m≤n,所以在上式中
O
(
n
+
m
)
=
O
(
n
)
O(n+m) = O(n)
O(n+m)=O(n)是成立的。
下面组出该算法的伪代码。
要对整个字符串数组
A
A
A进行排序,只需要调用
S
O
R
T
−
S
T
R
I
N
G
(
A
,
1
,
A
.
l
e
n
g
t
h
,
1
)
{\rm SORT-STRING}(A, 1, A.length, 1)
SORT−STRING(A,1,A.length,1)即可。
代码链接
https://github.com/yangtzhou2012/Introduction_to_Algorithms_3rd/tree/master/Chapter08/Problem_8-3