如何正确打乱Swift数组:从新手到专家
\n您是否曾想过在应用中创建随机播放列表?或者,您可能需要打乱测验中的问题,使它们每次都以不同顺序出现?如果您使用Swift,迟早会遇到这样的任务:如何打乱数组(shuffle array)。这个问题在Stack Overflow上获得了数千次浏览,这并非偶然——其中的陷阱比表面看起来要多得多。
\n在本文中,我们不仅会展示如何调用shuffle()。我们将深入底层:分析算法、比较性能、学习如何进行优化打乱,并理解何时标准方法是灾难,何时又是人类最好的发明。开始吧!
为什么简单的"random"是个坏主意?
\n假设您有一个数组:
\nvar cards = ["Ace", "King", "Queen", "Jack"]\n最天真的方法是创建一个新数组,并将元素插入到随机位置。看起来很有道理,对吧?
\nfunc 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万次。应用会开始"卡顿",用户会离开,评分会下降。不要这样做。
内置shuffle()方法:您的新好朋友
\n从Swift 4.2开始,我们有了原生方法shuffle()(用于可变数组)和shuffled()(返回新数组)。使用方法如下:
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\nFisher-Yates算法:通俗易懂的解析
\n让我们手动实现它,以理解其中的奥秘:
\nextension 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的标准实现——它是最优的。