This commit is contained in:
krahets
2023-08-27 00:50:10 +08:00
parent 3bb4725cbb
commit 8350023f50
12 changed files with 239 additions and 230 deletions

View File

@ -1619,14 +1619,14 @@
<li class="md-nav__item">
<a href="#821" class="md-nav__link">
8.2.1 &nbsp; 借助入堆方法实现
8.2.1 &nbsp; 自上而下构建
</a>
</li>
<li class="md-nav__item">
<a href="#822" class="md-nav__link">
8.2.2 &nbsp; 基于堆化操作实现
8.2.2 &nbsp; 自下而上构建
</a>
</li>
@ -3413,14 +3413,14 @@
<li class="md-nav__item">
<a href="#821" class="md-nav__link">
8.2.1 &nbsp; 借助入堆方法实现
8.2.1 &nbsp; 自上而下构建
</a>
</li>
<li class="md-nav__item">
<a href="#822" class="md-nav__link">
8.2.2 &nbsp; 基于堆化操作实现
8.2.2 &nbsp; 自下而上构建
</a>
</li>
@ -3457,12 +3457,21 @@
<h1 id="82">8.2 &nbsp; 建堆操作<a class="headerlink" href="#82" title="Permanent link">&para;</a></h1>
<p>在某些情况下,我们希望使用一个列表的所有元素来构建一个堆,这个过程被称为“建堆操作”。</p>
<h2 id="821">8.2.1 &nbsp; 借助入堆方法实现<a class="headerlink" href="#821" title="Permanent link">&para;</a></h2>
<p>最直接的方法是借助“元素入堆操作”实现。我们首先创建一个空堆,然后将列表元素依次执行“入堆”</p>
<p>设元素数量为 <span class="arithmatex">\(n\)</span> ,入堆操作使用 <span class="arithmatex">\(O(\log{n})\)</span> 时间,因此将所有元素入堆的时间复杂度为 <span class="arithmatex">\(O(n \log n)\)</span> </p>
<h2 id="822">8.2.2 &nbsp; 基于堆化操作实现<a class="headerlink" href="#822" title="Permanent link">&para;</a></h2>
<p>有趣的是,存在一种更高效的建堆方法,其时间复杂度可以达到 <span class="arithmatex">\(O(n)\)</span> 。我们先将列表所有元素原封不动添加到堆中,然后倒序遍历该堆,依次对每个节点执行“从顶至底堆化”。</p>
<p>请注意,因为叶节点没有子节点,所以无须堆化。在代码实现中,我们从最后一个节点的父节点开始进行堆化</p>
<h2 id="821">8.2.1 &nbsp; 自上而下构建<a class="headerlink" href="#821" title="Permanent link">&para;</a></h2>
<p>我们首先创建一个空堆,然后遍历列表,依次对每个元素执行“入堆操作”,即先将元素添加至堆的尾部,再对该元素执行“从底至顶”堆化</p>
<p>每当一个元素入堆,堆的长度就加一,因此堆是“自上而下”地构建的</p>
<p>设元素数量为 <span class="arithmatex">\(n\)</span> ,每个元素的入堆操作使用 <span class="arithmatex">\(O(\log{n})\)</span> 时间,因此该建堆方法的时间复杂度为 <span class="arithmatex">\(O(n \log n)\)</span></p>
<h2 id="822">8.2.2 &nbsp; 自下而上构建<a class="headerlink" href="#822" title="Permanent link">&para;</a></h2>
<p>实际上,我们可以实现一种更为高效的建堆方法,共分为两步</p>
<ol>
<li>将列表所有元素原封不动添加到堆中。</li>
<li>倒序遍历堆(即层序遍历的倒序),依次对每个非叶节点执行“从顶至底堆化”。</li>
</ol>
<p>在倒序遍历中,堆是“自下而上”地构建的,需要重点理解以下两点。</p>
<ul>
<li>由于叶节点没有子节点,因此无需对它们执行堆化。最后一个节点的父节点是最后一个非叶节点。</li>
<li>在倒序遍历中,我们能够保证当前节点之下的子树已经完成堆化(已经是合法的堆),而这是堆化当前节点的前置条件。</li>
</ul>
<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">
@ -3619,32 +3628,32 @@
</div>
</div>
<h2 id="823">8.2.3 &nbsp; 复杂度分析<a class="headerlink" href="#823" title="Permanent link">&para;</a></h2>
<p>为什么第二种建堆方法的时间复杂度<span class="arithmatex">\(O(n)\)</span> ?我们来展开推算一下</p>
<p>下面,我们来尝试推算第二种建堆方法的时间复杂度。</p>
<ul>
<li>完全二叉树中,设节点总数<span class="arithmatex">\(n\)</span> ,则叶节点数量为 <span class="arithmatex">\((n + 1) / 2\)</span> ,其中 <span class="arithmatex">\(/\)</span> 为向下整除。因此,在排除叶节点后,需要堆化的节点数量为 <span class="arithmatex">\((n - 1)/2\)</span> ,复杂度为 <span class="arithmatex">\(O(n)\)</span></li>
<li>在从顶至底堆化的过程中,每个节点最多堆化到叶节点,因此最大迭代次数为二叉树高度 <span class="arithmatex">\(O(\log n)\)</span></li>
<li>假设完全二叉树的节点数量<span class="arithmatex">\(n\)</span> ,则叶节点数量为 <span class="arithmatex">\((n + 1) / 2\)</span> ,其中 <span class="arithmatex">\(/\)</span> 为向下整除。因此需要堆化的节点数量为 <span class="arithmatex">\((n - 1) / 2\)</span></li>
<li>在从顶至底堆化的过程中,每个节点最多堆化到叶节点,因此最大迭代次数为二叉树高度 <span class="arithmatex">\(\log n\)</span></li>
</ul>
<p>将上述两者相乘,可得到建堆过程的时间复杂度为 <span class="arithmatex">\(O(n \log n)\)</span><strong>然而,这个估算结果并不准确,因为我们没有考虑到二叉树底层节点数量远多于顶层节点的</strong></p>
<p>接下来我们来进行更为详细的计算。为了减小计算难度,我们假设树是一个“完美二叉树”,该假设不会影响计算结果的正确性。设二叉树(即堆)节点数量为 <span class="arithmatex">\(n\)</span> 高度为 <span class="arithmatex">\(h\)</span></p>
<p>将上述两者相乘,可得到建堆过程的时间复杂度为 <span class="arithmatex">\(O(n \log n)\)</span><strong>这个估算结果并不准确,因为我们没有考虑到二叉树底层节点数量远多于顶层节点的性</strong></p>
<p>接下来我们来进行更为准确的计算。为了减小计算难度,假设给定一个节点数量为 <span class="arithmatex">\(n\)</span> ,高度为 <span class="arithmatex">\(h\)</span> 的“完美二叉树”,该假设不会影响计算结果的正确性</p>
<p><img alt="完美二叉树的各层节点数量" src="../build_heap.assets/heapify_operations_count.png" /></p>
<p align="center"> 图 8-5 &nbsp; 完美二叉树的各层节点数量 </p>
<p>如图 8-5 所示,<strong>节点“从顶至底堆化”的最大迭代次数等于该节点到叶节点的距离,而该距离正是“节点高度”</strong>。因此,我们可以将各层的“节点数量 <span class="arithmatex">\(\times\)</span> 节点高度”求和,<strong>从而得到所有节点的堆化迭代次数的总和</strong></p>
<p>如图 8-5 所示,节点“从顶至底堆化”的最大迭代次数等于该节点到叶节点的距离,而该距离正是“节点高度”。因此,我们可以将各层的“节点数量 <span class="arithmatex">\(\times\)</span> 节点高度”求和,<strong>从而得到所有节点的堆化迭代次数的总和</strong></p>
<div class="arithmatex">\[
T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \dots + 2^{(h-1)}\times1
\]</div>
<p>化简上式需要借助中学的数列知识,先对 <span class="arithmatex">\(T(h)\)</span> 乘以 <span class="arithmatex">\(2\)</span> ,得到</p>
<p>化简上式需要借助中学的数列知识,先对 <span class="arithmatex">\(T(h)\)</span> 乘以 <span class="arithmatex">\(2\)</span> ,得到</p>
<div class="arithmatex">\[
\begin{aligned}
T(h) &amp; = 2^0h + 2^1(h-1) + 2^2(h-2) + \dots + 2^{h-1}\times1 \newline
2 T(h) &amp; = 2^1h + 2^2(h-1) + 2^3(h-2) + \dots + 2^{h}\times1 \newline
\end{aligned}
\]</div>
<p>使用错位相减法,用下式 <span class="arithmatex">\(2 T(h)\)</span> 减去上式 <span class="arithmatex">\(T(h)\)</span> ,可得</p>
<p>使用错位相减法,用下式 <span class="arithmatex">\(2 T(h)\)</span> 减去上式 <span class="arithmatex">\(T(h)\)</span> ,可得</p>
<div class="arithmatex">\[
2T(h) - T(h) = T(h) = -2^0h + 2^1 + 2^2 + \dots + 2^{h-1} + 2^h
\]</div>
<p>观察上式,发现 <span class="arithmatex">\(T(h)\)</span> 是一个等比数列,可直接使用求和公式,得到时间复杂度为</p>
<p>观察上式,发现 <span class="arithmatex">\(T(h)\)</span> 是一个等比数列,可直接使用求和公式,得到时间复杂度为</p>
<div class="arithmatex">\[
\begin{aligned}
T(h) &amp; = 2 \frac{1 - 2^h}{1 - 2} - h \newline