diff --git a/Contents/02.Linked-List/01.Linked-List-Basic/01.Linked-List-Basic.md b/Contents/02.Linked-List/01.Linked-List-Basic/01.Linked-List-Basic.md index 84ad3f7f..3f71376d 100644 --- a/Contents/02.Linked-List/01.Linked-List-Basic/01.Linked-List-Basic.md +++ b/Contents/02.Linked-List/01.Linked-List-Basic/01.Linked-List-Basic.md @@ -73,18 +73,20 @@ class LinkedList: > **建立一个线性链表**:根据线性表的数据元素动态生成链节点,并依次将其连接到链表中。 > -> 1. 从所给线性表的第 $1$ 个数据元素开始依次获取表中的数据元素。 +> 1. 从所给线性表中取出第 $1$ 个数据元素,建立链表头节点。然后依次获取表中的数据元素。 > 2. 每获取一个数据元素,就为该数据元素生成一个新节点,将新节点插入到链表的尾部。 -> 3. 插入完毕之后返回第 $1$ 个链节点的地址。 +> 3. 插入完毕之后返回第 $1$ 个链节点(即头节点)的地址。 **「建立一个线性链表」** 的代码如下: ```python # 根据 data 初始化一个新链表 def create(self, data): - self.head = ListNode(0) + if not data: + return + self.head = ListNode(data[0]) cur = self.head - for i in range(len(data)): + for i in range(1, len(data)): node = ListNode(data[i]) cur.next = node cur = cur.next diff --git a/Contents/07.Tree/05.Union-Find/01.Union-Find.md b/Contents/07.Tree/05.Union-Find/01.Union-Find.md index 238ff57a..df417128 100644 --- a/Contents/07.Tree/05.Union-Find/01.Union-Find.md +++ b/Contents/07.Tree/05.Union-Find/01.Union-Find.md @@ -38,17 +38,21 @@ 在使用「快速查询」思路实现并查集时,我们可以使用一个「数组结构」来表示集合中的元素。数组元素和集合元素是一一对应的,我们可以将数组的索引值作为每个元素的集合编号,称为 $id$。然后可以对数组进行以下操作来实现并查集: -- **当初始化时**:将每个元素的集合编号初始化为数组下标索引。则所有元素的 $id$ 都是唯一的,代表着每个元素单独属于一个集合。 +- **当初始化时**:将数组下标索引值作为每个元素的集合编号。所有元素的 $id$ 都是唯一的,代表着每个元素单独属于一个集合。 - **合并操作时**:需要将其中一个集合中的所有元素 $id$ 更改为另一个集合中的 $id$,这样能够保证在合并后一个集合中所有元素的 $id$ 均相同。 - **查找操作时**:如果两个元素的 $id$ 一样,则说明它们属于同一个集合;如果两个元素的 $id$ 不一样,则说明它们不属于同一个集合。 -举个例子来说明一下,我们使用数组来表示一系列集合元素 $\left\{ 0 \right\}, \left\{ 1 \right\}, \left\{ 2 \right\}, \left\{ 3 \right\}, \left\{ 4 \right\}, \left\{ 5 \right\}, \left\{ 6 \right\}, \left\{ 7 \right\}$,初始化时如下图所示。从下图中可以看出:元素的集合编号就是数组的索引值,代表着每个元素属于一个集合。 +举个例子来说明一下,我们使用数组来表示一系列集合元素 $\left\{ 0 \right\}, \left\{ 1 \right\}, \left\{ 2 \right\}, \left\{ 3 \right\}, \left\{ 4 \right\}, \left\{ 5 \right\}, \left\{ 6 \right\}, \left\{ 7 \right\}$,初始化时如下图所示。 -![](https://qcdn.itcharge.cn/images/20220505145234.png) +![基于数组实现:初始化操作](https://qcdn.itcharge.cn/images/20240513150949.png) -当我们进行一系列的合并操作后,比如合并后变为 $\left\{ 0 \right\}, \left\{ 1, 2, 3 \right\}, \left\{ 4 \right\}, \left\{5, 6\right\}, \left\{ 7 \right\}$,合并操作的结果如下图所示。从图中可以看出,在进行一系列合并操作后,下标为 $1$、$2$、$3$ 的元素集合编号是一致的,说明这 $3$ 个 元素同属于一个集合。同理下标为 $5$ 和 $6$ 的元素则同属于另一个集合。 +从上图中可以看出:数组的每个下标索引值对应一个元素的集合编号,代表着每个元素单独属于一个集合。 -![](https://qcdn.itcharge.cn/images/20220505145302.png) +当我们进行一系列的合并操作后,比如合并后变为 $\left\{ 0 \right\}, \left\{ 1, 2, 3 \right\}, \left\{ 4 \right\}, \left\{5, 6\right\}, \left\{ 7 \right\}$,合并操作的结果如下图所示。 + +![基于数组实现:合并操作](https://qcdn.itcharge.cn/images/20240513151310.png) + +从上图中可以看出,在进行一系列合并操作后,下标为 $1$、$2$、$3$ 的元素集合编号是一致的,说明这 $3$ 个元素同属于一个集合。同理下标为 $5$ 和 $6$ 的元素则同属于另一个集合。 在快速查询的实现思路中,单次查询操作的时间复杂度是 $O(1)$,而单次合并操作的时间复杂度为 $O(n)$(每次合并操作需要遍历数组)。两者的时间复杂度相差得比较大,完全牺牲了合并操作的性能。因此,这种并查集的实现思路并不常用。 @@ -92,17 +96,41 @@ class UnionFind: 总结一下,我们可以对数组 $fa$ 进行以下操作来实现并查集: -- **当初始化时**:将每个元素的集合编号初始化为数组 $fa$ 的下标索引。所有元素的根节点的集合编号不一样,代表着每个元素单独属于一个集合。 +- **当初始化时**:将数组 $fa$​ 的下标索引作为每个元素的集合编号。所有元素的根节点的集合编号都不一样,代表着每个元素单独属于一个集合。 - **合并操作时**:需要将两个集合的树根节点相连接。即令其中一个集合的树根节点指向另一个集合的树根节点(`fa[root1] = root2`),这样合并后当前集合中的所有元素的树根节点均为同一个。 - **查找操作时**:分别从两个元素开始,通过数组 $fa$ 存储的值,不断递归访问元素的父节点,直到到达树根节点。如果两个元素的树根节点一样,则说明它们属于同一个集合;如果两个元素的树根节点不一样,则说明它们不属于同一个集合。 -举个例子来说明一下,我们使用数组来表示一系列集合元素 $\left\{0\right\}, \left\{ 1 \right\}, \left\{ 2 \right\}, \left\{ 3 \right\}, \left\{ 4 \right\}, \left\{ 5 \right\}, \left\{ 6 \right\}, \left\{ 7 \right\}$,初始化时如下图所示。从下图中可以看出:元素的集合编号就是数组 $fa$ 的索引值,代表着每个元素属于一个集合。 +举个例子来说明一下,我们使用数组来表示一系列集合元素 $\left\{0\right\}, \left\{ 1 \right\}, \left\{ 2 \right\}, \left\{ 3 \right\}, \left\{ 4 \right\}, \left\{ 5 \right\}, \left\{ 6 \right\}, \left\{ 7 \right\}$,初始化时如下图所示。 + +![基于森林实现:初始化操作](https://qcdn.itcharge.cn/images/20240513151548.png) + +从上图中可以看出:$fa$ 数组的每个下标索引值对应一个元素的集合编号,代表着每个元素属于一个集合。 + +当我们进行一系列的合并操作后,比如 `union(4, 5)`、`union(6, 7)`、`union(4, 7)` 操作后变为 $\left\{ 0 \right\}, \left\{ 1 \right\}, \left\{ 2 \right\}, \left\{ 3 \right\}, \left\{ 4, 5, 6, 7 \right\}$​,合并操作的步骤及结果如下图所示。 + +::: tabs#union + +@tab <1> + +- 合并 $(4, 5)$:令 $4$ 的根节点指向 $5$,即将 $fa[4]$ 更改为 $5$。 + +![基于森林实现:合并操作 1](https://qcdn.itcharge.cn/images/20240513154015.png) + +@tab <2> + +- 合并 $(6, 7)$:令 $6$ 的根节点指向 $7$,即将 $fa[6]$ 更改为 $7$。 + +![基于森林实现:合并操作 2](https://qcdn.itcharge.cn/images/20240513154022.png) + +@tab <3> + +- 合并 $(4, 7)$:令 $4$ 的的根节点指向 $7$,即将 $fa[fa[4]]$(也就是 $fa[5]$)更改为 $7$。 -![](https://qcdn.itcharge.cn/images/20220507112934.png) +![基于森林实现:合并操作 3](https://qcdn.itcharge.cn/images/20240513154030.png) -当我们进行一系列的合并操作后,比如 `union(4, 5)`、`union(6, 7)`、`union(4, 7)` 操作后变为 $\left\{ 0 \right\}, \left\{ 1 \right\}, \left\{ 2 \right\}, \left\{ 3 \right\}, \left\{ 4, 5, 6, 7 \right\}$,合并操作的步骤及结果如下图所示。从图中可以看出,在进行一系列合并操作后,`fa[4] == fa[5] == fa[6] == fa[fa[7]]`,即 $4$、$5$、$6$、$7$ 的元素根节点编号都是 $4$,说明这 $4$ 个 元素同属于一个集合。 +::: -![](https://qcdn.itcharge.cn/images/20220507142647.png) +从上图中可以看出,在进行一系列合并操作后,`fa[fa[4]] == fa[5] == fa[6] == f[7]`,即 $4$、$5$、$6$、$7$ 的元素根节点编号都是 $4$,说明这 $4$ 个元素同属于一个集合。 - 使用「快速合并」思路实现并查集代码如下所示: @@ -132,7 +160,7 @@ class UnionFind: 在集合很大或者树很不平衡时,使用上述「快速合并」思路实现并查集的代码效率很差,最坏情况下,树会退化成一条链,单次查询的时间复杂度高达 $O(n)$。并查集的最坏情况如下图所示。 -![](https://qcdn.itcharge.cn/images/20220507172300.png) +![并查集最坏情况](https://qcdn.itcharge.cn/images/20240513154732.png) 为了避免出现最坏情况,一个常见的优化方式是「路径压缩」。 @@ -146,7 +174,7 @@ class UnionFind: 下面是一个「隔代压缩」的例子。 -![](https://qcdn.itcharge.cn/images/20220509113954.png) +![路径压缩:隔代压缩](https://qcdn.itcharge.cn/images/20240513154745.png) - 隔代压缩的查找代码如下: @@ -164,7 +192,7 @@ def find(self, x): # 查找元素根节点的集合 相比较于「隔代压缩」,「完全压缩」压缩的更加彻底。下面是一个「完全压缩」的例子。 -![](https://qcdn.itcharge.cn/images/20220507174723.png) +![路径压缩:完全压缩](https://qcdn.itcharge.cn/images/20240513154759.png) - 完全压缩的查找代码如下: @@ -195,7 +223,7 @@ def find(self, x): # 查找元素根节点的集合 下面是一个「按深度合并」的例子。 -![](https://qcdn.itcharge.cn/images/20220509094655.png) +![按秩合并:按深度合并](https://qcdn.itcharge.cn/images/20240513154814.png) - 按深度合并的实现代码如下: @@ -240,7 +268,7 @@ class UnionFind: 下面是一个「按大小合并」的例子。 -![](https://qcdn.itcharge.cn/images/20220509094634.png) +![按秩合并:按大小合并](https://qcdn.itcharge.cn/images/20240513154835.png) - 按大小合并的实现代码如下: diff --git a/Contents/09.Algorithm-Base/02.Recursive-Algorithm/01.Recursive-Algorithm.md b/Contents/09.Algorithm-Base/02.Recursive-Algorithm/01.Recursive-Algorithm.md index bf69f8d8..16ef0dfb 100644 --- a/Contents/09.Algorithm-Base/02.Recursive-Algorithm/01.Recursive-Algorithm.md +++ b/Contents/09.Algorithm-Base/02.Recursive-Algorithm/01.Recursive-Algorithm.md @@ -54,9 +54,9 @@ fact(6) 这两个部分也可以叫做「递推过程」和「回归过程」,如下面两幅图所示: -![](https://qcdn.itcharge.cn/images/20220407160648.png) +![递推过程](https://qcdn.itcharge.cn/images/20220407160648.png) -![](https://qcdn.itcharge.cn/images/20220407160659.png) +![回归过程](https://qcdn.itcharge.cn/images/20220407160659.png) 如上面所说,我们可以把「递归」分为两个部分:「递推过程」和「回归过程」。 @@ -175,7 +175,7 @@ $f(n) = \begin{cases} 0 & n = 0 \cr 1 & n = 1 \cr f(n - 2) + f(n - 1) & n > 1 \e 其对应的递归过程如下图所示: -![](https://qcdn.itcharge.cn/images/20230307164107.png) +![斐波那契数列的递归过程](https://qcdn.itcharge.cn/images/20230307164107.png) 从图中可以看出:想要计算 $f(5)$,需要先计算 $f(3)$ 和 $f(4)$,而在计算 $f(4)$ 时还需要计算 $f(3)$,这样 $f(3)$ 就进行了多次计算。同理 $f(0)$、$f(1)$、$f(2)$ 都进行了多次计算,就导致了重复计算问题。 diff --git a/Contents/09.Algorithm-Base/03.Divide-And-Conquer-Algorithm/01.Divide-And-Conquer-Algorithm.md b/Contents/09.Algorithm-Base/03.Divide-And-Conquer-Algorithm/01.Divide-And-Conquer-Algorithm.md index 916725c4..d3807f8d 100644 --- a/Contents/09.Algorithm-Base/03.Divide-And-Conquer-Algorithm/01.Divide-And-Conquer-Algorithm.md +++ b/Contents/09.Algorithm-Base/03.Divide-And-Conquer-Algorithm/01.Divide-And-Conquer-Algorithm.md @@ -6,7 +6,7 @@ 简单来说,分治算法的基本思想就是: **把规模大的问题不断分解为子问题,使得问题规模减小到可以直接求解为止。** -![](https://qcdn.itcharge.cn/images/20220413153059.png) +![分治算法的基本思想](https://qcdn.itcharge.cn/images/20220413153059.png) ### 1.2 分治算法和递归算法的异同 @@ -16,7 +16,7 @@ 分治算法从实现方式上来划分,可以分为两种:「递归算法」和「迭代算法」。 -![](https://qcdn.itcharge.cn/images/20220414093828.png) +![分治算法的实现方式](https://qcdn.itcharge.cn/images/20240513162133.png) 一般情况下,分治算法比较适合使用递归算法来实现。但除了递归算法之外,分治算法还可以通过迭代算法来实现。比较常见的例子有:快速傅里叶变换算法、二分查找算法、非递归实现的归并排序算法等等。 @@ -68,7 +68,7 @@ def divide_and_conquer(problems_n): # problems_n 为问题规模 一般来讲,分治算法将一个问题划分为 $a$ 个形式相同的子问题,每个子问题的规模为 $n/b$,则总的时间复杂度的递归表达式可以表示为: -$T(n) = \begin{cases} \Theta{(1)} & n = 1 \cr a \times T(n/b) + f(n) & n > 1 \end{cases}$ +$T(n) = \begin{cases} \Theta{(1)} & n = 1 \cr a \times T(n/b) + f(n) & n > 1 \end{cases}$ 其中,每次分解时产生的子问题个数是 $a$ ,每个子问题的规模是原问题规模的 $1 / b$,分解和合并 $a$ 个子问题的时间复杂度是 $f(n)$。 @@ -110,9 +110,9 @@ $T(n) = \begin{cases} O{(1)} & n = 1 \cr 2T(n/2) + O(n) & n > 1 \end{cases}$ 其对应的递归树如下图所示。 -![](https://qcdn.itcharge.cn/images/20220414171458.png) +![归并排序算法的递归树](https://qcdn.itcharge.cn/images/20220414171458.png) -因为 $n = 2^x$,则 $x = \log_2n$,则归并排序算法的时间复杂度为:$2^x \times T(1) + x \times O(n) = n + \log_2n \times O(n) = O(n \times log_2n)$。 +因为 $n = 2^x$,则 $x = \log_2n$,则归并排序算法的时间复杂度为:$2^x \times T(1) + x \times O(n) = n + \log_2n \times O(n) = O(n \times \log_2n)$。 ## 4. 分治算法的应用 @@ -150,7 +150,7 @@ $T(n) = \begin{cases} O{(1)} & n = 1 \cr 2T(n/2) + O(n) & n > 1 \end{cases}$ 使用归并排序算法对数组排序的过程如下图所示。 -![](https://qcdn.itcharge.cn/images/20220414204405.png) +![归并排序算法对数组排序的过程](https://qcdn.itcharge.cn/images/20220414204405.png) #### 4.1.4 代码 @@ -221,7 +221,7 @@ class Solution: 二分查找的的分治算法过程如下图所示。 -![](https://qcdn.itcharge.cn/images/20211223115032.png) +![二分查找的的分治算法过程](https://qcdn.itcharge.cn/images/20211223115032.png) #### 4.2.4 代码 diff --git a/Contents/09.Algorithm-Base/04.Backtracking-Algorithm/01.Backtracking-Algorithm.md b/Contents/09.Algorithm-Base/04.Backtracking-Algorithm/01.Backtracking-Algorithm.md index 7c3c1001..8a9dde5e 100644 --- a/Contents/09.Algorithm-Base/04.Backtracking-Algorithm/01.Backtracking-Algorithm.md +++ b/Contents/09.Algorithm-Base/04.Backtracking-Algorithm/01.Backtracking-Algorithm.md @@ -33,7 +33,7 @@ 对于上述决策过程,我们也可以用一棵决策树来表示: -![](https://qcdn.itcharge.cn/images/20220425102048.png) +![全排列问题的决策树](https://qcdn.itcharge.cn/images/20220425102048.png) 从全排列的决策树中我们可以看出: @@ -205,7 +205,7 @@ for i in range(len(nums)): # 枚举可选元素列表 1. **明确所有选择**:根据数组中每个位置上的元素选与不选两种选择,画出决策树,如下图所示。 - - ![](https://qcdn.itcharge.cn/images/20220425210640.png) + - ![子集的决策树](https://qcdn.itcharge.cn/images/20220425210640.png) 2. **明确终止条件**: @@ -303,7 +303,7 @@ class Solution: 1. **明确所有选择**:根据棋盘中当前行的所有列位置上是否选择放置皇后,画出决策树,如下图所示。 - - ![](https://qcdn.itcharge.cn/images/20220426095225.png) + - ![n 皇后问题的决策树](https://qcdn.itcharge.cn/images/20220426095225.png) 2. **明确终止条件**: diff --git a/Contents/09.Algorithm-Base/05.Greedy-Algorithm/01.Greedy-Algorithm.md b/Contents/09.Algorithm-Base/05.Greedy-Algorithm/01.Greedy-Algorithm.md index 8d46a631..d735adc7 100644 --- a/Contents/09.Algorithm-Base/05.Greedy-Algorithm/01.Greedy-Algorithm.md +++ b/Contents/09.Algorithm-Base/05.Greedy-Algorithm/01.Greedy-Algorithm.md @@ -23,7 +23,7 @@ 换句话说,当进行选择时,我们直接做出在当前问题中看来最优的选择,而不用去考虑子问题的解。在做出选择之后,才会去求解剩下的子问题,如下图所示。 -![](https://qcdn.itcharge.cn/images/20220511174939.png) +![贪心选择性质](https://qcdn.itcharge.cn/images/20240513163300.png) 贪心算法在进行选择时,可能会依赖之前做出的选择,但不会依赖任何将来的选择或是子问题的解。运用贪心算法解决的问题在程序的运行过程中无回溯过程。 @@ -37,7 +37,7 @@ 也就是说,如果原问题的最优解包含子问题的最优解,则说明该问题满足最优子结构性质。 -![](https://qcdn.itcharge.cn/images/20220511175042.png) +![最优子结构性质](https://qcdn.itcharge.cn/images/20240513163310.png) 在做了贪心选择后,满足最优子结构性质的原问题可以分解成规模更小的类似子问题来解决,并且可以通过贪心选择和子问题的最优解推导出问题的最优解。 diff --git a/Templates/02.LinkedList/LinkedList.py b/Templates/02.LinkedList/LinkedList.py index a1bdc664..a3165e86 100644 --- a/Templates/02.LinkedList/LinkedList.py +++ b/Templates/02.LinkedList/LinkedList.py @@ -9,9 +9,11 @@ def __init__(self): # 根据 data 初始化一个新链表 def create(self, data): - self.head = ListNode(0) + if not data: + return + self.head = ListNode(data[0]) cur = self.head - for i in range(len(data)): + for i in range(1, len(data)): node = ListNode(data[i]) cur.next = node cur = cur.next