Add translated chapter exercise pages (#1941)

Add dedicated chapter exercise sections and generated exercise pages for the English, Japanese, and Russian editions.

Add navigation for all published languages while keeping Taiwan Traditional Chinese exercise pages generated at build time.
This commit is contained in:
Yudong Jin
2026-07-19 04:25:58 +08:00
committed by GitHub
parent 4935d2d387
commit 9135647d93
62 changed files with 5778 additions and 3 deletions
@@ -0,0 +1,108 @@
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
# 练习
## 知识巩固
### 数组和链表怎样找到元素
数组和单向链表中都按顺序保存了 `[A, B, C, D, E]`。现在要读取第 4 个元素 `D`
<!-- numbered-subquestions -->
1. 数组可以直接使用哪个索引?
2. 单向链表从头节点 `A` 开始,要沿着 `next` 依次经过哪些节点?
3. 当要读取的元素位置越来越靠后时,两种结构所需的步骤会怎样变化?哪一种更适合反复按位置读取?为什么?
??? success "参考答案"
1. 若索引从 0 开始,第 4 个元素的索引是 3,数组可直接读取 `arr[3]`
2. 单向链表必须从头开始,访问路径为 `A → B → C → D`,需要沿 `next` 前进 3 次。
3. 数组可以根据首地址和索引直接定位元素,按位置访问的时间复杂度为 $O(1)$。
单向链表访问第 $k$ 个节点时,必须从头节点开始,沿着 `next` 前进 $k-1$ 次,
最坏需要 $O(n)$ 时间。
这里只比较按位置读取,不表示链表在所有操作上都更慢。
### 数组和链表怎样插入元素
数组和单向链表中都保存了 `A、B、C、D`。现在要在 `B` 后面插入 `X`
- 数组容量为 5,当前状态是 `[A, B, C, D, _]`
- 链表为 `A → B → C → D`,并且已经拿到了指向节点 `B` 的引用。
<!-- numbered-subquestions -->
1. 数组需要移动哪些元素?写出插入后的数组。
2. 链表应按什么顺序修改 `X.next``B.next`?写出插入后的链表。
3. 为什么比较插入效率时,要特别说明“已经拿到了节点 B 的引用”?
??? success "参考答案"
1. 数组要先把 `D` 向右移动一格,再把 `C` 向右移动一格,最后把 `X` 放入索引 2
得到 `[A, B, X, C, D]`
2. `B.next` 原本指向 `C`。应先令 `X.next = B.next`,让 `X` 指向 `C`
再令 `B.next = X`。结果为 `A → B → X → C → D`
如果先覆盖 `B.next` 又没有保存原来的连接,就可能找不到 `C`
3. 已知 `B` 的位置后,链表插入只需修改两个连接,可以在 $O(1)$ 时间完成。
如果还要从头查找 `B`,查找过程本身可能需要 $O(n)$ 时间。
### 列表容量是怎样增长的
一个基于数组实现的列表当前内容为 `[A, B, C]`,长度 `size = 3`,容量 `capacity = 4`
规定容量不足时,新数组的容量扩大为原来的 2 倍。
<!-- numbered-subquestions -->
1. 追加 `D` 后,列表的长度和容量分别是多少?是否需要扩容?
2. 接着追加 `E` 时,容量会变为多少?需要复制几个原有元素?
3. 底层数组的长度不可变,为什么列表的容量看起来却可以增长?
??? success "参考答案"
1. `D` 可以放入最后一个空位。此时内容为 `[A, B, C, D]`
`size = 4``capacity = 4`,不需要扩容。
2. 再追加 `E` 时已经没有空位,需要创建容量为 8 的新数组,
`A、B、C、D` 共 4 个原有元素复制过去,再加入 `E`
此时 `size = 5``capacity = 8`
3. 原来的数组本身没有变长。列表创建了一个更大的新数组,复制原有元素,
再改用新数组作为底层存储,因此对使用者来说容量增长了。
## 编程练习
### 数组表示的大整数加一
数组 `digits` 从左到右保存一个非负整数的各位数字,例如 `[3, 0, 8]` 表示 308。
数字 0 用 `[0]` 表示;其他输入的第一位均不为 0。
请模拟一次十进制竖式加法,将这个整数增加 1,并把结果仍按相同的数组形式返回。
可以直接修改 `digits`;若最前面产生新的进位,可以返回一个更长的数组。
??? tip "解题提示"
1. 像做竖式加法一样,从数组最后一位开始
2. 当前位小于 9 时加一后即可立即返回
3. 当前位等于 9 时把它改成 0;若所有位都是 9,需要在最前面补 1
[LeetCode](https://leetcode.cn/problems/plus-one/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
### 反转单向链表
给定一个单向链表的头节点 `head`。每个节点包含一个值和指向下一节点的 `next`
请使用迭代方法反转所有节点之间的连接,并返回反转后的头节点。
要求不创建新的链表节点。
??? tip "解题提示"
1. 先在纸上画出三个相连的节点和 prev、cur 两个指针
2. 改写 cur.next 前,必须先用 nxt 保存原来的下一个节点
3. 反转 cur.next 后,令 prev = cur,再令 cur = nxt,继续处理原链表中的下一个节点
[LeetCode](https://leetcode.cn/problems/reverse-linked-list/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [题目解析](https://leetcode.cn/problems/reverse-linked-list/solutions/2361282/206-fan-zhuan-lian-biao-shuang-zhi-zhen-r1jel/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
+91
View File
@@ -0,0 +1,91 @@
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
# 练习
## 知识巩固
### 这段排列算法会漏掉结果吗
一个回溯算法按 `1、2、3` 的顺序尝试生成全部排列。每次选择数字 `x` 时,它会:
1.`x` 加到当前路径末尾;
2.`x` 标记为“已使用”;
3. 递归填写下一个位置。
递归返回后,同学只从路径末尾删除了 `x`,然后继续尝试下一个数字。
<!-- numbered-subquestions -->
1. 算法首先得到哪个排列?它还能得到全部 6 个排列吗?
2. 递归返回上一层前,只删除路径末尾的数字是否足够?如果不够,还需要做什么?说明理由。
??? success "参考答案"
1. 它首先得到 `[1, 2, 3]`,但无法得到全部排列。虽然返回时路径变短了,
数字 1、2、3 的标记仍都是“已使用”,后续分支便没有可选数字。
2. 不够。删除路径末尾的 `x` 后,还必须把 `x` 重新标记为“未使用”。
当前路径和已使用标记共同描述搜索状态;选择时修改了两处,回退时也必须把两处都恢复,
其他分支才能再次选择 `x`
### 数字的选择顺序重要吗
给定排好序的数组 `[2, 3, 5]` 和目标值 5,每个数可以重复选择。
算法规定每条搜索路径中的数字只能按从小到大的顺序出现。
<!-- numbered-subquestions -->
1. 能得到哪些不同的组合?
2. 为什么同一组数字不需要按不同顺序重复搜索?“从小到大”的限制起到了什么作用?
3. 当前路径为 `[3]`、还差 2 时,下一个候选数是 3。为什么此时可以停止检查这一层后面的所有候选数?
??? success "参考答案"
1. 不同的组合为 `[2, 3]``[5]`
2. 本题把 `[2, 3]``[3, 2]` 看作同一种组合,数字的选择顺序不计入答案。
规定路径中的数字从小到大出现,就能在搜索时直接跳过 `[3, 2]` 这类重复组合。
3. 当前还差 2,而候选数 3 已经大于 2。因为数组已经排好序,
3 后面的候选数只会更大,也都不可能加入当前组合,所以可以直接结束这一层的检查。
### 下一枚皇后可以放在哪些位置
在一个 `4 × 4` 棋盘上按行放置皇后,行、列下标都从 0 开始。
目前已经在 `(0, 1)``(1, 3)` 放置了皇后,现在要在第 2 行放置下一个皇后。
<!-- numbered-subquestions -->
1. 哪些列会因为“同列”而被排除?
2. 在剩余列中,哪些位置会因为“同一条对角线”而被排除?
3. 第 2 行还剩哪些位置可以尝试?
??? success "参考答案"
1. 第 1 列和第 3 列已经有皇后,因此位置 `(2, 1)``(2, 3)` 被排除。
2. 在剩余位置中,`(2, 2)``(1, 3)` 位于同一条对角线上,因此也被排除。
位置 `(2, 0)` 与已有两个皇后都不在同列或同一条对角线上。
3. 第 2 行可以尝试的位置只有 `(2, 0)`
这一步只说明当前放置合法;之后若无法完成棋盘,仍需回退并尝试更早的其他选择。
## 编程练习
### 无重复元素的全排列
整数数组 `nums` 至少包含一个元素,并且其中各元素互不相同。
请列出把这些元素各使用一次所能形成的全部顺序,并将每一种顺序作为一个数组返回。
结果中各排列的先后次序不作要求。
请使用回溯,并用布尔数组记录每个位置的元素是否已经选入当前排列。
??? tip "解题提示"
1. 递归深度表示正在填写排列中的第几个位置
2. 每一层只尝试尚未使用的元素
3. 路径长度达到 `nums` 的长度时,把它的副本加入答案
[LeetCode](https://leetcode.cn/problems/permutations/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [题目解析](https://leetcode.cn/problems/permutations/solutions/2363882/46-quan-pai-lie-hui-su-qing-xi-tu-jie-by-6o7h/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
> **说明:** 链接题解通过交换数组元素,把已选元素依次放到数组前部;本练习使用布尔数组记录每个元素是否已选。两种方法都能避免同一元素被重复选择,但代码结构不同
@@ -0,0 +1,112 @@
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
# 练习
## 知识巩固
### 迭代与递归的时间和空间
下面两段代码都计算 $1 + 2 + \dots + n$(设 $n \ge 1$)。请把 `n` 设为 4
按照程序实际执行的顺序回答问题,然后比较两种写法的效率。
```python
def sum_iter(n):
s = 0
for i in range(1, n + 1):
s += i
return s
def sum_recur(n):
if n == 1:
return 1
return n + sum_recur(n - 1)
```
<!-- numbered-subquestions -->
1. 执行 `sum_iter(4)` 时,每轮循环结束后,变量 `s` 的值分别是多少?
2. 执行 `sum_recur(4)` 时,会依次调用哪些函数?从最深的一层开始返回时,结果怎样得到?
3. 两种写法的时间复杂度和空间复杂度分别是多少?结合第 1、2 问的执行过程说明理由。
??? success "参考答案"
1. 循环变量 `i` 依次为 `1、2、3、4`,每轮结束后,`s` 依次变为
`1、3、6、10`,所以 `sum_iter(4)` 返回 10。
2. 函数依次调用
`sum_recur(4) → sum_recur(3) → sum_recur(2) → sum_recur(1)`
`sum_recur(1)` 返回 1,随后各层依次得到 `2 + 1 = 3``3 + 3 = 6``4 + 6 = 10`
在最深处,4 次函数调用都尚未结束。
3. 两段代码都进行与 $n$ 成正比的循环或调用,因此时间复杂度均为 $O(n)$ 。
空间复杂度不同:迭代版只使用常数个变量,为 $O(1)$ ;
递归版在到达终止条件前,前面的函数调用都要等待返回结果,因此调用栈中最多同时保存 $n$ 次调用,
空间复杂度为 $O(n)$。
分析空间复杂度时,除代码中的变量外,还要考虑递归调用占用的空间。
### 三段代码的时间复杂度
以下三个代码片段的输入均为正整数 $n$ 。请按时间复杂度从低到高排序,并写出各自的复杂度。
```python
# 片段一
s = 0
for i in range(n):
s += i
# 片段二
s = 0
for i in range(n):
for j in range(i, n):
s += j
# 片段三
while n > 1:
n = n // 2
```
??? success "参考答案"
从低到高为:片段三 $O(\log n)$、片段一 $O(n)$、片段二 $O(n^2)$。
片段三每轮把 $n$ 缩小为原来的一半,约循环 $\log_2 n$ 次。
片段一的循环恰好执行 $n$ 次。片段二的内层循环次数依次为
$n,n-1,\dots,1$,总次数为 $n(n+1)/2$,因此属于平方阶。
### 哪种反转更节省空间
要将数组 `nums` 中的元素全部反转,有两种做法:
<!-- numbered-subquestions -->
1. 新建一个等长数组 `res`,倒序复制后返回;
2. 用两个索引 `i``j` 分别从首、尾向中间移动,逐对交换 `nums[i]``nums[j]`
两种做法的空间复杂度各是多少?哪种属于“原地”操作?
??? success "参考答案"
1. 需要与输入等长的辅助数组,空间复杂度 $O(n)$。
2. 只使用两个索引变量,
空间复杂度 $O(1)$ ,属于原地操作。
需要注意:原地反转会修改输入数组,
仅在允许修改输入时才应优先选用;若需保留原数组,第 1 种做法的复制开销不可避免。
## 编程练习
### 斐波那契数
斐波那契数列满足:$F(0)=0$、$F(1)=1$,并且当 $n\ge2$ 时,
$F(n)=F(n-1)+F(n-2)$。
给定非负整数 `n`,请使用循环计算并返回 $F(n)$,不使用递归。
??? tip "解题提示"
1. 先单独处理 n 为 0 和 1 的情况
2. 计算下一项时只需要前两项,无须保存整个数列
3. 更新两个变量时,注意不要过早覆盖仍会用到的旧值
[LeetCode](https://leetcode.cn/problems/fibonacci-number/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [题目解析](https://leetcode.cn/problems/fibonacci-number/solutions/2361746/509-fei-bo-na-qi-shu-dong-tai-gui-hua-qi-so8h/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
+88
View File
@@ -0,0 +1,88 @@
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
# 练习
## 知识巩固
### 生活场景中的数据关系
请根据数据之间的关系,为下面三个场景选择“线性结构、树形结构、网状结构”中的一种,并说明理由:
<!-- numbered-subquestions -->
1. 同学们排成一列,每人只关心自己前面和后面的人;
2. 学校按“学校 → 年级 → 班级”分层管理;
3. 城市道路连接多个路口,一个路口可以通向多个其他路口,也可能形成环路。
??? success "参考答案"
1. 是线性结构。除排在最前和最后的人外,每个人都只与前面和后面各一人相邻,关系沿一条线展开。
2. 是树形结构。每个班级属于一个年级,每个年级又属于学校,关系从上到下分层展开。
3. 是网状结构。一个路口可以连接多个路口,路线之间还能形成环,不能排成单一顺序或严格层级。
判断结构时应先观察元素之间的关系,而不是先考虑它在内存中占多少空间。
### 逻辑顺序怎样存进内存
为了保存逻辑顺序 `A → B → C`,现有两种简化的内存安排:
- 方案甲:`A、B、C` 分别放在编号为 `20、21、22` 的内存格中;
- 方案乙:`A、B、C` 分别放在编号为 `20、7、31` 的内存格中,并由 `A` 记录 `B` 的位置、`B` 记录 `C` 的位置。
<!-- numbered-subquestions -->
1. 哪个方案属于连续空间存储?哪个属于分散空间存储?
2. 两个方案分别更接近数组还是链表?
3. 方案乙的内存格编号没有按大小排列,为什么仍能表示 `A → B → C` 的逻辑顺序?
??? success "参考答案"
1. 方案甲使用连续的内存格,属于连续空间存储;方案乙的节点分散在不同位置,属于分散空间存储。
2. 方案甲更接近数组,方案乙更接近链表。
3. 逻辑顺序由节点之间记录的连接关系决定,而不是由内存格编号的大小决定。
`A` 记录的位置可以找到 `B`,再从 `B` 记录的位置找到 `C`,所以仍能依次访问 `A、B、C`
这也说明逻辑结构和物理结构是观察同一组数据的两个不同角度。
### 作业记录中的数据类型与结构
某学习小组按座位顺序记录 4 名同学是否交了作业,得到:
`[true, false, true, true]`
<!-- numbered-subquestions -->
1. 每个元素适合使用哪种基本数据类型?
2. 这 4 个元素按座位顺序排成一列,使用了什么逻辑结构?
3. 如果以后改为记录每人的作业分数 `[90, 0, 85, 100]`,改变的是数据的“内容类型”还是“组织方式”?说明理由。
??? success "参考答案"
1. 每个元素只表示“是”或“否”,适合使用布尔类型 `bool`
2. 这些元素按座位顺序排列,形成线性结构,可以用数组保存。
3. 改变的是内容类型:元素由布尔值变成了整数。组织方式没有改变,
这些数据仍然按座位顺序排成一列,仍可使用数组这一线性结构。
基本数据类型描述“存的是什么”,数据结构描述“数据怎样组织”。
## 编程练习
### 统计二进制表示中的 1
给定非负整数 `n`,请统计它的二进制表示中共有多少个 1。
要求使用位运算完成,不把二进制表示转换成字符串,也不使用直接统计 1 的内置函数。
??? tip "解题提示"
1. n & 1 可以取出 n 的最右边一位,用它判断这一位是否为 1
2. 右移一位表示丢掉当前最右边的二进制位;多数语言使用运算符 >>
3. 完成逐位检查并右移的方法后,再观察:n & (n - 1) 会把 n 中最右边的一个 1 变成 0
[LeetCode](https://leetcode.cn/problems/number-of-1-bits/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [题目解析](https://leetcode.cn/problems/number-of-1-bits/solutions/2361978/191-wei-1-de-ge-shu-wei-yun-suan-qing-xi-40rw/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
@@ -0,0 +1,100 @@
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
# 练习
## 知识巩固
### 哪些任务适合分治
一位同学想用“先分成两半,分别解决,再合并结果”的方法完成以下任务。
请分别判断它们是“适合分治”“可以分治,但不会减少总工作量”还是“两半不能独立解决”,并说明理由。
<!-- numbered-subquestions -->
1. 将一个无序数组排序;
2. 求一个数组中的最大值;
3. 按顺序执行一串 `push(x)``pop()` 栈操作,并输出每次 `pop()` 得到的元素。
??? success "参考答案"
1. 适合:对半分解、两半独立排序、$O(n)$ 合并——这正是归并排序。
2. 可以分治,但不会减少总工作量:左右两半仍需合计检查全部 $n$ 个元素,
和直接扫描一样都是 $O(n)$ 时间。
3. 两半不能独立解决:后半段开始时的栈内容取决于前半段执行结果,
两半不能在互不知道结果时独立完成。
### 快速幂是怎样减少计算的
下面的递归函数用分治计算 $x^n$:
```python
def fast_pow(x, n):
if n == 0:
return 1
half = fast_pow(x, n // 2)
if n % 2 == 0:
return half * half
return half * half * x
```
用它计算 `fast_pow(3, 5)`
<!-- numbered-subquestions -->
1. 递归调用时,参数 `n` 依次变成哪些值?
2. 从最深层开始返回时,各层依次返回什么值?
3. 为什么要先保存 `half`,而不是把 `fast_pow(x, n // 2)` 写两遍?
??? success "参考答案"
1. 参数依次为 `5 → 2 → 1 → 0`,每次都把指数减半,直到到达终止条件。
2. `n = 0` 时返回 1`n = 1` 时返回 $1×1×3=3$
`n = 2` 时返回 $3×3=9$`n = 5` 时返回 $9×9×3=243$。
3. 如果把 `fast_pow(x, n // 2)` 在乘法两边各写一次,两次递归会计算完全相同的子问题。
先把结果保存为 `half`,每层就只递归一次,递归深度约为 $\log n$;
调用两次会造成大量重复计算。
### 从遍历序列拆分左右子树
一棵没有重复节点的二叉树,其前序遍历和中序遍历分别为:
- 前序遍历:`[A, B, D, E, C]`
- 中序遍历:`[D, B, E, A, C]`
只完成根节点这一层的拆分,不必继续递归,也不必画出整棵树:
<!-- numbered-subquestions -->
1. 根节点是什么?
2. 左、右子树在中序遍历中分别对应哪一段?
3. 左、右子树在前序遍历中分别对应哪一段?根节点有哪些直接孩子?
??? success "参考答案"
1. 前序遍历的第一个节点是根节点,因此根节点为 `A`
2. `A` 把中序遍历分成两部分:左子树为 `[D, B, E]`,右子树为 `[C]`
3. 左子树有 3 个节点,因此根节点 `A` 后面的 3 个前序元素属于左子树,
`[B, D, E]`;剩余的 `[C]` 属于右子树。
所以根节点的左孩子是 `B`,右孩子是 `C`
## 编程练习
### 快速幂
给定实数 `x` 和整数 `n`,请计算 $x^n$,且不能调用语言内置的幂函数。
要求使用递归分治:每次把指数缩小一半,并复用已经算出的子问题结果。
本题规定 $x^0=1$,包括 `x = 0` 时;当 `n < 0` 时保证 `x != 0`,答案可转化为 $(1/x)^{-n}$。
??? tip "解题提示"
1. n 等于 0 时答案是 1
2. 算出 x 的 n // 2 次方后保存为 half,不要递归调用第二次
3. 当 n < 0 时,先把 x 改为 1 / x,再把 n 改为 -n;使用 C++ 或 Java 时,可先把 n 转为 64 位整数,避免最小的 32 位整数取相反数时溢出
[LeetCode](https://leetcode.cn/problems/powx-n/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [题目解析](https://leetcode.cn/problems/powx-n/solutions/241471/50-powx-n-kuai-su-mi-qing-xi-tu-jie-by-jyd/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
> **说明:** 链接中的题解使用迭代位运算,与本练习指定的递归分治方法不同;请根据本题的解题提示完成递归版本
@@ -0,0 +1,115 @@
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
# 练习
## 知识巩固
### 什么时候适合使用动态规划
一位同学说:“只要能写出递推式,就应该使用动态规划。”
请判断下面三个任务分别更适合使用动态规划、回溯,还是使用循环或数学公式而不建立 `dp` 表,并写出一个关键理由。
<!-- numbered-subquestions -->
1. 使用面值 `[1, 3, 4]` 的硬币凑出金额 6,求最少硬币数,每种硬币可重复使用;
2. 输出 `[1, 2, 3]` 的全部排列;
3. 计算 $1 + 2 + \dots + n$。
对于你判断为适合动态规划的任务,再说明 `dp[i]` 表示什么。
??? success "参考答案"
1. 适合动态规划。令 `dp[i]` 表示凑出金额 `i` 所需的最少硬币数。
对于每枚不超过 `i` 的硬币 `c`,都可以把 `dp[i-c] + 1` 作为候选答案,
再从这些候选值中取最小值。不同选择会反复遇到同一金额,较大金额的最优解也能由较小金额的最优解构成。
金额 6 的答案是 2,即 `3 + 3`
2. 应使用回溯。题目要求逐个生成全部 6 个排列,回溯可以系统地尝试一个选择、继续搜索,
再撤销选择并尝试其他分支。无论采用什么方法,实际输出这些排列时都不能跳过枚举。
3. 使用循环或等差数列公式即可。虽然能写出 `S(i) = S(i-1) + i`,但计算 `S(i)` 时只依赖一个更小的 `S(i-1)`
每个部分和只需计算一次,不存在重复子问题,因此无需建立 `dp` 表。“能写递推式”不等于“需要动态规划”。
### 背包表中的一个格子怎么算
0-1 背包:物品重量 `wgt = [1, 2, 3]` ,价值 `val = [5, 11, 15]` ,背包容量 4 。
`dp[i][c]` 表示只考虑前 $i$ 件物品、背包容量上限为 $c$ 时,能够取得的最大价值;
不要求恰好装满背包。
现在只计算状态 `dp[3][4]`。已知 `dp[2][4] = 16``dp[2][1] = 5`
<!-- numbered-subquestions -->
1. 不选第 3 件物品时,候选价值是多少?
2. 选择第 3 件物品时,还剩多少容量,候选价值是多少?
3. `dp[3][4]` 应取多少?对应选择了哪些物品?
??? success "参考答案"
1. 不选第 3 件物品时,沿用前两件物品的结果,候选价值为 `dp[2][4] = 16`
2. 第 3 件物品重量为 3,放入后还剩容量 $4-3=1$;候选价值为
`dp[2][1] + 15 = 5 + 15 = 20`
3. 比较 16 和 20,得到 `dp[3][4] = 20`。它对应选择第 1、3 件物品,
总重量为 $1+3=4$,总价值为 $5+15=20$。
这个状态的计算体现了 0-1 背包的一次“选或不选”比较。
### 背包容量应该按什么顺序更新
一个 0-1 背包只有一件物品:重量为 2,价值为 5;背包容量为 4。
每件物品最多选择一次,初始一维数组为 `dp = [0, 0, 0, 0, 0]`
一位同学在处理这件物品时,按容量从 2 到 4 更新:
- 更新 `dp[2]` 后得到 5
- 更新 `dp[3]` 后也得到 5
- 更新 `dp[4]` 时又使用刚得到的 `dp[2]`,于是得到 `dp[4] = 10`
<!-- numbered-subquestions -->
1. `dp[4] = 10` 这个结果是否正确?为什么?
2. 根据“每件物品最多选择一次”的条件,`dp[4]` 应为多少?
3. 处理每件物品时,容量应从大到小还是从小到大更新?这样做避免了什么问题?
??? success "参考答案"
1. 这个结果不正确。价值 10 相当于把这件价值为 5 的物品放入了两次,
违反了“每件物品最多选择一次”的条件。
2. 背包中最多只能放入这一件物品,所以正确的 `dp[4]` 为 5。
3. 容量应从大到小更新,即依次处理 4、3、2。
这样在计算 `dp[c]` 时,所读取的 `dp[c-2]` 仍是处理当前物品之前的结果,
不会在同一轮中重复使用当前物品。
## 编程练习
### 爬楼梯的方案数
一段楼梯共有 `n` 阶。你每次只能向上走 1 阶或 2 阶,且必须恰好到达第 `n` 阶。
请计算共有多少种不同的走法。规定 `n >= 1`,走法只按每一步走 1 阶还是 2 阶来区分。
请使用一维动态规划数组完成,暂不使用只保留两个状态的空间优化写法。
??? tip "解题提示"
1. 到达第 i 阶的最后一步,只可能跨 1 阶或 2 阶
2. 因此有 dp[i] = dp[i-1] + dp[i-2]
3. 先处理 n 为 1 和 2 的情况,再从第 3 阶开始填表
[LeetCode](https://leetcode.cn/problems/climbing-stairs/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [题目解析](https://leetcode.cn/problems/climbing-stairs/solutions/2361764/70-pa-lou-ti-dong-tai-gui-hua-qing-xi-tu-ruwa/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
> **说明:** 题解讲解了一维 dp 状态和转移,但给出的代码采用滚动变量压缩空间;本练习要求先实现一维 dp 表,滚动变量仅作为选做优化
### 0-1 背包
给定等长数组 `wgt``val`,第 `i` 件物品的重量为正整数 `wgt[i]`、价值为非负整数 `val[i]`
背包容量 `cap` 为非负整数。每件物品最多选择一次,请在总重量不超过 `cap` 的条件下,
求能够装入背包的最大总价值。请使用一维动态规划实现。
??? tip "解题提示"
1. 初始化长度为 cap + 1 的数组 dp,其中 dp[c] 表示容量上限为 c 时的最大价值
2. 处理物品 i 时,比较“不选它”的 dp[c] 与“选它”的 dp[c-wgt[i]] + val[i]
3. 容量必须从大到小更新,避免在同一轮中重复选择当前物品
+105
View File
@@ -0,0 +1,105 @@
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
# 练习
## 知识巩固
### 用两种方式表示同一张图
一个无向图有 4 个顶点 `A、B、C、D`,边为
`A-B、A-C、B-C、C-D`
<!-- numbered-subquestions -->
1. 写出它的邻接表;
2. 填写只含 0 和 1 的邻接矩阵;
3. 如果要判断 `A``D` 是否直接相连,哪种图的表示方法只需查看一个存储条目?
4. 如果图的顶点很多但边很少,哪种表示通常更节省空间?
??? success "参考答案"
1. 邻接表为:
```text
A: B, C
B: A, C
C: A, B, D
D: C
```
2. 邻接矩阵为:
| | A | B | C | D |
| --- | --- | --- | --- | --- |
| A | 0 | 1 | 1 | 0 |
| B | 1 | 0 | 1 | 0 |
| C | 1 | 1 | 0 | 1 |
| D | 0 | 0 | 1 | 0 |
3. 邻接矩阵可直接查看第 `A` 行、第 `D` 列,因此很适合判断任意两点是否直接相连。
4. 顶点很多但边很少时,邻接表只记录实际存在的边,通常比为每一对顶点都预留位置的邻接矩阵更节省空间。
### 广度优先与深度优先访问顺序
一个无向图的顶点为 `A、B、C、D、E`,边为
`A-B、A-C、B-D、C-D、D-E`。
从 A 出发,并规定遇到多个未访问邻接顶点时按字母顺序选择:
<!-- numbered-subquestions -->
1. 写出广度优先遍历(BFS)的访问顺序;
2. 写出递归深度优先遍历(DFS)的访问顺序;
3. 为什么两种遍历都需要记录已经访问过的顶点?
??? success "参考答案"
1. BFS 的访问顺序为 `A, B, C, D, E`。它先访问离 A 一条边的 B、C,
再访问更远的 D、E。
2. DFS 的访问顺序为 `A, B, D, C, E`。它依次进入当前顶点的未访问邻接顶点,
因此先走过 `A → B → D → C`;C 没有新的邻接顶点时回到 D,再访问 E。
3. 图中存在环,例如 `A-B-D-C-A`。如果不记录已访问顶点,
遍历可能反复沿环访问同一批顶点,无法正常结束。
### 一次 BFS 能访问整张图吗
一个无向图有顶点 `A、B、C、D、E、F`,边只有
`A-B、B-C、D-E`。
<!-- numbered-subquestions -->
1. 从 A 开始进行一次 BFS,可以访问哪些顶点?
2. 根据第 1 问,这一次 BFS 是否已经访问图中的所有顶点?为什么?
3. 若按字母顺序扫描所有顶点,每遇到一个未访问顶点就重新开始 BFS,
各次 BFS 的起点是什么?这个图被分成几个互不连通的部分(连通分量)?
??? success "参考答案"
1. 从 A 出发只能访问 `A、B、C`。
2. 没有访问所有顶点。`D、E` 组成另一个互相连通的部分,F 是单独的顶点;
它们与 A 之间都没有路径,因此从 A 出发无法到达。
3. 三次 BFS 的起点依次为 `A、D、F`,分别访问
`{A, B, C}`、`{D, E}` 和 `{F}`。因此这个图有 3 个连通分量。
## 编程练习
### 判断无向图中是否存在路径
给定一个含有 $n$ 个顶点的无向图,顶点编号为 $0$ 到 $n-1$。数组 `edges` 中的每一项 `[u, v]` 表示顶点 `u` 和 `v` 之间有一条无向边。
再给定起点 `source` 和终点 `destination`。请先根据 `edges` 建立邻接表,再使用 BFS 或 DFS
判断是否存在一条从 `source` 到 `destination` 的路径:存在则返回 `true`,否则返回 `false`。
图中可能有环,也可能不连通。
??? tip "解题提示"
1. 每条无向边需要同时加入两个方向
2. 图中可能有环,必须记录已经访问过的节点
3. 从 source 出发,遇到 destination 返回 true,遍历结束仍未遇到则返回 false
[LeetCode](https://leetcode.cn/problems/find-if-path-exists-in-graph/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
+85
View File
@@ -0,0 +1,85 @@
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
# 练习
## 知识巩固
### 每次选最大硬币一定最好吗
硬币面值为 `[1, 7, 10]`,目标金额为 14。
<!-- numbered-subquestions -->
1. 按“每次取不超过剩余金额的最大面值”执行,写出所取硬币;
2. 是否存在使用更少硬币的方案?如果存在,请写出一种;如果不存在,请说明理由;
3. 这能否说明该贪心策略对任意硬币面值都正确?
??? success "参考答案"
1. 贪心依次选择 `10 + 1 + 1 + 1 + 1`,共 5 枚硬币。
2. 存在使用更少硬币的方案:`7 + 7`,只需 2 枚硬币。
3. 不能。这个反例说明,对任意硬币面值,每次选择当前能取的最大面值并不一定得到最少硬币数;
眼前最大的选择可能破坏后续更好的组合。
### 背包应该先装哪件物品
一个容量为 4 千克的背包中可以装入以下物品,物品允许只取一部分,
所取价值与重量成正比:
- 物品 A:重量 4 千克,价值 20;
- 物品 B:重量 3 千克,价值 18。
<!-- numbered-subquestions -->
1. 两件物品每千克的价值分别是多少?应先装哪一件?
2. 按分数背包的贪心策略装满背包,最终价值是多少?
3. 在物品可以切分、背包限制总重量的情况下,选择物品时应该比较总价值还是每千克价值?为什么?
??? success "参考答案"
1. A 每千克价值为 `20 ÷ 4 = 5`B 每千克价值为 `18 ÷ 3 = 6`
因此应先装单位价值更高的 B。
2. 先装入全部 B,占用 3 千克、获得价值 18;剩余 1 千克容量,
再装入 1 千克 A,获得价值 5。最终价值为 `18 + 5 = 23`
3. 背包限制的是总重量,而且物品可以切分,所以应比较单位重量的价值。
A 的总价值虽然更高,但每千克价值低于 B;若先装满 A,只能得到价值 20。
### 两个指针下一步怎样移动
隔板高度为 `[1, 8, 6, 2, 5]`,用首尾双指针寻找最大容量。
容量等于“两块隔板中较短者的高度 × 两块隔板的下标距离”。
<!-- numbered-subquestions -->
1. 初始时左指针在下标 0、右指针在下标 4,当前容量是多少?下一步应移动哪个指针?
2. 按照第 1 问的选择移动一次后,两个指针分别位于哪个下标?此时容量是多少?下一步又应移动哪个指针?
3. 在当前一对隔板中,可以移动较短隔板对应的指针,也可以移动较长隔板对应的指针。哪一种移动仍有可能得到更大的容量?为什么?
??? success "参考答案"
1. 当前容量为 `min(1, 5) × (4 - 0) = 4`。左侧隔板较短,因此移动左指针。
2. 移动左指针后,两个指针分别位于下标 1 和 4。当前容量为
`min(8, 5) × (4 - 1) = 15`。右侧隔板较短,因此下一步移动右指针。
3. 移动较短隔板对应的指针,才仍有可能得到更大的容量。移动较长隔板后,两块隔板的距离一定缩短,而容器高度仍受未移动的短板限制,
只可能不变或变小。因此容量不可能超过移动前;只有移动短板,才有机会遇到更高的隔板。
## 编程练习
### 分数背包
给定等长数组 `wgt``val`,其中 `wgt[i] > 0``val[i] >= 0`,背包容量 `cap >= 0`
每种物品只有一件,但允许只装入其中一部分,
获得的价值按所装重量占该物品总重量的比例计算。请使用贪心算法,
并以实数返回背包能够获得的最大总价值。
??? tip "解题提示"
1. 先计算每件物品的单位重量价值 val[i] / wgt[i],除法结果要保留小数
2. 单位价值越高的物品越先装入背包
3. 若剩余容量小于当前物品重量,就装入恰好填满背包的部分并结束
+103
View File
@@ -0,0 +1,103 @@
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
# 练习
## 知识巩固
### 发生哈希冲突后怎样查找
一个哈希表有 5 个桶,哈希函数为 $h(x)=x \bmod 5$,冲突时把元素依次放进该桶的列表中。
依次插入 `[1, 6, 11, 7]`
<!-- numbered-subquestions -->
1. 写出 04 号桶中的内容;
2. 查找 6 时会先进入哪个桶,并依次检查哪些元素?
3. 根据第 1 问写出的桶内容,后插入的元素是否覆盖了先插入的元素?结合这种冲突处理方式说明理由。
??? success "参考答案"
1. 因为 $1\bmod5=6\bmod5=11\bmod5=1$,而 $7\bmod5=2$,各桶为:
```text
0: []
1: [1, 6, 11]
2: [7]
3: []
4: []
```
2. 查找 6 时先进入 1 号桶,再依次比较 1、6,第二次比较时找到目标。
3. 哈希值相同只表示它们落入同一个桶,并不表示这些元素相等。链式地址把冲突元素都保存在桶内,
查询时再逐个比较,因此 1、6、11 不会互相覆盖。
### 哈希表扩容后元素去哪儿
一个使用链式地址的哈希表原有 5 个桶,哈希函数为 $h(x)=x\bmod5$。
键 `[1, 6, 11]` 都在 1 号桶中。
现在把哈希表扩容为 7 个桶,哈希函数相应变为 $h(x)=x\bmod7$
<!-- numbered-subquestions -->
1. 分别计算 1、6、11 的新桶号。
2. 扩容后哪些桶中有元素?
3. 扩容时能否把原来 1 号桶中的列表原样复制到新的 1 号桶?结合第 1、2 问的结果说明理由。
??? success "参考答案"
1. 新桶号分别为:
- $1\bmod7=1$
- $6\bmod7=6$
- $11\bmod7=4$。
2. 1 号桶保存 1,4 号桶保存 11,6 号桶保存 6。三个键不再挤在同一个桶中。
3. 不能原样复制。桶号由“键对桶数取余”得到。桶数从 5 变成 7 后,同一个键的新桶号可能改变,
因此必须重新计算每个键的位置。若把旧的 1 号桶原样复制过去,之后按新公式查找 6 和 11 时,
会分别前往 6 号桶和 4 号桶,从而找不到它们。
### 删除 6 后还能找到 11 吗
一个哈希表有 5 个位置,索引为 `04`,哈希函数为 $h(x)=x\bmod5$。
发生冲突时,从哈希函数算出的索引开始,向右寻找第一个空位。
依次插入 `[1, 6, 11]`
<!-- numbered-subquestions -->
1. 三个数最终分别放在哪个索引?
2. 查找 11 时,会依次检查哪些索引?
3. 如果删除 6 时直接把它的位置改成“从未使用的空位”,而查找遇到空位就停止,
再查找 11 时会发生什么?这个查找结果是否正确?如果有问题,应怎样避免?
??? success "参考答案"
1. 1 放在索引 1。6 也映射到索引 1,发生冲突后放在索引 2。
11 同样从索引 1 开始,依次跳过已占用的索引 1、2,最终放在索引 3。
2. 查找 11 时依次检查索引 `1、2、3`,在索引 3 找到它。
3. 如果把索引 2 改成表示“从未使用”的空位,查找 11 时检查索引 1 后就会在索引 2 停止,
从而错误地认为 11 不存在。删除时应留下“已删除”标记:
查找遇到该标记时继续检查下一个索引(越过索引 4 后回到索引 0),而以后的插入仍可重新使用这个位置。
## 编程练习
### 比较两个字符串的字符组成
给定两个只含小写英文字母的字符串 `s` 和 `t`。
可以任意调整 `s` 中字符的位置,但不能添加、删除或替换字符。
请判断调整后能否得到 `t`;可以则返回 `true`,否则返回 `false`。
请使用哈希表记录各字母的出现次数,不对字符串中的字符排序。
??? tip "解题提示"
1. 两个字符串长度不同,它们的字符组成一定不同
2. 用哈希表记录每种字母的数量;扫描 s 时把对应计数加 1
3. 扫描 t 时把对应计数减 1;所有计数最终都为 0,字符组成才相同
[LeetCode](https://leetcode.cn/problems/valid-anagram/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [题目解析](https://leetcode.cn/problems/valid-anagram/solutions/2362065/242-you-xiao-de-zi-mu-yi-wei-ci-ha-xi-bi-cch7/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
+90
View File
@@ -0,0 +1,90 @@
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
# 练习
## 知识巩固
### 数字 10 入堆后怎样调整
数组 `[9, 7, 8, 3, 5]` 表示一个大顶堆。现在将数字 10 入堆。
<!-- numbered-subquestions -->
1. 先把 10 添加到数组末尾,它的父节点值是多少?
2. 从新节点开始向上堆化,写出每次交换后的数组。
3. 最终的堆顶元素是什么?一共交换了几次?
??? success "参考答案"
1. 10 添加后的索引为 5,其父节点索引为
$\lfloor(5-1)/2\rfloor=2$,父节点值为 8。
2. 10 大于 8,第一次交换后为 `[9, 7, 10, 3, 5, 8]`
10 又大于父节点 9,第二次交换后为 `[10, 7, 9, 3, 5, 8]`
此时 10 已到达根节点,堆化结束。
3. 最终堆顶为 10,共交换 2 次。
### 检查小顶堆的父子关系
数组 `[1, 4, 3, 7, 6, 2]` 表示一棵完全二叉树。小顶堆要求每个父节点都不大于它的孩子。
对索引 $i$,左、右孩子索引分别为 $2i+1$ 和 $2i+2$。
<!-- numbered-subquestions -->
1. 索引 2 的孩子索引和值分别是什么?
2. 索引 2 处的节点值为 3,它与孩子节点之间是否违反小顶堆规则?如果违反,应交换哪两个元素?
3. 根据第 2 问的判断:若违反规则,写出交换后的数组;若不违反,说明无须交换。最后检查其余父子关系。
??? success "参考答案"
1. 索引 2 的左孩子索引是 5、值为 2;右孩子索引是 6,但数组长度为 6,因此右孩子不存在。
2. 父节点值 3 大于孩子值 2,违反小顶堆规则,应交换索引 2 和索引 5 的元素。
3. 交换后得到 `[1, 4, 2, 7, 6, 3]`。逐一检查:
`1 ≤ 4``1 ≤ 2``4 ≤ 7``4 ≤ 6``2 ≤ 3`
所有父节点都不大于自己的孩子,因此现在满足小顶堆规则。
### 用小顶堆保留最大的三个数
要从数据流 `[4, 1, 7, 3, 8]` 中保留最大的 3 个数,可以维护一个大小不超过 3 的小顶堆。
先把前 3 个数依次放入小顶堆。堆满后,每读入一个新数:
如果它大于堆顶,就删除堆顶并放入新数;否则堆保持不变。
请写出每次读入数字后堆中保留的数字和堆顶。
只需把保留的数字写成集合,不要求写出它们在堆数组中的排列。
??? success "参考答案"
每次读入数字后的结果如下:
| 读入数字 | 保留的数字 | 堆顶 |
| --- | --- | --- |
| 4 | `{4}` | 4 |
| 1 | `{1, 4}` | 1 |
| 7 | `{1, 4, 7}` | 1 |
| 3 | `{3, 4, 7}` | 3 |
| 8 | `{4, 7, 8}` | 4 |
堆满后,堆顶是当前保留数字中最小的一个。只有新数字大于堆顶时,
才用新数字替换堆顶。最终保留的 `{4, 7, 8}` 正是最大的 3 个数。
## 编程练习
### 寻找数组中的第 k 大元素
给定整数数组 `nums` 和整数 $k$,其中 $1 \le k \le n$,$n$ 是数组长度。把数组按从大到小排列后,请返回第 $k$ 个位置上的元素。
重复元素需要分别计数。例如,`[5, 5, 2]` 的第 2 大元素仍是 5。请使用一个大小不超过 $k$ 的小顶堆完成。
??? tip "解题提示"
1. 第 k 大元素是最大的 k 个数中最小的那个
2. 每个数先入小顶堆,大小超过 k 时弹出最小值
3. 遍历结束时堆中保留最大的 k 个数,堆顶就是答案
[LeetCode](https://leetcode.cn/problems/kth-largest-element-in-an-array/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [题目解析](https://leetcode.cn/problems/kth-largest-element-in-an-array/solutions/2361969/215-shu-zu-zhong-de-di-k-ge-zui-da-yuan-d786p/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
> **说明:** 链接中的题解讲解了快速排序和快速选择,没有使用堆;本练习请按照正文 8.3 节使用大小不超过 k 的小顶堆完成
+97
View File
@@ -0,0 +1,97 @@
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
# 练习
## 知识巩固
### 二分查找怎样缩小搜索区间
在有序数组 `[2, 5, 8, 12, 16, 23, 38]` 中查找 16。
使用双闭区间 `[i, j]`,中点为
$m=i+(j-i)/2$(向下取整)。
请写出每轮的 `(i, j, m)`、中点元素,以及下一步如何缩小区间,
直到找到目标。
??? success "参考答案"
每轮查找过程如下:
| 轮次 | `(i, j, m)` | 中点元素 | 下一步 |
| --- | --- | --- | --- |
| 1 | `(0, 6, 3)` | 12 | `12 < 16`,令 `i = 4` |
| 2 | `(4, 6, 5)` | 23 | `23 > 16`,令 `j = 4` |
| 3 | `(4, 4, 4)` | 16 | 找到目标,返回索引 4 |
数组有序,所以中点值小于目标时可以排除中点及其左侧;
中点值大于目标时可以排除中点及其右侧。
### 重复元素的左右边界
在数组 `[1, 2, 2, 2, 4, 6]` 中查找数字 2。
一名同学用二分查找在索引 2 找到目标后立即返回,并说:
“索引 2 就是数字 2 的左边界。”
<!-- numbered-subquestions -->
1. 这名同学的说法是否正确?数字 2 的左、右边界分别是多少?请说明理由。
2. 查找左边界时,若中点元素等于目标,接下来应继续搜索哪一侧?
3. 查找右边界时又应继续搜索哪一侧?只需说明方向,无须写出完整的查找过程。
??? success "参考答案"
1. 这名同学的说法不正确。找到一个 2 后立即返回,只能保证找到某一个 2,不能保证找到最左或最右的 2。
本题的左边界为索引 1,右边界为索引 3。
2. 查找左边界时,即使中点元素等于 2,也要继续搜索左侧,
例如在双闭区间写法中令 `j = m - 1`
3. 查找右边界时,中点元素等于 2 后应继续搜索右侧,
例如令 `i = m + 1`
### 不同数据该选哪种搜索方法
请在“顺序查找、二分查找、哈希表”中,为下面三个场景选择合适的方法,并说明理由:
<!-- numbered-subquestions -->
1. 在 $10^7$ 个已经有序且不再变动的整数中反复查找,不额外建立其他数据结构;
2. 在频繁插入、删除的数据集中反复判断某个键是否存在,不要求保持有序,也不进行范围查找;
3. 在无序数组中只查找一次某个值。
??? success "参考答案"
1. 二分查找:数据有序且静态,$O(\log n)$ 且无须额外空间。
2. 哈希表:当哈希函数能把键较均匀地分散到各桶时,插入、删除和按键查找的平均时间复杂度都是 $O(1)$。
3. 直接从头到尾遍历:只查找一次时,排序或建立哈希表都要先处理整个数组,
并不会减少这一次任务的总工作量。
选择取决于数据是否有序、是否允许建立额外结构、查询次数以及需要执行哪些操作。
## 编程练习
### 有序数组的二分查找
给定一个按严格递增顺序排列的整数数组 `nums` 和目标值 `target`。请使用二分查找找到 `target`:若存在,返回它的数组索引;若不存在,返回 -1。
??? tip "解题提示"
1. 初始区间是 left = 0、right = n - 1,非空条件是 left <= right
2. 用 mid = left + (right - left) // 2 计算中点
3.`nums[mid]` 小于 `target`,就把左边界移到 `mid + 1`;若 `nums[mid]` 大于 `target`,就把右边界移到 `mid - 1`;相等时立即返回
[LeetCode](https://leetcode.cn/problems/binary-search/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [题目解析](https://leetcode.cn/problems/binary-search/solutions/1692151/by-jyd-i7xr/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
### 有序数组的插入位置
给定一个按严格递增顺序排列的整数数组 `nums` 和目标值 `target`
`target` 已在数组中,返回它的索引;否则,返回把 `target` 插入数组后仍能保持严格递增顺序的位置。答案可能是 0,也可能等于数组长度。请使用二分查找。
??? tip "解题提示"
1. 答案可能是 0,也可能是数组长度 n
2. 使用双闭区间时,若 `nums[mid]` 大于等于 `target`,就令 `right = mid - 1`,继续检查更靠左的位置;否则令 `left = mid + 1`
3. 循环结束时 left 就是插入位置
[LeetCode](https://leetcode.cn/problems/search-insert-position/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
+96
View File
@@ -0,0 +1,96 @@
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
# 练习
## 知识巩固
### 选择排序和冒泡排序的前几轮
给定数组 `[4, 2, 5, 1, 3]`,以下都按从小到大排序。
<!-- numbered-subquestions -->
1. 按选择排序模拟前两轮,写出每轮结束后的数组,并指出此时哪些位置已经确定。
2. 按冒泡排序模拟第一轮,写出数组状态和交换次数,并指出哪个位置已经确定。
??? success "参考答案"
1. 前两轮为:
| 轮次 | 数组状态 | 说明 |
| --- | --- | --- |
| 1 | `[1, 2, 5, 4, 3]` | 最小元素 1 与首位交换 |
| 2 | `[1, 2, 5, 4, 3]` | 数值 2 已位于索引 1,无须交换 |
此时前两个位置已经确定,后续只需在 `[5, 4, 3]` 中继续选择最小元素。
2. 依次比较相邻元素:4 与 2 交换、4 与 5 不换、5 与 1 交换、5 与 3 交换,
`[2, 4, 1, 3, 5]` ,共 3 次交换;最大元素 5 已移动到数组末尾,因此最后一个位置已经确定。
### 相同元素的先后次序会变吗
数组 $[2_a, 2_b, 1]$ 中,$2_a$ 和 $2_b$ 的数值相同,但用下标标出了原有次序。
<!-- numbered-subquestions -->
1. 写出选择排序第一轮结束后的数组。$2_a$ 和 $2_b$ 的相对次序是否改变?
2. 写出冒泡排序第一轮结束后的数组。$2_a$ 和 $2_b$ 的相对次序是否改变?
3. 根据前两问,说明两种排序在保持相等元素原有次序方面有什么不同。
??? success "参考答案"
1. 选择排序第一轮选出最小元素 1,并与首位 $2_a$ 交换,得
$[1, 2_b, 2_a]$ 。$2_a$ 被换到了 $2_b$ 之后,两者的相对次序已经改变。
2. 冒泡排序先比较 $2_a$ 和 $2_b$,因为两者相等,不交换;再比较 $2_b$ 和 1 并交换,
第一轮结束后为 $[2_a, 1, 2_b]$ 。$2_a$ 仍在 $2_b$ 之前,相对次序没有改变。
3. 这个例子中,选择排序会改变相等元素的原有次序;冒泡排序只在左边元素大于右边元素时
交换相邻元素。相等元素之间不交换,因此能保持它们原有的先后次序。
### 比较计数排序和基数排序
学校要排序许多固定为 8 位的学号。请回答:
<!-- numbered-subquestions -->
1. 从最低位开始的基数排序需要处理几轮?
2. 如果把学号直接当作整数使用计数排序,为什么会准备大量没有用到的计数位置?
3. 根据前两问,如果在计数排序和基数排序之间选择一种处理大量固定为 8 位的学号,你会选择哪一种?为什么?
??? success "参考答案"
1. 学号有 8 个数位,因此从最低位到最高位共处理 8 轮;每轮只按 0~9 分组。
2. 直接计数需要为所有可能的 8 位取值预留位置,但实际学生只使用其中很少一部分,
大多数计数位置会一直为 0。
3. 应选择基数排序。它利用了“位数固定、每位只有 10 种取值”的特点,只需重复 8 轮稳定分组。
计数排序若把整个 8 位学号当作整数下标,则需要为大量实际不会出现的数值预留计数位置。
## 编程练习
### 用归并排序排列数组
给定一个整数数组 `nums`,请自行实现归并排序,把其中的元素按非递减顺序排列并返回。不要调用语言自带的排序函数。
??? tip "解题提示"
1. 区间长度不超过 1 时,它已经有序
2. 从中点把区间分成两半,并分别递归排序
3. 用两个指针合并两个有序半区,再把结果写回原数组
[LeetCode](https://leetcode.cn/problems/sort-an-array/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
### 用计数排序排列整数数组
给定整数数组 `nums` 和非负整数 $K$,数组中的每个元素都在 $0$ 到 $K$ 之间。
请实现计数排序,把结果按非递减顺序写回 `nums` 并返回 `nums`
不要使用元素之间的大小比较来决定顺序,也不要调用语言自带的排序函数。
??? tip "解题提示"
1. 因为每个元素都在 0 到 K 之间,可以直接用元素值作为计数数组的索引
2. 第一遍扫描 nums,令对应位置的计数加 1
3. 再从 0 到 K 扫描计数数组;数值 x 出现多少次,就向 nums 中连续写入多少个 x
+101
View File
@@ -0,0 +1,101 @@
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
# 练习
## 知识巩固
### 栈和队列会先取出谁
准备一个空栈 `S` 和一个空队列 `Q`,分别对它们执行下面同一组操作:
步骤 1:加入 `A`
步骤 2:加入 `B`
步骤 3:移除一个元素并记录;
步骤 4:加入 `C`
步骤 5:不断移除并记录,直到容器为空。
请分别写出 `S``Q` 中元素被移除的顺序,并用“先入后出”或“先入先出”解释差异。
??? success "参考答案"
`S` 的移除顺序是 `B、C、A`:加入 `A、B` 后先弹出最近加入的 `B`,再加入 `C`
此后依次弹出 `C、A`,体现“先入后出”。
队列 `Q` 的移除顺序是 `A、B、C`:加入 `A、B` 后先移除最早加入的 `A`
再加入 `C`,此后依次移除 `B、C`,体现“先入先出”。
### 队尾越过数组末尾怎么办
用长度为 5 的环形数组实现队列,数组索引为 `04`
当前 `front = 3``size = 2`,队列中的 `A、B` 分别位于索引 3、4。
<!-- numbered-subquestions -->
1. 执行“`C` 入队”时,`C` 应放在哪个索引?入队后 `size` 是多少?
2. 接着执行一次出队,弹出哪个元素?新的 `front``size` 分别是多少?
3. 此时从队首到队尾的逻辑顺序是什么?出队时是否需要移动数组中的其他元素?为什么?
??? success "参考答案"
1. 新元素的位置为
`(front + size) % 5 = (3 + 2) % 5 = 0`
所以 `C` 放在索引 0。入队后 `size = 3`
2. 出队弹出当前队首 `A`。新的队首索引为
`(3 + 1) % 5 = 4`,因此 `front = 4``size = 2`
3. 有效元素的逻辑顺序为 `B、C`,其中 `B` 位于索引 4`C` 位于索引 0。
出队时只需移动 `front` 并修改 `size`,环形数组用取余让索引回到开头,
因此无须把其他元素整体向前移动。
### 双向队列的两端操作
这里规定:`push_first` 表示从队首加入,`push_last` 表示从队尾加入,
`pop_first` 表示从队首弹出,`pop_last` 表示从队尾弹出。
对一个空的双向队列 `deq` 依次执行:
1. `push_last(A)`
2. `push_last(B)`
3. `push_first(C)`
4. `pop_last()`
5. `push_last(D)`
6. `pop_first()`
<!-- numbered-subquestions -->
1. 两次弹出的元素分别是什么?
2. 全部操作完成后,从队首到队尾还剩哪些元素?
3. 检查这 6 步操作:只允许从队尾加入、从队首删除的队列能否全部完成?如果不能,请指出无法完成的操作;再说明双向队列能否完成及其原因。
??? success "参考答案"
前三步后,双向队列从队首到队尾为 `[C, A, B]`
<!-- numbered-subquestions -->
1. `pop_last()` 弹出 `B`;加入 `D` 后队列为 `[C, A, D]`
`pop_first()` 再弹出 `C`
2. 最后剩下 `[A, D]`
3. 只允许在队尾添加、在队首删除的队列不能完成全部操作:
第 3 步 `push_first(C)` 要求从队首加入,第 4 步 `pop_last()` 要求从队尾删除,都超出了这种队列的操作范围。
双向队列的两端都可以添加和删除,因此能够完成这 6 步操作。
## 编程练习
### 检查括号序列
给定一个只包含 `()``[]``{}` 这三类括号的字符串 `s`,请使用栈判断它是否合法。
合法序列须同时满足:每个右括号都必须与最近一个尚未配对的左括号类型匹配,
并且遍历结束后没有未配对的左括号。返回布尔值表示判断结果。
??? tip "解题提示"
1. 可以建立“右括号到对应左括号”的映射
2. 遇到右括号时,先检查栈是否为空,再检查栈顶是否匹配
3. 遍历结束后,栈也必须为空
[LeetCode](https://leetcode.cn/problems/valid-parentheses/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [题目解析](https://leetcode.cn/problems/valid-parentheses/solutions/9185/valid-parentheses-fu-zhu-zhan-fa-by-jin407891080/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
+128
View File
@@ -0,0 +1,128 @@
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
# 练习
## 知识巩固
### 完全、完满与完美二叉树
下面两个数组按层序表示二叉树,`None` 表示空位:
- 树 A`[1, 2, 3, 4, 5, 6]`
- 树 B`[1, 2, 3, None, None, 6, 7]`
<!-- numbered-subquestions -->
1. 哪一棵是完全二叉树?
2. 哪一棵是完满二叉树,即每个非叶节点都有两个子节点?
3. 两棵树中是否有完美二叉树?分别说明理由。
??? success "参考答案"
1. 树 A 是完全二叉树。它只有最底层未填满,并且节点从左到右连续排列。
树 B 不是完全二叉树,因为最底层左侧已有空位,右侧却仍有节点。
2. 树 B 是完满二叉树:节点 1 和节点 3 各有两个子节点,其余节点都是叶节点。
树 A 不是完满二叉树,因为节点 3 只有左子节点 6。
3. 两棵都不是完美二叉树,因为它们的最底层都没有完全填满。
### 同一棵树的三种遍历顺序
将数组 `[1, 2, 3, 4, 5, 6, 7]` 按层序存入一棵完全二叉树。
<!-- numbered-subquestions -->
1. 画出这棵树。
2. 写出它的前序、中序、后序遍历序列。
3. 在中序序列中,根节点 1 左侧和右侧的两段序列,分别对应树的哪一部分?
??? success "参考答案"
1. 这棵树为:
```text
1
/ \
2 3
/ \ / \
4 5 6 7
```
2. 前序遍历为 `1, 2, 4, 5, 3, 6, 7`
中序遍历为 `4, 2, 5, 1, 6, 3, 7`
后序遍历为 `4, 5, 2, 6, 7, 3, 1`。
3. 根节点 1 左侧的 `4, 2, 5` 是左子树的中序遍历序列;
右侧的 `6, 3, 7` 是右子树的中序遍历序列。
### 比较两棵二叉搜索树
将下面两组序列从左到右依次插入空的二叉搜索树:
- 序列 A`[4, 2, 6, 1, 3, 5, 7]`
- 序列 B`[1, 2, 3, 4, 5, 6, 7]`
<!-- numbered-subquestions -->
1. 分别写出查找数字 7 时经过的节点。
2. 若高度按根节点到最远叶节点经过的边数计算,两棵树的高度分别是多少?
3. 根据前两问,你认为两棵树查找数字 7 时的效率是否相同?请结合两棵树的形状和查找路径说明理由。
??? success "参考答案"
1. 序列 A 建成的树中,查找路径为 `4 → 6 → 7`
序列 B 建成的树中,查找路径为 `1 → 2 → 3 → 4 → 5 → 6 → 7`。
2. 第一棵树的每一层都填满,高度为 2;第二棵树只有右孩子,高度为 6。
3. 两棵树查找 7 时的效率不同。插入顺序改变了二叉搜索树的形状和高度。查找 7 时,第一棵树只经过 3 个节点,
第二棵树则要依次经过 7 个节点;树越高,最坏情况下需要沿路径比较的节点就越多。
## 编程练习
### 二叉树的最大深度
给定一棵二叉树的根节点 `root`。每个节点都包含一个整数值,以及指向左、右子节点的引用。
把从根节点到最远叶节点经过的**节点数**称为二叉树的最大深度。请返回这棵树的最大深度;空树的最大深度为 0。
请使用递归完成。
??? tip "解题提示"
1. 本题按节点数计算深度:只有一个根节点时,最大深度为 1
2. 让递归函数返回以当前节点为根的子树的最大深度
3. 空节点返回 0,非空节点返回 max(depth(left), depth(right)) + 1
[LeetCode](https://leetcode.cn/problems/maximum-depth-of-binary-tree/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [题目解析](https://leetcode.cn/problems/maximum-depth-of-binary-tree/solutions/2361697/104-er-cha-shu-de-zui-da-shen-du-hou-xu-txzrx/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
### 按层遍历二叉树
给定一棵二叉树的根节点 `root`。请使用队列从上到下逐层访问所有节点,同一层内按照从左到右的顺序访问。
返回一个二维数组:第一个子数组保存根节点所在层的节点值,第二个子数组保存下一层的节点值,依此类推。
若二叉树为空,返回空数组。
??? tip "解题提示"
1. 层序遍历需要先进入的节点先访问,因此使用队列
2. 一轮开始时队列中的节点正好属于同一层
3. 先记录队列长度,再弹出这么多个节点并加入它们的孩子
[LeetCode](https://leetcode.cn/problems/binary-tree-level-order-traversal/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [题目解析](https://leetcode.cn/problems/binary-tree-level-order-traversal/solutions/2361604/102-er-cha-shu-de-ceng-xu-bian-li-yan-du-dyf7/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
### 二叉搜索树中的第 k 小元素
一棵二叉搜索树共有 `n` 个节点,各节点值互不相同。
将所有节点值从小到大排列后,位置从 1 开始编号。
给定根节点 `root` 和满足 `1 <= k <= n` 的整数 `k`,请返回排在第 `k` 位的节点值。
请在中序遍历过程中直接寻找答案,不先收集全部节点值。
??? tip "解题提示"
1. 二叉搜索树的中序遍历会按从小到大的顺序访问节点值
2. 中序遍历依次处理左子树、当前节点和右子树;访问当前节点时把计数加 1
3. 当计数第一次等于 k 时,当前节点值就是答案,此后无须继续遍历
[LeetCode](https://leetcode.cn/problems/kth-smallest-element-in-a-bst/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } [题目解析](https://leetcode.cn/problems/kth-smallest-element-in-a-bst/solutions/2361685/230-er-cha-sou-suo-shu-zhong-di-k-xiao-d-n3he/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }