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

背景

如何只调用一次rand()就实现洗牌算法(将一个列表随机打乱顺序)?
考虑一串长度为nnn的数字序列[0,1,2,3,...,n−1][0,1,2,3,...,n-1][0,1,2,3,...,n1],其的不同排列顺序共有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对应倒序序列
  • 数字iiii+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!位始终是0001!=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,...,n1]可以发现比数字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,2444之后比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

算法效果

算法效果

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值