diff --git a/docs/chapter_array_and_linkedlist/exercises.md b/docs/chapter_array_and_linkedlist/exercises.md new file mode 100644 index 000000000..2e31541aa --- /dev/null +++ b/docs/chapter_array_and_linkedlist/exercises.md @@ -0,0 +1,112 @@ +--- +comments: true +--- + + + +# 4.6   练习 + +## 4.6.1   知识巩固 + +### 1.   数组和链表怎样找到元素 + +数组和单向链表中都按顺序保存了 `[A, B, C, D, E]`。现在要读取第 4 个元素 `D`: + + + +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.   数组和链表怎样插入元素 + +数组和单向链表中都保存了 `A、B、C、D`。现在要在 `B` 后面插入 `X`: + +- 数组容量为 5,当前状态是 `[A, B, C, D, _]`; +- 链表为 `A → B → C → D`,并且已经拿到了指向节点 `B` 的引用。 + + + +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.   列表容量是怎样增长的 + +一个基于数组实现的列表当前内容为 `[A, B, C]`,长度 `size = 3`,容量 `capacity = 4`。 +规定容量不足时,新数组的容量扩大为原来的 2 倍。 + + + +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   编程练习 + +### 1.   数组表示的大整数加一 + +数组 `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.   反转单向链表 + +给定一个单向链表的头节点 `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" } diff --git a/docs/chapter_array_and_linkedlist/index.md b/docs/chapter_array_and_linkedlist/index.md index a1cdfd0b8..d7bc1001d 100644 --- a/docs/chapter_array_and_linkedlist/index.md +++ b/docs/chapter_array_and_linkedlist/index.md @@ -20,3 +20,4 @@ icon: material/view-list-outline - [4.3   列表](list.md) - [4.4   内存与缓存 *](ram_and_cache.md) - [4.5   小结](summary.md) +- [4.6   练习](exercises.md) diff --git a/docs/chapter_backtracking/exercises.md b/docs/chapter_backtracking/exercises.md new file mode 100644 index 000000000..a7b3b4f7d --- /dev/null +++ b/docs/chapter_backtracking/exercises.md @@ -0,0 +1,95 @@ +--- +comments: true +--- + + + +# 13.6   练习 + +## 13.6.1   知识巩固 + +### 1.   这段排列算法会漏掉结果吗 + +一个回溯算法按 `1、2、3` 的顺序尝试生成全部排列。每次选择数字 `x` 时,它会: + +1. 把 `x` 加到当前路径末尾; +2. 把 `x` 标记为“已使用”; +3. 递归填写下一个位置。 + +递归返回后,同学只从路径末尾删除了 `x`,然后继续尝试下一个数字。 + + + +1. 算法首先得到哪个排列?它还能得到全部 6 个排列吗? +2. 递归返回上一层前,只删除路径末尾的数字是否足够?如果不够,还需要做什么?说明理由。 + +??? success "参考答案" + + 1. 它首先得到 `[1, 2, 3]`,但无法得到全部排列。虽然返回时路径变短了, + 数字 1、2、3 的标记仍都是“已使用”,后续分支便没有可选数字。 + + 2. 不够。删除路径末尾的 `x` 后,还必须把 `x` 重新标记为“未使用”。 + 当前路径和已使用标记共同描述搜索状态;选择时修改了两处,回退时也必须把两处都恢复, + 其他分支才能再次选择 `x`。 + +### 2.   数字的选择顺序重要吗 + +给定排好序的数组 `[2, 3, 5]` 和目标值 5,每个数可以重复选择。 +算法规定每条搜索路径中的数字只能按从小到大的顺序出现。 + + + +1. 能得到哪些不同的组合? +2. 为什么同一组数字不需要按不同顺序重复搜索?“从小到大”的限制起到了什么作用? +3. 当前路径为 `[3]`、还差 2 时,下一个候选数是 3。为什么此时可以停止检查这一层后面的所有候选数? + +??? success "参考答案" + + 1. 不同的组合为 `[2, 3]` 和 `[5]`。 + + 2. 本题把 `[2, 3]` 和 `[3, 2]` 看作同一种组合,数字的选择顺序不计入答案。 + 规定路径中的数字从小到大出现,就能在搜索时直接跳过 `[3, 2]` 这类重复组合。 + + 3. 当前还差 2,而候选数 3 已经大于 2。因为数组已经排好序, + 3 后面的候选数只会更大,也都不可能加入当前组合,所以可以直接结束这一层的检查。 + +### 3.   下一枚皇后可以放在哪些位置 + +在一个 `4 × 4` 棋盘上按行放置皇后,行、列下标都从 0 开始。 +目前已经在 `(0, 1)` 和 `(1, 3)` 放置了皇后,现在要在第 2 行放置下一个皇后。 + + + +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   编程练习 + +### 1.   无重复元素的全排列 + +整数数组 `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" } + +> **说明:** 链接题解通过交换数组元素,把已选元素依次放到数组前部;本练习使用布尔数组记录每个元素是否已选。两种方法都能避免同一元素被重复选择,但代码结构不同 diff --git a/docs/chapter_backtracking/index.md b/docs/chapter_backtracking/index.md index d6c5e1936..a90f895d8 100644 --- a/docs/chapter_backtracking/index.md +++ b/docs/chapter_backtracking/index.md @@ -20,3 +20,4 @@ icon: material/map-marker-path - [13.3   子集和问题](subset_sum_problem.md) - [13.4   N 皇后问题](n_queens_problem.md) - [13.5   小结](summary.md) +- [13.6   练习](exercises.md) diff --git a/docs/chapter_computational_complexity/exercises.md b/docs/chapter_computational_complexity/exercises.md new file mode 100644 index 000000000..91eafdb9e --- /dev/null +++ b/docs/chapter_computational_complexity/exercises.md @@ -0,0 +1,116 @@ +--- +comments: true +--- + + + +# 2.6   练习 + +## 2.6.1   知识巩固 + +### 1.   迭代与递归的时间和空间 + +下面两段代码都计算 $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) +``` + + + +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.   三段代码的时间复杂度 + +以下三个代码片段的输入均为正整数 $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.   哪种反转更节省空间 + +要将数组 `nums` 中的元素全部反转,有两种做法: + + + +1. 新建一个等长数组 `res`,倒序复制后返回; +2. 用两个索引 `i` 和 `j` 分别从首、尾向中间移动,逐对交换 `nums[i]` 与 `nums[j]` 。 + + 两种做法的空间复杂度各是多少?哪种属于“原地”操作? + +??? success "参考答案" + + 1. 需要与输入等长的辅助数组,空间复杂度 $O(n)$。 + + 2. 只使用两个索引变量, + 空间复杂度 $O(1)$ ,属于原地操作。 + + 需要注意:原地反转会修改输入数组, + 仅在允许修改输入时才应优先选用;若需保留原数组,第 1 种做法的复制开销不可避免。 + +## 2.6.2   编程练习 + +### 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" } diff --git a/docs/chapter_computational_complexity/index.md b/docs/chapter_computational_complexity/index.md index b74410df5..76dca8387 100644 --- a/docs/chapter_computational_complexity/index.md +++ b/docs/chapter_computational_complexity/index.md @@ -20,3 +20,4 @@ icon: material/timer-sand - [2.3   时间复杂度](time_complexity.md) - [2.4   空间复杂度](space_complexity.md) - [2.5   小结](summary.md) +- [2.6   练习](exercises.md) diff --git a/docs/chapter_data_structure/exercises.md b/docs/chapter_data_structure/exercises.md new file mode 100644 index 000000000..4862d8d69 --- /dev/null +++ b/docs/chapter_data_structure/exercises.md @@ -0,0 +1,92 @@ +--- +comments: true +--- + + + +# 3.6   练习 + +## 3.6.1   知识巩固 + +### 1.   生活场景中的数据关系 + +请根据数据之间的关系,为下面三个场景选择“线性结构、树形结构、网状结构”中的一种,并说明理由: + + + +1. 同学们排成一列,每人只关心自己前面和后面的人; +2. 学校按“学校 → 年级 → 班级”分层管理; +3. 城市道路连接多个路口,一个路口可以通向多个其他路口,也可能形成环路。 + +??? success "参考答案" + + 1. 是线性结构。除排在最前和最后的人外,每个人都只与前面和后面各一人相邻,关系沿一条线展开。 + + 2. 是树形结构。每个班级属于一个年级,每个年级又属于学校,关系从上到下分层展开。 + + 3. 是网状结构。一个路口可以连接多个路口,路线之间还能形成环,不能排成单一顺序或严格层级。 + + 判断结构时应先观察元素之间的关系,而不是先考虑它在内存中占多少空间。 + +### 2.   逻辑顺序怎样存进内存 + +为了保存逻辑顺序 `A → B → C`,现有两种简化的内存安排: + +- 方案甲:`A、B、C` 分别放在编号为 `20、21、22` 的内存格中; +- 方案乙:`A、B、C` 分别放在编号为 `20、7、31` 的内存格中,并由 `A` 记录 `B` 的位置、`B` 记录 `C` 的位置。 + + + +1. 哪个方案属于连续空间存储?哪个属于分散空间存储? +2. 两个方案分别更接近数组还是链表? +3. 方案乙的内存格编号没有按大小排列,为什么仍能表示 `A → B → C` 的逻辑顺序? + +??? success "参考答案" + + 1. 方案甲使用连续的内存格,属于连续空间存储;方案乙的节点分散在不同位置,属于分散空间存储。 + + 2. 方案甲更接近数组,方案乙更接近链表。 + + 3. 逻辑顺序由节点之间记录的连接关系决定,而不是由内存格编号的大小决定。 + 从 `A` 记录的位置可以找到 `B`,再从 `B` 记录的位置找到 `C`,所以仍能依次访问 `A、B、C`。 + + 这也说明逻辑结构和物理结构是观察同一组数据的两个不同角度。 + +### 3.   作业记录中的数据类型与结构 + +某学习小组按座位顺序记录 4 名同学是否交了作业,得到: + +`[true, false, true, true]` + + + +1. 每个元素适合使用哪种基本数据类型? +2. 这 4 个元素按座位顺序排成一列,使用了什么逻辑结构? +3. 如果以后改为记录每人的作业分数 `[90, 0, 85, 100]`,改变的是数据的“内容类型”还是“组织方式”?说明理由。 + +??? success "参考答案" + + 1. 每个元素只表示“是”或“否”,适合使用布尔类型 `bool`。 + + 2. 这些元素按座位顺序排列,形成线性结构,可以用数组保存。 + + 3. 改变的是内容类型:元素由布尔值变成了整数。组织方式没有改变, + 这些数据仍然按座位顺序排成一列,仍可使用数组这一线性结构。 + + 基本数据类型描述“存的是什么”,数据结构描述“数据怎样组织”。 + +## 3.6.2   编程练习 + +### 1.   统计二进制表示中的 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" } diff --git a/docs/chapter_data_structure/index.md b/docs/chapter_data_structure/index.md index e64a926c7..088e21022 100644 --- a/docs/chapter_data_structure/index.md +++ b/docs/chapter_data_structure/index.md @@ -20,3 +20,4 @@ icon: material/shape-outline - [3.3   数字编码 *](number_encoding.md) - [3.4   字符编码 *](character_encoding.md) - [3.5   小结](summary.md) +- [3.6   练习](exercises.md) diff --git a/docs/chapter_divide_and_conquer/exercises.md b/docs/chapter_divide_and_conquer/exercises.md new file mode 100644 index 000000000..7b3e2b305 --- /dev/null +++ b/docs/chapter_divide_and_conquer/exercises.md @@ -0,0 +1,104 @@ +--- +comments: true +--- + + + +# 12.6   练习 + +## 12.6.1   知识巩固 + +### 1.   哪些任务适合分治 + +一位同学想用“先分成两半,分别解决,再合并结果”的方法完成以下任务。 +请分别判断它们是“适合分治”“可以分治,但不会减少总工作量”还是“两半不能独立解决”,并说明理由。 + + + +1. 将一个无序数组排序; +2. 求一个数组中的最大值; +3. 按顺序执行一串 `push(x)`、`pop()` 栈操作,并输出每次 `pop()` 得到的元素。 + +??? success "参考答案" + + 1. 适合:对半分解、两半独立排序、$O(n)$ 合并——这正是归并排序。 + 2. 可以分治,但不会减少总工作量:左右两半仍需合计检查全部 $n$ 个元素, + 和直接扫描一样都是 $O(n)$ 时间。 + 3. 两半不能独立解决:后半段开始时的栈内容取决于前半段执行结果, + 两半不能在互不知道结果时独立完成。 + +### 2.   快速幂是怎样减少计算的 + +下面的递归函数用分治计算 $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)`: + + + +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.   从遍历序列拆分左右子树 + +一棵没有重复节点的二叉树,其前序遍历和中序遍历分别为: + +- 前序遍历:`[A, B, D, E, C]` +- 中序遍历:`[D, B, E, A, C]` + +只完成根节点这一层的拆分,不必继续递归,也不必画出整棵树: + + + +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   编程练习 + +### 1.   快速幂 + +给定实数 `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" } + +> **说明:** 链接中的题解使用迭代位运算,与本练习指定的递归分治方法不同;请根据本题的解题提示完成递归版本 diff --git a/docs/chapter_divide_and_conquer/index.md b/docs/chapter_divide_and_conquer/index.md index 71d90b257..5e53fc595 100644 --- a/docs/chapter_divide_and_conquer/index.md +++ b/docs/chapter_divide_and_conquer/index.md @@ -20,3 +20,4 @@ icon: material/set-split - [12.3   构建树问题](build_binary_tree_problem.md) - [12.4   汉诺塔问题](hanota_problem.md) - [12.5   小结](summary.md) +- [12.6   练习](exercises.md) diff --git a/docs/chapter_dynamic_programming/exercises.md b/docs/chapter_dynamic_programming/exercises.md new file mode 100644 index 000000000..859ee65b0 --- /dev/null +++ b/docs/chapter_dynamic_programming/exercises.md @@ -0,0 +1,119 @@ +--- +comments: true +--- + + + +# 14.8   练习 + +## 14.8.1   知识巩固 + +### 1.   什么时候适合使用动态规划 + +一位同学说:“只要能写出递推式,就应该使用动态规划。” +请判断下面三个任务分别更适合使用动态规划、回溯,还是使用循环或数学公式而不建立 `dp` 表,并写出一个关键理由。 + + + +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.   背包表中的一个格子怎么算 + +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`: + + + +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.   背包容量应该按什么顺序更新 + +一个 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`。 + + + +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   编程练习 + +### 1.   爬楼梯的方案数 + +一段楼梯共有 `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.   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. 容量必须从大到小更新,避免在同一轮中重复选择当前物品 diff --git a/docs/chapter_dynamic_programming/index.md b/docs/chapter_dynamic_programming/index.md index 12b9d350d..631e7dbfd 100644 --- a/docs/chapter_dynamic_programming/index.md +++ b/docs/chapter_dynamic_programming/index.md @@ -22,3 +22,4 @@ icon: material/table-pivot - [14.5   完全背包问题](unbounded_knapsack_problem.md) - [14.6   编辑距离问题](edit_distance_problem.md) - [14.7   小结](summary.md) +- [14.8   练习](exercises.md) diff --git a/docs/chapter_graph/exercises.md b/docs/chapter_graph/exercises.md new file mode 100644 index 000000000..28543e652 --- /dev/null +++ b/docs/chapter_graph/exercises.md @@ -0,0 +1,109 @@ +--- +comments: true +--- + + + +# 9.5   练习 + +## 9.5.1   知识巩固 + +### 1.   用两种方式表示同一张图 + +一个无向图有 4 个顶点 `A、B、C、D`,边为 +`A-B、A-C、B-C、C-D`。 + + + +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.   广度优先与深度优先访问顺序 + +一个无向图的顶点为 `A、B、C、D、E`,边为 +`A-B、A-C、B-D、C-D、D-E`。 + +从 A 出发,并规定遇到多个未访问邻接顶点时按字母顺序选择: + + + +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.   一次 BFS 能访问整张图吗 + +一个无向图有顶点 `A、B、C、D、E、F`,边只有 +`A-B、B-C、D-E`。 + + + +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   编程练习 + +### 1.   判断无向图中是否存在路径 + +给定一个含有 $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" } diff --git a/docs/chapter_graph/index.md b/docs/chapter_graph/index.md index 8abb5b8a9..0efa5262b 100644 --- a/docs/chapter_graph/index.md +++ b/docs/chapter_graph/index.md @@ -19,3 +19,4 @@ icon: material/graphql - [9.2   图基础操作](graph_operations.md) - [9.3   图的遍历](graph_traversal.md) - [9.4   小结](summary.md) +- [9.5   练习](exercises.md) diff --git a/docs/chapter_greedy/exercises.md b/docs/chapter_greedy/exercises.md new file mode 100644 index 000000000..d2bea9289 --- /dev/null +++ b/docs/chapter_greedy/exercises.md @@ -0,0 +1,89 @@ +--- +comments: true +--- + + + +# 15.6   练习 + +## 15.6.1   知识巩固 + +### 1.   每次选最大硬币一定最好吗 + +硬币面值为 `[1, 7, 10]`,目标金额为 14。 + + + +1. 按“每次取不超过剩余金额的最大面值”执行,写出所取硬币; +2. 是否存在使用更少硬币的方案?如果存在,请写出一种;如果不存在,请说明理由; +3. 这能否说明该贪心策略对任意硬币面值都正确? + +??? success "参考答案" + + 1. 贪心依次选择 `10 + 1 + 1 + 1 + 1`,共 5 枚硬币。 + + 2. 存在使用更少硬币的方案:`7 + 7`,只需 2 枚硬币。 + + 3. 不能。这个反例说明,对任意硬币面值,每次选择当前能取的最大面值并不一定得到最少硬币数; + 眼前最大的选择可能破坏后续更好的组合。 + +### 2.   背包应该先装哪件物品 + +一个容量为 4 千克的背包中可以装入以下物品,物品允许只取一部分, +所取价值与重量成正比: + +- 物品 A:重量 4 千克,价值 20; +- 物品 B:重量 3 千克,价值 18。 + + + +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.   两个指针下一步怎样移动 + +隔板高度为 `[1, 8, 6, 2, 5]`,用首尾双指针寻找最大容量。 +容量等于“两块隔板中较短者的高度 × 两块隔板的下标距离”。 + + + +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   编程练习 + +### 1.   分数背包 + +给定等长数组 `wgt` 和 `val`,其中 `wgt[i] > 0`、`val[i] >= 0`,背包容量 `cap >= 0`。 +每种物品只有一件,但允许只装入其中一部分, +获得的价值按所装重量占该物品总重量的比例计算。请使用贪心算法, +并以实数返回背包能够获得的最大总价值。 + +??? tip "解题提示" + + 1. 先计算每件物品的单位重量价值 val[i] / wgt[i],除法结果要保留小数 + 2. 单位价值越高的物品越先装入背包 + 3. 若剩余容量小于当前物品重量,就装入恰好填满背包的部分并结束 diff --git a/docs/chapter_greedy/index.md b/docs/chapter_greedy/index.md index 020b72e30..824957cdb 100644 --- a/docs/chapter_greedy/index.md +++ b/docs/chapter_greedy/index.md @@ -20,3 +20,4 @@ icon: material/head-heart-outline - [15.3   最大容量问题](max_capacity_problem.md) - [15.4   最大切分乘积问题](max_product_cutting_problem.md) - [15.5   小结](summary.md) +- [15.6   练习](exercises.md) diff --git a/docs/chapter_hashing/exercises.md b/docs/chapter_hashing/exercises.md new file mode 100644 index 000000000..43f3baae9 --- /dev/null +++ b/docs/chapter_hashing/exercises.md @@ -0,0 +1,107 @@ +--- +comments: true +--- + + + +# 6.5   练习 + +## 6.5.1   知识巩固 + +### 1.   发生哈希冲突后怎样查找 + +一个哈希表有 5 个桶,哈希函数为 $h(x)=x \bmod 5$,冲突时把元素依次放进该桶的列表中。 +依次插入 `[1, 6, 11, 7]`: + + + +1. 写出 0~4 号桶中的内容; +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.   哈希表扩容后元素去哪儿 + +一个使用链式地址的哈希表原有 5 个桶,哈希函数为 $h(x)=x\bmod5$。 +键 `[1, 6, 11]` 都在 1 号桶中。 + +现在把哈希表扩容为 7 个桶,哈希函数相应变为 $h(x)=x\bmod7$: + + + +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.   删除 6 后还能找到 11 吗 + +一个哈希表有 5 个位置,索引为 `0~4`,哈希函数为 $h(x)=x\bmod5$。 +发生冲突时,从哈希函数算出的索引开始,向右寻找第一个空位。 + +依次插入 `[1, 6, 11]`: + + + +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   编程练习 + +### 1.   比较两个字符串的字符组成 + +给定两个只含小写英文字母的字符串 `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" } diff --git a/docs/chapter_hashing/index.md b/docs/chapter_hashing/index.md index 3ddfc0ea0..2f07479b6 100644 --- a/docs/chapter_hashing/index.md +++ b/docs/chapter_hashing/index.md @@ -19,3 +19,4 @@ icon: material/table-search - [6.2   哈希冲突](hash_collision.md) - [6.3   哈希算法](hash_algorithm.md) - [6.4   小结](summary.md) +- [6.5   练习](exercises.md) diff --git a/docs/chapter_heap/exercises.md b/docs/chapter_heap/exercises.md new file mode 100644 index 000000000..dd5ab0821 --- /dev/null +++ b/docs/chapter_heap/exercises.md @@ -0,0 +1,94 @@ +--- +comments: true +--- + + + +# 8.5   练习 + +## 8.5.1   知识巩固 + +### 1.   数字 10 入堆后怎样调整 + +数组 `[9, 7, 8, 3, 5]` 表示一个大顶堆。现在将数字 10 入堆。 + + + +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.   检查小顶堆的父子关系 + +数组 `[1, 4, 3, 7, 6, 2]` 表示一棵完全二叉树。小顶堆要求每个父节点都不大于它的孩子。 +对索引 $i$,左、右孩子索引分别为 $2i+1$ 和 $2i+2$。 + + + +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.   用小顶堆保留最大的三个数 + +要从数据流 `[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   编程练习 + +### 1.   寻找数组中的第 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 的小顶堆完成 diff --git a/docs/chapter_heap/index.md b/docs/chapter_heap/index.md index d72e2aebe..a0203f06d 100644 --- a/docs/chapter_heap/index.md +++ b/docs/chapter_heap/index.md @@ -19,3 +19,4 @@ icon: material/family-tree - [8.2   建堆操作](build_heap.md) - [8.3   Top-k 问题](top_k.md) - [8.4   小结](summary.md) +- [8.5   练习](exercises.md) diff --git a/docs/chapter_searching/exercises.md b/docs/chapter_searching/exercises.md new file mode 100644 index 000000000..81ed8b86c --- /dev/null +++ b/docs/chapter_searching/exercises.md @@ -0,0 +1,101 @@ +--- +comments: true +--- + + + +# 10.7   练习 + +## 10.7.1   知识巩固 + +### 1.   二分查找怎样缩小搜索区间 + +在有序数组 `[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.   重复元素的左右边界 + +在数组 `[1, 2, 2, 2, 4, 6]` 中查找数字 2。 +一名同学用二分查找在索引 2 找到目标后立即返回,并说: +“索引 2 就是数字 2 的左边界。” + + + +1. 这名同学的说法是否正确?数字 2 的左、右边界分别是多少?请说明理由。 +2. 查找左边界时,若中点元素等于目标,接下来应继续搜索哪一侧? +3. 查找右边界时又应继续搜索哪一侧?只需说明方向,无须写出完整的查找过程。 + +??? success "参考答案" + + 1. 这名同学的说法不正确。找到一个 2 后立即返回,只能保证找到某一个 2,不能保证找到最左或最右的 2。 + 本题的左边界为索引 1,右边界为索引 3。 + + 2. 查找左边界时,即使中点元素等于 2,也要继续搜索左侧, + 例如在双闭区间写法中令 `j = m - 1`。 + + 3. 查找右边界时,中点元素等于 2 后应继续搜索右侧, + 例如令 `i = m + 1`。 + +### 3.   不同数据该选哪种搜索方法 + +请在“顺序查找、二分查找、哈希表”中,为下面三个场景选择合适的方法,并说明理由: + + + +1. 在 $10^7$ 个已经有序且不再变动的整数中反复查找,不额外建立其他数据结构; +2. 在频繁插入、删除的数据集中反复判断某个键是否存在,不要求保持有序,也不进行范围查找; +3. 在无序数组中只查找一次某个值。 + +??? success "参考答案" + + 1. 二分查找:数据有序且静态,$O(\log n)$ 且无须额外空间。 + 2. 哈希表:当哈希函数能把键较均匀地分散到各桶时,插入、删除和按键查找的平均时间复杂度都是 $O(1)$。 + 3. 直接从头到尾遍历:只查找一次时,排序或建立哈希表都要先处理整个数组, + 并不会减少这一次任务的总工作量。 + + 选择取决于数据是否有序、是否允许建立额外结构、查询次数以及需要执行哪些操作。 + +## 10.7.2   编程练习 + +### 1.   有序数组的二分查找 + +给定一个按严格递增顺序排列的整数数组 `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.   有序数组的插入位置 + +给定一个按严格递增顺序排列的整数数组 `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" } diff --git a/docs/chapter_searching/index.md b/docs/chapter_searching/index.md index d2e9d75b6..a5522a718 100644 --- a/docs/chapter_searching/index.md +++ b/docs/chapter_searching/index.md @@ -21,3 +21,4 @@ icon: material/text-search - [10.4   哈希优化策略](replace_linear_by_hashing.md) - [10.5   重识搜索算法](searching_algorithm_revisited.md) - [10.6   小结](summary.md) +- [10.7   练习](exercises.md) diff --git a/docs/chapter_sorting/exercises.md b/docs/chapter_sorting/exercises.md new file mode 100644 index 000000000..2ab0cbedf --- /dev/null +++ b/docs/chapter_sorting/exercises.md @@ -0,0 +1,100 @@ +--- +comments: true +--- + + + +# 11.12   练习 + +## 11.12.1   知识巩固 + +### 1.   选择排序和冒泡排序的前几轮 + +给定数组 `[4, 2, 5, 1, 3]`,以下都按从小到大排序。 + + + +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.   相同元素的先后次序会变吗 + +数组 $[2_a, 2_b, 1]$ 中,$2_a$ 和 $2_b$ 的数值相同,但用下标标出了原有次序。 + + + +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.   比较计数排序和基数排序 + +学校要排序许多固定为 8 位的学号。请回答: + + + +1. 从最低位开始的基数排序需要处理几轮? +2. 如果把学号直接当作整数使用计数排序,为什么会准备大量没有用到的计数位置? +3. 根据前两问,如果在计数排序和基数排序之间选择一种处理大量固定为 8 位的学号,你会选择哪一种?为什么? + +??? success "参考答案" + + 1. 学号有 8 个数位,因此从最低位到最高位共处理 8 轮;每轮只按 0~9 分组。 + + 2. 直接计数需要为所有可能的 8 位取值预留位置,但实际学生只使用其中很少一部分, + 大多数计数位置会一直为 0。 + + 3. 应选择基数排序。它利用了“位数固定、每位只有 10 种取值”的特点,只需重复 8 轮稳定分组。 + 计数排序若把整个 8 位学号当作整数下标,则需要为大量实际不会出现的数值预留计数位置。 + +## 11.12.2   编程练习 + +### 1.   用归并排序排列数组 + +给定一个整数数组 `nums`,请自行实现归并排序,把其中的元素按非递减顺序排列并返回。不要调用语言自带的排序函数。 + +??? tip "解题提示" + + 1. 区间长度不超过 1 时,它已经有序 + 2. 从中点把区间分成两半,并分别递归排序 + 3. 用两个指针合并两个有序半区,再把结果写回原数组 + +[LeetCode](https://leetcode.cn/problems/sort-an-array/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } + +### 2.   用计数排序排列整数数组 + +给定整数数组 `nums` 和非负整数 $K$,数组中的每个元素都在 $0$ 到 $K$ 之间。 + +请实现计数排序,把结果按非递减顺序写回 `nums` 并返回 `nums`。 +不要使用元素之间的大小比较来决定顺序,也不要调用语言自带的排序函数。 + +??? tip "解题提示" + + 1. 因为每个元素都在 0 到 K 之间,可以直接用元素值作为计数数组的索引 + 2. 第一遍扫描 nums,令对应位置的计数加 1 + 3. 再从 0 到 K 扫描计数数组;数值 x 出现多少次,就向 nums 中连续写入多少个 x diff --git a/docs/chapter_sorting/index.md b/docs/chapter_sorting/index.md index 28a328a4b..6e08bb6ad 100644 --- a/docs/chapter_sorting/index.md +++ b/docs/chapter_sorting/index.md @@ -26,3 +26,4 @@ icon: material/sort-ascending - [11.9   计数排序](counting_sort.md) - [11.10   基数排序](radix_sort.md) - [11.11   小结](summary.md) +- [11.12   练习](exercises.md) diff --git a/docs/chapter_stack_and_queue/exercises.md b/docs/chapter_stack_and_queue/exercises.md new file mode 100644 index 000000000..db8815db0 --- /dev/null +++ b/docs/chapter_stack_and_queue/exercises.md @@ -0,0 +1,105 @@ +--- +comments: true +--- + + + +# 5.5   练习 + +## 5.5.1   知识巩固 + +### 1.   栈和队列会先取出谁 + +准备一个空栈 `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.   队尾越过数组末尾怎么办 + +用长度为 5 的环形数组实现队列,数组索引为 `0~4`。 +当前 `front = 3`、`size = 2`,队列中的 `A、B` 分别位于索引 3、4。 + + + +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.   双向队列的两端操作 + +这里规定:`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()` + + + +1. 两次弹出的元素分别是什么? +2. 全部操作完成后,从队首到队尾还剩哪些元素? +3. 检查这 6 步操作:只允许从队尾加入、从队首删除的队列能否全部完成?如果不能,请指出无法完成的操作;再说明双向队列能否完成及其原因。 + +??? success "参考答案" + + 前三步后,双向队列从队首到队尾为 `[C, A, B]`。 + + + + 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   编程练习 + +### 1.   检查括号序列 + +给定一个只包含 `()`、`[]`、`{}` 这三类括号的字符串 `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" } diff --git a/docs/chapter_stack_and_queue/index.md b/docs/chapter_stack_and_queue/index.md index 6a414509b..d57955b1d 100644 --- a/docs/chapter_stack_and_queue/index.md +++ b/docs/chapter_stack_and_queue/index.md @@ -19,3 +19,4 @@ icon: material/stack-overflow - [5.2   队列](queue.md) - [5.3   双向队列](deque.md) - [5.4   小结](summary.md) +- [5.5   练习](exercises.md) diff --git a/docs/chapter_tree/exercises.md b/docs/chapter_tree/exercises.md new file mode 100644 index 000000000..50efe1c8c --- /dev/null +++ b/docs/chapter_tree/exercises.md @@ -0,0 +1,132 @@ +--- +comments: true +--- + + + +# 7.7   练习 + +## 7.7.1   知识巩固 + +### 1.   完全、完满与完美二叉树 + +下面两个数组按层序表示二叉树,`None` 表示空位: + +- 树 A:`[1, 2, 3, 4, 5, 6]` +- 树 B:`[1, 2, 3, None, None, 6, 7]` + + + +1. 哪一棵是完全二叉树? +2. 哪一棵是完满二叉树,即每个非叶节点都有两个子节点? +3. 两棵树中是否有完美二叉树?分别说明理由。 + +??? success "参考答案" + + 1. 树 A 是完全二叉树。它只有最底层未填满,并且节点从左到右连续排列。 + 树 B 不是完全二叉树,因为最底层左侧已有空位,右侧却仍有节点。 + + 2. 树 B 是完满二叉树:节点 1 和节点 3 各有两个子节点,其余节点都是叶节点。 + 树 A 不是完满二叉树,因为节点 3 只有左子节点 6。 + + 3. 两棵都不是完美二叉树,因为它们的最底层都没有完全填满。 + +### 2.   同一棵树的三种遍历顺序 + +将数组 `[1, 2, 3, 4, 5, 6, 7]` 按层序存入一棵完全二叉树。 + + + +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.   比较两棵二叉搜索树 + +将下面两组序列从左到右依次插入空的二叉搜索树: + +- 序列 A:`[4, 2, 6, 1, 3, 5, 7]` +- 序列 B:`[1, 2, 3, 4, 5, 6, 7]` + + + +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   编程练习 + +### 1.   二叉树的最大深度 + +给定一棵二叉树的根节点 `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.   按层遍历二叉树 + +给定一棵二叉树的根节点 `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.   二叉搜索树中的第 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" } diff --git a/docs/chapter_tree/index.md b/docs/chapter_tree/index.md index aa938d11d..66e06e3cd 100644 --- a/docs/chapter_tree/index.md +++ b/docs/chapter_tree/index.md @@ -21,3 +21,4 @@ icon: material/graph-outline - [7.4   二叉搜索树](binary_search_tree.md) - [7.5   AVL 树 *](avl_tree.md) - [7.6   小结](summary.md) +- [7.7   练习](exercises.md) diff --git a/en/docs/chapter_array_and_linkedlist/exercises.md b/en/docs/chapter_array_and_linkedlist/exercises.md new file mode 100644 index 000000000..3185b569f --- /dev/null +++ b/en/docs/chapter_array_and_linkedlist/exercises.md @@ -0,0 +1,112 @@ +--- +comments: true +--- + + + +# 4.6   Exercises + +## 4.6.1   Concept Review + +### 1.   How Arrays and Linked Lists Find an Element + +An array and a singly linked list both store `[A, B, C, D, E]` in order. You now need to access the fourth element, `D`: + + + +1. Which index can the array use directly? +2. Starting from the head node `A`, which nodes are visited in order as you follow `next`? +3. As the requested element moves farther toward the end, how does the number of steps change for each structure? Which structure is better for repeated access by position, and why? + +??? success "Answer" + + 1. With zero-based indexing, the fourth element has index 3, so the array can access `arr[3]` directly. + + 2. The singly linked list must start at the head. Its access path is `A → B → C → D`, requiring three moves through `next`. + + 3. An array can locate an element directly from its starting address and index, so access by position has a time complexity of $O(1)$. + To access the $k$th node, a singly linked list must start at the head node and follow `next` $k-1$ times, + which takes $O(n)$ time in the worst case. + + This comparison concerns only access by position; it does not mean that linked lists are slower for every operation. + +### 2.   How Arrays and Linked Lists Insert an Element + +An array and a singly linked list both store `A, B, C, D`. You now need to insert `X` after `B`: + +- The array has capacity 5 and is currently `[A, B, C, D, _]`. +- The linked list is `A → B → C → D`, and you already have a reference to node `B`. + + + +1. Which elements must the array move? Write the array after insertion. +2. In what order should the linked list update `X.next` and `B.next`? Write the linked list after insertion. +3. When comparing insertion efficiency, why is it important to state that "you already have a reference to node B"? + +??? success "Answer" + + 1. The array first moves `D` one position to the right, then moves `C` one position to the right, and finally places `X` at index 2, + producing `[A, B, X, C, D]`. + + 2. `B.next` originally points to `C`. First set `X.next = B.next`, making `X` point to `C`; + then set `B.next = X`. The result is `A → B → X → C → D`. + If `B.next` is overwritten first without saving the original link, `C` may become unreachable. + + 3. Once the location of `B` is known, insertion into the linked list only changes two links and takes $O(1)$ time. + If `B` must first be found from the head, that search alone may take $O(n)$ time. + +### 3.   How a List's Capacity Grows + +An array-based list currently contains `[A, B, C]`, with length `size = 3` and capacity `capacity = 4`. +When there is not enough capacity, a new array with twice the old capacity is created. + + + +1. After appending `D`, what are the list's length and capacity? Is capacity expansion needed? +2. When `E` is appended next, what does the capacity become? How many existing elements must be copied? +3. The length of the underlying array cannot change. Why does the list's capacity nevertheless appear able to grow? + +??? success "Answer" + + 1. `D` fits in the last empty position. The contents are now `[A, B, C, D]`, + with `size = 4` and `capacity = 4`, so no capacity expansion is needed. + + 2. There is no empty position when `E` is appended, so a new array with capacity 8 must be created. + The four existing elements `A, B, C, D` are copied into it before `E` is added. + The result is `size = 5` and `capacity = 8`. + + 3. The original array itself does not grow. The list creates a larger new array, copies the existing elements, + and then uses the new array as its underlying storage. From the user's perspective, the capacity has grown. + +## 4.6.2   Programming Exercises + +### 1.   Add One to a Large Integer Stored as an Array + +The array `digits` stores the decimal digits of a non-negative integer from left to right. For example, `[3, 0, 8]` represents 308. +The number 0 is represented by `[0]`; for every other input, the first digit is not 0. + +Simulate decimal column addition to add 1 to this integer, and return the result in the same array format. +You may modify `digits` directly. If a new carry appears at the front, you may return a longer array. + +??? tip "Hints" + + 1. Start with the last digit of the array, just as in ordinary column addition + 2. If the current digit is less than 9, add one and return immediately + 3. If the current digit is 9, change it to 0; if every digit is 9, place a 1 at the front + +[LeetCode](https://leetcode.com/problems/plus-one/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } + +### 2.   Reverse a Singly Linked List + +You are given the head node `head` of a singly linked list. Each node contains a value and a `next` reference to the following node. + +Use an iterative approach to reverse all links between the nodes, and return the new head node. +Do not create any new linked list nodes. + +??? tip "Hints" + + 1. First draw three connected nodes and the two pointers prev and cur on paper + 2. Before changing cur.next, save the original next node in nxt + 3. After reversing cur.next, set prev = cur and then cur = nxt to continue with the next node in the original list + +[LeetCode](https://leetcode.com/problems/reverse-linked-list/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } diff --git a/en/docs/chapter_array_and_linkedlist/index.md b/en/docs/chapter_array_and_linkedlist/index.md index 2dbde69e4..dacf19911 100644 --- a/en/docs/chapter_array_and_linkedlist/index.md +++ b/en/docs/chapter_array_and_linkedlist/index.md @@ -20,3 +20,4 @@ icon: material/view-list-outline - [4.3   List](list.md) - [4.4   Random-Access Memory and Cache *](ram_and_cache.md) - [4.5   Summary](summary.md) +- [4.6   Exercises](exercises.md) diff --git a/en/docs/chapter_backtracking/exercises.md b/en/docs/chapter_backtracking/exercises.md new file mode 100644 index 000000000..e5350d30a --- /dev/null +++ b/en/docs/chapter_backtracking/exercises.md @@ -0,0 +1,93 @@ +--- +comments: true +--- + + + +# 13.6   Exercises + +## 13.6.1   Concept Review + +### 1.   Will This Permutation Algorithm Miss Results? + +A backtracking algorithm tries to generate all permutations using `1, 2, 3` in that order. Each time it chooses a number `x`, it: + +1. Appends `x` to the current path. +2. Marks `x` as "used." +3. Recursively fills the next position. + +When the recursive call returns, the student removes only `x` from the end of the path and then tries the next number. + + + +1. Which permutation does the algorithm generate first? Can it still generate all 6 permutations? +2. Before returning to the previous level, is removing only the last number from the path enough? If not, what else must be done? Explain why. + +??? success "Answer" + + 1. It first generates `[1, 2, 3]`, but it cannot generate all the permutations. Although the path becomes shorter when calls return, + the markers for 1, 2, and 3 all remain "used," leaving no available number for later branches. + + 2. It is not enough. After removing `x` from the end of the path, the algorithm must also mark `x` as "unused" again. + The current path and the used markers together describe the search state. A choice changes both of them, so backtracking must restore both + before another branch can choose `x` again. + +### 2.   Does the Order of Choosing Numbers Matter? + +You are given the sorted array `[2, 3, 5]` and target value 5. Each number may be chosen repeatedly. +The algorithm requires the numbers along each search path to appear only in nondecreasing order. + + + +1. Which distinct combinations can be found? +2. Why is there no need to search for the same group of numbers in different orders? What does the nondecreasing-order restriction accomplish? +3. Suppose the current path is `[3]`, the remaining amount is 2, and the next candidate is 3. Why can the algorithm stop checking all later candidates at this level? + +??? success "Answer" + + 1. The distinct combinations are `[2, 3]` and `[5]`. + + 2. This exercise treats `[2, 3]` and `[3, 2]` as the same combination; the order in which numbers are selected does not create a different answer. + Requiring numbers in a path to appear in nondecreasing order lets the search skip duplicate arrangements such as `[3, 2]`. + + 3. The remaining amount is 2, while candidate 3 is already greater than 2. Because the array is sorted, + all later candidates are even larger and cannot be added to the current combination, so the algorithm can stop checking this level immediately. + +### 3.   Where Can the Next Queen Be Placed? + +Place queens row by row on a `4 × 4` chessboard, with both row and column indices starting at 0. +Queens have already been placed at `(0, 1)` and `(1, 3)`. The next queen must be placed in row 2. + + + +1. Which columns are ruled out because they already contain a queen? +2. Among the remaining columns, which positions are ruled out because they share a diagonal with an existing queen? +3. Which positions in row 2 remain available to try? + +??? success "Answer" + + 1. Columns 1 and 3 already contain queens, so positions `(2, 1)` and `(2, 3)` are ruled out. + + 2. Among the remaining positions, `(2, 2)` lies on the same diagonal as `(1, 3)`, so it is also ruled out. + Position `(2, 0)` shares neither a column nor a diagonal with either existing queen. + + 3. The only position to try in row 2 is `(2, 0)`. + + This step shows only that the current placement is valid. If the board cannot be completed later, the algorithm must still backtrack and try an earlier alternative. + +## 13.6.2   Programming Exercises + +### 1.   Permutations of Distinct Elements + +The integer array `nums` contains at least one element, and all its elements are distinct. +List every possible order formed by using each element exactly once, and return each order as an array. +The permutations may appear in any order in the result. +Use backtracking, with a Boolean array recording whether the element at each position has already been selected for the current permutation. + +??? tip "Hints" + + 1. The recursion depth indicates which position of the permutation is currently being filled + 2. At each level, try only elements that have not yet been used + 3. When the path's length equals the length of `nums`, add a copy of the path to the answer + +[LeetCode](https://leetcode.com/problems/permutations/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } diff --git a/en/docs/chapter_backtracking/index.md b/en/docs/chapter_backtracking/index.md index 4f2c35f24..377819997 100644 --- a/en/docs/chapter_backtracking/index.md +++ b/en/docs/chapter_backtracking/index.md @@ -20,3 +20,4 @@ icon: material/map-marker-path - [13.3   Subset-Sum Problem](subset_sum_problem.md) - [13.4   N-Queens Problem](n_queens_problem.md) - [13.5   Summary](summary.md) +- [13.6   Exercises](exercises.md) diff --git a/en/docs/chapter_computational_complexity/exercises.md b/en/docs/chapter_computational_complexity/exercises.md new file mode 100644 index 000000000..a84541542 --- /dev/null +++ b/en/docs/chapter_computational_complexity/exercises.md @@ -0,0 +1,116 @@ +--- +comments: true +--- + + + +# 2.6   Exercises + +## 2.6.1   Concept Review + +### 1.   Time and Space Complexity of Iteration and Recursion + +The two functions below both calculate $1 + 2 + \dots + n$ (assume $n \ge 1$). Set `n` to 4, +answer the questions by following the program's actual execution order, and then compare the efficiency of the two approaches. + +```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) +``` + + + +1. When `sum_iter(4)` runs, what is the value of `s` after each loop iteration? +2. When `sum_recur(4)` runs, which function calls occur in order? As the calls return from the deepest level, how is the result obtained? +3. What are the time and space complexities of the two approaches? Explain your reasoning using the execution processes from Questions 1 and 2. + +??? success "Answer" + + 1. The loop variable `i` takes the values `1, 2, 3, 4`. After each iteration, `s` becomes + `1, 3, 6, 10`, respectively, so `sum_iter(4)` returns 10. + + 2. The function calls occur in this order: + `sum_recur(4) → sum_recur(3) → sum_recur(2) → sum_recur(1)`. + `sum_recur(1)` returns 1. The remaining calls then obtain `2 + 1 = 3`, `3 + 3 = 6`, and `4 + 6 = 10`, in that order. + At the deepest point, all four function calls are still unfinished. + + 3. Both functions perform a number of loop iterations or calls proportional to $n$, so both have a time complexity of $O(n)$. + Their space complexities differ. The iterative version uses only a constant number of variables, so its space complexity is $O(1)$. + In the recursive version, earlier calls must wait for a result before returning, so the call stack holds up to $n$ calls at the same time. + Its space complexity is $O(n)$. + + When analyzing space complexity, remember to include the space used by recursive calls as well as the variables written in the code. + +### 2.   Time Complexity of Three Code Fragments + +Each of the following code fragments takes a positive integer $n$ as input. Order them from lowest to highest time complexity, and give the complexity of each one. + +```python +# Fragment 1 +s = 0 +for i in range(n): + s += i + +# Fragment 2 +s = 0 +for i in range(n): + for j in range(i, n): + s += j + +# Fragment 3 +while n > 1: + n = n // 2 +``` + +??? success "Answer" + + From lowest to highest, the order is Fragment 3 with $O(\log n)$, Fragment 1 with $O(n)$, and Fragment 2 with $O(n^2)$. + Fragment 3 halves $n$ in each iteration, so it runs about $\log_2 n$ times. + The loop in Fragment 1 runs exactly $n$ times. The inner loop in Fragment 2 runs + $n,n-1,\dots,1$ times, for a total of $n(n+1)/2$, so its time complexity is quadratic. + +### 3.   Which Reversal Uses Less Space? + +There are two ways to reverse all the elements in the array `nums`: + + + +1. Create a new array `res` of the same length, copy the elements into it in reverse order, and return it. +2. Move two indices `i` and `j` inward from the beginning and end, swapping `nums[i]` and `nums[j]` at each step. + + What is the space complexity of each approach? Which one is an "in-place" operation? + +??? success "Answer" + + 1. This approach needs an auxiliary array with the same length as the input, so its space complexity is $O(n)$. + + 2. This approach uses only two index variables, + so its space complexity is $O(1)$. It is an in-place operation. + + Note that an in-place reversal changes the input array, + so it should be preferred only when modifying the input is allowed. If the original array must be kept, the copying cost of the first approach is unavoidable. + +## 2.6.2   Programming Exercises + +### 1.   Fibonacci Number + +The Fibonacci sequence is defined by $F(0)=0$, $F(1)=1$, and, for $n\ge2$, +$F(n)=F(n-1)+F(n-2)$. + +Given a non-negative integer `n`, use a loop to calculate and return $F(n)$. Do not use recursion. + +??? tip "Hints" + + 1. Handle the cases where n is 0 or 1 separately + 2. Only the previous two terms are needed to calculate the next term; there is no need to store the entire sequence + 3. When updating the two variables, take care not to overwrite an old value before it is used + +[LeetCode](https://leetcode.com/problems/fibonacci-number/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } diff --git a/en/docs/chapter_computational_complexity/index.md b/en/docs/chapter_computational_complexity/index.md index 39d6bfe90..8aa68f28d 100644 --- a/en/docs/chapter_computational_complexity/index.md +++ b/en/docs/chapter_computational_complexity/index.md @@ -20,3 +20,4 @@ icon: material/timer-sand - [2.3   Time Complexity](time_complexity.md) - [2.4   Space Complexity](space_complexity.md) - [2.5   Summary](summary.md) +- [2.6   Exercises](exercises.md) diff --git a/en/docs/chapter_data_structure/exercises.md b/en/docs/chapter_data_structure/exercises.md new file mode 100644 index 000000000..ae4811cc5 --- /dev/null +++ b/en/docs/chapter_data_structure/exercises.md @@ -0,0 +1,92 @@ +--- +comments: true +--- + + + +# 3.6   Exercises + +## 3.6.1   Concept Review + +### 1.   Data Relationships in Everyday Situations + +Based on the relationships among the data, choose one of "linear structure," "tree structure," and "network structure" for each situation below, and explain why: + + + +1. Students stand in a line; we consider only each student's immediate neighbors in front of and behind them. +2. A school is organized in levels: "school → grade → class." +3. City roads connect many intersections. One intersection may lead to several others, and the roads may form cycles. + +??? success "Answer" + + 1. This is a linear structure. Except for the first and last students, each person is adjacent to exactly one person in front and one behind, so the relationships extend along a single line. + + 2. This is a tree structure. Each class belongs to one grade, and each grade belongs to the school, so the relationships form levels from top to bottom. + + 3. This is a network structure. An intersection can connect to several other intersections, and the routes can form cycles, so they cannot be arranged in a single order or a strict hierarchy. + + To identify a structure, first examine the relationships among the elements rather than how much space it uses in memory. + +### 2.   Storing a Logical Order in Memory + +Consider two simplified memory layouts for storing the logical order `A → B → C`: + +- Layout A: `A, B, C` are stored in memory cells numbered `20, 21, 22`, respectively. +- Layout B: `A, B, C` are stored in memory cells numbered `20, 7, 31`, respectively. `A` records the location of `B`, and `B` records the location of `C`. + + + +1. Which layout uses contiguous-space storage, and which uses dispersed-space storage? +2. Which layout is more like an array, and which is more like a linked list? +3. The memory-cell numbers in Layout B are not in increasing order. Why can it still represent the logical order `A → B → C`? + +??? success "Answer" + + 1. Layout A uses consecutive memory cells, so it uses contiguous-space storage. The nodes in Layout B are scattered across different locations, so it uses dispersed-space storage. + + 2. Layout A is more like an array, and Layout B is more like a linked list. + + 3. The logical order is determined by the links recorded between nodes, not by the numerical order of the memory-cell numbers. + The location stored by `A` leads to `B`, and the location stored by `B` then leads to `C`, so `A, B, C` can still be visited in order. + + This also shows that logical structure and physical structure are two different ways of viewing the same data. + +### 3.   Data Types and Structures in Homework Records + +A study group records, in seating order, whether each of four students submitted their homework: + +`[true, false, true, true]` + + + +1. Which basic data type is suitable for each element? +2. The four elements are arranged in a line by seating order. What logical structure does this use? +3. Suppose the group later records each student's homework score as `[90, 0, 85, 100]`. Does this change the data's "content type" or its "organization"? Explain why. + +??? success "Answer" + + 1. Each element represents only "yes" or "no," so the Boolean type `bool` is suitable. + + 2. The elements are arranged in seating order, forming a linear structure that can be stored in an array. + + 3. The content type changes: the elements change from Boolean values to integers. The organization does not change. + The data is still arranged in a line by seating order and can still be stored in an array, which is a linear structure. + + A basic data type describes "what is stored," while a data structure describes "how the data is organized." + +## 3.6.2   Programming Exercises + +### 1.   Count the 1s in a Binary Representation + +Given a non-negative integer `n`, count the number of 1s in its binary representation. + +Use bitwise operations. Do not convert the binary representation to a string or use a built-in function that directly counts 1s. + +??? tip "Hints" + + 1. n & 1 extracts the rightmost bit of n, which tells you whether that bit is 1 + 2. Shifting right by one discards the current rightmost bit; most languages use the operator >> + 3. After implementing the method that checks and shifts one bit at a time, observe that n & (n - 1) turns the rightmost 1 in n into 0 + +[LeetCode](https://leetcode.com/problems/number-of-1-bits/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } diff --git a/en/docs/chapter_data_structure/index.md b/en/docs/chapter_data_structure/index.md index 40177aaa6..10b13cfa7 100644 --- a/en/docs/chapter_data_structure/index.md +++ b/en/docs/chapter_data_structure/index.md @@ -20,3 +20,4 @@ icon: material/shape-outline - [3.3   Number Encoding *](number_encoding.md) - [3.4   Character Encoding *](character_encoding.md) - [3.5   Summary](summary.md) +- [3.6   Exercises](exercises.md) diff --git a/en/docs/chapter_divide_and_conquer/exercises.md b/en/docs/chapter_divide_and_conquer/exercises.md new file mode 100644 index 000000000..a20845c0d --- /dev/null +++ b/en/docs/chapter_divide_and_conquer/exercises.md @@ -0,0 +1,102 @@ +--- +comments: true +--- + + + +# 12.6   Exercises + +## 12.6.1   Concept Review + +### 1.   Which Tasks Are Suitable for Divide and Conquer? + +A student wants to solve each task below by "dividing it into two halves, solving each half separately, and then combining the results." +Classify each task as "suitable for divide and conquer," "can use divide and conquer, but it will not reduce the total work," or "the two halves cannot be solved independently," and explain why. + + + +1. Sort an unsorted array. +2. Find the maximum value in an array. +3. Execute a sequence of `push(x)` and `pop()` stack operations in order, and output the element returned by each `pop()`. + +??? success "Answer" + + 1. Suitable: divide the array in half, sort each half independently, and merge them in $O(n)$ time. This is exactly merge sort. + 2. Divide and conquer can be used, but it does not reduce the total work. The two halves still require examining all $n$ elements in total, + so the time complexity remains $O(n)$, just like a direct scan. + 3. The two halves cannot be solved independently. The stack's contents at the beginning of the second half depend on the result of executing the first half, + so the two halves cannot be completed without knowing each other's results. + +### 2.   How Exponentiation by Squaring Reduces Computation + +The recursive function below uses divide and conquer to calculate $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 +``` + +Use it to calculate `fast_pow(3, 5)`: + + + +1. As the recursive calls proceed, which values does the argument `n` take in order? +2. Starting from the deepest call, what value does each level return? +3. Why should the result be stored in `half` instead of writing `fast_pow(x, n // 2)` twice? + +??? success "Answer" + + 1. The argument takes the values `5 → 2 → 1 → 0`. The exponent is halved at each step until the base case is reached. + + 2. When `n = 0`, the function returns 1. When `n = 1`, it returns $1×1×3=3$. + When `n = 2`, it returns $3×3=9$. When `n = 5`, it returns $9×9×3=243$. + + 3. If `fast_pow(x, n // 2)` were written once on each side of the multiplication, the two recursive calls would calculate exactly the same subproblem. + Storing the result in `half` means that each level makes only one recursive call, so the recursion depth is about $\log n$. + Making two calls would cause a great deal of repeated computation. + +### 3.   Split Traversal Sequences into Left and Right Subtrees + +A binary tree has no duplicate nodes. Its preorder and inorder traversals are: + +- Preorder: `[A, B, D, E, C]` +- Inorder: `[D, B, E, A, C]` + +Split the sequences only once, at the root. You do not need to continue recursively or draw the whole tree: + + + +1. Which node is the root? +2. Which subsequences of the inorder traversal correspond to the left and right subtrees? +3. Which subsequences of the preorder traversal correspond to the left and right subtrees? Which nodes are the root's direct children? + +??? success "Answer" + + 1. The first node in a preorder traversal is the root, so the root is `A`. + + 2. `A` divides the inorder traversal into two parts: `[D, B, E]` for the left subtree and `[C]` for the right subtree. + + 3. The left subtree contains 3 nodes, so the 3 preorder elements after the root `A` belong to the left subtree, + giving `[B, D, E]`. The remaining `[C]` belongs to the right subtree. + Therefore, the root's left child is `B`, and its right child is `C`. + +## 12.6.2   Programming Exercises + +### 1.   Exponentiation by Squaring + +Given a real number `x` and an integer `n`, calculate $x^n$ without calling the language's built-in power function. +Use recursive divide and conquer: halve the exponent at each step and reuse the result of the subproblem already calculated. +This exercise defines $x^0=1$, including when `x = 0`. When `n < 0`, `x != 0` is guaranteed, and the answer can be transformed into $(1/x)^{-n}$. + +??? tip "Hints" + + 1. When n is 0, the answer is 1 + 2. After calculating x to the power n // 2, store the result in half rather than making the recursive call a second time + 3. When n < 0, first change x to 1 / x and then change n to -n; in C++ or Java, first convert n to a 64-bit integer to avoid overflow when negating the smallest 32-bit integer + +[LeetCode](https://leetcode.com/problems/powx-n/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } diff --git a/en/docs/chapter_divide_and_conquer/index.md b/en/docs/chapter_divide_and_conquer/index.md index 4071a8578..5ce90549f 100644 --- a/en/docs/chapter_divide_and_conquer/index.md +++ b/en/docs/chapter_divide_and_conquer/index.md @@ -20,3 +20,4 @@ icon: material/set-split - [12.3   Building a Binary Tree Problem](build_binary_tree_problem.md) - [12.4   Hanota Problem](hanota_problem.md) - [12.5   Summary](summary.md) +- [12.6   Exercises](exercises.md) diff --git a/en/docs/chapter_dynamic_programming/exercises.md b/en/docs/chapter_dynamic_programming/exercises.md new file mode 100644 index 000000000..e68b0d032 --- /dev/null +++ b/en/docs/chapter_dynamic_programming/exercises.md @@ -0,0 +1,117 @@ +--- +comments: true +--- + + + +# 14.8   Exercises + +## 14.8.1   Concept Review + +### 1.   When Is Dynamic Programming Appropriate? + +A student says, "Whenever a recurrence can be written, dynamic programming should be used." +For each task below, decide whether dynamic programming, backtracking, or a loop or mathematical formula without a `dp` table is more appropriate. Give one key reason. + + + +1. Using coin denominations `[1, 3, 4]`, make an amount of 6 with the fewest coins. Each denomination may be used repeatedly. +2. Output all permutations of `[1, 2, 3]`. +3. Calculate $1 + 2 + \dots + n$. + + For the task you consider suitable for dynamic programming, also state what `dp[i]` represents. + +??? success "Answer" + + 1. Dynamic programming is suitable. Let `dp[i]` be the minimum number of coins needed to make amount `i`. + For every coin `c` that does not exceed `i`, `dp[i-c] + 1` is a candidate answer, + and the minimum of these candidates is chosen. Different choices repeatedly encounter the same amounts, and an optimal solution for a larger amount can be built from optimal solutions for smaller amounts. + The answer for amount 6 is 2, using `3 + 3`. + + 2. Backtracking is suitable. The task requires generating all 6 permutations one by one. Backtracking can systematically make a choice, continue searching, + undo the choice, and then try another branch. Regardless of the method, actually outputting every permutation requires enumerating them. + + 3. A loop or the arithmetic-series formula is sufficient. Although the recurrence `S(i) = S(i-1) + i` can be written, calculating `S(i)` depends on only one smaller value, `S(i-1)`. + Each partial sum needs to be calculated only once, so there are no repeated subproblems and no need for a `dp` table. "A recurrence can be written" does not mean "dynamic programming is needed." + +### 2.   Calculating One Cell in a Knapsack Table + +Consider this 0-1 knapsack problem: item weights `wgt = [1, 2, 3]`, values `val = [5, 11, 15]`, and knapsack capacity 4. +`dp[i][c]` is the maximum value obtainable using only the first $i$ items with a knapsack capacity limit of $c$; +the knapsack does not have to be filled exactly. + +Calculate only the state `dp[3][4]`. You are given `dp[2][4] = 16` and `dp[2][1] = 5`: + + + +1. If the third item is not selected, what is the candidate value? +2. If the third item is selected, how much capacity remains, and what is the candidate value? +3. What should `dp[3][4]` be? Which items does this value correspond to selecting? + +??? success "Answer" + + 1. If the third item is not selected, keep the result from the first two items. The candidate value is `dp[2][4] = 16`. + + 2. The third item has weight 3, so capacity $4-3=1$ remains after it is placed in the knapsack. The candidate value is + `dp[2][1] + 15 = 5 + 15 = 20`. + + 3. Comparing 16 and 20 gives `dp[3][4] = 20`. This corresponds to selecting the first and third items, + whose total weight is $1+3=4$ and total value is $5+15=20$. + + This state calculation demonstrates one "select or do not select" comparison in the 0-1 knapsack problem. + +### 3.   In Which Order Should Knapsack Capacities Be Updated? + +A 0-1 knapsack problem has only one item, with weight 2 and value 5, and the knapsack has capacity 4. +The item can be selected at most once. The initial one-dimensional array is `dp = [0, 0, 0, 0, 0]`. + +A student processes the item by updating capacities from 2 to 4: + +- After updating `dp[2]`, its value is 5. +- After updating `dp[3]`, its value is also 5. +- When updating `dp[4]`, the student uses the newly obtained `dp[2]`, producing `dp[4] = 10`. + + + +1. Is `dp[4] = 10` correct? Why or why not? +2. Given that each item may be selected at most once, what should `dp[4]` be? +3. When processing each item, should capacities be updated from largest to smallest or from smallest to largest? What problem does this avoid? + +??? success "Answer" + + 1. The result is incorrect. A value of 10 is equivalent to placing the item with value 5 into the knapsack twice, + violating the condition that each item may be selected at most once. + + 2. The knapsack can contain at most this one item, so the correct value of `dp[4]` is 5. + + 3. Capacities should be updated from largest to smallest, in the order 4, 3, 2. + Then, when calculating `dp[c]`, the value read from `dp[c-2]` still comes from before the current item was processed, + preventing the current item from being reused during the same round. + +## 14.8.2   Programming Exercises + +### 1.   Number of Ways to Climb Stairs + +A staircase has `n` steps. Each move climbs either 1 or 2 steps, and you must land exactly on step `n`. +Calculate the number of distinct ways to reach the top. Assume `n >= 1`; ways are distinguished only by their sequence of 1-step and 2-step moves. +Use a one-dimensional dynamic programming array. For now, do not use the space optimization that keeps only two states. + +??? tip "Hints" + + 1. The last move to step i can cover only 1 or 2 steps + 2. Therefore, dp[i] = dp[i-1] + dp[i-2] + 3. Handle the cases where n is 1 or 2 first, then fill the table starting from step 3 + +[LeetCode](https://leetcode.com/problems/climbing-stairs/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } + +### 2.   0-1 Knapsack + +You are given equal-length arrays `wgt` and `val`. Item `i` has positive integer weight `wgt[i]` and non-negative integer value `val[i]`. +The knapsack capacity `cap` is a non-negative integer. Each item may be selected at most once. Find the maximum total value that can be placed in the knapsack +without exceeding `cap`. Use one-dimensional dynamic programming. + +??? tip "Hints" + + 1. Initialize an array dp of length cap + 1, where dp[c] is the maximum value for a capacity limit of c + 2. When processing item i, compare dp[c], which does not select it, with dp[c-wgt[i]] + val[i], which does + 3. Update capacities from largest to smallest to avoid selecting the current item repeatedly in the same round diff --git a/en/docs/chapter_dynamic_programming/index.md b/en/docs/chapter_dynamic_programming/index.md index 8381a5348..4e22c9029 100644 --- a/en/docs/chapter_dynamic_programming/index.md +++ b/en/docs/chapter_dynamic_programming/index.md @@ -22,3 +22,4 @@ icon: material/table-pivot - [14.5   Unbounded Knapsack Problem](unbounded_knapsack_problem.md) - [14.6   Edit Distance Problem](edit_distance_problem.md) - [14.7   Summary](summary.md) +- [14.8   Exercises](exercises.md) diff --git a/en/docs/chapter_graph/exercises.md b/en/docs/chapter_graph/exercises.md new file mode 100644 index 000000000..99ef88a66 --- /dev/null +++ b/en/docs/chapter_graph/exercises.md @@ -0,0 +1,109 @@ +--- +comments: true +--- + + + +# 9.5   Exercises + +## 9.5.1   Concept Review + +### 1.   Represent the Same Graph in Two Ways + +An undirected graph has four vertices, `A, B, C, D`, and the edges +`A-B, A-C, B-C, C-D`. + + + +1. Write its adjacency list. +2. Fill in its adjacency matrix using only 0s and 1s. +3. To determine whether `A` and `D` are directly connected, which graph representation requires checking only one stored entry? +4. If a graph has many vertices but few edges, which representation usually uses less space? + +??? success "Answer" + + 1. The adjacency list is: + + ```text + A: B, C + B: A, C + C: A, B, D + D: C + ``` + + 2. The adjacency matrix is: + + | | 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. In an adjacency matrix, you can directly check row `A`, column `D`, making it well suited to determining whether any two vertices are directly connected. + + 4. When a graph has many vertices but few edges, an adjacency list records only the edges that actually exist. It usually uses less space than an adjacency matrix, which reserves a position for every pair of vertices. + +### 2.   Breadth-First and Depth-First Traversal Orders + +An undirected graph has vertices `A, B, C, D, E` and edges +`A-B, A-C, B-D, C-D, D-E`. + +Start at A. Whenever there are several unvisited adjacent vertices, choose them in alphabetical order: + + + +1. Write the visit order of breadth-first traversal (BFS). +2. Write the visit order of recursive depth-first traversal (DFS). +3. Why must both traversals record which vertices have already been visited? + +??? success "Answer" + + 1. The BFS visit order is `A, B, C, D, E`. It first visits B and C, which are one edge away from A, + and then visits the more distant D and E. + + 2. The DFS visit order is `A, B, D, C, E`. It repeatedly enters an unvisited adjacent vertex, + first following `A → B → D → C`. When C has no new adjacent vertex, it returns to D and then visits E. + + 3. The graph contains a cycle, such as `A-B-D-C-A`. Without recording visited vertices, + a traversal could repeatedly visit the same vertices around the cycle and fail to terminate normally. + +### 3.   Can One BFS Visit the Entire Graph? + +An undirected graph has vertices `A, B, C, D, E, F` and only the edges +`A-B, B-C, D-E`. + + + +1. Which vertices can one BFS starting at A visit? +2. Based on Question 1, has this BFS visited every vertex in the graph? Why or why not? +3. Suppose you scan all vertices in alphabetical order and start a new BFS whenever you reach an unvisited vertex. + What is the starting vertex of each BFS? Into how many mutually disconnected parts (connected components) is the graph divided? + +??? success "Answer" + + 1. Starting from A, the traversal can visit only `A, B, C`. + + 2. It has not visited every vertex. `D, E` form another connected part, while F is an isolated vertex. + None of them has a path to A, so they cannot be reached from A. + + 3. The three BFS traversals start at `A, D, F`, and visit + `{A, B, C}`, `{D, E}`, and `{F}`, respectively. Therefore, the graph has 3 connected components. + +## 9.5.2   Programming Exercises + +### 1.   Determine Whether a Path Exists in an Undirected Graph + +You are given an undirected graph with $n$ vertices numbered from $0$ to $n-1$. Each entry `[u, v]` in the array `edges` represents an undirected edge between vertices `u` and `v`. + +You are also given a starting vertex `source` and a destination vertex `destination`. First build an adjacency list from `edges`, then use BFS or DFS +to determine whether a path exists from `source` to `destination`. Return `true` if one exists and `false` otherwise. +The graph may contain cycles and may be disconnected. + +??? tip "Hints" + + 1. Add every undirected edge in both directions + 2. The graph may contain cycles, so you must record which vertices have already been visited + 3. Starting from source, return true if you encounter destination; if the traversal ends without reaching it, return false + +[LeetCode](https://leetcode.com/problems/find-if-path-exists-in-graph/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } diff --git a/en/docs/chapter_graph/index.md b/en/docs/chapter_graph/index.md index c7f2c63cf..88a441b82 100644 --- a/en/docs/chapter_graph/index.md +++ b/en/docs/chapter_graph/index.md @@ -19,3 +19,4 @@ icon: material/graphql - [9.2   Basic Operations on Graphs](graph_operations.md) - [9.3   Graph Traversal](graph_traversal.md) - [9.4   Summary](summary.md) +- [9.5   Exercises](exercises.md) diff --git a/en/docs/chapter_greedy/exercises.md b/en/docs/chapter_greedy/exercises.md new file mode 100644 index 000000000..098047ca8 --- /dev/null +++ b/en/docs/chapter_greedy/exercises.md @@ -0,0 +1,89 @@ +--- +comments: true +--- + + + +# 15.6   Exercises + +## 15.6.1   Concept Review + +### 1.   Is Choosing the Largest Coin Always Best? + +The coin denominations are `[1, 7, 10]`, and the target amount is 14. + + + +1. Follow the rule "always choose the largest denomination that does not exceed the remaining amount," and write the coins selected. +2. Is there a solution using fewer coins? If so, give one; otherwise, explain why not. +3. Does this example show that the greedy strategy is correct for every set of coin denominations? + +??? success "Answer" + + 1. The greedy strategy selects `10 + 1 + 1 + 1 + 1`, using 5 coins in total. + + 2. There is a solution using fewer coins: `7 + 7`, which uses only 2 coins. + + 3. No. This counterexample shows that, for arbitrary coin denominations, repeatedly choosing the largest currently available denomination does not necessarily minimize the number of coins. + The largest immediate choice may prevent a better combination later. + +### 2.   Which Item Should Go into the Knapsack First? + +A knapsack with a capacity of 4 kilograms can hold the following items. A fraction of an item may be taken, +and the value obtained is proportional to its weight: + +- Item A: weight 4 kilograms, value 20. +- Item B: weight 3 kilograms, value 18. + + + +1. What is the value per kilogram of each item? Which item should be placed in the knapsack first? +2. Fill the knapsack using the greedy strategy for the fractional knapsack problem. What is the final value? +3. When items can be divided and the knapsack limits total weight, should items be compared by total value or by value per kilogram? Why? + +??? success "Answer" + + 1. A has value `20 ÷ 4 = 5` per kilogram, while B has value `18 ÷ 3 = 6` per kilogram, + so B, with the higher value per unit weight, should be placed in the knapsack first. + + 2. First take all of B, using 3 kilograms of capacity and gaining a value of 18. With 1 kilogram of capacity remaining, + take 1 kilogram of A, gaining a value of 5. The final value is `18 + 5 = 23`. + + 3. The knapsack limits total weight, and items can be divided, so they should be compared by value per unit weight. + Although A has a higher total value, its value per kilogram is lower than B's. Filling the knapsack with A first would yield only a value of 20. + +### 3.   Which Pointer Should Move Next? + +The partition heights are `[1, 8, 6, 2, 5]`. Use two pointers, one at each end, to find the maximum capacity. +The capacity equals "the height of the shorter partition × the distance between the partitions' indices." + + + +1. Initially, the left pointer is at index 0 and the right pointer is at index 4. What is the current capacity? Which pointer should move next? +2. After making the move chosen in Question 1, at which indices are the two pointers? What is the capacity now? Which pointer should move next? +3. For the current pair of partitions, you could move either the pointer at the shorter partition or the pointer at the taller partition. Which move could still produce a greater capacity, and why? + +??? success "Answer" + + 1. The current capacity is `min(1, 5) × (4 - 0) = 4`. The left partition is shorter, so move the left pointer. + + 2. After the left pointer moves, the pointers are at indices 1 and 4. The current capacity is + `min(8, 5) × (4 - 1) = 15`. The right partition is shorter, so move the right pointer next. + + 3. Moving the pointer at the shorter partition is the only move that could still produce a greater capacity. If the taller partition's pointer moves, the distance certainly decreases while the height remains limited by the unmoved shorter partition, + so the capacity can only stay the same or decrease. Only by moving the shorter partition can a taller partition possibly be found. + +## 15.6.2   Programming Exercises + +### 1.   Fractional Knapsack + +You are given equal-length arrays `wgt` and `val`, where `wgt[i] > 0` and `val[i] >= 0`. The knapsack has capacity `cap >= 0`. +There is only one of each item, but any fraction of an item may be placed in the knapsack. +The value obtained is proportional to the fraction of the item's total weight that is included. Use a greedy algorithm +and return the maximum total value the knapsack can hold as a real number. + +??? tip "Hints" + + 1. First calculate each item's value per unit weight as val[i] / wgt[i], keeping the fractional part of the division + 2. Place items with higher value per unit weight into the knapsack first + 3. If the remaining capacity is less than the current item's weight, take exactly the fraction that fills the knapsack and stop diff --git a/en/docs/chapter_greedy/index.md b/en/docs/chapter_greedy/index.md index 95dac47ac..fbdf10ab1 100644 --- a/en/docs/chapter_greedy/index.md +++ b/en/docs/chapter_greedy/index.md @@ -20,3 +20,4 @@ icon: material/head-heart-outline - [15.3   Maximum Capacity Problem](max_capacity_problem.md) - [15.4   Maximum Product Cutting Problem](max_product_cutting_problem.md) - [15.5   Summary](summary.md) +- [15.6   Exercises](exercises.md) diff --git a/en/docs/chapter_hashing/exercises.md b/en/docs/chapter_hashing/exercises.md new file mode 100644 index 000000000..a138bf4a4 --- /dev/null +++ b/en/docs/chapter_hashing/exercises.md @@ -0,0 +1,107 @@ +--- +comments: true +--- + + + +# 6.5   Exercises + +## 6.5.1   Concept Review + +### 1.   Searching After a Hash Collision + +A hash table has 5 buckets and uses the hash function $h(x)=x \bmod 5$. When a collision occurs, elements are placed one after another in a list within that bucket. +Insert `[1, 6, 11, 7]` in order: + + + +1. Write the contents of buckets 0–4. +2. When searching for 6, which bucket is checked first, and which elements are examined in order? +3. Based on the bucket contents from Question 1, do later insertions overwrite earlier ones? Explain using this collision-resolution method. + +??? success "Answer" + + 1. Because $1\bmod5=6\bmod5=11\bmod5=1$, while $7\bmod5=2$, the buckets are: + + ```text + 0: [] + 1: [1, 6, 11] + 2: [7] + 3: [] + 4: [] + ``` + + 2. A search for 6 first goes to bucket 1, then compares 1 and 6 in order. The target is found on the second comparison. + + 3. Equal hash values mean only that the elements go into the same bucket, not that the elements are equal. Separate chaining keeps all colliding elements in the bucket + and compares them one at a time during a search, so 1, 6, and 11 do not overwrite one another. + +### 2.   Where Do Elements Go After a Hash Table Expands? + +A hash table using separate chaining originally has 5 buckets and the hash function $h(x)=x\bmod5$. +The keys `[1, 6, 11]` are all in bucket 1. + +The table is now expanded to 7 buckets, and the hash function becomes $h(x)=x\bmod7$: + + + +1. Calculate the new bucket number for 1, 6, and 11. +2. Which buckets contain elements after the expansion? +3. During the expansion, can the list from the old bucket 1 simply be copied into the new bucket 1? Explain using the results from Questions 1 and 2. + +??? success "Answer" + + 1. The new bucket numbers are: + + - $1\bmod7=1$; + - $6\bmod7=6$; + - $11\bmod7=4$. + + 2. Bucket 1 stores 1, bucket 4 stores 11, and bucket 6 stores 6. The three keys are no longer crowded into the same bucket. + + 3. The list cannot be copied as is. A bucket number is calculated by taking the key modulo the number of buckets. When the bucket count changes from 5 to 7, a key's bucket number may change, + so the position of every key must be recalculated. If the old bucket 1 were copied unchanged, later searches using the new formula would go to bucket 6 for 6 and bucket 4 for 11 + and would fail to find them. + +### 3.   Can 11 Still Be Found After Deleting 6? + +A hash table has 5 positions with indices `0–4` and uses the hash function $h(x)=x\bmod5$. +When a collision occurs, it searches to the right from the index produced by the hash function for the first empty position. + +Insert `[1, 6, 11]` in order: + + + +1. At which index is each number ultimately stored? +2. When searching for 11, which indices are examined in order? +3. Suppose deleting 6 changes its position directly to an "unused empty position," and a search stops whenever it reaches an empty position. + What happens when searching for 11 afterward? Is this search result correct? If there is a problem, how can it be avoided? + +??? success "Answer" + + 1. The number 1 is stored at index 1. The number 6 also maps to index 1, so after the collision it is stored at index 2. + The number 11 also starts at index 1, skips the occupied indices 1 and 2, and is ultimately stored at index 3. + + 2. A search for 11 examines indices `1, 2, 3` in order and finds it at index 3. + + 3. If index 2 is changed to mean "never used," a search for 11 checks index 1 and then stops at index 2, + incorrectly concluding that 11 is absent. Deletion should leave a "deleted" marker. + A search that reaches this marker continues to the next index (wrapping from index 4 to index 0), while a later insertion may still reuse the position. + +## 6.5.2   Programming Exercises + +### 1.   Compare the Character Counts of Two Strings + +Given two strings `s` and `t` containing only lowercase English letters, +you may rearrange the characters in `s` in any order, but you may not add, remove, or replace characters. + +Determine whether the rearranged string can form `t`. Return `true` if it can and `false` otherwise. +Use a hash table to record how many times each letter occurs. Do not sort the characters in the strings. + +??? tip "Hints" + + 1. If the two strings have different lengths, they cannot contain each character the same number of times + 2. Use a hash table to record the count of each letter; increment the corresponding count while scanning s + 3. Decrement the corresponding count while scanning t; the strings contain each character the same number of times only if every count is 0 at the end + +[LeetCode](https://leetcode.com/problems/valid-anagram/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } diff --git a/en/docs/chapter_hashing/index.md b/en/docs/chapter_hashing/index.md index dcdd4161c..b1643d5fc 100644 --- a/en/docs/chapter_hashing/index.md +++ b/en/docs/chapter_hashing/index.md @@ -19,3 +19,4 @@ icon: material/table-search - [6.2   Hash Collision](hash_collision.md) - [6.3   Hash Algorithm](hash_algorithm.md) - [6.4   Summary](summary.md) +- [6.5   Exercises](exercises.md) diff --git a/en/docs/chapter_heap/exercises.md b/en/docs/chapter_heap/exercises.md new file mode 100644 index 000000000..672fa8ab4 --- /dev/null +++ b/en/docs/chapter_heap/exercises.md @@ -0,0 +1,92 @@ +--- +comments: true +--- + + + +# 8.5   Exercises + +## 8.5.1   Concept Review + +### 1.   How Does the Heap Change After Inserting 10? + +The array `[9, 7, 8, 3, 5]` represents a max heap. Now insert the number 10. + + + +1. First append 10 to the end of the array. What is the value of its parent node? +2. Starting from the new node, perform bottom-to-top heapify and write the array after each swap. +3. What is the final top element of the heap? How many swaps occur in total? + +??? success "Answer" + + 1. After 10 is appended, its index is 5, so its parent's index is + $\lfloor(5-1)/2\rfloor=2$. The parent node's value is 8. + + 2. Since 10 is greater than 8, the array after the first swap is `[9, 7, 10, 3, 5, 8]`. + Since 10 is also greater than its parent 9, the array after the second swap is `[10, 7, 9, 3, 5, 8]`. + The value 10 has now reached the root node, so heapification is complete. + + 3. The final top element is 10, and 2 swaps occur in total. + +### 2.   Check Parent–Child Relationships in a Min Heap + +The array `[1, 4, 3, 7, 6, 2]` represents a complete binary tree. In a min heap, every parent node must be no greater than its children. +For index $i$, the left and right child indices are $2i+1$ and $2i+2$, respectively. + + + +1. What are the indices and values of the children of index 2? +2. The node at index 2 has value 3. Does it violate the min-heap rule with its child? If so, which two elements should be swapped? +3. Based on your answer to Question 2, write the array after the swap if the rule is violated; otherwise, explain why no swap is needed. Finally, check the remaining parent–child relationships. + +??? success "Answer" + + 1. The left child of index 2 is at index 5 and has value 2. The right child's index is 6, but the array has length 6, so the right child does not exist. + + 2. The parent value 3 is greater than the child value 2, violating the min-heap rule. The elements at indices 2 and 5 should be swapped. + + 3. After the swap, the array is `[1, 4, 2, 7, 6, 3]`. Check each relationship: + `1 ≤ 4`, `1 ≤ 2`; `4 ≤ 7`, `4 ≤ 6`; and `2 ≤ 3`. + Every parent is now no greater than its children, so the min-heap rule is satisfied. + +### 3.   Keep the Three Largest Numbers with a Min Heap + +To keep the 3 largest numbers from the data stream `[4, 1, 7, 3, 8]`, maintain a min heap containing no more than 3 elements. + +First insert the first 3 numbers into the min heap in order. Once the heap is full, for each new number: +if it is greater than the top element, remove the top and insert the new number; otherwise, leave the heap unchanged. + +After each number is read, write the numbers kept in the heap and the top element. +Write the kept numbers as a set; you do not need to give their order in the heap array. + +??? success "Answer" + + The result after each number is read is: + + | Number read | Numbers kept | Top | + | --- | --- | --- | + | 4 | `{4}` | 4 | + | 1 | `{1, 4}` | 1 | + | 7 | `{1, 4, 7}` | 1 | + | 3 | `{3, 4, 7}` | 3 | + | 8 | `{4, 7, 8}` | 4 | + + Once the heap is full, its top is the smallest of the numbers currently kept. A new number replaces the top only when it is greater than the top. + The final set `{4, 7, 8}` contains exactly the 3 largest numbers. + +## 8.5.2   Programming Exercises + +### 1.   Find the Kth Largest Element in an Array + +Given an integer array `nums` and an integer $k$, where $1 \le k \le n$ and $n$ is the array's length, return the element that would appear at position $k$ if the array were arranged from largest to smallest. + +Count duplicate elements separately. For example, the second-largest element of `[5, 5, 2]` is still 5. Use a min heap containing no more than $k$ elements. + +??? tip "Hints" + + 1. The kth largest element is the smallest among the k largest numbers + 2. Insert each number into the min heap, and remove the smallest value whenever the heap's size exceeds k + 3. After the traversal, the heap contains the k largest numbers, and its top is the answer + +[LeetCode](https://leetcode.com/problems/kth-largest-element-in-an-array/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } diff --git a/en/docs/chapter_heap/index.md b/en/docs/chapter_heap/index.md index 39988253d..edbcf5fde 100644 --- a/en/docs/chapter_heap/index.md +++ b/en/docs/chapter_heap/index.md @@ -19,3 +19,4 @@ icon: material/family-tree - [8.2   Heap Construction Operation](build_heap.md) - [8.3   Top-k Problem](top_k.md) - [8.4   Summary](summary.md) +- [8.5   Exercises](exercises.md) diff --git a/en/docs/chapter_searching/exercises.md b/en/docs/chapter_searching/exercises.md new file mode 100644 index 000000000..8737debee --- /dev/null +++ b/en/docs/chapter_searching/exercises.md @@ -0,0 +1,102 @@ +--- +comments: true +--- + + + +# 10.7   Exercises + +## 10.7.1   Concept Review + +### 1.   How Binary Search Narrows the Search Interval + +Search for 16 in the sorted array `[2, 5, 8, 12, 16, 23, 38]`. +Use the closed interval `[i, j]` and calculate the midpoint as +$m=i+(j-i)/2$, rounded down. + +For each round, write `(i, j, m)`, the middle element, and how the interval is narrowed next, +until the target is found. + +??? success "Answer" + + The search proceeds as follows: + + | Round | `(i, j, m)` | Middle element | Next step | + | --- | --- | --- | --- | + | 1 | `(0, 6, 3)` | 12 | `12 < 16`, set `i = 4` | + | 2 | `(4, 6, 5)` | 23 | `23 > 16`, set `j = 4` | + | 3 | `(4, 4, 4)` | 16 | Target found; return index 4 | + + Because the array is sorted, when the middle value is smaller than the target, the middle and everything to its left can be excluded. + When the middle value is greater than the target, the middle and everything to its right can be excluded. + +### 2.   Left and Right Boundaries of Duplicate Elements + +Search for the number 2 in the array `[1, 2, 2, 2, 4, 6]`. +A student uses binary search, returns immediately after finding the target at index 2, and says, +"Index 2 is the left boundary of the number 2." + + + +1. Is the student's statement correct? What are the left and right boundaries of 2? Explain why. +2. When searching for the left boundary, if the middle element equals the target, which side should be searched next? +3. When searching for the right boundary, which side should be searched next? State only the direction; you do not need to write the complete search process. + +??? success "Answer" + + 1. The student's statement is incorrect. Returning as soon as a 2 is found guarantees only that some occurrence of 2 was found, not the leftmost or rightmost one. + Here, the left boundary is index 1 and the right boundary is index 3. + + 2. When searching for the left boundary, continue searching on the left even if the middle element equals 2. + For example, with a closed interval, set `j = m - 1`. + + 3. When searching for the right boundary, continue searching on the right after the middle element equals 2. + For example, set `i = m + 1`. + +### 3.   Choosing a Search Method for Different Data + +For each situation below, choose an appropriate method from "linear search," "binary search," and "hash table," and explain why: + + + +1. Repeatedly search among $10^7$ integers that are already sorted and never change, without building any additional data structure. +2. Repeatedly test whether a key exists in a collection with frequent insertions and deletions. The collection need not remain sorted, and no range searches are needed. +3. Search an unsorted array for a value only once. + +??? success "Answer" + + 1. Binary search: the data is sorted and static, so searching takes $O(\log n)$ time without extra space. + 2. Hash table: when the hash function distributes keys fairly evenly among the buckets, insertion, deletion, and lookup by key all take $O(1)$ time on average. + 3. Scan directly from beginning to end. When searching only once, sorting the array or building a hash table still requires processing the entire array first, + so neither reduces the total work for this single task. + + The choice depends on whether the data is sorted, whether an additional structure is allowed, how many searches are needed, and which operations must be supported. + +## 10.7.2   Programming Exercises + +### 1.   Binary Search in a Sorted Array + +Given an integer array `nums` in strictly increasing order and a target value `target`, use binary search to find `target`. If it exists, return its array index; otherwise, return -1. + +??? tip "Hints" + + 1. The initial interval is left = 0 and right = n - 1; it is nonempty while left <= right + 2. Calculate the midpoint with mid = left + (right - left) // 2 + 3. If `nums[mid]` is less than `target`, move the left boundary to `mid + 1`; if `nums[mid]` is greater than `target`, move the right boundary to `mid - 1`; if they are equal, return immediately + +[LeetCode](https://leetcode.com/problems/binary-search/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } + +### 2.   Insertion Point in a Sorted Array + +You are given an integer array `nums` in strictly increasing order and a target value `target`. + +Return the index of `target` if it is already in the array. Otherwise, return the insertion point at which `target` can be inserted while keeping the array in strictly increasing order. +The insertion point may be 0 or may equal the array's length. Use binary search. + +??? tip "Hints" + + 1. The answer may be 0 or the array length n + 2. With a closed interval, if `nums[mid]` is greater than or equal to `target`, set `right = mid - 1` and continue checking farther left; otherwise, set `left = mid + 1` + 3. When the loop ends, left is the insertion point + +[LeetCode](https://leetcode.com/problems/search-insert-position/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } diff --git a/en/docs/chapter_searching/index.md b/en/docs/chapter_searching/index.md index 4619ceddb..783cfa98f 100644 --- a/en/docs/chapter_searching/index.md +++ b/en/docs/chapter_searching/index.md @@ -21,3 +21,4 @@ icon: material/text-search - [10.4   Hash Optimization Strategy](replace_linear_by_hashing.md) - [10.5   Searching Algorithms Revisited](searching_algorithm_revisited.md) - [10.6   Summary](summary.md) +- [10.7   Exercises](exercises.md) diff --git a/en/docs/chapter_sorting/exercises.md b/en/docs/chapter_sorting/exercises.md new file mode 100644 index 000000000..7315b4236 --- /dev/null +++ b/en/docs/chapter_sorting/exercises.md @@ -0,0 +1,100 @@ +--- +comments: true +--- + + + +# 11.12   Exercises + +## 11.12.1   Concept Review + +### 1.   The First Few Rounds of Selection Sort and Bubble Sort + +Given the array `[4, 2, 5, 1, 3]`, sort it in ascending order in both parts below. + + + +1. Simulate the first two rounds of selection sort. Write the array after each round and identify which positions are now fixed. +2. Simulate the first round of bubble sort. Write the resulting array and the number of swaps, and identify which position is now fixed. + +??? success "Answer" + + 1. The first two rounds are: + + | Round | Array | Explanation | + | --- | --- | --- | + | 1 | `[1, 2, 5, 4, 3]` | The smallest element, 1, is swapped with the first element | + | 2 | `[1, 2, 5, 4, 3]` | The value 2 is already at index 1, so no swap is needed | + + The first two positions are now fixed. Later rounds need to find the smallest element only within `[5, 4, 3]`. + + 2. Compare adjacent elements in order: swap 4 and 2; do not swap 4 and 5; swap 5 and 1; then swap 5 and 3. + The result is `[2, 4, 1, 3, 5]`, after 3 swaps. The largest element, 5, has moved to the end of the array, so the last position is now fixed. + +### 2.   Can Equal Elements Change Their Relative Order? + +In the array $[2_a, 2_b, 1]$, $2_a$ and $2_b$ have equal values, but their subscripts mark their original order. + + + +1. Write the array after the first round of selection sort. Has the relative order of $2_a$ and $2_b$ changed? +2. Write the array after the first round of bubble sort. Has the relative order of $2_a$ and $2_b$ changed? +3. Based on the first two questions, explain how the two sorting algorithms differ in preserving the original order of equal elements. + +??? success "Answer" + + 1. In its first round, selection sort selects the smallest element, 1, and swaps it with the first element, $2_a$, producing + $[1, 2_b, 2_a]$. The relative order has changed because $2_a$ has moved behind $2_b$. + + 2. Bubble sort first compares $2_a$ and $2_b$. Because they are equal, it does not swap them. It then compares $2_b$ and 1 and swaps them, + producing $[2_a, 1, 2_b]$ after the first round. $2_a$ is still before $2_b$, so their relative order has not changed. + + 3. In this example, selection sort changes the original order of equal elements. Bubble sort swaps adjacent elements only when the left element is greater than the right one. + Equal elements are not swapped, so their original relative order is preserved. + +### 3.   Compare Counting Sort and Radix Sort + +A school needs to sort many student ID numbers, each exactly 8 digits long. Answer the following questions: + + + +1. How many rounds does radix sort need when it starts with the least significant digit? +2. If the student IDs are treated directly as integers for counting sort, why would the count array need many entries that are never used? +3. Based on the first two questions, which would you choose for sorting many fixed-length 8-digit student IDs: counting sort or radix sort? Why? + +??? success "Answer" + + 1. A student ID has 8 digits, so 8 rounds are needed from the least significant to the most significant digit. Each round groups values only by 0–9. + + 2. Direct counting would require an entry for every possible 8-digit value, but only a small fraction of those values are actually assigned to students. + Most entries in the count array would remain 0. + + 3. Radix sort is the better choice. It uses the facts that the length is fixed and each digit has only 10 possible values, requiring only 8 rounds of stable grouping. + Counting sort, if it used the entire 8-digit ID as an integer index, would require count-array entries for many values that never occur. + +## 11.12.2   Programming Exercises + +### 1.   Sort an Array with Merge Sort + +Given an integer array `nums`, implement merge sort yourself, arrange its elements in nondecreasing order, and return the result. Do not call the language's built-in sorting function. + +??? tip "Hints" + + 1. An interval of length at most 1 is already sorted + 2. Divide the interval in half at its midpoint and recursively sort both halves + 3. Use two pointers to merge the two sorted halves, then write the result back into the original array + +[LeetCode](https://leetcode.com/problems/sort-an-array/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } + +### 2.   Sort an Integer Array with Counting Sort + +You are given an integer array `nums` and a non-negative integer $K$. Every element of the array is between $0$ and $K$. + +Implement counting sort, write the result back into `nums` in nondecreasing order, and return `nums`. +Do not determine the order by comparing elements, and do not call the language's built-in sorting function. + +??? tip "Hints" + + 1. Because every element is between 0 and K, use each element's value directly as an index in the counting array + 2. Scan nums once and increment the count at the corresponding position + 3. Then scan the counting array from 0 to K; if the value x occurs a certain number of times, write x into nums that many consecutive times diff --git a/en/docs/chapter_sorting/index.md b/en/docs/chapter_sorting/index.md index 2735dbd3c..d27c4a881 100644 --- a/en/docs/chapter_sorting/index.md +++ b/en/docs/chapter_sorting/index.md @@ -26,3 +26,4 @@ icon: material/sort-ascending - [11.9   Counting Sort](counting_sort.md) - [11.10   Radix Sort](radix_sort.md) - [11.11   Summary](summary.md) +- [11.12   Exercises](exercises.md) diff --git a/en/docs/chapter_stack_and_queue/exercises.md b/en/docs/chapter_stack_and_queue/exercises.md new file mode 100644 index 000000000..731111a07 --- /dev/null +++ b/en/docs/chapter_stack_and_queue/exercises.md @@ -0,0 +1,105 @@ +--- +comments: true +--- + + + +# 5.5   Exercises + +## 5.5.1   Concept Review + +### 1.   Which Element Leaves a Stack or Queue First? + +Prepare an empty stack `S` and an empty queue `Q`. Perform the same sequence of operations on each one: + +Step 1: Add `A`. +Step 2: Add `B`. +Step 3: Remove and record one element. +Step 4: Add `C`. +Step 5: Keep removing and recording elements until the container is empty. + +Write the order in which elements are removed from `S` and from `Q`. Explain the difference using "last-in-first-out" or "first-in-first-out." + +??? success "Answer" + + The removal order for stack `S` is `B, C, A`. After the elements `A, B` are added, the most recently added element, `B`, is popped first. After `C` is added, + `C, A` are popped in that order. This is "last-in-first-out." + + The removal order for queue `Q` is `A, B, C`. After the elements `A, B` are added, the earliest added element, `A`, is removed first. + After `C` is added, `B, C` are removed in that order. This is "first-in-first-out." + +### 2.   What Happens When the Rear Passes the End of the Array? + +A queue is implemented with a circular array of length 5, whose indices are `0–4`. +Currently, `front = 3` and `size = 2`; `A, B` are stored at indices 3 and 4, respectively. + + + +1. When `C` is enqueued, at which index should `C` be stored? What is `size` after the enqueue? +2. Next, dequeue once. Which element is removed? What are the new values of `front` and `size`? +3. What is the logical order from the front to the rear now? Does dequeuing require moving the other elements in the array? Why or why not? + +??? success "Answer" + + 1. The new element's position is + `(front + size) % 5 = (3 + 2) % 5 = 0`, + so `C` is stored at index 0. After the enqueue, `size = 3`. + + 2. Dequeuing removes the current front element, `A`. The new front index is + `(3 + 1) % 5 = 4`, so `front = 4` and `size = 2`. + + 3. The logical order of valid elements is `B, C`, with `B` at index 4 and `C` at index 0. + Dequeuing requires only changing `front` and `size`. The circular array uses the remainder operation to wrap the index back to the beginning, + so there is no need to shift all the other elements forward. + +### 3.   Operations at Both Ends of a Deque + +Here, `push_first` adds an element at the front, `push_last` adds one at the rear, +`pop_first` removes one from the front, and `pop_last` removes one from the rear. + +Perform the following operations on an empty deque `deq`: + +1. `push_last(A)` +2. `push_last(B)` +3. `push_first(C)` +4. `pop_last()` +5. `push_last(D)` +6. `pop_first()` + + + +1. Which elements are returned by the two pop operations? +2. After all operations are complete, which elements remain from front to rear? +3. Examine the six operations. Can a queue that allows insertion only at the rear and removal only at the front perform all of them? If not, identify the operations it cannot perform. Then state whether a deque can perform them and explain why. + +??? success "Answer" + + After the first three steps, the deque from front to rear is `[C, A, B]`. + + + + 1. `pop_last()` removes `B`. After `D` is added, the deque is `[C, A, D]`, + and `pop_first()` then removes `C`. + + 2. `[A, D]` remains. + + 3. A queue that permits insertion only at the rear and removal only at the front cannot perform all the operations. + Step 3, `push_first(C)`, requires insertion at the front, and Step 4, `pop_last()`, requires removal at the rear. Both are outside the operations supported by such a queue. + A deque permits insertion and removal at both ends, so it can perform all six operations. + +## 5.5.2   Programming Exercises + +### 1.   Check a Bracket Sequence + +Given a string `s` containing only the three types of brackets `()`, `[]`, and `{}`, use a stack to determine whether the string is valid. + +A valid sequence must satisfy both conditions: every closing bracket matches the type of the most recent unmatched opening bracket, +and no unmatched opening bracket remains after the traversal. Return a Boolean value for the result. + +??? tip "Hints" + + 1. You can create a mapping from each closing bracket to its matching opening bracket + 2. When you encounter a closing bracket, first check whether the stack is empty, and then check whether the top matches + 3. The stack must also be empty after the traversal + +[LeetCode](https://leetcode.com/problems/valid-parentheses/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } diff --git a/en/docs/chapter_stack_and_queue/index.md b/en/docs/chapter_stack_and_queue/index.md index 0e77245c4..6fbb1f2d7 100644 --- a/en/docs/chapter_stack_and_queue/index.md +++ b/en/docs/chapter_stack_and_queue/index.md @@ -19,3 +19,4 @@ icon: material/stack-overflow - [5.2   Queue](queue.md) - [5.3   Deque](deque.md) - [5.4   Summary](summary.md) +- [5.5   Exercises](exercises.md) diff --git a/en/docs/chapter_tree/exercises.md b/en/docs/chapter_tree/exercises.md new file mode 100644 index 000000000..0d84f30e1 --- /dev/null +++ b/en/docs/chapter_tree/exercises.md @@ -0,0 +1,132 @@ +--- +comments: true +--- + + + +# 7.7   Exercises + +## 7.7.1   Concept Review + +### 1.   Complete, Full, and Perfect Binary Trees + +The following two arrays represent binary trees in level order, where `None` marks an empty position: + +- Tree A: `[1, 2, 3, 4, 5, 6]` +- Tree B: `[1, 2, 3, None, None, 6, 7]` + + + +1. Which tree is a complete binary tree? +2. Which tree is a full binary tree, meaning every non-leaf node has two children? +3. Is either tree a perfect binary tree? Explain the reason for each tree. + +??? success "Answer" + + 1. Tree A is a complete binary tree. Only its lowest level is not full, and the nodes on that level occupy consecutive positions from left to right. + Tree B is not complete because there are empty positions on the left of the lowest level while nodes still appear on the right. + + 2. Tree B is a full binary tree: nodes 1 and 3 each have two children, and all other nodes are leaves. + Tree A is not full because node 3 has only one child, its left child 6. + + 3. Neither tree is perfect because the lowest level of each tree is not completely filled. + +### 2.   Three Traversal Orders for the Same Tree + +Store the array `[1, 2, 3, 4, 5, 6, 7]` in level order in a complete binary tree. + + + +1. Draw the tree. +2. Write its preorder, inorder, and postorder traversal sequences. +3. In the inorder sequence, which parts of the tree correspond to the subsequences to the left and right of root node 1? + +??? success "Answer" + + 1. The tree is: + + ```text + 1 + / \ + 2 3 + / \ / \ + 4 5 6 7 + ``` + + 2. The preorder traversal is `1, 2, 4, 5, 3, 6, 7`; + the inorder traversal is `4, 2, 5, 1, 6, 3, 7`; + the postorder traversal is `4, 5, 2, 6, 7, 3, 1`. + + 3. The sequence `4, 2, 5` to the left of root node 1 is the inorder traversal of the left subtree; + the sequence `6, 3, 7` to its right is the inorder traversal of the right subtree. + +### 3.   Compare Two Binary Search Trees + +Insert each of the following sequences from left to right into an empty binary search tree: + +- Sequence A: `[4, 2, 6, 1, 3, 5, 7]` +- Sequence B: `[1, 2, 3, 4, 5, 6, 7]` + + + +1. For each tree, write the nodes visited when searching for 7. +2. If height is measured by the number of edges from the root node to the farthest leaf node, what is the height of each tree? +3. Based on the first two questions, is searching for 7 equally efficient in the two trees? Explain using the trees' shapes and search paths. + +??? success "Answer" + + 1. In the tree built from Sequence A, the search path is `4 → 6 → 7`. + In the tree built from Sequence B, the search path is `1 → 2 → 3 → 4 → 5 → 6 → 7`. + + 2. Every level of the first tree is full, and its height is 2. The second tree has only right children, and its height is 6. + + 3. Searching for 7 is not equally efficient in the two trees. The insertion order changes the shape and height of a binary search tree. The search visits only 3 nodes in the first tree + but all 7 nodes in the second. The taller the tree, the more nodes may need to be compared along a path in the worst case. + +## 7.7.2   Programming Exercises + +### 1.   Maximum Depth of a Binary Tree + +You are given the root node `root` of a binary tree. Each node contains an integer value and references to its left and right children. + +The maximum depth is the **number of nodes** on the path from the root node to the farthest leaf node. Return the maximum depth of the tree; the maximum depth of an empty tree is 0. +Use recursion. + +??? tip "Hints" + + 1. Depth is measured by the number of nodes in this exercise, so a tree containing only a root node has a maximum depth of 1 + 2. Let the recursive function return the maximum depth of the subtree rooted at the current node + 3. Return 0 for an empty node; for a nonempty node, return max(depth(left), depth(right)) + 1 + +[LeetCode](https://leetcode.com/problems/maximum-depth-of-binary-tree/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } + +### 2.   Traverse a Binary Tree Level by Level + +Given the root node `root` of a binary tree, use a queue to visit all nodes level by level from top to bottom and from left to right within each level. + +Return a two-dimensional array: the first subarray stores the values at the root's level, the second stores the values at the next level, and so on. +If the tree is empty, return an empty array. + +??? tip "Hints" + + 1. Level-order traversal visits earlier enqueued nodes first, so use a queue + 2. At the beginning of each round, all nodes currently in the queue belong to the same level + 3. First record the queue's length, then remove exactly that many nodes and enqueue their children + +[LeetCode](https://leetcode.com/problems/binary-tree-level-order-traversal/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } + +### 3.   Kth Smallest Element in a Binary Search Tree + +A binary search tree contains `n` nodes with distinct values. +If all node values are arranged from smallest to largest, their positions are numbered starting from 1. + +Given the root node `root` and an integer `k` satisfying `1 <= k <= n`, return the value at position `k`. +Find the answer directly during an inorder traversal rather than collecting all node values first. + +??? tip "Hints" + + 1. An inorder traversal of a binary search tree visits node values from smallest to largest + 2. Inorder traversal processes the left subtree, the current node, and then the right subtree; increment the count when visiting the current node + 3. When the count first equals k, the current node's value is the answer, so no further traversal is needed + +[LeetCode](https://leetcode.com/problems/kth-smallest-element-in-a-bst/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } diff --git a/en/docs/chapter_tree/index.md b/en/docs/chapter_tree/index.md index b03e60681..89348246e 100644 --- a/en/docs/chapter_tree/index.md +++ b/en/docs/chapter_tree/index.md @@ -21,3 +21,4 @@ icon: material/graph-outline - [7.4   Binary Search Tree](binary_search_tree.md) - [7.5   AVL Tree *](avl_tree.md) - [7.6   Summary](summary.md) +- [7.7   Exercises](exercises.md) diff --git a/ja/docs/chapter_array_and_linkedlist/exercises.md b/ja/docs/chapter_array_and_linkedlist/exercises.md new file mode 100644 index 000000000..021307aea --- /dev/null +++ b/ja/docs/chapter_array_and_linkedlist/exercises.md @@ -0,0 +1,112 @@ +--- +comments: true +--- + + + +# 4.6   演習 + +## 4.6.1   確認問題 + +### 1.   配列と連結リストが要素を見つける方法 + +配列と単方向連結リストに、どちらも `[A, B, C, D, E]` が順に保存されています。ここで 4 番目の要素 `D` を読み取ります。 + + + +1. 配列では、どのインデックスを直接使えますか? +2. 単方向連結リストでは、先頭ノード `A` から始めて、`next` をたどりながらどのノードを通りますか? +3. 読み取る位置が後ろになるほど、2 つの構造で必要な手順はどのように変わりますか?位置を指定した読み取りを繰り返すには、どちらが適していますか?その理由も説明してください。 + +??? success "解答" + + 1. インデックスが 0 から始まる場合、4 番目の要素のインデックスは 3 なので、配列では `arr[3]` を直接読み取れます。 + + 2. 単方向連結リストでは、必ず先頭から始めます。アクセスする経路は `A → B → C → D` で、`next` を 3 回たどります。 + + 3. 配列は先頭アドレスとインデックスから要素の位置を直接求められるため、位置を指定したアクセスの時間計算量は $O(1)$ です。 + 単方向連結リストで $k$ 番目のノードにアクセスするには、先頭ノードから `next` を $k-1$ 回たどる必要があり、 + 最悪の場合は $O(n)$ の時間がかかります。 + + ここでは位置を指定した読み取りだけを比較しており、連結リストがすべての操作で遅いという意味ではありません。 + +### 2.   配列と連結リストに要素を挿入する方法 + +配列と単方向連結リストに、どちらも `A、B、C、D` が保存されています。ここで `B` の後ろに `X` を挿入します。 + +- 配列の容量は 5 で、現在の状態は `[A, B, C, D, _]` です。 +- 連結リストは `A → B → C → D` で、ノード `B` を指す参照はすでに得られています。 + + + +1. 配列では、どの要素を移動する必要がありますか?挿入後の配列も書いてください。 +2. 連結リストでは、`X.next` と `B.next` をどの順序で変更すべきですか?挿入後の連結リストも書いてください。 +3. 挿入の効率を比較するときに、「ノード B を指す参照はすでに得られている」と明記する必要があるのはなぜですか? + +??? success "解答" + + 1. 配列では、まず `D` を 1 つ右へ移動し、次に `C` を 1 つ右へ移動します。最後に `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` の位置が分かっていれば、連結リストへの挿入は 2 つの接続を変更するだけなので、$O(1)$ の時間で行えます。 + 先頭から `B` を探す必要もある場合、その探索自体に $O(n)$ の時間がかかる可能性があります。 + +### 3.   リストの容量が増える仕組み + +配列を基に実装されたリストの現在の内容が `[A, B, C]`、長さが `size = 3`、容量が `capacity = 4` であるとします。 +容量が足りないときは、新しい配列の容量を元の 2 倍にするものとします。 + + + +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   プログラミング演習 + +### 1.   配列で表した大きな整数に 1 を足す + +配列 `digits` には、0 以上の整数の各桁が左から右へ保存されています。たとえば、`[3, 0, 8]` は 308 を表します。 +数値 0 は `[0]` で表し、それ以外の入力の先頭の桁は 0 ではありません。 + +十進数の筆算と同じように、この整数に 1 を足し、結果を同じ配列形式で返してください。 +`digits` は直接変更してかまいません。先頭に新しい繰り上がりが生じた場合は、より長い配列を返せます。 + +??? tip "解法のヒント" + + 1. 筆算の加算と同じように、配列の末尾の桁から始めます + 2. 現在の桁が 9 より小さければ、1 を足してすぐに返せます + 3. 現在の桁が 9 なら 0 に変えます。すべての桁が 9 だった場合は、先頭に新しく 1 を置く必要があります + +[LeetCode](https://leetcode.com/problems/plus-one/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } + +### 2.   単方向連結リストの反転 + +単方向連結リストの先頭ノード `head` が与えられます。各ノードには、値と次のノードを指す `next` が含まれています。 + +反復を使ってすべてのノード間の接続を反転し、反転後の先頭ノードを返してください。 +新しい連結リストノードを作ってはいけません。 + +??? tip "解法のヒント" + + 1. まず紙に 3 つの連続したノードと、2 つのポインタ prev、cur を描きます + 2. cur.next を書き換える前に、元の次のノードを nxt に保存する必要があります + 3. cur.next を反転した後、prev = cur、続いて cur = nxt とし、元の連結リストの次のノードを処理します + +[LeetCode](https://leetcode.com/problems/reverse-linked-list/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } diff --git a/ja/docs/chapter_array_and_linkedlist/index.md b/ja/docs/chapter_array_and_linkedlist/index.md index 0f8bf9dd6..3dbf4ba35 100644 --- a/ja/docs/chapter_array_and_linkedlist/index.md +++ b/ja/docs/chapter_array_and_linkedlist/index.md @@ -20,3 +20,4 @@ icon: material/view-list-outline - [4.3   リスト](list.md) - [4.4   メモリとキャッシュ *](ram_and_cache.md) - [4.5   まとめ](summary.md) +- [4.6   演習](exercises.md) diff --git a/ja/docs/chapter_backtracking/exercises.md b/ja/docs/chapter_backtracking/exercises.md new file mode 100644 index 000000000..ff2d6bd24 --- /dev/null +++ b/ja/docs/chapter_backtracking/exercises.md @@ -0,0 +1,93 @@ +--- +comments: true +--- + + + +# 13.6   演習 + +## 13.6.1   確認問題 + +### 1.   この順列アルゴリズムは結果を取りこぼすか + +あるバックトラッキングアルゴリズムは、`1、2、3` の順で試しながら、すべての順列を生成します。数値 `x` を選ぶたびに、次の処理を行います。 + +1. `x` を現在の経路の末尾に追加する。 +2. `x` を「使用済み」として記録する。 +3. 次の位置を埋めるために再帰する。 + +再帰から戻った後、ある生徒は経路の末尾から `x` を削除するだけで、次の数値を試しました。 + + + +1. このアルゴリズムが最初に得る順列は何ですか?6 通りすべての順列を得られますか? +2. 1 つ上の層へ戻る前に、経路の末尾の数値を削除するだけで十分ですか?不十分な場合、ほかに何をする必要がありますか?理由も説明してください。 + +??? success "解答" + + 1. 最初に `[1, 2, 3]` を得ますが、すべての順列を得ることはできません。戻るときに経路は短くなっても、 + 数値 1、2、3 の記録はすべて「使用済み」のままなので、それ以降の分岐では選べる数値がなくなります。 + + 2. 十分ではありません。経路の末尾から `x` を削除した後、`x` を「未使用」に戻す必要もあります。 + 現在の経路と使用済みの記録が一緒になって探索状態を表しています。選択時に 2 か所を変更したので、戻るときにも両方を元に戻さなければ、 + ほかの分岐で再び `x` を選べません。 + +### 2.   数値を選ぶ順序は重要か + +ソート済み配列 `[2, 3, 5]` と目標値 5 が与えられ、各数値は何度でも選べます。 +アルゴリズムでは、各探索経路の数値を小さい順にだけ並べるものとします。 + + + +1. どのような異なる組合せを得られますか? +2. 同じ数値の組を異なる順序で繰り返し探索する必要がないのはなぜですか?「小さい順」という制限にはどのような役割がありますか? +3. 現在の経路が `[3]` で、残りが 2 のとき、次の候補は 3 です。この時点で、同じ層の後ろにある候補をすべて調べずに終了できるのはなぜですか? + +??? success "解答" + + 1. 異なる組合せは `[2, 3]` と `[5]` です。 + + 2. この問題では `[2, 3]` と `[3, 2]` を同じ組合せとみなし、数値を選ぶ順序は答えに含めません。 + 経路の数値を小さい順に並べると定めることで、探索時に `[3, 2]` のような重複する組合せを最初から除外できます。 + + 3. 残りは 2 ですが、候補の 3 はすでに 2 より大きくなっています。配列はソート済みなので、 + 3 より後ろの候補はさらに大きく、どれも現在の組合せへ追加できません。そのため、この層の確認をそのまま終了できます。 + +### 3.   次のクイーンを置ける位置 + +`4 × 4` のチェス盤に、行ごとにクイーンを置きます。行と列のインデックスはいずれも 0 から始まります。 +現在、`(0, 1)` と `(1, 3)` にクイーンがあり、2 行目に次のクイーンを置こうとしています。 + + + +1. 「同じ列」にあるため除外される列はどれですか? +2. 残った列のうち、「同じ対角線」にあるため除外される位置はどれですか? +3. 2 行目で試せる位置はどこですか? + +??? success "解答" + + 1. 1 列目と 3 列目にはすでにクイーンがあるため、位置 `(2, 1)` と `(2, 3)` は除外されます。 + + 2. 残った位置のうち、`(2, 2)` は `(1, 3)` と同じ対角線上にあるため、これも除外されます。 + 位置 `(2, 0)` は、すでに置かれた 2 つのクイーンのどちらとも、同じ列にも同じ対角線上にもありません。 + + 3. 2 行目で試せる位置は `(2, 0)` だけです。 + + この段階では、現在の配置が条件を満たすことだけが分かります。この先でチェス盤を完成できなければ、戻って、それより前の別の選択を試す必要があります。 + +## 13.6.2   プログラミング演習 + +### 1.   重複要素のない全順列 + +整数配列 `nums` には少なくとも 1 つの要素があり、すべての要素が互いに異なります。 +各要素を 1 回ずつ使って作れるすべての並びを列挙し、それぞれの並びを配列として返してください。 +返す結果の中で、各順列を並べる順序は問いません。 +バックトラッキングを使い、各位置の要素が現在の順列にすでに選ばれているかをブール配列で記録してください。 + +??? tip "解法のヒント" + + 1. 再帰の深さは、順列の何番目の位置を埋めているかを表します + 2. 各層では、まだ使っていない要素だけを試します + 3. 経路の長さが `nums` の長さに達したら、そのコピーを答えへ追加します + +[LeetCode](https://leetcode.com/problems/permutations/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } diff --git a/ja/docs/chapter_backtracking/index.md b/ja/docs/chapter_backtracking/index.md index 491293e3a..c02e23667 100644 --- a/ja/docs/chapter_backtracking/index.md +++ b/ja/docs/chapter_backtracking/index.md @@ -20,3 +20,4 @@ icon: material/map-marker-path - [13.3   部分和問題](subset_sum_problem.md) - [13.4   n クイーン問題](n_queens_problem.md) - [13.5   まとめ](summary.md) +- [13.6   演習](exercises.md) diff --git a/ja/docs/chapter_computational_complexity/exercises.md b/ja/docs/chapter_computational_complexity/exercises.md new file mode 100644 index 000000000..5fb80f656 --- /dev/null +++ b/ja/docs/chapter_computational_complexity/exercises.md @@ -0,0 +1,116 @@ +--- +comments: true +--- + + + +# 2.6   演習 + +## 2.6.1   確認問題 + +### 1.   反復と再帰の時間・空間計算量 + +次の 2 つのコードは、どちらも $1 + 2 + \dots + n$ を計算します($n \ge 1$ とします)。`n` を 4 として、 +プログラムが実際に実行される順序に沿って次の問いに答え、2 つの書き方の効率を比較してください。 + +```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) +``` + + + +1. `sum_iter(4)` を実行すると、各ループの終了時に変数 `s` はそれぞれいくつになりますか? +2. `sum_recur(4)` を実行すると、どの関数が順に呼び出されますか?最も深い呼び出しから戻るとき、結果はどのように求められますか? +3. 2 つの書き方の時間計算量と空間計算量は、それぞれいくつですか?問い 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.   3 つのコードの時間計算量 + +次の 3 つのコード片はいずれも、正の整数 $n$ を入力とします。時間計算量が小さい順に並べ、それぞれの計算量を書いてください。 + +```python +# コード片 1 +s = 0 +for i in range(n): + s += i + +# コード片 2 +s = 0 +for i in range(n): + for j in range(i, n): + s += j + +# コード片 3 +while n > 1: + n = n // 2 +``` + +??? success "解答" + + 小さい順に、コード片 3 は $O(\log n)$、コード片 1 は $O(n)$、コード片 2 は $O(n^2)$ です。 + コード片 3 では、ループのたびに $n$ が半分になるため、ループ回数は約 $\log_2 n$ 回です。 + コード片 1 のループはちょうど $n$ 回実行されます。コード片 2 の内側のループ回数は順に + $n,n-1,\dots,1$ で、合計は $n(n+1)/2$ となるため、二次の計算量です。 + +### 3.   どちらの反転が空間を節約できるか + +配列 `nums` のすべての要素を逆順にする方法として、次の 2 つがあります。 + + + +1. 同じ長さの新しい配列 `res` を作り、逆順にコピーして返す。 +2. 2 つのインデックス `i` と `j` をそれぞれ先頭と末尾から中央へ動かし、`nums[i]` と `nums[j]` を順に交換する。 + + 2 つの方法の空間計算量は、それぞれいくつですか?どちらが「インプレース」な操作ですか? + +??? success "解答" + + 1. 入力と同じ長さの補助配列が必要なため、空間計算量は $O(n)$ です。 + + 2. 使うのは 2 つのインデックス変数だけなので、 + 空間計算量は $O(1)$ であり、インプレースな操作です。 + + ただし、インプレースな反転は入力配列を変更します。 + 入力の変更が許される場合にのみ優先して使うべきです。元の配列を残す必要がある場合は、方法 1 のコピーにかかる空間を省くことはできません。 + +## 2.6.2   プログラミング演習 + +### 1.   フィボナッチ数 + +フィボナッチ数列は、$F(0)=0$、$F(1)=1$ を満たし、$n\ge2$ のとき +$F(n)=F(n-1)+F(n-2)$ となります。 + +0 以上の整数 `n` が与えられます。再帰を使わず、ループを使って $F(n)$ を計算し、返してください。 + +??? tip "解法のヒント" + + 1. まず、n が 0 と 1 の場合をそれぞれ処理します + 2. 次の項の計算に必要なのは直前の 2 項だけで、数列全体を保存する必要はありません + 3. 2 つの変数を更新するときは、後で使う古い値を先に上書きしないよう注意します + +[LeetCode](https://leetcode.com/problems/fibonacci-number/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } diff --git a/ja/docs/chapter_computational_complexity/index.md b/ja/docs/chapter_computational_complexity/index.md index 0a0be1948..3fe11a1a2 100644 --- a/ja/docs/chapter_computational_complexity/index.md +++ b/ja/docs/chapter_computational_complexity/index.md @@ -20,3 +20,4 @@ icon: material/timer-sand - [2.3   時間計算量](time_complexity.md) - [2.4   空間計算量](space_complexity.md) - [2.5   まとめ](summary.md) +- [2.6   演習](exercises.md) diff --git a/ja/docs/chapter_data_structure/exercises.md b/ja/docs/chapter_data_structure/exercises.md new file mode 100644 index 000000000..f53232730 --- /dev/null +++ b/ja/docs/chapter_data_structure/exercises.md @@ -0,0 +1,92 @@ +--- +comments: true +--- + + + +# 3.6   演習 + +## 3.6.1   確認問題 + +### 1.   身近な場面にあるデータの関係 + +データ同士の関係に基づいて、次の 3 つの場面に「線形構造、木構造、網状構造」のいずれかを選び、理由を説明してください。 + + + +1. 生徒が一列に並び、それぞれが自分の前と後ろにいる人だけに注目する。 +2. 学校が「学校 → 学年 → クラス」という階層で管理されている。 +3. 都市の道路が複数の交差点を結び、1 つの交差点から複数の別の交差点へ行けるほか、道路が環状につながることもある。 + +??? success "解答" + + 1. 線形構造です。列の先頭と最後にいる人を除き、それぞれの人は前後の 1 人ずつとだけ隣り合い、関係が一本の線に沿って並んでいます。 + + 2. 木構造です。各クラスはいずれか 1 つの学年に属し、各学年はさらに学校に属しており、関係が上から下へ階層的に広がっています。 + + 3. 網状構造です。1 つの交差点を複数の交差点と結ぶことができ、経路が環状につながる場合もあるため、1 つの順序や厳密な階層では表せません。 + + 構造を判断するときは、メモリをどれだけ使うかを先に考えるのではなく、まず要素同士の関係を観察します。 + +### 2.   論理的な順序をメモリに保存する方法 + +論理的な順序 `A → B → C` を保存するために、次の 2 つの簡略化したメモリ配置を考えます。 + +- 方法 A:`A、B、C` を、それぞれ番号 `20、21、22` のメモリ領域に置く。 +- 方法 B:`A、B、C` を、それぞれ番号 `20、7、31` のメモリ領域に置き、`A` に `B` の位置、`B` に `C` の位置を記録する。 + + + +1. どちらが連続した領域への保存で、どちらが分散した領域への保存ですか? +2. 2 つの方法は、それぞれ配列と連結リストのどちらに近いですか? +3. 方法 B のメモリ領域の番号は大きさの順に並んでいません。それでも `A → B → C` という論理的な順序を表せるのはなぜですか? + +??? success "解答" + + 1. 方法 A は連続したメモリ領域を使っているため、連続した領域への保存です。方法 B のノードは別々の場所に分散しているため、分散した領域への保存です。 + + 2. 方法 A は配列に、方法 B は連結リストに近いです。 + + 3. 論理的な順序を決めるのはノード間に記録された接続関係であり、メモリ領域の番号の大小ではありません。 + `A` に記録された位置から `B` を見つけ、さらに `B` に記録された位置から `C` を見つけられるため、`A、B、C` の順にアクセスできます。 + + このことからも、論理構造と物理構造は、同じデータを異なる視点から捉えたものだと分かります。 + +### 3.   宿題の記録におけるデータ型と構造 + +ある学習グループが、座席順に 4 人の生徒が宿題を提出したかどうかを記録したところ、次の結果になりました。 + +`[true, false, true, true]` + + + +1. 各要素には、どの基本データ型が適していますか? +2. この 4 つの要素は座席順に一列に並んでいます。どのような論理構造を使っていますか? +3. 今後、各生徒の宿題の点数を `[90, 0, 85, 100]` と記録するように変えた場合、変わるのはデータの「内容の型」と「構成方法」のどちらですか?理由も説明してください。 + +??? success "解答" + + 1. 各要素は「はい」か「いいえ」だけを表すため、ブール型 `bool` が適しています。 + + 2. これらの要素は座席順に並んで線形構造を作っており、配列で保存できます。 + + 3. 変わるのは内容の型です。要素がブール値から整数に変わります。一方、構成方法は変わりません。 + データは引き続き座席順に一列に並び、配列という線形構造を使えます。 + + 基本データ型は「何を保存するか」を、データ構造は「データをどのように構成するか」を表します。 + +## 3.6.2   プログラミング演習 + +### 1.   二進表現に含まれる 1 の個数 + +0 以上の整数 `n` が与えられます。その二進表現に 1 がいくつ含まれるかを数えてください。 + +ビット演算を使って求めてください。二進表現を文字列に変換したり、1 を直接数える組み込み関数を使ったりしてはいけません。 + +??? tip "解法のヒント" + + 1. n & 1 で n の最下位ビットを取り出し、そのビットが 1 かどうかを判定できます + 2. 1 ビット右へシフトすると、現在の最下位ビットが取り除かれます。多くの言語では演算子 >> を使います + 3. 各ビットを調べて右へシフトする方法を完成させたら、n & (n - 1) が n の最も右にある 1 を 0 に変えることも確認してみましょう + +[LeetCode](https://leetcode.com/problems/number-of-1-bits/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } diff --git a/ja/docs/chapter_data_structure/index.md b/ja/docs/chapter_data_structure/index.md index ea49b8dc8..6a773c948 100644 --- a/ja/docs/chapter_data_structure/index.md +++ b/ja/docs/chapter_data_structure/index.md @@ -20,3 +20,4 @@ icon: material/shape-outline - [3.3   数値エンコーディング *](number_encoding.md) - [3.4   文字エンコーディング *](character_encoding.md) - [3.5   まとめ](summary.md) +- [3.6   演習](exercises.md) diff --git a/ja/docs/chapter_divide_and_conquer/exercises.md b/ja/docs/chapter_divide_and_conquer/exercises.md new file mode 100644 index 000000000..409ae4325 --- /dev/null +++ b/ja/docs/chapter_divide_and_conquer/exercises.md @@ -0,0 +1,102 @@ +--- +comments: true +--- + + + +# 12.6   演習 + +## 12.6.1   確認問題 + +### 1.   分割統治に適した処理 + +ある生徒は、「まず 2 つに分け、それぞれを解いてから結果をまとめる」という方法で、次の処理を行おうとしています。 +それぞれについて、「分割統治に適している」「分割統治はできるが、作業の総量は減らない」「2 つを独立して解けない」のいずれかを判断し、理由を説明してください。 + + + +1. ソートされていない配列をソートする。 +2. 配列の最大値を求める。 +3. 一連の `push(x)`、`pop()` というスタック操作を順に実行し、各 `pop()` で得た要素を出力する。 + +??? success "解答" + + 1. 適しています。半分に分け、2 つを独立してソートし、$O(n)$ でマージします。これはマージソートそのものです。 + 2. 分割統治はできますが、作業の総量は減りません。左右の 2 つを合わせると、結局は $n$ 個の要素をすべて確認する必要があり、 + 直接走査する場合と同じく $O(n)$ の時間がかかります。 + 3. 2 つを独立して解くことはできません。後半を開始するときのスタックの内容が前半の実行結果に依存するため、 + 互いの結果を知らない状態では独立して処理できません。 + +### 2.   高速べき乗で計算を減らす仕組み + +次の再帰関数は、分割統治を使って $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)` を計算します。 + + + +1. 再帰呼び出しでは、引数 `n` はどのような値に順に変わりますか? +2. 最も深い呼び出しから戻るとき、各呼び出しはどの値を順に返しますか? +3. `fast_pow(x, n // 2)` を 2 回書くのではなく、先に `half` へ保存するのはなぜですか? + +??? 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)` を 1 回ずつ書くと、2 つの再帰呼び出しがまったく同じ部分問題を計算します。 + 結果を先に `half` へ保存すれば、各層で再帰するのは 1 回だけとなり、再帰の深さは約 $\log n$ です。 + 2 回呼び出すと、大量の重複計算が発生します。 + +### 3.   走査列から左右の部分木を分ける + +重複するノードを持たない二分木の先行順走査と中間順走査は、それぞれ次のとおりです。 + +- 先行順走査:`[A, B, D, E, C]` +- 中間順走査:`[D, B, E, A, C]` + +根ノードの層だけを分けてください。それ以降の再帰を行ったり、木全体を描いたりする必要はありません。 + + + +1. 根ノードは何ですか? +2. 左右の部分木は、中間順走査のどの部分にそれぞれ対応しますか? +3. 左右の部分木は、先行順走査のどの部分にそれぞれ対応しますか?根ノードの直接の子ノードは何ですか? + +??? success "解答" + + 1. 先行順走査の最初のノードが根ノードなので、根ノードは `A` です。 + + 2. `A` によって中間順走査は 2 つに分かれます。左部分木は `[D, B, E]`、右部分木は `[C]` です。 + + 3. 左部分木には 3 つのノードがあるため、根ノード `A` の後ろにある 3 つの先行順走査の要素が左部分木に属し、 + `[B, D, E]` となります。残りの `[C]` は右部分木に属します。 + したがって、根ノードの左子ノードは `B`、右子ノードは `C` です。 + +## 12.6.2   プログラミング演習 + +### 1.   高速べき乗 + +実数 `x` と整数 `n` が与えられます。言語に組み込まれたべき乗関数を呼び出さず、$x^n$ を計算してください。 +再帰による分割統治を使い、毎回指数を半分にして、計算済みの部分問題の結果を再利用してください。 +この問題では `x = 0` の場合も含めて $x^0=1$ とします。`n < 0` のときは `x != 0` が保証され、答えを $(1/x)^{-n}$ に変換できます。 + +??? tip "解法のヒント" + + 1. n が 0 のとき、答えは 1 です + 2. x の n // 2 乗を計算したら half に保存し、2 回目の再帰呼び出しを行わないようにします + 3. n < 0 のときは、まず x を 1 / x に変え、次に n を -n に変えます。C++ または Java を使う場合は、最小の 32 ビット整数の符号を反転したときのオーバーフローを避けるため、先に n を 64 ビット整数へ変換できます + +[LeetCode](https://leetcode.com/problems/powx-n/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } diff --git a/ja/docs/chapter_divide_and_conquer/index.md b/ja/docs/chapter_divide_and_conquer/index.md index 5abacb4f6..8a8391815 100644 --- a/ja/docs/chapter_divide_and_conquer/index.md +++ b/ja/docs/chapter_divide_and_conquer/index.md @@ -20,3 +20,4 @@ icon: material/set-split - [12.3   二分木の構築問題](build_binary_tree_problem.md) - [12.4   ハノイの塔の問題](hanota_problem.md) - [12.5   まとめ](summary.md) +- [12.6   演習](exercises.md) diff --git a/ja/docs/chapter_dynamic_programming/exercises.md b/ja/docs/chapter_dynamic_programming/exercises.md new file mode 100644 index 000000000..87fe12674 --- /dev/null +++ b/ja/docs/chapter_dynamic_programming/exercises.md @@ -0,0 +1,117 @@ +--- +comments: true +--- + + + +# 14.8   演習 + +## 14.8.1   確認問題 + +### 1.   動的計画法が適している場面 + +ある生徒は、「漸化式を書けるなら、必ず動的計画法を使うべきです」と言いました。 +次の 3 つの処理には、動的計画法、バックトラッキング、または `dp` テーブルを作らないループや数式のどれが適しているかを判断し、重要な理由を 1 つ書いてください。 + + + +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 通りの順列を 1 つずつ生成する問題なので、1 つの選択を試して探索を続け、 + 選択を取り消して別の分岐を試すバックトラッキングが使えます。どのような方法でも、これらの順列を実際に出力するには列挙を省けません。 + + 3. ループまたは等差数列の公式で十分です。`S(i) = S(i-1) + i` という漸化式は書けますが、`S(i)` の計算が依存するのは 1 つ前の `S(i-1)` だけで、 + 各部分和は 1 回だけ計算すればよく、重複部分問題がありません。そのため、`dp` テーブルは不要です。「漸化式を書ける」ことと「動的計画法が必要である」ことは同じではありません。 + +### 2.   ナップサック表の 1 つの値を求める + +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` であることが分かっています。 + + + +1. 3 番目の品物を選ばない場合、候補となる価値はいくつですか? +2. 3 番目の品物を選ぶ場合、容量はいくつ残り、候補となる価値はいくつですか? +3. `dp[3][4]` はいくつにすべきですか?どの品物を選んだことに対応しますか? + +??? success "解答" + + 1. 3 番目の品物を選ばない場合は、最初の 2 つの品物による結果をそのまま使うため、候補となる価値は `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 ナップサックにおける「選ぶか、選ばないか」という 1 回の比較を表しています。 + +### 3.   ナップサック容量を更新する順序 + +0-1 ナップサックに、重さ 2、価値 5 の品物が 1 つだけあり、ナップサックの容量は 4 です。 +各品物は 1 回までしか選べません。最初の一次元配列は `dp = [0, 0, 0, 0, 0]` です。 + +ある生徒は、この品物を処理するとき、容量を 2 から 4 の順で更新しました。 + +- `dp[2]` を更新すると 5 になる。 +- `dp[3]` を更新しても 5 になる。 +- `dp[4]` を更新するとき、更新したばかりの `dp[2]` を使うため、`dp[4] = 10` になる。 + + + +1. `dp[4] = 10` という結果は正しいですか?その理由も説明してください。 +2. 「各品物は 1 回までしか選べない」という条件に基づくと、`dp[4]` はいくつですか? +3. 各品物を処理するとき、容量は大きいほうから小さいほうへ更新しますか、それとも小さいほうから大きいほうへ更新しますか?その順序によって、どのような問題を防げますか? + +??? success "解答" + + 1. この結果は正しくありません。価値 10 は、この価値 5 の品物を 2 回入れたことに相当し、 + 「各品物は 1 回までしか選べない」という条件に反します。 + + 2. ナップサックへ入れられるのはこの品物 1 つだけなので、正しい `dp[4]` は 5 です。 + + 3. 容量は大きいほうから小さいほうへ、つまり 4、3、2 の順に更新します。 + この順序なら、`dp[c]` の計算時に読み取る `dp[c-2]` は、現在の品物を処理する前の値のままなので、 + 同じ回の中で現在の品物を繰り返し使うことを防げます。 + +## 14.8.2   プログラミング演習 + +### 1.   階段を上る方法の数 + +`n` 段の階段があります。1 回に 1 段または 2 段だけ上ることができ、ちょうど `n` 段目に着かなければなりません。 +異なる上り方が何通りあるかを求めてください。`n >= 1` とし、それぞれの上り方は、各回に 1 段進むか 2 段進むかだけで区別します。 +一次元の動的計画法の配列を使って求め、2 つの状態だけを残す空間最適化は、ここでは使わないでください。 + +??? tip "解法のヒント" + + 1. i 段目へ到達する最後の一歩では、1 段または 2 段だけ上れます + 2. そのため、dp[i] = dp[i-1] + dp[i-2] となります + 3. まず n が 1 と 2 の場合を処理し、3 段目から表を埋めます + +[LeetCode](https://leetcode.com/problems/climbing-stairs/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } + +### 2.   0-1 ナップサック + +同じ長さの配列 `wgt` と `val` が与えられます。`i` 番目の品物の重さは正の整数 `wgt[i]`、価値は 0 以上の整数 `val[i]` で、 +ナップサックの容量 `cap` は 0 以上の整数です。各品物は 1 回までしか選べません。総重量が `cap` 以下という条件で、 +ナップサックに入れられる最大の総価値を求めてください。一次元の動的計画法を使って実装してください。 + +??? tip "解法のヒント" + + 1. 長さ cap + 1 の配列 dp を初期化します。dp[c] は、容量上限が c のときの最大価値を表します + 2. 品物 i を処理するとき、「選ばない」場合の dp[c] と、「選ぶ」場合の dp[c-wgt[i]] + val[i] を比較します + 3. 同じ回の中で現在の品物を繰り返し選ばないように、容量は必ず大きいほうから小さいほうへ更新します diff --git a/ja/docs/chapter_dynamic_programming/index.md b/ja/docs/chapter_dynamic_programming/index.md index dff7b1047..468846bc3 100644 --- a/ja/docs/chapter_dynamic_programming/index.md +++ b/ja/docs/chapter_dynamic_programming/index.md @@ -22,3 +22,4 @@ icon: material/table-pivot - [14.5   完全ナップサック問題](unbounded_knapsack_problem.md) - [14.6   編集距離問題](edit_distance_problem.md) - [14.7   まとめ](summary.md) +- [14.8   演習](exercises.md) diff --git a/ja/docs/chapter_graph/exercises.md b/ja/docs/chapter_graph/exercises.md new file mode 100644 index 000000000..f4ed8bbc8 --- /dev/null +++ b/ja/docs/chapter_graph/exercises.md @@ -0,0 +1,109 @@ +--- +comments: true +--- + + + +# 9.5   演習 + +## 9.5.1   確認問題 + +### 1.   同じグラフを 2 つの方法で表す + +無向グラフに 4 つの頂点 `A、B、C、D` があり、辺は +`A-B、A-C、B-C、C-D` です。 + + + +1. このグラフの隣接リストを書いてください。 +2. 0 と 1 だけを使って隣接行列を埋めてください。 +3. `A` と `D` が直接つながっているかを調べるとき、1 つの記録を見るだけで済むのは、どちらの表現方法ですか? +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` 列を直接見ればよいため、任意の 2 頂点が直接つながっているかを調べるのに適しています。 + + 4. 頂点が多く、辺が少ない場合、隣接リストは実際に存在する辺だけを記録します。そのため、頂点のすべての組に対して領域を用意する隣接行列よりも、一般に空間を節約できます。 + +### 2.   幅優先走査と深さ優先走査の訪問順序 + +無向グラフの頂点は `A、B、C、D、E`、辺は +`A-B、A-C、B-D、C-D、D-E` です。 + +A から開始し、未訪問の隣接頂点が複数ある場合はアルファベット順に選ぶものとします。 + + + +1. 幅優先走査(BFS)の訪問順序を書いてください。 +2. 再帰による深さ優先走査(DFS)の訪問順序を書いてください。 +3. どちらの走査でも、訪問済みの頂点を記録する必要があるのはなぜですか? + +??? success "解答" + + 1. BFS の訪問順序は `A, B, C, D, E` です。まず A から辺 1 本で到達できる 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.   1 回の BFS でグラフ全体を訪問できるか + +無向グラフに頂点 `A、B、C、D、E、F` があり、辺は +`A-B、B-C、D-E` だけです。 + + + +1. A から 1 回 BFS を行うと、どの頂点を訪問できますか? +2. 問い 1 の結果から、この 1 回の BFS でグラフのすべての頂点を訪問できたといえますか?その理由も説明してください。 +3. すべての頂点をアルファベット順に調べ、未訪問の頂点に出会うたびに新しい BFS を開始します。 + 各 BFS の開始頂点は何ですか?このグラフはいくつの互いにつながっていない部分(連結成分)に分かれますか? + +??? success "解答" + + 1. A からは `A、B、C` だけを訪問できます。 + + 2. すべての頂点を訪問できていません。`D、E` は別の互いにつながった部分を作り、F は単独の頂点です。 + これらと A の間には経路がないため、A から到達できません。 + + 3. 3 回の BFS の開始頂点は順に `A、D、F` で、それぞれ + `{A, B, C}`、`{D, E}`、`{F}` を訪問します。したがって、このグラフには 3 つの連結成分があります。 + +## 9.5.2   プログラミング演習 + +### 1.   無向グラフに経路が存在するかを判定する + +$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.com/problems/find-if-path-exists-in-graph/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } diff --git a/ja/docs/chapter_graph/index.md b/ja/docs/chapter_graph/index.md index 6b5d53eec..1c04374cb 100644 --- a/ja/docs/chapter_graph/index.md +++ b/ja/docs/chapter_graph/index.md @@ -19,3 +19,4 @@ icon: material/graphql - [9.2   グラフの基本操作](graph_operations.md) - [9.3   グラフの走査](graph_traversal.md) - [9.4   まとめ](summary.md) +- [9.5   演習](exercises.md) diff --git a/ja/docs/chapter_greedy/exercises.md b/ja/docs/chapter_greedy/exercises.md new file mode 100644 index 000000000..c744a0a56 --- /dev/null +++ b/ja/docs/chapter_greedy/exercises.md @@ -0,0 +1,89 @@ +--- +comments: true +--- + + + +# 15.6   演習 + +## 15.6.1   確認問題 + +### 1.   毎回最大の硬貨を選べば必ず最善か + +硬貨の額面は `[1, 7, 10]`、目標金額は 14 です。 + + + +1. 「残りの金額を超えない最大の額面を毎回選ぶ」という方法を使い、選ぶ硬貨を書いてください。 +2. より少ない硬貨を使う方法はありますか?ある場合は 1 つ書き、ない場合は理由を説明してください。 +3. この結果から、この貪欲戦略がどのような硬貨の額面でも正しいといえますか? + +??? success "解答" + + 1. 貪欲戦略では `10 + 1 + 1 + 1 + 1` の順に選び、硬貨は合計 5 枚です。 + + 2. より少ない硬貨を使う方法があります。`7 + 7` なら、硬貨は 2 枚だけです。 + + 3. 正しいとはいえません。この反例から、どのような硬貨の額面でも、現在選べる最大の額面を毎回選ぶ方法が最小の硬貨枚数を得るとは限らないと分かります。 + 目の前で最大の選択をすると、その後のよりよい組合せを作れなくなる場合があります。 + +### 2.   ナップサックに先に入れる品物 + +容量 4 キログラムのナップサックに、次の品物を入れます。品物の一部だけを入れることもでき、 +得られる価値は入れた重さに比例します。 + +- 品物 A:重さ 4 キログラム、価値 20。 +- 品物 B:重さ 3 キログラム、価値 18。 + + + +1. 2 つの品物の 1 キログラム当たりの価値は、それぞれいくつですか?どちらを先に入れるべきですか? +2. 分数ナップサックの貪欲戦略に従ってナップサックを満たすと、最終的な価値はいくつですか? +3. 品物を分割でき、ナップサックに総重量の制限がある場合、品物を選ぶときに比較すべきなのは、総価値と 1 キログラム当たりの価値のどちらですか?その理由も説明してください。 + +??? success "解答" + + 1. A の 1 キログラム当たりの価値は `20 ÷ 4 = 5`、B は `18 ÷ 3 = 6` です。 + したがって、単位重量当たりの価値が高い B を先に入れます。 + + 2. まず B をすべて入れ、3 キログラムを使って価値 18 を得ます。残りの容量は 1 キログラムなので、 + 次に A を 1 キログラムだけ入れて価値 5 を得ます。最終的な価値は `18 + 5 = 23` です。 + + 3. ナップサックが制限するのは総重量で、品物を分割できるため、単位重量当たりの価値を比較すべきです。 + A は総価値が高くても、1 キログラム当たりの価値は B より低くなっています。A を先に入れてナップサックを満たすと、価値 20 しか得られません。 + +### 3.   2 つのポインタを次にどう動かすか + +仕切り板の高さが `[1, 8, 6, 2, 5]` であるとき、先頭と末尾に置いた 2 つのポインタを使って最大容量を求めます。 +容量は「2 枚の仕切り板のうち低いほうの高さ × 2 枚の仕切り板のインデックスの差」です。 + + + +1. 最初は左のポインタがインデックス 0、右のポインタがインデックス 4 にあります。現在の容量はいくつですか?次にどちらのポインタを動かしますか? +2. 問い 1 で選んだポインタを 1 回動かした後、2 つのポインタはそれぞれどのインデックスにありますか?このときの容量はいくつですか?次はどちらのポインタを動かしますか? +3. 現在の 2 枚の仕切り板について、低い仕切り板のポインタを動かすことも、高い仕切り板のポインタを動かすこともできます。より大きな容量を得られる可能性が残るのはどちらですか?その理由も説明してください。 + +??? success "解答" + + 1. 現在の容量は `min(1, 5) × (4 - 0) = 4` です。左側の仕切り板のほうが低いため、左のポインタを動かします。 + + 2. 左のポインタを動かすと、2 つのポインタはそれぞれインデックス 1 と 4 にあります。現在の容量は + `min(8, 5) × (4 - 1) = 15` です。右側の仕切り板のほうが低いため、次は右のポインタを動かします。 + + 3. 低い仕切り板に対応するポインタを動かす場合に限り、より大きな容量を得られる可能性が残ります。高い仕切り板を動かすと、2 枚の間の距離は必ず短くなり、容器の高さは動かしていない低い仕切り板によって引き続き制限されるため、 + 高さは変わらないか低くなります。したがって、容量が移動前より大きくなることはありません。低い仕切り板を動かした場合にだけ、より高い仕切り板に出会える可能性があります。 + +## 15.6.2   プログラミング演習 + +### 1.   分数ナップサック + +同じ長さの配列 `wgt` と `val` が与えられ、`wgt[i] > 0`、`val[i] >= 0`、ナップサックの容量は `cap >= 0` です。 +各種類の品物は 1 個ずつしかありませんが、その一部だけを入れることもできます。 +得られる価値は、入れた重さがその品物の総重量に占める割合に応じて決まります。貪欲法を使い、 +ナップサックで得られる最大の総価値を実数で返してください。 + +??? tip "解法のヒント" + + 1. まず、各品物の単位重量当たりの価値 val[i] / wgt[i] を計算します。除算結果の小数部分も残します + 2. 単位重量当たりの価値が高い品物からナップサックへ入れます + 3. 残り容量が現在の品物の重さより小さければ、ナップサックをちょうど満たす分だけ入れて終了します diff --git a/ja/docs/chapter_greedy/index.md b/ja/docs/chapter_greedy/index.md index 71d4ff400..e2eba4d2b 100644 --- a/ja/docs/chapter_greedy/index.md +++ b/ja/docs/chapter_greedy/index.md @@ -20,3 +20,4 @@ icon: material/head-heart-outline - [15.3   最大容量問題](max_capacity_problem.md) - [15.4   最大積分割問題](max_product_cutting_problem.md) - [15.5   まとめ](summary.md) +- [15.6   演習](exercises.md) diff --git a/ja/docs/chapter_hashing/exercises.md b/ja/docs/chapter_hashing/exercises.md new file mode 100644 index 000000000..8362eaaed --- /dev/null +++ b/ja/docs/chapter_hashing/exercises.md @@ -0,0 +1,107 @@ +--- +comments: true +--- + + + +# 6.5   演習 + +## 6.5.1   確認問題 + +### 1.   ハッシュ衝突後の探索方法 + +5 個のバケットを持つハッシュテーブルがあり、ハッシュ関数は $h(x)=x \bmod 5$ です。衝突が起きた場合は、要素をそのバケットのリストへ順に格納します。 +`[1, 6, 11, 7]` をこの順に挿入します。 + + + +1. 0~4 番の各バケットの内容を書いてください。 +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 の順に比較します。2 回目の比較で目的の値が見つかります。 + + 3. ハッシュ値が同じであることは、同じバケットに入ることを表すだけで、それらの要素が等しいという意味ではありません。連鎖アドレス法では、衝突した要素をすべてバケット内に保存し、 + 探索時に 1 つずつ比較します。そのため、1、6、11 が互いに上書きされることはありません。 + +### 2.   ハッシュテーブルを拡張すると要素はどこへ移るか + +連鎖アドレス法を使うハッシュテーブルには、もともと 5 個のバケットがあり、ハッシュ関数は $h(x)=x\bmod5$ です。 +キー `[1, 6, 11]` はすべて 1 番のバケットに入っています。 + +ここでハッシュテーブルを 7 個のバケットに拡張し、ハッシュ関数も $h(x)=x\bmod7$ に変えます。 + + + +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 つのキーは同じバケットに集中しなくなります。 + + 3. そのままコピーすることはできません。バケット番号は「キーをバケット数で割った余り」で決まります。バケット数が 5 から 7 に変わると、同じキーでも新しいバケット番号が変わる場合があるため、 + 各キーの位置を計算し直す必要があります。古い 1 番のバケットをそのままコピーすると、新しい式で 6 と 11 を探索したときに、 + それぞれ 6 番と 4 番のバケットへ進むため、見つけられなくなります。 + +### 3.   6 を削除した後も 11 を見つけられるか + +5 個の位置を持つハッシュテーブルがあり、インデックスは `0~4`、ハッシュ関数は $h(x)=x\bmod5$ です。 +衝突が起きた場合は、ハッシュ関数で得たインデックスから右へ進み、最初の空き位置を探します。 + +`[1, 6, 11]` をこの順に挿入します。 + + + +1. 3 つの数は、最終的にそれぞれどのインデックスに入りますか? +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   プログラミング演習 + +### 1.   2 つの文字列の文字構成を比較する + +小文字の英字だけを含む 2 つの文字列 `s` と `t` が与えられます。 +`s` の文字は自由に並べ替えられますが、文字の追加、削除、置換はできません。 + +並べ替えることで `t` を作れる場合は `true`、作れない場合は `false` を返してください。 +文字列内の文字をソートせず、ハッシュテーブルを使って各文字の出現回数を記録してください。 + +??? tip "解法のヒント" + + 1. 2 つの文字列の長さが異なる場合、文字の構成も必ず異なります + 2. ハッシュテーブルに各文字の個数を記録します。s を走査するとき、対応するカウントに 1 を足します + 3. t を走査するとき、対応するカウントから 1 を引きます。最終的にすべてのカウントが 0 の場合に限り、文字の構成が同じです + +[LeetCode](https://leetcode.com/problems/valid-anagram/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } diff --git a/ja/docs/chapter_hashing/index.md b/ja/docs/chapter_hashing/index.md index aa862e991..3b67518b4 100644 --- a/ja/docs/chapter_hashing/index.md +++ b/ja/docs/chapter_hashing/index.md @@ -19,3 +19,4 @@ icon: material/table-search - [6.2   ハッシュ衝突](hash_collision.md) - [6.3   ハッシュアルゴリズム](hash_algorithm.md) - [6.4   まとめ](summary.md) +- [6.5   演習](exercises.md) diff --git a/ja/docs/chapter_heap/exercises.md b/ja/docs/chapter_heap/exercises.md new file mode 100644 index 000000000..5b4396b73 --- /dev/null +++ b/ja/docs/chapter_heap/exercises.md @@ -0,0 +1,92 @@ +--- +comments: true +--- + + + +# 8.5   演習 + +## 8.5.1   確認問題 + +### 1.   10 をヒープへ追加した後の調整 + +配列 `[9, 7, 8, 3, 5]` は最大ヒープを表しています。ここに 10 を追加します。 + + + +1. まず 10 を配列の末尾に追加すると、その親ノードの値はいくつですか? +2. 新しいノードから下から上へヒープ化し、交換するたびに配列を書いてください。 +3. 最終的なヒープ頂点の要素はいくつですか?交換は全部で何回行われますか? + +??? success "解答" + + 1. 10 を追加した後のインデックスは 5 で、その親ノードのインデックスは + $\lfloor(5-1)/2\rfloor=2$ です。親ノードの値は 8 です。 + + 2. 10 は 8 より大きいため、1 回目の交換後は `[9, 7, 10, 3, 5, 8]` です。 + 10 は親ノードの 9 よりも大きいため、2 回目の交換後は `[10, 7, 9, 3, 5, 8]` です。 + これで 10 が根ノードに到達したため、ヒープ化は終了します。 + + 3. 最終的なヒープ頂点は 10 で、交換回数は 2 回です。 + +### 2.   最小ヒープの親子関係を確認する + +配列 `[1, 4, 3, 7, 6, 2]` は完全二分木を表しています。最小ヒープでは、すべての親ノードがその子ノード以下でなければなりません。 +インデックス $i$ に対して、左右の子ノードのインデックスはそれぞれ $2i+1$ と $2i+2$ です。 + + + +1. インデックス 2 の子ノードのインデックスと値は、それぞれいくつですか? +2. インデックス 2 にあるノードの値は 3 です。子ノードとの間で最小ヒープの条件に反していますか?反している場合、どの 2 要素を交換しますか? +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.   最小ヒープで最大の 3 つの数を保持する + +データストリーム `[4, 1, 7, 3, 8]` から最大の 3 つの数を保持するために、大きさが 3 以下の最小ヒープを管理します。 + +まず、最初の 3 つの数を順に最小ヒープへ入れます。ヒープが満杯になった後は、新しい数を 1 つ読み込むたびに、 +その数がヒープ頂点より大きければヒープ頂点を削除して新しい数を入れ、そうでなければヒープを変更しません。 + +各数を読み込んだ後、ヒープに保持されている数とヒープ頂点を書いてください。 +保持されている数は集合として書けばよく、ヒープ配列内での並びを書く必要はありません。 + +??? 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   プログラミング演習 + +### 1.   配列内の 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.com/problems/kth-largest-element-in-an-array/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } diff --git a/ja/docs/chapter_heap/index.md b/ja/docs/chapter_heap/index.md index 86d5b7ad3..2daaee741 100644 --- a/ja/docs/chapter_heap/index.md +++ b/ja/docs/chapter_heap/index.md @@ -19,3 +19,4 @@ icon: material/family-tree - [8.2   ヒープ構築](build_heap.md) - [8.3   Top-k 問題](top_k.md) - [8.4   まとめ](summary.md) +- [8.5   演習](exercises.md) diff --git a/ja/docs/chapter_searching/exercises.md b/ja/docs/chapter_searching/exercises.md new file mode 100644 index 000000000..e3c174ff7 --- /dev/null +++ b/ja/docs/chapter_searching/exercises.md @@ -0,0 +1,100 @@ +--- +comments: true +--- + + + +# 10.7   演習 + +## 10.7.1   確認問題 + +### 1.   二分探索で探索区間を狭める方法 + +ソート済み配列 `[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.   重複する要素の左右の境界 + +配列 `[1, 2, 2, 2, 4, 6]` から数値 2 を探索します。 +ある生徒は、二分探索でインデックス 2 に目的の値を見つけるとすぐに返し、次のように言いました。 +「インデックス 2 が数値 2 の左端境界です。」 + + + +1. この生徒の説明は正しいですか?数値 2 の左端境界と右端境界は、それぞれどこですか?理由も説明してください。 +2. 左端境界を探索するとき、中点の要素が目的の値と等しければ、次にどちら側を探索すべきですか? +3. 右端境界を探索するときは、どちら側を探索すべきですか?方向だけを説明し、探索過程をすべて書く必要はありません。 + +??? success "解答" + + 1. この生徒の説明は正しくありません。2 を 1 つ見つけてすぐに返す方法では、いずれかの 2 を見つけたことしか保証できず、最も左または最も右の 2 だとは限りません。 + この問題の左端境界はインデックス 1、右端境界はインデックス 3 です。 + + 2. 左端境界を探索するときは、中点の要素が 2 と等しくても、引き続き左側を探索します。 + たとえば、両閉区間を使う場合は `j = m - 1` とします。 + + 3. 右端境界を探索するときは、中点の要素が 2 と等しければ、引き続き右側を探索します。 + たとえば、`i = m + 1` とします。 + +### 3.   データに応じた探索方法の選び方 + +「線形探索、二分探索、ハッシュテーブル」から、次の 3 つの場面に適した方法を選び、理由を説明してください。 + + + +1. ソート済みで今後変更されない $10^7$ 個の整数を繰り返し探索する。ほかのデータ構造は追加で作らない。 +2. 挿入と削除が頻繁に行われるデータ集合で、あるキーが存在するかを繰り返し判定する。順序を保つ必要も、範囲探索を行う必要もない。 +3. ソートされていない配列から、ある値を 1 回だけ探索する。 + +??? success "解答" + + 1. 二分探索です。データがソート済みで変更されないため、追加の空間を使わず $O(\log n)$ で探索できます。 + 2. ハッシュテーブルです。ハッシュ関数によってキーが各バケットへほぼ均等に分散される場合、挿入、削除、キーによる探索の平均時間計算量はいずれも $O(1)$ です。 + 3. 先頭から末尾まで直接走査します。1 回しか探索しない場合、ソートやハッシュテーブルの作成でも、最初に配列全体を処理する必要があるため、 + この 1 回の処理に必要な作業の総量は減りません。 + + どの方法を選ぶかは、データがソート済みか、追加の構造を作れるか、探索回数、必要な操作の種類によって決まります。 + +## 10.7.2   プログラミング演習 + +### 1.   ソート済み配列の二分探索 + +重複なく昇順に並んだ整数配列 `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.com/problems/binary-search/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } + +### 2.   ソート済み配列への挿入位置 + +重複なく昇順に並んだ整数配列 `nums` と目的の値 `target` が与えられます。 + +`target` がすでに配列にあれば、そのインデックスを返します。なければ、`target` を挿入した後も重複のない昇順を保てる位置を返します。答えは 0 の場合も、配列の長さと等しい場合もあります。二分探索を使って求めてください。 + +??? tip "解法のヒント" + + 1. 答えは 0 の場合も、配列の長さ n の場合もあります + 2. 両閉区間を使う場合、`nums[mid]` が `target` 以上なら `right = mid - 1` とし、さらに左の位置を調べます。そうでなければ `left = mid + 1` とします + 3. ループが終了した時点で、left が挿入位置です + +[LeetCode](https://leetcode.com/problems/search-insert-position/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } diff --git a/ja/docs/chapter_searching/index.md b/ja/docs/chapter_searching/index.md index d550a9a38..926bf70ce 100644 --- a/ja/docs/chapter_searching/index.md +++ b/ja/docs/chapter_searching/index.md @@ -21,3 +21,4 @@ icon: material/text-search - [10.4   ハッシュによる最適化戦略](replace_linear_by_hashing.md) - [10.5   探索アルゴリズム再考](searching_algorithm_revisited.md) - [10.6   まとめ](summary.md) +- [10.7   演習](exercises.md) diff --git a/ja/docs/chapter_sorting/exercises.md b/ja/docs/chapter_sorting/exercises.md new file mode 100644 index 000000000..1f0f60577 --- /dev/null +++ b/ja/docs/chapter_sorting/exercises.md @@ -0,0 +1,100 @@ +--- +comments: true +--- + + + +# 11.12   演習 + +## 11.12.1   確認問題 + +### 1.   選択ソートとバブルソートの最初の数回 + +配列 `[4, 2, 5, 1, 3]` が与えられます。次の操作では、いずれも小さい順にソートします。 + + + +1. 選択ソートの最初の 2 回を行い、各回の終了時の配列を書いてください。また、この時点で位置が確定した要素を示してください。 +2. バブルソートの最初の 1 回を行い、配列の状態と交換回数を書いてください。また、どの位置が確定したかを示してください。 + +??? success "解答" + + 1. 最初の 2 回は次のとおりです。 + + | 回 | 配列の状態 | 説明 | + | --- | --- | --- | + | 1 | `[1, 2, 5, 4, 3]` | 最小要素 1 を先頭の要素と交換する | + | 2 | `[1, 2, 5, 4, 3]` | 値 2 はすでにインデックス 1 にあるため、交換は不要 | + + この時点で、先頭の 2 つの位置が確定しています。以後は `[5, 4, 3]` から最小要素を選び続ければよいです。 + + 2. 隣り合う要素を順に比較します。4 と 2 は交換し、4 と 5 は交換せず、5 と 1、5 と 3 はそれぞれ交換します。 + その結果は `[2, 4, 1, 3, 5]` で、交換回数は 3 回です。最大要素 5 が配列の末尾へ移動したため、最後の位置が確定しています。 + +### 2.   等しい要素の順序は変わるか + +配列 $[2_a, 2_b, 1]$ で、$2_a$ と $2_b$ の値は等しいものの、添字によって元の順序を区別しています。 + + + +1. 選択ソートの 1 回目が終わった後の配列を書いてください。$2_a$ と $2_b$ の相対的な順序は変わりますか? +2. バブルソートの 1 回目が終わった後の配列を書いてください。$2_a$ と $2_b$ の相対的な順序は変わりますか? +3. 問い 1、2 の結果から、等しい要素の元の順序を保つという点で、2 つのソートにはどのような違いがあるかを説明してください。 + +??? success "解答" + + 1. 選択ソートの 1 回目では最小要素 1 を選び、先頭の $2_a$ と交換するため、 + $[1, 2_b, 2_a]$ となります。$2_a$ は $2_b$ の後ろへ移動し、相対的な順序が変わっています。 + + 2. バブルソートでは、まず $2_a$ と $2_b$ を比較します。両者は等しいので交換しません。次に $2_b$ と 1 を比較して交換すると、 + 1 回目の終了時は $[2_a, 1, 2_b]$ となります。$2_a$ は引き続き $2_b$ より前にあり、相対的な順序は変わっていません。 + + 3. この例では、選択ソートは等しい要素の元の順序を変えます。バブルソートは、左側の要素が右側の要素より大きい場合にだけ + 隣り合う要素を交換します。等しい要素同士は交換しないため、元の順序を保てます。 + +### 3.   計数ソートと基数ソートの比較 + +学校で、8 桁に固定された多数の学籍番号をソートします。次の問いに答えてください。 + + + +1. 最下位桁から始める基数ソートでは、何回の処理が必要ですか? +2. 学籍番号をそのまま整数として計数ソートを行うと、使われないカウント位置を大量に用意することになるのはなぜですか? +3. 問い 1、2 の結果から、8 桁に固定された多数の学籍番号を計数ソートと基数ソートのどちらかで処理するなら、どちらを選びますか?その理由も説明してください。 + +??? success "解答" + + 1. 学籍番号には 8 個の桁があるため、最下位桁から最上位桁まで、合計 8 回処理します。各回では 0~9 だけを使って分類します。 + + 2. そのまま数えるには、取り得るすべての 8 桁の値に対して位置を用意する必要があります。しかし、実際に生徒が使うのはそのうちのごく一部なので、 + ほとんどのカウント位置は常に 0 のままです。 + + 3. 基数ソートを選びます。「桁数が固定され、各桁が 10 種類の値だけを取る」という特徴を利用し、安定した分類を 8 回繰り返すだけで済むためです。 + 8 桁の学籍番号全体を整数のインデックスとして計数ソートを行うと、実際には現れない多数の値にもカウント位置を用意する必要があります。 + +## 11.12.2   プログラミング演習 + +### 1.   マージソートで配列を並べる + +整数配列 `nums` が与えられます。マージソートを自分で実装し、要素を非減少順に並べて返してください。言語に組み込まれたソート関数を呼び出してはいけません。 + +??? tip "解法のヒント" + + 1. 区間の長さが 1 以下なら、すでにソートされています + 2. 中点で区間を 2 つに分け、それぞれを再帰的にソートします + 3. 2 つのポインタでソート済みの左右の区間をマージし、結果を元の配列へ書き戻します + +[LeetCode](https://leetcode.com/problems/sort-an-array/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } + +### 2.   計数ソートで整数配列を並べる + +整数配列 `nums` と 0 以上の整数 $K$ が与えられ、配列の各要素は $0$ 以上 $K$ 以下です。 + +計数ソートを実装し、要素を非減少順に `nums` へ書き戻して、`nums` を返してください。 +要素同士の大小比較によって順序を決めたり、言語に組み込まれたソート関数を呼び出したりしてはいけません。 + +??? tip "解法のヒント" + + 1. 各要素は 0 以上 K 以下なので、その値をカウント配列のインデックスとして直接使えます + 2. 1 回目に nums を走査し、対応する位置のカウントに 1 を足します + 3. 次にカウント配列を 0 から K まで走査します。値 x が現れた回数だけ、nums に x を続けて書き込みます diff --git a/ja/docs/chapter_sorting/index.md b/ja/docs/chapter_sorting/index.md index 96eb8963f..a85184557 100644 --- a/ja/docs/chapter_sorting/index.md +++ b/ja/docs/chapter_sorting/index.md @@ -26,3 +26,4 @@ icon: material/sort-ascending - [11.9   計数ソート](counting_sort.md) - [11.10   基数ソート](radix_sort.md) - [11.11   まとめ](summary.md) +- [11.12   演習](exercises.md) diff --git a/ja/docs/chapter_stack_and_queue/exercises.md b/ja/docs/chapter_stack_and_queue/exercises.md new file mode 100644 index 000000000..d84e97f8c --- /dev/null +++ b/ja/docs/chapter_stack_and_queue/exercises.md @@ -0,0 +1,105 @@ +--- +comments: true +--- + + + +# 5.5   演習 + +## 5.5.1   確認問題 + +### 1.   スタックとキューではどれが先に取り出されるか + +空のスタック `S` と空のキュー `Q` を用意し、それぞれに同じ次の操作を行います。 + +手順 1:`A` を追加する。 +手順 2:`B` を追加する。 +手順 3:要素を 1 つ取り出して記録する。 +手順 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.   キューの末尾が配列の末尾を越えたら + +長さ 5 の環状配列を使ってキューを実装します。配列のインデックスは `0~4` です。 +現在、`front = 3`、`size = 2` で、キュー内の `A、B` はそれぞれインデックス 3、4 にあります。 + + + +1. 「`C` をエンキューする」とき、`C` はどのインデックスに置きますか?エンキュー後の `size` はいくつですか? +2. 続けて 1 回デキューすると、どの要素が取り出されますか?新しい `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.   両端キューの両端での操作 + +ここでは、`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()` + + + +1. 2 回の取り出しで、それぞれどの要素が得られますか? +2. すべての操作を終えた後、キュー先頭から末尾までにどの要素が残っていますか? +3. この 6 つの操作を確認してください。キュー末尾への追加とキュー先頭からの削除だけが許される通常のキューで、すべてを実行できますか?できない場合は実行できない操作を挙げ、両端キューなら実行できるか、その理由とともに説明してください。 + +??? success "解答" + + 最初の 3 つの操作を終えた時点で、両端キューは先頭から末尾へ `[C, A, B]` となっています。 + + + + 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   プログラミング演習 + +### 1.   括弧列の検査 + +`()`、`[]`、`{}` の 3 種類の括弧だけを含む文字列 `s` が与えられます。スタックを使って、それが正しい括弧列かどうかを判定してください。 + +正しい括弧列は、次の条件を両方とも満たします。各閉じ括弧は、まだ対応付けられていない開き括弧のうち最も直近のものと種類が一致し、 +文字列を最後まで調べたときに、対応する閉じ括弧のない開き括弧が残っていません。判定結果をブール値で返してください。 + +??? tip "解法のヒント" + + 1. 「閉じ括弧から対応する開き括弧」への対応表を作れます + 2. 閉じ括弧に出会ったら、まずスタックが空かどうかを確認し、次にスタックトップと対応しているかを確認します + 3. 文字列を最後まで調べた後、スタックも空でなければなりません + +[LeetCode](https://leetcode.com/problems/valid-parentheses/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } diff --git a/ja/docs/chapter_stack_and_queue/index.md b/ja/docs/chapter_stack_and_queue/index.md index d057e9475..2d4ac5520 100644 --- a/ja/docs/chapter_stack_and_queue/index.md +++ b/ja/docs/chapter_stack_and_queue/index.md @@ -19,3 +19,4 @@ icon: material/stack-overflow - [5.2   キュー](queue.md) - [5.3   両端キュー](deque.md) - [5.4   まとめ](summary.md) +- [5.5   演習](exercises.md) diff --git a/ja/docs/chapter_tree/exercises.md b/ja/docs/chapter_tree/exercises.md new file mode 100644 index 000000000..12a1d8efd --- /dev/null +++ b/ja/docs/chapter_tree/exercises.md @@ -0,0 +1,132 @@ +--- +comments: true +--- + + + +# 7.7   演習 + +## 7.7.1   確認問題 + +### 1.   完全二分木・充満二分木・充足二分木 + +次の 2 つの配列は二分木をレベル順に表しており、`None` は空き位置を表します。 + +- 木 A:`[1, 2, 3, 4, 5, 6]` +- 木 B:`[1, 2, 3, None, None, 6, 7]` + + + +1. どちらが完全二分木ですか? +2. どちらが充満二分木、つまりすべての非葉ノードが 2 つの子ノードを持つ二分木ですか? +3. この 2 つに充足二分木はありますか?それぞれ理由を説明してください。 + +??? success "解答" + + 1. 木 A は完全二分木です。最下層だけがすべて埋まっておらず、ノードが左から右へ隙間なく並んでいます。 + 木 B は完全二分木ではありません。最下層の左側に空き位置がある一方で、その右側にはノードがあるためです。 + + 2. 木 B は充満二分木です。ノード 1 とノード 3 はそれぞれ 2 つの子ノードを持ち、それ以外のノードはすべて葉ノードです。 + 木 A は充満二分木ではありません。ノード 3 が左子ノード 6 だけを持つためです。 + + 3. どちらも充足二分木ではありません。どちらの木も最下層がすべて埋まっていないためです。 + +### 2.   同じ木を走査する 3 つの順序 + +配列 `[1, 2, 3, 4, 5, 6, 7]` をレベル順に完全二分木へ格納します。 + + + +1. この木を描いてください。 +2. 先行順・中間順・後行順走査の列をそれぞれ書いてください。 +3. 中間順走査の列で、根ノード 1 の左側と右側にある 2 つの列は、木のどの部分にそれぞれ対応しますか? + +??? 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.   2 つの二分探索木を比較する + +次の 2 つの列を、左から右へ順に空の二分探索木へ挿入します。 + +- 列 A:`[4, 2, 6, 1, 3, 5, 7]` +- 列 B:`[1, 2, 3, 4, 5, 6, 7]` + + + +1. それぞれの木で 7 を探索するときに通るノードを書いてください。 +2. 高さを根ノードから最も遠い葉ノードまでに通る辺の数とすると、2 つの木の高さはそれぞれいくつですか? +3. 問い 1、2 の結果から、2 つの木で 7 を探索する効率は同じだと考えられますか?木の形と探索経路をもとに理由を説明してください。 + +??? success "解答" + + 1. 列 A から作った木では、探索経路は `4 → 6 → 7` です。 + 列 B から作った木では、探索経路は `1 → 2 → 3 → 4 → 5 → 6 → 7` です。 + + 2. 1 つ目の木はすべての層が埋まっているため、高さは 2 です。2 つ目の木では各ノードが右の子だけを持ち、高さは 6 です。 + + 3. 2 つの木で 7 を探索する効率は異なります。挿入順序によって二分探索木の形と高さが変わります。1 つ目の木で 7 を探索するときに通るノードは 3 つだけですが、 + 2 つ目の木では 7 つのノードを順に通ります。木が高いほど、最悪の場合に経路上で比較するノードが多くなります。 + +## 7.7.2   プログラミング演習 + +### 1.   二分木の最大の深さ + +二分木の根ノード `root` が与えられます。各ノードには、整数値と左右の子ノードを指す参照が含まれています。 + +根ノードから最も遠い葉ノードまでに通る**ノード数**を、二分木の最大の深さとします。この木の最大の深さを返してください。空の木の最大の深さは 0 です。 +再帰を使って求めてください。 + +??? tip "解法のヒント" + + 1. この問題ではノード数で深さを数えます。根ノードだけがある場合、最大の深さは 1 です + 2. 再帰関数が、現在のノードを根とする部分木の最大の深さを返すようにします + 3. 空のノードは 0 を返し、空でないノードは max(depth(left), depth(right)) + 1 を返します + +[LeetCode](https://leetcode.com/problems/maximum-depth-of-binary-tree/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } + +### 2.   二分木のレベル順走査 + +二分木の根ノード `root` が与えられます。キューを使い、上から下へ層ごとにすべてのノードを訪問してください。同じ層では、左から右の順に訪問します。 + +二次元配列を返してください。1 つ目の部分配列には根ノードがある層のノード値、2 つ目の部分配列には次の層のノード値を保存し、以降も同様に続けます。 +二分木が空の場合は、空の配列を返してください。 + +??? tip "解法のヒント" + + 1. レベル順走査では先に入ったノードを先に訪問するため、キューを使います + 2. 各回の開始時にキューに入っているノードは、ちょうど同じ層に属しています + 3. 先にキューの長さを記録し、その個数のノードを取り出して、それぞれの子ノードを追加します + +[LeetCode](https://leetcode.com/problems/binary-tree-level-order-traversal/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } + +### 3.   二分探索木で k 番目に小さい要素 + +二分探索木には `n` 個のノードがあり、すべてのノード値は互いに異なります。 +すべてのノード値を小さい順に並べたときの位置には、1 から番号を付けます。 + +根ノード `root` と、`1 <= k <= n` を満たす整数 `k` が与えられます。`k` 番目のノード値を返してください。 +すべてのノード値を先に集めるのではなく、中間順走査の途中で答えを直接探してください。 + +??? tip "解法のヒント" + + 1. 二分探索木を中間順走査すると、ノード値を小さい順に訪問できます + 2. 中間順走査では、左部分木、現在のノード、右部分木の順に処理します。現在のノードを訪問するとき、カウントに 1 を足します + 3. カウントが初めて k になったとき、現在のノード値が答えです。それ以降は走査を続ける必要がありません + +[LeetCode](https://leetcode.com/problems/kth-smallest-element-in-a-bst/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } diff --git a/ja/docs/chapter_tree/index.md b/ja/docs/chapter_tree/index.md index 625017de6..9a8e0f20f 100644 --- a/ja/docs/chapter_tree/index.md +++ b/ja/docs/chapter_tree/index.md @@ -21,3 +21,4 @@ icon: material/graph-outline - [7.4   二分探索木](binary_search_tree.md) - [7.5   AVL 木 *](avl_tree.md) - [7.6   まとめ](summary.md) +- [7.7   演習](exercises.md) diff --git a/overrides/stylesheets/extra.css b/overrides/stylesheets/extra.css index 96f7453b8..293aae449 100644 --- a/overrides/stylesheets/extra.css +++ b/overrides/stylesheets/extra.css @@ -232,12 +232,12 @@ html:has(body[data-md-color-scheme="default"]) { filter: brightness(0.85) invert(0.05); } -.md-typeset a:not(.md-button) { +.md-typeset a:not(.md-button):not(.rounded-button) { text-decoration: none; } -.md-typeset a:not(.md-button):hover, -.md-typeset a:not(.md-button):focus-visible { +.md-typeset a:not(.md-button):not(.rounded-button):hover, +.md-typeset a:not(.md-button):not(.rounded-button):focus-visible { color: var(--md-typeset-a-color); text-decoration: underline; } @@ -519,6 +519,44 @@ html:has(body[data-md-color-scheme="default"]) { text-decoration: none; } +/* Exercise actions reuse the landing-page CTA with a light-content treatment. */ +.rounded-button.exercise-button { + border-color: rgb(52 152 144 / 0.3); + background-color: rgb(52 152 144 / 0.08); + color: #277d76 !important; + font-size: 0.85em; + box-shadow: none; + text-decoration: none; + backdrop-filter: none; + -webkit-backdrop-filter: none; +} + +.rounded-button.exercise-button:hover, +.rounded-button.exercise-button:focus-visible { + border-color: rgb(52 152 144 / 0.45); + background-color: rgb(52 152 144 / 0.14); + color: #216c66 !important; + box-shadow: none; + text-decoration: none; +} + +[data-md-color-scheme="slate"] .rounded-button.exercise-button { + border-color: rgb(86 116 122); + background-color: rgb(36 52 57); + color: rgb(233 245 243) !important; + box-shadow: none; + backdrop-filter: saturate(115%) blur(0.18rem); + -webkit-backdrop-filter: saturate(115%) blur(0.18rem); +} + +[data-md-color-scheme="slate"] .rounded-button.exercise-button:hover, +[data-md-color-scheme="slate"] .rounded-button.exercise-button:focus-visible { + border-color: rgb(92 111 117); + background-color: rgb(47 80 83); + color: rgb(242 249 248) !important; + box-shadow: none; +} + .heading-div .rounded-button:first-of-type { border-color: rgb(160 223 217 / 0.42); background-color: rgb(42 104 99 / 0.2); diff --git a/ru/docs/chapter_array_and_linkedlist/exercises.md b/ru/docs/chapter_array_and_linkedlist/exercises.md new file mode 100644 index 000000000..e3d3e6421 --- /dev/null +++ b/ru/docs/chapter_array_and_linkedlist/exercises.md @@ -0,0 +1,112 @@ +--- +comments: true +--- + + + +# 4.6   Упражнения + +## 4.6.1   Вопросы для самопроверки + +### 1.   Как массив и связный список находят элемент + +В массиве и односвязном списке по порядку хранятся `[A, B, C, D, E]`. Требуется прочитать 4-й элемент `D`. + + + +1. Какой индекс можно напрямую использовать в массиве? +2. Через какие узлы нужно последовательно пройти по `next` в односвязном списке, начиная с головного узла `A`? +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.   Как вставить элемент в массив и связный список + +В массиве и односвязном списке хранятся `A, B, C, D`. Требуется вставить `X` после `B`. + +- емкость массива равна 5, его текущее состояние — `[A, B, C, D, _]`; +- связный список имеет вид `A → B → C → D`, и ссылка на узел `B` уже получена. + + + +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.   Как растет емкость списка + +Список на основе массива сейчас содержит `[A, B, C]`, его длина `size = 3`, а емкость `capacity = 4`. +При нехватке места емкость нового массива увеличивается в 2 раза. + + + +1. Чему будут равны длина и емкость списка после добавления `D`? Потребуется ли увеличить емкость? +2. Затем добавляют `E`. Какой станет емкость и сколько прежних элементов потребуется скопировать? +3. Длина базового массива неизменна. Почему тогда кажется, что емкость списка может расти? + +??? success "Ответ" + + 1. `D` можно поместить в последнюю свободную ячейку. Теперь содержимое равно `[A, B, C, D]`, + `size = 4`, `capacity = 4`, и увеличивать емкость не требуется. + + 2. Для `E` свободного места уже нет, поэтому нужно создать новый массив емкостью 8, + скопировать в него 4 прежних элемента `A, B, C, D`, а затем добавить `E`. + После этого `size = 5`, `capacity = 8`. + + 3. Исходный массив сам по себе не удлиняется. Список создает новый массив большего размера, копирует прежние элементы + и затем использует новый массив как базовое хранилище. Поэтому для пользователя емкость выглядит увеличившейся. + +## 4.6.2   Задачи по программированию + +### 1.   Прибавление единицы к большому числу в массиве + +Массив `digits` хранит слева направо цифры неотрицательного целого числа. Например, `[3, 0, 8]` обозначает 308. +Число 0 представлено как `[0]`; у всех остальных входных данных первая цифра не равна 0. + +Смоделируйте сложение столбиком в десятичной системе: увеличьте это число на 1 и верните результат в том же формате массива. +Массив `digits` можно изменять непосредственно. Если перед первой цифрой появляется новый перенос, можно вернуть более длинный массив. + +??? tip "Подсказки" + + 1. Как при сложении столбиком, начните с последней цифры массива + 2. Если текущая цифра меньше 9, увеличьте ее на один и сразу верните результат + 3. Если текущая цифра равна 9, замените ее на 0; если все цифры равны 9, добавьте 1 в начало + +[LeetCode](https://leetcode.com/problems/plus-one/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } + +### 2.   Разворот односвязного списка + +Дан головной узел `head` односвязного списка. Каждый узел содержит значение и поле `next`, указывающее на следующий узел. + +Итеративно разверните все связи между узлами и верните новый головной узел. +Не создавайте новые узлы связного списка. + +??? tip "Подсказки" + + 1. Нарисуйте на бумаге три связанных узла и два указателя prev и cur + 2. Прежде чем изменить cur.next, сохраните исходный следующий узел в nxt + 3. Развернув cur.next, выполните prev = cur, затем cur = nxt и продолжайте с очередным узлом исходного списка + +[LeetCode](https://leetcode.com/problems/reverse-linked-list/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } diff --git a/ru/docs/chapter_array_and_linkedlist/index.md b/ru/docs/chapter_array_and_linkedlist/index.md index 37209ed93..d51f73f0e 100644 --- a/ru/docs/chapter_array_and_linkedlist/index.md +++ b/ru/docs/chapter_array_and_linkedlist/index.md @@ -20,3 +20,4 @@ icon: material/view-list-outline - [4.3   Список](list.md) - [4.4   Оперативная память и кэш *](ram_and_cache.md) - [4.5   Резюме](summary.md) +- [4.6   Упражнения](exercises.md) diff --git a/ru/docs/chapter_backtracking/exercises.md b/ru/docs/chapter_backtracking/exercises.md new file mode 100644 index 000000000..76b191557 --- /dev/null +++ b/ru/docs/chapter_backtracking/exercises.md @@ -0,0 +1,93 @@ +--- +comments: true +--- + + + +# 13.6   Упражнения + +## 13.6.1   Вопросы для самопроверки + +### 1.   Пропустит ли этот алгоритм некоторые перестановки + +Алгоритм поиска с возвратом пытается построить все перестановки, перебирая числа в порядке `1, 2, 3`. Выбрав число `x`, он: + +1. добавляет `x` в конец текущего пути; +2. помечает `x` как «использованное»; +3. рекурсивно заполняет следующую позицию. + +После возврата из рекурсии ученик удалил `x` только из конца пути и перешел к следующему числу. + + + +1. Какую перестановку алгоритм получит первой? Сможет ли он получить все 6 перестановок? +2. Достаточно ли перед возвратом на предыдущий уровень удалить число из конца пути? Если нет, что еще нужно сделать и почему? + +??? success "Ответ" + + 1. Первой получится перестановка `[1, 2, 3]`, но алгоритм не сможет получить все перестановки. Хотя после возврата путь становится короче, + числа 1, 2 и 3 по-прежнему помечены как «использованные», поэтому в последующих ветвях не останется доступных чисел. + + 2. Нет. После удаления `x` из конца пути нужно также снова пометить `x` как «неиспользованное». + Текущий путь и отметки «использовано» вместе описывают состояние поиска. При выборе изменяются обе части состояния, поэтому при возврате нужно восстановить обе, + чтобы другие ветви могли снова выбрать `x`. + +### 2.   Важен ли порядок выбора чисел + +Дан упорядоченный массив `[2, 3, 5]` и целевое значение 5. Каждое число разрешено выбирать повторно. +Алгоритм требует, чтобы числа в каждом пути поиска располагались только в неубывающем порядке. + + + +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. + + + +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   Задачи по программированию + +### 1.   Все перестановки различных элементов + +Массив целых чисел `nums` содержит хотя бы один элемент, и все его элементы попарно различны. +Перечислите все возможные порядки, в которых каждый элемент используется ровно один раз, и верните каждый порядок как отдельный массив. +Порядок самих перестановок в результате не имеет значения. +Используйте поиск с возвратом и булев массив, отмечающий, выбран ли элемент из каждой позиции входного массива в текущую перестановку. + +??? tip "Подсказки" + + 1. Глубина рекурсии показывает, какую по счету позицию перестановки нужно заполнить + 2. На каждом уровне пробуйте только еще не использованные элементы + 3. Когда длина пути станет равна длине `nums`, добавьте в ответ копию пути + +[LeetCode](https://leetcode.com/problems/permutations/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } diff --git a/ru/docs/chapter_backtracking/index.md b/ru/docs/chapter_backtracking/index.md index 51fb4d494..1d2d60f17 100644 --- a/ru/docs/chapter_backtracking/index.md +++ b/ru/docs/chapter_backtracking/index.md @@ -20,3 +20,4 @@ icon: material/map-marker-path - [13.3   Задача о сумме подмножеств](subset_sum_problem.md) - [13.4   Задача о n ферзях](n_queens_problem.md) - [13.5   Резюме](summary.md) +- [13.6   Упражнения](exercises.md) diff --git a/ru/docs/chapter_computational_complexity/exercises.md b/ru/docs/chapter_computational_complexity/exercises.md new file mode 100644 index 000000000..a8f26f5d3 --- /dev/null +++ b/ru/docs/chapter_computational_complexity/exercises.md @@ -0,0 +1,116 @@ +--- +comments: true +--- + + + +# 2.6   Упражнения + +## 2.6.1   Вопросы для самопроверки + +### 1.   Временная и пространственная сложность итерации и рекурсии + +Обе функции ниже вычисляют $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) +``` + + + +1. Какие значения принимает переменная `s` после каждой итерации при выполнении `sum_iter(4)`? +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.   Временная сложность трех фрагментов кода + +Во всех трех фрагментах кода входные данные — положительное целое число $n$. Расположите фрагменты в порядке возрастания временной сложности и укажите сложность каждого из них. + +```python +# Фрагмент 1 +s = 0 +for i in range(n): + s += i + +# Фрагмент 2 +s = 0 +for i in range(n): + for j in range(i, n): + s += j + +# Фрагмент 3 +while n > 1: + n = n // 2 +``` + +??? success "Ответ" + + Порядок от меньшей сложности к большей: фрагмент 3 — $O(\log n)$, фрагмент 1 — $O(n)$, фрагмент 2 — $O(n^2)$. + В фрагменте 3 на каждой итерации $n$ уменьшается вдвое, поэтому цикл выполняется примерно $\log_2 n$ раз. + Цикл во фрагменте 1 выполняется ровно $n$ раз. Число итераций внутреннего цикла во фрагменте 2 последовательно равно + $n,n-1,\dots,1$, а их сумма — $n(n+1)/2$, поэтому сложность квадратичная. + +### 3.   Какой способ разворота экономнее по памяти + +Все элементы массива `nums` можно расположить в обратном порядке двумя способами: + + + +1. создать новый массив `res` той же длины, скопировать в него элементы в обратном порядке и вернуть его; +2. задать два индекса `i` и `j`, перемещать их от начала и конца массива к середине и попарно менять местами `nums[i]` и `nums[j]`. + + Какова пространственная сложность каждого способа? Какой из них выполняет операцию «на месте»? + +??? success "Ответ" + + 1. Требуется вспомогательный массив той же длины, что и входной, поэтому пространственная сложность равна $O(n)$. + + 2. Используются только две индексные переменные, + поэтому пространственная сложность равна $O(1)$, а операция выполняется на месте. + + Обратите внимание: разворот на месте изменяет входной массив, + поэтому его следует предпочитать только тогда, когда менять входные данные разрешено. Если исходный массив нужно сохранить, затрат на копирование в способе 1 не избежать. + +## 2.6.2   Задачи по программированию + +### 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.com/problems/fibonacci-number/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } diff --git a/ru/docs/chapter_computational_complexity/index.md b/ru/docs/chapter_computational_complexity/index.md index b1e4155d4..7856297ae 100644 --- a/ru/docs/chapter_computational_complexity/index.md +++ b/ru/docs/chapter_computational_complexity/index.md @@ -20,3 +20,4 @@ icon: material/timer-sand - [2.3   Временная сложность](time_complexity.md) - [2.4   Пространственная сложность](space_complexity.md) - [2.5   Резюме](summary.md) +- [2.6   Упражнения](exercises.md) diff --git a/ru/docs/chapter_data_structure/exercises.md b/ru/docs/chapter_data_structure/exercises.md new file mode 100644 index 000000000..00c794169 --- /dev/null +++ b/ru/docs/chapter_data_structure/exercises.md @@ -0,0 +1,92 @@ +--- +comments: true +--- + + + +# 3.6   Упражнения + +## 3.6.1   Вопросы для самопроверки + +### 1.   Связи между данными в повседневных ситуациях + +Для каждого из трех примеров выберите по характеру связей между данными один вариант: «линейная структура», «древовидная структура» или «сетевая структура». Объясните свой выбор. + + + +1. Ученики стоят в ряд, и каждого интересуют только стоящие непосредственно перед ним и за ним; +2. школа организована по уровням «школа → параллель → класс»; +3. городские дороги соединяют множество перекрестков: из одного перекрестка можно попасть в несколько других, а дороги могут образовывать циклы. + +??? success "Ответ" + + 1. Это линейная структура. Кроме первого и последнего в ряду, каждый ученик соседствует ровно с одним человеком впереди и одним сзади, а связи вытянуты в одну линию. + + 2. Это древовидная структура. Каждый класс относится к одной параллели, а каждая параллель — к школе; связи образуют уровни сверху вниз. + + 3. Это сетевая структура. Один перекресток может быть связан с несколькими другими, а дороги могут образовывать циклы, поэтому связи нельзя выстроить в единую последовательность или строгую иерархию. + + Определяя структуру, сначала рассматривайте связи между элементами, а не объем памяти, который они занимают. + +### 2.   Как сохранить логический порядок в памяти + +Логический порядок `A → B → C` можно сохранить в памяти двумя упрощенными способами: + +- вариант 1: `A, B, C` размещены в ячейках памяти с номерами `20, 21, 22` соответственно; +- вариант 2: `A, B, C` размещены в ячейках с номерами `20, 7, 31` соответственно, причем `A` хранит расположение `B`, а `B` — расположение `C`. + + + +1. Какой вариант использует хранение в непрерывном, а какой — в разрозненном пространстве памяти? +2. Какой вариант ближе к массиву, а какой — к связному списку? +3. Почему вариант 2 по-прежнему задает логический порядок `A → B → C`, хотя номера ячеек памяти не упорядочены по величине? + +??? success "Ответ" + + 1. Вариант 1 использует соседние ячейки памяти, поэтому хранение происходит в непрерывном пространстве. Узлы в варианте 2 находятся в разных местах, поэтому они хранятся в разрозненном пространстве. + + 2. Вариант 1 ближе к массиву, а вариант 2 — к связному списку. + + 3. Логический порядок задается связями, записанными между узлами, а не величиной номеров ячеек памяти. + По адресу, который хранит `A`, можно найти `B`, а затем по адресу из `B` найти `C`, поэтому элементы по-прежнему доступны в порядке `A, B, C`. + + Это также показывает, что логическая и физическая структуры — два разных взгляда на одни и те же данные. + +### 3.   Типы данных и структура записей о домашних заданиях + +Учебная группа записала в порядке рассадки, сдали ли домашнее задание 4 ученика: + +`[true, false, true, true]` + + + +1. Какой базовый тип данных подходит для каждого элемента? +2. Какую логическую структуру образуют эти 4 элемента, расположенные в ряд по порядку рассадки? +3. Если затем записывать оценки за задание каждого ученика как `[90, 0, 85, 100]`, изменится «тип содержимого» или «способ организации» данных? Объясните ответ. + +??? success "Ответ" + + 1. Каждый элемент выражает только «да» или «нет», поэтому подходит логический тип `bool`. + + 2. Элементы расположены по порядку рассадки и образуют линейную структуру, которую можно хранить в массиве. + + 3. Изменится тип содержимого: логические значения будут заменены целыми числами. Способ организации не изменится: + данные по-прежнему расположены в ряд по порядку рассадки и могут храниться в массиве как линейная структура. + + Базовый тип данных описывает, «что хранится», а структура данных — «как организованы данные». + +## 3.6.2   Задачи по программированию + +### 1.   Число единиц в двоичной записи + +Дано неотрицательное целое число `n`. Подсчитайте количество единиц в его двоичной записи. + +Используйте побитовые операции. Не преобразовывайте двоичную запись в строку и не применяйте встроенную функцию, которая напрямую подсчитывает единицы. + +??? tip "Подсказки" + + 1. Выражение n & 1 извлекает крайний правый бит n и позволяет проверить, равен ли он 1 + 2. Сдвиг вправо на один разряд отбрасывает текущий крайний правый бит; в большинстве языков для этого используется оператор >> + 3. Реализовав проверку каждого бита со сдвигом вправо, обратите внимание: n & (n - 1) превращает в 0 крайнюю правую единицу в n + +[LeetCode](https://leetcode.com/problems/number-of-1-bits/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } diff --git a/ru/docs/chapter_data_structure/index.md b/ru/docs/chapter_data_structure/index.md index 638a900dc..9b8237e3d 100644 --- a/ru/docs/chapter_data_structure/index.md +++ b/ru/docs/chapter_data_structure/index.md @@ -20,3 +20,4 @@ icon: material/shape-outline - [3.3   Кодирование чисел *](number_encoding.md) - [3.4   Кодирование символов *](character_encoding.md) - [3.5   Резюме](summary.md) +- [3.6   Упражнения](exercises.md) diff --git a/ru/docs/chapter_divide_and_conquer/exercises.md b/ru/docs/chapter_divide_and_conquer/exercises.md new file mode 100644 index 000000000..0294d2bc7 --- /dev/null +++ b/ru/docs/chapter_divide_and_conquer/exercises.md @@ -0,0 +1,102 @@ +--- +comments: true +--- + + + +# 12.6   Упражнения + +## 12.6.1   Вопросы для самопроверки + +### 1.   Какие задачи подходят для метода «разделяй и властвуй» + +Ученик хочет решить следующие задачи способом «разделить на две половины, решить каждую отдельно, затем объединить результаты». +Для каждой задачи выберите одно из утверждений: «подходит для метода “разделяй и властвуй”», «разделение возможно, но не уменьшает общий объем работы» или «половины нельзя решить независимо». Объясните свой выбор. + + + +1. Отсортировать неупорядоченный массив; +2. найти максимальный элемент массива; +3. последовательно выполнить набор операций стека `push(x)` и `pop()`, выводя элемент, полученный при каждой операции `pop()`. + +??? success "Ответ" + + 1. Подходит: массив делится пополам, обе половины независимо сортируются, а затем сливаются за $O(n)$ — именно так работает сортировка слиянием. + 2. Разделение возможно, но оно не уменьшит общий объем работы: в обеих половинах в сумме все равно нужно проверить все $n$ элементов, + поэтому, как и при прямом обходе, потребуется $O(n)$ времени. + 3. Половины нельзя решить независимо: содержимое стека в начале второй половины операций зависит от результата первой, + поэтому решить их независимо, не зная результатов друг друга, невозможно. + +### 2.   Как быстрое возведение в степень уменьшает число вычислений + +Рекурсивная функция ниже вычисляет $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)`. + + + +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.   Разделение левого и правого поддеревьев по последовательностям обхода + +Двоичное дерево без повторяющихся узлов имеет следующие последовательности прямого и симметричного обходов: + +- прямой обход: `[A, B, D, E, C]` +- симметричный обход: `[D, B, E, A, C]` + +Выполните только разделение на уровне корневого узла: продолжать рекурсию и рисовать все дерево не требуется. + + + +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   Задачи по программированию + +### 1.   Быстрое возведение в степень + +Даны вещественное число `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.com/problems/powx-n/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } diff --git a/ru/docs/chapter_divide_and_conquer/index.md b/ru/docs/chapter_divide_and_conquer/index.md index a69d2199d..8129ae255 100644 --- a/ru/docs/chapter_divide_and_conquer/index.md +++ b/ru/docs/chapter_divide_and_conquer/index.md @@ -20,3 +20,4 @@ icon: material/set-split - [12.3   Задача построения двоичного дерева](build_binary_tree_problem.md) - [12.4   Задача о Ханойской башне](hanota_problem.md) - [12.5   Резюме](summary.md) +- [12.6   Упражнения](exercises.md) diff --git a/ru/docs/chapter_dynamic_programming/exercises.md b/ru/docs/chapter_dynamic_programming/exercises.md new file mode 100644 index 000000000..93faeb4b1 --- /dev/null +++ b/ru/docs/chapter_dynamic_programming/exercises.md @@ -0,0 +1,117 @@ +--- +comments: true +--- + + + +# 14.8   Упражнения + +## 14.8.1   Вопросы для самопроверки + +### 1.   Когда подходит динамическое программирование + +Ученик утверждает: «Если можно записать рекуррентное соотношение, нужно использовать динамическое программирование». +Для каждой из трех задач выберите наиболее подходящий из трех вариантов: динамическое программирование; поиск с возвратом; цикл или математическая формула без таблицы `dp`. Укажите одну основную причину. + + + +1. Набрать сумму 6 с помощью монет номиналов `[1, 3, 4]`, используя каждый номинал любое число раз, и найти минимальное число монет; +2. вывести все перестановки `[1, 2, 3]`; +3. вычислить $1 + 2 + \dots + n$. + + Для задачи, которой подходит динамическое программирование, также объясните значение `dp[i]`. + +??? success "Ответ" + + 1. Подходит динамическое программирование. Пусть `dp[i]` — минимальное число монет, необходимое для получения суммы `i`. + Для каждой монеты `c`, не превосходящей `i`, значение `dp[i-c] + 1` можно рассматривать как возможный ответ, + а затем выбрать минимум среди всех вариантов. Разные последовательности выбора многократно приводят к одной и той же сумме, а оптимальное решение для большей суммы можно составить из оптимальных решений для меньших сумм. + Для суммы 6 ответ равен 2: `3 + 3`. + + 2. Следует использовать поиск с возвратом. Задача требует поочередно получить все 6 перестановок; поиск с возвратом позволяет систематически сделать выбор, продолжить поиск, + затем отменить выбор и перейти к другой ветви. Каким бы ни был метод, при фактическом выводе всех перестановок пропустить их перебор нельзя. + + 3. Достаточно цикла или формулы суммы арифметической прогрессии. Хотя можно записать `S(i) = S(i-1) + i`, при вычислении `S(i)` требуется только одно меньшее значение `S(i-1)`, + а каждая частичная сумма вычисляется один раз. Повторяющихся подзадач нет, поэтому таблица `dp` не нужна. Возможность записать рекуррентное соотношение не означает необходимости динамического программирования. + +### 2.   Как вычислить одну ячейку таблицы рюкзака + +Задача о рюкзаке 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`. + + + +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.   В каком порядке обновлять вместимость рюкзака + +В задаче о рюкзаке 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`. + + + +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   Задачи по программированию + +### 1.   Число способов подняться по лестнице + +Лестница состоит из `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.com/problems/climbing-stairs/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } + +### 2.   Рюкзак 0-1 + +Даны массивы одинаковой длины `wgt` и `val`: вес предмета `i` — положительное целое число `wgt[i]`, а его стоимость — неотрицательное целое число `val[i]`. +Вместимость рюкзака `cap` — неотрицательное целое число. Каждый предмет можно выбрать не более одного раза. Найдите максимальную суммарную стоимость предметов, +общий вес которых не превышает `cap`. Используйте одномерное динамическое программирование. + +??? tip "Подсказки" + + 1. Создайте массив dp длины cap + 1, где dp[c] — максимальная стоимость при предельной вместимости c + 2. Обрабатывая предмет i, сравните dp[c] для варианта «не брать предмет» и dp[c-wgt[i]] + val[i] для варианта «взять предмет» + 3. Вместимость нужно обновлять от большей к меньшей, чтобы не выбрать текущий предмет повторно на одной итерации diff --git a/ru/docs/chapter_dynamic_programming/index.md b/ru/docs/chapter_dynamic_programming/index.md index fa475c826..bd8cef17f 100644 --- a/ru/docs/chapter_dynamic_programming/index.md +++ b/ru/docs/chapter_dynamic_programming/index.md @@ -22,3 +22,4 @@ icon: material/table-pivot - [14.5   Задача о полном рюкзаке](unbounded_knapsack_problem.md) - [14.6   Задача о расстоянии редактирования](edit_distance_problem.md) - [14.7   Резюме](summary.md) +- [14.8   Упражнения](exercises.md) diff --git a/ru/docs/chapter_graph/exercises.md b/ru/docs/chapter_graph/exercises.md new file mode 100644 index 000000000..80a4bc748 --- /dev/null +++ b/ru/docs/chapter_graph/exercises.md @@ -0,0 +1,109 @@ +--- +comments: true +--- + + + +# 9.5   Упражнения + +## 9.5.1   Вопросы для самопроверки + +### 1.   Два представления одного графа + +Неориентированный граф содержит 4 вершины `A, B, C, D` и ребра +`A-B, A-C, B-C, C-D`. + + + +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.   Порядок обхода в ширину и в глубину + +Неориентированный граф содержит вершины `A, B, C, D, E` и ребра +`A-B, A-C, B-D, C-D, D-E`. + +Начните с A и при наличии нескольких непосещенных смежных вершин выбирайте их в алфавитном порядке. + + + +1. Запишите порядок обхода в ширину (BFS). +2. Запишите порядок рекурсивного обхода в глубину (DFS). +3. Почему при обоих обходах необходимо отмечать уже посещенные вершины? + +??? success "Ответ" + + 1. Порядок BFS: `A, B, C, D, E`. Сначала посещаются B и C, отстоящие от A на одно ребро, + а затем более далекие D и E. + + 2. Порядок DFS: `A, B, D, C, E`. Обход последовательно переходит в непосещенную смежную вершину текущей вершины, + поэтому сначала проходит путь `A → B → D → C`. Когда у C не остается новых смежных вершин, обход возвращается к D и посещает E. + + 3. В графе есть цикл, например `A-B-D-C-A`. Если не отмечать посещенные вершины, + обход может снова и снова проходить по циклу через одни и те же вершины и не завершиться. + +### 3.   Можно ли посетить весь граф за один BFS + +Неориентированный граф содержит вершины `A, B, C, D, E, F` и только ребра +`A-B, B-C, D-E`. + + + +1. Какие вершины можно посетить за один BFS, начав с A? +2. Посещены ли после этого все вершины графа? Почему? +3. Если перебирать все вершины в алфавитном порядке и запускать новый 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   Задачи по программированию + +### 1.   Проверка существования пути в неориентированном графе + +Дан неориентированный граф из $n$ вершин, пронумерованных от $0$ до $n-1$. Каждый элемент `[u, v]` массива `edges` обозначает неориентированное ребро между вершинами `u` и `v`. + +Также даны начальная вершина `source` и конечная вершина `destination`. Сначала постройте по `edges` список смежности, а затем с помощью BFS или DFS +определите, существует ли путь от `source` до `destination`: если существует, верните `true`, иначе — `false`. +Граф может содержать циклы и быть несвязным. + +??? tip "Подсказки" + + 1. Каждое неориентированное ребро нужно добавить в обоих направлениях + 2. Граф может содержать циклы, поэтому обязательно отмечайте уже посещенные вершины + 3. Начните с source: встретив destination, верните true; если обход завершился без нее, верните false + +[LeetCode](https://leetcode.com/problems/find-if-path-exists-in-graph/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } diff --git a/ru/docs/chapter_graph/index.md b/ru/docs/chapter_graph/index.md index e5bd8b50c..25a9893b0 100644 --- a/ru/docs/chapter_graph/index.md +++ b/ru/docs/chapter_graph/index.md @@ -19,3 +19,4 @@ icon: material/graphql - [9.2   Базовые операции графа](graph_operations.md) - [9.3   Обход графа](graph_traversal.md) - [9.4   Краткие итоги](summary.md) +- [9.5   Упражнения](exercises.md) diff --git a/ru/docs/chapter_greedy/exercises.md b/ru/docs/chapter_greedy/exercises.md new file mode 100644 index 000000000..a20c5bbeb --- /dev/null +++ b/ru/docs/chapter_greedy/exercises.md @@ -0,0 +1,90 @@ +--- +comments: true +--- + + + +# 15.6   Упражнения + +## 15.6.1   Вопросы для самопроверки + +### 1.   Всегда ли лучше брать самую крупную монету + +Даны монеты номиналов `[1, 7, 10]` и целевая сумма 14. + + + +1. Запишите монеты, выбранные по правилу «каждый раз брать наибольший номинал, не превышающий оставшуюся сумму». +2. Существует ли способ использовать меньше монет? Если да, приведите один пример; если нет, объясните почему. +3. Доказывает ли результат, что эта жадная стратегия верна для любых номиналов монет? + +??? success "Ответ" + + 1. Жадный алгоритм последовательно выбирает `10 + 1 + 1 + 1 + 1`, всего 5 монет. + + 2. Способ с меньшим числом монет существует: `7 + 7`, всего 2 монеты. + + 3. Нет. Этот контрпример показывает, что для произвольных номиналов выбор наибольшей допустимой в данный момент монеты не всегда дает минимальное число монет; + лучший на текущем шаге выбор может помешать получить более выгодную комбинацию в дальнейшем. + +### 2.   Какой предмет первым положить в рюкзак + +В рюкзак вместимостью 4 килограмма можно положить следующие предметы. Разрешено брать только часть предмета, +а полученная ценность пропорциональна взятому весу. + +- предмет A: вес 4 килограмма, ценность 20; +- предмет B: вес 3 килограмма, ценность 18. + + + +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.   Как переместить два указателя на следующем шаге + +Высоты вертикальных перегородок заданы массивом `[1, 8, 6, 2, 5]`. Для поиска наибольшей вместимости используются два указателя на концах массива. +Вместимость равна произведению высоты более короткой из двух перегородок на расстояние между их индексами. + + + +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   Задачи по программированию + +### 1.   Дробный рюкзак + +Даны массивы одинаковой длины `wgt` и `val`, где `wgt[i] > 0`, `val[i] >= 0`, а вместимость рюкзака `cap >= 0`. +Каждый предмет имеется в одном экземпляре, но разрешено положить в рюкзак только его часть; +полученная ценность пропорциональна доле веса предмета, которую положили в рюкзак. Используйте жадный алгоритм +и верните максимальную суммарную ценность как вещественное число. + +??? tip "Подсказки" + + 1. Сначала вычислите ценность единицы веса каждого предмета как val[i] / wgt[i]; результат деления должен сохранять дробную часть + 2. Чем выше ценность единицы веса предмета, тем раньше его нужно класть в рюкзак + 3. Если оставшаяся вместимость меньше веса текущего предмета, возьмите такую его часть, которая точно заполнит рюкзак, и завершите работу diff --git a/ru/docs/chapter_greedy/index.md b/ru/docs/chapter_greedy/index.md index a28127938..175e9250e 100644 --- a/ru/docs/chapter_greedy/index.md +++ b/ru/docs/chapter_greedy/index.md @@ -20,3 +20,4 @@ icon: material/head-heart-outline - [15.3   Задача о максимальной вместимости](max_capacity_problem.md) - [15.4   Задача о максимальном произведении разбиения](max_product_cutting_problem.md) - [15.5   Резюме](summary.md) +- [15.6   Упражнения](exercises.md) diff --git a/ru/docs/chapter_hashing/exercises.md b/ru/docs/chapter_hashing/exercises.md new file mode 100644 index 000000000..fb9c65b28 --- /dev/null +++ b/ru/docs/chapter_hashing/exercises.md @@ -0,0 +1,107 @@ +--- +comments: true +--- + + + +# 6.5   Упражнения + +## 6.5.1   Вопросы для самопроверки + +### 1.   Как искать после хеш-коллизии + +Хеш-таблица содержит 5 бакетов и использует хеш-функцию $h(x)=x \bmod 5$. При коллизии элементы последовательно добавляются в список соответствующего бакета. +В таблицу по порядку вставляют `[1, 6, 11, 7]`. + + + +1. Запишите содержимое бакетов с номерами 0–4. +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.   Куда перемещаются элементы при расширении хеш-таблицы + +Хеш-таблица с цепной адресацией первоначально содержит 5 бакетов и использует хеш-функцию $h(x)=x\bmod5$. +Ключи `[1, 6, 11]` находятся в бакете 1. + +Теперь таблицу расширяют до 7 бакетов, и хеш-функция принимает вид $h(x)=x\bmod7$. + + + +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 в бакет 6, а за 11 — в бакет 4 + и не найдет эти ключи. + +### 3.   Можно ли найти 11 после удаления 6 + +Хеш-таблица содержит 5 позиций с индексами `0–4` и использует хеш-функцию $h(x)=x\bmod5$. +При коллизии поиск первого свободного места начинается с вычисленного хеш-функцией индекса и продолжается вправо. + +В таблицу по порядку вставляют `[1, 6, 11]`. + + + +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 остановится на индексе 2 после проверки индекса 1 + и ошибочно решит, что числа 11 нет. При удалении следует оставлять отметку «удалено»: + встретив ее, поиск продолжает проверять следующий индекс (после индекса 4 возвращаясь к индексу 0), а при последующей вставке эту позицию можно использовать снова. + +## 6.5.2   Задачи по программированию + +### 1.   Сравнение состава символов в двух строках + +Даны две строки `s` и `t`, состоящие только из строчных английских букв. +Символы в `s` можно переставлять произвольным образом, но нельзя добавлять, удалять или заменять. + +Определите, можно ли после перестановки получить `t`. Если можно, верните `true`, иначе — `false`. +Используйте хеш-таблицу для подсчета числа вхождений каждой буквы и не сортируйте символы в строках. + +??? tip "Подсказки" + + 1. Если строки имеют разную длину, их состав символов не может совпадать + 2. Сохраняйте в хеш-таблице количество каждой буквы; при обходе s увеличивайте соответствующий счетчик на 1 + 3. При обходе t уменьшайте соответствующий счетчик на 1; состав символов совпадает, только если все счетчики в конце равны 0 + +[LeetCode](https://leetcode.com/problems/valid-anagram/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } diff --git a/ru/docs/chapter_hashing/index.md b/ru/docs/chapter_hashing/index.md index 1d70f87d2..187280534 100644 --- a/ru/docs/chapter_hashing/index.md +++ b/ru/docs/chapter_hashing/index.md @@ -19,3 +19,4 @@ icon: material/table-search - [6.2   Хеш-коллизии](hash_collision.md) - [6.3   Алгоритмы хеширования](hash_algorithm.md) - [6.4   Резюме](summary.md) +- [6.5   Упражнения](exercises.md) diff --git a/ru/docs/chapter_heap/exercises.md b/ru/docs/chapter_heap/exercises.md new file mode 100644 index 000000000..668b2eebc --- /dev/null +++ b/ru/docs/chapter_heap/exercises.md @@ -0,0 +1,92 @@ +--- +comments: true +--- + + + +# 8.5   Упражнения + +## 8.5.1   Вопросы для самопроверки + +### 1.   Как перестраивается куча после добавления числа 10 + +Массив `[9, 7, 8, 3, 5]` представляет максимальную кучу. Теперь в нее добавляют число 10. + + + +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.   Проверка отношений между родительскими и дочерними узлами минимальной кучи + +Массив `[1, 4, 3, 7, 6, 2]` представляет полное двоичное дерево. В минимальной куче значение каждого родительского узла не превышает значения его дочерних узлов. +Для индекса $i$ индексы левого и правого дочерних узлов равны $2i+1$ и $2i+2$ соответственно. + + + +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.   Сохранение трех наибольших чисел в минимальной куче + +Чтобы сохранять 3 наибольших числа из потока данных `[4, 1, 7, 3, 8]`, можно поддерживать минимальную кучу размером не более 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   Задачи по программированию + +### 1.   k-й наибольший элемент массива + +Даны массив целых чисел `nums` и целое число $k$, где $1 \le k \le n$, а $n$ — длина массива. Если расположить массив по убыванию, верните элемент на позиции $k$. + +Повторяющиеся элементы учитываются по отдельности. Например, 2-й наибольший элемент массива `[5, 5, 2]` по-прежнему равен 5. Используйте минимальную кучу размером не более $k$. + +??? tip "Подсказки" + + 1. k-й наибольший элемент — наименьший среди k наибольших чисел + 2. Добавляйте каждое число в минимальную кучу, а когда ее размер превышает k, извлекайте наименьшее значение + 3. После обхода в куче останутся k наибольших чисел, а на ее вершине будет находиться ответ + +[LeetCode](https://leetcode.com/problems/kth-largest-element-in-an-array/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } diff --git a/ru/docs/chapter_heap/index.md b/ru/docs/chapter_heap/index.md index 17131ff7f..3598fe2ed 100644 --- a/ru/docs/chapter_heap/index.md +++ b/ru/docs/chapter_heap/index.md @@ -19,3 +19,4 @@ icon: material/family-tree - [8.2   Построение кучи](build_heap.md) - [8.3   Задача Top-k](top_k.md) - [8.4   Резюме](summary.md) +- [8.5   Упражнения](exercises.md) diff --git a/ru/docs/chapter_searching/exercises.md b/ru/docs/chapter_searching/exercises.md new file mode 100644 index 000000000..b3460a4e0 --- /dev/null +++ b/ru/docs/chapter_searching/exercises.md @@ -0,0 +1,101 @@ +--- +comments: true +--- + + + +# 10.7   Упражнения + +## 10.7.1   Вопросы для самопроверки + +### 1.   Как двоичный поиск сужает область поиска + +Найдите число 16 в упорядоченном массиве `[2, 5, 8, 12, 16, 23, 38]`. +Используйте двойной замкнутый интервал `[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.   Левая и правая границы повторяющегося элемента + +В массиве `[1, 2, 2, 2, 4, 6]` ищут число 2. +Ученик выполнил двоичный поиск, нашел цель по индексу 2, сразу вернул ответ и сказал: +«Индекс 2 — левая граница числа 2». + + + +1. Верно ли утверждение ученика? Чему равны левая и правая границы числа 2? Объясните ответ. +2. В какой части массива нужно продолжить поиск левой границы, если средний элемент равен цели? +3. В какой части нужно продолжить поиск правой границы? Достаточно указать направление, полный процесс поиска записывать не нужно. + +??? success "Ответ" + + 1. Утверждение неверно. Немедленный возврат после обнаружения числа 2 гарантирует лишь, что найден один из таких элементов, но не самый левый или самый правый. + Левая граница в этом массиве имеет индекс 1, а правая — индекс 3. + + 2. При поиске левой границы, даже если средний элемент равен 2, нужно продолжить поиск слева, + например, при двойном замкнутом интервале присвоить `j = m - 1`. + + 3. При поиске правой границы после совпадения среднего элемента с 2 нужно продолжить поиск справа, + например, присвоить `i = m + 1`. + +### 3.   Как выбрать способ поиска для разных данных + +Для каждой ситуации выберите подходящий вариант из «линейный поиск, двоичный поиск, хеш-таблица» и объясните свой выбор. + + + +1. Многократно выполнять поиск среди $10^7$ уже упорядоченных и больше не изменяющихся целых чисел, не создавая дополнительных структур данных; +2. многократно проверять наличие ключа в наборе с частыми вставками и удалениями, не поддерживая порядок и не выполняя поиск по диапазону; +3. один раз найти некоторое значение в неупорядоченном массиве. + +??? success "Ответ" + + 1. Двоичный поиск: данные упорядочены и не меняются, сложность равна $O(\log n)$, дополнительная память не нужна. + 2. Хеш-таблица: если хеш-функция достаточно равномерно распределяет ключи по бакетам, средняя временная сложность вставки, удаления и поиска по ключу равна $O(1)$. + 3. Прямой обход от начала до конца: ради одного поиска сортировка или построение хеш-таблицы все равно потребует предварительно обработать весь массив + и не уменьшит общий объем работы для этой единственной задачи. + + Выбор зависит от того, упорядочены ли данные, разрешено ли создавать дополнительную структуру, сколько будет запросов и какие операции нужно выполнять. + +## 10.7.2   Задачи по программированию + +### 1.   Двоичный поиск в упорядоченном массиве + +Дан массив целых чисел `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.com/problems/binary-search/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } + +### 2.   Позиция вставки в упорядоченный массив + +Дан массив целых чисел `nums`, упорядоченный строго по возрастанию, и целевое значение `target`. + +Если `target` уже находится в массиве, верните его индекс. Иначе верните позицию, в которую нужно вставить `target`, чтобы массив остался строго возрастающим. Ответ может быть равен 0 или длине массива. Используйте двоичный поиск. + +??? tip "Подсказки" + + 1. Ответ может быть равен 0 или длине массива n + 2. При двойном замкнутом интервале, если `nums[mid]` больше или равно `target`, присвойте `right = mid - 1` и продолжите проверять позиции левее; иначе присвойте `left = mid + 1` + 3. После завершения цикла left будет позицией вставки + +[LeetCode](https://leetcode.com/problems/search-insert-position/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } diff --git a/ru/docs/chapter_searching/index.md b/ru/docs/chapter_searching/index.md index 6c4dec922..10e04625e 100644 --- a/ru/docs/chapter_searching/index.md +++ b/ru/docs/chapter_searching/index.md @@ -21,3 +21,4 @@ icon: material/text-search - [10.4   Стратегии оптимизации хеширования](replace_linear_by_hashing.md) - [10.5   Переосмысление алгоритмов поиска](searching_algorithm_revisited.md) - [10.6   Резюме](summary.md) +- [10.7   Упражнения](exercises.md) diff --git a/ru/docs/chapter_sorting/exercises.md b/ru/docs/chapter_sorting/exercises.md new file mode 100644 index 000000000..716ef18b6 --- /dev/null +++ b/ru/docs/chapter_sorting/exercises.md @@ -0,0 +1,100 @@ +--- +comments: true +--- + + + +# 11.12   Упражнения + +## 11.12.1   Вопросы для самопроверки + +### 1.   Первые итерации сортировки выбором и пузырьком + +Дан массив `[4, 2, 5, 1, 3]`. В обоих случаях элементы сортируются по возрастанию. + + + +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.   Меняется ли взаимный порядок равных элементов + +В массиве $[2_a, 2_b, 1]$ значения $2_a$ и $2_b$ равны, но нижние индексы отмечают их исходный порядок. + + + +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.   Сравнение сортировки подсчетом и поразрядной сортировки + +Школе нужно упорядочить большое количество номеров учеников, каждый из которых состоит ровно из 8 цифр. Ответьте на вопросы. + + + +1. Сколько итераций потребуется поразрядной сортировке, начинающей с младшего разряда? +2. Почему сортировка подсчетом, использующая весь номер как целое число, подготовит множество неиспользуемых позиций для счетчиков? +3. Какой из двух способов вы выберете для большого количества 8-значных номеров — сортировку подсчетом или поразрядную сортировку? Обоснуйте выбор с учетом первых двух ответов. + +??? success "Ответ" + + 1. Номер состоит из 8 разрядов, поэтому от младшего до старшего потребуется 8 итераций; на каждой из них элементы распределяются только по цифрам 0–9. + + 2. Для прямого подсчета пришлось бы выделить позиции для всех возможных 8-значных значений, хотя в действительности используется лишь небольшая их часть. + Большинство счетчиков всегда оставалось бы равным 0. + + 3. Следует выбрать поразрядную сортировку. Она использует свойства «фиксированное число разрядов и только 10 возможных значений каждого разряда» и требует лишь 8 повторений стабильной группировки. + Если же использовать весь 8-значный номер как индекс при сортировке подсчетом, пришлось бы выделить счетчики для множества значений, которые никогда не встретятся. + +## 11.12.2   Задачи по программированию + +### 1.   Сортировка массива слиянием + +Дан массив целых чисел `nums`. Самостоятельно реализуйте сортировку слиянием, расположите элементы в неубывающем порядке и верните массив. Не используйте встроенную функцию сортировки языка. + +??? tip "Подсказки" + + 1. Интервал длины не более 1 уже упорядочен + 2. Разделите интервал в середине на две половины и рекурсивно отсортируйте каждую + 3. Слейте два упорядоченных полуинтервала с помощью двух указателей, а затем запишите результат обратно в исходный массив + +[LeetCode](https://leetcode.com/problems/sort-an-array/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } + +### 2.   Сортировка массива целых чисел подсчетом + +Даны массив целых чисел `nums` и неотрицательное целое число $K$. Каждый элемент массива находится в диапазоне от $0$ до $K$. + +Реализуйте сортировку подсчетом, запишите результат в `nums` в неубывающем порядке и верните `nums`. +Не определяйте порядок элементов с помощью сравнений между ними и не используйте встроенную функцию сортировки языка. + +??? tip "Подсказки" + + 1. Поскольку каждый элемент находится в диапазоне от 0 до K, значение элемента можно напрямую использовать как индекс массива счетчиков + 2. При первом обходе nums увеличивайте счетчик в соответствующей позиции на 1 + 3. Затем переберите массив счетчиков от 0 до K: если значение x встретилось несколько раз, запишите x в nums подряд столько же раз diff --git a/ru/docs/chapter_sorting/index.md b/ru/docs/chapter_sorting/index.md index 1fcc141e3..6350079ff 100644 --- a/ru/docs/chapter_sorting/index.md +++ b/ru/docs/chapter_sorting/index.md @@ -26,3 +26,4 @@ icon: material/sort-ascending - [11.9   Сортировка подсчетом](counting_sort.md) - [11.10   Поразрядная сортировка](radix_sort.md) - [11.11   Резюме](summary.md) +- [11.12   Упражнения](exercises.md) diff --git a/ru/docs/chapter_stack_and_queue/exercises.md b/ru/docs/chapter_stack_and_queue/exercises.md new file mode 100644 index 000000000..5852f9167 --- /dev/null +++ b/ru/docs/chapter_stack_and_queue/exercises.md @@ -0,0 +1,105 @@ +--- +comments: true +--- + + + +# 5.5   Упражнения + +## 5.5.1   Вопросы для самопроверки + +### 1.   Что первым извлекается из стека и очереди + +Подготовьте пустой стек `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.   Что происходит, когда хвост очереди проходит конец массива + +Очередь реализована с помощью кольцевого массива длины 5 с индексами `0–4`. +Сейчас `front = 3`, `size = 2`, а элементы очереди `A, B` находятся по индексам 3 и 4 соответственно. + + + +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.   Операции с обоих концов двусторонней очереди + +Здесь `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()` + + + +1. Какие элементы возвращают две операции извлечения? +2. Какие элементы остаются после всех операций в порядке от головы к хвосту? +3. Рассмотрите все 6 шагов. Можно ли выполнить их с обычной очередью, в которую разрешено добавлять только с хвоста, а удалять только с головы? Если нет, укажите недопустимые операции. Объясните также, может ли выполнить все шаги двусторонняя очередь и почему. + +??? success "Ответ" + + После первых трех шагов двусторонняя очередь от головы к хвосту имеет вид `[C, A, B]`. + + + + 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   Задачи по программированию + +### 1.   Проверка последовательности скобок + +Дана строка `s`, содержащая только скобки трех видов: `()`, `[]`, `{}`. С помощью стека определите, является ли последовательность правильной. + +Правильная последовательность должна удовлетворять двум условиям: каждая закрывающая скобка соответствует по виду ближайшей еще не сопоставленной открывающей скобке, +а после обхода не остается несопоставленных открывающих скобок. Верните логическое значение результата проверки. + +??? tip "Подсказки" + + 1. Можно создать отображение из закрывающей скобки в соответствующую открывающую + 2. Встретив закрывающую скобку, сначала проверьте, не пуст ли стек, а затем — совпадает ли его вершина + 3. После обхода стек также должен быть пуст + +[LeetCode](https://leetcode.com/problems/valid-parentheses/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } diff --git a/ru/docs/chapter_stack_and_queue/index.md b/ru/docs/chapter_stack_and_queue/index.md index e5d86d7da..39242e15f 100644 --- a/ru/docs/chapter_stack_and_queue/index.md +++ b/ru/docs/chapter_stack_and_queue/index.md @@ -19,3 +19,4 @@ icon: material/stack-overflow - [5.2   Очередь](queue.md) - [5.3   Двусторонняя очередь](deque.md) - [5.4   Резюме](summary.md) +- [5.5   Упражнения](exercises.md) diff --git a/ru/docs/chapter_tree/exercises.md b/ru/docs/chapter_tree/exercises.md new file mode 100644 index 000000000..78f69553c --- /dev/null +++ b/ru/docs/chapter_tree/exercises.md @@ -0,0 +1,132 @@ +--- +comments: true +--- + + + +# 7.7   Упражнения + +## 7.7.1   Вопросы для самопроверки + +### 1.   Полные, строгие и идеальные двоичные деревья + +Два массива ниже представляют двоичные деревья в порядке обхода по уровням; `None` обозначает пустую позицию. + +- дерево A: `[1, 2, 3, 4, 5, 6]` +- дерево B: `[1, 2, 3, None, None, 6, 7]` + + + +1. Какое из деревьев является полным? +2. Какое из деревьев является строгим, то есть каждый его нелистовой узел имеет два дочерних узла? +3. Есть ли среди них идеальное двоичное дерево? Объясните ответ для каждого дерева. + +??? success "Ответ" + + 1. Дерево A полное. В нем не заполнен только последний уровень, причем узлы на нем расположены непрерывно слева направо. + Дерево B не является полным: на последнем уровне слева уже есть пустые позиции, но справа от них еще находятся узлы. + + 2. Дерево B строгое: узлы 1 и 3 имеют по два дочерних узла, а все остальные узлы являются листьями. + Дерево A не является строгим, потому что у узла 3 есть только левый дочерний узел 6. + + 3. Ни одно дерево не является идеальным, поскольку последний уровень каждого из них заполнен не полностью. + +### 2.   Три порядка обхода одного дерева + +Поместите массив `[1, 2, 3, 4, 5, 6, 7]` по уровням в полное двоичное дерево. + + + +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. Последовательность `4, 2, 5` слева от корневого узла 1 — симметричный обход левого поддерева, + а `6, 3, 7` справа — симметричный обход правого поддерева. + +### 3.   Сравнение двух двоичных деревьев поиска + +Последовательно, слева направо, вставьте каждую из двух последовательностей в пустое двоичное дерево поиска: + +- последовательность A: `[4, 2, 6, 1, 3, 5, 7]` +- последовательность B: `[1, 2, 3, 4, 5, 6, 7]` + + + +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   Задачи по программированию + +### 1.   Максимальная глубина двоичного дерева + +Дан корневой узел `root` двоичного дерева. Каждый узел содержит целое значение и ссылки на левый и правый дочерние узлы. + +Максимальной глубиной двоичного дерева назовем **число узлов** на пути от корневого узла до самого удаленного листового узла. Верните максимальную глубину дерева; глубина пустого дерева равна 0. +Используйте рекурсию. + +??? tip "Подсказки" + + 1. В этой задаче глубина измеряется числом узлов: если в дереве есть только корневой узел, максимальная глубина равна 1 + 2. Пусть рекурсивная функция возвращает максимальную глубину поддерева с корнем в текущем узле + 3. Для пустого узла верните 0, для непустого — max(depth(left), depth(right)) + 1 + +[LeetCode](https://leetcode.com/problems/maximum-depth-of-binary-tree/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } + +### 2.   Обход двоичного дерева по уровням + +Дан корневой узел `root` двоичного дерева. С помощью очереди посетите все узлы уровень за уровнем сверху вниз, а внутри каждого уровня — слева направо. + +Верните двумерный массив: первый вложенный массив хранит значения узлов на уровне корня, второй — значения следующего уровня и так далее. +Для пустого дерева верните пустой массив. + +??? tip "Подсказки" + + 1. При обходе по уровням узлы нужно посещать в порядке их добавления, поэтому используйте очередь + 2. В начале каждой итерации в очереди находятся ровно узлы одного уровня + 3. Сначала запомните длину очереди, затем извлеките столько узлов и добавьте их дочерние узлы + +[LeetCode](https://leetcode.com/problems/binary-tree-level-order-traversal/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } + +### 3.   k-й наименьший элемент двоичного дерева поиска + +Двоичное дерево поиска содержит `n` узлов с попарно различными значениями. +Если расположить все значения по возрастанию, их позиции нумеруются начиная с 1. + +Даны корневой узел `root` и целое число `k`, удовлетворяющее `1 <= k <= n`. Верните значение узла на позиции `k`. +Найдите ответ непосредственно во время симметричного обхода, не собирая предварительно значения всех узлов. + +??? tip "Подсказки" + + 1. Симметричный обход двоичного дерева поиска посещает значения узлов в порядке возрастания + 2. При симметричном обходе последовательно обрабатываются левое поддерево, текущий узел и правое поддерево; при посещении текущего узла увеличьте счетчик на 1 + 3. Когда счетчик впервые станет равен k, значение текущего узла будет ответом и продолжать обход не потребуется + +[LeetCode](https://leetcode.com/problems/kth-smallest-element-in-a-bst/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } diff --git a/ru/docs/chapter_tree/index.md b/ru/docs/chapter_tree/index.md index 8d35f9a33..452e269b2 100644 --- a/ru/docs/chapter_tree/index.md +++ b/ru/docs/chapter_tree/index.md @@ -21,3 +21,4 @@ icon: material/graph-outline - [7.4   Двоичное дерево поиска](binary_search_tree.md) - [7.5   AVL-дерево *](avl_tree.md) - [7.6   Краткие итоги](summary.md) +- [7.7   Упражнения](exercises.md) diff --git a/zh-Hant/docs/chapter_appendix/contribution.md b/zh-hant/docs/chapter_appendix/contribution.md similarity index 100% rename from zh-Hant/docs/chapter_appendix/contribution.md rename to zh-hant/docs/chapter_appendix/contribution.md diff --git a/zh-Hant/docs/chapter_appendix/index.md b/zh-hant/docs/chapter_appendix/index.md similarity index 100% rename from zh-Hant/docs/chapter_appendix/index.md rename to zh-hant/docs/chapter_appendix/index.md diff --git a/zh-Hant/docs/chapter_appendix/installation.md b/zh-hant/docs/chapter_appendix/installation.md similarity index 100% rename from zh-Hant/docs/chapter_appendix/installation.md rename to zh-hant/docs/chapter_appendix/installation.md diff --git a/zh-Hant/docs/chapter_appendix/terminology.md b/zh-hant/docs/chapter_appendix/terminology.md similarity index 100% rename from zh-Hant/docs/chapter_appendix/terminology.md rename to zh-hant/docs/chapter_appendix/terminology.md diff --git a/zh-Hant/docs/chapter_array_and_linkedlist/array.md b/zh-hant/docs/chapter_array_and_linkedlist/array.md similarity index 100% rename from zh-Hant/docs/chapter_array_and_linkedlist/array.md rename to zh-hant/docs/chapter_array_and_linkedlist/array.md diff --git a/zh-hant/docs/chapter_array_and_linkedlist/exercises.md b/zh-hant/docs/chapter_array_and_linkedlist/exercises.md new file mode 100644 index 000000000..df17cfb99 --- /dev/null +++ b/zh-hant/docs/chapter_array_and_linkedlist/exercises.md @@ -0,0 +1,113 @@ +--- +comments: true +hide: [edit] +--- + + + +# 4.6   練習 + +## 4.6.1   知識鞏固 + +### 1.   陣列和鏈結串列怎樣找到元素 + +陣列和單向鏈結串列中都按順序儲存了 `[A, B, C, D, E]`。現在要讀取第 4 個元素 `D`: + + + +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.   陣列和鏈結串列怎樣插入元素 + +陣列和單向鏈結串列中都儲存了 `A、B、C、D`。現在要在 `B` 後面插入 `X`: + +- 陣列容量為 5,當前狀態是 `[A, B, C, D, _]`; +- 鏈結串列為 `A → B → C → D`,並且已經拿到了指向節點 `B` 的引用。 + + + +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.   串列容量是怎樣增長的 + +一個基於陣列實現的串列當前內容為 `[A, B, C]`,長度 `size = 3`,容量 `capacity = 4`。 +規定容量不足時,新陣列的容量擴大為原來的 2 倍。 + + + +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   程式設計練習 + +### 1.   陣列表示的大整數加一 + +陣列 `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.   反轉單向鏈結串列 + +給定一個單向鏈結串列的頭節點 `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" } diff --git a/zh-Hant/docs/chapter_array_and_linkedlist/index.md b/zh-hant/docs/chapter_array_and_linkedlist/index.md similarity index 94% rename from zh-Hant/docs/chapter_array_and_linkedlist/index.md rename to zh-hant/docs/chapter_array_and_linkedlist/index.md index 1d00c0f66..a6343ebf5 100644 --- a/zh-Hant/docs/chapter_array_and_linkedlist/index.md +++ b/zh-hant/docs/chapter_array_and_linkedlist/index.md @@ -20,3 +20,4 @@ icon: material/view-list-outline - [4.3   串列](list.md) - [4.4   記憶體與快取 *](ram_and_cache.md) - [4.5   小結](summary.md) +- [4.6   練習](exercises.md) diff --git a/zh-Hant/docs/chapter_array_and_linkedlist/linked_list.md b/zh-hant/docs/chapter_array_and_linkedlist/linked_list.md similarity index 100% rename from zh-Hant/docs/chapter_array_and_linkedlist/linked_list.md rename to zh-hant/docs/chapter_array_and_linkedlist/linked_list.md diff --git a/zh-Hant/docs/chapter_array_and_linkedlist/list.md b/zh-hant/docs/chapter_array_and_linkedlist/list.md similarity index 100% rename from zh-Hant/docs/chapter_array_and_linkedlist/list.md rename to zh-hant/docs/chapter_array_and_linkedlist/list.md diff --git a/zh-Hant/docs/chapter_array_and_linkedlist/ram_and_cache.md b/zh-hant/docs/chapter_array_and_linkedlist/ram_and_cache.md similarity index 100% rename from zh-Hant/docs/chapter_array_and_linkedlist/ram_and_cache.md rename to zh-hant/docs/chapter_array_and_linkedlist/ram_and_cache.md diff --git a/zh-Hant/docs/chapter_array_and_linkedlist/summary.md b/zh-hant/docs/chapter_array_and_linkedlist/summary.md similarity index 100% rename from zh-Hant/docs/chapter_array_and_linkedlist/summary.md rename to zh-hant/docs/chapter_array_and_linkedlist/summary.md diff --git a/zh-Hant/docs/chapter_backtracking/backtracking_algorithm.md b/zh-hant/docs/chapter_backtracking/backtracking_algorithm.md similarity index 100% rename from zh-Hant/docs/chapter_backtracking/backtracking_algorithm.md rename to zh-hant/docs/chapter_backtracking/backtracking_algorithm.md diff --git a/zh-hant/docs/chapter_backtracking/exercises.md b/zh-hant/docs/chapter_backtracking/exercises.md new file mode 100644 index 000000000..862fedde8 --- /dev/null +++ b/zh-hant/docs/chapter_backtracking/exercises.md @@ -0,0 +1,96 @@ +--- +comments: true +hide: [edit] +--- + + + +# 13.6   練習 + +## 13.6.1   知識鞏固 + +### 1.   這段排列演算法會漏掉結果嗎 + +一個回溯演算法按 `1、2、3` 的順序嘗試生成全部排列。每次選擇數字 `x` 時,它會: + +1. 把 `x` 加到當前路徑末尾; +2. 把 `x` 標記為“已使用”; +3. 遞迴填寫下一個位置。 + +遞迴返回後,同學只從路徑末尾刪除了 `x`,然後繼續嘗試下一個數字。 + + + +1. 演算法首先得到哪個排列?它還能得到全部 6 個排列嗎? +2. 遞迴返回上一層前,只刪除路徑末尾的數字是否足夠?如果不夠,還需要做什麼?說明理由。 + +??? success "參考答案" + + 1. 它首先得到 `[1, 2, 3]`,但無法得到全部排列。雖然返回時路徑變短了, + 數字 1、2、3 的標記仍都是“已使用”,後續分支便沒有可選數字。 + + 2. 不夠。刪除路徑末尾的 `x` 後,還必須把 `x` 重新標記為“未使用”。 + 當前路徑和已使用標記共同描述搜尋狀態;選擇時修改了兩處,回退時也必須把兩處都恢復, + 其他分支才能再次選擇 `x`。 + +### 2.   數字的選擇順序重要嗎 + +給定排好序的陣列 `[2, 3, 5]` 和目標值 5,每個數可以重複選擇。 +演算法規定每條搜尋路徑中的數字只能按從小到大的順序出現。 + + + +1. 能得到哪些不同的組合? +2. 為什麼同一組數字不需要按不同順序重複搜尋?“從小到大”的限制起到了什麼作用? +3. 當前路徑為 `[3]`、還差 2 時,下一個候選數是 3。為什麼此時可以停止檢查這一層後面的所有候選數? + +??? success "參考答案" + + 1. 不同的組合為 `[2, 3]` 和 `[5]`。 + + 2. 本題把 `[2, 3]` 和 `[3, 2]` 看作同一種組合,數字的選擇順序不計入答案。 + 規定路徑中的數字從小到大出現,就能在搜尋時直接跳過 `[3, 2]` 這類重複組合。 + + 3. 當前還差 2,而候選數 3 已經大於 2。因為陣列已經排好序, + 3 後面的候選數只會更大,也都不可能加入當前組合,所以可以直接結束這一層的檢查。 + +### 3.   下一枚皇后可以放在哪些位置 + +在一個 `4 × 4` 棋盤上按行放置皇后,行、列下標都從 0 開始。 +目前已經在 `(0, 1)` 和 `(1, 3)` 放置了皇后,現在要在第 2 行放置下一個皇后。 + + + +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   程式設計練習 + +### 1.   無重複元素的全排列 + +整數陣列 `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" } + +> **說明:** 連結題解透過交換陣列元素,把已選元素依次放到陣列前部;本練習使用布林陣列記錄每個元素是否已選。兩種方法都能避免同一元素被重複選擇,但程式碼結構不同 diff --git a/zh-Hant/docs/chapter_backtracking/index.md b/zh-hant/docs/chapter_backtracking/index.md similarity index 94% rename from zh-Hant/docs/chapter_backtracking/index.md rename to zh-hant/docs/chapter_backtracking/index.md index 66b09b9dd..81f66802e 100644 --- a/zh-Hant/docs/chapter_backtracking/index.md +++ b/zh-hant/docs/chapter_backtracking/index.md @@ -20,3 +20,4 @@ icon: material/map-marker-path - [13.3   子集和問題](subset_sum_problem.md) - [13.4   N 皇后問題](n_queens_problem.md) - [13.5   小結](summary.md) +- [13.6   練習](exercises.md) diff --git a/zh-Hant/docs/chapter_backtracking/n_queens_problem.md b/zh-hant/docs/chapter_backtracking/n_queens_problem.md similarity index 100% rename from zh-Hant/docs/chapter_backtracking/n_queens_problem.md rename to zh-hant/docs/chapter_backtracking/n_queens_problem.md diff --git a/zh-Hant/docs/chapter_backtracking/permutations_problem.md b/zh-hant/docs/chapter_backtracking/permutations_problem.md similarity index 100% rename from zh-Hant/docs/chapter_backtracking/permutations_problem.md rename to zh-hant/docs/chapter_backtracking/permutations_problem.md diff --git a/zh-Hant/docs/chapter_backtracking/subset_sum_problem.md b/zh-hant/docs/chapter_backtracking/subset_sum_problem.md similarity index 100% rename from zh-Hant/docs/chapter_backtracking/subset_sum_problem.md rename to zh-hant/docs/chapter_backtracking/subset_sum_problem.md diff --git a/zh-Hant/docs/chapter_backtracking/summary.md b/zh-hant/docs/chapter_backtracking/summary.md similarity index 100% rename from zh-Hant/docs/chapter_backtracking/summary.md rename to zh-hant/docs/chapter_backtracking/summary.md diff --git a/zh-hant/docs/chapter_computational_complexity/exercises.md b/zh-hant/docs/chapter_computational_complexity/exercises.md new file mode 100644 index 000000000..8ac0ec2a5 --- /dev/null +++ b/zh-hant/docs/chapter_computational_complexity/exercises.md @@ -0,0 +1,117 @@ +--- +comments: true +hide: [edit] +--- + + + +# 2.6   練習 + +## 2.6.1   知識鞏固 + +### 1.   迭代與遞迴的時間和空間 + +下面兩段程式碼都計算 $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) +``` + + + +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.   三段程式碼的時間複雜度 + +以下三個程式碼片段的輸入均為正整數 $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.   哪種反轉更節省空間 + +要將陣列 `nums` 中的元素全部反轉,有兩種做法: + + + +1. 新建一個等長陣列 `res`,倒序複製後返回; +2. 用兩個索引 `i` 和 `j` 分別從首、尾向中間移動,逐對交換 `nums[i]` 與 `nums[j]` 。 + + 兩種做法的空間複雜度各是多少?哪種屬於“原地”操作? + +??? success "參考答案" + + 1. 需要與輸入等長的輔助陣列,空間複雜度 $O(n)$。 + + 2. 只使用兩個索引變數, + 空間複雜度 $O(1)$ ,屬於原地操作。 + + 需要注意:原地反轉會修改輸入陣列, + 僅在允許修改輸入時才應優先選用;若需保留原陣列,第 1 種做法的複製開銷不可避免。 + +## 2.6.2   程式設計練習 + +### 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" } diff --git a/zh-Hant/docs/chapter_computational_complexity/index.md b/zh-hant/docs/chapter_computational_complexity/index.md similarity index 94% rename from zh-Hant/docs/chapter_computational_complexity/index.md rename to zh-hant/docs/chapter_computational_complexity/index.md index 8bcfd745b..118bd2151 100644 --- a/zh-Hant/docs/chapter_computational_complexity/index.md +++ b/zh-hant/docs/chapter_computational_complexity/index.md @@ -20,3 +20,4 @@ icon: material/timer-sand - [2.3   時間複雜度](time_complexity.md) - [2.4   空間複雜度](space_complexity.md) - [2.5   小結](summary.md) +- [2.6   練習](exercises.md) diff --git a/zh-Hant/docs/chapter_computational_complexity/iteration_and_recursion.md b/zh-hant/docs/chapter_computational_complexity/iteration_and_recursion.md similarity index 100% rename from zh-Hant/docs/chapter_computational_complexity/iteration_and_recursion.md rename to zh-hant/docs/chapter_computational_complexity/iteration_and_recursion.md diff --git a/zh-Hant/docs/chapter_computational_complexity/performance_evaluation.md b/zh-hant/docs/chapter_computational_complexity/performance_evaluation.md similarity index 100% rename from zh-Hant/docs/chapter_computational_complexity/performance_evaluation.md rename to zh-hant/docs/chapter_computational_complexity/performance_evaluation.md diff --git a/zh-Hant/docs/chapter_computational_complexity/space_complexity.md b/zh-hant/docs/chapter_computational_complexity/space_complexity.md similarity index 100% rename from zh-Hant/docs/chapter_computational_complexity/space_complexity.md rename to zh-hant/docs/chapter_computational_complexity/space_complexity.md diff --git a/zh-Hant/docs/chapter_computational_complexity/summary.md b/zh-hant/docs/chapter_computational_complexity/summary.md similarity index 100% rename from zh-Hant/docs/chapter_computational_complexity/summary.md rename to zh-hant/docs/chapter_computational_complexity/summary.md diff --git a/zh-Hant/docs/chapter_computational_complexity/time_complexity.md b/zh-hant/docs/chapter_computational_complexity/time_complexity.md similarity index 100% rename from zh-Hant/docs/chapter_computational_complexity/time_complexity.md rename to zh-hant/docs/chapter_computational_complexity/time_complexity.md diff --git a/zh-Hant/docs/chapter_data_structure/basic_data_types.md b/zh-hant/docs/chapter_data_structure/basic_data_types.md similarity index 100% rename from zh-Hant/docs/chapter_data_structure/basic_data_types.md rename to zh-hant/docs/chapter_data_structure/basic_data_types.md diff --git a/zh-Hant/docs/chapter_data_structure/character_encoding.md b/zh-hant/docs/chapter_data_structure/character_encoding.md similarity index 100% rename from zh-Hant/docs/chapter_data_structure/character_encoding.md rename to zh-hant/docs/chapter_data_structure/character_encoding.md diff --git a/zh-Hant/docs/chapter_data_structure/classification_of_data_structure.md b/zh-hant/docs/chapter_data_structure/classification_of_data_structure.md similarity index 100% rename from zh-Hant/docs/chapter_data_structure/classification_of_data_structure.md rename to zh-hant/docs/chapter_data_structure/classification_of_data_structure.md diff --git a/zh-hant/docs/chapter_data_structure/exercises.md b/zh-hant/docs/chapter_data_structure/exercises.md new file mode 100644 index 000000000..98ab33ca5 --- /dev/null +++ b/zh-hant/docs/chapter_data_structure/exercises.md @@ -0,0 +1,93 @@ +--- +comments: true +hide: [edit] +--- + + + +# 3.6   練習 + +## 3.6.1   知識鞏固 + +### 1.   生活場景中的資料關係 + +請根據資料之間的關係,為下面三個場景選擇“線性結構、樹形結構、網狀結構”中的一種,並說明理由: + + + +1. 同學們排成一列,每人只關心自己前面和後面的人; +2. 學校按“學校 → 年級 → 班級”分層管理; +3. 城市道路連線多個路口,一個路口可以通向多個其他路口,也可能形成環路。 + +??? success "參考答案" + + 1. 是線性結構。除排在最前和最後的人外,每個人都只與前面和後面各一人相鄰,關係沿一條線展開。 + + 2. 是樹形結構。每個班級屬於一個年級,每個年級又屬於學校,關係從上到下分層展開。 + + 3. 是網狀結構。一個路口可以連線多個路口,路線之間還能形成環,不能排成單一順序或嚴格層級。 + + 判斷結構時應先觀察元素之間的關係,而不是先考慮它在記憶體中佔多少空間。 + +### 2.   邏輯順序怎樣存進記憶體 + +為了儲存邏輯順序 `A → B → C`,現有兩種簡化的記憶體安排: + +- 方案甲:`A、B、C` 分別放在編號為 `20、21、22` 的記憶體格中; +- 方案乙:`A、B、C` 分別放在編號為 `20、7、31` 的記憶體格中,並由 `A` 記錄 `B` 的位置、`B` 記錄 `C` 的位置。 + + + +1. 哪個方案屬於連續空間儲存?哪個屬於分散空間儲存? +2. 兩個方案分別更接近陣列還是鏈結串列? +3. 方案乙的記憶體格編號沒有按大小排列,為什麼仍能表示 `A → B → C` 的邏輯順序? + +??? success "參考答案" + + 1. 方案甲使用連續的記憶體格,屬於連續空間儲存;方案乙的節點分散在不同位置,屬於分散空間儲存。 + + 2. 方案甲更接近陣列,方案乙更接近鏈結串列。 + + 3. 邏輯順序由節點之間記錄的連線關係決定,而不是由記憶體格編號的大小決定。 + 從 `A` 記錄的位置可以找到 `B`,再從 `B` 記錄的位置找到 `C`,所以仍能依次訪問 `A、B、C`。 + + 這也說明邏輯結構和物理結構是觀察同一組資料的兩個不同角度。 + +### 3.   作業記錄中的資料型別與結構 + +某學習小組按座位順序記錄 4 名同學是否交了作業,得到: + +`[true, false, true, true]` + + + +1. 每個元素適合使用哪種基本資料型別? +2. 這 4 個元素按座位順序排成一列,使用了什麼邏輯結構? +3. 如果以後改為記錄每人的作業分數 `[90, 0, 85, 100]`,改變的是資料的“內容型別”還是“組織方式”?說明理由。 + +??? success "參考答案" + + 1. 每個元素只表示“是”或“否”,適合使用布林型別 `bool`。 + + 2. 這些元素按座位順序排列,形成線性結構,可以用陣列儲存。 + + 3. 改變的是內容型別:元素由布林值變成了整數。組織方式沒有改變, + 這些資料仍然按座位順序排成一列,仍可使用陣列這一線性結構。 + + 基本資料型別描述“存的是什麼”,資料結構描述“資料怎樣組織”。 + +## 3.6.2   程式設計練習 + +### 1.   統計二進位制表示中的 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" } diff --git a/zh-Hant/docs/chapter_data_structure/index.md b/zh-hant/docs/chapter_data_structure/index.md similarity index 94% rename from zh-Hant/docs/chapter_data_structure/index.md rename to zh-hant/docs/chapter_data_structure/index.md index 8e0c0d2b3..1eaeced16 100644 --- a/zh-Hant/docs/chapter_data_structure/index.md +++ b/zh-hant/docs/chapter_data_structure/index.md @@ -20,3 +20,4 @@ icon: material/shape-outline - [3.3   數字編碼 *](number_encoding.md) - [3.4   字元編碼 *](character_encoding.md) - [3.5   小結](summary.md) +- [3.6   練習](exercises.md) diff --git a/zh-Hant/docs/chapter_data_structure/number_encoding.md b/zh-hant/docs/chapter_data_structure/number_encoding.md similarity index 100% rename from zh-Hant/docs/chapter_data_structure/number_encoding.md rename to zh-hant/docs/chapter_data_structure/number_encoding.md diff --git a/zh-Hant/docs/chapter_data_structure/summary.md b/zh-hant/docs/chapter_data_structure/summary.md similarity index 100% rename from zh-Hant/docs/chapter_data_structure/summary.md rename to zh-hant/docs/chapter_data_structure/summary.md diff --git a/zh-Hant/docs/chapter_divide_and_conquer/binary_search_recur.md b/zh-hant/docs/chapter_divide_and_conquer/binary_search_recur.md similarity index 100% rename from zh-Hant/docs/chapter_divide_and_conquer/binary_search_recur.md rename to zh-hant/docs/chapter_divide_and_conquer/binary_search_recur.md diff --git a/zh-Hant/docs/chapter_divide_and_conquer/build_binary_tree_problem.md b/zh-hant/docs/chapter_divide_and_conquer/build_binary_tree_problem.md similarity index 100% rename from zh-Hant/docs/chapter_divide_and_conquer/build_binary_tree_problem.md rename to zh-hant/docs/chapter_divide_and_conquer/build_binary_tree_problem.md diff --git a/zh-Hant/docs/chapter_divide_and_conquer/divide_and_conquer.md b/zh-hant/docs/chapter_divide_and_conquer/divide_and_conquer.md similarity index 100% rename from zh-Hant/docs/chapter_divide_and_conquer/divide_and_conquer.md rename to zh-hant/docs/chapter_divide_and_conquer/divide_and_conquer.md diff --git a/zh-hant/docs/chapter_divide_and_conquer/exercises.md b/zh-hant/docs/chapter_divide_and_conquer/exercises.md new file mode 100644 index 000000000..61da83ff3 --- /dev/null +++ b/zh-hant/docs/chapter_divide_and_conquer/exercises.md @@ -0,0 +1,105 @@ +--- +comments: true +hide: [edit] +--- + + + +# 12.6   練習 + +## 12.6.1   知識鞏固 + +### 1.   哪些任務適合分治 + +一位同學想用“先分成兩半,分別解決,再合併結果”的方法完成以下任務。 +請分別判斷它們是“適合分治”“可以分治,但不會減少總工作量”還是“兩半不能獨立解決”,並說明理由。 + + + +1. 將一個無序陣列排序; +2. 求一個陣列中的最大值; +3. 按順序執行一串 `push(x)`、`pop()` 堆疊操作,並輸出每次 `pop()` 得到的元素。 + +??? success "參考答案" + + 1. 適合:對半分解、兩半獨立排序、$O(n)$ 合併——這正是合併排序。 + 2. 可以分治,但不會減少總工作量:左右兩半仍需合計檢查全部 $n$ 個元素, + 和直接掃描一樣都是 $O(n)$ 時間。 + 3. 兩半不能獨立解決:後半段開始時的堆疊內容取決於前半段執行結果, + 兩半不能在互不知道結果時獨立完成。 + +### 2.   快速冪是怎樣減少計算的 + +下面的遞迴函式用分治計算 $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)`: + + + +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.   從走訪序列拆分左右子樹 + +一棵沒有重複節點的二元樹,其前序走訪和中序走訪分別為: + +- 前序走訪:`[A, B, D, E, C]` +- 中序走訪:`[D, B, E, A, C]` + +只完成根節點這一層的拆分,不必繼續遞迴,也不必畫出整棵樹: + + + +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   程式設計練習 + +### 1.   快速冪 + +給定實數 `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" } + +> **說明:** 連結中的題解使用迭代位運算,與本練習指定的遞迴分治方法不同;請根據本題的解題提示完成遞迴版本 diff --git a/zh-Hant/docs/chapter_divide_and_conquer/hanota_problem.md b/zh-hant/docs/chapter_divide_and_conquer/hanota_problem.md similarity index 100% rename from zh-Hant/docs/chapter_divide_and_conquer/hanota_problem.md rename to zh-hant/docs/chapter_divide_and_conquer/hanota_problem.md diff --git a/zh-Hant/docs/chapter_divide_and_conquer/index.md b/zh-hant/docs/chapter_divide_and_conquer/index.md similarity index 94% rename from zh-Hant/docs/chapter_divide_and_conquer/index.md rename to zh-hant/docs/chapter_divide_and_conquer/index.md index de010e855..a92b4fbf4 100644 --- a/zh-Hant/docs/chapter_divide_and_conquer/index.md +++ b/zh-hant/docs/chapter_divide_and_conquer/index.md @@ -20,3 +20,4 @@ icon: material/set-split - [12.3   構建樹問題](build_binary_tree_problem.md) - [12.4   河內塔問題](hanota_problem.md) - [12.5   小結](summary.md) +- [12.6   練習](exercises.md) diff --git a/zh-Hant/docs/chapter_divide_and_conquer/summary.md b/zh-hant/docs/chapter_divide_and_conquer/summary.md similarity index 100% rename from zh-Hant/docs/chapter_divide_and_conquer/summary.md rename to zh-hant/docs/chapter_divide_and_conquer/summary.md diff --git a/zh-Hant/docs/chapter_dynamic_programming/dp_problem_features.md b/zh-hant/docs/chapter_dynamic_programming/dp_problem_features.md similarity index 100% rename from zh-Hant/docs/chapter_dynamic_programming/dp_problem_features.md rename to zh-hant/docs/chapter_dynamic_programming/dp_problem_features.md diff --git a/zh-Hant/docs/chapter_dynamic_programming/dp_solution_pipeline.md b/zh-hant/docs/chapter_dynamic_programming/dp_solution_pipeline.md similarity index 100% rename from zh-Hant/docs/chapter_dynamic_programming/dp_solution_pipeline.md rename to zh-hant/docs/chapter_dynamic_programming/dp_solution_pipeline.md diff --git a/zh-Hant/docs/chapter_dynamic_programming/edit_distance_problem.md b/zh-hant/docs/chapter_dynamic_programming/edit_distance_problem.md similarity index 100% rename from zh-Hant/docs/chapter_dynamic_programming/edit_distance_problem.md rename to zh-hant/docs/chapter_dynamic_programming/edit_distance_problem.md diff --git a/zh-hant/docs/chapter_dynamic_programming/exercises.md b/zh-hant/docs/chapter_dynamic_programming/exercises.md new file mode 100644 index 000000000..d4beba11e --- /dev/null +++ b/zh-hant/docs/chapter_dynamic_programming/exercises.md @@ -0,0 +1,120 @@ +--- +comments: true +hide: [edit] +--- + + + +# 14.8   練習 + +## 14.8.1   知識鞏固 + +### 1.   什麼時候適合使用動態規劃 + +一位同學說:“只要能寫出遞推式,就應該使用動態規劃。” +請判斷下面三個任務分別更適合使用動態規劃、回溯,還是使用迴圈或數學公式而不建立 `dp` 表,並寫出一個關鍵理由。 + + + +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.   背包表中的一個格子怎麼算 + +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`: + + + +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.   背包容量應該按什麼順序更新 + +一個 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`。 + + + +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   程式設計練習 + +### 1.   爬樓梯的方案數 + +一段樓梯共有 `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.   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. 容量必須從大到小更新,避免在同一輪中重複選擇當前物品 diff --git a/zh-Hant/docs/chapter_dynamic_programming/index.md b/zh-hant/docs/chapter_dynamic_programming/index.md similarity index 95% rename from zh-Hant/docs/chapter_dynamic_programming/index.md rename to zh-hant/docs/chapter_dynamic_programming/index.md index 94eff4d45..bab76060d 100644 --- a/zh-Hant/docs/chapter_dynamic_programming/index.md +++ b/zh-hant/docs/chapter_dynamic_programming/index.md @@ -22,3 +22,4 @@ icon: material/table-pivot - [14.5   完全背包問題](unbounded_knapsack_problem.md) - [14.6   編輯距離問題](edit_distance_problem.md) - [14.7   小結](summary.md) +- [14.8   練習](exercises.md) diff --git a/zh-Hant/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md b/zh-hant/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md similarity index 100% rename from zh-Hant/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md rename to zh-hant/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md diff --git a/zh-Hant/docs/chapter_dynamic_programming/knapsack_problem.md b/zh-hant/docs/chapter_dynamic_programming/knapsack_problem.md similarity index 100% rename from zh-Hant/docs/chapter_dynamic_programming/knapsack_problem.md rename to zh-hant/docs/chapter_dynamic_programming/knapsack_problem.md diff --git a/zh-Hant/docs/chapter_dynamic_programming/summary.md b/zh-hant/docs/chapter_dynamic_programming/summary.md similarity index 100% rename from zh-Hant/docs/chapter_dynamic_programming/summary.md rename to zh-hant/docs/chapter_dynamic_programming/summary.md diff --git a/zh-Hant/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md b/zh-hant/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md similarity index 100% rename from zh-Hant/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md rename to zh-hant/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md diff --git a/zh-hant/docs/chapter_graph/exercises.md b/zh-hant/docs/chapter_graph/exercises.md new file mode 100644 index 000000000..600f3b061 --- /dev/null +++ b/zh-hant/docs/chapter_graph/exercises.md @@ -0,0 +1,110 @@ +--- +comments: true +hide: [edit] +--- + + + +# 9.5   練習 + +## 9.5.1   知識鞏固 + +### 1.   用兩種方式表示同一張圖 + +一個無向圖有 4 個頂點 `A、B、C、D`,邊為 +`A-B、A-C、B-C、C-D`。 + + + +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.   廣度優先與深度優先訪問順序 + +一個無向圖的頂點為 `A、B、C、D、E`,邊為 +`A-B、A-C、B-D、C-D、D-E`。 + +從 A 出發,並規定遇到多個未訪問鄰接頂點時按字母順序選擇: + + + +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.   一次 BFS 能訪問整張圖嗎 + +一個無向圖有頂點 `A、B、C、D、E、F`,邊只有 +`A-B、B-C、D-E`。 + + + +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   程式設計練習 + +### 1.   判斷無向圖中是否存在路徑 + +給定一個含有 $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" } diff --git a/zh-Hant/docs/chapter_graph/graph.md b/zh-hant/docs/chapter_graph/graph.md similarity index 100% rename from zh-Hant/docs/chapter_graph/graph.md rename to zh-hant/docs/chapter_graph/graph.md diff --git a/zh-Hant/docs/chapter_graph/graph_operations.md b/zh-hant/docs/chapter_graph/graph_operations.md similarity index 100% rename from zh-Hant/docs/chapter_graph/graph_operations.md rename to zh-hant/docs/chapter_graph/graph_operations.md diff --git a/zh-Hant/docs/chapter_graph/graph_traversal.md b/zh-hant/docs/chapter_graph/graph_traversal.md similarity index 100% rename from zh-Hant/docs/chapter_graph/graph_traversal.md rename to zh-hant/docs/chapter_graph/graph_traversal.md diff --git a/zh-Hant/docs/chapter_graph/index.md b/zh-hant/docs/chapter_graph/index.md similarity index 93% rename from zh-Hant/docs/chapter_graph/index.md rename to zh-hant/docs/chapter_graph/index.md index 6fb2dc91f..824e52e8e 100644 --- a/zh-Hant/docs/chapter_graph/index.md +++ b/zh-hant/docs/chapter_graph/index.md @@ -19,3 +19,4 @@ icon: material/graphql - [9.2   圖基礎操作](graph_operations.md) - [9.3   圖的走訪](graph_traversal.md) - [9.4   小結](summary.md) +- [9.5   練習](exercises.md) diff --git a/zh-Hant/docs/chapter_graph/summary.md b/zh-hant/docs/chapter_graph/summary.md similarity index 100% rename from zh-Hant/docs/chapter_graph/summary.md rename to zh-hant/docs/chapter_graph/summary.md diff --git a/zh-hant/docs/chapter_greedy/exercises.md b/zh-hant/docs/chapter_greedy/exercises.md new file mode 100644 index 000000000..0862a5a7e --- /dev/null +++ b/zh-hant/docs/chapter_greedy/exercises.md @@ -0,0 +1,90 @@ +--- +comments: true +hide: [edit] +--- + + + +# 15.6   練習 + +## 15.6.1   知識鞏固 + +### 1.   每次選最大硬幣一定最好嗎 + +硬幣面值為 `[1, 7, 10]`,目標金額為 14。 + + + +1. 按“每次取不超過剩餘金額的最大面值”執行,寫出所取硬幣; +2. 是否存在使用更少硬幣的方案?如果存在,請寫出一種;如果不存在,請說明理由; +3. 這能否說明該貪婪策略對任意硬幣面值都正確? + +??? success "參考答案" + + 1. 貪婪依次選擇 `10 + 1 + 1 + 1 + 1`,共 5 枚硬幣。 + + 2. 存在使用更少硬幣的方案:`7 + 7`,只需 2 枚硬幣。 + + 3. 不能。這個反例說明,對任意硬幣面值,每次選擇當前能取的最大面值並不一定得到最少硬幣數; + 眼前最大的選擇可能破壞後續更好的組合。 + +### 2.   背包應該先裝哪件物品 + +一個容量為 4 千克的背包中可以裝入以下物品,物品允許只取一部分, +所取價值與重量成正比: + +- 物品 A:重量 4 千克,價值 20; +- 物品 B:重量 3 千克,價值 18。 + + + +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.   兩個指標下一步怎樣移動 + +隔板高度為 `[1, 8, 6, 2, 5]`,用首尾雙指標尋找最大容量。 +容量等於“兩塊隔板中較短者的高度 × 兩塊隔板的下標距離”。 + + + +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   程式設計練習 + +### 1.   分數背包 + +給定等長陣列 `wgt` 和 `val`,其中 `wgt[i] > 0`、`val[i] >= 0`,背包容量 `cap >= 0`。 +每種物品只有一件,但允許只裝入其中一部分, +獲得的價值按所裝重量佔該物品總重量的比例計算。請使用貪婪演算法, +並以實數返回背包能夠獲得的最大總價值。 + +??? tip "解題提示" + + 1. 先計算每件物品的單位重量價值 val[i] / wgt[i],除法結果要保留小數 + 2. 單位價值越高的物品越先裝入背包 + 3. 若剩餘容量小於當前物品重量,就裝入恰好填滿背包的部分並結束 diff --git a/zh-Hant/docs/chapter_greedy/fractional_knapsack_problem.md b/zh-hant/docs/chapter_greedy/fractional_knapsack_problem.md similarity index 100% rename from zh-Hant/docs/chapter_greedy/fractional_knapsack_problem.md rename to zh-hant/docs/chapter_greedy/fractional_knapsack_problem.md diff --git a/zh-Hant/docs/chapter_greedy/greedy_algorithm.md b/zh-hant/docs/chapter_greedy/greedy_algorithm.md similarity index 100% rename from zh-Hant/docs/chapter_greedy/greedy_algorithm.md rename to zh-hant/docs/chapter_greedy/greedy_algorithm.md diff --git a/zh-Hant/docs/chapter_greedy/index.md b/zh-hant/docs/chapter_greedy/index.md similarity index 94% rename from zh-Hant/docs/chapter_greedy/index.md rename to zh-hant/docs/chapter_greedy/index.md index a4af506a7..c8d0c67a4 100644 --- a/zh-Hant/docs/chapter_greedy/index.md +++ b/zh-hant/docs/chapter_greedy/index.md @@ -20,3 +20,4 @@ icon: material/head-heart-outline - [15.3   最大容量問題](max_capacity_problem.md) - [15.4   最大切分乘積問題](max_product_cutting_problem.md) - [15.5   小結](summary.md) +- [15.6   練習](exercises.md) diff --git a/zh-Hant/docs/chapter_greedy/max_capacity_problem.md b/zh-hant/docs/chapter_greedy/max_capacity_problem.md similarity index 100% rename from zh-Hant/docs/chapter_greedy/max_capacity_problem.md rename to zh-hant/docs/chapter_greedy/max_capacity_problem.md diff --git a/zh-Hant/docs/chapter_greedy/max_product_cutting_problem.md b/zh-hant/docs/chapter_greedy/max_product_cutting_problem.md similarity index 100% rename from zh-Hant/docs/chapter_greedy/max_product_cutting_problem.md rename to zh-hant/docs/chapter_greedy/max_product_cutting_problem.md diff --git a/zh-Hant/docs/chapter_greedy/summary.md b/zh-hant/docs/chapter_greedy/summary.md similarity index 100% rename from zh-Hant/docs/chapter_greedy/summary.md rename to zh-hant/docs/chapter_greedy/summary.md diff --git a/zh-hant/docs/chapter_hashing/exercises.md b/zh-hant/docs/chapter_hashing/exercises.md new file mode 100644 index 000000000..ff0c18e05 --- /dev/null +++ b/zh-hant/docs/chapter_hashing/exercises.md @@ -0,0 +1,108 @@ +--- +comments: true +hide: [edit] +--- + + + +# 6.5   練習 + +## 6.5.1   知識鞏固 + +### 1.   發生雜湊衝突後怎樣查詢 + +一個雜湊表有 5 個桶,雜湊函式為 $h(x)=x \bmod 5$,衝突時把元素依次放進該桶的串列中。 +依次插入 `[1, 6, 11, 7]`: + + + +1. 寫出 0~4 號桶中的內容; +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.   雜湊表擴容後元素去哪兒 + +一個使用鏈式位址的雜湊表原有 5 個桶,雜湊函式為 $h(x)=x\bmod5$。 +鍵 `[1, 6, 11]` 都在 1 號桶中。 + +現在把雜湊表擴容為 7 個桶,雜湊函式相應變為 $h(x)=x\bmod7$: + + + +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.   刪除 6 後還能找到 11 嗎 + +一個雜湊表有 5 個位置,索引為 `0~4`,雜湊函式為 $h(x)=x\bmod5$。 +發生衝突時,從雜湊函式算出的索引開始,向右尋找第一個空位。 + +依次插入 `[1, 6, 11]`: + + + +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   程式設計練習 + +### 1.   比較兩個字串的字元組成 + +給定兩個只含小寫英文字母的字串 `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" } diff --git a/zh-Hant/docs/chapter_hashing/hash_algorithm.md b/zh-hant/docs/chapter_hashing/hash_algorithm.md similarity index 100% rename from zh-Hant/docs/chapter_hashing/hash_algorithm.md rename to zh-hant/docs/chapter_hashing/hash_algorithm.md diff --git a/zh-Hant/docs/chapter_hashing/hash_collision.md b/zh-hant/docs/chapter_hashing/hash_collision.md similarity index 100% rename from zh-Hant/docs/chapter_hashing/hash_collision.md rename to zh-hant/docs/chapter_hashing/hash_collision.md diff --git a/zh-Hant/docs/chapter_hashing/hash_map.md b/zh-hant/docs/chapter_hashing/hash_map.md similarity index 100% rename from zh-Hant/docs/chapter_hashing/hash_map.md rename to zh-hant/docs/chapter_hashing/hash_map.md diff --git a/zh-Hant/docs/chapter_hashing/index.md b/zh-hant/docs/chapter_hashing/index.md similarity index 93% rename from zh-Hant/docs/chapter_hashing/index.md rename to zh-hant/docs/chapter_hashing/index.md index 299791fd6..f24c993b9 100644 --- a/zh-Hant/docs/chapter_hashing/index.md +++ b/zh-hant/docs/chapter_hashing/index.md @@ -19,3 +19,4 @@ icon: material/table-search - [6.2   雜湊衝突](hash_collision.md) - [6.3   雜湊演算法](hash_algorithm.md) - [6.4   小結](summary.md) +- [6.5   練習](exercises.md) diff --git a/zh-Hant/docs/chapter_hashing/summary.md b/zh-hant/docs/chapter_hashing/summary.md similarity index 100% rename from zh-Hant/docs/chapter_hashing/summary.md rename to zh-hant/docs/chapter_hashing/summary.md diff --git a/zh-Hant/docs/chapter_heap/build_heap.md b/zh-hant/docs/chapter_heap/build_heap.md similarity index 100% rename from zh-Hant/docs/chapter_heap/build_heap.md rename to zh-hant/docs/chapter_heap/build_heap.md diff --git a/zh-hant/docs/chapter_heap/exercises.md b/zh-hant/docs/chapter_heap/exercises.md new file mode 100644 index 000000000..aef44d033 --- /dev/null +++ b/zh-hant/docs/chapter_heap/exercises.md @@ -0,0 +1,95 @@ +--- +comments: true +hide: [edit] +--- + + + +# 8.5   練習 + +## 8.5.1   知識鞏固 + +### 1.   數字 10 入堆積後怎樣調整 + +陣列 `[9, 7, 8, 3, 5]` 表示一個大頂堆積。現在將數字 10 入堆積。 + + + +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.   檢查小頂堆積的父子關係 + +陣列 `[1, 4, 3, 7, 6, 2]` 表示一棵完全二元樹。小頂堆積要求每個父節點都不大於它的孩子。 +對索引 $i$,左、右孩子索引分別為 $2i+1$ 和 $2i+2$。 + + + +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.   用小頂堆積保留最大的三個數 + +要從資料流 `[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   程式設計練習 + +### 1.   尋找陣列中的第 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 的小頂堆積完成 diff --git a/zh-Hant/docs/chapter_heap/heap.md b/zh-hant/docs/chapter_heap/heap.md similarity index 100% rename from zh-Hant/docs/chapter_heap/heap.md rename to zh-hant/docs/chapter_heap/heap.md diff --git a/zh-Hant/docs/chapter_heap/index.md b/zh-hant/docs/chapter_heap/index.md similarity index 92% rename from zh-Hant/docs/chapter_heap/index.md rename to zh-hant/docs/chapter_heap/index.md index 4c9581c92..eb0933e07 100644 --- a/zh-Hant/docs/chapter_heap/index.md +++ b/zh-hant/docs/chapter_heap/index.md @@ -19,3 +19,4 @@ icon: material/family-tree - [8.2   建堆積操作](build_heap.md) - [8.3   Top-k 問題](top_k.md) - [8.4   小結](summary.md) +- [8.5   練習](exercises.md) diff --git a/zh-Hant/docs/chapter_heap/summary.md b/zh-hant/docs/chapter_heap/summary.md similarity index 100% rename from zh-Hant/docs/chapter_heap/summary.md rename to zh-hant/docs/chapter_heap/summary.md diff --git a/zh-Hant/docs/chapter_heap/top_k.md b/zh-hant/docs/chapter_heap/top_k.md similarity index 100% rename from zh-Hant/docs/chapter_heap/top_k.md rename to zh-hant/docs/chapter_heap/top_k.md diff --git a/zh-Hant/docs/chapter_hello_algo/index.md b/zh-hant/docs/chapter_hello_algo/index.md similarity index 100% rename from zh-Hant/docs/chapter_hello_algo/index.md rename to zh-hant/docs/chapter_hello_algo/index.md diff --git a/zh-Hant/docs/chapter_introduction/algorithms_are_everywhere.md b/zh-hant/docs/chapter_introduction/algorithms_are_everywhere.md similarity index 100% rename from zh-Hant/docs/chapter_introduction/algorithms_are_everywhere.md rename to zh-hant/docs/chapter_introduction/algorithms_are_everywhere.md diff --git a/zh-Hant/docs/chapter_introduction/index.md b/zh-hant/docs/chapter_introduction/index.md similarity index 100% rename from zh-Hant/docs/chapter_introduction/index.md rename to zh-hant/docs/chapter_introduction/index.md diff --git a/zh-Hant/docs/chapter_introduction/summary.md b/zh-hant/docs/chapter_introduction/summary.md similarity index 100% rename from zh-Hant/docs/chapter_introduction/summary.md rename to zh-hant/docs/chapter_introduction/summary.md diff --git a/zh-Hant/docs/chapter_introduction/what_is_dsa.md b/zh-hant/docs/chapter_introduction/what_is_dsa.md similarity index 100% rename from zh-Hant/docs/chapter_introduction/what_is_dsa.md rename to zh-hant/docs/chapter_introduction/what_is_dsa.md diff --git a/zh-Hant/docs/chapter_preface/about_the_book.md b/zh-hant/docs/chapter_preface/about_the_book.md similarity index 100% rename from zh-Hant/docs/chapter_preface/about_the_book.md rename to zh-hant/docs/chapter_preface/about_the_book.md diff --git a/zh-Hant/docs/chapter_preface/index.md b/zh-hant/docs/chapter_preface/index.md similarity index 100% rename from zh-Hant/docs/chapter_preface/index.md rename to zh-hant/docs/chapter_preface/index.md diff --git a/zh-Hant/docs/chapter_preface/suggestions.md b/zh-hant/docs/chapter_preface/suggestions.md similarity index 100% rename from zh-Hant/docs/chapter_preface/suggestions.md rename to zh-hant/docs/chapter_preface/suggestions.md diff --git a/zh-Hant/docs/chapter_preface/summary.md b/zh-hant/docs/chapter_preface/summary.md similarity index 100% rename from zh-Hant/docs/chapter_preface/summary.md rename to zh-hant/docs/chapter_preface/summary.md diff --git a/zh-Hant/docs/chapter_reference/index.md b/zh-hant/docs/chapter_reference/index.md similarity index 100% rename from zh-Hant/docs/chapter_reference/index.md rename to zh-hant/docs/chapter_reference/index.md diff --git a/zh-Hant/docs/chapter_searching/binary_search.md b/zh-hant/docs/chapter_searching/binary_search.md similarity index 100% rename from zh-Hant/docs/chapter_searching/binary_search.md rename to zh-hant/docs/chapter_searching/binary_search.md diff --git a/zh-Hant/docs/chapter_searching/binary_search_edge.md b/zh-hant/docs/chapter_searching/binary_search_edge.md similarity index 100% rename from zh-Hant/docs/chapter_searching/binary_search_edge.md rename to zh-hant/docs/chapter_searching/binary_search_edge.md diff --git a/zh-Hant/docs/chapter_searching/binary_search_insertion.md b/zh-hant/docs/chapter_searching/binary_search_insertion.md similarity index 100% rename from zh-Hant/docs/chapter_searching/binary_search_insertion.md rename to zh-hant/docs/chapter_searching/binary_search_insertion.md diff --git a/zh-hant/docs/chapter_searching/exercises.md b/zh-hant/docs/chapter_searching/exercises.md new file mode 100644 index 000000000..90171db0b --- /dev/null +++ b/zh-hant/docs/chapter_searching/exercises.md @@ -0,0 +1,102 @@ +--- +comments: true +hide: [edit] +--- + + + +# 10.7   練習 + +## 10.7.1   知識鞏固 + +### 1.   二分搜尋怎樣縮小搜尋區間 + +在有序陣列 `[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.   重複元素的左右邊界 + +在陣列 `[1, 2, 2, 2, 4, 6]` 中查詢數字 2。 +一名同學用二分搜尋在索引 2 找到目標後立即返回,並說: +“索引 2 就是數字 2 的左邊界。” + + + +1. 這名同學的說法是否正確?數字 2 的左、右邊界分別是多少?請說明理由。 +2. 查詢左邊界時,若中點元素等於目標,接下來應繼續搜尋哪一側? +3. 查詢右邊界時又應繼續搜尋哪一側?只需說明方向,無須寫出完整的查詢過程。 + +??? success "參考答案" + + 1. 這名同學的說法不正確。找到一個 2 後立即返回,只能保證找到某一個 2,不能保證找到最左或最右的 2。 + 本題的左邊界為索引 1,右邊界為索引 3。 + + 2. 查詢左邊界時,即使中點元素等於 2,也要繼續搜尋左側, + 例如在雙閉區間寫法中令 `j = m - 1`。 + + 3. 查詢右邊界時,中點元素等於 2 後應繼續搜尋右側, + 例如令 `i = m + 1`。 + +### 3.   不同資料該選哪種搜尋方法 + +請在“順序查詢、二分搜尋、雜湊表”中,為下面三個場景選擇合適的方法,並說明理由: + + + +1. 在 $10^7$ 個已經有序且不再變動的整數中反覆查詢,不額外建立其他資料結構; +2. 在頻繁插入、刪除的資料集中反覆判斷某個鍵是否存在,不要求保持有序,也不進行範圍查詢; +3. 在無序陣列中只查詢一次某個值。 + +??? success "參考答案" + + 1. 二分搜尋:資料有序且靜態,$O(\log n)$ 且無須額外空間。 + 2. 雜湊表:當雜湊函式能把鍵較均勻地分散到各桶時,插入、刪除和按鍵查詢的平均時間複雜度都是 $O(1)$。 + 3. 直接從頭到尾走訪:只查詢一次時,排序或建立雜湊表都要先處理整個陣列, + 並不會減少這一次任務的總工作量。 + + 選擇取決於資料是否有序、是否允許建立額外結構、查詢次數以及需要執行哪些操作。 + +## 10.7.2   程式設計練習 + +### 1.   有序陣列的二分搜尋 + +給定一個按嚴格遞增順序排列的整數陣列 `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.   有序陣列的插入位置 + +給定一個按嚴格遞增順序排列的整數陣列 `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" } diff --git a/zh-Hant/docs/chapter_searching/index.md b/zh-hant/docs/chapter_searching/index.md similarity index 95% rename from zh-Hant/docs/chapter_searching/index.md rename to zh-hant/docs/chapter_searching/index.md index ec8ae8cd6..0469d41f0 100644 --- a/zh-Hant/docs/chapter_searching/index.md +++ b/zh-hant/docs/chapter_searching/index.md @@ -21,3 +21,4 @@ icon: material/text-search - [10.4   雜湊最佳化策略](replace_linear_by_hashing.md) - [10.5   重識搜尋演算法](searching_algorithm_revisited.md) - [10.6   小結](summary.md) +- [10.7   練習](exercises.md) diff --git a/zh-Hant/docs/chapter_searching/replace_linear_by_hashing.md b/zh-hant/docs/chapter_searching/replace_linear_by_hashing.md similarity index 100% rename from zh-Hant/docs/chapter_searching/replace_linear_by_hashing.md rename to zh-hant/docs/chapter_searching/replace_linear_by_hashing.md diff --git a/zh-Hant/docs/chapter_searching/searching_algorithm_revisited.md b/zh-hant/docs/chapter_searching/searching_algorithm_revisited.md similarity index 100% rename from zh-Hant/docs/chapter_searching/searching_algorithm_revisited.md rename to zh-hant/docs/chapter_searching/searching_algorithm_revisited.md diff --git a/zh-Hant/docs/chapter_searching/summary.md b/zh-hant/docs/chapter_searching/summary.md similarity index 100% rename from zh-Hant/docs/chapter_searching/summary.md rename to zh-hant/docs/chapter_searching/summary.md diff --git a/zh-Hant/docs/chapter_sorting/bubble_sort.md b/zh-hant/docs/chapter_sorting/bubble_sort.md similarity index 100% rename from zh-Hant/docs/chapter_sorting/bubble_sort.md rename to zh-hant/docs/chapter_sorting/bubble_sort.md diff --git a/zh-Hant/docs/chapter_sorting/bucket_sort.md b/zh-hant/docs/chapter_sorting/bucket_sort.md similarity index 100% rename from zh-Hant/docs/chapter_sorting/bucket_sort.md rename to zh-hant/docs/chapter_sorting/bucket_sort.md diff --git a/zh-Hant/docs/chapter_sorting/counting_sort.md b/zh-hant/docs/chapter_sorting/counting_sort.md similarity index 100% rename from zh-Hant/docs/chapter_sorting/counting_sort.md rename to zh-hant/docs/chapter_sorting/counting_sort.md diff --git a/zh-hant/docs/chapter_sorting/exercises.md b/zh-hant/docs/chapter_sorting/exercises.md new file mode 100644 index 000000000..37152d015 --- /dev/null +++ b/zh-hant/docs/chapter_sorting/exercises.md @@ -0,0 +1,101 @@ +--- +comments: true +hide: [edit] +--- + + + +# 11.12   練習 + +## 11.12.1   知識鞏固 + +### 1.   選擇排序和泡沫排序的前幾輪 + +給定陣列 `[4, 2, 5, 1, 3]`,以下都按從小到大排序。 + + + +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.   相同元素的先後次序會變嗎 + +陣列 $[2_a, 2_b, 1]$ 中,$2_a$ 和 $2_b$ 的數值相同,但用下標標出了原有次序。 + + + +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.   比較計數排序和基數排序 + +學校要排序許多固定為 8 位的學號。請回答: + + + +1. 從最低位開始的基數排序需要處理幾輪? +2. 如果把學號直接當作整數使用計數排序,為什麼會準備大量沒有用到的計數位置? +3. 根據前兩問,如果在計數排序和基數排序之間選擇一種處理大量固定為 8 位的學號,你會選擇哪一種?為什麼? + +??? success "參考答案" + + 1. 學號有 8 個數位,因此從最低位到最高位共處理 8 輪;每輪只按 0~9 分組。 + + 2. 直接計數需要為所有可能的 8 位取值預留位置,但實際學生只使用其中很少一部分, + 大多數計數位置會一直為 0。 + + 3. 應選擇基數排序。它利用了“位數固定、每位只有 10 種取值”的特點,只需重複 8 輪穩定分組。 + 計數排序若把整個 8 位學號當作整數下標,則需要為大量實際不會出現的數值預留計數位置。 + +## 11.12.2   程式設計練習 + +### 1.   用合併排序排列陣列 + +給定一個整數陣列 `nums`,請自行實現合併排序,把其中的元素按非遞減順序排列並返回。不要呼叫語言自帶的排序函式。 + +??? tip "解題提示" + + 1. 區間長度不超過 1 時,它已經有序 + 2. 從中點把區間分成兩半,並分別遞迴排序 + 3. 用兩個指標合併兩個有序半區,再把結果寫回原陣列 + +[LeetCode](https://leetcode.cn/problems/sort-an-array/){ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" } + +### 2.   用計數排序排列整數陣列 + +給定整數陣列 `nums` 和非負整數 $K$,陣列中的每個元素都在 $0$ 到 $K$ 之間。 + +請實現計數排序,把結果按非遞減順序寫回 `nums` 並返回 `nums`。 +不要使用元素之間的大小比較來決定順序,也不要呼叫語言自帶的排序函式。 + +??? tip "解題提示" + + 1. 因為每個元素都在 0 到 K 之間,可以直接用元素值作為計數陣列的索引 + 2. 第一遍掃描 nums,令對應位置的計數加 1 + 3. 再從 0 到 K 掃描計數陣列;數值 x 出現多少次,就向 nums 中連續寫入多少個 x diff --git a/zh-Hant/docs/chapter_sorting/heap_sort.md b/zh-hant/docs/chapter_sorting/heap_sort.md similarity index 100% rename from zh-Hant/docs/chapter_sorting/heap_sort.md rename to zh-hant/docs/chapter_sorting/heap_sort.md diff --git a/zh-Hant/docs/chapter_sorting/index.md b/zh-hant/docs/chapter_sorting/index.md similarity index 96% rename from zh-Hant/docs/chapter_sorting/index.md rename to zh-hant/docs/chapter_sorting/index.md index 1142a29ef..3b13acfe2 100644 --- a/zh-Hant/docs/chapter_sorting/index.md +++ b/zh-hant/docs/chapter_sorting/index.md @@ -26,3 +26,4 @@ icon: material/sort-ascending - [11.9   計數排序](counting_sort.md) - [11.10   基數排序](radix_sort.md) - [11.11   小結](summary.md) +- [11.12   練習](exercises.md) diff --git a/zh-Hant/docs/chapter_sorting/insertion_sort.md b/zh-hant/docs/chapter_sorting/insertion_sort.md similarity index 100% rename from zh-Hant/docs/chapter_sorting/insertion_sort.md rename to zh-hant/docs/chapter_sorting/insertion_sort.md diff --git a/zh-Hant/docs/chapter_sorting/merge_sort.md b/zh-hant/docs/chapter_sorting/merge_sort.md similarity index 100% rename from zh-Hant/docs/chapter_sorting/merge_sort.md rename to zh-hant/docs/chapter_sorting/merge_sort.md diff --git a/zh-Hant/docs/chapter_sorting/quick_sort.md b/zh-hant/docs/chapter_sorting/quick_sort.md similarity index 100% rename from zh-Hant/docs/chapter_sorting/quick_sort.md rename to zh-hant/docs/chapter_sorting/quick_sort.md diff --git a/zh-Hant/docs/chapter_sorting/radix_sort.md b/zh-hant/docs/chapter_sorting/radix_sort.md similarity index 100% rename from zh-Hant/docs/chapter_sorting/radix_sort.md rename to zh-hant/docs/chapter_sorting/radix_sort.md diff --git a/zh-Hant/docs/chapter_sorting/selection_sort.md b/zh-hant/docs/chapter_sorting/selection_sort.md similarity index 100% rename from zh-Hant/docs/chapter_sorting/selection_sort.md rename to zh-hant/docs/chapter_sorting/selection_sort.md diff --git a/zh-Hant/docs/chapter_sorting/sorting_algorithm.md b/zh-hant/docs/chapter_sorting/sorting_algorithm.md similarity index 100% rename from zh-Hant/docs/chapter_sorting/sorting_algorithm.md rename to zh-hant/docs/chapter_sorting/sorting_algorithm.md diff --git a/zh-Hant/docs/chapter_sorting/summary.md b/zh-hant/docs/chapter_sorting/summary.md similarity index 100% rename from zh-Hant/docs/chapter_sorting/summary.md rename to zh-hant/docs/chapter_sorting/summary.md diff --git a/zh-Hant/docs/chapter_stack_and_queue/deque.md b/zh-hant/docs/chapter_stack_and_queue/deque.md similarity index 100% rename from zh-Hant/docs/chapter_stack_and_queue/deque.md rename to zh-hant/docs/chapter_stack_and_queue/deque.md diff --git a/zh-hant/docs/chapter_stack_and_queue/exercises.md b/zh-hant/docs/chapter_stack_and_queue/exercises.md new file mode 100644 index 000000000..cb8f85e48 --- /dev/null +++ b/zh-hant/docs/chapter_stack_and_queue/exercises.md @@ -0,0 +1,106 @@ +--- +comments: true +hide: [edit] +--- + + + +# 5.5   練習 + +## 5.5.1   知識鞏固 + +### 1.   堆疊和佇列會先取出誰 + +準備一個空堆疊 `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.   佇列尾越過陣列末尾怎麼辦 + +用長度為 5 的環形陣列實現佇列,陣列索引為 `0~4`。 +當前 `front = 3`、`size = 2`,佇列中的 `A、B` 分別位於索引 3、4。 + + + +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.   雙向佇列的兩端操作 + +這裡規定:`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()` + + + +1. 兩次彈出的元素分別是什麼? +2. 全部操作完成後,從佇列首到佇列尾還剩哪些元素? +3. 檢查這 6 步操作:只允許從佇列尾加入、從佇列首刪除的佇列能否全部完成?如果不能,請指出無法完成的操作;再說明雙向佇列能否完成及其原因。 + +??? success "參考答案" + + 前三步後,雙向佇列從佇列首到佇列尾為 `[C, A, B]`。 + + + + 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   程式設計練習 + +### 1.   檢查括號序列 + +給定一個只包含 `()`、`[]`、`{}` 這三類括號的字串 `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" } diff --git a/zh-Hant/docs/chapter_stack_and_queue/index.md b/zh-hant/docs/chapter_stack_and_queue/index.md similarity index 93% rename from zh-Hant/docs/chapter_stack_and_queue/index.md rename to zh-hant/docs/chapter_stack_and_queue/index.md index 9a6bfde74..ea75f3329 100644 --- a/zh-Hant/docs/chapter_stack_and_queue/index.md +++ b/zh-hant/docs/chapter_stack_and_queue/index.md @@ -19,3 +19,4 @@ icon: material/stack-overflow - [5.2   佇列](queue.md) - [5.3   雙向佇列](deque.md) - [5.4   小結](summary.md) +- [5.5   練習](exercises.md) diff --git a/zh-Hant/docs/chapter_stack_and_queue/queue.md b/zh-hant/docs/chapter_stack_and_queue/queue.md similarity index 100% rename from zh-Hant/docs/chapter_stack_and_queue/queue.md rename to zh-hant/docs/chapter_stack_and_queue/queue.md diff --git a/zh-Hant/docs/chapter_stack_and_queue/stack.md b/zh-hant/docs/chapter_stack_and_queue/stack.md similarity index 100% rename from zh-Hant/docs/chapter_stack_and_queue/stack.md rename to zh-hant/docs/chapter_stack_and_queue/stack.md diff --git a/zh-Hant/docs/chapter_stack_and_queue/summary.md b/zh-hant/docs/chapter_stack_and_queue/summary.md similarity index 100% rename from zh-Hant/docs/chapter_stack_and_queue/summary.md rename to zh-hant/docs/chapter_stack_and_queue/summary.md diff --git a/zh-Hant/docs/chapter_tree/array_representation_of_tree.md b/zh-hant/docs/chapter_tree/array_representation_of_tree.md similarity index 100% rename from zh-Hant/docs/chapter_tree/array_representation_of_tree.md rename to zh-hant/docs/chapter_tree/array_representation_of_tree.md diff --git a/zh-Hant/docs/chapter_tree/avl_tree.md b/zh-hant/docs/chapter_tree/avl_tree.md similarity index 100% rename from zh-Hant/docs/chapter_tree/avl_tree.md rename to zh-hant/docs/chapter_tree/avl_tree.md diff --git a/zh-Hant/docs/chapter_tree/binary_search_tree.md b/zh-hant/docs/chapter_tree/binary_search_tree.md similarity index 100% rename from zh-Hant/docs/chapter_tree/binary_search_tree.md rename to zh-hant/docs/chapter_tree/binary_search_tree.md diff --git a/zh-Hant/docs/chapter_tree/binary_tree.md b/zh-hant/docs/chapter_tree/binary_tree.md similarity index 100% rename from zh-Hant/docs/chapter_tree/binary_tree.md rename to zh-hant/docs/chapter_tree/binary_tree.md diff --git a/zh-Hant/docs/chapter_tree/binary_tree_traversal.md b/zh-hant/docs/chapter_tree/binary_tree_traversal.md similarity index 100% rename from zh-Hant/docs/chapter_tree/binary_tree_traversal.md rename to zh-hant/docs/chapter_tree/binary_tree_traversal.md diff --git a/zh-hant/docs/chapter_tree/exercises.md b/zh-hant/docs/chapter_tree/exercises.md new file mode 100644 index 000000000..9aab730a1 --- /dev/null +++ b/zh-hant/docs/chapter_tree/exercises.md @@ -0,0 +1,133 @@ +--- +comments: true +hide: [edit] +--- + + + +# 7.7   練習 + +## 7.7.1   知識鞏固 + +### 1.   完全、完滿與完美二元樹 + +下面兩個陣列按層序表示二元樹,`None` 表示空位: + +- 樹 A:`[1, 2, 3, 4, 5, 6]` +- 樹 B:`[1, 2, 3, None, None, 6, 7]` + + + +1. 哪一棵是完全二元樹? +2. 哪一棵是完滿二元樹,即每個非葉節點都有兩個子節點? +3. 兩棵樹中是否有完美二元樹?分別說明理由。 + +??? success "參考答案" + + 1. 樹 A 是完全二元樹。它只有最底層未填滿,並且節點從左到右連續排列。 + 樹 B 不是完全二元樹,因為最底層左側已有空位,右側卻仍有節點。 + + 2. 樹 B 是完滿二元樹:節點 1 和節點 3 各有兩個子節點,其餘節點都是葉節點。 + 樹 A 不是完滿二元樹,因為節點 3 只有左子節點 6。 + + 3. 兩棵都不是完美二元樹,因為它們的最底層都沒有完全填滿。 + +### 2.   同一棵樹的三種走訪順序 + +將陣列 `[1, 2, 3, 4, 5, 6, 7]` 按層序存入一棵完全二元樹。 + + + +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.   比較兩棵二元搜尋樹 + +將下面兩組序列從左到右依次插入空的二元搜尋樹: + +- 序列 A:`[4, 2, 6, 1, 3, 5, 7]` +- 序列 B:`[1, 2, 3, 4, 5, 6, 7]` + + + +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   程式設計練習 + +### 1.   二元樹的最大深度 + +給定一棵二元樹的根節點 `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.   按層走訪二元樹 + +給定一棵二元樹的根節點 `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.   二元搜尋樹中的第 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" } diff --git a/zh-Hant/docs/chapter_tree/index.md b/zh-hant/docs/chapter_tree/index.md similarity index 94% rename from zh-Hant/docs/chapter_tree/index.md rename to zh-hant/docs/chapter_tree/index.md index 6be4456e6..7dd1be7da 100644 --- a/zh-Hant/docs/chapter_tree/index.md +++ b/zh-hant/docs/chapter_tree/index.md @@ -21,3 +21,4 @@ icon: material/graph-outline - [7.4   二元搜尋樹](binary_search_tree.md) - [7.5   AVL *](avl_tree.md) - [7.6   小結](summary.md) +- [7.7   練習](exercises.md) diff --git a/zh-Hant/docs/chapter_tree/summary.md b/zh-hant/docs/chapter_tree/summary.md similarity index 100% rename from zh-Hant/docs/chapter_tree/summary.md rename to zh-hant/docs/chapter_tree/summary.md diff --git a/zh-Hant/docs/index.assets/animation.mp4 b/zh-hant/docs/index.assets/animation.mp4 similarity index 100% rename from zh-Hant/docs/index.assets/animation.mp4 rename to zh-hant/docs/index.assets/animation.mp4 diff --git a/zh-Hant/docs/index.assets/btn_download_pdf_epub_dark.svg b/zh-hant/docs/index.assets/btn_download_pdf_epub_dark.svg similarity index 100% rename from zh-Hant/docs/index.assets/btn_download_pdf_epub_dark.svg rename to zh-hant/docs/index.assets/btn_download_pdf_epub_dark.svg diff --git a/zh-Hant/docs/index.assets/btn_read_online_dark.svg b/zh-hant/docs/index.assets/btn_read_online_dark.svg similarity index 100% rename from zh-Hant/docs/index.assets/btn_read_online_dark.svg rename to zh-hant/docs/index.assets/btn_read_online_dark.svg diff --git a/zh-Hant/docs/index.assets/comment.mp4 b/zh-hant/docs/index.assets/comment.mp4 similarity index 100% rename from zh-Hant/docs/index.assets/comment.mp4 rename to zh-hant/docs/index.assets/comment.mp4 diff --git a/zh-Hant/docs/index.assets/running_code.mp4 b/zh-hant/docs/index.assets/running_code.mp4 similarity index 100% rename from zh-Hant/docs/index.assets/running_code.mp4 rename to zh-hant/docs/index.assets/running_code.mp4 diff --git a/zh-Hant/docs/index.html b/zh-hant/docs/index.html similarity index 100% rename from zh-Hant/docs/index.html rename to zh-hant/docs/index.html diff --git a/zh-Hant/docs/index.md b/zh-hant/docs/index.md similarity index 100% rename from zh-Hant/docs/index.md rename to zh-hant/docs/index.md