在Swift中从数组中获取随机元素

30

我有一个像这样的数组:

var names: String = [ "Peter", "Steve", "Max", "Sandra", "Roman", "Julia" ]

我想从该数组中随机获取3个元素。我来自C#,但在Swift中,我不确定应该从哪里开始。我认为我应该先打乱数组,然后选择其中的前3个项目。

我尝试使用以下扩展对其进行洗牌:

extension Array
{
    mutating func shuffle()
    {
        for _ in 0..<10
        {
            sort { (_,_) in arc4random() < arc4random() }
        }
    }
}

但是它接着说在"shuffle()"位置"'()'不可转换为'[Int]'"。

对于选择元素数量,我使用:

var randomPicks = names[0..<4];

目前看起来不错。

如何洗牌?还是有人对此有更好/更优雅的解决方案吗?


5
请参考 https://dev59.com/yWAg5IYBdhLWcg3wDHU4 获取更好的数组随机排序方法。 - Martin R
1
谢谢,我现在使用被接受回答的可变扩展方法进行洗牌。 - Patric
2
是的,有更好/更优雅的解决方案:完全洗牌并不是最佳选择,因为如果你需要从10个元素中选择4个随机元素,则逐个选择这些元素仅需4个 arc4random_uniform,但完全洗牌则需要9个 arc4random_uniform - Cœur
使用 sort 来洗牌是行不通的。排序有意地尽可能少地进行比较,肯定不足以实现一个好的洗牌。 - Alexander
6个回答

64

Xcode 11 • Swift 5.1

extension Collection {
    func choose(_ n: Int) -> ArraySlice<Element> { shuffled().prefix(n) }
}

Playground测试

var alphabet = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"]
let shuffledAlphabet = alphabet.shuffled()  // "O", "X", "L", "D", "N", "K", "R", "E", "S", "Z", "I", "T", "H", "C", "U", "B", "W", "M", "Q", "Y", "V", "A", "G", "P", "F", "J"]
let letter = alphabet.randomElement()  // "D"
var numbers = Array(0...9)
let shuffledNumbers = numbers.shuffled()
shuffledNumbers                              // [8, 9, 3, 6, 0, 1, 4, 2, 5, 7]
numbers            // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
numbers.shuffle() // mutate it  [6, 0, 2, 3, 9, 1, 5, 7, 4, 8]
numbers            // [6, 0, 2, 3, 9, 1, 5, 7, 4, 8]
let pick3numbers = numbers.choose(3)  // [8, 9, 2]

extension RangeReplaceableCollection {
    /// Returns a new Collection shuffled
    var shuffled: Self { .init(shuffled()) }
    /// Shuffles this Collection in place
    @discardableResult
    mutating func shuffledInPlace() -> Self  {
        self = shuffled
        return self
    }
    func choose(_ n: Int) -> SubSequence { shuffled.prefix(n) }
}

var alphabetString = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
let shuffledAlphabetString = alphabetString.shuffled  // "DRGXNSJLFQHPUZTBKVMYAWEICO"
let character = alphabetString.randomElement()  // "K"
alphabetString.shuffledInPlace() // mutate it  "WYQVBLGZKPFUJTHOXERADMCINS"
alphabetString            // "WYQVBLGZKPFUJTHOXERADMCINS"
let pick3Characters = alphabetString.choose(3)  // "VYA"

当我尝试使用indexRandom()方法时,它会引发编译错误,指出find()不可用。请告诉我是否有任何您忘记提到的自定义方法...我正在使用Xcode 7。 - DShah
感谢您的迅速回复...但我认为您删除了Int扩展,因此indexRandom()方法会出错。 - DShah
1
不错!我得承认,你想出了一些非常好的扩展。 - Lance Samaria
你好,使用这个扩展程序是否会从数组中随机选择。比如说,我有一个包括40多次心率读数的数组,我只想提取其中的40个,但是我不希望它们的顺序被打乱,这样我可以在图表中呈现出来。我可以使用var fortyReadings = heartratereading.choose(40)来获取40个按原来顺序排列的数值吗?或者,还有其他方法能够实现这个功能吗?提前感谢您的回答!~ Kurt - Kurt L.
@Joannes 当然。对于任何集合子序列都是如此。如果你得到了一个 Substring 并且需要一个 String,你需要初始化一个新的字符串。 - Leo Dabus
显示剩余2条评论

25

还是有更好/更优雅的解决方案吗?

我有。在算法上比被接受的答案更好,因为对于完整的洗牌,它只进行了计数 -1 arc4random_uniform 操作,我们只需在 n arc4random_uniform 操作中选择 n 个值即可。

实际上,我有两种比被接受的答案更好的方法:

更好的解决方案

extension Array {
    /// Picks `n` random elements (straightforward approach)
    subscript (randomPick n: Int) -> [Element] {
        var indices = [Int](0..<count)
        var randoms = [Int]()
        for _ in 0..<n {
            randoms.append(indices.remove(at: Int(arc4random_uniform(UInt32(indices.count)))))
        }
        return randoms.map { self[$0] }
    }
}

最佳解决方案

以下解决方案比之前的方案快两倍。

适用于 Swift 3.0 和 3.1

extension Array {
    /// Picks `n` random elements (partial Fisher-Yates shuffle approach)
    subscript (randomPick n: Int) -> [Element] {
        var copy = self
        for i in stride(from: count - 1, to: count - n - 1, by: -1) {
            let j = Int(arc4random_uniform(UInt32(i + 1)))
            if j != i {
                swap(&copy[i], &copy[j])
            }
        }
        return Array(copy.suffix(n))
    }
}

适用于Swift 3.2和4.x

extension Array {
    /// Picks `n` random elements (partial Fisher-Yates shuffle approach)
    subscript (randomPick n: Int) -> [Element] {
        var copy = self
        for i in stride(from: count - 1, to: count - n - 1, by: -1) {
            copy.swapAt(i, Int(arc4random_uniform(UInt32(i + 1))))
        }
        return Array(copy.suffix(n))
    }
}

使用方法:

let digits = Array(0...9)  // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
let pick3digits = digits[randomPick: 3]  // [8, 9, 0]

非常感谢您的算法!我尝试了修复,效果非常好。此外,我一直以为我在运行Swift 3.2(Xcode 8.3.3),但是Swift版本标志只让我选择“Swift 3”和“未指定”。然而,在终端中快速输入swift --version后,我发现我使用的是令人讨厌的Swift 3.1版本。我得找出如何强制使用Swift 3.2(转换到最新的Swift版本并没有起作用)。但这是另一个问题。 - H4Hugo

5

你可以在数组上定义一个扩展:

extension Array {
    func pick(_ n: Int) -> [Element] {
        guard count >= n else {
            fatalError("The count has to be at least \(n)")
        }
        guard n >= 0 else {
            fatalError("The number of elements to be picked must be positive")
        }

        let shuffledIndices = indices.shuffled().prefix(upTo: n)
        return shuffledIndices.map {self[$0]}
    }
}

[ "Peter", "Steve", "Max", "Sandra", "Roman", "Julia" ].pick(3)

如果初始数组可能具有重复项,并且您希望值是唯一的:
extension Array where Element: Hashable {
    func pickUniqueInValue(_ n: Int) -> [Element] {
        let set: Set<Element> = Set(self)
        guard set.count >= n else {
            fatalError("The array has to have at least \(n) unique values")
        }
        guard n >= 0 else {
            fatalError("The number of elements to be picked must be positive")
        }

        return Array(set.prefix(upTo: set.index(set.startIndex, offsetBy: n)))
    }
}

[ "Peter", "Steve", "Max", "Sandra", "Roman", "Julia" ].pickUniqueInValue(3)

不管是编写者还是用户,崩溃都不太友好。如果数组元素数量不足,最好返回 nil。 - Cristik

4

Swift 4.1及以下版本

let playlist = ["Nothing Else Matters", "Stairway to Heaven", "I Want to Break Free", "Yesterday"]
let index = Int(arc4random_uniform(UInt32(playlist.count)))
let song = playlist[index]

Swift 4.2及以上版本

if let song = playlist.randomElement() {
  print(song)
} else {
  print("Empty playlist.")
}

3
您可以使用shuffle()方法并从洗牌后的数组中选择前3个项目,以获取原始数组中的3个随机元素:

Xcode 14 • Swift 5.7

    var names: String = [ "Peter", "Steve", "Max", "Sandra", "Roman", "Julia" ]
    let shuffledNameArray = names.shuffled()
    let randomNames = Array(shuffledNameArray.prefix(3))
    print(randomNames)

1
你也可以使用arc4random()从数组中选择三个元素。就像这样:
extension Array {
    func getRandomElements() -> (T, T, T) {
        return (self[Int(arc4random()) % Int(count)],
                self[Int(arc4random()) % Int(count)],
                self[Int(arc4random()) % Int(count)])
    }
}

let names = ["Peter", "Steve", "Max", "Sandra", "Roman", "Julia"]
names.getRandomElements()

这只是一个示例,您还可以在函数中添加逻辑,以获取每个名称的不同版本。

1
(a) 你会遇到模数偏差的问题,应该使用arc4random_uniform; (b) 我认为在32位架构上,这可能会有一半的时间崩溃,因为你将UInt32的值(从arc4random()返回)放入了一个(带符号的,32位的)Int中,导致负数组索引; (c) 这可能会导致例如“Peter”,“Steve”,“Peter”等结果,因为没有代码来避免重复选择同一项。 - Matt Gibson
1
是的,谢谢你的回答,但这并不能确保只返回元素一次(虽然我没有直接说明,但这正是我要寻找的)。 - Patric

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