This commit is contained in:
krahets
2026-04-02 03:08:50 +08:00
parent 09a136c9fa
commit aaf9f58eb3
157 changed files with 3002 additions and 2994 deletions
+38 -38
View File
@@ -6,7 +6,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="Data Structures and Algorithms Crash Course with Animated Illustrations and Off-the-Shelf Code">
<meta name="description" content="Data structures and algorithms tutorial with animated illustrations and ready-to-run code">
<meta name="author" content="krahets">
@@ -576,7 +576,7 @@
<span class="md-ellipsis">
Chapter 1. Encounter With Algorithms
Chapter 1. Encounter with Algorithms
@@ -598,7 +598,7 @@
<span class="md-nav__icon md-icon"></span>
Chapter 1. Encounter With Algorithms
Chapter 1. Encounter with Algorithms
</label>
@@ -1183,7 +1183,7 @@
<span class="md-ellipsis">
Chapter 4. Array and Linked List
Chapter 4. Arrays and Linked Lists
@@ -1205,7 +1205,7 @@
<span class="md-nav__icon md-icon"></span>
Chapter 4. Array and Linked List
Chapter 4. Arrays and Linked Lists
</label>
@@ -1311,7 +1311,7 @@
<span class="md-ellipsis">
4.4 Memory and Cache *
4.4 Random-Access Memory and Cache *
@@ -1402,7 +1402,7 @@
<span class="md-ellipsis">
Chapter 5. Stack and Queue
Chapter 5. Stacks and Queues
@@ -1424,7 +1424,7 @@
<span class="md-nav__icon md-icon"></span>
Chapter 5. Stack and Queue
Chapter 5. Stacks and Queues
</label>
@@ -1502,7 +1502,7 @@
<span class="md-ellipsis">
5.3 Double-Ended Queue
5.3 Deque
@@ -1593,7 +1593,7 @@
<span class="md-ellipsis">
Chapter 6. Hashing
Chapter 6. Hash Table
@@ -1615,7 +1615,7 @@
<span class="md-nav__icon md-icon"></span>
Chapter 6. Hashing
Chapter 6. Hash Table
</label>
@@ -1888,7 +1888,7 @@
<span class="md-ellipsis">
7.3 Array Representation of Tree
7.3 Array Representation of Binary Trees
@@ -2107,7 +2107,7 @@
<span class="md-ellipsis">
8.2 Building a Heap
8.2 Heap Construction Operation
@@ -2135,7 +2135,7 @@
<span class="md-ellipsis">
8.3 Top-K Problem
8.3 Top-k Problem
@@ -2493,7 +2493,7 @@
<span class="md-ellipsis">
10.2 Binary Search Insertion
10.2 Binary Search Insertion Point
@@ -2521,7 +2521,7 @@
<span class="md-ellipsis">
10.3 Binary Search Edge Cases
10.3 Binary Search Boundaries
@@ -2577,7 +2577,7 @@
<span class="md-ellipsis">
10.5 Search Algorithms Revisited
10.5 Searching Algorithms Revisited
@@ -2728,7 +2728,7 @@
<span class="md-ellipsis">
11.1 Sorting Algorithms
11.1 Sorting Algorithm
@@ -3304,7 +3304,7 @@
<span class="md-ellipsis">
12.4 Hanoi Tower Problem
12.4 Hanota Problem
@@ -4216,7 +4216,7 @@
<span class="md-ellipsis">
16.3 Terminology Table
16.3 Glossary
@@ -4423,12 +4423,12 @@
<!-- 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 (quick sort)</u> is a sorting algorithm based on the divide-and-conquer strategy, which operates efficiently and is widely applied.</p>
<p>The core operation of quick sort is "sentinel partitioning", which aims to: select a certain element in the array as the "pivot", move all elements smaller than the pivot to its left, and move elements larger than the pivot to its right. Specifically, the flow of sentinel partitioning is shown in Figure 11-8.</p>
<p><u>Quick sort</u> is an efficient and widely used sorting algorithm based on the divide-and-conquer strategy.</p>
<p>The core operation of quick sort is "sentinel partitioning", whose goal is to select an element as the "pivot", move all elements smaller than the pivot to its left, and move all elements larger than the pivot to its right. Specifically, the process is shown 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> pointing to the two ends of the array.</li>
<li>Set up a loop in which <code>i</code> (<code>j</code>) is used in each round to find the first element larger (smaller) than the pivot, and then swap these two elements.</li>
<li>Loop through step <code>2.</code> until <code>i</code> and <code>j</code> meet, and finally swap the pivot to the boundary line of the two sub-arrays.</li>
<li>Select the leftmost element as the pivot, and initialize two pointers <code>i</code> and <code>j</code> at the two ends of the array.</li>
<li>Enter a loop. In each round, use <code>i</code> (<code>j</code>) to find the first element larger (smaller) than the pivot, and then swap the two elements.</li>
<li>Repeat step <code>2.</code> until <code>i</code> and <code>j</code> meet, then swap the pivot into the boundary position 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>
<div class="tabbed-content">
@@ -4463,7 +4463,7 @@
</div>
<p align="center"> Figure 11-8 &nbsp; Sentinel partitioning steps </p>
<p>After sentinel partitioning is complete, the original array is divided into three parts: left sub-array, pivot, right sub-array, satisfying "any element in left sub-array <span class="arithmatex">\(\leq\)</span> pivot <span class="arithmatex">\(\leq\)</span> any element in right sub-array". Therefore, we next only need to sort these two sub-arrays.</p>
<p>After sentinel partitioning, the original array is divided into three parts: the left sub-array, the pivot, and the right sub-array, such that "any element in the left sub-array <span class="arithmatex">\(\leq\)</span> the pivot <span class="arithmatex">\(\leq\)</span> any element in the right sub-array". Therefore, we only need to sort the two sub-arrays next.</p>
<div class="admonition note">
<p class="admonition-title">Divide-and-conquer strategy of quick sort</p>
<p>The essence of sentinel partitioning is to simplify the sorting problem of a longer array into the sorting problems of two shorter arrays.</p>
@@ -4965,22 +4965,22 @@
</div>
<h2 id="1152-algorithm-characteristics">11.5.2 &nbsp; Algorithm Characteristics<a class="headerlink" href="#1152-algorithm-characteristics" 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 the average case, the number of recursive levels of sentinel partitioning is <span class="arithmatex">\(\log n\)</span>, and the total number of loops at each 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 sentinel partitioning divides an array of length <span class="arithmatex">\(n\)</span> into two sub-arrays of length <span class="arithmatex">\(0\)</span> and <span class="arithmatex">\(n - 1\)</span>, at which point the number of recursive levels reaches <span class="arithmatex">\(n\)</span>, the number of loops at each level is <span class="arithmatex">\(n\)</span>, and the total time used is <span class="arithmatex">\(O(n^2)\)</span>.</li>
<li><strong>Time complexity of <span class="arithmatex">\(O(n \log n)\)</span>, non-adaptive sorting</strong>: On average, sentinel partitioning produces <span class="arithmatex">\(\log n\)</span> recursive levels, and the total number of loop iterations across each level is <span class="arithmatex">\(n\)</span>, so the overall time complexity is <span class="arithmatex">\(O(n \log n)\)</span>. In the worst case, each round of sentinel partitioning splits an array of length <span class="arithmatex">\(n\)</span> into sub-arrays of lengths <span class="arithmatex">\(0\)</span> and <span class="arithmatex">\(n - 1\)</span>. The recursion depth then reaches <span class="arithmatex">\(n\)</span>, with <span class="arithmatex">\(n\)</span> loop iterations at each level, yielding an overall time complexity of <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 an additional array.</li>
<li><strong>Non-stable sorting</strong>: In the last step of sentinel partitioning, the pivot may be swapped to the right of equal elements.</li>
<li><strong>Unstable sorting</strong>: In the last step of sentinel partitioning, the pivot may be swapped to the right of an equal element.</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 the name, we can see that quick sort should have certain advantages in terms of efficiency. Although the average time complexity of quick sort is the same as "merge sort" and "heap sort", quick sort is usually more efficient, mainly for the following reasons.</p>
<p>As the name suggests, quick sort has clear efficiency advantages. Although its average time complexity is the same as that of "merge sort" and "heap sort", quick sort is usually faster in practice for the following reasons.</p>
<ul>
<li><strong>The probability of the worst case occurring is very low</strong>: Although the worst-case time complexity of quick sort is <span class="arithmatex">\(O(n^2)\)</span>, which is not as stable as merge sort, in the vast majority of cases, quick sort can run with a time complexity of <span class="arithmatex">\(O(n \log n)\)</span>.</li>
<li><strong>High cache utilization</strong>: When performing sentinel partitioning operations, the system can load the entire sub-array into the cache, so element access efficiency is relatively high. Algorithms like "heap sort" require jump-style access to elements, thus lacking this characteristic.</li>
<li><strong>Small constant coefficient of complexity</strong>: Among the three algorithms mentioned above, quick sort has the smallest total number of operations such as comparisons, assignments, and swaps. This is similar to the reason why "insertion sort" is faster than "bubble sort".</li>
<li><strong>The worst case is unlikely to occur</strong>: Although the worst-case time complexity of quick sort is <span class="arithmatex">\(O(n^2)\)</span> and its performance is less predictable than that of merge sort, quick sort runs in <span class="arithmatex">\(O(n \log n)\)</span> time in the vast majority of cases.</li>
<li><strong>High cache efficiency</strong>: During sentinel partitioning, the system can load the entire sub-array into cache, so accessing elements is relatively efficient. By contrast, algorithms such as "heap sort" require non-contiguous access to elements and therefore do not enjoy this advantage.</li>
<li><strong>Small constant factors</strong>: Among the three algorithms above, quick sort performs the fewest comparisons, assignments, and swaps in total. 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 may have reduced time efficiency for certain inputs</strong>. Take an extreme example: suppose the input array is completely reversed. Since we select the leftmost element as the pivot, after sentinel partitioning is complete, the pivot is swapped to the rightmost end of the array, 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 we recurse down like this, each round of sentinel partitioning will have a sub-array length of <span class="arithmatex">\(0\)</span>, the divide-and-conquer strategy fails, and quick sort degrades to a form approximate to "bubble sort".</p>
<p>To avoid this situation as much as possible, <strong>we can optimize the pivot selection strategy in sentinel partitioning</strong>. For example, we can randomly select an element as the pivot. However, if luck is not good and we select a non-ideal pivot every time, efficiency is still not satisfactory.</p>
<p>It should be noted 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 in the array (usually the first, last, and middle elements of the array), <strong>and use the median of these three candidate elements as the pivot</strong>. In 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 improve the robustness of the algorithm. After adopting this method, the probability of time complexity degrading to <span class="arithmatex">\(O(n^2)\)</span> is greatly reduced.</p>
<p><strong>Quick sort can become less time-efficient for certain inputs</strong>. Consider an extreme example in which the input array is in completely descending order. Because we choose the leftmost element as the pivot, once sentinel partitioning is complete, the pivot is swapped to the far right of the array, leaving a left sub-array of length <span class="arithmatex">\(n - 1\)</span> and a right sub-array of length <span class="arithmatex">\(0\)</span>. If this continues recursively, each round of sentinel partitioning produces one sub-array of length <span class="arithmatex">\(0\)</span>, the divide-and-conquer strategy breaks down, and quick sort degenerates into an approximation of "bubble sort".</p>
<p>To reduce the chance of this happening, <strong>we can optimize the pivot selection strategy used in sentinel partitioning</strong>. For example, we can choose a pivot at random. However, if we are unlucky and repeatedly pick poor pivots, performance can still be unsatisfactory.</p>
<p>It should be noted that programming languages usually generate "pseudo-random numbers". If we construct a specific test case against a pseudo-random sequence, quick sort can still suffer degraded performance.</p>
<p>To improve further, we can choose three candidate elements from the array, usually the first, last, and middle elements, <strong>and use the median of the three as the pivot</strong>. This greatly increases the chance that the pivot is "neither too small nor too large". We can also choose more candidate elements to further improve the robustness of the algorithm. With this method, the probability that the time complexity degrades to <span class="arithmatex">\(O(n^2)\)</span> is significantly reduced.</p>
<p>Example code is as follows:</p>
<div class="tabbed-set tabbed-alternate" data-tabs="4:13"><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" /><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></div>
<div class="tabbed-content">
@@ -5419,8 +5419,8 @@
</div>
</div>
<h2 id="1155-recursive-depth-optimization">11.5.5 &nbsp; Recursive Depth Optimization<a class="headerlink" href="#1155-recursive-depth-optimization" title="Permanent link">&para;</a></h2>
<p><strong>For certain inputs, quick sort may occupy more space</strong>. Taking a completely ordered input array as an example, let the length of the sub-array in recursion be <span class="arithmatex">\(m\)</span>. Each round of sentinel partitioning will produce a left sub-array of length <span class="arithmatex">\(0\)</span> and a right sub-array of length <span class="arithmatex">\(m - 1\)</span>, which means that the problem scale reduced per recursive call is very small (only one element is reduced), and the height of the recursion tree will reach <span class="arithmatex">\(n - 1\)</span>, at which point <span class="arithmatex">\(O(n)\)</span> size of stack frame space is required.</p>
<p>To prevent the accumulation of stack frame space, we can compare the lengths of the two sub-arrays after each round of sentinel sorting is complete, <strong>and only recurse on the shorter sub-array</strong>. Since the length of the shorter sub-array will not exceed <span class="arithmatex">\(n / 2\)</span>, this method can ensure that the recursion depth does not exceed <span class="arithmatex">\(\log n\)</span>, thus optimizing the worst-case space complexity to <span class="arithmatex">\(O(\log n)\)</span>. The code is as follows:</p>
<p><strong>Quick sort may also use more space for certain inputs</strong>. Consider a fully sorted input array. Let the length of the current sub-array in the recursion be <span class="arithmatex">\(m\)</span>. Each round of sentinel 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>, which means each recursive call reduces the problem size by only one element. The recursion tree can therefore reach a height of <span class="arithmatex">\(n - 1\)</span>, requiring <span class="arithmatex">\(O(n)\)</span> stack-frame space.</p>
<p>To prevent stack frames from accumulating, we can compare the lengths of the two sub-arrays after each round of sentinel partitioning, <strong>and recurse only on the shorter one</strong>. Because the shorter sub-array has length at most <span class="arithmatex">\(n / 2\)</span>, this method ensures that the recursion depth does not exceed <span class="arithmatex">\(\log n\)</span>, reducing the worst-case space complexity to <span class="arithmatex">\(O(\log n)\)</span>. The code is shown below:</p>
<div class="tabbed-set tabbed-alternate" data-tabs="5:13"><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" /><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></div>
<div class="tabbed-content">
<div class="tabbed-block">