如何只调用一次rand()就实现洗牌算法(将一个列表随机打乱顺序)?
考虑一串长度为 n n n的数字序列 [ 0 , 1 , 2 , 3 , . . . , n − 1 ] [0,1,2,3,...,n-1] [0,1,2,3,...,n−1],其的不同排列顺序共有 n ! n! n!种,其包含的信息量为 I = l o g ( n ! ) I=log(n!) I=log(n!),也就是说存在一种方法能将所有排序的序列一一映射到 [ 0 , n ! − 1 ] [0,n!-1] [0,n!−1]上的整数。那num2order(rand()%n)就能实现洗牌算法了!
满足上述条件的映射方法有很多,为了获得一个足够优雅的映射方法,有如下要求:
首先介绍一下阶乘进制。就如同2进制、10进制、16进制一样,阶乘进制也是一种数的表示方法,但其基底不再是某个数字的幂(如二进制中各个位置代表的值为: 2 0 , 2 1 , 2 2 , 2 3 , . . . 2^0,2^1,2^2,2^3,... 20,21,22,23,...),而是位的阶乘: 0 ! , 1 ! , 2 ! , 3 ! , 4 ! , . . . 0!,1!, 2!, 3!, 4!, ... 0!,1!,2!,3!,4!,...。在阶乘进制中第 i i i位可选的数字不再是固定的,而是 [ 0 , i ] [0,i] [0,i]。
阶乘进位公式: 1 + ∑ i = 0 n i × i ! = ( n + 1 ) ! 1+\sum^n_{i=0}i \times i!=(n+1)! 1+∑i=0ni×i!=(n+1)!。
例子(注意 1 + 119 = 5 ! 1+119 = 5! 1+119=5!和上述公式的含义):
可以发现 0 ! 0! 0!位始终是 0 0 0, 1 ! = 1 1!=1 1!=1了没 0 ! = 1 0!=1 0!=1什么事,何况 0 ! 0! 0!只能取 0 0 0。这一点性质也方便了下文中整数与序列的转换。
以 0 0 0为基础开始构建一个序列: [ 0 ] [0] [0]
首先插入数字 1 1 1,有两种插入位置—— 0 : [ 0 , 1 ] , 1 : [ 1 , 0 ] 0:[0,1],1:[1,0] 0:[0,1],1:[1,0]
再插入数字 2 2 2,有三种插入位置—— 0 : [ X , X , 2 ] , 1 : [ X , 2 , X ] , 2 : [ 2 , X , X ] 0:[X,X,2],1:[X,2,X],2:[2,X,X] 0:[X,X,2],1:[X,2,X],2:[2,X,X]
以此类推,可以发现数字 i i i插入时有 i + 1 i+1 i+1种插入位置,而且 X X X必定小于 i i i,因为是从 0 0 0开始由小到大插入数字的。所以以记录插入信息为基础来解析或者生成序列是一个很好的思路。
那么如何记录插入信息?观察一个序列 [ 0 , 1 , 2 , 3 , . . . , n − 1 ] [0,1,2,3,...,n-1] [0,1,2,3,...,n−1]可以发现比数字 i i i小的数数字一共有 i i i个,那么数字 i i i后面比 i i i小的数字个数可能为 [ 0 , i ] [0,i] [0,i]个。后插入的数字对之前插入数字之间的位置关系并无影响。所以数字 i i i后面比 i i i小的数字个数就相当于插入信息了。
数字 i i i后面比 i i i小的数字个数与阶乘进制中第i位可选数字都是是 [ 0 , i ] [0, i] [0,i],通过阶乘进制与其他进制转换就能将插入信息转换为一个整数了。
在一个序列中,将数字 i i i之后比 i i i小的数字当作一个阶乘进制数字中的第 i i i位,再将该数字转换为10进制,就完成了序列到整数的一一映射。
举个例子,序列 [ 3 , 0 , 4 , 1 , 2 ] [3, 0, 4, 1, 2] [3,0,4,1,2]之中: 0 0 0之后比 0 0 0小的数字为 0 0 0个; 1 1 1之后比 1 1 1小的数字为 0 0 0个; 2 2 2之后比 2 2 2小的数字为 0 0 0个; 3 3 3之后比 3 3 3小的数字为 3 3 3个 ( 0 , 1 , 2 ) (0,1,2) (0,1,2); 4 4 4之后比 4 4 4小的数字为 2 2 2个 ( 1 , 2 ) (1,2) (1,2)。可得阶乘进制数字 ( 2 , 3 , 0 , 0 , 0 ) ! = 2 × 4 ! + 3 × 3 ! = 66 (2,3,0,0,0)_!=2×4!+3×3!=66 (2,3,0,0,0)!=2×4!+3×3!=66。
以此思路写出Python代码如下:
def order2num(lst): # 这个函数对无重复的可排序序列都能使用
num_lst = []
lst_sorted = sorted(lst)
for ele in lst_sorted:
count = 0
for i in range(lst.index(ele), len(lst)):
if lst[i] < ele:
count += 1
num_lst.append(count)
num = 0
for i in range(len(num_lst)):
num += num_lst[i] * factorial(i)
return num
根据插入法的思想,首先将整数转换为阶乘进制,再根据阶乘进制下数字记录的插入信息,从 0 0 0开始逐个插入数字到序列中,最终就能还原序列。注意一点,在不确定序列长度的情况下,一个整数其实对应了无穷多个序列。如数字 0 0 0对应了所有正序序列,数字 1 1 1对应了所有 [ 1 , 0 , 2 , 3 , 4 , . . . ] [1,0,2,3,4,...] [1,0,2,3,4,...]序列。所以整数到序列的映射过程还需要知道序列的长度。
举个例子,数字 55 55 55对应的长度为5的序列:首先将 55 55 55转换为阶乘进制 ( 2 , 1 , 0 , 1 , 0 ) ! (2,1,0,1,0)_! (2,1,0,1,0)!;第 0 0 0位插入 0 , [ 0 ] 0,[0] 0,[0];第 1 1 1位插入 1 , [ 0 , 1 ] 1,[0,1] 1,[0,1];第 0 0 0位插入 2 , [ 2 , 0 , 1 ] 2,[2,0,1] 2,[2,0,1];第 1 1 1位插入 3 , [ 2 , 3 , 0 , 1 ] 3,[2,3,0,1] 3,[2,3,0,1];第 2 2 2位插入 4 , [ 2 , 3 , 4 , 0 , 1 ] 4,[2,3,4,0,1] 4,[2,3,4,0,1];逆序 [ 1 , 0 , 4 , 3 , 2 ] [1,0,4,3,2] [1,0,4,3,2]。
以此思路写出Python代码如下:
def num2order(num, length=None):
if length is None: # 缺省长度为该整数可映射的最小长度
length = 1
while factorial(length) <= num:
length += 1
elif num >= factorial(length):
return False
num_lst = []
while length != 0:
length -= 1
fac = factorial(length)
num_lst.append(int(num/fac))
num %= fac
num_lst.reverse()
lst = []
for i in range(len(num_lst)):
lst.insert(num_lst[i], i)
lst.reverse()
return lst