【问题标题】:How to efficiently select a random element from a Scala immutable HashSet如何有效地从 Scala 不可变 HashSet 中选择随机元素
【发布时间】:2015-04-22 15:32:55
【问题描述】:

我有一个scala.collection.immutable.HashSet,我想从中随机选择一个元素。

我可以用这样的扩展方法解决这个问题:

implicit class HashSetExtensions[T](h: HashSet[T]) {
  def nextRandomElement (): Option[T] = {
    val list = h.toList
    list match {
      case null | Nil => None
      case _ => Some (list (Random.nextInt (list.length)))
    }
  }
}

...但是转换为列表会很慢。什么是最有效的解决方案?

【问题讨论】:

  • 你用的是mutable.HashSet还是immutable.HashSet
  • 我怀疑直接在 Set 上使用 Iterator,并在 0 和 set 大小之间随机推进它可能比转换为 List 更好,但我不知道是什么大小或迭代器的实现在 HashSet 上,所以我不确定。
  • 实际上我用Random.shuffle(h).headOption 提出的解决方案是完全错误的,它总是返回相同的结果

标签: scala scala-collections


【解决方案1】:

警告 此答案仅供实验使用。对于实际项目,您可能应该使用自己的集合类型。

所以我对HashSet source 做了一些研究,我认为几乎没有机会在不违反包的情况下提取最有价值的class HashTrieSet 的内部结构。

我确实想出了这段代码,它扩展了Ben Reich's solution

package scala.collection

import scala.collection.immutable.HashSet
import scala.util.Random

package object random {
  implicit class HashSetRandom[T](set: HashSet[T]) {
    def randomElem: Option[T] = set match {
      case trie: HashSet.HashTrieSet[T] => {
        trie.elems(Random.nextInt(trie.elems.length)).randomElem
      }
      case _ => Some(set.size) collect {
        case size if size > 0 => set.iterator.drop(Random.nextInt(size)).next
      }
    }
  }
}

文件应在src/scala/collection/random 文件夹中的某处创建

注意scala.collection 包 - 这个东西使HashTrieSetelems 部分可见。这是我能想到的唯一解决方案,它可能比O(n) 运行得更好。当前版本应该具有复杂性 O(ln(n))immutable.HashSet 的任何操作一样。

另一个警告 - HashSet 的私​​有结构不是 scala 标准库 API 的一部分,因此它可能会更改任何版本,从而导致此代码错误(尽管自 2.8 以来它没有更改)

【讨论】:

  • 可能过头了,但很聪明。你也可以对可变的HashSet 做类似的事情(如果你可以公开内部哈希表数组,甚至可以在恒定时间内得到那个)。另外,如果我们无论如何都要过顶,您可能可以在这里重新配置一些东西,使您的方法在这里尾递归!
  • @BenReich 这里不需要提供尾递归,因为它不会被调用超过2*log_32(n) 次。我们可以尝试对其进行优化,使其仅调用一次Random.nextInt,因为多次调用不仅代价高昂,而且会在不完全平衡的树的叶子上提供非均匀分布。
【解决方案2】:

由于sizeHashSet 上是O(1),而iterator 尽可能地懒惰,我认为这种解决方案会相对高效:

implicit class RichHashSet[T](val h: HashSet[T]) extends AnyVal {
    def nextRandom: Option[T] = Some(h.size) collect {
        case size if size > 0 => h.iterator.drop(Random.nextInt(size)).next
    }
}

如果您想获得每一盎司的效率,您可以在此处使用match,而不是此处使用的更简洁的Some/collect

您可以查看mutable HashSet 实现以查看size 方法。那里定义的iterator 方法基本上只是在FlatHashTable 上调用iterator。如果您正在使用这些方法,那么这些方法的基本效率同样适用于 immutable HashSet。作为比较,您可以看到HashSet 上的toList 实现一直位于TraversableOnce 的类型层次结构中,并且使用了更多可能效率较低的原始元素,并且(当然)必须迭代整个集合生成List。如果您将整个集合转换为Traversable 集合,则应使用具有恒定时间查找的ArrayVector

您可能还注意到,在上述方法中 HashSet 并没有什么特别之处,如果您这样选择,您可以改为丰富 Set[T](尽管不能保证这在其他 @ 上同样有效) 987654349@ 实现,当然)。

附带说明,在为扩展方法实现丰富类时,您应该始终考虑通过扩展AnyVal 来创建一个隐式的、用户定义的值类。您可以在docsthis answer 中了解一些优点和限制。

【讨论】:

  • Iterator.drop 将遍历集合的整个丢弃部分。它的复杂度真的是 O(n)
  • 对——我并不是说整个方法是O(1),只是说sizeO(1)。这仍然是 O(n),但具有更好的平均和最坏情况。
  • 这绝对比构建新集合要好,但我在 HashSet.scala 中寻找如何破解 HashTrieSet 的部分来制作这个 O(log(n))
  • 如果你扩展了HashSet,你可以暴露内部哈希表(这是一个Array),然后在恒定时间内选择一个随机元素!虽然这可能是不明智的。
  • 问题是我们不能暴露。这是scala.immutable.collection 的私有内容,不受保护。所以我们唯一能做的就是在 collection 包中定义新类,这完全是垃圾
猜你喜欢
  • 1970-01-01
  • 2012-08-30
  • 2010-09-08
  • 1970-01-01
  • 1970-01-01
  • 2011-06-30
  • 2012-01-07
  • 1970-01-01
  • 2011-02-01
相关资源
最近更新 更多