This commit is contained in:
krahets
2023-04-10 23:59:39 +08:00
parent 9393f5957c
commit df51fb3ba3
13 changed files with 285 additions and 387 deletions

View File

@@ -1839,7 +1839,7 @@
<p>「二叉搜索树 Binary Search Tree」满足以下条件</p>
<ol>
<li>对于根节点,左子树中所有节点的值 <span class="arithmatex">\(&lt;\)</span> 根节点的值 <span class="arithmatex">\(&lt;\)</span> 右子树中所有节点的值;</li>
<li>任意节点的左子树和右子树也是二叉搜索树,即满足条件 <code>1.</code> </li>
<li>任意节点的左右子树也是二叉搜索树,即同样满足条件 <code>1.</code> </li>
</ol>
<p><img alt="二叉搜索树" src="../binary_search_tree.assets/binary_search_tree.png" /></p>
<p align="center"> Fig. 二叉搜索树 </p>
@@ -1850,12 +1850,12 @@
<ul>
<li><code>cur.val &lt; num</code> ,说明目标节点在 <code>cur</code> 的右子树中,因此执行 <code>cur = cur.right</code> </li>
<li><code>cur.val &gt; num</code> ,说明目标节点在 <code>cur</code> 的左子树中,因此执行 <code>cur = cur.left</code> </li>
<li><code>cur.val = num</code> ,说明找到目标节点,跳出循环并返回该节点即可</li>
<li><code>cur.val = num</code> ,说明找到目标节点,跳出循环并返回该节点;</li>
</ul>
<div class="tabbed-set tabbed-alternate" data-tabs="1:4"><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" /><div class="tabbed-labels"><label for="__tabbed_1_1">&lt;1&gt;</label><label for="__tabbed_1_2">&lt;2&gt;</label><label for="__tabbed_1_3">&lt;3&gt;</label><label for="__tabbed_1_4">&lt;4&gt;</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
<p><img alt="查找节点步骤" src="../binary_search_tree.assets/bst_search_step1.png" /></p>
<p><img alt="bst_search_step1" src="../binary_search_tree.assets/bst_search_step1.png" /></p>
</div>
<div class="tabbed-block">
<p><img alt="bst_search_step2" src="../binary_search_tree.assets/bst_search_step2.png" /></p>
@@ -1868,7 +1868,7 @@
</div>
</div>
</div>
<p>二叉搜索树的查找操作二分查找算法如出一辙,也是在每轮排除一半情况。循环次数最多为二叉树的高度,当二叉树平衡时,使用 <span class="arithmatex">\(O(\log n)\)</span> 时间。</p>
<p>二叉搜索树的查找操作二分查找算法的工作原理一致,都是每轮排除一半情况。循环次数最多为二叉树的高度,当二叉树平衡时,使用 <span class="arithmatex">\(O(\log n)\)</span> 时间。</p>
<div class="tabbed-set tabbed-alternate" data-tabs="2:10"><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" /><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">JavaScript</label><label for="__tabbed_2_6">TypeScript</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></div>
<div class="tabbed-content">
<div class="tabbed-block">
@@ -2059,10 +2059,10 @@
<h3 id="_2">插入节点<a class="headerlink" href="#_2" title="Permanent link">&para;</a></h3>
<p>给定一个待插入元素 <code>num</code> ,为了保持二叉搜索树“左子树 &lt; 根节点 &lt; 右子树”的性质,插入操作分为两步:</p>
<ol>
<li><strong>查找插入位置</strong>:与查找操作似,我们从根节点出发,根据当前节点值和 <code>num</code> 的大小关系循环向下搜索,直到越过叶节点(遍历 <span class="arithmatex">\(\text{null}\)</span> )时跳出循环;</li>
<li><strong>在该位置插入节点</strong>:初始化节点 <code>num</code> ,将该节点放到 <span class="arithmatex">\(\text{null}\)</span> 的位置 </li>
<li><strong>查找插入位置</strong>:与查找操作似,从根节点出发,根据当前节点值和 <code>num</code> 的大小关系循环向下搜索,直到越过叶节点(遍历 <span class="arithmatex">\(\text{null}\)</span> )时跳出循环;</li>
<li><strong>在该位置插入节点</strong>:初始化节点 <code>num</code> ,将该节点置于 <span class="arithmatex">\(\text{null}\)</span> 的位置;</li>
</ol>
<p>二叉搜索树不允许存在重复节点,否则将会违背其定义。因此若待插入节点在树中已存在,则不执行插入,直接返回即可</p>
<p>二叉搜索树不允许存在重复节点,否则将违反其定义。因此若待插入节点在树中已存在,则不执行插入,直接返回。</p>
<p><img alt="在二叉搜索树中插入节点" src="../binary_search_tree.assets/bst_insert.png" /></p>
<p align="center"> Fig. 在二叉搜索树中插入节点 </p>
@@ -2339,28 +2339,28 @@
</div>
</div>
</div>
<p>为了插入节点,需要借助 <strong>辅助节点 <code>pre</code></strong> 保存上一轮循环的节点,这样在遍历 <span class="arithmatex">\(\text{null}\)</span> 时,我们可以获取到其父节点,从而完成节点插入操作。</p>
<p>为了插入节点,我们需要利用辅助节点 <code>pre</code> 保存上一轮循环的节点,这样在遍历 <span class="arithmatex">\(\text{null}\)</span> 时,我们可以获取到其父节点,从而完成节点插入操作。</p>
<p>与查找节点相同,插入节点使用 <span class="arithmatex">\(O(\log n)\)</span> 时间。</p>
<h3 id="_3">删除节点<a class="headerlink" href="#_3" title="Permanent link">&para;</a></h3>
<p>与插入节点一样,我们需要在删除操作后维持二叉搜索树的“左子树 &lt; 根节点 &lt; 右子树”的性质。首先,我们需要在二叉树中执行查找操作,获取待删除节点。接下来,根据待删除节点的子节点数量,删除操作需分为三种情况:</p>
<p><strong>当待删除节点的子节点数量 <span class="arithmatex">\(= 0\)</span></strong>,表待删除节点是叶节点,直接删除即可</p>
<p>与插入节点类似,我们需要在删除操作后维持二叉搜索树的“左子树 &lt; 根节点 &lt; 右子树”的性质。首先,我们需要在二叉树中执行查找操作,获取待删除节点。接下来,根据待删除节点的子节点数量,删除操作需分为三种情况:</p>
<p>当待删除节点的子节点数量 <span class="arithmatex">\(= 0\)</span> 时,表待删除节点是叶节点,可以直接删除。</p>
<p><img alt="在二叉搜索树中删除节点(度为 0" src="../binary_search_tree.assets/bst_remove_case1.png" /></p>
<p align="center"> Fig. 在二叉搜索树中删除节点(度为 0 </p>
<p><strong>当待删除节点的子节点数量 <span class="arithmatex">\(= 1\)</span></strong>,将待删除节点替换为其子节点即可。</p>
<p>当待删除节点的子节点数量 <span class="arithmatex">\(= 1\)</span> 时,将待删除节点替换为其子节点即可。</p>
<p><img alt="在二叉搜索树中删除节点(度为 1" src="../binary_search_tree.assets/bst_remove_case2.png" /></p>
<p align="center"> Fig. 在二叉搜索树中删除节点(度为 1 </p>
<p><strong>当待删除节点的子节点数量 <span class="arithmatex">\(= 2\)</span></strong>,删除操作分为三步:</p>
<p>当待删除节点的子节点数量 <span class="arithmatex">\(= 2\)</span> 时,删除操作分为三步:</p>
<ol>
<li>找到待删除节点在 <strong>中序遍历序列</strong> 中的下一个节点,记为 <code>nex</code> </li>
<li>找到待删除节点在“中序遍历序列”中的下一个节点,记为 nex</li>
<li>在树中递归删除节点 <code>nex</code> </li>
<li>使用 <code>nex</code> 替换待删除节点;</li>
</ol>
<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">&lt;1&gt;</label><label for="__tabbed_4_2">&lt;2&gt;</label><label for="__tabbed_4_3">&lt;3&gt;</label><label for="__tabbed_4_4">&lt;4&gt;</label></div>
<div class="tabbed-content">
<div class="tabbed-block">
<p><img alt="删除节点(度为 2步骤" src="../binary_search_tree.assets/bst_remove_case3_step1.png" /></p>
<p><img alt="bst_remove_case3_step1" src="../binary_search_tree.assets/bst_remove_case3_step1.png" /></p>
</div>
<div class="tabbed-block">
<p><img alt="bst_remove_case3_step2" src="../binary_search_tree.assets/bst_remove_case3_step2.png" /></p>
@@ -2373,7 +2373,7 @@
</div>
</div>
</div>
<p>删除节点操作使用 <span class="arithmatex">\(O(\log n)\)</span> 时间,其中查找待删除节点 <span class="arithmatex">\(O(\log n)\)</span> ,获取中序遍历后继节点 <span class="arithmatex">\(O(\log n)\)</span></p>
<p>删除节点操作同样使用 <span class="arithmatex">\(O(\log n)\)</span> 时间,其中查找待删除节点需要 <span class="arithmatex">\(O(\log n)\)</span> 时间,获取中序遍历后继节点需要 <span class="arithmatex">\(O(\log n)\)</span> 时间</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">
<div class="tabbed-block">
@@ -2912,27 +2912,27 @@
</div>
</div>
<h3 id="_4">排序<a class="headerlink" href="#_4" title="Permanent link">&para;</a></h3>
<p>我们知道,中序遍历遵循“左 <span class="arithmatex">\(\rightarrow\)</span><span class="arithmatex">\(\rightarrow\)</span> 右”的遍历优先级,而二叉搜索树遵循“左子节点 <span class="arithmatex">\(&lt;\)</span> 根节点 <span class="arithmatex">\(&lt;\)</span> 右子节点”的大小关系。因此,在二叉搜索树中进行中序遍历时,总是会优先遍历下一个最小节点,从而得出一重要性质:<strong>二叉搜索树的中序遍历序列是升序的</strong></p>
<p>借助中序遍历升序的性质,我们在二叉搜索树中获取有序数据仅需 <span class="arithmatex">\(O(n)\)</span> 时间,无需额外排序,非常高效。</p>
<p>我们知道,二叉树的中序遍历遵循“左 <span class="arithmatex">\(\rightarrow\)</span><span class="arithmatex">\(\rightarrow\)</span> 右”的遍历顺序,而二叉搜索树满足“左子节点 <span class="arithmatex">\(&lt;\)</span> 根节点 <span class="arithmatex">\(&lt;\)</span> 右子节点”的大小关系。因此,在二叉搜索树中进行中序遍历时,总是会优先遍历下一个最小节点,从而得出一重要性质:<strong>二叉搜索树的中序遍历序列是升序的</strong></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"> Fig. 二叉搜索树的中序遍历序列 </p>
<h2 id="732">7.3.2. &nbsp; 二叉搜索树的效率<a class="headerlink" href="#732" title="Permanent link">&para;</a></h2>
<p>假设给定 <span class="arithmatex">\(n\)</span> 个数字,最常的存储方式是「数组」,那么对于这串乱序的数字,常见操作的效率</p>
<p>假设给定 <span class="arithmatex">\(n\)</span> 个数字,最常的存储方式是「数组」对于这串乱序的数字,常见操作的效率如下</p>
<ul>
<li><strong>查找元素</strong>:由于数组是无序的,因此需要遍历数组来确定,使用 <span class="arithmatex">\(O(n)\)</span> 时间;</li>
<li><strong>插入元素</strong>:只需将元素添加至数组尾部即可,使用 <span class="arithmatex">\(O(1)\)</span> 时间;</li>
<li><strong>删除元素</strong>:先查找元素,使用 <span class="arithmatex">\(O(n)\)</span> 时间,再在数组中删除该元素,使用 <span class="arithmatex">\(O(n)\)</span> 时间;</li>
<li><strong>获取最小 / 最大元素</strong>:需要遍历数组来确定,使用 <span class="arithmatex">\(O(n)\)</span> 时间;</li>
</ul>
<p>为了得先验信息,我们可以预先将数组元素进行排序,得到一个「排序数组」此时操作效率</p>
<p>为了得先验信息,我们可以预先将数组元素进行排序,得到一个「排序数组」此时操作效率如下</p>
<ul>
<li><strong>查找元素</strong>:由于数组已排序,可以使用二分查找,平均使用 <span class="arithmatex">\(O(\log n)\)</span> 时间;</li>
<li><strong>插入元素</strong>:先查找插入位置,使用 <span class="arithmatex">\(O(\log n)\)</span> 时间,再插入到指定位置,使用 <span class="arithmatex">\(O(n)\)</span> 时间;</li>
<li><strong>删除元素</strong>:先查找元素,使用 <span class="arithmatex">\(O(\log n)\)</span> 时间,再在数组中删除该元素,使用 <span class="arithmatex">\(O(n)\)</span> 时间;</li>
<li><strong>获取最小 / 最大元素</strong>:数组头部和尾部元素即是最小和最大元素,使用 <span class="arithmatex">\(O(1)\)</span> 时间;</li>
</ul>
<p>观察发现,无序数组和有序数组中的各项操作的时间复杂度“偏科”的,即有的快有的慢<strong>二叉搜索树的各项操作的时间复杂度都是对数阶,在数据量 <span class="arithmatex">\(n\)</span> 大时有巨大优势</strong></p>
<p>观察可知,无序数组和有序数组中的各项操作的时间复杂度呈现“偏科”的特点,即有的快有的慢<strong>然而,二叉搜索树的各项操作的时间复杂度都是对数阶,在数据量 <span class="arithmatex">\(n\)</span> 大时具有显著优势</strong></p>
<div class="center-table">
<table>
<thead>
@@ -2972,20 +2972,16 @@
</table>
</div>
<h2 id="733">7.3.3. &nbsp; 二叉搜索树的退化<a class="headerlink" href="#733" title="Permanent link">&para;</a></h2>
<p>理想情况下,我们希望二叉搜索树是“左右平衡”的(详见「平衡二叉树」章节),此时可以在 <span class="arithmatex">\(\log n\)</span> 轮循环内查找任意节点。</p>
<p>如果我们动态地在二叉搜索树中插入删除节点,<strong>则可能导致二叉树退化为链表</strong>,此时各种操作的时间复杂度也退化 <span class="arithmatex">\(O(n)\)</span></p>
<div class="admonition note">
<p class="admonition-title">Note</p>
<p>在实际应用中,如何保持二叉搜索树的平衡,也是一个需要重要考虑的问题。</p>
</div>
<p>理想情况下,我们希望二叉搜索树是“平衡”的,这样就可以在 <span class="arithmatex">\(\log n\)</span> 轮循环内查找任意节点。</p>
<p>然而,如果我们在二叉搜索树中不断地插入删除节点,可能导致二叉树退化为链表,这时各种操作的时间复杂度也退化 <span class="arithmatex">\(O(n)\)</span></p>
<p><img alt="二叉搜索树的平衡与退化" src="../binary_search_tree.assets/bst_degradation.png" /></p>
<p align="center"> Fig. 二叉搜索树的平衡与退化 </p>
<h2 id="734">7.3.4. &nbsp; 二叉搜索树常见应用<a class="headerlink" href="#734" title="Permanent link">&para;</a></h2>
<ul>
<li>系统中的多级索引,高效查找、插入、删除操作。</li>
<li>各种搜索算法的底层数据结构。</li>
<li>存储数据流,保持其已排序</li>
<li>用作系统中的多级索引,实现高效查找、插入、删除操作。</li>
<li>作为某些搜索算法的底层数据结构。</li>
<li>用于存储数据流,保持其有序状态</li>
</ul>