mirror of
https://github.com/krahets/hello-algo.git
synced 2026-04-13 18:00:18 +08:00
deploy
This commit is contained in:
@@ -1839,7 +1839,7 @@
|
||||
<p>「二叉搜索树 Binary Search Tree」满足以下条件:</p>
|
||||
<ol>
|
||||
<li>对于根节点,左子树中所有节点的值 <span class="arithmatex">\(<\)</span> 根节点的值 <span class="arithmatex">\(<\)</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 < 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>
|
||||
<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"><1></label><label for="__tabbed_1_2"><2></label><label for="__tabbed_1_3"><3></label><label for="__tabbed_1_4"><4></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">¶</a></h3>
|
||||
<p>给定一个待插入元素 <code>num</code> ,为了保持二叉搜索树“左子树 < 根节点 < 右子树”的性质,插入操作分为两步:</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">¶</a></h3>
|
||||
<p>与插入节点一样,我们需要在删除操作后维持二叉搜索树的“左子树 < 根节点 < 右子树”的性质。首先,我们需要在二叉树中执行查找操作,获取待删除节点。接下来,根据待删除节点的子节点数量,删除操作需要分为三种情况:</p>
|
||||
<p><strong>当待删除节点的子节点数量 <span class="arithmatex">\(= 0\)</span> 时</strong>,表明待删除节点是叶节点,直接删除即可。</p>
|
||||
<p>与插入节点类似,我们需要在删除操作后维持二叉搜索树的“左子树 < 根节点 < 右子树”的性质。首先,我们需要在二叉树中执行查找操作,获取待删除节点。接下来,根据待删除节点的子节点数量,删除操作需分为三种情况:</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"><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">
|
||||
<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">¶</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>我们知道,二叉树的中序遍历遵循“左 <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="二叉搜索树的中序遍历序列" src="../binary_search_tree.assets/bst_inorder_traversal.png" /></p>
|
||||
<p align="center"> Fig. 二叉搜索树的中序遍历序列 </p>
|
||||
|
||||
<h2 id="732">7.3.2. 二叉搜索树的效率<a class="headerlink" href="#732" title="Permanent link">¶</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. 二叉搜索树的退化<a class="headerlink" href="#733" title="Permanent link">¶</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. 二叉搜索树常见应用<a class="headerlink" href="#734" title="Permanent link">¶</a></h2>
|
||||
<ul>
|
||||
<li>系统中的多级索引,高效查找、插入、删除操作。</li>
|
||||
<li>各种搜索算法的底层数据结构。</li>
|
||||
<li>存储数据流,保持其已排序。</li>
|
||||
<li>用作系统中的多级索引,实现高效的查找、插入、删除操作。</li>
|
||||
<li>作为某些搜索算法的底层数据结构。</li>
|
||||
<li>用于存储数据流,以保持其有序状态。</li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user