Loading content...
Every recursive algorithm needs a stopping condition—a point at which the algorithm says "this problem is simple enough to solve directly" rather than dividing further. In Divide and Conquer, these stopping conditions are called base cases, and they are far more important than they might initially appear.
Base cases aren't just technicalities to prevent infinite recursion. They are the foundation of correctness. Every solution your D&C algorithm produces is ultimately built from base case solutions combined upward through the recursion tree. If your base cases are wrong, propagate errors upward. If your base cases are missing, your program crashes or hangs.
In this page, we'll explore base cases comprehensively: what they are, why they matter, how to identify them, common patterns, edge cases that often go wrong, and strategies for testing and validating your base case logic. This knowledge will make your D&C implementations robust and correct.
By the end of this page, you'll understand the role of base cases in D&C correctness, identify the base cases for any D&C algorithm, recognize common base case patterns, avoid the subtle bugs that arise from incorrect or missing base cases, and design base cases that are both correct and efficient.
A base case is a subproblem that is simple enough to solve directly without further recursion. It represents the smallest or simplest instance of the problem type.
Formal Definition:
A base case for a D&C algorithm solving problem P is an instance p ∈ P such that:
The Role in D&C:
In the D&C pattern:
solve(problem):
if is_base_case(problem):
return solve_directly(problem) ← Base case handling
else:
subproblems = divide(problem)
subsolutions = [solve(sp) for sp in subproblems]
return combine(subsolutions)
The base case check is the first thing that happens in every invocation. It's the escape hatch that prevents infinite recursion.
| Algorithm | Base Case | Solution | Why It's Correct |
|---|---|---|---|
| Merge Sort | Array of length 0 or 1 | Return array as-is | Arrays of length ≤1 are trivially sorted |
| Binary Search | Empty range (low > high) | Return "not found" | No elements to search means target absent |
| Quick Sort | Array of length 0 or 1 | Return array as-is | Same as Merge Sort |
| Maximum in Array | Single element | Return that element | One element is its own maximum |
| Fibonacci (D&C) | n = 0 or n = 1 | Return n | By definition: F(0)=0, F(1)=1 |
| Exponentiation | exponent = 0 | Return 1 | By definition: x⁰ = 1 |
| Counting Inversions | Single element | Return 0 inversions | No pairs = no inversions |
Base cases serve the same role as the base case in mathematical induction. They establish that the property (correctness) holds for the smallest instances. Combined with the inductive step (proving that if D&C works for smaller problems, it works for larger ones via correct combine), base cases complete the proof of correctness.
Base cases are not optional polish—they are structurally necessary for a D&C algorithm to work. Here's why:
Reason 1: Termination
Without base cases, recursion never ends. Each call creates more calls, and the algorithm runs until it exhausts the call stack (Stack Overflow error) or memory.
// BROKEN: No base case
function sort(arr):
mid = length(arr) / 2
left = sort(arr[0:mid]) // What if arr has 0 or 1 elements?
right = sort(arr[mid:end])
return merge(left, right)
For a 1-element array, mid = 0, so we recursively call sort on arr[0:0] (empty) and arr[0:1] (the same 1-element array). The second call is the same as the original—infinite recursion!
Reason 2: Correctness Foundation
Every solution is built from base cases. Consider the recursion tree: leaves are base cases, internal nodes are recursive cases. The algorithm builds solutions from leaves upward. If leaves are wrong, everything above them is wrong.
Reason 3: Edge Case Coverage
Base cases handle the edge cases that would otherwise break the algorithm:
Without explicit base case handling, these inputs cause:
Reason 4: The Inductive Proof Structure
To prove a D&C algorithm correct using induction:
Without verified base cases, step 1 fails, and the entire proof collapses.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
# Demonstration: What happens without proper base cases import syssys.setrecursionlimit(100) # Low limit to see the problem quickly def merge_sort_broken(arr): """ BROKEN: Missing base case for empty/single-element arrays. """ mid = len(arr) // 2 # When len(arr) = 1: mid = 0 # left = arr[0:0] = [] # right = arr[0:1] = [original_element] # Right branch is the SAME as original call → infinite recursion left = merge_sort_broken(arr[:mid]) right = merge_sort_broken(arr[mid:]) return merge(left, right) def merge_sort_correct(arr): """ CORRECT: Proper base case handling. """ # BASE CASE: Arrays of length 0 or 1 are already sorted if len(arr) <= 1: return arr[:] # Return a copy (good practice) mid = len(arr) // 2 left = merge_sort_correct(arr[:mid]) right = merge_sort_correct(arr[mid:]) return merge(left, right) def merge(left, right): """Standard merge of two sorted arrays.""" result = [] i = j = 0 while i < len(left) and j < len(right): if left[i] <= right[j]: result.append(left[i]) i += 1 else: result.append(right[j]) j += 1 result.extend(left[i:]) result.extend(right[j:]) return result # Testtry: merge_sort_broken([5, 2, 8, 1])except RecursionError: print("BROKEN version: RecursionError (infinite recursion)") result = merge_sort_correct([5, 2, 8, 1])print(f"CORRECT version: {result}") # [1, 2, 5, 8]Sometimes incorrect base cases don't crash the program—they just produce wrong answers. A base case that returns 0 instead of 1 for counting problems, or returns the wrong element for search problems, can corrupt all results that depend on it. This is harder to detect than a crash.
Identifying the correct base cases for a D&C algorithm requires systematic thinking. Here are strategies that work:
Strategy 1: Ask "What's the Smallest Valid Input?"
Every problem has a minimum meaningful input size. For the problem you're solving, what's the smallest input that makes sense?
Strategy 2: Ask "When Does Division Stop Making Sense?"
Division eventually produces subproblems too small to divide further:
These "can't divide" points are natural base cases.
Strategy 3: Trace Backward from Recursion
Start from a larger instance and trace what happens during division:
Array of 8 elements
↓ divide
Arrays of 4 and 4
↓ divide
Arrays of 2 and 2 (×2)
↓ divide
Arrays of 1 and 1 (×4) ← Can't divide further: BASE CASE
Whatever you reach at the bottom of this trace is your base case.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
# Examples of identifying base cases for various problems def find_base_cases_maximum(arr, left, right): """ Problem: Find maximum element in arr[left..right] Base case identification: - Smallest valid range: single element (left == right) - Solution: That element is its own maximum - Progress check: Each call shrinks range by half """ # BASE CASE: Single element if left == right: return arr[left] mid = left + (right - left) // 2 left_max = find_base_cases_maximum(arr, left, mid) right_max = find_base_cases_maximum(arr, mid + 1, right) return max(left_max, right_max) def find_base_cases_binary_search(arr, left, right, target): """ Problem: Find target in sorted arr[left..right] Base case identification: - Two types needed: 1. Empty range (left > right): target not found 2. Target found (arr[mid] == target): success """ # BASE CASE 1: Empty range if left > right: return -1 # Not found mid = left + (right - left) // 2 # BASE CASE 2: Found if arr[mid] == target: return mid # Recursive case if arr[mid] < target: return find_base_cases_binary_search(arr, mid + 1, right, target) else: return find_base_cases_binary_search(arr, left, mid - 1, target) def find_base_cases_power(base, exp): """ Problem: Compute base^exp using binary exponentiation Base case identification: - By definition: anything^0 = 1 - Progress: exp is halved each recursion - When exp reaches 0, we're done """ # BASE CASE: x^0 = 1 if exp == 0: return 1 # Divide: Compute base^(exp/2) half = find_base_cases_power(base, exp // 2) # Combine: Square the result if exp % 2 == 0: return half * half else: return half * half * base def find_base_cases_tree_height(node): """ Problem: Find height of a binary tree Base case identification: - Smallest subtree: empty tree (null node) - Height of empty tree: -1 or 0 (depending on convention) - We'll use 0 for null, so leaf has height 1 """ # BASE CASE: Empty tree if node is None: return 0 # Recursive case left_height = find_base_cases_tree_height(node.left) right_height = find_base_cases_tree_height(node.right) return 1 + max(left_height, right_height)Many D&C algorithms need more than one base case. Binary Search has both 'empty range' and 'found target'. Power computation might have special handling for exp=0, exp=1, and negative exponents. When in doubt, add more base cases—redundant handling of edge cases is better than missing them.
While base cases vary by problem, several patterns recur frequently across D&C algorithms. Recognizing these patterns helps you quickly identify the right base cases for new problems.
Pattern 1: Size-Zero or Size-One
The most common pattern. Arrays, lists, ranges, and sequences often have base cases at size 0 or 1.
# Size 0 or 1: nothing to sort/search/process
if len(arr) <= 1:
return arr
Pattern 2: Range Boundaries
For algorithms working with index ranges [left, right]:
# Single element
if left == right:
return solve_single(arr[left])
# Empty range
if left > right:
return empty_result()
| Pattern | Condition | Typical Solution | Examples |
|---|---|---|---|
| Empty collection | len(arr) == 0 or left > right | Return identity/empty | Merge Sort, Binary Search |
| Single element | len(arr) == 1 or left == right | Return element or trivial result | Maximum, Minimum, Sort |
| Small fixed size | len(arr) <= k (small constant) | Handle directly with loop | Optimized sorts (k=10) |
| Mathematical identity | n == 0 or n == 1 | Return defined value | Factorial, Fibonacci, Power |
| Null structure | node is None, tree is empty | Return identity for operation | Tree traversals, Tree DP |
| Goal reached | found target, condition met | Return success result | Search, Optimization |
| Impossible state | contradiction detected | Return failure indicator | Constraint problems |
Pattern 3: Mathematical Definitions
Some problems have mathematically defined base cases:
# Factorial: 0! = 1 (by definition)
if n == 0:
return 1
# Fibonacci: F(0) = 0, F(1) = 1 (by definition)
if n <= 1:
return n
# Exponentiation: x^0 = 1 (by definition)
if exp == 0:
return 1
Pattern 4: Structural Boundaries
For tree and graph algorithms:
# Tree: null node is base case
if node is None:
return neutral_value # 0 for height, 0 for sum, etc.
# Leaf node (no children) can also be a base case
if node.left is None and node.right is None:
return node.value
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
# Comprehensive examples of base case patterns # PATTERN 1: Size-Zero or Size-Onedef merge_sort(arr): """Size-zero/one pattern.""" if len(arr) <= 1: return arr[:] # ... rest of algorithm # PATTERN 2: Range Boundariesdef find_median(arr, left, right, k): """ Find k-th smallest element in arr[left..right]. Uses multiple range-based base cases. """ # Empty range: shouldn't happen with valid input if left > right: raise ValueError("Invalid range") # Single element: that's our answer if left == right: return arr[left] # ... partition and recurse # PATTERN 3: Mathematical Definitionsdef factorial(n): """Mathematical identity base case.""" if n == 0 or n == 1: return 1 return n * factorial(n - 1) def power(base, exp): """Mathematical identity: x^0 = 1.""" if exp == 0: return 1 half = power(base, exp // 2) if exp % 2 == 0: return half * half return half * half * base # PATTERN 4: Structural Boundariesclass TreeNode: def __init__(self, val, left=None, right=None): self.val = val self.left = left self.right = right def tree_sum(node): """Null structure base case.""" if node is None: return 0 # Identity for addition return node.val + tree_sum(node.left) + tree_sum(node.right) def tree_max(node): """Null structure with leaf handling.""" if node is None: return float('-inf') # Identity for max # Leaf node: direct answer if node.left is None and node.right is None: return node.val return max(node.val, tree_max(node.left), tree_max(node.right)) # PATTERN 5: Goal Reacheddef binary_search(arr, left, right, target): """Goal-reached base case: found the target.""" if left > right: return -1 # Base case: not found mid = left + (right - left) // 2 if arr[mid] == target: return mid # Base case: found! if arr[mid] < target: return binary_search(arr, mid + 1, right, target) return binary_search(arr, left, mid - 1, target) # PATTERN 6: Small Fixed Size (Optimization)def optimized_merge_sort(arr): """ Use insertion sort for small subarrays. This is a practical optimization used in real implementations. """ THRESHOLD = 10 # Base case: small arrays handled by simpler algorithm if len(arr) <= THRESHOLD: return insertion_sort(arr) mid = len(arr) // 2 left = optimized_merge_sort(arr[:mid]) right = optimized_merge_sort(arr[mid:]) return merge(left, right) def insertion_sort(arr): """Simple O(n²) sort, efficient for small n.""" result = arr[:] for i in range(1, len(result)): j = i while j > 0 and result[j-1] > result[j]: result[j-1], result[j] = result[j], result[j-1] j -= 1 return resultBase cases are a frequent source of bugs in D&C implementations. Here are the most common mistakes and how to avoid them.
Bug #1: Missing the Empty Case
Forgetting to handle empty input is extremely common:
# BROKEN: What if arr is empty?
def max_element(arr):
if len(arr) == 1: # Only handles size 1
return arr[0]
mid = len(arr) // 2
return max(max_element(arr[:mid]), max_element(arr[mid:]))
# When called with []: infinite recursion between [] and []
Fix: Always check for size 0 AND size 1:
if len(arr) == 0:
raise ValueError("Empty array has no maximum")
if len(arr) == 1:
return arr[0]
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
# Common base case bugs and their fixes # BUG #2: Off-by-one in range checkdef binary_search_broken(arr, left, right, target): """BROKEN: Uses >= instead of >.""" # When left == right, this exits without checking arr[left]! if left >= right: # BUG: should be left > right return -1 mid = left + (right - left) // 2 if arr[mid] == target: return mid elif arr[mid] < target: return binary_search_broken(arr, mid + 1, right, target) else: return binary_search_broken(arr, left, mid - 1, target) def binary_search_fixed(arr, left, right, target): """FIXED: Correct range termination.""" if left > right: # Correct: > not >= return -1 mid = left + (right - left) // 2 if arr[mid] == target: return mid elif arr[mid] < target: return binary_search_fixed(arr, mid + 1, right, target) else: return binary_search_fixed(arr, left, mid - 1, target) # BUG #3: Wrong return value for identitydef product_of_array_broken(arr, left, right): """BROKEN: Returns 0 for empty range (should be 1).""" if left > right: return 0 # BUG: 0 × anything = 0, corrupts all results! if left == right: return arr[left] mid = left + (right - left) // 2 left_product = product_of_array_broken(arr, left, mid) right_product = product_of_array_broken(arr, mid + 1, right) return left_product * right_product # Will always be 0 due to bug def product_of_array_fixed(arr, left, right): """FIXED: Returns 1 (multiplicative identity) for empty range.""" if left > right: return 1 # Correct: 1 × anything = anything if left == right: return arr[left] mid = left + (right - left) // 2 left_product = product_of_array_fixed(arr, left, mid) right_product = product_of_array_fixed(arr, mid + 1, right) return left_product * right_product # BUG #5: Base case too broaddef sum_array_too_broad(arr): """ Might work, but broad base case can hide bugs and is less efficient than necessary. """ # Too broad: handles 3 elements specially when 1 would suffice if len(arr) <= 3: return sum(arr) # Works, but is it necessary? mid = len(arr) // 2 return sum_array_too_broad(arr[:mid]) + sum_array_too_broad(arr[mid:]) def sum_array_minimal(arr): """Minimal, clean base case.""" if len(arr) == 0: return 0 # Identity for addition if len(arr) == 1: return arr[0] # Single element: its own sum mid = len(arr) // 2 return sum_array_minimal(arr[:mid]) + sum_array_minimal(arr[mid:]) # Testing edge casestest_cases = [ [], # Empty [5], # Single element [3, 7], # Two elements [1, 2, 3, 4], # Multiple elements] for arr in test_cases: try: result = product_of_array_fixed(arr, 0, len(arr) - 1) expected = 1 for x in arr: expected *= x print(f"Array {arr}: result={result}, expected={expected}, match={result==expected}") except Exception as e: print(f"Array {arr}: error - {e}")Add unit tests that specifically target base case inputs: empty arrays, single elements, two elements, null nodes, etc. Many bugs only manifest with these edge cases, and production data rarely tests them naturally.
Many D&C algorithms require multiple base cases. Understanding when and why helps you design more robust algorithms.
When Multiple Base Cases Are Needed:
Different input conditions require different handling: Binary Search has "not found" and "found" as distinct base cases.
Edge cases have different solutions: Empty array vs single element might return different types of results.
Optimization: Handling small fixed sizes (e.g., n ≤ 10) with a simpler algorithm can improve performance.
Mathematical definitions: Fibonacci requires F(0) = 0 and F(1) = 1—two distinct base cases.
Structuring Multiple Base Cases:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
# Strategies for handling multiple base cases # Strategy 1: Sequential checks (most common)def binary_search_multi_base(arr, left, right, target): """ Multiple sequential base case checks. Order matters: check more specific conditions first. """ # Base case 1: Empty range (target not in this range) if left > right: return -1 mid = left + (right - left) // 2 # Base case 2: Found the target if arr[mid] == target: return mid # Recursive cases if arr[mid] < target: return binary_search_multi_base(arr, mid + 1, right, target) return binary_search_multi_base(arr, left, mid - 1, target) # Strategy 2: Combined base case with conditional logicdef tree_diameter(node): """ Find diameter of binary tree. Uses a helper that returns (height, diameter). """ def helper(node): # Base case: null node if node is None: return (0, 0) # height=0, diameter=0 # Base case: leaf node (optimization, not strictly required) if node.left is None and node.right is None: return (1, 0) # height=1, diameter=0 left_height, left_diam = helper(node.left) right_height, right_diam = helper(node.right) height = 1 + max(left_height, right_height) # Diameter could be through this node or entirely in a subtree through_node = left_height + right_height diameter = max(through_node, left_diam, right_diam) return (height, diameter) return helper(node)[1] # Strategy 3: Threshold-based base case (optimization)INSERTION_SORT_THRESHOLD = 16 def hybrid_merge_sort(arr): """ Use insertion sort for small arrays. Multiple base cases for termination and optimization. """ n = len(arr) # Base case 1: Empty array if n == 0: return [] # Base case 2: Single element if n == 1: return arr[:] # Base case 3: Small array - use simpler algorithm if n <= INSERTION_SORT_THRESHOLD: return insertion_sort(arr) # Recursive case mid = n // 2 left = hybrid_merge_sort(arr[:mid]) right = hybrid_merge_sort(arr[mid:]) return merge(left, right) # Strategy 4: Guard clause style (early returns)def find_peak_element(arr, left, right): """ Find any peak element (greater than neighbors). Multiple base cases handled as early returns. """ # Guard: Empty range if left > right: return -1 # Guard: Single element is always a peak if left == right: return left mid = left + (right - left) // 2 # Guard: Found a peak (local maximum) is_greater_than_left = (mid == 0 or arr[mid] >= arr[mid - 1]) is_greater_than_right = (mid == len(arr) - 1 or arr[mid] >= arr[mid + 1]) if is_greater_than_left and is_greater_than_right: return mid # Base case: peak found # Recursive: move toward the higher neighbor if mid > 0 and arr[mid - 1] > arr[mid]: return find_peak_element(arr, left, mid - 1) return find_peak_element(arr, mid + 1, right) def insertion_sort(arr): """Helper for hybrid sort.""" result = arr[:] for i in range(1, len(result)): key = result[i] j = i - 1 while j >= 0 and result[j] > key: result[j + 1] = result[j] j -= 1 result[j + 1] = key return result def merge(left, right): """Helper for merge sort.""" result = [] i = j = 0 while i < len(left) and j < len(right): if left[i] <= right[j]: result.append(left[i]) i += 1 else: result.append(right[j]) j += 1 result.extend(left[i:]) result.extend(right[j:]) return resultWhen you have multiple base cases, check them in order from most specific to least specific. Check 'empty' before 'single element', check 'found' before 'continue searching'. This prevents accidentally falling through to the wrong case.
Thorough testing of base cases is essential for robust D&C implementations. Here's a systematic approach to validation.
The Base Case Test Suite:
For any D&C algorithm, your test suite should include:
Testing Strategy:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
import unittest class TestMergeSortBaseCases(unittest.TestCase): """Comprehensive base case testing for Merge Sort.""" def test_empty_array(self): """Base case: empty array returns empty.""" self.assertEqual(merge_sort([]), []) def test_single_element(self): """Base case: single element returns same element.""" self.assertEqual(merge_sort([5]), [5]) def test_two_elements_sorted(self): """Minimal divide case: already sorted pair.""" self.assertEqual(merge_sort([1, 2]), [1, 2]) def test_two_elements_unsorted(self): """Minimal divide case: unsorted pair.""" self.assertEqual(merge_sort([2, 1]), [1, 2]) def test_two_elements_equal(self): """Edge case: duplicate elements.""" self.assertEqual(merge_sort([3, 3]), [3, 3]) def test_power_of_two_size(self): """Tests clean division (no odd cases).""" arr = [8, 4, 6, 2, 7, 5, 3, 1] self.assertEqual(merge_sort(arr), [1, 2, 3, 4, 5, 6, 7, 8]) def test_non_power_of_two_size(self): """Tests uneven division.""" arr = [5, 2, 8, 1, 9] self.assertEqual(merge_sort(arr), [1, 2, 5, 8, 9]) class TestBinarySearchBaseCases(unittest.TestCase): """Comprehensive base case testing for Binary Search.""" def test_empty_array(self): """Base case: empty array returns not found.""" self.assertEqual(binary_search([], 5), -1) def test_single_element_found(self): """Base case: single element that is the target.""" self.assertEqual(binary_search([5], 5), 0) def test_single_element_not_found(self): """Base case: single element that is not the target.""" self.assertEqual(binary_search([5], 3), -1) def test_target_at_left_boundary(self): """Target is the first element.""" self.assertEqual(binary_search([1, 2, 3, 4, 5], 1), 0) def test_target_at_right_boundary(self): """Target is the last element.""" self.assertEqual(binary_search([1, 2, 3, 4, 5], 5), 4) def test_target_smaller_than_all(self): """Target is smaller than all elements.""" self.assertEqual(binary_search([2, 4, 6, 8], 1), -1) def test_target_larger_than_all(self): """Target is larger than all elements.""" self.assertEqual(binary_search([2, 4, 6, 8], 10), -1) class TestTreeAlgorithmBaseCases(unittest.TestCase): """Base case testing for tree-based D&C algorithms.""" def test_null_tree(self): """Base case: null/empty tree.""" self.assertEqual(tree_height(None), 0) self.assertEqual(tree_sum(None), 0) def test_single_node(self): """Base case: single node tree (root only).""" root = TreeNode(5) self.assertEqual(tree_height(root), 1) self.assertEqual(tree_sum(root), 5) def test_left_only_child(self): """Edge case: only left child.""" root = TreeNode(5, TreeNode(3), None) self.assertEqual(tree_height(root), 2) def test_right_only_child(self): """Edge case: only right child.""" root = TreeNode(5, None, TreeNode(7)) self.assertEqual(tree_height(root), 2) # Helper implementations for testing def merge_sort(arr): if len(arr) <= 1: return arr[:] mid = len(arr) // 2 left = merge_sort(arr[:mid]) right = merge_sort(arr[mid:]) return merge(left, right) def merge(left, right): result = [] i = j = 0 while i < len(left) and j < len(right): if left[i] <= right[j]: result.append(left[i]) i += 1 else: result.append(right[j]) j += 1 result.extend(left[i:]) result.extend(right[j:]) return result def binary_search(arr, target): return _binary_search(arr, 0, len(arr) - 1, target) def _binary_search(arr, left, right, target): if left > right: return -1 mid = left + (right - left) // 2 if arr[mid] == target: return mid if arr[mid] < target: return _binary_search(arr, mid + 1, right, target) return _binary_search(arr, left, mid - 1, target) class TreeNode: def __init__(self, val, left=None, right=None): self.val = val self.left = left self.right = right def tree_height(node): if node is None: return 0 return 1 + max(tree_height(node.left), tree_height(node.right)) def tree_sum(node): if node is None: return 0 return node.val + tree_sum(node.left) + tree_sum(node.right) if __name__ == '__main__': unittest.main()Consider adding fuzz tests that generate random small inputs (size 0-5). These often find edge cases you didn't think of. Property-based testing libraries like Hypothesis (Python) or fast-check (JS) are excellent for this.
Base cases are the bedrock upon which all D&C solutions are built. Without correct base cases, D&C algorithms fail—either through infinite recursion or through propagating incorrect partial solutions up the recursion tree.
You now have a comprehensive understanding of base cases in D&C: their role, identification, common patterns, pitfalls, and testing strategies. Next, we'll explore the recursive structure of D&C algorithms—how recursion naturally expresses the D&C pattern and how to reason about recursive solutions.