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
+45 -45
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
@@ -1595,7 +1595,7 @@
<span class="md-ellipsis">
Chapter 6. Hashing
Chapter 6. Hash Table
@@ -1617,7 +1617,7 @@
<span class="md-nav__icon md-icon"></span>
Chapter 6. Hashing
Chapter 6. Hash Table
</label>
@@ -1766,10 +1766,10 @@
</li>
<li class="md-nav__item">
<a href="#3-double-hashing" class="md-nav__link">
<a href="#3-multiple-hashing" class="md-nav__link">
<span class="md-ellipsis">
3. &nbsp; Double Hashing
3. &nbsp; Multiple Hashing
</span>
</a>
@@ -2010,7 +2010,7 @@
<span class="md-ellipsis">
7.3 Array Representation of Tree
7.3 Array Representation of Binary Trees
@@ -2229,7 +2229,7 @@
<span class="md-ellipsis">
8.2 Building a Heap
8.2 Heap Construction Operation
@@ -2257,7 +2257,7 @@
<span class="md-ellipsis">
8.3 Top-K Problem
8.3 Top-k Problem
@@ -2615,7 +2615,7 @@
<span class="md-ellipsis">
10.2 Binary Search Insertion
10.2 Binary Search Insertion Point
@@ -2643,7 +2643,7 @@
<span class="md-ellipsis">
10.3 Binary Search Edge Cases
10.3 Binary Search Boundaries
@@ -2699,7 +2699,7 @@
<span class="md-ellipsis">
10.5 Search Algorithms Revisited
10.5 Searching Algorithms Revisited
@@ -2848,7 +2848,7 @@
<span class="md-ellipsis">
11.1 Sorting Algorithms
11.1 Sorting Algorithm
@@ -3321,7 +3321,7 @@
<span class="md-ellipsis">
12.4 Hanoi Tower Problem
12.4 Hanota Problem
@@ -4233,7 +4233,7 @@
<span class="md-ellipsis">
16.3 Terminology Table
16.3 Glossary
@@ -4393,10 +4393,10 @@
</li>
<li class="md-nav__item">
<a href="#3-double-hashing" class="md-nav__link">
<a href="#3-multiple-hashing" class="md-nav__link">
<span class="md-ellipsis">
3. &nbsp; Double Hashing
3. &nbsp; Multiple Hashing
</span>
</a>
@@ -4463,17 +4463,17 @@
<li>Improve the hash table data structure so that <strong>the hash table can function normally when hash collisions occur</strong>.</li>
<li>Only expand when necessary, that is, only when hash collisions are severe.</li>
</ol>
<p>The main methods for improving the structure of hash tables include "separate chaining" and "open addressing".</p>
<p>The main approaches to improving a hash table's structure are separate chaining and open addressing.</p>
<h2 id="621-separate-chaining">6.2.1 &nbsp; Separate Chaining<a class="headerlink" href="#621-separate-chaining" title="Permanent link">&para;</a></h2>
<p>In the original hash table, each bucket can store only one key-value pair. <u>Separate chaining</u> converts a single element into a linked list, treating key-value pairs as linked list nodes and storing all colliding key-value pairs in the same linked list. Figure 6-5 shows an example of a separate chaining hash table.</p>
<p>In the original hash table, each bucket can store only one key-value pair. <u>Separate chaining</u> replaces the single element in each bucket with a linked list, treating each key-value pair as a node and storing all colliding key-value pairs in the same list. Figure 6-5 shows an example of a separate chaining hash table.</p>
<p><img alt="Separate chaining hash table" class="animation-figure" src="../hash_collision.assets/hash_table_chaining.png" /></p>
<p align="center"> Figure 6-5 &nbsp; Separate chaining hash table </p>
<p>The operations of a hash table implemented with separate chaining have changed as follows:</p>
<p>In a hash table implemented with separate chaining, the basic operations work as follows:</p>
<ul>
<li><strong>Querying elements</strong>: Input <code>key</code>, obtain the bucket index through the hash function, then access the head node of the linked list, then traverse the linked list and compare <code>key</code> to find the target key-value pair.</li>
<li><strong>Adding elements</strong>: First access the linked list head node through the hash function, then append the node (key-value pair) to the linked list.</li>
<li><strong>Deleting elements</strong>: Access the head of the linked list based on the result of the hash function, then traverse the linked list to find the target node and delete it.</li>
<li><strong>Querying elements</strong>: Input <code>key</code>, compute the bucket index using the hash function, access the head of the corresponding linked list, and traverse the list while comparing keys until the target key-value pair is found.</li>
<li><strong>Adding elements</strong>: First use the hash function to locate the corresponding linked list, then insert the node (key-value pair) into the list.</li>
<li><strong>Deleting elements</strong>: Use the hash function to locate the corresponding linked list, then traverse it to find and delete the target node.</li>
</ul>
<p>Separate chaining has the following limitations:</p>
<ul>
@@ -5943,28 +5943,28 @@
</div>
</div>
</div>
<p>It's worth noting that when the linked list is very long, the query efficiency <span class="arithmatex">\(O(n)\)</span> is poor. <strong>In this case, the list can be converted to an "AVL tree" or "Red-Black tree"</strong> to optimize the time complexity of the query operation to <span class="arithmatex">\(O(\log n)\)</span>.</p>
<p>It's worth noting that when the linked list becomes very long, the query time <span class="arithmatex">\(O(n)\)</span> is poor. <strong>In this case, the linked list can be converted into an AVL tree or a red-black tree</strong>, reducing the time complexity of lookups to <span class="arithmatex">\(O(\log n)\)</span>.</p>
<h2 id="622-open-addressing">6.2.2 &nbsp; Open Addressing<a class="headerlink" href="#622-open-addressing" title="Permanent link">&para;</a></h2>
<p><u>Open addressing</u> does not introduce additional data structures but instead handles hash collisions through "multiple probes". The probing methods mainly include linear probing, quadratic probing, and double hashing.</p>
<p><u>Open addressing</u> does not introduce additional data structures. Instead, it handles hash collisions through repeated probing. Common probing strategies include linear probing, quadratic probing, and multiple hashing.</p>
<p>Let's use linear probing as an example to introduce the mechanism of open addressing hash tables.</p>
<h3 id="1-linear-probing">1. &nbsp; Linear Probing<a class="headerlink" href="#1-linear-probing" title="Permanent link">&para;</a></h3>
<p>Linear probing uses a fixed-step linear search for probing, and its operation method differs from ordinary hash tables.</p>
<p>Linear probing uses a fixed step size to probe sequentially, so its operations differ somewhat from those of an ordinary hash table.</p>
<ul>
<li><strong>Inserting elements</strong>: Calculate the bucket index using the hash function. If the bucket already contains an element, linearly traverse forward from the conflict position (usually with a step size of <span class="arithmatex">\(1\)</span>) until an empty bucket is found, then insert the element.</li>
<li><strong>Searching for elements</strong>: If a hash collision is encountered, use the same step size to linearly traverse forward until the corresponding element is found and return <code>value</code>; if an empty bucket is encountered, it means the target element is not in the hash table, so return <code>None</code>.</li>
<li><strong>Inserting elements</strong>: Compute the bucket index using the hash function. If the bucket is already occupied, continue probing forward from the collision position with a fixed step size (usually <span class="arithmatex">\(1\)</span>) until an empty bucket is found, then insert the element there.</li>
<li><strong>Searching for elements</strong>: If a collision occurs, continue probing forward with the same step size until the corresponding element is found and return its <code>value</code>; if an empty bucket is encountered, the target element is not in the hash table, so return <code>None</code>.</li>
</ul>
<p>Figure 6-6 shows the distribution of key-value pairs in an open addressing (linear probing) hash table. According to this hash function, keys with the same last two digits will be mapped to the same bucket. Through linear probing, they are stored sequentially in that bucket and the buckets below it.</p>
<p>Figure 6-6 shows the distribution of key-value pairs in an open-addressing hash table that uses linear probing. Under this hash function, keys with the same last two digits are mapped to the same bucket. Linear probing then places them in that bucket and the subsequent buckets.</p>
<p><img alt="Distribution of key-value pairs in open addressing (linear probing) hash table" class="animation-figure" src="../hash_collision.assets/hash_table_linear_probing.png" /></p>
<p align="center"> Figure 6-6 &nbsp; Distribution of key-value pairs in open addressing (linear probing) hash table </p>
<p>However, <strong>linear probing is prone to create "clustering"</strong>. Specifically, the longer the continuously occupied positions in the array, the greater the probability of hash collisions occurring in these continuous positions, further promoting clustering growth at that position, forming a vicious cycle, and ultimately leading to degraded efficiency of insertion, deletion, query, and update operations.</p>
<p>It's important to note that <strong>we cannot directly delete elements in an open addressing hash table</strong>. Deleting an element creates an empty bucket <code>None</code> in the array. When searching for elements, if linear probing encounters this empty bucket, it will return, making the elements below this empty bucket inaccessible. The program may incorrectly assume these elements do not exist, as shown in Figure 6-7.</p>
<p>However, <strong>linear probing is prone to clustering</strong>. Specifically, the longer a contiguous occupied region in the array becomes, the more likely new collisions are to occur within that region. This in turn makes the cluster grow even further, creating a vicious cycle that gradually degrades the efficiency of insertion, deletion, lookup, and update operations.</p>
<p>It's important to note that <strong>we cannot directly delete elements from an open-addressing hash table</strong>. Deleting an element creates an empty bucket <code>None</code> in the array. During lookup, once linear probing reaches that empty bucket, it stops, which means any elements stored farther along the probe sequence become unreachable. As a result, the program may incorrectly conclude that those elements do not exist, as shown in Figure 6-7.</p>
<p><img alt="Query issues caused by deletion in open addressing" class="animation-figure" src="../hash_collision.assets/hash_table_open_addressing_deletion.png" /></p>
<p align="center"> Figure 6-7 &nbsp; Query issues caused by deletion in open addressing </p>
<p>To solve this problem, we can adopt the <u>lazy deletion</u> mechanism: instead of directly removing elements from the hash table, <strong>use a constant <code>TOMBSTONE</code> to mark the bucket</strong>. In this mechanism, both <code>None</code> and <code>TOMBSTONE</code> represent empty buckets and can hold key-value pairs. However, when linear probing encounters <code>TOMBSTONE</code>, it should continue traversing since there may still be key-value pairs below it.</p>
<p>However, <strong>lazy deletion may accelerate the performance degradation of the hash table</strong>. Every deletion operation produces a deletion mark, and as <code>TOMBSTONE</code> increases, the search time will also increase because linear probing may need to skip multiple <code>TOMBSTONE</code> to find the target element.</p>
<p>To address this, consider recording the index of the first encountered <code>TOMBSTONE</code> during linear probing and swapping the searched target element with that <code>TOMBSTONE</code>. The benefit of doing this is that each time an element is queried or added, the element will be moved to a bucket closer to its ideal position (the starting point of probing), thereby optimizing query efficiency.</p>
<p>To solve this problem, we can adopt <u>lazy deletion</u>: instead of directly removing an element from the hash table, <strong>use a constant <code>TOMBSTONE</code> to mark the bucket</strong>. Under this mechanism, both <code>None</code> and <code>TOMBSTONE</code> denote buckets that can accept key-value pairs. The difference is that when linear probing encounters <code>TOMBSTONE</code>, it must continue probing, because key-value pairs may still exist farther along the sequence.</p>
<p>However, <strong>lazy deletion may accelerate hash-table performance degradation</strong>. Each deletion leaves behind a marker, and as the number of <code>TOMBSTONE</code> entries grows, search time increases as well, because linear probing may need to skip over multiple tombstones before finding the target element.</p>
<p>To address this, we can record the index of the first <code>TOMBSTONE</code> encountered during linear probing and swap the found target element into that position. The benefit is that each query or insertion can move elements closer to their ideal positions, that is, closer to where probing begins, which improves lookup efficiency.</p>
<p>The code below implements an open addressing (linear probing) hash table with lazy deletion. To make better use of the hash table space, we treat the hash table as a "circular array". When going beyond the end of the array, we return to the beginning and continue traversing.</p>
<div class="tabbed-set tabbed-alternate" data-tabs="2:13"><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" /><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></div>
<div class="tabbed-content">
@@ -7679,16 +7679,16 @@
<li>Clustering still exists, i.e., some positions are more likely to be occupied than others.</li>
<li>Due to the growth of squares, quadratic probing may not probe the entire hash table, meaning that even if there are empty buckets in the hash table, quadratic probing may not be able to access them.</li>
</ul>
<h3 id="3-double-hashing">3. &nbsp; Double Hashing<a class="headerlink" href="#3-double-hashing" title="Permanent link">&para;</a></h3>
<p>As the name suggests, the double hashing method uses multiple hash functions <span class="arithmatex">\(f_1(x)\)</span>, <span class="arithmatex">\(f_2(x)\)</span>, <span class="arithmatex">\(f_3(x)\)</span>, <span class="arithmatex">\(\dots\)</span> for probing.</p>
<h3 id="3-multiple-hashing">3. &nbsp; Multiple Hashing<a class="headerlink" href="#3-multiple-hashing" title="Permanent link">&para;</a></h3>
<p>As the name suggests, multiple hashing uses multiple hash functions <span class="arithmatex">\(f_1(x)\)</span>, <span class="arithmatex">\(f_2(x)\)</span>, <span class="arithmatex">\(f_3(x)\)</span>, <span class="arithmatex">\(\dots\)</span> for probing.</p>
<ul>
<li><strong>Inserting elements</strong>: If hash function <span class="arithmatex">\(f_1(x)\)</span> encounters a conflict, try <span class="arithmatex">\(f_2(x)\)</span>, and so on, until an empty position is found and the element is inserted.</li>
<li><strong>Searching for elements</strong>: Search in the same order of hash functions until the target element is found and return it; if an empty position is encountered or all hash functions have been tried, it indicates the element is not in the hash table, then return <code>None</code>.</li>
</ul>
<p>Compared to linear probing, the double hashing method is less prone to clustering, but multiple hash functions introduce additional computational overhead.</p>
<p>Compared with linear probing, multiple hashing is less prone to clustering, but using multiple hash functions introduces additional computational overhead.</p>
<div class="admonition tip">
<p class="admonition-title">Tip</p>
<p>Please note that open addressing (linear probing, quadratic probing, and double hashing) hash tables all have the problem of "cannot directly delete elements".</p>
<p>Please note that hash tables based on open addressing, including linear probing, quadratic probing, and multiple hashing, all have the problem that elements cannot be deleted directly.</p>
</div>
<h2 id="623-choice-of-programming-languages">6.2.3 &nbsp; Choice of Programming Languages<a class="headerlink" href="#623-choice-of-programming-languages" title="Permanent link">&para;</a></h2>
<p>Different programming languages adopt different hash table implementation strategies. Here are a few examples:</p>