鉴于一个包含数百万个条目的python dict,从中获取和删除随机(k,v)对的最有效方法是什么?
dict不断增长,并且经常调用随机删除功能。
python2 random_key = random.choice(the_dict.keys())
引用最多的解决方案太慢了,因为首先创建了所有键的列表。由于dict中有许多元素,因此该解决方案不起作用。
另一个提议的解决方案是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可以有效地解决这个问题吗?
答案 0 :(得分:6)
解决方案是使用双向映射每个键到一个整数,以允许通过使用random.randrange(0,N)随机选择一个键来从一个双向映射到键的整数范围中进行选择,其中N是键的数量。
添加新密钥只会为其指定下一个更高的int。在删除键值对之前,删除键会将该键的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 :(得分:3)
不,正如您所发现的那样,这不能用简单的词典有效地完成。请参阅this issue,了解有关为集合实施random.choice
的原因难以解释的原因;相同的论点适用于字典。
但是可以创建一个类似于dict的数据结构, 支持有效的随机选择。这是一个这样的对象的配方,部分基于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)