Python 在 O(1) 中的字典中获取随机键
Posted
技术标签:
【中文标题】Python 在 O(1) 中的字典中获取随机键【英文标题】:Python get random key in a dictionary in O(1) 【发布时间】:2012-05-31 20:36:16 【问题描述】:我需要一个支持 FAST 插入和删除(键、值)对的数据结构,以及“获取随机键”,它与字典的 random.choice(dict.keys()) 执行相同的操作.我在互联网上搜索过,尽管它是线性时间,但大多数人似乎对 random.choice(dict.keys()) 方法感到满意。
我知道更快地实现这一点是可能的:
我可以使用调整大小的哈希表。如果我保持键与槽的比率在 1 和 2 之间,那么我可以选择随机索引,直到遇到非空槽。我只看 1 到 2 个键,在意料之中。 我可以使用 AVL 树在保证最坏情况 O(log n) 的情况下获得这些操作,并增加等级。但是,有没有什么简单的方法可以在 Python 中实现这一点?好像应该有!
【问题讨论】:
这里有一个想法:保留另一个dict
形式为i: key
,其中i
是一个计数器。然后,要进行随机查找,请在另一个字典上调用 randint
。如果我错了,请纠正我,但这听起来像 O(1)。
嗯,从你说的情况来看,如何进行插入和删除并不明显。但是,这在一定程度上确实有效。跟踪最大计数器值,称为 n。对于插入,我们首先尝试 1 和 n 之间的 2 个(或 5 个,或任何恒定数量的)随机值。如果它们都被占用,请使用 n 并增加最大计数器值。否则,插入空白处。整洁!
我很好奇这个数据结构的预期用途。用例是什么?
不好解释。 500 字符版本:我正在为概率编程语言编写编译器(请参阅link),推理需要对可能的随机性选择进行随机游走(请参阅link)。程序的执行点有一个复杂的标记系统,其中发生随机性。这些标签是我字典中的键。要进行推理,需要插入、删除和“获取随机密钥”(以便使用 Metropolis-Hastings 中的提议密度)。
【参考方案1】:
这可能与上面列出的特定用例没有特别相关,但这是我在寻找一种很好地掌握字典中“任何”键的方法时遇到的问题。
如果您不需要真正随机的选择,而只需要一些任意键,那么我找到了两个简单的选项:
key = next(iter(d)) # may be a little expensive, but presumably O(1)
仅当您乐于使用字典中的键+值时,第二个才真正有用,并且由于突变不会在算法上有效:
key, value = d.popitem() # may not be O(1) especially if next step
if MUST_LEAVE_VALUE:
d[key] = value
【讨论】:
next(iter(d))
适用于 Python3 和 Python2(而不是 d.iterkeys().next()
)
@kd88 谢谢!我更新了我的答案以包含该提示。【参考方案2】:
[edit:完全重写,但保留问题,cmets 完好无损。]
以下是字典包装器的实现,其中 O(1) 获取/插入/删除,以及 O(1) 选择随机元素。
主要思想是我们希望有一个从 range(len(mapping))
到键的 O(1) 但任意映射。这将让我们获得random.randrange(len(mapping))
,并通过映射传递它。
这很难实现,直到您意识到我们可以利用 映射可以是任意的这一事实。 实现 O(1) 时间硬绑定的关键思想是:无论何时删除一个元素,都将其与任意 ID 最高的元素交换,并更新任何指针。
class RandomChoiceDict(object):
def __init__(self):
self.mapping = # wraps a dictionary
# e.g. 'a':'Alice', 'b':'Bob', 'c':'Carrie'
# the arbitrary mapping mentioned above
self.idToKey = # e.g. 0:'a', 1:'c' 2:'b',
# or 0:'b', 1:'a' 2:'c', etc.
self.keyToId = # needed to help delete elements
获取、设置和删除:
def __getitem__(self, key): # O(1)
return self.mapping[key]
def __setitem__(self, key, value): # O(1)
if key in self.mapping:
self.mapping[key] = value
else: # new item
newId = len(self.mapping)
self.mapping[key] = value
# add it to the arbitrary bijection
self.idToKey[newId] = key
self.keyToId[key] = newId
def __delitem__(self, key): # O(1)
del self.mapping[key] # O(1) average case
# see http://wiki.python.org/moin/TimeComplexity
emptyId = self.keyToId[key]
largestId = len(self.mapping) # about to be deleted
largestIdKey = self.idToKey[largestId] # going to store this in empty Id
# swap deleted element with highest-id element in arbitrary map:
self.idToKey[emptyId] = largestIdKey
self.keyToId[largestIdKey] = emptyId
del self.keyToId[key]
del self.idToKey[largestId]
随机选择一个(键,元素):
def randomItem(self): # O(1)
r = random.randrange(len(self.mapping))
k = self.idToKey[r]
return (k, self.mapping[k])
【讨论】:
保留list
的密钥,而不是 set
。
@BasicWolf:不,set.pop
不是随机的。
嗯,它不能同时是“快速、好和便宜” :) 但无论如何,我的错,谢谢你的解释。
@WuTheFWasThat:在 CPython 中将其列为列表的问题在于,它无法执行 O(1) 删除操作。通常,如果编程语言实现良好,如果您仅从列表的末尾或开头删除它应该能够,但 CPython 并没有声称能够在 O(1) 时间内这样做。虽然也许我的来源不够具体:wiki.python.org/moin/TimeComplexity 由于 CPython 可以在 O(1) 时间内 append
它可能会在 O(1) 时间内从最后删除,但来源没有说。
@ninjagecko 似乎上面的许多 cmets(以及最后几个)都是无用的,甚至会产生误导,因为您对代码进行了大幅编辑。那么我们应该删除它们吗?我猜我们做不到?【参考方案3】:
这是一个有点复杂的方法:
为每个键分配一个索引,将其与字典中的值一起存储。 保留一个表示下一个索引的整数(我们称之为 next_index)。 保留已删除索引(间隙)的链接列表。 保留将索引映射到键的字典。 添加键时,检查是否使用(并删除)链表中的第一个索引作为索引,或者如果列表为空,则使用并递增 next_index。然后将键、值和索引添加到字典 (dictionary[key] = (index, value)
) 并将键添加到索引到键的字典 (indexdict[index] = key
)。
移除键时,从字典中获取索引,从字典中移除键,从索引到键的字典中移除索引,并将索引插入到链表的最前面。
要获取随机密钥,请使用类似random.randrange(0, next_index)
的方式获取随机整数。如果索引不在 key-to-index 字典中,请重试(这种情况应该很少见)。
这是一个实现:
import random
class RandomDict(object):
def __init__(self): # O(1)
self.dictionary =
self.indexdict =
self.next_index = 0
self.removed_indices = None
self.len = 0
def __len__(self): # might as well include this
return self.len
def __getitem__(self, key): # O(1)
return self.dictionary[key][1]
def __setitem__(self, key, value): # O(1)
if key in self.dictionary: # O(1)
self.dictionary[key][1] = value # O(1)
return
if self.removed_indices is None:
index = self.next_index
self.next_index += 1
else:
index = self.removed_indices[0]
self.removed_indices = self.removed_indices[1]
self.dictionary[key] = [index, value] # O(1)
self.indexdict[index] = key # O(1)
self.len += 1
def __delitem__(self, key): # O(1)
index = self.dictionary[key][0] # O(1)
del self.dictionary[key] # O(1)
del self.indexdict[index] # O(1)
self.removed_indices = (index, self.removed_indices)
self.len -= 1
def random_key(self): # O(log(next_item/len))
if self.len == 0: # which is usually close to O(1)
raise KeyError
while True:
r = random.randrange(0, self.next_index)
if r in self.indexdict:
return self.indexdict[r]
【讨论】:
谢谢!是的,这会奏效。不知道为什么我没有想到。嗯……如果你做了很多删除它就不好了,所以 next_index 比项目数大得多。这实际上有时会在我的程序中发生。但是,我可以进行优化,这样这不是问题。 @WuTheFWasThat 是的,我想不出一个简单的方法来解决这个问题。至少在删除后添加内容时,它会重用它们的索引。 我不相信这会导致 O(1)random_key()
函数。例如,如果您插入 1000000 个元素并删除 1000000 个元素,则每次调用 random_key
都会导致 1/1000000 的成功机会,尽管映射中有少量元素。
@ninjagecko 你说得对,我们只是在讨论这个问题。 (虽然从技术上讲你错了,1000000 - 1000000 = 0
,所以它会立即引发异常。你的意思可能是删除 999999 个元素。)
@ninjagecko - 是的,我想马特和我都知道这个问题。这是一个不比哈希表调整大小更糟糕的解决方案,但不涉及重新实现字典:跟踪表中的事物数量。当它低于 next_index / 2 时,使用 new_next_index = next_index/2 重建整个索引系统,对表中的人使用索引 1、...、new_next_index【参考方案4】:
要获得 O(1) 空间,您需要一个数组数据结构和一个在数组中存储值及其索引的字典。
然后,在添加值时,您只需将它们推送到数组和字典中,并在数组中使用它的索引。
那么,由于您使用的是数组数据结构,因此您可以随机访问。
删除值时,您会查看要在字典中删除的值的索引。然后用数组中的最后一个值替换数组中的那个值(确保它不是最后一个元素)和 pop() 数组中的最后一个值。 之后,您使用已删除的值索引更新字典中替换值(数组中的最后一个值)的键。最后,您删除要删除的值的键和值,因为在字典中没有意义。
class RandomizedSet:
def __init__(self):
self.container = []
self.indices =
def insert(self, val: int) -> bool:
if val in self.indices:
return False
self.indices[val] = len(self.container)
self.container.append(val)
return True
def remove(self, val: int) -> bool:
if val not in self.indices:
return False
idxOfValueToRemove = self.indices[val]
lastValue = self.container[-1]
if idxOfValueToRemove < len(self.container)-1:
self.container[idxOfValueToRemove] = lastValue
self.indices[lastValue] = idxOfValueToRemove
self.container.pop()
del self.indices[val]
return True
def getRandom(self) -> int:
return random.choice(list(self.container))
【讨论】:
【参考方案5】:我遇到了同样的问题,写了
https://github.com/robtandy/randomdict
希望对你有帮助!它提供对随机键、值或项目的 O(1) 访问。
【讨论】:
不建议发布指向外部资源的链接,因为那样将来可能会中断。我建议你在这里提供一些解释并提供参考。 也就是说,我认为这实际上是代码方面的最佳解决方案。它很干净,似乎没有任何主要的算法缺陷。有一个需要注意的错误(很容易修复,但在未合并的 PR 中)。 正如@zplizzi 建议的那样,randomdict
是最佳解决方案。 与@ninjagecko 的otherwise useful solution that internally maintains three (!) dictionaries 相比,randomdict
仅在内部维护一个字典和 2 个列表-元组。是的,不幸的是,这个答案有效地简化为 HTTP 301 URL 重定向——但这就是开源代码野兽的本质。有时,最好的实现已经作为黑盒包存在。这是其中之一。
实际上... 也许不是。 randomdict
删除是 O(n),因为底层实现从内部 list
中删除。这是经典的空间与时间权衡。一般来说,我们更喜欢消耗空间来节省时间——这使得randomdict
对于一般情况来说不是最理想的。尽管如此,randomdict
空间消耗确实对于更大的字典大小显着更好地扩展。 </shrug>
以上是关于Python 在 O(1) 中的字典中获取随机键的主要内容,如果未能解决你的问题,请参考以下文章
如何在不从参考节点获取所有数据的情况下获取 Firebase 数据库中的随机键?