From 5013a23d0c3af39828a0e212fd1de9748fb46fc9 Mon Sep 17 00:00:00 2001 From: Ryan Maleki Date: Tue, 18 Mar 2025 20:10:59 -0700 Subject: [PATCH 1/2] Add examples and clarification for recursion problems, add diagram to recursion readme --- recursion/README.md | 12 +++++------- recursion/climbing_stairs_test.go | 13 +++++++++++++ recursion/exponentiation_test.go | 2 ++ recursion/expression_operators_test.go | 4 ++-- recursion/is_palindrome_test.go | 4 +++- recursion/multiplication_test.go | 2 ++ recursion/regular_expression_test.go | 4 ++++ recursion/reverse_number_test.go | 6 ++++-- 8 files changed, 35 insertions(+), 12 deletions(-) diff --git a/recursion/README.md b/recursion/README.md index 66da4dc..8241926 100644 --- a/recursion/README.md +++ b/recursion/README.md @@ -3,11 +3,11 @@ Recursion is a computational technique that implements a [divide-and-conquer](../dnc) approach to problem-solving by breaking down a complex problem into smaller sub-problems. It consists of two components: * One or more base cases that provide output for simple inputs and terminate recursion -* A recursive case that combines the outputs obtained from recursive function calls to generate a solution for the original problem. +* A recursive case that combines the outputs obtained from recursive function calls to generate a solution for the original problem Although recursions enhance code readability, they are usually inefficient and challenging to debug. Consequently, unless they provide a more efficient solution to a problem, such as in the case of [quicksort](../dnc/quick_sort_test.go), they are generally not preferred. -During execution, a program typically stores function variables in a memory area known as the stack before executing recursion. The recursive function may assign different values to the same variables during each recursion. When the recursion ends, the stack pops and remembers the values. However, the stack will grow with each call if recursion continues indefinitely, causing the familiar stack overflow error. Since recursion employs the stack to execute, every recursive problem can be converted into an iterative one. This transformation, however, typically leads to more complex code and may require a [stack](../stack). +During execution, a program typically stores function variables in a memory area known as the stack before executing recursion. The recursive function may assign different values to the same variables that are stored separately for each call during each recursion. When the recursion ends, the stack pops and remembers the values. However, the stack will grow with each call if recursion continues indefinitely, causing the familiar stack overflow error. Since recursion employs the stack to execute, every recursive problem can be converted into an iterative one. This transformation, however, typically leads to more complex code and may require a [stack](../stack). ## Implementation @@ -16,9 +16,7 @@ The computation of the nth Fibonacci number can be achieved with recursion. For ```Go package main -import ( - "fmt" -) +import "fmt" func main() { for i := 1; i <= 10; i++ { @@ -37,8 +35,8 @@ func fibonacci(n int) int { When formulating recursive algorithms, it is essential to consider the following four rules of recursion: 1. It is imperative to establish a base case, or else the program will terminate abruptly -2. The algorithm should progress toward the base case at each recursive call. -3. Recursive calls are presumed effective; thus, traversing every recursive call and performing bookkeeping is unnecessary. +2. The algorithm should progress toward the base case at each recursive call +3. Recursive calls are presumed effective; thus, traversing every recursive call and performing bookkeeping is unnecessary 4. Use memoization, a technique that prevents redundant computation by caching previously computed results, can enhance the algorithm's efficiency. ## Complexity diff --git a/recursion/climbing_stairs_test.go b/recursion/climbing_stairs_test.go index b69ff40..a7f5048 100644 --- a/recursion/climbing_stairs_test.go +++ b/recursion/climbing_stairs_test.go @@ -9,6 +9,19 @@ TestClimbingStairs tests solution(s) with the following signature and problem de Given n the number of steps, return in how many ways you can climb these stairs if you are only able to climb 1 or 2 steps at a time. + +For example given 5 we can climb the stairs in the following ways: + + 1, 1, 1, 1, 1 + 1, 1, 1, 2 + 1, 1, 2, 1 + 1, 2, 1, 1 + 2, 1, 1, 1 + 2, 2, 1, + 1, 2, 2, + 2, 1, 2, + +So the algorithm should return 8. */ func TestClimbingStairs(t *testing.T) { tests := []struct { diff --git a/recursion/exponentiation_test.go b/recursion/exponentiation_test.go index 0d55a53..beee527 100644 --- a/recursion/exponentiation_test.go +++ b/recursion/exponentiation_test.go @@ -8,6 +8,8 @@ TestPowerOf tests solution(s) with the following signature and problem descripti func PowerOf(x, n int) int Given x and n, return x raised to the power of n in an efficient manner. + +For example given x=2 and n=3 the algorithm should return 8. */ func TestPowerOf(t *testing.T) { tests := []struct { diff --git a/recursion/expression_operators_test.go b/recursion/expression_operators_test.go index e76fbb7..4771cc4 100644 --- a/recursion/expression_operators_test.go +++ b/recursion/expression_operators_test.go @@ -7,12 +7,12 @@ TestExpressionOperators tests solution(s) with the following signature and probl func ExpressionOperators(list []int, target int) string -Given a list of numbers representing operands in an equation, and a target integer representing +Given a slice of numbers representing operands in an equation, and a target integer representing the result of the equation, return a string representing operators that can be inserted between the operands to form the equation and yield the target result. Only + and - operators are allowed and the are assumed to have the same priority -For example given {1,5,3} and 3, return +- because 1+5-3 = 3. +For example given {1,5,3} and 3 return {+,-} because 1+5-3 = 3. */ func TestExpressionOperators(t *testing.T) { tests := []struct { diff --git a/recursion/is_palindrome_test.go b/recursion/is_palindrome_test.go index 6704b2b..9d38a93 100644 --- a/recursion/is_palindrome_test.go +++ b/recursion/is_palindrome_test.go @@ -7,7 +7,9 @@ TestIsPalindrome tests solution(s) with the following signature and problem desc func IsPalindrome(s string) bool -Given a string like `abba` return true if it's a palindrome and false otherwise. +Given a string return true if it's a palindrome and false otherwise. + +For example given `abba` return true. Given `abca` return false. */ func TestIsPalindrome(t *testing.T) { tests := []struct { diff --git a/recursion/multiplication_test.go b/recursion/multiplication_test.go index c7b582c..1019974 100644 --- a/recursion/multiplication_test.go +++ b/recursion/multiplication_test.go @@ -9,6 +9,8 @@ TestMultiplication tests solution(s) with the following signature and problem de Given two integers, return their product using recursion and without using the multiplication operator. + +For example given 2 and 3 return 6. */ func TestMultiplication(t *testing.T) { tests := []struct { diff --git a/recursion/regular_expression_test.go b/recursion/regular_expression_test.go index 1f9f849..6abc087 100644 --- a/recursion/regular_expression_test.go +++ b/recursion/regular_expression_test.go @@ -13,6 +13,9 @@ Given an input and a regular expression pattern where `*` denotes to zero or more of the proceeding characters Write a recursive function to return true if the input matches the pattern and false otherwise. + +For example given input `aa` and pattern `a*` the algorithm should return true, but given the same +pattern and "ba" it should return false. */ func TestRegularExpressions(t *testing.T) { tests := []struct { @@ -27,6 +30,7 @@ func TestRegularExpressions(t *testing.T) { {"aa", "*", false}, {"aa", "*a", false}, {"aa", "a*", true}, + {"ba", "a*", false}, {"aa", ".", false}, {"ab", ".", false}, {"ad", "d", false}, diff --git a/recursion/reverse_number_test.go b/recursion/reverse_number_test.go index da092d3..1e0e503 100644 --- a/recursion/reverse_number_test.go +++ b/recursion/reverse_number_test.go @@ -7,8 +7,9 @@ TestReverseDigits tests solution(s) with the following signature and problem des func ReverseDigits(n int) int -Given an integer like 321 return a reversed number using recursion where the same digits -are repeated in the reverse order like 321. +Given an integer reverse the order of the digits. + +For example given 123 return 321. */ func TestReverseDigits(t *testing.T) { tests := []struct { @@ -19,6 +20,7 @@ func TestReverseDigits(t *testing.T) { {12, 21}, {112, 211}, {110, 11}, + {123, 321}, } for i, test := range tests { From f9363ac9dac9c35ae0c3d59e0150b203d33b5859 Mon Sep 17 00:00:00 2001 From: Ryan Maleki Date: Wed, 26 Mar 2025 21:33:48 -0700 Subject: [PATCH 2/2] Add details to recursion readme --- recursion/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/recursion/README.md b/recursion/README.md index 8241926..f84b47f 100644 --- a/recursion/README.md +++ b/recursion/README.md @@ -34,7 +34,7 @@ func fibonacci(n int) int { When formulating recursive algorithms, it is essential to consider the following four rules of recursion: -1. It is imperative to establish a base case, or else the program will terminate abruptly +1. It is imperative to establish a base case, or else the program keep recursing and terminate abruptly after running out of stack memory 2. The algorithm should progress toward the base case at each recursive call 3. Recursive calls are presumed effective; thus, traversing every recursive call and performing bookkeeping is unnecessary 4. Use memoization, a technique that prevents redundant computation by caching previously computed results, can enhance the algorithm's efficiency. @@ -45,9 +45,9 @@ Recursions are often inefficient in both time and space complexity. The number o There are a few different ways of determining the time complexity of recursive algorithms: -1. Recurrence Relations: This approach involves defining a recurrence relation that expresses the algorithm's time complexity in terms of its sub-problems' time complexity. For example, for the recursive Fibonacci algorithm, the recurrence relation is T(n) = T(n-1) + T(n-2) + O(1), where T(n) represents the time complexity of the algorithm for an input of size n. -2. Recursion Trees: This method involves drawing a tree to represent the algorithm's recursive calls. The algorithm's time complexity can be calculated by summing the work done at each level of the tree. For example, for the recursive factorial algorithm, each level of the tree represents a call to the function with a smaller input size, and the work done at each level is constant. -3. Master Theorem: This approach is a formula for solving recurrence relations that have the form T(n) = aT(n/b) + f(n). The Master Theorem can be used to quickly determine the time complexity of some [Divide-and-conquer](../dnc) algorithms. +1. Recurrence Relations: This approach involves defining a recurrence relation that expresses the algorithm's time complexity in terms of its sub-problems' time complexity. For example, for the recursive Fibonacci algorithm, the recurrence relation is T(n) = T(n-1) + T(n-2) + O(1), where T(n) represents the time complexity of the algorithm for an input of size n +2. Recursion Trees: This method involves drawing a tree to represent the algorithm's recursive calls. The algorithm's time complexity can be calculated by summing the work done at each level of the tree. For example, for the recursive factorial algorithm, each level of the tree represents a call to the function with a smaller input size, and the work done at each level is constant +3. Master Theorem: This approach is a formula for solving recurrence relations that have the form T(n) = aT(n/b) + f(n). The Master Theorem can be used to quickly determine the time complexity of some [Divide-and-conquer](../dnc) algorithms The space complexity of recursive calls is affected by having to store a copy of the state and variables in the stack with each recursion.