Review: Introduction to Recursive Programming
It’s a good book overall, but not outstanding. The content is not treated evenly between chapters. Nonetheless, there are some good chapters here and there:
- Chapter 1 and 2: The author does a good job introducing the concept of recursion intuitively with plenty of examples. Using diagrams to decompose and combine problems is a good technique for one new to recursion.
- Chapters 5 to 9: There is a wide range of examples and exercises on different types of recursion. Some of them are from interview questions, which is a plus.
It could be a great book if the author demonstrated more examples with better explanations on how to think recursively. Specifically, I want more examples similar to what is done for the Fibonacci sequence.
Sometimes, especially in the later chapters, the author is too verbose in explaining the code line by line, which makes the reading experience less enjoyable.
Python is used throughout the book, which is too easy for me.
So, I opted to explore Raku instead while following the examples in the book.
Due to the badly experience when writing Raku code in Neovim, I decided to use D language instead.
Going back to the code examples, some are bad, particularly when the author uses lists to represent tree structures.
Drawing fractals using recursion is a nice touch, though. I wish the book had more.
The code for the recursive descent parser is too verbose for my taste.
Overall: 3.5/5. I definitely recommend some chapters of the book to newbies, but not the whole book.
Code Examples & Exercises
The recursive version of matrix multiplication (Chapter 6.10) is really not efficient since there are 2 matrix allocations occurred at each level of recursion.
By simply removing the add_matrices_limits and using += instead of = in the base case, we do not have to allocate for the two temporary matrices.
A quick benchmark shows a significant speedup:
Benchmarking recur_matmul:76 ms, 107 μs, and 5 hnsecs
Benchmarking recur_matmul2:1 ms, 490 μs, and 7 hnsecs
mergesort suffers the similar issue, i.e., allocating new arrays at each level of recursion.
A better approach is to use an auxiliary array and swap the roles of the input and auxiliary arrays at each level of recursion as mentioned by (Sedgewick 1998).
void merge(int[] a, int[] aux, ulong low, ulong mid, ulong high) {
ulong i = low, j = mid + 1;
for (ulong k = low; k <= high; k++) {
if (i > mid) a[k] = aux[j++];
else if (j > high) a[k] = aux[i++];
else if (aux[j] < aux[i]) a[k] = aux[j++];
else a[k] = aux[i++];
}
}
void _mergesort(int[] a, int[] aux, ulong low, ulong high) {
if (high <= low) return;
auto mid = low + (high - low) / 2;
_mergesort(aux, a, low, mid);
_mergesort(aux, a, mid+1, high);
merge(a, aux, low, mid, high);
}
void mergesort(int[] a) {
int[] aux = a.dup;
_mergesort(a, aux, 0, a.length - 1);
}
I was excited when seeing the wood cutter problem being mentioned in the book. However, it is a shame that the code example does not demonstrate the subtlety of binary search implementation, i.e., how we calculate the middle point for the maximum finding problem.
import std.algorithm;
bool cut(int c, int k, const int[] heights) {
int total = 0;
foreach (h; heights) total += max(h-c, 0);
return total >= k;
}
int woodcutter(int k, int[] heights) {
int low = 0, high = maxElement(heights);
while (low < high) {
int mid = (low + high + 1) / 2;
if (!cut(mid, k, heights)) {
high = mid - 1;
} else {
low = mid;
}
}
return low;
}
int woodcutter2(int k, int[] heights) {
int low = 0, high = maxElement(heights);
int answer = 0;
while (low <= high) {
int mid = low + (high - low) / 2;
if (cut(mid, k, heights)) {
answer = mid;
low = mid + 1;
} else {
high = mid - 1;
}
}
return answer;
}
Fractals
Here is what Opus 4.5 suggested for drawing Kocke curve:
import raylib;
void drawKnockLine(Vector2 start, Vector2 end, int n) {
if (n == 0) {
DrawLineEx(start, end, THINK, COLOR);
return;
}
auto delta = Vector2Subtract(end, start);
auto p1 = Vector2Add(start, Vector2Scale(delta, 1.0/3));
auto p2 = Vector2Add(start, Vector2Scale(delta, 2.0/3));
auto mid = Vector2Add(p1, Vector2Scale(delta, 0.5/3));
auto peak = Vector2Add(mid, Vector2Rotate(Vector2Scale(delta, 1.0/6), -PI/3));
drawKnockLine(start, p1, n - 1);
drawKnockLine(p1, peak, n - 1);
drawKnockLine(peak, p2, n - 1);
drawKnockLine(p2, end, n - 1);
}
The mistake is subtle though, instead of using the delta vector to calculate the peak point, we should use the vector from p1 to p2.
References
- Sedgewick, Robert. 1998. Algorithms in C++, Parts 1 – 4: Fundamentals, Data Structure, Sorting, Searching. Pearson Education.