Skip to content

Sorting Algorithms - Bubble, Selection, Insertion, Quicksort

Comparison of fundamental sorting algorithms with implementations, complexity analysis, and practical trade-offs. Includes quickselect for finding kth element without full sort.

Key Facts

  • Quicksort O(N log N) average is used in most standard library sort functions
  • Insertion sort O(N) on nearly sorted data makes it ideal for small/mostly-sorted arrays
  • Selection sort does fewer swaps than bubble sort (at most N-1) but same O(N^2) comparisons
  • Quicksort worst case O(N^2) occurs on already-sorted arrays with naive pivot selection
  • Quickselect finds kth smallest/largest in O(N) average without full sort

Patterns

Bubble Sort - O(N^2)

Each pass: compare adjacent pairs, swap if out of order. Largest unsorted element "bubbles" to end.

def bubble_sort(list):
    unsorted_until_index = len(list) - 1
    sorted = False
    while not sorted:
        sorted = True
        for i in range(unsorted_until_index):
            if list[i] > list[i+1]:
                list[i], list[i+1] = list[i+1], list[i]
                sorted = False
        unsorted_until_index -= 1
    return list

Selection Sort - O(N^2)

Each pass: find minimum in unsorted portion, swap to front. ~N^2/2 comparisons, at most N-1 swaps.

function selectionSort(array) {
  for (let i = 0; i < array.length - 1; i++) {
    let lowestNumberIndex = i;
    for (let j = i + 1; j < array.length; j++) {
      if (array[j] < array[lowestNumberIndex]) {
        lowestNumberIndex = j;
      }
    }
    if (lowestNumberIndex != i) {
      let temp = array[i];
      array[i] = array[lowestNumberIndex];
      array[lowestNumberIndex] = temp;
    }
  }
  return array;
}

Insertion Sort

Case Complexity
Best (nearly sorted) O(N)
Average O(N^2)
Worst (reverse sorted) O(N^2)

Maintain sorted left portion. For each new element, shift right until correct position found.

Quicksort - O(N log N) average

Partitioning: pick pivot, arrange elements so left < pivot < right. Recursively sort subarrays.

class SortableArray
  def partition!(left_index, right_index)
    pivot_index = right_index
    pivot = @array[pivot_index]
    right_index -= 1
    loop do
      left_index += 1 while @array[left_index] < pivot
      right_index -= 1 while @array[right_index] > pivot
      break if left_index >= right_index
      @array[left_index], @array[right_index] = @array[right_index], @array[left_index]
    end
    @array[left_index], @array[pivot_index] = @array[pivot_index], @array[left_index]
    left_index
  end

  def quicksort!(left_index, right_index)
    return if right_index - left_index <= 0
    pivot_index = partition!(left_index, right_index)
    quicksort!(left_index, pivot_index - 1)
    quicksort!(pivot_index + 1, right_index)
  end
end

Why O(N log N): log N partition rounds x N comparisons per round. Worst case O(N^2): pivot always lands at edge (already sorted). Fix: random pivot or median-of-three.

Quickselect - O(N) average

Find kth smallest/largest without full sort. After each partition, pivot is in final position. Only recurse into the half containing target index.

Comparison

Algorithm Best Average Worst Swaps
Bubble Sort O(N) O(N^2) O(N^2) Many
Selection Sort O(N^2) O(N^2) O(N^2) <= N-1
Insertion Sort O(N) O(N^2) O(N^2) Moderate
Quicksort O(N log N) O(N log N) O(N^2) Moderate

Gotchas

  • Quicksort is not stable (equal elements may change relative order); use mergesort if stability required
  • Already-sorted input is quicksort's worst case with rightmost pivot selection
  • Insertion sort's O(N) best case makes it the practical choice for small subarrays (often used as base case in quicksort)
  • Selection sort's advantage is minimal write operations - useful when writes are expensive

See Also

  • [[data-structures-fundamentals]] - Big O analysis, arrays
  • [[trees-and-graphs]] - heap sort via priority queue
  • [[algorithm-problem-patterns]] - when to sort as preprocessing step