Introduction to Algorithms (third edition)
第八章:Sorting in Linear Time
之前介绍的插入排序、归并排序、堆排序和快速排序都是基于比较的排序方法,平均情况下的时间复杂度都是O(nlgn)(其中插入排序需要修改成二分查找而不是线性查找,但是这会影响到插入排序最优情况下的O(n)时间复杂度),本章首先证明了这也是基于比较的排序方法所能达到的最好下界。直观的解释就是考虑n个元素的比较决策树,由于n个元素的排列有n!种可能,因此决策树的深度为lg(n!) = O(nlgn),因此基于比较的排序算法平均情况下的时间复杂度下界是O(nlgn),所以在基于比较的大前提下,不用费尽心思去寻找O(n)的排序算法,否则就如同试着发明永动机一样。
但是不要悲观,对于一些特殊的输入数据,可以不用基于比较的排序方法,而取得线性的时间复杂度。比如,有n个数,每个数的取值范围是0~k,其中k = O(n),在这种情况下,可以使用计数排序,时间复杂度是O(n)。计数排序的基本思想就是顺序扫描数组,统计每个数的出现次数,然后计算有多少个数比某个数小,然后再直接输出排序结果。时间复杂度是O(n+k),由于k=O(n),因此时间复杂度即是O(n)。
有了计数排序,再深一步,就有了基数排序。比如要排序一些十进制表达的数组,每一位的数范围都是0~9,从低位到高位,对每位上实行0~9的计数排序,就是基数排序了。不妨设每一位的范围是0~k,有d位,总共有n个数,那么基数排序的时间复杂度就是O(d(n+k))。注意必须从低位开始排序,不能从高位开始,否则会打乱之前的高位排序,造成结果错误。
RADIX-SORT(A, d)
1 for i = 1 to d
2 use a stable sort to sort array A on digit i
最后介绍的桶排序,基于输入满足均匀分布的假设,比如n个满足[0, 1)上的均匀分布的数。不妨将[0, 1)区间均分成n份,[0, 1/n), [1/n, 2/n), ..., [(n-1)/n, 1),顺序扫描输入,将n个数加入到对应的桶中,这个步骤的时间复杂度是O(n),然后对每个桶中的数进行插入排序,最后逐个输出桶中的数即可。如果数据分布比较均匀,每个桶中的数都比较少,可以证明,每个桶中插入排序的期望时间复杂度是O(2 - 1/n),因此总的时间复杂度是O(n) + n*O(2 - 1/n) = O(n)
这一章介绍的排序方法都有非常优秀的时间复杂度,但是对输入有特殊要求,所以在通用性上不及之前介绍的基于比较的排序方法。但是,在某些特殊领域,或者在某些情况下,这几种排序方法还是大有用武之地的。
Exercises
8.1-1
n, when the array is in order and there is n-1 comparison
8.1-3
lg(n!/2) = lg(n!) - 1 = O(nlg(n))
lg(n!/n) = lg(n!) - lgn = O(nlg(n))
lg(n!/2^n) = lg(n!) - n = O(nlg(n))
8.1-4
for each of the n/k subsequences, needs lg(k!) = klg(k) comparisons, so the total lower bound of comparisons needed is n/k * klg(k) = nlg(k)
8.2-2
由于输出排序结果时,是从后往前扫描原数组的,因此原数组中相同的值能保持它们在输出数组中的相对顺序。如果输出数组时从前往后扫描原数组,那么排序就不稳定。
8.2-3
很显然仍能正常工作,但是不稳定。
8.2-4
顺序扫描数组,统计0~k中每个数出现的次数,记录在数组C中,然后叠加C中数组的值,使得C[i]表示原数组中小于等于i的数的个数,因此落入[a..b]中的值个数为C[b] - C[a-1],其中C[-1]=0
8.3-2
插入排序和归并排序是稳定的,堆排序和快排不稳定。为了使得所有排序都稳定,可以额外记录每个元素的在原数组中的位置做索引,比较时如果值相等再比较索引,由于有n个元素,每个元素的索引可以用lg(n)个bit表示,因此额外的空间是O(nlg(n))。但在时间复杂度的增加上,最坏情况下是每次比较都多一次索引的标记,因此时间复杂度不变。
8.3-3
如果排序不是稳定的,那么如果输入是123和133,排序最低两位时都正常,123在前,133在后,排序最高位时,可能输出123在前,也可能输出133在前,显然是错误的,因此基数排序所使用的排序算法必须是稳定的。
8.3-4
将数写成n进制表达,不妨记x的n进制表达为ab,那么x = a*n + b,其中a,b的取值范围都是0~n-1,很显然表达范围是0~n^3-1,因此只需要在两位上做计数排序,时间复杂度是O(n+n) = O(n)
8.4-2
最坏情况下,所有的n个数落入一个桶中,而插入排序的时间复杂度是O(n^2)。将插入排序改成归并、堆排序或者快速排序等时间复杂度是O(nlg(n))的排序方法即可。
8.4-3
E(X^2) = 0*1/4 + 1*1/2 + 4*1/4 = 1.5, E^2(X) = (0*1/4 + 1*1/2 + 2*1/4)^2 = 1
8.4-4
计算点与原点的距离,将(0, 1]平均分成n份,根据每个点与原点的距离放入对应的桶中,然后对每个桶中的点排序,最后输出结果即可。
8.4-5
能说得更明白点吗?不明白啥意思呢,不过我知道使用桶排序就是了……
Problems
8-2 Sorting in place in linear time
a. counting sort
b. use the partition algorithm from quicksort
c. insert sort
d. yes, use (a), it is counting sort
e. non-stable, after computing how many numbers of each 1~k value, just fill the original array the corresponding value, it's not stable, otherwise it will need O(n) additional storage
8-3 Sorting variable-length items
a. group integers by the number of digits they have, then for each group, use radix sort
b. group strings by their first character, and in each group, recursively do this for each group
8-4 Water jugs
a. for each red jug, compare it to every blue jug until finds its pair
b. for the first red jug, there may be n available blue jug; while for the second, there may be n-1;... so the total outcome number (leaf nodes) in decision tree is n!, then the height is lg(n!) = nlg(n)
c. randomly take a red jug, and compare it to every blue jug, divide them into three categories: { B<, B=, B>}; then take one from B=, compare it to every red jug, also divides them into three categories: {R<, R=, R>}, recursively do this on {R<, B<} and {R>, B>}.
worst case is that each time either B< or B> is empty, then the time is O(n^2)