Skip to content

Commit

Permalink
Implement quicksort
Browse files Browse the repository at this point in the history
  • Loading branch information
kylehughes committed Jun 22, 2024
1 parent f3ca5fd commit 4e400bc
Show file tree
Hide file tree
Showing 6 changed files with 632 additions and 3 deletions.
2 changes: 0 additions & 2 deletions Sources/SortStateUniversity/Merge Sort/MergeSort.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,6 @@ public struct MergeSort<Element>: Identifiable {
ongoingMerge = nil
output = input
partitionSize = 1

_ = self()
}

// MARK: Public Instance Interface
Expand Down
47 changes: 47 additions & 0 deletions Sources/SortStateUniversity/Quicksort/Quicksort+Partition.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// Quicksort+Partition.swift
// Sort State University
//
// Created by Kyle Hughes on 6/22/24.
//

extension Quicksort {
/// Represents a partition of the array being sorted by the ``Quicksort`` algorithm.
///
/// A partition is a section of the array that is being sorted independently. It is defined by a lower bound, an
/// upper bound, a current index, and a partition index. The ``Quicksort`` algorithm works by repeatedly
/// partitioning the array and sorting these partitions.
public struct Partition: Codable, Equatable, Hashable {
/// The upper bound of the partition in the array being sorted.
public let high: Elements.Index

/// The lower bound of the partition in the array being sorted.
public let low: Elements.Index

/// The current index being examined in the partitioning process.
///
/// This index moves from `low` towards `high`, comparing each element with the pivot element.
public var currentIndex: Elements.Index

/// The index that divides elements less than or equal to the pivot from those greater than the pivot.
///
/// Elements to the left of this index (inclusive) are less than or equal to the pivot, while elements to the
/// right are greater than the pivot.
public var partitionIndex: Elements.Index

/// Creates a new partition with the specified lower and upper bounds.
///
/// Initially, both `currentIndex` and `partitionIndex` are set to `low`.
///
/// - Parameter low: The lower bound of the partition.
/// - Parameter high: The upper bound of the partition.
@inlinable
public init(low: Elements.Index, high: Elements.Index) {
self.low = low
self.high = high

currentIndex = low
partitionIndex = low
}
}
}
232 changes: 232 additions & 0 deletions Sources/SortStateUniversity/Quicksort/Quicksort.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
//
// Quicksort.swift
// SortStateUniversity
//
// Created by Kyle Hughes on 6/22/24.
//

import Foundation

/// A divide-and-conquer sorting algorithm that uses a pivot element for partitioning the input array.
///
/// - SeeAlso: https://en.wikipedia.org/wiki/Quicksort
public struct Quicksort<Element>: Identifiable {
/// A type that represents the collection of the elements that the algorithm is sorting.
public typealias Elements = [Element]

/// The stable identity of the algorithm.
public let id: UUID

/// The given elements that the algorithm is sorting.
///
/// This value is constant and will not change after instantiation.
public let input: Elements

/// The current partition being sorted.
public private(set) var currentPartition: Partition?

/// The current result of applying the sorting algorithm to `input`.
///
/// This value is "live" and will change as the algorithm is executed. When the algorithm is finished this value
/// will contain the sorted result of `input`. It is primarily exposed to allow the internals of the algorithm to
/// be observed.
public private(set) var output: Elements

/// A stack of partitions to be sorted.
public private(set) var partitionStack: [Partition]

// MARK: Public Initialization

/// Creates an algorithm to sort the given input using quicksort.
///
/// - Parameter input: The elements to sort.
public init(input: Elements) {
self.input = input

currentPartition = nil
id = UUID()
output = input
partitionStack = [Partition(low: input.startIndex, high: input.index(before: input.endIndex))]
}
}

// MARK: - SortingAlgorithm Extension

extension Quicksort: SortingAlgorithm {
// MARK: Public Static Interface

@inlinable
public static var complexity: Complexity {
.linearithmic
}

@inlinable
public static var label: SortingAlgorithmLabel {
.quicksort
}

/// Returns the average number of comparisons that the algorithm will perform given an input with `n` elements.
///
/// This calculation uses the exact recurrence relation for Quicksort's average case.
///
/// - SeeAlso: Sedgewick, R., & Flajolet, P. (1996). An Introduction to the Analysis of Algorithms.
/// Addison-Wesley. Section 8.2: Quicksort.
/// - Parameter n: The number of elements.
/// - Returns: The average number of comparisons that the algorithm will perform.
@inlinable
public static func averageNumberOfComparisons(for n: Int) -> Int {
guard 1 < n else {
return 0
}

var dynamicProgrammingTable = Array(repeating: 0, count: n + 1)

for i in 2...n {
dynamicProgrammingTable[i] = i - 1 + 2 * (0..<i).reduce(0) { $0 + dynamicProgrammingTable[$1] } / i
}

return dynamicProgrammingTable[n]
}

/// Returns the maximum number of comparisons that the algorithm will perform given an input with `n` elements.
///
/// The worst case for quicksort occurs when the pivot is always the smallest or largest element.
///
/// - SeeAlso: Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2009). Introduction to Algorithms
/// (3rd ed.). MIT Press. Section 7.4.1: The worst-case partitioning.
/// - Parameter n: The number of elements.
/// - Returns: The maximum number of comparisons that the algorithm will perform.
@inlinable
public static func maximumNumberOfComparisons(for n: Int) -> Int {
guard 1 < n else {
return 0
}

return (n * (n - 1)) / 2
}

/// Returns the minimum number of comparisons that the algorithm will perform given an input with `n` elements.
///
/// The best case for quicksort occurs when the pivot always divides the array into two equal halves.
///
/// - SeeAlso: Knuth, D. E. (1998). The Art of Computer Programming, Volume 3: Sorting and Searching (2nd ed.).
/// Addison-Wesley Professional. Section 5.2.2: Sorting by exchanging.
/// - Parameter n: The number of elements.
/// - Returns: The minimum number of comparisons that the algorithm will perform.
@inlinable
public static func minimumNumberOfComparisons(for n: Int) -> Int {
guard 1 < n else {
return 0
}

var result = 0
var size = n

while 1 < size {
result += size - 1
size /= 2
}

return result
}

// MARK: Public Instance Interface

@inlinable
public var isFinished: Bool {
partitionStack.isEmpty && currentPartition == nil
}

public mutating func answer(_ answer: Comparison<Self>.Side) {
guard var partition = currentPartition else {
return
}

switch answer {
case .left:
output.swapAt(partition.currentIndex, partition.partitionIndex)
partition.partitionIndex += 1
partition.currentIndex += 1
case .right:
partition.currentIndex += 1
}

currentPartition = partition
}

public mutating func callAsFunction() -> SortingAlgorithmStep<Self> {
guard !isFinished else {
return .finished(output)
}

if currentPartition == nil {
guard let nextPartition = partitionStack.popLast() else {
return .finished(output)
}

currentPartition = nextPartition
}

guard let partition = currentPartition else {
return .finished(output)
}

guard partition.currentIndex >= partition.high else {
return .comparison(Comparison(source: self))
}

output.swapAt(partition.partitionIndex, partition.high)

let pivotIndex = partition.partitionIndex

if partition.low < pivotIndex {
partitionStack.append(Partition(low: partition.low, high: pivotIndex - 1))
}

if pivotIndex + 1 < partition.high {
partitionStack.append(Partition(low: pivotIndex + 1, high: partition.high))
}

currentPartition = nil

return self()
}

@inlinable
public func peekAtElement(for answer: Comparison<Self>.Side) -> Element? {
guard let partition = currentPartition else {
return nil
}

switch answer {
case .left:
return output[partition.currentIndex]
case .right:
return output[partition.high]
}
}
}

// MARK: - Decodable Extension

extension Quicksort: Decodable where Element: Decodable {
// NO-OP
}

// MARK: - Encodable Extension

extension Quicksort: Encodable where Element: Encodable {
// NO-OP
}

// MARK: - Equatable Extension

extension Quicksort: Equatable where Element: Equatable {
// NO-OP
}

// MARK: - Hashable Extension

extension Quicksort: Hashable where Element: Hashable {
// NO-OP
}
3 changes: 3 additions & 0 deletions Sources/SortStateUniversity/SortingAlgorithmLabel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,7 @@ extension SortingAlgorithmLabel {

/// The label of the ``MergeSort`` algorithm.
public static let merge = SortingAlgorithmLabel(id: "merge", name: "Merge Sort")

/// The label of the ``Quicksort`` algorithm.
public static let quicksort = SortingAlgorithmLabel(id: "quicksort", name: "Quicksort")
}
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,8 @@ where
@inlinable
public func test_peekAtElement() {
let input = [1, 2]
let sort = target(for: input)
var sort = target(for: input)
_ = sort()

XCTAssertEqual(sort.peekAtElement(for: .left), 1)
XCTAssertEqual(sort.peekAtElement(for: .right), 2)
Expand Down
Loading

0 comments on commit 4e400bc

Please sign in to comment.