【问题标题】:Python set with the ability to pop a random element具有弹出随机元素的能力的 Python 集
【发布时间】:2012-09-25 16:37:51
【问题描述】:

我需要一个功能类似于集合(快速插入、删除和成员资格检查)但能够返回随机值的 Python (2.7) 对象。以前在 stackoverflow 上提出的问题的答案如下:

import random
random.sample(mySet, 1)

但这对于大型集合来说非常慢(它在 O(n) 时间内运行)。

其他解决方案不够随机(它们依赖于 python 集合的内部表示,这会产生一些非常非随机的结果):

for e in mySet:
    break
# e is now an element from mySet

我编写了自己的基本类,它具有恒定的时间查找、删除和随机值。

class randomSet:
    def __init__(self):
        self.dict = {}
        self.list = []

    def add(self, item):
        if item not in self.dict:
            self.dict[item] = len(self.list)
            self.list.append(item)

    def addIterable(self, item):
        for a in item:
            self.add(a)

    def delete(self, item):
        if item in self.dict:
            index = self.dict[item]
            if index == len(self.list)-1:
                del self.dict[self.list[index]]
                del self.list[index]
            else:
                self.list[index] = self.list.pop()
                self.dict[self.list[index]] = index
                del self.dict[item]

    def getRandom(self):
        if self.list:
            return self.list[random.randomint(0,len(self.list)-1)]

    def popRandom(self):
        if self.list:
            index = random.randint(0,len(self.list)-1)
            if index == len(self.list)-1:
                del self.dict[self.list[index]]
                return self.list.pop()
            returnValue = self.list[index]
            self.list[index] = self.list.pop()
            self.dict[self.list[index]] = index
            del self.dict[returnValue]
            return returnValue

有没有更好的实现,或者对这段代码有什么大的改进?

【问题讨论】:

  • 当你需要集合操作的东西时,为什么不直接使用列表并将其转换为集合呢? ...
  • 这允许您在不影响性能的情况下交叉添加元素和选择元素。你的现实生活场景真的遵循这种模式吗?如果您可以预先添加所有元素,则可以从一个集合开始,然后在抓取随机元素之前转换为一个列表。
  • 因为对于大型列表来说这非常慢(在 O(n) 时间内运行)。
  • @GrantS:你的意思是创建列表很慢?那只需要做一次。这并不比在课堂上创建列表和字典慢。
  • @GrantS:如果您正在使用 Python 2.x,为了更大的利益:从“对象”或其他新样式库继承。不要使用旧式类(通过不声明超类) - 难以调试的问题可能会严重伤害您

标签: python random set


【解决方案1】:

我认为最好的方法是在collections 中使用MutableSet 抽象基类。继承MutableSet,然后定义adddiscard__len__,__iter____contains__;还重写__init__ 以可选地接受一个序列,就像set 构造函数一样。 MutableSet 提供基于这些方法的所有其他 set 方法的内置定义。这样您就可以廉价地获得完整的set 界面。 (如果你这样做,addIterable 会为你定义,名称为 extend。)

标准set 接口中的discard 似乎就是您在此处所说的delete。所以将delete 重命名为discard。此外,您可以像这样定义popRandom,而不是使用单独的popRandom 方法:

def popRandom(self):
    item = self.getRandom()
    self.discard(item)
    return item

这样您就不必维护两个单独的项目删除方法。

最后,在您的项目删除方法中(delete 现在,discard 根据标准集接口),您不需要 if 语句。无需测试是否为index == len(self.list) - 1,只需将列表中的最后一项与要弹出的列表索引处的项交换,并对反向索引字典进行必要的更改。然后从列表中弹出最后一项并将其从字典中删除。无论index == len(self.list) - 1 与否,这都有效:

def discard(self, item):
    if item in self.dict:
        index = self.dict[item]
        self.list[index], self.list[-1] = self.list[-1], self.list[index]
        self.dict[self.list[index]] = index
        del self.list[-1]                    # or in one line:
        del self.dict[item]                  # del self.dict[self.list.pop()]

【讨论】:

  • 如果可以的话+2。用这么少的钱就能搞定一个界面真是太好了。以及关于简化实施的优秀建议。
  • 如果这是线程开启器使用的解决方案,我会非常有兴趣查看此解决方案的 T(setsize) 图表和默认集上的 O(N) 查找解决方案,使用T 是查找所需的时间。
【解决方案2】:

您可以采取的一种方法是从set 派生一个新类,该类用派生自int 的类型的随机对象来加盐。

然后您可以使用pop 选择一个随机元素,如果它不是salt 类型,则重新插入并返回它,但如果它是salt 类型,则插入一个新的随机生成的salt 对象(并弹出以选择一个新对象)。

这将倾向于改变选择对象的顺序。平均而言,尝试次数将取决于加盐元素的比例,即摊销 O(k) 性能。

【讨论】:

  • +1,这个想法,但我想知道是否测试过某种形式。我不确定盐渍套装是否会那么有效。
  • @jsbueno 已知插入顺序会影响集合中的迭代顺序,但是是的,我想这也取决于所使用的散列方案的细节。
【解决方案3】:

我们不能实现一个继承自set 的新类,并进行一些(骇人听闻的)修改,使我们能够以 O(1) 的查找时间从列表中检索随机元素吗?顺便说一句,在 Python 2.x 上,您应该从 object 继承,即使用 class randomSet(object)PEP8 也值得你考虑 :-)

编辑: 为了了解一些骇人听闻的解决方案可能具有的功能,该线程值得一读: http://python.6.n6.nabble.com/Get-item-from-set-td1530758.html

【讨论】:

  • 如果你的类没有其他基类并且你没有使用python3,你应该总是显式地从对象继承:)
  • 我为什么要从 set 继承?唯一可行的方法是使用 dict 指向列表中值的索引。它根本不使用集合,所以我看不出从集合继承会有什么帮助。编辑:等等,我可能会明白你在说什么。我在这里完全没有必要使用 dict 吗?
  • 没有。从set 继承将是低效的,因为random.sample 需要遍历整个集合使其成为 O(n)。
  • @GrantS:我并不是说你应该让你的randomSet 继承自set。我正在考虑基于set 的完全不同的解决方案,但以某种方式破解set 以访问随机项目。现在,我发现如果不迭代集合,就没有明显的方法可以做到这一点。
  • @Jan-Philip Gehrcke 确实如此。我可以只用 setlist 来完成,但这会使删除在 O(n) 时间内运行(这就是为什么需要 dict 的原因)。
【解决方案4】:

这是一个从头开始的解决方案,它在恒定时间内添加和弹出。我还包括了一些额外的集合函数用于演示目的。

from random import randint


class RandomSet(object):
  """
  Implements a set in which elements can be
  added and drawn uniformly and randomly in
  constant time.
  """

  def __init__(self, seq=None):
    self.dict = {}
    self.list = []
    if seq is not None:
      for x in seq:
        self.add(x)

  def add(self, x):
    if x not in self.dict:
      self.dict[x] = len(self.list)
      self.list.append(x)

  def pop(self, x=None):
    if x is None:
      i = randint(0,len(self.list)-1)
      x = self.list[i]
    else:
      i = self.dict[x]
    self.list[i] = self.list[-1]
    self.dict[self.list[-1]] = i
    self.list.pop()
    self.dict.pop(x)
    return x

  def __contains__(self, x):
    return x in self.dict

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

  def __repr__(self):
    return "{" + ", ".join(str(x) for x in self.list) + "}"

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

【讨论】:

  • pop方法出错:if x != None, i is not defined
【解决方案5】:

是的,我会以与您大致相同的方式实现“有序集” - 并使用列表作为内部数据结构。

但是,我会直接从“set”继承,并且只跟踪添加的项目 内部列表(就像你做的那样) - 留下我不单独使用的方法。

可能会添加一个“同步”方法来在更新集合时更新内部列表 通过特定于集合的操作,例如 *_update 方法。

如果使用“有序字典”不涵盖您的用例。 (我刚刚发现尝试将 ordered_dict 键转换为常规集合并没有优化,因此如果您需要对数据进行集合操作,这不是一个选项)

【讨论】:

    【解决方案6】:

    如果您不介意只支持可比较的元素,那么您可以使用blist.sortedset

    【讨论】:

      猜你喜欢
      • 2017-12-11
      • 2011-11-08
      • 2017-11-20
      • 2017-08-03
      • 1970-01-01
      • 1970-01-01
      • 2021-12-22
      • 2019-08-25
      • 1970-01-01
      相关资源
      最近更新 更多