From ec3f58fb706859a09cffd8af49bb43af8676d7a5 Mon Sep 17 00:00:00 2001 From: krahets Date: Mon, 9 Oct 2023 05:05:19 +0800 Subject: [PATCH] build --- docs/chapter_backtracking/n_queens_problem.md | 61 ++++++++++++++++++- .../subset_sum_problem.md | 2 +- .../time_complexity.md | 24 +++----- .../dp_problem_features.md | 21 ++++++- .../edit_distance_problem.md | 55 ++++++++++++++++- docs/chapter_hashing/hash_map.md | 4 +- docs/chapter_tree/summary.md | 2 +- 7 files changed, 144 insertions(+), 25 deletions(-) diff --git a/docs/chapter_backtracking/n_queens_problem.md b/docs/chapter_backtracking/n_queens_problem.md index 3114ba4e6..8eacb07ea 100644 --- a/docs/chapter_backtracking/n_queens_problem.md +++ b/docs/chapter_backtracking/n_queens_problem.md @@ -601,9 +601,66 @@ comments: true === "C" ```c title="n_queens.c" - [class]{}-[func]{backtrack} + /* 放置结果 */ + struct result { + char ***data; + int size; + }; - [class]{}-[func]{nQueens} + typedef struct result Result; + + /* 回溯算法:N 皇后 */ + void backtrack(int row, int n, char state[MAX_N][MAX_N], Result *res, + bool cols[MAX_N], bool diags1[2 * MAX_N - 1], bool diags2[2 * MAX_N - 1]) { + // 当放置完所有行时,记录解 + if (row == n) { + res->data[res->size] = (char **)malloc(sizeof(char *) * n); + for (int i = 0; i < n; ++i) { + res->data[res->size][i] = (char *)malloc(sizeof(char) * (n + 1)); + strcpy(res->data[res->size][i], state[i]); + } + res->size++; + return; + } + // 遍历所有列 + for (int col = 0; col < n; col++) { + // 计算该格子对应的主对角线和副对角线 + int diag1 = row - col + n - 1; + int diag2 = row + col; + // 剪枝:不允许该格子所在列、主对角线、副对角线存在皇后 + if (!cols[col] && !diags1[diag1] && !diags2[diag2]) { + // 尝试:将皇后放置在该格子 + state[row][col] = 'Q'; + cols[col] = diags1[diag1] = diags2[diag2] = true; + // 放置下一行 + backtrack(row + 1, n, state, res, cols, diags1, diags2); + // 回退:将该格子恢复为空位 + state[row][col] = '#'; + cols[col] = diags1[diag1] = diags2[diag2] = false; + } + } + } + + /* 求解 N 皇后 */ + Result *nQueens(int n) { + char state[MAX_N][MAX_N]; + // 初始化 n*n 大小的棋盘,其中 'Q' 代表皇后,'#' 代表空位 + for (int i = 0; i < n; ++i) { + for (int j = 0; j < n; ++j) { + state[i][j] = '#'; + } + state[i][n] = '\0'; + } + bool cols[MAX_N] = {false}; // 记录列是否有皇后 + bool diags1[2 * MAX_N - 1] = {false}; // 记录主对角线是否有皇后 + bool diags2[2 * MAX_N - 1] = {false}; // 记录副对角线是否有皇后 + + Result *res = malloc(sizeof(Result)); + res->data = (char ***)malloc(sizeof(char **) * MAX_RES); + res->size = 0; + backtrack(0, n, state, res, cols, diags1, diags2); + return res; + } ``` === "Zig" diff --git a/docs/chapter_backtracking/subset_sum_problem.md b/docs/chapter_backtracking/subset_sum_problem.md index 1de720aea..c5d98ba98 100644 --- a/docs/chapter_backtracking/subset_sum_problem.md +++ b/docs/chapter_backtracking/subset_sum_problem.md @@ -936,7 +936,7 @@ comments: true 为解决此问题,**我们需要限制相等元素在每一轮中只被选择一次**。实现方式比较巧妙:由于数组是已排序的,因此相等元素都是相邻的。这意味着在某轮选择中,若当前元素与其左边元素相等,则说明它已经被选择过,因此直接跳过当前元素。 -与此同时,**本题规定中的每个数组元素只能被选择一次**。幸运的是,我们也可以利用变量 `start` 来满足该约束:当做出选择 $x_{i}$ 后,设定下一轮从索引 $i + 1$ 开始向后遍历。这样即能去除重复子集,也能避免重复选择元素。 +与此同时,**本题规定数组中的每个元素只能被选择一次**。幸运的是,我们也可以利用变量 `start` 来满足该约束:当做出选择 $x_{i}$ 后,设定下一轮从索引 $i + 1$ 开始向后遍历。这样即能去除重复子集,也能避免重复选择元素。 ### 2.   代码实现 diff --git a/docs/chapter_computational_complexity/time_complexity.md b/docs/chapter_computational_complexity/time_complexity.md index c163cf6ed..61edcb1ba 100755 --- a/docs/chapter_computational_complexity/time_complexity.md +++ b/docs/chapter_computational_complexity/time_complexity.md @@ -443,7 +443,7 @@ $$ 相较于直接统计算法运行时间,时间复杂度分析有哪些特点呢? - **时间复杂度能够有效评估算法效率**。例如,算法 `B` 的运行时间呈线性增长,在 $n > 1$ 时比算法 `A` 更慢,在 $n > 1000000$ 时比算法 `C` 更慢。事实上,只要输入数据大小 $n$ 足够大,复杂度为“常数阶”的算法一定优于“线性阶”的算法,这正是时间增长趋势所表达的含义。 -- **时间复杂度的推算方法更简便**。显然,运行平台和计算操作类型都与算法运行时间的增长趋势无关。因此在时间复杂度分析中,我们可以简单地将所有计算操作的执行时间视为相同的“单位时间”,从而将“计算操作的运行时间的统计”简化为“计算操作的数量的统计”,这样以来估算难度就大大降低了。 +- **时间复杂度的推算方法更简便**。显然,运行平台和计算操作类型都与算法运行时间的增长趋势无关。因此在时间复杂度分析中,我们可以简单地将所有计算操作的执行时间视为相同的“单位时间”,从而将“计算操作的运行时间的统计”简化为“计算操作的数量的统计”,这样一来估算难度就大大降低了。 - **时间复杂度也存在一定的局限性**。例如,尽管算法 `A` 和 `C` 的时间复杂度相同,但实际运行时间差别很大。同样,尽管算法 `B` 的时间复杂度比 `C` 高,但在输入数据大小 $n$ 较小时,算法 `B` 明显优于算法 `C` 。在这些情况下,我们很难仅凭时间复杂度判断算法效率的高低。当然,尽管存在上述问题,复杂度分析仍然是评判算法效率最有效且常用的方法。 ## 2.3.2   函数渐近上界 @@ -2597,8 +2597,7 @@ $$ int linearLogRecur(float n) { if (n <= 1) return 1; - int count = linearLogRecur(n / 2) + - linearLogRecur(n / 2); + int count = linearLogRecur(n / 2) + linearLogRecur(n / 2); for (int i = 0; i < n; i++) { count++; } @@ -2612,8 +2611,7 @@ $$ /* 线性对数阶 */ int LinearLogRecur(float n) { if (n <= 1) return 1; - int count = LinearLogRecur(n / 2) + - LinearLogRecur(n / 2); + int count = LinearLogRecur(n / 2) + LinearLogRecur(n / 2); for (int i = 0; i < n; i++) { count++; } @@ -2629,8 +2627,7 @@ $$ if n <= 1 { return 1 } - count := linearLogRecur(n/2) + - linearLogRecur(n/2) + count := linearLogRecur(n/2) + linearLogRecur(n/2) for i := 0.0; i < n; i++ { count++ } @@ -2704,8 +2701,7 @@ $$ if n <= 1.0 { return 1; } - let mut count = linear_log_recur(n / 2.0) + - linear_log_recur(n / 2.0); + let mut count = linear_log_recur(n / 2.0) + linear_log_recur(n / 2.0); for _ in 0 ..n as i32 { count += 1; } @@ -2734,8 +2730,7 @@ $$ // 线性对数阶 fn linearLogRecur(n: f32) i32 { if (n <= 1) return 1; - var count: i32 = linearLogRecur(n / 2) + - linearLogRecur(n / 2); + var count: i32 = linearLogRecur(n / 2) + linearLogRecur(n / 2); var i: f32 = 0; while (i < n) : (i += 1) { count += 1; @@ -3060,11 +3055,8 @@ $$ // 随机打乱数组元素 for (int i = 0; i < nums.Length; i++) { - var index = new Random().Next(i, nums.Length); - var tmp = nums[i]; - var ran = nums[index]; - nums[i] = ran; - nums[index] = tmp; + int index = new Random().Next(i, nums.Length); + (nums[i], nums[index]) = (nums[index], nums[i]); } return nums; } diff --git a/docs/chapter_dynamic_programming/dp_problem_features.md b/docs/chapter_dynamic_programming/dp_problem_features.md index 7a61bed94..ba578f04d 100644 --- a/docs/chapter_dynamic_programming/dp_problem_features.md +++ b/docs/chapter_dynamic_programming/dp_problem_features.md @@ -809,7 +809,26 @@ $$ === "C" ```c title="climbing_stairs_constraint_dp.c" - [class]{}-[func]{climbingStairsConstraintDP} + /* 带约束爬楼梯:动态规划 */ + int climbingStairsConstraintDP(int n) { + if (n == 1 || n == 2) { + return 1; + } + // 初始化 dp 表,用于存储子问题的解 + int dp[n + 1][3]; + memset(dp, 0, sizeof(dp)); + // 初始状态:预设最小子问题的解 + dp[1][1] = 1; + dp[1][2] = 0; + dp[2][1] = 0; + dp[2][2] = 1; + // 状态转移:从较小子问题逐步求解较大子问题 + for (int i = 3; i <= n; i++) { + dp[i][1] = dp[i - 1][2]; + dp[i][2] = dp[i - 2][1] + dp[i - 2][2]; + } + return dp[n][1] + dp[n][2]; + } ``` === "Zig" diff --git a/docs/chapter_dynamic_programming/edit_distance_problem.md b/docs/chapter_dynamic_programming/edit_distance_problem.md index 4e2eac73b..fa3612be3 100644 --- a/docs/chapter_dynamic_programming/edit_distance_problem.md +++ b/docs/chapter_dynamic_programming/edit_distance_problem.md @@ -391,7 +391,31 @@ $$ === "C" ```c title="edit_distance.c" - [class]{}-[func]{editDistanceDP} + /* 编辑距离:动态规划 */ + int editDistanceDP(char *s, char *t, int n, int m) { + int dp[n + 1][m + 1]; + memset(dp, 0, sizeof(dp)); + // 状态转移:首行首列 + for (int i = 1; i <= n; i++) { + dp[i][0] = i; + } + for (int j = 1; j <= m; j++) { + dp[0][j] = j; + } + // 状态转移:其余行列 + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= m; j++) { + if (s[i - 1] == t[j - 1]) { + // 若两字符相等,则直接跳过此两字符 + dp[i][j] = dp[i - 1][j - 1]; + } else { + // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1 + dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1; + } + } + } + return dp[n][m]; + } ``` === "Zig" @@ -812,7 +836,34 @@ $$ === "C" ```c title="edit_distance.c" - [class]{}-[func]{editDistanceDPComp} + /* 编辑距离:空间优化后的动态规划 */ + int editDistanceDPComp(char *s, char *t, int n, int m) { + int dp[m + 1]; + memset(dp, 0, sizeof(dp)); + // 状态转移:首行 + for (int j = 1; j <= m; j++) { + dp[j] = j; + } + // 状态转移:其余行 + for (int i = 1; i <= n; i++) { + // 状态转移:首列 + int leftup = dp[0]; // 暂存 dp[i-1, j-1] + dp[0] = i; + // 状态转移:其余列 + for (int j = 1; j <= m; j++) { + int temp = dp[j]; + if (s[i - 1] == t[j - 1]) { + // 若两字符相等,则直接跳过此两字符 + dp[j] = leftup; + } else { + // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1 + dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1; + } + leftup = temp; // 更新为下一轮的 dp[i-1, j-1] + } + } + return dp[m]; + } ``` === "Zig" diff --git a/docs/chapter_hashing/hash_map.md b/docs/chapter_hashing/hash_map.md index ade653731..6a6be2c48 100755 --- a/docs/chapter_hashing/hash_map.md +++ b/docs/chapter_hashing/hash_map.md @@ -178,7 +178,7 @@ comments: true ```javascript title="hash_map.js" /* 初始化哈希表 */ - const map = new ArrayHashMap(); + const map = new Map(); /* 添加操作 */ // 在哈希表中添加键值对 (key, value) map.set(12836, '小哈'); @@ -349,7 +349,7 @@ comments: true Console.WriteLine(key); } // 单独遍历值 value - foreach (String val in map.Values) { + foreach (string val in map.Values) { Console.WriteLine(val); } ``` diff --git a/docs/chapter_tree/summary.md b/docs/chapter_tree/summary.md index 6d93d651d..7a5f17d7d 100644 --- a/docs/chapter_tree/summary.md +++ b/docs/chapter_tree/summary.md @@ -10,7 +10,7 @@ comments: true - 对于二叉树中的某个节点,其左(右)子节点及其以下形成的树被称为该节点的左(右)子树。 - 二叉树的相关术语包括根节点、叶节点、层、度、边、高度和深度等。 - 二叉树的初始化、节点插入和节点删除操作与链表操作方法类似。 -- 常见的二叉树类型有完美二叉树、完全二叉树、满二叉树和平衡二叉树。完美二叉树是最理想的状态,而链表是退化后的最差状态。 +- 常见的二叉树类型有完美二叉树、完全二叉树、完满二叉树和平衡二叉树。完美二叉树是最理想的状态,而链表是退化后的最差状态。 - 二叉树可以用数组表示,方法是将节点值和空位按层序遍历顺序排列,并根据父节点与子节点之间的索引映射关系来实现指针。 - 二叉树的层序遍历是一种广度优先搜索方法,它体现了“一圈一圈向外”的分层遍历方式,通常通过队列来实现。 - 前序、中序、后序遍历皆属于深度优先搜索,它们体现了“走到尽头,再回头继续”的回溯遍历方式,通常使用递归来实现。