This article takes you to read the latest open source algorithm library in Swift community

Recently, Swift community has made frequent actions, logging in to Windows and launching the underlying basic library. Now we have launched the Swift algorithm library. Now let's see what's in it, whether it's worth applying in production, and whether the rich raywenderlich / Swift algorithm club is competitive enough.

introduce

Warehouse address: https://github.com/apple/swift-algorithms

At present, the project is still version 0.0.1, which only provides SwiftPM package management support. There are 11 algorithms in total, only for sequence and set types. The algorithms are as follows:

  • Permutation and combination algorithms: Combinations / Permutations
  • Transformation algorithm: Rotate / Partition
  • Collection merging algorithm: Chain / Product / Cycle
  • Subset operation algorithm: Random Sampling / Unique
  • Other operation algorithms: Chunked / Indexed

usage method

The use of Swift Package has been integrated in Xcode 11 and above. Just add a package in the project settings. Address: https://github.com/apple/swift-algorithms , select version 0.0.1.

If you use the Package.swift file, add the following:

let package = Package(
    // name, platforms, products, etc.
    dependencies: [
        .package(url: "https://github.com/apple/swift-algorithms", from: "0.0.1"),
        // other dependencies
    ],
    targets: [
        .target(name: "<target>", dependencies: [
            .product(name: "Algorithms", package: "swift-algorithms"),
        ]),
        // other targets
    ]
)

Permutation and combination

Combinations

Combinations is mainly used to generate all possible combination results of a collection. If two elements have the same value, they are also counted as different elements. The usage is as follows:

let numbers = [10, 20, 30, 40]
for combo in numbers.combinations(ofCount: 2) {
    print(combo)
}
// [10, 20]
// [10, 30]
// [10, 40]
// [20, 30]
// [20, 40]
// [30, 40]

let numbers2 = [20, 10, 10]
for combo in numbers2.combinations(ofCount: 2) {
    print(combo)
}
// [20, 10]
// [20, 10]
// [10, 10]

The Combinations structure implements the Sequence protocol. Each combination is read through iteration. If all results need to be obtained directly, they can be converted into an array:

let numbers = [10, 20, 30, 40]
let combinedArray = Array(numbers.combinations(ofCount: 3))
print(combinedArray)
// [[10, 20, 30], [10, 20, 40], [10, 30, 40], [20, 30, 40]]

Permutations

Permutations is used to generate the full permutation of the set. It is also implemented by complying with the Sequence protocol. It can include all elements by default or specify the number. Similarly, the same value is treated as different elements:

let numbers = [10, 20, 30]
for perm in numbers.permutations() {
    print(perm)
}
// [10, 20, 30]
// [10, 30, 20]
// [20, 10, 30]
// [20, 30, 10]
// [30, 10, 20]
// [30, 20, 10]

for perm in numbers.permutations(ofCount: 2) {
    print(perm)
}
// [10, 20]
// [10, 30]
// [20, 10]
// [20, 30]
// [30, 10]
// [30, 20]

Set transformation

Rotate

Rotate is used to move a piece of data in the collection to the front. For example, the following code moves all the following elements starting with index = 2 to the front, and returns the new index after the original front element is moved:

var numbers = [10, 20, 30, 40, 50, 60]
let p = numbers.rotate(at: 2)
// numbers == [30, 40, 50, 60, 10, 20]
// p == 4

You can also select a part of the set to move. Next, move the elements within the range of index 0.. < 3 from index = 1 to the front of the range, and other elements outside the range remain unchanged:

var numbers = [10, 20, 30, 40, 50, 60]
numbers.rotate(subrange: 0..<3, at: 1)
// numbers = [20, 30, 10, 40, 50, 60]

Rotate algorithm is often used to solve the problems of write time replication and slice modification in divide and conquer algorithm.

Partition

Partition is used to divide the set into two parts according to the specified conditions, and the qualified parts are moved to the end of the set.

The following methods are provided:

  • stablePartition(by:) moves the elements that meet the closure judgment conditions to the end of the array. The moved elements still maintain the original relative order, and returns the index of the first element of the qualified part after the move (if there is no qualified element, the next index of the last element of the array is returned):
numbers = [10, 20, 30, 40, 50, 60, 70, 80]
let p2 = numbers.stablePartition(by: { $0.isMultiple(of: 20) })
// numbers = [10, 30, 50, 70, 20, 40, 60, 80]
  • stablePartition(subrange:by:) is within the specified range and moves the eligible elements to the end of the range:
numbers = [10, 20, 30, 40, 50, 60, 70, 80]
let p2 = numbers.stablePartition(subrange: 1..<4, by: { $0.isMultiple(of: 20) })
// numbers = [10, 30, 20, 40, 50, 60, 70, 80]

There is a problem to note: in version 0.0.1, the stablePartition(subrange:by:) method is defective. If the set subrange does not cover all set elements, an error will be reported. The author has submitted a pr for this problem and merged it into the trunk.

  • In addition, it should be noted that a partition(by:) method has been provided in the set method built in swift, but this method only moves the qualified elements to the end and does not guarantee the relative position of the elements after moving. The time complexity of partition is O (n), and the time complexity of stablepartition is O(n log n):
var numbers = [10, 100, 30, 40, 50, 60, 70, 80]
let p2 = numbers.partition(by: { $0.isMultiple(of: 20) })
// numbers = [10, 70, 30, 50, 40, 60, 100, 80]

A method is also provided to obtain the index of the first eligible element of the collection for which the partition operation has been performed:

let numbers = [10, 30, 50, 70, 20, 40, 60]
let p = numbers.partitioningIndex(where: { $0.isMultiple(of: 20) })
// p = 4
// numbers[..<p] == [10, 30, 50, 70]
// numbers[p...] = [20, 40, 60]

Collection merge

Chain

Chain is used to merge two sets into one set:

let numbers = [10, 20, 30].chained(with: 1...5)
// Array(numbers) == [10, 20, 30, 1, 2, 3, 4, 5]
// 
let letters = "abcde".chained(with: "FGHIJ")
// String(letters) == "abcdeFGHIJ"

The Chain type implements the Sequence and Collection protocols and can be used as a common Collection type.

Product

Product is used to provide traversal support for two paired combinations of all elements of two sets:

let seasons = ["winter", "spring", "summer", "fall"]
for (year, season) in product(1900...2020, seasons) {
    print(year, season)
}
// 1900 winter
// 1900 spring
// 1900 summer
// 1900 fall
// 1902 winter
// ..
// 2020 fall

The product method requires the first parameter passed in to comply with the Sequence protocol, while the second parameter complies with the Collection protocol, because the first parameter only needs to be traversed once, while the second parameter needs to be traversed multiple times. The Sequence protocol does not guarantee that the same value is output after repeated traversal.

Cycle

Cycle provides the ability to traverse a collection infinitely:

for (odd, n) in zip([true, false].cycled(), 1...) {
  print(odd, n)
}
// true 1
// false 2
// true 3
// false 4
// true 5
// false 6
// ...

Or specify the number of iterations:

for x in (1...3).cycled(times: 3) {
    print(x)
}
// 1 2 3 1 2 3 1 2 3

The cycled(times:) method combines the repeatElement and joined methods in the standard library to provide a more convenient method for finite repeated sets.

Subset operation

Random Sampling

Random Sampling provides the ability to randomly select elements from a set to form a new set. The results of each execution may be different:

var source = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
source.randomSample(count: 4)
// e.g. [30, 10, 70, 50]

A randomstabilesample (count:) method is also provided to ensure that the selected collection maintains the relative order of the original elements:

source.randomStableSample(count: 4)
// e.g. [20, 30, 80, 100]

You can also customize the random number generation method:

var rng = SplitMix64(seed: 0)
source.randomSample(count: 4, using: &rng)

Unique

Unique can remove duplicate elements in the Set, which is implemented internally by using the Set type:

let numbers = [1, 2, 3, 3, 2, 3, 3, 2, 2, 2, 1]
let unique = numbers.uniqued()
// unique == [1, 2, 3]

A closed packet can also be provided to determine whether it is unique:

let source = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
let uniq = source.uniqued { $0 % 12 }
// [10, 20, 30, 40, 50, 60]

In the above code, the elements with the same remainder divided by 12 no longer appear in the result.

Other collection operations

Chunked

Chunked can divide a set into new sets containing multiple subsets according to certain conditions:

let numbers = [10, 20, 30, 10, 40, 40, 10, 20]
let chunks = numbers.chunked(by: { $0 <= $1 })
// [[10, 20, 30], [10, 40, 40], [10, 20]]

The above code divides the original array into multiple ascending subsets according to the closure {0 < = 1}

A chunked(on:) method is also provided to compare whether the closure calculation results are the same. The same elements are in a subset:

let names = ["David", "Kyle", "Karoy", "Nate"]
let chunks = names.chunked(on: \.first!)
// [["David"], ["Kyle", "Karoy"], ["Nate"]]

The above code puts the same initials in a subset.

Indexed

Indexed converts the set into a new set. Each element is a tuple containing the index and value of the original set element:

let numbers = [10, 20, 30, 40, 50]
var matchingIndices: Set<Int> = []
for (i, n) in numbers.indexed() {
    print(i, n)
}
// 0 10
// 1 20
// 2 30
// 3 40
// 4 50

summary

As can be seen from the above, the Swift algorithm library is still relatively simple, and many places need to be improved. After the community contributes more content, it is still worth looking forward to in the future, and there is no need to write all kinds of common algorithms in the future.

Posted by trinitywave on Fri, 26 Nov 2021 17:01:00 -0800