This commit is contained in:
krahets 2023-10-09 05:05:19 +08:00
parent 2f761d9865
commit ec3f58fb70
7 changed files with 144 additions and 25 deletions

View File

@ -601,9 +601,66 @@ comments: true
=== "C"
```c title="n_queens.c"
[class]{}-[func]{backtrack}
/* 放置结果 */
struct result {
char ***data;
int size;
};
[class]{}-[func]{nQueens}
typedef struct result Result;
/* 回溯算法N 皇后 */
void backtrack(int row, int n, char state[MAX_N][MAX_N], Result *res,
bool cols[MAX_N], bool diags1[2 * MAX_N - 1], bool diags2[2 * MAX_N - 1]) {
// 当放置完所有行时,记录解
if (row == n) {
res->data[res->size] = (char **)malloc(sizeof(char *) * n);
for (int i = 0; i < n; ++i) {
res->data[res->size][i] = (char *)malloc(sizeof(char) * (n + 1));
strcpy(res->data[res->size][i], state[i]);
}
res->size++;
return;
}
// 遍历所有列
for (int col = 0; col < n; col++) {
// 计算该格子对应的主对角线和副对角线
int diag1 = row - col + n - 1;
int diag2 = row + col;
// 剪枝:不允许该格子所在列、主对角线、副对角线存在皇后
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
// 尝试:将皇后放置在该格子
state[row][col] = 'Q';
cols[col] = diags1[diag1] = diags2[diag2] = true;
// 放置下一行
backtrack(row + 1, n, state, res, cols, diags1, diags2);
// 回退:将该格子恢复为空位
state[row][col] = '#';
cols[col] = diags1[diag1] = diags2[diag2] = false;
}
}
}
/* 求解 N 皇后 */
Result *nQueens(int n) {
char state[MAX_N][MAX_N];
// 初始化 n*n 大小的棋盘,其中 'Q' 代表皇后,'#' 代表空位
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
state[i][j] = '#';
}
state[i][n] = '\0';
}
bool cols[MAX_N] = {false}; // 记录列是否有皇后
bool diags1[2 * MAX_N - 1] = {false}; // 记录主对角线是否有皇后
bool diags2[2 * MAX_N - 1] = {false}; // 记录副对角线是否有皇后
Result *res = malloc(sizeof(Result));
res->data = (char ***)malloc(sizeof(char **) * MAX_RES);
res->size = 0;
backtrack(0, n, state, res, cols, diags1, diags2);
return res;
}
```
=== "Zig"

View File

@ -936,7 +936,7 @@ comments: true
为解决此问题,**我们需要限制相等元素在每一轮中只被选择一次**。实现方式比较巧妙:由于数组是已排序的,因此相等元素都是相邻的。这意味着在某轮选择中,若当前元素与其左边元素相等,则说明它已经被选择过,因此直接跳过当前元素。
与此同时,**本题规定中的每个数组元素只能被选择一次**。幸运的是,我们也可以利用变量 `start` 来满足该约束:当做出选择 $x_{i}$ 后,设定下一轮从索引 $i + 1$ 开始向后遍历。这样即能去除重复子集,也能避免重复选择元素。
与此同时,**本题规定数组中的每个元素只能被选择一次**。幸运的是,我们也可以利用变量 `start` 来满足该约束:当做出选择 $x_{i}$ 后,设定下一轮从索引 $i + 1$ 开始向后遍历。这样即能去除重复子集,也能避免重复选择元素。
### 2. &nbsp; 代码实现

View File

@ -443,7 +443,7 @@ $$
相较于直接统计算法运行时间,时间复杂度分析有哪些特点呢?
- **时间复杂度能够有效评估算法效率**。例如,算法 `B` 的运行时间呈线性增长,在 $n > 1$ 时比算法 `A` 更慢,在 $n > 1000000$ 时比算法 `C` 更慢。事实上,只要输入数据大小 $n$ 足够大,复杂度为“常数阶”的算法一定优于“线性阶”的算法,这正是时间增长趋势所表达的含义。
- **时间复杂度的推算方法更简便**。显然,运行平台和计算操作类型都与算法运行时间的增长趋势无关。因此在时间复杂度分析中,我们可以简单地将所有计算操作的执行时间视为相同的“单位时间”,从而将“计算操作的运行时间的统计”简化为“计算操作的数量的统计”,这样来估算难度就大大降低了。
- **时间复杂度的推算方法更简便**。显然,运行平台和计算操作类型都与算法运行时间的增长趋势无关。因此在时间复杂度分析中,我们可以简单地将所有计算操作的执行时间视为相同的“单位时间”,从而将“计算操作的运行时间的统计”简化为“计算操作的数量的统计”,这样来估算难度就大大降低了。
- **时间复杂度也存在一定的局限性**。例如,尽管算法 `A``C` 的时间复杂度相同,但实际运行时间差别很大。同样,尽管算法 `B` 的时间复杂度比 `C` 高,但在输入数据大小 $n$ 较小时,算法 `B` 明显优于算法 `C` 。在这些情况下,我们很难仅凭时间复杂度判断算法效率的高低。当然,尽管存在上述问题,复杂度分析仍然是评判算法效率最有效且常用的方法。
## 2.3.2 &nbsp; 函数渐近上界
@ -2597,8 +2597,7 @@ $$
int linearLogRecur(float n) {
if (n <= 1)
return 1;
int count = linearLogRecur(n / 2) +
linearLogRecur(n / 2);
int count = linearLogRecur(n / 2) + linearLogRecur(n / 2);
for (int i = 0; i < n; i++) {
count++;
}
@ -2612,8 +2611,7 @@ $$
/* 线性对数阶 */
int LinearLogRecur(float n) {
if (n <= 1) return 1;
int count = LinearLogRecur(n / 2) +
LinearLogRecur(n / 2);
int count = LinearLogRecur(n / 2) + LinearLogRecur(n / 2);
for (int i = 0; i < n; i++) {
count++;
}
@ -2629,8 +2627,7 @@ $$
if n <= 1 {
return 1
}
count := linearLogRecur(n/2) +
linearLogRecur(n/2)
count := linearLogRecur(n/2) + linearLogRecur(n/2)
for i := 0.0; i < n; i++ {
count++
}
@ -2704,8 +2701,7 @@ $$
if n <= 1.0 {
return 1;
}
let mut count = linear_log_recur(n / 2.0) +
linear_log_recur(n / 2.0);
let mut count = linear_log_recur(n / 2.0) + linear_log_recur(n / 2.0);
for _ in 0 ..n as i32 {
count += 1;
}
@ -2734,8 +2730,7 @@ $$
// 线性对数阶
fn linearLogRecur(n: f32) i32 {
if (n <= 1) return 1;
var count: i32 = linearLogRecur(n / 2) +
linearLogRecur(n / 2);
var count: i32 = linearLogRecur(n / 2) + linearLogRecur(n / 2);
var i: f32 = 0;
while (i < n) : (i += 1) {
count += 1;
@ -3060,11 +3055,8 @@ $$
// 随机打乱数组元素
for (int i = 0; i < nums.Length; i++) {
var index = new Random().Next(i, nums.Length);
var tmp = nums[i];
var ran = nums[index];
nums[i] = ran;
nums[index] = tmp;
int index = new Random().Next(i, nums.Length);
(nums[i], nums[index]) = (nums[index], nums[i]);
}
return nums;
}

View File

@ -809,7 +809,26 @@ $$
=== "C"
```c title="climbing_stairs_constraint_dp.c"
[class]{}-[func]{climbingStairsConstraintDP}
/* 带约束爬楼梯:动态规划 */
int climbingStairsConstraintDP(int n) {
if (n == 1 || n == 2) {
return 1;
}
// 初始化 dp 表,用于存储子问题的解
int dp[n + 1][3];
memset(dp, 0, sizeof(dp));
// 初始状态:预设最小子问题的解
dp[1][1] = 1;
dp[1][2] = 0;
dp[2][1] = 0;
dp[2][2] = 1;
// 状态转移:从较小子问题逐步求解较大子问题
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];
}
```
=== "Zig"

View File

@ -391,7 +391,31 @@ $$
=== "C"
```c title="edit_distance.c"
[class]{}-[func]{editDistanceDP}
/* 编辑距离:动态规划 */
int editDistanceDP(char *s, char *t, int n, int m) {
int dp[n + 1][m + 1];
memset(dp, 0, sizeof(dp));
// 状态转移:首行首列
for (int i = 1; i <= n; i++) {
dp[i][0] = i;
}
for (int j = 1; j <= m; j++) {
dp[0][j] = j;
}
// 状态转移:其余行列
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (s[i - 1] == t[j - 1]) {
// 若两字符相等,则直接跳过此两字符
dp[i][j] = dp[i - 1][j - 1];
} else {
// 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1
dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
}
}
}
return dp[n][m];
}
```
=== "Zig"
@ -812,7 +836,34 @@ $$
=== "C"
```c title="edit_distance.c"
[class]{}-[func]{editDistanceDPComp}
/* 编辑距离:空间优化后的动态规划 */
int editDistanceDPComp(char *s, char *t, int n, int m) {
int dp[m + 1];
memset(dp, 0, sizeof(dp));
// 状态转移:首行
for (int j = 1; j <= m; j++) {
dp[j] = j;
}
// 状态转移:其余行
for (int i = 1; i <= n; i++) {
// 状态转移:首列
int leftup = dp[0]; // 暂存 dp[i-1, j-1]
dp[0] = i;
// 状态转移:其余列
for (int j = 1; j <= m; j++) {
int temp = dp[j];
if (s[i - 1] == t[j - 1]) {
// 若两字符相等,则直接跳过此两字符
dp[j] = leftup;
} else {
// 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1
dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1;
}
leftup = temp; // 更新为下一轮的 dp[i-1, j-1]
}
}
return dp[m];
}
```
=== "Zig"

View File

@ -178,7 +178,7 @@ comments: true
```javascript title="hash_map.js"
/* 初始化哈希表 */
const map = new ArrayHashMap();
const map = new Map();
/* 添加操作 */
// 在哈希表中添加键值对 (key, value)
map.set(12836, '小哈');
@ -349,7 +349,7 @@ comments: true
Console.WriteLine(key);
}
// 单独遍历值 value
foreach (String val in map.Values) {
foreach (string val in map.Values) {
Console.WriteLine(val);
}
```

View File

@ -10,7 +10,7 @@ comments: true
- 对于二叉树中的某个节点,其左(右)子节点及其以下形成的树被称为该节点的左(右)子树。
- 二叉树的相关术语包括根节点、叶节点、层、度、边、高度和深度等。
- 二叉树的初始化、节点插入和节点删除操作与链表操作方法类似。
- 常见的二叉树类型有完美二叉树、完全二叉树、满二叉树和平衡二叉树。完美二叉树是最理想的状态,而链表是退化后的最差状态。
- 常见的二叉树类型有完美二叉树、完全二叉树、满二叉树和平衡二叉树。完美二叉树是最理想的状态,而链表是退化后的最差状态。
- 二叉树可以用数组表示,方法是将节点值和空位按层序遍历顺序排列,并根据父节点与子节点之间的索引映射关系来实现指针。
- 二叉树的层序遍历是一种广度优先搜索方法,它体现了“一圈一圈向外”的分层遍历方式,通常通过队列来实现。
- 前序、中序、后序遍历皆属于深度优先搜索,它们体现了“走到尽头,再回头继续”的回溯遍历方式,通常使用递归来实现。