mirror of
https://github.com/krahets/hello-algo.git
synced 2026-08-17 14:10:57 +00:00
Translate all code to English (#1836)
* Review the EN heading format. * Fix pythontutor headings. * Fix pythontutor headings. * bug fixes * Fix headings in **/summary.md * Revisit the CN-to-EN translation for Python code using Claude-4.5 * Revisit the CN-to-EN translation for Java code using Claude-4.5 * Revisit the CN-to-EN translation for Cpp code using Claude-4.5. * Fix the dictionary. * Fix cpp code translation for the multipart strings. * Translate Go code to English. * Update workflows to test EN code. * Add EN translation for C. * Add EN translation for CSharp. * Add EN translation for Swift. * Trigger the CI check. * Revert. * Update en/hash_map.md * Add the EN version of Dart code. * Add the EN version of Kotlin code. * Add missing code files. * Add the EN version of JavaScript code. * Add the EN version of TypeScript code. * Fix the workflows. * Add the EN version of Ruby code. * Add the EN version of Rust code. * Update the CI check for the English version code. * Update Python CI check. * Fix cmakelists for en/C code. * Fix Ruby comments
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Programming Environment Installation
|
||||
|
||||
## Installing IDE
|
||||
## Installing Ide
|
||||
|
||||
We recommend using the open-source and lightweight VS Code as the local integrated development environment (IDE). Visit the [VS Code official website](https://code.visualstudio.com/), and download and install the appropriate version of VS Code according to your operating system.
|
||||
|
||||
@@ -18,7 +18,7 @@ VS Code has a powerful ecosystem of extensions that supports running and debuggi
|
||||
2. Search for `python` in the VS Code extension marketplace and install the Python Extension Pack.
|
||||
3. (Optional) Enter `pip install black` on the command line to install the code formatter.
|
||||
|
||||
### C/C++ Environment
|
||||
### C/c++ Environment
|
||||
|
||||
1. Windows systems need to install [MinGW](https://sourceforge.net/projects/mingw-w64/files/) ([configuration tutorial](https://blog.csdn.net/qq_33698226/article/details/129031241)); macOS comes with Clang built-in and does not require installation.
|
||||
2. Search for `c++` in the VS Code extension marketplace and install the C/C++ Extension Pack.
|
||||
@@ -46,12 +46,12 @@ VS Code has a powerful ecosystem of extensions that supports running and debuggi
|
||||
1. Download and install [Swift](https://www.swift.org/download/).
|
||||
2. Search for `swift` in the VS Code extension marketplace and install [Swift for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=sswg.swift-lang).
|
||||
|
||||
### JavaScript Environment
|
||||
### Javascript Environment
|
||||
|
||||
1. Download and install [Node.js](https://nodejs.org/en/).
|
||||
2. (Optional) Search for `Prettier` in the VS Code extension marketplace and install the code formatter.
|
||||
|
||||
### TypeScript Environment
|
||||
### Typescript Environment
|
||||
|
||||
1. Follow the same installation steps as the JavaScript environment.
|
||||
2. Install [TypeScript Execute (tsx)](https://github.com/privatenumber/tsx?tab=readme-ov-file#global-installation).
|
||||
|
||||
@@ -134,7 +134,7 @@ We can choose between two array initialization methods based on our needs: witho
|
||||
const nums = [_]i32{ 1, 3, 2, 5, 4 };
|
||||
```
|
||||
|
||||
??? pythontutor "Visualize code execution"
|
||||
??? pythontutor "Code Visualization"
|
||||
|
||||
https://pythontutor.com/render.html#code=%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0Aarr%20%3D%20%5B0%5D%20*%205%20%20%23%20%5B%200,%200,%200,%200,%200%20%5D%0Anums%20%3D%20%5B1,%203,%202,%205,%204%5D&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Arrays and Linked Lists
|
||||
# Array and Linked List
|
||||
|
||||

|
||||

|
||||
|
||||
!!! abstract
|
||||
|
||||
|
||||
@@ -450,7 +450,7 @@ Building a linked list involves two steps: first, initializing each node object;
|
||||
n3.next = &n4;
|
||||
```
|
||||
|
||||
??? pythontutor "Visualize code execution"
|
||||
??? pythontutor "Code Visualization"
|
||||
|
||||
https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E9%93%BE%E8%A1%A8%201%20-%3E%203%20-%3E%202%20-%3E%205%20-%3E%204%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%90%84%E4%B8%AA%E8%8A%82%E7%82%B9%0A%20%20%20%20n0%20%3D%20ListNode%281%29%0A%20%20%20%20n1%20%3D%20ListNode%283%29%0A%20%20%20%20n2%20%3D%20ListNode%282%29%0A%20%20%20%20n3%20%3D%20ListNode%285%29%0A%20%20%20%20n4%20%3D%20ListNode%284%29%0A%20%20%20%20%23%20%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%0A%20%20%20%20n0.next%20%3D%20n1%0A%20%20%20%20n1.next%20%3D%20n2%0A%20%20%20%20n2.next%20%3D%20n3%0A%20%20%20%20n3.next%20%3D%20n4&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ We typically use two initialization methods: "without initial values" and "with
|
||||
try nums.appendSlice(&[_]i32{ 1, 3, 2, 5, 4 });
|
||||
```
|
||||
|
||||
??? pythontutor "Visualize Execution"
|
||||
??? pythontutor "Code Visualization"
|
||||
|
||||
https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20%23%20%E6%97%A0%E5%88%9D%E5%A7%8B%E5%80%BC%0A%20%20%20%20nums1%20%3D%20%5B%5D%0A%20%20%20%20%23%20%E6%9C%89%E5%88%9D%E5%A7%8B%E5%80%BC%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D&cumulative=false&curInstr=4&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
||||
|
||||
@@ -297,7 +297,7 @@ Since a list is essentially an array, we can access and update elements in $O(1)
|
||||
nums.items[1] = 0; // Update element at index 1 to 0
|
||||
```
|
||||
|
||||
??? pythontutor "Visualize Execution"
|
||||
??? pythontutor "Code Visualization"
|
||||
|
||||
https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E5%85%83%E7%B4%A0%0A%20%20%20%20num%20%3D%20nums%5B1%5D%20%20%23%20%E8%AE%BF%E9%97%AE%E7%B4%A2%E5%BC%95%201%20%E5%A4%84%E7%9A%84%E5%85%83%E7%B4%A0%0A%0A%20%20%20%20%23%20%E6%9B%B4%E6%96%B0%E5%85%83%E7%B4%A0%0A%20%20%20%20nums%5B1%5D%20%3D%200%20%20%20%20%23%20%E5%B0%86%E7%B4%A2%E5%BC%95%201%20%E5%A4%84%E7%9A%84%E5%85%83%E7%B4%A0%E6%9B%B4%E6%96%B0%E4%B8%BA%200&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
||||
|
||||
@@ -571,7 +571,7 @@ Compared to arrays, lists can freely add and delete elements. Adding an element
|
||||
_ = nums.orderedRemove(3); // Delete element at index 3
|
||||
```
|
||||
|
||||
??? pythontutor "Visualize Execution"
|
||||
??? pythontutor "Code Visualization"
|
||||
|
||||
https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E6%9C%89%E5%88%9D%E5%A7%8B%E5%80%BC%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B8%85%E7%A9%BA%E5%88%97%E8%A1%A8%0A%20%20%20%20nums.clear%28%29%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%9C%A8%E5%B0%BE%E9%83%A8%E6%B7%BB%E5%8A%A0%E5%85%83%E7%B4%A0%0A%20%20%20%20nums.append%281%29%0A%20%20%20%20nums.append%283%29%0A%20%20%20%20nums.append%282%29%0A%20%20%20%20nums.append%285%29%0A%20%20%20%20nums.append%284%29%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%9C%A8%E4%B8%AD%E9%97%B4%E6%8F%92%E5%85%A5%E5%85%83%E7%B4%A0%0A%20%20%20%20nums.insert%283,%206%29%20%20%23%20%E5%9C%A8%E7%B4%A2%E5%BC%95%203%20%E5%A4%84%E6%8F%92%E5%85%A5%E6%95%B0%E5%AD%97%206%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E5%85%83%E7%B4%A0%0A%20%20%20%20nums.pop%283%29%20%20%20%20%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E7%B4%A2%E5%BC%95%203%20%E5%A4%84%E7%9A%84%E5%85%83%E7%B4%A0&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
||||
|
||||
@@ -789,7 +789,7 @@ Like arrays, lists can be traversed by index or by directly iterating through el
|
||||
}
|
||||
```
|
||||
|
||||
??? pythontutor "Visualize Execution"
|
||||
??? pythontutor "Code Visualization"
|
||||
|
||||
https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E9%80%9A%E8%BF%87%E7%B4%A2%E5%BC%95%E9%81%8D%E5%8E%86%E5%88%97%E8%A1%A8%0A%20%20%20%20count%20%3D%200%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20count%20%2B%3D%20nums%5Bi%5D%0A%0A%20%20%20%20%23%20%E7%9B%B4%E6%8E%A5%E9%81%8D%E5%8E%86%E5%88%97%E8%A1%A8%E5%85%83%E7%B4%A0%0A%20%20%20%20for%20num%20in%20nums%3A%0A%20%20%20%20%20%20%20%20count%20%2B%3D%20num&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
||||
|
||||
@@ -910,7 +910,7 @@ Given a new list `nums1`, we can concatenate it to the end of the original list.
|
||||
try nums.insertSlice(nums.items.len, nums1.items); // Concatenate list nums1 to the end of nums
|
||||
```
|
||||
|
||||
??? pythontutor "Visualize Execution"
|
||||
??? pythontutor "Code Visualization"
|
||||
|
||||
https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%8B%BC%E6%8E%A5%E4%B8%A4%E4%B8%AA%E5%88%97%E8%A1%A8%0A%20%20%20%20nums1%20%3D%20%5B6,%208,%207,%2010,%209%5D%0A%20%20%20%20nums%20%2B%3D%20nums1%20%20%23%20%E5%B0%86%E5%88%97%E8%A1%A8%20nums1%20%E6%8B%BC%E6%8E%A5%E5%88%B0%20nums%20%E4%B9%8B%E5%90%8E&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
||||
|
||||
@@ -1015,7 +1015,7 @@ After sorting a list, we can use "binary search" and "two-pointer" algorithms, w
|
||||
std.sort.sort(i32, nums.items, {}, comptime std.sort.asc(i32));
|
||||
```
|
||||
|
||||
??? pythontutor "Visualize Execution"
|
||||
??? pythontutor "Code Visualization"
|
||||
|
||||
https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%8E%92%E5%BA%8F%E5%88%97%E8%A1%A8%0A%20%20%20%20nums.sort%28%29%20%20%23%20%E6%8E%92%E5%BA%8F%E5%90%8E%EF%BC%8C%E5%88%97%E8%A1%A8%E5%85%83%E7%B4%A0%E4%BB%8E%E5%B0%8F%E5%88%B0%E5%A4%A7%E6%8E%92%E5%88%97&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Summary
|
||||
|
||||
### Key Takeaways
|
||||
### Key Review
|
||||
|
||||
- Arrays and linked lists are two fundamental data structures, representing two different ways data can be stored in computer memory: contiguous memory storage and scattered memory storage. The characteristics of the two complement each other.
|
||||
- Arrays support random access and use less memory; however, inserting and deleting elements is inefficient, and the length is immutable after initialization.
|
||||
@@ -28,7 +28,7 @@ Linked lists are composed of nodes, with nodes connected through references (poi
|
||||
In contrast, array elements must be of the same type, so that the corresponding element position can be obtained by calculating the offset. For example, if an array contains both `int` and `long` types, with individual elements occupying 4 bytes and 8 bytes respectively, then the following formula cannot be used to calculate the offset, because the array contains two different "element lengths".
|
||||
|
||||
```shell
|
||||
# Element memory address = Array memory address (first element memory address) + Element length * Element index
|
||||
# Element Memory Address = Array Memory Address (first Element Memory address) + Element Length * Element Index
|
||||
```
|
||||
|
||||
**Q**: After deleting node `P`, do we need to set `P.next` to `None`?
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# n-queens problem
|
||||
# N-Queens Problem
|
||||
|
||||
!!! question
|
||||
|
||||
@@ -12,7 +12,7 @@ The figure below illustrates the three constraints of this problem: **multiple q
|
||||
|
||||

|
||||
|
||||
### Row-by-row placement strategy
|
||||
### Row-By-Row Placement Strategy
|
||||
|
||||
Since both the number of queens and the number of rows on the chessboard are $n$, we can easily derive a conclusion: **each row of the chessboard allows and only allows exactly one queen to be placed**.
|
||||
|
||||
@@ -24,7 +24,7 @@ The figure below shows the row-by-row placement process for the 4-queens problem
|
||||
|
||||
Essentially, **the row-by-row placement strategy serves a pruning function**, as it avoids all search branches where multiple queens appear in the same row.
|
||||
|
||||
### Column and diagonal pruning
|
||||
### Column and Diagonal Pruning
|
||||
|
||||
To satisfy the column constraint, we can use a boolean array `cols` of length $n$ to record whether each column has a queen. Before each placement decision, we use `cols` to prune columns that already have queens, and dynamically update the state of `cols` during backtracking.
|
||||
|
||||
@@ -40,7 +40,7 @@ Similarly, **for all squares on an anti-diagonal, the sum $row + col$ is a const
|
||||
|
||||

|
||||
|
||||
### Code implementation
|
||||
### Code Implementation
|
||||
|
||||
Please note that in an $n$-dimensional square matrix, the range of $row - col$ is $[-n + 1, n - 1]$, and the range of $row + col$ is $[0, 2n - 2]$. Therefore, the number of both main diagonals and anti-diagonals is $2n - 1$, meaning the length of both arrays `diags1` and `diags2` is $2n - 1$.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ In algorithms, repeatedly executing a task is very common and closely related to
|
||||
|
||||
<u>Iteration</u> is a control structure for repeatedly executing a task. In iteration, a program repeatedly executes a segment of code under certain conditions until those conditions are no longer satisfied.
|
||||
|
||||
### for Loop
|
||||
### For Loop
|
||||
|
||||
The `for` loop is one of the most common forms of iteration, **suitable for use when the number of iterations is known in advance**.
|
||||
|
||||
@@ -22,7 +22,7 @@ The figure below shows the flowchart of this summation function.
|
||||
|
||||
The number of operations in this summation function is proportional to the input data size $n$, or has a "linear relationship". In fact, **time complexity describes precisely this "linear relationship"**. Related content will be introduced in detail in the next section.
|
||||
|
||||
### while Loop
|
||||
### While Loop
|
||||
|
||||
Similar to the `for` loop, the `while` loop is also a method for implementing iteration. In a `while` loop, the program first checks the condition in each round; if the condition is true, it continues execution, otherwise it ends the loop.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Time complexity
|
||||
# Time Complexity
|
||||
|
||||
Runtime can intuitively and accurately reflect the efficiency of an algorithm. If we want to accurately estimate the runtime of a piece of code, how should we proceed?
|
||||
|
||||
@@ -224,7 +224,7 @@ $$
|
||||
|
||||
In reality, however, **counting an algorithm's runtime is neither reasonable nor realistic**. First, we do not want to tie the estimated time to the running platform, because algorithms need to run on various different platforms. Second, it is difficult to know the runtime of each type of operation, which brings great difficulty to the estimation process.
|
||||
|
||||
## Counting time growth trends
|
||||
## Counting Time Growth Trends
|
||||
|
||||
Time complexity analysis does not count the algorithm's runtime, **but rather counts the growth trend of the algorithm's runtime as the data volume increases**.
|
||||
|
||||
@@ -536,7 +536,7 @@ Compared to directly counting the algorithm's runtime, what are the characterist
|
||||
- **The derivation method for time complexity is simpler**. Obviously, the running platform and the types of computational operations are both unrelated to the growth trend of the algorithm's runtime. Therefore, in time complexity analysis, we can simply treat the execution time of all computational operations as the same "unit time", thus simplifying "counting computational operation runtime" to "counting the number of computational operations", which greatly reduces the difficulty of estimation.
|
||||
- **Time complexity also has certain limitations**. For example, although algorithms `A` and `C` have the same time complexity, their actual runtimes differ significantly. Similarly, although algorithm `B` has a higher time complexity than `C`, when the input data size $n$ is small, algorithm `B` is clearly superior to algorithm `C`. In such cases, it is often difficult to judge the efficiency of algorithms based solely on time complexity. Of course, despite the above issues, complexity analysis remains the most effective and commonly used method for evaluating algorithm efficiency.
|
||||
|
||||
## Asymptotic upper bound of functions
|
||||
## Asymptotic Upper Bound of Functions
|
||||
|
||||
Given a function with input size $n$:
|
||||
|
||||
@@ -755,13 +755,13 @@ As shown in the figure below, calculating the asymptotic upper bound is to find
|
||||
|
||||

|
||||
|
||||
## Derivation method
|
||||
## Derivation Method
|
||||
|
||||
The asymptotic upper bound has a bit of mathematical flavor. If you feel you haven't fully understood it, don't worry. We can first master the derivation method, and gradually grasp its mathematical meaning through continuous practice.
|
||||
|
||||
According to the definition, after determining $f(n)$, we can obtain the time complexity $O(f(n))$. So how do we determine the asymptotic upper bound $f(n)$? Overall, it is divided into two steps: first count the number of operations, then determine the asymptotic upper bound.
|
||||
|
||||
### Step 1: Count the number of operations
|
||||
### Step 1: Count the Number of Operations
|
||||
|
||||
For code, count from top to bottom line by line. However, since the constant coefficient $c$ in $c \cdot f(n)$ above can be of any size, **coefficients and constant terms in the number of operations $T(n)$ can all be ignored**. According to this principle, the following counting simplification techniques can be summarized.
|
||||
|
||||
@@ -1043,7 +1043,7 @@ T(n) & = n^2 + n & \text{Simplified count (o.O)}
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
### Step 2: Determine the asymptotic upper bound
|
||||
### Step 2: Determine the Asymptotic Upper Bound
|
||||
|
||||
**Time complexity is determined by the highest-order term in $T(n)$**. This is because as $n$ tends to infinity, the highest-order term will play a dominant role, and the influence of other terms can be ignored.
|
||||
|
||||
@@ -1059,7 +1059,7 @@ The table below shows some examples, where some exaggerated values are used to e
|
||||
| $n^3 + 10000n^2$ | $O(n^3)$ |
|
||||
| $2^n + 10000n^{10000}$ | $O(2^n)$ |
|
||||
|
||||
## Common types
|
||||
## Common Types
|
||||
|
||||
Let the input data size be $n$. Common time complexity types are shown in the figure below (arranged in order from low to high).
|
||||
|
||||
@@ -1072,7 +1072,7 @@ $$
|
||||
|
||||

|
||||
|
||||
### Constant order $O(1)$
|
||||
### Constant Order $O(1)$
|
||||
|
||||
The number of operations in constant order is independent of the input data size $n$, meaning it does not change as $n$ changes.
|
||||
|
||||
@@ -1082,7 +1082,7 @@ In the following function, although the number of operations `size` may be large
|
||||
[file]{time_complexity}-[class]{}-[func]{constant}
|
||||
```
|
||||
|
||||
### Linear order $O(n)$
|
||||
### Linear Order $O(n)$
|
||||
|
||||
The number of operations in linear order grows linearly relative to the input data size $n$. Linear order typically appears in single-layer loops:
|
||||
|
||||
@@ -1098,7 +1098,7 @@ Operations such as traversing arrays and traversing linked lists have a time com
|
||||
|
||||
It is worth noting that **the input data size $n$ should be determined according to the type of input data**. For example, in the first example, the variable $n$ is the input data size; in the second example, the array length $n$ is the data size.
|
||||
|
||||
### Quadratic order $O(n^2)$
|
||||
### Quadratic Order $O(n^2)$
|
||||
|
||||
The number of operations in quadratic order grows quadratically relative to the input data size $n$. Quadratic order typically appears in nested loops, where both the outer and inner loops have a time complexity of $O(n)$, resulting in an overall time complexity of $O(n^2)$:
|
||||
|
||||
@@ -1116,7 +1116,7 @@ Taking bubble sort as an example, the outer loop executes $n - 1$ times, and the
|
||||
[file]{time_complexity}-[class]{}-[func]{bubble_sort}
|
||||
```
|
||||
|
||||
### Exponential order $O(2^n)$
|
||||
### Exponential Order $O(2^n)$
|
||||
|
||||
Biological "cell division" is a typical example of exponential order growth: the initial state is $1$ cell, after one round of division it becomes $2$, after two rounds it becomes $4$, and so on; after $n$ rounds of division there are $2^n$ cells.
|
||||
|
||||
@@ -1136,7 +1136,7 @@ In actual algorithms, exponential order often appears in recursive functions. Fo
|
||||
|
||||
Exponential order growth is very rapid and is common in exhaustive methods (brute force search, backtracking, etc.). For problems with large data scales, exponential order is unacceptable and typically requires dynamic programming or greedy algorithms to solve.
|
||||
|
||||
### Logarithmic order $O(\log n)$
|
||||
### Logarithmic Order $O(\log n)$
|
||||
|
||||
In contrast to exponential order, logarithmic order reflects the situation of "reducing to half each round". Let the input data size be $n$. Since it is reduced to half each round, the number of loops is $\log_2 n$, which is the inverse function of $2^n$.
|
||||
|
||||
@@ -1166,7 +1166,7 @@ Logarithmic order commonly appears in algorithms based on the divide-and-conquer
|
||||
|
||||
That is to say, the base $m$ can be converted without affecting the complexity. Therefore, we usually omit the base $m$ and denote logarithmic order simply as $O(\log n)$.
|
||||
|
||||
### Linearithmic order $O(n \log n)$
|
||||
### Linearithmic Order $O(n \log n)$
|
||||
|
||||
Linearithmic order commonly appears in nested loops, where the time complexities of the two layers of loops are $O(\log n)$ and $O(n)$ respectively. The relevant code is as follows:
|
||||
|
||||
@@ -1180,7 +1180,7 @@ The figure below shows how linearithmic order is generated. Each level of the bi
|
||||
|
||||
Mainstream sorting algorithms typically have a time complexity of $O(n \log n)$, such as quicksort, merge sort, and heap sort.
|
||||
|
||||
### Factorial order $O(n!)$
|
||||
### Factorial Order $O(n!)$
|
||||
|
||||
Factorial order corresponds to the mathematical "permutation" problem. Given $n$ distinct elements, find all possible permutation schemes; the number of schemes is:
|
||||
|
||||
@@ -1198,7 +1198,7 @@ Factorials are typically implemented using recursion. As shown in the figure bel
|
||||
|
||||
Note that because when $n \geq 4$ we always have $n! > 2^n$, factorial order grows faster than exponential order, and is also unacceptable for large $n$.
|
||||
|
||||
## Worst, best, and average time complexities
|
||||
## Worst, Best, and Average Time Complexities
|
||||
|
||||
**The time efficiency of an algorithm is often not fixed, but is related to the distribution of the input data**. Suppose we input an array `nums` of length $n$, where `nums` consists of numbers from $1$ to $n$, with each number appearing only once, but the element order is randomly shuffled. The task is to return the index of element $1$. We can draw the following conclusions.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
In computers, all data is stored in binary form, and character `char` is no exception. To represent characters, we need to establish a "character set" that defines a one-to-one correspondence between each character and binary numbers. With a character set, computers can convert binary numbers to characters by looking up the table.
|
||||
|
||||
## ASCII Character Set
|
||||
## Ascii Character Set
|
||||
|
||||
<u>ASCII code</u> is the earliest character set, with the full name American Standard Code for Information Interchange. It uses 7 binary bits (the lower 7 bits of one byte) to represent a character, and can represent a maximum of 128 different characters. As shown in the figure below, ASCII code includes uppercase and lowercase English letters, numbers 0 ~ 9, some punctuation marks, and some control characters (such as newline and tab).
|
||||
|
||||
@@ -12,7 +12,7 @@ However, **ASCII code can only represent English**. With the globalization of co
|
||||
|
||||
Worldwide, a batch of EASCII character sets suitable for different regions have appeared successively. The first 128 characters of these character sets are unified as ASCII code, and the last 128 characters are defined differently to adapt to the needs of different languages.
|
||||
|
||||
## GBK Character Set
|
||||
## Gbk Character Set
|
||||
|
||||
Later, people found that **EASCII code still cannot meet the character quantity requirements of many languages**. For example, there are nearly one hundred thousand Chinese characters, and several thousand are used daily. In 1980, the China National Standardization Administration released the <u>GB2312</u> character set, which included 6,763 Chinese characters, basically meeting the needs for computer processing of Chinese characters.
|
||||
|
||||
@@ -36,7 +36,7 @@ For the above problem, **a straightforward solution is to store all characters a
|
||||
|
||||
However, ASCII code has already proven to us that encoding English only requires 1 byte. If the above scheme is adopted, the size of English text will be twice that under ASCII encoding, which is very wasteful of memory space. Therefore, we need a more efficient Unicode encoding method.
|
||||
|
||||
## UTF-8 Encoding
|
||||
## Utf-8 Encoding
|
||||
|
||||
Currently, UTF-8 has become the most widely used Unicode encoding method internationally. **It is a variable-length encoding** that uses 1 to 4 bytes to represent a character, depending on the complexity of the character. ASCII characters only require 1 byte, Latin and Greek letters require 2 bytes, commonly used Chinese characters require 3 bytes, and some other rare characters require 4 bytes.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Data Structure
|
||||
# Data Structures
|
||||
|
||||

|
||||

|
||||
|
||||
!!! abstract
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Summary
|
||||
|
||||
### Key review
|
||||
### Key Review
|
||||
|
||||
- Data structures can be classified from two perspectives: logical structure and physical structure. Logical structure describes the logical relationships between data elements, while physical structure describes how data is stored in computer memory.
|
||||
- Common logical structures include linear, tree, and network structures. We typically classify data structures as linear (arrays, linked lists, stacks, queues) and non-linear (trees, graphs, heaps) based on their logical structure. The implementation of hash tables may involve both linear and non-linear data structures.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Divide and conquer search strategy
|
||||
# Divide and Conquer Search Strategy
|
||||
|
||||
We have already learned that search algorithms are divided into two major categories.
|
||||
|
||||
@@ -18,7 +18,7 @@ The divide and conquer strategy of binary search is as follows.
|
||||
|
||||
Divide and conquer can improve search efficiency because brute-force search can only eliminate one option per round, **while divide and conquer search can eliminate half of the options per round**.
|
||||
|
||||
### Implementing binary search based on divide and conquer
|
||||
### Implementing Binary Search Based on Divide and Conquer
|
||||
|
||||
In previous sections, binary search was implemented based on iteration. Now we implement it based on divide and conquer (recursion).
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Building a binary tree problem
|
||||
# Building a Binary Tree Problem
|
||||
|
||||
!!! question
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||

|
||||
|
||||
### Determining if it is a divide and conquer problem
|
||||
### Determining If It Is a Divide and Conquer Problem
|
||||
|
||||
The original problem is defined as constructing a binary tree from `preorder` and `inorder`, which is a typical divide and conquer problem.
|
||||
|
||||
@@ -14,7 +14,7 @@ The original problem is defined as constructing a binary tree from `preorder` an
|
||||
- **Subproblems are independent**: The left and right subtrees are independent of each other; there is no overlap between them. When constructing the left subtree, we only need to focus on the parts of the inorder and preorder traversals corresponding to the left subtree. The same applies to the right subtree.
|
||||
- **Solutions of subproblems can be merged**: Once we have the left and right subtrees (solutions of subproblems), we can link them to the root node to obtain the solution to the original problem.
|
||||
|
||||
### How to divide subtrees
|
||||
### How to Divide Subtrees
|
||||
|
||||
Based on the above analysis, this problem can be solved using divide and conquer, **but how do we divide the left and right subtrees through the preorder traversal `preorder` and inorder traversal `inorder`**?
|
||||
|
||||
@@ -31,7 +31,7 @@ Using the data from the figure above as an example, we can obtain the division r
|
||||
|
||||

|
||||
|
||||
### Describing subtree intervals based on variables
|
||||
### Describing Subtree Intervals Based on Variables
|
||||
|
||||
Based on the above division method, **we have obtained the index intervals of the root node, left subtree, and right subtree in `preorder` and `inorder`**. To describe these index intervals, we need to use several pointer variables.
|
||||
|
||||
@@ -53,7 +53,7 @@ Please note that $(m-l)$ in the right subtree root node index means "the number
|
||||
|
||||

|
||||
|
||||
### Code implementation
|
||||
### Code Implementation
|
||||
|
||||
To improve the efficiency of querying $m$, we use a hash table `hmap` to store the mapping from elements in the `inorder` array to their indices:
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Divide and conquer algorithms
|
||||
# Divide and Conquer Algorithms
|
||||
|
||||
<u>Divide and conquer</u> is a very important and common algorithm strategy. Divide and conquer is typically implemented based on recursion, consisting of two steps: "divide" and "conquer".
|
||||
|
||||
@@ -12,7 +12,7 @@ As shown in the figure below, "merge sort" is one of the typical applications of
|
||||
|
||||

|
||||
|
||||
## How to determine divide and conquer problems
|
||||
## How to Determine Divide and Conquer Problems
|
||||
|
||||
Whether a problem is suitable for solving with divide and conquer can usually be determined based on the following criteria.
|
||||
|
||||
@@ -26,13 +26,13 @@ Clearly, merge sort satisfies these three criteria.
|
||||
2. **Subproblems are independent**: Each subarray can be sorted independently (subproblems can be solved independently).
|
||||
3. **Solutions of subproblems can be merged**: Two sorted subarrays (solutions of subproblems) can be merged into one sorted array (solution of the original problem).
|
||||
|
||||
## Improving efficiency through divide and conquer
|
||||
## Improving Efficiency Through Divide and Conquer
|
||||
|
||||
**Divide and conquer can not only effectively solve algorithmic problems but often also improve algorithm efficiency**. In sorting algorithms, quick sort, merge sort, and heap sort are faster than selection, bubble, and insertion sort because they apply the divide and conquer strategy.
|
||||
|
||||
This raises the question: **Why can divide and conquer improve algorithm efficiency, and what is the underlying logic**? In other words, why is dividing a large problem into multiple subproblems, solving the subproblems, and merging their solutions more efficient than directly solving the original problem? This question can be discussed from two aspects: operation count and parallel computation.
|
||||
|
||||
### Operation count optimization
|
||||
### Operation Count Optimization
|
||||
|
||||
Taking "bubble sort" as an example, processing an array of length $n$ requires $O(n^2)$ time. Suppose we divide the array into two subarrays from the midpoint as shown in the figure below, the division requires $O(n)$ time, sorting each subarray requires $O((n / 2)^2)$ time, and merging the two subarrays requires $O(n)$ time, resulting in an overall time complexity of:
|
||||
|
||||
@@ -58,7 +58,7 @@ Going further, **what if we continuously divide the subarrays from their midpoin
|
||||
|
||||
Thinking further, **what if we set multiple division points** and evenly divide the original array into $k$ subarrays? This situation is very similar to "bucket sort", which is well-suited for sorting massive amounts of data, with a theoretical time complexity of $O(n + k)$.
|
||||
|
||||
### Parallel computation optimization
|
||||
### Parallel Computation Optimization
|
||||
|
||||
We know that the subproblems generated by divide and conquer are independent of each other, **so they can typically be solved in parallel**. This means divide and conquer can not only reduce the time complexity of algorithms, **but also benefits from parallel optimization by operating systems**.
|
||||
|
||||
@@ -68,7 +68,7 @@ For example, in the "bucket sort" shown in the figure below, we evenly distribut
|
||||
|
||||

|
||||
|
||||
## Common applications of divide and conquer
|
||||
## Common Applications of Divide and Conquer
|
||||
|
||||
On one hand, divide and conquer can be used to solve many classic algorithmic problems.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Hanota problem
|
||||
# Hanota Problem
|
||||
|
||||
In merge sort and building binary trees, we decompose the original problem into two subproblems, each half the size of the original problem. However, for the hanota problem, we adopt a different decomposition strategy.
|
||||
|
||||
@@ -14,7 +14,7 @@ In merge sort and building binary trees, we decompose the original problem into
|
||||
|
||||
**We denote the hanota problem of size $i$ as $f(i)$**. For example, $f(3)$ represents moving $3$ discs from `A` to `C`.
|
||||
|
||||
### Considering the base cases
|
||||
### Considering the Base Cases
|
||||
|
||||
As shown in the figure below, for problem $f(1)$, when there is only one disc, we can move it directly from `A` to `C`.
|
||||
|
||||
@@ -44,7 +44,7 @@ As shown in the figure below, for problem $f(2)$, when there are two discs, **si
|
||||
|
||||
The process of solving problem $f(2)$ can be summarized as: **moving two discs from `A` to `C` with the help of `B`**. Here, `C` is called the target pillar, and `B` is called the buffer pillar.
|
||||
|
||||
### Subproblem decomposition
|
||||
### Subproblem Decomposition
|
||||
|
||||
For problem $f(3)$, when there are three discs, the situation becomes slightly more complex.
|
||||
|
||||
@@ -78,7 +78,7 @@ For these two subproblems $f(n-1)$, **we can recursively divide them in the same
|
||||
|
||||

|
||||
|
||||
### Code implementation
|
||||
### Code Implementation
|
||||
|
||||
In the code, we declare a recursive function `dfs(i, src, buf, tar)`, whose purpose is to move the top $i$ discs from pillar `src` to target pillar `tar` with the help of buffer pillar `buf`:
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Divide and conquer
|
||||
# Divide and Conquer
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Summary
|
||||
|
||||
### Key Review
|
||||
|
||||
- Divide and conquer is a common algorithm design strategy, consisting of two phases: divide (partition) and conquer (merge), typically implemented based on recursion.
|
||||
- The criteria for determining whether a problem is a divide and conquer problem include: whether the problem can be decomposed, whether subproblems are independent, and whether subproblems can be merged.
|
||||
- Merge sort is a typical application of the divide and conquer strategy. It recursively divides an array into two equal-length subarrays until only one element remains, then merges them layer by layer to complete the sorting.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Characteristics of dynamic programming problems
|
||||
# Characteristics of Dynamic Programming Problems
|
||||
|
||||
In the previous section, we learned how dynamic programming solves the original problem by decomposing it into subproblems. In fact, subproblem decomposition is a general algorithmic approach, with different emphases in divide and conquer, dynamic programming, and backtracking.
|
||||
|
||||
@@ -8,7 +8,7 @@ In the previous section, we learned how dynamic programming solves the original
|
||||
|
||||
In fact, dynamic programming is commonly used to solve optimization problems, which not only contain overlapping subproblems but also have two other major characteristics: optimal substructure and no aftereffects.
|
||||
|
||||
## Optimal substructure
|
||||
## Optimal Substructure
|
||||
|
||||
We make a slight modification to the stair climbing problem to make it more suitable for demonstrating the concept of optimal substructure.
|
||||
|
||||
@@ -48,7 +48,7 @@ This problem can also be space-optimized, compressing from one dimension to zero
|
||||
[file]{min_cost_climbing_stairs_dp}-[class]{}-[func]{min_cost_climbing_stairs_dp_comp}
|
||||
```
|
||||
|
||||
## No aftereffects
|
||||
## No Aftereffects
|
||||
|
||||
No aftereffects is one of the important characteristics that enable dynamic programming to solve problems effectively. Its definition is: **given a certain state, its future development is only related to the current state and has nothing to do with all past states**.
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Dynamic programming problem-solving approach
|
||||
# Dynamic Programming Problem-Solving Approach
|
||||
|
||||
The previous two sections introduced the main characteristics of dynamic programming problems. Next, let us explore two more practical issues together.
|
||||
|
||||
1. How to determine whether a problem is a dynamic programming problem?
|
||||
2. What is the complete process for solving a dynamic programming problem, and where should we start?
|
||||
|
||||
## Problem determination
|
||||
## Problem Determination
|
||||
|
||||
Generally speaking, if a problem contains overlapping subproblems, optimal substructure, and satisfies no aftereffects, then it is usually suitable for solving with dynamic programming. However, it is difficult to directly extract these characteristics from the problem description. Therefore, we usually relax the conditions and **first observe whether the problem is suitable for solving with backtracking (exhaustive search)**.
|
||||
|
||||
@@ -25,7 +25,7 @@ Correspondingly, there are also some "penalty points".
|
||||
|
||||
If a problem satisfies the decision tree model and has relatively obvious "bonus points", we can assume it is a dynamic programming problem and verify it during the solving process.
|
||||
|
||||
## Problem-solving steps
|
||||
## Problem-Solving Steps
|
||||
|
||||
The problem-solving process for dynamic programming varies depending on the nature and difficulty of the problem, but generally follows these steps: describe decisions, define states, establish the $dp$ table, derive state transition equations, determine boundary conditions, etc.
|
||||
|
||||
@@ -89,7 +89,7 @@ As shown in the figure below, since each cell is transferred from the cell to it
|
||||
|
||||
Based on the above analysis, we can directly write the dynamic programming code. However, subproblem decomposition is a top-down approach, so implementing in the order "brute force search $\rightarrow$ memoization $\rightarrow$ dynamic programming" is more aligned with thinking habits.
|
||||
|
||||
### Method 1: Brute force search
|
||||
### Method 1: Brute Force Search
|
||||
|
||||
Starting from state $[i, j]$, continuously decompose into smaller states $[i-1, j]$ and $[i, j-1]$. The recursive function includes the following elements.
|
||||
|
||||
@@ -124,7 +124,7 @@ As shown in the figure below, after introducing memoization, all subproblem solu
|
||||
|
||||

|
||||
|
||||
### Method 3: Dynamic programming
|
||||
### Method 3: Dynamic Programming
|
||||
|
||||
Implement the dynamic programming solution based on iteration, as shown in the code below:
|
||||
|
||||
@@ -172,7 +172,7 @@ The array `dp` has size $n \times m$, **thus the space complexity is $O(nm)$**.
|
||||
=== "<12>"
|
||||

|
||||
|
||||
### Space optimization
|
||||
### Space Optimization
|
||||
|
||||
Since each cell is only related to the cell to its left and the cell above it, we can use a single-row array to implement the $dp$ table.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Edit distance problem
|
||||
# Edit Distance Problem
|
||||
|
||||
Edit distance, also known as Levenshtein distance, refers to the minimum number of edits required to transform one string into another, commonly used in information retrieval and natural language processing to measure the similarity between two sequences.
|
||||
|
||||
@@ -20,7 +20,7 @@ From the perspective of the decision tree, the goal of this problem is to find t
|
||||
|
||||

|
||||
|
||||
### Dynamic programming approach
|
||||
### Dynamic Programming Approach
|
||||
|
||||
**Step 1: Think about the decisions in each round, define the state, and thus obtain the $dp$ table**
|
||||
|
||||
@@ -65,7 +65,7 @@ When both strings are empty, the number of edit steps is $0$, i.e., $dp[0, 0] =
|
||||
|
||||
Observing the state transition equation, the solution $dp[i, j]$ depends on solutions to the left, above, and upper-left, so the entire $dp$ table can be traversed in order through two nested loops.
|
||||
|
||||
### Code implementation
|
||||
### Code Implementation
|
||||
|
||||
```src
|
||||
[file]{edit_distance}-[class]{}-[func]{edit_distance_dp}
|
||||
@@ -118,7 +118,7 @@ As shown in the figure below, the state transition process for the edit distance
|
||||
=== "<15>"
|
||||

|
||||
|
||||
### Space optimization
|
||||
### Space Optimization
|
||||
|
||||
Since $dp[i, j]$ is transferred from the solutions above $dp[i-1, j]$, to the left $dp[i, j-1]$, and to the upper-left $dp[i-1, j-1]$, forward traversal will lose the upper-left solution $dp[i-1, j-1]$, and reverse traversal cannot build $dp[i, j-1]$ in advance, so neither traversal order is feasible.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Dynamic programming
|
||||
# Dynamic Programming
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Introduction to dynamic programming
|
||||
# Introduction to Dynamic Programming
|
||||
|
||||
<u>Dynamic programming</u> is an important algorithmic paradigm that decomposes a problem into a series of smaller subproblems and avoids redundant computation by storing the solutions to subproblems, thereby significantly improving time efficiency.
|
||||
|
||||
@@ -18,7 +18,7 @@ The goal of this problem is to find the number of ways, **we can consider using
|
||||
[file]{climbing_stairs_backtrack}-[class]{}-[func]{climbing_stairs_backtrack}
|
||||
```
|
||||
|
||||
## Method 1: Brute force search
|
||||
## Method 1: Brute Force Search
|
||||
|
||||
Backtracking algorithms typically do not explicitly decompose problems, but rather treat solving the problem as a series of decision steps, searching for all possible solutions through trial and pruning.
|
||||
|
||||
@@ -73,7 +73,7 @@ Observe the figure below, **after memoization, all overlapping subproblems only
|
||||
|
||||

|
||||
|
||||
## Method 3: Dynamic programming
|
||||
## Method 3: Dynamic Programming
|
||||
|
||||
**Memoization is a "top-down" method**: we start from the original problem (root node), recursively decompose larger subproblems into smaller ones, until reaching the smallest known subproblems (leaf nodes). Afterward, by backtracking, we collect the solutions to the subproblems layer by layer to construct the solution to the original problem.
|
||||
|
||||
@@ -97,7 +97,7 @@ Based on the above content, we can summarize the commonly used terminology in dy
|
||||
- The states corresponding to the smallest subproblems (the $1$st and $2$nd steps) are called <u>initial states</u>.
|
||||
- The recurrence formula $dp[i] = dp[i-1] + dp[i-2]$ is called the <u>state transition equation</u>.
|
||||
|
||||
## Space optimization
|
||||
## Space Optimization
|
||||
|
||||
Observant readers may have noticed that **since $dp[i]$ is only related to $dp[i-1]$ and $dp[i-2]$, we do not need to use an array `dp` to store the solutions to all subproblems**, but can simply use two variables to roll forward. The code is as follows:
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 0-1 knapsack problem
|
||||
# 0-1 Knapsack Problem
|
||||
|
||||
The knapsack problem is an excellent introductory problem for dynamic programming and is one of the most common problem forms in dynamic programming. It has many variants, such as the 0-1 knapsack problem, the unbounded knapsack problem, and the multiple knapsack problem.
|
||||
|
||||
@@ -47,7 +47,7 @@ The current state $[i, c]$ is transferred from the state above $[i-1, c]$ and th
|
||||
|
||||
Based on the above analysis, we will next implement the brute force search, memoization, and dynamic programming solutions in order.
|
||||
|
||||
### Method 1: Brute force search
|
||||
### Method 1: Brute Force Search
|
||||
|
||||
The search code includes the following elements.
|
||||
|
||||
@@ -80,7 +80,7 @@ The figure below shows the search branches pruned in memoization.
|
||||
|
||||

|
||||
|
||||
### Method 3: Dynamic programming
|
||||
### Method 3: Dynamic Programming
|
||||
|
||||
Dynamic programming is essentially the process of filling the $dp$ table during state transitions. The code is as follows:
|
||||
|
||||
@@ -132,7 +132,7 @@ As shown in the figure below, both time complexity and space complexity are dete
|
||||
=== "<14>"
|
||||

|
||||
|
||||
### Space optimization
|
||||
### Space Optimization
|
||||
|
||||
Since each state is only related to the state in the row above it, we can use two arrays rolling forward to reduce the space complexity from $O(n^2)$ to $O(n)$.
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Summary
|
||||
|
||||
### Key Review
|
||||
|
||||
- Dynamic programming decomposes problems and avoids redundant computation by storing the solutions to subproblems, thereby significantly improving computational efficiency.
|
||||
- Without considering time constraints, all dynamic programming problems can be solved using backtracking (brute force search), but the recursion tree contains a large number of overlapping subproblems, resulting in extremely low efficiency. By introducing a memo list, we can store the solutions to all computed subproblems, ensuring that overlapping subproblems are only computed once.
|
||||
- Memoization is a top-down recursive solution, while the corresponding dynamic programming is a bottom-up iterative solution, similar to "filling in a table". Since the current state only depends on certain local states, we can eliminate one dimension of the $dp$ table to reduce space complexity.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Unbounded knapsack problem
|
||||
# Unbounded Knapsack Problem
|
||||
|
||||
In this section, we first solve another common knapsack problem: the unbounded knapsack, and then explore a special case of it: the coin change problem.
|
||||
|
||||
## Unbounded knapsack problem
|
||||
## Unbounded Knapsack Problem
|
||||
|
||||
!!! question
|
||||
|
||||
@@ -10,7 +10,7 @@ In this section, we first solve another common knapsack problem: the unbounded k
|
||||
|
||||

|
||||
|
||||
### Dynamic programming approach
|
||||
### Dynamic Programming Approach
|
||||
|
||||
The unbounded knapsack problem is very similar to the 0-1 knapsack problem, **differing only in that there is no limit on the number of times an item can be selected**.
|
||||
|
||||
@@ -28,7 +28,7 @@ $$
|
||||
dp[i, c] = \max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1])
|
||||
$$
|
||||
|
||||
### Code implementation
|
||||
### Code Implementation
|
||||
|
||||
Comparing the code for the two problems, there is one change in state transition from $i-1$ to $i$, with everything else identical:
|
||||
|
||||
@@ -36,7 +36,7 @@ Comparing the code for the two problems, there is one change in state transition
|
||||
[file]{unbounded_knapsack}-[class]{}-[func]{unbounded_knapsack_dp}
|
||||
```
|
||||
|
||||
### Space optimization
|
||||
### Space Optimization
|
||||
|
||||
Since the current state is transferred from states on the left and above, **after space optimization, each row in the $dp$ table should be traversed in forward order**.
|
||||
|
||||
@@ -66,7 +66,7 @@ The code implementation is relatively simple, just delete the first dimension of
|
||||
[file]{unbounded_knapsack}-[class]{}-[func]{unbounded_knapsack_dp_comp}
|
||||
```
|
||||
|
||||
## Coin change problem
|
||||
## Coin Change Problem
|
||||
|
||||
The knapsack problem represents a large class of dynamic programming problems and has many variants, such as the coin change problem.
|
||||
|
||||
@@ -76,7 +76,7 @@ The knapsack problem represents a large class of dynamic programming problems an
|
||||
|
||||

|
||||
|
||||
### Dynamic programming approach
|
||||
### Dynamic Programming Approach
|
||||
|
||||
**The coin change problem can be viewed as a special case of the unbounded knapsack problem**, with the following connections and differences.
|
||||
|
||||
@@ -107,7 +107,7 @@ When the target amount is $0$, the minimum number of coins needed to make it up
|
||||
|
||||
When there are no coins, **it is impossible to make up any amount $> 0$**, which is an invalid solution. To enable the $\min()$ function in the state transition equation to identify and filter out invalid solutions, we consider using $+ \infty$ to represent them, i.e., set all $dp[0, a]$ in the first row to $+ \infty$.
|
||||
|
||||
### Code implementation
|
||||
### Code Implementation
|
||||
|
||||
Most programming languages do not provide a $+ \infty$ variable, and can only use the maximum value of integer type `int` as a substitute. However, this can lead to large number overflow: the $+ 1$ operation in the state transition equation may cause overflow.
|
||||
|
||||
@@ -164,7 +164,7 @@ The figure below shows the dynamic programming process for coin change, which is
|
||||
=== "<15>"
|
||||

|
||||
|
||||
### Space optimization
|
||||
### Space Optimization
|
||||
|
||||
The space optimization for the coin change problem is handled in the same way as the unbounded knapsack problem:
|
||||
|
||||
@@ -172,7 +172,7 @@ The space optimization for the coin change problem is handled in the same way as
|
||||
[file]{coin_change}-[class]{}-[func]{coin_change_dp_comp}
|
||||
```
|
||||
|
||||
## Coin change problem II
|
||||
## Coin Change Problem Ii
|
||||
|
||||
!!! question
|
||||
|
||||
@@ -180,7 +180,7 @@ The space optimization for the coin change problem is handled in the same way as
|
||||
|
||||

|
||||
|
||||
### Dynamic programming approach
|
||||
### Dynamic Programming Approach
|
||||
|
||||
Compared to the previous problem, this problem's goal is to find the number of combinations, so the subproblem becomes: **the number of combinations among the first $i$ types of coins that can make up amount $a$**. The $dp$ table remains a two-dimensional matrix of size $(n+1) \times (amt + 1)$.
|
||||
|
||||
@@ -192,13 +192,13 @@ $$
|
||||
|
||||
When the target amount is $0$, no coins need to be selected to make up the target amount, so all $dp[i, 0]$ in the first column should be initialized to $1$. When there are no coins, it is impossible to make up any amount $>0$, so all $dp[0, a]$ in the first row equal $0$.
|
||||
|
||||
### Code implementation
|
||||
### Code Implementation
|
||||
|
||||
```src
|
||||
[file]{coin_change_ii}-[class]{}-[func]{coin_change_ii_dp}
|
||||
```
|
||||
|
||||
### Space optimization
|
||||
### Space Optimization
|
||||
|
||||
The space optimization is handled in the same way, just delete the coin dimension:
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ If we view vertices as nodes and edges as references (pointers) connecting the n
|
||||
|
||||

|
||||
|
||||
## Common types and terminology of graphs
|
||||
## Common Types and Terminology of Graphs
|
||||
|
||||
Graphs can be divided into <u>undirected graphs</u> and <u>directed graphs</u> based on whether edges have direction, as shown in the figure below.
|
||||
|
||||
@@ -40,11 +40,11 @@ Graph data structures include the following commonly used terms.
|
||||
- <u>Path</u>: The sequence of edges from vertex A to vertex B is called a "path" from A to B. In the figure above, the edge sequence 1-5-2-4 is a path from vertex 1 to vertex 4.
|
||||
- <u>Degree</u>: The number of edges a vertex has. For directed graphs, <u>in-degree</u> indicates how many edges point to the vertex, and <u>out-degree</u> indicates how many edges point out from the vertex.
|
||||
|
||||
## Representation of graphs
|
||||
## Representation of Graphs
|
||||
|
||||
Common representations of graphs include "adjacency matrices" and "adjacency lists". The following uses undirected graphs as examples.
|
||||
|
||||
### Adjacency matrix
|
||||
### Adjacency Matrix
|
||||
|
||||
Given a graph with $n$ vertices, an <u>adjacency matrix</u> uses an $n \times n$ matrix to represent the graph, where each row (column) represents a vertex, and matrix elements represent edges, using $1$ or $0$ to indicate whether an edge exists between two vertices.
|
||||
|
||||
@@ -60,7 +60,7 @@ Adjacency matrices have the following properties.
|
||||
|
||||
When using adjacency matrices to represent graphs, we can directly access matrix elements to obtain edges, resulting in highly efficient addition, deletion, lookup, and modification operations, all with a time complexity of $O(1)$. However, the space complexity of the matrix is $O(n^2)$, which consumes significant memory.
|
||||
|
||||
### Adjacency list
|
||||
### Adjacency List
|
||||
|
||||
An <u>adjacency list</u> uses $n$ linked lists to represent a graph, with linked list nodes representing vertices. The $i$-th linked list corresponds to vertex $i$ and stores all adjacent vertices of that vertex (vertices connected to that vertex). The figure below shows an example of a graph stored using an adjacency list.
|
||||
|
||||
@@ -70,7 +70,7 @@ Adjacency lists only store edges that actually exist, and the total number of ed
|
||||
|
||||
Observing the figure above, **the structure of adjacency lists is very similar to "chaining" in hash tables, so we can adopt similar methods to optimize efficiency**. For example, when linked lists are long, they can be converted to AVL trees or red-black trees, thereby optimizing time efficiency from $O(n)$ to $O(\log n)$; linked lists can also be converted to hash tables, thereby reducing time complexity to $O(1)$.
|
||||
|
||||
## Common applications of graphs
|
||||
## Common Applications of Graphs
|
||||
|
||||
As shown in the table below, many real-world systems can be modeled using graphs, and corresponding problems can be reduced to graph computation problems.
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Basic operations on graphs
|
||||
# Basic Operations on Graphs
|
||||
|
||||
Basic operations on graphs can be divided into operations on "edges" and operations on "vertices". Under the two representation methods of "adjacency matrix" and "adjacency list", the implementation methods differ.
|
||||
|
||||
## Implementation based on adjacency matrix
|
||||
## Implementation Based on Adjacency Matrix
|
||||
|
||||
Given an undirected graph with $n$ vertices, the various operations are implemented as shown in the figure below.
|
||||
|
||||
@@ -32,7 +32,7 @@ The following is the implementation code for graphs represented using an adjacen
|
||||
[file]{graph_adjacency_matrix}-[class]{graph_adj_mat}-[func]{}
|
||||
```
|
||||
|
||||
## Implementation based on adjacency list
|
||||
## Implementation Based on Adjacency List
|
||||
|
||||
Given an undirected graph with a total of $n$ vertices and $m$ edges, the various operations can be implemented as shown in the figure below.
|
||||
|
||||
@@ -68,7 +68,7 @@ Additionally, we use the `Vertex` class to represent vertices in the adjacency l
|
||||
[file]{graph_adjacency_list}-[class]{graph_adj_list}-[func]{}
|
||||
```
|
||||
|
||||
## Efficiency comparison
|
||||
## Efficiency Comparison
|
||||
|
||||
Assuming the graph has $n$ vertices and $m$ edges, the table below compares the time efficiency and space efficiency of adjacency matrices and adjacency lists. Note that the adjacency list (linked list) corresponds to the implementation in this text, while the adjacency list (hash table) refers specifically to the implementation where all linked lists are replaced with hash tables.
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
# Graph traversal
|
||||
# Graph Traversal
|
||||
|
||||
Trees represent "one-to-many" relationships, while graphs have a higher degree of freedom and can represent any "many-to-many" relationships. Therefore, we can view trees as a special case of graphs. Clearly, **tree traversal operations are also a special case of graph traversal operations**.
|
||||
|
||||
Both graphs and trees require the application of search algorithms to implement traversal operations. Graph traversal methods can also be divided into two types: <u>breadth-first traversal</u> and <u>depth-first traversal</u>.
|
||||
|
||||
## Breadth-first search
|
||||
## Breadth-First Search
|
||||
|
||||
**Breadth-first search is a near-to-far traversal method that, starting from a certain node, always prioritizes visiting the nearest vertices and expands outward layer by layer**. As shown in the figure below, starting from the top-left vertex, first traverse all adjacent vertices of that vertex, then traverse all adjacent vertices of the next vertex, and so on, until all vertices have been visited.
|
||||
|
||||

|
||||
|
||||
### Algorithm implementation
|
||||
### Algorithm Implementation
|
||||
|
||||
BFS is typically implemented with the help of a queue, as shown in the code below. The queue has a "first in, first out" property, which aligns with the BFS idea of "near to far".
|
||||
|
||||
@@ -67,19 +67,19 @@ The code is relatively abstract; it is recommended to refer to the figure below
|
||||
|
||||
Not unique. Breadth-first search only requires traversing in a "near to far" order, **and the traversal order of vertices at the same distance can be arbitrarily shuffled**. Taking the figure above as an example, the visit order of vertices $1$ and $3$ can be swapped, as can the visit order of vertices $2$, $4$, and $6$.
|
||||
|
||||
### Complexity analysis
|
||||
### Complexity Analysis
|
||||
|
||||
**Time complexity**: All vertices will be enqueued and dequeued once, using $O(|V|)$ time; in the process of traversing adjacent vertices, since it is an undirected graph, all edges will be visited $2$ times, using $O(2|E|)$ time; overall using $O(|V| + |E|)$ time.
|
||||
|
||||
**Space complexity**: The list `res`, hash set `visited`, and queue `que` can contain at most $|V|$ vertices, using $O(|V|)$ space.
|
||||
|
||||
## Depth-first search
|
||||
## Depth-First Search
|
||||
|
||||
**Depth-first search is a traversal method that prioritizes going as far as possible, then backtracks when no path remains**. As shown in the figure below, starting from the top-left vertex, visit an adjacent vertex of the current vertex, continuing until reaching a dead end, then return and continue going as far as possible before returning again, and so on, until all vertices have been traversed.
|
||||
|
||||

|
||||
|
||||
### Algorithm implementation
|
||||
### Algorithm Implementation
|
||||
|
||||
This "go as far as possible then return" algorithm paradigm is typically implemented using recursion. Similar to breadth-first search, in depth-first search we also need a hash set `visited` to record visited vertices and avoid revisiting.
|
||||
|
||||
@@ -133,7 +133,7 @@ To deepen understanding, it is recommended to combine the figure below with the
|
||||
|
||||
Taking tree traversal as an example, "root $\rightarrow$ left $\rightarrow$ right", "left $\rightarrow$ root $\rightarrow$ right", and "left $\rightarrow$ right $\rightarrow$ root" correspond to pre-order, in-order, and post-order traversals, respectively. They represent three different traversal priorities, yet all three belong to depth-first search.
|
||||
|
||||
### Complexity analysis
|
||||
### Complexity Analysis
|
||||
|
||||
**Time complexity**: All vertices will be visited $1$ time, using $O(|V|)$ time; all edges will be visited $2$ times, using $O(2|E|)$ time; overall using $O(|V| + |E|)$ time.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Summary
|
||||
|
||||
### Key review
|
||||
### Key Review
|
||||
|
||||
- Graphs consist of vertices and edges and can be represented as a set of vertices and a set of edges.
|
||||
- Compared to linear relationships (linked lists) and divide-and-conquer relationships (trees), network relationships (graphs) have a higher degree of freedom and are therefore more complex.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Fractional knapsack problem
|
||||
# Fractional Knapsack Problem
|
||||
|
||||
!!! question
|
||||
|
||||
@@ -15,7 +15,7 @@ The difference is that this problem allows selecting only a portion of an item.
|
||||
|
||||

|
||||
|
||||
### Greedy strategy determination
|
||||
### Greedy Strategy Determination
|
||||
|
||||
Maximizing the total value of items in the knapsack **is essentially maximizing the value per unit weight of items**. From this, we can derive the greedy strategy shown in the figure below.
|
||||
|
||||
@@ -25,7 +25,7 @@ Maximizing the total value of items in the knapsack **is essentially maximizing
|
||||
|
||||

|
||||
|
||||
### Code implementation
|
||||
### Code Implementation
|
||||
|
||||
We created an `Item` class to facilitate sorting items by unit value. We loop to make greedy selections, breaking when the knapsack is full and returning the solution:
|
||||
|
||||
@@ -39,7 +39,7 @@ Apart from sorting, in the worst case the entire item list needs to be traversed
|
||||
|
||||
Since an `Item` object list is initialized, **the space complexity is $O(n)$**.
|
||||
|
||||
### Correctness proof
|
||||
### Correctness Proof
|
||||
|
||||
Using proof by contradiction. Suppose item $x$ has the highest unit value, and some algorithm yields a maximum value of `res`, but this solution does not include item $x$.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Greedy algorithm
|
||||
# Greedy Algorithm
|
||||
|
||||
<u>Greedy algorithm</u> is a common algorithm for solving optimization problems. Its basic idea is to make the seemingly best choice at each decision stage of the problem, that is, to greedily make locally optimal decisions in hopes of obtaining a globally optimal solution. Greedy algorithms are simple and efficient, and are widely applied in many practical problems.
|
||||
|
||||
@@ -25,7 +25,7 @@ The implementation code is as follows:
|
||||
|
||||
You might exclaim: So clean! The greedy algorithm solves the coin change problem in about ten lines of code.
|
||||
|
||||
## Advantages and limitations of greedy algorithms
|
||||
## Advantages and Limitations of Greedy Algorithms
|
||||
|
||||
**Greedy algorithms are not only straightforward and simple to implement, but are also usually very efficient**. In the code above, if the smallest coin denomination is $\min(coins)$, the greedy choice loops at most $amt / \min(coins)$ times, giving a time complexity of $O(amt / \min(coins))$. This is an order of magnitude smaller than the time complexity of the dynamic programming solution $O(n \times amt)$.
|
||||
|
||||
@@ -44,7 +44,7 @@ Generally, the applicability of greedy algorithms falls into the following two s
|
||||
1. **Can guarantee finding the optimal solution**: In this situation, greedy algorithms are often the best choice, because they tend to be more efficient than backtracking and dynamic programming.
|
||||
2. **Can find an approximate optimal solution**: Greedy algorithms are also applicable in this situation. For many complex problems, finding the global optimal solution is very difficult, and being able to find a suboptimal solution with high efficiency is also very good.
|
||||
|
||||
## Characteristics of greedy algorithms
|
||||
## Characteristics of Greedy Algorithms
|
||||
|
||||
So the question arises: what kind of problems are suitable for solving with greedy algorithms? Or in other words, under what conditions can greedy algorithms guarantee finding the optimal solution?
|
||||
|
||||
@@ -65,7 +65,7 @@ For example, in the coin change problem, although we can easily provide countere
|
||||
|
||||
Pearson, D. A polynomial-time algorithm for the change-making problem[J]. Operations Research Letters, 2005, 33(3): 231-234.
|
||||
|
||||
## Steps for solving problems with greedy algorithms
|
||||
## Steps for Solving Problems with Greedy Algorithms
|
||||
|
||||
The problem-solving process for greedy problems can generally be divided into the following three steps.
|
||||
|
||||
@@ -82,7 +82,7 @@ To ensure correctness, we should rigorously mathematically prove the greedy stra
|
||||
|
||||
However, correctness proofs may also not be easy. If we have no clue, we usually choose to debug the code based on test cases, step by step modifying and verifying the greedy strategy.
|
||||
|
||||
## Typical problems solved by greedy algorithms
|
||||
## Typical Problems Solved by Greedy Algorithms
|
||||
|
||||
Greedy algorithms are often applied to optimization problems that satisfy greedy choice property and optimal substructure. Below are some typical greedy algorithm problems.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Max capacity problem
|
||||
# Max Capacity Problem
|
||||
|
||||
!!! question
|
||||
|
||||
@@ -20,7 +20,7 @@ $$
|
||||
|
||||
Let the array length be $n$, then the number of combinations of two partitions (total number of states) is $C_n^2 = \frac{n(n - 1)}{2}$. Most directly, **we can exhaustively enumerate all states** to find the maximum capacity, with time complexity $O(n^2)$.
|
||||
|
||||
### Greedy strategy determination
|
||||
### Greedy Strategy Determination
|
||||
|
||||
This problem has a more efficient solution. As shown in the figure below, select a state $[i, j]$ where index $i < j$ and height $ht[i] < ht[j]$, meaning $i$ is the short partition and $j$ is the long partition.
|
||||
|
||||
@@ -72,7 +72,7 @@ The figure below shows the execution process of the greedy strategy.
|
||||
=== "<9>"
|
||||

|
||||
|
||||
### Code implementation
|
||||
### Code Implementation
|
||||
|
||||
The code loops at most $n$ rounds, **therefore the time complexity is $O(n)$**.
|
||||
|
||||
@@ -82,7 +82,7 @@ Variables $i$, $j$, and $res$ use a constant amount of extra space, **therefore
|
||||
[file]{max_capacity}-[class]{}-[func]{max_capacity}
|
||||
```
|
||||
|
||||
### Correctness proof
|
||||
### Correctness Proof
|
||||
|
||||
The reason greedy is faster than exhaustive enumeration is that each round of greedy selection "skips" some states.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Max product cutting problem
|
||||
# Max Product Cutting Problem
|
||||
|
||||
!!! question
|
||||
|
||||
@@ -20,7 +20,7 @@ $$
|
||||
|
||||
We need to think about: how large should the splitting count $m$ be, and what should each $n_i$ be?
|
||||
|
||||
### Greedy strategy determination
|
||||
### Greedy Strategy Determination
|
||||
|
||||
Based on experience, the product of two integers is often greater than their sum. Suppose we split out a factor of $2$ from $n$, then their product is $2(n-2)$. We compare this product with $n$:
|
||||
|
||||
@@ -53,7 +53,7 @@ In summary, the following greedy strategies can be derived.
|
||||
3. When the remainder is $2$, do not continue splitting, keep it.
|
||||
4. When the remainder is $1$, since $2 \times 2 > 1 \times 3$, the last $3$ should be replaced with $2$.
|
||||
|
||||
### Code implementation
|
||||
### Code Implementation
|
||||
|
||||
As shown in the figure below, we don't need to use loops to split the integer, but can use integer division to get the count of $3$s as $a$, and modulo operation to get the remainder as $b$, at which point we have:
|
||||
|
||||
@@ -76,7 +76,7 @@ Please note that for the edge case of $n \leq 3$, a $1$ must be split out, with
|
||||
|
||||
Variables $a$ and $b$ use a constant amount of extra space, **therefore the space complexity is $O(1)$**.
|
||||
|
||||
### Correctness proof
|
||||
### Correctness Proof
|
||||
|
||||
Using proof by contradiction, only analyzing the case where $n \geq 4$.
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Summary
|
||||
|
||||
### Key Review
|
||||
|
||||
- Greedy algorithms are typically used to solve optimization problems. The principle is to make locally optimal decisions at each decision stage in hopes of obtaining a globally optimal solution.
|
||||
- Greedy algorithms iteratively make one greedy choice after another, transforming the problem into a smaller subproblem in each round, until the problem is solved.
|
||||
- Greedy algorithms are not only simple to implement, but also have high problem-solving efficiency. Compared to dynamic programming, greedy algorithms typically have lower time complexity.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Hash algorithm
|
||||
# Hash Algorithm
|
||||
|
||||
The previous two sections introduced the working principle of hash tables and the methods to handle hash collisions. However, both open addressing and separate chaining **can only ensure that the hash table functions normally when hash collisions occur, but cannot reduce the frequency of hash collisions**.
|
||||
|
||||
@@ -16,7 +16,7 @@ Observing the above formula, when the hash table capacity `capacity` is fixed, *
|
||||
|
||||
This means that, to reduce the probability of hash collisions, we should focus on the design of the hash algorithm `hash()`.
|
||||
|
||||
## Goals of hash algorithms
|
||||
## Goals of Hash Algorithms
|
||||
|
||||
To achieve a "fast and stable" hash table data structure, hash algorithms should have the following characteristics:
|
||||
|
||||
@@ -37,7 +37,7 @@ For cryptographic applications, to prevent reverse engineering such as deducing
|
||||
|
||||
Note that **"uniform distribution" and "collision resistance" are two independent concepts**. Satisfying uniform distribution does not necessarily mean collision resistance. For example, under random input `key`, the hash function `key % 100` can produce a uniformly distributed output. However, this hash algorithm is too simple, and all `key` with the same last two digits will have the same output, making it easy to deduce a usable `key` from the hash value, thereby cracking the password.
|
||||
|
||||
## Design of hash algorithms
|
||||
## Design of Hash Algorithms
|
||||
|
||||
The design of hash algorithms is a complex issue that requires consideration of many factors. However, for some less demanding scenarios, we can also design some simple hash algorithms.
|
||||
|
||||
@@ -78,7 +78,7 @@ It is worth noting that if the `key` is guaranteed to be randomly and uniformly
|
||||
|
||||
In summary, we usually choose a prime number as the modulus, and this prime number should be large enough to eliminate periodic patterns as much as possible, enhancing the robustness of the hash algorithm.
|
||||
|
||||
## Common hash algorithms
|
||||
## Common Hash Algorithms
|
||||
|
||||
It is not hard to see that the simple hash algorithms mentioned above are quite "fragile" and far from reaching the design goals of hash algorithms. For example, since addition and XOR obey the commutative law, additive hash and XOR hash cannot distinguish strings with the same content but in different order, which may exacerbate hash collisions and cause security issues.
|
||||
|
||||
@@ -100,7 +100,7 @@ Over the past century, hash algorithms have been in a continuous process of upgr
|
||||
| Security Level | Low, has been successfully attacked | Low, has been successfully attacked | High | High |
|
||||
| Applications | Abandoned, still used for data integrity checks | Abandoned | Cryptocurrency transaction verification, digital signatures, etc. | Can be used to replace SHA-2 |
|
||||
|
||||
# Hash values in data structures
|
||||
# Hash Values in Data Structures
|
||||
|
||||
We know that the keys in a hash table can be of various data types such as integers, decimals, or strings. Programming languages usually provide built-in hash algorithms for these data types to calculate the bucket indices in the hash table. Taking Python as an example, we can use the `hash()` function to compute the hash values for various data types.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Hash collision
|
||||
# Hash Collision
|
||||
|
||||
The previous section mentioned that, **in most cases, the input space of a hash function is much larger than the output space**, so theoretically, hash collisions are inevitable. For example, if the input space is all integers and the output space is the array capacity size, then multiple integers will inevitably be mapped to the same bucket index.
|
||||
|
||||
@@ -9,7 +9,7 @@ Hash collisions can lead to incorrect query results, severely impacting the usab
|
||||
|
||||
The main methods for improving the structure of hash tables include "separate chaining" and "open addressing".
|
||||
|
||||
## Separate chaining
|
||||
## Separate Chaining
|
||||
|
||||
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. The figure below shows an example of a separate chaining hash table.
|
||||
|
||||
@@ -37,13 +37,13 @@ The code below provides a simple implementation of a separate chaining hash tabl
|
||||
|
||||
It's worth noting that when the linked list is very long, the query efficiency $O(n)$ is poor. **In this case, the list can be converted to an "AVL tree" or "Red-Black tree"** to optimize the time complexity of the query operation to $O(\log n)$.
|
||||
|
||||
## Open addressing
|
||||
## Open Addressing
|
||||
|
||||
<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.
|
||||
|
||||
Let's use linear probing as an example to introduce the mechanism of open addressing hash tables.
|
||||
|
||||
### Linear probing
|
||||
### Linear Probing
|
||||
|
||||
Linear probing uses a fixed-step linear search for probing, and its operation method differs from ordinary hash tables.
|
||||
|
||||
@@ -72,7 +72,7 @@ The code below implements an open addressing (linear probing) hash table with la
|
||||
[file]{hash_map_open_addressing}-[class]{hash_map_open_addressing}-[func]{}
|
||||
```
|
||||
|
||||
### Quadratic probing
|
||||
### Quadratic Probing
|
||||
|
||||
Quadratic probing is similar to linear probing and is one of the common strategies for open addressing. When a collision occurs, quadratic probing does not simply skip a fixed number of steps but skips a number of steps equal to the "square of the number of probes", i.e., $1, 4, 9, \dots$ steps.
|
||||
|
||||
@@ -86,7 +86,7 @@ However, quadratic probing is not perfect:
|
||||
- Clustering still exists, i.e., some positions are more likely to be occupied than others.
|
||||
- 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.
|
||||
|
||||
### Double hashing
|
||||
### Double Hashing
|
||||
|
||||
As the name suggests, the double hashing method uses multiple hash functions $f_1(x)$, $f_2(x)$, $f_3(x)$, $\dots$ for probing.
|
||||
|
||||
@@ -99,7 +99,7 @@ Compared to linear probing, the double hashing method is less prone to clusterin
|
||||
|
||||
Please note that open addressing (linear probing, quadratic probing, and double hashing) hash tables all have the problem of "cannot directly delete elements".
|
||||
|
||||
## Choice of programming languages
|
||||
## Choice of Programming Languages
|
||||
|
||||
Different programming languages adopt different hash table implementation strategies. Here are a few examples:
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Hash table
|
||||
# Hash Table
|
||||
|
||||
A <u>hash table</u>, also known as a <u>hash map</u>, establishes a mapping between keys `key` and values `value`, enabling efficient element retrieval. Specifically, when we input a key `key` into a hash table, we can retrieve the corresponding value `value` in $O(1)$ time.
|
||||
|
||||
@@ -22,7 +22,7 @@ In addition to hash tables, arrays and linked lists can also implement query fun
|
||||
|
||||
As observed, **the time complexity for insertion, deletion, search, and modification operations in a hash table is $O(1)$**, which is very efficient.
|
||||
|
||||
## Common hash table operations
|
||||
## Common Hash Table Operations
|
||||
|
||||
Common operations on hash tables include: initialization, query operations, adding key-value pairs, and deleting key-value pairs. Example code is as follows:
|
||||
|
||||
@@ -34,11 +34,11 @@ Common operations on hash tables include: initialization, query operations, addi
|
||||
|
||||
# Add operation
|
||||
# Add key-value pair (key, value) to hash table
|
||||
hmap[12836] = "小哈"
|
||||
hmap[15937] = "小啰"
|
||||
hmap[16750] = "小算"
|
||||
hmap[13276] = "小法"
|
||||
hmap[10583] = "小鸭"
|
||||
hmap[12836] = "XiaoHa"
|
||||
hmap[15937] = "XiaoLuo"
|
||||
hmap[16750] = "XiaoSuan"
|
||||
hmap[13276] = "XiaoFa"
|
||||
hmap[10583] = "XiaoYa"
|
||||
|
||||
# Query operation
|
||||
# Input key into hash table to get value
|
||||
@@ -57,11 +57,11 @@ Common operations on hash tables include: initialization, query operations, addi
|
||||
|
||||
/* Add operation */
|
||||
// Add key-value pair (key, value) to hash table
|
||||
map[12836] = "小哈";
|
||||
map[15937] = "小啰";
|
||||
map[16750] = "小算";
|
||||
map[13276] = "小法";
|
||||
map[10583] = "小鸭";
|
||||
map[12836] = "XiaoHa";
|
||||
map[15937] = "XiaoLuo";
|
||||
map[16750] = "XiaoSuan";
|
||||
map[13276] = "XiaoFa";
|
||||
map[10583] = "XiaoYa";
|
||||
|
||||
/* Query operation */
|
||||
// Input key into hash table to get value
|
||||
@@ -80,11 +80,11 @@ Common operations on hash tables include: initialization, query operations, addi
|
||||
|
||||
/* Add operation */
|
||||
// Add key-value pair (key, value) to hash table
|
||||
map.put(12836, "小哈");
|
||||
map.put(15937, "小啰");
|
||||
map.put(16750, "小算");
|
||||
map.put(13276, "小法");
|
||||
map.put(10583, "小鸭");
|
||||
map.put(12836, "XiaoHa");
|
||||
map.put(15937, "XiaoLuo");
|
||||
map.put(16750, "XiaoSuan");
|
||||
map.put(13276, "XiaoFa");
|
||||
map.put(10583, "XiaoYa");
|
||||
|
||||
/* Query operation */
|
||||
// Input key into hash table to get value
|
||||
@@ -102,11 +102,11 @@ Common operations on hash tables include: initialization, query operations, addi
|
||||
Dictionary<int, string> map = new() {
|
||||
/* Add operation */
|
||||
// Add key-value pair (key, value) to hash table
|
||||
{ 12836, "小哈" },
|
||||
{ 15937, "小啰" },
|
||||
{ 16750, "小算" },
|
||||
{ 13276, "小法" },
|
||||
{ 10583, "小鸭" }
|
||||
{ 12836, "XiaoHa" },
|
||||
{ 15937, "XiaoLuo" },
|
||||
{ 16750, "XiaoSuan" },
|
||||
{ 13276, "XiaoFa" },
|
||||
{ 10583, "XiaoYa" }
|
||||
};
|
||||
|
||||
/* Query operation */
|
||||
@@ -126,11 +126,11 @@ Common operations on hash tables include: initialization, query operations, addi
|
||||
|
||||
/* Add operation */
|
||||
// Add key-value pair (key, value) to hash table
|
||||
hmap[12836] = "小哈"
|
||||
hmap[15937] = "小啰"
|
||||
hmap[16750] = "小算"
|
||||
hmap[13276] = "小法"
|
||||
hmap[10583] = "小鸭"
|
||||
hmap[12836] = "XiaoHa"
|
||||
hmap[15937] = "XiaoLuo"
|
||||
hmap[16750] = "XiaoSuan"
|
||||
hmap[13276] = "XiaoFa"
|
||||
hmap[10583] = "XiaoYa"
|
||||
|
||||
/* Query operation */
|
||||
// Input key into hash table to get value
|
||||
@@ -149,11 +149,11 @@ Common operations on hash tables include: initialization, query operations, addi
|
||||
|
||||
/* Add operation */
|
||||
// Add key-value pair (key, value) to hash table
|
||||
map[12836] = "小哈"
|
||||
map[15937] = "小啰"
|
||||
map[16750] = "小算"
|
||||
map[13276] = "小法"
|
||||
map[10583] = "小鸭"
|
||||
map[12836] = "XiaoHa"
|
||||
map[15937] = "XiaoLuo"
|
||||
map[16750] = "XiaoSuan"
|
||||
map[13276] = "XiaoFa"
|
||||
map[10583] = "XiaoYa"
|
||||
|
||||
/* Query operation */
|
||||
// Input key into hash table to get value
|
||||
@@ -171,11 +171,11 @@ Common operations on hash tables include: initialization, query operations, addi
|
||||
const map = new Map();
|
||||
/* Add operation */
|
||||
// Add key-value pair (key, value) to hash table
|
||||
map.set(12836, '小哈');
|
||||
map.set(15937, '小啰');
|
||||
map.set(16750, '小算');
|
||||
map.set(13276, '小法');
|
||||
map.set(10583, '小鸭');
|
||||
map.set(12836, 'XiaoHa');
|
||||
map.set(15937, 'XiaoLuo');
|
||||
map.set(16750, 'XiaoSuan');
|
||||
map.set(13276, 'XiaoFa');
|
||||
map.set(10583, 'XiaoYa');
|
||||
|
||||
/* Query operation */
|
||||
// Input key into hash table to get value
|
||||
@@ -193,11 +193,11 @@ Common operations on hash tables include: initialization, query operations, addi
|
||||
const map = new Map<number, string>();
|
||||
/* Add operation */
|
||||
// Add key-value pair (key, value) to hash table
|
||||
map.set(12836, '小哈');
|
||||
map.set(15937, '小啰');
|
||||
map.set(16750, '小算');
|
||||
map.set(13276, '小法');
|
||||
map.set(10583, '小鸭');
|
||||
map.set(12836, 'XiaoHa');
|
||||
map.set(15937, 'XiaoLuo');
|
||||
map.set(16750, 'XiaoSuan');
|
||||
map.set(13276, 'XiaoFa');
|
||||
map.set(10583, 'XiaoYa');
|
||||
console.info('\nAfter adding, hash table is\nKey -> Value');
|
||||
console.info(map);
|
||||
|
||||
@@ -221,11 +221,11 @@ Common operations on hash tables include: initialization, query operations, addi
|
||||
|
||||
/* Add operation */
|
||||
// Add key-value pair (key, value) to hash table
|
||||
map[12836] = "小哈";
|
||||
map[15937] = "小啰";
|
||||
map[16750] = "小算";
|
||||
map[13276] = "小法";
|
||||
map[10583] = "小鸭";
|
||||
map[12836] = "XiaoHa";
|
||||
map[15937] = "XiaoLuo";
|
||||
map[16750] = "XiaoSuan";
|
||||
map[13276] = "XiaoFa";
|
||||
map[10583] = "XiaoYa";
|
||||
|
||||
/* Query operation */
|
||||
// Input key into hash table to get value
|
||||
@@ -246,11 +246,11 @@ Common operations on hash tables include: initialization, query operations, addi
|
||||
|
||||
/* Add operation */
|
||||
// Add key-value pair (key, value) to hash table
|
||||
map.insert(12836, "小哈".to_string());
|
||||
map.insert(15937, "小啰".to_string());
|
||||
map.insert(16750, "小算".to_string());
|
||||
map.insert(13279, "小法".to_string());
|
||||
map.insert(10583, "小鸭".to_string());
|
||||
map.insert(12836, "XiaoHa".to_string());
|
||||
map.insert(15937, "XiaoLuo".to_string());
|
||||
map.insert(16750, "XiaoSuan".to_string());
|
||||
map.insert(13279, "XiaoFa".to_string());
|
||||
map.insert(10583, "XiaoYa".to_string());
|
||||
|
||||
/* Query operation */
|
||||
// Input key into hash table to get value
|
||||
@@ -275,11 +275,11 @@ Common operations on hash tables include: initialization, query operations, addi
|
||||
|
||||
/* Add operation */
|
||||
// Add key-value pair (key, value) to hash table
|
||||
map[12836] = "小哈"
|
||||
map[15937] = "小啰"
|
||||
map[16750] = "小算"
|
||||
map[13276] = "小法"
|
||||
map[10583] = "小鸭"
|
||||
map[12836] = "XiaoHa"
|
||||
map[15937] = "XiaoLuo"
|
||||
map[16750] = "XiaoSuan"
|
||||
map[13276] = "XiaoFa"
|
||||
map[10583] = "XiaoYa"
|
||||
|
||||
/* Query operation */
|
||||
// Input key into hash table to get value
|
||||
@@ -298,11 +298,11 @@ Common operations on hash tables include: initialization, query operations, addi
|
||||
|
||||
# Add operation
|
||||
# Add key-value pair (key, value) to hash table
|
||||
hmap[12836] = "小哈"
|
||||
hmap[15937] = "小啰"
|
||||
hmap[16750] = "小算"
|
||||
hmap[13276] = "小法"
|
||||
hmap[10583] = "小鸭"
|
||||
hmap[12836] = "XiaoHa"
|
||||
hmap[15937] = "XiaoLuo"
|
||||
hmap[16750] = "XiaoSuan"
|
||||
hmap[13276] = "XiaoFa"
|
||||
hmap[10583] = "XiaoYa"
|
||||
|
||||
# Query operation
|
||||
# Input key into hash table to get value
|
||||
@@ -550,7 +550,7 @@ There are three common ways to traverse a hash table: traversing key-value pairs
|
||||
|
||||
https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B7%BB%E5%8A%A0%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E6%B7%BB%E5%8A%A0%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%E5%B0%8F%E5%93%88%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%E5%B0%8F%E5%95%B0%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%E5%B0%8F%E7%AE%97%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%E5%B0%8F%E6%B3%95%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%E5%B0%8F%E9%B8%AD%22%0A%20%20%20%20%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E9%94%AE%E5%80%BC%E5%AF%B9%20key-%3Evalue%0A%20%20%20%20for%20key,%20value%20in%20hmap.items%28%29%3A%0A%20%20%20%20%20%20%20%20print%28key,%20%22-%3E%22,%20value%29%0A%20%20%20%20%23%20%E5%8D%95%E7%8B%AC%E9%81%8D%E5%8E%86%E9%94%AE%20key%0A%20%20%20%20for%20key%20in%20hmap.keys%28%29%3A%0A%20%20%20%20%20%20%20%20print%28key%29%0A%20%20%20%20%23%20%E5%8D%95%E7%8B%AC%E9%81%8D%E5%8E%86%E5%80%BC%20value%0A%20%20%20%20for%20value%20in%20hmap.values%28%29%3A%0A%20%20%20%20%20%20%20%20print%28value%29&cumulative=false&curInstr=8&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
||||
|
||||
## Simple hash table implementation
|
||||
## Simple Hash Table Implementation
|
||||
|
||||
Let's first consider the simplest case: **implementing a hash table using only an array**. In a hash table, each empty position in the array is called a <u>bucket</u>, and each bucket can store a key-value pair. Therefore, the query operation is to find the bucket corresponding to `key` and retrieve the `value` from the bucket.
|
||||
|
||||
@@ -577,7 +577,7 @@ The following code implements a simple hash table. Here, we encapsulate `key` an
|
||||
[file]{array_hash_map}-[class]{array_hash_map}-[func]{}
|
||||
```
|
||||
|
||||
## Hash collision and resizing
|
||||
## Hash Collision and Resizing
|
||||
|
||||
Fundamentally, the role of a hash function is to map the input space consisting of all `key`s to the output space consisting of all array indices, and the input space is often much larger than the output space. Therefore, **theoretically there must be cases where "multiple inputs correspond to the same output"**.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Summary
|
||||
|
||||
### Key review
|
||||
### Key Review
|
||||
|
||||
- Given an input `key`, a hash table can retrieve the corresponding `value` in $O(1)$ time, which is highly efficient.
|
||||
- Common hash table operations include querying, adding key-value pairs, deleting key-value pairs, and traversing the hash table.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Heap construction operation
|
||||
# Heap Construction Operation
|
||||
|
||||
In some cases, we want to build a heap using all elements of a list, and this process is called "heap construction operation."
|
||||
|
||||
## Implementing with element insertion
|
||||
## Implementing with Element Insertion
|
||||
|
||||
We first create an empty heap, then iterate through the list, performing the "element insertion operation" on each element in sequence. This means adding the element to the bottom of the heap and then performing "bottom-to-top" heapify on that element.
|
||||
|
||||
@@ -10,7 +10,7 @@ Each time an element is inserted into the heap, the heap's length increases by o
|
||||
|
||||
Given $n$ elements, each element's insertion operation takes $O(\log{n})$ time, so the time complexity of this heap construction method is $O(n \log n)$.
|
||||
|
||||
## Implementing through heapify traversal
|
||||
## Implementing Through Heapify Traversal
|
||||
|
||||
In fact, we can implement a more efficient heap construction method in two steps.
|
||||
|
||||
@@ -27,7 +27,7 @@ It's worth noting that **since leaf nodes have no children, they are naturally v
|
||||
[file]{my_heap}-[class]{max_heap}-[func]{__init__}
|
||||
```
|
||||
|
||||
## Complexity analysis
|
||||
## Complexity Analysis
|
||||
|
||||
Next, let's attempt to derive the time complexity of this second heap construction method.
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ As a special case of a complete binary tree, heaps have the following characteri
|
||||
- We call the root node of the binary tree the "heap top" and the bottom-rightmost node the "heap bottom."
|
||||
- For max heaps (min heaps), the value of the heap top element (root node) is the largest (smallest).
|
||||
|
||||
## Common heap operations
|
||||
## Common Heap Operations
|
||||
|
||||
It should be noted that many programming languages provide a <u>priority queue</u>, which is an abstract data structure defined as a queue with priority sorting.
|
||||
|
||||
@@ -418,11 +418,11 @@ Similar to "ascending order" and "descending order" in sorting algorithms, we ca
|
||||
|
||||
https://pythontutor.com/render.html#code=import%20heapq%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%B0%8F%E9%A1%B6%E5%A0%86%0A%20%20%20%20min_heap,%20flag%20%3D%20%5B%5D,%201%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%A4%A7%E9%A1%B6%E5%A0%86%0A%20%20%20%20max_heap,%20flag%20%3D%20%5B%5D,%20-1%0A%20%20%20%20%0A%20%20%20%20%23%20Python%20%E7%9A%84%20heapq%20%E6%A8%A1%E5%9D%97%E9%BB%98%E8%AE%A4%E5%AE%9E%E7%8E%B0%E5%B0%8F%E9%A1%B6%E5%A0%86%0A%20%20%20%20%23%20%E8%80%83%E8%99%91%E5%B0%86%E2%80%9C%E5%85%83%E7%B4%A0%E5%8F%96%E8%B4%9F%E2%80%9D%E5%90%8E%E5%86%8D%E5%85%A5%E5%A0%86%EF%BC%8C%E8%BF%99%E6%A0%B7%E5%B0%B1%E5%8F%AF%E4%BB%A5%E5%B0%86%E5%A4%A7%E5%B0%8F%E5%85%B3%E7%B3%BB%E9%A2%A0%E5%80%92%EF%BC%8C%E4%BB%8E%E8%80%8C%E5%AE%9E%E7%8E%B0%E5%A4%A7%E9%A1%B6%E5%A0%86%0A%20%20%20%20%23%20%E5%9C%A8%E6%9C%AC%E7%A4%BA%E4%BE%8B%E4%B8%AD%EF%BC%8Cflag%20%3D%201%20%E6%97%B6%E5%AF%B9%E5%BA%94%E5%B0%8F%E9%A1%B6%E5%A0%86%EF%BC%8Cflag%20%3D%20-1%20%E6%97%B6%E5%AF%B9%E5%BA%94%E5%A4%A7%E9%A1%B6%E5%A0%86%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E5%A0%86%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%201%29%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%203%29%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%202%29%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%205%29%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%204%29%0A%20%20%20%20%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E5%A0%86%E9%A1%B6%E5%85%83%E7%B4%A0%0A%20%20%20%20peek%20%3D%20flag%20*%20max_heap%5B0%5D%20%23%205%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%A0%86%E9%A1%B6%E5%85%83%E7%B4%A0%E5%87%BA%E5%A0%86%0A%20%20%20%20%23%20%E5%87%BA%E5%A0%86%E5%85%83%E7%B4%A0%E4%BC%9A%E5%BD%A2%E6%88%90%E4%B8%80%E4%B8%AA%E4%BB%8E%E5%A4%A7%E5%88%B0%E5%B0%8F%E7%9A%84%E5%BA%8F%E5%88%97%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%205%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%204%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%203%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%202%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%201%0A%20%20%20%20%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E5%A0%86%E5%A4%A7%E5%B0%8F%0A%20%20%20%20size%20%3D%20len%28max_heap%29%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E5%A0%86%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20not%20max_heap%0A%20%20%20%20%0A%20%20%20%20%23%20%E8%BE%93%E5%85%A5%E5%88%97%E8%A1%A8%E5%B9%B6%E5%BB%BA%E5%A0%86%0A%20%20%20%20min_heap%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20heapq.heapify%28min_heap%29&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
||||
|
||||
## Implementation of the heap
|
||||
## Implementation of the Heap
|
||||
|
||||
The following implementation is of a max heap. To convert it to a min heap, simply invert all size logic comparisons (for example, replace $\geq$ with $\leq$). Interested readers are encouraged to implement this on their own.
|
||||
|
||||
### Heap storage and representation
|
||||
### Heap Storage and Representation
|
||||
|
||||
As mentioned in the "Binary Tree" chapter, complete binary trees are well-suited for array representation. Since heaps are a type of complete binary tree, **we will use arrays to store heaps**.
|
||||
|
||||
@@ -438,7 +438,7 @@ We can encapsulate the index mapping formula into functions for convenient subse
|
||||
[file]{my_heap}-[class]{max_heap}-[func]{parent}
|
||||
```
|
||||
|
||||
### Accessing the heap top element
|
||||
### Accessing the Heap Top Element
|
||||
|
||||
The heap top element is the root node of the binary tree, which is also the first element of the list:
|
||||
|
||||
@@ -446,7 +446,7 @@ The heap top element is the root node of the binary tree, which is also the firs
|
||||
[file]{my_heap}-[class]{max_heap}-[func]{peek}
|
||||
```
|
||||
|
||||
### Inserting an element into the heap
|
||||
### Inserting an Element Into the Heap
|
||||
|
||||
Given an element `val`, we first add it to the bottom of the heap. After addition, since `val` may be larger than other elements in the heap, the heap's property may be violated. **Therefore, it's necessary to repair the path from the inserted node to the root node**. This operation is called <u>heapify</u>.
|
||||
|
||||
@@ -485,7 +485,7 @@ Given a total of $n$ nodes, the tree height is $O(\log n)$. Thus, the number of
|
||||
[file]{my_heap}-[class]{max_heap}-[func]{sift_up}
|
||||
```
|
||||
|
||||
### Removing the heap top element
|
||||
### Removing the Heap Top Element
|
||||
|
||||
The heap top element is the root node of the binary tree, which is the first element of the list. If we directly remove the first element from the list, all node indexes in the binary tree would change, making subsequent repair with heapify difficult. To minimize changes in element indexes, we use the following steps.
|
||||
|
||||
@@ -531,7 +531,7 @@ Similar to the element insertion operation, the time complexity of the heap top
|
||||
[file]{my_heap}-[class]{max_heap}-[func]{sift_down}
|
||||
```
|
||||
|
||||
## Common applications of heaps
|
||||
## Common Applications of Heaps
|
||||
|
||||
- **Priority queue**: Heaps are typically the preferred data structure for implementing priority queues, with both enqueue and dequeue operations having a time complexity of $O(\log n)$, and the heap construction operation having $O(n)$, all of which are highly efficient.
|
||||
- **Heap sort**: Given a set of data, we can build a heap with them and then continuously perform element removal operations to obtain sorted data. However, we usually use a more elegant approach to implement heap sort, as detailed in the "Heap Sort" chapter.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Summary
|
||||
|
||||
### Key review
|
||||
### Key Review
|
||||
|
||||
- A heap is a complete binary tree that can be categorized as a max heap or min heap based on its property. The heap top element of a max heap (min heap) is the largest (smallest).
|
||||
- A priority queue is defined as a queue with priority sorting, typically implemented using heaps.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Top-k problem
|
||||
# Top-K Problem
|
||||
|
||||
!!! question
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
For this problem, we'll first introduce two solutions with relatively straightforward approaches, then introduce a more efficient heap-based solution.
|
||||
|
||||
## Method 1: Iterative selection
|
||||
## Method 1: Iterative Selection
|
||||
|
||||
We can perform $k$ rounds of traversal as shown in the figure below, extracting the $1^{st}$, $2^{nd}$, $\dots$, $k^{th}$ largest elements in each round, with a time complexity of $O(nk)$.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Algorithms are everywhere
|
||||
# Algorithms Are Everywhere
|
||||
|
||||
When we hear the term "algorithm," we naturally think of mathematics. However, many algorithms do not involve complex mathematics but rely more on basic logic, which can be seen everywhere in our daily lives.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Introduction to algorithms
|
||||
# Encounter with Algorithms
|
||||
|
||||

|
||||

|
||||
|
||||
!!! abstract
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Summary
|
||||
|
||||
### Key Review
|
||||
|
||||
- Algorithms are ubiquitous in daily life and are not distant, esoteric knowledge. In fact, we have already learned many algorithms unconsciously and use them to solve problems big and small in life.
|
||||
- The principle of looking up a dictionary is consistent with the binary search algorithm. Binary search embodies the important algorithmic idea of divide and conquer.
|
||||
- The process of organizing playing cards is very similar to the insertion sort algorithm. Insertion sort is suitable for sorting small datasets.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# What is an algorithm
|
||||
# What Is an Algorithm
|
||||
|
||||
## Algorithm definition
|
||||
## Algorithm Definition
|
||||
|
||||
An <u>algorithm</u> is a set of instructions or operational steps that solves a specific problem within a finite amount of time. It has the following characteristics.
|
||||
|
||||
@@ -8,7 +8,7 @@ An <u>algorithm</u> is a set of instructions or operational steps that solves a
|
||||
- It is feasible and can be completed within a finite number of steps, time, and memory space.
|
||||
- Each step has a definite meaning, and under the same input and operating conditions, the output is always the same.
|
||||
|
||||
## Data structure definition
|
||||
## Data Structure Definition
|
||||
|
||||
A <u>data structure</u> is a way of organizing and storing data, covering the data content, relationships between data, and methods for data operations. It has the following design objectives.
|
||||
|
||||
@@ -21,7 +21,7 @@ A <u>data structure</u> is a way of organizing and storing data, covering the da
|
||||
- Compared to arrays, linked lists are more convenient for data addition and deletion operations but sacrifice data access speed.
|
||||
- Compared to linked lists, graphs provide richer logical information but require larger memory space.
|
||||
|
||||
## The relationship between data structures and algorithms
|
||||
## The Relationship Between Data Structures and Algorithms
|
||||
|
||||
As shown in the figure below, data structures and algorithms are highly related and tightly coupled, specifically manifested in the following three aspects.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# About this book
|
||||
# About This Book
|
||||
|
||||
This project aims to create an open-source, free, beginner-friendly introductory tutorial on data structures and algorithms.
|
||||
|
||||
@@ -6,7 +6,7 @@ This project aims to create an open-source, free, beginner-friendly introductory
|
||||
- The source code can be run with one click, helping readers improve their programming skills through practice and understand how algorithms work and the underlying implementation of data structures.
|
||||
- We encourage readers to learn from each other, and everyone is welcome to ask questions and share insights in the comments section, making progress together through discussion and exchange.
|
||||
|
||||
## Target audience
|
||||
## Target Audience
|
||||
|
||||
If you are an algorithm beginner who has never been exposed to algorithms, or if you already have some problem-solving experience and have a vague understanding of data structures and algorithms, oscillating between knowing and not knowing, then this book is tailor-made for you!
|
||||
|
||||
@@ -18,7 +18,7 @@ If you are an algorithm "expert," we look forward to receiving your valuable sug
|
||||
|
||||
You need to have at least a programming foundation in any language, and be able to read and write simple code.
|
||||
|
||||
## Content structure
|
||||
## Content Structure
|
||||
|
||||
The main content of this book is shown in the figure below.
|
||||
|
||||
@@ -47,6 +47,6 @@ During the creation of this book, I received help from many people.
|
||||
|
||||
During the writing process, I read many textbooks and articles on data structures and algorithms. These works provided excellent examples for this book and ensured the accuracy and quality of the book's content. I would like to thank all the teachers and predecessors for their outstanding contributions!
|
||||
|
||||
This book advocates a learning method that combines hands and brain, and in this regard I was deeply inspired by [*Dive into Deep Learning*](https://github.com/d2l-ai/d2l-zh). I highly recommend this excellent work to all readers.
|
||||
This book advocates a learning method that combines hands and brain, and in this regard I was deeply inspired by [Dive into Deep Learning](https://github.com/d2l-ai/d2l-zh). I highly recommend this excellent work to all readers.
|
||||
|
||||
**Heartfelt thanks to my parents, it is your support and encouragement that has given me the opportunity to do this interesting thing**.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# How to use this book
|
||||
# How to Use This Book
|
||||
|
||||
!!! tip
|
||||
|
||||
For the best reading experience, it is recommended that you read through this section.
|
||||
|
||||
## Writing style conventions
|
||||
## Writing Style Conventions
|
||||
|
||||
- Titles marked with `*` are optional sections with relatively difficult content. If you have limited time, you can skip them first.
|
||||
- Technical terms will be in bold (in paper and PDF versions) or underlined (in web versions), such as <u>array</u>. It is recommended to memorize them for reading literature.
|
||||
@@ -191,7 +191,7 @@
|
||||
// comment
|
||||
```
|
||||
|
||||
## Learning efficiently with animated illustrations
|
||||
## Learning Efficiently with Animated Illustrations
|
||||
|
||||
Compared to text, videos and images have higher information density and structural organization, making them easier to understand. In this book, **key and difficult knowledge will mainly be presented in the form of animated illustrations**, with text serving as explanation and supplement.
|
||||
|
||||
@@ -199,7 +199,7 @@ If you find that a section of content provides animated illustrations as shown i
|
||||
|
||||

|
||||
|
||||
## Deepening understanding through code practice
|
||||
## Deepening Understanding Through Code Practice
|
||||
|
||||
The accompanying code for this book is hosted in the [GitHub repository](https://github.com/krahets/hello-algo). As shown in the figure below, **the source code comes with test cases and can be run with one click**.
|
||||
|
||||
@@ -231,7 +231,7 @@ In addition to running code locally, **the web version also supports visual runn
|
||||
|
||||

|
||||
|
||||
## Growing together through questions and discussions
|
||||
## Growing Together Through Questions and Discussions
|
||||
|
||||
When reading this book, please do not easily skip knowledge points that you have not learned well. **Feel free to ask your questions in the comments section**, and my friends and I will do our best to answer you, and generally reply within two days.
|
||||
|
||||
@@ -239,7 +239,7 @@ As shown in the figure below, the web version has a comments section at the bott
|
||||
|
||||

|
||||
|
||||
## Algorithm learning roadmap
|
||||
## Algorithm Learning Roadmap
|
||||
|
||||
From an overall perspective, we can divide the process of learning data structures and algorithms into three stages.
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Summary
|
||||
|
||||
### Key Review
|
||||
|
||||
- The main audience of this book is algorithm beginners. If you already have a certain foundation, this book can help you systematically review algorithm knowledge, and the source code in the book can also be used as a "problem-solving toolkit."
|
||||
- The content of the book mainly includes three parts: complexity analysis, data structures, and algorithms, covering most topics in this field.
|
||||
- For algorithm novices, reading an introductory book during the initial learning stage is crucial, as it can help you avoid many detours.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Binary search
|
||||
# Binary Search
|
||||
|
||||
<u>Binary search</u> is an efficient searching algorithm based on the divide-and-conquer strategy. It leverages the orderliness of data to reduce the search range by half in each round until the target element is found or the search interval becomes empty.
|
||||
|
||||
@@ -53,7 +53,7 @@ The code is shown below:
|
||||
|
||||
**Space complexity is $O(1)$**: Pointers $i$ and $j$ use constant-size space.
|
||||
|
||||
## Interval representation methods
|
||||
## Interval Representation Methods
|
||||
|
||||
In addition to the closed interval mentioned above, another common interval representation is the "left-closed right-open" interval, defined as $[0, n)$, meaning the left boundary includes itself while the right boundary does not. Under this representation, the interval $[i, j)$ is empty when $i = j$.
|
||||
|
||||
@@ -69,7 +69,7 @@ Since both the left and right boundaries in the "closed interval" representation
|
||||
|
||||

|
||||
|
||||
## Advantages and limitations
|
||||
## Advantages and Limitations
|
||||
|
||||
Binary search performs well in both time and space aspects.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Binary search edge cases
|
||||
# Binary Search Edge Cases
|
||||
|
||||
## Finding the left boundary
|
||||
## Finding the Left Boundary
|
||||
|
||||
!!! question
|
||||
|
||||
@@ -19,13 +19,13 @@ When either of these situations occurs, simply return $-1$. The code is shown be
|
||||
[file]{binary_search_edge}-[class]{}-[func]{binary_search_left_edge}
|
||||
```
|
||||
|
||||
## Finding the right boundary
|
||||
## Finding the Right Boundary
|
||||
|
||||
So how do we find the rightmost `target`? The most direct approach is to modify the code and replace the pointer shrinking operation in the `nums[m] == target` case. The code is omitted here; interested readers can implement it themselves.
|
||||
|
||||
Below we introduce two more clever methods.
|
||||
|
||||
### Reusing left boundary search
|
||||
### Reusing Left Boundary Search
|
||||
|
||||
In fact, we can use the function for finding the leftmost element to find the rightmost element. The specific method is: **Convert finding the rightmost `target` into finding the leftmost `target + 1`**.
|
||||
|
||||
@@ -39,7 +39,7 @@ Note that the returned insertion point is $i$, so we need to subtract $1$ from i
|
||||
[file]{binary_search_edge}-[class]{}-[func]{binary_search_right_edge}
|
||||
```
|
||||
|
||||
### Converting to element search
|
||||
### Converting to Element Search
|
||||
|
||||
We know that when the array does not contain `target`, $i$ and $j$ will eventually point to the first elements greater than and less than `target`, respectively.
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Binary search insertion point
|
||||
# Binary Search Insertion Point
|
||||
|
||||
Binary search can not only be used to search for target elements but also to solve many variant problems, such as searching for the insertion position of a target element.
|
||||
|
||||
## Case without duplicate elements
|
||||
## Case Without Duplicate Elements
|
||||
|
||||
!!! question
|
||||
|
||||
@@ -26,7 +26,7 @@ Therefore, when the binary search ends, we must have: $i$ points to the first el
|
||||
[file]{binary_search_insertion}-[class]{}-[func]{binary_search_insertion_simple}
|
||||
```
|
||||
|
||||
## Case with duplicate elements
|
||||
## Case with Duplicate Elements
|
||||
|
||||
!!! question
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Hash optimization strategy
|
||||
# Hash Optimization Strategy
|
||||
|
||||
In algorithm problems, **we often reduce the time complexity of algorithms by replacing linear search with hash-based search**. Let's use an algorithm problem to deepen our understanding.
|
||||
|
||||
@@ -6,7 +6,7 @@ In algorithm problems, **we often reduce the time complexity of algorithms by re
|
||||
|
||||
Given an integer array `nums` and a target element `target`, search for two elements in the array whose "sum" equals `target`, and return their array indices. Any solution will do.
|
||||
|
||||
## Linear search: trading time for space
|
||||
## Linear Search: Trading Time for Space
|
||||
|
||||
Consider directly traversing all possible combinations. As shown in the figure below, we open a two-layer loop and judge in each round whether the sum of two integers equals `target`. If so, return their indices.
|
||||
|
||||
@@ -20,7 +20,7 @@ The code is shown below:
|
||||
|
||||
This method has a time complexity of $O(n^2)$ and a space complexity of $O(1)$, which is very time-consuming with large data volumes.
|
||||
|
||||
## Hash-based search: trading space for time
|
||||
## Hash-Based Search: Trading Space for Time
|
||||
|
||||
Consider using a hash table where key-value pairs are array elements and element indices respectively. Loop through the array, performing the steps shown in the figure below in each round:
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Searching algorithms revisited
|
||||
# Searching Algorithms Revisited
|
||||
|
||||
<u>Searching algorithms</u> are used to search for one or a group of elements that meet specific conditions in data structures (such as arrays, linked lists, trees, or graphs).
|
||||
|
||||
@@ -9,7 +9,7 @@ Searching algorithms can be divided into the following two categories based on t
|
||||
|
||||
It's not hard to see that these topics have all been covered in previous chapters, so searching algorithms are not unfamiliar to us. In this section, we will approach from a more systematic perspective and re-examine searching algorithms.
|
||||
|
||||
## Brute-force search
|
||||
## Brute-Force Search
|
||||
|
||||
Brute-force search locates target elements by traversing each element of the data structure.
|
||||
|
||||
@@ -20,7 +20,7 @@ The advantage of brute-force search is that it is simple and has good generality
|
||||
|
||||
However, **the time complexity of such algorithms is $O(n)$**, where $n$ is the number of elements, so performance is poor when dealing with large amounts of data.
|
||||
|
||||
## Adaptive search
|
||||
## Adaptive Search
|
||||
|
||||
Adaptive search utilizes the unique properties of data (such as orderliness) to optimize the search process, thereby locating target elements more efficiently.
|
||||
|
||||
@@ -36,7 +36,7 @@ However, **using these algorithms often requires data preprocessing**. For examp
|
||||
|
||||
Adaptive search algorithms are often called lookup algorithms, **mainly used to quickly retrieve target elements in specific data structures**.
|
||||
|
||||
## Search method selection
|
||||
## Search Method Selection
|
||||
|
||||
Given a dataset of size $n$, we can use linear search, binary search, tree search, hash-based search, and other methods to search for the target element. The working principles of each method are shown in the figure below.
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Summary
|
||||
|
||||
### Key Review
|
||||
|
||||
- Binary search relies on data orderliness and progressively reduces the search interval by half through loops. It requires input data to be sorted and is only applicable to arrays or data structures based on array implementations.
|
||||
- Brute-force search locates data by traversing the data structure. Linear search is applicable to arrays and linked lists, while breadth-first search and depth-first search are applicable to graphs and trees. Such algorithms have good generality and require no data preprocessing, but have a relatively high time complexity of $O(n)$.
|
||||
- Hash-based search, tree search, and binary search are efficient search methods that can quickly locate target elements in specific data structures. Such algorithms are highly efficient with time complexity reaching $O(\log n)$ or even $O(1)$, but typically require additional data structures.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Bubble sort
|
||||
# Bubble Sort
|
||||
|
||||
<u>Bubble sort (bubble sort)</u> achieves sorting by continuously comparing and swapping adjacent elements. This process is like bubbles rising from the bottom to the top, hence the name bubble sort.
|
||||
|
||||
@@ -25,7 +25,7 @@ As shown in the figure below, the bubbling process can be simulated using elemen
|
||||
=== "<7>"
|
||||

|
||||
|
||||
## Algorithm flow
|
||||
## Algorithm Flow
|
||||
|
||||
Assume the array has length $n$. The steps of bubble sort are shown in the figure below.
|
||||
|
||||
@@ -42,7 +42,7 @@ Example code is as follows:
|
||||
[file]{bubble_sort}-[class]{}-[func]{bubble_sort}
|
||||
```
|
||||
|
||||
## Efficiency optimization
|
||||
## Efficiency Optimization
|
||||
|
||||
We notice that if no swap operations are performed during a certain round of "bubbling", it means the array has already completed sorting and can directly return the result. Therefore, we can add a flag `flag` to monitor this situation and return immediately once it occurs.
|
||||
|
||||
@@ -52,7 +52,7 @@ After optimization, the worst-case time complexity and average time complexity o
|
||||
[file]{bubble_sort}-[class]{}-[func]{bubble_sort_with_flag}
|
||||
```
|
||||
|
||||
## Algorithm characteristics
|
||||
## Algorithm Characteristics
|
||||
|
||||
- **Time complexity of $O(n^2)$, adaptive sorting**: The array lengths traversed in each round of "bubbling" are $n - 1$, $n - 2$, $\dots$, $2$, $1$, totaling $(n - 1) n / 2$. After introducing the `flag` optimization, the best-case time complexity can reach $O(n)$.
|
||||
- **Space complexity of $O(1)$, in-place sorting**: Pointers $i$ and $j$ use a constant amount of extra space.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Bucket sort
|
||||
# Bucket Sort
|
||||
|
||||
The several sorting algorithms mentioned earlier all belong to "comparison-based sorting algorithms", which achieve sorting by comparing the size of elements. The time complexity of such sorting algorithms cannot exceed $O(n \log n)$. Next, we will explore several "non-comparison sorting algorithms", whose time complexity can reach linear order.
|
||||
|
||||
<u>Bucket sort (bucket sort)</u> is a typical application of the divide-and-conquer strategy. It works by setting up buckets with size order, each bucket corresponding to a data range, evenly distributing data to each bucket; then, sorting within each bucket separately; finally, merging all data in the order of the buckets.
|
||||
|
||||
## Algorithm flow
|
||||
## Algorithm Flow
|
||||
|
||||
Consider an array of length $n$, whose elements are floating-point numbers in the range $[0, 1)$. The flow of bucket sort is shown in the figure below.
|
||||
|
||||
@@ -20,7 +20,7 @@ The code is as follows:
|
||||
[file]{bucket_sort}-[class]{}-[func]{bucket_sort}
|
||||
```
|
||||
|
||||
## Algorithm characteristics
|
||||
## Algorithm Characteristics
|
||||
|
||||
Bucket sort is suitable for processing very large data volumes. For example, if the input data contains 1 million elements and system memory cannot load all the data at once, the data can be divided into 1000 buckets, each bucket sorted separately, and then the results merged.
|
||||
|
||||
@@ -28,7 +28,7 @@ Bucket sort is suitable for processing very large data volumes. For example, if
|
||||
- **Space complexity of $O(n + k)$, non-in-place sorting**: Additional space is required for $k$ buckets and a total of $n$ elements.
|
||||
- Whether bucket sort is stable depends on whether the algorithm for sorting elements within buckets is stable.
|
||||
|
||||
## How to achieve even distribution
|
||||
## How to Achieve Even Distribution
|
||||
|
||||
Theoretically, bucket sort can achieve $O(n)$ time complexity. **The key is to evenly distribute elements to each bucket**, because real data is often not evenly distributed. For example, if we want to evenly distribute all products on Taobao into 10 buckets by price range, there may be very many products below 100 yuan and very few above 1000 yuan. If the price intervals are evenly divided into 10, the difference in the number of products in each bucket will be very large.
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Counting sort
|
||||
# Counting Sort
|
||||
|
||||
<u>Counting sort (counting sort)</u> achieves sorting by counting the number of elements, typically applied to integer arrays.
|
||||
|
||||
## Simple implementation
|
||||
## Simple Implementation
|
||||
|
||||
Let's start with a simple example. Given an array `nums` of length $n$, where the elements are all "non-negative integers", the overall flow of counting sort is shown in the figure below.
|
||||
|
||||
@@ -22,7 +22,7 @@ The code is as follows:
|
||||
|
||||
From the perspective of bucket sort, we can regard each index of the counting array `counter` in counting sort as a bucket, and the process of counting quantities as distributing each element to the corresponding bucket. Essentially, counting sort is a special case of bucket sort for integer data.
|
||||
|
||||
## Complete implementation
|
||||
## Complete Implementation
|
||||
|
||||
Observant readers may have noticed that **if the input data is objects, step `3.` above becomes invalid**. Suppose the input data is product objects, and we want to sort the products by price (a member variable of the class), but the above algorithm can only give the sorting result of prices.
|
||||
|
||||
@@ -69,7 +69,7 @@ The implementation code of counting sort is as follows:
|
||||
[file]{counting_sort}-[class]{}-[func]{counting_sort}
|
||||
```
|
||||
|
||||
## Algorithm characteristics
|
||||
## Algorithm Characteristics
|
||||
|
||||
- **Time complexity of $O(n + m)$, non-adaptive sorting**: Involves traversing `nums` and traversing `counter`, both using linear time. Generally, $n \gg m$, and time complexity tends toward $O(n)$.
|
||||
- **Space complexity of $O(n + m)$, non-in-place sorting**: Uses arrays `res` and `counter` of lengths $n$ and $m$ respectively.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Heap sort
|
||||
# Heap Sort
|
||||
|
||||
!!! tip
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
Although the above method is feasible, it requires an additional array to save the popped elements, which is quite wasteful of space. In practice, we usually use a more elegant implementation method.
|
||||
|
||||
## Algorithm flow
|
||||
## Algorithm Flow
|
||||
|
||||
Assume the array length is $n$. The flow of heap sort is shown in the figure below.
|
||||
|
||||
@@ -66,7 +66,7 @@ In the code implementation, we use the same top-to-bottom heapify function `sift
|
||||
[file]{heap_sort}-[class]{}-[func]{heap_sort}
|
||||
```
|
||||
|
||||
## Algorithm characteristics
|
||||
## Algorithm Characteristics
|
||||
|
||||
- **Time complexity of $O(n \log n)$, non-adaptive sorting**: The build heap operation uses $O(n)$ time. Extracting the largest element from the heap has a time complexity of $O(\log n)$, looping a total of $n - 1$ rounds.
|
||||
- **Space complexity of $O(1)$, in-place sorting**: A few pointer variables use $O(1)$ space. Element swapping and heapify operations are both performed on the original array.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Insertion sort
|
||||
# Insertion Sort
|
||||
|
||||
<u>Insertion sort (insertion sort)</u> is a simple sorting algorithm that works very similarly to the process of manually organizing a deck of cards.
|
||||
|
||||
@@ -8,7 +8,7 @@ The figure below shows the operation flow of inserting an element into the array
|
||||
|
||||

|
||||
|
||||
## Algorithm flow
|
||||
## Algorithm Flow
|
||||
|
||||
The overall flow of insertion sort is shown in the figure below.
|
||||
|
||||
@@ -25,13 +25,13 @@ Example code is as follows:
|
||||
[file]{insertion_sort}-[class]{}-[func]{insertion_sort}
|
||||
```
|
||||
|
||||
## Algorithm characteristics
|
||||
## Algorithm Characteristics
|
||||
|
||||
- **Time complexity of $O(n^2)$, adaptive sorting**: In the worst case, each insertion operation requires loops of $n - 1$, $n-2$, $\dots$, $2$, $1$, summing to $(n - 1) n / 2$, so the time complexity is $O(n^2)$. When encountering ordered data, the insertion operation will terminate early. When the input array is completely ordered, insertion sort achieves the best-case time complexity of $O(n)$.
|
||||
- **Space complexity of $O(1)$, in-place sorting**: Pointers $i$ and $j$ use a constant amount of extra space.
|
||||
- **Stable sorting**: During the insertion operation process, we insert elements to the right of equal elements, without changing their order.
|
||||
|
||||
## Advantages of insertion sort
|
||||
## Advantages of Insertion Sort
|
||||
|
||||
The time complexity of insertion sort is $O(n^2)$, while the time complexity of quick sort, which we will learn about next, is $O(n \log n)$. Although insertion sort has a higher time complexity, **insertion sort is usually faster for smaller data volumes**.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Merge sort
|
||||
# Merge Sort
|
||||
|
||||
<u>Merge sort (merge sort)</u> is a sorting algorithm based on the divide-and-conquer strategy, which includes the "divide" and "merge" phases shown in the figure below.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||

|
||||
|
||||
## Algorithm flow
|
||||
## Algorithm Flow
|
||||
|
||||
As shown in the figure below, the "divide phase" recursively splits the array from the midpoint into two sub-arrays from top to bottom.
|
||||
|
||||
@@ -57,13 +57,13 @@ The implementation of merge sort is shown in the code below. Note that the inter
|
||||
[file]{merge_sort}-[class]{}-[func]{merge_sort}
|
||||
```
|
||||
|
||||
## Algorithm characteristics
|
||||
## Algorithm Characteristics
|
||||
|
||||
- **Time complexity of $O(n \log n)$, non-adaptive sorting**: The division produces a recursion tree of height $\log n$, and the total number of merge operations at each level is $n$, so the overall time complexity is $O(n \log n)$.
|
||||
- **Space complexity of $O(n)$, non-in-place sorting**: The recursion depth is $\log n$, using $O(\log n)$ size of stack frame space. The merge operation requires the aid of an auxiliary array, using $O(n)$ size of additional space.
|
||||
- **Stable sorting**: In the merge process, the order of equal elements remains unchanged.
|
||||
|
||||
## Linked list sorting
|
||||
## Linked List Sorting
|
||||
|
||||
For linked lists, merge sort has significant advantages over other sorting algorithms, **and can optimize the space complexity of linked list sorting tasks to $O(1)$**.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Quick sort
|
||||
# Quick Sort
|
||||
|
||||
<u>Quick sort (quick sort)</u> is a sorting algorithm based on the divide-and-conquer strategy, which operates efficiently and is widely applied.
|
||||
|
||||
@@ -45,7 +45,7 @@ After sentinel partitioning is complete, the original array is divided into thre
|
||||
[file]{quick_sort}-[class]{quick_sort}-[func]{partition}
|
||||
```
|
||||
|
||||
## Algorithm flow
|
||||
## Algorithm Flow
|
||||
|
||||
The overall flow of quick sort is shown in the figure below.
|
||||
|
||||
@@ -59,13 +59,13 @@ The overall flow of quick sort is shown in the figure below.
|
||||
[file]{quick_sort}-[class]{quick_sort}-[func]{quick_sort}
|
||||
```
|
||||
|
||||
## Algorithm characteristics
|
||||
## Algorithm Characteristics
|
||||
|
||||
- **Time complexity of $O(n \log n)$, non-adaptive sorting**: In the average case, the number of recursive levels of sentinel partitioning is $\log n$, and the total number of loops at each level is $n$, using $O(n \log n)$ time overall. In the worst case, each round of sentinel partitioning divides an array of length $n$ into two sub-arrays of length $0$ and $n - 1$, at which point the number of recursive levels reaches $n$, the number of loops at each level is $n$, and the total time used is $O(n^2)$.
|
||||
- **Space complexity of $O(n)$, in-place sorting**: In the case where the input array is completely reversed, the worst recursive depth reaches $n$, using $O(n)$ stack frame space. The sorting operation is performed on the original array without the aid of an additional array.
|
||||
- **Non-stable sorting**: In the last step of sentinel partitioning, the pivot may be swapped to the right of equal elements.
|
||||
|
||||
## Why is quick sort fast
|
||||
## Why Is Quick Sort Fast
|
||||
|
||||
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.
|
||||
|
||||
@@ -73,7 +73,7 @@ From the name, we can see that quick sort should have certain advantages in term
|
||||
- **High cache utilization**: 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.
|
||||
- **Small constant coefficient of complexity**: 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".
|
||||
|
||||
## Pivot optimization
|
||||
## Pivot Optimization
|
||||
|
||||
**Quick sort may have reduced time efficiency for certain inputs**. 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 $n - 1$ and the right sub-array length to be $0$. If we recurse down like this, each round of sentinel partitioning will have a sub-array length of $0$, the divide-and-conquer strategy fails, and quick sort degrades to a form approximate to "bubble sort".
|
||||
|
||||
@@ -89,7 +89,7 @@ Example code is as follows:
|
||||
[file]{quick_sort}-[class]{quick_sort_median}-[func]{partition}
|
||||
```
|
||||
|
||||
## Recursive depth optimization
|
||||
## Recursive Depth Optimization
|
||||
|
||||
**For certain inputs, quick sort may occupy more space**. Taking a completely ordered input array as an example, let the length of the sub-array in recursion be $m$. Each round of sentinel partitioning will produce a left sub-array of length $0$ and a right sub-array of length $m - 1$, 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 $n - 1$, at which point $O(n)$ size of stack frame space is required.
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Radix sort
|
||||
# Radix Sort
|
||||
|
||||
The previous section introduced counting sort, which is suitable for situations where the data volume $n$ is large but the data range $m$ is small. Suppose we need to sort $n = 10^6$ student IDs, and the student ID is an 8-digit number, which means the data range $m = 10^8$ is very large. Using counting sort would require allocating a large amount of memory space, whereas radix sort can avoid this situation.
|
||||
|
||||
<u>Radix sort (radix sort)</u> has a core idea consistent with counting sort, which also achieves sorting by counting quantities. Building on this, radix sort utilizes the progressive relationship between the digits of numbers, sorting each digit in turn to obtain the final sorting result.
|
||||
|
||||
## Algorithm flow
|
||||
## Algorithm Flow
|
||||
|
||||
Taking student ID data as an example, assume the lowest digit is the $1$st digit and the highest digit is the $8$th digit. The flow of radix sort is shown in the figure below.
|
||||
|
||||
@@ -32,7 +32,7 @@ Additionally, we need to slightly modify the counting sort code to make it sort
|
||||
|
||||
In successive sorting rounds, the result of a later round will override the result of an earlier round. For example, if the first round result is $a < b$, while the second round result is $a > b$, then the second round's result will replace the first round's result. Since higher-order digits have higher priority than lower-order digits, we should sort the lower digits first and then sort the higher digits.
|
||||
|
||||
## Algorithm characteristics
|
||||
## Algorithm Characteristics
|
||||
|
||||
Compared to counting sort, radix sort is suitable for larger numerical ranges, **but the prerequisite is that the data must be representable in a fixed number of digits, and the number of digits should not be too large**. For example, floating-point numbers are not suitable for radix sort because their number of digits $k$ may be too large, potentially leading to time complexity $O(nk) \gg O(n^2)$.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Selection sort
|
||||
# Selection Sort
|
||||
|
||||
<u>Selection sort (selection sort)</u> works very simply: it opens a loop, and in each round, selects the smallest element from the unsorted interval and places it at the end of the sorted interval.
|
||||
|
||||
@@ -49,7 +49,7 @@ In the code, we use $k$ to record the smallest element within the unsorted inter
|
||||
[file]{selection_sort}-[class]{}-[func]{selection_sort}
|
||||
```
|
||||
|
||||
## Algorithm characteristics
|
||||
## Algorithm Characteristics
|
||||
|
||||
- **Time complexity of $O(n^2)$, non-adaptive sorting**: The outer loop has $n - 1$ rounds in total. The length of the unsorted interval in the first round is $n$, and the length of the unsorted interval in the last round is $2$. That is, each round of the outer loop contains $n$, $n - 1$, $\dots$, $3$, $2$ inner loop iterations, summing to $\frac{(n - 1)(n + 2)}{2}$.
|
||||
- **Space complexity of $O(1)$, in-place sorting**: Pointers $i$ and $j$ use a constant amount of extra space.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Sorting algorithm
|
||||
# Sorting Algorithm
|
||||
|
||||
<u>Sorting algorithm (sorting algorithm)</u> is used to arrange a group of data in a specific order. Sorting algorithms have extensive applications because ordered data can usually be searched, analyzed, and processed more efficiently.
|
||||
|
||||
@@ -6,7 +6,7 @@ As shown in the figure below, data types in sorting algorithms can be integers,
|
||||
|
||||

|
||||
|
||||
## Evaluation dimensions
|
||||
## Evaluation Dimensions
|
||||
|
||||
**Execution efficiency**: We expect the time complexity of sorting algorithms to be as low as possible, with a smaller total number of operations (reducing the constant factor in time complexity). For large data volumes, execution efficiency is particularly important.
|
||||
|
||||
@@ -17,7 +17,7 @@ As shown in the figure below, data types in sorting algorithms can be integers,
|
||||
Stable sorting is a necessary condition for multi-level sorting scenarios. Suppose we have a table storing student information, where column 1 and column 2 are name and age, respectively. In this case, <u>unstable sorting</u> may cause the ordered nature of the input data to be lost:
|
||||
|
||||
```shell
|
||||
# Input data is sorted by name
|
||||
# Input Data Is Sorted by Name
|
||||
# (name, age)
|
||||
('A', 19)
|
||||
('B', 18)
|
||||
@@ -25,9 +25,9 @@ Stable sorting is a necessary condition for multi-level sorting scenarios. Suppo
|
||||
('D', 19)
|
||||
('E', 23)
|
||||
|
||||
# Assuming we use an unstable sorting algorithm to sort the list by age,
|
||||
# in the result, the relative positions of ('D', 19) and ('A', 19) are changed,
|
||||
# and the property that the input data is sorted by name is lost
|
||||
# Assuming We Use an Unstable Sorting Algorithm to Sort the List by Age,
|
||||
# In the Result, the Relative Positions of ('D', 19) and ('A', 19) Are Changed,
|
||||
# And the Property That the Input Data Is Sorted by Name Is Lost
|
||||
('B', 18)
|
||||
('D', 19)
|
||||
('A', 19)
|
||||
@@ -39,7 +39,7 @@ Stable sorting is a necessary condition for multi-level sorting scenarios. Suppo
|
||||
|
||||
**Comparison-based or not**: <u>Comparison-based sorting</u> relies on comparison operators ($<$, $=$, $>$) to determine the relative order of elements, thereby sorting the entire array, with a theoretical optimal time complexity of $O(n \log n)$. <u>Non-comparison sorting</u> does not use comparison operators and can achieve a time complexity of $O(n)$, but its versatility is relatively limited.
|
||||
|
||||
## Ideal sorting algorithm
|
||||
## Ideal Sorting Algorithm
|
||||
|
||||
**Fast execution, in-place, stable, adaptive, good versatility**. Clearly, no sorting algorithm has been discovered to date that combines all of these characteristics. Therefore, when selecting a sorting algorithm, it is necessary to decide based on the specific characteristics of the data and the requirements of the problem.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Summary
|
||||
|
||||
### Key review
|
||||
### Key Review
|
||||
|
||||
- Bubble sort achieves sorting by swapping adjacent elements. By adding a flag to enable early return, we can optimize the best-case time complexity of bubble sort to $O(n)$.
|
||||
- Insertion sort completes sorting by inserting elements from the unsorted interval into the correct position in the sorted interval each round. Although the time complexity of insertion sort is $O(n^2)$, it is very popular in small data volume sorting tasks because it involves relatively few unit operations.
|
||||
|
||||
@@ -389,7 +389,7 @@ Similarly, we can directly use the deque classes already implemented in programm
|
||||
|
||||
```
|
||||
|
||||
??? pythontutor "Visualize Execution"
|
||||
??? pythontutor "Code Visualization"
|
||||
|
||||
https://pythontutor.com/render.html#code=from%20collections%20import%20deque%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%0A%20%20%20%20deq%20%3D%20deque%28%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E9%98%9F%0A%20%20%20%20deq.append%282%29%20%20%23%20%E6%B7%BB%E5%8A%A0%E8%87%B3%E9%98%9F%E5%B0%BE%0A%20%20%20%20deq.append%285%29%0A%20%20%20%20deq.append%284%29%0A%20%20%20%20deq.appendleft%283%29%20%20%23%20%E6%B7%BB%E5%8A%A0%E8%87%B3%E9%98%9F%E9%A6%96%0A%20%20%20%20deq.appendleft%281%29%0A%20%20%20%20print%28%22%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%20deque%20%3D%22,%20deq%29%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E5%85%83%E7%B4%A0%0A%20%20%20%20front%20%3D%20deq%5B0%5D%20%20%23%20%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%0A%20%20%20%20print%28%22%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%20front%20%3D%22,%20front%29%0A%20%20%20%20rear%20%3D%20deq%5B-1%5D%20%20%23%20%E9%98%9F%E5%B0%BE%E5%85%83%E7%B4%A0%0A%20%20%20%20print%28%22%E9%98%9F%E5%B0%BE%E5%85%83%E7%B4%A0%20rear%20%3D%22,%20rear%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%87%BA%E9%98%9F%0A%20%20%20%20pop_front%20%3D%20deq.popleft%28%29%20%20%23%20%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%E5%87%BA%E9%98%9F%0A%20%20%20%20print%28%22%E9%98%9F%E9%A6%96%E5%87%BA%E9%98%9F%E5%85%83%E7%B4%A0%20%20pop_front%20%3D%22,%20pop_front%29%0A%20%20%20%20print%28%22%E9%98%9F%E9%A6%96%E5%87%BA%E9%98%9F%E5%90%8E%20deque%20%3D%22,%20deq%29%0A%20%20%20%20pop_rear%20%3D%20deq.pop%28%29%20%20%23%20%E9%98%9F%E5%B0%BE%E5%85%83%E7%B4%A0%E5%87%BA%E9%98%9F%0A%20%20%20%20print%28%22%E9%98%9F%E5%B0%BE%E5%87%BA%E9%98%9F%E5%85%83%E7%B4%A0%20%20pop_rear%20%3D%22,%20pop_rear%29%0A%20%20%20%20print%28%22%E9%98%9F%E5%B0%BE%E5%87%BA%E9%98%9F%E5%90%8E%20deque%20%3D%22,%20deq%29%0A%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E7%9A%84%E9%95%BF%E5%BA%A6%0A%20%20%20%20size%20%3D%20len%28deq%29%0A%20%20%20%20print%28%22%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E9%95%BF%E5%BA%A6%20size%20%3D%22,%20size%29%0A%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20len%28deq%29%20%3D%3D%200%0A%20%20%20%20print%28%22%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%20%3D%22,%20is_empty%29&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
||||
|
||||
|
||||
@@ -362,7 +362,7 @@ We can directly use the ready-made queue classes in programming languages:
|
||||
|
||||
```
|
||||
|
||||
??? pythontutor "Visualize Execution"
|
||||
??? pythontutor "Code Visualization"
|
||||
|
||||
https://pythontutor.com/render.html#code=from%20collections%20import%20deque%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E9%98%9F%E5%88%97%0A%20%20%20%20%23%20%E5%9C%A8%20Python%20%E4%B8%AD%EF%BC%8C%E6%88%91%E4%BB%AC%E4%B8%80%E8%88%AC%E5%B0%86%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E7%B1%BB%20deque%20%E7%9C%8B%E4%BD%9C%E9%98%9F%E5%88%97%E4%BD%BF%E7%94%A8%0A%20%20%20%20%23%20%E8%99%BD%E7%84%B6%20queue.Queue%28%29%20%E6%98%AF%E7%BA%AF%E6%AD%A3%E7%9A%84%E9%98%9F%E5%88%97%E7%B1%BB%EF%BC%8C%E4%BD%86%E4%B8%8D%E5%A4%AA%E5%A5%BD%E7%94%A8%0A%20%20%20%20que%20%3D%20deque%28%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E9%98%9F%0A%20%20%20%20que.append%281%29%0A%20%20%20%20que.append%283%29%0A%20%20%20%20que.append%282%29%0A%20%20%20%20que.append%285%29%0A%20%20%20%20que.append%284%29%0A%20%20%20%20print%28%22%E9%98%9F%E5%88%97%20que%20%3D%22,%20que%29%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%0A%20%20%20%20front%20%3D%20que%5B0%5D%0A%20%20%20%20print%28%22%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%20front%20%3D%22,%20front%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%87%BA%E9%98%9F%0A%20%20%20%20pop%20%3D%20que.popleft%28%29%0A%20%20%20%20print%28%22%E5%87%BA%E9%98%9F%E5%85%83%E7%B4%A0%20pop%20%3D%22,%20pop%29%0A%20%20%20%20print%28%22%E5%87%BA%E9%98%9F%E5%90%8E%20que%20%3D%22,%20que%29%0A%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E9%98%9F%E5%88%97%E7%9A%84%E9%95%BF%E5%BA%A6%0A%20%20%20%20size%20%3D%20len%28que%29%0A%20%20%20%20print%28%22%E9%98%9F%E5%88%97%E9%95%BF%E5%BA%A6%20size%20%3D%22,%20size%29%0A%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E9%98%9F%E5%88%97%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20len%28que%29%20%3D%3D%200%0A%20%20%20%20print%28%22%E9%98%9F%E5%88%97%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%20%3D%22,%20is_empty%29&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
||||
|
||||
|
||||
@@ -355,7 +355,7 @@ Typically, we can directly use the built-in stack class provided by the programm
|
||||
|
||||
```
|
||||
|
||||
??? pythontutor "Visualize Execution"
|
||||
??? pythontutor "Code Visualization"
|
||||
|
||||
https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%A0%88%0A%20%20%20%20%23%20Python%20%E6%B2%A1%E6%9C%89%E5%86%85%E7%BD%AE%E7%9A%84%E6%A0%88%E7%B1%BB%EF%BC%8C%E5%8F%AF%E4%BB%A5%E6%8A%8A%20list%20%E5%BD%93%E4%BD%9C%E6%A0%88%E6%9D%A5%E4%BD%BF%E7%94%A8%0A%20%20%20%20stack%20%3D%20%5B%5D%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E6%A0%88%0A%20%20%20%20stack.append%281%29%0A%20%20%20%20stack.append%283%29%0A%20%20%20%20stack.append%282%29%0A%20%20%20%20stack.append%285%29%0A%20%20%20%20stack.append%284%29%0A%20%20%20%20print%28%22%E6%A0%88%20stack%20%3D%22,%20stack%29%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%0A%20%20%20%20peek%20%3D%20stack%5B-1%5D%0A%20%20%20%20print%28%22%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%20peek%20%3D%22,%20peek%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%87%BA%E6%A0%88%0A%20%20%20%20pop%20%3D%20stack.pop%28%29%0A%20%20%20%20print%28%22%E5%87%BA%E6%A0%88%E5%85%83%E7%B4%A0%20pop%20%3D%22,%20pop%29%0A%20%20%20%20print%28%22%E5%87%BA%E6%A0%88%E5%90%8E%20stack%20%3D%22,%20stack%29%0A%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%0A%20%20%20%20size%20%3D%20len%28stack%29%0A%20%20%20%20print%28%22%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%20size%20%3D%22,%20size%29%0A%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20len%28stack%29%20%3D%3D%200%0A%20%20%20%20print%28%22%E6%A0%88%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%20%3D%22,%20is_empty%29&cumulative=false&curInstr=2&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Array representation of binary trees
|
||||
# Array Representation of Binary Trees
|
||||
|
||||
Under the linked list representation, the storage unit of a binary tree is a node `TreeNode`, and nodes are connected by pointers. The previous section introduced the basic operations of binary trees under the linked list representation.
|
||||
|
||||
So, can we use an array to represent a binary tree? The answer is yes.
|
||||
|
||||
## Representing perfect binary trees
|
||||
## Representing Perfect Binary Trees
|
||||
|
||||
Let's analyze a simple case first. Given a perfect binary tree, we store all nodes in an array according to the order of level-order traversal, where each node corresponds to a unique array index.
|
||||
|
||||
@@ -14,7 +14,7 @@ Based on the characteristics of level-order traversal, we can derive a "mapping
|
||||
|
||||
**The mapping formula plays a role similar to the node references (pointers) in linked lists**. Given any node in the array, we can access its left (right) child node using the mapping formula.
|
||||
|
||||
## Representing any binary tree
|
||||
## Representing Any Binary Tree
|
||||
|
||||
Perfect binary trees are a special case; in the middle levels of a binary tree, there are typically many `None` values. Since the level-order traversal sequence does not include these `None` values, we cannot infer the number and distribution of `None` values based on this sequence alone. **This means multiple binary tree structures can correspond to the same level-order traversal sequence**.
|
||||
|
||||
@@ -151,7 +151,7 @@ The following code implements a binary tree based on array representation, inclu
|
||||
[file]{array_binary_tree}-[class]{array_binary_tree}-[func]{}
|
||||
```
|
||||
|
||||
## Advantages and limitations
|
||||
## Advantages and Limitations
|
||||
|
||||
The array representation of binary trees has the following advantages:
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# AVL tree *
|
||||
# Avl Tree *
|
||||
|
||||
In the "Binary Search Tree" section, we mentioned that after multiple insertion and removal operations, a binary search tree may degenerate into a linked list. In this case, the time complexity of all operations degrades from $O(\log n)$ to $O(n)$.
|
||||
|
||||
@@ -12,11 +12,11 @@ For example, in the perfect binary tree shown in the figure below, after inserti
|
||||
|
||||
In 1962, G. M. Adelson-Velsky and E. M. Landis proposed the <u>AVL tree</u> in their paper "An algorithm for the organization of information". The paper described in detail a series of operations ensuring that after continuously adding and removing nodes, the AVL tree does not degenerate, thus keeping the time complexity of various operations at the $O(\log n)$ level. In other words, in scenarios requiring frequent insertions, deletions, searches, and modifications, the AVL tree can always maintain efficient data operation performance, making it very valuable in applications.
|
||||
|
||||
## Common terminology in AVL trees
|
||||
## Common Terminology in Avl Trees
|
||||
|
||||
An AVL tree is both a binary search tree and a balanced binary tree, simultaneously satisfying all the properties of these two types of binary trees, hence it is a <u>balanced binary search tree</u>.
|
||||
|
||||
### Node height
|
||||
### Node Height
|
||||
|
||||
Since the operations related to AVL trees require obtaining node heights, we need to add a `height` variable to the node class:
|
||||
|
||||
@@ -240,7 +240,7 @@ The "node height" refers to the distance from that node to its farthest leaf nod
|
||||
[file]{avl_tree}-[class]{avl_tree}-[func]{update_height}
|
||||
```
|
||||
|
||||
### Node balance factor
|
||||
### Node Balance Factor
|
||||
|
||||
The <u>balance factor</u> of a node is defined as the height of the node's left subtree minus the height of its right subtree, and the balance factor of a null node is defined as $0$. We also encapsulate the function to obtain the node's balance factor for convenient subsequent use:
|
||||
|
||||
@@ -252,13 +252,13 @@ The <u>balance factor</u> of a node is defined as the height of the node's left
|
||||
|
||||
Let the balance factor be $f$, then the balance factor of any node in an AVL tree satisfies $-1 \le f \le 1$.
|
||||
|
||||
## Rotations in AVL trees
|
||||
## Rotations in Avl Trees
|
||||
|
||||
The characteristic of AVL trees lies in the "rotation" operation, which can restore balance to unbalanced nodes without affecting the inorder traversal sequence of the binary tree. In other words, **rotation operations can both maintain the property of a "binary search tree" and make the tree return to a "balanced binary tree"**.
|
||||
|
||||
We call nodes with a balance factor absolute value $> 1$ "unbalanced nodes". Depending on the imbalance situation, rotation operations are divided into four types: right rotation, left rotation, left rotation then right rotation, and right rotation then left rotation. Below we describe these rotation operations in detail.
|
||||
|
||||
### Right rotation
|
||||
### Right Rotation
|
||||
|
||||
As shown in the figure below, the value below the node is the balance factor. From bottom to top, the first unbalanced node in the binary tree is "node 3". We focus on the subtree with this unbalanced node as the root, denoting the node as `node` and its left child as `child`, and perform a "right rotation" operation. After the right rotation is completed, the subtree regains balance and still maintains the properties of a binary search tree.
|
||||
|
||||
@@ -284,7 +284,7 @@ As shown in the figure below, when the `child` node has a right child (denoted a
|
||||
[file]{avl_tree}-[class]{avl_tree}-[func]{right_rotate}
|
||||
```
|
||||
|
||||
### Left rotation
|
||||
### Left Rotation
|
||||
|
||||
Correspondingly, if considering the "mirror" of the above unbalanced binary tree, the "left rotation" operation shown in the figure below needs to be performed.
|
||||
|
||||
@@ -300,19 +300,19 @@ It can be observed that **right rotation and left rotation operations are mirror
|
||||
[file]{avl_tree}-[class]{avl_tree}-[func]{left_rotate}
|
||||
```
|
||||
|
||||
### Left rotation then right rotation
|
||||
### Left Rotation Then Right Rotation
|
||||
|
||||
For the unbalanced node 3 in the figure below, using either left rotation or right rotation alone cannot restore the subtree to balance. In this case, a "left rotation" needs to be performed on `child` first, followed by a "right rotation" on `node`.
|
||||
|
||||

|
||||
|
||||
### Right rotation then left rotation
|
||||
### Right Rotation Then Left Rotation
|
||||
|
||||
As shown in the figure below, for the mirror case of the above unbalanced binary tree, a "right rotation" needs to be performed on `child` first, then a "left rotation" on `node`.
|
||||
|
||||

|
||||
|
||||
### Choice of rotation
|
||||
### Choice of Rotation
|
||||
|
||||
The four imbalances shown in the figure below correspond one-to-one with the above cases, requiring right rotation, left rotation then right rotation, right rotation then left rotation, and left rotation operations respectively.
|
||||
|
||||
@@ -335,9 +335,9 @@ For ease of use, we encapsulate the rotation operations into a function. **With
|
||||
[file]{avl_tree}-[class]{avl_tree}-[func]{rotate}
|
||||
```
|
||||
|
||||
## Common operations in AVL trees
|
||||
## Common Operations in Avl Trees
|
||||
|
||||
### Node insertion
|
||||
### Node Insertion
|
||||
|
||||
The node insertion operation in AVL trees is similar in principle to that in binary search trees. The only difference is that after inserting a node in an AVL tree, a series of unbalanced nodes may appear on the path from that node to the root. Therefore, **we need to start from this node and perform rotation operations from bottom to top, restoring balance to all unbalanced nodes**. The code is as follows:
|
||||
|
||||
@@ -345,7 +345,7 @@ The node insertion operation in AVL trees is similar in principle to that in bin
|
||||
[file]{avl_tree}-[class]{avl_tree}-[func]{insert_helper}
|
||||
```
|
||||
|
||||
### Node removal
|
||||
### Node Removal
|
||||
|
||||
Similarly, on the basis of the binary search tree's node removal method, rotation operations need to be performed from bottom to top to restore balance to all unbalanced nodes. The code is as follows:
|
||||
|
||||
@@ -353,11 +353,11 @@ Similarly, on the basis of the binary search tree's node removal method, rotatio
|
||||
[file]{avl_tree}-[class]{avl_tree}-[func]{remove_helper}
|
||||
```
|
||||
|
||||
### Node search
|
||||
### Node Search
|
||||
|
||||
The node search operation in AVL trees is consistent with that in binary search trees, and will not be elaborated here.
|
||||
|
||||
## Typical applications of AVL trees
|
||||
## Typical Applications of Avl Trees
|
||||
|
||||
- Organizing and storing large-scale data, suitable for scenarios with high-frequency searches and low-frequency insertions and deletions.
|
||||
- Used to build index systems in databases.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Binary search tree
|
||||
# Binary Search Tree
|
||||
|
||||
As shown in the figure below, a <u>binary search tree</u> satisfies the following conditions.
|
||||
|
||||
@@ -7,11 +7,11 @@ As shown in the figure below, a <u>binary search tree</u> satisfies the followin
|
||||
|
||||

|
||||
|
||||
## Operations on a binary search tree
|
||||
## Operations on a Binary Search Tree
|
||||
|
||||
We encapsulate the binary search tree as a class `BinarySearchTree` and declare a member variable `root` pointing to the tree's root node.
|
||||
|
||||
### Searching for a node
|
||||
### Searching for a Node
|
||||
|
||||
Given a target node value `num`, we can search according to the properties of the binary search tree. As shown in the figure below, we declare a node `cur` and start from the binary tree's root node `root`, looping to compare the node value `cur.val` with `num`.
|
||||
|
||||
@@ -37,7 +37,7 @@ The search operation in a binary search tree works on the same principle as the
|
||||
[file]{binary_search_tree}-[class]{binary_search_tree}-[func]{search}
|
||||
```
|
||||
|
||||
### Inserting a node
|
||||
### Inserting a Node
|
||||
|
||||
Given an element `num` to be inserted, in order to maintain the property of the binary search tree "left subtree < root node < right subtree," the insertion process is as shown in the figure below.
|
||||
|
||||
@@ -57,7 +57,7 @@ In the code implementation, note the following two points:
|
||||
|
||||
Similar to searching for a node, inserting a node uses $O(\log n)$ time.
|
||||
|
||||
### Removing a node
|
||||
### Removing a Node
|
||||
|
||||
First, find the target node in the binary tree, then remove it. Similar to node insertion, we need to ensure that after the removal operation is completed, the binary search tree's property of "left subtree $<$ root node $<$ right subtree" is still maintained. Therefore, depending on the number of child nodes the target node has, we divide it into 0, 1, and 2 three cases, and execute the corresponding node removal operations.
|
||||
|
||||
@@ -94,7 +94,7 @@ The node removal operation also uses $O(\log n)$ time, where finding the node to
|
||||
[file]{binary_search_tree}-[class]{binary_search_tree}-[func]{remove}
|
||||
```
|
||||
|
||||
### Inorder traversal is ordered
|
||||
### Inorder Traversal Is Ordered
|
||||
|
||||
As shown in the figure below, the inorder traversal of a binary tree follows the "left $\rightarrow$ root $\rightarrow$ right" traversal order, while the binary search tree satisfies the "left child node $<$ root node $<$ right child node" size relationship.
|
||||
|
||||
@@ -104,7 +104,7 @@ Using the property of inorder traversal being ascending, we can obtain ordered d
|
||||
|
||||

|
||||
|
||||
## Efficiency of binary search trees
|
||||
## Efficiency of Binary Search Trees
|
||||
|
||||
Given a set of data, we consider using an array or a binary search tree for storage. Observing the table below, all operations in a binary search tree have logarithmic time complexity, providing stable and efficient performance. Arrays are more efficient than binary search trees only in scenarios with high-frequency additions and low-frequency searches and deletions.
|
||||
|
||||
@@ -122,7 +122,7 @@ However, if we continuously insert and remove nodes in a binary search tree, it
|
||||
|
||||

|
||||
|
||||
## Common applications of binary search trees
|
||||
## Common Applications of Binary Search Trees
|
||||
|
||||
- Used as multi-level indexes in systems to implement efficient search, insertion, and removal operations.
|
||||
- Serves as the underlying data structure for certain search algorithms.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Binary tree
|
||||
# Binary Tree
|
||||
|
||||
A <u>binary tree</u> is a non-linear data structure that represents the derivation relationship between "ancestors" and "descendants" and embodies the divide-and-conquer logic of "one divides into two". Similar to a linked list, the basic unit of a binary tree is a node, and each node contains a value, a reference to its left child node, and a reference to its right child node.
|
||||
|
||||
@@ -213,7 +213,7 @@ Each node has two references (pointers), pointing respectively to the <u>left-ch
|
||||
|
||||

|
||||
|
||||
## Common terminology of binary trees
|
||||
## Common Terminology of Binary Trees
|
||||
|
||||
The commonly used terminology of binary trees is shown in the figure below.
|
||||
|
||||
@@ -232,9 +232,9 @@ The commonly used terminology of binary trees is shown in the figure below.
|
||||
|
||||
Please note that we usually define "height" and "depth" as "the number of edges traversed", but some questions or textbooks may define them as "the number of nodes traversed". In this case, both height and depth need to be incremented by 1.
|
||||
|
||||
## Basic operations of binary trees
|
||||
## Basic Operations of Binary Trees
|
||||
|
||||
### Initializing a binary tree
|
||||
### Initializing a Binary Tree
|
||||
|
||||
Similar to a linked list, the initialization of a binary tree involves first creating the nodes and then establishing the references (pointers) between them.
|
||||
|
||||
@@ -461,11 +461,11 @@ Similar to a linked list, the initialization of a binary tree involves first cre
|
||||
|
||||
```
|
||||
|
||||
??? pythontutor "Code visualization"
|
||||
??? pythontutor "Code Visualization"
|
||||
|
||||
https://pythontutor.com/render.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%8F%89%E6%A0%91%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%23%20%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E4%BA%8C%E5%8F%89%E6%A0%91%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E8%8A%82%E7%82%B9%0A%20%20%20%20n1%20%3D%20TreeNode%28val%3D1%29%0A%20%20%20%20n2%20%3D%20TreeNode%28val%3D2%29%0A%20%20%20%20n3%20%3D%20TreeNode%28val%3D3%29%0A%20%20%20%20n4%20%3D%20TreeNode%28val%3D4%29%0A%20%20%20%20n5%20%3D%20TreeNode%28val%3D5%29%0A%20%20%20%20%23%20%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%EF%BC%88%E6%8C%87%E9%92%88%EF%BC%89%0A%20%20%20%20n1.left%20%3D%20n2%0A%20%20%20%20n1.right%20%3D%20n3%0A%20%20%20%20n2.left%20%3D%20n4%0A%20%20%20%20n2.right%20%3D%20n5&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
||||
|
||||
### Inserting and removing nodes
|
||||
### Inserting and Removing Nodes
|
||||
|
||||
Similar to a linked list, inserting and removing nodes in a binary tree can be achieved by modifying pointers. The figure below provides an example.
|
||||
|
||||
@@ -629,7 +629,7 @@ Similar to a linked list, inserting and removing nodes in a binary tree can be a
|
||||
|
||||
```
|
||||
|
||||
??? pythontutor "Code visualization"
|
||||
??? pythontutor "Code Visualization"
|
||||
|
||||
https://pythontutor.com/render.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%8F%89%E6%A0%91%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%23%20%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E4%BA%8C%E5%8F%89%E6%A0%91%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E8%8A%82%E7%82%B9%0A%20%20%20%20n1%20%3D%20TreeNode%28val%3D1%29%0A%20%20%20%20n2%20%3D%20TreeNode%28val%3D2%29%0A%20%20%20%20n3%20%3D%20TreeNode%28val%3D3%29%0A%20%20%20%20n4%20%3D%20TreeNode%28val%3D4%29%0A%20%20%20%20n5%20%3D%20TreeNode%28val%3D5%29%0A%20%20%20%20%23%20%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%EF%BC%88%E6%8C%87%E9%92%88%EF%BC%89%0A%20%20%20%20n1.left%20%3D%20n2%0A%20%20%20%20n1.right%20%3D%20n3%0A%20%20%20%20n2.left%20%3D%20n4%0A%20%20%20%20n2.right%20%3D%20n5%0A%0A%20%20%20%20%23%20%E6%8F%92%E5%85%A5%E4%B8%8E%E5%88%A0%E9%99%A4%E8%8A%82%E7%82%B9%0A%20%20%20%20p%20%3D%20TreeNode%280%29%0A%20%20%20%20%23%20%E5%9C%A8%20n1%20-%3E%20n2%20%E4%B8%AD%E9%97%B4%E6%8F%92%E5%85%A5%E8%8A%82%E7%82%B9%20P%0A%20%20%20%20n1.left%20%3D%20p%0A%20%20%20%20p.left%20%3D%20n2%0A%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E8%8A%82%E7%82%B9%20P%0A%20%20%20%20n1.left%20%3D%20n2&cumulative=false&curInstr=37&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
||||
|
||||
@@ -637,9 +637,9 @@ Similar to a linked list, inserting and removing nodes in a binary tree can be a
|
||||
|
||||
It should be noted that inserting nodes may change the original logical structure of the binary tree, while removing nodes typically involves removing the node and all its subtrees. Therefore, in a binary tree, insertion and removal are usually performed through a set of operations to achieve meaningful outcomes.
|
||||
|
||||
## Common types of binary trees
|
||||
## Common Types of Binary Trees
|
||||
|
||||
### Perfect binary tree
|
||||
### Perfect Binary Tree
|
||||
|
||||
As shown in the figure below, a <u>perfect binary tree</u> has all levels completely filled with nodes. In a perfect binary tree, leaf nodes have a degree of $0$, while all other nodes have a degree of $2$. If the tree height is $h$, the total number of nodes is $2^{h+1} - 1$, exhibiting a standard exponential relationship that reflects the common phenomenon of cell division in nature.
|
||||
|
||||
@@ -649,25 +649,25 @@ As shown in the figure below, a <u>perfect binary tree</u> has all levels comple
|
||||
|
||||

|
||||
|
||||
### Complete binary tree
|
||||
### Complete Binary Tree
|
||||
|
||||
As shown in the figure below, a <u>complete binary tree</u> only allows the bottom level to be incompletely filled, and the nodes at the bottom level must be filled continuously from left to right. Note that a perfect binary tree is also a complete binary tree.
|
||||
|
||||

|
||||
|
||||
### Full binary tree
|
||||
### Full Binary Tree
|
||||
|
||||
As shown in the figure below, in a <u>full binary tree</u>, all nodes except leaf nodes have two child nodes.
|
||||
|
||||

|
||||
|
||||
### Balanced binary tree
|
||||
### Balanced Binary Tree
|
||||
|
||||
As shown in the figure below, in a <u>balanced binary tree</u>, the absolute difference between the height of the left and right subtrees of any node does not exceed 1.
|
||||
|
||||

|
||||
|
||||
## Degeneration of binary trees
|
||||
## Degeneration of Binary Trees
|
||||
|
||||
The figure below shows the ideal and degenerate structures of binary trees. When every level of a binary tree is filled, it reaches the "perfect binary tree" state; when all nodes are biased toward one side, the binary tree degenerates into a "linked list".
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Binary tree traversal
|
||||
# Binary Tree Traversal
|
||||
|
||||
From a physical structure perspective, a tree is a data structure based on linked lists. Hence, its traversal method involves accessing nodes one by one through pointers. However, a tree is a non-linear data structure, which makes traversing a tree more complex than traversing a linked list, requiring the assistance of search algorithms.
|
||||
|
||||
The common traversal methods for binary trees include level-order traversal, pre-order traversal, in-order traversal, and post-order traversal.
|
||||
|
||||
## Level-order traversal
|
||||
## Level-Order Traversal
|
||||
|
||||
As shown in the figure below, <u>level-order traversal</u> traverses the binary tree from top to bottom, layer by layer. Within each level, it visits nodes from left to right.
|
||||
|
||||
@@ -12,7 +12,7 @@ Level-order traversal is essentially <u>breadth-first traversal</u>, also known
|
||||
|
||||

|
||||
|
||||
### Code implementation
|
||||
### Code Implementation
|
||||
|
||||
Breadth-first traversal is typically implemented with the help of a "queue". The queue follows the "first in, first out" rule, while breadth-first traversal follows the "layer-by-layer progression" rule; the underlying ideas of the two are consistent. The implementation code is as follows:
|
||||
|
||||
@@ -20,12 +20,12 @@ Breadth-first traversal is typically implemented with the help of a "queue". The
|
||||
[file]{binary_tree_bfs}-[class]{}-[func]{level_order}
|
||||
```
|
||||
|
||||
### Complexity analysis
|
||||
### Complexity Analysis
|
||||
|
||||
- **Time complexity is $O(n)$**: All nodes are visited once, using $O(n)$ time, where $n$ is the number of nodes.
|
||||
- **Space complexity is $O(n)$**: In the worst case, i.e., a full binary tree, before traversing to the bottom level, the queue contains at most $(n + 1) / 2$ nodes simultaneously, occupying $O(n)$ space.
|
||||
|
||||
## Preorder, inorder, and postorder traversal
|
||||
## Preorder, Inorder, and Postorder Traversal
|
||||
|
||||
Correspondingly, preorder, inorder, and postorder traversals all belong to <u>depth-first traversal</u>, also known as <u>depth-first search (DFS)</u>, which embodies a "first go to the end, then backtrack and continue" traversal method.
|
||||
|
||||
@@ -33,7 +33,7 @@ The figure below shows how depth-first traversal works on a binary tree. **Depth
|
||||
|
||||

|
||||
|
||||
### Code implementation
|
||||
### Code Implementation
|
||||
|
||||
Depth-first search is usually implemented based on recursion:
|
||||
|
||||
@@ -83,7 +83,7 @@ The figure below shows the recursive process of preorder traversal of a binary t
|
||||
=== "<11>"
|
||||

|
||||
|
||||
### Complexity analysis
|
||||
### Complexity Analysis
|
||||
|
||||
- **Time complexity is $O(n)$**: All nodes are visited once, using $O(n)$ time.
|
||||
- **Space complexity is $O(n)$**: In the worst case, i.e., the tree degenerates into a linked list, the recursion depth reaches $n$, and the system occupies $O(n)$ stack frame space.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Summary
|
||||
|
||||
### Key review
|
||||
### Key Review
|
||||
|
||||
- A binary tree is a non-linear data structure that embodies the divide-and-conquer logic of "one divides into two". Each binary tree node contains a value and two pointers, which respectively point to its left and right child nodes.
|
||||
- For a certain node in a binary tree, the tree formed by its left (right) child node and all nodes below is called the left (right) subtree of that node.
|
||||
|
||||
Reference in New Issue
Block a user