算法、Python——如何将数字序列映射为整数

背景

如何只调用一次rand()就实现洗牌算法(将一个列表随机打乱顺序)?
考虑一串长度为 n n n的数字序列 [ 0 , 1 , 2 , 3 , . . . , n − 1 ] [0,1,2,3,...,n-1] [0,1,2,3,...,n1],其的不同排列顺序共有 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,...,n1]可以发现比数字 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

算法效果

算法效果

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值