mirror of
https://github.com/krahets/hello-algo.git
synced 2026-03-12 17:51:33 +08:00
build
This commit is contained in:
@@ -181,7 +181,19 @@ comments: true
|
||||
=== "Zig"
|
||||
|
||||
```zig title=""
|
||||
const hello = [5]u8{ 'h', 'e', 'l', 'l', 'o' };
|
||||
// 以上代码展示了定义一个字面量数组的方式,其中你可以选择指明数组的大小或者使用 _ 代替。使用 _ 时,Zig 会尝试自动计算数组的长度
|
||||
|
||||
const matrix_4x4 = [4][4]f32{
|
||||
[_]f32{ 1.0, 0.0, 0.0, 0.0 },
|
||||
[_]f32{ 0.0, 1.0, 0.0, 1.0 },
|
||||
[_]f32{ 0.0, 0.0, 1.0, 0.0 },
|
||||
[_]f32{ 0.0, 0.0, 0.0, 1.0 },
|
||||
};
|
||||
// 多维数组(矩阵)实际上就是嵌套数组,我们很容易就可以创建一个多维数组出来
|
||||
|
||||
const array = [_:0]u8{ 1, 2, 3, 4 };
|
||||
// 定义一个哨兵终止数组,本质上来说,这是为了兼容 C 中的规定的字符串结尾字符\0。我们使用语法 [N:x]T 来描述一个元素为类型 T,长度为 N 的数组,在它对应 N 的索引处的值应该是 x
|
||||
```
|
||||
|
||||
??? pythontutor "可视化运行"
|
||||
|
||||
@@ -1759,7 +1759,8 @@ comments: true
|
||||
if (
|
||||
!this.adjList.has(vet1) ||
|
||||
!this.adjList.has(vet2) ||
|
||||
vet1 === vet2
|
||||
vet1 === vet2 ||
|
||||
this.adjList.get(vet1).indexOf(vet2) === -1
|
||||
) {
|
||||
throw new Error('Illegal Argument Exception');
|
||||
}
|
||||
@@ -1848,7 +1849,8 @@ comments: true
|
||||
if (
|
||||
!this.adjList.has(vet1) ||
|
||||
!this.adjList.has(vet2) ||
|
||||
vet1 === vet2
|
||||
vet1 === vet2 ||
|
||||
this.adjList.get(vet1).indexOf(vet2) === -1
|
||||
) {
|
||||
throw new Error('Illegal Argument Exception');
|
||||
}
|
||||
|
||||
@@ -183,10 +183,7 @@ comments: true
|
||||
// 简单实现,无法用于排序对象
|
||||
function countingSortNaive(nums) {
|
||||
// 1. 统计数组最大元素 m
|
||||
let m = 0;
|
||||
for (const num of nums) {
|
||||
m = Math.max(m, num);
|
||||
}
|
||||
let m = Math.max(...nums);
|
||||
// 2. 统计各数字的出现次数
|
||||
// counter[num] 代表 num 的出现次数
|
||||
const counter = new Array(m + 1).fill(0);
|
||||
@@ -210,10 +207,7 @@ comments: true
|
||||
// 简单实现,无法用于排序对象
|
||||
function countingSortNaive(nums: number[]): void {
|
||||
// 1. 统计数组最大元素 m
|
||||
let m = 0;
|
||||
for (const num of nums) {
|
||||
m = Math.max(m, num);
|
||||
}
|
||||
let m: number = Math.max(...nums);
|
||||
// 2. 统计各数字的出现次数
|
||||
// counter[num] 代表 num 的出现次数
|
||||
const counter: number[] = new Array<number>(m + 1).fill(0);
|
||||
@@ -650,10 +644,7 @@ $$
|
||||
// 完整实现,可排序对象,并且是稳定排序
|
||||
function countingSort(nums) {
|
||||
// 1. 统计数组最大元素 m
|
||||
let m = 0;
|
||||
for (const num of nums) {
|
||||
m = Math.max(m, num);
|
||||
}
|
||||
let m = Math.max(...nums);
|
||||
// 2. 统计各数字的出现次数
|
||||
// counter[num] 代表 num 的出现次数
|
||||
const counter = new Array(m + 1).fill(0);
|
||||
@@ -688,10 +679,7 @@ $$
|
||||
// 完整实现,可排序对象,并且是稳定排序
|
||||
function countingSort(nums: number[]): void {
|
||||
// 1. 统计数组最大元素 m
|
||||
let m = 0;
|
||||
for (const num of nums) {
|
||||
m = Math.max(m, num);
|
||||
}
|
||||
let m: number = Math.max(...nums);
|
||||
// 2. 统计各数字的出现次数
|
||||
// counter[num] 代表 num 的出现次数
|
||||
const counter: number[] = new Array<number>(m + 1).fill(0);
|
||||
|
||||
@@ -388,12 +388,7 @@ $$
|
||||
/* 基数排序 */
|
||||
function radixSort(nums) {
|
||||
// 获取数组的最大元素,用于判断最大位数
|
||||
let m = Number.MIN_VALUE;
|
||||
for (const num of nums) {
|
||||
if (num > m) {
|
||||
m = num;
|
||||
}
|
||||
}
|
||||
let m = Math.max(... nums);
|
||||
// 按照从低位到高位的顺序遍历
|
||||
for (let exp = 1; exp <= m; exp *= 10) {
|
||||
// 对数组元素的第 k 位执行计数排序
|
||||
@@ -445,12 +440,7 @@ $$
|
||||
/* 基数排序 */
|
||||
function radixSort(nums: number[]): void {
|
||||
// 获取数组的最大元素,用于判断最大位数
|
||||
let m = Number.MIN_VALUE;
|
||||
for (const num of nums) {
|
||||
if (num > m) {
|
||||
m = num;
|
||||
}
|
||||
}
|
||||
let m: number = Math.max(... nums);
|
||||
// 按照从低位到高位的顺序遍历
|
||||
for (let exp = 1; exp <= m; exp *= 10) {
|
||||
// 对数组元素的第 k 位执行计数排序
|
||||
|
||||
@@ -6,7 +6,7 @@ comments: true
|
||||
|
||||
<u>Dynamic programming</u> is an important algorithmic paradigm that decomposes a problem into a series of smaller subproblems, and stores the solutions of these subproblems to avoid redundant computations, thereby significantly improving time efficiency.
|
||||
|
||||
In this section, we start with a classic problem, first presenting its brute force backtracking solution, observing the overlapping subproblems contained within, and then gradually deriving a more efficient dynamic programming solution.
|
||||
In this section, we start with a classic problem, first presenting its brute force backtracking solution, identifying the overlapping subproblems, and then gradually deriving a more efficient dynamic programming solution.
|
||||
|
||||
!!! question "Climbing stairs"
|
||||
|
||||
@@ -18,7 +18,7 @@ As shown in Figure 14-1, there are $3$ ways to reach the top of a $3$-step stair
|
||||
|
||||
<p align="center"> Figure 14-1 Number of ways to reach the 3rd step </p>
|
||||
|
||||
The goal of this problem is to determine the number of ways, **considering using backtracking to exhaust all possibilities**. Specifically, imagine climbing stairs as a multi-round choice process: starting from the ground, choosing to go up $1$ or $2$ steps each round, adding one to the count of ways upon reaching the top of the stairs, and pruning the process when exceeding the top. The code is as follows:
|
||||
This problem aims to calculate the number of ways by **using backtracking to exhaust all possibilities**. Specifically, it considers the problem of climbing stairs as a multi-round choice process: starting from the ground, choosing to move up either $1$ or $2$ steps each round, incrementing the count of ways upon reaching the top of the stairs, and pruning the process when it exceeds the top. The code is as follows:
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -195,15 +195,15 @@ The goal of this problem is to determine the number of ways, **considering using
|
||||
|
||||
## 14.1.1 Method 1: Brute force search
|
||||
|
||||
Backtracking algorithms do not explicitly decompose the problem but treat solving the problem as a series of decision steps, searching for all possible solutions through exploration and pruning.
|
||||
Backtracking algorithms do not explicitly decompose the problem into subproblems. Instead, they treat the problem as a sequence of decision steps, exploring all possibilities through trial and pruning.
|
||||
|
||||
We can try to analyze this problem from the perspective of decomposition. Let $dp[i]$ be the number of ways to reach the $i^{th}$ step, then $dp[i]$ is the original problem, and its subproblems include:
|
||||
We can analyze this problem using a decomposition approach. Let $dp[i]$ represent the number of ways to reach the $i^{th}$ step. In this case, $dp[i]$ is the original problem, and its subproblems are:
|
||||
|
||||
$$
|
||||
dp[i-1], dp[i-2], \dots, dp[2], dp[1]
|
||||
$$
|
||||
|
||||
Since each round can only advance $1$ or $2$ steps, when we stand on the $i^{th}$ step, the previous round must have been either on the $i-1^{th}$ or the $i-2^{th}$ step. In other words, we can only step from the $i-1^{th}$ or the $i-2^{th}$ step to the $i^{th}$ step.
|
||||
Since each move can only advance $1$ or $2$ steps, when we stand on the $i^{th}$ step, the previous step must have been either on the $i-1^{th}$ or the $i-2^{th}$ step. In other words, we can only reach the $i^{th}$ from the $i-1^{th}$ or $i-2^{th}$ step.
|
||||
|
||||
This leads to an important conclusion: **the number of ways to reach the $i-1^{th}$ step plus the number of ways to reach the $i-2^{th}$ step equals the number of ways to reach the $i^{th}$ step**. The formula is as follows:
|
||||
|
||||
@@ -217,7 +217,7 @@ This means that in the stair climbing problem, there is a recursive relationship
|
||||
|
||||
<p align="center"> Figure 14-2 Recursive relationship of solution counts </p>
|
||||
|
||||
We can obtain the brute force search solution according to the recursive formula. Starting with $dp[n]$, **recursively decompose a larger problem into the sum of two smaller problems**, until reaching the smallest subproblems $dp[1]$ and $dp[2]$ where the solutions are known, with $dp[1] = 1$ and $dp[2] = 2$, representing $1$ and $2$ ways to climb to the first and second steps, respectively.
|
||||
We can obtain the brute force search solution according to the recursive formula. Starting with $dp[n]$, **we recursively break a larger problem into the sum of two smaller subproblems**, until reaching the smallest subproblems $dp[1]$ and $dp[2]$ where the solutions are known, with $dp[1] = 1$ and $dp[2] = 2$, representing $1$ and $2$ ways to climb to the first and second steps, respectively.
|
||||
|
||||
Observe the following code, which, like standard backtracking code, belongs to depth-first search but is more concise:
|
||||
|
||||
@@ -364,13 +364,13 @@ Observe the following code, which, like standard backtracking code, belongs to d
|
||||
[class]{}-[func]{climbingStairsDFS}
|
||||
```
|
||||
|
||||
Figure 14-3 shows the recursive tree formed by brute force search. For the problem $dp[n]$, the depth of its recursive tree is $n$, with a time complexity of $O(2^n)$. Exponential order represents explosive growth, and entering a long wait if a relatively large $n$ is input.
|
||||
Figure 14-3 shows the recursive tree formed by brute force search. For the problem $dp[n]$, the depth of its recursive tree is $n$, with a time complexity of $O(2^n)$. This exponential growth causes the program to run much more slowly when $n$ is large, leading to long wait times.
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 14-3 Recursive tree for climbing stairs </p>
|
||||
|
||||
Observing Figure 14-3, **the exponential time complexity is caused by 'overlapping subproblems'**. For example, $dp[9]$ is decomposed into $dp[8]$ and $dp[7]$, $dp[8]$ into $dp[7]$ and $dp[6]$, both containing the subproblem $dp[7]$.
|
||||
Observing Figure 14-3, **the exponential time complexity is caused by 'overlapping subproblems'**. For example, $dp[9]$ is broken down into $dp[8]$ and $dp[7]$, and $dp[8]$ is further broken into $dp[7]$ and $dp[6]$, both containing the subproblem $dp[7]$.
|
||||
|
||||
Thus, subproblems include even smaller overlapping subproblems, endlessly. A vast majority of computational resources are wasted on these overlapping subproblems.
|
||||
|
||||
@@ -556,11 +556,11 @@ Observe Figure 14-4, **after memoization, all overlapping subproblems need to be
|
||||
|
||||
## 14.1.3 Method 3: Dynamic programming
|
||||
|
||||
**Memoized search is a 'top-down' method**: we start with the original problem (root node), recursively decompose larger subproblems into smaller ones until the solutions to the smallest known subproblems (leaf nodes) are reached. Subsequently, by backtracking, we collect the solutions of the subproblems, constructing the solution to the original problem.
|
||||
**Memoized search is a 'top-down' method**: we start with the original problem (root node), recursively break larger subproblems into smaller ones until the solutions to the smallest known subproblems (leaf nodes) are reached. Subsequently, by backtracking, we collect the solutions of the subproblems, constructing the solution to the original problem.
|
||||
|
||||
On the contrary, **dynamic programming is a 'bottom-up' method**: starting with the solutions to the smallest subproblems, iteratively construct the solutions to larger subproblems until the original problem is solved.
|
||||
On the contrary, **dynamic programming is a 'bottom-up' method**: starting with the solutions to the smallest subproblems, it iteratively constructs the solutions to larger subproblems until the original problem is solved.
|
||||
|
||||
Since dynamic programming does not include a backtracking process, it only requires looping iteration to implement, without needing recursion. In the following code, we initialize an array `dp` to store the solutions to the subproblems, serving the same recording function as the array `mem` in memoized search:
|
||||
Since dynamic programming does not involve backtracking, it only requires iteration using loops and does not need recursion. In the following code, we initialize an array `dp` to store the solutions to subproblems, serving the same recording function as the array `mem` in memoized search:
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -818,4 +818,4 @@ Observant readers may have noticed that **since $dp[i]$ is only related to $dp[i
|
||||
|
||||
Observing the above code, since the space occupied by the array `dp` is eliminated, the space complexity is reduced from $O(n)$ to $O(1)$.
|
||||
|
||||
In dynamic programming problems, the current state is often only related to a limited number of previous states, allowing us to retain only the necessary states and save memory space by "dimension reduction". **This space optimization technique is known as 'rolling variable' or 'rolling array'**.
|
||||
In many dynamic programming problems, the current state depends only on a limited number of previous states, allowing us to retain only the necessary states and save memory space by "dimension reduction". **This space optimization technique is known as 'rolling variable' or 'rolling array'**.
|
||||
|
||||
@@ -181,7 +181,19 @@ comments: true
|
||||
=== "Zig"
|
||||
|
||||
```zig title=""
|
||||
const hello = [5]u8{ 'h', 'e', 'l', 'l', 'o' };
|
||||
// 以上程式碼展示了定義一個字面量陣列的方式,其中你可以選擇指明陣列的大小或者使用 _ 代替。使用 _ 時,Zig 會嘗試自動計算陣列的長度
|
||||
|
||||
const matrix_4x4 = [4][4]f32{
|
||||
[_]f32{ 1.0, 0.0, 0.0, 0.0 },
|
||||
[_]f32{ 0.0, 1.0, 0.0, 1.0 },
|
||||
[_]f32{ 0.0, 0.0, 1.0, 0.0 },
|
||||
[_]f32{ 0.0, 0.0, 0.0, 1.0 },
|
||||
};
|
||||
// 多維陣列(矩陣)實際上就是巢狀陣列,我們很容易就可以建立一個多維陣列出來
|
||||
|
||||
const array = [_:0]u8{ 1, 2, 3, 4 };
|
||||
// 定義一個哨兵終止陣列,本質上來說,這是為了相容 C 中的規定的字串結尾字元\0。我們使用語法 [N:x]T 來描述一個元素為型別 T,長度為 N 的陣列,在它對應 N 的索引處的值應該是 x
|
||||
```
|
||||
|
||||
??? pythontutor "視覺化執行"
|
||||
|
||||
@@ -1759,7 +1759,8 @@ comments: true
|
||||
if (
|
||||
!this.adjList.has(vet1) ||
|
||||
!this.adjList.has(vet2) ||
|
||||
vet1 === vet2
|
||||
vet1 === vet2 ||
|
||||
this.adjList.get(vet1).indexOf(vet2) === -1
|
||||
) {
|
||||
throw new Error('Illegal Argument Exception');
|
||||
}
|
||||
@@ -1848,7 +1849,8 @@ comments: true
|
||||
if (
|
||||
!this.adjList.has(vet1) ||
|
||||
!this.adjList.has(vet2) ||
|
||||
vet1 === vet2
|
||||
vet1 === vet2 ||
|
||||
this.adjList.get(vet1).indexOf(vet2) === -1
|
||||
) {
|
||||
throw new Error('Illegal Argument Exception');
|
||||
}
|
||||
|
||||
@@ -758,4 +758,4 @@ comments: true
|
||||
|
||||
- 二分搜尋僅適用於有序資料。若輸入資料無序,為了使用二分搜尋而專門進行排序,得不償失。因為排序演算法的時間複雜度通常為 $O(n \log n)$ ,比線性查詢和二分搜尋都更高。對於頻繁插入元素的場景,為保持陣列有序性,需要將元素插入到特定位置,時間複雜度為 $O(n)$ ,也是非常昂貴的。
|
||||
- 二分搜尋僅適用於陣列。二分搜尋需要跳躍式(非連續地)訪問元素,而在鏈結串列中執行跳躍式訪問的效率較低,因此不適合應用在鏈結串列或基於鏈結串列實現的資料結構。
|
||||
- 小資料量下,線性查詢效能更佳。線上性查詢中,每輪只需 1 次判斷操作;而在二分搜尋中,需要 1 次加法、1 次除法、1 ~ 3 次判斷操作、1 次加法(減法),共 4 ~ 6 個單元操作;因此,當資料量 $n$ 較小時,線性查詢反而比二分搜尋更快。
|
||||
- 小資料量下,線性查詢效能更佳。在線性查詢中,每輪只需 1 次判斷操作;而在二分搜尋中,需要 1 次加法、1 次除法、1 ~ 3 次判斷操作、1 次加法(減法),共 4 ~ 6 個單元操作;因此,當資料量 $n$ 較小時,線性查詢反而比二分搜尋更快。
|
||||
|
||||
@@ -183,10 +183,7 @@ comments: true
|
||||
// 簡單實現,無法用於排序物件
|
||||
function countingSortNaive(nums) {
|
||||
// 1. 統計陣列最大元素 m
|
||||
let m = 0;
|
||||
for (const num of nums) {
|
||||
m = Math.max(m, num);
|
||||
}
|
||||
let m = Math.max(...nums);
|
||||
// 2. 統計各數字的出現次數
|
||||
// counter[num] 代表 num 的出現次數
|
||||
const counter = new Array(m + 1).fill(0);
|
||||
@@ -210,10 +207,7 @@ comments: true
|
||||
// 簡單實現,無法用於排序物件
|
||||
function countingSortNaive(nums: number[]): void {
|
||||
// 1. 統計陣列最大元素 m
|
||||
let m = 0;
|
||||
for (const num of nums) {
|
||||
m = Math.max(m, num);
|
||||
}
|
||||
let m: number = Math.max(...nums);
|
||||
// 2. 統計各數字的出現次數
|
||||
// counter[num] 代表 num 的出現次數
|
||||
const counter: number[] = new Array<number>(m + 1).fill(0);
|
||||
@@ -650,10 +644,7 @@ $$
|
||||
// 完整實現,可排序物件,並且是穩定排序
|
||||
function countingSort(nums) {
|
||||
// 1. 統計陣列最大元素 m
|
||||
let m = 0;
|
||||
for (const num of nums) {
|
||||
m = Math.max(m, num);
|
||||
}
|
||||
let m = Math.max(...nums);
|
||||
// 2. 統計各數字的出現次數
|
||||
// counter[num] 代表 num 的出現次數
|
||||
const counter = new Array(m + 1).fill(0);
|
||||
@@ -688,10 +679,7 @@ $$
|
||||
// 完整實現,可排序物件,並且是穩定排序
|
||||
function countingSort(nums: number[]): void {
|
||||
// 1. 統計陣列最大元素 m
|
||||
let m = 0;
|
||||
for (const num of nums) {
|
||||
m = Math.max(m, num);
|
||||
}
|
||||
let m: number = Math.max(...nums);
|
||||
// 2. 統計各數字的出現次數
|
||||
// counter[num] 代表 num 的出現次數
|
||||
const counter: number[] = new Array<number>(m + 1).fill(0);
|
||||
|
||||
@@ -388,12 +388,7 @@ $$
|
||||
/* 基數排序 */
|
||||
function radixSort(nums) {
|
||||
// 獲取陣列的最大元素,用於判斷最大位數
|
||||
let m = Number.MIN_VALUE;
|
||||
for (const num of nums) {
|
||||
if (num > m) {
|
||||
m = num;
|
||||
}
|
||||
}
|
||||
let m = Math.max(... nums);
|
||||
// 按照從低位到高位的順序走訪
|
||||
for (let exp = 1; exp <= m; exp *= 10) {
|
||||
// 對陣列元素的第 k 位執行計數排序
|
||||
@@ -445,12 +440,7 @@ $$
|
||||
/* 基數排序 */
|
||||
function radixSort(nums: number[]): void {
|
||||
// 獲取陣列的最大元素,用於判斷最大位數
|
||||
let m = Number.MIN_VALUE;
|
||||
for (const num of nums) {
|
||||
if (num > m) {
|
||||
m = num;
|
||||
}
|
||||
}
|
||||
let m: number = Math.max(... nums);
|
||||
// 按照從低位到高位的順序走訪
|
||||
for (let exp = 1; exp <= m; exp *= 10) {
|
||||
// 對陣列元素的第 k 位執行計數排序
|
||||
|
||||
Reference in New Issue
Block a user