mirror of
https://github.com/krahets/hello-algo.git
synced 2026-04-02 02:02:57 +08:00
First commit
This commit is contained in:
910
chapter_array_and_linkedlist/array.md
Executable file
910
chapter_array_and_linkedlist/array.md
Executable file
@@ -0,0 +1,910 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 4.1. 数组
|
||||
|
||||
「数组 Array」是一种将 **相同类型元素** 存储在 **连续内存空间** 的数据结构,将元素在数组中的位置称为元素的「索引 Index」。
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 数组定义与存储方式 </p>
|
||||
|
||||
!!! note
|
||||
|
||||
观察上图,我们发现 **数组首元素的索引为 $0$** 。你可能会想,这并不符合日常习惯,首个元素的索引为什么不是 $1$ 呢,这不是更加自然吗?我认同你的想法,但请先记住这个设定,后面讲内存地址计算时,我会尝试解答这个问题。
|
||||
|
||||
**数组有多种初始化写法**。根据实际需要,选代码最短的那一种就好。
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="array.java"
|
||||
/* 初始化数组 */
|
||||
int[] arr = new int[5]; // { 0, 0, 0, 0, 0 }
|
||||
int[] nums = { 1, 3, 2, 5, 4 };
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="array.cpp"
|
||||
/* 初始化数组 */
|
||||
int* arr = new int[5];
|
||||
int* nums = new int[5] { 1, 3, 2, 5, 4 };
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="array.py"
|
||||
""" 初始化数组 """
|
||||
arr = [0] * 5 # [ 0, 0, 0, 0, 0 ]
|
||||
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}
|
||||
```
|
||||
|
||||
=== "JavaScript"
|
||||
|
||||
```javascript title="array.js"
|
||||
/* 初始化数组 */
|
||||
var arr = new Array(5).fill(0);
|
||||
var nums = [1, 3, 2, 5, 4];
|
||||
```
|
||||
|
||||
=== "TypeScript"
|
||||
|
||||
```typescript title="array.ts"
|
||||
/* 初始化数组 */
|
||||
let arr: number[] = new Array(5).fill(0);
|
||||
let nums: number[] = [1, 3, 2, 5, 4];
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="array.c"
|
||||
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="array.cs"
|
||||
/* 初始化数组 */
|
||||
int[] arr = new int[5]; // { 0, 0, 0, 0, 0 }
|
||||
int[] nums = { 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]
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="array.zig"
|
||||
// 初始化数组
|
||||
var arr = [_]i32{0} ** 5; // { 0, 0, 0, 0, 0 }
|
||||
var nums = [_]i32{ 1, 3, 2, 5, 4 };
|
||||
```
|
||||
|
||||
## 4.1.1. 数组优点
|
||||
|
||||
**在数组中访问元素非常高效**。这是因为在数组中,计算元素的内存地址非常容易。给定数组首个元素的地址、和一个元素的索引,利用以下公式可以直接计算得到该元素的内存地址,从而直接访问此元素。
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 数组元素的内存地址计算 </p>
|
||||
|
||||
```java title=""
|
||||
// 元素内存地址 = 数组内存地址 + 元素长度 * 元素索引
|
||||
elementAddr = firtstElementAddr + elementLength * elementIndex
|
||||
```
|
||||
|
||||
**为什么数组元素索引从 0 开始编号?** 根据地址计算公式,**索引本质上表示的是内存地址偏移量**,首个元素的地址偏移量是 $0$ ,那么索引是 $0$ 也就很自然了。
|
||||
|
||||
访问元素的高效性带来了许多便利。例如,我们可以在 $O(1)$ 时间内随机获取一个数组中的元素。
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="array.java"
|
||||
/* 随机返回一个数组元素 */
|
||||
int randomAccess(int[] nums) {
|
||||
// 在区间 [0, nums.length) 中随机抽取一个数字
|
||||
int randomIndex = ThreadLocalRandom.current().
|
||||
nextInt(0, nums.length);
|
||||
// 获取并返回随机元素
|
||||
int randomNum = nums[randomIndex];
|
||||
return randomNum;
|
||||
}
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="array.cpp"
|
||||
/* 随机返回一个数组元素 */
|
||||
int randomAccess(int* nums, int size) {
|
||||
// 在区间 [0, size) 中随机抽取一个数字
|
||||
int randomIndex = rand() % size;
|
||||
// 获取并返回随机元素
|
||||
int randomNum = nums[randomIndex];
|
||||
return randomNum;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="array.py"
|
||||
""" 随机访问元素 """
|
||||
def random_access(nums):
|
||||
# 在区间 [0, len(nums)-1] 中随机抽取一个数字
|
||||
random_index = random.randint(0, len(nums) - 1)
|
||||
# 获取并返回随机元素
|
||||
random_num = nums[random_index]
|
||||
return random_num
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="array.go"
|
||||
/* 随机返回一个数组元素 */
|
||||
func randomAccess(nums []int) (randomNum int) {
|
||||
// 在区间 [0, nums.length) 中随机抽取一个数字
|
||||
randomIndex := rand.Intn(len(nums))
|
||||
// 获取并返回随机元素
|
||||
randomNum = nums[randomIndex]
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
=== "JavaScript"
|
||||
|
||||
```javascript title="array.js"
|
||||
/* 随机返回一个数组元素 */
|
||||
function randomAccess(nums) {
|
||||
// 在区间 [0, nums.length) 中随机抽取一个数字
|
||||
const random_index = Math.floor(Math.random() * nums.length);
|
||||
// 获取并返回随机元素
|
||||
const random_num = nums[random_index];
|
||||
return random_num;
|
||||
}
|
||||
```
|
||||
|
||||
=== "TypeScript"
|
||||
|
||||
```typescript title="array.ts"
|
||||
/* 随机返回一个数组元素 */
|
||||
function randomAccess(nums: number[]): number {
|
||||
// 在区间 [0, nums.length) 中随机抽取一个数字
|
||||
const random_index = Math.floor(Math.random() * nums.length);
|
||||
// 获取并返回随机元素
|
||||
const random_num = nums[random_index];
|
||||
return random_num;
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="array.c"
|
||||
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="array.cs"
|
||||
/* 随机返回一个数组元素 */
|
||||
int RandomAccess(int[] nums)
|
||||
{
|
||||
Random random=new();
|
||||
// 在区间 [0, nums.Length) 中随机抽取一个数字
|
||||
int randomIndex = random.Next(nums.Length);
|
||||
// 获取并返回随机元素
|
||||
int randomNum = nums[randomIndex];
|
||||
return randomNum;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="array.swift"
|
||||
/* 随机返回一个数组元素 */
|
||||
func randomAccess(nums: [Int]) -> Int {
|
||||
// 在区间 [0, nums.count) 中随机抽取一个数字
|
||||
let randomIndex = nums.indices.randomElement()!
|
||||
// 获取并返回随机元素
|
||||
let randomNum = nums[randomIndex]
|
||||
return randomNum
|
||||
}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="array.zig"
|
||||
// 随机返回一个数组元素
|
||||
pub fn randomAccess(nums: []i32) i32 {
|
||||
// 在区间 [0, nums.len) 中随机抽取一个整数
|
||||
var randomIndex = std.crypto.random.intRangeLessThan(usize, 0, nums.len);
|
||||
// 获取并返回随机元素
|
||||
var randomNum = nums[randomIndex];
|
||||
return randomNum;
|
||||
}
|
||||
```
|
||||
|
||||
## 4.1.2. 数组缺点
|
||||
|
||||
**数组在初始化后长度不可变**。由于系统无法保证数组之后的内存空间是可用的,因此数组长度无法扩展。而若希望扩容数组,则需新建一个数组,然后把原数组元素依次拷贝到新数组,在数组很大的情况下,这是非常耗时的。
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="array.java"
|
||||
/* 扩展数组长度 */
|
||||
int[] extend(int[] nums, int enlarge) {
|
||||
// 初始化一个扩展长度后的数组
|
||||
int[] res = new int[nums.length + enlarge];
|
||||
// 将原数组中的所有元素复制到新数组
|
||||
for (int i = 0; i < nums.length; i++) {
|
||||
res[i] = nums[i];
|
||||
}
|
||||
// 返回扩展后的新数组
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="array.cpp"
|
||||
/* 扩展数组长度 */
|
||||
int* extend(int* nums, int size, int enlarge) {
|
||||
// 初始化一个扩展长度后的数组
|
||||
int* res = new int[size + enlarge];
|
||||
// 将原数组中的所有元素复制到新数组
|
||||
for (int i = 0; i < size; i++) {
|
||||
res[i] = nums[i];
|
||||
}
|
||||
// 释放内存
|
||||
delete[] nums;
|
||||
// 返回扩展后的新数组
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="array.py"
|
||||
""" 扩展数组长度 """
|
||||
# 请注意,Python 的 list 是动态数组,可以直接扩展
|
||||
# 为了方便学习,本函数将 list 看作是长度不可变的数组
|
||||
def extend(nums, enlarge):
|
||||
# 初始化一个扩展长度后的数组
|
||||
res = [0] * (len(nums) + enlarge)
|
||||
# 将原数组中的所有元素复制到新数组
|
||||
for i in range(len(nums)):
|
||||
res[i] = nums[i]
|
||||
# 返回扩展后的新数组
|
||||
return res
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="array.go"
|
||||
/* 扩展数组长度 */
|
||||
func extend(nums []int, enlarge int) []int {
|
||||
// 初始化一个扩展长度后的数组
|
||||
res := make([]int, len(nums)+enlarge)
|
||||
// 将原数组中的所有元素复制到新数组
|
||||
for i, num := range nums {
|
||||
res[i] = num
|
||||
}
|
||||
// 返回扩展后的新数组
|
||||
return res
|
||||
}
|
||||
```
|
||||
|
||||
=== "JavaScript"
|
||||
|
||||
```javascript title="array.js"
|
||||
/* 扩展数组长度 */
|
||||
function extend(nums, enlarge) {
|
||||
// 初始化一个扩展长度后的数组
|
||||
const res = new Array(nums.length + enlarge).fill(0);
|
||||
// 将原数组中的所有元素复制到新数组
|
||||
for (let i = 0; i < nums.length; i++) {
|
||||
res[i] = nums[i];
|
||||
}
|
||||
// 返回扩展后的新数组
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "TypeScript"
|
||||
|
||||
```typescript title="array.ts"
|
||||
/* 扩展数组长度 */
|
||||
function extend(nums: number[], enlarge: number): number[] {
|
||||
// 初始化一个扩展长度后的数组
|
||||
const res = new Array(nums.length + enlarge).fill(0);
|
||||
// 将原数组中的所有元素复制到新数组
|
||||
for (let i = 0; i < nums.length; i++) {
|
||||
res[i] = nums[i];
|
||||
}
|
||||
// 返回扩展后的新数组
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="array.c"
|
||||
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="array.cs"
|
||||
/* 扩展数组长度 */
|
||||
int[] Extend(int[] nums, int enlarge)
|
||||
{
|
||||
// 初始化一个扩展长度后的数组
|
||||
int[] res = new int[nums.Length + enlarge];
|
||||
// 将原数组中的所有元素复制到新数组
|
||||
for (int i = 0; i < nums.Length; i++)
|
||||
{
|
||||
res[i] = nums[i];
|
||||
}
|
||||
// 返回扩展后的新数组
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="array.swift"
|
||||
/* 扩展数组长度 */
|
||||
func extend(nums: [Int], enlarge: Int) -> [Int] {
|
||||
// 初始化一个扩展长度后的数组
|
||||
var res = Array(repeating: 0, count: nums.count + enlarge)
|
||||
// 将原数组中的所有元素复制到新数组
|
||||
for i in nums.indices {
|
||||
res[i] = nums[i]
|
||||
}
|
||||
// 返回扩展后的新数组
|
||||
return res
|
||||
}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="array.zig"
|
||||
// 扩展数组长度
|
||||
pub fn extend(mem_allocator: std.mem.Allocator, nums: []i32, enlarge: usize) ![]i32 {
|
||||
// 初始化一个扩展长度后的数组
|
||||
var res = try mem_allocator.alloc(i32, nums.len + enlarge);
|
||||
std.mem.set(i32, res, 0);
|
||||
// 将原数组中的所有元素复制到新数组
|
||||
std.mem.copy(i32, res, nums);
|
||||
// 返回扩展后的新数组
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
**数组中插入或删除元素效率低下**。假设我们想要在数组中间某位置插入一个元素,由于数组元素在内存中是“紧挨着的”,它们之间没有空间再放任何数据。因此,我们不得不将此索引之后的所有元素都向后移动一位,然后再把元素赋值给该索引。删除元素也是类似,需要把此索引之后的元素都向前移动一位。总体看有以下缺点:
|
||||
|
||||
- **时间复杂度高**:数组的插入和删除的平均时间复杂度均为 $O(N)$ ,其中 $N$ 为数组长度。
|
||||
- **丢失元素**:由于数组的长度不可变,因此在插入元素后,超出数组长度范围的元素会被丢失。
|
||||
- **内存浪费**:我们一般会初始化一个比较长的数组,只用前面一部分,这样在插入数据时,丢失的末尾元素都是我们不关心的,但这样做同时也会造成内存空间的浪费。
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 在数组中插入与删除元素 </p>
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="array.java"
|
||||
/* 在数组的索引 index 处插入元素 num */
|
||||
void insert(int[] nums, int num, int index) {
|
||||
// 把索引 index 以及之后的所有元素向后移动一位
|
||||
for (int i = nums.length - 1; i > index; i--) {
|
||||
nums[i] = nums[i - 1];
|
||||
}
|
||||
// 将 num 赋给 index 处元素
|
||||
nums[index] = num;
|
||||
}
|
||||
|
||||
/* 删除索引 index 处元素 */
|
||||
void remove(int[] nums, int index) {
|
||||
// 把索引 index 之后的所有元素向前移动一位
|
||||
for (int i = index; i < nums.length - 1; i++) {
|
||||
nums[i] = nums[i + 1];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="array.cpp"
|
||||
/* 在数组的索引 index 处插入元素 num */
|
||||
void insert(int* nums, int size, int num, int index) {
|
||||
// 把索引 index 以及之后的所有元素向后移动一位
|
||||
for (int i = size - 1; i > index; i--) {
|
||||
nums[i] = nums[i - 1];
|
||||
}
|
||||
// 将 num 赋给 index 处元素
|
||||
nums[index] = num;
|
||||
}
|
||||
|
||||
/* 删除索引 index 处元素 */
|
||||
void remove(int* nums, int size, int index) {
|
||||
// 把索引 index 之后的所有元素向前移动一位
|
||||
for (int i = index; i < size - 1; i++) {
|
||||
nums[i] = nums[i + 1];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="array.py"
|
||||
""" 在数组的索引 index 处插入元素 num """
|
||||
def insert(nums, num, index):
|
||||
# 把索引 index 以及之后的所有元素向后移动一位
|
||||
for i in range(len(nums) - 1, index, -1):
|
||||
nums[i] = nums[i - 1]
|
||||
# 将 num 赋给 index 处元素
|
||||
nums[index] = num
|
||||
|
||||
""" 删除索引 index 处元素 """
|
||||
def remove(nums, index):
|
||||
# 把索引 index 之后的所有元素向前移动一位
|
||||
for i in range(index, len(nums) - 1):
|
||||
nums[i] = nums[i + 1]
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="array.go"
|
||||
/* 在数组的索引 index 处插入元素 num */
|
||||
func insert(nums []int, num int, index int) {
|
||||
// 把索引 index 以及之后的所有元素向后移动一位
|
||||
for i := len(nums) - 1; i > index; i-- {
|
||||
nums[i] = nums[i-1]
|
||||
}
|
||||
// 将 num 赋给 index 处元素
|
||||
nums[index] = num
|
||||
}
|
||||
|
||||
/* 删除索引 index 处元素 */
|
||||
func remove(nums []int, index int) {
|
||||
// 把索引 index 之后的所有元素向前移动一位
|
||||
for i := index; i < len(nums)-1; i++ {
|
||||
nums[i] = nums[i+1]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "JavaScript"
|
||||
|
||||
```javascript title="array.js"
|
||||
/* 在数组的索引 index 处插入元素 num */
|
||||
function insert(nums, num, index) {
|
||||
// 把索引 index 以及之后的所有元素向后移动一位
|
||||
for (let i = nums.length - 1; i > index; i--) {
|
||||
nums[i] = nums[i - 1];
|
||||
}
|
||||
// 将 num 赋给 index 处元素
|
||||
nums[index] = num;
|
||||
}
|
||||
|
||||
/* 删除索引 index 处元素 */
|
||||
function remove(nums, index) {
|
||||
// 把索引 index 之后的所有元素向前移动一位
|
||||
for (let i = index; i < nums.length - 1; i++) {
|
||||
nums[i] = nums[i + 1];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "TypeScript"
|
||||
|
||||
```typescript title="array.ts"
|
||||
/* 在数组的索引 index 处插入元素 num */
|
||||
function insert(nums: number[], num: number, index: number): void {
|
||||
// 把索引 index 以及之后的所有元素向后移动一位
|
||||
for (let i = nums.length - 1; i > index; i--) {
|
||||
nums[i] = nums[i - 1];
|
||||
}
|
||||
// 将 num 赋给 index 处元素
|
||||
nums[index] = num;
|
||||
}
|
||||
|
||||
/* 删除索引 index 处元素 */
|
||||
function remove(nums: number[], index: number): void {
|
||||
// 把索引 index 之后的所有元素向前移动一位
|
||||
for (let i = index; i < nums.length - 1; i++) {
|
||||
nums[i] = nums[i + 1];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="array.c"
|
||||
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="array.cs"
|
||||
/* 在数组的索引 index 处插入元素 num */
|
||||
void Insert(int[] nums, int num, int index)
|
||||
{
|
||||
// 把索引 index 以及之后的所有元素向后移动一位
|
||||
for (int i = nums.Length - 1; i > index; i--)
|
||||
{
|
||||
nums[i] = nums[i - 1];
|
||||
}
|
||||
// 将 num 赋给 index 处元素
|
||||
nums[index] = num;
|
||||
}
|
||||
/* 删除索引 index 处元素 */
|
||||
void Remove(int[] nums, int index)
|
||||
{
|
||||
// 把索引 index 之后的所有元素向前移动一位
|
||||
for (int i = index; i < nums.Length - 1; i++)
|
||||
{
|
||||
nums[i] = nums[i + 1];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="array.swift"
|
||||
/* 在数组的索引 index 处插入元素 num */
|
||||
func insert(nums: inout [Int], num: Int, index: Int) {
|
||||
// 把索引 index 以及之后的所有元素向后移动一位
|
||||
for i in sequence(first: nums.count - 1, next: { $0 > index + 1 ? $0 - 1 : nil }) {
|
||||
nums[i] = nums[i - 1]
|
||||
}
|
||||
// 将 num 赋给 index 处元素
|
||||
nums[index] = num
|
||||
}
|
||||
|
||||
/* 删除索引 index 处元素 */
|
||||
func remove(nums: inout [Int], index: Int) {
|
||||
let count = nums.count
|
||||
// 把索引 index 之后的所有元素向前移动一位
|
||||
for i in sequence(first: index, next: { $0 < count - 1 - 1 ? $0 + 1 : nil }) {
|
||||
nums[i] = nums[i + 1]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="array.zig"
|
||||
// 在数组的索引 index 处插入元素 num
|
||||
pub fn insert(nums: []i32, num: i32, index: usize) void {
|
||||
// 把索引 index 以及之后的所有元素向后移动一位
|
||||
var i = nums.len - 1;
|
||||
while (i > index) : (i -= 1) {
|
||||
nums[i] = nums[i - 1];
|
||||
}
|
||||
// 将 num 赋给 index 处元素
|
||||
nums[index] = num;
|
||||
}
|
||||
|
||||
// 删除索引 index 处元素
|
||||
pub fn remove(nums: []i32, index: usize) void {
|
||||
// 把索引 index 之后的所有元素向前移动一位
|
||||
var i = index;
|
||||
while (i < nums.len - 1) : (i += 1) {
|
||||
nums[i] = nums[i + 1];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4.1.3. 数组常用操作
|
||||
|
||||
**数组遍历**。以下介绍两种常用的遍历方法。
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="array.java"
|
||||
/* 遍历数组 */
|
||||
void traverse(int[] nums) {
|
||||
int count = 0;
|
||||
// 通过索引遍历数组
|
||||
for (int i = 0; i < nums.length; i++) {
|
||||
count++;
|
||||
}
|
||||
// 直接遍历数组
|
||||
for (int num : nums) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="array.cpp"
|
||||
/* 遍历数组 */
|
||||
void traverse(int* nums, int size) {
|
||||
int count = 0;
|
||||
// 通过索引遍历数组
|
||||
for (int i = 0; i < size; i++) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="array.py"
|
||||
""" 遍历数组 """
|
||||
def traverse(nums):
|
||||
count = 0
|
||||
# 通过索引遍历数组
|
||||
for i in range(len(nums)):
|
||||
count += 1
|
||||
# 直接遍历数组
|
||||
for num in nums:
|
||||
count += 1
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="array.go"
|
||||
/* 遍历数组 */
|
||||
func traverse(nums []int) {
|
||||
count := 0
|
||||
// 通过索引遍历数组
|
||||
for i := 0; i < len(nums); i++ {
|
||||
count++
|
||||
}
|
||||
// 直接遍历数组
|
||||
for range nums {
|
||||
count++
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "JavaScript"
|
||||
|
||||
```javascript title="array.js"
|
||||
/* 遍历数组 */
|
||||
function traverse(nums) {
|
||||
let count = 0;
|
||||
// 通过索引遍历数组
|
||||
for (let i = 0; i < nums.length; i++) {
|
||||
count++;
|
||||
}
|
||||
// 直接遍历数组
|
||||
for (let num of nums) {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "TypeScript"
|
||||
|
||||
```typescript title="array.ts"
|
||||
/* 遍历数组 */
|
||||
function traverse(nums: number[]): void {
|
||||
let count = 0;
|
||||
// 通过索引遍历数组
|
||||
for (let i = 0; i < nums.length; i++) {
|
||||
count++;
|
||||
}
|
||||
// 直接遍历数组
|
||||
for(let num of nums){
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="array.c"
|
||||
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="array.cs"
|
||||
/* 遍历数组 */
|
||||
void Traverse(int[] nums)
|
||||
{
|
||||
int count = 0;
|
||||
// 通过索引遍历数组
|
||||
for (int i = 0; i < nums.Length; i++)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
// 直接遍历数组
|
||||
foreach (int num in nums)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="array.swift"
|
||||
/* 遍历数组 */
|
||||
func traverse(nums: [Int]) {
|
||||
var count = 0
|
||||
// 通过索引遍历数组
|
||||
for _ in nums.indices {
|
||||
count += 1
|
||||
}
|
||||
// 直接遍历数组
|
||||
for _ in nums {
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="array.zig"
|
||||
// 遍历数组
|
||||
pub fn traverse(nums: []i32) void {
|
||||
var count: i32 = 0;
|
||||
// 通过索引遍历数组
|
||||
var i: i32 = 0;
|
||||
while (i < nums.len) : (i += 1) {
|
||||
count += 1;
|
||||
}
|
||||
count = 0;
|
||||
// 直接遍历数组
|
||||
for (nums) |_| {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**数组查找**。通过遍历数组,查找数组内的指定元素,并输出对应索引。
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="array.java"
|
||||
/* 在数组中查找指定元素 */
|
||||
int find(int[] nums, int target) {
|
||||
for (int i = 0; i < nums.length; i++) {
|
||||
if (nums[i] == target)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="array.cpp"
|
||||
/* 在数组中查找指定元素 */
|
||||
int find(int* nums, int size, int target) {
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (nums[i] == target)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="array.py"
|
||||
""" 在数组中查找指定元素 """
|
||||
def find(nums, target):
|
||||
for i in range(len(nums)):
|
||||
if nums[i] == target:
|
||||
return i
|
||||
return -1
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="array.go"
|
||||
/* 在数组中查找指定元素 */
|
||||
func find(nums []int, target int) (index int) {
|
||||
index = -1
|
||||
for i := 0; i < len(nums); i++ {
|
||||
if nums[i] == target {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
=== "JavaScript"
|
||||
|
||||
```javascript title="array.js"
|
||||
/* 在数组中查找指定元素 */
|
||||
function find(nums, target) {
|
||||
for (let i = 0; i < nums.length; i++) {
|
||||
if (nums[i] == target) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "TypeScript"
|
||||
|
||||
```typescript title="array.ts"
|
||||
/* 在数组中查找指定元素 */
|
||||
function find(nums: number[], target: number): number {
|
||||
for (let i = 0; i < nums.length; i++) {
|
||||
if (nums[i] === target) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="array.c"
|
||||
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="array.cs"
|
||||
/* 在数组中查找指定元素 */
|
||||
int Find(int[] nums, int target)
|
||||
{
|
||||
for (int i = 0; i < nums.Length; i++)
|
||||
{
|
||||
if (nums[i] == target)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="array.swift"
|
||||
/* 在数组中查找指定元素 */
|
||||
func find(nums: [Int], target: Int) -> Int {
|
||||
for i in nums.indices {
|
||||
if nums[i] == target {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="array.zig"
|
||||
// 在数组中查找指定元素
|
||||
pub fn find(nums: []i32, target: i32) i32 {
|
||||
for (nums) |num, i| {
|
||||
if (num == target) return @intCast(i32, i);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
## 4.1.4. 数组典型应用
|
||||
|
||||
**随机访问**。如果我们想要随机抽取一些样本,那么可以用数组存储,并生成一个随机序列,根据索引实现样本的随机抽取。
|
||||
|
||||
**二分查找**。例如前文查字典的例子,我们可以将字典中的所有字按照拼音顺序存储在数组中,然后使用与日常查纸质字典相同的“翻开中间,排除一半”的方式,来实现一个查电子字典的算法。
|
||||
|
||||
**深度学习**。神经网络中大量使用了向量、矩阵、张量之间的线性代数运算,这些数据都是以数组的形式构建的。数组是神经网络编程中最常使用的数据结构。
|
||||
979
chapter_array_and_linkedlist/linked_list.md
Executable file
979
chapter_array_and_linkedlist/linked_list.md
Executable file
@@ -0,0 +1,979 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 4.2. 链表
|
||||
|
||||
!!! note "引言"
|
||||
|
||||
内存空间是所有程序的公共资源,排除已占用的内存,空闲内存往往是散落在内存各处的。我们知道,存储数组需要内存空间连续,当我们需要申请一个很大的数组时,系统不一定存在这么大的连续内存空间。而链表则更加灵活,不需要内存是连续的,只要剩余内存空间大小够用即可。
|
||||
|
||||
「链表 Linked List」是一种线性数据结构,其中每个元素都是单独的对象,各个元素(一般称为结点)之间通过指针连接。由于结点中记录了连接关系,因此链表的存储方式相比于数组更加灵活,系统不必保证内存地址的连续性。
|
||||
|
||||
链表的「结点 Node」包含两项数据,一是结点「值 Value」,二是指向下一结点的「指针 Pointer」(或称「引用 Reference」)。
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 链表定义与存储方式 </p>
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title=""
|
||||
/* 链表结点类 */
|
||||
class ListNode {
|
||||
int val; // 结点值
|
||||
ListNode next; // 指向下一结点的指针(引用)
|
||||
ListNode(int x) { val = x; } // 构造函数
|
||||
}
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title=""
|
||||
/* 链表结点结构体 */
|
||||
struct ListNode {
|
||||
int val; // 结点值
|
||||
ListNode *next; // 指向下一结点的指针(引用)
|
||||
ListNode(int x) : val(x), next(nullptr) {} // 构造函数
|
||||
};
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title=""
|
||||
""" 链表结点类 """
|
||||
class ListNode:
|
||||
def __init__(self, x):
|
||||
self.val = x # 结点值
|
||||
self.next = None # 指向下一结点的指针(引用)
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title=""
|
||||
/* 链表结点结构体 */
|
||||
type ListNode struct {
|
||||
Val int // 结点值
|
||||
Next *ListNode // 指向下一结点的指针(引用)
|
||||
}
|
||||
|
||||
// NewListNode 构造函数,创建一个新的链表
|
||||
func NewListNode(val int) *ListNode {
|
||||
return &ListNode{
|
||||
Val: val,
|
||||
Next: nil,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "JavaScript"
|
||||
|
||||
```javascript title=""
|
||||
/* 链表结点结构体 */
|
||||
class ListNode {
|
||||
val;
|
||||
next;
|
||||
constructor(val, next) {
|
||||
this.val = (val === undefined ? 0 : val); // 结点值
|
||||
this.next = (next === undefined ? null : next); // 指向下一结点的引用
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "TypeScript"
|
||||
|
||||
```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; // 指向下一结点的引用
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title=""
|
||||
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title=""
|
||||
/* 链表结点类 */
|
||||
class ListNode
|
||||
{
|
||||
int val; // 结点值
|
||||
ListNode next; // 指向下一结点的引用
|
||||
ListNode(int x) => val = x; //构造函数
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title=""
|
||||
/* 链表结点类 */
|
||||
class ListNode {
|
||||
var val: Int // 结点值
|
||||
var next: ListNode? // 指向下一结点的指针(引用)
|
||||
|
||||
init(x: Int) { // 构造函数
|
||||
val = x
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "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;
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**尾结点指向什么?** 我们一般将链表的最后一个结点称为「尾结点」,其指向的是「空」,在 Java / C++ / Python 中分别记为 `null` / `nullptr` / `None` 。在不引起歧义下,本书都使用 `null` 来表示空。
|
||||
|
||||
**链表初始化方法**。建立链表分为两步,第一步是初始化各个结点对象,第二步是构建引用指向关系。完成后,即可以从链表的首个结点(即头结点)出发,访问其余所有的结点。
|
||||
|
||||
!!! tip
|
||||
|
||||
我们通常将头结点当作链表的代称,例如头结点 `head` 和链表 `head` 实际上是同义的。
|
||||
|
||||
=== "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++"
|
||||
|
||||
```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;
|
||||
```
|
||||
|
||||
=== "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
|
||||
```
|
||||
|
||||
=== "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
|
||||
```
|
||||
|
||||
=== "JavaScript"
|
||||
|
||||
```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;
|
||||
```
|
||||
|
||||
=== "TypeScript"
|
||||
|
||||
```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;
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="linked_list.c"
|
||||
|
||||
```
|
||||
|
||||
=== "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;
|
||||
```
|
||||
|
||||
=== "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
|
||||
```
|
||||
|
||||
=== "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;
|
||||
```
|
||||
|
||||
## 4.2.1. 链表优点
|
||||
|
||||
**在链表中,插入与删除结点的操作效率高**。例如,如果想在链表中间的两个结点 `A` , `B` 之间插入一个新结点 `P` ,我们只需要改变两个结点指针即可,时间复杂度为 $O(1)$ ,相比数组的插入操作高效很多。在链表中删除某个结点也很方便,只需要改变一个结点指针即可。
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 在链表中插入与删除结点 </p>
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="linked_list.java"
|
||||
/* 在链表的结点 n0 之后插入结点 P */
|
||||
void insert(ListNode n0, ListNode P) {
|
||||
ListNode n1 = n0.next;
|
||||
n0.next = P;
|
||||
P.next = n1;
|
||||
}
|
||||
|
||||
/* 删除链表的结点 n0 之后的首个结点 */
|
||||
void remove(ListNode n0) {
|
||||
if (n0.next == null)
|
||||
return;
|
||||
// n0 -> P -> n1
|
||||
ListNode P = n0.next;
|
||||
ListNode n1 = P.next;
|
||||
n0.next = n1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="linked_list.cpp"
|
||||
/* 在链表的结点 n0 之后插入结点 P */
|
||||
void insert(ListNode* n0, ListNode* P) {
|
||||
ListNode* n1 = n0->next;
|
||||
n0->next = P;
|
||||
P->next = n1;
|
||||
}
|
||||
|
||||
/* 删除链表的结点 n0 之后的首个结点 */
|
||||
void remove(ListNode* n0) {
|
||||
if (n0->next == nullptr)
|
||||
return;
|
||||
// n0 -> P -> n1
|
||||
ListNode* P = n0->next;
|
||||
ListNode* n1 = P->next;
|
||||
n0->next = n1;
|
||||
// 释放内存
|
||||
delete P;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="linked_list.py"
|
||||
""" 在链表的结点 n0 之后插入结点 P """
|
||||
def insert(n0, P):
|
||||
n1 = n0.next
|
||||
n0.next = P
|
||||
P.next = n1
|
||||
|
||||
""" 删除链表的结点 n0 之后的首个结点 """
|
||||
def remove(n0):
|
||||
if not n0.next:
|
||||
return
|
||||
# n0 -> P -> n1
|
||||
P = n0.next
|
||||
n1 = P.next
|
||||
n0.next = n1
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="linked_list.go"
|
||||
/* 在链表的结点 n0 之后插入结点 P */
|
||||
func insert(n0 *ListNode, P *ListNode) {
|
||||
n1 := n0.Next
|
||||
n0.Next = P
|
||||
P.Next = n1
|
||||
}
|
||||
|
||||
/* 删除链表的结点 n0 之后的首个结点 */
|
||||
func removeNode(n0 *ListNode) {
|
||||
if n0.Next == nil {
|
||||
return
|
||||
}
|
||||
// n0 -> P -> n1
|
||||
P := n0.Next
|
||||
n1 := P.Next
|
||||
n0.Next = n1
|
||||
}
|
||||
```
|
||||
|
||||
=== "JavaScript"
|
||||
|
||||
```javascript title="linked_list.js"
|
||||
/* 在链表的结点 n0 之后插入结点 P */
|
||||
function insert(n0, P) {
|
||||
let n1 = n0.next;
|
||||
n0.next = P;
|
||||
P.next = n1;
|
||||
}
|
||||
|
||||
/* 删除链表的结点 n0 之后的首个结点 */
|
||||
function remove(n0) {
|
||||
if (!n0.next)
|
||||
return;
|
||||
// n0 -> P -> n1
|
||||
let P = n0.next;
|
||||
let n1 = P.next;
|
||||
n0.next = n1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "TypeScript"
|
||||
|
||||
```typescript title="linked_list.ts"
|
||||
/* 在链表的结点 n0 之后插入结点 P */
|
||||
function insert(n0: ListNode, P: ListNode): void {
|
||||
const n1 = n0.next;
|
||||
n0.next = P;
|
||||
P.next = n1;
|
||||
}
|
||||
|
||||
/* 删除链表的结点 n0 之后的首个结点 */
|
||||
function remove(n0: ListNode): void {
|
||||
if (!n0.next) {
|
||||
return;
|
||||
}
|
||||
// n0 -> P -> n1
|
||||
const P = n0.next;
|
||||
const n1 = P.next;
|
||||
n0.next = n1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="linked_list.c"
|
||||
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="linked_list.cs"
|
||||
// 在链表的结点 n0 之后插入结点 P
|
||||
void Insert(ListNode n0, ListNode P)
|
||||
{
|
||||
ListNode n1 = n0.next;
|
||||
n0.next = P;
|
||||
P.next = n1;
|
||||
}
|
||||
|
||||
// 删除链表的结点 n0 之后的首个结点
|
||||
void Remove(ListNode n0)
|
||||
{
|
||||
if (n0.next == null)
|
||||
return;
|
||||
// n0 -> P -> n1
|
||||
ListNode P = n0.next;
|
||||
ListNode n1 = P.next;
|
||||
n0.next = n1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="linked_list.swift"
|
||||
/* 在链表的结点 n0 之后插入结点 P */
|
||||
func insert(n0: ListNode, P: ListNode) {
|
||||
let n1 = n0.next
|
||||
n0.next = P
|
||||
P.next = n1
|
||||
}
|
||||
|
||||
/* 删除链表的结点 n0 之后的首个结点 */
|
||||
func remove(n0: ListNode) {
|
||||
if n0.next == nil {
|
||||
return
|
||||
}
|
||||
// n0 -> P -> n1
|
||||
let P = n0.next
|
||||
let n1 = P?.next
|
||||
n0.next = n1
|
||||
P?.next = nil
|
||||
}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="linked_list.zig"
|
||||
// 在链表的结点 n0 之后插入结点 P
|
||||
pub fn insert(n0: ?*inc.ListNode(i32), P: ?*inc.ListNode(i32)) void {
|
||||
var n1 = n0.?.next;
|
||||
n0.?.next = P;
|
||||
P.?.next = n1;
|
||||
}
|
||||
|
||||
// 删除链表的结点 n0 之后的首个结点
|
||||
pub fn remove(n0: ?*inc.ListNode(i32)) void {
|
||||
if (n0.?.next == null) return;
|
||||
// n0 -> P -> n1
|
||||
var P = n0.?.next;
|
||||
var n1 = P.?.next;
|
||||
n0.?.next = n1;
|
||||
}
|
||||
```
|
||||
|
||||
## 4.2.2. 链表缺点
|
||||
|
||||
**链表访问结点效率低**。上节提到,数组可以在 $O(1)$ 时间下访问任意元素,但链表无法直接访问任意结点。这是因为计算机需要从头结点出发,一个一个地向后遍历到目标结点。例如,倘若想要访问链表索引为 `index` (即第 `index + 1` 个)的结点,那么需要 `index` 次访问操作。
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="linked_list.java"
|
||||
/* 访问链表中索引为 index 的结点 */
|
||||
ListNode access(ListNode head, int index) {
|
||||
for (int i = 0; i < index; i++) {
|
||||
if (head == null)
|
||||
return null;
|
||||
head = head.next;
|
||||
}
|
||||
return head;
|
||||
}
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="linked_list.cpp"
|
||||
/* 访问链表中索引为 index 的结点 */
|
||||
ListNode* access(ListNode* head, int index) {
|
||||
for (int i = 0; i < index; i++) {
|
||||
if (head == nullptr)
|
||||
return nullptr;
|
||||
head = head->next;
|
||||
}
|
||||
return head;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="linked_list.py"
|
||||
""" 访问链表中索引为 index 的结点 """
|
||||
def access(head, index):
|
||||
for _ in range(index):
|
||||
if not head:
|
||||
return None
|
||||
head = head.next
|
||||
return head
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="linked_list.go"
|
||||
/* 访问链表中索引为 index 的结点 */
|
||||
func access(head *ListNode, index int) *ListNode {
|
||||
for i := 0; i < index; i++ {
|
||||
if head == nil {
|
||||
return nil
|
||||
}
|
||||
head = head.Next
|
||||
}
|
||||
return head
|
||||
}
|
||||
```
|
||||
|
||||
=== "JavaScript"
|
||||
|
||||
```javascript title="linked_list.js"
|
||||
/* 访问链表中索引为 index 的结点 */
|
||||
function access(head, index) {
|
||||
for (let i = 0; i < index; i++) {
|
||||
if (!head)
|
||||
return null;
|
||||
head = head.next;
|
||||
}
|
||||
return head;
|
||||
}
|
||||
```
|
||||
|
||||
=== "TypeScript"
|
||||
|
||||
```typescript title="linked_list.ts"
|
||||
/* 访问链表中索引为 index 的结点 */
|
||||
function access(head: ListNode | null, index: number): ListNode | null {
|
||||
for (let i = 0; i < index; i++) {
|
||||
if (!head) {
|
||||
return null;
|
||||
}
|
||||
head = head.next;
|
||||
}
|
||||
return head;
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="linked_list.c"
|
||||
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="linked_list.cs"
|
||||
// 访问链表中索引为 index 的结点
|
||||
ListNode Access(ListNode head, int index)
|
||||
{
|
||||
for (int i = 0; i < index; i++)
|
||||
{
|
||||
if (head == null)
|
||||
return null;
|
||||
head = head.next;
|
||||
}
|
||||
return head;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="linked_list.swift"
|
||||
/* 访问链表中索引为 index 的结点 */
|
||||
func access(head: ListNode, index: Int) -> ListNode? {
|
||||
var head: ListNode? = head
|
||||
for _ in 0 ..< index {
|
||||
if head == nil {
|
||||
return nil
|
||||
}
|
||||
head = head?.next
|
||||
}
|
||||
return head
|
||||
}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="linked_list.zig"
|
||||
// 访问链表中索引为 index 的结点
|
||||
pub fn access(node: ?*inc.ListNode(i32), index: i32) ?*inc.ListNode(i32) {
|
||||
var head = node;
|
||||
var i: i32 = 0;
|
||||
while (i < index) : (i += 1) {
|
||||
head = head.?.next;
|
||||
if (head == null) return null;
|
||||
}
|
||||
return head;
|
||||
}
|
||||
```
|
||||
|
||||
**链表的内存占用多**。链表以结点为单位,每个结点除了保存值外,还需额外保存指针(引用)。这意味着同样数据量下,链表比数组需要占用更多内存空间。
|
||||
|
||||
## 4.2.3. 链表常用操作
|
||||
|
||||
**遍历链表查找**。遍历链表,查找链表内值为 `target` 的结点,输出结点在链表中的索引。
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="linked_list.java"
|
||||
/* 在链表中查找值为 target 的首个结点 */
|
||||
int find(ListNode head, int target) {
|
||||
int index = 0;
|
||||
while (head != null) {
|
||||
if (head.val == target)
|
||||
return index;
|
||||
head = head.next;
|
||||
index++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="linked_list.cpp"
|
||||
/* 在链表中查找值为 target 的首个结点 */
|
||||
int find(ListNode* head, int target) {
|
||||
int index = 0;
|
||||
while (head != nullptr) {
|
||||
if (head->val == target)
|
||||
return index;
|
||||
head = head->next;
|
||||
index++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="linked_list.py"
|
||||
""" 在链表中查找值为 target 的首个结点 """
|
||||
def find(head, target):
|
||||
index = 0
|
||||
while head:
|
||||
if head.val == target:
|
||||
return index
|
||||
head = head.next
|
||||
index += 1
|
||||
return -1
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="linked_list.go"
|
||||
/* 在链表中查找值为 target 的首个结点 */
|
||||
func find(head *ListNode, target int) int {
|
||||
index := 0
|
||||
for head != nil {
|
||||
if head.Val == target {
|
||||
return index
|
||||
}
|
||||
head = head.Next
|
||||
index++
|
||||
}
|
||||
return -1
|
||||
}
|
||||
```
|
||||
|
||||
=== "JavaScript"
|
||||
|
||||
```javascript title="linked_list.js"
|
||||
/* 在链表中查找值为 target 的首个结点 */
|
||||
function find(head, target) {
|
||||
let index = 0;
|
||||
while (head !== null) {
|
||||
if (head.val === target) {
|
||||
return index;
|
||||
}
|
||||
head = head.next;
|
||||
index += 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "TypeScript"
|
||||
|
||||
```typescript title="linked_list.ts"
|
||||
/* 在链表中查找值为 target 的首个结点 */
|
||||
function find(head: ListNode | null, target: number): number {
|
||||
let index = 0;
|
||||
while (head !== null) {
|
||||
if (head.val === target) {
|
||||
return index;
|
||||
}
|
||||
head = head.next;
|
||||
index += 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="linked_list.c"
|
||||
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="linked_list.cs"
|
||||
// 在链表中查找值为 target 的首个结点
|
||||
int Find(ListNode head, int target)
|
||||
{
|
||||
int index = 0;
|
||||
while (head != null)
|
||||
{
|
||||
if (head.val == target)
|
||||
return index;
|
||||
head = head.next;
|
||||
index++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="linked_list.swift"
|
||||
/* 在链表中查找值为 target 的首个结点 */
|
||||
func find(head: ListNode, target: Int) -> Int {
|
||||
var head: ListNode? = head
|
||||
var index = 0
|
||||
while head != nil {
|
||||
if head?.val == target {
|
||||
return index
|
||||
}
|
||||
head = head?.next
|
||||
index += 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="linked_list.zig"
|
||||
// 在链表中查找值为 target 的首个结点
|
||||
pub fn find(node: ?*inc.ListNode(i32), target: i32) i32 {
|
||||
var head = node;
|
||||
var index: i32 = 0;
|
||||
while (head != null) {
|
||||
if (head.?.val == target) return index;
|
||||
head = head.?.next;
|
||||
index += 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
## 4.2.4. 常见链表类型
|
||||
|
||||
**单向链表**。即上述介绍的普通链表。单向链表的结点有「值」和指向下一结点的「指针(引用)」两项数据。我们将首个结点称为头结点,尾结点指向 `null` 。
|
||||
|
||||
**环形链表**。如果我们令单向链表的尾结点指向头结点(即首尾相接),则得到一个环形链表。在环形链表中,我们可以将任意结点看作是头结点。
|
||||
|
||||
**双向链表**。单向链表仅记录了一个方向的指针(引用),在双向链表的结点定义中,同时有指向下一结点(后继结点)和上一结点(前驱结点)的「指针(引用)」。双向链表相对于单向链表更加灵活,即可以朝两个方向遍历链表,但也需要占用更多的内存空间。
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title=""
|
||||
/* 双向链表结点类 */
|
||||
class ListNode {
|
||||
int val; // 结点值
|
||||
ListNode next; // 指向后继结点的指针(引用)
|
||||
ListNode prev; // 指向前驱结点的指针(引用)
|
||||
ListNode(int x) { val = x; } // 构造函数
|
||||
}
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title=""
|
||||
/* 链表结点结构体 */
|
||||
struct ListNode {
|
||||
int val; // 结点值
|
||||
ListNode *next; // 指向后继结点的指针(引用)
|
||||
ListNode *prev; // 指向前驱结点的指针(引用)
|
||||
ListNode(int x) : val(x), next(nullptr), prev(nullptr) {} // 构造函数
|
||||
};
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title=""
|
||||
""" 双向链表结点类 """
|
||||
class ListNode:
|
||||
def __init__(self, x):
|
||||
self.val = x # 结点值
|
||||
self.next = None # 指向后继结点的指针(引用)
|
||||
self.prev = None # 指向前驱结点的指针(引用)
|
||||
```
|
||||
|
||||
=== "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,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "JavaScript"
|
||||
|
||||
```javascript title=""
|
||||
/* 双向链表结点类 */
|
||||
class ListNode {
|
||||
val;
|
||||
next;
|
||||
prev;
|
||||
constructor(val, next) {
|
||||
this.val = val === undefined ? 0 : val; // 结点值
|
||||
this.next = next === undefined ? null : next; // 指向后继结点的指针(引用)
|
||||
this.prev = prev === undefined ? null : prev; // 指向前驱结点的指针(引用)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "TypeScript"
|
||||
|
||||
```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; // 指向前驱结点的指针(引用)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title=""
|
||||
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title=""
|
||||
/* 双向链表结点类 */
|
||||
class ListNode {
|
||||
int val; // 结点值
|
||||
ListNode next; // 指向后继结点的指针(引用)
|
||||
ListNode prev; // 指向前驱结点的指针(引用)
|
||||
ListNode(int x) => val = x; // 构造函数
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title=""
|
||||
/* 双向链表结点类 */
|
||||
class ListNode {
|
||||
var val: Int // 结点值
|
||||
var next: ListNode? // 指向后继结点的指针(引用)
|
||||
var prev: ListNode? // 指向前驱结点的指针(引用)
|
||||
|
||||
init(x: Int) { // 构造函数
|
||||
val = x
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "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;
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
<p align="center"> Fig. 常见链表类型 </p>
|
||||
1620
chapter_array_and_linkedlist/list.md
Executable file
1620
chapter_array_and_linkedlist/list.md
Executable file
File diff suppressed because it is too large
Load Diff
41
chapter_array_and_linkedlist/summary.md
Normal file
41
chapter_array_and_linkedlist/summary.md
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 4.4. 小结
|
||||
|
||||
- 数组和链表是两种基本数据结构,代表了数据在计算机内存中的两种存储方式,即连续空间存储和离散空间存储。两者的优点与缺点呈现出此消彼长的关系。
|
||||
- 数组支持随机访问、内存空间占用小;但插入与删除元素效率低,且初始化后长度不可变。
|
||||
- 链表可通过更改指针实现高效的结点插入与删除,并且可以灵活地修改长度;但结点访问效率低、占用内存多。常见的链表类型有单向链表、循环链表、双向链表。
|
||||
- 列表又称动态数组,是基于数组实现的一种数据结构,其保存了数组的优势,且可以灵活改变长度。列表的出现大大提升了数组的实用性,但副作用是会造成部分内存空间浪费。
|
||||
|
||||
## 4.4.1. 数组 VS 链表
|
||||
|
||||
<p align="center"> Table. 数组与链表特点对比 </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
| | 数组 | 链表 |
|
||||
| ------------ | ------------------------ | ------------ |
|
||||
| 存储方式 | 连续内存空间 | 离散内存空间 |
|
||||
| 数据结构长度 | 长度不可变 | 长度可变 |
|
||||
| 内存使用率 | 占用内存少、缓存局部性好 | 占用内存多 |
|
||||
| 优势操作 | 随机访问 | 插入、删除 |
|
||||
|
||||
</div>
|
||||
|
||||
!!! tip
|
||||
|
||||
「缓存局部性(Cache locality)」涉及到了计算机操作系统,在本书不做展开介绍,建议有兴趣的同学 Google / Baidu 一下。
|
||||
|
||||
<p align="center"> Table. 数组与链表操作时间复杂度 </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
| 操作 | 数组 | 链表 |
|
||||
| ------- | ------ | ------ |
|
||||
| 访问元素 | $O(1)$ | $O(N)$ |
|
||||
| 添加元素 | $O(N)$ | $O(1)$ |
|
||||
| 删除元素 | $O(N)$ | $O(1)$ |
|
||||
|
||||
</div>
|
||||
Reference in New Issue
Block a user