Divide and Conquer

1. Merge Sort Pattern Application

Phase Operation Complexity Key Insight
Divide Split array into two halves O(1) Find middle index
Conquer Recursively sort both halves T(n/2) + T(n/2) Two subproblems of size n/2
Combine Merge two sorted arrays O(n) Linear scan with two pointers
Total Time Master theorem analysis O(n log n) log n levels, O(n) work per level

Example: Merge sort implementation

function mergeSort(arr) {
    if (arr.length <= 1) return arr;
    
    // Divide
    const mid = Math.floor(arr.length / 2);
    const left = arr.slice(0, mid);
    const right = arr.slice(mid);
    
    // Conquer
    const sortedLeft = mergeSort(left);
    const sortedRight = mergeSort(right);
    
    // Combine
    return merge(sortedLeft, sortedRight);
}

function merge(left, right) {
    const result = [];
    let i = 0, j = 0;
    
    while (i < left.length && j < right.length) {
        if (left[i] <= right[j]) {
            result.push(left[i]);
            i++;
        } else {
            result.push(right[j]);
            j++;
        }
    }
    
    // Add remaining elements
    while (i < left.length) {
        result.push(left[i]);
        i++;
    }
    
    while (j < right.length) {
        result.push(right[j]);
        j++;
    }
    
    return result;
}

// [38,27,43,3,9,82,10] -> [3,9,10,27,38,43,82]
// Time: O(n log n), Space: O(n)

Example: Count inversions using merge sort

function countInversions(arr) {
    if (arr.length <= 1) return { sorted: arr, count: 0 };
    
    const mid = Math.floor(arr.length / 2);
    const left = arr.slice(0, mid);
    const right = arr.slice(mid);
    
    const leftResult = countInversions(left);
    const rightResult = countInversions(right);
    
    const merged = mergeAndCount(leftResult.sorted, rightResult.sorted);
    
    return {
        sorted: merged.sorted,
        count: leftResult.count + rightResult.count + merged.count
    };
}

function mergeAndCount(left, right) {
    const result = [];
    let i = 0, j = 0, count = 0;
    
    while (i < left.length && j < right.length) {
        if (left[i] <= right[j]) {
            result.push(left[i]);
            i++;
        } else {
            result.push(right[j]);
            j++;
            // All remaining elements in left are inversions
            count += left.length - i;
        }
    }
    
    while (i < left.length) result.push(left[i++]);
    while (j < right.length) result.push(right[j++]);
    
    return { sorted: result, count };
}

// [2,4,1,3,5] -> 3 inversions: (2,1), (4,1), (4,3)
// Application: Measure of "sortedness"

2. Quick Sort Partition Strategy

Partition Scheme Strategy Swaps Characteristics
Lomuto Partition Single pointer, swap smaller elements to front More swaps Simpler, easier to understand
Hoare Partition Two pointers from both ends Fewer swaps More efficient, trickier to implement
3-way Partition Three regions: <pivot, =pivot, >pivot Optimal for duplicates Handles many equal elements well
Average Case Random pivot selection O(n log n) Expected performance

Example: Quick sort with Lomuto partition

function quickSort(arr, left = 0, right = arr.length - 1) {
    if (left < right) {
        const pivotIndex = lomutoPartition(arr, left, right);
        quickSort(arr, left, pivotIndex - 1);
        quickSort(arr, pivotIndex + 1, right);
    }
    return arr;
}

function lomutoPartition(arr, left, right) {
    const pivot = arr[right]; // Choose last as pivot
    let i = left;
    
    for (let j = left; j < right; j++) {
        if (arr[j] < pivot) {
            [arr[i], arr[j]] = [arr[j], arr[i]];
            i++;
        }
    }
    
    [arr[i], arr[right]] = [arr[right], arr[i]];
    return i;
}

// Time: O(n log n) average, O(n²) worst
// Space: O(log n) recursion stack

Example: Quick sort with Hoare partition

function quickSortHoare(arr, left = 0, right = arr.length - 1) {
    if (left < right) {
        const pivotIndex = hoarePartition(arr, left, right);
        quickSortHoare(arr, left, pivotIndex);
        quickSortHoare(arr, pivotIndex + 1, right);
    }
    return arr;
}

function hoarePartition(arr, left, right) {
    const pivot = arr[Math.floor((left + right) / 2)];
    let i = left - 1;
    let j = right + 1;
    
    while (true) {
        do {
            i++;
        } while (arr[i] < pivot);
        
        do {
            j--;
        } while (arr[j] > pivot);
        
        if (i >= j) return j;
        
        [arr[i], arr[j]] = [arr[j], arr[i]];
    }
}

// Fewer swaps than Lomuto

Example: 3-way partition (Dutch National Flag)

function quickSort3Way(arr, left = 0, right = arr.length - 1) {
    if (left >= right) return arr;
    
    const pivot = arr[left];
    let lt = left;      // Elements < pivot
    let i = left + 1;   // Current element
    let gt = right;     // Elements > pivot
    
    while (i <= gt) {
        if (arr[i] < pivot) {
            [arr[lt], arr[i]] = [arr[i], arr[lt]];
            lt++;
            i++;
        } else if (arr[i] > pivot) {
            [arr[i], arr[gt]] = [arr[gt], arr[i]];
            gt--;
        } else {
            i++;
        }
    }
    
    quickSort3Way(arr, left, lt - 1);
    quickSort3Way(arr, gt + 1, right);
    
    return arr;
}

// [3,5,3,2,3,1,3,4,3] -> sorted efficiently
// Optimal when many duplicates exist
// Equal elements not recursed on

3. Binary Tree Divide Conquer

Pattern Approach Combine Step Example Problem
Height/Depth Recurse on children, return max + 1 max(left, right) + 1 Tree height, depth calculation
Path Sum Recurse with remaining sum Check if current + child paths work Path sum, binary tree paths
Diameter Track max path through each node Max of left+right or child diameters Longest path in tree
Balanced Check Return height and balance status Check |left.height - right.height| <= 1 Validate balanced tree

Example: Tree height/depth

function maxDepth(root) {
    // Base case
    if (!root) return 0;
    
    // Divide: recurse on children
    const leftDepth = maxDepth(root.left);
    const rightDepth = maxDepth(root.right);
    
    // Conquer: combine results
    return Math.max(leftDepth, rightDepth) + 1;
}

// Tree with 3 levels -> depth = 3
// Time: O(n), Space: O(h) where h = height

Example: Balanced binary tree

function isBalanced(root) {
    function checkHeight(node) {
        if (!node) return 0;
        
        const leftHeight = checkHeight(node.left);
        if (leftHeight === -1) return -1;
        
        const rightHeight = checkHeight(node.right);
        if (rightHeight === -1) return -1;
        
        // Check balance condition
        if (Math.abs(leftHeight - rightHeight) > 1) {
            return -1; // Not balanced
        }
        
        return Math.max(leftHeight, rightHeight) + 1;
    }
    
    return checkHeight(root) !== -1;
}

// Returns true if balanced, false otherwise

Example: Binary tree diameter

function diameterOfBinaryTree(root) {
    let maxDiameter = 0;
    
    function height(node) {
        if (!node) return 0;
        
        const leftHeight = height(node.left);
        const rightHeight = height(node.right);
        
        // Update diameter: path through current node
        maxDiameter = Math.max(maxDiameter, leftHeight + rightHeight);
        
        // Return height for parent
        return Math.max(leftHeight, rightHeight) + 1;
    }
    
    height(root);
    return maxDiameter;
}

//       1
//      / \
//     2   3
//    / \
//   4   5
// Diameter = 3 (4-2-1-3 or 5-2-1-3)
// Time: O(n), Space: O(h)

Example: Lowest common ancestor (LCA)

function lowestCommonAncestor(root, p, q) {
    // Base case
    if (!root || root === p || root === q) {
        return root;
    }
    
    // Divide: search in subtrees
    const left = lowestCommonAncestor(root.left, p, q);
    const right = lowestCommonAncestor(root.right, p, q);
    
    // Conquer: combine results
    if (left && right) {
        // p and q on different sides
        return root;
    }
    
    // Both on same side or one not found
    return left || right;
}

// Finds the lowest node that has both p and q as descendants
// Time: O(n), Space: O(h)

4. Closest Pair of Points

Step Operation Complexity Purpose
Preprocessing Sort points by x-coordinate O(n log n) Enable divide step
Divide Split into left/right halves O(1) Find middle x-coordinate
Conquer Find closest in each half recursively T(n/2) Solve subproblems
Combine Check strip around midline O(n) Find cross-boundary pairs

Example: Closest pair of points

function closestPair(points) {
    // Sort by x-coordinate
    points.sort((a, b) => a.x - b.x);
    
    function distance(p1, p2) {
        const dx = p1.x - p2.x;
        const dy = p1.y - p2.y;
        return Math.sqrt(dx * dx + dy * dy);
    }
    
    function bruteForce(pts) {
        let minDist = Infinity;
        for (let i = 0; i < pts.length; i++) {
            for (let j = i + 1; j < pts.length; j++) {
                minDist = Math.min(minDist, distance(pts[i], pts[j]));
            }
        }
        return minDist;
    }
    
    function closestUtil(pts) {
        const n = pts.length;
        
        // Base case: brute force for small n
        if (n <= 3) return bruteForce(pts);
        
        // Divide
        const mid = Math.floor(n / 2);
        const midPoint = pts[mid];
        const left = pts.slice(0, mid);
        const right = pts.slice(mid);
        
        // Conquer
        const dl = closestUtil(left);
        const dr = closestUtil(right);
        const d = Math.min(dl, dr);
        
        // Combine: check strip
        const strip = pts.filter(p => Math.abs(p.x - midPoint.x) < d);
        
        // Sort strip by y-coordinate
        strip.sort((a, b) => a.y - b.y);
        
        let minDist = d;
        for (let i = 0; i < strip.length; i++) {
            // Only check next 7 points (proven upper bound)
            for (let j = i + 1; j < strip.length && 
                 (strip[j].y - strip[i].y) < minDist; j++) {
                minDist = Math.min(minDist, distance(strip[i], strip[j]));
            }
        }
        
        return minDist;
    }
    
    return closestUtil(points);
}

// Points: [{x:2,y:3}, {x:12,y:30}, {x:40,y:50}, {x:5,y:1}]
// Returns minimum distance between any two points
// Time: O(n log² n), can be optimized to O(n log n)

5. Maximum Subarray Divide Conquer

Case Location Computation Complexity
Left Half Entirely in left subarray Recursive call on left T(n/2)
Right Half Entirely in right subarray Recursive call on right T(n/2)
Crossing Middle Spans across midpoint Find max from mid to left + mid to right O(n)
Total Max of all three cases max(left, right, cross) O(n log n)

Example: Maximum subarray divide and conquer

function maxSubArrayDC(nums) {
    function maxCrossingSum(nums, left, mid, right) {
        // Left side of mid
        let leftSum = -Infinity;
        let sum = 0;
        for (let i = mid; i >= left; i--) {
            sum += nums[i];
            leftSum = Math.max(leftSum, sum);
        }
        
        // Right side of mid
        let rightSum = -Infinity;
        sum = 0;
        for (let i = mid + 1; i <= right; i++) {
            sum += nums[i];
            rightSum = Math.max(rightSum, sum);
        }
        
        return leftSum + rightSum;
    }
    
    function maxSubArrayHelper(nums, left, right) {
        // Base case: single element
        if (left === right) return nums[left];
        
        const mid = Math.floor((left + right) / 2);
        
        // Divide
        const leftMax = maxSubArrayHelper(nums, left, mid);
        const rightMax = maxSubArrayHelper(nums, mid + 1, right);
        const crossMax = maxCrossingSum(nums, left, mid, right);
        
        // Conquer
        return Math.max(leftMax, rightMax, crossMax);
    }
    
    return maxSubArrayHelper(nums, 0, nums.length - 1);
}

// [-2,1,-3,4,-1,2,1,-5,4] -> 6 (subarray [4,-1,2,1])
// Time: O(n log n), Space: O(log n)
// Note: Kadane's algorithm is O(n), but this demonstrates D&C

6. Karatsuba Multiplication

Step Formula Multiplications Insight
Standard Multiplication Split into halves, 4 recursive mults 4 T(n) = 4T(n/2) + O(n) = O(n²)
Karatsuba Trick Use (a+b)(c+d) to eliminate one mult 3 T(n) = 3T(n/2) + O(n) = O(n^1.58)
Key Formula ac×10^n + ((a+b)(c+d) - ac - bd)×10^(n/2) + bd 3: ac, bd, (a+b)(c+d) Trade multiplications for additions
Application Large integer multiplication Asymptotically faster Used in cryptography, bignum libraries

Example: Karatsuba multiplication

function karatsuba(x, y) {
    // Base case: small numbers
    if (x < 10 || y < 10) {
        return x * y;
    }
    
    // Find the size of the numbers
    const n = Math.max(x.toString().length, y.toString().length);
    const half = Math.floor(n / 2);
    const divisor = Math.pow(10, half);
    
    // Split x and y
    const a = Math.floor(x / divisor);  // High part of x
    const b = x % divisor;               // Low part of x
    const c = Math.floor(y / divisor);  // High part of y
    const d = y % divisor;               // Low part of y
    
    // Three recursive multiplications (Karatsuba's trick)
    const ac = karatsuba(a, c);
    const bd = karatsuba(b, d);
    const ad_plus_bc = karatsuba(a + b, c + d) - ac - bd;
    
    // Combine results
    // x*y = ac*10^n + (ad+bc)*10^(n/2) + bd
    return ac * Math.pow(10, 2 * half) + 
           ad_plus_bc * Math.pow(10, half) + 
           bd;
}

// 1234 * 5678
// Standard: 4 multiplications at each level
// Karatsuba: 3 multiplications at each level
// Result: 7006652
// Time: O(n^log₂3) ≈ O(n^1.585) vs O(n²)

Example: String-based Karatsuba for very large numbers

function karatsubaString(x, y) {
    // Remove leading zeros
    x = x.replace(/^0+/, '') || '0';
    y = y.replace(/^0+/, '') || '0';
    
    // Base case
    if (x.length === 1 || y.length === 1) {
        return (parseInt(x) * parseInt(y)).toString();
    }
    
    // Pad to same length
    const n = Math.max(x.length, y.length);
    x = x.padStart(n, '0');
    y = y.padStart(n, '0');
    
    const half = Math.floor(n / 2);
    
    // Split
    const a = x.slice(0, n - half);
    const b = x.slice(n - half);
    const c = y.slice(0, n - half);
    const d = y.slice(n - half);
    
    // Helper to add large numbers as strings
    function addStrings(num1, num2) {
        let result = '';
        let carry = 0;
        let i = num1.length - 1;
        let j = num2.length - 1;
        
        while (i >= 0 || j >= 0 || carry) {
            const digit1 = i >= 0 ? parseInt(num1[i]) : 0;
            const digit2 = j >= 0 ? parseInt(num2[j]) : 0;
            const sum = digit1 + digit2 + carry;
            result = (sum % 10) + result;
            carry = Math.floor(sum / 10);
            i--;
            j--;
        }
        
        return result;
    }
    
    // Helper to subtract large numbers
    function subtractStrings(num1, num2) {
        // Simplified: assumes num1 >= num2
        let result = '';
        let borrow = 0;
        let i = num1.length - 1;
        let j = num2.length - 1;
        
        while (i >= 0) {
            let digit1 = parseInt(num1[i]);
            const digit2 = j >= 0 ? parseInt(num2[j]) : 0;
            digit1 -= borrow;
            
            if (digit1 < digit2) {
                digit1 += 10;
                borrow = 1;
            } else {
                borrow = 0;
            }
            
            result = (digit1 - digit2) + result;
            i--;
            j--;
        }
        
        return result.replace(/^0+/, '') || '0';
    }
    
    // Three multiplications
    const ac = karatsubaString(a, c);
    const bd = karatsubaString(b, d);
    const abcd = karatsubaString(addStrings(a, b), addStrings(c, d));
    const middle = subtractStrings(subtractStrings(abcd, ac), bd);
    
    // Combine with powers of 10 (append zeros)
    return addStrings(
        addStrings(ac + '0'.repeat(2 * half), middle + '0'.repeat(half)),
        bd
    );
}

// Can multiply arbitrarily large integers
// "12345678901234567890" * "98765432109876543210"

Divide and Conquer Summary

  • Core Pattern: Divide → Conquer → Combine
  • Merge Sort: O(n log n), stable, predictable performance
  • Quick Sort: O(n log n) average, O(n²) worst, in-place
  • Binary Tree: Natural recursion on left/right subtrees
  • Closest Pair: Geometric divide and conquer, O(n log n)
  • Max Subarray: Consider left, right, and crossing cases
  • Karatsuba: Reduce multiplications from 4 to 3, O(n^1.58)
Note: Divide and conquer achieves O(n log n) for many problems by breaking into smaller subproblems. Master theorem: T(n) = aT(n/b) + f(n). For merge sort: a=2, b=2, f(n)=O(n) → O(n log n). For Karatsuba: a=3, b=2 → O(n^log₂3) ≈ O(n^1.585).
Warning: Divide and conquer has recursion overhead - for small inputs, simple iterative solutions may be faster. Quick sort's O(n²) worst case occurs with poor pivot selection (already sorted array with first/last pivot). Use random pivot or median-of-3 for better performance. Merge sort requires O(n) extra space.