如何在Swift中打乱数组:优化与性能

面向初学者的在线Python训练器

轻松学习Python,无需理论负担。在浏览器中直接解决带自动检查的实践任务,无需安装任何东西。

开始课程

如何正确打乱Swift数组:从新手到专家

\n

您是否曾想过在应用中创建随机播放列表?或者,您可能需要打乱测验中的问题,使它们每次都以不同顺序出现?如果您使用Swift,迟早会遇到这样的任务:如何打乱数组(shuffle array)。这个问题在Stack Overflow上获得了数千次浏览,这并非偶然——其中的陷阱比表面看起来要多得多。

\n

在本文中,我们不仅会展示如何调用shuffle()。我们将深入底层:分析算法、比较性能、学习如何进行优化打乱,并理解何时标准方法是灾难,何时又是人类最好的发明。开始吧!

\n\n

为什么简单的"random"是个坏主意?

\n

假设您有一个数组:

\n
var cards = ["Ace", "King", "Queen", "Jack"]
\n

最天真的方法是创建一个新数组,并将元素插入到随机位置。看起来很有道理,对吧?

\n
func naiveShuffle(_ array: [String]) -> [String] {\n    var shuffled = [String]()\n    for element in array {\n        let randomIndex = Int.random(in: 0...shuffled.count)\n        shuffled.insert(element, at: randomIndex)\n    }\n    return shuffled\n}
\n

对于小数组(4-10个元素),这没问题。但一旦元素数量超过一百,您就会感到痛苦。为什么?因为insert(at:)的时间复杂度是O(n)——每次将元素插入到开头或中间时,所有后续元素都会移动。最终复杂度:O(n²)。对于1000个元素的数组,这是500,000次操作。对于10,000个元素,已经是5000万次。应用会开始"卡顿",用户会离开,评分会下降。不要这样做。

\n\n

内置shuffle()方法:您的新好朋友

\n

从Swift 4.2开始,我们有了原生方法shuffle()(用于可变数组)和shuffled()(返回新数组)。使用方法如下:

\n
var numbers = [1, 2, 3, 4, 5]\nnumbers.shuffle()  // 修改原数组\nprint(numbers)     // 例如:[3, 1, 5, 2, 4]\n\nlet shuffledNumbers = numbers.shuffled()  // 新数组\nprint(shuffledNumbers)                    // [4, 2, 1, 5, 3]
\n

在底层,Apple使用了Fisher-Yates算法(也称为Knuth洗牌算法)。这是打乱算法的黄金标准。它是如何工作的?从最后一个元素到第一个元素遍历数组,为每个元素选择一个从0到当前位置的随机索引,并交换这两个元素。时间复杂度:O(n)。对于10,000个元素的数组,只需10,000次操作。比天真的方法快5000倍!

\n\n

Fisher-Yates算法:通俗易懂的解析

\n

让我们手动实现它,以理解其中的奥秘:

\n
extension Array {\n    mutating func customShuffle() {\n        for i in stride(from: count - 1, through: 1, by: -1) {\n            let j = Int.random(in: 0...i)\n            swapAt(i, j)\n        }\n    }\n}\n\nvar deck = Array(1...10)\ndeck.customShuffle()\nprint(deck) // [6, 2, 9, 1, 8, 4, 7, 3, 10, 5]
\n

这里发生了什么?假设有一个5个元素的数组。从索引4(最后一个)开始。选择一个0到4之间的随机数,交换位置。然后移动到索引3,选择0到3之间的随机数,交换。依此类推,直到索引1。每个元素保证恰好被随机放置一次。没有移动,没有重复插入——纯粹的数学。

\n

一个重要细节:在Fisher-Yates算法中,不能在每一步都选择0到count-1之间的随机索引。为什么?因为那样某些元素可能留在原位,而其他元素可能被多次移动,从而破坏分布的均匀性。请相信Apple的标准实现——它是最优的。

博客

书籍推荐