13.3 随机排列
一些使用随机样本的程序要求样本的元素以随机的顺序出现。这样的序列被称为无重复的随机排列。例如,在测试一个排序程序的时候,随机产生的输入必须以随机的顺序出现;如果输入总是有序的,那么可能不能充分地测试排序代码。
我们可以利用Floyd算法F2产生一组随机样本,然后把它复制到一个数组中,最后打乱数组中元素的顺序。这段代码用于随机地打乱数组 的顺序:
- for I := M downto 2 do
- J := RandInt(1, I)
- Swap(X[J], X[I])
这个只有三个步骤的方法调用了RandInt函数2M次。
当本章原来在《ACM通讯》上发表后,几位读者发现上面的伪代码经过小的修改后,能够从1..N的整数中产生M元随机排列并放入X[1..M]中:
- for I := 1 to N do
- X[I] := I
- for I := 1 to M do
- J := RandInt(I, N)
- Swap(X[J], X[I])
这个算法很容易实现成代码,但是它需要O(N)的运行时间和O(N)的空间。下面我们会看到,Floyd的算法在N相对于M比较大的时候,相比之下会更有效率。
Floyd的随机排列产生器与他的算法F2类似。为了产生1~N内的一组M元排列,它会先从1~N 1中产生一组M 1元的排列。(算法的递归版本中没有变量J。)但是,排列产生器的主要数据结构是序列而非集合。下面是Floyd的算法P。
算法P
- initialize sequence S to empty
- for J := N - M + 1 to N do
- T = RandInt(1, J)
- if T is not in S then
- prefix T to S
- else
- insert J in S after T
从习题5可以看出,算法P在随机位的使用上尤其高效。习题6讨论了序列S的高效率实现。
我们可以从算法在M=N时的行为得到关于算法P的直观的感觉,此时算法生成N元的随机排列,其中J从1到N循环。在执行循环体之前,S是一个1~J 1的整数的随机排列。循环体把J插入到序列中仍然保持了这一点;当T=J时,J成为第一个元素,否则J被随机放置于已经存在的J 1个元素的某一个之后。
一般地,算法P以等概率生成1~N内的每一个M元排列。Floyd对于正确性的证明用到循环不变式:第i轮循环后,J=N M+i且S可能是1~J中i个不同整数的任意排列,并且只有一种途径可以生成这个排列。
Doug McIlroy发现了一种优雅的方式来说明Floyd的证明:对于任何一个排列,有且仅有一种途径来生成它,因为算法是可以逆推的。例如,假设M=5,N=10,且最终的序列为
- 7 2 9 1 5
由于10(J的最终取值)不在S中出现,所以之前的序列肯定是
- 2 9 1 5
且RandInt返回值为T=7。又因为9(相应的J的值)出现在4元序列中的2之后,所以之前的T是2。习题4说明了可以类似地恢复出整个随机数序列。由于假定了所有的随机序列是以相同的可能性出现的,于是所有的排列也同样是等概率的。
我们现在可以利用与算法P的相似性来证明算法F2。在算法的每一步,算法F2中的集合S和算法P中的序列S所含的元素是相同的。因此,1~N的每一个M元子集都由M!个随机序列生成,于是它们是等概率的。