mirror of
https://github.com/krahets/hello-algo.git
synced 2025-07-29 05:13:14 +08:00
deploy
This commit is contained in:
@ -1619,14 +1619,14 @@
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#821" class="md-nav__link">
|
||||
8.2.1 借助入堆方法实现
|
||||
8.2.1 自上而下构建
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#822" class="md-nav__link">
|
||||
8.2.2 基于堆化操作实现
|
||||
8.2.2 自下而上构建
|
||||
</a>
|
||||
|
||||
</li>
|
||||
@ -3413,14 +3413,14 @@
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#821" class="md-nav__link">
|
||||
8.2.1 借助入堆方法实现
|
||||
8.2.1 自上而下构建
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#822" class="md-nav__link">
|
||||
8.2.2 基于堆化操作实现
|
||||
8.2.2 自下而上构建
|
||||
</a>
|
||||
|
||||
</li>
|
||||
@ -3457,12 +3457,21 @@
|
||||
|
||||
<h1 id="82">8.2 建堆操作<a class="headerlink" href="#82" title="Permanent link">¶</a></h1>
|
||||
<p>在某些情况下,我们希望使用一个列表的所有元素来构建一个堆,这个过程被称为“建堆操作”。</p>
|
||||
<h2 id="821">8.2.1 借助入堆方法实现<a class="headerlink" href="#821" title="Permanent link">¶</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 基于堆化操作实现<a class="headerlink" href="#822" title="Permanent link">¶</a></h2>
|
||||
<p>有趣的是,存在一种更高效的建堆方法,其时间复杂度可以达到 <span class="arithmatex">\(O(n)\)</span> 。我们先将列表所有元素原封不动添加到堆中,然后倒序遍历该堆,依次对每个节点执行“从顶至底堆化”。</p>
|
||||
<p>请注意,因为叶节点没有子节点,所以无须堆化。在代码实现中,我们从最后一个节点的父节点开始进行堆化。</p>
|
||||
<h2 id="821">8.2.1 自上而下构建<a class="headerlink" href="#821" title="Permanent link">¶</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 自下而上构建<a class="headerlink" href="#822" title="Permanent link">¶</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 复杂度分析<a class="headerlink" href="#823" title="Permanent link">¶</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 完美二叉树的各层节点数量 </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) & = 2^0h + 2^1(h-1) + 2^2(h-2) + \dots + 2^{h-1}\times1 \newline
|
||||
2 T(h) & = 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) & = 2 \frac{1 - 2^h}{1 - 2} - h \newline
|
||||
|
Reference in New Issue
Block a user