Python实现乱序算法并模拟验证算法的可行性

把m个元素在n大小的素组中随机打乱,实验次数为N,来验证随机算法的可行性

乱序算法(第一版)

先把所有的需要打乱的样品放在容器的最前面,随机生成一个位置进行交换

# Author: WenRuo 
# CreateDate: 2021/11/29

import random


class KnuthShuffle:
    """
    使用一维数组模拟乱序结果是否真随机
    """

    def __init__(self, N, n, m):
        """
        把m个样品随机分布在n个容器中 统计每一个容器被分配到的有样品的频率是多少
        :param N: 试验次数
        :param n: 容器总数
        :param m: 样品总数
        """
        if N <= 0:
            print("频次不能小于0")
            return
        if n < m:
            print("样品不能大于等于容器")
            return
        self.N = N
        self.n = n
        self.m = m

    def run(self):
        """
        进行N次概率模拟 先把所有元素排列在数组最前面 然后洗牌打乱
        :return:
        """
        # 记录频次 初始化都为0 freq[i]的位置指在i的位置出现的频次
        freq = []
        # 记录容器
        arr = []
        for i in range(self.n):
            freq.append(0)
        for i in range(self.n):
            arr.append(0)
        # 模拟试验次数
        for i in range(self.N):
            self.reset(arr)  # 先初始化数组 把所有排列到最前面
            self.shuffle(arr)  # 洗牌
            for j in range(self.n):
                freq[j] += arr[j]
        # 打印频次
        print(">>>索引 : 出现概率")
        for i in range(self.n):
            msg = "{0} : {1}".format(i, round(freq[i] / self.N, 3))
            print(msg)

        print(">>> 打印被打乱后的数组:")
        for i in arr:
            print(i, end=" ")

        print("\n>>> 打印各个位置出现的频次:")
        for i in freq:
            print(i, end=" ")

    def reset(self, arr):
        """
        模拟将样品全部排列在容器的最前面
        将arr前m个都设置1,m之后的全部设置成0
        :param arr:
        :return:
        """
        for i in range(self.m):
            arr[i] = 1
        for i in range(self.m, self.n):
            arr[i] = 0

    def shuffle(self, arr):
        """
        随机打乱
        """
        for i in range(self.n):
            x = int(random.random() * self.n)
            arr[i], arr[x] = arr[x], arr[i]


if __name__ == '__main__':
    N = 100000
    n = 10
    m = 5
    exp = KnuthShuffle(N, n, m)
    exp.run()

打印结果:

>>>索引 : 出现概率
0 : 0.564
1 : 0.544
2 : 0.519
3 : 0.49
4 : 0.461
5 : 0.47
6 : 0.478
7 : 0.484
8 : 0.492
9 : 0.499
>>> 打印被打乱后的数组:
0 0 0 1 1 1 0 1 1 0 
>>> 打印各个位置出现的频次:
56421 54446 51852 49049 46108 46970 47751 48364 49154 49885 

0.49与0.56之间的误差过大,说明随机洗牌算法的结果是有偏的,并不能保证每一个位置随机样品的概率是五五开的

尝试改进Shuffle函数(第二版)

由于关注点在m个样品随机打乱,所以每一次把一个样品与随机的另外一个位置进行交换,只需要循环m次即样品的总数即可。

def shuffle(self, arr):
     """
     随机打乱
     """
     for i in range(self.m):
         x = int(random.random() * self.n)
         arr[i], arr[x] = arr[x], arr[i]

打印结果

>>>索引 : 出现概率
0 : 0.671
1 : 0.635
2 : 0.593
3 : 0.548
4 : 0.5
5 : 0.413
6 : 0.411
7 : 0.408
8 : 0.407
9 : 0.413
>>> 打印被打乱后的数组:
1 0 1 0 1 0 0 0 1 1 
>>> 打印各个位置出现的频次:
67110 63510 59333 54835 50023 41253 41092 40832 40692 41320 

0,41-0.67之间的误差更大,比第一版更加有偏。打乱后的数组看似随机,但通过上面的频次结果可以明显观察到随机算法并不合理!!!

上面两种尝试都可以发现随机的结果是有偏的,并不能做到等概率的生成某一种结果之一。对随机的公平性要求比较高的程序,上面的两种洗牌算法都不能接受!

等概率的洗牌算法(最优解)

假设有一副扑克牌,进行洗牌,首先从54张牌中随机一张放在数组的第1个位置,然后从剩下的53张牌中随机选出一张放在数组第二个位置,以此类推。

在不开辟新的空间的情况下,假设有54大小的数组,随机一个数与数组最末尾进行交换,然后从剩下的53个中,再次随机一个数与倒数第二数进行交换,维护一个i–即可

def shuffle(self, arr):
    """
    洗牌算法
    """
    for i in range(self.n - 1, -1, -1):
        x = int(random.random() * (i + 1))
        arr[i], arr[x] = arr[x], arr[i]

打印结果:

>>>索引 : 出现概率
0 : 0.501
1 : 0.499
2 : 0.5
3 : 0.501
4 : 0.499
5 : 0.5
6 : 0.497
7 : 0.5
8 : 0.501
9 : 0.503
>>> 打印被打乱后的数组:
1 0 0 0 1 1 1 0 1 0 
>>> 打印各个位置出现的频次:
50051 49861 49978 50139 49869 49977 49743 49976 50148 50258 

通过结果可以直观的发现频次保持在0.49-0.5 之间,概率差不超过百分之一,说明随机得到的乱序结果保证了足够的乱。是一个较好的算法!

猜你喜欢

转载自blog.csdn.net/wankcn/article/details/121633639