Skip to content

Sorting

Sorting refers to the process of arranging data in a particular order, typically ascending or descending. Efficient sorting is crucial for optimizing the performance of other algorithms (such as search and merge operations) and for presenting data in a human-readable format.

Types of Sorting Algorithms

Sorting algorithms can be broadly classified based on their approach, stability, and time complexity. Here are some of the most common types:

1. Bubble Sort

A simple comparison-based algorithm that repeatedly steps through the list compares adjacent elements and swaps them if they are in the wrong order. It is easy to implement but inefficient for large datasets.

  • Time Complexity: O(n²)
  • Space Complexity: O(1)
  • Stable: Yes
fun bubbleSort(arr: MutableList<Int>) {
    var swap = false
    for(i in 0 until arr.size) {
        swap = false
        for (j in 1 until arr.size - i) {
            if (arr[j - 1] > arr[j])
                arr[j - 1] = arr[j].also { arr[j] = arr[j - 1]; swap = true }
        }
        if(!swap) return
    }
}

2. Selection Sort

This algorithm divides the input list into two parts: a sorted sublist and an unsorted sublist. It repeatedly selects the smallest (or largest) element from the unsorted sublist and moves it to the end of the sorted sublist.

  • Time Complexity: O(n²)
  • Space Complexity: O(1)
  • Stable: No
fun selectionSort(arr: MutableList<Int>) {
    for(i in 0 until arr.size) {
        var smallest = i
        for(j in i + 1 until arr.size) {
            if(arr[j] < arr[smallest]) {
                smallest = j
            }
        }
        arr[i] = arr[smallest].also { arr[smallest] = arr[i] }
    }
}

3. Insertion Sort

Builds the final sorted array one item at a time. It is efficient for small datasets and mostly sorted arrays.

  • Time Complexity: O(n²)
  • Space Complexity: O(1)
  • Stable: Yes
fun insertionSort(arr: MutableList<Int>) {
    for(i in 1 until arr.size) {
        for(j in i downTo 0) {
            if(arr[j] < arr[j - 1]) {
                arr[j] = arr[j-1].also{arr[j - 1] == arr[j] }
            } else break
        }
    }
}

4. Shell Sort

An optimization over insertion sort that allows the exchange of far-apart elements. It uses a gap sequence to compare and sort elements.

  • Time Complexity: Depends on gap sequence (best: O(n log n), worst: O(n²))
  • Space Complexity: O(1)
  • Stable: No
fun shellSort(arr: MutableList<Int>) {
    var gap = arr.size / 2
    while (gap > 0) {
        for (i in gap until arr.size) {
            val temp = arr[i]
            var j = i
            while (j >= gap && arr[j - gap] > temp) {
                arr[j] = arr[j - gap]
                j -= gap
            }
            arr[j] = temp
        }
        gap /= 2
    }
}

5. Merge Sort

A divide-and-conquer algorithm that divides the array into halves, sorts them, and then merges the sorted halves. It is efficient and stable.

  • Time Complexity: O(n log n)
  • Space Complexity: O(n)
  • Stable: Yes
fun mergeSort(arr: MutableList<Int>, l: Int, r: Int) {
    if (r - l <= 1) return
    val mid = (r + l) / 2
    mergeSort(arr, 0, mid)
    mergeSort(arr, mid, r)
    merge(arr, l, mid, r)
}

fun merge(arr: MutableList<Int>, l: Int, m: Int, r: Int) {
    val n1 = m - l + 1
    val n2 = r - m
    val left = MutableList(n1) { arr[l + it] }
    val right = MutableList(n2) { arr[m + 1 + it] }
    var i = 0
    var j = 0
    var k = l
    while (i < n1 && j < n2) {
        if (left[i] <= right[j]) {
            arr[k++] = left[i++]
        } else {
            arr[k++] = right[j++]
        }
    }
    while (i < n1) {
        arr[k++] = left[i++]
    }
    while (j < n2) {
        arr[k++] = right[j++]
    }
}

6. Quick Sort

Another divide-and-conquer algorithm that selects a 'pivot' element and partitions the array around the pivot. It is very efficient for large datasets.

  • Time Complexity: O(n log n) average, O(n²) worst
  • Space Complexity: O(log n)
  • Stable: No
fun quickSort(arr: MutableList<Int>, low: Int = 0, high: Int = arr.size - 1) {
    if (low < high) {
        val pi = partition(arr, low, high)
        quickSort(arr, low, pi - 1)
        quickSort(arr, pi + 1, high)
    }
}

fun partition(arr: MutableList<Int>, low: Int, high: Int): Int {
    val pivot = arr[high]
    var i = low - 1
    for (j in low until high) {
        if (arr[j] <= pivot) {
            i++
            arr[i] = arr[j].also { arr[j] = arr[i] }
        }
    }
    arr[i + 1] = arr[high].also { arr[high] = arr[i + 1] }
    return i + 1
}

Randomized Quick Sort

A variation where the pivot is chosen randomly to avoid worst-case scenarios.

7. Tree Sort

Use a binary search tree to insert all elements and then perform an in-order traversal to retrieve them in sorted order.

  • Time Complexity: O(n log n) average, O(n²) worst
  • Space Complexity: O(n)
  • Stable: Yes (if self-balancing tree is used)

8. Counting Sort

A non-comparison-based algorithm suitable for sorting integers within a specific range. It counts the occurrences of each value and calculates their positions.

  • Time Complexity: O(n + k) where k is the range of input
  • Space Complexity: O(k)
  • Stable: Yes
fun countingSort(arr: MutableList<Int>) {
    val min = arr.min()
    val k = arr.max() - min
    val c = IntArray(k) { 0 }
    arr.forEach { c[it - min] += 1 }
    var i = 0
    for(j in 0 until k) {
        while(c[j] > 0) {
            arr[i++] = j - min
            c[j]--
        }
    }
}

9. Bucket Sort

Divides the input into several buckets and sorts each bucket individually (often using another sorting algorithm). Suitable for uniformly distributed data.

  • Time Complexity: O(n + k)
  • Space Complexity: O(n + k)
  • Stable: Yes
fun bucketSort(arr: MutableList<Int>) {
    if (arr.isEmpty()) return
    val minValue = arr.minOrNull() ?: return
    val maxValue = arr.maxOrNull() ?: return
    val bucketCount = arr.size
    val buckets = Array<MutableList<Int>>(bucketCount) { mutableListOf() }
    val range = (maxValue - minValue + 1).toDouble() / bucketCount
    for (num in arr) {
        val idx = ((num - minValue) / range).toInt().coerceAtMost(bucketCount - 1)
        buckets[idx].add(num)
    }
    arr.clear()
    for (bucket in buckets) {
        bucket.sort() // Any sorting algorithm can be used
        arr.addAll(bucket)
    }
}

10. Radix Sort

Processes integer keys by individual digits, starting from the least significant digit to the most significant. It uses a stable sub-sorting algorithm (like counting sort) for each digit.

  • Time Complexity: O(nk) where k is the number of digits
  • Space Complexity: O(n + k)
  • Stable: Yes

11. External Sorting

Used for massive datasets that do not fit into memory. It divides data into manageable chunks, sorts them in memory, and then merges the sorted chunks.

Examples: Given 900 Mb data on disk, sort it using 100 Mb RAM.

  • Read 100 Mb of data, sort it and write it to disk.
  • Read the next 100 Mb data sort it and write to disk. Repeat for all chunks.
  • Read 10 Mb from each sorted chunk (9 chunks total) and the remaining 10 Mb for buffer.
  • Using merge logic from merge sort, write the merge results into buffer. When the output buffer is full, flush it to the disk. When any of 10Mb is used, load the next 10Mb from the chunks.

Stability in Sorting

A sorting algorithm is stable if it preserves the relative order of equal elements. Stability is important when sorting records with multiple fields.

Comparison of Sorting Algorithms

Algorithm Time Complexity Space Complexity Stable
Bubble Sort O(n²) O(1) Yes
Selection Sort O(n²) O(1) No
Insertion Sort O(n²) O(1) Yes
Shell Sort O(n log n)* O(1) No
Merge Sort O(n log n) O(n) Yes
Quick Sort O(n log n)* O(log n) No
Tree Sort O(n log n)* O(n) Yes
Counting Sort O(n + k) O(k) Yes
Bucket Sort O(n + k) O(n + k) Yes
Radix Sort O(nk) O(n + k) Yes
External Sorting O(n log n) O(n) Depends

*Depends on implementation and input data.

Choosing the Right Sorting Algorithm

  • For small or nearly sorted datasets: Insertion Sort
  • For large datasets in memory: Merge Sort or Quick Sort
  • For data with a small range of integer keys: Counting Sort, Radix Sort, or Bucket Sort
  • For massive datasets (external storage): External Sorting