如何在Python中对存储在文件中的非常大的列表进行洗牌?

时间:2017-07-01 05:52:58

标签: python list numpy random pycrypto

我需要确定性地生成一个随机列表,其中包含从0到2 ^ 32-1的数字。

这将是天真的(并且完全没有功能)这样做的方式,所以我很清楚我想要的是什么。

import random
numbers = range(2**32)
random.seed(0)
random.shuffle(numbers)

我尝试使用numpy.arange()创建列表,并使用pycrypto的random.shuffle()对其进行随机播放。让列表吃掉大约8gb的ram,然后将洗牌提升到大约25gb。我只有32gb给。但那并不重要因为......

我已经尝试将列表切割成1024个切片并尝试上述方法,但即使其中一个切片也需要太长时间。我将其中一个切片切成128个更小的切片,而 每个切片大约需要620毫秒。如果它线性增长,则意味着整个过程需要大约22个半小时才能完成。这听起来不错,但它没有线性增长。

我尝试过的另一件事是为每个条目生成随机数,并将它们用作新位置的索引。然后我沿着列表向下并尝试将数字放在新索引处。如果该索引已在使用中,则索引会递增,直到找到一个空闲索引。这在理论上是有效的,它可以做到大约一半,但接近结束时它不断搜索新的点,在列表中多次包裹。

有没有办法解决这个问题?这是一个可行的目标吗?

5 个答案:

答案 0 :(得分:3)

如果您有连续的数字范围,则根本不需要存储它们。很容易设计一个混洗列表中的值与其在该列表中的位置之间的双向映射。这个想法是使用伪随机排列,这正是block ciphers提供的。

诀窍是找到一个完全符合32位整数要求的块密码。很少有这样的块密码,但 Simon Speck 密码(由NSA发布)是可参数化的,并且支持32位的块大小(通常块大小是更大)。

This library似乎提供了一个实现。我们可以设计以下功能:

def get_value_from_index(key, i):
    cipher = SpeckCipher(key, mode='ECB', key_size=64, block_size=32)
    return cipher.encrypt(i)

def get_index_from_value(key, val):
    cipher = SpeckCipher(key, mode='ECB', key_size=64, block_size=32)
    return cipher.decrypt(val)

该库使用Python的大整数,因此您甚至可能不需要对它们进行编码。

64位密钥(例如0x123456789ABCDEF0)并不多。您可以使用类似的结构将DES中的密钥大小增加到Triple DES。请记住,键应该随机选择,如果你想要确定性,它们必须是恒定的。

如果您不想使用NSA的算法,我会理解。还有其他人,但我现在无法找到它们。 Hasty Pudding密码更加灵活,但我不知道是否有针对Python的实现。

答案 1 :(得分:2)

我创建的类使用了一个跟踪数据已经被使用过的数据。通过评论,我认为代码非常自我解释。

import bitarray
import random


class UniqueRandom:
    def __init__(self):
        """ Init boolean array of used numbers and set all to False
        """
        self.used = bitarray.bitarray(2**32)
        self.used.setall(False)

    def draw(self):
        """ Draw a previously unused number
         Return False if no free numbers are left
        """

        # Check if there are numbers left to use; return False if none are left
        if self._free() == 0:
            return False

        # Draw a random index
        i = random.randint(0, 2**32-1)

        # Skip ahead from the random index to a undrawn number
        while self.used[i]:
            i = (i+1) % 2**32

        # Update used array
        self.used[i] = True

        # return the selected number
        return i

    def _free(self):
        """ Check how many places are unused
        """
        return self.used.count(False)


def main():
    r = UniqueRandom()
    for _ in range(20):
        print r.draw()


if __name__ == '__main__':
    main()

设计注意事项
虽然Garrigan Stafford的答案非常好,但该解决方案的内存占用量要小得多(略高于4 GB)。我们的答案之间的另一个区别是,当生成的数量增加时,Garrigan的算法需要更多的时间来生成随机数(因为他一直在迭代直到找到未使用的数字)。如果已经使用了某个数字,该算法只查找下一个未使用的数字。这使得每次实际绘制一个数字所花费的时间几乎相同,无论空闲数据池的耗尽程度如何。

答案 2 :(得分:2)

计算所有值似乎是不可能的,因为Crypto计算一个大约一个毫秒的随机整数,所以整个工作需要几天。

以下是作为生成器的Knuth算法实现:

from Crypto.Random.random import randint  
import numpy as np

def onthefly(n):
    numbers=np.arange(n,dtype=np.uint32)
    for i in range(n):
        j=randint(i,n-1)
        numbers[i],numbers[j]=numbers[j],numbers[i]
        yield numbers[i]

n=10

gen=onthefly(10)
print([next(gen) for i in range(9)])
print(next(gen))
#[9, 0, 2, 6, 4, 8, 7, 3, 1]
#5

对于n=2**32,生成器需要一分钟来初始化,但调用是O(1)。

答案 3 :(得分:1)

所以一种方法是跟踪你已经发出的数字,并一次分发一个新的随机数,考虑

import random
random.seed(0)

class RandomDeck:
      def __init__(self):
           self.usedNumbers = set()

      def draw(self):
          number = random.randint(0,2**32)
          while number in self.usedNumbers:
                 number = random.randint(0,2**32)
          self.usedNumbers.append(number)
          return number

      def shuffle(self):
          self.usedNumbers = set()

正如你所看到的,我们基本上有一组介于0和2 ^ 32之间的随机数,但我们只存储我们发出的数字以确保我们没有重复。然后,您可以通过忘记已经发出的所有数字来重新洗牌。

这在大多数用例中应该是有效的,只要你没有重新洗牌就不能抽取大约100万个数字。

答案 4 :(得分:1)

这是我写的一个置换 RNG,它使用了一个事实,即一个数模一个素数的平方(加上一些复杂性)给出了一个伪随机置换。

https://github.com/pytorch/pytorch/blob/09b4f4f2ff88088306ecedf1bbe23d8aac2d3f75/torch/utils/data/_utils/index_utils.py

简短版本:

def _is_prime(n):
    if n == 2:
        return True
    if n == 1 or n % 2 == 0:
        return False

    for d in range(3, floor(sqrt(n)) + 1, 2):  # can use isqrt in Python 3.8
        if n % d == 0:
            return False

    return True


class Permutation(Range):
    """
    Generates a random permutation of integers from 0 up to size.
    Inspired by https://preshing.com/20121224/how-to-generate-a-sequence-of-unique-random-integers/
    """

    size: int
    prime: int
    seed: int

    def __init__(self, size: int, seed: int):
        self.size = size
        self.prime = self._get_prime(size)
        self.seed = seed % self.prime

    def __getitem__(self, index):
        x = self._map(index)

        while x >= self.size:
            # If we map to a number greater than size, then the cycle of successive mappings must eventually result
            # in a number less than size. Proof: The cycle of successive mappings traces a path
            # that either always stays in the set n>=size or it enters and leaves it,
            # else the 1:1 mapping would be violated (two numbers would map to the same number).
            # Moreover, `set(range(size)) - set(map(n) for n in range(size) if map(n) < size)`
            # equals the `set(map(n) for n in range(size, prime) if map(n) < size)`
            # because the total mapping is exhaustive.
            # Which means we'll arrive at a number that wasn't mapped to by any other valid index.
            # This will take at most `prime-size` steps, and `prime-size` is on the order of log(size), so fast.
            # But usually we just need to remap once.
            x = self._map(x)

        return x

    @staticmethod
    def _get_prime(size):
        """
        Returns the prime number >= size which has the form (4n-1)
        """
        n = size + (3 - size % 4)
        while not _is_prime(n):
            # We expect to find a prime after O(log(size)) iterations
            # Using a brute-force primehood test, total complexity is O(log(size)*sqrt(size)), which is pretty good.
            n = n + 4
        return n

    def _map(self, index):
        a = self._permute_qpr(index)
        b = (a + self.seed) % self.prime
        c = self._permute_qpr(b)
        return c

    def _permute_qpr(self, x):
        residue = pow(x, 2, self.prime)

        if x * 2 < self.prime:
            return residue
        else:
            return self.prime - residue