πΉ 1. Recursion Basics
Recursion = A function calling itself with a smaller subproblem until a base case is reached.
General template:
void recurse(params) {
if (base case) return; // stopping condition
// work
recurse(smaller problem);
}
πΉ 2. Pass by Value vs Pass by Reference
-
Java:
- Primitives (int, char, double) β pass by value (a copy is passed).
- Objects (ArrayList, HashMap) β reference is passed (so changes affect original).
-
Python:
- Everything is an object.
- Mutable objects (list, dict, set) β changes persist across function calls.
- Immutable objects (int, str, tuple) β new objects created instead of modifying.
πΉ 3. Mutable vs Immutable
Mutable: Can be changed after creation.
Examples: Pythonlist
,dict
; JavaArrayList
,HashMap
.Immutable: Cannot be changed after creation.
Examples: Pythonint
,str
,tuple
; JavaString
,Integer
.
β‘ Why does this matter?
π Because backtracking is needed only when recursion modifies a mutable object.
πΉ 4. When Do We Need Backtracking?
π Ask yourself:
Am I mutating a shared data structure?
-
β Yes (mutable, like list/array) β Backtracking needed
- Pattern:
add β recurse β remove
- Shared object β undo is required.
- Pattern:
-
β No (immutable, like string/number/new copy) β No backtracking
- Each recursive call gets its own copy.
- No undo required.
πΉ 5. Immutable State Approach (No Backtracking)
Each recursive call gets a new snapshot of the state.
- Safe: no undo needed.
- Trade-off: more memory usage.
Example (Phone keypad combinations, immutable strings)
def pad(p, up):
if not up:
print(p)
return
digit = int(up[0])
mapping = ["", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"]
for ch in mapping[digit]:
pad(p + ch, up[1:]) # creates new string each call
Here p + ch
creates a new string each time β no need to undo.
πΉ 6. Mutable State Approach (Needs Backtracking)
Recursive calls share the same object.
We must undo changes after recursion to avoid corrupting results.
- Efficient: less memory usage.
- Risk: must backtrack properly.
Example (Phone keypad combinations, mutable list)
def letter_combinations(digits):
mapping = ["", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"]
ans = []
def solve(index, path):
if index == len(digits):
ans.append("".join(path))
return
for ch in mapping[int(digits[index])]:
path.append(ch) # add
solve(index+1, path)
path.pop() # undo (backtrack)
solve(0, [])
return ans
Here path
is shared β after exploring one branch, we pop()
to reset it.
πΉ 7. Rule of Thumb
- If using immutable state (string, int, tuple) β No backtracking.
- If using mutable state (list, array, dict, set) β Backtracking required.
πΉ 8. Comparing Both Approaches
Approach | Example | Memory | Undo Needed? | When to Use |
---|---|---|---|---|
Immutable State | Passing p + ch in strings |
High | β No | When input size is small / immutables are used |
Mutable State | Modifying list with .append()
|
Low | β Yes | When input size is large / memory optimization needed |
πΉ 9. Example: Subset Problem
Without Backtracking (immutable list copies)
def subsets(nums):
ans = []
def dfs(index, path):
if index == len(nums):
ans.append(path[:]) # copy (new list)
return
dfs(index+1, path + [nums[index]]) # new list created
dfs(index+1, path) # no change
dfs(0, [])
return ans
With Backtracking (mutable shared list)
def subsets(nums):
ans = []
def dfs(index, path):
if index == len(nums):
ans.append(path[:]) # copy final
return
path.append(nums[index]) # add
dfs(index+1, path)
path.pop() # undo
dfs(index+1, path)
dfs(0, [])
return ans
πΉ 10. Key Takeaways
- Recursion = breaking problem into smaller subproblems.
- Backtracking = recursion + undoing changes.
- Check mutability:
- Immutable β no backtracking.
-
Mutable β backtracking needed.
- Trade-off:
Immutable β simpler, more memory.
Mutable β efficient, needs careful undo.
Top comments (0)