Move docs/* to docs/zh/*

This commit is contained in:
krahets
2023-10-08 01:33:09 +08:00
parent 400b3914f6
commit 6f7e768cb7
591 changed files with 0 additions and 0 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

View File

@ -1,618 +0,0 @@
# 数组
「数组 array」是一种线性数据结构其将相同类型元素存储在连续的内存空间中。我们将元素在数组中的位置称为该元素的「索引 index」。下图展示了数组的主要术语和概念。
![数组定义与存储方式](array.assets/array_definition.png)
## 数组常用操作
### 初始化数组
我们可以根据需求选用数组的两种初始化方式:无初始值、给定初始值。在未指定初始值的情况下,大多数编程语言会将数组元素初始化为 $0$ 。
=== "Python"
```python title="array.py"
# 初始化数组
arr: list[int] = [0] * 5 # [ 0, 0, 0, 0, 0 ]
nums: list[int] = [1, 3, 2, 5, 4]
```
=== "C++"
```cpp title="array.cpp"
/* 初始化数组 */
// 存储在栈上
int arr[5];
int nums[5] { 1, 3, 2, 5, 4 };
// 存储在堆上(需要手动释放空间)
int* arr1 = new int[5];
int* nums1 = new int[5] { 1, 3, 2, 5, 4 };
```
=== "Java"
```java title="array.java"
/* 初始化数组 */
int[] arr = new int[5]; // { 0, 0, 0, 0, 0 }
int[] nums = { 1, 3, 2, 5, 4 };
```
=== "C#"
```csharp title="array.cs"
/* 初始化数组 */
int[] arr = new int[5]; // { 0, 0, 0, 0, 0 }
int[] nums = { 1, 3, 2, 5, 4 };
```
=== "Go"
```go title="array.go"
/* 初始化数组 */
var arr [5]int
// 在 Go 中,指定长度时([5]int为数组不指定长度时[]int为切片
// 由于 Go 的数组被设计为在编译期确定长度,因此只能使用常量来指定长度
// 为了方便实现扩容 extend() 方法以下将切片Slice看作数组Array
nums := []int{1, 3, 2, 5, 4}
```
=== "Swift"
```swift title="array.swift"
/* 初始化数组 */
let arr = Array(repeating: 0, count: 5) // [0, 0, 0, 0, 0]
let nums = [1, 3, 2, 5, 4]
```
=== "JS"
```javascript title="array.js"
/* 初始化数组 */
var arr = new Array(5).fill(0);
var nums = [1, 3, 2, 5, 4];
```
=== "TS"
```typescript title="array.ts"
/* 初始化数组 */
let arr: number[] = new Array(5).fill(0);
let nums: number[] = [1, 3, 2, 5, 4];
```
=== "Dart"
```dart title="array.dart"
/* 初始化数组 */
List<int> arr = List.filled(5, 0); // [0, 0, 0, 0, 0]
List<int> nums = [1, 3, 2, 5, 4];
```
=== "Rust"
```rust title="array.rs"
/* 初始化数组 */
let arr: Vec<i32> = vec![0; 5]; // [0, 0, 0, 0, 0]
let nums: Vec<i32> = vec![1, 3, 2, 5, 4];
```
=== "C"
```c title="array.c"
/* 初始化数组 */
int arr[5] = { 0 }; // { 0, 0, 0, 0, 0 }
int nums[5] = { 1, 3, 2, 5, 4 };
```
=== "Zig"
```zig title="array.zig"
// 初始化数组
var arr = [_]i32{0} ** 5; // { 0, 0, 0, 0, 0 }
var nums = [_]i32{ 1, 3, 2, 5, 4 };
```
### 访问元素
数组元素被存储在连续的内存空间中,这意味着计算数组元素的内存地址非常容易。给定数组内存地址(即首元素内存地址)和某个元素的索引,我们可以使用下图所示的公式计算得到该元素的内存地址,从而直接访问此元素。
![数组元素的内存地址计算](array.assets/array_memory_location_calculation.png)
观察上图,我们发现数组首个元素的索引为 $0$ ,这似乎有些反直觉,因为从 $1$ 开始计数会更自然。但从地址计算公式的角度看,**索引的含义本质上是内存地址的偏移量**。首个元素的地址偏移量是 $0$ ,因此它的索引为 $0$ 也是合理的。
在数组中访问元素是非常高效的,我们可以在 $O(1)$ 时间内随机访问数组中的任意一个元素。
=== "Python"
```python title="array.py"
[class]{}-[func]{random_access}
```
=== "C++"
```cpp title="array.cpp"
[class]{}-[func]{randomAccess}
```
=== "Java"
```java title="array.java"
[class]{array}-[func]{randomAccess}
```
=== "C#"
```csharp title="array.cs"
[class]{array}-[func]{randomAccess}
```
=== "Go"
```go title="array.go"
[class]{}-[func]{randomAccess}
```
=== "Swift"
```swift title="array.swift"
[class]{}-[func]{randomAccess}
```
=== "JS"
```javascript title="array.js"
[class]{}-[func]{randomAccess}
```
=== "TS"
```typescript title="array.ts"
[class]{}-[func]{randomAccess}
```
=== "Dart"
```dart title="array.dart"
[class]{}-[func]{randomAccess}
```
=== "Rust"
```rust title="array.rs"
[class]{}-[func]{random_access}
```
=== "C"
```c title="array.c"
[class]{}-[func]{randomAccess}
```
=== "Zig"
```zig title="array.zig"
[class]{}-[func]{randomAccess}
```
### 插入元素
数组元素在内存中是“紧挨着的”,它们之间没有空间再存放任何数据。如下图所示,如果想要在数组中间插入一个元素,则需要将该元素之后的所有元素都向后移动一位,之后再把元素赋值给该索引。
![数组插入元素示例](array.assets/array_insert_element.png)
值得注意的是,由于数组的长度是固定的,因此插入一个元素必定会导致数组尾部元素的“丢失”。我们将这个问题的解决方案留在列表章节中讨论。
=== "Python"
```python title="array.py"
[class]{}-[func]{insert}
```
=== "C++"
```cpp title="array.cpp"
[class]{}-[func]{insert}
```
=== "Java"
```java title="array.java"
[class]{array}-[func]{insert}
```
=== "C#"
```csharp title="array.cs"
[class]{array}-[func]{insert}
```
=== "Go"
```go title="array.go"
[class]{}-[func]{insert}
```
=== "Swift"
```swift title="array.swift"
[class]{}-[func]{insert}
```
=== "JS"
```javascript title="array.js"
[class]{}-[func]{insert}
```
=== "TS"
```typescript title="array.ts"
[class]{}-[func]{insert}
```
=== "Dart"
```dart title="array.dart"
[class]{}-[func]{insert}
```
=== "Rust"
```rust title="array.rs"
[class]{}-[func]{insert}
```
=== "C"
```c title="array.c"
[class]{}-[func]{insert}
```
=== "Zig"
```zig title="array.zig"
[class]{}-[func]{insert}
```
### 删除元素
同理,如下图所示,若想要删除索引 $i$ 处的元素,则需要把索引 $i$ 之后的元素都向前移动一位。
![数组删除元素示例](array.assets/array_remove_element.png)
请注意,删除元素完成后,原先末尾的元素变得“无意义”了,所以我们无须特意去修改它。
=== "Python"
```python title="array.py"
[class]{}-[func]{remove}
```
=== "C++"
```cpp title="array.cpp"
[class]{}-[func]{remove}
```
=== "Java"
```java title="array.java"
[class]{array}-[func]{remove}
```
=== "C#"
```csharp title="array.cs"
[class]{array}-[func]{remove}
```
=== "Go"
```go title="array.go"
[class]{}-[func]{remove}
```
=== "Swift"
```swift title="array.swift"
[class]{}-[func]{remove}
```
=== "JS"
```javascript title="array.js"
[class]{}-[func]{remove}
```
=== "TS"
```typescript title="array.ts"
[class]{}-[func]{remove}
```
=== "Dart"
```dart title="array.dart"
[class]{}-[func]{remove}
```
=== "Rust"
```rust title="array.rs"
[class]{}-[func]{remove}
```
=== "C"
```c title="array.c"
[class]{}-[func]{removeItem}
```
=== "Zig"
```zig title="array.zig"
[class]{}-[func]{remove}
```
总的来看,数组的插入与删除操作有以下缺点。
- **时间复杂度高**:数组的插入和删除的平均时间复杂度均为 $O(n)$ ,其中 $n$ 为数组长度。
- **丢失元素**:由于数组的长度不可变,因此在插入元素后,超出数组长度范围的元素会丢失。
- **内存浪费**:我们可以初始化一个比较长的数组,只用前面一部分,这样在插入数据时,丢失的末尾元素都是“无意义”的,但这样做也会造成部分内存空间的浪费。
### 遍历数组
在大多数编程语言中,我们既可以通过索引遍历数组,也可以直接遍历获取数组中的每个元素。
=== "Python"
```python title="array.py"
[class]{}-[func]{traverse}
```
=== "C++"
```cpp title="array.cpp"
[class]{}-[func]{traverse}
```
=== "Java"
```java title="array.java"
[class]{array}-[func]{traverse}
```
=== "C#"
```csharp title="array.cs"
[class]{array}-[func]{traverse}
```
=== "Go"
```go title="array.go"
[class]{}-[func]{traverse}
```
=== "Swift"
```swift title="array.swift"
[class]{}-[func]{traverse}
```
=== "JS"
```javascript title="array.js"
[class]{}-[func]{traverse}
```
=== "TS"
```typescript title="array.ts"
[class]{}-[func]{traverse}
```
=== "Dart"
```dart title="array.dart"
[class]{}-[func]{traverse}
```
=== "Rust"
```rust title="array.rs"
[class]{}-[func]{traverse}
```
=== "C"
```c title="array.c"
[class]{}-[func]{traverse}
```
=== "Zig"
```zig title="array.zig"
[class]{}-[func]{traverse}
```
### 查找元素
在数组中查找指定元素需要遍历数组,每轮判断元素值是否匹配,若匹配则输出对应索引。
因为数组是线性数据结构,所以上述查找操作被称为“线性查找”。
=== "Python"
```python title="array.py"
[class]{}-[func]{find}
```
=== "C++"
```cpp title="array.cpp"
[class]{}-[func]{find}
```
=== "Java"
```java title="array.java"
[class]{array}-[func]{find}
```
=== "C#"
```csharp title="array.cs"
[class]{array}-[func]{find}
```
=== "Go"
```go title="array.go"
[class]{}-[func]{find}
```
=== "Swift"
```swift title="array.swift"
[class]{}-[func]{find}
```
=== "JS"
```javascript title="array.js"
[class]{}-[func]{find}
```
=== "TS"
```typescript title="array.ts"
[class]{}-[func]{find}
```
=== "Dart"
```dart title="array.dart"
[class]{}-[func]{find}
```
=== "Rust"
```rust title="array.rs"
[class]{}-[func]{find}
```
=== "C"
```c title="array.c"
[class]{}-[func]{find}
```
=== "Zig"
```zig title="array.zig"
[class]{}-[func]{find}
```
### 扩容数组
在复杂的系统环境中,程序难以保证数组之后的内存空间是可用的,从而无法安全地扩展数组容量。因此在大多数编程语言中,**数组的长度是不可变的**。
如果我们希望扩容数组,则需重新建立一个更大的数组,然后把原数组元素依次拷贝到新数组。这是一个 $O(n)$ 的操作,在数组很大的情况下是非常耗时的。
=== "Python"
```python title="array.py"
[class]{}-[func]{extend}
```
=== "C++"
```cpp title="array.cpp"
[class]{}-[func]{extend}
```
=== "Java"
```java title="array.java"
[class]{array}-[func]{extend}
```
=== "C#"
```csharp title="array.cs"
[class]{array}-[func]{extend}
```
=== "Go"
```go title="array.go"
[class]{}-[func]{extend}
```
=== "Swift"
```swift title="array.swift"
[class]{}-[func]{extend}
```
=== "JS"
```javascript title="array.js"
[class]{}-[func]{extend}
```
=== "TS"
```typescript title="array.ts"
[class]{}-[func]{extend}
```
=== "Dart"
```dart title="array.dart"
[class]{}-[func]{extend}
```
=== "Rust"
```rust title="array.rs"
[class]{}-[func]{extend}
```
=== "C"
```c title="array.c"
[class]{}-[func]{extend}
```
=== "Zig"
```zig title="array.zig"
[class]{}-[func]{extend}
```
## 数组优点与局限性
数组存储在连续的内存空间内,且元素类型相同。这种做法包含丰富的先验信息,系统可以利用这些信息来优化数据结构的操作效率。
- **空间效率高**: 数组为数据分配了连续的内存块,无须额外的结构开销。
- **支持随机访问**: 数组允许在 $O(1)$ 时间内访问任何元素。
- **缓存局部性**: 当访问数组元素时,计算机不仅会加载它,还会缓存其周围的其他数据,从而借助高速缓存来提升后续操作的执行速度。
连续空间存储是一把双刃剑,其存在以下缺点。
- **插入与删除效率低**:当数组中元素较多时,插入与删除操作需要移动大量的元素。
- **长度不可变**: 数组在初始化后长度就固定了,扩容数组需要将所有数据复制到新数组,开销很大。
- **空间浪费**: 如果数组分配的大小超过了实际所需,那么多余的空间就被浪费了。
## 数组典型应用
数组是一种基础且常见的数据结构,既频繁应用在各类算法之中,也可用于实现各种复杂数据结构。
- **随机访问**:如果我们想要随机抽取一些样本,那么可以用数组存储,并生成一个随机序列,根据索引实现样本的随机抽取。
- **排序和搜索**:数组是排序和搜索算法最常用的数据结构。快速排序、归并排序、二分查找等都主要在数组上进行。
- **查找表**:当我们需要快速查找一个元素或者需要查找一个元素的对应关系时,可以使用数组作为查找表。假如我们想要实现字符到 ASCII 码的映射,则可以将字符的 ASCII 码值作为索引,对应的元素存放在数组中的对应位置。
- **机器学习**:神经网络中大量使用了向量、矩阵、张量之间的线性代数运算,这些数据都是以数组的形式构建的。数组是神经网络编程中最常使用的数据结构。
- **数据结构实现**:数组可以用于实现栈、队列、哈希表、堆、图等数据结构。例如,图的邻接矩阵表示实际上是一个二维数组。

View File

@ -1,13 +0,0 @@
# 数组与链表
<div class="center-table" markdown>
![数组与链表](../assets/covers/chapter_array_and_linkedlist.jpg){ width="600" }
</div>
!!! abstract
数据结构的世界如同一堵厚实的砖墙。
数组的砖块整齐排列,逐个紧贴。链表的砖块分散各处,连接的藤蔓自由地穿梭于砖缝之间。

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

View File

@ -1,952 +0,0 @@
# 链表
内存空间是所有程序的公共资源,在一个复杂的系统运行环境下,空闲的内存空间可能散落在内存各处。我们知道,存储数组的内存空间必须是连续的,而当数组非常大时,内存可能无法提供如此大的连续空间。此时链表的灵活性优势就体现出来了。
「链表 linked list」是一种线性数据结构其中的每个元素都是一个节点对象各个节点通过“引用”相连接。引用记录了下一个节点的内存地址通过它可以从当前节点访问到下一个节点。
链表的设计使得各个节点可以被分散存储在内存各处,它们的内存地址是无须连续的。
![链表定义与存储方式](linked_list.assets/linkedlist_definition.png)
观察上图,链表的组成单位是「节点 node」对象。每个节点都包含两项数据节点的“值”和指向下一节点的“引用”。
- 链表的首个节点被称为“头节点”,最后一个节点被称为“尾节点”。
- 尾节点指向的是“空”,它在 Java、C++ 和 Python 中分别被记为 $\text{null}$、$\text{nullptr}$ 和 $\text{None}$ 。
- 在 C、C++、Go 和 Rust 等支持指针的语言中,上述的“引用”应被替换为“指针”。
如以下代码所示,链表节点 `ListNode` 除了包含值,还需额外保存一个引用(指针)。因此在相同数据量下,**链表比数组占用更多的内存空间**。
=== "Python"
```python title=""
class ListNode:
"""链表节点类"""
def __init__(self, val: int):
self.val: int = val # 节点值
self.next: ListNode | None = None # 指向下一节点的引用
```
=== "C++"
```cpp title=""
/* 链表节点结构体 */
struct ListNode {
int val; // 节点值
ListNode *next; // 指向下一节点的指针
ListNode(int x) : val(x), next(nullptr) {} // 构造函数
};
```
=== "Java"
```java title=""
/* 链表节点类 */
class ListNode {
int val; // 节点值
ListNode next; // 指向下一节点的引用
ListNode(int x) { val = x; } // 构造函数
}
```
=== "C#"
```csharp title=""
/* 链表节点类 */
class ListNode {
int val; // 节点值
ListNode next; // 指向下一节点的引用
ListNode(int x) => val = x; //构造函数
}
```
=== "Go"
```go title=""
/* 链表节点结构体 */
type ListNode struct {
Val int // 节点值
Next *ListNode // 指向下一节点的指针
}
// NewListNode 构造函数,创建一个新的链表
func NewListNode(val int) *ListNode {
return &ListNode{
Val: val,
Next: nil,
}
}
```
=== "Swift"
```swift title=""
/* 链表节点类 */
class ListNode {
var val: Int // 节点值
var next: ListNode? // 指向下一节点的引用
init(x: Int) { // 构造函数
val = x
}
}
```
=== "JS"
```javascript title=""
/* 链表节点类 */
class ListNode {
val;
next;
constructor(val, next) {
this.val = (val === undefined ? 0 : val); // 节点值
this.next = (next === undefined ? null : next); // 指向下一节点的引用
}
}
```
=== "TS"
```typescript title=""
/* 链表节点类 */
class ListNode {
val: number;
next: ListNode | null;
constructor(val?: number, next?: ListNode | null) {
this.val = val === undefined ? 0 : val; // 节点值
this.next = next === undefined ? null : next; // 指向下一节点的引用
}
}
```
=== "Dart"
```dart title=""
/* 链表节点类 */
class ListNode {
int val; // 节点值
ListNode? next; // 指向下一节点的引用
ListNode(this.val, [this.next]); // 构造函数
}
```
=== "Rust"
```rust title=""
use std::rc::Rc;
use std::cell::RefCell;
/* 链表节点类 */
#[derive(Debug)]
struct ListNode {
val: i32, // 节点值
next: Option<Rc<RefCell<ListNode>>>, // 指向下一节点的指针
}
```
=== "C"
```c title=""
/* 链表节点结构体 */
struct ListNode {
int val; // 节点值
struct ListNode *next; // 指向下一节点的指针
};
typedef struct ListNode ListNode;
/* 构造函数 */
ListNode *newListNode(int val) {
ListNode *node, *next;
node = (ListNode *) malloc(sizeof(ListNode));
node->val = val;
node->next = NULL;
return node;
}
```
=== "Zig"
```zig title=""
// 链表节点类
pub fn ListNode(comptime T: type) type {
return struct {
const Self = @This();
val: T = 0, // 节点值
next: ?*Self = null, // 指向下一节点的指针
// 构造函数
pub fn init(self: *Self, x: i32) void {
self.val = x;
self.next = null;
}
};
}
```
## 链表常用操作
### 初始化链表
建立链表分为两步,第一步是初始化各个节点对象,第二步是构建引用指向关系。初始化完成后,我们就可以从链表的头节点出发,通过引用指向 `next` 依次访问所有节点。
=== "Python"
```python title="linked_list.py"
# 初始化链表 1 -> 3 -> 2 -> 5 -> 4
# 初始化各个节点
n0 = ListNode(1)
n1 = ListNode(3)
n2 = ListNode(2)
n3 = ListNode(5)
n4 = ListNode(4)
# 构建引用指向
n0.next = n1
n1.next = n2
n2.next = n3
n3.next = n4
```
=== "C++"
```cpp title="linked_list.cpp"
/* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */
// 初始化各个节点
ListNode* n0 = new ListNode(1);
ListNode* n1 = new ListNode(3);
ListNode* n2 = new ListNode(2);
ListNode* n3 = new ListNode(5);
ListNode* n4 = new ListNode(4);
// 构建引用指向
n0->next = n1;
n1->next = n2;
n2->next = n3;
n3->next = n4;
```
=== "Java"
```java title="linked_list.java"
/* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */
// 初始化各个节点
ListNode n0 = new ListNode(1);
ListNode n1 = new ListNode(3);
ListNode n2 = new ListNode(2);
ListNode n3 = new ListNode(5);
ListNode n4 = new ListNode(4);
// 构建引用指向
n0.next = n1;
n1.next = n2;
n2.next = n3;
n3.next = n4;
```
=== "C#"
```csharp title="linked_list.cs"
/* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */
// 初始化各个节点
ListNode n0 = new ListNode(1);
ListNode n1 = new ListNode(3);
ListNode n2 = new ListNode(2);
ListNode n3 = new ListNode(5);
ListNode n4 = new ListNode(4);
// 构建引用指向
n0.next = n1;
n1.next = n2;
n2.next = n3;
n3.next = n4;
```
=== "Go"
```go title="linked_list.go"
/* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */
// 初始化各个节点
n0 := NewListNode(1)
n1 := NewListNode(3)
n2 := NewListNode(2)
n3 := NewListNode(5)
n4 := NewListNode(4)
// 构建引用指向
n0.Next = n1
n1.Next = n2
n2.Next = n3
n3.Next = n4
```
=== "Swift"
```swift title="linked_list.swift"
/* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */
// 初始化各个节点
let n0 = ListNode(x: 1)
let n1 = ListNode(x: 3)
let n2 = ListNode(x: 2)
let n3 = ListNode(x: 5)
let n4 = ListNode(x: 4)
// 构建引用指向
n0.next = n1
n1.next = n2
n2.next = n3
n3.next = n4
```
=== "JS"
```javascript title="linked_list.js"
/* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */
// 初始化各个节点
const n0 = new ListNode(1);
const n1 = new ListNode(3);
const n2 = new ListNode(2);
const n3 = new ListNode(5);
const n4 = new ListNode(4);
// 构建引用指向
n0.next = n1;
n1.next = n2;
n2.next = n3;
n3.next = n4;
```
=== "TS"
```typescript title="linked_list.ts"
/* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */
// 初始化各个节点
const n0 = new ListNode(1);
const n1 = new ListNode(3);
const n2 = new ListNode(2);
const n3 = new ListNode(5);
const n4 = new ListNode(4);
// 构建引用指向
n0.next = n1;
n1.next = n2;
n2.next = n3;
n3.next = n4;
```
=== "Dart"
```dart title="linked_list.dart"
/* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */\
// 初始化各个节点
ListNode n0 = ListNode(1);
ListNode n1 = ListNode(3);
ListNode n2 = ListNode(2);
ListNode n3 = ListNode(5);
ListNode n4 = ListNode(4);
// 构建引用指向
n0.next = n1;
n1.next = n2;
n2.next = n3;
n3.next = n4;
```
=== "Rust"
```rust title="linked_list.rs"
/* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */
// 初始化各个节点
let n0 = Rc::new(RefCell::new(ListNode { val: 1, next: None }));
let n1 = Rc::new(RefCell::new(ListNode { val: 3, next: None }));
let n2 = Rc::new(RefCell::new(ListNode { val: 2, next: None }));
let n3 = Rc::new(RefCell::new(ListNode { val: 5, next: None }));
let n4 = Rc::new(RefCell::new(ListNode { val: 4, next: None }));
// 构建引用指向
n0.borrow_mut().next = Some(n1.clone());
n1.borrow_mut().next = Some(n2.clone());
n2.borrow_mut().next = Some(n3.clone());
n3.borrow_mut().next = Some(n4.clone());
```
=== "C"
```c title="linked_list.c"
/* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */
// 初始化各个节点
ListNode* n0 = newListNode(1);
ListNode* n1 = newListNode(3);
ListNode* n2 = newListNode(2);
ListNode* n3 = newListNode(5);
ListNode* n4 = newListNode(4);
// 构建引用指向
n0->next = n1;
n1->next = n2;
n2->next = n3;
n3->next = n4;
```
=== "Zig"
```zig title="linked_list.zig"
// 初始化链表
// 初始化各个节点
var n0 = inc.ListNode(i32){.val = 1};
var n1 = inc.ListNode(i32){.val = 3};
var n2 = inc.ListNode(i32){.val = 2};
var n3 = inc.ListNode(i32){.val = 5};
var n4 = inc.ListNode(i32){.val = 4};
// 构建引用指向
n0.next = &n1;
n1.next = &n2;
n2.next = &n3;
n3.next = &n4;
```
数组整体是一个变量,比如数组 `nums` 包含元素 `nums[0]` 和 `nums[1]` 等,而链表是由多个独立的节点对象组成的。**我们通常将头节点当作链表的代称**,比如以上代码中的链表可被记做链表 `n0` 。
### 插入节点
在链表中插入节点非常容易。如下图所示,假设我们想在相邻的两个节点 `n0` 和 `n1` 之间插入一个新节点 `P` **则只需要改变两个节点引用(指针)即可**,时间复杂度为 $O(1)$ 。
相比之下,在数组中插入元素的时间复杂度为 $O(n)$ ,在大数据量下的效率较低。
![链表插入节点示例](linked_list.assets/linkedlist_insert_node.png)
=== "Python"
```python title="linked_list.py"
[class]{}-[func]{insert}
```
=== "C++"
```cpp title="linked_list.cpp"
[class]{}-[func]{insert}
```
=== "Java"
```java title="linked_list.java"
[class]{linked_list}-[func]{insert}
```
=== "C#"
```csharp title="linked_list.cs"
[class]{linked_list}-[func]{insert}
```
=== "Go"
```go title="linked_list.go"
[class]{}-[func]{insertNode}
```
=== "Swift"
```swift title="linked_list.swift"
[class]{}-[func]{insert}
```
=== "JS"
```javascript title="linked_list.js"
[class]{}-[func]{insert}
```
=== "TS"
```typescript title="linked_list.ts"
[class]{}-[func]{insert}
```
=== "Dart"
```dart title="linked_list.dart"
[class]{}-[func]{insert}
```
=== "Rust"
```rust title="linked_list.rs"
[class]{}-[func]{insert}
```
=== "C"
```c title="linked_list.c"
[class]{}-[func]{insert}
```
=== "Zig"
```zig title="linked_list.zig"
[class]{}-[func]{insert}
```
### 删除节点
如下图所示,在链表中删除节点也非常方便,**只需改变一个节点的引用(指针)即可**。
请注意,尽管在删除操作完成后节点 `P` 仍然指向 `n1` ,但实际上遍历此链表已经无法访问到 `P` ,这意味着 `P` 已经不再属于该链表了。
![链表删除节点](linked_list.assets/linkedlist_remove_node.png)
=== "Python"
```python title="linked_list.py"
[class]{}-[func]{remove}
```
=== "C++"
```cpp title="linked_list.cpp"
[class]{}-[func]{remove}
```
=== "Java"
```java title="linked_list.java"
[class]{linked_list}-[func]{remove}
```
=== "C#"
```csharp title="linked_list.cs"
[class]{linked_list}-[func]{remove}
```
=== "Go"
```go title="linked_list.go"
[class]{}-[func]{removeNode}
```
=== "Swift"
```swift title="linked_list.swift"
[class]{}-[func]{remove}
```
=== "JS"
```javascript title="linked_list.js"
[class]{}-[func]{remove}
```
=== "TS"
```typescript title="linked_list.ts"
[class]{}-[func]{remove}
```
=== "Dart"
```dart title="linked_list.dart"
[class]{}-[func]{remove}
```
=== "Rust"
```rust title="linked_list.rs"
[class]{}-[func]{remove}
```
=== "C"
```c title="linked_list.c"
[class]{}-[func]{removeNode}
```
=== "Zig"
```zig title="linked_list.zig"
[class]{}-[func]{remove}
```
### 访问节点
**在链表访问节点的效率较低**。如上节所述,我们可以在 $O(1)$ 时间下访问数组中的任意元素。链表则不然,程序需要从头节点出发,逐个向后遍历,直至找到目标节点。也就是说,访问链表的第 $i$ 个节点需要循环 $i - 1$ 轮,时间复杂度为 $O(n)$ 。
=== "Python"
```python title="linked_list.py"
[class]{}-[func]{access}
```
=== "C++"
```cpp title="linked_list.cpp"
[class]{}-[func]{access}
```
=== "Java"
```java title="linked_list.java"
[class]{linked_list}-[func]{access}
```
=== "C#"
```csharp title="linked_list.cs"
[class]{linked_list}-[func]{access}
```
=== "Go"
```go title="linked_list.go"
[class]{}-[func]{access}
```
=== "Swift"
```swift title="linked_list.swift"
[class]{}-[func]{access}
```
=== "JS"
```javascript title="linked_list.js"
[class]{}-[func]{access}
```
=== "TS"
```typescript title="linked_list.ts"
[class]{}-[func]{access}
```
=== "Dart"
```dart title="linked_list.dart"
[class]{}-[func]{access}
```
=== "Rust"
```rust title="linked_list.rs"
[class]{}-[func]{access}
```
=== "C"
```c title="linked_list.c"
[class]{}-[func]{access}
```
=== "Zig"
```zig title="linked_list.zig"
[class]{}-[func]{access}
```
### 查找节点
遍历链表,查找链表内值为 `target` 的节点,输出节点在链表中的索引。此过程也属于线性查找。
=== "Python"
```python title="linked_list.py"
[class]{}-[func]{find}
```
=== "C++"
```cpp title="linked_list.cpp"
[class]{}-[func]{find}
```
=== "Java"
```java title="linked_list.java"
[class]{linked_list}-[func]{find}
```
=== "C#"
```csharp title="linked_list.cs"
[class]{linked_list}-[func]{find}
```
=== "Go"
```go title="linked_list.go"
[class]{}-[func]{findNode}
```
=== "Swift"
```swift title="linked_list.swift"
[class]{}-[func]{find}
```
=== "JS"
```javascript title="linked_list.js"
[class]{}-[func]{find}
```
=== "TS"
```typescript title="linked_list.ts"
[class]{}-[func]{find}
```
=== "Dart"
```dart title="linked_list.dart"
[class]{}-[func]{find}
```
=== "Rust"
```rust title="linked_list.rs"
[class]{}-[func]{find}
```
=== "C"
```c title="linked_list.c"
[class]{}-[func]{find}
```
=== "Zig"
```zig title="linked_list.zig"
[class]{}-[func]{find}
```
## 数组 VS 链表
下表总结对比了数组和链表的各项特点与操作效率。由于它们采用两种相反的存储策略,因此各种性质和操作效率也呈现对立的特点。
<p align="center"> 表 <id> &nbsp; 数组与链表的效率对比 </p>
| | 数组 | 链表 |
| ---------- | ------------------------ | ------------ |
| 存储方式 | 连续内存空间 | 分散内存空间 |
| 缓存局部性 | 友好 | 不友好 |
| 容量扩展 | 长度不可变 | 可灵活扩展 |
| 内存效率 | 占用内存少、浪费部分空间 | 占用内存多 |
| 访问元素 | $O(1)$ | $O(n)$ |
| 添加元素 | $O(n)$ | $O(1)$ |
| 删除元素 | $O(n)$ | $O(1)$ |
## 常见链表类型
如下图所示,常见的链表类型包括三种。
- **单向链表**:即上述介绍的普通链表。单向链表的节点包含值和指向下一节点的引用两项数据。我们将首个节点称为头节点,将最后一个节点称为尾节点,尾节点指向空 $\text{None}$ 。
- **环形链表**:如果我们令单向链表的尾节点指向头节点(即首尾相接),则得到一个环形链表。在环形链表中,任意节点都可以视作头节点。
- **双向链表**:与单向链表相比,双向链表记录了两个方向的引用。双向链表的节点定义同时包含指向后继节点(下一个节点)和前驱节点(上一个节点)的引用(指针)。相较于单向链表,双向链表更具灵活性,可以朝两个方向遍历链表,但相应地也需要占用更多的内存空间。
=== "Python"
```python title=""
class ListNode:
"""双向链表节点类"""
def __init__(self, val: int):
self.val: int = val # 节点值
self.next: ListNode | None = None # 指向后继节点的引用
self.prev: ListNode | None = None # 指向前驱节点的引用
```
=== "C++"
```cpp title=""
/* 双向链表节点结构体 */
struct ListNode {
int val; // 节点值
ListNode *next; // 指向后继节点的指针
ListNode *prev; // 指向前驱节点的指针
ListNode(int x) : val(x), next(nullptr), prev(nullptr) {} // 构造函数
};
```
=== "Java"
```java title=""
/* 双向链表节点类 */
class ListNode {
int val; // 节点值
ListNode next; // 指向后继节点的引用
ListNode prev; // 指向前驱节点的引用
ListNode(int x) { val = x; } // 构造函数
}
```
=== "C#"
```csharp title=""
/* 双向链表节点类 */
class ListNode {
int val; // 节点值
ListNode next; // 指向后继节点的引用
ListNode prev; // 指向前驱节点的引用
ListNode(int x) => val = x; // 构造函数
}
```
=== "Go"
```go title=""
/* 双向链表节点结构体 */
type DoublyListNode struct {
Val int // 节点值
Next *DoublyListNode // 指向后继节点的指针
Prev *DoublyListNode // 指向前驱节点的指针
}
// NewDoublyListNode 初始化
func NewDoublyListNode(val int) *DoublyListNode {
return &DoublyListNode{
Val: val,
Next: nil,
Prev: nil,
}
}
```
=== "Swift"
```swift title=""
/* 双向链表节点类 */
class ListNode {
var val: Int // 节点值
var next: ListNode? // 指向后继节点的引用
var prev: ListNode? // 指向前驱节点的引用
init(x: Int) { // 构造函数
val = x
}
}
```
=== "JS"
```javascript title=""
/* 双向链表节点类 */
class ListNode {
val;
next;
prev;
constructor(val, next, prev) {
this.val = val === undefined ? 0 : val; // 节点值
this.next = next === undefined ? null : next; // 指向后继节点的引用
this.prev = prev === undefined ? null : prev; // 指向前驱节点的引用
}
}
```
=== "TS"
```typescript title=""
/* 双向链表节点类 */
class ListNode {
val: number;
next: ListNode | null;
prev: ListNode | null;
constructor(val?: number, next?: ListNode | null, prev?: ListNode | null) {
this.val = val === undefined ? 0 : val; // 节点值
this.next = next === undefined ? null : next; // 指向后继节点的引用
this.prev = prev === undefined ? null : prev; // 指向前驱节点的引用
}
}
```
=== "Dart"
```dart title=""
/* 双向链表节点类 */
class ListNode {
int val; // 节点值
ListNode next; // 指向后继节点的引用
ListNode prev; // 指向前驱节点的引用
ListNode(this.val, [this.next, this.prev]); // 构造函数
}
```
=== "Rust"
```rust title=""
use std::rc::Rc;
use std::cell::RefCell;
/* 双向链表节点类型 */
#[derive(Debug)]
struct ListNode {
val: i32, // 节点值
next: Option<Rc<RefCell<ListNode>>>, // 指向后继节点的指针
prev: Option<Rc<RefCell<ListNode>>>, // 指向前驱节点的指针
}
/* 构造函数 */
impl ListNode {
fn new(val: i32) -> Self {
ListNode {
val,
next: None,
prev: None,
}
}
}
```
=== "C"
```c title=""
/* 双向链表节点结构体 */
struct ListNode {
int val; // 节点值
struct ListNode *next; // 指向后继节点的指针
struct ListNode *prev; // 指向前驱节点的指针
};
typedef struct ListNode ListNode;
/* 构造函数 */
ListNode *newListNode(int val) {
ListNode *node, *next;
node = (ListNode *) malloc(sizeof(ListNode));
node->val = val;
node->next = NULL;
node->prev = NULL;
return node;
}
```
=== "Zig"
```zig title=""
// 双向链表节点类
pub fn ListNode(comptime T: type) type {
return struct {
const Self = @This();
val: T = 0, // 节点值
next: ?*Self = null, // 指向后继节点的指针
prev: ?*Self = null, // 指向前驱节点的指针
// 构造函数
pub fn init(self: *Self, x: i32) void {
self.val = x;
self.next = null;
self.prev = null;
}
};
}
```
![常见链表种类](linked_list.assets/linkedlist_common_types.png)
## 链表典型应用
单向链表通常用于实现栈、队列、哈希表和图等数据结构。
- **栈与队列**:当插入和删除操作都在链表的一端进行时,它表现出先进后出的的特性,对应栈;当插入操作在链表的一端进行,删除操作在链表的另一端进行,它表现出先进先出的特性,对应队列。
- **哈希表**:链地址法是解决哈希冲突的主流方案之一,在该方案中,所有冲突的元素都会被放到一个链表中。
- **图**:邻接表是表示图的一种常用方式,在其中,图的每个顶点都与一个链表相关联,链表中的每个元素都代表与该顶点相连的其他顶点。
双向链表常被用于需要快速查找前一个和下一个元素的场景。
- **高级数据结构**比如在红黑树、B 树中,我们需要访问节点的父节点,这可以通过在节点中保存一个指向父节点的引用来实现,类似于双向链表。
- **浏览器历史**:在网页浏览器中,当用户点击前进或后退按钮时,浏览器需要知道用户访问过的前一个和后一个网页。双向链表的特性使得这种操作变得简单。
- **LRU 算法**在缓存淘汰算法LRU我们需要快速找到最近最少使用的数据以及支持快速地添加和删除节点。这时候使用双向链表就非常合适。
循环链表常被用于需要周期性操作的场景,比如操作系统的资源调度。
- **时间片轮转调度算法**:在操作系统中,时间片轮转调度算法是一种常见的 CPU 调度算法它需要对一组进程进行循环。每个进程被赋予一个时间片当时间片用完时CPU 将切换到下一个进程。这种循环的操作就可以通过循环链表来实现。
- **数据缓冲区**:在某些数据缓冲区的实现中,也可能会使用到循环链表。比如在音频、视频播放器中,数据流可能会被分成多个缓冲块并放入一个循环链表,以便实现无缝播放。

View File

@ -1,933 +0,0 @@
# 列表
**数组长度不可变导致实用性降低**。在实际中,我们可能事先无法确定需要存储多少数据,这使数组长度的选择变得困难。若长度过小,需要在持续添加数据时频繁扩容数组;若长度过大,则会造成内存空间的浪费。
为解决此问题,出现了一种被称为「动态数组 dynamic array」的数据结构即长度可变的数组也常被称为「列表 list」。列表基于数组实现继承了数组的优点并且可以在程序运行过程中动态扩容。我们可以在列表中自由地添加元素而无须担心超过容量限制。
## 列表常用操作
### 初始化列表
我们通常使用“无初始值”和“有初始值”这两种初始化方法。
=== "Python"
```python title="list.py"
# 初始化列表
# 无初始值
list1: list[int] = []
# 有初始值
list: list[int] = [1, 3, 2, 5, 4]
```
=== "C++"
```cpp title="list.cpp"
/* 初始化列表 */
// 需注意C++ 中 vector 即是本文描述的 list
// 无初始值
vector<int> list1;
// 有初始值
vector<int> list = { 1, 3, 2, 5, 4 };
```
=== "Java"
```java title="list.java"
/* 初始化列表 */
// 无初始值
List<Integer> list1 = new ArrayList<>();
// 有初始值(注意数组的元素类型需为 int[] 的包装类 Integer[]
Integer[] numbers = new Integer[] { 1, 3, 2, 5, 4 };
List<Integer> list = new ArrayList<>(Arrays.asList(numbers));
```
=== "C#"
```csharp title="list.cs"
/* 初始化列表 */
// 无初始值
List<int> list1 = new ();
// 有初始值
int[] numbers = new int[] { 1, 3, 2, 5, 4 };
List<int> list = numbers.ToList();
```
=== "Go"
```go title="list_test.go"
/* 初始化列表 */
// 无初始值
list1 := []int
// 有初始值
list := []int{1, 3, 2, 5, 4}
```
=== "Swift"
```swift title="list.swift"
/* 初始化列表 */
// 无初始值
let list1: [Int] = []
// 有初始值
var list = [1, 3, 2, 5, 4]
```
=== "JS"
```javascript title="list.js"
/* 初始化列表 */
// 无初始值
const list1 = [];
// 有初始值
const list = [1, 3, 2, 5, 4];
```
=== "TS"
```typescript title="list.ts"
/* 初始化列表 */
// 无初始值
const list1: number[] = [];
// 有初始值
const list: number[] = [1, 3, 2, 5, 4];
```
=== "Dart"
```dart title="list.dart"
/* 初始化列表 */
// 无初始值
List<int> list1 = [];
// 有初始值
List<int> list = [1, 3, 2, 5, 4];
```
=== "Rust"
```rust title="list.rs"
/* 初始化列表 */
// 无初始值
let list1: Vec<i32> = Vec::new();
// 有初始值
let list2: Vec<i32> = vec![1, 3, 2, 5, 4];
```
=== "C"
```c title="list.c"
// C 未提供内置动态数组
```
=== "Zig"
```zig title="list.zig"
// 初始化列表
var list = std.ArrayList(i32).init(std.heap.page_allocator);
defer list.deinit();
try list.appendSlice(&[_]i32{ 1, 3, 2, 5, 4 });
```
### 访问元素
列表本质上是数组,因此可以在 $O(1)$ 时间内访问和更新元素,效率很高。
=== "Python"
```python title="list.py"
# 访问元素
num: int = list[1] # 访问索引 1 处的元素
# 更新元素
list[1] = 0 # 将索引 1 处的元素更新为 0
```
=== "C++"
```cpp title="list.cpp"
/* 访问元素 */
int num = list[1]; // 访问索引 1 处的元素
/* 更新元素 */
list[1] = 0; // 将索引 1 处的元素更新为 0
```
=== "Java"
```java title="list.java"
/* 访问元素 */
int num = list.get(1); // 访问索引 1 处的元素
/* 更新元素 */
list.set(1, 0); // 将索引 1 处的元素更新为 0
```
=== "C#"
```csharp title="list.cs"
/* 访问元素 */
int num = list[1]; // 访问索引 1 处的元素
/* 更新元素 */
list[1] = 0; // 将索引 1 处的元素更新为 0
```
=== "Go"
```go title="list_test.go"
/* 访问元素 */
num := list[1] // 访问索引 1 处的元素
/* 更新元素 */
list[1] = 0 // 将索引 1 处的元素更新为 0
```
=== "Swift"
```swift title="list.swift"
/* 访问元素 */
let num = list[1] // 访问索引 1 处的元素
/* 更新元素 */
list[1] = 0 // 将索引 1 处的元素更新为 0
```
=== "JS"
```javascript title="list.js"
/* 访问元素 */
const num = list[1]; // 访问索引 1 处的元素
/* 更新元素 */
list[1] = 0; // 将索引 1 处的元素更新为 0
```
=== "TS"
```typescript title="list.ts"
/* 访问元素 */
const num: number = list[1]; // 访问索引 1 处的元素
/* 更新元素 */
list[1] = 0; // 将索引 1 处的元素更新为 0
```
=== "Dart"
```dart title="list.dart"
/* 访问元素 */
int num = list[1]; // 访问索引 1 处的元素
/* 更新元素 */
list[1] = 0; // 将索引 1 处的元素更新为 0
```
=== "Rust"
```rust title="list.rs"
/* 访问元素 */
let num: i32 = list[1]; // 访问索引 1 处的元素
/* 更新元素 */
list[1] = 0; // 将索引 1 处的元素更新为 0
```
=== "C"
```c title="list.c"
// C 未提供内置动态数组
```
=== "Zig"
```zig title="list.zig"
// 访问元素
var num = list.items[1]; // 访问索引 1 处的元素
// 更新元素
list.items[1] = 0; // 将索引 1 处的元素更新为 0
```
### 插入与删除元素
相较于数组,列表可以自由地添加与删除元素。在列表尾部添加元素的时间复杂度为 $O(1)$ ,但插入和删除元素的效率仍与数组相同,时间复杂度为 $O(n)$ 。
=== "Python"
```python title="list.py"
# 清空列表
list.clear()
# 尾部添加元素
list.append(1)
list.append(3)
list.append(2)
list.append(5)
list.append(4)
# 中间插入元素
list.insert(3, 6) # 在索引 3 处插入数字 6
# 删除元素
list.pop(3) # 删除索引 3 处的元素
```
=== "C++"
```cpp title="list.cpp"
/* 清空列表 */
list.clear();
/* 尾部添加元素 */
list.push_back(1);
list.push_back(3);
list.push_back(2);
list.push_back(5);
list.push_back(4);
/* 中间插入元素 */
list.insert(list.begin() + 3, 6); // 在索引 3 处插入数字 6
/* 删除元素 */
list.erase(list.begin() + 3); // 删除索引 3 处的元素
```
=== "Java"
```java title="list.java"
/* 清空列表 */
list.clear();
/* 尾部添加元素 */
list.add(1);
list.add(3);
list.add(2);
list.add(5);
list.add(4);
/* 中间插入元素 */
list.add(3, 6); // 在索引 3 处插入数字 6
/* 删除元素 */
list.remove(3); // 删除索引 3 处的元素
```
=== "C#"
```csharp title="list.cs"
/* 清空列表 */
list.Clear();
/* 尾部添加元素 */
list.Add(1);
list.Add(3);
list.Add(2);
list.Add(5);
list.Add(4);
/* 中间插入元素 */
list.Insert(3, 6);
/* 删除元素 */
list.RemoveAt(3);
```
=== "Go"
```go title="list_test.go"
/* 清空列表 */
list = nil
/* 尾部添加元素 */
list = append(list, 1)
list = append(list, 3)
list = append(list, 2)
list = append(list, 5)
list = append(list, 4)
/* 中间插入元素 */
list = append(list[:3], append([]int{6}, list[3:]...)...) // 在索引 3 处插入数字 6
/* 删除元素 */
list = append(list[:3], list[4:]...) // 删除索引 3 处的元素
```
=== "Swift"
```swift title="list.swift"
/* 清空列表 */
list.removeAll()
/* 尾部添加元素 */
list.append(1)
list.append(3)
list.append(2)
list.append(5)
list.append(4)
/* 中间插入元素 */
list.insert(6, at: 3) // 在索引 3 处插入数字 6
/* 删除元素 */
list.remove(at: 3) // 删除索引 3 处的元素
```
=== "JS"
```javascript title="list.js"
/* 清空列表 */
list.length = 0;
/* 尾部添加元素 */
list.push(1);
list.push(3);
list.push(2);
list.push(5);
list.push(4);
/* 中间插入元素 */
list.splice(3, 0, 6);
/* 删除元素 */
list.splice(3, 1);
```
=== "TS"
```typescript title="list.ts"
/* 清空列表 */
list.length = 0;
/* 尾部添加元素 */
list.push(1);
list.push(3);
list.push(2);
list.push(5);
list.push(4);
/* 中间插入元素 */
list.splice(3, 0, 6);
/* 删除元素 */
list.splice(3, 1);
```
=== "Dart"
```dart title="list.dart"
/* 清空列表 */
list.clear();
/* 尾部添加元素 */
list.add(1);
list.add(3);
list.add(2);
list.add(5);
list.add(4);
/* 中间插入元素 */
list.insert(3, 6); // 在索引 3 处插入数字 6
/* 删除元素 */
list.removeAt(3); // 删除索引 3 处的元素
```
=== "Rust"
```rust title="list.rs"
/* 清空列表 */
list.clear();
/* 尾部添加元素 */
list.push(1);
list.push(3);
list.push(2);
list.push(5);
list.push(4);
/* 中间插入元素 */
list.insert(3, 6); // 在索引 3 处插入数字 6
/* 删除元素 */
list.remove(3); // 删除索引 3 处的元素
```
=== "C"
```c title="list.c"
// C 未提供内置动态数组
```
=== "Zig"
```zig title="list.zig"
// 清空列表
list.clearRetainingCapacity();
// 尾部添加元素
try list.append(1);
try list.append(3);
try list.append(2);
try list.append(5);
try list.append(4);
// 中间插入元素
try list.insert(3, 6); // 在索引 3 处插入数字 6
// 删除元素
_ = list.orderedRemove(3); // 删除索引 3 处的元素
```
### 遍历列表
与数组一样,列表可以根据索引遍历,也可以直接遍历各元素。
=== "Python"
```python title="list.py"
# 通过索引遍历列表
count = 0
for i in range(len(list)):
count += 1
# 直接遍历列表元素
count = 0
for n in list:
count += 1
```
=== "C++"
```cpp title="list.cpp"
/* 通过索引遍历列表 */
int count = 0;
for (int i = 0; i < list.size(); i++) {
count++;
}
/* 直接遍历列表元素 */
count = 0;
for (int n : list) {
count++;
}
```
=== "Java"
```java title="list.java"
/* 通过索引遍历列表 */
int count = 0;
for (int i = 0; i < list.size(); i++) {
count++;
}
/* 直接遍历列表元素 */
count = 0;
for (int n : list) {
count++;
}
```
=== "C#"
```csharp title="list.cs"
/* 通过索引遍历列表 */
int count = 0;
for (int i = 0; i < list.Count; i++) {
count++;
}
/* 直接遍历列表元素 */
count = 0;
foreach (int n in list) {
count++;
}
```
=== "Go"
```go title="list_test.go"
/* 通过索引遍历列表 */
count := 0
for i := 0; i < len(list); i++ {
count++
}
/* 直接遍历列表元素 */
count = 0
for range list {
count++
}
```
=== "Swift"
```swift title="list.swift"
/* 通过索引遍历列表 */
var count = 0
for _ in list.indices {
count += 1
}
/* 直接遍历列表元素 */
count = 0
for _ in list {
count += 1
}
```
=== "JS"
```javascript title="list.js"
/* 通过索引遍历列表 */
let count = 0;
for (let i = 0; i < list.length; i++) {
count++;
}
/* 直接遍历列表元素 */
count = 0;
for (const n of list) {
count++;
}
```
=== "TS"
```typescript title="list.ts"
/* 通过索引遍历列表 */
let count = 0;
for (let i = 0; i < list.length; i++) {
count++;
}
/* 直接遍历列表元素 */
count = 0;
for (const n of list) {
count++;
}
```
=== "Dart"
```dart title="list.dart"
/* 通过索引遍历列表 */
int count = 0;
for (int i = 0; i < list.length; i++) {
count++;
}
/* 直接遍历列表元素 */
count = 0;
for (int n in list) {
count++;
}
```
=== "Rust"
```rust title="list.rs"
/* 通过索引遍历列表 */
let mut count = 0;
for (index, value) in list.iter().enumerate() {
count += 1;
}
/* 直接遍历列表元素 */
let mut count = 0;
for value in list.iter() {
count += 1;
}
```
=== "C"
```c title="list.c"
// C 未提供内置动态数组
```
=== "Zig"
```zig title="list.zig"
// 通过索引遍历列表
var count: i32 = 0;
var i: i32 = 0;
while (i < list.items.len) : (i += 1) {
count += 1;
}
// 直接遍历列表元素
count = 0;
for (list.items) |_| {
count += 1;
}
```
### 拼接列表
给定一个新列表 `list1` ,我们可以将该列表拼接到原列表的尾部。
=== "Python"
```python title="list.py"
# 拼接两个列表
list1: list[int] = [6, 8, 7, 10, 9]
list += list1 # 将列表 list1 拼接到 list 之后
```
=== "C++"
```cpp title="list.cpp"
/* 拼接两个列表 */
vector<int> list1 = { 6, 8, 7, 10, 9 };
// 将列表 list1 拼接到 list 之后
list.insert(list.end(), list1.begin(), list1.end());
```
=== "Java"
```java title="list.java"
/* 拼接两个列表 */
List<Integer> list1 = new ArrayList<>(Arrays.asList(new Integer[] { 6, 8, 7, 10, 9 }));
list.addAll(list1); // 将列表 list1 拼接到 list 之后
```
=== "C#"
```csharp title="list.cs"
/* 拼接两个列表 */
List<int> list1 = new() { 6, 8, 7, 10, 9 };
list.AddRange(list1); // 将列表 list1 拼接到 list 之后
```
=== "Go"
```go title="list_test.go"
/* 拼接两个列表 */
list1 := []int{6, 8, 7, 10, 9}
list = append(list, list1...) // 将列表 list1 拼接到 list 之后
```
=== "Swift"
```swift title="list.swift"
/* 拼接两个列表 */
let list1 = [6, 8, 7, 10, 9]
list.append(contentsOf: list1) // 将列表 list1 拼接到 list 之后
```
=== "JS"
```javascript title="list.js"
/* 拼接两个列表 */
const list1 = [6, 8, 7, 10, 9];
list.push(...list1); // 将列表 list1 拼接到 list 之后
```
=== "TS"
```typescript title="list.ts"
/* 拼接两个列表 */
const list1: number[] = [6, 8, 7, 10, 9];
list.push(...list1); // 将列表 list1 拼接到 list 之后
```
=== "Dart"
```dart title="list.dart"
/* 拼接两个列表 */
List<int> list1 = [6, 8, 7, 10, 9];
list.addAll(list1); // 将列表 list1 拼接到 list 之后
```
=== "Rust"
```rust title="list.rs"
/* 拼接两个列表 */
let list1: Vec<i32> = vec![6, 8, 7, 10, 9];
list.extend(list1);
```
=== "C"
```c title="list.c"
// C 未提供内置动态数组
```
=== "Zig"
```zig title="list.zig"
// 拼接两个列表
var list1 = std.ArrayList(i32).init(std.heap.page_allocator);
defer list1.deinit();
try list1.appendSlice(&[_]i32{ 6, 8, 7, 10, 9 });
try list.insertSlice(list.items.len, list1.items); // 将列表 list1 拼接到 list 之后
```
### 排序列表
完成列表排序后,我们便可以使用在数组类算法题中经常考察的“二分查找”和“双指针”算法。
=== "Python"
```python title="list.py"
# 排序列表
list.sort() # 排序后,列表元素从小到大排列
```
=== "C++"
```cpp title="list.cpp"
/* 排序列表 */
sort(list.begin(), list.end()); // 排序后,列表元素从小到大排列
```
=== "Java"
```java title="list.java"
/* 排序列表 */
Collections.sort(list); // 排序后,列表元素从小到大排列
```
=== "C#"
```csharp title="list.cs"
/* 排序列表 */
list.Sort(); // 排序后,列表元素从小到大排列
```
=== "Go"
```go title="list_test.go"
/* 排序列表 */
sort.Ints(list) // 排序后,列表元素从小到大排列
```
=== "Swift"
```swift title="list.swift"
/* 排序列表 */
list.sort() // 排序后,列表元素从小到大排列
```
=== "JS"
```javascript title="list.js"
/* 排序列表 */
list.sort((a, b) => a - b); // 排序后,列表元素从小到大排列
```
=== "TS"
```typescript title="list.ts"
/* 排序列表 */
list.sort((a, b) => a - b); // 排序后,列表元素从小到大排列
```
=== "Dart"
```dart title="list.dart"
/* 排序列表 */
list.sort(); // 排序后,列表元素从小到大排列
```
=== "Rust"
```rust title="list.rs"
/* 排序列表 */
list.sort(); // 排序后,列表元素从小到大排列
```
=== "C"
```c title="list.c"
// C 未提供内置动态数组
```
=== "Zig"
```zig title="list.zig"
// 排序列表
std.sort.sort(i32, list.items, {}, comptime std.sort.asc(i32));
```
## 列表实现
许多编程语言都提供内置的列表,例如 Java、C++、Python 等。它们的实现比较复杂,各个参数的设定也非常有考究,例如初始容量、扩容倍数等。感兴趣的读者可以查阅源码进行学习。
为了加深对列表工作原理的理解,我们尝试实现一个简易版列表,包括以下三个重点设计。
- **初始容量**:选取一个合理的数组初始容量。在本示例中,我们选择 10 作为初始容量。
- **数量记录**:声明一个变量 `size` ,用于记录列表当前元素数量,并随着元素插入和删除实时更新。根据此变量,我们可以定位列表尾部,以及判断是否需要扩容。
- **扩容机制**:若插入元素时列表容量已满,则需要进行扩容。首先根据扩容倍数创建一个更大的数组,再将当前数组的所有元素依次移动至新数组。在本示例中,我们规定每次将数组扩容至之前的 2 倍。
=== "Python"
```python title="my_list.py"
[class]{MyList}-[func]{}
```
=== "C++"
```cpp title="my_list.cpp"
[class]{MyList}-[func]{}
```
=== "Java"
```java title="my_list.java"
[class]{MyList}-[func]{}
```
=== "C#"
```csharp title="my_list.cs"
[class]{MyList}-[func]{}
```
=== "Go"
```go title="my_list.go"
[class]{myList}-[func]{}
```
=== "Swift"
```swift title="my_list.swift"
[class]{MyList}-[func]{}
```
=== "JS"
```javascript title="my_list.js"
[class]{MyList}-[func]{}
```
=== "TS"
```typescript title="my_list.ts"
[class]{MyList}-[func]{}
```
=== "Dart"
```dart title="my_list.dart"
[class]{MyList}-[func]{}
```
=== "Rust"
```rust title="my_list.rs"
[class]{MyList}-[func]{}
```
=== "C"
```c title="my_list.c"
[class]{myList}-[func]{}
```
=== "Zig"
```zig title="my_list.zig"
[class]{MyList}-[func]{}
```

View File

@ -1,68 +0,0 @@
# 小结
### 重点回顾
- 数组和链表是两种基本的数据结构,分别代表数据在计算机内存中的两种存储方式:连续空间存储和分散空间存储。两者的特点呈现出互补的特性。
- 数组支持随机访问、占用内存较少;但插入和删除元素效率低,且初始化后长度不可变。
- 链表通过更改引用(指针)实现高效的节点插入与删除,且可以灵活调整长度;但节点访问效率低、占用内存较多。常见的链表类型包括单向链表、循环链表、双向链表。
- 动态数组,又称列表,是基于数组实现的一种数据结构。它保留了数组的优势,同时可以灵活调整长度。列表的出现极大地提高了数组的易用性,但可能导致部分内存空间浪费。
### Q & A
!!! question "数组存储在栈上和存储在堆上,对时间效率和空间效率是否有影响?"
存储在栈上和堆上的数组都被存储在连续内存空间内,数据操作效率是基本一致的。然而,栈和堆具有各自的特点,从而导致以下不同点。
1. 分配和释放效率:栈是一块较小的内存,分配由编译器自动完成;而堆内存相对更大,可以在代码中动态分配,更容易碎片化。因此,堆上的分配和释放操作通常比栈上的慢。
2. 大小限制:栈内存相对较小,堆的大小一般受限于可用内存。因此堆更加适合存储大型数组。
3. 灵活性:栈上的数组的大小需要在编译时确定,而堆上的数组的大小可以在运行时动态确定。
!!! question "为什么数组要求相同类型的元素,而在链表中却没有强调同类型呢?"
链表由结点组成,结点之间通过引用(指针)连接,各个结点可以存储不同类型的数据,例如 int、double、string、object 等。
相对地,数组元素则必须是相同类型的,这样才能通过计算偏移量来获取对应元素位置。例如,如果数组同时包含 int 和 long 两种类型,单个元素分别占用 4 bytes 和 8 bytes ,那么此时就不能用以下公式计算偏移量了,因为数组中包含了两种长度的元素。
```shell
# 元素内存地址 = 数组内存地址 + 元素长度 * 元素索引
```
!!! question "删除节点后,是否需要把 `P.next` 设为 $\text{None}$ 呢?"
不修改 `P.next` 也可以。从该链表的角度看,从头结点遍历到尾结点已经遇不到 `P` 了。这意味着结点 `P` 已经从链表中删除了,此时结点 `P` 指向哪里都不会对这条链表产生影响了。
从垃圾回收的角度看,对于 Java、Python、Go 等拥有自动垃圾回收的语言来说,节点 `P` 是否被回收取决于是否有仍存在指向它的引用,而不是 `P.next` 的值。在 C 和 C++ 等语言中,我们需要手动释放节点内存。
!!! question "在链表中插入和删除操作的时间复杂度是 $O(1)$ 。但是增删之前都需要 $O(n)$ 查找元素,那为什么时间复杂度不是 $O(n)$ 呢?"
如果是先查找元素、再删除元素,确实是 $O(n)$ 。然而,链表的 $O(1)$ 增删的优势可以在其他应用上得到体现。例如,双向队列适合使用链表实现,我们维护一个指针变量始终指向头结点、尾结点,每次插入与删除操作都是 $O(1)$ 。
!!! question "图片“链表定义与存储方式”中,浅蓝色的存储结点指针是占用一块内存地址吗?还是和结点值各占一半呢?"
文中的示意图只是定性表示,定量表示需要根据具体情况进行分析。
- 不同类型的结点值占用的空间是不同的,比如 int、long、double 和实例对象等。
- 指针变量占用的内存空间大小根据所使用的操作系统及编译环境而定,大多为 8 字节或 4 字节。
!!! question "在列表末尾添加元素是否时时刻刻都为 $O(1)$ "
如果添加元素时超出列表长度,则需要先扩容列表再添加。系统会申请一块新的内存,并将原列表的所有元素搬运过去,这时候时间复杂度就会是 $O(n)$ 。
!!! question "“列表的出现大大提升了数组的实用性,但副作用是会造成部分内存空间浪费”,这里的空间浪费是指额外增加的变量如容量、长度、扩容倍数所占的内存吗?"
这里的空间浪费主要有两方面含义:一方面,列表都会设定一个初始长度,我们不一定需要用这么多。另一方面,为了防止频繁扩容,扩容一般都会乘以一个系数,比如 $\times 1.5$ 。这样一来,也会出现很多空位,我们通常不能完全填满它们。
!!! question "在 Python 中初始化 `n = [1, 2, 3]` 后,这 3 个元素的地址是相连的,但是初始化 `m = [2, 1, 3]` 会发现它们每个元素的 id 并不是连续的,而是分别跟 `n` 中的相同。这些元素地址不连续,那么 `m` 还是数组吗?"
假如把列表元素换成链表节点 `n = [n1, n2, n3, n4, n5]` ,通常情况下这五个节点对象也是被分散存储在内存各处的。然而,给定一个列表索引,我们仍然可以在 $O(1)$ 时间内获取到节点内存地址,从而访问到对应的节点。这是因为数组中存储的是节点的引用,而非节点本身。
与许多语言不同的是,在 Python 中数字也被包装为对象,列表中存储的不是数字本身,而是对数字的引用。因此,我们会发现两个数组中的相同数字拥有同一个 id ,并且这些数字的内存地址是无须连续的。
!!! question "C++ STL 里面的 std::list 已经实现了双向链表,但好像一些算法的书上都不怎么直接用这个,是不是有什么局限性呢?"
一方面,我们往往更青睐使用数组实现算法,而只有在必要时才使用链表,主要有两个原因。
- 空间开销:由于每个元素需要两个额外的指针(一个用于前一个元素,一个用于后一个元素),所以 `std::list` 通常比 `std::vector` 更占用空间。
- 缓存不友好:由于数据不是连续存放的,`std::list` 对缓存的利用率较低。一般情况下,`std::vector` 的性能会更好。
另一方面,必要使用链表的情况主要是二叉树和图。栈和队列往往会使用编程语言提供的 `stack` 和 `queue` ,而非链表。