mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-25 12:36:07 +00:00
deploy
This commit is contained in:
@@ -3427,7 +3427,7 @@
|
||||
|
||||
<h1 id="73">7.3 二叉树数组表示<a class="headerlink" href="#73" title="Permanent link">¶</a></h1>
|
||||
<p>在链表表示下,二叉树的存储单元为节点 <code>TreeNode</code> ,节点之间通过指针相连接。在上节中,我们学习了在链表表示下的二叉树的各项基本操作。</p>
|
||||
<p>那么,能否用「数组」来表示二叉树呢?答案是肯定的。</p>
|
||||
<p>那么,我们能否用数组来表示二叉树呢?答案是肯定的。</p>
|
||||
<h2 id="731">7.3.1 表示完美二叉树<a class="headerlink" href="#731" title="Permanent link">¶</a></h2>
|
||||
<p>先分析一个简单案例。给定一个完美二叉树,我们将所有节点按照层序遍历的顺序存储在一个数组中,则每个节点都对应唯一的数组索引。</p>
|
||||
<p>根据层序遍历的特性,我们可以推导出父节点索引与子节点索引之间的“映射公式”:<strong>若节点的索引为 <span class="arithmatex">\(i\)</span> ,则该节点的左子节点索引为 <span class="arithmatex">\(2i + 1\)</span> ,右子节点索引为 <span class="arithmatex">\(2i + 2\)</span></strong> 。</p>
|
||||
|
||||
@@ -3627,7 +3627,7 @@
|
||||
|
||||
<p>G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorithm for the organization of information" 中提出了「AVL 树」。论文中详细描述了一系列操作,确保在持续添加和删除节点后,AVL 树不会退化,从而使得各种操作的时间复杂度保持在 <span class="arithmatex">\(O(\log n)\)</span> 级别。换句话说,在需要频繁进行增删查改操作的场景中,AVL 树能始终保持高效的数据操作性能,具有很好的应用价值。</p>
|
||||
<h2 id="751-avl">7.5.1 AVL 树常见术语<a class="headerlink" href="#751-avl" title="Permanent link">¶</a></h2>
|
||||
<p>「AVL 树」既是二叉搜索树也是平衡二叉树,同时满足这两类二叉树的所有性质,因此也被称为「平衡二叉搜索树」。</p>
|
||||
<p>AVL 树既是二叉搜索树也是平衡二叉树,同时满足这两类二叉树的所有性质,因此也被称为「平衡二叉搜索树 balanced binary search tree」。</p>
|
||||
<h3 id="1">1. 节点高度<a class="headerlink" href="#1" title="Permanent link">¶</a></h3>
|
||||
<p>在操作 AVL 树时,我们需要获取节点的高度,因此需要为 AVL 树的节点类添加 <code>height</code> 变量。</p>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="1:12"><input checked="checked" id="__tabbed_1_1" name="__tabbed_1" type="radio" /><input id="__tabbed_1_2" name="__tabbed_1" type="radio" /><input id="__tabbed_1_3" name="__tabbed_1" type="radio" /><input id="__tabbed_1_4" name="__tabbed_1" type="radio" /><input id="__tabbed_1_5" name="__tabbed_1" type="radio" /><input id="__tabbed_1_6" name="__tabbed_1" type="radio" /><input id="__tabbed_1_7" name="__tabbed_1" type="radio" /><input id="__tabbed_1_8" name="__tabbed_1" type="radio" /><input id="__tabbed_1_9" name="__tabbed_1" type="radio" /><input id="__tabbed_1_10" name="__tabbed_1" type="radio" /><input id="__tabbed_1_11" name="__tabbed_1" type="radio" /><input id="__tabbed_1_12" name="__tabbed_1" type="radio" /><div class="tabbed-labels"><label for="__tabbed_1_1">Java</label><label for="__tabbed_1_2">C++</label><label for="__tabbed_1_3">Python</label><label for="__tabbed_1_4">Go</label><label for="__tabbed_1_5">JS</label><label for="__tabbed_1_6">TS</label><label for="__tabbed_1_7">C</label><label for="__tabbed_1_8">C#</label><label for="__tabbed_1_9">Swift</label><label for="__tabbed_1_10">Zig</label><label for="__tabbed_1_11">Dart</label><label for="__tabbed_1_12">Rust</label></div>
|
||||
@@ -3778,7 +3778,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>「节点高度」是指从该节点到最远叶节点的距离,即所经过的“边”的数量。需要特别注意的是,叶节点的高度为 0 ,而空节点的高度为 -1 。我们将创建两个工具函数,分别用于获取和更新节点的高度。</p>
|
||||
<p>“节点高度”是指从该节点到最远叶节点的距离,即所经过的“边”的数量。需要特别注意的是,叶节点的高度为 0 ,而空节点的高度为 -1 。我们将创建两个工具函数,分别用于获取和更新节点的高度。</p>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="2:12"><input checked="checked" id="__tabbed_2_1" name="__tabbed_2" type="radio" /><input id="__tabbed_2_2" name="__tabbed_2" type="radio" /><input id="__tabbed_2_3" name="__tabbed_2" type="radio" /><input id="__tabbed_2_4" name="__tabbed_2" type="radio" /><input id="__tabbed_2_5" name="__tabbed_2" type="radio" /><input id="__tabbed_2_6" name="__tabbed_2" type="radio" /><input id="__tabbed_2_7" name="__tabbed_2" type="radio" /><input id="__tabbed_2_8" name="__tabbed_2" type="radio" /><input id="__tabbed_2_9" name="__tabbed_2" type="radio" /><input id="__tabbed_2_10" name="__tabbed_2" type="radio" /><input id="__tabbed_2_11" name="__tabbed_2" type="radio" /><input id="__tabbed_2_12" name="__tabbed_2" type="radio" /><div class="tabbed-labels"><label for="__tabbed_2_1">Java</label><label for="__tabbed_2_2">C++</label><label for="__tabbed_2_3">Python</label><label for="__tabbed_2_4">Go</label><label for="__tabbed_2_5">JS</label><label for="__tabbed_2_6">TS</label><label for="__tabbed_2_7">C</label><label for="__tabbed_2_8">C#</label><label for="__tabbed_2_9">Swift</label><label for="__tabbed_2_10">Zig</label><label for="__tabbed_2_11">Dart</label><label for="__tabbed_2_12">Rust</label></div>
|
||||
<div class="tabbed-content">
|
||||
<div class="tabbed-block">
|
||||
@@ -3980,7 +3980,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<h3 id="2">2. 节点平衡因子<a class="headerlink" href="#2" title="Permanent link">¶</a></h3>
|
||||
<p>节点的「平衡因子 Balance Factor」定义为节点左子树的高度减去右子树的高度,同时规定空节点的平衡因子为 0 。我们同样将获取节点平衡因子的功能封装成函数,方便后续使用。</p>
|
||||
<p>节点的「平衡因子 balance factor」定义为节点左子树的高度减去右子树的高度,同时规定空节点的平衡因子为 0 。我们同样将获取节点平衡因子的功能封装成函数,方便后续使用。</p>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="3:12"><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" /><input id="__tabbed_3_11" name="__tabbed_3" type="radio" /><input id="__tabbed_3_12" 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">JS</label><label for="__tabbed_3_6">TS</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><label for="__tabbed_3_11">Dart</label><label for="__tabbed_3_12">Rust</label></div>
|
||||
<div class="tabbed-content">
|
||||
<div class="tabbed-block">
|
||||
@@ -4120,10 +4120,10 @@
|
||||
<p>设平衡因子为 <span class="arithmatex">\(f\)</span> ,则一棵 AVL 树的任意节点的平衡因子皆满足 <span class="arithmatex">\(-1 \le f \le 1\)</span> 。</p>
|
||||
</div>
|
||||
<h2 id="752-avl">7.5.2 AVL 树旋转<a class="headerlink" href="#752-avl" title="Permanent link">¶</a></h2>
|
||||
<p>AVL 树的特点在于「旋转 Rotation」操作,它能够在不影响二叉树的中序遍历序列的前提下,使失衡节点重新恢复平衡。换句话说,<strong>旋转操作既能保持树的「二叉搜索树」属性,也能使树重新变为「平衡二叉树」</strong>。</p>
|
||||
<p>我们将平衡因子绝对值 <span class="arithmatex">\(> 1\)</span> 的节点称为「失衡节点」。根据节点失衡情况的不同,旋转操作分为四种:右旋、左旋、先右旋后左旋、先左旋后右旋。下面我们将详细介绍这些旋转操作。</p>
|
||||
<p>AVL 树的特点在于“旋转”操作,它能够在不影响二叉树的中序遍历序列的前提下,使失衡节点重新恢复平衡。换句话说,<strong>旋转操作既能保持“二叉搜索树”的性质,也能使树重新变为“平衡二叉树”</strong>。</p>
|
||||
<p>我们将平衡因子绝对值 <span class="arithmatex">\(> 1\)</span> 的节点称为“失衡节点”。根据节点失衡情况的不同,旋转操作分为四种:右旋、左旋、先右旋后左旋、先左旋后右旋。下面我们将详细介绍这些旋转操作。</p>
|
||||
<h3 id="1_1">1. 右旋<a class="headerlink" href="#1_1" title="Permanent link">¶</a></h3>
|
||||
<p>如下图所示,节点下方为平衡因子。从底至顶看,二叉树中首个失衡节点是“节点 3”。我们关注以该失衡节点为根节点的子树,将该节点记为 <code>node</code> ,其左子节点记为 <code>child</code> ,执行「右旋」操作。完成右旋后,子树已经恢复平衡,并且仍然保持二叉搜索树的特性。</p>
|
||||
<p>如下图所示,节点下方为平衡因子。从底至顶看,二叉树中首个失衡节点是“节点 3”。我们关注以该失衡节点为根节点的子树,将该节点记为 <code>node</code> ,其左子节点记为 <code>child</code> ,执行“右旋”操作。完成右旋后,子树已经恢复平衡,并且仍然保持二叉搜索树的特性。</p>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="4:4"><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" /><div class="tabbed-labels"><label for="__tabbed_4_1"><1></label><label for="__tabbed_4_2"><2></label><label for="__tabbed_4_3"><3></label><label for="__tabbed_4_4"><4></label></div>
|
||||
<div class="tabbed-content">
|
||||
<div class="tabbed-block">
|
||||
@@ -4142,7 +4142,7 @@
|
||||
</div>
|
||||
<p align="center"> 图:右旋操作步骤 </p>
|
||||
|
||||
<p>此外,如果节点 <code>child</code> 本身有右子节点(记为 <code>grandChild</code> ),则需要在「右旋」中添加一步:将 <code>grandChild</code> 作为 <code>node</code> 的左子节点。</p>
|
||||
<p>此外,如果节点 <code>child</code> 本身有右子节点(记为 <code>grandChild</code> ),则需要在右旋中添加一步:将 <code>grandChild</code> 作为 <code>node</code> 的左子节点。</p>
|
||||
<p><img alt="有 grandChild 的右旋操作" src="../avl_tree.assets/avltree_right_rotate_with_grandchild.png" /></p>
|
||||
<p align="center"> 图:有 grandChild 的右旋操作 </p>
|
||||
|
||||
@@ -4349,15 +4349,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<h3 id="2_1">2. 左旋<a class="headerlink" href="#2_1" title="Permanent link">¶</a></h3>
|
||||
<p>相应的,如果考虑上述失衡二叉树的“镜像”,则需要执行「左旋」操作。</p>
|
||||
<p>相应的,如果考虑上述失衡二叉树的“镜像”,则需要执行“左旋”操作。</p>
|
||||
<p><img alt="左旋操作" src="../avl_tree.assets/avltree_left_rotate.png" /></p>
|
||||
<p align="center"> 图:左旋操作 </p>
|
||||
|
||||
<p>同理,若节点 <code>child</code> 本身有左子节点(记为 <code>grandChild</code> ),则需要在「左旋」中添加一步:将 <code>grandChild</code> 作为 <code>node</code> 的右子节点。</p>
|
||||
<p>同理,若节点 <code>child</code> 本身有左子节点(记为 <code>grandChild</code> ),则需要在左旋中添加一步:将 <code>grandChild</code> 作为 <code>node</code> 的右子节点。</p>
|
||||
<p><img alt="有 grandChild 的左旋操作" src="../avl_tree.assets/avltree_left_rotate_with_grandchild.png" /></p>
|
||||
<p align="center"> 图:有 grandChild 的左旋操作 </p>
|
||||
|
||||
<p>可以观察到,<strong>右旋和左旋操作在逻辑上是镜像对称的,它们分别解决的两种失衡情况也是对称的</strong>。基于对称性,我们可以轻松地从右旋的代码推导出左旋的代码。具体地,只需将「右旋」代码中的把所有的 <code>left</code> 替换为 <code>right</code> ,将所有的 <code>right</code> 替换为 <code>left</code> ,即可得到「左旋」代码。</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:12"><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" /><input id="__tabbed_6_11" name="__tabbed_6" type="radio" /><input id="__tabbed_6_12" 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">JS</label><label for="__tabbed_6_6">TS</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><label for="__tabbed_6_11">Dart</label><label for="__tabbed_6_12">Rust</label></div>
|
||||
<div class="tabbed-content">
|
||||
<div class="tabbed-block">
|
||||
@@ -4560,12 +4560,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<h3 id="3">3. 先左旋后右旋<a class="headerlink" href="#3" title="Permanent link">¶</a></h3>
|
||||
<p>对于下图中的失衡节点 3,仅使用左旋或右旋都无法使子树恢复平衡。此时需要先左旋后右旋,即先对 <code>child</code> 执行「左旋」,再对 <code>node</code> 执行「右旋」。</p>
|
||||
<p>对于下图中的失衡节点 3,仅使用左旋或右旋都无法使子树恢复平衡。此时需要先左旋后右旋,即先对 <code>child</code> 执行“左旋”,再对 <code>node</code> 执行“右旋”。</p>
|
||||
<p><img alt="先左旋后右旋" src="../avl_tree.assets/avltree_left_right_rotate.png" /></p>
|
||||
<p align="center"> 图:先左旋后右旋 </p>
|
||||
|
||||
<h3 id="4">4. 先右旋后左旋<a class="headerlink" href="#4" title="Permanent link">¶</a></h3>
|
||||
<p>同理,对于上述失衡二叉树的镜像情况,需要先右旋后左旋,即先对 <code>child</code> 执行「右旋」,然后对 <code>node</code> 执行「左旋」。</p>
|
||||
<p>同理,对于上述失衡二叉树的镜像情况,需要先右旋后左旋,即先对 <code>child</code> 执行“右旋”,然后对 <code>node</code> 执行“左旋”。</p>
|
||||
<p><img alt="先右旋后左旋" src="../avl_tree.assets/avltree_right_left_rotate.png" /></p>
|
||||
<p align="center"> 图:先右旋后左旋 </p>
|
||||
|
||||
@@ -5002,7 +5002,7 @@
|
||||
</div>
|
||||
<h2 id="753-avl">7.5.3 AVL 树常用操作<a class="headerlink" href="#753-avl" title="Permanent link">¶</a></h2>
|
||||
<h3 id="1_2">1. 插入节点<a class="headerlink" href="#1_2" title="Permanent link">¶</a></h3>
|
||||
<p>「AVL 树」的节点插入操作与「二叉搜索树」在主体上类似。唯一的区别在于,在 AVL 树中插入节点后,从该节点到根节点的路径上可能会出现一系列失衡节点。因此,<strong>我们需要从这个节点开始,自底向上执行旋转操作,使所有失衡节点恢复平衡</strong>。</p>
|
||||
<p>AVL 树的节点插入操作与二叉搜索树在主体上类似。唯一的区别在于,在 AVL 树中插入节点后,从该节点到根节点的路径上可能会出现一系列失衡节点。因此,<strong>我们需要从这个节点开始,自底向上执行旋转操作,使所有失衡节点恢复平衡</strong>。</p>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="8:12"><input checked="checked" id="__tabbed_8_1" name="__tabbed_8" type="radio" /><input id="__tabbed_8_2" name="__tabbed_8" type="radio" /><input id="__tabbed_8_3" name="__tabbed_8" type="radio" /><input id="__tabbed_8_4" name="__tabbed_8" type="radio" /><input id="__tabbed_8_5" name="__tabbed_8" type="radio" /><input id="__tabbed_8_6" name="__tabbed_8" type="radio" /><input id="__tabbed_8_7" name="__tabbed_8" type="radio" /><input id="__tabbed_8_8" name="__tabbed_8" type="radio" /><input id="__tabbed_8_9" name="__tabbed_8" type="radio" /><input id="__tabbed_8_10" name="__tabbed_8" type="radio" /><input id="__tabbed_8_11" name="__tabbed_8" type="radio" /><input id="__tabbed_8_12" name="__tabbed_8" type="radio" /><div class="tabbed-labels"><label for="__tabbed_8_1">Java</label><label for="__tabbed_8_2">C++</label><label for="__tabbed_8_3">Python</label><label for="__tabbed_8_4">Go</label><label for="__tabbed_8_5">JS</label><label for="__tabbed_8_6">TS</label><label for="__tabbed_8_7">C</label><label for="__tabbed_8_8">C#</label><label for="__tabbed_8_9">Swift</label><label for="__tabbed_8_10">Zig</label><label for="__tabbed_8_11">Dart</label><label for="__tabbed_8_12">Rust</label></div>
|
||||
<div class="tabbed-content">
|
||||
<div class="tabbed-block">
|
||||
|
||||
@@ -3494,7 +3494,7 @@
|
||||
|
||||
|
||||
<h1 id="74">7.4 二叉搜索树<a class="headerlink" href="#74" title="Permanent link">¶</a></h1>
|
||||
<p>「二叉搜索树 Binary Search Tree」满足以下条件:</p>
|
||||
<p>「二叉搜索树 binary search tree」满足以下条件:</p>
|
||||
<ol>
|
||||
<li>对于根节点,左子树中所有节点的值 <span class="arithmatex">\(<\)</span> 根节点的值 <span class="arithmatex">\(<\)</span> 右子树中所有节点的值。</li>
|
||||
<li>任意节点的左、右子树也是二叉搜索树,即同样满足条件 <code>1.</code> 。</li>
|
||||
|
||||
@@ -3508,7 +3508,7 @@
|
||||
|
||||
|
||||
<h1 id="71">7.1 二叉树<a class="headerlink" href="#71" title="Permanent link">¶</a></h1>
|
||||
<p>「二叉树 Binary Tree」是一种非线性数据结构,代表着祖先与后代之间的派生关系,体现着“一分为二”的分治逻辑。与链表类似,二叉树的基本单元是节点,每个节点包含:值、左子节点引用、右子节点引用。</p>
|
||||
<p>「二叉树 binary tree」是一种非线性数据结构,代表着祖先与后代之间的派生关系,体现着“一分为二”的分治逻辑。与链表类似,二叉树的基本单元是节点,每个节点包含:值、左子节点引用、右子节点引用。</p>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="1:12"><input checked="checked" id="__tabbed_1_1" name="__tabbed_1" type="radio" /><input id="__tabbed_1_2" name="__tabbed_1" type="radio" /><input id="__tabbed_1_3" name="__tabbed_1" type="radio" /><input id="__tabbed_1_4" name="__tabbed_1" type="radio" /><input id="__tabbed_1_5" name="__tabbed_1" type="radio" /><input id="__tabbed_1_6" name="__tabbed_1" type="radio" /><input id="__tabbed_1_7" name="__tabbed_1" type="radio" /><input id="__tabbed_1_8" name="__tabbed_1" type="radio" /><input id="__tabbed_1_9" name="__tabbed_1" type="radio" /><input id="__tabbed_1_10" name="__tabbed_1" type="radio" /><input id="__tabbed_1_11" name="__tabbed_1" type="radio" /><input id="__tabbed_1_12" name="__tabbed_1" type="radio" /><div class="tabbed-labels"><label for="__tabbed_1_1">Java</label><label for="__tabbed_1_2">C++</label><label for="__tabbed_1_3">Python</label><label for="__tabbed_1_4">Go</label><label for="__tabbed_1_5">JS</label><label for="__tabbed_1_6">TS</label><label for="__tabbed_1_7">C</label><label for="__tabbed_1_8">C#</label><label for="__tabbed_1_9">Swift</label><label for="__tabbed_1_10">Zig</label><label for="__tabbed_1_11">Dart</label><label for="__tabbed_1_12">Rust</label></div>
|
||||
<div class="tabbed-content">
|
||||
<div class="tabbed-block">
|
||||
@@ -3648,29 +3648,29 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>节点的两个指针分别指向「左子节点」和「右子节点」,同时该节点被称为这两个子节点的「父节点」。当给定一个二叉树的节点时,我们将该节点的左子节点及其以下节点形成的树称为该节点的「左子树」,同理可得「右子树」。</p>
|
||||
<p><strong>在二叉树中,除叶节点外,其他所有节点都包含子节点和非空子树</strong>。例如,在以下示例中,若将“节点 2”视为父节点,则其左子节点和右子节点分别是“节点 4”和“节点 5”,左子树是“节点 4 及其以下节点形成的树”,右子树是“节点 5 及其以下节点形成的树”。</p>
|
||||
<p>每个节点都有两个引用(指针),分别指向「左子节点 left-child node」和「右子节点 right-child node」,该节点被称为这两个子节点的「父节点 parent node」。当给定一个二叉树的节点时,我们将该节点的左子节点及其以下节点形成的树称为该节点的「左子树 left subtree」,同理可得「右子树 right subtree」。</p>
|
||||
<p><strong>在二叉树中,除叶节点外,其他所有节点都包含子节点和非空子树</strong>。在以下示例中,若将“节点 2”视为父节点,则其左子节点和右子节点分别是“节点 4”和“节点 5”,左子树是“节点 4 及其以下节点形成的树”,右子树是“节点 5 及其以下节点形成的树”。</p>
|
||||
<p><img alt="父节点、子节点、子树" src="../binary_tree.assets/binary_tree_definition.png" /></p>
|
||||
<p align="center"> 图:父节点、子节点、子树 </p>
|
||||
|
||||
<h2 id="711">7.1.1 二叉树常见术语<a class="headerlink" href="#711" title="Permanent link">¶</a></h2>
|
||||
<p>二叉树涉及的术语较多,建议尽量理解并记住。</p>
|
||||
<ul>
|
||||
<li>「根节点 Root Node」:位于二叉树顶层的节点,没有父节点。</li>
|
||||
<li>「叶节点 Leaf Node」:没有子节点的节点,其两个指针均指向 <span class="arithmatex">\(\text{None}\)</span> 。</li>
|
||||
<li>节点的「层 Level」:从顶至底递增,根节点所在层为 1 。</li>
|
||||
<li>节点的「度 Degree」:节点的子节点的数量。在二叉树中,度的范围是 0, 1, 2 。</li>
|
||||
<li>「边 Edge」:连接两个节点的线段,即节点指针。</li>
|
||||
<li>二叉树的「高度」:从根节点到最远叶节点所经过的边的数量。</li>
|
||||
<li>节点的「深度 Depth」 :从根节点到该节点所经过的边的数量。</li>
|
||||
<li>节点的「高度 Height」:从最远叶节点到该节点所经过的边的数量。</li>
|
||||
<li>「根节点 root node」:位于二叉树顶层的节点,没有父节点。</li>
|
||||
<li>「叶节点 leaf node」:没有子节点的节点,其两个指针均指向 <span class="arithmatex">\(\text{None}\)</span> 。</li>
|
||||
<li>「边 edge」:连接两个节点的线段,即节点引用(指针)。</li>
|
||||
<li>节点所在的「层 level」:从顶至底递增,根节点所在层为 1 。</li>
|
||||
<li>节点的「度 degree」:节点的子节点的数量。在二叉树中,度的取值范围是 0, 1, 2 。</li>
|
||||
<li>二叉树的「高度 height」:从根节点到最远叶节点所经过的边的数量。</li>
|
||||
<li>节点的「深度 depth」 :从根节点到该节点所经过的边的数量。</li>
|
||||
<li>节点的「高度 height」:从最远叶节点到该节点所经过的边的数量。</li>
|
||||
</ul>
|
||||
<p><img alt="二叉树的常用术语" src="../binary_tree.assets/binary_tree_terminology.png" /></p>
|
||||
<p align="center"> 图:二叉树的常用术语 </p>
|
||||
|
||||
<div class="admonition tip">
|
||||
<p class="admonition-title">高度与深度的定义</p>
|
||||
<p>请注意,我们通常将「高度」和「深度」定义为“走过边的数量”,但有些题目或教材可能会将其定义为“走过节点的数量”。在这种情况下,高度和深度都需要加 1 。</p>
|
||||
<p>请注意,我们通常将“高度”和“深度”定义为“走过边的数量”,但有些题目或教材可能会将其定义为“走过节点的数量”。在这种情况下,高度和深度都需要加 1 。</p>
|
||||
</div>
|
||||
<h2 id="712">7.1.2 二叉树基本操作<a class="headerlink" href="#712" title="Permanent link">¶</a></h2>
|
||||
<p><strong>初始化二叉树</strong>。与链表类似,首先初始化节点,然后构建引用指向(即指针)。</p>
|
||||
@@ -3954,31 +3954,31 @@
|
||||
</div>
|
||||
<h2 id="713">7.1.3 常见二叉树类型<a class="headerlink" href="#713" title="Permanent link">¶</a></h2>
|
||||
<h3 id="1">1. 完美二叉树<a class="headerlink" href="#1" title="Permanent link">¶</a></h3>
|
||||
<p>「完美二叉树 Perfect Binary Tree」除了最底层外,其余所有层的节点都被完全填满。在完美二叉树中,叶节点的度为 <span class="arithmatex">\(0\)</span> ,其余所有节点的度都为 <span class="arithmatex">\(2\)</span> ;若树高度为 <span class="arithmatex">\(h\)</span> ,则节点总数为 <span class="arithmatex">\(2^{h+1} - 1\)</span> ,呈现标准的指数级关系,反映了自然界中常见的细胞分裂现象。</p>
|
||||
<p>「完美二叉树 perfect binary tree」除了最底层外,其余所有层的节点都被完全填满。在完美二叉树中,叶节点的度为 <span class="arithmatex">\(0\)</span> ,其余所有节点的度都为 <span class="arithmatex">\(2\)</span> ;若树高度为 <span class="arithmatex">\(h\)</span> ,则节点总数为 <span class="arithmatex">\(2^{h+1} - 1\)</span> ,呈现标准的指数级关系,反映了自然界中常见的细胞分裂现象。</p>
|
||||
<div class="admonition tip">
|
||||
<p class="admonition-title">Tip</p>
|
||||
<p>在中文社区中,完美二叉树常被称为「满二叉树」,请注意区分。</p>
|
||||
<p>请注意,在中文社区中,完美二叉树常被称为「满二叉树」。</p>
|
||||
</div>
|
||||
<p><img alt="完美二叉树" src="../binary_tree.assets/perfect_binary_tree.png" /></p>
|
||||
<p align="center"> 图:完美二叉树 </p>
|
||||
|
||||
<h3 id="2">2. 完全二叉树<a class="headerlink" href="#2" title="Permanent link">¶</a></h3>
|
||||
<p>「完全二叉树 Complete Binary Tree」只有最底层的节点未被填满,且最底层节点尽量靠左填充。</p>
|
||||
<p>「完全二叉树 complete binary tree」只有最底层的节点未被填满,且最底层节点尽量靠左填充。</p>
|
||||
<p><img alt="完全二叉树" src="../binary_tree.assets/complete_binary_tree.png" /></p>
|
||||
<p align="center"> 图:完全二叉树 </p>
|
||||
|
||||
<h3 id="3">3. 完满二叉树<a class="headerlink" href="#3" title="Permanent link">¶</a></h3>
|
||||
<p>「完满二叉树 Full Binary Tree」除了叶节点之外,其余所有节点都有两个子节点。</p>
|
||||
<p>「完满二叉树 full binary tree」除了叶节点之外,其余所有节点都有两个子节点。</p>
|
||||
<p><img alt="完满二叉树" src="../binary_tree.assets/full_binary_tree.png" /></p>
|
||||
<p align="center"> 图:完满二叉树 </p>
|
||||
|
||||
<h3 id="4">4. 平衡二叉树<a class="headerlink" href="#4" title="Permanent link">¶</a></h3>
|
||||
<p>「平衡二叉树 Balanced Binary Tree」中任意节点的左子树和右子树的高度之差的绝对值不超过 1 。</p>
|
||||
<p>「平衡二叉树 balanced binary tree」中任意节点的左子树和右子树的高度之差的绝对值不超过 1 。</p>
|
||||
<p><img alt="平衡二叉树" src="../binary_tree.assets/balanced_binary_tree.png" /></p>
|
||||
<p align="center"> 图:平衡二叉树 </p>
|
||||
|
||||
<h2 id="714">7.1.4 二叉树的退化<a class="headerlink" href="#714" title="Permanent link">¶</a></h2>
|
||||
<p>当二叉树的每层节点都被填满时,达到「完美二叉树」;而当所有节点都偏向一侧时,二叉树退化为「链表」。</p>
|
||||
<p>当二叉树的每层节点都被填满时,达到“完美二叉树”;而当所有节点都偏向一侧时,二叉树退化为“链表”。</p>
|
||||
<ul>
|
||||
<li>完美二叉树是理想情况,可以充分发挥二叉树“分治”的优势。</li>
|
||||
<li>链表则是另一个极端,各项操作都变为线性操作,时间复杂度退化至 <span class="arithmatex">\(O(n)\)</span> 。</li>
|
||||
|
||||
@@ -3415,12 +3415,12 @@
|
||||
<p>从物理结构的角度来看,树是一种基于链表的数据结构,因此其遍历方式是通过指针逐个访问节点。然而,树是一种非线性数据结构,这使得遍历树比遍历链表更加复杂,需要借助搜索算法来实现。</p>
|
||||
<p>二叉树常见的遍历方式包括层序遍历、前序遍历、中序遍历和后序遍历等。</p>
|
||||
<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>「层序遍历 level-order traversal」从顶部到底部逐层遍历二叉树,并在每一层按照从左到右的顺序访问节点。</p>
|
||||
<p>层序遍历本质上属于「广度优先遍历 breadth-first traversal」,它体现了一种“一圈一圈向外扩展”的逐层遍历方式。</p>
|
||||
<p><img alt="二叉树的层序遍历" src="../binary_tree_traversal.assets/binary_tree_bfs.png" /></p>
|
||||
<p align="center"> 图:二叉树的层序遍历 </p>
|
||||
|
||||
<p>广度优先遍历通常借助「队列」来实现。队列遵循“先进先出”的规则,而广度优先遍历则遵循“逐层推进”的规则,两者背后的思想是一致的。</p>
|
||||
<p>广度优先遍历通常借助“队列”来实现。队列遵循“先进先出”的规则,而广度优先遍历则遵循“逐层推进”的规则,两者背后的思想是一致的。</p>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="1:12"><input checked="checked" id="__tabbed_1_1" name="__tabbed_1" type="radio" /><input id="__tabbed_1_2" name="__tabbed_1" type="radio" /><input id="__tabbed_1_3" name="__tabbed_1" type="radio" /><input id="__tabbed_1_4" name="__tabbed_1" type="radio" /><input id="__tabbed_1_5" name="__tabbed_1" type="radio" /><input id="__tabbed_1_6" name="__tabbed_1" type="radio" /><input id="__tabbed_1_7" name="__tabbed_1" type="radio" /><input id="__tabbed_1_8" name="__tabbed_1" type="radio" /><input id="__tabbed_1_9" name="__tabbed_1" type="radio" /><input id="__tabbed_1_10" name="__tabbed_1" type="radio" /><input id="__tabbed_1_11" name="__tabbed_1" type="radio" /><input id="__tabbed_1_12" name="__tabbed_1" type="radio" /><div class="tabbed-labels"><label for="__tabbed_1_1">Java</label><label for="__tabbed_1_2">C++</label><label for="__tabbed_1_3">Python</label><label for="__tabbed_1_4">Go</label><label for="__tabbed_1_5">JS</label><label for="__tabbed_1_6">TS</label><label for="__tabbed_1_7">C</label><label for="__tabbed_1_8">C#</label><label for="__tabbed_1_9">Swift</label><label for="__tabbed_1_10">Zig</label><label for="__tabbed_1_11">Dart</label><label for="__tabbed_1_12">Rust</label></div>
|
||||
<div class="tabbed-content">
|
||||
<div class="tabbed-block">
|
||||
@@ -3706,7 +3706,7 @@
|
||||
<p><strong>时间复杂度</strong>:所有节点被访问一次,使用 <span class="arithmatex">\(O(n)\)</span> 时间,其中 <span class="arithmatex">\(n\)</span> 为节点数量。</p>
|
||||
<p><strong>空间复杂度</strong>:在最差情况下,即满二叉树时,遍历到最底层之前,队列中最多同时存在 <span class="arithmatex">\(\frac{n + 1}{2}\)</span> 个节点,占用 <span class="arithmatex">\(O(n)\)</span> 空间。</p>
|
||||
<h2 id="722">7.2.2 前序、中序、后序遍历<a class="headerlink" href="#722" title="Permanent link">¶</a></h2>
|
||||
<p>相应地,前序、中序和后序遍历都属于「深度优先遍历 Depth-First Traversal」,它体现了一种“先走到尽头,再回溯继续”的遍历方式。</p>
|
||||
<p>相应地,前序、中序和后序遍历都属于「深度优先遍历 depth-first traversal」,它体现了一种“先走到尽头,再回溯继续”的遍历方式。</p>
|
||||
<p>如下图所示,左侧是深度优先遍历的示意图,右上方是对应的递归代码。深度优先遍历就像是绕着整个二叉树的外围“走”一圈,在这个过程中,在每个节点都会遇到三个位置,分别对应前序遍历、中序遍历和后序遍历。</p>
|
||||
<p><img alt="二叉搜索树的前、中、后序遍历" src="../binary_tree_traversal.assets/binary_tree_dfs.png" /></p>
|
||||
<p align="center"> 图:二叉搜索树的前、中、后序遍历 </p>
|
||||
|
||||
Reference in New Issue
Block a user