This commit is contained in:
krahets 2023-08-31 02:30:38 +08:00
parent 5f4a7728b2
commit 00adffaca7
6 changed files with 79 additions and 64 deletions

View File

@ -120,12 +120,7 @@ comments: true
### 2.   访问元素
数组元素被存储在连续的内存空间中,这意味着计算数组元素的内存地址非常容易。给定数组内存地址(即首元素内存地址)和某个元素的索引,我们可以使用以下公式计算得到该元素的内存地址,从而直接访问此元素。
```shell
# 元素内存地址 = 数组内存地址(首元素内存地址) + 元素长度 * 元素索引
elementAddr = firtstElementAddr + elementLength * elementIndex
```
数组元素被存储在连续的内存空间中,这意味着计算数组元素的内存地址非常容易。给定数组内存地址(即首元素内存地址)和某个元素的索引,我们可以使用图 4-2 所示的公式计算得到该元素的内存地址,从而直接访问此元素。
![数组元素的内存地址计算](array.assets/array_memory_location_calculation.png)

View File

@ -25,11 +25,10 @@ comments: true
链表由结点组成,结点之间通过引用(指针)连接,各个结点可以存储不同类型的数据,例如 int、double、string、object 等。
相对地,数组元素则必须是相同类型的,这样才能通过计算偏移量来获取对应元素位置。例如,如果数组同时包含 int 和 long 两种类型,单个元素分别占用 4 bytes 和 8 bytes ,那么此时就不能用以下公式计算偏移量了,因为数组中包含了两种 `elementLength`
相对地,数组元素则必须是相同类型的,这样才能通过计算偏移量来获取对应元素位置。例如,如果数组同时包含 int 和 long 两种类型,单个元素分别占用 4 bytes 和 8 bytes ,那么此时就不能用以下公式计算偏移量了,因为数组中包含了两种长度的元素
```
// 元素内存地址 = 数组内存地址 + 元素长度 * 元素索引
elementAddr = firtstElementAddr + elementLength * elementIndex
```shell
# 元素内存地址 = 数组内存地址 + 元素长度 * 元素索引
```
!!! question "删除节点后,是否需要把 `P.next` 设为 $\text{None}$ 呢?"

View File

@ -541,7 +541,7 @@ $$
/* 带约束爬楼梯:动态规划 */
int climbingStairsConstraintDP(int n) {
if (n == 1 || n == 2) {
return n;
return 1;
}
// 初始化 dp 表,用于存储子问题的解
int[][] dp = new int[n + 1][3];
@ -565,7 +565,7 @@ $$
/* 带约束爬楼梯:动态规划 */
int climbingStairsConstraintDP(int n) {
if (n == 1 || n == 2) {
return n;
return 1;
}
// 初始化 dp 表,用于存储子问题的解
vector<vector<int>> dp(n + 1, vector<int>(3, 0));
@ -589,7 +589,7 @@ $$
def climbing_stairs_constraint_dp(n: int) -> int:
"""带约束爬楼梯:动态规划"""
if n == 1 or n == 2:
return n
return 1
# 初始化 dp 表,用于存储子问题的解
dp = [[0] * 3 for _ in range(n + 1)]
# 初始状态:预设最小子问题的解
@ -608,7 +608,7 @@ $$
/* 带约束爬楼梯:动态规划 */
func climbingStairsConstraintDP(n int) int {
if n == 1 || n == 2 {
return n
return 1
}
// 初始化 dp 表,用于存储子问题的解
dp := make([][3]int, n+1)
@ -632,7 +632,7 @@ $$
/* 带约束爬楼梯:动态规划 */
function climbingStairsConstraintDP(n) {
if (n === 1 || n === 2) {
return n;
return 1;
}
// 初始化 dp 表,用于存储子问题的解
const dp = Array.from(new Array(n + 1), () => new Array(3));
@ -656,7 +656,7 @@ $$
/* 带约束爬楼梯:动态规划 */
function climbingStairsConstraintDP(n: number): number {
if (n === 1 || n === 2) {
return n;
return 1;
}
// 初始化 dp 表,用于存储子问题的解
const dp = Array.from(
@ -689,7 +689,7 @@ $$
/* 带约束爬楼梯:动态规划 */
int climbingStairsConstraintDP(int n) {
if (n == 1 || n == 2) {
return n;
return 1;
}
// 初始化 dp 表,用于存储子问题的解
int[,] dp = new int[n + 1, 3];
@ -713,7 +713,7 @@ $$
/* 带约束爬楼梯:动态规划 */
func climbingStairsConstraintDP(n: Int) -> Int {
if n == 1 || n == 2 {
return n
return 1
}
// 初始化 dp 表,用于存储子问题的解
var dp = Array(repeating: Array(repeating: 0, count: 3), count: n + 1)
@ -737,7 +737,7 @@ $$
// 带约束爬楼梯:动态规划
fn climbingStairsConstraintDP(comptime n: usize) i32 {
if (n == 1 or n == 2) {
return @intCast(n);
return 1;
}
// 初始化 dp 表,用于存储子问题的解
var dp = [_][3]i32{ [_]i32{ -1, -1, -1 } } ** (n + 1);
@ -761,7 +761,7 @@ $$
/* 带约束爬楼梯:动态规划 */
int climbingStairsConstraintDP(int n) {
if (n == 1 || n == 2) {
return n;
return 1;
}
// 初始化 dp 表,用于存储子问题的解
List<List<int>> dp = List.generate(n + 1, (index) => List.filled(3, 0));
@ -784,7 +784,7 @@ $$
```rust title="climbing_stairs_constraint_dp.rs"
/* 带约束爬楼梯:动态规划 */
fn climbing_stairs_constraint_dp(n: usize) -> i32 {
if n == 1 || n == 2 { return n as i32 };
if n == 1 || n == 2 { return 1 };
// 初始化 dp 表,用于存储子问题的解
let mut dp = vec![vec![-1; 3]; n + 1];
// 初始状态:预设最小子问题的解

View File

@ -526,12 +526,14 @@ $$
在实际中,我们通常会用一些标准哈希算法,例如 MD5、SHA-1、SHA-2、SHA3 等。它们可以将任意长度的输入数据映射到恒定长度的哈希值。
近一个世纪以来,哈希算法处在不断升级与优化的过程中。一部分研究人员努力提升哈希算法的性能,另一部分研究人员和黑客则致力于寻找哈希算法的安全性问题。
近一个世纪以来,哈希算法处在不断升级与优化的过程中。一部分研究人员努力提升哈希算法的性能,另一部分研究人员和黑客则致力于寻找哈希算法的安全性问题。表 6-2 展示了在实际应用中常见的哈希算法。
- MD5 和 SHA-1 已多次被成功攻击,因此它们被各类安全应用弃用。
- SHA-2 系列中的 SHA-256 是最安全的哈希算法之一,仍未出现成功的攻击案例,因此常被用在各类安全应用与协议中。
- SHA-3 相较 SHA-2 的实现开销更低、计算效率更高,但目前使用覆盖度不如 SHA-2 系列。
<p align="center"> 表 6-2 &nbsp; 常见的哈希算法 </p>
<div class="center-table" markdown>
| | MD5 | SHA-1 | SHA-2 | SHA-3 |

View File

@ -1186,12 +1186,12 @@ comments: true
由于数组删除首元素的时间复杂度为 $O(n)$ ,这会导致出队操作效率较低。然而,我们可以采用以下巧妙方法来避免这个问题。
我们可以使用一个变量 `front` 指向队首元素的索引,并维护一个变量 `queSize` 用于记录队列长度。定义 `rear = front + queSize` ,这个公式计算出的 `rear` 指向队尾元素之后的下一个位置。
我们可以使用一个变量 `front` 指向队首元素的索引,并维护一个变量 `size` 用于记录队列长度。定义 `rear = front + size` ,这个公式计算出的 `rear` 指向队尾元素之后的下一个位置。
基于此设计,**数组中包含元素的有效区间为 `[front, rear - 1]`**,各种操作的实现方法如图 5-6 所示。
- 入队操作:将输入元素赋值给 `rear` 索引处,并将 `queSize` 增加 1 。
- 出队操作:只需将 `front` 增加 1 ,并将 `queSize` 减少 1 。
- 入队操作:将输入元素赋值给 `rear` 索引处,并将 `size` 增加 1 。
- 出队操作:只需将 `front` 增加 1 ,并将 `size` 减少 1 。
可以看到,入队和出队操作都只需进行一次操作,时间复杂度均为 $O(1)$ 。

View File

@ -92,7 +92,7 @@ comments: true
```python title="binary_search_tree.py"
def search(self, num: int) -> TreeNode | None:
"""查找节点"""
cur: TreeNode | None = self.root
cur = self.__root
# 循环查找,越过叶节点后跳出
while cur is not None:
# 目标节点在 cur 的右子树中
@ -135,8 +135,8 @@ comments: true
```javascript title="binary_search_tree.js"
/* 查找节点 */
function search(num) {
let cur = root;
search(num) {
let cur = this.root;
// 循环查找,越过叶节点后跳出
while (cur !== null) {
// 目标节点在 cur 的右子树中
@ -340,9 +340,11 @@ comments: true
```java title="binary_search_tree.java"
/* 插入节点 */
void insert(int num) {
// 若树为空,直接提前返回
if (root == null)
// 若树为空,则初始化根节点
if (root == null) {
root = new TreeNode(num);
return;
}
TreeNode cur = root, pre = null;
// 循环查找,越过叶节点后跳出
while (cur != null) {
@ -371,9 +373,11 @@ comments: true
```cpp title="binary_search_tree.cpp"
/* 插入节点 */
void insert(int num) {
// 若树为空,直接提前返回
if (root == nullptr)
// 若树为空,则初始化根节点
if (root == nullptr) {
root = new TreeNode(num);
return;
}
TreeNode *cur = root, *pre = nullptr;
// 循环查找,越过叶节点后跳出
while (cur != nullptr) {
@ -402,12 +406,12 @@ comments: true
```python title="binary_search_tree.py"
def insert(self, num: int):
"""插入节点"""
# 若树为空,直接提前返回
if self.root is None:
# 若树为空,则初始化根节点
if self.__root is None:
self.__root = TreeNode(num)
return
# 循环查找,越过叶节点后跳出
cur, pre = self.root, None
cur, pre = self.__root, None
while cur is not None:
# 找到重复节点,直接返回
if cur.val == num:
@ -419,7 +423,6 @@ comments: true
# 插入位置在 cur 的左子树中
else:
cur = cur.left
# 插入节点
node = TreeNode(num)
if pre.val < num:
@ -434,8 +437,9 @@ comments: true
/* 插入节点 */
func (bst *binarySearchTree) insert(num int) {
cur := bst.root
// 若树为空,直接提前返回
// 若树为空,则初始化根节点
if cur == nil {
bst.root = NewTreeNode(num)
return
}
// 待插入节点之前的节点位置
@ -466,10 +470,13 @@ comments: true
```javascript title="binary_search_tree.js"
/* 插入节点 */
function insert(num) {
// 若树为空,直接提前返回
if (root === null) return;
let cur = root,
insert(num) {
// 若树为空,则初始化根节点
if (this.root === null) {
this.root = new TreeNode(num);
return;
}
let cur = this.root,
pre = null;
// 循环查找,越过叶节点后跳出
while (cur !== null) {
@ -493,8 +500,9 @@ comments: true
```typescript title="binary_search_tree.ts"
/* 插入节点 */
function insert(num: number): void {
// 若树为空,直接提前返回
// 若树为空,则初始化根节点
if (root === null) {
root = new TreeNode(num);
return;
}
let cur = root,
@ -526,9 +534,11 @@ comments: true
```c title="binary_search_tree.c"
/* 插入节点 */
void insert(binarySearchTree *bst, int num) {
// 若树为空,直接提前返回
if (bst->root == NULL)
// 若树为空,则初始化根节点
if (bst->root == NULL) {
bst->root = newTreeNode(num);
return;
}
TreeNode *cur = bst->root, *pre = NULL;
// 循环查找,越过叶节点后跳出
while (cur != NULL) {
@ -560,9 +570,11 @@ comments: true
```csharp title="binary_search_tree.cs"
/* 插入节点 */
void insert(int num) {
// 若树为空,直接提前返回
if (root == null)
// 若树为空,则初始化根节点
if (root == null) {
root = new TreeNode(num);
return;
}
TreeNode? cur = root, pre = null;
// 循环查找,越过叶节点后跳出
while (cur != null) {
@ -594,8 +606,9 @@ comments: true
```swift title="binary_search_tree.swift"
/* 插入节点 */
func insert(num: Int) {
// 若树为空,直接提前返回
// 若树为空,则初始化根节点
if root == nil {
root = TreeNode(x: num)
return
}
var cur = root
@ -631,8 +644,11 @@ comments: true
```zig title="binary_search_tree.zig"
// 插入节点
fn insert(self: *Self, num: T) !void {
// 若树为空,直接提前返回
if (self.root == null) return;
// 若树为空,则初始化根节点
if (self.root == null) {
self.root = try self.mem_allocator.create(inc.TreeNode(T));
return;
}
var cur = self.root;
var pre: ?*inc.TreeNode(T) = null;
// 循环查找,越过叶节点后跳出
@ -664,8 +680,11 @@ comments: true
```dart title="binary_search_tree.dart"
/* 插入节点 */
void insert(int num) {
// 若树为空,直接提前返回
if (_root == null) return;
// 若树为空,则初始化根节点
if (_root == null) {
_root = TreeNode(num);
return;
}
TreeNode? cur = _root;
TreeNode? pre = null;
// 循环查找,越过叶节点后跳出
@ -694,8 +713,9 @@ comments: true
```rust title="binary_search_tree.rs"
/* 插入节点 */
pub fn insert(&mut self, num: i32) {
// 若树为空,直接提前返回
// 若树为空,则初始化根节点
if self.root.is_none() {
self.root = TreeNode::new(num);
return;
}
let mut cur = self.root.clone();
@ -891,11 +911,10 @@ comments: true
def remove(self, num: int):
"""删除节点"""
# 若树为空,直接提前返回
if self.root is None:
if self.__root is None:
return
# 循环查找,越过叶节点后跳出
cur, pre = self.root, None
cur, pre = self.__root, None
while cur is not None:
# 找到待删除节点,跳出循环
if cur.val == num:
@ -916,14 +935,14 @@ comments: true
# 当子节点数量 = 0 / 1 时, child = null / 该子节点
child = cur.left or cur.right
# 删除节点 cur
if cur != self.root:
if cur != self.__root:
if pre.left == cur:
pre.left = child
else:
pre.right = child
else:
# 若删除节点为根节点,则重新指定根节点
self.root = child
self.__root = child
# 子节点数量 = 2
else:
# 获取中序遍历中 cur 的下一个节点
@ -1005,10 +1024,10 @@ comments: true
```javascript title="binary_search_tree.js"
/* 删除节点 */
function remove(num) {
remove(num) {
// 若树为空,直接提前返回
if (root === null) return;
let cur = root,
if (this.root === null) return;
let cur = this.root,
pre = null;
// 循环查找,越过叶节点后跳出
while (cur !== null) {
@ -1027,12 +1046,12 @@ comments: true
// 当子节点数量 = 0 / 1 时, child = null / 该子节点
let child = cur.left !== null ? cur.left : cur.right;
// 删除节点 cur
if (cur != root) {
if (cur !== this.root) {
if (pre.left === cur) pre.left = child;
else pre.right = child;
} else {
// 若删除节点为根节点,则重新指定根节点
root = child;
this.root = child;
}
}
// 子节点数量 = 2
@ -1043,7 +1062,7 @@ comments: true
tmp = tmp.left;
}
// 递归删除节点 tmp
remove(tmp.val);
this.remove(tmp.val);
// 用 tmp 覆盖 cur
cur.val = tmp.val;
}