This commit is contained in:
krahets
2026-07-19 03:42:10 +08:00
parent cb5235bc3f
commit 2b8612bc41
238 changed files with 7477 additions and 3 deletions
@@ -0,0 +1,112 @@
---
comments: true
---
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
# 4.6 &nbsp; 练习
## 4.6.1 &nbsp; 知识巩固
### 1. &nbsp; 数组和链表怎样找到元素
数组和单向链表中都按顺序保存了 `[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)$ 时间。
这里只比较按位置读取,不表示链表在所有操作上都更慢。
### 2. &nbsp; 数组和链表怎样插入元素
数组和单向链表中都保存了 `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)$ 时间。
### 3. &nbsp; 列表容量是怎样增长的
一个基于数组实现的列表当前内容为 `[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. 原来的数组本身没有变长。列表创建了一个更大的新数组,复制原有元素,
再改用新数组作为底层存储,因此对使用者来说容量增长了。
## 4.6.2 &nbsp; 编程练习
### 1. &nbsp; 数组表示的大整数加一
数组 `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" }
### 2. &nbsp; 反转单向链表
给定一个单向链表的头节点 `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" }
@@ -20,3 +20,4 @@ icon: material/view-list-outline
- [4.3 &nbsp; 列表](list.md)
- [4.4 &nbsp; 内存与缓存 *](ram_and_cache.md)
- [4.5 &nbsp; 小结](summary.md)
- [4.6 &nbsp; 练习](exercises.md)
+95
View File
@@ -0,0 +1,95 @@
---
comments: true
---
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
# 13.6 &nbsp; 练习
## 13.6.1 &nbsp; 知识巩固
### 1. &nbsp; 这段排列算法会漏掉结果吗
一个回溯算法按 `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. &nbsp; 数字的选择顺序重要吗
给定排好序的数组 `[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 后面的候选数只会更大,也都不可能加入当前组合,所以可以直接结束这一层的检查。
### 3. &nbsp; 下一枚皇后可以放在哪些位置
在一个 `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)`
这一步只说明当前放置合法;之后若无法完成棋盘,仍需回退并尝试更早的其他选择。
## 13.6.2 &nbsp; 编程练习
### 1. &nbsp; 无重复元素的全排列
整数数组 `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" }
> **说明:** 链接题解通过交换数组元素,把已选元素依次放到数组前部;本练习使用布尔数组记录每个元素是否已选。两种方法都能避免同一元素被重复选择,但代码结构不同
+1
View File
@@ -20,3 +20,4 @@ icon: material/map-marker-path
- [13.3 &nbsp; 子集和问题](subset_sum_problem.md)
- [13.4 &nbsp; N 皇后问题](n_queens_problem.md)
- [13.5 &nbsp; 小结](summary.md)
- [13.6 &nbsp; 练习](exercises.md)
@@ -0,0 +1,116 @@
---
comments: true
---
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
# 2.6 &nbsp; 练习
## 2.6.1 &nbsp; 知识巩固
### 1. &nbsp; 迭代与递归的时间和空间
下面两段代码都计算 $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)$。
分析空间复杂度时,除代码中的变量外,还要考虑递归调用占用的空间。
### 2. &nbsp; 三段代码的时间复杂度
以下三个代码片段的输入均为正整数 $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$,因此属于平方阶。
### 3. &nbsp; 哪种反转更节省空间
要将数组 `nums` 中的元素全部反转,有两种做法:
<!-- numbered-subquestions -->
1. 新建一个等长数组 `res`,倒序复制后返回;
2. 用两个索引 `i``j` 分别从首、尾向中间移动,逐对交换 `nums[i]``nums[j]`
两种做法的空间复杂度各是多少?哪种属于“原地”操作?
??? success "参考答案"
1. 需要与输入等长的辅助数组,空间复杂度 $O(n)$。
2. 只使用两个索引变量,
空间复杂度 $O(1)$ ,属于原地操作。
需要注意:原地反转会修改输入数组,
仅在允许修改输入时才应优先选用;若需保留原数组,第 1 种做法的复制开销不可避免。
## 2.6.2 &nbsp; 编程练习
### 1. &nbsp; 斐波那契数
斐波那契数列满足:$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" }
@@ -20,3 +20,4 @@ icon: material/timer-sand
- [2.3 &nbsp; 时间复杂度](time_complexity.md)
- [2.4 &nbsp; 空间复杂度](space_complexity.md)
- [2.5 &nbsp; 小结](summary.md)
- [2.6 &nbsp; 练习](exercises.md)
+92
View File
@@ -0,0 +1,92 @@
---
comments: true
---
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
# 3.6 &nbsp; 练习
## 3.6.1 &nbsp; 知识巩固
### 1. &nbsp; 生活场景中的数据关系
请根据数据之间的关系,为下面三个场景选择“线性结构、树形结构、网状结构”中的一种,并说明理由:
<!-- numbered-subquestions -->
1. 同学们排成一列,每人只关心自己前面和后面的人;
2. 学校按“学校 → 年级 → 班级”分层管理;
3. 城市道路连接多个路口,一个路口可以通向多个其他路口,也可能形成环路。
??? success "参考答案"
1. 是线性结构。除排在最前和最后的人外,每个人都只与前面和后面各一人相邻,关系沿一条线展开。
2. 是树形结构。每个班级属于一个年级,每个年级又属于学校,关系从上到下分层展开。
3. 是网状结构。一个路口可以连接多个路口,路线之间还能形成环,不能排成单一顺序或严格层级。
判断结构时应先观察元素之间的关系,而不是先考虑它在内存中占多少空间。
### 2. &nbsp; 逻辑顺序怎样存进内存
为了保存逻辑顺序 `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`
这也说明逻辑结构和物理结构是观察同一组数据的两个不同角度。
### 3. &nbsp; 作业记录中的数据类型与结构
某学习小组按座位顺序记录 4 名同学是否交了作业,得到:
`[true, false, true, true]`
<!-- numbered-subquestions -->
1. 每个元素适合使用哪种基本数据类型?
2. 这 4 个元素按座位顺序排成一列,使用了什么逻辑结构?
3. 如果以后改为记录每人的作业分数 `[90, 0, 85, 100]`,改变的是数据的“内容类型”还是“组织方式”?说明理由。
??? success "参考答案"
1. 每个元素只表示“是”或“否”,适合使用布尔类型 `bool`
2. 这些元素按座位顺序排列,形成线性结构,可以用数组保存。
3. 改变的是内容类型:元素由布尔值变成了整数。组织方式没有改变,
这些数据仍然按座位顺序排成一列,仍可使用数组这一线性结构。
基本数据类型描述“存的是什么”,数据结构描述“数据怎样组织”。
## 3.6.2 &nbsp; 编程练习
### 1. &nbsp; 统计二进制表示中的 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" }
+1
View File
@@ -20,3 +20,4 @@ icon: material/shape-outline
- [3.3 &nbsp; 数字编码 *](number_encoding.md)
- [3.4 &nbsp; 字符编码 *](character_encoding.md)
- [3.5 &nbsp; 小结](summary.md)
- [3.6 &nbsp; 练习](exercises.md)
@@ -0,0 +1,104 @@
---
comments: true
---
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
# 12.6 &nbsp; 练习
## 12.6.1 &nbsp; 知识巩固
### 1. &nbsp; 哪些任务适合分治
一位同学想用“先分成两半,分别解决,再合并结果”的方法完成以下任务。
请分别判断它们是“适合分治”“可以分治,但不会减少总工作量”还是“两半不能独立解决”,并说明理由。
<!-- numbered-subquestions -->
1. 将一个无序数组排序;
2. 求一个数组中的最大值;
3. 按顺序执行一串 `push(x)``pop()` 栈操作,并输出每次 `pop()` 得到的元素。
??? success "参考答案"
1. 适合:对半分解、两半独立排序、$O(n)$ 合并——这正是归并排序。
2. 可以分治,但不会减少总工作量:左右两半仍需合计检查全部 $n$ 个元素,
和直接扫描一样都是 $O(n)$ 时间。
3. 两半不能独立解决:后半段开始时的栈内容取决于前半段执行结果,
两半不能在互不知道结果时独立完成。
### 2. &nbsp; 快速幂是怎样减少计算的
下面的递归函数用分治计算 $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$;
调用两次会造成大量重复计算。
### 3. &nbsp; 从遍历序列拆分左右子树
一棵没有重复节点的二叉树,其前序遍历和中序遍历分别为:
- 前序遍历:`[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`
## 12.6.2 &nbsp; 编程练习
### 1. &nbsp; 快速幂
给定实数 `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" }
> **说明:** 链接中的题解使用迭代位运算,与本练习指定的递归分治方法不同;请根据本题的解题提示完成递归版本
+1
View File
@@ -20,3 +20,4 @@ icon: material/set-split
- [12.3 &nbsp; 构建树问题](build_binary_tree_problem.md)
- [12.4 &nbsp; 汉诺塔问题](hanota_problem.md)
- [12.5 &nbsp; 小结](summary.md)
- [12.6 &nbsp; 练习](exercises.md)
@@ -0,0 +1,119 @@
---
comments: true
---
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
# 14.8 &nbsp; 练习
## 14.8.1 &nbsp; 知识巩固
### 1. &nbsp; 什么时候适合使用动态规划
一位同学说:“只要能写出递推式,就应该使用动态规划。”
请判断下面三个任务分别更适合使用动态规划、回溯,还是使用循环或数学公式而不建立 `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` 表。“能写递推式”不等于“需要动态规划”。
### 2. &nbsp; 背包表中的一个格子怎么算
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 背包的一次“选或不选”比较。
### 3. &nbsp; 背包容量应该按什么顺序更新
一个 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]` 仍是处理当前物品之前的结果,
不会在同一轮中重复使用当前物品。
## 14.8.2 &nbsp; 编程练习
### 1. &nbsp; 爬楼梯的方案数
一段楼梯共有 `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 表,滚动变量仅作为选做优化
### 2. &nbsp; 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. 容量必须从大到小更新,避免在同一轮中重复选择当前物品
@@ -22,3 +22,4 @@ icon: material/table-pivot
- [14.5 &nbsp; 完全背包问题](unbounded_knapsack_problem.md)
- [14.6 &nbsp; 编辑距离问题](edit_distance_problem.md)
- [14.7 &nbsp; 小结](summary.md)
- [14.8 &nbsp; 练习](exercises.md)
+109
View File
@@ -0,0 +1,109 @@
---
comments: true
---
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
# 9.5 &nbsp; 练习
## 9.5.1 &nbsp; 知识巩固
### 1. &nbsp; 用两种方式表示同一张图
一个无向图有 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. 顶点很多但边很少时,邻接表只记录实际存在的边,通常比为每一对顶点都预留位置的邻接矩阵更节省空间。
### 2. &nbsp; 广度优先与深度优先访问顺序
一个无向图的顶点为 `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`。如果不记录已访问顶点,
遍历可能反复沿环访问同一批顶点,无法正常结束。
### 3. &nbsp; 一次 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 个连通分量。
## 9.5.2 &nbsp; 编程练习
### 1. &nbsp; 判断无向图中是否存在路径
给定一个含有 $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" }
+1
View File
@@ -19,3 +19,4 @@ icon: material/graphql
- [9.2 &nbsp; 图基础操作](graph_operations.md)
- [9.3 &nbsp; 图的遍历](graph_traversal.md)
- [9.4 &nbsp; 小结](summary.md)
- [9.5 &nbsp; 练习](exercises.md)
+89
View File
@@ -0,0 +1,89 @@
---
comments: true
---
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
# 15.6 &nbsp; 练习
## 15.6.1 &nbsp; 知识巩固
### 1. &nbsp; 每次选最大硬币一定最好吗
硬币面值为 `[1, 7, 10]`,目标金额为 14。
<!-- numbered-subquestions -->
1. 按“每次取不超过剩余金额的最大面值”执行,写出所取硬币;
2. 是否存在使用更少硬币的方案?如果存在,请写出一种;如果不存在,请说明理由;
3. 这能否说明该贪心策略对任意硬币面值都正确?
??? success "参考答案"
1. 贪心依次选择 `10 + 1 + 1 + 1 + 1`,共 5 枚硬币。
2. 存在使用更少硬币的方案:`7 + 7`,只需 2 枚硬币。
3. 不能。这个反例说明,对任意硬币面值,每次选择当前能取的最大面值并不一定得到最少硬币数;
眼前最大的选择可能破坏后续更好的组合。
### 2. &nbsp; 背包应该先装哪件物品
一个容量为 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。
### 3. &nbsp; 两个指针下一步怎样移动
隔板高度为 `[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. 移动较短隔板对应的指针,才仍有可能得到更大的容量。移动较长隔板后,两块隔板的距离一定缩短,而容器高度仍受未移动的短板限制,
只可能不变或变小。因此容量不可能超过移动前;只有移动短板,才有机会遇到更高的隔板。
## 15.6.2 &nbsp; 编程练习
### 1. &nbsp; 分数背包
给定等长数组 `wgt``val`,其中 `wgt[i] > 0``val[i] >= 0`,背包容量 `cap >= 0`
每种物品只有一件,但允许只装入其中一部分,
获得的价值按所装重量占该物品总重量的比例计算。请使用贪心算法,
并以实数返回背包能够获得的最大总价值。
??? tip "解题提示"
1. 先计算每件物品的单位重量价值 val[i] / wgt[i],除法结果要保留小数
2. 单位价值越高的物品越先装入背包
3. 若剩余容量小于当前物品重量,就装入恰好填满背包的部分并结束
+1
View File
@@ -20,3 +20,4 @@ icon: material/head-heart-outline
- [15.3 &nbsp; 最大容量问题](max_capacity_problem.md)
- [15.4 &nbsp; 最大切分乘积问题](max_product_cutting_problem.md)
- [15.5 &nbsp; 小结](summary.md)
- [15.6 &nbsp; 练习](exercises.md)
+107
View File
@@ -0,0 +1,107 @@
---
comments: true
---
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
# 6.5 &nbsp; 练习
## 6.5.1 &nbsp; 知识巩固
### 1. &nbsp; 发生哈希冲突后怎样查找
一个哈希表有 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 不会互相覆盖。
### 2. &nbsp; 哈希表扩容后元素去哪儿
一个使用链式地址的哈希表原有 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 号桶,从而找不到它们。
### 3. &nbsp; 删除 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),而以后的插入仍可重新使用这个位置。
## 6.5.2 &nbsp; 编程练习
### 1. &nbsp; 比较两个字符串的字符组成
给定两个只含小写英文字母的字符串 `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" }
+1
View File
@@ -19,3 +19,4 @@ icon: material/table-search
- [6.2 &nbsp; 哈希冲突](hash_collision.md)
- [6.3 &nbsp; 哈希算法](hash_algorithm.md)
- [6.4 &nbsp; 小结](summary.md)
- [6.5 &nbsp; 练习](exercises.md)
+94
View File
@@ -0,0 +1,94 @@
---
comments: true
---
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
# 8.5 &nbsp; 练习
## 8.5.1 &nbsp; 知识巩固
### 1. &nbsp; 数字 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 次。
### 2. &nbsp; 检查小顶堆的父子关系
数组 `[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`
所有父节点都不大于自己的孩子,因此现在满足小顶堆规则。
### 3. &nbsp; 用小顶堆保留最大的三个数
要从数据流 `[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 个数。
## 8.5.2 &nbsp; 编程练习
### 1. &nbsp; 寻找数组中的第 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 的小顶堆完成
+1
View File
@@ -19,3 +19,4 @@ icon: material/family-tree
- [8.2 &nbsp; 建堆操作](build_heap.md)
- [8.3 &nbsp; Top-k 问题](top_k.md)
- [8.4 &nbsp; 小结](summary.md)
- [8.5 &nbsp; 练习](exercises.md)
+101
View File
@@ -0,0 +1,101 @@
---
comments: true
---
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
# 10.7 &nbsp; 练习
## 10.7.1 &nbsp; 知识巩固
### 1. &nbsp; 二分查找怎样缩小搜索区间
在有序数组 `[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 |
数组有序,所以中点值小于目标时可以排除中点及其左侧;
中点值大于目标时可以排除中点及其右侧。
### 2. &nbsp; 重复元素的左右边界
在数组 `[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`
### 3. &nbsp; 不同数据该选哪种搜索方法
请在“顺序查找、二分查找、哈希表”中,为下面三个场景选择合适的方法,并说明理由:
<!-- numbered-subquestions -->
1. 在 $10^7$ 个已经有序且不再变动的整数中反复查找,不额外建立其他数据结构;
2. 在频繁插入、删除的数据集中反复判断某个键是否存在,不要求保持有序,也不进行范围查找;
3. 在无序数组中只查找一次某个值。
??? success "参考答案"
1. 二分查找:数据有序且静态,$O(\log n)$ 且无须额外空间。
2. 哈希表:当哈希函数能把键较均匀地分散到各桶时,插入、删除和按键查找的平均时间复杂度都是 $O(1)$。
3. 直接从头到尾遍历:只查找一次时,排序或建立哈希表都要先处理整个数组,
并不会减少这一次任务的总工作量。
选择取决于数据是否有序、是否允许建立额外结构、查询次数以及需要执行哪些操作。
## 10.7.2 &nbsp; 编程练习
### 1. &nbsp; 有序数组的二分查找
给定一个按严格递增顺序排列的整数数组 `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" }
### 2. &nbsp; 有序数组的插入位置
给定一个按严格递增顺序排列的整数数组 `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" }
+1
View File
@@ -21,3 +21,4 @@ icon: material/text-search
- [10.4 &nbsp; 哈希优化策略](replace_linear_by_hashing.md)
- [10.5 &nbsp; 重识搜索算法](searching_algorithm_revisited.md)
- [10.6 &nbsp; 小结](summary.md)
- [10.7 &nbsp; 练习](exercises.md)
+100
View File
@@ -0,0 +1,100 @@
---
comments: true
---
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
# 11.12 &nbsp; 练习
## 11.12.1 &nbsp; 知识巩固
### 1. &nbsp; 选择排序和冒泡排序的前几轮
给定数组 `[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. &nbsp; 相同元素的先后次序会变吗
数组 $[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. 这个例子中,选择排序会改变相等元素的原有次序;冒泡排序只在左边元素大于右边元素时
交换相邻元素。相等元素之间不交换,因此能保持它们原有的先后次序。
### 3. &nbsp; 比较计数排序和基数排序
学校要排序许多固定为 8 位的学号。请回答:
<!-- numbered-subquestions -->
1. 从最低位开始的基数排序需要处理几轮?
2. 如果把学号直接当作整数使用计数排序,为什么会准备大量没有用到的计数位置?
3. 根据前两问,如果在计数排序和基数排序之间选择一种处理大量固定为 8 位的学号,你会选择哪一种?为什么?
??? success "参考答案"
1. 学号有 8 个数位,因此从最低位到最高位共处理 8 轮;每轮只按 0~9 分组。
2. 直接计数需要为所有可能的 8 位取值预留位置,但实际学生只使用其中很少一部分,
大多数计数位置会一直为 0。
3. 应选择基数排序。它利用了“位数固定、每位只有 10 种取值”的特点,只需重复 8 轮稳定分组。
计数排序若把整个 8 位学号当作整数下标,则需要为大量实际不会出现的数值预留计数位置。
## 11.12.2 &nbsp; 编程练习
### 1. &nbsp; 用归并排序排列数组
给定一个整数数组 `nums`,请自行实现归并排序,把其中的元素按非递减顺序排列并返回。不要调用语言自带的排序函数。
??? tip "解题提示"
1. 区间长度不超过 1 时,它已经有序
2. 从中点把区间分成两半,并分别递归排序
3. 用两个指针合并两个有序半区,再把结果写回原数组
[LeetCode](https://leetcode.cn/problems/sort-an-array/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }
### 2. &nbsp; 用计数排序排列整数数组
给定整数数组 `nums` 和非负整数 $K$,数组中的每个元素都在 $0$ 到 $K$ 之间。
请实现计数排序,把结果按非递减顺序写回 `nums` 并返回 `nums`
不要使用元素之间的大小比较来决定顺序,也不要调用语言自带的排序函数。
??? tip "解题提示"
1. 因为每个元素都在 0 到 K 之间,可以直接用元素值作为计数数组的索引
2. 第一遍扫描 nums,令对应位置的计数加 1
3. 再从 0 到 K 扫描计数数组;数值 x 出现多少次,就向 nums 中连续写入多少个 x
+1
View File
@@ -26,3 +26,4 @@ icon: material/sort-ascending
- [11.9 &nbsp; 计数排序](counting_sort.md)
- [11.10 &nbsp; 基数排序](radix_sort.md)
- [11.11 &nbsp; 小结](summary.md)
- [11.12 &nbsp; 练习](exercises.md)
+105
View File
@@ -0,0 +1,105 @@
---
comments: true
---
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
# 5.5 &nbsp; 练习
## 5.5.1 &nbsp; 知识巩固
### 1. &nbsp; 栈和队列会先取出谁
准备一个空栈 `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`,体现“先入先出”。
### 2. &nbsp; 队尾越过数组末尾怎么办
用长度为 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`,环形数组用取余让索引回到开头,
因此无须把其他元素整体向前移动。
### 3. &nbsp; 双向队列的两端操作
这里规定:`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 步操作。
## 5.5.2 &nbsp; 编程练习
### 1. &nbsp; 检查括号序列
给定一个只包含 `()``[]``{}` 这三类括号的字符串 `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" }
+1
View File
@@ -19,3 +19,4 @@ icon: material/stack-overflow
- [5.2 &nbsp; 队列](queue.md)
- [5.3 &nbsp; 双向队列](deque.md)
- [5.4 &nbsp; 小结](summary.md)
- [5.5 &nbsp; 练习](exercises.md)
+132
View File
@@ -0,0 +1,132 @@
---
comments: true
---
<!-- 此文件由 utils/exercises/publish_exercises.py 根据 exercises.yaml 自动生成,请勿直接修改。 -->
# 7.7 &nbsp; 练习
## 7.7.1 &nbsp; 知识巩固
### 1. &nbsp; 完全、完满与完美二叉树
下面两个数组按层序表示二叉树,`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. 两棵都不是完美二叉树,因为它们的最底层都没有完全填满。
### 2. &nbsp; 同一棵树的三种遍历顺序
将数组 `[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` 是右子树的中序遍历序列。
### 3. &nbsp; 比较两棵二叉搜索树
将下面两组序列从左到右依次插入空的二叉搜索树:
- 序列 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 个节点;树越高,最坏情况下需要沿路径比较的节点就越多。
## 7.7.2 &nbsp; 编程练习
### 1. &nbsp; 二叉树的最大深度
给定一棵二叉树的根节点 `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" }
### 2. &nbsp; 按层遍历二叉树
给定一棵二叉树的根节点 `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" }
### 3. &nbsp; 二叉搜索树中的第 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" }
+1
View File
@@ -21,3 +21,4 @@ icon: material/graph-outline
- [7.4 &nbsp; 二叉搜索树](binary_search_tree.md)
- [7.5 &nbsp; AVL 树 *](avl_tree.md)
- [7.6 &nbsp; 小结](summary.md)
- [7.7 &nbsp; 练习](exercises.md)