This commit is contained in:
krahets
2025-03-14 17:51:07 +08:00
parent 1b1e1e354a
commit 3b08169043
39 changed files with 956 additions and 958 deletions
+19 -18
View File
@@ -3638,11 +3638,11 @@
<!-- Page content -->
<h1 id="115-quick-sort">11.5 &nbsp; Quick sort<a class="headerlink" href="#115-quick-sort" title="Permanent link">&para;</a></h1>
<p><u>Quick sort</u> is a sorting algorithm based on the divide and conquer strategy, known for its efficiency and wide application.</p>
<p>The core operation of quick sort is "pivot partitioning," aiming to: select an element from the array as the "pivot," move all elements smaller than the pivot to its left, and move elements greater than the pivot to its right. Specifically, the pivot partitioning process is illustrated in Figure 11-8.</p>
<p><u>Quick sort</u> is a sorting algorithm based on the divide-and-conquer strategy, known for its efficiency and wide application.</p>
<p>The core operation of quick sort is "pivot partitioning," which aims to select an element from the array as the "pivot" and move all elements less than the pivot to its left side, while moving all elements greater than the pivot to its right side. Specifically, the process of pivot partitioning is illustrated in Figure 11-8.</p>
<ol>
<li>Select the leftmost element of the array as the pivot, and initialize two pointers <code>i</code> and <code>j</code> at both ends of the array.</li>
<li>Set up a loop where each round uses <code>i</code> (<code>j</code>) to find the first element larger (smaller) than the pivot, then swap these two elements.</li>
<li>Select the leftmost element of the array as the pivot, and initialize two pointers <code>i</code> and <code>j</code> to point to the two ends of the array respectively.</li>
<li>Set up a loop where each round uses <code>i</code> (<code>j</code>) to search for the first element larger (smaller) than the pivot, then swap these two elements.</li>
<li>Repeat step <code>2.</code> until <code>i</code> and <code>j</code> meet, finally swap the pivot to the boundary between the two sub-arrays.</li>
</ol>
<div class="tabbed-set tabbed-alternate" data-tabs="1:9"><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" /><div class="tabbed-labels"><label for="__tabbed_1_1">&lt;1&gt;</label><label for="__tabbed_1_2">&lt;2&gt;</label><label for="__tabbed_1_3">&lt;3&gt;</label><label for="__tabbed_1_4">&lt;4&gt;</label><label for="__tabbed_1_5">&lt;5&gt;</label><label for="__tabbed_1_6">&lt;6&gt;</label><label for="__tabbed_1_7">&lt;7&gt;</label><label for="__tabbed_1_8">&lt;8&gt;</label><label for="__tabbed_1_9">&lt;9&gt;</label></div>
@@ -3678,10 +3678,10 @@
</div>
<p align="center"> Figure 11-8 &nbsp; Pivot division process </p>
<p>After the pivot partitioning, the original array is divided into three parts: left sub-array, pivot, and right sub-array, satisfying "any element in the left sub-array <span class="arithmatex">\(\leq\)</span> pivot <span class="arithmatex">\(\leq\)</span> any element in the right sub-array." Therefore, we only need to sort these two sub-arrays next.</p>
<p>After the pivot partitioning, the original array is divided into three parts: left sub-array, pivot, and right sub-array, satisfying "any element in the left sub-array <span class="arithmatex">\(\leq\)</span> pivot <span class="arithmatex">\(\leq\)</span> any element in the right sub-array." Therefore, we then only need to sort these two sub-arrays.</p>
<div class="admonition note">
<p class="admonition-title">Quick sort's divide and conquer strategy</p>
<p>The essence of pivot partitioning is to simplify a longer array's sorting problem into two shorter arrays' sorting problems.</p>
<p class="admonition-title">Divide-and-conquer strategy for quick sort</p>
<p>The essence of pivot partitioning is to simplify the sorting problem of a longer array into two shorter arrays.</p>
</div>
<div class="tabbed-set tabbed-alternate" data-tabs="2:14"><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" /><input id="__tabbed_2_11" name="__tabbed_2" type="radio" /><input id="__tabbed_2_12" name="__tabbed_2" type="radio" /><input id="__tabbed_2_13" name="__tabbed_2" type="radio" /><input id="__tabbed_2_14" name="__tabbed_2" type="radio" /><div class="tabbed-labels"><label for="__tabbed_2_1">Python</label><label for="__tabbed_2_2">C++</label><label for="__tabbed_2_3">Java</label><label for="__tabbed_2_4">C#</label><label for="__tabbed_2_5">Go</label><label for="__tabbed_2_6">Swift</label><label for="__tabbed_2_7">JS</label><label for="__tabbed_2_8">TS</label><label for="__tabbed_2_9">Dart</label><label for="__tabbed_2_10">Rust</label><label for="__tabbed_2_11">C</label><label for="__tabbed_2_12">Kotlin</label><label for="__tabbed_2_13">Ruby</label><label for="__tabbed_2_14">Zig</label></div>
<div class="tabbed-content">
@@ -3807,8 +3807,8 @@
<p>The overall process of quick sort is shown in Figure 11-9.</p>
<ol>
<li>First, perform a "pivot partitioning" on the original array to obtain the unsorted left and right sub-arrays.</li>
<li>Then, recursively perform "pivot partitioning" on both the left and right sub-arrays.</li>
<li>Continue recursively until the sub-array length reaches 1, thus completing the sorting of the entire array.</li>
<li>Then, recursively perform "pivot partitioning" on the left and right sub-arrays separately.</li>
<li>Continue recursively until the length of sub-array is 1, thus completing the sorting of the entire array.</li>
</ol>
<p><a class="glightbox" href="../quick_sort.assets/quick_sort_overview.png" data-type="image" data-width="100%" data-height="auto" data-desc-position="bottom"><img alt="Quick sort process" class="animation-figure" src="../quick_sort.assets/quick_sort_overview.png" /></a></p>
<p align="center"> Figure 11-9 &nbsp; Quick sort process </p>
@@ -3904,22 +3904,22 @@
</div>
<h2 id="1152-algorithm-features">11.5.2 &nbsp; Algorithm features<a class="headerlink" href="#1152-algorithm-features" title="Permanent link">&para;</a></h2>
<ul>
<li><strong>Time complexity of <span class="arithmatex">\(O(n \log n)\)</span>, non-adaptive sorting</strong>: In average cases, the recursive levels of pivot partitioning are <span class="arithmatex">\(\log n\)</span>, and the total number of loops per level is <span class="arithmatex">\(n\)</span>, using <span class="arithmatex">\(O(n \log n)\)</span> time overall. In the worst case, each round of pivot partitioning divides an array of length <span class="arithmatex">\(n\)</span> into two sub-arrays of lengths <span class="arithmatex">\(0\)</span> and <span class="arithmatex">\(n - 1\)</span>, reaching <span class="arithmatex">\(n\)</span> recursive levels, and using <span class="arithmatex">\(O(n^2)\)</span> time overall.</li>
<li><strong>Space complexity of <span class="arithmatex">\(O(n)\)</span>, in-place sorting</strong>: In completely reversed input arrays, reaching the worst recursion depth of <span class="arithmatex">\(n\)</span>, using <span class="arithmatex">\(O(n)\)</span> stack frame space. The sorting operation is performed on the original array without the aid of additional arrays.</li>
<li><strong>Time complexity of <span class="arithmatex">\(O(n \log n)\)</span>, non-adaptive sorting</strong>: In average cases, the recursive levels of pivot partitioning are <span class="arithmatex">\(\log n\)</span>, and the total number of loops per level is <span class="arithmatex">\(n\)</span>, using <span class="arithmatex">\(O(n \log n)\)</span> time overall. In the worst case, each round of pivot partitioning divides an array of length <span class="arithmatex">\(n\)</span> into two sub-arrays of lengths <span class="arithmatex">\(0\)</span> and <span class="arithmatex">\(n - 1\)</span>, when the number of recursive levels reaches <span class="arithmatex">\(n\)</span>, the number of loops in each level is <span class="arithmatex">\(n\)</span>, and the total time used is <span class="arithmatex">\(O(n^2)\)</span>.</li>
<li><strong>Space complexity of <span class="arithmatex">\(O(n)\)</span>, in-place sorting</strong>: In the case where the input array is completely reversed, the worst recursive depth reaches <span class="arithmatex">\(n\)</span>, using <span class="arithmatex">\(O(n)\)</span> stack frame space. The sorting operation is performed on the original array without the aid of additional arrays.</li>
<li><strong>Non-stable sorting</strong>: In the final step of pivot partitioning, the pivot may be swapped to the right of equal elements.</li>
</ul>
<h2 id="1153-why-is-quick-sort-fast">11.5.3 &nbsp; Why is quick sort fast<a class="headerlink" href="#1153-why-is-quick-sort-fast" title="Permanent link">&para;</a></h2>
<p>From its name, it is apparent that quick sort should have certain efficiency advantages. Although the average time complexity of quick sort is the same as "merge sort" and "heap sort," quick sort is generally more efficient, mainly for the following reasons.</p>
<p>As the name suggests, quick sort should have certain advantages in terms of efficiency. Although the average time complexity of quick sort is the same as that of "merge sort" and "heap sort," it is generally more efficient for the following reasons.</p>
<ul>
<li><strong>Low probability of worst-case scenarios</strong>: Although the worst time complexity of quick sort is <span class="arithmatex">\(O(n^2)\)</span>, less stable than merge sort, in most cases, quick sort can operate under a time complexity of <span class="arithmatex">\(O(n \log n)\)</span>.</li>
<li><strong>High cache usage efficiency</strong>: During the pivot partitioning operation, the system can load the entire sub-array into the cache, thus accessing elements more efficiently. In contrast, algorithms like "heap sort" need to access elements in a jumping manner, lacking this feature.</li>
<li><strong>Small constant coefficient of complexity</strong>: Among the mentioned algorithms, quick sort has the fewest total number of comparisons, assignments, and swaps. This is similar to why "insertion sort" is faster than "bubble sort."</li>
<li><strong>High cache utilization</strong>: During the pivot partitioning operation, the system can load the entire sub-array into the cache, thus accessing elements more efficiently. In contrast, algorithms like "heap sort" need to access elements in a jumping manner, lacking this feature.</li>
<li><strong>Small constant coefficient of complexity</strong>: Among the three algorithms mentioned above, quick sort has the least total number of operations such as comparisons, assignments, and swaps. This is similar to why "insertion sort" is faster than "bubble sort."</li>
</ul>
<h2 id="1154-pivot-optimization">11.5.4 &nbsp; Pivot optimization<a class="headerlink" href="#1154-pivot-optimization" title="Permanent link">&para;</a></h2>
<p><strong>Quick sort's time efficiency may decrease under certain inputs</strong>. For example, if the input array is completely reversed, since we select the leftmost element as the pivot, after the pivot partitioning, the pivot is swapped to the array's right end, causing the left sub-array length to be <span class="arithmatex">\(n - 1\)</span> and the right sub-array length to be <span class="arithmatex">\(0\)</span>. If this recursion continues, each round of pivot partitioning will have a sub-array length of <span class="arithmatex">\(0\)</span>, and the divide and conquer strategy fails, degrading quick sort to a form similar to "bubble sort."</p>
<p>To avoid this situation, <strong>we can optimize the strategy for selecting the pivot in the pivot partitioning</strong>. For instance, we can randomly select an element as the pivot. However, if luck is not on our side, and we keep selecting suboptimal pivots, the efficiency is still not satisfactory.</p>
<p><strong>Quick sort's time efficiency may degrade under certain inputs</strong>. For example, if the input array is completely reversed, since we select the leftmost element as the pivot, after the pivot partitioning, the pivot is swapped to the array's right end, causing the left sub-array length to be <span class="arithmatex">\(n - 1\)</span> and the right sub-array length to be <span class="arithmatex">\(0\)</span>. Continuing this way, each round of pivot partitioning will have a sub-array length of <span class="arithmatex">\(0\)</span>, and the divide-and-conquer strategy fails, degrading quick sort to a form similar to "bubble sort."</p>
<p>To avoid this situation, <strong>we can optimize the pivot selection strategy in the pivot partitioning</strong>. For instance, we can randomly select an element as the pivot. However, if luck is not on our side, and we consistently select suboptimal pivots, the efficiency is still not satisfactory.</p>
<p>It's important to note that programming languages usually generate "pseudo-random numbers". If we construct a specific test case for a pseudo-random number sequence, the efficiency of quick sort may still degrade.</p>
<p>For further improvement, we can select three candidate elements (usually the first, last, and midpoint elements of the array), <strong>and use the median of these three candidate elements as the pivot</strong>. This significantly increases the probability that the pivot is "neither too small nor too large". Of course, we can also select more candidate elements to further enhance the algorithm's robustness. Using this method significantly reduces the probability of time complexity degradation to <span class="arithmatex">\(O(n^2)\)</span>.</p>
<p>For further improvement, we can select three candidate elements (usually the first, last, and midpoint elements of the array), <strong>and use the median of these three candidate elements as the pivot</strong>. This way, the probability that the pivot is "neither too small nor too large" will be greatly increased. Of course, we can also select more candidate elements to further enhance robustness of the algorithm. With this method, the probability of the time complexity degrading to <span class="arithmatex">\(O(n^2)\)</span> is greatly reduced.</p>
<p>Sample code is as follows:</p>
<div class="tabbed-set tabbed-alternate" data-tabs="4:14"><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" /><input id="__tabbed_4_13" name="__tabbed_4" type="radio" /><input id="__tabbed_4_14" name="__tabbed_4" type="radio" /><div class="tabbed-labels"><label for="__tabbed_4_1">Python</label><label for="__tabbed_4_2">C++</label><label for="__tabbed_4_3">Java</label><label for="__tabbed_4_4">C#</label><label for="__tabbed_4_5">Go</label><label for="__tabbed_4_6">Swift</label><label for="__tabbed_4_7">JS</label><label for="__tabbed_4_8">TS</label><label for="__tabbed_4_9">Dart</label><label for="__tabbed_4_10">Rust</label><label for="__tabbed_4_11">C</label><label for="__tabbed_4_12">Kotlin</label><label for="__tabbed_4_13">Ruby</label><label for="__tabbed_4_14">Zig</label></div>
<div class="tabbed-content">
@@ -4084,7 +4084,8 @@
</div>
</div>
<h2 id="1155-tail-recursion-optimization">11.5.5 &nbsp; Tail recursion optimization<a class="headerlink" href="#1155-tail-recursion-optimization" title="Permanent link">&para;</a></h2>
<p><strong>Under certain inputs, quick sort may occupy more space</strong>. For a completely ordered input array, assume the sub-array length in recursion is <span class="arithmatex">\(m\)</span>, each round of pivot partitioning produces a left sub-array of length <span class="arithmatex">\(0\)</span> and a right sub-array of length <span class="arithmatex">\(m - 1\)</span>, meaning the problem size reduced per recursive call is very small (only one element), and the height of the recursion tree can reach <span class="arithmatex">\(n - 1\)</span>, requiring <span class="arithmatex">\(O(n)\)</span> stack frame space.</p>
<p><strong>Under certain inputs, quick sort may occupy more space</strong>. For example, consider a completely ordered input array. Let the length of the sub-array in the recursion be <span class="arithmatex">\(m\)</span>. In each round of pivot partitioning, a left sub-array of length <span class="arithmatex">\(0\)</span> and a right sub-array of length <span class="arithmatex">\(m - 1\)</span> are produced. This means that the problem size is reduced by only one element per recursive call, resulting in a very small reduction at each level of recursion.
As a result, the height of the recursion tree can reach <span class="arithmatex">\(n 1\)</span> , which requires <span class="arithmatex">\(O(n)\)</span> of stack frame space.</p>
<p>To prevent the accumulation of stack frame space, we can compare the lengths of the two sub-arrays after each round of pivot sorting, <strong>and only recursively sort the shorter sub-array</strong>. Since the length of the shorter sub-array will not exceed <span class="arithmatex">\(n / 2\)</span>, this method ensures that the recursion depth does not exceed <span class="arithmatex">\(\log n\)</span>, thus optimizing the worst space complexity to <span class="arithmatex">\(O(\log n)\)</span>. The code is as follows:</p>
<div class="tabbed-set tabbed-alternate" data-tabs="5:14"><input checked="checked" id="__tabbed_5_1" name="__tabbed_5" type="radio" /><input id="__tabbed_5_2" name="__tabbed_5" type="radio" /><input id="__tabbed_5_3" name="__tabbed_5" type="radio" /><input id="__tabbed_5_4" name="__tabbed_5" type="radio" /><input id="__tabbed_5_5" name="__tabbed_5" type="radio" /><input id="__tabbed_5_6" name="__tabbed_5" type="radio" /><input id="__tabbed_5_7" name="__tabbed_5" type="radio" /><input id="__tabbed_5_8" name="__tabbed_5" type="radio" /><input id="__tabbed_5_9" name="__tabbed_5" type="radio" /><input id="__tabbed_5_10" name="__tabbed_5" type="radio" /><input id="__tabbed_5_11" name="__tabbed_5" type="radio" /><input id="__tabbed_5_12" name="__tabbed_5" type="radio" /><input id="__tabbed_5_13" name="__tabbed_5" type="radio" /><input id="__tabbed_5_14" name="__tabbed_5" type="radio" /><div class="tabbed-labels"><label for="__tabbed_5_1">Python</label><label for="__tabbed_5_2">C++</label><label for="__tabbed_5_3">Java</label><label for="__tabbed_5_4">C#</label><label for="__tabbed_5_5">Go</label><label for="__tabbed_5_6">Swift</label><label for="__tabbed_5_7">JS</label><label for="__tabbed_5_8">TS</label><label for="__tabbed_5_9">Dart</label><label for="__tabbed_5_10">Rust</label><label for="__tabbed_5_11">C</label><label for="__tabbed_5_12">Kotlin</label><label for="__tabbed_5_13">Ruby</label><label for="__tabbed_5_14">Zig</label></div>
<div class="tabbed-content">