如何在Swift中从一个集合中获取随机元素?

15

自Swift 1.2起,苹果公司引入了Set集合类型。

比如说,我有一个集合:

var set = Set<Int>(arrayLiteral: 1, 2, 3, 4, 5)
现在我想从中获取一个随机元素。问题是如何做到?Set不像Array那样提供subscript(Int),而是提供了subscript(SetIndex<T>)。但首先,SetIndex<T>没有可访问的初始化程序(因此,我无法只使用所需的偏移量创建索引),其次即使我可以得到集合中第一个元素的索引(var startIndex = set.startIndex),那么我唯一能够获得第N个索引的方法是通过连续调用successor()

因此,目前我只能看到两种选择,都很丑陋和昂贵:

  • 将集合转换为数组(var array = [Int](set)),然后使用其下标(该下标完美地接受Int);或者
  • 获取集合中第一个元素的索引,遍历successor()方法链以获取第N个索引,然后通过集合的下标读取相应的元素。

我错过了其他的方法吗?

6个回答

13

从Swift 4.2开始,您可以使用randomElement

let random = set.randomElement()

8

可能最好的方法是使用advance,它可以为您遍历successor

func randomElementIndex<T>(s: Set<T>) -> T {
    let n = Int(arc4random_uniform(UInt32(s.count)))
    let i = advance(s.startIndex, n)
    return s[i]
}

你也可以遍历集合,而不是索引(这是我的第一反应,但是我想起了 advance
func randomElement<T>(s: Set<T>) -> T {
    let n = Int(arc4random_uniform(UInt32(s.count)))
    for (i, e) in enumerate(s) {
        if i == n { return e }
    }
    fatalError("The above loop must succeed")
}

是的。然而,Set中的advance()复杂度为O(N),这是有趣的部分。因此,在技术上,它与遍历successor()链相同,只是看起来更简洁和意图明确。我在Apple开发者论坛上提出了类似的问题,并引起了工作人员的关注=>也许在Swift的某个未来版本中会有更好的工具来实现我想要的功能。目前,我可能会继续使用advance()。我将使用的集合不是很大。 - 0x416e746f6e
高级方法在Xcode 7 Beta 6中不再编译。 - Justin Lewis
2
Justin Lewis,唯一改变的是现在必须在索引上调用advancedBy(_:)extension Set { func randomElement() -> Element { let n = Int(arc4random_uniform(UInt32(count))); let i = startIndex.advancedBy(n); return self[i];} } - griotspeak
对于Swift 2,advance已不再可用。将第3行更改为let i = s.startIndex.advancedBy(n) - tebs1200
在 Swift 2 中的一个小改动:要枚举一个序列,你必须调用 s.enumerate()。 - jerrygdm

5

在Swift 3中

extension Set {
    public func randomObject() -> Element? {
        let n = Int(arc4random_uniform(UInt32(self.count)))
        let index = self.index(self.startIndex, offsetBy: n)
        return self.count > 0 ? self[index] : nil
    }
}

可爱。谢谢! - Ali Parr

4
extension Set {
    func randomElement() -> Element? {
        return count == 0 ? nil : self[advance(self.startIndex, Int(arc4random()) % count)]
    }
}

2

根据上述有关Swift更新的评论,针对Set进行了一个扩展的微小更改:

func randomElement() -> Element?
{
    let randomInt = Int(arc4random_uniform(UInt32(self.count)))
    let index = startIndex.advancedBy(randomInt)
    return count == 0 ? nil: self[index]
}

1
如果您想从一个Set中获取一个“随机”元素,则可以使用以下代码:
/// A member of the set, or `nil` if the set is empty.
var first: T? { get }

获取第0个索引或第1,000,000个索引都没有区别 - 它们都是一个任意对象。
但是,如果您希望重复调用每次返回一个可能不同的元素,则first可能不适合。

当然,你怎么可能知道 .first. 与 '从第 N-1 步走到第 N 步' 不同呢?Swift 是否保证遍历顺序随时间恒定?有些语言不会这样做(因为 Set 的哈希值取决于内存地址,在垃圾回收系统中内存地址会发生变化)。 - GoZoner
因为哈希是确定性的。给定相同的输入,每次运行都会得到相同的结果。这就像xkcd算法一样。它是“随机”的,但每次都会重复。事实上,这并不意味着在实际程序中不会发生。试试看。在[10,20,30,40,50]的集合中,我发现first总是50。 - Rob Napier
1
单纯的经验证据并不能证明Set遍历是确定性的。 - GoZoner
2
不要完全相信它,你不能依赖它(你对此很重视是正确的)。但是,任何依赖于它具有确定性的程序(即用户期望在不同运行期间得到不同答案的程序)都会非常失望。Go语言在这方面很有意思;它实际上保证该迭代映射(字典)时以随机顺序进行,明确防止您依赖某个您认为已经保证的顺序。但是,在Swift中,使用"first"很聪明,但不太可能匹配用户对任何使用情况的期望。 - Rob Napier
1
@GoZoner - 而且,轶事证据不能证明.first返回的集合元素确实是随机的。一个“任意”的对象并不等于一个随机对象。 - daver

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接