mirror of
https://github.com/krahets/hello-algo.git
synced 2026-05-11 11:07:14 +08:00
deploy
This commit is contained in:
@@ -1778,9 +1778,9 @@
|
||||
<h1 id="74-avl">7.4. AVL 树 *<a class="headerlink" href="#74-avl" title="Permanent link">¶</a></h1>
|
||||
<p>在「二叉搜索树」章节中提到,在进行多次插入与删除操作后,二叉搜索树可能会退化为链表。此时所有操作的时间复杂度都会由 <span class="arithmatex">\(O(\log n)\)</span> 劣化至 <span class="arithmatex">\(O(n)\)</span> 。</p>
|
||||
<p>如下图所示,执行两步删除结点后,该二叉搜索树就会退化为链表。</p>
|
||||
<p><img alt="avltree_degradation_from_removing_node" src="../avl_tree.assets/avltree_degradation_from_removing_node.png" /></p>
|
||||
<p><img alt="AVL 树在删除结点后发生退化" src="../avl_tree.assets/avltree_degradation_from_removing_node.png" /></p>
|
||||
<p>再比如,在以下完美二叉树中插入两个结点后,树严重向左偏斜,查找操作的时间复杂度也随之发生劣化。</p>
|
||||
<p><img alt="avltree_degradation_from_inserting_node" src="../avl_tree.assets/avltree_degradation_from_inserting_node.png" /></p>
|
||||
<p><img alt="AVL 树在插入结点后发生退化" src="../avl_tree.assets/avltree_degradation_from_inserting_node.png" /></p>
|
||||
<p>G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorithm for the organization of information" 中提出了「AVL 树」。<strong>论文中描述了一系列操作,使得在不断添加与删除结点后,AVL 树仍然不会发生退化</strong>,进而使得各种操作的时间复杂度均能保持在 <span class="arithmatex">\(O(\log n)\)</span> 级别。</p>
|
||||
<p>换言之,在频繁增删查改的使用场景中,AVL 树可始终保持很高的数据增删查改效率,具有很好的应用价值。</p>
|
||||
<h2 id="741-avl">7.4.1. AVL 树常见术语<a class="headerlink" href="#741-avl" title="Permanent link">¶</a></h2>
|
||||
@@ -2176,7 +2176,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<p>进而,如果结点 <code>child</code> 本身有右子结点(记为 <code>grandChild</code> ),则需要在「右旋」中添加一步:将 <code>grandChild</code> 作为 <code>node</code> 的左子结点。</p>
|
||||
<p><img alt="avltree_right_rotate_with_grandchild" src="../avl_tree.assets/avltree_right_rotate_with_grandchild.png" /></p>
|
||||
<p><img alt="有 grandChild 的右旋操作" src="../avl_tree.assets/avltree_right_rotate_with_grandchild.png" /></p>
|
||||
<p>“向右旋转”是一种形象化的说法,实际需要通过修改结点指针实现,代码如下所示。</p>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="5:10"><input checked="checked" id="__tabbed_5_1" name="__tabbed_5" type="radio" /><input id="__tabbed_5_2" name="__tabbed_5" type="radio" /><input id="__tabbed_5_3" name="__tabbed_5" type="radio" /><input id="__tabbed_5_4" name="__tabbed_5" type="radio" /><input id="__tabbed_5_5" name="__tabbed_5" type="radio" /><input id="__tabbed_5_6" name="__tabbed_5" type="radio" /><input id="__tabbed_5_7" name="__tabbed_5" type="radio" /><input id="__tabbed_5_8" name="__tabbed_5" type="radio" /><input id="__tabbed_5_9" name="__tabbed_5" type="radio" /><input id="__tabbed_5_10" name="__tabbed_5" type="radio" /><div class="tabbed-labels"><label for="__tabbed_5_1">Java</label><label for="__tabbed_5_2">C++</label><label for="__tabbed_5_3">Python</label><label for="__tabbed_5_4">Go</label><label for="__tabbed_5_5">JavaScript</label><label for="__tabbed_5_6">TypeScript</label><label for="__tabbed_5_7">C</label><label for="__tabbed_5_8">C#</label><label for="__tabbed_5_9">Swift</label><label for="__tabbed_5_10">Zig</label></div>
|
||||
<div class="tabbed-content">
|
||||
@@ -2332,9 +2332,9 @@
|
||||
</div>
|
||||
<h3 id="case-2-">Case 2 - 左旋<a class="headerlink" href="#case-2-" title="Permanent link">¶</a></h3>
|
||||
<p>类似地,如果将取上述失衡二叉树的“镜像”,那么则需要「左旋」操作。</p>
|
||||
<p><img alt="avltree_left_rotate" src="../avl_tree.assets/avltree_left_rotate.png" /></p>
|
||||
<p><img alt="左旋操作" src="../avl_tree.assets/avltree_left_rotate.png" /></p>
|
||||
<p>同理,若结点 <code>child</code> 本身有左子结点(记为 <code>grandChild</code> ),则需要在「左旋」中添加一步:将 <code>grandChild</code> 作为 <code>node</code> 的右子结点。</p>
|
||||
<p><img alt="avltree_left_rotate_with_grandchild" src="../avl_tree.assets/avltree_left_rotate_with_grandchild.png" /></p>
|
||||
<p><img alt="有 grandChild 的左旋操作" src="../avl_tree.assets/avltree_left_rotate_with_grandchild.png" /></p>
|
||||
<p>观察发现,<strong>「左旋」和「右旋」操作是镜像对称的,两者对应解决的两种失衡情况也是对称的</strong>。根据对称性,我们可以很方便地从「右旋」推导出「左旋」。具体地,只需将「右旋」代码中的把所有的 <code>left</code> 替换为 <code>right</code> 、所有的 <code>right</code> 替换为 <code>left</code> ,即可得到「左旋」代码。</p>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="6:10"><input checked="checked" id="__tabbed_6_1" name="__tabbed_6" type="radio" /><input id="__tabbed_6_2" name="__tabbed_6" type="radio" /><input id="__tabbed_6_3" name="__tabbed_6" type="radio" /><input id="__tabbed_6_4" name="__tabbed_6" type="radio" /><input id="__tabbed_6_5" name="__tabbed_6" type="radio" /><input id="__tabbed_6_6" name="__tabbed_6" type="radio" /><input id="__tabbed_6_7" name="__tabbed_6" type="radio" /><input id="__tabbed_6_8" name="__tabbed_6" type="radio" /><input id="__tabbed_6_9" name="__tabbed_6" type="radio" /><input id="__tabbed_6_10" name="__tabbed_6" type="radio" /><div class="tabbed-labels"><label for="__tabbed_6_1">Java</label><label for="__tabbed_6_2">C++</label><label for="__tabbed_6_3">Python</label><label for="__tabbed_6_4">Go</label><label for="__tabbed_6_5">JavaScript</label><label for="__tabbed_6_6">TypeScript</label><label for="__tabbed_6_7">C</label><label for="__tabbed_6_8">C#</label><label for="__tabbed_6_9">Swift</label><label for="__tabbed_6_10">Zig</label></div>
|
||||
<div class="tabbed-content">
|
||||
@@ -2490,13 +2490,13 @@
|
||||
</div>
|
||||
<h3 id="case-3-">Case 3 - 先左后右<a class="headerlink" href="#case-3-" title="Permanent link">¶</a></h3>
|
||||
<p>对于下图的失衡结点 3 ,<strong>单一使用左旋或右旋都无法使子树恢复平衡</strong>,此时需要「先左旋后右旋」,即先对 <code>child</code> 执行「左旋」,再对 <code>node</code> 执行「右旋」。</p>
|
||||
<p><img alt="avltree_left_right_rotate" src="../avl_tree.assets/avltree_left_right_rotate.png" /></p>
|
||||
<p><img alt="先左旋后右旋" src="../avl_tree.assets/avltree_left_right_rotate.png" /></p>
|
||||
<h3 id="case-4-">Case 4 - 先右后左<a class="headerlink" href="#case-4-" title="Permanent link">¶</a></h3>
|
||||
<p>同理,取以上失衡二叉树的镜像,则需要「先右旋后左旋」,即先对 <code>child</code> 执行「右旋」,然后对 <code>node</code> 执行「左旋」。</p>
|
||||
<p><img alt="avltree_right_left_rotate" src="../avl_tree.assets/avltree_right_left_rotate.png" /></p>
|
||||
<p><img alt="先右旋后左旋" src="../avl_tree.assets/avltree_right_left_rotate.png" /></p>
|
||||
<h3 id="_3">旋转的选择<a class="headerlink" href="#_3" title="Permanent link">¶</a></h3>
|
||||
<p>下图描述的四种失衡情况与上述 Cases 逐个对应,分别需采用 <strong>右旋、左旋、先右后左、先左后右</strong> 的旋转操作。</p>
|
||||
<p><img alt="avltree_rotation_cases" src="../avl_tree.assets/avltree_rotation_cases.png" /></p>
|
||||
<p><img alt="AVL 树的四种旋转情况" src="../avl_tree.assets/avltree_rotation_cases.png" /></p>
|
||||
<p>具体地,在代码中使用 <strong>失衡结点的平衡因子、较高一侧子结点的平衡因子</strong> 来确定失衡结点属于上图中的哪种情况。</p>
|
||||
<div class="center-table">
|
||||
<table>
|
||||
|
||||
@@ -1673,7 +1673,7 @@
|
||||
<li>对于根结点,左子树中所有结点的值 <span class="arithmatex">\(<\)</span> 根结点的值 <span class="arithmatex">\(<\)</span> 右子树中所有结点的值;</li>
|
||||
<li>任意结点的左子树和右子树也是二叉搜索树,即也满足条件 <code>1.</code> ;</li>
|
||||
</ol>
|
||||
<p><img alt="binary_search_tree" src="../binary_search_tree.assets/binary_search_tree.png" /></p>
|
||||
<p><img alt="二叉搜索树" src="../binary_search_tree.assets/binary_search_tree.png" /></p>
|
||||
<h2 id="731">7.3.1. 二叉搜索树的操作<a class="headerlink" href="#731" title="Permanent link">¶</a></h2>
|
||||
<h3 id="_1">查找结点<a class="headerlink" href="#_1" title="Permanent link">¶</a></h3>
|
||||
<p>给定目标结点值 <code>num</code> ,可以根据二叉搜索树的性质来查找。我们声明一个结点 <code>cur</code> ,从二叉树的根结点 <code>root</code> 出发,循环比较结点值 <code>cur.val</code> 和 <code>num</code> 之间的大小关系</p>
|
||||
@@ -1893,7 +1893,7 @@
|
||||
<li><strong>在该位置插入结点</strong>:初始化结点 <code>num</code> ,将该结点放到 <span class="arithmatex">\(\text{null}\)</span> 的位置 ;</li>
|
||||
</ol>
|
||||
<p>二叉搜索树不允许存在重复结点,否则将会违背其定义。因此若待插入结点在树中已经存在,则不执行插入,直接返回即可。</p>
|
||||
<p><img alt="bst_insert" src="../binary_search_tree.assets/bst_insert.png" /></p>
|
||||
<p><img alt="在二叉搜索树中插入结点" src="../binary_search_tree.assets/bst_insert.png" /></p>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="3:10"><input checked="checked" id="__tabbed_3_1" name="__tabbed_3" type="radio" /><input id="__tabbed_3_2" name="__tabbed_3" type="radio" /><input id="__tabbed_3_3" name="__tabbed_3" type="radio" /><input id="__tabbed_3_4" name="__tabbed_3" type="radio" /><input id="__tabbed_3_5" name="__tabbed_3" type="radio" /><input id="__tabbed_3_6" name="__tabbed_3" type="radio" /><input id="__tabbed_3_7" name="__tabbed_3" type="radio" /><input id="__tabbed_3_8" name="__tabbed_3" type="radio" /><input id="__tabbed_3_9" name="__tabbed_3" type="radio" /><input id="__tabbed_3_10" name="__tabbed_3" type="radio" /><div class="tabbed-labels"><label for="__tabbed_3_1">Java</label><label for="__tabbed_3_2">C++</label><label for="__tabbed_3_3">Python</label><label for="__tabbed_3_4">Go</label><label for="__tabbed_3_5">JavaScript</label><label for="__tabbed_3_6">TypeScript</label><label for="__tabbed_3_7">C</label><label for="__tabbed_3_8">C#</label><label for="__tabbed_3_9">Swift</label><label for="__tabbed_3_10">Zig</label></div>
|
||||
<div class="tabbed-content">
|
||||
<div class="tabbed-block">
|
||||
@@ -2173,9 +2173,9 @@
|
||||
<h3 id="_3">删除结点<a class="headerlink" href="#_3" title="Permanent link">¶</a></h3>
|
||||
<p>与插入结点一样,我们需要在删除操作后维持二叉搜索树的“左子树 < 根结点 < 右子树”的性质。首先,我们需要在二叉树中执行查找操作,获取待删除结点。接下来,根据待删除结点的子结点数量,删除操作需要分为三种情况:</p>
|
||||
<p><strong>当待删除结点的子结点数量 <span class="arithmatex">\(= 0\)</span> 时</strong>,表明待删除结点是叶结点,直接删除即可。</p>
|
||||
<p><img alt="bst_remove_case1" src="../binary_search_tree.assets/bst_remove_case1.png" /></p>
|
||||
<p><img alt="在二叉搜索树中删除结点(度为 0)" src="../binary_search_tree.assets/bst_remove_case1.png" /></p>
|
||||
<p><strong>当待删除结点的子结点数量 <span class="arithmatex">\(= 1\)</span> 时</strong>,将待删除结点替换为其子结点即可。</p>
|
||||
<p><img alt="bst_remove_case2" src="../binary_search_tree.assets/bst_remove_case2.png" /></p>
|
||||
<p><img alt="在二叉搜索树中删除结点(度为 1)" src="../binary_search_tree.assets/bst_remove_case2.png" /></p>
|
||||
<p><strong>当待删除结点的子结点数量 <span class="arithmatex">\(= 2\)</span> 时</strong>,删除操作分为三步:</p>
|
||||
<ol>
|
||||
<li>找到待删除结点在 <strong>中序遍历序列</strong> 中的下一个结点,记为 <code>nex</code> ;</li>
|
||||
@@ -2740,7 +2740,7 @@
|
||||
<h3 id="_4">排序<a class="headerlink" href="#_4" title="Permanent link">¶</a></h3>
|
||||
<p>我们知道,「中序遍历」遵循“左 <span class="arithmatex">\(\rightarrow\)</span> 根 <span class="arithmatex">\(\rightarrow\)</span> 右”的遍历优先级,而二叉搜索树遵循“左子结点 <span class="arithmatex">\(<\)</span> 根结点 <span class="arithmatex">\(<\)</span> 右子结点”的大小关系。因此,在二叉搜索树中进行中序遍历时,总是会优先遍历下一个最小结点,从而得出一条重要性质:<strong>二叉搜索树的中序遍历序列是升序的</strong>。</p>
|
||||
<p>借助中序遍历升序的性质,我们在二叉搜索树中获取有序数据仅需 <span class="arithmatex">\(O(n)\)</span> 时间,而无需额外排序,非常高效。</p>
|
||||
<p><img alt="bst_inorder_traversal" src="../binary_search_tree.assets/bst_inorder_traversal.png" /></p>
|
||||
<p><img alt="二叉搜索树的中序遍历序列" src="../binary_search_tree.assets/bst_inorder_traversal.png" /></p>
|
||||
<h2 id="732">7.3.2. 二叉搜索树的效率<a class="headerlink" href="#732" title="Permanent link">¶</a></h2>
|
||||
<p>假设给定 <span class="arithmatex">\(n\)</span> 个数字,最常用的存储方式是「数组」,那么对于这串乱序的数字,常见操作的效率为:</p>
|
||||
<ul>
|
||||
@@ -2802,7 +2802,7 @@
|
||||
<p class="admonition-title">Note</p>
|
||||
<p>在实际应用中,如何保持二叉搜索树的平衡,也是一个需要重要考虑的问题。</p>
|
||||
</div>
|
||||
<p><img alt="bst_degradation" src="../binary_search_tree.assets/bst_degradation.png" /></p>
|
||||
<p><img alt="二叉搜索树的平衡与退化" src="../binary_search_tree.assets/bst_degradation.png" /></p>
|
||||
<h2 id="734">7.3.4. 二叉搜索树常见应用<a class="headerlink" href="#734" title="Permanent link">¶</a></h2>
|
||||
<ul>
|
||||
<li>系统中的多级索引,高效查找、插入、删除操作。</li>
|
||||
|
||||
@@ -1790,9 +1790,7 @@
|
||||
</div>
|
||||
<p>结点的两个指针分别指向「左子结点 Left Child Node」和「右子结点 Right Child Node」,并且称该结点为两个子结点的「父结点 Parent Node」。给定二叉树某结点,将左子结点以下的树称为该结点的「左子树 Left Subtree」,右子树同理。</p>
|
||||
<p>除了叶结点外,每个结点都有子结点和子树。例如,若将下图的「结点 2」看作父结点,那么其左子结点和右子结点分别为「结点 4」和「结点 5」,左子树和右子树分别为「结点 4 及其以下结点形成的树」和「结点 5 及其以下结点形成的树」。</p>
|
||||
<p><img alt="binary_tree_definition" src="../binary_tree.assets/binary_tree_definition.png" /></p>
|
||||
<p align="center"> Fig. 子结点与子树 </p>
|
||||
|
||||
<p><img alt="父结点、子结点、子树" src="../binary_tree.assets/binary_tree_definition.png" /></p>
|
||||
<h2 id="711">7.1.1. 二叉树常见术语<a class="headerlink" href="#711" title="Permanent link">¶</a></h2>
|
||||
<p>二叉树的术语较多,建议尽量理解并记住。后续可能遗忘,可以在需要使用时回来查看确认。</p>
|
||||
<ul>
|
||||
@@ -1805,9 +1803,7 @@
|
||||
<li>结点「深度 Depth」 :根结点到该结点走过边的数量;</li>
|
||||
<li>结点「高度 Height」:最远叶结点到该结点走过边的数量;</li>
|
||||
</ul>
|
||||
<p><img alt="binary_tree_terminology" src="../binary_tree.assets/binary_tree_terminology.png" /></p>
|
||||
<p align="center"> Fig. 二叉树的常见术语 </p>
|
||||
|
||||
<p><img alt="二叉树的常用术语" src="../binary_tree.assets/binary_tree_terminology.png" /></p>
|
||||
<div class="admonition tip">
|
||||
<p class="admonition-title">高度与深度的定义</p>
|
||||
<p>值得注意,我们通常将「高度」和「深度」定义为“走过边的数量”,而有些题目或教材会将其定义为“走过结点的数量”,此时高度或深度都需要 + 1 。</p>
|
||||
@@ -1945,9 +1941,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<p><strong>插入与删除结点</strong>。与链表类似,插入与删除结点都可以通过修改指针实现。</p>
|
||||
<p><img alt="binary_tree_add_remove" src="../binary_tree.assets/binary_tree_add_remove.png" /></p>
|
||||
<p align="center"> Fig. 在二叉树中插入与删除结点 </p>
|
||||
|
||||
<p><img alt="在二叉树中插入与删除结点" src="../binary_tree.assets/binary_tree_add_remove.png" /></p>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="3:10"><input checked="checked" id="__tabbed_3_1" name="__tabbed_3" type="radio" /><input id="__tabbed_3_2" name="__tabbed_3" type="radio" /><input id="__tabbed_3_3" name="__tabbed_3" type="radio" /><input id="__tabbed_3_4" name="__tabbed_3" type="radio" /><input id="__tabbed_3_5" name="__tabbed_3" type="radio" /><input id="__tabbed_3_6" name="__tabbed_3" type="radio" /><input id="__tabbed_3_7" name="__tabbed_3" type="radio" /><input id="__tabbed_3_8" name="__tabbed_3" type="radio" /><input id="__tabbed_3_9" name="__tabbed_3" type="radio" /><input id="__tabbed_3_10" name="__tabbed_3" type="radio" /><div class="tabbed-labels"><label for="__tabbed_3_1">Java</label><label for="__tabbed_3_2">C++</label><label for="__tabbed_3_3">Python</label><label for="__tabbed_3_4">Go</label><label for="__tabbed_3_5">JavaScript</label><label for="__tabbed_3_6">TypeScript</label><label for="__tabbed_3_7">C</label><label for="__tabbed_3_8">C#</label><label for="__tabbed_3_9">Swift</label><label for="__tabbed_3_10">Zig</label></div>
|
||||
<div class="tabbed-content">
|
||||
<div class="tabbed-block">
|
||||
@@ -2049,26 +2043,24 @@
|
||||
<p class="admonition-title">Tip</p>
|
||||
<p>在中文社区中,完美二叉树常被称为「满二叉树」,请注意与完满二叉树区分。</p>
|
||||
</div>
|
||||
<p><img alt="perfect_binary_tree" src="../binary_tree.assets/perfect_binary_tree.png" /></p>
|
||||
<p><img alt="完美二叉树" src="../binary_tree.assets/perfect_binary_tree.png" /></p>
|
||||
<h3 id="_2">完全二叉树<a class="headerlink" href="#_2" title="Permanent link">¶</a></h3>
|
||||
<p>「完全二叉树 Complete Binary Tree」只有最底层的结点未被填满,且最底层结点尽量靠左填充。</p>
|
||||
<p><strong>完全二叉树非常适合用数组来表示</strong>。如果按照层序遍历序列的顺序来存储,那么空结点 <code>null</code> 一定全部出现在序列的尾部,因此我们就可以不用存储这些 null 了。</p>
|
||||
<p><img alt="complete_binary_tree" src="../binary_tree.assets/complete_binary_tree.png" /></p>
|
||||
<p><img alt="完全二叉树" src="../binary_tree.assets/complete_binary_tree.png" /></p>
|
||||
<h3 id="_3">完满二叉树<a class="headerlink" href="#_3" title="Permanent link">¶</a></h3>
|
||||
<p>「完满二叉树 Full Binary Tree」除了叶结点之外,其余所有结点都有两个子结点。</p>
|
||||
<p><img alt="full_binary_tree" src="../binary_tree.assets/full_binary_tree.png" /></p>
|
||||
<p><img alt="完满二叉树" src="../binary_tree.assets/full_binary_tree.png" /></p>
|
||||
<h3 id="_4">平衡二叉树<a class="headerlink" href="#_4" title="Permanent link">¶</a></h3>
|
||||
<p>「平衡二叉树 Balanced Binary Tree」中任意结点的左子树和右子树的高度之差的绝对值 <span class="arithmatex">\(\leq 1\)</span> 。</p>
|
||||
<p><img alt="balanced_binary_tree" src="../binary_tree.assets/balanced_binary_tree.png" /></p>
|
||||
<p><img alt="平衡二叉树" src="../binary_tree.assets/balanced_binary_tree.png" /></p>
|
||||
<h2 id="714">7.1.4. 二叉树的退化<a class="headerlink" href="#714" title="Permanent link">¶</a></h2>
|
||||
<p>当二叉树的每层的结点都被填满时,达到「完美二叉树」;而当所有结点都偏向一边时,二叉树退化为「链表」。</p>
|
||||
<ul>
|
||||
<li>完美二叉树是一个二叉树的“最佳状态”,可以完全发挥出二叉树“分治”的优势;</li>
|
||||
<li>链表则是另一个极端,各项操作都变为线性操作,时间复杂度退化至 <span class="arithmatex">\(O(n)\)</span> ;</li>
|
||||
</ul>
|
||||
<p><img alt="binary_tree_corner_cases" src="../binary_tree.assets/binary_tree_corner_cases.png" /></p>
|
||||
<p align="center"> Fig. 二叉树的最佳和最差结构 </p>
|
||||
|
||||
<p><img alt="二叉树的最佳与最二叉树的最佳和最差结构差情况" src="../binary_tree.assets/binary_tree_corner_cases.png" /></p>
|
||||
<p>如下表所示,在最佳和最差结构下,二叉树的叶结点数量、结点总数、高度等达到极大或极小值。</p>
|
||||
<div class="center-table">
|
||||
<table>
|
||||
@@ -2107,9 +2099,9 @@
|
||||
<p>我们一般使用二叉树的「链表表示」,即存储单位为结点 <code>TreeNode</code> ,结点之间通过指针(引用)相连接。本文前述示例代码展示了二叉树在链表表示下的各项基本操作。</p>
|
||||
<p>那能否可以用「数组表示」二叉树呢?答案是肯定的。先来分析一个简单案例,给定一个「完美二叉树」,将结点按照层序遍历的顺序编号(从 0 开始),那么可以推导得出父结点索引与子结点索引之间的「映射公式」:<strong>设结点的索引为 <span class="arithmatex">\(i\)</span> ,则该结点的左子结点索引为 <span class="arithmatex">\(2i + 1\)</span> 、右子结点索引为 <span class="arithmatex">\(2i + 2\)</span></strong> 。</p>
|
||||
<p><strong>本质上,映射公式的作用就是链表中的指针</strong>。对于层序遍历序列中的任意结点,我们都可以使用映射公式来访问子结点。因此,可以直接使用层序遍历序列(即数组)来表示完美二叉树。</p>
|
||||
<p><img alt="array_representation_mapping" src="../binary_tree.assets/array_representation_mapping.png" /></p>
|
||||
<p><img alt="完美二叉树的数组表示" src="../binary_tree.assets/array_representation_mapping.png" /></p>
|
||||
<p>然而,完美二叉树只是个例,二叉树中间层往往存在许多空结点(即 <code>null</code> ),而层序遍历序列并不包含这些空结点,并且我们无法单凭序列来猜测空结点的数量和分布位置,<strong>即理论上存在许多种二叉树都符合该层序遍历序列</strong>。显然,这种情况无法使用数组来存储二叉树。</p>
|
||||
<p><img alt="array_representation_without_empty" src="../binary_tree.assets/array_representation_without_empty.png" /></p>
|
||||
<p><img alt="给定数组对应多种二叉树可能性" src="../binary_tree.assets/array_representation_without_empty.png" /></p>
|
||||
<p>为了解决此问题,考虑按照完美二叉树的形式来表示所有二叉树,<strong>即在序列中使用特殊符号来显式地表示“空位”</strong>。如下图所示,这样处理后,序列(数组)就可以唯一表示二叉树了。</p>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="4:10"><input checked="checked" id="__tabbed_4_1" name="__tabbed_4" type="radio" /><input id="__tabbed_4_2" name="__tabbed_4" type="radio" /><input id="__tabbed_4_3" name="__tabbed_4" type="radio" /><input id="__tabbed_4_4" name="__tabbed_4" type="radio" /><input id="__tabbed_4_5" name="__tabbed_4" type="radio" /><input id="__tabbed_4_6" name="__tabbed_4" type="radio" /><input id="__tabbed_4_7" name="__tabbed_4" type="radio" /><input id="__tabbed_4_8" name="__tabbed_4" type="radio" /><input id="__tabbed_4_9" name="__tabbed_4" type="radio" /><input id="__tabbed_4_10" name="__tabbed_4" type="radio" /><div class="tabbed-labels"><label for="__tabbed_4_1">Java</label><label for="__tabbed_4_2">C++</label><label for="__tabbed_4_3">Python</label><label for="__tabbed_4_4">Go</label><label for="__tabbed_4_5">JavaScript</label><label for="__tabbed_4_6">TypeScript</label><label for="__tabbed_4_7">C</label><label for="__tabbed_4_8">C#</label><label for="__tabbed_4_9">Swift</label><label for="__tabbed_4_10">Zig</label></div>
|
||||
<div class="tabbed-content">
|
||||
@@ -2170,9 +2162,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p><img alt="array_representation_with_empty" src="../binary_tree.assets/array_representation_with_empty.png" /></p>
|
||||
<p><img alt="任意类型二叉树的数组表示" src="../binary_tree.assets/array_representation_with_empty.png" /></p>
|
||||
<p>回顾「完全二叉树」的定义,其只有最底层有空结点,并且最底层的结点尽量靠左,因而所有空结点都一定出现在层序遍历序列的末尾。<strong>因为我们先验地确定了空位的位置,所以在使用数组表示完全二叉树时,可以省略存储“空位”</strong>。因此,完全二叉树非常适合使用数组来表示。</p>
|
||||
<p><img alt="array_representation_complete_binary_tree" src="../binary_tree.assets/array_representation_complete_binary_tree.png" /></p>
|
||||
<p><img alt="完全二叉树的数组表示" src="../binary_tree.assets/array_representation_complete_binary_tree.png" /></p>
|
||||
<p>数组表示有两个优点: 一是不需要存储指针,节省空间;二是可以随机访问结点。然而,当二叉树中的“空位”很多时,数组中只包含很少结点的数据,空间利用率很低。</p>
|
||||
|
||||
|
||||
|
||||
@@ -1657,7 +1657,7 @@
|
||||
<h2 id="721">7.2.1. 层序遍历<a class="headerlink" href="#721" title="Permanent link">¶</a></h2>
|
||||
<p>「层序遍历 Level-Order Traversal」从顶至底、一层一层地遍历二叉树,并在每层中按照从左到右的顺序访问结点。</p>
|
||||
<p>层序遍历本质上是「广度优先搜索 Breadth-First Traversal」,其体现着一种“一圈一圈向外”的层进遍历方式。</p>
|
||||
<p><img alt="binary_tree_bfs" src="../binary_tree_traversal.assets/binary_tree_bfs.png" /></p>
|
||||
<p><img alt="二叉树的层序遍历" src="../binary_tree_traversal.assets/binary_tree_bfs.png" /></p>
|
||||
<p align="center"> Fig. 二叉树的层序遍历 </p>
|
||||
|
||||
<h3 id="_1">算法实现<a class="headerlink" href="#_1" title="Permanent link">¶</a></h3>
|
||||
@@ -1874,7 +1874,7 @@
|
||||
<h2 id="722">7.2.2. 前序、中序、后序遍历<a class="headerlink" href="#722" title="Permanent link">¶</a></h2>
|
||||
<p>相对地,前、中、后序遍历皆属于「深度优先遍历 Depth-First Traversal」,其体现着一种“先走到尽头,再回头继续”的回溯遍历方式。</p>
|
||||
<p>如下图所示,左侧是深度优先遍历的的示意图,右上方是对应的递归实现代码。深度优先遍历就像是绕着整个二叉树的外围“走”一圈,走的过程中,在每个结点都会遇到三个位置,分别对应前序遍历、中序遍历、后序遍历。</p>
|
||||
<p><img alt="binary_tree_dfs" src="../binary_tree_traversal.assets/binary_tree_dfs.png" /></p>
|
||||
<p><img alt="二叉搜索树的前、中、后序遍历" src="../binary_tree_traversal.assets/binary_tree_dfs.png" /></p>
|
||||
<p align="center"> Fig. 二叉树的前 / 中 / 后序遍历 </p>
|
||||
|
||||
<div class="center-table">
|
||||
|
||||
Reference in New Issue
Block a user