背景
如何只调用一次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)就能实现洗牌算法了!
满足上述条件的映射方法有很多,为了获得一个足够优雅的映射方法,有如下要求:
- 数字 0 0 0对应正序序列
- 数字 n ! − 1 n!-1 n!−1对应倒序序列
- 数字 i i i与 i + 1 i+1 i+1对应的两个序列可以通过选择一个数字插入其他位置来互相转换
算法原理
1.阶乘进制
首先介绍一下阶乘进制。就如同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!和上述公式的含义):
- ( 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!位始终是 0 0 0, 1 ! = 1 1!=1 1!=1了没 0 ! = 1 0!=1 0!=1什么事,何况 0 ! 0! 0!只能取 0 0 0。这一点性质也方便了下文中整数与序列的转换。
2.插入法
以
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],通过阶乘进制与其他进制转换就能将插入信息转换为一个整数了。
3.序列到整数
在一个序列中,将数字
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
4.整数到序列
根据插入法的思想,首先将整数转换为阶乘进制,再根据阶乘进制下数字记录的插入信息,从
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