mirror of
https://github.com/krahets/hello-algo.git
synced 2025-07-28 12:52:57 +08:00
deploy
This commit is contained in:
@ -1754,22 +1754,21 @@
|
||||
|
||||
|
||||
<h1 id="115">11.5. 归并排序<a class="headerlink" href="#115" title="Permanent link">¶</a></h1>
|
||||
<p>「归并排序 Merge Sort」是算法中“分治思想”的典型体现,其有「划分」和「合并」两个阶段:</p>
|
||||
<p>「归并排序 Merge Sort」基于分治思想实现排序,包含“划分”和“合并”两个阶段:</p>
|
||||
<ol>
|
||||
<li><strong>划分阶段</strong>:通过递归不断 <strong>将数组从中点位置划分开</strong>,将长数组的排序问题转化为短数组的排序问题;</li>
|
||||
<li><strong>合并阶段</strong>:划分到子数组长度为 1 时,开始向上合并,不断将 <strong>左、右两个短排序数组</strong> 合并为 <strong>一个长排序数组</strong>,直至合并至原数组时完成排序;</li>
|
||||
<li><strong>划分阶段</strong>:通过递归不断地将数组从中点处分开,将长数组的排序问题转换为短数组的排序问题;</li>
|
||||
<li><strong>合并阶段</strong>:当子数组长度为 1 时终止划分,开始合并,持续地将左右两个较短的有序数组合并为一个较长的有序数组,直至结束;</li>
|
||||
</ol>
|
||||
<p><img alt="归并排序的划分与合并阶段" src="../merge_sort.assets/merge_sort_overview.png" /></p>
|
||||
<p align="center"> Fig. 归并排序的划分与合并阶段 </p>
|
||||
|
||||
<h2 id="1151">11.5.1. 算法流程<a class="headerlink" href="#1151" title="Permanent link">¶</a></h2>
|
||||
<p><strong>「递归划分」</strong> 从顶至底递归地 <strong>将数组从中点切为两个子数组</strong>,直至长度为 1 ;</p>
|
||||
<p>“划分阶段”从顶至底递归地将数组从中点切为两个子数组,直至长度为 1 ;</p>
|
||||
<ol>
|
||||
<li>计算数组中点 <code>mid</code> ,递归划分左子数组(区间 <code>[left, mid]</code> )和右子数组(区间 <code>[mid + 1, right]</code> );</li>
|
||||
<li>递归执行 <code>1.</code> 步骤,直至子数组区间长度为 1 时,终止递归划分;</li>
|
||||
<li>递归执行步骤 <code>1.</code> ,直至子数组区间长度为 1 时,终止递归划分;</li>
|
||||
</ol>
|
||||
<p><strong>「回溯合并」</strong> 从底至顶地将左子数组和右子数组合并为一个 <strong>有序数组</strong> ;</p>
|
||||
<p>需要注意,由于从长度为 1 的子数组开始合并,所以 <strong>每个子数组都是有序的</strong>。因此,合并任务本质是要 <strong>将两个有序子数组合并为一个有序数组</strong>。</p>
|
||||
<p>“合并阶段”从底至顶地将左子数组和右子数组合并为一个有序数组。需要注意的是,从长度为 1 的子数组开始合并,合并阶段中的每个子数组都是有序的。</p>
|
||||
<div class="tabbed-set tabbed-alternate" data-tabs="1:10"><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" /><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><label for="__tabbed_1_5"><5></label><label for="__tabbed_1_6"><6></label><label for="__tabbed_1_7"><7></label><label for="__tabbed_1_8"><8></label><label for="__tabbed_1_9"><9></label><label for="__tabbed_1_10"><10></label></div>
|
||||
<div class="tabbed-content">
|
||||
<div class="tabbed-block">
|
||||
@ -1804,10 +1803,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>观察发现,归并排序的递归顺序就是二叉树的「后序遍历」。</p>
|
||||
<p>观察发现,归并排序的递归顺序与二叉树的后序遍历相同,具体来看:</p>
|
||||
<ul>
|
||||
<li><strong>后序遍历</strong>:先递归左子树、再递归右子树、最后处理根节点。</li>
|
||||
<li><strong>归并排序</strong>:先递归左子树、再递归右子树、最后处理合并。</li>
|
||||
<li><strong>后序遍历</strong>:先递归左子树,再递归右子树,最后处理根节点。</li>
|
||||
<li><strong>归并排序</strong>:先递归左子数组,再递归右子数组,最后处理合并。</li>
|
||||
</ul>
|
||||
<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">
|
||||
@ -2218,30 +2217,22 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>下面重点解释一下合并方法 <code>merge()</code> 的流程:</p>
|
||||
<ol>
|
||||
<li>初始化一个辅助数组 <code>tmp</code> 暂存待合并区间 <code>[left, right]</code> 内的元素,后续通过覆盖原数组 <code>nums</code> 的元素来实现合并;</li>
|
||||
<li>初始化指针 <code>i</code> , <code>j</code> , <code>k</code> 分别指向左子数组、右子数组、原数组的首元素;</li>
|
||||
<li>循环判断 <code>tmp[i]</code> 和 <code>tmp[j]</code> 的大小,将较小的先覆盖至 <code>nums[k]</code> ,指针 <code>i</code> , <code>j</code> 根据判断结果交替前进(指针 <code>k</code> 也前进),直至两个子数组都遍历完,即可完成合并。</li>
|
||||
</ol>
|
||||
<p>合并方法 <code>merge()</code> 代码中的主要难点:</p>
|
||||
<p>合并方法 <code>merge()</code> 代码中的难点包括:</p>
|
||||
<ul>
|
||||
<li><code>nums</code> 的待合并区间为 <code>[left, right]</code> ,而因为 <code>tmp</code> 只复制了 <code>nums</code> 该区间元素,所以 <code>tmp</code> 对应区间为 <code>[0, right - left]</code> ,<strong>需要特别注意代码中各个变量的含义</strong>。</li>
|
||||
<li>判断 <code>tmp[i]</code> 和 <code>tmp[j]</code> 的大小的操作中,还 <strong>需考虑当子数组遍历完成后的索引越界问题</strong>,即 <code>i > leftEnd</code> 和 <code>j > rightEnd</code> 的情况,索引越界的优先级是最高的,例如如果左子数组已经被合并完了,那么不用继续判断,直接合并右子数组元素即可。</li>
|
||||
<li><strong>在阅读代码时,需要特别注意各个变量的含义</strong>。<code>nums</code> 的待合并区间为 <code>[left, right]</code> ,但由于 <code>tmp</code> 仅复制了 <code>nums</code> 该区间的元素,因此 <code>tmp</code> 对应区间为 <code>[0, right - left]</code> 。</li>
|
||||
<li>在比较 <code>tmp[i]</code> 和 <code>tmp[j]</code> 的大小时,<strong>还需考虑子数组遍历完成后的索引越界问题</strong>,即 <code>i > leftEnd</code> 和 <code>j > rightEnd</code> 的情况。索引越界的优先级是最高的,如果左子数组已经被合并完了,那么不需要继续比较,直接合并右子数组元素即可。</li>
|
||||
</ul>
|
||||
<h2 id="1152">11.5.2. 算法特性<a class="headerlink" href="#1152" title="Permanent link">¶</a></h2>
|
||||
<p><strong>时间复杂度 <span class="arithmatex">\(O(n \log n)\)</span></strong> :划分形成高度为 <span class="arithmatex">\(\log n\)</span> 的递归树,每层合并的总操作数量为 <span class="arithmatex">\(n\)</span> ,总体使用 <span class="arithmatex">\(O(n \log n)\)</span> 时间。</p>
|
||||
<p><strong>空间复杂度 <span class="arithmatex">\(O(n)\)</span></strong> :需借助辅助数组实现合并,使用 <span class="arithmatex">\(O(n)\)</span> 大小的额外空间;递归深度为 <span class="arithmatex">\(\log n\)</span> ,使用 <span class="arithmatex">\(O(\log n)\)</span> 大小的栈帧空间,因此是“非原地排序”。</p>
|
||||
<p>在合并时,不改变相等元素的次序,是“稳定排序”。</p>
|
||||
<p><strong>时间复杂度 <span class="arithmatex">\(O(n \log n)\)</span></strong> :划分产生高度为 <span class="arithmatex">\(\log n\)</span> 的递归树,每层合并的总操作数量为 <span class="arithmatex">\(n\)</span> ,因此总体时间复杂度为 <span class="arithmatex">\(O(n \log n)\)</span> 。</p>
|
||||
<p><strong>空间复杂度 <span class="arithmatex">\(O(n)\)</span></strong> :递归深度为 <span class="arithmatex">\(\log n\)</span> ,使用 <span class="arithmatex">\(O(\log n)\)</span> 大小的栈帧空间;合并操作需要借助辅助数组实现,使用 <span class="arithmatex">\(O(n)\)</span> 大小的额外空间;因此是“非原地排序”。</p>
|
||||
<p>在合并过程中,相等元素的次序保持不变,因此归并排序是“稳定排序”。</p>
|
||||
<h2 id="1153">11.5.3. 链表排序 *<a class="headerlink" href="#1153" title="Permanent link">¶</a></h2>
|
||||
<p>归并排序有一个很特别的优势,用于排序链表时有很好的性能表现,<strong>空间复杂度可被优化至 <span class="arithmatex">\(O(1)\)</span></strong> ,这是因为:</p>
|
||||
<p>归并排序在排序链表时具有显著优势,空间复杂度可以优化至 <span class="arithmatex">\(O(1)\)</span> ,原因如下:</p>
|
||||
<ul>
|
||||
<li>由于链表可仅通过改变指针来实现节点增删,因此“将两个短有序链表合并为一个长有序链表”无需使用额外空间,即回溯合并阶段不用像排序数组一样建立辅助数组 <code>tmp</code> ;</li>
|
||||
<li>通过使用「迭代」代替「递归划分」,可省去递归使用的栈帧空间;</li>
|
||||
<li>由于链表仅需改变指针就可实现节点的增删操作,因此合并阶段(将两个短有序链表合并为一个长有序链表)无需创建辅助链表。</li>
|
||||
<li>通过使用“迭代划分”替代“递归划分”,可省去递归使用的栈帧空间;</li>
|
||||
</ul>
|
||||
<blockquote>
|
||||
<p>详情参考:<a href="https://leetcode-cn.com/problems/sort-list/solution/sort-list-gui-bing-pai-xu-lian-biao-by-jyd/">148. 排序链表</a></p>
|
||||
</blockquote>
|
||||
<p>具体实现细节比较复杂,有兴趣的同学可以查阅相关资料进行学习。</p>
|
||||
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user