Module 8: Sorting & Searching Algorithms

Both languages ship a built-in sort — but knowing how one actually works underneath is a different skill, and one interviewers ask for directly. This module builds six classic sorting algorithms from scratch, then reuses Module 5's binary search as the base for four searching variants: first/last occurrence, a rotated sorted array, and finding a peak.

Module 8 of 12 Problems 71–80 JS + Python ~60 Min

By the end of this module, you'll be able to

  • Implement bubble, selection, insertion, merge, quick and counting sort from memory
  • Explain why merge sort and quick sort split the problem instead of scanning repeatedly
  • Adapt binary search's halving logic to unusual targets: a boundary, a rotation point, a peak

1. Problems 71–80

Same format as the previous modules: expand a problem to see the approach and both commented solutions. All six sorts use the same test input, [5, 2, 9, 1, 5, 6], so you can compare how differently each one gets to the same answer.

P71

Bubble Sort

Repeatedly swap adjacent out-of-order pairs.

Approach: on each full pass, compare every adjacent pair and swap if they're out of order — the largest unsorted value "bubbles" to its correct position by the end of each pass, so each pass can check one element fewer than the last.

JavaScript
bubble-sort.js
// Repeatedly swap adjacent out-of-order pairs; the largest unsorted value
// "bubbles" to the end on each full pass.
function bubbleSort(arr) {
  const result = [...arr];
  for (let i = 0; i < result.length - 1; i++) {
    for (let j = 0; j < result.length - 1 - i; j++) {
      if (result[j] > result[j + 1]) {
        [result[j], result[j + 1]] = [result[j + 1], result[j]]; // swap
      }
    }
  }
  return result;
}

console.log(bubbleSort([5, 2, 9, 1, 5, 6])); // [1, 2, 5, 5, 6, 9]
Python
bubble_sort.py
def bubble_sort(items: list) -> list:
    result = items[:]
    n = len(result)
    for i in range(n - 1):
        for j in range(n - 1 - i):
            if result[j] > result[j + 1]:
                result[j], result[j + 1] = result[j + 1], result[j]  # swap
    return result

print(bubble_sort([5, 2, 9, 1, 5, 6]))  # [1, 2, 5, 5, 6, 9]
P72

Selection Sort

Find the minimum of the unsorted remainder, swap it into place.

Approach: scan the unsorted remainder for its smallest value, then swap that value into the next sorted position — one swap per pass, unlike bubble sort's many.

JavaScript
selection-sort.js
// Find the minimum of the unsorted remainder and swap it into place, one
// position at a time.
function selectionSort(arr) {
  const result = [...arr];
  for (let i = 0; i < result.length - 1; i++) {
    let minIndex = i;
    for (let j = i + 1; j < result.length; j++) {
      if (result[j] < result[minIndex]) minIndex = j; // track the smallest so far
    }
    if (minIndex !== i) {
      [result[i], result[minIndex]] = [result[minIndex], result[i]];
    }
  }
  return result;
}

console.log(selectionSort([5, 2, 9, 1, 5, 6])); // [1, 2, 5, 5, 6, 9]
Python
selection_sort.py
def selection_sort(items: list) -> list:
    result = items[:]
    n = len(result)
    for i in range(n - 1):
        min_index = i
        for j in range(i + 1, n):
            if result[j] < result[min_index]:
                min_index = j  # track the smallest so far
        if min_index != i:
            result[i], result[min_index] = result[min_index], result[i]
    return result

print(selection_sort([5, 2, 9, 1, 5, 6]))  # [1, 2, 5, 5, 6, 9]
P73

Insertion Sort

Grow a sorted region at the front, one element at a time.

Approach: take the next unsorted element and slide it backward through the already-sorted region until it lands in the right spot — the same motion as sorting a hand of playing cards.

JavaScript
insertion-sort.js
// Grow a sorted region at the front one element at a time, sliding each new
// element backward until it lands in the right spot.
function insertionSort(arr) {
  const result = [...arr];
  for (let i = 1; i < result.length; i++) {
    const current = result[i];
    let j = i - 1;
    while (j >= 0 && result[j] > current) {
      result[j + 1] = result[j]; // shift larger elements one step right
      j--;
    }
    result[j + 1] = current; // drop it into the gap left behind
  }
  return result;
}

console.log(insertionSort([5, 2, 9, 1, 5, 6])); // [1, 2, 5, 5, 6, 9]
Python
insertion_sort.py
def insertion_sort(items: list) -> list:
    result = items[:]
    for i in range(1, len(result)):
        current = result[i]
        j = i - 1
        while j >= 0 and result[j] > current:
            result[j + 1] = result[j]  # shift larger elements one step right
            j -= 1
        result[j + 1] = current  # drop it into the gap left behind
    return result

print(insertion_sort([5, 2, 9, 1, 5, 6]))  # [1, 2, 5, 5, 6, 9]
P74

Merge Sort

Split in half recursively, then merge sorted halves back together.

Approach: split the list in half recursively down to single elements (always sorted on their own), then merge pairs of already-sorted halves back together — reusing the exact two-pointer merge from Module 3's "merge two sorted lists" problem.

JavaScript
merge-sort.js
// Split in half recursively down to single elements, then merge pairs of
// already-sorted halves back together (reusing Module 3's mergeSorted idea).
function mergeSort(arr) {
  if (arr.length <= 1) return arr; // a list of 0 or 1 elements is already sorted
  const mid = Math.floor(arr.length / 2);
  const left = mergeSort(arr.slice(0, mid));
  const right = mergeSort(arr.slice(mid));
  return merge(left, right);
}

function merge(a, b) {
  const merged = [];
  let i = 0, j = 0;
  while (i < a.length && j < b.length) {
    merged.push(a[i] <= b[j] ? a[i++] : b[j++]);
  }
  return merged.concat(a.slice(i)).concat(b.slice(j));
}

console.log(mergeSort([5, 2, 9, 1, 5, 6])); // [1, 2, 5, 5, 6, 9]
Python
merge_sort.py
def merge_sort(items: list) -> list:
    if len(items) <= 1:  # a list of 0 or 1 elements is already sorted
        return items
    mid = len(items) // 2
    left = merge_sort(items[:mid])
    right = merge_sort(items[mid:])
    return merge(left, right)

def merge(a: list, b: list) -> list:
    merged = []
    i = j = 0
    while i < len(a) and j < len(b):
        if a[i] <= b[j]:
            merged.append(a[i]); i += 1
        else:
            merged.append(b[j]); j += 1
    return merged + a[i:] + b[j:]

print(merge_sort([5, 2, 9, 1, 5, 6]))  # [1, 2, 5, 5, 6, 9]
P75

Quick Sort

Pick a pivot, partition around it, recurse on each side.

Approach: pick the first element as a pivot, split the rest into "smaller than pivot" and "greater than or equal to pivot" buckets, recursively sort each bucket, then stitch them back together with the pivot in the middle.

JavaScript
quick-sort.js
// Pick a pivot, partition everything into "less than" and "greater than"
// buckets around it, then recursively sort each bucket.
function quickSort(arr) {
  if (arr.length <= 1) return arr;
  const [pivot, ...rest] = arr;
  const left = rest.filter((n) => n < pivot);
  const right = rest.filter((n) => n >= pivot);
  return [...quickSort(left), pivot, ...quickSort(right)];
}

console.log(quickSort([5, 2, 9, 1, 5, 6])); // [1, 2, 5, 5, 6, 9]
Python
quick_sort.py
def quick_sort(items: list) -> list:
    if len(items) <= 1:
        return items
    pivot, *rest = items
    left = [n for n in rest if n < pivot]
    right = [n for n in rest if n >= pivot]
    return quick_sort(left) + [pivot] + quick_sort(right)

print(quick_sort([5, 2, 9, 1, 5, 6]))  # [1, 2, 5, 5, 6, 9]
P76

Counting Sort

Sort non-negative integers by tallying, not comparing.

Approach: only works for non-negative integers in a known range. Count how many times each value appears, then write each value back out that many times, in order — no comparisons between elements at all.

JavaScript
counting-sort.js
// Only works for non-negative integers in a known range -- count how many
// times each value appears, then read the counts back out in order.
function countingSort(arr) {
  const max = Math.max(...arr);
  const counts = new Array(max + 1).fill(0);
  for (const n of arr) counts[n]++; // tally each value
  const result = [];
  for (let value = 0; value <= max; value++) {
    for (let i = 0; i < counts[value]; i++) result.push(value); // write it out `count` times
  }
  return result;
}

console.log(countingSort([5, 2, 9, 1, 5, 6])); // [1, 2, 5, 5, 6, 9]
Python
counting_sort.py
def counting_sort(items: list) -> list:
    maximum = max(items)
    counts = [0] * (maximum + 1)
    for n in items:
        counts[n] += 1  # tally each value
    result = []
    for value in range(maximum + 1):
        result.extend([value] * counts[value])  # write it out `count` times
    return result

print(counting_sort([5, 2, 9, 1, 5, 6]))  # [1, 2, 5, 5, 6, 9]
P77

Find the First Occurrence in a Sorted Array

[1,2,2,2,3,4] searching for 2 → index 1, the earliest one.

Approach: a variant of Module 5's binary search. A plain binary search stops the moment it finds any match — to find the first one, record the match but keep narrowing into the left half, in case an earlier match exists.

JavaScript
first-occurrence.js
// Standard binary search stops at ANY match. To find the FIRST one, keep
// searching the left half even after finding a match, in case an earlier one exists.
function firstOccurrence(sortedArr, target) {
  let low = 0, high = sortedArr.length - 1, result = -1;
  while (low <= high) {
    const mid = Math.floor((low + high) / 2);
    if (sortedArr[mid] === target) {
      result = mid; // record it, but keep looking further left
      high = mid - 1;
    } else if (sortedArr[mid] < target) {
      low = mid + 1;
    } else {
      high = mid - 1;
    }
  }
  return result;
}

console.log(firstOccurrence([1, 2, 2, 2, 3, 4], 2)); // 1
Python
first_occurrence.py
def first_occurrence(sorted_items: list, target) -> int:
    low, high, result = 0, len(sorted_items) - 1, -1
    while low <= high:
        mid = (low + high) // 2
        if sorted_items[mid] == target:
            result = mid  # record it, but keep looking further left
            high = mid - 1
        elif sorted_items[mid] < target:
            low = mid + 1
        else:
            high = mid - 1
    return result

print(first_occurrence([1, 2, 2, 2, 3, 4], 2))  # 1
P78

Find the Last Occurrence in a Sorted Array

[1,2,2,2,3,4] searching for 2 → index 3, the last one.

Approach: the mirror image of the previous problem — record a match and keep narrowing into the right half instead of the left.

JavaScript
last-occurrence.js
// The mirror image of the previous problem: keep searching right after a match.
function lastOccurrence(sortedArr, target) {
  let low = 0, high = sortedArr.length - 1, result = -1;
  while (low <= high) {
    const mid = Math.floor((low + high) / 2);
    if (sortedArr[mid] === target) {
      result = mid; // record it, but keep looking further right
      low = mid + 1;
    } else if (sortedArr[mid] < target) {
      low = mid + 1;
    } else {
      high = mid - 1;
    }
  }
  return result;
}

console.log(lastOccurrence([1, 2, 2, 2, 3, 4], 2)); // 3
Python
last_occurrence.py
def last_occurrence(sorted_items: list, target) -> int:
    low, high, result = 0, len(sorted_items) - 1, -1
    while low <= high:
        mid = (low + high) // 2
        if sorted_items[mid] == target:
            result = mid  # record it, but keep looking further right
            low = mid + 1
        elif sorted_items[mid] < target:
            low = mid + 1
        else:
            high = mid - 1
    return result

print(last_occurrence([1, 2, 2, 2, 3, 4], 2))  # 3
P79

Search in a Rotated Sorted Array

[4,5,6,7,0,1,2] searching for 0 → index 4.

Approach: a rotated sorted array is still "half sorted" at every midpoint — at least one side is guaranteed to be in proper ascending order. Check which side that is, then decide whether the target could be hiding in it.

JavaScript
search-rotated.js
// A rotated sorted array is still "half sorted" at every step: at least one
// side of any midpoint is guaranteed to be in proper order. Check which side
// is sorted, then decide if the target could be in it.
function searchRotated(arr, target) {
  let low = 0, high = arr.length - 1;
  while (low <= high) {
    const mid = Math.floor((low + high) / 2);
    if (arr[mid] === target) return mid;
    if (arr[low] <= arr[mid]) { // left half is sorted
      if (arr[low] <= target && target < arr[mid]) high = mid - 1;
      else low = mid + 1;
    } else { // right half is sorted
      if (arr[mid] < target && target <= arr[high]) low = mid + 1;
      else high = mid - 1;
    }
  }
  return -1;
}

console.log(searchRotated([4, 5, 6, 7, 0, 1, 2], 0)); // 4
Python
search_rotated.py
def search_rotated(items: list, target) -> int:
    low, high = 0, len(items) - 1
    while low <= high:
        mid = (low + high) // 2
        if items[mid] == target:
            return mid
        if items[low] <= items[mid]:  # left half is sorted
            if items[low] <= target < items[mid]:
                high = mid - 1
            else:
                low = mid + 1
        else:  # right half is sorted
            if items[mid] < target <= items[high]:
                low = mid + 1
            else:
                high = mid - 1
    return -1

print(search_rotated([4, 5, 6, 7, 0, 1, 2], 0))  # 4
P80

Find a Peak Element

[1,3,20,4,1,0] → index 2 (value 20), bigger than both neighbors.

Approach: binary search still works even though the array isn't sorted, because at any midpoint you can tell which direction is "uphill" — walk toward the slope that's still climbing, and you'll land on a peak.

JavaScript
find-peak.js
// A peak is bigger than both neighbors. Binary search works even though the
// array isn't sorted: walk toward whichever side is still climbing uphill.
function findPeak(arr) {
  let low = 0, high = arr.length - 1;
  while (low < high) {
    const mid = Math.floor((low + high) / 2);
    if (arr[mid] > arr[mid + 1]) {
      high = mid; // downhill to the right -- the peak is at mid or to the left
    } else {
      low = mid + 1; // uphill to the right -- the peak is further right
    }
  }
  return low; // low === high: the peak's index
}

console.log(findPeak([1, 3, 20, 4, 1, 0])); // 2 (value 20)
Python
find_peak.py
def find_peak(items: list) -> int:
    low, high = 0, len(items) - 1
    while low < high:
        mid = (low + high) // 2
        if items[mid] > items[mid + 1]:
            high = mid  # downhill to the right -- the peak is at mid or to the left
        else:
            low = mid + 1  # uphill to the right -- the peak is further right
    return low  # low == high: the peak's index

print(find_peak([1, 3, 20, 4, 1, 0]))  # 2 (value 20)

2. Key Takeaways

  • Bubble, selection and insertion sort are all O(n²) — fine for small or nearly-sorted lists, but merge sort and quick sort's O(n log n) matters once the list gets large.
  • Merge sort's "split, recurse, merge" and quick sort's "partition, recurse, combine" are the two dominant shapes behind almost every divide-and-conquer algorithm you'll meet later.
  • Binary search isn't just for "find this exact value" — the same halving logic finds a boundary (first/last occurrence), survives a rotation, and even works on unsorted data if there's still a monotonic direction to follow (peak-finding).