@@ -1501,7 +1501,7 @@
< li class = "md-nav__item" >
< a href = "#4" class = "md-nav__link" >
4. 中序遍历性质
4. 中序遍历有序
< / a >
< / li >
@@ -3476,7 +3476,7 @@
< li class = "md-nav__item" >
< a href = "#4" class = "md-nav__link" >
4. 中序遍历性质
4. 中序遍历有序
< / a >
< / li >
@@ -3524,7 +3524,7 @@
< h1 id = "74" > 7.4 二叉搜索树< a class = "headerlink" href = "#74" title = "Permanent link" > ¶ < / a > < / h1 >
< p > 如图 7-16 所示,「二叉搜索树 binary search tree」满足以下条件: < / p >
< p > 如图 7-16 所示,「二叉搜索树 binary search tree」满足以下条件。 < / p >
< ol >
< li > 对于根节点,左子树中所有节点的值 < span class = "arithmatex" > \(< \)< / span > 根节点的值 < span class = "arithmatex" > \(< \)< / span > 右子树中所有节点的值。< / li >
< li > 任意节点的左、右子树也是二叉搜索树,即同样满足条件 < code > 1.< / code > 。< / li >
@@ -3535,7 +3535,7 @@
< h2 id = "741" > 7.4.1 二叉搜索树的操作< a class = "headerlink" href = "#741" title = "Permanent link" > ¶ < / a > < / h2 >
< p > 我们将二叉搜索树封装为一个类 < code > ArrayBinaryTree< / code > ,并声明一个成员变量 < code > root< / code > ,指向树的根节点。< / p >
< h3 id = "1" > 1. 查找节点< a class = "headerlink" href = "#1" title = "Permanent link" > ¶ < / a > < / h3 >
< p > 给定目标节点值 < code > num< / code > ,可以根据二叉搜索树的性质来查找。如图 7-17 所示,我们声明一个节点 < code > cur< / code > ,从二叉树的根节点 < code > root< / code > 出发,循环比较节点值 < code > cur.val< / code > 和 < code > num< / code > 之间的大小关系: < / p >
< p > 给定目标节点值 < code > num< / code > ,可以根据二叉搜索树的性质来查找。如图 7-17 所示,我们声明一个节点 < code > cur< / code > ,从二叉树的根节点 < code > root< / code > 出发,循环比较节点值 < code > cur.val< / code > 和 < code > num< / code > 之间的大小关系。 < / p >
< ul >
< li > 若 < code > cur.val < num< / code > ,说明目标节点在 < code > cur< / code > 的右子树中,因此执行 < code > cur = cur.right< / code > 。< / li >
< li > 若 < code > cur.val > num< / code > ,说明目标节点在 < code > cur< / code > 的左子树中,因此执行 < code > cur = cur.left< / code > 。< / li >
@@ -3827,7 +3827,7 @@
< p > < img alt = "在二叉搜索树中插入节点" src = "../binary_search_tree.assets/bst_insert.png" / > < / p >
< p align = "center" > 图 7-18 在二叉搜索树中插入节点 < / p >
< p > 在代码实现中,需要注意以下两点: < / p >
< p > 在代码实现中,需要注意以下两点。 < / p >
< ul >
< li > 二叉搜索树不允许存在重复节点,否则将违反其定义。因此,若待插入节点在树中已存在,则不执行插入,直接返回。< / li >
< li > 为了实现插入节点,我们需要借助节点 < code > pre< / code > 保存上一轮循环的节点。这样在遍历至 < span class = "arithmatex" > \(\text{None}\)< / span > 时,我们可以获取到其父节点,从而完成节点插入操作。< / li >
@@ -4206,8 +4206,12 @@
< / div >
< p > 与查找节点相同,插入节点使用 < span class = "arithmatex" > \(O(\log n)\)< / span > 时间。< / p >
< h3 id = "3" > 3. 删除节点< a class = "headerlink" href = "#3" title = "Permanent link" > ¶ < / a > < / h3 >
< p > 与插入节点类似,我们需要在删除操作后维持 二叉搜索树的“左子树 < 根节点 < 右子树”的性质。首先,我们需要在二叉树中执行查找操作,获取待删除节点。接下来,根据待删除节点的子节点数量,删除操作需分为三种情况: < / p >
< p > 如图 7-19 所示,当待删除节点的度为 < span class = "arithmatex" > \(0\) < / span > 时,表示待删除节点是叶节点,可以直接删除。 < / p >
< p > 与插入节点类似,我们需要保证 在删除操作完成后, 二叉搜索树的“左子树 < 根节点 < 右子树”的性质仍然满足 。< / p >
< ol >
< li > 在二叉树中执行查找操作,获取待删除节点。< / li >
< li > 根据待删除节点的子节点数量(三种情况),执行对应的删除节点操作。< / li >
< / ol >
< p > 如图 7-19 所示,当待删除节点的度为 < span class = "arithmatex" > \(0\)< / span > 时,表示该节点是叶节点,可以直接删除。< / p >
< p > < img alt = "在二叉搜索树中删除节点(度为 0)" src = "../binary_search_tree.assets/bst_remove_case1.png" / > < / p >
< p align = "center" > 图 7-19 在二叉搜索树中删除节点(度为 0) < / p >
@@ -4216,7 +4220,7 @@
< p align = "center" > 图 7-20 在二叉搜索树中删除节点(度为 1) < / p >
< p > 当待删除节点的度为 < span class = "arithmatex" > \(2\)< / span > 时,我们无法直接删除它,而需要使用一个节点替换该节点。由于要保持二叉搜索树“左 < span class = "arithmatex" > \(< \)< / span > 根 < span class = "arithmatex" > \(< \)< / span > 右”的性质,< strong > 因此这个节点可以是右子树的最小节点或左子树的最大节点< / strong > 。< / p >
< p > 假设我们选择右子树的最小节点(即中序遍历的下一个节点),则删除操作如图 7-21 所示。< / p >
< p > 假设我们选择右子树的最小节点(即中序遍历的下一个节点),则删除操作流程 如图 7-21 所示。< / p >
< ol >
< li > 找到待删除节点在“中序遍历序列”中的下一个节点,记为 < code > tmp< / code > 。< / li >
< li > 将 < code > tmp< / code > 的值覆盖待删除节点的值,并在树中递归删除节点 < code > tmp< / code > 。< / li >
@@ -4931,16 +4935,15 @@ void insert(int num) {
< / div >
< / div >
< / div >
< h3 id = "4" > 4. 中序遍历性质 < a class = "headerlink" href = "#4" title = "Permanent link" > ¶ < / a > < / h3 >
< h3 id = "4" > 4. 中序遍历有序 < a class = "headerlink" href = "#4" title = "Permanent link" > ¶ < / a > < / h3 >
< p > 如图 7-22 所示,二叉树的中序遍历遵循“左 < span class = "arithmatex" > \(\rightarrow\)< / span > 根 < span class = "arithmatex" > \(\rightarrow\)< / span > 右”的遍历顺序,而二叉搜索树满足“左子节点 < span class = "arithmatex" > \(< \)< / span > 根节点 < span class = "arithmatex" > \(< \)< / span > 右子节点”的大小关系。< / p >
< p > 这意味着在二叉搜索树中进行中序遍历时,总是会优先遍历下一个最小节点,从而得出一个重要性质:< strong > 二叉搜索树的中序遍历序列是升序的< / strong > 。< / p >
< p > 利用中序遍历升序的性质,我们在二叉搜索树中获取有序数据仅需 < span class = "arithmatex" > \(O(n)\)< / span > 时间,无须额外排序,非常高效。< / p >
< p > 利用中序遍历升序的性质,我们在二叉搜索树中获取有序数据仅需 < span class = "arithmatex" > \(O(n)\)< / span > 时间,无须进行 额外的 排序操作 ,非常高效。< / p >
< p > < img alt = "二叉搜索树的中序遍历序列" src = "../binary_search_tree.assets/bst_inorder_traversal.png" / > < / p >
< p align = "center" > 图 7-22 二叉搜索树的中序遍历序列 < / p >
< h2 id = "742" > 7.4.2 二叉搜索树的效率< a class = "headerlink" href = "#742" title = "Permanent link" > ¶ < / a > < / h2 >
< p > 给定一组数据,我们考虑使用数组或二叉搜索树存储。< / p >
< p > 观察表 7-2 ,二叉搜索树的各项操作的时间复杂度都是对数阶,具有稳定且高效的性能表现。只有在高频添加、低频查找删除的数据适用场景下,数组比二叉搜索树的效率更高。< / p >
< p > 给定一组数据,我们考虑使用数组或二叉搜索树存储。观察表 7-2 ,二叉搜索树的各项操作的时间复杂度都是对数阶,具有稳定且高效的性能表现。只有在高频添加、低频查找删除的数据适用场景下,数组比二叉搜索树的效率更高。 < / p >
< p align = "center" > 表 7-2 数组与搜索树的效率对比 < / p >
< div class = "center-table" >
@@ -4973,8 +4976,8 @@ void insert(int num) {
< / div >
< p > 在理想情况下,二叉搜索树是“平衡”的,这样就可以在 < span class = "arithmatex" > \(\log n\)< / span > 轮循环内查找任意节点。< / p >
< p > 然而,如果我们在二叉搜索树中不断地插入和删除节点,可能导致二叉树退化为图 7-23 所示的链表,这时各种操作的时间复杂度也会退化为 < span class = "arithmatex" > \(O(n)\)< / span > 。< / p >
< p > < img alt = "二叉搜索树的平衡与 退化" src = "../binary_search_tree.assets/bst_degradation.png" / > < / p >
< p align = "center" > 图 7-23 二叉搜索树的平衡与 退化 < / p >
< p > < img alt = "二叉搜索树的退化" src = "../binary_search_tree.assets/bst_degradation.png" / > < / p >
< p align = "center" > 图 7-23 二叉搜索树的退化 < / p >
< h2 id = "743" > 7.4.3 二叉搜索树常见应用< a class = "headerlink" href = "#743" title = "Permanent link" > ¶ < / a > < / h2 >
< ul >