diff --git a/build/.gitignore b/build/.gitignore new file mode 100644 index 000000000..fd1014369 --- /dev/null +++ b/build/.gitignore @@ -0,0 +1,6 @@ +assets +*.assets + +*.png +*.jpg +*.gif diff --git a/build/chapter_array_and_linkedlist/array.md b/build/chapter_array_and_linkedlist/array.md new file mode 100755 index 000000000..fa4418ee8 --- /dev/null +++ b/build/chapter_array_and_linkedlist/array.md @@ -0,0 +1,909 @@ +--- +comments: true +--- + +# 4.1. 数组 + +「数组 Array」是一种将 **相同类型元素** 存储在 **连续内存空间** 的数据结构,将元素在数组中的位置称为元素的「索引 Index」。 + +![array_definition](array.assets/array_definition.png) + +

Fig. 数组定义与存储方式

+ +!!! 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. 数组优点 + +**在数组中访问元素非常高效**。这是因为在数组中,计算元素的内存地址非常容易。给定数组首个元素的地址、和一个元素的索引,利用以下公式可以直接计算得到该元素的内存地址,从而直接访问此元素。 + +![array_memory_location_calculation](array.assets/array_memory_location_calculation.png) + +

Fig. 数组元素的内存地址计算

+ +```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$ 为数组长度。 +- **丢失元素**:由于数组的长度不可变,因此在插入元素后,超出数组长度范围的元素会被丢失。 +- **内存浪费**:我们一般会初始化一个比较长的数组,只用前面一部分,这样在插入数据时,丢失的末尾元素都是我们不关心的,但这样做同时也会造成内存空间的浪费。 + +![array_insert_remove_element](array.assets/array_insert_remove_element.png) + +

Fig. 在数组中插入与删除元素

+ +=== "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. 数组典型应用 + +**随机访问**。如果我们想要随机抽取一些样本,那么可以用数组存储,并生成一个随机序列,根据索引实现样本的随机抽取。 + +**二分查找**。例如前文查字典的例子,我们可以将字典中的所有字按照拼音顺序存储在数组中,然后使用与日常查纸质字典相同的“翻开中间,排除一半”的方式,来实现一个查电子字典的算法。 + +**深度学习**。神经网络中大量使用了向量、矩阵、张量之间的线性代数运算,这些数据都是以数组的形式构建的。数组是神经网络编程中最常使用的数据结构。 diff --git a/build/chapter_array_and_linkedlist/linked_list.md b/build/chapter_array_and_linkedlist/linked_list.md new file mode 100755 index 000000000..0e17954a5 --- /dev/null +++ b/build/chapter_array_and_linkedlist/linked_list.md @@ -0,0 +1,979 @@ +--- +comments: true +--- + +# 4.2. 链表 + +!!! note "引言" + + 内存空间是所有程序的公共资源,排除已占用的内存,空闲内存往往是散落在内存各处的。我们知道,存储数组需要内存空间连续,当我们需要申请一个很大的数组时,系统不一定存在这么大的连续内存空间。而链表则更加灵活,不需要内存是连续的,只要剩余内存空间大小够用即可。 + +「链表 Linked List」是一种线性数据结构,其中每个元素都是单独的对象,各个元素(一般称为结点)之间通过指针连接。由于结点中记录了连接关系,因此链表的存储方式相比于数组更加灵活,系统不必保证内存地址的连续性。 + +链表的「结点 Node」包含两项数据,一是结点「值 Value」,二是指向下一结点的「指针 Pointer」(或称「引用 Reference」)。 + +![linkedlist_definition](linked_list.assets/linkedlist_definition.png) + +

Fig. 链表定义与存储方式

+ +=== "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" + + ```js 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" + + ```js 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)$ ,相比数组的插入操作高效很多。在链表中删除某个结点也很方便,只需要改变一个结点指针即可。 + +![linkedlist_insert_remove_node](linked_list.assets/linkedlist_insert_remove_node.png) + +

Fig. 在链表中插入与删除结点

+ +=== "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" + + ```js 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" + + ```js 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" + + ```js 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" + + ```js 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; + } + }; + } + ``` + +![linkedlist_common_types](linked_list.assets/linkedlist_common_types.png) + +

Fig. 常见链表类型

diff --git a/build/chapter_array_and_linkedlist/list.md b/build/chapter_array_and_linkedlist/list.md new file mode 100755 index 000000000..5887e6f3f --- /dev/null +++ b/build/chapter_array_and_linkedlist/list.md @@ -0,0 +1,1599 @@ +--- +comments: true +--- + +# 4.3. 列表 + +**由于长度不可变,数组的实用性大大降低**。在很多情况下,我们事先并不知道会输入多少数据,这就为数组长度的选择带来了很大困难。长度选小了,需要在添加数据中频繁地扩容数组;长度选大了,又造成内存空间的浪费。 + +为了解决此问题,诞生了一种被称为「列表 List」的数据结构。列表可以被理解为长度可变的数组,因此也常被称为「动态数组 Dynamic Array」。列表基于数组实现,继承了数组的优点,同时还可以在程序运行中实时扩容。在列表中,我们可以自由地添加元素,而不用担心超过容量限制。 + +## 4.3.1. 列表常用操作 + +**初始化列表**。我们通常会使用到“无初始值”和“有初始值”的两种初始化方法。 + +=== "Java" + + ```java title="list.java" + /* 初始化列表 */ + // 无初始值 + List list1 = new ArrayList<>(); + // 有初始值(注意数组的元素类型需为 int[] 的包装类 Integer[]) + Integer[] numbers = new Integer[] { 1, 3, 2, 5, 4 }; + List list = new ArrayList<>(Arrays.asList(numbers)); + ``` + +=== "C++" + + ```cpp title="list.cpp" + /* 初始化列表 */ + // 需注意,C++ 中 vector 即是本文描述的 list + // 无初始值 + vector list1; + // 有初始值 + vector list = { 1, 3, 2, 5, 4 }; + ``` + +=== "Python" + + ```python title="list.py" + """ 初始化列表 """ + # 无初始值 + list1 = [] + # 有初始值 + list = [1, 3, 2, 5, 4] + ``` + +=== "Go" + + ```go title="list_test.go" + /* 初始化列表 */ + // 无初始值 + list1 := []int + // 有初始值 + list := []int{1, 3, 2, 5, 4} + ``` + +=== "JavaScript" + + ```js title="list.js" + /* 初始化列表 */ + // 无初始值 + const list1 = []; + // 有初始值 + const list = [1, 3, 2, 5, 4]; + ``` + +=== "TypeScript" + + ```typescript title="list.ts" + /* 初始化列表 */ + // 无初始值 + const list1: number[] = []; + // 有初始值 + const list: number[] = [1, 3, 2, 5, 4]; + ``` + +=== "C" + + ```c title="list.c" + + ``` + +=== "C#" + + ```csharp title="list.cs" + /* 初始化列表 */ + // 无初始值 + List list1 = new (); + // 有初始值(注意数组的元素类型需为 int[] 的包装类 Integer[]) + int[] numbers = new int[] { 1, 3, 2, 5, 4 }; + List list = numbers.ToList(); + ``` + +=== "Swift" + + ```swift title="list.swift" + /* 初始化列表 */ + // 无初始值 + let list1: [Int] = [] + // 有初始值 + var list = [1, 3, 2, 5, 4] + ``` + +=== "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)$ 时间内访问与更新元素,效率很高。 + +=== "Java" + + ```java title="list.java" + /* 访问元素 */ + int num = list.get(1); // 访问索引 1 处的元素 + + /* 更新元素 */ + list.set(1, 0); // 将索引 1 处的元素更新为 0 + ``` + +=== "C++" + + ```cpp title="list.cpp" + /* 访问元素 */ + int num = list[1]; // 访问索引 1 处的元素 + + /* 更新元素 */ + list[1] = 0; // 将索引 1 处的元素更新为 0 + ``` + +=== "Python" + + ```python title="list.py" + """ 访问元素 """ + num = list[1] # 访问索引 1 处的元素 + + """ 更新元素 """ + list[1] = 0 # 将索引 1 处的元素更新为 0 + ``` + +=== "Go" + + ```go title="list_test.go" + /* 访问元素 */ + num := list[1] // 访问索引 1 处的元素 + + /* 更新元素 */ + list[1] = 0 // 将索引 1 处的元素更新为 0 + ``` + +=== "JavaScript" + + ```js title="list.js" + /* 访问元素 */ + const num = list[1]; // 访问索引 1 处的元素 + + /* 更新元素 */ + list[1] = 0; // 将索引 1 处的元素更新为 0 + ``` + +=== "TypeScript" + + ```typescript title="list.ts" + /* 访问元素 */ + const num: number = list[1]; // 访问索引 1 处的元素 + + /* 更新元素 */ + list[1] = 0; // 将索引 1 处的元素更新为 0 + ``` + +=== "C" + + ```c title="list.c" + + ``` + +=== "C#" + + ```csharp title="list.cs" + /* 访问元素 */ + int num = list[1]; // 访问索引 1 处的元素 + + /* 更新元素 */ + list[1] = 0; // 将索引 1 处的元素更新为 0 + ``` + +=== "Swift" + + ```swift title="list.swift" + /* 访问元素 */ + let num = list[1] // 访问索引 1 处的元素 + + /* 更新元素 */ + list[1] = 0 // 将索引 1 处的元素更新为 0 + ``` + +=== "Zig" + + ```zig title="list.zig" + // 访问元素 + var num = list.items[1]; // 访问索引 1 处的元素 + + // 更新元素 + list.items[1] = 0; // 将索引 1 处的元素更新为 0 + ``` + +**在列表中添加、插入、删除元素**。相对于数组,列表可以自由地添加与删除元素。在列表尾部添加元素的时间复杂度为 $O(1)$ ,但是插入与删除元素的效率仍与数组一样低,时间复杂度为 $O(N)$ 。 + +=== "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++" + + ```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 处的元素 + ``` + +=== "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 处的元素 + ``` + +=== "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 处的元素 + ``` + +=== "JavaScript" + + ```js 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); + ``` + +=== "TypeScript" + + ```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); + ``` + +=== "C" + + ```c title="list.c" + + ``` + +=== "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); + ``` + +=== "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 处的元素 + ``` + +=== "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 处的元素 + ``` + +**遍历列表**。与数组一样,列表可以使用索引遍历,也可以使用 `for-each` 直接遍历。 + +=== "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++" + + ```cpp title="list.cpp" + /* 通过索引遍历列表 */ + int count = 0; + for (int i = 0; i < list.size(); i++) { + count++; + } + + /* 直接遍历列表元素 */ + count = 0; + for (int n : list) { + count++; + } + ``` + +=== "Python" + + ```python title="list.py" + """ 通过索引遍历列表 """ + count = 0 + for i in range(len(list)): + count += 1 + + """ 直接遍历列表元素 """ + count = 0 + for n in list: + count += 1 + ``` + +=== "Go" + + ```go title="list_test.go" + /* 通过索引遍历列表 */ + count := 0 + for i := 0; i < len(list); i++ { + count++ + } + + /* 直接遍历列表元素 */ + count = 0 + for range list { + count++ + } + ``` + +=== "JavaScript" + + ```js title="list.js" + /* 通过索引遍历列表 */ + let count = 0; + for (let i = 0; i < list.length; i++) { + count++; + } + + /* 直接遍历列表元素 */ + count = 0; + for (const n of list) { + count++; + } + ``` + +=== "TypeScript" + + ```typescript title="list.ts" + /* 通过索引遍历列表 */ + let count = 0; + for (let i = 0; i < list.length; i++) { + count++; + } + + /* 直接遍历列表元素 */ + count = 0; + for (const n of list) { + count++; + } + ``` + +=== "C" + + ```c title="list.c" + + ``` + +=== "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++; + } + ``` + +=== "Swift" + + ```swift title="list.swift" + /* 通过索引遍历列表 */ + var count = 0 + for _ in list.indices { + count += 1 + } + + /* 直接遍历列表元素 */ + count = 0 + for _ in list { + count += 1 + } + ``` + +=== "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` ,我们可以将其中一个列表拼接到另一个的尾部。 + +=== "Java" + + ```java title="list.java" + /* 拼接两个列表 */ + List list1 = new ArrayList<>(Arrays.asList(new Integer[] { 6, 8, 7, 10, 9 })); + list.addAll(list1); // 将列表 list1 拼接到 list 之后 + ``` + +=== "C++" + + ```cpp title="list.cpp" + /* 拼接两个列表 */ + vector list1 = { 6, 8, 7, 10, 9 }; + // 将列表 list1 拼接到 list 之后 + list.insert(list.end(), list1.begin(), list1.end()); + ``` + +=== "Python" + + ```python title="list.py" + """ 拼接两个列表 """ + list1 = [6, 8, 7, 10, 9] + list += list1 # 将列表 list1 拼接到 list 之后 + ``` + +=== "Go" + + ```go title="list_test.go" + /* 拼接两个列表 */ + list1 := []int{6, 8, 7, 10, 9} + list = append(list, list1...) // 将列表 list1 拼接到 list 之后 + ``` + +=== "JavaScript" + + ```js title="list.js" + /* 拼接两个列表 */ + const list1 = [6, 8, 7, 10, 9]; + list.push(...list1); // 将列表 list1 拼接到 list 之后 + ``` + +=== "TypeScript" + + ```typescript title="list.ts" + /* 拼接两个列表 */ + const list1: number[] = [6, 8, 7, 10, 9]; + list.push(...list1); // 将列表 list1 拼接到 list 之后 + ``` + +=== "C" + + ```c title="list.c" + + ``` + +=== "C#" + + ```csharp title="list.cs" + /* 拼接两个列表 */ + List list1 = new() { 6, 8, 7, 10, 9 }; + list.AddRange(list1); // 将列表 list1 拼接到 list 之后 + ``` + +=== "Swift" + + ```swift title="list.swift" + /* 拼接两个列表 */ + let list1 = [6, 8, 7, 10, 9] + list.append(contentsOf: list1) // 将列表 list1 拼接到 list 之后 + ``` + +=== "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 之后 + ``` + +**排序列表**。排序也是常用的方法之一,完成列表排序后,我们就可以使用在数组类算法题中经常考察的「二分查找」和「双指针」算法了。 + +=== "Java" + + ```java title="list.java" + /* 排序列表 */ + Collections.sort(list); // 排序后,列表元素从小到大排列 + ``` + +=== "C++" + + ```cpp title="list.cpp" + /* 排序列表 */ + sort(list.begin(), list.end()); // 排序后,列表元素从小到大排列 + ``` + +=== "Python" + + ```python title="list.py" + """ 排序列表 """ + list.sort() # 排序后,列表元素从小到大排列 + ``` + +=== "Go" + + ```go title="list_test.go" + /* 排序列表 */ + sort.Ints(list) // 排序后,列表元素从小到大排列 + ``` + +=== "JavaScript" + + ```js title="list.js" + /* 排序列表 */ + list.sort((a, b) => a - b); // 排序后,列表元素从小到大排列 + ``` + +=== "TypeScript" + + ```typescript title="list.ts" + /* 排序列表 */ + list.sort((a, b) => a - b); // 排序后,列表元素从小到大排列 + ``` + +=== "C" + + ```c title="list.c" + + ``` + +=== "C#" + + ```csharp title="list.cs" + /* 排序列表 */ + list.Sort(); // 排序后,列表元素从小到大排列 + ``` + +=== "Swift" + + ```swift title="list.swift" + /* 排序列表 */ + list.sort() // 排序后,列表元素从小到大排列 + ``` + +=== "Zig" + + ```zig title="list.zig" + // 排序列表 + std.sort.sort(i32, list.items, {}, comptime std.sort.asc(i32)); + ``` + +## 4.3.2. 列表简易实现 * + +为了帮助加深对列表的理解,我们在此提供一个列表的简易版本的实现。需要关注三个核心点: + +- **初始容量**:选取一个合理的数组的初始容量 `initialCapacity` 。在本示例中,我们选择 10 作为初始容量。 +- **数量记录**:需要声明一个变量 `size` ,用来记录列表当前有多少个元素,并随着元素插入与删除实时更新。根据此变量,可以定位列表的尾部,以及判断是否需要扩容。 +- **扩容机制**:插入元素有可能导致超出列表容量,此时需要扩容列表,方法是建立一个更大的数组来替换当前数组。需要给定一个扩容倍数 `extendRatio` ,在本示例中,我们规定每次将数组扩容至之前的 2 倍。 + +本示例是为了帮助读者对如何实现列表产生直观的认识。实际编程语言中,列表的实现远比以下代码复杂且标准,感兴趣的读者可以查阅源码学习。 + +=== "Java" + + ```java title="my_list.java" + /* 列表类简易实现 */ + class MyList { + private int[] nums; // 数组(存储列表元素) + private int capacity = 10; // 列表容量 + private int size = 0; // 列表长度(即当前元素数量) + private int extendRatio = 2; // 每次列表扩容的倍数 + + /* 构造函数 */ + public MyList() { + nums = new int[capacity]; + } + + /* 获取列表长度(即当前元素数量)*/ + public int size() { + return size; + } + + /* 获取列表容量 */ + public int capacity() { + return capacity; + } + + /* 访问元素 */ + public int get(int index) { + // 索引如果越界则抛出异常,下同 + if (index < 0 || index >= size) + throw new IndexOutOfBoundsException("索引越界"); + return nums[index]; + } + + /* 更新元素 */ + public void set(int index, int num) { + if (index < 0 || index >= size) + throw new IndexOutOfBoundsException("索引越界"); + nums[index] = num; + } + + /* 尾部添加元素 */ + public void add(int num) { + // 元素数量超出容量时,触发扩容机制 + if (size == capacity()) + extendCapacity(); + nums[size] = num; + // 更新元素数量 + size++; + } + + /* 中间插入元素 */ + public void insert(int index, int num) { + if (index < 0 || index >= size) + throw new IndexOutOfBoundsException("索引越界"); + // 元素数量超出容量时,触发扩容机制 + if (size == capacity()) + extendCapacity(); + // 将索引 index 以及之后的元素都向后移动一位 + for (int j = size - 1; j >= index; j--) { + nums[j + 1] = nums[j]; + } + nums[index] = num; + // 更新元素数量 + size++; + } + + /* 删除元素 */ + public int remove(int index) { + if (index < 0 || index >= size) + throw new IndexOutOfBoundsException("索引越界"); + int num = nums[index]; + // 将索引 index 之后的元素都向前移动一位 + for (int j = index; j < size - 1; j++) { + nums[j] = nums[j + 1]; + } + // 更新元素数量 + size--; + // 返回被删除元素 + return num; + } + + /* 列表扩容 */ + public void extendCapacity() { + // 新建一个长度为 size 的数组,并将原数组拷贝到新数组 + nums = Arrays.copyOf(nums, capacity() * extendRatio); + // 更新列表容量 + capacity = nums.length; + } + } + ``` + +=== "C++" + + ```cpp title="my_list.cpp" + /* 列表类简易实现 */ + class MyList { + private: + int* nums; // 数组(存储列表元素) + int numsCapacity = 10; // 列表容量 + int numsSize = 0; // 列表长度(即当前元素数量) + int extendRatio = 2; // 每次列表扩容的倍数 + + public: + /* 构造函数 */ + MyList() { + nums = new int[numsCapacity]; + } + + /* 析构函数 */ + ~MyList() { + delete[] nums; + } + + /* 获取列表长度(即当前元素数量)*/ + int size() { + return numsSize; + } + + /* 获取列表容量 */ + int capacity() { + return numsCapacity; + } + + /* 访问元素 */ + int get(int index) { + // 索引如果越界则抛出异常,下同 + if (index < 0 || index >= size()) + throw out_of_range("索引越界"); + return nums[index]; + } + + /* 更新元素 */ + void set(int index, int num) { + if (index < 0 || index >= size()) + throw out_of_range("索引越界"); + nums[index] = num; + } + + /* 尾部添加元素 */ + void add(int num) { + // 元素数量超出容量时,触发扩容机制 + if (size() == capacity()) + extendCapacity(); + nums[size()] = num; + // 更新元素数量 + numsSize++; + } + + /* 中间插入元素 */ + void insert(int index, int num) { + if (index < 0 || index >= size()) + throw out_of_range("索引越界"); + // 元素数量超出容量时,触发扩容机制 + if (size() == capacity()) + extendCapacity(); + // 索引 i 以及之后的元素都向后移动一位 + for (int j = size() - 1; j >= index; j--) { + nums[j + 1] = nums[j]; + } + nums[index] = num; + // 更新元素数量 + numsSize++; + } + + /* 删除元素 */ + int remove(int index) { + if (index < 0 || index >= size()) + throw out_of_range("索引越界"); + int num = nums[index]; + // 索引 i 之后的元素都向前移动一位 + for (int j = index; j < size() - 1; j++) { + nums[j] = nums[j + 1]; + } + // 更新元素数量 + numsSize--; + // 返回被删除元素 + return num; + } + + /* 列表扩容 */ + void extendCapacity() { + // 新建一个长度为 size * extendRatio 的数组,并将原数组拷贝到新数组 + int newCapacity = capacity() * extendRatio; + int* tmp = nums; + nums = new int[newCapacity]; + // 将原数组中的所有元素复制到新数组 + for (int i = 0; i < size(); i++) { + nums[i] = tmp[i]; + } + // 释放内存 + delete[] tmp; + numsCapacity = newCapacity; + } + }; + ``` + +=== "Python" + + ```python title="my_list.py" + """ 列表类简易实现 """ + class MyList: + """ 构造函数 """ + def __init__(self): + self.__capacity = 10 # 列表容量 + self.__nums = [0] * self.__capacity # 数组(存储列表元素) + self.__size = 0 # 列表长度(即当前元素数量) + self.__extend_ratio = 2 # 每次列表扩容的倍数 + + """ 获取列表长度(即当前元素数量) """ + def size(self): + return self.__size + + """ 获取列表容量 """ + def capacity(self): + return self.__capacity + + """ 访问元素 """ + def get(self, index): + # 索引如果越界则抛出异常,下同 + assert index >= 0 and index < self.__size, "索引越界" + return self.__nums[index] + + """ 更新元素 """ + def set(self, num, index): + assert index >= 0 and index < self.__size, "索引越界" + self.__nums[index] = num + + """ 中间插入(尾部添加)元素 """ + def add(self, num, index=-1): + assert index >= 0 and index < self.__size, "索引越界" + # 若不指定索引 index ,则向数组尾部添加元素 + if index == -1: + index = self.__size + # 元素数量超出容量时,触发扩容机制 + if self.__size == self.capacity(): + self.extend_capacity() + # 索引 i 以及之后的元素都向后移动一位 + for j in range(self.__size - 1, index - 1, -1): + self.__nums[j + 1] = self.__nums[j] + self.__nums[index] = num + # 更新元素数量 + self.__size += 1 + + """ 删除元素 """ + def remove(self, index): + assert index >= 0 and index < self.__size, "索引越界" + num = self.nums[index] + # 索引 i 之后的元素都向前移动一位 + for j in range(index, self.__size - 1): + self.__nums[j] = self.__nums[j + 1] + # 更新元素数量 + self.__size -= 1 + # 返回被删除元素 + return num + + """ 列表扩容 """ + def extend_capacity(self): + # 新建一个长度为 self.__size 的数组,并将原数组拷贝到新数组 + self.__nums = self.__nums + [0] * self.capacity() * (self.__extend_ratio - 1) + # 更新列表容量 + self.__capacity = len(self.__nums) + + """ 返回有效长度的列表 """ + def to_array(self): + return self.__nums[:self.__size] + ``` + +=== "Go" + + ```go title="my_list.go" + /* 列表类简易实现 */ + type myList struct { + numsCapacity int + nums []int + numsSize int + extendRatio int + } + + /* 构造函数 */ + func newMyList() *myList { + return &myList{ + numsCapacity: 10, // 列表容量 + nums: make([]int, 10), // 数组(存储列表元素) + numsSize: 0, // 列表长度(即当前元素数量) + extendRatio: 2, // 每次列表扩容的倍数 + } + } + + /* 获取列表长度(即当前元素数量) */ + func (l *myList) size() int { + return l.numsSize + } + + /* 获取列表容量 */ + func (l *myList) capacity() int { + return l.numsCapacity + } + + /* 访问元素 */ + func (l *myList) get(index int) int { + // 索引如果越界则抛出异常,下同 + if index < 0 || index >= l.numsSize { + panic("索引越界") + } + return l.nums[index] + } + + /* 更新元素 */ + func (l *myList) set(num, index int) { + if index < 0 || index >= l.numsSize { + panic("索引越界") + } + l.nums[index] = num + } + + /* 尾部添加元素 */ + func (l *myList) add(num int) { + // 元素数量超出容量时,触发扩容机制 + if l.numsSize == l.numsCapacity { + l.extendCapacity() + } + l.nums[l.numsSize] = num + // 更新元素数量 + l.numsSize++ + } + + /* 中间插入元素 */ + func (l *myList) insert(num, index int) { + if index < 0 || index >= l.numsSize { + panic("索引越界") + } + // 元素数量超出容量时,触发扩容机制 + if l.numsSize == l.numsCapacity { + l.extendCapacity() + } + // 索引 i 以及之后的元素都向后移动一位 + for j := l.numsSize - 1; j >= index; j-- { + l.nums[j+1] = l.nums[j] + } + l.nums[index] = num + // 更新元素数量 + l.numsSize++ + } + + /* 删除元素 */ + func (l *myList) remove(index int) int { + if index < 0 || index >= l.numsSize { + panic("索引越界") + } + num := l.nums[index] + // 索引 i 之后的元素都向前移动一位 + for j := index; j < l.numsSize-1; j++ { + l.nums[j] = l.nums[j+1] + } + // 更新元素数量 + l.numsSize-- + // 返回被删除元素 + return num + } + + /* 列表扩容 */ + func (l *myList) extendCapacity() { + // 新建一个长度为 self.__size 的数组,并将原数组拷贝到新数组 + l.nums = append(l.nums, make([]int, l.numsCapacity*(l.extendRatio-1))...) + // 更新列表容量 + l.numsCapacity = len(l.nums) + } + ``` + +=== "JavaScript" + + ```js title="my_list.js" + /* 列表类简易实现 */ + class MyList { + #nums = new Array(); // 数组(存储列表元素) + #capacity = 10; // 列表容量 + #size = 0; // 列表长度(即当前元素数量) + #extendRatio = 2; // 每次列表扩容的倍数 + + /* 构造函数 */ + constructor() { + this.#nums = new Array(this.#capacity); + } + + /* 获取列表长度(即当前元素数量)*/ + size() { + return this.#size; + } + + /* 获取列表容量 */ + capacity() { + return this.#capacity; + } + + /* 访问元素 */ + get(index) { + // 索引如果越界则抛出异常,下同 + if (index < 0 || index >= this.#size) + throw new Error('索引越界'); + return this.#nums[index]; + } + + /* 更新元素 */ + set(index, num) { + if (index < 0 || index >= this.#size) + throw new Error('索引越界'); + this.#nums[index] = num; + } + + /* 尾部添加元素 */ + add(num) { + // 如果长度等于容量,则需要扩容 + if (this.#size === this.#capacity) { + this.extendCapacity(); + } + // 将新元素添加到列表尾部 + this.#nums[this.#size] = num; + this.#size++; + } + + /* 中间插入元素 */ + insert(index, num) { + if (index < 0 || index >= this.#size) + throw new Error('索引越界'); + // 元素数量超出容量时,触发扩容机制 + if (this.#size === this.#capacity) { + this.extendCapacity(); + } + // 将索引 index 以及之后的元素都向后移动一位 + for (let j = this.#size - 1; j >= index; j--) { + this.#nums[j + 1] = this.#nums[j]; + } + // 更新元素数量 + this.#nums[index] = num; + this.#size++; + } + + /* 删除元素 */ + remove(index) { + if (index < 0 || index >= this.#size) + throw new Error('索引越界'); + let num = this.#nums[index]; + // 将索引 index 之后的元素都向前移动一位 + for (let j = index; j < this.#size - 1; j++) { + this.#nums[j] = this.#nums[j + 1]; + } + // 更新元素数量 + this.#size--; + // 返回被删除元素 + return num; + } + + /* 列表扩容 */ + extendCapacity() { + // 新建一个长度为 size 的数组,并将原数组拷贝到新数组 + this.#nums = this.#nums.concat( + new Array(this.capacity() * (this.#extendRatio - 1)) + ); + // 更新列表容量 + this.#capacity = this.#nums.length; + } + } + ``` + +=== "TypeScript" + + ```typescript title="my_list.ts" + /* 列表类简易实现 */ + class MyList { + private nums: Array; // 数组(存储列表元素) + private _capacity: number = 10; // 列表容量 + private _size: number = 0; // 列表长度(即当前元素数量) + private extendRatio: number = 2; // 每次列表扩容的倍数 + + /* 构造函数 */ + constructor() { + this.nums = new Array(this._capacity); + } + + /* 获取列表长度(即当前元素数量)*/ + public size(): number { + return this._size; + } + + /* 获取列表容量 */ + public capacity(): number { + return this._capacity; + } + + /* 访问元素 */ + public get(index: number): number { + // 索引如果越界则抛出异常,下同 + if (index < 0 || index >= this._size) + throw new Error('索引越界'); + return this.nums[index]; + } + + /* 更新元素 */ + public set(index: number, num: number): void { + if (index < 0 || index >= this._size) + throw new Error('索引越界'); + this.nums[index] = num; + } + + /* 尾部添加元素 */ + public add(num: number): void { + // 如果长度等于容量,则需要扩容 + if (this._size === this._capacity) + this.extendCapacity(); + // 将新元素添加到列表尾部 + this.nums[this._size] = num; + this._size++; + } + + /* 中间插入元素 */ + public insert(index: number, num: number): void { + if (index < 0 || index >= this._size) + throw new Error('索引越界'); + // 元素数量超出容量时,触发扩容机制 + if (this._size === this._capacity) { + this.extendCapacity(); + } + // 将索引 index 以及之后的元素都向后移动一位 + for (let j = this._size - 1; j >= index; j--) { + this.nums[j + 1] = this.nums[j]; + } + // 更新元素数量 + this.nums[index] = num; + this._size++; + } + + /* 删除元素 */ + public remove(index: number): number { + if (index < 0 || index >= this._size) + throw new Error('索引越界'); + let num = this.nums[index]; + // 将索引 index 之后的元素都向前移动一位 + for (let j = index; j < this._size - 1; j++) { + this.nums[j] = this.nums[j + 1]; + } + // 更新元素数量 + this._size--; + // 返回被删除元素 + return num; + } + + /* 列表扩容 */ + public extendCapacity(): void { + // 新建一个长度为 size 的数组,并将原数组拷贝到新数组 + this.nums = this.nums.concat( + new Array(this.capacity() * (this.extendRatio - 1)) + ); + // 更新列表容量 + this._capacity = this.nums.length; + } + } + ``` + +=== "C" + + ```c title="my_list.c" + + ``` + +=== "C#" + + ```csharp title="my_list.cs" + class MyList + { + private int[] nums; // 数组(存储列表元素) + private int capacity = 10; // 列表容量 + private int size = 0; // 列表长度(即当前元素数量) + private int extendRatio = 2; // 每次列表扩容的倍数 + + /* 构造函数 */ + public MyList() + { + nums = new int[capacity]; + } + + /* 获取列表长度(即当前元素数量)*/ + public int Size() + { + return size; + } + + /* 获取列表容量 */ + public int Capacity() + { + return capacity; + } + + /* 访问元素 */ + public int Get(int index) + { + // 索引如果越界则抛出异常,下同 + if (index < 0 || index >= size) + throw new IndexOutOfRangeException("索引越界"); + return nums[index]; + } + + /* 更新元素 */ + public void Set(int index, int num) + { + if (index < 0 || index >= size) + throw new IndexOutOfRangeException("索引越界"); + nums[index] = num; + } + + /* 尾部添加元素 */ + public void Add(int num) + { + // 元素数量超出容量时,触发扩容机制 + if (size == Capacity()) + ExtendCapacity(); + nums[size] = num; + // 更新元素数量 + size++; + } + + /* 中间插入元素 */ + public void Insert(int index, int num) + { + if (index < 0 || index >= size) + throw new IndexOutOfRangeException("索引越界"); + // 元素数量超出容量时,触发扩容机制 + if (size == Capacity()) + ExtendCapacity(); + // 将索引 index 以及之后的元素都向后移动一位 + for (int j = size - 1; j >= index; j--) + { + nums[j + 1] = nums[j]; + } + nums[index] = num; + // 更新元素数量 + size++; + } + + /* 删除元素 */ + public int Remove(int index) + { + if (index < 0 || index >= size) + throw new IndexOutOfRangeException("索引越界"); + int num = nums[index]; + // 将索引 index 之后的元素都向前移动一位 + for (int j = index; j < size - 1; j++) + { + nums[j] = nums[j + 1]; + } + // 更新元素数量 + size--; + // 返回被删除元素 + return num; + } + + /* 列表扩容 */ + public void ExtendCapacity() + { + // 新建一个长度为 size 的数组,并将原数组拷贝到新数组 + System.Array.Resize(ref nums, Capacity() * extendRatio); + // 更新列表容量 + capacity = nums.Length; + } + } + ``` + +=== "Swift" + + ```swift title="my_list.swift" + /* 列表类简易实现 */ + class MyList { + private var nums: [Int] // 数组(存储列表元素) + private var _capacity = 10 // 列表容量 + private var _size = 0 // 列表长度(即当前元素数量) + private let extendRatio = 2 // 每次列表扩容的倍数 + + /* 构造函数 */ + init() { + nums = Array(repeating: 0, count: _capacity) + } + + /* 获取列表长度(即当前元素数量)*/ + func size() -> Int { + _size + } + + /* 获取列表容量 */ + func capacity() -> Int { + _capacity + } + + /* 访问元素 */ + func get(index: Int) -> Int { + // 索引如果越界则抛出错误,下同 + if index < 0 || index >= _size { + fatalError("索引越界") + } + return nums[index] + } + + /* 更新元素 */ + func set(index: Int, num: Int) { + if index < 0 || index >= _size { + fatalError("索引越界") + } + nums[index] = num + } + + /* 尾部添加元素 */ + func add(num: Int) { + // 元素数量超出容量时,触发扩容机制 + if _size == _capacity { + extendCapacity() + } + nums[_size] = num + // 更新元素数量 + _size += 1 + } + + /* 中间插入元素 */ + func insert(index: Int, num: Int) { + if index < 0 || index >= _size { + fatalError("索引越界") + } + // 元素数量超出容量时,触发扩容机制 + if _size == _capacity { + extendCapacity() + } + // 将索引 index 以及之后的元素都向后移动一位 + for j in sequence(first: _size - 1, next: { $0 >= index + 1 ? $0 - 1 : nil }) { + nums[j + 1] = nums[j] + } + nums[index] = num + // 更新元素数量 + _size += 1 + } + + /* 删除元素 */ + @discardableResult + func remove(index: Int) -> Int { + if index < 0 || index >= _size { + fatalError("索引越界") + } + let num = nums[index] + // 将索引 index 之后的元素都向前移动一位 + for j in index ..< (_size - 1) { + nums[j] = nums[j + 1] + } + // 更新元素数量 + _size -= 1 + // 返回被删除元素 + return num + } + + /* 列表扩容 */ + func extendCapacity() { + // 新建一个长度为 size 的数组,并将原数组拷贝到新数组 + nums = nums + Array(repeating: 0, count: _capacity * (extendRatio - 1)) + // 更新列表容量 + _capacity = nums.count + } + } + ``` + +=== "Zig" + + ```zig title="my_list.zig" + // 列表类简易实现 + pub fn MyList(comptime T: type) type { + return struct { + const Self = @This(); + + nums: []T = undefined, // 数组(存储列表元素) + numsCapacity: usize = 10, // 列表容量 + numSize: usize = 0, // 列表长度(即当前元素数量) + extendRatio: usize = 2, // 每次列表扩容的倍数 + mem_arena: ?std.heap.ArenaAllocator = null, + mem_allocator: std.mem.Allocator = undefined, // 内存分配器 + + // 构造函数(分配内存+初始化列表) + pub fn init(self: *Self, allocator: std.mem.Allocator) !void { + if (self.mem_arena == null) { + self.mem_arena = std.heap.ArenaAllocator.init(allocator); + self.mem_allocator = self.mem_arena.?.allocator(); + } + self.nums = try self.mem_allocator.alloc(T, self.numsCapacity); + std.mem.set(T, self.nums, @as(T, 0)); + } + + // 析构函数(释放内存) + pub fn deinit(self: *Self) void { + if (self.mem_arena == null) return; + self.mem_arena.?.deinit(); + } + + // 获取列表长度(即当前元素数量) + pub fn size(self: *Self) usize { + return self.numSize; + } + + // 获取列表容量 + pub fn capacity(self: *Self) usize { + return self.numsCapacity; + } + + // 访问元素 + pub fn get(self: *Self, index: usize) T { + // 索引如果越界则抛出异常,下同 + if (index < 0 or index >= self.size()) @panic("索引越界"); + return self.nums[index]; + } + + // 更新元素 + pub fn set(self: *Self, index: usize, num: T) void { + // 索引如果越界则抛出异常,下同 + if (index < 0 or index >= self.size()) @panic("索引越界"); + self.nums[index] = num; + } + + // 尾部添加元素 + pub fn add(self: *Self, num: T) !void { + // 元素数量超出容量时,触发扩容机制 + if (self.size() == self.capacity()) try self.extendCapacity(); + self.nums[self.size()] = num; + // 更新元素数量 + self.numSize += 1; + } + + // 中间插入元素 + pub fn insert(self: *Self, index: usize, num: T) !void { + if (index < 0 or index >= self.size()) @panic("索引越界"); + // 元素数量超出容量时,触发扩容机制 + if (self.size() == self.capacity()) try self.extendCapacity(); + // 索引 i 以及之后的元素都向后移动一位 + var j = self.size() - 1; + while (j >= index) : (j -= 1) { + self.nums[j + 1] = self.nums[j]; + } + self.nums[index] = num; + // 更新元素数量 + self.numSize += 1; + } + + // 删除元素 + pub fn remove(self: *Self, index: usize) T { + if (index < 0 or index >= self.size()) @panic("索引越界"); + var num = self.nums[index]; + // 索引 i 之后的元素都向前移动一位 + var j = index; + while (j < self.size() - 1) : (j += 1) { + self.nums[j] = self.nums[j + 1]; + } + // 更新元素数量 + self.numSize -= 1; + // 返回被删除元素 + return num; + } + + // 列表扩容 + pub fn extendCapacity(self: *Self) !void { + // 新建一个长度为 size * extendRatio 的数组,并将原数组拷贝到新数组 + var newCapacity = self.capacity() * self.extendRatio; + var extend = try self.mem_allocator.alloc(T, newCapacity); + std.mem.set(T, extend, @as(T, 0)); + // 将原数组中的所有元素复制到新数组 + std.mem.copy(T, extend, self.nums); + self.nums = extend; + // 更新列表容量 + self.numsCapacity = newCapacity; + } + + // 将列表转换为数组 + pub fn toArray(self: *Self) ![]T { + // 仅转换有效长度范围内的列表元素 + var nums = try self.mem_allocator.alloc(T, self.size()); + std.mem.set(T, nums, @as(T, 0)); + for (nums) |*num, i| { + num.* = self.get(i); + } + return nums; + } + }; + } + ``` diff --git a/build/chapter_array_and_linkedlist/summary.md b/build/chapter_array_and_linkedlist/summary.md new file mode 100644 index 000000000..3b6ac2cb0 --- /dev/null +++ b/build/chapter_array_and_linkedlist/summary.md @@ -0,0 +1,41 @@ +--- +comments: true +--- + +# 4.4. 小结 + +- 数组和链表是两种基本数据结构,代表了数据在计算机内存中的两种存储方式,即连续空间存储和离散空间存储。两者的优点与缺点呈现出此消彼长的关系。 +- 数组支持随机访问、内存空间占用小;但插入与删除元素效率低,且初始化后长度不可变。 +- 链表可通过更改指针实现高效的结点插入与删除,并且可以灵活地修改长度;但结点访问效率低、占用内存多。常见的链表类型有单向链表、循环链表、双向链表。 +- 列表又称动态数组,是基于数组实现的一种数据结构,其保存了数组的优势,且可以灵活改变长度。列表的出现大大提升了数组的实用性,但副作用是会造成部分内存空间浪费。 + +## 4.4.1. 数组 VS 链表 + +

Table. 数组与链表特点对比

+ +
+ +| | 数组 | 链表 | +| ------------ | ------------------------ | ------------ | +| 存储方式 | 连续内存空间 | 离散内存空间 | +| 数据结构长度 | 长度不可变 | 长度可变 | +| 内存使用率 | 占用内存少、缓存局部性好 | 占用内存多 | +| 优势操作 | 随机访问 | 插入、删除 | + +
+ +!!! tip + + 「缓存局部性(Cache locality)」涉及到了计算机操作系统,在本书不做展开介绍,建议有兴趣的同学 Google / Baidu 一下。 + +

Table. 数组与链表操作时间复杂度

+ +
+ +| 操作 | 数组 | 链表 | +| ------- | ------ | ------ | +| 访问元素 | $O(1)$ | $O(N)$ | +| 添加元素 | $O(N)$ | $O(1)$ | +| 删除元素 | $O(N)$ | $O(1)$ | + +
diff --git a/build/chapter_computational_complexity/performance_evaluation.md b/build/chapter_computational_complexity/performance_evaluation.md new file mode 100644 index 000000000..6c236c864 --- /dev/null +++ b/build/chapter_computational_complexity/performance_evaluation.md @@ -0,0 +1,43 @@ +--- +comments: true +--- + +# 2.1. 算法效率评估 + +## 2.1.1. 算法评价维度 + +在开始学习算法之前,我们首先要想清楚算法的设计目标是什么,或者说,如何来评判算法的好与坏。整体上看,我们设计算法时追求两个层面的目标。 + +1. **找到问题解法**。算法需要能够在规定的输入范围下,可靠地求得问题的正确解。 +2. **寻求最优解法**。同一个问题可能存在多种解法,而我们希望算法效率尽可能的高。 + +换言之,在可以解决问题的前提下,算法效率则是主要评价维度,包括: + +- **时间效率**,即算法的运行速度的快慢。 +- **空间效率**,即算法占用的内存空间大小。 + +数据结构与算法追求“运行速度快、占用内存少”,而如何去评价算法效率则是非常重要的问题,因为只有知道如何评价算法,才能去做算法之间的对比分析,以及优化算法设计。 + +## 2.1.2. 效率评估方法 + +### 实际测试 + +假设我们现在有算法 A 和 算法 B ,都能够解决同一问题,现在需要对比两个算法之间的效率。我们能够想到的最直接的方式,就是找一台计算机,把两个算法都完整跑一遍,并监控记录运行时间和内存占用情况。这种评估方式能够反映真实情况,但是也存在很大的硬伤。 + +**难以排除测试环境的干扰因素**。硬件配置会影响到算法的性能表现。例如,在某台计算机中,算法 A 比算法 B 运行时间更短;但换到另一台配置不同的计算机中,可能会得到相反的测试结果。这意味着我们需要在各种机器上展开测试,而这是不现实的。 + +**展开完整测试非常耗费资源**。随着输入数据量的大小变化,算法会呈现出不同的效率表现。比如,有可能输入数据量较小时,算法 A 运行时间短于算法 B ,而在输入数据量较大时,测试结果截然相反。因此,若想要达到具有说服力的对比结果,那么需要输入各种体量数据,这样的测试需要占用大量计算资源。 + +### 理论估算 + +既然实际测试具有很大的局限性,那么我们是否可以仅通过一些计算,就获知算法的效率水平呢?答案是肯定的,我们将此估算方法称为「复杂度分析 Complexity Analysis」或「渐近复杂度分析 Asymptotic Complexity Analysis」。 + +**复杂度分析评估随着输入数据量的增长,算法的运行时间和占用空间的增长趋势**。根据时间和空间两方面,复杂度可分为「时间复杂度 Time Complexity」和「空间复杂度 Space Complexity」。 + +**复杂度分析克服了实际测试方法的弊端**。一是独立于测试环境,分析结果适用于所有运行平台。二是可以体现不同数据量下的算法效率,尤其是可以反映大数据量下的算法性能。 + +## 2.1.3. 复杂度分析重要性 + +复杂度分析给出一把评价算法效率的“标尺”,告诉我们执行某个算法需要多少时间和空间资源,也让我们可以开展不同算法之间的效率对比。 + +计算复杂度是个数学概念,对于初学者可能比较抽象,学习难度相对较高。从这个角度出发,其并不适合作为第一章内容。但是,当我们讨论某个数据结构或者算法的特点时,难以避免需要分析它的运行速度和空间使用情况。**因此,在展开学习数据结构与算法之前,建议读者先对计算复杂度建立起初步的了解,并且能够完成简单案例的复杂度分析**。 diff --git a/build/chapter_computational_complexity/space_complexity.md b/build/chapter_computational_complexity/space_complexity.md new file mode 100755 index 000000000..61a217f6e --- /dev/null +++ b/build/chapter_computational_complexity/space_complexity.md @@ -0,0 +1,1512 @@ +--- +comments: true +--- + +# 2.3. 空间复杂度 + +「空间复杂度 Space Complexity」统计 **算法使用内存空间随着数据量变大时的增长趋势**。这个概念与时间复杂度很类似。 + +## 2.3.1. 算法相关空间 + +算法运行中,使用的内存空间主要有以下几种: + +- 「输入空间」用于存储算法的输入数据; +- 「暂存空间」用于存储算法运行中的变量、对象、函数上下文等数据; +- 「输出空间」用于存储算法的输出数据; + +!!! tip + + 通常情况下,空间复杂度统计范围是「暂存空间」+「输出空间」。 + +暂存空间可分为三个部分: + +- 「暂存数据」用于保存算法运行中的各种 **常量、变量、对象** 等。 +- 「栈帧空间」用于保存调用函数的上下文数据。系统每次调用函数都会在栈的顶部创建一个栈帧,函数返回时,栈帧空间会被释放。 +- 「指令空间」用于保存编译后的程序指令,**在实际统计中一般忽略不计**。 + +![space_types](space_complexity.assets/space_types.png) + +

Fig. 算法使用的相关空间

+ +=== "Java" + + ```java title="" + /* 类 */ + class Node { + int val; + Node next; + Node(int x) { val = x; } + } + + /* 函数 */ + int function() { + // do something... + return 0; + } + + int algorithm(int n) { // 输入数据 + final int a = 0; // 暂存数据(常量) + int b = 0; // 暂存数据(变量) + Node node = new Node(0); // 暂存数据(对象) + int c = function(); // 栈帧空间(调用函数) + return a + b + c; // 输出数据 + } + ``` + +=== "C++" + + ```cpp title="" + /* 结构体 */ + struct Node { + int val; + Node *next; + Node(int x) : val(x), next(nullptr) {} + }; + + /* 函数 */ + int func() { + // do something... + return 0; + } + + int algorithm(int n) { // 输入数据 + const int a = 0; // 暂存数据(常量) + int b = 0; // 暂存数据(变量) + Node* node = new Node(0); // 暂存数据(对象) + int c = func(); // 栈帧空间(调用函数) + return a + b + c; // 输出数据 + } + ``` + +=== "Python" + + ```python title="" + """ 类 """ + class Node: + def __init__(self, x): + self.val = x # 结点值 + self.next = None # 指向下一结点的指针(引用) + + """ 函数 """ + def function(): + # do something... + return 0 + + def algorithm(n): # 输入数据 + b = 0 # 暂存数据(变量) + node = Node(0) # 暂存数据(对象) + c = function() # 栈帧空间(调用函数) + return a + b + c # 输出数据 + ``` + +=== "Go" + + ```go title="" + /* 结构体 */ + type node struct { + val int + next *node + } + + /* 创建 node 结构体 */ + func newNode(val int) *node { + return &node{val: val} + } + + /* 函数 */ + func function() int { + // do something... + return 0 + } + + func algorithm(n int) int { // 输入数据 + const a = 0 // 暂存数据(常量) + b := 0 // 暂存数据(变量) + newNode(0) // 暂存数据(对象) + c := function() // 栈帧空间(调用函数) + return a + b + c // 输出数据 + } + ``` + +=== "JavaScript" + + ```js title="" + /* 类 */ + class Node { + val; + next; + constructor(val) { + this.val = val === undefined ? 0 : val; // 结点值 + this.next = null; // 指向下一结点的引用 + } + } + + /* 函数 */ + function constFunc() { + // do something + return 0; + } + + function algorithm(n) { // 输入数据 + const a = 0; // 暂存数据(常量) + const b = 0; // 暂存数据(变量) + const node = new Node(0); // 暂存数据(对象) + const c = constFunc(); // 栈帧空间(调用函数) + return a + b + c; // 输出数据 + } + ``` + +=== "TypeScript" + + ```typescript title="" + /* 类 */ + class Node { + val: number; + next: Node | null; + constructor(val?: number) { + this.val = val === undefined ? 0 : val; // 结点值 + this.next = null; // 指向下一结点的引用 + } + } + + /* 函数 */ + function constFunc(): number { + // do something + return 0; + } + + function algorithm(n: number): number { // 输入数据 + const a = 0; // 暂存数据(常量) + const b = 0; // 暂存数据(变量) + const node = new Node(0); // 暂存数据(对象) + const c = constFunc(); // 栈帧空间(调用函数) + return a + b + c; // 输出数据 + } + ``` + +=== "C" + + ```c title="" + + ``` + +=== "C#" + + ```csharp title="" + /* 类 */ + class Node + { + int val; + Node next; + Node(int x) { val = x; } + } + + /* 函数 */ + int function() + { + // do something... + return 0; + } + + int algorithm(int n) // 输入数据 + { + int a = 0; // 暂存数据(常量) + int b = 0; // 暂存数据(变量) + Node node = new Node(0); // 暂存数据(对象) + int c = function(); // 栈帧空间(调用函数) + return a + b + c; // 输出数据 + } + ``` + +=== "Swift" + + ```swift title="" + /* 类 */ + class Node { + var val: Int + var next: Node? + + init(x: Int) { + val = x + } + } + + /* 函数 */ + func function() -> Int { + // do something... + return 0 + } + + func algorithm(n: Int) -> Int { // 输入数据 + let a = 0 // 暂存数据(常量) + var b = 0 // 暂存数据(变量) + let node = Node(x: 0) // 暂存数据(对象) + let c = function() // 栈帧空间(调用函数) + return a + b + c // 输出数据 + } + ``` + +=== "Zig" + + ```zig title="" + + ``` + +## 2.3.2. 推算方法 + +空间复杂度的推算方法和时间复杂度总体类似,只是从统计“计算操作数量”变为统计“使用空间大小”。与时间复杂度不同的是,**我们一般只关注「最差空间复杂度」**。这是因为内存空间是一个硬性要求,我们必须保证在所有输入数据下都有足够的内存空间预留。 + +**最差空间复杂度中的“最差”有两层含义**,分别为输入数据的最差分布、算法运行中的最差时间点。 + +- **以最差输入数据为准**。当 $n < 10$ 时,空间复杂度为 $O(1)$ ;但是当 $n > 10$ 时,初始化的数组 `nums` 使用 $O(n)$ 空间;因此最差空间复杂度为 $O(n)$ ; +- **以算法运行过程中的峰值内存为准**。程序在执行最后一行之前,使用 $O(1)$ 空间;当初始化数组 `nums` 时,程序使用 $O(n)$ 空间;因此最差空间复杂度为 $O(n)$ ; + +=== "Java" + + ```java title="" + void algorithm(int n) { + int a = 0; // O(1) + int[] b = new int[10000]; // O(1) + if (n > 10) + int[] nums = new int[n]; // O(n) + } + ``` + +=== "C++" + + ```cpp title="" + void algorithm(int n) { + int a = 0; // O(1) + vector b(10000); // O(1) + if (n > 10) + vector nums(n); // O(n) + } + ``` + +=== "Python" + + ```python title="" + def algorithm(n): + a = 0 # O(1) + b = [0] * 10000 # O(1) + if n > 10: + nums = [0] * n # O(n) + ``` + +=== "Go" + + ```go title="" + func algorithm(n int) { + a := 0 // O(1) + b := make([]int, 10000) // O(1) + var nums []int + if n > 10 { + nums := make([]int, n) // O(n) + } + fmt.Println(a, b, nums) + } + ``` + +=== "JavaScript" + + ```js title="" + function algorithm(n) { + const a = 0; // O(1) + const b = new Array(10000); // O(1) + if (n > 10) { + const nums = new Array(n); // O(n) + } + } + ``` + +=== "TypeScript" + + ```typescript title="" + function algorithm(n: number): void { + const a = 0; // O(1) + const b = new Array(10000); // O(1) + if (n > 10) { + const nums = new Array(n); // O(n) + } + } + ``` + +=== "C" + + ```c title="" + + ``` + +=== "C#" + + ```csharp title="" + void algorithm(int n) + { + int a = 0; // O(1) + int[] b = new int[10000]; // O(1) + if (n > 10) + { + int[] nums = new int[n]; // O(n) + } + } + ``` + +=== "Swift" + + ```swift title="" + func algorithm(n: Int) { + let a = 0 // O(1) + let b = Array(repeating: 0, count: 10000) // O(1) + if n > 10 { + let nums = Array(repeating: 0, count: n) // O(n) + } + } + ``` + +=== "Zig" + + ```zig title="" + + ``` + +**在递归函数中,需要注意统计栈帧空间**。例如函数 `loop()`,在循环中调用了 $n$ 次 `function()` ,每轮中的 `function()` 都返回并释放了栈帧空间,因此空间复杂度仍为 $O(1)$ 。而递归函数 `recur()` 在运行中会同时存在 $n$ 个未返回的 `recur()` ,从而使用 $O(n)$ 的栈帧空间。 + +=== "Java" + + ```java title="" + int function() { + // do something + return 0; + } + /* 循环 O(1) */ + void loop(int n) { + for (int i = 0; i < n; i++) { + function(); + } + } + /* 递归 O(n) */ + void recur(int n) { + if (n == 1) return; + return recur(n - 1); + } + ``` + +=== "C++" + + ```cpp title="" + int func() { + // do something + return 0; + } + /* 循环 O(1) */ + void loop(int n) { + for (int i = 0; i < n; i++) { + func(); + } + } + /* 递归 O(n) */ + void recur(int n) { + if (n == 1) return; + return recur(n - 1); + } + ``` + +=== "Python" + + ```python title="" + def function(): + # do something + return 0 + + """ 循环 O(1) """ + def loop(n): + for _ in range(n): + function() + + """ 递归 O(n) """ + def recur(n): + if n == 1: return + return recur(n - 1) + ``` + +=== "Go" + + ```go title="" + func function() int { + // do something + return 0 + } + + /* 循环 O(1) */ + func loop(n int) { + for i := 0; i < n; i++ { + function() + } + } + + /* 递归 O(n) */ + func recur(n int) { + if n == 1 { + return + } + recur(n - 1) + } + ``` + +=== "JavaScript" + + ```js title="" + function constFunc() { + // do something + return 0; + } + /* 循环 O(1) */ + function loop(n) { + for (let i = 0; i < n; i++) { + constFunc(); + } + } + /* 递归 O(n) */ + function recur(n) { + if (n === 1) return; + return recur(n - 1); + } + ``` + +=== "TypeScript" + + ```typescript title="" + function constFunc(): number { + // do something + return 0; + } + /* 循环 O(1) */ + function loop(n: number): void { + for (let i = 0; i < n; i++) { + constFunc(); + } + } + /* 递归 O(n) */ + function recur(n: number): void { + if (n === 1) return; + return recur(n - 1); + } + ``` + +=== "C" + + ```c title="" + + ``` + +=== "C#" + + ```csharp title="" + int function() + { + // do something + return 0; + } + /* 循环 O(1) */ + void loop(int n) + { + for (int i = 0; i < n; i++) + { + function(); + } + } + /* 递归 O(n) */ + int recur(int n) + { + if (n == 1) return 1; + return recur(n - 1); + } + ``` + +=== "Swift" + + ```swift title="" + @discardableResult + func function() -> Int { + // do something + return 0 + } + + /* 循环 O(1) */ + func loop(n: Int) { + for _ in 0 ..< n { + function() + } + } + + /* 递归 O(n) */ + func recur(n: Int) { + if n == 1 { + return + } + recur(n: n - 1) + } + ``` + +=== "Zig" + + ```zig title="" + + ``` + +## 2.3.3. 常见类型 + +设输入数据大小为 $n$ ,常见的空间复杂度类型有(从低到高排列) + +$$ +\begin{aligned} +O(1) < O(\log n) < O(n) < O(n^2) < O(2^n) \newline +\text{常数阶} < \text{对数阶} < \text{线性阶} < \text{平方阶} < \text{指数阶} +\end{aligned} +$$ + +![space_complexity_common_types](space_complexity.assets/space_complexity_common_types.png) + +

Fig. 空间复杂度的常见类型

+ +!!! tip + + 部分示例代码需要一些前置知识,包括数组、链表、二叉树、递归算法等。如果遇到看不懂的地方无需担心,可以在学习完后面章节后再来复习,现阶段先聚焦在理解空间复杂度含义和推算方法上。 + +### 常数阶 $O(1)$ + +常数阶常见于数量与输入数据大小 $n$ 无关的常量、变量、对象。 + +需要注意的是,在循环中初始化变量或调用函数而占用的内存,在进入下一循环后就会被释放,即不会累积占用空间,空间复杂度仍为 $O(1)$ 。 + +=== "Java" + + ```java title="space_complexity.java" + /* 常数阶 */ + void constant(int n) { + // 常量、变量、对象占用 O(1) 空间 + final int a = 0; + int b = 0; + int[] nums = new int[10000]; + ListNode node = new ListNode(0); + // 循环中的变量占用 O(1) 空间 + for (int i = 0; i < n; i++) { + int c = 0; + } + // 循环中的函数占用 O(1) 空间 + for (int i = 0; i < n; i++) { + function(); + } + } + ``` + +=== "C++" + + ```cpp title="space_complexity.cpp" + /* 常数阶 */ + void constant(int n) { + // 常量、变量、对象占用 O(1) 空间 + const int a = 0; + int b = 0; + vector nums(10000); + ListNode node(0); + // 循环中的变量占用 O(1) 空间 + for (int i = 0; i < n; i++) { + int c = 0; + } + // 循环中的函数占用 O(1) 空间 + for (int i = 0; i < n; i++) { + func(); + } + } + ``` + +=== "Python" + + ```python title="space_complexity.py" + """ 常数阶 """ + def constant(n): + # 常量、变量、对象占用 O(1) 空间 + a = 0 + nums = [0] * 10000 + node = ListNode(0) + # 循环中的变量占用 O(1) 空间 + for _ in range(n): + c = 0 + # 循环中的函数占用 O(1) 空间 + for _ in range(n): + function() + ``` + +=== "Go" + + ```go title="space_complexity.go" + /* 常数阶 */ + func spaceConstant(n int) { + // 常量、变量、对象占用 O(1) 空间 + const a = 0 + b := 0 + nums := make([]int, 10000) + ListNode := newNode(0) + // 循环中的变量占用 O(1) 空间 + var c int + for i := 0; i < n; i++ { + c = 0 + } + // 循环中的函数占用 O(1) 空间 + for i := 0; i < n; i++ { + function() + } + fmt.Println(a, b, nums, c, ListNode) + } + ``` + +=== "JavaScript" + + ```js title="space_complexity.js" + /* 常数阶 */ + function constant(n) { + // 常量、变量、对象占用 O(1) 空间 + const a = 0; + const b = 0; + const nums = new Array(10000); + const node = new ListNode(0); + // 循环中的变量占用 O(1) 空间 + for (let i = 0; i < n; i++) { + const c = 0; + } + // 循环中的函数占用 O(1) 空间 + for (let i = 0; i < n; i++) { + constFunc(); + } + } + ``` + +=== "TypeScript" + + ```typescript title="space_complexity.ts" + /* 常数阶 */ + function constant(n: number): void { + // 常量、变量、对象占用 O(1) 空间 + const a = 0; + const b = 0; + const nums = new Array(10000); + const node = new ListNode(0); + // 循环中的变量占用 O(1) 空间 + for (let i = 0; i < n; i++) { + const c = 0; + } + // 循环中的函数占用 O(1) 空间 + for (let i = 0; i < n; i++) { + constFunc(); + } + } + ``` + +=== "C" + + ```c title="space_complexity.c" + + ``` + +=== "C#" + + ```csharp title="space_complexity.cs" + /* 常数阶 */ + void constant(int n) + { + // 常量、变量、对象占用 O(1) 空间 + int a = 0; + int b = 0; + int[] nums = new int[10000]; + ListNode node = new ListNode(0); + // 循环中的变量占用 O(1) 空间 + for (int i = 0; i < n; i++) + { + int c = 0; + } + // 循环中的函数占用 O(1) 空间 + for (int i = 0; i < n; i++) + { + function(); + } + } + ``` + +=== "Swift" + + ```swift title="space_complexity.swift" + /* 常数阶 */ + func constant(n: Int) { + // 常量、变量、对象占用 O(1) 空间 + let a = 0 + var b = 0 + let nums = Array(repeating: 0, count: 10000) + let node = ListNode(x: 0) + // 循环中的变量占用 O(1) 空间 + for _ in 0 ..< n { + let c = 0 + } + // 循环中的函数占用 O(1) 空间 + for _ in 0 ..< n { + function() + } + } + ``` + +=== "Zig" + + ```zig title="space_complexity.zig" + // 常数阶 + fn constant(n: i32) void { + // 常量、变量、对象占用 O(1) 空间 + const a: i32 = 0; + var b: i32 = 0; + var nums = [_]i32{0}**10000; + var node = inc.ListNode(i32){.val = 0}; + var i: i32 = 0; + // 循环中的变量占用 O(1) 空间 + while (i < n) : (i += 1) { + var c: i32 = 0; + _ = c; + } + // 循环中的函数占用 O(1) 空间 + i = 0; + while (i < n) : (i += 1) { + _ = function(); + } + _ = a; + _ = b; + _ = nums; + _ = node; + } + ``` + +### 线性阶 $O(n)$ + +线性阶常见于元素数量与 $n$ 成正比的数组、链表、栈、队列等。 + +=== "Java" + + ```java title="space_complexity.java" + /* 线性阶 */ + void linear(int n) { + // 长度为 n 的数组占用 O(n) 空间 + int[] nums = new int[n]; + // 长度为 n 的列表占用 O(n) 空间 + List nodes = new ArrayList<>(); + for (int i = 0; i < n; i++) { + nodes.add(new ListNode(i)); + } + // 长度为 n 的哈希表占用 O(n) 空间 + Map map = new HashMap<>(); + for (int i = 0; i < n; i++) { + map.put(i, String.valueOf(i)); + } + } + ``` + +=== "C++" + + ```cpp title="space_complexity.cpp" + /* 线性阶 */ + void linear(int n) { + // 长度为 n 的数组占用 O(n) 空间 + vector nums(n); + // 长度为 n 的列表占用 O(n) 空间 + vector nodes; + for (int i = 0; i < n; i++) { + nodes.push_back(ListNode(i)); + } + // 长度为 n 的哈希表占用 O(n) 空间 + unordered_map map; + for (int i = 0; i < n; i++) { + map[i] = to_string(i); + } + } + ``` + +=== "Python" + + ```python title="space_complexity.py" + """ 线性阶 """ + def linear(n): + # 长度为 n 的列表占用 O(n) 空间 + nums = [0] * n + # 长度为 n 的哈希表占用 O(n) 空间 + mapp = {} + for i in range(n): + mapp[i] = str(i) + ``` + +=== "Go" + + ```go title="space_complexity.go" + /* 线性阶 */ + func spaceLinear(n int) { + // 长度为 n 的数组占用 O(n) 空间 + _ = make([]int, n) + // 长度为 n 的列表占用 O(n) 空间 + var nodes []*node + for i := 0; i < n; i++ { + nodes = append(nodes, newNode(i)) + } + // 长度为 n 的哈希表占用 O(n) 空间 + m := make(map[int]string, n) + for i := 0; i < n; i++ { + m[i] = strconv.Itoa(i) + } + } + ``` + +=== "JavaScript" + + ```js title="space_complexity.js" + /* 线性阶 */ + function linear(n) { + // 长度为 n 的数组占用 O(n) 空间 + const nums = new Array(n); + // 长度为 n 的列表占用 O(n) 空间 + const nodes = []; + for (let i = 0; i < n; i++) { + nodes.push(new ListNode(i)); + } + // 长度为 n 的哈希表占用 O(n) 空间 + const map = new Map(); + for (let i = 0; i < n; i++) { + map.set(i, i.toString()); + } + } + ``` + +=== "TypeScript" + + ```typescript title="space_complexity.ts" + /* 线性阶 */ + function linear(n: number): void { + // 长度为 n 的数组占用 O(n) 空间 + const nums = new Array(n); + // 长度为 n 的列表占用 O(n) 空间 + const nodes: ListNode[] = []; + for (let i = 0; i < n; i++) { + nodes.push(new ListNode(i)); + } + // 长度为 n 的哈希表占用 O(n) 空间 + const map = new Map(); + for (let i = 0; i < n; i++) { + map.set(i, i.toString()); + } + } + ``` + +=== "C" + + ```c title="space_complexity.c" + + ``` + +=== "C#" + + ```csharp title="space_complexity.cs" + /* 线性阶 */ + void linear(int n) + { + // 长度为 n 的数组占用 O(n) 空间 + int[] nums = new int[n]; + // 长度为 n 的列表占用 O(n) 空间 + List nodes = new(); + for (int i = 0; i < n; i++) + { + nodes.Add(new ListNode(i)); + } + // 长度为 n 的哈希表占用 O(n) 空间 + Dictionary map = new(); + for (int i = 0; i < n; i++) + { + map.Add(i, i.ToString()); + } + } + ``` + +=== "Swift" + + ```swift title="space_complexity.swift" + /* 线性阶 */ + func linear(n: Int) { + // 长度为 n 的数组占用 O(n) 空间 + let nums = Array(repeating: 0, count: n) + // 长度为 n 的列表占用 O(n) 空间 + let nodes = (0 ..< n).map { ListNode(x: $0) } + // 长度为 n 的哈希表占用 O(n) 空间 + let map = Dictionary(uniqueKeysWithValues: (0 ..< n).map { ($0, "\($0)") }) + } + ``` + +=== "Zig" + + ```zig title="space_complexity.zig" + // 线性阶 + fn linear(comptime n: i32) !void { + // 长度为 n 的数组占用 O(n) 空间 + var nums = [_]i32{0}**n; + // 长度为 n 的列表占用 O(n) 空间 + var nodes = std.ArrayList(i32).init(std.heap.page_allocator); + defer nodes.deinit(); + var i: i32 = 0; + while (i < n) : (i += 1) { + try nodes.append(i); + } + // 长度为 n 的哈希表占用 O(n) 空间 + var map = std.AutoArrayHashMap(i32, []const u8).init(std.heap.page_allocator); + defer map.deinit(); + var j: i32 = 0; + while (j < n) : (j += 1) { + const string = try std.fmt.allocPrint(std.heap.page_allocator, "{d}", .{j}); + defer std.heap.page_allocator.free(string); + try map.put(i, string); + } + _ = nums; + } + ``` + +以下递归函数会同时存在 $n$ 个未返回的 `algorithm()` 函数,使用 $O(n)$ 大小的栈帧空间。 + +=== "Java" + + ```java title="space_complexity.java" + /* 线性阶(递归实现) */ + void linearRecur(int n) { + System.out.println("递归 n = " + n); + if (n == 1) return; + linearRecur(n - 1); + } + ``` + +=== "C++" + + ```cpp title="space_complexity.cpp" + /* 线性阶(递归实现) */ + void linearRecur(int n) { + cout << "递归 n = " << n << endl; + if (n == 1) return; + linearRecur(n - 1); + } + ``` + +=== "Python" + + ```python title="space_complexity.py" + """ 线性阶(递归实现) """ + def linear_recur(n): + print("递归 n =", n) + if n == 1: return + linear_recur(n - 1) + ``` + +=== "Go" + + ```go title="space_complexity.go" + /* 线性阶(递归实现) */ + func spaceLinearRecur(n int) { + fmt.Println("递归 n =", n) + if n == 1 { + return + } + spaceLinearRecur(n - 1) + } + ``` + +=== "JavaScript" + + ```js title="space_complexity.js" + /* 线性阶(递归实现) */ + function linearRecur(n) { + console.log(`递归 n = ${n}`); + if (n === 1) return; + linearRecur(n - 1); + } + ``` + +=== "TypeScript" + + ```typescript title="space_complexity.ts" + /* 线性阶(递归实现) */ + function linearRecur(n: number): void { + console.log(`递归 n = ${n}`); + if (n === 1) return; + linearRecur(n - 1); + } + ``` + +=== "C" + + ```c title="space_complexity.c" + + ``` + +=== "C#" + + ```csharp title="space_complexity.cs" + /* 线性阶(递归实现) */ + void linearRecur(int n) + { + Console.WriteLine("递归 n = " + n); + if (n == 1) return; + linearRecur(n - 1); + } + ``` + +=== "Swift" + + ```swift title="space_complexity.swift" + /* 线性阶(递归实现) */ + func linearRecur(n: Int) { + print("递归 n = \(n)") + if n == 1 { + return + } + linearRecur(n: n - 1) + } + ``` + +=== "Zig" + + ```zig title="space_complexity.zig" + // 线性阶(递归实现) + fn linearRecur(comptime n: i32) void { + std.debug.print("递归 n = {}\n", .{n}); + if (n == 1) return; + linearRecur(n - 1); + } + ``` + +![space_complexity_recursive_linear](space_complexity.assets/space_complexity_recursive_linear.png) + +

Fig. 递归函数产生的线性阶空间复杂度

+ +### 平方阶 $O(n^2)$ + +平方阶常见于元素数量与 $n$ 成平方关系的矩阵、图。 + +=== "Java" + + ```java title="space_complexity.java" + /* 平方阶 */ + void quadratic(int n) { + // 矩阵占用 O(n^2) 空间 + int [][]numMatrix = new int[n][n]; + // 二维列表占用 O(n^2) 空间 + List> numList = new ArrayList<>(); + for (int i = 0; i < n; i++) { + List tmp = new ArrayList<>(); + for (int j = 0; j < n; j++) { + tmp.add(0); + } + numList.add(tmp); + } + } + ``` + +=== "C++" + + ```cpp title="space_complexity.cpp" + /* 平方阶 */ + void quadratic(int n) { + // 二维列表占用 O(n^2) 空间 + vector> numMatrix; + for (int i = 0; i < n; i++) { + vector tmp; + for (int j = 0; j < n; j++) { + tmp.push_back(0); + } + numMatrix.push_back(tmp); + } + } + ``` + +=== "Python" + + ```python title="space_complexity.py" + """ 平方阶 """ + def quadratic(n): + # 二维列表占用 O(n^2) 空间 + num_matrix = [[0] * n for _ in range(n)] + ``` + +=== "Go" + + ```go title="space_complexity.go" + /* 平方阶 */ + func spaceQuadratic(n int) { + // 矩阵占用 O(n^2) 空间 + numMatrix := make([][]int, n) + for i := 0; i < n; i++ { + numMatrix[i] = make([]int, n) + } + } + ``` + +=== "JavaScript" + + ```js title="space_complexity.js" + /* 平方阶 */ + function quadratic(n) { + // 矩阵占用 O(n^2) 空间 + const numMatrix = Array(n).fill(null).map(() => Array(n).fill(null)); + // 二维列表占用 O(n^2) 空间 + const numList = []; + for (let i = 0; i < n; i++) { + const tmp = []; + for (let j = 0; j < n; j++) { + tmp.push(0); + } + numList.push(tmp); + } + } + ``` + +=== "TypeScript" + + ```typescript title="space_complexity.ts" + /* 平方阶 */ + function quadratic(n: number): void { + // 矩阵占用 O(n^2) 空间 + const numMatrix = Array(n).fill(null).map(() => Array(n).fill(null)); + // 二维列表占用 O(n^2) 空间 + const numList = []; + for (let i = 0; i < n; i++) { + const tmp = []; + for (let j = 0; j < n; j++) { + tmp.push(0); + } + numList.push(tmp); + } + } + ``` + +=== "C" + + ```c title="space_complexity.c" + + ``` + +=== "C#" + + ```csharp title="space_complexity.cs" + /* 平方阶 */ + void quadratic(int n) + { + // 矩阵占用 O(n^2) 空间 + int[,] numMatrix = new int[n, n]; + // 二维列表占用 O(n^2) 空间 + List> numList = new(); + for (int i = 0; i < n; i++) + { + List tmp = new(); + for (int j = 0; j < n; j++) + { + tmp.Add(0); + } + numList.Add(tmp); + } + } + ``` + +=== "Swift" + + ```swift title="space_complexity.swift" + /* 平方阶 */ + func quadratic(n: Int) { + // 二维列表占用 O(n^2) 空间 + let numList = Array(repeating: Array(repeating: 0, count: n), count: n) + } + ``` + +=== "Zig" + + ```zig title="space_complexity.zig" + // 平方阶 + fn quadratic(n: i32) !void { + // 二维列表占用 O(n^2) 空间 + var nodes = std.ArrayList(std.ArrayList(i32)).init(std.heap.page_allocator); + defer nodes.deinit(); + var i: i32 = 0; + while (i < n) : (i += 1) { + var tmp = std.ArrayList(i32).init(std.heap.page_allocator); + defer tmp.deinit(); + var j: i32 = 0; + while (j < n) : (j += 1) { + try tmp.append(0); + } + try nodes.append(tmp); + } + } + ``` + +在以下递归函数中,同时存在 $n$ 个未返回的 `algorithm()` ,并且每个函数中都初始化了一个数组,长度分别为 $n, n-1, n-2, ..., 2, 1$ ,平均长度为 $\frac{n}{2}$ ,因此总体使用 $O(n^2)$ 空间。 + +=== "Java" + + ```java title="space_complexity.java" + /* 平方阶(递归实现) */ + int quadraticRecur(int n) { + if (n <= 0) return 0; + // 数组 nums 长度为 n, n-1, ..., 2, 1 + int[] nums = new int[n]; + return quadraticRecur(n - 1); + } + ``` + +=== "C++" + + ```cpp title="space_complexity.cpp" + /* 平方阶(递归实现) */ + int quadraticRecur(int n) { + if (n <= 0) return 0; + vector nums(n); + cout << "递归 n = " << n << " 中的 nums 长度 = " << nums.size() << endl; + return quadraticRecur(n - 1); + } + ``` + +=== "Python" + + ```python title="space_complexity.py" + """ 平方阶(递归实现) """ + def quadratic_recur(n): + if n <= 0: return 0 + # 数组 nums 长度为 n, n-1, ..., 2, 1 + nums = [0] * n + return quadratic_recur(n - 1) + ``` + +=== "Go" + + ```go title="space_complexity.go" + /* 平方阶(递归实现) */ + func spaceQuadraticRecur(n int) int { + if n <= 0 { + return 0 + } + // 数组 nums 长度为 n, n-1, ..., 2, 1 + nums := make([]int, n) + return spaceQuadraticRecur(n - 1) + } + ``` + +=== "JavaScript" + + ```js title="space_complexity.js" + /* 平方阶(递归实现) */ + function quadraticRecur(n) { + if (n <= 0) return 0; + const nums = new Array(n); + console.log(`递归 n = ${n} 中的 nums 长度 = ${nums.length}`); + return quadraticRecur(n - 1); + } + ``` + +=== "TypeScript" + + ```typescript title="space_complexity.ts" + /* 平方阶(递归实现) */ + function quadraticRecur(n: number): number { + if (n <= 0) return 0; + const nums = new Array(n); + console.log(`递归 n = ${n} 中的 nums 长度 = ${nums.length}`); + return quadraticRecur(n - 1); + } + ``` + +=== "C" + + ```c title="space_complexity.c" + + ``` + +=== "C#" + + ```csharp title="space_complexity.cs" + /* 平方阶(递归实现) */ + int quadraticRecur(int n) + { + if (n <= 0) return 0; + // 数组 nums 长度为 n, n-1, ..., 2, 1 + int[] nums = new int[n]; + return quadraticRecur(n - 1); + } + ``` + +=== "Swift" + + ```swift title="space_complexity.swift" + /* 平方阶(递归实现) */ + func quadraticRecur(n: Int) -> Int { + if n <= 0 { + return 0 + } + // 数组 nums 长度为 n, n-1, ..., 2, 1 + let nums = Array(repeating: 0, count: n) + return quadraticRecur(n: n - 1) + } + ``` + +=== "Zig" + + ```zig title="space_complexity.zig" + // 平方阶(递归实现) + fn quadraticRecur(comptime n: i32) i32 { + if (n <= 0) return 0; + var nums = [_]i32{0}**n; + std.debug.print("递归 n = {} 中的 nums 长度 = {}\n", .{n, nums.len}); + return quadraticRecur(n - 1); + } + ``` + +![space_complexity_recursive_quadratic](space_complexity.assets/space_complexity_recursive_quadratic.png) + +

Fig. 递归函数产生的平方阶空间复杂度

+ +### 指数阶 $O(2^n)$ + +指数阶常见于二叉树。高度为 $n$ 的「满二叉树」的结点数量为 $2^n - 1$ ,使用 $O(2^n)$ 空间。 + +=== "Java" + + ```java title="space_complexity.java" + /* 指数阶(建立满二叉树) */ + TreeNode buildTree(int n) { + if (n == 0) return null; + TreeNode root = new TreeNode(0); + root.left = buildTree(n - 1); + root.right = buildTree(n - 1); + return root; + } + ``` + +=== "C++" + + ```cpp title="space_complexity.cpp" + /* 指数阶(建立满二叉树) */ + TreeNode* buildTree(int n) { + if (n == 0) return nullptr; + TreeNode* root = new TreeNode(0); + root->left = buildTree(n - 1); + root->right = buildTree(n - 1); + return root; + } + ``` + +=== "Python" + + ```python title="space_complexity.py" + """ 指数阶(建立满二叉树) """ + def build_tree(n): + if n == 0: return None + root = TreeNode(0) + root.left = build_tree(n - 1) + root.right = build_tree(n - 1) + return root + ``` + +=== "Go" + + ```go title="space_complexity.go" + /* 指数阶(建立满二叉树) */ + func buildTree(n int) *treeNode { + if n == 0 { + return nil + } + root := newTreeNode(0) + root.left = buildTree(n - 1) + root.right = buildTree(n - 1) + return root + } + ``` + +=== "JavaScript" + + ```js title="space_complexity.js" + /* 指数阶(建立满二叉树) */ + function buildTree(n) { + if (n === 0) return null; + const root = new TreeNode(0); + root.left = buildTree(n - 1); + root.right = buildTree(n - 1); + return root; + } + ``` + +=== "TypeScript" + + ```typescript title="space_complexity.ts" + /* 指数阶(建立满二叉树) */ + function buildTree(n: number): TreeNode | null { + if (n === 0) return null; + const root = new TreeNode(0); + root.left = buildTree(n - 1); + root.right = buildTree(n - 1); + return root; + } + ``` + +=== "C" + + ```c title="space_complexity.c" + + ``` + +=== "C#" + + ```csharp title="space_complexity.cs" + /* 指数阶(建立满二叉树) */ + TreeNode? buildTree(int n) + { + if (n == 0) return null; + TreeNode root = new TreeNode(0); + root.left = buildTree(n - 1); + root.right = buildTree(n - 1); + return root; + } + ``` + +=== "Swift" + + ```swift title="space_complexity.swift" + /* 指数阶(建立满二叉树) */ + func buildTree(n: Int) -> TreeNode? { + if n == 0 { + return nil + } + let root = TreeNode(x: 0) + root.left = buildTree(n: n - 1) + root.right = buildTree(n: n - 1) + return root + } + ``` + +=== "Zig" + + ```zig title="space_complexity.zig" + // 指数阶(建立满二叉树) + fn buildTree(mem_allocator: std.mem.Allocator, n: i32) !?*inc.TreeNode(i32) { + if (n == 0) return null; + const root = try mem_allocator.create(inc.TreeNode(i32)); + root.init(0); + root.left = try buildTree(mem_allocator, n - 1); + root.right = try buildTree(mem_allocator, n - 1); + return root; + } + ``` + +![space_complexity_exponential](space_complexity.assets/space_complexity_exponential.png) + +

Fig. 满二叉树下的指数阶空间复杂度

+ +### 对数阶 $O(\log n)$ + +对数阶常见于分治算法、数据类型转换等。 + +例如「归并排序」,长度为 $n$ 的数组可以形成高度为 $\log n$ 的递归树,因此空间复杂度为 $O(\log n)$ 。 + +再例如「数字转化为字符串」,输入任意正整数 $n$ ,它的位数为 $\log_{10} n$ ,即对应字符串长度为 $\log_{10} n$ ,因此空间复杂度为 $O(\log_{10} n) = O(\log n)$ 。 diff --git a/build/chapter_computational_complexity/space_time_tradeoff.md b/build/chapter_computational_complexity/space_time_tradeoff.md new file mode 100755 index 000000000..07eea2e1a --- /dev/null +++ b/build/chapter_computational_complexity/space_time_tradeoff.md @@ -0,0 +1,386 @@ +--- +comments: true +--- + +# 2.4. 权衡时间与空间 + +理想情况下,我们希望算法的时间复杂度和空间复杂度都能够达到最优,而实际上,同时优化时间复杂度和空间复杂度是非常困难的。 + +**降低时间复杂度,往往是以提升空间复杂度为代价的,反之亦然**。我们把牺牲内存空间来提升算法运行速度的思路称为「以空间换时间」;反之,称之为「以时间换空间」。选择哪种思路取决于我们更看重哪个方面。 + +大多数情况下,时间都是比空间更宝贵的,只要空间复杂度不要太离谱、能接受就行,**因此以空间换时间最为常用**。 + +## 2.4.1. 示例题目 * + +以 LeetCode 全站第一题 [两数之和](https://leetcode.cn/problems/two-sum/) 为例。 + +!!! question "两数之和" + + 给定一个整数数组 `nums` 和一个整数目标值 `target` ,请你在该数组中找出“和”为目标值 `target` 的那两个整数,并返回它们的数组下标。 + + 你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。 + + 你可以按任意顺序返回答案。 + +「暴力枚举」和「辅助哈希表」分别为 **空间最优** 和 **时间最优** 的两种解法。本着时间比空间更宝贵的原则,后者是本题的最佳解法。 + +### 方法一:暴力枚举 + +时间复杂度 $O(N^2)$ ,空间复杂度 $O(1)$ ,属于「时间换空间」。 + +虽然仅使用常数大小的额外空间,但运行速度过慢。 + +=== "Java" + + ```java title="leetcode_two_sum.java" + class SolutionBruteForce { + public int[] twoSum(int[] nums, int target) { + int size = nums.length; + // 两层循环,时间复杂度 O(n^2) + for (int i = 0; i < size - 1; i++) { + for (int j = i + 1; j < size; j++) { + if (nums[i] + nums[j] == target) + return new int[] { i, j }; + } + } + return new int[0]; + } + } + ``` + +=== "C++" + + ```cpp title="leetcode_two_sum.cpp" + class SolutionBruteForce { + public: + vector twoSum(vector& nums, int target) { + int size = nums.size(); + // 两层循环,时间复杂度 O(n^2) + for (int i = 0; i < size - 1; i++) { + for (int j = i + 1; j < size; j++) { + if (nums[i] + nums[j] == target) + return { i, j }; + } + } + return {}; + } + }; + ``` + +=== "Python" + + ```python title="leetcode_two_sum.py" + """ 方法一:暴力枚举 """ + class SolutionBruteForce: + def twoSum(self, nums: List[int], target: int) -> List[int]: + # 两层循环,时间复杂度 O(n^2) + for i in range(len(nums) - 1): + for j in range(i + 1, len(nums)): + if nums[i] + nums[j] == target: + return i, j + return [] + ``` + +=== "Go" + + ```go title="leetcode_two_sum.go" + func twoSumBruteForce(nums []int, target int) []int { + size := len(nums) + // 两层循环,时间复杂度 O(n^2) + for i := 0; i < size-1; i++ { + for j := i + 1; i < size; j++ { + if nums[i]+nums[j] == target { + return []int{i, j} + } + } + } + return nil + } + ``` + +=== "JavaScript" + + ```js title="leetcode_two_sum.js" + function twoSumBruteForce(nums, target) { + const n = nums.length; + // 两层循环,时间复杂度 O(n^2) + for (let i = 0; i < n; i++) { + for (let j = i + 1; j < n; j++) { + if (nums[i] + nums[j] === target) { + return [i, j]; + } + } + } + return []; + } + ``` + +=== "TypeScript" + + ```typescript title="leetcode_two_sum.ts" + function twoSumBruteForce(nums: number[], target: number): number[] { + const n = nums.length; + // 两层循环,时间复杂度 O(n^2) + for (let i = 0; i < n; i++) { + for (let j = i + 1; j < n; j++) { + if (nums[i] + nums[j] === target) { + return [i, j]; + } + } + } + return []; + }; + ``` + +=== "C" + + ```c title="leetcode_two_sum.c" + + ``` + +=== "C#" + + ```csharp title="leetcode_two_sum.cs" + class SolutionBruteForce + { + public int[] twoSum(int[] nums, int target) + { + int size = nums.Length; + // 两层循环,时间复杂度 O(n^2) + for (int i = 0; i < size - 1; i++) + { + for (int j = i + 1; j < size; j++) + { + if (nums[i] + nums[j] == target) + return new int[] { i, j }; + } + } + return new int[0]; + } + } + ``` + +=== "Swift" + + ```swift title="leetcode_two_sum.swift" + func twoSumBruteForce(nums: [Int], target: Int) -> [Int] { + // 两层循环,时间复杂度 O(n^2) + for i in nums.indices.dropLast() { + for j in nums.indices.dropFirst(i + 1) { + if nums[i] + nums[j] == target { + return [i, j] + } + } + } + return [0] + } + ``` + +=== "Zig" + + ```zig title="leetcode_two_sum.zig" + const SolutionBruteForce = struct { + pub fn twoSum(self: *SolutionBruteForce, nums: []i32, target: i32) [2]i32 { + _ = self; + var size: usize = nums.len; + var i: usize = 0; + // 两层循环,时间复杂度 O(n^2) + while (i < size - 1) : (i += 1) { + var j = i + 1; + while (j < size) : (j += 1) { + if (nums[i] + nums[j] == target) { + return [_]i32{@intCast(i32, i), @intCast(i32, j)}; + } + } + } + return undefined; + } + }; + ``` + +### 方法二:辅助哈希表 + +时间复杂度 $O(N)$ ,空间复杂度 $O(N)$ ,属于「空间换时间」。 + +借助辅助哈希表 dic ,通过保存数组元素与索引的映射来提升算法运行速度。 + +=== "Java" + + ```java title="leetcode_two_sum.java" + class SolutionHashMap { + public int[] twoSum(int[] nums, int target) { + int size = nums.length; + // 辅助哈希表,空间复杂度 O(n) + Map dic = new HashMap<>(); + // 单层循环,时间复杂度 O(n) + for (int i = 0; i < size; i++) { + if (dic.containsKey(target - nums[i])) { + return new int[] { dic.get(target - nums[i]), i }; + } + dic.put(nums[i], i); + } + return new int[0]; + } + } + ``` + +=== "C++" + + ```cpp title="leetcode_two_sum.cpp" + class SolutionHashMap { + public: + vector twoSum(vector& nums, int target) { + int size = nums.size(); + // 辅助哈希表,空间复杂度 O(n) + unordered_map dic; + // 单层循环,时间复杂度 O(n) + for (int i = 0; i < size; i++) { + if (dic.find(target - nums[i]) != dic.end()) { + return { dic[target - nums[i]], i }; + } + dic.emplace(nums[i], i); + } + return {}; + } + }; + ``` + +=== "Python" + + ```python title="leetcode_two_sum.py" + """ 方法二:辅助哈希表 """ + class SolutionHashMap: + def twoSum(self, nums: List[int], target: int) -> List[int]: + # 辅助哈希表,空间复杂度 O(n) + dic = {} + # 单层循环,时间复杂度 O(n) + for i in range(len(nums)): + if target - nums[i] in dic: + return dic[target - nums[i]], i + dic[nums[i]] = i + return [] + ``` + +=== "Go" + + ```go title="leetcode_two_sum.go" + func twoSumHashTable(nums []int, target int) []int { + // 辅助哈希表,空间复杂度 O(n) + hashTable := map[int]int{} + // 单层循环,时间复杂度 O(n) + for idx, val := range nums { + if preIdx, ok := hashTable[target-val]; ok { + return []int{preIdx, idx} + } + hashTable[val] = idx + } + return nil + } + ``` + +=== "JavaScript" + + ```js title="leetcode_two_sum.js" + function twoSumHashTable(nums, target) { + // 辅助哈希表,空间复杂度 O(n) + let m = {}; + // 单层循环,时间复杂度 O(n) + for (let i = 0; i < nums.length; i++) { + if (m[nums[i]] !== undefined) { + return [m[nums[i]], i]; + } else { + m[target - nums[i]] = i; + } + } + return []; + } + ``` + +=== "TypeScript" + + ```typescript title="leetcode_two_sum.ts" + function twoSumHashTable(nums: number[], target: number): number[] { + // 辅助哈希表,空间复杂度 O(n) + let m: Map = new Map(); + // 单层循环,时间复杂度 O(n) + for (let i = 0; i < nums.length; i++) { + let index = m.get(nums[i]); + if (index !== undefined) { + return [index, i]; + } else { + m.set(target - nums[i], i); + } + } + return []; + }; + ``` + +=== "C" + + ```c title="leetcode_two_sum.c" + + ``` + +=== "C#" + + ```csharp title="leetcode_two_sum.cs" + class SolutionHashMap + { + public int[] twoSum(int[] nums, int target) + { + int size = nums.Length; + // 辅助哈希表,空间复杂度 O(n) + Dictionary dic = new(); + // 单层循环,时间复杂度 O(n) + for (int i = 0; i < size; i++) + { + if (dic.ContainsKey(target - nums[i])) + { + return new int[] { dic[target - nums[i]], i }; + } + dic.Add(nums[i], i); + } + return new int[0]; + } + } + ``` + +=== "Swift" + + ```swift title="leetcode_two_sum.swift" + func twoSumHashTable(nums: [Int], target: Int) -> [Int] { + // 辅助哈希表,空间复杂度 O(n) + var dic: [Int: Int] = [:] + // 单层循环,时间复杂度 O(n) + for i in nums.indices { + if let j = dic[target - nums[i]] { + return [j, i] + } + dic[nums[i]] = i + } + return [0] + } + ``` + +=== "Zig" + + ```zig title="leetcode_two_sum.zig" + const SolutionHashMap = struct { + pub fn twoSum(self: *SolutionHashMap, nums: []i32, target: i32) ![2]i32 { + _ = self; + var size: usize = nums.len; + // 辅助哈希表,空间复杂度 O(n) + var dic = std.AutoHashMap(i32, i32).init(std.heap.page_allocator); + defer dic.deinit(); + var i: usize = 0; + // 单层循环,时间复杂度 O(n) + while (i < size) : (i += 1) { + if (dic.contains(target - nums[i])) { + return [_]i32{dic.get(target - nums[i]).?, @intCast(i32, i)}; + } + try dic.put(nums[i], @intCast(i32, i)); + } + return undefined; + } + }; + ``` diff --git a/build/chapter_computational_complexity/summary.md b/build/chapter_computational_complexity/summary.md new file mode 100644 index 000000000..acd745f9f --- /dev/null +++ b/build/chapter_computational_complexity/summary.md @@ -0,0 +1,28 @@ +--- +comments: true +--- + +# 2.5. 小结 + +### 算法效率评估 + +- 「时间效率」和「空间效率」是算法性能的两个重要的评价维度。 +- 我们可以通过「实际测试」来评估算法效率,但难以排除测试环境的干扰,并且非常耗费计算资源。 +- 「复杂度分析」克服了实际测试的弊端,分析结果适用于所有运行平台,并且可以体现不同数据大小下的算法效率。 + +### 时间复杂度 + +- 「时间复杂度」统计算法运行时间随着数据量变大时的增长趋势,可以有效评估算法效率,但在某些情况下可能失效,比如在输入数据量较小或时间复杂度相同时,无法精确对比算法效率的优劣性。 +- 「最差时间复杂度」使用大 $O$ 符号表示,即函数渐近上界,其反映当 $n$ 趋于正无穷时,$T(n)$ 处于何种增长级别。 +- 推算时间复杂度分为两步,首先统计计算操作数量,再判断渐近上界。 +- 常见时间复杂度从小到大排列有 $O(1)$ , $O(\log n)$ , $O(n)$ , $O(n \log n)$ , $O(n^2)$ , $O(2^n)$ , $O(n!)$ 。 +- 某些算法的时间复杂度不是恒定的,而是与输入数据的分布有关。时间复杂度分为「最差时间复杂度」和「最佳时间复杂度」,后者几乎不用,因为输入数据需要满足苛刻的条件才能达到最佳情况。 +- 「平均时间复杂度」可以反映在随机数据输入下的算法效率,最贴合实际使用情况下的算法性能。计算平均时间复杂度需要统计输入数据的分布,以及综合后的数学期望。 + +### 空间复杂度 + +- 与时间复杂度的定义类似,「空间复杂度」统计算法占用空间随着数据量变大时的增长趋势。 + +- 算法运行中相关内存空间可分为输入空间、暂存空间、输出空间。通常情况下,输入空间不计入空间复杂度计算。暂存空间可分为指令空间、数据空间、栈帧空间,其中栈帧空间一般在递归函数中才会影响到空间复杂度。 +- 我们一般只关心「最差空间复杂度」,即统计算法在「最差输入数据」和「最差运行时间点」下的空间复杂度。 +- 常见空间复杂度从小到大排列有 $O(1)$ , $O(\log n)$ , $O(n)$ , $O(n^2)$ , $O(2^n)$ 。 diff --git a/build/chapter_computational_complexity/time_complexity.md b/build/chapter_computational_complexity/time_complexity.md new file mode 100755 index 000000000..dd8df29b2 --- /dev/null +++ b/build/chapter_computational_complexity/time_complexity.md @@ -0,0 +1,2839 @@ +--- +comments: true +--- + +# 2.2. 时间复杂度 + +## 2.2.1. 统计算法运行时间 + +运行时间能够直观且准确地体现出算法的效率水平。如果我们想要 **准确预估一段代码的运行时间** ,该如何做呢? + +1. 首先需要 **确定运行平台** ,包括硬件配置、编程语言、系统环境等,这些都会影响到代码的运行效率。 +2. 评估 **各种计算操作的所需运行时间** ,例如加法操作 `+` 需要 1 ns ,乘法操作 `*` 需要 10 ns ,打印操作需要 5 ns 等。 +3. 根据代码 **统计所有计算操作的数量** ,并将所有操作的执行时间求和,即可得到运行时间。 + +例如以下代码,输入数据大小为 $n$ ,根据以上方法,可以得到算法运行时间为 $6n + 12$ ns 。 + +$$ +1 + 1 + 10 + (1 + 5) \times n = 6n + 12 +$$ + +=== "Java" + + ```java title="" + // 在某运行平台下 + void algorithm(int n) { + int a = 2; // 1 ns + a = a + 1; // 1 ns + a = a * 2; // 10 ns + // 循环 n 次 + for (int i = 0; i < n; i++) { // 1 ns ,每轮都要执行 i++ + System.out.println(0); // 5 ns + } + } + ``` + +=== "C++" + + ```cpp title="" + // 在某运行平台下 + void algorithm(int n) { + int a = 2; // 1 ns + a = a + 1; // 1 ns + a = a * 2; // 10 ns + // 循环 n 次 + for (int i = 0; i < n; i++) { // 1 ns ,每轮都要执行 i++ + cout << 0 << endl; // 5 ns + } + } + ``` + +=== "Python" + + ```python title="" + # 在某运行平台下 + def algorithm(n): + a = 2 # 1 ns + a = a + 1 # 1 ns + a = a * 2 # 10 ns + # 循环 n 次 + for _ in range(n): # 1 ns + print(0) # 5 ns + ``` + +=== "Go" + + ```go title="" + // 在某运行平台下 + func algorithm(n int) { + a := 2 // 1 ns + a = a + 1 // 1 ns + a = a * 2 // 10 ns + // 循环 n 次 + for i := 0; i < n; i++ { // 1 ns + fmt.Println(a) // 5 ns + } + } + ``` + +=== "JavaScript" + + ```js title="" + // 在某运行平台下 + function algorithm(n) { + var a = 2; // 1 ns + a = a + 1; // 1 ns + a = a * 2; // 10 ns + // 循环 n 次 + for(let i = 0; i < n; i++) { // 1 ns ,每轮都要执行 i++ + console.log(0); // 5 ns + } + } + ``` + +=== "TypeScript" + + ```typescript title="" + // 在某运行平台下 + function algorithm(n: number): void { + var a: number = 2; // 1 ns + a = a + 1; // 1 ns + a = a * 2; // 10 ns + // 循环 n 次 + for(let i = 0; i < n; i++) { // 1 ns ,每轮都要执行 i++ + console.log(0); // 5 ns + } + } + ``` + +=== "C" + + ```c title="" + // 在某运行平台下 + void algorithm(int n) { + int a = 2; // 1 ns + a = a + 1; // 1 ns + a = a * 2; // 10 ns + // 循环 n 次 + for (int i = 0; i < n; i++) { // 1 ns ,每轮都要执行 i++ + printf("%d", 0); // 5 ns + } + } + ``` + +=== "C#" + + ```csharp title="" + // 在某运行平台下 + void algorithm(int n) + { + int a = 2; // 1 ns + a = a + 1; // 1 ns + a = a * 2; // 10 ns + // 循环 n 次 + for (int i = 0; i < n; i++) + { // 1 ns ,每轮都要执行 i++ + Console.WriteLine(0); // 5 ns + } + } + ``` + +=== "Swift" + + ```swift title="" + // 在某运行平台下 + func algorithm(_ n: Int) { + var a = 2 // 1 ns + a = a + 1 // 1 ns + a = a * 2 // 10 ns + // 循环 n 次 + for _ in 0 ..< n { // 1 ns + print(0) // 5 ns + } + } + ``` + +=== "Zig" + + ```zig title="" + + ``` + +但实际上, **统计算法的运行时间既不合理也不现实**。首先,我们不希望预估时间和运行平台绑定,毕竟算法需要跑在各式各样的平台之上。其次,我们很难获知每一种操作的运行时间,这为预估过程带来了极大的难度。 + +## 2.2.2. 统计时间增长趋势 + +「时间复杂度分析」采取了不同的做法,其统计的不是算法运行时间,而是 **算法运行时间随着数据量变大时的增长趋势** 。 + +“时间增长趋势”这个概念比较抽象,我们借助一个例子来理解。设输入数据大小为 $n$ ,给定三个算法 `A` , `B` , `C` 。 + +- 算法 `A` 只有 $1$ 个打印操作,算法运行时间不随着 $n$ 增大而增长。我们称此算法的时间复杂度为「常数阶」。 +- 算法 `B` 中的打印操作需要循环 $n$ 次,算法运行时间随着 $n$ 增大成线性增长。此算法的时间复杂度被称为「线性阶」。 +- 算法 `C` 中的打印操作需要循环 $1000000$ 次,但运行时间仍与输入数据大小 $n$ 无关。因此 `C` 的时间复杂度和 `A` 相同,仍为「常数阶」。 + +=== "Java" + + ```java title="" + // 算法 A 时间复杂度:常数阶 + void algorithm_A(int n) { + System.out.println(0); + } + // 算法 B 时间复杂度:线性阶 + void algorithm_B(int n) { + for (int i = 0; i < n; i++) { + System.out.println(0); + } + } + // 算法 C 时间复杂度:常数阶 + void algorithm_C(int n) { + for (int i = 0; i < 1000000; i++) { + System.out.println(0); + } + } + ``` + +=== "C++" + + ```cpp title="" + // 算法 A 时间复杂度:常数阶 + void algorithm_A(int n) { + cout << 0 << endl; + } + // 算法 B 时间复杂度:线性阶 + void algorithm_B(int n) { + for (int i = 0; i < n; i++) { + cout << 0 << endl; + } + } + // 算法 C 时间复杂度:常数阶 + void algorithm_C(int n) { + for (int i = 0; i < 1000000; i++) { + cout << 0 << endl; + } + } + ``` + +=== "Python" + + ```python title="" + # 算法 A 时间复杂度:常数阶 + def algorithm_A(n): + print(0) + # 算法 B 时间复杂度:线性阶 + def algorithm_B(n): + for _ in range(n): + print(0) + # 算法 C 时间复杂度:常数阶 + def algorithm_C(n): + for _ in range(1000000): + print(0) + ``` + +=== "Go" + + ```go title="" + // 算法 A 时间复杂度:常数阶 + func algorithm_A(n int) { + fmt.Println(0) + } + // 算法 B 时间复杂度:线性阶 + func algorithm_B(n int) { + for i := 0; i < n; i++ { + fmt.Println(0) + } + } + // 算法 C 时间复杂度:常数阶 + func algorithm_C(n int) { + for i := 0; i < 1000000; i++ { + fmt.Println(0) + } + } + ``` + +=== "JavaScript" + + ```js title="" + // 算法 A 时间复杂度:常数阶 + function algorithm_A(n) { + console.log(0); + } + // 算法 B 时间复杂度:线性阶 + function algorithm_B(n) { + for (let i = 0; i < n; i++) { + console.log(0); + } + } + // 算法 C 时间复杂度:常数阶 + function algorithm_C(n) { + for (let i = 0; i < 1000000; i++) { + console.log(0); + } + } + + ``` + +=== "TypeScript" + + ```typescript title="" + // 算法 A 时间复杂度:常数阶 + function algorithm_A(n: number): void { + console.log(0); + } + // 算法 B 时间复杂度:线性阶 + function algorithm_B(n: number): void { + for (let i = 0; i < n; i++) { + console.log(0); + } + } + // 算法 C 时间复杂度:常数阶 + function algorithm_C(n: number): void { + for (let i = 0; i < 1000000; i++) { + console.log(0); + } + } + ``` + +=== "C" + + ```c title="" + // 算法 A 时间复杂度:常数阶 + void algorithm_A(int n) { + printf("%d", 0); + } + // 算法 B 时间复杂度:线性阶 + void algorithm_B(int n) { + for (int i = 0; i < n; i++) { + printf("%d", 0); + } + } + // 算法 C 时间复杂度:常数阶 + void algorithm_C(int n) { + for (int i = 0; i < 1000000; i++) { + printf("%d", 0); + } + } + ``` + +=== "C#" + + ```csharp title="" + // 算法 A 时间复杂度:常数阶 + void algorithm_A(int n) + { + Console.WriteLine(0); + } + // 算法 B 时间复杂度:线性阶 + void algorithm_B(int n) + { + for (int i = 0; i < n; i++) + { + Console.WriteLine(0); + } + } + // 算法 C 时间复杂度:常数阶 + void algorithm_C(int n) + { + for (int i = 0; i < 1000000; i++) + { + Console.WriteLine(0); + } + } + ``` + +=== "Swift" + + ```swift title="" + // 算法 A 时间复杂度:常数阶 + func algorithmA(_ n: Int) { + print(0) + } + + // 算法 B 时间复杂度:线性阶 + func algorithmB(_ n: Int) { + for _ in 0 ..< n { + print(0) + } + } + + // 算法 C 时间复杂度:常数阶 + func algorithmC(_ n: Int) { + for _ in 0 ..< 1000000 { + print(0) + } + } + ``` + +=== "Zig" + + ```zig title="" + + ``` + +![time_complexity_first_example](time_complexity.assets/time_complexity_first_example.png) + +

Fig. 算法 A, B, C 的时间增长趋势

+ +相比直接统计算法运行时间,时间复杂度分析的做法有什么好处呢?以及有什么不足? + +**时间复杂度可以有效评估算法效率**。算法 `B` 运行时间的增长是线性的,在 $n > 1$ 时慢于算法 `A` ,在 $n > 1000000$ 时慢于算法 `C` 。实质上,只要输入数据大小 $n$ 足够大,复杂度为「常数阶」的算法一定优于「线性阶」的算法,这也正是时间增长趋势的含义。 + +**时间复杂度的推算方法更加简便**。在时间复杂度分析中,我们可以将统计「计算操作的运行时间」简化为统计「计算操作的数量」,这是因为,无论是运行平台还是计算操作类型,都与算法运行时间的增长趋势无关。因而,我们可以简单地将所有计算操作的执行时间统一看作是相同的“单位时间”,这样的简化做法大大降低了估算难度。 + +**时间复杂度也存在一定的局限性**。比如,虽然算法 `A` 和 `C` 的时间复杂度相同,但是实际的运行时间有非常大的差别。再比如,虽然算法 `B` 比 `C` 的时间复杂度要更高,但在输入数据大小 $n$ 比较小时,算法 `B` 是要明显优于算法 `C` 的。对于以上情况,我们很难仅凭时间复杂度来判定算法效率高低。然而,即使存在这些问题,计算复杂度仍然是评判算法效率的最有效且常用的方法。 + +## 2.2.3. 函数渐近上界 + +设算法「计算操作数量」为 $T(n)$ ,其是一个关于输入数据大小 $n$ 的函数。例如,以下算法的操作数量为 + +$$ +T(n) = 3 + 2n +$$ + +=== "Java" + + ```java title="" + void algorithm(int n) { + int a = 1; // +1 + a = a + 1; // +1 + a = a * 2; // +1 + // 循环 n 次 + for (int i = 0; i < n; i++) { // +1(每轮都执行 i ++) + System.out.println(0); // +1 + } + } + ``` + +=== "C++" + + ```cpp title="" + void algorithm(int n) { + int a = 1; // +1 + a = a + 1; // +1 + a = a * 2; // +1 + // 循环 n 次 + for (int i = 0; i < n; i++) { // +1(每轮都执行 i ++) + cout << 0 << endl; // +1 + } + } + ``` + +=== "Python" + + ```python title="" + def algorithm(n): + a = 1 # +1 + a = a + 1 # +1 + a = a * 2 # +1 + # 循环 n 次 + for i in range(n): # +1 + print(0) # +1 + ``` + +=== "Go" + + ```go title="" + func algorithm(n int) { + a := 1 // +1 + a = a + 1 // +1 + a = a * 2 // +1 + // 循环 n 次 + for i := 0; i < n; i++ { // +1 + fmt.Println(a) // +1 + } + } + ``` + +=== "JavaScript" + + ```js title="" + function algorithm(n){ + var a = 1; // +1 + a += 1; // +1 + a *= 2; // +1 + // 循环 n 次 + for(let i = 0; i < n; i++){ // +1(每轮都执行 i ++) + console.log(0); // +1 + } + + } + ``` + +=== "TypeScript" + + ```typescript title="" + function algorithm(n: number): void{ + var a: number = 1; // +1 + a += 1; // +1 + a *= 2; // +1 + // 循环 n 次 + for(let i = 0; i < n; i++){ // +1(每轮都执行 i ++) + console.log(0); // +1 + } + + } + ``` + +=== "C" + + ```c title="" + void algorithm(int n) { + int a = 1; // +1 + a = a + 1; // +1 + a = a * 2; // +1 + // 循环 n 次 + for (int i = 0; i < n; i++) { // +1(每轮都执行 i ++) + printf("%d", 0); // +1 + } + } + ``` + +=== "C#" + + ```csharp title="" + void algorithm(int n) { + int a = 1; // +1 + a = a + 1; // +1 + a = a * 2; // +1 + // 循环 n 次 + for (int i = 0; i < n; i++) { // +1(每轮都执行 i ++) + Console.WriteLine(0); // +1 + } + } + ``` + +=== "Swift" + + ```swift title="" + func algorithm(n: Int) { + var a = 1 // +1 + a = a + 1 // +1 + a = a * 2 // +1 + // 循环 n 次 + for _ in 0 ..< n { // +1 + print(0) // +1 + } + } + ``` + +=== "Zig" + + ```zig title="" + + ``` + +$T(n)$ 是个一次函数,说明时间增长趋势是线性的,因此易得时间复杂度是线性阶。 + +我们将线性阶的时间复杂度记为 $O(n)$ ,这个数学符号被称为「大 $O$ 记号 Big-$O$ Notation」,代表函数 $T(n)$ 的「渐近上界 asymptotic upper bound」。 + +我们要推算时间复杂度,本质上是在计算「操作数量函数 $T(n)$ 」的渐近上界。下面我们先来看看函数渐近上界的数学定义。 + +!!! abstract "函数渐近上界" + + 若存在正实数 $c$ 和实数 $n_0$ ,使得对于所有的 $n > n_0$ ,均有 + $$ + T(n) \leq c \cdot f(n) + $$ + 则可认为 $f(n)$ 给出了 $T(n)$ 的一个渐近上界,记为 + $$ + T(n) = O(f(n)) + $$ + +![asymptotic_upper_bound](time_complexity.assets/asymptotic_upper_bound.png) + +

Fig. 函数的渐近上界

+ +本质上看,计算渐近上界就是在找一个函数 $f(n)$ ,**使得在 $n$ 趋向于无穷大时,$T(n)$ 和 $f(n)$ 处于相同的增长级别(仅相差一个常数项 $c$ 的倍数)**。 + +!!! tip + + 渐近上界的数学味儿有点重,如果你感觉没有完全理解,无需担心,因为在实际使用中我们只需要会推算即可,数学意义可以慢慢领悟。 + +## 2.2.4. 推算方法 + +推算出 $f(n)$ 后,我们就得到时间复杂度 $O(f(n))$ 。那么,如何来确定渐近上界 $f(n)$ 呢?总体分为两步,首先「统计操作数量」,然后「判断渐近上界」。 + +### 1) 统计操作数量 + +对着代码,从上到下一行一行地计数即可。然而,**由于上述 $c \cdot f(n)$ 中的常数项 $c$ 可以取任意大小,因此操作数量 $T(n)$ 中的各种系数、常数项都可以被忽略**。根据此原则,可以总结出以下计数偷懒技巧: + +1. **跳过数量与 $n$ 无关的操作**。因为他们都是 $T(n)$ 中的常数项,对时间复杂度不产生影响。 +2. **省略所有系数**。例如,循环 $2n$ 次、$5n + 1$ 次、……,都可以化简记为 $n$ 次,因为 $n$ 前面的系数对时间复杂度也不产生影响。 +3. **循环嵌套时使用乘法**。总操作数量等于外层循环和内层循环操作数量之积,每一层循环依然可以分别套用上述 `1.` 和 `2.` 技巧。 + +根据以下示例,使用上述技巧前、后的统计结果分别为 + +$$ +\begin{aligned} +T(n) & = 2n(n + 1) + (5n + 1) + 2 & \text{完整统计 (-.-|||)} \newline +& = 2n^2 + 7n + 3 \newline +T(n) & = n^2 + n & \text{偷懒统计 (o.O)} +\end{aligned} +$$ + +最终,两者都能推出相同的时间复杂度结果,即 $O(n^2)$ 。 + +=== "Java" + + ```java title="" + void algorithm(int n) { + int a = 1; // +0(技巧 1) + a = a + n; // +0(技巧 1) + // +n(技巧 2) + for (int i = 0; i < 5 * n + 1; i++) { + System.out.println(0); + } + // +n*n(技巧 3) + for (int i = 0; i < 2 * n; i++) { + for (int j = 0; j < n + 1; j++) { + System.out.println(0); + } + } + } + ``` + +=== "C++" + + ```cpp title="" + void algorithm(int n) { + int a = 1; // +0(技巧 1) + a = a + n; // +0(技巧 1) + // +n(技巧 2) + for (int i = 0; i < 5 * n + 1; i++) { + cout << 0 << endl; + } + // +n*n(技巧 3) + for (int i = 0; i < 2 * n; i++) { + for (int j = 0; j < n + 1; j++) { + cout << 0 << endl; + } + } + } + ``` + +=== "Python" + + ```python title="" + def algorithm(n): + a = 1 # +0(技巧 1) + a = a + n # +0(技巧 1) + # +n(技巧 2) + for i in range(5 * n + 1): + print(0) + # +n*n(技巧 3) + for i in range(2 * n): + for j in range(n + 1): + print(0) + ``` + +=== "Go" + + ```go title="" + func algorithm(n int) { + a := 1 // +0(技巧 1) + a = a + n // +0(技巧 1) + // +n(技巧 2) + for i := 0; i < 5 * n + 1; i++ { + fmt.Println(0) + } + // +n*n(技巧 3) + for i := 0; i < 2 * n; i++ { + for j := 0; j < n + 1; j++ { + fmt.Println(0) + } + } + } + ``` + +=== "JavaScript" + + ```js title="" + function algorithm(n) { + let a = 1; // +0(技巧 1) + a = a + n; // +0(技巧 1) + // +n(技巧 2) + for (let i = 0; i < 5 * n + 1; i++) { + console.log(0); + } + // +n*n(技巧 3) + for (let i = 0; i < 2 * n; i++) { + for (let j = 0; j < n + 1; j++) { + console.log(0); + } + } + } + ``` + +=== "TypeScript" + + ```typescript title="" + function algorithm(n: number): void { + let a = 1; // +0(技巧 1) + a = a + n; // +0(技巧 1) + // +n(技巧 2) + for (let i = 0; i < 5 * n + 1; i++) { + console.log(0); + } + // +n*n(技巧 3) + for (let i = 0; i < 2 * n; i++) { + for (let j = 0; j < n + 1; j++) { + console.log(0); + } + } + } + ``` + +=== "C" + + ```c title="" + void algorithm(int n) { + int a = 1; // +0(技巧 1) + a = a + n; // +0(技巧 1) + // +n(技巧 2) + for (int i = 0; i < 5 * n + 1; i++) { + printf("%d", 0); + } + // +n*n(技巧 3) + for (int i = 0; i < 2 * n; i++) { + for (int j = 0; j < n + 1; j++) { + printf("%d", 0); + } + } + } + ``` + +=== "C#" + + ```csharp title="" + void algorithm(int n) + { + int a = 1; // +0(技巧 1) + a = a + n; // +0(技巧 1) + // +n(技巧 2) + for (int i = 0; i < 5 * n + 1; i++) + { + Console.WriteLine(0); + } + // +n*n(技巧 3) + for (int i = 0; i < 2 * n; i++) + { + for (int j = 0; j < n + 1; j++) + { + Console.WriteLine(0); + } + } + } + ``` + +=== "Swift" + + ```swift title="" + func algorithm(n: Int) { + var a = 1 // +0(技巧 1) + a = a + n // +0(技巧 1) + // +n(技巧 2) + for _ in 0 ..< (5 * n + 1) { + print(0) + } + // +n*n(技巧 3) + for _ in 0 ..< (2 * n) { + for _ in 0 ..< (n + 1) { + print(0) + } + } + } + ``` + +=== "Zig" + + ```zig title="" + + ``` + +### 2) 判断渐近上界 + +**时间复杂度由多项式 $T(n)$ 中最高阶的项来决定**。这是因为在 $n$ 趋于无穷大时,最高阶的项将处于主导作用,其它项的影响都可以被忽略。 + +以下表格给出了一些例子,其中有一些夸张的值,是想要向大家强调 **系数无法撼动阶数** 这一结论。在 $n$ 趋于无穷大时,这些常数都是“浮云”。 + +
+ +| 操作数量 $T(n)$ | 时间复杂度 $O(f(n))$ | +| ---------------------- | -------------------- | +| $100000$ | $O(1)$ | +| $3n + 2$ | $O(n)$ | +| $2n^2 + 3n + 2$ | $O(n^2)$ | +| $n^3 + 10000n^2$ | $O(n^3)$ | +| $2^n + 10000n^{10000}$ | $O(2^n)$ | + +
+ +## 2.2.5. 常见类型 + +设输入数据大小为 $n$ ,常见的时间复杂度类型有(从低到高排列) + +$$ +\begin{aligned} +O(1) < O(\log n) < O(n) < O(n \log n) < O(n^2) < O(2^n) < O(n!) \newline +\text{常数阶} < \text{对数阶} < \text{线性阶} < \text{线性对数阶} < \text{平方阶} < \text{指数阶} < \text{阶乘阶} +\end{aligned} +$$ + +![time_complexity_common_types](time_complexity.assets/time_complexity_common_types.png) + +

Fig. 时间复杂度的常见类型

+ +!!! tip + + 部分示例代码需要一些前置知识,包括数组、递归算法等。如果遇到看不懂的地方无需担心,可以在学习完后面章节后再来复习,现阶段先聚焦在理解时间复杂度含义和推算方法上。 + +### 常数阶 $O(1)$ + +常数阶的操作数量与输入数据大小 $n$ 无关,即不随着 $n$ 的变化而变化。 + +对于以下算法,无论操作数量 `size` 有多大,只要与数据大小 $n$ 无关,时间复杂度就仍为 $O(1)$ 。 + +=== "Java" + + ```java title="time_complexity.java" + /* 常数阶 */ + int constant(int n) { + int count = 0; + int size = 100000; + for (int i = 0; i < size; i++) + count++; + return count; + } + ``` + +=== "C++" + + ```cpp title="time_complexity.cpp" + /* 常数阶 */ + int constant(int n) { + int count = 0; + int size = 100000; + for (int i = 0; i < size; i++) + count++; + return count; + } + ``` + +=== "Python" + + ```python title="time_complexity.py" + """ 常数阶 """ + def constant(n): + count = 0 + size = 100000 + for _ in range(size): + count += 1 + return count + ``` + +=== "Go" + + ```go title="time_complexity.go" + /* 常数阶 */ + func constant(n int) int { + count := 0 + size := 100000 + for i := 0; i < size; i++ { + count ++ + } + return count + } + ``` + +=== "JavaScript" + + ```js title="time_complexity.js" + /* 常数阶 */ + function constant(n) { + let count = 0; + const size = 100000; + for (let i = 0; i < size; i++) count++; + return count; + } + ``` + +=== "TypeScript" + + ```typescript title="time_complexity.ts" + /* 常数阶 */ + function constant(n: number): number { + let count = 0; + const size = 100000; + for (let i = 0; i < size; i++) count++; + return count; + } + ``` + +=== "C" + + ```c title="time_complexity.c" + /* 常数阶 */ + int constant(int n) { + int count = 0; + int size = 100000; + int i = 0; + for (int i = 0; i < size; i++) { + count ++; + } + return count; + } + ``` + +=== "C#" + + ```csharp title="time_complexity.cs" + /* 常数阶 */ + int constant(int n) + { + int count = 0; + int size = 100000; + for (int i = 0; i < size; i++) + count++; + return count; + } + ``` + +=== "Swift" + + ```swift title="time_complexity.swift" + /* 常数阶 */ + func constant(n: Int) -> Int { + var count = 0 + let size = 100000 + for _ in 0 ..< size { + count += 1 + } + return count + } + ``` + +=== "Zig" + + ```zig title="time_complexity.zig" + // 常数阶 + fn constant(n: i32) i32 { + _ = n; + var count: i32 = 0; + const size: i32 = 100_000; + var i: i32 = 0; + while(i Int { + var count = 0 + for _ in 0 ..< n { + count += 1 + } + return count + } + ``` + +=== "Zig" + + ```zig title="time_complexity.zig" + // 线性阶 + fn linear(n: i32) i32 { + var count: i32 = 0; + var i: i32 = 0; + while (i < n) : (i += 1) { + count += 1; + } + return count; + } + ``` + +「遍历数组」和「遍历链表」等操作,时间复杂度都为 $O(n)$ ,其中 $n$ 为数组或链表的长度。 + +!!! tip + + **数据大小 $n$ 是根据输入数据的类型来确定的**。比如,在上述示例中,我们直接将 $n$ 看作输入数据大小;以下遍历数组示例中,数据大小 $n$ 为数组的长度。 + +=== "Java" + + ```java title="time_complexity.java" + /* 线性阶(遍历数组) */ + int arrayTraversal(int[] nums) { + int count = 0; + // 循环次数与数组长度成正比 + for (int num : nums) { + count++; + } + return count; + } + ``` + +=== "C++" + + ```cpp title="time_complexity.cpp" + /* 线性阶(遍历数组) */ + int arrayTraversal(vector& nums) { + int count = 0; + // 循环次数与数组长度成正比 + for (int num : nums) { + count++; + } + return count; + } + ``` + +=== "Python" + + ```python title="time_complexity.py" + """ 线性阶(遍历数组)""" + def array_traversal(nums): + count = 0 + # 循环次数与数组长度成正比 + for num in nums: + count += 1 + return count + ``` + +=== "Go" + + ```go title="time_complexity.go" + /* 线性阶(遍历数组) */ + func arrayTraversal(nums []int) int { + count := 0 + // 循环次数与数组长度成正比 + for range nums { + count++ + } + return count + } + ``` + +=== "JavaScript" + + ```js title="time_complexity.js" + /* 线性阶(遍历数组) */ + function arrayTraversal(nums) { + let count = 0; + // 循环次数与数组长度成正比 + for (let i = 0; i < nums.length; i++) { + count++; + } + return count; + } + ``` + +=== "TypeScript" + + ```typescript title="time_complexity.ts" + /* 线性阶(遍历数组) */ + function arrayTraversal(nums: number[]): number { + let count = 0; + // 循环次数与数组长度成正比 + for (let i = 0; i < nums.length; i++) { + count++; + } + return count; + } + ``` + +=== "C" + + ```c title="time_complexity.c" + /* 线性阶(遍历数组) */ + int arrayTraversal(int *nums, int n) { + int count = 0; + // 循环次数与数组长度成正比 + for (int i = 0; i < n; i++) { + count ++; + } + return count; + } + ``` + +=== "C#" + + ```csharp title="time_complexity.cs" + /* 线性阶(遍历数组) */ + int arrayTraversal(int[] nums) + { + int count = 0; + // 循环次数与数组长度成正比 + foreach(int num in nums) + { + count++; + } + return count; + } + ``` + +=== "Swift" + + ```swift title="time_complexity.swift" + /* 线性阶(遍历数组) */ + func arrayTraversal(nums: [Int]) -> Int { + var count = 0 + // 循环次数与数组长度成正比 + for _ in nums { + count += 1 + } + return count + } + ``` + +=== "Zig" + + ```zig title="time_complexity.zig" + // 线性阶(遍历数组) + fn arrayTraversal(nums: []i32) i32 { + var count: i32 = 0; + // 循环次数与数组长度成正比 + for (nums) |_| { + count += 1; + } + return count; + } + ``` + +### 平方阶 $O(n^2)$ + +平方阶的操作数量相对输入数据大小成平方级别增长。平方阶常出现于嵌套循环,外层循环和内层循环都为 $O(n)$ ,总体为 $O(n^2)$ 。 + +=== "Java" + + ```java title="time_complexity.java" + /* 平方阶 */ + int quadratic(int n) { + int count = 0; + // 循环次数与数组长度成平方关系 + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + count++; + } + } + return count; + } + ``` + +=== "C++" + + ```cpp title="time_complexity.cpp" + /* 平方阶 */ + int quadratic(int n) { + int count = 0; + // 循环次数与数组长度成平方关系 + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + count++; + } + } + return count; + } + ``` + +=== "Python" + + ```python title="time_complexity.py" + """ 平方阶 """ + def quadratic(n): + count = 0 + # 循环次数与数组长度成平方关系 + for i in range(n): + for j in range(n): + count += 1 + return count + ``` + +=== "Go" + + ```go title="time_complexity.go" + /* 平方阶 */ + func quadratic(n int) int { + count := 0 + // 循环次数与数组长度成平方关系 + for i := 0; i < n; i++ { + for j := 0; j < n; j++ { + count++ + } + } + return count + } + ``` + +=== "JavaScript" + + ```js title="time_complexity.js" + /* 平方阶 */ + function quadratic(n) { + let count = 0; + // 循环次数与数组长度成平方关系 + for (let i = 0; i < n; i++) { + for (let j = 0; j < n; j++) { + count++; + } + } + return count; + } + ``` + +=== "TypeScript" + + ```typescript title="time_complexity.ts" + /* 平方阶 */ + function quadratic(n: number): number { + let count = 0; + // 循环次数与数组长度成平方关系 + for (let i = 0; i < n; i++) { + for (let j = 0; j < n; j++) { + count++; + } + } + return count; + } + ``` + +=== "C" + + ```c title="time_complexity.c" + /* 平方阶 */ + int quadratic(int n) { + int count = 0; + // 循环次数与数组长度成平方关系 + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + count ++; + } + } + return count; + } + ``` + +=== "C#" + + ```csharp title="time_complexity.cs" + /* 平方阶 */ + int quadratic(int n) + { + int count = 0; + // 循环次数与数组长度成平方关系 + for (int i = 0; i < n; i++) + { + for (int j = 0; j < n; j++) + { + count++; + } + } + return count; + } + ``` + +=== "Swift" + + ```swift title="time_complexity.swift" + /* 平方阶 */ + func quadratic(n: Int) -> Int { + var count = 0 + // 循环次数与数组长度成平方关系 + for _ in 0 ..< n { + for _ in 0 ..< n { + count += 1 + } + } + return count + } + ``` + +=== "Zig" + + ```zig title="time_complexity.zig" + // 平方阶 + fn quadratic(n: i32) i32 { + var count: i32 = 0; + var i: i32 = 0; + // 循环次数与数组长度成平方关系 + while (i < n) : (i += 1) { + var j: i32 = 0; + while (j < n) : (j += 1) { + count += 1; + } + } + return count; + } + ``` + +![time_complexity_constant_linear_quadratic](time_complexity.assets/time_complexity_constant_linear_quadratic.png) + +

Fig. 常数阶、线性阶、平方阶的时间复杂度

+ +以「冒泡排序」为例,外层循环 $n - 1$ 次,内层循环 $n-1, n-2, \cdots, 2, 1$ 次,平均为 $\frac{n}{2}$ 次,因此时间复杂度为 $O(n^2)$ 。 + +$$ +O((n - 1) \frac{n}{2}) = O(n^2) +$$ + +=== "Java" + + ```java title="time_complexity.java" + /* 平方阶(冒泡排序) */ + int bubbleSort(int[] nums) { + int count = 0; // 计数器 + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for (int i = nums.length - 1; i > 0; i--) { + // 内循环:冒泡操作 + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交换 nums[j] 与 nums[j + 1] + int tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + count += 3; // 元素交换包含 3 个单元操作 + } + } + } + return count; + } + ``` + +=== "C++" + + ```cpp title="time_complexity.cpp" + /* 平方阶(冒泡排序) */ + int bubbleSort(vector& nums) { + int count = 0; // 计数器 + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for (int i = nums.size() - 1; i > 0; i--) { + // 内循环:冒泡操作 + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交换 nums[j] 与 nums[j + 1] + int tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + count += 3; // 元素交换包含 3 个单元操作 + } + } + } + return count; + } + ``` + +=== "Python" + + ```python title="time_complexity.py" + """ 平方阶(冒泡排序)""" + def bubble_sort(nums): + count = 0 # 计数器 + # 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for i in range(len(nums) - 1, 0, -1): + # 内循环:冒泡操作 + for j in range(i): + if nums[j] > nums[j + 1]: + # 交换 nums[j] 与 nums[j + 1] + tmp = nums[j] + nums[j] = nums[j + 1] + nums[j + 1] = tmp + count += 3 # 元素交换包含 3 个单元操作 + return count + ``` + +=== "Go" + + ```go title="time_complexity.go" + /* 平方阶(冒泡排序) */ + func bubbleSort(nums []int) int { + count := 0 // 计数器 + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for i := len(nums) - 1; i > 0; i-- { + // 内循环:冒泡操作 + for j := 0; j < i; j++ { + if nums[j] > nums[j+1] { + // 交换 nums[j] 与 nums[j + 1] + tmp := nums[j] + nums[j] = nums[j+1] + nums[j+1] = tmp + count += 3 // 元素交换包含 3 个单元操作 + } + } + } + return count + } + ``` + +=== "JavaScript" + + ```js title="time_complexity.js" + /* 平方阶(冒泡排序) */ + function bubbleSort(nums) { + let count = 0; // 计数器 + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for (let i = nums.length - 1; i > 0; i--) { + // 内循环:冒泡操作 + for (let j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交换 nums[j] 与 nums[j + 1] + let tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + count += 3; // 元素交换包含 3 个单元操作 + } + } + } + return count; + } + ``` + +=== "TypeScript" + + ```typescript title="time_complexity.ts" + /* 平方阶(冒泡排序) */ + function bubbleSort(nums: number[]): number { + let count = 0; // 计数器 + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for (let i = nums.length - 1; i > 0; i--) { + // 内循环:冒泡操作 + for (let j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交换 nums[j] 与 nums[j + 1] + let tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + count += 3; // 元素交换包含 3 个单元操作 + } + } + } + return count; + } + ``` + +=== "C" + + ```c title="time_complexity.c" + /* 平方阶(冒泡排序) */ + int bubbleSort(int *nums, int n) { + int count = 0; // 计数器 + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for (int i = n - 1; i > 0; i--) { + // 内循环:冒泡操作 + for (int j = 0; j < i; j++) { + if (nums[j] > nums [j + 1]) + { + // 交换 nums[j] 与 nums[j + 1] + int tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + count += 3; // 元素交换包含 3 个单元操作 + } + } + + } + return count; + } + ``` + +=== "C#" + + ```csharp title="time_complexity.cs" + /* 平方阶(冒泡排序) */ + int bubbleSort(int[] nums) + { + int count = 0; // 计数器 + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for (int i = nums.Length - 1; i > 0; i--) + { + // 内循环:冒泡操作 + for (int j = 0; j < i; j++) + { + if (nums[j] > nums[j + 1]) + { + // 交换 nums[j] 与 nums[j + 1] + int tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + count += 3; // 元素交换包含 3 个单元操作 + } + } + } + return count; + } + + ``` + +=== "Swift" + + ```swift title="time_complexity.swift" + /* 平方阶(冒泡排序) */ + func bubbleSort(nums: inout [Int]) -> Int { + var count = 0 // 计数器 + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for i in sequence(first: nums.count - 1, next: { $0 > 0 + 1 ? $0 - 1 : nil }) { + // 内循环:冒泡操作 + for j in 0 ..< i { + if nums[j] > nums[j + 1] { + // 交换 nums[j] 与 nums[j + 1] + let tmp = nums[j] + nums[j] = nums[j + 1] + nums[j + 1] = tmp + count += 3 // 元素交换包含 3 个单元操作 + } + } + } + return count + } + ``` + +=== "Zig" + + ```zig title="time_complexity.zig" + // 平方阶(冒泡排序) + fn bubbleSort(nums: []i32) i32 { + var count: i32 = 0; // 计数器 + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + var i: i32 = @intCast(i32, nums.len ) - 1; + while (i > 0) : (i -= 1) { + var j: usize = 0; + // 内循环:冒泡操作 + while (j < i) : (j += 1) { + if (nums[j] > nums[j + 1]) { + // 交换 nums[j] 与 nums[j + 1] + var tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + count += 3; // 元素交换包含 3 个单元操作 + } + } + } + return count; + } + ``` + +### 指数阶 $O(2^n)$ + +!!! note + + 生物学科中的“细胞分裂”即是指数阶增长:初始状态为 $1$ 个细胞,分裂一轮后为 $2$ 个,分裂两轮后为 $4$ 个,……,分裂 $n$ 轮后有 $2^n$ 个细胞。 + +指数阶增长得非常快,在实际应用中一般是不能被接受的。若一个问题使用「暴力枚举」求解的时间复杂度是 $O(2^n)$ ,那么一般都需要使用「动态规划」或「贪心算法」等算法来求解。 + +=== "Java" + + ```java title="time_complexity.java" + /* 指数阶(循环实现) */ + int exponential(int n) { + int count = 0, base = 1; + // cell 每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1) + for (int i = 0; i < n; i++) { + for (int j = 0; j < base; j++) { + count++; + } + base *= 2; + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + return count; + } + ``` + +=== "C++" + + ```cpp title="time_complexity.cpp" + /* 指数阶(循环实现) */ + int exponential(int n) { + int count = 0, base = 1; + // cell 每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1) + for (int i = 0; i < n; i++) { + for (int j = 0; j < base; j++) { + count++; + } + base *= 2; + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + return count; + } + ``` + +=== "Python" + + ```python title="time_complexity.py" + """ 指数阶(循环实现)""" + def exponential(n): + count, base = 0, 1 + # cell 每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1) + for _ in range(n): + for _ in range(base): + count += 1 + base *= 2 + # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + return count + ``` + +=== "Go" + + ```go title="time_complexity.go" + /* 指数阶(循环实现)*/ + func exponential(n int) int { + count, base := 0, 1 + // cell 每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1) + for i := 0; i < n; i++ { + for j := 0; j < base; j++ { + count++ + } + base *= 2 + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + return count + } + ``` + +=== "JavaScript" + + ```js title="time_complexity.js" + /* 指数阶(循环实现) */ + function exponential(n) { + let count = 0, + base = 1; + // cell 每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1) + for (let i = 0; i < n; i++) { + for (let j = 0; j < base; j++) { + count++; + } + base *= 2; + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + return count; + } + + ``` + +=== "TypeScript" + + ```typescript title="time_complexity.ts" + /* 指数阶(循环实现) */ + function exponential(n: number): number { + let count = 0, + base = 1; + // cell 每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1) + for (let i = 0; i < n; i++) { + for (let j = 0; j < base; j++) { + count++; + } + base *= 2; + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + return count; + } + ``` + +=== "C" + + ```c title="time_complexity.c" + /* 指数阶(循环实现) */ + int exponential(int n) { + int count = 0; + int bas = 1; + // cell 每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1) + for (int i = 0; i < n; i++) { + for (int j = 0; j < bas; j++) { + count++; + } + bas *= 2; + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + return count; + } + ``` + +=== "C#" + + ```csharp title="time_complexity.cs" + /* 指数阶(循环实现) */ + int exponential(int n) + { + int count = 0, bas = 1; + // cell 每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1) + for (int i = 0; i < n; i++) + { + for (int j = 0; j < bas; j++) + { + count++; + } + bas *= 2; + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + return count; + } + ``` + +=== "Swift" + + ```swift title="time_complexity.swift" + /* 指数阶(循环实现) */ + func exponential(n: Int) -> Int { + var count = 0 + var base = 1 + // cell 每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1) + for _ in 0 ..< n { + for _ in 0 ..< base { + count += 1 + } + base *= 2 + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + return count + } + ``` + +=== "Zig" + + ```zig title="time_complexity.zig" + // 指数阶(循环实现) + fn exponential(n: i32) i32{ + var count: i32 = 0; + var bas: i32 = 1; + var i: i32 = 0; + // cell 每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1) + while (i < n) : (i += 1) { + var j: i32 = 0; + while (j < bas) : (j += 1) { + count += 1; + } + bas *= 2; + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + return count; + } + ``` + +![time_complexity_exponential](time_complexity.assets/time_complexity_exponential.png) + +

Fig. 指数阶的时间复杂度

+ +在实际算法中,指数阶常出现于递归函数。例如以下代码,不断地一分为二,分裂 $n$ 次后停止。 + +=== "Java" + + ```java title="time_complexity.java" + /* 指数阶(递归实现) */ + int expRecur(int n) { + if (n == 1) return 1; + return expRecur(n - 1) + expRecur(n - 1) + 1; + } + ``` + +=== "C++" + + ```cpp title="time_complexity.cpp" + /* 指数阶(递归实现) */ + int expRecur(int n) { + if (n == 1) return 1; + return expRecur(n - 1) + expRecur(n - 1) + 1; + } + ``` + +=== "Python" + + ```python title="time_complexity.py" + """ 指数阶(递归实现)""" + def exp_recur(n): + if n == 1: return 1 + return exp_recur(n - 1) + exp_recur(n - 1) + 1 + ``` + +=== "Go" + + ```go title="time_complexity.go" + /* 指数阶(递归实现)*/ + func expRecur(n int) int { + if n == 1 { + return 1 + } + return expRecur(n-1) + expRecur(n-1) + 1 + } + ``` + +=== "JavaScript" + + ```js title="time_complexity.js" + /* 指数阶(递归实现) */ + function expRecur(n) { + if (n == 1) return 1; + return expRecur(n - 1) + expRecur(n - 1) + 1; + } + ``` + +=== "TypeScript" + + ```typescript title="time_complexity.ts" + /* 指数阶(递归实现) */ + function expRecur(n: number): number { + if (n == 1) return 1; + return expRecur(n - 1) + expRecur(n - 1) + 1; + } + + ``` + +=== "C" + + ```c title="time_complexity.c" + /* 指数阶(递归实现) */ + int expRecur(int n) { + if (n == 1) return 1; + return expRecur(n - 1) + expRecur(n - 1) + 1; + } + ``` + +=== "C#" + + ```csharp title="time_complexity.cs" + /* 指数阶(递归实现) */ + int expRecur(int n) + { + if (n == 1) return 1; + return expRecur(n - 1) + expRecur(n - 1) + 1; + } + ``` + +=== "Swift" + + ```swift title="time_complexity.swift" + /* 指数阶(递归实现) */ + func expRecur(n: Int) -> Int { + if n == 1 { + return 1 + } + return expRecur(n: n - 1) + expRecur(n: n - 1) + 1 + } + ``` + +=== "Zig" + + ```zig title="time_complexity.zig" + // 指数阶(递归实现) + fn expRecur(n: i32) i32{ + if (n == 1) return 1; + return expRecur(n - 1) + expRecur(n - 1) + 1; + } + ``` + +### 对数阶 $O(\log n)$ + +对数阶与指数阶正好相反,后者反映“每轮增加到两倍的情况”,而前者反映“每轮缩减到一半的情况”。对数阶仅次于常数阶,时间增长得很慢,是理想的时间复杂度。 + +对数阶常出现于「二分查找」和「分治算法」中,体现“一分为多”、“化繁为简”的算法思想。 + +设输入数据大小为 $n$ ,由于每轮缩减到一半,因此循环次数是 $\log_2 n$ ,即 $2^n$ 的反函数。 + +=== "Java" + + ```java title="time_complexity.java" + /* 对数阶(循环实现) */ + int logarithmic(float n) { + int count = 0; + while (n > 1) { + n = n / 2; + count++; + } + return count; + } + ``` + +=== "C++" + + ```cpp title="time_complexity.cpp" + /* 对数阶(循环实现) */ + int logarithmic(float n) { + int count = 0; + while (n > 1) { + n = n / 2; + count++; + } + return count; + } + ``` + +=== "Python" + + ```python title="time_complexity.py" + """ 对数阶(循环实现)""" + def logarithmic(n): + count = 0 + while n > 1: + n = n / 2 + count += 1 + return count + ``` + +=== "Go" + + ```go title="time_complexity.go" + /* 对数阶(循环实现)*/ + func logarithmic(n float64) int { + count := 0 + for n > 1 { + n = n / 2 + count++ + } + return count + } + ``` + +=== "JavaScript" + + ```js title="time_complexity.js" + /* 对数阶(循环实现) */ + function logarithmic(n) { + let count = 0; + while (n > 1) { + n = n / 2; + count++; + } + return count; + } + ``` + +=== "TypeScript" + + ```typescript title="time_complexity.ts" + /* 对数阶(循环实现) */ + function logarithmic(n: number): number { + let count = 0; + while (n > 1) { + n = n / 2; + count++; + } + return count; + } + ``` + +=== "C" + + ```c title="time_complexity.c" + /* 对数阶(循环实现) */ + int logarithmic(float n) { + int count = 0; + while (n > 1) { + n = n / 2; + count++; + } + return count; + } + ``` + +=== "C#" + + ```csharp title="time_complexity.cs" + /* 对数阶(循环实现) */ + int logarithmic(float n) + { + int count = 0; + while (n > 1) + { + n = n / 2; + count++; + } + return count; + } + ``` + +=== "Swift" + + ```swift title="time_complexity.swift" + /* 对数阶(循环实现) */ + func logarithmic(n: Int) -> Int { + var count = 0 + var n = n + while n > 1 { + n = n / 2 + count += 1 + } + return count + } + ``` + +=== "Zig" + + ```zig title="time_complexity.zig" + // 对数阶(循环实现) + fn logarithmic(n: f32) i32 + { + var count: i32 = 0; + var n_var = n; + while (n_var > 1) + { + n_var = n_var / 2; + count +=1; + } + return count; + } + ``` + +![time_complexity_logarithmic](time_complexity.assets/time_complexity_logarithmic.png) + +

Fig. 对数阶的时间复杂度

+ +与指数阶类似,对数阶也常出现于递归函数。以下代码形成了一个高度为 $\log_2 n$ 的递归树。 + +=== "Java" + + ```java title="time_complexity.java" + /* 对数阶(递归实现) */ + int logRecur(float n) { + if (n <= 1) return 0; + return logRecur(n / 2) + 1; + } + ``` + +=== "C++" + + ```cpp title="time_complexity.cpp" + /* 对数阶(递归实现) */ + int logRecur(float n) { + if (n <= 1) return 0; + return logRecur(n / 2) + 1; + } + ``` + +=== "Python" + + ```python title="time_complexity.py" + """ 对数阶(递归实现)""" + def log_recur(n): + if n <= 1: return 0 + return log_recur(n / 2) + 1 + ``` + +=== "Go" + + ```go title="time_complexity.go" + /* 对数阶(递归实现)*/ + func logRecur(n float64) int { + if n <= 1 { + return 0 + } + return logRecur(n/2) + 1 + } + ``` + +=== "JavaScript" + + ```js title="time_complexity.js" + /* 对数阶(递归实现) */ + function logRecur(n) { + if (n <= 1) return 0; + return logRecur(n / 2) + 1; + } + ``` + +=== "TypeScript" + + ```typescript title="time_complexity.ts" + /* 对数阶(递归实现) */ + function logRecur(n: number): number { + if (n <= 1) return 0; + return logRecur(n / 2) + 1; + } + ``` + +=== "C" + + ```c title="time_complexity.c" + /* 对数阶(递归实现) */ + int logRecur(float n) { + if (n <= 1) return 0; + return logRecur(n / 2) + 1; + } + ``` + +=== "C#" + + ```csharp title="time_complexity.cs" + /* 对数阶(递归实现) */ + int logRecur(float n) + { + if (n <= 1) return 0; + return logRecur(n / 2) + 1; + } + ``` + +=== "Swift" + + ```swift title="time_complexity.swift" + /* 对数阶(递归实现) */ + func logRecur(n: Int) -> Int { + if n <= 1 { + return 0 + } + return logRecur(n: n / 2) + 1 + } + ``` + +=== "Zig" + + ```zig title="time_complexity.zig" + // 对数阶(递归实现) + fn logRecur(n: f32) i32 + { + if (n <= 1) return 0; + return logRecur(n / 2) + 1; + } + ``` + +### 线性对数阶 $O(n \log n)$ + +线性对数阶常出现于嵌套循环中,两层循环的时间复杂度分别为 $O(\log n)$ 和 $O(n)$ 。 + +主流排序算法的时间复杂度都是 $O(n \log n )$ ,例如快速排序、归并排序、堆排序等。 + +=== "Java" + + ```java title="time_complexity.java" + /* 线性对数阶 */ + int linearLogRecur(float n) { + if (n <= 1) return 1; + int count = linearLogRecur(n / 2) + + linearLogRecur(n / 2); + for (int i = 0; i < n; i++) { + count++; + } + return count; + } + ``` + +=== "C++" + + ```cpp title="time_complexity.cpp" + /* 线性对数阶 */ + int linearLogRecur(float n) { + if (n <= 1) return 1; + int count = linearLogRecur(n / 2) + + linearLogRecur(n / 2); + for (int i = 0; i < n; i++) { + count++; + } + return count; + } + ``` + +=== "Python" + + ```python title="time_complexity.py" + """ 线性对数阶 """ + def linear_log_recur(n): + if n <= 1: return 1 + count = linear_log_recur(n // 2) + \ + linear_log_recur(n // 2) + for _ in range(n): + count += 1 + return count + ``` + +=== "Go" + + ```go title="time_complexity.go" + /* 线性对数阶 */ + func linearLogRecur(n float64) int { + if n <= 1 { + return 1 + } + count := linearLogRecur(n/2) + + linearLogRecur(n/2) + for i := 0.0; i < n; i++ { + count++ + } + return count + } + ``` + +=== "JavaScript" + + ```js title="time_complexity.js" + /* 线性对数阶 */ + function linearLogRecur(n) { + if (n <= 1) return 1; + let count = linearLogRecur(n / 2) + linearLogRecur(n / 2); + for (let i = 0; i < n; i++) { + count++; + } + return count; + } + ``` + +=== "TypeScript" + + ```typescript title="time_complexity.ts" + /* 线性对数阶 */ + function linearLogRecur(n: number): number { + if (n <= 1) return 1; + let count = linearLogRecur(n / 2) + linearLogRecur(n / 2); + for (let i = 0; i < n; i++) { + count++; + } + return count; + } + ``` + +=== "C" + + ```c title="time_complexity.c" + /* 线性对数阶 */ + int linearLogRecur(float n) { + if (n <= 1) return 1; + int count = linearLogRecur(n / 2) + + linearLogRecur(n / 2); + for (int i = 0; i < n; i++) { + count ++; + } + return count; + } + ``` + +=== "C#" + + ```csharp title="time_complexity.cs" + /* 线性对数阶 */ + int linearLogRecur(float n) + { + if (n <= 1) return 1; + int count = linearLogRecur(n / 2) + + linearLogRecur(n / 2); + for (int i = 0; i < n; i++) + { + count++; + } + return count; + } + ``` + +=== "Swift" + + ```swift title="time_complexity.swift" + /* 线性对数阶 */ + func linearLogRecur(n: Double) -> Int { + if n <= 1 { + return 1 + } + var count = linearLogRecur(n: n / 2) + linearLogRecur(n: n / 2) + for _ in 0 ..< Int(n) { + count += 1 + } + return count + } + ``` + +=== "Zig" + + ```zig title="time_complexity.zig" + // 线性对数阶 + fn linearLogRecur(n: f32) i32 + { + if (n <= 1) return 1; + var count: i32 = linearLogRecur(n / 2) + + linearLogRecur(n / 2); + var i: f32 = 0; + while (i < n) : (i += 1) { + count += 1; + } + return count; + } + ``` + +![time_complexity_logarithmic_linear](time_complexity.assets/time_complexity_logarithmic_linear.png) + +

Fig. 线性对数阶的时间复杂度

+ +### 阶乘阶 $O(n!)$ + +阶乘阶对应数学上的「全排列」。即给定 $n$ 个互不重复的元素,求其所有可能的排列方案,则方案数量为 + +$$ +n! = n \times (n - 1) \times (n - 2) \times \cdots \times 2 \times 1 +$$ + +阶乘常使用递归实现。例如以下代码,第一层分裂出 $n$ 个,第二层分裂出 $n - 1$ 个,…… ,直至到第 $n$ 层时终止分裂。 + +=== "Java" + + ```java title="time_complexity.java" + /* 阶乘阶(递归实现) */ + int factorialRecur(int n) { + if (n == 0) return 1; + int count = 0; + // 从 1 个分裂出 n 个 + for (int i = 0; i < n; i++) { + count += factorialRecur(n - 1); + } + return count; + } + ``` + +=== "C++" + + ```cpp title="time_complexity.cpp" + /* 阶乘阶(递归实现) */ + int factorialRecur(int n) { + if (n == 0) return 1; + int count = 0; + // 从 1 个分裂出 n 个 + for (int i = 0; i < n; i++) { + count += factorialRecur(n - 1); + } + return count; + } + ``` + +=== "Python" + + ```python title="time_complexity.py" + """ 阶乘阶(递归实现)""" + def factorial_recur(n): + if n == 0: return 1 + count = 0 + # 从 1 个分裂出 n 个 + for _ in range(n): + count += factorial_recur(n - 1) + return count + ``` + +=== "Go" + + ```go title="time_complexity.go" + /* 阶乘阶(递归实现) */ + func factorialRecur(n int) int { + if n == 0 { + return 1 + } + count := 0 + // 从 1 个分裂出 n 个 + for i := 0; i < n; i++ { + count += factorialRecur(n - 1) + } + return count + } + ``` + +=== "JavaScript" + + ```js title="time_complexity.js" + /* 阶乘阶(递归实现) */ + function factorialRecur(n) { + if (n == 0) return 1; + let count = 0; + // 从 1 个分裂出 n 个 + for (let i = 0; i < n; i++) { + count += factorialRecur(n - 1); + } + return count; + } + ``` + +=== "TypeScript" + + ```typescript title="time_complexity.ts" + /* 阶乘阶(递归实现) */ + function factorialRecur(n: number): number { + if (n == 0) return 1; + let count = 0; + // 从 1 个分裂出 n 个 + for (let i = 0; i < n; i++) { + count += factorialRecur(n - 1); + } + return count; + } + ``` + +=== "C" + + ```c title="time_complexity.c" + /* 阶乘阶(递归实现) */ + int factorialRecur(int n) { + if (n == 0) return 1; + int count = 0; + for (int i = 0; i < n; i++) { + count += factorialRecur(n - 1); + } + return count; + } + ``` + +=== "C#" + + ```csharp title="time_complexity.cs" + /* 阶乘阶(递归实现) */ + int factorialRecur(int n) + { + if (n == 0) return 1; + int count = 0; + // 从 1 个分裂出 n 个 + for (int i = 0; i < n; i++) + { + count += factorialRecur(n - 1); + } + return count; + } + ``` + +=== "Swift" + + ```swift title="time_complexity.swift" + /* 阶乘阶(递归实现) */ + func factorialRecur(n: Int) -> Int { + if n == 0 { + return 1 + } + var count = 0 + // 从 1 个分裂出 n 个 + for _ in 0 ..< n { + count += factorialRecur(n: n - 1) + } + return count + } + ``` + +=== "Zig" + + ```zig title="time_complexity.zig" + // 阶乘阶(递归实现) + fn factorialRecur(n: i32) i32 { + if (n == 0) return 1; + var count: i32 = 0; + var i: i32 = 0; + // 从 1 个分裂出 n 个 + while (i < n) : (i += 1) { + count += factorialRecur(n - 1); + } + return count; + } + ``` + +![time_complexity_factorial](time_complexity.assets/time_complexity_factorial.png) + +

Fig. 阶乘阶的时间复杂度

+ +## 2.2.6. 最差、最佳、平均时间复杂度 + +**某些算法的时间复杂度不是恒定的,而是与输入数据的分布有关**。举一个例子,输入一个长度为 $n$ 数组 `nums` ,其中 `nums` 由从 $1$ 至 $n$ 的数字组成,但元素顺序是随机打乱的;算法的任务是返回元素 $1$ 的索引。我们可以得出以下结论: + +- 当 `nums = [?, ?, ..., 1]`,即当末尾元素是 $1$ 时,则需完整遍历数组,此时达到 **最差时间复杂度 $O(n)$** ; +- 当 `nums = [1, ?, ?, ...]` ,即当首个数字为 $1$ 时,无论数组多长都不需要继续遍历,此时达到 **最佳时间复杂度 $\Omega(1)$** ; + +「函数渐近上界」使用大 $O$ 记号表示,代表「最差时间复杂度」。与之对应,「函数渐近下界」用 $\Omega$ 记号(Omega Notation)来表示,代表「最佳时间复杂度」。 + +=== "Java" + + ```java title="worst_best_time_complexity.java" + public class worst_best_time_complexity { + /* 生成一个数组,元素为 { 1, 2, ..., n },顺序被打乱 */ + int[] randomNumbers(int n) { + Integer[] nums = new Integer[n]; + // 生成数组 nums = { 1, 2, 3, ..., n } + for (int i = 0; i < n; i++) { + nums[i] = i + 1; + } + // 随机打乱数组元素 + Collections.shuffle(Arrays.asList(nums)); + // Integer[] -> int[] + int[] res = new int[n]; + for (int i = 0; i < n; i++) { + res[i] = nums[i]; + } + return res; + } + + /* 查找数组 nums 中数字 1 所在索引 */ + int findOne(int[] nums) { + for (int i = 0; i < nums.length; i++) { + // 当元素 1 在数组头部时,达到最佳时间复杂度 O(1) + // 当元素 1 在数组尾部时,达到最差时间复杂度 O(n) + if (nums[i] == 1) + return i; + } + return -1; + } + } + ``` + +=== "C++" + + ```cpp title="worst_best_time_complexity.cpp" + /* 生成一个数组,元素为 { 1, 2, ..., n },顺序被打乱 */ + vector randomNumbers(int n) { + vector nums(n); + // 生成数组 nums = { 1, 2, 3, ..., n } + for (int i = 0; i < n; i++) { + nums[i] = i + 1; + } + // 使用系统时间生成随机种子 + unsigned seed = chrono::system_clock::now().time_since_epoch().count(); + // 随机打乱数组元素 + shuffle(nums.begin(), nums.end(), default_random_engine(seed)); + return nums; + } + + /* 查找数组 nums 中数字 1 所在索引 */ + int findOne(vector& nums) { + for (int i = 0; i < nums.size(); i++) { + // 当元素 1 在数组头部时,达到最佳时间复杂度 O(1) + // 当元素 1 在数组尾部时,达到最差时间复杂度 O(n) + if (nums[i] == 1) + return i; + } + return -1; + } + ``` + +=== "Python" + + ```python title="worst_best_time_complexity.py" + """ 生成一个数组,元素为: 1, 2, ..., n ,顺序被打乱 """ + def random_numbers(n): + # 生成数组 nums =: 1, 2, 3, ..., n + nums = [i for i in range(1, n + 1)] + # 随机打乱数组元素 + random.shuffle(nums) + return nums + + """ 查找数组 nums 中数字 1 所在索引 """ + def find_one(nums): + for i in range(len(nums)): + # 当元素 1 在数组头部时,达到最佳时间复杂度 O(1) + # 当元素 1 在数组尾部时,达到最差时间复杂度 O(n) + if nums[i] == 1: + return i + return -1 + ``` + +=== "Go" + + ```go title="worst_best_time_complexity.go" + /* 生成一个数组,元素为 { 1, 2, ..., n },顺序被打乱 */ + func randomNumbers(n int) []int { + nums := make([]int, n) + // 生成数组 nums = { 1, 2, 3, ..., n } + for i := 0; i < n; i++ { + nums[i] = i + 1 + } + // 随机打乱数组元素 + rand.Shuffle(len(nums), func(i, j int) { + nums[i], nums[j] = nums[j], nums[i] + }) + return nums + } + + /* 查找数组 nums 中数字 1 所在索引 */ + func findOne(nums []int) int { + for i := 0; i < len(nums); i++ { + // 当元素 1 在数组头部时,达到最佳时间复杂度 O(1) + // 当元素 1 在数组尾部时,达到最差时间复杂度 O(n) + if nums[i] == 1 { + return i + } + } + return -1 + } + ``` + +=== "JavaScript" + + ```js title="worst_best_time_complexity.js" + /* 生成一个数组,元素为 { 1, 2, ..., n },顺序被打乱 */ + function randomNumbers(n) { + let nums = Array(n); + // 生成数组 nums = { 1, 2, 3, ..., n } + for (let i = 0; i < n; i++) { + nums[i] = i + 1; + } + // 随机打乱数组元素 + for (let i = 0; i < n; i++) { + const r = Math.floor(Math.random() * (i + 1)); + const temp = nums[i]; + nums[i] = nums[r]; + nums[r] = temp; + } + return nums; + } + + /* 查找数组 nums 中数字 1 所在索引 */ + function findOne(nums) { + for (let i = 0; i < nums.length; i++) { + // 当元素 1 在数组头部时,达到最佳时间复杂度 O(1) + // 当元素 1 在数组尾部时,达到最差时间复杂度 O(n) + if (nums[i] === 1) { + return i; + } + } + return -1; + } + ``` + +=== "TypeScript" + + ```typescript title="worst_best_time_complexity.ts" + /* 生成一个数组,元素为 { 1, 2, ..., n },顺序被打乱 */ + function randomNumbers(n: number): number[] { + let nums = Array(n); + // 生成数组 nums = { 1, 2, 3, ..., n } + for (let i = 0; i < n; i++) { + nums[i] = i + 1; + } + // 随机打乱数组元素 + for (let i = 0; i < n; i++) { + let r = Math.floor(Math.random() * (i + 1)); + let temp = nums[i]; + nums[i] = nums[r]; + nums[r] = temp; + } + return nums; + } + + /* 查找数组 nums 中数字 1 所在索引 */ + function findOne(nums: number[]): number { + for (let i = 0; i < nums.length; i++) { + // 当元素 1 在数组头部时,达到最佳时间复杂度 O(1) + // 当元素 1 在数组尾部时,达到最差时间复杂度 O(n) + if (nums[i] === 1) { + return i; + } + } + return -1; + } + ``` + +=== "C" + + ```c title="worst_best_time_complexity.c" + /* 生成一个数组,元素为 { 1, 2, ..., n },顺序被打乱 */ + int *randomNumbers(int n) { + // 分配堆区内存(创建一维可变长数组:数组中元素数量为n,元素类型为int) + int *nums = (int *)malloc(n * sizeof(int)); + // 生成数组 nums = { 1, 2, 3, ..., n } + for (int i = 0; i < n; i++) { + nums[i] = i + 1; + } + // 随机打乱数组元素 + for (int i = n - 1; i > 0; i--) { + int j = rand() % (i + 1); + int temp = nums[i]; + nums[i] = nums[j]; + nums[j] = temp; + } + return nums; + } + + /* 查找数组 nums 中数字 1 所在索引 */ + int findOne(int *nums, int n) { + for (int i = 0; i < n; i++) { + // 当元素 1 在数组头部时,达到最佳时间复杂度 O(1) + // 当元素 1 在数组尾部时,达到最差时间复杂度 O(n) + if (nums[i] == 1) return i; + } + return -1; + } + ``` + +=== "C#" + + ```csharp title="worst_best_time_complexity.cs" + /* 生成一个数组,元素为 { 1, 2, ..., n },顺序被打乱 */ + int[] randomNumbers(int n) + { + int[] nums = new int[n]; + // 生成数组 nums = { 1, 2, 3, ..., n } + for (int i = 0; i < n; i++) + { + nums[i] = i + 1; + } + + // 随机打乱数组元素 + for (int i = 0; i < nums.Length; i++) + { + var index = new Random().Next(i, nums.Length); + var tmp = nums[i]; + var ran = nums[index]; + nums[i] = ran; + nums[index] = tmp; + } + return nums; + } + + /* 查找数组 nums 中数字 1 所在索引 */ + int findOne(int[] nums) + { + for (int i = 0; i < nums.Length; i++) + { + // 当元素 1 在数组头部时,达到最佳时间复杂度 O(1) + // 当元素 1 在数组尾部时,达到最差时间复杂度 O(n) + if (nums[i] == 1) + return i; + } + return -1; + } + ``` + +=== "Swift" + + ```swift title="worst_best_time_complexity.swift" + /* 生成一个数组,元素为 { 1, 2, ..., n },顺序被打乱 */ + func randomNumbers(n: Int) -> [Int] { + // 生成数组 nums = { 1, 2, 3, ..., n } + var nums = Array(1 ... n) + // 随机打乱数组元素 + nums.shuffle() + return nums + } + + /* 查找数组 nums 中数字 1 所在索引 */ + func findOne(nums: [Int]) -> Int { + for i in nums.indices { + // 当元素 1 在数组头部时,达到最佳时间复杂度 O(1) + // 当元素 1 在数组尾部时,达到最差时间复杂度 O(n) + if nums[i] == 1 { + return i + } + } + return -1 + } + ``` + +=== "Zig" + + ```zig title="worst_best_time_complexity.zig" + // 生成一个数组,元素为 { 1, 2, ..., n },顺序被打乱 + pub fn randomNumbers(comptime n: usize) [n]i32 { + var nums: [n]i32 = undefined; + // 生成数组 nums = { 1, 2, 3, ..., n } + for (nums) |*num, i| { + num.* = @intCast(i32, i) + 1; + } + // 随机打乱数组元素 + const rand = std.crypto.random; + rand.shuffle(i32, &nums); + return nums; + } + + // 查找数组 nums 中数字 1 所在索引 + pub fn findOne(nums: []i32) i32 { + for (nums) |num, i| { + // 当元素 1 在数组头部时,达到最佳时间复杂度 O(1) + // 当元素 1 在数组尾部时,达到最差时间复杂度 O(n) + if (num == 1) return @intCast(i32, i); + } + return -1; + } + ``` + +!!! tip + + 我们在实际应用中很少使用「最佳时间复杂度」,因为往往只有很小概率下才能达到,会带来一定的误导性。反之,「最差时间复杂度」最为实用,因为它给出了一个“效率安全值”,让我们可以放心地使用算法。 + +从上述示例可以看出,最差或最佳时间复杂度只出现在“特殊分布的数据”中,这些情况的出现概率往往很小,因此并不能最真实地反映算法运行效率。**相对地,「平均时间复杂度」可以体现算法在随机输入数据下的运行效率,用 $\Theta$ 记号(Theta Notation)来表示**。 + +对于部分算法,我们可以简单地推算出随机数据分布下的平均情况。比如上述示例,由于输入数组是被打乱的,因此元素 $1$ 出现在任意索引的概率都是相等的,那么算法的平均循环次数则是数组长度的一半 $\frac{n}{2}$ ,平均时间复杂度为 $\Theta(\frac{n}{2}) = \Theta(n)$ 。 + +但在实际应用中,尤其是较为复杂的算法,计算平均时间复杂度比较困难,因为很难简便地分析出在数据分布下的整体数学期望。这种情况下,我们一般使用最差时间复杂度来作为算法效率的评判标准。 + +!!! question "为什么很少看到 $\Theta$ 符号?" + + 实际中我们经常使用「大 $O$ 符号」来表示「平均复杂度」,这样严格意义上来说是不规范的。这可能是因为 $O$ 符号实在是太朗朗上口了。
如果在本书和其他资料中看到类似 **平均时间复杂度 $O(n)$** 的表述,请你直接理解为 $\Theta(n)$ 即可。 diff --git a/build/chapter_data_structure/classification_of_data_structure.md b/build/chapter_data_structure/classification_of_data_structure.md new file mode 100644 index 000000000..1b8d7b739 --- /dev/null +++ b/build/chapter_data_structure/classification_of_data_structure.md @@ -0,0 +1,43 @@ +--- +comments: true +--- + +# 3.2. 数据结构分类 + +数据结构主要可根据「逻辑结构」和「物理结构」两种角度进行分类。 + +## 3.2.1. 逻辑结构:线性与非线性 + +**「逻辑结构」反映了数据之间的逻辑关系**。数组和链表的数据按照顺序依次排列,反映了数据间的线性关系;树从顶至底按层级排列,反映了祖先与后代之间的派生关系;图由结点和边组成,反映了复杂网络关系。 + +我们一般将逻辑结构分为「线性」和「非线性」两种。“线性”这个概念很直观,即表明数据在逻辑关系上是排成一条线的;而如果数据之间的逻辑关系是非线性的(例如是网状或树状的),那么就是非线性数据结构。 + +- **线性数据结构**:数组、链表、栈、队列、哈希表; +- **非线性数据结构**:树、图、堆、哈希表; + +![classification_logic_structure](classification_of_data_structure.assets/classification_logic_structure.png) + +

Fig. 线性与非线性数据结构

+ +## 3.2.2. 物理结构:连续与离散 + +!!! note + + 若感到阅读困难,建议先看完下个章节「数组与链表」,再回过头来理解物理结构的含义。 + +**「物理结构」反映了数据在计算机内存中的存储方式**。从本质上看,分别是 **数组的连续空间存储** 和 **链表的离散空间存储**。物理结构从底层上决定了数据的访问、更新、增删等操作方法,在时间效率和空间效率方面呈现出此消彼长的特性。 + +![classification_phisical_structure](classification_of_data_structure.assets/classification_phisical_structure.png) + +

Fig. 连续空间存储与离散空间存储

+ +**所有数据结构都是基于数组、或链表、或两者组合实现的**。例如栈和队列,既可以使用数组实现、也可以使用链表实现,而例如哈希表,其实现同时包含了数组和链表。 + +- **基于数组可实现**:栈、队列、哈希表、树、堆、图、矩阵、张量(维度 $\geq 3$ 的数组)等; +- **基于链表可实现**:栈、队列、哈希表、树、堆、图等; + +基于数组实现的数据结构也被称为「静态数据结构」,这意味着该数据结构在在被初始化后,长度不可变。相反地,基于链表实现的数据结构被称为「动态数据结构」,该数据结构在被初始化后,我们也可以在程序运行中修改其长度。 + +!!! tip + + 数组与链表是其他所有数据结构的“底层积木”,建议读者一定要多花些时间了解。 diff --git a/build/chapter_data_structure/data_and_memory.md b/build/chapter_data_structure/data_and_memory.md new file mode 100644 index 000000000..c10980616 --- /dev/null +++ b/build/chapter_data_structure/data_and_memory.md @@ -0,0 +1,149 @@ +--- +comments: true +--- + +# 3.1. 数据与内存 + +## 3.1.1. 基本数据类型 + +谈到计算机中的数据,我们能够想到文本、图片、视频、语音、3D 模型等等,这些数据虽然组织形式不同,但是有一个共同点,即都是由各种基本数据类型构成的。 + +**「基本数据类型」是 CPU 可以直接进行运算的类型,在算法中直接被使用。** + +- 「整数」根据不同的长度分为 byte, short, int, long ,根据算法需求选用,即在满足取值范围的情况下尽量减小内存空间占用; +- 「浮点数」代表小数,根据长度分为 float, double ,同样根据算法的实际需求选用; +- 「字符」在计算机中是以字符集的形式保存的,char 的值实际上是数字,代表字符集中的编号,计算机通过字符集查表来完成编号到字符的转换。占用空间与具体编程语言有关,通常为 2 bytes 或 1 byte ; +- 「布尔」代表逻辑中的 “是” 与 “否” ,其占用空间需要具体根据编程语言确定,通常为 1 byte 或 1 bit ; + +!!! note "字节与比特" + + 1 字节 (byte) = 8 比特 (bit) , 1 比特即最基本的 1 个二进制位 + +

Table. Java 的基本数据类型

+ +
+ +| 类别 | 符号 | 占用空间 | 取值范围 | 默认值 | +| ------ | ----------- | ----------------- | ---------------------------------------------- | -------------- | +| 整数 | byte | 1 byte | $-2^7$ ~ $2^7 - 1$ ( $-128$ ~ $127$ ) | $0$ | +| | short | 2 bytes | $-2^{15}$ ~ $2^{15} - 1$ | $0$ | +| | **int** | 4 bytes | $-2^{31}$ ~ $2^{31} - 1$ | $0$ | +| | long | 8 bytes | $-2^{63}$ ~ $2^{63} - 1$ | $0$ | +| 浮点数 | **float** | 4 bytes | $-3.4 \times 10^{38}$ ~ $3.4 \times 10^{38}$ | $0.0$ f | +| | double | 8 bytes | $-1.7 \times 10^{308}$ ~ $1.7 \times 10^{308}$ | $0.0$ | +| 字符 | **char** | 2 bytes / 1 byte | $0$ ~ $2^{16} - 1$ | $0$ | +| 布尔 | **boolean(bool)** | 1 byte / 1 bit | $\text{true}$ 或 $\text{false}$ | $\text{false}$ | + +
+ +!!! tip + + 以上表格中,加粗项在「算法题」中最为常用。此表格无需硬背,大致理解即可,需要时可以通过查表来回忆。 + +**「基本数据类型」与「数据结构」之间的联系与区别** + +我们知道,数据结构是在计算机中 **组织与存储数据的方式**,它的主语是“结构”,而不是“数据”。比如,我们想要表示“一排数字”,自然应该使用「数组」这个数据结构。数组的存储方式使之可以表示数字的相邻关系、先后关系等一系列我们需要的信息,但至于其中存储的是整数 int ,还是小数 float ,或是字符 char ,**则与所谓的数据的结构无关了**。 + +=== "Java" + + ```java title="" + /* 使用多种「基本数据类型」来初始化「数组」 */ + int[] numbers = new int[5]; + float[] decimals = new float[5]; + char[] characters = new char[5]; + boolean[] booleans = new boolean[5]; + ``` + +=== "C++" + + ```cpp title="" + /* 使用多种「基本数据类型」来初始化「数组」 */ + int numbers[5]; + float decimals[5]; + char characters[5]; + bool booleans[5]; + ``` + +=== "Python" + + ```python title="" + """ Python 的 list 可以自由存储各种基本数据类型和对象 """ + list = [0, 0.0, 'a', False] + ``` + +=== "Go" + + ```go title="" + // 使用多种「基本数据类型」来初始化「数组」 + var numbers = [5]int{} + var decimals = [5]float64{} + var characters = [5]byte{} + var booleans = [5]bool{} + ``` + +=== "JavaScript" + + ```js title="" + /* JavaScript 的数组可以自由存储各种基本数据类型和对象 */ + const array = [0, 0.0, 'a', false]; + ``` + +=== "TypeScript" + + ```typescript title="" + /* 使用多种「基本数据类型」来初始化「数组」 */ + const numbers: number[] = []; + const characters: string[] = []; + const booleans: boolean[] = []; + ``` + +=== "C" + + ```c title="" + /* 使用多种「基本数据类型」来初始化「数组」 */ + int numbers[10]; + float decimals[10]; + char characters[10]; + bool booleans[10]; + + ``` + +=== "C#" + + ```csharp title="" + /* 使用多种「基本数据类型」来初始化「数组」 */ + int[] numbers = new int[5]; + float[] decimals = new float[5]; + char[] characters = new char[5]; + bool[] booleans = new bool[5]; + ``` + +=== "Swift" + + ```swift title="" + /* 使用多种「基本数据类型」来初始化「数组」 */ + let numbers = Array(repeating: Int(), count: 5) + let decimals = Array(repeating: Double(), count: 5) + let characters = Array(repeating: Character("a"), count: 5) + let booleans = Array(repeating: Bool(), count: 5) + ``` + +=== "Zig" + + ```zig title="" + + ``` + +## 3.1.2. 计算机内存 + +在计算机中,内存和硬盘是两种主要的存储硬件设备。「硬盘」主要用于长期存储数据,容量较大(通常可达到 TB 级别)、速度较慢。「内存」用于运行程序时暂存数据,速度较快,但容量较小(通常为 GB 级别)。 + +**算法运行中,相关数据都被存储在内存中**。下图展示了一个计算机内存条,其中每个黑色方块都包含一块内存空间。我们可以将内存想象成一个巨大的 Excel 表格,其中每个单元格都可以存储 1 byte 的数据,在算法运行时,所有数据都被存储在这些单元格中。 + +**系统通过「内存地址 Memory Location」来访问目标内存位置的数据**。计算机根据特定规则给表格中每个单元格编号,保证每块内存空间都有独立的内存地址。自此,程序便通过这些地址,访问内存中的数据。 + +![computer_memory_location](data_and_memory.assets/computer_memory_location.png) + +

Fig. 内存条、内存空间、内存地址

+ +**内存资源是设计数据结构与算法的重要考虑因素**。内存是所有程序的公共资源,当内存被某程序占用时,不能被其它程序同时使用。我们需要根据剩余内存资源的情况来设计算法。例如,若剩余内存空间有限,则要求算法占用的峰值内存不能超过系统剩余内存;若运行的程序很多、缺少大块连续的内存空间,则要求选取的数据结构必须能够存储在离散的内存空间内。 diff --git a/build/chapter_data_structure/summary.md b/build/chapter_data_structure/summary.md new file mode 100644 index 000000000..5340746a3 --- /dev/null +++ b/build/chapter_data_structure/summary.md @@ -0,0 +1,11 @@ +--- +comments: true +--- + +# 3.3. 小结 + +- 整数 byte, short, int, long 、浮点数 float, double 、字符 char 、布尔 boolean 是计算机中的基本数据类型,占用空间的大小决定了它们的取值范围。 +- 在程序运行时,数据存储在计算机的内存中。内存中每块空间都有独立的内存地址,程序是通过内存地址来访问数据的。 +- 数据结构主要可以从逻辑结构和物理结构两个角度进行分类。逻辑结构反映了数据中元素之间的逻辑关系,物理结构反映了数据在计算机内存中的存储形式。 +- 常见的逻辑结构有线性、树状、网状等。我们一般根据逻辑结构将数据结构分为线性(数组、链表、栈、队列)和非线性(树、图、堆)两种。根据实现方式的不同,哈希表可能是线性或非线性。 +- 物理结构主要有两种,分别是连续空间存储(数组)和离散空间存储(链表),所有的数据结构都是由数组、或链表、或两者组合实现的。 diff --git a/build/chapter_graph/graph.md b/build/chapter_graph/graph.md new file mode 100644 index 000000000..5f8d861e5 --- /dev/null +++ b/build/chapter_graph/graph.md @@ -0,0 +1,87 @@ +--- +comments: true +--- + +# 9.1. 图 + +「图 Graph」是一种非线性数据结构,由「顶点 Vertex」和「边 Edge」组成。我们可将图 $G$ 抽象地表示为一组顶点 $V$ 和一组边 $E$ 的集合。例如,以下表示一个包含 5 个顶点和 7 条边的图 + +$$ +\begin{aligned} +V & = \{ 1, 2, 3, 4, 5 \} \newline +E & = \{ (1,2), (1,3), (1,5), (2,3), (2,4), (2,5), (4,5) \} \newline +G & = \{ V, E \} \newline +\end{aligned} +$$ + +![linkedlist_tree_graph](graph.assets/linkedlist_tree_graph.png) + +那么,图与其他数据结构的关系是什么?如果我们把「顶点」看作结点,把「边」看作连接各个结点的指针,则可将「图」看成一种从「链表」拓展而来的数据结构。**相比线性关系(链表)和分治关系(树),网络关系(图)的自由度更高,也从而更为复杂**。 + +## 9.1.1. 图常见类型 + +根据边是否有方向,分为「无向图 Undirected Graph」和「有向图 Directed Graph」。 + +- 在无向图中,边表示两结点之间“双向”的连接关系,例如微信或 QQ 中的“好友关系”; +- 在有向图中,边是有方向的,即 $A \rightarrow B$ 和 $A \leftarrow B$ 两个方向的边是相互独立的,例如微博或抖音上的“关注”与“被关注”关系; + +![directed_graph](graph.assets/directed_graph.png) + +根据所有顶点是否连通,分为「连通图 Connected Graph」和「非连通图 Disconnected Graph」。 + +- 对于连通图,从某个结点出发,可以到达其余任意结点; +- 对于非连通图,从某个结点出发,至少有一个结点无法到达; + +![connected_graph](graph.assets/connected_graph.png) + +我们可以给边添加“权重”变量,得到「有权图 Weighted Graph」。例如,在王者荣耀等游戏中,系统会根据共同游戏时间来计算玩家之间的“亲密度”,这种亲密度网络就可以使用有权图来表示。 + +![weighted_graph](graph.assets/weighted_graph.png) + +## 9.1.2. 图常用术语 + +- 「邻接 Adjacency」:当两顶点之间有边相连时,称此两顶点“邻接”。 +- 「路径 Path」:从顶点 A 到顶点 B 走过的边构成的序列,被称为从 A 到 B 的“路径”。 +- 「度 Degree」表示一个顶点具有多少条边。对于有向图,「入度 In-Degree」表示有多少条边指向该顶点,「出度 Out-Degree」表示有多少条边从该顶点指出。 + +## 9.1.3. 图的表示 + +图的常用表示方法有「邻接矩阵」和「邻接表」。以下使用「无向图」来举例。 + +### 邻接矩阵 + +设图的顶点数量为 $n$ ,「邻接矩阵 Adjacency Matrix」使用一个 $n \times n$ 大小的矩阵来表示图,每一行(列)代表一个顶点,矩阵元素代表边,使用 $1$ 或 $0$ 来表示两个顶点之间有边或无边。 + +![adjacency_matrix](graph.assets/adjacency_matrix.png) + +邻接矩阵具有以下性质: + +- 顶点不能与自身相连,因而邻接矩阵主对角线元素没有意义。 +- 「无向图」两个方向的边等价,此时邻接矩阵关于主对角线对称。 +- 将邻接矩阵的元素从 $1$ , $0$ 替换为权重,则能够表示「有权图」。 + +使用邻接矩阵表示图时,我们可以直接通过访问矩阵元素来获取边,因此增删查操作的效率很高,时间复杂度均为 $O(1)$ 。然而,矩阵的空间复杂度为 $O(n^2)$ ,内存占用较大。 + +### 邻接表 + +「邻接表 Adjacency List」使用 $n$ 个链表来表示图,链表结点表示顶点。第 $i$ 条链表对应顶点 $i$ ,其中存储了所有与该顶点相连的顶点。 + +![adjacency_list](graph.assets/adjacency_list.png) + +邻接表仅存储存在的边,而边的总数往往远小于 $n^2$ ,因此更加节省空间。但是,因为在邻接表中需要通过遍历链表来查找边,所以其时间效率不如邻接矩阵。 + +观察上图发现,**邻接表结构与哈希表「链地址法」非常相似,因此我们也可以用类似方法来优化效率**。比如,当链表较长时,可以把链表转化为「AVL 树」,从而将时间效率从 $O(n)$ 优化至 $O(\log n)$ ,还可以通过中序遍历获取有序序列;还可以将链表转化为 HashSet(即哈希表),将时间复杂度降低至 $O(1)$ ,。 + +## 9.1.4. 图常见应用 + +现实中的许多系统都可以使用图来建模,对应的待求解问题也可以被约化为图计算问题。 + +
+ +| | 顶点 | 边 | 图计算问题 | +| -------- | ---- | -------------------- | ------------ | +| 社交网络 | 用户 | 好友关系 | 潜在好友推荐 | +| 地铁线路 | 站点 | 站点间的连通性 | 最短路线推荐 | +| 太阳系 | 星体 | 星体间的万有引力作用 | 行星轨道计算 | + +
diff --git a/build/chapter_graph/graph_operations.md b/build/chapter_graph/graph_operations.md new file mode 100644 index 000000000..4b86d4d39 --- /dev/null +++ b/build/chapter_graph/graph_operations.md @@ -0,0 +1,662 @@ +--- +comments: true +--- + +# 9.2. 图基础操作 + +图的基础操作分为对「边」的操作和对「顶点」的操作,在「邻接矩阵」和「邻接表」这两种表示下的实现方式不同。 + +## 9.2.1. 基于邻接矩阵的实现 + +设图的顶点总数为 $n$ ,则有: + +- **添加或删除边**:直接在邻接矩阵中修改指定边的对应元素即可,使用 $O(1)$ 时间。而由于是无向图,因此需要同时更新两个方向的边。 +- **添加顶点**:在邻接矩阵的尾部添加一行一列,并全部填 $0$ 即可,使用 $O(n)$ 时间。 +- **删除顶点**:在邻接矩阵中删除一行一列。当删除首行首列时达到最差情况,需要将 $(n-1)^2$ 个元素“向左上移动”,从而使用 $O(n^2)$ 时间。 +- **初始化**:传入 $n$ 个顶点,初始化长度为 $n$ 的顶点列表 `vertices` ,使用 $O(n)$ 时间;初始化 $n \times n$ 大小的邻接矩阵 `adjMat` ,使用 $O(n^2)$ 时间。 + +=== "初始化邻接矩阵" + ![adjacency_matrix_initialization](graph_operations.assets/adjacency_matrix_initialization.png) + +=== "添加边" + ![adjacency_matrix_add_edge](graph_operations.assets/adjacency_matrix_add_edge.png) + +=== "删除边" + ![adjacency_matrix_remove_edge](graph_operations.assets/adjacency_matrix_remove_edge.png) + +=== "添加顶点" + ![adjacency_matrix_add_vertex](graph_operations.assets/adjacency_matrix_add_vertex.png) + +=== "删除顶点" + ![adjacency_matrix_remove_vertex](graph_operations.assets/adjacency_matrix_remove_vertex.png) + +以下是基于邻接矩阵表示图的实现代码。 + +=== "Java" + + ```java title="graph_adjacency_matrix.java" + /* 基于邻接矩阵实现的无向图类 */ + class GraphAdjMat { + List vertices; // 顶点列表,元素代表“顶点值”,索引代表“顶点索引” + List> adjMat; // 邻接矩阵,行列索引对应“顶点索引” + + /* 构造函数 */ + public GraphAdjMat(int[] vertices, int[][] edges) { + this.vertices = new ArrayList<>(); + this.adjMat = new ArrayList<>(); + // 添加顶点 + for (int val : vertices) { + addVertex(val); + } + // 添加边 + // 请注意,edges 元素代表顶点索引,即对应 vertices 元素索引 + for (int[] e : edges) { + addEdge(e[0], e[1]); + } + } + + /* 获取顶点数量 */ + public int size() { + return vertices.size(); + } + + /* 添加顶点 */ + public void addVertex(int val) { + int n = size(); + // 向顶点列表中添加新顶点的值 + vertices.add(val); + // 在邻接矩阵中添加一行 + List newRow = new ArrayList<>(n); + for (int j = 0; j < n; j++) { + newRow.add(0); + } + adjMat.add(newRow); + // 在邻接矩阵中添加一列 + for (List row : adjMat) { + row.add(0); + } + } + + /* 删除顶点 */ + public void removeVertex(int index) { + if (index >= size()) + throw new IndexOutOfBoundsException(); + // 在顶点列表中移除索引 index 的顶点 + vertices.remove(index); + // 在邻接矩阵中删除索引 index 的行 + adjMat.remove(index); + // 在邻接矩阵中删除索引 index 的列 + for (List row : adjMat) { + row.remove(index); + } + } + + /* 添加边 */ + // 参数 i, j 对应 vertices 元素索引 + public void addEdge(int i, int j) { + // 索引越界与相等处理 + if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) + throw new IndexOutOfBoundsException(); + // 在无向图中,邻接矩阵沿主对角线对称,即满足 (i, j) == (j, i) + adjMat.get(i).set(j, 1); + adjMat.get(j).set(i, 1); + } + + /* 删除边 */ + // 参数 i, j 对应 vertices 元素索引 + public void removeEdge(int i, int j) { + // 索引越界与相等处理 + if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) + throw new IndexOutOfBoundsException(); + adjMat.get(i).set(j, 0); + adjMat.get(j).set(i, 0); + } + } + ``` + +=== "C++" + + ```cpp title="graph_adjacency_matrix.cpp" + + ``` + +=== "Python" + + ```python title="graph_adjacency_matrix.py" + + ``` + +=== "Go" + + ```go title="graph_adjacency_matrix.go" + /* 基于邻接矩阵实现的无向图类 */ + type graphAdjMat struct { + // 顶点列表,元素代表“顶点值”,索引代表“顶点索引” + vertices []int + // 邻接矩阵,行列索引对应“顶点索引” + adjMat [][]int + } + + func newGraphAdjMat(vertices []int, edges [][]int) *graphAdjMat { + // 添加顶点 + n := len(vertices) + adjMat := make([][]int, n) + for i := range adjMat { + adjMat[i] = make([]int, n) + } + // 初始化图 + g := &graphAdjMat{ + vertices: vertices, + adjMat: adjMat, + } + // 添加边 + // 请注意,edges 元素代表顶点索引,即对应 vertices 元素索引 + for i := range edges { + g.addEdge(edges[i][0], edges[i][1]) + } + return g + } + + /* 获取顶点数量 */ + func (g *graphAdjMat) size() int { + return len(g.vertices) + } + + /* 添加顶点 */ + func (g *graphAdjMat) addVertex(val int) { + n := g.size() + // 向顶点列表中添加新顶点的值 + g.vertices = append(g.vertices, val) + // 在邻接矩阵中添加一行 + newRow := make([]int, n) + g.adjMat = append(g.adjMat, newRow) + // 在邻接矩阵中添加一列 + for i := range g.adjMat { + g.adjMat[i] = append(g.adjMat[i], 0) + } + } + + /* 删除顶点 */ + func (g *graphAdjMat) removeVertex(index int) { + if index >= g.size() { + return + } + // 在顶点列表中移除索引 index 的顶点 + g.vertices = append(g.vertices[:index], g.vertices[index+1:]...) + // 在邻接矩阵中删除索引 index 的行 + g.adjMat = append(g.adjMat[:index], g.adjMat[index+1:]...) + // 在邻接矩阵中删除索引 index 的列 + for i := range g.adjMat { + g.adjMat[i] = append(g.adjMat[i][:index], g.adjMat[i][index+1:]...) + } + } + + /* 添加边 */ + // 参数 i, j 对应 vertices 元素索引 + func (g *graphAdjMat) addEdge(i, j int) { + // 索引越界与相等处理 + if i < 0 || j < 0 || i >= g.size() || j >= g.size() || i == j { + fmt.Errorf("%s", "Index Out Of Bounds Exception") + } + // 在无向图中,邻接矩阵沿主对角线对称,即满足 (i, j) == (j, i) + g.adjMat[i][j] = 1 + g.adjMat[j][i] = 1 + } + + /* 删除边 */ + // 参数 i, j 对应 vertices 元素索引 + func (g *graphAdjMat) removeEdge(i, j int) { + // 索引越界与相等处理 + if i < 0 || j < 0 || i >= g.size() || j >= g.size() || i == j { + fmt.Errorf("%s", "Index Out Of Bounds Exception") + } + g.adjMat[i][j] = 0 + g.adjMat[j][i] = 0 + } + ``` + +=== "JavaScript" + + ```js title="graph_adjacency_matrix.js" + + ``` + +=== "TypeScript" + + ```typescript title="graph_adjacency_matrix.ts" + + ``` + +=== "C" + + ```c title="graph_adjacency_matrix.c" + + ``` + +=== "C#" + + ```csharp title="graph_adjacency_matrix.cs" + + ``` + +=== "Swift" + + ```swift title="graph_adjacency_matrix.swift" + /* 基于邻接矩阵实现的无向图类 */ + class GraphAdjMat { + private var vertices: [Int] // 顶点列表,元素代表“顶点值”,索引代表“顶点索引” + private var adjMat: [[Int]] // 邻接矩阵,行列索引对应“顶点索引” + + /* 构造函数 */ + init(vertices: [Int], edges: [[Int]]) { + self.vertices = [] + adjMat = [] + // 添加顶点 + for val in vertices { + addVertex(val: val) + } + // 添加边 + // 请注意,edges 元素代表顶点索引,即对应 vertices 元素索引 + for e in edges { + addEdge(i: e[0], j: e[1]) + } + } + + /* 获取顶点数量 */ + func size() -> Int { + vertices.count + } + + /* 添加顶点 */ + func addVertex(val: Int) { + let n = size() + // 向顶点列表中添加新顶点的值 + vertices.append(val) + // 在邻接矩阵中添加一行 + let newRow = Array(repeating: 0, count: n) + adjMat.append(newRow) + // 在邻接矩阵中添加一列 + for i in adjMat.indices { + adjMat[i].append(0) + } + } + + /* 删除顶点 */ + func removeVertex(index: Int) { + if index >= size() { + fatalError("越界") + } + // 在顶点列表中移除索引 index 的顶点 + vertices.remove(at: index) + // 在邻接矩阵中删除索引 index 的行 + adjMat.remove(at: index) + // 在邻接矩阵中删除索引 index 的列 + for i in adjMat.indices { + adjMat[i].remove(at: index) + } + } + + /* 添加边 */ + // 参数 i, j 对应 vertices 元素索引 + func addEdge(i: Int, j: Int) { + // 索引越界与相等处理 + if i < 0 || j < 0 || i >= size() || j >= size() || i == j { + fatalError("越界") + } + // 在无向图中,邻接矩阵沿主对角线对称,即满足 (i, j) == (j, i) + adjMat[i][j] = 1 + adjMat[j][i] = 1 + } + + /* 删除边 */ + // 参数 i, j 对应 vertices 元素索引 + func removeEdge(i: Int, j: Int) { + // 索引越界与相等处理 + if i < 0 || j < 0 || i >= size() || j >= size() || i == j { + fatalError("越界") + } + adjMat[i][j] = 0 + adjMat[j][i] = 0 + } + } + ``` + +=== "Zig" + + ```zig title="graph_adjacency_matrix.zig" + + ``` + +## 9.2.2. 基于邻接表的实现 + +设图的顶点总数为 $n$ 、边总数为 $m$ ,则有: + +- **添加边**:在顶点对应链表的尾部添加边即可,使用 $O(1)$ 时间。因为是无向图,所以需要同时添加两个方向的边。 +- **删除边**:在顶点对应链表中查询与删除指定边,使用 $O(m)$ 时间。与添加边一样,需要同时删除两个方向的边。 +- **添加顶点**:在邻接表中添加一个链表即可,并以新增顶点为链表头结点,使用 $O(1)$ 时间。 +- **删除顶点**:需要遍历整个邻接表,删除包含指定顶点的所有边,使用 $O(n + m)$ 时间。 +- **初始化**:需要在邻接表中建立 $n$ 个结点和 $2m$ 条边,使用 $O(n + m)$ 时间。 + +=== "初始化邻接表" + ![adjacency_list_initialization](graph_operations.assets/adjacency_list_initialization.png) + +=== "添加边" + ![adjacency_list_add_edge](graph_operations.assets/adjacency_list_add_edge.png) + +=== "删除边" + ![adjacency_list_remove_edge](graph_operations.assets/adjacency_list_remove_edge.png) + +=== "添加顶点" + ![adjacency_list_add_vertex](graph_operations.assets/adjacency_list_add_vertex.png) + +=== "删除顶点" + ![adjacency_list_remove_vertex](graph_operations.assets/adjacency_list_remove_vertex.png) + +基于邻接表实现图的代码如下所示。 + +=== "Java" + + ```java title="graph_adjacency_list.java" + /* 顶点类 */ + class Vertex { + int val; + public Vertex(int val) { + this.val = val; + } + } + + /* 基于邻接表实现的无向图类 */ + class GraphAdjList { + // 请注意,vertices 和 adjList 中存储的都是 Vertex 对象 + Map> adjList; // 邻接表(使用哈希表实现) + + /* 构造函数 */ + public GraphAdjList(Vertex[][] edges) { + this.adjList = new HashMap<>(); + // 添加所有顶点和边 + for (Vertex[] edge : edges) { + addVertex(edge[0]); + addVertex(edge[1]); + addEdge(edge[0], edge[1]); + } + } + + /* 获取顶点数量 */ + public int size() { + return adjList.size(); + } + + /* 添加边 */ + public void addEdge(Vertex vet1, Vertex vet2) { + if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2) + throw new IllegalArgumentException(); + // 添加边 vet1 - vet2 + adjList.get(vet1).add(vet2); + adjList.get(vet2).add(vet1); + } + + /* 删除边 */ + public void removeEdge(Vertex vet1, Vertex vet2) { + if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2) + throw new IllegalArgumentException(); + // 删除边 vet1 - vet2 + adjList.get(vet1).remove(vet2); + adjList.get(vet2).remove(vet1); + } + + /* 添加顶点 */ + public void addVertex(Vertex vet) { + if (adjList.containsKey(vet)) + return; + // 在邻接表中添加一个新链表(即 HashSet) + adjList.put(vet, new HashSet<>()); + } + + /* 删除顶点 */ + public void removeVertex(Vertex vet) { + if (!adjList.containsKey(vet)) + throw new IllegalArgumentException(); + // 在邻接表中删除顶点 vet 对应的链表(即 HashSet) + adjList.remove(vet); + // 遍历其它顶点的链表(即 HashSet),删除所有包含 vet 的边 + for (Set set : adjList.values()) { + set.remove(vet); + } + } + } + ``` + +=== "C++" + + ```cpp title="graph_adjacency_list.cpp" + + ``` + +=== "Python" + + ```python title="graph_adjacency_list.py" + + ``` + +=== "Go" + + ```go title="graph_adjacency_list.go" + /* 顶点类 */ + type vertex struct { + val int + } + + func newVertex(val int) vertex { + return vertex{ + val: val, + } + } + + /* 基于邻接表实现的无向图类 */ + type graphAdjList struct { + // 请注意,vertices 和 adjList 中存储的都是 Vertex 对象 + // 邻接表(使用哈希表实现), 使用哈希表模拟集合 + adjList map[vertex]map[vertex]struct{} + } + + /* 构造函数 */ + func newGraphAdjList(edges [][]vertex) *graphAdjList { + g := &graphAdjList{ + adjList: make(map[vertex]map[vertex]struct{}), + } + // 添加所有顶点和边 + for _, edge := range edges { + g.addVertex(edge[0]) + g.addVertex(edge[1]) + g.addEdge(edge[0], edge[1]) + } + return g + } + + /* 获取顶点数量 */ + func (g *graphAdjList) size() int { + return len(g.adjList) + } + + /* 添加边 */ + func (g *graphAdjList) addEdge(vet1 vertex, vet2 vertex) { + _, ok1 := g.adjList[vet1] + _, ok2 := g.adjList[vet2] + if !ok1 || !ok2 || vet1 == vet2 { + panic("error") + } + // 添加边 vet1 - vet2, 添加匿名 struct{}, + g.adjList[vet1][vet2] = struct{}{} + g.adjList[vet2][vet1] = struct{}{} + } + + /* 删除边 */ + func (g *graphAdjList) removeEdge(vet1 vertex, vet2 vertex) { + _, ok1 := g.adjList[vet1] + _, ok2 := g.adjList[vet2] + if !ok1 || !ok2 || vet1 == vet2 { + panic("error") + } + // 删除边 vet1 - vet2, 借助 delete 来删除 map 中的键 + delete(g.adjList[vet1], vet2) + delete(g.adjList[vet2], vet1) + } + + /* 添加顶点 */ + func (g *graphAdjList) addVertex(vet vertex) { + _, ok := g.adjList[vet] + if ok { + return + } + // 在邻接表中添加一个新链表(即 set) + g.adjList[vet] = make(map[vertex]struct{}) + } + + /* 删除顶点 */ + func (g *graphAdjList) removeVertex(vet vertex) { + _, ok := g.adjList[vet] + if !ok { + panic("error") + } + // 在邻接表中删除顶点 vet 对应的链表 + delete(g.adjList, vet) + // 遍历其它顶点的链表(即 Set),删除所有包含 vet 的边 + for _, set := range g.adjList { + // 操作 + delete(set, vet) + } + } + ``` + +=== "JavaScript" + + ```js title="graph_adjacency_list.js" + + ``` + +=== "TypeScript" + + ```typescript title="graph_adjacency_list.ts" + + ``` + +=== "C" + + ```c title="graph_adjacency_list.c" + + ``` + +=== "C#" + + ```csharp title="graph_adjacency_list.cs" + + ``` + +=== "Swift" + + ```swift title="graph_adjacency_list.swift" + /* 顶点类 */ + class Vertex: Hashable { + var val: Int + + init(val: Int) { + self.val = val + } + + static func == (lhs: Vertex, rhs: Vertex) -> Bool { + lhs.val == rhs.val + } + + func hash(into hasher: inout Hasher) { + hasher.combine(val) + } + } + + /* 基于邻接表实现的无向图类 */ + class GraphAdjList { + // 请注意,vertices 和 adjList 中存储的都是 Vertex 对象 + private var adjList: [Vertex: Set] // 邻接表(使用哈希表实现) + + init(edges: [[Vertex]]) { + adjList = [:] + // 添加所有顶点和边 + for edge in edges { + addVertex(vet: edge[0]) + addVertex(vet: edge[1]) + addEdge(vet1: edge[0], vet2: edge[1]) + } + } + + /* 获取顶点数量 */ + func size() -> Int { + adjList.count + } + + /* 添加边 */ + func addEdge(vet1: Vertex, vet2: Vertex) { + if adjList[vet1] == nil || adjList[vet2] == nil || vet1 == vet2 { + fatalError("参数错误") + } + // 添加边 vet1 - vet2 + adjList[vet1]?.insert(vet2) + adjList[vet2]?.insert(vet1) + } + + /* 删除边 */ + func removeEdge(vet1: Vertex, vet2: Vertex) { + if adjList[vet1] == nil || adjList[vet2] == nil || vet1 == vet2 { + fatalError("参数错误") + } + // 删除边 vet1 - vet2 + adjList[vet1]?.remove(vet2) + adjList[vet2]?.remove(vet1) + } + + /* 添加顶点 */ + func addVertex(vet: Vertex) { + if adjList[vet] != nil { + return + } + // 在邻接表中添加一个新链表(即 HashSet) + adjList[vet] = [] + } + + /* 删除顶点 */ + func removeVertex(vet: Vertex) { + if adjList[vet] == nil { + fatalError("参数错误") + } + // 在邻接表中删除顶点 vet 对应的链表(即 HashSet) + adjList.removeValue(forKey: vet) + // 遍历其它顶点的链表(即 HashSet),删除所有包含 vet 的边 + for key in adjList.keys { + adjList[key]?.remove(vet) + } + } + } + ``` + +=== "Zig" + + ```zig title="graph_adjacency_list.zig" + + ``` + +## 9.2.3. 效率对比 + +设图中共有 $n$ 个顶点和 $m$ 条边,下表为邻接矩阵和邻接表的时间和空间效率对比。 + +
+ +| | 邻接矩阵 | 邻接表(链表) | 邻接表(哈希表) | +| ------------ | -------- | -------------- | ---------------- | +| 判断是否邻接 | $O(1)$ | $O(m)$ | $O(1)$ | +| 添加边 | $O(1)$ | $O(1)$ | $O(1)$ | +| 删除边 | $O(1)$ | $O(m)$ | $O(1)$ | +| 添加顶点 | $O(n)$ | $O(1)$ | $O(1)$ | +| 删除顶点 | $O(n^2)$ | $O(n + m)$ | $O(n)$ | +| 内存空间占用 | $O(n^2)$ | $O(n + m)$ | $O(n + m)$ | + +
+ +观察上表,貌似邻接表(哈希表)的时间与空间效率最优。但实际上,在邻接矩阵中操作边的效率更高,只需要一次数组访问或赋值操作即可。总结以上,**邻接矩阵体现“以空间换时间”,邻接表体现“以时间换空间”**。 diff --git a/build/chapter_hashing/hash_collision.md b/build/chapter_hashing/hash_collision.md new file mode 100644 index 000000000..269254c6c --- /dev/null +++ b/build/chapter_hashing/hash_collision.md @@ -0,0 +1,81 @@ +--- +comments: true +--- + +# 6.2. 哈希冲突 + +理想情况下,哈希函数应该为每个输入产生唯一的输出,使得 key 和 value 一一对应。而实际上,往往存在向哈希函数输入不同的 key 而产生相同输出的情况,这种情况被称为「哈希冲突 Hash Collision」。哈希冲突会导致查询结果错误,从而严重影响哈希表的可用性。 + +那么,为什么会出现哈希冲突呢?本质上看,**由于哈希函数的输入空间往往远大于输出空间**,因此不可避免地会出现多个输入产生相同输出的情况,即为哈希冲突。比如,输入空间是全体整数,输出空间是一个固定大小的桶(数组)的索引范围,那么必定会有多个整数同时映射到一个桶索引。 + +为了缓解哈希冲突,一方面,我们可以通过「哈希表扩容」来减小冲突概率。极端情况下,当输入空间和输出空间大小相等时,哈希表就等价于数组了,可谓“大力出奇迹”。 + +另一方面,**考虑通过优化数据结构以缓解哈希冲突**,常见的方法有「链式地址」和「开放寻址」。 + +## 6.2.1. 哈希表扩容 + +「负载因子 Load Factor」定义为 **哈希表中元素数量除以桶槽数量(即数组大小)**,代表哈希冲突的严重程度。 + +**负载因子常用作哈希表扩容的触发条件**。比如在 Java 中,当负载因子 $> 0.75$ 时则触发扩容,将 HashMap 大小扩充至原先的 $2$ 倍。 + +与数组扩容类似,**哈希表扩容操作的开销很大**,因为需要将所有键值对从原哈希表依次移动至新哈希表。 + +## 6.2.2. 链式地址 + +在原始哈希表中,桶内的每个地址只能存储一个元素(即键值对)。**考虑将单个元素转化成一个链表,将所有冲突元素都存储在一个链表中**。 + +![hash_collision_chaining](hash_collision.assets/hash_collision_chaining.png) + +链式地址下,哈希表操作方法为: + +- **查询元素**:先将 key 输入到哈希函数得到桶内索引,即可访问链表头结点,再通过遍历链表查找对应 value 。 +- **添加元素**:先通过哈希函数访问链表头部,再将结点(即键值对)添加到链表头部即可。 +- **删除元素**:同样先根据哈希函数结果访问链表头部,再遍历链表查找对应结点,删除之即可。 + +链式地址虽然解决了哈希冲突问题,但仍存在局限性,包括: + +- **占用空间变大**,因为链表或二叉树包含结点指针,相比于数组更加耗费内存空间; +- **查询效率降低**,因为需要线性遍历链表来查找对应元素; + +为了缓解时间效率问题,**可以把「链表」转化为「AVL 树」或「红黑树」**,将查询操作的时间复杂度优化至 $O(\log n)$ 。 + +## 6.2.3. 开放寻址 + +「开放寻址」不引入额外数据结构,而是通过“多次探测”来解决哈希冲突。根据探测方法的不同,主要分为 **线性探测、平方探测、多次哈希**。 + +### 线性探测 + +「线性探测」使用固定步长的线性查找来解决哈希冲突。 + +**插入元素**:如果出现哈希冲突,则从冲突位置向后线性遍历(步长一般取 1 ),直到找到一个空位,则将元素插入到该空位中。 + +**查找元素**:若出现哈希冲突,则使用相同步长执行线性查找,会遇到两种情况: + +1. 找到对应元素,返回 value 即可; +2. 若遇到空位,则说明查找键值对不在哈希表中; + +![hash_collision_linear_probing](hash_collision.assets/hash_collision_linear_probing.png) + +线性探测存在以下缺陷: + +- **不能直接删除元素**。删除元素会导致桶内出现一个空位,在查找其他元素时,该空位有可能导致程序认为元素不存在(即上述第 `2.` 种情况)。因此需要借助一个标志位来标记删除元素。 +- **容易产生聚集**。桶内被占用的连续位置越长,这些连续位置发生哈希冲突的可能性越大,从而进一步促进这一位置的“聚堆生长”,最终导致增删查改操作效率的劣化。 + +### 多次哈希 + +顾名思义,「多次哈希」的思路是使用多个哈希函数 $f_1(x)$ , $f_2(x)$ , $f_3(x)$ , $\cdots$ 进行探测。 + +**插入元素**:若哈希函数 $f_1(x)$ 出现冲突,则尝试 $f_2(x)$ ,以此类推……直到找到空位后插入元素。 + +**查找元素**:以相同的哈希函数顺序查找,存在两种情况: + +1. 找到目标元素,则返回之; +2. 到空位或已尝试所有哈希函数,说明哈希表中无此元素; + +相比于「线性探测」,「多次哈希」方法更不容易产生聚集,代价是多个哈希函数增加了额外计算量。 + +!!! note "工业界方案" + + Java 采用「链式地址」。在 JDK 1.8 之后,HashMap 内数组长度大于 64 时,长度大于 8 的链表会被转化为「红黑树」,以提升查找性能。 + + Python 采用「开放寻址」。字典 dict 使用伪随机数进行探测。 diff --git a/build/chapter_hashing/hash_map.md b/build/chapter_hashing/hash_map.md new file mode 100755 index 000000000..40913ec14 --- /dev/null +++ b/build/chapter_hashing/hash_map.md @@ -0,0 +1,887 @@ +--- +comments: true +--- + +# 6.1. 哈希表 + +哈希表通过建立「键 key」和「值 value」之间的映射,实现高效的元素查找。具体地,输入一个 key ,在哈希表中查询并获取 value ,时间复杂度为 $O(1)$ 。 + +例如,给定一个包含 $n$ 个学生的数据库,每个学生有“姓名 `name` ”和“学号 `id` ”两项数据,希望实现一个查询功能:**输入一个学号,返回对应的姓名**,则可以使用哈希表实现。 + +![hash_map](hash_map.assets/hash_map.png) + +

Fig. 哈希表抽象表示

+ +## 6.1.1. 哈希表效率 + +除了哈希表之外,还可以使用以下数据结构来实现上述查询功能: + +1. **无序数组**:每个元素为 `[学号, 姓名]` ; +2. **有序数组**:将 `1.` 中的数组按照学号从小到大排序; +3. **链表**:每个结点的值为 `[学号, 姓名]` ; +4. **二叉搜索树**:每个结点的值为 `[学号, 姓名]` ,根据学号大小来构建树; + +使用上述方法,各项操作的时间复杂度如下表所示(在此不做赘述,详解可见 [二叉搜索树章节](https://www.hello-algo.com/chapter_tree/binary_search_tree/#_6))。无论是查找元素、还是增删元素,哈希表的时间复杂度都是 $O(1)$ ,全面胜出! + +
+ +| | 无序数组 | 有序数组 | 链表 | 二叉搜索树 | 哈希表 | +| -------- | -------- | ----------- | ------ | ----------- | ------ | +| 查找元素 | $O(n)$ | $O(\log n)$ | $O(n)$ | $O(\log n)$ | $O(1)$ | +| 插入元素 | $O(1)$ | $O(n)$ | $O(1)$ | $O(\log n)$ | $O(1)$ | +| 删除元素 | $O(n)$ | $O(n)$ | $O(n)$ | $O(\log n)$ | $O(1)$ | + +
+ +## 6.1.2. 哈希表常用操作 + +哈希表的基本操作包括 **初始化、查询操作、添加与删除键值对**。 + +=== "Java" + + ```java title="hash_map.java" + /* 初始化哈希表 */ + Map map = new HashMap<>(); + + /* 添加操作 */ + // 在哈希表中添加键值对 (key, value) + map.put(12836, "小哈"); + map.put(15937, "小啰"); + map.put(16750, "小算"); + map.put(13276, "小法"); + map.put(10583, "小鸭"); + + /* 查询操作 */ + // 向哈希表输入键 key ,得到值 value + String name = map.get(15937); + + /* 删除操作 */ + // 在哈希表中删除键值对 (key, value) + map.remove(10583); + ``` + +=== "C++" + + ```cpp title="hash_map.cpp" + /* 初始化哈希表 */ + unordered_map map; + + /* 添加操作 */ + // 在哈希表中添加键值对 (key, value) + map[12836] = "小哈"; + map[15937] = "小啰"; + map[16750] = "小算"; + map[13276] = "小法"; + map[10583] = "小鸭"; + + /* 查询操作 */ + // 向哈希表输入键 key ,得到值 value + string name = map[15937]; + + /* 删除操作 */ + // 在哈希表中删除键值对 (key, value) + map.erase(10583); + ``` + +=== "Python" + + ```python title="hash_map.py" + """ 初始化哈希表 """ + mapp = {} + + """ 添加操作 """ + # 在哈希表中添加键值对 (key, value) + mapp[12836] = "小哈" + mapp[15937] = "小啰" + mapp[16750] = "小算" + mapp[13276] = "小法" + mapp[10583] = "小鸭" + + """ 查询操作 """ + # 向哈希表输入键 key ,得到值 value + name = mapp[15937] + + """ 删除操作 """ + # 在哈希表中删除键值对 (key, value) + mapp.pop(10583) + ``` + +=== "Go" + + ```go title="hash_map.go" + /* 初始化哈希表 */ + mapp := make(map[int]string) + + /* 添加操作 */ + // 在哈希表中添加键值对 (key, value) + mapp[12836] = "小哈" + mapp[15937] = "小啰" + mapp[16750] = "小算" + mapp[13276] = "小法" + mapp[10583] = "小鸭" + + /* 查询操作 */ + // 向哈希表输入键 key ,得到值 value + name := mapp[15937] + + /* 删除操作 */ + // 在哈希表中删除键值对 (key, value) + delete(mapp, 10583) + ``` + +=== "JavaScript" + + ```js title="hash_map.js" + /* 初始化哈希表 */ + const map = new ArrayHashMap(); + /* 添加操作 */ + // 在哈希表中添加键值对 (key, value) + map.set(12836, '小哈'); + map.set(15937, '小啰'); + map.set(16750, '小算'); + map.set(13276, '小法'); + map.set(10583, '小鸭'); + + /* 查询操作 */ + // 向哈希表输入键 key ,得到值 value + let name = map.get(15937); + + /* 删除操作 */ + // 在哈希表中删除键值对 (key, value) + map.delete(10583); + ``` + +=== "TypeScript" + + ```typescript title="hash_map.ts" + /* 初始化哈希表 */ + const map = new Map(); + /* 添加操作 */ + // 在哈希表中添加键值对 (key, value) + map.set(12836, '小哈'); + map.set(15937, '小啰'); + map.set(16750, '小算'); + map.set(13276, '小法'); + map.set(10583, '小鸭'); + console.info('\n添加完成后,哈希表为\nKey -> Value'); + console.info(map); + + /* 查询操作 */ + // 向哈希表输入键 key ,得到值 value + let name = map.get(15937); + console.info('\n输入学号 15937 ,查询到姓名 ' + name); + + /* 删除操作 */ + // 在哈希表中删除键值对 (key, value) + map.delete(10583); + console.info('\n删除 10583 后,哈希表为\nKey -> Value'); + console.info(map); + ``` + +=== "C" + + ```c title="hash_map.c" + + ``` + +=== "C#" + + ```csharp title="hash_map.cs" + /* 初始化哈希表 */ + Dictionary map = new (); + + /* 添加操作 */ + // 在哈希表中添加键值对 (key, value) + map.Add(12836, "小哈"); + map.Add(15937, "小啰"); + map.Add(16750, "小算"); + map.Add(13276, "小法"); + map.Add(10583, "小鸭"); + + /* 查询操作 */ + // 向哈希表输入键 key ,得到值 value + String name = map[15937]; + + /* 删除操作 */ + // 在哈希表中删除键值对 (key, value) + map.Remove(10583); + ``` + +=== "Swift" + + ```swift title="hash_map.swift" + /* 初始化哈希表 */ + var map: [Int: String] = [:] + + /* 添加操作 */ + // 在哈希表中添加键值对 (key, value) + map[12836] = "小哈" + map[15937] = "小啰" + map[16750] = "小算" + map[13276] = "小法" + map[10583] = "小鸭" + + /* 查询操作 */ + // 向哈希表输入键 key ,得到值 value + let name = map[15937]! + + /* 删除操作 */ + // 在哈希表中删除键值对 (key, value) + map.removeValue(forKey: 10583) + ``` + +=== "Zig" + + ```zig title="hash_map.zig" + + ``` + +遍历哈希表有三种方式,即 **遍历键值对、遍历键、遍历值**。 + +=== "Java" + + ```java title="hash_map.java" + /* 遍历哈希表 */ + // 遍历键值对 key->value + for (Map.Entry kv: map.entrySet()) { + System.out.println(kv.getKey() + " -> " + kv.getValue()); + } + // 单独遍历键 key + for (int key: map.keySet()) { + System.out.println(key); + } + // 单独遍历值 value + for (String val: map.values()) { + System.out.println(val); + } + ``` + +=== "C++" + + ```cpp title="hash_map.cpp" + /* 遍历哈希表 */ + // 遍历键值对 key->value + for (auto kv: map) { + cout << kv.first << " -> " << kv.second << endl; + } + // 单独遍历键 key + for (auto key: map) { + cout << key.first << endl; + } + // 单独遍历值 value + for (auto val: map) { + cout << val.second << endl; + } + ``` + +=== "Python" + + ```python title="hash_map.py" + """ 遍历哈希表 """ + # 遍历键值对 key->value + for key, value in mapp.items(): + print(key, "->", value) + # 单独遍历键 key + for key in mapp.keys(): + print(key) + # 单独遍历值 value + for value in mapp.values(): + print(value) + ``` + +=== "Go" + + ```go title="hash_map_test.go" + /* 遍历哈希表 */ + // 遍历键值对 key->value + for key, value := range mapp { + fmt.Println(key, "->", value) + } + // 单独遍历键 key + for key := range mapp { + fmt.Println(key) + } + // 单独遍历值 value + for _, value := range mapp { + fmt.Println(value) + } + ``` + +=== "JavaScript" + + ```js title="hash_map.js" + /* 遍历哈希表 */ + // 遍历键值对 key->value + for (const entry of map.entries()) { + if (!entry) continue; + console.info(entry.key + ' -> ' + entry.val); + } + // 单独遍历键 key + for (const key of map.keys()) { + console.info(key); + } + // 单独遍历值 value + for (const val of map.values()) { + console.info(val); + } + ``` + +=== "TypeScript" + + ```typescript title="hash_map.ts" + /* 遍历哈希表 */ + console.info('\n遍历键值对 Key->Value'); + for (const [k, v] of map.entries()) { + console.info(k + ' -> ' + v); + } + console.info('\n单独遍历键 Key'); + for (const k of map.keys()) { + console.info(k); + } + console.info('\n单独遍历值 Value'); + for (const v of map.values()) { + console.info(v); + } + ``` + +=== "C" + + ```c title="hash_map.c" + + ``` + +=== "C#" + + ```csharp title="hash_map.cs" + /* 遍历哈希表 */ + // 遍历键值对 Key->Value + foreach (var kv in map) { + Console.WriteLine(kv.Key + " -> " + kv.Value); + } + // 单独遍历键 key + foreach (int key in map.Keys) { + Console.WriteLine(key); + } + // 单独遍历值 value + foreach (String val in map.Values) { + Console.WriteLine(val); + } + ``` + +=== "Swift" + + ```swift title="hash_map.swift" + /* 遍历哈希表 */ + // 遍历键值对 Key->Value + for (key, value) in map { + print("\(key) -> \(value)") + } + // 单独遍历键 Key + for key in map.keys { + print(key) + } + // 单独遍历值 Value + for value in map.values { + print(value) + } + ``` + +=== "Zig" + + ```zig title="hash_map.zig" + + ``` + +## 6.1.3. 哈希函数 + +哈希表中存储元素的数据结构被称为「桶 Bucket」,底层实现可能是数组、链表、二叉树(红黑树),或是它们的组合。 + +最简单地,**我们可以仅用一个「数组」来实现哈希表**。首先,将所有 value 放入数组中,那么每个 value 在数组中都有唯一的「索引」。显然,访问 value 需要给定索引,而为了 **建立 key 和索引之间的映射关系**,我们需要使用「哈希函数 Hash Function」。 + +设数组为 `bucket` ,哈希函数为 `f(x)` ,输入键为 `key` 。那么获取 value 的步骤为: + +1. 通过哈希函数计算出索引,即 `index = f(key)` ; +2. 通过索引在数组中获取值,即 `value = bucket[index]` ; + +以上述学生数据 `key 学号 -> value 姓名` 为例,我们可以将「哈希函数」设计为 + +$$ +f(x) = x \% 100 +$$ + +![hash_function](hash_map.assets/hash_function.png) + +

Fig. 哈希函数

+ +=== "Java" + + ```java title="array_hash_map.java" + /* 键值对 int->String */ + class Entry { + public int key; // 键 + public String val; // 值 + public Entry(int key, String val) { + this.key = key; + this.val = val; + } + } + + /* 基于数组简易实现的哈希表 */ + class ArrayHashMap { + private List bucket; + public ArrayHashMap() { + // 初始化一个长度为 100 的桶(数组) + bucket = new ArrayList<>(); + for (int i = 0; i < 100; i++) { + bucket.add(null); + } + } + + /* 哈希函数 */ + private int hashFunc(int key) { + int index = key % 100; + return index; + } + + /* 查询操作 */ + public String get(int key) { + int index = hashFunc(key); + Entry pair = bucket.get(index); + if (pair == null) return null; + return pair.val; + } + + /* 添加操作 */ + public void put(int key, String val) { + Entry pair = new Entry(key, val); + int index = hashFunc(key); + bucket.set(index, pair); + } + + /* 删除操作 */ + public void remove(int key) { + int index = hashFunc(key); + // 置为 null,代表删除 + bucket.set(index, null); + } + } + ``` + +=== "C++" + + ```cpp title="array_hash_map.cpp" + /* 键值对 int->String */ + struct Entry { + public: + int key; + string val; + Entry(int key, string val) { + this->key = key; + this->val = val; + } + }; + + /* 基于数组简易实现的哈希表 */ + class ArrayHashMap { + private: + vector bucket; + public: + ArrayHashMap() { + // 初始化一个长度为 100 的桶(数组) + bucket= vector(100); + } + + /* 哈希函数 */ + int hashFunc(int key) { + int index = key % 100; + return index; + } + + /* 查询操作 */ + string get(int key) { + int index = hashFunc(key); + Entry* pair = bucket[index]; + return pair->val; + } + + /* 添加操作 */ + void put(int key, string val) { + Entry* pair = new Entry(key, val); + int index = hashFunc(key); + bucket[index] = pair; + } + + /* 删除操作 */ + void remove(int key) { + int index = hashFunc(key); + // 置为 nullptr ,代表删除 + bucket[index] = nullptr; + } + }; + ``` + +=== "Python" + + ```python title="array_hash_map.py" + """ 键值对 int->String """ + class Entry: + def __init__(self, key, val): + self.key = key + self.val = val + + """ 基于数组简易实现的哈希表 """ + class ArrayHashMap: + def __init__(self): + # 初始化一个长度为 100 的桶(数组) + self.bucket = [None] * 100 + + """ 哈希函数 """ + def hash_func(self, key): + index = key % 100 + return index + + """ 查询操作 """ + def get(self, key): + index = self.hash_func(key) + pair = self.bucket[index] + if pair is None: + return None + return pair.val + + """ 添加操作 """ + def put(self, key, val): + pair = Entry(key, val) + index = self.hash_func(key) + self.bucket[index] = pair + + """ 删除操作 """ + def remove(self, key): + index = self.hash_func(key) + # 置为 None ,代表删除 + self.bucket[index] = None + + """ 获取所有键值对 """ + def entry_set(self): + result = [] + for pair in self.bucket: + if pair is not None: + result.append(pair) + return result + + """ 获取所有键 """ + def key_set(self): + result = [] + for pair in self.bucket: + if pair is not None: + result.append(pair.key) + return result + + """ 获取所有值 """ + def value_set(self): + result = [] + for pair in self.bucket: + if pair is not None: + result.append(pair.val) + return result + + """ 打印哈希表 """ + def print(self): + for pair in self.bucket: + if pair is not None: + print(pair.key, "->", pair.val) + ``` + +=== "Go" + + ```go title="array_hash_map.go" + /* 键值对 int->String */ + type entry struct { + key int + val string + } + + /* 基于数组简易实现的哈希表 */ + type arrayHashMap struct { + bucket []*entry + } + + func newArrayHashMap() *arrayHashMap { + // 初始化一个长度为 100 的桶(数组) + bucket := make([]*entry, 100) + return &arrayHashMap{bucket: bucket} + } + + /* 哈希函数 */ + func (a *arrayHashMap) hashFunc(key int) int { + index := key % 100 + return index + } + + /* 查询操作 */ + func (a *arrayHashMap) get(key int) string { + index := a.hashFunc(key) + pair := a.bucket[index] + if pair == nil { + return "Not Found" + } + return pair.val + } + + /* 添加操作 */ + func (a *arrayHashMap) put(key int, val string) { + pair := &entry{key: key, val: val} + index := a.hashFunc(key) + a.bucket[index] = pair + } + + /* 删除操作 */ + func (a *arrayHashMap) remove(key int) { + index := a.hashFunc(key) + // 置为 nil ,代表删除 + a.bucket[index] = nil + } + ``` + +=== "JavaScript" + + ```js title="array_hash_map.js" + /* 键值对 Number -> String */ + class Entry { + constructor(key, val) { + this.key = key; + this.val = val; + } + } + + /* 基于数组简易实现的哈希表 */ + class ArrayHashMap { + #bucket; + constructor() { + // 初始化一个长度为 100 的桶(数组) + this.#bucket = new Array(100).fill(null); + } + + /* 哈希函数 */ + #hashFunc(key) { + return key % 100; + } + + /* 查询操作 */ + get(key) { + let index = this.#hashFunc(key); + let entry = this.#bucket[index]; + if (entry === null) return null; + return entry.val; + } + + /* 添加操作 */ + set(key, val) { + let index = this.#hashFunc(key); + this.#bucket[index] = new Entry(key, val); + } + + /* 删除操作 */ + delete(key) { + let index = this.#hashFunc(key); + // 置为 null ,代表删除 + this.#bucket[index] = null; + } + } + ``` + +=== "TypeScript" + + ```typescript title="array_hash_map.ts" + /* 键值对 Number -> String */ + class Entry { + public key: number; + public val: string; + + constructor(key: number, val: string) { + this.key = key; + this.val = val; + } + } + + /* 基于数组简易实现的哈希表 */ + class ArrayHashMap { + + private readonly bucket: (Entry | null)[]; + + constructor() { + // 初始化一个长度为 100 的桶(数组) + this.bucket = (new Array(100)).fill(null); + } + + /* 哈希函数 */ + private hashFunc(key: number): number { + return key % 100; + } + + /* 查询操作 */ + public get(key: number): string | null { + let index = this.hashFunc(key); + let entry = this.bucket[index]; + if (entry === null) return null; + return entry.val; + } + + /* 添加操作 */ + public set(key: number, val: string) { + let index = this.hashFunc(key); + this.bucket[index] = new Entry(key, val); + } + + /* 删除操作 */ + public delete(key: number) { + let index = this.hashFunc(key); + // 置为 null ,代表删除 + this.bucket[index] = null; + } + } + ``` + +=== "C" + + ```c title="array_hash_map.c" + + ``` + +=== "C#" + + ```csharp title="array_hash_map.cs" + /* 键值对 int->String */ + class Entry + { + public int key; + public String val; + public Entry(int key, String val) + { + this.key = key; + this.val = val; + } + } + + /* 基于数组简易实现的哈希表 */ + class ArrayHashMap + { + private List bucket; + public ArrayHashMap() + { + // 初始化一个长度为 100 的桶(数组) + bucket = new (); + for (int i = 0; i < 100; i++) + { + bucket.Add(null); + } + } + /* 哈希函数 */ + private int hashFunc(int key) + { + int index = key % 100; + return index; + } + /* 查询操作 */ + public String? get(int key) + { + int index = hashFunc(key); + Entry? pair = bucket[index]; + if (pair == null) return null; + return pair.val; + } + /* 添加操作 */ + public void put(int key, String val) + { + Entry pair = new Entry(key, val); + int index = hashFunc(key); + bucket[index]=pair; + } + /* 删除操作 */ + public void remove(int key) + { + int index = hashFunc(key); + // 置为 null ,代表删除 + bucket[index]=null; + } + } + ``` + +=== "Swift" + + ```swift title="array_hash_map.swift" + /* 键值对 int->String */ + class Entry { + var key: Int + var val: String + + init(key: Int, val: String) { + self.key = key + self.val = val + } + } + + /* 基于数组简易实现的哈希表 */ + class ArrayHashMap { + private var bucket: [Entry?] = [] + + init() { + // 初始化一个长度为 100 的桶(数组) + for _ in 0 ..< 100 { + bucket.append(nil) + } + } + + /* 哈希函数 */ + private func hashFunc(key: Int) -> Int { + let index = key % 100 + return index + } + + /* 查询操作 */ + func get(key: Int) -> String? { + let index = hashFunc(key: key) + let pair = bucket[index] + return pair?.val + } + + /* 添加操作 */ + func put(key: Int, val: String) { + let pair = Entry(key: key, val: val) + let index = hashFunc(key: key) + bucket[index] = pair + } + + /* 删除操作 */ + func remove(key: Int) { + let index = hashFunc(key: key) + // 置为 nil ,代表删除 + bucket[index] = nil + } + } + ``` + +=== "Zig" + + ```zig title="array_hash_map.zig" + + ``` + +## 6.1.4. 哈希冲突 + +细心的同学可能会发现,**哈希函数 $f(x) = x \% 100$ 会在某些情况下失效**。具体地,当输入的 key 后两位相同时,哈希函数的计算结果也相同,指向同一个 value 。例如,分别查询两个学号 $12836$ 和 $20336$ ,则有 + +$$ +f(12836) = f(20336) = 36 +$$ + +两个学号指向了同一个姓名,这明显是不对的,我们将这种现象称为「哈希冲突 Hash Collision」。如何避免哈希冲突的问题将被留在下章讨论。 + +![hash_collision](hash_map.assets/hash_collision.png) + +

Fig. 哈希冲突

+ +综上所述,一个优秀的「哈希函数」应该具备以下特性: + +- 尽量少地发生哈希冲突; +- 时间复杂度 $O(1)$ ,计算尽可能高效; +- 空间使用率高,即“键值对占用空间 / 哈希表总占用空间”尽可能大; diff --git a/build/chapter_hashing/summary.md b/build/chapter_hashing/summary.md new file mode 100644 index 000000000..a66b041a3 --- /dev/null +++ b/build/chapter_hashing/summary.md @@ -0,0 +1,5 @@ +--- +comments: true +--- + +# 6.3. 小结 diff --git a/build/chapter_heap/heap.md b/build/chapter_heap/heap.md new file mode 100644 index 000000000..79c1ded80 --- /dev/null +++ b/build/chapter_heap/heap.md @@ -0,0 +1,1040 @@ +--- +comments: true +--- + +# 8.1. 堆 + +「堆 Heap」是一棵限定条件下的「完全二叉树」。根据成立条件,堆主要分为两种类型: + +- 「大顶堆 Max Heap」,任意结点的值 $\geq$ 其子结点的值; +- 「小顶堆 Min Heap」,任意结点的值 $\leq$ 其子结点的值; + +![min_heap_and_max_heap](heap.assets/min_heap_and_max_heap.png) + +## 8.1.1. 堆术语与性质 + +- 由于堆是完全二叉树,因此最底层结点靠左填充,其它层结点皆被填满。 +- 二叉树中的根结点对应「堆顶」,底层最靠右结点对应「堆底」。 +- 对于大顶堆 / 小顶堆,其堆顶元素(即根结点)的值最大 / 最小。 + +## 8.1.2. 堆常用操作 + +值得说明的是,多数编程语言提供的是「优先队列 Priority Queue」,其是一种抽象数据结构,**定义为具有出队优先级的队列**。 + +而恰好,**堆的定义与优先队列的操作逻辑完全吻合**,大顶堆就是一个元素从大到小出队的优先队列。从使用角度看,我们可以将「优先队列」和「堆」理解为等价的数据结构。因此,本文与代码对两者不做特别区分,统一使用「堆」来命名。 + +堆的常用操作见下表(方法命名以 Java 为例)。 + +

Table. 堆的常用操作

+ +
+ +| 方法 | 描述 | 时间复杂度 | +| --------- | ----------------------------------------- | ----------- | +| add() | 元素入堆 | $O(\log n)$ | +| poll() | 堆顶元素出堆 | $O(\log n)$ | +| peek() | 访问堆顶元素(大 / 小顶堆分别为最大 / 小值) | $O(1)$ | +| size() | 获取堆的元素数量 | $O(1)$ | +| isEmpty() | 判断堆是否为空 | $O(1)$ | + +
+ +我们可以直接使用编程语言提供的堆类(或优先队列类)。 + +!!! tip + + 类似于排序中“从小到大排列”和“从大到小排列”,“大顶堆”和“小顶堆”可仅通过修改 Comparator 来互相转换。 + +=== "Java" + + ```java title="heap.java" + /* 初始化堆 */ + // 初始化小顶堆 + Queue minHeap = new PriorityQueue<>(); + // 初始化大顶堆(使用 lambda 表达式修改 Comparator 即可) + Queue maxHeap = new PriorityQueue<>((a, b) -> { return b - a; }); + + /* 元素入堆 */ + maxHeap.add(1); + maxHeap.add(3); + maxHeap.add(2); + maxHeap.add(5); + maxHeap.add(4); + + /* 获取堆顶元素 */ + int peek = maxHeap.peek(); // 5 + + /* 堆顶元素出堆 */ + // 出堆元素会形成一个从大到小的序列 + peek = heap.poll(); // 5 + peek = heap.poll(); // 4 + peek = heap.poll(); // 3 + peek = heap.poll(); // 2 + peek = heap.poll(); // 1 + + /* 获取堆大小 */ + int size = maxHeap.size(); + + /* 判断堆是否为空 */ + boolean isEmpty = maxHeap.isEmpty(); + + /* 输入列表并建堆 */ + minHeap = new PriorityQueue<>(Arrays.asList(1, 3, 2, 5, 4)); + ``` + +=== "C++" + + ```cpp title="heap.cpp" + /* 初始化堆 */ + // 初始化小顶堆 + priority_queue, greater> minHeap; + // 初始化大顶堆 + priority_queue, less> maxHeap; + + /* 元素入堆 */ + maxHeap.push(1); + maxHeap.push(3); + maxHeap.push(2); + maxHeap.push(5); + maxHeap.push(4); + + /* 获取堆顶元素 */ + int peek = maxHeap.top(); // 5 + + /* 堆顶元素出堆 */ + // 出堆元素会形成一个从大到小的序列 + maxHeap.pop(); // 5 + maxHeap.pop(); // 4 + maxHeap.pop(); // 3 + maxHeap.pop(); // 2 + maxHeap.pop(); // 1 + + /* 获取堆大小 */ + int size = maxHeap.size(); + + /* 判断堆是否为空 */ + bool isEmpty = maxHeap.empty(); + + /* 输入列表并建堆 */ + vector input{1, 3, 2, 5, 4}; + priority_queue, greater> minHeap(input.begin(), input.end()); + ``` + +=== "Python" + + ```python title="heap.py" + + ``` + +=== "Go" + + ```go title="heap.go" + // Go 语言中可以通过实现 heap.Interface 来构建整数大顶堆 + // 实现 heap.Interface 需要同时实现 sort.Interface + type intHeap []any + + // Push heap.Interface 的方法,实现推入元素到堆 + func (h *intHeap) Push(x any) { + // Push 和 Pop 使用 pointer receiver 作为参数 + // 因为它们不仅会对切片的内容进行调整,还会修改切片的长度。 + *h = append(*h, x.(int)) + } + + // Pop heap.Interface 的方法,实现弹出堆顶元素 + func (h *intHeap) Pop() any { + // 待出堆元素存放在最后 + last := (*h)[len(*h)-1] + *h = (*h)[:len(*h)-1] + return last + } + + // Len sort.Interface 的方法 + func (h *intHeap) Len() int { + return len(*h) + } + + // Less sort.Interface 的方法 + func (h *intHeap) Less(i, j int) bool { + // 如果实现小顶堆,则需要调整为小于号 + return (*h)[i].(int) > (*h)[j].(int) + } + + // Swap sort.Interface 的方法 + func (h *intHeap) Swap(i, j int) { + (*h)[i], (*h)[j] = (*h)[j], (*h)[i] + } + + // Top 获取堆顶元素 + func (h *intHeap) Top() any { + return (*h)[0] + } + + /* Driver Code */ + func TestHeap(t *testing.T) { + /* 初始化堆 */ + // 初始化大顶堆 + maxHeap := &intHeap{} + heap.Init(maxHeap) + /* 元素入堆 */ + // 调用 heap.Interface 的方法,来添加元素 + heap.Push(maxHeap, 1) + heap.Push(maxHeap, 3) + heap.Push(maxHeap, 2) + heap.Push(maxHeap, 4) + heap.Push(maxHeap, 5) + + /* 获取堆顶元素 */ + top := maxHeap.Top() + fmt.Printf("堆顶元素为 %d\n", top) + + /* 堆顶元素出堆 */ + // 调用 heap.Interface 的方法,来移除元素 + heap.Pop(maxHeap) + heap.Pop(maxHeap) + heap.Pop(maxHeap) + heap.Pop(maxHeap) + heap.Pop(maxHeap) + + /* 获取堆大小 */ + size := len(*maxHeap) + fmt.Printf("堆元素数量为 %d\n", size) + + /* 判断堆是否为空 */ + isEmpty := len(*maxHeap) == 0 + fmt.Printf("堆是否为空 %t\n", isEmpty) + } + ``` + +=== "JavaScript" + + ```js title="heap.js" + + ``` + +=== "TypeScript" + + ```typescript title="heap.ts" + + ``` + +=== "C" + + ```c title="heap.c" + + ``` + +=== "C#" + + ```csharp title="heap.cs" + + ``` + +=== "Swift" + + ```swift title="heap.swift" + // Swift 未提供内置 heap 类 + ``` + +=== "Zig" + + ```zig title="heap.zig" + + ``` + +## 8.1.3. 堆的实现 + +下文实现的是「大顶堆」,若想转换为「小顶堆」,将所有大小逻辑判断取逆(例如将 $\geq$ 替换为 $\leq$ )即可,有兴趣的同学可自行实现。 + +### 堆的存储与表示 + +在二叉树章节我们学过,「完全二叉树」非常适合使用「数组」来表示,而堆恰好是一棵完全二叉树,**因而我们采用「数组」来存储「堆」**。 + +**二叉树指针**。使用数组表示二叉树时,元素代表结点值,索引代表结点在二叉树中的位置,**而结点指针通过索引映射公式来实现**。 + +具体地,给定索引 $i$ ,那么其左子结点索引为 $2i + 1$ 、右子结点索引为 $2i + 2$ 、父结点索引为 $(i - 1) / 2$ (向下整除)。当索引越界时,代表空结点或结点不存在。 + +![representation_of_heap](heap.assets/representation_of_heap.png) + +我们将索引映射公式封装成函数,以便后续使用。 + +=== "Java" + + ```java title="my_heap.java" + // 使用列表而非数组,这样无需考虑扩容问题 + List maxHeap; + + /* 构造函数,建立空堆 */ + public MaxHeap() { + maxHeap = new ArrayList<>(); + } + + /* 获取左子结点索引 */ + int left(int i) { + return 2 * i + 1; + } + + /* 获取右子结点索引 */ + int right(int i) { + return 2 * i + 2; + } + + /* 获取父结点索引 */ + int parent(int i) { + return (i - 1) / 2; // 向下整除 + } + ``` + +=== "C++" + + ```cpp title="my_heap.cpp" + // 使用动态数组,这样无需考虑扩容问题 + vector maxHeap; + + /* 获取左子结点索引 */ + int left(int i) { + return 2 * i + 1; + } + + /* 获取右子结点索引 */ + int right(int i) { + return 2 * i + 2; + } + + /* 获取父结点索引 */ + int parent(int i) { + return (i - 1) / 2; // 向下取整 + } + ``` + +=== "Python" + + ```python title="my_heap.py" + + ``` + +=== "Go" + + ```go title="my_heap.go" + type maxHeap struct { + // 使用切片而非数组,这样无需考虑扩容问题 + data []any + } + + /* 构造函数,建立空堆 */ + func newHeap() *maxHeap { + return &maxHeap{ + data: make([]any, 0), + } + } + + /* 获取左子结点索引 */ + func (h *maxHeap) left(i int) int { + return 2*i + 1 + } + + /* 获取右子结点索引 */ + func (h *maxHeap) right(i int) int { + return 2*i + 2 + } + + /* 获取父结点索引 */ + func (h *maxHeap) parent(i int) int { + // 向下整除 + return (i - 1) / 2 + } + ``` + +=== "JavaScript" + + ```js title="my_heap.js" + + ``` + +=== "TypeScript" + + ```typescript title="my_heap.ts" + + ``` + +=== "C" + + ```c title="my_heap.c" + + ``` + +=== "C#" + + ```csharp title="my_heap.cs" + + ``` + +=== "Swift" + + ```swift title="my_heap.swift" + var maxHeap: [Int] + + /* 构造函数,建立空堆 */ + init() { + maxHeap = [] + } + + /* 获取左子结点索引 */ + func left(i: Int) -> Int { + 2 * i + 1 + } + + /* 获取右子结点索引 */ + func right(i: Int) -> Int { + 2 * i + 2 + } + + /* 获取父结点索引 */ + func parent(i: Int) -> Int { + (i - 1) / 2 // 向下整除 + } + ``` + +=== "Zig" + + ```zig title="my_heap.zig" + + ``` + +### 访问堆顶元素 + +堆顶元素是二叉树的根结点,即列表首元素。 + +=== "Java" + + ```java title="my_heap.java" + /* 访问堆顶元素 */ + public int peek() { + return maxHeap.get(0); + } + ``` + +=== "C++" + + ```cpp title="my_heap.cpp" + /* 访问堆顶元素 */ + int peek() { + return maxHeap[0]; + } + ``` + +=== "Python" + + ```python title="my_heap.py" + + ``` + +=== "Go" + + ```go title="my_heap.go" + /* 访问堆顶元素 */ + func (h *maxHeap) peek() any { + return h.data[0] + } + ``` + +=== "JavaScript" + + ```js title="my_heap.js" + + ``` + +=== "TypeScript" + + ```typescript title="my_heap.ts" + + ``` + +=== "C" + + ```c title="my_heap.c" + + ``` + +=== "C#" + + ```csharp title="my_heap.cs" + + ``` + +=== "Swift" + + ```swift title="my_heap.swift" + /* 访问堆顶元素 */ + func peek() -> Int { + maxHeap[0] + } + ``` + +=== "Zig" + + ```zig title="my_heap.zig" + + ``` + +### 元素入堆 + +给定元素 `val` ,我们先将其添加到堆底。添加后,由于 `val` 可能大于堆中其它元素,此时堆的成立条件可能已经被破坏,**因此需要修复从插入结点到根结点这条路径上的各个结点**,该操作被称为「堆化 Heapify」。 + +考虑从入堆结点开始,**从底至顶执行堆化**。具体地,比较插入结点与其父结点的值,若插入结点更大则将它们交换;并循环以上操作,从底至顶地修复堆中的各个结点;直至越过根结点时结束,或当遇到无需交换的结点时提前结束。 + +=== "Step 1" + ![heap_push_step1](heap.assets/heap_push_step1.png) + +=== "Step 2" + ![heap_push_step2](heap.assets/heap_push_step2.png) + +=== "Step 3" + ![heap_push_step3](heap.assets/heap_push_step3.png) + +=== "Step 4" + ![heap_push_step4](heap.assets/heap_push_step4.png) + +=== "Step 5" + ![heap_push_step5](heap.assets/heap_push_step5.png) + +=== "Step 6" + ![heap_push_step6](heap.assets/heap_push_step6.png) + +设结点总数为 $n$ ,则树的高度为 $O(\log n)$ ,易得堆化操作的循环轮数最多为 $O(\log n)$ ,**因而元素入堆操作的时间复杂度为 $O(\log n)$** 。 + +=== "Java" + + ```java title="my_heap.java" + /* 元素入堆 */ + void push(int val) { + // 添加结点 + maxHeap.add(val); + // 从底至顶堆化 + siftUp(size() - 1); + } + + /* 从结点 i 开始,从底至顶堆化 */ + void siftUp(int i) { + while (true) { + // 获取结点 i 的父结点 + int p = parent(i); + // 若“越过根结点”或“结点无需修复”,则结束堆化 + if (p < 0 || maxHeap.get(i) <= maxHeap.get(p)) + break; + // 交换两结点 + swap(i, p); + // 循环向上堆化 + i = p; + } + } + ``` + +=== "C++" + + ```cpp title="my_heap.cpp" + /* 元素入堆 */ + void push(int val) { + // 添加结点 + maxHeap.push_back(val); + // 从底至顶堆化 + shifUp(size() - 1); + } + + /* 从结点 i 开始,从底至顶堆化 */ + void shifUp(int i) { + while (true) { + // 获取结点 i 的父结点 + int p = parent(i); + // 当“越过根结点”或“结点无需修复”时,结束堆化 + if (p < 0 || maxHeap[i] <= maxHeap[p]) + break; + // 交换两结点 + swap(maxHeap[i], maxHeap[p]); + // 循环向上堆化 + i = p; + } + } + ``` + +=== "Python" + + ```python title="my_heap.py" + + ``` + +=== "Go" + + ```go title="my_heap.go" + /* 元素入堆 */ + func (h *maxHeap) push(val any) { + // 添加结点 + h.data = append(h.data, val) + // 从底至顶堆化 + h.siftUp(len(h.data) - 1) + } + + /* 从结点 i 开始,从底至顶堆化 */ + func (h *maxHeap) siftUp(i int) { + for true { + // 获取结点 i 的父结点 + p := h.parent(i) + // 当“越过根结点”或“结点无需修复”时,结束堆化 + if p < 0 || h.data[i].(int) <= h.data[p].(int) { + break + } + // 交换两结点 + h.swap(i, p) + // 循环向上堆化 + i = p + } + } + ``` + +=== "JavaScript" + + ```js title="my_heap.js" + + ``` + +=== "TypeScript" + + ```typescript title="my_heap.ts" + + ``` + +=== "C" + + ```c title="my_heap.c" + + ``` + +=== "C#" + + ```csharp title="my_heap.cs" + + ``` + +=== "Swift" + + ```swift title="my_heap.swift" + /* 元素入堆 */ + func push(val: Int) { + // 添加结点 + maxHeap.append(val) + // 从底至顶堆化 + siftUp(i: size() - 1) + } + + /* 从结点 i 开始,从底至顶堆化 */ + func siftUp(i: Int) { + var i = i + while true { + // 获取结点 i 的父结点 + let p = parent(i: i) + // 当“越过根结点”或“结点无需修复”时,结束堆化 + if p < 0 || maxHeap[i] <= maxHeap[p] { + break + } + // 交换两结点 + swap(i: i, j: p) + // 循环向上堆化 + i = p + } + } + ``` + +=== "Zig" + + ```zig title="my_heap.zig" + + ``` + +### 堆顶元素出堆 + +堆顶元素是二叉树根结点,即列表首元素,如果我们直接将首元素从列表中删除,则二叉树中所有结点都会随之发生移位(索引发生变化),这样后续使用堆化修复就很麻烦了。为了尽量减少元素索引变动,采取以下操作步骤: + +1. 交换堆顶元素与堆底元素(即交换根结点与最右叶结点); +2. 交换完成后,将堆底从列表中删除(注意,因为已经交换,实际上删除的是原来的堆顶元素); +3. 从根结点开始,**从顶至底执行堆化**; + +顾名思义,**从顶至底堆化的操作方向与从底至顶堆化相反**,我们比较根结点的值与其两个子结点的值,将最大的子结点与根结点执行交换,并循环以上操作,直到越过叶结点时结束,或当遇到无需交换的结点时提前结束。 + +=== "Step 1" + ![heap_poll_step1](heap.assets/heap_poll_step1.png) + +=== "Step 2" + ![heap_poll_step2](heap.assets/heap_poll_step2.png) + +=== "Step 3" + ![heap_poll_step3](heap.assets/heap_poll_step3.png) + +=== "Step 4" + ![heap_poll_step4](heap.assets/heap_poll_step4.png) + +=== "Step 5" + ![heap_poll_step5](heap.assets/heap_poll_step5.png) + +=== "Step 6" + ![heap_poll_step6](heap.assets/heap_poll_step6.png) + +=== "Step 7" + ![heap_poll_step7](heap.assets/heap_poll_step7.png) + +=== "Step 8" + ![heap_poll_step8](heap.assets/heap_poll_step8.png) + +=== "Step 9" + ![heap_poll_step9](heap.assets/heap_poll_step9.png) + +=== "Step 10" + ![heap_poll_step10](heap.assets/heap_poll_step10.png) + +与元素入堆操作类似,**堆顶元素出堆操作的时间复杂度为 $O(\log n)$** 。 + +=== "Java" + + ```java title="my_heap.java" + /* 元素出堆 */ + int poll() { + // 判空处理 + if (isEmpty()) + throw new EmptyStackException(); + // 交换根结点与最右叶结点(即交换首元素与尾元素) + swap(0, size() - 1); + // 删除结点 + int val = maxHeap.remove(size() - 1); + // 从顶至底堆化 + siftDown(0); + // 返回堆顶元素 + return val; + } + + /* 从结点 i 开始,从顶至底堆化 */ + void siftDown(int i) { + while (true) { + // 判断结点 i, l, r 中值最大的结点,记为 ma + int l = left(i), r = right(i), ma = i; + if (l < size() && maxHeap.get(l) > maxHeap.get(ma)) + ma = l; + if (r < size() && maxHeap.get(r) > maxHeap.get(ma)) + ma = r; + // 若“结点 i 最大”或“越过叶结点”,则结束堆化 + if (ma == i) break; + // 交换两结点 + swap(i, ma); + // 循环向下堆化 + i = ma; + } + } + ``` + +=== "C++" + + ```cpp title="my_heap.cpp" + /* 从结点 i 开始,从顶至底堆化 */ + void shifDown(int i) { + while (true) { + // 判断结点 i, l, r 中值最大的结点,记为 ma + int l = left(i), r = right(i), ma = i; + // 若结点 i 最大或索引 l, r 越界,则无需继续堆化,跳出 + if (l < size() && maxHeap[l] > maxHeap[ma]) + ma = l; + if (r < size() && maxHeap[r] > maxHeap[ma]) + ma = r; + // 若结点 i 最大或索引 l, r 越界,则无需继续堆化,跳出 + if (ma == i) + break; + swap(maxHeap[i], maxHeap[ma]); + // 循环向下堆化 + i = ma; + } + } + + /* 元素出堆 */ + void poll() { + // 判空处理 + if (empty()) { + cout << "Error:堆为空" << endl; + return; + } + // 交换根结点与最右叶结点(即交换首元素与尾元素) + swap(maxHeap[0], maxHeap[size() - 1]); + // 删除结点 + maxHeap.pop_back(); + // 从顶至底堆化 + shifDown(0); + } + ``` + +=== "Python" + + ```python title="my_heap.py" + + ``` + +=== "Go" + + ```go title="my_heap.go" + /* 元素出堆 */ + func (h *maxHeap) poll() any { + // 判空处理 + if h.isEmpty() { + fmt.Println("error") + return nil + } + // 交换根结点与最右叶结点(即交换首元素与尾元素) + h.swap(0, h.size()-1) + // 删除结点 + val := h.data[len(h.data)-1] + h.data = h.data[:len(h.data)-1] + // 从顶至底堆化 + h.siftDown(0) + + // 返回堆顶元素 + return val + } + + /* 从结点 i 开始,从顶至底堆化 */ + func (h *maxHeap) siftDown(i int) { + for true { + // 判断结点 i, l, r 中值最大的结点,记为 max + l, r, max := h.left(i), h.right(i), i + if l < h.size() && h.data[l].(int) > h.data[max].(int) { + max = l + } + if r < h.size() && h.data[r].(int) > h.data[max].(int) { + max = r + } + // 若结点 i 最大或索引 l, r 越界,则无需继续堆化,跳出 + if max == i { + break + } + // 交换两结点 + h.swap(i, max) + // 循环向下堆化 + i = max + } + } + ``` + +=== "JavaScript" + + ```js title="my_heap.js" + + ``` + +=== "TypeScript" + + ```typescript title="my_heap.ts" + + ``` + +=== "C" + + ```c title="my_heap.c" + + ``` + +=== "C#" + + ```csharp title="my_heap.cs" + + ``` + +=== "Swift" + + ```swift title="my_heap.swift" + /* 元素出堆 */ + func poll() -> Int { + // 判空处理 + if isEmpty() { + fatalError("堆为空") + } + // 交换根结点与最右叶结点(即交换首元素与尾元素) + swap(i: 0, j: size() - 1) + // 删除结点 + let val = maxHeap.remove(at: size() - 1) + // 从顶至底堆化 + siftDown(i: 0) + // 返回堆顶元素 + return val + } + + /* 从结点 i 开始,从顶至底堆化 */ + func siftDown(i: Int) { + var i = i + while true { + // 判断结点 i, l, r 中值最大的结点,记为 ma + let l = left(i: i) + let r = right(i: i) + var ma = i + if l < size(), maxHeap[l] > maxHeap[ma] { + ma = l + } + if r < size(), maxHeap[r] > maxHeap[ma] { + ma = r + } + // 若结点 i 最大或索引 l, r 越界,则无需继续堆化,跳出 + if ma == i { + break + } + // 交换两结点 + swap(i: i, j: ma) + // 循环向下堆化 + i = ma + } + } + ``` + +=== "Zig" + + ```zig title="my_heap.zig" + + ``` + +### 输入数据并建堆 * + +如果我们想要直接输入一个列表并将其建堆,那么该怎么做呢?最直接地,考虑使用「元素入堆」方法,将列表元素依次入堆。元素入堆的时间复杂度为 $O(n)$ ,而平均长度为 $\frac{n}{2}$ ,因此该方法的总体时间复杂度为 $O(n \log n)$ 。 + +然而,存在一种更加优雅的建堆方法。设结点数量为 $n$ ,我们先将列表所有元素原封不动添加进堆,**然后迭代地对各个结点执行「从顶至底堆化」**。当然,**无需对叶结点执行堆化**,因为其没有子结点。 + +=== "Java" + + ```java title="my_heap.java" + /* 构造函数,根据输入列表建堆 */ + public MaxHeap(List nums) { + // 将列表元素原封不动添加进堆 + maxHeap = new ArrayList<>(nums); + // 堆化除叶结点以外的其他所有结点 + for (int i = parent(size() - 1); i >= 0; i--) { + siftDown(i); + } + } + ``` + +=== "C++" + + ```cpp title="my_heap.cpp" + /* 构造函数,根据输入列表建堆 */ + MaxHeap(vector nums) { + // 将列表元素原封不动添加进堆 + maxHeap = nums; + // 堆化除叶结点以外的其他所有结点 + for (int i = parent(size() - 1); i >= 0; i--) { + shifDown(i); + } + } + ``` + +=== "Python" + + ```python title="my_heap.py" + + ``` + +=== "Go" + + ```go title="my_heap.go" + /* 构造函数,根据切片建堆 */ + func newMaxHeap(nums []any) *maxHeap { + // 将列表元素原封不动添加进堆 + h := &maxHeap{data: nums} + // 堆化除叶结点以外的其他所有结点 + for i := len(h.data) - 1; i >= 0; i-- { + h.siftDown(i) + } + return h + } + ``` + +=== "JavaScript" + + ```js title="my_heap.js" + + ``` + +=== "TypeScript" + + ```typescript title="my_heap.ts" + + ``` + +=== "C" + + ```c title="my_heap.c" + + ``` + +=== "C#" + + ```csharp title="my_heap.cs" + + ``` + +=== "Swift" + + ```swift title="my_heap.swift" + /* 构造函数,根据输入列表建堆 */ + init(nums: [Int]) { + // 将列表元素原封不动添加进堆 + maxHeap = nums + // 堆化除叶结点以外的其他所有结点 + for i in stride(from: parent(i: size() - 1), through: 0, by: -1) { + siftDown(i: i) + } + } + ``` + +=== "Zig" + + ```zig title="my_heap.zig" + + ``` + +那么,第二种建堆方法的时间复杂度时多少呢?我们来做一下简单推算。 + +- 完全二叉树中,设结点总数为 $n$ ,则叶结点数量为 $(n + 1) / 2$ ,其中 $/$ 为向下整除。因此在排除叶结点后,需要堆化结点数量为 $(n - 1)/2$ ,即为 $O(n)$ ; +- 从顶至底堆化中,每个结点最多堆化至叶结点,因此最大迭代次数为二叉树高度 $O(\log n)$ ; + +将上述两者相乘,可得时间复杂度为 $O(n \log n)$ 。然而,该估算结果仍不够准确,因为我们没有考虑到 **二叉树底层结点远多于顶层结点** 的性质。 + +下面我们来尝试展开计算。为了减小计算难度,我们假设树是一个「完美二叉树」,该假设不会影响计算结果的正确性。设二叉树(即堆)结点数量为 $n$ ,树高度为 $h$ 。上文提到,**结点堆化最大迭代次数等于该结点到叶结点的距离,而这正是“结点高度”**。因此,我们将各层的“结点数量 $\times$ 结点高度”求和,即可得到所有结点的堆化的迭代次数总和。 + +$$ +T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \cdots + 2^{(h-1)}\times1 +$$ + +![heapify_count](heap.assets/heapify_count.png) + +化简上式需要借助中学的数列知识,先对 $T(h)$ 乘以 $2$ ,易得 + +$$ +\begin{aligned} +T(h) & = 2^0h + 2^1(h-1) + 2^2(h-2) + \cdots + 2^{h-1}\times1 \newline +2 T(h) & = 2^1h + 2^2(h-1) + 2^3(h-2) + \cdots + 2^{h}\times1 \newline +\end{aligned} +$$ + +**使用错位相减法**,令下式 $2 T(h)$ 减去上式 $T(h)$ ,可得 + +$$ +2T(h) - T(h) = T(h) = -2^0h + 2^1 + 2^2 + \cdots + 2^{h-1} + 2^h +$$ + +观察上式,$T(h)$ 是一个等比数列,可直接使用求和公式,得到时间复杂度为 + +$$ +\begin{aligned} +T(h) & = 2 \frac{1 - 2^h}{1 - 2} - h \newline +& = 2^{h+1} - h \newline +& = O(2^h) +\end{aligned} +$$ + +进一步地,高度为 $h$ 的完美二叉树的结点数量为 $n = 2^{h+1} - 1$ ,易得复杂度为 $O(2^h) = O(n)$。以上推算表明,**输入列表并建堆的时间复杂度为 $O(n)$ ,非常高效**。 + +## 8.1.4. 堆常见应用 + +- **优先队列**。堆常作为实现优先队列的首选数据结构,入队和出队操作时间复杂度为 $O(\log n)$ ,建队操作为 $O(n)$ ,皆非常高效。 +- **堆排序**。给定一组数据,我们使用其建堆,并依次全部弹出,则可以得到有序的序列。当然,堆排序一般无需弹出元素,仅需每轮将堆顶元素交换至数组尾部并减小堆的长度即可。 +- **获取最大的 $k$ 个元素**。这既是一道经典算法题目,也是一种常见应用,例如选取热度前 10 的新闻作为微博热搜,选取前 10 销量的商品等。 diff --git a/build/chapter_introduction/algorithms_are_everywhere.md b/build/chapter_introduction/algorithms_are_everywhere.md new file mode 100644 index 000000000..980fba81e --- /dev/null +++ b/build/chapter_introduction/algorithms_are_everywhere.md @@ -0,0 +1,38 @@ +--- +comments: true +--- + +# 1.1. 算法无处不在 + +听到“算法”这个词,我们一般会联想到数学。但实际上,大多数算法并不包含复杂的数学,而更像是在考察基本逻辑,而这些逻辑在我们日常生活中处处可见。 + +在正式介绍算法之前,我想告诉你一件有趣的事:**其实,你在过去已经学会了很多算法,并且已经习惯将它们应用到日常生活中**。接下来,我将介绍两个具体例子来佐证。 + +**例一:拼积木**。一套积木,除了有许多部件之外,还会附送详细的拼装说明书。我们按照说明书上一步步操作,即可拼出复杂的积木模型。 + +如果从数据结构与算法的角度看,大大小小的「积木」就是数据结构,而「拼装说明书」上的一系列步骤就是算法。 + +**例二:查字典**。在字典中,每个汉字都有一个对应的拼音,而字典是按照拼音的英文字母表顺序排列的。假设需要在字典中查询任意一个拼音首字母为 $r$ 的字,一般我们会这样做: + +1. 打开字典大致一半页数的位置,查看此页的首字母是什么(假设为 $m$ ); +2. 由于在英文字母表中 $r$ 在 $m$ 的后面,因此应排除字典前半部分,查找范围仅剩后半部分; +3. 循环执行步骤 1-2 ,直到找到拼音首字母为 $r$ 的页码时终止。 + +=== "Step 1" + ![look_up_dictionary_step_1](algorithms_are_everywhere.assets/look_up_dictionary_step_1.png) +=== "Step 2" + ![look_up_dictionary_step_2](algorithms_are_everywhere.assets/look_up_dictionary_step_2.png) +=== "Step 3" + ![look_up_dictionary_step_3](algorithms_are_everywhere.assets/look_up_dictionary_step_3.png) +=== "Step 4" + ![look_up_dictionary_step_4](algorithms_are_everywhere.assets/look_up_dictionary_step_4.png) +=== "Step 5" + ![look_up_dictionary_step_5](algorithms_are_everywhere.assets/look_up_dictionary_step_5.png) + +查字典这个小学生的标配技能,实际上就是大名鼎鼎的「二分查找」。从数据结构角度,我们可以将字典看作是一个已排序的「数组」;而从算法角度,我们可将上述查字典的一系列指令看作是「二分查找」算法。 + +小到烹饪一道菜、大到星际航行,几乎所有问题的解决都离不开算法。计算机的出现,使我们可以通过编程将数据结构存储在内存中,也可以编写代码来调用 CPU, GPU 执行算法,从而将生活中的问题搬运到计算机中,更加高效地解决各式各样的复杂问题。 + +!!! tip + + 读到这里,如果你感到对数据结构、算法、数组、二分查找等此类概念一知半解,那么就太好了!因为这正是本书存在的价值,接下来,本书将会一步步地引导你进入数据结构与算法的知识殿堂。 diff --git a/build/chapter_introduction/what_is_dsa.md b/build/chapter_introduction/what_is_dsa.md new file mode 100644 index 000000000..d104e4852 --- /dev/null +++ b/build/chapter_introduction/what_is_dsa.md @@ -0,0 +1,53 @@ +--- +comments: true +--- + +# 1.2. 算法是什么 + +## 1.2.1. 算法定义 + +「算法 Algorithm」是在有限时间内解决特定问题的一组指令或操作步骤。算法具有以下特性: + +- 问题是明确的,需要拥有明确的输入和输出定义。 +- 解具有确定性,即给定相同输入时,输出一定相同。 +- 具有可行性,可在有限步骤、有限时间、有限内存空间下完成。 +- 独立于编程语言,即可用多种语言实现。 + +## 1.2.2. 数据结构定义 + +「数据结构 Data Structure」是在计算机中组织与存储数据的方式。为了提高数据存储和操作性能,数据结构的设计原则有: + +- 空间占用尽可能小,节省计算机内存。 +- 数据操作尽量快,包括数据访问、添加、删除、更新等。 +- 提供简洁的数据表示和逻辑信息,以便算法高效运行。 + +数据结构的设计是一个充满权衡的过程,这意味着如果获得某方面的优势,则往往需要在另一方面做出妥协。例如,链表相对于数组,数据添加删除操作更加方便,但牺牲了数据的访问速度;图相对于链表,提供了更多的逻辑信息,但需要占用更多的内存空间。 + +## 1.2.3. 数据结构与算法的关系 + +「数据结构」与「算法」是高度相关、紧密嵌合的,体现在: + +- 数据结构是算法的底座。数据结构为算法提供结构化存储的数据,以及操作数据的对应方法。 +- 算法是发挥数据结构优势的舞台。数据结构仅存储数据信息,结合算法才可解决特定问题。 +- 算法有对应最优的数据结构。给定算法,一般可基于不同的数据结构实现,而最终执行效率往往相差很大。 + +![relationship_between_data_structure_and_algorithm](what_is_dsa.assets/relationship_between_data_structure_and_algorithm.png) + +

Fig. 数据结构与算法的关系

+ +如果将「LEGO 乐高」类比到「数据结构与算法」,那么可以得到下表所示的对应关系。 + +
+ +| 数据结构与算法 | LEGO 乐高 | +| -------------- | ---------------------------------------- | +| 输入数据 | 未拼装的积木 | +| 数据结构 | 积木组织形式,包括形状、大小、连接方式等 | +| 算法 | 把积木拼成目标形态的一系列操作步骤 | +| 输出数据 | 积木模型 | + +
+ +!!! tip "约定俗成的简称" + + 在实际讨论中,我们通常会将「数据结构与算法」直接简称为「算法」。例如,我们熟称的 LeetCode 算法题目,实际上同时考察了数据结构和算法两部分知识。 diff --git a/build/chapter_preface/about_the_book.md b/build/chapter_preface/about_the_book.md new file mode 100644 index 000000000..12e5b7e93 --- /dev/null +++ b/build/chapter_preface/about_the_book.md @@ -0,0 +1,265 @@ +--- +comments: true +--- + +# 0.1. 关于本书 + +五年前发生的一件事,成为了我职业生涯的重要转折点。当时的我在交大读研,对互联网求职一无所知,但仍然硬着头皮申请了 Microsoft 软件工程师实习。面试官让我在白板上写出“快速排序”代码,我畏畏缩缩地写了一个“冒泡排序”,并且还写错了` (ToT) ` 。从面试官的表情上,我看到了一个大大的 "GG" 。 + +此次失利倒逼我开始刷算法题。我采用“扫雷游戏”式的学习方法,两眼一抹黑刷题,扫到不会的“雷”就通过查资料把它“排掉”,配合周期性总结,逐渐形成了数据结构与算法的知识图景。幸运地,我在秋招斩获了多家大厂的 Offer 。 + +回想自己当初在“扫雷式”刷题中被炸的满头包的痛苦,思考良久,我意识到一本“前期刷题必看”的读物可以使算法小白少走许多弯路。写作意愿滚滚袭来,那就动笔吧: + +

Hello,算法!

+ +## 0.1.1. 读者对象 + +!!! success "前置条件" + + 您需要至少具备任一语言的编程基础,能够阅读和编写简单代码。 + +如果您是 **算法初学者**,完全没有接触过算法,或者已经有少量刷题,对数据结构与算法有朦胧的理解,在会与不会之间反复横跳,那么这本书就是为您而写!本书能够带来: + +- 了解刷题所需的 **数据结构**,包括常用操作、优势和劣势、典型应用、实现方法等。 +- 学习各类 **算法**,介绍算法的设计思想、运行效率、优势劣势、实现方法等。 +- 可一键运行的 **配套代码**,包含详细注释,帮助你通过实践加深理解。 + +如果您是 **算法熟练工**,已经积累一定刷题量,接触过大多数题型,那么本书内容对你来说可能稍显基础,但仍能够带来以下价值: + +- 本书篇幅不长,可以帮助你提纲挈领地回顾算法知识。 +- 书中包含许多对比性、总结性的算法内容,可以帮助你梳理算法知识体系。 +- 源代码实现了各种经典数据结构和算法,可以作为“刷题工具库”来使用。 + +如果您是 **算法大佬**,请受我膜拜!希望您可以抽时间提出意见建议,或者[一起参与创作](https://www.hello-algo.com/chapter_preface/contribution/),帮助各位同学获取更好的学习内容,感谢! + +## 0.1.2. 内容结构 + +本书主要内容分为复杂度分析、数据结构、算法三个部分。 + +![mindmap](about_the_book.assets/mindmap.png) + +

Fig. 知识点思维导图

+ +### 复杂度分析 + +首先介绍数据结构与算法的评价维度、算法效率的评估方法,引出了计算复杂度概念。 + +接下来,从 **函数渐近上界** 入手,分别介绍了 **时间复杂度** 和 **空间复杂度**,包括推算方法、常见类型、示例等。同时,剖析了 **最差、最佳、平均** 时间复杂度的联系与区别。 + +### 数据结构 + +首先介绍了常用的 **基本数据类型** 、以及它们是如何在内存中存储的。 + +接下来,介绍了两种 **数据结构分类方法**,包括逻辑结构与物理结构。 + +后续展开介绍了 **数组、链表、栈、队列、散列表、树、堆、图** 等数据结构,关心以下内容: + +- 基本定义:数据结构的设计来源、存在意义; +- 主要特点:在各项数据操作中的优势、劣势; +- 常用操作:例如访问、更新、插入、删除、遍历、搜索等; +- 常见类型:在算法题或工程实际中,经常碰到的数据结构类型; +- 典型应用:此数据结构经常搭配哪些算法使用; +- 实现方法:对于重要的数据结构,将给出完整的实现示例; + +### 算法 + +包括 **查找算法、排序算法、搜索与回溯、动态规划、分治算法**,内容包括: + +- 基本定义:算法的设计思想; +- 主要特点:使用前置条件、优势和劣势; +- 算法效率:最差和平均时间复杂度、空间复杂度; +- 实现方法:完整的算法实现,以及优化措施; +- 示例题目:结合例题加深理解; + +## 0.1.3. 配套代码 + +完整代码托管在 [GitHub 仓库](https://github.com/krahets/hello-algo) ,皆可一键运行。 + +!!! tip "前置工作" + + 1. [编程环境安装](https://www.hello-algo.com/chapter_preface/installation/) ,若有请跳过 + 2. 代码下载与使用方法请见 [如何使用本书](https://www.hello-algo.com/chapter_preface/suggestions/#_4) + +## 0.1.4. 风格约定 + +- 标题后标注 * 符号的是选读章节,如果你的时间有限,可以先跳过这些章节。 +- 文章中的重要名词会用「」符号标注,例如「数组 Array」。名词混淆会导致不必要的歧义,因此最好可以记住这类名词(包括中文和英文),以便后续阅读文献时使用。 +- 重点内容、总起句、总结句会被 **加粗**,此类文字值得特别关注。 +- 专有名词和有特指含义的词句会使用 “ ” 标注,以避免歧义。 +- 在工程应用中,每种语言都有注释规范;而本书放弃了一部分的注释规范性,以换取更加紧凑的内容排版。注释主要分为三种类型:标题注释、内容注释、多行注释。 + +=== "Java" + + ```java title="" + /* 标题注释,用于标注函数、类、测试样例等 */ + + // 内容注释,用于详解代码 + + /** + * 多行 + * 注释 + */ + ``` + +=== "C++" + + ```cpp title="" + /* 标题注释,用于标注函数、类、测试样例等 */ + + // 内容注释,用于详解代码 + + /** + * 多行 + * 注释 + */ + ``` + +=== "Python" + + ```python title="" + """ 标题注释,用于标注函数、类、测试样例等 """ + + # 内容注释,用于详解代码 + + """ + 多行 + 注释 + """ + ``` + +=== "Go" + + ```go title="" + /* 标题注释,用于标注函数、类、测试样例等 */ + + // 内容注释,用于详解代码 + + /** + * 多行 + * 注释 + */ + ``` + +=== "JavaScript" + + ```js title="" + /* 标题注释,用于标注函数、类、测试样例等 */ + + // 内容注释,用于详解代码 + + /** + * 多行 + * 注释 + */ + ``` + +=== "TypeScript" + + ```typescript title="" + /* 标题注释,用于标注函数、类、测试样例等 */ + + // 内容注释,用于详解代码 + + /** + * 多行 + * 注释 + */ + ``` + +=== "C" + + ```c title="" + /* 标题注释,用于标注函数、类、测试样例等 */ + + // 内容注释,用于详解代码 + + /** + * 多行 + * 注释 + */ + ``` + +=== "C#" + + ```csharp title="" + /* 标题注释,用于标注函数、类、测试样例等 */ + + // 内容注释,用于详解代码 + + /** + * 多行 + * 注释 + */ + ``` + +=== "Swift" + + ```swift title="" + /* 标题注释,用于标注函数、类、测试样例等 */ + + // 内容注释,用于详解代码 + + /** + * 多行 + * 注释 + */ + ``` + +=== "Zig" + + ```zig title="" + // 标题注释,用于标注函数、类、测试样例等 + + // 内容注释,用于详解代码 + + // 多行 + // 注释 + ``` + +## 0.1.5. 本书特点 * + +??? abstract "默认折叠,可以跳过" + + **以实践为主**。我们知道,学习英语期间光啃书本是远远不够的,需要多听、多说、多写,在实践中培养语感、积累经验。编程语言也是一门语言,因此学习方法也应是类似的,需要多看优秀代码、多敲键盘、多思考代码逻辑。 + + 本书的理论部分占少量篇幅,主要分为两类:一是基础且必要的概念知识,以培养读者对于算法的感性认识;二是重要的分类、对比或总结,这是为了帮助你站在更高视角俯瞰各个知识点,形成连点成面的效果。 + + 实践部分主要由示例和代码组成。代码配有简要注释,复杂示例会尽可能地使用视觉化的形式呈现。我强烈建议读者对照着代码自己敲一遍,如果时间有限,也至少逐行读、复制并运行一遍,配合着讲解将代码吃透。 + + **视觉化学习**。信息时代以来,视觉化的脚步从未停止。媒体形式经历了文字短信、图文 Email 、动图、短(长)视频、交互式 Web 、3D 游戏等演变过程,信息的视觉化程度越来越高、愈加符合人类感官、信息传播效率大大提升。科技界也在向视觉化迈进,iPhone 就是一个典型例子,其相对于传统手机是高度视觉化的,包含精心设计的字体、主题配色、交互动画等。 + + 近两年,短视频成为最受欢迎的信息媒介,可以在短时间内将高密度的信息“灌”给我们,有着极其舒适的观看体验。阅读则不然,读者与书本之间天然存在一种“疏离感”,我们看书会累、会走神、会停下来想其他事、会划下喜欢的句子、会思考某一片段的含义,这种疏离感给了读者与书本之间对话的可能,拓宽了想象空间。 + + 本书作为一本入门教材,希望可以保有书本的“慢节奏”,但也会避免与读者产生过多“疏离感”,而是努力将知识完整清晰地推送到你聪明的小脑袋瓜中。我将采用视觉化的方式(例如配图、动画),尽我可能清晰易懂地讲解复杂概念和抽象示例。 + + **内容精简化**。大多数的经典教科书,会把每个主题都讲的很透彻。虽然透彻性正是其获得读者青睐的原因,但对于想要快速入门的初学者来说,这些教材的实用性不足。本书会避免引入非必要的概念、名词、定义等,也避免展开不必要的理论分析,毕竟这不是一本真正意义上的教材,主要任务是尽快地带领读者入门。 + + 引入一些生活案例或趣味内容,非常适合作为知识点的引子或者解释的补充,但当融入过多额外元素时,内容会稍显冗长,也许反而使读者容易迷失、抓不住重点,这也是本书需要避免的。 + + 敲代码如同写字,“美”是统一的追求。本书力求美观的代码,保证规范的变量命名、统一的空格与换行、对齐的缩进、整齐的注释等。 + +## 0.1.6. 致谢 + +本书的成书过程中,我获得了许多人的帮助,包括但不限于: + +- 感谢我的女朋友泡泡担任本书的首位读者,从算法小白的视角为本书的写作提出了许多建议,使这本书更加适合算法初学者来阅读。 +- 感谢腾宝、琦宝、飞宝为本书起了个响当当的名字,好听又有梗,直接唤起我最初敲下第一行代码 "Hello, World!" 的回忆。 +- 感谢我的导师李博,在小酌畅谈时您告诉我“觉得适合、想做就去做”,坚定了我写这本书的决心。 +- 感谢苏潼为本书设计了封面和 LOGO ,我有些强迫症,前后多次修改,谢谢你的耐心。 +- 感谢 @squidfunk ,包括 [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material/tree/master) 顶级开源项目以及给出的写作排版建议。 + +在写作过程中,我阅读了许多与数据结构与算法的书籍材料,学习到了许多知识,感谢前辈们的精彩创作。 + +感谢父母,你们一贯的支持与鼓励给了我自由度来做这些有趣的事。 + +## 0.1.7. 作者简介 + +![profile](about_the_book.assets/profile.png){: .center} + +

Krahets

+ +
大厂高级算法工程师、算法爱好者
+ +

力扣(LeetCode)全网阅读量最高博主

+

分享近百道算法题解,累积回复数千读者的评论问题

+

创作 LeetBook《图解算法数据结构》,已免费售出 22 万本

diff --git a/build/chapter_preface/contribution.md b/build/chapter_preface/contribution.md new file mode 100644 index 000000000..073c3f5f4 --- /dev/null +++ b/build/chapter_preface/contribution.md @@ -0,0 +1,65 @@ +--- +comments: true +--- + +# 0.4. 一起参与创作 + +!!! success "开源的魅力" + + 纸质书籍的两次印刷的间隔时间往往需要数年,内容更新非常不方便。
但在本开源 HTML 书中,内容更迭的时间被缩短至数日甚至几个小时。 + +由于作者水平有限,书中内容难免疏漏谬误,请您谅解。此外,期待您可以一同参与本书的创作。如果发现笔误、无效链接、内容缺失、文字歧义、解释不清晰、行文结构不合理等问题,烦请您修正内容,以帮助其他读者获取更优质的学习内容。所有 [撰稿人](https://github.com/krahets/hello-algo/graphs/contributors) 将被展示在仓库主页,以感谢您对开源社区的无私奉献。 + +## 0.4.1. 修改文字与代码 + +每个页面的右上角都有一个「编辑」按钮,你可以按照以下步骤修改文章: + +1. 点击编辑按钮,如果遇到提示“需要 Fork 此仓库”,请通过; +2. 修改 Markdown 源文件内容; +3. 在页面底部填写更改说明,然后单击“Propose file change”按钮; +4. 页面跳转后,点击“Create pull request”按钮发起拉取请求即可,我会第一时间查看处理并及时更新内容。 + +![edit_markdown](contribution.assets/edit_markdown.png) + +## 0.4.2. 修改图片与动画 + +书中的配图无法直接修改,需要通过以下途径提出修改意见: + +1. 新建一个 Issue ,将需要修改的图片复制或截图,粘贴在面板中; +2. 描述图片问题,应如何修改; +3. 提交 Issue 即可,我会第一时间重新画图并替换图片。 + +## 0.4.3. 创作新内容 + +如果您想要创作新内容,例如 **重写章节、新增章节、修改代码、翻译代码至其他编程语言** 等,那么需要实施 Pull Request 工作流程: + +1. 登录 GitHub ,并 Fork [本仓库](https://github.com/krahets/hello-algo) 至个人账号; +2. 进入 Fork 仓库网页,使用 `git clone` 克隆该仓库至本地; +3. 在本地进行内容创作(建议通过运行测试来验证代码正确性); +4. 将本地更改 Commit ,并 Push 至远程仓库; +5. 刷新仓库网页,点击“Create pull request”按钮发起拉取请求(Pull Request)即可; + +非常欢迎您和我一同来创作本书! + +## 0.4.4. 本地部署 hello-algo + +### Docker + +请确保 Docker 已经安装并启动,并根据如下命令离线部署。 + +稍等片刻,即可使用浏览器打开 `http://localhost:8000` 访问本项目。 + +```bash +git clone https://github.com/krahets/hello-algo.git +cd hello-algo + +docker-compose up -d +``` + +使用如下命令即可删除部署。 + +```bash +docker-compose down +``` + +(TODO:教学视频) diff --git a/build/chapter_preface/installation.md b/build/chapter_preface/installation.md new file mode 100644 index 000000000..adab95b21 --- /dev/null +++ b/build/chapter_preface/installation.md @@ -0,0 +1,52 @@ +--- +comments: true +--- + +# 0.3. 编程环境安装 + +(TODO 视频教程) + +## 0.3.1. 安装 VSCode + +本书推荐使用开源轻量的 VSCode 作为本地 IDE ,下载并安装 [VSCode](https://code.visualstudio.com/) 。 + +## 0.3.2. Java 环境 + +1. 下载并安装 [OpenJDK](https://jdk.java.net/18/)(版本需满足 > JDK 9)。 +2. 在 VSCode 的插件市场中搜索 `java` ,安装 Java Extension Pack 。 + +## 0.3.3. C/C++ 环境 + +1. Windows 系统需要安装 [MinGW](https://sourceforge.net/projects/mingw-w64/files/) ([配置教程](https://glj0.netlify.app/d-%E8%BD%AF%E4%BB%B6%E6%8A%80%E8%83%BD/windows%20%E4%B8%8B%E4%BD%BF%E7%94%A8%20vscode%20+%20mingw%20%E5%AE%8C%E6%88%90%E7%AE%80%E5%8D%95%20c%20%E6%88%96%20cpp%20%E4%BB%A3%E7%A0%81%E7%9A%84%E8%BF%90%E8%A1%8C%E4%B8%8E%E8%B0%83%E8%AF%95/)),MacOS 自带 Clang 无需安装。 +2. 在 VSCode 的插件市场中搜索 `c++` ,安装 C/C++ Extension Pack 。 + +## 0.3.4. Python 环境 + +1. 下载并安装 [Miniconda3](https://docs.conda.io/en/latest/miniconda.html) 。 +2. 在 VSCode 的插件市场中搜索 `python` ,安装 Python Extension Pack 。 + +## 0.3.5. Go 环境 + +1. 下载并安装 [go](https://go.dev/dl/) 。 +2. 在 VSCode 的插件市场中搜索 `go` ,安装 Go 。 +3. 快捷键 `Ctrl + Shift + P` 呼出命令栏,输入 go ,选择 `Go: Install/Update Tools` ,全部勾选并安装即可。 + +## 0.3.6. JavaScript 环境 + +1. 下载并安装 [node.js](https://nodejs.org/en/) 。 +2. 在 VSCode 的插件市场中搜索 `javascript` ,安装 JavaScript (ES6) code snippets 。 + +## 0.3.7. C# 环境 + +1. 下载并安装 [.Net 6.0](https://dotnet.microsoft.com/en-us/download) ; +2. 在 VSCode 的插件市场中搜索 `c#` ,安装 c# 。 + +## 0.3.8. Swift 环境 + +1. 下载并安装 [Swift](https://www.swift.org/download/); +2. 在 VSCode 的插件市场中搜索 `swift`,安装 [Swift for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=sswg.swift-lang)。 + +## 0.3.9. Rust 环境 + +1. 下载并安装 [Rust](https://www.rust-lang.org/tools/install); +2. 在 VSCode 的插件市场中搜索 `rust`,安装 [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)。 diff --git a/build/chapter_preface/suggestions.md b/build/chapter_preface/suggestions.md new file mode 100644 index 000000000..7907db787 --- /dev/null +++ b/build/chapter_preface/suggestions.md @@ -0,0 +1,63 @@ +--- +comments: true +--- + +# 0.2. 如何使用本书 + +## 0.2.1. 图文搭配学 + +视频和图片相比于文字的信息密度和结构化程度更高,更容易让人理解。在本书中,重点和难点知识会主要以动画、图解的形式呈现,而文字的作用则是作为动画和图的解释与补充。 + +在阅读本书的过程中,若发现某段内容提供了动画或图解,**建议你以图为主线**,将文字内容(一般在图的上方)对齐到图中内容,综合来理解。 + +![animation](suggestions.assets/animation.gif) + +## 0.2.2. 代码实践学 + +!!! tip "前置工作" + + 如果没有本地编程环境,可以参照下节 [编程环境安装](https://www.hello-algo.com/chapter_preface/installation/) 。 + +### 下载代码仓 + +如果已经安装 [Git](https://git-scm.com/downloads) ,可以通过命令行来克隆代码仓。 + +```shell +git clone https://github.com/krahets/hello-algo.git +``` + +当然,你也可以点击“Download ZIP”直接下载代码压缩包,解压即可。 + +![download_code](suggestions.assets/download_code.png) + +### 运行源代码 + +本书提供配套 Java, C++, Python 代码仓(后续可能拓展支持语言)。书中的代码栏上若标有 `*.java` , `*.cpp` , `*.py` ,则可在仓库 codes 文件夹中找到对应的 **代码源文件**。 + +![code_md_to_repo](suggestions.assets/code_md_to_repo.png) + +这些源文件中包含详细注释,配有测试样例,可以直接运行,帮助你省去不必要的调试时间,可以将精力集中在学习内容上。 + +![running_code](suggestions.assets/running_code.gif) + +!!! tip "代码学习建议" + + 若学习时间紧张,**请至少将所有代码通读并运行一遍**。若时间允许,**强烈建议对照着代码自己敲一遍**,逐渐锻炼肌肉记忆。相比于读代码,写代码的过程往往能带来新的收获。 + +## 0.2.3. 提问讨论学 + +阅读本书时,请不要“惯着”那些弄不明白的知识点。如果有任何疑惑,**可以在评论区留下你的问题**,小伙伴们和我都会给予解答(您一般 3 天内会得到回复)。 + +同时,也希望你可以多花时间逛逛评论区。一方面,可以看看大家遇到了什么问题,反过来查漏补缺,这往往可以引起更加深度的思考。另一方面,也希望你可以慷慨地解答小伙伴们的问题、分享自己的见解,大家一起加油与进步! + +![comment](suggestions.assets/comment.gif) + +## 0.2.4. 算法学习“三步走” + +**第一阶段,算法入门,也正是本书的定位**。熟悉各种数据结构的特点、用法,学习各种算法的工作原理、用途、效率等。 + +**第二阶段,刷算法题**。可以先从热门题单开刷,推荐 [剑指 Offer](https://leetcode.cn/problem-list/xb9nqhhg/)、[LeetCode 热题 HOT 100](https://leetcode.cn/problem-list/2cktkvj/) ,先积累至少 100 道题量,熟悉大多数的算法问题。刚开始刷题时,“遗忘”是最大的困扰点,但这是很正常的,请不要担心。学习中有一种概念叫“周期性回顾”,同一道题隔段时间做一次,当做了三遍以上,往往就能牢记于心了。 + +**第三阶段,搭建知识体系**。在学习方面,可以阅读算法专栏文章、解题框架、算法教材,不断地丰富知识体系。在刷题方面,可以开始采用进阶刷题方案,例如按专题分类、一题多解、一解多题等,刷题方案在社区中可以找到一些讲解,在此不做赘述。 + +![learning_route](suggestions.assets/learning_route.png) diff --git a/build/chapter_reference/index.md b/build/chapter_reference/index.md new file mode 100644 index 000000000..fcc5da56f --- /dev/null +++ b/build/chapter_reference/index.md @@ -0,0 +1,17 @@ +# 参考文献 + +[1] Thomas H. Cormen, et al. Introduction to Algorithms (3rd Edition). + +[2] Aditya Bhargava. Grokking Algorithms: An Illustrated Guide for Programmers and Other Curious People (1st Edition). + +[3] 程杰. 大话数据结构. + +[4] 王争. 数据结构与算法之美. + +[5] 严蔚敏. 数据结构( C 语言版). + +[6] 邓俊辉. 数据结构( C++ 语言版,第三版). + +[7] 马克·艾伦·维斯著,陈越译. 数据结构与算法分析:Java语言描述(第三版). + +[8] Gayle Laakmann McDowell. Cracking the Coding Interview: 189 Programming Questions and Solutions (6th Edition). diff --git a/build/chapter_searching/binary_search.md b/build/chapter_searching/binary_search.md new file mode 100755 index 000000000..9ac254cec --- /dev/null +++ b/build/chapter_searching/binary_search.md @@ -0,0 +1,555 @@ +--- +comments: true +--- + +# 10.2. 二分查找 + +「二分查找 Binary Search」利用数据的有序性,通过每轮缩小一半搜索区间来查找目标元素。 + +使用二分查找有两个前置条件: + +- **要求输入数据是有序的**,这样才能通过判断大小关系来排除一半的搜索区间; +- **二分查找仅适用于数组**,而在链表中使用效率很低,因为其在循环中需要跳跃式(非连续地)访问元素。 + +## 10.2.1. 算法实现 + +给定一个长度为 $n$ 的排序数组 `nums` ,元素从小到大排列。数组的索引取值范围为 + +$$ +0, 1, 2, \cdots, n-1 +$$ + +使用「区间」来表示这个取值范围的方法主要有两种: + +1. **双闭区间 $[0, n-1]$** ,即两个边界都包含自身;此方法下,区间 $[0, 0]$ 仍包含一个元素; +2. **左闭右开 $[0, n)$** ,即左边界包含自身、右边界不包含自身;此方法下,区间 $[0, 0)$ 为空; + +### “双闭区间”实现 + +首先,我们先采用“双闭区间”的表示,在数组 `nums` 中查找目标元素 `target` 的对应索引。 + +=== "Step 1" + ![binary_search_step1](binary_search.assets/binary_search_step1.png) + +=== "Step 2" + ![binary_search_step2](binary_search.assets/binary_search_step2.png) + +=== "Step 3" + ![binary_search_step3](binary_search.assets/binary_search_step3.png) + +=== "Step 4" + ![binary_search_step4](binary_search.assets/binary_search_step4.png) + +=== "Step 5" + ![binary_search_step5](binary_search.assets/binary_search_step5.png) + +=== "Step 6" + ![binary_search_step6](binary_search.assets/binary_search_step6.png) + +=== "Step 7" + ![binary_search_step7](binary_search.assets/binary_search_step7.png) + +二分查找“双闭区间”表示下的代码如下所示。 + +=== "Java" + + ```java title="binary_search.java" + /* 二分查找(双闭区间) */ + int binarySearch(int[] nums, int target) { + // 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素 + int i = 0, j = nums.length - 1; + // 循环,当搜索区间为空时跳出(当 i > j 时为空) + while (i <= j) { + int m = (i + j) / 2; // 计算中点索引 m + if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j] 中 + i = m + 1; + else if (nums[m] > target) // 此情况说明 target 在区间 [i, m-1] 中 + j = m - 1; + else // 找到目标元素,返回其索引 + return m; + } + // 未找到目标元素,返回 -1 + return -1; + } + ``` + +=== "C++" + + ```cpp title="binary_search.cpp" + /* 二分查找(双闭区间) */ + int binarySearch(vector& nums, int target) { + // 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素 + int i = 0, j = nums.size() - 1; + // 循环,当搜索区间为空时跳出(当 i > j 时为空) + while (i <= j) { + int m = (i + j) / 2; // 计算中点索引 m + if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j] 中 + i = m + 1; + else if (nums[m] > target) // 此情况说明 target 在区间 [i, m-1] 中 + j = m - 1; + else // 找到目标元素,返回其索引 + return m; + } + // 未找到目标元素,返回 -1 + return -1; + } + ``` + +=== "Python" + + ```python title="binary_search.py" + """ 二分查找(双闭区间) """ + def binary_search(nums, target): + # 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素 + i, j = 0, len(nums) - 1 + while i <= j: + m = (i + j) // 2 # 计算中点索引 m + if nums[m] < target: # 此情况说明 target 在区间 [m+1, j] 中 + i = m + 1 + elif nums[m] > target: # 此情况说明 target 在区间 [i, m-1] 中 + j = m - 1 + else: + return m # 找到目标元素,返回其索引 + return -1 # 未找到目标元素,返回 -1 + ``` + +=== "Go" + + ```go title="binary_search.go" + /* 二分查找(双闭区间) */ + func binarySearch(nums []int, target int) int { + // 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素 + i, j := 0, len(nums)-1 + // 循环,当搜索区间为空时跳出(当 i > j 时为空) + for i <= j { + m := (i + j) / 2 // 计算中点索引 m + if nums[m] < target { // 此情况说明 target 在区间 [m+1, j] 中 + i = m + 1 + } else if nums[m] > target { // 此情况说明 target 在区间 [i, m-1] 中 + j = m - 1 + } else { // 找到目标元素,返回其索引 + return m + } + } + // 未找到目标元素,返回 -1 + return -1 + } + ``` + +=== "JavaScript" + + ```js title="binary_search.js" + /* 二分查找(双闭区间) */ + function binarySearch(nums, target) { + // 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素 + let i = 0, j = nums.length - 1; + // 循环,当搜索区间为空时跳出(当 i > j 时为空) + while (i <= j) { + let m = parseInt((i + j) / 2); // 计算中点索引 m ,在 JS 中需使用 parseInt 函数取整 + if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j] 中 + i = m + 1; + else if (nums[m] > target) // 此情况说明 target 在区间 [i, m-1] 中 + j = m - 1; + else + return m; // 找到目标元素,返回其索引 + } + // 未找到目标元素,返回 -1 + return -1; + } + ``` + +=== "TypeScript" + + ```typescript title="binary_search.ts" + /* 二分查找(双闭区间) */ + const binarySearch = function (nums: number[], target: number): number { + // 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素 + let i = 0, j = nums.length - 1; + // 循环,当搜索区间为空时跳出(当 i > j 时为空) + while (i <= j) { + const m = Math.floor(i + (j - i) / 2); // 计算中点索引 m + if (nums[m] < target) { // 此情况说明 target 在区间 [m+1, j] 中 + i = m + 1; + } else if (nums[m] > target) { // 此情况说明 target 在区间 [i, m-1] 中 + j = m - 1; + } else { // 找到目标元素,返回其索引 + return m; + } + } + return -1; // 未找到目标元素,返回 -1 + } + ``` + +=== "C" + + ```c title="binary_search.c" + + ``` + +=== "C#" + + ```csharp title="binary_search.cs" + /* 二分查找(双闭区间) */ + int binarySearch(int[] nums, int target) + { + // 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素 + int i = 0, j = nums.Length - 1; + // 循环,当搜索区间为空时跳出(当 i > j 时为空) + while (i <= j) + { + int m = (i + j) / 2; // 计算中点索引 m + if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j] 中 + i = m + 1; + else if (nums[m] > target) // 此情况说明 target 在区间 [i, m-1] 中 + j = m - 1; + else // 找到目标元素,返回其索引 + return m; + } + // 未找到目标元素,返回 -1 + return -1; + } + ``` + +=== "Swift" + + ```swift title="binary_search.swift" + /* 二分查找(双闭区间) */ + func binarySearch(nums: [Int], target: Int) -> Int { + // 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素 + var i = 0 + var j = nums.count - 1 + // 循环,当搜索区间为空时跳出(当 i > j 时为空) + while i <= j { + let m = (i + j) / 2 // 计算中点索引 m + if nums[m] < target { // 此情况说明 target 在区间 [m+1, j] 中 + i = m + 1 + } else if nums[m] > target { // 此情况说明 target 在区间 [i, m-1] 中 + j = m - 1 + } else { // 找到目标元素,返回其索引 + return m + } + } + // 未找到目标元素,返回 -1 + return -1 + } + ``` + +=== "Zig" + + ```zig title="binary_search.zig" + + ``` + +### “左闭右开”实现 + +当然,我们也可以使用“左闭右开”的表示方法,写出相同功能的二分查找代码。 + +=== "Java" + + ```java title="binary_search.java" + /* 二分查找(左闭右开) */ + int binarySearch1(int[] nums, int target) { + // 初始化左闭右开 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1 + int i = 0, j = nums.length; + // 循环,当搜索区间为空时跳出(当 i = j 时为空) + while (i < j) { + int m = (i + j) / 2; // 计算中点索引 m + if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j) 中 + i = m + 1; + else if (nums[m] > target) // 此情况说明 target 在区间 [i, m) 中 + j = m; + else // 找到目标元素,返回其索引 + return m; + } + // 未找到目标元素,返回 -1 + return -1; + } + ``` + +=== "C++" + + ```cpp title="binary_search.cpp" + /* 二分查找(左闭右开) */ + int binarySearch1(vector& nums, int target) { + // 初始化左闭右开 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1 + int i = 0, j = nums.size(); + // 循环,当搜索区间为空时跳出(当 i = j 时为空) + while (i < j) { + int m = (i + j) / 2; // 计算中点索引 m + if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j) 中 + i = m + 1; + else if (nums[m] > target) // 此情况说明 target 在区间 [i, m) 中 + j = m; + else // 找到目标元素,返回其索引 + return m; + } + // 未找到目标元素,返回 -1 + return -1; + } + ``` + +=== "Python" + + ```python title="binary_search.py" + """ 二分查找(左闭右开) """ + def binary_search1(nums, target): + # 初始化左闭右开 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1 + i, j = 0, len(nums) + # 循环,当搜索区间为空时跳出(当 i = j 时为空) + while i < j: + m = (i + j) // 2 # 计算中点索引 m + if nums[m] < target: # 此情况说明 target 在区间 [m+1, j) 中 + i = m + 1 + elif nums[m] > target: # 此情况说明 target 在区间 [i, m) 中 + j = m + else: # 找到目标元素,返回其索引 + return m + return -1 # 未找到目标元素,返回 -1 + ``` + +=== "Go" + + ```go title="binary_search.go" + /* 二分查找(左闭右开) */ + func binarySearch1(nums []int, target int) int { + // 初始化左闭右开 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1 + i, j := 0, len(nums) + // 循环,当搜索区间为空时跳出(当 i = j 时为空) + for i < j { + m := (i + j) / 2 // 计算中点索引 m + if nums[m] < target { // 此情况说明 target 在区间 [m+1, j) 中 + i = m + 1 + } else if nums[m] > target { // 此情况说明 target 在区间 [i, m) 中 + j = m + } else { // 找到目标元素,返回其索引 + return m + } + } + // 未找到目标元素,返回 -1 + return -1 + } + ``` + +=== "JavaScript" + + ```js title="binary_search.js" + /* 二分查找(左闭右开) */ + function binarySearch1(nums, target) { + // 初始化左闭右开 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1 + let i = 0, j = nums.length; + // 循环,当搜索区间为空时跳出(当 i = j 时为空) + while (i < j) { + let m = parseInt((i + j) / 2); // 计算中点索引 m ,在 JS 中需使用 parseInt 函数取整 + if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j) 中 + i = m + 1; + else if (nums[m] > target) // 此情况说明 target 在区间 [i, m) 中 + j = m; + else // 找到目标元素,返回其索引 + return m; + } + // 未找到目标元素,返回 -1 + return -1; + } + ``` + +=== "TypeScript" + + ```typescript title="binary_search.ts" + /* 二分查找(左闭右开) */ + const binarySearch1 = function (nums: number[], target: number): number { + // 初始化左闭右开 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1 + let i = 0, j = nums.length; + // 循环,当搜索区间为空时跳出(当 i = j 时为空) + while (i < j) { + const m = Math.floor(i + (j - i) / 2); // 计算中点索引 m + if (nums[m] < target) { // 此情况说明 target 在区间 [m+1, j) 中 + i = m + 1; + } else if (nums[m] > target) { // 此情况说明 target 在区间 [i, m) 中 + j = m; + } else { // 找到目标元素,返回其索引 + return m; + } + } + return -1; // 未找到目标元素,返回 -1 + } + ``` + +=== "C" + + ```c title="binary_search.c" + + ``` + +=== "C#" + + ```csharp title="binary_search.cs" + /* 二分查找(左闭右开) */ + int binarySearch1(int[] nums, int target) + { + // 初始化左闭右开 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1 + int i = 0, j = nums.Length; + // 循环,当搜索区间为空时跳出(当 i = j 时为空) + while (i < j) + { + int m = (i + j) / 2; // 计算中点索引 m + if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j) 中 + i = m + 1; + else if (nums[m] > target) // 此情况说明 target 在区间 [i, m) 中 + j = m; + else // 找到目标元素,返回其索引 + return m; + } + // 未找到目标元素,返回 -1 + return -1; + } + ``` + +=== "Swift" + + ```swift title="binary_search.swift" + /* 二分查找(左闭右开) */ + func binarySearch1(nums: [Int], target: Int) -> Int { + // 初始化左闭右开 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1 + var i = 0 + var j = nums.count + // 循环,当搜索区间为空时跳出(当 i = j 时为空) + while i < j { + let m = (i + j) / 2 // 计算中点索引 m + if nums[m] < target { // 此情况说明 target 在区间 [m+1, j) 中 + i = m + 1 + } else if nums[m] > target { // 此情况说明 target 在区间 [i, m) 中 + j = m + } else { // 找到目标元素,返回其索引 + return m + } + } + // 未找到目标元素,返回 -1 + return -1 + } + ``` + +=== "Zig" + + ```zig title="binary_search.zig" + + ``` + +### 两种表示对比 + +对比下来,两种表示的代码写法有以下不同点: + +
+ +| 表示方法 | 初始化指针 | 缩小区间 | 循环终止条件 | +| ------------------- | ------------------- | ------------------------- | ------------ | +| 双闭区间 $[0, n-1]$ | $i = 0$ , $j = n-1$ | $i = m + 1$ , $j = m - 1$ | $i > j$ | +| 左闭右开 $[0, n)$ | $i = 0$ , $j = n$ | $i = m + 1$ , $j = m$ | $i = j$ | + +
+ +观察发现,在“双闭区间”表示中,由于对左右两边界的定义是相同的,因此缩小区间的 $i$ , $j$ 处理方法也是对称的,这样更不容易出错。综上所述,**建议你采用“双闭区间”的写法。** + +### 大数越界处理 + +当数组长度很大时,加法 $i + j$ 的结果有可能会超出 `int` 类型的取值范围。在此情况下,我们需要换一种计算中点的写法。 + +=== "Java" + + ```java title="" + // (i + j) 有可能超出 int 的取值范围 + int m = (i + j) / 2; + // 更换为此写法则不会越界 + int m = i + (j - i) / 2; + ``` + +=== "C++" + + ```cpp title="" + // (i + j) 有可能超出 int 的取值范围 + int m = (i + j) / 2; + // 更换为此写法则不会越界 + int m = i + (j - i) / 2; + ``` + +=== "Python" + + ```py title="" + # Python 中的数字理论上可以无限大(取决于内存大小) + # 因此无需考虑大数越界问题 + ``` + +=== "Go" + + ```go title="" + // (i + j) 有可能超出 int 的取值范围 + m := (i + j) / 2 + // 更换为此写法则不会越界 + m := i + (j - i) / 2 + ``` + +=== "JavaScript" + + ```js title="" + // (i + j) 有可能超出 int 的取值范围 + let m = parseInt((i + j) / 2); + // 更换为此写法则不会越界 + let m = parseInt(i + (j - i) / 2); + ``` + +=== "TypeScript" + + ```typescript title="" + // (i + j) 有可能超出 Number 的取值范围 + let m = Math.floor((i + j) / 2); + // 更换为此写法则不会越界 + let m = Math.floor(i + (j - i) / 2); + ``` + +=== "C" + + ```c title="" + + ``` + +=== "C#" + + ```csharp title="" + // (i + j) 有可能超出 int 的取值范围 + int m = (i + j) / 2; + // 更换为此写法则不会越界 + int m = i + (j - i) / 2; + ``` + +=== "Swift" + + ```swift title="" + // (i + j) 有可能超出 int 的取值范围 + let m = (i + j) / 2 + // 更换为此写法则不会越界 + let m = i + (j - 1) / 2 + ``` + +=== "Zig" + + ```zig title="" + + ``` + +## 10.2.2. 复杂度分析 + +**时间复杂度 $O(\log n)$** :其中 $n$ 为数组或链表长度;每轮排除一半的区间,因此循环轮数为 $\log_2 n$ ,使用 $O(\log n)$ 时间。 + +**空间复杂度 $O(1)$** :指针 `i` , `j` 使用常数大小空间。 + +## 10.2.3. 优点与缺点 + +二分查找效率很高,体现在: + +- **二分查找时间复杂度低**。对数阶在数据量很大时具有巨大优势,例如,当数据大小 $n = 2^{20}$ 时,线性查找需要 $2^{20} = 1048576$ 轮循环,而二分查找仅需要 $\log_2 2^{20} = 20$ 轮循环。 +- **二分查找不需要额外空间**。相对于借助额外数据结构来实现查找的算法来说,其更加节约空间使用。 + +但并不意味着所有情况下都应使用二分查找,这是因为: + +- **二分查找仅适用于有序数据**。如果输入数据是无序的,为了使用二分查找而专门执行数据排序,那么是得不偿失的,因为排序算法的时间复杂度一般为 $O(n \log n)$ ,比线性查找和二分查找都更差。再例如,对于频繁插入元素的场景,为了保持数组的有序性,需要将元素插入到特定位置,时间复杂度为 $O(n)$ ,也是非常昂贵的。 +- **二分查找仅适用于数组**。由于在二分查找中,访问索引是 “非连续” 的,因此链表或者基于链表实现的数据结构都无法使用。 +- **在小数据量下,线性查找的性能更好**。在线性查找中,每轮只需要 1 次判断操作;而在二分查找中,需要 1 次加法、1 次除法、1 ~ 3 次判断操作、1 次加法(减法),共 4 ~ 6 个单元操作;因此,在数据量 $n$ 较小时,线性查找反而比二分查找更快。 diff --git a/build/chapter_searching/hashing_search.md b/build/chapter_searching/hashing_search.md new file mode 100755 index 000000000..157e67a2e --- /dev/null +++ b/build/chapter_searching/hashing_search.md @@ -0,0 +1,251 @@ +--- +comments: true +--- + +# 10.3. 哈希查找 + +!!! question + + 在数据量很大时,「线性查找」太慢;而「二分查找」要求数据必须是有序的,并且只能在数组中应用。那么是否有方法可以同时避免上述缺点呢?答案是肯定的,此方法被称为「哈希查找」。 + +「哈希查找 Hash Searching」借助一个哈希表来存储需要的「键值对 Key Value Pair」,我们可以在 $O(1)$ 时间下实现“键 $\rightarrow$ 值”映射查找,体现着“以空间换时间”的算法思想。 + +## 10.3.1. 算法实现 + +如果我们想要给定数组中的一个目标元素 `target` ,获取该元素的索引,那么可以借助一个哈希表实现查找。 + +![hash_search_index](hashing_search.assets/hash_search_index.png) + +=== "Java" + + ```java title="hashing_search.java" + /* 哈希查找(数组) */ + int hashingSearchArray(Map map, int target) { + // 哈希表的 key: 目标元素,value: 索引 + // 若哈希表中无此 key ,返回 -1 + return map.getOrDefault(target, -1); + } + ``` + +=== "C++" + + ```cpp title="hashing_search.cpp" + /* 哈希查找(数组) */ + int hashingSearchArray(unordered_map map, int target) { + // 哈希表的 key: 目标元素,value: 索引 + // 若哈希表中无此 key ,返回 -1 + if (map.find(target) == map.end()) + return -1; + return map[target]; + } + ``` + +=== "Python" + + ```python title="hashing_search.py" + """ 哈希查找(数组) """ + def hashing_search_array(mapp, target): + # 哈希表的 key: 目标元素,value: 索引 + # 若哈希表中无此 key ,返回 -1 + return mapp.get(target, -1) + ``` + +=== "Go" + + ```go title="hashing_search.go" + /* 哈希查找(数组) */ + func hashingSearchArray(m map[int]int, target int) int { + // 哈希表的 key: 目标元素,value: 索引 + // 若哈希表中无此 key ,返回 -1 + if index, ok := m[target]; ok { + return index + } else { + return -1 + } + } + ``` + +=== "JavaScript" + + ```js title="hashing_search.js" + /* 哈希查找(数组) */ + function hashingSearchArray(map, target) { + // 哈希表的 key: 目标元素,value: 索引 + // 若哈希表中无此 key ,返回 -1 + return map.has(target) ? map.get(target) : -1; + } + ``` + +=== "TypeScript" + + ```typescript title="hashing_search.ts" + /* 哈希查找(数组) */ + function hashingSearchArray(map: Map, target: number): number { + // 哈希表的 key: 目标元素,value: 索引 + // 若哈希表中无此 key ,返回 -1 + return map.has(target) ? map.get(target) as number : -1; + } + ``` + +=== "C" + + ```c title="hashing_search.c" + + ``` + +=== "C#" + + ```csharp title="hashing_search.cs" + /* 哈希查找(数组) */ + int hashingSearchArray(Dictionary map, int target) + { + // 哈希表的 key: 目标元素,value: 索引 + // 若哈希表中无此 key ,返回 -1 + return map.GetValueOrDefault(target, -1); + } + ``` + +=== "Swift" + + ```swift title="hashing_search.swift" + /* 哈希查找(数组) */ + func hashingSearchArray(map: [Int: Int], target: Int) -> Int { + // 哈希表的 key: 目标元素,value: 索引 + // 若哈希表中无此 key ,返回 -1 + return map[target, default: -1] + } + ``` + +=== "Zig" + + ```zig title="hashing_search.zig" + + ``` + +再比如,如果我们想要给定一个目标结点值 `target` ,获取对应的链表结点对象,那么也可以使用哈希查找实现。 + +![hash_search_listnode](hashing_search.assets/hash_search_listnode.png) + +=== "Java" + + ```java title="hashing_search.java" + /* 哈希查找(链表) */ + ListNode hashingSearchLinkedList(Map map, int target) { + // 哈希表的 key: 目标结点值,value: 结点对象 + // 若哈希表中无此 key ,返回 null + return map.getOrDefault(target, null); + } + ``` + +=== "C++" + + ```cpp title="hashing_search.cpp" + /* 哈希查找(链表) */ + ListNode* hashingSearchLinkedList(unordered_map map, int target) { + // 哈希表的 key: 目标结点值,value: 结点对象 + // 若哈希表中无此 key ,返回 nullptr + if (map.find(target) == map.end()) + return nullptr; + return map[target]; + } + ``` + +=== "Python" + + ```python title="hashing_search.py" + """ 哈希查找(链表) """ + def hashing_search_linkedlist(mapp, target): + # 哈希表的 key: 目标元素,value: 结点对象 + # 若哈希表中无此 key ,返回 -1 + return mapp.get(target, -1) + ``` + +=== "Go" + + ```go title="hashing_search.go" + /* 哈希查找(链表) */ + func hashingSearchLinkedList(m map[int]*ListNode, target int) *ListNode { + // 哈希表的 key: 目标结点值,value: 结点对象 + // 若哈希表中无此 key ,返回 nil + if node, ok := m[target]; ok { + return node + } else { + return nil + } + } + ``` + +=== "JavaScript" + + ```js title="hashing_search.js" + /* 哈希查找(链表) */ + function hashingSearchLinkedList(map, target) { + // 哈希表的 key: 目标结点值,value: 结点对象 + // 若哈希表中无此 key ,返回 null + return map.has(target) ? map.get(target) : null; + } + ``` + +=== "TypeScript" + + ```typescript title="hashing_search.ts" + /* 哈希查找(链表) */ + function hashingSearchLinkedList(map: Map, target: number): ListNode | null { + // 哈希表的 key: 目标结点值,value: 结点对象 + // 若哈希表中无此 key ,返回 null + return map.has(target) ? map.get(target) as ListNode : null; + } + ``` + +=== "C" + + ```c title="hashing_search.c" + + ``` + +=== "C#" + + ```csharp title="hashing_search.cs" + /* 哈希查找(链表) */ + ListNode? hashingSearchLinkedList(Dictionary map, int target) + { + + // 哈希表的 key: 目标结点值,value: 结点对象 + // 若哈希表中无此 key ,返回 null + return map.GetValueOrDefault(target); + } + ``` + +=== "Swift" + + ```swift title="hashing_search.swift" + /* 哈希查找(链表) */ + func hashingSearchLinkedList(map: [Int: ListNode], target: Int) -> ListNode? { + // 哈希表的 key: 目标结点值,value: 结点对象 + // 若哈希表中无此 key ,返回 null + return map[target] + } + ``` + +=== "Zig" + + ```zig title="hashing_search.zig" + + ``` + +## 10.3.2. 复杂度分析 + +**时间复杂度 $O(1)$** :哈希表的查找操作使用 $O(1)$ 时间。 + +**空间复杂度 $O(n)$** :其中 $n$ 为数组或链表长度。 + +## 10.3.3. 优点与缺点 + +在哈希表中,**查找、插入、删除操作的平均时间复杂度都为 $O(1)$** ,这意味着无论是高频增删还是高频查找场景,哈希查找的性能表现都非常好。当然,一切的前提是保证哈希表未退化。 + +即使如此,哈希查找仍存在一些问题,在实际应用中,需要根据情况灵活选择方法。 + +- 辅助哈希表 **需要使用 $O(n)$ 的额外空间**,意味着需要预留更多的计算机内存; +- 建立和维护哈希表需要时间,因此哈希查找 **不适合高频增删、低频查找的使用场景**; +- 当哈希冲突严重时,哈希表会退化为链表,**时间复杂度劣化至 $O(n)$** ; +- **当数据量很小时,线性查找比哈希查找更快**。这是因为计算哈希映射函数可能比遍历一个小型数组更慢; diff --git a/build/chapter_searching/linear_search.md b/build/chapter_searching/linear_search.md new file mode 100755 index 000000000..5a8bb05fa --- /dev/null +++ b/build/chapter_searching/linear_search.md @@ -0,0 +1,322 @@ +--- +comments: true +--- + +# 10.1. 线性查找 + +「线性查找 Linear Search」是一种最基础的查找方法,其从数据结构的一端开始,依次访问每个元素,直到另一端后停止。 + +## 10.1.1. 算法实现 + +线性查找实质上就是遍历数据结构 + 判断条件。比如,我们想要在数组 `nums` 中查找目标元素 `target` 的对应索引,那么可以在数组中进行线性查找。 + +![linear_search](linear_search.assets/linear_search.png) + +=== "Java" + + ```java title="linear_search.java" + /* 线性查找(数组) */ + int linearSearchArray(int[] nums, int target) { + // 遍历数组 + for (int i = 0; i < nums.length; i++) { + // 找到目标元素,返回其索引 + if (nums[i] == target) + return i; + } + // 未找到目标元素,返回 -1 + return -1; + } + ``` + +=== "C++" + + ```cpp title="linear_search.cpp" + /* 线性查找(数组) */ + int linearSearchArray(vector& nums, int target) { + // 遍历数组 + for (int i = 0; i < nums.size(); i++) { + // 找到目标元素,返回其索引 + if (nums[i] == target) + return i; + } + // 未找到目标元素,返回 -1 + return -1; + } + ``` + +=== "Python" + + ```python title="linear_search.py" + """ 线性查找(数组) """ + def linear_search_array(nums, target): + # 遍历数组 + for i in range(len(nums)): + if nums[i] == target: # 找到目标元素,返回其索引 + return i + return -1 # 未找到目标元素,返回 -1 + ``` + +=== "Go" + + ```go title="linear_search.go" + /* 线性查找(数组) */ + func linearSearchArray(nums []int, target int) int { + // 遍历数组 + for i := 0; i < len(nums); i++ { + // 找到目标元素,返回其索引 + if nums[i] == target { + return i + } + } + // 未找到目标元素,返回 -1 + return -1 + } + ``` + +=== "JavaScript" + + ```js title="linear_search.js" + /* 线性查找(数组) */ + function linearSearchArray(nums, target) { + // 遍历数组 + for (let i = 0; i < nums.length; i++) { + // 找到目标元素,返回其索引 + if (nums[i] === target) { + return i; + } + } + // 未找到目标元素,返回 -1 + return -1; + } + + ``` + +=== "TypeScript" + + ```typescript title="linear_search.ts" + /* 线性查找(数组)*/ + function linearSearchArray(nums: number[], target: number): number { + // 遍历数组 + for (let i = 0; i < nums.length; i++) { + // 找到目标元素,返回其索引 + if (nums[i] === target) { + return i; + } + } + // 未找到目标元素,返回 -1 + return -1; + } + ``` + +=== "C" + + ```c title="linear_search.c" + + ``` + +=== "C#" + + ```csharp title="linear_search.cs" + /* 线性查找(数组) */ + int linearSearchArray(int[] nums, int target) + { + // 遍历数组 + for (int i = 0; i < nums.Length; i++) + { + // 找到目标元素,返回其索引 + if (nums[i] == target) + return i; + } + // 未找到目标元素,返回 -1 + return -1; + } + + ``` + +=== "Swift" + + ```swift title="linear_search.swift" + /* 线性查找(数组) */ + func linearSearchArray(nums: [Int], target: Int) -> Int { + // 遍历数组 + for i in nums.indices { + // 找到目标元素,返回其索引 + if nums[i] == target { + return i + } + } + // 未找到目标元素,返回 -1 + return -1 + } + ``` + +=== "Zig" + + ```zig title="linear_search.zig" + + ``` + +再比如,我们想要在给定一个目标结点值 `target` ,返回此结点对象,也可以在链表中进行线性查找。 + +=== "Java" + + ```java title="linear_search.java" + /* 线性查找(链表) */ + ListNode linearSearchLinkedList(ListNode head, int target) { + // 遍历链表 + while (head != null) { + // 找到目标结点,返回之 + if (head.val == target) + return head; + head = head.next; + } + // 未找到目标结点,返回 null + return null; + } + ``` + +=== "C++" + + ```cpp title="linear_search.cpp" + /* 线性查找(链表) */ + ListNode* linearSearchLinkedList(ListNode* head, int target) { + // 遍历链表 + while (head != nullptr) { + // 找到目标结点,返回之 + if (head->val == target) + return head; + head = head->next; + } + // 未找到目标结点,返回 nullptr + return nullptr; + } + ``` + +=== "Python" + + ```python title="linear_search.py" + """ 线性查找(链表) """ + def linear_search_linkedlist(head, target): + # 遍历链表 + while head: + if head.val == target: # 找到目标结点,返回之 + return head + head = head.next + return None # 未找到目标结点,返回 None + ``` + +=== "Go" + + ```go title="linear_search.go" + /* 线性查找(链表)*/ + func linerSearchLinkedList(node *ListNode, target int) *ListNode { + // 遍历链表 + for node != nil { + // 找到目标结点,返回之 + if node.Val == target { + return node + } + node = node.Next + } + // 未找到目标元素,返回 nil + return nil + } + ``` + +=== "JavaScript" + + ```js title="linear_search.js" + /* 线性查找(链表)*/ + function linearSearchLinkedList(head, target) { + // 遍历链表 + while(head) { + // 找到目标结点,返回之 + if(head.val === target) { + return head; + } + head = head.next; + } + // 未找到目标结点,返回 null + return null; + } + ``` + +=== "TypeScript" + + ```typescript title="linear_search.ts" + /* 线性查找(链表)*/ + function linearSearchLinkedList(head: ListNode | null, target: number): ListNode | null { + // 遍历链表 + while (head) { + // 找到目标结点,返回之 + if (head.val === target) { + return head; + } + head = head.next; + } + // 未找到目标结点,返回 null + return null; + } + ``` + +=== "C" + + ```c title="linear_search.c" + + ``` + +=== "C#" + + ```csharp title="linear_search.cs" + /* 线性查找(链表) */ + ListNode? linearSearchLinkedList(ListNode head, int target) + { + // 遍历链表 + while (head != null) + { + // 找到目标结点,返回之 + if (head.val == target) + return head; + head = head.next; + } + // 未找到目标结点,返回 null + return null; + } + ``` + +=== "Swift" + + ```swift title="linear_search.swift" + /* 线性查找(链表) */ + func linearSearchLinkedList(head: ListNode?, target: Int) -> ListNode? { + var head = head + // 遍历链表 + while head != nil { + // 找到目标结点,返回之 + if head?.val == target { + return head + } + head = head?.next + } + // 未找到目标结点,返回 null + return nil + } + ``` + +=== "Zig" + + ```zig title="linear_search.zig" + + ``` + +## 10.1.2. 复杂度分析 + +**时间复杂度 $O(n)$** :其中 $n$ 为数组或链表长度。 + +**空间复杂度 $O(1)$** :无需使用额外空间。 + +## 10.1.3. 优点与缺点 + +**线性查找的通用性极佳**。由于线性查找是依次访问元素的,即没有跳跃访问元素,因此数组或链表皆适用。 + +**线性查找的时间复杂度太高**。在数据量 $n$ 很大时,查找效率很低。 diff --git a/build/chapter_searching/summary.md b/build/chapter_searching/summary.md new file mode 100644 index 000000000..738255889 --- /dev/null +++ b/build/chapter_searching/summary.md @@ -0,0 +1,23 @@ +--- +comments: true +--- + +# 10.4. 小结 + +- 线性查找是一种最基础的查找方法,通过遍历数据结构 + 判断条件实现查找。 +- 二分查找利用数据的有序性,通过循环不断缩小一半搜索区间来实现查找,其要求输入数据是有序的,并且仅适用于数组或基于数组实现的数据结构。 +- 哈希查找借助哈希表来实现常数阶时间复杂度的查找操作,体现以空间换时间的算法思想。 + +

Table. 三种查找方法对比

+ +
+ +| | 线性查找 | 二分查找 | 哈希查找 | +| ------------------------------------- | ------------------------ | ----------------------------- | ------------------------ | +| 适用数据结构 | 数组、链表 | 数组 | 数组、链表 | +| 输入数据要求 | 无 | 有序 | 无 | +| 平均时间复杂度
查找 / 插入 / 删除 | $O(n)$ / $O(1)$ / $O(n)$ | $O(\log n)$ / $O(n)$ / $O(n)$ | $O(1)$ / $O(1)$ / $O(1)$ | +| 最差时间复杂度
查找 / 插入 / 删除 | $O(n)$ / $O(1)$ / $O(n)$ | $O(\log n)$ / $O(n)$ / $O(n)$ | $O(n)$ / $O(n)$ / $O(n)$ | +| 空间复杂度 | $O(1)$ | $O(1)$ | $O(n)$ | + +
diff --git a/build/chapter_sorting/bubble_sort.md b/build/chapter_sorting/bubble_sort.md new file mode 100755 index 000000000..e23edcaab --- /dev/null +++ b/build/chapter_sorting/bubble_sort.md @@ -0,0 +1,465 @@ +--- +comments: true +--- + +# 11.2. 冒泡排序 + +「冒泡排序 Bubble Sort」是一种最基础的排序算法,非常适合作为第一个学习的排序算法。顾名思义,「冒泡」是该算法的核心操作。 + +!!! question "为什么叫“冒泡”" + + 在水中,越大的泡泡浮力越大,所以最大的泡泡会最先浮到水面。 + +「冒泡」操作则是在模拟上述过程,具体做法为:从数组最左端开始向右遍历,依次对比相邻元素大小,若 **左元素 > 右元素** 则将它俩交换,最终可将最大元素移动至数组最右端。 + +完成此次冒泡操作后,**数组最大元素已在正确位置,接下来只需排序剩余 $n - 1$ 个元素**。 + +=== "Step 1" + ![bubble_operation_step1](bubble_sort.assets/bubble_operation_step1.png) + +=== "Step 2" + ![bubble_operation_step2](bubble_sort.assets/bubble_operation_step2.png) + +=== "Step 3" + ![bubble_operation_step3](bubble_sort.assets/bubble_operation_step3.png) + +=== "Step 4" + ![bubble_operation_step4](bubble_sort.assets/bubble_operation_step4.png) + +=== "Step 5" + ![bubble_operation_step5](bubble_sort.assets/bubble_operation_step5.png) + +=== "Step 6" + ![bubble_operation_step6](bubble_sort.assets/bubble_operation_step6.png) + +=== "Step 7" + ![bubble_operation_step7](bubble_sort.assets/bubble_operation_step7.png) + +

Fig. 冒泡操作

+ +## 11.2.1. 算法流程 + +1. 设数组长度为 $n$ ,完成第一轮「冒泡」后,数组最大元素已在正确位置,接下来只需排序剩余 $n - 1$ 个元素。 +2. 同理,对剩余 $n - 1$ 个元素执行「冒泡」,可将第二大元素交换至正确位置,因而待排序元素只剩 $n - 2$ 个。 +3. 以此类推…… **循环 $n - 1$ 轮「冒泡」,即可完成整个数组的排序**。 + +![bubble_sort](bubble_sort.assets/bubble_sort.png) + +

Fig. 冒泡排序流程

+ +=== "Java" + + ```java title="bubble_sort.java" + /* 冒泡排序 */ + void bubbleSort(int[] nums) { + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for (int i = nums.length - 1; i > 0; i--) { + // 内循环:冒泡操作 + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交换 nums[j] 与 nums[j + 1] + int tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + } + } + } + } + ``` + +=== "C++" + + ```cpp title="bubble_sort.cpp" + /* 冒泡排序 */ + void bubbleSort(vector& nums) { + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for (int i = nums.size() - 1; i > 0; i--) { + // 内循环:冒泡操作 + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交换 nums[j] 与 nums[j + 1] + // 这里使用了 std::swap() 函数 + swap(nums[j], nums[j + 1]); + } + } + } + } + ``` + +=== "Python" + + ```python title="bubble_sort.py" + """ 冒泡排序 """ + def bubble_sort(nums): + n = len(nums) + # 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for i in range(n - 1, 0, -1): + # 内循环:冒泡操作 + for j in range(i): + if nums[j] > nums[j + 1]: + # 交换 nums[j] 与 nums[j + 1] + nums[j], nums[j + 1] = nums[j + 1], nums[j] + ``` + +=== "Go" + + ```go title="bubble_sort.go" + /* 冒泡排序 */ + func bubbleSort(nums []int) { + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for i := len(nums) - 1; i > 0; i-- { + // 内循环:冒泡操作 + for j := 0; j < i; j++ { + if nums[j] > nums[j+1] { + // 交换 nums[j] 与 nums[j + 1] + nums[j], nums[j+1] = nums[j+1], nums[j] + } + } + } + } + ``` + +=== "JavaScript" + + ```js title="bubble_sort.js" + /* 冒泡排序 */ + function bubbleSort(nums) { + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for (let i = nums.length - 1; i > 0; i--) { + // 内循环:冒泡操作 + for (let j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交换 nums[j] 与 nums[j + 1] + let tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + } + } + } + } + ``` + +=== "TypeScript" + + ```typescript title="bubble_sort.ts" + /* 冒泡排序 */ + function bubbleSort(nums: number[]): void { + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for (let i = nums.length - 1; i > 0; i--) { + // 内循环:冒泡操作 + for (let j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交换 nums[j] 与 nums[j + 1] + let tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + } + } + } + } + ``` + +=== "C" + + ```c title="bubble_sort.c" + /* 冒泡排序 */ + void bubbleSort(int nums[], int size) { + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for (int i = 0; i < size - 1; i++) + { + // 内循环:冒泡操作 + for (int j = 0; j < size - 1 - i; j++) + { + if (nums[j] > nums[j + 1]) + { + int temp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = temp; + } + } + } + } + ``` + +=== "C#" + + ```csharp title="bubble_sort.cs" + /* 冒泡排序 */ + void bubbleSort(int[] nums) + { + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for (int i = nums.Length - 1; i > 0; i--) + { + // 内循环:冒泡操作 + for (int j = 0; j < i; j++) + { + if (nums[j] > nums[j + 1]) + { + // 交换 nums[j] 与 nums[j + 1] + int tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + } + } + } + } + ``` + +=== "Swift" + + ```swift title="bubble_sort.swift" + /* 冒泡排序 */ + func bubbleSort(nums: inout [Int]) { + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for i in stride(from: nums.count - 1, to: 0, by: -1) { + // 内循环:冒泡操作 + for j in stride(from: 0, to: i, by: 1) { + if nums[j] > nums[j + 1] { + // 交换 nums[j] 与 nums[j + 1] + let tmp = nums[j] + nums[j] = nums[j + 1] + nums[j + 1] = tmp + } + } + } + } + ``` + +=== "Zig" + + ```zig title="bubble_sort.zig" + + ``` + +## 11.2.2. 算法特性 + +**时间复杂度 $O(n^2)$** :各轮「冒泡」遍历的数组长度为 $n - 1$ , $n - 2$ , $\cdots$ , $2$ , $1$ 次,求和为 $\frac{(n - 1) n}{2}$ ,因此使用 $O(n^2)$ 时间。 + +**空间复杂度 $O(1)$** :指针 $i$ , $j$ 使用常数大小的额外空间。 + +**原地排序**:指针变量仅使用常数大小额外空间。 + +**稳定排序**:不交换相等元素。 + +**自适应排序**:引入 `flag` 优化后(见下文),最佳时间复杂度为 $O(N)$ 。 + +## 11.2.3. 效率优化 + +我们发现,若在某轮「冒泡」中未执行任何交换操作,则说明数组已经完成排序,可直接返回结果。考虑可以增加一个标志位 `flag` 来监听该情况,若出现则直接返回。 + +优化后,冒泡排序的最差和平均时间复杂度仍为 $O(n^2)$ ;而在输入数组 **已排序** 时,达到 **最佳时间复杂度** $O(n)$ 。 + +=== "Java" + + ```java title="bubble_sort.java" + /* 冒泡排序(标志优化)*/ + void bubbleSortWithFlag(int[] nums) { + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for (int i = nums.length - 1; i > 0; i--) { + boolean flag = false; // 初始化标志位 + // 内循环:冒泡操作 + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交换 nums[j] 与 nums[j + 1] + int tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + flag = true; // 记录交换元素 + } + } + if (!flag) break; // 此轮冒泡未交换任何元素,直接跳出 + } + } + ``` + +=== "C++" + + ```cpp title="bubble_sort.cpp" + /* 冒泡排序(标志优化)*/ + void bubbleSortWithFlag(vector& nums) { + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for (int i = nums.size() - 1; i > 0; i--) { + bool flag = false; // 初始化标志位 + // 内循环:冒泡操作 + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交换 nums[j] 与 nums[j + 1] + // 这里使用了 std::swap() 函数 + swap(nums[j], nums[j + 1]); + flag = true; // 记录交换元素 + } + } + if (!flag) break; // 此轮冒泡未交换任何元素,直接跳出 + } + } + ``` + +=== "Python" + + ```python title="bubble_sort.py" + """ 冒泡排序(标志优化) """ + def bubble_sort_with_flag(nums): + n = len(nums) + # 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for i in range(n - 1, 0, -1): + flag = False # 初始化标志位 + # 内循环:冒泡操作 + for j in range(i): + if nums[j] > nums[j + 1]: + # 交换 nums[j] 与 nums[j + 1] + nums[j], nums[j + 1] = nums[j + 1], nums[j] + flag = True # 记录交换元素 + if not flag: + break # 此轮冒泡未交换任何元素,直接跳出 + ``` + +=== "Go" + + ```go title="bubble_sort.go" + /* 冒泡排序(标志优化)*/ + func bubbleSortWithFlag(nums []int) { + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for i := len(nums) - 1; i > 0; i-- { + flag := false // 初始化标志位 + // 内循环:冒泡操作 + for j := 0; j < i; j++ { + if nums[j] > nums[j+1] { + // 交换 nums[j] 与 nums[j + 1] + nums[j], nums[j+1] = nums[j+1], nums[j] + flag = true // 记录交换元素 + } + } + if flag == false { // 此轮冒泡未交换任何元素,直接跳出 + break + } + } + } + ``` + +=== "JavaScript" + + ```js title="bubble_sort.js" + /* 冒泡排序(标志优化)*/ + function bubbleSortWithFlag(nums) { + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for (let i = nums.length - 1; i > 0; i--) { + let flag = false; // 初始化标志位 + // 内循环:冒泡操作 + for (let j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交换 nums[j] 与 nums[j + 1] + let tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + flag = true; // 记录交换元素 + } + } + if (!flag) break; // 此轮冒泡未交换任何元素,直接跳出 + } + } + ``` + +=== "TypeScript" + + ```typescript title="bubble_sort.ts" + /* 冒泡排序(标志优化)*/ + function bubbleSortWithFlag(nums: number[]): void { + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for (let i = nums.length - 1; i > 0; i--) { + let flag = false; // 初始化标志位 + // 内循环:冒泡操作 + for (let j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // 交换 nums[j] 与 nums[j + 1] + let tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + flag = true; // 记录交换元素 + } + } + if (!flag) break; // 此轮冒泡未交换任何元素,直接跳出 + } + } + ``` + +=== "C" + + ```c title="bubble_sort.c" + /* 冒泡排序 */ + void bubbleSortWithFlag(int nums[], int size) { + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for (int i = 0; i < size - 1; i++) + { + bool flag = false; + // 内循环:冒泡操作 + for (int j = 0; j < size - 1 - i; j++) + { + if (nums[j] > nums[j + 1]) + { + int temp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = temp; + flag = true; + } + } + if(!flag) break; + } + } + ``` + +=== "C#" + + ```csharp title="bubble_sort.cs" + /* 冒泡排序(标志优化)*/ + void bubbleSortWithFlag(int[] nums) + { + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for (int i = nums.Length - 1; i > 0; i--) + { + bool flag = false; // 初始化标志位 + // 内循环:冒泡操作 + for (int j = 0; j < i; j++) + { + if (nums[j] > nums[j + 1]) + { + // 交换 nums[j] 与 nums[j + 1] + int tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + flag = true; // 记录交换元素 + } + } + if (!flag) break; // 此轮冒泡未交换任何元素,直接跳出 + } + } + ``` + +=== "Swift" + + ```swift title="bubble_sort.swift" + /* 冒泡排序(标志优化)*/ + func bubbleSortWithFlag(nums: inout [Int]) { + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for i in stride(from: nums.count - 1, to: 0, by: -1) { + var flag = false // 初始化标志位 + for j in stride(from: 0, to: i, by: 1) { + if nums[j] > nums[j + 1] { + // 交换 nums[j] 与 nums[j + 1] + let tmp = nums[j] + nums[j] = nums[j + 1] + nums[j + 1] = tmp + flag = true // 记录交换元素 + } + } + if !flag { // 此轮冒泡未交换任何元素,直接跳出 + break + } + } + } + ``` + +=== "Zig" + + ```zig title="bubble_sort.zig" + + ``` diff --git a/build/chapter_sorting/insertion_sort.md b/build/chapter_sorting/insertion_sort.md new file mode 100755 index 000000000..0c3cee929 --- /dev/null +++ b/build/chapter_sorting/insertion_sort.md @@ -0,0 +1,228 @@ +--- +comments: true +--- + +# 11.3. 插入排序 + +「插入排序 Insertion Sort」是一种基于 **数组插入操作** 的排序算法。 + +「插入操作」原理:选定某个待排序元素为基准数 `base`,将 `base` 与其左侧已排序区间元素依次对比大小,并插入到正确位置。 + +回忆数组插入操作,我们需要将从目标索引到 `base` 之间的所有元素向右移动一位,然后再将 `base` 赋值给目标索引。 + +![insertion_operation](insertion_sort.assets/insertion_operation.png) + +

Fig. 插入操作

+ +## 11.3.1. 算法流程 + +1. 第 1 轮先选取数组的 **第 2 个元素** 为 `base` ,执行「插入操作」后,**数组前 2 个元素已完成排序**。 +2. 第 2 轮选取 **第 3 个元素** 为 `base` ,执行「插入操作」后,**数组前 3 个元素已完成排序**。 +3. 以此类推……最后一轮选取 **数组尾元素** 为 `base` ,执行「插入操作」后,**所有元素已完成排序**。 + +![insertion_sort](insertion_sort.assets/insertion_sort.png) + +

Fig. 插入排序流程

+ +=== "Java" + + ```java title="insertion_sort.java" + /* 插入排序 */ + void insertionSort(int[] nums) { + // 外循环:base = nums[1], nums[2], ..., nums[n-1] + for (int i = 1; i < nums.length; i++) { + int base = nums[i], j = i - 1; + // 内循环:将 base 插入到左边的正确位置 + while (j >= 0 && nums[j] > base) { + nums[j + 1] = nums[j]; // 1. 将 nums[j] 向右移动一位 + j--; + } + nums[j + 1] = base; // 2. 将 base 赋值到正确位置 + } + } + ``` + +=== "C++" + + ```cpp title="insertion_sort.cpp" + /* 插入排序 */ + void insertionSort(vector& nums) { + // 外循环:base = nums[1], nums[2], ..., nums[n-1] + for (int i = 1; i < nums.size(); i++) { + int base = nums[i], j = i - 1; + // 内循环:将 base 插入到左边的正确位置 + while (j >= 0 && nums[j] > base) { + nums[j + 1] = nums[j]; // 1. 将 nums[j] 向右移动一位 + j--; + } + nums[j + 1] = base; // 2. 将 base 赋值到正确位置 + } + } + ``` + +=== "Python" + + ```python title="insertion_sort.py" + """ 插入排序 """ + def insertion_sort(nums): + # 外循环:base = nums[1], nums[2], ..., nums[n-1] + for i in range(1, len(nums)): + base = nums[i] + j = i - 1 + # 内循环:将 base 插入到左边的正确位置 + while j >= 0 and nums[j] > base: + nums[j + 1] = nums[j] # 1. 将 nums[j] 向右移动一位 + j -= 1 + nums[j + 1] = base # 2. 将 base 赋值到正确位置 + ``` + +=== "Go" + + ```go title="insertion_sort.go" + /* 插入排序 */ + func insertionSort(nums []int) { + // 外循环:待排序元素数量为 n-1, n-2, ..., 1 + for i := 1; i < len(nums); i++ { + base := nums[i] + j := i - 1 + // 内循环:将 base 插入到左边的正确位置 + for j >= 0 && nums[j] > base { + nums[j+1] = nums[j] // 1. 将 nums[j] 向右移动一位 + j-- + } + nums[j+1] = base // 2. 将 base 赋值到正确位置 + } + } + ``` + +=== "JavaScript" + + ```js title="insertion_sort.js" + /* 插入排序 */ + function insertionSort(nums) { + // 外循环:base = nums[1], nums[2], ..., nums[n-1] + for (let i = 1; i < nums.length; i++) { + let base = nums[i], j = i - 1; + // 内循环:将 base 插入到左边的正确位置 + while (j >= 0 && nums[j] > base) { + nums[j + 1] = nums[j]; // 1. 将 nums[j] 向右移动一位 + j--; + } + nums[j + 1] = base; // 2. 将 base 赋值到正确位置 + } + } + ``` + +=== "TypeScript" + + ```typescript title="insertion_sort.ts" + /* 插入排序 */ + function insertionSort(nums: number[]): void { + // 外循环:base = nums[1], nums[2], ..., nums[n-1] + for (let i = 1; i < nums.length; i++) { + const base = nums[i]; + let j = i - 1; + // 内循环:将 base 插入到左边的正确位置 + while (j >= 0 && nums[j] > base) { + nums[j + 1] = nums[j]; // 1. 将 nums[j] 向右移动一位 + j--; + } + nums[j + 1] = base; // 2. 将 base 赋值到正确位置 + } + } + ``` + +=== "C" + + ```c title="insertion_sort.c" + /* 插入排序 */ + void insertionSort(int nums[], int size) { + // 外循环:base = nums[1], nums[2], ..., nums[n-1] + for (int i = 1; i < size; i++) + { + int base = nums[i], j = i - 1; + // 内循环:将 base 插入到左边的正确位置 + while (j >= 0 && nums[j] > base) + { + // 1. 将 nums[j] 向右移动一位 + nums[j + 1] = nums[j]; + j--; + } + // 2. 将 base 赋值到正确位置 + nums[j + 1] = base; + } + } + ``` + +=== "C#" + + ```csharp title="insertion_sort.cs" + /* 插入排序 */ + void insertionSort(int[] nums) + { + // 外循环:base = nums[1], nums[2], ..., nums[n-1] + for (int i = 1; i < nums.Length; i++) + { + int bas = nums[i], j = i - 1; + // 内循环:将 base 插入到左边的正确位置 + while (j >= 0 && nums[j] > bas) + { + nums[j + 1] = nums[j]; // 1. 将 nums[j] 向右移动一位 + j--; + } + nums[j + 1] = bas; // 2. 将 base 赋值到正确位置 + } + } + ``` + +=== "Swift" + + ```swift title="insertion_sort.swift" + /* 插入排序 */ + func insertionSort(nums: inout [Int]) { + // 外循环:base = nums[1], nums[2], ..., nums[n-1] + for i in stride(from: 1, to: nums.count, by: 1) { + let base = nums[i] + var j = i - 1 + // 内循环:将 base 插入到左边的正确位置 + while j >= 0, nums[j] > base { + nums[j + 1] = nums[j] // 1. 将 nums[j] 向右移动一位 + j -= 1 + } + nums[j + 1] = base // 2. 将 base 赋值到正确位置 + } + } + ``` + +=== "Zig" + + ```zig title="insertion_sort.zig" + + ``` + +## 11.3.2. 算法特性 + +**时间复杂度 $O(n^2)$** :最差情况下,各轮插入操作循环 $n - 1$ , $n-2$ , $\cdots$ , $2$ , $1$ 次,求和为 $\frac{(n - 1) n}{2}$ ,使用 $O(n^2)$ 时间。 + +**空间复杂度 $O(1)$** :指针 $i$ , $j$ 使用常数大小的额外空间。 + +**原地排序**:指针变量仅使用常数大小额外空间。 + +**稳定排序**:不交换相等元素。 + +**自适应排序**:最佳情况下,时间复杂度为 $O(n)$ 。 + +## 11.3.3. 插入排序 vs 冒泡排序 + +!!! question + + 虽然「插入排序」和「冒泡排序」的时间复杂度皆为 $O(n^2)$ ,但实际运行速度却有很大差别,这是为什么呢? + +回顾复杂度分析,两个方法的循环次数都是 $\frac{(n - 1) n}{2}$ 。但不同的是,「冒泡操作」是在做 **元素交换**,需要借助一个临时变量实现,共 3 个单元操作;而「插入操作」是在做 **赋值**,只需 1 个单元操作;因此,可以粗略估计出冒泡排序的计算开销约为插入排序的 3 倍。 + +插入排序运行速度快,并且具有原地、稳定、自适应的优点,因此很受欢迎。实际上,包括 Java 在内的许多编程语言的排序库函数的实现都用到了插入排序。库函数的大致思路: + +- 对于 **长数组**,采用基于分治的排序算法,例如「快速排序」,时间复杂度为 $O(n \log n)$ ; +- 对于 **短数组**,直接使用「插入排序」,时间复杂度为 $O(n^2)$ ; + +在数组较短时,复杂度中的常数项(即每轮中的单元操作数量)占主导作用,此时插入排序运行地更快。这个现象与「线性查找」和「二分查找」的情况类似。 diff --git a/build/chapter_sorting/intro_to_sort.md b/build/chapter_sorting/intro_to_sort.md new file mode 100644 index 000000000..e4e1552ac --- /dev/null +++ b/build/chapter_sorting/intro_to_sort.md @@ -0,0 +1,74 @@ +--- +comments: true +--- + +# 11.1. 排序简介 + +「排序算法 Sorting Algorithm」使得列表中的所有元素按照从小到大的顺序排列。 + +- 待排序的列表的 **元素类型** 可以是整数、浮点数、字符、或字符串; +- 排序算法可以根据需要设定 **判断规则**,例如数字大小、字符 ASCII 码顺序、自定义规则; + +![sorting_examples](intro_to_sort.assets/sorting_examples.png) + +

Fig. 排序中的不同元素类型和判断规则

+ +## 11.1.1. 评价维度 + +排序算法主要可根据 **稳定性 、就地性 、自适应性 、比较类** 来分类。 + +### 稳定性 + +- 「稳定排序」在完成排序后,**不改变** 相等元素在数组中的相对顺序。 +- 「非稳定排序」在完成排序后,相等元素在数组中的相对位置 **可能被改变**。 + +假设我们有一个存储学生信息的表格,第 1, 2 列分别是姓名和年龄。那么在以下示例中,「非稳定排序」会导致输入数据的有序性丢失。因此「稳定排序」是很好的特性,**在多级排序中是必须的**。 + +```shell + # 输入数据是按照姓名排序好的 + # (name, age) + ('A', 19) + ('B', 18) + ('C', 21) + ('D', 19) + ('E', 23) + + # 假设使用非稳定排序算法按年龄排序列表, + # 结果中 ('D', 19) 和 ('A', 19) 的相对位置改变, + # 输入数据按姓名排序的性质丢失 + ('B', 18) + ('D', 19) + ('A', 19) + ('C', 21) + ('E', 23) +``` + +### 就地性 + +- 「原地排序」无需辅助数据,不使用额外空间; +- 「非原地排序」需要借助辅助数据,使用额外空间; + +「原地排序」不使用额外空间,可以节约内存;并且一般情况下,由于数据操作减少,原地排序的运行效率也更高。 + +### 自适应性 + +- 「自适应排序」的时间复杂度受输入数据影响,即最佳 / 最差 / 平均时间复杂度不相等。 +- 「非自适应排序」的时间复杂度恒定,与输入数据无关。 + +我们希望 **最差 = 平均**,即不希望排序算法的运行效率在某些输入数据下发生劣化。 + +### 比较类 + +- 「比较类排序」基于元素之间的比较算子(小于、相等、大于)来决定元素的相对顺序。 +- 「非比较类排序」不基于元素之间的比较算子来决定元素的相对顺序。 + +「比较类排序」的时间复杂度最优为 $O(n \log n)$ ;而「非比较类排序」可以达到 $O(n)$ 的时间复杂度,但通用性较差。 + +## 11.1.2. 理想排序算法 + +- **运行快**,即时间复杂度低; +- **稳定排序**,即排序后相等元素的相对位置不变化; +- **原地排序**,即运行中不使用额外的辅助空间; +- **正向自适应性**,即算法的运行效率不会在某些输入数据下发生劣化; + +然而,**没有排序算法同时具备以上所有特性**。排序算法的选型使用取决于具体的列表类型、列表长度、元素分布等因素。 diff --git a/build/chapter_sorting/merge_sort.md b/build/chapter_sorting/merge_sort.md new file mode 100755 index 000000000..cf2854917 --- /dev/null +++ b/build/chapter_sorting/merge_sort.md @@ -0,0 +1,475 @@ +--- +comments: true +--- + +# 11.5. 归并排序 + +「归并排序 Merge Sort」是算法中“分治思想”的典型体现,其有「划分」和「合并」两个阶段: + +1. **划分阶段**:通过递归不断 **将数组从中点位置划分开**,将长数组的排序问题转化为短数组的排序问题; +2. **合并阶段**:划分到子数组长度为 1 时,开始向上合并,不断将 **左、右两个短排序数组** 合并为 **一个长排序数组**,直至合并至原数组时完成排序; + +![merge_sort_preview](merge_sort.assets/merge_sort_preview.png) + +

Fig. 归并排序两阶段:划分与合并

+ +## 11.5.1. 算法流程 + +**「递归划分」** 从顶至底递归地 **将数组从中点切为两个子数组**,直至长度为 1 ; + +1. 计算数组中点 `mid` ,递归划分左子数组(区间 `[left, mid]` )和右子数组(区间 `[mid + 1, right]` ); +2. 递归执行 `1.` 步骤,直至子数组区间长度为 1 时,终止递归划分; + +**「回溯合并」** 从底至顶地将左子数组和右子数组合并为一个 **有序数组** ; + +需要注意,由于从长度为 1 的子数组开始合并,所以 **每个子数组都是有序的**。因此,合并任务本质是要 **将两个有序子数组合并为一个有序数组**。 + +=== "Step1" + ![merge_sort_step1](merge_sort.assets/merge_sort_step1.png) + +=== "Step2" + ![merge_sort_step2](merge_sort.assets/merge_sort_step2.png) + +=== "Step3" + ![merge_sort_step3](merge_sort.assets/merge_sort_step3.png) + +=== "Step4" + ![merge_sort_step4](merge_sort.assets/merge_sort_step4.png) + +=== "Step5" + ![merge_sort_step5](merge_sort.assets/merge_sort_step5.png) + +=== "Step6" + ![merge_sort_step6](merge_sort.assets/merge_sort_step6.png) + +=== "Step7" + ![merge_sort_step7](merge_sort.assets/merge_sort_step7.png) + +=== "Step8" + ![merge_sort_step8](merge_sort.assets/merge_sort_step8.png) + +=== "Step9" + ![merge_sort_step9](merge_sort.assets/merge_sort_step9.png) + +=== "Step10" + ![merge_sort_step10](merge_sort.assets/merge_sort_step10.png) + +观察发现,归并排序的递归顺序就是二叉树的「后序遍历」。 + +- **后序遍历**:先递归左子树、再递归右子树、最后处理根结点。 +- **归并排序**:先递归左子树、再递归右子树、最后处理合并。 + +=== "Java" + + ```java title="merge_sort.java" + /** + * 合并左子数组和右子数组 + * 左子数组区间 [left, mid] + * 右子数组区间 [mid + 1, right] + */ + void merge(int[] nums, int left, int mid, int right) { + // 初始化辅助数组 + int[] tmp = Arrays.copyOfRange(nums, left, right + 1); + // 左子数组的起始索引和结束索引 + int leftStart = left - left, leftEnd = mid - left; + // 右子数组的起始索引和结束索引 + int rightStart = mid + 1 - left, rightEnd = right - left; + // i, j 分别指向左子数组、右子数组的首元素 + int i = leftStart, j = rightStart; + // 通过覆盖原数组 nums 来合并左子数组和右子数组 + for (int k = left; k <= right; k++) { + // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ + if (i > leftEnd) + nums[k] = tmp[j++]; + // 否则,若“右子数组已全部合并完”或“左子数组元素 <= 右子数组元素”,则选取左子数组元素,并且 i++ + else if (j > rightEnd || tmp[i] <= tmp[j]) + nums[k] = tmp[i++]; + // 否则,若“左右子数组都未全部合并完”且“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ + else + nums[k] = tmp[j++]; + } + } + + /* 归并排序 */ + void mergeSort(int[] nums, int left, int right) { + // 终止条件 + if (left >= right) return; // 当子数组长度为 1 时终止递归 + // 递归划分 + int mid = (left + right) / 2; // 计算数组中点 + mergeSort(nums, left, mid); // 递归左子数组 + mergeSort(nums, mid + 1, right); // 递归右子数组 + // 回溯合并 + merge(nums, left, mid, right); + } + ``` + +=== "C++" + + ```cpp title="merge_sort.cpp" + /** + * 合并左子数组和右子数组 + * 左子数组区间 [left, mid] + * 右子数组区间 [mid + 1, right] + */ + void merge(vector& nums, int left, int mid, int right) { + // 初始化辅助数组 + vector tmp(nums.begin() + left, nums.begin() + right + 1); + // 左子数组的起始索引和结束索引 + int leftStart = left - left, leftEnd = mid - left; + // 右子数组的起始索引和结束索引 + int rightStart = mid + 1 - left, rightEnd = right - left; + // i, j 分别指向左子数组、右子数组的首元素 + int i = leftStart, j = rightStart; + // 通过覆盖原数组 nums 来合并左子数组和右子数组 + for (int k = left; k <= right; k++) { + // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ + if (i > leftEnd) + nums[k] = tmp[j++]; + // 否则,若“右子数组已全部合并完”或“左子数组元素 <= 右子数组元素”,则选取左子数组元素,并且 i++ + else if (j > rightEnd || tmp[i] <= tmp[j]) + nums[k] = tmp[i++]; + // 否则,若“左右子数组都未全部合并完”且“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ + else + nums[k] = tmp[j++]; + } + } + + /* 归并排序 */ + void mergeSort(vector& nums, int left, int right) { + // 终止条件 + if (left >= right) return; // 当子数组长度为 1 时终止递归 + // 划分阶段 + int mid = (left + right) / 2; // 计算中点 + mergeSort(nums, left, mid); // 递归左子数组 + mergeSort(nums, mid + 1, right); // 递归右子数组 + // 合并阶段 + merge(nums, left, mid, right); + } + ``` + +=== "Python" + + ```python title="merge_sort.py" + """ 合并左子数组和右子数组 """ + # 左子数组区间 [left, mid] + # 右子数组区间 [mid + 1, right] + def merge(nums, left, mid, right): + # 初始化辅助数组 借助 copy模块 + tmp = nums[left:right + 1] + # 左子数组的起始索引和结束索引 + left_start, left_end = left - left, mid - left + # 右子数组的起始索引和结束索引 + right_start, right_end = mid + 1 - left, right - left + # i, j 分别指向左子数组、右子数组的首元素 + i, j = left_start, right_start + # 通过覆盖原数组 nums 来合并左子数组和右子数组 + for k in range(left, right + 1): + # 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ + if i > left_end: + nums[k] = tmp[j] + j += 1 + # 否则,若“右子数组已全部合并完”或“左子数组元素 <= 右子数组元素”,则选取左子数组元素,并且 i++ + elif j > right_end or tmp[i] <= tmp[j]: + nums[k] = tmp[i] + i += 1 + # 否则,若“左右子数组都未全部合并完”且“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ + else: + nums[k] = tmp[j] + j += 1 + + """ 归并排序 """ + def merge_sort(nums, left, right): + # 终止条件 + if left >= right: + return # 当子数组长度为 1 时终止递归 + # 划分阶段 + mid = (left + right) // 2 # 计算中点 + merge_sort(nums, left, mid) # 递归左子数组 + merge_sort(nums, mid + 1, right) # 递归右子数组 + # 合并阶段 + merge(nums, left, mid, right) + ``` + +=== "Go" + + ```go title="merge_sort.go" + /* + 合并左子数组和右子数组 + 左子数组区间 [left, mid] + 右子数组区间 [mid + 1, right] + */ + func merge(nums []int, left, mid, right int) { + // 初始化辅助数组 借助 copy 模块 + tmp := make([]int, right-left+1) + for i := left; i <= right; i++ { + tmp[i-left] = nums[i] + } + // 左子数组的起始索引和结束索引 + leftStart, leftEnd := left-left, mid-left + // 右子数组的起始索引和结束索引 + rightStart, rightEnd := mid+1-left, right-left + // i, j 分别指向左子数组、右子数组的首元素 + i, j := leftStart, rightStart + // 通过覆盖原数组 nums 来合并左子数组和右子数组 + for k := left; k <= right; k++ { + // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ + if i > leftEnd { + nums[k] = tmp[j] + j++ + // 否则,若“右子数组已全部合并完”或“左子数组元素 <= 右子数组元素”,则选取左子数组元素,并且 i++ + } else if j > rightEnd || tmp[i] <= tmp[j] { + nums[k] = tmp[i] + i++ + // 否则,若“左右子数组都未全部合并完”且“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ + } else { + nums[k] = tmp[j] + j++ + } + } + } + + func mergeSort(nums []int, left, right int) { + // 终止条件 + if left >= right { + return + } + // 划分阶段 + mid := (left + right) / 2 + mergeSort(nums, left, mid) + mergeSort(nums, mid+1, right) + // 合并阶段 + merge(nums, left, mid, right) + } + ``` + +=== "JavaScript" + + ```js title="merge_sort.js" + /** + * 合并左子数组和右子数组 + * 左子数组区间 [left, mid] + * 右子数组区间 [mid + 1, right] + */ + function merge(nums, left, mid, right) { + // 初始化辅助数组 + let tmp = nums.slice(left, right + 1); + // 左子数组的起始索引和结束索引 + let leftStart = left - left, leftEnd = mid - left; + // 右子数组的起始索引和结束索引 + let rightStart = mid + 1 - left, rightEnd = right - left; + // i, j 分别指向左子数组、右子数组的首元素 + let i = leftStart, j = rightStart; + // 通过覆盖原数组 nums 来合并左子数组和右子数组 + for (let k = left; k <= right; k++) { + // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ + if (i > leftEnd) { + nums[k] = tmp[j++]; + // 否则,若“右子数组已全部合并完”或“左子数组元素 <= 右子数组元素”,则选取左子数组元素,并且 i++ + } else if (j > rightEnd || tmp[i] <= tmp[j]) { + nums[k] = tmp[i++]; + // 否则,若“左右子数组都未全部合并完”且“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ + } else { + nums[k] = tmp[j++]; + } + } + } + + /* 归并排序 */ + function mergeSort(nums, left, right) { + // 终止条件 + if (left >= right) return; // 当子数组长度为 1 时终止递归 + // 划分阶段 + let mid = Math.floor((left + right) / 2); // 计算中点 + mergeSort(nums, left, mid); // 递归左子数组 + mergeSort(nums, mid + 1, right); // 递归右子数组 + // 合并阶段 + merge(nums, left, mid, right); + } + ``` + +=== "TypeScript" + + ```typescript title="merge_sort.ts" + /** + * 合并左子数组和右子数组 + * 左子数组区间 [left, mid] + * 右子数组区间 [mid + 1, right] + */ + function merge(nums: number[], left: number, mid: number, right: number): void { + // 初始化辅助数组 + let tmp = nums.slice(left, right + 1); + // 左子数组的起始索引和结束索引 + let leftStart = left - left, leftEnd = mid - left; + // 右子数组的起始索引和结束索引 + let rightStart = mid + 1 - left, rightEnd = right - left; + // i, j 分别指向左子数组、右子数组的首元素 + let i = leftStart, j = rightStart; + // 通过覆盖原数组 nums 来合并左子数组和右子数组 + for (let k = left; k <= right; k++) { + // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ + if (i > leftEnd) { + nums[k] = tmp[j++]; + // 否则,若“右子数组已全部合并完”或“左子数组元素 <= 右子数组元素”,则选取左子数组元素,并且 i++ + } else if (j > rightEnd || tmp[i] <= tmp[j]) { + nums[k] = tmp[i++]; + // 否则,若“左右子数组都未全部合并完”且“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ + } else { + nums[k] = tmp[j++]; + } + } + } + + /* 归并排序 */ + function mergeSort(nums: number[], left: number, right: number): void { + // 终止条件 + if (left >= right) return; // 当子数组长度为 1 时终止递归 + // 划分阶段 + let mid = Math.floor((left + right) / 2); // 计算中点 + mergeSort(nums, left, mid); // 递归左子数组 + mergeSort(nums, mid + 1, right); // 递归右子数组 + // 合并阶段 + merge(nums, left, mid, right); + } + ``` + +=== "C" + + ```c title="merge_sort.c" + + ``` + +=== "C#" + + ```csharp title="merge_sort.cs" + /** + * 合并左子数组和右子数组 + * 左子数组区间 [left, mid] + * 右子数组区间 [mid + 1, right] + */ + void merge(int[] nums, int left, int mid, int right) + { + // 初始化辅助数组 + int[] tmp = nums[left..(right + 1)]; + // 左子数组的起始索引和结束索引 + int leftStart = left - left, leftEnd = mid - left; + // 右子数组的起始索引和结束索引 + int rightStart = mid + 1 - left, rightEnd = right - left; + // i, j 分别指向左子数组、右子数组的首元素 + int i = leftStart, j = rightStart; + // 通过覆盖原数组 nums 来合并左子数组和右子数组 + for (int k = left; k <= right; k++) + { + // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ + if (i > leftEnd) + nums[k] = tmp[j++]; + // 否则,若“右子数组已全部合并完”或“左子数组元素 <= 右子数组元素”,则选取左子数组元素,并且 i++ + else if (j > rightEnd || tmp[i] <= tmp[j]) + nums[k] = tmp[i++]; + // 否则,若“左右子数组都未全部合并完”且“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ + else + nums[k] = tmp[j++]; + } + } + + /* 归并排序 */ + void mergeSort(int[] nums, int left, int right) + { + // 终止条件 + if (left >= right) return; // 当子数组长度为 1 时终止递归 + // 划分阶段 + int mid = (left + right) / 2; // 计算中点 + mergeSort(nums, left, mid); // 递归左子数组 + mergeSort(nums, mid + 1, right); // 递归右子数组 + // 合并阶段 + merge(nums, left, mid, right); + } + ``` + +=== "Swift" + + ```swift title="merge_sort.swift" + /** + * 合并左子数组和右子数组 + * 左子数组区间 [left, mid] + * 右子数组区间 [mid + 1, right] + */ + func merge(nums: inout [Int], left: Int, mid: Int, right: Int) { + // 初始化辅助数组 + let tmp = Array(nums[left ..< (right + 1)]) + // 左子数组的起始索引和结束索引 + let leftStart = left - left + let leftEnd = mid - left + // 右子数组的起始索引和结束索引 + let rightStart = mid + 1 - left + let rightEnd = right - left + // i, j 分别指向左子数组、右子数组的首元素 + var i = leftStart + var j = rightStart + // 通过覆盖原数组 nums 来合并左子数组和右子数组 + for k in left ... right { + // 若“左子数组已全部合并完”,则选取右子数组元素,并且 j++ + if i > leftEnd { + nums[k] = tmp[j] + j += 1 + } + // 否则,若“右子数组已全部合并完”或“左子数组元素 <= 右子数组元素”,则选取左子数组元素,并且 i++ + else if j > rightEnd || tmp[i] <= tmp[j] { + nums[k] = tmp[i] + i += 1 + } + // 否则,若“左右子数组都未全部合并完”且“左子数组元素 > 右子数组元素”,则选取右子数组元素,并且 j++ + else { + nums[k] = tmp[j] + j += 1 + } + } + } + + /* 归并排序 */ + func mergeSort(nums: inout [Int], left: Int, right: Int) { + // 终止条件 + if left >= right { // 当子数组长度为 1 时终止递归 + return + } + // 划分阶段 + let mid = (left + right) / 2 // 计算中点 + mergeSort(nums: &nums, left: left, right: mid) // 递归左子数组 + mergeSort(nums: &nums, left: mid + 1, right: right) // 递归右子数组 + // 合并阶段 + merge(nums: &nums, left: left, mid: mid, right: right) + } + ``` + +=== "Zig" + + ```zig title="merge_sort.zig" + + ``` + +下面重点解释一下合并方法 `merge()` 的流程: + +1. 初始化一个辅助数组 `tmp` 暂存待合并区间 `[left, right]` 内的元素,后续通过覆盖原数组 `nums` 的元素来实现合并; +2. 初始化指针 `i` , `j` , `k` 分别指向左子数组、右子数组、原数组的首元素; +3. 循环判断 `tmp[i]` 和 `tmp[j]` 的大小,将较小的先覆盖至 `nums[k]` ,指针 `i` , `j` 根据判断结果交替前进(指针 `k` 也前进),直至两个子数组都遍历完,即可完成合并。 + +合并方法 `merge()` 代码中的主要难点: + +- `nums` 的待合并区间为 `[left, right]` ,而因为 `tmp` 只复制了 `nums` 该区间元素,所以 `tmp` 对应区间为 `[0, right - left]` ,**需要特别注意代码中各个变量的含义**。 +- 判断 `tmp[i]` 和 `tmp[j]` 的大小的操作中,还 **需考虑当子数组遍历完成后的索引越界问题**,即 `i > leftEnd` 和 `j > rightEnd` 的情况,索引越界的优先级是最高的,例如如果左子数组已经被合并完了,那么不用继续判断,直接合并右子数组元素即可。 + +## 11.5.2. 算法特性 + +- **时间复杂度 $O(n \log n)$** :划分形成高度为 $\log n$ 的递归树,每层合并的总操作数量为 $n$ ,总体使用 $O(n \log n)$ 时间。 +- **空间复杂度 $O(n)$** :需借助辅助数组实现合并,使用 $O(n)$ 大小的额外空间;递归深度为 $\log n$ ,使用 $O(\log n)$ 大小的栈帧空间。 +- **非原地排序**:辅助数组需要使用 $O(n)$ 额外空间。 +- **稳定排序**:在合并时可保证相等元素的相对位置不变。 +- **非自适应排序**:对于任意输入数据,归并排序的时间复杂度皆相同。 + +## 11.5.3. 链表排序 * + +归并排序有一个很特别的优势,用于排序链表时有很好的性能表现,**空间复杂度可被优化至 $O(1)$** ,这是因为: + +- 由于链表可仅通过改变指针来实现结点增删,因此“将两个短有序链表合并为一个长有序链表”无需使用额外空间,即回溯合并阶段不用像排序数组一样建立辅助数组 `tmp` ; +- 通过使用「迭代」代替「递归划分」,可省去递归使用的栈帧空间; + +> 详情参考:[148. 排序链表](https://leetcode-cn.com/problems/sort-list/solution/sort-list-gui-bing-pai-xu-lian-biao-by-jyd/) diff --git a/build/chapter_sorting/quick_sort.md b/build/chapter_sorting/quick_sort.md new file mode 100755 index 000000000..7ef7cd9c1 --- /dev/null +++ b/build/chapter_sorting/quick_sort.md @@ -0,0 +1,871 @@ +--- +comments: true +--- + +# 11.4. 快速排序 + +「快速排序 Quick Sort」是一种基于“分治思想”的排序算法,速度很快、应用很广。 + +快速排序的核心操作为「哨兵划分」,其目标为:选取数组某个元素为 **基准数**,将所有小于基准数的元素移动至其左边,大于基准数的元素移动至其右边。「哨兵划分」的实现流程为: + +1. 以数组最左端元素作为基准数,初始化两个指针 `i` , `j` 指向数组两端; +2. 设置一个循环,每轮中使用 `i` / `j` 分别寻找首个比基准数大 / 小的元素,并交换此两元素; +3. 不断循环步骤 `2.` ,直至 `i` , `j` 相遇时跳出,最终把基准数交换至两个子数组的分界线; + +「哨兵划分」执行完毕后,原数组被划分成两个部分,即 **左子数组** 和 **右子数组**,且满足 **左子数组任意元素 < 基准数 < 右子数组任意元素**。因此,接下来我们只需要排序两个子数组即可。 + +=== "Step 1" + ![pivot_division_step1](quick_sort.assets/pivot_division_step1.png) + +=== "Step 2" + ![pivot_division_step2](quick_sort.assets/pivot_division_step2.png) + +=== "Step 3" + ![pivot_division_step3](quick_sort.assets/pivot_division_step3.png) + +=== "Step 4" + ![pivot_division_step4](quick_sort.assets/pivot_division_step4.png) + +=== "Step 5" + ![pivot_division_step5](quick_sort.assets/pivot_division_step5.png) + +=== "Step 6" + ![pivot_division_step6](quick_sort.assets/pivot_division_step6.png) + +=== "Step 7" + ![pivot_division_step7](quick_sort.assets/pivot_division_step7.png) + +=== "Step 8" + ![pivot_division_step8](quick_sort.assets/pivot_division_step8.png) + +=== "Step 9" + ![pivot_division_step9](quick_sort.assets/pivot_division_step9.png) + +

Fig. 哨兵划分

+ +=== "Java" + + ``` java title="quick_sort.java" + /* 元素交换 */ + void swap(int[] nums, int i, int j) { + int tmp = nums[i]; + nums[i] = nums[j]; + nums[j] = tmp; + } + + /* 哨兵划分 */ + int partition(int[] nums, int left, int right) { + // 以 nums[left] 作为基准数 + int i = left, j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) + j--; // 从右向左找首个小于基准数的元素 + while (i < j && nums[i] <= nums[left]) + i++; // 从左向右找首个大于基准数的元素 + swap(nums, i, j); // 交换这两个元素 + } + swap(nums, i, left); // 将基准数交换至两子数组的分界线 + return i; // 返回基准数的索引 + } + ``` + +=== "C++" + + ```cpp title="quick_sort.cpp" + /* 元素交换 */ + void swap(vector& nums, int i, int j) { + int tmp = nums[i]; + nums[i] = nums[j]; + nums[j] = tmp; + } + + /* 哨兵划分 */ + int partition(vector& nums, int left, int right) { + // 以 nums[left] 作为基准数 + int i = left, j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) + j--; // 从右向左找首个小于基准数的元素 + while (i < j && nums[i] <= nums[left]) + i++; // 从左向右找首个大于基准数的元素 + swap(nums, i, j); // 交换这两个元素 + } + swap(nums, i, left); // 将基准数交换至两子数组的分界线 + return i; // 返回基准数的索引 + } + ``` + +=== "Python" + + ```python title="quick_sort.py" + """ 哨兵划分 """ + def partition(self, nums, left, right): + # 以 nums[left] 作为基准数 + i, j = left, right + while i < j: + while i < j and nums[j] >= nums[left]: + j -= 1 # 从右向左找首个小于基准数的元素 + while i < j and nums[i] <= nums[left]: + i += 1 # 从左向右找首个大于基准数的元素 + # 元素交换 + nums[i], nums[j] = nums[j], nums[i] + # 将基准数交换至两子数组的分界线 + nums[i], nums[left] = nums[left], nums[i] + return i # 返回基准数的索引 + ``` + +=== "Go" + + ```go title="quick_sort.go" + /* 哨兵划分 */ + func partition(nums []int, left, right int) int { + // 以 nums[left] 作为基准数 + i, j := left, right + for i < j { + for i < j && nums[j] >= nums[left] { + j-- // 从右向左找首个小于基准数的元素 + } + for i < j && nums[i] <= nums[left] { + i++ // 从左向右找首个大于基准数的元素 + } + //元素交换 + nums[i], nums[j] = nums[j], nums[i] + } + // 将基准数交换至两子数组的分界线 + nums[i], nums[left] = nums[left], nums[i] + return i // 返回基准数的索引 + } + ``` + +=== "JavaScript" + + ``` js title="quick_sort.js" + /* 元素交换 */ + function swap(nums, i, j) { + let tmp = nums[i]; + nums[i] = nums[j]; + nums[j] = tmp; + } + + /* 哨兵划分 */ + function partition(nums, left, right) { + // 以 nums[left] 作为基准数 + let i = left, j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) { + j -= 1; // 从右向左找首个小于基准数的元素 + } + while (i < j && nums[i] <= nums[left]) { + i += 1; // 从左向右找首个大于基准数的元素 + } + // 元素交换 + swap(nums, i, j); // 交换这两个元素 + } + swap(nums, i, left); // 将基准数交换至两子数组的分界线 + return i; // 返回基准数的索引 + } + ``` + +=== "TypeScript" + + ```typescript title="quick_sort.ts" + /* 元素交换 */ + function swap(nums: number[], i: number, j: number): void { + let tmp = nums[i]; + nums[i] = nums[j]; + nums[j] = tmp; + } + + /* 哨兵划分 */ + function partition(nums: number[], left: number, right: number): number { + // 以 nums[left] 作为基准数 + let i = left, j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) { + j -= 1; // 从右向左找首个小于基准数的元素 + } + while (i < j && nums[i] <= nums[left]) { + i += 1; // 从左向右找首个大于基准数的元素 + } + // 元素交换 + swap(nums, i, j); // 交换这两个元素 + } + swap(nums, i, left); // 将基准数交换至两子数组的分界线 + return i; // 返回基准数的索引 + } + ``` + +=== "C" + + ```c title="quick_sort.c" + + ``` + +=== "C#" + + ```csharp title="quick_sort.cs" + /* 元素交换 */ + void swap(int[] nums, int i, int j) + { + int tmp = nums[i]; + nums[i] = nums[j]; + nums[j] = tmp; + } + + /* 哨兵划分 */ + int partition(int[] nums, int left, int right) + { + // 以 nums[left] 作为基准数 + int i = left, j = right; + while (i < j) + { + while (i < j && nums[j] >= nums[left]) + j--; // 从右向左找首个小于基准数的元素 + while (i < j && nums[i] <= nums[left]) + i++; // 从左向右找首个大于基准数的元素 + swap(nums, i, j); // 交换这两个元素 + } + swap(nums, i, left); // 将基准数交换至两子数组的分界线 + return i; // 返回基准数的索引 + } + ``` + +=== "Swift" + + ```swift title="quick_sort.swift" + /* 元素交换 */ + func swap(nums: inout [Int], i: Int, j: Int) { + let tmp = nums[i] + nums[i] = nums[j] + nums[j] = tmp + } + + /* 哨兵划分 */ + func partition(nums: inout [Int], left: Int, right: Int) -> Int { + // 以 nums[left] 作为基准数 + var i = left + var j = right + while i < j { + while i < j, nums[j] >= nums[left] { + j -= 1 // 从右向左找首个小于基准数的元素 + } + while i < j, nums[i] <= nums[left] { + i += 1 // 从左向右找首个大于基准数的元素 + } + swap(nums: &nums, i: i, j: j) // 交换这两个元素 + } + swap(nums: &nums, i: i, j: left) // 将基准数交换至两子数组的分界线 + return i // 返回基准数的索引 + } + ``` + +=== "Zig" + + ```zig title="quick_sort.zig" + + ``` + +!!! note "快速排序的分治思想" + + 哨兵划分的实质是将 **一个长数组的排序问题** 简化为 **两个短数组的排序问题**。 + +## 11.4.1. 算法流程 + +1. 首先,对数组执行一次「哨兵划分」,得到待排序的 **左子数组** 和 **右子数组**; +2. 接下来,对 **左子数组** 和 **右子数组** 分别 **递归执行**「哨兵划分」…… +3. 直至子数组长度为 1 时 **终止递归**,即可完成对整个数组的排序; + +观察发现,快速排序和「二分查找」的原理类似,都是以对数阶的时间复杂度来缩小处理区间。 + +![quick_sort](quick_sort.assets/quick_sort.png) + +

Fig. 快速排序流程

+ +=== "Java" + + ```java title="quick_sort.java" + /* 快速排序 */ + void quickSort(int[] nums, int left, int right) { + // 子数组长度为 1 时终止递归 + if (left >= right) + return; + // 哨兵划分 + int pivot = partition(nums, left, right); + // 递归左子数组、右子数组 + quickSort(nums, left, pivot - 1); + quickSort(nums, pivot + 1, right); + } + ``` + +=== "C++" + + ```cpp title="quick_sort.cpp" + /* 快速排序 */ + void quickSort(vector& nums, int left, int right) { + // 子数组长度为 1 时终止递归 + if (left >= right) + return; + // 哨兵划分 + int pivot = partition(nums, left, right); + // 递归左子数组、右子数组 + quickSort(nums, left, pivot - 1); + quickSort(nums, pivot + 1, right); + } + ``` + +=== "Python" + + ```python title="quick_sort.py" + """ 快速排序 """ + def quick_sort(self, nums, left, right): + # 子数组长度为 1 时终止递归 + if left >= right: + return + # 哨兵划分 + pivot = self.partition(nums, left, right) + # 递归左子数组、右子数组 + self.quick_sort(nums, left, pivot - 1) + self.quick_sort(nums, pivot + 1, right) + ``` + +=== "Go" + + ```go title="quick_sort.go" + /* 快速排序 */ + func quickSort(nums []int, left, right int) { + // 子数组长度为 1 时终止递归 + if left >= right { + return + } + // 哨兵划分 + pivot := partition(nums, left, right) + // 递归左子数组、右子数组 + quickSort(nums, left, pivot-1) + quickSort(nums, pivot+1, right) + } + ``` + +=== "JavaScript" + + ```js title="quick_sort.js" + /* 快速排序 */ + function quickSort(nums, left, right) { + // 子数组长度为 1 时终止递归 + if (left >= right) return; + // 哨兵划分 + const pivot = partition(nums, left, right); + // 递归左子数组、右子数组 + quickSort(nums, left, pivot - 1); + quickSort(nums, pivot + 1, right); + } + ``` + +=== "TypeScript" + + ```typescript title="quick_sort.ts" + /* 快速排序 */ + function quickSort(nums: number[], left: number, right: number): void { + // 子数组长度为 1 时终止递归 + if (left >= right) { + return; + } + // 哨兵划分 + const pivot = partition(nums, left, right); + // 递归左子数组、右子数组 + quickSort(nums, left, pivot - 1); + quickSort(nums, pivot + 1, right); + } + ``` + +=== "C" + + ```c title="quick_sort.c" + + ``` + +=== "C#" + + ```csharp title="quick_sort.cs" + /* 快速排序 */ + void quickSort(int[] nums, int left, int right) + { + // 子数组长度为 1 时终止递归 + if (left >= right) + return; + // 哨兵划分 + int pivot = partition(nums, left, right); + // 递归左子数组、右子数组 + quickSort(nums, left, pivot - 1); + quickSort(nums, pivot + 1, right); + } + + ``` + +=== "Swift" + + ```swift title="quick_sort.swift" + /* 快速排序 */ + func quickSort(nums: inout [Int], left: Int, right: Int) { + // 子数组长度为 1 时终止递归 + if left >= right { + return + } + // 哨兵划分 + let pivot = partition(nums: &nums, left: left, right: right) + // 递归左子数组、右子数组 + quickSort(nums: &nums, left: left, right: pivot - 1) + quickSort(nums: &nums, left: pivot + 1, right: right) + } + ``` + +=== "Zig" + + ```zig title="quick_sort.zig" + + ``` + +## 11.4.2. 算法特性 + +**平均时间复杂度 $O(n \log n)$** :平均情况下,哨兵划分的递归层数为 $\log n$ ,每层中的总循环数为 $n$ ,总体使用 $O(n \log n)$ 时间。 + +**最差时间复杂度 $O(n^2)$** :最差情况下,哨兵划分操作将长度为 $n$ 的数组划分为长度为 $0$ 和 $n - 1$ 的两个子数组,此时递归层数达到 $n$ 层,每层中的循环数为 $n$ ,总体使用 $O(n^2)$ 时间。 + +**空间复杂度 $O(n)$** :输入数组完全倒序下,达到最差递归深度 $n$ 。 + +**原地排序**:只在递归中使用 $O(\log n)$ 大小的栈帧空间。 + +**非稳定排序**:哨兵划分操作可能改变相等元素的相对位置。 + +**自适应排序**:最差情况下,时间复杂度劣化至 $O(n^2)$ 。 + +## 11.4.3. 快排为什么快? + +从命名能够看出,快速排序在效率方面一定“有两把刷子”。快速排序的平均时间复杂度虽然与「归并排序」和「堆排序」一致,但实际 **效率更高**,这是因为: + +- **出现最差情况的概率很低**:虽然快速排序的最差时间复杂度为 $O(n^2)$ ,不如归并排序,但绝大部分情况下,快速排序可以达到 $O(n \log n)$ 的复杂度。 +- **缓存使用效率高**:哨兵划分操作时,将整个子数组加载入缓存中,访问元素效率很高。而诸如「堆排序」需要跳跃式访问元素,因此不具有此特性。 +- **复杂度的常数系数低**:在提及的三种算法中,快速排序的 **比较**、**赋值**、**交换** 三种操作的总体数量最少(类似于「插入排序」快于「冒泡排序」的原因)。 + +## 11.4.4. 基准数优化 + +**普通快速排序在某些输入下的时间效率变差**。举个极端例子,假设输入数组是完全倒序的,由于我们选取最左端元素为基准数,那么在哨兵划分完成后,基准数被交换至数组最右端,从而 **左子数组长度为 $n - 1$、右子数组长度为 $0$** 。这样进一步递归下去,**每轮哨兵划分后的右子数组长度都为 $0$** ,分治策略失效,快速排序退化为「冒泡排序」了。 + +为了尽量避免这种情况发生,我们可以优化一下基准数的选取策略。首先,在哨兵划分中,我们可以 **随机选取一个元素作为基准数**。但如果运气很差,每次都选择到比较差的基准数,那么效率依然不好。 + +进一步地,我们可以在数组中选取 3 个候选元素(一般为数组的首、尾、中点元素),**并将三个候选元素的中位数作为基准数**,这样基准数“既不大也不小”的概率就大大提升了。当然,如果数组很长的话,我们也可以选取更多候选元素,来进一步提升算法的稳健性。采取该方法后,时间复杂度劣化至 $O(n^2)$ 的概率极低。 + +=== "Java" + + ```java title="quick_sort.java" + /* 选取三个元素的中位数 */ + int medianThree(int[] nums, int left, int mid, int right) { + // 使用了异或操作来简化代码 + // 异或规则为 0 ^ 0 = 1 ^ 1 = 0, 0 ^ 1 = 1 ^ 0 = 1 + if ((nums[left] < nums[mid]) ^ (nums[left] < nums[right])) + return left; + else if ((nums[mid] < nums[left]) ^ (nums[mid] < nums[right])) + return mid; + else + return right; + } + + /* 哨兵划分(三数取中值) */ + int partition(int[] nums, int left, int right) { + // 选取三个候选元素的中位数 + int med = medianThree(nums, left, (left + right) / 2, right); + // 将中位数交换至数组最左端 + swap(nums, left, med); + // 以 nums[left] 作为基准数 + // 下同省略... + } + ``` + +=== "C++" + + ```cpp title="quick_sort.cpp" + /* 选取三个元素的中位数 */ + int medianThree(vector& nums, int left, int mid, int right) { + // 使用了异或操作来简化代码 + // 异或规则为 0 ^ 0 = 1 ^ 1 = 0, 0 ^ 1 = 1 ^ 0 = 1 + if ((nums[left] < nums[mid]) ^ (nums[left] < nums[right])) + return left; + else if ((nums[mid] < nums[left]) ^ (nums[mid] < nums[right])) + return mid; + else + return right; + } + + /* 哨兵划分(三数取中值) */ + int partition(vector& nums, int left, int right) { + // 选取三个候选元素的中位数 + int med = medianThree(nums, left, (left + right) / 2, right); + // 将中位数交换至数组最左端 + swap(nums, left, med); + // 以 nums[left] 作为基准数 + // 下同省略... + } + ``` + +=== "Python" + + ```python title="quick_sort.py" + """ 选取三个元素的中位数 """ + def median_three(self, nums, left, mid, right): + # 使用了异或操作来简化代码 + # 异或规则为 0 ^ 0 = 1 ^ 1 = 0, 0 ^ 1 = 1 ^ 0 = 1 + if (nums[left] < nums[mid]) ^ (nums[left] < nums[right]): + return left + elif (nums[mid] < nums[left]) ^ (nums[mid] > nums[right]): + return mid + return right + + """ 哨兵划分(三数取中值) """ + def partition(self, nums, left, right): + # 以 nums[left] 作为基准数 + med = self.median_three(nums, left, (left + right) // 2, right) + # 将中位数交换至数组最左端 + nums[left], nums[med] = nums[med], nums[left] + # 以 nums[left] 作为基准数 + i, j = left, right + while i < j: + while i < j and nums[j] >= nums[left]: + j -= 1 # 从右向左找首个小于基准数的元素 + while i < j and nums[i] <= nums[left]: + i += 1 # 从左向右找首个大于基准数的元素 + # 元素交换 + nums[i], nums[j] = nums[j], nums[i] + # 将基准数交换至两子数组的分界线 + nums[i], nums[left] = nums[left], nums[i] + return i # 返回基准数的索引 + ``` + +=== "Go" + + ```go title="quick_sort.go" + /* 选取三个元素的中位数 */ + func medianThree(nums []int, left, mid, right int) int { + if (nums[left] < nums[mid]) != (nums[left] < nums[right]) { + return left + } else if (nums[mid] > nums[left]) != (nums[mid] > nums[right]) { + return mid + } + return right + } + + /* 哨兵划分(三数取中值)*/ + func partition(nums []int, left, right int) int { + // 以 nums[left] 作为基准数 + med := medianThree(nums, left, (left+right)/2, right) + // 将中位数交换至数组最左端 + nums[left], nums[med] = nums[med], nums[left] + // 以 nums[left] 作为基准数 + // 下同省略... + } + ``` + +=== "JavaScript" + + ```js title="quick_sort.js" + /* 选取三个元素的中位数 */ + function medianThree(nums, left, mid, right) { + // 使用了异或操作来简化代码 + // 异或规则为 0 ^ 0 = 1 ^ 1 = 0, 0 ^ 1 = 1 ^ 0 = 1 + if ((nums[left] < nums[mid]) ^ (nums[left] < nums[right])) + return left; + else if ((nums[mid] < nums[left]) ^ (nums[mid] < nums[right])) + return mid; + else + return right; + } + + /* 哨兵划分(三数取中值) */ + function partition(nums, left, right) { + // 选取三个候选元素的中位数 + let med = medianThree(nums, left, Math.floor((left + right) / 2), right); + // 将中位数交换至数组最左端 + swap(nums, left, med); + // 以 nums[left] 作为基准数 + // 下同省略... + } + ``` + +=== "TypeScript" + + ```typescript title="quick_sort.ts" + /* 选取三个元素的中位数 */ + function medianThree(nums: number[], left: number, mid: number, right: number): number { + // 使用了异或操作来简化代码 + // 异或规则为 0 ^ 0 = 1 ^ 1 = 0, 0 ^ 1 = 1 ^ 0 = 1 + if (Number(nums[left] < nums[mid]) ^ Number(nums[left] < nums[right])) { + return left; + } else if (Number(nums[mid] < nums[left]) ^ Number(nums[mid] < nums[right])) { + return mid; + } else { + return right; + } + } + + /* 哨兵划分(三数取中值) */ + function partition(nums: number[], left: number, right: number): number { + // 选取三个候选元素的中位数 + let med = medianThree(nums, left, Math.floor((left + right) / 2), right); + // 将中位数交换至数组最左端 + swap(nums, left, med); + // 以 nums[left] 作为基准数 + // 下同省略... + ``` + +=== "C" + + ```c title="quick_sort.c" + + ``` + +=== "C#" + + ```csharp title="quick_sort.cs" + /* 选取三个元素的中位数 */ + int medianThree(int[] nums, int left, int mid, int right) + { + // 使用了异或操作来简化代码 + // 异或规则为 0 ^ 0 = 1 ^ 1 = 0, 0 ^ 1 = 1 ^ 0 = 1 + if ((nums[left] < nums[mid]) ^ (nums[left] < nums[right])) + return left; + else if ((nums[mid] < nums[left]) ^ (nums[mid] < nums[right])) + return mid; + else + return right; + } + + /* 哨兵划分(三数取中值) */ + int partition(int[] nums, int left, int right) + { + // 选取三个候选元素的中位数 + int med = medianThree(nums, left, (left + right) / 2, right); + // 将中位数交换至数组最左端 + swap(nums, left, med); + // 以 nums[left] 作为基准数 + // 下同省略... + } + ``` + +=== "Swift" + + ```swift title="quick_sort.swift" + /* 选取三个元素的中位数 */ + func medianThree(nums: [Int], left: Int, mid: Int, right: Int) -> Int { + if (nums[left] < nums[mid]) != (nums[left] < nums[right]) { + return left + } else if (nums[mid] < nums[left]) != (nums[mid] < nums[right]) { + return mid + } else { + return right + } + } + + /* 哨兵划分(三数取中值) */ + func partition(nums: inout [Int], left: Int, right: Int) -> Int { + // 选取三个候选元素的中位数 + let med = medianThree(nums: nums, left: left, mid: (left + right) / 2, right: right) + // 将中位数交换至数组最左端 + swap(nums: &nums, i: left, j: med) + // 以 nums[left] 作为基准数 + // 下同省略... + } + ``` + +=== "Zig" + + ```zig title="quick_sort.zig" + + ``` + +## 11.4.5. 尾递归优化 + +**普通快速排序在某些输入下的空间效率变差**。仍然以完全倒序的输入数组为例,由于每轮哨兵划分后右子数组长度为 0 ,那么将形成一个高度为 $n - 1$ 的递归树,此时使用的栈帧空间大小劣化至 $O(n)$ 。 + +为了避免栈帧空间的累积,我们可以在每轮哨兵排序完成后,判断两个子数组的长度大小,仅递归排序较短的子数组。由于较短的子数组长度不会超过 $\frac{n}{2}$ ,因此这样做能保证递归深度不超过 $\log n$ ,即最差空间复杂度被优化至 $O(\log n)$ 。 + +=== "Java" + + ```java title="quick_sort.java" + /* 快速排序(尾递归优化) */ + void quickSort(int[] nums, int left, int right) { + // 子数组长度为 1 时终止 + while (left < right) { + // 哨兵划分操作 + int pivot = partition(nums, left, right); + // 对两个子数组中较短的那个执行快排 + if (pivot - left < right - pivot) { + quickSort(nums, left, pivot - 1); // 递归排序左子数组 + left = pivot + 1; // 剩余待排序区间为 [pivot + 1, right] + } else { + quickSort(nums, pivot + 1, right); // 递归排序右子数组 + right = pivot - 1; // 剩余待排序区间为 [left, pivot - 1] + } + } + } + ``` + +=== "C++" + + ```cpp title="quick_sort.cpp" + /* 快速排序(尾递归优化) */ + void quickSort(vector& nums, int left, int right) { + // 子数组长度为 1 时终止 + while (left < right) { + // 哨兵划分操作 + int pivot = partition(nums, left, right); + // 对两个子数组中较短的那个执行快排 + if (pivot - left < right - pivot) { + quickSort(nums, left, pivot - 1); // 递归排序左子数组 + left = pivot + 1; // 剩余待排序区间为 [pivot + 1, right] + } else { + quickSort(nums, pivot + 1, right); // 递归排序右子数组 + right = pivot - 1; // 剩余待排序区间为 [left, pivot - 1] + } + } + } + ``` + +=== "Python" + + ```python title="quick_sort.py" + """ 快速排序(尾递归优化) """ + def quick_sort(self, nums, left, right): + # 子数组长度为 1 时终止 + while left < right: + # 哨兵划分操作 + pivot = self.partition(nums, left, right) + # 对两个子数组中较短的那个执行快排 + if pivot - left < right - pivot: + self.quick_sort(nums, left, pivot - 1) # 递归排序左子数组 + left = pivot + 1 # 剩余待排序区间为 [pivot + 1, right] + else: + self.quick_sort(nums, pivot + 1, right) # 递归排序右子数组 + right = pivot - 1 # 剩余待排序区间为 [left, pivot - 1] + ``` + +=== "Go" + + ```go title="quick_sort.go" + /* 快速排序(尾递归优化)*/ + func quickSort(nums []int, left, right int) { + // 子数组长度为 1 时终止 + for left < right { + // 哨兵划分操作 + pivot := partition(nums, left, right) + // 对两个子数组中较短的那个执行快排 + if pivot-left < right-pivot { + quickSort(nums, left, pivot-1) // 递归排序左子数组 + left = pivot + 1 // 剩余待排序区间为 [pivot + 1, right] + } else { + quickSort(nums, pivot+1, right) // 递归排序右子数组 + right = pivot - 1 // 剩余待排序区间为 [left, pivot - 1] + } + } + } + ``` + +=== "JavaScript" + + ```js title="quick_sort.js" + /* 快速排序(尾递归优化) */ + function quickSort(nums, left, right) { + // 子数组长度为 1 时终止 + while (left < right) { + // 哨兵划分操作 + let pivot = partition(nums, left, right); + // 对两个子数组中较短的那个执行快排 + if (pivot - left < right - pivot) { + quickSort(nums, left, pivot - 1); // 递归排序左子数组 + left = pivot + 1; // 剩余待排序区间为 [pivot + 1, right] + } else { + quickSort(nums, pivot + 1, right); // 递归排序右子数组 + right = pivot - 1; // 剩余待排序区间为 [left, pivot - 1] + } + } + } + ``` + +=== "TypeScript" + + ```typescript title="quick_sort.ts" + /* 快速排序(尾递归优化) */ + function quickSort(nums: number[], left: number, right: number): void { + // 子数组长度为 1 时终止 + while (left < right) { + // 哨兵划分操作 + let pivot = partition(nums, left, right); + // 对两个子数组中较短的那个执行快排 + if (pivot - left < right - pivot) { + quickSort(nums, left, pivot - 1); // 递归排序左子数组 + left = pivot + 1; // 剩余待排序区间为 [pivot + 1, right] + } else { + quickSort(nums, pivot + 1, right); // 递归排序右子数组 + right = pivot - 1; // 剩余待排序区间为 [left, pivot - 1] + } + } + } + ``` + +=== "C" + + ```c title="quick_sort.c" + + ``` + +=== "C#" + + ```csharp title="quick_sort.cs" + /* 快速排序(尾递归优化) */ + void quickSort(int[] nums, int left, int right) + { + // 子数组长度为 1 时终止 + while (left < right) + { + // 哨兵划分操作 + int pivot = partition(nums, left, right); + // 对两个子数组中较短的那个执行快排 + if (pivot - left < right - pivot) + { + quickSort(nums, left, pivot - 1); // 递归排序左子数组 + left = pivot + 1; // 剩余待排序区间为 [pivot + 1, right] + } + else + { + quickSort(nums, pivot + 1, right); // 递归排序右子数组 + right = pivot - 1; // 剩余待排序区间为 [left, pivot - 1] + } + } + } + ``` + +=== "Swift" + + ```swift title="quick_sort.swift" + /* 快速排序(尾递归优化) */ + func quickSort(nums: inout [Int], left: Int, right: Int) { + var left = left + var right = right + // 子数组长度为 1 时终止 + while left < right { + // 哨兵划分操作 + let pivot = partition(nums: &nums, left: left, right: right) + // 对两个子数组中较短的那个执行快排 + if (pivot - left) < (right - pivot) { + quickSort(nums: &nums, left: left, right: pivot - 1) // 递归排序左子数组 + left = pivot + 1 // 剩余待排序区间为 [pivot + 1, right] + } else { + quickSort(nums: &nums, left: pivot + 1, right: right) // 递归排序右子数组 + right = pivot - 1 // 剩余待排序区间为 [left, pivot - 1] + } + } + } + ``` + +=== "Zig" + + ```zig title="quick_sort.zig" + + ``` diff --git a/build/chapter_sorting/summary.md b/build/chapter_sorting/summary.md new file mode 100644 index 000000000..71cc82609 --- /dev/null +++ b/build/chapter_sorting/summary.md @@ -0,0 +1,6 @@ +--- +comments: true +--- + +# 11.6. 小结 + diff --git a/build/chapter_stack_and_queue/deque.md b/build/chapter_stack_and_queue/deque.md new file mode 100644 index 000000000..2ed4c866f --- /dev/null +++ b/build/chapter_stack_and_queue/deque.md @@ -0,0 +1,733 @@ +--- +comments: true +--- + +# 5.3. 双向队列 + +对于队列,我们只能在头部删除或在尾部添加元素,而「双向队列 Deque」更加灵活,在其头部和尾部都能执行元素添加或删除操作。 + +![deque_operations](deque.assets/deque_operations.png) + +

Fig. 双向队列的操作

+ +## 5.3.1. 双向队列常用操作 + +双向队列的常用操作见下表,方法名需根据特定语言来确定。 + +

Table. 双向队列的常用操作

+ +
+ +| 方法名 | 描述 | 时间复杂度 | +| ------------ | ---------------- | ---------- | +| pushFirst() | 将元素添加至队首 | $O(1)$ | +| pushLast() | 将元素添加至队尾 | $O(1)$ | +| pollFirst() | 删除队首元素 | $O(1)$ | +| pollLast() | 删除队尾元素 | $O(1)$ | +| peekFirst() | 访问队首元素 | $O(1)$ | +| peekLast() | 访问队尾元素 | $O(1)$ | +| size() | 获取队列的长度 | $O(1)$ | +| isEmpty() | 判断队列是否为空 | $O(1)$ | + +
+ +相同地,我们可以直接使用编程语言实现好的双向队列类。 + +=== "Java" + + ```java title="deque.java" + /* 初始化双向队列 */ + Deque deque = new LinkedList<>(); + + /* 元素入队 */ + deque.offerLast(2); // 添加至队尾 + deque.offerLast(5); + deque.offerLast(4); + deque.offerFirst(3); // 添加至队首 + deque.offerFirst(1); + + /* 访问元素 */ + int peekFirst = deque.peekFirst(); // 队首元素 + int peekLast = deque.peekLast(); // 队尾元素 + + /* 元素出队 */ + int pollFirst = deque.pollFirst(); // 队首元素出队 + int pollLast = deque.pollLast(); // 队尾元素出队 + + /* 获取双向队列的长度 */ + int size = deque.size(); + + /* 判断双向队列是否为空 */ + boolean isEmpty = deque.isEmpty(); + ``` + +=== "C++" + + ```cpp title="deque.cpp" + /* 初始化双向队列 */ + deque deque; + + /* 元素入队 */ + deque.push_back(2); // 添加至队尾 + deque.push_back(5); + deque.push_back(4); + deque.push_front(3); // 添加至队首 + deque.push_front(1); + + /* 访问元素 */ + int front = deque.front(); // 队首元素 + int back = deque.back(); // 队尾元素 + + /* 元素出队 */ + deque.pop_front(); // 队首元素出队 + deque.pop_back(); // 队尾元素出队 + + /* 获取双向队列的长度 */ + int size = deque.size(); + + /* 判断双向队列是否为空 */ + bool empty = deque.empty(); + ``` + +=== "Python" + + ```python title="deque.py" + """ 初始化双向队列 """ + duque = deque() + + """ 元素入队 """ + duque.append(2) # 添加至队尾 + duque.append(5) + duque.append(4) + duque.appendleft(3) # 添加至队首 + duque.appendleft(1) + + """ 访问元素 """ + front = duque[0] # 队首元素 + rear = duque[-1] # 队尾元素 + + """ 元素出队 """ + pop_front = duque.popleft() # 队首元素出队 + pop_rear = duque.pop() # 队尾元素出队 + + """ 获取双向队列的长度 """ + size = len(duque) + + """ 判断双向队列是否为空 """ + is_empty = len(duque) == 0 + ``` + +=== "Go" + + ```go title="deque_test.go" + /* 初始化双向队列 */ + // 在 Go 中,将 list 作为双向队列使用 + deque := list.New() + + /* 元素入队 */ + deque.PushBack(2) // 添加至队尾 + deque.PushBack(5) + deque.PushBack(4) + deque.PushFront(3) // 添加至队首 + deque.PushFront(1) + + /* 访问元素 */ + front := deque.Front() // 队首元素 + rear := deque.Back() // 队尾元素 + + /* 元素出队 */ + deque.Remove(front) // 队首元素出队 + deque.Remove(rear) // 队尾元素出队 + + /* 获取双向队列的长度 */ + size := deque.Len() + + /* 判断双向队列是否为空 */ + isEmpty := deque.Len() == 0 + ``` + +=== "JavaScript" + + ```js title="deque.js" + /* 初始化双向队列 */ + // JavaScript 没有内置的双端队列,只能把 Array 当作双端队列来使用 + const deque = []; + + /* 元素入队 */ + deque.push(2); + deque.push(5); + deque.push(4); + // 请注意,由于是数组,unshift() 方法的时间复杂度为 O(n) + deque.unshift(3); + deque.unshift(1); + console.log("双向队列 deque = ", deque); + + /* 访问元素 */ + const peekFirst = deque[0]; + console.log("队首元素 peekFirst = " + peekFirst); + const peekLast = deque[deque.length - 1]; + console.log("队尾元素 peekLast = " + peekLast); + + /* 元素出队 */ + // 请注意,由于是数组,shift() 方法的时间复杂度为 O(n) + const popFront = deque.shift(); + console.log("队首出队元素 popFront = " + popFront + ",队首出队后 deque = " + deque); + const popBack = deque.pop(); + console.log("队尾出队元素 popBack = " + popBack + ",队尾出队后 deque = " + deque); + + /* 获取双向队列的长度 */ + const size = deque.length; + console.log("双向队列长度 size = " + size); + + /* 判断双向队列是否为空 */ + const isEmpty = size === 0; + console.log("双向队列是否为空 = " + isEmpty); + ``` + +=== "TypeScript" + + ```typescript title="deque.ts" + /* 初始化双向队列 */ + // TypeScript 没有内置的双端队列,只能把 Array 当作双端队列来使用 + const deque: number[] = []; + + /* 元素入队 */ + deque.push(2); + deque.push(5); + deque.push(4); + // 请注意,由于是数组,unshift() 方法的时间复杂度为 O(n) + deque.unshift(3); + deque.unshift(1); + console.log("双向队列 deque = ", deque); + + /* 访问元素 */ + const peekFirst: number = deque[0]; + console.log("队首元素 peekFirst = " + peekFirst); + const peekLast: number = deque[deque.length - 1]; + console.log("队尾元素 peekLast = " + peekLast); + + /* 元素出队 */ + // 请注意,由于是数组,shift() 方法的时间复杂度为 O(n) + const popFront: number = deque.shift() as number; + console.log("队首出队元素 popFront = " + popFront + ",队首出队后 deque = " + deque); + const popBack: number = deque.pop() as number; + console.log("队尾出队元素 popBack = " + popBack + ",队尾出队后 deque = " + deque); + + /* 获取双向队列的长度 */ + const size: number = deque.length; + console.log("双向队列长度 size = " + size); + + /* 判断双向队列是否为空 */ + const isEmpty: boolean = size === 0; + console.log("双向队列是否为空 = " + isEmpty); + ``` + +=== "C" + + ```c title="deque.c" + + ``` + +=== "C#" + + ```csharp title="deque.cs" + /* 初始化双向队列 */ + // 在 C# 中,将链表 LinkedList 看作双向队列来使用 + LinkedList deque = new LinkedList(); + + /* 元素入队 */ + deque.AddLast(2); // 添加至队尾 + deque.AddLast(5); + deque.AddLast(4); + deque.AddFirst(3); // 添加至队首 + deque.AddFirst(1); + + /* 访问元素 */ + int peekFirst = deque.First.Value; // 队首元素 + int peekLast = deque.Last.Value; // 队尾元素 + + /* 元素出队 */ + deque.RemoveFirst(); // 队首元素出队 + deque.RemoveLast(); // 队尾元素出队 + + /* 获取双向队列的长度 */ + int size = deque.Count; + + /* 判断双向队列是否为空 */ + bool isEmpty = deque.Count == 0; + ``` + +=== "Swift" + + ```swift title="deque.swift" + /* 初始化双向队列 */ + // Swift 没有内置的双向队列类,可以把 Array 当作双向队列来使用 + var deque: [Int] = [] + + /* 元素入队 */ + deque.append(2) // 添加至队尾 + deque.append(5) + deque.append(4) + deque.insert(3, at: 0) // 添加至队首 + deque.insert(1, at: 0) + + /* 访问元素 */ + let peekFirst = deque.first! // 队首元素 + let peekLast = deque.last! // 队尾元素 + + /* 元素出队 */ + // 使用 Array 模拟时 pollFirst 的复杂度为 O(n) + let pollFirst = deque.removeFirst() // 队首元素出队 + let pollLast = deque.removeLast() // 队尾元素出队 + + /* 获取双向队列的长度 */ + let size = deque.count + + /* 判断双向队列是否为空 */ + let isEmpty = deque.isEmpty + ``` + +=== "Zig" + + ```zig title="deque.zig" + + ``` + +## 5.3.2. 双向队列实现 + +双向队列需要一种可以在两端添加、两端删除的数据结构。与队列的实现方法类似,双向队列也可以使用双向链表和循环数组来实现。 + +### 基于双向链表的实现 + +我们将双向链表的头结点和尾结点分别看作双向队列的队首和队尾,并且实现在两端都能添加与删除结点。 + +=== "LinkedListDeque" + ![linkedlist_deque](deque.assets/linkedlist_deque.png) + +=== "pushLast()" + ![linkedlist_deque_push_last](deque.assets/linkedlist_deque_push_last.png) + +=== "pushFirst()" + ![linkedlist_deque_push_first](deque.assets/linkedlist_deque_push_first.png) + +=== "pollLast()" + ![linkedlist_deque_poll_last](deque.assets/linkedlist_deque_poll_last.png) + +=== "pollFirst()" + ![linkedlist_deque_poll_first](deque.assets/linkedlist_deque_poll_first.png) + +以下是使用双向链表实现双向队列的示例代码。 + +=== "Java" + + ```java title="linkedlist_deque.java" + /* 双向链表结点 */ + class ListNode { + int val; // 结点值 + ListNode next; // 后继结点引用(指针) + ListNode prev; // 前驱结点引用(指针) + ListNode(int val) { + this.val = val; + prev = next = null; + } + } + + /* 基于双向链表实现的双向队列 */ + class LinkedListDeque { + private ListNode front, rear; // 头结点 front ,尾结点 rear + private int size = 0; // 双向队列的长度 + + public LinkedListDeque() { + front = rear = null; + } + + /* 获取双向队列的长度 */ + public int size() { + return size; + } + + /* 判断双向队列是否为空 */ + public boolean isEmpty() { + return size() == 0; + } + + /* 入队操作 */ + private void push(int num, boolean isFront) { + ListNode node = new ListNode(num); + // 若链表为空,则令 front, rear 都指向 node + if (isEmpty()) + front = rear = node; + // 队首入队操作 + else if (isFront) { + // 将 node 添加至链表头部 + front.prev = node; + node.next = front; + front = node; // 更新头结点 + // 队尾入队操作 + } else { + // 将 node 添加至链表尾部 + rear.next = node; + node.prev = rear; + rear = node; // 更新尾结点 + } + size++; // 更新队列长度 + } + + /* 队首入队 */ + public void pushFirst(int num) { + push(num, true); + } + + /* 队尾入队 */ + public void pushLast(int num) { + push(num, false); + } + + /* 出队操作 */ + private Integer poll(boolean isFront) { + // 若队列为空,直接返回 null + if (isEmpty()) + return null; + int val; + // 队首出队操作 + if (isFront) { + val = front.val; // 暂存头结点值 + // 删除头结点 + ListNode fNext = front.next; + if (fNext != null) { + fNext.prev = null; + front.next = null; + } + front = fNext; // 更新头结点 + // 队尾出队操作 + } else { + val = rear.val; // 暂存尾结点值 + // 删除尾结点 + ListNode rPrev = rear.prev; + if (rPrev != null) { + rPrev.next = null; + rear.prev = null; + } + rear = rPrev; // 更新尾结点 + } + size--; // 更新队列长度 + return val; + } + + /* 队首出队 */ + public Integer pollFirst() { + return poll(true); + } + + /* 队尾出队 */ + public Integer pollLast() { + return poll(false); + } + + /* 访问队首元素 */ + public Integer peekFirst() { + return isEmpty() ? null : front.val; + } + + /* 访问队尾元素 */ + public Integer peekLast() { + return isEmpty() ? null : rear.val; + } + } + ``` + +=== "C++" + + ```cpp title="linkedlist_deque.cpp" + + ``` + +=== "Python" + + ```python title="linkedlist_deque.py" + + ``` + +=== "Go" + + ```go title="linkedlist_deque.go" + + ``` + +=== "JavaScript" + + ```js title="linkedlist_deque.js" + /* 双向链表结点 */ + class ListNode { + prev; // 前驱结点引用 (指针) + next; // 后继结点引用 (指针) + val; // 结点值 + + constructor(val) { + this.val = val; + this.next = null; + this.prev = null; + } + } + + /* 基于双向链表实现的双向队列 */ + class LinkedListDeque { + front; // 头结点 front + rear; // 尾结点 rear + len; // 双向队列的长度 + + constructor() { + this.front = null; + this.rear = null; + this.len = 0; + } + + /* 队尾入队操作 */ + pushLast(val) { + const node = new ListNode(val); + // 若链表为空,则令 front, rear 都指向 node + if (this.len === 0) { + this.front = node; + this.rear = node; + } else { + // 将 node 添加至链表尾部 + this.rear.next = node; + node.prev = this.rear; + this.rear = node; // 更新尾结点 + } + this.len++; + } + + /* 队首入队操作 */ + pushFirst(val) { + const node = new ListNode(val); + // 若链表为空,则令 front, rear 都指向 node + if (this.len === 0) { + this.front = node; + this.rear = node; + } else { + // 将 node 添加至链表头部 + this.front.prev = node; + node.next = this.front; + this.front = node; // 更新头结点 + } + this.len++; + } + + /* 队尾出队操作 */ + pollLast() { + if (this.len === 0) { + return null; + } + const value = this.rear.val; // 存储尾结点值 + // 删除尾结点 + let temp = this.rear.prev; + if (temp !== null) { + temp.next = null; + this.rear.prev = null; + } + this.rear = temp; // 更新尾结点 + this.len--; + return value; + } + + /* 队首出队操作 */ + pollFirst() { + if (this.len === 0) { + return null; + } + const value = this.front.val; // 存储尾结点值 + // 删除头结点 + let temp = this.front.next; + if (temp !== null) { + temp.prev = null; + this.front.next = null; + } + this.front = temp; // 更新头结点 + this.len--; + return value; + } + + /* 访问队尾元素 */ + peekLast() { + return this.len === 0 ? null : this.rear.val; + } + + /* 访问队首元素 */ + peekFirst() { + return this.len === 0 ? null : this.front.val; + } + + /* 获取双向队列的长度 */ + size() { + return this.len; + } + + /* 判断双向队列是否为空 */ + isEmpty() { + return this.len === 0; + } + + /* 打印双向队列 */ + print() { + const arr = []; + let temp = this.front; + while (temp !== null) { + arr.push(temp.val); + temp = temp.next; + } + console.log("[" + arr.join(", ") + "]"); + } + } + ``` + +=== "TypeScript" + + ```typescript title="linkedlist_deque.ts" + /* 双向链表结点 */ + class ListNode { + prev: ListNode; // 前驱结点引用 (指针) + next: ListNode; // 后继结点引用 (指针) + val: number; // 结点值 + + constructor(val: number) { + this.val = val; + this.next = null; + this.prev = null; + } + } + + /* 基于双向链表实现的双向队列 */ + class LinkedListDeque { + front: ListNode; // 头结点 front + rear: ListNode; // 尾结点 rear + len: number; // 双向队列的长度 + + constructor() { + this.front = null; + this.rear = null; + this.len = 0; + } + + /* 队尾入队操作 */ + pushLast(val: number): void { + const node: ListNode = new ListNode(val); + // 若链表为空,则令 front, rear 都指向 node + if (this.len === 0) { + this.front = node; + this.rear = node; + } else { + // 将 node 添加至链表尾部 + this.rear.next = node; + node.prev = this.rear; + this.rear = node; // 更新尾结点 + } + this.len++; + } + + /* 队首入队操作 */ + pushFirst(val: number): void { + const node: ListNode = new ListNode(val); + // 若链表为空,则令 front, rear 都指向 node + if (this.len === 0) { + this.front = node; + this.rear = node; + } else { + // 将 node 添加至链表头部 + this.front.prev = node; + node.next = this.front; + this.front = node; // 更新头结点 + } + this.len++; + } + + /* 队尾出队操作 */ + pollLast(): number { + if (this.len === 0) { + return null; + } + const value: number = this.rear.val; // 存储尾结点值 + // 删除尾结点 + let temp: ListNode = this.rear.prev; + if (temp !== null) { + temp.next = null; + this.rear.prev = null; + } + this.rear = temp; // 更新尾结点 + this.len--; + return value; + } + + /* 队首出队操作 */ + pollFirst(): number { + if (this.len === 0) { + return null; + } + const value: number = this.front.val; // 存储尾结点值 + // 删除头结点 + let temp: ListNode = this.front.next; + if (temp !== null) { + temp.prev = null; + this.front.next = null; + } + this.front = temp; // 更新头结点 + this.len--; + return value; + } + + /* 访问队尾元素 */ + peekLast(): number { + return this.len === 0 ? null : this.rear.val; + } + + /* 访问队首元素 */ + peekFirst(): number { + return this.len === 0 ? null : this.front.val; + } + + /* 获取双向队列的长度 */ + size(): number { + return this.len; + } + + /* 判断双向队列是否为空 */ + isEmpty(): boolean { + return this.len === 0; + } + + /* 打印双向队列 */ + print(): void { + const arr: number[] = []; + let temp: ListNode = this.front; + while (temp !== null) { + arr.push(temp.val); + temp = temp.next; + } + console.log("[" + arr.join(", ") + "]"); + } + } + ``` + +=== "C" + + ```c title="linkedlist_deque.c" + + ``` + +=== "C#" + + ```csharp title="linkedlist_deque.cs" + + ``` + +=== "Swift" + + ```swift title="linkedlist_deque.swift" + + ``` + +=== "Zig" + + ```zig title="linkedlist_deque.zig" + + ``` diff --git a/build/chapter_stack_and_queue/queue.md b/build/chapter_stack_and_queue/queue.md new file mode 100755 index 000000000..88de0a0bf --- /dev/null +++ b/build/chapter_stack_and_queue/queue.md @@ -0,0 +1,1293 @@ +--- +comments: true +--- + +# 5.2. 队列 + +「队列 Queue」是一种遵循「先入先出 first in, first out」数据操作规则的线性数据结构。顾名思义,队列模拟的是排队现象,即外面的人不断加入队列尾部,而处于队列头部的人不断地离开。 + +我们将队列头部称为「队首」,队列尾部称为「队尾」,将把元素加入队尾的操作称为「入队」,删除队首元素的操作称为「出队」。 + +![queue_operations](queue.assets/queue_operations.png) + +

Fig. 队列的先入先出特性

+ +## 5.2.1. 队列常用操作 + +队列的常用操作见下表,方法名需根据特定语言来确定。 + +

Table. 队列的常用操作

+ +
+ +| 方法名 | 描述 | 时间复杂度 | +| --------- | -------------------------- | -------- | +| push() | 元素入队,即将元素添加至队尾 | $O(1)$ | +| poll() | 队首元素出队 | $O(1)$ | +| front() | 访问队首元素 | $O(1)$ | +| size() | 获取队列的长度 | $O(1)$ | +| isEmpty() | 判断队列是否为空 | $O(1)$ | + +
+ +我们可以直接使用编程语言实现好的队列类。 + +=== "Java" + + ```java title="queue.java" + /* 初始化队列 */ + Queue queue = new LinkedList<>(); + + /* 元素入队 */ + queue.offer(1); + queue.offer(3); + queue.offer(2); + queue.offer(5); + queue.offer(4); + + /* 访问队首元素 */ + int peek = queue.peek(); + + /* 元素出队 */ + int poll = queue.poll(); + + /* 获取队列的长度 */ + int size = queue.size(); + + /* 判断队列是否为空 */ + boolean isEmpty = queue.isEmpty(); + ``` + +=== "C++" + + ```cpp title="queue.cpp" + /* 初始化队列 */ + queue queue; + + /* 元素入队 */ + queue.push(1); + queue.push(3); + queue.push(2); + queue.push(5); + queue.push(4); + + /* 访问队首元素 */ + int front = queue.front(); + + /* 元素出队 */ + queue.pop(); + + /* 获取队列的长度 */ + int size = queue.size(); + + /* 判断队列是否为空 */ + bool empty = queue.empty(); + ``` + +=== "Python" + + ```python title="queue.py" + """ 初始化队列 """ + # 在 Python 中,我们一般将双向队列类 deque 看作队列使用 + # 虽然 queue.Queue() 是纯正的队列类,但不太好用,因此不建议 + que = collections.deque() + + """ 元素入队 """ + que.append(1) + que.append(3) + que.append(2) + que.append(5) + que.append(4) + + """ 访问队首元素 """ + front = que[0]; + + """ 元素出队 """ + pop = que.popleft() + + """ 获取队列的长度 """ + size = len(que) + + """ 判断队列是否为空 """ + is_empty = len(que) == 0 + ``` + +=== "Go" + + ```go title="queue_test.go" + /* 初始化队列 */ + // 在 Go 中,将 list 作为队列来使用 + queue := list.New() + + /* 元素入队 */ + queue.PushBack(1) + queue.PushBack(3) + queue.PushBack(2) + queue.PushBack(5) + queue.PushBack(4) + + /* 访问队首元素 */ + peek := queue.Front() + + /* 元素出队 */ + poll := queue.Front() + queue.Remove(poll) + + /* 获取队列的长度 */ + size := queue.Len() + + /* 判断队列是否为空 */ + isEmpty := queue.Len() == 0 + ``` + +=== "JavaScript" + + ```js title="queue.js" + /* 初始化队列 */ + // JavaScript 没有内置的队列,可以把 Array 当作队列来使用 + const queue = []; + + /* 元素入队 */ + queue.push(1); + queue.push(3); + queue.push(2); + queue.push(5); + queue.push(4); + + /* 访问队首元素 */ + const peek = queue[0]; + + /* 元素出队 */ + // 底层是数组,因此 shift() 方法的时间复杂度为 O(n) + const poll = queue.shift(); + + /* 获取队列的长度 */ + const size = queue.length; + + /* 判断队列是否为空 */ + const empty = queue.length === 0; + ``` + +=== "TypeScript" + + ```typescript title="queue.ts" + /* 初始化队列 */ + // TypeScript 没有内置的队列,可以把 Array 当作队列来使用 + const queue: number[] = []; + + /* 元素入队 */ + queue.push(1); + queue.push(3); + queue.push(2); + queue.push(5); + queue.push(4); + + /* 访问队首元素 */ + const peek = queue[0]; + + /* 元素出队 */ + // 底层是数组,因此 shift() 方法的时间复杂度为 O(n) + const poll = queue.shift(); + + /* 获取队列的长度 */ + const size = queue.length; + + /* 判断队列是否为空 */ + const empty = queue.length === 0; + ``` + +=== "C" + + ```c title="queue.c" + + ``` + +=== "C#" + + ```csharp title="queue.cs" + /* 初始化队列 */ + Queue queue = new(); + + /* 元素入队 */ + queue.Enqueue(1); + queue.Enqueue(3); + queue.Enqueue(2); + queue.Enqueue(5); + queue.Enqueue(4); + + /* 访问队首元素 */ + int peek = queue.Peek(); + + /* 元素出队 */ + int poll = queue.Dequeue(); + + /* 获取队列的长度 */ + int size = queue.Count(); + + /* 判断队列是否为空 */ + bool isEmpty = queue.Count() == 0; + ``` + +=== "Swift" + + ```swift title="queue.swift" + /* 初始化队列 */ + // Swift 没有内置的队列类,可以把 Array 当作队列来使用 + var queue: [Int] = [] + + /* 元素入队 */ + queue.append(1) + queue.append(3) + queue.append(2) + queue.append(5) + queue.append(4) + + /* 访问队首元素 */ + let peek = queue.first! + + /* 元素出队 */ + // 使用 Array 模拟时 poll 的复杂度为 O(n) + let pool = queue.removeFirst() + + /* 获取队列的长度 */ + let size = queue.count + + /* 判断队列是否为空 */ + let isEmpty = queue.isEmpty + ``` + +=== "Zig" + + ```zig title="queue.zig" + + ``` + +## 5.2.2. 队列实现 + +队列需要一种可以在一端添加,并在另一端删除的数据结构,也可以使用链表或数组来实现。 + +### 基于链表的实现 + +我们将链表的「头结点」和「尾结点」分别看作是队首和队尾,并规定队尾只可添加结点,队首只可删除结点。 + +=== "LinkedListQueue" + ![linkedlist_queue](queue.assets/linkedlist_queue.png) + +=== "push()" + ![linkedlist_queue_push](queue.assets/linkedlist_queue_push.png) + +=== "poll()" + ![linkedlist_queue_poll](queue.assets/linkedlist_queue_poll.png) + +以下是使用链表实现队列的示例代码。 + +=== "Java" + + ```java title="linkedlist_queue.java" + /* 基于链表实现的队列 */ + class LinkedListQueue { + private ListNode front, rear; // 头结点 front ,尾结点 rear + private int queSize = 0; + + public LinkedListQueue() { + front = null; + rear = null; + } + /* 获取队列的长度 */ + public int size() { + return queSize; + } + /* 判断队列是否为空 */ + public boolean isEmpty() { + return size() == 0; + } + /* 入队 */ + public void push(int num) { + // 尾结点后添加 num + ListNode node = new ListNode(num); + // 如果队列为空,则令头、尾结点都指向该结点 + if (front == null) { + front = node; + rear = node; + // 如果队列不为空,则将该结点添加到尾结点后 + } else { + rear.next = node; + rear = node; + } + queSize++; + } + /* 出队 */ + public int poll() { + int num = peek(); + // 删除头结点 + front = front.next; + queSize--; + return num; + } + /* 访问队首元素 */ + public int peek() { + if (size() == 0) + throw new EmptyStackException(); + return front.val; + } + } + ``` + +=== "C++" + + ```cpp title="linkedlist_queue.cpp" + /* 基于链表实现的队列 */ + class LinkedListQueue { + private: + ListNode *front, *rear; // 头结点 front ,尾结点 rear + int queSize; + + public: + LinkedListQueue() { + front = nullptr; + rear = nullptr; + queSize = 0; + } + ~LinkedListQueue() { + delete front; + delete rear; + } + /* 获取队列的长度 */ + int size() { + return queSize; + } + /* 判断队列是否为空 */ + bool empty() { + return queSize == 0; + } + /* 入队 */ + void push(int num) { + // 尾结点后添加 num + ListNode* node = new ListNode(num); + // 如果队列为空,则令头、尾结点都指向该结点 + if (front == nullptr) { + front = node; + rear = node; + } + // 如果队列不为空,则将该结点添加到尾结点后 + else { + rear->next = node; + rear = node; + } + queSize++; + } + /* 出队 */ + void poll() { + int num = peek(); + // 删除头结点 + ListNode *tmp = front; + front = front->next; + // 释放内存 + delete tmp; + queSize--; + } + /* 访问队首元素 */ + int peek() { + if (size() == 0) + throw out_of_range("队列为空"); + return front->val; + } + }; + ``` + +=== "Python" + + ```python title="linkedlist_queue.py" + """ 基于链表实现的队列 """ + class LinkedListQueue: + def __init__(self): + self.__front = None # 头结点 front + self.__rear = None # 尾结点 rear + self.__size = 0 + + """ 获取队列的长度 """ + def size(self): + return self.__size + + """ 判断队列是否为空 """ + def is_empty(self): + return not self.__front + + """ 入队 """ + def push(self, num): + # 尾结点后添加 num + node = ListNode(num) + # 如果队列为空,则令头、尾结点都指向该结点 + if self.__front is None: + self.__front = node + self.__rear = node + # 如果队列不为空,则将该结点添加到尾结点后 + else: + self.__rear.next = node + self.__rear = node + self.__size += 1 + + """ 出队 """ + def poll(self): + num = self.peek() + # 删除头结点 + self.__front = self.__front.next + self.__size -= 1 + return num + + """ 访问队首元素 """ + def peek(self): + if self.size() == 0: + print("队列为空") + return False + return self.__front.val + + """ 转化为列表用于打印 """ + def to_list(self): + queue = [] + temp = self.__front + while temp: + queue.append(temp.val) + temp = temp.next + return queue + ``` + +=== "Go" + + ```go title="linkedlist_queue.go" + /* 基于链表实现的队列 */ + type linkedListQueue struct { + // 使用内置包 list 来实现队列 + data *list.List + } + + // newLinkedListQueue 初始化链表 + func newLinkedListQueue() *linkedListQueue { + return &linkedListQueue{ + data: list.New(), + } + } + + // push 入队 + func (s *linkedListQueue) push(value any) { + s.data.PushBack(value) + } + + // poll 出队 + func (s *linkedListQueue) poll() any { + if s.isEmpty() { + return nil + } + e := s.data.Front() + s.data.Remove(e) + return e.Value + } + + // peek 访问队首元素 + func (s *linkedListQueue) peek() any { + if s.isEmpty() { + return nil + } + e := s.data.Front() + return e.Value + } + + // size 获取队列的长度 + func (s *linkedListQueue) size() int { + return s.data.Len() + } + + // isEmpty 判断队列是否为空 + func (s *linkedListQueue) isEmpty() bool { + return s.data.Len() == 0 + } + ``` + +=== "JavaScript" + + ```js title="linkedlist_queue.js" + /* 基于链表实现的队列 */ + class LinkedListQueue { + #front; // 头结点 #front + #rear; // 尾结点 #rear + #queSize = 0; + constructor() { + this.#front = null; + this.#rear = null; + } + /* 获取队列的长度 */ + get size() { + return this.#queSize; + } + /* 判断队列是否为空 */ + isEmpty() { + return this.size === 0; + } + /* 入队 */ + push(num) { + // 尾结点后添加 num + const node = new ListNode(num); + // 如果队列为空,则令头、尾结点都指向该结点 + if (!this.#front) { + this.#front = node; + this.#rear = node; + // 如果队列不为空,则将该结点添加到尾结点后 + } else { + this.#rear.next = node; + this.#rear = node; + } + this.#queSize++; + } + /* 出队 */ + poll() { + const num = this.peek(); + // 删除头结点 + this.#front = this.#front.next; + this.#queSize--; + return num; + } + /* 访问队首元素 */ + peek() { + if (this.size === 0) + throw new Error("队列为空"); + return this.#front.val; + } + } + ``` + +=== "TypeScript" + + ```typescript title="linkedlist_queue.ts" + /* 基于链表实现的队列 */ + class LinkedListQueue { + private front: ListNode | null; // 头结点 front + private rear: ListNode | null; // 尾结点 rear + private queSize: number = 0; + constructor() { + this.front = null; + this.rear = null; + } + /* 获取队列的长度 */ + get size(): number { + return this.queSize; + } + /* 判断队列是否为空 */ + isEmpty(): boolean { + return this.size === 0; + } + /* 入队 */ + push(num: number): void { + // 尾结点后添加 num + const node = new ListNode(num); + // 如果队列为空,则令头、尾结点都指向该结点 + if (!this.front) { + this.front = node; + this.rear = node; + // 如果队列不为空,则将该结点添加到尾结点后 + } else { + this.rear!.next = node; + this.rear = node; + } + this.queSize++; + } + /* 出队 */ + poll(): number { + const num = this.peek(); + if (!this.front) + throw new Error("队列为空") + // 删除头结点 + this.front = this.front.next; + this.queSize--; + return num; + } + /* 访问队首元素 */ + peek(): number { + if (this.size === 0) + throw new Error("队列为空"); + return this.front!.val; + } + } + ``` + +=== "C" + + ```c title="linkedlist_queue.c" + + ``` + +=== "C#" + + ```csharp title="linkedlist_queue.cs" + /* 基于链表实现的队列 */ + class LinkedListQueue + { + private ListNode? front, rear; // 头结点 front ,尾结点 rear + private int queSize = 0; + public LinkedListQueue() + { + front = null; + rear = null; + } + /* 获取队列的长度 */ + public int size() + { + return queSize; + } + /* 判断队列是否为空 */ + public bool isEmpty() + { + return size() == 0; + } + /* 入队 */ + public void push(int num) + { + // 尾结点后添加 num + ListNode node = new ListNode(num); + // 如果队列为空,则令头、尾结点都指向该结点 + if (front == null) + { + front = node; + rear = node; + // 如果队列不为空,则将该结点添加到尾结点后 + } + else if (rear != null) + { + rear.next = node; + rear = node; + } + queSize++; + } + /* 出队 */ + public int poll() + { + int num = peek(); + // 删除头结点 + front = front?.next; + queSize--; + return num; + } + /* 访问队首元素 */ + public int peek() + { + if (size() == 0 || front == null) + throw new Exception(); + return front.val; + } + } + ``` + +=== "Swift" + + ```swift title="linkedlist_queue.swift" + /* 基于链表实现的队列 */ + class LinkedListQueue { + private var front: ListNode? // 头结点 + private var rear: ListNode? // 尾结点 + private var _size = 0 + + init() {} + + /* 获取队列的长度 */ + func size() -> Int { + _size + } + + /* 判断队列是否为空 */ + func isEmpty() -> Bool { + size() == 0 + } + + /* 入队 */ + func push(num: Int) { + // 尾结点后添加 num + let node = ListNode(x: num) + // 如果队列为空,则令头、尾结点都指向该结点 + if front == nil { + front = node + rear = node + } + // 如果队列不为空,则将该结点添加到尾结点后 + else { + rear?.next = node + rear = node + } + _size += 1 + } + + /* 出队 */ + @discardableResult + func poll() -> Int { + let num = peek() + // 删除头结点 + front = front?.next + _size -= 1 + return num + } + + /* 访问队首元素 */ + func peek() -> Int { + if isEmpty() { + fatalError("队列为空") + } + return front!.val + } + } + ``` + +=== "Zig" + + ```zig title="linkedlist_queue.zig" + + ``` + +### 基于数组的实现 + +数组的删除首元素的时间复杂度为 $O(n)$ ,因此不适合直接用来实现队列。然而,我们可以借助两个指针 `front` , `rear` 来分别记录队首和队尾的索引位置,在入队 / 出队时分别将 `front` / `rear` 向后移动一位即可,这样每次仅需操作一个元素,时间复杂度降至 $O(1)$ 。 + +=== "ArrayQueue" + ![array_queue](queue.assets/array_queue.png) + +=== "push()" + ![array_queue_push](queue.assets/array_queue_push.png) + +=== "poll()" + ![array_queue_poll](queue.assets/array_queue_poll.png) + +细心的同学可能会发现一个问题,即在入队与出队的过程中,两个指针都在向后移动,**在到达尾部后则无法继续移动了**。 + +为了解决此问题,我们可以采取一个取巧方案,**即将数组看作是“环形”的**。具体做法是规定指针越过数组尾部后,再次回到头部接续遍历,这样相当于使数组“首尾相连”了。在环形数组的设定下,获取长度 `size()` 、入队 `push()` 、出队 `poll()` 方法都需要做相应的取余操作处理,使得当尾指针绕回数组头部时,仍然可以正确处理操作。 + +=== "Java" + + ```java title="array_queue.java" + /* 基于环形数组实现的队列 */ + class ArrayQueue { + private int[] nums; // 用于存储队列元素的数组 + private int front; // 队首指针,指向队首元素 + private int queSize; // 队列长度 + + public ArrayQueue(int capacity) { + nums = new int[capacity]; + front = queSize = 0; + } + + /* 获取队列的容量 */ + public int capacity() { + return nums.length; + } + + /* 获取队列的长度 */ + public int size() { + return queSize; + } + + /* 判断队列是否为空 */ + public boolean isEmpty() { + return queSize == 0; + } + + /* 入队 */ + public void push(int num) { + if (queSize == capacity()) { + System.out.println("队列已满"); + return; + } + // 计算尾指针,指向队尾索引 + 1 + // 通过取余操作,实现 rear 越过数组尾部后回到头部 + int rear = (front + queSize) % capacity(); + // 尾结点后添加 num + nums[rear] = num; + queSize++; + } + + /* 出队 */ + public int poll() { + int num = peek(); + // 队首指针向后移动一位,若越过尾部则返回到数组头部 + front = (front + 1) % capacity(); + queSize--; + return num; + } + + /* 访问队首元素 */ + public int peek() { + if (isEmpty()) + throw new EmptyStackException(); + return nums[front]; + } + } + ``` + +=== "C++" + + ```cpp title="array_queue.cpp" + /* 基于环形数组实现的队列 */ + class ArrayQueue { + private: + int *nums; // 用于存储队列元素的数组 + int front; // 队首指针,指向队首元素 + int queSize; // 队列长度 + int queCapacity; // 队列容量 + + public: + ArrayQueue(int capacity) { + // 初始化数组 + nums = new int[capacity]; + queCapacity = capacity; + front = queSize = 0; + } + + ~ArrayQueue() { + delete[] nums; + } + + /* 获取队列的容量 */ + int capacity() { + return queCapacity; + } + + /* 获取队列的长度 */ + int size() { + return queSize; + } + + /* 判断队列是否为空 */ + bool empty() { + return size() == 0; + } + + /* 入队 */ + void push(int num) { + if (queSize == queCapacity) { + cout << "队列已满" << endl; + return; + } + // 计算队尾指针,指向队尾索引 + 1 + // 通过取余操作,实现 rear 越过数组尾部后回到头部 + int rear = (front + queSize) % queCapacity; + // 尾结点后添加 num + nums[rear] = num; + queSize++; + } + + /* 出队 */ + void poll() { + int num = peek(); + // 队首指针向后移动一位,若越过尾部则返回到数组头部 + front = (front + 1) % queCapacity; + queSize--; + } + + /* 访问队首元素 */ + int peek() { + if (empty()) + throw out_of_range("队列为空"); + return nums[front]; + } + }; + ``` + +=== "Python" + + ```python title="array_queue.py" + """ 基于环形数组实现的队列 """ + class ArrayQueue: + def __init__(self, size): + self.__nums = [0] * size # 用于存储队列元素的数组 + self.__front = 0 # 队首指针,指向队首元素 + self.__size = 0 # 队列长度 + + """ 获取队列的容量 """ + def capacity(self): + return len(self.__nums) + + """ 获取队列的长度 """ + def size(self): + return self.__size + + """ 判断队列是否为空 """ + def is_empty(self): + return self.__size == 0 + + """ 入队 """ + def push(self, num): + assert self.__size < self.capacity(), "队列已满" + # 计算尾指针,指向队尾索引 + 1 + # 通过取余操作,实现 rear 越过数组尾部后回到头部 + rear = (self.__front + self.__size) % self.capacity() + # 尾结点后添加 num + self.__nums[rear] = num + self.__size += 1 + + """ 出队 """ + def poll(self): + num = self.peek() + # 队首指针向后移动一位,若越过尾部则返回到数组头部 + self.__front = (self.__front + 1) % self.capacity() + self.__size -= 1 + return num + + """ 访问队首元素 """ + def peek(self): + assert not self.is_empty(), "队列为空" + return self.__nums[self.__front] + + """ 返回列表用于打印 """ + def to_list(self): + res = [0] * self.size() + j = self.__front + for i in range(self.size()): + res[i] = self.__nums[(j % self.capacity())] + j += 1 + return res + ``` + +=== "Go" + + ```go title="array_queue.go" + /* 基于环形数组实现的队列 */ + type arrayQueue struct { + nums []int // 用于存储队列元素的数组 + front int // 队首指针,指向队首元素 + queSize int // 队列长度 + queCapacity int // 队列容量(即最大容纳元素数量) + } + + // newArrayQueue 基于环形数组实现的队列 + func newArrayQueue(queCapacity int) *arrayQueue { + return &arrayQueue{ + nums: make([]int, queCapacity), + queCapacity: queCapacity, + front: 0, + queSize: 0, + } + } + + // size 获取队列的长度 + func (q *arrayQueue) size() int { + return q.queSize + } + + // isEmpty 判断队列是否为空 + func (q *arrayQueue) isEmpty() bool { + return q.queSize == 0 + } + + // push 入队 + func (q *arrayQueue) push(num int) { + // 当 rear == queCapacity 表示队列已满 + if q.queSize == q.queCapacity { + return + } + // 计算尾指针,指向队尾索引 + 1 + // 通过取余操作,实现 rear 越过数组尾部后回到头部 + rear := (q.front + q.queSize) % q.queCapacity + // 尾结点后添加 num + q.nums[rear] = num + q.queSize++ + } + + // poll 出队 + func (q *arrayQueue) poll() any { + num := q.peek() + // 队首指针向后移动一位,若越过尾部则返回到数组头部 + q.front = (q.front + 1) % q.queCapacity + q.queSize-- + return num + } + + // peek 访问队首元素 + func (q *arrayQueue) peek() any { + if q.isEmpty() { + return nil + } + return q.nums[q.front] + } + + // 获取 Slice 用于打印 + func (q *arrayQueue) toSlice() []int { + rear := (q.front + q.queSize) + if rear >= q.queCapacity { + rear %= q.queCapacity + return append(q.nums[q.front:], q.nums[:rear]...) + } + return q.nums[q.front:rear] + } + ``` + +=== "JavaScript" + + ```js title="array_queue.js" + /* 基于环形数组实现的队列 */ + class ArrayQueue { + #nums; // 用于存储队列元素的数组 + #front = 0; // 队首指针,指向队首元素 + #queSize = 0; // 队列长度 + + constructor(capacity) { + this.#nums = new Array(capacity); + } + + /* 获取队列的容量 */ + get capacity() { + return this.#nums.length; + } + + /* 获取队列的长度 */ + get size() { + return this.#queSize; + } + + /* 判断队列是否为空 */ + empty() { + return this.#queSize == 0; + } + + /* 入队 */ + push(num) { + if (this.size == this.capacity) { + console.log("队列已满"); + return; + } + // 计算尾指针,指向队尾索引 + 1 + // 通过取余操作,实现 rear 越过数组尾部后回到头部 + const rear = (this.#front + this.size) % this.capacity; + // 尾结点后添加 num + this.#nums[rear] = num; + this.#queSize++; + } + + /* 出队 */ + poll() { + const num = this.peek(); + // 队首指针向后移动一位,若越过尾部则返回到数组头部 + this.#front = (this.#front + 1) % this.capacity; + this.#queSize--; + return num; + } + + /* 访问队首元素 */ + peek() { + if (this.empty()) + throw new Error("队列为空"); + return this.#nums[this.#front]; + } + } + ``` + +=== "TypeScript" + + ```typescript title="array_queue.ts" + /* 基于环形数组实现的队列 */ + class ArrayQueue { + private nums: number[]; // 用于存储队列元素的数组 + private front: number; // 队首指针,指向队首元素 + private queSize: number; // 队列长度 + + constructor(capacity: number) { + this.nums = new Array(capacity); + this.front = this.queSize = 0; + } + + /* 获取队列的容量 */ + get capacity(): number { + return this.nums.length; + } + + /* 获取队列的长度 */ + get size(): number { + return this.queSize; + } + + /* 判断队列是否为空 */ + empty(): boolean { + return this.queSize == 0; + } + + /* 入队 */ + push(num: number): void { + if (this.size == this.capacity) { + console.log("队列已满"); + return; + } + // 计算尾指针,指向队尾索引 + 1 + // 通过取余操作,实现 rear 越过数组尾部后回到头部 + const rear = (this.front + this.queSize) % this.capacity; + // 尾结点后添加 num + this.nums[rear] = num; + this.queSize++; + } + + /* 出队 */ + poll(): number { + const num = this.peek(); + // 队首指针向后移动一位,若越过尾部则返回到数组头部 + this.front = (this.front + 1) % this.capacity; + this.queSize--; + return num; + } + + /* 访问队首元素 */ + peek(): number { + if (this.empty()) + throw new Error("队列为空"); + return this.nums[this.front]; + } + } + ``` + +=== "C" + + ```c title="array_queue.c" + + ``` + +=== "C#" + + ```csharp title="array_queue.cs" + /* 基于环形数组实现的队列 */ + class ArrayQueue + { + private int[] nums; // 用于存储队列元素的数组 + private int front; // 队首指针,指向队首元素 + private int queSize; // 队列长度 + + public ArrayQueue(int capacity) + { + nums = new int[capacity]; + front = queSize = 0; + } + + /* 获取队列的容量 */ + public int capacity() + { + return nums.Length; + } + + /* 获取队列的长度 */ + public int size() + { + return queSize; + } + + /* 判断队列是否为空 */ + public bool isEmpty() + { + return queSize == 0; + } + + /* 入队 */ + public void push(int num) + { + if (queSize == capacity()) + { + Console.WriteLine("队列已满"); + return; + } + // 计算尾指针,指向队尾索引 + 1 + // 通过取余操作,实现 rear 越过数组尾部后回到头部 + int rear = (front + queSize) % capacity(); + // 尾结点后添加 num + nums[rear] = num; + queSize++; + } + + /* 出队 */ + public int poll() + { + int num = peek(); + // 队首指针向后移动一位,若越过尾部则返回到数组头部 + front = (front + 1) % capacity(); + queSize--; + return num; + } + + /* 访问队首元素 */ + public int peek() + { + if (isEmpty()) + throw new Exception(); + return nums[front]; + } + } + ``` + +=== "Swift" + + ```swift title="array_queue.swift" + /* 基于环形数组实现的队列 */ + class ArrayQueue { + private var nums: [Int] // 用于存储队列元素的数组 + private var front = 0 // 队首指针,指向队首元素 + private var queSize = 0 // 队列长度 + + init(capacity: Int) { + // 初始化数组 + nums = Array(repeating: 0, count: capacity) + } + + /* 获取队列的容量 */ + func capacity() -> Int { + nums.count + } + + /* 获取队列的长度 */ + func size() -> Int { + queSize + } + + /* 判断队列是否为空 */ + func isEmpty() -> Bool { + queSize == 0 + } + + /* 入队 */ + func push(num: Int) { + if size() == capacity() { + print("队列已满") + return + } + // 计算尾指针,指向队尾索引 + 1 + // 通过取余操作,实现 rear 越过数组尾部后回到头部 + let rear = (front + queSize) % capacity() + // 尾结点后添加 num + nums[rear] = num + queSize += 1 + } + + /* 出队 */ + @discardableResult + func poll() -> Int { + let num = peek() + // 队首指针向后移动一位,若越过尾部则返回到数组头部 + front = (front + 1) % capacity() + queSize -= 1 + return num + } + + /* 访问队首元素 */ + func peek() -> Int { + if isEmpty() { + fatalError("队列为空") + } + return nums[front] + } + } + ``` + +=== "Zig" + + ```zig title="array_queue.zig" + + ``` + +以上代码仍存在局限性,即长度不可变。然而,我们可以通过将数组替换为列表(即动态数组)来引入扩容机制,有兴趣的同学可以尝试实现。 + +## 5.2.3. 两种实现对比 + +与栈的结论一致,在此不再赘述。 + +## 5.2.4. 队列典型应用 + +- **淘宝订单**。购物者下单后,订单就被加入到队列之中,随后系统再根据顺序依次处理队列中的订单。在双十一时,在短时间内会产生海量的订单,如何处理「高并发」则是工程师们需要重点思考的问题。 +- **各种待办事项**。例如打印机的任务队列、餐厅的出餐队列等等。 diff --git a/build/chapter_stack_and_queue/stack.md b/build/chapter_stack_and_queue/stack.md new file mode 100755 index 000000000..3eda1a4df --- /dev/null +++ b/build/chapter_stack_and_queue/stack.md @@ -0,0 +1,1086 @@ +--- +comments: true +--- + +# 5.1. 栈 + +「栈 Stack」是一种遵循「先入后出 first in, last out」数据操作规则的线性数据结构。我们可以将栈类比为放在桌面上的一摞盘子,如果需要拿出底部的盘子,则需要先将上面的盘子依次取出。 + +“盘子”是一种形象比喻,我们将盘子替换为任意一种元素(例如整数、字符、对象等),就得到了栈数据结构。 + +我们将这一摞元素的顶部称为「栈顶」,将底部称为「栈底」,将把元素添加到栈顶的操作称为「入栈」,将删除栈顶元素的操作称为「出栈」。 + +![stack_operations](stack.assets/stack_operations.png) + +

Fig. 栈的先入后出特性

+ +## 5.1.1. 栈常用操作 + +栈的常用操作见下表(方法命名以 Java 为例)。 + +

Table. 栈的常用操作

+ +
+ +| 方法 | 描述 | 时间复杂度 | +| --------- | ---------------------- | ---------- | +| push() | 元素入栈(添加至栈顶) | $O(1)$ | +| pop() | 栈顶元素出栈 | $O(1)$ | +| peek() | 访问栈顶元素 | $O(1)$ | +| size() | 获取栈的长度 | $O(1)$ | +| isEmpty() | 判断栈是否为空 | $O(1)$ | + +
+ +我们可以直接使用编程语言实现好的栈类。 某些语言并未专门提供栈类,但我们可以直接把该语言的「数组」或「链表」看作栈来使用,并通过“脑补”来屏蔽无关操作。 + +=== "Java" + + ```java title="stack.java" + /* 初始化栈 */ + // 在 Java 中,推荐将 ArrayList 当作栈来使用 + List stack = new ArrayList<>(); + + /* 元素入栈 */ + stack.add(1); + stack.add(3); + stack.add(2); + stack.add(5); + stack.add(4); + + /* 访问栈顶元素 */ + int peek = stack.get(stack.size() - 1); + + /* 元素出栈 */ + int pop = stack.remove(stack.size() - 1); + + /* 获取栈的长度 */ + int size = stack.size(); + + /* 判断是否为空 */ + boolean isEmpty = stack.isEmpty(); + ``` + +=== "C++" + + ```cpp title="stack.cpp" + /* 初始化栈 */ + stack stack; + + /* 元素入栈 */ + stack.push(1); + stack.push(3); + stack.push(2); + stack.push(5); + stack.push(4); + + /* 访问栈顶元素 */ + int top = stack.top(); + + /* 元素出栈 */ + stack.pop(); + + /* 获取栈的长度 */ + int size = stack.size(); + + /* 判断是否为空 */ + bool empty = stack.empty(); + ``` + +=== "Python" + + ```python title="stack.py" + """ 初始化栈 """ + # Python 没有内置的栈类,可以把 List 当作栈来使用 + stack = [] + + """ 元素入栈 """ + stack.append(1) + stack.append(3) + stack.append(2) + stack.append(5) + stack.append(4) + + """ 访问栈顶元素 """ + peek = stack[-1] + + """ 元素出栈 """ + pop = stack.pop() + + """ 获取栈的长度 """ + size = len(stack) + + """ 判断是否为空 """ + is_empty = len(stack) == 0 + ``` + +=== "Go" + + ```go title="stack_test.go" + /* 初始化栈 */ + // 在 Go 中,推荐将 Slice 当作栈来使用 + var stack []int + + /* 元素入栈 */ + stack = append(stack, 1) + stack = append(stack, 3) + stack = append(stack, 2) + stack = append(stack, 5) + stack = append(stack, 4) + + /* 访问栈顶元素 */ + peek := stack[len(stack)-1] + + /* 元素出栈 */ + pop := stack[len(stack)-1] + stack = stack[:len(stack)-1] + + /* 获取栈的长度 */ + size := len(stack) + + /* 判断是否为空 */ + isEmpty := len(stack) == 0 + ``` + +=== "JavaScript" + + ```js title="stack.js" + /* 初始化栈 */ + // Javascript 没有内置的栈类,可以把 Array 当作栈来使用 + const stack = []; + + /* 元素入栈 */ + stack.push(1); + stack.push(3); + stack.push(2); + stack.push(5); + stack.push(4); + + /* 访问栈顶元素 */ + const peek = stack[stack.length-1]; + + /* 元素出栈 */ + const pop = stack.pop(); + + /* 获取栈的长度 */ + const size = stack.length; + + /* 判断是否为空 */ + const is_empty = stack.length === 0; + ``` + +=== "TypeScript" + + ```typescript title="stack.ts" + /* 初始化栈 */ + // Typescript 没有内置的栈类,可以把 Array 当作栈来使用 + const stack: number[] = []; + + /* 元素入栈 */ + stack.push(1); + stack.push(3); + stack.push(2); + stack.push(5); + stack.push(4); + + /* 访问栈顶元素 */ + const peek = stack[stack.length - 1]; + + /* 元素出栈 */ + const pop = stack.pop(); + + /* 获取栈的长度 */ + const size = stack.length; + + /* 判断是否为空 */ + const is_empty = stack.length === 0; + ``` + +=== "C" + + ```c title="stack.c" + + ``` + +=== "C#" + + ```csharp title="stack.cs" + /* 初始化栈 */ + Stack stack = new (); + + /* 元素入栈 */ + stack.Push(1); + stack.Push(3); + stack.Push(2); + stack.Push(5); + stack.Push(4); + + /* 访问栈顶元素 */ + int peek = stack.Peek(); + + /* 元素出栈 */ + int pop = stack.Pop(); + + /* 获取栈的长度 */ + int size = stack.Count(); + + /* 判断是否为空 */ + bool isEmpty = stack.Count()==0; + ``` + +=== "Swift" + + ```swift title="stack.swift" + /* 初始化栈 */ + // Swift 没有内置的栈类,可以把 Array 当作栈来使用 + var stack: [Int] = [] + + /* 元素入栈 */ + stack.append(1) + stack.append(3) + stack.append(2) + stack.append(5) + stack.append(4) + + /* 访问栈顶元素 */ + let peek = stack.last! + + /* 元素出栈 */ + let pop = stack.removeLast() + + /* 获取栈的长度 */ + let size = stack.count + + /* 判断是否为空 */ + let isEmpty = stack.isEmpty + ``` + +=== "Zig" + + ```zig title="stack.zig" + + ``` + +## 5.1.2. 栈的实现 + +为了更加清晰地了解栈的运行机制,接下来我们来自己动手实现一个栈类。 + +栈规定元素是先入后出的,因此我们只能在栈顶添加或删除元素。然而,数组或链表都可以在任意位置添加删除元素,因此 **栈可被看作是一种受约束的数组或链表**。换言之,我们可以“屏蔽”数组或链表的部分无关操作,使之对外的表现逻辑符合栈的规定即可。 + +### 基于链表的实现 + +使用「链表」实现栈时,将链表的头结点看作栈顶,将尾结点看作栈底。 + +对于入栈操作,将元素插入到链表头部即可,这种结点添加方式被称为“头插法”。而对于出栈操作,则将头结点从链表中删除即可。 + +=== "LinkedListStack" + ![linkedlist_stack](stack.assets/linkedlist_stack.png) + +=== "push()" + ![linkedlist_stack_push](stack.assets/linkedlist_stack_push.png) + +=== "pop()" + ![linkedlist_stack_pop](stack.assets/linkedlist_stack_pop.png) + +以下是基于链表实现栈的示例代码。 + +=== "Java" + + ```java title="linkedlist_stack.java" + /* 基于链表实现的栈 */ + class LinkedListStack { + private ListNode stackPeek; // 将头结点作为栈顶 + private int stkSize = 0; // 栈的长度 + public LinkedListStack() { + stackPeek = null; + } + /* 获取栈的长度 */ + public int size() { + return stkSize; + } + /* 判断栈是否为空 */ + public boolean isEmpty() { + return size() == 0; + } + /* 入栈 */ + public void push(int num) { + ListNode node = new ListNode(num); + node.next = stackPeek; + stackPeek = node; + stkSize++; + } + /* 出栈 */ + public int pop() { + int num = peek(); + stackPeek = stackPeek.next; + stkSize--; + return num; + } + /* 访问栈顶元素 */ + public int peek() { + if (size() == 0) + throw new EmptyStackException(); + return stackPeek.val; + } + } + ``` + +=== "C++" + + ```cpp title="linkedlist_stack.cpp" + /* 基于链表实现的栈 */ + class LinkedListStack { + private: + ListNode* stackTop; // 将头结点作为栈顶 + int stkSize; // 栈的长度 + + public: + LinkedListStack() { + stackTop = nullptr; + stkSize = 0; + } + ~LinkedListStack() { + freeMemoryLinkedList(stackTop); + } + /* 获取栈的长度 */ + int size() { + return stkSize; + } + /* 判断栈是否为空 */ + bool empty() { + return size() == 0; + } + /* 入栈 */ + void push(int num) { + ListNode* node = new ListNode(num); + node->next = stackTop; + stackTop = node; + stkSize++; + } + /* 出栈 */ + void pop() { + int num = top(); + ListNode *tmp = stackTop; + stackTop = stackTop->next; + // 释放内存 + delete tmp; + stkSize--; + } + /* 访问栈顶元素 */ + int top() { + if (size() == 0) + throw out_of_range("栈为空"); + return stackTop->val; + } + }; + ``` + +=== "Python" + + ```python title="linkedlist_stack.py" + """ 基于链表实现的栈 """ + class LinkedListStack: + def __init__(self): + self.__peek = None + self.__size = 0 + + """ 获取栈的长度 """ + def size(self): + return self.__size + + """ 判断栈是否为空 """ + def is_empty(self): + return not self.__peek + + """ 入栈 """ + def push(self, val): + node = ListNode(val) + node.next = self.__peek + self.__peek = node + self.__size += 1 + + """ 出栈 """ + def pop(self): + num = self.peek() + self.__peek = self.__peek.next + self.__size -= 1 + return num + + """ 访问栈顶元素 """ + def peek(self): + # 判空处理 + if not self.__peek: return None + return self.__peek.val + + """ 转化为列表用于打印 """ + def to_list(self): + arr = [] + node = self.__peek + while node: + arr.append(node.val) + node = node.next + arr.reverse() + return arr + ``` + +=== "Go" + + ```go title="linkedlist_stack.go" + /* 基于链表实现的栈 */ + type linkedListStack struct { + // 使用内置包 list 来实现栈 + data *list.List + } + + // newLinkedListStack 初始化链表 + func newLinkedListStack() *linkedListStack { + return &linkedListStack{ + data: list.New(), + } + } + + // push 入栈 + func (s *linkedListStack) push(value int) { + s.data.PushBack(value) + } + + // pop 出栈 + func (s *linkedListStack) pop() any { + if s.isEmpty() { + return nil + } + e := s.data.Back() + s.data.Remove(e) + return e.Value + } + + // peek 访问栈顶元素 + func (s *linkedListStack) peek() any { + if s.isEmpty() { + return nil + } + e := s.data.Back() + return e.Value + } + + // size 获取栈的长度 + func (s *linkedListStack) size() int { + return s.data.Len() + } + + // isEmpty 判断栈是否为空 + func (s *linkedListStack) isEmpty() bool { + return s.data.Len() == 0 + } + ``` + +=== "JavaScript" + + ```js title="linkedlist_stack.js" + /* 基于链表实现的栈 */ + class LinkedListStack { + #stackPeek; // 将头结点作为栈顶 + #stkSize = 0; // 栈的长度 + + constructor() { + this.#stackPeek = null; + } + + /* 获取栈的长度 */ + get size() { + return this.#stkSize; + } + + /* 判断栈是否为空 */ + isEmpty() { + return this.size == 0; + } + + /* 入栈 */ + push(num) { + const node = new ListNode(num); + node.next = this.#stackPeek; + this.#stackPeek = node; + this.#stkSize++; + } + + /* 出栈 */ + pop() { + const num = this.peek(); + if (!this.#stackPeek) { + throw new Error("栈为空!"); + } + this.#stackPeek = this.#stackPeek.next; + this.#stkSize--; + return num; + } + + /* 访问栈顶元素 */ + peek() { + if (!this.#stackPeek) { + throw new Error("栈为空!"); + } + return this.#stackPeek.val; + } + + /* 将链表转化为 Array 并返回 */ + toArray() { + let node = this.#stackPeek; + const res = new Array(this.size); + for (let i = res.length - 1; i >= 0; i--) { + res[i] = node.val; + node = node.next; + } + return res; + } + } + ``` + +=== "TypeScript" + + ```typescript title="linkedlist_stack.ts" + /* 基于链表实现的栈 */ + class LinkedListStack { + private stackPeek: ListNode | null; // 将头结点作为栈顶 + private stkSize: number = 0; // 栈的长度 + + constructor() { + this.stackPeek = null; + } + + /* 获取栈的长度 */ + get size(): number { + return this.stkSize; + } + + /* 判断栈是否为空 */ + isEmpty(): boolean { + return this.size == 0; + } + + /* 入栈 */ + push(num: number): void { + const node = new ListNode(num); + node.next = this.stackPeek; + this.stackPeek = node; + this.stkSize++; + } + + /* 出栈 */ + pop(): number { + const num = this.peek(); + if (!this.stackPeek) { + throw new Error("栈为空!"); + } + this.stackPeek = this.stackPeek.next; + this.stkSize--; + return num; + } + + /* 访问栈顶元素 */ + peek(): number { + if (!this.stackPeek) { + throw new Error("栈为空!"); + } + return this.stackPeek.val; + } + + /* 将链表转化为 Array 并返回 */ + toArray(): number[] { + let node = this.stackPeek; + const res = new Array(this.size); + for (let i = res.length - 1; i >= 0; i--) { + res[i] = node!.val; + node = node!.next; + } + return res; + } + } + ``` + +=== "C" + + ```c title="linkedlist_stack.c" + + ``` + +=== "C#" + + ```csharp title="linkedlist_stack.cs" + /* 基于链表实现的栈 */ + class LinkedListStack + { + private ListNode stackPeek; // 将头结点作为栈顶 + private int stkSize = 0; // 栈的长度 + public LinkedListStack() + { + stackPeek = null; + } + /* 获取栈的长度 */ + public int size() + { + return stkSize; + } + /* 判断栈是否为空 */ + public bool isEmpty() + { + return size() == 0; + } + /* 入栈 */ + public void push(int num) + { + ListNode node = new ListNode(num); + node.next = stackPeek; + stackPeek = node; + stkSize++; + } + /* 出栈 */ + public int pop() + { + int num = peek(); + stackPeek = stackPeek?.next; + stkSize--; + return num; + } + /* 访问栈顶元素 */ + public int peek() + { + if (size() == 0) + throw new Exception(); + return stackPeek.val; + } + } + ``` + +=== "Swift" + + ```swift title="linkedlist_stack.swift" + /* 基于链表实现的栈 */ + class LinkedListStack { + private var _peek: ListNode? // 将头结点作为栈顶 + private var _size = 0 // 栈的长度 + + init() {} + + /* 获取栈的长度 */ + func size() -> Int { + _size + } + + /* 判断栈是否为空 */ + func isEmpty() -> Bool { + size() == 0 + } + + /* 入栈 */ + func push(num: Int) { + let node = ListNode(x: num) + node.next = _peek + _peek = node + _size += 1 + } + + /* 出栈 */ + @discardableResult + func pop() -> Int { + let num = peek() + _peek = _peek?.next + _size -= 1 + return num + } + + /* 访问栈顶元素 */ + func peek() -> Int { + if isEmpty() { + fatalError("栈为空") + } + return _peek!.val + } + } + ``` + +=== "Zig" + + ```zig title="linkedlist_stack.zig" + + ``` + +### 基于数组的实现 + +使用「数组」实现栈时,考虑将数组的尾部当作栈顶。这样设计下,「入栈」与「出栈」操作就对应在数组尾部「添加元素」与「删除元素」,时间复杂度都为 $O(1)$ 。 + +=== "ArrayStack" + ![array_stack](stack.assets/array_stack.png) + +=== "push()" + ![array_stack_push](stack.assets/array_stack_push.png) + +=== "pop()" + ![array_stack_pop](stack.assets/array_stack_pop.png) + +由于入栈的元素可能是源源不断的,因此可以使用支持动态扩容的「列表」,这样就无需自行实现数组扩容了。以下是示例代码。 + +=== "Java" + + ```java title="array_stack.java" + /* 基于数组实现的栈 */ + class ArrayStack { + private ArrayList stack; + public ArrayStack() { + // 初始化列表(动态数组) + stack = new ArrayList<>(); + } + /* 获取栈的长度 */ + public int size() { + return stack.size(); + } + /* 判断栈是否为空 */ + public boolean isEmpty() { + return size() == 0; + } + /* 入栈 */ + public void push(int num) { + stack.add(num); + } + /* 出栈 */ + public int pop() { + if (isEmpty()) + throw new EmptyStackException(); + return stack.remove(size() - 1); + } + /* 访问栈顶元素 */ + public int peek() { + if (isEmpty()) + throw new EmptyStackException(); + return stack.get(size() - 1); + } + } + ``` + +=== "C++" + + ```cpp title="array_stack.cpp" + /* 基于数组实现的栈 */ + class ArrayStack { + private: + vector stack; + + public: + /* 获取栈的长度 */ + int size() { + return stack.size(); + } + /* 判断栈是否为空 */ + bool empty() { + return stack.empty(); + } + /* 入栈 */ + void push(int num) { + stack.push_back(num); + } + /* 出栈 */ + void pop() { + int oldTop = top(); + stack.pop_back(); + } + /* 访问栈顶元素 */ + int top() { + if(empty()) + throw out_of_range("栈为空"); + return stack.back(); + } + }; + ``` + +=== "Python" + + ```python title="array_stack.py" + """ 基于数组实现的栈 """ + class ArrayStack: + def __init__(self): + self.__stack = [] + + """ 获取栈的长度 """ + def size(self): + return len(self.__stack) + + """ 判断栈是否为空 """ + def is_empty(self): + return self.__stack == [] + + """ 入栈 """ + def push(self, item): + self.__stack.append(item) + + """ 出栈 """ + def pop(self): + assert not self.is_empty(), "栈为空" + return self.__stack.pop() + + """ 访问栈顶元素 """ + def peek(self): + assert not self.is_empty(), "栈为空" + return self.__stack[-1] + + """ 返回列表用于打印 """ + def to_list(self): + return self.__stack + ``` + +=== "Go" + + ```go title="array_stack.go" + /* 基于数组实现的栈 */ + type arrayStack struct { + data []int // 数据 + } + + func newArrayStack() *arrayStack { + return &arrayStack{ + // 设置栈的长度为 0,容量为 16 + data: make([]int, 0, 16), + } + } + + // size 栈的长度 + func (s *arrayStack) size() int { + return len(s.data) + } + + // isEmpty 栈是否为空 + func (s *arrayStack) isEmpty() bool { + return s.size() == 0 + } + + // push 入栈 + func (s *arrayStack) push(v int) { + // 切片会自动扩容 + s.data = append(s.data, v) + } + + // pop 出栈 + func (s *arrayStack) pop() any { + // 弹出栈前,先判断是否为空 + if s.isEmpty() { + return nil + } + val := s.peek() + s.data = s.data[:len(s.data)-1] + return val + } + + // peek 获取栈顶元素 + func (s *arrayStack) peek() any { + if s.isEmpty() { + return nil + } + val := s.data[len(s.data)-1] + return val + } + ``` + +=== "JavaScript" + + ```js title="array_stack.js" + /* 基于数组实现的栈 */ + class ArrayStack { + stack; + constructor() { + this.stack = []; + } + /* 获取栈的长度 */ + get size() { + return this.stack.length; + } + /* 判断栈是否为空 */ + empty() { + return this.stack.length === 0; + } + /* 入栈 */ + push(num) { + this.stack.push(num); + } + /* 出栈 */ + pop() { + if (this.empty()) + throw new Error("栈为空"); + return this.stack.pop(); + } + /* 访问栈顶元素 */ + top() { + if (this.empty()) + throw new Error("栈为空"); + return this.stack[this.stack.length - 1]; + } + }; + ``` + +=== "TypeScript" + + ```typescript title="array_stack.ts" + /* 基于数组实现的栈 */ + class ArrayStack { + private stack: number[]; + constructor() { + this.stack = []; + } + /* 获取栈的长度 */ + get size(): number { + return this.stack.length; + } + /* 判断栈是否为空 */ + empty(): boolean { + return this.stack.length === 0; + } + /* 入栈 */ + push(num: number): void { + this.stack.push(num); + } + /* 出栈 */ + pop(): number | undefined { + if (this.empty()) + throw new Error('栈为空'); + return this.stack.pop(); + } + /* 访问栈顶元素 */ + top(): number | undefined { + if (this.empty()) + throw new Error('栈为空'); + return this.stack[this.stack.length - 1]; + } + }; + ``` + +=== "C" + + ```c title="array_stack.c" + + ``` + +=== "C#" + + ```csharp title="array_stack.cs" + /* 基于数组实现的栈 */ + class ArrayStack + { + private List stack; + public ArrayStack() + { + // 初始化列表(动态数组) + stack = new(); + } + /* 获取栈的长度 */ + public int size() + { + return stack.Count(); + } + /* 判断栈是否为空 */ + public bool isEmpty() + { + return size() == 0; + } + /* 入栈 */ + public void push(int num) + { + stack.Add(num); + } + /* 出栈 */ + public int pop() + { + if (isEmpty()) + throw new Exception(); + var val = peek(); + stack.RemoveAt(size() - 1); + return val; + } + /* 访问栈顶元素 */ + public int peek() + { + if (isEmpty()) + throw new Exception(); + return stack[size() - 1]; + } + } + ``` + +=== "Swift" + + ```swift title="array_stack.swift" + /* 基于数组实现的栈 */ + class ArrayStack { + private var stack: [Int] + + init() { + // 初始化列表(动态数组) + stack = [] + } + + /* 获取栈的长度 */ + func size() -> Int { + stack.count + } + + /* 判断栈是否为空 */ + func isEmpty() -> Bool { + stack.isEmpty + } + + /* 入栈 */ + func push(num: Int) { + stack.append(num) + } + + /* 出栈 */ + @discardableResult + func pop() -> Int { + if isEmpty() { + fatalError("栈为空") + } + return stack.removeLast() + } + + /* 访问栈顶元素 */ + func peek() -> Int { + if isEmpty() { + fatalError("栈为空") + } + return stack.last! + } + } + ``` + +=== "Zig" + + ```zig title="array_stack.zig" + + ``` + +## 5.1.3. 两种实现对比 + +### 支持操作 + +两种实现都支持栈定义中的各项操作,数组实现额外支持随机访问,但这已经超出栈的定义范畴,一般不会用到。 + +### 时间效率 + +在数组(列表)实现中,入栈与出栈操作都是在预先分配好的连续内存中操作,具有很好的缓存本地性,效率很好。然而,如果入栈时超出数组容量,则会触发扩容机制,那么该次入栈操作的时间复杂度为 $O(n)$ 。 + +在链表实现中,链表的扩容非常灵活,不存在上述数组扩容时变慢的问题。然而,入栈操作需要初始化结点对象并修改指针,因而效率不如数组。进一步地思考,如果入栈元素不是 `int` 而是结点对象,那么就可以省去初始化步骤,从而提升效率。 + +综上所述,当入栈与出栈操作的元素是基本数据类型(例如 `int` , `double` )时,则结论如下: + +- 数组实现的栈在触发扩容时会变慢,但由于扩容是低频操作,因此 **总体效率更高**; +- 链表实现的栈可以提供 **更加稳定的效率表现**; + +### 空间效率 + +在初始化列表时,系统会给列表分配“初始容量”,该容量可能超过我们的需求。并且扩容机制一般是按照特定倍率(比如 2 倍)进行扩容,扩容后的容量也可能超出我们的需求。因此,**数组实现栈会造成一定的空间浪费**。 + +当然,由于结点需要额外存储指针,因此 **链表结点比数组元素占用更大**。 + +综上,我们不能简单地确定哪种实现更加省内存,需要 case-by-case 地分析。 + +## 5.1.4. 栈典型应用 + +- **浏览器中的后退与前进、软件中的撤销与反撤销**。每当我们打开新的网页,浏览器就将上一个网页执行入栈,这样我们就可以通过「后退」操作来回到上一页面,后退操作实际上是在执行出栈。如果要同时支持后退和前进,那么则需要两个栈来配合实现。 +- **程序内存管理**。每当调用函数时,系统就会在栈顶添加一个栈帧,用来记录函数的上下文信息。在递归函数中,向下递推会不断执行入栈,向上回溯阶段时出栈。 diff --git a/build/chapter_stack_and_queue/summary.md b/build/chapter_stack_and_queue/summary.md new file mode 100644 index 000000000..19cf6c8c3 --- /dev/null +++ b/build/chapter_stack_and_queue/summary.md @@ -0,0 +1,11 @@ +--- +comments: true +--- + +# 5.4. 小结 + +- 栈是一种遵循先入后出的数据结构,可以使用数组或链表实现。 +- 在时间效率方面,栈的数组实现具有更好的平均效率,但扩容时会导致单次入栈操作的时间复杂度劣化至 $O(n)$ 。相对地,栈的链表实现具有更加稳定的效率表现。 +- 在空间效率方面,栈的数组实现会造成一定空间浪费,然而链表结点比数组元素占用内存更大。 +- 队列是一种遵循先入先出的数据结构,可以使用数组或链表实现。对于两种实现的时间效率与空间效率对比,与上述栈的结论相同。 +- 双向队列的两端都可以添加与删除元素。 diff --git a/build/chapter_tree/avl_tree.md b/build/chapter_tree/avl_tree.md new file mode 100755 index 000000000..b1b2adb52 --- /dev/null +++ b/build/chapter_tree/avl_tree.md @@ -0,0 +1,1584 @@ +--- +comments: true +--- + +# 7.4. AVL 树 * + +在「二叉搜索树」章节中提到,在进行多次插入与删除操作后,二叉搜索树可能会退化为链表。此时所有操作的时间复杂度都会由 $O(\log n)$ 劣化至 $O(n)$ 。 + +如下图所示,执行两步删除结点后,该二叉搜索树就会退化为链表。 + +![degradation_from_removing_node](avl_tree.assets/degradation_from_removing_node.png) + +再比如,在以下完美二叉树中插入两个结点后,树严重向左偏斜,查找操作的时间复杂度也随之发生劣化。 + +![degradation_from_inserting_node](avl_tree.assets/degradation_from_inserting_node.png) + +G. M. Adelson-Velsky 和 E. M. Landis 在其 1962 年发表的论文 "An algorithm for the organization of information" 中提出了「AVL 树」。**论文中描述了一系列操作,使得在不断添加与删除结点后,AVL 树仍然不会发生退化**,进而使得各种操作的时间复杂度均能保持在 $O(\log n)$ 级别。 + +换言之,在频繁增删查改的使用场景中,AVL 树可始终保持很高的数据增删查改效率,具有很好的应用价值。 + +## 7.4.1. AVL 树常见术语 + +「AVL 树」既是「二叉搜索树」又是「平衡二叉树」,同时满足这两种二叉树的所有性质,因此又被称为「平衡二叉搜索树」。 + +### 结点高度 + +在 AVL 树的操作中,需要获取结点「高度 Height」,所以给 AVL 树的结点类添加 `height` 变量。 + +=== "Java" + + ```java title="avl_tree.java" + /* AVL 树结点类 */ + class TreeNode { + public int val; // 结点值 + public int height; // 结点高度 + public TreeNode left; // 左子结点 + public TreeNode right; // 右子结点 + public TreeNode(int x) { val = x; } + } + ``` + +=== "C++" + + ```cpp title="avl_tree.cpp" + /* AVL 树结点类 */ + struct TreeNode { + int val{}; // 结点值 + int height = 0; // 结点高度 + TreeNode *left{}; // 左子结点 + TreeNode *right{}; // 右子结点 + TreeNode() = default; + explicit TreeNode(int x) : val(x){} + }; + ``` + +=== "Python" + + ```python title="avl_tree.py" + """ AVL 树结点类 """ + class TreeNode: + def __init__(self, val=None, left=None, right=None): + self.val = val # 结点值 + self.height = 0 # 结点高度 + self.left = left # 左子结点引用 + self.right = right # 右子结点引用 + ``` + +=== "Go" + + ```go title="avl_tree.go" + /* AVL 树结点类 */ + type TreeNode struct { + Val int // 结点值 + Height int // 结点高度 + Left *TreeNode // 左子结点引用 + Right *TreeNode // 右子结点引用 + } + ``` + +=== "JavaScript" + + ```js title="avl_tree.js" + class TreeNode { + val; // 结点值 + left; // 左子结点指针 + right; // 右子结点指针 + height; //结点高度 + constructor(val, left, right, height) { + this.val = val === undefined ? 0 : val; + this.left = left === undefined ? null : left; + this.right = right === undefined ? null : right; + this.height = height === undefined ? 0 : height; + } + } + ``` + +=== "TypeScript" + + ```typescript title="avl_tree.ts" + + ``` + +=== "C" + + ```c title="avl_tree.c" + + ``` + +=== "C#" + + ```csharp title="avl_tree.cs" + /* AVL 树结点类 */ + class TreeNode { + public int val; // 结点值 + public int height; // 结点高度 + public TreeNode? left; // 左子结点 + public TreeNode? right; // 右子结点 + public TreeNode(int x) { val = x; } + } + ``` + +=== "Swift" + + ```swift title="avl_tree.swift" + /* AVL 树结点类 */ + class TreeNode { + var val: Int // 结点值 + var height: Int // 结点高度 + var left: TreeNode? // 左子结点 + var right: TreeNode? // 右子结点 + + init(x: Int) { + val = x + height = 0 + } + } + ``` + +=== "Zig" + + ```zig title="avl_tree.zig" + + ``` + +「结点高度」是最远叶结点到该结点的距离,即走过的「边」的数量。需要特别注意,**叶结点的高度为 0 ,空结点的高度为 -1**。我们封装两个工具函数,分别用于获取与更新结点的高度。 + +=== "Java" + + ```java title="avl_tree.java" + /* 获取结点高度 */ + int height(TreeNode node) { + // 空结点高度为 -1 ,叶结点高度为 0 + return node == null ? -1 : node.height; + } + + /* 更新结点高度 */ + void updateHeight(TreeNode node) { + // 结点高度等于最高子树高度 + 1 + node.height = Math.max(height(node.left), height(node.right)) + 1; + } + ``` + +=== "C++" + + ```cpp title="avl_tree.cpp" + /* 获取结点高度 */ + int height(TreeNode* node) { + // 空结点高度为 -1 ,叶结点高度为 0 + return node == nullptr ? -1 : node->height; + } + + /* 更新结点高度 */ + void updateHeight(TreeNode* node) { + // 结点高度等于最高子树高度 + 1 + node->height = max(height(node->left), height(node->right)) + 1; + } + ``` + +=== "Python" + + ```python title="avl_tree.py" + """ 获取结点高度 """ + def height(self, node: Optional[TreeNode]) -> int: + # 空结点高度为 -1 ,叶结点高度为 0 + if node is not None: + return node.height + return -1 + + """ 更新结点高度 """ + def __update_height(self, node: Optional[TreeNode]): + # 结点高度等于最高子树高度 + 1 + node.height = max([self.height(node.left), self.height(node.right)]) + 1 + ``` + +=== "Go" + + ```go title="avl_tree.go" + /* 获取结点高度 */ + func height(node *TreeNode) int { + // 空结点高度为 -1 ,叶结点高度为 0 + if node != nil { + return node.Height + } + return -1 + } + + /* 更新结点高度 */ + func updateHeight(node *TreeNode) { + lh := height(node.Left) + rh := height(node.Right) + // 结点高度等于最高子树高度 + 1 + if lh > rh { + node.Height = lh + 1 + } else { + node.Height = rh + 1 + } + } + ``` + +=== "JavaScript" + + ```js title="avl_tree.js" + /* 获取结点高度 */ + height(node) { + // 空结点高度为 -1 ,叶结点高度为 0 + return node === null ? -1 : node.height; + } + + /* 更新结点高度 */ + updateHeight(node) { + // 结点高度等于最高子树高度 + 1 + node.height = Math.max(this.height(node.left), this.height(node.right)) + 1; + } + ``` + +=== "TypeScript" + + ```typescript title="avl_tree.ts" + + ``` + +=== "C" + + ```c title="avl_tree.c" + + ``` + +=== "C#" + + ```csharp title="avl_tree.cs" + /* 获取结点高度 */ + public int height(TreeNode? node) + { + // 空结点高度为 -1 ,叶结点高度为 0 + return node == null ? -1 : node.height; + } + + /* 更新结点高度 */ + private void updateHeight(TreeNode node) + { + // 结点高度等于最高子树高度 + 1 + node.height = Math.Max(height(node.left), height(node.right)) + 1; + } + ``` + +=== "Swift" + + ```swift title="avl_tree.swift" + /* 获取结点高度 */ + func height(node: TreeNode?) -> Int { + // 空结点高度为 -1 ,叶结点高度为 0 + node == nil ? -1 : node!.height + } + + /* 更新结点高度 */ + func updateHeight(node: TreeNode?) { + // 结点高度等于最高子树高度 + 1 + node?.height = max(height(node: node?.left), height(node: node?.right)) + 1 + } + ``` + +=== "Zig" + + ```zig title="avl_tree.zig" + + ``` + +### 结点平衡因子 + +结点的「平衡因子 Balance Factor」是 **结点的左子树高度减去右子树高度**,并定义空结点的平衡因子为 0 。同样地,我们将获取结点平衡因子封装成函数,以便后续使用。 + +=== "Java" + + ```java title="avl_tree.java" + /* 获取结点平衡因子 */ + public int balanceFactor(TreeNode node) { + // 空结点平衡因子为 0 + if (node == null) return 0; + // 结点平衡因子 = 左子树高度 - 右子树高度 + return height(node.left) - height(node.right); + } + ``` + +=== "C++" + + ```cpp title="avl_tree.cpp" + /* 获取平衡因子 */ + int balanceFactor(TreeNode* node) { + // 空结点平衡因子为 0 + if (node == nullptr) return 0; + // 结点平衡因子 = 左子树高度 - 右子树高度 + return height(node->left) - height(node->right); + } + ``` + +=== "Python" + + ```python title="avl_tree.py" + """ 获取平衡因子 """ + def balance_factor(self, node: Optional[TreeNode]) -> int: + # 空结点平衡因子为 0 + if node is None: + return 0 + # 结点平衡因子 = 左子树高度 - 右子树高度 + return self.height(node.left) - self.height(node.right) + ``` + +=== "Go" + + ```go title="avl_tree.go" + /* 获取平衡因子 */ + func balanceFactor(node *TreeNode) int { + // 空结点平衡因子为 0 + if node == nil { + return 0 + } + // 结点平衡因子 = 左子树高度 - 右子树高度 + return height(node.Left) - height(node.Right) + } + ``` + +=== "JavaScript" + + ```js title="avl_tree.js" + /* 获取平衡因子 */ + balanceFactor(node) { + // 空结点平衡因子为 0 + if (node === null) return 0; + // 结点平衡因子 = 左子树高度 - 右子树高度 + return this.height(node.left) - this.height(node.right); + } + ``` + +=== "TypeScript" + + ```typescript title="avl_tree.ts" + + ``` + +=== "C" + + ```c title="avl_tree.c" + + + + ``` + +=== "C#" + + ```csharp title="avl_tree.cs" + /* 获取平衡因子 */ + public int balanceFactor(TreeNode? node) + { + // 空结点平衡因子为 0 + if (node == null) return 0; + // 结点平衡因子 = 左子树高度 - 右子树高度 + return height(node.left) - height(node.right); + } + ``` + +=== "Swift" + + ```swift title="avl_tree.swift" + /* 获取平衡因子 */ + func balanceFactor(node: TreeNode?) -> Int { + // 空结点平衡因子为 0 + guard let node = node else { return 0 } + // 结点平衡因子 = 左子树高度 - 右子树高度 + return height(node: node.left) - height(node: node.right) + } + ``` + +=== "Zig" + + ```zig title="avl_tree.zig" + + ``` + +!!! note + + 设平衡因子为 $f$ ,则一棵 AVL 树的任意结点的平衡因子皆满足 $-1 \le f \le 1$ 。 + +## 7.4.2. AVL 树旋转 + +AVL 树的独特之处在于「旋转 Rotation」的操作,其可 **在不影响二叉树中序遍历序列的前提下,使失衡结点重新恢复平衡**。换言之,旋转操作既可以使树保持为「二叉搜索树」,也可以使树重新恢复为「平衡二叉树」。 + +我们将平衡因子的绝对值 $> 1$ 的结点称为「失衡结点」。根据结点的失衡情况,旋转操作分为 **右旋、左旋、先右旋后左旋、先左旋后右旋**,接下来我们来一起来看看它们是如何操作的。 + +### Case 1 - 右旋 + +如下图所示(结点下方为「平衡因子」),从底至顶看,二叉树中首个失衡结点是 **结点 3**。我们聚焦在以该失衡结点为根结点的子树上,将该结点记为 `node` ,将其左子节点记为 `child` ,执行「右旋」操作。完成右旋后,该子树已经恢复平衡,并且仍然为二叉搜索树。 + +=== "Step 1" + ![right_rotate_step1](avl_tree.assets/right_rotate_step1.png) + +=== "Step 2" + ![right_rotate_step2](avl_tree.assets/right_rotate_step2.png) + +=== "Step 3" + ![right_rotate_step3](avl_tree.assets/right_rotate_step3.png) + +=== "Step 4" + ![right_rotate_step4](avl_tree.assets/right_rotate_step4.png) + +进而,如果结点 `child` 本身有右子结点(记为 `grandChild` ),则需要在「右旋」中添加一步:将 `grandChild` 作为 `node` 的左子结点。 + +![right_rotate_with_grandchild](avl_tree.assets/right_rotate_with_grandchild.png) + +“向右旋转”是一种形象化的说法,实际需要通过修改结点指针实现,代码如下所示。 + +=== "Java" + + ```java title="avl_tree.java" + /* 右旋操作 */ + TreeNode rightRotate(TreeNode node) { + TreeNode child = node.left; + TreeNode grandChild = child.right; + // 以 child 为原点,将 node 向右旋转 + child.right = node; + node.left = grandChild; + // 更新结点高度 + updateHeight(node); + updateHeight(child); + // 返回旋转后子树的根节点 + return child; + } + ``` + +=== "C++" + + ```cpp title="avl_tree.cpp" + /* 右旋操作 */ + TreeNode* rightRotate(TreeNode* node) { + TreeNode* child = node->left; + TreeNode* grandChild = child->right; + // 以 child 为原点,将 node 向右旋转 + child->right = node; + node->left = grandChild; + // 更新结点高度 + updateHeight(node); + updateHeight(child); + // 返回旋转后子树的根节点 + return child; + } + ``` + +=== "Python" + + ```python title="avl_tree.py" + """ 右旋操作 """ + def __right_rotate(self, node: Optional[TreeNode]) -> TreeNode: + child = node.left + grand_child = child.right + # 以 child 为原点,将 node 向右旋转 + child.right = node + node.left = grand_child + # 更新结点高度 + self.__update_height(node) + self.__update_height(child) + # 返回旋转后子树的根节点 + return child + ``` + +=== "Go" + + ```go title="avl_tree.go" + /* 右旋操作 */ + func rightRotate(node *TreeNode) *TreeNode { + child := node.Left + grandChild := child.Right + // 以 child 为原点,将 node 向右旋转 + child.Right = node + node.Left = grandChild + // 更新结点高度 + updateHeight(node) + updateHeight(child) + // 返回旋转后子树的根节点 + return child + } + ``` + +=== "JavaScript" + + ```js title="avl_tree.js" + /* 右旋操作 */ + rightRotate(node) { + let child = node.left; + let grandChild = child.right; + // 以 child 为原点,将 node 向右旋转 + child.right = node; + node.left = grandChild; + // 更新结点高度 + this.updateHeight(node); + this.updateHeight(child); + // 返回旋转后子树的根节点 + return child; + } + ``` + +=== "TypeScript" + + ```typescript title="avl_tree.ts" + + ``` + +=== "C" + + ```c title="avl_tree.c" + + ``` + +=== "C#" + + ```csharp title="avl_tree.cs" + /* 右旋操作 */ + TreeNode? rightRotate(TreeNode? node) + { + TreeNode? child = node.left; + TreeNode? grandChild = child?.right; + // 以 child 为原点,将 node 向右旋转 + child.right = node; + node.left = grandChild; + // 更新结点高度 + updateHeight(node); + updateHeight(child); + // 返回旋转后子树的根节点 + return child; + } + ``` + +=== "Swift" + + ```swift title="avl_tree.swift" + /* 右旋操作 */ + func rightRotate(node: TreeNode?) -> TreeNode? { + let child = node?.left + let grandChild = child?.right + // 以 child 为原点,将 node 向右旋转 + child?.right = node + node?.left = grandChild + // 更新结点高度 + updateHeight(node: node) + updateHeight(node: child) + // 返回旋转后子树的根节点 + return child + } + ``` + +=== "Zig" + + ```zig title="avl_tree.zig" + + ``` + +### Case 2 - 左旋 + +类似地,如果将取上述失衡二叉树的“镜像”,那么则需要「左旋」操作。 + +![left_rotate](avl_tree.assets/left_rotate.png) + +同理,若结点 `child` 本身有左子结点(记为 `grandChild` ),则需要在「左旋」中添加一步:将 `grandChild` 作为 `node` 的右子结点。 + +![left_rotate_with_grandchild](avl_tree.assets/left_rotate_with_grandchild.png) + +观察发现,**「左旋」和「右旋」操作是镜像对称的,两者对应解决的两种失衡情况也是对称的**。根据对称性,我们可以很方便地从「右旋」推导出「左旋」。具体地,只需将「右旋」代码中的把所有的 `left` 替换为 `right` 、所有的 `right` 替换为 `left` ,即可得到「左旋」代码。 + +=== "Java" + + ```java title="avl_tree.java" + /* 左旋操作 */ + private TreeNode leftRotate(TreeNode node) { + TreeNode child = node.right; + TreeNode grandChild = child.left; + // 以 child 为原点,将 node 向左旋转 + child.left = node; + node.right = grandChild; + // 更新结点高度 + updateHeight(node); + updateHeight(child); + // 返回旋转后子树的根节点 + return child; + } + ``` + +=== "C++" + + ```cpp title="avl_tree.cpp" + /* 左旋操作 */ + TreeNode* leftRotate(TreeNode* node) { + TreeNode* child = node->right; + TreeNode* grandChild = child->left; + // 以 child 为原点,将 node 向左旋转 + child->left = node; + node->right = grandChild; + // 更新结点高度 + updateHeight(node); + updateHeight(child); + // 返回旋转后子树的根节点 + return child; + } + ``` + +=== "Python" + + ```python title="avl_tree.py" + """ 左旋操作 """ + def __left_rotate(self, node: Optional[TreeNode]) -> TreeNode: + child = node.right + grand_child = child.left + # 以 child 为原点,将 node 向左旋转 + child.left = node + node.right = grand_child + # 更新结点高度 + self.__update_height(node) + self.__update_height(child) + # 返回旋转后子树的根节点 + return child + ``` + +=== "Go" + + ```go title="avl_tree.go" + /* 左旋操作 */ + func leftRotate(node *TreeNode) *TreeNode { + child := node.Right + grandChild := child.Left + // 以 child 为原点,将 node 向左旋转 + child.Left = node + node.Right = grandChild + // 更新结点高度 + updateHeight(node) + updateHeight(child) + // 返回旋转后子树的根节点 + return child + } + ``` + +=== "JavaScript" + + ```js title="avl_tree.js" + /* 左旋操作 */ + leftRotate(node) { + let child = node.right; + let grandChild = child.left; + // 以 child 为原点,将 node 向左旋转 + child.left = node; + node.right = grandChild; + // 更新结点高度 + this.updateHeight(node); + this.updateHeight(child); + // 返回旋转后子树的根节点 + return child; + } + ``` + +=== "TypeScript" + + ```typescript title="avl_tree.ts" + + ``` + +=== "C" + + ```c title="avl_tree.c" + + + + ``` + +=== "C#" + + ```csharp title="avl_tree.cs" + /* 左旋操作 */ + TreeNode? leftRotate(TreeNode? node) + { + TreeNode? child = node.right; + TreeNode? grandChild = child?.left; + // 以 child 为原点,将 node 向左旋转 + child.left = node; + node.right = grandChild; + // 更新结点高度 + updateHeight(node); + updateHeight(child); + // 返回旋转后子树的根节点 + return child; + } + ``` + +=== "Swift" + + ```swift title="avl_tree.swift" + /* 左旋操作 */ + func leftRotate(node: TreeNode?) -> TreeNode? { + let child = node?.right + let grandChild = child?.left + // 以 child 为原点,将 node 向左旋转 + child?.left = node + node?.right = grandChild + // 更新结点高度 + updateHeight(node: node) + updateHeight(node: child) + // 返回旋转后子树的根节点 + return child + } + + ``` + +=== "Zig" + + ```zig title="avl_tree.zig" + + ``` + +### Case 3 - 先左后右 + +对于下图的失衡结点 3 ,**单一使用左旋或右旋都无法使子树恢复平衡**,此时需要「先左旋后右旋」,即先对 `child` 执行「左旋」,再对 `node` 执行「右旋」。 + +![left_right_rotate](avl_tree.assets/left_right_rotate.png) + +### Case 4 - 先右后左 + +同理,取以上失衡二叉树的镜像,则需要「先右旋后左旋」,即先对 `child` 执行「右旋」,然后对 `node` 执行「左旋」。 + +![right_left_rotate](avl_tree.assets/right_left_rotate.png) + +### 旋转的选择 + +下图描述的四种失衡情况与上述 Cases 逐个对应,分别需采用 **右旋、左旋、先右后左、先左后右** 的旋转操作。 + +![rotation_cases](avl_tree.assets/rotation_cases.png) + +具体地,在代码中使用 **失衡结点的平衡因子、较高一侧子结点的平衡因子** 来确定失衡结点属于上图中的哪种情况。 + +
+ +| 失衡结点的平衡因子 | 子结点的平衡因子 | 应采用的旋转方法 | +| ------------------ | ---------------- | ---------------- | +| $>0$ (即左偏树) | $\geq 0$ | 右旋 | +| $>0$ (即左偏树) | $<0$ | 先左旋后右旋 | +| $<0$ (即右偏树) | $\leq 0$ | 左旋 | +| $<0$ (即右偏树) | $>0$ | 先右旋后左旋 | + +
+ +为方便使用,我们将旋转操作封装成一个函数。至此,**我们可以使用此函数来旋转各种失衡情况,使失衡结点重新恢复平衡**。 + +=== "Java" + + ```java title="avl_tree.java" + /* 执行旋转操作,使该子树重新恢复平衡 */ + TreeNode rotate(TreeNode node) { + // 获取结点 node 的平衡因子 + int balanceFactor = balanceFactor(node); + // 左偏树 + if (balanceFactor > 1) { + if (balanceFactor(node.left) >= 0) { + // 右旋 + return rightRotate(node); + } else { + // 先左旋后右旋 + node.left = leftRotate(node.left); + return rightRotate(node); + } + } + // 右偏树 + if (balanceFactor < -1) { + if (balanceFactor(node.right) <= 0) { + // 左旋 + return leftRotate(node); + } else { + // 先右旋后左旋 + node.right = rightRotate(node.right); + return leftRotate(node); + } + } + // 平衡树,无需旋转,直接返回 + return node; + } + ``` + +=== "C++" + + ```cpp title="avl_tree.cpp" + /* 执行旋转操作,使该子树重新恢复平衡 */ + TreeNode* rotate(TreeNode* node) { + // 获取结点 node 的平衡因子 + int _balanceFactor = balanceFactor(node); + // 左偏树 + if (_balanceFactor > 1) { + if (balanceFactor(node->left) >= 0) { + // 右旋 + return rightRotate(node); + } else { + // 先左旋后右旋 + node->left = leftRotate(node->left); + return rightRotate(node); + } + } + // 右偏树 + if (_balanceFactor < -1) { + if (balanceFactor(node->right) <= 0) { + // 左旋 + return leftRotate(node); + } else { + // 先右旋后左旋 + node->right = rightRotate(node->right); + return leftRotate(node); + } + } + // 平衡树,无需旋转,直接返回 + return node; + } + ``` + +=== "Python" + + ```python title="avl_tree.py" + """ 执行旋转操作,使该子树重新恢复平衡 """ + def __rotate(self, node: Optional[TreeNode]) -> TreeNode: + # 获取结点 node 的平衡因子 + balance_factor = self.balance_factor(node) + # 左偏树 + if balance_factor > 1: + if self.balance_factor(node.left) >= 0: + # 右旋 + return self.__right_rotate(node) + else: + # 先左旋后右旋 + node.left = self.__left_rotate(node.left) + return self.__right_rotate(node) + # 右偏树 + elif balance_factor < -1: + if self.balance_factor(node.right) <= 0: + # 左旋 + return self.__left_rotate(node) + else: + # 先右旋后左旋 + node.right = self.__right_rotate(node.right) + return self.__left_rotate(node) + # 平衡树,无需旋转,直接返回 + return node + ``` + +=== "Go" + + ```go title="avl_tree.go" + /* 执行旋转操作,使该子树重新恢复平衡 */ + func rotate(node *TreeNode) *TreeNode { + // 获取结点 node 的平衡因子 + // Go 推荐短变量,这里 bf 指代 balanceFactor + bf := balanceFactor(node) + // 左偏树 + if bf > 1 { + if balanceFactor(node.Left) >= 0 { + // 右旋 + return rightRotate(node) + } else { + // 先左旋后右旋 + node.Left = leftRotate(node.Left) + return rightRotate(node) + } + } + // 右偏树 + if bf < -1 { + if balanceFactor(node.Right) <= 0 { + // 左旋 + return leftRotate(node) + } else { + // 先右旋后左旋 + node.Right = rightRotate(node.Right) + return leftRotate(node) + } + } + // 平衡树,无需旋转,直接返回 + return node + } + ``` + +=== "JavaScript" + + ```js title="avl_tree.js" + /* 执行旋转操作,使该子树重新恢复平衡 */ + rotate(node) { + // 获取结点 node 的平衡因子 + let balanceFactor = this.balanceFactor(node); + // 左偏树 + if (balanceFactor > 1) { + if (this.balanceFactor(node.left) >= 0) { + // 右旋 + return this.rightRotate(node); + } else { + // 先左旋后右旋 + node.left = this.leftRotate(node.left); + return this.rightRotate(node); + } + } + // 右偏树 + if (balanceFactor < -1) { + if (this.balanceFactor(node.right) <= 0) { + // 左旋 + return this.leftRotate(node); + } else { + // 先右旋后左旋 + node.right = this.rightRotate(node.right); + return this.leftRotate(node); + } + } + // 平衡树,无需旋转,直接返回 + return node; + } + ``` + +=== "TypeScript" + + ```typescript title="avl_tree.ts" + + ``` + +=== "C" + + ```c title="avl_tree.c" + + ``` + +=== "C#" + + ```csharp title="avl_tree.cs" + /* 执行旋转操作,使该子树重新恢复平衡 */ + TreeNode? rotate(TreeNode? node) + { + // 获取结点 node 的平衡因子 + int balanceFactorInt = balanceFactor(node); + // 左偏树 + if (balanceFactorInt > 1) + { + if (balanceFactor(node.left) >= 0) + { + // 右旋 + return rightRotate(node); + } + else + { + // 先左旋后右旋 + node.left = leftRotate(node?.left); + return rightRotate(node); + } + } + // 右偏树 + if (balanceFactorInt < -1) + { + if (balanceFactor(node.right) <= 0) + { + // 左旋 + return leftRotate(node); + } + else + { + // 先右旋后左旋 + node.right = rightRotate(node?.right); + return leftRotate(node); + } + } + // 平衡树,无需旋转,直接返回 + return node; + } + ``` + +=== "Swift" + + ```swift title="avl_tree.swift" + /* 执行旋转操作,使该子树重新恢复平衡 */ + func rotate(node: TreeNode?) -> TreeNode? { + // 获取结点 node 的平衡因子 + let balanceFactor = balanceFactor(node: node) + // 左偏树 + if balanceFactor > 1 { + if self.balanceFactor(node: node?.left) >= 0 { + // 右旋 + return rightRotate(node: node) + } else { + // 先左旋后右旋 + node?.left = leftRotate(node: node?.left) + return rightRotate(node: node) + } + } + // 右偏树 + if balanceFactor < -1 { + if self.balanceFactor(node: node?.right) <= 0 { + // 左旋 + return leftRotate(node: node) + } else { + // 先右旋后左旋 + node?.right = rightRotate(node: node?.right) + return leftRotate(node: node) + } + } + // 平衡树,无需旋转,直接返回 + return node + } + ``` + +=== "Zig" + + ```zig title="avl_tree.zig" + + ``` + +## 7.4.3. AVL 树常用操作 + +### 插入结点 + +「AVL 树」的结点插入操作与「二叉搜索树」主体类似。不同的是,在插入结点后,从该结点到根结点的路径上会出现一系列「失衡结点」。所以,**我们需要从该结点开始,从底至顶地执行旋转操作,使所有失衡结点恢复平衡**。 + +=== "Java" + + ```java title="avl_tree.java" + /* 插入结点 */ + TreeNode insert(int val) { + root = insertHelper(root, val); + return root; + } + + /* 递归插入结点(辅助函数) */ + TreeNode insertHelper(TreeNode node, int val) { + if (node == null) return new TreeNode(val); + /* 1. 查找插入位置,并插入结点 */ + if (val < node.val) + node.left = insertHelper(node.left, val); + else if (val > node.val) + node.right = insertHelper(node.right, val); + else + return node; // 重复结点不插入,直接返回 + updateHeight(node); // 更新结点高度 + /* 2. 执行旋转操作,使该子树重新恢复平衡 */ + node = rotate(node); + // 返回子树的根节点 + return node; + } + ``` + +=== "C++" + + ```cpp title="avl_tree.cpp" + /* 插入结点 */ + TreeNode* insert(int val) { + root = insertHelper(root, val); + return root; + } + + /* 递归插入结点(辅助函数) */ + TreeNode* insertHelper(TreeNode* node, int val) { + if (node == nullptr) return new TreeNode(val); + /* 1. 查找插入位置,并插入结点 */ + if (val < node->val) + node->left = insertHelper(node->left, val); + else if (val > node->val) + node->right = insertHelper(node->right, val); + else + return node; // 重复结点不插入,直接返回 + updateHeight(node); // 更新结点高度 + /* 2. 执行旋转操作,使该子树重新恢复平衡 */ + node = rotate(node); + // 返回子树的根节点 + return node; + } + ``` + +=== "Python" + + ```python title="avl_tree.py" + """ 插入结点 """ + def insert(self, val) -> TreeNode: + self.root = self.__insert_helper(self.root, val) + return self.root + + """ 递归插入结点(辅助函数)""" + def __insert_helper(self, node: Optional[TreeNode], val: int) -> TreeNode: + if node is None: + return TreeNode(val) + # 1. 查找插入位置,并插入结点 + if val < node.val: + node.left = self.__insert_helper(node.left, val) + elif val > node.val: + node.right = self.__insert_helper(node.right, val) + else: + # 重复结点不插入,直接返回 + return node + # 更新结点高度 + self.__update_height(node) + # 2. 执行旋转操作,使该子树重新恢复平衡 + return self.__rotate(node) + ``` + +=== "Go" + + ```go title="avl_tree.go" + /* 插入结点 */ + func (t *avlTree) insert(val int) *TreeNode { + t.root = insertHelper(t.root, val) + return t.root + } + /* 递归插入结点(辅助函数) */ + func insertHelper(node *TreeNode, val int) *TreeNode { + if node == nil { + return NewTreeNode(val) + } + /* 1. 查找插入位置,并插入结点 */ + if val < node.Val { + node.Left = insertHelper(node.Left, val) + } else if val > node.Val { + node.Right = insertHelper(node.Right, val) + } else { + // 重复结点不插入,直接返回 + return node + } + // 更新结点高度 + updateHeight(node) + /* 2. 执行旋转操作,使该子树重新恢复平衡 */ + node = rotate(node) + // 返回子树的根节点 + return node + } + ``` + +=== "JavaScript" + + ```js title="avl_tree.js" + /* 插入结点 */ + insert(val) { + this.root = this.insertHelper(this.root, val); + return this.root; + } + + /* 递归插入结点(辅助函数) */ + insertHelper(node, val) { + if (node === null) return new TreeNode(val); + /* 1. 查找插入位置,并插入结点 */ + if (val < node.val) node.left = this.insertHelper(node.left, val); + else if (val > node.val) node.right = this.insertHelper(node.right, val); + else return node; // 重复结点不插入,直接返回 + this.updateHeight(node); // 更新结点高度 + /* 2. 执行旋转操作,使该子树重新恢复平衡 */ + node = this.rotate(node); + // 返回子树的根节点 + return node; + } + ``` + +=== "TypeScript" + + ```typescript title="avl_tree.ts" + + ``` + +=== "C" + + ```c title="avl_tree.c" + + + + ``` + +=== "C#" + + ```csharp title="avl_tree.cs" + /* 插入结点 */ + public TreeNode? insert(int val) + { + root = insertHelper(root, val); + return root; + } + + /* 递归插入结点(辅助函数) */ + private TreeNode? insertHelper(TreeNode? node, int val) + { + if (node == null) return new TreeNode(val); + /* 1. 查找插入位置,并插入结点 */ + if (val < node.val) + node.left = insertHelper(node.left, val); + else if (val > node.val) + node.right = insertHelper(node.right, val); + else + return node; // 重复结点不插入,直接返回 + updateHeight(node); // 更新结点高度 + /* 2. 执行旋转操作,使该子树重新恢复平衡 */ + node = rotate(node); + // 返回子树的根节点 + return node; + } + ``` + +=== "Swift" + + ```swift title="avl_tree.swift" + /* 插入结点 */ + @discardableResult + func insert(val: Int) -> TreeNode? { + root = insertHelper(node: root, val: val) + return root + } + + /* 递归插入结点(辅助函数) */ + func insertHelper(node: TreeNode?, val: Int) -> TreeNode? { + var node = node + if node == nil { + return TreeNode(x: val) + } + /* 1. 查找插入位置,并插入结点 */ + if val < node!.val { + node?.left = insertHelper(node: node?.left, val: val) + } else if val > node!.val { + node?.right = insertHelper(node: node?.right, val: val) + } else { + return node // 重复结点不插入,直接返回 + } + updateHeight(node: node) // 更新结点高度 + /* 2. 执行旋转操作,使该子树重新恢复平衡 */ + node = rotate(node: node) + // 返回子树的根节点 + return node + } + ``` + +=== "Zig" + + ```zig title="avl_tree.zig" + + ``` + +### 删除结点 + +「AVL 树」删除结点操作与「二叉搜索树」删除结点操作总体相同。类似地,**在删除结点后,也需要从底至顶地执行旋转操作,使所有失衡结点恢复平衡**。 + +=== "Java" + + ```java title="avl_tree.java" + /* 删除结点 */ + TreeNode remove(int val) { + root = removeHelper(root, val); + return root; + } + + /* 递归删除结点(辅助函数) */ + TreeNode removeHelper(TreeNode node, int val) { + if (node == null) return null; + /* 1. 查找结点,并删除之 */ + if (val < node.val) + node.left = removeHelper(node.left, val); + else if (val > node.val) + node.right = removeHelper(node.right, val); + else { + if (node.left == null || node.right == null) { + TreeNode child = node.left != null ? node.left : node.right; + // 子结点数量 = 0 ,直接删除 node 并返回 + if (child == null) + return null; + // 子结点数量 = 1 ,直接删除 node + else + node = child; + } else { + // 子结点数量 = 2 ,则将中序遍历的下个结点删除,并用该结点替换当前结点 + TreeNode temp = getInOrderNext(node.right); + node.right = removeHelper(node.right, temp.val); + node.val = temp.val; + } + } + updateHeight(node); // 更新结点高度 + /* 2. 执行旋转操作,使该子树重新恢复平衡 */ + node = rotate(node); + // 返回子树的根节点 + return node; + } + ``` + +=== "C++" + + ```cpp title="avl_tree.cpp" + /* 删除结点 */ + TreeNode* remove(int val) { + root = removeHelper(root, val); + return root; + } + + /* 递归删除结点(辅助函数) */ + TreeNode* removeHelper(TreeNode* node, int val) { + if (node == nullptr) return nullptr; + /* 1. 查找结点,并删除之 */ + if (val < node->val) + node->left = removeHelper(node->left, val); + else if (val > node->val) + node->right = removeHelper(node->right, val); + else { + if (node->left == nullptr || node->right == nullptr) { + TreeNode* child = node->left != nullptr ? node->left : node->right; + // 子结点数量 = 0 ,直接删除 node 并返回 + if (child == nullptr) { + delete node; + return nullptr; + } + // 子结点数量 = 1 ,直接删除 node + else { + delete node; + node = child; + } + } else { + // 子结点数量 = 2 ,则将中序遍历的下个结点删除,并用该结点替换当前结点 + TreeNode* temp = getInOrderNext(node->right); + node->right = removeHelper(node->right, temp->val); + node->val = temp->val; + } + } + updateHeight(node); // 更新结点高度 + /* 2. 执行旋转操作,使该子树重新恢复平衡 */ + node = rotate(node); + // 返回子树的根节点 + return node; + } + ``` + +=== "Python" + + ```python title="avl_tree.py" + """ 删除结点 """ + def remove(self, val: int): + root = self.__remove_helper(self.root, val) + return root + + """ 递归删除结点(辅助函数) """ + def __remove_helper(self, node: Optional[TreeNode], val: int) -> Optional[TreeNode]: + if node is None: + return None + # 1. 查找结点,并删除之 + if val < node.val: + node.left = self.__remove_helper(node.left, val) + elif val > node.val: + node.right = self.__remove_helper(node.right, val) + else: + if node.left is None or node.right is None: + child = node.left or node.right + # 子结点数量 = 0 ,直接删除 node 并返回 + if child is None: + return None + # 子结点数量 = 1 ,直接删除 node + else: + node = child + else: # 子结点数量 = 2 ,则将中序遍历的下个结点删除,并用该结点替换当前结点 + temp = self.__get_inorder_next(node.right) + node.right = self.__remove_helper(node.right, temp.val) + node.val = temp.val + # 更新结点高度 + self.__update_height(node) + # 2. 执行旋转操作,使该子树重新恢复平衡 + return self.__rotate(node) + ``` + +=== "Go" + + ```go title="avl_tree.go" + /* 删除结点 */ + func (t *avlTree) remove(val int) *TreeNode { + root := removeHelper(t.root, val) + return root + } + + /* 递归删除结点(辅助函数) */ + func removeHelper(node *TreeNode, val int) *TreeNode { + if node == nil { + return nil + } + /* 1. 查找结点,并删除之 */ + if val < node.Val { + node.Left = removeHelper(node.Left, val) + } else if val > node.Val { + node.Right = removeHelper(node.Right, val) + } else { + if node.Left == nil || node.Right == nil { + child := node.Left + if node.Right != nil { + child = node.Right + } + // 子结点数量 = 0 ,直接删除 node 并返回 + if child == nil { + return nil + } else { + // 子结点数量 = 1 ,直接删除 node + node = child + } + } else { + // 子结点数量 = 2 ,则将中序遍历的下个结点删除,并用该结点替换当前结点 + temp := getInOrderNext(node.Right) + node.Right = removeHelper(node.Right, temp.Val) + node.Val = temp.Val + } + } + // 更新结点高度 + updateHeight(node) + /* 2. 执行旋转操作,使该子树重新恢复平衡 */ + node = rotate(node) + // 返回子树的根节点 + return node + } + ``` + +=== "JavaScript" + + ```js title="avl_tree.js" + /* 删除结点 */ + remove(val) { + this.root = this.removeHelper(this.root, val); + return this.root; + } + + /* 递归删除结点(辅助函数) */ + removeHelper(node, val) { + if (node === null) return null; + /* 1. 查找结点,并删除之 */ + if (val < node.val) node.left = this.removeHelper(node.left, val); + else if (val > node.val) node.right = this.removeHelper(node.right, val); + else { + if (node.left === null || node.right === null) { + let child = node.left !== null ? node.left : node.right; + // 子结点数量 = 0 ,直接删除 node 并返回 + if (child === null) return null; + // 子结点数量 = 1 ,直接删除 node + else node = child; + } else { + // 子结点数量 = 2 ,则将中序遍历的下个结点删除,并用该结点替换当前结点 + let temp = this.getInOrderNext(node.right); + node.right = this.removeHelper(node.right, temp.val); + node.val = temp.val; + } + } + this.updateHeight(node); // 更新结点高度 + /* 2. 执行旋转操作,使该子树重新恢复平衡 */ + node = this.rotate(node); + // 返回子树的根节点 + return node; + } + ``` + +=== "TypeScript" + + ```typescript title="avl_tree.ts" + + ``` + +=== "C" + + ```c title="avl_tree.c" + + ``` + +=== "C#" + + ```csharp title="avl_tree.cs" + /* 删除结点 */ + public TreeNode? remove(int val) + { + root = removeHelper(root, val); + return root; + } + + /* 递归删除结点(辅助函数) */ + private TreeNode? removeHelper(TreeNode? node, int val) + { + if (node == null) return null; + /* 1. 查找结点,并删除之 */ + if (val < node.val) + node.left = removeHelper(node.left, val); + else if (val > node.val) + node.right = removeHelper(node.right, val); + else + { + if (node.left == null || node.right == null) + { + TreeNode? child = node.left != null ? node.left : node.right; + // 子结点数量 = 0 ,直接删除 node 并返回 + if (child == null) + return null; + // 子结点数量 = 1 ,直接删除 node + else + node = child; + } + else + { + // 子结点数量 = 2 ,则将中序遍历的下个结点删除,并用该结点替换当前结点 + TreeNode? temp = getInOrderNext(node.right); + node.right = removeHelper(node.right, temp.val); + node.val = temp.val; + } + } + updateHeight(node); // 更新结点高度 + /* 2. 执行旋转操作,使该子树重新恢复平衡 */ + node = rotate(node); + // 返回子树的根节点 + return node; + } + ``` + +=== "Swift" + + ```swift title="avl_tree.swift" + /* 删除结点 */ + @discardableResult + func remove(val: Int) -> TreeNode? { + root = removeHelper(node: root, val: val) + return root + } + + /* 递归删除结点(辅助函数) */ + func removeHelper(node: TreeNode?, val: Int) -> TreeNode? { + var node = node + if node == nil { + return nil + } + /* 1. 查找结点,并删除之 */ + if val < node!.val { + node?.left = removeHelper(node: node?.left, val: val) + } else if val > node!.val { + node?.right = removeHelper(node: node?.right, val: val) + } else { + if node?.left == nil || node?.right == nil { + let child = node?.left != nil ? node?.left : node?.right + // 子结点数量 = 0 ,直接删除 node 并返回 + if child == nil { + return nil + } + // 子结点数量 = 1 ,直接删除 node + else { + node = child + } + } else { + // 子结点数量 = 2 ,则将中序遍历的下个结点删除,并用该结点替换当前结点 + let temp = getInOrderNext(node: node?.right) + node?.right = removeHelper(node: node?.right, val: temp!.val) + node?.val = temp!.val + } + } + updateHeight(node: node) // 更新结点高度 + /* 2. 执行旋转操作,使该子树重新恢复平衡 */ + node = rotate(node: node) + // 返回子树的根节点 + return node + } + ``` + +=== "Zig" + + ```zig title="avl_tree.zig" + + ``` + +### 查找结点 + +「AVL 树」的结点查找操作与「二叉搜索树」一致,在此不再赘述。 + +## 7.4.4. AVL 树典型应用 + +- 组织存储大型数据,适用于高频查找、低频增删场景; +- 用于建立数据库中的索引系统; + +!!! question "为什么红黑树比 AVL 树更受欢迎?" + + 红黑树的平衡条件相对宽松,因此在红黑树中插入与删除结点所需的旋转操作相对更少,结点增删操作相比 AVL 树的效率更高。 diff --git a/build/chapter_tree/binary_search_tree.md b/build/chapter_tree/binary_search_tree.md new file mode 100755 index 000000000..df69cecc2 --- /dev/null +++ b/build/chapter_tree/binary_search_tree.md @@ -0,0 +1,1083 @@ +--- +comments: true +--- + +# 7.3. 二叉搜索树 + +「二叉搜索树 Binary Search Tree」满足以下条件: + +1. 对于根结点,左子树中所有结点的值 $<$ 根结点的值 $<$ 右子树中所有结点的值; +2. 任意结点的左子树和右子树也是二叉搜索树,即也满足条件 `1.` ; + +![binary_search_tree](binary_search_tree.assets/binary_search_tree.png) + +## 7.3.1. 二叉搜索树的操作 + +### 查找结点 + +给定目标结点值 `num` ,可以根据二叉搜索树的性质来查找。我们声明一个结点 `cur` ,从二叉树的根结点 `root` 出发,循环比较结点值 `cur.val` 和 `num` 之间的大小关系 + +- 若 `cur.val < num` ,说明目标结点在 `cur` 的右子树中,因此执行 `cur = cur.right` ; +- 若 `cur.val > num` ,说明目标结点在 `cur` 的左子树中,因此执行 `cur = cur.left` ; +- 若 `cur.val = num` ,说明找到目标结点,跳出循环并返回该结点即可; + +=== "Step 1" + ![bst_search_1](binary_search_tree.assets/bst_search_1.png) + +=== "Step 2" + ![bst_search_2](binary_search_tree.assets/bst_search_2.png) + +=== "Step 3" + ![bst_search_3](binary_search_tree.assets/bst_search_3.png) + +=== "Step 4" + ![bst_search_4](binary_search_tree.assets/bst_search_4.png) + +二叉搜索树的查找操作和二分查找算法如出一辙,也是在每轮排除一半情况。循环次数最多为二叉树的高度,当二叉树平衡时,使用 $O(\log n)$ 时间。 + +=== "Java" + + ```java title="binary_search_tree.java" + /* 查找结点 */ + TreeNode search(int num) { + TreeNode cur = root; + // 循环查找,越过叶结点后跳出 + while (cur != null) { + // 目标结点在 cur 的右子树中 + if (cur.val < num) cur = cur.right; + // 目标结点在 cur 的左子树中 + else if (cur.val > num) cur = cur.left; + // 找到目标结点,跳出循环 + else break; + } + // 返回目标结点 + return cur; + } + ``` + +=== "C++" + + ```cpp title="binary_search_tree.cpp" + /* 查找结点 */ + TreeNode* search(int num) { + TreeNode* cur = root; + // 循环查找,越过叶结点后跳出 + while (cur != nullptr) { + // 目标结点在 cur 的右子树中 + if (cur->val < num) cur = cur->right; + // 目标结点在 cur 的左子树中 + else if (cur->val > num) cur = cur->left; + // 找到目标结点,跳出循环 + else break; + } + // 返回目标结点 + return cur; + } + ``` + +=== "Python" + + ```python title="binary_search_tree.py" + """ 查找结点 """ + def search(self, num: int) -> Optional[TreeNode]: + cur = self.root + # 循环查找,越过叶结点后跳出 + while cur is not None: + # 目标结点在 cur 的右子树中 + if cur.val < num: + cur = cur.right + # 目标结点在 cur 的左子树中 + elif cur.val > num: + cur = cur.left + # 找到目标结点,跳出循环 + else: + break + return cur + ``` + +=== "Go" + + ```go title="binary_search_tree.go" + /* 查找结点 */ + func (bst *binarySearchTree) search(num int) *TreeNode { + node := bst.root + // 循环查找,越过叶结点后跳出 + for node != nil { + if node.Val < num { + // 目标结点在 cur 的右子树中 + node = node.Right + } else if node.Val > num { + // 目标结点在 cur 的左子树中 + node = node.Left + } else { + // 找到目标结点,跳出循环 + break + } + } + // 返回目标结点 + return node + } + ``` + +=== "JavaScript" + + ```js title="binary_search_tree.js" + /* 查找结点 */ + function search(num) { + let cur = root; + // 循环查找,越过叶结点后跳出 + while (cur !== null) { + // 目标结点在 cur 的右子树中 + if (cur.val < num) cur = cur.right; + // 目标结点在 cur 的左子树中 + else if (cur.val > num) cur = cur.left; + // 找到目标结点,跳出循环 + else break; + } + // 返回目标结点 + return cur; + } + ``` + +=== "TypeScript" + + ```typescript title="binary_search_tree.ts" + /* 查找结点 */ + function search(num: number): TreeNode | null { + let cur = root; + // 循环查找,越过叶结点后跳出 + while (cur !== null) { + if (cur.val < num) { + cur = cur.right; // 目标结点在 cur 的右子树中 + } else if (cur.val > num) { + cur = cur.left; // 目标结点在 cur 的左子树中 + } else { + break; // 找到目标结点,跳出循环 + } + } + // 返回目标结点 + return cur; + } + ``` + +=== "C" + + ```c title="binary_search_tree.c" + + ``` + +=== "C#" + + ```csharp title="binary_search_tree.cs" + /* 查找结点 */ + TreeNode? search(int num) + { + TreeNode? cur = root; + // 循环查找,越过叶结点后跳出 + while (cur != null) + { + // 目标结点在 cur 的右子树中 + if (cur.val < num) cur = cur.right; + // 目标结点在 cur 的左子树中 + else if (cur.val > num) cur = cur.left; + // 找到目标结点,跳出循环 + else break; + } + // 返回目标结点 + return cur; + } + ``` + +=== "Swift" + + ```swift title="binary_search_tree.swift" + /* 查找结点 */ + func search(num: Int) -> TreeNode? { + var cur = root + // 循环查找,越过叶结点后跳出 + while cur != nil { + // 目标结点在 cur 的右子树中 + if cur!.val < num { + cur = cur?.right + } + // 目标结点在 cur 的左子树中 + else if cur!.val > num { + cur = cur?.left + } + // 找到目标结点,跳出循环 + else { + break + } + } + // 返回目标结点 + return cur + } + ``` + +=== "Zig" + + ```zig title="binary_search_tree.zig" + + ``` + +### 插入结点 + +给定一个待插入元素 `num` ,为了保持二叉搜索树“左子树 < 根结点 < 右子树”的性质,插入操作分为两步: + +1. **查找插入位置**:与查找操作类似,我们从根结点出发,根据当前结点值和 `num` 的大小关系循环向下搜索,直到越过叶结点(遍历到 $\text{null}$ )时跳出循环; +2. **在该位置插入结点**:初始化结点 `num` ,将该结点放到 $\text{null}$ 的位置 ; + +二叉搜索树不允许存在重复结点,否则将会违背其定义。因此若待插入结点在树中已经存在,则不执行插入,直接返回即可。 + +![bst_insert](binary_search_tree.assets/bst_insert.png) + +=== "Java" + + ```java title="binary_search_tree.java" + /* 插入结点 */ + TreeNode insert(int num) { + // 若树为空,直接提前返回 + if (root == null) return null; + TreeNode cur = root, pre = null; + // 循环查找,越过叶结点后跳出 + while (cur != null) { + // 找到重复结点,直接返回 + if (cur.val == num) return null; + pre = cur; + // 插入位置在 cur 的右子树中 + if (cur.val < num) cur = cur.right; + // 插入位置在 cur 的左子树中 + else cur = cur.left; + } + // 插入结点 val + TreeNode node = new TreeNode(num); + if (pre.val < num) pre.right = node; + else pre.left = node; + return node; + } + ``` + +=== "C++" + + ```cpp title="binary_search_tree.cpp" + /* 插入结点 */ + TreeNode* insert(int num) { + // 若树为空,直接提前返回 + if (root == nullptr) return nullptr; + TreeNode *cur = root, *pre = nullptr; + // 循环查找,越过叶结点后跳出 + while (cur != nullptr) { + // 找到重复结点,直接返回 + if (cur->val == num) return nullptr; + pre = cur; + // 插入位置在 cur 的右子树中 + if (cur->val < num) cur = cur->right; + // 插入位置在 cur 的左子树中 + else cur = cur->left; + } + // 插入结点 val + TreeNode* node = new TreeNode(num); + if (pre->val < num) pre->right = node; + else pre->left = node; + return node; + } + ``` + +=== "Python" + + ```python title="binary_search_tree.py" + """ 插入结点 """ + def insert(self, num: int) -> Optional[TreeNode]: + root = self.root + # 若树为空,直接提前返回 + if root is None: + return None + + # 循环查找,越过叶结点后跳出 + cur, pre = root, None + while cur is not None: + # 找到重复结点,直接返回 + if cur.val == num: + return None + pre = cur + # 插入位置在 cur 的右子树中 + if cur.val < num: + cur = cur.right + # 插入位置在 cur 的左子树中 + else: + cur = cur.left + + # 插入结点 val + node = TreeNode(num) + if pre.val < num: + pre.right = node + else: + pre.left = node + return node + ``` + +=== "Go" + + ```go title="binary_search_tree.go" + /* 插入结点 */ + func (bst *binarySearchTree) insert(num int) *TreeNode { + cur := bst.root + // 若树为空,直接提前返回 + if cur == nil { + return nil + } + // 待插入结点之前的结点位置 + var pre *TreeNode = nil + // 循环查找,越过叶结点后跳出 + for cur != nil { + if cur.Val == num { + return nil + } + pre = cur + if cur.Val < num { + cur = cur.Right + } else { + cur = cur.Left + } + } + // 插入结点 + node := NewTreeNode(num) + if pre.Val < num { + pre.Right = node + } else { + pre.Left = node + } + return cur + } + ``` + +=== "JavaScript" + + ```js title="binary_search_tree.js" + /* 插入结点 */ + function insert(num) { + // 若树为空,直接提前返回 + if (root === null) return null; + let cur = root, pre = null; + // 循环查找,越过叶结点后跳出 + while (cur !== null) { + // 找到重复结点,直接返回 + if (cur.val === num) return null; + pre = cur; + // 插入位置在 cur 的右子树中 + if (cur.val < num) cur = cur.right; + // 插入位置在 cur 的左子树中 + else cur = cur.left; + } + // 插入结点 val + let node = new Tree.TreeNode(num); + if (pre.val < num) pre.right = node; + else pre.left = node; + return node; + } + ``` + +=== "TypeScript" + + ```typescript title="binary_search_tree.ts" + /* 插入结点 */ + function insert(num: number): TreeNode | null { + // 若树为空,直接提前返回 + if (root === null) { + return null; + } + let cur = root, + pre: TreeNode | null = null; + // 循环查找,越过叶结点后跳出 + while (cur !== null) { + if (cur.val === num) { + return null; // 找到重复结点,直接返回 + } + pre = cur; + if (cur.val < num) { + cur = cur.right as TreeNode; // 插入位置在 cur 的右子树中 + } else { + cur = cur.left as TreeNode; // 插入位置在 cur 的左子树中 + } + } + // 插入结点 val + let node = new TreeNode(num); + if (pre!.val < num) { + pre!.right = node; + } else { + pre!.left = node; + } + return node; + } + ``` + +=== "C" + + ```c title="binary_search_tree.c" + + ``` + +=== "C#" + + ```csharp title="binary_search_tree.cs" + /* 插入结点 */ + TreeNode? insert(int num) + { + // 若树为空,直接提前返回 + if (root == null) return null; + TreeNode? cur = root, pre = null; + // 循环查找,越过叶结点后跳出 + while (cur != null) + { + // 找到重复结点,直接返回 + if (cur.val == num) return null; + pre = cur; + // 插入位置在 cur 的右子树中 + if (cur.val < num) cur = cur.right; + // 插入位置在 cur 的左子树中 + else cur = cur.left; + } + + // 插入结点 val + TreeNode node = new TreeNode(num); + if (pre != null) + { + if (pre.val < num) pre.right = node; + else pre.left = node; + } + return node; + } + ``` + +=== "Swift" + + ```swift title="binary_search_tree.swift" + /* 插入结点 */ + func insert(num: Int) -> TreeNode? { + // 若树为空,直接提前返回 + if root == nil { + return nil + } + var cur = root + var pre: TreeNode? + // 循环查找,越过叶结点后跳出 + while cur != nil { + // 找到重复结点,直接返回 + if cur!.val == num { + return nil + } + pre = cur + // 插入位置在 cur 的右子树中 + if cur!.val < num { + cur = cur?.right + } + // 插入位置在 cur 的左子树中 + else { + cur = cur?.left + } + } + // 插入结点 val + let node = TreeNode(x: num) + if pre!.val < num { + pre?.right = node + } else { + pre?.left = node + } + return node + } + ``` + +=== "Zig" + + ```zig title="binary_search_tree.zig" + + ``` + +为了插入结点,需要借助 **辅助结点 `pre`** 保存上一轮循环的结点,这样在遍历到 $\text{null}$ 时,我们也可以获取到其父结点,从而完成结点插入操作。 + +与查找结点相同,插入结点使用 $O(\log n)$ 时间。 + +### 删除结点 + +与插入结点一样,我们需要在删除操作后维持二叉搜索树的“左子树 < 根结点 < 右子树”的性质。首先,我们需要在二叉树中执行查找操作,获取待删除结点。接下来,根据待删除结点的子结点数量,删除操作需要分为三种情况: + +**当待删除结点的子结点数量 $= 0$ 时**,表明待删除结点是叶结点,直接删除即可。 + +![bst_remove_case1](binary_search_tree.assets/bst_remove_case1.png) + +**当待删除结点的子结点数量 $= 1$ 时**,将待删除结点替换为其子结点即可。 + +![bst_remove_case2](binary_search_tree.assets/bst_remove_case2.png) + +**当待删除结点的子结点数量 $= 2$ 时**,删除操作分为三步: + +1. 找到待删除结点在 **中序遍历序列** 中的下一个结点,记为 `nex` ; +2. 在树中递归删除结点 `nex` ; +3. 使用 `nex` 替换待删除结点; + +=== "Step 1" + ![bst_remove_case3_1](binary_search_tree.assets/bst_remove_case3_1.png) + +=== "Step 2" + ![bst_remove_case3_2](binary_search_tree.assets/bst_remove_case3_2.png) + +=== "Step 3" + ![bst_remove_case3_3](binary_search_tree.assets/bst_remove_case3_3.png) + +=== "Step 4" + ![bst_remove_case3_4](binary_search_tree.assets/bst_remove_case3_4.png) + +删除结点操作也使用 $O(\log n)$ 时间,其中查找待删除结点 $O(\log n)$ ,获取中序遍历后继结点 $O(\log n)$ 。 + +=== "Java" + + ```java title="binary_search_tree.java" + /* 删除结点 */ + TreeNode remove(int num) { + // 若树为空,直接提前返回 + if (root == null) return null; + TreeNode cur = root, pre = null; + // 循环查找,越过叶结点后跳出 + while (cur != null) { + // 找到待删除结点,跳出循环 + if (cur.val == num) break; + pre = cur; + // 待删除结点在 cur 的右子树中 + if (cur.val < num) cur = cur.right; + // 待删除结点在 cur 的左子树中 + else cur = cur.left; + } + // 若无待删除结点,则直接返回 + if (cur == null) return null; + // 子结点数量 = 0 or 1 + if (cur.left == null || cur.right == null) { + // 当子结点数量 = 0 / 1 时, child = null / 该子结点 + TreeNode child = cur.left != null ? cur.left : cur.right; + // 删除结点 cur + if (pre.left == cur) pre.left = child; + else pre.right = child; + // 释放内存 + delete cur; + } + // 子结点数量 = 2 + else { + // 获取中序遍历中 cur 的下一个结点 + TreeNode nex = getInOrderNext(cur.right); + int tmp = nex.val; + // 递归删除结点 nex + remove(nex.val); + // 将 nex 的值复制给 cur + cur.val = tmp; + } + return cur; + } + + /* 获取中序遍历中的下一个结点(仅适用于 root 有左子结点的情况) */ + public TreeNode getInOrderNext(TreeNode root) { + if (root == null) return root; + // 循环访问左子结点,直到叶结点时为最小结点,跳出 + while (root.left != null) { + root = root.left; + } + return root; + } + ``` + +=== "C++" + + ```cpp title="binary_search_tree.cpp" + /* 删除结点 */ + TreeNode* remove(int num) { + // 若树为空,直接提前返回 + if (root == nullptr) return nullptr; + TreeNode *cur = root, *pre = nullptr; + // 循环查找,越过叶结点后跳出 + while (cur != nullptr) { + // 找到待删除结点,跳出循环 + if (cur->val == num) break; + pre = cur; + // 待删除结点在 cur 的右子树中 + if (cur->val < num) cur = cur->right; + // 待删除结点在 cur 的左子树中 + else cur = cur->left; + } + // 若无待删除结点,则直接返回 + if (cur == nullptr) return nullptr; + // 子结点数量 = 0 or 1 + if (cur->left == nullptr || cur->right == nullptr) { + // 当子结点数量 = 0 / 1 时, child = nullptr / 该子结点 + TreeNode* child = cur->left != nullptr ? cur->left : cur->right; + // 删除结点 cur + if (pre->left == cur) pre->left = child; + else pre->right = child; + } + // 子结点数量 = 2 + else { + // 获取中序遍历中 cur 的下一个结点 + TreeNode* nex = getInOrderNext(cur->right); + int tmp = nex->val; + // 递归删除结点 nex + remove(nex->val); + // 将 nex 的值复制给 cur + cur->val = tmp; + } + return cur; + } + + /* 获取中序遍历中的下一个结点(仅适用于 root 有左子结点的情况) */ + TreeNode* getInOrderNext(TreeNode* root) { + if (root == nullptr) return root; + // 循环访问左子结点,直到叶结点时为最小结点,跳出 + while (root->left != nullptr) { + root = root->left; + } + return root; + } + ``` + +=== "Python" + + ```python title="binary_search_tree.py" + """ 删除结点 """ + def remove(self, num: int) -> Optional[TreeNode]: + root = self.root + # 若树为空,直接提前返回 + if root is None: + return None + + # 循环查找,越过叶结点后跳出 + cur, pre = root, None + while cur is not None: + # 找到待删除结点,跳出循环 + if cur.val == num: + break + pre = cur + if cur.val < num: # 待删除结点在 cur 的右子树中 + cur = cur.right + else: # 待删除结点在 cur 的左子树中 + cur = cur.left + # 若无待删除结点,则直接返回 + if cur is None: + return None + + # 子结点数量 = 0 or 1 + if cur.left is None or cur.right is None: + # 当子结点数量 = 0 / 1 时, child = null / 该子结点 + child = cur.left or cur.right + # 删除结点 cur + if pre.left == cur: + pre.left = child + else: + pre.right = child + # 子结点数量 = 2 + else: + # 获取中序遍历中 cur 的下一个结点 + nex = self.get_inorder_next(cur.right) + tmp = nex.val + # 递归删除结点 nex + self.remove(nex.val) + # 将 nex 的值复制给 cur + cur.val = tmp + return cur + + """ 获取中序遍历中的下一个结点(仅适用于 root 有左子结点的情况) """ + def get_inorder_next(self, root: Optional[TreeNode]) -> Optional[TreeNode]: + if root is None: + return root + # 循环访问左子结点,直到叶结点时为最小结点,跳出 + while root.left is not None: + root = root.left + return root + ``` + +=== "Go" + + ```go title="binary_search_tree.go" + /* 删除结点 */ + func (bst *binarySearchTree) remove(num int) *TreeNode { + cur := bst.root + // 若树为空,直接提前返回 + if cur == nil { + return nil + } + // 待删除结点之前的结点位置 + var pre *TreeNode = nil + // 循环查找,越过叶结点后跳出 + for cur != nil { + if cur.Val == num { + break + } + pre = cur + if cur.Val < num { + // 待删除结点在右子树中 + cur = cur.Right + } else { + // 待删除结点在左子树中 + cur = cur.Left + } + } + // 若无待删除结点,则直接返回 + if cur == nil { + return nil + } + // 子结点数为 0 或 1 + if cur.Left == nil || cur.Right == nil { + var child *TreeNode = nil + // 取出待删除结点的子结点 + if cur.Left != nil { + child = cur.Left + } else { + child = cur.Right + } + // 将子结点替换为待删除结点 + if pre.Left == cur { + pre.Left = child + } else { + pre.Right = child + } + // 子结点数为 2 + } else { + // 获取中序遍历中待删除结点 cur 的下一个结点 + next := bst.getInOrderNext(cur) + temp := next.Val + // 递归删除结点 next + bst.remove(next.Val) + // 将 next 的值复制给 cur + cur.Val = temp + } + return cur + } + + /* 获取中序遍历的下一个结点(仅适用于 root 有左子结点的情况) */ + func (bst *binarySearchTree) getInOrderNext(node *TreeNode) *TreeNode { + if node == nil { + return node + } + // 循环访问左子结点,直到叶结点时为最小结点,跳出 + for node.Left != nil { + node = node.Left + } + return node + } + ``` + +=== "JavaScript" + + ```js title="binary_search_tree.js" + /* 删除结点 */ + function remove(num) { + // 若树为空,直接提前返回 + if (root === null) return null; + let cur = root, pre = null; + // 循环查找,越过叶结点后跳出 + while (cur !== null) { + // 找到待删除结点,跳出循环 + if (cur.val === num) break; + pre = cur; + // 待删除结点在 cur 的右子树中 + if (cur.val < num) cur = cur.right; + // 待删除结点在 cur 的左子树中 + else cur = cur.left; + } + // 若无待删除结点,则直接返回 + if (cur === null) return null; + // 子结点数量 = 0 or 1 + if (cur.left === null || cur.right === null) { + // 当子结点数量 = 0 / 1 时, child = null / 该子结点 + let child = cur.left !== null ? cur.left : cur.right; + // 删除结点 cur + if (pre.left === cur) pre.left = child; + else pre.right = child; + } + // 子结点数量 = 2 + else { + // 获取中序遍历中 cur 的下一个结点 + let nex = getInOrderNext(cur.right); + let tmp = nex.val; + // 递归删除结点 nex + remove(nex.val); + // 将 nex 的值复制给 cur + cur.val = tmp; + } + return cur; + } + + /* 获取中序遍历中的下一个结点(仅适用于 root 有左子结点的情况) */ + function getInOrderNext(root) { + if (root === null) return root; + // 循环访问左子结点,直到叶结点时为最小结点,跳出 + while (root.left !== null) { + root = root.left; + } + return root; + } + ``` + +=== "TypeScript" + + ```typescript title="binary_search_tree.ts" + /* 删除结点 */ + function remove(num: number): TreeNode | null { + // 若树为空,直接提前返回 + if (root === null) { + return null; + } + let cur = root, + pre: TreeNode | null = null; + // 循环查找,越过叶结点后跳出 + while (cur !== null) { + // 找到待删除结点,跳出循环 + if (cur.val === num) { + break; + } + pre = cur; + if (cur.val < num) { + cur = cur.right as TreeNode; // 待删除结点在 cur 的右子树中 + } else { + cur = cur.left as TreeNode; // 待删除结点在 cur 的左子树中 + } + } + // 若无待删除结点,则直接返回 + if (cur === null) { + return null; + } + // 子结点数量 = 0 or 1 + if (cur.left === null || cur.right === null) { + // 当子结点数量 = 0 / 1 时, child = null / 该子结点 + let child = cur.left !== null ? cur.left : cur.right; + // 删除结点 cur + if (pre!.left === cur) { + pre!.left = child; + } else { + pre!.right = child; + } + } + // 子结点数量 = 2 + else { + // 获取中序遍历中 cur 的下一个结点 + let next = getInOrderNext(cur.right); + let tmp = next!.val; + // 递归删除结点 nex + remove(next!.val); + // 将 nex 的值复制给 cur + cur.val = tmp; + } + return cur; + } + + /* 获取中序遍历中的下一个结点(仅适用于 root 有左子结点的情况) */ + function getInOrderNext(root: TreeNode | null): TreeNode | null { + if (root === null) { + return null; + } + // 循环访问左子结点,直到叶结点时为最小结点,跳出 + while (root.left !== null) { + root = root.left; + } + return root; + } + ``` + +=== "C" + + ```c title="binary_search_tree.c" + + ``` + +=== "C#" + + ```csharp title="binary_search_tree.cs" + /* 删除结点 */ + TreeNode? remove(int num) + { + // 若树为空,直接提前返回 + if (root == null) return null; + TreeNode? cur = root, pre = null; + // 循环查找,越过叶结点后跳出 + while (cur != null) + { + // 找到待删除结点,跳出循环 + if (cur.val == num) break; + pre = cur; + // 待删除结点在 cur 的右子树中 + if (cur.val < num) cur = cur.right; + // 待删除结点在 cur 的左子树中 + else cur = cur.left; + } + // 若无待删除结点,则直接返回 + if (cur == null || pre == null) return null; + // 子结点数量 = 0 or 1 + if (cur.left == null || cur.right == null) + { + // 当子结点数量 = 0 / 1 时, child = null / 该子结点 + TreeNode? child = cur.left != null ? cur.left : cur.right; + // 删除结点 cur + if (pre.left == cur) + { + pre.left = child; + } + else + { + pre.right = child; + } + } + // 子结点数量 = 2 + else + { + // 获取中序遍历中 cur 的下一个结点 + TreeNode? nex = getInOrderNext(cur.right); + if (nex != null) + { + int tmp = nex.val; + // 递归删除结点 nex + remove(nex.val); + // 将 nex 的值复制给 cur + cur.val = tmp; + } + } + return cur; + } + + /* 获取中序遍历中的下一个结点(仅适用于 root 有左子结点的情况) */ + private TreeNode? getInOrderNext(TreeNode? root) + { + if (root == null) return root; + // 循环访问左子结点,直到叶结点时为最小结点,跳出 + while (root.left != null) + { + root = root.left; + } + return root; + } + ``` + +=== "Swift" + + ```swift title="binary_search_tree.swift" + /* 删除结点 */ + @discardableResult + func remove(num: Int) -> TreeNode? { + // 若树为空,直接提前返回 + if root == nil { + return nil + } + var cur = root + var pre: TreeNode? + // 循环查找,越过叶结点后跳出 + while cur != nil { + // 找到待删除结点,跳出循环 + if cur!.val == num { + break + } + pre = cur + // 待删除结点在 cur 的右子树中 + if cur!.val < num { + cur = cur?.right + } + // 待删除结点在 cur 的左子树中 + else { + cur = cur?.left + } + } + // 若无待删除结点,则直接返回 + if cur == nil { + return nil + } + // 子结点数量 = 0 or 1 + if cur?.left == nil || cur?.right == nil { + // 当子结点数量 = 0 / 1 时, child = null / 该子结点 + let child = cur?.left != nil ? cur?.left : cur?.right + // 删除结点 cur + if pre?.left === cur { + pre?.left = child + } else { + pre?.right = child + } + } + // 子结点数量 = 2 + else { + // 获取中序遍历中 cur 的下一个结点 + let nex = getInOrderNext(root: cur?.right) + let tmp = nex!.val + // 递归删除结点 nex + remove(num: nex!.val) + // 将 nex 的值复制给 cur + cur?.val = tmp + } + return cur + } + + /* 获取中序遍历中的下一个结点(仅适用于 root 有左子结点的情况) */ + func getInOrderNext(root: TreeNode?) -> TreeNode? { + var root = root + if root == nil { + return root + } + // 循环访问左子结点,直到叶结点时为最小结点,跳出 + while root?.left != nil { + root = root?.left + } + return root + } + ``` + +=== "Zig" + + ```zig title="binary_search_tree.zig" + + ``` + +### 排序 + +我们知道,「中序遍历」遵循“左 $\rightarrow$ 根 $\rightarrow$ 右”的遍历优先级,而二叉搜索树遵循“左子结点 $<$ 根结点 $<$ 右子结点”的大小关系。因此,在二叉搜索树中进行中序遍历时,总是会优先遍历下一个最小结点,从而得出一条重要性质:**二叉搜索树的中序遍历序列是升序的**。 + +借助中序遍历升序的性质,我们在二叉搜索树中获取有序数据仅需 $O(n)$ 时间,而无需额外排序,非常高效。 + +![bst_inorder_traversal](binary_search_tree.assets/bst_inorder_traversal.png) + +## 7.3.2. 二叉搜索树的效率 + +假设给定 $n$ 个数字,最常用的存储方式是「数组」,那么对于这串乱序的数字,常见操作的效率为: + +- **查找元素**:由于数组是无序的,因此需要遍历数组来确定,使用 $O(n)$ 时间; +- **插入元素**:只需将元素添加至数组尾部即可,使用 $O(1)$ 时间; +- **删除元素**:先查找元素,使用 $O(n)$ 时间,再在数组中删除该元素,使用 $O(n)$ 时间; +- **获取最小 / 最大元素**:需要遍历数组来确定,使用 $O(n)$ 时间; + +为了得到先验信息,我们也可以预先将数组元素进行排序,得到一个「排序数组」,此时操作效率为: + +- **查找元素**:由于数组已排序,可以使用二分查找,平均使用 $O(\log n)$ 时间; +- **插入元素**:先查找插入位置,使用 $O(\log n)$ 时间,再插入到指定位置,使用 $O(n)$ 时间; +- **删除元素**:先查找元素,使用 $O(\log n)$ 时间,再在数组中删除该元素,使用 $O(n)$ 时间; +- **获取最小 / 最大元素**:数组头部和尾部元素即是最小和最大元素,使用 $O(1)$ 时间; + +观察发现,无序数组和有序数组中的各项操作的时间复杂度是“偏科”的,即有的快有的慢;**而二叉搜索树的各项操作的时间复杂度都是对数阶,在数据量 $n$ 很大时有巨大优势**。 + +
+ +| | 无序数组 | 有序数组 | 二叉搜索树 | +| ------------------- | -------- | ----------- | ----------- | +| 查找指定元素 | $O(n)$ | $O(\log n)$ | $O(\log n)$ | +| 插入元素 | $O(1)$ | $O(n)$ | $O(\log n)$ | +| 删除元素 | $O(n)$ | $O(n)$ | $O(\log n)$ | +| 获取最小 / 最大元素 | $O(n)$ | $O(1)$ | $O(\log n)$ | + +
+ +## 7.3.3. 二叉搜索树的退化 + +理想情况下,我们希望二叉搜索树的是“左右平衡”的(详见「平衡二叉树」章节),此时可以在 $\log n$ 轮循环内查找任意结点。 + +如果我们动态地在二叉搜索树中插入与删除结点,**则可能导致二叉树退化为链表**,此时各种操作的时间复杂度也退化之 $O(n)$ 。 + +!!! note + + 在实际应用中,如何保持二叉搜索树的平衡,也是一个需要重要考虑的问题。 + +![bst_degradation](binary_search_tree.assets/bst_degradation.png) + +## 7.3.4. 二叉搜索树常见应用 + +- 系统中的多级索引,高效查找、插入、删除操作。 +- 各种搜索算法的底层数据结构。 +- 存储数据流,保持其已排序。 diff --git a/build/chapter_tree/binary_tree.md b/build/chapter_tree/binary_tree.md new file mode 100644 index 000000000..2e876d539 --- /dev/null +++ b/build/chapter_tree/binary_tree.md @@ -0,0 +1,578 @@ +--- +comments: true +--- + +# 7.1. 二叉树 + +「二叉树 Binary Tree」是一种非线性数据结构,代表着祖先与后代之间的派生关系,体现着“一分为二”的分治逻辑。类似于链表,二叉树也是以结点为单位存储的,结点包含「值」和两个「指针」。 + +=== "Java" + + ```java title="" + /* 链表结点类 */ + class TreeNode { + int val; // 结点值 + TreeNode left; // 左子结点指针 + TreeNode right; // 右子结点指针 + TreeNode(int x) { val = x; } + } + ``` + +=== "C++" + + ```cpp title="" + /* 链表结点结构体 */ + struct TreeNode { + int val; // 结点值 + TreeNode *left; // 左子结点指针 + TreeNode *right; // 右子结点指针 + TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} + }; + ``` + +=== "Python" + + ```python title="" + """ 链表结点类 """ + class TreeNode: + def __init__(self, val=None, left=None, right=None): + self.val = val # 结点值 + self.left = left # 左子结点指针 + self.right = right # 右子结点指针 + ``` + +=== "Go" + + ```go title="" + /* 链表结点类 */ + type TreeNode struct { + Val int + Left *TreeNode + Right *TreeNode + } + /* 结点初始化方法 */ + func NewTreeNode(v int) *TreeNode { + return &TreeNode{ + Left: nil, + Right: nil, + Val: v, + } + } + ``` + +=== "JavaScript" + + ```js title="" + /* 链表结点类 */ + function TreeNode(val, left, right) { + this.val = (val === undefined ? 0 : val); // 结点值 + this.left = (left === undefined ? null : left); // 左子结点指针 + this.right = (right === undefined ? null : right); // 右子结点指针 + } + ``` + +=== "TypeScript" + + ```typescript title="" + /* 链表结点类 */ + class TreeNode { + val: number; + left: TreeNode | null; + right: TreeNode | null; + + constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) { + this.val = val === undefined ? 0 : val; // 结点值 + this.left = left === undefined ? null : left; // 左子结点指针 + this.right = right === undefined ? null : right; // 右子结点指针 + } + } + ``` + +=== "C" + + ```c title="" + + ``` + +=== "C#" + + ```csharp title="" + /* 链表结点类 */ + class TreeNode { + int val; // 结点值 + TreeNode? left; // 左子结点指针 + TreeNode? right; // 右子结点指针 + TreeNode(int x) { val = x; } + } + ``` + +=== "Swift" + + ```swift title="" + /* 链表结点类 */ + class TreeNode { + var val: Int // 结点值 + var left: TreeNode? // 左子结点指针 + var right: TreeNode? // 右子结点指针 + + init(x: Int) { + val = x + } + } + ``` + +=== "Zig" + + ```zig title="" + + ``` + +结点的两个指针分别指向「左子结点 Left Child Node」和「右子结点 Right Child Node」,并且称该结点为两个子结点的「父结点 Parent Node」。给定二叉树某结点,将左子结点以下的树称为该结点的「左子树 Left Subtree」,右子树同理。 + +除了叶结点外,每个结点都有子结点和子树。例如,若将下图的「结点 2」看作父结点,那么其左子结点和右子结点分别为「结点 4」和「结点 5」,左子树和右子树分别为「结点 4 及其以下结点形成的树」和「结点 5 及其以下结点形成的树」。 + +![binary_tree_definition](binary_tree.assets/binary_tree_definition.png) + +

Fig. 子结点与子树

+ +## 7.1.1. 二叉树常见术语 + +二叉树的术语较多,建议尽量理解并记住。后续可能遗忘,可以在需要使用时回来查看确认。 + +- 「根结点 Root Node」:二叉树最顶层的结点,其没有父结点; +- 「叶结点 Leaf Node」:没有子结点的结点,其两个指针都指向 $\text{null}$ ; +- 结点所处「层 Level」:从顶至底依次增加,根结点所处层为 1 ; +- 结点「度 Degree」:结点的子结点数量。二叉树中,度的范围是 0, 1, 2 ; +- 「边 Edge」:连接两个结点的边,即结点指针; +- 二叉树「高度」:二叉树中根结点到最远叶结点走过边的数量; +- 结点「深度 Depth」 :根结点到该结点走过边的数量; +- 结点「高度 Height」:最远叶结点到该结点走过边的数量; + +![binary_tree_terminology](binary_tree.assets/binary_tree_terminology.png) + +

Fig. 二叉树的常见术语

+ +!!! tip "高度与深度的定义" + + 值得注意,我们通常将「高度」和「深度」定义为“走过边的数量”,而有些题目或教材会将其定义为“走过结点的数量”,此时高度或深度都需要 + 1 。 + +## 7.1.2. 二叉树基本操作 + +**初始化二叉树**。与链表类似,先初始化结点,再构建引用指向(即指针)。 + +=== "Java" + + ```java title="binary_tree.java" + // 初始化结点 + TreeNode n1 = new TreeNode(1); + TreeNode n2 = new TreeNode(2); + TreeNode n3 = new TreeNode(3); + TreeNode n4 = new TreeNode(4); + TreeNode n5 = new TreeNode(5); + // 构建引用指向(即指针) + n1.left = n2; + n1.right = n3; + n2.left = n4; + n2.right = n5; + ``` + +=== "C++" + + ```cpp title="binary_tree.cpp" + /* 初始化二叉树 */ + // 初始化结点 + TreeNode* n1 = new TreeNode(1); + TreeNode* n2 = new TreeNode(2); + TreeNode* n3 = new TreeNode(3); + TreeNode* n4 = new TreeNode(4); + TreeNode* n5 = new TreeNode(5); + // 构建引用指向(即指针) + n1->left = n2; + n1->right = n3; + n2->left = n4; + n2->right = n5; + ``` + +=== "Python" + + ```python title="binary_tree.py" + """ 初始化二叉树 """ + # 初始化节点 + n1 = TreeNode(val=1) + n2 = TreeNode(val=2) + n3 = TreeNode(val=3) + n4 = TreeNode(val=4) + n5 = TreeNode(val=5) + # 构建引用指向(即指针) + n1.left = n2 + n1.right = n3 + n2.left = n4 + n2.right = n5 + ``` + +=== "Go" + + ```go title="binary_tree.go" + /* 初始化二叉树 */ + // 初始化结点 + n1 := NewTreeNode(1) + n2 := NewTreeNode(2) + n3 := NewTreeNode(3) + n4 := NewTreeNode(4) + n5 := NewTreeNode(5) + // 构建引用指向(即指针) + n1.Left = n2 + n1.Right = n3 + n2.Left = n4 + n2.Right = n5 + ``` + +=== "JavaScript" + + ```js title="binary_tree.js" + /* 初始化二叉树 */ + // 初始化结点 + let n1 = new TreeNode(1), + n2 = new TreeNode(2), + n3 = new TreeNode(3), + n4 = new TreeNode(4), + n5 = new TreeNode(5); + // 构建引用指向(即指针) + n1.left = n2; + n1.right = n3; + n2.left = n4; + n2.right = n5; + ``` + +=== "TypeScript" + + ```typescript title="binary_tree.ts" + /* 初始化二叉树 */ + // 初始化结点 + let n1 = new TreeNode(1), + n2 = new TreeNode(2), + n3 = new TreeNode(3), + n4 = new TreeNode(4), + n5 = new TreeNode(5); + // 构建引用指向(即指针) + n1.left = n2; + n1.right = n3; + n2.left = n4; + n2.right = n5; + ``` + +=== "C" + + ```c title="binary_tree.c" + + ``` + +=== "C#" + + ```csharp title="binary_tree.cs" + /* 初始化二叉树 */ + // 初始化结点 + TreeNode n1 = new TreeNode(1); + TreeNode n2 = new TreeNode(2); + TreeNode n3 = new TreeNode(3); + TreeNode n4 = new TreeNode(4); + TreeNode n5 = new TreeNode(5); + // 构建引用指向(即指针) + n1.left = n2; + n1.right = n3; + n2.left = n4; + n2.right = n5; + ``` + +=== "Swift" + + ```swift title="binary_tree.swift" + // 初始化结点 + let n1 = TreeNode(x: 1) + let n2 = TreeNode(x: 2) + let n3 = TreeNode(x: 3) + let n4 = TreeNode(x: 4) + let n5 = TreeNode(x: 5) + // 构建引用指向(即指针) + n1.left = n2 + n1.right = n3 + n2.left = n4 + n2.right = n5 + ``` + +=== "Zig" + + ```zig title="binary_tree.zig" + + ``` + +**插入与删除结点**。与链表类似,插入与删除结点都可以通过修改指针实现。 + +![binary_tree_add_remove](binary_tree.assets/binary_tree_add_remove.png) + +

Fig. 在二叉树中插入与删除结点

+ +=== "Java" + + ```java title="binary_tree.java" + TreeNode P = new TreeNode(0); + // 在 n1 -> n2 中间插入结点 P + n1.left = P; + P.left = n2; + // 删除结点 P + n1.left = n2; + ``` + +=== "C++" + + ```cpp title="binary_tree.cpp" + /* 插入与删除结点 */ + TreeNode* P = new TreeNode(0); + // 在 n1 -> n2 中间插入结点 P + n1->left = P; + P->left = n2; + // 删除结点 P + n1->left = n2; + ``` + +=== "Python" + + ```python title="binary_tree.py" + """ 插入与删除结点 """ + p = TreeNode(0) + # 在 n1 -> n2 中间插入结点 P + n1.left = p + p.left = n2 + # 删除节点 P + n1.left = n2 + ``` + +=== "Go" + + ```go title="binary_tree.go" + /* 插入与删除结点 */ + // 在 n1 -> n2 中间插入结点 P + p := NewTreeNode(0) + n1.Left = p + p.Left = n2 + // 删除结点 P + n1.Left = n2 + ``` + +=== "JavaScript" + + ```js title="binary_tree.js" + /* 插入与删除结点 */ + let P = new TreeNode(0); + // 在 n1 -> n2 中间插入结点 P + n1.left = P; + P.left = n2; + // 删除结点 P + n1.left = n2; + ``` + +=== "TypeScript" + + ```typescript title="binary_tree.ts" + /* 插入与删除结点 */ + const P = new TreeNode(0); + // 在 n1 -> n2 中间插入结点 P + n1.left = P; + P.left = n2; + // 删除结点 P + n1.left = n2; + ``` + +=== "C" + + ```c title="binary_tree.c" + + ``` + +=== "C#" + + ```csharp title="binary_tree.cs" + /* 插入与删除结点 */ + TreeNode P = new TreeNode(0); + // 在 n1 -> n2 中间插入结点 P + n1.left = P; + P.left = n2; + // 删除结点 P + n1.left = n2; + ``` + +=== "Swift" + + ```swift title="binary_tree.swift" + let P = TreeNode(x: 0) + // 在 n1 -> n2 中间插入结点 P + n1.left = P + P.left = n2 + // 删除结点 P + n1.left = n2 + ``` + +=== "Zig" + + ```zig title="binary_tree.zig" + + ``` + +!!! note + + 插入结点会改变二叉树的原有逻辑结构,删除结点往往意味着删除了该结点的所有子树。因此,二叉树中的插入与删除一般都是由一套操作配合完成的,这样才能实现有意义的操作。 + +## 7.1.3. 常见二叉树类型 + +### 完美二叉树 + +「完美二叉树 Perfect Binary Tree」的所有层的结点都被完全填满。在完美二叉树中,所有结点的度 = 2 ;若树高度 $= h$ ,则结点总数 $= 2^{h+1} - 1$ ,呈标准的指数级关系,反映着自然界中常见的细胞分裂。 + +!!! tip + + 在中文社区中,完美二叉树常被称为「满二叉树」,请注意与完满二叉树区分。 + +![perfect_binary_tree](binary_tree.assets/perfect_binary_tree.png) + +### 完全二叉树 + +「完全二叉树 Complete Binary Tree」只有最底层的结点未被填满,且最底层结点尽量靠左填充。 + +**完全二叉树非常适合用数组来表示**。如果按照层序遍历序列的顺序来存储,那么空结点 `null` 一定全部出现在序列的尾部,因此我们就可以不用存储这些 null 了。 + +![complete_binary_tree](binary_tree.assets/complete_binary_tree.png) + +### 完满二叉树 + +「完满二叉树 Full Binary Tree」除了叶结点之外,其余所有结点都有两个子结点。 + +![full_binary_tree](binary_tree.assets/full_binary_tree.png) + +### 平衡二叉树 + +「平衡二叉树 Balanced Binary Tree」中任意结点的左子树和右子树的高度之差的绝对值 $\leq 1$ 。 + +![balanced_binary_tree](binary_tree.assets/balanced_binary_tree.png) + +## 7.1.4. 二叉树的退化 + +当二叉树的每层的结点都被填满时,达到「完美二叉树」;而当所有结点都偏向一边时,二叉树退化为「链表」。 + +- 完美二叉树是一个二叉树的“最佳状态”,可以完全发挥出二叉树“分治”的优势; +- 链表则是另一个极端,各项操作都变为线性操作,时间复杂度退化至 $O(n)$ ; + +![binary_tree_corner_cases](binary_tree.assets/binary_tree_corner_cases.png) + +

Fig. 二叉树的最佳和最差结构

+ +如下表所示,在最佳和最差结构下,二叉树的叶结点数量、结点总数、高度等达到极大或极小值。 + +
+ +| | 完美二叉树 | 链表 | +| ----------------------------- | ---------- | ---------- | +| 第 $i$ 层的结点数量 | $2^{i-1}$ | $1$ | +| 树的高度为 $h$ 时的叶结点数量 | $2^h$ | $1$ | +| 树的高度为 $h$ 时的结点总数 | $2^{h+1} - 1$ | $h + 1$ | +| 树的结点总数为 $n$ 时的高度 | $\log_2 (n+1) - 1$ | $n - 1$ | + +
+ +## 7.1.5. 二叉树表示方式 * + +我们一般使用二叉树的「链表表示」,即存储单位为结点 `TreeNode` ,结点之间通过指针(引用)相连接。本文前述示例代码展示了二叉树在链表表示下的各项基本操作。 + +那能否可以用「数组表示」二叉树呢?答案是肯定的。先来分析一个简单案例,给定一个「完美二叉树」,将结点按照层序遍历的顺序编号(从 0 开始),那么可以推导得出父结点索引与子结点索引之间的「映射公式」:**设结点的索引为 $i$ ,则该结点的左子结点索引为 $2i + 1$ 、右子结点索引为 $2i + 2$** 。 + +**本质上,映射公式的作用就是链表中的指针**。对于层序遍历序列中的任意结点,我们都可以使用映射公式来访问子结点。因此,可以直接使用层序遍历序列(即数组)来表示完美二叉树。 + +![array_representation_mapping](binary_tree.assets/array_representation_mapping.png) + +然而,完美二叉树只是个例,二叉树中间层往往存在许多空结点(即 `null` ),而层序遍历序列并不包含这些空结点,并且我们无法单凭序列来猜测空结点的数量和分布位置,**即理论上存在许多种二叉树都符合该层序遍历序列**。显然,这种情况无法使用数组来存储二叉树。 + +![array_representation_without_empty](binary_tree.assets/array_representation_without_empty.png) + +为了解决此问题,考虑按照完美二叉树的形式来表示所有二叉树,**即在序列中使用特殊符号来显式地表示“空位”**。如下图所示,这样处理后,序列(数组)就可以唯一表示二叉树了。 + +=== "Java" + + ```java title="" + /* 二叉树的数组表示 */ + // 使用 int 的包装类 Integer ,就可以使用 null 来标记空位 + Integer[] tree = { 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 }; + ``` + +=== "C++" + + ```cpp title="" + /* 二叉树的数组表示 */ + // 为了符合数据类型为 int ,使用 int 最大值标记空位 + // 该方法的使用前提是没有结点的值 = INT_MAX + vector tree = { 1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15 }; + ``` + +=== "Python" + + ```python title="" + """ 二叉树的数组表示 """ + # 直接使用 None 来表示空位 + tree = [1, 2, 3, 4, None, 6, 7, 8, 9, None, None, 12, None, None, 15] + ``` + +=== "Go" + + ```go title="" + + ``` + +=== "JavaScript" + + ```js title="" + /* 二叉树的数组表示 */ + // 直接使用 null 来表示空位 + let tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15]; + ``` + +=== "TypeScript" + + ```typescript title="" + /* 二叉树的数组表示 */ + // 直接使用 null 来表示空位 + let tree: (number | null)[] = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15]; + ``` + +=== "C" + + ```c title="" + + ``` + +=== "C#" + + ```csharp title="" + /* 二叉树的数组表示 */ + // 使用 int? 可空类型 ,就可以使用 null 来标记空位 + int?[] tree = { 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 }; + ``` + +=== "Swift" + + ```swift title="" + /* 二叉树的数组表示 */ + // 使用 Int? 可空类型 ,就可以使用 nil 来标记空位 + let tree: [Int?] = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15] + ``` + +=== "Zig" + + ```zig title="" + + ``` + +![array_representation_with_empty](binary_tree.assets/array_representation_with_empty.png) + +回顾「完全二叉树」的定义,其只有最底层有空结点,并且最底层的结点尽量靠左,因而所有空结点都一定出现在层序遍历序列的末尾。**因为我们先验地确定了空位的位置,所以在使用数组表示完全二叉树时,可以省略存储“空位”**。因此,完全二叉树非常适合使用数组来表示。 + +![array_representation_complete_binary_tree](binary_tree.assets/array_representation_complete_binary_tree.png) + +数组表示有两个优点: 一是不需要存储指针,节省空间;二是可以随机访问结点。然而,当二叉树中的“空位”很多时,数组中只包含很少结点的数据,空间利用率很低。 diff --git a/build/chapter_tree/binary_tree_traversal.md b/build/chapter_tree/binary_tree_traversal.md new file mode 100755 index 000000000..197cb0bb5 --- /dev/null +++ b/build/chapter_tree/binary_tree_traversal.md @@ -0,0 +1,520 @@ +--- +comments: true +--- + +# 7.2. 二叉树遍历 + +非线性数据结构的遍历操作比线性数据结构更加复杂,往往需要使用搜索算法来实现。常见的二叉树遍历方式有层序遍历、前序遍历、中序遍历、后序遍历。 + +## 7.2.1. 层序遍历 + +「层序遍历 Hierarchical-Order Traversal」从顶至底、一层一层地遍历二叉树,并在每层中按照从左到右的顺序访问结点。 + +层序遍历本质上是「广度优先搜索 Breadth-First Traversal」,其体现着一种“一圈一圈向外”的层进遍历方式。 + +![binary_tree_bfs](binary_tree_traversal.assets/binary_tree_bfs.png) + +

Fig. 二叉树的层序遍历

+ +广度优先遍历一般借助「队列」来实现。队列的规则是“先进先出”,广度优先遍历的规则是 ”一层层平推“ ,两者背后的思想是一致的。 + +=== "Java" + + ```java title="binary_tree_bfs.java" + /* 层序遍历 */ + List hierOrder(TreeNode root) { + // 初始化队列,加入根结点 + Queue queue = new LinkedList<>() {{ add(root); }}; + // 初始化一个列表,用于保存遍历序列 + List list = new ArrayList<>(); + while (!queue.isEmpty()) { + TreeNode node = queue.poll(); // 队列出队 + list.add(node.val); // 保存结点值 + if (node.left != null) + queue.offer(node.left); // 左子结点入队 + if (node.right != null) + queue.offer(node.right); // 右子结点入队 + } + return list; + } + ``` + +=== "C++" + + ```cpp title="binary_tree_bfs.cpp" + /* 层序遍历 */ + vector hierOrder(TreeNode* root) { + // 初始化队列,加入根结点 + queue queue; + queue.push(root); + // 初始化一个列表,用于保存遍历序列 + vector vec; + while (!queue.empty()) { + TreeNode* node = queue.front(); + queue.pop(); // 队列出队 + vec.push_back(node->val); // 保存结点 + if (node->left != nullptr) + queue.push(node->left); // 左子结点入队 + if (node->right != nullptr) + queue.push(node->right); // 右子结点入队 + } + return vec; + } + ``` + +=== "Python" + + ```python title="binary_tree_bfs.py" + """ 层序遍历 """ + def hier_order(root: Optional[TreeNode]): + # 初始化队列,加入根结点 + queue = collections.deque() + queue.append(root) + # 初始化一个列表,用于保存遍历序列 + res = [] + while queue: + node = queue.popleft() # 队列出队 + res.append(node.val) # 保存节点值 + if node.left is not None: + queue.append(node.left) # 左子结点入队 + if node.right is not None: + queue.append(node.right) # 右子结点入队 + return res + ``` + +=== "Go" + + ```go title="binary_tree_bfs.go" + /* 层序遍历 */ + func levelOrder(root *TreeNode) []int { + // 初始化队列,加入根结点 + queue := list.New() + queue.PushBack(root) + // 初始化一个切片,用于保存遍历序列 + nums := make([]int, 0) + for queue.Len() > 0 { + // poll + node := queue.Remove(queue.Front()).(*TreeNode) + // 保存结点 + nums = append(nums, node.Val) + if node.Left != nil { + // 左子结点入队 + queue.PushBack(node.Left) + } + if node.Right != nil { + // 右子结点入队 + queue.PushBack(node.Right) + } + } + return nums + } + ``` + +=== "JavaScript" + + ```js title="binary_tree_bfs.js" + /* 层序遍历 */ + function hierOrder(root) { + // 初始化队列,加入根结点 + let queue = [root]; + // 初始化一个列表,用于保存遍历序列 + let list = []; + while (queue.length) { + let node = queue.shift(); // 队列出队 + list.push(node.val); // 保存结点 + if (node.left) + queue.push(node.left); // 左子结点入队 + if (node.right) + queue.push(node.right); // 右子结点入队 + } + return list; + } + ``` + +=== "TypeScript" + + ```typescript title="binary_tree_bfs.ts" + /* 层序遍历 */ + function hierOrder(root: TreeNode | null): number[] { + // 初始化队列,加入根结点 + const queue = [root]; + // 初始化一个列表,用于保存遍历序列 + const list: number[] = []; + while (queue.length) { + let node = queue.shift() as TreeNode; // 队列出队 + list.push(node.val); // 保存结点 + if (node.left) { + queue.push(node.left); // 左子结点入队 + } + if (node.right) { + queue.push(node.right); // 右子结点入队 + } + } + return list; + } + ``` + +=== "C" + + ```c title="binary_tree_bfs.c" + + ``` + +=== "C#" + + ```csharp title="binary_tree_bfs.cs" + /* 层序遍历 */ + public List hierOrder(TreeNode root) + { + // 初始化队列,加入根结点 + Queue queue = new(); + queue.Enqueue(root); + // 初始化一个列表,用于保存遍历序列 + List list = new(); + while (queue.Count != 0) + { + TreeNode node = queue.Dequeue(); // 队列出队 + list.Add(node.val); // 保存结点值 + if (node.left != null) + queue.Enqueue(node.left); // 左子结点入队 + if (node.right != null) + queue.Enqueue(node.right); // 右子结点入队 + } + return list; + } + + ``` + +=== "Swift" + + ```swift title="binary_tree_bfs.swift" + /* 层序遍历 */ + func hierOrder(root: TreeNode) -> [Int] { + // 初始化队列,加入根结点 + var queue: [TreeNode] = [root] + // 初始化一个列表,用于保存遍历序列 + var list: [Int] = [] + while !queue.isEmpty { + let node = queue.removeFirst() // 队列出队 + list.append(node.val) // 保存结点 + if let left = node.left { + queue.append(left) // 左子结点入队 + } + if let right = node.right { + queue.append(right) // 右子结点入队 + } + } + return list + } + ``` + +=== "Zig" + + ```zig title="binary_tree_bfs.zig" + + ``` + +## 7.2.2. 前序、中序、后序遍历 + +相对地,前、中、后序遍历皆属于「深度优先遍历 Depth-First Traversal」,其体现着一种“先走到尽头,再回头继续”的回溯遍历方式。 + +如下图所示,左侧是深度优先遍历的的示意图,右上方是对应的递归实现代码。深度优先遍历就像是绕着整个二叉树的外围“走”一圈,走的过程中,在每个结点都会遇到三个位置,分别对应前序遍历、中序遍历、后序遍历。 + +![binary_tree_dfs](binary_tree_traversal.assets/binary_tree_dfs.png) + +

Fig. 二叉树的前 / 中 / 后序遍历

+ +
+ +| 位置 | 含义 | 此处访问结点时对应 | +| ---------- | ------------------------------------ | ----------------------------- | +| 橙色圆圈处 | 刚进入此结点,即将访问该结点的左子树 | 前序遍历 Pre-Order Traversal | +| 蓝色圆圈处 | 已访问完左子树,即将访问右子树 | 中序遍历 In-Order Traversal | +| 紫色圆圈处 | 已访问完左子树和右子树,即将返回 | 后序遍历 Post-Order Traversal | + +
+ +=== "Java" + + ```java title="binary_tree_dfs.java" + /* 前序遍历 */ + void preOrder(TreeNode root) { + if (root == null) return; + // 访问优先级:根结点 -> 左子树 -> 右子树 + list.add(root.val); + preOrder(root.left); + preOrder(root.right); + } + + /* 中序遍历 */ + void inOrder(TreeNode root) { + if (root == null) return; + // 访问优先级:左子树 -> 根结点 -> 右子树 + inOrder(root.left); + list.add(root.val); + inOrder(root.right); + } + + /* 后序遍历 */ + void postOrder(TreeNode root) { + if (root == null) return; + // 访问优先级:左子树 -> 右子树 -> 根结点 + postOrder(root.left); + postOrder(root.right); + list.add(root.val); + } + ``` + +=== "C++" + + ```cpp title="binary_tree_dfs.cpp" + /* 前序遍历 */ + void preOrder(TreeNode* root) { + if (root == nullptr) return; + // 访问优先级:根结点 -> 左子树 -> 右子树 + vec.push_back(root->val); + preOrder(root->left); + preOrder(root->right); + } + + /* 中序遍历 */ + void inOrder(TreeNode* root) { + if (root == nullptr) return; + // 访问优先级:左子树 -> 根结点 -> 右子树 + inOrder(root->left); + vec.push_back(root->val); + inOrder(root->right); + } + + /* 后序遍历 */ + void postOrder(TreeNode* root) { + if (root == nullptr) return; + // 访问优先级:左子树 -> 右子树 -> 根结点 + postOrder(root->left); + postOrder(root->right); + vec.push_back(root->val); + } + ``` + +=== "Python" + + ```python title="binary_tree_dfs.py" + """ 前序遍历 """ + def pre_order(root: Optional[TreeNode]): + if root is None: + return + # 访问优先级:根结点 -> 左子树 -> 右子树 + res.append(root.val) + pre_order(root=root.left) + pre_order(root=root.right) + + """ 中序遍历 """ + def in_order(root: Optional[TreeNode]): + if root is None: + return + # 访问优先级:左子树 -> 根结点 -> 右子树 + in_order(root=root.left) + res.append(root.val) + in_order(root=root.right) + + """ 后序遍历 """ + def post_order(root: Optional[TreeNode]): + if root is None: + return + # 访问优先级:左子树 -> 右子树 -> 根结点 + post_order(root=root.left) + post_order(root=root.right) + res.append(root.val) + ``` + +=== "Go" + + ```go title="binary_tree_dfs.go" + /* 前序遍历 */ + func preOrder(node *TreeNode) { + if node == nil { + return + } + // 访问优先级:根结点 -> 左子树 -> 右子树 + nums = append(nums, node.Val) + preOrder(node.Left) + preOrder(node.Right) + } + + /* 中序遍历 */ + func inOrder(node *TreeNode) { + if node == nil { + return + } + // 访问优先级:左子树 -> 根结点 -> 右子树 + inOrder(node.Left) + nums = append(nums, node.Val) + inOrder(node.Right) + } + + /* 后序遍历 */ + func postOrder(node *TreeNode) { + if node == nil { + return + } + // 访问优先级:左子树 -> 右子树 -> 根结点 + postOrder(node.Left) + postOrder(node.Right) + nums = append(nums, node.Val) + } + ``` + +=== "JavaScript" + + ```js title="binary_tree_dfs.js" + /* 前序遍历 */ + function preOrder(root){ + if (root === null) return; + // 访问优先级:根结点 -> 左子树 -> 右子树 + list.push(root.val); + preOrder(root.left); + preOrder(root.right); + } + + /* 中序遍历 */ + function inOrder(root) { + if (root === null) return; + // 访问优先级:左子树 -> 根结点 -> 右子树 + inOrder(root.left); + list.push(root.val); + inOrder(root.right); + } + + /* 后序遍历 */ + function postOrder(root) { + if (root === null) return; + // 访问优先级:左子树 -> 右子树 -> 根结点 + postOrder(root.left); + postOrder(root.right); + list.push(root.val); + } + ``` + +=== "TypeScript" + + ```typescript title="binary_tree_dfs.ts" + /* 前序遍历 */ + function preOrder(root: TreeNode | null): void { + if (root === null) { + return; + } + // 访问优先级:根结点 -> 左子树 -> 右子树 + list.push(root.val); + preOrder(root.left); + preOrder(root.right); + } + + /* 中序遍历 */ + function inOrder(root: TreeNode | null): void { + if (root === null) { + return; + } + // 访问优先级:左子树 -> 根结点 -> 右子树 + inOrder(root.left); + list.push(root.val); + inOrder(root.right); + } + + /* 后序遍历 */ + function postOrder(root: TreeNode | null): void { + if (root === null) { + return; + } + // 访问优先级:左子树 -> 右子树 -> 根结点 + postOrder(root.left); + postOrder(root.right); + list.push(root.val); + } + ``` + +=== "C" + + ```c title="binary_tree_dfs.c" + + ``` + +=== "C#" + + ```csharp title="binary_tree_dfs.cs" + /* 前序遍历 */ + void preOrder(TreeNode? root) + { + if (root == null) return; + // 访问优先级:根结点 -> 左子树 -> 右子树 + list.Add(root.val); + preOrder(root.left); + preOrder(root.right); + } + + /* 中序遍历 */ + void inOrder(TreeNode? root) + { + if (root == null) return; + // 访问优先级:左子树 -> 根结点 -> 右子树 + inOrder(root.left); + list.Add(root.val); + inOrder(root.right); + } + + /* 后序遍历 */ + void postOrder(TreeNode? root) + { + if (root == null) return; + // 访问优先级:左子树 -> 右子树 -> 根结点 + postOrder(root.left); + postOrder(root.right); + list.Add(root.val); + } + ``` + +=== "Swift" + + ```swift title="binary_tree_dfs.swift" + /* 前序遍历 */ + func preOrder(root: TreeNode?) { + guard let root = root else { + return + } + // 访问优先级:根结点 -> 左子树 -> 右子树 + list.append(root.val) + preOrder(root: root.left) + preOrder(root: root.right) + } + + /* 中序遍历 */ + func inOrder(root: TreeNode?) { + guard let root = root else { + return + } + // 访问优先级:左子树 -> 根结点 -> 右子树 + inOrder(root: root.left) + list.append(root.val) + inOrder(root: root.right) + } + + /* 后序遍历 */ + func postOrder(root: TreeNode?) { + guard let root = root else { + return + } + // 访问优先级:左子树 -> 右子树 -> 根结点 + postOrder(root: root.left) + postOrder(root: root.right) + list.append(root.val) + } + ``` + +=== "Zig" + + ```zig title="binary_tree_dfs.zig" + + ``` + +!!! note + + 使用循环一样可以实现前、中、后序遍历,但代码相对繁琐,有兴趣的同学可以自行实现。 diff --git a/build/chapter_tree/summary.md b/build/chapter_tree/summary.md new file mode 100644 index 000000000..8566eaa8e --- /dev/null +++ b/build/chapter_tree/summary.md @@ -0,0 +1,18 @@ +--- +comments: true +--- + +# 7.5. 小结 + +- 二叉树是一种非线性数据结构,代表着“一分为二”的分治逻辑。二叉树的结点包含「值」和两个「指针」,分别指向左子结点和右子结点。 +- 选定二叉树中某结点,将其左(右)子结点以下形成的树称为左(右)子树。 +- 二叉树的术语较多,包括根结点、叶结点、层、度、边、高度、深度等。 +- 二叉树的初始化、结点插入、结点删除操作与链表的操作方法类似。 +- 常见的二叉树类型包括完美二叉树、完全二叉树、完满二叉树、平衡二叉树。完美二叉树是理想状态,链表则是退化后的最差状态。 +- 二叉树可以使用数组表示,具体做法是将结点值和空位按照层序遍历的顺序排列,并基于父结点和子结点之间的索引映射公式实现指针。 + +- 二叉树层序遍历是一种广度优先搜索,体现着“一圈一圈向外”的层进式遍历方式,通常借助队列来实现。 +- 前序、中序、后序遍历是深度优先搜索,体现着“走到头、再回头继续”的回溯遍历方式,通常使用递归实现。 +- 二叉搜索树是一种高效的元素查找数据结构,查找、插入、删除操作的时间复杂度皆为 $O(\log n)$ 。二叉搜索树退化为链表后,各项时间复杂度劣化至 $O(n)$ ,因此如何避免退化是非常重要的课题。 +- AVL 树又称平衡二叉搜索树,其通过旋转操作,使得在不断插入与删除结点后,仍然可以保持二叉树的平衡(不退化)。 +- AVL 树的旋转操作分为右旋、左旋、先右旋后左旋、先左旋后右旋。在插入或删除结点后,AVL 树会从底至顶地执行旋转操作,使树恢复平衡。 diff --git a/build/index.md b/build/index.md new file mode 100644 index 000000000..0ab257690 --- /dev/null +++ b/build/index.md @@ -0,0 +1,77 @@ +--- +comments: true +hide: + - footer +--- + +=== " " + +
+ ![conceptual_rendering](index.assets/conceptual_rendering.png){ align=left width=350 } +




+

《 Hello,算法 》

+

动画图解、能运行、可提问的
数据结构与算法快速入门教程

+

[![github-stars](https://img.shields.io/github/stars/krahets/hello-algo?style=social)](https://github.com/krahets/hello-algo)

+
[@Krahets](https://leetcode.cn/u/jyd/)
+
+ +--- + +

「清晰动画讲解」

+ +

动画诠释重点,平滑学习曲线
电脑、平板、手机全终端阅读

+ +![algorithm_animation](index.assets/animation.gif) + +!!! quote "" + +

"A picture is worth a thousand words."

+

“一图胜千言”

+ +--- + +

「代码实践导向」

+ +

提供经典算法的清晰实现与测试代码
多种语言,详细注释,皆可一键运行

+ +![running_code](index.assets/running_code.gif) + +!!! quote "" + +

"Talk is cheap. Show me the code."

+

“少吹牛,看代码”

+ +--- + +

「可讨论与提问」

+ +

作者一般 72h 内回复评论问题
与小伙伴们一起讨论学习进步

+ +![comment](index.assets/comment.gif) + +!!! quote "" + +

“追风赶月莫停留,平芜尽处是春山”

+

一起加油!

+ +--- + +

推荐语

+ +!!! quote + + “一本通俗易懂的数据结构与算法入门书,引导读者手脑并用地学习,强烈推荐算法初学者阅读。” + + **—— 邓俊辉,清华大学计算机系教授** + +

致谢

+ +感谢本开源书的每一位撰稿人,是他们的无私奉献让这本书变得更好,他们是: + +

+ + + +

+ +--- diff --git a/build/overrides/partials/comments.html b/build/overrides/partials/comments.html new file mode 100755 index 000000000..bac275a6e --- /dev/null +++ b/build/overrides/partials/comments.html @@ -0,0 +1,50 @@ +{% if page.meta.comments %} +

{{ lang.t("meta.comments") }}

+ + + + +{% endif %} \ No newline at end of file diff --git a/build/stylesheets/extra.css b/build/stylesheets/extra.css new file mode 100644 index 000000000..4f153c794 --- /dev/null +++ b/build/stylesheets/extra.css @@ -0,0 +1,70 @@ + +/* Color Settings */ +/* https://github.com/squidfunk/mkdocs-material/blob/6b5035f5580f97532d664e3d1babf5f320e88ee9/src/assets/stylesheets/main/_colors.scss */ +/* https://squidfunk.github.io/mkdocs-material/setup/changing-the-colors/#custom-colors */ +:root > * { + --md-primary-fg-color: #FFFFFF; + --md-primary-bg-color: #1D1D20; + + --md-accent-fg-color: #999; + + --md-typeset-color: #1D1D20; + --md-typeset-a-color: #2AA996; +} + +[data-md-color-scheme="slate"] { + --md-primary-fg-color: #2E303E; + --md-primary-bg-color: #FEFEFE; + + --md-accent-fg-color: #999; + + --md-typeset-color: #FEFEFE; + --md-typeset-a-color: #21C8B8; +} + +/* https://github.com/squidfunk/mkdocs-material/issues/4832#issuecomment-1374891676 */ +.md-nav__link[for] { + color: var(--md-default-fg-color) !important +} + +/* Center Markdown Tables (requires md_in_html extension) */ +.center-table { + text-align: center; +} + +.md-typeset .center-table :is(td,th):not([align]) { + /* Reset alignment for table cells */ + text-align: initial; +} + + +/* Markdown Header */ +/* https://github.com/squidfunk/mkdocs-material/blob/dcab57dd1cced4b77875c1aa1b53467c62709d31/src/assets/stylesheets/main/_typeset.scss */ +.md-typeset h1 { + font-weight: 400; + color: var(--md-default-fg-color); +} + +.md-typeset h2 { + font-weight: 400; +} + +.md-typeset h3 { + font-weight: 500; +} + +.md-typeset a { + text-decoration: underline; +} + +/* Image align center */ +.center { + display: block; + margin: 0 auto; +} + +/* font-family setting for Win10 */ +body { + --md-text-font-family: -apple-system,BlinkMacSystemFont,var(--md-text-font,_),Helvetica,Arial,sans-serif; + --md-code-font-family: var(--md-code-font,_),SFMono-Regular,Consolas,Menlo,-apple-system,BlinkMacSystemFont,var(--md-text-font,_),monospace; +} \ No newline at end of file