mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-11 23:16:07 +00:00
deploy
This commit is contained in:
@@ -3454,8 +3454,8 @@
|
||||
|
||||
|
||||
<h1 id="115">11.5 快速排序<a class="headerlink" href="#115" title="Permanent link">¶</a></h1>
|
||||
<p>「快速排序 Quick Sort」是一种基于分治思想的排序算法,运行高效,应用广泛。</p>
|
||||
<p>快速排序的核心操作是「哨兵划分」,其目标是:选择数组中的某个元素作为“基准数”,将所有小于基准数的元素移到其左侧,而大于基准数的元素移到其右侧。具体来说,哨兵划分的流程为:</p>
|
||||
<p>「快速排序 quick sort」是一种基于分治思想的排序算法,运行高效,应用广泛。</p>
|
||||
<p>快速排序的核心操作是“哨兵划分”,其目标是:选择数组中的某个元素作为“基准数”,将所有小于基准数的元素移到其左侧,而大于基准数的元素移到其右侧。具体来说,哨兵划分的流程为:</p>
|
||||
<ol>
|
||||
<li>选取数组最左端元素作为基准数,初始化两个指针 <code>i</code> 和 <code>j</code> 分别指向数组的两端。</li>
|
||||
<li>设置一个循环,在每轮中使用 <code>i</code>(<code>j</code>)分别寻找第一个比基准数大(小)的元素,然后交换这两个元素。</li>
|
||||
@@ -3794,8 +3794,8 @@
|
||||
</div>
|
||||
<h2 id="1151">11.5.1 算法流程<a class="headerlink" href="#1151" title="Permanent link">¶</a></h2>
|
||||
<ol>
|
||||
<li>首先,对原数组执行一次「哨兵划分」,得到未排序的左子数组和右子数组。</li>
|
||||
<li>然后,对左子数组和右子数组分别递归执行「哨兵划分」。</li>
|
||||
<li>首先,对原数组执行一次“哨兵划分”,得到未排序的左子数组和右子数组。</li>
|
||||
<li>然后,对左子数组和右子数组分别递归执行“哨兵划分”。</li>
|
||||
<li>持续递归,直至子数组长度为 1 时终止,从而完成整个数组的排序。</li>
|
||||
</ol>
|
||||
<p><img alt="快速排序流程" src="../quick_sort.assets/quick_sort_overview.png" /></p>
|
||||
@@ -4004,14 +4004,14 @@
|
||||
<li><strong>非稳定排序</strong>:在哨兵划分的最后一步,基准数可能会被交换至相等元素的右侧。</li>
|
||||
</ul>
|
||||
<h2 id="1153">11.5.3 快排为什么快?<a class="headerlink" href="#1153" title="Permanent link">¶</a></h2>
|
||||
<p>从名称上就能看出,快速排序在效率方面应该具有一定的优势。尽管快速排序的平均时间复杂度与「归并排序」和「堆排序」相同,但通常快速排序的效率更高,原因如下:</p>
|
||||
<p>从名称上就能看出,快速排序在效率方面应该具有一定的优势。尽管快速排序的平均时间复杂度与“归并排序”和“堆排序”相同,但通常快速排序的效率更高,原因如下:</p>
|
||||
<ul>
|
||||
<li><strong>出现最差情况的概率很低</strong>:虽然快速排序的最差时间复杂度为 <span class="arithmatex">\(O(n^2)\)</span> ,没有归并排序稳定,但在绝大多数情况下,快速排序能在 <span class="arithmatex">\(O(n \log n)\)</span> 的时间复杂度下运行。</li>
|
||||
<li><strong>缓存使用效率高</strong>:在执行哨兵划分操作时,系统可将整个子数组加载到缓存,因此访问元素的效率较高。而像「堆排序」这类算法需要跳跃式访问元素,从而缺乏这一特性。</li>
|
||||
<li><strong>复杂度的常数系数低</strong>:在上述三种算法中,快速排序的比较、赋值、交换等操作的总数量最少。这与「插入排序」比「冒泡排序」更快的原因类似。</li>
|
||||
<li><strong>缓存使用效率高</strong>:在执行哨兵划分操作时,系统可将整个子数组加载到缓存,因此访问元素的效率较高。而像“堆排序”这类算法需要跳跃式访问元素,从而缺乏这一特性。</li>
|
||||
<li><strong>复杂度的常数系数低</strong>:在上述三种算法中,快速排序的比较、赋值、交换等操作的总数量最少。这与“插入排序”比“冒泡排序”更快的原因类似。</li>
|
||||
</ul>
|
||||
<h2 id="1154">11.5.4 基准数优化<a class="headerlink" href="#1154" title="Permanent link">¶</a></h2>
|
||||
<p><strong>快速排序在某些输入下的时间效率可能降低</strong>。举一个极端例子,假设输入数组是完全倒序的,由于我们选择最左端元素作为基准数,那么在哨兵划分完成后,基准数被交换至数组最右端,导致左子数组长度为 <span class="arithmatex">\(n - 1\)</span> 、右子数组长度为 <span class="arithmatex">\(0\)</span> 。如此递归下去,每轮哨兵划分后的右子数组长度都为 <span class="arithmatex">\(0\)</span> ,分治策略失效,快速排序退化为「冒泡排序」。</p>
|
||||
<p><strong>快速排序在某些输入下的时间效率可能降低</strong>。举一个极端例子,假设输入数组是完全倒序的,由于我们选择最左端元素作为基准数,那么在哨兵划分完成后,基准数被交换至数组最右端,导致左子数组长度为 <span class="arithmatex">\(n - 1\)</span> 、右子数组长度为 <span class="arithmatex">\(0\)</span> 。如此递归下去,每轮哨兵划分后的右子数组长度都为 <span class="arithmatex">\(0\)</span> ,分治策略失效,快速排序退化为“冒泡排序”。</p>
|
||||
<p>为了尽量避免这种情况发生,<strong>我们可以优化哨兵划分中的基准数的选取策略</strong>。例如,我们可以随机选取一个元素作为基准数。然而,如果运气不佳,每次都选到不理想的基准数,效率仍然不尽如人意。</p>
|
||||
<p>需要注意的是,编程语言通常生成的是“伪随机数”。如果我们针对伪随机数序列构建一个特定的测试样例,那么快速排序的效率仍然可能劣化。</p>
|
||||
<p>为了进一步改进,我们可以在数组中选取三个候选元素(通常为数组的首、尾、中点元素),<strong>并将这三个候选元素的中位数作为基准数</strong>。这样一来,基准数“既不太小也不太大”的概率将大幅提升。当然,我们还可以选取更多候选元素,以进一步提高算法的稳健性。采用这种方法后,时间复杂度劣化至 <span class="arithmatex">\(O(n^2)\)</span> 的概率大大降低。</p>
|
||||
|
||||
Reference in New Issue
Block a user