Fix naming of the section
build_binary_tree_problem
|
@ -18,7 +18,7 @@ def dfs(
|
|||
l: int,
|
||||
r: int,
|
||||
) -> TreeNode | None:
|
||||
"""构建二叉树 DFS"""
|
||||
"""构建二叉树:分治"""
|
||||
# 子树区间为空时终止
|
||||
if r - l < 0:
|
||||
return None
|
||||
|
@ -26,9 +26,9 @@ def dfs(
|
|||
root = TreeNode(preorder[i])
|
||||
# 查询 m ,从而划分左右子树
|
||||
m = hmap[preorder[i]]
|
||||
# 递归构建左子树
|
||||
# 子问题:构建左子树
|
||||
root.left = dfs(preorder, inorder, hmap, i + 1, l, m - 1)
|
||||
# 递归构建右子树
|
||||
# 子问题:构建右子树
|
||||
root.right = dfs(preorder, inorder, hmap, i + 1 + m - l, m + 1, r)
|
||||
# 返回根节点
|
||||
return root
|
||||
|
|
|
@ -22,6 +22,6 @@
|
|||
|
||||
哈希表底层是数组,而为了解决哈希冲突,我们可能会使用“拉链法”(后续散列表章节会讲)。在拉链法中,数组中每个地址(桶)指向一个链表;当这个链表长度超过一定阈值时,又可能被转化为树(通常为红黑树)。因此,哈希表可能同时包含线性(数组、链表)和非线性(树)数据结构。
|
||||
|
||||
!!! question "char 类型的长度是 1 bytes 吗?"
|
||||
!!! question "char 类型的长度是 1 byte 吗?"
|
||||
|
||||
这个与编程语言采用的编码方法有关。例如,Java, JS, TS, C# 都采用 UTF-16 编码(保存 Unicode 码点),因此 char 类型的长度为 2 bytes 。
|
||||
char 类型的长度由编程语言采用的编码方法决定。例如,Java, JS, TS, C# 都采用 UTF-16 编码(保存 Unicode 码点),因此 char 类型的长度为 2 bytes 。
|
||||
|
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
@ -4,7 +4,7 @@
|
|||
|
||||
给定一个二叉树的前序遍历 `preorder` 和中序遍历 `inorder` ,请从中构建二叉树,返回二叉树的根节点。
|
||||
|
||||
![构建二叉树的示例数据](build_binary_tree.assets/build_tree_example.png)
|
||||
![构建二叉树的示例数据](build_binary_tree_problem.assets/build_tree_example.png)
|
||||
|
||||
原问题定义为从 `preorder` 和 `inorder` 构建二叉树。我们首先从分治的角度分析这道题:
|
||||
|
||||
|
@ -25,7 +25,7 @@
|
|||
2. 查找根节点在 `inorder` 中的索引,基于该索引可将 `inorder` 划分为 `[ 9 | 3 | 1 2 7 ]` ;
|
||||
3. 根据 `inorder` 划分结果,可得左子树和右子树分别有 1 个和 3 个节点,从而可将 `preorder` 划分为 `[ 3 | 9 | 2 1 7 ]` ;
|
||||
|
||||
![在前序和中序遍历中划分子树](build_binary_tree.assets/build_tree_preorder_inorder_division.png)
|
||||
![在前序和中序遍历中划分子树](build_binary_tree_problem.assets/build_tree_preorder_inorder_division.png)
|
||||
|
||||
至此,**我们已经推导出根节点、左子树、右子树在 `preorder` 和 `inorder` 中的索引区间**。而为了描述这些索引区间,我们需要借助几个指针变量:
|
||||
|
||||
|
@ -47,7 +47,7 @@
|
|||
|
||||
请注意,右子树根节点索引中的 $(m-l)$ 的含义是“左子树的节点数量”,建议配合下图理解。
|
||||
|
||||
![根节点和左右子树的索引区间表示](build_binary_tree.assets/build_tree_division_pointers.png)
|
||||
![根节点和左右子树的索引区间表示](build_binary_tree_problem.assets/build_tree_division_pointers.png)
|
||||
|
||||
接下来就可以实现代码了。为了提升查询 $m$ 的效率,我们借助一个哈希表 `hmap` 来存储 `inorder` 列表元素到索引的映射。
|
||||
|
||||
|
@ -142,34 +142,34 @@
|
|||
下图展示了构建二叉树的递归过程,各个节点是在向下“递”的过程中建立的,而各条边是在向上“归”的过程中建立的。
|
||||
|
||||
=== "<1>"
|
||||
![built_tree_step1](build_binary_tree.assets/built_tree_step1.png)
|
||||
![构建二叉树的递归过程](build_binary_tree_problem.assets/built_tree_step1.png)
|
||||
|
||||
=== "<2>"
|
||||
![built_tree_step2](build_binary_tree.assets/built_tree_step2.png)
|
||||
![built_tree_step2](build_binary_tree_problem.assets/built_tree_step2.png)
|
||||
|
||||
=== "<3>"
|
||||
![built_tree_step3](build_binary_tree.assets/built_tree_step3.png)
|
||||
![built_tree_step3](build_binary_tree_problem.assets/built_tree_step3.png)
|
||||
|
||||
=== "<4>"
|
||||
![built_tree_step4](build_binary_tree.assets/built_tree_step4.png)
|
||||
![built_tree_step4](build_binary_tree_problem.assets/built_tree_step4.png)
|
||||
|
||||
=== "<5>"
|
||||
![built_tree_step5](build_binary_tree.assets/built_tree_step5.png)
|
||||
![built_tree_step5](build_binary_tree_problem.assets/built_tree_step5.png)
|
||||
|
||||
=== "<6>"
|
||||
![built_tree_step6](build_binary_tree.assets/built_tree_step6.png)
|
||||
![built_tree_step6](build_binary_tree_problem.assets/built_tree_step6.png)
|
||||
|
||||
=== "<7>"
|
||||
![built_tree_step7](build_binary_tree.assets/built_tree_step7.png)
|
||||
![built_tree_step7](build_binary_tree_problem.assets/built_tree_step7.png)
|
||||
|
||||
=== "<8>"
|
||||
![built_tree_step8](build_binary_tree.assets/built_tree_step8.png)
|
||||
![built_tree_step8](build_binary_tree_problem.assets/built_tree_step8.png)
|
||||
|
||||
=== "<9>"
|
||||
![built_tree_step9](build_binary_tree.assets/built_tree_step9.png)
|
||||
![built_tree_step9](build_binary_tree_problem.assets/built_tree_step9.png)
|
||||
|
||||
=== "<10>"
|
||||
![built_tree_step10](build_binary_tree.assets/built_tree_step10.png)
|
||||
![built_tree_step10](build_binary_tree_problem.assets/built_tree_step10.png)
|
||||
|
||||
设树的节点数量为 $n$ ,初始化每一个节点(执行一个递归函数 `dfs()` )使用 $O(1)$ 时间。**因此总体时间复杂度为 $O(n)$** 。
|
||||
|
|
@ -476,5 +476,5 @@ $$
|
|||
总的看来,**子问题分解是一种通用的算法思路,在分治、动态规划、回溯中各有特点**:
|
||||
|
||||
- 分治算法将原问题划分为几个独立的子问题,然后递归解决子问题,最后合并子问题的解得到原问题的解。例如,归并排序将长数组不断划分为两个短子数组,再将排序好的子数组合并为排序好的长数组。
|
||||
- 动态规划也是将原问题分解为多个子问题,但与分治算法的主要区别是,**动态规划中的子问题往往不是相互独立的**,原问题的解依赖于子问题的解,而子问题的解又依赖于更小的子问题的解。因此,动态规划通常会引入记忆化,保存已经解决的子问题的解,避免重复计算。
|
||||
- 回溯算法在尝试和回退中穷举所有可能的解,并通过剪枝避免不必要的搜索分支。原问题的解由一系列决策步骤构成,我们可以将每个决策步骤之后的剩余问题看作为一个子问题。
|
||||
- 动态规划也是将原问题分解为多个子问题,但与分治算法的主要区别是,**动态规划中的子问题往往不是相互独立的**,原问题的解依赖于子问题的解,而子问题的解又依赖于更小的子问题的解。
|
||||
- 回溯算法在尝试和回退中穷举所有可能的解,并通过剪枝避免不必要的搜索分支。原问题的解由一系列决策步骤构成,我们可以将每个决策步骤之前的子序列看作为一个子问题。
|
||||
|
|
|
@ -86,7 +86,7 @@
|
|||
|
||||
## 算法特性
|
||||
|
||||
- **时间复杂度 $O(n + k)$** :假设元素在各个桶内平均分布,那么每个桶内的元素数量为 $\frac{n}{k}$ 。假设排序单个桶使用 $O(\frac{n}{k} \log\frac{n}{k})$ 时间,则排序所有桶使用 $O(n \log\frac{n}{k})$ 时间。**当桶数量 $k$ 比较大时,时间复杂度则趋向于 $O(n)$** 。合并结果时需要遍历 $n$ 个桶,花费 $O(k)$ 时间。
|
||||
- **时间复杂度 $O(n + k)$** :假设元素在各个桶内平均分布,那么每个桶内的元素数量为 $\frac{n}{k}$ 。假设排序单个桶使用 $O(\frac{n}{k} \log\frac{n}{k})$ 时间,则排序所有桶使用 $O(n \log\frac{n}{k})$ 时间。**当桶数量 $k$ 比较大时,时间复杂度则趋向于 $O(n)$** 。合并结果时需要遍历所有桶和元素,花费 $O(n + k)$ 时间。
|
||||
- **自适应排序**:在最坏情况下,所有数据被分配到一个桶中,且排序该桶使用 $O(n^2)$ 时间。
|
||||
- **空间复杂度 $O(n + k)$ 、非原地排序** :需要借助 $k$ 个桶和总共 $n$ 个元素的额外空间。
|
||||
- 桶排序是否稳定取决于排序桶内元素的算法是否稳定。
|
||||
|
|