Algorithm and data structure interview guide - detailed explanation of iteration and recursion

Iteration and recursion

In data structures and algorithms, it is very common to perform a certain task repeatedly, which is closely related to the complexity of the algorithm. To repeatedly perform a task, we usually use two basic program structures: iteration and recursion.

Iterate

"Iteration" is a control structure that repeatedly performs a task. In iteration, the program will repeatedly execute a certain code if certain conditions are met until the condition is no longer met.

for loop

forLoops are one of the most common forms of iteration and are suitable for use when the number of iterations is known in advance .

The following function forimplements the sum 1 + 2 + ⋯ + n 1 + 2 + \dots + n based on the loop1+2++n , the summation resultresis recorded using the variable. It should be noted thatrange(a, b)the corresponding interval in Python is "closed on the left and open on the right", and the corresponding traversal range isa, a + 1, …, b − 1 a, a + 1, \dots, b-1a,a+1,,b1

=== “Python”

```python title="iteration.py"
[class]{}-[func]{for_loop}
```

=== “C++”

```cpp title="iteration.cpp"
[class]{}-[func]{forLoop}
```

=== “Java”

```java title="iteration.java"
[class]{iteration}-[func]{forLoop}
```

=== “C#”

```csharp title="iteration.cs"
[class]{iteration}-[func]{forLoop}
```

=== “Go”

```go title="iteration.go"
[class]{}-[func]{forLoop}
```

=== “Swift”

```swift title="iteration.swift"
[class]{}-[func]{forLoop}
```

=== “JS”

```javascript title="iteration.js"
[class]{}-[func]{forLoop}
```

=== “TS”

```typescript title="iteration.ts"
[class]{}-[func]{forLoop}
```

=== “Dart”

```dart title="iteration.dart"
[class]{}-[func]{forLoop}
```

=== “Rust”

```rust title="iteration.rs"
[class]{}-[func]{for_loop}
```

=== “C”

```c title="iteration.c"
[class]{}-[func]{forLoop}
```

=== “Zig”

```zig title="iteration.zig"
[class]{}-[func]{forLoop}
```

The figure below shows the flow diagram of the summation function.

Insert image description here

The number of operations of this summation function is related to the input data size nnn is directly proportional, or a "linear relationship". In fact,time complexity describes this "linear relationship". Relevant content will be introduced in detail in the next section.

while loop

Similar to forloops, whileloops are also a way to implement iteration. In whilethe loop, the program will first check the condition each round, and if the condition is true, it will continue to execute, otherwise it will end the loop.

Below, we use whilea loop to implement the sum 1 + 2 + ⋯ + n 1 + 2 + \dots + n1+2++n

=== “Python”

```python title="iteration.py"
[class]{}-[func]{while_loop}
```

=== “C++”

```cpp title="iteration.cpp"
[class]{}-[func]{whileLoop}
```

=== “Java”

```java title="iteration.java"
[class]{iteration}-[func]{whileLoop}
```

=== “C#”

```csharp title="iteration.cs"
[class]{iteration}-[func]{whileLoop}
```

=== “Go”

```go title="iteration.go"
[class]{}-[func]{whileLoop}
```

=== “Swift”

```swift title="iteration.swift"
[class]{}-[func]{whileLoop}
```

=== “JS”

```javascript title="iteration.js"
[class]{}-[func]{whileLoop}
```

=== “TS”

```typescript title="iteration.ts"
[class]{}-[func]{whileLoop}
```

=== “Dart”

```dart title="iteration.dart"
[class]{}-[func]{whileLoop}
```

=== “Rust”

```rust title="iteration.rs"
[class]{}-[func]{while_loop}
```

=== “C”

```c title="iteration.c"
[class]{}-[func]{whileLoop}
```

=== “Zig”

```zig title="iteration.zig"
[class]{}-[func]{whileLoop}
```

In whilea loop, since the steps of initializing and updating condition variables are independent of the loop structure, it has fora higher degree of freedom than a loop .

For example in the following code, the condition variable iii is updated twice in each round. This situation is not convenient to usefora loop to implement.

=== “Python”

```python title="iteration.py"
[class]{}-[func]{while_loop_ii}
```

=== “C++”

```cpp title="iteration.cpp"
[class]{}-[func]{whileLoopII}
```

=== “Java”

```java title="iteration.java"
[class]{iteration}-[func]{whileLoopII}
```

=== “C#”

```csharp title="iteration.cs"
[class]{iteration}-[func]{whileLoopII}
```

=== “Go”

```go title="iteration.go"
[class]{}-[func]{whileLoopII}
```

=== “Swift”

```swift title="iteration.swift"
[class]{}-[func]{whileLoopII}
```

=== “JS”

```javascript title="iteration.js"
[class]{}-[func]{whileLoopII}
```

=== “TS”

```typescript title="iteration.ts"
[class]{}-[func]{whileLoopII}
```

=== “Dart”

```dart title="iteration.dart"
[class]{}-[func]{whileLoopII}
```

=== “Rust”

```rust title="iteration.rs"
[class]{}-[func]{while_loop_ii}
```

=== “C”

```c title="iteration.c"
[class]{}-[func]{whileLoopII}
```

=== “Zig”

```zig title="iteration.zig"
[class]{}-[func]{whileLoopII}
```

In general, forthe code for loops is more compact, whileloops are more flexible , and both can implement iterative structures. The choice of which one to use should be based on the needs of the specific problem.

Nested loops

We can nest another loop structure within a loop structure, taking forloop as an example:

=== “Python”

```python title="iteration.py"
[class]{}-[func]{nested_for_loop}
```

=== “C++”

```cpp title="iteration.cpp"
[class]{}-[func]{nestedForLoop}
```

=== “Java”

```java title="iteration.java"
[class]{iteration}-[func]{nestedForLoop}
```

=== “C#”

```csharp title="iteration.cs"
[class]{iteration}-[func]{nestedForLoop}
```

=== “Go”

```go title="iteration.go"
[class]{}-[func]{nestedForLoop}
```

=== “Swift”

```swift title="iteration.swift"
[class]{}-[func]{nestedForLoop}
```

=== “JS”

```javascript title="iteration.js"
[class]{}-[func]{nestedForLoop}
```

=== “TS”

```typescript title="iteration.ts"
[class]{}-[func]{nestedForLoop}
```

=== “Dart”

```dart title="iteration.dart"
[class]{}-[func]{nestedForLoop}
```

=== “Rust”

```rust title="iteration.rs"
[class]{}-[func]{nested_for_loop}
```

=== “C”

```c title="iteration.c"
[class]{}-[func]{nestedForLoop}
```

=== “Zig”

```zig title="iteration.zig"
[class]{}-[func]{nestedForLoop}
```

The figure below shows the flow diagram of this nested loop.

Insert image description here

In this case, the function has as many operations as n 2 n^2n2 is proportional to nn, or the algorithm running time is proportional to the input data sizennn becomes a "square relationship".

We can continue to add nested loops. Each nesting is a "dimension increase", which will increase the time complexity to "cubic relationship", "fourth power relationship", and so on.

recursion

"Recursion" is an algorithmic strategy that solves problems by calling a function itself. It mainly consists of two stages.

  1. Pass : The program calls itself deeper and deeper, usually passing in smaller or simplified parameters, until a "termination condition" is reached.
  2. Return : After triggering the "termination condition", the program returns layer by layer starting from the deepest recursive function, and aggregates the results of each layer.

From an implementation perspective, recursive code mainly contains three elements.

  1. Termination condition : used to determine when to switch from "delivery" to "return".
  2. Recursive call : Corresponding to "recursive", the function calls itself, usually inputting smaller or simplified parameters.
  3. Return result : Corresponds to "return", returning the results of the current recursive level to the previous level.

Observe the following code, we only need to call the function recur(n)to complete 1 + 2 + ⋯ + n 1 + 2 + \dots + n1+2++Calculation of n :

=== “Python”

```python title="recursion.py"
[class]{}-[func]{recur}
```

=== “C++”

```cpp title="recursion.cpp"
[class]{}-[func]{recur}
```

=== “Java”

```java title="recursion.java"
[class]{recursion}-[func]{recur}
```

=== “C#”

```csharp title="recursion.cs"
[class]{recursion}-[func]{recur}
```

=== “Go”

```go title="recursion.go"
[class]{}-[func]{recur}
```

=== “Swift”

```swift title="recursion.swift"
[class]{}-[func]{recur}
```

=== “JS”

```javascript title="recursion.js"
[class]{}-[func]{recur}
```

=== “TS”

```typescript title="recursion.ts"
[class]{}-[func]{recur}
```

=== “Dart”

```dart title="recursion.dart"
[class]{}-[func]{recur}
```

=== “Rust”

```rust title="recursion.rs"
[class]{}-[func]{recur}
```

=== “C”

```c title="recursion.c"
[class]{}-[func]{recur}
```

=== “Zig”

```zig title="recursion.zig"
[class]{}-[func]{recur}
```

The figure below shows the recursive process of this function.

Insert image description here

Although iteration and recursion can achieve the same results from a computational perspective, they represent two completely different paradigms for thinking and solving problems .

  • Iteration : Solving problems “from the bottom up.” Start with the most basic steps and then repeat or add up to them until the task is complete.
  • Recursion : Solving problems “from the top down”. Decompose the original problem into smaller sub-problems that have the same form as the original problem. Next, the sub-problem is continued to be decomposed into smaller sub-problems until it stops at the base case (the solution to the base case is known).

Taking the above summation function as an example, suppose the problem f ( n ) = 1 + 2 + ⋯ + nf(n) = 1 + 2 + \dots + nf(n)=1+2++n

  • Iteration : Simulate the summation process in a loop, starting from 1 11 traverses tonnn , perform the summation operation in each round to obtainf (n) f(n)f(n)
  • Recursion : decompose the problem into subproblems f ( n ) = n + f ( n − 1 ) f(n) = n + f(n-1)f(n)=n+f(n1 ) , continue to decompose (recursively) until the basic casef (0) = 0 f(0) = 0f(0)=Terminates at 0 o'clock.

call stack

Each time a recursive function calls itself, the system allocates memory for the newly opened function to store local variables, calling addresses and other information. This will have two consequences.

  • The context data of the function is stored in a memory area called the "stack frame space" and will not be released until the function returns. Therefore, recursion usually consumes more memory space than iteration .
  • Calling functions recursively incurs additional overhead. Therefore recursion is usually less time efficient than looping .

As shown in the figure below, before triggering the termination condition, there is also nnn non-returning recursive functionswith a recursion depth of nnn

Insert image description here

In practice, the recursion depth allowed by a programming language is usually limited, and excessively deep recursion may result in a stack overflow error.

tail recursion

Interestingly, if a function makes a recursive call as the last step before returning , the function can be optimized by the compiler or interpreter to be as space-efficient as iteration. This situation is called "tail recursion".

  • Ordinary recursion : When the function returns to the function at the upper level, it needs to continue executing the code, so the system needs to save the context of the previous level call.
  • Tail recursion : Recursive call is the last operation before the function returns, which means that after the function returns to the previous level, there is no need to continue to perform other operations, so the system does not need to save the context of the previous level function.

To calculate 1 + 2 + ⋯ + n 1 + 2 + \dots + n1+2++n as an example, we can set the result variableresas a function parameter to achieve tail recursion.

=== “Python”

```python title="recursion.py"
[class]{}-[func]{tail_recur}
```

=== “C++”

```cpp title="recursion.cpp"
[class]{}-[func]{tailRecur}
```

=== “Java”

```java title="recursion.java"
[class]{recursion}-[func]{tailRecur}
```

=== “C#”

```csharp title="recursion.cs"
[class]{recursion}-[func]{tailRecur}
```

=== “Go”

```go title="recursion.go"
[class]{}-[func]{tailRecur}
```

=== “Swift”

```swift title="recursion.swift"
[class]{}-[func]{tailRecur}
```

=== “JS”

```javascript title="recursion.js"
[class]{}-[func]{tailRecur}
```

=== “TS”

```typescript title="recursion.ts"
[class]{}-[func]{tailRecur}
```

=== “Dart”

```dart title="recursion.dart"
[class]{}-[func]{tailRecur}
```

=== “Rust”

```rust title="recursion.rs"
[class]{}-[func]{tail_recur}
```

=== “C”

```c title="recursion.c"
[class]{}-[func]{tailRecur}
```

=== “Zig”

```zig title="recursion.zig"
[class]{}-[func]{tailRecur}
```

The execution process of tail recursion is shown in the figure below. Comparing ordinary recursion and tail recursion, the execution points of the sum operation are different.

  • Ordinary recursion : The summation operation is performed during the "return" process, and the summation operation must be performed again after each layer returns.
  • Tail recursion : The summation operation is performed in the "recursive" process, and the "return" process only needs to return layer by layer.

Insert image description here

!!! tip

请注意,许多编译器或解释器并不支持尾递归优化。例如,Python 默认不支持尾递归优化,因此即使函数是尾递归形式,但仍然可能会遇到栈溢出问题。

recursive tree

When dealing with algorithmic problems related to "divide and conquer", recursion is often more intuitive and the code is more readable than iteration. Take the "Fibonacci Sequence" as an example.

!!! question

给定一个斐波那契数列 $0, 1, 1, 2, 3, 5, 8, 13, \dots$ ,求该数列的第 $n$ 个数字。

Let the nnth Fibonacci sequence ben numbers aref ( n ) f(n)f ( n ) , it is easy to draw two conclusions.

  • The first two numbers of the sequence are f ( 1 ) = 0 f(1) = 0f(1)=0 andf(2) = 1 f(2) = 1f(2)=1
  • Each number in the sequence is the sum of the previous two numbers, i.e. f ( n ) = f ( n − 1 ) + f ( n − 2 ) f(n) = f(n - 1) + f(n - 2 )f(n)=f(n1)+f(n2)

Make recursive calls according to the recursion relationship, and use the first two numbers as the termination condition to write recursive code. Call to get the nnthfib(n) Fibonacci sequencen numbers.

=== “Python”

```python title="recursion.py"
[class]{}-[func]{fib}
```

=== “C++”

```cpp title="recursion.cpp"
[class]{}-[func]{fib}
```

=== “Java”

```java title="recursion.java"
[class]{recursion}-[func]{fib}
```

=== “C#”

```csharp title="recursion.cs"
[class]{recursion}-[func]{fib}
```

=== “Go”

```go title="recursion.go"
[class]{}-[func]{fib}
```

=== “Swift”

```swift title="recursion.swift"
[class]{}-[func]{fib}
```

=== “JS”

```javascript title="recursion.js"
[class]{}-[func]{fib}
```

=== “TS”

```typescript title="recursion.ts"
[class]{}-[func]{fib}
```

=== “Dart”

```dart title="recursion.dart"
[class]{}-[func]{fib}
```

=== “Rust”

```rust title="recursion.rs"
[class]{}-[func]{fib}
```

=== “C”

```c title="recursion.c"
[class]{}-[func]{fib}
```

=== “Zig”

```zig title="recursion.zig"
[class]{}-[func]{fib}
```

Observing the above code, we are calling two functions recursively within the function, which means that two call branches are generated from one call . As shown in the figure below, if we continue to call recursively in this way, we will eventually produce a layer with a layer number of nn."Recursion tree" of n .

Insert image description here

In essence, recursion embodies the thinking paradigm of "decomposing the problem into smaller sub-problems". This divide-and-conquer strategy is crucial.

  • From an algorithmic perspective, many important algorithmic strategies such as search, sorting, backtracking, divide and conquer, and dynamic programming directly or indirectly apply this way of thinking.
  • From a data structure perspective, recursion is naturally suitable for dealing with problems related to linked lists, trees and graphs, because they are very suitable for analysis using divide and conquer thinking.

Compare the two

To summarize the above, as shown in the table below, iteration and recursion differ in implementation, performance, and applicability.

Table Comparison of characteristics of iteration and recursion

Iterate recursion
Method to realize Loop structure function calls itself
time efficiency Usually more efficient, no function call overhead Each function call incurs overhead
memory usage Usually uses a fixed size memory space Cumulative function calls may use a large amount of stack frame space
Applicable issues Suitable for simple loop tasks, the code is intuitive and readable Suitable for sub-problem decomposition, such as trees, graphs, divide and conquer, backtracking, etc., the code structure is concise and clear

!!! tip

如果感觉以下内容理解困难,可以在读完“栈”章节后再来复习。

So, what is the intrinsic connection between iteration and recursion? Taking the above recursive function as an example, the summation operation is performed in the "return" phase of the recursion. This means that the function called initially is actually the last to complete its sum operation. This working mechanism is similar to the "first in, last out" principle of the stack .

In fact, recursive terms such as "call stack" and "stack frame space" already imply the close relationship between recursion and the stack.

  1. Pass : When a function is called, the system will allocate a new stack frame for the function on the "call stack" to store the function's local variables, parameters, return address and other data.
  2. Return : When the function completes execution and returns, the corresponding stack frame will be removed from the "call stack" and the execution environment of the previous function will be restored.

Therefore, we can use an explicit stack to simulate the behavior of a call stack , thus converting recursion into iterative form:

=== “Python”

```python title="recursion.py"
[class]{}-[func]{for_loop_recur}
```

=== “C++”

```cpp title="recursion.cpp"
[class]{}-[func]{forLoopRecur}
```

=== “Java”

```java title="recursion.java"
[class]{recursion}-[func]{forLoopRecur}
```

=== “C#”

```csharp title="recursion.cs"
[class]{recursion}-[func]{forLoopRecur}
```

=== “Go”

```go title="recursion.go"
[class]{}-[func]{forLoopRecur}
```

=== “Swift”

```swift title="recursion.swift"
[class]{}-[func]{forLoopRecur}
```

=== “JS”

```javascript title="recursion.js"
[class]{}-[func]{forLoopRecur}
```

=== “TS”

```typescript title="recursion.ts"
[class]{}-[func]{forLoopRecur}
```

=== “Dart”

```dart title="recursion.dart"
[class]{}-[func]{forLoopRecur}
```

=== “Rust”

```rust title="recursion.rs"
[class]{}-[func]{for_loop_recur}
```

=== “C”

```c title="recursion.c"
[class]{}-[func]{forLoopRecur}
```

=== “Zig”

```zig title="recursion.zig"
[class]{}-[func]{forLoopRecur}
```

Observe the above code, when the recursion is converted into iteration, the code becomes more complex. Although iteration and recursion can be converted into each other in many cases, it is not always worth doing so for two reasons.

  • 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 summary, the choice between iteration and recursion depends on the nature of the particular problem . In programming practice, it is crucial to weigh the pros and cons of both and choose the appropriate method based on the situation.

Guess you like

Origin blog.csdn.net/zy_dreamer/article/details/132865558