【问题标题】:Get random elements from array in Swift在 Swift 中从数组中获取随机元素
【发布时间】:2015-01-31 06:50:01
【问题描述】:

我有一个类似的数组:

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

我想从该数组中获取 3 个随机元素。我来自 C#,但很快我不确定从哪里开始。例如,我认为我应该先对数组进行洗牌,然后从中挑选前 3 个项目?

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

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

但它会在“shuffle()”的位置显示“'()' is not convertible to '[Int]'”。

为了挑选我使用的一些元素:

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

目前看起来不错。

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

【问题讨论】:

  • 查看stackoverflow.com/questions/24026510/… 以获得更好的随机播放方法。
  • 谢谢,我现在使用公认答案的变异扩展方法进行改组。
  • 是的,有更好/更优雅的解决方案:完全洗牌不是最佳选择,就好像你需要 10 个随机元素中的 4 个一样,一个一个选择只需要 4 个arc4random_uniform,但完全洗牌需要 9 arc4random_uniform
  • 使用sort 像那样洗牌是行不通的。排序故意尽可能少地进行比较,当然这还不足以实现像样的洗牌。

标签: arrays swift random shuffle


【解决方案1】:

Xcode 11 • Swift 5.1

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

游乐场测试

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。
  • 感谢您的及时回复...但我认为您删除了 Int 扩展,因为 indexRandom() 方法会出错。
  • 不错!我必须把它给你,你想出了一些非常好的扩展。
  • 嗨,使用这个扩展是否从数组中随机选择。例如,我有一个超过 40 多个心率读数的数组,我只想将其变为 40,但我不希望它们出现故障,所以我可以将它们绘制在图表中,我可以使用 @987654326 @ 按它们所在的顺序获取 40 个值?或者我有什么办法可以做到这一点?在此先感谢〜库尔特
  • 我不确定我是否理解您的问题,但看起来您只需将元素洗牌一次,然后它们将随机分组为 n (40) 个元素 stackoverflow.com/a/48089097/2303865
【解决方案2】:

或者有人对此有更好/更优雅的解决方案吗?

我愿意。在算法上比公认的答案更好,它为完全随机播放执行 count-1 arc4random_uniform 操作,我们可以简单地在 n arc4random_uniform 操作中选择 n 个值。 p>

实际上,我有 两种 方法比公认的答案做得更好:

更好的解决方案

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 版本并不能解决问题)。但这是另一个问题。
【解决方案3】:

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.")
}

【讨论】:

    【解决方案4】:

    您可以在 Array 上定义扩展:

    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)
    

    【讨论】:

      【解决方案5】:

      您也可以使用 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()
      

      这只是一个示例,您还可以在函数中包含逻辑来为每个函数获取不同的名称。

      【讨论】:

      • (a) 你会在那里得到模偏差;你应该使用 arc4random_uniform; (b) 我认为这可能会在 32 位架构上崩溃大约一半的时间,因为您将 UInt32 的值(arc4random() 的返回值)放入(有符号,32 位)Int ,导致一个负数组索引; (c) 这可能会导致,例如,“Peter”、“Steve”、“Peter”,因为没有代码可以避免重复选择同一项目。
      • 是的,感谢您的回答,但这并不能确保只返回一次元素(我没有直接说明,但这是我正在寻找的)
      猜你喜欢
      • 1970-01-01
      • 2018-05-03
      • 1970-01-01
      • 1970-01-01
      • 2019-12-10
      • 2014-12-19
      • 2023-04-06
      • 2011-11-01
      • 1970-01-01
      相关资源
      最近更新 更多