diff --git a/Contents/06.String/03.String-Multi-Pattern-Matching/01.Trie.md b/Contents/06.String/03.String-Multi-Pattern-Matching/01.Trie.md index fff8834b..404ecc83 100644 --- a/Contents/06.String/03.String-Multi-Pattern-Matching/01.Trie.md +++ b/Contents/06.String/03.String-Multi-Pattern-Matching/01.Trie.md @@ -4,7 +4,7 @@ 例如下图就是一棵字典树,其中包含有 `"a"`、`"abc"`、`"acb"`、`"acc"`、`"ach"`、`"b"`、`"chb"` 这 7 个单词。 -![](https://qcdn.itcharge.cn/images/20220210142321.png) +![字典树](https://qcdn.itcharge.cn/images/20240511165918.png) 从图中可以发现,这棵字典树用边来表示字母,从根节点到树上某一节点的路径就代表了一个单词。比如 $1 \rightarrow 2 \rightarrow 6 \rightarrow 10$ 表示的就是单词 `"acc"`。为了清楚地判断某节点路径是否表示一个单词,我们还可以在每个单词对应路径的结束位置增加一个结束标记 $end$(图中红色节点),表示从根节点到这里有一个单词。 @@ -201,7 +201,7 @@ class Trie: # 字典树 例如下图,当我们输入「字典树」后,底下会出现一些以「字典树」为前缀的相关搜索内容。 -![](https://qcdn.itcharge.cn/images/20220210134829.png) +![字典树的应用](https://qcdn.itcharge.cn/images/20220210134829.png) 这个功能实现的基本原理就是字典树。当然,像 Google、必应、百度这样的搜索引擎,在这个功能能的背后肯定做了大量的改进和优化,但它的底层最基本的原理就是「字典树」这种数据结构。 diff --git a/Contents/07.Tree/01.Binary-Tree/01.Binary-Tree-Basic.md b/Contents/07.Tree/01.Binary-Tree/01.Binary-Tree-Basic.md index 55f040ae..982c2f06 100644 --- a/Contents/07.Tree/01.Binary-Tree/01.Binary-Tree-Basic.md +++ b/Contents/07.Tree/01.Binary-Tree/01.Binary-Tree-Basic.md @@ -6,7 +6,7 @@ 之所以把这种数据结构称为「树」是因为这种数据结构看起来就像是一棵倒挂的树,也就是说数据结构中的「树」是根朝上,而叶朝下的。如下图所示。 -![](https://qcdn.itcharge.cn/images/20220221091603.png) +![树](https://qcdn.itcharge.cn/images/20240511171215.png) 「树」具有以下的特点: @@ -17,9 +17,7 @@ 如下图所示,红色节点 $A$ 是根节点,除了根节点之外,还有 $3$ 棵互不相交的子树 $T_1(B, E, H, I, G)$、$T_2(C)$、$T_3(D, F, G, K)$。 -![](https://qcdn.itcharge.cn/images/20220218104556.png) - - +![树与子树](https://qcdn.itcharge.cn/images/20240511171233.png) ### 1.2 树的相关术语 @@ -29,7 +27,7 @@ **「树的节点」** 由一个数据元素和若干个指向其子树的树的分支组成。而节点所含有的子树个数称为 **「节点的度」**。度为 $0$ 的节点称为 **「叶子节点」** 或者 **「终端节点」**,度不为 $0$ 的节点称为 **「分支节点」** 或者 **「非终端节点」**。树中各节点的最大度数称为 **「树的度」**。 -![](https://qcdn.itcharge.cn/images/20220218134918.png) +![节点分类](https://qcdn.itcharge.cn/images/20240511171300.png) - **树的节点**:由一个数据元素和若干个指向其子树的树的分支组成。 - **节点的度**:一个节点所含有的子树个数。 @@ -41,7 +39,7 @@ 一个节点的子树的根节点称为该节点的 **「孩子节点」**,相应的,该节点称为孩子的 **「父亲节点」**。同一个父亲节点的孩子节点之间互称为 **「兄弟节点」**。 -![](https://qcdn.itcharge.cn/images/20220218142604.png) +![节点间关系](https://qcdn.itcharge.cn/images/20240511171311.png) - **孩子节点(子节点)**:一个节点含有的子树的根节点称为该节点的子节点。例如图中 $B$ 是 $A$ 的孩子节点。 - **父亲节点(父节点)**:如果一个节点含有子节点,则这个节点称为其子节点的父节点。例如图中 $B$ 是 $E$ 的父亲节点。 @@ -51,7 +49,7 @@ **「节点的层次」** 是从根节点开始定义,将根节点作为第 1 层,根的孩子节点作为第 2 层,以此类推,如果某个节点在第 $i$ 层,则其孩子节点在第 $i + 1$ 层。而父亲节点在同一层的节点互为 **「堂兄弟节点」**。树中所有节点最大的层数称为 **「树的深度」** 或 **「树的高度」**。树中,两个节点之间所经过节点序列称为 **「路径」**,两个节点之间路径上经过的边数称为 **「路径长度」**。 -![树的其他术语](https://qcdn.itcharge.cn/images/20231225174453.png) +![树的其他术语](https://qcdn.itcharge.cn/images/20240511171325.png) - **节点的层次**:从根节点开始定义,根为第 $1$ 层,根的子节点为第 $2$ 层,以此类推。 - **树的深度(高度)**:所有节点中最大的层数。例如图中树的深度为 $4$。 @@ -78,7 +76,7 @@ 下图就是一棵二叉树。 -![](https://qcdn.itcharge.cn/images/20220221094909.png) +![二叉树](https://qcdn.itcharge.cn/images/20240511171342.png) 二叉树也可以使用递归方式来定义,即二叉树满足以下两个要求之一: @@ -89,7 +87,7 @@ 二叉树在逻辑上可以分为 $5$ 种基本形态,如下图所示。 -![](https://qcdn.itcharge.cn/images/20220218164839.png) +![二叉树的形态](https://qcdn.itcharge.cn/images/20220218164839.png) ### 2.2 特殊的二叉树 @@ -109,7 +107,7 @@ 我们可以来看几个例子。 -![](https://qcdn.itcharge.cn/images/20220218173007.png) +![满二叉树与非满二叉树](https://qcdn.itcharge.cn/images/20220218173007.png) #### 2.2.2 完全二叉树 @@ -127,7 +125,7 @@ 我们可以来看几个例子。 -![](https://qcdn.itcharge.cn/images/20220218174000.png) +![完全二叉树与非完全二叉树](https://qcdn.itcharge.cn/images/20220218174000.png) #### 2.2.3 二叉搜索树 @@ -139,7 +137,7 @@ 如图所示,这 $3$ 棵树都是二叉搜索树。 -![](https://qcdn.itcharge.cn/images/20220218175944.png) +![二叉搜索树](https://qcdn.itcharge.cn/images/20240511171406.png) #### 2.2.4 平衡二叉搜索树 @@ -153,7 +151,7 @@ 如图所示,前 $2$ 棵树是平衡二叉搜索树,最后一棵树不是平衡二叉搜索树,因为这棵树的左右子树的高度差的绝对值超过了 $1$。 -![](https://qcdn.itcharge.cn/images/20220221103552.png) +![平衡二叉树与非平衡二叉树](https://qcdn.itcharge.cn/images/20220221103552.png) ### 2.3 二叉树的存储结构 @@ -167,7 +165,7 @@ 下图为二叉树的顺序存储结构。 -![](https://qcdn.itcharge.cn/images/20220221144552.png) +![二叉树的顺序存储结构](https://qcdn.itcharge.cn/images/20240511171423.png) 从图中我们也可以看出节点之间的逻辑关系。 @@ -180,7 +178,7 @@ 二叉树采用链式存储结构时,每个链节点包含一个用于数据域 $val$,存储节点信息;还包含两个指针域 $left$ 和 $right$,分别指向左右两个孩子节点,当左孩子或者右孩子不存在时,相应指针域值为空。二叉链节点结构如下图所示。 -![](https://qcdn.itcharge.cn/images/20220221151412.png) +![二叉链节点](https://qcdn.itcharge.cn/images/20240511171434.png) 二叉链节点结构的对应代码为: @@ -194,7 +192,7 @@ class TreeNode: 下面我们将值为 $[1, 2, 3, 4, 5, 6, 7]$ 的二叉树使用链式存储结构进行存储,即为下图所示。 -![](https://qcdn.itcharge.cn/images/20220221153539.png) +![二叉树的链式存储结构](https://qcdn.itcharge.cn/images/20240511171446.png) 二叉树的链表存储结构具有灵活、方便的特点。节点的最大数目只受系统最大可存储空间的限制。一般情况下,二叉树的链表存储结构比顺序存储结构更省空间(用于存储指针域的空间开销只是二叉树中节点数的线性函数),而且对于二叉树实施相关操作也很方便,因此,一般我们使用链式存储结构来存储二叉树。 diff --git a/Contents/07.Tree/01.Binary-Tree/02.Binary-Tree-Traverse.md b/Contents/07.Tree/01.Binary-Tree/02.Binary-Tree-Traverse.md index 7e6d3c0b..5fda7753 100644 --- a/Contents/07.Tree/01.Binary-Tree/02.Binary-Tree-Traverse.md +++ b/Contents/07.Tree/01.Binary-Tree/02.Binary-Tree-Traverse.md @@ -24,7 +24,7 @@ 如下图所示,该二叉树的前序遍历顺序为:$A - B - D - H - I - E - C - F - J - G - K$。 -![](https://qcdn.itcharge.cn/images/20220222165249.png) +![二叉树的前序遍历](https://qcdn.itcharge.cn/images/20240511171628.png) ### 2.1 二叉树的前序遍历递归实现 @@ -104,7 +104,7 @@ class Solution: 如下图所示,该二叉树的中序遍历顺序为:$H - D - I - B - E - A - F - J - C - K - G$。 -![](https://qcdn.itcharge.cn/images/20220222165231.png) +![二叉树的中序遍历](https://qcdn.itcharge.cn/images/20240511171643.png) ### 3.1 二叉树的中序遍历递归实现 @@ -186,7 +186,7 @@ class Solution: 如下图所示,该二叉树的后序遍历顺序为:$H - I - D - E - B - J - F - K - G - C - A$。 -![](https://qcdn.itcharge.cn/images/20220222165218.png) +![二叉树的后序遍历](https://qcdn.itcharge.cn/images/20240511171658.png) ### 4.1 二叉树的后序遍历递归实现 @@ -273,7 +273,7 @@ class Solution: 如下图所示,该二叉树的后序遍历顺序为:$A - B - C - D - E - F - G - H - I - J - K$。 -![](https://qcdn.itcharge.cn/images/20220222165158.png) +![二叉树的层序遍历](https://qcdn.itcharge.cn/images/20240511171712.png) 二叉树的层序遍历是通过队列来实现的。具体步骤如下: diff --git a/Contents/07.Tree/02.Binary-Search-Tree/01.Binary-Search-Tree.md b/Contents/07.Tree/02.Binary-Search-Tree/01.Binary-Search-Tree.md index 8baa3d92..3764b423 100644 --- a/Contents/07.Tree/02.Binary-Search-Tree/01.Binary-Search-Tree.md +++ b/Contents/07.Tree/02.Binary-Search-Tree/01.Binary-Search-Tree.md @@ -8,7 +8,7 @@ 如图所示,这 $3$ 棵树都是二叉搜索树。 -![img](https://qcdn.itcharge.cn/images/20220218175944.png) +![二叉搜索树](https://qcdn.itcharge.cn/images/20240511171406.png) 二叉树具有一个特性,即:**左子树的节点值 < 根节点值 < 右子树的节点值**。 diff --git a/Contents/07.Tree/03.Segment-Tree/01.Segment-Tree.md b/Contents/07.Tree/03.Segment-Tree/01.Segment-Tree.md index 368f40bb..26216110 100644 --- a/Contents/07.Tree/03.Segment-Tree/01.Segment-Tree.md +++ b/Contents/07.Tree/03.Segment-Tree/01.Segment-Tree.md @@ -2,22 +2,22 @@ ### 1.1 线段树的定义 -> **线段树(Segment Tree)**:一种基于分治思想的二叉树,用于在区间上进行信息统计。它的每一个节点都对应一个区间 `[left, right]` ,`left`、`right` 通常是整数。每一个叶子节点表示了一个单位区间(长度为 `1`),叶子节点对应区间上 `left == right`。每一个非叶子节点 `[left, right]` 的左子节点表示的区间都为 `[left, (left + right) / 2]`,右子节点表示的的区间都为 `[(left + right) / 2 + 1, right]`。 +> **线段树(Segment Tree)**:一种基于分治思想的二叉树,用于在区间上进行信息统计。它的每一个节点都对应一个区间 $[left, right]$ ,$left$、$right$ 通常是整数。每一个叶子节点表示了一个单位区间(长度为 $1$),叶子节点对应区间上 $left == right$。每一个非叶子节点 $[left, right]$ 的左子节点表示的区间都为 $[left, (left + right) / 2]$,右子节点表示的的区间都为 $[(left + right) / 2 + 1, right]$。 -线段树是一棵平衡二叉树,树上的每个节点维护一个区间。根节点维护的是整个区间,每个节点维护的是父亲节点的区间二等分之后的其中一个子区间。当有 `n` 个元素时,对区间的操作(单点更新、区间更新、区间查询等)可以在 $O(log_2n)$ 的时间复杂度内完成。 +线段树是一棵平衡二叉树,树上的每个节点维护一个区间。根节点维护的是整个区间,每个节点维护的是父亲节点的区间二等分之后的其中一个子区间。当有 $n$ 个元素时,对区间的操作(单点更新、区间更新、区间查询等)可以在 $O(\log_2n)$ 的时间复杂度内完成。 -如下图所示,这是一棵区间为 `[0, 7]` 的线段树。 +如下图所示,这是一棵区间为 $[0, 7]$ 的线段树。 -![](https://qcdn.itcharge.cn/images/20220302103338.png) +![区间 [0, 7] 对应的线段树](https://qcdn.itcharge.cn/images/20240511173328.png) ### 1.2 线段树的特点 根据上述描述,我们可以总结一下线段树的特点: 1. 线段树的每个节点都代表一个区间。 -2. 线段树具有唯一的根节点,代表的区间是整个统计范围,比如 `[1, n]`。 -3. 线段树的每个叶子节点都代表一个长度为 `1` 的单位区间 `[x, x]`。 -4. 对于每个内部节点 `[left, right]`,它的左子节点是 `[left, mid]`,右子节点是 `[mid + 1, right]`。其中 `mid = (left + right) / 2`(向下取整)。 +2. 线段树具有唯一的根节点,代表的区间是整个统计范围,比如 $[1, n]$。 +3. 线段树的每个叶子节点都代表一个长度为 $1$ 的单位区间 $[x, x]$。 +4. 对于每个内部节点 $[left, right]$,它的左子节点是 $[left, mid]$,右子节点是 $[mid + 1, right]$。其中 $mid = (left + right) / 2$(向下取整)。 ## 2. 线段树的构建 @@ -29,22 +29,22 @@ 我们可以采用与完全二叉树类似的编号方法来对线段树进行编号,方法如下: -- 根节点的编号为 `0`。 -- 如果某二叉树节点(非叶子节点)的下标为 `i`,那么其左孩子节点下标为 `2 * i + 1`,右孩子节点下标为 `2 * i + 2`。 -- 如果某二叉树节点(非根节点)的下标为 `i`,那么其父节点下标为 `(i - 1) // 2`,`//` 表示整除。 +- 根节点的编号为 $0$。 +- 如果某二叉树节点(非叶子节点)的下标为 $i$,那么其左孩子节点下标为 $2 \times i + 1$,右孩子节点下标为 $2 \times i + 2$。 +- 如果某二叉树节点(非根节点)的下标为 $i$,那么其父节点下标为 $(i - 1) // 2$,$//$ 表示整除。 这样我们就能使用一个数组来保存线段树。那么这个数组的大小应该设置为多少才合适? -- 在理想情况下,`n` 个单位区间构成的线段树是一棵满二叉树,节点数为 $n + n/2 + n/4 + ... + 2 + 1 = 2 * n - 1$ 个。 因为 $2 * n - 1 < 2 * n$,所以在理想情况下,只需要使用一个大小为 $2 * n$ 的数组来存储线段树就足够了。 -- 但是在一般情况下,有些区间元素需要开辟新的一层来存储元素。线段树的深度为 $\lceil log_2n \rceil$,最坏情况下叶子节点(包括无用的节点)的数量为 $2^{\lceil log_2n \rceil}$ 个,总节点数为 $2^{\lceil log_2n \rceil + 1} - 1$ 个,可以近似看做是 $4 * n$,所以我们可以使用一个大小为 $4 * n$ 的数组来存储线段树。 +- 在理想情况下,$n$ 个单位区间构成的线段树是一棵满二叉树,节点数为 $n + n/2 + n/4 + ... + 2 + 1 = 2 \times n - 1$ 个。 因为 $2 \times n - 1 < 2 \times n$,所以在理想情况下,只需要使用一个大小为 $2 \times n$ 的数组来存储线段树就足够了。 +- 但是在一般情况下,有些区间元素需要开辟新的一层来存储元素。线段树的深度为 $\lceil \log_2n \rceil$,最坏情况下叶子节点(包括无用的节点)的数量为 $2^{\lceil \log_2n \rceil}$ 个,总节点数为 $2^{\lceil \log_2n \rceil + 1} - 1$ 个,可以近似看做是 $4 * n$,所以我们可以使用一个大小为 $4 \times n$ 的数组来存储线段树。 ### 2.2 线段树的构建方法 -![线段树父子节点下标关系](https://qcdn.itcharge.cn/images/20220303131328.png) +![线段树父子节点下标关系](https://qcdn.itcharge.cn/images/20240511173417.png) -通过上图可知:下标为 `i` 的节点的孩子节点下标为 `2 * i + 1` 和 `2 * i + 2`。所以线段树十分适合采用递归的方法来创建。具体步骤如下: +通过上图可知:下标为 $i$ 的节点的孩子节点下标为 $2 \times i + 1$ 和 $2 \times i + 2$。所以线段树十分适合采用递归的方法来创建。具体步骤如下: -1. 如果是叶子节点(`left == right`),则节点的值就是对应位置的元素值。 +1. 如果是叶子节点($left == right$),则节点的值就是对应位置的元素值。 2. 如果是非叶子节点,则递归创建左子树和右子树。 3. 节点的区间值(区间和、区间最大值、区间最小值)等于该节点左右子节点元素值的对应计算结果。 @@ -100,11 +100,11 @@ class SegmentTree: ### 3.1 线段树的单点更新 -> **线段树的单点更新**:修改一个元素的值,例如将 `nums[i]` 修改为 `val`。 +> **线段树的单点更新**:修改一个元素的值,例如将 $nums[i]$ 修改为 $val$。 我们可以采用递归的方式进行单点更新,具体步骤如下: -1. 如果是叶子节点,满足 `left == right`,则更新该节点的值。 +1. 如果是叶子节点,满足 $left == right$,则更新该节点的值。 2. 如果是非叶子节点,则判断应该在左子树中更新,还是应该在右子树中更新。 3. 在对应的左子树或右子树中更新节点值。 4. 左右子树更新返回之后,向上更新节点的区间值(区间和、区间最大值、区间最小值等),区间值等于该节点左右子节点元素值的聚合计算结果。 @@ -135,15 +135,15 @@ class SegmentTree: ### 3.2 线段树的区间查询 -> **线段树的区间查询**:查询一个区间为 `[q_left, q_right]` 的区间值。 +> **线段树的区间查询**:查询一个区间为 $[q\underline{\hspace{0.5em}}left, q\underline{\hspace{0.5em}}right]$ 的区间值。 我们可以采用递归的方式进行区间查询,具体步骤如下: -1. 如果区间 `[q_left, q_right]` 完全覆盖了当前节点所在区间 `[left, right]` ,即 `left >= q_left` 并且 `right <= q_right`,则返回该节点的区间值。 -2. 如果区间 `[q_left, q_right]` 与当前节点所在区间 `[left, right]` 毫无关系,即 `right < q_left` 或者 `left > q_right`,则返回 `0`。 -3. 如果区间 `[q_left, q_right]` 与当前节点所在区间有交集,则: - 1. 如果区间 `[q_left, q_right]` 与左子节点所在区间 `[left, mid]` 有交集,即 `q_left <= mid`,则在当前节点的左子树中进行查询并保存查询结果 `res_left`。 - 2. 如果区间 `[q_left, q_right]` 与右子节点所在区间 `[mid + 1, right]` 有交集,即 `q_right > mid`,则在当前节点的右子树中进行查询并保存查询结果 `res_right`。 +1. 如果区间 $[q\underline{\hspace{0.5em}}left, q\underline{\hspace{0.5em}}right]$ 完全覆盖了当前节点所在区间 $[left, right]$ ,即 $left \ge q\underline{\hspace{0.5em}}left$ 并且 $right \le q\underline{\hspace{0.5em}}right$,则返回该节点的区间值。 +2. 如果区间 $[q\underline{\hspace{0.5em}}left, q\underline{\hspace{0.5em}}right]$ 与当前节点所在区间 $[left, right]$ 毫无关系,即 $right < q\underline{\hspace{0.5em}}left$ 或者 $left > q\underline{\hspace{0.5em}}right$,则返回 $0$。 +3. 如果区间 $[q\underline{\hspace{0.5em}}left, q\underline{\hspace{0.5em}}right]$ 与当前节点所在区间有交集,则: + 1. 如果区间 $[q\underline{\hspace{0.5em}}left, q\underline{\hspace{0.5em}}right]$ 与左子节点所在区间 $[left, mid]$ 有交集,即 $q\underline{\hspace{0.5em}}left \le mid$,则在当前节点的左子树中进行查询并保存查询结果 $res_\underline{\hspace{0.5em}}left$。 + 2. 如果区间 $[q\underline{\hspace{0.5em}}left, q\underline{\hspace{0.5em}}right]$ 与右子节点所在区间 $[mid + 1, right]$ 有交集,即 $q\underline{\hspace{0.5em}}right > mid$,则在当前节点的右子树中进行查询并保存查询结果 $res_\underline{\hspace{0.5em}}right$。 3. 最后返回左右子树元素区间值的聚合计算结果。 线段树的区间查询代码如下: @@ -176,37 +176,37 @@ class SegmentTree: ### 3.3 线段树的区间更新 -> **线段树的区间更新**:对 `[q_left, q_right]` 区间进行更新,例如将 `[q_left, q_right]` 区间内所有元素都更新为 `val`。 +> **线段树的区间更新**:对 $[q\underline{\hspace{0.5em}}left, q\underline{\hspace{0.5em}}right]$ 区间进行更新,例如将 $[q\underline{\hspace{0.5em}}left, q\underline{\hspace{0.5em}}right]$ 区间内所有元素都更新为 $val$。 #### 3.3.1 延迟标记 -线段树在进行单点更新、区间查询时,区间 `[q_left, q_right]` 在线段树上会被分成 $O(log_2n)$ 个小区间(节点),从而在 $O(log_2n)$ 的时间复杂度内完成操作。 +线段树在进行单点更新、区间查询时,区间 $[q\underline{\hspace{0.5em}}left, q\underline{\hspace{0.5em}}right]$ 在线段树上会被分成 $O(\log_2n)$ 个小区间(节点),从而在 $O(\log_2n)$ 的时间复杂度内完成操作。 -而在「区间更新」操作中,如果某个节点区间 `[left, right]` 被修改区间 `[q_left, q_right]` 完全覆盖,则以该节点为根的整棵子树中所有节点的区间值都要发生变化,如果逐一进行更新的话,将使得一次区间更新操作的时间复杂度增加到 $O(n)$。 +而在「区间更新」操作中,如果某个节点区间 $[left, right]$ 被修改区间 $[q\underline{\hspace{0.5em}}left, q\underline{\hspace{0.5em}}right]$ 完全覆盖,则以该节点为根的整棵子树中所有节点的区间值都要发生变化,如果逐一进行更新的话,将使得一次区间更新操作的时间复杂度增加到 $O(n)$。 -设想这一种情况:如果我们在一次执行更新操作时,发现当前节点区间 `[left, right]` 被修改区间 `[q_left, q_right]` 完全覆盖,然后逐一更新了区间 `[left, right]` 对应子树中的所有节点,但是在后续的区间查询操作中却根本没有用到 `[left, right]` 作为候选答案,则更新 `[left, right]` 对应子树的工作就是徒劳的。 +设想这一种情况:如果我们在一次执行更新操作时,发现当前节点区间 $[left, right]$ 被修改区间 $[q\underline{\hspace{0.5em}}left, q\underline{\hspace{0.5em}}right]$ 完全覆盖,然后逐一更新了区间 $[left, right]$ 对应子树中的所有节点,但是在后续的区间查询操作中却根本没有用到 $[left, right]$ 作为候选答案,则更新 $[left, right]$ 对应子树的工作就是徒劳的。 如果我们减少更新的次数和时间复杂度,应该怎么办? -我们可以向线段树的节点类中增加一个 **「延迟标记」**,标识为 **「该区间曾经被修改为 `val`,但其子节点区间值尚未更新」**。也就是说除了在进行区间更新时,将区间子节点的更新操作延迟到 **「在后续操作中递归进入子节点时」** 再执行。这样一来,每次区间更新和区间查询的时间复杂度都降低到了 $O(log_2n)$。 +我们可以向线段树的节点类中增加一个 **「延迟标记」**,标识为 **「该区间曾经被修改为 $val$,但其子节点区间值尚未更新」**。也就是说除了在进行区间更新时,将区间子节点的更新操作延迟到 **「在后续操作中递归进入子节点时」** 再执行。这样一来,每次区间更新和区间查询的时间复杂度都降低到了 $O(\log_2n)$。 使用「延迟标记」的区间更新步骤为: -1. 如果区间 `[q_left, q_right]` 完全覆盖了当前节点所在区间 `[left, right]` ,即 `left >= q_left` 并且 `right <= q_right`,则更新当前节点所在区间的值,并将当前节点的延迟标记为区间值。 -2. 如果区间 `[q_left, q_right]` 与当前节点所在区间 `[left, right]` 毫无关系,即 `right < q_left` 或者 `left > q_right`,则直接返回。 -3. 如果区间 `[q_left, q_right]` 与当前节点所在区间有交集,则: - 1. 如果当前节点使用了「延迟标记」,即延迟标记不为 `None`,则将当前区间的更新操作应用到该节点的子节点上(即向下更新)。 - 2. 如果区间 `[q_left, q_right]` 与左子节点所在区间 `[left, mid]` 有交集,即 `q_left <= mid`,则在当前节点的左子树中更新区间值。 - 3. 如果区间 `[q_left, q_right]` 与右子节点所在区间 `[mid + 1, right]` 有交集,即 `q_right > mid`,则在当前节点的右子树中更新区间值。 +1. 如果区间 $[q\underline{\hspace{0.5em}}left, q\underline{\hspace{0.5em}}right]$ 完全覆盖了当前节点所在区间 $[left, right]$ ,即 $left \ge q\underline{\hspace{0.5em}}left$ 并且 $right \le q\underline{\hspace{0.5em}}right$,则更新当前节点所在区间的值,并将当前节点的延迟标记为区间值。 +2. 如果区间 $[q\underline{\hspace{0.5em}}left, q\underline{\hspace{0.5em}}right]$ 与当前节点所在区间 $[left, right]$ 毫无关系,即 $right < q\underline{\hspace{0.5em}}left$ 或者 $left > q\underline{\hspace{0.5em}}right$,则直接返回。 +3. 如果区间 $[q\underline{\hspace{0.5em}}left, q\underline{\hspace{0.5em}}right]$ 与当前节点所在区间有交集,则: + 1. 如果当前节点使用了「延迟标记」,即延迟标记不为 $None$,则将当前区间的更新操作应用到该节点的子节点上(即向下更新)。 + 2. 如果区间 $[q\underline{\hspace{0.5em}}left, q\underline{\hspace{0.5em}}right]$ 与左子节点所在区间 $[left, mid]$ 有交集,即 $q\underline{\hspace{0.5em}}left \le mid$,则在当前节点的左子树中更新区间值。 + 3. 如果区间 $[q\underline{\hspace{0.5em}}left, q\underline{\hspace{0.5em}}right]$ 与右子节点所在区间 $[mid + 1, right]$ 有交集,即 $q\underline{\hspace{0.5em}}right > mid$,则在当前节点的右子树中更新区间值。 4. 左右子树更新返回之后,向上更新节点的区间值(区间和、区间最大值、区间最小值),区间值等于该节点左右子节点元素值的对应计算结果。 #### 3.3.2 向下更新 -上面提到了如果当前节点使用了「延迟标记」,即延迟标记不为 `None`,则将当前区间的更新操作应用到该节点的子节点上(即向下更新)。这里描述一下向下更新的具体步骤: +上面提到了如果当前节点使用了「延迟标记」,即延迟标记不为 $None$,则将当前区间的更新操作应用到该节点的子节点上(即向下更新)。这里描述一下向下更新的具体步骤: -1. 更新左子节点值和左子节点懒惰标记为 `val`。 -2. 更新右子节点值和右子节点懒惰标记为 `val`。 -3. 将当前节点的懒惰标记更新为 `None`。 +1. 更新左子节点值和左子节点懒惰标记为 $val$。 +2. 更新右子节点值和右子节点懒惰标记为 $val$。 +3. 将当前节点的懒惰标记更新为 $None$。 使用「延迟标记」的区间更新实现代码如下: @@ -258,9 +258,9 @@ class SegmentTree: self.tree[index].lazy_tag = None # 更新当前节点的懒惰标记 ``` -> **注意**:有些题目中不是将 `[q_left, q_right]` 区间更新为 `val`,而是将 `[q_left, q_right]` 区间中每一个元素值在原值基础增加或减去 `val`。 +> **注意**:有些题目中不是将 $[q\underline{\hspace{0.5em}}left, q\underline{\hspace{0.5em}}right]$ 区间更新为 $val$,而是将 $[q\underline{\hspace{0.5em}}left, q\underline{\hspace{0.5em}}right]$ 区间中每一个元素值在原值基础增加或减去 $val$。 > -> 对于这种情况,我们可以更改一下「延迟标记」的定义。改变为: **「该区间曾经变化了 `val`,但其子节点区间值尚未更新」**。并更改对应的代码逻辑。 +> 对于这种情况,我们可以更改一下「延迟标记」的定义。改变为: **「该区间曾经变化了 $val$,但其子节点区间值尚未更新」**。并更改对应的代码逻辑。 使用「延迟标记」的区间增减更新实现代码如下: @@ -329,16 +329,16 @@ class SegmentTree: ### 4.1 RMQ 问题 -> **RMQ 问题**:Range Maximum / Minimum Query 的缩写,指的是对于长度为 `n` 的数组序列 `nums`,回答若干个询问问题 `RMQ(nums, q_left, q_right)`,要求返回数组序列 `nums` 在区间 `[q_left, q_right]` 中的最大(最小)值。也就是求区间最大(最小)值问题。 +> **RMQ 问题**:Range Maximum / Minimum Query 的缩写,指的是对于长度为 $n$ 的数组序列 $nums$,回答若干个询问问题 `RMQ(nums, q_left, q_right)`,要求返回数组序列 $nums$ 在区间 $[q\underline{\hspace{0.5em}}left, q\underline{\hspace{0.5em}}right]$ 中的最大(最小)值。也就是求区间最大(最小)值问题。 -假设查询次数为 `q`,则使用朴素算法解决 RMQ 问题的时间复杂度为 $O(q * n)$。而使用线段树解决 RMQ 问题的时间复杂度为 $O(q * n)$ ~ $Q(q * log_2n)$ 之间。 +假设查询次数为 $q$,则使用朴素算法解决 RMQ 问题的时间复杂度为 $O(q \times n)$。而使用线段树解决 RMQ 问题的时间复杂度为 $O(q \times n) \sim Q(q \times \log_2n)$ 之间。 ### 4.2 单点更新,区间查询问题 > **单点更新,区间查询问题**: > > 1. 修改某一个元素的值。 -> 2. 查询区间为 `[q_left, q_right]` 的区间值。 +> 2. 查询区间为 $[q\underline{\hspace{0.5em}}left, q\underline{\hspace{0.5em}}right]$ 的区间值。 这类问题直接使用「3.1 线段树的单点更新」和「3.2 线段树的区间查询」即可解决。 @@ -347,7 +347,7 @@ class SegmentTree: > **区间更新,区间查询问题**: > > 1. 修改某一个区间的值。 -> 2. 查询区间为 `[q_left, q_right]` 的区间值。 +> 2. 查询区间为 $[q\underline{\hspace{0.5em}}left, q\underline{\hspace{0.5em}}right]$ 的区间值。 这类问题直接使用「3.3 线段树的区间更新」和「3.2 线段树的区间查询」即可解决。 @@ -356,7 +356,7 @@ class SegmentTree: > **区间合并,区间查询问题**: > > 1. 修改某一个区间的值。 -> 2. 查询区间为 `[q_left, q_right]` 中满足条件的连续最长区间值。 +> 2. 查询区间为 $[q\underline{\hspace{0.5em}}left, q\underline{\hspace{0.5em}}right]$ 中满足条件的连续最长区间值。 这类问题需要在「3.3 线段树的区间更新」和「3.2 线段树的区间查询」的基础上增加变动,在进行向上更新时需要对左右子节点的区间进行合并。 @@ -366,7 +366,7 @@ class SegmentTree: > > 主要思想为:想象一条线(通常是一条垂直线)在平面上扫过或移动,在某些点停止。几何操作仅限于几何对象,无论何时停止,它们都与扫描线相交或紧邻扫描线,并且一旦线穿过所有对象,就可以获得完整的解。 -这类问题通常坐标跨度很大,需要先对每条扫描线的坐标进行离散化处理,将 `y` 坐标映射到 `0, 1, 2, ...` 中。然后将每条竖线的端点作为区间范围,使用线段树存储每条竖线的信息(`x` 坐标、是左竖线还是右竖线等),然后再进行区间合并,并统计相关信息。 +这类问题通常坐标跨度很大,需要先对每条扫描线的坐标进行离散化处理,将 $y$ 坐标映射到 $0, 1, 2, ...$ 中。然后将每条竖线的端点作为区间范围,使用线段树存储每条竖线的信息($x$ 坐标、是左竖线还是右竖线等),然后再进行区间合并,并统计相关信息。 ## 5. 线段树的拓展 @@ -374,7 +374,7 @@ class SegmentTree: 在有些情况下,线段树需要维护的区间很大(例如 $[1, 10^9]$),在实际中用到的节点却很少。 -如果使用之前数组形式实现线段树,则需要 $4 * n$ 大小的空间,空间消耗有点过大了。 +如果使用之前数组形式实现线段树,则需要 $4 \times n$ 大小的空间,空间消耗有点过大了。 这时候我们就可以使用动态开点的思想来构建线段树。