Master the paradigm that powers sorting, searching, and matrix algorithms. Learn to decompose problems, analyze recursive complexity with the Master Theorem, and implement proven algorithms from Merge Sort to Closest Pair.
Divide & Conquer is a top-down algorithmic paradigm that solves problems by breaking them into smaller, independent subproblems, solving those recursively, and combining their solutions.
Many problems have naturally recursive structure. By dividing into equal-sized subproblems, logarithmic trees emerge, leading to O(n log n) complexity rather than O(n²) naive approaches. Examples: Merge Sort, Quick Sort, Binary Search.
The Master Theorem provides a closed-form solution for recurrences of the form: T(n) = a·T(n/b) + f(n), where a ≥ 1, b > 1.
| Recurrence | Values | Case | Solution |
|---|---|---|---|
| T(n) = 2T(n/2) + n | a=2, b=2, f(n)=n | Case 2 | Θ(n log n) |
| T(n) = 3T(n/2) + n | a=3, b=2, f(n)=n | Case 1 (n < n^1.58) | Θ(n^1.58) |
| T(n) = T(n/2) + n² | a=1, b=2, f(n)=n² | Case 3 | Θ(n²) |
| T(n) = 4T(n/2) + n | a=4, b=2, f(n)=n | Case 1 (n < n²) | Θ(n²) |
A stable, comparison-based sorting algorithm that divides the array into halves, recursively sorts them, and merges. Guaranteed O(n log n) time, O(n) space.
public class MergeSort { public static void Sort(int[] arr) { if (arr.Length <= 1) return; MergeSortHelper(arr, 0, arr.Length - 1); } private static void MergeSortHelper(int[] arr, int left, int right) { // Base case: single element if (left >= right) return; // Divide int mid = left + (right - left) / 2; // Conquer MergeSortHelper(arr, left, mid); MergeSortHelper(arr, mid + 1, right); // Combine Merge(arr, left, mid, right); } private static void Merge(int[] arr, int left, int mid, int right) { // Create temp arrays int[] leftArr = new int[mid - left + 1]; int[] rightArr = new int[right - mid]; // Copy data to temp arrays for (int i = 0; i < leftArr.Length; i++) leftArr[i] = arr[left + i]; for (int i = 0; i < rightArr.Length; i++) rightArr[i] = arr[mid + 1 + i]; // Merge the temp arrays back int l = 0, r = 0, k = left; while (l < leftArr.Length && r < rightArr.Length) { if (leftArr[l] <= rightArr[r]) { arr[k++] = leftArr[l++]; } else { arr[k++] = rightArr[r++]; } } // Copy remaining elements while (l < leftArr.Length) arr[k++] = leftArr[l++]; while (r < rightArr.Length) arr[k++] = rightArr[r++]; } }
Iterative version that avoids recursion overhead by merging subarrays of increasing size (1, 2, 4, 8, ...).
public static void MergeSortIterative(int[] arr) { int n = arr.Length; // Start with merge subarrays of size 1, then 2, 4, 8, ... for (int currSize = 1; currSize < n; currSize *= 2) { for (int leftStart = 0; leftStart < n; leftStart += currSize * 2) { int mid = Math.Min(leftStart + currSize - 1, n - 1); int rightEnd = Math.Min(leftStart + currSize * 2 - 1, n - 1); if (mid < rightEnd) Merge(arr, leftStart, mid, rightEnd); } } }
An inversion is a pair (i, j) where i < j but arr[i] > arr[j]. Merge Sort can count inversions in O(n log n) by counting splits during merge.
public class InversionCounter { public static long CountInversions(int[] arr) { if (arr.Length <= 1) return 0; long[] count = new long[1]; MergeSortCount(arr, 0, arr.Length - 1, count); return count[0]; } private static void MergeSortCount(int[] arr, int left, int right, long[] count) { if (left >= right) return; int mid = left + (right - left) / 2; MergeSortCount(arr, left, mid, count); MergeSortCount(arr, mid + 1, right, count); MergeCount(arr, left, mid, right, count); } private static void MergeCount(int[] arr, int left, int mid, int right, long[] count) { int[] leftArr = new int[mid - left + 1]; int[] rightArr = new int[right - mid]; for (int i = 0; i < leftArr.Length; i++) leftArr[i] = arr[left + i]; for (int i = 0; i < rightArr.Length; i++) rightArr[i] = arr[mid + 1 + i]; int l = 0, r = 0, k = left; while (l < leftArr.Length && r < rightArr.Length) { if (leftArr[l] <= rightArr[r]) { arr[k++] = leftArr[l++]; } else { // All remaining elements in left are inversions with rightArr[r] count[0] += leftArr.Length - l; arr[k++] = rightArr[r++]; } } while (l < leftArr.Length) arr[k++] = leftArr[l++]; while (r < rightArr.Length) arr[k++] = rightArr[r++]; } }
A highly efficient in-place sorting algorithm that partitions around a pivot. Average O(n log n), worst O(n²). Faster than Merge Sort in practice due to cache locality.
Simple partition strategy: place pivot at end, scan left accumulating smaller elements, swap at boundary.
public class QuickSort { public static void Sort(int[] arr) { QuickSortHelper(arr, 0, arr.Length - 1); } private static void QuickSortHelper(int[] arr, int left, int right) { if (left < right) { int pi = LomutoPartition(arr, left, right); QuickSortHelper(arr, left, pi - 1); QuickSortHelper(arr, pi + 1, right); } } // Partition using Lomuto scheme private static int LomutoPartition(int[] arr, int left, int right) { int pivot = arr[right]; // Choose rightmost as pivot int i = left - 1; // Index of smaller element - indicates right end of left partition for (int j = left; j < right; j++) { if (arr[j] < pivot) { i++; Swap(arr, i, j); } } Swap(arr, i + 1, right); return i + 1; } private static void Swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } }
More efficient: use two pointers from opposite ends, swap when inversion found. Fewer swaps than Lomuto.
private static int HoarePartition(int[] arr, int left, int right) { int pivot = arr[left]; int i = left - 1; int j = right + 1; while (true) { // Find leftmost element >= pivot do { i++; } while (arr[i] < pivot); // Find rightmost element <= pivot do { j--; } while (arr[j] > pivot); // If pointers crossed, partition is done if (i >= j) return j; Swap(arr, i, j); } }
Pick pivot randomly to avoid worst-case O(n²) on sorted input. Expected O(n log n) even on adversarial data.
private static int RandomizedQuickSort(int[] arr, int left, int right) { Random rand = new Random(); int randomIdx = left + rand.Next(right - left + 1); Swap(arr, randomIdx, right); // Move random pivot to end return LomutoPartition(arr, left, right); }
For arrays with many duplicates, partition into three regions: < pivot, = pivot, > pivot. Avoids redundant comparisons.
private static void QuickSort3Way(int[] arr, int left, int right) { if (left < right) { int lt = left; // arr[left..lt-1] < pivot int gt = right; // arr[gt+1..right] > pivot int i = left; // arr[lt..i-1] == pivot int pivot = arr[left]; while (i <= gt) { if (arr[i] < pivot) { Swap(arr, i++, lt++); } else if (arr[i] > pivot) { Swap(arr, i, gt--); } else { i++; } } QuickSort3Way(arr, left, lt - 1); QuickSort3Way(arr, gt + 1, right); } }
Find the kth smallest element without full sorting. Uses partitioning like Quick Sort. Average O(n), worst O(n²), but typically much faster than sorting.
After partitioning around pivot at index p: if k == p, found; if k < p, recurse left; if k > p, recurse right. Only one partition branch is explored, unlike Quick Sort which explores both.
public class QuickSelect { public static int FindKthSmallest(int[] arr, int k) { if (k < 1 || k > arr.Length) throw new ArgumentException("Invalid k"); return QuickSelectHelper(arr, 0, arr.Length - 1, k - 1); // k-1 for 0-indexed } private static int QuickSelectHelper(int[] arr, int left, int right, int k) { if (left == right) return arr[left]; // Partition and get pivot index int pi = Partition(arr, left, right); if (k == pi) { return arr[k]; } else if (k < pi) { return QuickSelectHelper(arr, left, pi - 1, k); } else { return QuickSelectHelper(arr, pi + 1, right, k); } } private static int Partition(int[] arr, int left, int right) { Random rand = new Random(); int randomIdx = left + rand.Next(right - left + 1); Swap(arr, randomIdx, right); int pivot = arr[right]; int i = left - 1; for (int j = left; j < right; j++) { if (arr[j] < pivot) { i++; Swap(arr, i, j); } } Swap(arr, i + 1, right); return i + 1; } private static void Swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } }
Find 3rd smallest in [3, 1, 4, 1, 5, 9, 2, 6]:
Binary Search is a fundamental D&C algorithm: divide search space by half each iteration, recurse on relevant half. O(log n) time.
public class BinarySearch { public static int Search(int[] arr, int target) { return BinarySearchHelper(arr, target, 0, arr.Length - 1); } private static int BinarySearchHelper(int[] arr, int target, int left, int right) { // Base case: element not found if (left > right) return -1; // Divide int mid = left + (right - left) / 2; // Check middle element if (arr[mid] == target) { return mid; // Found } else if (arr[mid] < target) { // Conquer: search right half return BinarySearchHelper(arr, target, mid + 1, right); } else { // Conquer: search left half return BinarySearchHelper(arr, target, left, mid - 1); } } }
More space-efficient than recursion; no call stack overhead.
public static int BinarySearchIterative(int[] arr, int target) { int left = 0, right = arr.Length - 1; while (left <= right) { int mid = left + (right - left) / 2; if (arr[mid] == target) return mid; else if (arr[mid] < target) left = mid + 1; else right = mid - 1; } return -1; }
T(n) = T(n/2) + O(1). Here a=1, b=2, f(n)=1 = O(n^0). By Case 2 (f(n) = Θ(n^log_b(a) = n^0)), T(n) = Θ(log n).
Find two points in 2D space with minimum Euclidean distance. Naïve O(n²), D&C achieves O(n log n) by cleverly dividing and merging.
public class Point { public double X, Y; public Point(double x, double y) { X = x; Y = y; } } public class ClosestPair { public Point P1, P2; public double Distance; public ClosestPair(Point p1, Point p2) { P1 = p1; P2 = p2; Distance = Distance(p1, p2); } } public class ClosestPairFinder { private static double Distance(Point p1, Point p2) { double dx = p1.X - p2.X, dy = p1.Y - p2.Y; return Math.Sqrt(dx * dx + dy * dy); } public static ClosestPair FindClosestPair(Point[] points) { // Sort by x-coordinate Array.Sort(points, (a, b) => a.X.CompareTo(b.X)); return FindClosestPairHelper(points, 0, points.Length - 1); } private static ClosestPair FindClosestPairHelper(Point[] points, int left, int right) { // Base case: brute force for small sets if (right - left <= 2) { return BruteForce(points, left, right); } // Divide int mid = left + (right - left) / 2; double midX = points[mid].X; // Conquer: find closest pairs in left and right ClosestPair leftPair = FindClosestPairHelper(points, left, mid); ClosestPair rightPair = FindClosestPairHelper(points, mid + 1, right); // Current best distance double d = Math.Min(leftPair.Distance, rightPair.Distance); ClosestPair bestPair = leftPair.Distance < rightPair.Distance ? leftPair : rightPair; // Find closest pair across the dividing line var stripPoints = new List<Point>(); for (int i = left; i <= right; i++) { if (Math.Abs(points[i].X - midX) < d) { stripPoints.Add(points[i]); } } // Sort strip by y-coordinate and check nearby points stripPoints.Sort((a, b) => a.Y.CompareTo(b.Y)); for (int i = 0; i < stripPoints.Count; i++) { for (int j = i + 1; j < stripPoints.Count && (stripPoints[j].Y - stripPoints[i].Y) < d; j++) { double dist = Distance(stripPoints[i], stripPoints[j]); if (dist < bestPair.Distance) { bestPair = new ClosestPair(stripPoints[i], stripPoints[j]); } } } return bestPair; } private static ClosestPair BruteForce(Point[] points, int left, int right) { ClosestPair best = new ClosestPair(points[left], points[left + 1]); for (int i = left; i <= right; i++) { for (int j = i + 1; j <= right; j++) { double dist = Distance(points[i], points[j]); if (dist < best.Distance) { best = new ClosestPair(points[i], points[j]); } } } return best; } }
Recurrence: T(n) = 2T(n/2) + O(n log n). The strip processing takes O(n log n) due to sorting; applying Master Theorem gives T(n) = O(n log² n). With preprocessing sort and careful strip handling, can optimize to O(n log n).
Classic D&C result: multiply two n×n matrices in O(n^2.807) instead of naïve O(n³). Remarkable algorithmic breakthrough.
Standard definition: C[i][j] = Σ A[i][k] · B[k][j]. Three nested loops, all of length n.
Split each n×n matrix into four (n/2)×(n/2) blocks. Instead of 8 multiplications (naive D&C would give T(n) = 8T(n/2) + O(n²) = O(n³)), perform only 7 multiplications and more additions.
Given matrices split as:
A = [A₁₁ A₁₂] B = [B₁₁ B₁₂] C = [C₁₁ C₁₂]
[A₂₁ A₂₂] [B₂₁ B₂₂] [C₂₁ C₂₂]
Then C blocks are computed from these M values:
Find contiguous subarray with largest sum. D&C solution: O(n log n). Kadane's algorithm: O(n). Both approaches demonstrated.
Maximum subarray either lies entirely in left half, right half, or spans the middle. Recursively check left, right, and middle-crossing maximum.
public class SubarrayResult { public int LeftIndex, RightIndex, Sum; public SubarrayResult(int left, int right, int sum) { LeftIndex = left; RightIndex = right; Sum = sum; } } public class MaximumSubarray { public static SubarrayResult FindMaxSubarrayDC(int[] arr) { return FindMaxSubarrayHelper(arr, 0, arr.Length - 1); } private static SubarrayResult FindMaxSubarrayHelper(int[] arr, int left, int right) { // Base case: single element if (left == right) return new SubarrayResult(left, right, arr[left]); // Divide int mid = left + (right - left) / 2; // Conquer: find max in left and right halves SubarrayResult leftMax = FindMaxSubarrayHelper(arr, left, mid); SubarrayResult rightMax = FindMaxSubarrayHelper(arr, mid + 1, right); // Find maximum subarray crossing the middle SubarrayResult midMax = FindMaxCrossingSubarray(arr, left, mid, right); // Return maximum of three if (leftMax.Sum >= rightMax.Sum && leftMax.Sum >= midMax.Sum) return leftMax; else if (rightMax.Sum >= midMax.Sum) return rightMax; else return midMax; } private static SubarrayResult FindMaxCrossingSubarray(int[] arr, int left, int mid, int right) { int leftSum = int.MinValue; int sum = 0, maxLeftIdx = mid; // Find max sum in left half ending at mid for (int i = mid; i >= left; i--) { sum += arr[i]; if (sum > leftSum) { leftSum = sum; maxLeftIdx = i; } } int rightSum = int.MinValue; sum = 0; int maxRightIdx = mid + 1; // Find max sum in right half starting from mid+1 for (int i = mid + 1; i <= right; i++) { sum += arr[i]; if (sum > rightSum) { rightSum = sum; maxRightIdx = i; } } return new SubarrayResult(maxLeftIdx, maxRightIdx, leftSum + rightSum); } }
Much simpler and faster: at each position, track max sum ending here. If extending decreases sum, start fresh.
public static SubarrayResult FindMaxSubarrayKadane(int[] arr) { int maxSum = arr[0], currentSum = arr[0]; int maxStart = 0, maxEnd = 0; int tempStart = 0; for (int i = 1; i < arr.Length; i++) { // If adding current element makes sum worse, start fresh if (currentSum + arr[i] < arr[i]) { currentSum = arr[i]; tempStart = i; } else { currentSum += arr[i]; } // Update max if current sum is better if (currentSum > maxSum) { maxSum = currentSum; maxStart = tempStart; maxEnd = i; } } return new SubarrayResult(maxStart, maxEnd, maxSum); }
Extended discussion of inversion counting. Covered in Merge Sort section (14.3); this section reinforces the pattern and variations.
Pair (i, j) where i < j but arr[i] > arr[j]. Inversions measure how "unsorted" an array is. Example: [2, 1, 3] has 1 inversion (2,1).
Multiply two n-digit numbers in O(n^1.585) instead of naïve O(n²). Classic result showing D&C power.
Standard grade-school multiplication: multiply each digit of first number by each digit of second, add results. For n-digit numbers, O(n²) multiplications.
Split numbers into high and low parts. For n-digit numbers x and y:
x = x₁·10^(n/2) + x₀
y = y₁·10^(n/2) + y₀
x·y = (x₁·y₁)·10^n + ((x₁·y₀ + x₀·y₁))·10^(n/2) + (x₀·y₀)
Standard approach: 4 multiplications of (n/2)-digit numbers. Karatsuba uses only 3 by clever substitution:
m₁ = x₁·y₁
m₂ = x₀·y₀
m₃ = (x₁+x₀)·(y₁+y₀) = x₁·y₁ + x₁·y₀ + x₀·y₁ + x₀·y₀
Then: x₁·y₀ + x₀·y₁ = m₃ - m₁ - m₂
Apply D&C patterns to interview-style problems. Complete C# implementations provided.
Prompt: Implement x^n for integer n (may be negative). Use D&C to achieve O(log n).
public class Power { public static double MyPow(double x, int n) { long N = n; // Handle int overflow for n = int.MinValue if (N < 0) { x = 1 / x; N = -N; } return MyPowHelper(x, N); } private static double MyPowHelper(double x, long n) { // Base case: x^0 = 1 if (n == 0) return 1.0; // Divide: compute x^(n/2) double half = MyPowHelper(x, n / 2); // Combine based on even/odd if (n % 2 == 0) { return half * half; } else { return half * half * x; } } }
Complexity: T(n) = T(n/2) + O(1). Master Theorem Case 2: Θ(log n).
Prompt: Find element appearing > n/2 times. Use D&C (also solvable with Boyer-Moore).
public class MajorityElement { public static int FindMajority(int[] nums) { return MajorityHelper(nums, 0, nums.Length - 1); } private static int MajorityHelper(int[] nums, int left, int right) { // Base case: single element if (left == right) return nums[left]; // Divide int mid = left + (right - left) / 2; // Conquer: find majority in left and right int leftMaj = MajorityHelper(nums, left, mid); int rightMaj = MajorityHelper(nums, mid + 1, right); // Combine: count occurrences and return majority int leftCount = CountInRange(nums, left, right, leftMaj); if (leftCount > (right - left + 1) / 2) return leftMaj; int rightCount = CountInRange(nums, left, right, rightMaj); if (rightCount > (right - left + 1) / 2) return rightMaj; return -1; // No majority (shouldn't reach if input guaranteed to have majority) } private static int CountInRange(int[] nums, int left, int right, int target) { int count = 0; for (int i = left; i <= right; i++) { if (nums[i] == target) count++; } return count; } }
Complexity: T(n) = 2T(n/2) + O(n). Master Theorem Case 2: Θ(n log n). Note: Boyer-Moore voting is O(n) with O(1) space, but this demonstrates D&C.
Prompt: Sort a linked list using Merge Sort (D&C ideal for linked structures).
public class ListNode { public int val; public ListNode next; public ListNode(int x) { val = x; } } public class SortList { public static ListNode Sort(ListNode head) { if (head == null || head.next == null) return head; // Find middle of list ListNode mid = FindMiddle(head); ListNode left = head; ListNode right = mid.next; mid.next = null; // Split into two lists // Recursively sort both halves left = Sort(left); right = Sort(right); // Merge sorted halves return Merge(left, right); } private static ListNode FindMiddle(ListNode head) { ListNode slow = head, fast = head.next; while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; } return slow; } private static ListNode Merge(ListNode l1, ListNode l2) { ListNode dummy = new ListNode(0); ListNode current = dummy; while (l1 != null && l2 != null) { if (l1.val <= l2.val) { current.next = l1; l1 = l1.next; } else { current.next = l2; l2 = l2.next; } current = current.next; } current.next = (l1 != null) ? l1 : l2; return dummy.next; } }
Complexity: T(n) = 2T(n/2) + O(n). Master Theorem Case 2: Θ(n log n). Space: O(log n) recursion depth.
Prompt: Find median of two sorted arrays without merging. Use binary search on smaller array.
public class MedianOfTwoArrays { public static double FindMedianSortedArrays(int[] nums1, int[] nums2) { // Ensure nums1 is smaller if (nums1.Length > nums2.Length) return FindMedianSortedArrays(nums2, nums1); int m = nums1.Length, n = nums2.Length; int left = 0, right = m; while (left <= right) { int partition1 = (left + right) / 2; int partition2 = (m + n + 1) / 2 - partition1; // Handle edge cases int leftMax1 = (partition1 == 0) ? int.MinValue : nums1[partition1 - 1]; int rightMin1 = (partition1 == m) ? int.MaxValue : nums1[partition1]; int leftMax2 = (partition2 == 0) ? int.MinValue : nums2[partition2 - 1]; int rightMin2 = (partition2 == n) ? int.MaxValue : nums2[partition2]; // Valid partition found if (leftMax1 <= rightMin2 && leftMax2 <= rightMin1) { if ((m + n) % 2 == 0) { return (Math.Max(leftMax1, leftMax2) + Math.Min(rightMin1, rightMin2)) / 2.0; } else { return Math.Max(leftMax1, leftMax2); } } else if (leftMax1 > rightMin2) { right = partition1 - 1; } else { left = partition1 + 1; } } return -1.0; // Shouldn't reach } }
Complexity: T(n) = T(n/2) + O(1). Master Theorem Case 2: Θ(log(min(m,n))).
| Aspect | Divide & Conquer | Dynamic Programming | Greedy |
|---|---|---|---|
| Subproblems | Independent, non-overlapping | Overlapping, with optimal substructure | Local optimal choices |
| Approach | Top-down (or bottom-up iterative) | Bottom-up tabulation or top-down memoization | Single pass, never backtrack |
| Complexity | Usually O(n log n) or O(n^log_b(a)) | Varies; often O(nm) or O(n²) | Usually O(n) or O(n log n) for sorting |
| Examples | Merge Sort, Quick Sort, Binary Search | Fibonacci, Knapsack, Edit Distance | Activity Selection, Huffman Coding |
| Memory | O(n) to O(log n) auxiliary | O(n²) or O(nm) for table | O(1) or O(log n) typically |