背景
如何只调用一次rand()就实现洗牌算法(将一个列表随机打乱顺序)?
考虑一串长度为nnn的数字序列[0,1,2,3,...,n−1][0,1,2,3,...,n-1][0,1,2,3,...,n−1],其的不同排列顺序共有n!n!n!种,其包含的信息量为I=log(n!)I=log(n!)I=log(n!),也就是说存在一种方法能将所有排序的序列一一映射到[0,n!−1][0,n!-1][0,n!−1]上的整数。那num2order(rand()%n)就能实现洗牌算法了!
满足上述条件的映射方法有很多,为了获得一个足够优雅的映射方法,有如下要求:
- 数字000对应正序序列
- 数字n!−1n!-1n!−1对应倒序序列
- 数字iii与i+1i+1i+1对应的两个序列可以通过选择一个数字插入其他位置来互相转换
算法原理
1.阶乘进制
首先介绍一下阶乘进制。就如同2进制、10进制、16进制一样,阶乘进制也是一种数的表示方法,但其基底不再是某个数字的幂(如二进制中各个位置代表的值为:20,21,22,23,...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!,...。在阶乘进制中第iii位可选的数字不再是固定的,而是[0,i][0,i][0,i]。
阶乘进位公式:1+∑i=0ni×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!和上述公式的含义):
- (119)10=4×4!+3×3!+2×2!+1×1!=(4,3,2,1,0)!(119)_{10} = 4×4!+3×3!+2×2!+1×1!=(4,3,2,1,0)_!(119)10=4×4!+3×3!+2×2!+1×1!=(4,3,2,1,0)!
- (12345)10=2×7!+3×6!+4×4!+1×3!+1×2!+1×1!=(2,3,0,4,1,1,1,0)!(12345)_{10} = 2×7!+3×6!+4×4!+1×3!+1×2!+1×1! = (2,3,0,4,1,1,1,0)_!(12345)10=2×7!+3×6!+4×4!+1×3!+1×2!+1×1!=(2,3,0,4,1,1,1,0)!
- (5463217)10=(1,5,0,3,6,4,4,0,0,1,0)!(5463217)_{10} = (1,5,0,3,6,4,4,0,0,1,0)_!(5463217)10=(1,5,0,3,6,4,4,0,0,1,0)!
- (48995463216)10=(7,11,3,4,8,3,2,0,2,4,0,0,0,0)!(48995463216)_{10}= (7,11,3,4,8,3,2,0,2,4,0,0,0,0)_!(48995463216)10=(7,11,3,4,8,3,2,0,2,4,0,0,0,0)!
可以发现0!0!0!位始终是000,1!=11!=11!=1了没0!=10!=10!=1什么事,何况0!0!0!只能取000。这一点性质也方便了下文中整数与序列的转换。
2.插入法
以000为基础开始构建一个序列:[0][0][0]
首先插入数字111,有两种插入位置——0:[0,1],1:[1,0]0:[0,1],1:[1,0]0:[0,1],1:[1,0]
再插入数字222,有三种插入位置——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]
以此类推,可以发现数字iii插入时有i+1i+1i+1种插入位置,而且XXX必定小于iii,因为是从000开始由小到大插入数字的。所以以记录插入信息为基础来解析或者生成序列是一个很好的思路。
那么如何记录插入信息?观察一个序列[0,1,2,3,...,n−1][0,1,2,3,...,n-1][0,1,2,3,...,n−1]可以发现比数字iii小的数数字一共有iii个,那么数字iii后面比iii小的数字个数可能为[0,i][0,i][0,i]个。后插入的数字对之前插入数字之间的位置关系并无影响。所以数字iii后面比iii小的数字个数就相当于插入信息了。
数字iii后面比iii小的数字个数与阶乘进制中第i位可选数字都是是[0,i][0, i][0,i],通过阶乘进制与其他进制转换就能将插入信息转换为一个整数了。
3.序列到整数
在一个序列中,将数字iii之后比iii小的数字当作一个阶乘进制数字中的第iii位,再将该数字转换为10进制,就完成了序列到整数的一一映射。
举个例子,序列[3,0,4,1,2][3, 0, 4, 1, 2][3,0,4,1,2]之中:000之后比000小的数字为000个;111之后比111小的数字为000个;222之后比222小的数字为000个;333之后比333小的数字为333个(0,1,2)(0,1,2)(0,1,2);444之后比444小的数字为222个(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
4.整数到序列
根据插入法的思想,首先将整数转换为阶乘进制,再根据阶乘进制下数字记录的插入信息,从000开始逐个插入数字到序列中,最终就能还原序列。注意一点,在不确定序列长度的情况下,一个整数其实对应了无穷多个序列。如数字000对应了所有正序序列,数字111对应了所有[1,0,2,3,4,...][1,0,2,3,4,...][1,0,2,3,4,...]序列。所以整数到序列的映射过程还需要知道序列的长度。
举个例子,数字555555对应的长度为5的序列:首先将555555转换为阶乘进制(2,1,0,1,0)!(2,1,0,1,0)_!(2,1,0,1,0)!;第000位插入0,[0]0,[0]0,[0];第111位插入1,[0,1]1,[0,1]1,[0,1];第000位插入2,[2,0,1]2,[2,0,1]2,[2,0,1];第111位插入3,[2,3,0,1]3,[2,3,0,1]3,[2,3,0,1];第222位插入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