Compare the common sorting algorithms by time, space and stability.
Merge and heap sort are O(n log n) always, quick sort on average, the simple sorts O(n²):
- Insertion, and bubble with an early exit: O(1) space, stable, O(n) on sorted input.
- Selection: O(1) space, not stable.
- Merge: O(n) space, stable.
- Quick: O(n²) worst, O(log n) stack on average, not stable.
- Heap: O(1) space, not stable.
- Counting: O(n + k), stable.
How do you implement binary search, and what are the common bugs?
Keep a range
[lo, hi]that holds the target if it exists, check the middle, and discard the half that cannot hold it. O(log n) on a sorted array.Pythondef binary_search(nums, target): lo, hi = 0, len(nums) - 1 while lo <= hi: mid = (lo + hi) // 2 if nums[mid] == target: return mid if nums[mid] < target: lo = mid + 1 else: hi = mid - 1 return -1 nums = [1, 3, 5, 7, 9, 11] print(binary_search(nums, 7), binary_search(nums, 4))Watch for
while lo < hion a closed range (skips the last candidate),lo = mid(loops forever), and in Java or C++(lo + hi) / 2overflowing. Watch binary search halve the range.Practice it: Binary Search, Square Root (Integer)
What is the difference between merge sort and quick sort?
Both are divide and conquer. Merge sort works while merging: O(n log n) always, stable, O(n) extra space, good for linked lists. Quick sort works while partitioning around a pivot: O(n²) at worst (rare with a random pivot), not stable, O(log n) stack on average, and usually faster on arrays.
Pythondef merge_sort(a): if len(a) <= 1: return a mid = len(a) // 2 left, right = merge_sort(a[:mid]), merge_sort(a[mid:]) out, i, j = [], 0, 0 while i < len(left) and j < len(right): if left[i] <= right[j]: # <= keeps it stable out.append(left[i]) i += 1 else: out.append(right[j]) j += 1 return out + left[i:] + right[j:] print(merge_sort([38, 27, 43, 3, 9, 82, 10]))Practice it: Merge Sorted Array
How does quick sort's partition work, and how do you avoid the O(n²) worst case?
Partition picks a pivot and moves smaller values before it and the rest after, leaving the pivot in its final place. Lomuto does it in one pass:
Pythonimport random def quick_sort(a, lo=0, hi=None): if hi is None: hi = len(a) - 1 if lo >= hi: return a p = random.randint(lo, hi) # random pivot a[p], a[hi] = a[hi], a[p] pivot, boundary = a[hi], lo for i in range(lo, hi): if a[i] < pivot: a[i], a[boundary] = a[boundary], a[i] boundary += 1 a[boundary], a[hi] = a[hi], a[boundary] quick_sort(a, lo, boundary - 1) quick_sort(a, boundary + 1, hi) return a print(quick_sort([5, 2, 9, 1, 5, 6, 3]))Pivots that split badly every time, like the last element of a sorted array, give O(n²); a random pivot avoids it. See quick sort partition an array.
Practice it: Sort Colors
Does Python's
sorted()keep people with the same age in their input order?Pythonpeople = [("Asha", 25), ("Ravi", 30), ("Neha", 25), ("John", 30)] print([name for name, age in sorted(people, key=lambda p: p[1])])Python's sort is guaranteed stable: items with equal keys keep their input order, so Asha stays before Neha and Ravi before John. To break ties by name in one call, use
key=lambda p: (p[1], p[0]).How do you search in a rotated sorted array in O(log n)?
Binary search, using the fact that one half of
[lo, hi]is always sorted. Comparenums[lo]withnums[mid]to find that half, search it if the target is in its range, otherwise search the other.Pythondef search_rotated(nums, target): lo, hi = 0, len(nums) - 1 while lo <= hi: mid = (lo + hi) // 2 if nums[mid] == target: return mid if nums[lo] <= nums[mid]: # left half sorted if nums[lo] <= target < nums[mid]: hi = mid - 1 else: lo = mid + 1 else: # right half sorted if nums[mid] < target <= nums[hi]: lo = mid + 1 else: hi = mid - 1 return -1 nums = [4, 5, 6, 7, 0, 1, 2] print(search_rotated(nums, 0), search_rotated(nums, 3))With duplicates like
[1, 1, 1, 3, 1]the worst case becomes O(n).Practice it: Search in Rotated Sorted Array, Find Minimum in Rotated Sorted Array
What is binary search on the answer?
When you need the smallest or largest value meeting a monotonic condition (if speed k works, every faster speed works), binary search over possible answers. With an O(n) check, O(n log range).
Pythondef min_eating_speed(piles, hours): def can_finish(speed): return sum((p + speed - 1) // speed for p in piles) <= hours lo, hi = 1, max(piles) while lo < hi: mid = (lo + hi) // 2 if can_finish(mid): hi = mid # mid works, try smaller else: lo = mid + 1 return lo print(min_eating_speed([3, 6, 7, 11], 8))Look for "minimum capacity" or "minimize the largest sum" with large limits, and argue the check is monotonic.
Practice it: Koko Eating Bananas, Split Array Largest Sum
How do you find the k-th largest element, and which approach is fastest?
Sort in O(n log n), keep a min heap of size k in O(n log k), or run quickselect in O(n) average, O(n²) worst. The heap wins when data streams in or k is small.
Pythonimport heapq def kth_largest(nums, k): heap = nums[:k] heapq.heapify(heap) for x in nums[k:]: if x > heap[0]: heapq.heapreplace(heap, x) return heap[0] print(kth_largest([3, 2, 1, 5, 6, 4], 2)) print(heapq.nlargest(2, [3, 2, 1, 5, 6, 4]))Quickselect partitions like quick sort but recurses into one side: n + n/2 + n/4 and so on, O(n).
Practice it: Kth Largest Element in an Array, Top K Frequent Elements
Why can't a comparison sort be faster than O(n log n)?
A comparison sort must tell apart all n! orderings, and each comparison has two outcomes, so it needs at least log₂(n!) comparisons, which grows like n log n. Counting and radix sort beat it by using values as indexes, which needs integer keys in a limited range. See counting sort place values by index.
How do you find the first and last position of a target in a sorted array?
Run binary search twice: once for the first index whose value is
>= target, once for the first index whose value is> target, and step back one. O(log n), while finding one copy and walking outward is O(n) when the target repeats.Pythondef lower_bound(nums, target): lo, hi = 0, len(nums) # half-open range [lo, hi) while lo < hi: mid = (lo + hi) // 2 if nums[mid] < target: lo = mid + 1 else: hi = mid return lo def search_range(nums, target): first = lower_bound(nums, target) if first == len(nums) or nums[first] != target: return [-1, -1] return [first, lower_bound(nums, target + 1) - 1] # integers only print(search_range([5, 7, 7, 8, 8, 10], 8)) print(search_range([5, 7, 7, 8, 8, 10], 6))It prints
[3, 4]and[-1, -1]. Python'sbisect_leftandbisect_rightare these two searches, and the number of copies islast - first + 1.Practice it: Find First and Last Position of Element in Sorted Array, Binary Search
Sorting and Searching Interview Questions
Sorting trade-offs, binary search and its variants, and selecting the k-th element.
10 questions