diff --git a/codes/cpp/chapter_computational_complexity/recursion.cpp b/codes/cpp/chapter_computational_complexity/recursion.cpp index 36f9214fc..03ab74b52 100644 --- a/codes/cpp/chapter_computational_complexity/recursion.cpp +++ b/codes/cpp/chapter_computational_complexity/recursion.cpp @@ -17,6 +17,26 @@ int recur(int n) { return n + res; } +/* 使用迭代模拟递归 */ +int forLoopRecur(int n) { + // 使用一个显式的栈来模拟系统调用栈 + stack stack; + int res = 0; + // 递:递归调用 + for (int i = n; i > 0; i--) { + // 通过“入栈操作”模拟“递” + stack.push(i); + } + // 归:返回结果 + while (!stack.empty()) { + // 通过“出栈操作”模拟“归” + res += stack.top(); + stack.pop(); + } + // res = 1+2+3+...+n + return res; +} + /* 尾递归 */ int tailRecur(int n, int res) { // 终止条件 @@ -45,6 +65,9 @@ int main() { res = recur(n); cout << "\n递归函数的求和结果 res = " << res << endl; + res = forLoopRecur(n); + cout << "\n使用迭代模拟递归求和结果 res = " << res << endl; + res = tailRecur(n, 0); cout << "\n尾递归函数的求和结果 res = " << res << endl; diff --git a/codes/java/chapter_computational_complexity/iteration.java b/codes/java/chapter_computational_complexity/iteration.java index fd628db14..ae8aaba0a 100644 --- a/codes/java/chapter_computational_complexity/iteration.java +++ b/codes/java/chapter_computational_complexity/iteration.java @@ -8,7 +8,7 @@ package chapter_computational_complexity; public class iteration { /* for 循环 */ - public static int forLoop(int n) { + static int forLoop(int n) { int res = 0; // 循环求和 1, 2, ..., n-1, n for (int i = 1; i <= n; i++) { @@ -18,7 +18,7 @@ public class iteration { } /* while 循环 */ - public static int whileLoop(int n) { + static int whileLoop(int n) { int res = 0; int i = 1; // 初始化条件变量 // 循环求和 1, 2, ..., n-1, n @@ -30,7 +30,7 @@ public class iteration { } /* while 循环(两次更新) */ - public static int whileLoopII(int n) { + static int whileLoopII(int n) { int res = 0; int i = 1; // 初始化条件变量 // 循环求和 1, 4, ... @@ -44,7 +44,7 @@ public class iteration { } /* 双层 for 循环 */ - public static String nestedForLoop(int n) { + static String nestedForLoop(int n) { StringBuilder res = new StringBuilder(); // 循环 i = 1, 2, ..., n-1, n for (int i = 1; i <= n; i++) { diff --git a/codes/java/chapter_computational_complexity/recursion.java b/codes/java/chapter_computational_complexity/recursion.java index 3c5d0b29d..d7ea1f850 100644 --- a/codes/java/chapter_computational_complexity/recursion.java +++ b/codes/java/chapter_computational_complexity/recursion.java @@ -6,9 +6,11 @@ package chapter_computational_complexity; +import java.util.Stack; + public class recursion { /* 递归 */ - public static int recur(int n) { + static int recur(int n) { // 终止条件 if (n == 1) return 1; @@ -18,8 +20,27 @@ public class recursion { return n + res; } + /* 使用迭代模拟递归 */ + static int forLoopRecur(int n) { + // 使用一个显式的栈来模拟系统调用栈 + Stack stack = new Stack<>(); + int res = 0; + // 递:递归调用 + for (int i = n; i > 0; i--) { + // 通过“入栈操作”模拟“递” + stack.push(i); + } + // 归:返回结果 + while (!stack.isEmpty()) { + // 通过“出栈操作”模拟“归” + res += stack.pop(); + } + // res = 1+2+3+...+n + return res; + } + /* 尾递归 */ - public static int tailRecur(int n, int res) { + static int tailRecur(int n, int res) { // 终止条件 if (n == 0) return res; @@ -28,7 +49,7 @@ public class recursion { } /* 斐波那契数列:递归 */ - public static int fib(int n) { + static int fib(int n) { // 终止条件 f(1) = 0, f(2) = 1 if (n == 1 || n == 2) return n - 1; @@ -46,6 +67,9 @@ public class recursion { res = recur(n); System.out.println("\n递归函数的求和结果 res = " + res); + res = forLoopRecur(n); + System.out.println("\n使用迭代模拟递归求和结果 res = " + res); + res = tailRecur(n, 0); System.out.println("\n尾递归函数的求和结果 res = " + res); diff --git a/codes/python/chapter_computational_complexity/recursion.py b/codes/python/chapter_computational_complexity/recursion.py index 435bcaa38..c44064ed4 100644 --- a/codes/python/chapter_computational_complexity/recursion.py +++ b/codes/python/chapter_computational_complexity/recursion.py @@ -16,6 +16,23 @@ def recur(n: int) -> int: return n + res +def for_loop_recur(n: int) -> int: + """使用迭代模拟递归""" + # 使用一个显式的栈来模拟系统调用栈 + stack = [] + res = 0 + # 递:递归调用 + for i in range(n, 0, -1): + # 通过“入栈操作”模拟“递” + stack.append(i) + # 归:返回结果 + while stack: + # 通过“出栈操作”模拟“归” + res += stack.pop() + # res = 1+2+3+...+n + return res + + def tail_recur(n, res): """尾递归""" # 终止条件 @@ -42,6 +59,9 @@ if __name__ == "__main__": res = recur(n) print(f"\n递归函数的求和结果 res = {res}") + res = for_loop_recur(n) + print(f"\n使用迭代模拟递归求和结果 res = {res}") + res = tail_recur(n, 0) print(f"\n尾递归函数的求和结果 res = {res}") diff --git a/docs/chapter_computational_complexity/iteration_and_recursion.assets/tail_recursion_sum.png b/docs/chapter_computational_complexity/iteration_and_recursion.assets/tail_recursion_sum.png index d194be3e9..34a8608f5 100644 Binary files a/docs/chapter_computational_complexity/iteration_and_recursion.assets/tail_recursion_sum.png and b/docs/chapter_computational_complexity/iteration_and_recursion.assets/tail_recursion_sum.png differ diff --git a/docs/chapter_computational_complexity/iteration_and_recursion.md b/docs/chapter_computational_complexity/iteration_and_recursion.md index cdbb8e5ed..7a4e2393a 100644 --- a/docs/chapter_computational_complexity/iteration_and_recursion.md +++ b/docs/chapter_computational_complexity/iteration_and_recursion.md @@ -525,14 +525,16 @@ [class]{}-[func]{tailRecur} ``` -两种递归的过程对比如下图所示。 +尾递归的执行过程如下图所示。对比普通递归和尾递归,求和操作的执行点是不同的。 - **普通递归**:求和操作是在“归”的过程中执行的,每层返回后都要再执行一次求和操作。 - **尾递归**:求和操作是在“递”的过程中执行的,“归”的过程只需层层返回。 ![尾递归过程](iteration_and_recursion.assets/tail_recursion_sum.png) -请注意,许多编译器或解释器并不支持尾递归优化。例如,Python 默认不支持尾递归优化,因此即使函数是尾递归形式,但仍然可能会遇到栈溢出问题。 +!!! tip + + 请注意,许多编译器或解释器并不支持尾递归优化。例如,Python 默认不支持尾递归优化,因此即使函数是尾递归形式,但仍然可能会遇到栈溢出问题。 ### 递归树 @@ -629,3 +631,108 @@ - 从算法角度看,搜索、排序、回溯、分治、动态规划等许多重要算法策略都直接或间接地应用这种思维方式。 - 从数据结构角度看,递归天然适合处理链表、树和图的相关问题,因为它们非常适合用分治思想进行分析。 + +## 两者对比 + +总结以上内容,如下表所示,迭代和递归在实现、性能和适用性上有所不同。 + +

  迭代与递归特点对比

+ +| | 迭代 | 递归 | +| -------- | -------------------------------------- | ------------------------------------------------------------ | +| 实现方式 | 循环结构 | 函数调用自身 | +| 时间效率 | 效率通常较高,无函数调用开销 | 每次函数调用都会产生开销 | +| 内存使用 | 通常使用固定大小的内存空间 | 累积函数调用可能使用大量的栈帧空间 | +| 适用问题 | 适用于简单循环任务,代码直观、可读性好 | 适用于子问题分解,如树、图、分治、回溯等,代码结构简洁、清晰 | + +!!! tip + + 如果感觉以下内容理解困难,可以在读完“栈”章节后再来复习。 + +那么,迭代和递归具有什么内在联系呢?以上述的递归函数为例,求和操作在递归的“归”阶段进行。这意味着最初被调用的函数实际上是最后完成其求和操作的,**这种工作机制与栈的“先入后出”原则是异曲同工的**。 + +事实上,“调用栈”和“栈帧空间”这类递归术语已经暗示了递归与栈之间的密切关系。 + +1. **递**:当函数被调用时,系统会在“调用栈”上为该函数分配新的栈帧,用于存储函数的局部变量、参数、返回地址等数据。 +2. **归**:当函数完成执行并返回时,对应的栈帧会从“调用栈”上被移除,恢复之前函数的执行环境。 + +因此,**我们可以使用一个显式的栈来模拟调用栈的行为**,从而将递归转化为迭代形式: + +=== "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} + ``` + +观察以上代码,当递归被转换为迭代后,代码变得更加复杂了。尽管迭代和递归在很多情况下可以互相转换,但也不一定值得这样做,有以下两点原因。 + +- 转化后的代码可能更加难以理解,可读性更差。 +- 对于某些复杂问题,模拟系统调用栈的行为可能非常困难。 + +总之,**选择迭代还是递归取决于特定问题的性质**。在编程实践中,权衡两者的优劣并根据情境选择合适的方法是至关重要的。 diff --git a/docs/chapter_preface/suggestions.md b/docs/chapter_preface/suggestions.md index 1cae45ec2..8c19b51f8 100644 --- a/docs/chapter_preface/suggestions.md +++ b/docs/chapter_preface/suggestions.md @@ -209,7 +209,7 @@ git clone https://github.com/krahets/hello-algo.git 从总体上看,我们可以将学习数据结构与算法的过程划分为三个阶段。 1. **算法入门**。我们需要熟悉各种数据结构的特点和用法,学习不同算法的原理、流程、用途和效率等方面内容。 -2. **刷算法题**。建议从热门题目开刷,如[剑指 Offer](https://leetcode.cn/problem-list/xb9nqhhg/)和[LeetCode Hot 100](https://leetcode.cn/problem-list/2cktkvj/),先积累至少 100 道题目,熟悉主流的算法问题。初次刷题时,“知识遗忘”可能是一个挑战,但请放心,这是很正常的。我们可以按照“艾宾浩斯遗忘曲线”来复习题目,通常在进行 3-5 轮的重复后,就能将其牢记在心。 +2. **刷算法题**。建议从热门题目开刷,如[剑指 Offer](https://leetcode.cn/studyplan/coding-interviews/)和[LeetCode Hot 100](https://leetcode.cn/studyplan/top-100-liked/),先积累至少 100 道题目,熟悉主流的算法问题。初次刷题时,“知识遗忘”可能是一个挑战,但请放心,这是很正常的。我们可以按照“艾宾浩斯遗忘曲线”来复习题目,通常在进行 3-5 轮的重复后,就能将其牢记在心。 3. **搭建知识体系**。在学习方面,我们可以阅读算法专栏文章、解题框架和算法教材,以不断丰富知识体系。在刷题方面,可以尝试采用进阶刷题策略,如按专题分类、一题多解、一解多题等,相关的刷题心得可以在各个社区找到。 如下图所示,本书内容主要涵盖“第一阶段”,旨在帮助你更高效地展开第二和第三阶段的学习。