【问题标题】:Selecting a Random Value from dictionary in constant time in Python 3?在 Python 3 中以恒定时间从字典中选择随机值?
【发布时间】:2015-12-24 12:10:40
【问题描述】:

我知道您可以通过多种方式从字典中选择随机值。

在 Python 2 中:

random.choice(d.keys())

在 Python 3 中:

random.choice(list(d.keys()))

尽管如此,这两种方法都需要在随机选择之前将其转换为一个列表,即线性时间 O(n)。例如,我知道在 Python 3 中 d.keys() 返回一个迭代器,我猜测在 Python 3 中列表是从字典内部创建的。

是否可以在常数时间内从字典中选择一个值,即 O(1)?

编辑:到目前为止,对于 cmets,我认为这是不可能的,至少不是直接的方式。需要辅助结构。

编辑2:我认为字典可以在恒定时间内随机选择,因为它在内部是一个哈希表,即在内部它必须有一个数组或相似的东西。当然,这取决于内部实现,但理论上我认为是可能的。

【问题讨论】:

  • 您的用例到底是什么,标准解决方案(调用 dict.keys() 并从中选择一个随机值)对您来说性能不够好?
  • 我正在构建一个计算机模拟,所以我需要多次致电dict.keys()。我很确定它在 Python 3 中需要 O(n),因为 dict.keys() 返回一个迭代器。我不认为 Python 2 中的情况会更好,它可能会在内部转换字典。
  • 我认为对于您的用例,您最好使用带有增量“值”的预定义“前缀”分配键,即。 "A1", "A2", "B1", "B2"...,那么要随机访问密钥,您只需要从正确的前缀中随机获取,并从 " 的长度中获取 O(1) 的随机值价值”,我认为这是你想要的。
  • @Anzel,问题是字典中不断删除和添加,所以我无法保持一致的键。
  • 奇怪的是这个重复的stackoverflow.com/questions/10840901/… 有完全不同的答案。

标签: python dictionary random


【解决方案1】:

next(islice(d.values(),np.random.randint(0, len(d)-1),None)) 是我发现从 dict d 中选择随机值的最佳执行方法Python 3。这在下面的讨论中进行了解释。

一些标准库随机方法比可比较的 numpy.random 方法花费更多的运行时间。例如:

import numpy as np

timeit random.randint(0, 10)
100000 loops, best of 3: 2.52 µs per loop

timeit np.random.randint(0, 10)
1000000 loops, best of 3: 453 ns per loop

使用 numpy.random.randint 可以提高选择 dict 随机值的方法的运行时间:

from itertools import islice
import random

d = {1:'a',2:'b',3:'c',4:'d',5:'e',6:'f',7:'g',8:'h',9:'i',10:'j'}

timeit next(islice(d.values(),random.randint(0, len(d)-1),None))
100000 loops, best of 3: 3.58 µs per loop

timeit next(islice(d.values(),np.random.randint(0, len(d)-1),None))
100000 loops, best of 3: 1.26 µs per loop

# d[5] access time is about 25X smaller than 1.26 µs
timeit d[5]
10000000 loops, best of 3: 51.3 ns per loop

def take_nth(sequence, n):
    i = iter(sequence)
    for _ in range(n):
        next(i)
    return next(i)

timeit d[take_nth(d.keys(), random.randint(0, len(d)-1))]
100000 loops, best of 3: 5.07 µs per loop

timeit d[take_nth(d.keys(), np.random.randint(0, len(d)-1))]
100000 loops, best of 3: 2.66 µs per loop

【讨论】:

  • 你是怎么做到的,random.sample 似乎不接受字典:TypeError: Population must be a sequence or set. For dicts, use list(d).
  • 它适用于 Python 2,我已经确定了这方面的答案。我使用 Python 2,因为它广泛用于 numpy 和依赖它的库(例如 pandas),并使与现有脚本和教程的兼容性变得更容易。此外,在比较基准测试时,我注意到 Python 2 通常比 Python 3 运行得更快,有时甚至快 50%,例如参见 stackoverflow.com/questions/32781288/…
  • Python 3 和数字栈一样工作。无论如何,random.sample in 2 并没有做任何神奇的事情——在这种情况下,它只是在内部调用 list(population),因此与 OP 直接调用它没有什么不同。
  • @DSM,这就是我的想法。我还在旁边使用 numpy、scipy 来保留一些结构,我也将使用它们以更手动(即基于咖啡)的方式解决这个问题。不管怎么说,还是要谢谢你。至少,我确信今天没有对我来说免费的方法。
  • @DSM:无论出于何种原因,random.sample(d,1) 今天的运行速度比 random.sample(list(d),1) 快 14%,其中 d = {1: 'a ', 2: 'b', 3: 'c', 4: 'd', 5: 'e', 6: 'f', 7: 'g', 8: 'h', 9: 'i', 10:'j'},基于我在 Windows 7 x64 上使用 Anaconda Python 2.7.10 x64 进行的 timeit 测试。
【解决方案2】:

我认为,很明显,通过标准 dict 公共 API 这是不可能的。

但是,dict 有几个直接替代品,可按某种排序顺序提供对密钥的有效访问。然后可以对其进行索引以获得随机元素。尽管它们的理论渐近性与 dict 不同,但在实践中它们通常表现得一样好或更好。

Stutzbach Enterprises 的 blist 包提供了 blist.sorteddict,它经过专门测试与 dict 完全兼容。它提供了对其关键视图的索引,这是对数复杂度。它是用 B+Trees 实现的。

Grant Jenks 中的 SortedContainers 包提供了 sortedcontainers.SortedDict,它同样提供了对其关键视图的高效索引。

其他也可用,通常基于搜索树。

【讨论】:

  • 我不明白为什么有些人不赞成这个答案。这实际上可能是一个替代方案。文档说,当字典的大小最近没有改变时,对索引的访问是 O(1),否则是 O(log)。我的 dict 经常变化,但相对不那么频繁。它可能会起作用。
  • ... 或者可能没有,它有 O(log) 用于删除密钥,如果我考虑保持发辫的恒定时间,这可能不是最好的主意。另外,我忘记了我正在使用 networkx 实现,所以我必须保留两个结构。无论如何,谢谢!,我想有人可能会觉得这个想法很有用。
  • 两个结构是个问题,但我不会担心 log(n) 的删除。 O(1) 和 O(log) 之间的理论差异经常被其他考虑所淹没——这两个容器类的基准肯定与 dict 相当。
  • 我会同意非常大的结构,但问题是结构不是那么大(1000 个节点),所以保持树的常量可能真的会扼杀好处。不过,我可能会尝试将 blist 作为辅助结构,使用 O(log n) 删除和添加(O(1) 当大小变化不是最近时)可能是我最好的选择。
  • 涉及的许多因素(例如处理器缓存、哈希冲突等)意味着唯一可以确定的方法就是尝试一下。也许也可以试试 sortedcontainer 包:这里的基准使它看起来很有竞争力:grantjenks.com/docs/sortedcontainers/performance.html 特别是因为我怀疑固定开销较低(因为它基于列表)。
【解决方案3】:

在这种情况下,我只能想象一种(次要)优化:不要创建列表,只需获取一个随机数 r 并迭代 d.keys() 直到你得到 r-th 项。

def take_nth(sequence, n):
    i = iter(sequence)
    for _ in range(n):
        next(i)

    return next(i)

import random
rand_key = d[take_nth(d.keys(), random.randint(0, len(d)-1))]

这会给您带来更好的性能,因为您不必每次都迭代整个列表,但这仍然是个坏主意。

如果您想在固定字典上重复进行随机选择,而不是将其键缓存到单独的列表中并使用随机索引值对其进行索引。

UPD:

总结 cmets 中的讨论,以下具有前向/后向缓存和重用已删除项目的类可能会有所帮助:

import random

class RandomSampleDict(object):

    def __init__(self):
        self.data     = {}
        self.cache_ik = {}
        self.cache_ki = {}
        self.track    = []

    def lookup(self, key):
        return self.data[key]

    def set(self, key, value):
        self.data[key] = value

    def add(self, key, value):
        self.data[key] = value
        if len(self.track) == 0:
            i = len(self.data) - 1
        else:
            i = self.track.pop()

        self.cache_ik[i] = key
        self.cache_ki[key] = i

    def delete(self, key):
        del self.data[key]
        i = self.cache_ik[i]
        del self.data_ik[i]
        del self.data_ki[key]

        self.track.append(i)

    def random_sample_key(self):
        key = None
        while key is None:
            i = random.randint(0, len(self.data))
            if i in self.cache_ik:
                return self.cache_ik[i]

【讨论】:

  • 忘记了结束括号,应该是 d[take_nth(d.keys(), random.randint(0, len(d)-1))]
  • @firegurafiku,我想这会给我带来更好的平均表现。谢谢。我现在就试试这个。我正在考虑在旁边维护一个元组或一个列表,但显然删除元素是 O(n) (wiki.python.org/moin/TimeComplexity),所以我可能会以最糟糕的位置结束。显然标准 Python 中没有链表 (stackoverflow.com/questions/6153348/…)
  • @toto_tico:您不必从缓存列表中删除项,只需在删除时将它们标记None并重复@ 987654330@ 如有需要。如果您最终不必删除大部分项目,这将起作用。
  • @toto_tico:我的意思类似于以下内容(只是为了说明一个想法,未经测试):gist.github.com/firegurafiku/1a10e7335e2e81e46883
  • @toto_tico 另一种方法可能是在纯 Python 中简单地推出自己的哈希表实现。如果您的哈希值足够大,由于复制消除,它们的性能将优于内置字典。
【解决方案4】:

假设仅使用 Python dict 无法做到这一点并且需要第二个数据结构,那么这里有一个廉价且高效的辅助数据结构,它只跟踪当前节点。

它只是将节点保存在列表中。为了支持删除,它只是清空该位置并保留另一个可用空间列表。

请注意,如果您只是随机删除节点,那么这很好。如果要删除通过其他方法选择的节点,则需要将序列号存储在节点中,以便找到要删除的节点。

除非您遇到随机采样变慢时节点列表大部分为空的情况,否则它运行良好。如果您需要处理这种情况,那么您需要在此时重新分配列表 - 这可以作为摊销成本,但会增加相当多的复杂性。例如,您需要添加一个从节点到序列号的字典,并在重新分配节点列表时更新它。

import random
RNG = random.Random()

class Tracker(object):

    def __init__(self):
        self.free = []
        self.nodes = []

    def add(self,node):
        if self.free:
            seq_num = self.free.pop()
            self.nodes[seq_num] = node
        else:
            seq_num = len(self.nodes)
            self.nodes.append(node)

    def random_node(self):
        seq_num = RNG.randint(0,len(self.nodes)-1)
        while self.nodes[seq_num] == None:
            seq_num = RNG.randint(0,len(self.nodes)-1)
        return self.nodes[seq_num],seq_num

    def delete(self,seq_num):
        self.nodes[seq_num] = None
        self.free.append(seq_num)

    def delete_random_node(self):
        node,seq_num = self.random_node()
        self.delete(seq_num)
        return node

这里可能有一些小的优化。用collections.deque 替换空闲列表可能会更快一些,因为如果它们的大小变化太频繁,列表会减慢一点。但这没什么大不了的。我认为您的节​​点列表将达到一个平衡大小,然后变得非常高效,但是您可以用 Nones 来填充它,以避免重复增长的启动成本。你可以做一些常见的子表达式消除。但所有这些都只会产生很小的影响。

【讨论】:

  • 这绝对值得一试。我的问题是我需要随机选择多个节点(例如 random.sample 而不是 random.choice)。模拟设置为 2.5% 的节点,但它是分析的因素之一。模拟有很多细节,如果我试图解释所有细节,我会混淆这个问题。详情请发文章here
  • 我认为您可以拨打random_node 正确的次数并检查是否有欺骗行为。节点数为len(self.nodes) - len(self.free)。除非有太多空闲插槽,否则这将正常工作。
猜你喜欢
  • 2016-04-14
  • 2015-01-09
  • 2015-05-25
  • 1970-01-01
  • 2013-09-16
  • 1970-01-01
  • 2017-03-30
  • 2020-04-06
  • 1970-01-01
相关资源
最近更新 更多