View difference between Paste ID: 2nfZVGmH and Tf20cjnv
SHOW: | | - or go back to the newest paste.
1
Yesterday I told you the core problem with the existing resources out there on recursion: They don’t go deep enough.
2
3
Simply put, knowing how to compute a Fibonacci sequence or factorials really isn’t going to help you when it comes to the sorts of question you might actually get asked in your interviews. What if you have to find all the permutations of a list? How are those problems even related.
4
5
And what does everyone say when you’re trying to solve harder problems? “Oh just figure out what the subproblems are.” Oh yeah, thanks mom. Because if it were that easy, wouldn’t we already be doing that?
6
7
Today I want to show you the 6 patterns that you should know to solve recursive problems. If you understand the basics of these patterns, you should be able to group almost every recursive problem into one of these categories. From there, it only takes some minor adjustments to get to the solution to any given problem.
8
9
WARNING: The rest of this email is super long and technical. I recommend you pick one pattern that interests you and start there. If you try to learn it all at once, it will be really easy to get overwhelmed. There’s no need to understand this whole email in one sitting.
10
11
So let’s talk about these different patterns.
12
13
Iteration
14
As you hopefully know, any problem that can be solved recursively can also be solved iteratively and vice versa. This is a fundamental concept behind functional languages, where you use recursion to do everything; there are no loops.
15
16
While this is not something that you’re often going to need to do recursively, this pattern can come in useful once in awhile, particularly if you want to be able to refer back to items you’ve looped through previously. For example, consider the problem of printing a linked list in reverse order. There are plenty of approaches we can take for this problem, but recursion is uniquely concise:
17
18
public static void printReverse(Node n) { 
19
    if (n == null) return;
20
    printReverse(n.next);
21
    System.out.println(n.val);
22
}
23
For iteration, we simply make our recursive call on the remainder of our inputs, either by passing in the next node (as we did above) or by incrementing some index. While this doesn’t come up too often, this is one of the simplest applications of recursion.
24
25
Selection
26
This is my favorite pattern to test people on because it is one of the most common patterns to come up in recursion (and dynamic programming). In this pattern, we are simply finding all of the combinations of our input that match a certain criteria.
27
28
Consider for example the 0-1 Knapsack Problem. In this problem, we have a series of items that have weights and values. We want to figure out what the maximum value is that we can achieve while remaining under some fixed weight. Many people recognize this as a dynamic programming problem, since it’s a classic example, but let’s look at it from a recursive standpoint.
29
30
What would be a brute force solution to this problem? Well we can easily validate for a given combination of items whether it is under the maximum weight, and we can also easily compute the weight for any combination. That means if we could just generate all the different combinations, we could figure out the optimal answer.
31
32
Figuring out all the combinations is the core of a selection problem. I like the term “selection” because the way our code works is to simply include/exclude, or “select”, each item in our list.
33
34
The brute force code to generate all combinations might look something like this (this is simplified, but you get the idea):
35
36
public static List<List<Integer>> combinations(int[] n) {
37
    List<List<Integer>> results = new LinkedList();
38
    combinations(n, 0, results, new LinkedList<Integer>());
39
    return results;
40
}
41
42
private static void combinations(int[] n, int i, List<List<Integer>> results, List<Integer> path) {
43
    if (i == n.length) {
44
        results.add(path);
45
        return;
46
    }
47
    List<Integer> pathWithCurrent = new LinkedList(path);
48
    pathWithCurrent.add(n[i]);
49
    // Find all the combinations that exclude the current item
50
    combinations(n, i+1, results, path);
51
    // Find all the combinations that include the current item
52
    combinations(n, i+1, results, pathWithCurrent);
53
}
54
Once we understand this basic pattern, we can start to make optimizations. In many cases, we may actually be able to filter out some combinations prematurely. For example, in the Knapsack problem, we can limit our recursion to only consider combinations of items that stay below the prescribed weight.
55
56
There is much more to this pattern than I can include in this email, but it is one of the most important patterns to understand, so it is worth taking time to go through several examples. 0-1 Knapsack, Word Break, and N Queens are some good places to start.
57
58
Ordering
59
This pattern is the permutation to Selection’s combination. Essentially here we’re looking at any case in which we want to consider different orderings of our values. The most straightforward problem here is just to figure out all of the permutations of a given set of elements, although just like with selection, we may add in additional restrictions.
60
61
Some examples of problems that fall under this category are Bogosort (sorting a list of items by generating all permutations and determining which is sorted), finding all numbers that can be made from a set of digits that match a certain property, determine all palindromic strings that can be made from a set of characters, and more.
62
63
In its most simple form, this is one way that we can brute force generate all permutations of a list. You can also see an alternative approach here:
64
65
public static List<List<Integer>> permutations(Set<Integer> n) {
66
    List<List<Integer>> results = new LinkedList();
67
    permutations(n, results, new LinkedList<Integer>());
68
    return results;
69
}
70
71
private static void permutations(Set<Integer> n, List<List<Integer>> results, List<Integer> path) {
72
    if (n.isEmpty()) {
73
        results.add(path);
74
        return;
75
    }
76
    for (int i : n) {
77
        // For now ignore concurrent modification issue
78
        n.remove(i);
79
        List<Integer> pathWithCurrent = new LinkedList(path);
80
        pathWithCurrent.add(i);
81
        permutations(n, results, pathWithCurrent);
82
        n.add(i);
83
    }
84
}
85
As you can hopefully see, there is a lot of similarity between this solution and the combinations solution above. By understanding these two patterns and their variants, you can cover a huge number of the different possible recursive problems you might be asked in your interview.
86
87
Subproblems
88
I know this may come as a surprise, since at the beginning of the email I was bashing on people who tell you to just figure out the subproblems, but there is a time and a place for this. Although I find that many recursive problems don’t have such obvious subproblems as people would lead you to believe, there are some problems in which the subproblems are obvious.
89
90
One example of this that you certainly know is the Fibonacci problem. In that case, even the mathematical definition of a Fibonacci sequence is recursive.
91
92
Another problem that frequently comes up is the Towers of Hanoi problem. In this case, we can simply break down our problem by considering the subproblem of moving the top n-1 disks. If we can move the top n-1 disks, we just move all of them to the middle pin, move our bottom disk to the destination pin, and then move the above disks again over to the destination pin. This works because all of the disks above our bottom pin are not affected by any pins below them.
93
94
In problems like this, you have to look for those subproblems with which to break up the problem. However, these types of problems don’t come up too often, so for the most part you can just memorize specific examples.
95
96
Divide and Conquer
97
Hopefully, if you know about some of the common applications of recursion, you saw this one coming. Divide and conquer is the backbone to how we use techniques such as mergesort, binary search, depth first search, and more. In this technique, we attempt to break the problem space in half, solve each half separately, and then come back together.
98
99
I’m not going to go into too much depth on this pattern because it is primarily used as part of common algorithms that you should already know, such as the one I mentioned above, but there are a handful of problems for which this can be valuable.
100
101
For example, consider trying to determine all of the unique binary trees that we can generate from a set of numbers. If you pick a root node, you simply need to generate all of the possible variations of a left and right subtree.
102
103
Consider trying to find the maximum and minimum value of a list using the minimum number of comparisons. One way that we can do this is by splitting the list repeatedly, much the same as how we would do mergesort.
104
105
This technique generally applies to tree and sorting/searching problems where we will be splitting the problem space and then recombining the results.
106
107
Depth First Search
108
This is our final pattern and I left it for last for two reasons:
109
110
In a lot of cases, this primarily applies to graphs and trees, which is somewhat out of the scope of our discussion on pure recursion.
111
Almost any other recursive problem can be framed as DFS.
112
Now in terms of graphs and trees, I’m not going to go into any real detail. DFS is a basic technique that you should really know like the back of your hand. If you don’t, it’s time to get studying.
113
114
What I would like to discuss, however, is how we can apply other problems to DFS. Almost any of the problems that we’ve discussed above can be understood using DFS.
115
116
Let’s consider the example of brute force finding all combinations. In this problem our starting point for our DFS is an empty list. Then each step, we can search down one of two different paths: Either we include the current element or we don’t in our result.
117
118
Here is a diagram of what this looks like in a tree format. combo(i) refers to the recursive call with index i in our array:
119
120
(If you don't see images below, click "Display images")
121
122
123
124
As you can see along the bottom, we find every possible combination of elements from the input array {1, 2, 3}.
125
126
Whether you find this visual helpful, much of recursion can be most easily explained using a tree. Whether you use DFS or one of the other patterns above is relatively unimportant.
127
128
129
130
Phew. That was kind of a lot, wasn’t it? Here’s a quick recap of the 6 recursive patterns:
131
132
Iteration
133
Selection
134
Ordering
135
Subproblems
136
Divide and Conquer
137
Depth-first Search
138
If you truly understand these 6 patterns, you will have everything that you need to solve any recursive problem that might come up during your interview.
139
140
Now I know that this may still be a lot, and this long-ass email barely scratched the surface. Tomorrow, I’m going to show you a better way to learn all of this stuff so that you can really crush your interviews and get that dream job.
141
142
For now, though, pick one of these patterns and really study it. Code up one or two brute force examples and see if you can optimize them. Work through the code and make sure you really understand it. This will really help accelerate you towards understanding recursion.
143
144
Best,
145
146
Sam