如何在 Swift 字典中找到前 3 个最大值?

How do you find the top 3 maximum values in a Swift dictionary?

我了解到我可以通过下面的代码显示字典中最高的键和值

// champions dictionary
var champions = ["Ekko": 20, "Ahri": 10, "Vayne": 2, "Neeko": 25, "Zed": 6]

let greatestChampion = champions.max { a, b in a.value < b.value }
print greatestChampion // optional(("Ekko": 20))

我的问题是如何显示价值最高的3个英雄?示例结果为

print greatestChampion // optional(("Ekko": 20, "Neeko": 25, "Ahri": 10))

如果可能的话,我很想学习如何做到这一点。

max 方法只能return 单个值。如果您需要获得前 3 名,则需要按降序对它们进行排序,并使用前缀方法获得前 3 个元素


let greatestChampion = champions.sorted { [=10=].value > .value }.prefix(3)

print(greatestChampion)

这将打印

[(key: "Neeko", value: 25), (key: "Ekko", value: 20), (key: "Ahri", value: 10)]

Leo 的直截了当的方法奏效了。或者,您可以查看 Apple 的 Swift Algos 存储库中的 a more performant algorithm

标准库 .sort().prefix(k) 需要 O(n log n) 时间。

对于偶尔计算的小型集合,比如说那些真正想和我约会的人,这就足够了。

但是为了在台湾最好(或最差)的boba供应商前面加上逐秒排名,您的最终用户或AWS发票会想:是否有更线性的时间复杂度方法?对大型集合进行排序会变得很昂贵。

的确如此。你可以得到接近线性的。

想象一下阅读酒单。你可以在心里列一张最合理和最离谱的价格清单,这取决于你是否能负担得起这顿饭。当你看到一个新的极端时,你会把它放在一个心理定势中并踢出一个元素。除非您的同事很无聊,否则您不会重新阅读酒单来对其进行排序。

Swift 算法存储库的 prefixSort 算法就是这样。它避免了预先对整个菜单进行排序,而是通过一次线性传递来找到最好的葡萄酒来向蒂姆收费。

如果逐行阅读,函数需要大小为 k 的前缀。它还要求您分叉出一种排序方法,该方法有望回答以下问题:“您的顺序是递增的吗?是或否?”。该方法只需要采用两个元素和 return 一个 Bool.

首先,该算法创建一个长度前缀为 k 的结果数组并对其进行排序。

然后它会读取一次集合(不包括前 k 个),根据您传入的方法检查元素是否可以在排序数组中找到位置。

字面意思是:

  • 如果元素 e 小于结果数组的最后一个(最大)值
  • 在结果数组中找到第一个索引,其中 e 小于下一个值 $0
  • 在该索引处插入 e
  • 弹出最后一个(最大)值

多田。前缀大小和排序顺序因此保持在一个小数组中,您的集合只需阅读一次。

但是,如果 k 大于集合大小的大约 10%,速度优势就会消失。届时,您将看到实现切换到标准库排序。

如果您阅读 repo 的代码,您会注意到我在编写“查找结果数组中 e 较小的第一个索引...”而不是从该数组,算法采用更快的方法:二进制搜索。它在 partioningIndex 方法中执行此操作。由于结果数组已经排序,因此通过从中心开始并将数组减半直到找到有效索引,可以更快地找到用于插入 e 的有效索引。这对概念来说不是必需的,但可以锦上添花。

extension Collection {

  /// - Complexity: O(k log k + nk)
  public func sortedPrefix(
    _ count: Int,
    by areInIncreasingOrder: (Element, Element) throws -> Bool
  ) rethrows -> [Self.Element] {
    assert(count >= 0, """
      Cannot prefix with a negative amount of elements!
      """
    )

    guard count > 0 else {
      return []
    }

    let prefixCount = Swift.min(count, self.count)

    guard prefixCount < (self.count / 10) else {
      return Array(try sorted(by: areInIncreasingOrder).prefix(prefixCount))
    }

    var result = try self.prefix(prefixCount).sorted(by: areInIncreasingOrder)

    for e in self.dropFirst(prefixCount) {
      if let last = result.last, try areInIncreasingOrder(last, e) {
        continue
      }
      let insertionIndex =
        try result.partitioningIndex { try areInIncreasingOrder(e, [=10=]) }
      let isLastElement = insertionIndex == result.endIndex
      result.removeLast()
      if isLastElement {
        result.append(e)
      } else {
        result.insert(e, at: insertionIndex)
      }
    }

    return result
  }
}

Code

Explanation and Performance

Tests