This commit is contained in:
krahets 2024-05-06 14:40:36 +08:00
parent 7e7eb6047a
commit 5c7d2c7f17
54 changed files with 3456 additions and 215 deletions

View File

@ -1874,6 +1874,6 @@ comments: true
## 8.1.3   堆的常见应用
- **优先队列**:堆通常作为实现优先队列的首选数据结构,其入队和出队操作的时间复杂度均为 $O(\log n)$ ,而建操作为 $O(n)$ ,这些操作都非常高效。
- **优先队列**:堆通常作为实现优先队列的首选数据结构,其入队和出队操作的时间复杂度均为 $O(\log n)$ ,而建操作为 $O(n)$ ,这些操作都非常高效。
- **堆排序**:给定一组数据,我们可以用它们建立一个堆,然后不断地执行元素出堆操作,从而得到有序数据。然而,我们通常会使用一种更优雅的方式实现堆排序,详见“堆排序”章节。
- **获取最大的 $k$ 个元素**:这是一个经典的算法问题,同时也是一种典型应用,例如选择热度前 10 的新闻作为微博热搜,选取销量前 10 的商品等。

View File

@ -133,7 +133,7 @@ Elements in an array are stored in contiguous memory spaces, making it simpler t
<p align="center"> Figure 4-2 &nbsp; Memory address calculation for array elements </p>
As observed in the above illustration, array indexing conventionally begins at $0$. While this might appear counterintuitive, considering counting usually starts at $1$, within the address calculation formula, **an index is essentially an offset from the memory address**. For the first element's address, this offset is $0$, validating its index as $0$.
As observed in Figure 4-2, array indexing conventionally begins at $0$. While this might appear counterintuitive, considering counting usually starts at $1$, within the address calculation formula, **an index is essentially an offset from the memory address**. For the first element's address, this offset is $0$, validating its index as $0$.
Accessing elements in an array is highly efficient, allowing us to randomly access any element in $O(1)$ time.
@ -152,7 +152,14 @@ Accessing elements in an array is highly efficient, allowing us to randomly acce
=== "C++"
```cpp title="array.cpp"
[class]{}-[func]{randomAccess}
/* Random access to elements */
int randomAccess(int *nums, int size) {
// Randomly select a number in the range [0, size)
int randomIndex = rand() % size;
// Retrieve and return a random element
int randomNum = nums[randomIndex];
return randomNum;
}
```
=== "Java"
@ -236,7 +243,7 @@ Accessing elements in an array is highly efficient, allowing us to randomly acce
### 3. &nbsp; Inserting elements
Array elements are tightly packed in memory, with no space available to accommodate additional data between them. Illustrated in Figure below, inserting an element in the middle of an array requires shifting all subsequent elements back by one position to create room for the new element.
Array elements are tightly packed in memory, with no space available to accommodate additional data between them. As illustrated in Figure 4-3, inserting an element in the middle of an array requires shifting all subsequent elements back by one position to create room for the new element.
![Array element insertion example](array.assets/array_insert_element.png){ class="animation-figure" }
@ -259,7 +266,15 @@ It's important to note that due to the fixed length of an array, inserting an el
=== "C++"
```cpp title="array.cpp"
[class]{}-[func]{insert}
/* Insert element num at `index` */
void insert(int *nums, int size, int num, int index) {
// Move all elements after `index` one position backward
for (int i = size - 1; i > index; i--) {
nums[i] = nums[i - 1];
}
// Assign num to the element at index
nums[index] = num;
}
```
=== "Java"
@ -365,7 +380,13 @@ Please note that after deletion, the former last element becomes "meaningless,"
=== "C++"
```cpp title="array.cpp"
[class]{}-[func]{remove}
/* Remove the element at `index` */
void remove(int *nums, int size, int index) {
// Move all elements after `index` one position forward
for (int i = index; i < size - 1; i++) {
nums[i] = nums[i + 1];
}
}
```
=== "Java"
@ -477,7 +498,14 @@ In most programming languages, we can traverse an array either by using indices
=== "C++"
```cpp title="array.cpp"
[class]{}-[func]{traverse}
/* Traverse array */
void traverse(int *nums, int size) {
int count = 0;
// Traverse array by index
for (int i = 0; i < size; i++) {
count += nums[i];
}
}
```
=== "Java"
@ -583,7 +611,14 @@ Because arrays are linear data structures, this operation is commonly referred t
=== "C++"
```cpp title="array.cpp"
[class]{}-[func]{find}
/* Search for a specified element in the array */
int find(int *nums, int size, int target) {
for (int i = 0; i < size; i++) {
if (nums[i] == target)
return i;
}
return -1;
}
```
=== "Java"
@ -688,7 +723,19 @@ To expand an array, it's necessary to create a larger array and then copy the e
=== "C++"
```cpp title="array.cpp"
[class]{}-[func]{extend}
/* Extend array length */
int *extend(int *nums, int size, int enlarge) {
// Initialize an extended length array
int *res = new int[size + enlarge];
// Copy all elements from the original array to the new array
for (int i = 0; i < size; i++) {
res[i] = nums[i];
}
// Free memory
delete[] nums;
// Return the new array after expansion
return res;
}
```
=== "Java"

View File

@ -14,7 +14,7 @@ The design of linked lists allows for their nodes to be distributed across memor
<p align="center"> Figure 4-5 &nbsp; Linked list definition and storage method </p>
As shown in the figure, we see that the basic building block of a linked list is the <u>node</u> object. Each node comprises two key components: the node's "value" and a "reference" to the next node.
As shown in Figure 4-5, we see that the basic building block of a linked list is the <u>node</u> object. Each node comprises two key components: the node's "value" and a "reference" to the next node.
- The first node in a linked list is the "head node", and the final one is the "tail node".
- The tail node points to "null", designated as `null` in Java, `nullptr` in C++, and `None` in Python.
@ -412,7 +412,7 @@ The array as a whole is a variable, for instance, the array `nums` includes elem
### 2. &nbsp; Inserting nodes
Inserting a node into a linked list is very easy. As shown in the figure, let's assume we aim to insert a new node `P` between two adjacent nodes `n0` and `n1`. **This can be achieved by simply modifying two node references (pointers)**, with a time complexity of $O(1)$.
Inserting a node into a linked list is very easy. As shown in Figure 4-6, let's assume we aim to insert a new node `P` between two adjacent nodes `n0` and `n1`. **This can be achieved by simply modifying two node references (pointers)**, with a time complexity of $O(1)$.
By comparison, inserting an element into an array has a time complexity of $O(n)$, which becomes less efficient when dealing with large data volumes.
@ -433,7 +433,12 @@ By comparison, inserting an element into an array has a time complexity of $O(n)
=== "C++"
```cpp title="linked_list.cpp"
[class]{}-[func]{insert}
/* Insert node P after node n0 in the linked list */
void insert(ListNode *n0, ListNode *P) {
ListNode *n1 = n0->next;
P->next = n1;
n0->next = P;
}
```
=== "Java"
@ -515,7 +520,7 @@ By comparison, inserting an element into an array has a time complexity of $O(n)
### 3. &nbsp; Deleting nodes
As shown in the figure, deleting a node from a linked list is also very easy, **involving only the modification of a single node's reference (pointer)**.
As shown in Figure 4-7, deleting a node from a linked list is also very easy, **involving only the modification of a single node's reference (pointer)**.
It's important to note that even though node `P` continues to point to `n1` after being deleted, it becomes inaccessible during linked list traversal. This effectively means that `P` is no longer a part of the linked list.
@ -539,7 +544,17 @@ It's important to note that even though node `P` continues to point to `n1` afte
=== "C++"
```cpp title="linked_list.cpp"
[class]{}-[func]{remove}
/* Remove the first node after node n0 in the linked list */
void remove(ListNode *n0) {
if (n0->next == nullptr)
return;
// n0 -> P -> n1
ListNode *P = n0->next;
ListNode *n1 = P->next;
n0->next = n1;
// Free memory
delete P;
}
```
=== "Java"
@ -641,7 +656,15 @@ It's important to note that even though node `P` continues to point to `n1` afte
=== "C++"
```cpp title="linked_list.cpp"
[class]{}-[func]{access}
/* Access the node at `index` in the linked list */
ListNode *access(ListNode *head, int index) {
for (int i = 0; i < index; i++) {
if (head == nullptr)
return nullptr;
head = head->next;
}
return head;
}
```
=== "Java"
@ -745,7 +768,17 @@ Traverse the linked list to locate a node whose value matches `target`, and then
=== "C++"
```cpp title="linked_list.cpp"
[class]{}-[func]{find}
/* Search for the first node with value target in the linked list */
int find(ListNode *head, int target) {
int index = 0;
while (head != nullptr) {
if (head->val == target)
return index;
head = head->next;
index++;
}
return -1;
}
```
=== "Java"
@ -851,7 +884,7 @@ Table 4-1 summarizes the characteristics of arrays and linked lists, and it also
## 4.2.3 &nbsp; Common types of linked lists
As shown in the figure, there are three common types of linked lists.
As shown in Figure 4-8, there are three common types of linked lists.
- **Singly linked list**: This is the standard linked list described earlier. Nodes in a singly linked list include a value and a reference to the next node. The first node is known as the head node, and the last node, which points to null (`None`), is the tail node.
- **Circular linked list**: This is formed when the tail node of a singly linked list points back to the head node, creating a loop. In a circular linked list, any node can function as the head node.

View File

@ -989,7 +989,116 @@ To enhance our understanding of how lists work, we will attempt to implement a s
=== "C++"
```cpp title="my_list.cpp"
[class]{MyList}-[func]{}
/* List class */
class MyList {
private:
int *arr; // Array (stores list elements)
int arrCapacity = 10; // List capacity
int arrSize = 0; // List length (current number of elements)
int extendRatio = 2; // Multiple for each list expansion
public:
/* Constructor */
MyList() {
arr = new int[arrCapacity];
}
/* Destructor */
~MyList() {
delete[] arr;
}
/* Get list length (current number of elements)*/
int size() {
return arrSize;
}
/* Get list capacity */
int capacity() {
return arrCapacity;
}
/* Access element */
int get(int index) {
// If the index is out of bounds, throw an exception, as below
if (index < 0 || index >= size())
throw out_of_range("Index out of bounds");
return arr[index];
}
/* Update element */
void set(int index, int num) {
if (index < 0 || index >= size())
throw out_of_range("Index out of bounds");
arr[index] = num;
}
/* Add element at the end */
void add(int num) {
// When the number of elements exceeds capacity, trigger the expansion mechanism
if (size() == capacity())
extendCapacity();
arr[size()] = num;
// Update the number of elements
arrSize++;
}
/* Insert element in the middle */
void insert(int index, int num) {
if (index < 0 || index >= size())
throw out_of_range("Index out of bounds");
// When the number of elements exceeds capacity, trigger the expansion mechanism
if (size() == capacity())
extendCapacity();
// Move all elements after `index` one position backward
for (int j = size() - 1; j >= index; j--) {
arr[j + 1] = arr[j];
}
arr[index] = num;
// Update the number of elements
arrSize++;
}
/* Remove element */
int remove(int index) {
if (index < 0 || index >= size())
throw out_of_range("Index out of bounds");
int num = arr[index];
// Move all elements after `index` one position forward
for (int j = index; j < size() - 1; j++) {
arr[j] = arr[j + 1];
}
// Update the number of elements
arrSize--;
// Return the removed element
return num;
}
/* Extend list */
void extendCapacity() {
// Create a new array with a length multiple of the original array by extendRatio
int newCapacity = capacity() * extendRatio;
int *tmp = arr;
arr = new int[newCapacity];
// Copy all elements from the original array to the new array
for (int i = 0; i < size(); i++) {
arr[i] = tmp[i];
}
// Free memory
delete[] tmp;
arrCapacity = newCapacity;
}
/* Convert the list to a Vector for printing */
vector<int> toVector() {
// Only convert elements within valid length range
vector<int> vec(size());
for (int i = 0; i < size(); i++) {
vec[i] = arr[i];
}
return vec;
}
};
```
=== "Java"

View File

@ -48,7 +48,7 @@ If an element is searched first and then deleted, the time complexity is indeed
**Q**: In the figure "Linked List Definition and Storage Method", do the light blue storage nodes occupy a single memory address, or do they share half with the node value?
The diagram is just a qualitative representation; quantitative analysis depends on specific situations.
The figure is just a qualitative representation; quantitative analysis depends on specific situations.
- Different types of node values occupy different amounts of space, such as int, long, double, and object instances.
- The memory space occupied by pointer variables depends on the operating system and compilation environment used, usually 8 bytes or 4 bytes.

View File

@ -12,7 +12,7 @@ Backtracking typically employs "depth-first search" to traverse the solution spa
Given a binary tree, search and record all nodes with a value of $7$, please return a list of nodes.
For this problem, we traverse this tree in preorder and check if the current node's value is $7$. If it is, we add the node's value to the result list `res`. The relevant process is shown in the following diagram and code:
For this problem, we traverse this tree in preorder and check if the current node's value is $7$. If it is, we add the node's value to the result list `res`. The relevant process is shown in Figure 13-1:
=== "Python"
@ -31,7 +31,18 @@ For this problem, we traverse this tree in preorder and check if the current nod
=== "C++"
```cpp title="preorder_traversal_i_compact.cpp"
[class]{}-[func]{preOrder}
/* Pre-order traversal: Example one */
void preOrder(TreeNode *root) {
if (root == nullptr) {
return;
}
if (root->val == 7) {
// Record solution
res.push_back(root);
}
preOrder(root->left);
preOrder(root->right);
}
```
=== "Java"
@ -156,7 +167,22 @@ Based on the code from Example One, we need to use a list `path` to record the v
=== "C++"
```cpp title="preorder_traversal_ii_compact.cpp"
[class]{}-[func]{preOrder}
/* Pre-order traversal: Example two */
void preOrder(TreeNode *root) {
if (root == nullptr) {
return;
}
// Attempt
path.push_back(root);
if (root->val == 7) {
// Record solution
res.push_back(path);
}
preOrder(root->left);
preOrder(root->right);
// Retract
path.pop_back();
}
```
=== "Java"
@ -317,7 +343,23 @@ To meet the above constraints, **we need to add a pruning operation**: during th
=== "C++"
```cpp title="preorder_traversal_iii_compact.cpp"
[class]{}-[func]{preOrder}
/* Pre-order traversal: Example three */
void preOrder(TreeNode *root) {
// Pruning
if (root == nullptr || root->val == 3) {
return;
}
// Attempt
path.push_back(root);
if (root->val == 7) {
// Record solution
res.push_back(path);
}
preOrder(root->left);
preOrder(root->right);
// Retract
path.pop_back();
}
```
=== "Java"
@ -408,7 +450,7 @@ To meet the above constraints, **we need to add a pruning operation**: during th
[class]{}-[func]{preOrder}
```
"Pruning" is a very vivid noun. As shown in the diagram below, in the search process, **we "cut off" the search branches that do not meet the constraints**, avoiding many meaningless attempts, thus enhancing the search efficiency.
"Pruning" is a very vivid noun. As shown in Figure 13-3, in the search process, **we "cut off" the search branches that do not meet the constraints**, avoiding many meaningless attempts, thus enhancing the search efficiency.
![Pruning based on constraints](backtracking_algorithm.assets/preorder_find_constrained_paths.png){ class="animation-figure" }
@ -788,17 +830,52 @@ Next, we solve Example Three based on the framework code. The `state` is the nod
=== "C++"
```cpp title="preorder_traversal_iii_template.cpp"
[class]{}-[func]{isSolution}
/* Determine if the current state is a solution */
bool isSolution(vector<TreeNode *> &state) {
return !state.empty() && state.back()->val == 7;
}
[class]{}-[func]{recordSolution}
/* Record solution */
void recordSolution(vector<TreeNode *> &state, vector<vector<TreeNode *>> &res) {
res.push_back(state);
}
[class]{}-[func]{isValid}
/* Determine if the choice is legal under the current state */
bool isValid(vector<TreeNode *> &state, TreeNode *choice) {
return choice != nullptr && choice->val != 3;
}
[class]{}-[func]{makeChoice}
/* Update state */
void makeChoice(vector<TreeNode *> &state, TreeNode *choice) {
state.push_back(choice);
}
[class]{}-[func]{undoChoice}
/* Restore state */
void undoChoice(vector<TreeNode *> &state, TreeNode *choice) {
state.pop_back();
}
[class]{}-[func]{backtrack}
/* Backtracking algorithm: Example three */
void backtrack(vector<TreeNode *> &state, vector<TreeNode *> &choices, vector<vector<TreeNode *>> &res) {
// Check if it's a solution
if (isSolution(state)) {
// Record solution
recordSolution(state, res);
}
// Traverse all choices
for (TreeNode *choice : choices) {
// Pruning: check if the choice is legal
if (isValid(state, choice)) {
// Attempt: make a choice, update the state
makeChoice(state, choice);
// Proceed to the next round of selection
vector<TreeNode *> nextChoices{choice->left, choice->right};
backtrack(state, nextChoices, res);
// Retract: undo the choice, restore to the previous state
undoChoice(state, choice);
}
}
}
```
=== "Java"
@ -1027,7 +1104,7 @@ Next, we solve Example Three based on the framework code. The `state` is the nod
[class]{}-[func]{backtrack}
```
As per the requirements, after finding a node with a value of $7$, the search should continue, **thus the `return` statement after recording the solution should be removed**. The following diagram compares the search processes with and without retaining the `return` statement.
As per the requirements, after finding a node with a value of $7$, the search should continue, **thus the `return` statement after recording the solution should be removed**. Figure 13-4 compares the search processes with and without retaining the `return` statement.
![Comparison of retaining and removing the return in the search process](backtracking_algorithm.assets/backtrack_remove_return_or_not.png){ class="animation-figure" }

View File

@ -101,9 +101,46 @@ Please note, in an $n$-dimensional matrix, the range of $row - col$ is $[-n + 1,
=== "C++"
```cpp title="n_queens.cpp"
[class]{}-[func]{backtrack}
/* Backtracking algorithm: n queens */
void backtrack(int row, int n, vector<vector<string>> &state, vector<vector<vector<string>>> &res, vector<bool> &cols,
vector<bool> &diags1, vector<bool> &diags2) {
// When all rows are placed, record the solution
if (row == n) {
res.push_back(state);
return;
}
// Traverse all columns
for (int col = 0; col < n; col++) {
// Calculate the main and minor diagonals corresponding to the cell
int diag1 = row - col + n - 1;
int diag2 = row + col;
// Pruning: do not allow queens on the column, main diagonal, or minor diagonal of the cell
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
// Attempt: place the queen in the cell
state[row][col] = "Q";
cols[col] = diags1[diag1] = diags2[diag2] = true;
// Place the next row
backtrack(row + 1, n, state, res, cols, diags1, diags2);
// Retract: restore the cell to an empty spot
state[row][col] = "#";
cols[col] = diags1[diag1] = diags2[diag2] = false;
}
}
}
[class]{}-[func]{nQueens}
/* Solve n queens */
vector<vector<vector<string>>> nQueens(int n) {
// Initialize an n*n size chessboard, where 'Q' represents the queen and '#' represents an empty spot
vector<vector<string>> state(n, vector<string>(n, "#"));
vector<bool> cols(n, false); // Record columns with queens
vector<bool> diags1(2 * n - 1, false); // Record main diagonals with queens
vector<bool> diags2(2 * n - 1, false); // Record minor diagonals with queens
vector<vector<vector<string>>> res;
backtrack(0, n, state, res, cols, diags1, diags2);
return res;
}
```
=== "Java"

View File

@ -89,9 +89,38 @@ After understanding the above information, we can "fill in the blanks" in the fr
=== "C++"
```cpp title="permutations_i.cpp"
[class]{}-[func]{backtrack}
/* Backtracking algorithm: Permutation I */
void backtrack(vector<int> &state, const vector<int> &choices, vector<bool> &selected, vector<vector<int>> &res) {
// When the state length equals the number of elements, record the solution
if (state.size() == choices.size()) {
res.push_back(state);
return;
}
// Traverse all choices
for (int i = 0; i < choices.size(); i++) {
int choice = choices[i];
// Pruning: do not allow repeated selection of elements
if (!selected[i]) {
// Attempt: make a choice, update the state
selected[i] = true;
state.push_back(choice);
// Proceed to the next round of selection
backtrack(state, choices, selected, res);
// Retract: undo the choice, restore to the previous state
selected[i] = false;
state.pop_back();
}
}
}
[class]{}-[func]{permutationsI}
/* Permutation I */
vector<vector<int>> permutationsI(vector<int> nums) {
vector<int> state;
vector<bool> selected(nums.size(), false);
vector<vector<int>> res;
backtrack(state, nums, selected, res);
return res;
}
```
=== "Java"
@ -285,9 +314,40 @@ Based on the code from the previous problem, we consider initiating a hash set `
=== "C++"
```cpp title="permutations_ii.cpp"
[class]{}-[func]{backtrack}
/* Backtracking algorithm: Permutation II */
void backtrack(vector<int> &state, const vector<int> &choices, vector<bool> &selected, vector<vector<int>> &res) {
// When the state length equals the number of elements, record the solution
if (state.size() == choices.size()) {
res.push_back(state);
return;
}
// Traverse all choices
unordered_set<int> duplicated;
for (int i = 0; i < choices.size(); i++) {
int choice = choices[i];
// Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements
if (!selected[i] && duplicated.find(choice) == duplicated.end()) {
// Attempt: make a choice, update the state
duplicated.emplace(choice); // Record selected element values
selected[i] = true;
state.push_back(choice);
// Proceed to the next round of selection
backtrack(state, choices, selected, res);
// Retract: undo the choice, restore to the previous state
selected[i] = false;
state.pop_back();
}
}
}
[class]{}-[func]{permutationsII}
/* Permutation II */
vector<vector<int>> permutationsII(vector<int> nums) {
vector<int> state;
vector<bool> selected(nums.size(), false);
vector<vector<int>> res;
backtrack(state, nums, selected, res);
return res;
}
```
=== "Java"

View File

@ -60,9 +60,36 @@ Unlike the permutation problem, **elements in this problem can be chosen an unli
=== "C++"
```cpp title="subset_sum_i_naive.cpp"
[class]{}-[func]{backtrack}
/* Backtracking algorithm: Subset Sum I */
void backtrack(vector<int> &state, int target, int total, vector<int> &choices, vector<vector<int>> &res) {
// When the subset sum equals target, record the solution
if (total == target) {
res.push_back(state);
return;
}
// Traverse all choices
for (size_t i = 0; i < choices.size(); i++) {
// Pruning: if the subset sum exceeds target, skip that choice
if (total + choices[i] > target) {
continue;
}
// Attempt: make a choice, update elements and total
state.push_back(choices[i]);
// Proceed to the next round of selection
backtrack(state, target, total + choices[i], choices, res);
// Retract: undo the choice, restore to the previous state
state.pop_back();
}
}
[class]{}-[func]{subsetSumINaive}
/* Solve Subset Sum I (including duplicate subsets) */
vector<vector<int>> subsetSumINaive(vector<int> &nums, int target) {
vector<int> state; // State (subset)
int total = 0; // Subset sum
vector<vector<int>> res; // Result list (subset list)
backtrack(state, target, total, nums, res);
return res;
}
```
=== "Java"
@ -267,9 +294,39 @@ Besides, we have made the following two optimizations to the code.
=== "C++"
```cpp title="subset_sum_i.cpp"
[class]{}-[func]{backtrack}
/* Backtracking algorithm: Subset Sum I */
void backtrack(vector<int> &state, int target, vector<int> &choices, int start, vector<vector<int>> &res) {
// When the subset sum equals target, record the solution
if (target == 0) {
res.push_back(state);
return;
}
// Traverse all choices
// Pruning two: start traversing from start to avoid generating duplicate subsets
for (int i = start; i < choices.size(); i++) {
// Pruning one: if the subset sum exceeds target, end the loop immediately
// This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target
if (target - choices[i] < 0) {
break;
}
// Attempt: make a choice, update target, start
state.push_back(choices[i]);
// Proceed to the next round of selection
backtrack(state, target - choices[i], choices, i, res);
// Retract: undo the choice, restore to the previous state
state.pop_back();
}
}
[class]{}-[func]{subsetSumI}
/* Solve Subset Sum I */
vector<vector<int>> subsetSumI(vector<int> &nums, int target) {
vector<int> state; // State (subset)
sort(nums.begin(), nums.end()); // Sort nums
int start = 0; // Start point for traversal
vector<vector<int>> res; // Result list (subset list)
backtrack(state, target, nums, start, res);
return res;
}
```
=== "Java"
@ -468,9 +525,44 @@ At the same time, **this question stipulates that each array element can only be
=== "C++"
```cpp title="subset_sum_ii.cpp"
[class]{}-[func]{backtrack}
/* Backtracking algorithm: Subset Sum II */
void backtrack(vector<int> &state, int target, vector<int> &choices, int start, vector<vector<int>> &res) {
// When the subset sum equals target, record the solution
if (target == 0) {
res.push_back(state);
return;
}
// Traverse all choices
// Pruning two: start traversing from start to avoid generating duplicate subsets
// Pruning three: start traversing from start to avoid repeatedly selecting the same element
for (int i = start; i < choices.size(); i++) {
// Pruning one: if the subset sum exceeds target, end the loop immediately
// This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target
if (target - choices[i] < 0) {
break;
}
// Pruning four: if the element equals the left element, it indicates that the search branch is repeated, skip it
if (i > start && choices[i] == choices[i - 1]) {
continue;
}
// Attempt: make a choice, update target, start
state.push_back(choices[i]);
// Proceed to the next round of selection
backtrack(state, target - choices[i], choices, i + 1, res);
// Retract: undo the choice, restore to the previous state
state.pop_back();
}
}
[class]{}-[func]{subsetSumII}
/* Solve Subset Sum II */
vector<vector<int>> subsetSumII(vector<int> &nums, int target) {
vector<int> state; // State (subset)
sort(nums.begin(), nums.end()); // Sort nums
int start = 0; // Start point for traversal
vector<vector<int>> res; // Result list (subset list)
backtrack(state, target, nums, start, res);
return res;
}
```
=== "Java"

View File

@ -31,7 +31,15 @@ The following function uses a `for` loop to perform a summation of $1 + 2 + \dot
=== "C++"
```cpp title="iteration.cpp"
[class]{}-[func]{forLoop}
/* for loop */
int forLoop(int n) {
int res = 0;
// Loop sum 1, 2, ..., n-1, n
for (int i = 1; i <= n; ++i) {
res += i;
}
return res;
}
```
=== "Java"
@ -114,7 +122,7 @@ The following function uses a `for` loop to perform a summation of $1 + 2 + \dot
[class]{}-[func]{forLoop}
```
The flowchart below represents this sum function.
Figure 2-1 represents this sum function.
![Flowchart of the sum function](iteration_and_recursion.assets/iteration.png){ class="animation-figure" }
@ -145,7 +153,17 @@ Below we use a `while` loop to implement the sum $1 + 2 + \dots + n$.
=== "C++"
```cpp title="iteration.cpp"
[class]{}-[func]{whileLoop}
/* while loop */
int whileLoop(int n) {
int res = 0;
int i = 1; // Initialize condition variable
// Loop sum 1, 2, ..., n-1, n
while (i <= n) {
res += i;
i++; // Update condition variable
}
return res;
}
```
=== "Java"
@ -253,7 +271,19 @@ For example, in the following code, the condition variable $i$ is updated twice
=== "C++"
```cpp title="iteration.cpp"
[class]{}-[func]{whileLoopII}
/* while loop (two updates) */
int whileLoopII(int n) {
int res = 0;
int i = 1; // Initialize condition variable
// Loop sum 1, 4, 10, ...
while (i <= n) {
res += i;
// Update condition variable
i++;
i *= 2;
}
return res;
}
```
=== "Java"
@ -363,7 +393,18 @@ We can nest one loop structure within another. Below is an example using `for` l
=== "C++"
```cpp title="iteration.cpp"
[class]{}-[func]{nestedForLoop}
/* Double for loop */
string nestedForLoop(int n) {
ostringstream res;
// Loop i = 1, 2, ..., n-1, n
for (int i = 1; i <= n; ++i) {
// Loop j = 1, 2, ..., n-1, n
for (int j = 1; j <= n; ++j) {
res << "(" << i << ", " << j << "), ";
}
}
return res.str();
}
```
=== "Java"
@ -449,7 +490,7 @@ We can nest one loop structure within another. Below is an example using `for` l
[class]{}-[func]{nestedForLoop}
```
The flowchart below represents this nested loop.
Figure 2-2 represents this nested loop.
![Flowchart of the nested loop](iteration_and_recursion.assets/nested_iteration.png){ class="animation-figure" }
@ -491,7 +532,16 @@ Observe the following code, where simply calling the function `recur(n)` can com
=== "C++"
```cpp title="recursion.cpp"
[class]{}-[func]{recur}
/* Recursion */
int recur(int n) {
// Termination condition
if (n == 1)
return 1;
// Recursive: recursive call
int res = recur(n - 1);
// Return: return result
return n + res;
}
```
=== "Java"
@ -630,7 +680,14 @@ For example, in calculating $1 + 2 + \dots + n$, we can make the result variable
=== "C++"
```cpp title="recursion.cpp"
[class]{}-[func]{tailRecur}
/* Tail recursion */
int tailRecur(int n, int res) {
// Termination condition
if (n == 0)
return res;
// Tail recursive call
return tailRecur(n - 1, res + n);
}
```
=== "Java"
@ -757,7 +814,16 @@ Using the recursive relation, and considering the first two numbers as terminati
=== "C++"
```cpp title="recursion.cpp"
[class]{}-[func]{fib}
/* Fibonacci sequence: Recursion */
int fib(int n) {
// Termination condition f(1) = 0, f(2) = 1
if (n == 1 || n == 2)
return n - 1;
// Recursive call f(n) = f(n-1) + f(n-2)
int res = fib(n - 1) + fib(n - 2);
// Return result f(n)
return res;
}
```
=== "Java"
@ -841,7 +907,7 @@ Using the recursive relation, and considering the first two numbers as terminati
[class]{}-[func]{fib}
```
Observing the above code, we see that it recursively calls two functions within itself, **meaning that one call generates two branching calls**. As illustrated below, this continuous recursive calling eventually creates a <u>recursion tree</u> with a depth of $n$.
Observing the above code, we see that it recursively calls two functions within itself, **meaning that one call generates two branching calls**. As illustrated in Figure 2-6, this continuous recursive calling eventually creates a <u>recursion tree</u> with a depth of $n$.
![Fibonacci sequence recursion tree](iteration_and_recursion.assets/recursion_tree.png){ class="animation-figure" }
@ -905,7 +971,25 @@ Therefore, **we can use an explicit stack to simulate the behavior of the call s
=== "C++"
```cpp title="recursion.cpp"
[class]{}-[func]{forLoopRecur}
/* Simulate recursion with iteration */
int forLoopRecur(int n) {
// Use an explicit stack to simulate the system call stack
stack<int> stack;
int res = 0;
// Recursive: recursive call
for (int i = n; i > 0; i--) {
// Simulate "recursive" by "pushing onto the stack"
stack.push(i);
}
// Return: return result
while (!stack.empty()) {
// Simulate "return" by "popping from the stack"
res += stack.top();
stack.pop();
}
// res = 1+2+3+...+n
return res;
}
```
=== "Java"

View File

@ -731,7 +731,7 @@ The time complexity of both `loop()` and `recur()` functions is $O(n)$, but thei
## 2.4.3 &nbsp; Common types
Let the size of the input data be $n$, the following chart displays common types of space complexities (arranged from low to high).
Let the size of the input data be $n$, Figure 2-16 displays common types of space complexities (arranged from low to high).
$$
\begin{aligned}
@ -775,9 +775,28 @@ Note that memory occupied by initializing variables or calling functions in a lo
=== "C++"
```cpp title="space_complexity.cpp"
[class]{}-[func]{func}
/* Function */
int func() {
// Perform some operations
return 0;
}
[class]{}-[func]{constant}
/* Constant complexity */
void constant(int n) {
// Constants, variables, objects occupy O(1) space
const int a = 0;
int b = 0;
vector<int> nums(10000);
ListNode node(0);
// Variables in a loop occupy O(1) space
for (int i = 0; i < n; i++) {
int c = 0;
}
// Functions in a loop occupy O(1) space
for (int i = 0; i < n; i++) {
func();
}
}
```
=== "Java"
@ -915,7 +934,21 @@ Linear order is common in arrays, linked lists, stacks, queues, etc., where the
=== "C++"
```cpp title="space_complexity.cpp"
[class]{}-[func]{linear}
/* Linear complexity */
void linear(int n) {
// Array of length n occupies O(n) space
vector<int> nums(n);
// A list of length n occupies O(n) space
vector<ListNode> nodes;
for (int i = 0; i < n; i++) {
nodes.push_back(ListNode(i));
}
// A hash table of length n occupies O(n) space
unordered_map<int, string> map;
for (int i = 0; i < n; i++) {
map[i] = to_string(i);
}
}
```
=== "Java"
@ -1022,7 +1055,13 @@ As shown in Figure 2-17, this function's recursive depth is $n$, meaning there a
=== "C++"
```cpp title="space_complexity.cpp"
[class]{}-[func]{linearRecur}
/* Linear complexity (recursive implementation) */
void linearRecur(int n) {
cout << "Recursion n = " << n << endl;
if (n == 1)
return;
linearRecur(n - 1);
}
```
=== "Java"
@ -1123,7 +1162,18 @@ Quadratic order is common in matrices and graphs, where the number of elements i
=== "C++"
```cpp title="space_complexity.cpp"
[class]{}-[func]{quadratic}
/* Quadratic complexity */
void quadratic(int n) {
// A two-dimensional list occupies O(n^2) space
vector<vector<int>> numMatrix;
for (int i = 0; i < n; i++) {
vector<int> tmp;
for (int j = 0; j < n; j++) {
tmp.push_back(0);
}
numMatrix.push_back(tmp);
}
}
```
=== "Java"
@ -1228,7 +1278,14 @@ As shown in Figure 2-18, the recursive depth of this function is $n$, and in eac
=== "C++"
```cpp title="space_complexity.cpp"
[class]{}-[func]{quadraticRecur}
/* Quadratic complexity (recursive implementation) */
int quadraticRecur(int n) {
if (n <= 0)
return 0;
vector<int> nums(n);
cout << "Recursive n = " << n << ", length of nums = " << nums.size() << endl;
return quadraticRecur(n - 1);
}
```
=== "Java"
@ -1335,7 +1392,15 @@ Exponential order is common in binary trees. Observe Figure 2-19, a "full binary
=== "C++"
```cpp title="space_complexity.cpp"
[class]{}-[func]{buildTree}
/* Exponential complexity (building a full binary tree) */
TreeNode *buildTree(int n) {
if (n == 0)
return nullptr;
TreeNode *root = new TreeNode(0);
root->left = buildTree(n - 1);
root->right = buildTree(n - 1);
return root;
}
```
=== "Java"

View File

@ -675,7 +675,7 @@ In essence, time complexity analysis is about finding the asymptotic upper bound
If there exist positive real numbers $c$ and $n_0$ such that for all $n > n_0$, $T(n) \leq c \cdot f(n)$, then $f(n)$ is considered an asymptotic upper bound of $T(n)$, denoted as $T(n) = O(f(n))$.
As illustrated below, calculating the asymptotic upper bound involves finding a function $f(n)$ such that, as $n$ approaches infinity, $T(n)$ and $f(n)$ have the same growth order, differing only by a constant factor $c$.
As shown in Figure 2-8, calculating the asymptotic upper bound involves finding a function $f(n)$ such that, as $n$ approaches infinity, $T(n)$ and $f(n)$ have the same growth order, differing only by a constant factor $c$.
![Asymptotic upper bound of a function](time_complexity.assets/asymptotic_upper_bound.png){ class="animation-figure" }
@ -963,7 +963,7 @@ The following table illustrates examples of different operation counts and their
## 2.3.4 &nbsp; Common types of time complexity
Let's consider the input data size as $n$. The common types of time complexities are illustrated below, arranged from lowest to highest:
Let's consider the input data size as $n$. The common types of time complexities are shown in Figure 2-9, arranged from lowest to highest:
$$
\begin{aligned}
@ -995,7 +995,14 @@ Constant order means the number of operations is independent of the input data s
=== "C++"
```cpp title="time_complexity.cpp"
[class]{}-[func]{constant}
/* Constant complexity */
int constant(int n) {
int count = 0;
int size = 100000;
for (int i = 0; i < size; i++)
count++;
return count;
}
```
=== "Java"
@ -1095,7 +1102,13 @@ Linear order indicates the number of operations grows linearly with the input da
=== "C++"
```cpp title="time_complexity.cpp"
[class]{}-[func]{linear}
/* Linear complexity */
int linear(int n) {
int count = 0;
for (int i = 0; i < n; i++)
count++;
return count;
}
```
=== "Java"
@ -1193,7 +1206,15 @@ Operations like array traversal and linked list traversal have a time complexity
=== "C++"
```cpp title="time_complexity.cpp"
[class]{}-[func]{arrayTraversal}
/* Linear complexity (traversing an array) */
int arrayTraversal(vector<int> &nums) {
int count = 0;
// Loop count is proportional to the length of the array
for (int num : nums) {
count++;
}
return count;
}
```
=== "Java"
@ -1298,7 +1319,17 @@ Quadratic order means the number of operations grows quadratically with the inpu
=== "C++"
```cpp title="time_complexity.cpp"
[class]{}-[func]{quadratic}
/* Quadratic complexity */
int quadratic(int n) {
int count = 0;
// Loop count is squared in relation to the data size n
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
count++;
}
}
return count;
}
```
=== "Java"
@ -1413,7 +1444,24 @@ For instance, in bubble sort, the outer loop runs $n - 1$ times, and the inner l
=== "C++"
```cpp title="time_complexity.cpp"
[class]{}-[func]{bubbleSort}
/* Quadratic complexity (bubble sort) */
int bubbleSort(vector<int> &nums) {
int count = 0; // Counter
// Outer loop: unsorted range is [0, i]
for (int i = nums.size() - 1; i > 0; i--) {
// Inner loop: swap the largest element in the unsorted range [0, i] to the right end of the range
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// Swap nums[j] and nums[j + 1]
int tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
count += 3; // Element swap includes 3 individual operations
}
}
}
return count;
}
```
=== "Java"
@ -1530,7 +1578,19 @@ Figure 2-11 and code simulate the cell division process, with a time complexity
=== "C++"
```cpp title="time_complexity.cpp"
[class]{}-[func]{exponential}
/* Exponential complexity (loop implementation) */
int exponential(int n) {
int count = 0, base = 1;
// Cells split into two every round, forming the sequence 1, 2, 4, 8, ..., 2^(n-1)
for (int i = 0; i < n; i++) {
for (int j = 0; j < base; j++) {
count++;
}
base *= 2;
}
// count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1
return count;
}
```
=== "Java"
@ -1636,7 +1696,12 @@ In practice, exponential order often appears in recursive functions. For example
=== "C++"
```cpp title="time_complexity.cpp"
[class]{}-[func]{expRecur}
/* Exponential complexity (recursive implementation) */
int expRecur(int n) {
if (n == 1)
return 1;
return expRecur(n - 1) + expRecur(n - 1) + 1;
}
```
=== "Java"
@ -1739,7 +1804,15 @@ Figure 2-12 and code simulate the "halving each round" process, with a time comp
=== "C++"
```cpp title="time_complexity.cpp"
[class]{}-[func]{logarithmic}
/* Logarithmic complexity (loop implementation) */
int logarithmic(int n) {
int count = 0;
while (n > 1) {
n = n / 2;
count++;
}
return count;
}
```
=== "Java"
@ -1841,7 +1914,12 @@ Like exponential order, logarithmic order also frequently appears in recursive f
=== "C++"
```cpp title="time_complexity.cpp"
[class]{}-[func]{logRecur}
/* Logarithmic complexity (recursive implementation) */
int logRecur(int n) {
if (n <= 1)
return 0;
return logRecur(n / 2) + 1;
}
```
=== "Java"
@ -1953,7 +2031,16 @@ Linear-logarithmic order often appears in nested loops, with the complexities of
=== "C++"
```cpp title="time_complexity.cpp"
[class]{}-[func]{linearLogRecur}
/* Linear logarithmic complexity */
int linearLogRecur(int n) {
if (n <= 1)
return 1;
int count = linearLogRecur(n / 2) + linearLogRecur(n / 2);
for (int i = 0; i < n; i++) {
count++;
}
return count;
}
```
=== "Java"
@ -2072,7 +2159,17 @@ Factorials are typically implemented using recursion. As shown in the code and F
=== "C++"
```cpp title="time_complexity.cpp"
[class]{}-[func]{factorialRecur}
/* Factorial complexity (recursive implementation) */
int factorialRecur(int n) {
if (n == 0)
return 1;
int count = 0;
// From 1 split into n
for (int i = 0; i < n; i++) {
count += factorialRecur(n - 1);
}
return count;
}
```
=== "Java"
@ -2196,9 +2293,30 @@ The "worst-case time complexity" corresponds to the asymptotic upper bound, deno
=== "C++"
```cpp title="worst_best_time_complexity.cpp"
[class]{}-[func]{randomNumbers}
/* Generate an array with elements {1, 2, ..., n} in a randomly shuffled order */
vector<int> randomNumbers(int n) {
vector<int> nums(n);
// Generate array nums = { 1, 2, 3, ..., n }
for (int i = 0; i < n; i++) {
nums[i] = i + 1;
}
// Generate a random seed using system time
unsigned seed = chrono::system_clock::now().time_since_epoch().count();
// Randomly shuffle array elements
shuffle(nums.begin(), nums.end(), default_random_engine(seed));
return nums;
}
[class]{}-[func]{findOne}
/* Find the index of number 1 in array nums */
int findOne(vector<int> &nums) {
for (int i = 0; i < nums.size(); i++) {
// When element 1 is at the start of the array, achieve best time complexity O(1)
// When element 1 is at the end of the array, achieve worst time complexity O(n)
if (nums[i] == 1)
return i;
}
return -1;
}
```
=== "Java"

View File

@ -18,7 +18,7 @@ Firstly, it's important to note that **numbers are stored in computers using the
- **One's complement**: The one's complement of a positive number is the same as its sign-magnitude. For negative numbers, it's obtained by inverting all bits except the sign bit.
- **Two's complement**: The two's complement of a positive number is the same as its sign-magnitude. For negative numbers, it's obtained by adding $1$ to their one's complement.
The following diagram illustrates the conversions among sign-magnitude, one's complement, and two's complement:
Figure 3-4 illustrates the conversions among sign-magnitude, one's complement, and two's complement:
![Conversions between sign-magnitude, one's complement, and two's complement](number_encoding.assets/1s_2s_complement.png){ class="animation-figure" }
@ -133,7 +133,7 @@ $$
<p align="center"> Figure 3-5 &nbsp; Example calculation of a float in IEEE 754 standard </p>
Observing the diagram, given an example data $\mathrm{S} = 0$, $\mathrm{E} = 124$, $\mathrm{N} = 2^{-2} + 2^{-3} = 0.375$, we have:
Observing Figure 3-5, given an example data $\mathrm{S} = 0$, $\mathrm{E} = 124$, $\mathrm{N} = 2^{-2} + 2^{-3} = 0.375$, we have:
$$
\text{val} = (-1)^0 \times 2^{124 - 127} \times (1 + 0.375) = 0.171875

View File

@ -38,7 +38,7 @@ Starting from the original problem $f(0, n-1)$, perform the binary search throug
2. Recursively solve the subproblem reduced by half in size, which could be $f(i, m-1)$ or $f(m+1, j)$.
3. Repeat steps `1.` and `2.`, until `target` is found or the interval is empty and returns.
The diagram below shows the divide-and-conquer process of binary search for element $6$ in an array.
Figure 12-4 shows the divide-and-conquer process of binary search for element $6$ in an array.
![The divide-and-conquer process of binary search](binary_search_recur.assets/binary_search_recur.png){ class="animation-figure" }
@ -76,9 +76,32 @@ In the implementation code, we declare a recursive function `dfs()` to solve the
=== "C++"
```cpp title="binary_search_recur.cpp"
[class]{}-[func]{dfs}
/* Binary search: problem f(i, j) */
int dfs(vector<int> &nums, int target, int i, int j) {
// If the interval is empty, indicating no target element, return -1
if (i > j) {
return -1;
}
// Calculate midpoint index m
int m = (i + j) / 2;
if (nums[m] < target) {
// Recursive subproblem f(m+1, j)
return dfs(nums, target, m + 1, j);
} else if (nums[m] > target) {
// Recursive subproblem f(i, m-1)
return dfs(nums, target, i, m - 1);
} else {
// Found the target element, thus return its index
return m;
}
}
[class]{}-[func]{binarySearch}
/* Binary search */
int binarySearch(vector<int> &nums, int target) {
int n = nums.size();
// Solve problem f(0, n-1)
return dfs(nums, target, 0, n - 1);
}
```
=== "Java"

View File

@ -6,7 +6,7 @@ comments: true
!!! question
Given the preorder traversal `preorder` and inorder traversal `inorder` of a binary tree, construct the binary tree and return the root node of the binary tree. Assume that there are no duplicate values in the nodes of the binary tree (as shown in the diagram below).
Given the preorder traversal `preorder` and inorder traversal `inorder` of a binary tree, construct the binary tree and return the root node of the binary tree. Assume that there are no duplicate values in the nodes of the binary tree (as shown in Figure 12-5).
![Example data for building a binary tree](build_binary_tree_problem.assets/build_tree_example.png){ class="animation-figure" }
@ -26,10 +26,10 @@ Based on the above analysis, this problem can be solved using divide and conquer
By definition, `preorder` and `inorder` can be divided into three parts.
- Preorder traversal: `[ Root | Left Subtree | Right Subtree ]`, for example, the tree in the diagram corresponds to `[ 3 | 9 | 2 1 7 ]`.
- Inorder traversal: `[ Left Subtree | Root | Right Subtree ]`, for example, the tree in the diagram corresponds to `[ 9 | 3 | 1 2 7 ]`.
- Preorder traversal: `[ Root | Left Subtree | Right Subtree ]`, for example, the tree in the figure corresponds to `[ 3 | 9 | 2 1 7 ]`.
- Inorder traversal: `[ Left Subtree | Root | Right Subtree ]`, for example, the tree in the figure corresponds to `[ 9 | 3 | 1 2 7 ]`.
Using the data in the diagram above, we can obtain the division results as shown in the steps below.
Using the data in the figure above, we can obtain the division results as shown in Figure 12-6.
1. The first element 3 in the preorder traversal is the value of the root node.
2. Find the index of the root node 3 in `inorder`, and use this index to divide `inorder` into `[ 9 | 3 1 2 7 ]`.
@ -61,7 +61,7 @@ As shown in Table 12-1, the above variables can represent the index of the root
</div>
Please note, the meaning of $(m-l)$ in the right subtree root index is "the number of nodes in the left subtree", which is suggested to be understood in conjunction with the diagram below.
Please note, the meaning of $(m-l)$ in the right subtree root index is "the number of nodes in the left subtree", which is suggested to be understood in conjunction with Figure 12-7.
![Indexes of the root node and left and right subtrees](build_binary_tree_problem.assets/build_tree_division_pointers.png){ class="animation-figure" }
@ -107,9 +107,33 @@ To improve the efficiency of querying $m$, we use a hash table `hmap` to store t
=== "C++"
```cpp title="build_tree.cpp"
[class]{}-[func]{dfs}
/* Build binary tree: Divide and conquer */
TreeNode *dfs(vector<int> &preorder, unordered_map<int, int> &inorderMap, int i, int l, int r) {
// Terminate when subtree interval is empty
if (r - l < 0)
return NULL;
// Initialize root node
TreeNode *root = new TreeNode(preorder[i]);
// Query m to divide left and right subtrees
int m = inorderMap[preorder[i]];
// Subproblem: build left subtree
root->left = dfs(preorder, inorderMap, i + 1, l, m - 1);
// Subproblem: build right subtree
root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);
// Return root node
return root;
}
[class]{}-[func]{buildTree}
/* Build binary tree */
TreeNode *buildTree(vector<int> &preorder, vector<int> &inorder) {
// Initialize hash table, storing in-order elements to indices mapping
unordered_map<int, int> inorderMap;
for (int i = 0; i < inorder.size(); i++) {
inorderMap[inorder[i]] = i;
}
TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorder.size() - 1);
return root;
}
```
=== "Java"
@ -232,7 +256,7 @@ To improve the efficiency of querying $m$, we use a hash table `hmap` to store t
[class]{}-[func]{buildTree}
```
The diagram below shows the recursive process of building the binary tree, where each node is established during the "descending" process, and each edge (reference) is established during the "ascending" process.
Figure 12-8 shows the recursive process of building the binary tree, where each node is established during the "descending" process, and each edge (reference) is established during the "ascending" process.
=== "<1>"
![Recursive process of building a binary tree](build_binary_tree_problem.assets/built_tree_step1.png){ class="animation-figure" }
@ -263,7 +287,7 @@ The diagram below shows the recursive process of building the binary tree, where
<p align="center"> Figure 12-8 &nbsp; Recursive process of building a binary tree </p>
Each recursive function's division results of `preorder` and `inorder` are shown in the diagram below.
Each recursive function's division results of `preorder` and `inorder` are shown in Figure 12-9.
![Division results in each recursive function](build_binary_tree_problem.assets/built_tree_overall.png){ class="animation-figure" }

View File

@ -129,11 +129,36 @@ In the code, we declare a recursive function `dfs(i, src, buf, tar)` whose role
=== "C++"
```cpp title="hanota.cpp"
[class]{}-[func]{move}
/* Move a disc */
void move(vector<int> &src, vector<int> &tar) {
// Take out a disc from the top of src
int pan = src.back();
src.pop_back();
// Place the disc on top of tar
tar.push_back(pan);
}
[class]{}-[func]{dfs}
/* Solve the Tower of Hanoi problem f(i) */
void dfs(int i, vector<int> &src, vector<int> &buf, vector<int> &tar) {
// If only one disc remains on src, move it to tar
if (i == 1) {
move(src, tar);
return;
}
// Subproblem f(i-1): move the top i-1 discs from src with the help of tar to buf
dfs(i - 1, src, tar, buf);
// Subproblem f(1): move the remaining one disc from src to tar
move(src, tar);
// Subproblem f(i-1): move the top i-1 discs from buf with the help of src to tar
dfs(i - 1, buf, src, tar);
}
[class]{}-[func]{solveHanota}
/* Solve the Tower of Hanoi problem */
void solveHanota(vector<int> &A, vector<int> &B, vector<int> &C) {
int n = A.size();
// Move the top n discs from A with the help of B to C
dfs(n, A, B, C);
}
```
=== "Java"

View File

@ -61,7 +61,22 @@ According to the state transition equation, and the initial states $dp[1] = cost
=== "C++"
```cpp title="min_cost_climbing_stairs_dp.cpp"
[class]{}-[func]{minCostClimbingStairsDP}
/* Climbing stairs with minimum cost: Dynamic programming */
int minCostClimbingStairsDP(vector<int> &cost) {
int n = cost.size() - 1;
if (n == 1 || n == 2)
return cost[n];
// Initialize dp table, used to store subproblem solutions
vector<int> dp(n + 1);
// Initial state: preset the smallest subproblem solution
dp[1] = cost[1];
dp[2] = cost[2];
// State transition: gradually solve larger subproblems from smaller ones
for (int i = 3; i <= n; i++) {
dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];
}
return dp[n];
}
```
=== "Java"
@ -176,7 +191,19 @@ This problem can also be space-optimized, compressing one dimension to zero, red
=== "C++"
```cpp title="min_cost_climbing_stairs_dp.cpp"
[class]{}-[func]{minCostClimbingStairsDPComp}
/* Climbing stairs with minimum cost: Space-optimized dynamic programming */
int minCostClimbingStairsDPComp(vector<int> &cost) {
int n = cost.size() - 1;
if (n == 1 || n == 2)
return cost[n];
int a = cost[1], b = cost[2];
for (int i = 3; i <= n; i++) {
int tmp = b;
b = min(a, tmp) + cost[i];
a = tmp;
}
return b;
}
```
=== "Java"
@ -327,7 +354,25 @@ In the end, returning $dp[n, 1] + dp[n, 2]$ will do, the sum of the two represen
=== "C++"
```cpp title="climbing_stairs_constraint_dp.cpp"
[class]{}-[func]{climbingStairsConstraintDP}
/* Constrained climbing stairs: Dynamic programming */
int climbingStairsConstraintDP(int n) {
if (n == 1 || n == 2) {
return 1;
}
// Initialize dp table, used to store subproblem solutions
vector<vector<int>> dp(n + 1, vector<int>(3, 0));
// Initial state: preset the smallest subproblem solution
dp[1][1] = 1;
dp[1][2] = 0;
dp[2][1] = 0;
dp[2][2] = 1;
// State transition: gradually solve larger subproblems from smaller ones
for (int i = 3; i <= n; i++) {
dp[i][1] = dp[i - 1][2];
dp[i][2] = dp[i - 2][1] + dp[i - 2][2];
}
return dp[n][1] + dp[n][2];
}
```
=== "Java"

View File

@ -133,7 +133,22 @@ Implementation code as follows:
=== "C++"
```cpp title="min_path_sum.cpp"
[class]{}-[func]{minPathSumDFS}
/* Minimum path sum: Brute force search */
int minPathSumDFS(vector<vector<int>> &grid, int i, int j) {
// If it's the top-left cell, terminate the search
if (i == 0 && j == 0) {
return grid[0][0];
}
// If the row or column index is out of bounds, return a +∞ cost
if (i < 0 || j < 0) {
return INT_MAX;
}
// Calculate the minimum path cost from the top-left to (i-1, j) and (i, j-1)
int up = minPathSumDFS(grid, i - 1, j);
int left = minPathSumDFS(grid, i, j - 1);
// Return the minimum path cost from the top-left to (i, j)
return min(left, up) != INT_MAX ? min(left, up) + grid[i][j] : INT_MAX;
}
```
=== "Java"
@ -264,7 +279,27 @@ We introduce a memo list `mem` of the same size as the grid `grid`, used to reco
=== "C++"
```cpp title="min_path_sum.cpp"
[class]{}-[func]{minPathSumDFSMem}
/* Minimum path sum: Memoized search */
int minPathSumDFSMem(vector<vector<int>> &grid, vector<vector<int>> &mem, int i, int j) {
// If it's the top-left cell, terminate the search
if (i == 0 && j == 0) {
return grid[0][0];
}
// If the row or column index is out of bounds, return a +∞ cost
if (i < 0 || j < 0) {
return INT_MAX;
}
// If there is a record, return it
if (mem[i][j] != -1) {
return mem[i][j];
}
// The minimum path cost from the left and top cells
int up = minPathSumDFSMem(grid, mem, i - 1, j);
int left = minPathSumDFSMem(grid, mem, i, j - 1);
// Record and return the minimum path cost from the top-left to (i, j)
mem[i][j] = min(left, up) != INT_MAX ? min(left, up) + grid[i][j] : INT_MAX;
return mem[i][j];
}
```
=== "Java"
@ -394,7 +429,28 @@ Implement the dynamic programming solution iteratively, code as shown below:
=== "C++"
```cpp title="min_path_sum.cpp"
[class]{}-[func]{minPathSumDP}
/* Minimum path sum: Dynamic programming */
int minPathSumDP(vector<vector<int>> &grid) {
int n = grid.size(), m = grid[0].size();
// Initialize dp table
vector<vector<int>> dp(n, vector<int>(m));
dp[0][0] = grid[0][0];
// State transition: first row
for (int j = 1; j < m; j++) {
dp[0][j] = dp[0][j - 1] + grid[0][j];
}
// State transition: first column
for (int i = 1; i < n; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
// State transition: the rest of the rows and columns
for (int i = 1; i < n; i++) {
for (int j = 1; j < m; j++) {
dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];
}
}
return dp[n - 1][m - 1];
}
```
=== "Java"
@ -563,7 +619,27 @@ Please note, since the array `dp` can only represent the state of one row, we ca
=== "C++"
```cpp title="min_path_sum.cpp"
[class]{}-[func]{minPathSumDPComp}
/* Minimum path sum: Space-optimized dynamic programming */
int minPathSumDPComp(vector<vector<int>> &grid) {
int n = grid.size(), m = grid[0].size();
// Initialize dp table
vector<int> dp(m);
// State transition: first row
dp[0] = grid[0][0];
for (int j = 1; j < m; j++) {
dp[j] = dp[j - 1] + grid[0][j];
}
// State transition: the rest of the rows
for (int i = 1; i < n; i++) {
// State transition: first column
dp[0] = dp[0] + grid[i][0];
// State transition: the rest of the columns
for (int j = 1; j < m; j++) {
dp[j] = min(dp[j - 1], dp[j]) + grid[i][j];
}
}
return dp[m - 1];
}
```
=== "Java"

View File

@ -104,7 +104,31 @@ Observing the state transition equation, solving $dp[i, j]$ depends on the solut
=== "C++"
```cpp title="edit_distance.cpp"
[class]{}-[func]{editDistanceDP}
/* Edit distance: Dynamic programming */
int editDistanceDP(string s, string t) {
int n = s.length(), m = t.length();
vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
// State transition: first row and first column
for (int i = 1; i <= n; i++) {
dp[i][0] = i;
}
for (int j = 1; j <= m; j++) {
dp[0][j] = j;
}
// State transition: the rest of the rows and columns
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (s[i - 1] == t[j - 1]) {
// If the two characters are equal, skip these two characters
dp[i][j] = dp[i - 1][j - 1];
} else {
// The minimum number of edits = the minimum number of edits from three operations (insert, remove, replace) + 1
dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
}
}
}
return dp[n][m];
}
```
=== "Java"
@ -289,7 +313,34 @@ For this reason, we can use a variable `leftup` to temporarily store the solutio
=== "C++"
```cpp title="edit_distance.cpp"
[class]{}-[func]{editDistanceDPComp}
/* Edit distance: Space-optimized dynamic programming */
int editDistanceDPComp(string s, string t) {
int n = s.length(), m = t.length();
vector<int> dp(m + 1, 0);
// State transition: first row
for (int j = 1; j <= m; j++) {
dp[j] = j;
}
// State transition: the rest of the rows
for (int i = 1; i <= n; i++) {
// State transition: first column
int leftup = dp[0]; // Temporarily store dp[i-1, j-1]
dp[0] = i;
// State transition: the rest of the columns
for (int j = 1; j <= m; j++) {
int temp = dp[j];
if (s[i - 1] == t[j - 1]) {
// If the two characters are equal, skip these two characters
dp[j] = leftup;
} else {
// The minimum number of edits = the minimum number of edits from three operations (insert, remove, replace) + 1
dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1;
}
leftup = temp; // Update for the next round of dp[i-1, j-1]
}
}
return dp[m];
}
```
=== "Java"

View File

@ -49,9 +49,30 @@ The goal of this problem is to determine the number of ways, **considering using
=== "C++"
```cpp title="climbing_stairs_backtrack.cpp"
[class]{}-[func]{backtrack}
/* Backtracking */
void backtrack(vector<int> &choices, int state, int n, vector<int> &res) {
// When climbing to the nth step, add 1 to the number of solutions
if (state == n)
res[0]++;
// Traverse all choices
for (auto &choice : choices) {
// Pruning: do not allow climbing beyond the nth step
if (state + choice > n)
continue;
// Attempt: make a choice, update the state
backtrack(choices, state + choice, n, res);
// Retract
}
}
[class]{}-[func]{climbingStairsBacktrack}
/* Climbing stairs: Backtracking */
int climbingStairsBacktrack(int n) {
vector<int> choices = {1, 2}; // Can choose to climb up 1 step or 2 steps
int state = 0; // Start climbing from the 0th step
vector<int> res = {0}; // Use res[0] to record the number of solutions
backtrack(choices, state, n, res);
return res[0];
}
```
=== "Java"
@ -220,9 +241,20 @@ Observe the following code, which, like standard backtracking code, belongs to d
=== "C++"
```cpp title="climbing_stairs_dfs.cpp"
[class]{}-[func]{dfs}
/* Search */
int dfs(int i) {
// Known dp[1] and dp[2], return them
if (i == 1 || i == 2)
return i;
// dp[i] = dp[i-1] + dp[i-2]
int count = dfs(i - 1) + dfs(i - 2);
return count;
}
[class]{}-[func]{climbingStairsDFS}
/* Climbing stairs: Search */
int climbingStairsDFS(int n) {
return dfs(n);
}
```
=== "Java"
@ -378,9 +410,27 @@ The code is as follows:
=== "C++"
```cpp title="climbing_stairs_dfs_mem.cpp"
[class]{}-[func]{dfs}
/* Memoized search */
int dfs(int i, vector<int> &mem) {
// Known dp[1] and dp[2], return them
if (i == 1 || i == 2)
return i;
// If there is a record for dp[i], return it
if (mem[i] != -1)
return mem[i];
// dp[i] = dp[i-1] + dp[i-2]
int count = dfs(i - 1, mem) + dfs(i - 2, mem);
// Record dp[i]
mem[i] = count;
return count;
}
[class]{}-[func]{climbingStairsDFSMem}
/* Climbing stairs: Memoized search */
int climbingStairsDFSMem(int n) {
// mem[i] records the total number of solutions for climbing to the ith step, -1 means no record
vector<int> mem(n + 1, -1);
return dfs(n, mem);
}
```
=== "Java"
@ -532,7 +582,21 @@ Since dynamic programming does not include a backtracking process, it only requi
=== "C++"
```cpp title="climbing_stairs_dp.cpp"
[class]{}-[func]{climbingStairsDP}
/* Climbing stairs: Dynamic programming */
int climbingStairsDP(int n) {
if (n == 1 || n == 2)
return n;
// Initialize dp table, used to store subproblem solutions
vector<int> dp(n + 1);
// Initial state: preset the smallest subproblem solution
dp[1] = 1;
dp[2] = 2;
// State transition: gradually solve larger subproblems from smaller ones
for (int i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
```
=== "Java"
@ -655,7 +719,18 @@ Observant readers may have noticed that **since $dp[i]$ is only related to $dp[i
=== "C++"
```cpp title="climbing_stairs_dp.cpp"
[class]{}-[func]{climbingStairsDPComp}
/* Climbing stairs: Space-optimized dynamic programming */
int climbingStairsDPComp(int n) {
if (n == 1 || n == 2)
return n;
int a = 1, b = 2;
for (int i = 3; i <= n; i++) {
int tmp = b;
b = a + b;
a = tmp;
}
return b;
}
```
=== "Java"

View File

@ -83,7 +83,22 @@ The search code includes the following elements.
=== "C++"
```cpp title="knapsack.cpp"
[class]{}-[func]{knapsackDFS}
/* 0-1 Knapsack: Brute force search */
int knapsackDFS(vector<int> &wgt, vector<int> &val, int i, int c) {
// If all items have been chosen or the knapsack has no remaining capacity, return value 0
if (i == 0 || c == 0) {
return 0;
}
// If exceeding the knapsack capacity, can only choose not to put it in the knapsack
if (wgt[i - 1] > c) {
return knapsackDFS(wgt, val, i - 1, c);
}
// Calculate the maximum value of not putting in and putting in item i
int no = knapsackDFS(wgt, val, i - 1, c);
int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];
// Return the greater value of the two options
return max(no, yes);
}
```
=== "Java"
@ -214,7 +229,27 @@ After introducing memoization, **the time complexity depends on the number of su
=== "C++"
```cpp title="knapsack.cpp"
[class]{}-[func]{knapsackDFSMem}
/* 0-1 Knapsack: Memoized search */
int knapsackDFSMem(vector<int> &wgt, vector<int> &val, vector<vector<int>> &mem, int i, int c) {
// If all items have been chosen or the knapsack has no remaining capacity, return value 0
if (i == 0 || c == 0) {
return 0;
}
// If there is a record, return it
if (mem[i][c] != -1) {
return mem[i][c];
}
// If exceeding the knapsack capacity, can only choose not to put it in the knapsack
if (wgt[i - 1] > c) {
return knapsackDFSMem(wgt, val, mem, i - 1, c);
}
// Calculate the maximum value of not putting in and putting in item i
int no = knapsackDFSMem(wgt, val, mem, i - 1, c);
int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];
// Record and return the greater value of the two options
mem[i][c] = max(no, yes);
return mem[i][c];
}
```
=== "Java"
@ -342,7 +377,25 @@ Dynamic programming essentially involves filling the $dp$ table during the state
=== "C++"
```cpp title="knapsack.cpp"
[class]{}-[func]{knapsackDP}
/* 0-1 Knapsack: Dynamic programming */
int knapsackDP(vector<int> &wgt, vector<int> &val, int cap) {
int n = wgt.size();
// Initialize dp table
vector<vector<int>> dp(n + 1, vector<int>(cap + 1, 0));
// State transition
for (int i = 1; i <= n; i++) {
for (int c = 1; c <= cap; c++) {
if (wgt[i - 1] > c) {
// If exceeding the knapsack capacity, do not choose item i
dp[i][c] = dp[i - 1][c];
} else {
// The greater value between not choosing and choosing item i
dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);
}
}
}
return dp[n][cap];
}
```
=== "Java"
@ -435,7 +488,7 @@ Dynamic programming essentially involves filling the $dp$ table during the state
[class]{}-[func]{knapsackDP}
```
As shown in the figures below, both the time complexity and space complexity are determined by the size of the array `dp`, i.e., $O(n \times cap)$.
As shown in Figure 14-20, both the time complexity and space complexity are determined by the size of the array `dp`, i.e., $O(n \times cap)$.
=== "<1>"
![The dynamic programming process of the 0-1 knapsack problem](knapsack_problem.assets/knapsack_dp_step1.png){ class="animation-figure" }
@ -538,7 +591,23 @@ In the code implementation, we only need to delete the first dimension $i$ of th
=== "C++"
```cpp title="knapsack.cpp"
[class]{}-[func]{knapsackDPComp}
/* 0-1 Knapsack: Space-optimized dynamic programming */
int knapsackDPComp(vector<int> &wgt, vector<int> &val, int cap) {
int n = wgt.size();
// Initialize dp table
vector<int> dp(cap + 1, 0);
// State transition
for (int i = 1; i <= n; i++) {
// Traverse in reverse order
for (int c = cap; c >= 1; c--) {
if (wgt[i - 1] <= c) {
// The greater value between not choosing and choosing item i
dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);
}
}
}
return dp[cap];
}
```
=== "Java"

View File

@ -61,7 +61,25 @@ Comparing the code for the two problems, the state transition changes from $i-1$
=== "C++"
```cpp title="unbounded_knapsack.cpp"
[class]{}-[func]{unboundedKnapsackDP}
/* Complete knapsack: Dynamic programming */
int unboundedKnapsackDP(vector<int> &wgt, vector<int> &val, int cap) {
int n = wgt.size();
// Initialize dp table
vector<vector<int>> dp(n + 1, vector<int>(cap + 1, 0));
// State transition
for (int i = 1; i <= n; i++) {
for (int c = 1; c <= cap; c++) {
if (wgt[i - 1] > c) {
// If exceeding the knapsack capacity, do not choose item i
dp[i][c] = dp[i - 1][c];
} else {
// The greater value between not choosing and choosing item i
dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);
}
}
}
return dp[n][cap];
}
```
=== "Java"
@ -206,7 +224,25 @@ The code implementation is quite simple, just remove the first dimension of the
=== "C++"
```cpp title="unbounded_knapsack.cpp"
[class]{}-[func]{unboundedKnapsackDPComp}
/* Complete knapsack: Space-optimized dynamic programming */
int unboundedKnapsackDPComp(vector<int> &wgt, vector<int> &val, int cap) {
int n = wgt.size();
// Initialize dp table
vector<int> dp(cap + 1, 0);
// State transition
for (int i = 1; i <= n; i++) {
for (int c = 1; c <= cap; c++) {
if (wgt[i - 1] > c) {
// If exceeding the knapsack capacity, do not choose item i
dp[c] = dp[c];
} else {
// The greater value between not choosing and choosing item i
dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);
}
}
}
return dp[cap];
}
```
=== "Java"
@ -375,7 +411,30 @@ For this reason, we use the number $amt + 1$ to represent an invalid solution, b
=== "C++"
```cpp title="coin_change.cpp"
[class]{}-[func]{coinChangeDP}
/* Coin change: Dynamic programming */
int coinChangeDP(vector<int> &coins, int amt) {
int n = coins.size();
int MAX = amt + 1;
// Initialize dp table
vector<vector<int>> dp(n + 1, vector<int>(amt + 1, 0));
// State transition: first row and first column
for (int a = 1; a <= amt; a++) {
dp[0][a] = MAX;
}
// State transition: the rest of the rows and columns
for (int i = 1; i <= n; i++) {
for (int a = 1; a <= amt; a++) {
if (coins[i - 1] > a) {
// If exceeding the target amount, do not choose coin i
dp[i][a] = dp[i - 1][a];
} else {
// The smaller value between not choosing and choosing coin i
dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);
}
}
}
return dp[n][amt] != MAX ? dp[n][amt] : -1;
}
```
=== "Java"
@ -552,7 +611,27 @@ The space optimization for the coin change problem is handled in the same way as
=== "C++"
```cpp title="coin_change.cpp"
[class]{}-[func]{coinChangeDPComp}
/* Coin change: Space-optimized dynamic programming */
int coinChangeDPComp(vector<int> &coins, int amt) {
int n = coins.size();
int MAX = amt + 1;
// Initialize dp table
vector<int> dp(amt + 1, MAX);
dp[0] = 0;
// State transition
for (int i = 1; i <= n; i++) {
for (int a = 1; a <= amt; a++) {
if (coins[i - 1] > a) {
// If exceeding the target amount, do not choose coin i
dp[a] = dp[a];
} else {
// The smaller value between not choosing and choosing coin i
dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1);
}
}
}
return dp[amt] != MAX ? dp[amt] : -1;
}
```
=== "Java"
@ -698,7 +777,29 @@ When the target amount is $0$, no coins are needed to make up the target amount,
=== "C++"
```cpp title="coin_change_ii.cpp"
[class]{}-[func]{coinChangeIIDP}
/* Coin change II: Dynamic programming */
int coinChangeIIDP(vector<int> &coins, int amt) {
int n = coins.size();
// Initialize dp table
vector<vector<int>> dp(n + 1, vector<int>(amt + 1, 0));
// Initialize first column
for (int i = 0; i <= n; i++) {
dp[i][0] = 1;
}
// State transition
for (int i = 1; i <= n; i++) {
for (int a = 1; a <= amt; a++) {
if (coins[i - 1] > a) {
// If exceeding the target amount, do not choose coin i
dp[i][a] = dp[i - 1][a];
} else {
// The sum of the two options of not choosing and choosing coin i
dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];
}
}
}
return dp[n][amt];
}
```
=== "Java"
@ -824,7 +925,26 @@ The space optimization approach is the same, just remove the coin dimension:
=== "C++"
```cpp title="coin_change_ii.cpp"
[class]{}-[func]{coinChangeIIDPComp}
/* Coin change II: Space-optimized dynamic programming */
int coinChangeIIDPComp(vector<int> &coins, int amt) {
int n = coins.size();
// Initialize dp table
vector<int> dp(amt + 1, 0);
dp[0] = 1;
// State transition
for (int i = 1; i <= n; i++) {
for (int a = 1; a <= amt; a++) {
if (coins[i - 1] > a) {
// If exceeding the target amount, do not choose coin i
dp[a] = dp[a];
} else {
// The sum of the two options of not choosing and choosing coin i
dp[a] = dp[a] + dp[a - coins[i - 1]];
}
}
}
return dp[amt];
}
```
=== "Java"

View File

@ -50,7 +50,7 @@ Below is the implementation code for graphs represented using an adjacency matri
for val in vertices:
self.add_vertex(val)
# Add edge
# Please note, edges elements represent vertex indices, corresponding to vertices elements indices
# Edges elements represent vertex indices
for e in edges:
self.add_edge(e[0], e[1])
@ -111,7 +111,89 @@ Below is the implementation code for graphs represented using an adjacency matri
=== "C++"
```cpp title="graph_adjacency_matrix.cpp"
[class]{GraphAdjMat}-[func]{}
/* Undirected graph class based on adjacency matrix */
class GraphAdjMat {
vector<int> vertices; // Vertex list, elements represent "vertex value", index represents "vertex index"
vector<vector<int>> adjMat; // Adjacency matrix, row and column indices correspond to "vertex index"
public:
/* Constructor */
GraphAdjMat(const vector<int> &vertices, const vector<vector<int>> &edges) {
// Add vertex
for (int val : vertices) {
addVertex(val);
}
// Add edge
// Edges elements represent vertex indices
for (const vector<int> &edge : edges) {
addEdge(edge[0], edge[1]);
}
}
/* Get the number of vertices */
int size() const {
return vertices.size();
}
/* Add vertex */
void addVertex(int val) {
int n = size();
// Add new vertex value to the vertex list
vertices.push_back(val);
// Add a row to the adjacency matrix
adjMat.emplace_back(vector<int>(n, 0));
// Add a column to the adjacency matrix
for (vector<int> &row : adjMat) {
row.push_back(0);
}
}
/* Remove vertex */
void removeVertex(int index) {
if (index >= size()) {
throw out_of_range("Vertex does not exist");
}
// Remove vertex at `index` from the vertex list
vertices.erase(vertices.begin() + index);
// Remove the row at `index` from the adjacency matrix
adjMat.erase(adjMat.begin() + index);
// Remove the column at `index` from the adjacency matrix
for (vector<int> &row : adjMat) {
row.erase(row.begin() + index);
}
}
/* Add edge */
// Parameters i, j correspond to vertices element indices
void addEdge(int i, int j) {
// Handle index out of bounds and equality
if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {
throw out_of_range("Vertex does not exist");
}
// In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., satisfies (i, j) == (j, i)
adjMat[i][j] = 1;
adjMat[j][i] = 1;
}
/* Remove edge */
// Parameters i, j correspond to vertices element indices
void removeEdge(int i, int j) {
// Handle index out of bounds and equality
if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {
throw out_of_range("Vertex does not exist");
}
adjMat[i][j] = 0;
adjMat[j][i] = 0;
}
/* Print adjacency matrix */
void print() {
cout << "Vertex list = ";
printVector(vertices);
cout << "Adjacency matrix =" << endl;
printVectorMatrix(adjMat);
}
};
```
=== "Java"
@ -131,7 +213,7 @@ Below is the implementation code for graphs represented using an adjacency matri
addVertex(val);
}
// Add edge
// Please note, edges elements represent vertex indices, corresponding to vertices elements indices
// Edges elements represent vertex indices
for (int[] e : edges) {
addEdge(e[0], e[1]);
}
@ -297,7 +379,7 @@ Given an undirected graph with a total of $n$ vertices and $m$ edges, the variou
<p align="center"> Figure 9-8 &nbsp; Initialization, adding and removing edges, adding and removing vertices in adjacency list </p>
Below is the adjacency list code implementation. Compared to the above diagram, the actual code has the following differences.
Below is the adjacency list code implementation. Compared to Figure 9-8, the actual code has the following differences.
- For convenience in adding and removing vertices, and to simplify the code, we use lists (dynamic arrays) instead of linked lists.
- Use a hash table to store the adjacency list, `key` being the vertex instance, `value` being the list (linked list) of adjacent vertices of that vertex.
@ -369,7 +451,86 @@ Additionally, we use the `Vertex` class to represent vertices in the adjacency l
=== "C++"
```cpp title="graph_adjacency_list.cpp"
[class]{GraphAdjList}-[func]{}
/* Undirected graph class based on adjacency list */
class GraphAdjList {
public:
// Adjacency list, key: vertex, value: all adjacent vertices of that vertex
unordered_map<Vertex *, vector<Vertex *>> adjList;
/* Remove a specified node from vector */
void remove(vector<Vertex *> &vec, Vertex *vet) {
for (int i = 0; i < vec.size(); i++) {
if (vec[i] == vet) {
vec.erase(vec.begin() + i);
break;
}
}
}
/* Constructor */
GraphAdjList(const vector<vector<Vertex *>> &edges) {
// Add all vertices and edges
for (const vector<Vertex *> &edge : edges) {
addVertex(edge[0]);
addVertex(edge[1]);
addEdge(edge[0], edge[1]);
}
}
/* Get the number of vertices */
int size() {
return adjList.size();
}
/* Add edge */
void addEdge(Vertex *vet1, Vertex *vet2) {
if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)
throw invalid_argument("Vertex does not exist");
// Add edge vet1 - vet2
adjList[vet1].push_back(vet2);
adjList[vet2].push_back(vet1);
}
/* Remove edge */
void removeEdge(Vertex *vet1, Vertex *vet2) {
if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)
throw invalid_argument("Vertex does not exist");
// Remove edge vet1 - vet2
remove(adjList[vet1], vet2);
remove(adjList[vet2], vet1);
}
/* Add vertex */
void addVertex(Vertex *vet) {
if (adjList.count(vet))
return;
// Add a new linked list to the adjacency list
adjList[vet] = vector<Vertex *>();
}
/* Remove vertex */
void removeVertex(Vertex *vet) {
if (!adjList.count(vet))
throw invalid_argument("Vertex does not exist");
// Remove the vertex vet's corresponding linked list from the adjacency list
adjList.erase(vet);
// Traverse other vertices' linked lists, removing all edges containing vet
for (auto &adj : adjList) {
remove(adj.second, vet);
}
}
/* Print the adjacency list */
void print() {
cout << "Adjacency list =" << endl;
for (auto &adj : adjList) {
const auto &key = adj.first;
const auto &vec = adj.second;
cout << key->val << ": ";
printVector(vetsToVals(vec));
}
}
};
```
=== "Java"

View File

@ -55,7 +55,32 @@ To prevent revisiting vertices, we use a hash set `visited` to record which node
=== "C++"
```cpp title="graph_bfs.cpp"
[class]{}-[func]{graphBFS}
/* Breadth-first traversal */
// Use adjacency list to represent the graph, to obtain all adjacent vertices of a specified vertex
vector<Vertex *> graphBFS(GraphAdjList &graph, Vertex *startVet) {
// Vertex traversal sequence
vector<Vertex *> res;
// Hash set, used to record visited vertices
unordered_set<Vertex *> visited = {startVet};
// Queue used to implement BFS
queue<Vertex *> que;
que.push(startVet);
// Starting from vertex vet, loop until all vertices are visited
while (!que.empty()) {
Vertex *vet = que.front();
que.pop(); // Dequeue the vertex at the head of the queue
res.push_back(vet); // Record visited vertex
// Traverse all adjacent vertices of that vertex
for (auto adjVet : graph.adjList[vet]) {
if (visited.count(adjVet))
continue; // Skip already visited vertices
que.push(adjVet); // Only enqueue unvisited vertices
visited.emplace(adjVet); // Mark the vertex as visited
}
}
// Return the vertex traversal sequence
return res;
}
```
=== "Java"
@ -246,9 +271,29 @@ This "go as far as possible and then return" algorithm paradigm is usually imple
=== "C++"
```cpp title="graph_dfs.cpp"
[class]{}-[func]{dfs}
/* Depth-first traversal helper function */
void dfs(GraphAdjList &graph, unordered_set<Vertex *> &visited, vector<Vertex *> &res, Vertex *vet) {
res.push_back(vet); // Record visited vertex
visited.emplace(vet); // Mark the vertex as visited
// Traverse all adjacent vertices of that vertex
for (Vertex *adjVet : graph.adjList[vet]) {
if (visited.count(adjVet))
continue; // Skip already visited vertices
// Recursively visit adjacent vertices
dfs(graph, visited, res, adjVet);
}
}
[class]{}-[func]{graphDFS}
/* Depth-first traversal */
// Use adjacency list to represent the graph, to obtain all adjacent vertices of a specified vertex
vector<Vertex *> graphDFS(GraphAdjList &graph, Vertex *startVet) {
// Vertex traversal sequence
vector<Vertex *> res;
// Hash set, used to record visited vertices
unordered_set<Vertex *> visited;
dfs(graph, visited, res, startVet);
return res;
}
```
=== "Java"

View File

@ -73,9 +73,41 @@ We have created an `Item` class in order to sort the items by their unit value.
=== "C++"
```cpp title="fractional_knapsack.cpp"
[class]{Item}-[func]{}
/* Item */
class Item {
public:
int w; // Item weight
int v; // Item value
[class]{}-[func]{fractionalKnapsack}
Item(int w, int v) : w(w), v(v) {
}
};
/* Fractional knapsack: Greedy */
double fractionalKnapsack(vector<int> &wgt, vector<int> &val, int cap) {
// Create an item list, containing two properties: weight, value
vector<Item> items;
for (int i = 0; i < wgt.size(); i++) {
items.push_back(Item(wgt[i], val[i]));
}
// Sort by unit value item.v / item.w from high to low
sort(items.begin(), items.end(), [](Item &a, Item &b) { return (double)a.v / a.w > (double)b.v / b.w; });
// Loop for greedy selection
double res = 0;
for (auto &item : items) {
if (item.w <= cap) {
// If the remaining capacity is sufficient, put the entire item into the knapsack
res += item.v;
cap -= item.w;
} else {
// If the remaining capacity is insufficient, put part of the item into the knapsack
res += (double)item.v / item.w * cap;
// No remaining capacity left, thus break the loop
break;
}
}
return res;
}
```
=== "Java"

View File

@ -48,7 +48,24 @@ The implementation code is as follows:
=== "C++"
```cpp title="coin_change_greedy.cpp"
[class]{}-[func]{coinChangeGreedy}
/* Coin change: Greedy */
int coinChangeGreedy(vector<int> &coins, int amt) {
// Assume coins list is ordered
int i = coins.size() - 1;
int count = 0;
// Loop for greedy selection until no remaining amount
while (amt > 0) {
// Find the smallest coin close to and less than the remaining amount
while (i > 0 && coins[i] > amt) {
i--;
}
// Choose coins[i]
amt -= coins[i];
count++;
}
// If no feasible solution is found, return -1
return amt == 0 ? count : -1;
}
```
=== "Java"

View File

@ -117,7 +117,26 @@ The variables $i$, $j$, and $res$ use a constant amount of extra space, **thus t
=== "C++"
```cpp title="max_capacity.cpp"
[class]{}-[func]{maxCapacity}
/* Maximum capacity: Greedy */
int maxCapacity(vector<int> &ht) {
// Initialize i, j, making them split the array at both ends
int i = 0, j = ht.size() - 1;
// Initial maximum capacity is 0
int res = 0;
// Loop for greedy selection until the two boards meet
while (i < j) {
// Update maximum capacity
int cap = min(ht[i], ht[j]) * (j - i);
res = max(res, cap);
// Move the shorter board inward
if (ht[i] < ht[j]) {
i++;
} else {
j--;
}
}
return res;
}
```
=== "Java"

View File

@ -6,7 +6,7 @@ comments: true
!!! question
Given a positive integer $n$, split it into at least two positive integers that sum up to $n$, and find the maximum product of these integers, as illustrated below.
Given a positive integer $n$, split it into at least two positive integers that sum up to $n$, and find the maximum product of these integers, as illustrated in Figure 15-13.
![Definition of the maximum product cutting problem](max_product_cutting_problem.assets/max_product_cutting_definition.png){ class="animation-figure" }
@ -96,7 +96,26 @@ Please note, for the boundary case where $n \leq 3$, a $1$ must be split out, wi
=== "C++"
```cpp title="max_product_cutting.cpp"
[class]{}-[func]{maxProductCutting}
/* Maximum product of cutting: Greedy */
int maxProductCutting(int n) {
// When n <= 3, must cut out a 1
if (n <= 3) {
return 1 * (n - 1);
}
// Greedy cut out 3s, a is the number of 3s, b is the remainder
int a = n / 3;
int b = n % 3;
if (b == 1) {
// When the remainder is 1, convert a pair of 1 * 3 into 2 * 2
return (int)pow(3, a - 1) * 2 * 2;
}
if (b == 2) {
// When the remainder is 2, do nothing
return (int)pow(3, a) * 2;
}
// When the remainder is 0, do nothing
return (int)pow(3, a);
}
```
=== "Java"

View File

@ -91,13 +91,45 @@ The design of hash algorithms is a complex issue that requires consideration of
=== "C++"
```cpp title="simple_hash.cpp"
[class]{}-[func]{addHash}
/* Additive hash */
int addHash(string key) {
long long hash = 0;
const int MODULUS = 1000000007;
for (unsigned char c : key) {
hash = (hash + (int)c) % MODULUS;
}
return (int)hash;
}
[class]{}-[func]{mulHash}
/* Multiplicative hash */
int mulHash(string key) {
long long hash = 0;
const int MODULUS = 1000000007;
for (unsigned char c : key) {
hash = (31 * hash + (int)c) % MODULUS;
}
return (int)hash;
}
[class]{}-[func]{xorHash}
/* XOR hash */
int xorHash(string key) {
int hash = 0;
const int MODULUS = 1000000007;
for (unsigned char c : key) {
hash ^= (int)c;
}
return hash & MODULUS;
}
[class]{}-[func]{rotHash}
/* Rotational hash */
int rotHash(string key) {
long long hash = 0;
const int MODULUS = 1000000007;
for (unsigned char c : key) {
hash = ((hash << 4) ^ (hash >> 28) ^ (int)c) % MODULUS;
}
return (int)hash;
}
```
=== "Java"

View File

@ -123,7 +123,119 @@ The code below provides a simple implementation of a separate chaining hash tabl
=== "C++"
```cpp title="hash_map_chaining.cpp"
[class]{HashMapChaining}-[func]{}
/* Chained address hash table */
class HashMapChaining {
private:
int size; // Number of key-value pairs
int capacity; // Hash table capacity
double loadThres; // Load factor threshold for triggering expansion
int extendRatio; // Expansion multiplier
vector<vector<Pair *>> buckets; // Bucket array
public:
/* Constructor */
HashMapChaining() : size(0), capacity(4), loadThres(2.0 / 3.0), extendRatio(2) {
buckets.resize(capacity);
}
/* Destructor */
~HashMapChaining() {
for (auto &bucket : buckets) {
for (Pair *pair : bucket) {
// Free memory
delete pair;
}
}
}
/* Hash function */
int hashFunc(int key) {
return key % capacity;
}
/* Load factor */
double loadFactor() {
return (double)size / (double)capacity;
}
/* Query operation */
string get(int key) {
int index = hashFunc(key);
// Traverse the bucket, if the key is found, return the corresponding val
for (Pair *pair : buckets[index]) {
if (pair->key == key) {
return pair->val;
}
}
// If key not found, return an empty string
return "";
}
/* Add operation */
void put(int key, string val) {
// When the load factor exceeds the threshold, perform expansion
if (loadFactor() > loadThres) {
extend();
}
int index = hashFunc(key);
// Traverse the bucket, if the specified key is encountered, update the corresponding val and return
for (Pair *pair : buckets[index]) {
if (pair->key == key) {
pair->val = val;
return;
}
}
// If the key is not found, add the key-value pair to the end
buckets[index].push_back(new Pair(key, val));
size++;
}
/* Remove operation */
void remove(int key) {
int index = hashFunc(key);
auto &bucket = buckets[index];
// Traverse the bucket, remove the key-value pair from it
for (int i = 0; i < bucket.size(); i++) {
if (bucket[i]->key == key) {
Pair *tmp = bucket[i];
bucket.erase(bucket.begin() + i); // Remove key-value pair
delete tmp; // Free memory
size--;
return;
}
}
}
/* Extend hash table */
void extend() {
// Temporarily store the original hash table
vector<vector<Pair *>> bucketsTmp = buckets;
// Initialize the extended new hash table
capacity *= extendRatio;
buckets.clear();
buckets.resize(capacity);
size = 0;
// Move key-value pairs from the original hash table to the new hash table
for (auto &bucket : bucketsTmp) {
for (Pair *pair : bucket) {
put(pair->key, pair->val);
// Free memory
delete pair;
}
}
}
/* Print hash table */
void print() {
for (auto &bucket : buckets) {
cout << "[";
for (Pair *pair : bucket) {
cout << pair->key << " -> " << pair->val << ", ";
}
cout << "]\n";
}
}
};
```
=== "Java"
@ -451,7 +563,140 @@ The code below implements an open addressing (linear probing) hash table with la
=== "C++"
```cpp title="hash_map_open_addressing.cpp"
[class]{HashMapOpenAddressing}-[func]{}
/* Open addressing hash table */
class HashMapOpenAddressing {
private:
int size; // Number of key-value pairs
int capacity = 4; // Hash table capacity
const double loadThres = 2.0 / 3.0; // Load factor threshold for triggering expansion
const int extendRatio = 2; // Expansion multiplier
vector<Pair *> buckets; // Bucket array
Pair *TOMBSTONE = new Pair(-1, "-1"); // Removal mark
public:
/* Constructor */
HashMapOpenAddressing() : size(0), buckets(capacity, nullptr) {
}
/* Destructor */
~HashMapOpenAddressing() {
for (Pair *pair : buckets) {
if (pair != nullptr && pair != TOMBSTONE) {
delete pair;
}
}
delete TOMBSTONE;
}
/* Hash function */
int hashFunc(int key) {
return key % capacity;
}
/* Load factor */
double loadFactor() {
return (double)size / capacity;
}
/* Search for the bucket index corresponding to key */
int findBucket(int key) {
int index = hashFunc(key);
int firstTombstone = -1;
// Linear probing, break when encountering an empty bucket
while (buckets[index] != nullptr) {
// If the key is encountered, return the corresponding bucket index
if (buckets[index]->key == key) {
// If a removal mark was encountered earlier, move the key-value pair to that index
if (firstTombstone != -1) {
buckets[firstTombstone] = buckets[index];
buckets[index] = TOMBSTONE;
return firstTombstone; // Return the moved bucket index
}
return index; // Return bucket index
}
// Record the first encountered removal mark
if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {
firstTombstone = index;
}
// Calculate the bucket index, return to the head if exceeding the tail
index = (index + 1) % capacity;
}
// If the key does not exist, return the index of the insertion point
return firstTombstone == -1 ? index : firstTombstone;
}
/* Query operation */
string get(int key) {
// Search for the bucket index corresponding to key
int index = findBucket(key);
// If the key-value pair is found, return the corresponding val
if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {
return buckets[index]->val;
}
// If key-value pair does not exist, return an empty string
return "";
}
/* Add operation */
void put(int key, string val) {
// When the load factor exceeds the threshold, perform expansion
if (loadFactor() > loadThres) {
extend();
}
// Search for the bucket index corresponding to key
int index = findBucket(key);
// If the key-value pair is found, overwrite val and return
if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {
buckets[index]->val = val;
return;
}
// If the key-value pair does not exist, add the key-value pair
buckets[index] = new Pair(key, val);
size++;
}
/* Remove operation */
void remove(int key) {
// Search for the bucket index corresponding to key
int index = findBucket(key);
// If the key-value pair is found, cover it with a removal mark
if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {
delete buckets[index];
buckets[index] = TOMBSTONE;
size--;
}
}
/* Extend hash table */
void extend() {
// Temporarily store the original hash table
vector<Pair *> bucketsTmp = buckets;
// Initialize the extended new hash table
capacity *= extendRatio;
buckets = vector<Pair *>(capacity, nullptr);
size = 0;
// Move key-value pairs from the original hash table to the new hash table
for (Pair *pair : bucketsTmp) {
if (pair != nullptr && pair != TOMBSTONE) {
put(pair->key, pair->val);
delete pair;
}
}
}
/* Print hash table */
void print() {
for (Pair *pair : buckets) {
if (pair == nullptr) {
cout << "nullptr" << endl;
} else if (pair == TOMBSTONE) {
cout << "TOMBSTONE" << endl;
} else {
cout << pair->key << " -> " << pair->val << endl;
}
}
}
};
```
=== "Java"

View File

@ -598,9 +598,106 @@ The following code implements a simple hash table. Here, we encapsulate `key` an
=== "C++"
```cpp title="array_hash_map.cpp"
[class]{Pair}-[func]{}
/* Key-value pair */
struct Pair {
public:
int key;
string val;
Pair(int key, string val) {
this->key = key;
this->val = val;
}
};
[class]{ArrayHashMap}-[func]{}
/* Hash table based on array implementation */
class ArrayHashMap {
private:
vector<Pair *> buckets;
public:
ArrayHashMap() {
// Initialize an array, containing 100 buckets
buckets = vector<Pair *>(100);
}
~ArrayHashMap() {
// Free memory
for (const auto &bucket : buckets) {
delete bucket;
}
buckets.clear();
}
/* Hash function */
int hashFunc(int key) {
int index = key % 100;
return index;
}
/* Query operation */
string get(int key) {
int index = hashFunc(key);
Pair *pair = buckets[index];
if (pair == nullptr)
return "";
return pair->val;
}
/* Add operation */
void put(int key, string val) {
Pair *pair = new Pair(key, val);
int index = hashFunc(key);
buckets[index] = pair;
}
/* Remove operation */
void remove(int key) {
int index = hashFunc(key);
// Free memory and set to nullptr
delete buckets[index];
buckets[index] = nullptr;
}
/* Get all key-value pairs */
vector<Pair *> pairSet() {
vector<Pair *> pairSet;
for (Pair *pair : buckets) {
if (pair != nullptr) {
pairSet.push_back(pair);
}
}
return pairSet;
}
/* Get all keys */
vector<int> keySet() {
vector<int> keySet;
for (Pair *pair : buckets) {
if (pair != nullptr) {
keySet.push_back(pair->key);
}
}
return keySet;
}
/* Get all values */
vector<string> valueSet() {
vector<string> valueSet;
for (Pair *pair : buckets) {
if (pair != nullptr) {
valueSet.push_back(pair->val);
}
}
return valueSet;
}
/* Print hash table */
void print() {
for (Pair *kv : pairSet()) {
cout << kv->key << " -> " << kv->val << endl;
}
}
};
```
=== "Java"

View File

@ -42,7 +42,15 @@ It's worth mentioning that **since leaf nodes have no children, they naturally f
=== "C++"
```cpp title="my_heap.cpp"
[class]{MaxHeap}-[func]{MaxHeap}
/* Constructor, build heap based on input list */
MaxHeap(vector<int> nums) {
// Add all list elements into the heap
maxHeap = nums;
// Heapify all nodes except leaves
for (int i = parent(size() - 1); i >= 0; i--) {
siftDown(i);
}
}
```
=== "Java"

View File

@ -465,11 +465,20 @@ We can encapsulate the index mapping formula into functions for convenient later
=== "C++"
```cpp title="my_heap.cpp"
[class]{MaxHeap}-[func]{left}
/* Get index of left child node */
int left(int i) {
return 2 * i + 1;
}
[class]{MaxHeap}-[func]{right}
/* Get index of right child node */
int right(int i) {
return 2 * i + 2;
}
[class]{MaxHeap}-[func]{parent}
/* Get index of parent node */
int parent(int i) {
return (i - 1) / 2; // Integer division down
}
```
=== "Java"
@ -616,7 +625,10 @@ The top element of the heap is the root node of the binary tree, which is also t
=== "C++"
```cpp title="my_heap.cpp"
[class]{MaxHeap}-[func]{peek}
/* Access heap top element */
int peek() {
return maxHeap[0];
}
```
=== "Java"
@ -758,9 +770,28 @@ Given a total of $n$ nodes, the height of the tree is $O(\log n)$. Hence, the lo
=== "C++"
```cpp title="my_heap.cpp"
[class]{MaxHeap}-[func]{push}
/* Push the element into heap */
void push(int val) {
// Add node
maxHeap.push_back(val);
// Heapify from bottom to top
siftUp(size() - 1);
}
[class]{MaxHeap}-[func]{siftUp}
/* Start heapifying node i, from bottom to top */
void siftUp(int i) {
while (true) {
// Get parent node of node i
int p = parent(i);
// When "crossing the root node" or "node does not need repair", end heapification
if (p < 0 || maxHeap[i] <= maxHeap[p])
break;
// Swap two nodes
swap(maxHeap[i], maxHeap[p]);
// Loop upwards heapification
i = p;
}
}
```
=== "Java"
@ -960,9 +991,37 @@ Similar to the element insertion operation, the time complexity of the top eleme
=== "C++"
```cpp title="my_heap.cpp"
[class]{MaxHeap}-[func]{pop}
/* Element exits heap */
void pop() {
// Empty handling
if (isEmpty()) {
throw out_of_range("Heap is empty");
}
// Swap the root node with the rightmost leaf node (swap the first element with the last element)
swap(maxHeap[0], maxHeap[size() - 1]);
// Remove node
maxHeap.pop_back();
// Heapify from top to bottom
siftDown(0);
}
[class]{MaxHeap}-[func]{siftDown}
/* Start heapifying node i, from top to bottom */
void siftDown(int i) {
while (true) {
// Determine the largest node among i, l, r, noted as ma
int l = left(i), r = right(i), ma = i;
if (l < size() && maxHeap[l] > maxHeap[ma])
ma = l;
if (r < size() && maxHeap[r] > maxHeap[ma])
ma = r;
// If node i is the largest or indices l, r are out of bounds, no further heapification needed, break
if (ma == i)
break;
swap(maxHeap[i], maxHeap[ma]);
// Loop downwards heapification
i = ma;
}
}
```
=== "Java"

View File

@ -96,7 +96,24 @@ Example code is as follows:
=== "C++"
```cpp title="top_k.cpp"
[class]{}-[func]{topKHeap}
/* Using heap to find the largest k elements in an array */
priority_queue<int, vector<int>, greater<int>> topKHeap(vector<int> &nums, int k) {
// Initialize min-heap
priority_queue<int, vector<int>, greater<int>> heap;
// Enter the first k elements of the array into the heap
for (int i = 0; i < k; i++) {
heap.push(nums[i]);
}
// From the k+1th element, keep the heap length as k
for (int i = k; i < nums.size(); i++) {
// If the current element is larger than the heap top element, remove the heap top element and enter the current element into the heap
if (nums[i] > heap.top()) {
heap.pop();
heap.push(nums[i]);
}
}
return heap;
}
```
=== "Java"

View File

@ -76,7 +76,23 @@ The code is as follows:
=== "C++"
```cpp title="binary_search.cpp"
[class]{}-[func]{binarySearch}
/* Binary search (double closed interval) */
int binarySearch(vector<int> &nums, int target) {
// Initialize double closed interval [0, n-1], i.e., i, j point to the first element and last element of the array respectively
int i = 0, j = nums.size() - 1;
// Loop until the search interval is empty (when i > j, it is empty)
while (i <= j) {
int m = i + (j - i) / 2; // Calculate midpoint index m
if (nums[m] < target) // This situation indicates that target is in the interval [m+1, j]
i = m + 1;
else if (nums[m] > target) // This situation indicates that target is in the interval [i, m-1]
j = m - 1;
else // Found the target element, thus return its index
return m;
}
// Did not find the target element, thus return -1
return -1;
}
```
=== "Java"
@ -199,7 +215,23 @@ We can implement a binary search algorithm with the same functionality based on
=== "C++"
```cpp title="binary_search.cpp"
[class]{}-[func]{binarySearchLCRO}
/* Binary search (left closed right open interval) */
int binarySearchLCRO(vector<int> &nums, int target) {
// Initialize left closed right open interval [0, n), i.e., i, j point to the first element and the last element +1 of the array respectively
int i = 0, j = nums.size();
// Loop until the search interval is empty (when i = j, it is empty)
while (i < j) {
int m = i + (j - i) / 2; // Calculate midpoint index m
if (nums[m] < target) // This situation indicates that target is in the interval [m+1, j)
i = m + 1;
else if (nums[m] > target) // This situation indicates that target is in the interval [i, m)
j = m;
else // Found the target element, thus return its index
return m;
}
// Did not find the target element, thus return -1
return -1;
}
```
=== "Java"

View File

@ -36,7 +36,17 @@ In these cases, simply return $-1$. The code is as follows:
=== "C++"
```cpp title="binary_search_edge.cpp"
[class]{}-[func]{binarySearchLeftEdge}
/* Binary search for the leftmost target */
int binarySearchLeftEdge(vector<int> &nums, int target) {
// Equivalent to finding the insertion point of target
int i = binarySearchInsertion(nums, target);
// Did not find target, thus return -1
if (i == nums.size() || nums[i] != target) {
return -1;
}
// Found target, return index i
return i;
}
```
=== "Java"
@ -158,7 +168,19 @@ Please note, the insertion point returned is $i$, therefore, it should be subtra
=== "C++"
```cpp title="binary_search_edge.cpp"
[class]{}-[func]{binarySearchRightEdge}
/* Binary search for the rightmost target */
int binarySearchRightEdge(vector<int> &nums, int target) {
// Convert to finding the leftmost target + 1
int i = binarySearchInsertion(nums, target + 1);
// j points to the rightmost target, i points to the first element greater than target
int j = i - 1;
// Did not find target, thus return -1
if (j == -1 || nums[j] != target) {
return -1;
}
// Found target, return index j
return j;
}
```
=== "Java"

View File

@ -49,7 +49,22 @@ Therefore, at the end of the binary, it is certain that: $i$ points to the first
=== "C++"
```cpp title="binary_search_insertion.cpp"
[class]{}-[func]{binarySearchInsertionSimple}
/* Binary search for insertion point (no duplicate elements) */
int binarySearchInsertionSimple(vector<int> &nums, int target) {
int i = 0, j = nums.size() - 1; // Initialize double closed interval [0, n-1]
while (i <= j) {
int m = i + (j - i) / 2; // Calculate midpoint index m
if (nums[m] < target) {
i = m + 1; // Target is in interval [m+1, j]
} else if (nums[m] > target) {
j = m - 1; // Target is in interval [i, m-1]
} else {
return m; // Found target, return insertion point m
}
}
// Did not find target, return insertion point i
return i;
}
```
=== "Java"
@ -216,7 +231,22 @@ Even so, we can still keep the conditions expanded, as their logic is clearer an
=== "C++"
```cpp title="binary_search_insertion.cpp"
[class]{}-[func]{binarySearchInsertion}
/* Binary search for insertion point (with duplicate elements) */
int binarySearchInsertion(vector<int> &nums, int target) {
int i = 0, j = nums.size() - 1; // Initialize double closed interval [0, n-1]
while (i <= j) {
int m = i + (j - i) / 2; // Calculate midpoint index m
if (nums[m] < target) {
i = m + 1; // Target is in interval [m+1, j]
} else if (nums[m] > target) {
j = m - 1; // Target is in interval [i, m-1]
} else {
j = m - 1; // First element less than target is in interval [i, m-1]
}
}
// Return insertion point i
return i;
}
```
=== "Java"

View File

@ -36,7 +36,18 @@ The code is shown below:
=== "C++"
```cpp title="two_sum.cpp"
[class]{}-[func]{twoSumBruteForce}
/* Method one: Brute force enumeration */
vector<int> twoSumBruteForce(vector<int> &nums, int target) {
int size = nums.size();
// Two-layer loop, time complexity is O(n^2)
for (int i = 0; i < size - 1; i++) {
for (int j = i + 1; j < size; j++) {
if (nums[i] + nums[j] == target)
return {i, j};
}
}
return {};
}
```
=== "Java"
@ -126,7 +137,7 @@ This method has a time complexity of $O(n^2)$ and a space complexity of $O(1)$,
## 10.4.2 &nbsp; Hash search: trading space for time
Consider using a hash table, with key-value pairs being the array elements and their indices, respectively. Loop through the array, performing the steps shown in the figures below each round.
Consider using a hash table, with key-value pairs being the array elements and their indices, respectively. Loop through the array, performing the steps shown in Figure 10-10 each round.
1. Check if the number `target - nums[i]` is in the hash table. If so, directly return the indices of these two elements.
2. Add the key-value pair `nums[i]` and index `i` to the hash table.
@ -162,7 +173,20 @@ The implementation code is shown below, requiring only a single loop:
=== "C++"
```cpp title="two_sum.cpp"
[class]{}-[func]{twoSumHashTable}
/* Method two: Auxiliary hash table */
vector<int> twoSumHashTable(vector<int> &nums, int target) {
int size = nums.size();
// Auxiliary hash table, space complexity is O(n)
unordered_map<int, int> dic;
// Single-layer loop, time complexity is O(n)
for (int i = 0; i < size; i++) {
if (dic.find(target - nums[i]) != dic.end()) {
return {dic[target - nums[i]], i};
}
dic.emplace(nums[i], i);
}
return {};
}
```
=== "Java"

View File

@ -64,7 +64,20 @@ Example code is as follows:
=== "C++"
```cpp title="bubble_sort.cpp"
[class]{}-[func]{bubbleSort}
/* Bubble sort */
void bubbleSort(vector<int> &nums) {
// Outer loop: unsorted range is [0, i]
for (int i = nums.size() - 1; i > 0; i--) {
// Inner loop: swap the largest element in the unsorted range [0, i] to the right end of the range
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// Swap nums[j] and nums[j + 1]
// Here, the std
swap(nums[j], nums[j + 1]);
}
}
}
}
```
=== "Java"
@ -181,7 +194,24 @@ Even after optimization, the worst-case time complexity and average time complex
=== "C++"
```cpp title="bubble_sort.cpp"
[class]{}-[func]{bubbleSortWithFlag}
/* Bubble sort (optimized with flag)*/
void bubbleSortWithFlag(vector<int> &nums) {
// Outer loop: unsorted range is [0, i]
for (int i = nums.size() - 1; i > 0; i--) {
bool flag = false; // Initialize flag
// Inner loop: swap the largest element in the unsorted range [0, i] to the right end of the range
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j + 1]) {
// Swap nums[j] and nums[j + 1]
// Here, the std
swap(nums[j], nums[j + 1]);
flag = true; // Record swapped elements
}
}
if (!flag)
break; // If no elements were swapped in this round of "bubbling", exit
}
}
```
=== "Java"

View File

@ -51,7 +51,31 @@ The code is shown as follows:
=== "C++"
```cpp title="bucket_sort.cpp"
[class]{}-[func]{bucketSort}
/* Bucket sort */
void bucketSort(vector<float> &nums) {
// Initialize k = n/2 buckets, expected to allocate 2 elements per bucket
int k = nums.size() / 2;
vector<vector<float>> buckets(k);
// 1. Distribute array elements into various buckets
for (float num : nums) {
// Input data range is [0, 1), use num * k to map to index range [0, k-1]
int i = num * k;
// Add number to bucket_idx
buckets[i].push_back(num);
}
// 2. Sort each bucket
for (vector<float> &bucket : buckets) {
// Use built-in sorting function, can also replace with other sorting algorithms
sort(bucket.begin(), bucket.end());
}
// 3. Traverse buckets to merge results
int i = 0;
for (vector<float> &bucket : buckets) {
for (float num : bucket) {
nums[i++] = num;
}
}
}
```
=== "Java"

View File

@ -8,7 +8,7 @@ comments: true
## 11.9.1 &nbsp; Simple implementation
Let's start with a simple example. Given an array `nums` of length $n$, where all elements are "non-negative integers", the overall process of counting sort is illustrated in the following diagram.
Let's start with a simple example. Given an array `nums` of length $n$, where all elements are "non-negative integers", the overall process of counting sort is illustrated in Figure 11-16.
1. Traverse the array to find the maximum number, denoted as $m$, then create an auxiliary array `counter` of length $m + 1$.
2. **Use `counter` to count the occurrence of each number in `nums`**, where `counter[num]` corresponds to the occurrence of the number `num`. The counting method is simple, just traverse `nums` (suppose the current number is `num`), and increase `counter[num]` by $1$ each round.
@ -46,7 +46,28 @@ The code is shown below:
=== "C++"
```cpp title="counting_sort.cpp"
[class]{}-[func]{countingSortNaive}
/* Counting sort */
// Simple implementation, cannot be used for sorting objects
void countingSortNaive(vector<int> &nums) {
// 1. Count the maximum element m in the array
int m = 0;
for (int num : nums) {
m = max(m, num);
}
// 2. Count the occurrence of each digit
// counter[num] represents the occurrence of num
vector<int> counter(m + 1, 0);
for (int num : nums) {
counter[num]++;
}
// 3. Traverse counter, filling each element back into the original array nums
int i = 0;
for (int num = 0; num < m + 1; num++) {
for (int j = 0; j < counter[num]; j++, i++) {
nums[i] = num;
}
}
}
```
=== "Java"
@ -161,7 +182,7 @@ $$
1. Fill `num` into the array `res` at the index `prefix[num] - 1`.
2. Reduce the prefix sum `prefix[num]` by $1$, thus obtaining the next index to place `num`.
After the traversal, the array `res` contains the sorted result, and finally, `res` replaces the original array `nums`. The complete counting sort process is shown in the figures below.
After the traversal, the array `res` contains the sorted result, and finally, `res` replaces the original array `nums`. The complete counting sort process is shown in Figure 11-17.
=== "<1>"
![Counting sort process](counting_sort.assets/counting_sort_step1.png){ class="animation-figure" }
@ -224,7 +245,37 @@ The implementation code of counting sort is shown below:
=== "C++"
```cpp title="counting_sort.cpp"
[class]{}-[func]{countingSort}
/* Counting sort */
// Complete implementation, can sort objects and is a stable sort
void countingSort(vector<int> &nums) {
// 1. Count the maximum element m in the array
int m = 0;
for (int num : nums) {
m = max(m, num);
}
// 2. Count the occurrence of each digit
// counter[num] represents the occurrence of num
vector<int> counter(m + 1, 0);
for (int num : nums) {
counter[num]++;
}
// 3. Calculate the prefix sum of counter, converting "occurrence count" to "tail index"
// counter[num]-1 is the last index where num appears in res
for (int i = 0; i < m; i++) {
counter[i + 1] += counter[i];
}
// 4. Traverse nums in reverse order, placing each element into the result array res
// Initialize the array res to record results
int n = nums.size();
vector<int> res(n);
for (int i = n - 1; i >= 0; i--) {
int num = nums[i];
res[counter[num] - 1] = num; // Place num at the corresponding index
counter[num]--; // Decrement the prefix sum by 1, getting the next index to place num
}
// Use result array res to overwrite the original array nums
nums = res;
}
```
=== "Java"

View File

@ -106,9 +106,42 @@ In the code implementation, we used the sift-down function `sift_down()` from th
=== "C++"
```cpp title="heap_sort.cpp"
[class]{}-[func]{siftDown}
/* Heap length is n, start heapifying node i, from top to bottom */
void siftDown(vector<int> &nums, int n, int i) {
while (true) {
// Determine the largest node among i, l, r, noted as ma
int l = 2 * i + 1;
int r = 2 * i + 2;
int ma = i;
if (l < n && nums[l] > nums[ma])
ma = l;
if (r < n && nums[r] > nums[ma])
ma = r;
// If node i is the largest or indices l, r are out of bounds, no further heapification needed, break
if (ma == i) {
break;
}
// Swap two nodes
swap(nums[i], nums[ma]);
// Loop downwards heapification
i = ma;
}
}
[class]{}-[func]{heapSort}
/* Heap sort */
void heapSort(vector<int> &nums) {
// Build heap operation: heapify all nodes except leaves
for (int i = nums.size() / 2 - 1; i >= 0; --i) {
siftDown(nums, nums.size(), i);
}
// Extract the largest element from the heap and repeat for n-1 rounds
for (int i = nums.size() - 1; i > 0; --i) {
// Swap the root node with the rightmost leaf node (swap the first element with the last element)
swap(nums[0], nums[i]);
// Start heapifying the root node, from top to bottom
siftDown(nums, i, 0);
}
}
```
=== "Java"

View File

@ -48,7 +48,19 @@ Example code is as follows:
=== "C++"
```cpp title="insertion_sort.cpp"
[class]{}-[func]{insertionSort}
/* Insertion sort */
void insertionSort(vector<int> &nums) {
// Outer loop: sorted range is [0, i-1]
for (int i = 1; i < nums.size(); i++) {
int base = nums[i], j = i - 1;
// Inner loop: insert base into the correct position within the sorted range [0, i-1]
while (j >= 0 && nums[j] > base) {
nums[j + 1] = nums[j]; // Move nums[j] to the right by one position
j--;
}
nums[j + 1] = base; // Assign base to the correct position
}
}
```
=== "Java"

View File

@ -109,9 +109,45 @@ The implementation of merge sort is shown in the following code. Note that the i
=== "C++"
```cpp title="merge_sort.cpp"
[class]{}-[func]{merge}
/* Merge left subarray and right subarray */
void merge(vector<int> &nums, int left, int mid, int right) {
// Left subarray interval is [left, mid], right subarray interval is [mid+1, right]
// Create a temporary array tmp to store the merged results
vector<int> tmp(right - left + 1);
// Initialize the start indices of the left and right subarrays
int i = left, j = mid + 1, k = 0;
// While both subarrays still have elements, compare and copy the smaller element into the temporary array
while (i <= mid && j <= right) {
if (nums[i] <= nums[j])
tmp[k++] = nums[i++];
else
tmp[k++] = nums[j++];
}
// Copy the remaining elements of the left and right subarrays into the temporary array
while (i <= mid) {
tmp[k++] = nums[i++];
}
while (j <= right) {
tmp[k++] = nums[j++];
}
// Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval
for (k = 0; k < tmp.size(); k++) {
nums[left + k] = tmp[k];
}
}
[class]{}-[func]{mergeSort}
/* Merge sort */
void mergeSort(vector<int> &nums, int left, int right) {
// Termination condition
if (left >= right)
return; // Terminate recursion when subarray length is 1
// Partition stage
int mid = (left + right) / 2; // Calculate midpoint
mergeSort(nums, left, mid); // Recursively process the left subarray
mergeSort(nums, mid + 1, right); // Recursively process the right subarray
// Merge stage
merge(nums, left, mid, right);
}
```
=== "Java"

View File

@ -6,7 +6,7 @@ comments: true
<u>Quick sort</u> is a sorting algorithm based on the divide and conquer strategy, known for its efficiency and wide application.
The core operation of quick sort is "pivot partitioning," aiming to: select an element from the array as the "pivot," move all elements smaller than the pivot to its left, and move elements greater than the pivot to its right. Specifically, the pivot partitioning process is illustrated as follows.
The core operation of quick sort is "pivot partitioning," aiming to: select an element from the array as the "pivot," move all elements smaller than the pivot to its left, and move elements greater than the pivot to its right. Specifically, the pivot partitioning process is illustrated in Figure 11-8.
1. Select the leftmost element of the array as the pivot, and initialize two pointers `i` and `j` at both ends of the array.
2. Set up a loop where each round uses `i` (`j`) to find the first element larger (smaller) than the pivot, then swap these two elements.
@ -69,9 +69,27 @@ After the pivot partitioning, the original array is divided into three parts: le
=== "C++"
```cpp title="quick_sort.cpp"
[class]{QuickSort}-[func]{swap}
/* Swap elements */
void swap(vector<int> &nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
[class]{QuickSort}-[func]{partition}
/* Partition */
int partition(vector<int> &nums, int left, int right) {
// Use nums[left] as the pivot
int i = left, j = right;
while (i < j) {
while (i < j && nums[j] >= nums[left])
j--; // Search from right to left for the first element smaller than the pivot
while (i < j && nums[i] <= nums[left])
i++; // Search from left to right for the first element greater than the pivot
swap(nums, i, j); // Swap these two elements
}
swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays
return i; // Return the index of the pivot
}
```
=== "Java"
@ -210,7 +228,17 @@ The overall process of quick sort is shown in Figure 11-9.
=== "C++"
```cpp title="quick_sort.cpp"
[class]{QuickSort}-[func]{quickSort}
/* Quick sort */
void quickSort(vector<int> &nums, int left, int right) {
// Terminate recursion when subarray length is 1
if (left >= right)
return;
// Partition
int pivot = partition(nums, left, right);
// Recursively process the left subarray and right subarray
quickSort(nums, left, pivot - 1);
quickSort(nums, pivot + 1, right);
}
```
=== "Java"
@ -356,9 +384,34 @@ Sample code is as follows:
=== "C++"
```cpp title="quick_sort.cpp"
[class]{QuickSortMedian}-[func]{medianThree}
/* Select the median of three candidate elements */
int medianThree(vector<int> &nums, int left, int mid, int right) {
int l = nums[left], m = nums[mid], r = nums[right];
if ((l <= m && m <= r) || (r <= m && m <= l))
return mid; // m is between l and r
if ((m <= l && l <= r) || (r <= l && l <= m))
return left; // l is between m and r
return right;
}
[class]{QuickSortMedian}-[func]{partition}
/* Partition (median of three) */
int partition(vector<int> &nums, int left, int right) {
// Select the median of three candidate elements
int med = medianThree(nums, left, (left + right) / 2, right);
// Swap the median to the array's leftmost position
swap(nums, left, med);
// Use nums[left] as the pivot
int i = left, j = right;
while (i < j) {
while (i < j && nums[j] >= nums[left])
j--; // Search from right to left for the first element smaller than the pivot
while (i < j && nums[i] <= nums[left])
i++; // Search from left to right for the first element greater than the pivot
swap(nums, i, j); // Swap these two elements
}
swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays
return i; // Return the index of the pivot
}
```
=== "Java"
@ -509,7 +562,22 @@ To prevent the accumulation of stack frame space, we can compare the lengths of
=== "C++"
```cpp title="quick_sort.cpp"
[class]{QuickSortTailCall}-[func]{quickSort}
/* Quick sort (tail recursion optimization) */
void quickSort(vector<int> &nums, int left, int right) {
// Terminate when subarray length is 1
while (left < right) {
// Partition operation
int pivot = partition(nums, left, right);
// Perform quick sort on the shorter of the two subarrays
if (pivot - left < right - pivot) {
quickSort(nums, left, pivot - 1); // Recursively sort the left subarray
left = pivot + 1; // Remaining unsorted interval is [pivot + 1, right]
} else {
quickSort(nums, pivot + 1, right); // Recursively sort the right subarray
right = pivot - 1; // Remaining unsorted interval is [left, pivot - 1]
}
}
}
```
=== "Java"

View File

@ -10,7 +10,7 @@ The previous section introduced counting sort, which is suitable for scenarios w
## 11.10.1 &nbsp; Algorithm process
Taking the student ID data as an example, assuming the least significant digit is the $1^{st}$ and the most significant is the $8^{th}$, the radix sort process is illustrated in the following diagram.
Taking the student ID data as an example, assuming the least significant digit is the $1^{st}$ and the most significant is the $8^{th}$, the radix sort process is illustrated in Figure 11-18.
1. Initialize digit $k = 1$.
2. Perform "counting sort" on the $k^{th}$ digit of the student IDs. After completion, the data will be sorted from smallest to largest based on the $k^{th}$ digit.
@ -79,11 +79,51 @@ Additionally, we need to slightly modify the counting sort code to allow sorting
=== "C++"
```cpp title="radix_sort.cpp"
[class]{}-[func]{digit}
/* Get the k-th digit of element num, where exp = 10^(k-1) */
int digit(int num, int exp) {
// Passing exp instead of k can avoid repeated expensive exponentiation here
return (num / exp) % 10;
}
[class]{}-[func]{countingSortDigit}
/* Counting sort (based on nums k-th digit) */
void countingSortDigit(vector<int> &nums, int exp) {
// Decimal digit range is 0~9, therefore need a bucket array of length 10
vector<int> counter(10, 0);
int n = nums.size();
// Count the occurrence of digits 0~9
for (int i = 0; i < n; i++) {
int d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d
counter[d]++; // Count the occurrence of digit d
}
// Calculate prefix sum, converting "occurrence count" into "array index"
for (int i = 1; i < 10; i++) {
counter[i] += counter[i - 1];
}
// Traverse in reverse, based on bucket statistics, place each element into res
vector<int> res(n, 0);
for (int i = n - 1; i >= 0; i--) {
int d = digit(nums[i], exp);
int j = counter[d] - 1; // Get the index j for d in the array
res[j] = nums[i]; // Place the current element at index j
counter[d]--; // Decrease the count of d by 1
}
// Use result to overwrite the original array nums
for (int i = 0; i < n; i++)
nums[i] = res[i];
}
[class]{}-[func]{radixSort}
/* Radix sort */
void radixSort(vector<int> &nums) {
// Get the maximum element of the array, used to determine the maximum number of digits
int m = *max_element(nums.begin(), nums.end());
// Traverse from the lowest to the highest digit
for (int exp = 1; exp <= m; exp *= 10)
// Perform counting sort on the k-th digit of array elements
// k = 1 -> exp = 1
// k = 2 -> exp = 10
// i.e., exp = 10^(k-1)
countingSortDigit(nums, exp);
}
```
=== "Java"

View File

@ -71,7 +71,21 @@ In the code, we use $k$ to record the smallest element within the unsorted inter
=== "C++"
```cpp title="selection_sort.cpp"
[class]{}-[func]{selectionSort}
/* Selection sort */
void selectionSort(vector<int> &nums) {
int n = nums.size();
// Outer loop: unsorted range is [i, n-1]
for (int i = 0; i < n - 1; i++) {
// Inner loop: find the smallest element within the unsorted range
int k = i;
for (int j = i + 1; j < n; j++) {
if (nums[j] < nums[k])
k = j; // Record the index of the smallest element
}
// Swap the smallest element with the first element of the unsorted range
swap(nums[i], nums[k]);
}
}
```
=== "Java"

View File

@ -4,7 +4,7 @@ comments: true
# 5.3 &nbsp; Double-ended queue
In a queue, we can only delete elements from the head or add elements to the tail. As shown in the following diagram, a <u>double-ended queue (deque)</u> offers more flexibility, allowing the addition or removal of elements at both the head and the tail.
In a queue, we can only delete elements from the head or add elements to the tail. As shown in Figure 5-7, a <u>double-ended queue (deque)</u> offers more flexibility, allowing the addition or removal of elements at both the head and the tail.
![Operations in double-ended queue](deque.assets/deque_operations.png){ class="animation-figure" }
@ -390,7 +390,7 @@ The implementation code is as follows:
def __init__(self, val: int):
"""Constructor"""
self.val: int = val
self.next: ListNode | None = None # Reference to the next node
self.next: ListNode | None = None # Reference to successor node
self.prev: ListNode | None = None # Reference to predecessor node
class LinkedListDeque:
@ -496,9 +496,146 @@ The implementation code is as follows:
=== "C++"
```cpp title="linkedlist_deque.cpp"
[class]{DoublyListNode}-[func]{}
/* Double-linked list node */
struct DoublyListNode {
int val; // Node value
DoublyListNode *next; // Pointer to successor node
DoublyListNode *prev; // Pointer to predecessor node
DoublyListNode(int val) : val(val), prev(nullptr), next(nullptr) {
}
};
[class]{LinkedListDeque}-[func]{}
/* Double-ended queue class based on double-linked list */
class LinkedListDeque {
private:
DoublyListNode *front, *rear; // Front node front, back node rear
int queSize = 0; // Length of the double-ended queue
public:
/* Constructor */
LinkedListDeque() : front(nullptr), rear(nullptr) {
}
/* Destructor */
~LinkedListDeque() {
// Traverse the linked list, remove nodes, free memory
DoublyListNode *pre, *cur = front;
while (cur != nullptr) {
pre = cur;
cur = cur->next;
delete pre;
}
}
/* Get the length of the double-ended queue */
int size() {
return queSize;
}
/* Determine if the double-ended queue is empty */
bool isEmpty() {
return size() == 0;
}
/* Enqueue operation */
void push(int num, bool isFront) {
DoublyListNode *node = new DoublyListNode(num);
// If the list is empty, make front and rear both point to node
if (isEmpty())
front = rear = node;
// Front enqueue operation
else if (isFront) {
// Add node to the head of the list
front->prev = node;
node->next = front;
front = node; // Update head node
// Rear enqueue operation
} else {
// Add node to the tail of the list
rear->next = node;
node->prev = rear;
rear = node; // Update tail node
}
queSize++; // Update queue length
}
/* Front enqueue */
void pushFirst(int num) {
push(num, true);
}
/* Rear enqueue */
void pushLast(int num) {
push(num, false);
}
/* Dequeue operation */
int pop(bool isFront) {
if (isEmpty())
throw out_of_range("Queue is empty");
int val;
// Front dequeue operation
if (isFront) {
val = front->val; // Temporarily store the head node value
// Remove head node
DoublyListNode *fNext = front->next;
if (fNext != nullptr) {
fNext->prev = nullptr;
front->next = nullptr;
}
delete front;
front = fNext; // Update head node
// Rear dequeue operation
} else {
val = rear->val; // Temporarily store the tail node value
// Remove tail node
DoublyListNode *rPrev = rear->prev;
if (rPrev != nullptr) {
rPrev->next = nullptr;
rear->prev = nullptr;
}
delete rear;
rear = rPrev; // Update tail node
}
queSize--; // Update queue length
return val;
}
/* Front dequeue */
int popFirst() {
return pop(true);
}
/* Rear dequeue */
int popLast() {
return pop(false);
}
/* Access front element */
int peekFirst() {
if (isEmpty())
throw out_of_range("Double-ended queue is empty");
return front->val;
}
/* Access rear element */
int peekLast() {
if (isEmpty())
throw out_of_range("Double-ended queue is empty");
return rear->val;
}
/* Return array for printing */
vector<int> toVector() {
DoublyListNode *node = front;
vector<int> res(size());
for (int i = 0; i < res.size(); i++) {
res[i] = node->val;
node = node->next;
}
return res;
}
};
```
=== "Java"
@ -507,7 +644,7 @@ The implementation code is as follows:
/* Double-linked list node */
class ListNode {
int val; // Node value
ListNode next; // Reference to the next node
ListNode next; // Reference to successor node
ListNode prev; // Reference to predecessor node
ListNode(int val) {
@ -837,7 +974,112 @@ The implementation only needs to add methods for "front enqueue" and "rear deque
=== "C++"
```cpp title="array_deque.cpp"
[class]{ArrayDeque}-[func]{}
/* Double-ended queue class based on circular array */
class ArrayDeque {
private:
vector<int> nums; // Array used to store elements of the double-ended queue
int front; // Front pointer, pointing to the front element
int queSize; // Length of the double-ended queue
public:
/* Constructor */
ArrayDeque(int capacity) {
nums.resize(capacity);
front = queSize = 0;
}
/* Get the capacity of the double-ended queue */
int capacity() {
return nums.size();
}
/* Get the length of the double-ended queue */
int size() {
return queSize;
}
/* Determine if the double-ended queue is empty */
bool isEmpty() {
return queSize == 0;
}
/* Calculate circular array index */
int index(int i) {
// Implement circular array by modulo operation
// When i exceeds the tail of the array, return to the head
// When i exceeds the head of the array, return to the tail
return (i + capacity()) % capacity();
}
/* Front enqueue */
void pushFirst(int num) {
if (queSize == capacity()) {
cout << "Double-ended queue is full" << endl;
return;
}
// Move the front pointer one position to the left
// Implement front crossing the head of the array to return to the tail by modulo operation
front = index(front - 1);
// Add num to the front
nums[front] = num;
queSize++;
}
/* Rear enqueue */
void pushLast(int num) {
if (queSize == capacity()) {
cout << "Double-ended queue is full" << endl;
return;
}
// Calculate rear pointer, pointing to rear index + 1
int rear = index(front + queSize);
// Add num to the rear
nums[rear] = num;
queSize++;
}
/* Front dequeue */
int popFirst() {
int num = peekFirst();
// Move front pointer one position backward
front = index(front + 1);
queSize--;
return num;
}
/* Rear dequeue */
int popLast() {
int num = peekLast();
queSize--;
return num;
}
/* Access front element */
int peekFirst() {
if (isEmpty())
throw out_of_range("Double-ended queue is empty");
return nums[front];
}
/* Access rear element */
int peekLast() {
if (isEmpty())
throw out_of_range("Double-ended queue is empty");
// Calculate rear element index
int last = index(front + queSize - 1);
return nums[last];
}
/* Return array for printing */
vector<int> toVector() {
// Only convert elements within valid length range
vector<int> res(queSize);
for (int i = 0, j = front; i < queSize; i++, j++) {
res[i] = nums[index(j)];
}
return res;
}
};
```
=== "Java"

View File

@ -411,7 +411,81 @@ Below is the code for implementing a queue using a linked list:
=== "C++"
```cpp title="linkedlist_queue.cpp"
[class]{LinkedListQueue}-[func]{}
/* Queue class based on linked list */
class LinkedListQueue {
private:
ListNode *front, *rear; // Front node front, back node rear
int queSize;
public:
LinkedListQueue() {
front = nullptr;
rear = nullptr;
queSize = 0;
}
~LinkedListQueue() {
// Traverse the linked list, remove nodes, free memory
freeMemoryLinkedList(front);
}
/* Get the length of the queue */
int size() {
return queSize;
}
/* Determine if the queue is empty */
bool isEmpty() {
return queSize == 0;
}
/* Enqueue */
void push(int num) {
// Add num behind the tail node
ListNode *node = new ListNode(num);
// If the queue is empty, make the head and tail nodes both point to that node
if (front == nullptr) {
front = node;
rear = node;
}
// If the queue is not empty, add that node behind the tail node
else {
rear->next = node;
rear = node;
}
queSize++;
}
/* Dequeue */
int pop() {
int num = peek();
// Remove head node
ListNode *tmp = front;
front = front->next;
// Free memory
delete tmp;
queSize--;
return num;
}
/* Access front element */
int peek() {
if (size() == 0)
throw out_of_range("Queue is empty");
return front->val;
}
/* Convert the linked list to Vector and return */
vector<int> toVector() {
ListNode *node = front;
vector<int> res(size());
for (int i = 0; i < res.size(); i++) {
res[i] = node->val;
node = node->next;
}
return res;
}
};
```
=== "Java"
@ -638,7 +712,81 @@ In a circular array, `front` or `rear` needs to loop back to the start of the ar
=== "C++"
```cpp title="array_queue.cpp"
[class]{ArrayQueue}-[func]{}
/* Queue class based on circular array */
class ArrayQueue {
private:
int *nums; // Array for storing queue elements
int front; // Front pointer, pointing to the front element
int queSize; // Queue length
int queCapacity; // Queue capacity
public:
ArrayQueue(int capacity) {
// Initialize an array
nums = new int[capacity];
queCapacity = capacity;
front = queSize = 0;
}
~ArrayQueue() {
delete[] nums;
}
/* Get the capacity of the queue */
int capacity() {
return queCapacity;
}
/* Get the length of the queue */
int size() {
return queSize;
}
/* Determine if the queue is empty */
bool isEmpty() {
return size() == 0;
}
/* Enqueue */
void push(int num) {
if (queSize == queCapacity) {
cout << "Queue is full" << endl;
return;
}
// Calculate rear pointer, pointing to rear index + 1
// Use modulo operation to wrap the rear pointer from the end of the array back to the start
int rear = (front + queSize) % queCapacity;
// Add num to the rear
nums[rear] = num;
queSize++;
}
/* Dequeue */
int pop() {
int num = peek();
// Move front pointer one position backward, returning to the head of the array if it exceeds the tail
front = (front + 1) % queCapacity;
queSize--;
return num;
}
/* Access front element */
int peek() {
if (isEmpty())
throw out_of_range("Queue is empty");
return nums[front];
}
/* Convert array to Vector and return */
vector<int> toVector() {
// Only convert elements within valid length range
vector<int> arr(queSize);
for (int i = 0, j = front; i < queSize; i++, j++) {
arr[i] = nums[j % queCapacity];
}
return arr;
}
};
```
=== "Java"

View File

@ -401,7 +401,70 @@ Below is an example code for implementing a stack based on a linked list:
=== "C++"
```cpp title="linkedlist_stack.cpp"
[class]{LinkedListStack}-[func]{}
/* Stack class based on linked list */
class LinkedListStack {
private:
ListNode *stackTop; // Use the head node as the top of the stack
int stkSize; // Length of the stack
public:
LinkedListStack() {
stackTop = nullptr;
stkSize = 0;
}
~LinkedListStack() {
// Traverse the linked list, remove nodes, free memory
freeMemoryLinkedList(stackTop);
}
/* Get the length of the stack */
int size() {
return stkSize;
}
/* Determine if the stack is empty */
bool isEmpty() {
return size() == 0;
}
/* Push */
void push(int num) {
ListNode *node = new ListNode(num);
node->next = stackTop;
stackTop = node;
stkSize++;
}
/* Pop */
int pop() {
int num = top();
ListNode *tmp = stackTop;
stackTop = stackTop->next;
// Free memory
delete tmp;
stkSize--;
return num;
}
/* Access stack top element */
int top() {
if (isEmpty())
throw out_of_range("Stack is empty");
return stackTop->val;
}
/* Convert the List to Array and return */
vector<int> toVector() {
ListNode *node = stackTop;
vector<int> res(size());
for (int i = res.size() - 1; i >= 0; i--) {
res[i] = node->val;
node = node->next;
}
return res;
}
};
```
=== "Java"
@ -587,7 +650,46 @@ Since the elements to be pushed onto the stack may continuously increase, we can
=== "C++"
```cpp title="array_stack.cpp"
[class]{ArrayStack}-[func]{}
/* Stack class based on array */
class ArrayStack {
private:
vector<int> stack;
public:
/* Get the length of the stack */
int size() {
return stack.size();
}
/* Determine if the stack is empty */
bool isEmpty() {
return stack.size() == 0;
}
/* Push */
void push(int num) {
stack.push_back(num);
}
/* Pop */
int pop() {
int num = top();
stack.pop_back();
return num;
}
/* Access stack top element */
int top() {
if (isEmpty())
throw out_of_range("Stack is empty");
return stack.back();
}
/* Return Vector */
vector<int> toVector() {
return stack;
}
};
```
=== "Java"

View File

@ -237,7 +237,95 @@ The following code implements a binary tree based on array representation, inclu
=== "C++"
```cpp title="array_binary_tree.cpp"
[class]{ArrayBinaryTree}-[func]{}
/* Array-based binary tree class */
class ArrayBinaryTree {
public:
/* Constructor */
ArrayBinaryTree(vector<int> arr) {
tree = arr;
}
/* List capacity */
int size() {
return tree.size();
}
/* Get the value of the node at index i */
int val(int i) {
// If index is out of bounds, return INT_MAX, representing a null
if (i < 0 || i >= size())
return INT_MAX;
return tree[i];
}
/* Get the index of the left child of the node at index i */
int left(int i) {
return 2 * i + 1;
}
/* Get the index of the right child of the node at index i */
int right(int i) {
return 2 * i + 2;
}
/* Get the index of the parent of the node at index i */
int parent(int i) {
return (i - 1) / 2;
}
/* Level-order traversal */
vector<int> levelOrder() {
vector<int> res;
// Traverse array
for (int i = 0; i < size(); i++) {
if (val(i) != INT_MAX)
res.push_back(val(i));
}
return res;
}
/* Pre-order traversal */
vector<int> preOrder() {
vector<int> res;
dfs(0, "pre", res);
return res;
}
/* In-order traversal */
vector<int> inOrder() {
vector<int> res;
dfs(0, "in", res);
return res;
}
/* Post-order traversal */
vector<int> postOrder() {
vector<int> res;
dfs(0, "post", res);
return res;
}
private:
vector<int> tree;
/* Depth-first traversal */
void dfs(int i, string order, vector<int> &res) {
// If it is an empty spot, return
if (val(i) == INT_MAX)
return;
// Pre-order traversal
if (order == "pre")
res.push_back(val(i));
dfs(left(i), order, res);
// In-order traversal
if (order == "in")
res.push_back(val(i));
dfs(right(i), order, res);
// Post-order traversal
if (order == "post")
res.push_back(val(i));
}
};
```
=== "Java"

View File

@ -252,9 +252,17 @@ The "node height" refers to the distance from that node to its farthest leaf nod
=== "C++"
```cpp title="avl_tree.cpp"
[class]{AVLTree}-[func]{height}
/* Get node height */
int height(TreeNode *node) {
// Empty node height is -1, leaf node height is 0
return node == nullptr ? -1 : node->height;
}
[class]{AVLTree}-[func]{updateHeight}
/* Update node height */
void updateHeight(TreeNode *node) {
// Node height equals the height of the tallest subtree + 1
node->height = max(height(node->left), height(node->right)) + 1;
}
```
=== "Java"
@ -380,7 +388,14 @@ The <u>balance factor</u> of a node is defined as the height of the node's left
=== "C++"
```cpp title="avl_tree.cpp"
[class]{AVLTree}-[func]{balanceFactor}
/* Get balance factor */
int balanceFactor(TreeNode *node) {
// Empty node balance factor is 0
if (node == nullptr)
return 0;
// Node balance factor = left subtree height - right subtree height
return height(node->left) - height(node->right);
}
```
=== "Java"
@ -518,7 +533,19 @@ As shown in Figure 7-27, when the `child` node has a right child (denoted as `gr
=== "C++"
```cpp title="avl_tree.cpp"
[class]{AVLTree}-[func]{rightRotate}
/* Right rotation operation */
TreeNode *rightRotate(TreeNode *node) {
TreeNode *child = node->left;
TreeNode *grandChild = child->right;
// Rotate node to the right around child
child->right = node;
node->left = grandChild;
// Update node height
updateHeight(node);
updateHeight(child);
// Return the root of the subtree after rotation
return child;
}
```
=== "Java"
@ -641,7 +668,19 @@ It can be observed that **the right and left rotation operations are logically s
=== "C++"
```cpp title="avl_tree.cpp"
[class]{AVLTree}-[func]{leftRotate}
/* Left rotation operation */
TreeNode *leftRotate(TreeNode *node) {
TreeNode *child = node->right;
TreeNode *grandChild = child->left;
// Rotate node to the left around child
child->left = node;
node->right = grandChild;
// Update node height
updateHeight(node);
updateHeight(child);
// Return the root of the subtree after rotation
return child;
}
```
=== "Java"
@ -801,7 +840,35 @@ For convenience, we encapsulate the rotation operations into a function. **With
=== "C++"
```cpp title="avl_tree.cpp"
[class]{AVLTree}-[func]{rotate}
/* Perform rotation operation to restore balance to the subtree */
TreeNode *rotate(TreeNode *node) {
// Get the balance factor of node
int _balanceFactor = balanceFactor(node);
// Left-leaning tree
if (_balanceFactor > 1) {
if (balanceFactor(node->left) >= 0) {
// Right rotation
return rightRotate(node);
} else {
// First left rotation then right rotation
node->left = leftRotate(node->left);
return rightRotate(node);
}
}
// Right-leaning tree
if (_balanceFactor < -1) {
if (balanceFactor(node->right) <= 0) {
// Left rotation
return leftRotate(node);
} else {
// First right rotation then left rotation
node->right = rightRotate(node->right);
return leftRotate(node);
}
}
// Balanced tree, no rotation needed, return
return node;
}
```
=== "Java"
@ -938,9 +1005,28 @@ The node insertion operation in AVL trees is similar to that in binary search tr
=== "C++"
```cpp title="avl_tree.cpp"
[class]{AVLTree}-[func]{insert}
/* Insert node */
void insert(int val) {
root = insertHelper(root, val);
}
[class]{AVLTree}-[func]{insertHelper}
/* Recursively insert node (helper method) */
TreeNode *insertHelper(TreeNode *node, int val) {
if (node == nullptr)
return new TreeNode(val);
/* 1. Find insertion position and insert node */
if (val < node->val)
node->left = insertHelper(node->left, val);
else if (val > node->val)
node->right = insertHelper(node->right, val);
else
return node; // Do not insert duplicate nodes, return
updateHeight(node); // Update node height
/* 2. Perform rotation operation to restore balance to the subtree */
node = rotate(node);
// Return the root node of the subtree
return node;
}
```
=== "Java"
@ -1103,9 +1189,50 @@ Similarly, based on the method of removing nodes in binary search trees, rotatio
=== "C++"
```cpp title="avl_tree.cpp"
[class]{AVLTree}-[func]{remove}
/* Remove node */
void remove(int val) {
root = removeHelper(root, val);
}
[class]{AVLTree}-[func]{removeHelper}
/* Recursively remove node (helper method) */
TreeNode *removeHelper(TreeNode *node, int val) {
if (node == nullptr)
return nullptr;
/* 1. Find and remove the node */
if (val < node->val)
node->left = removeHelper(node->left, val);
else if (val > node->val)
node->right = removeHelper(node->right, val);
else {
if (node->left == nullptr || node->right == nullptr) {
TreeNode *child = node->left != nullptr ? node->left : node->right;
// Number of child nodes = 0, remove node and return
if (child == nullptr) {
delete node;
return nullptr;
}
// Number of child nodes = 1, remove node
else {
delete node;
node = child;
}
} else {
// Number of child nodes = 2, remove the next node in in-order traversal and replace the current node with it
TreeNode *temp = node->right;
while (temp->left != nullptr) {
temp = temp->left;
}
int tempVal = temp->val;
node->right = removeHelper(node->right, temp->val);
node->val = tempVal;
}
}
updateHeight(node); // Update node height
/* 2. Perform rotation operation to restore balance to the subtree */
node = rotate(node);
// Return the root node of the subtree
return node;
}
```
=== "Java"

View File

@ -64,7 +64,24 @@ The search operation in a binary search tree works on the same principle as the
=== "C++"
```cpp title="binary_search_tree.cpp"
[class]{BinarySearchTree}-[func]{search}
/* Search node */
TreeNode *search(int num) {
TreeNode *cur = root;
// Loop find, break after passing leaf nodes
while (cur != nullptr) {
// Target node is in cur's right subtree
if (cur->val < num)
cur = cur->right;
// Target node is in cur's left subtree
else if (cur->val > num)
cur = cur->left;
// Found target node, break loop
else
break;
}
// Return target node
return cur;
}
```
=== "Java"
@ -205,7 +222,34 @@ In the code implementation, note the following two points.
=== "C++"
```cpp title="binary_search_tree.cpp"
[class]{BinarySearchTree}-[func]{insert}
/* Insert node */
void insert(int num) {
// If tree is empty, initialize root node
if (root == nullptr) {
root = new TreeNode(num);
return;
}
TreeNode *cur = root, *pre = nullptr;
// Loop find, break after passing leaf nodes
while (cur != nullptr) {
// Found duplicate node, thus return
if (cur->val == num)
return;
pre = cur;
// Insertion position is in cur's right subtree
if (cur->val < num)
cur = cur->right;
// Insertion position is in cur's left subtree
else
cur = cur->left;
}
// Insert node
TreeNode *node = new TreeNode(num);
if (pre->val < num)
pre->right = node;
else
pre->left = node;
}
```
=== "Java"
@ -401,7 +445,59 @@ The operation of removing a node also uses $O(\log n)$ time, where finding the n
=== "C++"
```cpp title="binary_search_tree.cpp"
[class]{BinarySearchTree}-[func]{remove}
/* Remove node */
void remove(int num) {
// If tree is empty, return
if (root == nullptr)
return;
TreeNode *cur = root, *pre = nullptr;
// Loop find, break after passing leaf nodes
while (cur != nullptr) {
// Found node to be removed, break loop
if (cur->val == num)
break;
pre = cur;
// Node to be removed is in cur's right subtree
if (cur->val < num)
cur = cur->right;
// Node to be removed is in cur's left subtree
else
cur = cur->left;
}
// If no node to be removed, return
if (cur == nullptr)
return;
// Number of child nodes = 0 or 1
if (cur->left == nullptr || cur->right == nullptr) {
// When the number of child nodes = 0 / 1, child = nullptr / that child node
TreeNode *child = cur->left != nullptr ? cur->left : cur->right;
// Remove node cur
if (cur != root) {
if (pre->left == cur)
pre->left = child;
else
pre->right = child;
} else {
// If the removed node is the root, reassign the root
root = child;
}
// Free memory
delete cur;
}
// Number of child nodes = 2
else {
// Get the next node in in-order traversal of cur
TreeNode *tmp = cur->right;
while (tmp->left != nullptr) {
tmp = tmp->left;
}
int tmpVal = tmp->val;
// Recursively remove node tmp
remove(tmp->val);
// Replace cur with tmp
cur->val = tmpVal;
}
}
```
=== "Java"

View File

@ -45,7 +45,24 @@ Breadth-first traversal is usually implemented with the help of a "queue". The q
=== "C++"
```cpp title="binary_tree_bfs.cpp"
[class]{}-[func]{levelOrder}
/* Level-order traversal */
vector<int> levelOrder(TreeNode *root) {
// Initialize queue, add root node
queue<TreeNode *> queue;
queue.push(root);
// Initialize a list to store the traversal sequence
vector<int> vec;
while (!queue.empty()) {
TreeNode *node = queue.front();
queue.pop(); // Queue dequeues
vec.push_back(node->val); // Save node value
if (node->left != nullptr)
queue.push(node->left); // Left child node enqueues
if (node->right != nullptr)
queue.push(node->right); // Right child node enqueues
}
return vec;
}
```
=== "Java"
@ -189,11 +206,35 @@ Depth-first search is usually implemented based on recursion:
=== "C++"
```cpp title="binary_tree_dfs.cpp"
[class]{}-[func]{preOrder}
/* Pre-order traversal */
void preOrder(TreeNode *root) {
if (root == nullptr)
return;
// Visit priority: root node -> left subtree -> right subtree
vec.push_back(root->val);
preOrder(root->left);
preOrder(root->right);
}
[class]{}-[func]{inOrder}
/* In-order traversal */
void inOrder(TreeNode *root) {
if (root == nullptr)
return;
// Visit priority: left subtree -> root node -> right subtree
inOrder(root->left);
vec.push_back(root->val);
inOrder(root->right);
}
[class]{}-[func]{postOrder}
/* Post-order traversal */
void postOrder(TreeNode *root) {
if (root == nullptr)
return;
// Visit priority: left subtree -> right subtree -> root node
postOrder(root->left);
postOrder(root->right);
vec.push_back(root->val);
}
```
=== "Java"