This commit is contained in:
krahets
2023-08-20 13:37:20 +08:00
parent 88e0b11361
commit 96fded547b
35 changed files with 777 additions and 716 deletions

View File

@ -2424,8 +2424,8 @@
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#_1" class="md-nav__link">
基于分治实现二分
<a href="#1" class="md-nav__link">
1. &nbsp; 基于分治实现二分
</a>
</li>
@ -3376,8 +3376,8 @@
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#_1" class="md-nav__link">
基于分治实现二分
<a href="#1" class="md-nav__link">
1. &nbsp; 基于分治实现二分
</a>
</li>
@ -3423,7 +3423,7 @@
<li><strong>子问题的解无需合并</strong>:二分查找旨在查找一个特定元素,因此不需要将子问题的解进行合并。当子问题得到解决时,原问题也会同时得到解决。</li>
</ul>
<p>分治能够提升搜索效率,本质上是因为暴力搜索每轮只能排除一个选项,<strong>而分治搜索每轮可以排除一半选项</strong></p>
<h3 id="_1">基于分治实现二分<a class="headerlink" href="#_1" title="Permanent link">&para;</a></h3>
<h3 id="1">1. &nbsp; 基于分治实现二分<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<p>在之前的章节中,二分查找是基于递推(迭代)实现的。现在我们基于分治(递归)来实现它。</p>
<div class="admonition question">
<p class="admonition-title">Question</p>

View File

@ -2452,29 +2452,29 @@
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#_1" class="md-nav__link">
判断是否为分治问题
<a href="#1" class="md-nav__link">
1. &nbsp; 判断是否为分治问题
</a>
</li>
<li class="md-nav__item">
<a href="#_2" class="md-nav__link">
如何划分子树
<a href="#2" class="md-nav__link">
2. &nbsp; 如何划分子树
</a>
</li>
<li class="md-nav__item">
<a href="#_3" class="md-nav__link">
基于变量描述子树区间
<a href="#3" class="md-nav__link">
3. &nbsp; 基于变量描述子树区间
</a>
</li>
<li class="md-nav__item">
<a href="#_4" class="md-nav__link">
代码实现
<a href="#4" class="md-nav__link">
4. &nbsp; 代码实现
</a>
</li>
@ -3397,29 +3397,29 @@
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#_1" class="md-nav__link">
判断是否为分治问题
<a href="#1" class="md-nav__link">
1. &nbsp; 判断是否为分治问题
</a>
</li>
<li class="md-nav__item">
<a href="#_2" class="md-nav__link">
如何划分子树
<a href="#2" class="md-nav__link">
2. &nbsp; 如何划分子树
</a>
</li>
<li class="md-nav__item">
<a href="#_3" class="md-nav__link">
基于变量描述子树区间
<a href="#3" class="md-nav__link">
3. &nbsp; 基于变量描述子树区间
</a>
</li>
<li class="md-nav__item">
<a href="#_4" class="md-nav__link">
代码实现
<a href="#4" class="md-nav__link">
4. &nbsp; 代码实现
</a>
</li>
@ -3455,14 +3455,14 @@
<p><img alt="构建二叉树的示例数据" src="../build_binary_tree_problem.assets/build_tree_example.png" /></p>
<p align="center"> 图:构建二叉树的示例数据 </p>
<h3 id="_1">判断是否为分治问题<a class="headerlink" href="#_1" title="Permanent link">&para;</a></h3>
<h3 id="1">1. &nbsp; 判断是否为分治问题<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<p>原问题定义为从 <code>preorder</code><code>inorder</code> 构建二叉树。我们首先从分治的角度分析这道题:</p>
<ul>
<li><strong>问题可以被分解</strong>:从分治的角度切入,我们可以将原问题划分为两个子问题:构建左子树、构建右子树,加上一步操作:初始化根节点。而对于每个子树(子问题),我们仍然可以复用以上划分方法,将其划分为更小的子树(子问题),直至达到最小子问题(空子树)时终止。</li>
<li><strong>子问题是独立的</strong>:左子树和右子树是相互独立的,它们之间没有交集。在构建左子树时,我们只需要关注中序遍历和前序遍历中与左子树对应的部分。右子树同理。</li>
<li><strong>子问题的解可以合并</strong>:一旦得到了左子树和右子树(子问题的解),我们就可以将它们链接到根节点上,得到原问题的解。</li>
</ul>
<h3 id="_2">如何划分子树<a class="headerlink" href="#_2" title="Permanent link">&para;</a></h3>
<h3 id="2">2. &nbsp; 如何划分子树<a class="headerlink" href="#2" title="Permanent link">&para;</a></h3>
<p>根据以上分析,这道题是可以使用分治来求解的,但问题是:<strong>如何通过前序遍历 <code>preorder</code> 和中序遍历 <code>inorder</code> 来划分左子树和右子树呢</strong></p>
<p>根据定义,<code>preorder</code><code>inorder</code> 都可以被划分为三个部分:</p>
<ul>
@ -3478,7 +3478,7 @@
<p><img alt="在前序和中序遍历中划分子树" src="../build_binary_tree_problem.assets/build_tree_preorder_inorder_division.png" /></p>
<p align="center"> 图:在前序和中序遍历中划分子树 </p>
<h3 id="_3">基于变量描述子树区间<a class="headerlink" href="#_3" title="Permanent link">&para;</a></h3>
<h3 id="3">3. &nbsp; 基于变量描述子树区间<a class="headerlink" href="#3" title="Permanent link">&para;</a></h3>
<p>根据以上划分方法,<strong>我们已经得到根节点、左子树、右子树在 <code>preorder</code><code>inorder</code> 中的索引区间</strong>。而为了描述这些索引区间,我们需要借助几个指针变量:</p>
<ul>
<li>将当前树的根节点在 <code>preorder</code> 中的索引记为 <span class="arithmatex">\(i\)</span></li>
@ -3520,7 +3520,7 @@
<p><img alt="根节点和左右子树的索引区间表示" src="../build_binary_tree_problem.assets/build_tree_division_pointers.png" /></p>
<p align="center"> 图:根节点和左右子树的索引区间表示 </p>
<h3 id="_4">代码实现<a class="headerlink" href="#_4" title="Permanent link">&para;</a></h3>
<h3 id="4">4. &nbsp; 代码实现<a class="headerlink" href="#4" title="Permanent link">&para;</a></h3>
<p>为了提升查询 <span class="arithmatex">\(m\)</span> 的效率,我们借助一个哈希表 <code>hmap</code> 来存储数组 <code>inorder</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>
<div class="tabbed-content">

View File

@ -2411,15 +2411,15 @@
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#_1" class="md-nav__link">
操作数量优化
<a href="#1" class="md-nav__link">
1. &nbsp; 操作数量优化
</a>
</li>
<li class="md-nav__item">
<a href="#_2" class="md-nav__link">
并行计算优化
<a href="#2" class="md-nav__link">
2. &nbsp; 并行计算优化
</a>
</li>
@ -3425,15 +3425,15 @@
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#_1" class="md-nav__link">
操作数量优化
<a href="#1" class="md-nav__link">
1. &nbsp; 操作数量优化
</a>
</li>
<li class="md-nav__item">
<a href="#_2" class="md-nav__link">
并行计算优化
<a href="#2" class="md-nav__link">
2. &nbsp; 并行计算优化
</a>
</li>
@ -3503,7 +3503,7 @@
<h2 id="1212">12.1.2 &nbsp; 通过分治提升效率<a class="headerlink" href="#1212" title="Permanent link">&para;</a></h2>
<p>分治不仅可以有效地解决算法问题,<strong>往往还可以带来算法效率的提升</strong>。在排序算法中,快速排序、归并排序、堆排序相较于选择、冒泡、插入排序更快,就是因为它们应用了分治策略。</p>
<p>那么,我们不禁发问:<strong>为什么分治可以提升算法效率,其底层逻辑是什么</strong>?换句话说,将大问题分解为多个子问题、解决子问题、将子问题的解合并为原问题的解,这几步的效率为什么比直接解决原问题的效率更高?这个问题可以从操作数量和并行计算两方面来讨论。</p>
<h3 id="_1">操作数量优化<a class="headerlink" href="#_1" title="Permanent link">&para;</a></h3>
<h3 id="1">1. &nbsp; 操作数量优化<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<p>以「冒泡排序」为例,其处理一个长度为 <span class="arithmatex">\(n\)</span> 的数组需要 <span class="arithmatex">\(O(n^2)\)</span> 时间。假设我们把数组从中点分为两个子数组,则划分需要 <span class="arithmatex">\(O(n)\)</span> 时间,排序每个子数组需要 <span class="arithmatex">\(O((\frac{n}{2})^2)\)</span> 时间,合并两个子数组需要 <span class="arithmatex">\(O(n)\)</span> 时间,总体时间复杂度为:</p>
<div class="arithmatex">\[
O(n + (\frac{n}{2})^2 \times 2 + n) = O(\frac{n^2}{2} + 2n)
@ -3522,7 +3522,7 @@ n(n - 4) &amp; &gt; 0
<p><strong>这意味着当 <span class="arithmatex">\(n &gt; 4\)</span> 时,划分后的操作数量更少,排序效率应该更高</strong>。请注意,划分后的时间复杂度仍然是平方阶 <span class="arithmatex">\(O(n^2)\)</span> ,只是复杂度中的常数项变小了。</p>
<p>进一步想,<strong>如果我们把子数组不断地再从中点划分为两个子数组</strong>,直至子数组只剩一个元素时停止划分呢?这种思路实际上就是「归并排序」,时间复杂度为 <span class="arithmatex">\(O(n \log n)\)</span></p>
<p>再思考,<strong>如果我们多设置几个划分点</strong>,将原数组平均划分为 <span class="arithmatex">\(k\)</span> 个子数组呢?这种情况与「桶排序」非常类似,它非常适合排序海量数据,理论上时间复杂度可以达到 <span class="arithmatex">\(O(n + k)\)</span></p>
<h3 id="_2">并行计算优化<a class="headerlink" href="#_2" title="Permanent link">&para;</a></h3>
<h3 id="2">2. &nbsp; 并行计算优化<a class="headerlink" href="#2" title="Permanent link">&para;</a></h3>
<p>我们知道,分治生成的子问题是相互独立的,<strong>因此通常可以并行解决</strong>。也就是说,分治不仅可以降低算法的时间复杂度,<strong>还有利于操作系统的并行优化</strong></p>
<p>并行优化在多核或多处理器的环境中尤其有效,因为系统可以同时处理多个子问题,更加充分地利用计算资源,从而显著减少总体的运行时间。</p>
<p>比如在桶排序中,我们将海量的数据平均分配到各个桶中,则可所有桶的排序任务分散到各个计算单元,完成后再进行结果合并。</p>

View File

@ -2480,22 +2480,22 @@
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#_1" class="md-nav__link">
考虑基本情况
<a href="#1" class="md-nav__link">
1. &nbsp; 考虑基本情况
</a>
</li>
<li class="md-nav__item">
<a href="#_2" class="md-nav__link">
子问题分解
<a href="#2" class="md-nav__link">
2. &nbsp; 子问题分解
</a>
</li>
<li class="md-nav__item">
<a href="#_3" class="md-nav__link">
代码实现
<a href="#3" class="md-nav__link">
3. &nbsp; 代码实现
</a>
</li>
@ -3390,22 +3390,22 @@
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#_1" class="md-nav__link">
考虑基本情况
<a href="#1" class="md-nav__link">
1. &nbsp; 考虑基本情况
</a>
</li>
<li class="md-nav__item">
<a href="#_2" class="md-nav__link">
子问题分解
<a href="#2" class="md-nav__link">
2. &nbsp; 子问题分解
</a>
</li>
<li class="md-nav__item">
<a href="#_3" class="md-nav__link">
代码实现
<a href="#3" class="md-nav__link">
3. &nbsp; 代码实现
</a>
</li>
@ -3448,7 +3448,7 @@
<p align="center"> 图:汉诺塔问题示例 </p>
<p><strong>我们将规模为 <span class="arithmatex">\(i\)</span> 的汉诺塔问题记做 <span class="arithmatex">\(f(i)\)</span></strong> 。例如 <span class="arithmatex">\(f(3)\)</span> 代表将 <span class="arithmatex">\(3\)</span> 个圆盘从 <code>A</code> 移动至 <code>C</code> 的汉诺塔问题。</p>
<h3 id="_1">考虑基本情况<a class="headerlink" href="#_1" title="Permanent link">&para;</a></h3>
<h3 id="1">1. &nbsp; 考虑基本情况<a class="headerlink" href="#1" title="Permanent link">&para;</a></h3>
<p>对于问题 <span class="arithmatex">\(f(1)\)</span> ,即当只有一个圆盘时,则将它直接从 <code>A</code> 移动至 <code>C</code> 即可。</p>
<div class="tabbed-set tabbed-alternate" data-tabs="1:2"><input checked="checked" id="__tabbed_1_1" name="__tabbed_1" type="radio" /><input id="__tabbed_1_2" 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></div>
<div class="tabbed-content">
@ -3487,7 +3487,7 @@
</div>
<p align="center"> 图:规模为 2 问题的解 </p>
<h3 id="_2">子问题分解<a class="headerlink" href="#_2" title="Permanent link">&para;</a></h3>
<h3 id="2">2. &nbsp; 子问题分解<a class="headerlink" href="#2" title="Permanent link">&para;</a></h3>
<p>对于问题 <span class="arithmatex">\(f(3)\)</span> ,即当有三个圆盘时,情况变得稍微复杂了一些。由于已知 <span class="arithmatex">\(f(1)\)</span><span class="arithmatex">\(f(2)\)</span> 的解,因此可从分治角度思考,<strong><code>A</code> 顶部的两个圆盘看做一个整体</strong>,执行以下步骤:</p>
<ol>
<li><code>B</code> 为目标柱、<code>C</code> 为缓冲柱,将两个圆盘从 <code>A</code> 移动至 <code>B</code></li>
@ -3524,7 +3524,7 @@
<p><img alt="汉诺塔问题的分治策略" src="../hanota_problem.assets/hanota_divide_and_conquer.png" /></p>
<p align="center"> 图:汉诺塔问题的分治策略 </p>
<h3 id="_3">代码实现<a class="headerlink" href="#_3" title="Permanent link">&para;</a></h3>
<h3 id="3">3. &nbsp; 代码实现<a class="headerlink" href="#3" title="Permanent link">&para;</a></h3>
<p>在代码中,我们声明一个递归函数 <code>dfs(i, src, buf, tar)</code> ,它的作用是将柱 <code>src</code> 顶部的 <span class="arithmatex">\(i\)</span> 个圆盘借助缓冲柱 <code>buf</code> 移动至目标柱 <code>tar</code></p>
<div class="tabbed-set tabbed-alternate" data-tabs="4:12"><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" /><input id="__tabbed_4_5" name="__tabbed_4" type="radio" /><input id="__tabbed_4_6" name="__tabbed_4" type="radio" /><input id="__tabbed_4_7" name="__tabbed_4" type="radio" /><input id="__tabbed_4_8" name="__tabbed_4" type="radio" /><input id="__tabbed_4_9" name="__tabbed_4" type="radio" /><input id="__tabbed_4_10" name="__tabbed_4" type="radio" /><input id="__tabbed_4_11" name="__tabbed_4" type="radio" /><input id="__tabbed_4_12" name="__tabbed_4" type="radio" /><div class="tabbed-labels"><label for="__tabbed_4_1">Java</label><label for="__tabbed_4_2">C++</label><label for="__tabbed_4_3">Python</label><label for="__tabbed_4_4">Go</label><label for="__tabbed_4_5">JS</label><label for="__tabbed_4_6">TS</label><label for="__tabbed_4_7">C</label><label for="__tabbed_4_8">C#</label><label for="__tabbed_4_9">Swift</label><label for="__tabbed_4_10">Zig</label><label for="__tabbed_4_11">Dart</label><label for="__tabbed_4_12">Rust</label></div>
<div class="tabbed-content">