Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug fixes and improvements #1380

Merged
merged 13 commits into from
May 31, 2024
4 changes: 2 additions & 2 deletions codes/c/chapter_stack_and_queue/array_stack.c
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,11 @@ int main() {

/* 获取栈的长度 */
int size = stack->size;
printf("栈的长度 size = %d\n", size);
printf("栈的长度 size = %d\n", size);

/* 判断是否为空 */
bool empty = isEmpty(stack);
printf("栈是否为空 = %stack\n", empty ? "true" : "false");
printf("栈是否为空 = %s\n", empty ? "true" : "false");

// 释放内存
delArrayStack(stack);
Expand Down
2 changes: 1 addition & 1 deletion codes/kotlin/chapter_sorting/merge_sort.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ fun merge(nums: IntArray, left: Int, mid: Int, right: Int) {
while (i <= mid && j <= right) {
if (nums[i] <= nums[j])
tmp[k++] = nums[i++]
else
else
tmp[k++] = nums[j++]
}
// 将左子数组和右子数组的剩余元素复制到临时数组中
Expand Down
26 changes: 14 additions & 12 deletions codes/python/chapter_computational_complexity/time_complexity.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ def linear_log_recur(n: int) -> int:
"""线性对数阶"""
if n <= 1:
return 1
count: int = linear_log_recur(n // 2) + linear_log_recur(n // 2)
# 一分为二,子问题的规模减小一半
count = linear_log_recur(n // 2) + linear_log_recur(n // 2)
# 当前子问题包含 n 个操作
for _ in range(n):
count += 1
return count
Expand All @@ -120,32 +122,32 @@ def factorial_recur(n: int) -> int:
n = 8
print("输入数据大小 n =", n)

count: int = constant(n)
count = constant(n)
print("常数阶的操作数量 =", count)

count: int = linear(n)
count = linear(n)
print("线性阶的操作数量 =", count)
count: int = array_traversal([0] * n)
count = array_traversal([0] * n)
print("线性阶(遍历数组)的操作数量 =", count)

count: int = quadratic(n)
count = quadratic(n)
print("平方阶的操作数量 =", count)
nums = [i for i in range(n, 0, -1)] # [n, n-1, ..., 2, 1]
count: int = bubble_sort(nums)
count = bubble_sort(nums)
print("平方阶(冒泡排序)的操作数量 =", count)

count: int = exponential(n)
count = exponential(n)
print("指数阶(循环实现)的操作数量 =", count)
count: int = exp_recur(n)
count = exp_recur(n)
print("指数阶(递归实现)的操作数量 =", count)

count: int = logarithmic(n)
count = logarithmic(n)
print("对数阶(循环实现)的操作数量 =", count)
count: int = log_recur(n)
count = log_recur(n)
print("对数阶(递归实现)的操作数量 =", count)

count: int = linear_log_recur(n)
count = linear_log_recur(n)
print("线性对数阶(递归实现)的操作数量 =", count)

count: int = factorial_recur(n)
count = factorial_recur(n)
print("阶乘阶(递归实现)的操作数量 =", count)
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
=end

### 带约束爬楼梯:动态规划 ###
def climbing_stairs_backtrack(n)
def climbing_stairs_constraint_dp(n)
return 1 if n == 1 || n == 2

# 初始化 dp 表,用于存储子问题的解
Expand All @@ -26,6 +26,6 @@ def climbing_stairs_backtrack(n)
if __FILE__ == $0
n = 9

res = climbing_stairs_backtrack(n)
res = climbing_stairs_constraint_dp(n)
puts "爬 #{n} 阶楼梯共有 #{res} 种方案"
end
Binary file added docs/assets/avatar/avatar_khoaxuantu.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/chapter_array_and_linkedlist/summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,4 @@

**Q**:初始化列表 `res = [0] * self.size()` 操作,会导致 `res` 的每个元素引用相同的地址吗?

不会。但二维数组会有这个问题,例如初始化二维列表 `res = [[0] * self.size()]` ,则多次引用了同一个列表 `[0]` 。
不会。但二维数组会有这个问题,例如初始化二维列表 `res = [[0]] * self.size()` ,则多次引用了同一个列表 `[0]` 。
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

也就是说,在能够解决问题的前提下,算法效率已成为衡量算法优劣的主要评价指标,它包括以下两个维度。

- **时间效率**:算法运行速度的快慢
- **时间效率**:算法运行时间的长短
- **空间效率**:算法占用内存空间的大小。

简而言之,**我们的目标是设计“既快又省”的数据结构与算法**。而有效地评估算法效率至关重要,因为只有这样,我们才能将各种算法进行对比,进而指导算法设计与优化过程。
Expand All @@ -18,7 +18,7 @@

假设我们现在有算法 `A` 和算法 `B` ,它们都能解决同一问题,现在需要对比这两个算法的效率。最直接的方法是找一台计算机,运行这两个算法,并监控记录它们的运行时间和内存占用情况。这种评估方式能够反映真实情况,但也存在较大的局限性。

一方面,**难以排除测试环境的干扰因素**。硬件配置会影响算法的性能。比如在某台计算机中,算法 `A` 的运行时间比算法 `B` 短;但在另一台配置不同的计算机中,可能得到相反的测试结果。这意味着我们需要在各种机器上进行测试,统计平均效率,而这是不现实的。
一方面,**难以排除测试环境的干扰因素**。硬件配置会影响算法的性能表现。比如一个算法的并行度较高,那么它就更适合在多核 CPU 上运行,一个算法的内存操作密集,那么它在高性能内存上的表现就会更好。也就是说,算法在不同的机器上的测试结果可能是不一致的。这意味着我们需要在各种机器上进行测试,统计平均效率,而这是不现实的。

另一方面,**展开完整测试非常耗费资源**。随着输入数据量的变化,算法会表现出不同的效率。例如,在输入数据量较小时,算法 `A` 的运行时间比算法 `B` 短;而在输入数据量较大时,测试结果可能恰恰相反。因此,为了得到有说服力的结论,我们需要测试各种规模的输入数据,而这需要耗费大量的计算资源。

Expand All @@ -32,8 +32,9 @@
- “随着输入数据大小的增加”意味着复杂度反映了算法运行效率与输入数据体量之间的关系。
- “时间和空间的增长趋势”表示复杂度分析关注的不是运行时间或占用空间的具体值,而是时间或空间增长的“快慢”。

**复杂度分析克服了实际测试方法的弊端**,体现在以下两个方面
**复杂度分析克服了实际测试方法的弊端**,体现在以下几个方面

- 它无需实际运行代码,更加绿色节能。
- 它独立于测试环境,分析结果适用于所有运行平台。
- 它可以体现不同数据量下的算法效率,尤其是在大数据量下的算法性能。

Expand Down
2 changes: 1 addition & 1 deletion docs/chapter_computational_complexity/time_complexity.md
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,7 @@ $$

- **时间复杂度能够有效评估算法效率**。例如,算法 `B` 的运行时间呈线性增长,在 $n > 1$ 时比算法 `A` 更慢,在 $n > 1000000$ 时比算法 `C` 更慢。事实上,只要输入数据大小 $n$ 足够大,复杂度为“常数阶”的算法一定优于“线性阶”的算法,这正是时间增长趋势的含义。
- **时间复杂度的推算方法更简便**。显然,运行平台和计算操作类型都与算法运行时间的增长趋势无关。因此在时间复杂度分析中,我们可以简单地将所有计算操作的执行时间视为相同的“单位时间”,从而将“计算操作运行时间统计”简化为“计算操作数量统计”,这样一来估算难度就大大降低了。
- **时间复杂度也存在一定的局限性**。例如,尽管算法 `A` 和 `C` 的时间复杂度相同,但实际运行时间差别很大。同样,尽管算法 `B` 的时间复杂度比 `C` 高,但在输入数据大小 $n$ 较小时,算法 `B` 明显优于算法 `C` 。在这些情况下,我们很难仅凭时间复杂度判断算法效率的高低。当然,尽管存在上述问题,复杂度分析仍然是评判算法效率最有效且常用的方法。
- **时间复杂度也存在一定的局限性**。例如,尽管算法 `A` 和 `C` 的时间复杂度相同,但实际运行时间差别很大。同样,尽管算法 `B` 的时间复杂度比 `C` 高,但在输入数据大小 $n$ 较小时,算法 `B` 明显优于算法 `C` 。对于此类情况,我们时常难以仅凭时间复杂度判断算法效率的高低。当然,尽管存在上述问题,复杂度分析仍然是评判算法效率最有效且常用的方法。

## 函数渐近上界

Expand Down
4 changes: 2 additions & 2 deletions docs/chapter_data_structure/summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@

**Q**:原码转补码的方法是“先取反后加 1”,那么补码转原码应该是逆运算“先减 1 后取反”,而补码转原码也一样可以通过“先取反后加 1”得到,这是为什么呢?

**A**:这是因为原码和补码的相互转换实际上是计算“补数”的过程。我们先给出补数的定义:假设 $a + b = c$ ,那么我们称 $a$ 是 $b$ 到 $c$ 的补数,反之也称 $b$ 是 $a$ 到 $c$ 的补数。
这是因为原码和补码的相互转换实际上是计算“补数”的过程。我们先给出补数的定义:假设 $a + b = c$ ,那么我们称 $a$ 是 $b$ 到 $c$ 的补数,反之也称 $b$ 是 $a$ 到 $c$ 的补数。

给定一个 $n = 4$ 位长度的二进制数 $0010$ ,如果将这个数字看作原码(不考虑符号位),那么它的补码需通过“先取反后加 1”得到:

Expand Down Expand Up @@ -63,4 +63,4 @@ $$

本质上看,“取反”操作实际上是求到 $1111$ 的补数(因为恒有 `原码 + 反码 = 1111`);而在反码基础上再加 1 得到的补码,就是到 $10000$ 的补数。

上述 $n = 4$ 为例,其可推广至任意位数的二进制数
上述以 $n = 4$ 为例,其可被推广至任意位数的二进制数
2 changes: 1 addition & 1 deletion docs/chapter_introduction/what_is_dsa.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

## 数据结构定义

<u>数据结构(data structure)</u>是计算机中组织和存储数据的方式,具有以下设计目标
<u>数据结构(data structure)</u>是组织和存储数据的方式,涵盖数据内容、数据之间关系和数据操作方法,它具有以下设计目标

- 空间占用尽量少,以节省计算机内存。
- 数据操作尽可能快速,涵盖数据访问、添加、删除、更新等。
Expand Down
7 changes: 7 additions & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,13 @@ <h3>代码审阅者</h3>
<br><sub>JS, TS</sub>
</a>
</div>
<div class="profile-cell">
<a href="https://github.com/khoaxuantu">
<img class="profile-img" src="assets/avatar/avatar_khoaxuantu.jpg" alt="Reviewer: khoaxuantu" />
<br><b>khoaxuantu</b>
<br><sub>Ruby</sub>
</a>
</div>
<div class="profile-cell">
<a href="https://github.com/krahets">
<img class="profile-img" src="assets/avatar/avatar_krahets.jpg" alt="Reviewer: krahets" />
Expand Down
2 changes: 1 addition & 1 deletion en/docs/chapter_array_and_linkedlist/summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ On the other hand, linked lists are primarily necessary for binary trees and gra

**Q**: Does initializing a list `res = [0] * self.size()` result in each element of `res` referencing the same address?

No. However, this issue arises with two-dimensional arrays, for example, initializing a two-dimensional list `res = [[0] * self.size()]` would reference the same list `[0]` multiple times.
No. However, this issue arises with two-dimensional arrays, for example, initializing a two-dimensional list `res = [[0]] * self.size()` would reference the same list `[0]` multiple times.

**Q**: In deleting a node, is it necessary to break the reference to its successor node?

Expand Down
8 changes: 4 additions & 4 deletions en/docs/chapter_backtracking/backtracking_algorithm.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@

<u>Backtracking algorithm</u> is a method to solve problems by exhaustive search, where the core idea is to start from an initial state and brute force all possible solutions, recording the correct ones until a solution is found or all possible choices are exhausted without finding a solution.

Backtracking typically employs "depth-first search" to traverse the solution space. In the "Binary Tree" chapter, we mentioned that preorder, inorder, and postorder traversals are all depth-first searches. Next, we use preorder traversal to construct a backtracking problem to gradually understand the workings of the backtracking algorithm.
Backtracking typically employs "depth-first search" to traverse the solution space. In the "Binary Tree" chapter, we mentioned that pre-order, in-order, and post-order traversals are all depth-first searches. Next, we use pre-order traversal to construct a backtracking problem to gradually understand the workings of the backtracking algorithm.

!!! question "Example One"

Given a binary tree, search and record all nodes with a value of $7$, please return a list of nodes.

For this problem, we traverse this tree in preorder and check if the current node's value is $7$. If it is, we add the node's value to the result list `res`. The relevant process is shown in the figure below:
For this problem, we traverse this tree in pre-order and check if the current node's value is $7$. If it is, we add the node's value to the result list `res`. The relevant process is shown in the figure below:

```src
[file]{preorder_traversal_i_compact}-[class]{}-[func]{pre_order}
```

![Searching nodes in preorder traversal](backtracking_algorithm.assets/preorder_find_nodes.png)
![Searching nodes in pre-order traversal](backtracking_algorithm.assets/preorder_find_nodes.png)

## Trying and retreating

Expand Down Expand Up @@ -425,7 +425,7 @@ As per the requirements, after finding a node with a value of $7$, the search sh

![Comparison of retaining and removing the return in the search process](backtracking_algorithm.assets/backtrack_remove_return_or_not.png)

Compared to the implementation based on preorder traversal, the code implementation based on the backtracking algorithm framework seems verbose, but it has better universality. In fact, **many backtracking problems can be solved within this framework**. We just need to define `state` and `choices` according to the specific problem and implement the methods in the framework.
Compared to the implementation based on pre-order traversal, the code implementation based on the backtracking algorithm framework seems verbose, but it has better universality. In fact, **many backtracking problems can be solved within this framework**. We just need to define `state` and `choices` according to the specific problem and implement the methods in the framework.

## Common terminology

Expand Down
16 changes: 8 additions & 8 deletions en/docs/chapter_divide_and_conquer/build_binary_tree_problem.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

!!! question

Given the preorder traversal `preorder` and inorder traversal `inorder` of a binary tree, construct the binary tree and return the root node of the binary tree. Assume that there are no duplicate values in the nodes of the binary tree (as shown in the figure below).
Given the pre-order traversal `preorder` and in-order traversal `inorder` of a binary tree, construct the binary tree and return the root node of the binary tree. Assume that there are no duplicate values in the nodes of the binary tree (as shown in the figure below).

![Example data for building a binary tree](build_binary_tree_problem.assets/build_tree_example.png)

Expand All @@ -11,25 +11,25 @@
The original problem of constructing a binary tree from `preorder` and `inorder` is a typical divide and conquer problem.

- **The problem can be decomposed**: From the perspective of divide and conquer, we can divide the original problem into two subproblems: building the left subtree and building the right subtree, plus one operation: initializing the root node. For each subtree (subproblem), we can still use the above division method, dividing it into smaller subtrees (subproblems), until the smallest subproblem (empty subtree) is reached.
- **The subproblems are independent**: The left and right subtrees are independent of each other, with no overlap. When building the left subtree, we only need to focus on the parts of the inorder and preorder traversals that correspond to the left subtree. The same applies to the right subtree.
- **The subproblems are independent**: The left and right subtrees are independent of each other, with no overlap. When building the left subtree, we only need to focus on the parts of the in-order and pre-order traversals that correspond to the left subtree. The same applies to the right subtree.
- **Solutions to subproblems can be combined**: Once the solutions for the left and right subtrees (solutions to subproblems) are obtained, we can link them to the root node to obtain the solution to the original problem.

### How to divide the subtrees

Based on the above analysis, this problem can be solved using divide and conquer, **but how do we use the preorder traversal `preorder` and inorder traversal `inorder` to divide the left and right subtrees?**
Based on the above analysis, this problem can be solved using divide and conquer, **but how do we use the pre-order traversal `preorder` and in-order traversal `inorder` to divide the left and right subtrees?**

By definition, `preorder` and `inorder` can be divided into three parts.

- Preorder traversal: `[ Root | Left Subtree | Right Subtree ]`, for example, the tree in the figure corresponds to `[ 3 | 9 | 2 1 7 ]`.
- Inorder traversal: `[ Left Subtree | Root | Right Subtree ]`, for example, the tree in the figure corresponds to `[ 9 | 3 | 1 2 7 ]`.
- Pre-order traversal: `[ Root | Left Subtree | Right Subtree ]`, for example, the tree in the figure corresponds to `[ 3 | 9 | 2 1 7 ]`.
- In-order traversal: `[ Left Subtree | Root | Right Subtree ]`, for example, the tree in the figure corresponds to `[ 9 | 3 | 1 2 7 ]`.

Using the data in the figure above, we can obtain the division results as shown in the figure below.

1. The first element 3 in the preorder traversal is the value of the root node.
1. The first element 3 in the pre-order traversal is the value of the root node.
2. Find the index of the root node 3 in `inorder`, and use this index to divide `inorder` into `[ 9 | 3 | 1 2 7 ]`.
3. Based on the division results of `inorder`, it is easy to determine the number of nodes in the left and right subtrees as 1 and 3, respectively, thus dividing `preorder` into `[ 3 | 9 | 2 1 7 ]`.

![Dividing the subtrees in preorder and inorder traversals](build_binary_tree_problem.assets/build_tree_preorder_inorder_division.png)
![Dividing the subtrees in pre-order and in-order traversals](build_binary_tree_problem.assets/build_tree_preorder_inorder_division.png)

### Describing subtree intervals based on variables

Expand All @@ -41,7 +41,7 @@ Based on the above division method, **we have now obtained the index intervals o

As shown in the table below, the above variables can represent the index of the root node in `preorder` as well as the index intervals of the subtrees in `inorder`.

<p align="center"> Table <id> &nbsp; Indexes of the root node and subtrees in preorder and inorder traversals </p>
<p align="center"> Table <id> &nbsp; Indexes of the root node and subtrees in pre-order and in-order traversals </p>

| | Root node index in `preorder` | Subtree index interval in `inorder` |
| ------------- | ----------------------------- | ----------------------------------- |
Expand Down
2 changes: 1 addition & 1 deletion en/docs/chapter_divide_and_conquer/summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
- Divide and conquer can solve many algorithm problems and is widely used in data structure and algorithm design, where its presence is ubiquitous.
- Compared to brute force search, adaptive search is more efficient. Search algorithms with a time complexity of $O(\log n)$ are usually based on the divide and conquer strategy.
- Binary search is another typical application of the divide and conquer strategy, which does not include the step of merging the solutions of subproblems. We can implement binary search through recursive divide and conquer.
- In the problem of constructing binary trees, building the tree (original problem) can be divided into building the left and right subtree (subproblems), which can be achieved by partitioning the index intervals of the preorder and inorder traversals.
- In the problem of constructing binary trees, building the tree (original problem) can be divided into building the left and right subtree (subproblems), which can be achieved by partitioning the index intervals of the pre-order and in-order traversals.
- In the Tower of Hanoi problem, a problem of size $n$ can be divided into two subproblems of size $n-1$ and one subproblem of size $1$. By solving these three subproblems in sequence, the original problem is consequently resolved.
Loading
Loading