【问题标题】:How to get a random value in a very large python dictionary如何在非常大的python字典中获取随机值
【发布时间】:2014-07-08 11:35:59
【问题描述】:

给定一个包含数百万个条目的 python dict,从中获取和删除随机 (k,v) 对的最有效方法是什么?

dict 不断增长,随机删除函数被非常频繁地调用。

python2 random_key = random.choice(the_dict.keys()) 被引用最多的解决方案太慢了,因为首先创建了所有键的列表。字典中有很多元素,这个解决方案不起作用。

另一个建议的解决方案是the_dict.popitem(),但这不会返回真正的随机对象,而是取决于字典的内部排序。

第三个同样会变慢的解决方案是迭代器:

 it = the_dict.iterkeys()

 for i in range (random.randint(0, len(the_dict)-1)):
     next(it)
 random_key = next(it)

remove_random() 旁边,有时特定键需要the_dict.pop(x)。因此,简单的基于列表的二级索引是行不通的。

这个问题可以用 dict 有效解决吗?

【问题讨论】:

标签: python dictionary random


【解决方案1】:

一种解决方案是使用双向映射每个键到一个整数,以允许通过使用 random.randrange(0,N) 从双向映射到键的整数范围中进行选择来随机选择一个键,其中N 是键的数量。

添加一个新键只是为其分配下一个更高的整数。删除键会将该键的 int 重新分配给在删除键值对之前分配给先前最高 int 的键。为了清楚起见,提供了 Python 代码。

Python 代码:

def create(D): # O(len(D))
    # Create the bidirectional maps from the dictionary, D
    keys = D.keys()
    ints = range(len(keys)
    int_to_key = dict(zip(keys, ints)) 
    key_to_int = dict(zip(ints, keys))
    return (int_to_key, key_to_int)

def add(D, int_to_key, key_to_int, key, value): # O(1)
    # Add key-value pair (no extra work needed for simply changing the value)
    new_int = len(D)
    D[key] = value
    int_to_key[new_int] = key
    key_to_int[key] = new_int

def remove(D, int_to_key, key_to_int, key): # O(1)
    # Update the bidirectional maps then remove the key-value pair

    # Get the two ints and keys.
    key_int = key_to_int[key]
    swap_int = len(D) - 1 # Should be the highest int
    swap_key = int_to_key[swap_int]

    # Update the bidirectional maps so that key now has the highest int
    key_to_int[key], key_to_int[swap_key] = swap_int, key_int
    int_to_key[key_int], int_to_key[swap_int] = swap_key, key

    # Remove elements from dictionaries
    D.remove(key)
    key_to_int.remove(key)
    int_to_key.remove(key)

def random_key(D, int_to_key): # O(1)
    # Select a random key from the dictionary using the int_to_key map
    return int_to_key[random.randrange(0, len(D))]

def remove_random(D, int_to_key, key_to_int): # O(1)
    # Randomly remove a key from the dictionary via the bidirectional maps
    key = random_key(D, int_to_key)
    remove(D, int_to_key, key_to_int, key)

注意:在 D 中添加/删除键而不使用上述适当的函数会破坏双向映射。这意味着最好将其作为一个类来实现。

【讨论】:

  • +1。不过,这只是乞求包含在类定义中。
  • ... 在我看来,您的 int_to_key 可以用一个简单的列表替换。
  • @MarkDickinson:我没有使用类,因为实现类包含一些与解决方案实际工作方式无关的额外内容。此外,您对 int_to_key 的权利。我想到了一个双向地图,所以用了两个字典,但是一个列表和一个字典也可以正常工作。我已经 +1 你对实现类和 int_to_key 改进的回答。
【解决方案2】:

不,正如您所发现的,使用普通的 dict 无法有效地完成此操作。请参阅this issue,了解为什么为集合实现random.choice 是困难的;同样的论点也适用于字典。

但是可以创建一个类似字典的数据结构,确实支持有效的随机选择。这是此类对象的配方,部分基于this question 及其响应。这只是一个起点,但它支持大多数现有的 dict 方法,其中许多方法可以方便地由MutableMapping ABC 填写。根据您的需要,您可能需要对其进行一些充实:例如,能够直接从常规字典创建RandomChoiceDict,或者添加有意义的__repr__,等等。

基本上,您需要维护三个结构:键的list,对应值的list,以及将键映射回索引的dict(键列表的倒数)。基本的__getitem____setitem____delitem__ 操作可以根据这些结构简单地实现,如果指定了__len____iter__,则抽象基类负责其余大部分操作。

from collections import MutableMapping
import random

class RandomChoiceDict(MutableMapping):
    """
    Dictionary-like object allowing efficient random selection.

    """
    def __init__(self):
        # Add code to initialize from existing dictionaries.
        self._keys = []
        self._values = []
        self._key_to_index = {}

    def __getitem__(self, key):
        return self._values[self._key_to_index[key]]

    def __setitem__(self, key, value):
        try:
            index = self._key_to_index[key]
        except KeyError:
            # Key doesn't exist; add a new one.
            index = len(self._keys)
            self._key_to_index[key] = index
            self._keys.append(key)
            self._values.append(value)
        else:
            # Key already exists; overwrite the value.
            self._values[index] = value

    def __delitem__(self, key):
        index = self._key_to_index.pop(key)
        # Remove *last* indexed element, then put
        # it back at position 'index' (overwriting the
        # one we're actually removing) if necessary.
        key, value = self._keys.pop(), self._values.pop()
        if index != len(self._key_to_index):
            self._keys[index] = key
            self._values[index] = value
            self._key_to_index[key] = index

    def __len__(self):
        return len(self._key_to_index)

    def __iter__(self):
        return iter(self._keys)

    def random_key(self):
        """Return a randomly chosen key."""
        if not self:
            raise KeyError("Empty collection")
        index = random.randrange(len(self))
        return self._keys[index]

    def popitem_random(self):
        key = self.random_key()
        value = self.pop(key)
        return key, value

示例用法:

>>> d = RandomChoiceDict()
>>> for x in range(10**6):  # populate with some values
...     d[x] = x**2
... 
>>> d.popitem_random()  # remove and return random item
(132545, 17568177025)
>>> 132545 in d
False
>>> d.popitem_random()
(954424, 910925171776)

【讨论】:

    猜你喜欢
    • 2023-03-10
    • 2013-09-28
    • 1970-01-01
    • 2015-10-12
    • 1970-01-01
    • 2020-03-17
    • 2020-01-26
    • 2021-08-02
    • 2022-11-17
    相关资源
    最近更新 更多