问题
I learned that I can show the key and value that is the highest in the dictionary by the code below
// 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))
My question is how can I show 3 champions with the highest value? Example result would be
print greatestChampion // optional(("Ekko": 20, "Neeko": 25, "Ahri": 10))
I would love to learn how to do this if possible.
回答1:
The max method can only return a single value. If you need to get the top 3 you would need to sort them in descending order and get the first 3 elements using prefix method
let greatestChampion = champions.sorted { $0.value > $1.value }.prefix(3)
print(greatestChampion)
This will print
[(key: "Neeko", value: 25), (key: "Ekko", value: 20), (key: "Ahri", value: 10)]
回答2:
Leo's straightforward approach works. Or, you can look at a more performant algorithm in Apple's Swift Algos repo.
Standard library .sort().prefix(k)
takes O(n log n) time.
For occasionally computed small collections, say of people who have genuinely meant to go on a date with me, that's performant enough.
But for prefixing the best (or worst) boba vendors across Taiwan for a second-by-second ranking, your end users or AWS invoice will wonder: is there a more linear time complexity method? Sorting a large collection gets expensive.
Indeed, yes. You can get nearly linear.
Imagine reading a wine list. You may keep a mental list of the most reasonable and outrageous prices, depending on whether you can expense the meal. When you see a new extreme, you put it into a mental set and kick an element out. You do not reread the wine list to sort it unless your coworkers are boring.
The Swift Algorithm repo's prefixSort algorithm is exactly that. It avoids sorting the whole menu up front, instead taking one linear pass to find the finest wine to bill to Tim.
If you read it line by line, the function requires a prefix of size k. It also demands you fork over a sorting method, which is expected to answer the question: "Are you in increasing order? Yes or no?". The method simply needs to take two elements and return a Bool.
To start, the algorithm creates a results array of length prefix k and sorts it.
It then reads through the collection once (sans the first k), checking to see if an element can find a place in the sorted array based on the method you passed in.
Literally, that means:
- If element e is smaller than the results array's last (largest) value
- Find the first index in the results array where e is smaller than the next value $0
- Insert e at that index
- Pop the last (largest) value
Tada. Prefix size and sort order is thus maintained in a small array and your collection need only be read through once.
The speed advantage fades, however, if k is larger than about 10% of the collection size. At that point, you'll see the implementation switches to standard library sort.
If you read the repo's code, you'll notice I diverged a bit from the code when I wrote "Find the first index in the results array where e is smaller..." Rather than search from the start of that array, the algorithm takes a faster approach: binary search. It does this in the partioningIndex method. Since the results array is already sorted, finding a valid index for inserting e is faster by starting at the center and halving the array until a valid index is found. It's non-essential to the concept, but some icing on the speed cake.
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, $0) }
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
来源:https://stackoverflow.com/questions/65746299/how-do-you-find-the-top-3-maximum-values-in-a-swift-dictionary