使用洗牌算法生成随机序列

  假如我们要构造一个取值在1到n之间的n项随机整数序列S,而且有一个可以生成i到j之间的随机整数的函数rand(i, j)。好的,我们可以简单的为序列S的每一项生成一个介于1到n之间的随机整数Si,伪代码如下:

for(i = 1; i <= n; ++i) { Si = rand(1, n); }

  OK,问题解决。使用这个算法只需要调用n次rand(1, n)。

  现在我们对约束进行扩展,扩展后的序列中不能存在重复的数。我们修改上述算法,让他可以满足新的约束:每当为序列S的某一项Si生成一个随机数时,检测Si是否与Sj( j = 1, 2, 3, ... , i-1 )相等。若相等,则为Si重新生成一个随机数,并重复上述检测,直到Si不与任何的Sj相等。伪代码如下:

for(i = 1; i <= n; ++i) { Si = rand(1, n); j = 1; while(j < i) { if(Sj == Si) { Si = rand(1, n); j = 1; } else { ++j; } } }

  问题再次解决。是吧?且慢,让我们考虑一下这个算法的时间复杂度。取序列长度n为输入规模,判断操作Sj == Si为基本操作,则该算法的时间复杂度为θ(n2)。这个复杂度和求解扩展前问题时θ(n)的复杂度相差太大了。

  问题来了,对于扩展后问题有没有时间复杂度为θ(n)的算法呢?洗牌算法就是救星,该算法使用一个已经存在的序列来生成一个新的随机序列。算法如下,假设S是一个已经存在的序列,该算法结束后S为一个新生成的随机序列。

for(i = 1; i <= n - 1; ++i) { j = rand(i, n); swap(Si, Sj); }

  该算法依次将S中的每一项与从其后随机挑选的一项交换,来生成新的随机序列,他的时间复杂度为θ(n)。该算法不再需要对重复元素进行的检测,他所需要的就是一个初始序列S。我们可以使用一种简单有效的策略:将S序列初始化为序列1, 2, 3, ... , n。这个初始化的复杂度也为θ(n)。现在我们得到了一个时间复杂度为θ(n)的生成不含有重复元素的随机序列的算法,如下:

for(i = 1; i <= n; ++i) { Si = i; } for(i = 1; i <= n - 1; ++i) { j = rand(i, n); swap(Si, Sj); }

  再来看看这两个算法的不同。序列是一个定义域为连续整数的特殊函数。要求序列中的元素不可重复,其实就是要保持这个特殊的函数是单射的。第一种算法需要在引入每一个随机数后检测是否破坏了函数单射的性质。而第二个算法在初始化序列的时候就保证了函数是单射的,又洗牌算法的每一步都不会破坏函数单射的性质,所以这里函数隐含是单射的,并不需要检测。这就是预构造和问题化简的思想。

你可能感兴趣的:(algorithm)