Remove elements of 2d array whose 1st column values are duplicates (Swift) - arrays

This query is a small twist on a commonly asked question.
The goal is to filter a 2d array to remove duplicate element pairs whose first-column values are duplicates. For example:
[[1, 1, 1, 2, 2, 3, 3, 3, 3], [0.1, 0.15, 0.2, 0.05, 0.1, 0.2, 0.25, 0.3, 0.35]]
-> [[1, 2, 3],[0.2, 0.1, 0.35]]
Since the second-column values vary, there is obviously some discretion that needs to be applied when filtering: here, the last value of the set of duplicates is chosen.
One of the myriad answers to this related question -- a functional programming solution by Tim MB -- can be adapted to the task:
// Use FP-style filtering to eliminate repeated elements
let rawArray: [[Float]] = [...]
let filteredArray = rawArray
.transpose
.enumerated()
.filter{ (rawArray[0]).lastIndex(of: $0.1[0]) == $0.0 }
.map{ $0.1 }
.transpose
However, this solution is rather slow, which is unfortunate because it's elegant.
A faster solution that keeps the FP spirit is to use dictionary hashing:
// Use array -> dict -> array trick to remove repeated elements
let rawArray: [[Float]] = [...]
let filteredArray = Array( Array(
rawArray
.transpose
.reduce(into: [:], { dict, elements in
dict[elements[0], default:(0,0)] = elements[1]
} )
.map{ ($0.key, $0.value) } )
.sorted{ $0.0 < $1.0 }
.map{ [$0.0, $0.1] }
.transpose) as! Array2D
My questions are:
Is this dictionary trick a good idea? Given that it uses floats as keys?
Why is the FP solution slow? Can it be speeded up?
Are there better alternatives?

Note on terminology: I'll use a to refer to your array, length to refer to its count (a.count), and width to refer to its elements widths (a[0].count).
There are a few things here that are each pretty brutal on your performance.
Transposing
Firstly, each array transposition is O(width * height). Depending on the implementation, it could also be particularly rough on your cache. And you do it twice. Thus, it's an important goal to avoid transposition is possible.
In your case, since you have vectors with only two elements, you can use zip to iterate your two column vectors in tandem. The result a sequence that does so lazily, so no copying happens, and no extra memory or time is used.
Deduplication
The implementation of deduplication that you stumbled on (.filter{ (rawArray[0]).lastIndex(of: $0.1[0]) == $0.0 }) is hot garbage. It's also O(width * height). It's actually worse than approaches that use Array.contains to maintain an array of "already seen" elements. When contains is looking for an element, it can bail-early when it finds a match. lastIndex(of:) always has to go through the entire array, never early-returning because there could always be a later instance of the sought-after element.
Where possible, use an implementation that takes advantage of the Hashability of your elements. Using a Set to track the "already seen" elements allows you to do O(1) contains checks, over array's O(count). I strongly recommend Cœur's implementation.
There's only one catch: that implementation only keeps the first elements, not the last. Luckily enough, that's really easy to fix: just reverse the elements, unique them (keeping the firsts of the reversed elemtents is like keeping the lasts of the original elements), and reverse them back.
My solution:
extension Sequence {
/// Returns an array containing, in order, the first instances of
/// elements of the sequence that compare equally for the keyPath.
func unique<T: Hashable>(for keyPath: KeyPath<Element, T>) -> [Element] {
var unique = Set<T>()
return filter { unique.insert($0[keyPath: keyPath]).inserted }
}
}
let points = zip(array[0], array[1])
let pointsUniquedByXs = points.reversed() // O(1) for collections
.unqiue() // O(count)
.reversed() // O(1) until you need to materalize as a reversed collection

You can accomplish what you want by first filtering the indices of the first array which the element is the first occurrence in reverse order. Then you just need to map the subsequences using them:
let rawArray: [[Float]] = [[1, 1, 1, 2, 2, 3, 3, 3, 3], [0.1, 0.15, 0.2, 0.05, 0.1, 0.2, 0.25, 0.3, 0.3]]
var set: Set<Float> = []
let indices = rawArray
.first?
.indices
.reversed()
.filter { set.insert(rawArray.first![$0]).inserted }
.reversed() ?? []
let result = rawArray.map { elements in indices.map { elements[$0] } }
print(result) // [[1, 2, 3], [0.2, 0.1, 0.3]]
Another option is to create two empty subsequences, iterate the first rawArray subsequence indices reversed and try to insert the float value into a set, if inserted append the corresponding elements to the subsequences, then you just need to recreate the resulting array with those two new sequences reversed:
let rawArray: [[Float]] = [[1, 1, 1, 2, 2, 3, 3, 3, 3], [0.1, 0.15, 0.2, 0.05, 0.1, 0.2, 0.25, 0.3, 0.3]]
var set: Set<Float> = []
var sub1: [Float] = []
var sub2: [Float] = []
rawArray[0].indices.reversed().forEach {
let value = rawArray[0][$0]
if set.insert(value).inserted {
sub1.append(value)
sub2.append(rawArray[1][$0])
}
}
let result: [[Float]] = [sub1.reversed(), sub2.reversed()] // [[1, 2, 3], [0.2, 0.1, 0.3]]
You can make it even faster if the result array is declared as a reversed collection of floating points. It would be O(1) for [ReversedCollection<[Float]>] instead of O(n) for [[Float]] for each subsequence.

Thanks to Alexander, here is solution adapted from Coeur's method in the long related thread.
let rawArray: [[Float]] = [[1, 1, 1, 2, 2, 3, 3, 3, 3],
[0.1, 0.15, 0.2, 0.05, 0.1, 0.2, 0.25, 0.3, 0.35]]
let filteredArray = rawArray
.transpose
.reversed()
.map{ ($0[0],$0[1]) }
.unique(for: \.0)
.map{ [$0.0,$0.1] }
.reversed()
.transpose
All that cruft arises because the data is a two-column float array rather than a 1d array of tuples, and because the last rather than the first duplicate value is required to be selected.
For this to work, Array needs to have the following extensions, the first courtesy of Alexander and Coeur, the second (revision) thanks to Leo Dabus:
extension RangeReplaceableCollection {
/// Returns a collection containing, in order, the first instances of
/// elements of the sequence that compare equally for the keyPath.
func unique<T: Hashable>(for keyPath: KeyPath<Element, T>) -> Self {
var unique = Set<T>()
return filter { unique.insert($0[keyPath: keyPath]).inserted }
}
}
extension RandomAccessCollection where Element: RandomAccessCollection {
/// Peform a transpose operation
var transpose: [[Element.Element]] {
guard !isEmpty,
var index = first?.startIndex,
let endIndex = first?.endIndex
else { return [] }
var result: [[Element.Element]] = []
while index < endIndex {
result.append(map{$0[index]})
first?.formIndex(after: &index) }
return result
}
}

Related

Swift returning Array from ArraySlices

I'm working with a sorting function that takes an array of Ints already sorted in descending order and places a new Int in its correct spot. (i.e if my sorted array was [10, 7, 2] and the new int was 5, the function would return [10, 7, 5, 2]). The function for doing this, once it has found the correct spot for the new Int, slices the original array into the items before the new Ints spot and those after, and then combines the slices with the new Int.
The problem I'm running into is that this won't give me an array but rather an array slice.
Code:
func addToSorted(sorted: [Int], new: Int) -> [Int] {
if sorted.count == 0 {
return [new]
} else {
for index in 0..<sorted.count {
let item = sorted[index]
if new > item {
return sorted[..<index] + [new] + sorted[index...]
}
}
}
}
let result = addToSorted(sorted: [10, 7, 2], new: 5)
print(result) // expected [10, 7, 5, 2]
This is a more generic (and efficient) alternative which uses binary search
extension RandomAccessCollection where Element : Comparable {
func descendingInsertionIndex(of value: Element) -> Index {
var slice : SubSequence = self[...]
while !slice.isEmpty {
let middle = slice.index(slice.startIndex, offsetBy: slice.count / 2)
if value > slice[middle] {
slice = slice[..<middle]
} else {
slice = slice[index(after: middle)...]
}
}
return slice.endIndex
}
}
And use it
var array = [10, 7, 5, 2]
let index = array.descendingInsertionIndex(of: 4)
array.insert(4, at: index)
print(array) // [10, 7, 5, 4, 2]
For ascending order replace if value > slice[middle] with if value < slice[middle] and return slice.endIndex with return slice.startIndex
If you use the Swift Algorithms, this insertion is a one-liner:
var arr = [10, 7, 2]
arr.insert(5, at: arr.partitioningIndex {$0 < 5})
print (arr) // [10, 7, 5, 2]
This is very efficient — O(log n) — because your array is already partitioned (sorted) and therefore it uses a binary search.
You would have to promote the slices to arrays:
return Array(sorted[..<index]) + [new] + Array(sorted[index...])
A few other points:
You should make a habit out of using sorted.isEmpty over sorted.count == 0, it's much faster for some collections that don't store their count, such as lazy collections or even String (IIRC).
A better approach would be to just use Array.insert(_:at:):
var sorted = sorted // Make a local mutable copy
sorted.insert(new, at: index)
BTW after your for loop, you need insert at the end of your array (this also removes the need for checking the empty case):
return sorted + [new]
Since this works even when sorted is empty, you can remove that special case.
Since you know your data structure is already sorted, you can use binary search instead of linear search to find the insertion index faster.

Splitting an Array into Sub-Arrays in Swift [duplicate]

This question already has answers here:
In Swift, an efficient function that separates an array into 2 arrays based on a predicate
(7 answers)
Closed 6 months ago.
Problem
Given an array of values how can I split it into sub-arrays made of elements that are equal?
Example
Given this array
let numbers = [1, 1, 1, 3, 3, 4]
I want this output
[[1,1,1], [3, 3], [4]]
What I am NOT looking for
A possible way of solving this would be creating some sort of index to indicate the occurrences of each element like this.
let indexes = [1:3, 3:2, 4:1]
And finally use the index to rebuild the output array.
let subsequences = indexes.sort { $0.0.0 < $0.1.0 }.reduce([Int]()) { (res, elm) -> [Int] in
return res + [Int](count: elm.1, repeatedValue: elm.0)
}
However with this solution I am losing the original values. Of course in this case it's not a big problem (an Int value is still and Inteven if recreated) but I would like to apply this solution to more complex data structures like this
struct Starship: Equatable {
let name: String
let warpSpeed: Int
}
func ==(left:Starship, right:Starship) -> Bool {
return left.warpSpeed == right.warpSpeed
}
Final considerations
The function I am looking for would be some kind of reverse of flatten(), infact
let subsequences: [[Int]] = [[1,1,1], [3, 3], [4]]
print(Array(subsequences.flatten())) // [1, 1, 1, 3, 3, 4]
I hope I made myself clear, let me know should you need further details.
// extract unique numbers using a set, then
// map sub-arrays of the original arrays with a filter on each distinct number
let numbers = [1, 1, 1, 3, 3, 4]
let numberGroups = Set(numbers).map{ value in return numbers.filter{$0==value} }
print(numberGroups)
[EDIT] changed to use Set Initializer as suggested by Hamish
[EDIT2] Swift 4 added an initializer to Dictionary that will do this more efficiently:
let numberGroups = Array(Dictionary(grouping:numbers){$0}.values)
For a list of objects to be grouped by one of their properties:
let objectGroups = Array(Dictionary(grouping:objects){$0.property}.values)
If you could use CocoaPods/Carthage/Swift Package Manager/etc. you could use packages like oisdk/SwiftSequence which provides the group() method:
numbers.lazy.group()
// should return a sequence that generates [1, 1, 1], [3, 3], [4].
or UsrNameu1/TraverSwift which provides groupBy:
groupBy(SequenceOf(numbers), ==)
If you don't want to add external dependencies, you could always write an algorithm like:
func group<S: SequenceType where S.Generator.Element: Equatable>(seq: S) -> [[S.Generator.Element]] {
var result: [[S.Generator.Element]] = []
var current: [S.Generator.Element] = []
for element in seq {
if current.isEmpty || element == current[0] {
current.append(element)
} else {
result.append(current)
current = [element]
}
}
result.append(current)
return result
}
group(numbers)
// returns [[1, 1, 1], [3, 3], [4]].
Let's assume that you have an unsorted array of items. You will need to sort the initial array then you will have something like this:
[1, 1, 1, 3, 3, 4]
After that you will initialize two arrays: one for storing arrays and another one to use it as a current array.
Loop through the initial array and:
if the current value isn't different from the last one, push it to the current array
otherwise push the current array to the first one then empty the current array.
Hope it helps!
Worth mentioning, using Swift Algorithms this is now a one-liner:
import Algorithms
let numbers = [1, 1, 1, 3, 3, 4]
let chunks: [[Int]] = numbers.chunked(by: ==).map { .init($0) }
print(chunks) // [[1, 1, 1], [3, 3], [4]]

In Swift how can I sort one array based on another array?

In Swift, say I have two arrays:
var array1: [Double] = [1.2, 2.4, 20.0, 10.9, 1.5]
var array2: [Int] = [1, 0, 2, 0, 3]
Now, I want to sort array1 in ascending order and reindex array2 accordingly so that I get
array1 = [1.2, 1.5, 2.4, 10.9, 20.4]
array2 = [1, 3, 0, 0, 2]
Is there a simple way to do this using Swift functions or syntax?
I know I can build a function to do it and can keep track of indices, but I'm curious if there is a more elegant solution.
let array1: [Double] = [1.2, 2.4, 20.0, 10.9, 1.5]
let array2: [Int] = [1, 0, 2, 0, 3]
// use zip to combine the two arrays and sort that based on the first
let combined = zip(array1, array2).sorted {$0.0 < $1.0}
print(combined) // "[(1.2, 1), (1.5, 3), (2.4, 0), (10.9, 0), (20.0, 2)]"
// use map to extract the individual arrays
let sorted1 = combined.map {$0.0}
let sorted2 = combined.map {$0.1}
print(sorted1) // "[1.2, 1.5, 2.4, 10.9, 20.0]"
print(sorted2) // "[1, 3, 0, 0, 2]"
Sorting more than 2 arrays together
If you have 3 or more arrays to sort together, you can sort one of the arrays along with its offsets, use map to extract the offsets, and then use map to order the other arrays:
let english = ["three", "five", "four", "one", "two"]
let ints = [3, 5, 4, 1, 2]
let doubles = [3.0, 5.0, 4.0, 1.0, 2.0]
let roman = ["III", "V", "IV", "I", "II"]
// Sort english array in alphabetical order along with its offsets
// and then extract the offsets using map
let offsets = english.enumerated().sorted { $0.element < $1.element }.map { $0.offset }
// Use map on the array of ordered offsets to order the other arrays
let sorted_english = offsets.map { english[$0] }
let sorted_ints = offsets.map { ints[$0] }
let sorted_doubles = offsets.map { doubles[$0] }
let sorted_roman = offsets.map { roman[$0] }
print(sorted_english)
print(sorted_ints)
print(sorted_doubles)
print(sorted_roman)
Output:
["five", "four", "one", "three", "two"]
[5, 4, 1, 3, 2]
[5.0, 4.0, 1.0, 3.0, 2.0]
["V", "IV", "I", "III", "II"]
You could "link" the items of each array by mapping over the indices to create an array of tuples, then sort the tuples according to the first array's values before extracting the original arrays.
assert(array1.count == array2.count, "The following technique will only work if the arrays are the same length.")
let count = array1.count
// Create the array of tuples and sort according to the
// first tuple value (i.e. the first array)
let sortedTuples = (0..<count).map { (array1[$0], array2[$0]) }.sort { $0.0 < $1.0 }
// Map over the sorted tuples array to separate out the
// original (now sorted) arrays.
let sortedArray1 = sortedTuples.map { $0.0 }
let sortedArray2 = sortedTuples.map { $0.1 }
Swift 4
This part is translated from #vacawama's answer to Swift 4 syntax
let array1: [Double] = [1.2, 2.4, 20.0, 10.9, 1.5]
let array2: [Int] = [1, 0, 2, 0, 3]
// use zip to combine the two arrays and sort that based on the first
let combined = zip(array1, array2).sorted(by: {$0.0 < $1.0})
print(combined) // "[(1.2, 1), (1.5, 3), (2.4, 0), (10.9, 0), (20.0, 2)]"
// use map to extract the individual arrays
let sorted1 = combined.map {$0.0}
let sorted2 = combined.map {$0.1}
print(sorted1) // "[1.2, 1.5, 2.4, 10.9, 20.0]"
print(sorted2) // "[1, 3, 0, 0, 2]"
The above logic can be expanded for three or more arrays:
(slow)
let array1: [Double] = [1.2, 2.4, 20.0, 10.9, 1.5]
let array2: [Int] = [1, 0, 2, 0, 3]
let array3: [Float] = [3.3, 1.1, 2.5, 5.1, 9.0]
// use zip to combine each (first, n.th) array pair and sort that based on the first
let combined12 = zip(array1, array2).sorted(by: {$0.0 < $1.0})
let combined13 = zip(array1, array3).sorted(by: {$0.0 < $1.0})
// use map to extract the individual arrays
let sorted1 = combined12.map {$0.0}
let sorted2 = combined12.map {$0.1}
let sorted3 = combined13.map {$0.1}
As #Duncan C pointed out, this approach is not very efficient as the first array is sorted repeatedly. #vacawama's approach should be used instead, which in Swift 4 syntax is:
(fast)
let offsets = array1.enumerated()sorted(by: {$0.element < $1.element}).map {$0.offset}
let sorted1 = offsets.map {array1[$0]}
let sorted2 = offsets.map {array2[$0]}
let sorted3 = offsets.map {array3[$0]}
Though not especially elegant, a simple solution when working with arrays of objects which must be compared, and whose orders are not known and may not even share the same lengths, is to loop the "ordered" array, finding the matching object in the unordered array, and appending it to a new empty array:
var sorted: [Foo] = []
// Loop the collection whose order will define the other
for item in originalOrder {
// Find the item in the unsorted collection
if let next = unsortedItems.first(where: { $0 === item }) {
// Move the item to the new collection, thus achieving order parity
sorted.append(next)
}
}
This is useful when you have an operation that provides a transformed version of a collection which may have 0..<original.count number of items in any order, and you want to get back to the original order using pointer/object equality.
If you need to additionally maintain index parity, you can skip the if let and just append the result of first(where:) directly into sorted, which will put nil into the blanks.
Note that this example solution will additionally act as a filter for items which are either duplicated or not in the original, which may or may not be what you want. Modify to your needs.

Split Big Array Into Two Arrays

I have a big array of objects and would like to split it into two arrays containing the objects in alternate order.
Example:
[0, 1, 2, 3, 4, 5, 6]
Becomes these two arrays (they should alternate)
[0, 2, 4, 6] and [1, 3, 5]
There are a ton of ways to split an array. But, what is the most efficient (least costly) if the array is huge.
There are various fancy ways to do it with filter but most would probably require two passes rather than one, so you may as well just use a for-loop.
Reserving space up-front could make a big difference in this case since if the source is large it’ll avoid unnecessary re-allocation as the new arrays grow, and the calculation of space needed is in constant time on arrays.
// could make this take a more generic random-access collection source
// if needed, or just make it an array extension instead
func splitAlternating<T>(source: [T]) -> ([T],[T]) {
var evens: [T] = [], odds: [T] = []
evens.reserveCapacity(source.count / 2 + 1)
odds.reserveCapacity(source.count / 2)
for idx in indices(source) {
if idx % 2 == 0 {
evens.append(source[idx])
}
else {
odds.append(source[idx])
}
}
return (evens,odds)
}
let a = [0,1,2,3,4,5,6]
splitAlternating(a) // ([0, 2, 4, 6], [1, 3, 5])
If performance is truly critical, you could use source.withUnsafeBufferPointer to access the source elements, to avoid the index bounds checking.
If the arrays are really huge, and you aren’t going to use the resulting data except to sample a small number of elements, you could consider using a lazy view instead (though the std lib lazy filter isn’t much use here as it returns sequence not a collection – you’d possibly need to write your own).
You can use the for in stride loop to fill two resulting arrays as follow:
extension Array {
var groupOfTwo:(firstArray:[T],secondArray:[T]) {
var firstArray:[T] = []
var secondArray:[T] = []
for index in stride(from: 0, to: count, by: 2) {
firstArray.append(self[index])
if index + 1 < count {
secondArray.append(self[index+1])
}
}
return (firstArray,secondArray)
}
}
[0, 1, 2, 3, 4, 5, 6].groupOfTwo.firstArray // [0, 2, 4, 6]
[0, 1, 2, 3, 4, 5, 6].groupOfTwo.secondArray // [1, 3, 5]
update: Xcode 7.1.1 • Swift 2.1
extension Array {
var groupOfTwo:(firstArray:[Element],secondArray:[Element]) {
var firstArray:[Element] = []
var secondArray:[Element] = []
for index in 0.stride(to: count, by: 2) {
firstArray.append(self[index])
if index + 1 < count {
secondArray.append(self[index+1])
}
}
return (firstArray,secondArray)
}
}
A more concise, functional approach would be to use reduce
let a = [0,1,2,3,4,5,6]
let (evens, odds) = a.enumerate().reduce(([Int](),[Int]())) { (cur, next) in
let even = next.index % 2 == 0
return (cur.0 + (even ? [next.element] : []),
cur.1 + (even ? [] : [next.element]))
}
evens // [0,2,4,6]
odds // [1,3,5]
Big/huge array always pose problems when being partially processed, like in this case, as creating two extra (even if half-sized) arrays can be both time and memory consuming. What if, for example, you just want to compute the mean and standard deviation of oddly and evenly positioned numbers, but this will require calling a dedicated function which requires a sequence as input?
Thus why not creating two sub-collections that instead of duplicating the array contents, they point to the original array, in a transparent manner to allow querying them for elements:
extension Collection where Index: Strideable{
func stride(from: Index, to: Index, by: Index.Stride) -> StridedToCollection<Self> {
return StridedToCollection(self, from: from, to: to, by: by)
}
}
struct StridedToCollection<C>: Collection where C: Collection, C.Index: Strideable {
private let _subscript : (C.Index) -> C.Element
private let step: C.Index.Stride
fileprivate init(_ collection: C, from: C.Index, to: C.Index, by: C.Index.Stride) {
startIndex = from
endIndex = Swift.max(to, startIndex)
step = by
_subscript = { collection[$0] }
}
let startIndex: C.Index
let endIndex: C.Index
func index(after i: C.Index) -> C.Index {
let next = i.advanced(by: step)
return next >= endIndex ? endIndex : next
}
subscript(_ index: C.Index) -> C.Element {
return _subscript(index)
}
}
The Collection extension and the associated struct would create a pseudo-array that you can use to access only the elements you are interested into.
Usage is simple:
let numbers: [Int] = [1, 2, 3, 4]
let stride1 = numbers.stride(from: 0, to: numbers.count, by: 2)
let stride2 = numbers.stride(from: 1, to: numbers.count, by: 2)
print(Array(stride1), Array(stride2))
With the above you can iterate the two strides without worrying you'll double the amount of memory. And if you actually need two sub-arrays, you just Array(stride)-ify them.
Use for loops. If the index value is even then send that to one array and if the index value is odd, then send that to odd array.
Here's, in my opinion, the easiest way
old_list = [0, 1, 2, 3, 4, 5, 6]
new_list1 =[]
new_list2 = []
while len(old_list)>0:
new_list1.append(old_list.pop(-1))
if len(old_list) != 0:
new_list2.append(old_list.pop(-1))
new_list1.reverse()
new_list2.reverse()
I just had to do this where I split an array into two in one place, and three into another. So I built this:
extension Array {
/// Splits the receiving array into multiple arrays
///
/// - Parameter subCollectionCount: The number of output arrays the receiver should be divided into
/// - Returns: An array containing `subCollectionCount` arrays. These arrays will be filled round robin style from the receiving array.
/// So if the receiver was `[0, 1, 2, 3, 4, 5, 6]` the output would be `[[0, 3, 6], [1, 4], [2, 5]]`. If the reviever is empty the output
/// Will still be `subCollectionCount` arrays, they just all will be empty. This way it's always safe to subscript into the output.
func split(subCollectionCount: Int) -> [[Element]] {
precondition(subCollectionCount > 1, "Can't split the array unless you ask for > 1")
var output: [[Element]] = []
(0..<subCollectionCount).forEach { (outputIndex) in
let indexesToKeep = stride(from: outputIndex, to: count, by: subCollectionCount)
let subCollection = enumerated().filter({ indexesToKeep.contains($0.offset)}).map({ $0.element })
output.append(subCollection)
}
precondition(output.count == subCollectionCount)
return output
}
}
It works on Swift 4.2 and 5.0 (as of 5.0 with Xcode 10.2 beta 2)

Find min / max value in Swift Array

Given an array of Swift numeric values, how can I find the minimum and maximum values?
I've so far got a simple (but potentially expensive) way:
var myMax = sort(myArray,>)[0]
And how I was taught to do it at school:
var myMax = 0
for i in 0..myArray.count {
if (myArray[i] > myMax){myMax = myArray[i]}
}
Is there a better way to get the minimum or maximum value from an integer Array in Swift? Ideally something that's one line such as Ruby's .min and .max.
Given:
let numbers = [1, 2, 3, 4, 5]
Swift 3:
numbers.min() // equals 1
numbers.max() // equals 5
Swift 2:
numbers.minElement() // equals 1
numbers.maxElement() // equals 5
To calculate an array's min and max values yourself, you can use reduce. This was a key solution prior to .min() and .max() appearing in Swift.
Use the almighty reduce:
let nums = [1, 6, 3, 9, 4, 6];
let numMax = nums.reduce(Int.min, { max($0, $1) })
Similarly:
let numMin = nums.reduce(Int.max, { min($0, $1) })
reduce takes a first value that is the initial value for an internal accumulator variable, then applies the passed function (here, it's anonymous) to the accumulator and each element of the array successively, and stores the new value in the accumulator. The last accumulator value is then returned.
With Swift 5, Array, like other Sequence Protocol conforming objects (Dictionary, Set, etc), has two methods called max() and max(by:) that return the maximum element in the sequence or nil if the sequence is empty.
#1. Using Array's max() method
If the element type inside your sequence conforms to Comparable protocol (may it be String, Float, Character or one of your custom class or struct), you will be able to use max() that has the following declaration:
#warn_unqualified_access func max() -> Element?
Returns the maximum element in the sequence.
The following Playground codes show to use max():
let intMax = [12, 15, 6].max()
let stringMax = ["bike", "car", "boat"].max()
print(String(describing: intMax)) // prints: Optional(15)
print(String(describing: stringMax)) // prints: Optional("car")
class Route: Comparable, CustomStringConvertible {
let distance: Int
var description: String { return "Route with distance: \(distance)" }
init(distance: Int) {
self.distance = distance
}
static func ==(lhs: Route, rhs: Route) -> Bool {
return lhs.distance == rhs.distance
}
static func <(lhs: Route, rhs: Route) -> Bool {
return lhs.distance < rhs.distance
}
}
let routes = [
Route(distance: 20),
Route(distance: 30),
Route(distance: 10)
]
let maxRoute = routes.max()
print(String(describing: maxRoute)) // prints: Optional(Route with distance: 30)
#2. Using Array's max(by:) method
If the element type inside your sequence does not conform to Comparable protocol, you will have to use max(by:) that has the following declaration:
#warn_unqualified_access func max(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows -> Element?
Returns the maximum element in the sequence, using the given predicate as the comparison between elements.
The following Playground codes show to use max(by:):
let dictionary = ["Boat" : 15, "Car" : 20, "Bike" : 40]
let keyMaxElement = dictionary.max(by: { (a, b) -> Bool in
return a.key < b.key
})
let valueMaxElement = dictionary.max(by: { (a, b) -> Bool in
return a.value < b.value
})
print(String(describing: keyMaxElement)) // prints: Optional(("Car", 20))
print(String(describing: valueMaxElement)) // prints: Optional(("Bike", 40))
class Route: CustomStringConvertible {
let distance: Int
var description: String { return "Route with distance: \(distance)" }
init(distance: Int) {
self.distance = distance
}
}
let routes = [
Route(distance: 20),
Route(distance: 30),
Route(distance: 10)
]
let maxRoute = routes.max(by: { (a, b) -> Bool in
return a.distance < b.distance
})
print(String(describing: maxRoute)) // prints: Optional(Route with distance: 30)
The other answers are all correct, but don't forget you could also use collection operators, as follows:
var list = [1, 2, 3, 4]
var max: Int = (list as AnyObject).valueForKeyPath("#max.self") as Int
you can also find the average in the same way:
var avg: Double = (list as AnyObject).valueForKeyPath("#avg.self") as Double
This syntax might be less clear than some of the other solutions, but it's interesting to see that -valueForKeyPath: can still be used :)
You can use with reduce:
let randomNumbers = [4, 7, 1, 9, 6, 5, 6, 9]
let maxNumber = randomNumbers.reduce(randomNumbers[0]) { $0 > $1 ? $0 : $1 } //result is 9
Swift 3.0
You can try this code programmatically.
func getSmallAndGreatestNumber() -> Void {
let numbers = [145, 206, 116, 809, 540, 176]
var i = 0
var largest = numbers[0]
var small = numbers[0]
while i < numbers.count{
if (numbers[i] > largest) {
largest = numbers[i]
}
if (numbers[i] < small) {
small = numbers[i]
}
i = i + 1
}
print("Maximum Number ====================\(largest)")// 809
print("Minimum Number ====================\(small)")// 116
}
With Swift 1.2 (and maybe earlier) you now need to use:
let nums = [1, 6, 3, 9, 4, 6];
let numMax = nums.reduce(Int.min, combine: { max($0, $1) })
For working with Double values I used something like this:
let nums = [1.3, 6.2, 3.6, 9.7, 4.9, 6.3];
let numMax = nums.reduce(-Double.infinity, combine: { max($0, $1) })
In Swift 2.0, the minElement and maxElement become methods of SequenceType protocol, you should call them like:
let a = [1, 2, 3]
print(a.maxElement()) //3
print(a.minElement()) //1
Using maxElement as a function like maxElement(a) is unavailable now.
The syntax of Swift is in flux, so I can just confirm this in Xcode version7 beta6.
It may be modified in the future, so I suggest that you'd better check the doc before you use these methods.
Here's a performance test for the solutions posted here. https://github.com/tedgonzalez/MaxElementInCollectionPerformance
This is the fastest for Swift 5
array.max()
var numbers = [1, 2, 7, 5];
var val = sort(numbers){$0 > $1}[0];
Apple's Swift Algorithms introduced 2021 contains Minima and/or Maxima which is likely highly optimized.
Example from the documentation:
let numbers = [7, 1, 6, 2, 8, 3, 9]
if let (smallest, largest) = numbers.minAndMax(by: <) {
// Work with 1 and 9....
}
The total complexity is O(k log k + nk), which will result in a runtime close to O(n) if k is a small amount. If k is a large amount (more than 10% of the collection), we fall back to sorting the entire array. Realistically, this means the worst case is actually O(n log n).
Updated for Swift 3/4:
Use below simple lines of code to find the max from array;
var num = [11, 2, 7, 5, 21]
var result = num.sorted(){
$0 > $1
}
print("max from result: \(result[0])") // 21
Just curious why you think the way it was taught in school is potentially expensive? You're running a for loop which has the time complexity of O(N). That's actually better than most sorting algorithms and equivalent to the performance of higher-order methods like reduce.
So I would think that in terms of performance, a for loop is as good as it gets. I don't think you'll find anything better than O(N) for finding max.
Of course, just use the .max() method provided by apple is the way to go.
If both minimum and maximum values are desired, an efficient approach could be to use a single reduce operation with a tuple:
let values = [11, 2, 7, 5, 21]
let (minimum, maximum) = values.reduce((Int.max, Int.min)) {
(min($0.0, $1), max($0.1, $1))
}
print(minimum, maximum) // 2, 21
let array: [Int] = [2, -22, -1, -5600, 333, -167]
var min = 0
var max = array[0]
for i in array {
// finding the min
if min > i {
min = i
}
// finding the max
if max < i {
max = i
}
}
print("Minimum: \(min)\nMaximum: \(max)")
You can also sort your array and then use array.first or array.last

Resources