Skip to content

Commit 717ba94

Browse files
author
Giuseppe Lanza
committed
Added readme file
1 parent 677e3ce commit 717ba94

8 files changed

+297
-0
lines changed

Introsort/HeapSort.swift

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import Foundation
2+
3+
private func shiftDown<T>(_ elements: inout [T], _ index: Int, _ range: Range<Int>, by areInIncreasingOrder: (T, T) -> Bool) {
4+
let countToIndex = elements.distance(from: range.lowerBound, to: index)
5+
let countFromIndex = elements.distance(from: index, to: range.upperBound)
6+
7+
guard countToIndex + 1 < countFromIndex else { return }
8+
9+
let left = elements.index(index, offsetBy: countToIndex + 1)
10+
var largest = index
11+
if areInIncreasingOrder(elements[largest], elements[left]) {
12+
largest = left
13+
}
14+
15+
if countToIndex + 2 < countFromIndex {
16+
let right = elements.index(after: left)
17+
if areInIncreasingOrder(elements[largest], elements[right]) {
18+
largest = right
19+
}
20+
}
21+
22+
if largest != index {
23+
elements.swapAt(index, largest)
24+
shiftDown(&elements, largest, range, by: areInIncreasingOrder)
25+
}
26+
27+
}
28+
29+
private func heapify<T>(_ list: inout [T], _ range: Range<Int>, by areInIncreasingOrder: (T, T) -> Bool) {
30+
let root = range.lowerBound
31+
var node = list.index(root, offsetBy: list.distance(from: range.lowerBound, to: range.upperBound)/2)
32+
33+
while node != root {
34+
list.formIndex(before: &node)
35+
shiftDown(&list, node, range, by: areInIncreasingOrder)
36+
}
37+
}
38+
39+
public func heapsort<T>(for array: inout [T], range: Range<Int>, by areInIncreasingOrder: (T, T) -> Bool) {
40+
var hi = range.upperBound
41+
let lo = range.lowerBound
42+
heapify(&array, range, by: areInIncreasingOrder)
43+
array.formIndex(before: &hi)
44+
45+
while hi != lo {
46+
array.swapAt(lo, hi)
47+
shiftDown(&array, lo, lo..<hi, by: areInIncreasingOrder)
48+
array.formIndex(before: &hi)
49+
}
50+
}

Introsort/InsertionSort.swift

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import Foundation
2+
3+
public func insertionSort<T>(for array: inout [T], range: Range<Int>, by areInIncreasingOrder: (T, T) -> Bool) {
4+
guard !range.isEmpty else { return }
5+
6+
let start = range.lowerBound
7+
var sortedEnd = start
8+
9+
array.formIndex(after: &sortedEnd)
10+
while sortedEnd != range.upperBound {
11+
let x = array[sortedEnd]
12+
13+
var i = sortedEnd
14+
repeat {
15+
let predecessor = array[array.index(before: i)]
16+
17+
guard areInIncreasingOrder(x, predecessor) else { break }
18+
array[i] = predecessor
19+
array.formIndex(before: &i)
20+
} while i != start
21+
22+
if i != sortedEnd {
23+
array[i] = x
24+
}
25+
array.formIndex(after: &sortedEnd)
26+
}
27+
28+
}

Introsort/IntroSort.swift

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import Foundation
2+
3+
public func introsort<T>(_ array: inout [T], by areInIncreasingOrder: (T, T) -> Bool) {
4+
//The depth limit is as best practice 2 * log( n )
5+
let depthLimit = 2 * floor(log2(Double(array.count)))
6+
7+
introSortImplementation(for: &array, range: 0..<array.count, depthLimit: Int(depthLimit), by: areInIncreasingOrder)
8+
}
9+
10+
///This method is recursively executed for each partition result of the quicksort part of the algorithm
11+
private func introSortImplementation<T>(for array: inout [T], range: Range<Int>, depthLimit: Int, by areInIncreasingOrder: (T, T) -> Bool) {
12+
if array.distance(from: range.lowerBound, to: range.upperBound) < 20 {
13+
//if the partition count is less than 20 we can sort it using insertion sort. This algorithm in fact performs well on collections
14+
//of this size, plus, at this point is quite probable that the quisksort part of the algorithm produced a partition which is
15+
//nearly sorted. As we knoe insertion sort tends to O( n ) if this is the case.
16+
insertionSort(for: &array, range: range, by: areInIncreasingOrder)
17+
} else if depthLimit == 0 {
18+
//If we reached the depth limit for this recursion branch, it's possible that we are hitting quick sort's worst case.
19+
//Since quicksort degrades to O( n^2 ) in its worst case we stop using quicksort for this recursion branch and we switch to heapsort.
20+
//Our preference remains quicksort, and we hope to be rare to see this condition to be true
21+
heapsort(for: &array, range: range, by: areInIncreasingOrder)
22+
} else {
23+
//By default we use quicksort to sort our collection. The partition index method chose a pivot, and puts all the
24+
//elements less than pivot on the left, and the ones bigger than pivot on the right. At the end of the operation the
25+
//position of the pivot in the array is returned so that we can form the two partitions.
26+
let partIdx = partitionIndex(for: &array, subRange: range, by: areInIncreasingOrder)
27+
28+
//We can recursively call introsort implementation, decreasing the depthLimit for the left partition and the right partition.
29+
introSortImplementation(for: &array, range: range.lowerBound..<partIdx, depthLimit: depthLimit &- 1, by: areInIncreasingOrder)
30+
introSortImplementation(for: &array, range: partIdx..<range.upperBound, depthLimit: depthLimit &- 1, by: areInIncreasingOrder)
31+
}
32+
}

Introsort/Partition.swift

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import Foundation
2+
3+
public func partitionIndex<T>(for elements: inout [T], subRange range: Range<Int>, by areInIncreasingOrder: (T, T) -> Bool) -> Int {
4+
var lo = range.lowerBound
5+
var hi = elements.index(before: range.upperBound)
6+
7+
// Sort the first, middle, and last elements, then use the middle value
8+
// as the pivot for the partition.
9+
let half = elements.distance(from: lo, to: hi) / 2
10+
let mid = elements.index(lo, offsetBy: half)
11+
12+
sort3(in: &elements, a: lo, b: mid, c: hi, by: areInIncreasingOrder)
13+
let pivot = elements[mid]
14+
15+
while true {
16+
elements.formIndex(after: &lo)
17+
guard findLo(in: elements, pivot: pivot, from: &lo, to: hi, by: areInIncreasingOrder) else { break }
18+
elements.formIndex(before: &hi)
19+
guard findHi(in: elements, pivot: pivot, from: lo, to: &hi, by: areInIncreasingOrder) else { break }
20+
elements.swapAt(lo, hi)
21+
}
22+
23+
24+
return lo
25+
}
26+
27+
private func findLo<T>(in array: [T], pivot: T, from lo: inout Int, to hi: Int, by areInIncreasingOrder: (T, T)->Bool) -> Bool {
28+
while lo != hi {
29+
if !areInIncreasingOrder(array[lo], pivot) {
30+
return true
31+
}
32+
array.formIndex(after: &lo)
33+
}
34+
return false
35+
}
36+
37+
private func findHi<T>(in array: [T], pivot: T, from lo: Int, to hi: inout Int, by areInIncreasingOrder: (T, T)->Bool) -> Bool {
38+
while hi != lo {
39+
if areInIncreasingOrder(array[hi], pivot) { return true }
40+
array.formIndex(before: &hi)
41+
}
42+
return false
43+
}

Introsort/README.markdown

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# IntroSort
2+
3+
Goal: Sort an array from low to high (or high to low).
4+
5+
IntroSort is the algorithm used by swift to sort a collection. Introsort is an hybrid algorithm invented by David Musser in 1993 with the purpose of giving a generic sorting algorithm for the C++ standard library. The classic implementation of introsort expect a recursive Quicksort with fallback to Heapsort in case the recursion depth level reached a certain max. The maximum depends on the number of elements in the collection and it is usually 2 * log(n). The reason behind this “fallback” is that if Quicksort was not able to get the solution after 2 * log(n) recursions for a branch, probably it hit its worst case and it is degrading to complexity O( n^2 ). To optimise even further this algorithm, the swift implementation introduce an extra step in each recursion where the partition is sorted using InsertionSort if the count of the partition is less than 20.
6+
7+
The number 20 is an empiric number obtained observing the behaviour of InsertionSort with lists of this size.
8+
9+
Here's an implementation in Pseudocode:
10+
11+
```
12+
procedure sort(A : array):
13+
let maxdepth = ⌊log(length(A))⌋ × 2
14+
introSort(A, maxdepth)
15+
16+
procedure introsort(A, maxdepth):
17+
n ← length(A)
18+
if n < 20:
19+
insertionsort(A)
20+
else if maxdepth = 0:
21+
heapsort(A)
22+
else:
23+
p ← partition(A) // the pivot is selected using median of 3
24+
introsort(A[0:p], maxdepth - 1)
25+
introsort(A[p+1:n], maxdepth - 1)
26+
```
27+
28+
## An example
29+
30+
Let's walk through the example. The array is initially:
31+
32+
[ 10, 0, 3, 9, 2, 14, 8, 27, 1, 5, 8, -1, 26 ]
33+
34+
35+
for this example let's assume that `maxDepth` is **2** and that the size of the partition for the insertionSort to kick in is **5**
36+
37+
At the first iteration we run introSort on the full collection that counts 13 elements. the maxDepth is 2, therefore we fall in the else case where quicksort acts.
38+
39+
The `partition` method picks the first element, the median and the last, it sorts them and uses the new median as pivot.
40+
41+
[ 10, 8, 26 ] -> [ 8, 10, 26 ]
42+
43+
Our array is now
44+
45+
[ 8, 0, 3, 9, 2, 14, 10, 27, 1, 5, 8, -1, 26 ]
46+
47+
**10** is the pivot. After the choice of the pivot, the `partition` method swaps elements to get all the elements less than pivot on the left, and all the elements more or equal than pivot on the right.
48+
49+
[ 8, 0, 3, 9, 2, 1, 5, 8, -1, 10, 27, 14, 26 ]
50+
51+
Because of the swaps, the index of of pivot is now changed and returned. The next step of introsort is to call recursively itself for the two sub arrays:
52+
53+
less: [ 8, 0, 3, 9, 2, 1, 5, 8, -1, 10 ]
54+
greater: [ 27, 14, 26 ]
55+
56+
## maxDepth: 1, branch: less
57+
58+
[ 8, 0, 3, 9, 2, 1, 5, 8, -1, 10 ]
59+
60+
The count of the array is still more than 5 so we don't meet yet the conditions for insertion sort to kick in. At this iteration maxDepth is decreased by one but it is still more than zero, so heapsort will not act.
61+
62+
Just like in the previous iteration quicksort wins and the `partition` method choses a pivot and sorts the elemets less than pivot on the left and the elements more or equeal than pivot on the right.
63+
64+
array: [ 8, 0, 3, 9, 2, 1, 5, 8, -1, 10 ]
65+
pivot candidates: [ 8, 1, 10] -> [ 1, 8, 10]
66+
pivot: 8
67+
before partition: [ 1, 0, 3, 9, 2, 8, 5, 8, -1, 10 ]
68+
after partition: [ 1, 0, 3, -1, 2, 5, 8, 8, 9, 10 ]
69+
70+
less: [ 1, 0, 3, -1, 2, 5, 8 ]
71+
greater: [ 8, 9, 10 ]
72+
73+
## maxDepth: 0, branch: less
74+
75+
[ 1, 0, 3, -1, 2, 5, 8 ]
76+
77+
Just like before, introsort is recursively executed for `less` and greater. This time `less`has a count more than **5** so it will not be sorted with insertion sort, but the maxDepth decreased again by 1 is now 0 and heapsort takes over sorting the array.
78+
79+
heapsort -> [ -1, 0, 1, 2, 3, 5, 8 ]
80+
81+
## maxDepth: 0, branch: greater
82+
83+
[ 8, 9, 10 ]
84+
85+
following greater in this recursion, the count of elements is 3, which is less than 5, so this partition is sorted using insertionSort.
86+
87+
insertionSort -> [ 8, 9 , 10]
88+
89+
90+
## back to maxDepth = 1, branch: greater
91+
92+
[ 27, 14, 26 ]
93+
94+
At this point the original array has mutated to be
95+
96+
[ -1, 0, 1, 2, 3, 5, 8, 8, 9, 10, 27, 14, 26 ]
97+
98+
now the `less` partition is sorted and since the count of the `greater` partition is 3 it will be sorted with insertion sort `[ 14, 26, 27 ]`
99+
100+
The array is now successfully sorted
101+
102+
[ -1, 0, 1, 2, 3, 5, 8, 8, 9, 10, 14, 26, 27 ]
103+
104+
105+
## See also
106+
107+
[Introsort on Wikipedia](https://en.wikipedia.org/wiki/Introsort)
108+
[Introsort comparison with other sorting algorithms](http://agostini.tech/2017/12/18/swift-sorting-algorithm/)
109+
110+
*Written for Swift Algorithm Club by Giuseppe Lanza*

Introsort/Randomize.swift

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Foundation
2+
3+
public func randomize(n: Int) -> [Int] {
4+
var unsorted = [Int]()
5+
for _ in 0..<n {
6+
unsorted.append(Int(arc4random()))
7+
}
8+
return unsorted
9+
}

Introsort/Sort3.swift

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import Foundation
2+
3+
public func sort3<T>(in array: inout [T], a: Int, b: Int, c: Int, by areInIncreasingOrder: (T, T) -> Bool) {
4+
switch (areInIncreasingOrder(array[b], array[a]),
5+
areInIncreasingOrder(array[c], array[b])) {
6+
case (false, false): break
7+
case (true, true): array.swapAt(a, c)
8+
case (true, false):
9+
array.swapAt(a, b)
10+
11+
if areInIncreasingOrder(array[c], array[b]) {
12+
array.swapAt(b, c)
13+
}
14+
case (false, true):
15+
array.swapAt(b, c)
16+
17+
if areInIncreasingOrder(array[b], array[a]) {
18+
array.swapAt(a, b)
19+
}
20+
}
21+
}

README.markdown

+4
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ Fast sorts:
7373
- [Merge Sort](Merge%20Sort/)
7474
- [Heap Sort](Heap%20Sort/)
7575

76+
Hybrid sorts:
77+
78+
- [Introsort](Introsort/)
79+
7680
Special-purpose sorts:
7781

7882
- [Counting Sort](Counting%20Sort/)

0 commit comments

Comments
 (0)