mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-14 16:16:06 +00:00
Revisit the English version (#1835)
* Review the English version using Claude-4.5. * Update mkdocs.yml * Align the section titles. * Bug fixes
This commit is contained in:
@@ -1,77 +1,77 @@
|
||||
# Iteration and recursion
|
||||
# Iteration and Recursion
|
||||
|
||||
In algorithms, the repeated execution of a task is quite common and is closely related to the analysis of complexity. Therefore, before delving into the concepts of time complexity and space complexity, let's first explore how to implement repetitive tasks in programming. This involves understanding two fundamental programming control structures: iteration and recursion.
|
||||
In algorithms, repeatedly executing a task is very common and closely related to complexity analysis. Therefore, before introducing time complexity and space complexity, let's first understand how to implement repeated task execution in programs, namely the two basic program control structures: iteration and recursion.
|
||||
|
||||
## Iteration
|
||||
|
||||
<u>Iteration</u> is a control structure for repeatedly performing a task. In iteration, a program repeats a block of code as long as a certain condition is met until this condition is no longer satisfied.
|
||||
<u>Iteration</u> is a control structure for repeatedly executing a task. In iteration, a program repeatedly executes a segment of code under certain conditions until those conditions are no longer satisfied.
|
||||
|
||||
### For loops
|
||||
### for Loop
|
||||
|
||||
The `for` loop is one of the most common forms of iteration, and **it's particularly suitable when the number of iterations is known in advance**.
|
||||
The `for` loop is one of the most common forms of iteration, **suitable for use when the number of iterations is known in advance**.
|
||||
|
||||
The following function uses a `for` loop to perform a summation of $1 + 2 + \dots + n$, with the sum being stored in the variable `res`. It's important to note that in Python, `range(a, b)` creates an interval that is inclusive of `a` but exclusive of `b`, meaning it iterates over the range from $a$ up to $b−1$.
|
||||
The following function implements the summation $1 + 2 + \dots + n$ based on a `for` loop, with the sum result recorded using the variable `res`. Note that in Python, `range(a, b)` corresponds to a "left-closed, right-open" interval, with the traversal range being $a, a + 1, \dots, b-1$:
|
||||
|
||||
```src
|
||||
[file]{iteration}-[class]{}-[func]{for_loop}
|
||||
```
|
||||
|
||||
The figure below represents this sum function.
|
||||
The figure below shows the flowchart of this summation function.
|
||||
|
||||

|
||||

|
||||
|
||||
The number of operations in this summation function is proportional to the size of the input data $n$, or in other words, it has a linear relationship. **This "linear relationship" is what time complexity describes**. This topic will be discussed in more detail in the next section.
|
||||
The number of operations in this summation function is proportional to the input data size $n$, or has a "linear relationship". In fact, **time complexity describes precisely this "linear relationship"**. Related content will be introduced in detail in the next section.
|
||||
|
||||
### While loops
|
||||
### while Loop
|
||||
|
||||
Similar to `for` loops, `while` loops are another approach for implementing iteration. In a `while` loop, the program checks a condition at the beginning of each iteration; if the condition is true, the execution continues, otherwise, the loop ends.
|
||||
Similar to the `for` loop, the `while` loop is also a method for implementing iteration. In a `while` loop, the program first checks the condition in each round; if the condition is true, it continues execution, otherwise it ends the loop.
|
||||
|
||||
Below we use a `while` loop to implement the sum $1 + 2 + \dots + n$.
|
||||
Below we use a `while` loop to implement the summation $1 + 2 + \dots + n$:
|
||||
|
||||
```src
|
||||
[file]{iteration}-[class]{}-[func]{while_loop}
|
||||
```
|
||||
|
||||
**`while` loops provide more flexibility than `for` loops**, especially since they allow for custom initialization and modification of the condition variable at each step.
|
||||
**The `while` loop has greater flexibility than the `for` loop**. In a `while` loop, we can freely design the initialization and update steps of the condition variable.
|
||||
|
||||
For example, in the following code, the condition variable $i$ is updated twice each round, which would be inconvenient to implement with a `for` loop.
|
||||
For example, in the following code, the condition variable $i$ is updated twice per round, which is not convenient to implement using a `for` loop:
|
||||
|
||||
```src
|
||||
[file]{iteration}-[class]{}-[func]{while_loop_ii}
|
||||
```
|
||||
|
||||
Overall, **`for` loops are more concise, while `while` loops are more flexible**. Both can implement iterative structures. Which one to use should be determined based on the specific requirements of the problem.
|
||||
Overall, **`for` loops have more compact code, while `while` loops are more flexible**; both can implement iterative structures. The choice of which to use should be determined based on the requirements of the specific problem.
|
||||
|
||||
### Nested loops
|
||||
### Nested Loops
|
||||
|
||||
We can nest one loop structure within another. Below is an example using `for` loops:
|
||||
We can nest one loop structure inside another. Below is an example using `for` loops:
|
||||
|
||||
```src
|
||||
[file]{iteration}-[class]{}-[func]{nested_for_loop}
|
||||
```
|
||||
|
||||
The figure below represents this nested loop.
|
||||
The figure below shows the flowchart of this nested loop.
|
||||
|
||||

|
||||

|
||||
|
||||
In such cases, the number of operations of the function is proportional to $n^2$, meaning the algorithm's runtime and the size of the input data $n$ has a 'quadratic relationship.'
|
||||
In this case, the number of operations of the function is proportional to $n^2$, or the algorithm's running time has a "quadratic relationship" with the input data size $n$.
|
||||
|
||||
We can further increase the complexity by adding more nested loops, each level of nesting effectively "increasing the dimension," which raises the time complexity to "cubic," "quartic," and so on.
|
||||
We can continue adding nested loops, where each nesting is a "dimension increase", raising the time complexity to "cubic relationship", "quartic relationship", and so on.
|
||||
|
||||
## Recursion
|
||||
|
||||
<u>Recursion</u> is an algorithmic strategy where a function solves a problem by calling itself. It primarily involves two phases:
|
||||
<u>Recursion</u> is an algorithmic strategy that solves problems by having a function call itself. It mainly consists of two phases.
|
||||
|
||||
1. **Calling**: This is where the program repeatedly calls itself, often with progressively smaller or simpler arguments, moving towards the "termination condition."
|
||||
2. **Returning**: Upon triggering the "termination condition," the program begins to return from the deepest recursive function, aggregating the results of each layer.
|
||||
1. **Descend**: The program continuously calls itself deeper, usually passing in smaller or more simplified parameters, until reaching a "termination condition".
|
||||
2. **Ascend**: After triggering the "termination condition", the program returns layer by layer from the deepest recursive function, aggregating the result of each layer.
|
||||
|
||||
From an implementation perspective, recursive code mainly includes three elements.
|
||||
From an implementation perspective, recursive code mainly consists of three elements.
|
||||
|
||||
1. **Termination Condition**: Determines when to switch from "calling" to "returning."
|
||||
2. **Recursive Call**: Corresponds to "calling," where the function calls itself, usually with smaller or more simplified parameters.
|
||||
3. **Return Result**: Corresponds to "returning," where the result of the current recursion level is returned to the previous layer.
|
||||
1. **Termination condition**: Used to determine when to switch from "descending" to "ascending".
|
||||
2. **Recursive call**: Corresponds to "descending", where the function calls itself, usually with smaller or more simplified parameters.
|
||||
3. **Return result**: Corresponds to "ascending", returning the result of the current recursion level to the previous layer.
|
||||
|
||||
Observe the following code, where simply calling the function `recur(n)` can compute the sum of $1 + 2 + \dots + n$:
|
||||
Observe the following code. We only need to call the function `recur(n)` to complete the calculation of $1 + 2 + \dots + n$:
|
||||
|
||||
```src
|
||||
[file]{recursion}-[class]{}-[func]{recur}
|
||||
@@ -79,116 +79,116 @@ Observe the following code, where simply calling the function `recur(n)` can com
|
||||
|
||||
The figure below shows the recursive process of this function.
|
||||
|
||||

|
||||

|
||||
|
||||
Although iteration and recursion can achieve the same results from a computational standpoint, **they represent two entirely different paradigms of thinking and problem-solving**.
|
||||
Although from a computational perspective, iteration and recursion can achieve the same results, **they represent two completely different paradigms for thinking about and solving problems**.
|
||||
|
||||
- **Iteration**: Solves problems "from the bottom up." It starts with the most basic steps, and then repeatedly adds or accumulates these steps until the task is complete.
|
||||
- **Recursion**: Solves problems "from the top down." It breaks down the original problem into smaller sub-problems, each of which has the same form as the original problem. These sub-problems are then further decomposed into even smaller sub-problems, stopping at the base case whose solution is known.
|
||||
- **Iteration**: Solves problems "bottom-up". Starting from the most basic steps, these steps are then repeatedly executed or accumulated until the task is complete.
|
||||
- **Recursion**: Solves problems "top-down". The original problem is decomposed into smaller subproblems that have the same form as the original problem. These subproblems continue to be decomposed into even smaller subproblems until reaching the base case (where the solution is known).
|
||||
|
||||
Let's take the earlier example of the summation function, defined as $f(n) = 1 + 2 + \dots + n$.
|
||||
Taking the above summation function as an example, let the problem be $f(n) = 1 + 2 + \dots + n$.
|
||||
|
||||
- **Iteration**: In this approach, we simulate the summation process within a loop. Starting from $1$ and traversing to $n$, we perform the summation operation in each iteration to eventually compute $f(n)$.
|
||||
- **Recursion**: Here, the problem is broken down into a sub-problem: $f(n) = n + f(n-1)$. This decomposition continues recursively until reaching the base case, $f(1) = 1$, at which point the recursion terminates.
|
||||
- **Iteration**: Simulates the summation process in a loop, traversing from $1$ to $n$, performing the summation operation in each round to obtain $f(n)$.
|
||||
- **Recursion**: Decomposes the problem into the subproblem $f(n) = n + f(n-1)$, continuously decomposing (recursively) until terminating at the base case $f(1) = 1$.
|
||||
|
||||
### Call stack
|
||||
### Call Stack
|
||||
|
||||
Every time a recursive function calls itself, the system allocates memory for the newly initiated function to store local variables, the return address, and other relevant information. This leads to two primary outcomes.
|
||||
Each time a recursive function calls itself, the system allocates memory for the newly opened function to store local variables, call addresses, and other information. This leads to two consequences.
|
||||
|
||||
- The function's context data is stored in a memory area called "stack frame space" and is only released after the function returns. Therefore, **recursion generally consumes more memory space than iteration**.
|
||||
- Recursive calls introduce additional overhead. **Hence, recursion is usually less time-efficient than loops.**
|
||||
- The function's context data is stored in a memory area called "stack frame space", which is not released until the function returns. Therefore, **recursion usually consumes more memory space than iteration**.
|
||||
- Recursive function calls incur additional overhead. **Therefore, recursion is usually less time-efficient than loops**.
|
||||
|
||||
As shown in the figure below, there are $n$ unreturned recursive functions before triggering the termination condition, indicating a **recursion depth of $n$**.
|
||||
As shown in the figure below, before the termination condition is triggered, there are $n$ unreturned recursive functions existing simultaneously, with a **recursion depth of $n$**.
|
||||
|
||||

|
||||
|
||||
In practice, the depth of recursion allowed by programming languages is usually limited, and excessively deep recursion can lead to stack overflow errors.
|
||||
In practice, the recursion depth allowed by programming languages is usually limited, and excessively deep recursion may lead to stack overflow errors.
|
||||
|
||||
### Tail recursion
|
||||
### Tail Recursion
|
||||
|
||||
Interestingly, **if a function performs its recursive call as the very last step before returning,** it can be optimized by the compiler or interpreter to be as space-efficient as iteration. This scenario is known as <u>tail recursion</u>.
|
||||
Interestingly, **if a function makes the recursive call as the very last step before returning**, the function can be optimized by the compiler or interpreter to have space efficiency comparable to iteration. This case is called <u>tail recursion</u>.
|
||||
|
||||
- **Regular recursion**: In standard recursion, when the function returns to the previous level, it continues to execute more code, requiring the system to save the context of the previous call.
|
||||
- **Tail recursion**: Here, the recursive call is the final operation before the function returns. This means that upon returning to the previous level, no further actions are needed, so the system does not need to save the context of the previous level.
|
||||
- **Regular recursion**: When a function returns to the previous level, it needs to continue executing code, so the system needs to save the context of the previous layer's call.
|
||||
- **Tail recursion**: The recursive call is the last operation before the function returns, meaning that after returning to the previous level, there is no need to continue executing other operations, so the system does not need to save the context of the previous layer's function.
|
||||
|
||||
For example, in calculating $1 + 2 + \dots + n$, we can make the result variable `res` a parameter of the function, thereby achieving tail recursion:
|
||||
Taking the calculation of $1 + 2 + \dots + n$ as an example, we can set the result variable `res` as a function parameter to implement tail recursion:
|
||||
|
||||
```src
|
||||
[file]{recursion}-[class]{}-[func]{tail_recur}
|
||||
```
|
||||
|
||||
The execution process of tail recursion is shown in the figure below. Comparing regular recursion and tail recursion, the point of the summation operation is different.
|
||||
The execution process of tail recursion is shown in the figure below. Comparing regular recursion and tail recursion, the execution point of the summation operation is different.
|
||||
|
||||
- **Regular recursion**: The summation operation occurs during the "returning" phase, requiring another summation after each layer returns.
|
||||
- **Tail recursion**: The summation operation occurs during the "calling" phase, and the "returning" phase only involves returning through each layer.
|
||||
- **Regular recursion**: The summation operation is performed during the "ascending" process, requiring an additional summation operation after each layer returns.
|
||||
- **Tail recursion**: The summation operation is performed during the "descending" process; the "ascending" process only needs to return layer by layer.
|
||||
|
||||

|
||||
|
||||
!!! tip
|
||||
|
||||
Note that many compilers or interpreters do not support tail recursion optimization. For example, Python does not support tail recursion optimization by default, so even if the function is in the form of tail recursion, it may still encounter stack overflow issues.
|
||||
Please note that many compilers or interpreters do not support tail recursion optimization. For example, Python does not support tail recursion optimization by default, so even if a function is in tail recursive form, it may still encounter stack overflow issues.
|
||||
|
||||
### Recursion tree
|
||||
### Recursion Tree
|
||||
|
||||
When dealing with algorithms related to "divide and conquer", recursion often offers a more intuitive approach and more readable code than iteration. Take the "Fibonacci sequence" as an example.
|
||||
When dealing with algorithmic problems related to "divide and conquer", recursion often provides a more intuitive approach and more readable code than iteration. Taking the "Fibonacci sequence" as an example.
|
||||
|
||||
!!! question
|
||||
|
||||
Given a Fibonacci sequence $0, 1, 1, 2, 3, 5, 8, 13, \dots$, find the $n$th number in the sequence.
|
||||
Given a Fibonacci sequence $0, 1, 1, 2, 3, 5, 8, 13, \dots$, find the $n$-th number in the sequence.
|
||||
|
||||
Let the $n$th number of the Fibonacci sequence be $f(n)$, it's easy to deduce two conclusions:
|
||||
Let the $n$-th number of the Fibonacci sequence be $f(n)$. Two conclusions can be easily obtained.
|
||||
|
||||
- The first two numbers of the sequence are $f(1) = 0$ and $f(2) = 1$.
|
||||
- Each number in the sequence is the sum of the two preceding ones, that is, $f(n) = f(n - 1) + f(n - 2)$.
|
||||
- Each number in the sequence is the sum of the previous two numbers, i.e., $f(n) = f(n - 1) + f(n - 2)$.
|
||||
|
||||
Using the recursive relation, and considering the first two numbers as termination conditions, we can write the recursive code. Calling `fib(n)` will yield the $n$th number of the Fibonacci sequence:
|
||||
Following the recurrence relation to make recursive calls, with the first two numbers as termination conditions, we can write the recursive code. Calling `fib(n)` will give us the $n$-th number of the Fibonacci sequence:
|
||||
|
||||
```src
|
||||
[file]{recursion}-[class]{}-[func]{fib}
|
||||
```
|
||||
|
||||
Observing the above code, we see that it recursively calls two functions within itself, **meaning that one call generates two branching calls**. As illustrated in the figure below, this continuous recursive calling eventually creates a <u>recursion tree</u> with a depth of $n$.
|
||||
Observing the above code, we recursively call two functions within the function, **meaning that one call produces two call branches**. As shown in the figure below, such continuous recursive calling will eventually produce a <u>recursion tree</u> with $n$ levels.
|
||||
|
||||

|
||||

|
||||
|
||||
Fundamentally, recursion embodies the paradigm of "breaking down a problem into smaller sub-problems." This divide-and-conquer strategy is crucial.
|
||||
Fundamentally, recursion embodies the paradigm of "decomposing a problem into smaller subproblems", and this divide-and-conquer strategy is crucial.
|
||||
|
||||
- From an algorithmic perspective, many important strategies like searching, sorting, backtracking, divide-and-conquer, and dynamic programming directly or indirectly use this way of thinking.
|
||||
- From a data structure perspective, recursion is naturally suited for dealing with linked lists, trees, and graphs, as they are well suited for analysis using the divide-and-conquer approach.
|
||||
- From an algorithmic perspective, many important algorithmic strategies such as searching, sorting, backtracking, divide and conquer, and dynamic programming directly or indirectly apply this way of thinking.
|
||||
- From a data structure perspective, recursion is naturally suited for handling problems related to linked lists, trees, and graphs, because they are well-suited for analysis using divide-and-conquer thinking.
|
||||
|
||||
## Comparison
|
||||
## Comparison of the Two
|
||||
|
||||
Summarizing the above content, the following table shows the differences between iteration and recursion in terms of implementation, performance, and applicability.
|
||||
Summarizing the above content, as shown in the table below, iteration and recursion differ in implementation, performance, and applicability.
|
||||
|
||||
<p align="center"> Table: Comparison of iteration and recursion characteristics </p>
|
||||
<p align="center"> Table <id> Comparison of iteration and recursion characteristics </p>
|
||||
|
||||
| | Iteration | Recursion |
|
||||
| ----------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Approach | Loop structure | Function calls itself |
|
||||
| Time Efficiency | Generally higher efficiency, no function call overhead | Each function call generates overhead |
|
||||
| Memory Usage | Typically uses a fixed size of memory space | Accumulative function calls can use a substantial amount of stack frame space |
|
||||
| Suitable Problems | Suitable for simple loop tasks, intuitive and readable code | Suitable for problem decomposition, like trees, graphs, divide-and-conquer, backtracking, etc., concise and clear code structure |
|
||||
| | Iteration | Recursion |
|
||||
| -------------- | -------------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
||||
| Implementation | Loop structure | Function calls itself |
|
||||
| Time efficiency | Generally more efficient, no function call overhead | Each function call incurs overhead |
|
||||
| Memory usage | Usually uses a fixed amount of memory space | Accumulated function calls may use a large amount of stack frame space |
|
||||
| Suitable problems | Suitable for simple loop tasks, with intuitive and readable code | Suitable for subproblem decomposition, such as trees, graphs, divide and conquer, backtracking, etc., with concise and clear code structure |
|
||||
|
||||
!!! tip
|
||||
|
||||
If you find the following content difficult to understand, consider revisiting it after reading the "Stack" chapter.
|
||||
If you find the following content difficult to understand, you can review it after reading the "Stack" chapter.
|
||||
|
||||
So, what is the intrinsic connection between iteration and recursion? Taking the above recursive function as an example, the summation operation occurs during the recursion's "return" phase. This means that the initially called function is the last to complete its summation operation, **mirroring the "last in, first out" principle of a stack**.
|
||||
What is the intrinsic relationship between iteration and recursion? Taking the above recursive function as an example, the summation operation is performed during the "ascending" phase of recursion. This means that the function called first actually completes its summation operation last, **and this working mechanism is similar to the "last-in, first-out" principle of stacks**.
|
||||
|
||||
Recursive terms like "call stack" and "stack frame space" hint at the close relationship between recursion and stacks.
|
||||
In fact, recursive terminology such as "call stack" and "stack frame space" already hints at the close relationship between recursion and stacks.
|
||||
|
||||
1. **Calling**: When a function is called, the system allocates a new stack frame on the "call stack" for that function, storing local variables, parameters, return addresses, and other data.
|
||||
2. **Returning**: When a function completes execution and returns, the corresponding stack frame is removed from the "call stack," restoring the execution environment of the previous function.
|
||||
1. **Descend**: When a function is called, the system allocates a new stack frame on the "call stack" for that function to store the function's local variables, parameters, return address, and other data.
|
||||
2. **Ascend**: When the function completes execution and returns, the corresponding stack frame is removed from the "call stack", restoring the execution environment of the previous function.
|
||||
|
||||
Therefore, **we can use an explicit stack to simulate the behavior of the call stack**, thus transforming recursion into an iterative form:
|
||||
Therefore, **we can use an explicit stack to simulate the behavior of the call stack**, thus transforming recursion into iterative form:
|
||||
|
||||
```src
|
||||
[file]{recursion}-[class]{}-[func]{for_loop_recur}
|
||||
```
|
||||
|
||||
Observing the above code, when recursion is transformed into iteration, the code becomes more complex. Although iteration and recursion can often be transformed into each other, it's not always advisable to do so for two reasons:
|
||||
Observing the above code, when recursion is transformed into iteration, the code becomes more complex. Although iteration and recursion can be converted into each other in many cases, it may not be worthwhile to do so for the following two reasons.
|
||||
|
||||
- The transformed code may become more challenging to understand and less readable.
|
||||
- For some complex problems, simulating the behavior of the system's call stack can be quite challenging.
|
||||
- The transformed code may be more difficult to understand and less readable.
|
||||
- For some complex problems, simulating the behavior of the system call stack can be very difficult.
|
||||
|
||||
In conclusion, **whether to choose iteration or recursion depends on the specific nature of the problem**. In programming practice, it's crucial to weigh the pros and cons of both and choose the most suitable approach for the situation at hand.
|
||||
In summary, **choosing between iteration and recursion depends on the nature of the specific problem**. In programming practice, it is crucial to weigh the pros and cons of both and choose the appropriate method based on the context.
|
||||
|
||||
Reference in New Issue
Block a user