{"config":{"separator":"[\\s\\-_,:!=\\[\\]()\\\\\"`/]+|\\.(?!\\d)"},"items":[{"location":"chapter_appendix/","level":1,"title":"第 16 章   附录","text":"","path":["第 16 章   附录"],"tags":[]},{"location":"chapter_appendix/#_1","level":2,"title":"本章内容","text":"","path":["第 16 章   附录"],"tags":[]},{"location":"chapter_appendix/contribution/","level":1,"title":"16.2   一起参与创作","text":"

由于笔者能力有限,书中难免存在一些遗漏和错误,请您谅解。如果您发现了笔误、链接失效、内容缺失、文字歧义、解释不清晰或行文结构不合理等问题,请协助我们进行修正,以给读者提供更优质的学习资源。

所有撰稿人的 GitHub ID 将在本书仓库、网页版和 PDF 版的主页上进行展示,以感谢他们对开源社区的无私奉献。

开源的魅力

纸质图书的两次印刷的间隔时间往往较久,内容更新非常不方便。

而在本开源书中,内容更迭的时间被缩短至数日甚至几个小时。

","path":["第 16 章   附录","16.2   一起参与创作"],"tags":[]},{"location":"chapter_appendix/contribution/#1","level":3,"title":"1.   内容微调","text":"

如图 16-3 所示,每个页面的右上角都有“编辑图标”。您可以按照以下步骤修改文本或代码。

  1. 点击“编辑图标”,如果遇到“需要 Fork 此仓库”的提示,请同意该操作。
  2. 修改 Markdown 源文件内容,检查内容的正确性,并尽量保持排版格式的统一。
  3. 在页面底部填写修改说明,然后点击“Propose file change”按钮。页面跳转后,点击“Create pull request”按钮即可发起拉取请求。

图 16-3   页面编辑按键

图片无法直接修改,需要通过新建 Issue 或评论留言来描述问题,我们会尽快重新绘制并替换图片。

","path":["第 16 章   附录","16.2   一起参与创作"],"tags":[]},{"location":"chapter_appendix/contribution/#2","level":3,"title":"2.   内容创作","text":"

如果您有兴趣参与此开源项目,包括将代码翻译成其他编程语言、扩展文章内容等,那么需要实施以下 Pull Request 工作流程。

  1. 登录 GitHub ,将本书的代码仓库 Fork 到个人账号下。
  2. 进入您的 Fork 仓库网页,使用 git clone 命令将仓库克隆至本地。
  3. 在本地进行内容创作,并进行完整测试,验证代码的正确性。
  4. 将本地所做更改 Commit ,然后 Push 至远程仓库。
  5. 刷新仓库网页,点击“Create pull request”按钮即可发起拉取请求。
","path":["第 16 章   附录","16.2   一起参与创作"],"tags":[]},{"location":"chapter_appendix/contribution/#3-docker","level":3,"title":"3.   Docker 部署","text":"

hello-algo 根目录下,执行以下 Docker 脚本,即可在 http://localhost:8000 访问本项目:

docker-compose up -d\n

使用以下命令即可删除部署:

docker-compose down\n
","path":["第 16 章   附录","16.2   一起参与创作"],"tags":[]},{"location":"chapter_appendix/installation/","level":1,"title":"16.1   编程环境安装","text":"","path":["第 16 章   附录","16.1   编程环境安装"],"tags":[]},{"location":"chapter_appendix/installation/#1611-ide","level":2,"title":"16.1.1   安装 IDE","text":"

推荐使用开源、轻量的 VS Code 作为本地集成开发环境(IDE)。访问 VS Code 官网,根据操作系统选择相应版本的 VS Code 进行下载和安装。

图 16-1   从官网下载 VS Code

VS Code 拥有强大的扩展包生态系统,支持大多数编程语言的运行和调试。以 Python 为例,安装“Python Extension Pack”扩展包之后,即可进行 Python 代码调试。安装步骤如图 16-2 所示。

图 16-2   安装 VS Code 扩展包

","path":["第 16 章   附录","16.1   编程环境安装"],"tags":[]},{"location":"chapter_appendix/installation/#1612","level":2,"title":"16.1.2   安装语言环境","text":"","path":["第 16 章   附录","16.1   编程环境安装"],"tags":[]},{"location":"chapter_appendix/installation/#1-python","level":3,"title":"1.   Python 环境","text":"
  1. 下载并安装 Miniconda3 ,需要 Python 3.10 或更新版本。
  2. 在 VS Code 的插件市场中搜索 python ,安装 Python Extension Pack 。
  3. (可选)在命令行输入 pip install black ,安装代码格式化工具。
","path":["第 16 章   附录","16.1   编程环境安装"],"tags":[]},{"location":"chapter_appendix/installation/#2-cc","level":3,"title":"2.   C/C++ 环境","text":"
  1. Windows 系统需要安装 MinGW(配置教程);MacOS 自带 Clang ,无须安装。
  2. 在 VS Code 的插件市场中搜索 c++ ,安装 C/C++ Extension Pack 。
  3. (可选)打开 Settings 页面,搜索 Clang_format_fallback Style 代码格式化选项,设置为 { BasedOnStyle: Microsoft, BreakBeforeBraces: Attach }
","path":["第 16 章   附录","16.1   编程环境安装"],"tags":[]},{"location":"chapter_appendix/installation/#3-java","level":3,"title":"3.   Java 环境","text":"
  1. 下载并安装 OpenJDK(版本需满足 > JDK 9)。
  2. 在 VS Code 的插件市场中搜索 java ,安装 Extension Pack for Java 。
","path":["第 16 章   附录","16.1   编程环境安装"],"tags":[]},{"location":"chapter_appendix/installation/#4-c","level":3,"title":"4.   C# 环境","text":"
  1. 下载并安装 .Net 8.0 。
  2. 在 VS Code 的插件市场中搜索 C# Dev Kit ,安装 C# Dev Kit (配置教程)。
  3. 也可使用 Visual Studio(安装教程)。
","path":["第 16 章   附录","16.1   编程环境安装"],"tags":[]},{"location":"chapter_appendix/installation/#5-go","level":3,"title":"5.   Go 环境","text":"
  1. 下载并安装 go 。
  2. 在 VS Code 的插件市场中搜索 go ,安装 Go 。
  3. 按快捷键 Ctrl + Shift + P 呼出命令栏,输入 go ,选择 Go: Install/Update Tools ,全部勾选并安装即可。
","path":["第 16 章   附录","16.1   编程环境安装"],"tags":[]},{"location":"chapter_appendix/installation/#6-swift","level":3,"title":"6.   Swift 环境","text":"
  1. 下载并安装 Swift 。
  2. 在 VS Code 的插件市场中搜索 swift ,安装 Swift for Visual Studio Code 。
","path":["第 16 章   附录","16.1   编程环境安装"],"tags":[]},{"location":"chapter_appendix/installation/#7-javascript","level":3,"title":"7.   JavaScript 环境","text":"
  1. 下载并安装 Node.js 。
  2. (可选)在 VS Code 的插件市场中搜索 Prettier ,安装代码格式化工具。
","path":["第 16 章   附录","16.1   编程环境安装"],"tags":[]},{"location":"chapter_appendix/installation/#8-typescript","level":3,"title":"8.   TypeScript 环境","text":"
  1. 同 JavaScript 环境安装步骤。
  2. 安装 TypeScript Execute (tsx) 。
  3. 在 VS Code 的插件市场中搜索 typescript ,安装 Pretty TypeScript Errors 。
","path":["第 16 章   附录","16.1   编程环境安装"],"tags":[]},{"location":"chapter_appendix/installation/#9-dart","level":3,"title":"9.   Dart 环境","text":"
  1. 下载并安装 Dart 。
  2. 在 VS Code 的插件市场中搜索 dart ,安装 Dart 。
","path":["第 16 章   附录","16.1   编程环境安装"],"tags":[]},{"location":"chapter_appendix/installation/#10-rust","level":3,"title":"10.   Rust 环境","text":"
  1. 下载并安装 Rust 。
  2. 在 VS Code 的插件市场中搜索 rust ,安装 rust-analyzer 。
","path":["第 16 章   附录","16.1   编程环境安装"],"tags":[]},{"location":"chapter_appendix/terminology/","level":1,"title":"16.3   术语表","text":"

表 16-1 列出了书中出现的重要术语。建议记住各个名词的英文叫法,以便阅读英文文献。

表 16-1   数据结构与算法的重要名词

English 简体中文 algorithm 算法 data structure 数据结构 code 代码 file 文件 function 函数 method 方法 variable 变量 asymptotic complexity analysis 渐近复杂度分析 time complexity 时间复杂度 space complexity 空间复杂度 loop 循环 iteration 迭代 recursion 递归 tail recursion 尾递归 recursion tree 递归树 big-\\(O\\) notation 大 \\(O\\) 记号 asymptotic upper bound 渐近上界 sign-magnitude 原码 1’s complement 反码 2’s complement 补码 array 数组 index 索引 linked list 链表 linked list node, list node 链表节点 head node 头节点 tail node 尾节点 list 列表 dynamic array 动态数组 hard disk 硬盘 random-access memory (RAM) 内存 cache memory 缓存 cache miss 缓存未命中 cache hit rate 缓存命中率 stack 栈 top of the stack 栈顶 bottom of the stack 栈底 queue 队列 double-ended queue 双向队列 front of the queue 队首 rear of the queue 队尾 hash table 哈希表 hash set 哈希集合 bucket 桶 hash function 哈希函数 hash collision 哈希冲突 load factor 负载因子 separate chaining 链式地址 open addressing 开放寻址 linear probing 线性探测 lazy deletion 懒删除 binary tree 二叉树 tree node 树节点 left-child node 左子节点 right-child node 右子节点 parent node 父节点 left subtree 左子树 right subtree 右子树 root node 根节点 leaf node 叶节点 edge 边 level 层 degree 度 height 高度 depth 深度 perfect binary tree 完美二叉树 complete binary tree 完全二叉树 full binary tree 完满二叉树 balanced binary tree 平衡二叉树 binary search tree 二叉搜索树 AVL tree AVL 树 red-black tree 红黑树 level-order traversal 层序遍历 breadth-first traversal 广度优先遍历 depth-first traversal 深度优先遍历 binary search tree 二叉搜索树 balanced binary search tree 平衡二叉搜索树 balance factor 平衡因子 heap 堆 max heap 大顶堆 min heap 小顶堆 priority queue 优先队列 heapify 堆化 top-\\(k\\) problem Top-\\(k\\) 问题 graph 图 vertex 顶点 undirected graph 无向图 directed graph 有向图 connected graph 连通图 disconnected graph 非连通图 weighted graph 有权图 adjacency 邻接 path 路径 in-degree 入度 out-degree 出度 adjacency matrix 邻接矩阵 adjacency list 邻接表 breadth-first search 广度优先搜索 depth-first search 深度优先搜索 binary search 二分查找 searching algorithm 搜索算法 sorting algorithm 排序算法 selection sort 选择排序 bubble sort 冒泡排序 insertion sort 插入排序 quick sort 快速排序 merge sort 归并排序 heap sort 堆排序 bucket sort 桶排序 counting sort 计数排序 radix sort 基数排序 divide and conquer 分治 hanota problem 汉诺塔问题 backtracking algorithm 回溯算法 constraint 约束 solution 解 state 状态 pruning 剪枝 permutations problem 全排列问题 subset-sum problem 子集和问题 \\(n\\)-queens problem \\(n\\) 皇后问题 dynamic programming 动态规划 initial state 初始状态 state-transition equation 状态转移方程 knapsack problem 背包问题 edit distance problem 编辑距离问题 greedy algorithm 贪心算法","path":["第 16 章   附录","16.3   术语表"],"tags":[]},{"location":"chapter_array_and_linkedlist/","level":1,"title":"第 4 章   数组与链表","text":"

Abstract

数据结构的世界如同一堵厚实的砖墙。

数组的砖块整齐排列,逐个紧贴。链表的砖块分散各处,连接的藤蔓自由地穿梭于砖缝之间。

","path":["第 4 章   数组与链表"],"tags":[]},{"location":"chapter_array_and_linkedlist/#_1","level":2,"title":"本章内容","text":"","path":["第 4 章   数组与链表"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/","level":1,"title":"4.1   数组","text":"

数组(array)是一种线性数据结构,其将相同类型的元素存储在连续的内存空间中。我们将元素在数组中的位置称为该元素的索引(index)。图 4-1 展示了数组的主要概念和存储方式。

图 4-1   数组定义与存储方式

","path":["第 4 章   数组与链表","4.1   数组"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#411","level":2,"title":"4.1.1   数组常用操作","text":"","path":["第 4 章   数组与链表","4.1   数组"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#1","level":3,"title":"1.   初始化数组","text":"

我们可以根据需求选用数组的两种初始化方式:无初始值、给定初始值。在未指定初始值的情况下,大多数编程语言会将数组元素初始化为 \\(0\\) :

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
# 初始化数组\narr: list[int] = [0] * 5  # [ 0, 0, 0, 0, 0 ]\nnums: list[int] = [1, 3, 2, 5, 4]\n
array.cpp
/* 初始化数组 */\n// 存储在栈上\nint arr[5];\nint nums[5] = { 1, 3, 2, 5, 4 };\n// 存储在堆上(需要手动释放空间)\nint* arr1 = new int[5];\nint* nums1 = new int[5] { 1, 3, 2, 5, 4 };\n
array.java
/* 初始化数组 */\nint[] arr = new int[5]; // { 0, 0, 0, 0, 0 }\nint[] nums = { 1, 3, 2, 5, 4 };\n
array.cs
/* 初始化数组 */\nint[] arr = new int[5]; // [ 0, 0, 0, 0, 0 ]\nint[] nums = [1, 3, 2, 5, 4];\n
array.go
/* 初始化数组 */\nvar arr [5]int\n// 在 Go 中,指定长度时([5]int)为数组,不指定长度时([]int)为切片\n// 由于 Go 的数组被设计为在编译期确定长度,因此只能使用常量来指定长度\n// 为了方便实现扩容 extend() 方法,以下将切片(Slice)看作数组(Array)\nnums := []int{1, 3, 2, 5, 4}\n
array.swift
/* 初始化数组 */\nlet arr = Array(repeating: 0, count: 5) // [0, 0, 0, 0, 0]\nlet nums = [1, 3, 2, 5, 4]\n
array.js
/* 初始化数组 */\nvar arr = new Array(5).fill(0);\nvar nums = [1, 3, 2, 5, 4];\n
array.ts
/* 初始化数组 */\nlet arr: number[] = new Array(5).fill(0);\nlet nums: number[] = [1, 3, 2, 5, 4];\n
array.dart
/* 初始化数组 */\nList<int> arr = List.filled(5, 0); // [0, 0, 0, 0, 0]\nList<int> nums = [1, 3, 2, 5, 4];\n
array.rs
/* 初始化数组 */\nlet arr: [i32; 5] = [0; 5]; // [0, 0, 0, 0, 0]\nlet slice: &[i32] = &[0; 5];\n// 在 Rust 中,指定长度时([i32; 5])为数组,不指定长度时(&[i32])为切片\n// 由于 Rust 的数组被设计为在编译期确定长度,因此只能使用常量来指定长度\n// Vector 是 Rust 一般情况下用作动态数组的类型\n// 为了方便实现扩容 extend() 方法,以下将 vector 看作数组(array)\nlet nums: Vec<i32> = vec![1, 3, 2, 5, 4];\n
array.c
/* 初始化数组 */\nint arr[5] = { 0 }; // { 0, 0, 0, 0, 0 }\nint nums[5] = { 1, 3, 2, 5, 4 };\n
array.kt
/* 初始化数组 */\nvar arr = IntArray(5) // { 0, 0, 0, 0, 0 }\nvar nums = intArrayOf(1, 3, 2, 5, 4)\n
array.rb
# 初始化数组\narr = Array.new(5, 0)\nnums = [1, 3, 2, 5, 4]\n
可视化运行

全屏观看 >

","path":["第 4 章   数组与链表","4.1   数组"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#2","level":3,"title":"2.   访问元素","text":"

数组元素被存储在连续的内存空间中,这意味着计算数组元素的内存地址非常容易。给定数组内存地址(首元素内存地址)和某个元素的索引,我们可以使用图 4-2 所示的公式计算得到该元素的内存地址,从而直接访问该元素。

图 4-2   数组元素的内存地址计算

观察图 4-2 ,我们发现数组首个元素的索引为 \\(0\\) ,这似乎有些反直觉,因为从 \\(1\\) 开始计数会更自然。但从地址计算公式的角度看,索引本质上是内存地址的偏移量。首个元素的地址偏移量是 \\(0\\) ,因此它的索引为 \\(0\\) 是合理的。

在数组中访问元素非常高效,我们可以在 \\(O(1)\\) 时间内随机访问数组中的任意一个元素。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
def random_access(nums: list[int]) -> int:\n    \"\"\"随机访问元素\"\"\"\n    # 在区间 [0, len(nums)-1] 中随机抽取一个数字\n    random_index = random.randint(0, len(nums) - 1)\n    # 获取并返回随机元素\n    random_num = nums[random_index]\n    return random_num\n
array.cpp
/* 随机访问元素 */\nint randomAccess(int *nums, int size) {\n    // 在区间 [0, size) 中随机抽取一个数字\n    int randomIndex = rand() % size;\n    // 获取并返回随机元素\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
array.java
/* 随机访问元素 */\nint randomAccess(int[] nums) {\n    // 在区间 [0, nums.length) 中随机抽取一个数字\n    int randomIndex = ThreadLocalRandom.current().nextInt(0, nums.length);\n    // 获取并返回随机元素\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
array.cs
/* 随机访问元素 */\nint RandomAccess(int[] nums) {\n    Random random = new();\n    // 在区间 [0, nums.Length) 中随机抽取一个数字\n    int randomIndex = random.Next(nums.Length);\n    // 获取并返回随机元素\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
array.go
/* 随机访问元素 */\nfunc randomAccess(nums []int) (randomNum int) {\n    // 在区间 [0, nums.length) 中随机抽取一个数字\n    randomIndex := rand.Intn(len(nums))\n    // 获取并返回随机元素\n    randomNum = nums[randomIndex]\n    return\n}\n
array.swift
/* 随机访问元素 */\nfunc randomAccess(nums: [Int]) -> Int {\n    // 在区间 [0, nums.count) 中随机抽取一个数字\n    let randomIndex = nums.indices.randomElement()!\n    // 获取并返回随机元素\n    let randomNum = nums[randomIndex]\n    return randomNum\n}\n
array.js
/* 随机访问元素 */\nfunction randomAccess(nums) {\n    // 在区间 [0, nums.length) 中随机抽取一个数字\n    const random_index = Math.floor(Math.random() * nums.length);\n    // 获取并返回随机元素\n    const random_num = nums[random_index];\n    return random_num;\n}\n
array.ts
/* 随机访问元素 */\nfunction randomAccess(nums: number[]): number {\n    // 在区间 [0, nums.length) 中随机抽取一个数字\n    const random_index = Math.floor(Math.random() * nums.length);\n    // 获取并返回随机元素\n    const random_num = nums[random_index];\n    return random_num;\n}\n
array.dart
/* 随机访问元素 */\nint randomAccess(List<int> nums) {\n  // 在区间 [0, nums.length) 中随机抽取一个数字\n  int randomIndex = Random().nextInt(nums.length);\n  // 获取并返回随机元素\n  int randomNum = nums[randomIndex];\n  return randomNum;\n}\n
array.rs
/* 随机访问元素 */\nfn random_access(nums: &[i32]) -> i32 {\n    // 在区间 [0, nums.len()) 中随机抽取一个数字\n    let random_index = rand::thread_rng().gen_range(0..nums.len());\n    // 获取并返回随机元素\n    let random_num = nums[random_index];\n    random_num\n}\n
array.c
/* 随机访问元素 */\nint randomAccess(int *nums, int size) {\n    // 在区间 [0, size) 中随机抽取一个数字\n    int randomIndex = rand() % size;\n    // 获取并返回随机元素\n    int randomNum = nums[randomIndex];\n    return randomNum;\n}\n
array.kt
/* 随机访问元素 */\nfun randomAccess(nums: IntArray): Int {\n    // 在区间 [0, nums.size) 中随机抽取一个数字\n    val randomIndex = ThreadLocalRandom.current().nextInt(0, nums.size)\n    // 获取并返回随机元素\n    val randomNum = nums[randomIndex]\n    return randomNum\n}\n
array.rb
### 随机访问元素 ###\ndef random_access(nums)\n  # 在区间 [0, nums.length) 中随机抽取一个数字\n  random_index = Random.rand(0...nums.length)\n\n  # 获取并返回随机元素\n  nums[random_index]\nend\n
可视化运行

全屏观看 >

","path":["第 4 章   数组与链表","4.1   数组"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#3","level":3,"title":"3.   插入元素","text":"

数组元素在内存中是“紧挨着的”,它们之间没有空间再存放任何数据。如图 4-3 所示,如果想在数组中间插入一个元素,则需要将该元素之后的所有元素都向后移动一位,之后再把元素赋值给该索引。

图 4-3   数组插入元素示例

值得注意的是,由于数组的长度是固定的,因此插入一个元素必定会导致数组尾部元素“丢失”。我们将这个问题的解决方案留在“列表”章节中讨论。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
def insert(nums: list[int], num: int, index: int):\n    \"\"\"在数组的索引 index 处插入元素 num\"\"\"\n    # 把索引 index 以及之后的所有元素向后移动一位\n    for i in range(len(nums) - 1, index, -1):\n        nums[i] = nums[i - 1]\n    # 将 num 赋给 index 处的元素\n    nums[index] = num\n
array.cpp
/* 在数组的索引 index 处插入元素 num */\nvoid insert(int *nums, int size, int num, int index) {\n    // 把索引 index 以及之后的所有元素向后移动一位\n    for (int i = size - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // 将 num 赋给 index 处的元素\n    nums[index] = num;\n}\n
array.java
/* 在数组的索引 index 处插入元素 num */\nvoid insert(int[] nums, int num, int index) {\n    // 把索引 index 以及之后的所有元素向后移动一位\n    for (int i = nums.length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // 将 num 赋给 index 处的元素\n    nums[index] = num;\n}\n
array.cs
/* 在数组的索引 index 处插入元素 num */\nvoid Insert(int[] nums, int num, int index) {\n    // 把索引 index 以及之后的所有元素向后移动一位\n    for (int i = nums.Length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // 将 num 赋给 index 处的元素\n    nums[index] = num;\n}\n
array.go
/* 在数组的索引 index 处插入元素 num */\nfunc insert(nums []int, num int, index int) {\n    // 把索引 index 以及之后的所有元素向后移动一位\n    for i := len(nums) - 1; i > index; i-- {\n        nums[i] = nums[i-1]\n    }\n    // 将 num 赋给 index 处的元素\n    nums[index] = num\n}\n
array.swift
/* 在数组的索引 index 处插入元素 num */\nfunc insert(nums: inout [Int], num: Int, index: Int) {\n    // 把索引 index 以及之后的所有元素向后移动一位\n    for i in nums.indices.dropFirst(index).reversed() {\n        nums[i] = nums[i - 1]\n    }\n    // 将 num 赋给 index 处的元素\n    nums[index] = num\n}\n
array.js
/* 在数组的索引 index 处插入元素 num */\nfunction insert(nums, num, index) {\n    // 把索引 index 以及之后的所有元素向后移动一位\n    for (let i = nums.length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // 将 num 赋给 index 处的元素\n    nums[index] = num;\n}\n
array.ts
/* 在数组的索引 index 处插入元素 num */\nfunction insert(nums: number[], num: number, index: number): void {\n    // 把索引 index 以及之后的所有元素向后移动一位\n    for (let i = nums.length - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // 将 num 赋给 index 处的元素\n    nums[index] = num;\n}\n
array.dart
/* 在数组的索引 index 处插入元素 _num */\nvoid insert(List<int> nums, int _num, int index) {\n  // 把索引 index 以及之后的所有元素向后移动一位\n  for (var i = nums.length - 1; i > index; i--) {\n    nums[i] = nums[i - 1];\n  }\n  // 将 _num 赋给 index 处元素\n  nums[index] = _num;\n}\n
array.rs
/* 在数组的索引 index 处插入元素 num */\nfn insert(nums: &mut [i32], num: i32, index: usize) {\n    // 把索引 index 以及之后的所有元素向后移动一位\n    for i in (index + 1..nums.len()).rev() {\n        nums[i] = nums[i - 1];\n    }\n    // 将 num 赋给 index 处的元素\n    nums[index] = num;\n}\n
array.c
/* 在数组的索引 index 处插入元素 num */\nvoid insert(int *nums, int size, int num, int index) {\n    // 把索引 index 以及之后的所有元素向后移动一位\n    for (int i = size - 1; i > index; i--) {\n        nums[i] = nums[i - 1];\n    }\n    // 将 num 赋给 index 处的元素\n    nums[index] = num;\n}\n
array.kt
/* 在数组的索引 index 处插入元素 num */\nfun insert(nums: IntArray, num: Int, index: Int) {\n    // 把索引 index 以及之后的所有元素向后移动一位\n    for (i in nums.size - 1 downTo index + 1) {\n        nums[i] = nums[i - 1]\n    }\n    // 将 num 赋给 index 处的元素\n    nums[index] = num\n}\n
array.rb
### 在数组的索引 index 处插入元素 num ###\ndef insert(nums, num, index)\n  # 把索引 index 以及之后的所有元素向后移动一位\n  for i in (nums.length - 1).downto(index + 1)\n    nums[i] = nums[i - 1]\n  end\n\n  # 将 num 赋给 index 处的元素\n  nums[index] = num\nend\n
可视化运行

全屏观看 >

","path":["第 4 章   数组与链表","4.1   数组"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#4","level":3,"title":"4.   删除元素","text":"

同理,如图 4-4 所示,若想删除索引 \\(i\\) 处的元素,则需要把索引 \\(i\\) 之后的元素都向前移动一位。

图 4-4   数组删除元素示例

请注意,删除元素完成后,原先末尾的元素变得“无意义”了,所以我们无须特意去修改它。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
def remove(nums: list[int], index: int):\n    \"\"\"删除索引 index 处的元素\"\"\"\n    # 把索引 index 之后的所有元素向前移动一位\n    for i in range(index, len(nums) - 1):\n        nums[i] = nums[i + 1]\n
array.cpp
/* 删除索引 index 处的元素 */\nvoid remove(int *nums, int size, int index) {\n    // 把索引 index 之后的所有元素向前移动一位\n    for (int i = index; i < size - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.java
/* 删除索引 index 处的元素 */\nvoid remove(int[] nums, int index) {\n    // 把索引 index 之后的所有元素向前移动一位\n    for (int i = index; i < nums.length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.cs
/* 删除索引 index 处的元素 */\nvoid Remove(int[] nums, int index) {\n    // 把索引 index 之后的所有元素向前移动一位\n    for (int i = index; i < nums.Length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.go
/* 删除索引 index 处的元素 */\nfunc remove(nums []int, index int) {\n    // 把索引 index 之后的所有元素向前移动一位\n    for i := index; i < len(nums)-1; i++ {\n        nums[i] = nums[i+1]\n    }\n}\n
array.swift
/* 删除索引 index 处的元素 */\nfunc remove(nums: inout [Int], index: Int) {\n    // 把索引 index 之后的所有元素向前移动一位\n    for i in nums.indices.dropFirst(index).dropLast() {\n        nums[i] = nums[i + 1]\n    }\n}\n
array.js
/* 删除索引 index 处的元素 */\nfunction remove(nums, index) {\n    // 把索引 index 之后的所有元素向前移动一位\n    for (let i = index; i < nums.length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.ts
/* 删除索引 index 处的元素 */\nfunction remove(nums: number[], index: number): void {\n    // 把索引 index 之后的所有元素向前移动一位\n    for (let i = index; i < nums.length - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.dart
/* 删除索引 index 处的元素 */\nvoid remove(List<int> nums, int index) {\n  // 把索引 index 之后的所有元素向前移动一位\n  for (var i = index; i < nums.length - 1; i++) {\n    nums[i] = nums[i + 1];\n  }\n}\n
array.rs
/* 删除索引 index 处的元素 */\nfn remove(nums: &mut [i32], index: usize) {\n    // 把索引 index 之后的所有元素向前移动一位\n    for i in index..nums.len() - 1 {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.c
/* 删除索引 index 处的元素 */\n// 注意:stdio.h 占用了 remove 关键词\nvoid removeItem(int *nums, int size, int index) {\n    // 把索引 index 之后的所有元素向前移动一位\n    for (int i = index; i < size - 1; i++) {\n        nums[i] = nums[i + 1];\n    }\n}\n
array.kt
/* 删除索引 index 处的元素 */\nfun remove(nums: IntArray, index: Int) {\n    // 把索引 index 之后的所有元素向前移动一位\n    for (i in index..<nums.size - 1) {\n        nums[i] = nums[i + 1]\n    }\n}\n
array.rb
### 删除索引 index 处的元素 ###\ndef remove(nums, index)\n  # 把索引 index 之后的所有元素向前移动一位\n  for i in index...(nums.length - 1)\n    nums[i] = nums[i + 1]\n  end\nend\n
可视化运行

全屏观看 >

总的来看,数组的插入与删除操作有以下缺点。

","path":["第 4 章   数组与链表","4.1   数组"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#5","level":3,"title":"5.   遍历数组","text":"

在大多数编程语言中,我们既可以通过索引遍历数组,也可以直接遍历获取数组中的每个元素:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
def traverse(nums: list[int]):\n    \"\"\"遍历数组\"\"\"\n    count = 0\n    # 通过索引遍历数组\n    for i in range(len(nums)):\n        count += nums[i]\n    # 直接遍历数组元素\n    for num in nums:\n        count += num\n    # 同时遍历数据索引和元素\n    for i, num in enumerate(nums):\n        count += nums[i]\n        count += num\n
array.cpp
/* 遍历数组 */\nvoid traverse(int *nums, int size) {\n    int count = 0;\n    // 通过索引遍历数组\n    for (int i = 0; i < size; i++) {\n        count += nums[i];\n    }\n}\n
array.java
/* 遍历数组 */\nvoid traverse(int[] nums) {\n    int count = 0;\n    // 通过索引遍历数组\n    for (int i = 0; i < nums.length; i++) {\n        count += nums[i];\n    }\n    // 直接遍历数组元素\n    for (int num : nums) {\n        count += num;\n    }\n}\n
array.cs
/* 遍历数组 */\nvoid Traverse(int[] nums) {\n    int count = 0;\n    // 通过索引遍历数组\n    for (int i = 0; i < nums.Length; i++) {\n        count += nums[i];\n    }\n    // 直接遍历数组元素\n    foreach (int num in nums) {\n        count += num;\n    }\n}\n
array.go
/* 遍历数组 */\nfunc traverse(nums []int) {\n    count := 0\n    // 通过索引遍历数组\n    for i := 0; i < len(nums); i++ {\n        count += nums[i]\n    }\n    count = 0\n    // 直接遍历数组元素\n    for _, num := range nums {\n        count += num\n    }\n    // 同时遍历数据索引和元素\n    for i, num := range nums {\n        count += nums[i]\n        count += num\n    }\n}\n
array.swift
/* 遍历数组 */\nfunc traverse(nums: [Int]) {\n    var count = 0\n    // 通过索引遍历数组\n    for i in nums.indices {\n        count += nums[i]\n    }\n    // 直接遍历数组元素\n    for num in nums {\n        count += num\n    }\n    // 同时遍历数据索引和元素\n    for (i, num) in nums.enumerated() {\n        count += nums[i]\n        count += num\n    }\n}\n
array.js
/* 遍历数组 */\nfunction traverse(nums) {\n    let count = 0;\n    // 通过索引遍历数组\n    for (let i = 0; i < nums.length; i++) {\n        count += nums[i];\n    }\n    // 直接遍历数组元素\n    for (const num of nums) {\n        count += num;\n    }\n}\n
array.ts
/* 遍历数组 */\nfunction traverse(nums: number[]): void {\n    let count = 0;\n    // 通过索引遍历数组\n    for (let i = 0; i < nums.length; i++) {\n        count += nums[i];\n    }\n    // 直接遍历数组元素\n    for (const num of nums) {\n        count += num;\n    }\n}\n
array.dart
/* 遍历数组元素 */\nvoid traverse(List<int> nums) {\n  int count = 0;\n  // 通过索引遍历数组\n  for (var i = 0; i < nums.length; i++) {\n    count += nums[i];\n  }\n  // 直接遍历数组元素\n  for (int _num in nums) {\n    count += _num;\n  }\n  // 通过 forEach 方法遍历数组\n  nums.forEach((_num) {\n    count += _num;\n  });\n}\n
array.rs
/* 遍历数组 */\nfn traverse(nums: &[i32]) {\n    let mut _count = 0;\n    // 通过索引遍历数组\n    for i in 0..nums.len() {\n        _count += nums[i];\n    }\n    // 直接遍历数组元素\n    _count = 0;\n    for &num in nums {\n        _count += num;\n    }\n}\n
array.c
/* 遍历数组 */\nvoid traverse(int *nums, int size) {\n    int count = 0;\n    // 通过索引遍历数组\n    for (int i = 0; i < size; i++) {\n        count += nums[i];\n    }\n}\n
array.kt
/* 遍历数组 */\nfun traverse(nums: IntArray) {\n    var count = 0\n    // 通过索引遍历数组\n    for (i in nums.indices) {\n        count += nums[i]\n    }\n    // 直接遍历数组元素\n    for (j in nums) {\n        count += j\n    }\n}\n
array.rb
### 遍历数组 ###\ndef traverse(nums)\n  count = 0\n\n  # 通过索引遍历数组\n  for i in 0...nums.length\n    count += nums[i]\n  end\n\n  # 直接遍历数组元素\n  for num in nums\n    count += num\n  end\nend\n
可视化运行

全屏观看 >

","path":["第 4 章   数组与链表","4.1   数组"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#6","level":3,"title":"6.   查找元素","text":"

在数组中查找指定元素需要遍历数组,每轮判断元素值是否匹配,若匹配则输出对应索引。

因为数组是线性数据结构,所以上述查找操作被称为“线性查找”。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
def find(nums: list[int], target: int) -> int:\n    \"\"\"在数组中查找指定元素\"\"\"\n    for i in range(len(nums)):\n        if nums[i] == target:\n            return i\n    return -1\n
array.cpp
/* 在数组中查找指定元素 */\nint find(int *nums, int size, int target) {\n    for (int i = 0; i < size; i++) {\n        if (nums[i] == target)\n            return i;\n    }\n    return -1;\n}\n
array.java
/* 在数组中查找指定元素 */\nint find(int[] nums, int target) {\n    for (int i = 0; i < nums.length; i++) {\n        if (nums[i] == target)\n            return i;\n    }\n    return -1;\n}\n
array.cs
/* 在数组中查找指定元素 */\nint Find(int[] nums, int target) {\n    for (int i = 0; i < nums.Length; i++) {\n        if (nums[i] == target)\n            return i;\n    }\n    return -1;\n}\n
array.go
/* 在数组中查找指定元素 */\nfunc find(nums []int, target int) (index int) {\n    index = -1\n    for i := 0; i < len(nums); i++ {\n        if nums[i] == target {\n            index = i\n            break\n        }\n    }\n    return\n}\n
array.swift
/* 在数组中查找指定元素 */\nfunc find(nums: [Int], target: Int) -> Int {\n    for i in nums.indices {\n        if nums[i] == target {\n            return i\n        }\n    }\n    return -1\n}\n
array.js
/* 在数组中查找指定元素 */\nfunction find(nums, target) {\n    for (let i = 0; i < nums.length; i++) {\n        if (nums[i] === target) return i;\n    }\n    return -1;\n}\n
array.ts
/* 在数组中查找指定元素 */\nfunction find(nums: number[], target: number): number {\n    for (let i = 0; i < nums.length; i++) {\n        if (nums[i] === target) {\n            return i;\n        }\n    }\n    return -1;\n}\n
array.dart
/* 在数组中查找指定元素 */\nint find(List<int> nums, int target) {\n  for (var i = 0; i < nums.length; i++) {\n    if (nums[i] == target) return i;\n  }\n  return -1;\n}\n
array.rs
/* 在数组中查找指定元素 */\nfn find(nums: &[i32], target: i32) -> Option<usize> {\n    for i in 0..nums.len() {\n        if nums[i] == target {\n            return Some(i);\n        }\n    }\n    None\n}\n
array.c
/* 在数组中查找指定元素 */\nint find(int *nums, int size, int target) {\n    for (int i = 0; i < size; i++) {\n        if (nums[i] == target)\n            return i;\n    }\n    return -1;\n}\n
array.kt
/* 在数组中查找指定元素 */\nfun find(nums: IntArray, target: Int): Int {\n    for (i in nums.indices) {\n        if (nums[i] == target)\n            return i\n    }\n    return -1\n}\n
array.rb
### 在数组中查找指定元素 ###\ndef find(nums, target)\n  for i in 0...nums.length\n    return i if nums[i] == target\n  end\n\n  -1\nend\n
可视化运行

全屏观看 >

","path":["第 4 章   数组与链表","4.1   数组"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#7","level":3,"title":"7.   扩容数组","text":"

在复杂的系统环境中,程序难以保证数组之后的内存空间是可用的,从而无法安全地扩展数组容量。因此在大多数编程语言中,数组的长度是不可变的。

如果我们希望扩容数组,则需重新建立一个更大的数组,然后把原数组元素依次复制到新数组。这是一个 \\(O(n)\\) 的操作,在数组很大的情况下非常耗时。代码如下所示:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array.py
def extend(nums: list[int], enlarge: int) -> list[int]:\n    \"\"\"扩展数组长度\"\"\"\n    # 初始化一个扩展长度后的数组\n    res = [0] * (len(nums) + enlarge)\n    # 将原数组中的所有元素复制到新数组\n    for i in range(len(nums)):\n        res[i] = nums[i]\n    # 返回扩展后的新数组\n    return res\n
array.cpp
/* 扩展数组长度 */\nint *extend(int *nums, int size, int enlarge) {\n    // 初始化一个扩展长度后的数组\n    int *res = new int[size + enlarge];\n    // 将原数组中的所有元素复制到新数组\n    for (int i = 0; i < size; i++) {\n        res[i] = nums[i];\n    }\n    // 释放内存\n    delete[] nums;\n    // 返回扩展后的新数组\n    return res;\n}\n
array.java
/* 扩展数组长度 */\nint[] extend(int[] nums, int enlarge) {\n    // 初始化一个扩展长度后的数组\n    int[] res = new int[nums.length + enlarge];\n    // 将原数组中的所有元素复制到新数组\n    for (int i = 0; i < nums.length; i++) {\n        res[i] = nums[i];\n    }\n    // 返回扩展后的新数组\n    return res;\n}\n
array.cs
/* 扩展数组长度 */\nint[] Extend(int[] nums, int enlarge) {\n    // 初始化一个扩展长度后的数组\n    int[] res = new int[nums.Length + enlarge];\n    // 将原数组中的所有元素复制到新数组\n    for (int i = 0; i < nums.Length; i++) {\n        res[i] = nums[i];\n    }\n    // 返回扩展后的新数组\n    return res;\n}\n
array.go
/* 扩展数组长度 */\nfunc extend(nums []int, enlarge int) []int {\n    // 初始化一个扩展长度后的数组\n    res := make([]int, len(nums)+enlarge)\n    // 将原数组中的所有元素复制到新数组\n    for i, num := range nums {\n        res[i] = num\n    }\n    // 返回扩展后的新数组\n    return res\n}\n
array.swift
/* 扩展数组长度 */\nfunc extend(nums: [Int], enlarge: Int) -> [Int] {\n    // 初始化一个扩展长度后的数组\n    var res = Array(repeating: 0, count: nums.count + enlarge)\n    // 将原数组中的所有元素复制到新数组\n    for i in nums.indices {\n        res[i] = nums[i]\n    }\n    // 返回扩展后的新数组\n    return res\n}\n
array.js
/* 扩展数组长度 */\n// 请注意,JavaScript 的 Array 是动态数组,可以直接扩展\n// 为了方便学习,本函数将 Array 看作长度不可变的数组\nfunction extend(nums, enlarge) {\n    // 初始化一个扩展长度后的数组\n    const res = new Array(nums.length + enlarge).fill(0);\n    // 将原数组中的所有元素复制到新数组\n    for (let i = 0; i < nums.length; i++) {\n        res[i] = nums[i];\n    }\n    // 返回扩展后的新数组\n    return res;\n}\n
array.ts
/* 扩展数组长度 */\n// 请注意,TypeScript 的 Array 是动态数组,可以直接扩展\n// 为了方便学习,本函数将 Array 看作长度不可变的数组\nfunction extend(nums: number[], enlarge: number): number[] {\n    // 初始化一个扩展长度后的数组\n    const res = new Array(nums.length + enlarge).fill(0);\n    // 将原数组中的所有元素复制到新数组\n    for (let i = 0; i < nums.length; i++) {\n        res[i] = nums[i];\n    }\n    // 返回扩展后的新数组\n    return res;\n}\n
array.dart
/* 扩展数组长度 */\nList<int> extend(List<int> nums, int enlarge) {\n  // 初始化一个扩展长度后的数组\n  List<int> res = List.filled(nums.length + enlarge, 0);\n  // 将原数组中的所有元素复制到新数组\n  for (var i = 0; i < nums.length; i++) {\n    res[i] = nums[i];\n  }\n  // 返回扩展后的新数组\n  return res;\n}\n
array.rs
/* 扩展数组长度 */\nfn extend(nums: &[i32], enlarge: usize) -> Vec<i32> {\n    // 初始化一个扩展长度后的数组\n    let mut res: Vec<i32> = vec![0; nums.len() + enlarge];\n    // 将原数组中的所有元素复制到新\n    res[0..nums.len()].copy_from_slice(nums);\n\n    // 返回扩展后的新数组\n    res\n}\n
array.c
/* 扩展数组长度 */\nint *extend(int *nums, int size, int enlarge) {\n    // 初始化一个扩展长度后的数组\n    int *res = (int *)malloc(sizeof(int) * (size + enlarge));\n    // 将原数组中的所有元素复制到新数组\n    for (int i = 0; i < size; i++) {\n        res[i] = nums[i];\n    }\n    // 初始化扩展后的空间\n    for (int i = size; i < size + enlarge; i++) {\n        res[i] = 0;\n    }\n    // 返回扩展后的新数组\n    return res;\n}\n
array.kt
/* 扩展数组长度 */\nfun extend(nums: IntArray, enlarge: Int): IntArray {\n    // 初始化一个扩展长度后的数组\n    val res = IntArray(nums.size + enlarge)\n    // 将原数组中的所有元素复制到新数组\n    for (i in nums.indices) {\n        res[i] = nums[i]\n    }\n    // 返回扩展后的新数组\n    return res\n}\n
array.rb
### 扩展数组长度 ###\n# 请注意,Ruby 的 Array 是动态数组,可以直接扩展\n# 为了方便学习,本函数将 Array 看作长度不可变的数组\ndef extend(nums, enlarge)\n  # 初始化一个扩展长度后的数组\n  res = Array.new(nums.length + enlarge, 0)\n\n  # 将原数组中的所有元素复制到新数组\n  for i in 0...nums.length\n    res[i] = nums[i]\n  end\n\n  # 返回扩展后的新数组\n  res\nend\n
可视化运行

全屏观看 >

","path":["第 4 章   数组与链表","4.1   数组"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#412","level":2,"title":"4.1.2   数组的优点与局限性","text":"

数组存储在连续的内存空间内,且元素类型相同。这种做法包含丰富的先验信息,系统可以利用这些信息来优化数据结构的操作效率。

连续空间存储是一把双刃剑,其存在以下局限性。

","path":["第 4 章   数组与链表","4.1   数组"],"tags":[]},{"location":"chapter_array_and_linkedlist/array/#413","level":2,"title":"4.1.3   数组典型应用","text":"

数组是一种基础且常见的数据结构,既频繁应用在各类算法之中,也可用于实现各种复杂数据结构。

","path":["第 4 章   数组与链表","4.1   数组"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/","level":1,"title":"4.2   链表","text":"

内存空间是所有程序的公共资源,在一个复杂的系统运行环境下,空闲的内存空间可能散落在内存各处。我们知道,存储数组的内存空间必须是连续的,而当数组非常大时,内存可能无法提供如此大的连续空间。此时链表的灵活性优势就体现出来了。

链表(linked list)是一种线性数据结构,其中的每个元素都是一个节点对象,各个节点通过“引用”相连接。引用记录了下一个节点的内存地址,通过它可以从当前节点访问到下一个节点。

链表的设计使得各个节点可以分散存储在内存各处,它们的内存地址无须连续。

图 4-5   链表定义与存储方式

观察图 4-5 ,链表的组成单位是节点(node)对象。每个节点都包含两项数据:节点的“值”和指向下一节点的“引用”。

如以下代码所示,链表节点 ListNode 除了包含值,还需额外保存一个引用(指针)。因此在相同数据量下,链表比数组占用更多的内存空间。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
class ListNode:\n    \"\"\"链表节点类\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val               # 节点值\n        self.next: ListNode | None = None # 指向下一节点的引用\n
/* 链表节点结构体 */\nstruct ListNode {\n    int val;         // 节点值\n    ListNode *next;  // 指向下一节点的指针\n    ListNode(int x) : val(x), next(nullptr) {}  // 构造函数\n};\n
/* 链表节点类 */\nclass ListNode {\n    int val;        // 节点值\n    ListNode next;  // 指向下一节点的引用\n    ListNode(int x) { val = x; }  // 构造函数\n}\n
/* 链表节点类 */\nclass ListNode(int x) {  //构造函数\n    int val = x;         // 节点值\n    ListNode? next;      // 指向下一节点的引用\n}\n
/* 链表节点结构体 */\ntype ListNode struct {\n    Val  int       // 节点值\n    Next *ListNode // 指向下一节点的指针\n}\n\n// NewListNode 构造函数,创建一个新的链表\nfunc NewListNode(val int) *ListNode {\n    return &ListNode{\n        Val:  val,\n        Next: nil,\n    }\n}\n
/* 链表节点类 */\nclass ListNode {\n    var val: Int // 节点值\n    var next: ListNode? // 指向下一节点的引用\n\n    init(x: Int) { // 构造函数\n        val = x\n    }\n}\n
/* 链表节点类 */\nclass ListNode {\n    constructor(val, next) {\n        this.val = (val === undefined ? 0 : val);       // 节点值\n        this.next = (next === undefined ? null : next); // 指向下一节点的引用\n    }\n}\n
/* 链表节点类 */\nclass ListNode {\n    val: number;\n    next: ListNode | null;\n    constructor(val?: number, next?: ListNode | null) {\n        this.val = val === undefined ? 0 : val;        // 节点值\n        this.next = next === undefined ? null : next;  // 指向下一节点的引用\n    }\n}\n
/* 链表节点类 */\nclass ListNode {\n  int val; // 节点值\n  ListNode? next; // 指向下一节点的引用\n  ListNode(this.val, [this.next]); // 构造函数\n}\n
use std::rc::Rc;\nuse std::cell::RefCell;\n/* 链表节点类 */\n#[derive(Debug)]\nstruct ListNode {\n    val: i32, // 节点值\n    next: Option<Rc<RefCell<ListNode>>>, // 指向下一节点的指针\n}\n
/* 链表节点结构体 */\ntypedef struct ListNode {\n    int val;               // 节点值\n    struct ListNode *next; // 指向下一节点的指针\n} ListNode;\n\n/* 构造函数 */\nListNode *newListNode(int val) {\n    ListNode *node;\n    node = (ListNode *) malloc(sizeof(ListNode));\n    node->val = val;\n    node->next = NULL;\n    return node;\n}\n
/* 链表节点类 */\n// 构造方法\nclass ListNode(x: Int) {\n    val _val: Int = x          // 节点值\n    val next: ListNode? = null // 指向下一个节点的引用\n}\n
# 链表节点类\nclass ListNode\n  attr_accessor :val  # 节点值\n  attr_accessor :next # 指向下一节点的引用\n\n  def initialize(val=0, next_node=nil)\n    @val = val\n    @next = next_node\n  end\nend\n
","path":["第 4 章   数组与链表","4.2   链表"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#421","level":2,"title":"4.2.1   链表常用操作","text":"","path":["第 4 章   数组与链表","4.2   链表"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#1","level":3,"title":"1.   初始化链表","text":"

建立链表分为两步,第一步是初始化各个节点对象,第二步是构建节点之间的引用关系。初始化完成后,我们就可以从链表的头节点出发,通过引用指向 next 依次访问所有节点。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
# 初始化链表 1 -> 3 -> 2 -> 5 -> 4\n# 初始化各个节点\nn0 = ListNode(1)\nn1 = ListNode(3)\nn2 = ListNode(2)\nn3 = ListNode(5)\nn4 = ListNode(4)\n# 构建节点之间的引用\nn0.next = n1\nn1.next = n2\nn2.next = n3\nn3.next = n4\n
linked_list.cpp
/* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */\n// 初始化各个节点\nListNode* n0 = new ListNode(1);\nListNode* n1 = new ListNode(3);\nListNode* n2 = new ListNode(2);\nListNode* n3 = new ListNode(5);\nListNode* n4 = new ListNode(4);\n// 构建节点之间的引用\nn0->next = n1;\nn1->next = n2;\nn2->next = n3;\nn3->next = n4;\n
linked_list.java
/* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */\n// 初始化各个节点\nListNode n0 = new ListNode(1);\nListNode n1 = new ListNode(3);\nListNode n2 = new ListNode(2);\nListNode n3 = new ListNode(5);\nListNode n4 = new ListNode(4);\n// 构建节点之间的引用\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
linked_list.cs
/* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */\n// 初始化各个节点\nListNode n0 = new(1);\nListNode n1 = new(3);\nListNode n2 = new(2);\nListNode n3 = new(5);\nListNode n4 = new(4);\n// 构建节点之间的引用\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
linked_list.go
/* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */\n// 初始化各个节点\nn0 := NewListNode(1)\nn1 := NewListNode(3)\nn2 := NewListNode(2)\nn3 := NewListNode(5)\nn4 := NewListNode(4)\n// 构建节点之间的引用\nn0.Next = n1\nn1.Next = n2\nn2.Next = n3\nn3.Next = n4\n
linked_list.swift
/* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */\n// 初始化各个节点\nlet n0 = ListNode(x: 1)\nlet n1 = ListNode(x: 3)\nlet n2 = ListNode(x: 2)\nlet n3 = ListNode(x: 5)\nlet n4 = ListNode(x: 4)\n// 构建节点之间的引用\nn0.next = n1\nn1.next = n2\nn2.next = n3\nn3.next = n4\n
linked_list.js
/* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */\n// 初始化各个节点\nconst n0 = new ListNode(1);\nconst n1 = new ListNode(3);\nconst n2 = new ListNode(2);\nconst n3 = new ListNode(5);\nconst n4 = new ListNode(4);\n// 构建节点之间的引用\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
linked_list.ts
/* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */\n// 初始化各个节点\nconst n0 = new ListNode(1);\nconst n1 = new ListNode(3);\nconst n2 = new ListNode(2);\nconst n3 = new ListNode(5);\nconst n4 = new ListNode(4);\n// 构建节点之间的引用\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
linked_list.dart
/* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */\\\n// 初始化各个节点\nListNode n0 = ListNode(1);\nListNode n1 = ListNode(3);\nListNode n2 = ListNode(2);\nListNode n3 = ListNode(5);\nListNode n4 = ListNode(4);\n// 构建节点之间的引用\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
linked_list.rs
/* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */\n// 初始化各个节点\nlet n0 = Rc::new(RefCell::new(ListNode { val: 1, next: None }));\nlet n1 = Rc::new(RefCell::new(ListNode { val: 3, next: None }));\nlet n2 = Rc::new(RefCell::new(ListNode { val: 2, next: None }));\nlet n3 = Rc::new(RefCell::new(ListNode { val: 5, next: None }));\nlet n4 = Rc::new(RefCell::new(ListNode { val: 4, next: None }));\n\n// 构建节点之间的引用\nn0.borrow_mut().next = Some(n1.clone());\nn1.borrow_mut().next = Some(n2.clone());\nn2.borrow_mut().next = Some(n3.clone());\nn3.borrow_mut().next = Some(n4.clone());\n
linked_list.c
/* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */\n// 初始化各个节点\nListNode* n0 = newListNode(1);\nListNode* n1 = newListNode(3);\nListNode* n2 = newListNode(2);\nListNode* n3 = newListNode(5);\nListNode* n4 = newListNode(4);\n// 构建节点之间的引用\nn0->next = n1;\nn1->next = n2;\nn2->next = n3;\nn3->next = n4;\n
linked_list.kt
/* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */\n// 初始化各个节点\nval n0 = ListNode(1)\nval n1 = ListNode(3)\nval n2 = ListNode(2)\nval n3 = ListNode(5)\nval n4 = ListNode(4)\n// 构建节点之间的引用\nn0.next = n1;\nn1.next = n2;\nn2.next = n3;\nn3.next = n4;\n
linked_list.rb
# 初始化链表 1 -> 3 -> 2 -> 5 -> 4\n# 初始化各个节点\nn0 = ListNode.new(1)\nn1 = ListNode.new(3)\nn2 = ListNode.new(2)\nn3 = ListNode.new(5)\nn4 = ListNode.new(4)\n# 构建节点之间的引用\nn0.next = n1\nn1.next = n2\nn2.next = n3\nn3.next = n4\n
可视化运行

全屏观看 >

数组整体是一个变量,比如数组 nums 包含元素 nums[0]nums[1] 等,而链表是由多个独立的节点对象组成的。我们通常将头节点当作链表的代称,比如以上代码中的链表可记作链表 n0

","path":["第 4 章   数组与链表","4.2   链表"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#2","level":3,"title":"2.   插入节点","text":"

在链表中插入节点非常容易。如图 4-6 所示,假设我们想在相邻的两个节点 n0n1 之间插入一个新节点 P ,则只需改变两个节点引用(指针)即可,时间复杂度为 \\(O(1)\\) 。

相比之下,在数组中插入元素的时间复杂度为 \\(O(n)\\) ,在大数据量下的效率较低。

图 4-6   链表插入节点示例

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
def insert(n0: ListNode, P: ListNode):\n    \"\"\"在链表的节点 n0 之后插入节点 P\"\"\"\n    n1 = n0.next\n    P.next = n1\n    n0.next = P\n
linked_list.cpp
/* 在链表的节点 n0 之后插入节点 P */\nvoid insert(ListNode *n0, ListNode *P) {\n    ListNode *n1 = n0->next;\n    P->next = n1;\n    n0->next = P;\n}\n
linked_list.java
/* 在链表的节点 n0 之后插入节点 P */\nvoid insert(ListNode n0, ListNode P) {\n    ListNode n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
linked_list.cs
/* 在链表的节点 n0 之后插入节点 P */\nvoid Insert(ListNode n0, ListNode P) {\n    ListNode? n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
linked_list.go
/* 在链表的节点 n0 之后插入节点 P */\nfunc insertNode(n0 *ListNode, P *ListNode) {\n    n1 := n0.Next\n    P.Next = n1\n    n0.Next = P\n}\n
linked_list.swift
/* 在链表的节点 n0 之后插入节点 P */\nfunc insert(n0: ListNode, P: ListNode) {\n    let n1 = n0.next\n    P.next = n1\n    n0.next = P\n}\n
linked_list.js
/* 在链表的节点 n0 之后插入节点 P */\nfunction insert(n0, P) {\n    const n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
linked_list.ts
/* 在链表的节点 n0 之后插入节点 P */\nfunction insert(n0: ListNode, P: ListNode): void {\n    const n1 = n0.next;\n    P.next = n1;\n    n0.next = P;\n}\n
linked_list.dart
/* 在链表的节点 n0 之后插入节点 P */\nvoid insert(ListNode n0, ListNode P) {\n  ListNode? n1 = n0.next;\n  P.next = n1;\n  n0.next = P;\n}\n
linked_list.rs
/* 在链表的节点 n0 之后插入节点 P */\n#[allow(non_snake_case)]\npub fn insert<T>(n0: &Rc<RefCell<ListNode<T>>>, P: Rc<RefCell<ListNode<T>>>) {\n    let n1 = n0.borrow_mut().next.take();\n    P.borrow_mut().next = n1;\n    n0.borrow_mut().next = Some(P);\n}\n
linked_list.c
/* 在链表的节点 n0 之后插入节点 P */\nvoid insert(ListNode *n0, ListNode *P) {\n    ListNode *n1 = n0->next;\n    P->next = n1;\n    n0->next = P;\n}\n
linked_list.kt
/* 在链表的节点 n0 之后插入节点 P */\nfun insert(n0: ListNode?, p: ListNode?) {\n    val n1 = n0?.next\n    p?.next = n1\n    n0?.next = p\n}\n
linked_list.rb
### 在链表的节点 n0 之后插入节点 _p ###\n# Ruby 的 `p` 是一个内置函数, `P` 是一个常量,所以可以使用 `_p` 代替\ndef insert(n0, _p)\n  n1 = n0.next\n  _p.next = n1\n  n0.next = _p\nend\n
可视化运行

全屏观看 >

","path":["第 4 章   数组与链表","4.2   链表"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#3","level":3,"title":"3.   删除节点","text":"

如图 4-7 所示,在链表中删除节点也非常方便,只需改变一个节点的引用(指针)即可。

请注意,尽管在删除操作完成后节点 P 仍然指向 n1 ,但实际上遍历此链表已经无法访问到 P ,这意味着 P 已经不再属于该链表了。

图 4-7   链表删除节点

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
def remove(n0: ListNode):\n    \"\"\"删除链表的节点 n0 之后的首个节点\"\"\"\n    if not n0.next:\n        return\n    # n0 -> P -> n1\n    P = n0.next\n    n1 = P.next\n    n0.next = n1\n
linked_list.cpp
/* 删除链表的节点 n0 之后的首个节点 */\nvoid remove(ListNode *n0) {\n    if (n0->next == nullptr)\n        return;\n    // n0 -> P -> n1\n    ListNode *P = n0->next;\n    ListNode *n1 = P->next;\n    n0->next = n1;\n    // 释放内存\n    delete P;\n}\n
linked_list.java
/* 删除链表的节点 n0 之后的首个节点 */\nvoid remove(ListNode n0) {\n    if (n0.next == null)\n        return;\n    // n0 -> P -> n1\n    ListNode P = n0.next;\n    ListNode n1 = P.next;\n    n0.next = n1;\n}\n
linked_list.cs
/* 删除链表的节点 n0 之后的首个节点 */\nvoid Remove(ListNode n0) {\n    if (n0.next == null)\n        return;\n    // n0 -> P -> n1\n    ListNode P = n0.next;\n    ListNode? n1 = P.next;\n    n0.next = n1;\n}\n
linked_list.go
/* 删除链表的节点 n0 之后的首个节点 */\nfunc removeItem(n0 *ListNode) {\n    if n0.Next == nil {\n        return\n    }\n    // n0 -> P -> n1\n    P := n0.Next\n    n1 := P.Next\n    n0.Next = n1\n}\n
linked_list.swift
/* 删除链表的节点 n0 之后的首个节点 */\nfunc remove(n0: ListNode) {\n    if n0.next == nil {\n        return\n    }\n    // n0 -> P -> n1\n    let P = n0.next\n    let n1 = P?.next\n    n0.next = n1\n}\n
linked_list.js
/* 删除链表的节点 n0 之后的首个节点 */\nfunction remove(n0) {\n    if (!n0.next) return;\n    // n0 -> P -> n1\n    const P = n0.next;\n    const n1 = P.next;\n    n0.next = n1;\n}\n
linked_list.ts
/* 删除链表的节点 n0 之后的首个节点 */\nfunction remove(n0: ListNode): void {\n    if (!n0.next) {\n        return;\n    }\n    // n0 -> P -> n1\n    const P = n0.next;\n    const n1 = P.next;\n    n0.next = n1;\n}\n
linked_list.dart
/* 删除链表的节点 n0 之后的首个节点 */\nvoid remove(ListNode n0) {\n  if (n0.next == null) return;\n  // n0 -> P -> n1\n  ListNode P = n0.next!;\n  ListNode? n1 = P.next;\n  n0.next = n1;\n}\n
linked_list.rs
/* 删除链表的节点 n0 之后的首个节点 */\n#[allow(non_snake_case)]\npub fn remove<T>(n0: &Rc<RefCell<ListNode<T>>>) {\n    // n0 -> P -> n1\n    let P = n0.borrow_mut().next.take();\n    if let Some(node) = P {\n        let n1 = node.borrow_mut().next.take();\n        n0.borrow_mut().next = n1;\n    }\n}\n
linked_list.c
/* 删除链表的节点 n0 之后的首个节点 */\n// 注意:stdio.h 占用了 remove 关键词\nvoid removeItem(ListNode *n0) {\n    if (!n0->next)\n        return;\n    // n0 -> P -> n1\n    ListNode *P = n0->next;\n    ListNode *n1 = P->next;\n    n0->next = n1;\n    // 释放内存\n    free(P);\n}\n
linked_list.kt
/* 删除链表的节点 n0 之后的首个节点 */\nfun remove(n0: ListNode?) {\n    if (n0?.next == null)\n        return\n    // n0 -> P -> n1\n    val p = n0.next\n    val n1 = p?.next\n    n0.next = n1\n}\n
linked_list.rb
### 删除链表的节点 n0 之后的首个节点 ###\ndef remove(n0)\n  return if n0.next.nil?\n\n  # n0 -> remove_node -> n1\n  remove_node = n0.next\n  n1 = remove_node.next\n  n0.next = n1\nend\n
可视化运行

全屏观看 >

","path":["第 4 章   数组与链表","4.2   链表"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#4","level":3,"title":"4.   访问节点","text":"

在链表中访问节点的效率较低。如上一节所述,我们可以在 \\(O(1)\\) 时间下访问数组中的任意元素。链表则不然,程序需要从头节点出发,逐个向后遍历,直至找到目标节点。也就是说,访问链表的第 \\(i\\) 个节点需要循环 \\(i - 1\\) 轮,时间复杂度为 \\(O(n)\\) 。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
def access(head: ListNode, index: int) -> ListNode | None:\n    \"\"\"访问链表中索引为 index 的节点\"\"\"\n    for _ in range(index):\n        if not head:\n            return None\n        head = head.next\n    return head\n
linked_list.cpp
/* 访问链表中索引为 index 的节点 */\nListNode *access(ListNode *head, int index) {\n    for (int i = 0; i < index; i++) {\n        if (head == nullptr)\n            return nullptr;\n        head = head->next;\n    }\n    return head;\n}\n
linked_list.java
/* 访问链表中索引为 index 的节点 */\nListNode access(ListNode head, int index) {\n    for (int i = 0; i < index; i++) {\n        if (head == null)\n            return null;\n        head = head.next;\n    }\n    return head;\n}\n
linked_list.cs
/* 访问链表中索引为 index 的节点 */\nListNode? Access(ListNode? head, int index) {\n    for (int i = 0; i < index; i++) {\n        if (head == null)\n            return null;\n        head = head.next;\n    }\n    return head;\n}\n
linked_list.go
/* 访问链表中索引为 index 的节点 */\nfunc access(head *ListNode, index int) *ListNode {\n    for i := 0; i < index; i++ {\n        if head == nil {\n            return nil\n        }\n        head = head.Next\n    }\n    return head\n}\n
linked_list.swift
/* 访问链表中索引为 index 的节点 */\nfunc access(head: ListNode, index: Int) -> ListNode? {\n    var head: ListNode? = head\n    for _ in 0 ..< index {\n        if head == nil {\n            return nil\n        }\n        head = head?.next\n    }\n    return head\n}\n
linked_list.js
/* 访问链表中索引为 index 的节点 */\nfunction access(head, index) {\n    for (let i = 0; i < index; i++) {\n        if (!head) {\n            return null;\n        }\n        head = head.next;\n    }\n    return head;\n}\n
linked_list.ts
/* 访问链表中索引为 index 的节点 */\nfunction access(head: ListNode | null, index: number): ListNode | null {\n    for (let i = 0; i < index; i++) {\n        if (!head) {\n            return null;\n        }\n        head = head.next;\n    }\n    return head;\n}\n
linked_list.dart
/* 访问链表中索引为 index 的节点 */\nListNode? access(ListNode? head, int index) {\n  for (var i = 0; i < index; i++) {\n    if (head == null) return null;\n    head = head.next;\n  }\n  return head;\n}\n
linked_list.rs
/* 访问链表中索引为 index 的节点 */\npub fn access<T>(head: Rc<RefCell<ListNode<T>>>, index: i32) -> Option<Rc<RefCell<ListNode<T>>>> {\n    fn dfs<T>(\n        head: Option<&Rc<RefCell<ListNode<T>>>>,\n        index: i32,\n    ) -> Option<Rc<RefCell<ListNode<T>>>> {\n        if index <= 0 {\n            return head.cloned();\n        }\n\n        if let Some(node) = head {\n            dfs(node.borrow().next.as_ref(), index - 1)\n        } else {\n            None\n        }\n    }\n\n    dfs(Some(head).as_ref(), index)\n}\n
linked_list.c
/* 访问链表中索引为 index 的节点 */\nListNode *access(ListNode *head, int index) {\n    for (int i = 0; i < index; i++) {\n        if (head == NULL)\n            return NULL;\n        head = head->next;\n    }\n    return head;\n}\n
linked_list.kt
/* 访问链表中索引为 index 的节点 */\nfun access(head: ListNode?, index: Int): ListNode? {\n    var h = head\n    for (i in 0..<index) {\n        if (h == null)\n            return null\n        h = h.next\n    }\n    return h\n}\n
linked_list.rb
### 访问链表中索引为 index 的节点 ###\ndef access(head, index)\n  for i in 0...index\n    return nil if head.nil?\n    head = head.next\n  end\n\n  head\nend\n
可视化运行

全屏观看 >

","path":["第 4 章   数组与链表","4.2   链表"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#5","level":3,"title":"5.   查找节点","text":"

遍历链表,查找其中值为 target 的节点,输出该节点在链表中的索引。此过程也属于线性查找。代码如下所示:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linked_list.py
def find(head: ListNode, target: int) -> int:\n    \"\"\"在链表中查找值为 target 的首个节点\"\"\"\n    index = 0\n    while head:\n        if head.val == target:\n            return index\n        head = head.next\n        index += 1\n    return -1\n
linked_list.cpp
/* 在链表中查找值为 target 的首个节点 */\nint find(ListNode *head, int target) {\n    int index = 0;\n    while (head != nullptr) {\n        if (head->val == target)\n            return index;\n        head = head->next;\n        index++;\n    }\n    return -1;\n}\n
linked_list.java
/* 在链表中查找值为 target 的首个节点 */\nint find(ListNode head, int target) {\n    int index = 0;\n    while (head != null) {\n        if (head.val == target)\n            return index;\n        head = head.next;\n        index++;\n    }\n    return -1;\n}\n
linked_list.cs
/* 在链表中查找值为 target 的首个节点 */\nint Find(ListNode? head, int target) {\n    int index = 0;\n    while (head != null) {\n        if (head.val == target)\n            return index;\n        head = head.next;\n        index++;\n    }\n    return -1;\n}\n
linked_list.go
/* 在链表中查找值为 target 的首个节点 */\nfunc findNode(head *ListNode, target int) int {\n    index := 0\n    for head != nil {\n        if head.Val == target {\n            return index\n        }\n        head = head.Next\n        index++\n    }\n    return -1\n}\n
linked_list.swift
/* 在链表中查找值为 target 的首个节点 */\nfunc find(head: ListNode, target: Int) -> Int {\n    var head: ListNode? = head\n    var index = 0\n    while head != nil {\n        if head?.val == target {\n            return index\n        }\n        head = head?.next\n        index += 1\n    }\n    return -1\n}\n
linked_list.js
/* 在链表中查找值为 target 的首个节点 */\nfunction find(head, target) {\n    let index = 0;\n    while (head !== null) {\n        if (head.val === target) {\n            return index;\n        }\n        head = head.next;\n        index += 1;\n    }\n    return -1;\n}\n
linked_list.ts
/* 在链表中查找值为 target 的首个节点 */\nfunction find(head: ListNode | null, target: number): number {\n    let index = 0;\n    while (head !== null) {\n        if (head.val === target) {\n            return index;\n        }\n        head = head.next;\n        index += 1;\n    }\n    return -1;\n}\n
linked_list.dart
/* 在链表中查找值为 target 的首个节点 */\nint find(ListNode? head, int target) {\n  int index = 0;\n  while (head != null) {\n    if (head.val == target) {\n      return index;\n    }\n    head = head.next;\n    index++;\n  }\n  return -1;\n}\n
linked_list.rs
/* 在链表中查找值为 target 的首个节点 */\npub fn find<T: PartialEq>(head: Rc<RefCell<ListNode<T>>>, target: T) -> i32 {\n    fn find<T: PartialEq>(head: Option<&Rc<RefCell<ListNode<T>>>>, target: T, idx: i32) -> i32 {\n        if let Some(node) = head {\n            if node.borrow().val == target {\n                return idx;\n            }\n            return find(node.borrow().next.as_ref(), target, idx + 1);\n        } else {\n            -1\n        }\n    }\n\n    find(Some(head).as_ref(), target, 0)\n}\n
linked_list.c
/* 在链表中查找值为 target 的首个节点 */\nint find(ListNode *head, int target) {\n    int index = 0;\n    while (head) {\n        if (head->val == target)\n            return index;\n        head = head->next;\n        index++;\n    }\n    return -1;\n}\n
linked_list.kt
/* 在链表中查找值为 target 的首个节点 */\nfun find(head: ListNode?, target: Int): Int {\n    var index = 0\n    var h = head\n    while (h != null) {\n        if (h._val == target)\n            return index\n        h = h.next\n        index++\n    }\n    return -1\n}\n
linked_list.rb
### 在链表中查找值为 target 的首个节点 ###\ndef find(head, target)\n  index = 0\n  while head\n    return index if head.val == target\n    head = head.next\n    index += 1\n  end\n\n  -1\nend\n
可视化运行

全屏观看 >

","path":["第 4 章   数组与链表","4.2   链表"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#422-vs","level":2,"title":"4.2.2   数组 vs. 链表","text":"

表 4-1 总结了数组和链表的各项特点并对比了操作效率。由于它们采用两种相反的存储策略,因此各种性质和操作效率也呈现对立的特点。

表 4-1   数组与链表的效率对比

数组 链表 存储方式 连续内存空间 分散内存空间 容量扩展 长度不可变 可灵活扩展 内存效率 元素占用内存少、但可能浪费空间 元素占用内存多 访问元素 \\(O(1)\\) \\(O(n)\\) 添加元素 \\(O(n)\\) \\(O(1)\\) 删除元素 \\(O(n)\\) \\(O(1)\\)","path":["第 4 章   数组与链表","4.2   链表"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#423","level":2,"title":"4.2.3   常见链表类型","text":"

如图 4-8 所示,常见的链表类型包括三种。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
class ListNode:\n    \"\"\"双向链表节点类\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val                # 节点值\n        self.next: ListNode | None = None  # 指向后继节点的引用\n        self.prev: ListNode | None = None  # 指向前驱节点的引用\n
/* 双向链表节点结构体 */\nstruct ListNode {\n    int val;         // 节点值\n    ListNode *next;  // 指向后继节点的指针\n    ListNode *prev;  // 指向前驱节点的指针\n    ListNode(int x) : val(x), next(nullptr), prev(nullptr) {}  // 构造函数\n};\n
/* 双向链表节点类 */\nclass ListNode {\n    int val;        // 节点值\n    ListNode next;  // 指向后继节点的引用\n    ListNode prev;  // 指向前驱节点的引用\n    ListNode(int x) { val = x; }  // 构造函数\n}\n
/* 双向链表节点类 */\nclass ListNode(int x) {  // 构造函数\n    int val = x;    // 节点值\n    ListNode next;  // 指向后继节点的引用\n    ListNode prev;  // 指向前驱节点的引用\n}\n
/* 双向链表节点结构体 */\ntype DoublyListNode struct {\n    Val  int             // 节点值\n    Next *DoublyListNode // 指向后继节点的指针\n    Prev *DoublyListNode // 指向前驱节点的指针\n}\n\n// NewDoublyListNode 初始化\nfunc NewDoublyListNode(val int) *DoublyListNode {\n    return &DoublyListNode{\n        Val:  val,\n        Next: nil,\n        Prev: nil,\n    }\n}\n
/* 双向链表节点类 */\nclass ListNode {\n    var val: Int // 节点值\n    var next: ListNode? // 指向后继节点的引用\n    var prev: ListNode? // 指向前驱节点的引用\n\n    init(x: Int) { // 构造函数\n        val = x\n    }\n}\n
/* 双向链表节点类 */\nclass ListNode {\n    constructor(val, next, prev) {\n        this.val = val  ===  undefined ? 0 : val;        // 节点值\n        this.next = next  ===  undefined ? null : next;  // 指向后继节点的引用\n        this.prev = prev  ===  undefined ? null : prev;  // 指向前驱节点的引用\n    }\n}\n
/* 双向链表节点类 */\nclass ListNode {\n    val: number;\n    next: ListNode | null;\n    prev: ListNode | null;\n    constructor(val?: number, next?: ListNode | null, prev?: ListNode | null) {\n        this.val = val  ===  undefined ? 0 : val;        // 节点值\n        this.next = next  ===  undefined ? null : next;  // 指向后继节点的引用\n        this.prev = prev  ===  undefined ? null : prev;  // 指向前驱节点的引用\n    }\n}\n
/* 双向链表节点类 */\nclass ListNode {\n    int val;        // 节点值\n    ListNode? next;  // 指向后继节点的引用\n    ListNode? prev;  // 指向前驱节点的引用\n    ListNode(this.val, [this.next, this.prev]);  // 构造函数\n}\n
use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* 双向链表节点类型 */\n#[derive(Debug)]\nstruct ListNode {\n    val: i32, // 节点值\n    next: Option<Rc<RefCell<ListNode>>>, // 指向后继节点的指针\n    prev: Option<Rc<RefCell<ListNode>>>, // 指向前驱节点的指针\n}\n\n/* 构造函数 */\nimpl ListNode {\n    fn new(val: i32) -> Self {\n        ListNode {\n            val,\n            next: None,\n            prev: None,\n        }\n    }\n}\n
/* 双向链表节点结构体 */\ntypedef struct ListNode {\n    int val;               // 节点值\n    struct ListNode *next; // 指向后继节点的指针\n    struct ListNode *prev; // 指向前驱节点的指针\n} ListNode;\n\n/* 构造函数 */\nListNode *newListNode(int val) {\n    ListNode *node;\n    node = (ListNode *) malloc(sizeof(ListNode));\n    node->val = val;\n    node->next = NULL;\n    node->prev = NULL;\n    return node;\n}\n
/* 双向链表节点类 */\n// 构造方法\nclass ListNode(x: Int) {\n    val _val: Int = x           // 节点值\n    val next: ListNode? = null  // 指向后继节点的引用\n    val prev: ListNode? = null  // 指向前驱节点的引用\n}\n
# 双向链表节点类\nclass ListNode\n  attr_accessor :val    # 节点值\n  attr_accessor :next   # 指向后继节点的引用\n  attr_accessor :prev   # 指向前驱节点的引用\n\n  def initialize(val=0, next_node=nil, prev_node=nil)\n    @val = val\n    @next = next_node\n    @prev = prev_node\n  end\nend\n

图 4-8   常见链表种类

","path":["第 4 章   数组与链表","4.2   链表"],"tags":[]},{"location":"chapter_array_and_linkedlist/linked_list/#424","level":2,"title":"4.2.4   链表典型应用","text":"

单向链表通常用于实现栈、队列、哈希表和图等数据结构。

双向链表常用于需要快速查找前一个和后一个元素的场景。

环形链表常用于需要周期性操作的场景,比如操作系统的资源调度。

","path":["第 4 章   数组与链表","4.2   链表"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/","level":1,"title":"4.3   列表","text":"

列表(list)是一个抽象的数据结构概念,它表示元素的有序集合,支持元素访问、修改、添加、删除和遍历等操作,无须使用者考虑容量限制的问题。列表可以基于链表或数组实现。

当使用数组实现列表时,长度不可变的性质会导致列表的实用性降低。这是因为我们通常无法事先确定需要存储多少数据,从而难以选择合适的列表长度。若长度过小,则很可能无法满足使用需求;若长度过大,则会造成内存空间浪费。

为解决此问题,我们可以使用动态数组(dynamic array)来实现列表。它继承了数组的各项优点,并且可以在程序运行过程中进行动态扩容。

实际上,许多编程语言中的标准库提供的列表是基于动态数组实现的,例如 Python 中的 list 、Java 中的 ArrayList 、C++ 中的 vector 和 C# 中的 List 等。在接下来的讨论中,我们将把“列表”和“动态数组”视为等同的概念。

","path":["第 4 章   数组与链表","4.3   列表"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#431","level":2,"title":"4.3.1   列表常用操作","text":"","path":["第 4 章   数组与链表","4.3   列表"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#1","level":3,"title":"1.   初始化列表","text":"

我们通常使用“无初始值”和“有初始值”这两种初始化方法:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
# 初始化列表\n# 无初始值\nnums1: list[int] = []\n# 有初始值\nnums: list[int] = [1, 3, 2, 5, 4]\n
list.cpp
/* 初始化列表 */\n// 需注意,C++ 中 vector 即是本文描述的 nums\n// 无初始值\nvector<int> nums1;\n// 有初始值\nvector<int> nums = { 1, 3, 2, 5, 4 };\n
list.java
/* 初始化列表 */\n// 无初始值\nList<Integer> nums1 = new ArrayList<>();\n// 有初始值(注意数组的元素类型需为 int[] 的包装类 Integer[])\nInteger[] numbers = new Integer[] { 1, 3, 2, 5, 4 };\nList<Integer> nums = new ArrayList<>(Arrays.asList(numbers));\n
list.cs
/* 初始化列表 */\n// 无初始值\nList<int> nums1 = [];\n// 有初始值\nint[] numbers = [1, 3, 2, 5, 4];\nList<int> nums = [.. numbers];\n
list_test.go
/* 初始化列表 */\n// 无初始值\nnums1 := []int{}\n// 有初始值\nnums := []int{1, 3, 2, 5, 4}\n
list.swift
/* 初始化列表 */\n// 无初始值\nlet nums1: [Int] = []\n// 有初始值\nvar nums = [1, 3, 2, 5, 4]\n
list.js
/* 初始化列表 */\n// 无初始值\nconst nums1 = [];\n// 有初始值\nconst nums = [1, 3, 2, 5, 4];\n
list.ts
/* 初始化列表 */\n// 无初始值\nconst nums1: number[] = [];\n// 有初始值\nconst nums: number[] = [1, 3, 2, 5, 4];\n
list.dart
/* 初始化列表 */\n// 无初始值\nList<int> nums1 = [];\n// 有初始值\nList<int> nums = [1, 3, 2, 5, 4];\n
list.rs
/* 初始化列表 */\n// 无初始值\nlet nums1: Vec<i32> = Vec::new();\n// 有初始值\nlet nums: Vec<i32> = vec![1, 3, 2, 5, 4];\n
list.c
// C 未提供内置动态数组\n
list.kt
/* 初始化列表 */\n// 无初始值\nvar nums1 = listOf<Int>()\n// 有初始值\nvar numbers = arrayOf(1, 3, 2, 5, 4)\nvar nums = numbers.toMutableList()\n
list.rb
# 初始化列表\n# 无初始值\nnums1 = []\n# 有初始值\nnums = [1, 3, 2, 5, 4]\n
可视化运行

全屏观看 >

","path":["第 4 章   数组与链表","4.3   列表"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#2","level":3,"title":"2.   访问元素","text":"

列表本质上是数组,因此可以在 \\(O(1)\\) 时间内访问和更新元素,效率很高。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
# 访问元素\nnum: int = nums[1]  # 访问索引 1 处的元素\n\n# 更新元素\nnums[1] = 0    # 将索引 1 处的元素更新为 0\n
list.cpp
/* 访问元素 */\nint num = nums[1];  // 访问索引 1 处的元素\n\n/* 更新元素 */\nnums[1] = 0;  // 将索引 1 处的元素更新为 0\n
list.java
/* 访问元素 */\nint num = nums.get(1);  // 访问索引 1 处的元素\n\n/* 更新元素 */\nnums.set(1, 0);  // 将索引 1 处的元素更新为 0\n
list.cs
/* 访问元素 */\nint num = nums[1];  // 访问索引 1 处的元素\n\n/* 更新元素 */\nnums[1] = 0;  // 将索引 1 处的元素更新为 0\n
list_test.go
/* 访问元素 */\nnum := nums[1]  // 访问索引 1 处的元素\n\n/* 更新元素 */\nnums[1] = 0     // 将索引 1 处的元素更新为 0\n
list.swift
/* 访问元素 */\nlet num = nums[1] // 访问索引 1 处的元素\n\n/* 更新元素 */\nnums[1] = 0 // 将索引 1 处的元素更新为 0\n
list.js
/* 访问元素 */\nconst num = nums[1];  // 访问索引 1 处的元素\n\n/* 更新元素 */\nnums[1] = 0;  // 将索引 1 处的元素更新为 0\n
list.ts
/* 访问元素 */\nconst num: number = nums[1];  // 访问索引 1 处的元素\n\n/* 更新元素 */\nnums[1] = 0;  // 将索引 1 处的元素更新为 0\n
list.dart
/* 访问元素 */\nint num = nums[1];  // 访问索引 1 处的元素\n\n/* 更新元素 */\nnums[1] = 0;  // 将索引 1 处的元素更新为 0\n
list.rs
/* 访问元素 */\nlet num: i32 = nums[1];  // 访问索引 1 处的元素\n/* 更新元素 */\nnums[1] = 0;             // 将索引 1 处的元素更新为 0\n
list.c
// C 未提供内置动态数组\n
list.kt
/* 访问元素 */\nval num = nums[1]       // 访问索引 1 处的元素\n/* 更新元素 */\nnums[1] = 0             // 将索引 1 处的元素更新为 0\n
list.rb
# 访问元素\nnum = nums[1] # 访问索引 1 处的元素\n# 更新元素\nnums[1] = 0 # 将索引 1 处的元素更新为 0\n
可视化运行

全屏观看 >

","path":["第 4 章   数组与链表","4.3   列表"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#3","level":3,"title":"3.   插入与删除元素","text":"

相较于数组,列表可以自由地添加与删除元素。在列表尾部添加元素的时间复杂度为 \\(O(1)\\) ,但插入和删除元素的效率仍与数组相同,时间复杂度为 \\(O(n)\\) 。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
# 清空列表\nnums.clear()\n\n# 在尾部添加元素\nnums.append(1)\nnums.append(3)\nnums.append(2)\nnums.append(5)\nnums.append(4)\n\n# 在中间插入元素\nnums.insert(3, 6)  # 在索引 3 处插入数字 6\n\n# 删除元素\nnums.pop(3)        # 删除索引 3 处的元素\n
list.cpp
/* 清空列表 */\nnums.clear();\n\n/* 在尾部添加元素 */\nnums.push_back(1);\nnums.push_back(3);\nnums.push_back(2);\nnums.push_back(5);\nnums.push_back(4);\n\n/* 在中间插入元素 */\nnums.insert(nums.begin() + 3, 6);  // 在索引 3 处插入数字 6\n\n/* 删除元素 */\nnums.erase(nums.begin() + 3);      // 删除索引 3 处的元素\n
list.java
/* 清空列表 */\nnums.clear();\n\n/* 在尾部添加元素 */\nnums.add(1);\nnums.add(3);\nnums.add(2);\nnums.add(5);\nnums.add(4);\n\n/* 在中间插入元素 */\nnums.add(3, 6);  // 在索引 3 处插入数字 6\n\n/* 删除元素 */\nnums.remove(3);  // 删除索引 3 处的元素\n
list.cs
/* 清空列表 */\nnums.Clear();\n\n/* 在尾部添加元素 */\nnums.Add(1);\nnums.Add(3);\nnums.Add(2);\nnums.Add(5);\nnums.Add(4);\n\n/* 在中间插入元素 */\nnums.Insert(3, 6);  // 在索引 3 处插入数字 6\n\n/* 删除元素 */\nnums.RemoveAt(3);  // 删除索引 3 处的元素\n
list_test.go
/* 清空列表 */\nnums = nil\n\n/* 在尾部添加元素 */\nnums = append(nums, 1)\nnums = append(nums, 3)\nnums = append(nums, 2)\nnums = append(nums, 5)\nnums = append(nums, 4)\n\n/* 在中间插入元素 */\nnums = append(nums[:3], append([]int{6}, nums[3:]...)...) // 在索引 3 处插入数字 6\n\n/* 删除元素 */\nnums = append(nums[:3], nums[4:]...) // 删除索引 3 处的元素\n
list.swift
/* 清空列表 */\nnums.removeAll()\n\n/* 在尾部添加元素 */\nnums.append(1)\nnums.append(3)\nnums.append(2)\nnums.append(5)\nnums.append(4)\n\n/* 在中间插入元素 */\nnums.insert(6, at: 3) // 在索引 3 处插入数字 6\n\n/* 删除元素 */\nnums.remove(at: 3) // 删除索引 3 处的元素\n
list.js
/* 清空列表 */\nnums.length = 0;\n\n/* 在尾部添加元素 */\nnums.push(1);\nnums.push(3);\nnums.push(2);\nnums.push(5);\nnums.push(4);\n\n/* 在中间插入元素 */\nnums.splice(3, 0, 6); // 在索引 3 处插入数字 6\n\n/* 删除元素 */\nnums.splice(3, 1);  // 删除索引 3 处的元素\n
list.ts
/* 清空列表 */\nnums.length = 0;\n\n/* 在尾部添加元素 */\nnums.push(1);\nnums.push(3);\nnums.push(2);\nnums.push(5);\nnums.push(4);\n\n/* 在中间插入元素 */\nnums.splice(3, 0, 6); // 在索引 3 处插入数字 6\n\n/* 删除元素 */\nnums.splice(3, 1);  // 删除索引 3 处的元素\n
list.dart
/* 清空列表 */\nnums.clear();\n\n/* 在尾部添加元素 */\nnums.add(1);\nnums.add(3);\nnums.add(2);\nnums.add(5);\nnums.add(4);\n\n/* 在中间插入元素 */\nnums.insert(3, 6); // 在索引 3 处插入数字 6\n\n/* 删除元素 */\nnums.removeAt(3); // 删除索引 3 处的元素\n
list.rs
/* 清空列表 */\nnums.clear();\n\n/* 在尾部添加元素 */\nnums.push(1);\nnums.push(3);\nnums.push(2);\nnums.push(5);\nnums.push(4);\n\n/* 在中间插入元素 */\nnums.insert(3, 6);  // 在索引 3 处插入数字 6\n\n/* 删除元素 */\nnums.remove(3);    // 删除索引 3 处的元素\n
list.c
// C 未提供内置动态数组\n
list.kt
/* 清空列表 */\nnums.clear();\n\n/* 在尾部添加元素 */\nnums.add(1);\nnums.add(3);\nnums.add(2);\nnums.add(5);\nnums.add(4);\n\n/* 在中间插入元素 */\nnums.add(3, 6);  // 在索引 3 处插入数字 6\n\n/* 删除元素 */\nnums.remove(3);  // 删除索引 3 处的元素\n
list.rb
# 清空列表\nnums.clear\n\n# 在尾部添加元素\nnums << 1\nnums << 3\nnums << 2\nnums << 5\nnums << 4\n\n# 在中间插入元素\nnums.insert(3, 6) # 在索引 3 处插入数字 6\n\n# 删除元素\nnums.delete_at(3) # 删除索引 3 处的元素\n
可视化运行

全屏观看 >

","path":["第 4 章   数组与链表","4.3   列表"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#4","level":3,"title":"4.   遍历列表","text":"

与数组一样,列表可以根据索引遍历,也可以直接遍历各元素。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
# 通过索引遍历列表\ncount = 0\nfor i in range(len(nums)):\n    count += nums[i]\n\n# 直接遍历列表元素\nfor num in nums:\n    count += num\n
list.cpp
/* 通过索引遍历列表 */\nint count = 0;\nfor (int i = 0; i < nums.size(); i++) {\n    count += nums[i];\n}\n\n/* 直接遍历列表元素 */\ncount = 0;\nfor (int num : nums) {\n    count += num;\n}\n
list.java
/* 通过索引遍历列表 */\nint count = 0;\nfor (int i = 0; i < nums.size(); i++) {\n    count += nums.get(i);\n}\n\n/* 直接遍历列表元素 */\nfor (int num : nums) {\n    count += num;\n}\n
list.cs
/* 通过索引遍历列表 */\nint count = 0;\nfor (int i = 0; i < nums.Count; i++) {\n    count += nums[i];\n}\n\n/* 直接遍历列表元素 */\ncount = 0;\nforeach (int num in nums) {\n    count += num;\n}\n
list_test.go
/* 通过索引遍历列表 */\ncount := 0\nfor i := 0; i < len(nums); i++ {\n    count += nums[i]\n}\n\n/* 直接遍历列表元素 */\ncount = 0\nfor _, num := range nums {\n    count += num\n}\n
list.swift
/* 通过索引遍历列表 */\nvar count = 0\nfor i in nums.indices {\n    count += nums[i]\n}\n\n/* 直接遍历列表元素 */\ncount = 0\nfor num in nums {\n    count += num\n}\n
list.js
/* 通过索引遍历列表 */\nlet count = 0;\nfor (let i = 0; i < nums.length; i++) {\n    count += nums[i];\n}\n\n/* 直接遍历列表元素 */\ncount = 0;\nfor (const num of nums) {\n    count += num;\n}\n
list.ts
/* 通过索引遍历列表 */\nlet count = 0;\nfor (let i = 0; i < nums.length; i++) {\n    count += nums[i];\n}\n\n/* 直接遍历列表元素 */\ncount = 0;\nfor (const num of nums) {\n    count += num;\n}\n
list.dart
/* 通过索引遍历列表 */\nint count = 0;\nfor (var i = 0; i < nums.length; i++) {\n    count += nums[i];\n}\n\n/* 直接遍历列表元素 */\ncount = 0;\nfor (var num in nums) {\n    count += num;\n}\n
list.rs
// 通过索引遍历列表\nlet mut _count = 0;\nfor i in 0..nums.len() {\n    _count += nums[i];\n}\n\n// 直接遍历列表元素\n_count = 0;\nfor num in &nums {\n    _count += num;\n}\n
list.c
// C 未提供内置动态数组\n
list.kt
/* 通过索引遍历列表 */\nvar count = 0\nfor (i in nums.indices) {\n    count += nums[i]\n}\n\n/* 直接遍历列表元素 */\nfor (num in nums) {\n    count += num\n}\n
list.rb
# 通过索引遍历列表\ncount = 0\nfor i in 0...nums.length\n    count += nums[i]\nend\n\n# 直接遍历列表元素\ncount = 0\nfor num in nums\n    count += num\nend\n
可视化运行

全屏观看 >

","path":["第 4 章   数组与链表","4.3   列表"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#5","level":3,"title":"5.   拼接列表","text":"

给定一个新列表 nums1 ,我们可以将其拼接到原列表的尾部。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
# 拼接两个列表\nnums1: list[int] = [6, 8, 7, 10, 9]\nnums += nums1  # 将列表 nums1 拼接到 nums 之后\n
list.cpp
/* 拼接两个列表 */\nvector<int> nums1 = { 6, 8, 7, 10, 9 };\n// 将列表 nums1 拼接到 nums 之后\nnums.insert(nums.end(), nums1.begin(), nums1.end());\n
list.java
/* 拼接两个列表 */\nList<Integer> nums1 = new ArrayList<>(Arrays.asList(new Integer[] { 6, 8, 7, 10, 9 }));\nnums.addAll(nums1);  // 将列表 nums1 拼接到 nums 之后\n
list.cs
/* 拼接两个列表 */\nList<int> nums1 = [6, 8, 7, 10, 9];\nnums.AddRange(nums1);  // 将列表 nums1 拼接到 nums 之后\n
list_test.go
/* 拼接两个列表 */\nnums1 := []int{6, 8, 7, 10, 9}\nnums = append(nums, nums1...)  // 将列表 nums1 拼接到 nums 之后\n
list.swift
/* 拼接两个列表 */\nlet nums1 = [6, 8, 7, 10, 9]\nnums.append(contentsOf: nums1) // 将列表 nums1 拼接到 nums 之后\n
list.js
/* 拼接两个列表 */\nconst nums1 = [6, 8, 7, 10, 9];\nnums.push(...nums1);  // 将列表 nums1 拼接到 nums 之后\n
list.ts
/* 拼接两个列表 */\nconst nums1: number[] = [6, 8, 7, 10, 9];\nnums.push(...nums1);  // 将列表 nums1 拼接到 nums 之后\n
list.dart
/* 拼接两个列表 */\nList<int> nums1 = [6, 8, 7, 10, 9];\nnums.addAll(nums1);  // 将列表 nums1 拼接到 nums 之后\n
list.rs
/* 拼接两个列表 */\nlet nums1: Vec<i32> = vec![6, 8, 7, 10, 9];\nnums.extend(nums1);\n
list.c
// C 未提供内置动态数组\n
list.kt
/* 拼接两个列表 */\nval nums1 = intArrayOf(6, 8, 7, 10, 9).toMutableList()\nnums.addAll(nums1)  // 将列表 nums1 拼接到 nums 之后\n
list.rb
# 拼接两个列表\nnums1 = [6, 8, 7, 10, 9]\nnums += nums1\n
可视化运行

全屏观看 >

","path":["第 4 章   数组与链表","4.3   列表"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#6","level":3,"title":"6.   排序列表","text":"

完成列表排序后,我们便可以使用在数组类算法题中经常考查的“二分查找”和“双指针”算法。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby list.py
# 排序列表\nnums.sort()  # 排序后,列表元素从小到大排列\n
list.cpp
/* 排序列表 */\nsort(nums.begin(), nums.end());  // 排序后,列表元素从小到大排列\n
list.java
/* 排序列表 */\nCollections.sort(nums);  // 排序后,列表元素从小到大排列\n
list.cs
/* 排序列表 */\nnums.Sort(); // 排序后,列表元素从小到大排列\n
list_test.go
/* 排序列表 */\nsort.Ints(nums)  // 排序后,列表元素从小到大排列\n
list.swift
/* 排序列表 */\nnums.sort() // 排序后,列表元素从小到大排列\n
list.js
/* 排序列表 */\nnums.sort((a, b) => a - b);  // 排序后,列表元素从小到大排列\n
list.ts
/* 排序列表 */\nnums.sort((a, b) => a - b);  // 排序后,列表元素从小到大排列\n
list.dart
/* 排序列表 */\nnums.sort(); // 排序后,列表元素从小到大排列\n
list.rs
/* 排序列表 */\nnums.sort(); // 排序后,列表元素从小到大排列\n
list.c
// C 未提供内置动态数组\n
list.kt
/* 排序列表 */\nnums.sort() // 排序后,列表元素从小到大排列\n
list.rb
# 排序列表\nnums = nums.sort { |a, b| a <=> b } # 排序后,列表元素从小到大排列\n
可视化运行

全屏观看 >

","path":["第 4 章   数组与链表","4.3   列表"],"tags":[]},{"location":"chapter_array_and_linkedlist/list/#432","level":2,"title":"4.3.2   列表实现","text":"

许多编程语言内置了列表,例如 Java、C++、Python 等。它们的实现比较复杂,各个参数的设定也非常考究,例如初始容量、扩容倍数等。感兴趣的读者可以查阅源码进行学习。

为了加深对列表工作原理的理解,我们尝试实现一个简易版列表,包括以下三个重点设计。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_list.py
class MyList:\n    \"\"\"列表类\"\"\"\n\n    def __init__(self):\n        \"\"\"构造方法\"\"\"\n        self._capacity: int = 10  # 列表容量\n        self._arr: list[int] = [0] * self._capacity  # 数组(存储列表元素)\n        self._size: int = 0  # 列表长度(当前元素数量)\n        self._extend_ratio: int = 2  # 每次列表扩容的倍数\n\n    def size(self) -> int:\n        \"\"\"获取列表长度(当前元素数量)\"\"\"\n        return self._size\n\n    def capacity(self) -> int:\n        \"\"\"获取列表容量\"\"\"\n        return self._capacity\n\n    def get(self, index: int) -> int:\n        \"\"\"访问元素\"\"\"\n        # 索引如果越界,则抛出异常,下同\n        if index < 0 or index >= self._size:\n            raise IndexError(\"索引越界\")\n        return self._arr[index]\n\n    def set(self, num: int, index: int):\n        \"\"\"更新元素\"\"\"\n        if index < 0 or index >= self._size:\n            raise IndexError(\"索引越界\")\n        self._arr[index] = num\n\n    def add(self, num: int):\n        \"\"\"在尾部添加元素\"\"\"\n        # 元素数量超出容量时,触发扩容机制\n        if self.size() == self.capacity():\n            self.extend_capacity()\n        self._arr[self._size] = num\n        self._size += 1\n\n    def insert(self, num: int, index: int):\n        \"\"\"在中间插入元素\"\"\"\n        if index < 0 or index >= self._size:\n            raise IndexError(\"索引越界\")\n        # 元素数量超出容量时,触发扩容机制\n        if self._size == self.capacity():\n            self.extend_capacity()\n        # 将索引 index 以及之后的元素都向后移动一位\n        for j in range(self._size - 1, index - 1, -1):\n            self._arr[j + 1] = self._arr[j]\n        self._arr[index] = num\n        # 更新元素数量\n        self._size += 1\n\n    def remove(self, index: int) -> int:\n        \"\"\"删除元素\"\"\"\n        if index < 0 or index >= self._size:\n            raise IndexError(\"索引越界\")\n        num = self._arr[index]\n        # 将索引 index 之后的元素都向前移动一位\n        for j in range(index, self._size - 1):\n            self._arr[j] = self._arr[j + 1]\n        # 更新元素数量\n        self._size -= 1\n        # 返回被删除的元素\n        return num\n\n    def extend_capacity(self):\n        \"\"\"列表扩容\"\"\"\n        # 新建一个长度为原数组 _extend_ratio 倍的新数组,并将原数组复制到新数组\n        self._arr = self._arr + [0] * self.capacity() * (self._extend_ratio - 1)\n        # 更新列表容量\n        self._capacity = len(self._arr)\n\n    def to_array(self) -> list[int]:\n        \"\"\"返回有效长度的列表\"\"\"\n        return self._arr[: self._size]\n
my_list.cpp
/* 列表类 */\nclass MyList {\n  private:\n    int *arr;             // 数组(存储列表元素)\n    int arrCapacity = 10; // 列表容量\n    int arrSize = 0;      // 列表长度(当前元素数量)\n    int extendRatio = 2;   // 每次列表扩容的倍数\n\n  public:\n    /* 构造方法 */\n    MyList() {\n        arr = new int[arrCapacity];\n    }\n\n    /* 析构方法 */\n    ~MyList() {\n        delete[] arr;\n    }\n\n    /* 获取列表长度(当前元素数量)*/\n    int size() {\n        return arrSize;\n    }\n\n    /* 获取列表容量 */\n    int capacity() {\n        return arrCapacity;\n    }\n\n    /* 访问元素 */\n    int get(int index) {\n        // 索引如果越界,则抛出异常,下同\n        if (index < 0 || index >= size())\n            throw out_of_range(\"索引越界\");\n        return arr[index];\n    }\n\n    /* 更新元素 */\n    void set(int index, int num) {\n        if (index < 0 || index >= size())\n            throw out_of_range(\"索引越界\");\n        arr[index] = num;\n    }\n\n    /* 在尾部添加元素 */\n    void add(int num) {\n        // 元素数量超出容量时,触发扩容机制\n        if (size() == capacity())\n            extendCapacity();\n        arr[size()] = num;\n        // 更新元素数量\n        arrSize++;\n    }\n\n    /* 在中间插入元素 */\n    void insert(int index, int num) {\n        if (index < 0 || index >= size())\n            throw out_of_range(\"索引越界\");\n        // 元素数量超出容量时,触发扩容机制\n        if (size() == capacity())\n            extendCapacity();\n        // 将索引 index 以及之后的元素都向后移动一位\n        for (int j = size() - 1; j >= index; j--) {\n            arr[j + 1] = arr[j];\n        }\n        arr[index] = num;\n        // 更新元素数量\n        arrSize++;\n    }\n\n    /* 删除元素 */\n    int remove(int index) {\n        if (index < 0 || index >= size())\n            throw out_of_range(\"索引越界\");\n        int num = arr[index];\n        // 将索引 index 之后的元素都向前移动一位\n        for (int j = index; j < size() - 1; j++) {\n            arr[j] = arr[j + 1];\n        }\n        // 更新元素数量\n        arrSize--;\n        // 返回被删除的元素\n        return num;\n    }\n\n    /* 列表扩容 */\n    void extendCapacity() {\n        // 新建一个长度为原数组 extendRatio 倍的新数组\n        int newCapacity = capacity() * extendRatio;\n        int *tmp = arr;\n        arr = new int[newCapacity];\n        // 将原数组中的所有元素复制到新数组\n        for (int i = 0; i < size(); i++) {\n            arr[i] = tmp[i];\n        }\n        // 释放内存\n        delete[] tmp;\n        arrCapacity = newCapacity;\n    }\n\n    /* 将列表转换为 Vector 用于打印 */\n    vector<int> toVector() {\n        // 仅转换有效长度范围内的列表元素\n        vector<int> vec(size());\n        for (int i = 0; i < size(); i++) {\n            vec[i] = arr[i];\n        }\n        return vec;\n    }\n};\n
my_list.java
/* 列表类 */\nclass MyList {\n    private int[] arr; // 数组(存储列表元素)\n    private int capacity = 10; // 列表容量\n    private int size = 0; // 列表长度(当前元素数量)\n    private int extendRatio = 2; // 每次列表扩容的倍数\n\n    /* 构造方法 */\n    public MyList() {\n        arr = new int[capacity];\n    }\n\n    /* 获取列表长度(当前元素数量) */\n    public int size() {\n        return size;\n    }\n\n    /* 获取列表容量 */\n    public int capacity() {\n        return capacity;\n    }\n\n    /* 访问元素 */\n    public int get(int index) {\n        // 索引如果越界,则抛出异常,下同\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"索引越界\");\n        return arr[index];\n    }\n\n    /* 更新元素 */\n    public void set(int index, int num) {\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"索引越界\");\n        arr[index] = num;\n    }\n\n    /* 在尾部添加元素 */\n    public void add(int num) {\n        // 元素数量超出容量时,触发扩容机制\n        if (size == capacity())\n            extendCapacity();\n        arr[size] = num;\n        // 更新元素数量\n        size++;\n    }\n\n    /* 在中间插入元素 */\n    public void insert(int index, int num) {\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"索引越界\");\n        // 元素数量超出容量时,触发扩容机制\n        if (size == capacity())\n            extendCapacity();\n        // 将索引 index 以及之后的元素都向后移动一位\n        for (int j = size - 1; j >= index; j--) {\n            arr[j + 1] = arr[j];\n        }\n        arr[index] = num;\n        // 更新元素数量\n        size++;\n    }\n\n    /* 删除元素 */\n    public int remove(int index) {\n        if (index < 0 || index >= size)\n            throw new IndexOutOfBoundsException(\"索引越界\");\n        int num = arr[index];\n        // 将将索引 index 之后的元素都向前移动一位\n        for (int j = index; j < size - 1; j++) {\n            arr[j] = arr[j + 1];\n        }\n        // 更新元素数量\n        size--;\n        // 返回被删除的元素\n        return num;\n    }\n\n    /* 列表扩容 */\n    public void extendCapacity() {\n        // 新建一个长度为原数组 extendRatio 倍的新数组,并将原数组复制到新数组\n        arr = Arrays.copyOf(arr, capacity() * extendRatio);\n        // 更新列表容量\n        capacity = arr.length;\n    }\n\n    /* 将列表转换为数组 */\n    public int[] toArray() {\n        int size = size();\n        // 仅转换有效长度范围内的列表元素\n        int[] arr = new int[size];\n        for (int i = 0; i < size; i++) {\n            arr[i] = get(i);\n        }\n        return arr;\n    }\n}\n
my_list.cs
/* 列表类 */\nclass MyList {\n    private int[] arr;           // 数组(存储列表元素)\n    private int arrCapacity = 10;    // 列表容量\n    private int arrSize = 0;         // 列表长度(当前元素数量)\n    private readonly int extendRatio = 2;  // 每次列表扩容的倍数\n\n    /* 构造方法 */\n    public MyList() {\n        arr = new int[arrCapacity];\n    }\n\n    /* 获取列表长度(当前元素数量)*/\n    public int Size() {\n        return arrSize;\n    }\n\n    /* 获取列表容量 */\n    public int Capacity() {\n        return arrCapacity;\n    }\n\n    /* 访问元素 */\n    public int Get(int index) {\n        // 索引如果越界,则抛出异常,下同\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"索引越界\");\n        return arr[index];\n    }\n\n    /* 更新元素 */\n    public void Set(int index, int num) {\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"索引越界\");\n        arr[index] = num;\n    }\n\n    /* 在尾部添加元素 */\n    public void Add(int num) {\n        // 元素数量超出容量时,触发扩容机制\n        if (arrSize == arrCapacity)\n            ExtendCapacity();\n        arr[arrSize] = num;\n        // 更新元素数量\n        arrSize++;\n    }\n\n    /* 在中间插入元素 */\n    public void Insert(int index, int num) {\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"索引越界\");\n        // 元素数量超出容量时,触发扩容机制\n        if (arrSize == arrCapacity)\n            ExtendCapacity();\n        // 将索引 index 以及之后的元素都向后移动一位\n        for (int j = arrSize - 1; j >= index; j--) {\n            arr[j + 1] = arr[j];\n        }\n        arr[index] = num;\n        // 更新元素数量\n        arrSize++;\n    }\n\n    /* 删除元素 */\n    public int Remove(int index) {\n        if (index < 0 || index >= arrSize)\n            throw new IndexOutOfRangeException(\"索引越界\");\n        int num = arr[index];\n        // 将将索引 index 之后的元素都向前移动一位\n        for (int j = index; j < arrSize - 1; j++) {\n            arr[j] = arr[j + 1];\n        }\n        // 更新元素数量\n        arrSize--;\n        // 返回被删除的元素\n        return num;\n    }\n\n    /* 列表扩容 */\n    public void ExtendCapacity() {\n        // 新建一个长度为 arrCapacity * extendRatio 的数组,并将原数组复制到新数组\n        Array.Resize(ref arr, arrCapacity * extendRatio);\n        // 更新列表容量\n        arrCapacity = arr.Length;\n    }\n\n    /* 将列表转换为数组 */\n    public int[] ToArray() {\n        // 仅转换有效长度范围内的列表元素\n        int[] arr = new int[arrSize];\n        for (int i = 0; i < arrSize; i++) {\n            arr[i] = Get(i);\n        }\n        return arr;\n    }\n}\n
my_list.go
/* 列表类 */\ntype myList struct {\n    arrCapacity int\n    arr         []int\n    arrSize     int\n    extendRatio int\n}\n\n/* 构造函数 */\nfunc newMyList() *myList {\n    return &myList{\n        arrCapacity: 10,              // 列表容量\n        arr:         make([]int, 10), // 数组(存储列表元素)\n        arrSize:     0,               // 列表长度(当前元素数量)\n        extendRatio: 2,               // 每次列表扩容的倍数\n    }\n}\n\n/* 获取列表长度(当前元素数量) */\nfunc (l *myList) size() int {\n    return l.arrSize\n}\n\n/*  获取列表容量 */\nfunc (l *myList) capacity() int {\n    return l.arrCapacity\n}\n\n/* 访问元素 */\nfunc (l *myList) get(index int) int {\n    // 索引如果越界,则抛出异常,下同\n    if index < 0 || index >= l.arrSize {\n        panic(\"索引越界\")\n    }\n    return l.arr[index]\n}\n\n/* 更新元素 */\nfunc (l *myList) set(num, index int) {\n    if index < 0 || index >= l.arrSize {\n        panic(\"索引越界\")\n    }\n    l.arr[index] = num\n}\n\n/* 在尾部添加元素 */\nfunc (l *myList) add(num int) {\n    // 元素数量超出容量时,触发扩容机制\n    if l.arrSize == l.arrCapacity {\n        l.extendCapacity()\n    }\n    l.arr[l.arrSize] = num\n    // 更新元素数量\n    l.arrSize++\n}\n\n/* 在中间插入元素 */\nfunc (l *myList) insert(num, index int) {\n    if index < 0 || index >= l.arrSize {\n        panic(\"索引越界\")\n    }\n    // 元素数量超出容量时,触发扩容机制\n    if l.arrSize == l.arrCapacity {\n        l.extendCapacity()\n    }\n    // 将索引 index 以及之后的元素都向后移动一位\n    for j := l.arrSize - 1; j >= index; j-- {\n        l.arr[j+1] = l.arr[j]\n    }\n    l.arr[index] = num\n    // 更新元素数量\n    l.arrSize++\n}\n\n/* 删除元素 */\nfunc (l *myList) remove(index int) int {\n    if index < 0 || index >= l.arrSize {\n        panic(\"索引越界\")\n    }\n    num := l.arr[index]\n    // 将索引 index 之后的元素都向前移动一位\n    for j := index; j < l.arrSize-1; j++ {\n        l.arr[j] = l.arr[j+1]\n    }\n    // 更新元素数量\n    l.arrSize--\n    // 返回被删除的元素\n    return num\n}\n\n/* 列表扩容 */\nfunc (l *myList) extendCapacity() {\n    // 新建一个长度为原数组 extendRatio 倍的新数组,并将原数组复制到新数组\n    l.arr = append(l.arr, make([]int, l.arrCapacity*(l.extendRatio-1))...)\n    // 更新列表容量\n    l.arrCapacity = len(l.arr)\n}\n\n/* 返回有效长度的列表 */\nfunc (l *myList) toArray() []int {\n    // 仅转换有效长度范围内的列表元素\n    return l.arr[:l.arrSize]\n}\n
my_list.swift
/* 列表类 */\nclass MyList {\n    private var arr: [Int] // 数组(存储列表元素)\n    private var _capacity: Int // 列表容量\n    private var _size: Int // 列表长度(当前元素数量)\n    private let extendRatio: Int // 每次列表扩容的倍数\n\n    /* 构造方法 */\n    init() {\n        _capacity = 10\n        _size = 0\n        extendRatio = 2\n        arr = Array(repeating: 0, count: _capacity)\n    }\n\n    /* 获取列表长度(当前元素数量)*/\n    func size() -> Int {\n        _size\n    }\n\n    /* 获取列表容量 */\n    func capacity() -> Int {\n        _capacity\n    }\n\n    /* 访问元素 */\n    func get(index: Int) -> Int {\n        // 索引如果越界则抛出错误,下同\n        if index < 0 || index >= size() {\n            fatalError(\"索引越界\")\n        }\n        return arr[index]\n    }\n\n    /* 更新元素 */\n    func set(index: Int, num: Int) {\n        if index < 0 || index >= size() {\n            fatalError(\"索引越界\")\n        }\n        arr[index] = num\n    }\n\n    /* 在尾部添加元素 */\n    func add(num: Int) {\n        // 元素数量超出容量时,触发扩容机制\n        if size() == capacity() {\n            extendCapacity()\n        }\n        arr[size()] = num\n        // 更新元素数量\n        _size += 1\n    }\n\n    /* 在中间插入元素 */\n    func insert(index: Int, num: Int) {\n        if index < 0 || index >= size() {\n            fatalError(\"索引越界\")\n        }\n        // 元素数量超出容量时,触发扩容机制\n        if size() == capacity() {\n            extendCapacity()\n        }\n        // 将索引 index 以及之后的元素都向后移动一位\n        for j in (index ..< size()).reversed() {\n            arr[j + 1] = arr[j]\n        }\n        arr[index] = num\n        // 更新元素数量\n        _size += 1\n    }\n\n    /* 删除元素 */\n    @discardableResult\n    func remove(index: Int) -> Int {\n        if index < 0 || index >= size() {\n            fatalError(\"索引越界\")\n        }\n        let num = arr[index]\n        // 将将索引 index 之后的元素都向前移动一位\n        for j in index ..< (size() - 1) {\n            arr[j] = arr[j + 1]\n        }\n        // 更新元素数量\n        _size -= 1\n        // 返回被删除的元素\n        return num\n    }\n\n    /* 列表扩容 */\n    func extendCapacity() {\n        // 新建一个长度为原数组 extendRatio 倍的新数组,并将原数组复制到新数组\n        arr = arr + Array(repeating: 0, count: capacity() * (extendRatio - 1))\n        // 更新列表容量\n        _capacity = arr.count\n    }\n\n    /* 将列表转换为数组 */\n    func toArray() -> [Int] {\n        Array(arr.prefix(size()))\n    }\n}\n
my_list.js
/* 列表类 */\nclass MyList {\n    #arr = new Array(); // 数组(存储列表元素)\n    #capacity = 10; // 列表容量\n    #size = 0; // 列表长度(当前元素数量)\n    #extendRatio = 2; // 每次列表扩容的倍数\n\n    /* 构造方法 */\n    constructor() {\n        this.#arr = new Array(this.#capacity);\n    }\n\n    /* 获取列表长度(当前元素数量)*/\n    size() {\n        return this.#size;\n    }\n\n    /* 获取列表容量 */\n    capacity() {\n        return this.#capacity;\n    }\n\n    /* 访问元素 */\n    get(index) {\n        // 索引如果越界,则抛出异常,下同\n        if (index < 0 || index >= this.#size) throw new Error('索引越界');\n        return this.#arr[index];\n    }\n\n    /* 更新元素 */\n    set(index, num) {\n        if (index < 0 || index >= this.#size) throw new Error('索引越界');\n        this.#arr[index] = num;\n    }\n\n    /* 在尾部添加元素 */\n    add(num) {\n        // 如果长度等于容量,则需要扩容\n        if (this.#size === this.#capacity) {\n            this.extendCapacity();\n        }\n        // 将新元素添加到列表尾部\n        this.#arr[this.#size] = num;\n        this.#size++;\n    }\n\n    /* 在中间插入元素 */\n    insert(index, num) {\n        if (index < 0 || index >= this.#size) throw new Error('索引越界');\n        // 元素数量超出容量时,触发扩容机制\n        if (this.#size === this.#capacity) {\n            this.extendCapacity();\n        }\n        // 将索引 index 以及之后的元素都向后移动一位\n        for (let j = this.#size - 1; j >= index; j--) {\n            this.#arr[j + 1] = this.#arr[j];\n        }\n        // 更新元素数量\n        this.#arr[index] = num;\n        this.#size++;\n    }\n\n    /* 删除元素 */\n    remove(index) {\n        if (index < 0 || index >= this.#size) throw new Error('索引越界');\n        let num = this.#arr[index];\n        // 将索引 index 之后的元素都向前移动一位\n        for (let j = index; j < this.#size - 1; j++) {\n            this.#arr[j] = this.#arr[j + 1];\n        }\n        // 更新元素数量\n        this.#size--;\n        // 返回被删除的元素\n        return num;\n    }\n\n    /* 列表扩容 */\n    extendCapacity() {\n        // 新建一个长度为原数组 extendRatio 倍的新数组,并将原数组复制到新数组\n        this.#arr = this.#arr.concat(\n            new Array(this.capacity() * (this.#extendRatio - 1))\n        );\n        // 更新列表容量\n        this.#capacity = this.#arr.length;\n    }\n\n    /* 将列表转换为数组 */\n    toArray() {\n        let size = this.size();\n        // 仅转换有效长度范围内的列表元素\n        const arr = new Array(size);\n        for (let i = 0; i < size; i++) {\n            arr[i] = this.get(i);\n        }\n        return arr;\n    }\n}\n
my_list.ts
/* 列表类 */\nclass MyList {\n    private arr: Array<number>; // 数组(存储列表元素)\n    private _capacity: number = 10; // 列表容量\n    private _size: number = 0; // 列表长度(当前元素数量)\n    private extendRatio: number = 2; // 每次列表扩容的倍数\n\n    /* 构造方法 */\n    constructor() {\n        this.arr = new Array(this._capacity);\n    }\n\n    /* 获取列表长度(当前元素数量)*/\n    public size(): number {\n        return this._size;\n    }\n\n    /* 获取列表容量 */\n    public capacity(): number {\n        return this._capacity;\n    }\n\n    /* 访问元素 */\n    public get(index: number): number {\n        // 索引如果越界,则抛出异常,下同\n        if (index < 0 || index >= this._size) throw new Error('索引越界');\n        return this.arr[index];\n    }\n\n    /* 更新元素 */\n    public set(index: number, num: number): void {\n        if (index < 0 || index >= this._size) throw new Error('索引越界');\n        this.arr[index] = num;\n    }\n\n    /* 在尾部添加元素 */\n    public add(num: number): void {\n        // 如果长度等于容量,则需要扩容\n        if (this._size === this._capacity) this.extendCapacity();\n        // 将新元素添加到列表尾部\n        this.arr[this._size] = num;\n        this._size++;\n    }\n\n    /* 在中间插入元素 */\n    public insert(index: number, num: number): void {\n        if (index < 0 || index >= this._size) throw new Error('索引越界');\n        // 元素数量超出容量时,触发扩容机制\n        if (this._size === this._capacity) {\n            this.extendCapacity();\n        }\n        // 将索引 index 以及之后的元素都向后移动一位\n        for (let j = this._size - 1; j >= index; j--) {\n            this.arr[j + 1] = this.arr[j];\n        }\n        // 更新元素数量\n        this.arr[index] = num;\n        this._size++;\n    }\n\n    /* 删除元素 */\n    public remove(index: number): number {\n        if (index < 0 || index >= this._size) throw new Error('索引越界');\n        let num = this.arr[index];\n        // 将将索引 index 之后的元素都向前移动一位\n        for (let j = index; j < this._size - 1; j++) {\n            this.arr[j] = this.arr[j + 1];\n        }\n        // 更新元素数量\n        this._size--;\n        // 返回被删除的元素\n        return num;\n    }\n\n    /* 列表扩容 */\n    public extendCapacity(): void {\n        // 新建一个长度为 size 的数组,并将原数组复制到新数组\n        this.arr = this.arr.concat(\n            new Array(this.capacity() * (this.extendRatio - 1))\n        );\n        // 更新列表容量\n        this._capacity = this.arr.length;\n    }\n\n    /* 将列表转换为数组 */\n    public toArray(): number[] {\n        let size = this.size();\n        // 仅转换有效长度范围内的列表元素\n        const arr = new Array(size);\n        for (let i = 0; i < size; i++) {\n            arr[i] = this.get(i);\n        }\n        return arr;\n    }\n}\n
my_list.dart
/* 列表类 */\nclass MyList {\n  late List<int> _arr; // 数组(存储列表元素)\n  int _capacity = 10; // 列表容量\n  int _size = 0; // 列表长度(当前元素数量)\n  int _extendRatio = 2; // 每次列表扩容的倍数\n\n  /* 构造方法 */\n  MyList() {\n    _arr = List.filled(_capacity, 0);\n  }\n\n  /* 获取列表长度(当前元素数量)*/\n  int size() => _size;\n\n  /* 获取列表容量 */\n  int capacity() => _capacity;\n\n  /* 访问元素 */\n  int get(int index) {\n    if (index >= _size) throw RangeError('索引越界');\n    return _arr[index];\n  }\n\n  /* 更新元素 */\n  void set(int index, int _num) {\n    if (index >= _size) throw RangeError('索引越界');\n    _arr[index] = _num;\n  }\n\n  /* 在尾部添加元素 */\n  void add(int _num) {\n    // 元素数量超出容量时,触发扩容机制\n    if (_size == _capacity) extendCapacity();\n    _arr[_size] = _num;\n    // 更新元素数量\n    _size++;\n  }\n\n  /* 在中间插入元素 */\n  void insert(int index, int _num) {\n    if (index >= _size) throw RangeError('索引越界');\n    // 元素数量超出容量时,触发扩容机制\n    if (_size == _capacity) extendCapacity();\n    // 将索引 index 以及之后的元素都向后移动一位\n    for (var j = _size - 1; j >= index; j--) {\n      _arr[j + 1] = _arr[j];\n    }\n    _arr[index] = _num;\n    // 更新元素数量\n    _size++;\n  }\n\n  /* 删除元素 */\n  int remove(int index) {\n    if (index >= _size) throw RangeError('索引越界');\n    int _num = _arr[index];\n    // 将将索引 index 之后的元素都向前移动一位\n    for (var j = index; j < _size - 1; j++) {\n      _arr[j] = _arr[j + 1];\n    }\n    // 更新元素数量\n    _size--;\n    // 返回被删除的元素\n    return _num;\n  }\n\n  /* 列表扩容 */\n  void extendCapacity() {\n    // 新建一个长度为原数组 _extendRatio 倍的新数组\n    final _newNums = List.filled(_capacity * _extendRatio, 0);\n    // 将原数组复制到新数组\n    List.copyRange(_newNums, 0, _arr);\n    // 更新 _arr 的引用\n    _arr = _newNums;\n    // 更新列表容量\n    _capacity = _arr.length;\n  }\n\n  /* 将列表转换为数组 */\n  List<int> toArray() {\n    List<int> arr = [];\n    for (var i = 0; i < _size; i++) {\n      arr.add(get(i));\n    }\n    return arr;\n  }\n}\n
my_list.rs
/* 列表类 */\n#[allow(dead_code)]\nstruct MyList {\n    arr: Vec<i32>,       // 数组(存储列表元素)\n    capacity: usize,     // 列表容量\n    size: usize,         // 列表长度(当前元素数量)\n    extend_ratio: usize, // 每次列表扩容的倍数\n}\n\n#[allow(unused, unused_comparisons)]\nimpl MyList {\n    /* 构造方法 */\n    pub fn new(capacity: usize) -> Self {\n        let mut vec = vec![0; capacity];\n        Self {\n            arr: vec,\n            capacity,\n            size: 0,\n            extend_ratio: 2,\n        }\n    }\n\n    /* 获取列表长度(当前元素数量)*/\n    pub fn size(&self) -> usize {\n        return self.size;\n    }\n\n    /* 获取列表容量 */\n    pub fn capacity(&self) -> usize {\n        return self.capacity;\n    }\n\n    /* 访问元素 */\n    pub fn get(&self, index: usize) -> i32 {\n        // 索引如果越界,则抛出异常,下同\n        if index >= self.size {\n            panic!(\"索引越界\")\n        };\n        return self.arr[index];\n    }\n\n    /* 更新元素 */\n    pub fn set(&mut self, index: usize, num: i32) {\n        if index >= self.size {\n            panic!(\"索引越界\")\n        };\n        self.arr[index] = num;\n    }\n\n    /* 在尾部添加元素 */\n    pub fn add(&mut self, num: i32) {\n        // 元素数量超出容量时,触发扩容机制\n        if self.size == self.capacity() {\n            self.extend_capacity();\n        }\n        self.arr[self.size] = num;\n        // 更新元素数量\n        self.size += 1;\n    }\n\n    /* 在中间插入元素 */\n    pub fn insert(&mut self, index: usize, num: i32) {\n        if index >= self.size() {\n            panic!(\"索引越界\")\n        };\n        // 元素数量超出容量时,触发扩容机制\n        if self.size == self.capacity() {\n            self.extend_capacity();\n        }\n        // 将索引 index 以及之后的元素都向后移动一位\n        for j in (index..self.size).rev() {\n            self.arr[j + 1] = self.arr[j];\n        }\n        self.arr[index] = num;\n        // 更新元素数量\n        self.size += 1;\n    }\n\n    /* 删除元素 */\n    pub fn remove(&mut self, index: usize) -> i32 {\n        if index >= self.size() {\n            panic!(\"索引越界\")\n        };\n        let num = self.arr[index];\n        // 将索引 index 之后的元素都向前移动一位\n        for j in index..self.size - 1 {\n            self.arr[j] = self.arr[j + 1];\n        }\n        // 更新元素数量\n        self.size -= 1;\n        // 返回被删除的元素\n        return num;\n    }\n\n    /* 列表扩容 */\n    pub fn extend_capacity(&mut self) {\n        // 新建一个长度为原数组 extend_ratio 倍的新数组,并将原数组复制到新数组\n        let new_capacity = self.capacity * self.extend_ratio;\n        self.arr.resize(new_capacity, 0);\n        // 更新列表容量\n        self.capacity = new_capacity;\n    }\n\n    /* 将列表转换为数组 */\n    pub fn to_array(&self) -> Vec<i32> {\n        // 仅转换有效长度范围内的列表元素\n        let mut arr = Vec::new();\n        for i in 0..self.size {\n            arr.push(self.get(i));\n        }\n        arr\n    }\n}\n
my_list.c
/* 列表类 */\ntypedef struct {\n    int *arr;        // 数组(存储列表元素)\n    int capacity;    // 列表容量\n    int size;        // 列表大小\n    int extendRatio; // 列表每次扩容的倍数\n} MyList;\n\n/* 构造函数 */\nMyList *newMyList() {\n    MyList *nums = malloc(sizeof(MyList));\n    nums->capacity = 10;\n    nums->arr = malloc(sizeof(int) * nums->capacity);\n    nums->size = 0;\n    nums->extendRatio = 2;\n    return nums;\n}\n\n/* 析构函数 */\nvoid delMyList(MyList *nums) {\n    free(nums->arr);\n    free(nums);\n}\n\n/* 获取列表长度 */\nint size(MyList *nums) {\n    return nums->size;\n}\n\n/* 获取列表容量 */\nint capacity(MyList *nums) {\n    return nums->capacity;\n}\n\n/* 访问元素 */\nint get(MyList *nums, int index) {\n    assert(index >= 0 && index < nums->size);\n    return nums->arr[index];\n}\n\n/* 更新元素 */\nvoid set(MyList *nums, int index, int num) {\n    assert(index >= 0 && index < nums->size);\n    nums->arr[index] = num;\n}\n\n/* 在尾部添加元素 */\nvoid add(MyList *nums, int num) {\n    if (size(nums) == capacity(nums)) {\n        extendCapacity(nums); // 扩容\n    }\n    nums->arr[size(nums)] = num;\n    nums->size++;\n}\n\n/* 在中间插入元素 */\nvoid insert(MyList *nums, int index, int num) {\n    assert(index >= 0 && index < size(nums));\n    // 元素数量超出容量时,触发扩容机制\n    if (size(nums) == capacity(nums)) {\n        extendCapacity(nums); // 扩容\n    }\n    for (int i = size(nums); i > index; --i) {\n        nums->arr[i] = nums->arr[i - 1];\n    }\n    nums->arr[index] = num;\n    nums->size++;\n}\n\n/* 删除元素 */\n// 注意:stdio.h 占用了 remove 关键词\nint removeItem(MyList *nums, int index) {\n    assert(index >= 0 && index < size(nums));\n    int num = nums->arr[index];\n    for (int i = index; i < size(nums) - 1; i++) {\n        nums->arr[i] = nums->arr[i + 1];\n    }\n    nums->size--;\n    return num;\n}\n\n/* 列表扩容 */\nvoid extendCapacity(MyList *nums) {\n    // 先分配空间\n    int newCapacity = capacity(nums) * nums->extendRatio;\n    int *extend = (int *)malloc(sizeof(int) * newCapacity);\n    int *temp = nums->arr;\n\n    // 拷贝旧数据到新数据\n    for (int i = 0; i < size(nums); i++)\n        extend[i] = nums->arr[i];\n\n    // 释放旧数据\n    free(temp);\n\n    // 更新新数据\n    nums->arr = extend;\n    nums->capacity = newCapacity;\n}\n\n/* 将列表转换为 Array 用于打印 */\nint *toArray(MyList *nums) {\n    return nums->arr;\n}\n
my_list.kt
/* 列表类 */\nclass MyList {\n    private var arr: IntArray = intArrayOf() // 数组(存储列表元素)\n    private var capacity: Int = 10 // 列表容量\n    private var size: Int = 0 // 列表长度(当前元素数量)\n    private var extendRatio: Int = 2 // 每次列表扩容的倍数\n\n    /* 构造方法 */\n    init {\n        arr = IntArray(capacity)\n    }\n\n    /* 获取列表长度(当前元素数量) */\n    fun size(): Int {\n        return size\n    }\n\n    /* 获取列表容量 */\n    fun capacity(): Int {\n        return capacity\n    }\n\n    /* 访问元素 */\n    fun get(index: Int): Int {\n        // 索引如果越界,则抛出异常,下同\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"索引越界\")\n        return arr[index]\n    }\n\n    /* 更新元素 */\n    fun set(index: Int, num: Int) {\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"索引越界\")\n        arr[index] = num\n    }\n\n    /* 在尾部添加元素 */\n    fun add(num: Int) {\n        // 元素数量超出容量时,触发扩容机制\n        if (size == capacity())\n            extendCapacity()\n        arr[size] = num\n        // 更新元素数量\n        size++\n    }\n\n    /* 在中间插入元素 */\n    fun insert(index: Int, num: Int) {\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"索引越界\")\n        // 元素数量超出容量时,触发扩容机制\n        if (size == capacity())\n            extendCapacity()\n        // 将索引 index 以及之后的元素都向后移动一位\n        for (j in size - 1 downTo index)\n            arr[j + 1] = arr[j]\n        arr[index] = num\n        // 更新元素数量\n        size++\n    }\n\n    /* 删除元素 */\n    fun remove(index: Int): Int {\n        if (index < 0 || index >= size)\n            throw IndexOutOfBoundsException(\"索引越界\")\n        val num = arr[index]\n        // 将将索引 index 之后的元素都向前移动一位\n        for (j in index..<size - 1)\n            arr[j] = arr[j + 1]\n        // 更新元素数量\n        size--\n        // 返回被删除的元素\n        return num\n    }\n\n    /* 列表扩容 */\n    fun extendCapacity() {\n        // 新建一个长度为原数组 extendRatio 倍的新数组,并将原数组复制到新数组\n        arr = arr.copyOf(capacity() * extendRatio)\n        // 更新列表容量\n        capacity = arr.size\n    }\n\n    /* 将列表转换为数组 */\n    fun toArray(): IntArray {\n        val size = size()\n        // 仅转换有效长度范围内的列表元素\n        val arr = IntArray(size)\n        for (i in 0..<size) {\n            arr[i] = get(i)\n        }\n        return arr\n    }\n}\n
my_list.rb
### 列表类 ###\nclass MyList\n  attr_reader :size       # 获取列表长度(当前元素数量)\n  attr_reader :capacity   # 获取列表容量\n\n  ### 构造方法 ###\n  def initialize\n    @capacity = 10\n    @size = 0\n    @extend_ratio = 2\n    @arr = Array.new(capacity)\n  end\n\n  ### 访问元素 ###\n  def get(index)\n    # 索引如果越界,则抛出异常,下同\n    raise IndexError, \"索引越界\" if index < 0 || index >= size\n    @arr[index]\n  end\n\n  ### 访问元素 ###\n  def set(index, num)\n    raise IndexError, \"索引越界\" if index < 0 || index >= size\n    @arr[index] = num\n  end\n\n  ### 在尾部添加元素 ###\n  def add(num)\n    # 元素数量超出容量时,触发扩容机制\n    extend_capacity if size == capacity\n    @arr[size] = num\n\n    # 更新元素数量\n    @size += 1\n  end\n\n  ### 在中间插入元素 ###\n  def insert(index, num)\n    raise IndexError, \"索引越界\" if index < 0 || index >= size\n\n    # 元素数量超出容量时,触发扩容机制\n    extend_capacity if size == capacity\n\n    # 将索引 index 以及之后的元素都向后移动一位\n    for j in (size - 1).downto(index)\n      @arr[j + 1] = @arr[j]\n    end\n    @arr[index] = num\n\n    # 更新元素数量\n    @size += 1\n  end\n\n  ### 删除元素 ###\n  def remove(index)\n    raise IndexError, \"索引越界\" if index < 0 || index >= size\n    num = @arr[index]\n\n    # 将将索引 index 之后的元素都向前移动一位\n    for j in index...size\n      @arr[j] = @arr[j + 1]\n    end\n\n    # 更新元素数量\n    @size -= 1\n\n    # 返回被删除的元素\n    num\n  end\n\n  ### 列表扩容 ###\n  def extend_capacity\n    # 新建一个长度为原数组 extend_ratio 倍的新数组,并将原数组复制到新数组\n    arr = @arr.dup + Array.new(capacity * (@extend_ratio - 1))\n    # 更新列表容量\n    @capacity = arr.length\n  end\n\n  ### 将列表转换为数组 ###\n  def to_array\n    sz = size\n    # 仅转换有效长度范围内的列表元素\n    arr = Array.new(sz)\n    for i in 0...sz\n      arr[i] = get(i)\n    end\n    arr\n  end\nend\n
可视化运行

全屏观看 >

","path":["第 4 章   数组与链表","4.3   列表"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/","level":1,"title":"4.4   内存与缓存 *","text":"

在本章的前两节中,我们探讨了数组和链表这两种基础且重要的数据结构,它们分别代表了“连续存储”和“分散存储”两种物理结构。

实际上,物理结构在很大程度上决定了程序对内存和缓存的使用效率,进而影响算法程序的整体性能。

","path":["第 4 章   数组与链表","4.4   内存与缓存 *"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/#441","level":2,"title":"4.4.1   计算机存储设备","text":"

计算机中包括三种类型的存储设备:硬盘(hard disk)、内存(random-access memory, RAM)、缓存(cache memory)。表 4-2 展示了它们在计算机系统中的不同角色和性能特点。

表 4-2   计算机的存储设备

硬盘 内存 缓存 用途 长期存储数据,包括操作系统、程序、文件等 临时存储当前运行的程序和正在处理的数据 存储经常访问的数据和指令,减少 CPU 访问内存的次数 易失性 断电后数据不会丢失 断电后数据会丢失 断电后数据会丢失 容量 较大,TB 级别 较小,GB 级别 非常小,MB 级别 速度 较慢,几百到几千 MB/s 较快,几十 GB/s 非常快,几十到几百 GB/s 价格(人民币) 较便宜,几毛到几元 / GB 较贵,几十到几百元 / GB 非常贵,随 CPU 打包计价

我们可以将计算机存储系统想象为图 4-9 所示的金字塔结构。越靠近金字塔顶端的存储设备的速度越快、容量越小、成本越高。这种多层级的设计并非偶然,而是计算机科学家和工程师们经过深思熟虑的结果。

图 4-9   计算机存储系统

Tip

计算机的存储层次结构体现了速度、容量和成本三者之间的精妙平衡。实际上,这种权衡普遍存在于所有工业领域,它要求我们在不同的优势和限制之间找到最佳平衡点。

总的来说,硬盘用于长期存储大量数据,内存用于临时存储程序运行中正在处理的数据,而缓存则用于存储经常访问的数据和指令,以提高程序运行效率。三者共同协作,确保计算机系统高效运行。

如图 4-10 所示,在程序运行时,数据会从硬盘中被读取到内存中,供 CPU 计算使用。缓存可以看作 CPU 的一部分,它通过智能地从内存加载数据,给 CPU 提供高速的数据读取,从而显著提升程序的执行效率,减少对较慢的内存的依赖。

图 4-10   硬盘、内存和缓存之间的数据流通

","path":["第 4 章   数组与链表","4.4   内存与缓存 *"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/#442","level":2,"title":"4.4.2   数据结构的内存效率","text":"

在内存空间利用方面,数组和链表各自具有优势和局限性。

一方面,内存是有限的,且同一块内存不能被多个程序共享,因此我们希望数据结构能够尽可能高效地利用空间。数组的元素紧密排列,不需要额外的空间来存储链表节点间的引用(指针),因此空间效率更高。然而,数组需要一次性分配足够的连续内存空间,这可能导致内存浪费,数组扩容也需要额外的时间和空间成本。相比之下,链表以“节点”为单位进行动态内存分配和回收,提供了更大的灵活性。

另一方面,在程序运行时,随着反复申请与释放内存,空闲内存的碎片化程度会越来越高,从而导致内存的利用效率降低。数组由于其连续的存储方式,相对不容易导致内存碎片化。相反,链表的元素是分散存储的,在频繁的插入与删除操作中,更容易导致内存碎片化。

","path":["第 4 章   数组与链表","4.4   内存与缓存 *"],"tags":[]},{"location":"chapter_array_and_linkedlist/ram_and_cache/#443","level":2,"title":"4.4.3   数据结构的缓存效率","text":"

缓存虽然在空间容量上远小于内存,但它比内存快得多,在程序执行速度上起着至关重要的作用。由于缓存的容量有限,只能存储一小部分频繁访问的数据,因此当 CPU 尝试访问的数据不在缓存中时,就会发生缓存未命中(cache miss),此时 CPU 不得不从速度较慢的内存中加载所需数据。

显然,“缓存未命中”越少,CPU 读写数据的效率就越高,程序性能也就越好。我们将 CPU 从缓存中成功获取数据的比例称为缓存命中率(cache hit rate),这个指标通常用来衡量缓存效率。

为了尽可能达到更高的效率,缓存会采取以下数据加载机制。

实际上,数组和链表对缓存的利用效率是不同的,主要体现在以下几个方面。

总体而言,数组具有更高的缓存命中率,因此它在操作效率上通常优于链表。这使得在解决算法问题时,基于数组实现的数据结构往往更受欢迎。

需要注意的是,高缓存效率并不意味着数组在所有情况下都优于链表。实际应用中选择哪种数据结构,应根据具体需求来决定。例如,数组和链表都可以实现“栈”数据结构(下一章会详细介绍),但它们适用于不同场景。

","path":["第 4 章   数组与链表","4.4   内存与缓存 *"],"tags":[]},{"location":"chapter_array_and_linkedlist/summary/","level":1,"title":"4.5   小结","text":"","path":["第 4 章   数组与链表","4.5   小结"],"tags":[]},{"location":"chapter_array_and_linkedlist/summary/#1","level":3,"title":"1.   重点回顾","text":"","path":["第 4 章   数组与链表","4.5   小结"],"tags":[]},{"location":"chapter_array_and_linkedlist/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q:数组存储在栈上和存储在堆上,对时间效率和空间效率是否有影响?

存储在栈上和堆上的数组都被存储在连续内存空间内,数据操作效率基本一致。然而,栈和堆具有各自的特点,从而导致以下不同点。

  1. 分配和释放效率:栈是一块较小的内存,分配由编译器自动完成;而堆内存相对更大,可以在代码中动态分配,更容易碎片化。因此,堆上的分配和释放操作通常比栈上的慢。
  2. 大小限制:栈内存相对较小,堆的大小一般受限于可用内存。因此堆更加适合存储大型数组。
  3. 灵活性:栈上的数组的大小需要在编译时确定,而堆上的数组的大小可以在运行时动态确定。

Q:为什么数组要求相同类型的元素,而在链表中却没有强调相同类型呢?

链表由节点组成,节点之间通过引用(指针)连接,各个节点可以存储不同类型的数据,例如 intdoublestringobject 等。

相对地,数组元素则必须是相同类型的,这样才能通过计算偏移量来获取对应元素位置。例如,数组同时包含 intlong 两种类型,单个元素分别占用 4 字节和 8 字节 ,此时就不能用以下公式计算偏移量了,因为数组中包含了两种“元素长度”。

# 元素内存地址 = 数组内存地址(首元素内存地址) + 元素长度 * 元素索引\n

Q:删除节点 P 后,是否需要把 P.next 设为 None 呢?

不修改 P.next 也可以。从该链表的角度看,从头节点遍历到尾节点已经不会遇到 P 了。这意味着节点 P 已经从链表中删除了,此时节点 P 指向哪里都不会对该链表产生影响。

从数据结构与算法(做题)的角度看,不断开没有关系,只要保证程序的逻辑是正确的就行。从标准库的角度看,断开更加安全、逻辑更加清晰。如果不断开,假设被删除节点未被正常回收,那么它会影响后继节点的内存回收。

Q:在链表中插入和删除操作的时间复杂度是 \\(O(1)\\) 。但是增删之前都需要 \\(O(n)\\) 的时间查找元素,那为什么时间复杂度不是 \\(O(n)\\) 呢?

如果是先查找元素、再删除元素,时间复杂度确实是 \\(O(n)\\) 。然而,链表的 \\(O(1)\\) 增删的优势可以在其他应用上得到体现。例如,双向队列适合使用链表实现,我们维护一个指针变量始终指向头节点、尾节点,每次插入与删除操作都是 \\(O(1)\\) 。

Q:图“链表定义与存储方式”中,浅蓝色的存储节点指针是占用一块内存地址吗?还是和节点值各占一半呢?

该示意图只是定性表示,定量表示需要根据具体情况进行分析。

Q:在列表末尾添加元素是否时时刻刻都为 \\(O(1)\\) ?

如果添加元素时超出列表长度,则需要先扩容列表再添加。系统会申请一块新的内存,并将原列表的所有元素搬运过去,这时候时间复杂度就会是 \\(O(n)\\) 。

Q:“列表的出现极大地提高了数组的实用性,但可能导致部分内存空间浪费”,这里的空间浪费是指额外增加的变量如容量、长度、扩容倍数所占的内存吗?

这里的空间浪费主要有两方面含义:一方面,列表都会设定一个初始长度,我们不一定需要用这么多;另一方面,为了防止频繁扩容,扩容一般会乘以一个系数,比如 \\(\\times 1.5\\) 。这样一来,也会出现很多空位,我们通常不能完全填满它们。

Q:在 Python 中初始化 n = [1, 2, 3] 后,这 3 个元素的地址是相连的,但是初始化 m = [2, 1, 3] 会发现它们每个元素的 id 并不是连续的,而是分别跟 n 中的相同。这些元素的地址不连续,那么 m 还是数组吗?

假如把列表元素换成链表节点 n = [n1, n2, n3, n4, n5] ,通常情况下这 5 个节点对象也分散存储在内存各处。然而,给定一个列表索引,我们仍然可以在 \\(O(1)\\) 时间内获取节点内存地址,从而访问到对应的节点。这是因为数组中存储的是节点的引用,而非节点本身。

与许多语言不同,Python 中的数字也被包装为对象,列表中存储的不是数字本身,而是对数字的引用。因此,我们会发现两个数组中的相同数字拥有同一个 id ,并且这些数字的内存地址无须连续。

Q:C++ STL 里面的 std::list 已经实现了双向链表,但好像一些算法书上不怎么直接使用它,是不是因为有什么局限性呢?

一方面,我们往往更青睐使用数组实现算法,而只在必要时才使用链表,主要有两个原因。

另一方面,必要使用链表的情况主要是二叉树和图。栈和队列往往会使用编程语言提供的 stackqueue ,而非链表。

Q:操作 res = [[0]] * n 生成了一个二维列表,其中每一个 [0] 都是独立的吗?

不是独立的。此二维列表中,所有的 [0] 实际上是同一个对象的引用。如果我们修改其中一个元素,会发现所有的对应元素都会随之改变。

如果希望二维列表中的每个 [0] 都是独立的,可以使用 res = [[0] for _ in range(n)] 来实现。这种方式的原理是初始化了 \\(n\\) 个独立的 [0] 列表对象。

Q:操作 res = [0] * n 生成了一个列表,其中每一个整数 0 都是独立的吗?

在该列表中,所有整数 0 都是同一个对象的引用。这是因为 Python 对小整数(通常是 -5 到 256)采用了缓存池机制,以便最大化对象复用,从而提升性能。

虽然它们指向同一个对象,但我们仍然可以独立修改列表中的每个元素,这是因为 Python 的整数是“不可变对象”。当我们修改某个元素时,实际上是切换为另一个对象的引用,而不是改变原有对象本身。

然而,当列表元素是“可变对象”时(例如列表、字典或类实例等),修改某个元素会直接改变该对象本身,所有引用该对象的元素都会产生相同变化。

","path":["第 4 章   数组与链表","4.5   小结"],"tags":[]},{"location":"chapter_backtracking/","level":1,"title":"第 13 章   回溯","text":"

Abstract

我们如同迷宫中的探索者,在前进的道路上可能会遇到困难。

回溯的力量让我们能够重新开始,不断尝试,最终找到通往光明的出口。

","path":["第 13 章   回溯"],"tags":[]},{"location":"chapter_backtracking/#_1","level":2,"title":"本章内容","text":"","path":["第 13 章   回溯"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/","level":1,"title":"13.1   回溯算法","text":"

回溯算法(backtracking algorithm)是一种通过穷举来解决问题的方法,它的核心思想是从一个初始状态出发,暴力搜索所有可能的解决方案,当遇到正确的解则将其记录,直到找到解或者尝试了所有可能的选择都无法找到解为止。

回溯算法通常采用“深度优先搜索”来遍历解空间。在“二叉树”章节中,我们提到前序、中序和后序遍历都属于深度优先搜索。接下来,我们利用前序遍历构造一个回溯问题,逐步了解回溯算法的工作原理。

例题一

给定一棵二叉树,搜索并记录所有值为 \\(7\\) 的节点,请返回节点列表。

对于此题,我们前序遍历这棵树,并判断当前节点的值是否为 \\(7\\) ,若是,则将该节点的值加入结果列表 res 之中。相关过程实现如图 13-1 和以下代码所示:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_i_compact.py
def pre_order(root: TreeNode):\n    \"\"\"前序遍历:例题一\"\"\"\n    if root is None:\n        return\n    if root.val == 7:\n        # 记录解\n        res.append(root)\n    pre_order(root.left)\n    pre_order(root.right)\n
preorder_traversal_i_compact.cpp
/* 前序遍历:例题一 */\nvoid preOrder(TreeNode *root) {\n    if (root == nullptr) {\n        return;\n    }\n    if (root->val == 7) {\n        // 记录解\n        res.push_back(root);\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n}\n
preorder_traversal_i_compact.java
/* 前序遍历:例题一 */\nvoid preOrder(TreeNode root) {\n    if (root == null) {\n        return;\n    }\n    if (root.val == 7) {\n        // 记录解\n        res.add(root);\n    }\n    preOrder(root.left);\n    preOrder(root.right);\n}\n
preorder_traversal_i_compact.cs
/* 前序遍历:例题一 */\nvoid PreOrder(TreeNode? root) {\n    if (root == null) {\n        return;\n    }\n    if (root.val == 7) {\n        // 记录解\n        res.Add(root);\n    }\n    PreOrder(root.left);\n    PreOrder(root.right);\n}\n
preorder_traversal_i_compact.go
/* 前序遍历:例题一 */\nfunc preOrderI(root *TreeNode, res *[]*TreeNode) {\n    if root == nil {\n        return\n    }\n    if (root.Val).(int) == 7 {\n        // 记录解\n        *res = append(*res, root)\n    }\n    preOrderI(root.Left, res)\n    preOrderI(root.Right, res)\n}\n
preorder_traversal_i_compact.swift
/* 前序遍历:例题一 */\nfunc preOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    if root.val == 7 {\n        // 记录解\n        res.append(root)\n    }\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n}\n
preorder_traversal_i_compact.js
/* 前序遍历:例题一 */\nfunction preOrder(root, res) {\n    if (root === null) {\n        return;\n    }\n    if (root.val === 7) {\n        // 记录解\n        res.push(root);\n    }\n    preOrder(root.left, res);\n    preOrder(root.right, res);\n}\n
preorder_traversal_i_compact.ts
/* 前序遍历:例题一 */\nfunction preOrder(root: TreeNode | null, res: TreeNode[]): void {\n    if (root === null) {\n        return;\n    }\n    if (root.val === 7) {\n        // 记录解\n        res.push(root);\n    }\n    preOrder(root.left, res);\n    preOrder(root.right, res);\n}\n
preorder_traversal_i_compact.dart
/* 前序遍历:例题一 */\nvoid preOrder(TreeNode? root, List<TreeNode> res) {\n  if (root == null) {\n    return;\n  }\n  if (root.val == 7) {\n    // 记录解\n    res.add(root);\n  }\n  preOrder(root.left, res);\n  preOrder(root.right, res);\n}\n
preorder_traversal_i_compact.rs
/* 前序遍历:例题一 */\nfn pre_order(res: &mut Vec<Rc<RefCell<TreeNode>>>, root: Option<&Rc<RefCell<TreeNode>>>) {\n    if root.is_none() {\n        return;\n    }\n    if let Some(node) = root {\n        if node.borrow().val == 7 {\n            // 记录解\n            res.push(node.clone());\n        }\n        pre_order(res, node.borrow().left.as_ref());\n        pre_order(res, node.borrow().right.as_ref());\n    }\n}\n
preorder_traversal_i_compact.c
/* 前序遍历:例题一 */\nvoid preOrder(TreeNode *root) {\n    if (root == NULL) {\n        return;\n    }\n    if (root->val == 7) {\n        // 记录解\n        res[resSize++] = root;\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n}\n
preorder_traversal_i_compact.kt
/* 前序遍历:例题一 */\nfun preOrder(root: TreeNode?) {\n    if (root == null) {\n        return\n    }\n    if (root._val == 7) {\n        // 记录解\n        res!!.add(root)\n    }\n    preOrder(root.left)\n    preOrder(root.right)\n}\n
preorder_traversal_i_compact.rb
### 前序遍历:例题一 ###\ndef pre_order(root)\n  return unless root\n\n  # 记录解\n  $res << root if root.val == 7\n\n  pre_order(root.left)\n  pre_order(root.right)\nend\n
可视化运行

全屏观看 >

图 13-1   在前序遍历中搜索节点

","path":["第 13 章   回溯","13.1   回溯算法"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1311","level":2,"title":"13.1.1   尝试与回退","text":"

之所以称之为回溯算法,是因为该算法在搜索解空间时会采用“尝试”与“回退”的策略。当算法在搜索过程中遇到某个状态无法继续前进或无法得到满足条件的解时,它会撤销上一步的选择,退回到之前的状态,并尝试其他可能的选择。

对于例题一,访问每个节点都代表一次“尝试”,而越过叶节点或返回父节点的 return 则表示“回退”。

值得说明的是,回退并不仅仅包括函数返回。为解释这一点,我们对例题一稍作拓展。

例题二

在二叉树中搜索所有值为 \\(7\\) 的节点,请返回根节点到这些节点的路径。

在例题一代码的基础上,我们需要借助一个列表 path 记录访问过的节点路径。当访问到值为 \\(7\\) 的节点时,则复制 path 并添加进结果列表 res 。遍历完成后,res 中保存的就是所有的解。代码如下所示:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_ii_compact.py
def pre_order(root: TreeNode):\n    \"\"\"前序遍历:例题二\"\"\"\n    if root is None:\n        return\n    # 尝试\n    path.append(root)\n    if root.val == 7:\n        # 记录解\n        res.append(list(path))\n    pre_order(root.left)\n    pre_order(root.right)\n    # 回退\n    path.pop()\n
preorder_traversal_ii_compact.cpp
/* 前序遍历:例题二 */\nvoid preOrder(TreeNode *root) {\n    if (root == nullptr) {\n        return;\n    }\n    // 尝试\n    path.push_back(root);\n    if (root->val == 7) {\n        // 记录解\n        res.push_back(path);\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n    // 回退\n    path.pop_back();\n}\n
preorder_traversal_ii_compact.java
/* 前序遍历:例题二 */\nvoid preOrder(TreeNode root) {\n    if (root == null) {\n        return;\n    }\n    // 尝试\n    path.add(root);\n    if (root.val == 7) {\n        // 记录解\n        res.add(new ArrayList<>(path));\n    }\n    preOrder(root.left);\n    preOrder(root.right);\n    // 回退\n    path.remove(path.size() - 1);\n}\n
preorder_traversal_ii_compact.cs
/* 前序遍历:例题二 */\nvoid PreOrder(TreeNode? root) {\n    if (root == null) {\n        return;\n    }\n    // 尝试\n    path.Add(root);\n    if (root.val == 7) {\n        // 记录解\n        res.Add(new List<TreeNode>(path));\n    }\n    PreOrder(root.left);\n    PreOrder(root.right);\n    // 回退\n    path.RemoveAt(path.Count - 1);\n}\n
preorder_traversal_ii_compact.go
/* 前序遍历:例题二 */\nfunc preOrderII(root *TreeNode, res *[][]*TreeNode, path *[]*TreeNode) {\n    if root == nil {\n        return\n    }\n    // 尝试\n    *path = append(*path, root)\n    if root.Val.(int) == 7 {\n        // 记录解\n        *res = append(*res, append([]*TreeNode{}, *path...))\n    }\n    preOrderII(root.Left, res, path)\n    preOrderII(root.Right, res, path)\n    // 回退\n    *path = (*path)[:len(*path)-1]\n}\n
preorder_traversal_ii_compact.swift
/* 前序遍历:例题二 */\nfunc preOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // 尝试\n    path.append(root)\n    if root.val == 7 {\n        // 记录解\n        res.append(path)\n    }\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n    // 回退\n    path.removeLast()\n}\n
preorder_traversal_ii_compact.js
/* 前序遍历:例题二 */\nfunction preOrder(root, path, res) {\n    if (root === null) {\n        return;\n    }\n    // 尝试\n    path.push(root);\n    if (root.val === 7) {\n        // 记录解\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // 回退\n    path.pop();\n}\n
preorder_traversal_ii_compact.ts
/* 前序遍历:例题二 */\nfunction preOrder(\n    root: TreeNode | null,\n    path: TreeNode[],\n    res: TreeNode[][]\n): void {\n    if (root === null) {\n        return;\n    }\n    // 尝试\n    path.push(root);\n    if (root.val === 7) {\n        // 记录解\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // 回退\n    path.pop();\n}\n
preorder_traversal_ii_compact.dart
/* 前序遍历:例题二 */\nvoid preOrder(\n  TreeNode? root,\n  List<TreeNode> path,\n  List<List<TreeNode>> res,\n) {\n  if (root == null) {\n    return;\n  }\n\n  // 尝试\n  path.add(root);\n  if (root.val == 7) {\n    // 记录解\n    res.add(List.from(path));\n  }\n  preOrder(root.left, path, res);\n  preOrder(root.right, path, res);\n  // 回退\n  path.removeLast();\n}\n
preorder_traversal_ii_compact.rs
/* 前序遍历:例题二 */\nfn pre_order(\n    res: &mut Vec<Vec<Rc<RefCell<TreeNode>>>>,\n    path: &mut Vec<Rc<RefCell<TreeNode>>>,\n    root: Option<&Rc<RefCell<TreeNode>>>,\n) {\n    if root.is_none() {\n        return;\n    }\n    if let Some(node) = root {\n        // 尝试\n        path.push(node.clone());\n        if node.borrow().val == 7 {\n            // 记录解\n            res.push(path.clone());\n        }\n        pre_order(res, path, node.borrow().left.as_ref());\n        pre_order(res, path, node.borrow().right.as_ref());\n        // 回退\n        path.pop();\n    }\n}\n
preorder_traversal_ii_compact.c
/* 前序遍历:例题二 */\nvoid preOrder(TreeNode *root) {\n    if (root == NULL) {\n        return;\n    }\n    // 尝试\n    path[pathSize++] = root;\n    if (root->val == 7) {\n        // 记录解\n        for (int i = 0; i < pathSize; ++i) {\n            res[resSize][i] = path[i];\n        }\n        resSize++;\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n    // 回退\n    pathSize--;\n}\n
preorder_traversal_ii_compact.kt
/* 前序遍历:例题二 */\nfun preOrder(root: TreeNode?) {\n    if (root == null) {\n        return\n    }\n    // 尝试\n    path!!.add(root)\n    if (root._val == 7) {\n        // 记录解\n        res!!.add(path!!.toMutableList())\n    }\n    preOrder(root.left)\n    preOrder(root.right)\n    // 回退\n    path!!.removeAt(path!!.size - 1)\n}\n
preorder_traversal_ii_compact.rb
### 前序遍历:例题二 ###\ndef pre_order(root)\n  return unless root\n\n  # 尝试\n  $path << root\n\n  # 记录解\n  $res << $path.dup if root.val == 7\n\n  pre_order(root.left)\n  pre_order(root.right)\n\n  # 回退\n  $path.pop\nend\n
可视化运行

全屏观看 >

在每次“尝试”中,我们通过将当前节点添加进 path 来记录路径;而在“回退”前,我们需要将该节点从 path 中弹出,以恢复本次尝试之前的状态。

观察图 13-2 所示的过程,我们可以将尝试和回退理解为“前进”与“撤销”,两个操作互为逆向。

<1><2><3><4><5><6><7><8><9><10><11>

图 13-2   尝试与回退

","path":["第 13 章   回溯","13.1   回溯算法"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1312","level":2,"title":"13.1.2   剪枝","text":"

复杂的回溯问题通常包含一个或多个约束条件,约束条件通常可用于“剪枝”。

例题三

在二叉树中搜索所有值为 \\(7\\) 的节点,请返回根节点到这些节点的路径,并要求路径中不包含值为 \\(3\\) 的节点。

为了满足以上约束条件,我们需要添加剪枝操作:在搜索过程中,若遇到值为 \\(3\\) 的节点,则提前返回,不再继续搜索。代码如下所示:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_iii_compact.py
def pre_order(root: TreeNode):\n    \"\"\"前序遍历:例题三\"\"\"\n    # 剪枝\n    if root is None or root.val == 3:\n        return\n    # 尝试\n    path.append(root)\n    if root.val == 7:\n        # 记录解\n        res.append(list(path))\n    pre_order(root.left)\n    pre_order(root.right)\n    # 回退\n    path.pop()\n
preorder_traversal_iii_compact.cpp
/* 前序遍历:例题三 */\nvoid preOrder(TreeNode *root) {\n    // 剪枝\n    if (root == nullptr || root->val == 3) {\n        return;\n    }\n    // 尝试\n    path.push_back(root);\n    if (root->val == 7) {\n        // 记录解\n        res.push_back(path);\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n    // 回退\n    path.pop_back();\n}\n
preorder_traversal_iii_compact.java
/* 前序遍历:例题三 */\nvoid preOrder(TreeNode root) {\n    // 剪枝\n    if (root == null || root.val == 3) {\n        return;\n    }\n    // 尝试\n    path.add(root);\n    if (root.val == 7) {\n        // 记录解\n        res.add(new ArrayList<>(path));\n    }\n    preOrder(root.left);\n    preOrder(root.right);\n    // 回退\n    path.remove(path.size() - 1);\n}\n
preorder_traversal_iii_compact.cs
/* 前序遍历:例题三 */\nvoid PreOrder(TreeNode? root) {\n    // 剪枝\n    if (root == null || root.val == 3) {\n        return;\n    }\n    // 尝试\n    path.Add(root);\n    if (root.val == 7) {\n        // 记录解\n        res.Add(new List<TreeNode>(path));\n    }\n    PreOrder(root.left);\n    PreOrder(root.right);\n    // 回退\n    path.RemoveAt(path.Count - 1);\n}\n
preorder_traversal_iii_compact.go
/* 前序遍历:例题三 */\nfunc preOrderIII(root *TreeNode, res *[][]*TreeNode, path *[]*TreeNode) {\n    // 剪枝\n    if root == nil || root.Val == 3 {\n        return\n    }\n    // 尝试\n    *path = append(*path, root)\n    if root.Val.(int) == 7 {\n        // 记录解\n        *res = append(*res, append([]*TreeNode{}, *path...))\n    }\n    preOrderIII(root.Left, res, path)\n    preOrderIII(root.Right, res, path)\n    // 回退\n    *path = (*path)[:len(*path)-1]\n}\n
preorder_traversal_iii_compact.swift
/* 前序遍历:例题三 */\nfunc preOrder(root: TreeNode?) {\n    // 剪枝\n    guard let root = root, root.val != 3 else {\n        return\n    }\n    // 尝试\n    path.append(root)\n    if root.val == 7 {\n        // 记录解\n        res.append(path)\n    }\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n    // 回退\n    path.removeLast()\n}\n
preorder_traversal_iii_compact.js
/* 前序遍历:例题三 */\nfunction preOrder(root, path, res) {\n    // 剪枝\n    if (root === null || root.val === 3) {\n        return;\n    }\n    // 尝试\n    path.push(root);\n    if (root.val === 7) {\n        // 记录解\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // 回退\n    path.pop();\n}\n
preorder_traversal_iii_compact.ts
/* 前序遍历:例题三 */\nfunction preOrder(\n    root: TreeNode | null,\n    path: TreeNode[],\n    res: TreeNode[][]\n): void {\n    // 剪枝\n    if (root === null || root.val === 3) {\n        return;\n    }\n    // 尝试\n    path.push(root);\n    if (root.val === 7) {\n        // 记录解\n        res.push([...path]);\n    }\n    preOrder(root.left, path, res);\n    preOrder(root.right, path, res);\n    // 回退\n    path.pop();\n}\n
preorder_traversal_iii_compact.dart
/* 前序遍历:例题三 */\nvoid preOrder(\n  TreeNode? root,\n  List<TreeNode> path,\n  List<List<TreeNode>> res,\n) {\n  if (root == null || root.val == 3) {\n    return;\n  }\n\n  // 尝试\n  path.add(root);\n  if (root.val == 7) {\n    // 记录解\n    res.add(List.from(path));\n  }\n  preOrder(root.left, path, res);\n  preOrder(root.right, path, res);\n  // 回退\n  path.removeLast();\n}\n
preorder_traversal_iii_compact.rs
/* 前序遍历:例题三 */\nfn pre_order(\n    res: &mut Vec<Vec<Rc<RefCell<TreeNode>>>>,\n    path: &mut Vec<Rc<RefCell<TreeNode>>>,\n    root: Option<&Rc<RefCell<TreeNode>>>,\n) {\n    // 剪枝\n    if root.is_none() || root.as_ref().unwrap().borrow().val == 3 {\n        return;\n    }\n    if let Some(node) = root {\n        // 尝试\n        path.push(node.clone());\n        if node.borrow().val == 7 {\n            // 记录解\n            res.push(path.clone());\n        }\n        pre_order(res, path, node.borrow().left.as_ref());\n        pre_order(res, path, node.borrow().right.as_ref());\n        // 回退\n        path.pop();\n    }\n}\n
preorder_traversal_iii_compact.c
/* 前序遍历:例题三 */\nvoid preOrder(TreeNode *root) {\n    // 剪枝\n    if (root == NULL || root->val == 3) {\n        return;\n    }\n    // 尝试\n    path[pathSize++] = root;\n    if (root->val == 7) {\n        // 记录解\n        for (int i = 0; i < pathSize; i++) {\n            res[resSize][i] = path[i];\n        }\n        resSize++;\n    }\n    preOrder(root->left);\n    preOrder(root->right);\n    // 回退\n    pathSize--;\n}\n
preorder_traversal_iii_compact.kt
/* 前序遍历:例题三 */\nfun preOrder(root: TreeNode?) {\n    // 剪枝\n    if (root == null || root._val == 3) {\n        return\n    }\n    // 尝试\n    path!!.add(root)\n    if (root._val == 7) {\n        // 记录解\n        res!!.add(path!!.toMutableList())\n    }\n    preOrder(root.left)\n    preOrder(root.right)\n    // 回退\n    path!!.removeAt(path!!.size - 1)\n}\n
preorder_traversal_iii_compact.rb
### 前序遍历:例题三 ###\ndef pre_order(root)\n  # 剪枝\n  return if !root || root.val == 3\n\n  # 尝试\n  $path.append(root)\n\n  # 记录解\n  $res << $path.dup if root.val == 7\n\n  pre_order(root.left)\n  pre_order(root.right)\n\n  # 回退\n  $path.pop\nend\n
可视化运行

全屏观看 >

“剪枝”是一个非常形象的名词。如图 13-3 所示,在搜索过程中,我们“剪掉”了不满足约束条件的搜索分支,避免许多无意义的尝试,从而提高了搜索效率。

图 13-3   根据约束条件剪枝

","path":["第 13 章   回溯","13.1   回溯算法"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1313","level":2,"title":"13.1.3   框架代码","text":"

接下来,我们尝试将回溯的“尝试、回退、剪枝”的主体框架提炼出来,提升代码的通用性。

在以下框架代码中,state 表示问题的当前状态,choices 表示当前状态下可以做出的选择:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
def backtrack(state: State, choices: list[choice], res: list[state]):\n    \"\"\"回溯算法框架\"\"\"\n    # 判断是否为解\n    if is_solution(state):\n        # 记录解\n        record_solution(state, res)\n        # 不再继续搜索\n        return\n    # 遍历所有选择\n    for choice in choices:\n        # 剪枝:判断选择是否合法\n        if is_valid(state, choice):\n            # 尝试:做出选择,更新状态\n            make_choice(state, choice)\n            backtrack(state, choices, res)\n            # 回退:撤销选择,恢复到之前的状态\n            undo_choice(state, choice)\n
/* 回溯算法框架 */\nvoid backtrack(State *state, vector<Choice *> &choices, vector<State *> &res) {\n    // 判断是否为解\n    if (isSolution(state)) {\n        // 记录解\n        recordSolution(state, res);\n        // 不再继续搜索\n        return;\n    }\n    // 遍历所有选择\n    for (Choice choice : choices) {\n        // 剪枝:判断选择是否合法\n        if (isValid(state, choice)) {\n            // 尝试:做出选择,更新状态\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // 回退:撤销选择,恢复到之前的状态\n            undoChoice(state, choice);\n        }\n    }\n}\n
/* 回溯算法框架 */\nvoid backtrack(State state, List<Choice> choices, List<State> res) {\n    // 判断是否为解\n    if (isSolution(state)) {\n        // 记录解\n        recordSolution(state, res);\n        // 不再继续搜索\n        return;\n    }\n    // 遍历所有选择\n    for (Choice choice : choices) {\n        // 剪枝:判断选择是否合法\n        if (isValid(state, choice)) {\n            // 尝试:做出选择,更新状态\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // 回退:撤销选择,恢复到之前的状态\n            undoChoice(state, choice);\n        }\n    }\n}\n
/* 回溯算法框架 */\nvoid Backtrack(State state, List<Choice> choices, List<State> res) {\n    // 判断是否为解\n    if (IsSolution(state)) {\n        // 记录解\n        RecordSolution(state, res);\n        // 不再继续搜索\n        return;\n    }\n    // 遍历所有选择\n    foreach (Choice choice in choices) {\n        // 剪枝:判断选择是否合法\n        if (IsValid(state, choice)) {\n            // 尝试:做出选择,更新状态\n            MakeChoice(state, choice);\n            Backtrack(state, choices, res);\n            // 回退:撤销选择,恢复到之前的状态\n            UndoChoice(state, choice);\n        }\n    }\n}\n
/* 回溯算法框架 */\nfunc backtrack(state *State, choices []Choice, res *[]State) {\n    // 判断是否为解\n    if isSolution(state) {\n        // 记录解\n        recordSolution(state, res)\n        // 不再继续搜索\n        return\n    }\n    // 遍历所有选择\n    for _, choice := range choices {\n        // 剪枝:判断选择是否合法\n        if isValid(state, choice) {\n            // 尝试:做出选择,更新状态\n            makeChoice(state, choice)\n            backtrack(state, choices, res)\n            // 回退:撤销选择,恢复到之前的状态\n            undoChoice(state, choice)\n        }\n    }\n}\n
/* 回溯算法框架 */\nfunc backtrack(state: inout State, choices: [Choice], res: inout [State]) {\n    // 判断是否为解\n    if isSolution(state: state) {\n        // 记录解\n        recordSolution(state: state, res: &res)\n        // 不再继续搜索\n        return\n    }\n    // 遍历所有选择\n    for choice in choices {\n        // 剪枝:判断选择是否合法\n        if isValid(state: state, choice: choice) {\n            // 尝试:做出选择,更新状态\n            makeChoice(state: &state, choice: choice)\n            backtrack(state: &state, choices: choices, res: &res)\n            // 回退:撤销选择,恢复到之前的状态\n            undoChoice(state: &state, choice: choice)\n        }\n    }\n}\n
/* 回溯算法框架 */\nfunction backtrack(state, choices, res) {\n    // 判断是否为解\n    if (isSolution(state)) {\n        // 记录解\n        recordSolution(state, res);\n        // 不再继续搜索\n        return;\n    }\n    // 遍历所有选择\n    for (let choice of choices) {\n        // 剪枝:判断选择是否合法\n        if (isValid(state, choice)) {\n            // 尝试:做出选择,更新状态\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // 回退:撤销选择,恢复到之前的状态\n            undoChoice(state, choice);\n        }\n    }\n}\n
/* 回溯算法框架 */\nfunction backtrack(state: State, choices: Choice[], res: State[]): void {\n    // 判断是否为解\n    if (isSolution(state)) {\n        // 记录解\n        recordSolution(state, res);\n        // 不再继续搜索\n        return;\n    }\n    // 遍历所有选择\n    for (let choice of choices) {\n        // 剪枝:判断选择是否合法\n        if (isValid(state, choice)) {\n            // 尝试:做出选择,更新状态\n            makeChoice(state, choice);\n            backtrack(state, choices, res);\n            // 回退:撤销选择,恢复到之前的状态\n            undoChoice(state, choice);\n        }\n    }\n}\n
/* 回溯算法框架 */\nvoid backtrack(State state, List<Choice>, List<State> res) {\n  // 判断是否为解\n  if (isSolution(state)) {\n    // 记录解\n    recordSolution(state, res);\n    // 不再继续搜索\n    return;\n  }\n  // 遍历所有选择\n  for (Choice choice in choices) {\n    // 剪枝:判断选择是否合法\n    if (isValid(state, choice)) {\n      // 尝试:做出选择,更新状态\n      makeChoice(state, choice);\n      backtrack(state, choices, res);\n      // 回退:撤销选择,恢复到之前的状态\n      undoChoice(state, choice);\n    }\n  }\n}\n
/* 回溯算法框架 */\nfn backtrack(state: &mut State, choices: &Vec<Choice>, res: &mut Vec<State>) {\n    // 判断是否为解\n    if is_solution(state) {\n        // 记录解\n        record_solution(state, res);\n        // 不再继续搜索\n        return;\n    }\n    // 遍历所有选择\n    for choice in choices {\n        // 剪枝:判断选择是否合法\n        if is_valid(state, choice) {\n            // 尝试:做出选择,更新状态\n            make_choice(state, choice);\n            backtrack(state, choices, res);\n            // 回退:撤销选择,恢复到之前的状态\n            undo_choice(state, choice);\n        }\n    }\n}\n
/* 回溯算法框架 */\nvoid backtrack(State *state, Choice *choices, int numChoices, State *res, int numRes) {\n    // 判断是否为解\n    if (isSolution(state)) {\n        // 记录解\n        recordSolution(state, res, numRes);\n        // 不再继续搜索\n        return;\n    }\n    // 遍历所有选择\n    for (int i = 0; i < numChoices; i++) {\n        // 剪枝:判断选择是否合法\n        if (isValid(state, &choices[i])) {\n            // 尝试:做出选择,更新状态\n            makeChoice(state, &choices[i]);\n            backtrack(state, choices, numChoices, res, numRes);\n            // 回退:撤销选择,恢复到之前的状态\n            undoChoice(state, &choices[i]);\n        }\n    }\n}\n
/* 回溯算法框架 */\nfun backtrack(state: State?, choices: List<Choice?>, res: List<State?>?) {\n    // 判断是否为解\n    if (isSolution(state)) {\n        // 记录解\n        recordSolution(state, res)\n        // 不再继续搜索\n        return\n    }\n    // 遍历所有选择\n    for (choice in choices) {\n        // 剪枝:判断选择是否合法\n        if (isValid(state, choice)) {\n            // 尝试:做出选择,更新状态\n            makeChoice(state, choice)\n            backtrack(state, choices, res)\n            // 回退:撤销选择,恢复到之前的状态\n            undoChoice(state, choice)\n        }\n    }\n}\n
### 回溯算法框架 ###\ndef backtrack(state, choices, res)\n    # 判断是否为解\n    if is_solution?(state)\n        # 记录解\n        record_solution(state, res)\n        return\n    end\n\n    # 遍历所有选择\n    for choice in choices\n        # 剪枝:判断选择是否合法\n        if is_valid?(state, choice)\n            # 尝试:做出选择,更新状态\n            make_choice(state, choice)\n            backtrack(state, choices, res)\n            # 回退:撤销选择,恢复到之前的状态\n            undo_choice(state, choice)\n        end\n    end\nend\n

接下来,我们基于框架代码来解决例题三。状态 state 为节点遍历路径,选择 choices 为当前节点的左子节点和右子节点,结果 res 是路径列表:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby preorder_traversal_iii_template.py
def is_solution(state: list[TreeNode]) -> bool:\n    \"\"\"判断当前状态是否为解\"\"\"\n    return state and state[-1].val == 7\n\ndef record_solution(state: list[TreeNode], res: list[list[TreeNode]]):\n    \"\"\"记录解\"\"\"\n    res.append(list(state))\n\ndef is_valid(state: list[TreeNode], choice: TreeNode) -> bool:\n    \"\"\"判断在当前状态下,该选择是否合法\"\"\"\n    return choice is not None and choice.val != 3\n\ndef make_choice(state: list[TreeNode], choice: TreeNode):\n    \"\"\"更新状态\"\"\"\n    state.append(choice)\n\ndef undo_choice(state: list[TreeNode], choice: TreeNode):\n    \"\"\"恢复状态\"\"\"\n    state.pop()\n\ndef backtrack(\n    state: list[TreeNode], choices: list[TreeNode], res: list[list[TreeNode]]\n):\n    \"\"\"回溯算法:例题三\"\"\"\n    # 检查是否为解\n    if is_solution(state):\n        # 记录解\n        record_solution(state, res)\n    # 遍历所有选择\n    for choice in choices:\n        # 剪枝:检查选择是否合法\n        if is_valid(state, choice):\n            # 尝试:做出选择,更新状态\n            make_choice(state, choice)\n            # 进行下一轮选择\n            backtrack(state, [choice.left, choice.right], res)\n            # 回退:撤销选择,恢复到之前的状态\n            undo_choice(state, choice)\n
preorder_traversal_iii_template.cpp
/* 判断当前状态是否为解 */\nbool isSolution(vector<TreeNode *> &state) {\n    return !state.empty() && state.back()->val == 7;\n}\n\n/* 记录解 */\nvoid recordSolution(vector<TreeNode *> &state, vector<vector<TreeNode *>> &res) {\n    res.push_back(state);\n}\n\n/* 判断在当前状态下,该选择是否合法 */\nbool isValid(vector<TreeNode *> &state, TreeNode *choice) {\n    return choice != nullptr && choice->val != 3;\n}\n\n/* 更新状态 */\nvoid makeChoice(vector<TreeNode *> &state, TreeNode *choice) {\n    state.push_back(choice);\n}\n\n/* 恢复状态 */\nvoid undoChoice(vector<TreeNode *> &state, TreeNode *choice) {\n    state.pop_back();\n}\n\n/* 回溯算法:例题三 */\nvoid backtrack(vector<TreeNode *> &state, vector<TreeNode *> &choices, vector<vector<TreeNode *>> &res) {\n    // 检查是否为解\n    if (isSolution(state)) {\n        // 记录解\n        recordSolution(state, res);\n    }\n    // 遍历所有选择\n    for (TreeNode *choice : choices) {\n        // 剪枝:检查选择是否合法\n        if (isValid(state, choice)) {\n            // 尝试:做出选择,更新状态\n            makeChoice(state, choice);\n            // 进行下一轮选择\n            vector<TreeNode *> nextChoices{choice->left, choice->right};\n            backtrack(state, nextChoices, res);\n            // 回退:撤销选择,恢复到之前的状态\n            undoChoice(state, choice);\n        }\n    }\n}\n
preorder_traversal_iii_template.java
/* 判断当前状态是否为解 */\nboolean isSolution(List<TreeNode> state) {\n    return !state.isEmpty() && state.get(state.size() - 1).val == 7;\n}\n\n/* 记录解 */\nvoid recordSolution(List<TreeNode> state, List<List<TreeNode>> res) {\n    res.add(new ArrayList<>(state));\n}\n\n/* 判断在当前状态下,该选择是否合法 */\nboolean isValid(List<TreeNode> state, TreeNode choice) {\n    return choice != null && choice.val != 3;\n}\n\n/* 更新状态 */\nvoid makeChoice(List<TreeNode> state, TreeNode choice) {\n    state.add(choice);\n}\n\n/* 恢复状态 */\nvoid undoChoice(List<TreeNode> state, TreeNode choice) {\n    state.remove(state.size() - 1);\n}\n\n/* 回溯算法:例题三 */\nvoid backtrack(List<TreeNode> state, List<TreeNode> choices, List<List<TreeNode>> res) {\n    // 检查是否为解\n    if (isSolution(state)) {\n        // 记录解\n        recordSolution(state, res);\n    }\n    // 遍历所有选择\n    for (TreeNode choice : choices) {\n        // 剪枝:检查选择是否合法\n        if (isValid(state, choice)) {\n            // 尝试:做出选择,更新状态\n            makeChoice(state, choice);\n            // 进行下一轮选择\n            backtrack(state, Arrays.asList(choice.left, choice.right), res);\n            // 回退:撤销选择,恢复到之前的状态\n            undoChoice(state, choice);\n        }\n    }\n}\n
preorder_traversal_iii_template.cs
/* 判断当前状态是否为解 */\nbool IsSolution(List<TreeNode> state) {\n    return state.Count != 0 && state[^1].val == 7;\n}\n\n/* 记录解 */\nvoid RecordSolution(List<TreeNode> state, List<List<TreeNode>> res) {\n    res.Add(new List<TreeNode>(state));\n}\n\n/* 判断在当前状态下,该选择是否合法 */\nbool IsValid(List<TreeNode> state, TreeNode choice) {\n    return choice != null && choice.val != 3;\n}\n\n/* 更新状态 */\nvoid MakeChoice(List<TreeNode> state, TreeNode choice) {\n    state.Add(choice);\n}\n\n/* 恢复状态 */\nvoid UndoChoice(List<TreeNode> state, TreeNode choice) {\n    state.RemoveAt(state.Count - 1);\n}\n\n/* 回溯算法:例题三 */\nvoid Backtrack(List<TreeNode> state, List<TreeNode> choices, List<List<TreeNode>> res) {\n    // 检查是否为解\n    if (IsSolution(state)) {\n        // 记录解\n        RecordSolution(state, res);\n    }\n    // 遍历所有选择\n    foreach (TreeNode choice in choices) {\n        // 剪枝:检查选择是否合法\n        if (IsValid(state, choice)) {\n            // 尝试:做出选择,更新状态\n            MakeChoice(state, choice);\n            // 进行下一轮选择\n            Backtrack(state, [choice.left!, choice.right!], res);\n            // 回退:撤销选择,恢复到之前的状态\n            UndoChoice(state, choice);\n        }\n    }\n}\n
preorder_traversal_iii_template.go
/* 判断当前状态是否为解 */\nfunc isSolution(state *[]*TreeNode) bool {\n    return len(*state) != 0 && (*state)[len(*state)-1].Val == 7\n}\n\n/* 记录解 */\nfunc recordSolution(state *[]*TreeNode, res *[][]*TreeNode) {\n    *res = append(*res, append([]*TreeNode{}, *state...))\n}\n\n/* 判断在当前状态下,该选择是否合法 */\nfunc isValid(state *[]*TreeNode, choice *TreeNode) bool {\n    return choice != nil && choice.Val != 3\n}\n\n/* 更新状态 */\nfunc makeChoice(state *[]*TreeNode, choice *TreeNode) {\n    *state = append(*state, choice)\n}\n\n/* 恢复状态 */\nfunc undoChoice(state *[]*TreeNode, choice *TreeNode) {\n    *state = (*state)[:len(*state)-1]\n}\n\n/* 回溯算法:例题三 */\nfunc backtrackIII(state *[]*TreeNode, choices *[]*TreeNode, res *[][]*TreeNode) {\n    // 检查是否为解\n    if isSolution(state) {\n        // 记录解\n        recordSolution(state, res)\n    }\n    // 遍历所有选择\n    for _, choice := range *choices {\n        // 剪枝:检查选择是否合法\n        if isValid(state, choice) {\n            // 尝试:做出选择,更新状态\n            makeChoice(state, choice)\n            // 进行下一轮选择\n            temp := make([]*TreeNode, 0)\n            temp = append(temp, choice.Left, choice.Right)\n            backtrackIII(state, &temp, res)\n            // 回退:撤销选择,恢复到之前的状态\n            undoChoice(state, choice)\n        }\n    }\n}\n
preorder_traversal_iii_template.swift
/* 判断当前状态是否为解 */\nfunc isSolution(state: [TreeNode]) -> Bool {\n    !state.isEmpty && state.last!.val == 7\n}\n\n/* 记录解 */\nfunc recordSolution(state: [TreeNode], res: inout [[TreeNode]]) {\n    res.append(state)\n}\n\n/* 判断在当前状态下,该选择是否合法 */\nfunc isValid(state: [TreeNode], choice: TreeNode?) -> Bool {\n    choice != nil && choice!.val != 3\n}\n\n/* 更新状态 */\nfunc makeChoice(state: inout [TreeNode], choice: TreeNode) {\n    state.append(choice)\n}\n\n/* 恢复状态 */\nfunc undoChoice(state: inout [TreeNode], choice: TreeNode) {\n    state.removeLast()\n}\n\n/* 回溯算法:例题三 */\nfunc backtrack(state: inout [TreeNode], choices: [TreeNode], res: inout [[TreeNode]]) {\n    // 检查是否为解\n    if isSolution(state: state) {\n        recordSolution(state: state, res: &res)\n    }\n    // 遍历所有选择\n    for choice in choices {\n        // 剪枝:检查选择是否合法\n        if isValid(state: state, choice: choice) {\n            // 尝试:做出选择,更新状态\n            makeChoice(state: &state, choice: choice)\n            // 进行下一轮选择\n            backtrack(state: &state, choices: [choice.left, choice.right].compactMap { $0 }, res: &res)\n            // 回退:撤销选择,恢复到之前的状态\n            undoChoice(state: &state, choice: choice)\n        }\n    }\n}\n
preorder_traversal_iii_template.js
/* 判断当前状态是否为解 */\nfunction isSolution(state) {\n    return state && state[state.length - 1]?.val === 7;\n}\n\n/* 记录解 */\nfunction recordSolution(state, res) {\n    res.push([...state]);\n}\n\n/* 判断在当前状态下,该选择是否合法 */\nfunction isValid(state, choice) {\n    return choice !== null && choice.val !== 3;\n}\n\n/* 更新状态 */\nfunction makeChoice(state, choice) {\n    state.push(choice);\n}\n\n/* 恢复状态 */\nfunction undoChoice(state) {\n    state.pop();\n}\n\n/* 回溯算法:例题三 */\nfunction backtrack(state, choices, res) {\n    // 检查是否为解\n    if (isSolution(state)) {\n        // 记录解\n        recordSolution(state, res);\n    }\n    // 遍历所有选择\n    for (const choice of choices) {\n        // 剪枝:检查选择是否合法\n        if (isValid(state, choice)) {\n            // 尝试:做出选择,更新状态\n            makeChoice(state, choice);\n            // 进行下一轮选择\n            backtrack(state, [choice.left, choice.right], res);\n            // 回退:撤销选择,恢复到之前的状态\n            undoChoice(state);\n        }\n    }\n}\n
preorder_traversal_iii_template.ts
/* 判断当前状态是否为解 */\nfunction isSolution(state: TreeNode[]): boolean {\n    return state && state[state.length - 1]?.val === 7;\n}\n\n/* 记录解 */\nfunction recordSolution(state: TreeNode[], res: TreeNode[][]): void {\n    res.push([...state]);\n}\n\n/* 判断在当前状态下,该选择是否合法 */\nfunction isValid(state: TreeNode[], choice: TreeNode): boolean {\n    return choice !== null && choice.val !== 3;\n}\n\n/* 更新状态 */\nfunction makeChoice(state: TreeNode[], choice: TreeNode): void {\n    state.push(choice);\n}\n\n/* 恢复状态 */\nfunction undoChoice(state: TreeNode[]): void {\n    state.pop();\n}\n\n/* 回溯算法:例题三 */\nfunction backtrack(\n    state: TreeNode[],\n    choices: TreeNode[],\n    res: TreeNode[][]\n): void {\n    // 检查是否为解\n    if (isSolution(state)) {\n        // 记录解\n        recordSolution(state, res);\n    }\n    // 遍历所有选择\n    for (const choice of choices) {\n        // 剪枝:检查选择是否合法\n        if (isValid(state, choice)) {\n            // 尝试:做出选择,更新状态\n            makeChoice(state, choice);\n            // 进行下一轮选择\n            backtrack(state, [choice.left, choice.right], res);\n            // 回退:撤销选择,恢复到之前的状态\n            undoChoice(state);\n        }\n    }\n}\n
preorder_traversal_iii_template.dart
/* 判断当前状态是否为解 */\nbool isSolution(List<TreeNode> state) {\n  return state.isNotEmpty && state.last.val == 7;\n}\n\n/* 记录解 */\nvoid recordSolution(List<TreeNode> state, List<List<TreeNode>> res) {\n  res.add(List.from(state));\n}\n\n/* 判断在当前状态下,该选择是否合法 */\nbool isValid(List<TreeNode> state, TreeNode? choice) {\n  return choice != null && choice.val != 3;\n}\n\n/* 更新状态 */\nvoid makeChoice(List<TreeNode> state, TreeNode? choice) {\n  state.add(choice!);\n}\n\n/* 恢复状态 */\nvoid undoChoice(List<TreeNode> state, TreeNode? choice) {\n  state.removeLast();\n}\n\n/* 回溯算法:例题三 */\nvoid backtrack(\n  List<TreeNode> state,\n  List<TreeNode?> choices,\n  List<List<TreeNode>> res,\n) {\n  // 检查是否为解\n  if (isSolution(state)) {\n    // 记录解\n    recordSolution(state, res);\n  }\n  // 遍历所有选择\n  for (TreeNode? choice in choices) {\n    // 剪枝:检查选择是否合法\n    if (isValid(state, choice)) {\n      // 尝试:做出选择,更新状态\n      makeChoice(state, choice);\n      // 进行下一轮选择\n      backtrack(state, [choice!.left, choice.right], res);\n      // 回退:撤销选择,恢复到之前的状态\n      undoChoice(state, choice);\n    }\n  }\n}\n
preorder_traversal_iii_template.rs
/* 判断当前状态是否为解 */\nfn is_solution(state: &mut Vec<Rc<RefCell<TreeNode>>>) -> bool {\n    return !state.is_empty() && state.last().unwrap().borrow().val == 7;\n}\n\n/* 记录解 */\nfn record_solution(\n    state: &mut Vec<Rc<RefCell<TreeNode>>>,\n    res: &mut Vec<Vec<Rc<RefCell<TreeNode>>>>,\n) {\n    res.push(state.clone());\n}\n\n/* 判断在当前状态下,该选择是否合法 */\nfn is_valid(_: &mut Vec<Rc<RefCell<TreeNode>>>, choice: Option<&Rc<RefCell<TreeNode>>>) -> bool {\n    return choice.is_some() && choice.unwrap().borrow().val != 3;\n}\n\n/* 更新状态 */\nfn make_choice(state: &mut Vec<Rc<RefCell<TreeNode>>>, choice: Rc<RefCell<TreeNode>>) {\n    state.push(choice);\n}\n\n/* 恢复状态 */\nfn undo_choice(state: &mut Vec<Rc<RefCell<TreeNode>>>, _: Rc<RefCell<TreeNode>>) {\n    state.pop();\n}\n\n/* 回溯算法:例题三 */\nfn backtrack(\n    state: &mut Vec<Rc<RefCell<TreeNode>>>,\n    choices: &Vec<Option<&Rc<RefCell<TreeNode>>>>,\n    res: &mut Vec<Vec<Rc<RefCell<TreeNode>>>>,\n) {\n    // 检查是否为解\n    if is_solution(state) {\n        // 记录解\n        record_solution(state, res);\n    }\n    // 遍历所有选择\n    for &choice in choices.iter() {\n        // 剪枝:检查选择是否合法\n        if is_valid(state, choice) {\n            // 尝试:做出选择,更新状态\n            make_choice(state, choice.unwrap().clone());\n            // 进行下一轮选择\n            backtrack(\n                state,\n                &vec![\n                    choice.unwrap().borrow().left.as_ref(),\n                    choice.unwrap().borrow().right.as_ref(),\n                ],\n                res,\n            );\n            // 回退:撤销选择,恢复到之前的状态\n            undo_choice(state, choice.unwrap().clone());\n        }\n    }\n}\n
preorder_traversal_iii_template.c
/* 判断当前状态是否为解 */\nbool isSolution(void) {\n    return pathSize > 0 && path[pathSize - 1]->val == 7;\n}\n\n/* 记录解 */\nvoid recordSolution(void) {\n    for (int i = 0; i < pathSize; i++) {\n        res[resSize][i] = path[i];\n    }\n    resSize++;\n}\n\n/* 判断在当前状态下,该选择是否合法 */\nbool isValid(TreeNode *choice) {\n    return choice != NULL && choice->val != 3;\n}\n\n/* 更新状态 */\nvoid makeChoice(TreeNode *choice) {\n    path[pathSize++] = choice;\n}\n\n/* 恢复状态 */\nvoid undoChoice(void) {\n    pathSize--;\n}\n\n/* 回溯算法:例题三 */\nvoid backtrack(TreeNode *choices[2]) {\n    // 检查是否为解\n    if (isSolution()) {\n        // 记录解\n        recordSolution();\n    }\n    // 遍历所有选择\n    for (int i = 0; i < 2; i++) {\n        TreeNode *choice = choices[i];\n        // 剪枝:检查选择是否合法\n        if (isValid(choice)) {\n            // 尝试:做出选择,更新状态\n            makeChoice(choice);\n            // 进行下一轮选择\n            TreeNode *nextChoices[2] = {choice->left, choice->right};\n            backtrack(nextChoices);\n            // 回退:撤销选择,恢复到之前的状态\n            undoChoice();\n        }\n    }\n}\n
preorder_traversal_iii_template.kt
/* 判断当前状态是否为解 */\nfun isSolution(state: MutableList<TreeNode?>): Boolean {\n    return state.isNotEmpty() && state[state.size - 1]?._val == 7\n}\n\n/* 记录解 */\nfun recordSolution(state: MutableList<TreeNode?>?, res: MutableList<MutableList<TreeNode?>?>) {\n    res.add(state!!.toMutableList())\n}\n\n/* 判断在当前状态下,该选择是否合法 */\nfun isValid(state: MutableList<TreeNode?>?, choice: TreeNode?): Boolean {\n    return choice != null && choice._val != 3\n}\n\n/* 更新状态 */\nfun makeChoice(state: MutableList<TreeNode?>, choice: TreeNode?) {\n    state.add(choice)\n}\n\n/* 恢复状态 */\nfun undoChoice(state: MutableList<TreeNode?>, choice: TreeNode?) {\n    state.removeLast()\n}\n\n/* 回溯算法:例题三 */\nfun backtrack(\n    state: MutableList<TreeNode?>,\n    choices: MutableList<TreeNode?>,\n    res: MutableList<MutableList<TreeNode?>?>\n) {\n    // 检查是否为解\n    if (isSolution(state)) {\n        // 记录解\n        recordSolution(state, res)\n    }\n    // 遍历所有选择\n    for (choice in choices) {\n        // 剪枝:检查选择是否合法\n        if (isValid(state, choice)) {\n            // 尝试:做出选择,更新状态\n            makeChoice(state, choice)\n            // 进行下一轮选择\n            backtrack(state, mutableListOf(choice!!.left, choice.right), res)\n            // 回退:撤销选择,恢复到之前的状态\n            undoChoice(state, choice)\n        }\n    }\n}\n
preorder_traversal_iii_template.rb
### 判断当前状态是否为解 ###\ndef is_solution?(state)\n  !state.empty? && state.last.val == 7\nend\n\n### 记录解 ###\ndef record_solution(state, res)\n  res << state.dup\nend\n\n### 判断在当前状态下,该选择是否合法 ###\ndef is_valid?(state, choice)\n  choice && choice.val != 3\nend\n\n### 更新状态 ###\ndef make_choice(state, choice)\n  state << choice\nend\n\n### 恢复状态 ###\ndef undo_choice(state, choice)\n  state.pop\nend\n\n### 回溯算法:例题三 ###\ndef backtrack(state, choices, res)\n  # 检查是否为解\n  record_solution(state, res) if is_solution?(state)\n\n  # 遍历所有选择\n  for choice in choices\n    # 剪枝:检查选择是否合法\n    if is_valid?(state, choice)\n      # 尝试:做出选择,更新状态\n      make_choice(state, choice)\n      # 进行下一轮选择\n      backtrack(state, [choice.left, choice.right], res)\n      # 回退:撤销选择,恢复到之前的状态\n      undo_choice(state, choice)\n    end\n  end\nend\n
可视化运行

全屏观看 >

根据题意,我们在找到值为 \\(7\\) 的节点后应该继续搜索,因此需要将记录解之后的 return 语句删除。图 13-4 对比了保留或删除 return 语句的搜索过程。

图 13-4   保留与删除 return 的搜索过程对比

相比基于前序遍历的代码实现,基于回溯算法框架的代码实现虽然显得啰唆,但通用性更好。实际上,许多回溯问题可以在该框架下解决。我们只需根据具体问题来定义 statechoices ,并实现框架中的各个方法即可。

","path":["第 13 章   回溯","13.1   回溯算法"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1314","level":2,"title":"13.1.4   常用术语","text":"

为了更清晰地分析算法问题,我们总结一下回溯算法中常用术语的含义,并对照例题三给出对应示例,如表 13-1 所示。

表 13-1   常见的回溯算法术语

名词 定义 例题三 解(solution) 解是满足问题特定条件的答案,可能有一个或多个 根节点到节点 \\(7\\) 的满足约束条件的所有路径 约束条件(constraint) 约束条件是问题中限制解的可行性的条件,通常用于剪枝 路径中不包含节点 \\(3\\) 状态(state) 状态表示问题在某一时刻的情况,包括已经做出的选择 当前已访问的节点路径,即 path 节点列表 尝试(attempt) 尝试是根据可用选择来探索解空间的过程,包括做出选择,更新状态,检查是否为解 递归访问左(右)子节点,将节点添加进 path ,判断节点的值是否为 \\(7\\) 回退(backtracking) 回退指遇到不满足约束条件的状态时,撤销前面做出的选择,回到上一个状态 当越过叶节点、结束节点访问、遇到值为 \\(3\\) 的节点时终止搜索,函数返回 剪枝(pruning) 剪枝是根据问题特性和约束条件避免无意义的搜索路径的方法,可提高搜索效率 当遇到值为 \\(3\\) 的节点时,则不再继续搜索

Tip

问题、解、状态等概念是通用的,在分治、回溯、动态规划、贪心等算法中都有涉及。

","path":["第 13 章   回溯","13.1   回溯算法"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1315","level":2,"title":"13.1.5   优点与局限性","text":"

回溯算法本质上是一种深度优先搜索算法,它尝试所有可能的解决方案直到找到满足条件的解。这种方法的优点在于能够找到所有可能的解决方案,而且在合理的剪枝操作下,具有很高的效率。

然而,在处理大规模或者复杂问题时,回溯算法的运行效率可能难以接受。

即便如此,回溯算法仍然是某些搜索问题和约束满足问题的最佳解决方案。对于这些问题,由于无法预测哪些选择可生成有效的解,因此我们必须对所有可能的选择进行遍历。在这种情况下,关键是如何优化效率,常见的效率优化方法有两种。

","path":["第 13 章   回溯","13.1   回溯算法"],"tags":[]},{"location":"chapter_backtracking/backtracking_algorithm/#1316","level":2,"title":"13.1.6   回溯典型例题","text":"

回溯算法可用于解决许多搜索问题、约束满足问题和组合优化问题。

搜索问题:这类问题的目标是找到满足特定条件的解决方案。

约束满足问题:这类问题的目标是找到满足所有约束条件的解。

组合优化问题:这类问题的目标是在一个组合空间中找到满足某些条件的最优解。

请注意,对于许多组合优化问题,回溯不是最优解决方案。

","path":["第 13 章   回溯","13.1   回溯算法"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/","level":1,"title":"13.4   n 皇后问题","text":"

Question

根据国际象棋的规则,皇后可以攻击与同处一行、一列或一条斜线上的棋子。给定 \\(n\\) 个皇后和一个 \\(n \\times n\\) 大小的棋盘,寻找使得所有皇后之间无法相互攻击的摆放方案。

如图 13-15 所示,当 \\(n = 4\\) 时,共可以找到两个解。从回溯算法的角度看,\\(n \\times n\\) 大小的棋盘共有 \\(n^2\\) 个格子,给出了所有的选择 choices 。在逐个放置皇后的过程中,棋盘状态在不断地变化,每个时刻的棋盘就是状态 state

图 13-15   4 皇后问题的解

图 13-16 展示了本题的三个约束条件:多个皇后不能在同一行、同一列、同一条对角线上。值得注意的是,对角线分为主对角线 \\ 和次对角线 / 两种。

图 13-16   n 皇后问题的约束条件

","path":["第 13 章   回溯","13.4   n 皇后问题"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#1","level":3,"title":"1.   逐行放置策略","text":"

皇后的数量和棋盘的行数都为 \\(n\\) ,因此我们容易得到一个推论:棋盘每行都允许且只允许放置一个皇后。

也就是说,我们可以采取逐行放置策略:从第一行开始,在每行放置一个皇后,直至最后一行结束。

图 13-17 所示为 4 皇后问题的逐行放置过程。受画幅限制,图 13-17 仅展开了第一行的其中一个搜索分支,并且将不满足列约束和对角线约束的方案都进行了剪枝。

图 13-17   逐行放置策略

从本质上看,逐行放置策略起到了剪枝的作用,它避免了同一行出现多个皇后的所有搜索分支。

","path":["第 13 章   回溯","13.4   n 皇后问题"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#2","level":3,"title":"2.   列与对角线剪枝","text":"

为了满足列约束,我们可以利用一个长度为 \\(n\\) 的布尔型数组 cols 记录每一列是否有皇后。在每次决定放置前,我们通过 cols 将已有皇后的列进行剪枝,并在回溯中动态更新 cols 的状态。

Tip

请注意,矩阵的起点位于左上角,其中行索引从上到下增加,列索引从左到右增加。

那么,如何处理对角线约束呢?设棋盘中某个格子的行列索引为 \\((row, col)\\) ,选定矩阵中的某条主对角线,我们发现该对角线上所有格子的行索引减列索引都相等,即主对角线上所有格子的 \\(row - col\\) 为恒定值。

也就是说,如果两个格子满足 \\(row_1 - col_1 = row_2 - col_2\\) ,则它们一定处在同一条主对角线上。利用该规律,我们可以借助图 13-18 所示的数组 diags1 记录每条主对角线上是否有皇后。

同理,次对角线上的所有格子的 \\(row + col\\) 是恒定值。我们同样也可以借助数组 diags2 来处理次对角线约束。

图 13-18   处理列约束和对角线约束

","path":["第 13 章   回溯","13.4   n 皇后问题"],"tags":[]},{"location":"chapter_backtracking/n_queens_problem/#3","level":3,"title":"3.   代码实现","text":"

请注意,\\(n\\) 维方阵中 \\(row - col\\) 的范围是 \\([-n + 1, n - 1]\\) ,\\(row + col\\) 的范围是 \\([0, 2n - 2]\\) ,所以主对角线和次对角线的数量都为 \\(2n - 1\\) ,即数组 diags1diags2 的长度都为 \\(2n - 1\\) 。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby n_queens.py
def backtrack(\n    row: int,\n    n: int,\n    state: list[list[str]],\n    res: list[list[list[str]]],\n    cols: list[bool],\n    diags1: list[bool],\n    diags2: list[bool],\n):\n    \"\"\"回溯算法:n 皇后\"\"\"\n    # 当放置完所有行时,记录解\n    if row == n:\n        res.append([list(row) for row in state])\n        return\n    # 遍历所有列\n    for col in range(n):\n        # 计算该格子对应的主对角线和次对角线\n        diag1 = row - col + n - 1\n        diag2 = row + col\n        # 剪枝:不允许该格子所在列、主对角线、次对角线上存在皇后\n        if not cols[col] and not diags1[diag1] and not diags2[diag2]:\n            # 尝试:将皇后放置在该格子\n            state[row][col] = \"Q\"\n            cols[col] = diags1[diag1] = diags2[diag2] = True\n            # 放置下一行\n            backtrack(row + 1, n, state, res, cols, diags1, diags2)\n            # 回退:将该格子恢复为空位\n            state[row][col] = \"#\"\n            cols[col] = diags1[diag1] = diags2[diag2] = False\n\ndef n_queens(n: int) -> list[list[list[str]]]:\n    \"\"\"求解 n 皇后\"\"\"\n    # 初始化 n*n 大小的棋盘,其中 'Q' 代表皇后,'#' 代表空位\n    state = [[\"#\" for _ in range(n)] for _ in range(n)]\n    cols = [False] * n  # 记录列是否有皇后\n    diags1 = [False] * (2 * n - 1)  # 记录主对角线上是否有皇后\n    diags2 = [False] * (2 * n - 1)  # 记录次对角线上是否有皇后\n    res = []\n    backtrack(0, n, state, res, cols, diags1, diags2)\n\n    return res\n
n_queens.cpp
/* 回溯算法:n 皇后 */\nvoid backtrack(int row, int n, vector<vector<string>> &state, vector<vector<vector<string>>> &res, vector<bool> &cols,\n               vector<bool> &diags1, vector<bool> &diags2) {\n    // 当放置完所有行时,记录解\n    if (row == n) {\n        res.push_back(state);\n        return;\n    }\n    // 遍历所有列\n    for (int col = 0; col < n; col++) {\n        // 计算该格子对应的主对角线和次对角线\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // 剪枝:不允许该格子所在列、主对角线、次对角线上存在皇后\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // 尝试:将皇后放置在该格子\n            state[row][col] = \"Q\";\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // 放置下一行\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // 回退:将该格子恢复为空位\n            state[row][col] = \"#\";\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* 求解 n 皇后 */\nvector<vector<vector<string>>> nQueens(int n) {\n    // 初始化 n*n 大小的棋盘,其中 'Q' 代表皇后,'#' 代表空位\n    vector<vector<string>> state(n, vector<string>(n, \"#\"));\n    vector<bool> cols(n, false);           // 记录列是否有皇后\n    vector<bool> diags1(2 * n - 1, false); // 记录主对角线上是否有皇后\n    vector<bool> diags2(2 * n - 1, false); // 记录次对角线上是否有皇后\n    vector<vector<vector<string>>> res;\n\n    backtrack(0, n, state, res, cols, diags1, diags2);\n\n    return res;\n}\n
n_queens.java
/* 回溯算法:n 皇后 */\nvoid backtrack(int row, int n, List<List<String>> state, List<List<List<String>>> res,\n        boolean[] cols, boolean[] diags1, boolean[] diags2) {\n    // 当放置完所有行时,记录解\n    if (row == n) {\n        List<List<String>> copyState = new ArrayList<>();\n        for (List<String> sRow : state) {\n            copyState.add(new ArrayList<>(sRow));\n        }\n        res.add(copyState);\n        return;\n    }\n    // 遍历所有列\n    for (int col = 0; col < n; col++) {\n        // 计算该格子对应的主对角线和次对角线\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // 剪枝:不允许该格子所在列、主对角线、次对角线上存在皇后\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // 尝试:将皇后放置在该格子\n            state.get(row).set(col, \"Q\");\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // 放置下一行\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // 回退:将该格子恢复为空位\n            state.get(row).set(col, \"#\");\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* 求解 n 皇后 */\nList<List<List<String>>> nQueens(int n) {\n    // 初始化 n*n 大小的棋盘,其中 'Q' 代表皇后,'#' 代表空位\n    List<List<String>> state = new ArrayList<>();\n    for (int i = 0; i < n; i++) {\n        List<String> row = new ArrayList<>();\n        for (int j = 0; j < n; j++) {\n            row.add(\"#\");\n        }\n        state.add(row);\n    }\n    boolean[] cols = new boolean[n]; // 记录列是否有皇后\n    boolean[] diags1 = new boolean[2 * n - 1]; // 记录主对角线上是否有皇后\n    boolean[] diags2 = new boolean[2 * n - 1]; // 记录次对角线上是否有皇后\n    List<List<List<String>>> res = new ArrayList<>();\n\n    backtrack(0, n, state, res, cols, diags1, diags2);\n\n    return res;\n}\n
n_queens.cs
/* 回溯算法:n 皇后 */\nvoid Backtrack(int row, int n, List<List<string>> state, List<List<List<string>>> res,\n        bool[] cols, bool[] diags1, bool[] diags2) {\n    // 当放置完所有行时,记录解\n    if (row == n) {\n        List<List<string>> copyState = [];\n        foreach (List<string> sRow in state) {\n            copyState.Add(new List<string>(sRow));\n        }\n        res.Add(copyState);\n        return;\n    }\n    // 遍历所有列\n    for (int col = 0; col < n; col++) {\n        // 计算该格子对应的主对角线和次对角线\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // 剪枝:不允许该格子所在列、主对角线、次对角线上存在皇后\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // 尝试:将皇后放置在该格子\n            state[row][col] = \"Q\";\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // 放置下一行\n            Backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // 回退:将该格子恢复为空位\n            state[row][col] = \"#\";\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* 求解 n 皇后 */\nList<List<List<string>>> NQueens(int n) {\n    // 初始化 n*n 大小的棋盘,其中 'Q' 代表皇后,'#' 代表空位\n    List<List<string>> state = [];\n    for (int i = 0; i < n; i++) {\n        List<string> row = [];\n        for (int j = 0; j < n; j++) {\n            row.Add(\"#\");\n        }\n        state.Add(row);\n    }\n    bool[] cols = new bool[n]; // 记录列是否有皇后\n    bool[] diags1 = new bool[2 * n - 1]; // 记录主对角线上是否有皇后\n    bool[] diags2 = new bool[2 * n - 1]; // 记录次对角线上是否有皇后\n    List<List<List<string>>> res = [];\n\n    Backtrack(0, n, state, res, cols, diags1, diags2);\n\n    return res;\n}\n
n_queens.go
/* 回溯算法:n 皇后 */\nfunc backtrack(row, n int, state *[][]string, res *[][][]string, cols, diags1, diags2 *[]bool) {\n    // 当放置完所有行时,记录解\n    if row == n {\n        newState := make([][]string, len(*state))\n        for i, _ := range newState {\n            newState[i] = make([]string, len((*state)[0]))\n            copy(newState[i], (*state)[i])\n\n        }\n        *res = append(*res, newState)\n        return\n    }\n    // 遍历所有列\n    for col := 0; col < n; col++ {\n        // 计算该格子对应的主对角线和次对角线\n        diag1 := row - col + n - 1\n        diag2 := row + col\n        // 剪枝:不允许该格子所在列、主对角线、次对角线上存在皇后\n        if !(*cols)[col] && !(*diags1)[diag1] && !(*diags2)[diag2] {\n            // 尝试:将皇后放置在该格子\n            (*state)[row][col] = \"Q\"\n            (*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = true, true, true\n            // 放置下一行\n            backtrack(row+1, n, state, res, cols, diags1, diags2)\n            // 回退:将该格子恢复为空位\n            (*state)[row][col] = \"#\"\n            (*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = false, false, false\n        }\n    }\n}\n\n/* 求解 n 皇后 */\nfunc nQueens(n int) [][][]string {\n    // 初始化 n*n 大小的棋盘,其中 'Q' 代表皇后,'#' 代表空位\n    state := make([][]string, n)\n    for i := 0; i < n; i++ {\n        row := make([]string, n)\n        for i := 0; i < n; i++ {\n            row[i] = \"#\"\n        }\n        state[i] = row\n    }\n    // 记录列是否有皇后\n    cols := make([]bool, n)\n    diags1 := make([]bool, 2*n-1)\n    diags2 := make([]bool, 2*n-1)\n    res := make([][][]string, 0)\n    backtrack(0, n, &state, &res, &cols, &diags1, &diags2)\n    return res\n}\n
n_queens.swift
/* 回溯算法:n 皇后 */\nfunc backtrack(row: Int, n: Int, state: inout [[String]], res: inout [[[String]]], cols: inout [Bool], diags1: inout [Bool], diags2: inout [Bool]) {\n    // 当放置完所有行时,记录解\n    if row == n {\n        res.append(state)\n        return\n    }\n    // 遍历所有列\n    for col in 0 ..< n {\n        // 计算该格子对应的主对角线和次对角线\n        let diag1 = row - col + n - 1\n        let diag2 = row + col\n        // 剪枝:不允许该格子所在列、主对角线、次对角线上存在皇后\n        if !cols[col] && !diags1[diag1] && !diags2[diag2] {\n            // 尝试:将皇后放置在该格子\n            state[row][col] = \"Q\"\n            cols[col] = true\n            diags1[diag1] = true\n            diags2[diag2] = true\n            // 放置下一行\n            backtrack(row: row + 1, n: n, state: &state, res: &res, cols: &cols, diags1: &diags1, diags2: &diags2)\n            // 回退:将该格子恢复为空位\n            state[row][col] = \"#\"\n            cols[col] = false\n            diags1[diag1] = false\n            diags2[diag2] = false\n        }\n    }\n}\n\n/* 求解 n 皇后 */\nfunc nQueens(n: Int) -> [[[String]]] {\n    // 初始化 n*n 大小的棋盘,其中 'Q' 代表皇后,'#' 代表空位\n    var state = Array(repeating: Array(repeating: \"#\", count: n), count: n)\n    var cols = Array(repeating: false, count: n) // 记录列是否有皇后\n    var diags1 = Array(repeating: false, count: 2 * n - 1) // 记录主对角线上是否有皇后\n    var diags2 = Array(repeating: false, count: 2 * n - 1) // 记录次对角线上是否有皇后\n    var res: [[[String]]] = []\n\n    backtrack(row: 0, n: n, state: &state, res: &res, cols: &cols, diags1: &diags1, diags2: &diags2)\n\n    return res\n}\n
n_queens.js
/* 回溯算法:n 皇后 */\nfunction backtrack(row, n, state, res, cols, diags1, diags2) {\n    // 当放置完所有行时,记录解\n    if (row === n) {\n        res.push(state.map((row) => row.slice()));\n        return;\n    }\n    // 遍历所有列\n    for (let col = 0; col < n; col++) {\n        // 计算该格子对应的主对角线和次对角线\n        const diag1 = row - col + n - 1;\n        const diag2 = row + col;\n        // 剪枝:不允许该格子所在列、主对角线、次对角线上存在皇后\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // 尝试:将皇后放置在该格子\n            state[row][col] = 'Q';\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // 放置下一行\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // 回退:将该格子恢复为空位\n            state[row][col] = '#';\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* 求解 n 皇后 */\nfunction nQueens(n) {\n    // 初始化 n*n 大小的棋盘,其中 'Q' 代表皇后,'#' 代表空位\n    const state = Array.from({ length: n }, () => Array(n).fill('#'));\n    const cols = Array(n).fill(false); // 记录列是否有皇后\n    const diags1 = Array(2 * n - 1).fill(false); // 记录主对角线上是否有皇后\n    const diags2 = Array(2 * n - 1).fill(false); // 记录次对角线上是否有皇后\n    const res = [];\n\n    backtrack(0, n, state, res, cols, diags1, diags2);\n    return res;\n}\n
n_queens.ts
/* 回溯算法:n 皇后 */\nfunction backtrack(\n    row: number,\n    n: number,\n    state: string[][],\n    res: string[][][],\n    cols: boolean[],\n    diags1: boolean[],\n    diags2: boolean[]\n): void {\n    // 当放置完所有行时,记录解\n    if (row === n) {\n        res.push(state.map((row) => row.slice()));\n        return;\n    }\n    // 遍历所有列\n    for (let col = 0; col < n; col++) {\n        // 计算该格子对应的主对角线和次对角线\n        const diag1 = row - col + n - 1;\n        const diag2 = row + col;\n        // 剪枝:不允许该格子所在列、主对角线、次对角线上存在皇后\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // 尝试:将皇后放置在该格子\n            state[row][col] = 'Q';\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // 放置下一行\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // 回退:将该格子恢复为空位\n            state[row][col] = '#';\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* 求解 n 皇后 */\nfunction nQueens(n: number): string[][][] {\n    // 初始化 n*n 大小的棋盘,其中 'Q' 代表皇后,'#' 代表空位\n    const state = Array.from({ length: n }, () => Array(n).fill('#'));\n    const cols = Array(n).fill(false); // 记录列是否有皇后\n    const diags1 = Array(2 * n - 1).fill(false); // 记录主对角线上是否有皇后\n    const diags2 = Array(2 * n - 1).fill(false); // 记录次对角线上是否有皇后\n    const res: string[][][] = [];\n\n    backtrack(0, n, state, res, cols, diags1, diags2);\n    return res;\n}\n
n_queens.dart
/* 回溯算法:n 皇后 */\nvoid backtrack(\n  int row,\n  int n,\n  List<List<String>> state,\n  List<List<List<String>>> res,\n  List<bool> cols,\n  List<bool> diags1,\n  List<bool> diags2,\n) {\n  // 当放置完所有行时,记录解\n  if (row == n) {\n    List<List<String>> copyState = [];\n    for (List<String> sRow in state) {\n      copyState.add(List.from(sRow));\n    }\n    res.add(copyState);\n    return;\n  }\n  // 遍历所有列\n  for (int col = 0; col < n; col++) {\n    // 计算该格子对应的主对角线和次对角线\n    int diag1 = row - col + n - 1;\n    int diag2 = row + col;\n    // 剪枝:不允许该格子所在列、主对角线、次对角线上存在皇后\n    if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n      // 尝试:将皇后放置在该格子\n      state[row][col] = \"Q\";\n      cols[col] = true;\n      diags1[diag1] = true;\n      diags2[diag2] = true;\n      // 放置下一行\n      backtrack(row + 1, n, state, res, cols, diags1, diags2);\n      // 回退:将该格子恢复为空位\n      state[row][col] = \"#\";\n      cols[col] = false;\n      diags1[diag1] = false;\n      diags2[diag2] = false;\n    }\n  }\n}\n\n/* 求解 n 皇后 */\nList<List<List<String>>> nQueens(int n) {\n  // 初始化 n*n 大小的棋盘,其中 'Q' 代表皇后,'#' 代表空位\n  List<List<String>> state = List.generate(n, (index) => List.filled(n, \"#\"));\n  List<bool> cols = List.filled(n, false); // 记录列是否有皇后\n  List<bool> diags1 = List.filled(2 * n - 1, false); // 记录主对角线上是否有皇后\n  List<bool> diags2 = List.filled(2 * n - 1, false); // 记录次对角线上是否有皇后\n  List<List<List<String>>> res = [];\n\n  backtrack(0, n, state, res, cols, diags1, diags2);\n\n  return res;\n}\n
n_queens.rs
/* 回溯算法:n 皇后 */\nfn backtrack(\n    row: usize,\n    n: usize,\n    state: &mut Vec<Vec<String>>,\n    res: &mut Vec<Vec<Vec<String>>>,\n    cols: &mut [bool],\n    diags1: &mut [bool],\n    diags2: &mut [bool],\n) {\n    // 当放置完所有行时,记录解\n    if row == n {\n        res.push(state.clone());\n        return;\n    }\n    // 遍历所有列\n    for col in 0..n {\n        // 计算该格子对应的主对角线和次对角线\n        let diag1 = row + n - 1 - col;\n        let diag2 = row + col;\n        // 剪枝:不允许该格子所在列、主对角线、次对角线上存在皇后\n        if !cols[col] && !diags1[diag1] && !diags2[diag2] {\n            // 尝试:将皇后放置在该格子\n            state[row][col] = \"Q\".into();\n            (cols[col], diags1[diag1], diags2[diag2]) = (true, true, true);\n            // 放置下一行\n            backtrack(row + 1, n, state, res, cols, diags1, diags2);\n            // 回退:将该格子恢复为空位\n            state[row][col] = \"#\".into();\n            (cols[col], diags1[diag1], diags2[diag2]) = (false, false, false);\n        }\n    }\n}\n\n/* 求解 n 皇后 */\nfn n_queens(n: usize) -> Vec<Vec<Vec<String>>> {\n    // 初始化 n*n 大小的棋盘,其中 'Q' 代表皇后,'#' 代表空位\n    let mut state: Vec<Vec<String>> = vec![vec![\"#\".to_string(); n]; n];\n    let mut cols = vec![false; n]; // 记录列是否有皇后\n    let mut diags1 = vec![false; 2 * n - 1]; // 记录主对角线上是否有皇后\n    let mut diags2 = vec![false; 2 * n - 1]; // 记录次对角线上是否有皇后\n    let mut res: Vec<Vec<Vec<String>>> = Vec::new();\n\n    backtrack(\n        0,\n        n,\n        &mut state,\n        &mut res,\n        &mut cols,\n        &mut diags1,\n        &mut diags2,\n    );\n\n    res\n}\n
n_queens.c
/* 回溯算法:n 皇后 */\nvoid backtrack(int row, int n, char state[MAX_SIZE][MAX_SIZE], char ***res, int *resSize, bool cols[MAX_SIZE],\n               bool diags1[2 * MAX_SIZE - 1], bool diags2[2 * MAX_SIZE - 1]) {\n    // 当放置完所有行时,记录解\n    if (row == n) {\n        res[*resSize] = (char **)malloc(sizeof(char *) * n);\n        for (int i = 0; i < n; ++i) {\n            res[*resSize][i] = (char *)malloc(sizeof(char) * (n + 1));\n            strcpy(res[*resSize][i], state[i]);\n        }\n        (*resSize)++;\n        return;\n    }\n    // 遍历所有列\n    for (int col = 0; col < n; col++) {\n        // 计算该格子对应的主对角线和次对角线\n        int diag1 = row - col + n - 1;\n        int diag2 = row + col;\n        // 剪枝:不允许该格子所在列、主对角线、次对角线上存在皇后\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // 尝试:将皇后放置在该格子\n            state[row][col] = 'Q';\n            cols[col] = diags1[diag1] = diags2[diag2] = true;\n            // 放置下一行\n            backtrack(row + 1, n, state, res, resSize, cols, diags1, diags2);\n            // 回退:将该格子恢复为空位\n            state[row][col] = '#';\n            cols[col] = diags1[diag1] = diags2[diag2] = false;\n        }\n    }\n}\n\n/* 求解 n 皇后 */\nchar ***nQueens(int n, int *returnSize) {\n    char state[MAX_SIZE][MAX_SIZE];\n    // 初始化 n*n 大小的棋盘,其中 'Q' 代表皇后,'#' 代表空位\n    for (int i = 0; i < n; ++i) {\n        for (int j = 0; j < n; ++j) {\n            state[i][j] = '#';\n        }\n        state[i][n] = '\\0';\n    }\n    bool cols[MAX_SIZE] = {false};           // 记录列是否有皇后\n    bool diags1[2 * MAX_SIZE - 1] = {false}; // 记录主对角线上是否有皇后\n    bool diags2[2 * MAX_SIZE - 1] = {false}; // 记录次对角线上是否有皇后\n\n    char ***res = (char ***)malloc(sizeof(char **) * MAX_SIZE);\n    *returnSize = 0;\n    backtrack(0, n, state, res, returnSize, cols, diags1, diags2);\n    return res;\n}\n
n_queens.kt
/* 回溯算法:n 皇后 */\nfun backtrack(\n    row: Int,\n    n: Int,\n    state: MutableList<MutableList<String>>,\n    res: MutableList<MutableList<MutableList<String>>?>,\n    cols: BooleanArray,\n    diags1: BooleanArray,\n    diags2: BooleanArray\n) {\n    // 当放置完所有行时,记录解\n    if (row == n) {\n        val copyState = mutableListOf<MutableList<String>>()\n        for (sRow in state) {\n            copyState.add(sRow.toMutableList())\n        }\n        res.add(copyState)\n        return\n    }\n    // 遍历所有列\n    for (col in 0..<n) {\n        // 计算该格子对应的主对角线和次对角线\n        val diag1 = row - col + n - 1\n        val diag2 = row + col\n        // 剪枝:不允许该格子所在列、主对角线、次对角线上存在皇后\n        if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {\n            // 尝试:将皇后放置在该格子\n            state[row][col] = \"Q\"\n            diags2[diag2] = true\n            diags1[diag1] = diags2[diag2]\n            cols[col] = diags1[diag1]\n            // 放置下一行\n            backtrack(row + 1, n, state, res, cols, diags1, diags2)\n            // 回退:将该格子恢复为空位\n            state[row][col] = \"#\"\n            diags2[diag2] = false\n            diags1[diag1] = diags2[diag2]\n            cols[col] = diags1[diag1]\n        }\n    }\n}\n\n/* 求解 n 皇后 */\nfun nQueens(n: Int): MutableList<MutableList<MutableList<String>>?> {\n    // 初始化 n*n 大小的棋盘,其中 'Q' 代表皇后,'#' 代表空位\n    val state = mutableListOf<MutableList<String>>()\n    for (i in 0..<n) {\n        val row = mutableListOf<String>()\n        for (j in 0..<n) {\n            row.add(\"#\")\n        }\n        state.add(row)\n    }\n    val cols = BooleanArray(n) // 记录列是否有皇后\n    val diags1 = BooleanArray(2 * n - 1) // 记录主对角线上是否有皇后\n    val diags2 = BooleanArray(2 * n - 1) // 记录次对角线上是否有皇后\n    val res = mutableListOf<MutableList<MutableList<String>>?>()\n\n    backtrack(0, n, state, res, cols, diags1, diags2)\n\n    return res\n}\n
n_queens.rb
### 回溯算法:n 皇后 ###\ndef backtrack(row, n, state, res, cols, diags1, diags2)\n  # 当放置完所有行时,记录解\n  if row == n\n    res << state.map { |row| row.dup }\n    return\n  end\n\n  # 遍历所有列\n  for col in 0...n\n    # 计算该格子对应的主对角线和次对角线\n    diag1 = row - col + n - 1\n    diag2 = row + col\n    # 剪枝:不允许该格子所在列、主对角线、次对角线上存在皇后\n    if !cols[col] && !diags1[diag1] && !diags2[diag2]\n      # 尝试:将皇后放置在该格子\n      state[row][col] = \"Q\"\n      cols[col] = diags1[diag1] = diags2[diag2] = true\n      # 放置下一行\n      backtrack(row + 1, n, state, res, cols, diags1, diags2)\n      # 回退:将该格子恢复为空位\n      state[row][col] = \"#\"\n      cols[col] = diags1[diag1] = diags2[diag2] = false\n    end\n  end\nend\n\n### 求解 n 皇后 ###\ndef n_queens(n)\n  # 初始化 n*n 大小的棋盘,其中 'Q' 代表皇后,'#' 代表空位\n  state = Array.new(n) { Array.new(n, \"#\") }\n  cols = Array.new(n, false) # 记录列是否有皇后\n  diags1 = Array.new(2 * n - 1, false) # 记录主对角线上是否有皇后\n  diags2 = Array.new(2 * n - 1, false) # 记录次对角线上是否有皇后\n  res = []\n  backtrack(0, n, state, res, cols, diags1, diags2)\n\n  res\nend\n
可视化运行

全屏观看 >

逐行放置 \\(n\\) 次,考虑列约束,则从第一行到最后一行分别有 \\(n\\)、\\(n-1\\)、\\(\\dots\\)、\\(2\\)、\\(1\\) 个选择,使用 \\(O(n!)\\) 时间。当记录解时,需要复制矩阵 state 并添加进 res ,复制操作使用 \\(O(n^2)\\) 时间。因此,总体时间复杂度为 \\(O(n! \\cdot n^2)\\) 。实际上,根据对角线约束的剪枝也能够大幅缩小搜索空间,因而搜索效率往往优于以上时间复杂度。

数组 state 使用 \\(O(n^2)\\) 空间,数组 colsdiags1diags2 皆使用 \\(O(n)\\) 空间。最大递归深度为 \\(n\\) ,使用 \\(O(n)\\) 栈帧空间。因此,空间复杂度为 \\(O(n^2)\\) 。

","path":["第 13 章   回溯","13.4   n 皇后问题"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/","level":1,"title":"13.2   全排列问题","text":"

全排列问题是回溯算法的一个典型应用。它的定义是在给定一个集合(如一个数组或字符串)的情况下,找出其中元素的所有可能的排列。

表 13-2 列举了几个示例数据,包括输入数组和对应的所有排列。

表 13-2   全排列示例

输入数组 所有排列 \\([1]\\) \\([1]\\) \\([1, 2]\\) \\([1, 2], [2, 1]\\) \\([1, 2, 3]\\) \\([1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]\\)","path":["第 13 章   回溯","13.2   全排列问题"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1321","level":2,"title":"13.2.1   无相等元素的情况","text":"

Question

输入一个整数数组,其中不包含重复元素,返回所有可能的排列。

从回溯算法的角度看,我们可以把生成排列的过程想象成一系列选择的结果。假设输入数组为 \\([1, 2, 3]\\) ,如果我们先选择 \\(1\\) ,再选择 \\(3\\) ,最后选择 \\(2\\) ,则获得排列 \\([1, 3, 2]\\) 。回退表示撤销一个选择,之后继续尝试其他选择。

从回溯代码的角度看,候选集合 choices 是输入数组中的所有元素,状态 state 是直至目前已被选择的元素。请注意,每个元素只允许被选择一次,因此 state 中的所有元素都应该是唯一的。

如图 13-5 所示,我们可以将搜索过程展开成一棵递归树,树中的每个节点代表当前状态 state 。从根节点开始,经过三轮选择后到达叶节点,每个叶节点都对应一个排列。

图 13-5   全排列的递归树

","path":["第 13 章   回溯","13.2   全排列问题"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1","level":3,"title":"1.   重复选择剪枝","text":"

为了实现每个元素只被选择一次,我们考虑引入一个布尔型数组 selected ,其中 selected[i] 表示 choices[i] 是否已被选择,并基于它实现以下剪枝操作。

如图 13-6 所示,假设我们第一轮选择 1 ,第二轮选择 3 ,第三轮选择 2 ,则需要在第二轮剪掉元素 1 的分支,在第三轮剪掉元素 1 和元素 3 的分支。

图 13-6   全排列剪枝示例

观察图 13-6 发现,该剪枝操作将搜索空间大小从 \\(O(n^n)\\) 减小至 \\(O(n!)\\) 。

","path":["第 13 章   回溯","13.2   全排列问题"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#2","level":3,"title":"2.   代码实现","text":"

想清楚以上信息之后,我们就可以在框架代码中做“完形填空”了。为了缩短整体代码,我们不单独实现框架代码中的各个函数,而是将它们展开在 backtrack() 函数中:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby permutations_i.py
def backtrack(\n    state: list[int], choices: list[int], selected: list[bool], res: list[list[int]]\n):\n    \"\"\"回溯算法:全排列 I\"\"\"\n    # 当状态长度等于元素数量时,记录解\n    if len(state) == len(choices):\n        res.append(list(state))\n        return\n    # 遍历所有选择\n    for i, choice in enumerate(choices):\n        # 剪枝:不允许重复选择元素\n        if not selected[i]:\n            # 尝试:做出选择,更新状态\n            selected[i] = True\n            state.append(choice)\n            # 进行下一轮选择\n            backtrack(state, choices, selected, res)\n            # 回退:撤销选择,恢复到之前的状态\n            selected[i] = False\n            state.pop()\n\ndef permutations_i(nums: list[int]) -> list[list[int]]:\n    \"\"\"全排列 I\"\"\"\n    res = []\n    backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res)\n    return res\n
permutations_i.cpp
/* 回溯算法:全排列 I */\nvoid backtrack(vector<int> &state, const vector<int> &choices, vector<bool> &selected, vector<vector<int>> &res) {\n    // 当状态长度等于元素数量时,记录解\n    if (state.size() == choices.size()) {\n        res.push_back(state);\n        return;\n    }\n    // 遍历所有选择\n    for (int i = 0; i < choices.size(); i++) {\n        int choice = choices[i];\n        // 剪枝:不允许重复选择元素\n        if (!selected[i]) {\n            // 尝试:做出选择,更新状态\n            selected[i] = true;\n            state.push_back(choice);\n            // 进行下一轮选择\n            backtrack(state, choices, selected, res);\n            // 回退:撤销选择,恢复到之前的状态\n            selected[i] = false;\n            state.pop_back();\n        }\n    }\n}\n\n/* 全排列 I */\nvector<vector<int>> permutationsI(vector<int> nums) {\n    vector<int> state;\n    vector<bool> selected(nums.size(), false);\n    vector<vector<int>> res;\n    backtrack(state, nums, selected, res);\n    return res;\n}\n
permutations_i.java
/* 回溯算法:全排列 I */\nvoid backtrack(List<Integer> state, int[] choices, boolean[] selected, List<List<Integer>> res) {\n    // 当状态长度等于元素数量时,记录解\n    if (state.size() == choices.length) {\n        res.add(new ArrayList<Integer>(state));\n        return;\n    }\n    // 遍历所有选择\n    for (int i = 0; i < choices.length; i++) {\n        int choice = choices[i];\n        // 剪枝:不允许重复选择元素\n        if (!selected[i]) {\n            // 尝试:做出选择,更新状态\n            selected[i] = true;\n            state.add(choice);\n            // 进行下一轮选择\n            backtrack(state, choices, selected, res);\n            // 回退:撤销选择,恢复到之前的状态\n            selected[i] = false;\n            state.remove(state.size() - 1);\n        }\n    }\n}\n\n/* 全排列 I */\nList<List<Integer>> permutationsI(int[] nums) {\n    List<List<Integer>> res = new ArrayList<List<Integer>>();\n    backtrack(new ArrayList<Integer>(), nums, new boolean[nums.length], res);\n    return res;\n}\n
permutations_i.cs
/* 回溯算法:全排列 I */\nvoid Backtrack(List<int> state, int[] choices, bool[] selected, List<List<int>> res) {\n    // 当状态长度等于元素数量时,记录解\n    if (state.Count == choices.Length) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // 遍历所有选择\n    for (int i = 0; i < choices.Length; i++) {\n        int choice = choices[i];\n        // 剪枝:不允许重复选择元素\n        if (!selected[i]) {\n            // 尝试:做出选择,更新状态\n            selected[i] = true;\n            state.Add(choice);\n            // 进行下一轮选择\n            Backtrack(state, choices, selected, res);\n            // 回退:撤销选择,恢复到之前的状态\n            selected[i] = false;\n            state.RemoveAt(state.Count - 1);\n        }\n    }\n}\n\n/* 全排列 I */\nList<List<int>> PermutationsI(int[] nums) {\n    List<List<int>> res = [];\n    Backtrack([], nums, new bool[nums.Length], res);\n    return res;\n}\n
permutations_i.go
/* 回溯算法:全排列 I */\nfunc backtrackI(state *[]int, choices *[]int, selected *[]bool, res *[][]int) {\n    // 当状态长度等于元素数量时,记录解\n    if len(*state) == len(*choices) {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n    }\n    // 遍历所有选择\n    for i := 0; i < len(*choices); i++ {\n        choice := (*choices)[i]\n        // 剪枝:不允许重复选择元素\n        if !(*selected)[i] {\n            // 尝试:做出选择,更新状态\n            (*selected)[i] = true\n            *state = append(*state, choice)\n            // 进行下一轮选择\n            backtrackI(state, choices, selected, res)\n            // 回退:撤销选择,恢复到之前的状态\n            (*selected)[i] = false\n            *state = (*state)[:len(*state)-1]\n        }\n    }\n}\n\n/* 全排列 I */\nfunc permutationsI(nums []int) [][]int {\n    res := make([][]int, 0)\n    state := make([]int, 0)\n    selected := make([]bool, len(nums))\n    backtrackI(&state, &nums, &selected, &res)\n    return res\n}\n
permutations_i.swift
/* 回溯算法:全排列 I */\nfunc backtrack(state: inout [Int], choices: [Int], selected: inout [Bool], res: inout [[Int]]) {\n    // 当状态长度等于元素数量时,记录解\n    if state.count == choices.count {\n        res.append(state)\n        return\n    }\n    // 遍历所有选择\n    for (i, choice) in choices.enumerated() {\n        // 剪枝:不允许重复选择元素\n        if !selected[i] {\n            // 尝试:做出选择,更新状态\n            selected[i] = true\n            state.append(choice)\n            // 进行下一轮选择\n            backtrack(state: &state, choices: choices, selected: &selected, res: &res)\n            // 回退:撤销选择,恢复到之前的状态\n            selected[i] = false\n            state.removeLast()\n        }\n    }\n}\n\n/* 全排列 I */\nfunc permutationsI(nums: [Int]) -> [[Int]] {\n    var state: [Int] = []\n    var selected = Array(repeating: false, count: nums.count)\n    var res: [[Int]] = []\n    backtrack(state: &state, choices: nums, selected: &selected, res: &res)\n    return res\n}\n
permutations_i.js
/* 回溯算法:全排列 I */\nfunction backtrack(state, choices, selected, res) {\n    // 当状态长度等于元素数量时,记录解\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // 遍历所有选择\n    choices.forEach((choice, i) => {\n        // 剪枝:不允许重复选择元素\n        if (!selected[i]) {\n            // 尝试:做出选择,更新状态\n            selected[i] = true;\n            state.push(choice);\n            // 进行下一轮选择\n            backtrack(state, choices, selected, res);\n            // 回退:撤销选择,恢复到之前的状态\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* 全排列 I */\nfunction permutationsI(nums) {\n    const res = [];\n    backtrack([], nums, Array(nums.length).fill(false), res);\n    return res;\n}\n
permutations_i.ts
/* 回溯算法:全排列 I */\nfunction backtrack(\n    state: number[],\n    choices: number[],\n    selected: boolean[],\n    res: number[][]\n): void {\n    // 当状态长度等于元素数量时,记录解\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // 遍历所有选择\n    choices.forEach((choice, i) => {\n        // 剪枝:不允许重复选择元素\n        if (!selected[i]) {\n            // 尝试:做出选择,更新状态\n            selected[i] = true;\n            state.push(choice);\n            // 进行下一轮选择\n            backtrack(state, choices, selected, res);\n            // 回退:撤销选择,恢复到之前的状态\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* 全排列 I */\nfunction permutationsI(nums: number[]): number[][] {\n    const res: number[][] = [];\n    backtrack([], nums, Array(nums.length).fill(false), res);\n    return res;\n}\n
permutations_i.dart
/* 回溯算法:全排列 I */\nvoid backtrack(\n  List<int> state,\n  List<int> choices,\n  List<bool> selected,\n  List<List<int>> res,\n) {\n  // 当状态长度等于元素数量时,记录解\n  if (state.length == choices.length) {\n    res.add(List.from(state));\n    return;\n  }\n  // 遍历所有选择\n  for (int i = 0; i < choices.length; i++) {\n    int choice = choices[i];\n    // 剪枝:不允许重复选择元素\n    if (!selected[i]) {\n      // 尝试:做出选择,更新状态\n      selected[i] = true;\n      state.add(choice);\n      // 进行下一轮选择\n      backtrack(state, choices, selected, res);\n      // 回退:撤销选择,恢复到之前的状态\n      selected[i] = false;\n      state.removeLast();\n    }\n  }\n}\n\n/* 全排列 I */\nList<List<int>> permutationsI(List<int> nums) {\n  List<List<int>> res = [];\n  backtrack([], nums, List.filled(nums.length, false), res);\n  return res;\n}\n
permutations_i.rs
/* 回溯算法:全排列 I */\nfn backtrack(mut state: Vec<i32>, choices: &[i32], selected: &mut [bool], res: &mut Vec<Vec<i32>>) {\n    // 当状态长度等于元素数量时,记录解\n    if state.len() == choices.len() {\n        res.push(state);\n        return;\n    }\n    // 遍历所有选择\n    for i in 0..choices.len() {\n        let choice = choices[i];\n        // 剪枝:不允许重复选择元素\n        if !selected[i] {\n            // 尝试:做出选择,更新状态\n            selected[i] = true;\n            state.push(choice);\n            // 进行下一轮选择\n            backtrack(state.clone(), choices, selected, res);\n            // 回退:撤销选择,恢复到之前的状态\n            selected[i] = false;\n            state.pop();\n        }\n    }\n}\n\n/* 全排列 I */\nfn permutations_i(nums: &mut [i32]) -> Vec<Vec<i32>> {\n    let mut res = Vec::new(); // 状态(子集)\n    backtrack(Vec::new(), nums, &mut vec![false; nums.len()], &mut res);\n    res\n}\n
permutations_i.c
/* 回溯算法:全排列 I */\nvoid backtrack(int *state, int stateSize, int *choices, int choicesSize, bool *selected, int **res, int *resSize) {\n    // 当状态长度等于元素数量时,记录解\n    if (stateSize == choicesSize) {\n        res[*resSize] = (int *)malloc(choicesSize * sizeof(int));\n        for (int i = 0; i < choicesSize; i++) {\n            res[*resSize][i] = state[i];\n        }\n        (*resSize)++;\n        return;\n    }\n    // 遍历所有选择\n    for (int i = 0; i < choicesSize; i++) {\n        int choice = choices[i];\n        // 剪枝:不允许重复选择元素\n        if (!selected[i]) {\n            // 尝试:做出选择,更新状态\n            selected[i] = true;\n            state[stateSize] = choice;\n            // 进行下一轮选择\n            backtrack(state, stateSize + 1, choices, choicesSize, selected, res, resSize);\n            // 回退:撤销选择,恢复到之前的状态\n            selected[i] = false;\n        }\n    }\n}\n\n/* 全排列 I */\nint **permutationsI(int *nums, int numsSize, int *returnSize) {\n    int *state = (int *)malloc(numsSize * sizeof(int));\n    bool *selected = (bool *)malloc(numsSize * sizeof(bool));\n    for (int i = 0; i < numsSize; i++) {\n        selected[i] = false;\n    }\n    int **res = (int **)malloc(MAX_SIZE * sizeof(int *));\n    *returnSize = 0;\n\n    backtrack(state, 0, nums, numsSize, selected, res, returnSize);\n\n    free(state);\n    free(selected);\n\n    return res;\n}\n
permutations_i.kt
/* 回溯算法:全排列 I */\nfun backtrack(\n    state: MutableList<Int>,\n    choices: IntArray,\n    selected: BooleanArray,\n    res: MutableList<MutableList<Int>?>\n) {\n    // 当状态长度等于元素数量时,记录解\n    if (state.size == choices.size) {\n        res.add(state.toMutableList())\n        return\n    }\n    // 遍历所有选择\n    for (i in choices.indices) {\n        val choice = choices[i]\n        // 剪枝:不允许重复选择元素\n        if (!selected[i]) {\n            // 尝试:做出选择,更新状态\n            selected[i] = true\n            state.add(choice)\n            // 进行下一轮选择\n            backtrack(state, choices, selected, res)\n            // 回退:撤销选择,恢复到之前的状态\n            selected[i] = false\n            state.removeAt(state.size - 1)\n        }\n    }\n}\n\n/* 全排列 I */\nfun permutationsI(nums: IntArray): MutableList<MutableList<Int>?> {\n    val res = mutableListOf<MutableList<Int>?>()\n    backtrack(mutableListOf(), nums, BooleanArray(nums.size), res)\n    return res\n}\n
permutations_i.rb
### 回溯算法:全排列 I ###\ndef backtrack(state, choices, selected, res)\n  # 当状态长度等于元素数量时,记录解\n  if state.length == choices.length\n    res << state.dup\n    return\n  end\n\n  # 遍历所有选择\n  choices.each_with_index do |choice, i|\n    # 剪枝:不允许重复选择元素\n    unless selected[i]\n      # 尝试:做出选择,更新状态\n      selected[i] = true\n      state << choice\n      # 进行下一轮选择\n      backtrack(state, choices, selected, res)\n      # 回退:撤销选择,恢复到之前的状态\n      selected[i] = false\n      state.pop\n    end\n  end\nend\n\n### 全排列 I ###\ndef permutations_i(nums)\n  res = []\n  backtrack([], nums, Array.new(nums.length, false), res)\n  res\nend\n
可视化运行

全屏观看 >

","path":["第 13 章   回溯","13.2   全排列问题"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1322","level":2,"title":"13.2.2   考虑相等元素的情况","text":"

Question

输入一个整数数组,数组中可能包含重复元素,返回所有不重复的排列。

假设输入数组为 \\([1, 1, 2]\\) 。为了方便区分两个重复元素 \\(1\\) ,我们将第二个 \\(1\\) 记为 \\(\\hat{1}\\) 。

如图 13-7 所示,上述方法生成的排列有一半是重复的。

图 13-7   重复排列

那么如何去除重复的排列呢?最直接地,考虑借助一个哈希集合,直接对排列结果进行去重。然而这样做不够优雅,因为生成重复排列的搜索分支没有必要,应当提前识别并剪枝,这样可以进一步提升算法效率。

","path":["第 13 章   回溯","13.2   全排列问题"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#1_1","level":3,"title":"1.   相等元素剪枝","text":"

观察图 13-8 ,在第一轮中,选择 \\(1\\) 或选择 \\(\\hat{1}\\) 是等价的,在这两个选择之下生成的所有排列都是重复的。因此应该把 \\(\\hat{1}\\) 剪枝。

同理,在第一轮选择 \\(2\\) 之后,第二轮选择中的 \\(1\\) 和 \\(\\hat{1}\\) 也会产生重复分支,因此也应将第二轮的 \\(\\hat{1}\\) 剪枝。

从本质上看,我们的目标是在某一轮选择中,保证多个相等的元素仅被选择一次。

图 13-8   重复排列剪枝

","path":["第 13 章   回溯","13.2   全排列问题"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#2_1","level":3,"title":"2.   代码实现","text":"

在上一题的代码的基础上,我们考虑在每一轮选择中开启一个哈希集合 duplicated ,用于记录该轮中已经尝试过的元素,并将重复元素剪枝:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby permutations_ii.py
def backtrack(\n    state: list[int], choices: list[int], selected: list[bool], res: list[list[int]]\n):\n    \"\"\"回溯算法:全排列 II\"\"\"\n    # 当状态长度等于元素数量时,记录解\n    if len(state) == len(choices):\n        res.append(list(state))\n        return\n    # 遍历所有选择\n    duplicated = set[int]()\n    for i, choice in enumerate(choices):\n        # 剪枝:不允许重复选择元素 且 不允许重复选择相等元素\n        if not selected[i] and choice not in duplicated:\n            # 尝试:做出选择,更新状态\n            duplicated.add(choice)  # 记录选择过的元素值\n            selected[i] = True\n            state.append(choice)\n            # 进行下一轮选择\n            backtrack(state, choices, selected, res)\n            # 回退:撤销选择,恢复到之前的状态\n            selected[i] = False\n            state.pop()\n\ndef permutations_ii(nums: list[int]) -> list[list[int]]:\n    \"\"\"全排列 II\"\"\"\n    res = []\n    backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res)\n    return res\n
permutations_ii.cpp
/* 回溯算法:全排列 II */\nvoid backtrack(vector<int> &state, const vector<int> &choices, vector<bool> &selected, vector<vector<int>> &res) {\n    // 当状态长度等于元素数量时,记录解\n    if (state.size() == choices.size()) {\n        res.push_back(state);\n        return;\n    }\n    // 遍历所有选择\n    unordered_set<int> duplicated;\n    for (int i = 0; i < choices.size(); i++) {\n        int choice = choices[i];\n        // 剪枝:不允许重复选择元素 且 不允许重复选择相等元素\n        if (!selected[i] && duplicated.find(choice) == duplicated.end()) {\n            // 尝试:做出选择,更新状态\n            duplicated.emplace(choice); // 记录选择过的元素值\n            selected[i] = true;\n            state.push_back(choice);\n            // 进行下一轮选择\n            backtrack(state, choices, selected, res);\n            // 回退:撤销选择,恢复到之前的状态\n            selected[i] = false;\n            state.pop_back();\n        }\n    }\n}\n\n/* 全排列 II */\nvector<vector<int>> permutationsII(vector<int> nums) {\n    vector<int> state;\n    vector<bool> selected(nums.size(), false);\n    vector<vector<int>> res;\n    backtrack(state, nums, selected, res);\n    return res;\n}\n
permutations_ii.java
/* 回溯算法:全排列 II */\nvoid backtrack(List<Integer> state, int[] choices, boolean[] selected, List<List<Integer>> res) {\n    // 当状态长度等于元素数量时,记录解\n    if (state.size() == choices.length) {\n        res.add(new ArrayList<Integer>(state));\n        return;\n    }\n    // 遍历所有选择\n    Set<Integer> duplicated = new HashSet<Integer>();\n    for (int i = 0; i < choices.length; i++) {\n        int choice = choices[i];\n        // 剪枝:不允许重复选择元素 且 不允许重复选择相等元素\n        if (!selected[i] && !duplicated.contains(choice)) {\n            // 尝试:做出选择,更新状态\n            duplicated.add(choice); // 记录选择过的元素值\n            selected[i] = true;\n            state.add(choice);\n            // 进行下一轮选择\n            backtrack(state, choices, selected, res);\n            // 回退:撤销选择,恢复到之前的状态\n            selected[i] = false;\n            state.remove(state.size() - 1);\n        }\n    }\n}\n\n/* 全排列 II */\nList<List<Integer>> permutationsII(int[] nums) {\n    List<List<Integer>> res = new ArrayList<List<Integer>>();\n    backtrack(new ArrayList<Integer>(), nums, new boolean[nums.length], res);\n    return res;\n}\n
permutations_ii.cs
/* 回溯算法:全排列 II */\nvoid Backtrack(List<int> state, int[] choices, bool[] selected, List<List<int>> res) {\n    // 当状态长度等于元素数量时,记录解\n    if (state.Count == choices.Length) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // 遍历所有选择\n    HashSet<int> duplicated = [];\n    for (int i = 0; i < choices.Length; i++) {\n        int choice = choices[i];\n        // 剪枝:不允许重复选择元素 且 不允许重复选择相等元素\n        if (!selected[i] && !duplicated.Contains(choice)) {\n            // 尝试:做出选择,更新状态\n            duplicated.Add(choice); // 记录选择过的元素值\n            selected[i] = true;\n            state.Add(choice);\n            // 进行下一轮选择\n            Backtrack(state, choices, selected, res);\n            // 回退:撤销选择,恢复到之前的状态\n            selected[i] = false;\n            state.RemoveAt(state.Count - 1);\n        }\n    }\n}\n\n/* 全排列 II */\nList<List<int>> PermutationsII(int[] nums) {\n    List<List<int>> res = [];\n    Backtrack([], nums, new bool[nums.Length], res);\n    return res;\n}\n
permutations_ii.go
/* 回溯算法:全排列 II */\nfunc backtrackII(state *[]int, choices *[]int, selected *[]bool, res *[][]int) {\n    // 当状态长度等于元素数量时,记录解\n    if len(*state) == len(*choices) {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n    }\n    // 遍历所有选择\n    duplicated := make(map[int]struct{}, 0)\n    for i := 0; i < len(*choices); i++ {\n        choice := (*choices)[i]\n        // 剪枝:不允许重复选择元素 且 不允许重复选择相等元素\n        if _, ok := duplicated[choice]; !ok && !(*selected)[i] {\n            // 尝试:做出选择,更新状态\n            // 记录选择过的元素值\n            duplicated[choice] = struct{}{}\n            (*selected)[i] = true\n            *state = append(*state, choice)\n            // 进行下一轮选择\n            backtrackII(state, choices, selected, res)\n            // 回退:撤销选择,恢复到之前的状态\n            (*selected)[i] = false\n            *state = (*state)[:len(*state)-1]\n        }\n    }\n}\n\n/* 全排列 II */\nfunc permutationsII(nums []int) [][]int {\n    res := make([][]int, 0)\n    state := make([]int, 0)\n    selected := make([]bool, len(nums))\n    backtrackII(&state, &nums, &selected, &res)\n    return res\n}\n
permutations_ii.swift
/* 回溯算法:全排列 II */\nfunc backtrack(state: inout [Int], choices: [Int], selected: inout [Bool], res: inout [[Int]]) {\n    // 当状态长度等于元素数量时,记录解\n    if state.count == choices.count {\n        res.append(state)\n        return\n    }\n    // 遍历所有选择\n    var duplicated: Set<Int> = []\n    for (i, choice) in choices.enumerated() {\n        // 剪枝:不允许重复选择元素 且 不允许重复选择相等元素\n        if !selected[i], !duplicated.contains(choice) {\n            // 尝试:做出选择,更新状态\n            duplicated.insert(choice) // 记录选择过的元素值\n            selected[i] = true\n            state.append(choice)\n            // 进行下一轮选择\n            backtrack(state: &state, choices: choices, selected: &selected, res: &res)\n            // 回退:撤销选择,恢复到之前的状态\n            selected[i] = false\n            state.removeLast()\n        }\n    }\n}\n\n/* 全排列 II */\nfunc permutationsII(nums: [Int]) -> [[Int]] {\n    var state: [Int] = []\n    var selected = Array(repeating: false, count: nums.count)\n    var res: [[Int]] = []\n    backtrack(state: &state, choices: nums, selected: &selected, res: &res)\n    return res\n}\n
permutations_ii.js
/* 回溯算法:全排列 II */\nfunction backtrack(state, choices, selected, res) {\n    // 当状态长度等于元素数量时,记录解\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // 遍历所有选择\n    const duplicated = new Set();\n    choices.forEach((choice, i) => {\n        // 剪枝:不允许重复选择元素 且 不允许重复选择相等元素\n        if (!selected[i] && !duplicated.has(choice)) {\n            // 尝试:做出选择,更新状态\n            duplicated.add(choice); // 记录选择过的元素值\n            selected[i] = true;\n            state.push(choice);\n            // 进行下一轮选择\n            backtrack(state, choices, selected, res);\n            // 回退:撤销选择,恢复到之前的状态\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* 全排列 II */\nfunction permutationsII(nums) {\n    const res = [];\n    backtrack([], nums, Array(nums.length).fill(false), res);\n    return res;\n}\n
permutations_ii.ts
/* 回溯算法:全排列 II */\nfunction backtrack(\n    state: number[],\n    choices: number[],\n    selected: boolean[],\n    res: number[][]\n): void {\n    // 当状态长度等于元素数量时,记录解\n    if (state.length === choices.length) {\n        res.push([...state]);\n        return;\n    }\n    // 遍历所有选择\n    const duplicated = new Set();\n    choices.forEach((choice, i) => {\n        // 剪枝:不允许重复选择元素 且 不允许重复选择相等元素\n        if (!selected[i] && !duplicated.has(choice)) {\n            // 尝试:做出选择,更新状态\n            duplicated.add(choice); // 记录选择过的元素值\n            selected[i] = true;\n            state.push(choice);\n            // 进行下一轮选择\n            backtrack(state, choices, selected, res);\n            // 回退:撤销选择,恢复到之前的状态\n            selected[i] = false;\n            state.pop();\n        }\n    });\n}\n\n/* 全排列 II */\nfunction permutationsII(nums: number[]): number[][] {\n    const res: number[][] = [];\n    backtrack([], nums, Array(nums.length).fill(false), res);\n    return res;\n}\n
permutations_ii.dart
/* 回溯算法:全排列 II */\nvoid backtrack(\n  List<int> state,\n  List<int> choices,\n  List<bool> selected,\n  List<List<int>> res,\n) {\n  // 当状态长度等于元素数量时,记录解\n  if (state.length == choices.length) {\n    res.add(List.from(state));\n    return;\n  }\n  // 遍历所有选择\n  Set<int> duplicated = {};\n  for (int i = 0; i < choices.length; i++) {\n    int choice = choices[i];\n    // 剪枝:不允许重复选择元素 且 不允许重复选择相等元素\n    if (!selected[i] && !duplicated.contains(choice)) {\n      // 尝试:做出选择,更新状态\n      duplicated.add(choice); // 记录选择过的元素值\n      selected[i] = true;\n      state.add(choice);\n      // 进行下一轮选择\n      backtrack(state, choices, selected, res);\n      // 回退:撤销选择,恢复到之前的状态\n      selected[i] = false;\n      state.removeLast();\n    }\n  }\n}\n\n/* 全排列 II */\nList<List<int>> permutationsII(List<int> nums) {\n  List<List<int>> res = [];\n  backtrack([], nums, List.filled(nums.length, false), res);\n  return res;\n}\n
permutations_ii.rs
/* 回溯算法:全排列 II */\nfn backtrack(mut state: Vec<i32>, choices: &[i32], selected: &mut [bool], res: &mut Vec<Vec<i32>>) {\n    // 当状态长度等于元素数量时,记录解\n    if state.len() == choices.len() {\n        res.push(state);\n        return;\n    }\n    // 遍历所有选择\n    let mut duplicated = HashSet::<i32>::new();\n    for i in 0..choices.len() {\n        let choice = choices[i];\n        // 剪枝:不允许重复选择元素 且 不允许重复选择相等元素\n        if !selected[i] && !duplicated.contains(&choice) {\n            // 尝试:做出选择,更新状态\n            duplicated.insert(choice); // 记录选择过的元素值\n            selected[i] = true;\n            state.push(choice);\n            // 进行下一轮选择\n            backtrack(state.clone(), choices, selected, res);\n            // 回退:撤销选择,恢复到之前的状态\n            selected[i] = false;\n            state.pop();\n        }\n    }\n}\n\n/* 全排列 II */\nfn permutations_ii(nums: &mut [i32]) -> Vec<Vec<i32>> {\n    let mut res = Vec::new();\n    backtrack(Vec::new(), nums, &mut vec![false; nums.len()], &mut res);\n    res\n}\n
permutations_ii.c
/* 回溯算法:全排列 II */\nvoid backtrack(int *state, int stateSize, int *choices, int choicesSize, bool *selected, int **res, int *resSize) {\n    // 当状态长度等于元素数量时,记录解\n    if (stateSize == choicesSize) {\n        res[*resSize] = (int *)malloc(choicesSize * sizeof(int));\n        for (int i = 0; i < choicesSize; i++) {\n            res[*resSize][i] = state[i];\n        }\n        (*resSize)++;\n        return;\n    }\n    // 遍历所有选择\n    bool duplicated[MAX_SIZE] = {false};\n    for (int i = 0; i < choicesSize; i++) {\n        int choice = choices[i];\n        // 剪枝:不允许重复选择元素 且 不允许重复选择相等元素\n        if (!selected[i] && !duplicated[choice]) {\n            // 尝试:做出选择,更新状态\n            duplicated[choice] = true; // 记录选择过的元素值\n            selected[i] = true;\n            state[stateSize] = choice;\n            // 进行下一轮选择\n            backtrack(state, stateSize + 1, choices, choicesSize, selected, res, resSize);\n            // 回退:撤销选择,恢复到之前的状态\n            selected[i] = false;\n        }\n    }\n}\n\n/* 全排列 II */\nint **permutationsII(int *nums, int numsSize, int *returnSize) {\n    int *state = (int *)malloc(numsSize * sizeof(int));\n    bool *selected = (bool *)malloc(numsSize * sizeof(bool));\n    for (int i = 0; i < numsSize; i++) {\n        selected[i] = false;\n    }\n    int **res = (int **)malloc(MAX_SIZE * sizeof(int *));\n    *returnSize = 0;\n\n    backtrack(state, 0, nums, numsSize, selected, res, returnSize);\n\n    free(state);\n    free(selected);\n\n    return res;\n}\n
permutations_ii.kt
/* 回溯算法:全排列 II */\nfun backtrack(\n    state: MutableList<Int>,\n    choices: IntArray,\n    selected: BooleanArray,\n    res: MutableList<MutableList<Int>?>\n) {\n    // 当状态长度等于元素数量时,记录解\n    if (state.size == choices.size) {\n        res.add(state.toMutableList())\n        return\n    }\n    // 遍历所有选择\n    val duplicated = HashSet<Int>()\n    for (i in choices.indices) {\n        val choice = choices[i]\n        // 剪枝:不允许重复选择元素 且 不允许重复选择相等元素\n        if (!selected[i] && !duplicated.contains(choice)) {\n            // 尝试:做出选择,更新状态\n            duplicated.add(choice) // 记录选择过的元素值\n            selected[i] = true\n            state.add(choice)\n            // 进行下一轮选择\n            backtrack(state, choices, selected, res)\n            // 回退:撤销选择,恢复到之前的状态\n            selected[i] = false\n            state.removeAt(state.size - 1)\n        }\n    }\n}\n\n/* 全排列 II */\nfun permutationsII(nums: IntArray): MutableList<MutableList<Int>?> {\n    val res = mutableListOf<MutableList<Int>?>()\n    backtrack(mutableListOf(), nums, BooleanArray(nums.size), res)\n    return res\n}\n
permutations_ii.rb
### 回溯算法:全排列 II ###\ndef backtrack(state, choices, selected, res)\n  # 当状态长度等于元素数量时,记录解\n  if state.length == choices.length\n    res << state.dup\n    return\n  end\n\n  # 遍历所有选择\n  duplicated = Set.new\n  choices.each_with_index do |choice, i|\n    # 剪枝:不允许重复选择元素 且 不允许重复选择相等元素\n    if !selected[i] && !duplicated.include?(choice)\n      # 尝试:做出选择,更新状态\n      duplicated.add(choice)\n      selected[i] = true\n      state << choice\n      # 进行下一轮选择\n      backtrack(state, choices, selected, res)\n      # 回退:撤销选择,恢复到之前的状态\n      selected[i] = false\n      state.pop\n    end\n  end\nend\n\n### 全排列 II ###\ndef permutations_ii(nums)\n  res = []\n  backtrack([], nums, Array.new(nums.length, false), res)\n  res\nend\n
可视化运行

全屏观看 >

假设元素两两之间互不相同,则 \\(n\\) 个元素共有 \\(n!\\) 种排列(阶乘);在记录结果时,需要复制长度为 \\(n\\) 的列表,使用 \\(O(n)\\) 时间。因此时间复杂度为 \\(O(n!n)\\) 。

最大递归深度为 \\(n\\) ,使用 \\(O(n)\\) 栈帧空间。selected 使用 \\(O(n)\\) 空间。同一时刻最多共有 \\(n\\) 个 duplicated ,使用 \\(O(n^2)\\) 空间。因此空间复杂度为 \\(O(n^2)\\) 。

","path":["第 13 章   回溯","13.2   全排列问题"],"tags":[]},{"location":"chapter_backtracking/permutations_problem/#3","level":3,"title":"3.   两种剪枝对比","text":"

请注意,虽然 selectedduplicated 都用于剪枝,但两者的目标不同。

图 13-9 展示了两个剪枝条件的生效范围。注意,树中的每个节点代表一个选择,从根节点到叶节点的路径上的各个节点构成一个排列。

图 13-9   两种剪枝条件的作用范围

","path":["第 13 章   回溯","13.2   全排列问题"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/","level":1,"title":"13.3   子集和问题","text":"","path":["第 13 章   回溯","13.3   子集和问题"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1331","level":2,"title":"13.3.1   无重复元素的情况","text":"

Question

给定一个正整数数组 nums 和一个目标正整数 target ,请找出所有可能的组合,使得组合中的元素和等于 target 。给定数组无重复元素,每个元素可以被选取多次。请以列表形式返回这些组合,列表中不应包含重复组合。

例如,输入集合 \\(\\{3, 4, 5\\}\\) 和目标整数 \\(9\\) ,解为 \\(\\{3, 3, 3\\}, \\{4, 5\\}\\) 。需要注意以下两点。

","path":["第 13 章   回溯","13.3   子集和问题"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1","level":3,"title":"1.   参考全排列解法","text":"

类似于全排列问题,我们可以把子集的生成过程想象成一系列选择的结果,并在选择过程中实时更新“元素和”,当元素和等于 target 时,就将子集记录至结果列表。

而与全排列问题不同的是,本题集合中的元素可以被无限次选取,因此无须借助 selected 布尔列表来记录元素是否已被选择。我们可以对全排列代码进行小幅修改,初步得到解题代码:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby subset_sum_i_naive.py
def backtrack(\n    state: list[int],\n    target: int,\n    total: int,\n    choices: list[int],\n    res: list[list[int]],\n):\n    \"\"\"回溯算法:子集和 I\"\"\"\n    # 子集和等于 target 时,记录解\n    if total == target:\n        res.append(list(state))\n        return\n    # 遍历所有选择\n    for i in range(len(choices)):\n        # 剪枝:若子集和超过 target ,则跳过该选择\n        if total + choices[i] > target:\n            continue\n        # 尝试:做出选择,更新元素和 total\n        state.append(choices[i])\n        # 进行下一轮选择\n        backtrack(state, target, total + choices[i], choices, res)\n        # 回退:撤销选择,恢复到之前的状态\n        state.pop()\n\ndef subset_sum_i_naive(nums: list[int], target: int) -> list[list[int]]:\n    \"\"\"求解子集和 I(包含重复子集)\"\"\"\n    state = []  # 状态(子集)\n    total = 0  # 子集和\n    res = []  # 结果列表(子集列表)\n    backtrack(state, target, total, nums, res)\n    return res\n
subset_sum_i_naive.cpp
/* 回溯算法:子集和 I */\nvoid backtrack(vector<int> &state, int target, int total, vector<int> &choices, vector<vector<int>> &res) {\n    // 子集和等于 target 时,记录解\n    if (total == target) {\n        res.push_back(state);\n        return;\n    }\n    // 遍历所有选择\n    for (size_t i = 0; i < choices.size(); i++) {\n        // 剪枝:若子集和超过 target ,则跳过该选择\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // 尝试:做出选择,更新元素和 total\n        state.push_back(choices[i]);\n        // 进行下一轮选择\n        backtrack(state, target, total + choices[i], choices, res);\n        // 回退:撤销选择,恢复到之前的状态\n        state.pop_back();\n    }\n}\n\n/* 求解子集和 I(包含重复子集) */\nvector<vector<int>> subsetSumINaive(vector<int> &nums, int target) {\n    vector<int> state;       // 状态(子集)\n    int total = 0;           // 子集和\n    vector<vector<int>> res; // 结果列表(子集列表)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
subset_sum_i_naive.java
/* 回溯算法:子集和 I */\nvoid backtrack(List<Integer> state, int target, int total, int[] choices, List<List<Integer>> res) {\n    // 子集和等于 target 时,记录解\n    if (total == target) {\n        res.add(new ArrayList<>(state));\n        return;\n    }\n    // 遍历所有选择\n    for (int i = 0; i < choices.length; i++) {\n        // 剪枝:若子集和超过 target ,则跳过该选择\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // 尝试:做出选择,更新元素和 total\n        state.add(choices[i]);\n        // 进行下一轮选择\n        backtrack(state, target, total + choices[i], choices, res);\n        // 回退:撤销选择,恢复到之前的状态\n        state.remove(state.size() - 1);\n    }\n}\n\n/* 求解子集和 I(包含重复子集) */\nList<List<Integer>> subsetSumINaive(int[] nums, int target) {\n    List<Integer> state = new ArrayList<>(); // 状态(子集)\n    int total = 0; // 子集和\n    List<List<Integer>> res = new ArrayList<>(); // 结果列表(子集列表)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
subset_sum_i_naive.cs
/* 回溯算法:子集和 I */\nvoid Backtrack(List<int> state, int target, int total, int[] choices, List<List<int>> res) {\n    // 子集和等于 target 时,记录解\n    if (total == target) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // 遍历所有选择\n    for (int i = 0; i < choices.Length; i++) {\n        // 剪枝:若子集和超过 target ,则跳过该选择\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // 尝试:做出选择,更新元素和 total\n        state.Add(choices[i]);\n        // 进行下一轮选择\n        Backtrack(state, target, total + choices[i], choices, res);\n        // 回退:撤销选择,恢复到之前的状态\n        state.RemoveAt(state.Count - 1);\n    }\n}\n\n/* 求解子集和 I(包含重复子集) */\nList<List<int>> SubsetSumINaive(int[] nums, int target) {\n    List<int> state = []; // 状态(子集)\n    int total = 0; // 子集和\n    List<List<int>> res = []; // 结果列表(子集列表)\n    Backtrack(state, target, total, nums, res);\n    return res;\n}\n
subset_sum_i_naive.go
/* 回溯算法:子集和 I */\nfunc backtrackSubsetSumINaive(total, target int, state, choices *[]int, res *[][]int) {\n    // 子集和等于 target 时,记录解\n    if target == total {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n        return\n    }\n    // 遍历所有选择\n    for i := 0; i < len(*choices); i++ {\n        // 剪枝:若子集和超过 target ,则跳过该选择\n        if total+(*choices)[i] > target {\n            continue\n        }\n        // 尝试:做出选择,更新元素和 total\n        *state = append(*state, (*choices)[i])\n        // 进行下一轮选择\n        backtrackSubsetSumINaive(total+(*choices)[i], target, state, choices, res)\n        // 回退:撤销选择,恢复到之前的状态\n        *state = (*state)[:len(*state)-1]\n    }\n}\n\n/* 求解子集和 I(包含重复子集) */\nfunc subsetSumINaive(nums []int, target int) [][]int {\n    state := make([]int, 0) // 状态(子集)\n    total := 0              // 子集和\n    res := make([][]int, 0) // 结果列表(子集列表)\n    backtrackSubsetSumINaive(total, target, &state, &nums, &res)\n    return res\n}\n
subset_sum_i_naive.swift
/* 回溯算法:子集和 I */\nfunc backtrack(state: inout [Int], target: Int, total: Int, choices: [Int], res: inout [[Int]]) {\n    // 子集和等于 target 时,记录解\n    if total == target {\n        res.append(state)\n        return\n    }\n    // 遍历所有选择\n    for i in choices.indices {\n        // 剪枝:若子集和超过 target ,则跳过该选择\n        if total + choices[i] > target {\n            continue\n        }\n        // 尝试:做出选择,更新元素和 total\n        state.append(choices[i])\n        // 进行下一轮选择\n        backtrack(state: &state, target: target, total: total + choices[i], choices: choices, res: &res)\n        // 回退:撤销选择,恢复到之前的状态\n        state.removeLast()\n    }\n}\n\n/* 求解子集和 I(包含重复子集) */\nfunc subsetSumINaive(nums: [Int], target: Int) -> [[Int]] {\n    var state: [Int] = [] // 状态(子集)\n    let total = 0 // 子集和\n    var res: [[Int]] = [] // 结果列表(子集列表)\n    backtrack(state: &state, target: target, total: total, choices: nums, res: &res)\n    return res\n}\n
subset_sum_i_naive.js
/* 回溯算法:子集和 I */\nfunction backtrack(state, target, total, choices, res) {\n    // 子集和等于 target 时,记录解\n    if (total === target) {\n        res.push([...state]);\n        return;\n    }\n    // 遍历所有选择\n    for (let i = 0; i < choices.length; i++) {\n        // 剪枝:若子集和超过 target ,则跳过该选择\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // 尝试:做出选择,更新元素和 total\n        state.push(choices[i]);\n        // 进行下一轮选择\n        backtrack(state, target, total + choices[i], choices, res);\n        // 回退:撤销选择,恢复到之前的状态\n        state.pop();\n    }\n}\n\n/* 求解子集和 I(包含重复子集) */\nfunction subsetSumINaive(nums, target) {\n    const state = []; // 状态(子集)\n    const total = 0; // 子集和\n    const res = []; // 结果列表(子集列表)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
subset_sum_i_naive.ts
/* 回溯算法:子集和 I */\nfunction backtrack(\n    state: number[],\n    target: number,\n    total: number,\n    choices: number[],\n    res: number[][]\n): void {\n    // 子集和等于 target 时,记录解\n    if (total === target) {\n        res.push([...state]);\n        return;\n    }\n    // 遍历所有选择\n    for (let i = 0; i < choices.length; i++) {\n        // 剪枝:若子集和超过 target ,则跳过该选择\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // 尝试:做出选择,更新元素和 total\n        state.push(choices[i]);\n        // 进行下一轮选择\n        backtrack(state, target, total + choices[i], choices, res);\n        // 回退:撤销选择,恢复到之前的状态\n        state.pop();\n    }\n}\n\n/* 求解子集和 I(包含重复子集) */\nfunction subsetSumINaive(nums: number[], target: number): number[][] {\n    const state = []; // 状态(子集)\n    const total = 0; // 子集和\n    const res = []; // 结果列表(子集列表)\n    backtrack(state, target, total, nums, res);\n    return res;\n}\n
subset_sum_i_naive.dart
/* 回溯算法:子集和 I */\nvoid backtrack(\n  List<int> state,\n  int target,\n  int total,\n  List<int> choices,\n  List<List<int>> res,\n) {\n  // 子集和等于 target 时,记录解\n  if (total == target) {\n    res.add(List.from(state));\n    return;\n  }\n  // 遍历所有选择\n  for (int i = 0; i < choices.length; i++) {\n    // 剪枝:若子集和超过 target ,则跳过该选择\n    if (total + choices[i] > target) {\n      continue;\n    }\n    // 尝试:做出选择,更新元素和 total\n    state.add(choices[i]);\n    // 进行下一轮选择\n    backtrack(state, target, total + choices[i], choices, res);\n    // 回退:撤销选择,恢复到之前的状态\n    state.removeLast();\n  }\n}\n\n/* 求解子集和 I(包含重复子集) */\nList<List<int>> subsetSumINaive(List<int> nums, int target) {\n  List<int> state = []; // 状态(子集)\n  int total = 0; // 元素和\n  List<List<int>> res = []; // 结果列表(子集列表)\n  backtrack(state, target, total, nums, res);\n  return res;\n}\n
subset_sum_i_naive.rs
/* 回溯算法:子集和 I */\nfn backtrack(\n    state: &mut Vec<i32>,\n    target: i32,\n    total: i32,\n    choices: &[i32],\n    res: &mut Vec<Vec<i32>>,\n) {\n    // 子集和等于 target 时,记录解\n    if total == target {\n        res.push(state.clone());\n        return;\n    }\n    // 遍历所有选择\n    for i in 0..choices.len() {\n        // 剪枝:若子集和超过 target ,则跳过该选择\n        if total + choices[i] > target {\n            continue;\n        }\n        // 尝试:做出选择,更新元素和 total\n        state.push(choices[i]);\n        // 进行下一轮选择\n        backtrack(state, target, total + choices[i], choices, res);\n        // 回退:撤销选择,恢复到之前的状态\n        state.pop();\n    }\n}\n\n/* 求解子集和 I(包含重复子集) */\nfn subset_sum_i_naive(nums: &[i32], target: i32) -> Vec<Vec<i32>> {\n    let mut state = Vec::new(); // 状态(子集)\n    let total = 0; // 子集和\n    let mut res = Vec::new(); // 结果列表(子集列表)\n    backtrack(&mut state, target, total, nums, &mut res);\n    res\n}\n
subset_sum_i_naive.c
/* 回溯算法:子集和 I */\nvoid backtrack(int target, int total, int *choices, int choicesSize) {\n    // 子集和等于 target 时,记录解\n    if (total == target) {\n        for (int i = 0; i < stateSize; i++) {\n            res[resSize][i] = state[i];\n        }\n        resColSizes[resSize++] = stateSize;\n        return;\n    }\n    // 遍历所有选择\n    for (int i = 0; i < choicesSize; i++) {\n        // 剪枝:若子集和超过 target ,则跳过该选择\n        if (total + choices[i] > target) {\n            continue;\n        }\n        // 尝试:做出选择,更新元素和 total\n        state[stateSize++] = choices[i];\n        // 进行下一轮选择\n        backtrack(target, total + choices[i], choices, choicesSize);\n        // 回退:撤销选择,恢复到之前的状态\n        stateSize--;\n    }\n}\n\n/* 求解子集和 I(包含重复子集) */\nvoid subsetSumINaive(int *nums, int numsSize, int target) {\n    resSize = 0; // 初始化解的数量为0\n    backtrack(target, 0, nums, numsSize);\n}\n
subset_sum_i_naive.kt
/* 回溯算法:子集和 I */\nfun backtrack(\n    state: MutableList<Int>,\n    target: Int,\n    total: Int,\n    choices: IntArray,\n    res: MutableList<MutableList<Int>?>\n) {\n    // 子集和等于 target 时,记录解\n    if (total == target) {\n        res.add(state.toMutableList())\n        return\n    }\n    // 遍历所有选择\n    for (i in choices.indices) {\n        // 剪枝:若子集和超过 target ,则跳过该选择\n        if (total + choices[i] > target) {\n            continue\n        }\n        // 尝试:做出选择,更新元素和 total\n        state.add(choices[i])\n        // 进行下一轮选择\n        backtrack(state, target, total + choices[i], choices, res)\n        // 回退:撤销选择,恢复到之前的状态\n        state.removeAt(state.size - 1)\n    }\n}\n\n/* 求解子集和 I(包含重复子集) */\nfun subsetSumINaive(nums: IntArray, target: Int): MutableList<MutableList<Int>?> {\n    val state = mutableListOf<Int>() // 状态(子集)\n    val total = 0 // 子集和\n    val res = mutableListOf<MutableList<Int>?>() // 结果列表(子集列表)\n    backtrack(state, target, total, nums, res)\n    return res\n}\n
subset_sum_i_naive.rb
### 回溯算法:子集和 I ###\ndef backtrack(state, target, total, choices, res)\n  # 子集和等于 target 时,记录解\n  if total == target\n    res << state.dup\n    return\n  end\n\n  # 遍历所有选择\n  for i in 0...choices.length\n    # 剪枝:若子集和超过 target ,则跳过该选择\n    next if total + choices[i] > target\n    # 尝试:做出选择,更新元素和 total\n    state << choices[i]\n    # 进行下一轮选择\n    backtrack(state, target, total + choices[i], choices, res)\n    # 回退:撤销选择,恢复到之前的状态\n    state.pop\n  end\nend\n\n### 求解子集和 I(包含重复子集)###\ndef subset_sum_i_naive(nums, target)\n  state = [] # 状态(子集)\n  total = 0 # 子集和\n  res = [] # 结果列表(子集列表)\n  backtrack(state, target, total, nums, res)\n  res\nend\n
可视化运行

全屏观看 >

向以上代码输入数组 \\([3, 4, 5]\\) 和目标元素 \\(9\\) ,输出结果为 \\([3, 3, 3], [4, 5], [5, 4]\\) 。虽然成功找出了所有和为 \\(9\\) 的子集,但其中存在重复的子集 \\([4, 5]\\) 和 \\([5, 4]\\) 。

这是因为搜索过程是区分选择顺序的,然而子集不区分选择顺序。如图 13-10 所示,先选 \\(4\\) 后选 \\(5\\) 与先选 \\(5\\) 后选 \\(4\\) 是不同的分支,但对应同一个子集。

图 13-10   子集搜索与越界剪枝

为了去除重复子集,一种直接的思路是对结果列表进行去重。但这个方法效率很低,有两方面原因。

","path":["第 13 章   回溯","13.3   子集和问题"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#2","level":3,"title":"2.   重复子集剪枝","text":"

我们考虑在搜索过程中通过剪枝进行去重。观察图 13-11 ,重复子集是在以不同顺序选择数组元素时产生的,例如以下情况。

  1. 当第一轮和第二轮分别选择 \\(3\\) 和 \\(4\\) 时,会生成包含这两个元素的所有子集,记为 \\([3, 4, \\dots]\\) 。
  2. 之后,当第一轮选择 \\(4\\) 时,则第二轮应该跳过 \\(3\\) ,因为该选择产生的子集 \\([4, 3, \\dots]\\) 和第 1. 步中生成的子集完全重复。

在搜索过程中,每一层的选择都是从左到右被逐个尝试的,因此越靠右的分支被剪掉的越多。

  1. 前两轮选择 \\(3\\) 和 \\(5\\) ,生成子集 \\([3, 5, \\dots]\\) 。
  2. 前两轮选择 \\(4\\) 和 \\(5\\) ,生成子集 \\([4, 5, \\dots]\\) 。
  3. 若第一轮选择 \\(5\\) ,则第二轮应该跳过 \\(3\\) 和 \\(4\\) ,因为子集 \\([5, 3, \\dots]\\) 和 \\([5, 4, \\dots]\\) 与第 1. 步和第 2. 步中描述的子集完全重复。

图 13-11   不同选择顺序导致的重复子集

总结来看,给定输入数组 \\([x_1, x_2, \\dots, x_n]\\) ,设搜索过程中的选择序列为 \\([x_{i_1}, x_{i_2}, \\dots, x_{i_m}]\\) ,则该选择序列需要满足 \\(i_1 \\leq i_2 \\leq \\dots \\leq i_m\\) ,不满足该条件的选择序列都会造成重复,应当剪枝。

","path":["第 13 章   回溯","13.3   子集和问题"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#3","level":3,"title":"3.   代码实现","text":"

为实现该剪枝,我们初始化变量 start ,用于指示遍历起始点。当做出选择 \\(x_{i}\\) 后,设定下一轮从索引 \\(i\\) 开始遍历。这样做就可以让选择序列满足 \\(i_1 \\leq i_2 \\leq \\dots \\leq i_m\\) ,从而保证子集唯一。

除此之外,我们还对代码进行了以下两项优化。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby subset_sum_i.py
def backtrack(\n    state: list[int], target: int, choices: list[int], start: int, res: list[list[int]]\n):\n    \"\"\"回溯算法:子集和 I\"\"\"\n    # 子集和等于 target 时,记录解\n    if target == 0:\n        res.append(list(state))\n        return\n    # 遍历所有选择\n    # 剪枝二:从 start 开始遍历,避免生成重复子集\n    for i in range(start, len(choices)):\n        # 剪枝一:若子集和超过 target ,则直接结束循环\n        # 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if target - choices[i] < 0:\n            break\n        # 尝试:做出选择,更新 target, start\n        state.append(choices[i])\n        # 进行下一轮选择\n        backtrack(state, target - choices[i], choices, i, res)\n        # 回退:撤销选择,恢复到之前的状态\n        state.pop()\n\ndef subset_sum_i(nums: list[int], target: int) -> list[list[int]]:\n    \"\"\"求解子集和 I\"\"\"\n    state = []  # 状态(子集)\n    nums.sort()  # 对 nums 进行排序\n    start = 0  # 遍历起始点\n    res = []  # 结果列表(子集列表)\n    backtrack(state, target, nums, start, res)\n    return res\n
subset_sum_i.cpp
/* 回溯算法:子集和 I */\nvoid backtrack(vector<int> &state, int target, vector<int> &choices, int start, vector<vector<int>> &res) {\n    // 子集和等于 target 时,记录解\n    if (target == 0) {\n        res.push_back(state);\n        return;\n    }\n    // 遍历所有选择\n    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    for (int i = start; i < choices.size(); i++) {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 尝试:做出选择,更新 target, start\n        state.push_back(choices[i]);\n        // 进行下一轮选择\n        backtrack(state, target - choices[i], choices, i, res);\n        // 回退:撤销选择,恢复到之前的状态\n        state.pop_back();\n    }\n}\n\n/* 求解子集和 I */\nvector<vector<int>> subsetSumI(vector<int> &nums, int target) {\n    vector<int> state;              // 状态(子集)\n    sort(nums.begin(), nums.end()); // 对 nums 进行排序\n    int start = 0;                  // 遍历起始点\n    vector<vector<int>> res;        // 结果列表(子集列表)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_i.java
/* 回溯算法:子集和 I */\nvoid backtrack(List<Integer> state, int target, int[] choices, int start, List<List<Integer>> res) {\n    // 子集和等于 target 时,记录解\n    if (target == 0) {\n        res.add(new ArrayList<>(state));\n        return;\n    }\n    // 遍历所有选择\n    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    for (int i = start; i < choices.length; i++) {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 尝试:做出选择,更新 target, start\n        state.add(choices[i]);\n        // 进行下一轮选择\n        backtrack(state, target - choices[i], choices, i, res);\n        // 回退:撤销选择,恢复到之前的状态\n        state.remove(state.size() - 1);\n    }\n}\n\n/* 求解子集和 I */\nList<List<Integer>> subsetSumI(int[] nums, int target) {\n    List<Integer> state = new ArrayList<>(); // 状态(子集)\n    Arrays.sort(nums); // 对 nums 进行排序\n    int start = 0; // 遍历起始点\n    List<List<Integer>> res = new ArrayList<>(); // 结果列表(子集列表)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_i.cs
/* 回溯算法:子集和 I */\nvoid Backtrack(List<int> state, int target, int[] choices, int start, List<List<int>> res) {\n    // 子集和等于 target 时,记录解\n    if (target == 0) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // 遍历所有选择\n    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    for (int i = start; i < choices.Length; i++) {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 尝试:做出选择,更新 target, start\n        state.Add(choices[i]);\n        // 进行下一轮选择\n        Backtrack(state, target - choices[i], choices, i, res);\n        // 回退:撤销选择,恢复到之前的状态\n        state.RemoveAt(state.Count - 1);\n    }\n}\n\n/* 求解子集和 I */\nList<List<int>> SubsetSumI(int[] nums, int target) {\n    List<int> state = []; // 状态(子集)\n    Array.Sort(nums); // 对 nums 进行排序\n    int start = 0; // 遍历起始点\n    List<List<int>> res = []; // 结果列表(子集列表)\n    Backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_i.go
/* 回溯算法:子集和 I */\nfunc backtrackSubsetSumI(start, target int, state, choices *[]int, res *[][]int) {\n    // 子集和等于 target 时,记录解\n    if target == 0 {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n        return\n    }\n    // 遍历所有选择\n    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    for i := start; i < len(*choices); i++ {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if target-(*choices)[i] < 0 {\n            break\n        }\n        // 尝试:做出选择,更新 target, start\n        *state = append(*state, (*choices)[i])\n        // 进行下一轮选择\n        backtrackSubsetSumI(i, target-(*choices)[i], state, choices, res)\n        // 回退:撤销选择,恢复到之前的状态\n        *state = (*state)[:len(*state)-1]\n    }\n}\n\n/* 求解子集和 I */\nfunc subsetSumI(nums []int, target int) [][]int {\n    state := make([]int, 0) // 状态(子集)\n    sort.Ints(nums)         // 对 nums 进行排序\n    start := 0              // 遍历起始点\n    res := make([][]int, 0) // 结果列表(子集列表)\n    backtrackSubsetSumI(start, target, &state, &nums, &res)\n    return res\n}\n
subset_sum_i.swift
/* 回溯算法:子集和 I */\nfunc backtrack(state: inout [Int], target: Int, choices: [Int], start: Int, res: inout [[Int]]) {\n    // 子集和等于 target 时,记录解\n    if target == 0 {\n        res.append(state)\n        return\n    }\n    // 遍历所有选择\n    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    for i in choices.indices.dropFirst(start) {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if target - choices[i] < 0 {\n            break\n        }\n        // 尝试:做出选择,更新 target, start\n        state.append(choices[i])\n        // 进行下一轮选择\n        backtrack(state: &state, target: target - choices[i], choices: choices, start: i, res: &res)\n        // 回退:撤销选择,恢复到之前的状态\n        state.removeLast()\n    }\n}\n\n/* 求解子集和 I */\nfunc subsetSumI(nums: [Int], target: Int) -> [[Int]] {\n    var state: [Int] = [] // 状态(子集)\n    let nums = nums.sorted() // 对 nums 进行排序\n    let start = 0 // 遍历起始点\n    var res: [[Int]] = [] // 结果列表(子集列表)\n    backtrack(state: &state, target: target, choices: nums, start: start, res: &res)\n    return res\n}\n
subset_sum_i.js
/* 回溯算法:子集和 I */\nfunction backtrack(state, target, choices, start, res) {\n    // 子集和等于 target 时,记录解\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // 遍历所有选择\n    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    for (let i = start; i < choices.length; i++) {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 尝试:做出选择,更新 target, start\n        state.push(choices[i]);\n        // 进行下一轮选择\n        backtrack(state, target - choices[i], choices, i, res);\n        // 回退:撤销选择,恢复到之前的状态\n        state.pop();\n    }\n}\n\n/* 求解子集和 I */\nfunction subsetSumI(nums, target) {\n    const state = []; // 状态(子集)\n    nums.sort((a, b) => a - b); // 对 nums 进行排序\n    const start = 0; // 遍历起始点\n    const res = []; // 结果列表(子集列表)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_i.ts
/* 回溯算法:子集和 I */\nfunction backtrack(\n    state: number[],\n    target: number,\n    choices: number[],\n    start: number,\n    res: number[][]\n): void {\n    // 子集和等于 target 时,记录解\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // 遍历所有选择\n    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    for (let i = start; i < choices.length; i++) {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 尝试:做出选择,更新 target, start\n        state.push(choices[i]);\n        // 进行下一轮选择\n        backtrack(state, target - choices[i], choices, i, res);\n        // 回退:撤销选择,恢复到之前的状态\n        state.pop();\n    }\n}\n\n/* 求解子集和 I */\nfunction subsetSumI(nums: number[], target: number): number[][] {\n    const state = []; // 状态(子集)\n    nums.sort((a, b) => a - b); // 对 nums 进行排序\n    const start = 0; // 遍历起始点\n    const res = []; // 结果列表(子集列表)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_i.dart
/* 回溯算法:子集和 I */\nvoid backtrack(\n  List<int> state,\n  int target,\n  List<int> choices,\n  int start,\n  List<List<int>> res,\n) {\n  // 子集和等于 target 时,记录解\n  if (target == 0) {\n    res.add(List.from(state));\n    return;\n  }\n  // 遍历所有选择\n  // 剪枝二:从 start 开始遍历,避免生成重复子集\n  for (int i = start; i < choices.length; i++) {\n    // 剪枝一:若子集和超过 target ,则直接结束循环\n    // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n    if (target - choices[i] < 0) {\n      break;\n    }\n    // 尝试:做出选择,更新 target, start\n    state.add(choices[i]);\n    // 进行下一轮选择\n    backtrack(state, target - choices[i], choices, i, res);\n    // 回退:撤销选择,恢复到之前的状态\n    state.removeLast();\n  }\n}\n\n/* 求解子集和 I */\nList<List<int>> subsetSumI(List<int> nums, int target) {\n  List<int> state = []; // 状态(子集)\n  nums.sort(); // 对 nums 进行排序\n  int start = 0; // 遍历起始点\n  List<List<int>> res = []; // 结果列表(子集列表)\n  backtrack(state, target, nums, start, res);\n  return res;\n}\n
subset_sum_i.rs
/* 回溯算法:子集和 I */\nfn backtrack(\n    state: &mut Vec<i32>,\n    target: i32,\n    choices: &[i32],\n    start: usize,\n    res: &mut Vec<Vec<i32>>,\n) {\n    // 子集和等于 target 时,记录解\n    if target == 0 {\n        res.push(state.clone());\n        return;\n    }\n    // 遍历所有选择\n    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    for i in start..choices.len() {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if target - choices[i] < 0 {\n            break;\n        }\n        // 尝试:做出选择,更新 target, start\n        state.push(choices[i]);\n        // 进行下一轮选择\n        backtrack(state, target - choices[i], choices, i, res);\n        // 回退:撤销选择,恢复到之前的状态\n        state.pop();\n    }\n}\n\n/* 求解子集和 I */\nfn subset_sum_i(nums: &mut [i32], target: i32) -> Vec<Vec<i32>> {\n    let mut state = Vec::new(); // 状态(子集)\n    nums.sort(); // 对 nums 进行排序\n    let start = 0; // 遍历起始点\n    let mut res = Vec::new(); // 结果列表(子集列表)\n    backtrack(&mut state, target, nums, start, &mut res);\n    res\n}\n
subset_sum_i.c
/* 回溯算法:子集和 I */\nvoid backtrack(int target, int *choices, int choicesSize, int start) {\n    // 子集和等于 target 时,记录解\n    if (target == 0) {\n        for (int i = 0; i < stateSize; ++i) {\n            res[resSize][i] = state[i];\n        }\n        resColSizes[resSize++] = stateSize;\n        return;\n    }\n    // 遍历所有选择\n    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    for (int i = start; i < choicesSize; i++) {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 尝试:做出选择,更新 target, start\n        state[stateSize] = choices[i];\n        stateSize++;\n        // 进行下一轮选择\n        backtrack(target - choices[i], choices, choicesSize, i);\n        // 回退:撤销选择,恢复到之前的状态\n        stateSize--;\n    }\n}\n\n/* 求解子集和 I */\nvoid subsetSumI(int *nums, int numsSize, int target) {\n    qsort(nums, numsSize, sizeof(int), cmp); // 对 nums 进行排序\n    int start = 0;                           // 遍历起始点\n    backtrack(target, nums, numsSize, start);\n}\n
subset_sum_i.kt
/* 回溯算法:子集和 I */\nfun backtrack(\n    state: MutableList<Int>,\n    target: Int,\n    choices: IntArray,\n    start: Int,\n    res: MutableList<MutableList<Int>?>\n) {\n    // 子集和等于 target 时,记录解\n    if (target == 0) {\n        res.add(state.toMutableList())\n        return\n    }\n    // 遍历所有选择\n    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    for (i in start..<choices.size) {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if (target - choices[i] < 0) {\n            break\n        }\n        // 尝试:做出选择,更新 target, start\n        state.add(choices[i])\n        // 进行下一轮选择\n        backtrack(state, target - choices[i], choices, i, res)\n        // 回退:撤销选择,恢复到之前的状态\n        state.removeAt(state.size - 1)\n    }\n}\n\n/* 求解子集和 I */\nfun subsetSumI(nums: IntArray, target: Int): MutableList<MutableList<Int>?> {\n    val state = mutableListOf<Int>() // 状态(子集)\n    nums.sort() // 对 nums 进行排序\n    val start = 0 // 遍历起始点\n    val res = mutableListOf<MutableList<Int>?>() // 结果列表(子集列表)\n    backtrack(state, target, nums, start, res)\n    return res\n}\n
subset_sum_i.rb
### 回溯算法:子集和 I ###\ndef backtrack(state, target, choices, start, res)\n  # 子集和等于 target 时,记录解\n  if target.zero?\n    res << state.dup\n    return\n  end\n  # 遍历所有选择\n  # 剪枝二:从 start 开始遍历,避免生成重复子集\n  for i in start...choices.length\n    # 剪枝一:若子集和超过 target ,则直接结束循环\n    # 这是因为数组已排序,后边元素更大,子集和一定超过 target\n    break if target - choices[i] < 0\n    # 尝试:做出选择,更新 target, start\n    state << choices[i]\n    # 进行下一轮选择\n    backtrack(state, target - choices[i], choices, i, res)\n    # 回退:撤销选择,恢复到之前的状态\n    state.pop\n  end\nend\n\n### 求解子集和 I ###\ndef subset_sum_i(nums, target)\n  state = [] # 状态(子集)\n  nums.sort! # 对 nums 进行排序\n  start = 0 # 遍历起始点\n  res = [] # 结果列表(子集列表)\n  backtrack(state, target, nums, start, res)\n  res\nend\n
可视化运行

全屏观看 >

图 13-12 所示为将数组 \\([3, 4, 5]\\) 和目标元素 \\(9\\) 输入以上代码后的整体回溯过程。

图 13-12   子集和 I 回溯过程

","path":["第 13 章   回溯","13.3   子集和问题"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1332","level":2,"title":"13.3.2   考虑重复元素的情况","text":"

Question

给定一个正整数数组 nums 和一个目标正整数 target ,请找出所有可能的组合,使得组合中的元素和等于 target 。给定数组可能包含重复元素,每个元素只可被选择一次。请以列表形式返回这些组合,列表中不应包含重复组合。

相比于上题,本题的输入数组可能包含重复元素,这引入了新的问题。例如,给定数组 \\([4, \\hat{4}, 5]\\) 和目标元素 \\(9\\) ,则现有代码的输出结果为 \\([4, 5], [\\hat{4}, 5]\\) ,出现了重复子集。

造成这种重复的原因是相等元素在某轮中被多次选择。在图 13-13 中,第一轮共有三个选择,其中两个都为 \\(4\\) ,会产生两个重复的搜索分支,从而输出重复子集;同理,第二轮的两个 \\(4\\) 也会产生重复子集。

图 13-13   相等元素导致的重复子集

","path":["第 13 章   回溯","13.3   子集和问题"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#1_1","level":3,"title":"1.   相等元素剪枝","text":"

为解决此问题,我们需要限制相等元素在每一轮中只能被选择一次。实现方式比较巧妙:由于数组是已排序的,因此相等元素都是相邻的。这意味着在某轮选择中,若当前元素与其左边元素相等,则说明它已经被选择过,因此直接跳过当前元素。

与此同时,本题规定每个数组元素只能被选择一次。幸运的是,我们也可以利用变量 start 来满足该约束:当做出选择 \\(x_{i}\\) 后,设定下一轮从索引 \\(i + 1\\) 开始向后遍历。这样既能去除重复子集,也能避免重复选择元素。

","path":["第 13 章   回溯","13.3   子集和问题"],"tags":[]},{"location":"chapter_backtracking/subset_sum_problem/#2_1","level":3,"title":"2.   代码实现","text":"PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby subset_sum_ii.py
def backtrack(\n    state: list[int], target: int, choices: list[int], start: int, res: list[list[int]]\n):\n    \"\"\"回溯算法:子集和 II\"\"\"\n    # 子集和等于 target 时,记录解\n    if target == 0:\n        res.append(list(state))\n        return\n    # 遍历所有选择\n    # 剪枝二:从 start 开始遍历,避免生成重复子集\n    # 剪枝三:从 start 开始遍历,避免重复选择同一元素\n    for i in range(start, len(choices)):\n        # 剪枝一:若子集和超过 target ,则直接结束循环\n        # 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if target - choices[i] < 0:\n            break\n        # 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\n        if i > start and choices[i] == choices[i - 1]:\n            continue\n        # 尝试:做出选择,更新 target, start\n        state.append(choices[i])\n        # 进行下一轮选择\n        backtrack(state, target - choices[i], choices, i + 1, res)\n        # 回退:撤销选择,恢复到之前的状态\n        state.pop()\n\ndef subset_sum_ii(nums: list[int], target: int) -> list[list[int]]:\n    \"\"\"求解子集和 II\"\"\"\n    state = []  # 状态(子集)\n    nums.sort()  # 对 nums 进行排序\n    start = 0  # 遍历起始点\n    res = []  # 结果列表(子集列表)\n    backtrack(state, target, nums, start, res)\n    return res\n
subset_sum_ii.cpp
/* 回溯算法:子集和 II */\nvoid backtrack(vector<int> &state, int target, vector<int> &choices, int start, vector<vector<int>> &res) {\n    // 子集和等于 target 时,记录解\n    if (target == 0) {\n        res.push_back(state);\n        return;\n    }\n    // 遍历所有选择\n    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    // 剪枝三:从 start 开始遍历,避免重复选择同一元素\n    for (int i = start; i < choices.size(); i++) {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // 尝试:做出选择,更新 target, start\n        state.push_back(choices[i]);\n        // 进行下一轮选择\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // 回退:撤销选择,恢复到之前的状态\n        state.pop_back();\n    }\n}\n\n/* 求解子集和 II */\nvector<vector<int>> subsetSumII(vector<int> &nums, int target) {\n    vector<int> state;              // 状态(子集)\n    sort(nums.begin(), nums.end()); // 对 nums 进行排序\n    int start = 0;                  // 遍历起始点\n    vector<vector<int>> res;        // 结果列表(子集列表)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_ii.java
/* 回溯算法:子集和 II */\nvoid backtrack(List<Integer> state, int target, int[] choices, int start, List<List<Integer>> res) {\n    // 子集和等于 target 时,记录解\n    if (target == 0) {\n        res.add(new ArrayList<>(state));\n        return;\n    }\n    // 遍历所有选择\n    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    // 剪枝三:从 start 开始遍历,避免重复选择同一元素\n    for (int i = start; i < choices.length; i++) {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // 尝试:做出选择,更新 target, start\n        state.add(choices[i]);\n        // 进行下一轮选择\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // 回退:撤销选择,恢复到之前的状态\n        state.remove(state.size() - 1);\n    }\n}\n\n/* 求解子集和 II */\nList<List<Integer>> subsetSumII(int[] nums, int target) {\n    List<Integer> state = new ArrayList<>(); // 状态(子集)\n    Arrays.sort(nums); // 对 nums 进行排序\n    int start = 0; // 遍历起始点\n    List<List<Integer>> res = new ArrayList<>(); // 结果列表(子集列表)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_ii.cs
/* 回溯算法:子集和 II */\nvoid Backtrack(List<int> state, int target, int[] choices, int start, List<List<int>> res) {\n    // 子集和等于 target 时,记录解\n    if (target == 0) {\n        res.Add(new List<int>(state));\n        return;\n    }\n    // 遍历所有选择\n    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    // 剪枝三:从 start 开始遍历,避免重复选择同一元素\n    for (int i = start; i < choices.Length; i++) {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // 尝试:做出选择,更新 target, start\n        state.Add(choices[i]);\n        // 进行下一轮选择\n        Backtrack(state, target - choices[i], choices, i + 1, res);\n        // 回退:撤销选择,恢复到之前的状态\n        state.RemoveAt(state.Count - 1);\n    }\n}\n\n/* 求解子集和 II */\nList<List<int>> SubsetSumII(int[] nums, int target) {\n    List<int> state = []; // 状态(子集)\n    Array.Sort(nums); // 对 nums 进行排序\n    int start = 0; // 遍历起始点\n    List<List<int>> res = []; // 结果列表(子集列表)\n    Backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_ii.go
/* 回溯算法:子集和 II */\nfunc backtrackSubsetSumII(start, target int, state, choices *[]int, res *[][]int) {\n    // 子集和等于 target 时,记录解\n    if target == 0 {\n        newState := append([]int{}, *state...)\n        *res = append(*res, newState)\n        return\n    }\n    // 遍历所有选择\n    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    // 剪枝三:从 start 开始遍历,避免重复选择同一元素\n    for i := start; i < len(*choices); i++ {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if target-(*choices)[i] < 0 {\n            break\n        }\n        // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\n        if i > start && (*choices)[i] == (*choices)[i-1] {\n            continue\n        }\n        // 尝试:做出选择,更新 target, start\n        *state = append(*state, (*choices)[i])\n        // 进行下一轮选择\n        backtrackSubsetSumII(i+1, target-(*choices)[i], state, choices, res)\n        // 回退:撤销选择,恢复到之前的状态\n        *state = (*state)[:len(*state)-1]\n    }\n}\n\n/* 求解子集和 II */\nfunc subsetSumII(nums []int, target int) [][]int {\n    state := make([]int, 0) // 状态(子集)\n    sort.Ints(nums)         // 对 nums 进行排序\n    start := 0              // 遍历起始点\n    res := make([][]int, 0) // 结果列表(子集列表)\n    backtrackSubsetSumII(start, target, &state, &nums, &res)\n    return res\n}\n
subset_sum_ii.swift
/* 回溯算法:子集和 II */\nfunc backtrack(state: inout [Int], target: Int, choices: [Int], start: Int, res: inout [[Int]]) {\n    // 子集和等于 target 时,记录解\n    if target == 0 {\n        res.append(state)\n        return\n    }\n    // 遍历所有选择\n    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    // 剪枝三:从 start 开始遍历,避免重复选择同一元素\n    for i in choices.indices.dropFirst(start) {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if target - choices[i] < 0 {\n            break\n        }\n        // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\n        if i > start, choices[i] == choices[i - 1] {\n            continue\n        }\n        // 尝试:做出选择,更新 target, start\n        state.append(choices[i])\n        // 进行下一轮选择\n        backtrack(state: &state, target: target - choices[i], choices: choices, start: i + 1, res: &res)\n        // 回退:撤销选择,恢复到之前的状态\n        state.removeLast()\n    }\n}\n\n/* 求解子集和 II */\nfunc subsetSumII(nums: [Int], target: Int) -> [[Int]] {\n    var state: [Int] = [] // 状态(子集)\n    let nums = nums.sorted() // 对 nums 进行排序\n    let start = 0 // 遍历起始点\n    var res: [[Int]] = [] // 结果列表(子集列表)\n    backtrack(state: &state, target: target, choices: nums, start: start, res: &res)\n    return res\n}\n
subset_sum_ii.js
/* 回溯算法:子集和 II */\nfunction backtrack(state, target, choices, start, res) {\n    // 子集和等于 target 时,记录解\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // 遍历所有选择\n    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    // 剪枝三:从 start 开始遍历,避免重复选择同一元素\n    for (let i = start; i < choices.length; i++) {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\n        if (i > start && choices[i] === choices[i - 1]) {\n            continue;\n        }\n        // 尝试:做出选择,更新 target, start\n        state.push(choices[i]);\n        // 进行下一轮选择\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // 回退:撤销选择,恢复到之前的状态\n        state.pop();\n    }\n}\n\n/* 求解子集和 II */\nfunction subsetSumII(nums, target) {\n    const state = []; // 状态(子集)\n    nums.sort((a, b) => a - b); // 对 nums 进行排序\n    const start = 0; // 遍历起始点\n    const res = []; // 结果列表(子集列表)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_ii.ts
/* 回溯算法:子集和 II */\nfunction backtrack(\n    state: number[],\n    target: number,\n    choices: number[],\n    start: number,\n    res: number[][]\n): void {\n    // 子集和等于 target 时,记录解\n    if (target === 0) {\n        res.push([...state]);\n        return;\n    }\n    // 遍历所有选择\n    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    // 剪枝三:从 start 开始遍历,避免重复选择同一元素\n    for (let i = start; i < choices.length; i++) {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if (target - choices[i] < 0) {\n            break;\n        }\n        // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\n        if (i > start && choices[i] === choices[i - 1]) {\n            continue;\n        }\n        // 尝试:做出选择,更新 target, start\n        state.push(choices[i]);\n        // 进行下一轮选择\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // 回退:撤销选择,恢复到之前的状态\n        state.pop();\n    }\n}\n\n/* 求解子集和 II */\nfunction subsetSumII(nums: number[], target: number): number[][] {\n    const state = []; // 状态(子集)\n    nums.sort((a, b) => a - b); // 对 nums 进行排序\n    const start = 0; // 遍历起始点\n    const res = []; // 结果列表(子集列表)\n    backtrack(state, target, nums, start, res);\n    return res;\n}\n
subset_sum_ii.dart
/* 回溯算法:子集和 II */\nvoid backtrack(\n  List<int> state,\n  int target,\n  List<int> choices,\n  int start,\n  List<List<int>> res,\n) {\n  // 子集和等于 target 时,记录解\n  if (target == 0) {\n    res.add(List.from(state));\n    return;\n  }\n  // 遍历所有选择\n  // 剪枝二:从 start 开始遍历,避免生成重复子集\n  // 剪枝三:从 start 开始遍历,避免重复选择同一元素\n  for (int i = start; i < choices.length; i++) {\n    // 剪枝一:若子集和超过 target ,则直接结束循环\n    // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n    if (target - choices[i] < 0) {\n      break;\n    }\n    // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\n    if (i > start && choices[i] == choices[i - 1]) {\n      continue;\n    }\n    // 尝试:做出选择,更新 target, start\n    state.add(choices[i]);\n    // 进行下一轮选择\n    backtrack(state, target - choices[i], choices, i + 1, res);\n    // 回退:撤销选择,恢复到之前的状态\n    state.removeLast();\n  }\n}\n\n/* 求解子集和 II */\nList<List<int>> subsetSumII(List<int> nums, int target) {\n  List<int> state = []; // 状态(子集)\n  nums.sort(); // 对 nums 进行排序\n  int start = 0; // 遍历起始点\n  List<List<int>> res = []; // 结果列表(子集列表)\n  backtrack(state, target, nums, start, res);\n  return res;\n}\n
subset_sum_ii.rs
/* 回溯算法:子集和 II */\nfn backtrack(\n    state: &mut Vec<i32>,\n    target: i32,\n    choices: &[i32],\n    start: usize,\n    res: &mut Vec<Vec<i32>>,\n) {\n    // 子集和等于 target 时,记录解\n    if target == 0 {\n        res.push(state.clone());\n        return;\n    }\n    // 遍历所有选择\n    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    // 剪枝三:从 start 开始遍历,避免重复选择同一元素\n    for i in start..choices.len() {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if target - choices[i] < 0 {\n            break;\n        }\n        // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\n        if i > start && choices[i] == choices[i - 1] {\n            continue;\n        }\n        // 尝试:做出选择,更新 target, start\n        state.push(choices[i]);\n        // 进行下一轮选择\n        backtrack(state, target - choices[i], choices, i + 1, res);\n        // 回退:撤销选择,恢复到之前的状态\n        state.pop();\n    }\n}\n\n/* 求解子集和 II */\nfn subset_sum_ii(nums: &mut [i32], target: i32) -> Vec<Vec<i32>> {\n    let mut state = Vec::new(); // 状态(子集)\n    nums.sort(); // 对 nums 进行排序\n    let start = 0; // 遍历起始点\n    let mut res = Vec::new(); // 结果列表(子集列表)\n    backtrack(&mut state, target, nums, start, &mut res);\n    res\n}\n
subset_sum_ii.c
/* 回溯算法:子集和 II */\nvoid backtrack(int target, int *choices, int choicesSize, int start) {\n    // 子集和等于 target 时,记录解\n    if (target == 0) {\n        for (int i = 0; i < stateSize; i++) {\n            res[resSize][i] = state[i];\n        }\n        resColSizes[resSize++] = stateSize;\n        return;\n    }\n    // 遍历所有选择\n    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    // 剪枝三:从 start 开始遍历,避免重复选择同一元素\n    for (int i = start; i < choicesSize; i++) {\n        // 剪枝一:若子集和超过 target ,则直接跳过\n        if (target - choices[i] < 0) {\n            continue;\n        }\n        // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue;\n        }\n        // 尝试:做出选择,更新 target, start\n        state[stateSize] = choices[i];\n        stateSize++;\n        // 进行下一轮选择\n        backtrack(target - choices[i], choices, choicesSize, i + 1);\n        // 回退:撤销选择,恢复到之前的状态\n        stateSize--;\n    }\n}\n\n/* 求解子集和 II */\nvoid subsetSumII(int *nums, int numsSize, int target) {\n    // 对 nums 进行排序\n    qsort(nums, numsSize, sizeof(int), cmp);\n    // 开始回溯\n    backtrack(target, nums, numsSize, 0);\n}\n
subset_sum_ii.kt
/* 回溯算法:子集和 II */\nfun backtrack(\n    state: MutableList<Int>,\n    target: Int,\n    choices: IntArray,\n    start: Int,\n    res: MutableList<MutableList<Int>?>\n) {\n    // 子集和等于 target 时,记录解\n    if (target == 0) {\n        res.add(state.toMutableList())\n        return\n    }\n    // 遍历所有选择\n    // 剪枝二:从 start 开始遍历,避免生成重复子集\n    // 剪枝三:从 start 开始遍历,避免重复选择同一元素\n    for (i in start..<choices.size) {\n        // 剪枝一:若子集和超过 target ,则直接结束循环\n        // 这是因为数组已排序,后边元素更大,子集和一定超过 target\n        if (target - choices[i] < 0) {\n            break\n        }\n        // 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\n        if (i > start && choices[i] == choices[i - 1]) {\n            continue\n        }\n        // 尝试:做出选择,更新 target, start\n        state.add(choices[i])\n        // 进行下一轮选择\n        backtrack(state, target - choices[i], choices, i + 1, res)\n        // 回退:撤销选择,恢复到之前的状态\n        state.removeAt(state.size - 1)\n    }\n}\n\n/* 求解子集和 II */\nfun subsetSumII(nums: IntArray, target: Int): MutableList<MutableList<Int>?> {\n    val state = mutableListOf<Int>() // 状态(子集)\n    nums.sort() // 对 nums 进行排序\n    val start = 0 // 遍历起始点\n    val res = mutableListOf<MutableList<Int>?>() // 结果列表(子集列表)\n    backtrack(state, target, nums, start, res)\n    return res\n}\n
subset_sum_ii.rb
### 回溯算法:子集和 II ###\ndef backtrack(state, target, choices, start, res)\n  # 子集和等于 target 时,记录解\n  if target.zero?\n    res << state.dup\n    return\n  end\n\n  # 遍历所有选择\n  # 剪枝二:从 start 开始遍历,避免生成重复子集\n  # 剪枝三:从 start 开始遍历,避免重复选择同一元素\n  for i in start...choices.length\n    # 剪枝一:若子集和超过 target ,则直接结束循环\n    # 这是因为数组已排序,后边元素更大,子集和一定超过 target\n    break if target - choices[i] < 0\n    # 剪枝四:如果该元素与左边元素相等,说明该搜索分支重复,直接跳过\n    next if i > start && choices[i] == choices[i - 1]\n    # 尝试:做出选择,更新 target, start\n    state << choices[i]\n    # 进行下一轮选择\n    backtrack(state, target - choices[i], choices, i + 1, res)\n    # 回退:撤销选择,恢复到之前的状态\n    state.pop\n  end\nend\n\n### 求解子集和 II ###\ndef subset_sum_ii(nums, target)\n  state = [] # 状态(子集)\n  nums.sort! # 对 nums 进行排序\n  start = 0 # 遍历起始点\n  res = [] # 结果列表(子集列表)\n  backtrack(state, target, nums, start, res)\n  res\nend\n
可视化运行

全屏观看 >

图 13-14 展示了数组 \\([4, 4, 5]\\) 和目标元素 \\(9\\) 的回溯过程,共包含四种剪枝操作。请你将图示与代码注释相结合,理解整个搜索过程,以及每种剪枝操作是如何工作的。

图 13-14   子集和 II 回溯过程

","path":["第 13 章   回溯","13.3   子集和问题"],"tags":[]},{"location":"chapter_backtracking/summary/","level":1,"title":"13.5   小结","text":"","path":["第 13 章   回溯","13.5   小结"],"tags":[]},{"location":"chapter_backtracking/summary/#1","level":3,"title":"1.   重点回顾","text":"","path":["第 13 章   回溯","13.5   小结"],"tags":[]},{"location":"chapter_backtracking/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q:怎么理解回溯和递归的关系?

总的来看,回溯是一种“算法策略”,而递归更像是一个“工具”。

","path":["第 13 章   回溯","13.5   小结"],"tags":[]},{"location":"chapter_computational_complexity/","level":1,"title":"第 2 章   复杂度分析","text":"

Abstract

复杂度分析犹如浩瀚的算法宇宙中的时空向导。

它带领我们在时间与空间这两个维度上深入探索,寻找更优雅的解决方案。

","path":["第 2 章   复杂度分析"],"tags":[]},{"location":"chapter_computational_complexity/#_1","level":2,"title":"本章内容","text":"","path":["第 2 章   复杂度分析"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/","level":1,"title":"2.2   迭代与递归","text":"

在算法中,重复执行某个任务是很常见的,它与复杂度分析息息相关。因此,在介绍时间复杂度和空间复杂度之前,我们先来了解如何在程序中实现重复执行任务,即两种基本的程序控制结构:迭代、递归。

","path":["第 2 章   复杂度分析","2.2   迭代与递归"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#221","level":2,"title":"2.2.1   迭代","text":"

迭代(iteration)是一种重复执行某个任务的控制结构。在迭代中,程序会在满足一定的条件下重复执行某段代码,直到这个条件不再满足。

","path":["第 2 章   复杂度分析","2.2   迭代与递归"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#1-for","level":3,"title":"1.   for 循环","text":"

for 循环是最常见的迭代形式之一,适合在预先知道迭代次数时使用。

以下函数基于 for 循环实现了求和 \\(1 + 2 + \\dots + n\\) ,求和结果使用变量 res 记录。需要注意的是,Python 中 range(a, b) 对应的区间是“左闭右开”的,对应的遍历范围为 \\(a, a + 1, \\dots, b-1\\) :

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
def for_loop(n: int) -> int:\n    \"\"\"for 循环\"\"\"\n    res = 0\n    # 循环求和 1, 2, ..., n-1, n\n    for i in range(1, n + 1):\n        res += i\n    return res\n
iteration.cpp
/* for 循环 */\nint forLoop(int n) {\n    int res = 0;\n    // 循环求和 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; ++i) {\n        res += i;\n    }\n    return res;\n}\n
iteration.java
/* for 循环 */\nint forLoop(int n) {\n    int res = 0;\n    // 循环求和 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
iteration.cs
/* for 循环 */\nint ForLoop(int n) {\n    int res = 0;\n    // 循环求和 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
iteration.go
/* for 循环 */\nfunc forLoop(n int) int {\n    res := 0\n    // 循环求和 1, 2, ..., n-1, n\n    for i := 1; i <= n; i++ {\n        res += i\n    }\n    return res\n}\n
iteration.swift
/* for 循环 */\nfunc forLoop(n: Int) -> Int {\n    var res = 0\n    // 循环求和 1, 2, ..., n-1, n\n    for i in 1 ... n {\n        res += i\n    }\n    return res\n}\n
iteration.js
/* for 循环 */\nfunction forLoop(n) {\n    let res = 0;\n    // 循环求和 1, 2, ..., n-1, n\n    for (let i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
iteration.ts
/* for 循环 */\nfunction forLoop(n: number): number {\n    let res = 0;\n    // 循环求和 1, 2, ..., n-1, n\n    for (let i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
iteration.dart
/* for 循环 */\nint forLoop(int n) {\n  int res = 0;\n  // 循环求和 1, 2, ..., n-1, n\n  for (int i = 1; i <= n; i++) {\n    res += i;\n  }\n  return res;\n}\n
iteration.rs
/* for 循环 */\nfn for_loop(n: i32) -> i32 {\n    let mut res = 0;\n    // 循环求和 1, 2, ..., n-1, n\n    for i in 1..=n {\n        res += i;\n    }\n    res\n}\n
iteration.c
/* for 循环 */\nint forLoop(int n) {\n    int res = 0;\n    // 循环求和 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        res += i;\n    }\n    return res;\n}\n
iteration.kt
/* for 循环 */\nfun forLoop(n: Int): Int {\n    var res = 0\n    // 循环求和 1, 2, ..., n-1, n\n    for (i in 1..n) {\n        res += i\n    }\n    return res\n}\n
iteration.rb
### for 循环 ###\ndef for_loop(n)\n  res = 0\n\n  # 循环求和 1, 2, ..., n-1, n\n  for i in 1..n\n    res += i\n  end\n\n  res\nend\n
可视化运行

全屏观看 >

图 2-1 是该求和函数的流程框图。

图 2-1   求和函数的流程框图

此求和函数的操作数量与输入数据大小 \\(n\\) 成正比,或者说成“线性关系”。实际上,时间复杂度描述的就是这个“线性关系”。相关内容将会在下一节中详细介绍。

","path":["第 2 章   复杂度分析","2.2   迭代与递归"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#2-while","level":3,"title":"2.   while 循环","text":"

for 循环类似,while 循环也是一种实现迭代的方法。在 while 循环中,程序每轮都会先检查条件,如果条件为真,则继续执行,否则就结束循环。

下面我们用 while 循环来实现求和 \\(1 + 2 + \\dots + n\\) :

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
def while_loop(n: int) -> int:\n    \"\"\"while 循环\"\"\"\n    res = 0\n    i = 1  # 初始化条件变量\n    # 循环求和 1, 2, ..., n-1, n\n    while i <= n:\n        res += i\n        i += 1  # 更新条件变量\n    return res\n
iteration.cpp
/* while 循环 */\nint whileLoop(int n) {\n    int res = 0;\n    int i = 1; // 初始化条件变量\n    // 循环求和 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // 更新条件变量\n    }\n    return res;\n}\n
iteration.java
/* while 循环 */\nint whileLoop(int n) {\n    int res = 0;\n    int i = 1; // 初始化条件变量\n    // 循环求和 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // 更新条件变量\n    }\n    return res;\n}\n
iteration.cs
/* while 循环 */\nint WhileLoop(int n) {\n    int res = 0;\n    int i = 1; // 初始化条件变量\n    // 循环求和 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i += 1; // 更新条件变量\n    }\n    return res;\n}\n
iteration.go
/* while 循环 */\nfunc whileLoop(n int) int {\n    res := 0\n    // 初始化条件变量\n    i := 1\n    // 循环求和 1, 2, ..., n-1, n\n    for i <= n {\n        res += i\n        // 更新条件变量\n        i++\n    }\n    return res\n}\n
iteration.swift
/* while 循环 */\nfunc whileLoop(n: Int) -> Int {\n    var res = 0\n    var i = 1 // 初始化条件变量\n    // 循环求和 1, 2, ..., n-1, n\n    while i <= n {\n        res += i\n        i += 1 // 更新条件变量\n    }\n    return res\n}\n
iteration.js
/* while 循环 */\nfunction whileLoop(n) {\n    let res = 0;\n    let i = 1; // 初始化条件变量\n    // 循环求和 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // 更新条件变量\n    }\n    return res;\n}\n
iteration.ts
/* while 循环 */\nfunction whileLoop(n: number): number {\n    let res = 0;\n    let i = 1; // 初始化条件变量\n    // 循环求和 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // 更新条件变量\n    }\n    return res;\n}\n
iteration.dart
/* while 循环 */\nint whileLoop(int n) {\n  int res = 0;\n  int i = 1; // 初始化条件变量\n  // 循环求和 1, 2, ..., n-1, n\n  while (i <= n) {\n    res += i;\n    i++; // 更新条件变量\n  }\n  return res;\n}\n
iteration.rs
/* while 循环 */\nfn while_loop(n: i32) -> i32 {\n    let mut res = 0;\n    let mut i = 1; // 初始化条件变量\n\n    // 循环求和 1, 2, ..., n-1, n\n    while i <= n {\n        res += i;\n        i += 1; // 更新条件变量\n    }\n    res\n}\n
iteration.c
/* while 循环 */\nint whileLoop(int n) {\n    int res = 0;\n    int i = 1; // 初始化条件变量\n    // 循环求和 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i;\n        i++; // 更新条件变量\n    }\n    return res;\n}\n
iteration.kt
/* while 循环 */\nfun whileLoop(n: Int): Int {\n    var res = 0\n    var i = 1 // 初始化条件变量\n    // 循环求和 1, 2, ..., n-1, n\n    while (i <= n) {\n        res += i\n        i++ // 更新条件变量\n    }\n    return res\n}\n
iteration.rb
### while 循环 ###\ndef while_loop(n)\n  res = 0\n  i = 1 # 初始化条件变量\n\n  # 循环求和 1, 2, ..., n-1, n\n  while i <= n\n    res += i\n    i += 1 # 更新条件变量\n  end\n\n  res\nend\n
可视化运行

全屏观看 >

while 循环比 for 循环的自由度更高。在 while 循环中,我们可以自由地设计条件变量的初始化和更新步骤。

例如在以下代码中,条件变量 \\(i\\) 每轮进行两次更新,这种情况就不太方便用 for 循环实现:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
def while_loop_ii(n: int) -> int:\n    \"\"\"while 循环(两次更新)\"\"\"\n    res = 0\n    i = 1  # 初始化条件变量\n    # 循环求和 1, 4, 10, ...\n    while i <= n:\n        res += i\n        # 更新条件变量\n        i += 1\n        i *= 2\n    return res\n
iteration.cpp
/* while 循环(两次更新) */\nint whileLoopII(int n) {\n    int res = 0;\n    int i = 1; // 初始化条件变量\n    // 循环求和 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // 更新条件变量\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
iteration.java
/* while 循环(两次更新) */\nint whileLoopII(int n) {\n    int res = 0;\n    int i = 1; // 初始化条件变量\n    // 循环求和 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // 更新条件变量\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
iteration.cs
/* while 循环(两次更新) */\nint WhileLoopII(int n) {\n    int res = 0;\n    int i = 1; // 初始化条件变量\n    // 循环求和 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // 更新条件变量\n        i += 1; \n        i *= 2;\n    }\n    return res;\n}\n
iteration.go
/* while 循环(两次更新) */\nfunc whileLoopII(n int) int {\n    res := 0\n    // 初始化条件变量\n    i := 1\n    // 循环求和 1, 4, 10, ...\n    for i <= n {\n        res += i\n        // 更新条件变量\n        i++\n        i *= 2\n    }\n    return res\n}\n
iteration.swift
/* while 循环(两次更新) */\nfunc whileLoopII(n: Int) -> Int {\n    var res = 0\n    var i = 1 // 初始化条件变量\n    // 循环求和 1, 4, 10, ...\n    while i <= n {\n        res += i\n        // 更新条件变量\n        i += 1\n        i *= 2\n    }\n    return res\n}\n
iteration.js
/* while 循环(两次更新) */\nfunction whileLoopII(n) {\n    let res = 0;\n    let i = 1; // 初始化条件变量\n    // 循环求和 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // 更新条件变量\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
iteration.ts
/* while 循环(两次更新) */\nfunction whileLoopII(n: number): number {\n    let res = 0;\n    let i = 1; // 初始化条件变量\n    // 循环求和 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // 更新条件变量\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
iteration.dart
/* while 循环(两次更新) */\nint whileLoopII(int n) {\n  int res = 0;\n  int i = 1; // 初始化条件变量\n  // 循环求和 1, 4, 10, ...\n  while (i <= n) {\n    res += i;\n    // 更新条件变量\n    i++;\n    i *= 2;\n  }\n  return res;\n}\n
iteration.rs
/* while 循环(两次更新) */\nfn while_loop_ii(n: i32) -> i32 {\n    let mut res = 0;\n    let mut i = 1; // 初始化条件变量\n\n    // 循环求和 1, 4, 10, ...\n    while i <= n {\n        res += i;\n        // 更新条件变量\n        i += 1;\n        i *= 2;\n    }\n    res\n}\n
iteration.c
/* while 循环(两次更新) */\nint whileLoopII(int n) {\n    int res = 0;\n    int i = 1; // 初始化条件变量\n    // 循环求和 1, 4, 10, ...\n    while (i <= n) {\n        res += i;\n        // 更新条件变量\n        i++;\n        i *= 2;\n    }\n    return res;\n}\n
iteration.kt
/* while 循环(两次更新) */\nfun whileLoopII(n: Int): Int {\n    var res = 0\n    var i = 1 // 初始化条件变量\n    // 循环求和 1, 4, 10, ...\n    while (i <= n) {\n        res += i\n        // 更新条件变量\n        i++\n        i *= 2\n    }\n    return res\n}\n
iteration.rb
### while 循环(两次更新)###\ndef while_loop_ii(n)\n  res = 0\n  i = 1 # 初始化条件变量\n\n  # 循环求和 1, 4, 10, ...\n  while i <= n\n    res += i\n    # 更新条件变量\n    i += 1\n    i *= 2\n  end\n\n  res\nend\n
可视化运行

全屏观看 >

总的来说,for 循环的代码更加紧凑,while 循环更加灵活,两者都可以实现迭代结构。选择使用哪一个应该根据特定问题的需求来决定。

","path":["第 2 章   复杂度分析","2.2   迭代与递归"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#3","level":3,"title":"3.   嵌套循环","text":"

我们可以在一个循环结构内嵌套另一个循环结构,下面以 for 循环为例:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby iteration.py
def nested_for_loop(n: int) -> str:\n    \"\"\"双层 for 循环\"\"\"\n    res = \"\"\n    # 循环 i = 1, 2, ..., n-1, n\n    for i in range(1, n + 1):\n        # 循环 j = 1, 2, ..., n-1, n\n        for j in range(1, n + 1):\n            res += f\"({i}, {j}), \"\n    return res\n
iteration.cpp
/* 双层 for 循环 */\nstring nestedForLoop(int n) {\n    ostringstream res;\n    // 循环 i = 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; ++i) {\n        // 循环 j = 1, 2, ..., n-1, n\n        for (int j = 1; j <= n; ++j) {\n            res << \"(\" << i << \", \" << j << \"), \";\n        }\n    }\n    return res.str();\n}\n
iteration.java
/* 双层 for 循环 */\nString nestedForLoop(int n) {\n    StringBuilder res = new StringBuilder();\n    // 循环 i = 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        // 循环 j = 1, 2, ..., n-1, n\n        for (int j = 1; j <= n; j++) {\n            res.append(\"(\" + i + \", \" + j + \"), \");\n        }\n    }\n    return res.toString();\n}\n
iteration.cs
/* 双层 for 循环 */\nstring NestedForLoop(int n) {\n    StringBuilder res = new();\n    // 循环 i = 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        // 循环 j = 1, 2, ..., n-1, n\n        for (int j = 1; j <= n; j++) {\n            res.Append($\"({i}, {j}), \");\n        }\n    }\n    return res.ToString();\n}\n
iteration.go
/* 双层 for 循环 */\nfunc nestedForLoop(n int) string {\n    res := \"\"\n    // 循环 i = 1, 2, ..., n-1, n\n    for i := 1; i <= n; i++ {\n        for j := 1; j <= n; j++ {\n            // 循环 j = 1, 2, ..., n-1, n\n            res += fmt.Sprintf(\"(%d, %d), \", i, j)\n        }\n    }\n    return res\n}\n
iteration.swift
/* 双层 for 循环 */\nfunc nestedForLoop(n: Int) -> String {\n    var res = \"\"\n    // 循环 i = 1, 2, ..., n-1, n\n    for i in 1 ... n {\n        // 循环 j = 1, 2, ..., n-1, n\n        for j in 1 ... n {\n            res.append(\"(\\(i), \\(j)), \")\n        }\n    }\n    return res\n}\n
iteration.js
/* 双层 for 循环 */\nfunction nestedForLoop(n) {\n    let res = '';\n    // 循环 i = 1, 2, ..., n-1, n\n    for (let i = 1; i <= n; i++) {\n        // 循环 j = 1, 2, ..., n-1, n\n        for (let j = 1; j <= n; j++) {\n            res += `(${i}, ${j}), `;\n        }\n    }\n    return res;\n}\n
iteration.ts
/* 双层 for 循环 */\nfunction nestedForLoop(n: number): string {\n    let res = '';\n    // 循环 i = 1, 2, ..., n-1, n\n    for (let i = 1; i <= n; i++) {\n        // 循环 j = 1, 2, ..., n-1, n\n        for (let j = 1; j <= n; j++) {\n            res += `(${i}, ${j}), `;\n        }\n    }\n    return res;\n}\n
iteration.dart
/* 双层 for 循环 */\nString nestedForLoop(int n) {\n  String res = \"\";\n  // 循环 i = 1, 2, ..., n-1, n\n  for (int i = 1; i <= n; i++) {\n    // 循环 j = 1, 2, ..., n-1, n\n    for (int j = 1; j <= n; j++) {\n      res += \"($i, $j), \";\n    }\n  }\n  return res;\n}\n
iteration.rs
/* 双层 for 循环 */\nfn nested_for_loop(n: i32) -> String {\n    let mut res = vec![];\n    // 循环 i = 1, 2, ..., n-1, n\n    for i in 1..=n {\n        // 循环 j = 1, 2, ..., n-1, n\n        for j in 1..=n {\n            res.push(format!(\"({}, {}), \", i, j));\n        }\n    }\n    res.join(\"\")\n}\n
iteration.c
/* 双层 for 循环 */\nchar *nestedForLoop(int n) {\n    // n * n 为对应点数量,\"(i, j), \" 对应字符串长最大为 6+10*2,加上最后一个空字符 \\0 的额外空间\n    int size = n * n * 26 + 1;\n    char *res = malloc(size * sizeof(char));\n    // 循环 i = 1, 2, ..., n-1, n\n    for (int i = 1; i <= n; i++) {\n        // 循环 j = 1, 2, ..., n-1, n\n        for (int j = 1; j <= n; j++) {\n            char tmp[26];\n            snprintf(tmp, sizeof(tmp), \"(%d, %d), \", i, j);\n            strncat(res, tmp, size - strlen(res) - 1);\n        }\n    }\n    return res;\n}\n
iteration.kt
/* 双层 for 循环 */\nfun nestedForLoop(n: Int): String {\n    val res = StringBuilder()\n    // 循环 i = 1, 2, ..., n-1, n\n    for (i in 1..n) {\n        // 循环 j = 1, 2, ..., n-1, n\n        for (j in 1..n) {\n            res.append(\" ($i, $j), \")\n        }\n    }\n    return res.toString()\n}\n
iteration.rb
### 双层 for 循环 ###\ndef nested_for_loop(n)\n  res = \"\"\n\n  # 循环 i = 1, 2, ..., n-1, n\n  for i in 1..n\n    # 循环 j = 1, 2, ..., n-1, n\n    for j in 1..n\n      res += \"(#{i}, #{j}), \"\n    end\n  end\n\n  res\nend\n
可视化运行

全屏观看 >

图 2-2 是该嵌套循环的流程框图。

图 2-2   嵌套循环的流程框图

在这种情况下,函数的操作数量与 \\(n^2\\) 成正比,或者说算法运行时间和输入数据大小 \\(n\\) 成“平方关系”。

我们可以继续添加嵌套循环,每一次嵌套都是一次“升维”,将会使时间复杂度提高至“立方关系”“四次方关系”,以此类推。

","path":["第 2 章   复杂度分析","2.2   迭代与递归"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#222","level":2,"title":"2.2.2   递归","text":"

递归(recursion)是一种算法策略,通过函数调用自身来解决问题。它主要包含两个阶段。

  1. 递:程序不断深入地调用自身,通常传入更小或更简化的参数,直到达到“终止条件”。
  2. 归:触发“终止条件”后,程序从最深层的递归函数开始逐层返回,汇聚每一层的结果。

而从实现的角度看,递归代码主要包含三个要素。

  1. 终止条件:用于决定什么时候由“递”转“归”。
  2. 递归调用:对应“递”,函数调用自身,通常输入更小或更简化的参数。
  3. 返回结果:对应“归”,将当前递归层级的结果返回至上一层。

观察以下代码,我们只需调用函数 recur(n) ,就可以完成 \\(1 + 2 + \\dots + n\\) 的计算:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
def recur(n: int) -> int:\n    \"\"\"递归\"\"\"\n    # 终止条件\n    if n == 1:\n        return 1\n    # 递:递归调用\n    res = recur(n - 1)\n    # 归:返回结果\n    return n + res\n
recursion.cpp
/* 递归 */\nint recur(int n) {\n    // 终止条件\n    if (n == 1)\n        return 1;\n    // 递:递归调用\n    int res = recur(n - 1);\n    // 归:返回结果\n    return n + res;\n}\n
recursion.java
/* 递归 */\nint recur(int n) {\n    // 终止条件\n    if (n == 1)\n        return 1;\n    // 递:递归调用\n    int res = recur(n - 1);\n    // 归:返回结果\n    return n + res;\n}\n
recursion.cs
/* 递归 */\nint Recur(int n) {\n    // 终止条件\n    if (n == 1)\n        return 1;\n    // 递:递归调用\n    int res = Recur(n - 1);\n    // 归:返回结果\n    return n + res;\n}\n
recursion.go
/* 递归 */\nfunc recur(n int) int {\n    // 终止条件\n    if n == 1 {\n        return 1\n    }\n    // 递:递归调用\n    res := recur(n - 1)\n    // 归:返回结果\n    return n + res\n}\n
recursion.swift
/* 递归 */\nfunc recur(n: Int) -> Int {\n    // 终止条件\n    if n == 1 {\n        return 1\n    }\n    // 递:递归调用\n    let res = recur(n: n - 1)\n    // 归:返回结果\n    return n + res\n}\n
recursion.js
/* 递归 */\nfunction recur(n) {\n    // 终止条件\n    if (n === 1) return 1;\n    // 递:递归调用\n    const res = recur(n - 1);\n    // 归:返回结果\n    return n + res;\n}\n
recursion.ts
/* 递归 */\nfunction recur(n: number): number {\n    // 终止条件\n    if (n === 1) return 1;\n    // 递:递归调用\n    const res = recur(n - 1);\n    // 归:返回结果\n    return n + res;\n}\n
recursion.dart
/* 递归 */\nint recur(int n) {\n  // 终止条件\n  if (n == 1) return 1;\n  // 递:递归调用\n  int res = recur(n - 1);\n  // 归:返回结果\n  return n + res;\n}\n
recursion.rs
/* 递归 */\nfn recur(n: i32) -> i32 {\n    // 终止条件\n    if n == 1 {\n        return 1;\n    }\n    // 递:递归调用\n    let res = recur(n - 1);\n    // 归:返回结果\n    n + res\n}\n
recursion.c
/* 递归 */\nint recur(int n) {\n    // 终止条件\n    if (n == 1)\n        return 1;\n    // 递:递归调用\n    int res = recur(n - 1);\n    // 归:返回结果\n    return n + res;\n}\n
recursion.kt
/* 递归 */\nfun recur(n: Int): Int {\n    // 终止条件\n    if (n == 1)\n        return 1\n    // 递: 递归调用\n    val res = recur(n - 1)\n    // 归: 返回结果\n    return n + res\n}\n
recursion.rb
### 递归 ###\ndef recur(n)\n  # 终止条件\n  return 1 if n == 1\n  # 递:递归调用\n  res = recur(n - 1)\n  # 归:返回结果\n  n + res\nend\n
可视化运行

全屏观看 >

图 2-3 展示了该函数的递归过程。

图 2-3   求和函数的递归过程

虽然从计算角度看,迭代与递归可以得到相同的结果,但它们代表了两种完全不同的思考和解决问题的范式。

以上述求和函数为例,设问题 \\(f(n) = 1 + 2 + \\dots + n\\) 。

","path":["第 2 章   复杂度分析","2.2   迭代与递归"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#1","level":3,"title":"1.   调用栈","text":"

递归函数每次调用自身时,系统都会为新开启的函数分配内存,以存储局部变量、调用地址和其他信息等。这将导致两方面的结果。

如图 2-4 所示,在触发终止条件前,同时存在 \\(n\\) 个未返回的递归函数,递归深度为 \\(n\\) 。

图 2-4   递归调用深度

在实际中,编程语言允许的递归深度通常是有限的,过深的递归可能导致栈溢出错误。

","path":["第 2 章   复杂度分析","2.2   迭代与递归"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#2","level":3,"title":"2.   尾递归","text":"

有趣的是,如果函数在返回前的最后一步才进行递归调用,则该函数可以被编译器或解释器优化,使其在空间效率上与迭代相当。这种情况被称为尾递归(tail recursion)。

以计算 \\(1 + 2 + \\dots + n\\) 为例,我们可以将结果变量 res 设为函数参数,从而实现尾递归:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
def tail_recur(n, res):\n    \"\"\"尾递归\"\"\"\n    # 终止条件\n    if n == 0:\n        return res\n    # 尾递归调用\n    return tail_recur(n - 1, res + n)\n
recursion.cpp
/* 尾递归 */\nint tailRecur(int n, int res) {\n    // 终止条件\n    if (n == 0)\n        return res;\n    // 尾递归调用\n    return tailRecur(n - 1, res + n);\n}\n
recursion.java
/* 尾递归 */\nint tailRecur(int n, int res) {\n    // 终止条件\n    if (n == 0)\n        return res;\n    // 尾递归调用\n    return tailRecur(n - 1, res + n);\n}\n
recursion.cs
/* 尾递归 */\nint TailRecur(int n, int res) {\n    // 终止条件\n    if (n == 0)\n        return res;\n    // 尾递归调用\n    return TailRecur(n - 1, res + n);\n}\n
recursion.go
/* 尾递归 */\nfunc tailRecur(n int, res int) int {\n    // 终止条件\n    if n == 0 {\n        return res\n    }\n    // 尾递归调用\n    return tailRecur(n-1, res+n)\n}\n
recursion.swift
/* 尾递归 */\nfunc tailRecur(n: Int, res: Int) -> Int {\n    // 终止条件\n    if n == 0 {\n        return res\n    }\n    // 尾递归调用\n    return tailRecur(n: n - 1, res: res + n)\n}\n
recursion.js
/* 尾递归 */\nfunction tailRecur(n, res) {\n    // 终止条件\n    if (n === 0) return res;\n    // 尾递归调用\n    return tailRecur(n - 1, res + n);\n}\n
recursion.ts
/* 尾递归 */\nfunction tailRecur(n: number, res: number): number {\n    // 终止条件\n    if (n === 0) return res;\n    // 尾递归调用\n    return tailRecur(n - 1, res + n);\n}\n
recursion.dart
/* 尾递归 */\nint tailRecur(int n, int res) {\n  // 终止条件\n  if (n == 0) return res;\n  // 尾递归调用\n  return tailRecur(n - 1, res + n);\n}\n
recursion.rs
/* 尾递归 */\nfn tail_recur(n: i32, res: i32) -> i32 {\n    // 终止条件\n    if n == 0 {\n        return res;\n    }\n    // 尾递归调用\n    tail_recur(n - 1, res + n)\n}\n
recursion.c
/* 尾递归 */\nint tailRecur(int n, int res) {\n    // 终止条件\n    if (n == 0)\n        return res;\n    // 尾递归调用\n    return tailRecur(n - 1, res + n);\n}\n
recursion.kt
/* 尾递归 */\ntailrec fun tailRecur(n: Int, res: Int): Int {\n    // 添加 tailrec 关键词,以开启尾递归优化\n    // 终止条件\n    if (n == 0)\n        return res\n    // 尾递归调用\n    return tailRecur(n - 1, res + n)\n}\n
recursion.rb
### 尾递归 ###\ndef tail_recur(n, res)\n  # 终止条件\n  return res if n == 0\n  # 尾递归调用\n  tail_recur(n - 1, res + n)\nend\n
可视化运行

全屏观看 >

尾递归的执行过程如图 2-5 所示。对比普通递归和尾递归,两者的求和操作的执行点是不同的。

图 2-5   尾递归过程

Tip

请注意,许多编译器或解释器并不支持尾递归优化。例如,Python 默认不支持尾递归优化,因此即使函数是尾递归形式,仍然可能会遇到栈溢出问题。

","path":["第 2 章   复杂度分析","2.2   迭代与递归"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#3_1","level":3,"title":"3.   递归树","text":"

当处理与“分治”相关的算法问题时,递归往往比迭代的思路更加直观、代码更加易读。以“斐波那契数列”为例。

Question

给定一个斐波那契数列 \\(0, 1, 1, 2, 3, 5, 8, 13, \\dots\\) ,求该数列的第 \\(n\\) 个数字。

设斐波那契数列的第 \\(n\\) 个数字为 \\(f(n)\\) ,易得两个结论。

按照递推关系进行递归调用,将前两个数字作为终止条件,便可写出递归代码。调用 fib(n) 即可得到斐波那契数列的第 \\(n\\) 个数字:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
def fib(n: int) -> int:\n    \"\"\"斐波那契数列:递归\"\"\"\n    # 终止条件 f(1) = 0, f(2) = 1\n    if n == 1 or n == 2:\n        return n - 1\n    # 递归调用 f(n) = f(n-1) + f(n-2)\n    res = fib(n - 1) + fib(n - 2)\n    # 返回结果 f(n)\n    return res\n
recursion.cpp
/* 斐波那契数列:递归 */\nint fib(int n) {\n    // 终止条件 f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // 递归调用 f(n) = f(n-1) + f(n-2)\n    int res = fib(n - 1) + fib(n - 2);\n    // 返回结果 f(n)\n    return res;\n}\n
recursion.java
/* 斐波那契数列:递归 */\nint fib(int n) {\n    // 终止条件 f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // 递归调用 f(n) = f(n-1) + f(n-2)\n    int res = fib(n - 1) + fib(n - 2);\n    // 返回结果 f(n)\n    return res;\n}\n
recursion.cs
/* 斐波那契数列:递归 */\nint Fib(int n) {\n    // 终止条件 f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // 递归调用 f(n) = f(n-1) + f(n-2)\n    int res = Fib(n - 1) + Fib(n - 2);\n    // 返回结果 f(n)\n    return res;\n}\n
recursion.go
/* 斐波那契数列:递归 */\nfunc fib(n int) int {\n    // 终止条件 f(1) = 0, f(2) = 1\n    if n == 1 || n == 2 {\n        return n - 1\n    }\n    // 递归调用 f(n) = f(n-1) + f(n-2)\n    res := fib(n-1) + fib(n-2)\n    // 返回结果 f(n)\n    return res\n}\n
recursion.swift
/* 斐波那契数列:递归 */\nfunc fib(n: Int) -> Int {\n    // 终止条件 f(1) = 0, f(2) = 1\n    if n == 1 || n == 2 {\n        return n - 1\n    }\n    // 递归调用 f(n) = f(n-1) + f(n-2)\n    let res = fib(n: n - 1) + fib(n: n - 2)\n    // 返回结果 f(n)\n    return res\n}\n
recursion.js
/* 斐波那契数列:递归 */\nfunction fib(n) {\n    // 终止条件 f(1) = 0, f(2) = 1\n    if (n === 1 || n === 2) return n - 1;\n    // 递归调用 f(n) = f(n-1) + f(n-2)\n    const res = fib(n - 1) + fib(n - 2);\n    // 返回结果 f(n)\n    return res;\n}\n
recursion.ts
/* 斐波那契数列:递归 */\nfunction fib(n: number): number {\n    // 终止条件 f(1) = 0, f(2) = 1\n    if (n === 1 || n === 2) return n - 1;\n    // 递归调用 f(n) = f(n-1) + f(n-2)\n    const res = fib(n - 1) + fib(n - 2);\n    // 返回结果 f(n)\n    return res;\n}\n
recursion.dart
/* 斐波那契数列:递归 */\nint fib(int n) {\n  // 终止条件 f(1) = 0, f(2) = 1\n  if (n == 1 || n == 2) return n - 1;\n  // 递归调用 f(n) = f(n-1) + f(n-2)\n  int res = fib(n - 1) + fib(n - 2);\n  // 返回结果 f(n)\n  return res;\n}\n
recursion.rs
/* 斐波那契数列:递归 */\nfn fib(n: i32) -> i32 {\n    // 终止条件 f(1) = 0, f(2) = 1\n    if n == 1 || n == 2 {\n        return n - 1;\n    }\n    // 递归调用 f(n) = f(n-1) + f(n-2)\n    let res = fib(n - 1) + fib(n - 2);\n    // 返回结果\n    res\n}\n
recursion.c
/* 斐波那契数列:递归 */\nint fib(int n) {\n    // 终止条件 f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1;\n    // 递归调用 f(n) = f(n-1) + f(n-2)\n    int res = fib(n - 1) + fib(n - 2);\n    // 返回结果 f(n)\n    return res;\n}\n
recursion.kt
/* 斐波那契数列:递归 */\nfun fib(n: Int): Int {\n    // 终止条件 f(1) = 0, f(2) = 1\n    if (n == 1 || n == 2)\n        return n - 1\n    // 递归调用 f(n) = f(n-1) + f(n-2)\n    val res = fib(n - 1) + fib(n - 2)\n    // 返回结果 f(n)\n    return res\n}\n
recursion.rb
### 斐波那契数列:递归 ###\ndef fib(n)\n  # 终止条件 f(1) = 0, f(2) = 1\n  return n - 1 if n == 1 || n == 2\n  # 递归调用 f(n) = f(n-1) + f(n-2)\n  res = fib(n - 1) + fib(n - 2)\n  # 返回结果 f(n)\n  res\nend\n
可视化运行

全屏观看 >

观察以上代码,我们在函数内递归调用了两个函数,这意味着从一个调用产生了两个调用分支。如图 2-6 所示,这样不断递归调用下去,最终将产生一棵层数为 \\(n\\) 的递归树(recursion tree)。

图 2-6   斐波那契数列的递归树

从本质上看,递归体现了“将问题分解为更小子问题”的思维范式,这种分治策略至关重要。

","path":["第 2 章   复杂度分析","2.2   迭代与递归"],"tags":[]},{"location":"chapter_computational_complexity/iteration_and_recursion/#223","level":2,"title":"2.2.3   两者对比","text":"

总结以上内容,如表 2-1 所示,迭代和递归在实现、性能和适用性上有所不同。

表 2-1   迭代与递归特点对比

迭代 递归 实现方式 循环结构 函数调用自身 时间效率 效率通常较高,无函数调用开销 每次函数调用都会产生开销 内存使用 通常使用固定大小的内存空间 累积函数调用可能使用大量的栈帧空间 适用问题 适用于简单循环任务,代码直观、可读性好 适用于子问题分解,如树、图、分治、回溯等,代码结构简洁、清晰

Tip

如果感觉以下内容理解困难,可以在读完“栈”章节后再来复习。

那么,迭代和递归具有什么内在联系呢?以上述递归函数为例,求和操作在递归的“归”阶段进行。这意味着最初被调用的函数实际上是最后完成其求和操作的,这种工作机制与栈的“先入后出”原则异曲同工。

事实上,“调用栈”和“栈帧空间”这类递归术语已经暗示了递归与栈之间的密切关系。

  1. 递:当函数被调用时,系统会在“调用栈”上为该函数分配新的栈帧,用于存储函数的局部变量、参数、返回地址等数据。
  2. 归:当函数完成执行并返回时,对应的栈帧会被从“调用栈”上移除,恢复之前函数的执行环境。

因此,我们可以使用一个显式的栈来模拟调用栈的行为,从而将递归转化为迭代形式:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby recursion.py
def for_loop_recur(n: int) -> int:\n    \"\"\"使用迭代模拟递归\"\"\"\n    # 使用一个显式的栈来模拟系统调用栈\n    stack = []\n    res = 0\n    # 递:递归调用\n    for i in range(n, 0, -1):\n        # 通过“入栈操作”模拟“递”\n        stack.append(i)\n    # 归:返回结果\n    while stack:\n        # 通过“出栈操作”模拟“归”\n        res += stack.pop()\n    # res = 1+2+3+...+n\n    return res\n
recursion.cpp
/* 使用迭代模拟递归 */\nint forLoopRecur(int n) {\n    // 使用一个显式的栈来模拟系统调用栈\n    stack<int> stack;\n    int res = 0;\n    // 递:递归调用\n    for (int i = n; i > 0; i--) {\n        // 通过“入栈操作”模拟“递”\n        stack.push(i);\n    }\n    // 归:返回结果\n    while (!stack.empty()) {\n        // 通过“出栈操作”模拟“归”\n        res += stack.top();\n        stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
recursion.java
/* 使用迭代模拟递归 */\nint forLoopRecur(int n) {\n    // 使用一个显式的栈来模拟系统调用栈\n    Stack<Integer> stack = new Stack<>();\n    int res = 0;\n    // 递:递归调用\n    for (int i = n; i > 0; i--) {\n        // 通过“入栈操作”模拟“递”\n        stack.push(i);\n    }\n    // 归:返回结果\n    while (!stack.isEmpty()) {\n        // 通过“出栈操作”模拟“归”\n        res += stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
recursion.cs
/* 使用迭代模拟递归 */\nint ForLoopRecur(int n) {\n    // 使用一个显式的栈来模拟系统调用栈\n    Stack<int> stack = new();\n    int res = 0;\n    // 递:递归调用\n    for (int i = n; i > 0; i--) {\n        // 通过“入栈操作”模拟“递”\n        stack.Push(i);\n    }\n    // 归:返回结果\n    while (stack.Count > 0) {\n        // 通过“出栈操作”模拟“归”\n        res += stack.Pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
recursion.go
/* 使用迭代模拟递归 */\nfunc forLoopRecur(n int) int {\n    // 使用一个显式的栈来模拟系统调用栈\n    stack := list.New()\n    res := 0\n    // 递:递归调用\n    for i := n; i > 0; i-- {\n        // 通过“入栈操作”模拟“递”\n        stack.PushBack(i)\n    }\n    // 归:返回结果\n    for stack.Len() != 0 {\n        // 通过“出栈操作”模拟“归”\n        res += stack.Back().Value.(int)\n        stack.Remove(stack.Back())\n    }\n    // res = 1+2+3+...+n\n    return res\n}\n
recursion.swift
/* 使用迭代模拟递归 */\nfunc forLoopRecur(n: Int) -> Int {\n    // 使用一个显式的栈来模拟系统调用栈\n    var stack: [Int] = []\n    var res = 0\n    // 递:递归调用\n    for i in (1 ... n).reversed() {\n        // 通过“入栈操作”模拟“递”\n        stack.append(i)\n    }\n    // 归:返回结果\n    while !stack.isEmpty {\n        // 通过“出栈操作”模拟“归”\n        res += stack.removeLast()\n    }\n    // res = 1+2+3+...+n\n    return res\n}\n
recursion.js
/* 使用迭代模拟递归 */\nfunction forLoopRecur(n) {\n    // 使用一个显式的栈来模拟系统调用栈\n    const stack = [];\n    let res = 0;\n    // 递:递归调用\n    for (let i = n; i > 0; i--) {\n        // 通过“入栈操作”模拟“递”\n        stack.push(i);\n    }\n    // 归:返回结果\n    while (stack.length) {\n        // 通过“出栈操作”模拟“归”\n        res += stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
recursion.ts
/* 使用迭代模拟递归 */\nfunction forLoopRecur(n: number): number {\n    // 使用一个显式的栈来模拟系统调用栈 \n    const stack: number[] = [];\n    let res: number = 0;\n    // 递:递归调用\n    for (let i = n; i > 0; i--) {\n        // 通过“入栈操作”模拟“递”\n        stack.push(i);\n    }\n    // 归:返回结果\n    while (stack.length) {\n        // 通过“出栈操作”模拟“归”\n        res += stack.pop();\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
recursion.dart
/* 使用迭代模拟递归 */\nint forLoopRecur(int n) {\n  // 使用一个显式的栈来模拟系统调用栈\n  List<int> stack = [];\n  int res = 0;\n  // 递:递归调用\n  for (int i = n; i > 0; i--) {\n    // 通过“入栈操作”模拟“递”\n    stack.add(i);\n  }\n  // 归:返回结果\n  while (!stack.isEmpty) {\n    // 通过“出栈操作”模拟“归”\n    res += stack.removeLast();\n  }\n  // res = 1+2+3+...+n\n  return res;\n}\n
recursion.rs
/* 使用迭代模拟递归 */\nfn for_loop_recur(n: i32) -> i32 {\n    // 使用一个显式的栈来模拟系统调用栈\n    let mut stack = Vec::new();\n    let mut res = 0;\n    // 递:递归调用\n    for i in (1..=n).rev() {\n        // 通过“入栈操作”模拟“递”\n        stack.push(i);\n    }\n    // 归:返回结果\n    while !stack.is_empty() {\n        // 通过“出栈操作”模拟“归”\n        res += stack.pop().unwrap();\n    }\n    // res = 1+2+3+...+n\n    res\n}\n
recursion.c
/* 使用迭代模拟递归 */\nint forLoopRecur(int n) {\n    int stack[1000]; // 借助一个大数组来模拟栈\n    int top = -1;    // 栈顶索引\n    int res = 0;\n    // 递:递归调用\n    for (int i = n; i > 0; i--) {\n        // 通过“入栈操作”模拟“递”\n        stack[1 + top++] = i;\n    }\n    // 归:返回结果\n    while (top >= 0) {\n        // 通过“出栈操作”模拟“归”\n        res += stack[top--];\n    }\n    // res = 1+2+3+...+n\n    return res;\n}\n
recursion.kt
/* 使用迭代模拟递归 */\nfun forLoopRecur(n: Int): Int {\n    // 使用一个显式的栈来模拟系统调用栈\n    val stack = Stack<Int>()\n    var res = 0\n    // 递: 递归调用\n    for (i in n downTo 0) {\n        // 通过“入栈操作”模拟“递”\n        stack.push(i)\n    }\n    // 归: 返回结果\n    while (stack.isNotEmpty()) {\n        // 通过“出栈操作”模拟“归”\n        res += stack.pop()\n    }\n    // res = 1+2+3+...+n\n    return res\n}\n
recursion.rb
### 使用迭代模拟递归 ###\ndef for_loop_recur(n)\n  # 使用一个显式的栈来模拟系统调用栈\n  stack = []\n  res = 0\n\n  # 递:递归调用\n  for i in n.downto(0)\n    # 通过“入栈操作”模拟“递”\n    stack << i\n  end\n  # 归:返回结果\n  while !stack.empty?\n    res += stack.pop\n  end\n\n  # res = 1+2+3+...+n\n  res\nend\n
可视化运行

全屏观看 >

观察以上代码,当递归转化为迭代后,代码变得更加复杂了。尽管迭代和递归在很多情况下可以互相转化,但不一定值得这样做,有以下两点原因。

总之,选择迭代还是递归取决于特定问题的性质。在编程实践中,权衡两者的优劣并根据情境选择合适的方法至关重要。

","path":["第 2 章   复杂度分析","2.2   迭代与递归"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/","level":1,"title":"2.1   算法效率评估","text":"

在算法设计中,我们先后追求以下两个层面的目标。

  1. 找到问题解法:算法需要在规定的输入范围内可靠地求得问题的正确解。
  2. 寻求最优解法:同一个问题可能存在多种解法,我们希望找到尽可能高效的算法。

也就是说,在能够解决问题的前提下,算法效率已成为衡量算法优劣的主要评价指标,它包括以下两个维度。

简而言之,我们的目标是设计“既快又省”的数据结构与算法。而有效地评估算法效率至关重要,因为只有这样,我们才能将各种算法进行对比,进而指导算法设计与优化过程。

效率评估方法主要分为两种:实际测试、理论估算。

","path":["第 2 章   复杂度分析","2.1   算法效率评估"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/#211","level":2,"title":"2.1.1   实际测试","text":"

假设我们现在有算法 A 和算法 B ,它们都能解决同一问题,现在需要对比这两个算法的效率。最直接的方法是找一台计算机,运行这两个算法,并监控记录它们的运行时间和内存占用情况。这种评估方式能够反映真实情况,但也存在较大的局限性。

一方面,难以排除测试环境的干扰因素。硬件配置会影响算法的性能表现。比如一个算法的并行度较高,那么它就更适合在多核 CPU 上运行,一个算法的内存操作密集,那么它在高性能内存上的表现就会更好。也就是说,算法在不同的机器上的测试结果可能是不一致的。这意味着我们需要在各种机器上进行测试,统计平均效率,而这是不现实的。

另一方面,展开完整测试非常耗费资源。随着输入数据量的变化,算法会表现出不同的效率。例如,在输入数据量较小时,算法 A 的运行时间比算法 B 短;而在输入数据量较大时,测试结果可能恰恰相反。因此,为了得到有说服力的结论,我们需要测试各种规模的输入数据,而这需要耗费大量的计算资源。

","path":["第 2 章   复杂度分析","2.1   算法效率评估"],"tags":[]},{"location":"chapter_computational_complexity/performance_evaluation/#212","level":2,"title":"2.1.2   理论估算","text":"

由于实际测试具有较大的局限性,我们可以考虑仅通过一些计算来评估算法的效率。这种估算方法被称为渐近复杂度分析(asymptotic complexity analysis),简称复杂度分析。

复杂度分析能够体现算法运行所需的时间和空间资源与输入数据规模之间的关系。它描述了随着输入数据规模的增加,算法执行所需时间和空间的增长趋势。这个定义有些拗口,我们可以将其分为三个重点来理解。

复杂度分析克服了实际测试方法的弊端,体现在以下几个方面。

Tip

如果你仍对复杂度的概念感到困惑,无须担心,我们会在后续章节中详细介绍。

复杂度分析为我们提供了一把评估算法效率的“标尺”,使我们可以衡量执行某个算法所需的时间和空间资源,对比不同算法之间的效率。

复杂度是个数学概念,对于初学者可能比较抽象,学习难度相对较高。从这个角度看,复杂度分析可能不太适合作为最先介绍的内容。然而,当我们讨论某个数据结构或算法的特点时,难以避免要分析其运行速度和空间使用情况。

综上所述,建议你在深入学习数据结构与算法之前,先对复杂度分析建立初步的了解,以便能够完成简单算法的复杂度分析。

","path":["第 2 章   复杂度分析","2.1   算法效率评估"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/","level":1,"title":"2.4   空间复杂度","text":"

空间复杂度(space complexity)用于衡量算法占用内存空间随着数据量变大时的增长趋势。这个概念与时间复杂度非常类似,只需将“运行时间”替换为“占用内存空间”。

","path":["第 2 章   复杂度分析","2.4   空间复杂度"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#241","level":2,"title":"2.4.1   算法相关空间","text":"

算法在运行过程中使用的内存空间主要包括以下几种。

一般情况下,空间复杂度的统计范围是“暂存空间”加上“输出空间”。

暂存空间可以进一步划分为三个部分。

在分析一段程序的空间复杂度时,我们通常统计暂存数据、栈帧空间和输出数据三部分,如图 2-15 所示。

图 2-15   算法使用的相关空间

相关代码如下:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
class Node:\n    \"\"\"类\"\"\"\n    def __init__(self, x: int):\n        self.val: int = x              # 节点值\n        self.next: Node | None = None  # 指向下一节点的引用\n\ndef function() -> int:\n    \"\"\"函数\"\"\"\n    # 执行某些操作...\n    return 0\n\ndef algorithm(n) -> int:  # 输入数据\n    A = 0                 # 暂存数据(常量,一般用大写字母表示)\n    b = 0                 # 暂存数据(变量)\n    node = Node(0)        # 暂存数据(对象)\n    c = function()        # 栈帧空间(调用函数)\n    return A + b + c      # 输出数据\n
/* 结构体 */\nstruct Node {\n    int val;\n    Node *next;\n    Node(int x) : val(x), next(nullptr) {}\n};\n\n/* 函数 */\nint func() {\n    // 执行某些操作...\n    return 0;\n}\n\nint algorithm(int n) {        // 输入数据\n    const int a = 0;          // 暂存数据(常量)\n    int b = 0;                // 暂存数据(变量)\n    Node* node = new Node(0); // 暂存数据(对象)\n    int c = func();           // 栈帧空间(调用函数)\n    return a + b + c;         // 输出数据\n}\n
/* 类 */\nclass Node {\n    int val;\n    Node next;\n    Node(int x) { val = x; }\n}\n\n/* 函数 */\nint function() {\n    // 执行某些操作...\n    return 0;\n}\n\nint algorithm(int n) {        // 输入数据\n    final int a = 0;          // 暂存数据(常量)\n    int b = 0;                // 暂存数据(变量)\n    Node node = new Node(0);  // 暂存数据(对象)\n    int c = function();       // 栈帧空间(调用函数)\n    return a + b + c;         // 输出数据\n}\n
/* 类 */\nclass Node(int x) {\n    int val = x;\n    Node next;\n}\n\n/* 函数 */\nint Function() {\n    // 执行某些操作...\n    return 0;\n}\n\nint Algorithm(int n) {        // 输入数据\n    const int a = 0;          // 暂存数据(常量)\n    int b = 0;                // 暂存数据(变量)\n    Node node = new(0);       // 暂存数据(对象)\n    int c = Function();       // 栈帧空间(调用函数)\n    return a + b + c;         // 输出数据\n}\n
/* 结构体 */\ntype node struct {\n    val  int\n    next *node\n}\n\n/* 创建 node 结构体  */\nfunc newNode(val int) *node {\n    return &node{val: val}\n}\n\n/* 函数 */\nfunc function() int {\n    // 执行某些操作...\n    return 0\n}\n\nfunc algorithm(n int) int { // 输入数据\n    const a = 0             // 暂存数据(常量)\n    b := 0                  // 暂存数据(变量)\n    newNode(0)              // 暂存数据(对象)\n    c := function()         // 栈帧空间(调用函数)\n    return a + b + c        // 输出数据\n}\n
/* 类 */\nclass Node {\n    var val: Int\n    var next: Node?\n\n    init(x: Int) {\n        val = x\n    }\n}\n\n/* 函数 */\nfunc function() -> Int {\n    // 执行某些操作...\n    return 0\n}\n\nfunc algorithm(n: Int) -> Int { // 输入数据\n    let a = 0             // 暂存数据(常量)\n    var b = 0             // 暂存数据(变量)\n    let node = Node(x: 0) // 暂存数据(对象)\n    let c = function()    // 栈帧空间(调用函数)\n    return a + b + c      // 输出数据\n}\n
/* 类 */\nclass Node {\n    val;\n    next;\n    constructor(val) {\n        this.val = val === undefined ? 0 : val; // 节点值\n        this.next = null;                       // 指向下一节点的引用\n    }\n}\n\n/* 函数 */\nfunction constFunc() {\n    // 执行某些操作\n    return 0;\n}\n\nfunction algorithm(n) {       // 输入数据\n    const a = 0;              // 暂存数据(常量)\n    let b = 0;                // 暂存数据(变量)\n    const node = new Node(0); // 暂存数据(对象)\n    const c = constFunc();    // 栈帧空间(调用函数)\n    return a + b + c;         // 输出数据\n}\n
/* 类 */\nclass Node {\n    val: number;\n    next: Node | null;\n    constructor(val?: number) {\n        this.val = val === undefined ? 0 : val; // 节点值\n        this.next = null;                       // 指向下一节点的引用\n    }\n}\n\n/* 函数 */\nfunction constFunc(): number {\n    // 执行某些操作\n    return 0;\n}\n\nfunction algorithm(n: number): number { // 输入数据\n    const a = 0;                        // 暂存数据(常量)\n    let b = 0;                          // 暂存数据(变量)\n    const node = new Node(0);           // 暂存数据(对象)\n    const c = constFunc();              // 栈帧空间(调用函数)\n    return a + b + c;                   // 输出数据\n}\n
/* 类 */\nclass Node {\n  int val;\n  Node next;\n  Node(this.val, [this.next]);\n}\n\n/* 函数 */\nint function() {\n  // 执行某些操作...\n  return 0;\n}\n\nint algorithm(int n) {  // 输入数据\n  const int a = 0;      // 暂存数据(常量)\n  int b = 0;            // 暂存数据(变量)\n  Node node = Node(0);  // 暂存数据(对象)\n  int c = function();   // 栈帧空间(调用函数)\n  return a + b + c;     // 输出数据\n}\n
use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* 结构体 */\nstruct Node {\n    val: i32,\n    next: Option<Rc<RefCell<Node>>>,\n}\n\n/* 创建 Node 结构体 */\nimpl Node {\n    fn new(val: i32) -> Self {\n        Self { val: val, next: None }\n    }\n}\n\n/* 函数 */\nfn function() -> i32 {      \n    // 执行某些操作...\n    return 0;\n}\n\nfn algorithm(n: i32) -> i32 {       // 输入数据\n    const a: i32 = 0;               // 暂存数据(常量)\n    let mut b = 0;                  // 暂存数据(变量)\n    let node = Node::new(0);        // 暂存数据(对象)\n    let c = function();             // 栈帧空间(调用函数)\n    return a + b + c;               // 输出数据\n}\n
/* 函数 */\nint func() {\n    // 执行某些操作...\n    return 0;\n}\n\nint algorithm(int n) { // 输入数据\n    const int a = 0;   // 暂存数据(常量)\n    int b = 0;         // 暂存数据(变量)\n    int c = func();    // 栈帧空间(调用函数)\n    return a + b + c;  // 输出数据\n}\n
/* 类 */\nclass Node(var _val: Int) {\n    var next: Node? = null\n}\n\n/* 函数 */\nfun function(): Int {\n    // 执行某些操作...\n    return 0\n}\n\nfun algorithm(n: Int): Int { // 输入数据\n    val a = 0                // 暂存数据(常量)\n    var b = 0                // 暂存数据(变量)\n    val node = Node(0)       // 暂存数据(对象)\n    val c = function()       // 栈帧空间(调用函数)\n    return a + b + c         // 输出数据\n}\n
### 类 ###\nclass Node\n    attr_accessor :val      # 节点值\n    attr_accessor :next     # 指向下一节点的引用\n\n    def initialize(x)\n        @val = x\n    end\nend\n\n### 函数 ###\ndef function\n    # 执行某些操作...\n    0\nend\n\n### 算法 ###\ndef algorithm(n)        # 输入数据\n    a = 0               # 暂存数据(常量)\n    b = 0               # 暂存数据(变量)\n    node = Node.new(0)  # 暂存数据(对象)\n    c = function        # 栈帧空间(调用函数)\n    a + b + c           # 输出数据\nend\n
","path":["第 2 章   复杂度分析","2.4   空间复杂度"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#242","level":2,"title":"2.4.2   推算方法","text":"

空间复杂度的推算方法与时间复杂度大致相同,只需将统计对象从“操作数量”转为“使用空间大小”。

而与时间复杂度不同的是,我们通常只关注最差空间复杂度。这是因为内存空间是一项硬性要求,我们必须确保在所有输入数据下都有足够的内存空间预留。

观察以下代码,最差空间复杂度中的“最差”有两层含义。

  1. 以最差输入数据为准:当 \\(n < 10\\) 时,空间复杂度为 \\(O(1)\\) ;但当 \\(n > 10\\) 时,初始化的数组 nums 占用 \\(O(n)\\) 空间,因此最差空间复杂度为 \\(O(n)\\) 。
  2. 以算法运行中的峰值内存为准:例如,程序在执行最后一行之前,占用 \\(O(1)\\) 空间;当初始化数组 nums 时,程序占用 \\(O(n)\\) 空间,因此最差空间复杂度为 \\(O(n)\\) 。
PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
def algorithm(n: int):\n    a = 0               # O(1)\n    b = [0] * 10000     # O(1)\n    if n > 10:\n        nums = [0] * n  # O(n)\n
void algorithm(int n) {\n    int a = 0;               // O(1)\n    vector<int> b(10000);    // O(1)\n    if (n > 10)\n        vector<int> nums(n); // O(n)\n}\n
void algorithm(int n) {\n    int a = 0;                   // O(1)\n    int[] b = new int[10000];    // O(1)\n    if (n > 10)\n        int[] nums = new int[n]; // O(n)\n}\n
void Algorithm(int n) {\n    int a = 0;                   // O(1)\n    int[] b = new int[10000];    // O(1)\n    if (n > 10) {\n        int[] nums = new int[n]; // O(n)\n    }\n}\n
func algorithm(n int) {\n    a := 0                      // O(1)\n    b := make([]int, 10000)     // O(1)\n    var nums []int\n    if n > 10 {\n        nums := make([]int, n)  // O(n)\n    }\n    fmt.Println(a, b, nums)\n}\n
func algorithm(n: Int) {\n    let a = 0 // O(1)\n    let b = Array(repeating: 0, count: 10000) // O(1)\n    if n > 10 {\n        let nums = Array(repeating: 0, count: n) // O(n)\n    }\n}\n
function algorithm(n) {\n    const a = 0;                   // O(1)\n    const b = new Array(10000);    // O(1)\n    if (n > 10) {\n        const nums = new Array(n); // O(n)\n    }\n}\n
function algorithm(n: number): void {\n    const a = 0;                   // O(1)\n    const b = new Array(10000);    // O(1)\n    if (n > 10) {\n        const nums = new Array(n); // O(n)\n    }\n}\n
void algorithm(int n) {\n  int a = 0;                            // O(1)\n  List<int> b = List.filled(10000, 0);  // O(1)\n  if (n > 10) {\n    List<int> nums = List.filled(n, 0); // O(n)\n  }\n}\n
fn algorithm(n: i32) {\n    let a = 0;                              // O(1)\n    let b = [0; 10000];                     // O(1)\n    if n > 10 {\n        let nums = vec![0; n as usize];     // O(n)\n    }\n}\n
void algorithm(int n) {\n    int a = 0;               // O(1)\n    int b[10000];            // O(1)\n    if (n > 10)\n        int nums[n] = {0};   // O(n)\n}\n
fun algorithm(n: Int) {\n    val a = 0                    // O(1)\n    val b = IntArray(10000)      // O(1)\n    if (n > 10) {\n        val nums = IntArray(n)   // O(n)\n    }\n}\n
def algorithm(n)\n    a = 0                           # O(1)\n    b = Array.new(10000)            # O(1)\n    nums = Array.new(n) if n > 10   # O(n)\nend\n

在递归函数中,需要注意统计栈帧空间。观察以下代码:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
def function() -> int:\n    # 执行某些操作\n    return 0\n\ndef loop(n: int):\n    \"\"\"循环的空间复杂度为 O(1)\"\"\"\n    for _ in range(n):\n        function()\n\ndef recur(n: int):\n    \"\"\"递归的空间复杂度为 O(n)\"\"\"\n    if n == 1:\n        return\n    return recur(n - 1)\n
int func() {\n    // 执行某些操作\n    return 0;\n}\n/* 循环的空间复杂度为 O(1) */\nvoid loop(int n) {\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n/* 递归的空间复杂度为 O(n) */\nvoid recur(int n) {\n    if (n == 1) return;\n    recur(n - 1);\n}\n
int function() {\n    // 执行某些操作\n    return 0;\n}\n/* 循环的空间复杂度为 O(1) */\nvoid loop(int n) {\n    for (int i = 0; i < n; i++) {\n        function();\n    }\n}\n/* 递归的空间复杂度为 O(n) */\nvoid recur(int n) {\n    if (n == 1) return;\n    recur(n - 1);\n}\n
int Function() {\n    // 执行某些操作\n    return 0;\n}\n/* 循环的空间复杂度为 O(1) */\nvoid Loop(int n) {\n    for (int i = 0; i < n; i++) {\n        Function();\n    }\n}\n/* 递归的空间复杂度为 O(n) */\nint Recur(int n) {\n    if (n == 1) return 1;\n    return Recur(n - 1);\n}\n
func function() int {\n    // 执行某些操作\n    return 0\n}\n\n/* 循环的空间复杂度为 O(1) */\nfunc loop(n int) {\n    for i := 0; i < n; i++ {\n        function()\n    }\n}\n\n/* 递归的空间复杂度为 O(n) */\nfunc recur(n int) {\n    if n == 1 {\n        return\n    }\n    recur(n - 1)\n}\n
@discardableResult\nfunc function() -> Int {\n    // 执行某些操作\n    return 0\n}\n\n/* 循环的空间复杂度为 O(1) */\nfunc loop(n: Int) {\n    for _ in 0 ..< n {\n        function()\n    }\n}\n\n/* 递归的空间复杂度为 O(n) */\nfunc recur(n: Int) {\n    if n == 1 {\n        return\n    }\n    recur(n: n - 1)\n}\n
function constFunc() {\n    // 执行某些操作\n    return 0;\n}\n/* 循环的空间复杂度为 O(1) */\nfunction loop(n) {\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n/* 递归的空间复杂度为 O(n) */\nfunction recur(n) {\n    if (n === 1) return;\n    return recur(n - 1);\n}\n
function constFunc(): number {\n    // 执行某些操作\n    return 0;\n}\n/* 循环的空间复杂度为 O(1) */\nfunction loop(n: number): void {\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n/* 递归的空间复杂度为 O(n) */\nfunction recur(n: number): void {\n    if (n === 1) return;\n    return recur(n - 1);\n}\n
int function() {\n  // 执行某些操作\n  return 0;\n}\n/* 循环的空间复杂度为 O(1) */\nvoid loop(int n) {\n  for (int i = 0; i < n; i++) {\n    function();\n  }\n}\n/* 递归的空间复杂度为 O(n) */\nvoid recur(int n) {\n  if (n == 1) return;\n  recur(n - 1);\n}\n
fn function() -> i32 {\n    // 执行某些操作\n    return 0;\n}\n/* 循环的空间复杂度为 O(1) */\nfn loop(n: i32) {\n    for i in 0..n {\n        function();\n    }\n}\n/* 递归的空间复杂度为 O(n) */\nfn recur(n: i32) {\n    if n == 1 {\n        return;\n    }\n    recur(n - 1);\n}\n
int func() {\n    // 执行某些操作\n    return 0;\n}\n/* 循环的空间复杂度为 O(1) */\nvoid loop(int n) {\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n/* 递归的空间复杂度为 O(n) */\nvoid recur(int n) {\n    if (n == 1) return;\n    recur(n - 1);\n}\n
fun function(): Int {\n    // 执行某些操作\n    return 0\n}\n/* 循环的空间复杂度为 O(1) */\nfun loop(n: Int) {\n    for (i in 0..<n) {\n        function()\n    }\n}\n/* 递归的空间复杂度为 O(n) */\nfun recur(n: Int) {\n    if (n == 1) return\n    return recur(n - 1)\n}\n
def function\n    # 执行某些操作\n    0\nend\n\n### 循环的空间复杂度为 O(1) ###\ndef loop(n)\n    (0...n).each { function }\nend\n\n### 递归的空间复杂度为 O(n) ###\ndef recur(n)\n    return if n == 1\n    recur(n - 1)\nend\n

函数 loop()recur() 的时间复杂度都为 \\(O(n)\\) ,但空间复杂度不同。

","path":["第 2 章   复杂度分析","2.4   空间复杂度"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#243","level":2,"title":"2.4.3   常见类型","text":"

设输入数据大小为 \\(n\\) ,图 2-16 展示了常见的空间复杂度类型(从低到高排列)。

\\[ \\begin{aligned} & O(1) < O(\\log n) < O(n) < O(n^2) < O(2^n) \\newline & \\text{常数阶} < \\text{对数阶} < \\text{线性阶} < \\text{平方阶} < \\text{指数阶} \\end{aligned} \\]

图 2-16   常见的空间复杂度类型

","path":["第 2 章   复杂度分析","2.4   空间复杂度"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#1-o1","level":3,"title":"1.   常数阶 \\(O(1)\\)","text":"

常数阶常见于数量与输入数据大小 \\(n\\) 无关的常量、变量、对象。

需要注意的是,在循环中初始化变量或调用函数而占用的内存,在进入下一循环后就会被释放,因此不会累积占用空间,空间复杂度仍为 \\(O(1)\\) :

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
def function() -> int:\n    \"\"\"函数\"\"\"\n    # 执行某些操作\n    return 0\n\ndef constant(n: int):\n    \"\"\"常数阶\"\"\"\n    # 常量、变量、对象占用 O(1) 空间\n    a = 0\n    nums = [0] * 10000\n    node = ListNode(0)\n    # 循环中的变量占用 O(1) 空间\n    for _ in range(n):\n        c = 0\n    # 循环中的函数占用 O(1) 空间\n    for _ in range(n):\n        function()\n
space_complexity.cpp
/* 函数 */\nint func() {\n    // 执行某些操作\n    return 0;\n}\n\n/* 常数阶 */\nvoid constant(int n) {\n    // 常量、变量、对象占用 O(1) 空间\n    const int a = 0;\n    int b = 0;\n    vector<int> nums(10000);\n    ListNode node(0);\n    // 循环中的变量占用 O(1) 空间\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // 循环中的函数占用 O(1) 空间\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n
space_complexity.java
/* 函数 */\nint function() {\n    // 执行某些操作\n    return 0;\n}\n\n/* 常数阶 */\nvoid constant(int n) {\n    // 常量、变量、对象占用 O(1) 空间\n    final int a = 0;\n    int b = 0;\n    int[] nums = new int[10000];\n    ListNode node = new ListNode(0);\n    // 循环中的变量占用 O(1) 空间\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // 循环中的函数占用 O(1) 空间\n    for (int i = 0; i < n; i++) {\n        function();\n    }\n}\n
space_complexity.cs
/* 函数 */\nint Function() {\n    // 执行某些操作\n    return 0;\n}\n\n/* 常数阶 */\nvoid Constant(int n) {\n    // 常量、变量、对象占用 O(1) 空间\n    int a = 0;\n    int b = 0;\n    int[] nums = new int[10000];\n    ListNode node = new(0);\n    // 循环中的变量占用 O(1) 空间\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // 循环中的函数占用 O(1) 空间\n    for (int i = 0; i < n; i++) {\n        Function();\n    }\n}\n
space_complexity.go
/* 函数 */\nfunc function() int {\n    // 执行某些操作...\n    return 0\n}\n\n/* 常数阶 */\nfunc spaceConstant(n int) {\n    // 常量、变量、对象占用 O(1) 空间\n    const a = 0\n    b := 0\n    nums := make([]int, 10000)\n    node := newNode(0)\n    // 循环中的变量占用 O(1) 空间\n    var c int\n    for i := 0; i < n; i++ {\n        c = 0\n    }\n    // 循环中的函数占用 O(1) 空间\n    for i := 0; i < n; i++ {\n        function()\n    }\n    b += 0\n    c += 0\n    nums[0] = 0\n    node.val = 0\n}\n
space_complexity.swift
/* 函数 */\n@discardableResult\nfunc function() -> Int {\n    // 执行某些操作\n    return 0\n}\n\n/* 常数阶 */\nfunc constant(n: Int) {\n    // 常量、变量、对象占用 O(1) 空间\n    let a = 0\n    var b = 0\n    let nums = Array(repeating: 0, count: 10000)\n    let node = ListNode(x: 0)\n    // 循环中的变量占用 O(1) 空间\n    for _ in 0 ..< n {\n        let c = 0\n    }\n    // 循环中的函数占用 O(1) 空间\n    for _ in 0 ..< n {\n        function()\n    }\n}\n
space_complexity.js
/* 函数 */\nfunction constFunc() {\n    // 执行某些操作\n    return 0;\n}\n\n/* 常数阶 */\nfunction constant(n) {\n    // 常量、变量、对象占用 O(1) 空间\n    const a = 0;\n    const b = 0;\n    const nums = new Array(10000);\n    const node = new ListNode(0);\n    // 循环中的变量占用 O(1) 空间\n    for (let i = 0; i < n; i++) {\n        const c = 0;\n    }\n    // 循环中的函数占用 O(1) 空间\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n
space_complexity.ts
/* 函数 */\nfunction constFunc(): number {\n    // 执行某些操作\n    return 0;\n}\n\n/* 常数阶 */\nfunction constant(n: number): void {\n    // 常量、变量、对象占用 O(1) 空间\n    const a = 0;\n    const b = 0;\n    const nums = new Array(10000);\n    const node = new ListNode(0);\n    // 循环中的变量占用 O(1) 空间\n    for (let i = 0; i < n; i++) {\n        const c = 0;\n    }\n    // 循环中的函数占用 O(1) 空间\n    for (let i = 0; i < n; i++) {\n        constFunc();\n    }\n}\n
space_complexity.dart
/* 函数 */\nint function() {\n  // 执行某些操作\n  return 0;\n}\n\n/* 常数阶 */\nvoid constant(int n) {\n  // 常量、变量、对象占用 O(1) 空间\n  final int a = 0;\n  int b = 0;\n  List<int> nums = List.filled(10000, 0);\n  ListNode node = ListNode(0);\n  // 循环中的变量占用 O(1) 空间\n  for (var i = 0; i < n; i++) {\n    int c = 0;\n  }\n  // 循环中的函数占用 O(1) 空间\n  for (var i = 0; i < n; i++) {\n    function();\n  }\n}\n
space_complexity.rs
/* 函数 */\nfn function() -> i32 {\n    // 执行某些操作\n    return 0;\n}\n\n/* 常数阶 */\n#[allow(unused)]\nfn constant(n: i32) {\n    // 常量、变量、对象占用 O(1) 空间\n    const A: i32 = 0;\n    let b = 0;\n    let nums = vec![0; 10000];\n    let node = ListNode::new(0);\n    // 循环中的变量占用 O(1) 空间\n    for i in 0..n {\n        let c = 0;\n    }\n    // 循环中的函数占用 O(1) 空间\n    for i in 0..n {\n        function();\n    }\n}\n
space_complexity.c
/* 函数 */\nint func() {\n    // 执行某些操作\n    return 0;\n}\n\n/* 常数阶 */\nvoid constant(int n) {\n    // 常量、变量、对象占用 O(1) 空间\n    const int a = 0;\n    int b = 0;\n    int nums[1000];\n    ListNode *node = newListNode(0);\n    free(node);\n    // 循环中的变量占用 O(1) 空间\n    for (int i = 0; i < n; i++) {\n        int c = 0;\n    }\n    // 循环中的函数占用 O(1) 空间\n    for (int i = 0; i < n; i++) {\n        func();\n    }\n}\n
space_complexity.kt
/* 函数 */\nfun function(): Int {\n    // 执行某些操作\n    return 0\n}\n\n/* 常数阶 */\nfun constant(n: Int) {\n    // 常量、变量、对象占用 O(1) 空间\n    val a = 0\n    var b = 0\n    val nums = Array(10000) { 0 }\n    val node = ListNode(0)\n    // 循环中的变量占用 O(1) 空间\n    for (i in 0..<n) {\n        val c = 0\n    }\n    // 循环中的函数占用 O(1) 空间\n    for (i in 0..<n) {\n        function()\n    }\n}\n
space_complexity.rb
### 函数 ###\ndef function\n  # 执行某些操作\n  0\nend\n\n### 常数阶 ###\ndef constant(n)\n  # 常量、变量、对象占用 O(1) 空间\n  a = 0\n  nums = [0] * 10000\n  node = ListNode.new\n\n  # 循环中的变量占用 O(1) 空间\n  (0...n).each { c = 0 }\n  # 循环中的函数占用 O(1) 空间\n  (0...n).each { function }\nend\n
可视化运行

全屏观看 >

","path":["第 2 章   复杂度分析","2.4   空间复杂度"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#2-on","level":3,"title":"2.   线性阶 \\(O(n)\\)","text":"

线性阶常见于元素数量与 \\(n\\) 成正比的数组、链表、栈、队列等:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
def linear(n: int):\n    \"\"\"线性阶\"\"\"\n    # 长度为 n 的列表占用 O(n) 空间\n    nums = [0] * n\n    # 长度为 n 的哈希表占用 O(n) 空间\n    hmap = dict[int, str]()\n    for i in range(n):\n        hmap[i] = str(i)\n
space_complexity.cpp
/* 线性阶 */\nvoid linear(int n) {\n    // 长度为 n 的数组占用 O(n) 空间\n    vector<int> nums(n);\n    // 长度为 n 的列表占用 O(n) 空间\n    vector<ListNode> nodes;\n    for (int i = 0; i < n; i++) {\n        nodes.push_back(ListNode(i));\n    }\n    // 长度为 n 的哈希表占用 O(n) 空间\n    unordered_map<int, string> map;\n    for (int i = 0; i < n; i++) {\n        map[i] = to_string(i);\n    }\n}\n
space_complexity.java
/* 线性阶 */\nvoid linear(int n) {\n    // 长度为 n 的数组占用 O(n) 空间\n    int[] nums = new int[n];\n    // 长度为 n 的列表占用 O(n) 空间\n    List<ListNode> nodes = new ArrayList<>();\n    for (int i = 0; i < n; i++) {\n        nodes.add(new ListNode(i));\n    }\n    // 长度为 n 的哈希表占用 O(n) 空间\n    Map<Integer, String> map = new HashMap<>();\n    for (int i = 0; i < n; i++) {\n        map.put(i, String.valueOf(i));\n    }\n}\n
space_complexity.cs
/* 线性阶 */\nvoid Linear(int n) {\n    // 长度为 n 的数组占用 O(n) 空间\n    int[] nums = new int[n];\n    // 长度为 n 的列表占用 O(n) 空间\n    List<ListNode> nodes = [];\n    for (int i = 0; i < n; i++) {\n        nodes.Add(new ListNode(i));\n    }\n    // 长度为 n 的哈希表占用 O(n) 空间\n    Dictionary<int, string> map = [];\n    for (int i = 0; i < n; i++) {\n        map.Add(i, i.ToString());\n    }\n}\n
space_complexity.go
/* 线性阶 */\nfunc spaceLinear(n int) {\n    // 长度为 n 的数组占用 O(n) 空间\n    _ = make([]int, n)\n    // 长度为 n 的列表占用 O(n) 空间\n    var nodes []*node\n    for i := 0; i < n; i++ {\n        nodes = append(nodes, newNode(i))\n    }\n    // 长度为 n 的哈希表占用 O(n) 空间\n    m := make(map[int]string, n)\n    for i := 0; i < n; i++ {\n        m[i] = strconv.Itoa(i)\n    }\n}\n
space_complexity.swift
/* 线性阶 */\nfunc linear(n: Int) {\n    // 长度为 n 的数组占用 O(n) 空间\n    let nums = Array(repeating: 0, count: n)\n    // 长度为 n 的列表占用 O(n) 空间\n    let nodes = (0 ..< n).map { ListNode(x: $0) }\n    // 长度为 n 的哈希表占用 O(n) 空间\n    let map = Dictionary(uniqueKeysWithValues: (0 ..< n).map { ($0, \"\\($0)\") })\n}\n
space_complexity.js
/* 线性阶 */\nfunction linear(n) {\n    // 长度为 n 的数组占用 O(n) 空间\n    const nums = new Array(n);\n    // 长度为 n 的列表占用 O(n) 空间\n    const nodes = [];\n    for (let i = 0; i < n; i++) {\n        nodes.push(new ListNode(i));\n    }\n    // 长度为 n 的哈希表占用 O(n) 空间\n    const map = new Map();\n    for (let i = 0; i < n; i++) {\n        map.set(i, i.toString());\n    }\n}\n
space_complexity.ts
/* 线性阶 */\nfunction linear(n: number): void {\n    // 长度为 n 的数组占用 O(n) 空间\n    const nums = new Array(n);\n    // 长度为 n 的列表占用 O(n) 空间\n    const nodes: ListNode[] = [];\n    for (let i = 0; i < n; i++) {\n        nodes.push(new ListNode(i));\n    }\n    // 长度为 n 的哈希表占用 O(n) 空间\n    const map = new Map();\n    for (let i = 0; i < n; i++) {\n        map.set(i, i.toString());\n    }\n}\n
space_complexity.dart
/* 线性阶 */\nvoid linear(int n) {\n  // 长度为 n 的数组占用 O(n) 空间\n  List<int> nums = List.filled(n, 0);\n  // 长度为 n 的列表占用 O(n) 空间\n  List<ListNode> nodes = [];\n  for (var i = 0; i < n; i++) {\n    nodes.add(ListNode(i));\n  }\n  // 长度为 n 的哈希表占用 O(n) 空间\n  Map<int, String> map = HashMap();\n  for (var i = 0; i < n; i++) {\n    map.putIfAbsent(i, () => i.toString());\n  }\n}\n
space_complexity.rs
/* 线性阶 */\n#[allow(unused)]\nfn linear(n: i32) {\n    // 长度为 n 的数组占用 O(n) 空间\n    let mut nums = vec![0; n as usize];\n    // 长度为 n 的列表占用 O(n) 空间\n    let mut nodes = Vec::new();\n    for i in 0..n {\n        nodes.push(ListNode::new(i))\n    }\n    // 长度为 n 的哈希表占用 O(n) 空间\n    let mut map = HashMap::new();\n    for i in 0..n {\n        map.insert(i, i.to_string());\n    }\n}\n
space_complexity.c
/* 哈希表 */\ntypedef struct {\n    int key;\n    int val;\n    UT_hash_handle hh; // 基于 uthash.h 实现\n} HashTable;\n\n/* 线性阶 */\nvoid linear(int n) {\n    // 长度为 n 的数组占用 O(n) 空间\n    int *nums = malloc(sizeof(int) * n);\n    free(nums);\n\n    // 长度为 n 的列表占用 O(n) 空间\n    ListNode **nodes = malloc(sizeof(ListNode *) * n);\n    for (int i = 0; i < n; i++) {\n        nodes[i] = newListNode(i);\n    }\n    // 内存释放\n    for (int i = 0; i < n; i++) {\n        free(nodes[i]);\n    }\n    free(nodes);\n\n    // 长度为 n 的哈希表占用 O(n) 空间\n    HashTable *h = NULL;\n    for (int i = 0; i < n; i++) {\n        HashTable *tmp = malloc(sizeof(HashTable));\n        tmp->key = i;\n        tmp->val = i;\n        HASH_ADD_INT(h, key, tmp);\n    }\n\n    // 内存释放\n    HashTable *curr, *tmp;\n    HASH_ITER(hh, h, curr, tmp) {\n        HASH_DEL(h, curr);\n        free(curr);\n    }\n}\n
space_complexity.kt
/* 线性阶 */\nfun linear(n: Int) {\n    // 长度为 n 的数组占用 O(n) 空间\n    val nums = Array(n) { 0 }\n    // 长度为 n 的列表占用 O(n) 空间\n    val nodes = mutableListOf<ListNode>()\n    for (i in 0..<n) {\n        nodes.add(ListNode(i))\n    }\n    // 长度为 n 的哈希表占用 O(n) 空间\n    val map = mutableMapOf<Int, String>()\n    for (i in 0..<n) {\n        map[i] = i.toString()\n    }\n}\n
space_complexity.rb
### 线性阶 ###\ndef linear(n)\n  # 长度为 n 的列表占用 O(n) 空间\n  nums = Array.new(n, 0)\n\n  # 长度为 n 的哈希表占用 O(n) 空间\n  hmap = {}\n  for i in 0...n\n    hmap[i] = i.to_s\n  end\nend\n
可视化运行

全屏观看 >

如图 2-17 所示,此函数的递归深度为 \\(n\\) ,即同时存在 \\(n\\) 个未返回的 linear_recur() 函数,使用 \\(O(n)\\) 大小的栈帧空间:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
def linear_recur(n: int):\n    \"\"\"线性阶(递归实现)\"\"\"\n    print(\"递归 n =\", n)\n    if n == 1:\n        return\n    linear_recur(n - 1)\n
space_complexity.cpp
/* 线性阶(递归实现) */\nvoid linearRecur(int n) {\n    cout << \"递归 n = \" << n << endl;\n    if (n == 1)\n        return;\n    linearRecur(n - 1);\n}\n
space_complexity.java
/* 线性阶(递归实现) */\nvoid linearRecur(int n) {\n    System.out.println(\"递归 n = \" + n);\n    if (n == 1)\n        return;\n    linearRecur(n - 1);\n}\n
space_complexity.cs
/* 线性阶(递归实现) */\nvoid LinearRecur(int n) {\n    Console.WriteLine(\"递归 n = \" + n);\n    if (n == 1) return;\n    LinearRecur(n - 1);\n}\n
space_complexity.go
/* 线性阶(递归实现) */\nfunc spaceLinearRecur(n int) {\n    fmt.Println(\"递归 n =\", n)\n    if n == 1 {\n        return\n    }\n    spaceLinearRecur(n - 1)\n}\n
space_complexity.swift
/* 线性阶(递归实现) */\nfunc linearRecur(n: Int) {\n    print(\"递归 n = \\(n)\")\n    if n == 1 {\n        return\n    }\n    linearRecur(n: n - 1)\n}\n
space_complexity.js
/* 线性阶(递归实现) */\nfunction linearRecur(n) {\n    console.log(`递归 n = ${n}`);\n    if (n === 1) return;\n    linearRecur(n - 1);\n}\n
space_complexity.ts
/* 线性阶(递归实现) */\nfunction linearRecur(n: number): void {\n    console.log(`递归 n = ${n}`);\n    if (n === 1) return;\n    linearRecur(n - 1);\n}\n
space_complexity.dart
/* 线性阶(递归实现) */\nvoid linearRecur(int n) {\n  print('递归 n = $n');\n  if (n == 1) return;\n  linearRecur(n - 1);\n}\n
space_complexity.rs
/* 线性阶(递归实现) */\nfn linear_recur(n: i32) {\n    println!(\"递归 n = {}\", n);\n    if n == 1 {\n        return;\n    };\n    linear_recur(n - 1);\n}\n
space_complexity.c
/* 线性阶(递归实现) */\nvoid linearRecur(int n) {\n    printf(\"递归 n = %d\\r\\n\", n);\n    if (n == 1)\n        return;\n    linearRecur(n - 1);\n}\n
space_complexity.kt
/* 线性阶(递归实现) */\nfun linearRecur(n: Int) {\n    println(\"递归 n = $n\")\n    if (n == 1)\n        return\n    linearRecur(n - 1)\n}\n
space_complexity.rb
### 线性阶(递归实现)###\ndef linear_recur(n)\n  puts \"递归 n = #{n}\"\n  return if n == 1\n  linear_recur(n - 1)\nend\n
可视化运行

全屏观看 >

图 2-17   递归函数产生的线性阶空间复杂度

","path":["第 2 章   复杂度分析","2.4   空间复杂度"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#3-on2","level":3,"title":"3.   平方阶 \\(O(n^2)\\)","text":"

平方阶常见于矩阵和图,元素数量与 \\(n\\) 成平方关系:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
def quadratic(n: int):\n    \"\"\"平方阶\"\"\"\n    # 二维列表占用 O(n^2) 空间\n    num_matrix = [[0] * n for _ in range(n)]\n
space_complexity.cpp
/* 平方阶 */\nvoid quadratic(int n) {\n    // 二维列表占用 O(n^2) 空间\n    vector<vector<int>> numMatrix;\n    for (int i = 0; i < n; i++) {\n        vector<int> tmp;\n        for (int j = 0; j < n; j++) {\n            tmp.push_back(0);\n        }\n        numMatrix.push_back(tmp);\n    }\n}\n
space_complexity.java
/* 平方阶 */\nvoid quadratic(int n) {\n    // 矩阵占用 O(n^2) 空间\n    int[][] numMatrix = new int[n][n];\n    // 二维列表占用 O(n^2) 空间\n    List<List<Integer>> numList = new ArrayList<>();\n    for (int i = 0; i < n; i++) {\n        List<Integer> tmp = new ArrayList<>();\n        for (int j = 0; j < n; j++) {\n            tmp.add(0);\n        }\n        numList.add(tmp);\n    }\n}\n
space_complexity.cs
/* 平方阶 */\nvoid Quadratic(int n) {\n    // 矩阵占用 O(n^2) 空间\n    int[,] numMatrix = new int[n, n];\n    // 二维列表占用 O(n^2) 空间\n    List<List<int>> numList = [];\n    for (int i = 0; i < n; i++) {\n        List<int> tmp = [];\n        for (int j = 0; j < n; j++) {\n            tmp.Add(0);\n        }\n        numList.Add(tmp);\n    }\n}\n
space_complexity.go
/* 平方阶 */\nfunc spaceQuadratic(n int) {\n    // 矩阵占用 O(n^2) 空间\n    numMatrix := make([][]int, n)\n    for i := 0; i < n; i++ {\n        numMatrix[i] = make([]int, n)\n    }\n}\n
space_complexity.swift
/* 平方阶 */\nfunc quadratic(n: Int) {\n    // 二维列表占用 O(n^2) 空间\n    let numList = Array(repeating: Array(repeating: 0, count: n), count: n)\n}\n
space_complexity.js
/* 平方阶 */\nfunction quadratic(n) {\n    // 矩阵占用 O(n^2) 空间\n    const numMatrix = Array(n)\n        .fill(null)\n        .map(() => Array(n).fill(null));\n    // 二维列表占用 O(n^2) 空间\n    const numList = [];\n    for (let i = 0; i < n; i++) {\n        const tmp = [];\n        for (let j = 0; j < n; j++) {\n            tmp.push(0);\n        }\n        numList.push(tmp);\n    }\n}\n
space_complexity.ts
/* 平方阶 */\nfunction quadratic(n: number): void {\n    // 矩阵占用 O(n^2) 空间\n    const numMatrix = Array(n)\n        .fill(null)\n        .map(() => Array(n).fill(null));\n    // 二维列表占用 O(n^2) 空间\n    const numList = [];\n    for (let i = 0; i < n; i++) {\n        const tmp = [];\n        for (let j = 0; j < n; j++) {\n            tmp.push(0);\n        }\n        numList.push(tmp);\n    }\n}\n
space_complexity.dart
/* 平方阶 */\nvoid quadratic(int n) {\n  // 矩阵占用 O(n^2) 空间\n  List<List<int>> numMatrix = List.generate(n, (_) => List.filled(n, 0));\n  // 二维列表占用 O(n^2) 空间\n  List<List<int>> numList = [];\n  for (var i = 0; i < n; i++) {\n    List<int> tmp = [];\n    for (int j = 0; j < n; j++) {\n      tmp.add(0);\n    }\n    numList.add(tmp);\n  }\n}\n
space_complexity.rs
/* 平方阶 */\n#[allow(unused)]\nfn quadratic(n: i32) {\n    // 矩阵占用 O(n^2) 空间\n    let num_matrix = vec![vec![0; n as usize]; n as usize];\n    // 二维列表占用 O(n^2) 空间\n    let mut num_list = Vec::new();\n    for i in 0..n {\n        let mut tmp = Vec::new();\n        for j in 0..n {\n            tmp.push(0);\n        }\n        num_list.push(tmp);\n    }\n}\n
space_complexity.c
/* 平方阶 */\nvoid quadratic(int n) {\n    // 二维列表占用 O(n^2) 空间\n    int **numMatrix = malloc(sizeof(int *) * n);\n    for (int i = 0; i < n; i++) {\n        int *tmp = malloc(sizeof(int) * n);\n        for (int j = 0; j < n; j++) {\n            tmp[j] = 0;\n        }\n        numMatrix[i] = tmp;\n    }\n\n    // 内存释放\n    for (int i = 0; i < n; i++) {\n        free(numMatrix[i]);\n    }\n    free(numMatrix);\n}\n
space_complexity.kt
/* 平方阶 */\nfun quadratic(n: Int) {\n    // 矩阵占用 O(n^2) 空间\n    val numMatrix = arrayOfNulls<Array<Int>?>(n)\n    // 二维列表占用 O(n^2) 空间\n    val numList = mutableListOf<MutableList<Int>>()\n    for (i in 0..<n) {\n        val tmp = mutableListOf<Int>()\n        for (j in 0..<n) {\n            tmp.add(0)\n        }\n        numList.add(tmp)\n    }\n}\n
space_complexity.rb
### 平方阶 ###\ndef quadratic(n)\n  # 二维列表占用 O(n^2) 空间\n  Array.new(n) { Array.new(n, 0) }\nend\n
可视化运行

全屏观看 >

如图 2-18 所示,该函数的递归深度为 \\(n\\) ,在每个递归函数中都初始化了一个数组,长度分别为 \\(n\\)、\\(n-1\\)、\\(\\dots\\)、\\(2\\)、\\(1\\) ,平均长度为 \\(n / 2\\) ,因此总体占用 \\(O(n^2)\\) 空间:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
def quadratic_recur(n: int) -> int:\n    \"\"\"平方阶(递归实现)\"\"\"\n    if n <= 0:\n        return 0\n    # 数组 nums 长度为 n, n-1, ..., 2, 1\n    nums = [0] * n\n    return quadratic_recur(n - 1)\n
space_complexity.cpp
/* 平方阶(递归实现) */\nint quadraticRecur(int n) {\n    if (n <= 0)\n        return 0;\n    vector<int> nums(n);\n    cout << \"递归 n = \" << n << \" 中的 nums 长度 = \" << nums.size() << endl;\n    return quadraticRecur(n - 1);\n}\n
space_complexity.java
/* 平方阶(递归实现) */\nint quadraticRecur(int n) {\n    if (n <= 0)\n        return 0;\n    // 数组 nums 长度为 n, n-1, ..., 2, 1\n    int[] nums = new int[n];\n    System.out.println(\"递归 n = \" + n + \" 中的 nums 长度 = \" + nums.length);\n    return quadraticRecur(n - 1);\n}\n
space_complexity.cs
/* 平方阶(递归实现) */\nint QuadraticRecur(int n) {\n    if (n <= 0) return 0;\n    int[] nums = new int[n];\n    Console.WriteLine(\"递归 n = \" + n + \" 中的 nums 长度 = \" + nums.Length);\n    return QuadraticRecur(n - 1);\n}\n
space_complexity.go
/* 平方阶(递归实现) */\nfunc spaceQuadraticRecur(n int) int {\n    if n <= 0 {\n        return 0\n    }\n    nums := make([]int, n)\n    fmt.Printf(\"递归 n = %d 中的 nums 长度 = %d \\n\", n, len(nums))\n    return spaceQuadraticRecur(n - 1)\n}\n
space_complexity.swift
/* 平方阶(递归实现) */\n@discardableResult\nfunc quadraticRecur(n: Int) -> Int {\n    if n <= 0 {\n        return 0\n    }\n    // 数组 nums 长度为 n, n-1, ..., 2, 1\n    let nums = Array(repeating: 0, count: n)\n    print(\"递归 n = \\(n) 中的 nums 长度 = \\(nums.count)\")\n    return quadraticRecur(n: n - 1)\n}\n
space_complexity.js
/* 平方阶(递归实现) */\nfunction quadraticRecur(n) {\n    if (n <= 0) return 0;\n    const nums = new Array(n);\n    console.log(`递归 n = ${n} 中的 nums 长度 = ${nums.length}`);\n    return quadraticRecur(n - 1);\n}\n
space_complexity.ts
/* 平方阶(递归实现) */\nfunction quadraticRecur(n: number): number {\n    if (n <= 0) return 0;\n    const nums = new Array(n);\n    console.log(`递归 n = ${n} 中的 nums 长度 = ${nums.length}`);\n    return quadraticRecur(n - 1);\n}\n
space_complexity.dart
/* 平方阶(递归实现) */\nint quadraticRecur(int n) {\n  if (n <= 0) return 0;\n  List<int> nums = List.filled(n, 0);\n  print('递归 n = $n 中的 nums 长度 = ${nums.length}');\n  return quadraticRecur(n - 1);\n}\n
space_complexity.rs
/* 平方阶(递归实现) */\nfn quadratic_recur(n: i32) -> i32 {\n    if n <= 0 {\n        return 0;\n    };\n    // 数组 nums 长度为 n, n-1, ..., 2, 1\n    let nums = vec![0; n as usize];\n    println!(\"递归 n = {} 中的 nums 长度 = {}\", n, nums.len());\n    return quadratic_recur(n - 1);\n}\n
space_complexity.c
/* 平方阶(递归实现) */\nint quadraticRecur(int n) {\n    if (n <= 0)\n        return 0;\n    int *nums = malloc(sizeof(int) * n);\n    printf(\"递归 n = %d 中的 nums 长度 = %d\\r\\n\", n, n);\n    int res = quadraticRecur(n - 1);\n    free(nums);\n    return res;\n}\n
space_complexity.kt
/* 平方阶(递归实现) */\ntailrec fun quadraticRecur(n: Int): Int {\n    if (n <= 0)\n        return 0\n    // 数组 nums 长度为 n, n-1, ..., 2, 1\n    val nums = Array(n) { 0 }\n    println(\"递归 n = $n 中的 nums 长度 = ${nums.size}\")\n    return quadraticRecur(n - 1)\n}\n
space_complexity.rb
### 平方阶(递归实现)###\ndef quadratic_recur(n)\n  return 0 unless n > 0\n\n  # 数组 nums 长度为 n, n-1, ..., 2, 1\n  nums = Array.new(n, 0)\n  quadratic_recur(n - 1)\nend\n
可视化运行

全屏观看 >

图 2-18   递归函数产生的平方阶空间复杂度

","path":["第 2 章   复杂度分析","2.4   空间复杂度"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#4-o2n","level":3,"title":"4.   指数阶 \\(O(2^n)\\)","text":"

指数阶常见于二叉树。观察图 2-19 ,层数为 \\(n\\) 的“满二叉树”的节点数量为 \\(2^n - 1\\) ,占用 \\(O(2^n)\\) 空间:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby space_complexity.py
def build_tree(n: int) -> TreeNode | None:\n    \"\"\"指数阶(建立满二叉树)\"\"\"\n    if n == 0:\n        return None\n    root = TreeNode(0)\n    root.left = build_tree(n - 1)\n    root.right = build_tree(n - 1)\n    return root\n
space_complexity.cpp
/* 指数阶(建立满二叉树) */\nTreeNode *buildTree(int n) {\n    if (n == 0)\n        return nullptr;\n    TreeNode *root = new TreeNode(0);\n    root->left = buildTree(n - 1);\n    root->right = buildTree(n - 1);\n    return root;\n}\n
space_complexity.java
/* 指数阶(建立满二叉树) */\nTreeNode buildTree(int n) {\n    if (n == 0)\n        return null;\n    TreeNode root = new TreeNode(0);\n    root.left = buildTree(n - 1);\n    root.right = buildTree(n - 1);\n    return root;\n}\n
space_complexity.cs
/* 指数阶(建立满二叉树) */\nTreeNode? BuildTree(int n) {\n    if (n == 0) return null;\n    TreeNode root = new(0) {\n        left = BuildTree(n - 1),\n        right = BuildTree(n - 1)\n    };\n    return root;\n}\n
space_complexity.go
/* 指数阶(建立满二叉树) */\nfunc buildTree(n int) *TreeNode {\n    if n == 0 {\n        return nil\n    }\n    root := NewTreeNode(0)\n    root.Left = buildTree(n - 1)\n    root.Right = buildTree(n - 1)\n    return root\n}\n
space_complexity.swift
/* 指数阶(建立满二叉树) */\nfunc buildTree(n: Int) -> TreeNode? {\n    if n == 0 {\n        return nil\n    }\n    let root = TreeNode(x: 0)\n    root.left = buildTree(n: n - 1)\n    root.right = buildTree(n: n - 1)\n    return root\n}\n
space_complexity.js
/* 指数阶(建立满二叉树) */\nfunction buildTree(n) {\n    if (n === 0) return null;\n    const root = new TreeNode(0);\n    root.left = buildTree(n - 1);\n    root.right = buildTree(n - 1);\n    return root;\n}\n
space_complexity.ts
/* 指数阶(建立满二叉树) */\nfunction buildTree(n: number): TreeNode | null {\n    if (n === 0) return null;\n    const root = new TreeNode(0);\n    root.left = buildTree(n - 1);\n    root.right = buildTree(n - 1);\n    return root;\n}\n
space_complexity.dart
/* 指数阶(建立满二叉树) */\nTreeNode? buildTree(int n) {\n  if (n == 0) return null;\n  TreeNode root = TreeNode(0);\n  root.left = buildTree(n - 1);\n  root.right = buildTree(n - 1);\n  return root;\n}\n
space_complexity.rs
/* 指数阶(建立满二叉树) */\nfn build_tree(n: i32) -> Option<Rc<RefCell<TreeNode>>> {\n    if n == 0 {\n        return None;\n    };\n    let root = TreeNode::new(0);\n    root.borrow_mut().left = build_tree(n - 1);\n    root.borrow_mut().right = build_tree(n - 1);\n    return Some(root);\n}\n
space_complexity.c
/* 指数阶(建立满二叉树) */\nTreeNode *buildTree(int n) {\n    if (n == 0)\n        return NULL;\n    TreeNode *root = newTreeNode(0);\n    root->left = buildTree(n - 1);\n    root->right = buildTree(n - 1);\n    return root;\n}\n
space_complexity.kt
/* 指数阶(建立满二叉树) */\nfun buildTree(n: Int): TreeNode? {\n    if (n == 0)\n        return null\n    val root = TreeNode(0)\n    root.left = buildTree(n - 1)\n    root.right = buildTree(n - 1)\n    return root\n}\n
space_complexity.rb
### 指数阶(建立满二叉树)###\ndef build_tree(n)\n  return if n == 0\n\n  TreeNode.new.tap do |root|\n    root.left = build_tree(n - 1)\n    root.right = build_tree(n - 1)\n  end\nend\n
可视化运行

全屏观看 >

图 2-19   满二叉树产生的指数阶空间复杂度

","path":["第 2 章   复杂度分析","2.4   空间复杂度"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#5-olog-n","level":3,"title":"5.   对数阶 \\(O(\\log n)\\)","text":"

对数阶常见于分治算法。例如归并排序,输入长度为 \\(n\\) 的数组,每轮递归将数组从中点处划分为两半,形成高度为 \\(\\log n\\) 的递归树,使用 \\(O(\\log n)\\) 栈帧空间。

再例如将数字转化为字符串,输入一个正整数 \\(n\\) ,它的位数为 \\(\\lfloor \\log_{10} n \\rfloor + 1\\) ,即对应字符串长度为 \\(\\lfloor \\log_{10} n \\rfloor + 1\\) ,因此空间复杂度为 \\(O(\\log_{10} n + 1) = O(\\log n)\\) 。

","path":["第 2 章   复杂度分析","2.4   空间复杂度"],"tags":[]},{"location":"chapter_computational_complexity/space_complexity/#244","level":2,"title":"2.4.4   权衡时间与空间","text":"

理想情况下,我们希望算法的时间复杂度和空间复杂度都能达到最优。然而在实际情况中,同时优化时间复杂度和空间复杂度通常非常困难。

降低时间复杂度通常需要以提升空间复杂度为代价,反之亦然。我们将牺牲内存空间来提升算法运行速度的思路称为“以空间换时间”;反之,则称为“以时间换空间”。

选择哪种思路取决于我们更看重哪个方面。在大多数情况下,时间比空间更宝贵,因此“以空间换时间”通常是更常用的策略。当然,在数据量很大的情况下,控制空间复杂度也非常重要。

","path":["第 2 章   复杂度分析","2.4   空间复杂度"],"tags":[]},{"location":"chapter_computational_complexity/summary/","level":1,"title":"2.5   小结","text":"","path":["第 2 章   复杂度分析","2.5   小结"],"tags":[]},{"location":"chapter_computational_complexity/summary/#1","level":3,"title":"1.   重点回顾","text":"

算法效率评估

时间复杂度

空间复杂度

","path":["第 2 章   复杂度分析","2.5   小结"],"tags":[]},{"location":"chapter_computational_complexity/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q:尾递归的空间复杂度是 \\(O(1)\\) 吗?

理论上,尾递归函数的空间复杂度可以优化至 \\(O(1)\\) 。不过绝大多数编程语言(例如 Java、Python、C++、Go、C# 等)不支持自动优化尾递归,因此通常认为空间复杂度是 \\(O(n)\\) 。

Q:函数和方法这两个术语的区别是什么?

函数(function)可以被独立执行,所有参数都以显式传递。方法(method)与一个对象关联,被隐式传递给调用它的对象,能够对类的实例中包含的数据进行操作。

下面以几种常见的编程语言为例来说明。

Q:图解“常见的空间复杂度类型”反映的是否是占用空间的绝对大小?

不是,该图展示的是空间复杂度,其反映的是增长趋势,而不是占用空间的绝对大小。

假设取 \\(n = 8\\) ,你可能会发现每条曲线的值与函数对应不上。这是因为每条曲线都包含一个常数项,用于将取值范围压缩到一个视觉舒适的范围内。

在实际中,因为我们通常不知道每个方法的“常数项”复杂度是多少,所以一般无法仅凭复杂度来选择 \\(n = 8\\) 之下的最优解法。但对于 \\(n = 8^5\\) 就很好选了,这时增长趋势已经占主导了。

Q 是否存在根据实际使用场景,选择牺牲时间(或空间)来设计算法的情况?

在实际应用中,大部分情况会选择牺牲空间换时间。例如数据库索引,我们通常选择建立 B+ 树或哈希索引,占用大量内存空间,以换取 \\(O(\\log n)\\) 甚至 \\(O(1)\\) 的高效查询。

在空间资源宝贵的场景,也会选择牺牲时间换空间。例如在嵌入式开发中,设备内存很宝贵,工程师可能会放弃使用哈希表,选择使用数组顺序查找,以节省内存占用,代价是查找变慢。

","path":["第 2 章   复杂度分析","2.5   小结"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/","level":1,"title":"2.3   时间复杂度","text":"

运行时间可以直观且准确地反映算法的效率。如果我们想准确预估一段代码的运行时间,应该如何操作呢?

  1. 确定运行平台,包括硬件配置、编程语言、系统环境等,这些因素都会影响代码的运行效率。
  2. 评估各种计算操作所需的运行时间,例如加法操作 + 需要 1 ns ,乘法操作 * 需要 10 ns ,打印操作 print() 需要 5 ns 等。
  3. 统计代码中所有的计算操作,并将所有操作的执行时间求和,从而得到运行时间。

例如在以下代码中,输入数据大小为 \\(n\\) :

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
# 在某运行平台下\ndef algorithm(n: int):\n    a = 2      # 1 ns\n    a = a + 1  # 1 ns\n    a = a * 2  # 10 ns\n    # 循环 n 次\n    for _ in range(n):  # 1 ns\n        print(0)        # 5 ns\n
// 在某运行平台下\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // 循环 n 次\n    for (int i = 0; i < n; i++) {  // 1 ns\n        cout << 0 << endl;         // 5 ns\n    }\n}\n
// 在某运行平台下\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // 循环 n 次\n    for (int i = 0; i < n; i++) {  // 1 ns\n        System.out.println(0);     // 5 ns\n    }\n}\n
// 在某运行平台下\nvoid Algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // 循环 n 次\n    for (int i = 0; i < n; i++) {  // 1 ns\n        Console.WriteLine(0);      // 5 ns\n    }\n}\n
// 在某运行平台下\nfunc algorithm(n int) {\n    a := 2     // 1 ns\n    a = a + 1  // 1 ns\n    a = a * 2  // 10 ns\n    // 循环 n 次\n    for i := 0; i < n; i++ {  // 1 ns\n        fmt.Println(a)        // 5 ns\n    }\n}\n
// 在某运行平台下\nfunc algorithm(n: Int) {\n    var a = 2 // 1 ns\n    a = a + 1 // 1 ns\n    a = a * 2 // 10 ns\n    // 循环 n 次\n    for _ in 0 ..< n { // 1 ns\n        print(0) // 5 ns\n    }\n}\n
// 在某运行平台下\nfunction algorithm(n) {\n    var a = 2; // 1 ns\n    a = a + 1; // 1 ns\n    a = a * 2; // 10 ns\n    // 循环 n 次\n    for(let i = 0; i < n; i++) { // 1 ns\n        console.log(0); // 5 ns\n    }\n}\n
// 在某运行平台下\nfunction algorithm(n: number): void {\n    var a: number = 2; // 1 ns\n    a = a + 1; // 1 ns\n    a = a * 2; // 10 ns\n    // 循环 n 次\n    for(let i = 0; i < n; i++) { // 1 ns\n        console.log(0); // 5 ns\n    }\n}\n
// 在某运行平台下\nvoid algorithm(int n) {\n  int a = 2; // 1 ns\n  a = a + 1; // 1 ns\n  a = a * 2; // 10 ns\n  // 循环 n 次\n  for (int i = 0; i < n; i++) { // 1 ns\n    print(0); // 5 ns\n  }\n}\n
// 在某运行平台下\nfn algorithm(n: i32) {\n    let mut a = 2;      // 1 ns\n    a = a + 1;          // 1 ns\n    a = a * 2;          // 10 ns\n    // 循环 n 次\n    for _ in 0..n {     // 1 ns\n        println!(\"{}\", 0);  // 5 ns\n    }\n}\n
// 在某运行平台下\nvoid algorithm(int n) {\n    int a = 2;  // 1 ns\n    a = a + 1;  // 1 ns\n    a = a * 2;  // 10 ns\n    // 循环 n 次\n    for (int i = 0; i < n; i++) {   // 1 ns\n        printf(\"%d\", 0);            // 5 ns\n    }\n}\n
// 在某运行平台下\nfun algorithm(n: Int) {\n    var a = 2 // 1 ns\n    a = a + 1 // 1 ns\n    a = a * 2 // 10 ns\n    // 循环 n 次\n    for (i in 0..<n) {  // 1 ns\n        println(0)      // 5 ns\n    }\n}\n
# 在某运行平台下\ndef algorithm(n)\n    a = 2       # 1 ns\n    a = a + 1   # 1 ns\n    a = a * 2   # 10 ns\n    # 循环 n 次\n    (0...n).each do # 1 ns\n        puts 0      # 5 ns\n    end\nend\n

根据以上方法,可以得到算法的运行时间为 \\((6n + 12)\\) ns :

\\[ 1 + 1 + 10 + (1 + 5) \\times n = 6n + 12 \\]

但实际上,统计算法的运行时间既不合理也不现实。首先,我们不希望将预估时间和运行平台绑定,因为算法需要在各种不同的平台上运行。其次,我们很难获知每种操作的运行时间,这给预估过程带来了极大的难度。

","path":["第 2 章   复杂度分析","2.3   时间复杂度"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#231","level":2,"title":"2.3.1   统计时间增长趋势","text":"

时间复杂度分析统计的不是算法运行时间,而是算法运行时间随着数据量变大时的增长趋势。

“时间增长趋势”这个概念比较抽象,我们通过一个例子来加以理解。假设输入数据大小为 \\(n\\) ,给定三个算法 ABC

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
# 算法 A 的时间复杂度:常数阶\ndef algorithm_A(n: int):\n    print(0)\n# 算法 B 的时间复杂度:线性阶\ndef algorithm_B(n: int):\n    for _ in range(n):\n        print(0)\n# 算法 C 的时间复杂度:常数阶\ndef algorithm_C(n: int):\n    for _ in range(1000000):\n        print(0)\n
// 算法 A 的时间复杂度:常数阶\nvoid algorithm_A(int n) {\n    cout << 0 << endl;\n}\n// 算法 B 的时间复杂度:线性阶\nvoid algorithm_B(int n) {\n    for (int i = 0; i < n; i++) {\n        cout << 0 << endl;\n    }\n}\n// 算法 C 的时间复杂度:常数阶\nvoid algorithm_C(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        cout << 0 << endl;\n    }\n}\n
// 算法 A 的时间复杂度:常数阶\nvoid algorithm_A(int n) {\n    System.out.println(0);\n}\n// 算法 B 的时间复杂度:线性阶\nvoid algorithm_B(int n) {\n    for (int i = 0; i < n; i++) {\n        System.out.println(0);\n    }\n}\n// 算法 C 的时间复杂度:常数阶\nvoid algorithm_C(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        System.out.println(0);\n    }\n}\n
// 算法 A 的时间复杂度:常数阶\nvoid AlgorithmA(int n) {\n    Console.WriteLine(0);\n}\n// 算法 B 的时间复杂度:线性阶\nvoid AlgorithmB(int n) {\n    for (int i = 0; i < n; i++) {\n        Console.WriteLine(0);\n    }\n}\n// 算法 C 的时间复杂度:常数阶\nvoid AlgorithmC(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        Console.WriteLine(0);\n    }\n}\n
// 算法 A 的时间复杂度:常数阶\nfunc algorithm_A(n int) {\n    fmt.Println(0)\n}\n// 算法 B 的时间复杂度:线性阶\nfunc algorithm_B(n int) {\n    for i := 0; i < n; i++ {\n        fmt.Println(0)\n    }\n}\n// 算法 C 的时间复杂度:常数阶\nfunc algorithm_C(n int) {\n    for i := 0; i < 1000000; i++ {\n        fmt.Println(0)\n    }\n}\n
// 算法 A 的时间复杂度:常数阶\nfunc algorithmA(n: Int) {\n    print(0)\n}\n\n// 算法 B 的时间复杂度:线性阶\nfunc algorithmB(n: Int) {\n    for _ in 0 ..< n {\n        print(0)\n    }\n}\n\n// 算法 C 的时间复杂度:常数阶\nfunc algorithmC(n: Int) {\n    for _ in 0 ..< 1_000_000 {\n        print(0)\n    }\n}\n
// 算法 A 的时间复杂度:常数阶\nfunction algorithm_A(n) {\n    console.log(0);\n}\n// 算法 B 的时间复杂度:线性阶\nfunction algorithm_B(n) {\n    for (let i = 0; i < n; i++) {\n        console.log(0);\n    }\n}\n// 算法 C 的时间复杂度:常数阶\nfunction algorithm_C(n) {\n    for (let i = 0; i < 1000000; i++) {\n        console.log(0);\n    }\n}\n
// 算法 A 的时间复杂度:常数阶\nfunction algorithm_A(n: number): void {\n    console.log(0);\n}\n// 算法 B 的时间复杂度:线性阶\nfunction algorithm_B(n: number): void {\n    for (let i = 0; i < n; i++) {\n        console.log(0);\n    }\n}\n// 算法 C 的时间复杂度:常数阶\nfunction algorithm_C(n: number): void {\n    for (let i = 0; i < 1000000; i++) {\n        console.log(0);\n    }\n}\n
// 算法 A 的时间复杂度:常数阶\nvoid algorithmA(int n) {\n  print(0);\n}\n// 算法 B 的时间复杂度:线性阶\nvoid algorithmB(int n) {\n  for (int i = 0; i < n; i++) {\n    print(0);\n  }\n}\n// 算法 C 的时间复杂度:常数阶\nvoid algorithmC(int n) {\n  for (int i = 0; i < 1000000; i++) {\n    print(0);\n  }\n}\n
// 算法 A 的时间复杂度:常数阶\nfn algorithm_A(n: i32) {\n    println!(\"{}\", 0);\n}\n// 算法 B 的时间复杂度:线性阶\nfn algorithm_B(n: i32) {\n    for _ in 0..n {\n        println!(\"{}\", 0);\n    }\n}\n// 算法 C 的时间复杂度:常数阶\nfn algorithm_C(n: i32) {\n    for _ in 0..1000000 {\n        println!(\"{}\", 0);\n    }\n}\n
// 算法 A 的时间复杂度:常数阶\nvoid algorithm_A(int n) {\n    printf(\"%d\", 0);\n}\n// 算法 B 的时间复杂度:线性阶\nvoid algorithm_B(int n) {\n    for (int i = 0; i < n; i++) {\n        printf(\"%d\", 0);\n    }\n}\n// 算法 C 的时间复杂度:常数阶\nvoid algorithm_C(int n) {\n    for (int i = 0; i < 1000000; i++) {\n        printf(\"%d\", 0);\n    }\n}\n
// 算法 A 的时间复杂度:常数阶\nfun algoritm_A(n: Int) {\n    println(0)\n}\n// 算法 B 的时间复杂度:线性阶\nfun algorithm_B(n: Int) {\n    for (i in 0..<n){\n        println(0)\n    }\n}\n// 算法 C 的时间复杂度:常数阶\nfun algorithm_C(n: Int) {\n    for (i in 0..<1000000) {\n        println(0)\n    }\n}\n
# 算法 A 的时间复杂度:常数阶\ndef algorithm_A(n)\n    puts 0\nend\n\n# 算法 B 的时间复杂度:线性阶\ndef algorithm_B(n)\n    (0...n).each { puts 0 }\nend\n\n# 算法 C 的时间复杂度:常数阶\ndef algorithm_C(n)\n    (0...1_000_000).each { puts 0 }\nend\n

图 2-7 展示了以上三个算法函数的时间复杂度。

图 2-7   算法 A、B 和 C 的时间增长趋势

相较于直接统计算法的运行时间,时间复杂度分析有哪些特点呢?

","path":["第 2 章   复杂度分析","2.3   时间复杂度"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#232","level":2,"title":"2.3.2   函数渐近上界","text":"

给定一个输入大小为 \\(n\\) 的函数:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
def algorithm(n: int):\n    a = 1      # +1\n    a = a + 1  # +1\n    a = a * 2  # +1\n    # 循环 n 次\n    for i in range(n):  # +1\n        print(0)        # +1\n
void algorithm(int n) {\n    int a = 1;  // +1\n    a = a + 1;  // +1\n    a = a * 2;  // +1\n    // 循环 n 次\n    for (int i = 0; i < n; i++) { // +1(每轮都执行 i ++)\n        cout << 0 << endl;    // +1\n    }\n}\n
void algorithm(int n) {\n    int a = 1;  // +1\n    a = a + 1;  // +1\n    a = a * 2;  // +1\n    // 循环 n 次\n    for (int i = 0; i < n; i++) { // +1(每轮都执行 i ++)\n        System.out.println(0);    // +1\n    }\n}\n
void Algorithm(int n) {\n    int a = 1;  // +1\n    a = a + 1;  // +1\n    a = a * 2;  // +1\n    // 循环 n 次\n    for (int i = 0; i < n; i++) {   // +1(每轮都执行 i ++)\n        Console.WriteLine(0);   // +1\n    }\n}\n
func algorithm(n int) {\n    a := 1      // +1\n    a = a + 1   // +1\n    a = a * 2   // +1\n    // 循环 n 次\n    for i := 0; i < n; i++ {   // +1\n        fmt.Println(a)         // +1\n    }\n}\n
func algorithm(n: Int) {\n    var a = 1 // +1\n    a = a + 1 // +1\n    a = a * 2 // +1\n    // 循环 n 次\n    for _ in 0 ..< n { // +1\n        print(0) // +1\n    }\n}\n
function algorithm(n) {\n    var a = 1; // +1\n    a += 1; // +1\n    a *= 2; // +1\n    // 循环 n 次\n    for(let i = 0; i < n; i++){ // +1(每轮都执行 i ++)\n        console.log(0); // +1\n    }\n}\n
function algorithm(n: number): void{\n    var a: number = 1; // +1\n    a += 1; // +1\n    a *= 2; // +1\n    // 循环 n 次\n    for(let i = 0; i < n; i++){ // +1(每轮都执行 i ++)\n        console.log(0); // +1\n    }\n}\n
void algorithm(int n) {\n  int a = 1; // +1\n  a = a + 1; // +1\n  a = a * 2; // +1\n  // 循环 n 次\n  for (int i = 0; i < n; i++) { // +1(每轮都执行 i ++)\n    print(0); // +1\n  }\n}\n
fn algorithm(n: i32) {\n    let mut a = 1;   // +1\n    a = a + 1;      // +1\n    a = a * 2;      // +1\n\n    // 循环 n 次\n    for _ in 0..n { // +1(每轮都执行 i ++)\n        println!(\"{}\", 0); // +1\n    }\n}\n
void algorithm(int n) {\n    int a = 1;  // +1\n    a = a + 1;  // +1\n    a = a * 2;  // +1\n    // 循环 n 次\n    for (int i = 0; i < n; i++) {   // +1(每轮都执行 i ++)\n        printf(\"%d\", 0);            // +1\n    }\n}\n
fun algorithm(n: Int) {\n    var a = 1 // +1\n    a = a + 1 // +1\n    a = a * 2 // +1\n    // 循环 n 次\n    for (i in 0..<n) { // +1(每轮都执行 i ++)\n        println(0) // +1\n    }\n}\n
def algorithm(n)\n    a = 1       # +1\n    a = a + 1   # +1\n    a = a * 2   # +1\n    # 循环 n 次\n    (0...n).each do # +1\n        puts 0      # +1\n    end\nend\n

设算法的操作数量是一个关于输入数据大小 \\(n\\) 的函数,记为 \\(T(n)\\) ,则以上函数的操作数量为:

\\[ T(n) = 3 + 2n \\]

\\(T(n)\\) 是一次函数,说明其运行时间的增长趋势是线性的,因此它的时间复杂度是线性阶。

我们将线性阶的时间复杂度记为 \\(O(n)\\) ,这个数学符号称为大 \\(O\\) 记号(big-\\(O\\) notation),表示函数 \\(T(n)\\) 的渐近上界(asymptotic upper bound)。

时间复杂度分析本质上是计算“操作数量 \\(T(n)\\)”的渐近上界,它具有明确的数学定义。

函数渐近上界

若存在正实数 \\(c\\) 和实数 \\(n_0\\) ,使得对于所有的 \\(n > n_0\\) ,均有 \\(T(n) \\leq c \\cdot f(n)\\) ,则可认为 \\(f(n)\\) 给出了 \\(T(n)\\) 的一个渐近上界,记为 \\(T(n) = O(f(n))\\) 。

如图 2-8 所示,计算渐近上界就是寻找一个函数 \\(f(n)\\) ,使得当 \\(n\\) 趋向于无穷大时,\\(T(n)\\) 和 \\(f(n)\\) 处于相同的增长级别,仅相差一个常数系数 \\(c\\)。

图 2-8   函数的渐近上界

","path":["第 2 章   复杂度分析","2.3   时间复杂度"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#233","level":2,"title":"2.3.3   推算方法","text":"

渐近上界的数学味儿有点重,如果你感觉没有完全理解,也无须担心。我们可以先掌握推算方法,在不断的实践中,就可以逐渐领悟其数学意义。

根据定义,确定 \\(f(n)\\) 之后,我们便可得到时间复杂度 \\(O(f(n))\\) 。那么如何确定渐近上界 \\(f(n)\\) 呢?总体分为两步:首先统计操作数量,然后判断渐近上界。

","path":["第 2 章   复杂度分析","2.3   时间复杂度"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#1","level":3,"title":"1.   第一步:统计操作数量","text":"

针对代码,逐行从上到下计算即可。然而,由于上述 \\(c \\cdot f(n)\\) 中的常数系数 \\(c\\) 可以取任意大小,因此操作数量 \\(T(n)\\) 中的各种系数、常数项都可以忽略。根据此原则,可以总结出以下计数简化技巧。

  1. 忽略 \\(T(n)\\) 中的常数。因为它们都与 \\(n\\) 无关,所以对时间复杂度不产生影响。
  2. 省略所有系数。例如,循环 \\(2n\\) 次、\\(5n + 1\\) 次等,都可以简化记为 \\(n\\) 次,因为 \\(n\\) 前面的系数对时间复杂度没有影响。
  3. 循环嵌套时使用乘法。总操作数量等于外层循环和内层循环操作数量之积,每一层循环依然可以分别套用第 1. 点和第 2. 点的技巧。

给定一个函数,我们可以用上述技巧来统计操作数量:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
def algorithm(n: int):\n    a = 1      # +0(技巧 1)\n    a = a + n  # +0(技巧 1)\n    # +n(技巧 2)\n    for i in range(5 * n + 1):\n        print(0)\n    # +n*n(技巧 3)\n    for i in range(2 * n):\n        for j in range(n + 1):\n            print(0)\n
void algorithm(int n) {\n    int a = 1;  // +0(技巧 1)\n    a = a + n;  // +0(技巧 1)\n    // +n(技巧 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        cout << 0 << endl;\n    }\n    // +n*n(技巧 3)\n    for (int i = 0; i < 2 * n; i++) {\n        for (int j = 0; j < n + 1; j++) {\n            cout << 0 << endl;\n        }\n    }\n}\n
void algorithm(int n) {\n    int a = 1;  // +0(技巧 1)\n    a = a + n;  // +0(技巧 1)\n    // +n(技巧 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        System.out.println(0);\n    }\n    // +n*n(技巧 3)\n    for (int i = 0; i < 2 * n; i++) {\n        for (int j = 0; j < n + 1; j++) {\n            System.out.println(0);\n        }\n    }\n}\n
void Algorithm(int n) {\n    int a = 1;  // +0(技巧 1)\n    a = a + n;  // +0(技巧 1)\n    // +n(技巧 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        Console.WriteLine(0);\n    }\n    // +n*n(技巧 3)\n    for (int i = 0; i < 2 * n; i++) {\n        for (int j = 0; j < n + 1; j++) {\n            Console.WriteLine(0);\n        }\n    }\n}\n
func algorithm(n int) {\n    a := 1     // +0(技巧 1)\n    a = a + n  // +0(技巧 1)\n    // +n(技巧 2)\n    for i := 0; i < 5 * n + 1; i++ {\n        fmt.Println(0)\n    }\n    // +n*n(技巧 3)\n    for i := 0; i < 2 * n; i++ {\n        for j := 0; j < n + 1; j++ {\n            fmt.Println(0)\n        }\n    }\n}\n
func algorithm(n: Int) {\n    var a = 1 // +0(技巧 1)\n    a = a + n // +0(技巧 1)\n    // +n(技巧 2)\n    for _ in 0 ..< (5 * n + 1) {\n        print(0)\n    }\n    // +n*n(技巧 3)\n    for _ in 0 ..< (2 * n) {\n        for _ in 0 ..< (n + 1) {\n            print(0)\n        }\n    }\n}\n
function algorithm(n) {\n    let a = 1;  // +0(技巧 1)\n    a = a + n;  // +0(技巧 1)\n    // +n(技巧 2)\n    for (let i = 0; i < 5 * n + 1; i++) {\n        console.log(0);\n    }\n    // +n*n(技巧 3)\n    for (let i = 0; i < 2 * n; i++) {\n        for (let j = 0; j < n + 1; j++) {\n            console.log(0);\n        }\n    }\n}\n
function algorithm(n: number): void {\n    let a = 1;  // +0(技巧 1)\n    a = a + n;  // +0(技巧 1)\n    // +n(技巧 2)\n    for (let i = 0; i < 5 * n + 1; i++) {\n        console.log(0);\n    }\n    // +n*n(技巧 3)\n    for (let i = 0; i < 2 * n; i++) {\n        for (let j = 0; j < n + 1; j++) {\n            console.log(0);\n        }\n    }\n}\n
void algorithm(int n) {\n  int a = 1; // +0(技巧 1)\n  a = a + n; // +0(技巧 1)\n  // +n(技巧 2)\n  for (int i = 0; i < 5 * n + 1; i++) {\n    print(0);\n  }\n  // +n*n(技巧 3)\n  for (int i = 0; i < 2 * n; i++) {\n    for (int j = 0; j < n + 1; j++) {\n      print(0);\n    }\n  }\n}\n
fn algorithm(n: i32) {\n    let mut a = 1;     // +0(技巧 1)\n    a = a + n;        // +0(技巧 1)\n\n    // +n(技巧 2)\n    for i in 0..(5 * n + 1) {\n        println!(\"{}\", 0);\n    }\n\n    // +n*n(技巧 3)\n    for i in 0..(2 * n) {\n        for j in 0..(n + 1) {\n            println!(\"{}\", 0);\n        }\n    }\n}\n
void algorithm(int n) {\n    int a = 1;  // +0(技巧 1)\n    a = a + n;  // +0(技巧 1)\n    // +n(技巧 2)\n    for (int i = 0; i < 5 * n + 1; i++) {\n        printf(\"%d\", 0);\n    }\n    // +n*n(技巧 3)\n    for (int i = 0; i < 2 * n; i++) {\n        for (int j = 0; j < n + 1; j++) {\n            printf(\"%d\", 0);\n        }\n    }\n}\n
fun algorithm(n: Int) {\n    var a = 1   // +0(技巧 1)\n    a = a + n   // +0(技巧 1)\n    // +n(技巧 2)\n    for (i in 0..<5 * n + 1) {\n        println(0)\n    }\n    // +n*n(技巧 3)\n    for (i in 0..<2 * n) {\n        for (j in 0..<n + 1) {\n            println(0)\n        }\n    }\n}\n
def algorithm(n)\n    a = 1       # +0(技巧 1)\n    a = a + n   # +0(技巧 1)\n    # +n(技巧 2)\n    (0...(5 * n + 1)).each do { puts 0 }\n    # +n*n(技巧 3)\n    (0...(2 * n)).each do\n        (0...(n + 1)).each do { puts 0 }\n    end\nend\n

以下公式展示了使用上述技巧前后的统计结果,两者推算出的时间复杂度都为 \\(O(n^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} \\]","path":["第 2 章   复杂度分析","2.3   时间复杂度"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#2","level":3,"title":"2.   第二步:判断渐近上界","text":"

时间复杂度由 \\(T(n)\\) 中最高阶的项来决定。这是因为在 \\(n\\) 趋于无穷大时,最高阶的项将发挥主导作用,其他项的影响都可以忽略。

表 2-2 展示了一些例子,其中一些夸张的值是为了强调“系数无法撼动阶数”这一结论。当 \\(n\\) 趋于无穷大时,这些常数变得无足轻重。

表 2-2   不同操作数量对应的时间复杂度

操作数量 \\(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)\\)","path":["第 2 章   复杂度分析","2.3   时间复杂度"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#234","level":2,"title":"2.3.4   常见类型","text":"

设输入数据大小为 \\(n\\) ,常见的时间复杂度类型如图 2-9 所示(按照从低到高的顺序排列)。

\\[ \\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} \\]

图 2-9   常见的时间复杂度类型

","path":["第 2 章   复杂度分析","2.3   时间复杂度"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#1-o1","level":3,"title":"1.   常数阶 \\(O(1)\\)","text":"

常数阶的操作数量与输入数据大小 \\(n\\) 无关,即不随着 \\(n\\) 的变化而变化。

在以下函数中,尽管操作数量 size 可能很大,但由于其与输入数据大小 \\(n\\) 无关,因此时间复杂度仍为 \\(O(1)\\) :

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def constant(n: int) -> int:\n    \"\"\"常数阶\"\"\"\n    count = 0\n    size = 100000\n    for _ in range(size):\n        count += 1\n    return count\n
time_complexity.cpp
/* 常数阶 */\nint constant(int n) {\n    int count = 0;\n    int size = 100000;\n    for (int i = 0; i < size; i++)\n        count++;\n    return count;\n}\n
time_complexity.java
/* 常数阶 */\nint constant(int n) {\n    int count = 0;\n    int size = 100000;\n    for (int i = 0; i < size; i++)\n        count++;\n    return count;\n}\n
time_complexity.cs
/* 常数阶 */\nint Constant(int n) {\n    int count = 0;\n    int size = 100000;\n    for (int i = 0; i < size; i++)\n        count++;\n    return count;\n}\n
time_complexity.go
/* 常数阶 */\nfunc constant(n int) int {\n    count := 0\n    size := 100000\n    for i := 0; i < size; i++ {\n        count++\n    }\n    return count\n}\n
time_complexity.swift
/* 常数阶 */\nfunc constant(n: Int) -> Int {\n    var count = 0\n    let size = 100_000\n    for _ in 0 ..< size {\n        count += 1\n    }\n    return count\n}\n
time_complexity.js
/* 常数阶 */\nfunction constant(n) {\n    let count = 0;\n    const size = 100000;\n    for (let i = 0; i < size; i++) count++;\n    return count;\n}\n
time_complexity.ts
/* 常数阶 */\nfunction constant(n: number): number {\n    let count = 0;\n    const size = 100000;\n    for (let i = 0; i < size; i++) count++;\n    return count;\n}\n
time_complexity.dart
/* 常数阶 */\nint constant(int n) {\n  int count = 0;\n  int size = 100000;\n  for (var i = 0; i < size; i++) {\n    count++;\n  }\n  return count;\n}\n
time_complexity.rs
/* 常数阶 */\nfn constant(n: i32) -> i32 {\n    _ = n;\n    let mut count = 0;\n    let size = 100_000;\n    for _ in 0..size {\n        count += 1;\n    }\n    count\n}\n
time_complexity.c
/* 常数阶 */\nint constant(int n) {\n    int count = 0;\n    int size = 100000;\n    int i = 0;\n    for (int i = 0; i < size; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.kt
/* 常数阶 */\nfun constant(n: Int): Int {\n    var count = 0\n    val size = 100000\n    for (i in 0..<size)\n        count++\n    return count\n}\n
time_complexity.rb
### 常数阶 ###\ndef constant(n)\n  count = 0\n  size = 100000\n\n  (0...size).each { count += 1 }\n\n  count\nend\n
可视化运行

全屏观看 >

","path":["第 2 章   复杂度分析","2.3   时间复杂度"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#2-on","level":3,"title":"2.   线性阶 \\(O(n)\\)","text":"

线性阶的操作数量相对于输入数据大小 \\(n\\) 以线性级别增长。线性阶通常出现在单层循环中:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def linear(n: int) -> int:\n    \"\"\"线性阶\"\"\"\n    count = 0\n    for _ in range(n):\n        count += 1\n    return count\n
time_complexity.cpp
/* 线性阶 */\nint linear(int n) {\n    int count = 0;\n    for (int i = 0; i < n; i++)\n        count++;\n    return count;\n}\n
time_complexity.java
/* 线性阶 */\nint linear(int n) {\n    int count = 0;\n    for (int i = 0; i < n; i++)\n        count++;\n    return count;\n}\n
time_complexity.cs
/* 线性阶 */\nint Linear(int n) {\n    int count = 0;\n    for (int i = 0; i < n; i++)\n        count++;\n    return count;\n}\n
time_complexity.go
/* 线性阶 */\nfunc linear(n int) int {\n    count := 0\n    for i := 0; i < n; i++ {\n        count++\n    }\n    return count\n}\n
time_complexity.swift
/* 线性阶 */\nfunc linear(n: Int) -> Int {\n    var count = 0\n    for _ in 0 ..< n {\n        count += 1\n    }\n    return count\n}\n
time_complexity.js
/* 线性阶 */\nfunction linear(n) {\n    let count = 0;\n    for (let i = 0; i < n; i++) count++;\n    return count;\n}\n
time_complexity.ts
/* 线性阶 */\nfunction linear(n: number): number {\n    let count = 0;\n    for (let i = 0; i < n; i++) count++;\n    return count;\n}\n
time_complexity.dart
/* 线性阶 */\nint linear(int n) {\n  int count = 0;\n  for (var i = 0; i < n; i++) {\n    count++;\n  }\n  return count;\n}\n
time_complexity.rs
/* 线性阶 */\nfn linear(n: i32) -> i32 {\n    let mut count = 0;\n    for _ in 0..n {\n        count += 1;\n    }\n    count\n}\n
time_complexity.c
/* 线性阶 */\nint linear(int n) {\n    int count = 0;\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.kt
/* 线性阶 */\nfun linear(n: Int): Int {\n    var count = 0\n    for (i in 0..<n)\n        count++\n    return count\n}\n
time_complexity.rb
### 线性阶 ###\ndef linear(n)\n  count = 0\n  (0...n).each { count += 1 }\n  count\nend\n
可视化运行

全屏观看 >

遍历数组和遍历链表等操作的时间复杂度均为 \\(O(n)\\) ,其中 \\(n\\) 为数组或链表的长度:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def array_traversal(nums: list[int]) -> int:\n    \"\"\"线性阶(遍历数组)\"\"\"\n    count = 0\n    # 循环次数与数组长度成正比\n    for num in nums:\n        count += 1\n    return count\n
time_complexity.cpp
/* 线性阶(遍历数组) */\nint arrayTraversal(vector<int> &nums) {\n    int count = 0;\n    // 循环次数与数组长度成正比\n    for (int num : nums) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.java
/* 线性阶(遍历数组) */\nint arrayTraversal(int[] nums) {\n    int count = 0;\n    // 循环次数与数组长度成正比\n    for (int num : nums) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.cs
/* 线性阶(遍历数组) */\nint ArrayTraversal(int[] nums) {\n    int count = 0;\n    // 循环次数与数组长度成正比\n    foreach (int num in nums) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.go
/* 线性阶(遍历数组) */\nfunc arrayTraversal(nums []int) int {\n    count := 0\n    // 循环次数与数组长度成正比\n    for range nums {\n        count++\n    }\n    return count\n}\n
time_complexity.swift
/* 线性阶(遍历数组) */\nfunc arrayTraversal(nums: [Int]) -> Int {\n    var count = 0\n    // 循环次数与数组长度成正比\n    for _ in nums {\n        count += 1\n    }\n    return count\n}\n
time_complexity.js
/* 线性阶(遍历数组) */\nfunction arrayTraversal(nums) {\n    let count = 0;\n    // 循环次数与数组长度成正比\n    for (let i = 0; i < nums.length; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.ts
/* 线性阶(遍历数组) */\nfunction arrayTraversal(nums: number[]): number {\n    let count = 0;\n    // 循环次数与数组长度成正比\n    for (let i = 0; i < nums.length; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.dart
/* 线性阶(遍历数组) */\nint arrayTraversal(List<int> nums) {\n  int count = 0;\n  // 循环次数与数组长度成正比\n  for (var _num in nums) {\n    count++;\n  }\n  return count;\n}\n
time_complexity.rs
/* 线性阶(遍历数组) */\nfn array_traversal(nums: &[i32]) -> i32 {\n    let mut count = 0;\n    // 循环次数与数组长度成正比\n    for _ in nums {\n        count += 1;\n    }\n    count\n}\n
time_complexity.c
/* 线性阶(遍历数组) */\nint arrayTraversal(int *nums, int n) {\n    int count = 0;\n    // 循环次数与数组长度成正比\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.kt
/* 线性阶(遍历数组) */\nfun arrayTraversal(nums: IntArray): Int {\n    var count = 0\n    // 循环次数与数组长度成正比\n    for (num in nums) {\n        count++\n    }\n    return count\n}\n
time_complexity.rb
### 线性阶(遍历数组)###\ndef array_traversal(nums)\n  count = 0\n\n  # 循环次数与数组长度成正比\n  for num in nums\n    count += 1\n  end\n\n  count\nend\n
可视化运行

全屏观看 >

值得注意的是,输入数据大小 \\(n\\) 需根据输入数据的类型来具体确定。比如在第一个示例中,变量 \\(n\\) 为输入数据大小;在第二个示例中,数组长度 \\(n\\) 为数据大小。

","path":["第 2 章   复杂度分析","2.3   时间复杂度"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#3-on2","level":3,"title":"3.   平方阶 \\(O(n^2)\\)","text":"

平方阶的操作数量相对于输入数据大小 \\(n\\) 以平方级别增长。平方阶通常出现在嵌套循环中,外层循环和内层循环的时间复杂度都为 \\(O(n)\\) ,因此总体的时间复杂度为 \\(O(n^2)\\) :

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def quadratic(n: int) -> int:\n    \"\"\"平方阶\"\"\"\n    count = 0\n    # 循环次数与数据大小 n 成平方关系\n    for i in range(n):\n        for j in range(n):\n            count += 1\n    return count\n
time_complexity.cpp
/* 平方阶 */\nint quadratic(int n) {\n    int count = 0;\n    // 循环次数与数据大小 n 成平方关系\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
time_complexity.java
/* 平方阶 */\nint quadratic(int n) {\n    int count = 0;\n    // 循环次数与数据大小 n 成平方关系\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
time_complexity.cs
/* 平方阶 */\nint Quadratic(int n) {\n    int count = 0;\n    // 循环次数与数据大小 n 成平方关系\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
time_complexity.go
/* 平方阶 */\nfunc quadratic(n int) int {\n    count := 0\n    // 循环次数与数据大小 n 成平方关系\n    for i := 0; i < n; i++ {\n        for j := 0; j < n; j++ {\n            count++\n        }\n    }\n    return count\n}\n
time_complexity.swift
/* 平方阶 */\nfunc quadratic(n: Int) -> Int {\n    var count = 0\n    // 循环次数与数据大小 n 成平方关系\n    for _ in 0 ..< n {\n        for _ in 0 ..< n {\n            count += 1\n        }\n    }\n    return count\n}\n
time_complexity.js
/* 平方阶 */\nfunction quadratic(n) {\n    let count = 0;\n    // 循环次数与数据大小 n 成平方关系\n    for (let i = 0; i < n; i++) {\n        for (let j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
time_complexity.ts
/* 平方阶 */\nfunction quadratic(n: number): number {\n    let count = 0;\n    // 循环次数与数据大小 n 成平方关系\n    for (let i = 0; i < n; i++) {\n        for (let j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
time_complexity.dart
/* 平方阶 */\nint quadratic(int n) {\n  int count = 0;\n  // 循环次数与数据大小 n 成平方关系\n  for (int i = 0; i < n; i++) {\n    for (int j = 0; j < n; j++) {\n      count++;\n    }\n  }\n  return count;\n}\n
time_complexity.rs
/* 平方阶 */\nfn quadratic(n: i32) -> i32 {\n    let mut count = 0;\n    // 循环次数与数据大小 n 成平方关系\n    for _ in 0..n {\n        for _ in 0..n {\n            count += 1;\n        }\n    }\n    count\n}\n
time_complexity.c
/* 平方阶 */\nint quadratic(int n) {\n    int count = 0;\n    // 循环次数与数据大小 n 成平方关系\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < n; j++) {\n            count++;\n        }\n    }\n    return count;\n}\n
time_complexity.kt
/* 平方阶 */\nfun quadratic(n: Int): Int {\n    var count = 0\n    // 循环次数与数据大小 n 成平方关系\n    for (i in 0..<n) {\n        for (j in 0..<n) {\n            count++\n        }\n    }\n    return count\n}\n
time_complexity.rb
### 平方阶 ###\ndef quadratic(n)\n  count = 0\n\n  # 循环次数与数据大小 n 成平方关系\n  for i in 0...n\n    for j in 0...n\n      count += 1\n    end\n  end\n\n  count\nend\n
可视化运行

全屏观看 >

图 2-10 对比了常数阶、线性阶和平方阶三种时间复杂度。

图 2-10   常数阶、线性阶和平方阶的时间复杂度

以冒泡排序为例,外层循环执行 \\(n - 1\\) 次,内层循环执行 \\(n-1\\)、\\(n-2\\)、\\(\\dots\\)、\\(2\\)、\\(1\\) 次,平均为 \\(n / 2\\) 次,因此时间复杂度为 \\(O((n - 1) n / 2) = O(n^2)\\) :

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def bubble_sort(nums: list[int]) -> int:\n    \"\"\"平方阶(冒泡排序)\"\"\"\n    count = 0  # 计数器\n    # 外循环:未排序区间为 [0, i]\n    for i in range(len(nums) - 1, 0, -1):\n        # 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n        for j in range(i):\n            if nums[j] > nums[j + 1]:\n                # 交换 nums[j] 与 nums[j + 1]\n                tmp: int = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = tmp\n                count += 3  # 元素交换包含 3 个单元操作\n    return count\n
time_complexity.cpp
/* 平方阶(冒泡排序) */\nint bubbleSort(vector<int> &nums) {\n    int count = 0; // 计数器\n    // 外循环:未排序区间为 [0, i]\n    for (int i = nums.size() - 1; i > 0; i--) {\n        // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // 交换 nums[j] 与 nums[j + 1]\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // 元素交换包含 3 个单元操作\n            }\n        }\n    }\n    return count;\n}\n
time_complexity.java
/* 平方阶(冒泡排序) */\nint bubbleSort(int[] nums) {\n    int count = 0; // 计数器\n    // 外循环:未排序区间为 [0, i]\n    for (int i = nums.length - 1; i > 0; i--) {\n        // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // 交换 nums[j] 与 nums[j + 1]\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // 元素交换包含 3 个单元操作\n            }\n        }\n    }\n    return count;\n}\n
time_complexity.cs
/* 平方阶(冒泡排序) */\nint BubbleSort(int[] nums) {\n    int count = 0;  // 计数器\n    // 外循环:未排序区间为 [0, i]\n    for (int i = nums.Length - 1; i > 0; i--) {\n        // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // 交换 nums[j] 与 nums[j + 1]\n                (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);\n                count += 3;  // 元素交换包含 3 个单元操作\n            }\n        }\n    }\n    return count;\n}\n
time_complexity.go
/* 平方阶(冒泡排序) */\nfunc bubbleSort(nums []int) int {\n    count := 0 // 计数器\n    // 外循环:未排序区间为 [0, i]\n    for i := len(nums) - 1; i > 0; i-- {\n        // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n        for j := 0; j < i; j++ {\n            if nums[j] > nums[j+1] {\n                // 交换 nums[j] 与 nums[j + 1]\n                tmp := nums[j]\n                nums[j] = nums[j+1]\n                nums[j+1] = tmp\n                count += 3 // 元素交换包含 3 个单元操作\n            }\n        }\n    }\n    return count\n}\n
time_complexity.swift
/* 平方阶(冒泡排序) */\nfunc bubbleSort(nums: inout [Int]) -> Int {\n    var count = 0 // 计数器\n    // 外循环:未排序区间为 [0, i]\n    for i in nums.indices.dropFirst().reversed() {\n        // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n        for j in 0 ..< i {\n            if nums[j] > nums[j + 1] {\n                // 交换 nums[j] 与 nums[j + 1]\n                let tmp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = tmp\n                count += 3 // 元素交换包含 3 个单元操作\n            }\n        }\n    }\n    return count\n}\n
time_complexity.js
/* 平方阶(冒泡排序) */\nfunction bubbleSort(nums) {\n    let count = 0; // 计数器\n    // 外循环:未排序区间为 [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // 交换 nums[j] 与 nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // 元素交换包含 3 个单元操作\n            }\n        }\n    }\n    return count;\n}\n
time_complexity.ts
/* 平方阶(冒泡排序) */\nfunction bubbleSort(nums: number[]): number {\n    let count = 0; // 计数器\n    // 外循环:未排序区间为 [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // 交换 nums[j] 与 nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // 元素交换包含 3 个单元操作\n            }\n        }\n    }\n    return count;\n}\n
time_complexity.dart
/* 平方阶(冒泡排序) */\nint bubbleSort(List<int> nums) {\n  int count = 0; // 计数器\n  // 外循环:未排序区间为 [0, i]\n  for (var i = nums.length - 1; i > 0; i--) {\n    // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n    for (var j = 0; j < i; j++) {\n      if (nums[j] > nums[j + 1]) {\n        // 交换 nums[j] 与 nums[j + 1]\n        int tmp = nums[j];\n        nums[j] = nums[j + 1];\n        nums[j + 1] = tmp;\n        count += 3; // 元素交换包含 3 个单元操作\n      }\n    }\n  }\n  return count;\n}\n
time_complexity.rs
/* 平方阶(冒泡排序) */\nfn bubble_sort(nums: &mut [i32]) -> i32 {\n    let mut count = 0; // 计数器\n\n    // 外循环:未排序区间为 [0, i]\n    for i in (1..nums.len()).rev() {\n        // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n        for j in 0..i {\n            if nums[j] > nums[j + 1] {\n                // 交换 nums[j] 与 nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // 元素交换包含 3 个单元操作\n            }\n        }\n    }\n    count\n}\n
time_complexity.c
/* 平方阶(冒泡排序) */\nint bubbleSort(int *nums, int n) {\n    int count = 0; // 计数器\n    // 外循环:未排序区间为 [0, i]\n    for (int i = n - 1; i > 0; i--) {\n        // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // 交换 nums[j] 与 nums[j + 1]\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                count += 3; // 元素交换包含 3 个单元操作\n            }\n        }\n    }\n    return count;\n}\n
time_complexity.kt
/* 平方阶(冒泡排序) */\nfun bubbleSort(nums: IntArray): Int {\n    var count = 0 // 计数器\n    // 外循环:未排序区间为 [0, i]\n    for (i in nums.size - 1 downTo 1) {\n        // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n        for (j in 0..<i) {\n            if (nums[j] > nums[j + 1]) {\n                // 交换 nums[j] 与 nums[j + 1]\n                val temp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = temp\n                count += 3 // 元素交换包含 3 个单元操作\n            }\n        }\n    }\n    return count\n}\n
time_complexity.rb
### 平方阶(冒泡排序)###\ndef bubble_sort(nums)\n  count = 0  # 计数器\n\n  # 外循环:未排序区间为 [0, i]\n  for i in (nums.length - 1).downto(0)\n    # 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # 交换 nums[j] 与 nums[j + 1]\n        tmp = nums[j]\n        nums[j] = nums[j + 1]\n        nums[j + 1] = tmp\n        count += 3 # 元素交换包含 3 个单元操作\n      end\n    end\n  end\n\n  count\nend\n
可视化运行

全屏观看 >

","path":["第 2 章   复杂度分析","2.3   时间复杂度"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#4-o2n","level":3,"title":"4.   指数阶 \\(O(2^n)\\)","text":"

生物学的“细胞分裂”是指数阶增长的典型例子:初始状态为 \\(1\\) 个细胞,分裂一轮后变为 \\(2\\) 个,分裂两轮后变为 \\(4\\) 个,以此类推,分裂 \\(n\\) 轮后有 \\(2^n\\) 个细胞。

图 2-11 和以下代码模拟了细胞分裂的过程,时间复杂度为 \\(O(2^n)\\) 。请注意,输入 \\(n\\) 表示分裂轮数,返回值 count 表示总分裂次数。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def exponential(n: int) -> int:\n    \"\"\"指数阶(循环实现)\"\"\"\n    count = 0\n    base = 1\n    # 细胞每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1)\n    for _ in range(n):\n        for _ in range(base):\n            count += 1\n        base *= 2\n    # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count\n
time_complexity.cpp
/* 指数阶(循环实现) */\nint exponential(int n) {\n    int count = 0, base = 1;\n    // 细胞每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1)\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < base; j++) {\n            count++;\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
time_complexity.java
/* 指数阶(循环实现) */\nint exponential(int n) {\n    int count = 0, base = 1;\n    // 细胞每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1)\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < base; j++) {\n            count++;\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
time_complexity.cs
/* 指数阶(循环实现) */\nint Exponential(int n) {\n    int count = 0, bas = 1;\n    // 细胞每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1)\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < bas; j++) {\n            count++;\n        }\n        bas *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
time_complexity.go
/* 指数阶(循环实现)*/\nfunc exponential(n int) int {\n    count, base := 0, 1\n    // 细胞每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1)\n    for i := 0; i < n; i++ {\n        for j := 0; j < base; j++ {\n            count++\n        }\n        base *= 2\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count\n}\n
time_complexity.swift
/* 指数阶(循环实现) */\nfunc exponential(n: Int) -> Int {\n    var count = 0\n    var base = 1\n    // 细胞每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1)\n    for _ in 0 ..< n {\n        for _ in 0 ..< base {\n            count += 1\n        }\n        base *= 2\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count\n}\n
time_complexity.js
/* 指数阶(循环实现) */\nfunction exponential(n) {\n    let count = 0,\n        base = 1;\n    // 细胞每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1)\n    for (let i = 0; i < n; i++) {\n        for (let j = 0; j < base; j++) {\n            count++;\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
time_complexity.ts
/* 指数阶(循环实现) */\nfunction exponential(n: number): number {\n    let count = 0,\n        base = 1;\n    // 细胞每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1)\n    for (let i = 0; i < n; i++) {\n        for (let j = 0; j < base; j++) {\n            count++;\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
time_complexity.dart
/* 指数阶(循环实现) */\nint exponential(int n) {\n  int count = 0, base = 1;\n  // 细胞每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1)\n  for (var i = 0; i < n; i++) {\n    for (var j = 0; j < base; j++) {\n      count++;\n    }\n    base *= 2;\n  }\n  // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n  return count;\n}\n
time_complexity.rs
/* 指数阶(循环实现) */\nfn exponential(n: i32) -> i32 {\n    let mut count = 0;\n    let mut base = 1;\n    // 细胞每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1)\n    for _ in 0..n {\n        for _ in 0..base {\n            count += 1\n        }\n        base *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    count\n}\n
time_complexity.c
/* 指数阶(循环实现) */\nint exponential(int n) {\n    int count = 0;\n    int bas = 1;\n    // 细胞每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1)\n    for (int i = 0; i < n; i++) {\n        for (int j = 0; j < bas; j++) {\n            count++;\n        }\n        bas *= 2;\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count;\n}\n
time_complexity.kt
/* 指数阶(循环实现) */\nfun exponential(n: Int): Int {\n    var count = 0\n    var base = 1\n    // 细胞每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1)\n    for (i in 0..<n) {\n        for (j in 0..<base) {\n            count++\n        }\n        base *= 2\n    }\n    // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n    return count\n}\n
time_complexity.rb
### 指数阶(循环实现)###\ndef exponential(n)\n  count, base = 0, 1\n\n  # 细胞每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1)\n  (0...n).each do\n    (0...base).each { count += 1 }\n    base *= 2\n  end\n\n  # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1\n  count\nend\n
可视化运行

全屏观看 >

图 2-11   指数阶的时间复杂度

在实际算法中,指数阶常出现于递归函数中。例如在以下代码中,其递归地一分为二,经过 \\(n\\) 次分裂后停止:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def exp_recur(n: int) -> int:\n    \"\"\"指数阶(递归实现)\"\"\"\n    if n == 1:\n        return 1\n    return exp_recur(n - 1) + exp_recur(n - 1) + 1\n
time_complexity.cpp
/* 指数阶(递归实现) */\nint expRecur(int n) {\n    if (n == 1)\n        return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
time_complexity.java
/* 指数阶(递归实现) */\nint expRecur(int n) {\n    if (n == 1)\n        return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
time_complexity.cs
/* 指数阶(递归实现) */\nint ExpRecur(int n) {\n    if (n == 1) return 1;\n    return ExpRecur(n - 1) + ExpRecur(n - 1) + 1;\n}\n
time_complexity.go
/* 指数阶(递归实现)*/\nfunc expRecur(n int) int {\n    if n == 1 {\n        return 1\n    }\n    return expRecur(n-1) + expRecur(n-1) + 1\n}\n
time_complexity.swift
/* 指数阶(递归实现) */\nfunc expRecur(n: Int) -> Int {\n    if n == 1 {\n        return 1\n    }\n    return expRecur(n: n - 1) + expRecur(n: n - 1) + 1\n}\n
time_complexity.js
/* 指数阶(递归实现) */\nfunction expRecur(n) {\n    if (n === 1) return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
time_complexity.ts
/* 指数阶(递归实现) */\nfunction expRecur(n: number): number {\n    if (n === 1) return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
time_complexity.dart
/* 指数阶(递归实现) */\nint expRecur(int n) {\n  if (n == 1) return 1;\n  return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
time_complexity.rs
/* 指数阶(递归实现) */\nfn exp_recur(n: i32) -> i32 {\n    if n == 1 {\n        return 1;\n    }\n    exp_recur(n - 1) + exp_recur(n - 1) + 1\n}\n
time_complexity.c
/* 指数阶(递归实现) */\nint expRecur(int n) {\n    if (n == 1)\n        return 1;\n    return expRecur(n - 1) + expRecur(n - 1) + 1;\n}\n
time_complexity.kt
/* 指数阶(递归实现) */\nfun expRecur(n: Int): Int {\n    if (n == 1) {\n        return 1\n    }\n    return expRecur(n - 1) + expRecur(n - 1) + 1\n}\n
time_complexity.rb
### 指数阶(递归实现)###\ndef exp_recur(n)\n  return 1 if n == 1\n  exp_recur(n - 1) + exp_recur(n - 1) + 1\nend\n
可视化运行

全屏观看 >

指数阶增长非常迅速,在穷举法(暴力搜索、回溯等)中比较常见。对于数据规模较大的问题,指数阶是不可接受的,通常需要使用动态规划或贪心算法等来解决。

","path":["第 2 章   复杂度分析","2.3   时间复杂度"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#5-olog-n","level":3,"title":"5.   对数阶 \\(O(\\log n)\\)","text":"

与指数阶相反,对数阶反映了“每轮缩减到一半”的情况。设输入数据大小为 \\(n\\) ,由于每轮缩减到一半,因此循环次数是 \\(\\log_2 n\\) ,即 \\(2^n\\) 的反函数。

图 2-12 和以下代码模拟了“每轮缩减到一半”的过程,时间复杂度为 \\(O(\\log_2 n)\\) ,简记为 \\(O(\\log n)\\) :

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def logarithmic(n: int) -> int:\n    \"\"\"对数阶(循环实现)\"\"\"\n    count = 0\n    while n > 1:\n        n = n / 2\n        count += 1\n    return count\n
time_complexity.cpp
/* 对数阶(循环实现) */\nint logarithmic(int n) {\n    int count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
time_complexity.java
/* 对数阶(循环实现) */\nint logarithmic(int n) {\n    int count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
time_complexity.cs
/* 对数阶(循环实现) */\nint Logarithmic(int n) {\n    int count = 0;\n    while (n > 1) {\n        n /= 2;\n        count++;\n    }\n    return count;\n}\n
time_complexity.go
/* 对数阶(循环实现)*/\nfunc logarithmic(n int) int {\n    count := 0\n    for n > 1 {\n        n = n / 2\n        count++\n    }\n    return count\n}\n
time_complexity.swift
/* 对数阶(循环实现) */\nfunc logarithmic(n: Int) -> Int {\n    var count = 0\n    var n = n\n    while n > 1 {\n        n = n / 2\n        count += 1\n    }\n    return count\n}\n
time_complexity.js
/* 对数阶(循环实现) */\nfunction logarithmic(n) {\n    let count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
time_complexity.ts
/* 对数阶(循环实现) */\nfunction logarithmic(n: number): number {\n    let count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
time_complexity.dart
/* 对数阶(循环实现) */\nint logarithmic(int n) {\n  int count = 0;\n  while (n > 1) {\n    n = n ~/ 2;\n    count++;\n  }\n  return count;\n}\n
time_complexity.rs
/* 对数阶(循环实现) */\nfn logarithmic(mut n: i32) -> i32 {\n    let mut count = 0;\n    while n > 1 {\n        n = n / 2;\n        count += 1;\n    }\n    count\n}\n
time_complexity.c
/* 对数阶(循环实现) */\nint logarithmic(int n) {\n    int count = 0;\n    while (n > 1) {\n        n = n / 2;\n        count++;\n    }\n    return count;\n}\n
time_complexity.kt
/* 对数阶(循环实现) */\nfun logarithmic(n: Int): Int {\n    var n1 = n\n    var count = 0\n    while (n1 > 1) {\n        n1 /= 2\n        count++\n    }\n    return count\n}\n
time_complexity.rb
### 对数阶(循环实现)###\ndef logarithmic(n)\n  count = 0\n\n  while n > 1\n    n /= 2\n    count += 1\n  end\n\n  count\nend\n
可视化运行

全屏观看 >

图 2-12   对数阶的时间复杂度

与指数阶类似,对数阶也常出现于递归函数中。以下代码形成了一棵高度为 \\(\\log_2 n\\) 的递归树:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def log_recur(n: int) -> int:\n    \"\"\"对数阶(递归实现)\"\"\"\n    if n <= 1:\n        return 0\n    return log_recur(n / 2) + 1\n
time_complexity.cpp
/* 对数阶(递归实现) */\nint logRecur(int n) {\n    if (n <= 1)\n        return 0;\n    return logRecur(n / 2) + 1;\n}\n
time_complexity.java
/* 对数阶(递归实现) */\nint logRecur(int n) {\n    if (n <= 1)\n        return 0;\n    return logRecur(n / 2) + 1;\n}\n
time_complexity.cs
/* 对数阶(递归实现) */\nint LogRecur(int n) {\n    if (n <= 1) return 0;\n    return LogRecur(n / 2) + 1;\n}\n
time_complexity.go
/* 对数阶(递归实现)*/\nfunc logRecur(n int) int {\n    if n <= 1 {\n        return 0\n    }\n    return logRecur(n/2) + 1\n}\n
time_complexity.swift
/* 对数阶(递归实现) */\nfunc logRecur(n: Int) -> Int {\n    if n <= 1 {\n        return 0\n    }\n    return logRecur(n: n / 2) + 1\n}\n
time_complexity.js
/* 对数阶(递归实现) */\nfunction logRecur(n) {\n    if (n <= 1) return 0;\n    return logRecur(n / 2) + 1;\n}\n
time_complexity.ts
/* 对数阶(递归实现) */\nfunction logRecur(n: number): number {\n    if (n <= 1) return 0;\n    return logRecur(n / 2) + 1;\n}\n
time_complexity.dart
/* 对数阶(递归实现) */\nint logRecur(int n) {\n  if (n <= 1) return 0;\n  return logRecur(n ~/ 2) + 1;\n}\n
time_complexity.rs
/* 对数阶(递归实现) */\nfn log_recur(n: i32) -> i32 {\n    if n <= 1 {\n        return 0;\n    }\n    log_recur(n / 2) + 1\n}\n
time_complexity.c
/* 对数阶(递归实现) */\nint logRecur(int n) {\n    if (n <= 1)\n        return 0;\n    return logRecur(n / 2) + 1;\n}\n
time_complexity.kt
/* 对数阶(递归实现) */\nfun logRecur(n: Int): Int {\n    if (n <= 1)\n        return 0\n    return logRecur(n / 2) + 1\n}\n
time_complexity.rb
### 对数阶(递归实现)###\ndef log_recur(n)\n  return 0 unless n > 1\n  log_recur(n / 2) + 1\nend\n
可视化运行

全屏观看 >

对数阶常出现于基于分治策略的算法中,体现了“一分为多”和“化繁为简”的算法思想。它增长缓慢,是仅次于常数阶的理想的时间复杂度。

\\(O(\\log n)\\) 的底数是多少?

准确来说,“一分为 \\(m\\)”对应的时间复杂度是 \\(O(\\log_m n)\\) 。而通过对数换底公式,我们可以得到具有不同底数、相等的时间复杂度:

\\[ O(\\log_m n) = O(\\log_k n / \\log_k m) = O(\\log_k n) \\]

也就是说,底数 \\(m\\) 可以在不影响复杂度的前提下转换。因此我们通常会省略底数 \\(m\\) ,将对数阶直接记为 \\(O(\\log n)\\) 。

","path":["第 2 章   复杂度分析","2.3   时间复杂度"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#6-on-log-n","level":3,"title":"6.   线性对数阶 \\(O(n \\log n)\\)","text":"

线性对数阶常出现于嵌套循环中,两层循环的时间复杂度分别为 \\(O(\\log n)\\) 和 \\(O(n)\\) 。相关代码如下:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def linear_log_recur(n: int) -> int:\n    \"\"\"线性对数阶\"\"\"\n    if n <= 1:\n        return 1\n    # 一分为二,子问题的规模减小一半\n    count = linear_log_recur(n // 2) + linear_log_recur(n // 2)\n    # 当前子问题包含 n 个操作\n    for _ in range(n):\n        count += 1\n    return count\n
time_complexity.cpp
/* 线性对数阶 */\nint linearLogRecur(int n) {\n    if (n <= 1)\n        return 1;\n    int count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.java
/* 线性对数阶 */\nint linearLogRecur(int n) {\n    if (n <= 1)\n        return 1;\n    int count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.cs
/* 线性对数阶 */\nint LinearLogRecur(int n) {\n    if (n <= 1) return 1;\n    int count = LinearLogRecur(n / 2) + LinearLogRecur(n / 2);\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.go
/* 线性对数阶 */\nfunc linearLogRecur(n int) int {\n    if n <= 1 {\n        return 1\n    }\n    count := linearLogRecur(n/2) + linearLogRecur(n/2)\n    for i := 0; i < n; i++ {\n        count++\n    }\n    return count\n}\n
time_complexity.swift
/* 线性对数阶 */\nfunc linearLogRecur(n: Int) -> Int {\n    if n <= 1 {\n        return 1\n    }\n    var count = linearLogRecur(n: n / 2) + linearLogRecur(n: n / 2)\n    for _ in stride(from: 0, to: n, by: 1) {\n        count += 1\n    }\n    return count\n}\n
time_complexity.js
/* 线性对数阶 */\nfunction linearLogRecur(n) {\n    if (n <= 1) return 1;\n    let count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (let i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.ts
/* 线性对数阶 */\nfunction linearLogRecur(n: number): number {\n    if (n <= 1) return 1;\n    let count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (let i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.dart
/* 线性对数阶 */\nint linearLogRecur(int n) {\n  if (n <= 1) return 1;\n  int count = linearLogRecur(n ~/ 2) + linearLogRecur(n ~/ 2);\n  for (var i = 0; i < n; i++) {\n    count++;\n  }\n  return count;\n}\n
time_complexity.rs
/* 线性对数阶 */\nfn linear_log_recur(n: i32) -> i32 {\n    if n <= 1 {\n        return 1;\n    }\n    let mut count = linear_log_recur(n / 2) + linear_log_recur(n / 2);\n    for _ in 0..n {\n        count += 1;\n    }\n    return count;\n}\n
time_complexity.c
/* 线性对数阶 */\nint linearLogRecur(int n) {\n    if (n <= 1)\n        return 1;\n    int count = linearLogRecur(n / 2) + linearLogRecur(n / 2);\n    for (int i = 0; i < n; i++) {\n        count++;\n    }\n    return count;\n}\n
time_complexity.kt
/* 线性对数阶 */\nfun linearLogRecur(n: Int): Int {\n    if (n <= 1)\n        return 1\n    var count = linearLogRecur(n / 2) + linearLogRecur(n / 2)\n    for (i in 0..<n) {\n        count++\n    }\n    return count\n}\n
time_complexity.rb
### 线性对数阶 ###\ndef linear_log_recur(n)\n  return 1 unless n > 1\n\n  count = linear_log_recur(n / 2) + linear_log_recur(n / 2)\n  (0...n).each { count += 1 }\n\n  count\nend\n
可视化运行

全屏观看 >

图 2-13 展示了线性对数阶的生成方式。二叉树的每一层的操作总数都为 \\(n\\) ,树共有 \\(\\log_2 n + 1\\) 层,因此时间复杂度为 \\(O(n \\log n)\\) 。

图 2-13   线性对数阶的时间复杂度

主流排序算法的时间复杂度通常为 \\(O(n \\log n)\\) ,例如快速排序、归并排序、堆排序等。

","path":["第 2 章   复杂度分析","2.3   时间复杂度"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#7-on","level":3,"title":"7.   阶乘阶 \\(O(n!)\\)","text":"

阶乘阶对应数学上的“全排列”问题。给定 \\(n\\) 个互不重复的元素,求其所有可能的排列方案,方案数量为:

\\[ n! = n \\times (n - 1) \\times (n - 2) \\times \\dots \\times 2 \\times 1 \\]

阶乘通常使用递归实现。如图 2-14 和以下代码所示,第一层分裂出 \\(n\\) 个,第二层分裂出 \\(n - 1\\) 个,以此类推,直至第 \\(n\\) 层时停止分裂:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby time_complexity.py
def factorial_recur(n: int) -> int:\n    \"\"\"阶乘阶(递归实现)\"\"\"\n    if n == 0:\n        return 1\n    count = 0\n    # 从 1 个分裂出 n 个\n    for _ in range(n):\n        count += factorial_recur(n - 1)\n    return count\n
time_complexity.cpp
/* 阶乘阶(递归实现) */\nint factorialRecur(int n) {\n    if (n == 0)\n        return 1;\n    int count = 0;\n    // 从 1 个分裂出 n 个\n    for (int i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
time_complexity.java
/* 阶乘阶(递归实现) */\nint factorialRecur(int n) {\n    if (n == 0)\n        return 1;\n    int count = 0;\n    // 从 1 个分裂出 n 个\n    for (int i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
time_complexity.cs
/* 阶乘阶(递归实现) */\nint FactorialRecur(int n) {\n    if (n == 0) return 1;\n    int count = 0;\n    // 从 1 个分裂出 n 个\n    for (int i = 0; i < n; i++) {\n        count += FactorialRecur(n - 1);\n    }\n    return count;\n}\n
time_complexity.go
/* 阶乘阶(递归实现) */\nfunc factorialRecur(n int) int {\n    if n == 0 {\n        return 1\n    }\n    count := 0\n    // 从 1 个分裂出 n 个\n    for i := 0; i < n; i++ {\n        count += factorialRecur(n - 1)\n    }\n    return count\n}\n
time_complexity.swift
/* 阶乘阶(递归实现) */\nfunc factorialRecur(n: Int) -> Int {\n    if n == 0 {\n        return 1\n    }\n    var count = 0\n    // 从 1 个分裂出 n 个\n    for _ in 0 ..< n {\n        count += factorialRecur(n: n - 1)\n    }\n    return count\n}\n
time_complexity.js
/* 阶乘阶(递归实现) */\nfunction factorialRecur(n) {\n    if (n === 0) return 1;\n    let count = 0;\n    // 从 1 个分裂出 n 个\n    for (let i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
time_complexity.ts
/* 阶乘阶(递归实现) */\nfunction factorialRecur(n: number): number {\n    if (n === 0) return 1;\n    let count = 0;\n    // 从 1 个分裂出 n 个\n    for (let i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
time_complexity.dart
/* 阶乘阶(递归实现) */\nint factorialRecur(int n) {\n  if (n == 0) return 1;\n  int count = 0;\n  // 从 1 个分裂出 n 个\n  for (var i = 0; i < n; i++) {\n    count += factorialRecur(n - 1);\n  }\n  return count;\n}\n
time_complexity.rs
/* 阶乘阶(递归实现) */\nfn factorial_recur(n: i32) -> i32 {\n    if n == 0 {\n        return 1;\n    }\n    let mut count = 0;\n    // 从 1 个分裂出 n 个\n    for _ in 0..n {\n        count += factorial_recur(n - 1);\n    }\n    count\n}\n
time_complexity.c
/* 阶乘阶(递归实现) */\nint factorialRecur(int n) {\n    if (n == 0)\n        return 1;\n    int count = 0;\n    for (int i = 0; i < n; i++) {\n        count += factorialRecur(n - 1);\n    }\n    return count;\n}\n
time_complexity.kt
/* 阶乘阶(递归实现) */\nfun factorialRecur(n: Int): Int {\n    if (n == 0)\n        return 1\n    var count = 0\n    // 从 1 个分裂出 n 个\n    for (i in 0..<n) {\n        count += factorialRecur(n - 1)\n    }\n    return count\n}\n
time_complexity.rb
### 阶乘阶(递归实现)###\ndef factorial_recur(n)\n  return 1 if n == 0\n\n  count = 0\n  # 从 1 个分裂出 n 个\n  (0...n).each { count += factorial_recur(n - 1) }\n\n  count\nend\n
可视化运行

全屏观看 >

图 2-14   阶乘阶的时间复杂度

请注意,因为当 \\(n \\geq 4\\) 时恒有 \\(n! > 2^n\\) ,所以阶乘阶比指数阶增长得更快,在 \\(n\\) 较大时也是不可接受的。

","path":["第 2 章   复杂度分析","2.3   时间复杂度"],"tags":[]},{"location":"chapter_computational_complexity/time_complexity/#235","level":2,"title":"2.3.5   最差、最佳、平均时间复杂度","text":"

算法的时间效率往往不是固定的,而是与输入数据的分布有关。假设输入一个长度为 \\(n\\) 的数组 nums ,其中 nums 由从 \\(1\\) 至 \\(n\\) 的数字组成,每个数字只出现一次;但元素顺序是随机打乱的,任务目标是返回元素 \\(1\\) 的索引。我们可以得出以下结论。

“最差时间复杂度”对应函数渐近上界,使用大 \\(O\\) 记号表示。相应地,“最佳时间复杂度”对应函数渐近下界,用 \\(\\Omega\\) 记号表示:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby worst_best_time_complexity.py
def random_numbers(n: int) -> list[int]:\n    \"\"\"生成一个数组,元素为: 1, 2, ..., n ,顺序被打乱\"\"\"\n    # 生成数组 nums =: 1, 2, 3, ..., n\n    nums = [i for i in range(1, n + 1)]\n    # 随机打乱数组元素\n    random.shuffle(nums)\n    return nums\n\ndef find_one(nums: list[int]) -> int:\n    \"\"\"查找数组 nums 中数字 1 所在索引\"\"\"\n    for i in range(len(nums)):\n        # 当元素 1 在数组头部时,达到最佳时间复杂度 O(1)\n        # 当元素 1 在数组尾部时,达到最差时间复杂度 O(n)\n        if nums[i] == 1:\n            return i\n    return -1\n
worst_best_time_complexity.cpp
/* 生成一个数组,元素为 { 1, 2, ..., n },顺序被打乱 */\nvector<int> randomNumbers(int n) {\n    vector<int> nums(n);\n    // 生成数组 nums = { 1, 2, 3, ..., n }\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // 使用系统时间生成随机种子\n    unsigned seed = chrono::system_clock::now().time_since_epoch().count();\n    // 随机打乱数组元素\n    shuffle(nums.begin(), nums.end(), default_random_engine(seed));\n    return nums;\n}\n\n/* 查找数组 nums 中数字 1 所在索引 */\nint findOne(vector<int> &nums) {\n    for (int i = 0; i < nums.size(); i++) {\n        // 当元素 1 在数组头部时,达到最佳时间复杂度 O(1)\n        // 当元素 1 在数组尾部时,达到最差时间复杂度 O(n)\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
worst_best_time_complexity.java
/* 生成一个数组,元素为 { 1, 2, ..., n },顺序被打乱 */\nint[] randomNumbers(int n) {\n    Integer[] nums = new Integer[n];\n    // 生成数组 nums = { 1, 2, 3, ..., n }\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // 随机打乱数组元素\n    Collections.shuffle(Arrays.asList(nums));\n    // Integer[] -> int[]\n    int[] res = new int[n];\n    for (int i = 0; i < n; i++) {\n        res[i] = nums[i];\n    }\n    return res;\n}\n\n/* 查找数组 nums 中数字 1 所在索引 */\nint findOne(int[] nums) {\n    for (int i = 0; i < nums.length; i++) {\n        // 当元素 1 在数组头部时,达到最佳时间复杂度 O(1)\n        // 当元素 1 在数组尾部时,达到最差时间复杂度 O(n)\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
worst_best_time_complexity.cs
/* 生成一个数组,元素为 { 1, 2, ..., n },顺序被打乱 */\nint[] RandomNumbers(int n) {\n    int[] nums = new int[n];\n    // 生成数组 nums = { 1, 2, 3, ..., n }\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n\n    // 随机打乱数组元素\n    for (int i = 0; i < nums.Length; i++) {\n        int index = new Random().Next(i, nums.Length);\n        (nums[i], nums[index]) = (nums[index], nums[i]);\n    }\n    return nums;\n}\n\n/* 查找数组 nums 中数字 1 所在索引 */\nint FindOne(int[] nums) {\n    for (int i = 0; i < nums.Length; i++) {\n        // 当元素 1 在数组头部时,达到最佳时间复杂度 O(1)\n        // 当元素 1 在数组尾部时,达到最差时间复杂度 O(n)\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
worst_best_time_complexity.go
/* 生成一个数组,元素为 { 1, 2, ..., n },顺序被打乱 */\nfunc randomNumbers(n int) []int {\n    nums := make([]int, n)\n    // 生成数组 nums = { 1, 2, 3, ..., n }\n    for i := 0; i < n; i++ {\n        nums[i] = i + 1\n    }\n    // 随机打乱数组元素\n    rand.Shuffle(len(nums), func(i, j int) {\n        nums[i], nums[j] = nums[j], nums[i]\n    })\n    return nums\n}\n\n/* 查找数组 nums 中数字 1 所在索引 */\nfunc findOne(nums []int) int {\n    for i := 0; i < len(nums); i++ {\n        // 当元素 1 在数组头部时,达到最佳时间复杂度 O(1)\n        // 当元素 1 在数组尾部时,达到最差时间复杂度 O(n)\n        if nums[i] == 1 {\n            return i\n        }\n    }\n    return -1\n}\n
worst_best_time_complexity.swift
/* 生成一个数组,元素为 { 1, 2, ..., n },顺序被打乱 */\nfunc randomNumbers(n: Int) -> [Int] {\n    // 生成数组 nums = { 1, 2, 3, ..., n }\n    var nums = Array(1 ... n)\n    // 随机打乱数组元素\n    nums.shuffle()\n    return nums\n}\n\n/* 查找数组 nums 中数字 1 所在索引 */\nfunc findOne(nums: [Int]) -> Int {\n    for i in nums.indices {\n        // 当元素 1 在数组头部时,达到最佳时间复杂度 O(1)\n        // 当元素 1 在数组尾部时,达到最差时间复杂度 O(n)\n        if nums[i] == 1 {\n            return i\n        }\n    }\n    return -1\n}\n
worst_best_time_complexity.js
/* 生成一个数组,元素为 { 1, 2, ..., n },顺序被打乱 */\nfunction randomNumbers(n) {\n    const nums = Array(n);\n    // 生成数组 nums = { 1, 2, 3, ..., n }\n    for (let i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // 随机打乱数组元素\n    for (let i = 0; i < n; i++) {\n        const r = Math.floor(Math.random() * (i + 1));\n        const temp = nums[i];\n        nums[i] = nums[r];\n        nums[r] = temp;\n    }\n    return nums;\n}\n\n/* 查找数组 nums 中数字 1 所在索引 */\nfunction findOne(nums) {\n    for (let i = 0; i < nums.length; i++) {\n        // 当元素 1 在数组头部时,达到最佳时间复杂度 O(1)\n        // 当元素 1 在数组尾部时,达到最差时间复杂度 O(n)\n        if (nums[i] === 1) {\n            return i;\n        }\n    }\n    return -1;\n}\n
worst_best_time_complexity.ts
/* 生成一个数组,元素为 { 1, 2, ..., n },顺序被打乱 */\nfunction randomNumbers(n: number): number[] {\n    const nums = Array(n);\n    // 生成数组 nums = { 1, 2, 3, ..., n }\n    for (let i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // 随机打乱数组元素\n    for (let i = 0; i < n; i++) {\n        const r = Math.floor(Math.random() * (i + 1));\n        const temp = nums[i];\n        nums[i] = nums[r];\n        nums[r] = temp;\n    }\n    return nums;\n}\n\n/* 查找数组 nums 中数字 1 所在索引 */\nfunction findOne(nums: number[]): number {\n    for (let i = 0; i < nums.length; i++) {\n        // 当元素 1 在数组头部时,达到最佳时间复杂度 O(1)\n        // 当元素 1 在数组尾部时,达到最差时间复杂度 O(n)\n        if (nums[i] === 1) {\n            return i;\n        }\n    }\n    return -1;\n}\n
worst_best_time_complexity.dart
/* 生成一个数组,元素为 { 1, 2, ..., n },顺序被打乱 */\nList<int> randomNumbers(int n) {\n  final nums = List.filled(n, 0);\n  // 生成数组 nums = { 1, 2, 3, ..., n }\n  for (var i = 0; i < n; i++) {\n    nums[i] = i + 1;\n  }\n  // 随机打乱数组元素\n  nums.shuffle();\n\n  return nums;\n}\n\n/* 查找数组 nums 中数字 1 所在索引 */\nint findOne(List<int> nums) {\n  for (var i = 0; i < nums.length; i++) {\n    // 当元素 1 在数组头部时,达到最佳时间复杂度 O(1)\n    // 当元素 1 在数组尾部时,达到最差时间复杂度 O(n)\n    if (nums[i] == 1) return i;\n  }\n\n  return -1;\n}\n
worst_best_time_complexity.rs
/* 生成一个数组,元素为 { 1, 2, ..., n },顺序被打乱 */\nfn random_numbers(n: i32) -> Vec<i32> {\n    // 生成数组 nums = { 1, 2, 3, ..., n }\n    let mut nums = (1..=n).collect::<Vec<i32>>();\n    // 随机打乱数组元素\n    nums.shuffle(&mut thread_rng());\n    nums\n}\n\n/* 查找数组 nums 中数字 1 所在索引 */\nfn find_one(nums: &[i32]) -> Option<usize> {\n    for i in 0..nums.len() {\n        // 当元素 1 在数组头部时,达到最佳时间复杂度 O(1)\n        // 当元素 1 在数组尾部时,达到最差时间复杂度 O(n)\n        if nums[i] == 1 {\n            return Some(i);\n        }\n    }\n    None\n}\n
worst_best_time_complexity.c
/* 生成一个数组,元素为 { 1, 2, ..., n },顺序被打乱 */\nint *randomNumbers(int n) {\n    // 分配堆区内存(创建一维可变长数组:数组中元素数量为 n ,元素类型为 int )\n    int *nums = (int *)malloc(n * sizeof(int));\n    // 生成数组 nums = { 1, 2, 3, ..., n }\n    for (int i = 0; i < n; i++) {\n        nums[i] = i + 1;\n    }\n    // 随机打乱数组元素\n    for (int i = n - 1; i > 0; i--) {\n        int j = rand() % (i + 1);\n        int temp = nums[i];\n        nums[i] = nums[j];\n        nums[j] = temp;\n    }\n    return nums;\n}\n\n/* 查找数组 nums 中数字 1 所在索引 */\nint findOne(int *nums, int n) {\n    for (int i = 0; i < n; i++) {\n        // 当元素 1 在数组头部时,达到最佳时间复杂度 O(1)\n        // 当元素 1 在数组尾部时,达到最差时间复杂度 O(n)\n        if (nums[i] == 1)\n            return i;\n    }\n    return -1;\n}\n
worst_best_time_complexity.kt
/* 生成一个数组,元素为 { 1, 2, ..., n },顺序被打乱 */\nfun randomNumbers(n: Int): Array<Int?> {\n    val nums = IntArray(n)\n    // 生成数组 nums = { 1, 2, 3, ..., n }\n    for (i in 0..<n) {\n        nums[i] = i + 1\n    }\n    // 随机打乱数组元素\n    nums.shuffle()\n    val res = arrayOfNulls<Int>(n)\n    for (i in 0..<n) {\n        res[i] = nums[i]\n    }\n    return res\n}\n\n/* 查找数组 nums 中数字 1 所在索引 */\nfun findOne(nums: Array<Int?>): Int {\n    for (i in nums.indices) {\n        // 当元素 1 在数组头部时,达到最佳时间复杂度 O(1)\n        // 当元素 1 在数组尾部时,达到最差时间复杂度 O(n)\n        if (nums[i] == 1)\n            return i\n    }\n    return -1\n}\n
worst_best_time_complexity.rb
### 生成一个数组,元素为: 1, 2, ..., n ,顺序被打乱 ###\ndef random_numbers(n)\n  # 生成数组 nums =: 1, 2, 3, ..., n\n  nums = Array.new(n) { |i| i + 1 }\n  # 随机打乱数组元素\n  nums.shuffle!\nend\n\n### 查找数组 nums 中数字 1 所在索引 ###\ndef find_one(nums)\n  for i in 0...nums.length\n    # 当元素 1 在数组头部时,达到最佳时间复杂度 O(1)\n    # 当元素 1 在数组尾部时,达到最差时间复杂度 O(n)\n    return i if nums[i] == 1\n  end\n\n  -1\nend\n
可视化运行

全屏观看 >

值得说明的是,我们在实际中很少使用最佳时间复杂度,因为通常只有在很小概率下才能达到,可能会带来一定的误导性。而最差时间复杂度更为实用,因为它给出了一个效率安全值,让我们可以放心地使用算法。

从上述示例可以看出,最差时间复杂度和最佳时间复杂度只出现于“特殊的数据分布”,这些情况的出现概率可能很小,并不能真实地反映算法运行效率。相比之下,平均时间复杂度可以体现算法在随机输入数据下的运行效率,用 \\(\\Theta\\) 记号来表示。

对于部分算法,我们可以简单地推算出随机数据分布下的平均情况。比如上述示例,由于输入数组是被打乱的,因此元素 \\(1\\) 出现在任意索引的概率都是相等的,那么算法的平均循环次数就是数组长度的一半 \\(n / 2\\) ,平均时间复杂度为 \\(\\Theta(n / 2) = \\Theta(n)\\) 。

但对于较为复杂的算法,计算平均时间复杂度往往比较困难,因为很难分析出在数据分布下的整体数学期望。在这种情况下,我们通常使用最差时间复杂度作为算法效率的评判标准。

为什么很少看到 \\(\\Theta\\) 符号?

可能由于 \\(O\\) 符号过于朗朗上口,因此我们常常使用它来表示平均时间复杂度。但从严格意义上讲,这种做法并不规范。在本书和其他资料中,若遇到类似“平均时间复杂度 \\(O(n)\\)”的表述,请将其直接理解为 \\(\\Theta(n)\\) 。

","path":["第 2 章   复杂度分析","2.3   时间复杂度"],"tags":[]},{"location":"chapter_data_structure/","level":1,"title":"第 3 章   数据结构","text":"

Abstract

数据结构如同一副稳固而多样的框架。

它为数据的有序组织提供了蓝图,算法得以在此基础上生动起来。

","path":["第 3 章   数据结构"],"tags":[]},{"location":"chapter_data_structure/#_1","level":2,"title":"本章内容","text":"","path":["第 3 章   数据结构"],"tags":[]},{"location":"chapter_data_structure/basic_data_types/","level":1,"title":"3.2   基本数据类型","text":"

当谈及计算机中的数据时,我们会想到文本、图片、视频、语音、3D 模型等各种形式。尽管这些数据的组织形式各异,但它们都由各种基本数据类型构成。

基本数据类型是 CPU 可以直接进行运算的类型,在算法中直接被使用,主要包括以下几种。

基本数据类型以二进制的形式存储在计算机中。一个二进制位即为 \\(1\\) 比特。在绝大多数现代操作系统中,\\(1\\) 字节(byte)由 \\(8\\) 比特(bit)组成。

基本数据类型的取值范围取决于其占用的空间大小。下面以 Java 为例。

表 3-1 列举了 Java 中各种基本数据类型的占用空间、取值范围和默认值。此表格无须死记硬背,大致理解即可,需要时可以通过查表来回忆。

表 3-1   基本数据类型的占用空间和取值范围

类型 符号 占用空间 最小值 最大值 默认值 整数 byte 1 字节 \\(-2^7\\) (\\(-128\\)) \\(2^7 - 1\\) (\\(127\\)) \\(0\\) short 2 字节 \\(-2^{15}\\) \\(2^{15} - 1\\) \\(0\\) int 4 字节 \\(-2^{31}\\) \\(2^{31} - 1\\) \\(0\\) long 8 字节 \\(-2^{63}\\) \\(2^{63} - 1\\) \\(0\\) 浮点数 float 4 字节 \\(1.175 \\times 10^{-38}\\) \\(3.403 \\times 10^{38}\\) \\(0.0\\text{f}\\) double 8 字节 \\(2.225 \\times 10^{-308}\\) \\(1.798 \\times 10^{308}\\) \\(0.0\\) 字符 char 2 字节 \\(0\\) \\(2^{16} - 1\\) \\(0\\) 布尔 bool 1 字节 \\(\\text{false}\\) \\(\\text{true}\\) \\(\\text{false}\\)

请注意,表 3-1 针对的是 Java 的基本数据类型的情况。每种编程语言都有各自的数据类型定义,它们的占用空间、取值范围和默认值可能会有所不同。

那么,基本数据类型与数据结构之间有什么联系呢?我们知道,数据结构是在计算机中组织与存储数据的方式。这句话的主语是“结构”而非“数据”。

如果想表示“一排数字”,我们自然会想到使用数组。这是因为数组的线性结构可以表示数字的相邻关系和顺序关系,但至于存储的内容是整数 int、小数 float 还是字符 char ,则与“数据结构”无关。

换句话说,基本数据类型提供了数据的“内容类型”,而数据结构提供了数据的“组织方式”。例如以下代码,我们用相同的数据结构(数组)来存储与表示不同的基本数据类型,包括 intfloatcharbool 等。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
# 使用多种基本数据类型来初始化数组\nnumbers: list[int] = [0] * 5\ndecimals: list[float] = [0.0] * 5\n# Python 的字符实际上是长度为 1 的字符串\ncharacters: list[str] = ['0'] * 5\nbools: list[bool] = [False] * 5\n# Python 的列表可以自由存储各种基本数据类型和对象引用\ndata = [0, 0.0, 'a', False, ListNode(0)]\n
// 使用多种基本数据类型来初始化数组\nint numbers[5];\nfloat decimals[5];\nchar characters[5];\nbool bools[5];\n
// 使用多种基本数据类型来初始化数组\nint[] numbers = new int[5];\nfloat[] decimals = new float[5];\nchar[] characters = new char[5];\nboolean[] bools = new boolean[5];\n
// 使用多种基本数据类型来初始化数组\nint[] numbers = new int[5];\nfloat[] decimals = new float[5];\nchar[] characters = new char[5];\nbool[] bools = new bool[5];\n
// 使用多种基本数据类型来初始化数组\nvar numbers = [5]int{}\nvar decimals = [5]float64{}\nvar characters = [5]byte{}\nvar bools = [5]bool{}\n
// 使用多种基本数据类型来初始化数组\nlet numbers = Array(repeating: 0, count: 5)\nlet decimals = Array(repeating: 0.0, count: 5)\nlet characters: [Character] = Array(repeating: \"a\", count: 5)\nlet bools = Array(repeating: false, count: 5)\n
// JavaScript 的数组可以自由存储各种基本数据类型和对象\nconst array = [0, 0.0, 'a', false];\n
// 使用多种基本数据类型来初始化数组\nconst numbers: number[] = [];\nconst characters: string[] = [];\nconst bools: boolean[] = [];\n
// 使用多种基本数据类型来初始化数组\nList<int> numbers = List.filled(5, 0);\nList<double> decimals = List.filled(5, 0.0);\nList<String> characters = List.filled(5, 'a');\nList<bool> bools = List.filled(5, false);\n
// 使用多种基本数据类型来初始化数组\nlet numbers: Vec<i32> = vec![0; 5];\nlet decimals: Vec<f32> = vec![0.0; 5];\nlet characters: Vec<char> = vec!['0'; 5];\nlet bools: Vec<bool> = vec![false; 5];\n
// 使用多种基本数据类型来初始化数组\nint numbers[10];\nfloat decimals[10];\nchar characters[10];\nbool bools[10];\n
// 使用多种基本数据类型来初始化数组\nval numbers = IntArray(5)\nval decinals = FloatArray(5)\nval characters = CharArray(5)\nval bools = BooleanArray(5)\n
# Ruby 的列表可以自由存储各种基本数据类型和对象引用\ndata = [0, 0.0, 'a', false, ListNode(0)]\n
可视化运行

全屏观看 >

","path":["第 3 章   数据结构","3.2   基本数据类型"],"tags":[]},{"location":"chapter_data_structure/character_encoding/","level":1,"title":"3.4   字符编码 *","text":"

在计算机中,所有数据都是以二进制数的形式存储的,字符 char 也不例外。为了表示字符,我们需要建立一套“字符集”,规定每个字符和二进制数之间的一一对应关系。有了字符集之后,计算机就可以通过查表完成二进制数到字符的转换。

","path":["第 3 章   数据结构","3.4   字符编码 *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#341-ascii","level":2,"title":"3.4.1   ASCII 字符集","text":"

ASCII 码是最早出现的字符集,其全称为 American Standard Code for Information Interchange(美国标准信息交换代码)。它使用 7 位二进制数(一个字节的低 7 位)表示一个字符,最多能够表示 128 个不同的字符。如图 3-6 所示,ASCII 码包括英文字母的大小写、数字 0 ~ 9、一些标点符号,以及一些控制字符(如换行符和制表符)。

图 3-6   ASCII 码

然而,ASCII 码仅能够表示英文。随着计算机的全球化,诞生了一种能够表示更多语言的 EASCII 字符集。它在 ASCII 的 7 位基础上扩展到 8 位,能够表示 256 个不同的字符。

在世界范围内,陆续出现了一批适用于不同地区的 EASCII 字符集。这些字符集的前 128 个字符统一为 ASCII 码,后 128 个字符定义不同,以适应不同语言的需求。

","path":["第 3 章   数据结构","3.4   字符编码 *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#342-gbk","level":2,"title":"3.4.2   GBK 字符集","text":"

后来人们发现,EASCII 码仍然无法满足许多语言的字符数量要求。比如汉字有近十万个,光日常使用的就有几千个。中国国家标准总局于 1980 年发布了 GB2312 字符集,其收录了 6763 个汉字,基本满足了汉字的计算机处理需要。

然而,GB2312 无法处理部分罕见字和繁体字。GBK 字符集是在 GB2312 的基础上扩展得到的,它共收录了 21886 个汉字。在 GBK 的编码方案中,ASCII 字符使用一个字节表示,汉字使用两个字节表示。

","path":["第 3 章   数据结构","3.4   字符编码 *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#343-unicode","level":2,"title":"3.4.3   Unicode 字符集","text":"

随着计算机技术的蓬勃发展,字符集与编码标准百花齐放,而这带来了许多问题。一方面,这些字符集一般只定义了特定语言的字符,无法在多语言环境下正常工作。另一方面,同一种语言存在多种字符集标准,如果两台计算机使用的是不同的编码标准,则在信息传递时就会出现乱码。

那个时代的研究人员就在想:如果推出一个足够完整的字符集,将世界范围内的所有语言和符号都收录其中,不就可以解决跨语言环境和乱码问题了吗?在这种想法的驱动下,一个大而全的字符集 Unicode 应运而生。

Unicode 的中文名称为“统一码”,理论上能容纳 100 多万个字符。它致力于将全球范围内的字符纳入统一的字符集之中,提供一种通用的字符集来处理和显示各种语言文字,减少因为编码标准不同而产生的乱码问题。自 1991 年发布以来,Unicode 不断扩充新的语言与字符。截至 2022 年 9 月,Unicode 已经包含 149186 个字符,包括各种语言的字符、符号甚至表情符号等。

Unicode 作为一种通用字符集,本质上是给每个字符分配唯一的“码点”(字符编号),其取值范围为 U+0000 至 U+10FFFF,构成了统一的字符编号空间。然而,Unicode 并没有规定在计算机中如何存储这些字符码点。我们不禁会问:当多种长度的 Unicode 码点同时出现在一个文本中时,系统如何解析字符?例如给定一个长度为 2 字节的编码,系统如何确认它是一个 2 字节的字符还是两个 1 字节的字符?

对于以上问题,一种直接的解决方案是将所有字符存储为等长的编码。如图 3-7 所示,“Hello”中的每个字符占用 1 字节,“算法”中的每个字符占用 2 字节。我们可以通过高位填 0 将“Hello 算法”中的所有字符都编码为 2 字节长度。这样系统就可以每隔 2 字节解析一个字符,恢复这个短语的内容了。

图 3-7   Unicode 编码示例

然而 ASCII 码已经向我们证明,编码英文只需 1 字节。若采用上述方案,英文文本占用空间的大小将会是 ASCII 编码下的两倍,非常浪费内存空间。因此,我们需要一种更加高效的 Unicode 编码方法。

","path":["第 3 章   数据结构","3.4   字符编码 *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#344-utf-8","level":2,"title":"3.4.4   UTF-8 编码","text":"

目前,UTF-8 已成为国际上使用最广泛的 Unicode 编码方法。它是一种可变长度的编码,使用 1 到 4 字节来表示一个字符,根据字符的复杂性而变。ASCII 字符只需 1 字节,拉丁字母和希腊字母需要 2 字节,常用的中文字符需要 3 字节,其他的一些生僻字符需要 4 字节。

UTF-8 的编码规则并不复杂,分为以下两种情况。

图 3-8 展示了“Hello算法”对应的 UTF-8 编码。观察发现,由于最高 \\(n\\) 位都设置为 \\(1\\) ,因此系统可以通过读取最高位 \\(1\\) 的个数来解析出字符的长度为 \\(n\\) 。

但为什么要将其余所有字节的高 2 位都设置为 \\(10\\) 呢?实际上,这个 \\(10\\) 能够起到校验符的作用。假设系统从一个错误的字节开始解析文本,字节头部的 \\(10\\) 能够帮助系统快速判断出异常。

之所以将 \\(10\\) 当作校验符,是因为在 UTF-8 编码规则下,不可能有字符的最高两位是 \\(10\\) 。这个结论可以用反证法来证明:假设一个字符的最高两位是 \\(10\\) ,说明该字符的长度为 \\(1\\) ,对应 ASCII 码。而 ASCII 码的最高位应该是 \\(0\\) ,与假设矛盾。

图 3-8   UTF-8 编码示例

除了 UTF-8 之外,常见的编码方式还包括以下两种。

从存储空间占用的角度看,使用 UTF-8 表示英文字符非常高效,因为它仅需 1 字节;使用 UTF-16 编码某些非英文字符(例如中文)会更加高效,因为它仅需 2 字节,而 UTF-8 可能需要 3 字节。

从兼容性的角度看,UTF-8 的通用性最佳,许多工具和库优先支持 UTF-8 。

","path":["第 3 章   数据结构","3.4   字符编码 *"],"tags":[]},{"location":"chapter_data_structure/character_encoding/#345","level":2,"title":"3.4.5   编程语言的字符编码","text":"

对于以往的大多数编程语言,程序运行中的字符串都采用 UTF-16 或 UTF-32 这类等长编码。在等长编码下,我们可以将字符串看作数组来处理,这种做法具有以下优点。

实际上,编程语言的字符编码方案设计是一个很有趣的话题,涉及许多因素。

由于以上编程语言对字符数量的低估,它们不得不采取“代理对”的方式来表示超过 16 位长度的 Unicode 字符。这是一个不得已为之的无奈之举。一方面,包含代理对的字符串中,一个字符可能占用 2 字节或 4 字节,从而丧失了等长编码的优势。另一方面,处理代理对需要额外增加代码,这提高了编程的复杂性和调试难度。

出于以上原因,部分编程语言提出了一些不同的编码方案。

需要注意的是,以上讨论的都是字符串在编程语言中的存储方式,这和字符串如何在文件中存储或在网络中传输是不同的问题。在文件存储或网络传输中,我们通常会将字符串编码为 UTF-8 格式,以达到最优的兼容性和空间效率。

","path":["第 3 章   数据结构","3.4   字符编码 *"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/","level":1,"title":"3.1   数据结构分类","text":"

常见的数据结构包括数组、链表、栈、队列、哈希表、树、堆、图,它们可以从“逻辑结构”和“物理结构”两个维度进行分类。

","path":["第 3 章   数据结构","3.1   数据结构分类"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/#311","level":2,"title":"3.1.1   逻辑结构:线性与非线性","text":"

逻辑结构揭示了数据元素之间的逻辑关系。在数组和链表中,数据按照一定顺序排列,体现了数据之间的线性关系;而在树中,数据从顶部向下按层次排列,表现出“祖先”与“后代”之间的派生关系;图则由节点和边构成,反映了复杂的网络关系。

如图 3-1 所示,逻辑结构可分为“线性”和“非线性”两大类。线性结构比较直观,指数据在逻辑关系上呈线性排列;非线性结构则相反,呈非线性排列。

非线性数据结构可以进一步划分为树形结构和网状结构。

图 3-1   线性数据结构与非线性数据结构

","path":["第 3 章   数据结构","3.1   数据结构分类"],"tags":[]},{"location":"chapter_data_structure/classification_of_data_structure/#312","level":2,"title":"3.1.2   物理结构:连续与分散","text":"

当算法程序运行时,正在处理的数据主要存储在内存中。图 3-2 展示了一个计算机内存条,其中每个黑色方块都包含一块内存空间。我们可以将内存想象成一个巨大的 Excel 表格,其中每个单元格都可以存储一定大小的数据。

系统通过内存地址来访问目标位置的数据。如图 3-2 所示,计算机根据特定规则为表格中的每个单元格分配编号,确保每个内存空间都有唯一的内存地址。有了这些地址,程序便可以访问内存中的数据。

图 3-2   内存条、内存空间、内存地址

Tip

值得说明的是,将内存比作 Excel 表格是一个简化的类比,实际内存的工作机制比较复杂,涉及地址空间、内存管理、缓存机制、虚拟内存和物理内存等概念。

内存是所有程序的共享资源,当某块内存被某个程序占用时,则通常无法被其他程序同时使用了。因此在数据结构与算法的设计中,内存资源是一个重要的考虑因素。比如,算法所占用的内存峰值不应超过系统剩余空闲内存;如果缺少连续大块的内存空间,那么所选用的数据结构必须能够存储在分散的内存空间内。

如图 3-3 所示,物理结构反映了数据在计算机内存中的存储方式,可分为连续空间存储(数组)和分散空间存储(链表)。物理结构从底层决定了数据的访问、更新、增删等操作方法,两种物理结构在时间效率和空间效率方面呈现出互补的特点。

图 3-3   连续空间存储与分散空间存储

值得说明的是,所有数据结构都是基于数组、链表或二者的组合实现的。例如,栈和队列既可以使用数组实现,也可以使用链表实现;而哈希表的实现可能同时包含数组和链表。

链表在初始化后,仍可以在程序运行过程中对其长度进行调整,因此也称“动态数据结构”。数组在初始化后长度不可变,因此也称“静态数据结构”。值得注意的是,数组可通过重新分配内存实现长度变化,从而具备一定的“动态性”。

Tip

如果你感觉物理结构理解起来有困难,建议先阅读下一章,然后再回顾本节内容。

","path":["第 3 章   数据结构","3.1   数据结构分类"],"tags":[]},{"location":"chapter_data_structure/number_encoding/","level":1,"title":"3.3   数字编码 *","text":"

Tip

在本书中,标题带有 * 符号的是选读章节。如果你时间有限或感到理解困难,可以先跳过,等学完必读章节后再单独攻克。

","path":["第 3 章   数据结构","3.3   数字编码 *"],"tags":[]},{"location":"chapter_data_structure/number_encoding/#331","level":2,"title":"3.3.1   原码、反码和补码","text":"

在上一节的表格中我们发现,所有整数类型能够表示的负数都比正数多一个,例如 byte 的取值范围是 \\([-128, 127]\\) 。这个现象比较反直觉,它的内在原因涉及原码、反码、补码的相关知识。

首先需要指出,数字是以“补码”的形式存储在计算机中的。在分析这样做的原因之前,首先给出三者的定义。

图 3-4 展示了原码、反码和补码之间的转换方法。

图 3-4   原码、反码与补码之间的相互转换

原码(sign-magnitude)虽然最直观,但存在一些局限性。一方面,负数的原码不能直接用于运算。例如在原码下计算 \\(1 + (-2)\\) ,得到的结果是 \\(-3\\) ,这显然是不对的。

\\[ \\begin{aligned} & 1 + (-2) \\newline & \\rightarrow 0000 \\; 0001 + 1000 \\; 0010 \\newline & = 1000 \\; 0011 \\newline & \\rightarrow -3 \\end{aligned} \\]

为了解决此问题,计算机引入了反码(1's complement)。如果我们先将原码转换为反码,并在反码下计算 \\(1 + (-2)\\) ,最后将结果从反码转换回原码,则可得到正确结果 \\(-1\\) 。

\\[ \\begin{aligned} & 1 + (-2) \\newline & \\rightarrow 0000 \\; 0001 \\; \\text{(原码)} + 1000 \\; 0010 \\; \\text{(原码)} \\newline & = 0000 \\; 0001 \\; \\text{(反码)} + 1111 \\; 1101 \\; \\text{(反码)} \\newline & = 1111 \\; 1110 \\; \\text{(反码)} \\newline & = 1000 \\; 0001 \\; \\text{(原码)} \\newline & \\rightarrow -1 \\end{aligned} \\]

另一方面,数字零的原码有 \\(+0\\) 和 \\(-0\\) 两种表示方式。这意味着数字零对应两个不同的二进制编码,这可能会带来歧义。比如在条件判断中,如果没有区分正零和负零,则可能会导致判断结果出错。而如果我们想处理正零和负零歧义,则需要引入额外的判断操作,这可能会降低计算机的运算效率。

\\[ \\begin{aligned} +0 & \\rightarrow 0000 \\; 0000 \\newline -0 & \\rightarrow 1000 \\; 0000 \\end{aligned} \\]

与原码一样,反码也存在正负零歧义问题,因此计算机进一步引入了补码(2's complement)。我们先来观察一下负零的原码、反码、补码的转换过程:

\\[ \\begin{aligned} -0 \\rightarrow \\; & 1000 \\; 0000 \\; \\text{(原码)} \\newline = \\; & 1111 \\; 1111 \\; \\text{(反码)} \\newline = 1 \\; & 0000 \\; 0000 \\; \\text{(补码)} \\newline \\end{aligned} \\]

在负零的反码基础上加 \\(1\\) 会产生进位,但 byte 类型的长度只有 8 位,因此溢出到第 9 位的 \\(1\\) 会被舍弃。也就是说,负零的补码为 \\(0000 \\; 0000\\) ,与正零的补码相同。这意味着在补码表示中只存在一个零,正负零歧义从而得到解决。

还剩最后一个疑惑:byte 类型的取值范围是 \\([-128, 127]\\) ,多出来的一个负数 \\(-128\\) 是如何得到的呢?我们注意到,区间 \\([-127, +127]\\) 内的所有整数都有对应的原码、反码和补码,并且原码和补码之间可以互相转换。

然而,补码 \\(1000 \\; 0000\\) 是一个例外,它并没有对应的原码。根据转换方法,我们得到该补码的原码为 \\(0000 \\; 0000\\) 。这显然是矛盾的,因为该原码表示数字 \\(0\\) ,它的补码应该是自身。计算机规定这个特殊的补码 \\(1000 \\; 0000\\) 代表 \\(-128\\) 。实际上,\\((-1) + (-127)\\) 在补码下的计算结果就是 \\(-128\\) 。

\\[ \\begin{aligned} & (-127) + (-1) \\newline & \\rightarrow 1111 \\; 1111 \\; \\text{(原码)} + 1000 \\; 0001 \\; \\text{(原码)} \\newline & = 1000 \\; 0000 \\; \\text{(反码)} + 1111 \\; 1110 \\; \\text{(反码)} \\newline & = 1000 \\; 0001 \\; \\text{(补码)} + 1111 \\; 1111 \\; \\text{(补码)} \\newline & = 1000 \\; 0000 \\; \\text{(补码)} \\newline & \\rightarrow -128 \\end{aligned} \\]

你可能已经发现了,上述所有计算都是加法运算。这暗示着一个重要事实:计算机内部的硬件电路主要是基于加法运算设计的。这是因为加法运算相对于其他运算(比如乘法、除法和减法)来说,硬件实现起来更简单,更容易进行并行化处理,运算速度更快。

请注意,这并不意味着计算机只能做加法。通过将加法与一些基本逻辑运算结合,计算机能够实现各种其他的数学运算。例如,计算减法 \\(a - b\\) 可以转换为计算加法 \\(a + (-b)\\) ;计算乘法和除法可以转换为计算多次加法或减法。

现在我们可以总结出计算机使用补码的原因:基于补码表示,计算机可以用同样的电路和操作来处理正数和负数的加法,不需要设计特殊的硬件电路来处理减法,并且无须特别处理正负零的歧义问题。这大大简化了硬件设计,提高了运算效率。

补码的设计非常精妙,因篇幅关系我们就先介绍到这里,建议有兴趣的读者进一步深入了解。

","path":["第 3 章   数据结构","3.3   数字编码 *"],"tags":[]},{"location":"chapter_data_structure/number_encoding/#332","level":2,"title":"3.3.2   浮点数编码","text":"

细心的你可能会发现:intfloat 长度相同,都是 4 字节 ,但为什么 float 的取值范围远大于 int ?这非常反直觉,因为按理说 float 需要表示小数,取值范围应该变小才对。

实际上,这是因为浮点数 float 采用了不同的表示方式。记一个 32 比特长度的二进制数为:

\\[ b_{31} b_{30} b_{29} \\ldots b_2 b_1 b_0 \\]

根据 IEEE 754 标准,32-bit 长度的 float 由以下三个部分构成。

二进制数 float 对应值的计算方法为:

\\[ \\text {val} = (-1)^{b_{31}} \\times 2^{\\left(b_{30} b_{29} \\ldots b_{23}\\right)_2-127} \\times\\left(1 . b_{22} b_{21} \\ldots b_0\\right)_2 \\]

转化到十进制下的计算公式为:

\\[ \\text {val}=(-1)^{\\mathrm{S}} \\times 2^{\\mathrm{E} -127} \\times (1 + \\mathrm{N}) \\]

其中各项的取值范围为:

\\[ \\begin{aligned} \\mathrm{S} \\in & \\{ 0, 1\\}, \\quad \\mathrm{E} \\in \\{ 1, 2, \\dots, 254 \\} \\newline (1 + \\mathrm{N}) = & (1 + \\sum_{i=1}^{23} b_{23-i} 2^{-i}) \\subset [1, 2 - 2^{-23}] \\end{aligned} \\]

图 3-5   IEEE 754 标准下的 float 的计算示例

观察图 3-5 ,给定一个示例数据 \\(\\mathrm{S} = 0\\) , \\(\\mathrm{E} = 124\\) ,\\(\\mathrm{N} = 2^{-2} + 2^{-3} = 0.375\\) ,则有:

\\[ \\text { val } = (-1)^0 \\times 2^{124 - 127} \\times (1 + 0.375) = 0.171875 \\]

现在我们可以回答最初的问题:float 的表示方式包含指数位,导致其取值范围远大于 int 。根据以上计算,float 可表示的最大正数为 \\(2^{254 - 127} \\times (2 - 2^{-23}) \\approx 3.4 \\times 10^{38}\\) ,切换符号位便可得到最小负数。

尽管浮点数 float 扩展了取值范围,但其副作用是牺牲了精度。整数类型 int 将全部 32 比特用于表示数字,数字是均匀分布的;而由于指数位的存在,浮点数 float 的数值越大,相邻两个数字之间的差值就会趋向越大。

如表 3-2 所示,指数位 \\(\\mathrm{E} = 0\\) 和 \\(\\mathrm{E} = 255\\) 具有特殊含义,用于表示零、无穷大、\\(\\mathrm{NaN}\\) 等。

表 3-2   指数位含义

指数位 E 分数位 \\(\\mathrm{N} = 0\\) 分数位 \\(\\mathrm{N} \\ne 0\\) 计算公式 \\(0\\) \\(\\pm 0\\) 次正规数 \\((-1)^{\\mathrm{S}} \\times 2^{-126} \\times (0.\\mathrm{N})\\) \\(1, 2, \\dots, 254\\) 正规数 正规数 \\((-1)^{\\mathrm{S}} \\times 2^{(\\mathrm{E} -127)} \\times (1.\\mathrm{N})\\) \\(255\\) \\(\\pm \\infty\\) \\(\\mathrm{NaN}\\)

值得说明的是,次正规数显著提升了浮点数的精度。最小正正规数为 \\(2^{-126}\\) ,最小正次正规数为 \\(2^{-126} \\times 2^{-23}\\) 。

双精度 double 也采用类似于 float 的表示方法,在此不做赘述。

","path":["第 3 章   数据结构","3.3   数字编码 *"],"tags":[]},{"location":"chapter_data_structure/summary/","level":1,"title":"3.5   小结","text":"","path":["第 3 章   数据结构","3.5   小结"],"tags":[]},{"location":"chapter_data_structure/summary/#1","level":3,"title":"1.   重点回顾","text":"","path":["第 3 章   数据结构","3.5   小结"],"tags":[]},{"location":"chapter_data_structure/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q:为什么哈希表同时包含线性数据结构和非线性数据结构?

哈希表底层是数组,而为了解决哈希冲突,我们可能会使用“链式地址”(后续“哈希冲突”章节会讲):数组中每个桶指向一个链表,当链表长度超过一定阈值时,又可能被转化为树(通常为红黑树)。

从存储的角度来看,哈希表的底层是数组,其中每一个桶槽位可能包含一个值,也可能包含一个链表或一棵树。因此,哈希表可能同时包含线性数据结构(数组、链表)和非线性数据结构(树)。

Q:char 类型的长度是 1 字节吗?

char 类型的长度由编程语言采用的编码方法决定。例如,Java、JavaScript、TypeScript、C# 都采用 UTF-16 编码(保存 Unicode 码点),因此 char 类型的长度为 2 字节。

Q:基于数组实现的数据结构也称“静态数据结构” 是否有歧义?栈也可以进行出栈和入栈等操作,这些操作都是“动态”的。

栈确实可以实现动态的数据操作,但数据结构仍然是“静态”(长度不可变)的。尽管基于数组的数据结构可以动态地添加或删除元素,但它们的容量是固定的。如果数据量超出了预分配的大小,就需要创建一个新的更大的数组,并将旧数组的内容复制到新数组中。

Q:在构建栈(队列)的时候,未指定它的大小,为什么它们是“静态数据结构”呢?

在高级编程语言中,我们无须人工指定栈(队列)的初始容量,这个工作由类内部自动完成。例如,Java 的 ArrayList 的初始容量通常为 10。另外,扩容操作也是自动实现的。详见后续的“列表”章节。

Q:原码转补码的方法是“先取反后加 1”,那么补码转原码应该是逆运算“先减 1 后取反”,而补码转原码也一样可以通过“先取反后加 1”得到,这是为什么呢?

这是因为原码和补码的相互转换实际上是计算“补数”的过程。我们先给出补数的定义:假设 \\(a + b = c\\) ,那么我们称 \\(a\\) 是 \\(b\\) 到 \\(c\\) 的补数,反之也称 \\(b\\) 是 \\(a\\) 到 \\(c\\) 的补数。

给定一个 \\(n = 4\\) 位长度的二进制数 \\(0010\\) ,如果将这个数字看作原码(不考虑符号位),那么它的补码需通过“先取反后加 1”得到:

\\[ 0010 \\rightarrow 1101 \\rightarrow 1110 \\]

我们会发现,原码和补码的和是 \\(0010 + 1110 = 10000\\) ,也就是说,补码 \\(1110\\) 是原码 \\(0010\\) 到 \\(10000\\) 的“补数”。这意味着上述“先取反后加 1”实际上是计算到 \\(10000\\) 的补数的过程。

那么,补码 \\(1110\\) 到 \\(10000\\) 的“补数”是多少呢?我们依然可以用“先取反后加 1”得到它:

\\[ 1110 \\rightarrow 0001 \\rightarrow 0010 \\]

换句话说,原码和补码互为对方到 \\(10000\\) 的“补数”,因此“原码转补码”和“补码转原码”可以用相同的操作(先取反后加 1 )实现。

当然,我们也可以用逆运算来求补码 \\(1110\\) 的原码,即“先减 1 后取反”:

\\[ 1110 \\rightarrow 1101 \\rightarrow 0010 \\]

总结来看,“先取反后加 1”和“先减 1 后取反”这两种运算都是在计算到 \\(10000\\) 的补数,它们是等价的。

本质上看,“取反”操作实际上是求到 \\(1111\\) 的补数(因为恒有 原码 + 反码 = 1111);而在反码基础上再加 1 得到的补码,就是到 \\(10000\\) 的补数。

上述以 \\(n = 4\\) 为例,其可被推广至任意位数的二进制数。

","path":["第 3 章   数据结构","3.5   小结"],"tags":[]},{"location":"chapter_divide_and_conquer/","level":1,"title":"第 12 章   分治","text":"

Abstract

难题被逐层拆解,每一次的拆解都使它变得更为简单。

分而治之揭示了一个重要的事实:从简单做起,一切都不再复杂。

","path":["第 12 章   分治"],"tags":[]},{"location":"chapter_divide_and_conquer/#_1","level":2,"title":"本章内容","text":"","path":["第 12 章   分治"],"tags":[]},{"location":"chapter_divide_and_conquer/binary_search_recur/","level":1,"title":"12.2   分治搜索策略","text":"

我们已经学过,搜索算法分为两大类。

实际上,时间复杂度为 \\(O(\\log n)\\) 的搜索算法通常是基于分治策略实现的,例如二分查找和树。

二分查找的分治策略如下所示。

分治能够提升搜索效率,本质上是因为暴力搜索每轮只能排除一个选项,而分治搜索每轮可以排除一半选项。

","path":["第 12 章   分治","12.2   分治搜索策略"],"tags":[]},{"location":"chapter_divide_and_conquer/binary_search_recur/#1","level":3,"title":"1.   基于分治实现二分查找","text":"

在之前的章节中,二分查找是基于递推(迭代)实现的。现在我们基于分治(递归)来实现它。

Question

给定一个长度为 \\(n\\) 的有序数组 nums ,其中所有元素都是唯一的,请查找元素 target

从分治角度,我们将搜索区间 \\([i, j]\\) 对应的子问题记为 \\(f(i, j)\\) 。

以原问题 \\(f(0, n-1)\\) 为起始点,通过以下步骤进行二分查找。

  1. 计算搜索区间 \\([i, j]\\) 的中点 \\(m\\) ,根据它排除一半搜索区间。
  2. 递归求解规模减小一半的子问题,可能为 \\(f(i, m-1)\\) 或 \\(f(m+1, j)\\) 。
  3. 循环第 1. 步和第 2. 步,直至找到 target 或区间为空时返回。

图 12-4 展示了在数组中二分查找元素 \\(6\\) 的分治过程。

图 12-4   二分查找的分治过程

在实现代码中,我们声明一个递归函数 dfs() 来求解问题 \\(f(i, j)\\) :

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_recur.py
def dfs(nums: list[int], target: int, i: int, j: int) -> int:\n    \"\"\"二分查找:问题 f(i, j)\"\"\"\n    # 若区间为空,代表无目标元素,则返回 -1\n    if i > j:\n        return -1\n    # 计算中点索引 m\n    m = (i + j) // 2\n    if nums[m] < target:\n        # 递归子问题 f(m+1, j)\n        return dfs(nums, target, m + 1, j)\n    elif nums[m] > target:\n        # 递归子问题 f(i, m-1)\n        return dfs(nums, target, i, m - 1)\n    else:\n        # 找到目标元素,返回其索引\n        return m\n\ndef binary_search(nums: list[int], target: int) -> int:\n    \"\"\"二分查找\"\"\"\n    n = len(nums)\n    # 求解问题 f(0, n-1)\n    return dfs(nums, target, 0, n - 1)\n
binary_search_recur.cpp
/* 二分查找:问题 f(i, j) */\nint dfs(vector<int> &nums, int target, int i, int j) {\n    // 若区间为空,代表无目标元素,则返回 -1\n    if (i > j) {\n        return -1;\n    }\n    // 计算中点索引 m\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // 递归子问题 f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // 递归子问题 f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // 找到目标元素,返回其索引\n        return m;\n    }\n}\n\n/* 二分查找 */\nint binarySearch(vector<int> &nums, int target) {\n    int n = nums.size();\n    // 求解问题 f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
binary_search_recur.java
/* 二分查找:问题 f(i, j) */\nint dfs(int[] nums, int target, int i, int j) {\n    // 若区间为空,代表无目标元素,则返回 -1\n    if (i > j) {\n        return -1;\n    }\n    // 计算中点索引 m\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // 递归子问题 f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // 递归子问题 f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // 找到目标元素,返回其索引\n        return m;\n    }\n}\n\n/* 二分查找 */\nint binarySearch(int[] nums, int target) {\n    int n = nums.length;\n    // 求解问题 f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
binary_search_recur.cs
/* 二分查找:问题 f(i, j) */\nint DFS(int[] nums, int target, int i, int j) {\n    // 若区间为空,代表无目标元素,则返回 -1\n    if (i > j) {\n        return -1;\n    }\n    // 计算中点索引 m\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // 递归子问题 f(m+1, j)\n        return DFS(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // 递归子问题 f(i, m-1)\n        return DFS(nums, target, i, m - 1);\n    } else {\n        // 找到目标元素,返回其索引\n        return m;\n    }\n}\n\n/* 二分查找 */\nint BinarySearch(int[] nums, int target) {\n    int n = nums.Length;\n    // 求解问题 f(0, n-1)\n    return DFS(nums, target, 0, n - 1);\n}\n
binary_search_recur.go
/* 二分查找:问题 f(i, j) */\nfunc dfs(nums []int, target, i, j int) int {\n    // 如果区间为空,代表没有目标元素,则返回 -1\n    if i > j {\n        return -1\n    }\n    //    计算索引中点\n    m := i + ((j - i) >> 1)\n    //判断中点与目标元素大小\n    if nums[m] < target {\n        // 小于则递归右半数组\n        // 递归子问题 f(m+1, j)\n        return dfs(nums, target, m+1, j)\n    } else if nums[m] > target {\n        // 大于则递归左半数组\n        // 递归子问题 f(i, m-1)\n        return dfs(nums, target, i, m-1)\n    } else {\n        // 找到目标元素,返回其索引\n        return m\n    }\n}\n\n/* 二分查找 */\nfunc binarySearch(nums []int, target int) int {\n    n := len(nums)\n    return dfs(nums, target, 0, n-1)\n}\n
binary_search_recur.swift
/* 二分查找:问题 f(i, j) */\nfunc dfs(nums: [Int], target: Int, i: Int, j: Int) -> Int {\n    // 若区间为空,代表无目标元素,则返回 -1\n    if i > j {\n        return -1\n    }\n    // 计算中点索引 m\n    let m = (i + j) / 2\n    if nums[m] < target {\n        // 递归子问题 f(m+1, j)\n        return dfs(nums: nums, target: target, i: m + 1, j: j)\n    } else if nums[m] > target {\n        // 递归子问题 f(i, m-1)\n        return dfs(nums: nums, target: target, i: i, j: m - 1)\n    } else {\n        // 找到目标元素,返回其索引\n        return m\n    }\n}\n\n/* 二分查找 */\nfunc binarySearch(nums: [Int], target: Int) -> Int {\n    // 求解问题 f(0, n-1)\n    dfs(nums: nums, target: target, i: nums.startIndex, j: nums.endIndex - 1)\n}\n
binary_search_recur.js
/* 二分查找:问题 f(i, j) */\nfunction dfs(nums, target, i, j) {\n    // 若区间为空,代表无目标元素,则返回 -1\n    if (i > j) {\n        return -1;\n    }\n    // 计算中点索引 m\n    const m = i + ((j - i) >> 1);\n    if (nums[m] < target) {\n        // 递归子问题 f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // 递归子问题 f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // 找到目标元素,返回其索引\n        return m;\n    }\n}\n\n/* 二分查找 */\nfunction binarySearch(nums, target) {\n    const n = nums.length;\n    // 求解问题 f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
binary_search_recur.ts
/* 二分查找:问题 f(i, j) */\nfunction dfs(nums: number[], target: number, i: number, j: number): number {\n    // 若区间为空,代表无目标元素,则返回 -1\n    if (i > j) {\n        return -1;\n    }\n    // 计算中点索引 m\n    const m = i + ((j - i) >> 1);\n    if (nums[m] < target) {\n        // 递归子问题 f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // 递归子问题 f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // 找到目标元素,返回其索引\n        return m;\n    }\n}\n\n/* 二分查找 */\nfunction binarySearch(nums: number[], target: number): number {\n    const n = nums.length;\n    // 求解问题 f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
binary_search_recur.dart
/* 二分查找:问题 f(i, j) */\nint dfs(List<int> nums, int target, int i, int j) {\n  // 若区间为空,代表无目标元素,则返回 -1\n  if (i > j) {\n    return -1;\n  }\n  // 计算中点索引 m\n  int m = (i + j) ~/ 2;\n  if (nums[m] < target) {\n    // 递归子问题 f(m+1, j)\n    return dfs(nums, target, m + 1, j);\n  } else if (nums[m] > target) {\n    // 递归子问题 f(i, m-1)\n    return dfs(nums, target, i, m - 1);\n  } else {\n    // 找到目标元素,返回其索引\n    return m;\n  }\n}\n\n/* 二分查找 */\nint binarySearch(List<int> nums, int target) {\n  int n = nums.length;\n  // 求解问题 f(0, n-1)\n  return dfs(nums, target, 0, n - 1);\n}\n
binary_search_recur.rs
/* 二分查找:问题 f(i, j) */\nfn dfs(nums: &[i32], target: i32, i: i32, j: i32) -> i32 {\n    // 若区间为空,代表无目标元素,则返回 -1\n    if i > j {\n        return -1;\n    }\n    let m: i32 = i + (j - i) / 2;\n    if nums[m as usize] < target {\n        // 递归子问题 f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if nums[m as usize] > target {\n        // 递归子问题 f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // 找到目标元素,返回其索引\n        return m;\n    }\n}\n\n/* 二分查找 */\nfn binary_search(nums: &[i32], target: i32) -> i32 {\n    let n = nums.len() as i32;\n    // 求解问题 f(0, n-1)\n    dfs(nums, target, 0, n - 1)\n}\n
binary_search_recur.c
/* 二分查找:问题 f(i, j) */\nint dfs(int nums[], int target, int i, int j) {\n    // 若区间为空,代表无目标元素,则返回 -1\n    if (i > j) {\n        return -1;\n    }\n    // 计算中点索引 m\n    int m = (i + j) / 2;\n    if (nums[m] < target) {\n        // 递归子问题 f(m+1, j)\n        return dfs(nums, target, m + 1, j);\n    } else if (nums[m] > target) {\n        // 递归子问题 f(i, m-1)\n        return dfs(nums, target, i, m - 1);\n    } else {\n        // 找到目标元素,返回其索引\n        return m;\n    }\n}\n\n/* 二分查找 */\nint binarySearch(int nums[], int target, int numsSize) {\n    int n = numsSize;\n    // 求解问题 f(0, n-1)\n    return dfs(nums, target, 0, n - 1);\n}\n
binary_search_recur.kt
/* 二分查找:问题 f(i, j) */\nfun dfs(\n    nums: IntArray,\n    target: Int,\n    i: Int,\n    j: Int\n): Int {\n    // 若区间为空,代表无目标元素,则返回 -1\n    if (i > j) {\n        return -1\n    }\n    // 计算中点索引 m\n    val m = (i + j) / 2\n    return if (nums[m] < target) {\n        // 递归子问题 f(m+1, j)\n        dfs(nums, target, m + 1, j)\n    } else if (nums[m] > target) {\n        // 递归子问题 f(i, m-1)\n        dfs(nums, target, i, m - 1)\n    } else {\n        // 找到目标元素,返回其索引\n        m\n    }\n}\n\n/* 二分查找 */\nfun binarySearch(nums: IntArray, target: Int): Int {\n    val n = nums.size\n    // 求解问题 f(0, n-1)\n    return dfs(nums, target, 0, n - 1)\n}\n
binary_search_recur.rb
### 二分查找:问题 f(i, j) ###\ndef dfs(nums, target, i, j)\n  # 若区间为空,代表无目标元素,则返回 -1\n  return -1 if i > j\n\n  # 计算中点索引 m\n  m = (i + j) / 2\n\n  if nums[m] < target\n    # 递归子问题 f(m+1, j)\n    return dfs(nums, target, m + 1, j)\n  elsif nums[m] > target\n    # 递归子问题 f(i, m-1)\n    return dfs(nums, target, i, m - 1)\n  else\n    # 找到目标元素,返回其索引\n    return m\n  end\nend\n\n### 二分查找 ###\ndef binary_search(nums, target)\n  n = nums.length\n  # 求解问题 f(0, n-1)\n  dfs(nums, target, 0, n - 1)\nend\n
可视化运行

全屏观看 >

","path":["第 12 章   分治","12.2   分治搜索策略"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/","level":1,"title":"12.3   构建二叉树问题","text":"

Question

给定一棵二叉树的前序遍历 preorder 和中序遍历 inorder ,请从中构建二叉树,返回二叉树的根节点。假设二叉树中没有值重复的节点(如图 12-5 所示)。

图 12-5   构建二叉树的示例数据

","path":["第 12 章   分治","12.3   构建二叉树问题"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#1","level":3,"title":"1.   判断是否为分治问题","text":"

原问题定义为从 preorderinorder 构建二叉树,是一个典型的分治问题。

","path":["第 12 章   分治","12.3   构建二叉树问题"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#2","level":3,"title":"2.   如何划分子树","text":"

根据以上分析,这道题可以使用分治来求解,但如何通过前序遍历 preorder 和中序遍历 inorder 来划分左子树和右子树呢?

根据定义,preorderinorder 都可以划分为三个部分。

以上图数据为例,我们可以通过图 12-6 所示的步骤得到划分结果。

  1. 前序遍历的首元素 3 是根节点的值。
  2. 查找根节点 3 在 inorder 中的索引,利用该索引可将 inorder 划分为 [ 9 | 3 | 1 2 7 ]
  3. 根据 inorder 的划分结果,易得左子树和右子树的节点数量分别为 1 和 3 ,从而可将 preorder 划分为 [ 3 | 9 | 2 1 7 ]

图 12-6   在前序遍历和中序遍历中划分子树

","path":["第 12 章   分治","12.3   构建二叉树问题"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#3","level":3,"title":"3.   基于变量描述子树区间","text":"

根据以上划分方法,我们已经得到根节点、左子树、右子树在 preorderinorder 中的索引区间。而为了描述这些索引区间,我们需要借助几个指针变量。

如表 12-1 所示,通过以上变量即可表示根节点在 preorder 中的索引,以及子树在 inorder 中的索引区间。

表 12-1   根节点和子树在前序遍历和中序遍历中的索引

根节点在 preorder 中的索引 子树在 inorder 中的索引区间 当前树 \\(i\\) \\([l, r]\\) 左子树 \\(i + 1\\) \\([l, m-1]\\) 右子树 \\(i + 1 + (m - l)\\) \\([m+1, r]\\)

请注意,右子树根节点索引中的 \\((m-l)\\) 的含义是“左子树的节点数量”,建议结合图 12-7 理解。

图 12-7   根节点和左右子树的索引区间表示

","path":["第 12 章   分治","12.3   构建二叉树问题"],"tags":[]},{"location":"chapter_divide_and_conquer/build_binary_tree_problem/#4","level":3,"title":"4.   代码实现","text":"

为了提升查询 \\(m\\) 的效率,我们借助一个哈希表 hmap 来存储数组 inorder 中元素到索引的映射:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby build_tree.py
def dfs(\n    preorder: list[int],\n    inorder_map: dict[int, int],\n    i: int,\n    l: int,\n    r: int,\n) -> TreeNode | None:\n    \"\"\"构建二叉树:分治\"\"\"\n    # 子树区间为空时终止\n    if r - l < 0:\n        return None\n    # 初始化根节点\n    root = TreeNode(preorder[i])\n    # 查询 m ,从而划分左右子树\n    m = inorder_map[preorder[i]]\n    # 子问题:构建左子树\n    root.left = dfs(preorder, inorder_map, i + 1, l, m - 1)\n    # 子问题:构建右子树\n    root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r)\n    # 返回根节点\n    return root\n\ndef build_tree(preorder: list[int], inorder: list[int]) -> TreeNode | None:\n    \"\"\"构建二叉树\"\"\"\n    # 初始化哈希表,存储 inorder 元素到索引的映射\n    inorder_map = {val: i for i, val in enumerate(inorder)}\n    root = dfs(preorder, inorder_map, 0, 0, len(inorder) - 1)\n    return root\n
build_tree.cpp
/* 构建二叉树:分治 */\nTreeNode *dfs(vector<int> &preorder, unordered_map<int, int> &inorderMap, int i, int l, int r) {\n    // 子树区间为空时终止\n    if (r - l < 0)\n        return NULL;\n    // 初始化根节点\n    TreeNode *root = new TreeNode(preorder[i]);\n    // 查询 m ,从而划分左右子树\n    int m = inorderMap[preorder[i]];\n    // 子问题:构建左子树\n    root->left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // 子问题:构建右子树\n    root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // 返回根节点\n    return root;\n}\n\n/* 构建二叉树 */\nTreeNode *buildTree(vector<int> &preorder, vector<int> &inorder) {\n    // 初始化哈希表,存储 inorder 元素到索引的映射\n    unordered_map<int, int> inorderMap;\n    for (int i = 0; i < inorder.size(); i++) {\n        inorderMap[inorder[i]] = i;\n    }\n    TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorder.size() - 1);\n    return root;\n}\n
build_tree.java
/* 构建二叉树:分治 */\nTreeNode dfs(int[] preorder, Map<Integer, Integer> inorderMap, int i, int l, int r) {\n    // 子树区间为空时终止\n    if (r - l < 0)\n        return null;\n    // 初始化根节点\n    TreeNode root = new TreeNode(preorder[i]);\n    // 查询 m ,从而划分左右子树\n    int m = inorderMap.get(preorder[i]);\n    // 子问题:构建左子树\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // 子问题:构建右子树\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // 返回根节点\n    return root;\n}\n\n/* 构建二叉树 */\nTreeNode buildTree(int[] preorder, int[] inorder) {\n    // 初始化哈希表,存储 inorder 元素到索引的映射\n    Map<Integer, Integer> inorderMap = new HashMap<>();\n    for (int i = 0; i < inorder.length; i++) {\n        inorderMap.put(inorder[i], i);\n    }\n    TreeNode root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);\n    return root;\n}\n
build_tree.cs
/* 构建二叉树:分治 */\nTreeNode? DFS(int[] preorder, Dictionary<int, int> inorderMap, int i, int l, int r) {\n    // 子树区间为空时终止\n    if (r - l < 0)\n        return null;\n    // 初始化根节点\n    TreeNode root = new(preorder[i]);\n    // 查询 m ,从而划分左右子树\n    int m = inorderMap[preorder[i]];\n    // 子问题:构建左子树\n    root.left = DFS(preorder, inorderMap, i + 1, l, m - 1);\n    // 子问题:构建右子树\n    root.right = DFS(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // 返回根节点\n    return root;\n}\n\n/* 构建二叉树 */\nTreeNode? BuildTree(int[] preorder, int[] inorder) {\n    // 初始化哈希表,存储 inorder 元素到索引的映射\n    Dictionary<int, int> inorderMap = [];\n    for (int i = 0; i < inorder.Length; i++) {\n        inorderMap.TryAdd(inorder[i], i);\n    }\n    TreeNode? root = DFS(preorder, inorderMap, 0, 0, inorder.Length - 1);\n    return root;\n}\n
build_tree.go
/* 构建二叉树:分治 */\nfunc dfsBuildTree(preorder []int, inorderMap map[int]int, i, l, r int) *TreeNode {\n    // 子树区间为空时终止\n    if r-l < 0 {\n        return nil\n    }\n    // 初始化根节点\n    root := NewTreeNode(preorder[i])\n    // 查询 m ,从而划分左右子树\n    m := inorderMap[preorder[i]]\n    // 子问题:构建左子树\n    root.Left = dfsBuildTree(preorder, inorderMap, i+1, l, m-1)\n    // 子问题:构建右子树\n    root.Right = dfsBuildTree(preorder, inorderMap, i+1+m-l, m+1, r)\n    // 返回根节点\n    return root\n}\n\n/* 构建二叉树 */\nfunc buildTree(preorder, inorder []int) *TreeNode {\n    // 初始化哈希表,存储 inorder 元素到索引的映射\n    inorderMap := make(map[int]int, len(inorder))\n    for i := 0; i < len(inorder); i++ {\n        inorderMap[inorder[i]] = i\n    }\n\n    root := dfsBuildTree(preorder, inorderMap, 0, 0, len(inorder)-1)\n    return root\n}\n
build_tree.swift
/* 构建二叉树:分治 */\nfunc dfs(preorder: [Int], inorderMap: [Int: Int], i: Int, l: Int, r: Int) -> TreeNode? {\n    // 子树区间为空时终止\n    if r - l < 0 {\n        return nil\n    }\n    // 初始化根节点\n    let root = TreeNode(x: preorder[i])\n    // 查询 m ,从而划分左右子树\n    let m = inorderMap[preorder[i]]!\n    // 子问题:构建左子树\n    root.left = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1, l: l, r: m - 1)\n    // 子问题:构建右子树\n    root.right = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1 + m - l, l: m + 1, r: r)\n    // 返回根节点\n    return root\n}\n\n/* 构建二叉树 */\nfunc buildTree(preorder: [Int], inorder: [Int]) -> TreeNode? {\n    // 初始化哈希表,存储 inorder 元素到索引的映射\n    let inorderMap = inorder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset }\n    return dfs(preorder: preorder, inorderMap: inorderMap, i: inorder.startIndex, l: inorder.startIndex, r: inorder.endIndex - 1)\n}\n
build_tree.js
/* 构建二叉树:分治 */\nfunction dfs(preorder, inorderMap, i, l, r) {\n    // 子树区间为空时终止\n    if (r - l < 0) return null;\n    // 初始化根节点\n    const root = new TreeNode(preorder[i]);\n    // 查询 m ,从而划分左右子树\n    const m = inorderMap.get(preorder[i]);\n    // 子问题:构建左子树\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // 子问题:构建右子树\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // 返回根节点\n    return root;\n}\n\n/* 构建二叉树 */\nfunction buildTree(preorder, inorder) {\n    // 初始化哈希表,存储 inorder 元素到索引的映射\n    let inorderMap = new Map();\n    for (let i = 0; i < inorder.length; i++) {\n        inorderMap.set(inorder[i], i);\n    }\n    const root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);\n    return root;\n}\n
build_tree.ts
/* 构建二叉树:分治 */\nfunction dfs(\n    preorder: number[],\n    inorderMap: Map<number, number>,\n    i: number,\n    l: number,\n    r: number\n): TreeNode | null {\n    // 子树区间为空时终止\n    if (r - l < 0) return null;\n    // 初始化根节点\n    const root: TreeNode = new TreeNode(preorder[i]);\n    // 查询 m ,从而划分左右子树\n    const m = inorderMap.get(preorder[i]);\n    // 子问题:构建左子树\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n    // 子问题:构建右子树\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n    // 返回根节点\n    return root;\n}\n\n/* 构建二叉树 */\nfunction buildTree(preorder: number[], inorder: number[]): TreeNode | null {\n    // 初始化哈希表,存储 inorder 元素到索引的映射\n    let inorderMap = new Map<number, number>();\n    for (let i = 0; i < inorder.length; i++) {\n        inorderMap.set(inorder[i], i);\n    }\n    const root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);\n    return root;\n}\n
build_tree.dart
/* 构建二叉树:分治 */\nTreeNode? dfs(\n  List<int> preorder,\n  Map<int, int> inorderMap,\n  int i,\n  int l,\n  int r,\n) {\n  // 子树区间为空时终止\n  if (r - l < 0) {\n    return null;\n  }\n  // 初始化根节点\n  TreeNode? root = TreeNode(preorder[i]);\n  // 查询 m ,从而划分左右子树\n  int m = inorderMap[preorder[i]]!;\n  // 子问题:构建左子树\n  root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);\n  // 子问题:构建右子树\n  root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);\n  // 返回根节点\n  return root;\n}\n\n/* 构建二叉树 */\nTreeNode? buildTree(List<int> preorder, List<int> inorder) {\n  // 初始化哈希表,存储 inorder 元素到索引的映射\n  Map<int, int> inorderMap = {};\n  for (int i = 0; i < inorder.length; i++) {\n    inorderMap[inorder[i]] = i;\n  }\n  TreeNode? root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);\n  return root;\n}\n
build_tree.rs
/* 构建二叉树:分治 */\nfn dfs(\n    preorder: &[i32],\n    inorder_map: &HashMap<i32, i32>,\n    i: i32,\n    l: i32,\n    r: i32,\n) -> Option<Rc<RefCell<TreeNode>>> {\n    // 子树区间为空时终止\n    if r - l < 0 {\n        return None;\n    }\n    // 初始化根节点\n    let root = TreeNode::new(preorder[i as usize]);\n    // 查询 m ,从而划分左右子树\n    let m = inorder_map.get(&preorder[i as usize]).unwrap();\n    // 子问题:构建左子树\n    root.borrow_mut().left = dfs(preorder, inorder_map, i + 1, l, m - 1);\n    // 子问题:构建右子树\n    root.borrow_mut().right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r);\n    // 返回根节点\n    Some(root)\n}\n\n/* 构建二叉树 */\nfn build_tree(preorder: &[i32], inorder: &[i32]) -> Option<Rc<RefCell<TreeNode>>> {\n    // 初始化哈希表,存储 inorder 元素到索引的映射\n    let mut inorder_map: HashMap<i32, i32> = HashMap::new();\n    for i in 0..inorder.len() {\n        inorder_map.insert(inorder[i], i as i32);\n    }\n    let root = dfs(preorder, &inorder_map, 0, 0, inorder.len() as i32 - 1);\n    root\n}\n
build_tree.c
/* 构建二叉树:分治 */\nTreeNode *dfs(int *preorder, int *inorderMap, int i, int l, int r, int size) {\n    // 子树区间为空时终止\n    if (r - l < 0)\n        return NULL;\n    // 初始化根节点\n    TreeNode *root = (TreeNode *)malloc(sizeof(TreeNode));\n    root->val = preorder[i];\n    root->left = NULL;\n    root->right = NULL;\n    // 查询 m ,从而划分左右子树\n    int m = inorderMap[preorder[i]];\n    // 子问题:构建左子树\n    root->left = dfs(preorder, inorderMap, i + 1, l, m - 1, size);\n    // 子问题:构建右子树\n    root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r, size);\n    // 返回根节点\n    return root;\n}\n\n/* 构建二叉树 */\nTreeNode *buildTree(int *preorder, int preorderSize, int *inorder, int inorderSize) {\n    // 初始化哈希表,存储 inorder 元素到索引的映射\n    int *inorderMap = (int *)malloc(sizeof(int) * MAX_SIZE);\n    for (int i = 0; i < inorderSize; i++) {\n        inorderMap[inorder[i]] = i;\n    }\n    TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorderSize - 1, inorderSize);\n    free(inorderMap);\n    return root;\n}\n
build_tree.kt
/* 构建二叉树:分治 */\nfun dfs(\n    preorder: IntArray,\n    inorderMap: Map<Int?, Int?>,\n    i: Int,\n    l: Int,\n    r: Int\n): TreeNode? {\n    // 子树区间为空时终止\n    if (r - l < 0) return null\n    // 初始化根节点\n    val root = TreeNode(preorder[i])\n    // 查询 m ,从而划分左右子树\n    val m = inorderMap[preorder[i]]!!\n    // 子问题:构建左子树\n    root.left = dfs(preorder, inorderMap, i + 1, l, m - 1)\n    // 子问题:构建右子树\n    root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r)\n    // 返回根节点\n    return root\n}\n\n/* 构建二叉树 */\nfun buildTree(preorder: IntArray, inorder: IntArray): TreeNode? {\n    // 初始化哈希表,存储 inorder 元素到索引的映射\n    val inorderMap = HashMap<Int?, Int?>()\n    for (i in inorder.indices) {\n        inorderMap[inorder[i]] = i\n    }\n    val root = dfs(preorder, inorderMap, 0, 0, inorder.size - 1)\n    return root\n}\n
build_tree.rb
### 构建二叉树:分治 ###\ndef dfs(preorder, inorder_map, i, l, r)\n  # 子树区间为空时终止\n  return if r - l < 0\n\n  # 初始化根节点\n  root = TreeNode.new(preorder[i])\n  # 查询 m ,从而划分左右子树\n  m = inorder_map[preorder[i]]\n  # 子问题:构建左子树\n  root.left = dfs(preorder, inorder_map, i + 1, l, m - 1)\n  # 子问题:构建右子树\n  root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r)\n\n  # 返回根节点\n  root\nend\n\n### 构建二叉树 ###\ndef build_tree(preorder, inorder)\n  # 初始化哈希表,存储 inorder 元素到索引的映射\n  inorder_map = {}\n  inorder.each_with_index { |val, i| inorder_map[val] = i }\n  dfs(preorder, inorder_map, 0, 0, inorder.length - 1)\nend\n
可视化运行

全屏观看 >

图 12-8 展示了构建二叉树的递归过程,各个节点是在向下“递”的过程中建立的,而各条边(引用)是在向上“归”的过程中建立的。

<1><2><3><4><5><6><7><8><9>

图 12-8   构建二叉树的递归过程

每个递归函数内的前序遍历 preorder 和中序遍历 inorder 的划分结果如图 12-9 所示。

图 12-9   每个递归函数中的划分结果

设树的节点数量为 \\(n\\) ,初始化每一个节点(执行一个递归函数 dfs() )使用 \\(O(1)\\) 时间。因此总体时间复杂度为 \\(O(n)\\) 。

哈希表存储 inorder 元素到索引的映射,空间复杂度为 \\(O(n)\\) 。在最差情况下,即二叉树退化为链表时,递归深度达到 \\(n\\) ,使用 \\(O(n)\\) 的栈帧空间。因此总体空间复杂度为 \\(O(n)\\) 。

","path":["第 12 章   分治","12.3   构建二叉树问题"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/","level":1,"title":"12.1   分治算法","text":"

分治(divide and conquer),全称分而治之,是一种非常重要且常见的算法策略。分治通常基于递归实现,包括“分”和“治”两个步骤。

  1. 分(划分阶段):递归地将原问题分解为两个或多个子问题,直至到达最小子问题时终止。
  2. 治(合并阶段):从已知解的最小子问题开始,从底至顶地将子问题的解进行合并,从而构建出原问题的解。

如图 12-1 所示,“归并排序”是分治策略的典型应用之一。

  1. 分:递归地将原数组(原问题)划分为两个子数组(子问题),直到子数组只剩一个元素(最小子问题)。
  2. 治:从底至顶地将有序的子数组(子问题的解)进行合并,从而得到有序的原数组(原问题的解)。

图 12-1   归并排序的分治策略

","path":["第 12 章   分治","12.1   分治算法"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1211","level":2,"title":"12.1.1   如何判断分治问题","text":"

一个问题是否适合使用分治解决,通常可以参考以下几个判断依据。

  1. 问题可以分解:原问题可以分解成规模更小、类似的子问题,以及能够以相同方式递归地进行划分。
  2. 子问题是独立的:子问题之间没有重叠,互不依赖,可以独立解决。
  3. 子问题的解可以合并:原问题的解通过合并子问题的解得来。

显然,归并排序满足以上三个判断依据。

  1. 问题可以分解:递归地将数组(原问题)划分为两个子数组(子问题)。
  2. 子问题是独立的:每个子数组都可以独立地进行排序(子问题可以独立进行求解)。
  3. 子问题的解可以合并:两个有序子数组(子问题的解)可以合并为一个有序数组(原问题的解)。
","path":["第 12 章   分治","12.1   分治算法"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1212","level":2,"title":"12.1.2   通过分治提升效率","text":"

分治不仅可以有效地解决算法问题,往往还可以提升算法效率。在排序算法中,快速排序、归并排序、堆排序相较于选择、冒泡、插入排序更快,就是因为它们应用了分治策略。

那么,我们不禁发问:为什么分治可以提升算法效率,其底层逻辑是什么?换句话说,将大问题分解为多个子问题、解决子问题、将子问题的解合并为原问题的解,这几步的效率为什么比直接解决原问题的效率更高?这个问题可以从操作数量和并行计算两方面来讨论。

","path":["第 12 章   分治","12.1   分治算法"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1","level":3,"title":"1.   操作数量优化","text":"

以“冒泡排序”为例,其处理一个长度为 \\(n\\) 的数组需要 \\(O(n^2)\\) 时间。假设我们按照图 12-2 所示的方式,将数组从中点处分为两个子数组,则划分需要 \\(O(n)\\) 时间,排序每个子数组需要 \\(O((n / 2)^2)\\) 时间,合并两个子数组需要 \\(O(n)\\) 时间,总体时间复杂度为:

\\[ O(n + (\\frac{n}{2})^2 \\times 2 + n) = O(\\frac{n^2}{2} + 2n) \\]

图 12-2   划分数组前后的冒泡排序

接下来,我们计算以下不等式,其左边和右边分别为划分前和划分后的操作总数:

\\[ \\begin{aligned} n^2 & > \\frac{n^2}{2} + 2n \\newline n^2 - \\frac{n^2}{2} - 2n & > 0 \\newline n(n - 4) & > 0 \\end{aligned} \\]

这意味着当 \\(n > 4\\) 时,划分后的操作数量更少,排序效率应该更高。请注意,划分后的时间复杂度仍然是平方阶 \\(O(n^2)\\) ,只是复杂度中的常数项变小了。

进一步想,如果我们把子数组不断地再从中点处划分为两个子数组,直至子数组只剩一个元素时停止划分呢?这种思路实际上就是“归并排序”,时间复杂度为 \\(O(n \\log n)\\) 。

再思考,如果我们多设置几个划分点,将原数组平均划分为 \\(k\\) 个子数组呢?这种情况与“桶排序”非常类似,它非常适合排序海量数据,理论上时间复杂度可以达到 \\(O(n + k)\\) 。

","path":["第 12 章   分治","12.1   分治算法"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#2","level":3,"title":"2.   并行计算优化","text":"

我们知道,分治生成的子问题是相互独立的,因此通常可以并行解决。也就是说,分治不仅可以降低算法的时间复杂度,还有利于操作系统的并行优化。

并行优化在多核或多处理器的环境中尤其有效,因为系统可以同时处理多个子问题,更加充分地利用计算资源,从而显著减少总体的运行时间。

比如在图 12-3 所示的“桶排序”中,我们将海量的数据平均分配到各个桶中,则可将所有桶的排序任务分散到各个计算单元,完成后再合并结果。

图 12-3   桶排序的并行计算

","path":["第 12 章   分治","12.1   分治算法"],"tags":[]},{"location":"chapter_divide_and_conquer/divide_and_conquer/#1213","level":2,"title":"12.1.3   分治常见应用","text":"

一方面,分治可以用来解决许多经典算法问题。

另一方面,分治在算法和数据结构的设计中应用得非常广泛。

可以看出,分治是一种“润物细无声”的算法思想,隐含在各种算法与数据结构之中。

","path":["第 12 章   分治","12.1   分治算法"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/","level":1,"title":"12.4   汉诺塔问题","text":"

在归并排序和构建二叉树中,我们都是将原问题分解为两个规模为原问题一半的子问题。然而对于汉诺塔问题,我们采用不同的分解策略。

Question

给定三根柱子,记为 ABC 。起始状态下,柱子 A 上套着 \\(n\\) 个圆盘,它们从上到下按照从小到大的顺序排列。我们的任务是要把这 \\(n\\) 个圆盘移到柱子 C 上,并保持它们的原有顺序不变(如图 12-10 所示)。在移动圆盘的过程中,需要遵守以下规则。

  1. 圆盘只能从一根柱子顶部拿出,从另一根柱子顶部放入。
  2. 每次只能移动一个圆盘。
  3. 小圆盘必须时刻位于大圆盘之上。

图 12-10   汉诺塔问题示例

我们将规模为 \\(i\\) 的汉诺塔问题记作 \\(f(i)\\) 。例如 \\(f(3)\\) 代表将 \\(3\\) 个圆盘从 A 移动至 C 的汉诺塔问题。

","path":["第 12 章   分治","12.4   汉诺塔问题"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#1","level":3,"title":"1.   考虑基本情况","text":"

如图 12-11 所示,对于问题 \\(f(1)\\) ,即当只有一个圆盘时,我们将它直接从 A 移动至 C 即可。

<1><2>

图 12-11   规模为 1 的问题的解

如图 12-12 所示,对于问题 \\(f(2)\\) ,即当有两个圆盘时,由于要时刻满足小圆盘在大圆盘之上,因此需要借助 B 来完成移动。

  1. 先将上面的小圆盘从 A 移至 B
  2. 再将大圆盘从 A 移至 C
  3. 最后将小圆盘从 B 移至 C
<1><2><3><4>

图 12-12   规模为 2 的问题的解

解决问题 \\(f(2)\\) 的过程可总结为:将两个圆盘借助 BA 移至 C 。其中,C 称为目标柱、B 称为缓冲柱。

","path":["第 12 章   分治","12.4   汉诺塔问题"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#2","level":3,"title":"2.   子问题分解","text":"

对于问题 \\(f(3)\\) ,即当有三个圆盘时,情况变得稍微复杂了一些。

因为已知 \\(f(1)\\) 和 \\(f(2)\\) 的解,所以我们可从分治角度思考,将 A 顶部的两个圆盘看作一个整体,执行图 12-13 所示的步骤。这样三个圆盘就被顺利地从 A 移至 C 了。

  1. B 为目标柱、C 为缓冲柱,将两个圆盘从 A 移至 B
  2. A 中剩余的一个圆盘从 A 直接移动至 C
  3. C 为目标柱、A 为缓冲柱,将两个圆盘从 B 移至 C
<1><2><3><4>

图 12-13   规模为 3 的问题的解

从本质上看,我们将问题 \\(f(3)\\) 划分为两个子问题 \\(f(2)\\) 和一个子问题 \\(f(1)\\) 。按顺序解决这三个子问题之后,原问题随之得到解决。这说明子问题是独立的,而且解可以合并。

至此,我们可总结出图 12-14 所示的解决汉诺塔问题的分治策略:将原问题 \\(f(n)\\) 划分为两个子问题 \\(f(n-1)\\) 和一个子问题 \\(f(1)\\) ,并按照以下顺序解决这三个子问题。

  1. 将 \\(n-1\\) 个圆盘借助 CA 移至 B
  2. 将剩余 \\(1\\) 个圆盘从 A 直接移至 C
  3. 将 \\(n-1\\) 个圆盘借助 AB 移至 C

对于这两个子问题 \\(f(n-1)\\) ,可以通过相同的方式进行递归划分,直至达到最小子问题 \\(f(1)\\) 。而 \\(f(1)\\) 的解是已知的,只需一次移动操作即可。

图 12-14   解决汉诺塔问题的分治策略

","path":["第 12 章   分治","12.4   汉诺塔问题"],"tags":[]},{"location":"chapter_divide_and_conquer/hanota_problem/#3","level":3,"title":"3.   代码实现","text":"

在代码中,我们声明一个递归函数 dfs(i, src, buf, tar) ,它的作用是将柱 src 顶部的 \\(i\\) 个圆盘借助缓冲柱 buf 移动至目标柱 tar

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hanota.py
def move(src: list[int], tar: list[int]):\n    \"\"\"移动一个圆盘\"\"\"\n    # 从 src 顶部拿出一个圆盘\n    pan = src.pop()\n    # 将圆盘放入 tar 顶部\n    tar.append(pan)\n\ndef dfs(i: int, src: list[int], buf: list[int], tar: list[int]):\n    \"\"\"求解汉诺塔问题 f(i)\"\"\"\n    # 若 src 只剩下一个圆盘,则直接将其移到 tar\n    if i == 1:\n        move(src, tar)\n        return\n    # 子问题 f(i-1) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n    dfs(i - 1, src, tar, buf)\n    # 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n    move(src, tar)\n    # 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\n    dfs(i - 1, buf, src, tar)\n\ndef solve_hanota(A: list[int], B: list[int], C: list[int]):\n    \"\"\"求解汉诺塔问题\"\"\"\n    n = len(A)\n    # 将 A 顶部 n 个圆盘借助 B 移到 C\n    dfs(n, A, B, C)\n
hanota.cpp
/* 移动一个圆盘 */\nvoid move(vector<int> &src, vector<int> &tar) {\n    // 从 src 顶部拿出一个圆盘\n    int pan = src.back();\n    src.pop_back();\n    // 将圆盘放入 tar 顶部\n    tar.push_back(pan);\n}\n\n/* 求解汉诺塔问题 f(i) */\nvoid dfs(int i, vector<int> &src, vector<int> &buf, vector<int> &tar) {\n    // 若 src 只剩下一个圆盘,则直接将其移到 tar\n    if (i == 1) {\n        move(src, tar);\n        return;\n    }\n    // 子问题 f(i-1) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n    dfs(i - 1, src, tar, buf);\n    // 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n    move(src, tar);\n    // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\n    dfs(i - 1, buf, src, tar);\n}\n\n/* 求解汉诺塔问题 */\nvoid solveHanota(vector<int> &A, vector<int> &B, vector<int> &C) {\n    int n = A.size();\n    // 将 A 顶部 n 个圆盘借助 B 移到 C\n    dfs(n, A, B, C);\n}\n
hanota.java
/* 移动一个圆盘 */\nvoid move(List<Integer> src, List<Integer> tar) {\n    // 从 src 顶部拿出一个圆盘\n    Integer pan = src.remove(src.size() - 1);\n    // 将圆盘放入 tar 顶部\n    tar.add(pan);\n}\n\n/* 求解汉诺塔问题 f(i) */\nvoid dfs(int i, List<Integer> src, List<Integer> buf, List<Integer> tar) {\n    // 若 src 只剩下一个圆盘,则直接将其移到 tar\n    if (i == 1) {\n        move(src, tar);\n        return;\n    }\n    // 子问题 f(i-1) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n    dfs(i - 1, src, tar, buf);\n    // 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n    move(src, tar);\n    // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\n    dfs(i - 1, buf, src, tar);\n}\n\n/* 求解汉诺塔问题 */\nvoid solveHanota(List<Integer> A, List<Integer> B, List<Integer> C) {\n    int n = A.size();\n    // 将 A 顶部 n 个圆盘借助 B 移到 C\n    dfs(n, A, B, C);\n}\n
hanota.cs
/* 移动一个圆盘 */\nvoid Move(List<int> src, List<int> tar) {\n    // 从 src 顶部拿出一个圆盘\n    int pan = src[^1];\n    src.RemoveAt(src.Count - 1);\n    // 将圆盘放入 tar 顶部\n    tar.Add(pan);\n}\n\n/* 求解汉诺塔问题 f(i) */\nvoid DFS(int i, List<int> src, List<int> buf, List<int> tar) {\n    // 若 src 只剩下一个圆盘,则直接将其移到 tar\n    if (i == 1) {\n        Move(src, tar);\n        return;\n    }\n    // 子问题 f(i-1) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n    DFS(i - 1, src, tar, buf);\n    // 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n    Move(src, tar);\n    // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\n    DFS(i - 1, buf, src, tar);\n}\n\n/* 求解汉诺塔问题 */\nvoid SolveHanota(List<int> A, List<int> B, List<int> C) {\n    int n = A.Count;\n    // 将 A 顶部 n 个圆盘借助 B 移到 C\n    DFS(n, A, B, C);\n}\n
hanota.go
/* 移动一个圆盘 */\nfunc move(src, tar *list.List) {\n    // 从 src 顶部拿出一个圆盘\n    pan := src.Back()\n    // 将圆盘放入 tar 顶部\n    tar.PushBack(pan.Value)\n    // 移除 src 顶部圆盘\n    src.Remove(pan)\n}\n\n/* 求解汉诺塔问题 f(i) */\nfunc dfsHanota(i int, src, buf, tar *list.List) {\n    // 若 src 只剩下一个圆盘,则直接将其移到 tar\n    if i == 1 {\n        move(src, tar)\n        return\n    }\n    // 子问题 f(i-1) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n    dfsHanota(i-1, src, tar, buf)\n    // 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n    move(src, tar)\n    // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\n    dfsHanota(i-1, buf, src, tar)\n}\n\n/* 求解汉诺塔问题 */\nfunc solveHanota(A, B, C *list.List) {\n    n := A.Len()\n    // 将 A 顶部 n 个圆盘借助 B 移到 C\n    dfsHanota(n, A, B, C)\n}\n
hanota.swift
/* 移动一个圆盘 */\nfunc move(src: inout [Int], tar: inout [Int]) {\n    // 从 src 顶部拿出一个圆盘\n    let pan = src.popLast()!\n    // 将圆盘放入 tar 顶部\n    tar.append(pan)\n}\n\n/* 求解汉诺塔问题 f(i) */\nfunc dfs(i: Int, src: inout [Int], buf: inout [Int], tar: inout [Int]) {\n    // 若 src 只剩下一个圆盘,则直接将其移到 tar\n    if i == 1 {\n        move(src: &src, tar: &tar)\n        return\n    }\n    // 子问题 f(i-1) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n    dfs(i: i - 1, src: &src, buf: &tar, tar: &buf)\n    // 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n    move(src: &src, tar: &tar)\n    // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\n    dfs(i: i - 1, src: &buf, buf: &src, tar: &tar)\n}\n\n/* 求解汉诺塔问题 */\nfunc solveHanota(A: inout [Int], B: inout [Int], C: inout [Int]) {\n    let n = A.count\n    // 列表尾部是柱子顶部\n    // 将 src 顶部 n 个圆盘借助 B 移到 C\n    dfs(i: n, src: &A, buf: &B, tar: &C)\n}\n
hanota.js
/* 移动一个圆盘 */\nfunction move(src, tar) {\n    // 从 src 顶部拿出一个圆盘\n    const pan = src.pop();\n    // 将圆盘放入 tar 顶部\n    tar.push(pan);\n}\n\n/* 求解汉诺塔问题 f(i) */\nfunction dfs(i, src, buf, tar) {\n    // 若 src 只剩下一个圆盘,则直接将其移到 tar\n    if (i === 1) {\n        move(src, tar);\n        return;\n    }\n    // 子问题 f(i-1) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n    dfs(i - 1, src, tar, buf);\n    // 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n    move(src, tar);\n    // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\n    dfs(i - 1, buf, src, tar);\n}\n\n/* 求解汉诺塔问题 */\nfunction solveHanota(A, B, C) {\n    const n = A.length;\n    // 将 A 顶部 n 个圆盘借助 B 移到 C\n    dfs(n, A, B, C);\n}\n
hanota.ts
/* 移动一个圆盘 */\nfunction move(src: number[], tar: number[]): void {\n    // 从 src 顶部拿出一个圆盘\n    const pan = src.pop();\n    // 将圆盘放入 tar 顶部\n    tar.push(pan);\n}\n\n/* 求解汉诺塔问题 f(i) */\nfunction dfs(i: number, src: number[], buf: number[], tar: number[]): void {\n    // 若 src 只剩下一个圆盘,则直接将其移到 tar\n    if (i === 1) {\n        move(src, tar);\n        return;\n    }\n    // 子问题 f(i-1) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n    dfs(i - 1, src, tar, buf);\n    // 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n    move(src, tar);\n    // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\n    dfs(i - 1, buf, src, tar);\n}\n\n/* 求解汉诺塔问题 */\nfunction solveHanota(A: number[], B: number[], C: number[]): void {\n    const n = A.length;\n    // 将 A 顶部 n 个圆盘借助 B 移到 C\n    dfs(n, A, B, C);\n}\n
hanota.dart
/* 移动一个圆盘 */\nvoid move(List<int> src, List<int> tar) {\n  // 从 src 顶部拿出一个圆盘\n  int pan = src.removeLast();\n  // 将圆盘放入 tar 顶部\n  tar.add(pan);\n}\n\n/* 求解汉诺塔问题 f(i) */\nvoid dfs(int i, List<int> src, List<int> buf, List<int> tar) {\n  // 若 src 只剩下一个圆盘,则直接将其移到 tar\n  if (i == 1) {\n    move(src, tar);\n    return;\n  }\n  // 子问题 f(i-1) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n  dfs(i - 1, src, tar, buf);\n  // 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n  move(src, tar);\n  // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\n  dfs(i - 1, buf, src, tar);\n}\n\n/* 求解汉诺塔问题 */\nvoid solveHanota(List<int> A, List<int> B, List<int> C) {\n  int n = A.length;\n  // 将 A 顶部 n 个圆盘借助 B 移到 C\n  dfs(n, A, B, C);\n}\n
hanota.rs
/* 移动一个圆盘 */\nfn move_pan(src: &mut Vec<i32>, tar: &mut Vec<i32>) {\n    // 从 src 顶部拿出一个圆盘\n    let pan = src.pop().unwrap();\n    // 将圆盘放入 tar 顶部\n    tar.push(pan);\n}\n\n/* 求解汉诺塔问题 f(i) */\nfn dfs(i: i32, src: &mut Vec<i32>, buf: &mut Vec<i32>, tar: &mut Vec<i32>) {\n    // 若 src 只剩下一个圆盘,则直接将其移到 tar\n    if i == 1 {\n        move_pan(src, tar);\n        return;\n    }\n    // 子问题 f(i-1) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n    dfs(i - 1, src, tar, buf);\n    // 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n    move_pan(src, tar);\n    // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\n    dfs(i - 1, buf, src, tar);\n}\n\n/* 求解汉诺塔问题 */\nfn solve_hanota(A: &mut Vec<i32>, B: &mut Vec<i32>, C: &mut Vec<i32>) {\n    let n = A.len() as i32;\n    // 将 A 顶部 n 个圆盘借助 B 移到 C\n    dfs(n, A, B, C);\n}\n
hanota.c
/* 移动一个圆盘 */\nvoid move(int *src, int *srcSize, int *tar, int *tarSize) {\n    // 从 src 顶部拿出一个圆盘\n    int pan = src[*srcSize - 1];\n    src[*srcSize - 1] = 0;\n    (*srcSize)--;\n    // 将圆盘放入 tar 顶部\n    tar[*tarSize] = pan;\n    (*tarSize)++;\n}\n\n/* 求解汉诺塔问题 f(i) */\nvoid dfs(int i, int *src, int *srcSize, int *buf, int *bufSize, int *tar, int *tarSize) {\n    // 若 src 只剩下一个圆盘,则直接将其移到 tar\n    if (i == 1) {\n        move(src, srcSize, tar, tarSize);\n        return;\n    }\n    // 子问题 f(i-1) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n    dfs(i - 1, src, srcSize, tar, tarSize, buf, bufSize);\n    // 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n    move(src, srcSize, tar, tarSize);\n    // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\n    dfs(i - 1, buf, bufSize, src, srcSize, tar, tarSize);\n}\n\n/* 求解汉诺塔问题 */\nvoid solveHanota(int *A, int *ASize, int *B, int *BSize, int *C, int *CSize) {\n    // 将 A 顶部 n 个圆盘借助 B 移到 C\n    dfs(*ASize, A, ASize, B, BSize, C, CSize);\n}\n
hanota.kt
/* 移动一个圆盘 */\nfun move(src: MutableList<Int>, tar: MutableList<Int>) {\n    // 从 src 顶部拿出一个圆盘\n    val pan = src.removeAt(src.size - 1)\n    // 将圆盘放入 tar 顶部\n    tar.add(pan)\n}\n\n/* 求解汉诺塔问题 f(i) */\nfun dfs(i: Int, src: MutableList<Int>, buf: MutableList<Int>, tar: MutableList<Int>) {\n    // 若 src 只剩下一个圆盘,则直接将其移到 tar\n    if (i == 1) {\n        move(src, tar)\n        return\n    }\n    // 子问题 f(i-1) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n    dfs(i - 1, src, tar, buf)\n    // 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n    move(src, tar)\n    // 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\n    dfs(i - 1, buf, src, tar)\n}\n\n/* 求解汉诺塔问题 */\nfun solveHanota(A: MutableList<Int>, B: MutableList<Int>, C: MutableList<Int>) {\n    val n = A.size\n    // 将 A 顶部 n 个圆盘借助 B 移到 C\n    dfs(n, A, B, C)\n}\n
hanota.rb
### 移动一个圆盘 ###\ndef move(src, tar)\n  # 从 src 顶部拿出一个圆盘\n  pan = src.pop\n  # 将圆盘放入 tar 顶部\n  tar << pan\nend\n\n### 求解汉诺塔问题 f(i) ###\ndef dfs(i, src, buf, tar)\n  # 若 src 只剩下一个圆盘,则直接将其移到 tar\n  if i == 1\n    move(src, tar)\n    return\n  end\n\n  # 子问题 f(i-1) :将 src 顶部 i-1 个圆盘借助 tar 移到 buf\n  dfs(i - 1, src, tar, buf)\n  # 子问题 f(1) :将 src 剩余一个圆盘移到 tar\n  move(src, tar)\n  # 子问题 f(i-1) :将 buf 顶部 i-1 个圆盘借助 src 移到 tar\n  dfs(i - 1, buf, src, tar)\nend\n\n### 求解汉诺塔问题 ###\ndef solve_hanota(_A, _B, _C)\n  n = _A.length\n  # 将 A 顶部 n 个圆盘借助 B 移到 C\n  dfs(n, _A, _B, _C)\nend\n
可视化运行

全屏观看 >

如图 12-15 所示,汉诺塔问题形成一棵高度为 \\(n\\) 的递归树,每个节点代表一个子问题,对应一个开启的 dfs() 函数,因此时间复杂度为 \\(O(2^n)\\) ,空间复杂度为 \\(O(n)\\) 。

图 12-15   汉诺塔问题的递归树

Quote

汉诺塔问题源自一个古老的传说。在古印度的一个寺庙里,僧侣们有三根高大的钻石柱子,以及 \\(64\\) 个大小不一的金圆盘。僧侣们不断地移动圆盘,他们相信在最后一个圆盘被正确放置的那一刻,这个世界就会结束。

然而,即使僧侣们每秒钟移动一次,总共需要大约 \\(2^{64} \\approx 1.84×10^{19}\\) 秒,合约 \\(5850\\) 亿年,远远超过了现在对宇宙年龄的估计。所以,倘若这个传说是真的,我们应该不需要担心世界末日的到来。

","path":["第 12 章   分治","12.4   汉诺塔问题"],"tags":[]},{"location":"chapter_divide_and_conquer/summary/","level":1,"title":"12.5   小结","text":"","path":["第 12 章   分治","12.5   小结"],"tags":[]},{"location":"chapter_divide_and_conquer/summary/#1","level":3,"title":"1.   重点回顾","text":"","path":["第 12 章   分治","12.5   小结"],"tags":[]},{"location":"chapter_dynamic_programming/","level":1,"title":"第 14 章   动态规划","text":"

Abstract

小溪汇入河流,江河汇入大海。

动态规划将小问题的解汇集成大问题的答案,一步步引领我们走向解决问题的彼岸。

","path":["第 14 章   动态规划"],"tags":[]},{"location":"chapter_dynamic_programming/#_1","level":2,"title":"本章内容","text":"","path":["第 14 章   动态规划"],"tags":[]},{"location":"chapter_dynamic_programming/dp_problem_features/","level":1,"title":"14.2   动态规划问题特性","text":"

在上一节中,我们学习了动态规划是如何通过子问题分解来求解原问题的。实际上,子问题分解是一种通用的算法思路,在分治、动态规划、回溯中的侧重点不同。

实际上,动态规划常用来求解最优化问题,它们不仅包含重叠子问题,还具有另外两大特性:最优子结构、无后效性。

","path":["第 14 章   动态规划","14.2   动态规划问题特性"],"tags":[]},{"location":"chapter_dynamic_programming/dp_problem_features/#1421","level":2,"title":"14.2.1   最优子结构","text":"

我们对爬楼梯问题稍作改动,使之更加适合展示最优子结构概念。

爬楼梯最小代价

给定一个楼梯,你每步可以上 \\(1\\) 阶或者 \\(2\\) 阶,每一阶楼梯上都贴有一个非负整数,表示你在该台阶所需要付出的代价。给定一个非负整数数组 \\(cost\\) ,其中 \\(cost[i]\\) 表示在第 \\(i\\) 个台阶需要付出的代价,\\(cost[0]\\) 为地面(起始点)。请计算最少需要付出多少代价才能到达顶部?

如图 14-6 所示,若第 \\(1\\)、\\(2\\)、\\(3\\) 阶的代价分别为 \\(1\\)、\\(10\\)、\\(1\\) ,则从地面爬到第 \\(3\\) 阶的最小代价为 \\(2\\) 。

图 14-6   爬到第 3 阶的最小代价

设 \\(dp[i]\\) 为爬到第 \\(i\\) 阶累计付出的代价,由于第 \\(i\\) 阶只可能从 \\(i - 1\\) 阶或 \\(i - 2\\) 阶走来,因此 \\(dp[i]\\) 只可能等于 \\(dp[i - 1] + cost[i]\\) 或 \\(dp[i - 2] + cost[i]\\) 。为了尽可能减少代价,我们应该选择两者中较小的那一个:

\\[ dp[i] = \\min(dp[i-1], dp[i-2]) + cost[i] \\]

这便可以引出最优子结构的含义:原问题的最优解是从子问题的最优解构建得来的。

本题显然具有最优子结构:我们从两个子问题最优解 \\(dp[i-1]\\) 和 \\(dp[i-2]\\) 中挑选出较优的那一个,并用它构建出原问题 \\(dp[i]\\) 的最优解。

那么,上一节的爬楼梯题目有没有最优子结构呢?它的目标是求解方案数量,看似是一个计数问题,但如果换一种问法:“求解最大方案数量”。我们意外地发现,虽然题目修改前后是等价的,但最优子结构浮现出来了:第 \\(n\\) 阶最大方案数量等于第 \\(n-1\\) 阶和第 \\(n-2\\) 阶最大方案数量之和。所以说,最优子结构的解释方式比较灵活,在不同问题中会有不同的含义。

根据状态转移方程,以及初始状态 \\(dp[1] = cost[1]\\) 和 \\(dp[2] = cost[2]\\) ,我们就可以得到动态规划代码:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_cost_climbing_stairs_dp.py
def min_cost_climbing_stairs_dp(cost: list[int]) -> int:\n    \"\"\"爬楼梯最小代价:动态规划\"\"\"\n    n = len(cost) - 1\n    if n == 1 or n == 2:\n        return cost[n]\n    # 初始化 dp 表,用于存储子问题的解\n    dp = [0] * (n + 1)\n    # 初始状态:预设最小子问题的解\n    dp[1], dp[2] = cost[1], cost[2]\n    # 状态转移:从较小子问题逐步求解较大子问题\n    for i in range(3, n + 1):\n        dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]\n    return dp[n]\n
min_cost_climbing_stairs_dp.cpp
/* 爬楼梯最小代价:动态规划 */\nint minCostClimbingStairsDP(vector<int> &cost) {\n    int n = cost.size() - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // 初始化 dp 表,用于存储子问题的解\n    vector<int> dp(n + 1);\n    // 初始状态:预设最小子问题的解\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // 状态转移:从较小子问题逐步求解较大子问题\n    for (int i = 3; i <= n; i++) {\n        dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
min_cost_climbing_stairs_dp.java
/* 爬楼梯最小代价:动态规划 */\nint minCostClimbingStairsDP(int[] cost) {\n    int n = cost.length - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // 初始化 dp 表,用于存储子问题的解\n    int[] dp = new int[n + 1];\n    // 初始状态:预设最小子问题的解\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // 状态转移:从较小子问题逐步求解较大子问题\n    for (int i = 3; i <= n; i++) {\n        dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
min_cost_climbing_stairs_dp.cs
/* 爬楼梯最小代价:动态规划 */\nint MinCostClimbingStairsDP(int[] cost) {\n    int n = cost.Length - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // 初始化 dp 表,用于存储子问题的解\n    int[] dp = new int[n + 1];\n    // 初始状态:预设最小子问题的解\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // 状态转移:从较小子问题逐步求解较大子问题\n    for (int i = 3; i <= n; i++) {\n        dp[i] = Math.Min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
min_cost_climbing_stairs_dp.go
/* 爬楼梯最小代价:动态规划 */\nfunc minCostClimbingStairsDP(cost []int) int {\n    n := len(cost) - 1\n    if n == 1 || n == 2 {\n        return cost[n]\n    }\n    min := func(a, b int) int {\n        if a < b {\n            return a\n        }\n        return b\n    }\n    // 初始化 dp 表,用于存储子问题的解\n    dp := make([]int, n+1)\n    // 初始状态:预设最小子问题的解\n    dp[1] = cost[1]\n    dp[2] = cost[2]\n    // 状态转移:从较小子问题逐步求解较大子问题\n    for i := 3; i <= n; i++ {\n        dp[i] = min(dp[i-1], dp[i-2]) + cost[i]\n    }\n    return dp[n]\n}\n
min_cost_climbing_stairs_dp.swift
/* 爬楼梯最小代价:动态规划 */\nfunc minCostClimbingStairsDP(cost: [Int]) -> Int {\n    let n = cost.count - 1\n    if n == 1 || n == 2 {\n        return cost[n]\n    }\n    // 初始化 dp 表,用于存储子问题的解\n    var dp = Array(repeating: 0, count: n + 1)\n    // 初始状态:预设最小子问题的解\n    dp[1] = cost[1]\n    dp[2] = cost[2]\n    // 状态转移:从较小子问题逐步求解较大子问题\n    for i in 3 ... n {\n        dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]\n    }\n    return dp[n]\n}\n
min_cost_climbing_stairs_dp.js
/* 爬楼梯最小代价:动态规划 */\nfunction minCostClimbingStairsDP(cost) {\n    const n = cost.length - 1;\n    if (n === 1 || n === 2) {\n        return cost[n];\n    }\n    // 初始化 dp 表,用于存储子问题的解\n    const dp = new Array(n + 1);\n    // 初始状态:预设最小子问题的解\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // 状态转移:从较小子问题逐步求解较大子问题\n    for (let i = 3; i <= n; i++) {\n        dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
min_cost_climbing_stairs_dp.ts
/* 爬楼梯最小代价:动态规划 */\nfunction minCostClimbingStairsDP(cost: Array<number>): number {\n    const n = cost.length - 1;\n    if (n === 1 || n === 2) {\n        return cost[n];\n    }\n    // 初始化 dp 表,用于存储子问题的解\n    const dp = new Array(n + 1);\n    // 初始状态:预设最小子问题的解\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // 状态转移:从较小子问题逐步求解较大子问题\n    for (let i = 3; i <= n; i++) {\n        dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    return dp[n];\n}\n
min_cost_climbing_stairs_dp.dart
/* 爬楼梯最小代价:动态规划 */\nint minCostClimbingStairsDP(List<int> cost) {\n  int n = cost.length - 1;\n  if (n == 1 || n == 2) return cost[n];\n  // 初始化 dp 表,用于存储子问题的解\n  List<int> dp = List.filled(n + 1, 0);\n  // 初始状态:预设最小子问题的解\n  dp[1] = cost[1];\n  dp[2] = cost[2];\n  // 状态转移:从较小子问题逐步求解较大子问题\n  for (int i = 3; i <= n; i++) {\n    dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];\n  }\n  return dp[n];\n}\n
min_cost_climbing_stairs_dp.rs
/* 爬楼梯最小代价:动态规划 */\nfn min_cost_climbing_stairs_dp(cost: &[i32]) -> i32 {\n    let n = cost.len() - 1;\n    if n == 1 || n == 2 {\n        return cost[n];\n    }\n    // 初始化 dp 表,用于存储子问题的解\n    let mut dp = vec![-1; n + 1];\n    // 初始状态:预设最小子问题的解\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // 状态转移:从较小子问题逐步求解较大子问题\n    for i in 3..=n {\n        dp[i] = cmp::min(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    dp[n]\n}\n
min_cost_climbing_stairs_dp.c
/* 爬楼梯最小代价:动态规划 */\nint minCostClimbingStairsDP(int cost[], int costSize) {\n    int n = costSize - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    // 初始化 dp 表,用于存储子问题的解\n    int *dp = calloc(n + 1, sizeof(int));\n    // 初始状态:预设最小子问题的解\n    dp[1] = cost[1];\n    dp[2] = cost[2];\n    // 状态转移:从较小子问题逐步求解较大子问题\n    for (int i = 3; i <= n; i++) {\n        dp[i] = myMin(dp[i - 1], dp[i - 2]) + cost[i];\n    }\n    int res = dp[n];\n    // 释放内存\n    free(dp);\n    return res;\n}\n
min_cost_climbing_stairs_dp.kt
/* 爬楼梯最小代价:动态规划 */\nfun minCostClimbingStairsDP(cost: IntArray): Int {\n    val n = cost.size - 1\n    if (n == 1 || n == 2) return cost[n]\n    // 初始化 dp 表,用于存储子问题的解\n    val dp = IntArray(n + 1)\n    // 初始状态:预设最小子问题的解\n    dp[1] = cost[1]\n    dp[2] = cost[2]\n    // 状态转移:从较小子问题逐步求解较大子问题\n    for (i in 3..n) {\n        dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]\n    }\n    return dp[n]\n}\n
min_cost_climbing_stairs_dp.rb
### 爬楼梯最小代价:动态规划 ###\ndef min_cost_climbing_stairs_dp(cost)\n  n = cost.length - 1\n  return cost[n] if n == 1 || n == 2\n  # 初始化 dp 表,用于存储子问题的解\n  dp = Array.new(n + 1, 0)\n  # 初始状态:预设最小子问题的解\n  dp[1], dp[2] = cost[1], cost[2]\n  # 状态转移:从较小子问题逐步求解较大子问题\n  (3...(n + 1)).each { |i| dp[i] = [dp[i - 1], dp[i - 2]].min + cost[i] }\n  dp[n]\nend\n
可视化运行

全屏观看 >

图 14-7 展示了以上代码的动态规划过程。

图 14-7   爬楼梯最小代价的动态规划过程

本题也可以进行空间优化,将一维压缩至零维,使得空间复杂度从 \\(O(n)\\) 降至 \\(O(1)\\) :

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_cost_climbing_stairs_dp.py
def min_cost_climbing_stairs_dp_comp(cost: list[int]) -> int:\n    \"\"\"爬楼梯最小代价:空间优化后的动态规划\"\"\"\n    n = len(cost) - 1\n    if n == 1 or n == 2:\n        return cost[n]\n    a, b = cost[1], cost[2]\n    for i in range(3, n + 1):\n        a, b = b, min(a, b) + cost[i]\n    return b\n
min_cost_climbing_stairs_dp.cpp
/* 爬楼梯最小代价:空间优化后的动态规划 */\nint minCostClimbingStairsDPComp(vector<int> &cost) {\n    int n = cost.size() - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    int a = cost[1], b = cost[2];\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
min_cost_climbing_stairs_dp.java
/* 爬楼梯最小代价:空间优化后的动态规划 */\nint minCostClimbingStairsDPComp(int[] cost) {\n    int n = cost.length - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    int a = cost[1], b = cost[2];\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = Math.min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
min_cost_climbing_stairs_dp.cs
/* 爬楼梯最小代价:空间优化后的动态规划 */\nint MinCostClimbingStairsDPComp(int[] cost) {\n    int n = cost.Length - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    int a = cost[1], b = cost[2];\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = Math.Min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
min_cost_climbing_stairs_dp.go
/* 爬楼梯最小代价:空间优化后的动态规划 */\nfunc minCostClimbingStairsDPComp(cost []int) int {\n    n := len(cost) - 1\n    if n == 1 || n == 2 {\n        return cost[n]\n    }\n    min := func(a, b int) int {\n        if a < b {\n            return a\n        }\n        return b\n    }\n    // 初始状态:预设最小子问题的解\n    a, b := cost[1], cost[2]\n    // 状态转移:从较小子问题逐步求解较大子问题\n    for i := 3; i <= n; i++ {\n        tmp := b\n        b = min(a, tmp) + cost[i]\n        a = tmp\n    }\n    return b\n}\n
min_cost_climbing_stairs_dp.swift
/* 爬楼梯最小代价:空间优化后的动态规划 */\nfunc minCostClimbingStairsDPComp(cost: [Int]) -> Int {\n    let n = cost.count - 1\n    if n == 1 || n == 2 {\n        return cost[n]\n    }\n    var (a, b) = (cost[1], cost[2])\n    for i in 3 ... n {\n        (a, b) = (b, min(a, b) + cost[i])\n    }\n    return b\n}\n
min_cost_climbing_stairs_dp.js
/* 爬楼梯最小代价:空间优化后的动态规划 */\nfunction minCostClimbingStairsDPComp(cost) {\n    const n = cost.length - 1;\n    if (n === 1 || n === 2) {\n        return cost[n];\n    }\n    let a = cost[1],\n        b = cost[2];\n    for (let i = 3; i <= n; i++) {\n        const tmp = b;\n        b = Math.min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
min_cost_climbing_stairs_dp.ts
/* 爬楼梯最小代价:空间优化后的动态规划 */\nfunction minCostClimbingStairsDPComp(cost: Array<number>): number {\n    const n = cost.length - 1;\n    if (n === 1 || n === 2) {\n        return cost[n];\n    }\n    let a = cost[1],\n        b = cost[2];\n    for (let i = 3; i <= n; i++) {\n        const tmp = b;\n        b = Math.min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
min_cost_climbing_stairs_dp.dart
/* 爬楼梯最小代价:空间优化后的动态规划 */\nint minCostClimbingStairsDPComp(List<int> cost) {\n  int n = cost.length - 1;\n  if (n == 1 || n == 2) return cost[n];\n  int a = cost[1], b = cost[2];\n  for (int i = 3; i <= n; i++) {\n    int tmp = b;\n    b = min(a, tmp) + cost[i];\n    a = tmp;\n  }\n  return b;\n}\n
min_cost_climbing_stairs_dp.rs
/* 爬楼梯最小代价:空间优化后的动态规划 */\nfn min_cost_climbing_stairs_dp_comp(cost: &[i32]) -> i32 {\n    let n = cost.len() - 1;\n    if n == 1 || n == 2 {\n        return cost[n];\n    };\n    let (mut a, mut b) = (cost[1], cost[2]);\n    for i in 3..=n {\n        let tmp = b;\n        b = cmp::min(a, tmp) + cost[i];\n        a = tmp;\n    }\n    b\n}\n
min_cost_climbing_stairs_dp.c
/* 爬楼梯最小代价:空间优化后的动态规划 */\nint minCostClimbingStairsDPComp(int cost[], int costSize) {\n    int n = costSize - 1;\n    if (n == 1 || n == 2)\n        return cost[n];\n    int a = cost[1], b = cost[2];\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = myMin(a, tmp) + cost[i];\n        a = tmp;\n    }\n    return b;\n}\n
min_cost_climbing_stairs_dp.kt
/* 爬楼梯最小代价:空间优化后的动态规划 */\nfun minCostClimbingStairsDPComp(cost: IntArray): Int {\n    val n = cost.size - 1\n    if (n == 1 || n == 2) return cost[n]\n    var a = cost[1]\n    var b = cost[2]\n    for (i in 3..n) {\n        val tmp = b\n        b = min(a, tmp) + cost[i]\n        a = tmp\n    }\n    return b\n}\n
min_cost_climbing_stairs_dp.rb
### 爬楼梯最小代价:动态规划 ###\ndef min_cost_climbing_stairs_dp(cost)\n  n = cost.length - 1\n  return cost[n] if n == 1 || n == 2\n  # 初始化 dp 表,用于存储子问题的解\n  dp = Array.new(n + 1, 0)\n  # 初始状态:预设最小子问题的解\n  dp[1], dp[2] = cost[1], cost[2]\n  # 状态转移:从较小子问题逐步求解较大子问题\n  (3...(n + 1)).each { |i| dp[i] = [dp[i - 1], dp[i - 2]].min + cost[i] }\n  dp[n]\nend\n\n# 爬楼梯最小代价:空间优化后的动态规划\ndef min_cost_climbing_stairs_dp_comp(cost)\n  n = cost.length - 1\n  return cost[n] if n == 1 || n == 2\n  a, b = cost[1], cost[2]\n  (3...(n + 1)).each { |i| a, b = b, [a, b].min + cost[i] }\n  b\nend\n
可视化运行

全屏观看 >

","path":["第 14 章   动态规划","14.2   动态规划问题特性"],"tags":[]},{"location":"chapter_dynamic_programming/dp_problem_features/#1422","level":2,"title":"14.2.2   无后效性","text":"

无后效性是动态规划能够有效解决问题的重要特性之一,其定义为:给定一个确定的状态,它的未来发展只与当前状态有关,而与过去经历的所有状态无关。

以爬楼梯问题为例,给定状态 \\(i\\) ,它会发展出状态 \\(i+1\\) 和状态 \\(i+2\\) ,分别对应跳 \\(1\\) 步和跳 \\(2\\) 步。在做出这两种选择时,我们无须考虑状态 \\(i\\) 之前的状态,它们对状态 \\(i\\) 的未来没有影响。

然而,如果我们给爬楼梯问题添加一个约束,情况就不一样了。

带约束爬楼梯

给定一个共有 \\(n\\) 阶的楼梯,你每步可以上 \\(1\\) 阶或者 \\(2\\) 阶,但不能连续两轮跳 \\(1\\) 阶,请问有多少种方案可以爬到楼顶?

如图 14-8 所示,爬上第 \\(3\\) 阶仅剩 \\(2\\) 种可行方案,其中连续三次跳 \\(1\\) 阶的方案不满足约束条件,因此被舍弃。

图 14-8   带约束爬到第 3 阶的方案数量

在该问题中,如果上一轮是跳 \\(1\\) 阶上来的,那么下一轮就必须跳 \\(2\\) 阶。这意味着,下一步选择不能由当前状态(当前所在楼梯阶数)独立决定,还和前一个状态(上一轮所在楼梯阶数)有关。

不难发现,此问题已不满足无后效性,状态转移方程 \\(dp[i] = dp[i-1] + dp[i-2]\\) 也失效了,因为 \\(dp[i-1]\\) 代表本轮跳 \\(1\\) 阶,但其中包含了许多“上一轮是跳 \\(1\\) 阶上来的”方案,而为了满足约束,我们就不能将 \\(dp[i-1]\\) 直接计入 \\(dp[i]\\) 中。

为此,我们需要扩展状态定义:状态 \\([i, j]\\) 表示处在第 \\(i\\) 阶并且上一轮跳了 \\(j\\) 阶,其中 \\(j \\in \\{1, 2\\}\\) 。此状态定义有效地区分了上一轮跳了 \\(1\\) 阶还是 \\(2\\) 阶,我们可以据此判断当前状态是从何而来的。

如图 14-9 所示,在该定义下,\\(dp[i, j]\\) 表示状态 \\([i, j]\\) 对应的方案数。此时状态转移方程为:

\\[ \\begin{cases} dp[i, 1] = dp[i-1, 2] \\\\ dp[i, 2] = dp[i-2, 1] + dp[i-2, 2] \\end{cases} \\]

图 14-9   考虑约束下的递推关系

最终,返回 \\(dp[n, 1] + dp[n, 2]\\) 即可,两者之和代表爬到第 \\(n\\) 阶的方案总数:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_constraint_dp.py
def climbing_stairs_constraint_dp(n: int) -> int:\n    \"\"\"带约束爬楼梯:动态规划\"\"\"\n    if n == 1 or n == 2:\n        return 1\n    # 初始化 dp 表,用于存储子问题的解\n    dp = [[0] * 3 for _ in range(n + 1)]\n    # 初始状态:预设最小子问题的解\n    dp[1][1], dp[1][2] = 1, 0\n    dp[2][1], dp[2][2] = 0, 1\n    # 状态转移:从较小子问题逐步求解较大子问题\n    for i in range(3, n + 1):\n        dp[i][1] = dp[i - 1][2]\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2]\n    return dp[n][1] + dp[n][2]\n
climbing_stairs_constraint_dp.cpp
/* 带约束爬楼梯:动态规划 */\nint climbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // 初始化 dp 表,用于存储子问题的解\n    vector<vector<int>> dp(n + 1, vector<int>(3, 0));\n    // 初始状态:预设最小子问题的解\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // 状态转移:从较小子问题逐步求解较大子问题\n    for (int i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    return dp[n][1] + dp[n][2];\n}\n
climbing_stairs_constraint_dp.java
/* 带约束爬楼梯:动态规划 */\nint climbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // 初始化 dp 表,用于存储子问题的解\n    int[][] dp = new int[n + 1][3];\n    // 初始状态:预设最小子问题的解\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // 状态转移:从较小子问题逐步求解较大子问题\n    for (int i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    return dp[n][1] + dp[n][2];\n}\n
climbing_stairs_constraint_dp.cs
/* 带约束爬楼梯:动态规划 */\nint ClimbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // 初始化 dp 表,用于存储子问题的解\n    int[,] dp = new int[n + 1, 3];\n    // 初始状态:预设最小子问题的解\n    dp[1, 1] = 1;\n    dp[1, 2] = 0;\n    dp[2, 1] = 0;\n    dp[2, 2] = 1;\n    // 状态转移:从较小子问题逐步求解较大子问题\n    for (int i = 3; i <= n; i++) {\n        dp[i, 1] = dp[i - 1, 2];\n        dp[i, 2] = dp[i - 2, 1] + dp[i - 2, 2];\n    }\n    return dp[n, 1] + dp[n, 2];\n}\n
climbing_stairs_constraint_dp.go
/* 带约束爬楼梯:动态规划 */\nfunc climbingStairsConstraintDP(n int) int {\n    if n == 1 || n == 2 {\n        return 1\n    }\n    // 初始化 dp 表,用于存储子问题的解\n    dp := make([][3]int, n+1)\n    // 初始状态:预设最小子问题的解\n    dp[1][1] = 1\n    dp[1][2] = 0\n    dp[2][1] = 0\n    dp[2][2] = 1\n    // 状态转移:从较小子问题逐步求解较大子问题\n    for i := 3; i <= n; i++ {\n        dp[i][1] = dp[i-1][2]\n        dp[i][2] = dp[i-2][1] + dp[i-2][2]\n    }\n    return dp[n][1] + dp[n][2]\n}\n
climbing_stairs_constraint_dp.swift
/* 带约束爬楼梯:动态规划 */\nfunc climbingStairsConstraintDP(n: Int) -> Int {\n    if n == 1 || n == 2 {\n        return 1\n    }\n    // 初始化 dp 表,用于存储子问题的解\n    var dp = Array(repeating: Array(repeating: 0, count: 3), count: n + 1)\n    // 初始状态:预设最小子问题的解\n    dp[1][1] = 1\n    dp[1][2] = 0\n    dp[2][1] = 0\n    dp[2][2] = 1\n    // 状态转移:从较小子问题逐步求解较大子问题\n    for i in 3 ... n {\n        dp[i][1] = dp[i - 1][2]\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2]\n    }\n    return dp[n][1] + dp[n][2]\n}\n
climbing_stairs_constraint_dp.js
/* 带约束爬楼梯:动态规划 */\nfunction climbingStairsConstraintDP(n) {\n    if (n === 1 || n === 2) {\n        return 1;\n    }\n    // 初始化 dp 表,用于存储子问题的解\n    const dp = Array.from(new Array(n + 1), () => new Array(3));\n    // 初始状态:预设最小子问题的解\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // 状态转移:从较小子问题逐步求解较大子问题\n    for (let i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    return dp[n][1] + dp[n][2];\n}\n
climbing_stairs_constraint_dp.ts
/* 带约束爬楼梯:动态规划 */\nfunction climbingStairsConstraintDP(n: number): number {\n    if (n === 1 || n === 2) {\n        return 1;\n    }\n    // 初始化 dp 表,用于存储子问题的解\n    const dp = Array.from({ length: n + 1 }, () => new Array(3));\n    // 初始状态:预设最小子问题的解\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // 状态转移:从较小子问题逐步求解较大子问题\n    for (let i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    return dp[n][1] + dp[n][2];\n}\n
climbing_stairs_constraint_dp.dart
/* 带约束爬楼梯:动态规划 */\nint climbingStairsConstraintDP(int n) {\n  if (n == 1 || n == 2) {\n    return 1;\n  }\n  // 初始化 dp 表,用于存储子问题的解\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(3, 0));\n  // 初始状态:预设最小子问题的解\n  dp[1][1] = 1;\n  dp[1][2] = 0;\n  dp[2][1] = 0;\n  dp[2][2] = 1;\n  // 状态转移:从较小子问题逐步求解较大子问题\n  for (int i = 3; i <= n; i++) {\n    dp[i][1] = dp[i - 1][2];\n    dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n  }\n  return dp[n][1] + dp[n][2];\n}\n
climbing_stairs_constraint_dp.rs
/* 带约束爬楼梯:动态规划 */\nfn climbing_stairs_constraint_dp(n: usize) -> i32 {\n    if n == 1 || n == 2 {\n        return 1;\n    };\n    // 初始化 dp 表,用于存储子问题的解\n    let mut dp = vec![vec![-1; 3]; n + 1];\n    // 初始状态:预设最小子问题的解\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // 状态转移:从较小子问题逐步求解较大子问题\n    for i in 3..=n {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    dp[n][1] + dp[n][2]\n}\n
climbing_stairs_constraint_dp.c
/* 带约束爬楼梯:动态规划 */\nint climbingStairsConstraintDP(int n) {\n    if (n == 1 || n == 2) {\n        return 1;\n    }\n    // 初始化 dp 表,用于存储子问题的解\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(3, sizeof(int));\n    }\n    // 初始状态:预设最小子问题的解\n    dp[1][1] = 1;\n    dp[1][2] = 0;\n    dp[2][1] = 0;\n    dp[2][2] = 1;\n    // 状态转移:从较小子问题逐步求解较大子问题\n    for (int i = 3; i <= n; i++) {\n        dp[i][1] = dp[i - 1][2];\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2];\n    }\n    int res = dp[n][1] + dp[n][2];\n    // 释放内存\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    free(dp);\n    return res;\n}\n
climbing_stairs_constraint_dp.kt
/* 带约束爬楼梯:动态规划 */\nfun climbingStairsConstraintDP(n: Int): Int {\n    if (n == 1 || n == 2) {\n        return 1\n    }\n    // 初始化 dp 表,用于存储子问题的解\n    val dp = Array(n + 1) { IntArray(3) }\n    // 初始状态:预设最小子问题的解\n    dp[1][1] = 1\n    dp[1][2] = 0\n    dp[2][1] = 0\n    dp[2][2] = 1\n    // 状态转移:从较小子问题逐步求解较大子问题\n    for (i in 3..n) {\n        dp[i][1] = dp[i - 1][2]\n        dp[i][2] = dp[i - 2][1] + dp[i - 2][2]\n    }\n    return dp[n][1] + dp[n][2]\n}\n
climbing_stairs_constraint_dp.rb
### 带约束爬楼梯:动态规划 ###\ndef climbing_stairs_constraint_dp(n)\n  return 1 if n == 1 || n == 2\n\n  # 初始化 dp 表,用于存储子问题的解\n  dp = Array.new(n + 1) { Array.new(3, 0) }\n  # 初始状态:预设最小子问题的解\n  dp[1][1], dp[1][2] = 1, 0\n  dp[2][1], dp[2][2] = 0, 1\n  # 状态转移:从较小子问题逐步求解较大子问题\n  for i in 3...(n + 1)\n    dp[i][1] = dp[i - 1][2]\n    dp[i][2] = dp[i - 2][1] + dp[i - 2][2]\n  end\n\n  dp[n][1] + dp[n][2]\nend\n
可视化运行

全屏观看 >

在上面的案例中,由于仅需多考虑前面一个状态,因此我们仍然可以通过扩展状态定义,使得问题重新满足无后效性。然而,某些问题具有非常严重的“有后效性”。

爬楼梯与障碍生成

给定一个共有 \\(n\\) 阶的楼梯,你每步可以上 \\(1\\) 阶或者 \\(2\\) 阶。规定当爬到第 \\(i\\) 阶时,系统自动会在第 \\(2i\\) 阶上放上障碍物,之后所有轮都不允许跳到第 \\(2i\\) 阶上。例如,前两轮分别跳到了第 \\(2\\)、\\(3\\) 阶上,则之后就不能跳到第 \\(4\\)、\\(6\\) 阶上。请问有多少种方案可以爬到楼顶?

在这个问题中,下次跳跃依赖过去所有的状态,因为每一次跳跃都会在更高的阶梯上设置障碍,并影响未来的跳跃。对于这类问题,动态规划往往难以解决。

实际上,许多复杂的组合优化问题(例如旅行商问题)不满足无后效性。对于这类问题,我们通常会选择使用其他方法,例如启发式搜索、遗传算法、强化学习等,从而在有限时间内得到可用的局部最优解。

","path":["第 14 章   动态规划","14.2   动态规划问题特性"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/","level":1,"title":"14.3   动态规划解题思路","text":"

上两节介绍了动态规划问题的主要特征,接下来我们一起探究两个更加实用的问题。

  1. 如何判断一个问题是不是动态规划问题?
  2. 求解动态规划问题该从何处入手,完整步骤是什么?
","path":["第 14 章   动态规划","14.3   动态规划解题思路"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1431","level":2,"title":"14.3.1   问题判断","text":"

总的来说,如果一个问题包含重叠子问题、最优子结构,并满足无后效性,那么它通常适合用动态规划求解。然而,我们很难从问题描述中直接提取出这些特性。因此我们通常会放宽条件,先观察问题是否适合使用回溯(穷举)解决。

适合用回溯解决的问题通常满足“决策树模型”,这种问题可以使用树形结构来描述,其中每一个节点代表一个决策,每一条路径代表一个决策序列。

换句话说,如果问题包含明确的决策概念,并且解是通过一系列决策产生的,那么它就满足决策树模型,通常可以使用回溯来解决。

在此基础上,动态规划问题还有一些判断的“加分项”。

相应地,也存在一些“减分项”。

如果一个问题满足决策树模型,并具有较为明显的“加分项”,我们就可以假设它是一个动态规划问题,并在求解过程中验证它。

","path":["第 14 章   动态规划","14.3   动态规划解题思路"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1432","level":2,"title":"14.3.2   问题求解步骤","text":"

动态规划的解题流程会因问题的性质和难度而有所不同,但通常遵循以下步骤:描述决策,定义状态,建立 \\(dp\\) 表,推导状态转移方程,确定边界条件等。

为了更形象地展示解题步骤,我们使用一个经典问题“最小路径和”来举例。

Question

给定一个 \\(n \\times m\\) 的二维网格 grid ,网格中的每个单元格包含一个非负整数,表示该单元格的代价。机器人以左上角单元格为起始点,每次只能向下或者向右移动一步,直至到达右下角单元格。请返回从左上角到右下角的最小路径和。

图 14-10 展示了一个例子,给定网格的最小路径和为 \\(13\\) 。

图 14-10   最小路径和示例数据

第一步:思考每轮的决策,定义状态,从而得到 \\(dp\\) 表

本题的每一轮的决策就是从当前格子向下或向右走一步。设当前格子的行列索引为 \\([i, j]\\) ,则向下或向右走一步后,索引变为 \\([i+1, j]\\) 或 \\([i, j+1]\\) 。因此,状态应包含行索引和列索引两个变量,记为 \\([i, j]\\) 。

状态 \\([i, j]\\) 对应的子问题为:从起始点 \\([0, 0]\\) 走到 \\([i, j]\\) 的最小路径和,解记为 \\(dp[i, j]\\) 。

至此,我们就得到了图 14-11 所示的二维 \\(dp\\) 矩阵,其尺寸与输入网格 \\(grid\\) 相同。

图 14-11   状态定义与 dp 表

Note

动态规划和回溯过程可以描述为一个决策序列,而状态由所有决策变量构成。它应当包含描述解题进度的所有变量,其包含了足够的信息,能够用来推导出下一个状态。

每个状态都对应一个子问题,我们会定义一个 \\(dp\\) 表来存储所有子问题的解,状态的每个独立变量都是 \\(dp\\) 表的一个维度。从本质上看,\\(dp\\) 表是状态和子问题的解之间的映射。

第二步:找出最优子结构,进而推导出状态转移方程

对于状态 \\([i, j]\\) ,它只能从上边格子 \\([i-1, j]\\) 和左边格子 \\([i, j-1]\\) 转移而来。因此最优子结构为:到达 \\([i, j]\\) 的最小路径和由 \\([i, j-1]\\) 的最小路径和与 \\([i-1, j]\\) 的最小路径和中较小的那一个决定。

根据以上分析,可推出图 14-12 所示的状态转移方程:

\\[ dp[i, j] = \\min(dp[i-1, j], dp[i, j-1]) + grid[i, j] \\]

图 14-12   最优子结构与状态转移方程

Note

根据定义好的 \\(dp\\) 表,思考原问题和子问题的关系,找出通过子问题的最优解来构造原问题的最优解的方法,即最优子结构。

一旦我们找到了最优子结构,就可以使用它来构建出状态转移方程。

第三步:确定边界条件和状态转移顺序

在本题中,处在首行的状态只能从其左边的状态得来,处在首列的状态只能从其上边的状态得来,因此首行 \\(i = 0\\) 和首列 \\(j = 0\\) 是边界条件。

如图 14-13 所示,由于每个格子是由其左方格子和上方格子转移而来,因此我们使用循环来遍历矩阵,外循环遍历各行,内循环遍历各列。

图 14-13   边界条件与状态转移顺序

Note

边界条件在动态规划中用于初始化 \\(dp\\) 表,在搜索中用于剪枝。

状态转移顺序的核心是要保证在计算当前问题的解时,所有它依赖的更小子问题的解都已经被正确地计算出来。

根据以上分析,我们已经可以直接写出动态规划代码。然而子问题分解是一种从顶至底的思想,因此按照“暴力搜索 \\(\\rightarrow\\) 记忆化搜索 \\(\\rightarrow\\) 动态规划”的顺序实现更加符合思维习惯。

","path":["第 14 章   动态规划","14.3   动态规划解题思路"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#1","level":3,"title":"1.   方法一:暴力搜索","text":"

从状态 \\([i, j]\\) 开始搜索,不断分解为更小的状态 \\([i-1, j]\\) 和 \\([i, j-1]\\) ,递归函数包括以下要素。

实现代码如下:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
def min_path_sum_dfs(grid: list[list[int]], i: int, j: int) -> int:\n    \"\"\"最小路径和:暴力搜索\"\"\"\n    # 若为左上角单元格,则终止搜索\n    if i == 0 and j == 0:\n        return grid[0][0]\n    # 若行列索引越界,则返回 +∞ 代价\n    if i < 0 or j < 0:\n        return inf\n    # 计算从左上角到 (i-1, j) 和 (i, j-1) 的最小路径代价\n    up = min_path_sum_dfs(grid, i - 1, j)\n    left = min_path_sum_dfs(grid, i, j - 1)\n    # 返回从左上角到 (i, j) 的最小路径代价\n    return min(left, up) + grid[i][j]\n
min_path_sum.cpp
/* 最小路径和:暴力搜索 */\nint minPathSumDFS(vector<vector<int>> &grid, int i, int j) {\n    // 若为左上角单元格,则终止搜索\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // 若行列索引越界,则返回 +∞ 代价\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // 计算从左上角到 (i-1, j) 和 (i, j-1) 的最小路径代价\n    int up = minPathSumDFS(grid, i - 1, j);\n    int left = minPathSumDFS(grid, i, j - 1);\n    // 返回从左上角到 (i, j) 的最小路径代价\n    return min(left, up) != INT_MAX ? min(left, up) + grid[i][j] : INT_MAX;\n}\n
min_path_sum.java
/* 最小路径和:暴力搜索 */\nint minPathSumDFS(int[][] grid, int i, int j) {\n    // 若为左上角单元格,则终止搜索\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // 若行列索引越界,则返回 +∞ 代价\n    if (i < 0 || j < 0) {\n        return Integer.MAX_VALUE;\n    }\n    // 计算从左上角到 (i-1, j) 和 (i, j-1) 的最小路径代价\n    int up = minPathSumDFS(grid, i - 1, j);\n    int left = minPathSumDFS(grid, i, j - 1);\n    // 返回从左上角到 (i, j) 的最小路径代价\n    return Math.min(left, up) + grid[i][j];\n}\n
min_path_sum.cs
/* 最小路径和:暴力搜索 */\nint MinPathSumDFS(int[][] grid, int i, int j) {\n    // 若为左上角单元格,则终止搜索\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // 若行列索引越界,则返回 +∞ 代价\n    if (i < 0 || j < 0) {\n        return int.MaxValue;\n    }\n    // 计算从左上角到 (i-1, j) 和 (i, j-1) 的最小路径代价\n    int up = MinPathSumDFS(grid, i - 1, j);\n    int left = MinPathSumDFS(grid, i, j - 1);\n    // 返回从左上角到 (i, j) 的最小路径代价\n    return Math.Min(left, up) + grid[i][j];\n}\n
min_path_sum.go
/* 最小路径和:暴力搜索 */\nfunc minPathSumDFS(grid [][]int, i, j int) int {\n    // 若为左上角单元格,则终止搜索\n    if i == 0 && j == 0 {\n        return grid[0][0]\n    }\n    // 若行列索引越界,则返回 +∞ 代价\n    if i < 0 || j < 0 {\n        return math.MaxInt\n    }\n    // 计算从左上角到 (i-1, j) 和 (i, j-1) 的最小路径代价\n    up := minPathSumDFS(grid, i-1, j)\n    left := minPathSumDFS(grid, i, j-1)\n    // 返回从左上角到 (i, j) 的最小路径代价\n    return int(math.Min(float64(left), float64(up))) + grid[i][j]\n}\n
min_path_sum.swift
/* 最小路径和:暴力搜索 */\nfunc minPathSumDFS(grid: [[Int]], i: Int, j: Int) -> Int {\n    // 若为左上角单元格,则终止搜索\n    if i == 0, j == 0 {\n        return grid[0][0]\n    }\n    // 若行列索引越界,则返回 +∞ 代价\n    if i < 0 || j < 0 {\n        return .max\n    }\n    // 计算从左上角到 (i-1, j) 和 (i, j-1) 的最小路径代价\n    let up = minPathSumDFS(grid: grid, i: i - 1, j: j)\n    let left = minPathSumDFS(grid: grid, i: i, j: j - 1)\n    // 返回从左上角到 (i, j) 的最小路径代价\n    return min(left, up) + grid[i][j]\n}\n
min_path_sum.js
/* 最小路径和:暴力搜索 */\nfunction minPathSumDFS(grid, i, j) {\n    // 若为左上角单元格,则终止搜索\n    if (i === 0 && j === 0) {\n        return grid[0][0];\n    }\n    // 若行列索引越界,则返回 +∞ 代价\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // 计算从左上角到 (i-1, j) 和 (i, j-1) 的最小路径代价\n    const up = minPathSumDFS(grid, i - 1, j);\n    const left = minPathSumDFS(grid, i, j - 1);\n    // 返回从左上角到 (i, j) 的最小路径代价\n    return Math.min(left, up) + grid[i][j];\n}\n
min_path_sum.ts
/* 最小路径和:暴力搜索 */\nfunction minPathSumDFS(\n    grid: Array<Array<number>>,\n    i: number,\n    j: number\n): number {\n    // 若为左上角单元格,则终止搜索\n    if (i === 0 && j == 0) {\n        return grid[0][0];\n    }\n    // 若行列索引越界,则返回 +∞ 代价\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // 计算从左上角到 (i-1, j) 和 (i, j-1) 的最小路径代价\n    const up = minPathSumDFS(grid, i - 1, j);\n    const left = minPathSumDFS(grid, i, j - 1);\n    // 返回从左上角到 (i, j) 的最小路径代价\n    return Math.min(left, up) + grid[i][j];\n}\n
min_path_sum.dart
/* 最小路径和:暴力搜索 */\nint minPathSumDFS(List<List<int>> grid, int i, int j) {\n  // 若为左上角单元格,则终止搜索\n  if (i == 0 && j == 0) {\n    return grid[0][0];\n  }\n  // 若行列索引越界,则返回 +∞ 代价\n  if (i < 0 || j < 0) {\n    // 在 Dart 中,int 类型是固定范围的整数,不存在表示“无穷大”的值\n    return BigInt.from(2).pow(31).toInt();\n  }\n  // 计算从左上角到 (i-1, j) 和 (i, j-1) 的最小路径代价\n  int up = minPathSumDFS(grid, i - 1, j);\n  int left = minPathSumDFS(grid, i, j - 1);\n  // 返回从左上角到 (i, j) 的最小路径代价\n  return min(left, up) + grid[i][j];\n}\n
min_path_sum.rs
/* 最小路径和:暴力搜索 */\nfn min_path_sum_dfs(grid: &Vec<Vec<i32>>, i: i32, j: i32) -> i32 {\n    // 若为左上角单元格,则终止搜索\n    if i == 0 && j == 0 {\n        return grid[0][0];\n    }\n    // 若行列索引越界,则返回 +∞ 代价\n    if i < 0 || j < 0 {\n        return i32::MAX;\n    }\n    // 计算从左上角到 (i-1, j) 和 (i, j-1) 的最小路径代价\n    let up = min_path_sum_dfs(grid, i - 1, j);\n    let left = min_path_sum_dfs(grid, i, j - 1);\n    // 返回从左上角到 (i, j) 的最小路径代价\n    std::cmp::min(left, up) + grid[i as usize][j as usize]\n}\n
min_path_sum.c
/* 最小路径和:暴力搜索 */\nint minPathSumDFS(int grid[MAX_SIZE][MAX_SIZE], int i, int j) {\n    // 若为左上角单元格,则终止搜索\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // 若行列索引越界,则返回 +∞ 代价\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // 计算从左上角到 (i-1, j) 和 (i, j-1) 的最小路径代价\n    int up = minPathSumDFS(grid, i - 1, j);\n    int left = minPathSumDFS(grid, i, j - 1);\n    // 返回从左上角到 (i, j) 的最小路径代价\n    return myMin(left, up) != INT_MAX ? myMin(left, up) + grid[i][j] : INT_MAX;\n}\n
min_path_sum.kt
/* 最小路径和:暴力搜索 */\nfun minPathSumDFS(grid: Array<IntArray>, i: Int, j: Int): Int {\n    // 若为左上角单元格,则终止搜索\n    if (i == 0 && j == 0) {\n        return grid[0][0]\n    }\n    // 若行列索引越界,则返回 +∞ 代价\n    if (i < 0 || j < 0) {\n        return Int.MAX_VALUE\n    }\n    // 计算从左上角到 (i-1, j) 和 (i, j-1) 的最小路径代价\n    val up = minPathSumDFS(grid, i - 1, j)\n    val left = minPathSumDFS(grid, i, j - 1)\n    // 返回从左上角到 (i, j) 的最小路径代价\n    return min(left, up) + grid[i][j]\n}\n
min_path_sum.rb
### 最小路径和:暴力搜索 ###\ndef min_path_sum_dfs(grid, i, j)\n  # 若为左上角单元格,则终止搜索\n  return grid[i][j] if i == 0 && j == 0\n  # 若行列索引越界,则返回 +∞ 代价\n  return Float::INFINITY if i < 0 || j < 0\n  # 计算从左上角到 (i-1, j) 和 (i, j-1) 的最小路径代价\n  up = min_path_sum_dfs(grid, i - 1, j)\n  left = min_path_sum_dfs(grid, i, j - 1)\n  # 返回从左上角到 (i, j) 的最小路径代价\n  [left, up].min + grid[i][j]\nend\n
可视化运行

全屏观看 >

图 14-14 给出了以 \\(dp[2, 1]\\) 为根节点的递归树,其中包含一些重叠子问题,其数量会随着网格 grid 的尺寸变大而急剧增多。

从本质上看,造成重叠子问题的原因为:存在多条路径可以从左上角到达某一单元格。

图 14-14   暴力搜索递归树

每个状态都有向下和向右两种选择,从左上角走到右下角总共需要 \\(m + n - 2\\) 步,所以最差时间复杂度为 \\(O(2^{m + n})\\) ,其中 \\(n\\) 和 \\(m\\) 分别为网格的行数和列数。请注意,这种计算方式未考虑临近网格边界的情况,当到达网格边界时只剩下一种选择,因此实际的路径数量会少一些。

","path":["第 14 章   动态规划","14.3   动态规划解题思路"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#2","level":3,"title":"2.   方法二:记忆化搜索","text":"

我们引入一个和网格 grid 相同尺寸的记忆列表 mem ,用于记录各个子问题的解,并将重叠子问题进行剪枝:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
def min_path_sum_dfs_mem(\n    grid: list[list[int]], mem: list[list[int]], i: int, j: int\n) -> int:\n    \"\"\"最小路径和:记忆化搜索\"\"\"\n    # 若为左上角单元格,则终止搜索\n    if i == 0 and j == 0:\n        return grid[0][0]\n    # 若行列索引越界,则返回 +∞ 代价\n    if i < 0 or j < 0:\n        return inf\n    # 若已有记录,则直接返回\n    if mem[i][j] != -1:\n        return mem[i][j]\n    # 左边和上边单元格的最小路径代价\n    up = min_path_sum_dfs_mem(grid, mem, i - 1, j)\n    left = min_path_sum_dfs_mem(grid, mem, i, j - 1)\n    # 记录并返回左上角到 (i, j) 的最小路径代价\n    mem[i][j] = min(left, up) + grid[i][j]\n    return mem[i][j]\n
min_path_sum.cpp
/* 最小路径和:记忆化搜索 */\nint minPathSumDFSMem(vector<vector<int>> &grid, vector<vector<int>> &mem, int i, int j) {\n    // 若为左上角单元格,则终止搜索\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // 若行列索引越界,则返回 +∞ 代价\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // 若已有记录,则直接返回\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // 左边和上边单元格的最小路径代价\n    int up = minPathSumDFSMem(grid, mem, i - 1, j);\n    int left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // 记录并返回左上角到 (i, j) 的最小路径代价\n    mem[i][j] = min(left, up) != INT_MAX ? min(left, up) + grid[i][j] : INT_MAX;\n    return mem[i][j];\n}\n
min_path_sum.java
/* 最小路径和:记忆化搜索 */\nint minPathSumDFSMem(int[][] grid, int[][] mem, int i, int j) {\n    // 若为左上角单元格,则终止搜索\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // 若行列索引越界,则返回 +∞ 代价\n    if (i < 0 || j < 0) {\n        return Integer.MAX_VALUE;\n    }\n    // 若已有记录,则直接返回\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // 左边和上边单元格的最小路径代价\n    int up = minPathSumDFSMem(grid, mem, i - 1, j);\n    int left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // 记录并返回左上角到 (i, j) 的最小路径代价\n    mem[i][j] = Math.min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
min_path_sum.cs
/* 最小路径和:记忆化搜索 */\nint MinPathSumDFSMem(int[][] grid, int[][] mem, int i, int j) {\n    // 若为左上角单元格,则终止搜索\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // 若行列索引越界,则返回 +∞ 代价\n    if (i < 0 || j < 0) {\n        return int.MaxValue;\n    }\n    // 若已有记录,则直接返回\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // 左边和上边单元格的最小路径代价\n    int up = MinPathSumDFSMem(grid, mem, i - 1, j);\n    int left = MinPathSumDFSMem(grid, mem, i, j - 1);\n    // 记录并返回左上角到 (i, j) 的最小路径代价\n    mem[i][j] = Math.Min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
min_path_sum.go
/* 最小路径和:记忆化搜索 */\nfunc minPathSumDFSMem(grid, mem [][]int, i, j int) int {\n    // 若为左上角单元格,则终止搜索\n    if i == 0 && j == 0 {\n        return grid[0][0]\n    }\n    // 若行列索引越界,则返回 +∞ 代价\n    if i < 0 || j < 0 {\n        return math.MaxInt\n    }\n    // 若已有记录,则直接返回\n    if mem[i][j] != -1 {\n        return mem[i][j]\n    }\n    // 左边和上边单元格的最小路径代价\n    up := minPathSumDFSMem(grid, mem, i-1, j)\n    left := minPathSumDFSMem(grid, mem, i, j-1)\n    // 记录并返回左上角到 (i, j) 的最小路径代价\n    mem[i][j] = int(math.Min(float64(left), float64(up))) + grid[i][j]\n    return mem[i][j]\n}\n
min_path_sum.swift
/* 最小路径和:记忆化搜索 */\nfunc minPathSumDFSMem(grid: [[Int]], mem: inout [[Int]], i: Int, j: Int) -> Int {\n    // 若为左上角单元格,则终止搜索\n    if i == 0, j == 0 {\n        return grid[0][0]\n    }\n    // 若行列索引越界,则返回 +∞ 代价\n    if i < 0 || j < 0 {\n        return .max\n    }\n    // 若已有记录,则直接返回\n    if mem[i][j] != -1 {\n        return mem[i][j]\n    }\n    // 左边和上边单元格的最小路径代价\n    let up = minPathSumDFSMem(grid: grid, mem: &mem, i: i - 1, j: j)\n    let left = minPathSumDFSMem(grid: grid, mem: &mem, i: i, j: j - 1)\n    // 记录并返回左上角到 (i, j) 的最小路径代价\n    mem[i][j] = min(left, up) + grid[i][j]\n    return mem[i][j]\n}\n
min_path_sum.js
/* 最小路径和:记忆化搜索 */\nfunction minPathSumDFSMem(grid, mem, i, j) {\n    // 若为左上角单元格,则终止搜索\n    if (i === 0 && j === 0) {\n        return grid[0][0];\n    }\n    // 若行列索引越界,则返回 +∞ 代价\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // 若已有记录,则直接返回\n    if (mem[i][j] !== -1) {\n        return mem[i][j];\n    }\n    // 左边和上边单元格的最小路径代价\n    const up = minPathSumDFSMem(grid, mem, i - 1, j);\n    const left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // 记录并返回左上角到 (i, j) 的最小路径代价\n    mem[i][j] = Math.min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
min_path_sum.ts
/* 最小路径和:记忆化搜索 */\nfunction minPathSumDFSMem(\n    grid: Array<Array<number>>,\n    mem: Array<Array<number>>,\n    i: number,\n    j: number\n): number {\n    // 若为左上角单元格,则终止搜索\n    if (i === 0 && j === 0) {\n        return grid[0][0];\n    }\n    // 若行列索引越界,则返回 +∞ 代价\n    if (i < 0 || j < 0) {\n        return Infinity;\n    }\n    // 若已有记录,则直接返回\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // 左边和上边单元格的最小路径代价\n    const up = minPathSumDFSMem(grid, mem, i - 1, j);\n    const left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // 记录并返回左上角到 (i, j) 的最小路径代价\n    mem[i][j] = Math.min(left, up) + grid[i][j];\n    return mem[i][j];\n}\n
min_path_sum.dart
/* 最小路径和:记忆化搜索 */\nint minPathSumDFSMem(List<List<int>> grid, List<List<int>> mem, int i, int j) {\n  // 若为左上角单元格,则终止搜索\n  if (i == 0 && j == 0) {\n    return grid[0][0];\n  }\n  // 若行列索引越界,则返回 +∞ 代价\n  if (i < 0 || j < 0) {\n    // 在 Dart 中,int 类型是固定范围的整数,不存在表示“无穷大”的值\n    return BigInt.from(2).pow(31).toInt();\n  }\n  // 若已有记录,则直接返回\n  if (mem[i][j] != -1) {\n    return mem[i][j];\n  }\n  // 左边和上边单元格的最小路径代价\n  int up = minPathSumDFSMem(grid, mem, i - 1, j);\n  int left = minPathSumDFSMem(grid, mem, i, j - 1);\n  // 记录并返回左上角到 (i, j) 的最小路径代价\n  mem[i][j] = min(left, up) + grid[i][j];\n  return mem[i][j];\n}\n
min_path_sum.rs
/* 最小路径和:记忆化搜索 */\nfn min_path_sum_dfs_mem(grid: &Vec<Vec<i32>>, mem: &mut Vec<Vec<i32>>, i: i32, j: i32) -> i32 {\n    // 若为左上角单元格,则终止搜索\n    if i == 0 && j == 0 {\n        return grid[0][0];\n    }\n    // 若行列索引越界,则返回 +∞ 代价\n    if i < 0 || j < 0 {\n        return i32::MAX;\n    }\n    // 若已有记录,则直接返回\n    if mem[i as usize][j as usize] != -1 {\n        return mem[i as usize][j as usize];\n    }\n    // 左边和上边单元格的最小路径代价\n    let up = min_path_sum_dfs_mem(grid, mem, i - 1, j);\n    let left = min_path_sum_dfs_mem(grid, mem, i, j - 1);\n    // 记录并返回左上角到 (i, j) 的最小路径代价\n    mem[i as usize][j as usize] = std::cmp::min(left, up) + grid[i as usize][j as usize];\n    mem[i as usize][j as usize]\n}\n
min_path_sum.c
/* 最小路径和:记忆化搜索 */\nint minPathSumDFSMem(int grid[MAX_SIZE][MAX_SIZE], int mem[MAX_SIZE][MAX_SIZE], int i, int j) {\n    // 若为左上角单元格,则终止搜索\n    if (i == 0 && j == 0) {\n        return grid[0][0];\n    }\n    // 若行列索引越界,则返回 +∞ 代价\n    if (i < 0 || j < 0) {\n        return INT_MAX;\n    }\n    // 若已有记录,则直接返回\n    if (mem[i][j] != -1) {\n        return mem[i][j];\n    }\n    // 左边和上边单元格的最小路径代价\n    int up = minPathSumDFSMem(grid, mem, i - 1, j);\n    int left = minPathSumDFSMem(grid, mem, i, j - 1);\n    // 记录并返回左上角到 (i, j) 的最小路径代价\n    mem[i][j] = myMin(left, up) != INT_MAX ? myMin(left, up) + grid[i][j] : INT_MAX;\n    return mem[i][j];\n}\n
min_path_sum.kt
/* 最小路径和:记忆化搜索 */\nfun minPathSumDFSMem(\n    grid: Array<IntArray>,\n    mem: Array<IntArray>,\n    i: Int,\n    j: Int\n): Int {\n    // 若为左上角单元格,则终止搜索\n    if (i == 0 && j == 0) {\n        return grid[0][0]\n    }\n    // 若行列索引越界,则返回 +∞ 代价\n    if (i < 0 || j < 0) {\n        return Int.MAX_VALUE\n    }\n    // 若已有记录,则直接返回\n    if (mem[i][j] != -1) {\n        return mem[i][j]\n    }\n    // 左边和上边单元格的最小路径代价\n    val up = minPathSumDFSMem(grid, mem, i - 1, j)\n    val left = minPathSumDFSMem(grid, mem, i, j - 1)\n    // 记录并返回左上角到 (i, j) 的最小路径代价\n    mem[i][j] = min(left, up) + grid[i][j]\n    return mem[i][j]\n}\n
min_path_sum.rb
### 最小路径和:记忆化搜索 ###\ndef min_path_sum_dfs_mem(grid, mem, i, j)\n  # 若为左上角单元格,则终止搜索\n  return grid[0][0] if i == 0 && j == 0\n  # 若行列索引越界,则返回 +∞ 代价\n  return Float::INFINITY if i < 0 || j < 0\n  # 若已有记录,则直接返回\n  return mem[i][j] if mem[i][j] != -1\n  # 左边和上边单元格的最小路径代价\n  up = min_path_sum_dfs_mem(grid, mem, i - 1, j)\n  left = min_path_sum_dfs_mem(grid, mem, i, j - 1)\n  # 记录并返回左上角到 (i, j) 的最小路径代价\n  mem[i][j] = [left, up].min + grid[i][j]\nend\n
可视化运行

全屏观看 >

如图 14-15 所示,在引入记忆化后,所有子问题的解只需计算一次,因此时间复杂度取决于状态总数,即网格尺寸 \\(O(nm)\\) 。

图 14-15   记忆化搜索递归树

","path":["第 14 章   动态规划","14.3   动态规划解题思路"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#3","level":3,"title":"3.   方法三:动态规划","text":"

基于迭代实现动态规划解法,代码如下所示:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
def min_path_sum_dp(grid: list[list[int]]) -> int:\n    \"\"\"最小路径和:动态规划\"\"\"\n    n, m = len(grid), len(grid[0])\n    # 初始化 dp 表\n    dp = [[0] * m for _ in range(n)]\n    dp[0][0] = grid[0][0]\n    # 状态转移:首行\n    for j in range(1, m):\n        dp[0][j] = dp[0][j - 1] + grid[0][j]\n    # 状态转移:首列\n    for i in range(1, n):\n        dp[i][0] = dp[i - 1][0] + grid[i][0]\n    # 状态转移:其余行和列\n    for i in range(1, n):\n        for j in range(1, m):\n            dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]\n    return dp[n - 1][m - 1]\n
min_path_sum.cpp
/* 最小路径和:动态规划 */\nint minPathSumDP(vector<vector<int>> &grid) {\n    int n = grid.size(), m = grid[0].size();\n    // 初始化 dp 表\n    vector<vector<int>> dp(n, vector<int>(m));\n    dp[0][0] = grid[0][0];\n    // 状态转移:首行\n    for (int j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // 状态转移:首列\n    for (int i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // 状态转移:其余行和列\n    for (int i = 1; i < n; i++) {\n        for (int j = 1; j < m; j++) {\n            dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1][m - 1];\n}\n
min_path_sum.java
/* 最小路径和:动态规划 */\nint minPathSumDP(int[][] grid) {\n    int n = grid.length, m = grid[0].length;\n    // 初始化 dp 表\n    int[][] dp = new int[n][m];\n    dp[0][0] = grid[0][0];\n    // 状态转移:首行\n    for (int j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // 状态转移:首列\n    for (int i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // 状态转移:其余行和列\n    for (int i = 1; i < n; i++) {\n        for (int j = 1; j < m; j++) {\n            dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1][m - 1];\n}\n
min_path_sum.cs
/* 最小路径和:动态规划 */\nint MinPathSumDP(int[][] grid) {\n    int n = grid.Length, m = grid[0].Length;\n    // 初始化 dp 表\n    int[,] dp = new int[n, m];\n    dp[0, 0] = grid[0][0];\n    // 状态转移:首行\n    for (int j = 1; j < m; j++) {\n        dp[0, j] = dp[0, j - 1] + grid[0][j];\n    }\n    // 状态转移:首列\n    for (int i = 1; i < n; i++) {\n        dp[i, 0] = dp[i - 1, 0] + grid[i][0];\n    }\n    // 状态转移:其余行和列\n    for (int i = 1; i < n; i++) {\n        for (int j = 1; j < m; j++) {\n            dp[i, j] = Math.Min(dp[i, j - 1], dp[i - 1, j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1, m - 1];\n}\n
min_path_sum.go
/* 最小路径和:动态规划 */\nfunc minPathSumDP(grid [][]int) int {\n    n, m := len(grid), len(grid[0])\n    // 初始化 dp 表\n    dp := make([][]int, n)\n    for i := 0; i < n; i++ {\n        dp[i] = make([]int, m)\n    }\n    dp[0][0] = grid[0][0]\n    // 状态转移:首行\n    for j := 1; j < m; j++ {\n        dp[0][j] = dp[0][j-1] + grid[0][j]\n    }\n    // 状态转移:首列\n    for i := 1; i < n; i++ {\n        dp[i][0] = dp[i-1][0] + grid[i][0]\n    }\n    // 状态转移:其余行和列\n    for i := 1; i < n; i++ {\n        for j := 1; j < m; j++ {\n            dp[i][j] = int(math.Min(float64(dp[i][j-1]), float64(dp[i-1][j]))) + grid[i][j]\n        }\n    }\n    return dp[n-1][m-1]\n}\n
min_path_sum.swift
/* 最小路径和:动态规划 */\nfunc minPathSumDP(grid: [[Int]]) -> Int {\n    let n = grid.count\n    let m = grid[0].count\n    // 初始化 dp 表\n    var dp = Array(repeating: Array(repeating: 0, count: m), count: n)\n    dp[0][0] = grid[0][0]\n    // 状态转移:首行\n    for j in 1 ..< m {\n        dp[0][j] = dp[0][j - 1] + grid[0][j]\n    }\n    // 状态转移:首列\n    for i in 1 ..< n {\n        dp[i][0] = dp[i - 1][0] + grid[i][0]\n    }\n    // 状态转移:其余行和列\n    for i in 1 ..< n {\n        for j in 1 ..< m {\n            dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]\n        }\n    }\n    return dp[n - 1][m - 1]\n}\n
min_path_sum.js
/* 最小路径和:动态规划 */\nfunction minPathSumDP(grid) {\n    const n = grid.length,\n        m = grid[0].length;\n    // 初始化 dp 表\n    const dp = Array.from({ length: n }, () =>\n        Array.from({ length: m }, () => 0)\n    );\n    dp[0][0] = grid[0][0];\n    // 状态转移:首行\n    for (let j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // 状态转移:首列\n    for (let i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // 状态转移:其余行和列\n    for (let i = 1; i < n; i++) {\n        for (let j = 1; j < m; j++) {\n            dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1][m - 1];\n}\n
min_path_sum.ts
/* 最小路径和:动态规划 */\nfunction minPathSumDP(grid: Array<Array<number>>): number {\n    const n = grid.length,\n        m = grid[0].length;\n    // 初始化 dp 表\n    const dp = Array.from({ length: n }, () =>\n        Array.from({ length: m }, () => 0)\n    );\n    dp[0][0] = grid[0][0];\n    // 状态转移:首行\n    for (let j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // 状态转移:首列\n    for (let i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // 状态转移:其余行和列\n    for (let i = 1; i < n; i++) {\n        for (let j: number = 1; j < m; j++) {\n            dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    return dp[n - 1][m - 1];\n}\n
min_path_sum.dart
/* 最小路径和:动态规划 */\nint minPathSumDP(List<List<int>> grid) {\n  int n = grid.length, m = grid[0].length;\n  // 初始化 dp 表\n  List<List<int>> dp = List.generate(n, (i) => List.filled(m, 0));\n  dp[0][0] = grid[0][0];\n  // 状态转移:首行\n  for (int j = 1; j < m; j++) {\n    dp[0][j] = dp[0][j - 1] + grid[0][j];\n  }\n  // 状态转移:首列\n  for (int i = 1; i < n; i++) {\n    dp[i][0] = dp[i - 1][0] + grid[i][0];\n  }\n  // 状态转移:其余行和列\n  for (int i = 1; i < n; i++) {\n    for (int j = 1; j < m; j++) {\n      dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n    }\n  }\n  return dp[n - 1][m - 1];\n}\n
min_path_sum.rs
/* 最小路径和:动态规划 */\nfn min_path_sum_dp(grid: &Vec<Vec<i32>>) -> i32 {\n    let (n, m) = (grid.len(), grid[0].len());\n    // 初始化 dp 表\n    let mut dp = vec![vec![0; m]; n];\n    dp[0][0] = grid[0][0];\n    // 状态转移:首行\n    for j in 1..m {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // 状态转移:首列\n    for i in 1..n {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // 状态转移:其余行和列\n    for i in 1..n {\n        for j in 1..m {\n            dp[i][j] = std::cmp::min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    dp[n - 1][m - 1]\n}\n
min_path_sum.c
/* 最小路径和:动态规划 */\nint minPathSumDP(int grid[MAX_SIZE][MAX_SIZE], int n, int m) {\n    // 初始化 dp 表\n    int **dp = malloc(n * sizeof(int *));\n    for (int i = 0; i < n; i++) {\n        dp[i] = calloc(m, sizeof(int));\n    }\n    dp[0][0] = grid[0][0];\n    // 状态转移:首行\n    for (int j = 1; j < m; j++) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j];\n    }\n    // 状态转移:首列\n    for (int i = 1; i < n; i++) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0];\n    }\n    // 状态转移:其余行和列\n    for (int i = 1; i < n; i++) {\n        for (int j = 1; j < m; j++) {\n            dp[i][j] = myMin(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];\n        }\n    }\n    int res = dp[n - 1][m - 1];\n    // 释放内存\n    for (int i = 0; i < n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
min_path_sum.kt
/* 最小路径和:动态规划 */\nfun minPathSumDP(grid: Array<IntArray>): Int {\n    val n = grid.size\n    val m = grid[0].size\n    // 初始化 dp 表\n    val dp = Array(n) { IntArray(m) }\n    dp[0][0] = grid[0][0]\n    // 状态转移:首行\n    for (j in 1..<m) {\n        dp[0][j] = dp[0][j - 1] + grid[0][j]\n    }\n    // 状态转移:首列\n    for (i in 1..<n) {\n        dp[i][0] = dp[i - 1][0] + grid[i][0]\n    }\n    // 状态转移:其余行和列\n    for (i in 1..<n) {\n        for (j in 1..<m) {\n            dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]\n        }\n    }\n    return dp[n - 1][m - 1]\n}\n
min_path_sum.rb
### 最小路径和:动态规划 ###\ndef min_path_sum_dp(grid)\n  n, m = grid.length, grid.first.length\n  # 初始化 dp 表\n  dp = Array.new(n) { Array.new(m, 0) }\n  dp[0][0] = grid[0][0]\n  # 状态转移:首行\n  (1...m).each { |j| dp[0][j] = dp[0][j - 1] + grid[0][j] }\n  # 状态转移:首列\n  (1...n).each { |i| dp[i][0] = dp[i - 1][0] + grid[i][0] }\n  # 状态转移:其余行和列\n  for i in 1...n\n    for j in 1...m\n      dp[i][j] = [dp[i][j - 1], dp[i - 1][j]].min + grid[i][j]\n    end\n  end\n  dp[n -1][m -1]\nend\n
可视化运行

全屏观看 >

图 14-16 展示了最小路径和的状态转移过程,其遍历了整个网格,因此时间复杂度为 \\(O(nm)\\) 。

数组 dp 大小为 \\(n \\times m\\) ,因此空间复杂度为 \\(O(nm)\\) 。

<1><2><3><4><5><6><7><8><9><10><11><12>

图 14-16   最小路径和的动态规划过程

","path":["第 14 章   动态规划","14.3   动态规划解题思路"],"tags":[]},{"location":"chapter_dynamic_programming/dp_solution_pipeline/#4","level":3,"title":"4.   空间优化","text":"

由于每个格子只与其左边和上边的格子有关,因此我们可以只用一个单行数组来实现 \\(dp\\) 表。

请注意,因为数组 dp 只能表示一行的状态,所以我们无法提前初始化首列状态,而是在遍历每行时更新它:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby min_path_sum.py
def min_path_sum_dp_comp(grid: list[list[int]]) -> int:\n    \"\"\"最小路径和:空间优化后的动态规划\"\"\"\n    n, m = len(grid), len(grid[0])\n    # 初始化 dp 表\n    dp = [0] * m\n    # 状态转移:首行\n    dp[0] = grid[0][0]\n    for j in range(1, m):\n        dp[j] = dp[j - 1] + grid[0][j]\n    # 状态转移:其余行\n    for i in range(1, n):\n        # 状态转移:首列\n        dp[0] = dp[0] + grid[i][0]\n        # 状态转移:其余列\n        for j in range(1, m):\n            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]\n    return dp[m - 1]\n
min_path_sum.cpp
/* 最小路径和:空间优化后的动态规划 */\nint minPathSumDPComp(vector<vector<int>> &grid) {\n    int n = grid.size(), m = grid[0].size();\n    // 初始化 dp 表\n    vector<int> dp(m);\n    // 状态转移:首行\n    dp[0] = grid[0][0];\n    for (int j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // 状态转移:其余行\n    for (int i = 1; i < n; i++) {\n        // 状态转移:首列\n        dp[0] = dp[0] + grid[i][0];\n        // 状态转移:其余列\n        for (int j = 1; j < m; j++) {\n            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
min_path_sum.java
/* 最小路径和:空间优化后的动态规划 */\nint minPathSumDPComp(int[][] grid) {\n    int n = grid.length, m = grid[0].length;\n    // 初始化 dp 表\n    int[] dp = new int[m];\n    // 状态转移:首行\n    dp[0] = grid[0][0];\n    for (int j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // 状态转移:其余行\n    for (int i = 1; i < n; i++) {\n        // 状态转移:首列\n        dp[0] = dp[0] + grid[i][0];\n        // 状态转移:其余列\n        for (int j = 1; j < m; j++) {\n            dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
min_path_sum.cs
/* 最小路径和:空间优化后的动态规划 */\nint MinPathSumDPComp(int[][] grid) {\n    int n = grid.Length, m = grid[0].Length;\n    // 初始化 dp 表\n    int[] dp = new int[m];\n    dp[0] = grid[0][0];\n    // 状态转移:首行\n    for (int j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // 状态转移:其余行\n    for (int i = 1; i < n; i++) {\n        // 状态转移:首列\n        dp[0] = dp[0] + grid[i][0];\n        // 状态转移:其余列\n        for (int j = 1; j < m; j++) {\n            dp[j] = Math.Min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
min_path_sum.go
/* 最小路径和:空间优化后的动态规划 */\nfunc minPathSumDPComp(grid [][]int) int {\n    n, m := len(grid), len(grid[0])\n    // 初始化 dp 表\n    dp := make([]int, m)\n    // 状态转移:首行\n    dp[0] = grid[0][0]\n    for j := 1; j < m; j++ {\n        dp[j] = dp[j-1] + grid[0][j]\n    }\n    // 状态转移:其余行和列\n    for i := 1; i < n; i++ {\n        // 状态转移:首列\n        dp[0] = dp[0] + grid[i][0]\n        // 状态转移:其余列\n        for j := 1; j < m; j++ {\n            dp[j] = int(math.Min(float64(dp[j-1]), float64(dp[j]))) + grid[i][j]\n        }\n    }\n    return dp[m-1]\n}\n
min_path_sum.swift
/* 最小路径和:空间优化后的动态规划 */\nfunc minPathSumDPComp(grid: [[Int]]) -> Int {\n    let n = grid.count\n    let m = grid[0].count\n    // 初始化 dp 表\n    var dp = Array(repeating: 0, count: m)\n    // 状态转移:首行\n    dp[0] = grid[0][0]\n    for j in 1 ..< m {\n        dp[j] = dp[j - 1] + grid[0][j]\n    }\n    // 状态转移:其余行\n    for i in 1 ..< n {\n        // 状态转移:首列\n        dp[0] = dp[0] + grid[i][0]\n        // 状态转移:其余列\n        for j in 1 ..< m {\n            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]\n        }\n    }\n    return dp[m - 1]\n}\n
min_path_sum.js
/* 最小路径和:空间优化后的动态规划 */\nfunction minPathSumDPComp(grid) {\n    const n = grid.length,\n        m = grid[0].length;\n    // 初始化 dp 表\n    const dp = new Array(m);\n    // 状态转移:首行\n    dp[0] = grid[0][0];\n    for (let j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // 状态转移:其余行\n    for (let i = 1; i < n; i++) {\n        // 状态转移:首列\n        dp[0] = dp[0] + grid[i][0];\n        // 状态转移:其余列\n        for (let j = 1; j < m; j++) {\n            dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
min_path_sum.ts
/* 最小路径和:空间优化后的动态规划 */\nfunction minPathSumDPComp(grid: Array<Array<number>>): number {\n    const n = grid.length,\n        m = grid[0].length;\n    // 初始化 dp 表\n    const dp = new Array(m);\n    // 状态转移:首行\n    dp[0] = grid[0][0];\n    for (let j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // 状态转移:其余行\n    for (let i = 1; i < n; i++) {\n        // 状态转移:首列\n        dp[0] = dp[0] + grid[i][0];\n        // 状态转移:其余列\n        for (let j = 1; j < m; j++) {\n            dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    return dp[m - 1];\n}\n
min_path_sum.dart
/* 最小路径和:空间优化后的动态规划 */\nint minPathSumDPComp(List<List<int>> grid) {\n  int n = grid.length, m = grid[0].length;\n  // 初始化 dp 表\n  List<int> dp = List.filled(m, 0);\n  dp[0] = grid[0][0];\n  for (int j = 1; j < m; j++) {\n    dp[j] = dp[j - 1] + grid[0][j];\n  }\n  // 状态转移:其余行\n  for (int i = 1; i < n; i++) {\n    // 状态转移:首列\n    dp[0] = dp[0] + grid[i][0];\n    // 状态转移:其余列\n    for (int j = 1; j < m; j++) {\n      dp[j] = min(dp[j - 1], dp[j]) + grid[i][j];\n    }\n  }\n  return dp[m - 1];\n}\n
min_path_sum.rs
/* 最小路径和:空间优化后的动态规划 */\nfn min_path_sum_dp_comp(grid: &Vec<Vec<i32>>) -> i32 {\n    let (n, m) = (grid.len(), grid[0].len());\n    // 初始化 dp 表\n    let mut dp = vec![0; m];\n    // 状态转移:首行\n    dp[0] = grid[0][0];\n    for j in 1..m {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // 状态转移:其余行\n    for i in 1..n {\n        // 状态转移:首列\n        dp[0] = dp[0] + grid[i][0];\n        // 状态转移:其余列\n        for j in 1..m {\n            dp[j] = std::cmp::min(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    dp[m - 1]\n}\n
min_path_sum.c
/* 最小路径和:空间优化后的动态规划 */\nint minPathSumDPComp(int grid[MAX_SIZE][MAX_SIZE], int n, int m) {\n    // 初始化 dp 表\n    int *dp = calloc(m, sizeof(int));\n    // 状态转移:首行\n    dp[0] = grid[0][0];\n    for (int j = 1; j < m; j++) {\n        dp[j] = dp[j - 1] + grid[0][j];\n    }\n    // 状态转移:其余行\n    for (int i = 1; i < n; i++) {\n        // 状态转移:首列\n        dp[0] = dp[0] + grid[i][0];\n        // 状态转移:其余列\n        for (int j = 1; j < m; j++) {\n            dp[j] = myMin(dp[j - 1], dp[j]) + grid[i][j];\n        }\n    }\n    int res = dp[m - 1];\n    // 释放内存\n    free(dp);\n    return res;\n}\n
min_path_sum.kt
/* 最小路径和:空间优化后的动态规划 */\nfun minPathSumDPComp(grid: Array<IntArray>): Int {\n    val n = grid.size\n    val m = grid[0].size\n    // 初始化 dp 表\n    val dp = IntArray(m)\n    // 状态转移:首行\n    dp[0] = grid[0][0]\n    for (j in 1..<m) {\n        dp[j] = dp[j - 1] + grid[0][j]\n    }\n    // 状态转移:其余行\n    for (i in 1..<n) {\n        // 状态转移:首列\n        dp[0] = dp[0] + grid[i][0]\n        // 状态转移:其余列\n        for (j in 1..<m) {\n            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]\n        }\n    }\n    return dp[m - 1]\n}\n
min_path_sum.rb
### 最小路径和:空间优化后的动态规划 ###\ndef min_path_sum_dp_comp(grid)\n  n, m = grid.length, grid.first.length\n  # 初始化 dp 表\n  dp = Array.new(m, 0)\n  # 状态转移:首行\n  dp[0] = grid[0][0]\n  (1...m).each { |j| dp[j] = dp[j - 1] + grid[0][j] }\n  # 状态转移:其余行\n  for i in 1...n\n    # 状态转移:首列\n    dp[0] = dp[0] + grid[i][0]\n    # 状态转移:其余列\n    (1...m).each { |j| dp[j] = [dp[j - 1], dp[j]].min + grid[i][j] }\n  end\n  dp[m - 1]\nend\n
可视化运行

全屏观看 >

","path":["第 14 章   动态规划","14.3   动态规划解题思路"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/","level":1,"title":"14.6   编辑距离问题","text":"

编辑距离,也称 Levenshtein 距离,指两个字符串之间互相转换的最少修改次数,通常用于在信息检索和自然语言处理中度量两个序列的相似度。

Question

输入两个字符串 \\(s\\) 和 \\(t\\) ,返回将 \\(s\\) 转换为 \\(t\\) 所需的最少编辑步数。

你可以在一个字符串中进行三种编辑操作:插入一个字符、删除一个字符、将字符替换为任意一个字符。

如图 14-27 所示,将 kitten 转换为 sitting 需要编辑 3 步,包括 2 次替换操作与 1 次添加操作;将 hello 转换为 algo 需要 3 步,包括 2 次替换操作和 1 次删除操作。

图 14-27   编辑距离的示例数据

编辑距离问题可以很自然地用决策树模型来解释。字符串对应树节点,一轮决策(一次编辑操作)对应树的一条边。

如图 14-28 所示,在不限制操作的情况下,每个节点都可以派生出许多条边,每条边对应一种操作,这意味着从 hello 转换到 algo 有许多种可能的路径。

从决策树的角度看,本题的目标是求解节点 hello 和节点 algo 之间的最短路径。

图 14-28   基于决策树模型表示编辑距离问题

","path":["第 14 章   动态规划","14.6   编辑距离问题"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/#1","level":3,"title":"1.   动态规划思路","text":"

第一步:思考每轮的决策,定义状态,从而得到 \\(dp\\) 表

每一轮的决策是对字符串 \\(s\\) 进行一次编辑操作。

我们希望在编辑操作的过程中,问题的规模逐渐缩小,这样才能构建子问题。设字符串 \\(s\\) 和 \\(t\\) 的长度分别为 \\(n\\) 和 \\(m\\) ,我们先考虑两字符串尾部的字符 \\(s[n-1]\\) 和 \\(t[m-1]\\) 。

也就是说,我们在字符串 \\(s\\) 中进行的每一轮决策(编辑操作),都会使得 \\(s\\) 和 \\(t\\) 中剩余的待匹配字符发生变化。因此,状态为当前在 \\(s\\) 和 \\(t\\) 中考虑的第 \\(i\\) 和第 \\(j\\) 个字符,记为 \\([i, j]\\) 。

状态 \\([i, j]\\) 对应的子问题:将 \\(s\\) 的前 \\(i\\) 个字符更改为 \\(t\\) 的前 \\(j\\) 个字符所需的最少编辑步数。

至此,得到一个尺寸为 \\((i+1) \\times (j+1)\\) 的二维 \\(dp\\) 表。

第二步:找出最优子结构,进而推导出状态转移方程

考虑子问题 \\(dp[i, j]\\) ,其对应的两个字符串的尾部字符为 \\(s[i-1]\\) 和 \\(t[j-1]\\) ,可根据不同编辑操作分为图 14-29 所示的三种情况。

  1. 在 \\(s[i-1]\\) 之后添加 \\(t[j-1]\\) ,则剩余子问题 \\(dp[i, j-1]\\) 。
  2. 删除 \\(s[i-1]\\) ,则剩余子问题 \\(dp[i-1, j]\\) 。
  3. 将 \\(s[i-1]\\) 替换为 \\(t[j-1]\\) ,则剩余子问题 \\(dp[i-1, j-1]\\) 。

图 14-29   编辑距离的状态转移

根据以上分析,可得最优子结构:\\(dp[i, j]\\) 的最少编辑步数等于 \\(dp[i, j-1]\\)、\\(dp[i-1, j]\\)、\\(dp[i-1, j-1]\\) 三者中的最少编辑步数,再加上本次的编辑步数 \\(1\\) 。对应的状态转移方程为:

\\[ dp[i, j] = \\min(dp[i, j-1], dp[i-1, j], dp[i-1, j-1]) + 1 \\]

请注意,当 \\(s[i-1]\\) 和 \\(t[j-1]\\) 相同时,无须编辑当前字符,这种情况下的状态转移方程为:

\\[ dp[i, j] = dp[i-1, j-1] \\]

第三步:确定边界条件和状态转移顺序

当两字符串都为空时,编辑步数为 \\(0\\) ,即 \\(dp[0, 0] = 0\\) 。当 \\(s\\) 为空但 \\(t\\) 不为空时,最少编辑步数等于 \\(t\\) 的长度,即首行 \\(dp[0, j] = j\\) 。当 \\(s\\) 不为空但 \\(t\\) 为空时,最少编辑步数等于 \\(s\\) 的长度,即首列 \\(dp[i, 0] = i\\) 。

观察状态转移方程,解 \\(dp[i, j]\\) 依赖左方、上方、左上方的解,因此通过两层循环正序遍历整个 \\(dp\\) 表即可。

","path":["第 14 章   动态规划","14.6   编辑距离问题"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/#2","level":3,"title":"2.   代码实现","text":"PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby edit_distance.py
def edit_distance_dp(s: str, t: str) -> int:\n    \"\"\"编辑距离:动态规划\"\"\"\n    n, m = len(s), len(t)\n    dp = [[0] * (m + 1) for _ in range(n + 1)]\n    # 状态转移:首行首列\n    for i in range(1, n + 1):\n        dp[i][0] = i\n    for j in range(1, m + 1):\n        dp[0][j] = j\n    # 状态转移:其余行和列\n    for i in range(1, n + 1):\n        for j in range(1, m + 1):\n            if s[i - 1] == t[j - 1]:\n                # 若两字符相等,则直接跳过此两字符\n                dp[i][j] = dp[i - 1][j - 1]\n            else:\n                # 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1\n                dp[i][j] = min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1\n    return dp[n][m]\n
edit_distance.cpp
/* 编辑距离:动态规划 */\nint editDistanceDP(string s, string t) {\n    int n = s.length(), m = t.length();\n    vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));\n    // 状态转移:首行首列\n    for (int i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (int j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // 状态转移:其余行和列\n    for (int i = 1; i <= n; i++) {\n        for (int j = 1; j <= m; j++) {\n            if (s[i - 1] == t[j - 1]) {\n                // 若两字符相等,则直接跳过此两字符\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1\n                dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n][m];\n}\n
edit_distance.java
/* 编辑距离:动态规划 */\nint editDistanceDP(String s, String t) {\n    int n = s.length(), m = t.length();\n    int[][] dp = new int[n + 1][m + 1];\n    // 状态转移:首行首列\n    for (int i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (int j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // 状态转移:其余行和列\n    for (int i = 1; i <= n; i++) {\n        for (int j = 1; j <= m; j++) {\n            if (s.charAt(i - 1) == t.charAt(j - 1)) {\n                // 若两字符相等,则直接跳过此两字符\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1\n                dp[i][j] = Math.min(Math.min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n][m];\n}\n
edit_distance.cs
/* 编辑距离:动态规划 */\nint EditDistanceDP(string s, string t) {\n    int n = s.Length, m = t.Length;\n    int[,] dp = new int[n + 1, m + 1];\n    // 状态转移:首行首列\n    for (int i = 1; i <= n; i++) {\n        dp[i, 0] = i;\n    }\n    for (int j = 1; j <= m; j++) {\n        dp[0, j] = j;\n    }\n    // 状态转移:其余行和列\n    for (int i = 1; i <= n; i++) {\n        for (int j = 1; j <= m; j++) {\n            if (s[i - 1] == t[j - 1]) {\n                // 若两字符相等,则直接跳过此两字符\n                dp[i, j] = dp[i - 1, j - 1];\n            } else {\n                // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1\n                dp[i, j] = Math.Min(Math.Min(dp[i, j - 1], dp[i - 1, j]), dp[i - 1, j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n, m];\n}\n
edit_distance.go
/* 编辑距离:动态规划 */\nfunc editDistanceDP(s string, t string) int {\n    n := len(s)\n    m := len(t)\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, m+1)\n    }\n    // 状态转移:首行首列\n    for i := 1; i <= n; i++ {\n        dp[i][0] = i\n    }\n    for j := 1; j <= m; j++ {\n        dp[0][j] = j\n    }\n    // 状态转移:其余行和列\n    for i := 1; i <= n; i++ {\n        for j := 1; j <= m; j++ {\n            if s[i-1] == t[j-1] {\n                // 若两字符相等,则直接跳过此两字符\n                dp[i][j] = dp[i-1][j-1]\n            } else {\n                // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1\n                dp[i][j] = MinInt(MinInt(dp[i][j-1], dp[i-1][j]), dp[i-1][j-1]) + 1\n            }\n        }\n    }\n    return dp[n][m]\n}\n
edit_distance.swift
/* 编辑距离:动态规划 */\nfunc editDistanceDP(s: String, t: String) -> Int {\n    let n = s.utf8CString.count\n    let m = t.utf8CString.count\n    var dp = Array(repeating: Array(repeating: 0, count: m + 1), count: n + 1)\n    // 状态转移:首行首列\n    for i in 1 ... n {\n        dp[i][0] = i\n    }\n    for j in 1 ... m {\n        dp[0][j] = j\n    }\n    // 状态转移:其余行和列\n    for i in 1 ... n {\n        for j in 1 ... m {\n            if s.utf8CString[i - 1] == t.utf8CString[j - 1] {\n                // 若两字符相等,则直接跳过此两字符\n                dp[i][j] = dp[i - 1][j - 1]\n            } else {\n                // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1\n                dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1\n            }\n        }\n    }\n    return dp[n][m]\n}\n
edit_distance.js
/* 编辑距离:动态规划 */\nfunction editDistanceDP(s, t) {\n    const n = s.length,\n        m = t.length;\n    const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));\n    // 状态转移:首行首列\n    for (let i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (let j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // 状态转移:其余行和列\n    for (let i = 1; i <= n; i++) {\n        for (let j = 1; j <= m; j++) {\n            if (s.charAt(i - 1) === t.charAt(j - 1)) {\n                // 若两字符相等,则直接跳过此两字符\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1\n                dp[i][j] =\n                    Math.min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n][m];\n}\n
edit_distance.ts
/* 编辑距离:动态规划 */\nfunction editDistanceDP(s: string, t: string): number {\n    const n = s.length,\n        m = t.length;\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: m + 1 }, () => 0)\n    );\n    // 状态转移:首行首列\n    for (let i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (let j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // 状态转移:其余行和列\n    for (let i = 1; i <= n; i++) {\n        for (let j = 1; j <= m; j++) {\n            if (s.charAt(i - 1) === t.charAt(j - 1)) {\n                // 若两字符相等,则直接跳过此两字符\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1\n                dp[i][j] =\n                    Math.min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    return dp[n][m];\n}\n
edit_distance.dart
/* 编辑距离:动态规划 */\nint editDistanceDP(String s, String t) {\n  int n = s.length, m = t.length;\n  List<List<int>> dp = List.generate(n + 1, (_) => List.filled(m + 1, 0));\n  // 状态转移:首行首列\n  for (int i = 1; i <= n; i++) {\n    dp[i][0] = i;\n  }\n  for (int j = 1; j <= m; j++) {\n    dp[0][j] = j;\n  }\n  // 状态转移:其余行和列\n  for (int i = 1; i <= n; i++) {\n    for (int j = 1; j <= m; j++) {\n      if (s[i - 1] == t[j - 1]) {\n        // 若两字符相等,则直接跳过此两字符\n        dp[i][j] = dp[i - 1][j - 1];\n      } else {\n        // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1\n        dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n      }\n    }\n  }\n  return dp[n][m];\n}\n
edit_distance.rs
/* 编辑距离:动态规划 */\nfn edit_distance_dp(s: &str, t: &str) -> i32 {\n    let (n, m) = (s.len(), t.len());\n    let mut dp = vec![vec![0; m + 1]; n + 1];\n    // 状态转移:首行首列\n    for i in 1..=n {\n        dp[i][0] = i as i32;\n    }\n    for j in 1..m {\n        dp[0][j] = j as i32;\n    }\n    // 状态转移:其余行和列\n    for i in 1..=n {\n        for j in 1..=m {\n            if s.chars().nth(i - 1) == t.chars().nth(j - 1) {\n                // 若两字符相等,则直接跳过此两字符\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1\n                dp[i][j] =\n                    std::cmp::min(std::cmp::min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    dp[n][m]\n}\n
edit_distance.c
/* 编辑距离:动态规划 */\nint editDistanceDP(char *s, char *t, int n, int m) {\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(m + 1, sizeof(int));\n    }\n    // 状态转移:首行首列\n    for (int i = 1; i <= n; i++) {\n        dp[i][0] = i;\n    }\n    for (int j = 1; j <= m; j++) {\n        dp[0][j] = j;\n    }\n    // 状态转移:其余行和列\n    for (int i = 1; i <= n; i++) {\n        for (int j = 1; j <= m; j++) {\n            if (s[i - 1] == t[j - 1]) {\n                // 若两字符相等,则直接跳过此两字符\n                dp[i][j] = dp[i - 1][j - 1];\n            } else {\n                // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1\n                dp[i][j] = myMin(myMin(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;\n            }\n        }\n    }\n    int res = dp[n][m];\n    // 释放内存\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
edit_distance.kt
/* 编辑距离:动态规划 */\nfun editDistanceDP(s: String, t: String): Int {\n    val n = s.length\n    val m = t.length\n    val dp = Array(n + 1) { IntArray(m + 1) }\n    // 状态转移:首行首列\n    for (i in 1..n) {\n        dp[i][0] = i\n    }\n    for (j in 1..m) {\n        dp[0][j] = j\n    }\n    // 状态转移:其余行和列\n    for (i in 1..n) {\n        for (j in 1..m) {\n            if (s[i - 1] == t[j - 1]) {\n                // 若两字符相等,则直接跳过此两字符\n                dp[i][j] = dp[i - 1][j - 1]\n            } else {\n                // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1\n                dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1\n            }\n        }\n    }\n    return dp[n][m]\n}\n
edit_distance.rb
### 编辑距离:动态规划 ###\ndef edit_distance_dp(s, t)\n  n, m = s.length, t.length\n  dp = Array.new(n + 1) { Array.new(m + 1, 0) }\n  # 状态转移:首行首列\n  (1...(n + 1)).each { |i| dp[i][0] = i }\n  (1...(m + 1)).each { |j| dp[0][j] = j }\n  # 状态转移:其余行和列\n  for i in 1...(n + 1)\n    for j in 1...(m +1)\n      if s[i - 1] == t[j - 1]\n        # 若两字符相等,则直接跳过此两字符\n        dp[i][j] = dp[i - 1][j - 1]\n      else\n        # 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1\n        dp[i][j] = [dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]].min + 1\n      end\n    end\n  end\n  dp[n][m]\nend\n
可视化运行

全屏观看 >

如图 14-30 所示,编辑距离问题的状态转移过程与背包问题非常类似,都可以看作填写一个二维网格的过程。

<1><2><3><4><5><6><7><8><9><10><11><12><13><14><15>

图 14-30   编辑距离的动态规划过程

","path":["第 14 章   动态规划","14.6   编辑距离问题"],"tags":[]},{"location":"chapter_dynamic_programming/edit_distance_problem/#3","level":3,"title":"3.   空间优化","text":"

由于 \\(dp[i,j]\\) 是由上方 \\(dp[i-1, j]\\)、左方 \\(dp[i, j-1]\\)、左上方 \\(dp[i-1, j-1]\\) 转移而来的,而正序遍历会丢失左上方 \\(dp[i-1, j-1]\\) ,倒序遍历无法提前构建 \\(dp[i, j-1]\\) ,因此两种遍历顺序都不可取。

为此,我们可以使用一个变量 leftup 来暂存左上方的解 \\(dp[i-1, j-1]\\) ,从而只需考虑左方和上方的解。此时的情况与完全背包问题相同,可使用正序遍历。代码如下所示:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby edit_distance.py
def edit_distance_dp_comp(s: str, t: str) -> int:\n    \"\"\"编辑距离:空间优化后的动态规划\"\"\"\n    n, m = len(s), len(t)\n    dp = [0] * (m + 1)\n    # 状态转移:首行\n    for j in range(1, m + 1):\n        dp[j] = j\n    # 状态转移:其余行\n    for i in range(1, n + 1):\n        # 状态转移:首列\n        leftup = dp[0]  # 暂存 dp[i-1, j-1]\n        dp[0] += 1\n        # 状态转移:其余列\n        for j in range(1, m + 1):\n            temp = dp[j]\n            if s[i - 1] == t[j - 1]:\n                # 若两字符相等,则直接跳过此两字符\n                dp[j] = leftup\n            else:\n                # 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1\n                dp[j] = min(dp[j - 1], dp[j], leftup) + 1\n            leftup = temp  # 更新为下一轮的 dp[i-1, j-1]\n    return dp[m]\n
edit_distance.cpp
/* 编辑距离:空间优化后的动态规划 */\nint editDistanceDPComp(string s, string t) {\n    int n = s.length(), m = t.length();\n    vector<int> dp(m + 1, 0);\n    // 状态转移:首行\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // 状态转移:其余行\n    for (int i = 1; i <= n; i++) {\n        // 状态转移:首列\n        int leftup = dp[0]; // 暂存 dp[i-1, j-1]\n        dp[0] = i;\n        // 状态转移:其余列\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s[i - 1] == t[j - 1]) {\n                // 若两字符相等,则直接跳过此两字符\n                dp[j] = leftup;\n            } else {\n                // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1\n                dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // 更新为下一轮的 dp[i-1, j-1]\n        }\n    }\n    return dp[m];\n}\n
edit_distance.java
/* 编辑距离:空间优化后的动态规划 */\nint editDistanceDPComp(String s, String t) {\n    int n = s.length(), m = t.length();\n    int[] dp = new int[m + 1];\n    // 状态转移:首行\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // 状态转移:其余行\n    for (int i = 1; i <= n; i++) {\n        // 状态转移:首列\n        int leftup = dp[0]; // 暂存 dp[i-1, j-1]\n        dp[0] = i;\n        // 状态转移:其余列\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s.charAt(i - 1) == t.charAt(j - 1)) {\n                // 若两字符相等,则直接跳过此两字符\n                dp[j] = leftup;\n            } else {\n                // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1\n                dp[j] = Math.min(Math.min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // 更新为下一轮的 dp[i-1, j-1]\n        }\n    }\n    return dp[m];\n}\n
edit_distance.cs
/* 编辑距离:空间优化后的动态规划 */\nint EditDistanceDPComp(string s, string t) {\n    int n = s.Length, m = t.Length;\n    int[] dp = new int[m + 1];\n    // 状态转移:首行\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // 状态转移:其余行\n    for (int i = 1; i <= n; i++) {\n        // 状态转移:首列\n        int leftup = dp[0]; // 暂存 dp[i-1, j-1]\n        dp[0] = i;\n        // 状态转移:其余列\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s[i - 1] == t[j - 1]) {\n                // 若两字符相等,则直接跳过此两字符\n                dp[j] = leftup;\n            } else {\n                // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1\n                dp[j] = Math.Min(Math.Min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // 更新为下一轮的 dp[i-1, j-1]\n        }\n    }\n    return dp[m];\n}\n
edit_distance.go
/* 编辑距离:空间优化后的动态规划 */\nfunc editDistanceDPComp(s string, t string) int {\n    n := len(s)\n    m := len(t)\n    dp := make([]int, m+1)\n    // 状态转移:首行\n    for j := 1; j <= m; j++ {\n        dp[j] = j\n    }\n    // 状态转移:其余行\n    for i := 1; i <= n; i++ {\n        // 状态转移:首列\n        leftUp := dp[0] // 暂存 dp[i-1, j-1]\n        dp[0] = i\n        // 状态转移:其余列\n        for j := 1; j <= m; j++ {\n            temp := dp[j]\n            if s[i-1] == t[j-1] {\n                // 若两字符相等,则直接跳过此两字符\n                dp[j] = leftUp\n            } else {\n                // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1\n                dp[j] = MinInt(MinInt(dp[j-1], dp[j]), leftUp) + 1\n            }\n            leftUp = temp // 更新为下一轮的 dp[i-1, j-1]\n        }\n    }\n    return dp[m]\n}\n
edit_distance.swift
/* 编辑距离:空间优化后的动态规划 */\nfunc editDistanceDPComp(s: String, t: String) -> Int {\n    let n = s.utf8CString.count\n    let m = t.utf8CString.count\n    var dp = Array(repeating: 0, count: m + 1)\n    // 状态转移:首行\n    for j in 1 ... m {\n        dp[j] = j\n    }\n    // 状态转移:其余行\n    for i in 1 ... n {\n        // 状态转移:首列\n        var leftup = dp[0] // 暂存 dp[i-1, j-1]\n        dp[0] = i\n        // 状态转移:其余列\n        for j in 1 ... m {\n            let temp = dp[j]\n            if s.utf8CString[i - 1] == t.utf8CString[j - 1] {\n                // 若两字符相等,则直接跳过此两字符\n                dp[j] = leftup\n            } else {\n                // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1\n                dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1\n            }\n            leftup = temp // 更新为下一轮的 dp[i-1, j-1]\n        }\n    }\n    return dp[m]\n}\n
edit_distance.js
/* 编辑距离:空间优化后的动态规划 */\nfunction editDistanceDPComp(s, t) {\n    const n = s.length,\n        m = t.length;\n    const dp = new Array(m + 1).fill(0);\n    // 状态转移:首行\n    for (let j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // 状态转移:其余行\n    for (let i = 1; i <= n; i++) {\n        // 状态转移:首列\n        let leftup = dp[0]; // 暂存 dp[i-1, j-1]\n        dp[0] = i;\n        // 状态转移:其余列\n        for (let j = 1; j <= m; j++) {\n            const temp = dp[j];\n            if (s.charAt(i - 1) === t.charAt(j - 1)) {\n                // 若两字符相等,则直接跳过此两字符\n                dp[j] = leftup;\n            } else {\n                // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1\n                dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1;\n            }\n            leftup = temp; // 更新为下一轮的 dp[i-1, j-1]\n        }\n    }\n    return dp[m];\n}\n
edit_distance.ts
/* 编辑距离:空间优化后的动态规划 */\nfunction editDistanceDPComp(s: string, t: string): number {\n    const n = s.length,\n        m = t.length;\n    const dp = new Array(m + 1).fill(0);\n    // 状态转移:首行\n    for (let j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // 状态转移:其余行\n    for (let i = 1; i <= n; i++) {\n        // 状态转移:首列\n        let leftup = dp[0]; // 暂存 dp[i-1, j-1]\n        dp[0] = i;\n        // 状态转移:其余列\n        for (let j = 1; j <= m; j++) {\n            const temp = dp[j];\n            if (s.charAt(i - 1) === t.charAt(j - 1)) {\n                // 若两字符相等,则直接跳过此两字符\n                dp[j] = leftup;\n            } else {\n                // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1\n                dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1;\n            }\n            leftup = temp; // 更新为下一轮的 dp[i-1, j-1]\n        }\n    }\n    return dp[m];\n}\n
edit_distance.dart
/* 编辑距离:空间优化后的动态规划 */\nint editDistanceDPComp(String s, String t) {\n  int n = s.length, m = t.length;\n  List<int> dp = List.filled(m + 1, 0);\n  // 状态转移:首行\n  for (int j = 1; j <= m; j++) {\n    dp[j] = j;\n  }\n  // 状态转移:其余行\n  for (int i = 1; i <= n; i++) {\n    // 状态转移:首列\n    int leftup = dp[0]; // 暂存 dp[i-1, j-1]\n    dp[0] = i;\n    // 状态转移:其余列\n    for (int j = 1; j <= m; j++) {\n      int temp = dp[j];\n      if (s[i - 1] == t[j - 1]) {\n        // 若两字符相等,则直接跳过此两字符\n        dp[j] = leftup;\n      } else {\n        // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1\n        dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1;\n      }\n      leftup = temp; // 更新为下一轮的 dp[i-1, j-1]\n    }\n  }\n  return dp[m];\n}\n
edit_distance.rs
/* 编辑距离:空间优化后的动态规划 */\nfn edit_distance_dp_comp(s: &str, t: &str) -> i32 {\n    let (n, m) = (s.len(), t.len());\n    let mut dp = vec![0; m + 1];\n    // 状态转移:首行\n    for j in 1..m {\n        dp[j] = j as i32;\n    }\n    // 状态转移:其余行\n    for i in 1..=n {\n        // 状态转移:首列\n        let mut leftup = dp[0]; // 暂存 dp[i-1, j-1]\n        dp[0] = i as i32;\n        // 状态转移:其余列\n        for j in 1..=m {\n            let temp = dp[j];\n            if s.chars().nth(i - 1) == t.chars().nth(j - 1) {\n                // 若两字符相等,则直接跳过此两字符\n                dp[j] = leftup;\n            } else {\n                // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1\n                dp[j] = std::cmp::min(std::cmp::min(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // 更新为下一轮的 dp[i-1, j-1]\n        }\n    }\n    dp[m]\n}\n
edit_distance.c
/* 编辑距离:空间优化后的动态规划 */\nint editDistanceDPComp(char *s, char *t, int n, int m) {\n    int *dp = calloc(m + 1, sizeof(int));\n    // 状态转移:首行\n    for (int j = 1; j <= m; j++) {\n        dp[j] = j;\n    }\n    // 状态转移:其余行\n    for (int i = 1; i <= n; i++) {\n        // 状态转移:首列\n        int leftup = dp[0]; // 暂存 dp[i-1, j-1]\n        dp[0] = i;\n        // 状态转移:其余列\n        for (int j = 1; j <= m; j++) {\n            int temp = dp[j];\n            if (s[i - 1] == t[j - 1]) {\n                // 若两字符相等,则直接跳过此两字符\n                dp[j] = leftup;\n            } else {\n                // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1\n                dp[j] = myMin(myMin(dp[j - 1], dp[j]), leftup) + 1;\n            }\n            leftup = temp; // 更新为下一轮的 dp[i-1, j-1]\n        }\n    }\n    int res = dp[m];\n    // 释放内存\n    free(dp);\n    return res;\n}\n
edit_distance.kt
/* 编辑距离:空间优化后的动态规划 */\nfun editDistanceDPComp(s: String, t: String): Int {\n    val n = s.length\n    val m = t.length\n    val dp = IntArray(m + 1)\n    // 状态转移:首行\n    for (j in 1..m) {\n        dp[j] = j\n    }\n    // 状态转移:其余行\n    for (i in 1..n) {\n        // 状态转移:首列\n        var leftup = dp[0] // 暂存 dp[i-1, j-1]\n        dp[0] = i\n        // 状态转移:其余列\n        for (j in 1..m) {\n            val temp = dp[j]\n            if (s[i - 1] == t[j - 1]) {\n                // 若两字符相等,则直接跳过此两字符\n                dp[j] = leftup\n            } else {\n                // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1\n                dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1\n            }\n            leftup = temp // 更新为下一轮的 dp[i-1, j-1]\n        }\n    }\n    return dp[m]\n}\n
edit_distance.rb
### 编辑距离:空间优化后的动态规划 ###\ndef edit_distance_dp_comp(s, t)\n  n, m = s.length, t.length\n  dp = Array.new(m + 1, 0)\n  # 状态转移:首行\n  (1...(m + 1)).each { |j| dp[j] = j }\n  # 状态转移:其余行\n  for i in 1...(n + 1)\n    # 状态转移:首列\n    leftup = dp.first # 暂存 dp[i-1, j-1]\n    dp[0] += 1\n    # 状态转移:其余列\n    for j in 1...(m + 1)\n      temp = dp[j]\n      if s[i - 1] == t[j - 1]\n        # 若两字符相等,则直接跳过此两字符\n        dp[j] = leftup\n      else\n        # 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1\n        dp[j] = [dp[j - 1], dp[j], leftup].min + 1\n      end\n      leftup = temp # 更新为下一轮的 dp[i-1, j-1]\n    end\n  end\n  dp[m]\nend\n
可视化运行

全屏观看 >

","path":["第 14 章   动态规划","14.6   编辑距离问题"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/","level":1,"title":"14.1   初探动态规划","text":"

动态规划(dynamic programming)是一个重要的算法范式,它将一个问题分解为一系列更小的子问题,并通过存储子问题的解来避免重复计算,从而大幅提升时间效率。

在本节中,我们从一个经典例题入手,先给出它的暴力回溯解法,观察其中包含的重叠子问题,再逐步导出更高效的动态规划解法。

爬楼梯

给定一个共有 \\(n\\) 阶的楼梯,你每步可以上 \\(1\\) 阶或者 \\(2\\) 阶,请问有多少种方案可以爬到楼顶?

如图 14-1 所示,对于一个 \\(3\\) 阶楼梯,共有 \\(3\\) 种方案可以爬到楼顶。

图 14-1   爬到第 3 阶的方案数量

本题的目标是求解方案数量,我们可以考虑通过回溯来穷举所有可能性。具体来说,将爬楼梯想象为一个多轮选择的过程:从地面出发,每轮选择上 \\(1\\) 阶或 \\(2\\) 阶,每当到达楼梯顶部时就将方案数量加 \\(1\\) ,当越过楼梯顶部时就将其剪枝。代码如下所示:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_backtrack.py
def backtrack(choices: list[int], state: int, n: int, res: list[int]) -> int:\n    \"\"\"回溯\"\"\"\n    # 当爬到第 n 阶时,方案数量加 1\n    if state == n:\n        res[0] += 1\n    # 遍历所有选择\n    for choice in choices:\n        # 剪枝:不允许越过第 n 阶\n        if state + choice > n:\n            continue\n        # 尝试:做出选择,更新状态\n        backtrack(choices, state + choice, n, res)\n        # 回退\n\ndef climbing_stairs_backtrack(n: int) -> int:\n    \"\"\"爬楼梯:回溯\"\"\"\n    choices = [1, 2]  # 可选择向上爬 1 阶或 2 阶\n    state = 0  # 从第 0 阶开始爬\n    res = [0]  # 使用 res[0] 记录方案数量\n    backtrack(choices, state, n, res)\n    return res[0]\n
climbing_stairs_backtrack.cpp
/* 回溯 */\nvoid backtrack(vector<int> &choices, int state, int n, vector<int> &res) {\n    // 当爬到第 n 阶时,方案数量加 1\n    if (state == n)\n        res[0]++;\n    // 遍历所有选择\n    for (auto &choice : choices) {\n        // 剪枝:不允许越过第 n 阶\n        if (state + choice > n)\n            continue;\n        // 尝试:做出选择,更新状态\n        backtrack(choices, state + choice, n, res);\n        // 回退\n    }\n}\n\n/* 爬楼梯:回溯 */\nint climbingStairsBacktrack(int n) {\n    vector<int> choices = {1, 2}; // 可选择向上爬 1 阶或 2 阶\n    int state = 0;                // 从第 0 阶开始爬\n    vector<int> res = {0};        // 使用 res[0] 记录方案数量\n    backtrack(choices, state, n, res);\n    return res[0];\n}\n
climbing_stairs_backtrack.java
/* 回溯 */\nvoid backtrack(List<Integer> choices, int state, int n, List<Integer> res) {\n    // 当爬到第 n 阶时,方案数量加 1\n    if (state == n)\n        res.set(0, res.get(0) + 1);\n    // 遍历所有选择\n    for (Integer choice : choices) {\n        // 剪枝:不允许越过第 n 阶\n        if (state + choice > n)\n            continue;\n        // 尝试:做出选择,更新状态\n        backtrack(choices, state + choice, n, res);\n        // 回退\n    }\n}\n\n/* 爬楼梯:回溯 */\nint climbingStairsBacktrack(int n) {\n    List<Integer> choices = Arrays.asList(1, 2); // 可选择向上爬 1 阶或 2 阶\n    int state = 0; // 从第 0 阶开始爬\n    List<Integer> res = new ArrayList<>();\n    res.add(0); // 使用 res[0] 记录方案数量\n    backtrack(choices, state, n, res);\n    return res.get(0);\n}\n
climbing_stairs_backtrack.cs
/* 回溯 */\nvoid Backtrack(List<int> choices, int state, int n, List<int> res) {\n    // 当爬到第 n 阶时,方案数量加 1\n    if (state == n)\n        res[0]++;\n    // 遍历所有选择\n    foreach (int choice in choices) {\n        // 剪枝:不允许越过第 n 阶\n        if (state + choice > n)\n            continue;\n        // 尝试:做出选择,更新状态\n        Backtrack(choices, state + choice, n, res);\n        // 回退\n    }\n}\n\n/* 爬楼梯:回溯 */\nint ClimbingStairsBacktrack(int n) {\n    List<int> choices = [1, 2]; // 可选择向上爬 1 阶或 2 阶\n    int state = 0; // 从第 0 阶开始爬\n    List<int> res = [0]; // 使用 res[0] 记录方案数量\n    Backtrack(choices, state, n, res);\n    return res[0];\n}\n
climbing_stairs_backtrack.go
/* 回溯 */\nfunc backtrack(choices []int, state, n int, res []int) {\n    // 当爬到第 n 阶时,方案数量加 1\n    if state == n {\n        res[0] = res[0] + 1\n    }\n    // 遍历所有选择\n    for _, choice := range choices {\n        // 剪枝:不允许越过第 n 阶\n        if state+choice > n {\n            continue\n        }\n        // 尝试:做出选择,更新状态\n        backtrack(choices, state+choice, n, res)\n        // 回退\n    }\n}\n\n/* 爬楼梯:回溯 */\nfunc climbingStairsBacktrack(n int) int {\n    // 可选择向上爬 1 阶或 2 阶\n    choices := []int{1, 2}\n    // 从第 0 阶开始爬\n    state := 0\n    res := make([]int, 1)\n    // 使用 res[0] 记录方案数量\n    res[0] = 0\n    backtrack(choices, state, n, res)\n    return res[0]\n}\n
climbing_stairs_backtrack.swift
/* 回溯 */\nfunc backtrack(choices: [Int], state: Int, n: Int, res: inout [Int]) {\n    // 当爬到第 n 阶时,方案数量加 1\n    if state == n {\n        res[0] += 1\n    }\n    // 遍历所有选择\n    for choice in choices {\n        // 剪枝:不允许越过第 n 阶\n        if state + choice > n {\n            continue\n        }\n        // 尝试:做出选择,更新状态\n        backtrack(choices: choices, state: state + choice, n: n, res: &res)\n        // 回退\n    }\n}\n\n/* 爬楼梯:回溯 */\nfunc climbingStairsBacktrack(n: Int) -> Int {\n    let choices = [1, 2] // 可选择向上爬 1 阶或 2 阶\n    let state = 0 // 从第 0 阶开始爬\n    var res: [Int] = []\n    res.append(0) // 使用 res[0] 记录方案数量\n    backtrack(choices: choices, state: state, n: n, res: &res)\n    return res[0]\n}\n
climbing_stairs_backtrack.js
/* 回溯 */\nfunction backtrack(choices, state, n, res) {\n    // 当爬到第 n 阶时,方案数量加 1\n    if (state === n) res.set(0, res.get(0) + 1);\n    // 遍历所有选择\n    for (const choice of choices) {\n        // 剪枝:不允许越过第 n 阶\n        if (state + choice > n) continue;\n        // 尝试:做出选择,更新状态\n        backtrack(choices, state + choice, n, res);\n        // 回退\n    }\n}\n\n/* 爬楼梯:回溯 */\nfunction climbingStairsBacktrack(n) {\n    const choices = [1, 2]; // 可选择向上爬 1 阶或 2 阶\n    const state = 0; // 从第 0 阶开始爬\n    const res = new Map();\n    res.set(0, 0); // 使用 res[0] 记录方案数量\n    backtrack(choices, state, n, res);\n    return res.get(0);\n}\n
climbing_stairs_backtrack.ts
/* 回溯 */\nfunction backtrack(\n    choices: number[],\n    state: number,\n    n: number,\n    res: Map<0, any>\n): void {\n    // 当爬到第 n 阶时,方案数量加 1\n    if (state === n) res.set(0, res.get(0) + 1);\n    // 遍历所有选择\n    for (const choice of choices) {\n        // 剪枝:不允许越过第 n 阶\n        if (state + choice > n) continue;\n        // 尝试:做出选择,更新状态\n        backtrack(choices, state + choice, n, res);\n        // 回退\n    }\n}\n\n/* 爬楼梯:回溯 */\nfunction climbingStairsBacktrack(n: number): number {\n    const choices = [1, 2]; // 可选择向上爬 1 阶或 2 阶\n    const state = 0; // 从第 0 阶开始爬\n    const res = new Map();\n    res.set(0, 0); // 使用 res[0] 记录方案数量\n    backtrack(choices, state, n, res);\n    return res.get(0);\n}\n
climbing_stairs_backtrack.dart
/* 回溯 */\nvoid backtrack(List<int> choices, int state, int n, List<int> res) {\n  // 当爬到第 n 阶时,方案数量加 1\n  if (state == n) {\n    res[0]++;\n  }\n  // 遍历所有选择\n  for (int choice in choices) {\n    // 剪枝:不允许越过第 n 阶\n    if (state + choice > n) continue;\n    // 尝试:做出选择,更新状态\n    backtrack(choices, state + choice, n, res);\n    // 回退\n  }\n}\n\n/* 爬楼梯:回溯 */\nint climbingStairsBacktrack(int n) {\n  List<int> choices = [1, 2]; // 可选择向上爬 1 阶或 2 阶\n  int state = 0; // 从第 0 阶开始爬\n  List<int> res = [];\n  res.add(0); // 使用 res[0] 记录方案数量\n  backtrack(choices, state, n, res);\n  return res[0];\n}\n
climbing_stairs_backtrack.rs
/* 回溯 */\nfn backtrack(choices: &[i32], state: i32, n: i32, res: &mut [i32]) {\n    // 当爬到第 n 阶时,方案数量加 1\n    if state == n {\n        res[0] = res[0] + 1;\n    }\n    // 遍历所有选择\n    for &choice in choices {\n        // 剪枝:不允许越过第 n 阶\n        if state + choice > n {\n            continue;\n        }\n        // 尝试:做出选择,更新状态\n        backtrack(choices, state + choice, n, res);\n        // 回退\n    }\n}\n\n/* 爬楼梯:回溯 */\nfn climbing_stairs_backtrack(n: usize) -> i32 {\n    let choices = vec![1, 2]; // 可选择向上爬 1 阶或 2 阶\n    let state = 0; // 从第 0 阶开始爬\n    let mut res = Vec::new();\n    res.push(0); // 使用 res[0] 记录方案数量\n    backtrack(&choices, state, n as i32, &mut res);\n    res[0]\n}\n
climbing_stairs_backtrack.c
/* 回溯 */\nvoid backtrack(int *choices, int state, int n, int *res, int len) {\n    // 当爬到第 n 阶时,方案数量加 1\n    if (state == n)\n        res[0]++;\n    // 遍历所有选择\n    for (int i = 0; i < len; i++) {\n        int choice = choices[i];\n        // 剪枝:不允许越过第 n 阶\n        if (state + choice > n)\n            continue;\n        // 尝试:做出选择,更新状态\n        backtrack(choices, state + choice, n, res, len);\n        // 回退\n    }\n}\n\n/* 爬楼梯:回溯 */\nint climbingStairsBacktrack(int n) {\n    int choices[2] = {1, 2}; // 可选择向上爬 1 阶或 2 阶\n    int state = 0;           // 从第 0 阶开始爬\n    int *res = (int *)malloc(sizeof(int));\n    *res = 0; // 使用 res[0] 记录方案数量\n    int len = sizeof(choices) / sizeof(int);\n    backtrack(choices, state, n, res, len);\n    int result = *res;\n    free(res);\n    return result;\n}\n
climbing_stairs_backtrack.kt
/* 回溯 */\nfun backtrack(\n    choices: MutableList<Int>,\n    state: Int,\n    n: Int,\n    res: MutableList<Int>\n) {\n    // 当爬到第 n 阶时,方案数量加 1\n    if (state == n)\n        res[0] = res[0] + 1\n    // 遍历所有选择\n    for (choice in choices) {\n        // 剪枝:不允许越过第 n 阶\n        if (state + choice > n) continue\n        // 尝试:做出选择,更新状态\n        backtrack(choices, state + choice, n, res)\n        // 回退\n    }\n}\n\n/* 爬楼梯:回溯 */\nfun climbingStairsBacktrack(n: Int): Int {\n    val choices = mutableListOf(1, 2) // 可选择向上爬 1 阶或 2 阶\n    val state = 0 // 从第 0 阶开始爬\n    val res = mutableListOf<Int>()\n    res.add(0) // 使用 res[0] 记录方案数量\n    backtrack(choices, state, n, res)\n    return res[0]\n}\n
climbing_stairs_backtrack.rb
### 回溯 ###\ndef backtrack(choices, state, n, res)\n  # 当爬到第 n 阶时,方案数量加 1\n  res[0] += 1 if state == n\n  # 遍历所有选择\n  for choice in choices\n    # 剪枝:不允许越过第 n 阶\n    next if state + choice > n\n\n    # 尝试:做出选择,更新状态\n    backtrack(choices, state + choice, n, res)\n  end\n  # 回退\nend\n\n### 爬楼梯:回溯 ###\ndef climbing_stairs_backtrack(n)\n  choices = [1, 2] # 可选择向上爬 1 阶或 2 阶\n  state = 0 # 从第 0 阶开始爬\n  res = [0] # 使用 res[0] 记录方案数量\n  backtrack(choices, state, n, res)\n  res.first\nend\n
可视化运行

全屏观看 >

","path":["第 14 章   动态规划","14.1   初探动态规划"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1411","level":2,"title":"14.1.1   方法一:暴力搜索","text":"

回溯算法通常并不显式地对问题进行拆解,而是将求解问题看作一系列决策步骤,通过试探和剪枝,搜索所有可能的解。

我们可以尝试从问题分解的角度分析这道题。设爬到第 \\(i\\) 阶共有 \\(dp[i]\\) 种方案,那么 \\(dp[i]\\) 就是原问题,其子问题包括:

\\[ dp[i-1], dp[i-2], \\dots, dp[2], dp[1] \\]

由于每轮只能上 \\(1\\) 阶或 \\(2\\) 阶,因此当我们站在第 \\(i\\) 阶楼梯上时,上一轮只可能站在第 \\(i - 1\\) 阶或第 \\(i - 2\\) 阶上。换句话说,我们只能从第 \\(i -1\\) 阶或第 \\(i - 2\\) 阶迈向第 \\(i\\) 阶。

由此便可得出一个重要推论:爬到第 \\(i - 1\\) 阶的方案数加上爬到第 \\(i - 2\\) 阶的方案数就等于爬到第 \\(i\\) 阶的方案数。公式如下:

\\[ dp[i] = dp[i-1] + dp[i-2] \\]

这意味着在爬楼梯问题中,各个子问题之间存在递推关系,原问题的解可以由子问题的解构建得来。图 14-2 展示了该递推关系。

图 14-2   方案数量递推关系

我们可以根据递推公式得到暴力搜索解法。以 \\(dp[n]\\) 为起始点,递归地将一个较大问题拆解为两个较小问题的和,直至到达最小子问题 \\(dp[1]\\) 和 \\(dp[2]\\) 时返回。其中,最小子问题的解是已知的,即 \\(dp[1] = 1\\)、\\(dp[2] = 2\\) ,表示爬到第 \\(1\\)、\\(2\\) 阶分别有 \\(1\\)、\\(2\\) 种方案。

观察以下代码,它和标准回溯代码都属于深度优先搜索,但更加简洁:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dfs.py
def dfs(i: int) -> int:\n    \"\"\"搜索\"\"\"\n    # 已知 dp[1] 和 dp[2] ,返回之\n    if i == 1 or i == 2:\n        return i\n    # dp[i] = dp[i-1] + dp[i-2]\n    count = dfs(i - 1) + dfs(i - 2)\n    return count\n\ndef climbing_stairs_dfs(n: int) -> int:\n    \"\"\"爬楼梯:搜索\"\"\"\n    return dfs(n)\n
climbing_stairs_dfs.cpp
/* 搜索 */\nint dfs(int i) {\n    // 已知 dp[1] 和 dp[2] ,返回之\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* 爬楼梯:搜索 */\nint climbingStairsDFS(int n) {\n    return dfs(n);\n}\n
climbing_stairs_dfs.java
/* 搜索 */\nint dfs(int i) {\n    // 已知 dp[1] 和 dp[2] ,返回之\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* 爬楼梯:搜索 */\nint climbingStairsDFS(int n) {\n    return dfs(n);\n}\n
climbing_stairs_dfs.cs
/* 搜索 */\nint DFS(int i) {\n    // 已知 dp[1] 和 dp[2] ,返回之\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = DFS(i - 1) + DFS(i - 2);\n    return count;\n}\n\n/* 爬楼梯:搜索 */\nint ClimbingStairsDFS(int n) {\n    return DFS(n);\n}\n
climbing_stairs_dfs.go
/* 搜索 */\nfunc dfs(i int) int {\n    // 已知 dp[1] 和 dp[2] ,返回之\n    if i == 1 || i == 2 {\n        return i\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    count := dfs(i-1) + dfs(i-2)\n    return count\n}\n\n/* 爬楼梯:搜索 */\nfunc climbingStairsDFS(n int) int {\n    return dfs(n)\n}\n
climbing_stairs_dfs.swift
/* 搜索 */\nfunc dfs(i: Int) -> Int {\n    // 已知 dp[1] 和 dp[2] ,返回之\n    if i == 1 || i == 2 {\n        return i\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    let count = dfs(i: i - 1) + dfs(i: i - 2)\n    return count\n}\n\n/* 爬楼梯:搜索 */\nfunc climbingStairsDFS(n: Int) -> Int {\n    dfs(i: n)\n}\n
climbing_stairs_dfs.js
/* 搜索 */\nfunction dfs(i) {\n    // 已知 dp[1] 和 dp[2] ,返回之\n    if (i === 1 || i === 2) return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    const count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* 爬楼梯:搜索 */\nfunction climbingStairsDFS(n) {\n    return dfs(n);\n}\n
climbing_stairs_dfs.ts
/* 搜索 */\nfunction dfs(i: number): number {\n    // 已知 dp[1] 和 dp[2] ,返回之\n    if (i === 1 || i === 2) return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    const count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* 爬楼梯:搜索 */\nfunction climbingStairsDFS(n: number): number {\n    return dfs(n);\n}\n
climbing_stairs_dfs.dart
/* 搜索 */\nint dfs(int i) {\n  // 已知 dp[1] 和 dp[2] ,返回之\n  if (i == 1 || i == 2) return i;\n  // dp[i] = dp[i-1] + dp[i-2]\n  int count = dfs(i - 1) + dfs(i - 2);\n  return count;\n}\n\n/* 爬楼梯:搜索 */\nint climbingStairsDFS(int n) {\n  return dfs(n);\n}\n
climbing_stairs_dfs.rs
/* 搜索 */\nfn dfs(i: usize) -> i32 {\n    // 已知 dp[1] 和 dp[2] ,返回之\n    if i == 1 || i == 2 {\n        return i as i32;\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    let count = dfs(i - 1) + dfs(i - 2);\n    count\n}\n\n/* 爬楼梯:搜索 */\nfn climbing_stairs_dfs(n: usize) -> i32 {\n    dfs(n)\n}\n
climbing_stairs_dfs.c
/* 搜索 */\nint dfs(int i) {\n    // 已知 dp[1] 和 dp[2] ,返回之\n    if (i == 1 || i == 2)\n        return i;\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1) + dfs(i - 2);\n    return count;\n}\n\n/* 爬楼梯:搜索 */\nint climbingStairsDFS(int n) {\n    return dfs(n);\n}\n
climbing_stairs_dfs.kt
/* 搜索 */\nfun dfs(i: Int): Int {\n    // 已知 dp[1] 和 dp[2] ,返回之\n    if (i == 1 || i == 2) return i\n    // dp[i] = dp[i-1] + dp[i-2]\n    val count = dfs(i - 1) + dfs(i - 2)\n    return count\n}\n\n/* 爬楼梯:搜索 */\nfun climbingStairsDFS(n: Int): Int {\n    return dfs(n)\n}\n
climbing_stairs_dfs.rb
### 搜索 ###\ndef dfs(i)\n  # 已知 dp[1] 和 dp[2] ,返回之\n  return i if i == 1 || i == 2\n  # dp[i] = dp[i-1] + dp[i-2]\n  dfs(i - 1) + dfs(i - 2)\nend\n\n### 爬楼梯:搜索 ###\ndef climbing_stairs_dfs(n)\n  dfs(n)\nend\n
可视化运行

全屏观看 >

图 14-3 展示了暴力搜索形成的递归树。对于问题 \\(dp[n]\\) ,其递归树的深度为 \\(n\\) ,时间复杂度为 \\(O(2^n)\\) 。指数阶属于爆炸式增长,如果我们输入一个比较大的 \\(n\\) ,则会陷入漫长的等待之中。

图 14-3   爬楼梯对应递归树

观察图 14-3 ,指数阶的时间复杂度是“重叠子问题”导致的。例如 \\(dp[9]\\) 被分解为 \\(dp[8]\\) 和 \\(dp[7]\\) ,\\(dp[8]\\) 被分解为 \\(dp[7]\\) 和 \\(dp[6]\\) ,两者都包含子问题 \\(dp[7]\\) 。

以此类推,子问题中包含更小的重叠子问题,子子孙孙无穷尽也。绝大部分计算资源都浪费在这些重叠的子问题上。

","path":["第 14 章   动态规划","14.1   初探动态规划"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1412","level":2,"title":"14.1.2   方法二:记忆化搜索","text":"

为了提升算法效率,我们希望所有的重叠子问题都只被计算一次。为此,我们声明一个数组 mem 来记录每个子问题的解,并在搜索过程中将重叠子问题剪枝。

  1. 当首次计算 \\(dp[i]\\) 时,我们将其记录至 mem[i] ,以便之后使用。
  2. 当再次需要计算 \\(dp[i]\\) 时,我们便可直接从 mem[i] 中获取结果,从而避免重复计算该子问题。

代码如下所示:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dfs_mem.py
def dfs(i: int, mem: list[int]) -> int:\n    \"\"\"记忆化搜索\"\"\"\n    # 已知 dp[1] 和 dp[2] ,返回之\n    if i == 1 or i == 2:\n        return i\n    # 若存在记录 dp[i] ,则直接返回之\n    if mem[i] != -1:\n        return mem[i]\n    # dp[i] = dp[i-1] + dp[i-2]\n    count = dfs(i - 1, mem) + dfs(i - 2, mem)\n    # 记录 dp[i]\n    mem[i] = count\n    return count\n\ndef climbing_stairs_dfs_mem(n: int) -> int:\n    \"\"\"爬楼梯:记忆化搜索\"\"\"\n    # mem[i] 记录爬到第 i 阶的方案总数,-1 代表无记录\n    mem = [-1] * (n + 1)\n    return dfs(n, mem)\n
climbing_stairs_dfs_mem.cpp
/* 记忆化搜索 */\nint dfs(int i, vector<int> &mem) {\n    // 已知 dp[1] 和 dp[2] ,返回之\n    if (i == 1 || i == 2)\n        return i;\n    // 若存在记录 dp[i] ,则直接返回之\n    if (mem[i] != -1)\n        return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // 记录 dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* 爬楼梯:记忆化搜索 */\nint climbingStairsDFSMem(int n) {\n    // mem[i] 记录爬到第 i 阶的方案总数,-1 代表无记录\n    vector<int> mem(n + 1, -1);\n    return dfs(n, mem);\n}\n
climbing_stairs_dfs_mem.java
/* 记忆化搜索 */\nint dfs(int i, int[] mem) {\n    // 已知 dp[1] 和 dp[2] ,返回之\n    if (i == 1 || i == 2)\n        return i;\n    // 若存在记录 dp[i] ,则直接返回之\n    if (mem[i] != -1)\n        return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // 记录 dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* 爬楼梯:记忆化搜索 */\nint climbingStairsDFSMem(int n) {\n    // mem[i] 记录爬到第 i 阶的方案总数,-1 代表无记录\n    int[] mem = new int[n + 1];\n    Arrays.fill(mem, -1);\n    return dfs(n, mem);\n}\n
climbing_stairs_dfs_mem.cs
/* 记忆化搜索 */\nint DFS(int i, int[] mem) {\n    // 已知 dp[1] 和 dp[2] ,返回之\n    if (i == 1 || i == 2)\n        return i;\n    // 若存在记录 dp[i] ,则直接返回之\n    if (mem[i] != -1)\n        return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = DFS(i - 1, mem) + DFS(i - 2, mem);\n    // 记录 dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* 爬楼梯:记忆化搜索 */\nint ClimbingStairsDFSMem(int n) {\n    // mem[i] 记录爬到第 i 阶的方案总数,-1 代表无记录\n    int[] mem = new int[n + 1];\n    Array.Fill(mem, -1);\n    return DFS(n, mem);\n}\n
climbing_stairs_dfs_mem.go
/* 记忆化搜索 */\nfunc dfsMem(i int, mem []int) int {\n    // 已知 dp[1] 和 dp[2] ,返回之\n    if i == 1 || i == 2 {\n        return i\n    }\n    // 若存在记录 dp[i] ,则直接返回之\n    if mem[i] != -1 {\n        return mem[i]\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    count := dfsMem(i-1, mem) + dfsMem(i-2, mem)\n    // 记录 dp[i]\n    mem[i] = count\n    return count\n}\n\n/* 爬楼梯:记忆化搜索 */\nfunc climbingStairsDFSMem(n int) int {\n    // mem[i] 记录爬到第 i 阶的方案总数,-1 代表无记录\n    mem := make([]int, n+1)\n    for i := range mem {\n        mem[i] = -1\n    }\n    return dfsMem(n, mem)\n}\n
climbing_stairs_dfs_mem.swift
/* 记忆化搜索 */\nfunc dfs(i: Int, mem: inout [Int]) -> Int {\n    // 已知 dp[1] 和 dp[2] ,返回之\n    if i == 1 || i == 2 {\n        return i\n    }\n    // 若存在记录 dp[i] ,则直接返回之\n    if mem[i] != -1 {\n        return mem[i]\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    let count = dfs(i: i - 1, mem: &mem) + dfs(i: i - 2, mem: &mem)\n    // 记录 dp[i]\n    mem[i] = count\n    return count\n}\n\n/* 爬楼梯:记忆化搜索 */\nfunc climbingStairsDFSMem(n: Int) -> Int {\n    // mem[i] 记录爬到第 i 阶的方案总数,-1 代表无记录\n    var mem = Array(repeating: -1, count: n + 1)\n    return dfs(i: n, mem: &mem)\n}\n
climbing_stairs_dfs_mem.js
/* 记忆化搜索 */\nfunction dfs(i, mem) {\n    // 已知 dp[1] 和 dp[2] ,返回之\n    if (i === 1 || i === 2) return i;\n    // 若存在记录 dp[i] ,则直接返回之\n    if (mem[i] != -1) return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    const count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // 记录 dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* 爬楼梯:记忆化搜索 */\nfunction climbingStairsDFSMem(n) {\n    // mem[i] 记录爬到第 i 阶的方案总数,-1 代表无记录\n    const mem = new Array(n + 1).fill(-1);\n    return dfs(n, mem);\n}\n
climbing_stairs_dfs_mem.ts
/* 记忆化搜索 */\nfunction dfs(i: number, mem: number[]): number {\n    // 已知 dp[1] 和 dp[2] ,返回之\n    if (i === 1 || i === 2) return i;\n    // 若存在记录 dp[i] ,则直接返回之\n    if (mem[i] != -1) return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    const count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // 记录 dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* 爬楼梯:记忆化搜索 */\nfunction climbingStairsDFSMem(n: number): number {\n    // mem[i] 记录爬到第 i 阶的方案总数,-1 代表无记录\n    const mem = new Array(n + 1).fill(-1);\n    return dfs(n, mem);\n}\n
climbing_stairs_dfs_mem.dart
/* 记忆化搜索 */\nint dfs(int i, List<int> mem) {\n  // 已知 dp[1] 和 dp[2] ,返回之\n  if (i == 1 || i == 2) return i;\n  // 若存在记录 dp[i] ,则直接返回之\n  if (mem[i] != -1) return mem[i];\n  // dp[i] = dp[i-1] + dp[i-2]\n  int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n  // 记录 dp[i]\n  mem[i] = count;\n  return count;\n}\n\n/* 爬楼梯:记忆化搜索 */\nint climbingStairsDFSMem(int n) {\n  // mem[i] 记录爬到第 i 阶的方案总数,-1 代表无记录\n  List<int> mem = List.filled(n + 1, -1);\n  return dfs(n, mem);\n}\n
climbing_stairs_dfs_mem.rs
/* 记忆化搜索 */\nfn dfs(i: usize, mem: &mut [i32]) -> i32 {\n    // 已知 dp[1] 和 dp[2] ,返回之\n    if i == 1 || i == 2 {\n        return i as i32;\n    }\n    // 若存在记录 dp[i] ,则直接返回之\n    if mem[i] != -1 {\n        return mem[i];\n    }\n    // dp[i] = dp[i-1] + dp[i-2]\n    let count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // 记录 dp[i]\n    mem[i] = count;\n    count\n}\n\n/* 爬楼梯:记忆化搜索 */\nfn climbing_stairs_dfs_mem(n: usize) -> i32 {\n    // mem[i] 记录爬到第 i 阶的方案总数,-1 代表无记录\n    let mut mem = vec![-1; n + 1];\n    dfs(n, &mut mem)\n}\n
climbing_stairs_dfs_mem.c
/* 记忆化搜索 */\nint dfs(int i, int *mem) {\n    // 已知 dp[1] 和 dp[2] ,返回之\n    if (i == 1 || i == 2)\n        return i;\n    // 若存在记录 dp[i] ,则直接返回之\n    if (mem[i] != -1)\n        return mem[i];\n    // dp[i] = dp[i-1] + dp[i-2]\n    int count = dfs(i - 1, mem) + dfs(i - 2, mem);\n    // 记录 dp[i]\n    mem[i] = count;\n    return count;\n}\n\n/* 爬楼梯:记忆化搜索 */\nint climbingStairsDFSMem(int n) {\n    // mem[i] 记录爬到第 i 阶的方案总数,-1 代表无记录\n    int *mem = (int *)malloc((n + 1) * sizeof(int));\n    for (int i = 0; i <= n; i++) {\n        mem[i] = -1;\n    }\n    int result = dfs(n, mem);\n    free(mem);\n    return result;\n}\n
climbing_stairs_dfs_mem.kt
/* 记忆化搜索 */\nfun dfs(i: Int, mem: IntArray): Int {\n    // 已知 dp[1] 和 dp[2] ,返回之\n    if (i == 1 || i == 2) return i\n    // 若存在记录 dp[i] ,则直接返回之\n    if (mem[i] != -1) return mem[i]\n    // dp[i] = dp[i-1] + dp[i-2]\n    val count = dfs(i - 1, mem) + dfs(i - 2, mem)\n    // 记录 dp[i]\n    mem[i] = count\n    return count\n}\n\n/* 爬楼梯:记忆化搜索 */\nfun climbingStairsDFSMem(n: Int): Int {\n    // mem[i] 记录爬到第 i 阶的方案总数,-1 代表无记录\n    val mem = IntArray(n + 1)\n    mem.fill(-1)\n    return dfs(n, mem)\n}\n
climbing_stairs_dfs_mem.rb
### 记忆化搜索 ###\ndef dfs(i, mem)\n  # 已知 dp[1] 和 dp[2] ,返回之\n  return i if i == 1 || i == 2\n  # 若存在记录 dp[i] ,则直接返回之\n  return mem[i] if mem[i] != -1\n\n  # dp[i] = dp[i-1] + dp[i-2]\n  count = dfs(i - 1, mem) + dfs(i - 2, mem)\n  # 记录 dp[i]\n  mem[i] = count\nend\n\n### 爬楼梯:记忆化搜索 ###\ndef climbing_stairs_dfs_mem(n)\n  # mem[i] 记录爬到第 i 阶的方案总数,-1 代表无记录\n  mem = Array.new(n + 1, -1)\n  dfs(n, mem)\nend\n
可视化运行

全屏观看 >

观察图 14-4 ,经过记忆化处理后,所有重叠子问题都只需计算一次,时间复杂度优化至 \\(O(n)\\) ,这是一个巨大的飞跃。

图 14-4   记忆化搜索对应递归树

","path":["第 14 章   动态规划","14.1   初探动态规划"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1413","level":2,"title":"14.1.3   方法三:动态规划","text":"

记忆化搜索是一种“从顶至底”的方法:我们从原问题(根节点)开始,递归地将较大子问题分解为较小子问题,直至解已知的最小子问题(叶节点)。之后,通过回溯逐层收集子问题的解,构建出原问题的解。

与之相反,动态规划是一种“从底至顶”的方法:从最小子问题的解开始,迭代地构建更大子问题的解,直至得到原问题的解。

由于动态规划不包含回溯过程,因此只需使用循环迭代实现,无须使用递归。在以下代码中,我们初始化一个数组 dp 来存储子问题的解,它起到了与记忆化搜索中数组 mem 相同的记录作用:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dp.py
def climbing_stairs_dp(n: int) -> int:\n    \"\"\"爬楼梯:动态规划\"\"\"\n    if n == 1 or n == 2:\n        return n\n    # 初始化 dp 表,用于存储子问题的解\n    dp = [0] * (n + 1)\n    # 初始状态:预设最小子问题的解\n    dp[1], dp[2] = 1, 2\n    # 状态转移:从较小子问题逐步求解较大子问题\n    for i in range(3, n + 1):\n        dp[i] = dp[i - 1] + dp[i - 2]\n    return dp[n]\n
climbing_stairs_dp.cpp
/* 爬楼梯:动态规划 */\nint climbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // 初始化 dp 表,用于存储子问题的解\n    vector<int> dp(n + 1);\n    // 初始状态:预设最小子问题的解\n    dp[1] = 1;\n    dp[2] = 2;\n    // 状态转移:从较小子问题逐步求解较大子问题\n    for (int i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
climbing_stairs_dp.java
/* 爬楼梯:动态规划 */\nint climbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // 初始化 dp 表,用于存储子问题的解\n    int[] dp = new int[n + 1];\n    // 初始状态:预设最小子问题的解\n    dp[1] = 1;\n    dp[2] = 2;\n    // 状态转移:从较小子问题逐步求解较大子问题\n    for (int i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
climbing_stairs_dp.cs
/* 爬楼梯:动态规划 */\nint ClimbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // 初始化 dp 表,用于存储子问题的解\n    int[] dp = new int[n + 1];\n    // 初始状态:预设最小子问题的解\n    dp[1] = 1;\n    dp[2] = 2;\n    // 状态转移:从较小子问题逐步求解较大子问题\n    for (int i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
climbing_stairs_dp.go
/* 爬楼梯:动态规划 */\nfunc climbingStairsDP(n int) int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    // 初始化 dp 表,用于存储子问题的解\n    dp := make([]int, n+1)\n    // 初始状态:预设最小子问题的解\n    dp[1] = 1\n    dp[2] = 2\n    // 状态转移:从较小子问题逐步求解较大子问题\n    for i := 3; i <= n; i++ {\n        dp[i] = dp[i-1] + dp[i-2]\n    }\n    return dp[n]\n}\n
climbing_stairs_dp.swift
/* 爬楼梯:动态规划 */\nfunc climbingStairsDP(n: Int) -> Int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    // 初始化 dp 表,用于存储子问题的解\n    var dp = Array(repeating: 0, count: n + 1)\n    // 初始状态:预设最小子问题的解\n    dp[1] = 1\n    dp[2] = 2\n    // 状态转移:从较小子问题逐步求解较大子问题\n    for i in 3 ... n {\n        dp[i] = dp[i - 1] + dp[i - 2]\n    }\n    return dp[n]\n}\n
climbing_stairs_dp.js
/* 爬楼梯:动态规划 */\nfunction climbingStairsDP(n) {\n    if (n === 1 || n === 2) return n;\n    // 初始化 dp 表,用于存储子问题的解\n    const dp = new Array(n + 1).fill(-1);\n    // 初始状态:预设最小子问题的解\n    dp[1] = 1;\n    dp[2] = 2;\n    // 状态转移:从较小子问题逐步求解较大子问题\n    for (let i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
climbing_stairs_dp.ts
/* 爬楼梯:动态规划 */\nfunction climbingStairsDP(n: number): number {\n    if (n === 1 || n === 2) return n;\n    // 初始化 dp 表,用于存储子问题的解\n    const dp = new Array(n + 1).fill(-1);\n    // 初始状态:预设最小子问题的解\n    dp[1] = 1;\n    dp[2] = 2;\n    // 状态转移:从较小子问题逐步求解较大子问题\n    for (let i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    return dp[n];\n}\n
climbing_stairs_dp.dart
/* 爬楼梯:动态规划 */\nint climbingStairsDP(int n) {\n  if (n == 1 || n == 2) return n;\n  // 初始化 dp 表,用于存储子问题的解\n  List<int> dp = List.filled(n + 1, 0);\n  // 初始状态:预设最小子问题的解\n  dp[1] = 1;\n  dp[2] = 2;\n  // 状态转移:从较小子问题逐步求解较大子问题\n  for (int i = 3; i <= n; i++) {\n    dp[i] = dp[i - 1] + dp[i - 2];\n  }\n  return dp[n];\n}\n
climbing_stairs_dp.rs
/* 爬楼梯:动态规划 */\nfn climbing_stairs_dp(n: usize) -> i32 {\n    // 已知 dp[1] 和 dp[2] ,返回之\n    if n == 1 || n == 2 {\n        return n as i32;\n    }\n    // 初始化 dp 表,用于存储子问题的解\n    let mut dp = vec![-1; n + 1];\n    // 初始状态:预设最小子问题的解\n    dp[1] = 1;\n    dp[2] = 2;\n    // 状态转移:从较小子问题逐步求解较大子问题\n    for i in 3..=n {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    dp[n]\n}\n
climbing_stairs_dp.c
/* 爬楼梯:动态规划 */\nint climbingStairsDP(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    // 初始化 dp 表,用于存储子问题的解\n    int *dp = (int *)malloc((n + 1) * sizeof(int));\n    // 初始状态:预设最小子问题的解\n    dp[1] = 1;\n    dp[2] = 2;\n    // 状态转移:从较小子问题逐步求解较大子问题\n    for (int i = 3; i <= n; i++) {\n        dp[i] = dp[i - 1] + dp[i - 2];\n    }\n    int result = dp[n];\n    free(dp);\n    return result;\n}\n
climbing_stairs_dp.kt
/* 爬楼梯:动态规划 */\nfun climbingStairsDP(n: Int): Int {\n    if (n == 1 || n == 2) return n\n    // 初始化 dp 表,用于存储子问题的解\n    val dp = IntArray(n + 1)\n    // 初始状态:预设最小子问题的解\n    dp[1] = 1\n    dp[2] = 2\n    // 状态转移:从较小子问题逐步求解较大子问题\n    for (i in 3..n) {\n        dp[i] = dp[i - 1] + dp[i - 2]\n    }\n    return dp[n]\n}\n
climbing_stairs_dp.rb
### 爬楼梯:动态规划 ###\ndef climbing_stairs_dp(n)\n  return n  if n == 1 || n == 2\n\n  # 初始化 dp 表,用于存储子问题的解\n  dp = Array.new(n + 1, 0)\n  # 初始状态:预设最小子问题的解\n  dp[1], dp[2] = 1, 2\n  # 状态转移:从较小子问题逐步求解较大子问题\n  (3...(n + 1)).each { |i| dp[i] = dp[i - 1] + dp[i - 2] }\n\n  dp[n]\nend\n
可视化运行

全屏观看 >

图 14-5 模拟了以上代码的执行过程。

图 14-5   爬楼梯的动态规划过程

与回溯算法一样,动态规划也使用“状态”概念来表示问题求解的特定阶段,每个状态都对应一个子问题以及相应的局部最优解。例如,爬楼梯问题的状态定义为当前所在楼梯阶数 \\(i\\) 。

根据以上内容,我们可以总结出动态规划的常用术语。

","path":["第 14 章   动态规划","14.1   初探动态规划"],"tags":[]},{"location":"chapter_dynamic_programming/intro_to_dynamic_programming/#1414","level":2,"title":"14.1.4   空间优化","text":"

细心的读者可能发现了,由于 \\(dp[i]\\) 只与 \\(dp[i-1]\\) 和 \\(dp[i-2]\\) 有关,因此我们无须使用一个数组 dp 来存储所有子问题的解,而只需两个变量滚动前进即可。代码如下所示:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby climbing_stairs_dp.py
def climbing_stairs_dp_comp(n: int) -> int:\n    \"\"\"爬楼梯:空间优化后的动态规划\"\"\"\n    if n == 1 or n == 2:\n        return n\n    a, b = 1, 2\n    for _ in range(3, n + 1):\n        a, b = b, a + b\n    return b\n
climbing_stairs_dp.cpp
/* 爬楼梯:空间优化后的动态规划 */\nint climbingStairsDPComp(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    int a = 1, b = 2;\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
climbing_stairs_dp.java
/* 爬楼梯:空间优化后的动态规划 */\nint climbingStairsDPComp(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    int a = 1, b = 2;\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
climbing_stairs_dp.cs
/* 爬楼梯:空间优化后的动态规划 */\nint ClimbingStairsDPComp(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    int a = 1, b = 2;\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
climbing_stairs_dp.go
/* 爬楼梯:空间优化后的动态规划 */\nfunc climbingStairsDPComp(n int) int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    a, b := 1, 2\n    // 状态转移:从较小子问题逐步求解较大子问题\n    for i := 3; i <= n; i++ {\n        a, b = b, a+b\n    }\n    return b\n}\n
climbing_stairs_dp.swift
/* 爬楼梯:空间优化后的动态规划 */\nfunc climbingStairsDPComp(n: Int) -> Int {\n    if n == 1 || n == 2 {\n        return n\n    }\n    var a = 1\n    var b = 2\n    for _ in 3 ... n {\n        (a, b) = (b, a + b)\n    }\n    return b\n}\n
climbing_stairs_dp.js
/* 爬楼梯:空间优化后的动态规划 */\nfunction climbingStairsDPComp(n) {\n    if (n === 1 || n === 2) return n;\n    let a = 1,\n        b = 2;\n    for (let i = 3; i <= n; i++) {\n        const tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
climbing_stairs_dp.ts
/* 爬楼梯:空间优化后的动态规划 */\nfunction climbingStairsDPComp(n: number): number {\n    if (n === 1 || n === 2) return n;\n    let a = 1,\n        b = 2;\n    for (let i = 3; i <= n; i++) {\n        const tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
climbing_stairs_dp.dart
/* 爬楼梯:空间优化后的动态规划 */\nint climbingStairsDPComp(int n) {\n  if (n == 1 || n == 2) return n;\n  int a = 1, b = 2;\n  for (int i = 3; i <= n; i++) {\n    int tmp = b;\n    b = a + b;\n    a = tmp;\n  }\n  return b;\n}\n
climbing_stairs_dp.rs
/* 爬楼梯:空间优化后的动态规划 */\nfn climbing_stairs_dp_comp(n: usize) -> i32 {\n    if n == 1 || n == 2 {\n        return n as i32;\n    }\n    let (mut a, mut b) = (1, 2);\n    for _ in 3..=n {\n        let tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    b\n}\n
climbing_stairs_dp.c
/* 爬楼梯:空间优化后的动态规划 */\nint climbingStairsDPComp(int n) {\n    if (n == 1 || n == 2)\n        return n;\n    int a = 1, b = 2;\n    for (int i = 3; i <= n; i++) {\n        int tmp = b;\n        b = a + b;\n        a = tmp;\n    }\n    return b;\n}\n
climbing_stairs_dp.kt
/* 爬楼梯:空间优化后的动态规划 */\nfun climbingStairsDPComp(n: Int): Int {\n    if (n == 1 || n == 2) return n\n    var a = 1\n    var b = 2\n    for (i in 3..n) {\n        val temp = b\n        b += a\n        a = temp\n    }\n    return b\n}\n
climbing_stairs_dp.rb
### 爬楼梯:空间优化后的动态规划 ###\ndef climbing_stairs_dp_comp(n)\n  return n if n == 1 || n == 2\n\n  a, b = 1, 2\n  (3...(n + 1)).each { a, b = b, a + b }\n\n  b\nend\n
可视化运行

全屏观看 >

观察以上代码,由于省去了数组 dp 占用的空间,因此空间复杂度从 \\(O(n)\\) 降至 \\(O(1)\\) 。

在动态规划问题中,当前状态往往仅与前面有限个状态有关,这时我们可以只保留必要的状态,通过“降维”来节省内存空间。这种空间优化技巧被称为“滚动变量”或“滚动数组”。

","path":["第 14 章   动态规划","14.1   初探动态规划"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/","level":1,"title":"14.4   0-1 背包问题","text":"

背包问题是一个非常好的动态规划入门题目,是动态规划中最常见的问题形式。其具有很多变种,例如 0-1 背包问题、完全背包问题、多重背包问题等。

在本节中,我们先来求解最常见的 0-1 背包问题。

Question

给定 \\(n\\) 个物品,第 \\(i\\) 个物品的重量为 \\(wgt[i-1]\\)、价值为 \\(val[i-1]\\) ,和一个容量为 \\(cap\\) 的背包。每个物品只能选择一次,问在限定背包容量下能放入物品的最大价值。

观察图 14-17 ,由于物品编号 \\(i\\) 从 \\(1\\) 开始计数,数组索引从 \\(0\\) 开始计数,因此物品 \\(i\\) 对应重量 \\(wgt[i-1]\\) 和价值 \\(val[i-1]\\) 。

图 14-17   0-1 背包的示例数据

我们可以将 0-1 背包问题看作一个由 \\(n\\) 轮决策组成的过程,对于每个物体都有不放入和放入两种决策,因此该问题满足决策树模型。

该问题的目标是求解“在限定背包容量下能放入物品的最大价值”,因此较大概率是一个动态规划问题。

第一步:思考每轮的决策,定义状态,从而得到 \\(dp\\) 表

对于每个物品来说,不放入背包,背包容量不变;放入背包,背包容量减小。由此可得状态定义:当前物品编号 \\(i\\) 和背包容量 \\(c\\) ,记为 \\([i, c]\\) 。

状态 \\([i, c]\\) 对应的子问题为:前 \\(i\\) 个物品在容量为 \\(c\\) 的背包中的最大价值,记为 \\(dp[i, c]\\) 。

待求解的是 \\(dp[n, cap]\\) ,因此需要一个尺寸为 \\((n+1) \\times (cap+1)\\) 的二维 \\(dp\\) 表。

第二步:找出最优子结构,进而推导出状态转移方程

当我们做出物品 \\(i\\) 的决策后,剩余的是前 \\(i-1\\) 个物品决策的子问题,可分为以下两种情况。

上述分析向我们揭示了本题的最优子结构:最大价值 \\(dp[i, c]\\) 等于不放入物品 \\(i\\) 和放入物品 \\(i\\) 两种方案中价值更大的那一个。由此可推导出状态转移方程:

\\[ dp[i, c] = \\max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1]) \\]

需要注意的是,若当前物品重量 \\(wgt[i - 1]\\) 超出剩余背包容量 \\(c\\) ,则只能选择不放入背包。

第三步:确定边界条件和状态转移顺序

当无物品或背包容量为 \\(0\\) 时最大价值为 \\(0\\) ,即首列 \\(dp[i, 0]\\) 和首行 \\(dp[0, c]\\) 都等于 \\(0\\) 。

当前状态 \\([i, c]\\) 从上方的状态 \\([i-1, c]\\) 和左上方的状态 \\([i-1, c-wgt[i-1]]\\) 转移而来,因此通过两层循环正序遍历整个 \\(dp\\) 表即可。

根据以上分析,我们接下来按顺序实现暴力搜索、记忆化搜索、动态规划解法。

","path":["第 14 章   动态规划","14.4   0-1 背包问题"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#1","level":3,"title":"1.   方法一:暴力搜索","text":"

搜索代码包含以下要素。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
def knapsack_dfs(wgt: list[int], val: list[int], i: int, c: int) -> int:\n    \"\"\"0-1 背包:暴力搜索\"\"\"\n    # 若已选完所有物品或背包无剩余容量,则返回价值 0\n    if i == 0 or c == 0:\n        return 0\n    # 若超过背包容量,则只能选择不放入背包\n    if wgt[i - 1] > c:\n        return knapsack_dfs(wgt, val, i - 1, c)\n    # 计算不放入和放入物品 i 的最大价值\n    no = knapsack_dfs(wgt, val, i - 1, c)\n    yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]\n    # 返回两种方案中价值更大的那一个\n    return max(no, yes)\n
knapsack.cpp
/* 0-1 背包:暴力搜索 */\nint knapsackDFS(vector<int> &wgt, vector<int> &val, int i, int c) {\n    // 若已选完所有物品或背包无剩余容量,则返回价值 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // 若超过背包容量,则只能选择不放入背包\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // 计算不放入和放入物品 i 的最大价值\n    int no = knapsackDFS(wgt, val, i - 1, c);\n    int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // 返回两种方案中价值更大的那一个\n    return max(no, yes);\n}\n
knapsack.java
/* 0-1 背包:暴力搜索 */\nint knapsackDFS(int[] wgt, int[] val, int i, int c) {\n    // 若已选完所有物品或背包无剩余容量,则返回价值 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // 若超过背包容量,则只能选择不放入背包\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // 计算不放入和放入物品 i 的最大价值\n    int no = knapsackDFS(wgt, val, i - 1, c);\n    int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // 返回两种方案中价值更大的那一个\n    return Math.max(no, yes);\n}\n
knapsack.cs
/* 0-1 背包:暴力搜索 */\nint KnapsackDFS(int[] weight, int[] val, int i, int c) {\n    // 若已选完所有物品或背包无剩余容量,则返回价值 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // 若超过背包容量,则只能选择不放入背包\n    if (weight[i - 1] > c) {\n        return KnapsackDFS(weight, val, i - 1, c);\n    }\n    // 计算不放入和放入物品 i 的最大价值\n    int no = KnapsackDFS(weight, val, i - 1, c);\n    int yes = KnapsackDFS(weight, val, i - 1, c - weight[i - 1]) + val[i - 1];\n    // 返回两种方案中价值更大的那一个\n    return Math.Max(no, yes);\n}\n
knapsack.go
/* 0-1 背包:暴力搜索 */\nfunc knapsackDFS(wgt, val []int, i, c int) int {\n    // 若已选完所有物品或背包无剩余容量,则返回价值 0\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // 若超过背包容量,则只能选择不放入背包\n    if wgt[i-1] > c {\n        return knapsackDFS(wgt, val, i-1, c)\n    }\n    // 计算不放入和放入物品 i 的最大价值\n    no := knapsackDFS(wgt, val, i-1, c)\n    yes := knapsackDFS(wgt, val, i-1, c-wgt[i-1]) + val[i-1]\n    // 返回两种方案中价值更大的那一个\n    return int(math.Max(float64(no), float64(yes)))\n}\n
knapsack.swift
/* 0-1 背包:暴力搜索 */\nfunc knapsackDFS(wgt: [Int], val: [Int], i: Int, c: Int) -> Int {\n    // 若已选完所有物品或背包无剩余容量,则返回价值 0\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // 若超过背包容量,则只能选择不放入背包\n    if wgt[i - 1] > c {\n        return knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c)\n    }\n    // 计算不放入和放入物品 i 的最大价值\n    let no = knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c)\n    let yes = knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c - wgt[i - 1]) + val[i - 1]\n    // 返回两种方案中价值更大的那一个\n    return max(no, yes)\n}\n
knapsack.js
/* 0-1 背包:暴力搜索 */\nfunction knapsackDFS(wgt, val, i, c) {\n    // 若已选完所有物品或背包无剩余容量,则返回价值 0\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // 若超过背包容量,则只能选择不放入背包\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // 计算不放入和放入物品 i 的最大价值\n    const no = knapsackDFS(wgt, val, i - 1, c);\n    const yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // 返回两种方案中价值更大的那一个\n    return Math.max(no, yes);\n}\n
knapsack.ts
/* 0-1 背包:暴力搜索 */\nfunction knapsackDFS(\n    wgt: Array<number>,\n    val: Array<number>,\n    i: number,\n    c: number\n): number {\n    // 若已选完所有物品或背包无剩余容量,则返回价值 0\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // 若超过背包容量,则只能选择不放入背包\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // 计算不放入和放入物品 i 的最大价值\n    const no = knapsackDFS(wgt, val, i - 1, c);\n    const yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // 返回两种方案中价值更大的那一个\n    return Math.max(no, yes);\n}\n
knapsack.dart
/* 0-1 背包:暴力搜索 */\nint knapsackDFS(List<int> wgt, List<int> val, int i, int c) {\n  // 若已选完所有物品或背包无剩余容量,则返回价值 0\n  if (i == 0 || c == 0) {\n    return 0;\n  }\n  // 若超过背包容量,则只能选择不放入背包\n  if (wgt[i - 1] > c) {\n    return knapsackDFS(wgt, val, i - 1, c);\n  }\n  // 计算不放入和放入物品 i 的最大价值\n  int no = knapsackDFS(wgt, val, i - 1, c);\n  int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n  // 返回两种方案中价值更大的那一个\n  return max(no, yes);\n}\n
knapsack.rs
/* 0-1 背包:暴力搜索 */\nfn knapsack_dfs(wgt: &[i32], val: &[i32], i: usize, c: usize) -> i32 {\n    // 若已选完所有物品或背包无剩余容量,则返回价值 0\n    if i == 0 || c == 0 {\n        return 0;\n    }\n    // 若超过背包容量,则只能选择不放入背包\n    if wgt[i - 1] > c as i32 {\n        return knapsack_dfs(wgt, val, i - 1, c);\n    }\n    // 计算不放入和放入物品 i 的最大价值\n    let no = knapsack_dfs(wgt, val, i - 1, c);\n    let yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1] as usize) + val[i - 1];\n    // 返回两种方案中价值更大的那一个\n    std::cmp::max(no, yes)\n}\n
knapsack.c
/* 0-1 背包:暴力搜索 */\nint knapsackDFS(int wgt[], int val[], int i, int c) {\n    // 若已选完所有物品或背包无剩余容量,则返回价值 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // 若超过背包容量,则只能选择不放入背包\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, val, i - 1, c);\n    }\n    // 计算不放入和放入物品 i 的最大价值\n    int no = knapsackDFS(wgt, val, i - 1, c);\n    int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // 返回两种方案中价值更大的那一个\n    return myMax(no, yes);\n}\n
knapsack.kt
/* 0-1 背包:暴力搜索 */\nfun knapsackDFS(\n    wgt: IntArray,\n    _val: IntArray,\n    i: Int,\n    c: Int\n): Int {\n    // 若已选完所有物品或背包无剩余容量,则返回价值 0\n    if (i == 0 || c == 0) {\n        return 0\n    }\n    // 若超过背包容量,则只能选择不放入背包\n    if (wgt[i - 1] > c) {\n        return knapsackDFS(wgt, _val, i - 1, c)\n    }\n    // 计算不放入和放入物品 i 的最大价值\n    val no = knapsackDFS(wgt, _val, i - 1, c)\n    val yes = knapsackDFS(wgt, _val, i - 1, c - wgt[i - 1]) + _val[i - 1]\n    // 返回两种方案中价值更大的那一个\n    return max(no, yes)\n}\n
knapsack.rb
### 0-1 背包:暴力搜索 ###\ndef knapsack_dfs(wgt, val, i, c)\n  # 若已选完所有物品或背包无剩余容量,则返回价值 0\n  return 0 if i == 0 || c == 0\n  # 若超过背包容量,则只能选择不放入背包\n  return knapsack_dfs(wgt, val, i - 1, c) if wgt[i - 1] > c\n  # 计算不放入和放入物品 i 的最大价值\n  no = knapsack_dfs(wgt, val, i - 1, c)\n  yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]\n  # 返回两种方案中价值更大的那一个\n  [no, yes].max\nend\n
可视化运行

全屏观看 >

如图 14-18 所示,由于每个物品都会产生不选和选两条搜索分支,因此时间复杂度为 \\(O(2^n)\\) 。

观察递归树,容易发现其中存在重叠子问题,例如 \\(dp[1, 10]\\) 等。而当物品较多、背包容量较大,尤其是相同重量的物品较多时,重叠子问题的数量将会大幅增多。

图 14-18   0-1 背包问题的暴力搜索递归树

","path":["第 14 章   动态规划","14.4   0-1 背包问题"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#2","level":3,"title":"2.   方法二:记忆化搜索","text":"

为了保证重叠子问题只被计算一次,我们借助记忆列表 mem 来记录子问题的解,其中 mem[i][c] 对应 \\(dp[i, c]\\) 。

引入记忆化之后,时间复杂度取决于子问题数量,也就是 \\(O(n \\times cap)\\) 。实现代码如下:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
def knapsack_dfs_mem(\n    wgt: list[int], val: list[int], mem: list[list[int]], i: int, c: int\n) -> int:\n    \"\"\"0-1 背包:记忆化搜索\"\"\"\n    # 若已选完所有物品或背包无剩余容量,则返回价值 0\n    if i == 0 or c == 0:\n        return 0\n    # 若已有记录,则直接返回\n    if mem[i][c] != -1:\n        return mem[i][c]\n    # 若超过背包容量,则只能选择不放入背包\n    if wgt[i - 1] > c:\n        return knapsack_dfs_mem(wgt, val, mem, i - 1, c)\n    # 计算不放入和放入物品 i 的最大价值\n    no = knapsack_dfs_mem(wgt, val, mem, i - 1, c)\n    yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1]\n    # 记录并返回两种方案中价值更大的那一个\n    mem[i][c] = max(no, yes)\n    return mem[i][c]\n
knapsack.cpp
/* 0-1 背包:记忆化搜索 */\nint knapsackDFSMem(vector<int> &wgt, vector<int> &val, vector<vector<int>> &mem, int i, int c) {\n    // 若已选完所有物品或背包无剩余容量,则返回价值 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // 若已有记录,则直接返回\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // 若超过背包容量,则只能选择不放入背包\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // 计算不放入和放入物品 i 的最大价值\n    int no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n    int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // 记录并返回两种方案中价值更大的那一个\n    mem[i][c] = max(no, yes);\n    return mem[i][c];\n}\n
knapsack.java
/* 0-1 背包:记忆化搜索 */\nint knapsackDFSMem(int[] wgt, int[] val, int[][] mem, int i, int c) {\n    // 若已选完所有物品或背包无剩余容量,则返回价值 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // 若已有记录,则直接返回\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // 若超过背包容量,则只能选择不放入背包\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // 计算不放入和放入物品 i 的最大价值\n    int no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n    int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // 记录并返回两种方案中价值更大的那一个\n    mem[i][c] = Math.max(no, yes);\n    return mem[i][c];\n}\n
knapsack.cs
/* 0-1 背包:记忆化搜索 */\nint KnapsackDFSMem(int[] weight, int[] val, int[][] mem, int i, int c) {\n    // 若已选完所有物品或背包无剩余容量,则返回价值 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // 若已有记录,则直接返回\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // 若超过背包容量,则只能选择不放入背包\n    if (weight[i - 1] > c) {\n        return KnapsackDFSMem(weight, val, mem, i - 1, c);\n    }\n    // 计算不放入和放入物品 i 的最大价值\n    int no = KnapsackDFSMem(weight, val, mem, i - 1, c);\n    int yes = KnapsackDFSMem(weight, val, mem, i - 1, c - weight[i - 1]) + val[i - 1];\n    // 记录并返回两种方案中价值更大的那一个\n    mem[i][c] = Math.Max(no, yes);\n    return mem[i][c];\n}\n
knapsack.go
/* 0-1 背包:记忆化搜索 */\nfunc knapsackDFSMem(wgt, val []int, mem [][]int, i, c int) int {\n    // 若已选完所有物品或背包无剩余容量,则返回价值 0\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // 若已有记录,则直接返回\n    if mem[i][c] != -1 {\n        return mem[i][c]\n    }\n    // 若超过背包容量,则只能选择不放入背包\n    if wgt[i-1] > c {\n        return knapsackDFSMem(wgt, val, mem, i-1, c)\n    }\n    // 计算不放入和放入物品 i 的最大价值\n    no := knapsackDFSMem(wgt, val, mem, i-1, c)\n    yes := knapsackDFSMem(wgt, val, mem, i-1, c-wgt[i-1]) + val[i-1]\n    // 返回两种方案中价值更大的那一个\n    mem[i][c] = int(math.Max(float64(no), float64(yes)))\n    return mem[i][c]\n}\n
knapsack.swift
/* 0-1 背包:记忆化搜索 */\nfunc knapsackDFSMem(wgt: [Int], val: [Int], mem: inout [[Int]], i: Int, c: Int) -> Int {\n    // 若已选完所有物品或背包无剩余容量,则返回价值 0\n    if i == 0 || c == 0 {\n        return 0\n    }\n    // 若已有记录,则直接返回\n    if mem[i][c] != -1 {\n        return mem[i][c]\n    }\n    // 若超过背包容量,则只能选择不放入背包\n    if wgt[i - 1] > c {\n        return knapsackDFSMem(wgt: wgt, val: val, mem: &mem, i: i - 1, c: c)\n    }\n    // 计算不放入和放入物品 i 的最大价值\n    let no = knapsackDFSMem(wgt: wgt, val: val, mem: &mem, i: i - 1, c: c)\n    let yes = knapsackDFSMem(wgt: wgt, val: val, mem: &mem, i: i - 1, c: c - wgt[i - 1]) + val[i - 1]\n    // 记录并返回两种方案中价值更大的那一个\n    mem[i][c] = max(no, yes)\n    return mem[i][c]\n}\n
knapsack.js
/* 0-1 背包:记忆化搜索 */\nfunction knapsackDFSMem(wgt, val, mem, i, c) {\n    // 若已选完所有物品或背包无剩余容量,则返回价值 0\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // 若已有记录,则直接返回\n    if (mem[i][c] !== -1) {\n        return mem[i][c];\n    }\n    // 若超过背包容量,则只能选择不放入背包\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // 计算不放入和放入物品 i 的最大价值\n    const no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n    const yes =\n        knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // 记录并返回两种方案中价值更大的那一个\n    mem[i][c] = Math.max(no, yes);\n    return mem[i][c];\n}\n
knapsack.ts
/* 0-1 背包:记忆化搜索 */\nfunction knapsackDFSMem(\n    wgt: Array<number>,\n    val: Array<number>,\n    mem: Array<Array<number>>,\n    i: number,\n    c: number\n): number {\n    // 若已选完所有物品或背包无剩余容量,则返回价值 0\n    if (i === 0 || c === 0) {\n        return 0;\n    }\n    // 若已有记录,则直接返回\n    if (mem[i][c] !== -1) {\n        return mem[i][c];\n    }\n    // 若超过背包容量,则只能选择不放入背包\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, mem, i - 1, c);\n    }\n    // 计算不放入和放入物品 i 的最大价值\n    const no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n    const yes =\n        knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // 记录并返回两种方案中价值更大的那一个\n    mem[i][c] = Math.max(no, yes);\n    return mem[i][c];\n}\n
knapsack.dart
/* 0-1 背包:记忆化搜索 */\nint knapsackDFSMem(\n  List<int> wgt,\n  List<int> val,\n  List<List<int>> mem,\n  int i,\n  int c,\n) {\n  // 若已选完所有物品或背包无剩余容量,则返回价值 0\n  if (i == 0 || c == 0) {\n    return 0;\n  }\n  // 若已有记录,则直接返回\n  if (mem[i][c] != -1) {\n    return mem[i][c];\n  }\n  // 若超过背包容量,则只能选择不放入背包\n  if (wgt[i - 1] > c) {\n    return knapsackDFSMem(wgt, val, mem, i - 1, c);\n  }\n  // 计算不放入和放入物品 i 的最大价值\n  int no = knapsackDFSMem(wgt, val, mem, i - 1, c);\n  int yes = knapsackDFSMem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n  // 记录并返回两种方案中价值更大的那一个\n  mem[i][c] = max(no, yes);\n  return mem[i][c];\n}\n
knapsack.rs
/* 0-1 背包:记忆化搜索 */\nfn knapsack_dfs_mem(wgt: &[i32], val: &[i32], mem: &mut Vec<Vec<i32>>, i: usize, c: usize) -> i32 {\n    // 若已选完所有物品或背包无剩余容量,则返回价值 0\n    if i == 0 || c == 0 {\n        return 0;\n    }\n    // 若已有记录,则直接返回\n    if mem[i][c] != -1 {\n        return mem[i][c];\n    }\n    // 若超过背包容量,则只能选择不放入背包\n    if wgt[i - 1] > c as i32 {\n        return knapsack_dfs_mem(wgt, val, mem, i - 1, c);\n    }\n    // 计算不放入和放入物品 i 的最大价值\n    let no = knapsack_dfs_mem(wgt, val, mem, i - 1, c);\n    let yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1] as usize) + val[i - 1];\n    // 记录并返回两种方案中价值更大的那一个\n    mem[i][c] = std::cmp::max(no, yes);\n    mem[i][c]\n}\n
knapsack.c
/* 0-1 背包:记忆化搜索 */\nint knapsackDFSMem(int wgt[], int val[], int memCols, int **mem, int i, int c) {\n    // 若已选完所有物品或背包无剩余容量,则返回价值 0\n    if (i == 0 || c == 0) {\n        return 0;\n    }\n    // 若已有记录,则直接返回\n    if (mem[i][c] != -1) {\n        return mem[i][c];\n    }\n    // 若超过背包容量,则只能选择不放入背包\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, val, memCols, mem, i - 1, c);\n    }\n    // 计算不放入和放入物品 i 的最大价值\n    int no = knapsackDFSMem(wgt, val, memCols, mem, i - 1, c);\n    int yes = knapsackDFSMem(wgt, val, memCols, mem, i - 1, c - wgt[i - 1]) + val[i - 1];\n    // 记录并返回两种方案中价值更大的那一个\n    mem[i][c] = myMax(no, yes);\n    return mem[i][c];\n}\n
knapsack.kt
/* 0-1 背包:记忆化搜索 */\nfun knapsackDFSMem(\n    wgt: IntArray,\n    _val: IntArray,\n    mem: Array<IntArray>,\n    i: Int,\n    c: Int\n): Int {\n    // 若已选完所有物品或背包无剩余容量,则返回价值 0\n    if (i == 0 || c == 0) {\n        return 0\n    }\n    // 若已有记录,则直接返回\n    if (mem[i][c] != -1) {\n        return mem[i][c]\n    }\n    // 若超过背包容量,则只能选择不放入背包\n    if (wgt[i - 1] > c) {\n        return knapsackDFSMem(wgt, _val, mem, i - 1, c)\n    }\n    // 计算不放入和放入物品 i 的最大价值\n    val no = knapsackDFSMem(wgt, _val, mem, i - 1, c)\n    val yes = knapsackDFSMem(wgt, _val, mem, i - 1, c - wgt[i - 1]) + _val[i - 1]\n    // 记录并返回两种方案中价值更大的那一个\n    mem[i][c] = max(no, yes)\n    return mem[i][c]\n}\n
knapsack.rb
### 0-1 背包:记忆化搜索 ###\ndef knapsack_dfs_mem(wgt, val, mem, i, c)\n  # 若已选完所有物品或背包无剩余容量,则返回价值 0\n  return 0 if i == 0 || c == 0\n  # 若已有记录,则直接返回\n  return mem[i][c] if mem[i][c] != -1\n  # 若超过背包容量,则只能选择不放入背包\n  return knapsack_dfs_mem(wgt, val, mem, i - 1, c) if wgt[i - 1] > c\n  # 计算不放入和放入物品 i 的最大价值\n  no = knapsack_dfs_mem(wgt, val, mem, i - 1, c)\n  yes = knapsack_dfs_mem(wgt, val, mem, i - 1, c - wgt[i - 1]) + val[i - 1]\n  # 记录并返回两种方案中价值更大的那一个\n  mem[i][c] = [no, yes].max\nend\n
可视化运行

全屏观看 >

图 14-19 展示了在记忆化搜索中被剪掉的搜索分支。

图 14-19   0-1 背包问题的记忆化搜索递归树

","path":["第 14 章   动态规划","14.4   0-1 背包问题"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#3","level":3,"title":"3.   方法三:动态规划","text":"

动态规划实质上就是在状态转移中填充 \\(dp\\) 表的过程,代码如下所示:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
def knapsack_dp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"0-1 背包:动态规划\"\"\"\n    n = len(wgt)\n    # 初始化 dp 表\n    dp = [[0] * (cap + 1) for _ in range(n + 1)]\n    # 状态转移\n    for i in range(1, n + 1):\n        for c in range(1, cap + 1):\n            if wgt[i - 1] > c:\n                # 若超过背包容量,则不选物品 i\n                dp[i][c] = dp[i - 1][c]\n            else:\n                # 不选和选物品 i 这两种方案的较大值\n                dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1])\n    return dp[n][cap]\n
knapsack.cpp
/* 0-1 背包:动态规划 */\nint knapsackDP(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // 初始化 dp 表\n    vector<vector<int>> dp(n + 1, vector<int>(cap + 1, 0));\n    // 状态转移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // 若超过背包容量,则不选物品 i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
knapsack.java
/* 0-1 背包:动态规划 */\nint knapsackDP(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // 初始化 dp 表\n    int[][] dp = new int[n + 1][cap + 1];\n    // 状态转移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // 若超过背包容量,则不选物品 i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[i][c] = Math.max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
knapsack.cs
/* 0-1 背包:动态规划 */\nint KnapsackDP(int[] weight, int[] val, int cap) {\n    int n = weight.Length;\n    // 初始化 dp 表\n    int[,] dp = new int[n + 1, cap + 1];\n    // 状态转移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (weight[i - 1] > c) {\n                // 若超过背包容量,则不选物品 i\n                dp[i, c] = dp[i - 1, c];\n            } else {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[i, c] = Math.Max(dp[i - 1, c - weight[i - 1]] + val[i - 1], dp[i - 1, c]);\n            }\n        }\n    }\n    return dp[n, cap];\n}\n
knapsack.go
/* 0-1 背包:动态规划 */\nfunc knapsackDP(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // 初始化 dp 表\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, cap+1)\n    }\n    // 状态转移\n    for i := 1; i <= n; i++ {\n        for c := 1; c <= cap; c++ {\n            if wgt[i-1] > c {\n                // 若超过背包容量,则不选物品 i\n                dp[i][c] = dp[i-1][c]\n            } else {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[i][c] = int(math.Max(float64(dp[i-1][c]), float64(dp[i-1][c-wgt[i-1]]+val[i-1])))\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
knapsack.swift
/* 0-1 背包:动态规划 */\nfunc knapsackDP(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // 初始化 dp 表\n    var dp = Array(repeating: Array(repeating: 0, count: cap + 1), count: n + 1)\n    // 状态转移\n    for i in 1 ... n {\n        for c in 1 ... cap {\n            if wgt[i - 1] > c {\n                // 若超过背包容量,则不选物品 i\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1])\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
knapsack.js
/* 0-1 背包:动态规划 */\nfunction knapsackDP(wgt, val, cap) {\n    const n = wgt.length;\n    // 初始化 dp 表\n    const dp = Array(n + 1)\n        .fill(0)\n        .map(() => Array(cap + 1).fill(0));\n    // 状态转移\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // 若超过背包容量,则不选物品 i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[i][c] = Math.max(\n                    dp[i - 1][c],\n                    dp[i - 1][c - wgt[i - 1]] + val[i - 1]\n                );\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
knapsack.ts
/* 0-1 背包:动态规划 */\nfunction knapsackDP(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // 初始化 dp 表\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: cap + 1 }, () => 0)\n    );\n    // 状态转移\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // 若超过背包容量,则不选物品 i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[i][c] = Math.max(\n                    dp[i - 1][c],\n                    dp[i - 1][c - wgt[i - 1]] + val[i - 1]\n                );\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
knapsack.dart
/* 0-1 背包:动态规划 */\nint knapsackDP(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // 初始化 dp 表\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(cap + 1, 0));\n  // 状态转移\n  for (int i = 1; i <= n; i++) {\n    for (int c = 1; c <= cap; c++) {\n      if (wgt[i - 1] > c) {\n        // 若超过背包容量,则不选物品 i\n        dp[i][c] = dp[i - 1][c];\n      } else {\n        // 不选和选物品 i 这两种方案的较大值\n        dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n      }\n    }\n  }\n  return dp[n][cap];\n}\n
knapsack.rs
/* 0-1 背包:动态规划 */\nfn knapsack_dp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // 初始化 dp 表\n    let mut dp = vec![vec![0; cap + 1]; n + 1];\n    // 状态转移\n    for i in 1..=n {\n        for c in 1..=cap {\n            if wgt[i - 1] > c as i32 {\n                // 若超过背包容量,则不选物品 i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[i][c] = std::cmp::max(\n                    dp[i - 1][c],\n                    dp[i - 1][c - wgt[i - 1] as usize] + val[i - 1],\n                );\n            }\n        }\n    }\n    dp[n][cap]\n}\n
knapsack.c
/* 0-1 背包:动态规划 */\nint knapsackDP(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // 初始化 dp 表\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(cap + 1, sizeof(int));\n    }\n    // 状态转移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // 若超过背包容量,则不选物品 i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[i][c] = myMax(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    int res = dp[n][cap];\n    // 释放内存\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
knapsack.kt
/* 0-1 背包:动态规划 */\nfun knapsackDP(wgt: IntArray, _val: IntArray, cap: Int): Int {\n    val n = wgt.size\n    // 初始化 dp 表\n    val dp = Array(n + 1) { IntArray(cap + 1) }\n    // 状态转移\n    for (i in 1..n) {\n        for (c in 1..cap) {\n            if (wgt[i - 1] > c) {\n                // 若超过背包容量,则不选物品 i\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + _val[i - 1])\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
knapsack.rb
### 0-1 背包:动态规划 ###\ndef knapsack_dp(wgt, val, cap)\n  n = wgt.length\n  # 初始化 dp 表\n  dp = Array.new(n + 1) { Array.new(cap + 1, 0) }\n  # 状态转移\n  for i in 1...(n + 1)\n    for c in 1...(cap + 1)\n      if wgt[i - 1] > c\n        # 若超过背包容量,则不选物品 i\n        dp[i][c] = dp[i - 1][c]\n      else\n        # 不选和选物品 i 这两种方案的较大值\n        dp[i][c] = [dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]].max\n      end\n    end\n  end\n  dp[n][cap]\nend\n
可视化运行

全屏观看 >

如图 14-20 所示,时间复杂度和空间复杂度都由数组 dp 大小决定,即 \\(O(n \\times cap)\\) 。

<1><2><3><4><5><6><7><8><9><10><11><12><13><14>

图 14-20   0-1 背包问题的动态规划过程

","path":["第 14 章   动态规划","14.4   0-1 背包问题"],"tags":[]},{"location":"chapter_dynamic_programming/knapsack_problem/#4","level":3,"title":"4.   空间优化","text":"

由于每个状态都只与其上一行的状态有关,因此我们可以使用两个数组滚动前进,将空间复杂度从 \\(O(n^2)\\) 降至 \\(O(n)\\) 。

进一步思考,我们能否仅用一个数组实现空间优化呢?观察可知,每个状态都是由正上方或左上方的格子转移过来的。假设只有一个数组,当开始遍历第 \\(i\\) 行时,该数组存储的仍然是第 \\(i-1\\) 行的状态。

图 14-21 展示了在单个数组下从第 \\(i = 1\\) 行转换至第 \\(i = 2\\) 行的过程。请思考正序遍历和倒序遍历的区别。

<1><2><3><4><5><6>

图 14-21   0-1 背包的空间优化后的动态规划过程

在代码实现中,我们仅需将数组 dp 的第一维 \\(i\\) 直接删除,并且把内循环更改为倒序遍历即可:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby knapsack.py
def knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"0-1 背包:空间优化后的动态规划\"\"\"\n    n = len(wgt)\n    # 初始化 dp 表\n    dp = [0] * (cap + 1)\n    # 状态转移\n    for i in range(1, n + 1):\n        # 倒序遍历\n        for c in range(cap, 0, -1):\n            if wgt[i - 1] > c:\n                # 若超过背包容量,则不选物品 i\n                dp[c] = dp[c]\n            else:\n                # 不选和选物品 i 这两种方案的较大值\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n    return dp[cap]\n
knapsack.cpp
/* 0-1 背包:空间优化后的动态规划 */\nint knapsackDPComp(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // 初始化 dp 表\n    vector<int> dp(cap + 1, 0);\n    // 状态转移\n    for (int i = 1; i <= n; i++) {\n        // 倒序遍历\n        for (int c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
knapsack.java
/* 0-1 背包:空间优化后的动态规划 */\nint knapsackDPComp(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // 初始化 dp 表\n    int[] dp = new int[cap + 1];\n    // 状态转移\n    for (int i = 1; i <= n; i++) {\n        // 倒序遍历\n        for (int c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
knapsack.cs
/* 0-1 背包:空间优化后的动态规划 */\nint KnapsackDPComp(int[] weight, int[] val, int cap) {\n    int n = weight.Length;\n    // 初始化 dp 表\n    int[] dp = new int[cap + 1];\n    // 状态转移\n    for (int i = 1; i <= n; i++) {\n        // 倒序遍历\n        for (int c = cap; c > 0; c--) {\n            if (weight[i - 1] > c) {\n                // 若超过背包容量,则不选物品 i\n                dp[c] = dp[c];\n            } else {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[c] = Math.Max(dp[c], dp[c - weight[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
knapsack.go
/* 0-1 背包:空间优化后的动态规划 */\nfunc knapsackDPComp(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // 初始化 dp 表\n    dp := make([]int, cap+1)\n    // 状态转移\n    for i := 1; i <= n; i++ {\n        // 倒序遍历\n        for c := cap; c >= 1; c-- {\n            if wgt[i-1] <= c {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[c] = int(math.Max(float64(dp[c]), float64(dp[c-wgt[i-1]]+val[i-1])))\n            }\n        }\n    }\n    return dp[cap]\n}\n
knapsack.swift
/* 0-1 背包:空间优化后的动态规划 */\nfunc knapsackDPComp(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // 初始化 dp 表\n    var dp = Array(repeating: 0, count: cap + 1)\n    // 状态转移\n    for i in 1 ... n {\n        // 倒序遍历\n        for c in (1 ... cap).reversed() {\n            if wgt[i - 1] <= c {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n            }\n        }\n    }\n    return dp[cap]\n}\n
knapsack.js
/* 0-1 背包:空间优化后的动态规划 */\nfunction knapsackDPComp(wgt, val, cap) {\n    const n = wgt.length;\n    // 初始化 dp 表\n    const dp = Array(cap + 1).fill(0);\n    // 状态转移\n    for (let i = 1; i <= n; i++) {\n        // 倒序遍历\n        for (let c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
knapsack.ts
/* 0-1 背包:空间优化后的动态规划 */\nfunction knapsackDPComp(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // 初始化 dp 表\n    const dp = Array(cap + 1).fill(0);\n    // 状态转移\n    for (let i = 1; i <= n; i++) {\n        // 倒序遍历\n        for (let c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
knapsack.dart
/* 0-1 背包:空间优化后的动态规划 */\nint knapsackDPComp(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // 初始化 dp 表\n  List<int> dp = List.filled(cap + 1, 0);\n  // 状态转移\n  for (int i = 1; i <= n; i++) {\n    // 倒序遍历\n    for (int c = cap; c >= 1; c--) {\n      if (wgt[i - 1] <= c) {\n        // 不选和选物品 i 这两种方案的较大值\n        dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n      }\n    }\n  }\n  return dp[cap];\n}\n
knapsack.rs
/* 0-1 背包:空间优化后的动态规划 */\nfn knapsack_dp_comp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // 初始化 dp 表\n    let mut dp = vec![0; cap + 1];\n    // 状态转移\n    for i in 1..=n {\n        // 倒序遍历\n        for c in (1..=cap).rev() {\n            if wgt[i - 1] <= c as i32 {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[c] = std::cmp::max(dp[c], dp[c - wgt[i - 1] as usize] + val[i - 1]);\n            }\n        }\n    }\n    dp[cap]\n}\n
knapsack.c
/* 0-1 背包:空间优化后的动态规划 */\nint knapsackDPComp(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // 初始化 dp 表\n    int *dp = calloc(cap + 1, sizeof(int));\n    // 状态转移\n    for (int i = 1; i <= n; i++) {\n        // 倒序遍历\n        for (int c = cap; c >= 1; c--) {\n            if (wgt[i - 1] <= c) {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[c] = myMax(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    int res = dp[cap];\n    // 释放内存\n    free(dp);\n    return res;\n}\n
knapsack.kt
/* 0-1 背包:空间优化后的动态规划 */\nfun knapsackDPComp(wgt: IntArray, _val: IntArray, cap: Int): Int {\n    val n = wgt.size\n    // 初始化 dp 表\n    val dp = IntArray(cap + 1)\n    // 状态转移\n    for (i in 1..n) {\n        // 倒序遍历\n        for (c in cap downTo 1) {\n            if (wgt[i - 1] <= c) {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + _val[i - 1])\n            }\n        }\n    }\n    return dp[cap]\n}\n
knapsack.rb
### 0-1 背包:空间优化后的动态规划 ###\ndef knapsack_dp_comp(wgt, val, cap)\n  n = wgt.length\n  # 初始化 dp 表\n  dp = Array.new(cap + 1, 0)\n  # 状态转移\n  for i in 1...(n + 1)\n    # 倒序遍历\n    for c in cap.downto(1)\n      if wgt[i - 1] > c\n        # 若超过背包容量,则不选物品 i\n        dp[c] = dp[c]\n      else\n        # 不选和选物品 i 这两种方案的较大值\n        dp[c] = [dp[c], dp[c - wgt[i - 1]] + val[i - 1]].max\n      end\n    end\n  end\n  dp[cap]\nend\n
可视化运行

全屏观看 >

","path":["第 14 章   动态规划","14.4   0-1 背包问题"],"tags":[]},{"location":"chapter_dynamic_programming/summary/","level":1,"title":"14.7   小结","text":"","path":["第 14 章   动态规划","14.7   小结"],"tags":[]},{"location":"chapter_dynamic_programming/summary/#1","level":3,"title":"1.   重点回顾","text":"

背包问题

编辑距离问题

","path":["第 14 章   动态规划","14.7   小结"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/","level":1,"title":"14.5   完全背包问题","text":"

在本节中,我们先求解另一个常见的背包问题:完全背包,再了解它的一种特例:零钱兑换。

","path":["第 14 章   动态规划","14.5   完全背包问题"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1451","level":2,"title":"14.5.1   完全背包问题","text":"

Question

给定 \\(n\\) 个物品,第 \\(i\\) 个物品的重量为 \\(wgt[i-1]\\)、价值为 \\(val[i-1]\\) ,和一个容量为 \\(cap\\) 的背包。每个物品可以重复选取,问在限定背包容量下能放入物品的最大价值。示例如图 14-22 所示。

图 14-22   完全背包问题的示例数据

","path":["第 14 章   动态规划","14.5   完全背包问题"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1","level":3,"title":"1.   动态规划思路","text":"

完全背包问题和 0-1 背包问题非常相似,区别仅在于不限制物品的选择次数。

在完全背包问题的规定下,状态 \\([i, c]\\) 的变化分为两种情况。

从而状态转移方程变为:

\\[ dp[i, c] = \\max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1]) \\]","path":["第 14 章   动态规划","14.5   完全背包问题"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#2","level":3,"title":"2.   代码实现","text":"

对比两道题目的代码,状态转移中有一处从 \\(i-1\\) 变为 \\(i\\) ,其余完全一致:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby unbounded_knapsack.py
def unbounded_knapsack_dp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"完全背包:动态规划\"\"\"\n    n = len(wgt)\n    # 初始化 dp 表\n    dp = [[0] * (cap + 1) for _ in range(n + 1)]\n    # 状态转移\n    for i in range(1, n + 1):\n        for c in range(1, cap + 1):\n            if wgt[i - 1] > c:\n                # 若超过背包容量,则不选物品 i\n                dp[i][c] = dp[i - 1][c]\n            else:\n                # 不选和选物品 i 这两种方案的较大值\n                dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1])\n    return dp[n][cap]\n
unbounded_knapsack.cpp
/* 完全背包:动态规划 */\nint unboundedKnapsackDP(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // 初始化 dp 表\n    vector<vector<int>> dp(n + 1, vector<int>(cap + 1, 0));\n    // 状态转移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // 若超过背包容量,则不选物品 i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
unbounded_knapsack.java
/* 完全背包:动态规划 */\nint unboundedKnapsackDP(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // 初始化 dp 表\n    int[][] dp = new int[n + 1][cap + 1];\n    // 状态转移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // 若超过背包容量,则不选物品 i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[i][c] = Math.max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
unbounded_knapsack.cs
/* 完全背包:动态规划 */\nint UnboundedKnapsackDP(int[] wgt, int[] val, int cap) {\n    int n = wgt.Length;\n    // 初始化 dp 表\n    int[,] dp = new int[n + 1, cap + 1];\n    // 状态转移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // 若超过背包容量,则不选物品 i\n                dp[i, c] = dp[i - 1, c];\n            } else {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[i, c] = Math.Max(dp[i - 1, c], dp[i, c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n, cap];\n}\n
unbounded_knapsack.go
/* 完全背包:动态规划 */\nfunc unboundedKnapsackDP(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // 初始化 dp 表\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, cap+1)\n    }\n    // 状态转移\n    for i := 1; i <= n; i++ {\n        for c := 1; c <= cap; c++ {\n            if wgt[i-1] > c {\n                // 若超过背包容量,则不选物品 i\n                dp[i][c] = dp[i-1][c]\n            } else {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[i][c] = int(math.Max(float64(dp[i-1][c]), float64(dp[i][c-wgt[i-1]]+val[i-1])))\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
unbounded_knapsack.swift
/* 完全背包:动态规划 */\nfunc unboundedKnapsackDP(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // 初始化 dp 表\n    var dp = Array(repeating: Array(repeating: 0, count: cap + 1), count: n + 1)\n    // 状态转移\n    for i in 1 ... n {\n        for c in 1 ... cap {\n            if wgt[i - 1] > c {\n                // 若超过背包容量,则不选物品 i\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1])\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
unbounded_knapsack.js
/* 完全背包:动态规划 */\nfunction unboundedKnapsackDP(wgt, val, cap) {\n    const n = wgt.length;\n    // 初始化 dp 表\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: cap + 1 }, () => 0)\n    );\n    // 状态转移\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // 若超过背包容量,则不选物品 i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[i][c] = Math.max(\n                    dp[i - 1][c],\n                    dp[i][c - wgt[i - 1]] + val[i - 1]\n                );\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
unbounded_knapsack.ts
/* 完全背包:动态规划 */\nfunction unboundedKnapsackDP(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // 初始化 dp 表\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: cap + 1 }, () => 0)\n    );\n    // 状态转移\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // 若超过背包容量,则不选物品 i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[i][c] = Math.max(\n                    dp[i - 1][c],\n                    dp[i][c - wgt[i - 1]] + val[i - 1]\n                );\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
unbounded_knapsack.dart
/* 完全背包:动态规划 */\nint unboundedKnapsackDP(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // 初始化 dp 表\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(cap + 1, 0));\n  // 状态转移\n  for (int i = 1; i <= n; i++) {\n    for (int c = 1; c <= cap; c++) {\n      if (wgt[i - 1] > c) {\n        // 若超过背包容量,则不选物品 i\n        dp[i][c] = dp[i - 1][c];\n      } else {\n        // 不选和选物品 i 这两种方案的较大值\n        dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n      }\n    }\n  }\n  return dp[n][cap];\n}\n
unbounded_knapsack.rs
/* 完全背包:动态规划 */\nfn unbounded_knapsack_dp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // 初始化 dp 表\n    let mut dp = vec![vec![0; cap + 1]; n + 1];\n    // 状态转移\n    for i in 1..=n {\n        for c in 1..=cap {\n            if wgt[i - 1] > c as i32 {\n                // 若超过背包容量,则不选物品 i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[i][c] = std::cmp::max(dp[i - 1][c], dp[i][c - wgt[i - 1] as usize] + val[i - 1]);\n            }\n        }\n    }\n    return dp[n][cap];\n}\n
unbounded_knapsack.c
/* 完全背包:动态规划 */\nint unboundedKnapsackDP(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // 初始化 dp 表\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(cap + 1, sizeof(int));\n    }\n    // 状态转移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // 若超过背包容量,则不选物品 i\n                dp[i][c] = dp[i - 1][c];\n            } else {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[i][c] = myMax(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    int res = dp[n][cap];\n    // 释放内存\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    return res;\n}\n
unbounded_knapsack.kt
/* 完全背包:动态规划 */\nfun unboundedKnapsackDP(wgt: IntArray, _val: IntArray, cap: Int): Int {\n    val n = wgt.size\n    // 初始化 dp 表\n    val dp = Array(n + 1) { IntArray(cap + 1) }\n    // 状态转移\n    for (i in 1..n) {\n        for (c in 1..cap) {\n            if (wgt[i - 1] > c) {\n                // 若超过背包容量,则不选物品 i\n                dp[i][c] = dp[i - 1][c]\n            } else {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + _val[i - 1])\n            }\n        }\n    }\n    return dp[n][cap]\n}\n
unbounded_knapsack.rb
### 完全背包:动态规划 ###\ndef unbounded_knapsack_dp(wgt, val, cap)\n  n = wgt.length\n  # 初始化 dp 表\n  dp = Array.new(n + 1) { Array.new(cap + 1, 0) }\n  # 状态转移\n  for i in 1...(n + 1)\n    for c in 1...(cap + 1)\n      if wgt[i - 1] > c\n        # 若超过背包容量,则不选物品 i\n        dp[i][c] = dp[i - 1][c]\n      else\n        # 不选和选物品 i 这两种方案的较大值\n        dp[i][c] = [dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]].max\n      end\n    end\n  end\n  dp[n][cap]\nend\n
可视化运行

全屏观看 >

","path":["第 14 章   动态规划","14.5   完全背包问题"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#3","level":3,"title":"3.   空间优化","text":"

由于当前状态是从左边和上边的状态转移而来的,因此空间优化后应该对 \\(dp\\) 表中的每一行进行正序遍历。

这个遍历顺序与 0-1 背包正好相反。请借助图 14-23 来理解两者的区别。

<1><2><3><4><5><6>

图 14-23   完全背包问题在空间优化后的动态规划过程

代码实现比较简单,仅需将数组 dp 的第一维删除:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby unbounded_knapsack.py
def unbounded_knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"完全背包:空间优化后的动态规划\"\"\"\n    n = len(wgt)\n    # 初始化 dp 表\n    dp = [0] * (cap + 1)\n    # 状态转移\n    for i in range(1, n + 1):\n        # 正序遍历\n        for c in range(1, cap + 1):\n            if wgt[i - 1] > c:\n                # 若超过背包容量,则不选物品 i\n                dp[c] = dp[c]\n            else:\n                # 不选和选物品 i 这两种方案的较大值\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n    return dp[cap]\n
unbounded_knapsack.cpp
/* 完全背包:空间优化后的动态规划 */\nint unboundedKnapsackDPComp(vector<int> &wgt, vector<int> &val, int cap) {\n    int n = wgt.size();\n    // 初始化 dp 表\n    vector<int> dp(cap + 1, 0);\n    // 状态转移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // 若超过背包容量,则不选物品 i\n                dp[c] = dp[c];\n            } else {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
unbounded_knapsack.java
/* 完全背包:空间优化后的动态规划 */\nint unboundedKnapsackDPComp(int[] wgt, int[] val, int cap) {\n    int n = wgt.length;\n    // 初始化 dp 表\n    int[] dp = new int[cap + 1];\n    // 状态转移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // 若超过背包容量,则不选物品 i\n                dp[c] = dp[c];\n            } else {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
unbounded_knapsack.cs
/* 完全背包:空间优化后的动态规划 */\nint UnboundedKnapsackDPComp(int[] wgt, int[] val, int cap) {\n    int n = wgt.Length;\n    // 初始化 dp 表\n    int[] dp = new int[cap + 1];\n    // 状态转移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // 若超过背包容量,则不选物品 i\n                dp[c] = dp[c];\n            } else {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[c] = Math.Max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
unbounded_knapsack.go
/* 完全背包:空间优化后的动态规划 */\nfunc unboundedKnapsackDPComp(wgt, val []int, cap int) int {\n    n := len(wgt)\n    // 初始化 dp 表\n    dp := make([]int, cap+1)\n    // 状态转移\n    for i := 1; i <= n; i++ {\n        for c := 1; c <= cap; c++ {\n            if wgt[i-1] > c {\n                // 若超过背包容量,则不选物品 i\n                dp[c] = dp[c]\n            } else {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[c] = int(math.Max(float64(dp[c]), float64(dp[c-wgt[i-1]]+val[i-1])))\n            }\n        }\n    }\n    return dp[cap]\n}\n
unbounded_knapsack.swift
/* 完全背包:空间优化后的动态规划 */\nfunc unboundedKnapsackDPComp(wgt: [Int], val: [Int], cap: Int) -> Int {\n    let n = wgt.count\n    // 初始化 dp 表\n    var dp = Array(repeating: 0, count: cap + 1)\n    // 状态转移\n    for i in 1 ... n {\n        for c in 1 ... cap {\n            if wgt[i - 1] > c {\n                // 若超过背包容量,则不选物品 i\n                dp[c] = dp[c]\n            } else {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])\n            }\n        }\n    }\n    return dp[cap]\n}\n
unbounded_knapsack.js
/* 完全背包:空间优化后的动态规划 */\nfunction unboundedKnapsackDPComp(wgt, val, cap) {\n    const n = wgt.length;\n    // 初始化 dp 表\n    const dp = Array.from({ length: cap + 1 }, () => 0);\n    // 状态转移\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // 若超过背包容量,则不选物品 i\n                dp[c] = dp[c];\n            } else {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
unbounded_knapsack.ts
/* 完全背包:空间优化后的动态规划 */\nfunction unboundedKnapsackDPComp(\n    wgt: Array<number>,\n    val: Array<number>,\n    cap: number\n): number {\n    const n = wgt.length;\n    // 初始化 dp 表\n    const dp = Array.from({ length: cap + 1 }, () => 0);\n    // 状态转移\n    for (let i = 1; i <= n; i++) {\n        for (let c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // 若超过背包容量,则不选物品 i\n                dp[c] = dp[c];\n            } else {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    return dp[cap];\n}\n
unbounded_knapsack.dart
/* 完全背包:空间优化后的动态规划 */\nint unboundedKnapsackDPComp(List<int> wgt, List<int> val, int cap) {\n  int n = wgt.length;\n  // 初始化 dp 表\n  List<int> dp = List.filled(cap + 1, 0);\n  // 状态转移\n  for (int i = 1; i <= n; i++) {\n    for (int c = 1; c <= cap; c++) {\n      if (wgt[i - 1] > c) {\n        // 若超过背包容量,则不选物品 i\n        dp[c] = dp[c];\n      } else {\n        // 不选和选物品 i 这两种方案的较大值\n        dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n      }\n    }\n  }\n  return dp[cap];\n}\n
unbounded_knapsack.rs
/* 完全背包:空间优化后的动态规划 */\nfn unbounded_knapsack_dp_comp(wgt: &[i32], val: &[i32], cap: usize) -> i32 {\n    let n = wgt.len();\n    // 初始化 dp 表\n    let mut dp = vec![0; cap + 1];\n    // 状态转移\n    for i in 1..=n {\n        for c in 1..=cap {\n            if wgt[i - 1] > c as i32 {\n                // 若超过背包容量,则不选物品 i\n                dp[c] = dp[c];\n            } else {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[c] = std::cmp::max(dp[c], dp[c - wgt[i - 1] as usize] + val[i - 1]);\n            }\n        }\n    }\n    dp[cap]\n}\n
unbounded_knapsack.c
/* 完全背包:空间优化后的动态规划 */\nint unboundedKnapsackDPComp(int wgt[], int val[], int cap, int wgtSize) {\n    int n = wgtSize;\n    // 初始化 dp 表\n    int *dp = calloc(cap + 1, sizeof(int));\n    // 状态转移\n    for (int i = 1; i <= n; i++) {\n        for (int c = 1; c <= cap; c++) {\n            if (wgt[i - 1] > c) {\n                // 若超过背包容量,则不选物品 i\n                dp[c] = dp[c];\n            } else {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[c] = myMax(dp[c], dp[c - wgt[i - 1]] + val[i - 1]);\n            }\n        }\n    }\n    int res = dp[cap];\n    // 释放内存\n    free(dp);\n    return res;\n}\n
unbounded_knapsack.kt
/* 完全背包:空间优化后的动态规划 */\nfun unboundedKnapsackDPComp(\n    wgt: IntArray,\n    _val: IntArray,\n    cap: Int\n): Int {\n    val n = wgt.size\n    // 初始化 dp 表\n    val dp = IntArray(cap + 1)\n    // 状态转移\n    for (i in 1..n) {\n        for (c in 1..cap) {\n            if (wgt[i - 1] > c) {\n                // 若超过背包容量,则不选物品 i\n                dp[c] = dp[c]\n            } else {\n                // 不选和选物品 i 这两种方案的较大值\n                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + _val[i - 1])\n            }\n        }\n    }\n    return dp[cap]\n}\n
unbounded_knapsack.rb
### 完全背包:动态规划 ###\ndef unbounded_knapsack_dp(wgt, val, cap)\n  n = wgt.length\n  # 初始化 dp 表\n  dp = Array.new(n + 1) { Array.new(cap + 1, 0) }\n  # 状态转移\n  for i in 1...(n + 1)\n    for c in 1...(cap + 1)\n      if wgt[i - 1] > c\n        # 若超过背包容量,则不选物品 i\n        dp[i][c] = dp[i - 1][c]\n      else\n        # 不选和选物品 i 这两种方案的较大值\n        dp[i][c] = [dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]].max\n      end\n    end\n  end\n  dp[n][cap]\nend\n\n### 完全背包:空间优化后的动态规划 ##3\ndef unbounded_knapsack_dp_comp(wgt, val, cap)\n  n = wgt.length\n  # 初始化 dp 表\n  dp = Array.new(cap + 1, 0)\n  # 状态转移\n  for i in 1...(n + 1)\n    # 正序遍历\n    for c in 1...(cap + 1)\n      if wgt[i -1] > c\n        # 若超过背包容量,则不选物品 i\n        dp[c] = dp[c]\n      else\n        # 不选和选物品 i 这两种方案的较大值\n        dp[c] = [dp[c], dp[c - wgt[i - 1]] + val[i - 1]].max\n      end\n    end\n  end\n  dp[cap]\nend\n
可视化运行

全屏观看 >

","path":["第 14 章   动态规划","14.5   完全背包问题"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1452","level":2,"title":"14.5.2   零钱兑换问题","text":"

背包问题是一大类动态规划问题的代表,其拥有很多变种,例如零钱兑换问题。

Question

给定 \\(n\\) 种硬币,第 \\(i\\) 种硬币的面值为 \\(coins[i - 1]\\) ,目标金额为 \\(amt\\) ,每种硬币可以重复选取,问能够凑出目标金额的最少硬币数量。如果无法凑出目标金额,则返回 \\(-1\\) 。示例如图 14-24 所示。

图 14-24   零钱兑换问题的示例数据

","path":["第 14 章   动态规划","14.5   完全背包问题"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1_1","level":3,"title":"1.   动态规划思路","text":"

零钱兑换可以看作完全背包问题的一种特殊情况,两者具有以下联系与不同点。

第一步:思考每轮的决策,定义状态,从而得到 \\(dp\\) 表

状态 \\([i, a]\\) 对应的子问题为:前 \\(i\\) 种硬币能够凑出金额 \\(a\\) 的最少硬币数量,记为 \\(dp[i, a]\\) 。

二维 \\(dp\\) 表的尺寸为 \\((n+1) \\times (amt+1)\\) 。

第二步:找出最优子结构,进而推导出状态转移方程

本题与完全背包问题的状态转移方程存在以下两点差异。

\\[ dp[i, a] = \\min(dp[i-1, a], dp[i, a - coins[i-1]] + 1) \\]

第三步:确定边界条件和状态转移顺序

当目标金额为 \\(0\\) 时,凑出它的最少硬币数量为 \\(0\\) ,即首列所有 \\(dp[i, 0]\\) 都等于 \\(0\\) 。

当无硬币时,无法凑出任意 \\(> 0\\) 的目标金额,即是无效解。为使状态转移方程中的 \\(\\min()\\) 函数能够识别并过滤无效解,我们考虑使用 \\(+ \\infty\\) 来表示它们,即令首行所有 \\(dp[0, a]\\) 都等于 \\(+ \\infty\\) 。

","path":["第 14 章   动态规划","14.5   完全背包问题"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#2_1","level":3,"title":"2.   代码实现","text":"

大多数编程语言并未提供 \\(+ \\infty\\) 变量,只能使用整型 int 的最大值来代替。而这又会导致大数越界:状态转移方程中的 \\(+ 1\\) 操作可能发生溢出。

为此,我们采用数字 \\(amt + 1\\) 来表示无效解,因为凑出 \\(amt\\) 的硬币数量最多为 \\(amt\\) 。最后返回前,判断 \\(dp[n, amt]\\) 是否等于 \\(amt + 1\\) ,若是则返回 \\(-1\\) ,代表无法凑出目标金额。代码如下所示:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change.py
def coin_change_dp(coins: list[int], amt: int) -> int:\n    \"\"\"零钱兑换:动态规划\"\"\"\n    n = len(coins)\n    MAX = amt + 1\n    # 初始化 dp 表\n    dp = [[0] * (amt + 1) for _ in range(n + 1)]\n    # 状态转移:首行首列\n    for a in range(1, amt + 1):\n        dp[0][a] = MAX\n    # 状态转移:其余行和列\n    for i in range(1, n + 1):\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # 若超过目标金额,则不选硬币 i\n                dp[i][a] = dp[i - 1][a]\n            else:\n                # 不选和选硬币 i 这两种方案的较小值\n                dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1)\n    return dp[n][amt] if dp[n][amt] != MAX else -1\n
coin_change.cpp
/* 零钱兑换:动态规划 */\nint coinChangeDP(vector<int> &coins, int amt) {\n    int n = coins.size();\n    int MAX = amt + 1;\n    // 初始化 dp 表\n    vector<vector<int>> dp(n + 1, vector<int>(amt + 1, 0));\n    // 状态转移:首行首列\n    for (int a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // 状态转移:其余行和列\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 若超过目标金额,则不选硬币 i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // 不选和选硬币 i 这两种方案的较小值\n                dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n][amt] != MAX ? dp[n][amt] : -1;\n}\n
coin_change.java
/* 零钱兑换:动态规划 */\nint coinChangeDP(int[] coins, int amt) {\n    int n = coins.length;\n    int MAX = amt + 1;\n    // 初始化 dp 表\n    int[][] dp = new int[n + 1][amt + 1];\n    // 状态转移:首行首列\n    for (int a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // 状态转移:其余行和列\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 若超过目标金额,则不选硬币 i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // 不选和选硬币 i 这两种方案的较小值\n                dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n][amt] != MAX ? dp[n][amt] : -1;\n}\n
coin_change.cs
/* 零钱兑换:动态规划 */\nint CoinChangeDP(int[] coins, int amt) {\n    int n = coins.Length;\n    int MAX = amt + 1;\n    // 初始化 dp 表\n    int[,] dp = new int[n + 1, amt + 1];\n    // 状态转移:首行首列\n    for (int a = 1; a <= amt; a++) {\n        dp[0, a] = MAX;\n    }\n    // 状态转移:其余行和列\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 若超过目标金额,则不选硬币 i\n                dp[i, a] = dp[i - 1, a];\n            } else {\n                // 不选和选硬币 i 这两种方案的较小值\n                dp[i, a] = Math.Min(dp[i - 1, a], dp[i, a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n, amt] != MAX ? dp[n, amt] : -1;\n}\n
coin_change.go
/* 零钱兑换:动态规划 */\nfunc coinChangeDP(coins []int, amt int) int {\n    n := len(coins)\n    max := amt + 1\n    // 初始化 dp 表\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, amt+1)\n    }\n    // 状态转移:首行首列\n    for a := 1; a <= amt; a++ {\n        dp[0][a] = max\n    }\n    // 状态转移:其余行和列\n    for i := 1; i <= n; i++ {\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // 若超过目标金额,则不选硬币 i\n                dp[i][a] = dp[i-1][a]\n            } else {\n                // 不选和选硬币 i 这两种方案的较小值\n                dp[i][a] = int(math.Min(float64(dp[i-1][a]), float64(dp[i][a-coins[i-1]]+1)))\n            }\n        }\n    }\n    if dp[n][amt] != max {\n        return dp[n][amt]\n    }\n    return -1\n}\n
coin_change.swift
/* 零钱兑换:动态规划 */\nfunc coinChangeDP(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    let MAX = amt + 1\n    // 初始化 dp 表\n    var dp = Array(repeating: Array(repeating: 0, count: amt + 1), count: n + 1)\n    // 状态转移:首行首列\n    for a in 1 ... amt {\n        dp[0][a] = MAX\n    }\n    // 状态转移:其余行和列\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // 若超过目标金额,则不选硬币 i\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // 不选和选硬币 i 这两种方案的较小值\n                dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1)\n            }\n        }\n    }\n    return dp[n][amt] != MAX ? dp[n][amt] : -1\n}\n
coin_change.js
/* 零钱兑换:动态规划 */\nfunction coinChangeDP(coins, amt) {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // 初始化 dp 表\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // 状态转移:首行首列\n    for (let a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // 状态转移:其余行和列\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 若超过目标金额,则不选硬币 i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // 不选和选硬币 i 这两种方案的较小值\n                dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n][amt] !== MAX ? dp[n][amt] : -1;\n}\n
coin_change.ts
/* 零钱兑换:动态规划 */\nfunction coinChangeDP(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // 初始化 dp 表\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // 状态转移:首行首列\n    for (let a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // 状态转移:其余行和列\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 若超过目标金额,则不选硬币 i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // 不选和选硬币 i 这两种方案的较小值\n                dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[n][amt] !== MAX ? dp[n][amt] : -1;\n}\n
coin_change.dart
/* 零钱兑换:动态规划 */\nint coinChangeDP(List<int> coins, int amt) {\n  int n = coins.length;\n  int MAX = amt + 1;\n  // 初始化 dp 表\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(amt + 1, 0));\n  // 状态转移:首行首列\n  for (int a = 1; a <= amt; a++) {\n    dp[0][a] = MAX;\n  }\n  // 状态转移:其余行和列\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // 若超过目标金额,则不选硬币 i\n        dp[i][a] = dp[i - 1][a];\n      } else {\n        // 不选和选硬币 i 这两种方案的较小值\n        dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n      }\n    }\n  }\n  return dp[n][amt] != MAX ? dp[n][amt] : -1;\n}\n
coin_change.rs
/* 零钱兑换:动态规划 */\nfn coin_change_dp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    let max = amt + 1;\n    // 初始化 dp 表\n    let mut dp = vec![vec![0; amt + 1]; n + 1];\n    // 状态转移:首行首列\n    for a in 1..=amt {\n        dp[0][a] = max;\n    }\n    // 状态转移:其余行和列\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // 若超过目标金额,则不选硬币 i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // 不选和选硬币 i 这两种方案的较小值\n                dp[i][a] = std::cmp::min(dp[i - 1][a], dp[i][a - coins[i - 1] as usize] + 1);\n            }\n        }\n    }\n    if dp[n][amt] != max {\n        return dp[n][amt] as i32;\n    } else {\n        -1\n    }\n}\n
coin_change.c
/* 零钱兑换:动态规划 */\nint coinChangeDP(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    int MAX = amt + 1;\n    // 初始化 dp 表\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(amt + 1, sizeof(int));\n    }\n    // 状态转移:首行首列\n    for (int a = 1; a <= amt; a++) {\n        dp[0][a] = MAX;\n    }\n    // 状态转移:其余行和列\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 若超过目标金额,则不选硬币 i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // 不选和选硬币 i 这两种方案的较小值\n                dp[i][a] = myMin(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    int res = dp[n][amt] != MAX ? dp[n][amt] : -1;\n    // 释放内存\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    free(dp);\n    return res;\n}\n
coin_change.kt
/* 零钱兑换:动态规划 */\nfun coinChangeDP(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    val MAX = amt + 1\n    // 初始化 dp 表\n    val dp = Array(n + 1) { IntArray(amt + 1) }\n    // 状态转移:首行首列\n    for (a in 1..amt) {\n        dp[0][a] = MAX\n    }\n    // 状态转移:其余行和列\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // 若超过目标金额,则不选硬币 i\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // 不选和选硬币 i 这两种方案的较小值\n                dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1)\n            }\n        }\n    }\n    return if (dp[n][amt] != MAX) dp[n][amt] else -1\n}\n
coin_change.rb
### 零钱兑换:动态规划 ###\ndef coin_change_dp(coins, amt)\n  n = coins.length\n  _MAX = amt + 1\n  # 初始化 dp 表\n  dp = Array.new(n + 1) { Array.new(amt + 1, 0) }\n  # 状态转移:首行首列\n  (1...(amt + 1)).each { |a| dp[0][a] = _MAX }\n  # 状态转移:其余行和列\n  for i in 1...(n + 1)\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # 若超过目标金额,则不选硬币 i\n        dp[i][a] = dp[i - 1][a]\n      else\n        # 不选和选硬币 i 这两种方案的较小值\n        dp[i][a] = [dp[i - 1][a], dp[i][a - coins[i - 1]] + 1].min\n      end\n    end\n  end\n  dp[n][amt] != _MAX ? dp[n][amt] : -1\nend\n
可视化运行

全屏观看 >

图 14-25 展示了零钱兑换的动态规划过程,和完全背包问题非常相似。

<1><2><3><4><5><6><7><8><9><10><11><12><13><14><15>

图 14-25   零钱兑换问题的动态规划过程

","path":["第 14 章   动态规划","14.5   完全背包问题"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#3_1","level":3,"title":"3.   空间优化","text":"

零钱兑换的空间优化的处理方式和完全背包问题一致:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change.py
def coin_change_dp_comp(coins: list[int], amt: int) -> int:\n    \"\"\"零钱兑换:空间优化后的动态规划\"\"\"\n    n = len(coins)\n    MAX = amt + 1\n    # 初始化 dp 表\n    dp = [MAX] * (amt + 1)\n    dp[0] = 0\n    # 状态转移\n    for i in range(1, n + 1):\n        # 正序遍历\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # 若超过目标金额,则不选硬币 i\n                dp[a] = dp[a]\n            else:\n                # 不选和选硬币 i 这两种方案的较小值\n                dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1)\n    return dp[amt] if dp[amt] != MAX else -1\n
coin_change.cpp
/* 零钱兑换:空间优化后的动态规划 */\nint coinChangeDPComp(vector<int> &coins, int amt) {\n    int n = coins.size();\n    int MAX = amt + 1;\n    // 初始化 dp 表\n    vector<int> dp(amt + 1, MAX);\n    dp[0] = 0;\n    // 状态转移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 若超过目标金额,则不选硬币 i\n                dp[a] = dp[a];\n            } else {\n                // 不选和选硬币 i 这两种方案的较小值\n                dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] != MAX ? dp[amt] : -1;\n}\n
coin_change.java
/* 零钱兑换:空间优化后的动态规划 */\nint coinChangeDPComp(int[] coins, int amt) {\n    int n = coins.length;\n    int MAX = amt + 1;\n    // 初始化 dp 表\n    int[] dp = new int[amt + 1];\n    Arrays.fill(dp, MAX);\n    dp[0] = 0;\n    // 状态转移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 若超过目标金额,则不选硬币 i\n                dp[a] = dp[a];\n            } else {\n                // 不选和选硬币 i 这两种方案的较小值\n                dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] != MAX ? dp[amt] : -1;\n}\n
coin_change.cs
/* 零钱兑换:空间优化后的动态规划 */\nint CoinChangeDPComp(int[] coins, int amt) {\n    int n = coins.Length;\n    int MAX = amt + 1;\n    // 初始化 dp 表\n    int[] dp = new int[amt + 1];\n    Array.Fill(dp, MAX);\n    dp[0] = 0;\n    // 状态转移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 若超过目标金额,则不选硬币 i\n                dp[a] = dp[a];\n            } else {\n                // 不选和选硬币 i 这两种方案的较小值\n                dp[a] = Math.Min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] != MAX ? dp[amt] : -1;\n}\n
coin_change.go
/* 零钱兑换:动态规划 */\nfunc coinChangeDPComp(coins []int, amt int) int {\n    n := len(coins)\n    max := amt + 1\n    // 初始化 dp 表\n    dp := make([]int, amt+1)\n    for i := 1; i <= amt; i++ {\n        dp[i] = max\n    }\n    // 状态转移\n    for i := 1; i <= n; i++ {\n        // 正序遍历\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // 若超过目标金额,则不选硬币 i\n                dp[a] = dp[a]\n            } else {\n                // 不选和选硬币 i 这两种方案的较小值\n                dp[a] = int(math.Min(float64(dp[a]), float64(dp[a-coins[i-1]]+1)))\n            }\n        }\n    }\n    if dp[amt] != max {\n        return dp[amt]\n    }\n    return -1\n}\n
coin_change.swift
/* 零钱兑换:空间优化后的动态规划 */\nfunc coinChangeDPComp(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    let MAX = amt + 1\n    // 初始化 dp 表\n    var dp = Array(repeating: MAX, count: amt + 1)\n    dp[0] = 0\n    // 状态转移\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // 若超过目标金额,则不选硬币 i\n                dp[a] = dp[a]\n            } else {\n                // 不选和选硬币 i 这两种方案的较小值\n                dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1)\n            }\n        }\n    }\n    return dp[amt] != MAX ? dp[amt] : -1\n}\n
coin_change.js
/* 零钱兑换:空间优化后的动态规划 */\nfunction coinChangeDPComp(coins, amt) {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // 初始化 dp 表\n    const dp = Array.from({ length: amt + 1 }, () => MAX);\n    dp[0] = 0;\n    // 状态转移\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 若超过目标金额,则不选硬币 i\n                dp[a] = dp[a];\n            } else {\n                // 不选和选硬币 i 这两种方案的较小值\n                dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] !== MAX ? dp[amt] : -1;\n}\n
coin_change.ts
/* 零钱兑换:空间优化后的动态规划 */\nfunction coinChangeDPComp(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    const MAX = amt + 1;\n    // 初始化 dp 表\n    const dp = Array.from({ length: amt + 1 }, () => MAX);\n    dp[0] = 0;\n    // 状态转移\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 若超过目标金额,则不选硬币 i\n                dp[a] = dp[a];\n            } else {\n                // 不选和选硬币 i 这两种方案的较小值\n                dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    return dp[amt] !== MAX ? dp[amt] : -1;\n}\n
coin_change.dart
/* 零钱兑换:空间优化后的动态规划 */\nint coinChangeDPComp(List<int> coins, int amt) {\n  int n = coins.length;\n  int MAX = amt + 1;\n  // 初始化 dp 表\n  List<int> dp = List.filled(amt + 1, MAX);\n  dp[0] = 0;\n  // 状态转移\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // 若超过目标金额,则不选硬币 i\n        dp[a] = dp[a];\n      } else {\n        // 不选和选硬币 i 这两种方案的较小值\n        dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1);\n      }\n    }\n  }\n  return dp[amt] != MAX ? dp[amt] : -1;\n}\n
coin_change.rs
/* 零钱兑换:空间优化后的动态规划 */\nfn coin_change_dp_comp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    let max = amt + 1;\n    // 初始化 dp 表\n    let mut dp = vec![0; amt + 1];\n    dp.fill(max);\n    dp[0] = 0;\n    // 状态转移\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // 若超过目标金额,则不选硬币 i\n                dp[a] = dp[a];\n            } else {\n                // 不选和选硬币 i 这两种方案的较小值\n                dp[a] = std::cmp::min(dp[a], dp[a - coins[i - 1] as usize] + 1);\n            }\n        }\n    }\n    if dp[amt] != max {\n        return dp[amt] as i32;\n    } else {\n        -1\n    }\n}\n
coin_change.c
/* 零钱兑换:空间优化后的动态规划 */\nint coinChangeDPComp(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    int MAX = amt + 1;\n    // 初始化 dp 表\n    int *dp = malloc((amt + 1) * sizeof(int));\n    for (int j = 1; j <= amt; j++) {\n        dp[j] = MAX;\n    } \n    dp[0] = 0;\n\n    // 状态转移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 若超过目标金额,则不选硬币 i\n                dp[a] = dp[a];\n            } else {\n                // 不选和选硬币 i 这两种方案的较小值\n                dp[a] = myMin(dp[a], dp[a - coins[i - 1]] + 1);\n            }\n        }\n    }\n    int res = dp[amt] != MAX ? dp[amt] : -1;\n    // 释放内存\n    free(dp);\n    return res;\n}\n
coin_change.kt
/* 零钱兑换:空间优化后的动态规划 */\nfun coinChangeDPComp(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    val MAX = amt + 1\n    // 初始化 dp 表\n    val dp = IntArray(amt + 1)\n    dp.fill(MAX)\n    dp[0] = 0\n    // 状态转移\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // 若超过目标金额,则不选硬币 i\n                dp[a] = dp[a]\n            } else {\n                // 不选和选硬币 i 这两种方案的较小值\n                dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1)\n            }\n        }\n    }\n    return if (dp[amt] != MAX) dp[amt] else -1\n}\n
coin_change.rb
### 零钱兑换:空间优化后的动态规划 ###\ndef coin_change_dp_comp(coins, amt)\n  n = coins.length\n  _MAX = amt + 1\n  # 初始化 dp 表\n  dp = Array.new(amt + 1, _MAX)\n  dp[0] = 0\n  # 状态转移\n  for i in 1...(n + 1)\n    # 正序遍历\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # 若超过目标金额,则不选硬币 i\n        dp[a] = dp[a]\n      else\n        # 不选和选硬币 i 这两种方案的较小值\n        dp[a] = [dp[a], dp[a - coins[i - 1]] + 1].min\n      end\n    end\n  end\n  dp[amt] != _MAX ? dp[amt] : -1\nend\n
可视化运行

全屏观看 >

","path":["第 14 章   动态规划","14.5   完全背包问题"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1453-ii","level":2,"title":"14.5.3   零钱兑换问题 II","text":"

Question

给定 \\(n\\) 种硬币,第 \\(i\\) 种硬币的面值为 \\(coins[i - 1]\\) ,目标金额为 \\(amt\\) ,每种硬币可以重复选取,问凑出目标金额的硬币组合数量。示例如图 14-26 所示。

图 14-26   零钱兑换问题 II 的示例数据

","path":["第 14 章   动态规划","14.5   完全背包问题"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#1_2","level":3,"title":"1.   动态规划思路","text":"

相比于上一题,本题目标是求组合数量,因此子问题变为:前 \\(i\\) 种硬币能够凑出金额 \\(a\\) 的组合数量。而 \\(dp\\) 表仍然是尺寸为 \\((n+1) \\times (amt + 1)\\) 的二维矩阵。

当前状态的组合数量等于不选当前硬币与选当前硬币这两种决策的组合数量之和。状态转移方程为:

\\[ dp[i, a] = dp[i-1, a] + dp[i, a - coins[i-1]] \\]

当目标金额为 \\(0\\) 时,无须选择任何硬币即可凑出目标金额,因此应将首列所有 \\(dp[i, 0]\\) 都初始化为 \\(1\\) 。当无硬币时,无法凑出任何 \\(>0\\) 的目标金额,因此首行所有 \\(dp[0, a]\\) 都等于 \\(0\\) 。

","path":["第 14 章   动态规划","14.5   完全背包问题"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#2_2","level":3,"title":"2.   代码实现","text":"PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change_ii.py
def coin_change_ii_dp(coins: list[int], amt: int) -> int:\n    \"\"\"零钱兑换 II:动态规划\"\"\"\n    n = len(coins)\n    # 初始化 dp 表\n    dp = [[0] * (amt + 1) for _ in range(n + 1)]\n    # 初始化首列\n    for i in range(n + 1):\n        dp[i][0] = 1\n    # 状态转移\n    for i in range(1, n + 1):\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # 若超过目标金额,则不选硬币 i\n                dp[i][a] = dp[i - 1][a]\n            else:\n                # 不选和选硬币 i 这两种方案之和\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]\n    return dp[n][amt]\n
coin_change_ii.cpp
/* 零钱兑换 II:动态规划 */\nint coinChangeIIDP(vector<int> &coins, int amt) {\n    int n = coins.size();\n    // 初始化 dp 表\n    vector<vector<int>> dp(n + 1, vector<int>(amt + 1, 0));\n    // 初始化首列\n    for (int i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // 状态转移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 若超过目标金额,则不选硬币 i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // 不选和选硬币 i 这两种方案之和\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n][amt];\n}\n
coin_change_ii.java
/* 零钱兑换 II:动态规划 */\nint coinChangeIIDP(int[] coins, int amt) {\n    int n = coins.length;\n    // 初始化 dp 表\n    int[][] dp = new int[n + 1][amt + 1];\n    // 初始化首列\n    for (int i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // 状态转移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 若超过目标金额,则不选硬币 i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // 不选和选硬币 i 这两种方案之和\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n][amt];\n}\n
coin_change_ii.cs
/* 零钱兑换 II:动态规划 */\nint CoinChangeIIDP(int[] coins, int amt) {\n    int n = coins.Length;\n    // 初始化 dp 表\n    int[,] dp = new int[n + 1, amt + 1];\n    // 初始化首列\n    for (int i = 0; i <= n; i++) {\n        dp[i, 0] = 1;\n    }\n    // 状态转移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 若超过目标金额,则不选硬币 i\n                dp[i, a] = dp[i - 1, a];\n            } else {\n                // 不选和选硬币 i 这两种方案之和\n                dp[i, a] = dp[i - 1, a] + dp[i, a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n, amt];\n}\n
coin_change_ii.go
/* 零钱兑换 II:动态规划 */\nfunc coinChangeIIDP(coins []int, amt int) int {\n    n := len(coins)\n    // 初始化 dp 表\n    dp := make([][]int, n+1)\n    for i := 0; i <= n; i++ {\n        dp[i] = make([]int, amt+1)\n    }\n    // 初始化首列\n    for i := 0; i <= n; i++ {\n        dp[i][0] = 1\n    }\n    // 状态转移:其余行和列\n    for i := 1; i <= n; i++ {\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // 若超过目标金额,则不选硬币 i\n                dp[i][a] = dp[i-1][a]\n            } else {\n                // 不选和选硬币 i 这两种方案之和\n                dp[i][a] = dp[i-1][a] + dp[i][a-coins[i-1]]\n            }\n        }\n    }\n    return dp[n][amt]\n}\n
coin_change_ii.swift
/* 零钱兑换 II:动态规划 */\nfunc coinChangeIIDP(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    // 初始化 dp 表\n    var dp = Array(repeating: Array(repeating: 0, count: amt + 1), count: n + 1)\n    // 初始化首列\n    for i in 0 ... n {\n        dp[i][0] = 1\n    }\n    // 状态转移\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // 若超过目标金额,则不选硬币 i\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // 不选和选硬币 i 这两种方案之和\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]\n            }\n        }\n    }\n    return dp[n][amt]\n}\n
coin_change_ii.js
/* 零钱兑换 II:动态规划 */\nfunction coinChangeIIDP(coins, amt) {\n    const n = coins.length;\n    // 初始化 dp 表\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // 初始化首列\n    for (let i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // 状态转移\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 若超过目标金额,则不选硬币 i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // 不选和选硬币 i 这两种方案之和\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n][amt];\n}\n
coin_change_ii.ts
/* 零钱兑换 II:动态规划 */\nfunction coinChangeIIDP(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    // 初始化 dp 表\n    const dp = Array.from({ length: n + 1 }, () =>\n        Array.from({ length: amt + 1 }, () => 0)\n    );\n    // 初始化首列\n    for (let i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // 状态转移\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 若超过目标金额,则不选硬币 i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // 不选和选硬币 i 这两种方案之和\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[n][amt];\n}\n
coin_change_ii.dart
/* 零钱兑换 II:动态规划 */\nint coinChangeIIDP(List<int> coins, int amt) {\n  int n = coins.length;\n  // 初始化 dp 表\n  List<List<int>> dp = List.generate(n + 1, (index) => List.filled(amt + 1, 0));\n  // 初始化首列\n  for (int i = 0; i <= n; i++) {\n    dp[i][0] = 1;\n  }\n  // 状态转移\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // 若超过目标金额,则不选硬币 i\n        dp[i][a] = dp[i - 1][a];\n      } else {\n        // 不选和选硬币 i 这两种方案之和\n        dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n      }\n    }\n  }\n  return dp[n][amt];\n}\n
coin_change_ii.rs
/* 零钱兑换 II:动态规划 */\nfn coin_change_ii_dp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    // 初始化 dp 表\n    let mut dp = vec![vec![0; amt + 1]; n + 1];\n    // 初始化首列\n    for i in 0..=n {\n        dp[i][0] = 1;\n    }\n    // 状态转移\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // 若超过目标金额,则不选硬币 i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // 不选和选硬币 i 这两种方案之和\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1] as usize];\n            }\n        }\n    }\n    dp[n][amt]\n}\n
coin_change_ii.c
/* 零钱兑换 II:动态规划 */\nint coinChangeIIDP(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    // 初始化 dp 表\n    int **dp = malloc((n + 1) * sizeof(int *));\n    for (int i = 0; i <= n; i++) {\n        dp[i] = calloc(amt + 1, sizeof(int));\n    }\n    // 初始化首列\n    for (int i = 0; i <= n; i++) {\n        dp[i][0] = 1;\n    }\n    // 状态转移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 若超过目标金额,则不选硬币 i\n                dp[i][a] = dp[i - 1][a];\n            } else {\n                // 不选和选硬币 i 这两种方案之和\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]];\n            }\n        }\n    }\n    int res = dp[n][amt];\n    // 释放内存\n    for (int i = 0; i <= n; i++) {\n        free(dp[i]);\n    }\n    free(dp);\n    return res;\n}\n
coin_change_ii.kt
/* 零钱兑换 II:动态规划 */\nfun coinChangeIIDP(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    // 初始化 dp 表\n    val dp = Array(n + 1) { IntArray(amt + 1) }\n    // 初始化首列\n    for (i in 0..n) {\n        dp[i][0] = 1\n    }\n    // 状态转移\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // 若超过目标金额,则不选硬币 i\n                dp[i][a] = dp[i - 1][a]\n            } else {\n                // 不选和选硬币 i 这两种方案之和\n                dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]\n            }\n        }\n    }\n    return dp[n][amt]\n}\n
coin_change_ii.rb
### 零钱兑换 II:动态规划 ###\ndef coin_change_ii_dp(coins, amt)\n  n = coins.length\n  # 初始化 dp 表\n  dp = Array.new(n + 1) { Array.new(amt + 1, 0) }\n  # 初始化首列\n  (0...(n + 1)).each { |i| dp[i][0] = 1 }\n  # 状态转移\n  for i in 1...(n + 1)\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # 若超过目标金额,则不选硬币 i\n        dp[i][a] = dp[i - 1][a]\n      else\n        # 不选和选硬币 i 这两种方案之和\n        dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]\n      end\n    end\n  end\n  dp[n][amt]\nend\n
可视化运行

全屏观看 >

","path":["第 14 章   动态规划","14.5   完全背包问题"],"tags":[]},{"location":"chapter_dynamic_programming/unbounded_knapsack_problem/#3_2","level":3,"title":"3.   空间优化","text":"

空间优化处理方式相同,删除硬币维度即可:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change_ii.py
def coin_change_ii_dp_comp(coins: list[int], amt: int) -> int:\n    \"\"\"零钱兑换 II:空间优化后的动态规划\"\"\"\n    n = len(coins)\n    # 初始化 dp 表\n    dp = [0] * (amt + 1)\n    dp[0] = 1\n    # 状态转移\n    for i in range(1, n + 1):\n        # 正序遍历\n        for a in range(1, amt + 1):\n            if coins[i - 1] > a:\n                # 若超过目标金额,则不选硬币 i\n                dp[a] = dp[a]\n            else:\n                # 不选和选硬币 i 这两种方案之和\n                dp[a] = dp[a] + dp[a - coins[i - 1]]\n    return dp[amt]\n
coin_change_ii.cpp
/* 零钱兑换 II:空间优化后的动态规划 */\nint coinChangeIIDPComp(vector<int> &coins, int amt) {\n    int n = coins.size();\n    // 初始化 dp 表\n    vector<int> dp(amt + 1, 0);\n    dp[0] = 1;\n    // 状态转移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 若超过目标金额,则不选硬币 i\n                dp[a] = dp[a];\n            } else {\n                // 不选和选硬币 i 这两种方案之和\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
coin_change_ii.java
/* 零钱兑换 II:空间优化后的动态规划 */\nint coinChangeIIDPComp(int[] coins, int amt) {\n    int n = coins.length;\n    // 初始化 dp 表\n    int[] dp = new int[amt + 1];\n    dp[0] = 1;\n    // 状态转移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 若超过目标金额,则不选硬币 i\n                dp[a] = dp[a];\n            } else {\n                // 不选和选硬币 i 这两种方案之和\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
coin_change_ii.cs
/* 零钱兑换 II:空间优化后的动态规划 */\nint CoinChangeIIDPComp(int[] coins, int amt) {\n    int n = coins.Length;\n    // 初始化 dp 表\n    int[] dp = new int[amt + 1];\n    dp[0] = 1;\n    // 状态转移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 若超过目标金额,则不选硬币 i\n                dp[a] = dp[a];\n            } else {\n                // 不选和选硬币 i 这两种方案之和\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
coin_change_ii.go
/* 零钱兑换 II:空间优化后的动态规划 */\nfunc coinChangeIIDPComp(coins []int, amt int) int {\n    n := len(coins)\n    // 初始化 dp 表\n    dp := make([]int, amt+1)\n    dp[0] = 1\n    // 状态转移\n    for i := 1; i <= n; i++ {\n        // 正序遍历\n        for a := 1; a <= amt; a++ {\n            if coins[i-1] > a {\n                // 若超过目标金额,则不选硬币 i\n                dp[a] = dp[a]\n            } else {\n                // 不选和选硬币 i 这两种方案之和\n                dp[a] = dp[a] + dp[a-coins[i-1]]\n            }\n        }\n    }\n    return dp[amt]\n}\n
coin_change_ii.swift
/* 零钱兑换 II:空间优化后的动态规划 */\nfunc coinChangeIIDPComp(coins: [Int], amt: Int) -> Int {\n    let n = coins.count\n    // 初始化 dp 表\n    var dp = Array(repeating: 0, count: amt + 1)\n    dp[0] = 1\n    // 状态转移\n    for i in 1 ... n {\n        for a in 1 ... amt {\n            if coins[i - 1] > a {\n                // 若超过目标金额,则不选硬币 i\n                dp[a] = dp[a]\n            } else {\n                // 不选和选硬币 i 这两种方案之和\n                dp[a] = dp[a] + dp[a - coins[i - 1]]\n            }\n        }\n    }\n    return dp[amt]\n}\n
coin_change_ii.js
/* 零钱兑换 II:空间优化后的动态规划 */\nfunction coinChangeIIDPComp(coins, amt) {\n    const n = coins.length;\n    // 初始化 dp 表\n    const dp = Array.from({ length: amt + 1 }, () => 0);\n    dp[0] = 1;\n    // 状态转移\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 若超过目标金额,则不选硬币 i\n                dp[a] = dp[a];\n            } else {\n                // 不选和选硬币 i 这两种方案之和\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
coin_change_ii.ts
/* 零钱兑换 II:空间优化后的动态规划 */\nfunction coinChangeIIDPComp(coins: Array<number>, amt: number): number {\n    const n = coins.length;\n    // 初始化 dp 表\n    const dp = Array.from({ length: amt + 1 }, () => 0);\n    dp[0] = 1;\n    // 状态转移\n    for (let i = 1; i <= n; i++) {\n        for (let a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 若超过目标金额,则不选硬币 i\n                dp[a] = dp[a];\n            } else {\n                // 不选和选硬币 i 这两种方案之和\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    return dp[amt];\n}\n
coin_change_ii.dart
/* 零钱兑换 II:空间优化后的动态规划 */\nint coinChangeIIDPComp(List<int> coins, int amt) {\n  int n = coins.length;\n  // 初始化 dp 表\n  List<int> dp = List.filled(amt + 1, 0);\n  dp[0] = 1;\n  // 状态转移\n  for (int i = 1; i <= n; i++) {\n    for (int a = 1; a <= amt; a++) {\n      if (coins[i - 1] > a) {\n        // 若超过目标金额,则不选硬币 i\n        dp[a] = dp[a];\n      } else {\n        // 不选和选硬币 i 这两种方案之和\n        dp[a] = dp[a] + dp[a - coins[i - 1]];\n      }\n    }\n  }\n  return dp[amt];\n}\n
coin_change_ii.rs
/* 零钱兑换 II:空间优化后的动态规划 */\nfn coin_change_ii_dp_comp(coins: &[i32], amt: usize) -> i32 {\n    let n = coins.len();\n    // 初始化 dp 表\n    let mut dp = vec![0; amt + 1];\n    dp[0] = 1;\n    // 状态转移\n    for i in 1..=n {\n        for a in 1..=amt {\n            if coins[i - 1] > a as i32 {\n                // 若超过目标金额,则不选硬币 i\n                dp[a] = dp[a];\n            } else {\n                // 不选和选硬币 i 这两种方案之和\n                dp[a] = dp[a] + dp[a - coins[i - 1] as usize];\n            }\n        }\n    }\n    dp[amt]\n}\n
coin_change_ii.c
/* 零钱兑换 II:空间优化后的动态规划 */\nint coinChangeIIDPComp(int coins[], int amt, int coinsSize) {\n    int n = coinsSize;\n    // 初始化 dp 表\n    int *dp = calloc(amt + 1, sizeof(int));\n    dp[0] = 1;\n    // 状态转移\n    for (int i = 1; i <= n; i++) {\n        for (int a = 1; a <= amt; a++) {\n            if (coins[i - 1] > a) {\n                // 若超过目标金额,则不选硬币 i\n                dp[a] = dp[a];\n            } else {\n                // 不选和选硬币 i 这两种方案之和\n                dp[a] = dp[a] + dp[a - coins[i - 1]];\n            }\n        }\n    }\n    int res = dp[amt];\n    // 释放内存\n    free(dp);\n    return res;\n}\n
coin_change_ii.kt
/* 零钱兑换 II:空间优化后的动态规划 */\nfun coinChangeIIDPComp(coins: IntArray, amt: Int): Int {\n    val n = coins.size\n    // 初始化 dp 表\n    val dp = IntArray(amt + 1)\n    dp[0] = 1\n    // 状态转移\n    for (i in 1..n) {\n        for (a in 1..amt) {\n            if (coins[i - 1] > a) {\n                // 若超过目标金额,则不选硬币 i\n                dp[a] = dp[a]\n            } else {\n                // 不选和选硬币 i 这两种方案之和\n                dp[a] = dp[a] + dp[a - coins[i - 1]]\n            }\n        }\n    }\n    return dp[amt]\n}\n
coin_change_ii.rb
### 零钱兑换 II:空间优化后的动态规划 ###\ndef coin_change_ii_dp_comp(coins, amt)\n  n = coins.length\n  # 初始化 dp 表\n  dp = Array.new(amt + 1, 0)\n  dp[0] = 1\n  # 状态转移\n  for i in 1...(n + 1)\n    # 正序遍历\n    for a in 1...(amt + 1)\n      if coins[i - 1] > a\n        # 若超过目标金额,则不选硬币 i\n        dp[a] = dp[a]\n      else\n        # 不选和选硬币 i 这两种方案之和\n        dp[a] = dp[a] + dp[a - coins[i - 1]]\n      end\n    end\n  end\n  dp[amt]\nend\n
可视化运行

全屏观看 >

","path":["第 14 章   动态规划","14.5   完全背包问题"],"tags":[]},{"location":"chapter_graph/","level":1,"title":"第 9 章   图","text":"

Abstract

在生命旅途中,我们就像是一个个节点,被无数看不见的边相连。

每一次的相识与相离,都在这张巨大的网络图中留下独特的印记。

","path":["第 9 章   图"],"tags":[]},{"location":"chapter_graph/#_1","level":2,"title":"本章内容","text":"","path":["第 9 章   图"],"tags":[]},{"location":"chapter_graph/graph/","level":1,"title":"9.1   图","text":"

图(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} \\]

如果将顶点看作节点,将边看作连接各个节点的引用(指针),我们就可以将图看作一种从链表拓展而来的数据结构。如图 9-1 所示,相较于线性关系(链表)和分治关系(树),网络关系(图)的自由度更高,因而更为复杂。

图 9-1   链表、树、图之间的关系

","path":["第 9 章   图","9.1   图"],"tags":[]},{"location":"chapter_graph/graph/#911","level":2,"title":"9.1.1   图的常见类型与术语","text":"

根据边是否具有方向,可分为无向图(undirected graph)和有向图(directed graph),如图 9-2 所示。

图 9-2   有向图与无向图

根据所有顶点是否连通,可分为连通图(connected graph)和非连通图(disconnected graph),如图 9-3 所示。

图 9-3   连通图与非连通图

我们还可以为边添加“权重”变量,从而得到如图 9-4 所示的有权图(weighted graph)。例如在《王者荣耀》等手游中,系统会根据共同游戏时间来计算玩家之间的“亲密度”,这种亲密度网络就可以用有权图来表示。

图 9-4   有权图与无权图

图数据结构包含以下常用术语。

","path":["第 9 章   图","9.1   图"],"tags":[]},{"location":"chapter_graph/graph/#912","level":2,"title":"9.1.2   图的表示","text":"

图的常用表示方式包括“邻接矩阵”和“邻接表”。以下使用无向图进行举例。

","path":["第 9 章   图","9.1   图"],"tags":[]},{"location":"chapter_graph/graph/#1","level":3,"title":"1.   邻接矩阵","text":"

设图的顶点数量为 \\(n\\) ,邻接矩阵(adjacency matrix)使用一个 \\(n \\times n\\) 大小的矩阵来表示图,每一行(列)代表一个顶点,矩阵元素代表边,用 \\(1\\) 或 \\(0\\) 表示两个顶点之间是否存在边。

如图 9-5 所示,设邻接矩阵为 \\(M\\)、顶点列表为 \\(V\\) ,那么矩阵元素 \\(M[i, j] = 1\\) 表示顶点 \\(V[i]\\) 到顶点 \\(V[j]\\) 之间存在边,反之 \\(M[i, j] = 0\\) 表示两顶点之间无边。

图 9-5   图的邻接矩阵表示

邻接矩阵具有以下特性。

使用邻接矩阵表示图时,我们可以直接访问矩阵元素以获取边,因此增删查改操作的效率很高,时间复杂度均为 \\(O(1)\\) 。然而,矩阵的空间复杂度为 \\(O(n^2)\\) ,内存占用较多。

","path":["第 9 章   图","9.1   图"],"tags":[]},{"location":"chapter_graph/graph/#2","level":3,"title":"2.   邻接表","text":"

邻接表(adjacency list)使用 \\(n\\) 个链表来表示图,链表节点表示顶点。第 \\(i\\) 个链表对应顶点 \\(i\\) ,其中存储了该顶点的所有邻接顶点(与该顶点相连的顶点)。图 9-6 展示了一个使用邻接表存储的图的示例。

图 9-6   图的邻接表表示

邻接表仅存储实际存在的边,而边的总数通常远小于 \\(n^2\\) ,因此它更加节省空间。然而,在邻接表中需要通过遍历链表来查找边,因此其时间效率不如邻接矩阵。

观察图 9-6 ,邻接表结构与哈希表中的“链式地址”非常相似,因此我们也可以采用类似的方法来优化效率。比如当链表较长时,可以将链表转化为 AVL 树或红黑树,从而将时间效率从 \\(O(n)\\) 优化至 \\(O(\\log n)\\) ;还可以把链表转换为哈希表,从而将时间复杂度降至 \\(O(1)\\) 。

","path":["第 9 章   图","9.1   图"],"tags":[]},{"location":"chapter_graph/graph/#913","level":2,"title":"9.1.3   图的常见应用","text":"

如表 9-1 所示,许多现实系统可以用图来建模,相应的问题也可以约化为图计算问题。

表 9-1   现实生活中常见的图

顶点 边 图计算问题 社交网络 用户 好友关系 潜在好友推荐 地铁线路 站点 站点间的连通性 最短路线推荐 太阳系 星体 星体间的万有引力作用 行星轨道计算","path":["第 9 章   图","9.1   图"],"tags":[]},{"location":"chapter_graph/graph_operations/","level":1,"title":"9.2   图的基础操作","text":"

图的基础操作可分为对“边”的操作和对“顶点”的操作。在“邻接矩阵”和“邻接表”两种表示方法下,实现方式有所不同。

","path":["第 9 章   图","9.2   图的基础操作"],"tags":[]},{"location":"chapter_graph/graph_operations/#921","level":2,"title":"9.2.1   基于邻接矩阵的实现","text":"

给定一个顶点数量为 \\(n\\) 的无向图,则各种操作的实现方式如图 9-7 所示。

<1><2><3><4><5>

图 9-7   邻接矩阵的初始化、增删边、增删顶点

以下是基于邻接矩阵表示图的实现代码:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_adjacency_matrix.py
class GraphAdjMat:\n    \"\"\"基于邻接矩阵实现的无向图类\"\"\"\n\n    def __init__(self, vertices: list[int], edges: list[list[int]]):\n        \"\"\"构造方法\"\"\"\n        # 顶点列表,元素代表“顶点值”,索引代表“顶点索引”\n        self.vertices: list[int] = []\n        # 邻接矩阵,行列索引对应“顶点索引”\n        self.adj_mat: list[list[int]] = []\n        # 添加顶点\n        for val in vertices:\n            self.add_vertex(val)\n        # 添加边\n        # 请注意,edges 元素代表顶点索引,即对应 vertices 元素索引\n        for e in edges:\n            self.add_edge(e[0], e[1])\n\n    def size(self) -> int:\n        \"\"\"获取顶点数量\"\"\"\n        return len(self.vertices)\n\n    def add_vertex(self, val: int):\n        \"\"\"添加顶点\"\"\"\n        n = self.size()\n        # 向顶点列表中添加新顶点的值\n        self.vertices.append(val)\n        # 在邻接矩阵中添加一行\n        new_row = [0] * n\n        self.adj_mat.append(new_row)\n        # 在邻接矩阵中添加一列\n        for row in self.adj_mat:\n            row.append(0)\n\n    def remove_vertex(self, index: int):\n        \"\"\"删除顶点\"\"\"\n        if index >= self.size():\n            raise IndexError()\n        # 在顶点列表中移除索引 index 的顶点\n        self.vertices.pop(index)\n        # 在邻接矩阵中删除索引 index 的行\n        self.adj_mat.pop(index)\n        # 在邻接矩阵中删除索引 index 的列\n        for row in self.adj_mat:\n            row.pop(index)\n\n    def add_edge(self, i: int, j: int):\n        \"\"\"添加边\"\"\"\n        # 参数 i, j 对应 vertices 元素索引\n        # 索引越界与相等处理\n        if i < 0 or j < 0 or i >= self.size() or j >= self.size() or i == j:\n            raise IndexError()\n        # 在无向图中,邻接矩阵关于主对角线对称,即满足 (i, j) == (j, i)\n        self.adj_mat[i][j] = 1\n        self.adj_mat[j][i] = 1\n\n    def remove_edge(self, i: int, j: int):\n        \"\"\"删除边\"\"\"\n        # 参数 i, j 对应 vertices 元素索引\n        # 索引越界与相等处理\n        if i < 0 or j < 0 or i >= self.size() or j >= self.size() or i == j:\n            raise IndexError()\n        self.adj_mat[i][j] = 0\n        self.adj_mat[j][i] = 0\n\n    def print(self):\n        \"\"\"打印邻接矩阵\"\"\"\n        print(\"顶点列表 =\", self.vertices)\n        print(\"邻接矩阵 =\")\n        print_matrix(self.adj_mat)\n
graph_adjacency_matrix.cpp
/* 基于邻接矩阵实现的无向图类 */\nclass GraphAdjMat {\n    vector<int> vertices;       // 顶点列表,元素代表“顶点值”,索引代表“顶点索引”\n    vector<vector<int>> adjMat; // 邻接矩阵,行列索引对应“顶点索引”\n\n  public:\n    /* 构造方法 */\n    GraphAdjMat(const vector<int> &vertices, const vector<vector<int>> &edges) {\n        // 添加顶点\n        for (int val : vertices) {\n            addVertex(val);\n        }\n        // 添加边\n        // 请注意,edges 元素代表顶点索引,即对应 vertices 元素索引\n        for (const vector<int> &edge : edges) {\n            addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* 获取顶点数量 */\n    int size() const {\n        return vertices.size();\n    }\n\n    /* 添加顶点 */\n    void addVertex(int val) {\n        int n = size();\n        // 向顶点列表中添加新顶点的值\n        vertices.push_back(val);\n        // 在邻接矩阵中添加一行\n        adjMat.emplace_back(vector<int>(n, 0));\n        // 在邻接矩阵中添加一列\n        for (vector<int> &row : adjMat) {\n            row.push_back(0);\n        }\n    }\n\n    /* 删除顶点 */\n    void removeVertex(int index) {\n        if (index >= size()) {\n            throw out_of_range(\"顶点不存在\");\n        }\n        // 在顶点列表中移除索引 index 的顶点\n        vertices.erase(vertices.begin() + index);\n        // 在邻接矩阵中删除索引 index 的行\n        adjMat.erase(adjMat.begin() + index);\n        // 在邻接矩阵中删除索引 index 的列\n        for (vector<int> &row : adjMat) {\n            row.erase(row.begin() + index);\n        }\n    }\n\n    /* 添加边 */\n    // 参数 i, j 对应 vertices 元素索引\n    void addEdge(int i, int j) {\n        // 索引越界与相等处理\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n            throw out_of_range(\"顶点不存在\");\n        }\n        // 在无向图中,邻接矩阵关于主对角线对称,即满足 (i, j) == (j, i)\n        adjMat[i][j] = 1;\n        adjMat[j][i] = 1;\n    }\n\n    /* 删除边 */\n    // 参数 i, j 对应 vertices 元素索引\n    void removeEdge(int i, int j) {\n        // 索引越界与相等处理\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n            throw out_of_range(\"顶点不存在\");\n        }\n        adjMat[i][j] = 0;\n        adjMat[j][i] = 0;\n    }\n\n    /* 打印邻接矩阵 */\n    void print() {\n        cout << \"顶点列表 = \";\n        printVector(vertices);\n        cout << \"邻接矩阵 =\" << endl;\n        printVectorMatrix(adjMat);\n    }\n};\n
graph_adjacency_matrix.java
/* 基于邻接矩阵实现的无向图类 */\nclass GraphAdjMat {\n    List<Integer> vertices; // 顶点列表,元素代表“顶点值”,索引代表“顶点索引”\n    List<List<Integer>> adjMat; // 邻接矩阵,行列索引对应“顶点索引”\n\n    /* 构造方法 */\n    public GraphAdjMat(int[] vertices, int[][] edges) {\n        this.vertices = new ArrayList<>();\n        this.adjMat = new ArrayList<>();\n        // 添加顶点\n        for (int val : vertices) {\n            addVertex(val);\n        }\n        // 添加边\n        // 请注意,edges 元素代表顶点索引,即对应 vertices 元素索引\n        for (int[] e : edges) {\n            addEdge(e[0], e[1]);\n        }\n    }\n\n    /* 获取顶点数量 */\n    public int size() {\n        return vertices.size();\n    }\n\n    /* 添加顶点 */\n    public void addVertex(int val) {\n        int n = size();\n        // 向顶点列表中添加新顶点的值\n        vertices.add(val);\n        // 在邻接矩阵中添加一行\n        List<Integer> newRow = new ArrayList<>(n);\n        for (int j = 0; j < n; j++) {\n            newRow.add(0);\n        }\n        adjMat.add(newRow);\n        // 在邻接矩阵中添加一列\n        for (List<Integer> row : adjMat) {\n            row.add(0);\n        }\n    }\n\n    /* 删除顶点 */\n    public void removeVertex(int index) {\n        if (index >= size())\n            throw new IndexOutOfBoundsException();\n        // 在顶点列表中移除索引 index 的顶点\n        vertices.remove(index);\n        // 在邻接矩阵中删除索引 index 的行\n        adjMat.remove(index);\n        // 在邻接矩阵中删除索引 index 的列\n        for (List<Integer> row : adjMat) {\n            row.remove(index);\n        }\n    }\n\n    /* 添加边 */\n    // 参数 i, j 对应 vertices 元素索引\n    public void addEdge(int i, int j) {\n        // 索引越界与相等处理\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n            throw new IndexOutOfBoundsException();\n        // 在无向图中,邻接矩阵关于主对角线对称,即满足 (i, j) == (j, i)\n        adjMat.get(i).set(j, 1);\n        adjMat.get(j).set(i, 1);\n    }\n\n    /* 删除边 */\n    // 参数 i, j 对应 vertices 元素索引\n    public void removeEdge(int i, int j) {\n        // 索引越界与相等处理\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n            throw new IndexOutOfBoundsException();\n        adjMat.get(i).set(j, 0);\n        adjMat.get(j).set(i, 0);\n    }\n\n    /* 打印邻接矩阵 */\n    public void print() {\n        System.out.print(\"顶点列表 = \");\n        System.out.println(vertices);\n        System.out.println(\"邻接矩阵 =\");\n        PrintUtil.printMatrix(adjMat);\n    }\n}\n
graph_adjacency_matrix.cs
/* 基于邻接矩阵实现的无向图类 */\nclass GraphAdjMat {\n    List<int> vertices;     // 顶点列表,元素代表“顶点值”,索引代表“顶点索引”\n    List<List<int>> adjMat; // 邻接矩阵,行列索引对应“顶点索引”\n\n    /* 构造函数 */\n    public GraphAdjMat(int[] vertices, int[][] edges) {\n        this.vertices = [];\n        this.adjMat = [];\n        // 添加顶点\n        foreach (int val in vertices) {\n            AddVertex(val);\n        }\n        // 添加边\n        // 请注意,edges 元素代表顶点索引,即对应 vertices 元素索引\n        foreach (int[] e in edges) {\n            AddEdge(e[0], e[1]);\n        }\n    }\n\n    /* 获取顶点数量 */\n    int Size() {\n        return vertices.Count;\n    }\n\n    /* 添加顶点 */\n    public void AddVertex(int val) {\n        int n = Size();\n        // 向顶点列表中添加新顶点的值\n        vertices.Add(val);\n        // 在邻接矩阵中添加一行\n        List<int> newRow = new(n);\n        for (int j = 0; j < n; j++) {\n            newRow.Add(0);\n        }\n        adjMat.Add(newRow);\n        // 在邻接矩阵中添加一列\n        foreach (List<int> row in adjMat) {\n            row.Add(0);\n        }\n    }\n\n    /* 删除顶点 */\n    public void RemoveVertex(int index) {\n        if (index >= Size())\n            throw new IndexOutOfRangeException();\n        // 在顶点列表中移除索引 index 的顶点\n        vertices.RemoveAt(index);\n        // 在邻接矩阵中删除索引 index 的行\n        adjMat.RemoveAt(index);\n        // 在邻接矩阵中删除索引 index 的列\n        foreach (List<int> row in adjMat) {\n            row.RemoveAt(index);\n        }\n    }\n\n    /* 添加边 */\n    // 参数 i, j 对应 vertices 元素索引\n    public void AddEdge(int i, int j) {\n        // 索引越界与相等处理\n        if (i < 0 || j < 0 || i >= Size() || j >= Size() || i == j)\n            throw new IndexOutOfRangeException();\n        // 在无向图中,邻接矩阵关于主对角线对称,即满足 (i, j) == (j, i)\n        adjMat[i][j] = 1;\n        adjMat[j][i] = 1;\n    }\n\n    /* 删除边 */\n    // 参数 i, j 对应 vertices 元素索引\n    public void RemoveEdge(int i, int j) {\n        // 索引越界与相等处理\n        if (i < 0 || j < 0 || i >= Size() || j >= Size() || i == j)\n            throw new IndexOutOfRangeException();\n        adjMat[i][j] = 0;\n        adjMat[j][i] = 0;\n    }\n\n    /* 打印邻接矩阵 */\n    public void Print() {\n        Console.Write(\"顶点列表 = \");\n        PrintUtil.PrintList(vertices);\n        Console.WriteLine(\"邻接矩阵 =\");\n        PrintUtil.PrintMatrix(adjMat);\n    }\n}\n
graph_adjacency_matrix.go
/* 基于邻接矩阵实现的无向图类 */\ntype graphAdjMat struct {\n    // 顶点列表,元素代表“顶点值”,索引代表“顶点索引”\n    vertices []int\n    // 邻接矩阵,行列索引对应“顶点索引”\n    adjMat [][]int\n}\n\n/* 构造函数 */\nfunc newGraphAdjMat(vertices []int, edges [][]int) *graphAdjMat {\n    // 添加顶点\n    n := len(vertices)\n    adjMat := make([][]int, n)\n    for i := range adjMat {\n        adjMat[i] = make([]int, n)\n    }\n    // 初始化图\n    g := &graphAdjMat{\n        vertices: vertices,\n        adjMat:   adjMat,\n    }\n    // 添加边\n    // 请注意,edges 元素代表顶点索引,即对应 vertices 元素索引\n    for i := range edges {\n        g.addEdge(edges[i][0], edges[i][1])\n    }\n    return g\n}\n\n/* 获取顶点数量 */\nfunc (g *graphAdjMat) size() int {\n    return len(g.vertices)\n}\n\n/* 添加顶点 */\nfunc (g *graphAdjMat) addVertex(val int) {\n    n := g.size()\n    // 向顶点列表中添加新顶点的值\n    g.vertices = append(g.vertices, val)\n    // 在邻接矩阵中添加一行\n    newRow := make([]int, n)\n    g.adjMat = append(g.adjMat, newRow)\n    // 在邻接矩阵中添加一列\n    for i := range g.adjMat {\n        g.adjMat[i] = append(g.adjMat[i], 0)\n    }\n}\n\n/* 删除顶点 */\nfunc (g *graphAdjMat) removeVertex(index int) {\n    if index >= g.size() {\n        return\n    }\n    // 在顶点列表中移除索引 index 的顶点\n    g.vertices = append(g.vertices[:index], g.vertices[index+1:]...)\n    // 在邻接矩阵中删除索引 index 的行\n    g.adjMat = append(g.adjMat[:index], g.adjMat[index+1:]...)\n    // 在邻接矩阵中删除索引 index 的列\n    for i := range g.adjMat {\n        g.adjMat[i] = append(g.adjMat[i][:index], g.adjMat[i][index+1:]...)\n    }\n}\n\n/* 添加边 */\n// 参数 i, j 对应 vertices 元素索引\nfunc (g *graphAdjMat) addEdge(i, j int) {\n    // 索引越界与相等处理\n    if i < 0 || j < 0 || i >= g.size() || j >= g.size() || i == j {\n        fmt.Errorf(\"%s\", \"Index Out Of Bounds Exception\")\n    }\n    // 在无向图中,邻接矩阵关于主对角线对称,即满足 (i, j) == (j, i)\n    g.adjMat[i][j] = 1\n    g.adjMat[j][i] = 1\n}\n\n/* 删除边 */\n// 参数 i, j 对应 vertices 元素索引\nfunc (g *graphAdjMat) removeEdge(i, j int) {\n    // 索引越界与相等处理\n    if i < 0 || j < 0 || i >= g.size() || j >= g.size() || i == j {\n        fmt.Errorf(\"%s\", \"Index Out Of Bounds Exception\")\n    }\n    g.adjMat[i][j] = 0\n    g.adjMat[j][i] = 0\n}\n\n/* 打印邻接矩阵 */\nfunc (g *graphAdjMat) print() {\n    fmt.Printf(\"\\t顶点列表 = %v\\n\", g.vertices)\n    fmt.Printf(\"\\t邻接矩阵 = \\n\")\n    for i := range g.adjMat {\n        fmt.Printf(\"\\t\\t\\t%v\\n\", g.adjMat[i])\n    }\n}\n
graph_adjacency_matrix.swift
/* 基于邻接矩阵实现的无向图类 */\nclass GraphAdjMat {\n    private var vertices: [Int] // 顶点列表,元素代表“顶点值”,索引代表“顶点索引”\n    private var adjMat: [[Int]] // 邻接矩阵,行列索引对应“顶点索引”\n\n    /* 构造方法 */\n    init(vertices: [Int], edges: [[Int]]) {\n        self.vertices = []\n        adjMat = []\n        // 添加顶点\n        for val in vertices {\n            addVertex(val: val)\n        }\n        // 添加边\n        // 请注意,edges 元素代表顶点索引,即对应 vertices 元素索引\n        for e in edges {\n            addEdge(i: e[0], j: e[1])\n        }\n    }\n\n    /* 获取顶点数量 */\n    func size() -> Int {\n        vertices.count\n    }\n\n    /* 添加顶点 */\n    func addVertex(val: Int) {\n        let n = size()\n        // 向顶点列表中添加新顶点的值\n        vertices.append(val)\n        // 在邻接矩阵中添加一行\n        let newRow = Array(repeating: 0, count: n)\n        adjMat.append(newRow)\n        // 在邻接矩阵中添加一列\n        for i in adjMat.indices {\n            adjMat[i].append(0)\n        }\n    }\n\n    /* 删除顶点 */\n    func removeVertex(index: Int) {\n        if index >= size() {\n            fatalError(\"越界\")\n        }\n        // 在顶点列表中移除索引 index 的顶点\n        vertices.remove(at: index)\n        // 在邻接矩阵中删除索引 index 的行\n        adjMat.remove(at: index)\n        // 在邻接矩阵中删除索引 index 的列\n        for i in adjMat.indices {\n            adjMat[i].remove(at: index)\n        }\n    }\n\n    /* 添加边 */\n    // 参数 i, j 对应 vertices 元素索引\n    func addEdge(i: Int, j: Int) {\n        // 索引越界与相等处理\n        if i < 0 || j < 0 || i >= size() || j >= size() || i == j {\n            fatalError(\"越界\")\n        }\n        // 在无向图中,邻接矩阵关于主对角线对称,即满足 (i, j) == (j, i)\n        adjMat[i][j] = 1\n        adjMat[j][i] = 1\n    }\n\n    /* 删除边 */\n    // 参数 i, j 对应 vertices 元素索引\n    func removeEdge(i: Int, j: Int) {\n        // 索引越界与相等处理\n        if i < 0 || j < 0 || i >= size() || j >= size() || i == j {\n            fatalError(\"越界\")\n        }\n        adjMat[i][j] = 0\n        adjMat[j][i] = 0\n    }\n\n    /* 打印邻接矩阵 */\n    func print() {\n        Swift.print(\"顶点列表 = \", terminator: \"\")\n        Swift.print(vertices)\n        Swift.print(\"邻接矩阵 =\")\n        PrintUtil.printMatrix(matrix: adjMat)\n    }\n}\n
graph_adjacency_matrix.js
/* 基于邻接矩阵实现的无向图类 */\nclass GraphAdjMat {\n    vertices; // 顶点列表,元素代表“顶点值”,索引代表“顶点索引”\n    adjMat; // 邻接矩阵,行列索引对应“顶点索引”\n\n    /* 构造函数 */\n    constructor(vertices, edges) {\n        this.vertices = [];\n        this.adjMat = [];\n        // 添加顶点\n        for (const val of vertices) {\n            this.addVertex(val);\n        }\n        // 添加边\n        // 请注意,edges 元素代表顶点索引,即对应 vertices 元素索引\n        for (const e of edges) {\n            this.addEdge(e[0], e[1]);\n        }\n    }\n\n    /* 获取顶点数量 */\n    size() {\n        return this.vertices.length;\n    }\n\n    /* 添加顶点 */\n    addVertex(val) {\n        const n = this.size();\n        // 向顶点列表中添加新顶点的值\n        this.vertices.push(val);\n        // 在邻接矩阵中添加一行\n        const newRow = [];\n        for (let j = 0; j < n; j++) {\n            newRow.push(0);\n        }\n        this.adjMat.push(newRow);\n        // 在邻接矩阵中添加一列\n        for (const row of this.adjMat) {\n            row.push(0);\n        }\n    }\n\n    /* 删除顶点 */\n    removeVertex(index) {\n        if (index >= this.size()) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        // 在顶点列表中移除索引 index 的顶点\n        this.vertices.splice(index, 1);\n\n        // 在邻接矩阵中删除索引 index 的行\n        this.adjMat.splice(index, 1);\n        // 在邻接矩阵中删除索引 index 的列\n        for (const row of this.adjMat) {\n            row.splice(index, 1);\n        }\n    }\n\n    /* 添加边 */\n    // 参数 i, j 对应 vertices 元素索引\n    addEdge(i, j) {\n        // 索引越界与相等处理\n        if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        // 在无向图中,邻接矩阵关于主对角线对称,即满足 (i, j) === (j, i)\n        this.adjMat[i][j] = 1;\n        this.adjMat[j][i] = 1;\n    }\n\n    /* 删除边 */\n    // 参数 i, j 对应 vertices 元素索引\n    removeEdge(i, j) {\n        // 索引越界与相等处理\n        if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        this.adjMat[i][j] = 0;\n        this.adjMat[j][i] = 0;\n    }\n\n    /* 打印邻接矩阵 */\n    print() {\n        console.log('顶点列表 = ', this.vertices);\n        console.log('邻接矩阵 =', this.adjMat);\n    }\n}\n
graph_adjacency_matrix.ts
/* 基于邻接矩阵实现的无向图类 */\nclass GraphAdjMat {\n    vertices: number[]; // 顶点列表,元素代表“顶点值”,索引代表“顶点索引”\n    adjMat: number[][]; // 邻接矩阵,行列索引对应“顶点索引”\n\n    /* 构造函数 */\n    constructor(vertices: number[], edges: number[][]) {\n        this.vertices = [];\n        this.adjMat = [];\n        // 添加顶点\n        for (const val of vertices) {\n            this.addVertex(val);\n        }\n        // 添加边\n        // 请注意,edges 元素代表顶点索引,即对应 vertices 元素索引\n        for (const e of edges) {\n            this.addEdge(e[0], e[1]);\n        }\n    }\n\n    /* 获取顶点数量 */\n    size(): number {\n        return this.vertices.length;\n    }\n\n    /* 添加顶点 */\n    addVertex(val: number): void {\n        const n: number = this.size();\n        // 向顶点列表中添加新顶点的值\n        this.vertices.push(val);\n        // 在邻接矩阵中添加一行\n        const newRow: number[] = [];\n        for (let j: number = 0; j < n; j++) {\n            newRow.push(0);\n        }\n        this.adjMat.push(newRow);\n        // 在邻接矩阵中添加一列\n        for (const row of this.adjMat) {\n            row.push(0);\n        }\n    }\n\n    /* 删除顶点 */\n    removeVertex(index: number): void {\n        if (index >= this.size()) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        // 在顶点列表中移除索引 index 的顶点\n        this.vertices.splice(index, 1);\n\n        // 在邻接矩阵中删除索引 index 的行\n        this.adjMat.splice(index, 1);\n        // 在邻接矩阵中删除索引 index 的列\n        for (const row of this.adjMat) {\n            row.splice(index, 1);\n        }\n    }\n\n    /* 添加边 */\n    // 参数 i, j 对应 vertices 元素索引\n    addEdge(i: number, j: number): void {\n        // 索引越界与相等处理\n        if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        // 在无向图中,邻接矩阵关于主对角线对称,即满足 (i, j) === (j, i)\n        this.adjMat[i][j] = 1;\n        this.adjMat[j][i] = 1;\n    }\n\n    /* 删除边 */\n    // 参数 i, j 对应 vertices 元素索引\n    removeEdge(i: number, j: number): void {\n        // 索引越界与相等处理\n        if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) {\n            throw new RangeError('Index Out Of Bounds Exception');\n        }\n        this.adjMat[i][j] = 0;\n        this.adjMat[j][i] = 0;\n    }\n\n    /* 打印邻接矩阵 */\n    print(): void {\n        console.log('顶点列表 = ', this.vertices);\n        console.log('邻接矩阵 =', this.adjMat);\n    }\n}\n
graph_adjacency_matrix.dart
/* 基于邻接矩阵实现的无向图类 */\nclass GraphAdjMat {\n  List<int> vertices = []; // 顶点元素,元素代表“顶点值”,索引代表“顶点索引”\n  List<List<int>> adjMat = []; //邻接矩阵,行列索引对应“顶点索引”\n\n  /* 构造方法 */\n  GraphAdjMat(List<int> vertices, List<List<int>> edges) {\n    this.vertices = [];\n    this.adjMat = [];\n    // 添加顶点\n    for (int val in vertices) {\n      addVertex(val);\n    }\n    // 添加边\n    // 请注意,edges 元素代表顶点索引,即对应 vertices 元素索引\n    for (List<int> e in edges) {\n      addEdge(e[0], e[1]);\n    }\n  }\n\n  /* 获取顶点数量 */\n  int size() {\n    return vertices.length;\n  }\n\n  /* 添加顶点 */\n  void addVertex(int val) {\n    int n = size();\n    // 向顶点列表中添加新顶点的值\n    vertices.add(val);\n    // 在邻接矩阵中添加一行\n    List<int> newRow = List.filled(n, 0, growable: true);\n    adjMat.add(newRow);\n    // 在邻接矩阵中添加一列\n    for (List<int> row in adjMat) {\n      row.add(0);\n    }\n  }\n\n  /* 删除顶点 */\n  void removeVertex(int index) {\n    if (index >= size()) {\n      throw IndexError;\n    }\n    // 在顶点列表中移除索引 index 的顶点\n    vertices.removeAt(index);\n    // 在邻接矩阵中删除索引 index 的行\n    adjMat.removeAt(index);\n    // 在邻接矩阵中删除索引 index 的列\n    for (List<int> row in adjMat) {\n      row.removeAt(index);\n    }\n  }\n\n  /* 添加边 */\n  // 参数 i, j 对应 vertices 元素索引\n  void addEdge(int i, int j) {\n    // 索引越界与相等处理\n    if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n      throw IndexError;\n    }\n    // 在无向图中,邻接矩阵关于主对角线对称,即满足 (i, j) == (j, i)\n    adjMat[i][j] = 1;\n    adjMat[j][i] = 1;\n  }\n\n  /* 删除边 */\n  // 参数 i, j 对应 vertices 元素索引\n  void removeEdge(int i, int j) {\n    // 索引越界与相等处理\n    if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) {\n      throw IndexError;\n    }\n    adjMat[i][j] = 0;\n    adjMat[j][i] = 0;\n  }\n\n  /* 打印邻接矩阵 */\n  void printAdjMat() {\n    print(\"顶点列表 = $vertices\");\n    print(\"邻接矩阵 = \");\n    printMatrix(adjMat);\n  }\n}\n
graph_adjacency_matrix.rs
/* 基于邻接矩阵实现的无向图类型 */\npub struct GraphAdjMat {\n    // 顶点列表,元素代表“顶点值”,索引代表“顶点索引”\n    pub vertices: Vec<i32>,\n    // 邻接矩阵,行列索引对应“顶点索引”\n    pub adj_mat: Vec<Vec<i32>>,\n}\n\nimpl GraphAdjMat {\n    /* 构造方法 */\n    pub fn new(vertices: Vec<i32>, edges: Vec<[usize; 2]>) -> Self {\n        let mut graph = GraphAdjMat {\n            vertices: vec![],\n            adj_mat: vec![],\n        };\n        // 添加顶点\n        for val in vertices {\n            graph.add_vertex(val);\n        }\n        // 添加边\n        // 请注意,edges 元素代表顶点索引,即对应 vertices 元素索引\n        for edge in edges {\n            graph.add_edge(edge[0], edge[1])\n        }\n\n        graph\n    }\n\n    /* 获取顶点数量 */\n    pub fn size(&self) -> usize {\n        self.vertices.len()\n    }\n\n    /* 添加顶点 */\n    pub fn add_vertex(&mut self, val: i32) {\n        let n = self.size();\n        // 向顶点列表中添加新顶点的值\n        self.vertices.push(val);\n        // 在邻接矩阵中添加一行\n        self.adj_mat.push(vec![0; n]);\n        // 在邻接矩阵中添加一列\n        for row in self.adj_mat.iter_mut() {\n            row.push(0);\n        }\n    }\n\n    /* 删除顶点 */\n    pub fn remove_vertex(&mut self, index: usize) {\n        if index >= self.size() {\n            panic!(\"index error\")\n        }\n        // 在顶点列表中移除索引 index 的顶点\n        self.vertices.remove(index);\n        // 在邻接矩阵中删除索引 index 的行\n        self.adj_mat.remove(index);\n        // 在邻接矩阵中删除索引 index 的列\n        for row in self.adj_mat.iter_mut() {\n            row.remove(index);\n        }\n    }\n\n    /* 添加边 */\n    pub fn add_edge(&mut self, i: usize, j: usize) {\n        // 参数 i, j 对应 vertices 元素索引\n        // 索引越界与相等处理\n        if i >= self.size() || j >= self.size() || i == j {\n            panic!(\"index error\")\n        }\n        // 在无向图中,邻接矩阵关于主对角线对称,即满足 (i, j) == (j, i)\n        self.adj_mat[i][j] = 1;\n        self.adj_mat[j][i] = 1;\n    }\n\n    /* 删除边 */\n    // 参数 i, j 对应 vertices 元素索引\n    pub fn remove_edge(&mut self, i: usize, j: usize) {\n        // 参数 i, j 对应 vertices 元素索引\n        // 索引越界与相等处理\n        if i >= self.size() || j >= self.size() || i == j {\n            panic!(\"index error\")\n        }\n        self.adj_mat[i][j] = 0;\n        self.adj_mat[j][i] = 0;\n    }\n\n    /* 打印邻接矩阵 */\n    pub fn print(&self) {\n        println!(\"顶点列表 = {:?}\", self.vertices);\n        println!(\"邻接矩阵 =\");\n        println!(\"[\");\n        for row in &self.adj_mat {\n            println!(\"  {:?},\", row);\n        }\n        println!(\"]\")\n    }\n}\n
graph_adjacency_matrix.c
/* 基于邻接矩阵实现的无向图结构体 */\ntypedef struct {\n    int vertices[MAX_SIZE];\n    int adjMat[MAX_SIZE][MAX_SIZE];\n    int size;\n} GraphAdjMat;\n\n/* 构造函数 */\nGraphAdjMat *newGraphAdjMat() {\n    GraphAdjMat *graph = (GraphAdjMat *)malloc(sizeof(GraphAdjMat));\n    graph->size = 0;\n    for (int i = 0; i < MAX_SIZE; i++) {\n        for (int j = 0; j < MAX_SIZE; j++) {\n            graph->adjMat[i][j] = 0;\n        }\n    }\n    return graph;\n}\n\n/* 析构函数 */\nvoid delGraphAdjMat(GraphAdjMat *graph) {\n    free(graph);\n}\n\n/* 添加顶点 */\nvoid addVertex(GraphAdjMat *graph, int val) {\n    if (graph->size == MAX_SIZE) {\n        fprintf(stderr, \"图的顶点数量已达最大值\\n\");\n        return;\n    }\n    // 添加第 n 个顶点,并将第 n 行和列置零\n    int n = graph->size;\n    graph->vertices[n] = val;\n    for (int i = 0; i <= n; i++) {\n        graph->adjMat[n][i] = graph->adjMat[i][n] = 0;\n    }\n    graph->size++;\n}\n\n/* 删除顶点 */\nvoid removeVertex(GraphAdjMat *graph, int index) {\n    if (index < 0 || index >= graph->size) {\n        fprintf(stderr, \"顶点索引越界\\n\");\n        return;\n    }\n    // 在顶点列表中移除索引 index 的顶点\n    for (int i = index; i < graph->size - 1; i++) {\n        graph->vertices[i] = graph->vertices[i + 1];\n    }\n    // 在邻接矩阵中删除索引 index 的行\n    for (int i = index; i < graph->size - 1; i++) {\n        for (int j = 0; j < graph->size; j++) {\n            graph->adjMat[i][j] = graph->adjMat[i + 1][j];\n        }\n    }\n    // 在邻接矩阵中删除索引 index 的列\n    for (int i = 0; i < graph->size; i++) {\n        for (int j = index; j < graph->size - 1; j++) {\n            graph->adjMat[i][j] = graph->adjMat[i][j + 1];\n        }\n    }\n    graph->size--;\n}\n\n/* 添加边 */\n// 参数 i, j 对应 vertices 元素索引\nvoid addEdge(GraphAdjMat *graph, int i, int j) {\n    if (i < 0 || j < 0 || i >= graph->size || j >= graph->size || i == j) {\n        fprintf(stderr, \"边索引越界或相等\\n\");\n        return;\n    }\n    graph->adjMat[i][j] = 1;\n    graph->adjMat[j][i] = 1;\n}\n\n/* 删除边 */\n// 参数 i, j 对应 vertices 元素索引\nvoid removeEdge(GraphAdjMat *graph, int i, int j) {\n    if (i < 0 || j < 0 || i >= graph->size || j >= graph->size || i == j) {\n        fprintf(stderr, \"边索引越界或相等\\n\");\n        return;\n    }\n    graph->adjMat[i][j] = 0;\n    graph->adjMat[j][i] = 0;\n}\n\n/* 打印邻接矩阵 */\nvoid printGraphAdjMat(GraphAdjMat *graph) {\n    printf(\"顶点列表 = \");\n    printArray(graph->vertices, graph->size);\n    printf(\"邻接矩阵 =\\n\");\n    for (int i = 0; i < graph->size; i++) {\n        printArray(graph->adjMat[i], graph->size);\n    }\n}\n
graph_adjacency_matrix.kt
/* 基于邻接矩阵实现的无向图类 */\nclass GraphAdjMat(vertices: IntArray, edges: Array<IntArray>) {\n    val vertices = mutableListOf<Int>() // 顶点列表,元素代表“顶点值”,索引代表“顶点索引”\n    val adjMat = mutableListOf<MutableList<Int>>() // 邻接矩阵,行列索引对应“顶点索引”\n\n    /* 构造方法 */\n    init {\n        // 添加顶点\n        for (vertex in vertices) {\n            addVertex(vertex)\n        }\n        // 添加边\n        // 请注意,edges 元素代表顶点索引,即对应 vertices 元素索引\n        for (edge in edges) {\n            addEdge(edge[0], edge[1])\n        }\n    }\n\n    /* 获取顶点数量 */\n    fun size(): Int {\n        return vertices.size\n    }\n\n    /* 添加顶点 */\n    fun addVertex(_val: Int) {\n        val n = size()\n        // 向顶点列表中添加新顶点的值\n        vertices.add(_val)\n        // 在邻接矩阵中添加一行\n        val newRow = mutableListOf<Int>()\n        for (j in 0..<n) {\n            newRow.add(0)\n        }\n        adjMat.add(newRow)\n        // 在邻接矩阵中添加一列\n        for (row in adjMat) {\n            row.add(0)\n        }\n    }\n\n    /* 删除顶点 */\n    fun removeVertex(index: Int) {\n        if (index >= size())\n            throw IndexOutOfBoundsException()\n        // 在顶点列表中移除索引 index 的顶点\n        vertices.removeAt(index)\n        // 在邻接矩阵中删除索引 index 的行\n        adjMat.removeAt(index)\n        // 在邻接矩阵中删除索引 index 的列\n        for (row in adjMat) {\n            row.removeAt(index)\n        }\n    }\n\n    /* 添加边 */\n    // 参数 i, j 对应 vertices 元素索引\n    fun addEdge(i: Int, j: Int) {\n        // 索引越界与相等处理\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n            throw IndexOutOfBoundsException()\n        // 在无向图中,邻接矩阵关于主对角线对称,即满足 (i, j) == (j, i)\n        adjMat[i][j] = 1\n        adjMat[j][i] = 1\n    }\n\n    /* 删除边 */\n    // 参数 i, j 对应 vertices 元素索引\n    fun removeEdge(i: Int, j: Int) {\n        // 索引越界与相等处理\n        if (i < 0 || j < 0 || i >= size() || j >= size() || i == j)\n            throw IndexOutOfBoundsException()\n        adjMat[i][j] = 0\n        adjMat[j][i] = 0\n    }\n\n    /* 打印邻接矩阵 */\n    fun print() {\n        print(\"顶点列表 = \")\n        println(vertices)\n        println(\"邻接矩阵 =\")\n        printMatrix(adjMat)\n    }\n}\n
graph_adjacency_matrix.rb
### 基于邻接矩阵实现的无向图类 ###\nclass GraphAdjMat\n  def initialize(vertices, edges)\n    ### 构造方法 ###\n    # 顶点列表,元素代表“顶点值”,索引代表“顶点索引”\n    @vertices = []\n    # 邻接矩阵,行列索引对应“顶点索引”\n    @adj_mat = []\n    # 添加顶点\n    vertices.each { |val| add_vertex(val) }\n    # 添加边\n    # 请注意,edges 元素代表顶点索引,即对应 vertices 元素索引\n    edges.each { |e| add_edge(e[0], e[1]) }\n  end\n\n  ### 获取顶点数量 ###\n  def size\n    @vertices.length\n  end\n\n  ### 添加顶点 ###\n  def add_vertex(val)\n    n = size\n    # 向顶点列表中添加新顶点的值\n    @vertices << val\n    # 在邻接矩阵中添加一行\n    new_row = Array.new(n, 0)\n    @adj_mat << new_row\n    # 在邻接矩阵中添加一列\n    @adj_mat.each { |row| row << 0 }\n  end\n\n  ### 删除顶点 ###\n  def remove_vertex(index)\n    raise IndexError if index >= size\n\n    # 在顶点列表中移除索引 index 的顶点\n    @vertices.delete_at(index)\n    # 在邻接矩阵中删除索引 index 的行\n    @adj_mat.delete_at(index)\n    # 在邻接矩阵中删除索引 index 的列\n    @adj_mat.each { |row| row.delete_at(index) }\n  end\n\n  ### 添加边 ###\n  def add_edge(i, j)\n    # 参数 i, j 对应 vertices 元素索引\n    # 索引越界与相等处理\n    if i < 0 || j < 0 || i >= size || j >= size || i == j\n      raise IndexError\n    end\n    # 在无向图中,邻接矩阵关于主对角线对称,即满足 (i, j) == (j, i)\n    @adj_mat[i][j] = 1\n    @adj_mat[j][i] = 1\n  end\n\n  ### 删除边 ###\n  def remove_edge(i, j)\n    # 参数 i, j 对应 vertices 元素索引\n    # 索引越界与相等处理\n    if i < 0 || j < 0 || i >= size || j >= size || i == j\n      raise IndexError\n    end\n    @adj_mat[i][j] = 0\n    @adj_mat[j][i] = 0\n  end\n\n  ### 打印邻接矩阵 ###\n  def __print__\n    puts \"顶点列表 = #{@vertices}\"\n    puts '邻接矩阵 ='\n    print_matrix(@adj_mat)\n  end\nend\n
可视化运行

全屏观看 >

","path":["第 9 章   图","9.2   图的基础操作"],"tags":[]},{"location":"chapter_graph/graph_operations/#922","level":2,"title":"9.2.2   基于邻接表的实现","text":"

设无向图的顶点总数为 \\(n\\)、边总数为 \\(m\\) ,则可根据图 9-8 所示的方法实现各种操作。

<1><2><3><4><5>

图 9-8   邻接表的初始化、增删边、增删顶点

以下是邻接表的代码实现。对比图 9-8 ,实际代码有以下不同。

另外,我们在邻接表中使用 Vertex 类来表示顶点,这样做的原因是:如果与邻接矩阵一样,用列表索引来区分不同顶点,那么假设要删除索引为 \\(i\\) 的顶点,则需遍历整个邻接表,将所有大于 \\(i\\) 的索引全部减 \\(1\\) ,效率很低。而如果每个顶点都是唯一的 Vertex 实例,删除某一顶点之后就无须改动其他顶点了。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_adjacency_list.py
class GraphAdjList:\n    \"\"\"基于邻接表实现的无向图类\"\"\"\n\n    def __init__(self, edges: list[list[Vertex]]):\n        \"\"\"构造方法\"\"\"\n        # 邻接表,key:顶点,value:该顶点的所有邻接顶点\n        self.adj_list = dict[Vertex, list[Vertex]]()\n        # 添加所有顶点和边\n        for edge in edges:\n            self.add_vertex(edge[0])\n            self.add_vertex(edge[1])\n            self.add_edge(edge[0], edge[1])\n\n    def size(self) -> int:\n        \"\"\"获取顶点数量\"\"\"\n        return len(self.adj_list)\n\n    def add_edge(self, vet1: Vertex, vet2: Vertex):\n        \"\"\"添加边\"\"\"\n        if vet1 not in self.adj_list or vet2 not in self.adj_list or vet1 == vet2:\n            raise ValueError()\n        # 添加边 vet1 - vet2\n        self.adj_list[vet1].append(vet2)\n        self.adj_list[vet2].append(vet1)\n\n    def remove_edge(self, vet1: Vertex, vet2: Vertex):\n        \"\"\"删除边\"\"\"\n        if vet1 not in self.adj_list or vet2 not in self.adj_list or vet1 == vet2:\n            raise ValueError()\n        # 删除边 vet1 - vet2\n        self.adj_list[vet1].remove(vet2)\n        self.adj_list[vet2].remove(vet1)\n\n    def add_vertex(self, vet: Vertex):\n        \"\"\"添加顶点\"\"\"\n        if vet in self.adj_list:\n            return\n        # 在邻接表中添加一个新链表\n        self.adj_list[vet] = []\n\n    def remove_vertex(self, vet: Vertex):\n        \"\"\"删除顶点\"\"\"\n        if vet not in self.adj_list:\n            raise ValueError()\n        # 在邻接表中删除顶点 vet 对应的链表\n        self.adj_list.pop(vet)\n        # 遍历其他顶点的链表,删除所有包含 vet 的边\n        for vertex in self.adj_list:\n            if vet in self.adj_list[vertex]:\n                self.adj_list[vertex].remove(vet)\n\n    def print(self):\n        \"\"\"打印邻接表\"\"\"\n        print(\"邻接表 =\")\n        for vertex in self.adj_list:\n            tmp = [v.val for v in self.adj_list[vertex]]\n            print(f\"{vertex.val}: {tmp},\")\n
graph_adjacency_list.cpp
/* 基于邻接表实现的无向图类 */\nclass GraphAdjList {\n  public:\n    // 邻接表,key:顶点,value:该顶点的所有邻接顶点\n    unordered_map<Vertex *, vector<Vertex *>> adjList;\n\n    /* 在 vector 中删除指定节点 */\n    void remove(vector<Vertex *> &vec, Vertex *vet) {\n        for (int i = 0; i < vec.size(); i++) {\n            if (vec[i] == vet) {\n                vec.erase(vec.begin() + i);\n                break;\n            }\n        }\n    }\n\n    /* 构造方法 */\n    GraphAdjList(const vector<vector<Vertex *>> &edges) {\n        // 添加所有顶点和边\n        for (const vector<Vertex *> &edge : edges) {\n            addVertex(edge[0]);\n            addVertex(edge[1]);\n            addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* 获取顶点数量 */\n    int size() {\n        return adjList.size();\n    }\n\n    /* 添加边 */\n    void addEdge(Vertex *vet1, Vertex *vet2) {\n        if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)\n            throw invalid_argument(\"不存在顶点\");\n        // 添加边 vet1 - vet2\n        adjList[vet1].push_back(vet2);\n        adjList[vet2].push_back(vet1);\n    }\n\n    /* 删除边 */\n    void removeEdge(Vertex *vet1, Vertex *vet2) {\n        if (!adjList.count(vet1) || !adjList.count(vet2) || vet1 == vet2)\n            throw invalid_argument(\"不存在顶点\");\n        // 删除边 vet1 - vet2\n        remove(adjList[vet1], vet2);\n        remove(adjList[vet2], vet1);\n    }\n\n    /* 添加顶点 */\n    void addVertex(Vertex *vet) {\n        if (adjList.count(vet))\n            return;\n        // 在邻接表中添加一个新链表\n        adjList[vet] = vector<Vertex *>();\n    }\n\n    /* 删除顶点 */\n    void removeVertex(Vertex *vet) {\n        if (!adjList.count(vet))\n            throw invalid_argument(\"不存在顶点\");\n        // 在邻接表中删除顶点 vet 对应的链表\n        adjList.erase(vet);\n        // 遍历其他顶点的链表,删除所有包含 vet 的边\n        for (auto &adj : adjList) {\n            remove(adj.second, vet);\n        }\n    }\n\n    /* 打印邻接表 */\n    void print() {\n        cout << \"邻接表 =\" << endl;\n        for (auto &adj : adjList) {\n            const auto &key = adj.first;\n            const auto &vec = adj.second;\n            cout << key->val << \": \";\n            printVector(vetsToVals(vec));\n        }\n    }\n};\n
graph_adjacency_list.java
/* 基于邻接表实现的无向图类 */\nclass GraphAdjList {\n    // 邻接表,key:顶点,value:该顶点的所有邻接顶点\n    Map<Vertex, List<Vertex>> adjList;\n\n    /* 构造方法 */\n    public GraphAdjList(Vertex[][] edges) {\n        this.adjList = new HashMap<>();\n        // 添加所有顶点和边\n        for (Vertex[] edge : edges) {\n            addVertex(edge[0]);\n            addVertex(edge[1]);\n            addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* 获取顶点数量 */\n    public int size() {\n        return adjList.size();\n    }\n\n    /* 添加边 */\n    public void addEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw new IllegalArgumentException();\n        // 添加边 vet1 - vet2\n        adjList.get(vet1).add(vet2);\n        adjList.get(vet2).add(vet1);\n    }\n\n    /* 删除边 */\n    public void removeEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw new IllegalArgumentException();\n        // 删除边 vet1 - vet2\n        adjList.get(vet1).remove(vet2);\n        adjList.get(vet2).remove(vet1);\n    }\n\n    /* 添加顶点 */\n    public void addVertex(Vertex vet) {\n        if (adjList.containsKey(vet))\n            return;\n        // 在邻接表中添加一个新链表\n        adjList.put(vet, new ArrayList<>());\n    }\n\n    /* 删除顶点 */\n    public void removeVertex(Vertex vet) {\n        if (!adjList.containsKey(vet))\n            throw new IllegalArgumentException();\n        // 在邻接表中删除顶点 vet 对应的链表\n        adjList.remove(vet);\n        // 遍历其他顶点的链表,删除所有包含 vet 的边\n        for (List<Vertex> list : adjList.values()) {\n            list.remove(vet);\n        }\n    }\n\n    /* 打印邻接表 */\n    public void print() {\n        System.out.println(\"邻接表 =\");\n        for (Map.Entry<Vertex, List<Vertex>> pair : adjList.entrySet()) {\n            List<Integer> tmp = new ArrayList<>();\n            for (Vertex vertex : pair.getValue())\n                tmp.add(vertex.val);\n            System.out.println(pair.getKey().val + \": \" + tmp + \",\");\n        }\n    }\n}\n
graph_adjacency_list.cs
/* 基于邻接表实现的无向图类 */\nclass GraphAdjList {\n    // 邻接表,key:顶点,value:该顶点的所有邻接顶点\n    public Dictionary<Vertex, List<Vertex>> adjList;\n\n    /* 构造函数 */\n    public GraphAdjList(Vertex[][] edges) {\n        adjList = [];\n        // 添加所有顶点和边\n        foreach (Vertex[] edge in edges) {\n            AddVertex(edge[0]);\n            AddVertex(edge[1]);\n            AddEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* 获取顶点数量 */\n    int Size() {\n        return adjList.Count;\n    }\n\n    /* 添加边 */\n    public void AddEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.ContainsKey(vet1) || !adjList.ContainsKey(vet2) || vet1 == vet2)\n            throw new InvalidOperationException();\n        // 添加边 vet1 - vet2\n        adjList[vet1].Add(vet2);\n        adjList[vet2].Add(vet1);\n    }\n\n    /* 删除边 */\n    public void RemoveEdge(Vertex vet1, Vertex vet2) {\n        if (!adjList.ContainsKey(vet1) || !adjList.ContainsKey(vet2) || vet1 == vet2)\n            throw new InvalidOperationException();\n        // 删除边 vet1 - vet2\n        adjList[vet1].Remove(vet2);\n        adjList[vet2].Remove(vet1);\n    }\n\n    /* 添加顶点 */\n    public void AddVertex(Vertex vet) {\n        if (adjList.ContainsKey(vet))\n            return;\n        // 在邻接表中添加一个新链表\n        adjList.Add(vet, []);\n    }\n\n    /* 删除顶点 */\n    public void RemoveVertex(Vertex vet) {\n        if (!adjList.ContainsKey(vet))\n            throw new InvalidOperationException();\n        // 在邻接表中删除顶点 vet 对应的链表\n        adjList.Remove(vet);\n        // 遍历其他顶点的链表,删除所有包含 vet 的边\n        foreach (List<Vertex> list in adjList.Values) {\n            list.Remove(vet);\n        }\n    }\n\n    /* 打印邻接表 */\n    public void Print() {\n        Console.WriteLine(\"邻接表 =\");\n        foreach (KeyValuePair<Vertex, List<Vertex>> pair in adjList) {\n            List<int> tmp = [];\n            foreach (Vertex vertex in pair.Value)\n                tmp.Add(vertex.val);\n            Console.WriteLine(pair.Key.val + \": [\" + string.Join(\", \", tmp) + \"],\");\n        }\n    }\n}\n
graph_adjacency_list.go
/* 基于邻接表实现的无向图类 */\ntype graphAdjList struct {\n    // 邻接表,key:顶点,value:该顶点的所有邻接顶点\n    adjList map[Vertex][]Vertex\n}\n\n/* 构造函数 */\nfunc newGraphAdjList(edges [][]Vertex) *graphAdjList {\n    g := &graphAdjList{\n        adjList: make(map[Vertex][]Vertex),\n    }\n    // 添加所有顶点和边\n    for _, edge := range edges {\n        g.addVertex(edge[0])\n        g.addVertex(edge[1])\n        g.addEdge(edge[0], edge[1])\n    }\n    return g\n}\n\n/* 获取顶点数量 */\nfunc (g *graphAdjList) size() int {\n    return len(g.adjList)\n}\n\n/* 添加边 */\nfunc (g *graphAdjList) addEdge(vet1 Vertex, vet2 Vertex) {\n    _, ok1 := g.adjList[vet1]\n    _, ok2 := g.adjList[vet2]\n    if !ok1 || !ok2 || vet1 == vet2 {\n        panic(\"error\")\n    }\n    // 添加边 vet1 - vet2, 添加匿名 struct{},\n    g.adjList[vet1] = append(g.adjList[vet1], vet2)\n    g.adjList[vet2] = append(g.adjList[vet2], vet1)\n}\n\n/* 删除边 */\nfunc (g *graphAdjList) removeEdge(vet1 Vertex, vet2 Vertex) {\n    _, ok1 := g.adjList[vet1]\n    _, ok2 := g.adjList[vet2]\n    if !ok1 || !ok2 || vet1 == vet2 {\n        panic(\"error\")\n    }\n    // 删除边 vet1 - vet2\n    g.adjList[vet1] = DeleteSliceElms(g.adjList[vet1], vet2)\n    g.adjList[vet2] = DeleteSliceElms(g.adjList[vet2], vet1)\n}\n\n/* 添加顶点 */\nfunc (g *graphAdjList) addVertex(vet Vertex) {\n    _, ok := g.adjList[vet]\n    if ok {\n        return\n    }\n    // 在邻接表中添加一个新链表\n    g.adjList[vet] = make([]Vertex, 0)\n}\n\n/* 删除顶点 */\nfunc (g *graphAdjList) removeVertex(vet Vertex) {\n    _, ok := g.adjList[vet]\n    if !ok {\n        panic(\"error\")\n    }\n    // 在邻接表中删除顶点 vet 对应的链表\n    delete(g.adjList, vet)\n    // 遍历其他顶点的链表,删除所有包含 vet 的边\n    for v, list := range g.adjList {\n        g.adjList[v] = DeleteSliceElms(list, vet)\n    }\n}\n\n/* 打印邻接表 */\nfunc (g *graphAdjList) print() {\n    var builder strings.Builder\n    fmt.Printf(\"邻接表 = \\n\")\n    for k, v := range g.adjList {\n        builder.WriteString(\"\\t\\t\" + strconv.Itoa(k.Val) + \": \")\n        for _, vet := range v {\n            builder.WriteString(strconv.Itoa(vet.Val) + \" \")\n        }\n        fmt.Println(builder.String())\n        builder.Reset()\n    }\n}\n
graph_adjacency_list.swift
/* 基于邻接表实现的无向图类 */\nclass GraphAdjList {\n    // 邻接表,key:顶点,value:该顶点的所有邻接顶点\n    public private(set) var adjList: [Vertex: [Vertex]]\n\n    /* 构造方法 */\n    public init(edges: [[Vertex]]) {\n        adjList = [:]\n        // 添加所有顶点和边\n        for edge in edges {\n            addVertex(vet: edge[0])\n            addVertex(vet: edge[1])\n            addEdge(vet1: edge[0], vet2: edge[1])\n        }\n    }\n\n    /* 获取顶点数量 */\n    public func size() -> Int {\n        adjList.count\n    }\n\n    /* 添加边 */\n    public func addEdge(vet1: Vertex, vet2: Vertex) {\n        if adjList[vet1] == nil || adjList[vet2] == nil || vet1 == vet2 {\n            fatalError(\"参数错误\")\n        }\n        // 添加边 vet1 - vet2\n        adjList[vet1]?.append(vet2)\n        adjList[vet2]?.append(vet1)\n    }\n\n    /* 删除边 */\n    public func removeEdge(vet1: Vertex, vet2: Vertex) {\n        if adjList[vet1] == nil || adjList[vet2] == nil || vet1 == vet2 {\n            fatalError(\"参数错误\")\n        }\n        // 删除边 vet1 - vet2\n        adjList[vet1]?.removeAll { $0 == vet2 }\n        adjList[vet2]?.removeAll { $0 == vet1 }\n    }\n\n    /* 添加顶点 */\n    public func addVertex(vet: Vertex) {\n        if adjList[vet] != nil {\n            return\n        }\n        // 在邻接表中添加一个新链表\n        adjList[vet] = []\n    }\n\n    /* 删除顶点 */\n    public func removeVertex(vet: Vertex) {\n        if adjList[vet] == nil {\n            fatalError(\"参数错误\")\n        }\n        // 在邻接表中删除顶点 vet 对应的链表\n        adjList.removeValue(forKey: vet)\n        // 遍历其他顶点的链表,删除所有包含 vet 的边\n        for key in adjList.keys {\n            adjList[key]?.removeAll { $0 == vet }\n        }\n    }\n\n    /* 打印邻接表 */\n    public func print() {\n        Swift.print(\"邻接表 =\")\n        for (vertex, list) in adjList {\n            let list = list.map { $0.val }\n            Swift.print(\"\\(vertex.val): \\(list),\")\n        }\n    }\n}\n
graph_adjacency_list.js
/* 基于邻接表实现的无向图类 */\nclass GraphAdjList {\n    // 邻接表,key:顶点,value:该顶点的所有邻接顶点\n    adjList;\n\n    /* 构造方法 */\n    constructor(edges) {\n        this.adjList = new Map();\n        // 添加所有顶点和边\n        for (const edge of edges) {\n            this.addVertex(edge[0]);\n            this.addVertex(edge[1]);\n            this.addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* 获取顶点数量 */\n    size() {\n        return this.adjList.size;\n    }\n\n    /* 添加边 */\n    addEdge(vet1, vet2) {\n        if (\n            !this.adjList.has(vet1) ||\n            !this.adjList.has(vet2) ||\n            vet1 === vet2\n        ) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // 添加边 vet1 - vet2\n        this.adjList.get(vet1).push(vet2);\n        this.adjList.get(vet2).push(vet1);\n    }\n\n    /* 删除边 */\n    removeEdge(vet1, vet2) {\n        if (\n            !this.adjList.has(vet1) ||\n            !this.adjList.has(vet2) ||\n            vet1 === vet2 ||\n            this.adjList.get(vet1).indexOf(vet2) === -1\n        ) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // 删除边 vet1 - vet2\n        this.adjList.get(vet1).splice(this.adjList.get(vet1).indexOf(vet2), 1);\n        this.adjList.get(vet2).splice(this.adjList.get(vet2).indexOf(vet1), 1);\n    }\n\n    /* 添加顶点 */\n    addVertex(vet) {\n        if (this.adjList.has(vet)) return;\n        // 在邻接表中添加一个新链表\n        this.adjList.set(vet, []);\n    }\n\n    /* 删除顶点 */\n    removeVertex(vet) {\n        if (!this.adjList.has(vet)) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // 在邻接表中删除顶点 vet 对应的链表\n        this.adjList.delete(vet);\n        // 遍历其他顶点的链表,删除所有包含 vet 的边\n        for (const set of this.adjList.values()) {\n            const index = set.indexOf(vet);\n            if (index > -1) {\n                set.splice(index, 1);\n            }\n        }\n    }\n\n    /* 打印邻接表 */\n    print() {\n        console.log('邻接表 =');\n        for (const [key, value] of this.adjList) {\n            const tmp = [];\n            for (const vertex of value) {\n                tmp.push(vertex.val);\n            }\n            console.log(key.val + ': ' + tmp.join());\n        }\n    }\n}\n
graph_adjacency_list.ts
/* 基于邻接表实现的无向图类 */\nclass GraphAdjList {\n    // 邻接表,key:顶点,value:该顶点的所有邻接顶点\n    adjList: Map<Vertex, Vertex[]>;\n\n    /* 构造方法 */\n    constructor(edges: Vertex[][]) {\n        this.adjList = new Map();\n        // 添加所有顶点和边\n        for (const edge of edges) {\n            this.addVertex(edge[0]);\n            this.addVertex(edge[1]);\n            this.addEdge(edge[0], edge[1]);\n        }\n    }\n\n    /* 获取顶点数量 */\n    size(): number {\n        return this.adjList.size;\n    }\n\n    /* 添加边 */\n    addEdge(vet1: Vertex, vet2: Vertex): void {\n        if (\n            !this.adjList.has(vet1) ||\n            !this.adjList.has(vet2) ||\n            vet1 === vet2\n        ) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // 添加边 vet1 - vet2\n        this.adjList.get(vet1).push(vet2);\n        this.adjList.get(vet2).push(vet1);\n    }\n\n    /* 删除边 */\n    removeEdge(vet1: Vertex, vet2: Vertex): void {\n        if (\n            !this.adjList.has(vet1) ||\n            !this.adjList.has(vet2) ||\n            vet1 === vet2 ||\n            this.adjList.get(vet1).indexOf(vet2) === -1\n        ) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // 删除边 vet1 - vet2\n        this.adjList.get(vet1).splice(this.adjList.get(vet1).indexOf(vet2), 1);\n        this.adjList.get(vet2).splice(this.adjList.get(vet2).indexOf(vet1), 1);\n    }\n\n    /* 添加顶点 */\n    addVertex(vet: Vertex): void {\n        if (this.adjList.has(vet)) return;\n        // 在邻接表中添加一个新链表\n        this.adjList.set(vet, []);\n    }\n\n    /* 删除顶点 */\n    removeVertex(vet: Vertex): void {\n        if (!this.adjList.has(vet)) {\n            throw new Error('Illegal Argument Exception');\n        }\n        // 在邻接表中删除顶点 vet 对应的链表\n        this.adjList.delete(vet);\n        // 遍历其他顶点的链表,删除所有包含 vet 的边\n        for (const set of this.adjList.values()) {\n            const index: number = set.indexOf(vet);\n            if (index > -1) {\n                set.splice(index, 1);\n            }\n        }\n    }\n\n    /* 打印邻接表 */\n    print(): void {\n        console.log('邻接表 =');\n        for (const [key, value] of this.adjList.entries()) {\n            const tmp = [];\n            for (const vertex of value) {\n                tmp.push(vertex.val);\n            }\n            console.log(key.val + ': ' + tmp.join());\n        }\n    }\n}\n
graph_adjacency_list.dart
/* 基于邻接表实现的无向图类 */\nclass GraphAdjList {\n  // 邻接表,key:顶点,value:该顶点的所有邻接顶点\n  Map<Vertex, List<Vertex>> adjList = {};\n\n  /* 构造方法 */\n  GraphAdjList(List<List<Vertex>> edges) {\n    for (List<Vertex> edge in edges) {\n      addVertex(edge[0]);\n      addVertex(edge[1]);\n      addEdge(edge[0], edge[1]);\n    }\n  }\n\n  /* 获取顶点数量 */\n  int size() {\n    return adjList.length;\n  }\n\n  /* 添加边 */\n  void addEdge(Vertex vet1, Vertex vet2) {\n    if (!adjList.containsKey(vet1) ||\n        !adjList.containsKey(vet2) ||\n        vet1 == vet2) {\n      throw ArgumentError;\n    }\n    // 添加边 vet1 - vet2\n    adjList[vet1]!.add(vet2);\n    adjList[vet2]!.add(vet1);\n  }\n\n  /* 删除边 */\n  void removeEdge(Vertex vet1, Vertex vet2) {\n    if (!adjList.containsKey(vet1) ||\n        !adjList.containsKey(vet2) ||\n        vet1 == vet2) {\n      throw ArgumentError;\n    }\n    // 删除边 vet1 - vet2\n    adjList[vet1]!.remove(vet2);\n    adjList[vet2]!.remove(vet1);\n  }\n\n  /* 添加顶点 */\n  void addVertex(Vertex vet) {\n    if (adjList.containsKey(vet)) return;\n    // 在邻接表中添加一个新链表\n    adjList[vet] = [];\n  }\n\n  /* 删除顶点 */\n  void removeVertex(Vertex vet) {\n    if (!adjList.containsKey(vet)) {\n      throw ArgumentError;\n    }\n    // 在邻接表中删除顶点 vet 对应的链表\n    adjList.remove(vet);\n    // 遍历其他顶点的链表,删除所有包含 vet 的边\n    adjList.forEach((key, value) {\n      value.remove(vet);\n    });\n  }\n\n  /* 打印邻接表 */\n  void printAdjList() {\n    print(\"邻接表 =\");\n    adjList.forEach((key, value) {\n      List<int> tmp = [];\n      for (Vertex vertex in value) {\n        tmp.add(vertex.val);\n      }\n      print(\"${key.val}: $tmp,\");\n    });\n  }\n}\n
graph_adjacency_list.rs
/* 基于邻接表实现的无向图类型 */\npub struct GraphAdjList {\n    // 邻接表,key:顶点,value:该顶点的所有邻接顶点\n    pub adj_list: HashMap<Vertex, Vec<Vertex>>, // maybe HashSet<Vertex> for value part is better?\n}\n\nimpl GraphAdjList {\n    /* 构造方法 */\n    pub fn new(edges: Vec<[Vertex; 2]>) -> Self {\n        let mut graph = GraphAdjList {\n            adj_list: HashMap::new(),\n        };\n        // 添加所有顶点和边\n        for edge in edges {\n            graph.add_vertex(edge[0]);\n            graph.add_vertex(edge[1]);\n            graph.add_edge(edge[0], edge[1]);\n        }\n\n        graph\n    }\n\n    /* 获取顶点数量 */\n    #[allow(unused)]\n    pub fn size(&self) -> usize {\n        self.adj_list.len()\n    }\n\n    /* 添加边 */\n    pub fn add_edge(&mut self, vet1: Vertex, vet2: Vertex) {\n        if vet1 == vet2 {\n            panic!(\"value error\");\n        }\n        // 添加边 vet1 - vet2\n        self.adj_list.entry(vet1).or_default().push(vet2);\n        self.adj_list.entry(vet2).or_default().push(vet1);\n    }\n\n    /* 删除边 */\n    #[allow(unused)]\n    pub fn remove_edge(&mut self, vet1: Vertex, vet2: Vertex) {\n        if vet1 == vet2 {\n            panic!(\"value error\");\n        }\n        // 删除边 vet1 - vet2\n        self.adj_list\n            .entry(vet1)\n            .and_modify(|v| v.retain(|&e| e != vet2));\n        self.adj_list\n            .entry(vet2)\n            .and_modify(|v| v.retain(|&e| e != vet1));\n    }\n\n    /* 添加顶点 */\n    pub fn add_vertex(&mut self, vet: Vertex) {\n        if self.adj_list.contains_key(&vet) {\n            return;\n        }\n        // 在邻接表中添加一个新链表\n        self.adj_list.insert(vet, vec![]);\n    }\n\n    /* 删除顶点 */\n    #[allow(unused)]\n    pub fn remove_vertex(&mut self, vet: Vertex) {\n        // 在邻接表中删除顶点 vet 对应的链表\n        self.adj_list.remove(&vet);\n        // 遍历其他顶点的链表,删除所有包含 vet 的边\n        for list in self.adj_list.values_mut() {\n            list.retain(|&v| v != vet);\n        }\n    }\n\n    /* 打印邻接表 */\n    pub fn print(&self) {\n        println!(\"邻接表 =\");\n        for (vertex, list) in &self.adj_list {\n            let list = list.iter().map(|vertex| vertex.val).collect::<Vec<i32>>();\n            println!(\"{}: {:?},\", vertex.val, list);\n        }\n    }\n}\n
graph_adjacency_list.c
/* 节点结构体 */\ntypedef struct AdjListNode {\n    Vertex *vertex;           // 顶点\n    struct AdjListNode *next; // 后继节点\n} AdjListNode;\n\n/* 查找顶点对应的节点 */\nAdjListNode *findNode(GraphAdjList *graph, Vertex *vet) {\n    for (int i = 0; i < graph->size; i++) {\n        if (graph->heads[i]->vertex == vet) {\n            return graph->heads[i];\n        }\n    }\n    return NULL;\n}\n\n/* 添加边辅助函数 */\nvoid addEdgeHelper(AdjListNode *head, Vertex *vet) {\n    AdjListNode *node = (AdjListNode *)malloc(sizeof(AdjListNode));\n    node->vertex = vet;\n    // 头插法\n    node->next = head->next;\n    head->next = node;\n}\n\n/* 删除边辅助函数 */\nvoid removeEdgeHelper(AdjListNode *head, Vertex *vet) {\n    AdjListNode *pre = head;\n    AdjListNode *cur = head->next;\n    // 在链表中搜索 vet 对应节点\n    while (cur != NULL && cur->vertex != vet) {\n        pre = cur;\n        cur = cur->next;\n    }\n    if (cur == NULL)\n        return;\n    // 将 vet 对应节点从链表中删除\n    pre->next = cur->next;\n    // 释放内存\n    free(cur);\n}\n\n/* 基于邻接表实现的无向图类 */\ntypedef struct {\n    AdjListNode *heads[MAX_SIZE]; // 节点数组\n    int size;                     // 节点数量\n} GraphAdjList;\n\n/* 构造函数 */\nGraphAdjList *newGraphAdjList() {\n    GraphAdjList *graph = (GraphAdjList *)malloc(sizeof(GraphAdjList));\n    if (!graph) {\n        return NULL;\n    }\n    graph->size = 0;\n    for (int i = 0; i < MAX_SIZE; i++) {\n        graph->heads[i] = NULL;\n    }\n    return graph;\n}\n\n/* 析构函数 */\nvoid delGraphAdjList(GraphAdjList *graph) {\n    for (int i = 0; i < graph->size; i++) {\n        AdjListNode *cur = graph->heads[i];\n        while (cur != NULL) {\n            AdjListNode *next = cur->next;\n            if (cur != graph->heads[i]) {\n                free(cur);\n            }\n            cur = next;\n        }\n        free(graph->heads[i]->vertex);\n        free(graph->heads[i]);\n    }\n    free(graph);\n}\n\n/* 查找顶点对应的节点 */\nAdjListNode *findNode(GraphAdjList *graph, Vertex *vet) {\n    for (int i = 0; i < graph->size; i++) {\n        if (graph->heads[i]->vertex == vet) {\n            return graph->heads[i];\n        }\n    }\n    return NULL;\n}\n\n/* 添加边 */\nvoid addEdge(GraphAdjList *graph, Vertex *vet1, Vertex *vet2) {\n    AdjListNode *head1 = findNode(graph, vet1);\n    AdjListNode *head2 = findNode(graph, vet2);\n    assert(head1 != NULL && head2 != NULL && head1 != head2);\n    // 添加边 vet1 - vet2\n    addEdgeHelper(head1, vet2);\n    addEdgeHelper(head2, vet1);\n}\n\n/* 删除边 */\nvoid removeEdge(GraphAdjList *graph, Vertex *vet1, Vertex *vet2) {\n    AdjListNode *head1 = findNode(graph, vet1);\n    AdjListNode *head2 = findNode(graph, vet2);\n    assert(head1 != NULL && head2 != NULL);\n    // 删除边 vet1 - vet2\n    removeEdgeHelper(head1, head2->vertex);\n    removeEdgeHelper(head2, head1->vertex);\n}\n\n/* 添加顶点 */\nvoid addVertex(GraphAdjList *graph, Vertex *vet) {\n    assert(graph != NULL && graph->size < MAX_SIZE);\n    AdjListNode *head = (AdjListNode *)malloc(sizeof(AdjListNode));\n    head->vertex = vet;\n    head->next = NULL;\n    // 在邻接表中添加一个新链表\n    graph->heads[graph->size++] = head;\n}\n\n/* 删除顶点 */\nvoid removeVertex(GraphAdjList *graph, Vertex *vet) {\n    AdjListNode *node = findNode(graph, vet);\n    assert(node != NULL);\n    // 在邻接表中删除顶点 vet 对应的链表\n    AdjListNode *cur = node, *pre = NULL;\n    while (cur) {\n        pre = cur;\n        cur = cur->next;\n        free(pre);\n    }\n    // 遍历其他顶点的链表,删除所有包含 vet 的边\n    for (int i = 0; i < graph->size; i++) {\n        cur = graph->heads[i];\n        pre = NULL;\n        while (cur) {\n            pre = cur;\n            cur = cur->next;\n            if (cur && cur->vertex == vet) {\n                pre->next = cur->next;\n                free(cur);\n                break;\n            }\n        }\n    }\n    // 将该顶点之后的顶点向前移动,以填补空缺\n    int i;\n    for (i = 0; i < graph->size; i++) {\n        if (graph->heads[i] == node)\n            break;\n    }\n    for (int j = i; j < graph->size - 1; j++) {\n        graph->heads[j] = graph->heads[j + 1];\n    }\n    graph->size--;\n    free(vet);\n}\n
graph_adjacency_list.kt
/* 基于邻接表实现的无向图类 */\nclass GraphAdjList(edges: Array<Array<Vertex?>>) {\n    // 邻接表,key:顶点,value:该顶点的所有邻接顶点\n    val adjList = HashMap<Vertex, MutableList<Vertex>>()\n\n    /* 构造方法 */\n    init {\n        // 添加所有顶点和边\n        for (edge in edges) {\n            addVertex(edge[0]!!)\n            addVertex(edge[1]!!)\n            addEdge(edge[0]!!, edge[1]!!)\n        }\n    }\n\n    /* 获取顶点数量 */\n    fun size(): Int {\n        return adjList.size\n    }\n\n    /* 添加边 */\n    fun addEdge(vet1: Vertex, vet2: Vertex) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw IllegalArgumentException()\n        // 添加边 vet1 - vet2\n        adjList[vet1]?.add(vet2)\n        adjList[vet2]?.add(vet1)\n    }\n\n    /* 删除边 */\n    fun removeEdge(vet1: Vertex, vet2: Vertex) {\n        if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2)\n            throw IllegalArgumentException()\n        // 删除边 vet1 - vet2\n        adjList[vet1]?.remove(vet2)\n        adjList[vet2]?.remove(vet1)\n    }\n\n    /* 添加顶点 */\n    fun addVertex(vet: Vertex) {\n        if (adjList.containsKey(vet))\n            return\n        // 在邻接表中添加一个新链表\n        adjList[vet] = mutableListOf()\n    }\n\n    /* 删除顶点 */\n    fun removeVertex(vet: Vertex) {\n        if (!adjList.containsKey(vet))\n            throw IllegalArgumentException()\n        // 在邻接表中删除顶点 vet 对应的链表\n        adjList.remove(vet)\n        // 遍历其他顶点的链表,删除所有包含 vet 的边\n        for (list in adjList.values) {\n            list.remove(vet)\n        }\n    }\n\n    /* 打印邻接表 */\n    fun print() {\n        println(\"邻接表 =\")\n        for (pair in adjList.entries) {\n            val tmp = mutableListOf<Int>()\n            for (vertex in pair.value) {\n                tmp.add(vertex._val)\n            }\n            println(\"${pair.key._val}: $tmp,\")\n        }\n    }\n}\n
graph_adjacency_list.rb
### 基于邻接表实现的无向图类 ###\nclass GraphAdjList\n  attr_reader :adj_list\n\n  ### 构造方法 ###\n  def initialize(edges)\n    # 邻接表,key:顶点,value:该顶点的所有邻接顶点\n    @adj_list = {}\n    # 添加所有顶点和边\n    for edge in edges\n      add_vertex(edge[0])\n      add_vertex(edge[1])\n      add_edge(edge[0], edge[1])\n    end\n  end\n\n  ### 获取顶点数量 ###\n  def size\n    @adj_list.length\n  end\n\n  ### 添加边 ###\n  def add_edge(vet1, vet2)\n    raise ArgumentError if !@adj_list.include?(vet1) || !@adj_list.include?(vet2)\n\n    @adj_list[vet1] << vet2\n    @adj_list[vet2] << vet1\n  end\n\n  ### 删除边 ###\n  def remove_edge(vet1, vet2)\n    raise ArgumentError if !@adj_list.include?(vet1) || !@adj_list.include?(vet2)\n\n    # 删除边 vet1 - vet2\n    @adj_list[vet1].delete(vet2)\n    @adj_list[vet2].delete(vet1)\n  end\n\n  ### 添加顶点 ###\n  def add_vertex(vet)\n    return if @adj_list.include?(vet)\n\n    # 在邻接表中添加一个新链表\n    @adj_list[vet] = []\n  end\n\n  ### 删除顶点 ###\n  def remove_vertex(vet)\n    raise ArgumentError unless @adj_list.include?(vet)\n\n    # 在邻接表中删除顶点 vet 对应的链表\n    @adj_list.delete(vet)\n    # 遍历其他顶点的链表,删除所有包含 vet 的边\n    for vertex in @adj_list\n      @adj_list[vertex.first].delete(vet) if @adj_list[vertex.first].include?(vet)\n    end\n  end\n\n  ### 打印邻接表 ###\n  def __print__\n    puts '邻接表 ='\n    for vertex in @adj_list\n      tmp = @adj_list[vertex.first].map { |v| v.val }\n      puts \"#{vertex.first.val}: #{tmp},\"\n    end\n  end\nend\n
可视化运行

全屏观看 >

","path":["第 9 章   图","9.2   图的基础操作"],"tags":[]},{"location":"chapter_graph/graph_operations/#923","level":2,"title":"9.2.3   效率对比","text":"

设图中共有 \\(n\\) 个顶点和 \\(m\\) 条边,表 9-2 对比了邻接矩阵和邻接表的时间效率和空间效率。请注意,邻接表(链表)对应本文实现,而邻接表(哈希表)专指将所有链表替换为哈希表后的实现。

表 9-2   邻接矩阵与邻接表对比

邻接矩阵 邻接表(链表) 邻接表(哈希表) 判断是否邻接 \\(O(1)\\) \\(O(n)\\) \\(O(1)\\) 添加边 \\(O(1)\\) \\(O(1)\\) \\(O(1)\\) 删除边 \\(O(1)\\) \\(O(n)\\) \\(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)\\)

观察表 9-2 ,似乎邻接表(哈希表)的时间效率与空间效率最优。但实际上,在邻接矩阵中操作边的效率更高,只需一次数组访问或赋值操作即可。综合来看,邻接矩阵体现了“以空间换时间”的原则,而邻接表体现了“以时间换空间”的原则。

","path":["第 9 章   图","9.2   图的基础操作"],"tags":[]},{"location":"chapter_graph/graph_traversal/","level":1,"title":"9.3   图的遍历","text":"

树代表的是“一对多”的关系,而图则具有更高的自由度,可以表示任意的“多对多”关系。因此,我们可以把树看作图的一种特例。显然,树的遍历操作也是图的遍历操作的一种特例。

图和树都需要应用搜索算法来实现遍历操作。图的遍历方式也可分为两种:广度优先遍历和深度优先遍历。

","path":["第 9 章   图","9.3   图的遍历"],"tags":[]},{"location":"chapter_graph/graph_traversal/#931","level":2,"title":"9.3.1   广度优先遍历","text":"

广度优先遍历是一种由近及远的遍历方式,从某个节点出发,始终优先访问距离最近的顶点,并一层层向外扩张。如图 9-9 所示,从左上角顶点出发,首先遍历该顶点的所有邻接顶点,然后遍历下一个顶点的所有邻接顶点,以此类推,直至所有顶点访问完毕。

图 9-9   图的广度优先遍历

","path":["第 9 章   图","9.3   图的遍历"],"tags":[]},{"location":"chapter_graph/graph_traversal/#1","level":3,"title":"1.   算法实现","text":"

BFS 通常借助队列来实现,代码如下所示。队列具有“先入先出”的性质,这与 BFS 的“由近及远”的思想异曲同工。

  1. 将遍历起始顶点 startVet 加入队列,并开启循环。
  2. 在循环的每轮迭代中,弹出队首顶点并记录访问,然后将该顶点的所有邻接顶点加入到队列尾部。
  3. 循环步骤 2. ,直到所有顶点被访问完毕后结束。

为了防止重复遍历顶点,我们需要借助一个哈希集合 visited 来记录哪些节点已被访问。

Tip

哈希集合可以看作一个只存储 key 而不存储 value 的哈希表,它可以在 \\(O(1)\\) 时间复杂度下进行 key 的增删查改操作。根据 key 的唯一性,哈希集合通常用于数据去重等场景。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_bfs.py
def graph_bfs(graph: GraphAdjList, start_vet: Vertex) -> list[Vertex]:\n    \"\"\"广度优先遍历\"\"\"\n    # 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点\n    # 顶点遍历序列\n    res = []\n    # 哈希集合,用于记录已被访问过的顶点\n    visited = set[Vertex]([start_vet])\n    # 队列用于实现 BFS\n    que = deque[Vertex]([start_vet])\n    # 以顶点 vet 为起点,循环直至访问完所有顶点\n    while len(que) > 0:\n        vet = que.popleft()  # 队首顶点出队\n        res.append(vet)  # 记录访问顶点\n        # 遍历该顶点的所有邻接顶点\n        for adj_vet in graph.adj_list[vet]:\n            if adj_vet in visited:\n                continue  # 跳过已被访问的顶点\n            que.append(adj_vet)  # 只入队未访问的顶点\n            visited.add(adj_vet)  # 标记该顶点已被访问\n    # 返回顶点遍历序列\n    return res\n
graph_bfs.cpp
/* 广度优先遍历 */\n// 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点\nvector<Vertex *> graphBFS(GraphAdjList &graph, Vertex *startVet) {\n    // 顶点遍历序列\n    vector<Vertex *> res;\n    // 哈希集合,用于记录已被访问过的顶点\n    unordered_set<Vertex *> visited = {startVet};\n    // 队列用于实现 BFS\n    queue<Vertex *> que;\n    que.push(startVet);\n    // 以顶点 vet 为起点,循环直至访问完所有顶点\n    while (!que.empty()) {\n        Vertex *vet = que.front();\n        que.pop();          // 队首顶点出队\n        res.push_back(vet); // 记录访问顶点\n        // 遍历该顶点的所有邻接顶点\n        for (auto adjVet : graph.adjList[vet]) {\n            if (visited.count(adjVet))\n                continue;            // 跳过已被访问的顶点\n            que.push(adjVet);        // 只入队未访问的顶点\n            visited.emplace(adjVet); // 标记该顶点已被访问\n        }\n    }\n    // 返回顶点遍历序列\n    return res;\n}\n
graph_bfs.java
/* 广度优先遍历 */\n// 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点\nList<Vertex> graphBFS(GraphAdjList graph, Vertex startVet) {\n    // 顶点遍历序列\n    List<Vertex> res = new ArrayList<>();\n    // 哈希集合,用于记录已被访问过的顶点\n    Set<Vertex> visited = new HashSet<>();\n    visited.add(startVet);\n    // 队列用于实现 BFS\n    Queue<Vertex> que = new LinkedList<>();\n    que.offer(startVet);\n    // 以顶点 vet 为起点,循环直至访问完所有顶点\n    while (!que.isEmpty()) {\n        Vertex vet = que.poll(); // 队首顶点出队\n        res.add(vet);            // 记录访问顶点\n        // 遍历该顶点的所有邻接顶点\n        for (Vertex adjVet : graph.adjList.get(vet)) {\n            if (visited.contains(adjVet))\n                continue;        // 跳过已被访问的顶点\n            que.offer(adjVet);   // 只入队未访问的顶点\n            visited.add(adjVet); // 标记该顶点已被访问\n        }\n    }\n    // 返回顶点遍历序列\n    return res;\n}\n
graph_bfs.cs
/* 广度优先遍历 */\n// 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点\nList<Vertex> GraphBFS(GraphAdjList graph, Vertex startVet) {\n    // 顶点遍历序列\n    List<Vertex> res = [];\n    // 哈希集合,用于记录已被访问过的顶点\n    HashSet<Vertex> visited = [startVet];\n    // 队列用于实现 BFS\n    Queue<Vertex> que = new();\n    que.Enqueue(startVet);\n    // 以顶点 vet 为起点,循环直至访问完所有顶点\n    while (que.Count > 0) {\n        Vertex vet = que.Dequeue(); // 队首顶点出队\n        res.Add(vet);               // 记录访问顶点\n        foreach (Vertex adjVet in graph.adjList[vet]) {\n            if (visited.Contains(adjVet)) {\n                continue;          // 跳过已被访问的顶点\n            }\n            que.Enqueue(adjVet);   // 只入队未访问的顶点\n            visited.Add(adjVet);   // 标记该顶点已被访问\n        }\n    }\n\n    // 返回顶点遍历序列\n    return res;\n}\n
graph_bfs.go
/* 广度优先遍历 */\n// 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点\nfunc graphBFS(g *graphAdjList, startVet Vertex) []Vertex {\n    // 顶点遍历序列\n    res := make([]Vertex, 0)\n    // 哈希集合,用于记录已被访问过的顶点\n    visited := make(map[Vertex]struct{})\n    visited[startVet] = struct{}{}\n    // 队列用于实现 BFS, 使用切片模拟队列\n    queue := make([]Vertex, 0)\n    queue = append(queue, startVet)\n    // 以顶点 vet 为起点,循环直至访问完所有顶点\n    for len(queue) > 0 {\n        // 队首顶点出队\n        vet := queue[0]\n        queue = queue[1:]\n        // 记录访问顶点\n        res = append(res, vet)\n        // 遍历该顶点的所有邻接顶点\n        for _, adjVet := range g.adjList[vet] {\n            _, isExist := visited[adjVet]\n            // 只入队未访问的顶点\n            if !isExist {\n                queue = append(queue, adjVet)\n                visited[adjVet] = struct{}{}\n            }\n        }\n    }\n    // 返回顶点遍历序列\n    return res\n}\n
graph_bfs.swift
/* 广度优先遍历 */\n// 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点\nfunc graphBFS(graph: GraphAdjList, startVet: Vertex) -> [Vertex] {\n    // 顶点遍历序列\n    var res: [Vertex] = []\n    // 哈希集合,用于记录已被访问过的顶点\n    var visited: Set<Vertex> = [startVet]\n    // 队列用于实现 BFS\n    var que: [Vertex] = [startVet]\n    // 以顶点 vet 为起点,循环直至访问完所有顶点\n    while !que.isEmpty {\n        let vet = que.removeFirst() // 队首顶点出队\n        res.append(vet) // 记录访问顶点\n        // 遍历该顶点的所有邻接顶点\n        for adjVet in graph.adjList[vet] ?? [] {\n            if visited.contains(adjVet) {\n                continue // 跳过已被访问的顶点\n            }\n            que.append(adjVet) // 只入队未访问的顶点\n            visited.insert(adjVet) // 标记该顶点已被访问\n        }\n    }\n    // 返回顶点遍历序列\n    return res\n}\n
graph_bfs.js
/* 广度优先遍历 */\n// 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点\nfunction graphBFS(graph, startVet) {\n    // 顶点遍历序列\n    const res = [];\n    // 哈希集合,用于记录已被访问过的顶点\n    const visited = new Set();\n    visited.add(startVet);\n    // 队列用于实现 BFS\n    const que = [startVet];\n    // 以顶点 vet 为起点,循环直至访问完所有顶点\n    while (que.length) {\n        const vet = que.shift(); // 队首顶点出队\n        res.push(vet); // 记录访问顶点\n        // 遍历该顶点的所有邻接顶点\n        for (const adjVet of graph.adjList.get(vet) ?? []) {\n            if (visited.has(adjVet)) {\n                continue; // 跳过已被访问的顶点\n            }\n            que.push(adjVet); // 只入队未访问的顶点\n            visited.add(adjVet); // 标记该顶点已被访问\n        }\n    }\n    // 返回顶点遍历序列\n    return res;\n}\n
graph_bfs.ts
/* 广度优先遍历 */\n// 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点\nfunction graphBFS(graph: GraphAdjList, startVet: Vertex): Vertex[] {\n    // 顶点遍历序列\n    const res: Vertex[] = [];\n    // 哈希集合,用于记录已被访问过的顶点\n    const visited: Set<Vertex> = new Set();\n    visited.add(startVet);\n    // 队列用于实现 BFS\n    const que = [startVet];\n    // 以顶点 vet 为起点,循环直至访问完所有顶点\n    while (que.length) {\n        const vet = que.shift(); // 队首顶点出队\n        res.push(vet); // 记录访问顶点\n        // 遍历该顶点的所有邻接顶点\n        for (const adjVet of graph.adjList.get(vet) ?? []) {\n            if (visited.has(adjVet)) {\n                continue; // 跳过已被访问的顶点\n            }\n            que.push(adjVet); // 只入队未访问\n            visited.add(adjVet); // 标记该顶点已被访问\n        }\n    }\n    // 返回顶点遍历序列\n    return res;\n}\n
graph_bfs.dart
/* 广度优先遍历 */\nList<Vertex> graphBFS(GraphAdjList graph, Vertex startVet) {\n  // 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点\n  // 顶点遍历序列\n  List<Vertex> res = [];\n  // 哈希集合,用于记录已被访问过的顶点\n  Set<Vertex> visited = {};\n  visited.add(startVet);\n  // 队列用于实现 BFS\n  Queue<Vertex> que = Queue();\n  que.add(startVet);\n  // 以顶点 vet 为起点,循环直至访问完所有顶点\n  while (que.isNotEmpty) {\n    Vertex vet = que.removeFirst(); // 队首顶点出队\n    res.add(vet); // 记录访问顶点\n    // 遍历该顶点的所有邻接顶点\n    for (Vertex adjVet in graph.adjList[vet]!) {\n      if (visited.contains(adjVet)) {\n        continue; // 跳过已被访问的顶点\n      }\n      que.add(adjVet); // 只入队未访问的顶点\n      visited.add(adjVet); // 标记该顶点已被访问\n    }\n  }\n  // 返回顶点遍历序列\n  return res;\n}\n
graph_bfs.rs
/* 广度优先遍历 */\n// 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点\nfn graph_bfs(graph: GraphAdjList, start_vet: Vertex) -> Vec<Vertex> {\n    // 顶点遍历序列\n    let mut res = vec![];\n    // 哈希集合,用于记录已被访问过的顶点\n    let mut visited = HashSet::new();\n    visited.insert(start_vet);\n    // 队列用于实现 BFS\n    let mut que = VecDeque::new();\n    que.push_back(start_vet);\n    // 以顶点 vet 为起点,循环直至访问完所有顶点\n    while let Some(vet) = que.pop_front() {\n        res.push(vet); // 记录访问顶点\n\n        // 遍历该顶点的所有邻接顶点\n        if let Some(adj_vets) = graph.adj_list.get(&vet) {\n            for &adj_vet in adj_vets {\n                if visited.contains(&adj_vet) {\n                    continue; // 跳过已被访问的顶点\n                }\n                que.push_back(adj_vet); // 只入队未访问的顶点\n                visited.insert(adj_vet); // 标记该顶点已被访问\n            }\n        }\n    }\n    // 返回顶点遍历序列\n    res\n}\n
graph_bfs.c
/* 节点队列结构体 */\ntypedef struct {\n    Vertex *vertices[MAX_SIZE];\n    int front, rear, size;\n} Queue;\n\n/* 构造函数 */\nQueue *newQueue() {\n    Queue *q = (Queue *)malloc(sizeof(Queue));\n    q->front = q->rear = q->size = 0;\n    return q;\n}\n\n/* 判断队列是否为空 */\nint isEmpty(Queue *q) {\n    return q->size == 0;\n}\n\n/* 入队操作 */\nvoid enqueue(Queue *q, Vertex *vet) {\n    q->vertices[q->rear] = vet;\n    q->rear = (q->rear + 1) % MAX_SIZE;\n    q->size++;\n}\n\n/* 出队操作 */\nVertex *dequeue(Queue *q) {\n    Vertex *vet = q->vertices[q->front];\n    q->front = (q->front + 1) % MAX_SIZE;\n    q->size--;\n    return vet;\n}\n\n/* 检查顶点是否已被访问 */\nint isVisited(Vertex **visited, int size, Vertex *vet) {\n    // 遍历查找节点,使用 O(n) 时间\n    for (int i = 0; i < size; i++) {\n        if (visited[i] == vet)\n            return 1;\n    }\n    return 0;\n}\n\n/* 广度优先遍历 */\n// 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点\nvoid graphBFS(GraphAdjList *graph, Vertex *startVet, Vertex **res, int *resSize, Vertex **visited, int *visitedSize) {\n    // 队列用于实现 BFS\n    Queue *queue = newQueue();\n    enqueue(queue, startVet);\n    visited[(*visitedSize)++] = startVet;\n    // 以顶点 vet 为起点,循环直至访问完所有顶点\n    while (!isEmpty(queue)) {\n        Vertex *vet = dequeue(queue); // 队首顶点出队\n        res[(*resSize)++] = vet;      // 记录访问顶点\n        // 遍历该顶点的所有邻接顶点\n        AdjListNode *node = findNode(graph, vet);\n        while (node != NULL) {\n            // 跳过已被访问的顶点\n            if (!isVisited(visited, *visitedSize, node->vertex)) {\n                enqueue(queue, node->vertex);             // 只入队未访问的顶点\n                visited[(*visitedSize)++] = node->vertex; // 标记该顶点已被访问\n            }\n            node = node->next;\n        }\n    }\n    // 释放内存\n    free(queue);\n}\n
graph_bfs.kt
/* 广度优先遍历 */\n// 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点\nfun graphBFS(graph: GraphAdjList, startVet: Vertex): MutableList<Vertex?> {\n    // 顶点遍历序列\n    val res = mutableListOf<Vertex?>()\n    // 哈希集合,用于记录已被访问过的顶点\n    val visited = HashSet<Vertex>()\n    visited.add(startVet)\n    // 队列用于实现 BFS\n    val que = LinkedList<Vertex>()\n    que.offer(startVet)\n    // 以顶点 vet 为起点,循环直至访问完所有顶点\n    while (!que.isEmpty()) {\n        val vet = que.poll() // 队首顶点出队\n        res.add(vet)         // 记录访问顶点\n        // 遍历该顶点的所有邻接顶点\n        for (adjVet in graph.adjList[vet]!!) {\n            if (visited.contains(adjVet))\n                continue        // 跳过已被访问的顶点\n            que.offer(adjVet)   // 只入队未访问的顶点\n            visited.add(adjVet) // 标记该顶点已被访问\n        }\n    }\n    // 返回顶点遍历序列\n    return res\n}\n
graph_bfs.rb
### 广度优先遍历 ###\ndef graph_bfs(graph, start_vet)\n  # 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点\n  # 顶点遍历序列\n  res = []\n  # 哈希集合,用于记录已被访问过的顶点\n  visited = Set.new([start_vet])\n  # 队列用于实现 BFS\n  que = [start_vet]\n  # 以顶点 vet 为起点,循环直至访问完所有顶点\n  while que.length > 0\n    vet = que.shift # 队首顶点出队\n    res << vet # 记录访问顶点\n    # 遍历该顶点的所有邻接顶点\n    for adj_vet in graph.adj_list[vet]\n      next if visited.include?(adj_vet) # 跳过已被访问的顶点\n      que << adj_vet # 只入队未访问的顶点\n      visited.add(adj_vet) # 标记该顶点已被访问\n    end\n  end\n  # 返回顶点遍历序列\n  res\nend\n
可视化运行

全屏观看 >

代码相对抽象,建议对照图 9-10 来加深理解。

<1><2><3><4><5><6><7><8><9><10><11>

图 9-10   图的广度优先遍历步骤

广度优先遍历的序列是否唯一?

不唯一。广度优先遍历只要求按“由近及远”的顺序遍历,而多个相同距离的顶点的遍历顺序允许被任意打乱。以图 9-10 为例,顶点 \\(1\\)、\\(3\\) 的访问顺序可以交换,顶点 \\(2\\)、\\(4\\)、\\(6\\) 的访问顺序也可以任意交换。

","path":["第 9 章   图","9.3   图的遍历"],"tags":[]},{"location":"chapter_graph/graph_traversal/#2","level":3,"title":"2.   复杂度分析","text":"

时间复杂度:所有顶点都会入队并出队一次,使用 \\(O(|V|)\\) 时间;在遍历邻接顶点的过程中,由于是无向图,因此所有边都会被访问 \\(2\\) 次,使用 \\(O(2|E|)\\) 时间;总体使用 \\(O(|V| + |E|)\\) 时间。

空间复杂度:列表 res ,哈希集合 visited ,队列 que 中的顶点数量最多为 \\(|V|\\) ,使用 \\(O(|V|)\\) 空间。

","path":["第 9 章   图","9.3   图的遍历"],"tags":[]},{"location":"chapter_graph/graph_traversal/#932","level":2,"title":"9.3.2   深度优先遍历","text":"

深度优先遍历是一种优先走到底、无路可走再回头的遍历方式。如图 9-11 所示,从左上角顶点出发,访问当前顶点的某个邻接顶点,直到走到尽头时返回,再继续走到尽头并返回,以此类推,直至所有顶点遍历完成。

图 9-11   图的深度优先遍历

","path":["第 9 章   图","9.3   图的遍历"],"tags":[]},{"location":"chapter_graph/graph_traversal/#1_1","level":3,"title":"1.   算法实现","text":"

这种“走到尽头再返回”的算法范式通常基于递归来实现。与广度优先遍历类似,在深度优先遍历中,我们也需要借助一个哈希集合 visited 来记录已被访问的顶点,以避免重复访问顶点。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby graph_dfs.py
def dfs(graph: GraphAdjList, visited: set[Vertex], res: list[Vertex], vet: Vertex):\n    \"\"\"深度优先遍历辅助函数\"\"\"\n    res.append(vet)  # 记录访问顶点\n    visited.add(vet)  # 标记该顶点已被访问\n    # 遍历该顶点的所有邻接顶点\n    for adjVet in graph.adj_list[vet]:\n        if adjVet in visited:\n            continue  # 跳过已被访问的顶点\n        # 递归访问邻接顶点\n        dfs(graph, visited, res, adjVet)\n\ndef graph_dfs(graph: GraphAdjList, start_vet: Vertex) -> list[Vertex]:\n    \"\"\"深度优先遍历\"\"\"\n    # 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点\n    # 顶点遍历序列\n    res = []\n    # 哈希集合,用于记录已被访问过的顶点\n    visited = set[Vertex]()\n    dfs(graph, visited, res, start_vet)\n    return res\n
graph_dfs.cpp
/* 深度优先遍历辅助函数 */\nvoid dfs(GraphAdjList &graph, unordered_set<Vertex *> &visited, vector<Vertex *> &res, Vertex *vet) {\n    res.push_back(vet);   // 记录访问顶点\n    visited.emplace(vet); // 标记该顶点已被访问\n    // 遍历该顶点的所有邻接顶点\n    for (Vertex *adjVet : graph.adjList[vet]) {\n        if (visited.count(adjVet))\n            continue; // 跳过已被访问的顶点\n        // 递归访问邻接顶点\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* 深度优先遍历 */\n// 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点\nvector<Vertex *> graphDFS(GraphAdjList &graph, Vertex *startVet) {\n    // 顶点遍历序列\n    vector<Vertex *> res;\n    // 哈希集合,用于记录已被访问过的顶点\n    unordered_set<Vertex *> visited;\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
graph_dfs.java
/* 深度优先遍历辅助函数 */\nvoid dfs(GraphAdjList graph, Set<Vertex> visited, List<Vertex> res, Vertex vet) {\n    res.add(vet);     // 记录访问顶点\n    visited.add(vet); // 标记该顶点已被访问\n    // 遍历该顶点的所有邻接顶点\n    for (Vertex adjVet : graph.adjList.get(vet)) {\n        if (visited.contains(adjVet))\n            continue; // 跳过已被访问的顶点\n        // 递归访问邻接顶点\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* 深度优先遍历 */\n// 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点\nList<Vertex> graphDFS(GraphAdjList graph, Vertex startVet) {\n    // 顶点遍历序列\n    List<Vertex> res = new ArrayList<>();\n    // 哈希集合,用于记录已被访问过的顶点\n    Set<Vertex> visited = new HashSet<>();\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
graph_dfs.cs
/* 深度优先遍历辅助函数 */\nvoid DFS(GraphAdjList graph, HashSet<Vertex> visited, List<Vertex> res, Vertex vet) {\n    res.Add(vet);     // 记录访问顶点\n    visited.Add(vet); // 标记该顶点已被访问\n    // 遍历该顶点的所有邻接顶点\n    foreach (Vertex adjVet in graph.adjList[vet]) {\n        if (visited.Contains(adjVet)) {\n            continue; // 跳过已被访问的顶点                             \n        }\n        // 递归访问邻接顶点\n        DFS(graph, visited, res, adjVet);\n    }\n}\n\n/* 深度优先遍历 */\n// 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点\nList<Vertex> GraphDFS(GraphAdjList graph, Vertex startVet) {\n    // 顶点遍历序列\n    List<Vertex> res = [];\n    // 哈希集合,用于记录已被访问过的顶点\n    HashSet<Vertex> visited = [];\n    DFS(graph, visited, res, startVet);\n    return res;\n}\n
graph_dfs.go
/* 深度优先遍历辅助函数 */\nfunc dfs(g *graphAdjList, visited map[Vertex]struct{}, res *[]Vertex, vet Vertex) {\n    // append 操作会返回新的的引用,必须让原引用重新赋值为新slice的引用\n    *res = append(*res, vet)\n    visited[vet] = struct{}{}\n    // 遍历该顶点的所有邻接顶点\n    for _, adjVet := range g.adjList[vet] {\n        _, isExist := visited[adjVet]\n        // 递归访问邻接顶点\n        if !isExist {\n            dfs(g, visited, res, adjVet)\n        }\n    }\n}\n\n/* 深度优先遍历 */\n// 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点\nfunc graphDFS(g *graphAdjList, startVet Vertex) []Vertex {\n    // 顶点遍历序列\n    res := make([]Vertex, 0)\n    // 哈希集合,用于记录已被访问过的顶点\n    visited := make(map[Vertex]struct{})\n    dfs(g, visited, &res, startVet)\n    // 返回顶点遍历序列\n    return res\n}\n
graph_dfs.swift
/* 深度优先遍历辅助函数 */\nfunc dfs(graph: GraphAdjList, visited: inout Set<Vertex>, res: inout [Vertex], vet: Vertex) {\n    res.append(vet) // 记录访问顶点\n    visited.insert(vet) // 标记该顶点已被访问\n    // 遍历该顶点的所有邻接顶点\n    for adjVet in graph.adjList[vet] ?? [] {\n        if visited.contains(adjVet) {\n            continue // 跳过已被访问的顶点\n        }\n        // 递归访问邻接顶点\n        dfs(graph: graph, visited: &visited, res: &res, vet: adjVet)\n    }\n}\n\n/* 深度优先遍历 */\n// 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点\nfunc graphDFS(graph: GraphAdjList, startVet: Vertex) -> [Vertex] {\n    // 顶点遍历序列\n    var res: [Vertex] = []\n    // 哈希集合,用于记录已被访问过的顶点\n    var visited: Set<Vertex> = []\n    dfs(graph: graph, visited: &visited, res: &res, vet: startVet)\n    return res\n}\n
graph_dfs.js
/* 深度优先遍历 */\n// 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点\nfunction dfs(graph, visited, res, vet) {\n    res.push(vet); // 记录访问顶点\n    visited.add(vet); // 标记该顶点已被访问\n    // 遍历该顶点的所有邻接顶点\n    for (const adjVet of graph.adjList.get(vet)) {\n        if (visited.has(adjVet)) {\n            continue; // 跳过已被访问的顶点\n        }\n        // 递归访问邻接顶点\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* 深度优先遍历 */\n// 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点\nfunction graphDFS(graph, startVet) {\n    // 顶点遍历序列\n    const res = [];\n    // 哈希集合,用于记录已被访问过的顶点\n    const visited = new Set();\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
graph_dfs.ts
/* 深度优先遍历辅助函数 */\nfunction dfs(\n    graph: GraphAdjList,\n    visited: Set<Vertex>,\n    res: Vertex[],\n    vet: Vertex\n): void {\n    res.push(vet); // 记录访问顶点\n    visited.add(vet); // 标记该顶点已被访问\n    // 遍历该顶点的所有邻接顶点\n    for (const adjVet of graph.adjList.get(vet)) {\n        if (visited.has(adjVet)) {\n            continue; // 跳过已被访问的顶点\n        }\n        // 递归访问邻接顶点\n        dfs(graph, visited, res, adjVet);\n    }\n}\n\n/* 深度优先遍历 */\n// 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点\nfunction graphDFS(graph: GraphAdjList, startVet: Vertex): Vertex[] {\n    // 顶点遍历序列\n    const res: Vertex[] = [];\n    // 哈希集合,用于记录已被访问过的顶点\n    const visited: Set<Vertex> = new Set();\n    dfs(graph, visited, res, startVet);\n    return res;\n}\n
graph_dfs.dart
/* 深度优先遍历辅助函数 */\nvoid dfs(\n  GraphAdjList graph,\n  Set<Vertex> visited,\n  List<Vertex> res,\n  Vertex vet,\n) {\n  res.add(vet); // 记录访问顶点\n  visited.add(vet); // 标记该顶点已被访问\n  // 遍历该顶点的所有邻接顶点\n  for (Vertex adjVet in graph.adjList[vet]!) {\n    if (visited.contains(adjVet)) {\n      continue; // 跳过已被访问的顶点\n    }\n    // 递归访问邻接顶点\n    dfs(graph, visited, res, adjVet);\n  }\n}\n\n/* 深度优先遍历 */\nList<Vertex> graphDFS(GraphAdjList graph, Vertex startVet) {\n  // 顶点遍历序列\n  List<Vertex> res = [];\n  // 哈希集合,用于记录已被访问过的顶点\n  Set<Vertex> visited = {};\n  dfs(graph, visited, res, startVet);\n  return res;\n}\n
graph_dfs.rs
/* 深度优先遍历辅助函数 */\nfn dfs(graph: &GraphAdjList, visited: &mut HashSet<Vertex>, res: &mut Vec<Vertex>, vet: Vertex) {\n    res.push(vet); // 记录访问顶点\n    visited.insert(vet); // 标记该顶点已被访问\n                         // 遍历该顶点的所有邻接顶点\n    if let Some(adj_vets) = graph.adj_list.get(&vet) {\n        for &adj_vet in adj_vets {\n            if visited.contains(&adj_vet) {\n                continue; // 跳过已被访问的顶点\n            }\n            // 递归访问邻接顶点\n            dfs(graph, visited, res, adj_vet);\n        }\n    }\n}\n\n/* 深度优先遍历 */\n// 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点\nfn graph_dfs(graph: GraphAdjList, start_vet: Vertex) -> Vec<Vertex> {\n    // 顶点遍历序列\n    let mut res = vec![];\n    // 哈希集合,用于记录已被访问过的顶点\n    let mut visited = HashSet::new();\n    dfs(&graph, &mut visited, &mut res, start_vet);\n\n    res\n}\n
graph_dfs.c
/* 检查顶点是否已被访问 */\nint isVisited(Vertex **res, int size, Vertex *vet) {\n    // 遍历查找节点,使用 O(n) 时间\n    for (int i = 0; i < size; i++) {\n        if (res[i] == vet) {\n            return 1;\n        }\n    }\n    return 0;\n}\n\n/* 深度优先遍历辅助函数 */\nvoid dfs(GraphAdjList *graph, Vertex **res, int *resSize, Vertex *vet) {\n    // 记录访问顶点\n    res[(*resSize)++] = vet;\n    // 遍历该顶点的所有邻接顶点\n    AdjListNode *node = findNode(graph, vet);\n    while (node != NULL) {\n        // 跳过已被访问的顶点\n        if (!isVisited(res, *resSize, node->vertex)) {\n            // 递归访问邻接顶点\n            dfs(graph, res, resSize, node->vertex);\n        }\n        node = node->next;\n    }\n}\n\n/* 深度优先遍历 */\n// 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点\nvoid graphDFS(GraphAdjList *graph, Vertex *startVet, Vertex **res, int *resSize) {\n    dfs(graph, res, resSize, startVet);\n}\n
graph_dfs.kt
/* 深度优先遍历辅助函数 */\nfun dfs(\n    graph: GraphAdjList,\n    visited: MutableSet<Vertex?>,\n    res: MutableList<Vertex?>,\n    vet: Vertex?\n) {\n    res.add(vet)     // 记录访问顶点\n    visited.add(vet) // 标记该顶点已被访问\n    // 遍历该顶点的所有邻接顶点\n    for (adjVet in graph.adjList[vet]!!) {\n        if (visited.contains(adjVet))\n            continue  // 跳过已被访问的顶点\n        // 递归访问邻接顶点\n        dfs(graph, visited, res, adjVet)\n    }\n}\n\n/* 深度优先遍历 */\n// 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点\nfun graphDFS(graph: GraphAdjList, startVet: Vertex?): MutableList<Vertex?> {\n    // 顶点遍历序列\n    val res = mutableListOf<Vertex?>()\n    // 哈希集合,用于记录已被访问过的顶点\n    val visited = HashSet<Vertex?>()\n    dfs(graph, visited, res, startVet)\n    return res\n}\n
graph_dfs.rb
### 深度优先遍历辅助函数 ###\ndef dfs(graph, visited, res, vet)\n  res << vet # 记录访问顶点\n  visited.add(vet) # 标记该顶点已被访问\n  # 遍历该顶点的所有邻接顶点\n  for adj_vet in graph.adj_list[vet]\n    next if visited.include?(adj_vet) # 跳过已被访问的顶点\n    # 递归访问邻接顶点\n    dfs(graph, visited, res, adj_vet)\n  end\nend\n\n### 深度优先遍历 ###\ndef graph_dfs(graph, start_vet)\n  # 使用邻接表来表示图,以便获取指定顶点的所有邻接顶点\n  # 顶点遍历序列\n  res = []\n  # 哈希集合,用于记录已被访问过的顶点\n  visited = Set.new\n  dfs(graph, visited, res, start_vet)\n  res\nend\n
可视化运行

全屏观看 >

深度优先遍历的算法流程如图 9-12 所示。

为了加深理解,建议将图 9-12 与代码结合起来,在脑中模拟(或者用笔画下来)整个 DFS 过程,包括每个递归方法何时开启、何时返回。

<1><2><3><4><5><6><7><8><9><10><11>

图 9-12   图的深度优先遍历步骤

深度优先遍历的序列是否唯一?

与广度优先遍历类似,深度优先遍历序列的顺序也不是唯一的。给定某顶点,先往哪个方向探索都可以,即邻接顶点的顺序可以任意打乱,都是深度优先遍历。

以树的遍历为例,“根 \\(\\rightarrow\\) 左 \\(\\rightarrow\\) 右”“左 \\(\\rightarrow\\) 根 \\(\\rightarrow\\) 右”“左 \\(\\rightarrow\\) 右 \\(\\rightarrow\\) 根”分别对应前序、中序、后序遍历,它们展示了三种遍历优先级,然而这三者都属于深度优先遍历。

","path":["第 9 章   图","9.3   图的遍历"],"tags":[]},{"location":"chapter_graph/graph_traversal/#2_1","level":3,"title":"2.   复杂度分析","text":"

时间复杂度:所有顶点都会被访问 \\(1\\) 次,使用 \\(O(|V|)\\) 时间;所有边都会被访问 \\(2\\) 次,使用 \\(O(2|E|)\\) 时间;总体使用 \\(O(|V| + |E|)\\) 时间。

空间复杂度:列表 res ,哈希集合 visited 顶点数量最多为 \\(|V|\\) ,递归深度最大为 \\(|V|\\) ,因此使用 \\(O(|V|)\\) 空间。

","path":["第 9 章   图","9.3   图的遍历"],"tags":[]},{"location":"chapter_graph/summary/","level":1,"title":"9.4   小结","text":"","path":["第 9 章   图","9.4   小结"],"tags":[]},{"location":"chapter_graph/summary/#1","level":3,"title":"1.   重点回顾","text":"","path":["第 9 章   图","9.4   小结"],"tags":[]},{"location":"chapter_graph/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q:路径的定义是顶点序列还是边序列?

维基百科上不同语言版本的定义不一致:英文版是“路径是一个边序列”,而中文版是“路径是一个顶点序列”。以下是英文版原文:In graph theory, a path in a graph is a finite or infinite sequence of edges which joins a sequence of vertices.

在本文中,路径被视为一个边序列,而不是一个顶点序列。这是因为两个顶点之间可能存在多条边连接,此时每条边都对应一条路径。

Q:非连通图中是否会有无法遍历到的点?

在非连通图中,从某个顶点出发,至少有一个顶点无法到达。遍历非连通图需要设置多个起点,以遍历到图的所有连通分量。

Q:在邻接表中,“与该顶点相连的所有顶点”的顶点顺序是否有要求?

可以是任意顺序。但在实际应用中,可能需要按照指定规则来排序,比如按照顶点添加的次序,或者按照顶点值大小的顺序等,这样有助于快速查找“带有某种极值”的顶点。

","path":["第 9 章   图","9.4   小结"],"tags":[]},{"location":"chapter_greedy/","level":1,"title":"第 15 章   贪心","text":"

Abstract

向日葵朝着太阳转动,时刻追求自身成长的最大可能。

贪心策略在一轮轮的简单选择中,逐步导向最佳答案。

","path":["第 15 章   贪心"],"tags":[]},{"location":"chapter_greedy/#_1","level":2,"title":"本章内容","text":"","path":["第 15 章   贪心"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/","level":1,"title":"15.2   分数背包问题","text":"

Question

给定 \\(n\\) 个物品,第 \\(i\\) 个物品的重量为 \\(wgt[i-1]\\)、价值为 \\(val[i-1]\\) ,和一个容量为 \\(cap\\) 的背包。每个物品只能选择一次,但可以选择物品的一部分,价值根据选择的重量比例计算,问在限定背包容量下背包中物品的最大价值。示例如图 15-3 所示。

图 15-3   分数背包问题的示例数据

分数背包问题和 0-1 背包问题整体上非常相似,状态包含当前物品 \\(i\\) 和容量 \\(c\\) ,目标是求限定背包容量下的最大价值。

不同点在于,本题允许只选择物品的一部分。如图 15-4 所示,我们可以对物品任意地进行切分,并按照重量比例来计算相应价值。

  1. 对于物品 \\(i\\) ,它在单位重量下的价值为 \\(val[i-1] / wgt[i-1]\\) ,简称单位价值。
  2. 假设放入一部分物品 \\(i\\) ,重量为 \\(w\\) ,则背包增加的价值为 \\(w \\times val[i-1] / wgt[i-1]\\) 。

图 15-4   物品在单位重量下的价值

","path":["第 15 章   贪心","15.2   分数背包问题"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/#1","level":3,"title":"1.   贪心策略确定","text":"

最大化背包内物品总价值,本质上是最大化单位重量下的物品价值。由此便可推理出图 15-5 所示的贪心策略。

  1. 将物品按照单位价值从高到低进行排序。
  2. 遍历所有物品,每轮贪心地选择单位价值最高的物品。
  3. 若剩余背包容量不足,则使用当前物品的一部分填满背包。

图 15-5   分数背包问题的贪心策略

","path":["第 15 章   贪心","15.2   分数背包问题"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/#2","level":3,"title":"2.   代码实现","text":"

我们建立了一个物品类 Item ,以便将物品按照单位价值进行排序。循环进行贪心选择,当背包已满时跳出并返回解:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby fractional_knapsack.py
class Item:\n    \"\"\"物品\"\"\"\n\n    def __init__(self, w: int, v: int):\n        self.w = w  # 物品重量\n        self.v = v  # 物品价值\n\ndef fractional_knapsack(wgt: list[int], val: list[int], cap: int) -> int:\n    \"\"\"分数背包:贪心\"\"\"\n    # 创建物品列表,包含两个属性:重量、价值\n    items = [Item(w, v) for w, v in zip(wgt, val)]\n    # 按照单位价值 item.v / item.w 从高到低进行排序\n    items.sort(key=lambda item: item.v / item.w, reverse=True)\n    # 循环贪心选择\n    res = 0\n    for item in items:\n        if item.w <= cap:\n            # 若剩余容量充足,则将当前物品整个装进背包\n            res += item.v\n            cap -= item.w\n        else:\n            # 若剩余容量不足,则将当前物品的一部分装进背包\n            res += (item.v / item.w) * cap\n            # 已无剩余容量,因此跳出循环\n            break\n    return res\n
fractional_knapsack.cpp
/* 物品 */\nclass Item {\n  public:\n    int w; // 物品重量\n    int v; // 物品价值\n\n    Item(int w, int v) : w(w), v(v) {\n    }\n};\n\n/* 分数背包:贪心 */\ndouble fractionalKnapsack(vector<int> &wgt, vector<int> &val, int cap) {\n    // 创建物品列表,包含两个属性:重量、价值\n    vector<Item> items;\n    for (int i = 0; i < wgt.size(); i++) {\n        items.push_back(Item(wgt[i], val[i]));\n    }\n    // 按照单位价值 item.v / item.w 从高到低进行排序\n    sort(items.begin(), items.end(), [](Item &a, Item &b) { return (double)a.v / a.w > (double)b.v / b.w; });\n    // 循环贪心选择\n    double res = 0;\n    for (auto &item : items) {\n        if (item.w <= cap) {\n            // 若剩余容量充足,则将当前物品整个装进背包\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // 若剩余容量不足,则将当前物品的一部分装进背包\n            res += (double)item.v / item.w * cap;\n            // 已无剩余容量,因此跳出循环\n            break;\n        }\n    }\n    return res;\n}\n
fractional_knapsack.java
/* 物品 */\nclass Item {\n    int w; // 物品重量\n    int v; // 物品价值\n\n    public Item(int w, int v) {\n        this.w = w;\n        this.v = v;\n    }\n}\n\n/* 分数背包:贪心 */\ndouble fractionalKnapsack(int[] wgt, int[] val, int cap) {\n    // 创建物品列表,包含两个属性:重量、价值\n    Item[] items = new Item[wgt.length];\n    for (int i = 0; i < wgt.length; i++) {\n        items[i] = new Item(wgt[i], val[i]);\n    }\n    // 按照单位价值 item.v / item.w 从高到低进行排序\n    Arrays.sort(items, Comparator.comparingDouble(item -> -((double) item.v / item.w)));\n    // 循环贪心选择\n    double res = 0;\n    for (Item item : items) {\n        if (item.w <= cap) {\n            // 若剩余容量充足,则将当前物品整个装进背包\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // 若剩余容量不足,则将当前物品的一部分装进背包\n            res += (double) item.v / item.w * cap;\n            // 已无剩余容量,因此跳出循环\n            break;\n        }\n    }\n    return res;\n}\n
fractional_knapsack.cs
/* 物品 */\nclass Item(int w, int v) {\n    public int w = w; // 物品重量\n    public int v = v; // 物品价值\n}\n\n/* 分数背包:贪心 */\ndouble FractionalKnapsack(int[] wgt, int[] val, int cap) {\n    // 创建物品列表,包含两个属性:重量、价值\n    Item[] items = new Item[wgt.Length];\n    for (int i = 0; i < wgt.Length; i++) {\n        items[i] = new Item(wgt[i], val[i]);\n    }\n    // 按照单位价值 item.v / item.w 从高到低进行排序\n    Array.Sort(items, (x, y) => (y.v / y.w).CompareTo(x.v / x.w));\n    // 循环贪心选择\n    double res = 0;\n    foreach (Item item in items) {\n        if (item.w <= cap) {\n            // 若剩余容量充足,则将当前物品整个装进背包\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // 若剩余容量不足,则将当前物品的一部分装进背包\n            res += (double)item.v / item.w * cap;\n            // 已无剩余容量,因此跳出循环\n            break;\n        }\n    }\n    return res;\n}\n
fractional_knapsack.go
/* 物品 */\ntype Item struct {\n    w int // 物品重量\n    v int // 物品价值\n}\n\n/* 分数背包:贪心 */\nfunc fractionalKnapsack(wgt []int, val []int, cap int) float64 {\n    // 创建物品列表,包含两个属性:重量、价值\n    items := make([]Item, len(wgt))\n    for i := 0; i < len(wgt); i++ {\n        items[i] = Item{wgt[i], val[i]}\n    }\n    // 按照单位价值 item.v / item.w 从高到低进行排序\n    sort.Slice(items, func(i, j int) bool {\n        return float64(items[i].v)/float64(items[i].w) > float64(items[j].v)/float64(items[j].w)\n    })\n    // 循环贪心选择\n    res := 0.0\n    for _, item := range items {\n        if item.w <= cap {\n            // 若剩余容量充足,则将当前物品整个装进背包\n            res += float64(item.v)\n            cap -= item.w\n        } else {\n            // 若剩余容量不足,则将当前物品的一部分装进背包\n            res += float64(item.v) / float64(item.w) * float64(cap)\n            // 已无剩余容量,因此跳出循环\n            break\n        }\n    }\n    return res\n}\n
fractional_knapsack.swift
/* 物品 */\nclass Item {\n    var w: Int // 物品重量\n    var v: Int // 物品价值\n\n    init(w: Int, v: Int) {\n        self.w = w\n        self.v = v\n    }\n}\n\n/* 分数背包:贪心 */\nfunc fractionalKnapsack(wgt: [Int], val: [Int], cap: Int) -> Double {\n    // 创建物品列表,包含两个属性:重量、价值\n    var items = zip(wgt, val).map { Item(w: $0, v: $1) }\n    // 按照单位价值 item.v / item.w 从高到低进行排序\n    items.sort { -(Double($0.v) / Double($0.w)) < -(Double($1.v) / Double($1.w)) }\n    // 循环贪心选择\n    var res = 0.0\n    var cap = cap\n    for item in items {\n        if item.w <= cap {\n            // 若剩余容量充足,则将当前物品整个装进背包\n            res += Double(item.v)\n            cap -= item.w\n        } else {\n            // 若剩余容量不足,则将当前物品的一部分装进背包\n            res += Double(item.v) / Double(item.w) * Double(cap)\n            // 已无剩余容量,因此跳出循环\n            break\n        }\n    }\n    return res\n}\n
fractional_knapsack.js
/* 物品 */\nclass Item {\n    constructor(w, v) {\n        this.w = w; // 物品重量\n        this.v = v; // 物品价值\n    }\n}\n\n/* 分数背包:贪心 */\nfunction fractionalKnapsack(wgt, val, cap) {\n    // 创建物品列表,包含两个属性:重量、价值\n    const items = wgt.map((w, i) => new Item(w, val[i]));\n    // 按照单位价值 item.v / item.w 从高到低进行排序\n    items.sort((a, b) => b.v / b.w - a.v / a.w);\n    // 循环贪心选择\n    let res = 0;\n    for (const item of items) {\n        if (item.w <= cap) {\n            // 若剩余容量充足,则将当前物品整个装进背包\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // 若剩余容量不足,则将当前物品的一部分装进背包\n            res += (item.v / item.w) * cap;\n            // 已无剩余容量,因此跳出循环\n            break;\n        }\n    }\n    return res;\n}\n
fractional_knapsack.ts
/* 物品 */\nclass Item {\n    w: number; // 物品重量\n    v: number; // 物品价值\n\n    constructor(w: number, v: number) {\n        this.w = w;\n        this.v = v;\n    }\n}\n\n/* 分数背包:贪心 */\nfunction fractionalKnapsack(wgt: number[], val: number[], cap: number): number {\n    // 创建物品列表,包含两个属性:重量、价值\n    const items: Item[] = wgt.map((w, i) => new Item(w, val[i]));\n    // 按照单位价值 item.v / item.w 从高到低进行排序\n    items.sort((a, b) => b.v / b.w - a.v / a.w);\n    // 循环贪心选择\n    let res = 0;\n    for (const item of items) {\n        if (item.w <= cap) {\n            // 若剩余容量充足,则将当前物品整个装进背包\n            res += item.v;\n            cap -= item.w;\n        } else {\n            // 若剩余容量不足,则将当前物品的一部分装进背包\n            res += (item.v / item.w) * cap;\n            // 已无剩余容量,因此跳出循环\n            break;\n        }\n    }\n    return res;\n}\n
fractional_knapsack.dart
/* 物品 */\nclass Item {\n  int w; // 物品重量\n  int v; // 物品价值\n\n  Item(this.w, this.v);\n}\n\n/* 分数背包:贪心 */\ndouble fractionalKnapsack(List<int> wgt, List<int> val, int cap) {\n  // 创建物品列表,包含两个属性:重量、价值\n  List<Item> items = List.generate(wgt.length, (i) => Item(wgt[i], val[i]));\n  // 按照单位价值 item.v / item.w 从高到低进行排序\n  items.sort((a, b) => (b.v / b.w).compareTo(a.v / a.w));\n  // 循环贪心选择\n  double res = 0;\n  for (Item item in items) {\n    if (item.w <= cap) {\n      // 若剩余容量充足,则将当前物品整个装进背包\n      res += item.v;\n      cap -= item.w;\n    } else {\n      // 若剩余容量不足,则将当前物品的一部分装进背包\n      res += item.v / item.w * cap;\n      // 已无剩余容量,因此跳出循环\n      break;\n    }\n  }\n  return res;\n}\n
fractional_knapsack.rs
/* 物品 */\nstruct Item {\n    w: i32, // 物品重量\n    v: i32, // 物品价值\n}\n\nimpl Item {\n    fn new(w: i32, v: i32) -> Self {\n        Self { w, v }\n    }\n}\n\n/* 分数背包:贪心 */\nfn fractional_knapsack(wgt: &[i32], val: &[i32], mut cap: i32) -> f64 {\n    // 创建物品列表,包含两个属性:重量、价值\n    let mut items = wgt\n        .iter()\n        .zip(val.iter())\n        .map(|(&w, &v)| Item::new(w, v))\n        .collect::<Vec<Item>>();\n    // 按照单位价值 item.v / item.w 从高到低进行排序\n    items.sort_by(|a, b| {\n        (b.v as f64 / b.w as f64)\n            .partial_cmp(&(a.v as f64 / a.w as f64))\n            .unwrap()\n    });\n    // 循环贪心选择\n    let mut res = 0.0;\n    for item in &items {\n        if item.w <= cap {\n            // 若剩余容量充足,则将当前物品整个装进背包\n            res += item.v as f64;\n            cap -= item.w;\n        } else {\n            // 若剩余容量不足,则将当前物品的一部分装进背包\n            res += item.v as f64 / item.w as f64 * cap as f64;\n            // 已无剩余容量,因此跳出循环\n            break;\n        }\n    }\n    res\n}\n
fractional_knapsack.c
/* 物品 */\ntypedef struct {\n    int w; // 物品重量\n    int v; // 物品价值\n} Item;\n\n/* 分数背包:贪心 */\nfloat fractionalKnapsack(int wgt[], int val[], int itemCount, int cap) {\n    // 创建物品列表,包含两个属性:重量、价值\n    Item *items = malloc(sizeof(Item) * itemCount);\n    for (int i = 0; i < itemCount; i++) {\n        items[i] = (Item){.w = wgt[i], .v = val[i]};\n    }\n    // 按照单位价值 item.v / item.w 从高到低进行排序\n    qsort(items, (size_t)itemCount, sizeof(Item), sortByValueDensity);\n    // 循环贪心选择\n    float res = 0.0;\n    for (int i = 0; i < itemCount; i++) {\n        if (items[i].w <= cap) {\n            // 若剩余容量充足,则将当前物品整个装进背包\n            res += items[i].v;\n            cap -= items[i].w;\n        } else {\n            // 若剩余容量不足,则将当前物品的一部分装进背包\n            res += (float)cap / items[i].w * items[i].v;\n            cap = 0;\n            break;\n        }\n    }\n    free(items);\n    return res;\n}\n
fractional_knapsack.kt
/* 物品 */\nclass Item(\n    val w: Int, // 物品\n    val v: Int  // 物品价值\n)\n\n/* 分数背包:贪心 */\nfun fractionalKnapsack(wgt: IntArray, _val: IntArray, c: Int): Double {\n    // 创建物品列表,包含两个属性:重量、价值\n    var cap = c\n    val items = arrayOfNulls<Item>(wgt.size)\n    for (i in wgt.indices) {\n        items[i] = Item(wgt[i], _val[i])\n    }\n    // 按照单位价值 item.v / item.w 从高到低进行排序\n    items.sortBy { item: Item? -> -(item!!.v.toDouble() / item.w) }\n    // 循环贪心选择\n    var res = 0.0\n    for (item in items) {\n        if (item!!.w <= cap) {\n            // 若剩余容量充足,则将当前物品整个装进背包\n            res += item.v\n            cap -= item.w\n        } else {\n            // 若剩余容量不足,则将当前物品的一部分装进背包\n            res += item.v.toDouble() / item.w * cap\n            // 已无剩余容量,因此跳出循环\n            break\n        }\n    }\n    return res\n}\n
fractional_knapsack.rb
### 物品 ###\nclass Item\n  attr_accessor :w # 物品重量\n  attr_accessor :v # 物品价值\n\n  def initialize(w, v)\n    @w = w\n    @v = v\n  end\nend\n\n### 分数背包:贪心 ###\ndef fractional_knapsack(wgt, val, cap)\n  # 创建物品列表,包含两个属性:重量,价值\n  items = wgt.each_with_index.map { |w, i| Item.new(w, val[i]) }\n  # 按照单位价值 item.v / item.w 从高到低进行排序\n  items.sort! { |a, b| (b.v.to_f / b.w) <=> (a.v.to_f / a.w) }\n  # 循环贪心选择\n  res = 0\n  for item in items\n    if item.w <= cap\n      # 若剩余容量充足,则将当前物品整个装进背包\n      res += item.v\n      cap -= item.w\n    else\n      # 若剩余容量不足,则将当前物品的一部分装进背包\n      res += (item.v.to_f / item.w) * cap\n      # 已无剩余容量,因此跳出循环\n      break\n    end\n  end\n  res\nend\n
可视化运行

全屏观看 >

内置排序算法的时间复杂度通常为 \\(O(\\log n)\\) ,空间复杂度通常为 \\(O(\\log n)\\) 或 \\(O(n)\\) ,取决于编程语言的具体实现。

除排序之外,在最差情况下,需要遍历整个物品列表,因此时间复杂度为 \\(O(n)\\) ,其中 \\(n\\) 为物品数量。

由于初始化了一个 Item 对象列表,因此空间复杂度为 \\(O(n)\\) 。

","path":["第 15 章   贪心","15.2   分数背包问题"],"tags":[]},{"location":"chapter_greedy/fractional_knapsack_problem/#3","level":3,"title":"3.   正确性证明","text":"

采用反证法。假设物品 \\(x\\) 是单位价值最高的物品,使用某算法求得最大价值为 res ,但该解中不包含物品 \\(x\\) 。

现在从背包中拿出单位重量的任意物品,并替换为单位重量的物品 \\(x\\) 。由于物品 \\(x\\) 的单位价值最高,因此替换后的总价值一定大于 res 。这与 res 是最优解矛盾,说明最优解中必须包含物品 \\(x\\) 。

对于该解中的其他物品,我们也可以构建出上述矛盾。总而言之,单位价值更大的物品总是更优选择,这说明贪心策略是有效的。

如图 15-6 所示,如果将物品重量和物品单位价值分别看作一张二维图表的横轴和纵轴,则分数背包问题可转化为“求在有限横轴区间下围成的最大面积”。这个类比可以帮助我们从几何角度理解贪心策略的有效性。

图 15-6   分数背包问题的几何表示

","path":["第 15 章   贪心","15.2   分数背包问题"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/","level":1,"title":"15.1   贪心算法","text":"

贪心算法(greedy algorithm)是一种常见的解决优化问题的算法,其基本思想是在问题的每个决策阶段,都选择当前看起来最优的选择,即贪心地做出局部最优的决策,以期获得全局最优解。贪心算法简洁且高效,在许多实际问题中有着广泛的应用。

贪心算法和动态规划都常用于解决优化问题。它们之间存在一些相似之处,比如都依赖最优子结构性质,但工作原理不同。

我们先通过例题“零钱兑换”了解贪心算法的工作原理。这道题已经在“完全背包问题”章节中介绍过,相信你对它并不陌生。

Question

给定 \\(n\\) 种硬币,第 \\(i\\) 种硬币的面值为 \\(coins[i - 1]\\) ,目标金额为 \\(amt\\) ,每种硬币可以重复选取,问能够凑出目标金额的最少硬币数量。如果无法凑出目标金额,则返回 \\(-1\\) 。

本题采取的贪心策略如图 15-1 所示。给定目标金额,我们贪心地选择不大于且最接近它的硬币,不断循环该步骤,直至凑出目标金额为止。

图 15-1   零钱兑换的贪心策略

实现代码如下所示:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby coin_change_greedy.py
def coin_change_greedy(coins: list[int], amt: int) -> int:\n    \"\"\"零钱兑换:贪心\"\"\"\n    # 假设 coins 列表有序\n    i = len(coins) - 1\n    count = 0\n    # 循环进行贪心选择,直到无剩余金额\n    while amt > 0:\n        # 找到小于且最接近剩余金额的硬币\n        while i > 0 and coins[i] > amt:\n            i -= 1\n        # 选择 coins[i]\n        amt -= coins[i]\n        count += 1\n    # 若未找到可行方案,则返回 -1\n    return count if amt == 0 else -1\n
coin_change_greedy.cpp
/* 零钱兑换:贪心 */\nint coinChangeGreedy(vector<int> &coins, int amt) {\n    // 假设 coins 列表有序\n    int i = coins.size() - 1;\n    int count = 0;\n    // 循环进行贪心选择,直到无剩余金额\n    while (amt > 0) {\n        // 找到小于且最接近剩余金额的硬币\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // 选择 coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // 若未找到可行方案,则返回 -1\n    return amt == 0 ? count : -1;\n}\n
coin_change_greedy.java
/* 零钱兑换:贪心 */\nint coinChangeGreedy(int[] coins, int amt) {\n    // 假设 coins 列表有序\n    int i = coins.length - 1;\n    int count = 0;\n    // 循环进行贪心选择,直到无剩余金额\n    while (amt > 0) {\n        // 找到小于且最接近剩余金额的硬币\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // 选择 coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // 若未找到可行方案,则返回 -1\n    return amt == 0 ? count : -1;\n}\n
coin_change_greedy.cs
/* 零钱兑换:贪心 */\nint CoinChangeGreedy(int[] coins, int amt) {\n    // 假设 coins 列表有序\n    int i = coins.Length - 1;\n    int count = 0;\n    // 循环进行贪心选择,直到无剩余金额\n    while (amt > 0) {\n        // 找到小于且最接近剩余金额的硬币\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // 选择 coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // 若未找到可行方案,则返回 -1\n    return amt == 0 ? count : -1;\n}\n
coin_change_greedy.go
/* 零钱兑换:贪心 */\nfunc coinChangeGreedy(coins []int, amt int) int {\n    // 假设 coins 列表有序\n    i := len(coins) - 1\n    count := 0\n    // 循环进行贪心选择,直到无剩余金额\n    for amt > 0 {\n        // 找到小于且最接近剩余金额的硬币\n        for i > 0 && coins[i] > amt {\n            i--\n        }\n        // 选择 coins[i]\n        amt -= coins[i]\n        count++\n    }\n    // 若未找到可行方案,则返回 -1\n    if amt != 0 {\n        return -1\n    }\n    return count\n}\n
coin_change_greedy.swift
/* 零钱兑换:贪心 */\nfunc coinChangeGreedy(coins: [Int], amt: Int) -> Int {\n    // 假设 coins 列表有序\n    var i = coins.count - 1\n    var count = 0\n    var amt = amt\n    // 循环进行贪心选择,直到无剩余金额\n    while amt > 0 {\n        // 找到小于且最接近剩余金额的硬币\n        while i > 0 && coins[i] > amt {\n            i -= 1\n        }\n        // 选择 coins[i]\n        amt -= coins[i]\n        count += 1\n    }\n    // 若未找到可行方案,则返回 -1\n    return amt == 0 ? count : -1\n}\n
coin_change_greedy.js
/* 零钱兑换:贪心 */\nfunction coinChangeGreedy(coins, amt) {\n    // 假设 coins 数组有序\n    let i = coins.length - 1;\n    let count = 0;\n    // 循环进行贪心选择,直到无剩余金额\n    while (amt > 0) {\n        // 找到小于且最接近剩余金额的硬币\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // 选择 coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // 若未找到可行方案,则返回 -1\n    return amt === 0 ? count : -1;\n}\n
coin_change_greedy.ts
/* 零钱兑换:贪心 */\nfunction coinChangeGreedy(coins: number[], amt: number): number {\n    // 假设 coins 数组有序\n    let i = coins.length - 1;\n    let count = 0;\n    // 循环进行贪心选择,直到无剩余金额\n    while (amt > 0) {\n        // 找到小于且最接近剩余金额的硬币\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // 选择 coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // 若未找到可行方案,则返回 -1\n    return amt === 0 ? count : -1;\n}\n
coin_change_greedy.dart
/* 零钱兑换:贪心 */\nint coinChangeGreedy(List<int> coins, int amt) {\n  // 假设 coins 列表有序\n  int i = coins.length - 1;\n  int count = 0;\n  // 循环进行贪心选择,直到无剩余金额\n  while (amt > 0) {\n    // 找到小于且最接近剩余金额的硬币\n    while (i > 0 && coins[i] > amt) {\n      i--;\n    }\n    // 选择 coins[i]\n    amt -= coins[i];\n    count++;\n  }\n  // 若未找到可行方案,则返回 -1\n  return amt == 0 ? count : -1;\n}\n
coin_change_greedy.rs
/* 零钱兑换:贪心 */\nfn coin_change_greedy(coins: &[i32], mut amt: i32) -> i32 {\n    // 假设 coins 列表有序\n    let mut i = coins.len() - 1;\n    let mut count = 0;\n    // 循环进行贪心选择,直到无剩余金额\n    while amt > 0 {\n        // 找到小于且最接近剩余金额的硬币\n        while i > 0 && coins[i] > amt {\n            i -= 1;\n        }\n        // 选择 coins[i]\n        amt -= coins[i];\n        count += 1;\n    }\n    // 若未找到可行方案,则返回 -1\n    if amt == 0 {\n        count\n    } else {\n        -1\n    }\n}\n
coin_change_greedy.c
/* 零钱兑换:贪心 */\nint coinChangeGreedy(int *coins, int size, int amt) {\n    // 假设 coins 列表有序\n    int i = size - 1;\n    int count = 0;\n    // 循环进行贪心选择,直到无剩余金额\n    while (amt > 0) {\n        // 找到小于且最接近剩余金额的硬币\n        while (i > 0 && coins[i] > amt) {\n            i--;\n        }\n        // 选择 coins[i]\n        amt -= coins[i];\n        count++;\n    }\n    // 若未找到可行方案,则返回 -1\n    return amt == 0 ? count : -1;\n}\n
coin_change_greedy.kt
/* 零钱兑换:贪心 */\nfun coinChangeGreedy(coins: IntArray, amt: Int): Int {\n    // 假设 coins 列表有序\n    var am = amt\n    var i = coins.size - 1\n    var count = 0\n    // 循环进行贪心选择,直到无剩余金额\n    while (am > 0) {\n        // 找到小于且最接近剩余金额的硬币\n        while (i > 0 && coins[i] > am) {\n            i--\n        }\n        // 选择 coins[i]\n        am -= coins[i]\n        count++\n    }\n    // 若未找到可行方案,则返回 -1\n    return if (am == 0) count else -1\n}\n
coin_change_greedy.rb
### 零钱兑换:贪心 ###\ndef coin_change_greedy(coins, amt)\n  # 假设 coins 列表有序\n  i = coins.length - 1\n  count = 0\n  # 循环进行贪心选择,直到无剩余金额\n  while amt > 0\n    # 找到小于且最接近剩余金额的硬币\n    while i > 0 && coins[i] > amt\n      i -= 1\n    end\n    # 选择 coins[i]\n    amt -= coins[i]\n    count += 1\n  end\n  # 若未找到可行方案, 则返回 -1\n  amt == 0 ? count : -1\nend\n
可视化运行

全屏观看 >

你可能会不由地发出感叹:So clean !贪心算法仅用约十行代码就解决了零钱兑换问题。

","path":["第 15 章   贪心","15.1   贪心算法"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1511","level":2,"title":"15.1.1   贪心算法的优点与局限性","text":"

贪心算法不仅操作直接、实现简单,而且通常效率也很高。在以上代码中,记硬币最小面值为 \\(\\min(coins)\\) ,则贪心选择最多循环 \\(amt / \\min(coins)\\) 次,时间复杂度为 \\(O(amt / \\min(coins))\\) 。这比动态规划解法的时间复杂度 \\(O(n \\times amt)\\) 小了一个数量级。

然而,对于某些硬币面值组合,贪心算法并不能找到最优解。图 15-2 给出了两个示例。

图 15-2   贪心算法无法找出最优解的示例

也就是说,对于零钱兑换问题,贪心算法无法保证找到全局最优解,并且有可能找到非常差的解。它更适合用动态规划解决。

一般情况下,贪心算法的适用情况分以下两种。

  1. 可以保证找到最优解:贪心算法在这种情况下往往是最优选择,因为它往往比回溯、动态规划更高效。
  2. 可以找到近似最优解:贪心算法在这种情况下也是可用的。对于很多复杂问题来说,寻找全局最优解非常困难,能以较高效率找到次优解也是非常不错的。
","path":["第 15 章   贪心","15.1   贪心算法"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1512","level":2,"title":"15.1.2   贪心算法特性","text":"

那么问题来了,什么样的问题适合用贪心算法求解呢?或者说,贪心算法在什么情况下可以保证找到最优解?

相较于动态规划,贪心算法的使用条件更加苛刻,其主要关注问题的两个性质。

最优子结构已经在“动态规划”章节中介绍过,这里不再赘述。值得注意的是,一些问题的最优子结构并不明显,但仍然可使用贪心算法解决。

我们主要探究贪心选择性质的判断方法。虽然它的描述看上去比较简单,但实际上对于许多问题,证明贪心选择性质并非易事。

例如零钱兑换问题,我们虽然能够容易地举出反例,对贪心选择性质进行证伪,但证实的难度较大。如果问:满足什么条件的硬币组合可以使用贪心算法求解?我们往往只能凭借直觉或举例子来给出一个模棱两可的答案,而难以给出严谨的数学证明。

Quote

有一篇论文给出了一个 \\(O(n^3)\\) 时间复杂度的算法,用于判断一个硬币组合能否使用贪心算法找出任意金额的最优解。

Pearson, D. A polynomial-time algorithm for the change-making problem[J]. Operations Research Letters, 2005, 33(3): 231-234.

","path":["第 15 章   贪心","15.1   贪心算法"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1513","level":2,"title":"15.1.3   贪心算法解题步骤","text":"

贪心问题的解决流程大体可分为以下三步。

  1. 问题分析:梳理与理解问题特性,包括状态定义、优化目标和约束条件等。这一步在回溯和动态规划中都有涉及。
  2. 确定贪心策略:确定如何在每一步中做出贪心选择。这个策略能够在每一步减小问题的规模,并最终解决整个问题。
  3. 正确性证明:通常需要证明问题具有贪心选择性质和最优子结构。这个步骤可能需要用到数学证明,例如归纳法或反证法等。

确定贪心策略是求解问题的核心步骤,但实施起来可能并不容易,主要有以下原因。

为了保证正确性,我们应该对贪心策略进行严谨的数学证明,通常需要用到反证法或数学归纳法。

然而,正确性证明也很可能不是一件易事。如若没有头绪,我们通常会选择面向测试用例进行代码调试,一步步修改与验证贪心策略。

","path":["第 15 章   贪心","15.1   贪心算法"],"tags":[]},{"location":"chapter_greedy/greedy_algorithm/#1514","level":2,"title":"15.1.4   贪心算法典型例题","text":"

贪心算法常常应用在满足贪心选择性质和最优子结构的优化问题中,以下列举了一些典型的贪心算法问题。

","path":["第 15 章   贪心","15.1   贪心算法"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/","level":1,"title":"15.3   最大容量问题","text":"

Question

输入一个数组 \\(ht\\) ,其中的每个元素代表一个垂直隔板的高度。数组中的任意两个隔板,以及它们之间的空间可以组成一个容器。

容器的容量等于高度和宽度的乘积(面积),其中高度由较短的隔板决定,宽度是两个隔板的数组索引之差。

请在数组中选择两个隔板,使得组成的容器的容量最大,返回最大容量。示例如图 15-7 所示。

图 15-7   最大容量问题的示例数据

容器由任意两个隔板围成,因此本题的状态为两个隔板的索引,记为 \\([i, j]\\) 。

根据题意,容量等于高度乘以宽度,其中高度由短板决定,宽度是两隔板的数组索引之差。设容量为 \\(cap[i, j]\\) ,则可得计算公式:

\\[ cap[i, j] = \\min(ht[i], ht[j]) \\times (j - i) \\]

设数组长度为 \\(n\\) ,两个隔板的组合数量(状态总数)为 \\(C_n^2 = \\frac{n(n - 1)}{2}\\) 个。最直接地,我们可以穷举所有状态,从而求得最大容量,时间复杂度为 \\(O(n^2)\\) 。

","path":["第 15 章   贪心","15.3   最大容量问题"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/#1","level":3,"title":"1.   贪心策略确定","text":"

这道题还有更高效率的解法。如图 15-8 所示,现选取一个状态 \\([i, j]\\) ,其满足索引 \\(i < j\\) 且高度 \\(ht[i] < ht[j]\\) ,即 \\(i\\) 为短板、\\(j\\) 为长板。

图 15-8   初始状态

如图 15-9 所示,若此时将长板 \\(j\\) 向短板 \\(i\\) 靠近,则容量一定变小。

这是因为在移动长板 \\(j\\) 后,宽度 \\(j-i\\) 肯定变小;而高度由短板决定,因此高度只可能不变( \\(i\\) 仍为短板)或变小(移动后的 \\(j\\) 成为短板)。

图 15-9   向内移动长板后的状态

反向思考,我们只有向内收缩短板 \\(i\\) ,才有可能使容量变大。因为虽然宽度一定变小,但高度可能会变大(移动后的短板 \\(i\\) 可能会变长)。例如在图 15-10 中,移动短板后面积变大。

图 15-10   向内移动短板后的状态

由此便可推出本题的贪心策略:初始化两指针,使其分列容器两端,每轮向内收缩短板对应的指针,直至两指针相遇。

图 15-11 展示了贪心策略的执行过程。

  1. 初始状态下,指针 \\(i\\) 和 \\(j\\) 分列数组两端。
  2. 计算当前状态的容量 \\(cap[i, j]\\) ,并更新最大容量。
  3. 比较板 \\(i\\) 和板 \\(j\\) 的高度,并将短板向内移动一格。
  4. 循环执行第 2. 步和第 3. 步,直至 \\(i\\) 和 \\(j\\) 相遇时结束。
<1><2><3><4><5><6><7><8><9>

图 15-11   最大容量问题的贪心过程

","path":["第 15 章   贪心","15.3   最大容量问题"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/#2","level":3,"title":"2.   代码实现","text":"

代码循环最多 \\(n\\) 轮,因此时间复杂度为 \\(O(n)\\) 。

变量 \\(i\\)、\\(j\\)、\\(res\\) 使用常数大小的额外空间,因此空间复杂度为 \\(O(1)\\) 。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby max_capacity.py
def max_capacity(ht: list[int]) -> int:\n    \"\"\"最大容量:贪心\"\"\"\n    # 初始化 i, j,使其分列数组两端\n    i, j = 0, len(ht) - 1\n    # 初始最大容量为 0\n    res = 0\n    # 循环贪心选择,直至两板相遇\n    while i < j:\n        # 更新最大容量\n        cap = min(ht[i], ht[j]) * (j - i)\n        res = max(res, cap)\n        # 向内移动短板\n        if ht[i] < ht[j]:\n            i += 1\n        else:\n            j -= 1\n    return res\n
max_capacity.cpp
/* 最大容量:贪心 */\nint maxCapacity(vector<int> &ht) {\n    // 初始化 i, j,使其分列数组两端\n    int i = 0, j = ht.size() - 1;\n    // 初始最大容量为 0\n    int res = 0;\n    // 循环贪心选择,直至两板相遇\n    while (i < j) {\n        // 更新最大容量\n        int cap = min(ht[i], ht[j]) * (j - i);\n        res = max(res, cap);\n        // 向内移动短板\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
max_capacity.java
/* 最大容量:贪心 */\nint maxCapacity(int[] ht) {\n    // 初始化 i, j,使其分列数组两端\n    int i = 0, j = ht.length - 1;\n    // 初始最大容量为 0\n    int res = 0;\n    // 循环贪心选择,直至两板相遇\n    while (i < j) {\n        // 更新最大容量\n        int cap = Math.min(ht[i], ht[j]) * (j - i);\n        res = Math.max(res, cap);\n        // 向内移动短板\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
max_capacity.cs
/* 最大容量:贪心 */\nint MaxCapacity(int[] ht) {\n    // 初始化 i, j,使其分列数组两端\n    int i = 0, j = ht.Length - 1;\n    // 初始最大容量为 0\n    int res = 0;\n    // 循环贪心选择,直至两板相遇\n    while (i < j) {\n        // 更新最大容量\n        int cap = Math.Min(ht[i], ht[j]) * (j - i);\n        res = Math.Max(res, cap);\n        // 向内移动短板\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
max_capacity.go
/* 最大容量:贪心 */\nfunc maxCapacity(ht []int) int {\n    // 初始化 i, j,使其分列数组两端\n    i, j := 0, len(ht)-1\n    // 初始最大容量为 0\n    res := 0\n    // 循环贪心选择,直至两板相遇\n    for i < j {\n        // 更新最大容量\n        capacity := int(math.Min(float64(ht[i]), float64(ht[j]))) * (j - i)\n        res = int(math.Max(float64(res), float64(capacity)))\n        // 向内移动短板\n        if ht[i] < ht[j] {\n            i++\n        } else {\n            j--\n        }\n    }\n    return res\n}\n
max_capacity.swift
/* 最大容量:贪心 */\nfunc maxCapacity(ht: [Int]) -> Int {\n    // 初始化 i, j,使其分列数组两端\n    var i = ht.startIndex, j = ht.endIndex - 1\n    // 初始最大容量为 0\n    var res = 0\n    // 循环贪心选择,直至两板相遇\n    while i < j {\n        // 更新最大容量\n        let cap = min(ht[i], ht[j]) * (j - i)\n        res = max(res, cap)\n        // 向内移动短板\n        if ht[i] < ht[j] {\n            i += 1\n        } else {\n            j -= 1\n        }\n    }\n    return res\n}\n
max_capacity.js
/* 最大容量:贪心 */\nfunction maxCapacity(ht) {\n    // 初始化 i, j,使其分列数组两端\n    let i = 0,\n        j = ht.length - 1;\n    // 初始最大容量为 0\n    let res = 0;\n    // 循环贪心选择,直至两板相遇\n    while (i < j) {\n        // 更新最大容量\n        const cap = Math.min(ht[i], ht[j]) * (j - i);\n        res = Math.max(res, cap);\n        // 向内移动短板\n        if (ht[i] < ht[j]) {\n            i += 1;\n        } else {\n            j -= 1;\n        }\n    }\n    return res;\n}\n
max_capacity.ts
/* 最大容量:贪心 */\nfunction maxCapacity(ht: number[]): number {\n    // 初始化 i, j,使其分列数组两端\n    let i = 0,\n        j = ht.length - 1;\n    // 初始最大容量为 0\n    let res = 0;\n    // 循环贪心选择,直至两板相遇\n    while (i < j) {\n        // 更新最大容量\n        const cap: number = Math.min(ht[i], ht[j]) * (j - i);\n        res = Math.max(res, cap);\n        // 向内移动短板\n        if (ht[i] < ht[j]) {\n            i += 1;\n        } else {\n            j -= 1;\n        }\n    }\n    return res;\n}\n
max_capacity.dart
/* 最大容量:贪心 */\nint maxCapacity(List<int> ht) {\n  // 初始化 i, j,使其分列数组两端\n  int i = 0, j = ht.length - 1;\n  // 初始最大容量为 0\n  int res = 0;\n  // 循环贪心选择,直至两板相遇\n  while (i < j) {\n    // 更新最大容量\n    int cap = min(ht[i], ht[j]) * (j - i);\n    res = max(res, cap);\n    // 向内移动短板\n    if (ht[i] < ht[j]) {\n      i++;\n    } else {\n      j--;\n    }\n  }\n  return res;\n}\n
max_capacity.rs
/* 最大容量:贪心 */\nfn max_capacity(ht: &[i32]) -> i32 {\n    // 初始化 i, j,使其分列数组两端\n    let mut i = 0;\n    let mut j = ht.len() - 1;\n    // 初始最大容量为 0\n    let mut res = 0;\n    // 循环贪心选择,直至两板相遇\n    while i < j {\n        // 更新最大容量\n        let cap = std::cmp::min(ht[i], ht[j]) * (j - i) as i32;\n        res = std::cmp::max(res, cap);\n        // 向内移动短板\n        if ht[i] < ht[j] {\n            i += 1;\n        } else {\n            j -= 1;\n        }\n    }\n    res\n}\n
max_capacity.c
/* 最大容量:贪心 */\nint maxCapacity(int ht[], int htLength) {\n    // 初始化 i, j,使其分列数组两端\n    int i = 0;\n    int j = htLength - 1;\n    // 初始最大容量为 0\n    int res = 0;\n    // 循环贪心选择,直至两板相遇\n    while (i < j) {\n        // 更新最大容量\n        int capacity = myMin(ht[i], ht[j]) * (j - i);\n        res = myMax(res, capacity);\n        // 向内移动短板\n        if (ht[i] < ht[j]) {\n            i++;\n        } else {\n            j--;\n        }\n    }\n    return res;\n}\n
max_capacity.kt
/* 最大容量:贪心 */\nfun maxCapacity(ht: IntArray): Int {\n    // 初始化 i, j,使其分列数组两端\n    var i = 0\n    var j = ht.size - 1\n    // 初始最大容量为 0\n    var res = 0\n    // 循环贪心选择,直至两板相遇\n    while (i < j) {\n        // 更新最大容量\n        val cap = min(ht[i], ht[j]) * (j - i)\n        res = max(res, cap)\n        // 向内移动短板\n        if (ht[i] < ht[j]) {\n            i++\n        } else {\n            j--\n        }\n    }\n    return res\n}\n
max_capacity.rb
### 最大容量:贪心 ###\ndef max_capacity(ht)\n  # 初始化 i, j,使其分列数组两端\n  i, j = 0, ht.length - 1\n  # 初始最大容量为 0\n  res = 0\n\n  # 循环贪心选择,直至两板相遇\n  while i < j\n    # 更新最大容量\n    cap = [ht[i], ht[j]].min * (j - i)\n    res = [res, cap].max\n    # 向内移动短板\n    if ht[i] < ht[j]\n      i += 1\n    else\n      j -= 1\n    end\n  end\n\n  res\nend\n
可视化运行

全屏观看 >

","path":["第 15 章   贪心","15.3   最大容量问题"],"tags":[]},{"location":"chapter_greedy/max_capacity_problem/#3","level":3,"title":"3.   正确性证明","text":"

之所以贪心比穷举更快,是因为每轮的贪心选择都会“跳过”一些状态。

比如在状态 \\(cap[i, j]\\) 下,\\(i\\) 为短板、\\(j\\) 为长板。若贪心地将短板 \\(i\\) 向内移动一格,会导致图 15-12 所示的状态被“跳过”。这意味着之后无法验证这些状态的容量大小。

\\[ cap[i, i+1], cap[i, i+2], \\dots, cap[i, j-2], cap[i, j-1] \\]

图 15-12   移动短板导致被跳过的状态

观察发现,这些被跳过的状态实际上就是将长板 \\(j\\) 向内移动的所有状态。前面我们已经证明内移长板一定会导致容量变小。也就是说,被跳过的状态都不可能是最优解,跳过它们不会导致错过最优解。

以上分析说明,移动短板的操作是“安全”的,贪心策略是有效的。

","path":["第 15 章   贪心","15.3   最大容量问题"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/","level":1,"title":"15.4   最大切分乘积问题","text":"

Question

给定一个正整数 \\(n\\) ,将其切分为至少两个正整数的和,求切分后所有整数的乘积最大是多少,如图 15-13 所示。

图 15-13   最大切分乘积的问题定义

假设我们将 \\(n\\) 切分为 \\(m\\) 个整数因子,其中第 \\(i\\) 个因子记为 \\(n_i\\) ,即

\\[ n = \\sum_{i=1}^{m}n_i \\]

本题的目标是求得所有整数因子的最大乘积,即

\\[ \\max(\\prod_{i=1}^{m}n_i) \\]

我们需要思考的是:切分数量 \\(m\\) 应该多大,每个 \\(n_i\\) 应该是多少?

","path":["第 15 章   贪心","15.4   最大切分乘积问题"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/#1","level":3,"title":"1.   贪心策略确定","text":"

根据经验,两个整数的乘积往往比它们的加和更大。假设从 \\(n\\) 中分出一个因子 \\(2\\) ,则它们的乘积为 \\(2(n-2)\\) 。我们将该乘积与 \\(n\\) 作比较:

\\[ \\begin{aligned} 2(n-2) & \\geq n \\newline 2n - n - 4 & \\geq 0 \\newline n & \\geq 4 \\end{aligned} \\]

如图 15-14 所示,当 \\(n \\geq 4\\) 时,切分出一个 \\(2\\) 后乘积会变大,这说明大于等于 \\(4\\) 的整数都应该被切分。

贪心策略一:如果切分方案中包含 \\(\\geq 4\\) 的因子,那么它就应该被继续切分。最终的切分方案只应出现 \\(1\\)、\\(2\\)、\\(3\\) 这三种因子。

图 15-14   切分导致乘积变大

接下来思考哪个因子是最优的。在 \\(1\\)、\\(2\\)、\\(3\\) 这三个因子中,显然 \\(1\\) 是最差的,因为 \\(1 \\times (n-1) < n\\) 恒成立,即切分出 \\(1\\) 反而会导致乘积减小。

如图 15-15 所示,当 \\(n = 6\\) 时,有 \\(3 \\times 3 > 2 \\times 2 \\times 2\\) 。这意味着切分出 \\(3\\) 比切分出 \\(2\\) 更优。

贪心策略二:在切分方案中,最多只应存在两个 \\(2\\) 。因为三个 \\(2\\) 总是可以替换为两个 \\(3\\) ,从而获得更大的乘积。

图 15-15   最优切分因子

综上所述,可推理出以下贪心策略。

  1. 输入整数 \\(n\\) ,从其不断地切分出因子 \\(3\\) ,直至余数为 \\(0\\)、\\(1\\)、\\(2\\) 。
  2. 当余数为 \\(0\\) 时,代表 \\(n\\) 是 \\(3\\) 的倍数,因此不做任何处理。
  3. 当余数为 \\(2\\) 时,不继续划分,保留。
  4. 当余数为 \\(1\\) 时,由于 \\(2 \\times 2 > 1 \\times 3\\) ,因此应将最后一个 \\(3\\) 替换为 \\(2\\) 。
","path":["第 15 章   贪心","15.4   最大切分乘积问题"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/#2","level":3,"title":"2.   代码实现","text":"

如图 15-16 所示,我们无须通过循环来切分整数,而可以利用向下整除运算得到 \\(3\\) 的个数 \\(a\\) ,用取模运算得到余数 \\(b\\) ,此时有:

\\[ n = 3 a + b \\]

请注意,对于 \\(n \\leq 3\\) 的边界情况,必须拆分出一个 \\(1\\) ,乘积为 \\(1 \\times (n - 1)\\) 。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby max_product_cutting.py
def max_product_cutting(n: int) -> int:\n    \"\"\"最大切分乘积:贪心\"\"\"\n    # 当 n <= 3 时,必须切分出一个 1\n    if n <= 3:\n        return 1 * (n - 1)\n    # 贪心地切分出 3 ,a 为 3 的个数,b 为余数\n    a, b = n // 3, n % 3\n    if b == 1:\n        # 当余数为 1 时,将一对 1 * 3 转化为 2 * 2\n        return int(math.pow(3, a - 1)) * 2 * 2\n    if b == 2:\n        # 当余数为 2 时,不做处理\n        return int(math.pow(3, a)) * 2\n    # 当余数为 0 时,不做处理\n    return int(math.pow(3, a))\n
max_product_cutting.cpp
/* 最大切分乘积:贪心 */\nint maxProductCutting(int n) {\n    // 当 n <= 3 时,必须切分出一个 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // 贪心地切分出 3 ,a 为 3 的个数,b 为余数\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // 当余数为 1 时,将一对 1 * 3 转化为 2 * 2\n        return (int)pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // 当余数为 2 时,不做处理\n        return (int)pow(3, a) * 2;\n    }\n    // 当余数为 0 时,不做处理\n    return (int)pow(3, a);\n}\n
max_product_cutting.java
/* 最大切分乘积:贪心 */\nint maxProductCutting(int n) {\n    // 当 n <= 3 时,必须切分出一个 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // 贪心地切分出 3 ,a 为 3 的个数,b 为余数\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // 当余数为 1 时,将一对 1 * 3 转化为 2 * 2\n        return (int) Math.pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // 当余数为 2 时,不做处理\n        return (int) Math.pow(3, a) * 2;\n    }\n    // 当余数为 0 时,不做处理\n    return (int) Math.pow(3, a);\n}\n
max_product_cutting.cs
/* 最大切分乘积:贪心 */\nint MaxProductCutting(int n) {\n    // 当 n <= 3 时,必须切分出一个 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // 贪心地切分出 3 ,a 为 3 的个数,b 为余数\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // 当余数为 1 时,将一对 1 * 3 转化为 2 * 2\n        return (int)Math.Pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // 当余数为 2 时,不做处理\n        return (int)Math.Pow(3, a) * 2;\n    }\n    // 当余数为 0 时,不做处理\n    return (int)Math.Pow(3, a);\n}\n
max_product_cutting.go
/* 最大切分乘积:贪心 */\nfunc maxProductCutting(n int) int {\n    // 当 n <= 3 时,必须切分出一个 1\n    if n <= 3 {\n        return 1 * (n - 1)\n    }\n    // 贪心地切分出 3 ,a 为 3 的个数,b 为余数\n    a := n / 3\n    b := n % 3\n    if b == 1 {\n        // 当余数为 1 时,将一对 1 * 3 转化为 2 * 2\n        return int(math.Pow(3, float64(a-1))) * 2 * 2\n    }\n    if b == 2 {\n        // 当余数为 2 时,不做处理\n        return int(math.Pow(3, float64(a))) * 2\n    }\n    // 当余数为 0 时,不做处理\n    return int(math.Pow(3, float64(a)))\n}\n
max_product_cutting.swift
/* 最大切分乘积:贪心 */\nfunc maxProductCutting(n: Int) -> Int {\n    // 当 n <= 3 时,必须切分出一个 1\n    if n <= 3 {\n        return 1 * (n - 1)\n    }\n    // 贪心地切分出 3 ,a 为 3 的个数,b 为余数\n    let a = n / 3\n    let b = n % 3\n    if b == 1 {\n        // 当余数为 1 时,将一对 1 * 3 转化为 2 * 2\n        return pow(3, a - 1) * 2 * 2\n    }\n    if b == 2 {\n        // 当余数为 2 时,不做处理\n        return pow(3, a) * 2\n    }\n    // 当余数为 0 时,不做处理\n    return pow(3, a)\n}\n
max_product_cutting.js
/* 最大切分乘积:贪心 */\nfunction maxProductCutting(n) {\n    // 当 n <= 3 时,必须切分出一个 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // 贪心地切分出 3 ,a 为 3 的个数,b 为余数\n    let a = Math.floor(n / 3);\n    let b = n % 3;\n    if (b === 1) {\n        // 当余数为 1 时,将一对 1 * 3 转化为 2 * 2\n        return Math.pow(3, a - 1) * 2 * 2;\n    }\n    if (b === 2) {\n        // 当余数为 2 时,不做处理\n        return Math.pow(3, a) * 2;\n    }\n    // 当余数为 0 时,不做处理\n    return Math.pow(3, a);\n}\n
max_product_cutting.ts
/* 最大切分乘积:贪心 */\nfunction maxProductCutting(n: number): number {\n    // 当 n <= 3 时,必须切分出一个 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // 贪心地切分出 3 ,a 为 3 的个数,b 为余数\n    let a: number = Math.floor(n / 3);\n    let b: number = n % 3;\n    if (b === 1) {\n        // 当余数为 1 时,将一对 1 * 3 转化为 2 * 2\n        return Math.pow(3, a - 1) * 2 * 2;\n    }\n    if (b === 2) {\n        // 当余数为 2 时,不做处理\n        return Math.pow(3, a) * 2;\n    }\n    // 当余数为 0 时,不做处理\n    return Math.pow(3, a);\n}\n
max_product_cutting.dart
/* 最大切分乘积:贪心 */\nint maxProductCutting(int n) {\n  // 当 n <= 3 时,必须切分出一个 1\n  if (n <= 3) {\n    return 1 * (n - 1);\n  }\n  // 贪心地切分出 3 ,a 为 3 的个数,b 为余数\n  int a = n ~/ 3;\n  int b = n % 3;\n  if (b == 1) {\n    // 当余数为 1 时,将一对 1 * 3 转化为 2 * 2\n    return (pow(3, a - 1) * 2 * 2).toInt();\n  }\n  if (b == 2) {\n    // 当余数为 2 时,不做处理\n    return (pow(3, a) * 2).toInt();\n  }\n  // 当余数为 0 时,不做处理\n  return pow(3, a).toInt();\n}\n
max_product_cutting.rs
/* 最大切分乘积:贪心 */\nfn max_product_cutting(n: i32) -> i32 {\n    // 当 n <= 3 时,必须切分出一个 1\n    if n <= 3 {\n        return 1 * (n - 1);\n    }\n    // 贪心地切分出 3 ,a 为 3 的个数,b 为余数\n    let a = n / 3;\n    let b = n % 3;\n    if b == 1 {\n        // 当余数为 1 时,将一对 1 * 3 转化为 2 * 2\n        3_i32.pow(a as u32 - 1) * 2 * 2\n    } else if b == 2 {\n        // 当余数为 2 时,不做处理\n        3_i32.pow(a as u32) * 2\n    } else {\n        // 当余数为 0 时,不做处理\n        3_i32.pow(a as u32)\n    }\n}\n
max_product_cutting.c
/* 最大切分乘积:贪心 */\nint maxProductCutting(int n) {\n    // 当 n <= 3 时,必须切分出一个 1\n    if (n <= 3) {\n        return 1 * (n - 1);\n    }\n    // 贪心地切分出 3 ,a 为 3 的个数,b 为余数\n    int a = n / 3;\n    int b = n % 3;\n    if (b == 1) {\n        // 当余数为 1 时,将一对 1 * 3 转化为 2 * 2\n        return pow(3, a - 1) * 2 * 2;\n    }\n    if (b == 2) {\n        // 当余数为 2 时,不做处理\n        return pow(3, a) * 2;\n    }\n    // 当余数为 0 时,不做处理\n    return pow(3, a);\n}\n
max_product_cutting.kt
/* 最大切分乘积:贪心 */\nfun maxProductCutting(n: Int): Int {\n    // 当 n <= 3 时,必须切分出一个 1\n    if (n <= 3) {\n        return 1 * (n - 1)\n    }\n    // 贪心地切分出 3 ,a 为 3 的个数,b 为余数\n    val a = n / 3\n    val b = n % 3\n    if (b == 1) {\n        // 当余数为 1 时,将一对 1 * 3 转化为 2 * 2\n        return 3.0.pow((a - 1)).toInt() * 2 * 2\n    }\n    if (b == 2) {\n        // 当余数为 2 时,不做处理\n        return 3.0.pow(a).toInt() * 2 * 2\n    }\n    // 当余数为 0 时,不做处理\n    return 3.0.pow(a).toInt()\n}\n
max_product_cutting.rb
### 最大切分乘积:贪心 ###\ndef max_product_cutting(n)\n  # 当 n <= 3 时,必须切分出一个 1\n  return 1 * (n - 1) if n <= 3\n  # 贪心地切分出 3 ,a 为 3 的个数,b 为余数\n  a, b = n / 3, n % 3\n  # 当余数为 1 时,将一对 1 * 3 转化为 2 * 2\n  return (3.pow(a - 1) * 2 * 2).to_i if b == 1\n  # 当余数为 2 时,不做处理\n  return (3.pow(a) * 2).to_i if b == 2\n  # 当余数为 0 时,不做处理\n  3.pow(a).to_i\nend\n
可视化运行

全屏观看 >

图 15-16   最大切分乘积的计算方法

时间复杂度取决于编程语言的幂运算的实现方法。以 Python 为例,常用的幂计算函数有三种。

变量 \\(a\\) 和 \\(b\\) 使用常数大小的额外空间,因此空间复杂度为 \\(O(1)\\) 。

","path":["第 15 章   贪心","15.4   最大切分乘积问题"],"tags":[]},{"location":"chapter_greedy/max_product_cutting_problem/#3","level":3,"title":"3.   正确性证明","text":"

使用反证法,只分析 \\(n \\geq 4\\) 的情况。

  1. 所有因子 \\(\\leq 3\\) :假设最优切分方案中存在 \\(\\geq 4\\) 的因子 \\(x\\) ,那么一定可以将其继续划分为 \\(2(x-2)\\) ,从而获得更大(或相等)的乘积。这与假设矛盾。
  2. 切分方案不包含 \\(1\\) :假设最优切分方案中存在一个因子 \\(1\\) ,那么它一定可以合并入另外一个因子中,以获得更大的乘积。这与假设矛盾。
  3. 切分方案最多包含两个 \\(2\\) :假设最优切分方案中包含三个 \\(2\\) ,那么一定可以替换为两个 \\(3\\) ,乘积更大。这与假设矛盾。
","path":["第 15 章   贪心","15.4   最大切分乘积问题"],"tags":[]},{"location":"chapter_greedy/summary/","level":1,"title":"15.5   小结","text":"","path":["第 15 章   贪心","15.5   小结"],"tags":[]},{"location":"chapter_greedy/summary/#1","level":3,"title":"1.   重点回顾","text":"","path":["第 15 章   贪心","15.5   小结"],"tags":[]},{"location":"chapter_hashing/","level":1,"title":"第 6 章   哈希表","text":"

Abstract

在计算机世界中,哈希表如同一位聪慧的图书管理员。

他知道如何计算索书号,从而可以快速找到目标图书。

","path":["第 6 章   哈希表"],"tags":[]},{"location":"chapter_hashing/#_1","level":2,"title":"本章内容","text":"","path":["第 6 章   哈希表"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/","level":1,"title":"6.3   哈希算法","text":"

前两节介绍了哈希表的工作原理和哈希冲突的处理方法。然而无论是开放寻址还是链式地址,它们只能保证哈希表可以在发生冲突时正常工作,而无法减少哈希冲突的发生。

如果哈希冲突过于频繁,哈希表的性能则会急剧劣化。如图 6-8 所示,对于链式地址哈希表,理想情况下键值对均匀分布在各个桶中,达到最佳查询效率;最差情况下所有键值对都存储到同一个桶中,时间复杂度退化至 \\(O(n)\\) 。

图 6-8   哈希冲突的最佳情况与最差情况

键值对的分布情况由哈希函数决定。回忆哈希函数的计算步骤,先计算哈希值,再对数组长度取模:

index = hash(key) % capacity\n

观察以上公式,当哈希表容量 capacity 固定时,哈希算法 hash() 决定了输出值,进而决定了键值对在哈希表中的分布情况。

这意味着,为了降低哈希冲突的发生概率,我们应当将注意力集中在哈希算法 hash() 的设计上。

","path":["第 6 章   哈希表","6.3   哈希算法"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#631","level":2,"title":"6.3.1   哈希算法的目标","text":"

为了实现“既快又稳”的哈希表数据结构,哈希算法应具备以下特点。

实际上,哈希算法除了可以用于实现哈希表,还广泛应用于其他领域中。

对于密码学的相关应用,为了防止从哈希值推导出原始密码等逆向工程,哈希算法需要具备更高等级的安全特性。

请注意,“均匀分布”与“抗碰撞性”是两个独立的概念,满足均匀分布不一定满足抗碰撞性。例如,在随机输入 key 下,哈希函数 key % 100 可以产生均匀分布的输出。然而该哈希算法过于简单,所有后两位相等的 key 的输出都相同,因此我们可以很容易地从哈希值反推出可用的 key ,从而破解密码。

","path":["第 6 章   哈希表","6.3   哈希算法"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#632","level":2,"title":"6.3.2   哈希算法的设计","text":"

哈希算法的设计是一个需要考虑许多因素的复杂问题。然而对于某些要求不高的场景,我们也能设计一些简单的哈希算法。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby simple_hash.py
def add_hash(key: str) -> int:\n    \"\"\"加法哈希\"\"\"\n    hash = 0\n    modulus = 1000000007\n    for c in key:\n        hash += ord(c)\n    return hash % modulus\n\ndef mul_hash(key: str) -> int:\n    \"\"\"乘法哈希\"\"\"\n    hash = 0\n    modulus = 1000000007\n    for c in key:\n        hash = 31 * hash + ord(c)\n    return hash % modulus\n\ndef xor_hash(key: str) -> int:\n    \"\"\"异或哈希\"\"\"\n    hash = 0\n    modulus = 1000000007\n    for c in key:\n        hash ^= ord(c)\n    return hash % modulus\n\ndef rot_hash(key: str) -> int:\n    \"\"\"旋转哈希\"\"\"\n    hash = 0\n    modulus = 1000000007\n    for c in key:\n        hash = (hash << 4) ^ (hash >> 28) ^ ord(c)\n    return hash % modulus\n
simple_hash.cpp
/* 加法哈希 */\nint addHash(string key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (unsigned char c : key) {\n        hash = (hash + (int)c) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* 乘法哈希 */\nint mulHash(string key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (unsigned char c : key) {\n        hash = (31 * hash + (int)c) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* 异或哈希 */\nint xorHash(string key) {\n    int hash = 0;\n    const int MODULUS = 1000000007;\n    for (unsigned char c : key) {\n        hash ^= (int)c;\n    }\n    return hash & MODULUS;\n}\n\n/* 旋转哈希 */\nint rotHash(string key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (unsigned char c : key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ (int)c) % MODULUS;\n    }\n    return (int)hash;\n}\n
simple_hash.java
/* 加法哈希 */\nint addHash(String key) {\n    long hash = 0;\n    final int MODULUS = 1000000007;\n    for (char c : key.toCharArray()) {\n        hash = (hash + (int) c) % MODULUS;\n    }\n    return (int) hash;\n}\n\n/* 乘法哈希 */\nint mulHash(String key) {\n    long hash = 0;\n    final int MODULUS = 1000000007;\n    for (char c : key.toCharArray()) {\n        hash = (31 * hash + (int) c) % MODULUS;\n    }\n    return (int) hash;\n}\n\n/* 异或哈希 */\nint xorHash(String key) {\n    int hash = 0;\n    final int MODULUS = 1000000007;\n    for (char c : key.toCharArray()) {\n        hash ^= (int) c;\n    }\n    return hash & MODULUS;\n}\n\n/* 旋转哈希 */\nint rotHash(String key) {\n    long hash = 0;\n    final int MODULUS = 1000000007;\n    for (char c : key.toCharArray()) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ (int) c) % MODULUS;\n    }\n    return (int) hash;\n}\n
simple_hash.cs
/* 加法哈希 */\nint AddHash(string key) {\n    long hash = 0;\n    const int MODULUS = 1000000007;\n    foreach (char c in key) {\n        hash = (hash + c) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* 乘法哈希 */\nint MulHash(string key) {\n    long hash = 0;\n    const int MODULUS = 1000000007;\n    foreach (char c in key) {\n        hash = (31 * hash + c) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* 异或哈希 */\nint XorHash(string key) {\n    int hash = 0;\n    const int MODULUS = 1000000007;\n    foreach (char c in key) {\n        hash ^= c;\n    }\n    return hash & MODULUS;\n}\n\n/* 旋转哈希 */\nint RotHash(string key) {\n    long hash = 0;\n    const int MODULUS = 1000000007;\n    foreach (char c in key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ c) % MODULUS;\n    }\n    return (int)hash;\n}\n
simple_hash.go
/* 加法哈希 */\nfunc addHash(key string) int {\n    var hash int64\n    var modulus int64\n\n    modulus = 1000000007\n    for _, b := range []byte(key) {\n        hash = (hash + int64(b)) % modulus\n    }\n    return int(hash)\n}\n\n/* 乘法哈希 */\nfunc mulHash(key string) int {\n    var hash int64\n    var modulus int64\n\n    modulus = 1000000007\n    for _, b := range []byte(key) {\n        hash = (31*hash + int64(b)) % modulus\n    }\n    return int(hash)\n}\n\n/* 异或哈希 */\nfunc xorHash(key string) int {\n    hash := 0\n    modulus := 1000000007\n    for _, b := range []byte(key) {\n        fmt.Println(int(b))\n        hash ^= int(b)\n        hash = (31*hash + int(b)) % modulus\n    }\n    return hash & modulus\n}\n\n/* 旋转哈希 */\nfunc rotHash(key string) int {\n    var hash int64\n    var modulus int64\n\n    modulus = 1000000007\n    for _, b := range []byte(key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ int64(b)) % modulus\n    }\n    return int(hash)\n}\n
simple_hash.swift
/* 加法哈希 */\nfunc addHash(key: String) -> Int {\n    var hash = 0\n    let MODULUS = 1_000_000_007\n    for c in key {\n        for scalar in c.unicodeScalars {\n            hash = (hash + Int(scalar.value)) % MODULUS\n        }\n    }\n    return hash\n}\n\n/* 乘法哈希 */\nfunc mulHash(key: String) -> Int {\n    var hash = 0\n    let MODULUS = 1_000_000_007\n    for c in key {\n        for scalar in c.unicodeScalars {\n            hash = (31 * hash + Int(scalar.value)) % MODULUS\n        }\n    }\n    return hash\n}\n\n/* 异或哈希 */\nfunc xorHash(key: String) -> Int {\n    var hash = 0\n    let MODULUS = 1_000_000_007\n    for c in key {\n        for scalar in c.unicodeScalars {\n            hash ^= Int(scalar.value)\n        }\n    }\n    return hash & MODULUS\n}\n\n/* 旋转哈希 */\nfunc rotHash(key: String) -> Int {\n    var hash = 0\n    let MODULUS = 1_000_000_007\n    for c in key {\n        for scalar in c.unicodeScalars {\n            hash = ((hash << 4) ^ (hash >> 28) ^ Int(scalar.value)) % MODULUS\n        }\n    }\n    return hash\n}\n
simple_hash.js
/* 加法哈希 */\nfunction addHash(key) {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = (hash + c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n\n/* 乘法哈希 */\nfunction mulHash(key) {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = (31 * hash + c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n\n/* 异或哈希 */\nfunction xorHash(key) {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash ^= c.charCodeAt(0);\n    }\n    return hash % MODULUS;\n}\n\n/* 旋转哈希 */\nfunction rotHash(key) {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n
simple_hash.ts
/* 加法哈希 */\nfunction addHash(key: string): number {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = (hash + c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n\n/* 乘法哈希 */\nfunction mulHash(key: string): number {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = (31 * hash + c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n\n/* 异或哈希 */\nfunction xorHash(key: string): number {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash ^= c.charCodeAt(0);\n    }\n    return hash % MODULUS;\n}\n\n/* 旋转哈希 */\nfunction rotHash(key: string): number {\n    let hash = 0;\n    const MODULUS = 1000000007;\n    for (const c of key) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ c.charCodeAt(0)) % MODULUS;\n    }\n    return hash;\n}\n
simple_hash.dart
/* 加法哈希 */\nint addHash(String key) {\n  int hash = 0;\n  final int MODULUS = 1000000007;\n  for (int i = 0; i < key.length; i++) {\n    hash = (hash + key.codeUnitAt(i)) % MODULUS;\n  }\n  return hash;\n}\n\n/* 乘法哈希 */\nint mulHash(String key) {\n  int hash = 0;\n  final int MODULUS = 1000000007;\n  for (int i = 0; i < key.length; i++) {\n    hash = (31 * hash + key.codeUnitAt(i)) % MODULUS;\n  }\n  return hash;\n}\n\n/* 异或哈希 */\nint xorHash(String key) {\n  int hash = 0;\n  final int MODULUS = 1000000007;\n  for (int i = 0; i < key.length; i++) {\n    hash ^= key.codeUnitAt(i);\n  }\n  return hash & MODULUS;\n}\n\n/* 旋转哈希 */\nint rotHash(String key) {\n  int hash = 0;\n  final int MODULUS = 1000000007;\n  for (int i = 0; i < key.length; i++) {\n    hash = ((hash << 4) ^ (hash >> 28) ^ key.codeUnitAt(i)) % MODULUS;\n  }\n  return hash;\n}\n
simple_hash.rs
/* 加法哈希 */\nfn add_hash(key: &str) -> i32 {\n    let mut hash = 0_i64;\n    const MODULUS: i64 = 1000000007;\n\n    for c in key.chars() {\n        hash = (hash + c as i64) % MODULUS;\n    }\n\n    hash as i32\n}\n\n/* 乘法哈希 */\nfn mul_hash(key: &str) -> i32 {\n    let mut hash = 0_i64;\n    const MODULUS: i64 = 1000000007;\n\n    for c in key.chars() {\n        hash = (31 * hash + c as i64) % MODULUS;\n    }\n\n    hash as i32\n}\n\n/* 异或哈希 */\nfn xor_hash(key: &str) -> i32 {\n    let mut hash = 0_i64;\n    const MODULUS: i64 = 1000000007;\n\n    for c in key.chars() {\n        hash ^= c as i64;\n    }\n\n    (hash & MODULUS) as i32\n}\n\n/* 旋转哈希 */\nfn rot_hash(key: &str) -> i32 {\n    let mut hash = 0_i64;\n    const MODULUS: i64 = 1000000007;\n\n    for c in key.chars() {\n        hash = ((hash << 4) ^ (hash >> 28) ^ c as i64) % MODULUS;\n    }\n\n    hash as i32\n}\n
simple_hash.c
/* 加法哈希 */\nint addHash(char *key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (int i = 0; i < strlen(key); i++) {\n        hash = (hash + (unsigned char)key[i]) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* 乘法哈希 */\nint mulHash(char *key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (int i = 0; i < strlen(key); i++) {\n        hash = (31 * hash + (unsigned char)key[i]) % MODULUS;\n    }\n    return (int)hash;\n}\n\n/* 异或哈希 */\nint xorHash(char *key) {\n    int hash = 0;\n    const int MODULUS = 1000000007;\n\n    for (int i = 0; i < strlen(key); i++) {\n        hash ^= (unsigned char)key[i];\n    }\n    return hash & MODULUS;\n}\n\n/* 旋转哈希 */\nint rotHash(char *key) {\n    long long hash = 0;\n    const int MODULUS = 1000000007;\n    for (int i = 0; i < strlen(key); i++) {\n        hash = ((hash << 4) ^ (hash >> 28) ^ (unsigned char)key[i]) % MODULUS;\n    }\n\n    return (int)hash;\n}\n
simple_hash.kt
/* 加法哈希 */\nfun addHash(key: String): Int {\n    var hash = 0L\n    val MODULUS = 1000000007\n    for (c in key.toCharArray()) {\n        hash = (hash + c.code) % MODULUS\n    }\n    return hash.toInt()\n}\n\n/* 乘法哈希 */\nfun mulHash(key: String): Int {\n    var hash = 0L\n    val MODULUS = 1000000007\n    for (c in key.toCharArray()) {\n        hash = (31 * hash + c.code) % MODULUS\n    }\n    return hash.toInt()\n}\n\n/* 异或哈希 */\nfun xorHash(key: String): Int {\n    var hash = 0\n    val MODULUS = 1000000007\n    for (c in key.toCharArray()) {\n        hash = hash xor c.code\n    }\n    return hash and MODULUS\n}\n\n/* 旋转哈希 */\nfun rotHash(key: String): Int {\n    var hash = 0L\n    val MODULUS = 1000000007\n    for (c in key.toCharArray()) {\n        hash = ((hash shl 4) xor (hash shr 28) xor c.code.toLong()) % MODULUS\n    }\n    return hash.toInt()\n}\n
simple_hash.rb
### 加法哈希 ###\ndef add_hash(key)\n  hash = 0\n  modulus = 1_000_000_007\n\n  key.each_char { |c| hash += c.ord }\n\n  hash % modulus\nend\n\n### 乘法哈希 ###\ndef mul_hash(key)\n  hash = 0\n  modulus = 1_000_000_007\n\n  key.each_char { |c| hash = 31 * hash + c.ord }\n\n  hash % modulus\nend\n\n### 异或哈希 ###\ndef xor_hash(key)\n  hash = 0\n  modulus = 1_000_000_007\n\n  key.each_char { |c| hash ^= c.ord }\n\n  hash % modulus\nend\n\n### 旋转哈希 ###\ndef rot_hash(key)\n  hash = 0\n  modulus = 1_000_000_007\n\n  key.each_char { |c| hash = (hash << 4) ^ (hash >> 28) ^ c.ord }\n\n  hash % modulus\nend\n
可视化运行

全屏观看 >

观察发现,每种哈希算法的最后一步都是对大质数 \\(1000000007\\) 取模,以确保哈希值在合适的范围内。值得思考的是,为什么要强调对质数取模,或者说对合数取模的弊端是什么?这是一个有趣的问题。

先给出结论:使用大质数作为模数,可以最大化地保证哈希值的均匀分布。因为质数不与其他数字存在公约数,可以减少因取模操作而产生的周期性模式,从而避免哈希冲突。

举个例子,假设我们选择合数 \\(9\\) 作为模数,它可以被 \\(3\\) 整除,那么所有可以被 \\(3\\) 整除的 key 都会被映射到 \\(0\\)、\\(3\\)、\\(6\\) 这三个哈希值。

\\[ \\begin{aligned} \\text{modulus} & = 9 \\newline \\text{key} & = \\{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \\dots \\} \\newline \\text{hash} & = \\{ 0, 3, 6, 0, 3, 6, 0, 3, 6, 0, 3, 6,\\dots \\} \\end{aligned} \\]

如果输入 key 恰好满足这种等差数列的数据分布,那么哈希值就会出现聚堆,从而加重哈希冲突。现在,假设将 modulus 替换为质数 \\(13\\) ,由于 keymodulus 之间不存在公约数,因此输出的哈希值的均匀性会明显提升。

\\[ \\begin{aligned} \\text{modulus} & = 13 \\newline \\text{key} & = \\{ 0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, \\dots \\} \\newline \\text{hash} & = \\{ 0, 3, 6, 9, 12, 2, 5, 8, 11, 1, 4, 7, \\dots \\} \\end{aligned} \\]

值得说明的是,如果能够保证 key 是随机均匀分布的,那么选择质数或者合数作为模数都可以,它们都能输出均匀分布的哈希值。而当 key 的分布存在某种周期性时,对合数取模更容易出现聚集现象。

总而言之,我们通常选取质数作为模数,并且这个质数最好足够大,以尽可能消除周期性模式,提升哈希算法的稳健性。

","path":["第 6 章   哈希表","6.3   哈希算法"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#633","level":2,"title":"6.3.3   常见哈希算法","text":"

不难发现,以上介绍的简单哈希算法都比较“脆弱”,远远没有达到哈希算法的设计目标。例如,由于加法和异或满足交换律,因此加法哈希和异或哈希无法区分内容相同但顺序不同的字符串,这可能会加剧哈希冲突,并引起一些安全问题。

在实际中,我们通常会用一些标准哈希算法,例如 MD5、SHA-1、SHA-2 和 SHA-3 等。它们可以将任意长度的输入数据映射到恒定长度的哈希值。

近一个世纪以来,哈希算法处在不断升级与优化的过程中。一部分研究人员努力提升哈希算法的性能,另一部分研究人员和黑客则致力于寻找哈希算法的安全性问题。表 6-2 展示了在实际应用中常见的哈希算法。

表 6-2   常见的哈希算法

MD5 SHA-1 SHA-2 SHA-3 推出时间 1992 1995 2002 2008 输出长度 128 bit 160 bit 256/512 bit 224/256/384/512 bit 哈希冲突 较多 较多 很少 很少 安全等级 低,已被成功攻击 低,已被成功攻击 高 高 应用 已被弃用,仍用于数据完整性检查 已被弃用 加密货币交易验证、数字签名等 可用于替代 SHA-2","path":["第 6 章   哈希表","6.3   哈希算法"],"tags":[]},{"location":"chapter_hashing/hash_algorithm/#634","level":2,"title":"6.3.4   数据结构的哈希值","text":"

我们知道,哈希表的 key 可以是整数、小数或字符串等数据类型。编程语言通常会为这些数据类型提供内置的哈希算法,用于计算哈希表中的桶索引。以 Python 为例,我们可以调用 hash() 函数来计算各种数据类型的哈希值。

Tip

请注意,不同编程语言的内置哈希值计算函数的定义和方法不同。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby built_in_hash.py
num = 3\nhash_num = hash(num)\n# 整数 3 的哈希值为 3\n\nbol = True\nhash_bol = hash(bol)\n# 布尔量 True 的哈希值为 1\n\ndec = 3.14159\nhash_dec = hash(dec)\n# 小数 3.14159 的哈希值为 326484311674566659\n\nstr = \"Hello 算法\"\nhash_str = hash(str)\n# 字符串“Hello 算法”的哈希值为 4617003410720528961\n\ntup = (12836, \"小哈\")\nhash_tup = hash(tup)\n# 元组 (12836, '小哈') 的哈希值为 1029005403108185979\n\nobj = ListNode(0)\nhash_obj = hash(obj)\n# 节点对象 <ListNode object at 0x1058fd810> 的哈希值为 274267521\n
built_in_hash.cpp
int num = 3;\nsize_t hashNum = hash<int>()(num);\n// 整数 3 的哈希值为 3\n\nbool bol = true;\nsize_t hashBol = hash<bool>()(bol);\n// 布尔量 1 的哈希值为 1\n\ndouble dec = 3.14159;\nsize_t hashDec = hash<double>()(dec);\n// 小数 3.14159 的哈希值为 4614256650576692846\n\nstring str = \"Hello 算法\";\nsize_t hashStr = hash<string>()(str);\n// 字符串“Hello 算法”的哈希值为 15466937326284535026\n\n// 在 C++ 中,内置 std:hash() 仅提供基本数据类型的哈希值计算\n// 数组、对象的哈希值计算需要自行实现\n
built_in_hash.java
int num = 3;\nint hashNum = Integer.hashCode(num);\n// 整数 3 的哈希值为 3\n\nboolean bol = true;\nint hashBol = Boolean.hashCode(bol);\n// 布尔量 true 的哈希值为 1231\n\ndouble dec = 3.14159;\nint hashDec = Double.hashCode(dec);\n// 小数 3.14159 的哈希值为 -1340954729\n\nString str = \"Hello 算法\";\nint hashStr = str.hashCode();\n// 字符串“Hello 算法”的哈希值为 -727081396\n\nObject[] arr = { 12836, \"小哈\" };\nint hashTup = Arrays.hashCode(arr);\n// 数组 [12836, 小哈] 的哈希值为 1151158\n\nListNode obj = new ListNode(0);\nint hashObj = obj.hashCode();\n// 节点对象 utils.ListNode@7dc5e7b4 的哈希值为 2110121908\n
built_in_hash.cs
int num = 3;\nint hashNum = num.GetHashCode();\n// 整数 3 的哈希值为 3;\n\nbool bol = true;\nint hashBol = bol.GetHashCode();\n// 布尔量 true 的哈希值为 1;\n\ndouble dec = 3.14159;\nint hashDec = dec.GetHashCode();\n// 小数 3.14159 的哈希值为 -1340954729;\n\nstring str = \"Hello 算法\";\nint hashStr = str.GetHashCode();\n// 字符串“Hello 算法”的哈希值为 -586107568;\n\nobject[] arr = [12836, \"小哈\"];\nint hashTup = arr.GetHashCode();\n// 数组 [12836, 小哈] 的哈希值为 42931033;\n\nListNode obj = new(0);\nint hashObj = obj.GetHashCode();\n// 节点对象 0 的哈希值为 39053774;\n
built_in_hash.go
// Go 未提供内置 hash code 函数\n
built_in_hash.swift
let num = 3\nlet hashNum = num.hashValue\n// 整数 3 的哈希值为 9047044699613009734\n\nlet bol = true\nlet hashBol = bol.hashValue\n// 布尔量 true 的哈希值为 -4431640247352757451\n\nlet dec = 3.14159\nlet hashDec = dec.hashValue\n// 小数 3.14159 的哈希值为 -2465384235396674631\n\nlet str = \"Hello 算法\"\nlet hashStr = str.hashValue\n// 字符串“Hello 算法”的哈希值为 -7850626797806988787\n\nlet arr = [AnyHashable(12836), AnyHashable(\"小哈\")]\nlet hashTup = arr.hashValue\n// 数组 [AnyHashable(12836), AnyHashable(\"小哈\")] 的哈希值为 -2308633508154532996\n\nlet obj = ListNode(x: 0)\nlet hashObj = obj.hashValue\n// 节点对象 utils.ListNode 的哈希值为 -2434780518035996159\n
built_in_hash.js
// JavaScript 未提供内置 hash code 函数\n
built_in_hash.ts
// TypeScript 未提供内置 hash code 函数\n
built_in_hash.dart
int num = 3;\nint hashNum = num.hashCode;\n// 整数 3 的哈希值为 34803\n\nbool bol = true;\nint hashBol = bol.hashCode;\n// 布尔值 true 的哈希值为 1231\n\ndouble dec = 3.14159;\nint hashDec = dec.hashCode;\n// 小数 3.14159 的哈希值为 2570631074981783\n\nString str = \"Hello 算法\";\nint hashStr = str.hashCode;\n// 字符串“Hello 算法”的哈希值为 468167534\n\nList arr = [12836, \"小哈\"];\nint hashArr = arr.hashCode;\n// 数组 [12836, 小哈] 的哈希值为 976512528\n\nListNode obj = new ListNode(0);\nint hashObj = obj.hashCode;\n// 节点对象 Instance of 'ListNode' 的哈希值为 1033450432\n
built_in_hash.rs
use std::collections::hash_map::DefaultHasher;\nuse std::hash::{Hash, Hasher};\n\nlet num = 3;\nlet mut num_hasher = DefaultHasher::new();\nnum.hash(&mut num_hasher);\nlet hash_num = num_hasher.finish();\n// 整数 3 的哈希值为 568126464209439262\n\nlet bol = true;\nlet mut bol_hasher = DefaultHasher::new();\nbol.hash(&mut bol_hasher);\nlet hash_bol = bol_hasher.finish();\n// 布尔量 true 的哈希值为 4952851536318644461\n\nlet dec: f32 = 3.14159;\nlet mut dec_hasher = DefaultHasher::new();\ndec.to_bits().hash(&mut dec_hasher);\nlet hash_dec = dec_hasher.finish();\n// 小数 3.14159 的哈希值为 2566941990314602357\n\nlet str = \"Hello 算法\";\nlet mut str_hasher = DefaultHasher::new();\nstr.hash(&mut str_hasher);\nlet hash_str = str_hasher.finish();\n// 字符串“Hello 算法”的哈希值为 16092673739211250988\n\nlet arr = (&12836, &\"小哈\");\nlet mut tup_hasher = DefaultHasher::new();\narr.hash(&mut tup_hasher);\nlet hash_tup = tup_hasher.finish();\n// 元组 (12836, \"小哈\") 的哈希值为 1885128010422702749\n\nlet node = ListNode::new(42);\nlet mut hasher = DefaultHasher::new();\nnode.borrow().val.hash(&mut hasher);\nlet hash = hasher.finish();\n// 节点对象 RefCell { value: ListNode { val: 42, next: None } } 的哈希值为15387811073369036852\n
built_in_hash.c
// C 未提供内置 hash code 函数\n
built_in_hash.kt
val num = 3\nval hashNum = num.hashCode()\n// 整数 3 的哈希值为 3\n\nval bol = true\nval hashBol = bol.hashCode()\n// 布尔量 true 的哈希值为 1231\n\nval dec = 3.14159\nval hashDec = dec.hashCode()\n// 小数 3.14159 的哈希值为 -1340954729\n\nval str = \"Hello 算法\"\nval hashStr = str.hashCode()\n// 字符串“Hello 算法”的哈希值为 -727081396\n\nval arr = arrayOf<Any>(12836, \"小哈\")\nval hashTup = arr.hashCode()\n// 数组 [12836, 小哈] 的哈希值为 189568618\n\nval obj = ListNode(0)\nval hashObj = obj.hashCode()\n// 节点对象 utils.ListNode@1d81eb93 的哈希值为 495053715\n
built_in_hash.rb
num = 3\nhash_num = num.hash\n# 整数 3 的哈希值为 -4385856518450339636\n\nbol = true\nhash_bol = bol.hash\n# 布尔量 true 的哈希值为 -1617938112149317027\n\ndec = 3.14159\nhash_dec = dec.hash\n# 小数 3.14159 的哈希值为 -1479186995943067893\n\nstr = \"Hello 算法\"\nhash_str = str.hash\n# 字符串“Hello 算法”的哈希值为 -4075943250025831763\n\ntup = [12836, '小哈']\nhash_tup = tup.hash\n# 元组 (12836, '小哈') 的哈希值为 1999544809202288822\n\nobj = ListNode.new(0)\nhash_obj = obj.hash\n# 节点对象 #<ListNode:0x000078133140ab70> 的哈希值为 4302940560806366381\n
可视化运行

全屏观看 >

在许多编程语言中,只有不可变对象才可作为哈希表的 key 。假如我们将列表(动态数组)作为 key ,当列表的内容发生变化时,它的哈希值也随之改变,我们就无法在哈希表中查询到原先的 value 了。

虽然自定义对象(比如链表节点)的成员变量是可变的,但它是可哈希的。这是因为对象的哈希值通常是基于内存地址生成的,即使对象的内容发生了变化,但它的内存地址不变,哈希值仍然是不变的。

细心的你可能发现在不同控制台中运行程序时,输出的哈希值是不同的。这是因为 Python 解释器在每次启动时,都会为字符串哈希函数加入一个随机的盐(salt)值。这种做法可以有效防止 HashDoS 攻击,提升哈希算法的安全性。

","path":["第 6 章   哈希表","6.3   哈希算法"],"tags":[]},{"location":"chapter_hashing/hash_collision/","level":1,"title":"6.2   哈希冲突","text":"

上一节提到,通常情况下哈希函数的输入空间远大于输出空间,因此理论上哈希冲突是不可避免的。比如,输入空间为全体整数,输出空间为数组容量大小,则必然有多个整数映射至同一桶索引。

哈希冲突会导致查询结果错误,严重影响哈希表的可用性。为了解决该问题,每当遇到哈希冲突时,我们就进行哈希表扩容,直至冲突消失为止。此方法简单粗暴且有效,但效率太低,因为哈希表扩容需要进行大量的数据搬运与哈希值计算。为了提升效率,我们可以采用以下策略。

  1. 改良哈希表数据结构,使得哈希表可以在出现哈希冲突时正常工作。
  2. 仅在必要时,即当哈希冲突比较严重时,才执行扩容操作。

哈希表的结构改良方法主要包括“链式地址”和“开放寻址”。

","path":["第 6 章   哈希表","6.2   哈希冲突"],"tags":[]},{"location":"chapter_hashing/hash_collision/#621","level":2,"title":"6.2.1   链式地址","text":"

在原始哈希表中,每个桶仅能存储一个键值对。链式地址(separate chaining)将单个元素转换为链表,将键值对作为链表节点,将所有发生冲突的键值对都存储在同一链表中。图 6-5 展示了一个链式地址哈希表的例子。

图 6-5   链式地址哈希表

基于链式地址实现的哈希表的操作方法发生了以下变化。

链式地址存在以下局限性。

以下代码给出了链式地址哈希表的简单实现,需要注意两点。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map_chaining.py
class HashMapChaining:\n    \"\"\"链式地址哈希表\"\"\"\n\n    def __init__(self):\n        \"\"\"构造方法\"\"\"\n        self.size = 0  # 键值对数量\n        self.capacity = 4  # 哈希表容量\n        self.load_thres = 2.0 / 3.0  # 触发扩容的负载因子阈值\n        self.extend_ratio = 2  # 扩容倍数\n        self.buckets = [[] for _ in range(self.capacity)]  # 桶数组\n\n    def hash_func(self, key: int) -> int:\n        \"\"\"哈希函数\"\"\"\n        return key % self.capacity\n\n    def load_factor(self) -> float:\n        \"\"\"负载因子\"\"\"\n        return self.size / self.capacity\n\n    def get(self, key: int) -> str | None:\n        \"\"\"查询操作\"\"\"\n        index = self.hash_func(key)\n        bucket = self.buckets[index]\n        # 遍历桶,若找到 key ,则返回对应 val\n        for pair in bucket:\n            if pair.key == key:\n                return pair.val\n        # 若未找到 key ,则返回 None\n        return None\n\n    def put(self, key: int, val: str):\n        \"\"\"添加操作\"\"\"\n        # 当负载因子超过阈值时,执行扩容\n        if self.load_factor() > self.load_thres:\n            self.extend()\n        index = self.hash_func(key)\n        bucket = self.buckets[index]\n        # 遍历桶,若遇到指定 key ,则更新对应 val 并返回\n        for pair in bucket:\n            if pair.key == key:\n                pair.val = val\n                return\n        # 若无该 key ,则将键值对添加至尾部\n        pair = Pair(key, val)\n        bucket.append(pair)\n        self.size += 1\n\n    def remove(self, key: int):\n        \"\"\"删除操作\"\"\"\n        index = self.hash_func(key)\n        bucket = self.buckets[index]\n        # 遍历桶,从中删除键值对\n        for pair in bucket:\n            if pair.key == key:\n                bucket.remove(pair)\n                self.size -= 1\n                break\n\n    def extend(self):\n        \"\"\"扩容哈希表\"\"\"\n        # 暂存原哈希表\n        buckets = self.buckets\n        # 初始化扩容后的新哈希表\n        self.capacity *= self.extend_ratio\n        self.buckets = [[] for _ in range(self.capacity)]\n        self.size = 0\n        # 将键值对从原哈希表搬运至新哈希表\n        for bucket in buckets:\n            for pair in bucket:\n                self.put(pair.key, pair.val)\n\n    def print(self):\n        \"\"\"打印哈希表\"\"\"\n        for bucket in self.buckets:\n            res = []\n            for pair in bucket:\n                res.append(str(pair.key) + \" -> \" + pair.val)\n            print(res)\n
hash_map_chaining.cpp
/* 链式地址哈希表 */\nclass HashMapChaining {\n  private:\n    int size;                       // 键值对数量\n    int capacity;                   // 哈希表容量\n    double loadThres;               // 触发扩容的负载因子阈值\n    int extendRatio;                // 扩容倍数\n    vector<vector<Pair *>> buckets; // 桶数组\n\n  public:\n    /* 构造方法 */\n    HashMapChaining() : size(0), capacity(4), loadThres(2.0 / 3.0), extendRatio(2) {\n        buckets.resize(capacity);\n    }\n\n    /* 析构方法 */\n    ~HashMapChaining() {\n        for (auto &bucket : buckets) {\n            for (Pair *pair : bucket) {\n                // 释放内存\n                delete pair;\n            }\n        }\n    }\n\n    /* 哈希函数 */\n    int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* 负载因子 */\n    double loadFactor() {\n        return (double)size / (double)capacity;\n    }\n\n    /* 查询操作 */\n    string get(int key) {\n        int index = hashFunc(key);\n        // 遍历桶,若找到 key ,则返回对应 val\n        for (Pair *pair : buckets[index]) {\n            if (pair->key == key) {\n                return pair->val;\n            }\n        }\n        // 若未找到 key ,则返回空字符串\n        return \"\";\n    }\n\n    /* 添加操作 */\n    void put(int key, string val) {\n        // 当负载因子超过阈值时,执行扩容\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        int index = hashFunc(key);\n        // 遍历桶,若遇到指定 key ,则更新对应 val 并返回\n        for (Pair *pair : buckets[index]) {\n            if (pair->key == key) {\n                pair->val = val;\n                return;\n            }\n        }\n        // 若无该 key ,则将键值对添加至尾部\n        buckets[index].push_back(new Pair(key, val));\n        size++;\n    }\n\n    /* 删除操作 */\n    void remove(int key) {\n        int index = hashFunc(key);\n        auto &bucket = buckets[index];\n        // 遍历桶,从中删除键值对\n        for (int i = 0; i < bucket.size(); i++) {\n            if (bucket[i]->key == key) {\n                Pair *tmp = bucket[i];\n                bucket.erase(bucket.begin() + i); // 从中删除键值对\n                delete tmp;                       // 释放内存\n                size--;\n                return;\n            }\n        }\n    }\n\n    /* 扩容哈希表 */\n    void extend() {\n        // 暂存原哈希表\n        vector<vector<Pair *>> bucketsTmp = buckets;\n        // 初始化扩容后的新哈希表\n        capacity *= extendRatio;\n        buckets.clear();\n        buckets.resize(capacity);\n        size = 0;\n        // 将键值对从原哈希表搬运至新哈希表\n        for (auto &bucket : bucketsTmp) {\n            for (Pair *pair : bucket) {\n                put(pair->key, pair->val);\n                // 释放内存\n                delete pair;\n            }\n        }\n    }\n\n    /* 打印哈希表 */\n    void print() {\n        for (auto &bucket : buckets) {\n            cout << \"[\";\n            for (Pair *pair : bucket) {\n                cout << pair->key << \" -> \" << pair->val << \", \";\n            }\n            cout << \"]\\n\";\n        }\n    }\n};\n
hash_map_chaining.java
/* 链式地址哈希表 */\nclass HashMapChaining {\n    int size; // 键值对数量\n    int capacity; // 哈希表容量\n    double loadThres; // 触发扩容的负载因子阈值\n    int extendRatio; // 扩容倍数\n    List<List<Pair>> buckets; // 桶数组\n\n    /* 构造方法 */\n    public HashMapChaining() {\n        size = 0;\n        capacity = 4;\n        loadThres = 2.0 / 3.0;\n        extendRatio = 2;\n        buckets = new ArrayList<>(capacity);\n        for (int i = 0; i < capacity; i++) {\n            buckets.add(new ArrayList<>());\n        }\n    }\n\n    /* 哈希函数 */\n    int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* 负载因子 */\n    double loadFactor() {\n        return (double) size / capacity;\n    }\n\n    /* 查询操作 */\n    String get(int key) {\n        int index = hashFunc(key);\n        List<Pair> bucket = buckets.get(index);\n        // 遍历桶,若找到 key ,则返回对应 val\n        for (Pair pair : bucket) {\n            if (pair.key == key) {\n                return pair.val;\n            }\n        }\n        // 若未找到 key ,则返回 null\n        return null;\n    }\n\n    /* 添加操作 */\n    void put(int key, String val) {\n        // 当负载因子超过阈值时,执行扩容\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        int index = hashFunc(key);\n        List<Pair> bucket = buckets.get(index);\n        // 遍历桶,若遇到指定 key ,则更新对应 val 并返回\n        for (Pair pair : bucket) {\n            if (pair.key == key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // 若无该 key ,则将键值对添加至尾部\n        Pair pair = new Pair(key, val);\n        bucket.add(pair);\n        size++;\n    }\n\n    /* 删除操作 */\n    void remove(int key) {\n        int index = hashFunc(key);\n        List<Pair> bucket = buckets.get(index);\n        // 遍历桶,从中删除键值对\n        for (Pair pair : bucket) {\n            if (pair.key == key) {\n                bucket.remove(pair);\n                size--;\n                break;\n            }\n        }\n    }\n\n    /* 扩容哈希表 */\n    void extend() {\n        // 暂存原哈希表\n        List<List<Pair>> bucketsTmp = buckets;\n        // 初始化扩容后的新哈希表\n        capacity *= extendRatio;\n        buckets = new ArrayList<>(capacity);\n        for (int i = 0; i < capacity; i++) {\n            buckets.add(new ArrayList<>());\n        }\n        size = 0;\n        // 将键值对从原哈希表搬运至新哈希表\n        for (List<Pair> bucket : bucketsTmp) {\n            for (Pair pair : bucket) {\n                put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* 打印哈希表 */\n    void print() {\n        for (List<Pair> bucket : buckets) {\n            List<String> res = new ArrayList<>();\n            for (Pair pair : bucket) {\n                res.add(pair.key + \" -> \" + pair.val);\n            }\n            System.out.println(res);\n        }\n    }\n}\n
hash_map_chaining.cs
/* 链式地址哈希表 */\nclass HashMapChaining {\n    int size; // 键值对数量\n    int capacity; // 哈希表容量\n    double loadThres; // 触发扩容的负载因子阈值\n    int extendRatio; // 扩容倍数\n    List<List<Pair>> buckets; // 桶数组\n\n    /* 构造方法 */\n    public HashMapChaining() {\n        size = 0;\n        capacity = 4;\n        loadThres = 2.0 / 3.0;\n        extendRatio = 2;\n        buckets = new List<List<Pair>>(capacity);\n        for (int i = 0; i < capacity; i++) {\n            buckets.Add([]);\n        }\n    }\n\n    /* 哈希函数 */\n    int HashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* 负载因子 */\n    double LoadFactor() {\n        return (double)size / capacity;\n    }\n\n    /* 查询操作 */\n    public string? Get(int key) {\n        int index = HashFunc(key);\n        // 遍历桶,若找到 key ,则返回对应 val\n        foreach (Pair pair in buckets[index]) {\n            if (pair.key == key) {\n                return pair.val;\n            }\n        }\n        // 若未找到 key ,则返回 null\n        return null;\n    }\n\n    /* 添加操作 */\n    public void Put(int key, string val) {\n        // 当负载因子超过阈值时,执行扩容\n        if (LoadFactor() > loadThres) {\n            Extend();\n        }\n        int index = HashFunc(key);\n        // 遍历桶,若遇到指定 key ,则更新对应 val 并返回\n        foreach (Pair pair in buckets[index]) {\n            if (pair.key == key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // 若无该 key ,则将键值对添加至尾部\n        buckets[index].Add(new Pair(key, val));\n        size++;\n    }\n\n    /* 删除操作 */\n    public void Remove(int key) {\n        int index = HashFunc(key);\n        // 遍历桶,从中删除键值对\n        foreach (Pair pair in buckets[index].ToList()) {\n            if (pair.key == key) {\n                buckets[index].Remove(pair);\n                size--;\n                break;\n            }\n        }\n    }\n\n    /* 扩容哈希表 */\n    void Extend() {\n        // 暂存原哈希表\n        List<List<Pair>> bucketsTmp = buckets;\n        // 初始化扩容后的新哈希表\n        capacity *= extendRatio;\n        buckets = new List<List<Pair>>(capacity);\n        for (int i = 0; i < capacity; i++) {\n            buckets.Add([]);\n        }\n        size = 0;\n        // 将键值对从原哈希表搬运至新哈希表\n        foreach (List<Pair> bucket in bucketsTmp) {\n            foreach (Pair pair in bucket) {\n                Put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* 打印哈希表 */\n    public void Print() {\n        foreach (List<Pair> bucket in buckets) {\n            List<string> res = [];\n            foreach (Pair pair in bucket) {\n                res.Add(pair.key + \" -> \" + pair.val);\n            }\n            foreach (string kv in res) {\n                Console.WriteLine(kv);\n            }\n        }\n    }\n}\n
hash_map_chaining.go
/* 链式地址哈希表 */\ntype hashMapChaining struct {\n    size        int      // 键值对数量\n    capacity    int      // 哈希表容量\n    loadThres   float64  // 触发扩容的负载因子阈值\n    extendRatio int      // 扩容倍数\n    buckets     [][]pair // 桶数组\n}\n\n/* 构造方法 */\nfunc newHashMapChaining() *hashMapChaining {\n    buckets := make([][]pair, 4)\n    for i := 0; i < 4; i++ {\n        buckets[i] = make([]pair, 0)\n    }\n    return &hashMapChaining{\n        size:        0,\n        capacity:    4,\n        loadThres:   2.0 / 3.0,\n        extendRatio: 2,\n        buckets:     buckets,\n    }\n}\n\n/* 哈希函数 */\nfunc (m *hashMapChaining) hashFunc(key int) int {\n    return key % m.capacity\n}\n\n/* 负载因子 */\nfunc (m *hashMapChaining) loadFactor() float64 {\n    return float64(m.size) / float64(m.capacity)\n}\n\n/* 查询操作 */\nfunc (m *hashMapChaining) get(key int) string {\n    idx := m.hashFunc(key)\n    bucket := m.buckets[idx]\n    // 遍历桶,若找到 key ,则返回对应 val\n    for _, p := range bucket {\n        if p.key == key {\n            return p.val\n        }\n    }\n    // 若未找到 key ,则返回空字符串\n    return \"\"\n}\n\n/* 添加操作 */\nfunc (m *hashMapChaining) put(key int, val string) {\n    // 当负载因子超过阈值时,执行扩容\n    if m.loadFactor() > m.loadThres {\n        m.extend()\n    }\n    idx := m.hashFunc(key)\n    // 遍历桶,若遇到指定 key ,则更新对应 val 并返回\n    for i := range m.buckets[idx] {\n        if m.buckets[idx][i].key == key {\n            m.buckets[idx][i].val = val\n            return\n        }\n    }\n    // 若无该 key ,则将键值对添加至尾部\n    p := pair{\n        key: key,\n        val: val,\n    }\n    m.buckets[idx] = append(m.buckets[idx], p)\n    m.size += 1\n}\n\n/* 删除操作 */\nfunc (m *hashMapChaining) remove(key int) {\n    idx := m.hashFunc(key)\n    // 遍历桶,从中删除键值对\n    for i, p := range m.buckets[idx] {\n        if p.key == key {\n            // 切片删除\n            m.buckets[idx] = append(m.buckets[idx][:i], m.buckets[idx][i+1:]...)\n            m.size -= 1\n            break\n        }\n    }\n}\n\n/* 扩容哈希表 */\nfunc (m *hashMapChaining) extend() {\n    // 暂存原哈希表\n    tmpBuckets := make([][]pair, len(m.buckets))\n    for i := 0; i < len(m.buckets); i++ {\n        tmpBuckets[i] = make([]pair, len(m.buckets[i]))\n        copy(tmpBuckets[i], m.buckets[i])\n    }\n    // 初始化扩容后的新哈希表\n    m.capacity *= m.extendRatio\n    m.buckets = make([][]pair, m.capacity)\n    for i := 0; i < m.capacity; i++ {\n        m.buckets[i] = make([]pair, 0)\n    }\n    m.size = 0\n    // 将键值对从原哈希表搬运至新哈希表\n    for _, bucket := range tmpBuckets {\n        for _, p := range bucket {\n            m.put(p.key, p.val)\n        }\n    }\n}\n\n/* 打印哈希表 */\nfunc (m *hashMapChaining) print() {\n    var builder strings.Builder\n\n    for _, bucket := range m.buckets {\n        builder.WriteString(\"[\")\n        for _, p := range bucket {\n            builder.WriteString(strconv.Itoa(p.key) + \" -> \" + p.val + \" \")\n        }\n        builder.WriteString(\"]\")\n        fmt.Println(builder.String())\n        builder.Reset()\n    }\n}\n
hash_map_chaining.swift
/* 链式地址哈希表 */\nclass HashMapChaining {\n    var size: Int // 键值对数量\n    var capacity: Int // 哈希表容量\n    var loadThres: Double // 触发扩容的负载因子阈值\n    var extendRatio: Int // 扩容倍数\n    var buckets: [[Pair]] // 桶数组\n\n    /* 构造方法 */\n    init() {\n        size = 0\n        capacity = 4\n        loadThres = 2.0 / 3.0\n        extendRatio = 2\n        buckets = Array(repeating: [], count: capacity)\n    }\n\n    /* 哈希函数 */\n    func hashFunc(key: Int) -> Int {\n        key % capacity\n    }\n\n    /* 负载因子 */\n    func loadFactor() -> Double {\n        Double(size) / Double(capacity)\n    }\n\n    /* 查询操作 */\n    func get(key: Int) -> String? {\n        let index = hashFunc(key: key)\n        let bucket = buckets[index]\n        // 遍历桶,若找到 key ,则返回对应 val\n        for pair in bucket {\n            if pair.key == key {\n                return pair.val\n            }\n        }\n        // 若未找到 key ,则返回 nil\n        return nil\n    }\n\n    /* 添加操作 */\n    func put(key: Int, val: String) {\n        // 当负载因子超过阈值时,执行扩容\n        if loadFactor() > loadThres {\n            extend()\n        }\n        let index = hashFunc(key: key)\n        let bucket = buckets[index]\n        // 遍历桶,若遇到指定 key ,则更新对应 val 并返回\n        for pair in bucket {\n            if pair.key == key {\n                pair.val = val\n                return\n            }\n        }\n        // 若无该 key ,则将键值对添加至尾部\n        let pair = Pair(key: key, val: val)\n        buckets[index].append(pair)\n        size += 1\n    }\n\n    /* 删除操作 */\n    func remove(key: Int) {\n        let index = hashFunc(key: key)\n        let bucket = buckets[index]\n        // 遍历桶,从中删除键值对\n        for (pairIndex, pair) in bucket.enumerated() {\n            if pair.key == key {\n                buckets[index].remove(at: pairIndex)\n                size -= 1\n                break\n            }\n        }\n    }\n\n    /* 扩容哈希表 */\n    func extend() {\n        // 暂存原哈希表\n        let bucketsTmp = buckets\n        // 初始化扩容后的新哈希表\n        capacity *= extendRatio\n        buckets = Array(repeating: [], count: capacity)\n        size = 0\n        // 将键值对从原哈希表搬运至新哈希表\n        for bucket in bucketsTmp {\n            for pair in bucket {\n                put(key: pair.key, val: pair.val)\n            }\n        }\n    }\n\n    /* 打印哈希表 */\n    func print() {\n        for bucket in buckets {\n            let res = bucket.map { \"\\($0.key) -> \\($0.val)\" }\n            Swift.print(res)\n        }\n    }\n}\n
hash_map_chaining.js
/* 链式地址哈希表 */\nclass HashMapChaining {\n    #size; // 键值对数量\n    #capacity; // 哈希表容量\n    #loadThres; // 触发扩容的负载因子阈值\n    #extendRatio; // 扩容倍数\n    #buckets; // 桶数组\n\n    /* 构造方法 */\n    constructor() {\n        this.#size = 0;\n        this.#capacity = 4;\n        this.#loadThres = 2.0 / 3.0;\n        this.#extendRatio = 2;\n        this.#buckets = new Array(this.#capacity).fill(null).map((x) => []);\n    }\n\n    /* 哈希函数 */\n    #hashFunc(key) {\n        return key % this.#capacity;\n    }\n\n    /* 负载因子 */\n    #loadFactor() {\n        return this.#size / this.#capacity;\n    }\n\n    /* 查询操作 */\n    get(key) {\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // 遍历桶,若找到 key ,则返回对应 val\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                return pair.val;\n            }\n        }\n        // 若未找到 key ,则返回 null\n        return null;\n    }\n\n    /* 添加操作 */\n    put(key, val) {\n        // 当负载因子超过阈值时,执行扩容\n        if (this.#loadFactor() > this.#loadThres) {\n            this.#extend();\n        }\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // 遍历桶,若遇到指定 key ,则更新对应 val 并返回\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // 若无该 key ,则将键值对添加至尾部\n        const pair = new Pair(key, val);\n        bucket.push(pair);\n        this.#size++;\n    }\n\n    /* 删除操作 */\n    remove(key) {\n        const index = this.#hashFunc(key);\n        let bucket = this.#buckets[index];\n        // 遍历桶,从中删除键值对\n        for (let i = 0; i < bucket.length; i++) {\n            if (bucket[i].key === key) {\n                bucket.splice(i, 1);\n                this.#size--;\n                break;\n            }\n        }\n    }\n\n    /* 扩容哈希表 */\n    #extend() {\n        // 暂存原哈希表\n        const bucketsTmp = this.#buckets;\n        // 初始化扩容后的新哈希表\n        this.#capacity *= this.#extendRatio;\n        this.#buckets = new Array(this.#capacity).fill(null).map((x) => []);\n        this.#size = 0;\n        // 将键值对从原哈希表搬运至新哈希表\n        for (const bucket of bucketsTmp) {\n            for (const pair of bucket) {\n                this.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* 打印哈希表 */\n    print() {\n        for (const bucket of this.#buckets) {\n            let res = [];\n            for (const pair of bucket) {\n                res.push(pair.key + ' -> ' + pair.val);\n            }\n            console.log(res);\n        }\n    }\n}\n
hash_map_chaining.ts
/* 链式地址哈希表 */\nclass HashMapChaining {\n    #size: number; // 键值对数量\n    #capacity: number; // 哈希表容量\n    #loadThres: number; // 触发扩容的负载因子阈值\n    #extendRatio: number; // 扩容倍数\n    #buckets: Pair[][]; // 桶数组\n\n    /* 构造方法 */\n    constructor() {\n        this.#size = 0;\n        this.#capacity = 4;\n        this.#loadThres = 2.0 / 3.0;\n        this.#extendRatio = 2;\n        this.#buckets = new Array(this.#capacity).fill(null).map((x) => []);\n    }\n\n    /* 哈希函数 */\n    #hashFunc(key: number): number {\n        return key % this.#capacity;\n    }\n\n    /* 负载因子 */\n    #loadFactor(): number {\n        return this.#size / this.#capacity;\n    }\n\n    /* 查询操作 */\n    get(key: number): string | null {\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // 遍历桶,若找到 key ,则返回对应 val\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                return pair.val;\n            }\n        }\n        // 若未找到 key ,则返回 null\n        return null;\n    }\n\n    /* 添加操作 */\n    put(key: number, val: string): void {\n        // 当负载因子超过阈值时,执行扩容\n        if (this.#loadFactor() > this.#loadThres) {\n            this.#extend();\n        }\n        const index = this.#hashFunc(key);\n        const bucket = this.#buckets[index];\n        // 遍历桶,若遇到指定 key ,则更新对应 val 并返回\n        for (const pair of bucket) {\n            if (pair.key === key) {\n                pair.val = val;\n                return;\n            }\n        }\n        // 若无该 key ,则将键值对添加至尾部\n        const pair = new Pair(key, val);\n        bucket.push(pair);\n        this.#size++;\n    }\n\n    /* 删除操作 */\n    remove(key: number): void {\n        const index = this.#hashFunc(key);\n        let bucket = this.#buckets[index];\n        // 遍历桶,从中删除键值对\n        for (let i = 0; i < bucket.length; i++) {\n            if (bucket[i].key === key) {\n                bucket.splice(i, 1);\n                this.#size--;\n                break;\n            }\n        }\n    }\n\n    /* 扩容哈希表 */\n    #extend(): void {\n        // 暂存原哈希表\n        const bucketsTmp = this.#buckets;\n        // 初始化扩容后的新哈希表\n        this.#capacity *= this.#extendRatio;\n        this.#buckets = new Array(this.#capacity).fill(null).map((x) => []);\n        this.#size = 0;\n        // 将键值对从原哈希表搬运至新哈希表\n        for (const bucket of bucketsTmp) {\n            for (const pair of bucket) {\n                this.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* 打印哈希表 */\n    print(): void {\n        for (const bucket of this.#buckets) {\n            let res = [];\n            for (const pair of bucket) {\n                res.push(pair.key + ' -> ' + pair.val);\n            }\n            console.log(res);\n        }\n    }\n}\n
hash_map_chaining.dart
/* 链式地址哈希表 */\nclass HashMapChaining {\n  late int size; // 键值对数量\n  late int capacity; // 哈希表容量\n  late double loadThres; // 触发扩容的负载因子阈值\n  late int extendRatio; // 扩容倍数\n  late List<List<Pair>> buckets; // 桶数组\n\n  /* 构造方法 */\n  HashMapChaining() {\n    size = 0;\n    capacity = 4;\n    loadThres = 2.0 / 3.0;\n    extendRatio = 2;\n    buckets = List.generate(capacity, (_) => []);\n  }\n\n  /* 哈希函数 */\n  int hashFunc(int key) {\n    return key % capacity;\n  }\n\n  /* 负载因子 */\n  double loadFactor() {\n    return size / capacity;\n  }\n\n  /* 查询操作 */\n  String? get(int key) {\n    int index = hashFunc(key);\n    List<Pair> bucket = buckets[index];\n    // 遍历桶,若找到 key ,则返回对应 val\n    for (Pair pair in bucket) {\n      if (pair.key == key) {\n        return pair.val;\n      }\n    }\n    // 若未找到 key ,则返回 null\n    return null;\n  }\n\n  /* 添加操作 */\n  void put(int key, String val) {\n    // 当负载因子超过阈值时,执行扩容\n    if (loadFactor() > loadThres) {\n      extend();\n    }\n    int index = hashFunc(key);\n    List<Pair> bucket = buckets[index];\n    // 遍历桶,若遇到指定 key ,则更新对应 val 并返回\n    for (Pair pair in bucket) {\n      if (pair.key == key) {\n        pair.val = val;\n        return;\n      }\n    }\n    // 若无该 key ,则将键值对添加至尾部\n    Pair pair = Pair(key, val);\n    bucket.add(pair);\n    size++;\n  }\n\n  /* 删除操作 */\n  void remove(int key) {\n    int index = hashFunc(key);\n    List<Pair> bucket = buckets[index];\n    // 遍历桶,从中删除键值对\n    for (Pair pair in bucket) {\n      if (pair.key == key) {\n        bucket.remove(pair);\n        size--;\n        break;\n      }\n    }\n  }\n\n  /* 扩容哈希表 */\n  void extend() {\n    // 暂存原哈希表\n    List<List<Pair>> bucketsTmp = buckets;\n    // 初始化扩容后的新哈希表\n    capacity *= extendRatio;\n    buckets = List.generate(capacity, (_) => []);\n    size = 0;\n    // 将键值对从原哈希表搬运至新哈希表\n    for (List<Pair> bucket in bucketsTmp) {\n      for (Pair pair in bucket) {\n        put(pair.key, pair.val);\n      }\n    }\n  }\n\n  /* 打印哈希表 */\n  void printHashMap() {\n    for (List<Pair> bucket in buckets) {\n      List<String> res = [];\n      for (Pair pair in bucket) {\n        res.add(\"${pair.key} -> ${pair.val}\");\n      }\n      print(res);\n    }\n  }\n}\n
hash_map_chaining.rs
/* 链式地址哈希表 */\nstruct HashMapChaining {\n    size: usize,\n    capacity: usize,\n    load_thres: f32,\n    extend_ratio: usize,\n    buckets: Vec<Vec<Pair>>,\n}\n\nimpl HashMapChaining {\n    /* 构造方法 */\n    fn new() -> Self {\n        Self {\n            size: 0,\n            capacity: 4,\n            load_thres: 2.0 / 3.0,\n            extend_ratio: 2,\n            buckets: vec![vec![]; 4],\n        }\n    }\n\n    /* 哈希函数 */\n    fn hash_func(&self, key: i32) -> usize {\n        key as usize % self.capacity\n    }\n\n    /* 负载因子 */\n    fn load_factor(&self) -> f32 {\n        self.size as f32 / self.capacity as f32\n    }\n\n    /* 删除操作 */\n    fn remove(&mut self, key: i32) -> Option<String> {\n        let index = self.hash_func(key);\n\n        // 遍历桶,从中删除键值对\n        for (i, p) in self.buckets[index].iter_mut().enumerate() {\n            if p.key == key {\n                let pair = self.buckets[index].remove(i);\n                self.size -= 1;\n                return Some(pair.val);\n            }\n        }\n\n        // 若未找到 key ,则返回 None\n        None\n    }\n\n    /* 扩容哈希表 */\n    fn extend(&mut self) {\n        // 暂存原哈希表\n        let buckets_tmp = std::mem::take(&mut self.buckets);\n\n        // 初始化扩容后的新哈希表\n        self.capacity *= self.extend_ratio;\n        self.buckets = vec![Vec::new(); self.capacity as usize];\n        self.size = 0;\n\n        // 将键值对从原哈希表搬运至新哈希表\n        for bucket in buckets_tmp {\n            for pair in bucket {\n                self.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* 打印哈希表 */\n    fn print(&self) {\n        for bucket in &self.buckets {\n            let mut res = Vec::new();\n            for pair in bucket {\n                res.push(format!(\"{} -> {}\", pair.key, pair.val));\n            }\n            println!(\"{:?}\", res);\n        }\n    }\n\n    /* 添加操作 */\n    fn put(&mut self, key: i32, val: String) {\n        // 当负载因子超过阈值时,执行扩容\n        if self.load_factor() > self.load_thres {\n            self.extend();\n        }\n\n        let index = self.hash_func(key);\n\n        // 遍历桶,若遇到指定 key ,则更新对应 val 并返回\n        for pair in self.buckets[index].iter_mut() {\n            if pair.key == key {\n                pair.val = val;\n                return;\n            }\n        }\n\n        // 若无该 key ,则将键值对添加至尾部\n        let pair = Pair { key, val };\n        self.buckets[index].push(pair);\n        self.size += 1;\n    }\n\n    /* 查询操作 */\n    fn get(&self, key: i32) -> Option<&str> {\n        let index = self.hash_func(key);\n\n        // 遍历桶,若找到 key ,则返回对应 val\n        for pair in self.buckets[index].iter() {\n            if pair.key == key {\n                return Some(&pair.val);\n            }\n        }\n\n        // 若未找到 key ,则返回 None\n        None\n    }\n}\n
hash_map_chaining.c
/* 链表节点 */\ntypedef struct Node {\n    Pair *pair;\n    struct Node *next;\n} Node;\n\n/* 链式地址哈希表 */\ntypedef struct {\n    int size;         // 键值对数量\n    int capacity;     // 哈希表容量\n    double loadThres; // 触发扩容的负载因子阈值\n    int extendRatio;  // 扩容倍数\n    Node **buckets;   // 桶数组\n} HashMapChaining;\n\n/* 构造函数 */\nHashMapChaining *newHashMapChaining() {\n    HashMapChaining *hashMap = (HashMapChaining *)malloc(sizeof(HashMapChaining));\n    hashMap->size = 0;\n    hashMap->capacity = 4;\n    hashMap->loadThres = 2.0 / 3.0;\n    hashMap->extendRatio = 2;\n    hashMap->buckets = (Node **)malloc(hashMap->capacity * sizeof(Node *));\n    for (int i = 0; i < hashMap->capacity; i++) {\n        hashMap->buckets[i] = NULL;\n    }\n    return hashMap;\n}\n\n/* 析构函数 */\nvoid delHashMapChaining(HashMapChaining *hashMap) {\n    for (int i = 0; i < hashMap->capacity; i++) {\n        Node *cur = hashMap->buckets[i];\n        while (cur) {\n            Node *tmp = cur;\n            cur = cur->next;\n            free(tmp->pair);\n            free(tmp);\n        }\n    }\n    free(hashMap->buckets);\n    free(hashMap);\n}\n\n/* 哈希函数 */\nint hashFunc(HashMapChaining *hashMap, int key) {\n    return key % hashMap->capacity;\n}\n\n/* 负载因子 */\ndouble loadFactor(HashMapChaining *hashMap) {\n    return (double)hashMap->size / (double)hashMap->capacity;\n}\n\n/* 查询操作 */\nchar *get(HashMapChaining *hashMap, int key) {\n    int index = hashFunc(hashMap, key);\n    // 遍历桶,若找到 key ,则返回对应 val\n    Node *cur = hashMap->buckets[index];\n    while (cur) {\n        if (cur->pair->key == key) {\n            return cur->pair->val;\n        }\n        cur = cur->next;\n    }\n    return \"\"; // 若未找到 key ,则返回空字符串\n}\n\n/* 添加操作 */\nvoid put(HashMapChaining *hashMap, int key, const char *val) {\n    // 当负载因子超过阈值时,执行扩容\n    if (loadFactor(hashMap) > hashMap->loadThres) {\n        extend(hashMap);\n    }\n    int index = hashFunc(hashMap, key);\n    // 遍历桶,若遇到指定 key ,则更新对应 val 并返回\n    Node *cur = hashMap->buckets[index];\n    while (cur) {\n        if (cur->pair->key == key) {\n            strcpy(cur->pair->val, val); // 若遇到指定 key ,则更新对应 val 并返回\n            return;\n        }\n        cur = cur->next;\n    }\n    // 若无该 key ,则将键值对添加至链表头部\n    Pair *newPair = (Pair *)malloc(sizeof(Pair));\n    newPair->key = key;\n    strcpy(newPair->val, val);\n    Node *newNode = (Node *)malloc(sizeof(Node));\n    newNode->pair = newPair;\n    newNode->next = hashMap->buckets[index];\n    hashMap->buckets[index] = newNode;\n    hashMap->size++;\n}\n\n/* 扩容哈希表 */\nvoid extend(HashMapChaining *hashMap) {\n    // 暂存原哈希表\n    int oldCapacity = hashMap->capacity;\n    Node **oldBuckets = hashMap->buckets;\n    // 初始化扩容后的新哈希表\n    hashMap->capacity *= hashMap->extendRatio;\n    hashMap->buckets = (Node **)malloc(hashMap->capacity * sizeof(Node *));\n    for (int i = 0; i < hashMap->capacity; i++) {\n        hashMap->buckets[i] = NULL;\n    }\n    hashMap->size = 0;\n    // 将键值对从原哈希表搬运至新哈希表\n    for (int i = 0; i < oldCapacity; i++) {\n        Node *cur = oldBuckets[i];\n        while (cur) {\n            put(hashMap, cur->pair->key, cur->pair->val);\n            Node *temp = cur;\n            cur = cur->next;\n            // 释放内存\n            free(temp->pair);\n            free(temp);\n        }\n    }\n\n    free(oldBuckets);\n}\n\n/* 删除操作 */\nvoid removeItem(HashMapChaining *hashMap, int key) {\n    int index = hashFunc(hashMap, key);\n    Node *cur = hashMap->buckets[index];\n    Node *pre = NULL;\n    while (cur) {\n        if (cur->pair->key == key) {\n            // 从中删除键值对\n            if (pre) {\n                pre->next = cur->next;\n            } else {\n                hashMap->buckets[index] = cur->next;\n            }\n            // 释放内存\n            free(cur->pair);\n            free(cur);\n            hashMap->size--;\n            return;\n        }\n        pre = cur;\n        cur = cur->next;\n    }\n}\n\n/* 打印哈希表 */\nvoid print(HashMapChaining *hashMap) {\n    for (int i = 0; i < hashMap->capacity; i++) {\n        Node *cur = hashMap->buckets[i];\n        printf(\"[\");\n        while (cur) {\n            printf(\"%d -> %s, \", cur->pair->key, cur->pair->val);\n            cur = cur->next;\n        }\n        printf(\"]\\n\");\n    }\n}\n
hash_map_chaining.kt
/* 链式地址哈希表 */\nclass HashMapChaining {\n    var size: Int // 键值对数量\n    var capacity: Int // 哈希表容量\n    val loadThres: Double // 触发扩容的负载因子阈值\n    val extendRatio: Int // 扩容倍数\n    var buckets: MutableList<MutableList<Pair>> // 桶数组\n\n    /* 构造方法 */\n    init {\n        size = 0\n        capacity = 4\n        loadThres = 2.0 / 3.0\n        extendRatio = 2\n        buckets = mutableListOf()\n        for (i in 0..<capacity) {\n            buckets.add(mutableListOf())\n        }\n    }\n\n    /* 哈希函数 */\n    fun hashFunc(key: Int): Int {\n        return key % capacity\n    }\n\n    /* 负载因子 */\n    fun loadFactor(): Double {\n        return (size / capacity).toDouble()\n    }\n\n    /* 查询操作 */\n    fun get(key: Int): String? {\n        val index = hashFunc(key)\n        val bucket = buckets[index]\n        // 遍历桶,若找到 key ,则返回对应 val\n        for (pair in bucket) {\n            if (pair.key == key) return pair._val\n        }\n        // 若未找到 key ,则返回 null\n        return null\n    }\n\n    /* 添加操作 */\n    fun put(key: Int, _val: String) {\n        // 当负载因子超过阈值时,执行扩容\n        if (loadFactor() > loadThres) {\n            extend()\n        }\n        val index = hashFunc(key)\n        val bucket = buckets[index]\n        // 遍历桶,若遇到指定 key ,则更新对应 val 并返回\n        for (pair in bucket) {\n            if (pair.key == key) {\n                pair._val = _val\n                return\n            }\n        }\n        // 若无该 key ,则将键值对添加至尾部\n        val pair = Pair(key, _val)\n        bucket.add(pair)\n        size++\n    }\n\n    /* 删除操作 */\n    fun remove(key: Int) {\n        val index = hashFunc(key)\n        val bucket = buckets[index]\n        // 遍历桶,从中删除键值对\n        for (pair in bucket) {\n            if (pair.key == key) {\n                bucket.remove(pair)\n                size--\n                break\n            }\n        }\n    }\n\n    /* 扩容哈希表 */\n    fun extend() {\n        // 暂存原哈希表\n        val bucketsTmp = buckets\n        // 初始化扩容后的新哈希表\n        capacity *= extendRatio\n        // mutablelist 无固定大小\n        buckets = mutableListOf()\n        for (i in 0..<capacity) {\n            buckets.add(mutableListOf())\n        }\n        size = 0\n        // 将键值对从原哈希表搬运至新哈希表\n        for (bucket in bucketsTmp) {\n            for (pair in bucket) {\n                put(pair.key, pair._val)\n            }\n        }\n    }\n\n    /* 打印哈希表 */\n    fun print() {\n        for (bucket in buckets) {\n            val res = mutableListOf<String>()\n            for (pair in bucket) {\n                val k = pair.key\n                val v = pair._val\n                res.add(\"$k -> $v\")\n            }\n            println(res)\n        }\n    }\n}\n
hash_map_chaining.rb
### 键式地址哈希表 ###\nclass HashMapChaining\n  ### 构造方法 ###\n  def initialize\n    @size = 0 # 键值对数量\n    @capacity = 4 # 哈希表容量\n    @load_thres = 2.0 / 3.0 # 触发扩容的负载因子阈值\n    @extend_ratio = 2 # 扩容倍数\n    @buckets = Array.new(@capacity) { [] } # 桶数组\n  end\n\n  ### 哈希函数 ###\n  def hash_func(key)\n    key % @capacity\n  end\n\n  ### 负载因子 ###\n  def load_factor\n    @size / @capacity\n  end\n\n  ### 查询操作 ###\n  def get(key)\n    index = hash_func(key)\n    bucket = @buckets[index]\n    # 遍历桶,若找到 key ,则返回对应 val\n    for pair in bucket\n      return pair.val if pair.key == key\n    end\n    # 若未找到 key , 则返回 nil\n    nil\n  end\n\n  ### 添加操作 ###\n  def put(key, val)\n    # 当负载因子超过阈值时,执行扩容\n    extend if load_factor > @load_thres\n    index = hash_func(key)\n    bucket = @buckets[index]\n    # 遍历桶,若遇到指定 key ,则更新对应 val 并返回\n    for pair in bucket\n      if pair.key == key\n        pair.val = val\n        return\n      end\n    end\n    # 若无该 key ,则将键值对添加至尾部\n    pair = Pair.new(key, val)\n    bucket << pair\n    @size += 1\n  end\n\n  ### 删除操作 ###\n  def remove(key)\n    index = hash_func(key)\n    bucket = @buckets[index]\n    # 遍历桶,从中删除键值对\n    for pair in bucket\n      if pair.key == key\n        bucket.delete(pair)\n        @size -= 1\n        break\n      end\n    end\n  end\n\n  ### 扩容哈希表 ###\n  def extend\n    # 暫存原哈希表\n    buckets = @buckets\n    # 初始化扩容后的新哈希表\n    @capacity *= @extend_ratio\n    @buckets = Array.new(@capacity) { [] }\n    @size = 0\n    # 将键值对从原哈希表搬运至新哈希表\n    for bucket in buckets\n      for pair in bucket\n        put(pair.key, pair.val)\n      end\n    end\n  end\n\n  ### 打印哈希表 ###\n  def print\n    for bucket in @buckets\n      res = []\n      for pair in bucket\n        res << \"#{pair.key} -> #{pair.val}\"\n      end\n      pp res\n    end\n  end\nend\n
可视化运行

全屏观看 >

值得注意的是,当链表很长时,查询效率 \\(O(n)\\) 很差。此时可以将链表转换为“AVL 树”或“红黑树”,从而将查询操作的时间复杂度优化至 \\(O(\\log n)\\) 。

","path":["第 6 章   哈希表","6.2   哈希冲突"],"tags":[]},{"location":"chapter_hashing/hash_collision/#622","level":2,"title":"6.2.2   开放寻址","text":"

开放寻址(open addressing)不引入额外的数据结构,而是通过“多次探测”来处理哈希冲突,探测方式主要包括线性探测、平方探测和多次哈希等。

下面以线性探测为例,介绍开放寻址哈希表的工作机制。

","path":["第 6 章   哈希表","6.2   哈希冲突"],"tags":[]},{"location":"chapter_hashing/hash_collision/#1","level":3,"title":"1.   线性探测","text":"

线性探测采用固定步长的线性搜索来进行探测,其操作方法与普通哈希表有所不同。

图 6-6 展示了开放寻址(线性探测)哈希表的键值对分布。根据此哈希函数,最后两位相同的 key 都会被映射到相同的桶。而通过线性探测,它们被依次存储在该桶以及之下的桶中。

图 6-6   开放寻址(线性探测)哈希表的键值对分布

然而,线性探测容易产生“聚集现象”。具体来说,数组中连续被占用的位置越长,这些连续位置发生哈希冲突的可能性越大,从而进一步促使该位置的聚堆生长,形成恶性循环,最终导致增删查改操作效率劣化。

值得注意的是,我们不能在开放寻址哈希表中直接删除元素。这是因为删除元素会在数组内产生一个空桶 None ,而当查询元素时,线性探测到该空桶就会返回,因此在该空桶之下的元素都无法再被访问到,程序可能误判这些元素不存在,如图 6-7 所示。

图 6-7   在开放寻址中删除元素导致的查询问题

为了解决该问题,我们可以采用懒删除(lazy deletion)机制:它不直接从哈希表中移除元素,而是利用一个常量 TOMBSTONE 来标记这个桶。在该机制下,NoneTOMBSTONE 都代表空桶,都可以放置键值对。但不同的是,线性探测到 TOMBSTONE 时应该继续遍历,因为其之下可能还存在键值对。

然而,懒删除可能会加速哈希表的性能退化。这是因为每次删除操作都会产生一个删除标记,随着 TOMBSTONE 的增加,搜索时间也会增加,因为线性探测可能需要跳过多个 TOMBSTONE 才能找到目标元素。

为此,考虑在线性探测中记录遇到的首个 TOMBSTONE 的索引,并将搜索到的目标元素与该 TOMBSTONE 交换位置。这样做的好处是当每次查询或添加元素时,元素会被移动至距离理想位置(探测起始点)更近的桶,从而优化查询效率。

以下代码实现了一个包含懒删除的开放寻址(线性探测)哈希表。为了更加充分地使用哈希表的空间,我们将哈希表看作一个“环形数组”,当越过数组尾部时,回到头部继续遍历。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map_open_addressing.py
class HashMapOpenAddressing:\n    \"\"\"开放寻址哈希表\"\"\"\n\n    def __init__(self):\n        \"\"\"构造方法\"\"\"\n        self.size = 0  # 键值对数量\n        self.capacity = 4  # 哈希表容量\n        self.load_thres = 2.0 / 3.0  # 触发扩容的负载因子阈值\n        self.extend_ratio = 2  # 扩容倍数\n        self.buckets: list[Pair | None] = [None] * self.capacity  # 桶数组\n        self.TOMBSTONE = Pair(-1, \"-1\")  # 删除标记\n\n    def hash_func(self, key: int) -> int:\n        \"\"\"哈希函数\"\"\"\n        return key % self.capacity\n\n    def load_factor(self) -> float:\n        \"\"\"负载因子\"\"\"\n        return self.size / self.capacity\n\n    def find_bucket(self, key: int) -> int:\n        \"\"\"搜索 key 对应的桶索引\"\"\"\n        index = self.hash_func(key)\n        first_tombstone = -1\n        # 线性探测,当遇到空桶时跳出\n        while self.buckets[index] is not None:\n            # 若遇到 key ,返回对应的桶索引\n            if self.buckets[index].key == key:\n                # 若之前遇到了删除标记,则将键值对移动至该索引处\n                if first_tombstone != -1:\n                    self.buckets[first_tombstone] = self.buckets[index]\n                    self.buckets[index] = self.TOMBSTONE\n                    return first_tombstone  # 返回移动后的桶索引\n                return index  # 返回桶索引\n            # 记录遇到的首个删除标记\n            if first_tombstone == -1 and self.buckets[index] is self.TOMBSTONE:\n                first_tombstone = index\n            # 计算桶索引,越过尾部则返回头部\n            index = (index + 1) % self.capacity\n        # 若 key 不存在,则返回添加点的索引\n        return index if first_tombstone == -1 else first_tombstone\n\n    def get(self, key: int) -> str:\n        \"\"\"查询操作\"\"\"\n        # 搜索 key 对应的桶索引\n        index = self.find_bucket(key)\n        # 若找到键值对,则返回对应 val\n        if self.buckets[index] not in [None, self.TOMBSTONE]:\n            return self.buckets[index].val\n        # 若键值对不存在,则返回 None\n        return None\n\n    def put(self, key: int, val: str):\n        \"\"\"添加操作\"\"\"\n        # 当负载因子超过阈值时,执行扩容\n        if self.load_factor() > self.load_thres:\n            self.extend()\n        # 搜索 key 对应的桶索引\n        index = self.find_bucket(key)\n        # 若找到键值对,则覆盖 val 并返回\n        if self.buckets[index] not in [None, self.TOMBSTONE]:\n            self.buckets[index].val = val\n            return\n        # 若键值对不存在,则添加该键值对\n        self.buckets[index] = Pair(key, val)\n        self.size += 1\n\n    def remove(self, key: int):\n        \"\"\"删除操作\"\"\"\n        # 搜索 key 对应的桶索引\n        index = self.find_bucket(key)\n        # 若找到键值对,则用删除标记覆盖它\n        if self.buckets[index] not in [None, self.TOMBSTONE]:\n            self.buckets[index] = self.TOMBSTONE\n            self.size -= 1\n\n    def extend(self):\n        \"\"\"扩容哈希表\"\"\"\n        # 暂存原哈希表\n        buckets_tmp = self.buckets\n        # 初始化扩容后的新哈希表\n        self.capacity *= self.extend_ratio\n        self.buckets = [None] * self.capacity\n        self.size = 0\n        # 将键值对从原哈希表搬运至新哈希表\n        for pair in buckets_tmp:\n            if pair not in [None, self.TOMBSTONE]:\n                self.put(pair.key, pair.val)\n\n    def print(self):\n        \"\"\"打印哈希表\"\"\"\n        for pair in self.buckets:\n            if pair is None:\n                print(\"None\")\n            elif pair is self.TOMBSTONE:\n                print(\"TOMBSTONE\")\n            else:\n                print(pair.key, \"->\", pair.val)\n
hash_map_open_addressing.cpp
/* 开放寻址哈希表 */\nclass HashMapOpenAddressing {\n  private:\n    int size;                             // 键值对数量\n    int capacity = 4;                     // 哈希表容量\n    const double loadThres = 2.0 / 3.0;     // 触发扩容的负载因子阈值\n    const int extendRatio = 2;            // 扩容倍数\n    vector<Pair *> buckets;               // 桶数组\n    Pair *TOMBSTONE = new Pair(-1, \"-1\"); // 删除标记\n\n  public:\n    /* 构造方法 */\n    HashMapOpenAddressing() : size(0), buckets(capacity, nullptr) {\n    }\n\n    /* 析构方法 */\n    ~HashMapOpenAddressing() {\n        for (Pair *pair : buckets) {\n            if (pair != nullptr && pair != TOMBSTONE) {\n                delete pair;\n            }\n        }\n        delete TOMBSTONE;\n    }\n\n    /* 哈希函数 */\n    int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* 负载因子 */\n    double loadFactor() {\n        return (double)size / capacity;\n    }\n\n    /* 搜索 key 对应的桶索引 */\n    int findBucket(int key) {\n        int index = hashFunc(key);\n        int firstTombstone = -1;\n        // 线性探测,当遇到空桶时跳出\n        while (buckets[index] != nullptr) {\n            // 若遇到 key ,返回对应的桶索引\n            if (buckets[index]->key == key) {\n                // 若之前遇到了删除标记,则将键值对移动至该索引处\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index];\n                    buckets[index] = TOMBSTONE;\n                    return firstTombstone; // 返回移动后的桶索引\n                }\n                return index; // 返回桶索引\n            }\n            // 记录遇到的首个删除标记\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index;\n            }\n            // 计算桶索引,越过尾部则返回头部\n            index = (index + 1) % capacity;\n        }\n        // 若 key 不存在,则返回添加点的索引\n        return firstTombstone == -1 ? index : firstTombstone;\n    }\n\n    /* 查询操作 */\n    string get(int key) {\n        // 搜索 key 对应的桶索引\n        int index = findBucket(key);\n        // 若找到键值对,则返回对应 val\n        if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {\n            return buckets[index]->val;\n        }\n        // 若键值对不存在,则返回空字符串\n        return \"\";\n    }\n\n    /* 添加操作 */\n    void put(int key, string val) {\n        // 当负载因子超过阈值时,执行扩容\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        // 搜索 key 对应的桶索引\n        int index = findBucket(key);\n        // 若找到键值对,则覆盖 val 并返回\n        if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {\n            buckets[index]->val = val;\n            return;\n        }\n        // 若键值对不存在,则添加该键值对\n        buckets[index] = new Pair(key, val);\n        size++;\n    }\n\n    /* 删除操作 */\n    void remove(int key) {\n        // 搜索 key 对应的桶索引\n        int index = findBucket(key);\n        // 若找到键值对,则用删除标记覆盖它\n        if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) {\n            delete buckets[index];\n            buckets[index] = TOMBSTONE;\n            size--;\n        }\n    }\n\n    /* 扩容哈希表 */\n    void extend() {\n        // 暂存原哈希表\n        vector<Pair *> bucketsTmp = buckets;\n        // 初始化扩容后的新哈希表\n        capacity *= extendRatio;\n        buckets = vector<Pair *>(capacity, nullptr);\n        size = 0;\n        // 将键值对从原哈希表搬运至新哈希表\n        for (Pair *pair : bucketsTmp) {\n            if (pair != nullptr && pair != TOMBSTONE) {\n                put(pair->key, pair->val);\n                delete pair;\n            }\n        }\n    }\n\n    /* 打印哈希表 */\n    void print() {\n        for (Pair *pair : buckets) {\n            if (pair == nullptr) {\n                cout << \"nullptr\" << endl;\n            } else if (pair == TOMBSTONE) {\n                cout << \"TOMBSTONE\" << endl;\n            } else {\n                cout << pair->key << \" -> \" << pair->val << endl;\n            }\n        }\n    }\n};\n
hash_map_open_addressing.java
/* 开放寻址哈希表 */\nclass HashMapOpenAddressing {\n    private int size; // 键值对数量\n    private int capacity = 4; // 哈希表容量\n    private final double loadThres = 2.0 / 3.0; // 触发扩容的负载因子阈值\n    private final int extendRatio = 2; // 扩容倍数\n    private Pair[] buckets; // 桶数组\n    private final Pair TOMBSTONE = new Pair(-1, \"-1\"); // 删除标记\n\n    /* 构造方法 */\n    public HashMapOpenAddressing() {\n        size = 0;\n        buckets = new Pair[capacity];\n    }\n\n    /* 哈希函数 */\n    private int hashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* 负载因子 */\n    private double loadFactor() {\n        return (double) size / capacity;\n    }\n\n    /* 搜索 key 对应的桶索引 */\n    private int findBucket(int key) {\n        int index = hashFunc(key);\n        int firstTombstone = -1;\n        // 线性探测,当遇到空桶时跳出\n        while (buckets[index] != null) {\n            // 若遇到 key ,返回对应的桶索引\n            if (buckets[index].key == key) {\n                // 若之前遇到了删除标记,则将键值对移动至该索引处\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index];\n                    buckets[index] = TOMBSTONE;\n                    return firstTombstone; // 返回移动后的桶索引\n                }\n                return index; // 返回桶索引\n            }\n            // 记录遇到的首个删除标记\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index;\n            }\n            // 计算桶索引,越过尾部则返回头部\n            index = (index + 1) % capacity;\n        }\n        // 若 key 不存在,则返回添加点的索引\n        return firstTombstone == -1 ? index : firstTombstone;\n    }\n\n    /* 查询操作 */\n    public String get(int key) {\n        // 搜索 key 对应的桶索引\n        int index = findBucket(key);\n        // 若找到键值对,则返回对应 val\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            return buckets[index].val;\n        }\n        // 若键值对不存在,则返回 null\n        return null;\n    }\n\n    /* 添加操作 */\n    public void put(int key, String val) {\n        // 当负载因子超过阈值时,执行扩容\n        if (loadFactor() > loadThres) {\n            extend();\n        }\n        // 搜索 key 对应的桶索引\n        int index = findBucket(key);\n        // 若找到键值对,则覆盖 val 并返回\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index].val = val;\n            return;\n        }\n        // 若键值对不存在,则添加该键值对\n        buckets[index] = new Pair(key, val);\n        size++;\n    }\n\n    /* 删除操作 */\n    public void remove(int key) {\n        // 搜索 key 对应的桶索引\n        int index = findBucket(key);\n        // 若找到键值对,则用删除标记覆盖它\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index] = TOMBSTONE;\n            size--;\n        }\n    }\n\n    /* 扩容哈希表 */\n    private void extend() {\n        // 暂存原哈希表\n        Pair[] bucketsTmp = buckets;\n        // 初始化扩容后的新哈希表\n        capacity *= extendRatio;\n        buckets = new Pair[capacity];\n        size = 0;\n        // 将键值对从原哈希表搬运至新哈希表\n        for (Pair pair : bucketsTmp) {\n            if (pair != null && pair != TOMBSTONE) {\n                put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* 打印哈希表 */\n    public void print() {\n        for (Pair pair : buckets) {\n            if (pair == null) {\n                System.out.println(\"null\");\n            } else if (pair == TOMBSTONE) {\n                System.out.println(\"TOMBSTONE\");\n            } else {\n                System.out.println(pair.key + \" -> \" + pair.val);\n            }\n        }\n    }\n}\n
hash_map_open_addressing.cs
/* 开放寻址哈希表 */\nclass HashMapOpenAddressing {\n    int size; // 键值对数量\n    int capacity = 4; // 哈希表容量\n    double loadThres = 2.0 / 3.0; // 触发扩容的负载因子阈值\n    int extendRatio = 2; // 扩容倍数\n    Pair[] buckets; // 桶数组\n    Pair TOMBSTONE = new(-1, \"-1\"); // 删除标记\n\n    /* 构造方法 */\n    public HashMapOpenAddressing() {\n        size = 0;\n        buckets = new Pair[capacity];\n    }\n\n    /* 哈希函数 */\n    int HashFunc(int key) {\n        return key % capacity;\n    }\n\n    /* 负载因子 */\n    double LoadFactor() {\n        return (double)size / capacity;\n    }\n\n    /* 搜索 key 对应的桶索引 */\n    int FindBucket(int key) {\n        int index = HashFunc(key);\n        int firstTombstone = -1;\n        // 线性探测,当遇到空桶时跳出\n        while (buckets[index] != null) {\n            // 若遇到 key ,返回对应的桶索引\n            if (buckets[index].key == key) {\n                // 若之前遇到了删除标记,则将键值对移动至该索引处\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index];\n                    buckets[index] = TOMBSTONE;\n                    return firstTombstone; // 返回移动后的桶索引\n                }\n                return index; // 返回桶索引\n            }\n            // 记录遇到的首个删除标记\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index;\n            }\n            // 计算桶索引,越过尾部则返回头部\n            index = (index + 1) % capacity;\n        }\n        // 若 key 不存在,则返回添加点的索引\n        return firstTombstone == -1 ? index : firstTombstone;\n    }\n\n    /* 查询操作 */\n    public string? Get(int key) {\n        // 搜索 key 对应的桶索引\n        int index = FindBucket(key);\n        // 若找到键值对,则返回对应 val\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            return buckets[index].val;\n        }\n        // 若键值对不存在,则返回 null\n        return null;\n    }\n\n    /* 添加操作 */\n    public void Put(int key, string val) {\n        // 当负载因子超过阈值时,执行扩容\n        if (LoadFactor() > loadThres) {\n            Extend();\n        }\n        // 搜索 key 对应的桶索引\n        int index = FindBucket(key);\n        // 若找到键值对,则覆盖 val 并返回\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index].val = val;\n            return;\n        }\n        // 若键值对不存在,则添加该键值对\n        buckets[index] = new Pair(key, val);\n        size++;\n    }\n\n    /* 删除操作 */\n    public void Remove(int key) {\n        // 搜索 key 对应的桶索引\n        int index = FindBucket(key);\n        // 若找到键值对,则用删除标记覆盖它\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index] = TOMBSTONE;\n            size--;\n        }\n    }\n\n    /* 扩容哈希表 */\n    void Extend() {\n        // 暂存原哈希表\n        Pair[] bucketsTmp = buckets;\n        // 初始化扩容后的新哈希表\n        capacity *= extendRatio;\n        buckets = new Pair[capacity];\n        size = 0;\n        // 将键值对从原哈希表搬运至新哈希表\n        foreach (Pair pair in bucketsTmp) {\n            if (pair != null && pair != TOMBSTONE) {\n                Put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* 打印哈希表 */\n    public void Print() {\n        foreach (Pair pair in buckets) {\n            if (pair == null) {\n                Console.WriteLine(\"null\");\n            } else if (pair == TOMBSTONE) {\n                Console.WriteLine(\"TOMBSTONE\");\n            } else {\n                Console.WriteLine(pair.key + \" -> \" + pair.val);\n            }\n        }\n    }\n}\n
hash_map_open_addressing.go
/* 开放寻址哈希表 */\ntype hashMapOpenAddressing struct {\n    size        int     // 键值对数量\n    capacity    int     // 哈希表容量\n    loadThres   float64 // 触发扩容的负载因子阈值\n    extendRatio int     // 扩容倍数\n    buckets     []*pair // 桶数组\n    TOMBSTONE   *pair   // 删除标记\n}\n\n/* 构造方法 */\nfunc newHashMapOpenAddressing() *hashMapOpenAddressing {\n    return &hashMapOpenAddressing{\n        size:        0,\n        capacity:    4,\n        loadThres:   2.0 / 3.0,\n        extendRatio: 2,\n        buckets:     make([]*pair, 4),\n        TOMBSTONE:   &pair{-1, \"-1\"},\n    }\n}\n\n/* 哈希函数 */\nfunc (h *hashMapOpenAddressing) hashFunc(key int) int {\n    return key % h.capacity // 根据键计算哈希值\n}\n\n/* 负载因子 */\nfunc (h *hashMapOpenAddressing) loadFactor() float64 {\n    return float64(h.size) / float64(h.capacity) // 计算当前负载因子\n}\n\n/* 搜索 key 对应的桶索引 */\nfunc (h *hashMapOpenAddressing) findBucket(key int) int {\n    index := h.hashFunc(key) // 获取初始索引\n    firstTombstone := -1     // 记录遇到的第一个TOMBSTONE的位置\n    for h.buckets[index] != nil {\n        if h.buckets[index].key == key {\n            if firstTombstone != -1 {\n                // 若之前遇到了删除标记,则将键值对移动至该索引处\n                h.buckets[firstTombstone] = h.buckets[index]\n                h.buckets[index] = h.TOMBSTONE\n                return firstTombstone // 返回移动后的桶索引\n            }\n            return index // 返回找到的索引\n        }\n        if firstTombstone == -1 && h.buckets[index] == h.TOMBSTONE {\n            firstTombstone = index // 记录遇到的首个删除标记的位置\n        }\n        index = (index + 1) % h.capacity // 线性探测,越过尾部则返回头部\n    }\n    // 若 key 不存在,则返回添加点的索引\n    if firstTombstone != -1 {\n        return firstTombstone\n    }\n    return index\n}\n\n/* 查询操作 */\nfunc (h *hashMapOpenAddressing) get(key int) string {\n    index := h.findBucket(key) // 搜索 key 对应的桶索引\n    if h.buckets[index] != nil && h.buckets[index] != h.TOMBSTONE {\n        return h.buckets[index].val // 若找到键值对,则返回对应 val\n    }\n    return \"\" // 若键值对不存在,则返回 \"\"\n}\n\n/* 添加操作 */\nfunc (h *hashMapOpenAddressing) put(key int, val string) {\n    if h.loadFactor() > h.loadThres {\n        h.extend() // 当负载因子超过阈值时,执行扩容\n    }\n    index := h.findBucket(key) // 搜索 key 对应的桶索引\n    if h.buckets[index] == nil || h.buckets[index] == h.TOMBSTONE {\n        h.buckets[index] = &pair{key, val} // 若键值对不存在,则添加该键值对\n        h.size++\n    } else {\n        h.buckets[index].val = val // 若找到键值对,则覆盖 val\n    }\n}\n\n/* 删除操作 */\nfunc (h *hashMapOpenAddressing) remove(key int) {\n    index := h.findBucket(key) // 搜索 key 对应的桶索引\n    if h.buckets[index] != nil && h.buckets[index] != h.TOMBSTONE {\n        h.buckets[index] = h.TOMBSTONE // 若找到键值对,则用删除标记覆盖它\n        h.size--\n    }\n}\n\n/* 扩容哈希表 */\nfunc (h *hashMapOpenAddressing) extend() {\n    oldBuckets := h.buckets               // 暂存原哈希表\n    h.capacity *= h.extendRatio           // 更新容量\n    h.buckets = make([]*pair, h.capacity) // 初始化扩容后的新哈希表\n    h.size = 0                            // 重置大小\n    // 将键值对从原哈希表搬运至新哈希表\n    for _, pair := range oldBuckets {\n        if pair != nil && pair != h.TOMBSTONE {\n            h.put(pair.key, pair.val)\n        }\n    }\n}\n\n/* 打印哈希表 */\nfunc (h *hashMapOpenAddressing) print() {\n    for _, pair := range h.buckets {\n        if pair == nil {\n            fmt.Println(\"nil\")\n        } else if pair == h.TOMBSTONE {\n            fmt.Println(\"TOMBSTONE\")\n        } else {\n            fmt.Printf(\"%d -> %s\\n\", pair.key, pair.val)\n        }\n    }\n}\n
hash_map_open_addressing.swift
/* 开放寻址哈希表 */\nclass HashMapOpenAddressing {\n    var size: Int // 键值对数量\n    var capacity: Int // 哈希表容量\n    var loadThres: Double // 触发扩容的负载因子阈值\n    var extendRatio: Int // 扩容倍数\n    var buckets: [Pair?] // 桶数组\n    var TOMBSTONE: Pair // 删除标记\n\n    /* 构造方法 */\n    init() {\n        size = 0\n        capacity = 4\n        loadThres = 2.0 / 3.0\n        extendRatio = 2\n        buckets = Array(repeating: nil, count: capacity)\n        TOMBSTONE = Pair(key: -1, val: \"-1\")\n    }\n\n    /* 哈希函数 */\n    func hashFunc(key: Int) -> Int {\n        key % capacity\n    }\n\n    /* 负载因子 */\n    func loadFactor() -> Double {\n        Double(size) / Double(capacity)\n    }\n\n    /* 搜索 key 对应的桶索引 */\n    func findBucket(key: Int) -> Int {\n        var index = hashFunc(key: key)\n        var firstTombstone = -1\n        // 线性探测,当遇到空桶时跳出\n        while buckets[index] != nil {\n            // 若遇到 key ,返回对应的桶索引\n            if buckets[index]!.key == key {\n                // 若之前遇到了删除标记,则将键值对移动至该索引处\n                if firstTombstone != -1 {\n                    buckets[firstTombstone] = buckets[index]\n                    buckets[index] = TOMBSTONE\n                    return firstTombstone // 返回移动后的桶索引\n                }\n                return index // 返回桶索引\n            }\n            // 记录遇到的首个删除标记\n            if firstTombstone == -1 && buckets[index] == TOMBSTONE {\n                firstTombstone = index\n            }\n            // 计算桶索引,越过尾部则返回头部\n            index = (index + 1) % capacity\n        }\n        // 若 key 不存在,则返回添加点的索引\n        return firstTombstone == -1 ? index : firstTombstone\n    }\n\n    /* 查询操作 */\n    func get(key: Int) -> String? {\n        // 搜索 key 对应的桶索引\n        let index = findBucket(key: key)\n        // 若找到键值对,则返回对应 val\n        if buckets[index] != nil, buckets[index] != TOMBSTONE {\n            return buckets[index]!.val\n        }\n        // 若键值对不存在,则返回 null\n        return nil\n    }\n\n    /* 添加操作 */\n    func put(key: Int, val: String) {\n        // 当负载因子超过阈值时,执行扩容\n        if loadFactor() > loadThres {\n            extend()\n        }\n        // 搜索 key 对应的桶索引\n        let index = findBucket(key: key)\n        // 若找到键值对,则覆盖 val 并返回\n        if buckets[index] != nil, buckets[index] != TOMBSTONE {\n            buckets[index]!.val = val\n            return\n        }\n        // 若键值对不存在,则添加该键值对\n        buckets[index] = Pair(key: key, val: val)\n        size += 1\n    }\n\n    /* 删除操作 */\n    func remove(key: Int) {\n        // 搜索 key 对应的桶索引\n        let index = findBucket(key: key)\n        // 若找到键值对,则用删除标记覆盖它\n        if buckets[index] != nil, buckets[index] != TOMBSTONE {\n            buckets[index] = TOMBSTONE\n            size -= 1\n        }\n    }\n\n    /* 扩容哈希表 */\n    func extend() {\n        // 暂存原哈希表\n        let bucketsTmp = buckets\n        // 初始化扩容后的新哈希表\n        capacity *= extendRatio\n        buckets = Array(repeating: nil, count: capacity)\n        size = 0\n        // 将键值对从原哈希表搬运至新哈希表\n        for pair in bucketsTmp {\n            if let pair, pair != TOMBSTONE {\n                put(key: pair.key, val: pair.val)\n            }\n        }\n    }\n\n    /* 打印哈希表 */\n    func print() {\n        for pair in buckets {\n            if pair == nil {\n                Swift.print(\"null\")\n            } else if pair == TOMBSTONE {\n                Swift.print(\"TOMBSTONE\")\n            } else {\n                Swift.print(\"\\(pair!.key) -> \\(pair!.val)\")\n            }\n        }\n    }\n}\n
hash_map_open_addressing.js
/* 开放寻址哈希表 */\nclass HashMapOpenAddressing {\n    #size; // 键值对数量\n    #capacity; // 哈希表容量\n    #loadThres; // 触发扩容的负载因子阈值\n    #extendRatio; // 扩容倍数\n    #buckets; // 桶数组\n    #TOMBSTONE; // 删除标记\n\n    /* 构造方法 */\n    constructor() {\n        this.#size = 0; // 键值对数量\n        this.#capacity = 4; // 哈希表容量\n        this.#loadThres = 2.0 / 3.0; // 触发扩容的负载因子阈值\n        this.#extendRatio = 2; // 扩容倍数\n        this.#buckets = Array(this.#capacity).fill(null); // 桶数组\n        this.#TOMBSTONE = new Pair(-1, '-1'); // 删除标记\n    }\n\n    /* 哈希函数 */\n    #hashFunc(key) {\n        return key % this.#capacity;\n    }\n\n    /* 负载因子 */\n    #loadFactor() {\n        return this.#size / this.#capacity;\n    }\n\n    /* 搜索 key 对应的桶索引 */\n    #findBucket(key) {\n        let index = this.#hashFunc(key);\n        let firstTombstone = -1;\n        // 线性探测,当遇到空桶时跳出\n        while (this.#buckets[index] !== null) {\n            // 若遇到 key ,返回对应的桶索引\n            if (this.#buckets[index].key === key) {\n                // 若之前遇到了删除标记,则将键值对移动至该索引处\n                if (firstTombstone !== -1) {\n                    this.#buckets[firstTombstone] = this.#buckets[index];\n                    this.#buckets[index] = this.#TOMBSTONE;\n                    return firstTombstone; // 返回移动后的桶索引\n                }\n                return index; // 返回桶索引\n            }\n            // 记录遇到的首个删除标记\n            if (\n                firstTombstone === -1 &&\n                this.#buckets[index] === this.#TOMBSTONE\n            ) {\n                firstTombstone = index;\n            }\n            // 计算桶索引,越过尾部则返回头部\n            index = (index + 1) % this.#capacity;\n        }\n        // 若 key 不存在,则返回添加点的索引\n        return firstTombstone === -1 ? index : firstTombstone;\n    }\n\n    /* 查询操作 */\n    get(key) {\n        // 搜索 key 对应的桶索引\n        const index = this.#findBucket(key);\n        // 若找到键值对,则返回对应 val\n        if (\n            this.#buckets[index] !== null &&\n            this.#buckets[index] !== this.#TOMBSTONE\n        ) {\n            return this.#buckets[index].val;\n        }\n        // 若键值对不存在,则返回 null\n        return null;\n    }\n\n    /* 添加操作 */\n    put(key, val) {\n        // 当负载因子超过阈值时,执行扩容\n        if (this.#loadFactor() > this.#loadThres) {\n            this.#extend();\n        }\n        // 搜索 key 对应的桶索引\n        const index = this.#findBucket(key);\n        // 若找到键值对,则覆盖 val 并返回\n        if (\n            this.#buckets[index] !== null &&\n            this.#buckets[index] !== this.#TOMBSTONE\n        ) {\n            this.#buckets[index].val = val;\n            return;\n        }\n        // 若键值对不存在,则添加该键值对\n        this.#buckets[index] = new Pair(key, val);\n        this.#size++;\n    }\n\n    /* 删除操作 */\n    remove(key) {\n        // 搜索 key 对应的桶索引\n        const index = this.#findBucket(key);\n        // 若找到键值对,则用删除标记覆盖它\n        if (\n            this.#buckets[index] !== null &&\n            this.#buckets[index] !== this.#TOMBSTONE\n        ) {\n            this.#buckets[index] = this.#TOMBSTONE;\n            this.#size--;\n        }\n    }\n\n    /* 扩容哈希表 */\n    #extend() {\n        // 暂存原哈希表\n        const bucketsTmp = this.#buckets;\n        // 初始化扩容后的新哈希表\n        this.#capacity *= this.#extendRatio;\n        this.#buckets = Array(this.#capacity).fill(null);\n        this.#size = 0;\n        // 将键值对从原哈希表搬运至新哈希表\n        for (const pair of bucketsTmp) {\n            if (pair !== null && pair !== this.#TOMBSTONE) {\n                this.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* 打印哈希表 */\n    print() {\n        for (const pair of this.#buckets) {\n            if (pair === null) {\n                console.log('null');\n            } else if (pair === this.#TOMBSTONE) {\n                console.log('TOMBSTONE');\n            } else {\n                console.log(pair.key + ' -> ' + pair.val);\n            }\n        }\n    }\n}\n
hash_map_open_addressing.ts
/* 开放寻址哈希表 */\nclass HashMapOpenAddressing {\n    private size: number; // 键值对数量\n    private capacity: number; // 哈希表容量\n    private loadThres: number; // 触发扩容的负载因子阈值\n    private extendRatio: number; // 扩容倍数\n    private buckets: Array<Pair | null>; // 桶数组\n    private TOMBSTONE: Pair; // 删除标记\n\n    /* 构造方法 */\n    constructor() {\n        this.size = 0; // 键值对数量\n        this.capacity = 4; // 哈希表容量\n        this.loadThres = 2.0 / 3.0; // 触发扩容的负载因子阈值\n        this.extendRatio = 2; // 扩容倍数\n        this.buckets = Array(this.capacity).fill(null); // 桶数组\n        this.TOMBSTONE = new Pair(-1, '-1'); // 删除标记\n    }\n\n    /* 哈希函数 */\n    private hashFunc(key: number): number {\n        return key % this.capacity;\n    }\n\n    /* 负载因子 */\n    private loadFactor(): number {\n        return this.size / this.capacity;\n    }\n\n    /* 搜索 key 对应的桶索引 */\n    private findBucket(key: number): number {\n        let index = this.hashFunc(key);\n        let firstTombstone = -1;\n        // 线性探测,当遇到空桶时跳出\n        while (this.buckets[index] !== null) {\n            // 若遇到 key ,返回对应的桶索引\n            if (this.buckets[index]!.key === key) {\n                // 若之前遇到了删除标记,则将键值对移动至该索引处\n                if (firstTombstone !== -1) {\n                    this.buckets[firstTombstone] = this.buckets[index];\n                    this.buckets[index] = this.TOMBSTONE;\n                    return firstTombstone; // 返回移动后的桶索引\n                }\n                return index; // 返回桶索引\n            }\n            // 记录遇到的首个删除标记\n            if (\n                firstTombstone === -1 &&\n                this.buckets[index] === this.TOMBSTONE\n            ) {\n                firstTombstone = index;\n            }\n            // 计算桶索引,越过尾部则返回头部\n            index = (index + 1) % this.capacity;\n        }\n        // 若 key 不存在,则返回添加点的索引\n        return firstTombstone === -1 ? index : firstTombstone;\n    }\n\n    /* 查询操作 */\n    get(key: number): string | null {\n        // 搜索 key 对应的桶索引\n        const index = this.findBucket(key);\n        // 若找到键值对,则返回对应 val\n        if (\n            this.buckets[index] !== null &&\n            this.buckets[index] !== this.TOMBSTONE\n        ) {\n            return this.buckets[index]!.val;\n        }\n        // 若键值对不存在,则返回 null\n        return null;\n    }\n\n    /* 添加操作 */\n    put(key: number, val: string): void {\n        // 当负载因子超过阈值时,执行扩容\n        if (this.loadFactor() > this.loadThres) {\n            this.extend();\n        }\n        // 搜索 key 对应的桶索引\n        const index = this.findBucket(key);\n        // 若找到键值对,则覆盖 val 并返回\n        if (\n            this.buckets[index] !== null &&\n            this.buckets[index] !== this.TOMBSTONE\n        ) {\n            this.buckets[index]!.val = val;\n            return;\n        }\n        // 若键值对不存在,则添加该键值对\n        this.buckets[index] = new Pair(key, val);\n        this.size++;\n    }\n\n    /* 删除操作 */\n    remove(key: number): void {\n        // 搜索 key 对应的桶索引\n        const index = this.findBucket(key);\n        // 若找到键值对,则用删除标记覆盖它\n        if (\n            this.buckets[index] !== null &&\n            this.buckets[index] !== this.TOMBSTONE\n        ) {\n            this.buckets[index] = this.TOMBSTONE;\n            this.size--;\n        }\n    }\n\n    /* 扩容哈希表 */\n    private extend(): void {\n        // 暂存原哈希表\n        const bucketsTmp = this.buckets;\n        // 初始化扩容后的新哈希表\n        this.capacity *= this.extendRatio;\n        this.buckets = Array(this.capacity).fill(null);\n        this.size = 0;\n        // 将键值对从原哈希表搬运至新哈希表\n        for (const pair of bucketsTmp) {\n            if (pair !== null && pair !== this.TOMBSTONE) {\n                this.put(pair.key, pair.val);\n            }\n        }\n    }\n\n    /* 打印哈希表 */\n    print(): void {\n        for (const pair of this.buckets) {\n            if (pair === null) {\n                console.log('null');\n            } else if (pair === this.TOMBSTONE) {\n                console.log('TOMBSTONE');\n            } else {\n                console.log(pair.key + ' -> ' + pair.val);\n            }\n        }\n    }\n}\n
hash_map_open_addressing.dart
/* 开放寻址哈希表 */\nclass HashMapOpenAddressing {\n  late int _size; // 键值对数量\n  int _capacity = 4; // 哈希表容量\n  double _loadThres = 2.0 / 3.0; // 触发扩容的负载因子阈值\n  int _extendRatio = 2; // 扩容倍数\n  late List<Pair?> _buckets; // 桶数组\n  Pair _TOMBSTONE = Pair(-1, \"-1\"); // 删除标记\n\n  /* 构造方法 */\n  HashMapOpenAddressing() {\n    _size = 0;\n    _buckets = List.generate(_capacity, (index) => null);\n  }\n\n  /* 哈希函数 */\n  int hashFunc(int key) {\n    return key % _capacity;\n  }\n\n  /* 负载因子 */\n  double loadFactor() {\n    return _size / _capacity;\n  }\n\n  /* 搜索 key 对应的桶索引 */\n  int findBucket(int key) {\n    int index = hashFunc(key);\n    int firstTombstone = -1;\n    // 线性探测,当遇到空桶时跳出\n    while (_buckets[index] != null) {\n      // 若遇到 key ,返回对应的桶索引\n      if (_buckets[index]!.key == key) {\n        // 若之前遇到了删除标记,则将键值对移动至该索引处\n        if (firstTombstone != -1) {\n          _buckets[firstTombstone] = _buckets[index];\n          _buckets[index] = _TOMBSTONE;\n          return firstTombstone; // 返回移动后的桶索引\n        }\n        return index; // 返回桶索引\n      }\n      // 记录遇到的首个删除标记\n      if (firstTombstone == -1 && _buckets[index] == _TOMBSTONE) {\n        firstTombstone = index;\n      }\n      // 计算桶索引,越过尾部则返回头部\n      index = (index + 1) % _capacity;\n    }\n    // 若 key 不存在,则返回添加点的索引\n    return firstTombstone == -1 ? index : firstTombstone;\n  }\n\n  /* 查询操作 */\n  String? get(int key) {\n    // 搜索 key 对应的桶索引\n    int index = findBucket(key);\n    // 若找到键值对,则返回对应 val\n    if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) {\n      return _buckets[index]!.val;\n    }\n    // 若键值对不存在,则返回 null\n    return null;\n  }\n\n  /* 添加操作 */\n  void put(int key, String val) {\n    // 当负载因子超过阈值时,执行扩容\n    if (loadFactor() > _loadThres) {\n      extend();\n    }\n    // 搜索 key 对应的桶索引\n    int index = findBucket(key);\n    // 若找到键值对,则覆盖 val 并返回\n    if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) {\n      _buckets[index]!.val = val;\n      return;\n    }\n    // 若键值对不存在,则添加该键值对\n    _buckets[index] = new Pair(key, val);\n    _size++;\n  }\n\n  /* 删除操作 */\n  void remove(int key) {\n    // 搜索 key 对应的桶索引\n    int index = findBucket(key);\n    // 若找到键值对,则用删除标记覆盖它\n    if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) {\n      _buckets[index] = _TOMBSTONE;\n      _size--;\n    }\n  }\n\n  /* 扩容哈希表 */\n  void extend() {\n    // 暂存原哈希表\n    List<Pair?> bucketsTmp = _buckets;\n    // 初始化扩容后的新哈希表\n    _capacity *= _extendRatio;\n    _buckets = List.generate(_capacity, (index) => null);\n    _size = 0;\n    // 将键值对从原哈希表搬运至新哈希表\n    for (Pair? pair in bucketsTmp) {\n      if (pair != null && pair != _TOMBSTONE) {\n        put(pair.key, pair.val);\n      }\n    }\n  }\n\n  /* 打印哈希表 */\n  void printHashMap() {\n    for (Pair? pair in _buckets) {\n      if (pair == null) {\n        print(\"null\");\n      } else if (pair == _TOMBSTONE) {\n        print(\"TOMBSTONE\");\n      } else {\n        print(\"${pair.key} -> ${pair.val}\");\n      }\n    }\n  }\n}\n
hash_map_open_addressing.rs
/* 开放寻址哈希表 */\nstruct HashMapOpenAddressing {\n    size: usize,                // 键值对数量\n    capacity: usize,            // 哈希表容量\n    load_thres: f64,            // 触发扩容的负载因子阈值\n    extend_ratio: usize,        // 扩容倍数\n    buckets: Vec<Option<Pair>>, // 桶数组\n    TOMBSTONE: Option<Pair>,    // 删除标记\n}\n\nimpl HashMapOpenAddressing {\n    /* 构造方法 */\n    fn new() -> Self {\n        Self {\n            size: 0,\n            capacity: 4,\n            load_thres: 2.0 / 3.0,\n            extend_ratio: 2,\n            buckets: vec![None; 4],\n            TOMBSTONE: Some(Pair {\n                key: -1,\n                val: \"-1\".to_string(),\n            }),\n        }\n    }\n\n    /* 哈希函数 */\n    fn hash_func(&self, key: i32) -> usize {\n        (key % self.capacity as i32) as usize\n    }\n\n    /* 负载因子 */\n    fn load_factor(&self) -> f64 {\n        self.size as f64 / self.capacity as f64\n    }\n\n    /* 搜索 key 对应的桶索引 */\n    fn find_bucket(&mut self, key: i32) -> usize {\n        let mut index = self.hash_func(key);\n        let mut first_tombstone = -1;\n        // 线性探测,当遇到空桶时跳出\n        while self.buckets[index].is_some() {\n            // 若遇到 key,返回对应的桶索引\n            if self.buckets[index].as_ref().unwrap().key == key {\n                // 若之前遇到了删除标记,则将建值对移动至该索引\n                if first_tombstone != -1 {\n                    self.buckets[first_tombstone as usize] = self.buckets[index].take();\n                    self.buckets[index] = self.TOMBSTONE.clone();\n                    return first_tombstone as usize; // 返回移动后的桶索引\n                }\n                return index; // 返回桶索引\n            }\n            // 记录遇到的首个删除标记\n            if first_tombstone == -1 && self.buckets[index] == self.TOMBSTONE {\n                first_tombstone = index as i32;\n            }\n            // 计算桶索引,越过尾部则返回头部\n            index = (index + 1) % self.capacity;\n        }\n        // 若 key 不存在,则返回添加点的索引\n        if first_tombstone == -1 {\n            index\n        } else {\n            first_tombstone as usize\n        }\n    }\n\n    /* 查询操作 */\n    fn get(&mut self, key: i32) -> Option<&str> {\n        // 搜索 key 对应的桶索引\n        let index = self.find_bucket(key);\n        // 若找到键值对,则返回对应 val\n        if self.buckets[index].is_some() && self.buckets[index] != self.TOMBSTONE {\n            return self.buckets[index].as_ref().map(|pair| &pair.val as &str);\n        }\n        // 若键值对不存在,则返回 null\n        None\n    }\n\n    /* 添加操作 */\n    fn put(&mut self, key: i32, val: String) {\n        // 当负载因子超过阈值时,执行扩容\n        if self.load_factor() > self.load_thres {\n            self.extend();\n        }\n        // 搜索 key 对应的桶索引\n        let index = self.find_bucket(key);\n        // 若找到键值对,则覆盖 val 并返回\n        if self.buckets[index].is_some() && self.buckets[index] != self.TOMBSTONE {\n            self.buckets[index].as_mut().unwrap().val = val;\n            return;\n        }\n        // 若键值对不存在,则添加该键值对\n        self.buckets[index] = Some(Pair { key, val });\n        self.size += 1;\n    }\n\n    /* 删除操作 */\n    fn remove(&mut self, key: i32) {\n        // 搜索 key 对应的桶索引\n        let index = self.find_bucket(key);\n        // 若找到键值对,则用删除标记覆盖它\n        if self.buckets[index].is_some() && self.buckets[index] != self.TOMBSTONE {\n            self.buckets[index] = self.TOMBSTONE.clone();\n            self.size -= 1;\n        }\n    }\n\n    /* 扩容哈希表 */\n    fn extend(&mut self) {\n        // 暂存原哈希表\n        let buckets_tmp = self.buckets.clone();\n        // 初始化扩容后的新哈希表\n        self.capacity *= self.extend_ratio;\n        self.buckets = vec![None; self.capacity];\n        self.size = 0;\n\n        // 将键值对从原哈希表搬运至新哈希表\n        for pair in buckets_tmp {\n            if pair.is_none() || pair == self.TOMBSTONE {\n                continue;\n            }\n            let pair = pair.unwrap();\n\n            self.put(pair.key, pair.val);\n        }\n    }\n    /* 打印哈希表 */\n    fn print(&self) {\n        for pair in &self.buckets {\n            if pair.is_none() {\n                println!(\"null\");\n            } else if pair == &self.TOMBSTONE {\n                println!(\"TOMBSTONE\");\n            } else {\n                let pair = pair.as_ref().unwrap();\n                println!(\"{} -> {}\", pair.key, pair.val);\n            }\n        }\n    }\n}\n
hash_map_open_addressing.c
/* 开放寻址哈希表 */\ntypedef struct {\n    int size;         // 键值对数量\n    int capacity;     // 哈希表容量\n    double loadThres; // 触发扩容的负载因子阈值\n    int extendRatio;  // 扩容倍数\n    Pair **buckets;   // 桶数组\n    Pair *TOMBSTONE;  // 删除标记\n} HashMapOpenAddressing;\n\n/* 构造函数 */\nHashMapOpenAddressing *newHashMapOpenAddressing() {\n    HashMapOpenAddressing *hashMap = (HashMapOpenAddressing *)malloc(sizeof(HashMapOpenAddressing));\n    hashMap->size = 0;\n    hashMap->capacity = 4;\n    hashMap->loadThres = 2.0 / 3.0;\n    hashMap->extendRatio = 2;\n    hashMap->buckets = (Pair **)calloc(hashMap->capacity, sizeof(Pair *));\n    hashMap->TOMBSTONE = (Pair *)malloc(sizeof(Pair));\n    hashMap->TOMBSTONE->key = -1;\n    hashMap->TOMBSTONE->val = \"-1\";\n\n    return hashMap;\n}\n\n/* 析构函数 */\nvoid delHashMapOpenAddressing(HashMapOpenAddressing *hashMap) {\n    for (int i = 0; i < hashMap->capacity; i++) {\n        Pair *pair = hashMap->buckets[i];\n        if (pair != NULL && pair != hashMap->TOMBSTONE) {\n            free(pair->val);\n            free(pair);\n        }\n    }\n    free(hashMap->buckets);\n    free(hashMap->TOMBSTONE);\n    free(hashMap);\n}\n\n/* 哈希函数 */\nint hashFunc(HashMapOpenAddressing *hashMap, int key) {\n    return key % hashMap->capacity;\n}\n\n/* 负载因子 */\ndouble loadFactor(HashMapOpenAddressing *hashMap) {\n    return (double)hashMap->size / (double)hashMap->capacity;\n}\n\n/* 搜索 key 对应的桶索引 */\nint findBucket(HashMapOpenAddressing *hashMap, int key) {\n    int index = hashFunc(hashMap, key);\n    int firstTombstone = -1;\n    // 线性探测,当遇到空桶时跳出\n    while (hashMap->buckets[index] != NULL) {\n        // 若遇到 key ,返回对应的桶索引\n        if (hashMap->buckets[index]->key == key) {\n            // 若之前遇到了删除标记,则将键值对移动至该索引处\n            if (firstTombstone != -1) {\n                hashMap->buckets[firstTombstone] = hashMap->buckets[index];\n                hashMap->buckets[index] = hashMap->TOMBSTONE;\n                return firstTombstone; // 返回移动后的桶索引\n            }\n            return index; // 返回桶索引\n        }\n        // 记录遇到的首个删除标记\n        if (firstTombstone == -1 && hashMap->buckets[index] == hashMap->TOMBSTONE) {\n            firstTombstone = index;\n        }\n        // 计算桶索引,越过尾部则返回头部\n        index = (index + 1) % hashMap->capacity;\n    }\n    // 若 key 不存在,则返回添加点的索引\n    return firstTombstone == -1 ? index : firstTombstone;\n}\n\n/* 查询操作 */\nchar *get(HashMapOpenAddressing *hashMap, int key) {\n    // 搜索 key 对应的桶索引\n    int index = findBucket(hashMap, key);\n    // 若找到键值对,则返回对应 val\n    if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) {\n        return hashMap->buckets[index]->val;\n    }\n    // 若键值对不存在,则返回空字符串\n    return \"\";\n}\n\n/* 添加操作 */\nvoid put(HashMapOpenAddressing *hashMap, int key, char *val) {\n    // 当负载因子超过阈值时,执行扩容\n    if (loadFactor(hashMap) > hashMap->loadThres) {\n        extend(hashMap);\n    }\n    // 搜索 key 对应的桶索引\n    int index = findBucket(hashMap, key);\n    // 若找到键值对,则覆盖 val 并返回\n    if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) {\n        free(hashMap->buckets[index]->val);\n        hashMap->buckets[index]->val = (char *)malloc(sizeof(strlen(val) + 1));\n        strcpy(hashMap->buckets[index]->val, val);\n        hashMap->buckets[index]->val[strlen(val)] = '\\0';\n        return;\n    }\n    // 若键值对不存在,则添加该键值对\n    Pair *pair = (Pair *)malloc(sizeof(Pair));\n    pair->key = key;\n    pair->val = (char *)malloc(sizeof(strlen(val) + 1));\n    strcpy(pair->val, val);\n    pair->val[strlen(val)] = '\\0';\n\n    hashMap->buckets[index] = pair;\n    hashMap->size++;\n}\n\n/* 删除操作 */\nvoid removeItem(HashMapOpenAddressing *hashMap, int key) {\n    // 搜索 key 对应的桶索引\n    int index = findBucket(hashMap, key);\n    // 若找到键值对,则用删除标记覆盖它\n    if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) {\n        Pair *pair = hashMap->buckets[index];\n        free(pair->val);\n        free(pair);\n        hashMap->buckets[index] = hashMap->TOMBSTONE;\n        hashMap->size--;\n    }\n}\n\n/* 扩容哈希表 */\nvoid extend(HashMapOpenAddressing *hashMap) {\n    // 暂存原哈希表\n    Pair **bucketsTmp = hashMap->buckets;\n    int oldCapacity = hashMap->capacity;\n    // 初始化扩容后的新哈希表\n    hashMap->capacity *= hashMap->extendRatio;\n    hashMap->buckets = (Pair **)calloc(hashMap->capacity, sizeof(Pair *));\n    hashMap->size = 0;\n    // 将键值对从原哈希表搬运至新哈希表\n    for (int i = 0; i < oldCapacity; i++) {\n        Pair *pair = bucketsTmp[i];\n        if (pair != NULL && pair != hashMap->TOMBSTONE) {\n            put(hashMap, pair->key, pair->val);\n            free(pair->val);\n            free(pair);\n        }\n    }\n    free(bucketsTmp);\n}\n\n/* 打印哈希表 */\nvoid print(HashMapOpenAddressing *hashMap) {\n    for (int i = 0; i < hashMap->capacity; i++) {\n        Pair *pair = hashMap->buckets[i];\n        if (pair == NULL) {\n            printf(\"NULL\\n\");\n        } else if (pair == hashMap->TOMBSTONE) {\n            printf(\"TOMBSTONE\\n\");\n        } else {\n            printf(\"%d -> %s\\n\", pair->key, pair->val);\n        }\n    }\n}\n
hash_map_open_addressing.kt
/* 开放寻址哈希表 */\nclass HashMapOpenAddressing {\n    private var size: Int               // 键值对数量\n    private var capacity: Int           // 哈希表容量\n    private val loadThres: Double       // 触发扩容的负载因子阈值\n    private val extendRatio: Int        // 扩容倍数\n    private var buckets: Array<Pair?>   // 桶数组\n    private val TOMBSTONE: Pair         // 删除标记\n\n    /* 构造方法 */\n    init {\n        size = 0\n        capacity = 4\n        loadThres = 2.0 / 3.0\n        extendRatio = 2\n        buckets = arrayOfNulls(capacity)\n        TOMBSTONE = Pair(-1, \"-1\")\n    }\n\n    /* 哈希函数 */\n    fun hashFunc(key: Int): Int {\n        return key % capacity\n    }\n\n    /* 负载因子 */\n    fun loadFactor(): Double {\n        return (size / capacity).toDouble()\n    }\n\n    /* 搜索 key 对应的桶索引 */\n    fun findBucket(key: Int): Int {\n        var index = hashFunc(key)\n        var firstTombstone = -1\n        // 线性探测,当遇到空桶时跳出\n        while (buckets[index] != null) {\n            // 若遇到 key ,返回对应的桶索引\n            if (buckets[index]?.key == key) {\n                // 若之前遇到了删除标记,则将键值对移动至该索引处\n                if (firstTombstone != -1) {\n                    buckets[firstTombstone] = buckets[index]\n                    buckets[index] = TOMBSTONE\n                    return firstTombstone // 返回移动后的桶索引\n                }\n                return index // 返回桶索引\n            }\n            // 记录遇到的首个删除标记\n            if (firstTombstone == -1 && buckets[index] == TOMBSTONE) {\n                firstTombstone = index\n            }\n            // 计算桶索引,越过尾部则返回头部\n            index = (index + 1) % capacity\n        }\n        // 若 key 不存在,则返回添加点的索引\n        return if (firstTombstone == -1) index else firstTombstone\n    }\n\n    /* 查询操作 */\n    fun get(key: Int): String? {\n        // 搜索 key 对应的桶索引\n        val index = findBucket(key)\n        // 若找到键值对,则返回对应 val\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            return buckets[index]?._val\n        }\n        // 若键值对不存在,则返回 null\n        return null\n    }\n\n    /* 添加操作 */\n    fun put(key: Int, _val: String) {\n        // 当负载因子超过阈值时,执行扩容\n        if (loadFactor() > loadThres) {\n            extend()\n        }\n        // 搜索 key 对应的桶索引\n        val index = findBucket(key)\n        // 若找到键值对,则覆盖 val 并返回\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index]!!._val = _val\n            return\n        }\n        // 若键值对不存在,则添加该键值对\n        buckets[index] = Pair(key, _val)\n        size++\n    }\n\n    /* 删除操作 */\n    fun remove(key: Int) {\n        // 搜索 key 对应的桶索引\n        val index = findBucket(key)\n        // 若找到键值对,则用删除标记覆盖它\n        if (buckets[index] != null && buckets[index] != TOMBSTONE) {\n            buckets[index] = TOMBSTONE\n            size--\n        }\n    }\n\n    /* 扩容哈希表 */\n    fun extend() {\n        // 暂存原哈希表\n        val bucketsTmp = buckets\n        // 初始化扩容后的新哈希表\n        capacity *= extendRatio\n        buckets = arrayOfNulls(capacity)\n        size = 0\n        // 将键值对从原哈希表搬运至新哈希表\n        for (pair in bucketsTmp) {\n            if (pair != null && pair != TOMBSTONE) {\n                put(pair.key, pair._val)\n            }\n        }\n    }\n\n    /* 打印哈希表 */\n    fun print() {\n        for (pair in buckets) {\n            if (pair == null) {\n                println(\"null\")\n            } else if (pair == TOMBSTONE) {\n                println(\"TOMESTOME\")\n            } else {\n                println(\"${pair.key} -> ${pair._val}\")\n            }\n        }\n    }\n}\n
hash_map_open_addressing.rb
### 开放寻址哈希表 ###\nclass HashMapOpenAddressing\n  TOMBSTONE = Pair.new(-1, '-1') # 删除标记\n\n  ### 构造方法 ###\n  def initialize\n    @size = 0 # 键值对数量\n    @capacity = 4 # 哈希表容量\n    @load_thres = 2.0 / 3.0 # 触发扩容的负载因子阈值\n    @extend_ratio = 2 # 扩容倍数\n    @buckets = Array.new(@capacity) # 桶数组\n  end\n\n  ### 哈希函数 ###\n  def hash_func(key)\n    key % @capacity\n  end\n\n  ### 负载因子 ###\n  def load_factor\n    @size / @capacity\n  end\n\n  ### 搜索 key 对应的桶索引 ###\n  def find_bucket(key)\n    index = hash_func(key)\n    first_tombstone = -1\n    # 线性探测,当遇到空桶时跳出\n    while !@buckets[index].nil?\n      # 若遇到 key ,返回对应的桶索引\n      if @buckets[index].key == key\n        # 若之前遇到了删除标记,则将键值对移动至该索引处\n        if first_tombstone != -1\n          @buckets[first_tombstone] = @buckets[index]\n          @buckets[index] = TOMBSTONE\n          return first_tombstone # 返回移动后的桶索引\n        end\n        return index # 返回桶索引\n      end\n      # 记录遇到的首个删除标记\n      first_tombstone = index if first_tombstone == -1 && @buckets[index] == TOMBSTONE\n      # 计算桶索引,越过尾部则返回头部\n      index = (index + 1) % @capacity\n    end\n    # 若 key 不存在,则返回添加点的索引\n    first_tombstone == -1 ? index : first_tombstone\n  end\n\n  ### 查询操作 ###\n  def get(key)\n    # 搜索 key 对应的桶索引\n    index = find_bucket(key)\n    # 若找到键值对,则返回对应 val\n    return @buckets[index].val unless [nil, TOMBSTONE].include?(@buckets[index])\n    # 若键值对不存在,则返回 nil\n    nil\n  end\n\n  ### 添加操作 ###\n  def put(key, val)\n    # 当负载因子超过阈值时,执行扩容\n    extend if load_factor > @load_thres\n    # 搜索 key 对应的桶索引\n    index = find_bucket(key)\n    # 若找到键值对,则覆盖 val 开返回\n    unless [nil, TOMBSTONE].include?(@buckets[index])\n      @buckets[index].val = val\n      return\n    end\n    # 若键值对不存在,则添加该键值对\n    @buckets[index] = Pair.new(key, val)\n    @size += 1\n  end\n\n  ### 删除操作 ###\n  def remove(key)\n    # 搜索 key 对应的桶索引\n    index = find_bucket(key)\n    # 若找到键值对,则用删除标记覆盖它\n    unless [nil, TOMBSTONE].include?(@buckets[index])\n      @buckets[index] = TOMBSTONE\n      @size -= 1\n    end\n  end\n\n  ### 扩容哈希表 ###\n  def extend\n    # 暂存原哈希表\n    buckets_tmp = @buckets\n    # 初始化扩容后的新哈希表\n    @capacity *= @extend_ratio\n    @buckets = Array.new(@capacity)\n    @size = 0\n    # 将键值对从原哈希表搬运至新哈希表\n    for pair in buckets_tmp\n      put(pair.key, pair.val) unless [nil, TOMBSTONE].include?(pair)\n    end\n  end\n\n  ### 打印哈希表 ###\n  def print\n    for pair in @buckets\n      if pair.nil?\n        puts \"Nil\"\n      elsif pair == TOMBSTONE\n        puts \"TOMBSTONE\"\n      else\n        puts \"#{pair.key} -> #{pair.val}\"\n      end\n    end\n  end\nend\n
","path":["第 6 章   哈希表","6.2   哈希冲突"],"tags":[]},{"location":"chapter_hashing/hash_collision/#2","level":3,"title":"2.   平方探测","text":"

平方探测与线性探测类似,都是开放寻址的常见策略之一。当发生冲突时,平方探测不是简单地跳过一个固定的步数,而是跳过“探测次数的平方”的步数,即 \\(1, 4, 9, \\dots\\) 步。

平方探测主要具有以下优势。

然而,平方探测并不是完美的。

","path":["第 6 章   哈希表","6.2   哈希冲突"],"tags":[]},{"location":"chapter_hashing/hash_collision/#3","level":3,"title":"3.   多次哈希","text":"

顾名思义,多次哈希方法使用多个哈希函数 \\(f_1(x)\\)、\\(f_2(x)\\)、\\(f_3(x)\\)、\\(\\dots\\) 进行探测。

与线性探测相比,多次哈希方法不易产生聚集,但多个哈希函数会带来额外的计算量。

Tip

请注意,开放寻址(线性探测、平方探测和多次哈希)哈希表都存在“不能直接删除元素”的问题。

","path":["第 6 章   哈希表","6.2   哈希冲突"],"tags":[]},{"location":"chapter_hashing/hash_collision/#623","level":2,"title":"6.2.3   编程语言的选择","text":"

各种编程语言采取了不同的哈希表实现策略,下面举几个例子。

","path":["第 6 章   哈希表","6.2   哈希冲突"],"tags":[]},{"location":"chapter_hashing/hash_map/","level":1,"title":"6.1   哈希表","text":"

哈希表(hash table),又称散列表,它通过建立键 key 与值 value 之间的映射,实现高效的元素查询。具体而言,我们向哈希表中输入一个键 key ,则可以在 \\(O(1)\\) 时间内获取对应的值 value

如图 6-1 所示,给定 \\(n\\) 个学生,每个学生都有“姓名”和“学号”两项数据。假如我们希望实现“输入一个学号,返回对应的姓名”的查询功能,则可以采用图 6-1 所示的哈希表来实现。

图 6-1   哈希表的抽象表示

除哈希表外,数组和链表也可以实现查询功能,它们的效率对比如表 6-1 所示。

表 6-1   元素查询效率对比

数组 链表 哈希表 查找元素 \\(O(n)\\) \\(O(n)\\) \\(O(1)\\) 添加元素 \\(O(1)\\) \\(O(1)\\) \\(O(1)\\) 删除元素 \\(O(n)\\) \\(O(n)\\) \\(O(1)\\)

观察发现,在哈希表中进行增删查改的时间复杂度都是 \\(O(1)\\) ,非常高效。

","path":["第 6 章   哈希表","6.1   哈希表"],"tags":[]},{"location":"chapter_hashing/hash_map/#611","level":2,"title":"6.1.1   哈希表常用操作","text":"

哈希表的常见操作包括:初始化、查询操作、添加键值对和删除键值对等,示例代码如下:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map.py
# 初始化哈希表\nhmap: dict = {}\n\n# 添加操作\n# 在哈希表中添加键值对 (key, value)\nhmap[12836] = \"小哈\"\nhmap[15937] = \"小啰\"\nhmap[16750] = \"小算\"\nhmap[13276] = \"小法\"\nhmap[10583] = \"小鸭\"\n\n# 查询操作\n# 向哈希表中输入键 key ,得到值 value\nname: str = hmap[15937]\n\n# 删除操作\n# 在哈希表中删除键值对 (key, value)\nhmap.pop(10583)\n
hash_map.cpp
/* 初始化哈希表 */\nunordered_map<int, string> map;\n\n/* 添加操作 */\n// 在哈希表中添加键值对 (key, value)\nmap[12836] = \"小哈\";\nmap[15937] = \"小啰\";\nmap[16750] = \"小算\";\nmap[13276] = \"小法\";\nmap[10583] = \"小鸭\";\n\n/* 查询操作 */\n// 向哈希表中输入键 key ,得到值 value\nstring name = map[15937];\n\n/* 删除操作 */\n// 在哈希表中删除键值对 (key, value)\nmap.erase(10583);\n
hash_map.java
/* 初始化哈希表 */\nMap<Integer, String> map = new HashMap<>();\n\n/* 添加操作 */\n// 在哈希表中添加键值对 (key, value)\nmap.put(12836, \"小哈\");\nmap.put(15937, \"小啰\");\nmap.put(16750, \"小算\");\nmap.put(13276, \"小法\");\nmap.put(10583, \"小鸭\");\n\n/* 查询操作 */\n// 向哈希表中输入键 key ,得到值 value\nString name = map.get(15937);\n\n/* 删除操作 */\n// 在哈希表中删除键值对 (key, value)\nmap.remove(10583);\n
hash_map.cs
/* 初始化哈希表 */\nDictionary<int, string> map = new() {\n    /* 添加操作 */\n    // 在哈希表中添加键值对 (key, value)\n    { 12836, \"小哈\" },\n    { 15937, \"小啰\" },\n    { 16750, \"小算\" },\n    { 13276, \"小法\" },\n    { 10583, \"小鸭\" }\n};\n\n/* 查询操作 */\n// 向哈希表中输入键 key ,得到值 value\nstring name = map[15937];\n\n/* 删除操作 */\n// 在哈希表中删除键值对 (key, value)\nmap.Remove(10583);\n
hash_map_test.go
/* 初始化哈希表 */\nhmap := make(map[int]string)\n\n/* 添加操作 */\n// 在哈希表中添加键值对 (key, value)\nhmap[12836] = \"小哈\"\nhmap[15937] = \"小啰\"\nhmap[16750] = \"小算\"\nhmap[13276] = \"小法\"\nhmap[10583] = \"小鸭\"\n\n/* 查询操作 */\n// 向哈希表中输入键 key ,得到值 value\nname := hmap[15937]\n\n/* 删除操作 */\n// 在哈希表中删除键值对 (key, value)\ndelete(hmap, 10583)\n
hash_map.swift
/* 初始化哈希表 */\nvar map: [Int: String] = [:]\n\n/* 添加操作 */\n// 在哈希表中添加键值对 (key, value)\nmap[12836] = \"小哈\"\nmap[15937] = \"小啰\"\nmap[16750] = \"小算\"\nmap[13276] = \"小法\"\nmap[10583] = \"小鸭\"\n\n/* 查询操作 */\n// 向哈希表中输入键 key ,得到值 value\nlet name = map[15937]!\n\n/* 删除操作 */\n// 在哈希表中删除键值对 (key, value)\nmap.removeValue(forKey: 10583)\n
hash_map.js
/* 初始化哈希表 */\nconst map = new Map();\n/* 添加操作 */\n// 在哈希表中添加键值对 (key, value)\nmap.set(12836, '小哈');\nmap.set(15937, '小啰');\nmap.set(16750, '小算');\nmap.set(13276, '小法');\nmap.set(10583, '小鸭');\n\n/* 查询操作 */\n// 向哈希表中输入键 key ,得到值 value\nlet name = map.get(15937);\n\n/* 删除操作 */\n// 在哈希表中删除键值对 (key, value)\nmap.delete(10583);\n
hash_map.ts
/* 初始化哈希表 */\nconst map = new Map<number, string>();\n/* 添加操作 */\n// 在哈希表中添加键值对 (key, value)\nmap.set(12836, '小哈');\nmap.set(15937, '小啰');\nmap.set(16750, '小算');\nmap.set(13276, '小法');\nmap.set(10583, '小鸭');\nconsole.info('\\n添加完成后,哈希表为\\nKey -> Value');\nconsole.info(map);\n\n/* 查询操作 */\n// 向哈希表中输入键 key ,得到值 value\nlet name = map.get(15937);\nconsole.info('\\n输入学号 15937 ,查询到姓名 ' + name);\n\n/* 删除操作 */\n// 在哈希表中删除键值对 (key, value)\nmap.delete(10583);\nconsole.info('\\n删除 10583 后,哈希表为\\nKey -> Value');\nconsole.info(map);\n
hash_map.dart
/* 初始化哈希表 */\nMap<int, String> map = {};\n\n/* 添加操作 */\n// 在哈希表中添加键值对 (key, value)\nmap[12836] = \"小哈\";\nmap[15937] = \"小啰\";\nmap[16750] = \"小算\";\nmap[13276] = \"小法\";\nmap[10583] = \"小鸭\";\n\n/* 查询操作 */\n// 向哈希表中输入键 key ,得到值 value\nString name = map[15937];\n\n/* 删除操作 */\n// 在哈希表中删除键值对 (key, value)\nmap.remove(10583);\n
hash_map.rs
use std::collections::HashMap;\n\n/* 初始化哈希表 */\nlet mut map: HashMap<i32, String> = HashMap::new();\n\n/* 添加操作 */\n// 在哈希表中添加键值对 (key, value)\nmap.insert(12836, \"小哈\".to_string());\nmap.insert(15937, \"小啰\".to_string());\nmap.insert(16750, \"小算\".to_string());\nmap.insert(13279, \"小法\".to_string());\nmap.insert(10583, \"小鸭\".to_string());\n\n/* 查询操作 */\n// 向哈希表中输入键 key ,得到值 value\nlet _name: Option<&String> = map.get(&15937);\n\n/* 删除操作 */\n// 在哈希表中删除键值对 (key, value)\nlet _removed_value: Option<String> = map.remove(&10583);\n
hash_map.c
// C 未提供内置哈希表\n
hash_map.kt
/* 初始化哈希表 */\nval map = HashMap<Int,String>()\n\n/* 添加操作 */\n// 在哈希表中添加键值对 (key, value)\nmap[12836] = \"小哈\"\nmap[15937] = \"小啰\"\nmap[16750] = \"小算\"\nmap[13276] = \"小法\"\nmap[10583] = \"小鸭\"\n\n/* 查询操作 */\n// 向哈希表中输入键 key ,得到值 value\nval name = map[15937]\n\n/* 删除操作 */\n// 在哈希表中删除键值对 (key, value)\nmap.remove(10583)\n
hash_map.rb
# 初始化哈希表\nhmap = {}\n\n# 添加操作\n# 在哈希表中添加键值对 (key, value)\nhmap[12836] = \"小哈\"\nhmap[15937] = \"小啰\"\nhmap[16750] = \"小算\"\nhmap[13276] = \"小法\"\nhmap[10583] = \"小鸭\"\n\n# 查询操作\n# 向哈希表中输入键 key ,得到值 value\nname = hmap[15937]\n\n# 删除操作\n# 在哈希表中删除键值对 (key, value)\nhmap.delete(10583)\n
可视化运行

全屏观看 >

哈希表有三种常用的遍历方式:遍历键值对、遍历键和遍历值。示例代码如下:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby hash_map.py
# 遍历哈希表\n# 遍历键值对 key->value\nfor key, value in hmap.items():\n    print(key, \"->\", value)\n# 单独遍历键 key\nfor key in hmap.keys():\n    print(key)\n# 单独遍历值 value\nfor value in hmap.values():\n    print(value)\n
hash_map.cpp
/* 遍历哈希表 */\n// 遍历键值对 key->value\nfor (auto kv: map) {\n    cout << kv.first << \" -> \" << kv.second << endl;\n}\n// 使用迭代器遍历 key->value\nfor (auto iter = map.begin(); iter != map.end(); iter++) {\n    cout << iter->first << \"->\" << iter->second << endl;\n}\n
hash_map.java
/* 遍历哈希表 */\n// 遍历键值对 key->value\nfor (Map.Entry <Integer, String> kv: map.entrySet()) {\n    System.out.println(kv.getKey() + \" -> \" + kv.getValue());\n}\n// 单独遍历键 key\nfor (int key: map.keySet()) {\n    System.out.println(key);\n}\n// 单独遍历值 value\nfor (String val: map.values()) {\n    System.out.println(val);\n}\n
hash_map.cs
/* 遍历哈希表 */\n// 遍历键值对 Key->Value\nforeach (var kv in map) {\n    Console.WriteLine(kv.Key + \" -> \" + kv.Value);\n}\n// 单独遍历键 key\nforeach (int key in map.Keys) {\n    Console.WriteLine(key);\n}\n// 单独遍历值 value\nforeach (string val in map.Values) {\n    Console.WriteLine(val);\n}\n
hash_map_test.go
/* 遍历哈希表 */\n// 遍历键值对 key->value\nfor key, value := range hmap {\n    fmt.Println(key, \"->\", value)\n}\n// 单独遍历键 key\nfor key := range hmap {\n    fmt.Println(key)\n}\n// 单独遍历值 value\nfor _, value := range hmap {\n    fmt.Println(value)\n}\n
hash_map.swift
/* 遍历哈希表 */\n// 遍历键值对 Key->Value\nfor (key, value) in map {\n    print(\"\\(key) -> \\(value)\")\n}\n// 单独遍历键 Key\nfor key in map.keys {\n    print(key)\n}\n// 单独遍历值 Value\nfor value in map.values {\n    print(value)\n}\n
hash_map.js
/* 遍历哈希表 */\nconsole.info('\\n遍历键值对 Key->Value');\nfor (const [k, v] of map.entries()) {\n    console.info(k + ' -> ' + v);\n}\nconsole.info('\\n单独遍历键 Key');\nfor (const k of map.keys()) {\n    console.info(k);\n}\nconsole.info('\\n单独遍历值 Value');\nfor (const v of map.values()) {\n    console.info(v);\n}\n
hash_map.ts
/* 遍历哈希表 */\nconsole.info('\\n遍历键值对 Key->Value');\nfor (const [k, v] of map.entries()) {\n    console.info(k + ' -> ' + v);\n}\nconsole.info('\\n单独遍历键 Key');\nfor (const k of map.keys()) {\n    console.info(k);\n}\nconsole.info('\\n单独遍历值 Value');\nfor (const v of map.values()) {\n    console.info(v);\n}\n
hash_map.dart
/* 遍历哈希表 */\n// 遍历键值对 Key->Value\nmap.forEach((key, value) {\n  print('$key -> $value');\n});\n\n// 单独遍历键 Key\nmap.keys.forEach((key) {\n  print(key);\n});\n\n// 单独遍历值 Value\nmap.values.forEach((value) {\n  print(value);\n});\n
hash_map.rs
/* 遍历哈希表 */\n// 遍历键值对 Key->Value\nfor (key, value) in &map {\n    println!(\"{key} -> {value}\");\n}\n\n// 单独遍历键 Key\nfor key in map.keys() {\n    println!(\"{key}\");\n}\n\n// 单独遍历值 Value\nfor value in map.values() {\n    println!(\"{value}\");\n}\n
hash_map.c
// C 未提供内置哈希表\n
hash_map.kt
/* 遍历哈希表 */\n// 遍历键值对 key->value\nfor ((key, value) in map) {\n    println(\"$key -> $value\")\n}\n// 单独遍历键 key\nfor (key in map.keys) {\n    println(key)\n}\n// 单独遍历值 value\nfor (_val in map.values) {\n    println(_val)\n}\n
hash_map.rb
# 遍历哈希表\n# 遍历键值对 key->value\nhmap.entries.each { |key, value| puts \"#{key} -> #{value}\" }\n\n# 单独遍历键 key\nhmap.keys.each { |key| puts key }\n\n# 单独遍历值 value\nhmap.values.each { |val| puts val }\n
可视化运行

全屏观看 >

","path":["第 6 章   哈希表","6.1   哈希表"],"tags":[]},{"location":"chapter_hashing/hash_map/#612","level":2,"title":"6.1.2   哈希表简单实现","text":"

我们先考虑最简单的情况,仅用一个数组来实现哈希表。在哈希表中,我们将数组中的每个空位称为桶(bucket),每个桶可存储一个键值对。因此,查询操作就是找到 key 对应的桶,并在桶中获取 value

那么,如何基于 key 定位对应的桶呢?这是通过哈希函数(hash function)实现的。哈希函数的作用是将一个较大的输入空间映射到一个较小的输出空间。在哈希表中,输入空间是所有 key ,输出空间是所有桶(数组索引)。换句话说,输入一个 key ,我们可以通过哈希函数得到该 key 对应的键值对在数组中的存储位置。

输入一个 key ,哈希函数的计算过程分为以下两步。

  1. 通过某种哈希算法 hash() 计算得到哈希值。
  2. 将哈希值对桶数量(数组长度)capacity 取模,从而获取该 key 对应的桶(数组索引)index
index = hash(key) % capacity\n

随后,我们就可以利用 index 在哈希表中访问对应的桶,从而获取 value

设数组长度 capacity = 100、哈希算法 hash(key) = key ,易得哈希函数为 key % 100 。图 6-2 以 key 学号和 value 姓名为例,展示了哈希函数的工作原理。

图 6-2   哈希函数工作原理

以下代码实现了一个简单哈希表。其中,我们将 keyvalue 封装成一个类 Pair ,以表示键值对。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_hash_map.py
class Pair:\n    \"\"\"键值对\"\"\"\n\n    def __init__(self, key: int, val: str):\n        self.key = key\n        self.val = val\n\nclass ArrayHashMap:\n    \"\"\"基于数组实现的哈希表\"\"\"\n\n    def __init__(self):\n        \"\"\"构造方法\"\"\"\n        # 初始化数组,包含 100 个桶\n        self.buckets: list[Pair | None] = [None] * 100\n\n    def hash_func(self, key: int) -> int:\n        \"\"\"哈希函数\"\"\"\n        index = key % 100\n        return index\n\n    def get(self, key: int) -> str | None:\n        \"\"\"查询操作\"\"\"\n        index: int = self.hash_func(key)\n        pair: Pair = self.buckets[index]\n        if pair is None:\n            return None\n        return pair.val\n\n    def put(self, key: int, val: str):\n        \"\"\"添加和更新操作\"\"\"\n        pair = Pair(key, val)\n        index: int = self.hash_func(key)\n        self.buckets[index] = pair\n\n    def remove(self, key: int):\n        \"\"\"删除操作\"\"\"\n        index: int = self.hash_func(key)\n        # 置为 None ,代表删除\n        self.buckets[index] = None\n\n    def entry_set(self) -> list[Pair]:\n        \"\"\"获取所有键值对\"\"\"\n        result: list[Pair] = []\n        for pair in self.buckets:\n            if pair is not None:\n                result.append(pair)\n        return result\n\n    def key_set(self) -> list[int]:\n        \"\"\"获取所有键\"\"\"\n        result = []\n        for pair in self.buckets:\n            if pair is not None:\n                result.append(pair.key)\n        return result\n\n    def value_set(self) -> list[str]:\n        \"\"\"获取所有值\"\"\"\n        result = []\n        for pair in self.buckets:\n            if pair is not None:\n                result.append(pair.val)\n        return result\n\n    def print(self):\n        \"\"\"打印哈希表\"\"\"\n        for pair in self.buckets:\n            if pair is not None:\n                print(pair.key, \"->\", pair.val)\n
array_hash_map.cpp
/* 键值对 */\nstruct Pair {\n  public:\n    int key;\n    string val;\n    Pair(int key, string val) {\n        this->key = key;\n        this->val = val;\n    }\n};\n\n/* 基于数组实现的哈希表 */\nclass ArrayHashMap {\n  private:\n    vector<Pair *> buckets;\n\n  public:\n    ArrayHashMap() {\n        // 初始化数组,包含 100 个桶\n        buckets = vector<Pair *>(100);\n    }\n\n    ~ArrayHashMap() {\n        // 释放内存\n        for (const auto &bucket : buckets) {\n            delete bucket;\n        }\n        buckets.clear();\n    }\n\n    /* 哈希函数 */\n    int hashFunc(int key) {\n        int index = key % 100;\n        return index;\n    }\n\n    /* 查询操作 */\n    string get(int key) {\n        int index = hashFunc(key);\n        Pair *pair = buckets[index];\n        if (pair == nullptr)\n            return \"\";\n        return pair->val;\n    }\n\n    /* 添加操作 */\n    void put(int key, string val) {\n        Pair *pair = new Pair(key, val);\n        int index = hashFunc(key);\n        buckets[index] = pair;\n    }\n\n    /* 删除操作 */\n    void remove(int key) {\n        int index = hashFunc(key);\n        // 释放内存并置为 nullptr\n        delete buckets[index];\n        buckets[index] = nullptr;\n    }\n\n    /* 获取所有键值对 */\n    vector<Pair *> pairSet() {\n        vector<Pair *> pairSet;\n        for (Pair *pair : buckets) {\n            if (pair != nullptr) {\n                pairSet.push_back(pair);\n            }\n        }\n        return pairSet;\n    }\n\n    /* 获取所有键 */\n    vector<int> keySet() {\n        vector<int> keySet;\n        for (Pair *pair : buckets) {\n            if (pair != nullptr) {\n                keySet.push_back(pair->key);\n            }\n        }\n        return keySet;\n    }\n\n    /* 获取所有值 */\n    vector<string> valueSet() {\n        vector<string> valueSet;\n        for (Pair *pair : buckets) {\n            if (pair != nullptr) {\n                valueSet.push_back(pair->val);\n            }\n        }\n        return valueSet;\n    }\n\n    /* 打印哈希表 */\n    void print() {\n        for (Pair *kv : pairSet()) {\n            cout << kv->key << \" -> \" << kv->val << endl;\n        }\n    }\n};\n
array_hash_map.java
/* 键值对 */\nclass Pair {\n    public int key;\n    public String val;\n\n    public Pair(int key, String val) {\n        this.key = key;\n        this.val = val;\n    }\n}\n\n/* 基于数组实现的哈希表 */\nclass ArrayHashMap {\n    private List<Pair> buckets;\n\n    public ArrayHashMap() {\n        // 初始化数组,包含 100 个桶\n        buckets = new ArrayList<>();\n        for (int i = 0; i < 100; i++) {\n            buckets.add(null);\n        }\n    }\n\n    /* 哈希函数 */\n    private int hashFunc(int key) {\n        int index = key % 100;\n        return index;\n    }\n\n    /* 查询操作 */\n    public String get(int key) {\n        int index = hashFunc(key);\n        Pair pair = buckets.get(index);\n        if (pair == null)\n            return null;\n        return pair.val;\n    }\n\n    /* 添加操作 */\n    public void put(int key, String val) {\n        Pair pair = new Pair(key, val);\n        int index = hashFunc(key);\n        buckets.set(index, pair);\n    }\n\n    /* 删除操作 */\n    public void remove(int key) {\n        int index = hashFunc(key);\n        // 置为 null ,代表删除\n        buckets.set(index, null);\n    }\n\n    /* 获取所有键值对 */\n    public List<Pair> pairSet() {\n        List<Pair> pairSet = new ArrayList<>();\n        for (Pair pair : buckets) {\n            if (pair != null)\n                pairSet.add(pair);\n        }\n        return pairSet;\n    }\n\n    /* 获取所有键 */\n    public List<Integer> keySet() {\n        List<Integer> keySet = new ArrayList<>();\n        for (Pair pair : buckets) {\n            if (pair != null)\n                keySet.add(pair.key);\n        }\n        return keySet;\n    }\n\n    /* 获取所有值 */\n    public List<String> valueSet() {\n        List<String> valueSet = new ArrayList<>();\n        for (Pair pair : buckets) {\n            if (pair != null)\n                valueSet.add(pair.val);\n        }\n        return valueSet;\n    }\n\n    /* 打印哈希表 */\n    public void print() {\n        for (Pair kv : pairSet()) {\n            System.out.println(kv.key + \" -> \" + kv.val);\n        }\n    }\n}\n
array_hash_map.cs
/* 键值对 int->string */\nclass Pair(int key, string val) {\n    public int key = key;\n    public string val = val;\n}\n\n/* 基于数组实现的哈希表 */\nclass ArrayHashMap {\n    List<Pair?> buckets;\n    public ArrayHashMap() {\n        // 初始化数组,包含 100 个桶\n        buckets = [];\n        for (int i = 0; i < 100; i++) {\n            buckets.Add(null);\n        }\n    }\n\n    /* 哈希函数 */\n    int HashFunc(int key) {\n        int index = key % 100;\n        return index;\n    }\n\n    /* 查询操作 */\n    public string? Get(int key) {\n        int index = HashFunc(key);\n        Pair? pair = buckets[index];\n        if (pair == null) return null;\n        return pair.val;\n    }\n\n    /* 添加操作 */\n    public void Put(int key, string val) {\n        Pair pair = new(key, val);\n        int index = HashFunc(key);\n        buckets[index] = pair;\n    }\n\n    /* 删除操作 */\n    public void Remove(int key) {\n        int index = HashFunc(key);\n        // 置为 null ,代表删除\n        buckets[index] = null;\n    }\n\n    /* 获取所有键值对 */\n    public List<Pair> PairSet() {\n        List<Pair> pairSet = [];\n        foreach (Pair? pair in buckets) {\n            if (pair != null)\n                pairSet.Add(pair);\n        }\n        return pairSet;\n    }\n\n    /* 获取所有键 */\n    public List<int> KeySet() {\n        List<int> keySet = [];\n        foreach (Pair? pair in buckets) {\n            if (pair != null)\n                keySet.Add(pair.key);\n        }\n        return keySet;\n    }\n\n    /* 获取所有值 */\n    public List<string> ValueSet() {\n        List<string> valueSet = [];\n        foreach (Pair? pair in buckets) {\n            if (pair != null)\n                valueSet.Add(pair.val);\n        }\n        return valueSet;\n    }\n\n    /* 打印哈希表 */\n    public void Print() {\n        foreach (Pair kv in PairSet()) {\n            Console.WriteLine(kv.key + \" -> \" + kv.val);\n        }\n    }\n}\n
array_hash_map.go
/* 键值对 */\ntype pair struct {\n    key int\n    val string\n}\n\n/* 基于数组实现的哈希表 */\ntype arrayHashMap struct {\n    buckets []*pair\n}\n\n/* 初始化哈希表 */\nfunc newArrayHashMap() *arrayHashMap {\n    // 初始化数组,包含 100 个桶\n    buckets := make([]*pair, 100)\n    return &arrayHashMap{buckets: buckets}\n}\n\n/* 哈希函数 */\nfunc (a *arrayHashMap) hashFunc(key int) int {\n    index := key % 100\n    return index\n}\n\n/* 查询操作 */\nfunc (a *arrayHashMap) get(key int) string {\n    index := a.hashFunc(key)\n    pair := a.buckets[index]\n    if pair == nil {\n        return \"Not Found\"\n    }\n    return pair.val\n}\n\n/* 添加操作 */\nfunc (a *arrayHashMap) put(key int, val string) {\n    pair := &pair{key: key, val: val}\n    index := a.hashFunc(key)\n    a.buckets[index] = pair\n}\n\n/* 删除操作 */\nfunc (a *arrayHashMap) remove(key int) {\n    index := a.hashFunc(key)\n    // 置为 nil ,代表删除\n    a.buckets[index] = nil\n}\n\n/* 获取所有键对 */\nfunc (a *arrayHashMap) pairSet() []*pair {\n    var pairs []*pair\n    for _, pair := range a.buckets {\n        if pair != nil {\n            pairs = append(pairs, pair)\n        }\n    }\n    return pairs\n}\n\n/* 获取所有键 */\nfunc (a *arrayHashMap) keySet() []int {\n    var keys []int\n    for _, pair := range a.buckets {\n        if pair != nil {\n            keys = append(keys, pair.key)\n        }\n    }\n    return keys\n}\n\n/* 获取所有值 */\nfunc (a *arrayHashMap) valueSet() []string {\n    var values []string\n    for _, pair := range a.buckets {\n        if pair != nil {\n            values = append(values, pair.val)\n        }\n    }\n    return values\n}\n\n/* 打印哈希表 */\nfunc (a *arrayHashMap) print() {\n    for _, pair := range a.buckets {\n        if pair != nil {\n            fmt.Println(pair.key, \"->\", pair.val)\n        }\n    }\n}\n
array_hash_map.swift
/* 键值对 */\nclass Pair: Equatable {\n    public var key: Int\n    public var val: String\n\n    public init(key: Int, val: String) {\n        self.key = key\n        self.val = val\n    }\n\n    public static func == (lhs: Pair, rhs: Pair) -> Bool {\n        lhs.key == rhs.key && lhs.val == rhs.val\n    }\n}\n\n/* 基于数组实现的哈希表 */\nclass ArrayHashMap {\n    private var buckets: [Pair?]\n\n    init() {\n        // 初始化数组,包含 100 个桶\n        buckets = Array(repeating: nil, count: 100)\n    }\n\n    /* 哈希函数 */\n    private func hashFunc(key: Int) -> Int {\n        let index = key % 100\n        return index\n    }\n\n    /* 查询操作 */\n    func get(key: Int) -> String? {\n        let index = hashFunc(key: key)\n        let pair = buckets[index]\n        return pair?.val\n    }\n\n    /* 添加操作 */\n    func put(key: Int, val: String) {\n        let pair = Pair(key: key, val: val)\n        let index = hashFunc(key: key)\n        buckets[index] = pair\n    }\n\n    /* 删除操作 */\n    func remove(key: Int) {\n        let index = hashFunc(key: key)\n        // 置为 nil ,代表删除\n        buckets[index] = nil\n    }\n\n    /* 获取所有键值对 */\n    func pairSet() -> [Pair] {\n        buckets.compactMap { $0 }\n    }\n\n    /* 获取所有键 */\n    func keySet() -> [Int] {\n        buckets.compactMap { $0?.key }\n    }\n\n    /* 获取所有值 */\n    func valueSet() -> [String] {\n        buckets.compactMap { $0?.val }\n    }\n\n    /* 打印哈希表 */\n    func print() {\n        for pair in pairSet() {\n            Swift.print(\"\\(pair.key) -> \\(pair.val)\")\n        }\n    }\n}\n
array_hash_map.js
/* 键值对 Number -> String */\nclass Pair {\n    constructor(key, val) {\n        this.key = key;\n        this.val = val;\n    }\n}\n\n/* 基于数组实现的哈希表 */\nclass ArrayHashMap {\n    #buckets;\n    constructor() {\n        // 初始化数组,包含 100 个桶\n        this.#buckets = new Array(100).fill(null);\n    }\n\n    /* 哈希函数 */\n    #hashFunc(key) {\n        return key % 100;\n    }\n\n    /* 查询操作 */\n    get(key) {\n        let index = this.#hashFunc(key);\n        let pair = this.#buckets[index];\n        if (pair === null) return null;\n        return pair.val;\n    }\n\n    /* 添加操作 */\n    set(key, val) {\n        let index = this.#hashFunc(key);\n        this.#buckets[index] = new Pair(key, val);\n    }\n\n    /* 删除操作 */\n    delete(key) {\n        let index = this.#hashFunc(key);\n        // 置为 null ,代表删除\n        this.#buckets[index] = null;\n    }\n\n    /* 获取所有键值对 */\n    entries() {\n        let arr = [];\n        for (let i = 0; i < this.#buckets.length; i++) {\n            if (this.#buckets[i]) {\n                arr.push(this.#buckets[i]);\n            }\n        }\n        return arr;\n    }\n\n    /* 获取所有键 */\n    keys() {\n        let arr = [];\n        for (let i = 0; i < this.#buckets.length; i++) {\n            if (this.#buckets[i]) {\n                arr.push(this.#buckets[i].key);\n            }\n        }\n        return arr;\n    }\n\n    /* 获取所有值 */\n    values() {\n        let arr = [];\n        for (let i = 0; i < this.#buckets.length; i++) {\n            if (this.#buckets[i]) {\n                arr.push(this.#buckets[i].val);\n            }\n        }\n        return arr;\n    }\n\n    /* 打印哈希表 */\n    print() {\n        let pairSet = this.entries();\n        for (const pair of pairSet) {\n            console.info(`${pair.key} -> ${pair.val}`);\n        }\n    }\n}\n
array_hash_map.ts
/* 键值对 Number -> String */\nclass Pair {\n    public key: number;\n    public val: string;\n\n    constructor(key: number, val: string) {\n        this.key = key;\n        this.val = val;\n    }\n}\n\n/* 基于数组实现的哈希表 */\nclass ArrayHashMap {\n    private readonly buckets: (Pair | null)[];\n\n    constructor() {\n        // 初始化数组,包含 100 个桶\n        this.buckets = new Array(100).fill(null);\n    }\n\n    /* 哈希函数 */\n    private hashFunc(key: number): number {\n        return key % 100;\n    }\n\n    /* 查询操作 */\n    public get(key: number): string | null {\n        let index = this.hashFunc(key);\n        let pair = this.buckets[index];\n        if (pair === null) return null;\n        return pair.val;\n    }\n\n    /* 添加操作 */\n    public set(key: number, val: string) {\n        let index = this.hashFunc(key);\n        this.buckets[index] = new Pair(key, val);\n    }\n\n    /* 删除操作 */\n    public delete(key: number) {\n        let index = this.hashFunc(key);\n        // 置为 null ,代表删除\n        this.buckets[index] = null;\n    }\n\n    /* 获取所有键值对 */\n    public entries(): (Pair | null)[] {\n        let arr: (Pair | null)[] = [];\n        for (let i = 0; i < this.buckets.length; i++) {\n            if (this.buckets[i]) {\n                arr.push(this.buckets[i]);\n            }\n        }\n        return arr;\n    }\n\n    /* 获取所有键 */\n    public keys(): (number | undefined)[] {\n        let arr: (number | undefined)[] = [];\n        for (let i = 0; i < this.buckets.length; i++) {\n            if (this.buckets[i]) {\n                arr.push(this.buckets[i].key);\n            }\n        }\n        return arr;\n    }\n\n    /* 获取所有值 */\n    public values(): (string | undefined)[] {\n        let arr: (string | undefined)[] = [];\n        for (let i = 0; i < this.buckets.length; i++) {\n            if (this.buckets[i]) {\n                arr.push(this.buckets[i].val);\n            }\n        }\n        return arr;\n    }\n\n    /* 打印哈希表 */\n    public print() {\n        let pairSet = this.entries();\n        for (const pair of pairSet) {\n            console.info(`${pair.key} -> ${pair.val}`);\n        }\n    }\n}\n
array_hash_map.dart
/* 键值对 */\nclass Pair {\n  int key;\n  String val;\n  Pair(this.key, this.val);\n}\n\n/* 基于数组实现的哈希表 */\nclass ArrayHashMap {\n  late List<Pair?> _buckets;\n\n  ArrayHashMap() {\n    // 初始化数组,包含 100 个桶\n    _buckets = List.filled(100, null);\n  }\n\n  /* 哈希函数 */\n  int _hashFunc(int key) {\n    final int index = key % 100;\n    return index;\n  }\n\n  /* 查询操作 */\n  String? get(int key) {\n    final int index = _hashFunc(key);\n    final Pair? pair = _buckets[index];\n    if (pair == null) {\n      return null;\n    }\n    return pair.val;\n  }\n\n  /* 添加操作 */\n  void put(int key, String val) {\n    final Pair pair = Pair(key, val);\n    final int index = _hashFunc(key);\n    _buckets[index] = pair;\n  }\n\n  /* 删除操作 */\n  void remove(int key) {\n    final int index = _hashFunc(key);\n    _buckets[index] = null;\n  }\n\n  /* 获取所有键值对 */\n  List<Pair> pairSet() {\n    List<Pair> pairSet = [];\n    for (final Pair? pair in _buckets) {\n      if (pair != null) {\n        pairSet.add(pair);\n      }\n    }\n    return pairSet;\n  }\n\n  /* 获取所有键 */\n  List<int> keySet() {\n    List<int> keySet = [];\n    for (final Pair? pair in _buckets) {\n      if (pair != null) {\n        keySet.add(pair.key);\n      }\n    }\n    return keySet;\n  }\n\n  /* 获取所有值 */\n  List<String> values() {\n    List<String> valueSet = [];\n    for (final Pair? pair in _buckets) {\n      if (pair != null) {\n        valueSet.add(pair.val);\n      }\n    }\n    return valueSet;\n  }\n\n  /* 打印哈希表 */\n  void printHashMap() {\n    for (final Pair kv in pairSet()) {\n      print(\"${kv.key} -> ${kv.val}\");\n    }\n  }\n}\n
array_hash_map.rs
/* 键值对 */\n#[derive(Debug, Clone, PartialEq)]\npub struct Pair {\n    pub key: i32,\n    pub val: String,\n}\n\n/* 基于数组实现的哈希表 */\npub struct ArrayHashMap {\n    buckets: Vec<Option<Pair>>,\n}\n\nimpl ArrayHashMap {\n    pub fn new() -> ArrayHashMap {\n        // 初始化数组,包含 100 个桶\n        Self {\n            buckets: vec![None; 100],\n        }\n    }\n\n    /* 哈希函数 */\n    fn hash_func(&self, key: i32) -> usize {\n        key as usize % 100\n    }\n\n    /* 查询操作 */\n    pub fn get(&self, key: i32) -> Option<&String> {\n        let index = self.hash_func(key);\n        self.buckets[index].as_ref().map(|pair| &pair.val)\n    }\n\n    /* 添加操作 */\n    pub fn put(&mut self, key: i32, val: &str) {\n        let index = self.hash_func(key);\n        self.buckets[index] = Some(Pair {\n            key,\n            val: val.to_string(),\n        });\n    }\n\n    /* 删除操作 */\n    pub fn remove(&mut self, key: i32) {\n        let index = self.hash_func(key);\n        // 置为 None ,代表删除\n        self.buckets[index] = None;\n    }\n\n    /* 获取所有键值对 */\n    pub fn entry_set(&self) -> Vec<&Pair> {\n        self.buckets\n            .iter()\n            .filter_map(|pair| pair.as_ref())\n            .collect()\n    }\n\n    /* 获取所有键 */\n    pub fn key_set(&self) -> Vec<&i32> {\n        self.buckets\n            .iter()\n            .filter_map(|pair| pair.as_ref().map(|pair| &pair.key))\n            .collect()\n    }\n\n    /* 获取所有值 */\n    pub fn value_set(&self) -> Vec<&String> {\n        self.buckets\n            .iter()\n            .filter_map(|pair| pair.as_ref().map(|pair| &pair.val))\n            .collect()\n    }\n\n    /* 打印哈希表 */\n    pub fn print(&self) {\n        for pair in self.entry_set() {\n            println!(\"{} -> {}\", pair.key, pair.val);\n        }\n    }\n}\n
array_hash_map.c
/* 键值对 int->string */\ntypedef struct {\n    int key;\n    char *val;\n} Pair;\n\n/* 基于数组实现的哈希表 */\ntypedef struct {\n    Pair *buckets[MAX_SIZE];\n} ArrayHashMap;\n\n/* 构造函数 */\nArrayHashMap *newArrayHashMap() {\n    ArrayHashMap *hmap = malloc(sizeof(ArrayHashMap));\n    for (int i=0; i < MAX_SIZE; i++) {\n        hmap->buckets[i] = NULL;\n    }\n    return hmap;\n}\n\n/* 析构函数 */\nvoid delArrayHashMap(ArrayHashMap *hmap) {\n    for (int i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            free(hmap->buckets[i]->val);\n            free(hmap->buckets[i]);\n        }\n    }\n    free(hmap);\n}\n\n/* 添加操作 */\nvoid put(ArrayHashMap *hmap, const int key, const char *val) {\n    Pair *Pair = malloc(sizeof(Pair));\n    Pair->key = key;\n    Pair->val = malloc(strlen(val) + 1);\n    strcpy(Pair->val, val);\n\n    int index = hashFunc(key);\n    hmap->buckets[index] = Pair;\n}\n\n/* 删除操作 */\nvoid removeItem(ArrayHashMap *hmap, const int key) {\n    int index = hashFunc(key);\n    free(hmap->buckets[index]->val);\n    free(hmap->buckets[index]);\n    hmap->buckets[index] = NULL;\n}\n\n/* 获取所有键值对 */\nvoid pairSet(ArrayHashMap *hmap, MapSet *set) {\n    Pair *entries;\n    int i = 0, index = 0;\n    int total = 0;\n    /* 统计有效键值对数量 */\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            total++;\n        }\n    }\n    entries = malloc(sizeof(Pair) * total);\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            entries[index].key = hmap->buckets[i]->key;\n            entries[index].val = malloc(strlen(hmap->buckets[i]->val) + 1);\n            strcpy(entries[index].val, hmap->buckets[i]->val);\n            index++;\n        }\n    }\n    set->set = entries;\n    set->len = total;\n}\n\n/* 获取所有键 */\nvoid keySet(ArrayHashMap *hmap, MapSet *set) {\n    int *keys;\n    int i = 0, index = 0;\n    int total = 0;\n    /* 统计有效键值对数量 */\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            total++;\n        }\n    }\n    keys = malloc(total * sizeof(int));\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            keys[index] = hmap->buckets[i]->key;\n            index++;\n        }\n    }\n    set->set = keys;\n    set->len = total;\n}\n\n/* 获取所有值 */\nvoid valueSet(ArrayHashMap *hmap, MapSet *set) {\n    char **vals;\n    int i = 0, index = 0;\n    int total = 0;\n    /* 统计有效键值对数量 */\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            total++;\n        }\n    }\n    vals = malloc(total * sizeof(char *));\n    for (i = 0; i < MAX_SIZE; i++) {\n        if (hmap->buckets[i] != NULL) {\n            vals[index] = hmap->buckets[i]->val;\n            index++;\n        }\n    }\n    set->set = vals;\n    set->len = total;\n}\n\n/* 打印哈希表 */\nvoid print(ArrayHashMap *hmap) {\n    int i;\n    MapSet set;\n    pairSet(hmap, &set);\n    Pair *entries = (Pair *)set.set;\n    for (i = 0; i < set.len; i++) {\n        printf(\"%d -> %s\\n\", entries[i].key, entries[i].val);\n    }\n    free(set.set);\n}\n
array_hash_map.kt
/* 键值对 */\nclass Pair(\n    var key: Int,\n    var _val: String\n)\n\n/* 基于数组实现的哈希表 */\nclass ArrayHashMap {\n    // 初始化数组,包含 100 个桶\n    private val buckets = arrayOfNulls<Pair>(100)\n\n    /* 哈希函数 */\n    fun hashFunc(key: Int): Int {\n        val index = key % 100\n        return index\n    }\n\n    /* 查询操作 */\n    fun get(key: Int): String? {\n        val index = hashFunc(key)\n        val pair = buckets[index] ?: return null\n        return pair._val\n    }\n\n    /* 添加操作 */\n    fun put(key: Int, _val: String) {\n        val pair = Pair(key, _val)\n        val index = hashFunc(key)\n        buckets[index] = pair\n    }\n\n    /* 删除操作 */\n    fun remove(key: Int) {\n        val index = hashFunc(key)\n        // 置为 null ,代表删除\n        buckets[index] = null\n    }\n\n    /* 获取所有键值对 */\n    fun pairSet(): MutableList<Pair> {\n        val pairSet = mutableListOf<Pair>()\n        for (pair in buckets) {\n            if (pair != null)\n                pairSet.add(pair)\n        }\n        return pairSet\n    }\n\n    /* 获取所有键 */\n    fun keySet(): MutableList<Int> {\n        val keySet = mutableListOf<Int>()\n        for (pair in buckets) {\n            if (pair != null)\n                keySet.add(pair.key)\n        }\n        return keySet\n    }\n\n    /* 获取所有值 */\n    fun valueSet(): MutableList<String> {\n        val valueSet = mutableListOf<String>()\n        for (pair in buckets) {\n            if (pair != null)\n                valueSet.add(pair._val)\n        }\n        return valueSet\n    }\n\n    /* 打印哈希表 */\n    fun print() {\n        for (kv in pairSet()) {\n            val key = kv.key\n            val _val = kv._val\n            println(\"$key -> $_val\")\n        }\n    }\n}\n
array_hash_map.rb
### 键值对 ###\nclass Pair\n  attr_accessor :key, :val\n\n  def initialize(key, val)\n    @key = key\n    @val = val\n  end\nend\n\n### 基于数组实现的哈希表 ###\nclass ArrayHashMap\n  ### 构造方法 ###\n  def initialize\n    # 初始化数组,包含 100 个桶\n    @buckets = Array.new(100)\n  end\n\n  ### 哈希函数 ###\n  def hash_func(key)\n    index = key % 100\n  end\n\n  ### 查询操作 ###\n  def get(key)\n    index = hash_func(key)\n    pair = @buckets[index]\n\n    return if pair.nil?\n    pair.val\n  end\n\n  ### 添加操作 ###\n  def put(key, val)\n    pair = Pair.new(key, val)\n    index = hash_func(key)\n    @buckets[index] = pair\n  end\n\n  ### 删除操作 ###\n  def remove(key)\n    index = hash_func(key)\n    # 置为 nil ,代表删除\n    @buckets[index] = nil\n  end\n\n  ### 获取所有键值对 ###\n  def entry_set\n    result = []\n    @buckets.each { |pair| result << pair unless pair.nil? }\n    result\n  end\n\n  ### 获取所有键 ###\n  def key_set\n    result = []\n    @buckets.each { |pair| result << pair.key unless pair.nil? }\n    result\n  end\n\n  ### 获取所有值 ###\n  def value_set\n    result = []\n    @buckets.each { |pair| result << pair.val unless pair.nil? }\n    result\n  end\n\n  ### 打印哈希表 ###\n  def print\n    @buckets.each { |pair| puts \"#{pair.key} -> #{pair.val}\" unless pair.nil? }\n  end\nend\n
可视化运行

全屏观看 >

","path":["第 6 章   哈希表","6.1   哈希表"],"tags":[]},{"location":"chapter_hashing/hash_map/#613","level":2,"title":"6.1.3   哈希冲突与扩容","text":"

从本质上看,哈希函数的作用是将所有 key 构成的输入空间映射到数组所有索引构成的输出空间,而输入空间往往远大于输出空间。因此,理论上一定存在“多个输入对应相同输出”的情况。

对于上述示例中的哈希函数,当输入的 key 后两位相同时,哈希函数的输出结果也相同。例如,查询学号为 12836 和 20336 的两个学生时,我们得到:

12836 % 100 = 36\n20336 % 100 = 36\n

如图 6-3 所示,两个学号指向了同一个姓名,这显然是不对的。我们将这种多个输入对应同一输出的情况称为哈希冲突(hash collision)。

图 6-3   哈希冲突示例

容易想到,哈希表容量 \\(n\\) 越大,多个 key 被分配到同一个桶中的概率就越低,冲突就越少。因此,我们可以通过扩容哈希表来减少哈希冲突。

如图 6-4 所示,扩容前键值对 (136, A)(236, D) 发生冲突,扩容后冲突消失。

图 6-4   哈希表扩容

类似于数组扩容,哈希表扩容需将所有键值对从原哈希表迁移至新哈希表,非常耗时;并且由于哈希表容量 capacity 改变,我们需要通过哈希函数来重新计算所有键值对的存储位置,这进一步增加了扩容过程的计算开销。为此,编程语言通常会预留足够大的哈希表容量,防止频繁扩容。

负载因子(load factor)是哈希表的一个重要概念,其定义为哈希表的元素数量除以桶数量,用于衡量哈希冲突的严重程度,也常作为哈希表扩容的触发条件。例如在 Java 中,当负载因子超过 \\(0.75\\) 时,系统会将哈希表扩容至原先的 \\(2\\) 倍。

","path":["第 6 章   哈希表","6.1   哈希表"],"tags":[]},{"location":"chapter_hashing/summary/","level":1,"title":"6.4   小结","text":"","path":["第 6 章   哈希表","6.4   小结"],"tags":[]},{"location":"chapter_hashing/summary/#1","level":3,"title":"1.   重点回顾","text":"","path":["第 6 章   哈希表","6.4   小结"],"tags":[]},{"location":"chapter_hashing/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q:哈希表的时间复杂度在什么情况下是 \\(O(n)\\) ?

当哈希冲突比较严重时,哈希表的时间复杂度会退化至 \\(O(n)\\) 。当哈希函数设计得比较好、容量设置比较合理、冲突比较平均时,时间复杂度是 \\(O(1)\\) 。我们使用编程语言内置的哈希表时,通常认为时间复杂度是 \\(O(1)\\) 。

Q:为什么不使用哈希函数 \\(f(x) = x\\) 呢?这样就不会有冲突了。

在 \\(f(x) = x\\) 哈希函数下,每个元素对应唯一的桶索引,这与数组等价。然而,输入空间通常远大于输出空间(数组长度),因此哈希函数的最后一步往往是对数组长度取模。换句话说,哈希表的目标是将一个较大的状态空间映射到一个较小的空间,并提供 \\(O(1)\\) 的查询效率。

Q:哈希表底层实现是数组、链表、二叉树,但为什么效率可以比它们更高呢?

首先,哈希表的时间效率变高,但空间效率变低了。哈希表有相当一部分内存未使用。

其次,只是在特定使用场景下时间效率变高了。如果一个功能能够在相同的时间复杂度下使用数组或链表实现,那么通常比哈希表更快。这是因为哈希函数计算需要开销,时间复杂度的常数项更大。

最后,哈希表的时间复杂度可能发生劣化。例如在链式地址中,我们采取在链表或红黑树中执行查找操作,仍然有退化至 \\(O(n)\\) 时间的风险。

Q:多次哈希有不能直接删除元素的缺陷吗?标记为已删除的空间还能再次使用吗?

多次哈希是开放寻址的一种,开放寻址法都有不能直接删除元素的缺陷,需要通过标记删除。标记为已删除的空间可以再次使用。当将新元素插入哈希表,并且通过哈希函数找到标记为已删除的位置时,该位置可以被新元素使用。这样做既能保持哈希表的探测序列不变,又能保证哈希表的空间使用率。

Q:为什么在线性探测中,查找元素的时候会出现哈希冲突呢?

查找的时候通过哈希函数找到对应的桶和键值对,发现 key 不匹配,这就代表有哈希冲突。因此,线性探测法会根据预先设定的步长依次向下查找,直至找到正确的键值对或无法找到跳出为止。

Q:为什么哈希表扩容能够缓解哈希冲突?

哈希函数的最后一步往往是对数组长度 \\(n\\) 取模(取余),让输出值落在数组索引范围内;在扩容后,数组长度 \\(n\\) 发生变化,而 key 对应的索引也可能发生变化。原先落在同一个桶的多个 key ,在扩容后可能会被分配到多个桶中,从而实现哈希冲突的缓解。

Q:如果为了高效的存取,那么直接使用数组不就好了吗?

当数据的 key 是连续的小范围整数时,直接用数组即可,简单高效。但当 key 是其他类型(例如字符串)时,就需要借助哈希函数将 key 映射为数组索引,再通过桶数组存储元素,这样的结构就是哈希表。

","path":["第 6 章   哈希表","6.4   小结"],"tags":[]},{"location":"chapter_heap/","level":1,"title":"第 8 章   堆","text":"

Abstract

堆就像是山岳峰峦,层叠起伏、形态各异。

座座山峰高低错落,而最高的山峰总是最先映入眼帘。

","path":["第 8 章   堆"],"tags":[]},{"location":"chapter_heap/#_1","level":2,"title":"本章内容","text":"","path":["第 8 章   堆"],"tags":[]},{"location":"chapter_heap/build_heap/","level":1,"title":"8.2   建堆操作","text":"

在某些情况下,我们希望使用一个列表的所有元素来构建一个堆,这个过程被称为“建堆操作”。

","path":["第 8 章   堆","8.2   建堆操作"],"tags":[]},{"location":"chapter_heap/build_heap/#821","level":2,"title":"8.2.1   借助入堆操作实现","text":"

我们首先创建一个空堆,然后遍历列表,依次对每个元素执行“入堆操作”,即先将元素添加至堆的尾部,再对该元素执行“从底至顶”堆化。

每当一个元素入堆,堆的长度就加一。由于节点是从顶到底依次被添加进二叉树的,因此堆是“自上而下”构建的。

设元素数量为 \\(n\\) ,每个元素的入堆操作使用 \\(O(\\log{n})\\) 时间,因此该建堆方法的时间复杂度为 \\(O(n \\log n)\\) 。

","path":["第 8 章   堆","8.2   建堆操作"],"tags":[]},{"location":"chapter_heap/build_heap/#822","level":2,"title":"8.2.2   通过遍历堆化实现","text":"

实际上,我们可以实现一种更为高效的建堆方法,共分为两步。

  1. 将列表所有元素原封不动地添加到堆中,此时堆的性质尚未得到满足。
  2. 倒序遍历堆(层序遍历的倒序),依次对每个非叶节点执行“从顶至底堆化”。

每当堆化一个节点后,以该节点为根节点的子树就形成一个合法的子堆。而由于是倒序遍历,因此堆是“自下而上”构建的。

之所以选择倒序遍历,是因为这样能够保证当前节点之下的子树已经是合法的子堆,这样堆化当前节点才是有效的。

值得说明的是,由于叶节点没有子节点,因此它们天然就是合法的子堆,无须堆化。如以下代码所示,最后一个非叶节点是最后一个节点的父节点,我们从它开始倒序遍历并执行堆化:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
def __init__(self, nums: list[int]):\n    \"\"\"构造方法,根据输入列表建堆\"\"\"\n    # 将列表元素原封不动添加进堆\n    self.max_heap = nums\n    # 堆化除叶节点以外的其他所有节点\n    for i in range(self.parent(self.size() - 1), -1, -1):\n        self.sift_down(i)\n
my_heap.cpp
/* 构造方法,根据输入列表建堆 */\nMaxHeap(vector<int> nums) {\n    // 将列表元素原封不动添加进堆\n    maxHeap = nums;\n    // 堆化除叶节点以外的其他所有节点\n    for (int i = parent(size() - 1); i >= 0; i--) {\n        siftDown(i);\n    }\n}\n
my_heap.java
/* 构造方法,根据输入列表建堆 */\nMaxHeap(List<Integer> nums) {\n    // 将列表元素原封不动添加进堆\n    maxHeap = new ArrayList<>(nums);\n    // 堆化除叶节点以外的其他所有节点\n    for (int i = parent(size() - 1); i >= 0; i--) {\n        siftDown(i);\n    }\n}\n
my_heap.cs
/* 构造函数,根据输入列表建堆 */\nMaxHeap(IEnumerable<int> nums) {\n    // 将列表元素原封不动添加进堆\n    maxHeap = new List<int>(nums);\n    // 堆化除叶节点以外的其他所有节点\n    var size = Parent(this.Size() - 1);\n    for (int i = size; i >= 0; i--) {\n        SiftDown(i);\n    }\n}\n
my_heap.go
/* 构造函数,根据切片建堆 */\nfunc newMaxHeap(nums []any) *maxHeap {\n    // 将列表元素原封不动添加进堆\n    h := &maxHeap{data: nums}\n    for i := h.parent(len(h.data) - 1); i >= 0; i-- {\n        // 堆化除叶节点以外的其他所有节点\n        h.siftDown(i)\n    }\n    return h\n}\n
my_heap.swift
/* 构造方法,根据输入列表建堆 */\ninit(nums: [Int]) {\n    // 将列表元素原封不动添加进堆\n    maxHeap = nums\n    // 堆化除叶节点以外的其他所有节点\n    for i in (0 ... parent(i: size() - 1)).reversed() {\n        siftDown(i: i)\n    }\n}\n
my_heap.js
/* 构造方法,建立空堆或根据输入列表建堆 */\nconstructor(nums) {\n    // 将列表元素原封不动添加进堆\n    this.#maxHeap = nums === undefined ? [] : [...nums];\n    // 堆化除叶节点以外的其他所有节点\n    for (let i = this.#parent(this.size() - 1); i >= 0; i--) {\n        this.#siftDown(i);\n    }\n}\n
my_heap.ts
/* 构造方法,建立空堆或根据输入列表建堆 */\nconstructor(nums?: number[]) {\n    // 将列表元素原封不动添加进堆\n    this.maxHeap = nums === undefined ? [] : [...nums];\n    // 堆化除叶节点以外的其他所有节点\n    for (let i = this.parent(this.size() - 1); i >= 0; i--) {\n        this.siftDown(i);\n    }\n}\n
my_heap.dart
/* 构造方法,根据输入列表建堆 */\nMaxHeap(List<int> nums) {\n  // 将列表元素原封不动添加进堆\n  _maxHeap = nums;\n  // 堆化除叶节点以外的其他所有节点\n  for (int i = _parent(size() - 1); i >= 0; i--) {\n    siftDown(i);\n  }\n}\n
my_heap.rs
/* 构造方法,根据输入列表建堆 */\nfn new(nums: Vec<i32>) -> Self {\n    // 将列表元素原封不动添加进堆\n    let mut heap = MaxHeap { max_heap: nums };\n    // 堆化除叶节点以外的其他所有节点\n    for i in (0..=Self::parent(heap.size() - 1)).rev() {\n        heap.sift_down(i);\n    }\n    heap\n}\n
my_heap.c
/* 构造函数,根据切片建堆 */\nMaxHeap *newMaxHeap(int nums[], int size) {\n    // 所有元素入堆\n    MaxHeap *maxHeap = (MaxHeap *)malloc(sizeof(MaxHeap));\n    maxHeap->size = size;\n    memcpy(maxHeap->data, nums, size * sizeof(int));\n    for (int i = parent(maxHeap, size - 1); i >= 0; i--) {\n        // 堆化除叶节点以外的其他所有节点\n        siftDown(maxHeap, i);\n    }\n    return maxHeap;\n}\n
my_heap.kt
/* 大顶堆 */\nclass MaxHeap(nums: MutableList<Int>?) {\n    // 使用列表而非数组,这样无须考虑扩容问题\n    private val maxHeap = mutableListOf<Int>()\n\n    /* 构造方法,根据输入列表建堆 */\n    init {\n        // 将列表元素原封不动添加进堆\n        maxHeap.addAll(nums!!)\n        // 堆化除叶节点以外的其他所有节点\n        for (i in parent(size() - 1) downTo 0) {\n            siftDown(i)\n        }\n    }\n\n    /* 获取左子节点的索引 */\n    private fun left(i: Int): Int {\n        return 2 * i + 1\n    }\n\n    /* 获取右子节点的索引 */\n    private fun right(i: Int): Int {\n        return 2 * i + 2\n    }\n\n    /* 获取父节点的索引 */\n    private fun parent(i: Int): Int {\n        return (i - 1) / 2 // 向下整除\n    }\n\n    /* 交换元素 */\n    private fun swap(i: Int, j: Int) {\n        val temp = maxHeap[i]\n        maxHeap[i] = maxHeap[j]\n        maxHeap[j] = temp\n    }\n\n    /* 获取堆大小 */\n    fun size(): Int {\n        return maxHeap.size\n    }\n\n    /* 判断堆是否为空 */\n    fun isEmpty(): Boolean {\n        /* 判断堆是否为空 */\n        return size() == 0\n    }\n\n    /* 访问堆顶元素 */\n    fun peek(): Int {\n        return maxHeap[0]\n    }\n\n    /* 元素入堆 */\n    fun push(_val: Int) {\n        // 添加节点\n        maxHeap.add(_val)\n        // 从底至顶堆化\n        siftUp(size() - 1)\n    }\n\n    /* 从节点 i 开始,从底至顶堆化 */\n    private fun siftUp(it: Int) {\n        // Kotlin的函数参数不可变,因此创建临时变量\n        var i = it\n        while (true) {\n            // 获取节点 i 的父节点\n            val p = parent(i)\n            // 当“越过根节点”或“节点无须修复”时,结束堆化\n            if (p < 0 || maxHeap[i] <= maxHeap[p]) break\n            // 交换两节点\n            swap(i, p)\n            // 循环向上堆化\n            i = p\n        }\n    }\n\n    /* 元素出堆 */\n    fun pop(): Int {\n        // 判空处理\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        // 交换根节点与最右叶节点(交换首元素与尾元素)\n        swap(0, size() - 1)\n        // 删除节点\n        val _val = maxHeap.removeAt(size() - 1)\n        // 从顶至底堆化\n        siftDown(0)\n        // 返回堆顶元素\n        return _val\n    }\n\n    /* 从节点 i 开始,从顶至底堆化 */\n    private fun siftDown(it: Int) {\n        // Kotlin的函数参数不可变,因此创建临时变量\n        var i = it\n        while (true) {\n            // 判断节点 i, l, r 中值最大的节点,记为 ma\n            val l = left(i)\n            val r = right(i)\n            var ma = i\n            if (l < size() && maxHeap[l] > maxHeap[ma]) ma = l\n            if (r < size() && maxHeap[r] > maxHeap[ma]) ma = r\n            // 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出\n            if (ma == i) break\n            // 交换两节点\n            swap(i, ma)\n            // 循环向下堆化\n            i = ma\n        }\n    }\n\n    /* 打印堆(二叉树) */\n    fun print() {\n        val queue = PriorityQueue { a: Int, b: Int -> b - a }\n        queue.addAll(maxHeap)\n        printHeap(queue)\n    }\n}\n
my_heap.rb
### 构造方法,根据输入列表建堆 ###\ndef initialize(nums)\n  # 将列表元素原封不动添加进堆\n  @max_heap = nums\n  # 堆化除叶节点以外的其他所有节点\n  parent(size - 1).downto(0) do |i|\n    sift_down(i)\n  end\nend\n
可视化运行

全屏观看 >

","path":["第 8 章   堆","8.2   建堆操作"],"tags":[]},{"location":"chapter_heap/build_heap/#823","level":2,"title":"8.2.3   复杂度分析","text":"

下面,我们来尝试推算第二种建堆方法的时间复杂度。

将上述两者相乘,可得到建堆过程的时间复杂度为 \\(O(n \\log n)\\) 。但这个估算结果并不准确,因为我们没有考虑到二叉树底层节点数量远多于顶层节点的性质。

接下来我们来进行更为准确的计算。为了降低计算难度,假设给定一个节点数量为 \\(n\\) 、高度为 \\(h\\) 的“完美二叉树”,该假设不会影响计算结果的正确性。

图 8-5   完美二叉树的各层节点数量

如图 8-5 所示,节点“从顶至底堆化”的最大迭代次数等于该节点到叶节点的距离,而该距离正是“节点高度”。因此,我们可以对各层的“节点数量 \\(\\times\\) 节点高度”求和,得到所有节点的堆化迭代次数的总和。

\\[ T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \\dots + 2^{(h-1)}\\times1 \\]

化简上式需要借助中学的数列知识,先将 \\(T(h)\\) 乘以 \\(2\\) ,得到:

\\[ \\begin{aligned} T(h) & = 2^0h + 2^1(h-1) + 2^2(h-2) + \\dots + 2^{h-1}\\times1 \\newline 2 T(h) & = 2^1h + 2^2(h-1) + 2^3(h-2) + \\dots + 2^{h}\\times1 \\newline \\end{aligned} \\]

使用错位相减法,用下式 \\(2 T(h)\\) 减去上式 \\(T(h)\\) ,可得:

\\[ 2T(h) - T(h) = T(h) = -2^0h + 2^1 + 2^2 + \\dots + 2^{h-1} + 2^h \\]

观察上式,发现 \\(T(h)\\) 是一个等比数列,可直接使用求和公式,得到时间复杂度为:

\\[ \\begin{aligned} T(h) & = 2 \\frac{1 - 2^h}{1 - 2} - h \\newline & = 2^{h+1} - h - 2 \\newline & = O(2^h) \\end{aligned} \\]

进一步,高度为 \\(h\\) 的完美二叉树的节点数量为 \\(n = 2^{h+1} - 1\\) ,易得复杂度为 \\(O(2^h) = O(n)\\) 。以上推算表明,输入列表并建堆的时间复杂度为 \\(O(n)\\) ,非常高效。

","path":["第 8 章   堆","8.2   建堆操作"],"tags":[]},{"location":"chapter_heap/heap/","level":1,"title":"8.1   堆","text":"

堆(heap)是一种满足特定条件的完全二叉树,主要可分为两种类型,如图 8-1 所示。

图 8-1   小顶堆与大顶堆

堆作为完全二叉树的一个特例,具有以下特性。

","path":["第 8 章   堆","8.1   堆"],"tags":[]},{"location":"chapter_heap/heap/#811","level":2,"title":"8.1.1   堆的常用操作","text":"

需要指出的是,许多编程语言提供的是优先队列(priority queue),这是一种抽象的数据结构,定义为具有优先级排序的队列。

实际上,堆通常用于实现优先队列,大顶堆相当于元素按从大到小的顺序出队的优先队列。从使用角度来看,我们可以将“优先队列”和“堆”看作等价的数据结构。因此,本书对两者不做特别区分,统一称作“堆”。

堆的常用操作见表 8-1 ,方法名需要根据编程语言来确定。

表 8-1   堆的操作效率

方法名 描述 时间复杂度 push() 元素入堆 \\(O(\\log n)\\) pop() 堆顶元素出堆 \\(O(\\log n)\\) peek() 访问堆顶元素(对于大 / 小顶堆分别为最大 / 小值) \\(O(1)\\) size() 获取堆的元素数量 \\(O(1)\\) isEmpty() 判断堆是否为空 \\(O(1)\\)

在实际应用中,我们可以直接使用编程语言提供的堆类(或优先队列类)。

类似于排序算法中的“从小到大排列”和“从大到小排列”,我们可以通过设置一个 flag 或修改 Comparator 实现“小顶堆”与“大顶堆”之间的转换。代码如下所示:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby heap.py
# 初始化小顶堆\nmin_heap, flag = [], 1\n# 初始化大顶堆\nmax_heap, flag = [], -1\n\n# Python 的 heapq 模块默认实现小顶堆\n# 考虑将“元素取负”后再入堆,这样就可以将大小关系颠倒,从而实现大顶堆\n# 在本示例中,flag = 1 时对应小顶堆,flag = -1 时对应大顶堆\n\n# 元素入堆\nheapq.heappush(max_heap, flag * 1)\nheapq.heappush(max_heap, flag * 3)\nheapq.heappush(max_heap, flag * 2)\nheapq.heappush(max_heap, flag * 5)\nheapq.heappush(max_heap, flag * 4)\n\n# 获取堆顶元素\npeek: int = flag * max_heap[0] # 5\n\n# 堆顶元素出堆\n# 出堆元素会形成一个从大到小的序列\nval = flag * heapq.heappop(max_heap) # 5\nval = flag * heapq.heappop(max_heap) # 4\nval = flag * heapq.heappop(max_heap) # 3\nval = flag * heapq.heappop(max_heap) # 2\nval = flag * heapq.heappop(max_heap) # 1\n\n# 获取堆大小\nsize: int = len(max_heap)\n\n# 判断堆是否为空\nis_empty: bool = not max_heap\n\n# 输入列表并建堆\nmin_heap: list[int] = [1, 3, 2, 5, 4]\nheapq.heapify(min_heap)\n
heap.cpp
/* 初始化堆 */\n// 初始化小顶堆\npriority_queue<int, vector<int>, greater<int>> minHeap;\n// 初始化大顶堆\npriority_queue<int, vector<int>, less<int>> maxHeap;\n\n/* 元素入堆 */\nmaxHeap.push(1);\nmaxHeap.push(3);\nmaxHeap.push(2);\nmaxHeap.push(5);\nmaxHeap.push(4);\n\n/* 获取堆顶元素 */\nint peek = maxHeap.top(); // 5\n\n/* 堆顶元素出堆 */\n// 出堆元素会形成一个从大到小的序列\nmaxHeap.pop(); // 5\nmaxHeap.pop(); // 4\nmaxHeap.pop(); // 3\nmaxHeap.pop(); // 2\nmaxHeap.pop(); // 1\n\n/* 获取堆大小 */\nint size = maxHeap.size();\n\n/* 判断堆是否为空 */\nbool isEmpty = maxHeap.empty();\n\n/* 输入列表并建堆 */\nvector<int> input{1, 3, 2, 5, 4};\npriority_queue<int, vector<int>, greater<int>> minHeap(input.begin(), input.end());\n
heap.java
/* 初始化堆 */\n// 初始化小顶堆\nQueue<Integer> minHeap = new PriorityQueue<>();\n// 初始化大顶堆(使用 lambda 表达式修改 Comparator 即可)\nQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);\n\n/* 元素入堆 */\nmaxHeap.offer(1);\nmaxHeap.offer(3);\nmaxHeap.offer(2);\nmaxHeap.offer(5);\nmaxHeap.offer(4);\n\n/* 获取堆顶元素 */\nint peek = maxHeap.peek(); // 5\n\n/* 堆顶元素出堆 */\n// 出堆元素会形成一个从大到小的序列\npeek = maxHeap.poll(); // 5\npeek = maxHeap.poll(); // 4\npeek = maxHeap.poll(); // 3\npeek = maxHeap.poll(); // 2\npeek = maxHeap.poll(); // 1\n\n/* 获取堆大小 */\nint size = maxHeap.size();\n\n/* 判断堆是否为空 */\nboolean isEmpty = maxHeap.isEmpty();\n\n/* 输入列表并建堆 */\nminHeap = new PriorityQueue<>(Arrays.asList(1, 3, 2, 5, 4));\n
heap.cs
/* 初始化堆 */\n// 初始化小顶堆\nPriorityQueue<int, int> minHeap = new();\n// 初始化大顶堆(使用 lambda 表达式修改 Comparer 即可)\nPriorityQueue<int, int> maxHeap = new(Comparer<int>.Create((x, y) => y.CompareTo(x)));\n\n/* 元素入堆 */\nmaxHeap.Enqueue(1, 1);\nmaxHeap.Enqueue(3, 3);\nmaxHeap.Enqueue(2, 2);\nmaxHeap.Enqueue(5, 5);\nmaxHeap.Enqueue(4, 4);\n\n/* 获取堆顶元素 */\nint peek = maxHeap.Peek();//5\n\n/* 堆顶元素出堆 */\n// 出堆元素会形成一个从大到小的序列\npeek = maxHeap.Dequeue();  // 5\npeek = maxHeap.Dequeue();  // 4\npeek = maxHeap.Dequeue();  // 3\npeek = maxHeap.Dequeue();  // 2\npeek = maxHeap.Dequeue();  // 1\n\n/* 获取堆大小 */\nint size = maxHeap.Count;\n\n/* 判断堆是否为空 */\nbool isEmpty = maxHeap.Count == 0;\n\n/* 输入列表并建堆 */\nminHeap = new PriorityQueue<int, int>([(1, 1), (3, 3), (2, 2), (5, 5), (4, 4)]);\n
heap.go
// Go 语言中可以通过实现 heap.Interface 来构建整数大顶堆\n// 实现 heap.Interface 需要同时实现 sort.Interface\ntype intHeap []any\n\n// Push heap.Interface 的方法,实现推入元素到堆\nfunc (h *intHeap) Push(x any) {\n    // Push 和 Pop 使用 pointer receiver 作为参数\n    // 因为它们不仅会对切片的内容进行调整,还会修改切片的长度。\n    *h = append(*h, x.(int))\n}\n\n// Pop heap.Interface 的方法,实现弹出堆顶元素\nfunc (h *intHeap) Pop() any {\n    // 待出堆元素存放在最后\n    last := (*h)[len(*h)-1]\n    *h = (*h)[:len(*h)-1]\n    return last\n}\n\n// Len sort.Interface 的方法\nfunc (h *intHeap) Len() int {\n    return len(*h)\n}\n\n// Less sort.Interface 的方法\nfunc (h *intHeap) Less(i, j int) bool {\n    // 如果实现小顶堆,则需要调整为小于号\n    return (*h)[i].(int) > (*h)[j].(int)\n}\n\n// Swap sort.Interface 的方法\nfunc (h *intHeap) Swap(i, j int) {\n    (*h)[i], (*h)[j] = (*h)[j], (*h)[i]\n}\n\n// Top 获取堆顶元素\nfunc (h *intHeap) Top() any {\n    return (*h)[0]\n}\n\n/* Driver Code */\nfunc TestHeap(t *testing.T) {\n    /* 初始化堆 */\n    // 初始化大顶堆\n    maxHeap := &intHeap{}\n    heap.Init(maxHeap)\n    /* 元素入堆 */\n    // 调用 heap.Interface 的方法,来添加元素\n    heap.Push(maxHeap, 1)\n    heap.Push(maxHeap, 3)\n    heap.Push(maxHeap, 2)\n    heap.Push(maxHeap, 4)\n    heap.Push(maxHeap, 5)\n\n    /* 获取堆顶元素 */\n    top := maxHeap.Top()\n    fmt.Printf(\"堆顶元素为 %d\\n\", top)\n\n    /* 堆顶元素出堆 */\n    // 调用 heap.Interface 的方法,来移除元素\n    heap.Pop(maxHeap) // 5\n    heap.Pop(maxHeap) // 4\n    heap.Pop(maxHeap) // 3\n    heap.Pop(maxHeap) // 2\n    heap.Pop(maxHeap) // 1\n\n    /* 获取堆大小 */\n    size := len(*maxHeap)\n    fmt.Printf(\"堆元素数量为 %d\\n\", size)\n\n    /* 判断堆是否为空 */\n    isEmpty := len(*maxHeap) == 0\n    fmt.Printf(\"堆是否为空 %t\\n\", isEmpty)\n}\n
heap.swift
/* 初始化堆 */\n// Swift 的 Heap 类型同时支持最大堆和最小堆,且需要引入 swift-collections\nvar heap = Heap<Int>()\n\n/* 元素入堆 */\nheap.insert(1)\nheap.insert(3)\nheap.insert(2)\nheap.insert(5)\nheap.insert(4)\n\n/* 获取堆顶元素 */\nvar peek = heap.max()!\n\n/* 堆顶元素出堆 */\npeek = heap.removeMax() // 5\npeek = heap.removeMax() // 4\npeek = heap.removeMax() // 3\npeek = heap.removeMax() // 2\npeek = heap.removeMax() // 1\n\n/* 获取堆大小 */\nlet size = heap.count\n\n/* 判断堆是否为空 */\nlet isEmpty = heap.isEmpty\n\n/* 输入列表并建堆 */\nlet heap2 = Heap([1, 3, 2, 5, 4])\n
heap.js
// JavaScript 未提供内置 Heap 类\n
heap.ts
// TypeScript 未提供内置 Heap 类\n
heap.dart
// Dart 未提供内置 Heap 类\n
heap.rs
use std::collections::BinaryHeap;\nuse std::cmp::Reverse;\n\n/* 初始化堆 */\n// 初始化小顶堆\nlet mut min_heap = BinaryHeap::<Reverse<i32>>::new();\n// 初始化大顶堆\nlet mut max_heap = BinaryHeap::new();\n\n/* 元素入堆 */\nmax_heap.push(1);\nmax_heap.push(3);\nmax_heap.push(2);\nmax_heap.push(5);\nmax_heap.push(4);\n\n/* 获取堆顶元素 */\nlet peek = max_heap.peek().unwrap();  // 5\n\n/* 堆顶元素出堆 */\n// 出堆元素会形成一个从大到小的序列\nlet peek = max_heap.pop().unwrap();   // 5\nlet peek = max_heap.pop().unwrap();   // 4\nlet peek = max_heap.pop().unwrap();   // 3\nlet peek = max_heap.pop().unwrap();   // 2\nlet peek = max_heap.pop().unwrap();   // 1\n\n/* 获取堆大小 */\nlet size = max_heap.len();\n\n/* 判断堆是否为空 */\nlet is_empty = max_heap.is_empty();\n\n/* 输入列表并建堆 */\nlet min_heap = BinaryHeap::from(vec![Reverse(1), Reverse(3), Reverse(2), Reverse(5), Reverse(4)]);\n
heap.c
// C 未提供内置 Heap 类\n
heap.kt
/* 初始化堆 */\n// 初始化小顶堆\nvar minHeap = PriorityQueue<Int>()\n// 初始化大顶堆(使用 lambda 表达式修改 Comparator 即可)\nval maxHeap = PriorityQueue { a: Int, b: Int -> b - a }\n\n/* 元素入堆 */\nmaxHeap.offer(1)\nmaxHeap.offer(3)\nmaxHeap.offer(2)\nmaxHeap.offer(5)\nmaxHeap.offer(4)\n\n/* 获取堆顶元素 */\nvar peek = maxHeap.peek() // 5\n\n/* 堆顶元素出堆 */\n// 出堆元素会形成一个从大到小的序列\npeek = maxHeap.poll() // 5\npeek = maxHeap.poll() // 4\npeek = maxHeap.poll() // 3\npeek = maxHeap.poll() // 2\npeek = maxHeap.poll() // 1\n\n/* 获取堆大小 */\nval size = maxHeap.size\n\n/* 判断堆是否为空 */\nval isEmpty = maxHeap.isEmpty()\n\n/* 输入列表并建堆 */\nminHeap = PriorityQueue(mutableListOf(1, 3, 2, 5, 4))\n
heap.rb
# Ruby 未提供内置 Heap 类\n
可视化运行

全屏观看 >

","path":["第 8 章   堆","8.1   堆"],"tags":[]},{"location":"chapter_heap/heap/#812","level":2,"title":"8.1.2   堆的实现","text":"

下文实现的是大顶堆。若要将其转换为小顶堆,只需将所有大小逻辑判断进行逆转(例如,将 \\(\\geq\\) 替换为 \\(\\leq\\) )。感兴趣的读者可以自行实现。

","path":["第 8 章   堆","8.1   堆"],"tags":[]},{"location":"chapter_heap/heap/#1","level":3,"title":"1.   堆的存储与表示","text":"

“二叉树”章节讲过,完全二叉树非常适合用数组来表示。由于堆正是一种完全二叉树,因此我们将采用数组来存储堆。

当使用数组表示二叉树时,元素代表节点值,索引代表节点在二叉树中的位置。节点指针通过索引映射公式来实现。

如图 8-2 所示,给定索引 \\(i\\) ,其左子节点的索引为 \\(2i + 1\\) ,右子节点的索引为 \\(2i + 2\\) ,父节点的索引为 \\((i - 1) / 2\\)(向下整除)。当索引越界时,表示空节点或节点不存在。

图 8-2   堆的表示与存储

我们可以将索引映射公式封装成函数,方便后续使用:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
def left(self, i: int) -> int:\n    \"\"\"获取左子节点的索引\"\"\"\n    return 2 * i + 1\n\ndef right(self, i: int) -> int:\n    \"\"\"获取右子节点的索引\"\"\"\n    return 2 * i + 2\n\ndef parent(self, i: int) -> int:\n    \"\"\"获取父节点的索引\"\"\"\n    return (i - 1) // 2  # 向下整除\n
my_heap.cpp
/* 获取左子节点的索引 */\nint left(int i) {\n    return 2 * i + 1;\n}\n\n/* 获取右子节点的索引 */\nint right(int i) {\n    return 2 * i + 2;\n}\n\n/* 获取父节点的索引 */\nint parent(int i) {\n    return (i - 1) / 2; // 向下整除\n}\n
my_heap.java
/* 获取左子节点的索引 */\nint left(int i) {\n    return 2 * i + 1;\n}\n\n/* 获取右子节点的索引 */\nint right(int i) {\n    return 2 * i + 2;\n}\n\n/* 获取父节点的索引 */\nint parent(int i) {\n    return (i - 1) / 2; // 向下整除\n}\n
my_heap.cs
/* 获取左子节点的索引 */\nint Left(int i) {\n    return 2 * i + 1;\n}\n\n/* 获取右子节点的索引 */\nint Right(int i) {\n    return 2 * i + 2;\n}\n\n/* 获取父节点的索引 */\nint Parent(int i) {\n    return (i - 1) / 2; // 向下整除\n}\n
my_heap.go
/* 获取左子节点的索引 */\nfunc (h *maxHeap) left(i int) int {\n    return 2*i + 1\n}\n\n/* 获取右子节点的索引 */\nfunc (h *maxHeap) right(i int) int {\n    return 2*i + 2\n}\n\n/* 获取父节点的索引 */\nfunc (h *maxHeap) parent(i int) int {\n    // 向下整除\n    return (i - 1) / 2\n}\n
my_heap.swift
/* 获取左子节点的索引 */\nfunc left(i: Int) -> Int {\n    2 * i + 1\n}\n\n/* 获取右子节点的索引 */\nfunc right(i: Int) -> Int {\n    2 * i + 2\n}\n\n/* 获取父节点的索引 */\nfunc parent(i: Int) -> Int {\n    (i - 1) / 2 // 向下整除\n}\n
my_heap.js
/* 获取左子节点的索引 */\n#left(i) {\n    return 2 * i + 1;\n}\n\n/* 获取右子节点的索引 */\n#right(i) {\n    return 2 * i + 2;\n}\n\n/* 获取父节点的索引 */\n#parent(i) {\n    return Math.floor((i - 1) / 2); // 向下整除\n}\n
my_heap.ts
/* 获取左子节点的索引 */\nleft(i: number): number {\n    return 2 * i + 1;\n}\n\n/* 获取右子节点的索引 */\nright(i: number): number {\n    return 2 * i + 2;\n}\n\n/* 获取父节点的索引 */\nparent(i: number): number {\n    return Math.floor((i - 1) / 2); // 向下整除\n}\n
my_heap.dart
/* 获取左子节点的索引 */\nint _left(int i) {\n  return 2 * i + 1;\n}\n\n/* 获取右子节点的索引 */\nint _right(int i) {\n  return 2 * i + 2;\n}\n\n/* 获取父节点的索引 */\nint _parent(int i) {\n  return (i - 1) ~/ 2; // 向下整除\n}\n
my_heap.rs
/* 获取左子节点的索引 */\nfn left(i: usize) -> usize {\n    2 * i + 1\n}\n\n/* 获取右子节点的索引 */\nfn right(i: usize) -> usize {\n    2 * i + 2\n}\n\n/* 获取父节点的索引 */\nfn parent(i: usize) -> usize {\n    (i - 1) / 2 // 向下整除\n}\n
my_heap.c
/* 获取左子节点的索引 */\nint left(MaxHeap *maxHeap, int i) {\n    return 2 * i + 1;\n}\n\n/* 获取右子节点的索引 */\nint right(MaxHeap *maxHeap, int i) {\n    return 2 * i + 2;\n}\n\n/* 获取父节点的索引 */\nint parent(MaxHeap *maxHeap, int i) {\n    return (i - 1) / 2; // 向下取整\n}\n
my_heap.kt
/* 获取左子节点的索引 */\nfun left(i: Int): Int {\n    return 2 * i + 1\n}\n\n/* 获取右子节点的索引 */\nfun right(i: Int): Int {\n    return 2 * i + 2\n}\n\n/* 获取父节点的索引 */\nfun parent(i: Int): Int {\n    return (i - 1) / 2 // 向下整除\n}\n
my_heap.rb
### 获取左子节点的索引 ###\ndef left(i)\n  2 * i + 1\nend\n\n### 获取右子节点的索引 ###\ndef right(i)\n  2 * i + 2\nend\n\n### 获取父节点的索引 ###\ndef parent(i)\n  (i - 1) / 2     # 向下整除\nend\n
","path":["第 8 章   堆","8.1   堆"],"tags":[]},{"location":"chapter_heap/heap/#2","level":3,"title":"2.   访问堆顶元素","text":"

堆顶元素即为二叉树的根节点,也就是列表的首个元素:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
def peek(self) -> int:\n    \"\"\"访问堆顶元素\"\"\"\n    return self.max_heap[0]\n
my_heap.cpp
/* 访问堆顶元素 */\nint peek() {\n    return maxHeap[0];\n}\n
my_heap.java
/* 访问堆顶元素 */\nint peek() {\n    return maxHeap.get(0);\n}\n
my_heap.cs
/* 访问堆顶元素 */\nint Peek() {\n    return maxHeap[0];\n}\n
my_heap.go
/* 访问堆顶元素 */\nfunc (h *maxHeap) peek() any {\n    return h.data[0]\n}\n
my_heap.swift
/* 访问堆顶元素 */\nfunc peek() -> Int {\n    maxHeap[0]\n}\n
my_heap.js
/* 访问堆顶元素 */\npeek() {\n    return this.#maxHeap[0];\n}\n
my_heap.ts
/* 访问堆顶元素 */\npeek(): number {\n    return this.maxHeap[0];\n}\n
my_heap.dart
/* 访问堆顶元素 */\nint peek() {\n  return _maxHeap[0];\n}\n
my_heap.rs
/* 访问堆顶元素 */\nfn peek(&self) -> Option<i32> {\n    self.max_heap.first().copied()\n}\n
my_heap.c
/* 访问堆顶元素 */\nint peek(MaxHeap *maxHeap) {\n    return maxHeap->data[0];\n}\n
my_heap.kt
/* 访问堆顶元素 */\nfun peek(): Int {\n    return maxHeap[0]\n}\n
my_heap.rb
### 访问堆顶元素 ###\ndef peek\n  @max_heap[0]\nend\n
可视化运行

全屏观看 >

","path":["第 8 章   堆","8.1   堆"],"tags":[]},{"location":"chapter_heap/heap/#3","level":3,"title":"3.   元素入堆","text":"

给定元素 val ,我们首先将其添加到堆底。添加之后,由于 val 可能大于堆中其他元素,堆的成立条件可能已被破坏,因此需要修复从插入节点到根节点的路径上的各个节点,这个操作被称为堆化(heapify)。

考虑从入堆节点开始,从底至顶执行堆化。如图 8-3 所示,我们比较插入节点与其父节点的值,如果插入节点更大,则将它们交换。然后继续执行此操作,从底至顶修复堆中的各个节点,直至越过根节点或遇到无须交换的节点时结束。

<1><2><3><4><5><6><7><8><9>

图 8-3   元素入堆步骤

设节点总数为 \\(n\\) ,则树的高度为 \\(O(\\log n)\\) 。由此可知,堆化操作的循环轮数最多为 \\(O(\\log n)\\) ,元素入堆操作的时间复杂度为 \\(O(\\log n)\\) 。代码如下所示:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
def push(self, val: int):\n    \"\"\"元素入堆\"\"\"\n    # 添加节点\n    self.max_heap.append(val)\n    # 从底至顶堆化\n    self.sift_up(self.size() - 1)\n\ndef sift_up(self, i: int):\n    \"\"\"从节点 i 开始,从底至顶堆化\"\"\"\n    while True:\n        # 获取节点 i 的父节点\n        p = self.parent(i)\n        # 当“越过根节点”或“节点无须修复”时,结束堆化\n        if p < 0 or self.max_heap[i] <= self.max_heap[p]:\n            break\n        # 交换两节点\n        self.swap(i, p)\n        # 循环向上堆化\n        i = p\n
my_heap.cpp
/* 元素入堆 */\nvoid push(int val) {\n    // 添加节点\n    maxHeap.push_back(val);\n    // 从底至顶堆化\n    siftUp(size() - 1);\n}\n\n/* 从节点 i 开始,从底至顶堆化 */\nvoid siftUp(int i) {\n    while (true) {\n        // 获取节点 i 的父节点\n        int p = parent(i);\n        // 当“越过根节点”或“节点无须修复”时,结束堆化\n        if (p < 0 || maxHeap[i] <= maxHeap[p])\n            break;\n        // 交换两节点\n        swap(maxHeap[i], maxHeap[p]);\n        // 循环向上堆化\n        i = p;\n    }\n}\n
my_heap.java
/* 元素入堆 */\nvoid push(int val) {\n    // 添加节点\n    maxHeap.add(val);\n    // 从底至顶堆化\n    siftUp(size() - 1);\n}\n\n/* 从节点 i 开始,从底至顶堆化 */\nvoid siftUp(int i) {\n    while (true) {\n        // 获取节点 i 的父节点\n        int p = parent(i);\n        // 当“越过根节点”或“节点无须修复”时,结束堆化\n        if (p < 0 || maxHeap.get(i) <= maxHeap.get(p))\n            break;\n        // 交换两节点\n        swap(i, p);\n        // 循环向上堆化\n        i = p;\n    }\n}\n
my_heap.cs
/* 元素入堆 */\nvoid Push(int val) {\n    // 添加节点\n    maxHeap.Add(val);\n    // 从底至顶堆化\n    SiftUp(Size() - 1);\n}\n\n/* 从节点 i 开始,从底至顶堆化 */\nvoid SiftUp(int i) {\n    while (true) {\n        // 获取节点 i 的父节点\n        int p = Parent(i);\n        // 若“越过根节点”或“节点无须修复”,则结束堆化\n        if (p < 0 || maxHeap[i] <= maxHeap[p])\n            break;\n        // 交换两节点\n        Swap(i, p);\n        // 循环向上堆化\n        i = p;\n    }\n}\n
my_heap.go
/* 元素入堆 */\nfunc (h *maxHeap) push(val any) {\n    // 添加节点\n    h.data = append(h.data, val)\n    // 从底至顶堆化\n    h.siftUp(len(h.data) - 1)\n}\n\n/* 从节点 i 开始,从底至顶堆化 */\nfunc (h *maxHeap) siftUp(i int) {\n    for true {\n        // 获取节点 i 的父节点\n        p := h.parent(i)\n        // 当“越过根节点”或“节点无须修复”时,结束堆化\n        if p < 0 || h.data[i].(int) <= h.data[p].(int) {\n            break\n        }\n        // 交换两节点\n        h.swap(i, p)\n        // 循环向上堆化\n        i = p\n    }\n}\n
my_heap.swift
/* 元素入堆 */\nfunc push(val: Int) {\n    // 添加节点\n    maxHeap.append(val)\n    // 从底至顶堆化\n    siftUp(i: size() - 1)\n}\n\n/* 从节点 i 开始,从底至顶堆化 */\nfunc siftUp(i: Int) {\n    var i = i\n    while true {\n        // 获取节点 i 的父节点\n        let p = parent(i: i)\n        // 当“越过根节点”或“节点无须修复”时,结束堆化\n        if p < 0 || maxHeap[i] <= maxHeap[p] {\n            break\n        }\n        // 交换两节点\n        swap(i: i, j: p)\n        // 循环向上堆化\n        i = p\n    }\n}\n
my_heap.js
/* 元素入堆 */\npush(val) {\n    // 添加节点\n    this.#maxHeap.push(val);\n    // 从底至顶堆化\n    this.#siftUp(this.size() - 1);\n}\n\n/* 从节点 i 开始,从底至顶堆化 */\n#siftUp(i) {\n    while (true) {\n        // 获取节点 i 的父节点\n        const p = this.#parent(i);\n        // 当“越过根节点”或“节点无须修复”时,结束堆化\n        if (p < 0 || this.#maxHeap[i] <= this.#maxHeap[p]) break;\n        // 交换两节点\n        this.#swap(i, p);\n        // 循环向上堆化\n        i = p;\n    }\n}\n
my_heap.ts
/* 元素入堆 */\npush(val: number): void {\n    // 添加节点\n    this.maxHeap.push(val);\n    // 从底至顶堆化\n    this.siftUp(this.size() - 1);\n}\n\n/* 从节点 i 开始,从底至顶堆化 */\nsiftUp(i: number): void {\n    while (true) {\n        // 获取节点 i 的父节点\n        const p = this.parent(i);\n        // 当“越过根节点”或“节点无须修复”时,结束堆化\n        if (p < 0 || this.maxHeap[i] <= this.maxHeap[p]) break;\n        // 交换两节点\n        this.swap(i, p);\n        // 循环向上堆化\n        i = p;\n    }\n}\n
my_heap.dart
/* 元素入堆 */\nvoid push(int val) {\n  // 添加节点\n  _maxHeap.add(val);\n  // 从底至顶堆化\n  siftUp(size() - 1);\n}\n\n/* 从节点 i 开始,从底至顶堆化 */\nvoid siftUp(int i) {\n  while (true) {\n    // 获取节点 i 的父节点\n    int p = _parent(i);\n    // 当“越过根节点”或“节点无须修复”时,结束堆化\n    if (p < 0 || _maxHeap[i] <= _maxHeap[p]) {\n      break;\n    }\n    // 交换两节点\n    _swap(i, p);\n    // 循环向上堆化\n    i = p;\n  }\n}\n
my_heap.rs
/* 元素入堆 */\nfn push(&mut self, val: i32) {\n    // 添加节点\n    self.max_heap.push(val);\n    // 从底至顶堆化\n    self.sift_up(self.size() - 1);\n}\n\n/* 从节点 i 开始,从底至顶堆化 */\nfn sift_up(&mut self, mut i: usize) {\n    loop {\n        // 节点 i 已经是堆顶节点了,结束堆化\n        if i == 0 {\n            break;\n        }\n        // 获取节点 i 的父节点\n        let p = Self::parent(i);\n        // 当“节点无须修复”时,结束堆化\n        if self.max_heap[i] <= self.max_heap[p] {\n            break;\n        }\n        // 交换两节点\n        self.swap(i, p);\n        // 循环向上堆化\n        i = p;\n    }\n}\n
my_heap.c
/* 元素入堆 */\nvoid push(MaxHeap *maxHeap, int val) {\n    // 默认情况下,不应该添加这么多节点\n    if (maxHeap->size == MAX_SIZE) {\n        printf(\"heap is full!\");\n        return;\n    }\n    // 添加节点\n    maxHeap->data[maxHeap->size] = val;\n    maxHeap->size++;\n\n    // 从底至顶堆化\n    siftUp(maxHeap, maxHeap->size - 1);\n}\n\n/* 从节点 i 开始,从底至顶堆化 */\nvoid siftUp(MaxHeap *maxHeap, int i) {\n    while (true) {\n        // 获取节点 i 的父节点\n        int p = parent(maxHeap, i);\n        // 当“越过根节点”或“节点无须修复”时,结束堆化\n        if (p < 0 || maxHeap->data[i] <= maxHeap->data[p]) {\n            break;\n        }\n        // 交换两节点\n        swap(maxHeap, i, p);\n        // 循环向上堆化\n        i = p;\n    }\n}\n
my_heap.kt
/* 元素入堆 */\nfun push(_val: Int) {\n    // 添加节点\n    maxHeap.add(_val)\n    // 从底至顶堆化\n    siftUp(size() - 1)\n}\n\n/* 从节点 i 开始,从底至顶堆化 */\nfun siftUp(it: Int) {\n    // Kotlin的函数参数不可变,因此创建临时变量\n    var i = it\n    while (true) {\n        // 获取节点 i 的父节点\n        val p = parent(i)\n        // 当“越过根节点”或“节点无须修复”时,结束堆化\n        if (p < 0 || maxHeap[i] <= maxHeap[p]) break\n        // 交换两节点\n        swap(i, p)\n        // 循环向上堆化\n        i = p\n    }\n}\n
my_heap.rb
### 元素入堆 ###\ndef push(val)\n  # 添加节点\n  @max_heap << val\n  # 从底至顶堆化\n  sift_up(size - 1)\nend\n\n### 从节点 i 开始,从底至顶堆化 ###\ndef sift_up(i)\n  loop do\n    # 获取节点 i 的父节点\n    p = parent(i)\n    # 当“越过根节点”或“节点无须修复”时,结束堆化\n    break if p < 0 || @max_heap[i] <= @max_heap[p]\n    # 交换两节点\n    swap(i, p)\n    # 循环向上堆化\n    i = p\n  end\nend\n
可视化运行

全屏观看 >

","path":["第 8 章   堆","8.1   堆"],"tags":[]},{"location":"chapter_heap/heap/#4","level":3,"title":"4.   堆顶元素出堆","text":"

堆顶元素是二叉树的根节点,即列表首元素。如果我们直接从列表中删除首元素,那么二叉树中所有节点的索引都会发生变化,这将使得后续使用堆化进行修复变得困难。为了尽量减少元素索引的变动,我们采用以下操作步骤。

  1. 交换堆顶元素与堆底元素(交换根节点与最右叶节点)。
  2. 交换完成后,将堆底从列表中删除(注意,由于已经交换,因此实际上删除的是原来的堆顶元素)。
  3. 从根节点开始,从顶至底执行堆化。

如图 8-4 所示,“从顶至底堆化”的操作方向与“从底至顶堆化”相反,我们将根节点的值与其两个子节点的值进行比较,将最大的子节点与根节点交换。然后循环执行此操作,直到越过叶节点或遇到无须交换的节点时结束。

<1><2><3><4><5><6><7><8><9><10>

图 8-4   堆顶元素出堆步骤

与元素入堆操作相似,堆顶元素出堆操作的时间复杂度也为 \\(O(\\log n)\\) 。代码如下所示:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby my_heap.py
def pop(self) -> int:\n    \"\"\"元素出堆\"\"\"\n    # 判空处理\n    if self.is_empty():\n        raise IndexError(\"堆为空\")\n    # 交换根节点与最右叶节点(交换首元素与尾元素)\n    self.swap(0, self.size() - 1)\n    # 删除节点\n    val = self.max_heap.pop()\n    # 从顶至底堆化\n    self.sift_down(0)\n    # 返回堆顶元素\n    return val\n\ndef sift_down(self, i: int):\n    \"\"\"从节点 i 开始,从顶至底堆化\"\"\"\n    while True:\n        # 判断节点 i, l, r 中值最大的节点,记为 ma\n        l, r, ma = self.left(i), self.right(i), i\n        if l < self.size() and self.max_heap[l] > self.max_heap[ma]:\n            ma = l\n        if r < self.size() and self.max_heap[r] > self.max_heap[ma]:\n            ma = r\n        # 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出\n        if ma == i:\n            break\n        # 交换两节点\n        self.swap(i, ma)\n        # 循环向下堆化\n        i = ma\n
my_heap.cpp
/* 元素出堆 */\nvoid pop() {\n    // 判空处理\n    if (isEmpty()) {\n        throw out_of_range(\"堆为空\");\n    }\n    // 交换根节点与最右叶节点(交换首元素与尾元素)\n    swap(maxHeap[0], maxHeap[size() - 1]);\n    // 删除节点\n    maxHeap.pop_back();\n    // 从顶至底堆化\n    siftDown(0);\n}\n\n/* 从节点 i 开始,从顶至底堆化 */\nvoid siftDown(int i) {\n    while (true) {\n        // 判断节点 i, l, r 中值最大的节点,记为 ma\n        int l = left(i), r = right(i), ma = i;\n        if (l < size() && maxHeap[l] > maxHeap[ma])\n            ma = l;\n        if (r < size() && maxHeap[r] > maxHeap[ma])\n            ma = r;\n        // 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出\n        if (ma == i)\n            break;\n        swap(maxHeap[i], maxHeap[ma]);\n        // 循环向下堆化\n        i = ma;\n    }\n}\n
my_heap.java
/* 元素出堆 */\nint pop() {\n    // 判空处理\n    if (isEmpty())\n        throw new IndexOutOfBoundsException();\n    // 交换根节点与最右叶节点(交换首元素与尾元素)\n    swap(0, size() - 1);\n    // 删除节点\n    int val = maxHeap.remove(size() - 1);\n    // 从顶至底堆化\n    siftDown(0);\n    // 返回堆顶元素\n    return val;\n}\n\n/* 从节点 i 开始,从顶至底堆化 */\nvoid siftDown(int i) {\n    while (true) {\n        // 判断节点 i, l, r 中值最大的节点,记为 ma\n        int l = left(i), r = right(i), ma = i;\n        if (l < size() && maxHeap.get(l) > maxHeap.get(ma))\n            ma = l;\n        if (r < size() && maxHeap.get(r) > maxHeap.get(ma))\n            ma = r;\n        // 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出\n        if (ma == i)\n            break;\n        // 交换两节点\n        swap(i, ma);\n        // 循环向下堆化\n        i = ma;\n    }\n}\n
my_heap.cs
/* 元素出堆 */\nint Pop() {\n    // 判空处理\n    if (IsEmpty())\n        throw new IndexOutOfRangeException();\n    // 交换根节点与最右叶节点(交换首元素与尾元素)\n    Swap(0, Size() - 1);\n    // 删除节点\n    int val = maxHeap.Last();\n    maxHeap.RemoveAt(Size() - 1);\n    // 从顶至底堆化\n    SiftDown(0);\n    // 返回堆顶元素\n    return val;\n}\n\n/* 从节点 i 开始,从顶至底堆化 */\nvoid SiftDown(int i) {\n    while (true) {\n        // 判断节点 i, l, r 中值最大的节点,记为 ma\n        int l = Left(i), r = Right(i), ma = i;\n        if (l < Size() && maxHeap[l] > maxHeap[ma])\n            ma = l;\n        if (r < Size() && maxHeap[r] > maxHeap[ma])\n            ma = r;\n        // 若“节点 i 最大”或“越过叶节点”,则结束堆化\n        if (ma == i) break;\n        // 交换两节点\n        Swap(i, ma);\n        // 循环向下堆化\n        i = ma;\n    }\n}\n
my_heap.go
/* 元素出堆 */\nfunc (h *maxHeap) pop() any {\n    // 判空处理\n    if h.isEmpty() {\n        fmt.Println(\"error\")\n        return nil\n    }\n    // 交换根节点与最右叶节点(交换首元素与尾元素)\n    h.swap(0, h.size()-1)\n    // 删除节点\n    val := h.data[len(h.data)-1]\n    h.data = h.data[:len(h.data)-1]\n    // 从顶至底堆化\n    h.siftDown(0)\n\n    // 返回堆顶元素\n    return val\n}\n\n/* 从节点 i 开始,从顶至底堆化 */\nfunc (h *maxHeap) siftDown(i int) {\n    for true {\n        // 判断节点 i, l, r 中值最大的节点,记为 max\n        l, r, max := h.left(i), h.right(i), i\n        if l < h.size() && h.data[l].(int) > h.data[max].(int) {\n            max = l\n        }\n        if r < h.size() && h.data[r].(int) > h.data[max].(int) {\n            max = r\n        }\n        // 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出\n        if max == i {\n            break\n        }\n        // 交换两节点\n        h.swap(i, max)\n        // 循环向下堆化\n        i = max\n    }\n}\n
my_heap.swift
/* 元素出堆 */\nfunc pop() -> Int {\n    // 判空处理\n    if isEmpty() {\n        fatalError(\"堆为空\")\n    }\n    // 交换根节点与最右叶节点(交换首元素与尾元素)\n    swap(i: 0, j: size() - 1)\n    // 删除节点\n    let val = maxHeap.remove(at: size() - 1)\n    // 从顶至底堆化\n    siftDown(i: 0)\n    // 返回堆顶元素\n    return val\n}\n\n/* 从节点 i 开始,从顶至底堆化 */\nfunc siftDown(i: Int) {\n    var i = i\n    while true {\n        // 判断节点 i, l, r 中值最大的节点,记为 ma\n        let l = left(i: i)\n        let r = right(i: i)\n        var ma = i\n        if l < size(), maxHeap[l] > maxHeap[ma] {\n            ma = l\n        }\n        if r < size(), maxHeap[r] > maxHeap[ma] {\n            ma = r\n        }\n        // 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出\n        if ma == i {\n            break\n        }\n        // 交换两节点\n        swap(i: i, j: ma)\n        // 循环向下堆化\n        i = ma\n    }\n}\n
my_heap.js
/* 元素出堆 */\npop() {\n    // 判空处理\n    if (this.isEmpty()) throw new Error('堆为空');\n    // 交换根节点与最右叶节点(交换首元素与尾元素)\n    this.#swap(0, this.size() - 1);\n    // 删除节点\n    const val = this.#maxHeap.pop();\n    // 从顶至底堆化\n    this.#siftDown(0);\n    // 返回堆顶元素\n    return val;\n}\n\n/* 从节点 i 开始,从顶至底堆化 */\n#siftDown(i) {\n    while (true) {\n        // 判断节点 i, l, r 中值最大的节点,记为 ma\n        const l = this.#left(i),\n            r = this.#right(i);\n        let ma = i;\n        if (l < this.size() && this.#maxHeap[l] > this.#maxHeap[ma]) ma = l;\n        if (r < this.size() && this.#maxHeap[r] > this.#maxHeap[ma]) ma = r;\n        // 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出\n        if (ma === i) break;\n        // 交换两节点\n        this.#swap(i, ma);\n        // 循环向下堆化\n        i = ma;\n    }\n}\n
my_heap.ts
/* 元素出堆 */\npop(): number {\n    // 判空处理\n    if (this.isEmpty()) throw new RangeError('Heap is empty.');\n    // 交换根节点与最右叶节点(交换首元素与尾元素)\n    this.swap(0, this.size() - 1);\n    // 删除节点\n    const val = this.maxHeap.pop();\n    // 从顶至底堆化\n    this.siftDown(0);\n    // 返回堆顶元素\n    return val;\n}\n\n/* 从节点 i 开始,从顶至底堆化 */\nsiftDown(i: number): void {\n    while (true) {\n        // 判断节点 i, l, r 中值最大的节点,记为 ma\n        const l = this.left(i),\n            r = this.right(i);\n        let ma = i;\n        if (l < this.size() && this.maxHeap[l] > this.maxHeap[ma]) ma = l;\n        if (r < this.size() && this.maxHeap[r] > this.maxHeap[ma]) ma = r;\n        // 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出\n        if (ma === i) break;\n        // 交换两节点\n        this.swap(i, ma);\n        // 循环向下堆化\n        i = ma;\n    }\n}\n
my_heap.dart
/* 元素出堆 */\nint pop() {\n  // 判空处理\n  if (isEmpty()) throw Exception('堆为空');\n  // 交换根节点与最右叶节点(交换首元素与尾元素)\n  _swap(0, size() - 1);\n  // 删除节点\n  int val = _maxHeap.removeLast();\n  // 从顶至底堆化\n  siftDown(0);\n  // 返回堆顶元素\n  return val;\n}\n\n/* 从节点 i 开始,从顶至底堆化 */\nvoid siftDown(int i) {\n  while (true) {\n    // 判断节点 i, l, r 中值最大的节点,记为 ma\n    int l = _left(i);\n    int r = _right(i);\n    int ma = i;\n    if (l < size() && _maxHeap[l] > _maxHeap[ma]) ma = l;\n    if (r < size() && _maxHeap[r] > _maxHeap[ma]) ma = r;\n    // 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出\n    if (ma == i) break;\n    // 交换两节点\n    _swap(i, ma);\n    // 循环向下堆化\n    i = ma;\n  }\n}\n
my_heap.rs
/* 元素出堆 */\nfn pop(&mut self) -> i32 {\n    // 判空处理\n    if self.is_empty() {\n        panic!(\"index out of bounds\");\n    }\n    // 交换根节点与最右叶节点(交换首元素与尾元素)\n    self.swap(0, self.size() - 1);\n    // 删除节点\n    let val = self.max_heap.pop().unwrap();\n    // 从顶至底堆化\n    self.sift_down(0);\n    // 返回堆顶元素\n    val\n}\n\n/* 从节点 i 开始,从顶至底堆化 */\nfn sift_down(&mut self, mut i: usize) {\n    loop {\n        // 判断节点 i, l, r 中值最大的节点,记为 ma\n        let (l, r, mut ma) = (Self::left(i), Self::right(i), i);\n        if l < self.size() && self.max_heap[l] > self.max_heap[ma] {\n            ma = l;\n        }\n        if r < self.size() && self.max_heap[r] > self.max_heap[ma] {\n            ma = r;\n        }\n        // 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出\n        if ma == i {\n            break;\n        }\n        // 交换两节点\n        self.swap(i, ma);\n        // 循环向下堆化\n        i = ma;\n    }\n}\n
my_heap.c
/* 元素出堆 */\nint pop(MaxHeap *maxHeap) {\n    // 判空处理\n    if (isEmpty(maxHeap)) {\n        printf(\"heap is empty!\");\n        return INT_MAX;\n    }\n    // 交换根节点与最右叶节点(交换首元素与尾元素)\n    swap(maxHeap, 0, size(maxHeap) - 1);\n    // 删除节点\n    int val = maxHeap->data[maxHeap->size - 1];\n    maxHeap->size--;\n    // 从顶至底堆化\n    siftDown(maxHeap, 0);\n\n    // 返回堆顶元素\n    return val;\n}\n\n/* 从节点 i 开始,从顶至底堆化 */\nvoid siftDown(MaxHeap *maxHeap, int i) {\n    while (true) {\n        // 判断节点 i, l, r 中值最大的节点,记为 max\n        int l = left(maxHeap, i);\n        int r = right(maxHeap, i);\n        int max = i;\n        if (l < size(maxHeap) && maxHeap->data[l] > maxHeap->data[max]) {\n            max = l;\n        }\n        if (r < size(maxHeap) && maxHeap->data[r] > maxHeap->data[max]) {\n            max = r;\n        }\n        // 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出\n        if (max == i) {\n            break;\n        }\n        // 交换两节点\n        swap(maxHeap, i, max);\n        // 循环向下堆化\n        i = max;\n    }\n}\n
my_heap.kt
/* 元素出堆 */\nfun pop(): Int {\n    // 判空处理\n    if (isEmpty()) throw IndexOutOfBoundsException()\n    // 交换根节点与最右叶节点(交换首元素与尾元素)\n    swap(0, size() - 1)\n    // 删除节点\n    val _val = maxHeap.removeAt(size() - 1)\n    // 从顶至底堆化\n    siftDown(0)\n    // 返回堆顶元素\n    return _val\n}\n\n/* 从节点 i 开始,从顶至底堆化 */\nfun siftDown(it: Int) {\n    // Kotlin的函数参数不可变,因此创建临时变量\n    var i = it\n    while (true) {\n        // 判断节点 i, l, r 中值最大的节点,记为 ma\n        val l = left(i)\n        val r = right(i)\n        var ma = i\n        if (l < size() && maxHeap[l] > maxHeap[ma]) ma = l\n        if (r < size() && maxHeap[r] > maxHeap[ma]) ma = r\n        // 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出\n        if (ma == i) break\n        // 交换两节点\n        swap(i, ma)\n        // 循环向下堆化\n        i = ma\n    }\n}\n
my_heap.rb
### 元素出堆 ###\ndef pop\n  # 判空处理\n  raise IndexError, \"堆为空\" if is_empty?\n  # 交换根节点与最右叶节点(交换首元素与尾元素)\n  swap(0, size - 1)\n  # 删除节点\n  val = @max_heap.pop\n  # 从顶至底堆化\n  sift_down(0)\n  # 返回堆顶元素\n  val\nend\n\n### 从节点 i 开始,从顶至底堆化 ###\ndef sift_down(i)\n  loop do\n    # 判断节点 i, l, r 中值最大的节点,记为 ma\n    l, r, ma = left(i), right(i), i\n    ma = l if l < size && @max_heap[l] > @max_heap[ma]\n    ma = r if r < size && @max_heap[r] > @max_heap[ma]\n\n    # 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出\n    break if ma == i\n\n    # 交换两节点\n    swap(i, ma)\n    # 循环向下堆化\n    i = ma\n  end\nend\n
可视化运行

全屏观看 >

","path":["第 8 章   堆","8.1   堆"],"tags":[]},{"location":"chapter_heap/heap/#813","level":2,"title":"8.1.3   堆的常见应用","text":"","path":["第 8 章   堆","8.1   堆"],"tags":[]},{"location":"chapter_heap/summary/","level":1,"title":"8.4   小结","text":"","path":["第 8 章   堆","8.4   小结"],"tags":[]},{"location":"chapter_heap/summary/#1","level":3,"title":"1.   重点回顾","text":"","path":["第 8 章   堆","8.4   小结"],"tags":[]},{"location":"chapter_heap/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q:数据结构的“堆”与内存管理的“堆”是同一个概念吗?

两者不是同一个概念,只是碰巧都叫“堆”。计算机系统内存中的堆是动态内存分配的一部分,程序在运行时可以使用它来存储数据。程序可以请求一定量的堆内存,用于存储如对象和数组等复杂结构。当这些数据不再需要时,程序需要释放这些内存,以防止内存泄漏。相较于栈内存,堆内存的管理和使用需要更谨慎,使用不当可能会导致内存泄漏和野指针等问题。

","path":["第 8 章   堆","8.4   小结"],"tags":[]},{"location":"chapter_heap/top_k/","level":1,"title":"8.3   Top-k 问题","text":"

Question

给定一个长度为 \\(n\\) 的无序数组 nums ,请返回数组中最大的 \\(k\\) 个元素。

对于该问题,我们先介绍两种思路比较直接的解法,再介绍效率更高的堆解法。

","path":["第 8 章   堆","8.3   Top-k 问题"],"tags":[]},{"location":"chapter_heap/top_k/#831","level":2,"title":"8.3.1   方法一:遍历选择","text":"

我们可以进行图 8-6 所示的 \\(k\\) 轮遍历,分别在每轮中提取第 \\(1\\)、\\(2\\)、\\(\\dots\\)、\\(k\\) 大的元素,时间复杂度为 \\(O(nk)\\) 。

此方法只适用于 \\(k \\ll n\\) 的情况,因为当 \\(k\\) 与 \\(n\\) 比较接近时,其时间复杂度趋向于 \\(O(n^2)\\) ,非常耗时。

图 8-6   遍历寻找最大的 k 个元素

Tip

当 \\(k = n\\) 时,我们可以得到完整的有序序列,此时等价于“选择排序”算法。

","path":["第 8 章   堆","8.3   Top-k 问题"],"tags":[]},{"location":"chapter_heap/top_k/#832","level":2,"title":"8.3.2   方法二:排序","text":"

如图 8-7 所示,我们可以先对数组 nums 进行排序,再返回最右边的 \\(k\\) 个元素,时间复杂度为 \\(O(n \\log n)\\) 。

显然,该方法“超额”完成任务了,因为我们只需找出最大的 \\(k\\) 个元素即可,而不需要排序其他元素。

图 8-7   排序寻找最大的 k 个元素

","path":["第 8 章   堆","8.3   Top-k 问题"],"tags":[]},{"location":"chapter_heap/top_k/#833","level":2,"title":"8.3.3   方法三:堆","text":"

我们可以基于堆更加高效地解决 Top-k 问题,流程如图 8-8 所示。

  1. 初始化一个小顶堆,其堆顶元素最小。
  2. 先将数组的前 \\(k\\) 个元素依次入堆。
  3. 从第 \\(k + 1\\) 个元素开始,若当前元素大于堆顶元素,则将堆顶元素出堆,并将当前元素入堆。
  4. 遍历完成后,堆中保存的就是最大的 \\(k\\) 个元素。
<1><2><3><4><5><6><7><8><9>

图 8-8   基于堆寻找最大的 k 个元素

示例代码如下:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby top_k.py
def top_k_heap(nums: list[int], k: int) -> list[int]:\n    \"\"\"基于堆查找数组中最大的 k 个元素\"\"\"\n    # 初始化小顶堆\n    heap = []\n    # 将数组的前 k 个元素入堆\n    for i in range(k):\n        heapq.heappush(heap, nums[i])\n    # 从第 k+1 个元素开始,保持堆的长度为 k\n    for i in range(k, len(nums)):\n        # 若当前元素大于堆顶元素,则将堆顶元素出堆、当前元素入堆\n        if nums[i] > heap[0]:\n            heapq.heappop(heap)\n            heapq.heappush(heap, nums[i])\n    return heap\n
top_k.cpp
/* 基于堆查找数组中最大的 k 个元素 */\npriority_queue<int, vector<int>, greater<int>> topKHeap(vector<int> &nums, int k) {\n    // 初始化小顶堆\n    priority_queue<int, vector<int>, greater<int>> heap;\n    // 将数组的前 k 个元素入堆\n    for (int i = 0; i < k; i++) {\n        heap.push(nums[i]);\n    }\n    // 从第 k+1 个元素开始,保持堆的长度为 k\n    for (int i = k; i < nums.size(); i++) {\n        // 若当前元素大于堆顶元素,则将堆顶元素出堆、当前元素入堆\n        if (nums[i] > heap.top()) {\n            heap.pop();\n            heap.push(nums[i]);\n        }\n    }\n    return heap;\n}\n
top_k.java
/* 基于堆查找数组中最大的 k 个元素 */\nQueue<Integer> topKHeap(int[] nums, int k) {\n    // 初始化小顶堆\n    Queue<Integer> heap = new PriorityQueue<Integer>();\n    // 将数组的前 k 个元素入堆\n    for (int i = 0; i < k; i++) {\n        heap.offer(nums[i]);\n    }\n    // 从第 k+1 个元素开始,保持堆的长度为 k\n    for (int i = k; i < nums.length; i++) {\n        // 若当前元素大于堆顶元素,则将堆顶元素出堆、当前元素入堆\n        if (nums[i] > heap.peek()) {\n            heap.poll();\n            heap.offer(nums[i]);\n        }\n    }\n    return heap;\n}\n
top_k.cs
/* 基于堆查找数组中最大的 k 个元素 */\nPriorityQueue<int, int> TopKHeap(int[] nums, int k) {\n    // 初始化小顶堆\n    PriorityQueue<int, int> heap = new();\n    // 将数组的前 k 个元素入堆\n    for (int i = 0; i < k; i++) {\n        heap.Enqueue(nums[i], nums[i]);\n    }\n    // 从第 k+1 个元素开始,保持堆的长度为 k\n    for (int i = k; i < nums.Length; i++) {\n        // 若当前元素大于堆顶元素,则将堆顶元素出堆、当前元素入堆\n        if (nums[i] > heap.Peek()) {\n            heap.Dequeue();\n            heap.Enqueue(nums[i], nums[i]);\n        }\n    }\n    return heap;\n}\n
top_k.go
/* 基于堆查找数组中最大的 k 个元素 */\nfunc topKHeap(nums []int, k int) *minHeap {\n    // 初始化小顶堆\n    h := &minHeap{}\n    heap.Init(h)\n    // 将数组的前 k 个元素入堆\n    for i := 0; i < k; i++ {\n        heap.Push(h, nums[i])\n    }\n    // 从第 k+1 个元素开始,保持堆的长度为 k\n    for i := k; i < len(nums); i++ {\n        // 若当前元素大于堆顶元素,则将堆顶元素出堆、当前元素入堆\n        if nums[i] > h.Top().(int) {\n            heap.Pop(h)\n            heap.Push(h, nums[i])\n        }\n    }\n    return h\n}\n
top_k.swift
/* 基于堆查找数组中最大的 k 个元素 */\nfunc topKHeap(nums: [Int], k: Int) -> [Int] {\n    // 初始化一个小顶堆,并将前 k 个元素建堆\n    var heap = Heap(nums.prefix(k))\n    // 从第 k+1 个元素开始,保持堆的长度为 k\n    for i in nums.indices.dropFirst(k) {\n        // 若当前元素大于堆顶元素,则将堆顶元素出堆、当前元素入堆\n        if nums[i] > heap.min()! {\n            _ = heap.removeMin()\n            heap.insert(nums[i])\n        }\n    }\n    return heap.unordered\n}\n
top_k.js
/* 元素入堆 */\nfunction pushMinHeap(maxHeap, val) {\n    // 元素取反\n    maxHeap.push(-val);\n}\n\n/* 元素出堆 */\nfunction popMinHeap(maxHeap) {\n    // 元素取反\n    return -maxHeap.pop();\n}\n\n/* 访问堆顶元素 */\nfunction peekMinHeap(maxHeap) {\n    // 元素取反\n    return -maxHeap.peek();\n}\n\n/* 取出堆中元素 */\nfunction getMinHeap(maxHeap) {\n    // 元素取反\n    return maxHeap.getMaxHeap().map((num) => -num);\n}\n\n/* 基于堆查找数组中最大的 k 个元素 */\nfunction topKHeap(nums, k) {\n    // 初始化小顶堆\n    // 请注意:我们将堆中所有元素取反,从而用大顶堆来模拟小顶堆\n    const maxHeap = new MaxHeap([]);\n    // 将数组的前 k 个元素入堆\n    for (let i = 0; i < k; i++) {\n        pushMinHeap(maxHeap, nums[i]);\n    }\n    // 从第 k+1 个元素开始,保持堆的长度为 k\n    for (let i = k; i < nums.length; i++) {\n        // 若当前元素大于堆顶元素,则将堆顶元素出堆、当前元素入堆\n        if (nums[i] > peekMinHeap(maxHeap)) {\n            popMinHeap(maxHeap);\n            pushMinHeap(maxHeap, nums[i]);\n        }\n    }\n    // 返回堆中元素\n    return getMinHeap(maxHeap);\n}\n
top_k.ts
/* 元素入堆 */\nfunction pushMinHeap(maxHeap: MaxHeap, val: number): void {\n    // 元素取反\n    maxHeap.push(-val);\n}\n\n/* 元素出堆 */\nfunction popMinHeap(maxHeap: MaxHeap): number {\n    // 元素取反\n    return -maxHeap.pop();\n}\n\n/* 访问堆顶元素 */\nfunction peekMinHeap(maxHeap: MaxHeap): number {\n    // 元素取反\n    return -maxHeap.peek();\n}\n\n/* 取出堆中元素 */\nfunction getMinHeap(maxHeap: MaxHeap): number[] {\n    // 元素取反\n    return maxHeap.getMaxHeap().map((num: number) => -num);\n}\n\n/* 基于堆查找数组中最大的 k 个元素 */\nfunction topKHeap(nums: number[], k: number): number[] {\n    // 初始化小顶堆\n    // 请注意:我们将堆中所有元素取反,从而用大顶堆来模拟小顶堆\n    const maxHeap = new MaxHeap([]);\n    // 将数组的前 k 个元素入堆\n    for (let i = 0; i < k; i++) {\n        pushMinHeap(maxHeap, nums[i]);\n    }\n    // 从第 k+1 个元素开始,保持堆的长度为 k\n    for (let i = k; i < nums.length; i++) {\n        // 若当前元素大于堆顶元素,则将堆顶元素出堆、当前元素入堆\n        if (nums[i] > peekMinHeap(maxHeap)) {\n            popMinHeap(maxHeap);\n            pushMinHeap(maxHeap, nums[i]);\n        }\n    }\n    // 返回堆中元素\n    return getMinHeap(maxHeap);\n}\n
top_k.dart
/* 基于堆查找数组中最大的 k 个元素 */\nMinHeap topKHeap(List<int> nums, int k) {\n  // 初始化小顶堆,将数组的前 k 个元素入堆\n  MinHeap heap = MinHeap(nums.sublist(0, k));\n  // 从第 k+1 个元素开始,保持堆的长度为 k\n  for (int i = k; i < nums.length; i++) {\n    // 若当前元素大于堆顶元素,则将堆顶元素出堆、当前元素入堆\n    if (nums[i] > heap.peek()) {\n      heap.pop();\n      heap.push(nums[i]);\n    }\n  }\n  return heap;\n}\n
top_k.rs
/* 基于堆查找数组中最大的 k 个元素 */\nfn top_k_heap(nums: Vec<i32>, k: usize) -> BinaryHeap<Reverse<i32>> {\n    // BinaryHeap 是大顶堆,使用 Reverse 将元素取反,从而实现小顶堆\n    let mut heap = BinaryHeap::<Reverse<i32>>::new();\n    // 将数组的前 k 个元素入堆\n    for &num in nums.iter().take(k) {\n        heap.push(Reverse(num));\n    }\n    // 从第 k+1 个元素开始,保持堆的长度为 k\n    for &num in nums.iter().skip(k) {\n        // 若当前元素大于堆顶元素,则将堆顶元素出堆、当前元素入堆\n        if num > heap.peek().unwrap().0 {\n            heap.pop();\n            heap.push(Reverse(num));\n        }\n    }\n    heap\n}\n
top_k.c
/* 元素入堆 */\nvoid pushMinHeap(MaxHeap *maxHeap, int val) {\n    // 元素取反\n    push(maxHeap, -val);\n}\n\n/* 元素出堆 */\nint popMinHeap(MaxHeap *maxHeap) {\n    // 元素取反\n    return -pop(maxHeap);\n}\n\n/* 访问堆顶元素 */\nint peekMinHeap(MaxHeap *maxHeap) {\n    // 元素取反\n    return -peek(maxHeap);\n}\n\n/* 取出堆中元素 */\nint *getMinHeap(MaxHeap *maxHeap) {\n    // 将堆中所有元素取反并存入 res 数组\n    int *res = (int *)malloc(maxHeap->size * sizeof(int));\n    for (int i = 0; i < maxHeap->size; i++) {\n        res[i] = -maxHeap->data[i];\n    }\n    return res;\n}\n\n/* 取出堆中元素 */\nint *getMinHeap(MaxHeap *maxHeap) {\n    // 将堆中所有元素取反并存入 res 数组\n    int *res = (int *)malloc(maxHeap->size * sizeof(int));\n    for (int i = 0; i < maxHeap->size; i++) {\n        res[i] = -maxHeap->data[i];\n    }\n    return res;\n}\n\n// 基于堆查找数组中最大的 k 个元素的函数\nint *topKHeap(int *nums, int sizeNums, int k) {\n    // 初始化小顶堆\n    // 请注意:我们将堆中所有元素取反,从而用大顶堆来模拟小顶堆\n    int *empty = (int *)malloc(0);\n    MaxHeap *maxHeap = newMaxHeap(empty, 0);\n    // 将数组的前 k 个元素入堆\n    for (int i = 0; i < k; i++) {\n        pushMinHeap(maxHeap, nums[i]);\n    }\n    // 从第 k+1 个元素开始,保持堆的长度为 k\n    for (int i = k; i < sizeNums; i++) {\n        // 若当前元素大于堆顶元素,则将堆顶元素出堆、当前元素入堆\n        if (nums[i] > peekMinHeap(maxHeap)) {\n            popMinHeap(maxHeap);\n            pushMinHeap(maxHeap, nums[i]);\n        }\n    }\n    int *res = getMinHeap(maxHeap);\n    // 释放内存\n    delMaxHeap(maxHeap);\n    return res;\n}\n
top_k.kt
/* 基于堆查找数组中最大的 k 个元素 */\nfun topKHeap(nums: IntArray, k: Int): Queue<Int> {\n    // 初始化小顶堆\n    val heap = PriorityQueue<Int>()\n    // 将数组的前 k 个元素入堆\n    for (i in 0..<k) {\n        heap.offer(nums[i])\n    }\n    // 从第 k+1 个元素开始,保持堆的长度为 k\n    for (i in k..<nums.size) {\n        // 若当前元素大于堆顶元素,则将堆顶元素出堆、当前元素入堆\n        if (nums[i] > heap.peek()) {\n            heap.poll()\n            heap.offer(nums[i])\n        }\n    }\n    return heap\n}\n
top_k.rb
### 基于堆查找数组中最大的 k 个元素 ###\ndef top_k_heap(nums, k)\n  # 初始化小顶堆\n  # 请注意:我们将堆中所有元素取反,从而用大顶堆来模拟小顶堆\n  max_heap = MaxHeap.new([])\n\n  # 将数组的前 k 个元素入堆\n  for i in 0...k\n    push_min_heap(max_heap, nums[i])\n  end\n\n  # 从第 k+1 个元素开始,保持堆的长度为 k\n  for i in k...nums.length\n    # 若当前元素大于堆顶元素,则将堆顶元素出堆、当前元素入堆\n    if nums[i] > peek_min_heap(max_heap)\n      pop_min_heap(max_heap)\n      push_min_heap(max_heap, nums[i])\n    end\n  end\n\n  get_min_heap(max_heap)\nend\n
可视化运行

全屏观看 >

总共执行了 \\(n\\) 轮入堆和出堆,堆的最大长度为 \\(k\\) ,因此时间复杂度为 \\(O(n \\log k)\\) 。该方法的效率很高,当 \\(k\\) 较小时,时间复杂度趋向 \\(O(n)\\) ;当 \\(k\\) 较大时,时间复杂度不会超过 \\(O(n \\log n)\\) 。

另外,该方法适用于动态数据流的使用场景。在不断加入数据时,我们可以持续维护堆内的元素,从而实现最大的 \\(k\\) 个元素的动态更新。

","path":["第 8 章   堆","8.3   Top-k 问题"],"tags":[]},{"location":"chapter_hello_algo/","level":1,"title":"序","text":"

几年前,我在力扣上分享了“剑指 Offer”系列题解,受到了许多读者的鼓励和支持。在与读者交流期间,我最常被问的一个问题是“如何入门算法”。逐渐地,我对这个问题产生了浓厚的兴趣。

两眼一抹黑地刷题似乎是最受欢迎的方法,简单、直接且有效。然而刷题就如同玩“扫雷”游戏,自学能力强的人能够顺利将地雷逐个排掉,而基础不足的人很可能被炸得满头是包,并在挫折中步步退缩。通读教材也是一种常见做法,但对于面向求职的人来说,毕业论文、投递简历、准备笔试和面试已经消耗了大部分精力,啃厚重的书往往变成了一项艰巨的挑战。

如果你也面临类似的困扰,那么很幸运这本书“找”到了你。本书是我对这个问题给出的答案,即使不是最优解,也至少是一次积极的尝试。本书虽然不足以让你直接拿到 Offer,但会引导你探索数据结构与算法的“知识地图”,带你了解不同“地雷”的形状、大小和分布位置,让你掌握各种“排雷方法”。有了这些本领,相信你可以更加自如地刷题和阅读文献,逐步构建起完整的知识体系。

我深深赞同费曼教授所言:“Knowledge isn't free. You have to pay attention.”从这个意义上看,这本书并非完全“免费”。为了不辜负你为本书所付出的宝贵“注意力”,我会竭尽所能,投入最大的“注意力”来完成本书的创作。

本人自知学疏才浅,书中内容虽然已经过一段时间的打磨,但一定仍有许多错误,恳请各位老师和同学批评指正。

Hello,算法!

计算机的出现给世界带来了巨大变革,它凭借高速的计算能力和出色的可编程性,成为了执行算法与处理数据的理想媒介。无论是电子游戏的逼真画面、自动驾驶的智能决策,还是 AlphaGo 的精彩棋局、ChatGPT 的自然交互,这些应用都是算法在计算机上的精妙演绎。

事实上,在计算机问世之前,算法和数据结构就已经存在于世界的各个角落。早期的算法相对简单,例如古代的计数方法和工具制作步骤等。随着文明的进步,算法逐渐变得更加精细和复杂。从巧夺天工的匠人技艺、到解放生产力的工业产品、再到宇宙运行的科学规律,几乎每一件平凡或令人惊叹的事物背后,都隐藏着精妙的算法思想。

同样,数据结构无处不在:大到社会网络,小到地铁线路,许多系统都可以建模为“图”;大到一个国家,小到一个家庭,社会的主要组织形式呈现出“树”的特征;冬天的衣服就像“栈”,最先穿上的最后才能脱下;羽毛球筒则如同“队列”,一端放入、另一端取出;字典就像一个“哈希表”,能够快速查找目标词条。

本书旨在通过清晰易懂的动画图解和可运行的代码示例,使读者理解算法和数据结构的核心概念,并能够通过编程来实现它们。在此基础上,本书致力于揭示算法在复杂世界中的生动体现,展现算法之美。希望本书能够帮助到你!

","path":["序"],"tags":[]},{"location":"chapter_introduction/","level":1,"title":"第 1 章   初识算法","text":"

Abstract

一位少女翩翩起舞,与数据交织在一起,裙摆上飘扬着算法的旋律。

她邀请你共舞,请紧跟她的步伐,踏入充满逻辑与美感的算法世界。

","path":["第 1 章   初识算法"],"tags":[]},{"location":"chapter_introduction/#_1","level":2,"title":"本章内容","text":"","path":["第 1 章   初识算法"],"tags":[]},{"location":"chapter_introduction/algorithms_are_everywhere/","level":1,"title":"1.1   算法无处不在","text":"

当我们听到“算法”这个词时,很自然地会想到数学。然而实际上,许多算法并不涉及复杂数学,而是更多地依赖基本逻辑,这些逻辑在我们的日常生活中处处可见。

在正式探讨算法之前,有一个有趣的事实值得分享:你已经在不知不觉中学会了许多算法,并习惯将它们应用到日常生活中了。下面我将举几个具体的例子来证实这一点。

例一:查字典。在字典里,每个汉字都对应一个拼音,而字典是按照拼音字母顺序排列的。假设我们需要查找一个拼音首字母为 \\(r\\) 的字,通常会按照图 1-1 所示的方式实现。

  1. 翻开字典约一半的页数,查看该页的首字母是什么,假设首字母为 \\(m\\) 。
  2. 由于在拼音字母表中 \\(r\\) 位于 \\(m\\) 之后,所以排除字典前半部分,查找范围缩小到后半部分。
  3. 不断重复步骤 1. 和步骤 2. ,直至找到拼音首字母为 \\(r\\) 的页码为止。
<1><2><3><4><5>

图 1-1   查字典步骤

查字典这个小学生必备技能,实际上就是著名的“二分查找”算法。从数据结构的角度,我们可以把字典视为一个已排序的“数组”;从算法的角度,我们可以将上述查字典的一系列操作看作“二分查找”。

例二:整理扑克。我们在打牌时,每局都需要整理手中的扑克牌,使其从小到大排列,实现流程如图 1-2 所示。

  1. 将扑克牌划分为“有序”和“无序”两部分,并假设初始状态下最左 1 张扑克牌已经有序。
  2. 在无序部分抽出一张扑克牌,插入至有序部分的正确位置;完成后最左 2 张扑克已经有序。
  3. 不断循环步骤 2. ,每一轮将一张扑克牌从无序部分插入至有序部分,直至所有扑克牌都有序。

图 1-2   扑克排序步骤

上述整理扑克牌的方法本质上是“插入排序”算法,它在处理小型数据集时非常高效。许多编程语言的排序库函数中都有插入排序的身影。

例三:货币找零。假设我们在超市购买了 \\(69\\) 元的商品,给了收银员 \\(100\\) 元,则收银员需要找我们 \\(31\\) 元。他会很自然地完成如图 1-3 所示的思考。

  1. 可选项是比 \\(31\\) 元面值更小的货币,包括 \\(1\\) 元、\\(5\\) 元、\\(10\\) 元、\\(20\\) 元。
  2. 从可选项中拿出最大的 \\(20\\) 元,剩余 \\(31 - 20 = 11\\) 元。
  3. 从剩余可选项中拿出最大的 \\(10\\) 元,剩余 \\(11 - 10 = 1\\) 元。
  4. 从剩余可选项中拿出最大的 \\(1\\) 元,剩余 \\(1 - 1 = 0\\) 元。
  5. 完成找零,方案为 \\(20 + 10 + 1 = 31\\) 元。

图 1-3   货币找零过程

在以上步骤中,我们每一步都采取当前看来最好的选择(尽可能用大面额的货币),最终得到了可行的找零方案。从数据结构与算法的角度看,这种方法本质上是“贪心”算法。

小到烹饪一道菜,大到星际航行,几乎所有问题的解决都离不开算法。计算机的出现使得我们能够通过编程将数据结构存储在内存中,同时编写代码调用 CPU 和 GPU 执行算法。这样一来,我们就能把生活中的问题转移到计算机上,以更高效的方式解决各种复杂问题。

Tip

如果你对数据结构、算法、数组和二分查找等概念仍感到一知半解,请继续往下阅读,本书将引导你迈入数据结构与算法的知识殿堂。

","path":["第 1 章   初识算法","1.1   算法无处不在"],"tags":[]},{"location":"chapter_introduction/summary/","level":1,"title":"1.3   小结","text":"","path":["第 1 章   初识算法","1.3   小结"],"tags":[]},{"location":"chapter_introduction/summary/#1","level":3,"title":"1.   重点回顾","text":"","path":["第 1 章   初识算法","1.3   小结"],"tags":[]},{"location":"chapter_introduction/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q:作为一名程序员,我在日常工作中从未用算法解决过问题,常用算法都被编程语言封装好了,直接用就可以了;这是否意味着我们工作中的问题还没有到达需要算法的程度?

如果把具体的工作技能比作是武功的“招式”的话,那么基础科目应该更像是“内功”。

我认为学算法(以及其他基础科目)的意义不是在于在工作中从零实现它,而是基于学到的知识,在解决问题时能够作出专业的反应和判断,从而提升工作的整体质量。举一个简单例子,每种编程语言都内置了排序函数:

在工程领域中,大量问题是难以达到最优解的,许多问题只是被“差不多”地解决了。问题的难易程度一方面取决于问题本身的性质,另一方面也取决于观测问题的人的知识储备。人的知识越完备、经验越多,分析问题就会越深入,问题就能被解决得更优雅。

","path":["第 1 章   初识算法","1.3   小结"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/","level":1,"title":"1.2   算法是什么","text":"","path":["第 1 章   初识算法","1.2   算法是什么"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#121","level":2,"title":"1.2.1   算法定义","text":"

算法(algorithm)是在有限时间内解决特定问题的一组指令或操作步骤,它具有以下特性。

","path":["第 1 章   初识算法","1.2   算法是什么"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#122","level":2,"title":"1.2.2   数据结构定义","text":"

数据结构(data structure)是组织和存储数据的方式,涵盖数据内容、数据之间关系和数据操作方法,它具有以下设计目标。

数据结构设计是一个充满权衡的过程。如果想在某方面取得提升,往往需要在另一方面作出妥协。下面举两个例子。

","path":["第 1 章   初识算法","1.2   算法是什么"],"tags":[]},{"location":"chapter_introduction/what_is_dsa/#123","level":2,"title":"1.2.3   数据结构与算法的关系","text":"

如图 1-4 所示,数据结构与算法高度相关、紧密结合,具体表现在以下三个方面。

图 1-4   数据结构与算法的关系

数据结构与算法犹如图 1-5 所示的拼装积木。一套积木,除了包含许多零件之外,还附有详细的组装说明书。我们按照说明书一步步操作,就能组装出精美的积木模型。

图 1-5   拼装积木

两者的详细对应关系如表 1-1 所示。

表 1-1   将数据结构与算法类比为拼装积木

数据结构与算法 拼装积木 输入数据 未拼装的积木 数据结构 积木组织形式,包括形状、大小、连接方式等 算法 把积木拼成目标形态的一系列操作步骤 输出数据 积木模型

值得说明的是,数据结构与算法是独立于编程语言的。正因如此,本书得以提供基于多种编程语言的实现。

约定俗成的简称

在实际讨论时,我们通常会将“数据结构与算法”简称为“算法”。比如众所周知的 LeetCode 算法题目,实际上同时考查数据结构和算法两方面的知识。

","path":["第 1 章   初识算法","1.2   算法是什么"],"tags":[]},{"location":"chapter_paperbook/","level":1,"title":"纸质书","text":"

经过长时间的打磨,《Hello 算法》纸质书终于发布了!此时的心情可以用一句诗来形容:

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

以下视频展示了纸质书,并且包含我的一些思考:

新人 UP 主,请多多关照、一键三连~谢谢!

附纸质书快照:

","path":["纸质书"],"tags":[]},{"location":"chapter_paperbook/#_2","level":2,"title":"优势与不足","text":"

总结一下纸质书可能会给大家带来惊喜的地方:

Tip

由于纸质书和网页版的同步难度较大,因此可能会有一些细节上的不同,请您见谅!

当然,纸质书也有一些值得大家入手前考虑的地方:

Tip

“印刷质量”和“价格”就像算法中的“时间效率”和“空间效率”,难以两全。而我认为,“印刷质量”对应的是“时间效率”,更应该被注重。

","path":["纸质书"],"tags":[]},{"location":"chapter_paperbook/#_3","level":2,"title":"购买链接","text":"

如果你对纸质书感兴趣,可以考虑入手一本。我们为大家争取到了新书 5 折优惠,请见此链接或扫描以下二维码:

","path":["纸质书"],"tags":[]},{"location":"chapter_paperbook/#_4","level":2,"title":"尾记","text":"

起初,我低估了纸质书出版的工作量,以为只要维护好了开源项目,纸质版就可以通过某些自动化手段生成出来。实践证明,纸质书的生产流程与开源项目的更新机制存在很大的不同,两者之间的转化需要做许多额外工作。

一本书的初稿与达到出版标准的定稿之间仍有较长距离,需要出版社(策划、编辑、设计、市场等)与作者的通力合作、长期雕琢。在此感谢图灵策划编辑王军花、以及人民邮电出版社和图灵社区每位参与本书出版流程的工作人员!

希望这本书能够帮助到你!

","path":["纸质书"],"tags":[]},{"location":"chapter_preface/","level":1,"title":"第 0 章   前言","text":"

Abstract

算法犹如美妙的交响乐,每一行代码都像韵律般流淌。

愿这本书在你的脑海中轻轻响起,留下独特而深刻的旋律。

","path":["第 0 章   前言"],"tags":[]},{"location":"chapter_preface/#_1","level":2,"title":"本章内容","text":"","path":["第 0 章   前言"],"tags":[]},{"location":"chapter_preface/about_the_book/","level":1,"title":"0.1   关于本书","text":"

本项目旨在创建一本开源、免费、对新手友好的数据结构与算法入门教程。

","path":["第 0 章   前言","0.1   关于本书"],"tags":[]},{"location":"chapter_preface/about_the_book/#011","level":2,"title":"0.1.1   读者对象","text":"

若你是算法初学者,从未接触过算法,或者已经有一些刷题经验,对数据结构与算法有模糊的认识,在会与不会之间反复横跳,那么本书正是为你量身定制的!

如果你已经积累一定的刷题量,熟悉大部分题型,那么本书可助你回顾与梳理算法知识体系,仓库源代码可以当作“刷题工具库”或“算法字典”来使用。

若你是算法“大神”,我们期待收到你的宝贵建议,或者一起参与创作。

前置条件

你需要至少具备任一语言的编程基础,能够阅读和编写简单代码。

","path":["第 0 章   前言","0.1   关于本书"],"tags":[]},{"location":"chapter_preface/about_the_book/#012","level":2,"title":"0.1.2   内容结构","text":"

本书的主要内容如图 0-1 所示。

图 0-1   本书主要内容

","path":["第 0 章   前言","0.1   关于本书"],"tags":[]},{"location":"chapter_preface/about_the_book/#013","level":2,"title":"0.1.3   致谢","text":"

本书在开源社区众多贡献者的共同努力下不断完善。感谢每一位投入时间与精力的撰稿人,他们是(按照 GitHub 自动生成的顺序):krahets、coderonion、Gonglja、nuomi1、Reanon、justin-tse、hpstory、danielsss、curtishd、night-cruise、S-N-O-R-L-A-X、rongyi、msk397、gvenusleo、khoaxuantu、rivertwilight、K3v123、gyt95、zhuoqinyue、yuelinxin、Zuoxun、mingXta、Phoenix0415、FangYuan33、GN-Yu、longsizhuo、pengchzn、QiLOL、Cathay-Chen、guowei-gong、xBLACKICEx、IsChristina、JoseHung、qualifier1024、hello-ikun、magentaqin、Guanngxu、thomasq0、sunshinesDL、L-Super、Transmigration-zhou、WSL0809、Slone123c、lhxsm、yuan0221、what-is-me、theNefelibatas、Shyam-Chen、sangxiaai、longranger2、codeberg-user、xiongsp、JeffersonHuang、prinpal、seven1240、Wonderdch、malone6、xiaomiusa87、gaofer、bluebean-cloud、a16su、SamJin98、hongyun-robot、nanlei、XiaChuerwu、yd-j、iron-irax、mgisr、steventimes、junminhong、heshuyue、danny900714、Nigh、Dr-XYZ、MolDuM、XC-Zero、reeswell、PXG-XPG、NI-SW、Horbin-Magician、Enlightenus、YangXuanyi、xjr7670、beatrix-chan、DullSword、qq909244296、iStig、boloboloda、hts0000、gledfish、fbigm、echo1937、jiaxianhua、wenjianmin、keshida、kilikilikid、lclc6、lwbaptx、linyejoe2、liuxjerry、szu17dmy、dshlstarr、Yucao-cy、coderlef、czruby、bongbongbakudan、beintentional、ZongYangL、ZhongYuuu、ZhongGuanbin、hezhizhen、linzeyan、ZJKung、JTCPOWI、KawaiiAsh、luluxia、xb534、ztkuaikuai、yw-1021、ElaBosak233、baagod、zhouLion、yishangzhang、yi427、yanedie、yabo083、weibk、wangwang105、th1nk3r-ing、tao363、4yDX3906、syd168、sslmj2020、smilelsb、siqyka、selear、sdshaoda、Xi-Row、popozhu、nuquist19、noobcodemaker、XiaoK29、chadyi、lyl625760、lucaswangdev、llql1211、0130w、shanghai-Jerry、EJackYang、Javesun99、eltociear、lipusheng、KNChiu、BlindTerran、ShiMaRing、lovelock、FreddieLi、FloranceYeh、fanchenggang、gltianwen、goerll、nedchu、curly210102、CuB3y0nd、KraHsu、CarrotDLaw、youshaoXG、bubble9um、Asashishi、Asa0oo0o0o、fanenr、eagleanurag、akshiterate、52coder、foursevenlove、KorsChen、hopkings2008、yang-le、realwujing、Evilrabbit520、Umer-Jahangir、Turing-1024-Lee、Suremotoo、paoxiaomooo、Chieko-Seren、Senrian、Allen-Scai、19santosh99、ymmmas、Risuntsy、Richard-Zhang1019、RafaelCaso、qingpeng9802、primexiao、Urbaner3、codetypess、nidhoggfgg、MwumLi、CreatorMetaSky、martinx、ZnYang2018、hugtyftg、logan-qiu、psychelzh、Kunchen-Luo、Keynman 和 KeiichiKasai。

本书的代码审阅工作由 coderonion、curtishd、Gonglja、gvenusleo、hpstory、justin-tse、khoaxuantu、krahets、night-cruise、nuomi1、Reanon 和 rongyi 完成(按照首字母顺序排列)。感谢他们付出的时间与精力,正是他们确保了各语言代码的规范与统一。

本书的英文版由 yuelinxin、K3v123、magentaqin、QiLOL、Phoenix0415、SamJin98、yanedie、RafaelCaso、pengchzn 和 thomasq0 审阅;日文版由 eltociear 审阅;俄文版由 И. А. Шевкун 和 Yuyan Huang 审阅;繁体中文版由 Shyam-Chen 和 Dr-XYZ 审阅。正是因为他们的贡献,这本书才能够服务于更广泛的读者群体,感谢他们。

本书的 ePub 电子书生成工具由 zhongfq 开发。感谢他的贡献,为读者提供了更灵活的阅读方式。

在本书的创作过程中,我得到了许多人的帮助。

在写作过程中,我阅读了许多关于数据结构与算法的教材和文章。这些作品为本书提供了优秀的范本,确保了本书内容的准确性与品质。在此感谢所有老师和前辈的杰出贡献!

本书倡导手脑并用的学习方式,在这一点上我深受《动手学深度学习》的启发。在此向各位读者强烈推荐这本优秀的著作。

衷心感谢我的父母,正是你们一直以来的支持与鼓励,让我有机会做这件富有趣味的事。

","path":["第 0 章   前言","0.1   关于本书"],"tags":[]},{"location":"chapter_preface/suggestions/","level":1,"title":"0.2   如何使用本书","text":"

Tip

为了获得最佳的阅读体验,建议你通读本节内容。

","path":["第 0 章   前言","0.2   如何使用本书"],"tags":[]},{"location":"chapter_preface/suggestions/#021","level":2,"title":"0.2.1   行文风格约定","text":" PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
\"\"\"标题注释,用于标注函数、类、测试样例等\"\"\"\n\n# 内容注释,用于详解代码\n\n\"\"\"\n多行\n注释\n\"\"\"\n
/* 标题注释,用于标注函数、类、测试样例等 */\n\n// 内容注释,用于详解代码\n\n/**\n * 多行\n * 注释\n */\n
/* 标题注释,用于标注函数、类、测试样例等 */\n\n// 内容注释,用于详解代码\n\n/**\n * 多行\n * 注释\n */\n
/* 标题注释,用于标注函数、类、测试样例等 */\n\n// 内容注释,用于详解代码\n\n/**\n * 多行\n * 注释\n */\n
/* 标题注释,用于标注函数、类、测试样例等 */\n\n// 内容注释,用于详解代码\n\n/**\n * 多行\n * 注释\n */\n
/* 标题注释,用于标注函数、类、测试样例等 */\n\n// 内容注释,用于详解代码\n\n/**\n * 多行\n * 注释\n */\n
/* 标题注释,用于标注函数、类、测试样例等 */\n\n// 内容注释,用于详解代码\n\n/**\n * 多行\n * 注释\n */\n
/* 标题注释,用于标注函数、类、测试样例等 */\n\n// 内容注释,用于详解代码\n\n/**\n * 多行\n * 注释\n */\n
/* 标题注释,用于标注函数、类、测试样例等 */\n\n// 内容注释,用于详解代码\n\n/**\n * 多行\n * 注释\n */\n
/* 标题注释,用于标注函数、类、测试样例等 */\n\n// 内容注释,用于详解代码\n\n// 多行\n// 注释\n
/* 标题注释,用于标注函数、类、测试样例等 */\n\n// 内容注释,用于详解代码\n\n/**\n * 多行\n * 注释\n */\n
/* 标题注释,用于标注函数、类、测试样例等 */\n\n// 内容注释,用于详解代码\n\n/**\n * 多行\n * 注释\n */\n
### 标题注释,用于标注函数、类、测试样例等 ###\n\n# 内容注释,用于详解代码\n\n# 多行\n# 注释\n
","path":["第 0 章   前言","0.2   如何使用本书"],"tags":[]},{"location":"chapter_preface/suggestions/#022","level":2,"title":"0.2.2   在动画图解中高效学习","text":"

相较于文字,视频和图片具有更高的信息密度和结构化程度,更易于理解。在本书中,重点和难点知识将主要通过动画以图解形式展示,而文字则作为解释与补充。

如果你在阅读本书时,发现某段内容提供了如图 0-2 所示的动画图解,请以图为主、以文字为辅,综合两者来理解内容。

图 0-2   动画图解示例

","path":["第 0 章   前言","0.2   如何使用本书"],"tags":[]},{"location":"chapter_preface/suggestions/#023","level":2,"title":"0.2.3   在代码实践中加深理解","text":"

本书的配套代码托管在 GitHub 仓库。如图 0-3 所示,源代码附有测试样例,可一键运行。

如果时间允许,建议你参照代码自行敲一遍。如果学习时间有限,请至少通读并运行所有代码。

与阅读代码相比,编写代码的过程往往能带来更多收获。动手学,才是真的学。

图 0-3   运行代码示例

运行代码的前置工作主要分为三步。

第一步:安装本地编程环境。请参照附录所示的教程进行安装,如果已安装,则可跳过此步骤。

第二步:克隆或下载代码仓库。前往 GitHub 仓库。如果已经安装 Git ,可以通过以下命令克隆本仓库:

git clone https://github.com/krahets/hello-algo.git\n

当然,你也可以在图 0-4 所示的位置,点击“Download ZIP”按钮直接下载代码压缩包,然后在本地解压即可。

图 0-4   克隆仓库与下载代码

第三步:运行源代码。如图 0-5 所示,对于顶部标有文件名称的代码块,我们可以在仓库的 codes 文件夹内找到对应的源代码文件。源代码文件可一键运行,将帮助你节省不必要的调试时间,让你能够专注于学习内容。

图 0-5   代码块与对应的源代码文件

除了本地运行代码,网页版还支持 Python 代码的可视化运行(基于 pythontutor 实现)。如图 0-6 所示,你可以点击代码块下方的“可视化运行”来展开视图,观察算法代码的执行过程;也可以点击“全屏观看”,以获得更好的阅览体验。

图 0-6   Python 代码的可视化运行

","path":["第 0 章   前言","0.2   如何使用本书"],"tags":[]},{"location":"chapter_preface/suggestions/#024","level":2,"title":"0.2.4   在提问讨论中共同成长","text":"

在阅读本书时,请不要轻易跳过那些没学明白的知识点。欢迎在评论区提出你的问题,我和小伙伴们将竭诚为你解答,一般情况下可在两天内回复。

如图 0-7 所示,网页版每个章节的底部都配有评论区。希望你能多关注评论区的内容。一方面,你可以了解大家遇到的问题,从而查漏补缺,激发更深入的思考。另一方面,期待你能慷慨地回答其他小伙伴的问题,分享你的见解,帮助他人进步。

图 0-7   评论区示例

","path":["第 0 章   前言","0.2   如何使用本书"],"tags":[]},{"location":"chapter_preface/suggestions/#025","level":2,"title":"0.2.5   算法学习路线","text":"

从总体上看,我们可以将学习数据结构与算法的过程划分为三个阶段。

  1. 阶段一:算法入门。我们需要熟悉各种数据结构的特点和用法,学习不同算法的原理、流程、用途和效率等方面的内容。
  2. 阶段二:刷算法题。建议从热门题目开刷,先积累至少 100 道题目,熟悉主流的算法问题。初次刷题时,“知识遗忘”可能是一个挑战,但请放心,这是很正常的。我们可以按照“艾宾浩斯遗忘曲线”来复习题目,通常在进行 3~5 轮的重复后,就能将其牢记在心。推荐的题单和刷题计划请见此 GitHub 仓库。
  3. 阶段三:搭建知识体系。在学习方面,我们可以阅读算法专栏文章、解题框架和算法教材,以不断丰富知识体系。在刷题方面,可以尝试采用进阶刷题策略,如按专题分类、一题多解、一解多题等,相关的刷题心得可以在各个社区找到。

如图 0-8 所示,本书内容主要涵盖“阶段一”,旨在帮助你更高效地展开阶段二和阶段三的学习。

图 0-8   算法学习路线

","path":["第 0 章   前言","0.2   如何使用本书"],"tags":[]},{"location":"chapter_preface/summary/","level":1,"title":"0.3   小结","text":"","path":["第 0 章   前言","0.3   小结"],"tags":[]},{"location":"chapter_preface/summary/#1","level":3,"title":"1.   重点回顾","text":"","path":["第 0 章   前言","0.3   小结"],"tags":[]},{"location":"chapter_reference/","level":1,"title":"参考文献","text":"

[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] Robert Sedgewick, et al. Algorithms (4th Edition).

[4] 严蔚敏. 数据结构(C 语言版).

[5] 邓俊辉. 数据结构(C++ 语言版,第三版).

[6] 马克 艾伦 维斯著,陈越译. 数据结构与算法分析:Java语言描述(第三版).

[7] 程杰. 大话数据结构.

[8] 王争. 数据结构与算法之美.

[9] Gayle Laakmann McDowell. Cracking the Coding Interview: 189 Programming Questions and Solutions (6th Edition).

[10] Aston Zhang, et al. Dive into Deep Learning.

","path":["参考文献"],"tags":[]},{"location":"chapter_searching/","level":1,"title":"第 10 章   搜索","text":"

Abstract

搜索是一场未知的冒险,我们或许需要走遍神秘空间的每个角落,又或许可以快速锁定目标。

在这场寻觅之旅中,每一次探索都可能得到一个未曾料想的答案。

","path":["第 10 章   搜索"],"tags":[]},{"location":"chapter_searching/#_1","level":2,"title":"本章内容","text":"","path":["第 10 章   搜索"],"tags":[]},{"location":"chapter_searching/binary_search/","level":1,"title":"10.1   二分查找","text":"

二分查找(binary search)是一种基于分治策略的高效搜索算法。它利用数据的有序性,每轮缩小一半搜索范围,直至找到目标元素或搜索区间为空为止。

Question

给定一个长度为 \\(n\\) 的数组 nums ,元素按从小到大的顺序排列且不重复。请查找并返回元素 target 在该数组中的索引。若数组不包含该元素,则返回 \\(-1\\) 。示例如图 10-1 所示。

图 10-1   二分查找示例数据

如图 10-2 所示,我们先初始化指针 \\(i = 0\\) 和 \\(j = n - 1\\) ,分别指向数组首元素和尾元素,代表搜索区间 \\([0, n - 1]\\) 。请注意,中括号表示闭区间,其包含边界值本身。

接下来,循环执行以下两步。

  1. 计算中点索引 \\(m = \\lfloor {(i + j) / 2} \\rfloor\\) ,其中 \\(\\lfloor \\: \\rfloor\\) 表示向下取整操作。
  2. 判断 nums[m]target 的大小关系,分为以下三种情况。
    1. nums[m] < target 时,说明 target 在区间 \\([m + 1, j]\\) 中,因此执行 \\(i = m + 1\\) 。
    2. nums[m] > target 时,说明 target 在区间 \\([i, m - 1]\\) 中,因此执行 \\(j = m - 1\\) 。
    3. nums[m] = target 时,说明找到 target ,因此返回索引 \\(m\\) 。

若数组不包含目标元素,搜索区间最终会缩小为空。此时返回 \\(-1\\) 。

<1><2><3><4><5><6><7>

图 10-2   二分查找流程

值得注意的是,由于 \\(i\\) 和 \\(j\\) 都是 int 类型,因此 \\(i + j\\) 可能会超出 int 类型的取值范围。为了避免大数越界,我们通常采用公式 \\(m = \\lfloor {i + (j - i) / 2} \\rfloor\\) 来计算中点。

代码如下所示:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search.py
def binary_search(nums: list[int], target: int) -> int:\n    \"\"\"二分查找(双闭区间)\"\"\"\n    # 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素\n    i, j = 0, len(nums) - 1\n    # 循环,当搜索区间为空时跳出(当 i > j 时为空)\n    while i <= j:\n        # 理论上 Python 的数字可以无限大(取决于内存大小),无须考虑大数越界问题\n        m = (i + j) // 2  # 计算中点索引 m\n        if nums[m] < target:\n            i = m + 1  # 此情况说明 target 在区间 [m+1, j] 中\n        elif nums[m] > target:\n            j = m - 1  # 此情况说明 target 在区间 [i, m-1] 中\n        else:\n            return m  # 找到目标元素,返回其索引\n    return -1  # 未找到目标元素,返回 -1\n
binary_search.cpp
/* 二分查找(双闭区间) */\nint binarySearch(vector<int> &nums, int target) {\n    // 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素\n    int i = 0, j = nums.size() - 1;\n    // 循环,当搜索区间为空时跳出(当 i > j 时为空)\n    while (i <= j) {\n        int m = i + (j - i) / 2; // 计算中点索引 m\n        if (nums[m] < target)    // 此情况说明 target 在区间 [m+1, j] 中\n            i = m + 1;\n        else if (nums[m] > target) // 此情况说明 target 在区间 [i, m-1] 中\n            j = m - 1;\n        else // 找到目标元素,返回其索引\n            return m;\n    }\n    // 未找到目标元素,返回 -1\n    return -1;\n}\n
binary_search.java
/* 二分查找(双闭区间) */\nint binarySearch(int[] nums, int target) {\n    // 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素\n    int i = 0, j = nums.length - 1;\n    // 循环,当搜索区间为空时跳出(当 i > j 时为空)\n    while (i <= j) {\n        int m = i + (j - i) / 2; // 计算中点索引 m\n        if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j] 中\n            i = m + 1;\n        else if (nums[m] > target) // 此情况说明 target 在区间 [i, m-1] 中\n            j = m - 1;\n        else // 找到目标元素,返回其索引\n            return m;\n    }\n    // 未找到目标元素,返回 -1\n    return -1;\n}\n
binary_search.cs
/* 二分查找(双闭区间) */\nint BinarySearch(int[] nums, int target) {\n    // 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素\n    int i = 0, j = nums.Length - 1;\n    // 循环,当搜索区间为空时跳出(当 i > j 时为空)\n    while (i <= j) {\n        int m = i + (j - i) / 2;   // 计算中点索引 m\n        if (nums[m] < target)      // 此情况说明 target 在区间 [m+1, j] 中\n            i = m + 1;\n        else if (nums[m] > target) // 此情况说明 target 在区间 [i, m-1] 中\n            j = m - 1;\n        else                       // 找到目标元素,返回其索引\n            return m;\n    }\n    // 未找到目标元素,返回 -1\n    return -1;\n}\n
binary_search.go
/* 二分查找(双闭区间) */\nfunc binarySearch(nums []int, target int) int {\n    // 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素\n    i, j := 0, len(nums)-1\n    // 循环,当搜索区间为空时跳出(当 i > j 时为空)\n    for i <= j {\n        m := i + (j-i)/2      // 计算中点索引 m\n        if nums[m] < target { // 此情况说明 target 在区间 [m+1, j] 中\n            i = m + 1\n        } else if nums[m] > target { // 此情况说明 target 在区间 [i, m-1] 中\n            j = m - 1\n        } else { // 找到目标元素,返回其索引\n            return m\n        }\n    }\n    // 未找到目标元素,返回 -1\n    return -1\n}\n
binary_search.swift
/* 二分查找(双闭区间) */\nfunc binarySearch(nums: [Int], target: Int) -> Int {\n    // 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素\n    var i = nums.startIndex\n    var j = nums.endIndex - 1\n    // 循环,当搜索区间为空时跳出(当 i > j 时为空)\n    while i <= j {\n        let m = i + (j - i) / 2 // 计算中点索引 m\n        if nums[m] < target { // 此情况说明 target 在区间 [m+1, j] 中\n            i = m + 1\n        } else if nums[m] > target { // 此情况说明 target 在区间 [i, m-1] 中\n            j = m - 1\n        } else { // 找到目标元素,返回其索引\n            return m\n        }\n    }\n    // 未找到目标元素,返回 -1\n    return -1\n}\n
binary_search.js
/* 二分查找(双闭区间) */\nfunction binarySearch(nums, target) {\n    // 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素\n    let i = 0,\n        j = nums.length - 1;\n    // 循环,当搜索区间为空时跳出(当 i > j 时为空)\n    while (i <= j) {\n        // 计算中点索引 m ,使用 parseInt() 向下取整\n        const m = parseInt(i + (j - i) / 2);\n        if (nums[m] < target)\n            // 此情况说明 target 在区间 [m+1, j] 中\n            i = m + 1;\n        else if (nums[m] > target)\n            // 此情况说明 target 在区间 [i, m-1] 中\n            j = m - 1;\n        else return m; // 找到目标元素,返回其索引\n    }\n    // 未找到目标元素,返回 -1\n    return -1;\n}\n
binary_search.ts
/* 二分查找(双闭区间) */\nfunction binarySearch(nums: number[], target: number): number {\n    // 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素\n    let i = 0,\n        j = nums.length - 1;\n    // 循环,当搜索区间为空时跳出(当 i > j 时为空)\n    while (i <= j) {\n        // 计算中点索引 m\n        const m = Math.floor(i + (j - i) / 2);\n        if (nums[m] < target) {\n            // 此情况说明 target 在区间 [m+1, j] 中\n            i = m + 1;\n        } else if (nums[m] > target) {\n            // 此情况说明 target 在区间 [i, m-1] 中\n            j = m - 1;\n        } else {\n            // 找到目标元素,返回其索引\n            return m;\n        }\n    }\n    return -1; // 未找到目标元素,返回 -1\n}\n
binary_search.dart
/* 二分查找(双闭区间) */\nint binarySearch(List<int> nums, int target) {\n  // 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素\n  int i = 0, j = nums.length - 1;\n  // 循环,当搜索区间为空时跳出(当 i > j 时为空)\n  while (i <= j) {\n    int m = i + (j - i) ~/ 2; // 计算中点索引 m\n    if (nums[m] < target) {\n      // 此情况说明 target 在区间 [m+1, j] 中\n      i = m + 1;\n    } else if (nums[m] > target) {\n      // 此情况说明 target 在区间 [i, m-1] 中\n      j = m - 1;\n    } else {\n      // 找到目标元素,返回其索引\n      return m;\n    }\n  }\n  // 未找到目标元素,返回 -1\n  return -1;\n}\n
binary_search.rs
/* 二分查找(双闭区间) */\nfn binary_search(nums: &[i32], target: i32) -> i32 {\n    // 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素\n    let mut i = 0;\n    let mut j = nums.len() as i32 - 1;\n    // 循环,当搜索区间为空时跳出(当 i > j 时为空)\n    while i <= j {\n        let m = i + (j - i) / 2; // 计算中点索引 m\n        if nums[m as usize] < target {\n            // 此情况说明 target 在区间 [m+1, j] 中\n            i = m + 1;\n        } else if nums[m as usize] > target {\n            // 此情况说明 target 在区间 [i, m-1] 中\n            j = m - 1;\n        } else {\n            // 找到目标元素,返回其索引\n            return m;\n        }\n    }\n    // 未找到目标元素,返回 -1\n    return -1;\n}\n
binary_search.c
/* 二分查找(双闭区间) */\nint binarySearch(int *nums, int len, int target) {\n    // 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素\n    int i = 0, j = len - 1;\n    // 循环,当搜索区间为空时跳出(当 i > j 时为空)\n    while (i <= j) {\n        int m = i + (j - i) / 2; // 计算中点索引 m\n        if (nums[m] < target)    // 此情况说明 target 在区间 [m+1, j] 中\n            i = m + 1;\n        else if (nums[m] > target) // 此情况说明 target 在区间 [i, m-1] 中\n            j = m - 1;\n        else // 找到目标元素,返回其索引\n            return m;\n    }\n    // 未找到目标元素,返回 -1\n    return -1;\n}\n
binary_search.kt
/* 二分查找(双闭区间) */\nfun binarySearch(nums: IntArray, target: Int): Int {\n    // 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素\n    var i = 0\n    var j = nums.size - 1\n    // 循环,当搜索区间为空时跳出(当 i > j 时为空)\n    while (i <= j) {\n        val m = i + (j - i) / 2 // 计算中点索引 m\n        if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j] 中\n            i = m + 1\n        else if (nums[m] > target) // 此情况说明 target 在区间 [i, m-1] 中\n            j = m - 1\n        else  // 找到目标元素,返回其索引\n            return m\n    }\n    // 未找到目标元素,返回 -1\n    return -1\n}\n
binary_search.rb
### 二分查找(双闭区间) ###\ndef binary_search(nums, target)\n  # 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素\n  i, j = 0, nums.length - 1\n\n  # 循环,当搜索区间为空时跳出(当 i > j 时为空)\n  while i <= j\n    # 理论上 Ruby 的数字可以无限大(取决于内存大小),无须考虑大数越界问题\n    m = (i + j) / 2   # 计算中点索引 m\n\n    if nums[m] < target\n      i = m + 1 # 此情况说明 target 在区间 [m+1, j] 中\n    elsif nums[m] > target\n      j = m - 1 # 此情况说明 target 在区间 [i, m-1] 中\n    else\n      return m  # 找到目标元素,返回其索引\n    end\n  end\n\n  -1  # 未找到目标元素,返回 -1\nend\n
可视化运行

全屏观看 >

时间复杂度为 \\(O(\\log n)\\) :在二分循环中,区间每轮缩小一半,因此循环次数为 \\(\\log_2 n\\) 。

空间复杂度为 \\(O(1)\\) :指针 \\(i\\) 和 \\(j\\) 使用常数大小空间。

","path":["第 10 章   搜索","10.1   二分查找"],"tags":[]},{"location":"chapter_searching/binary_search/#1011","level":2,"title":"10.1.1   区间表示方法","text":"

除了上述双闭区间外,常见的区间表示还有“左闭右开”区间,定义为 \\([0, n)\\) ,即左边界包含自身,右边界不包含自身。在该表示下,区间 \\([i, j)\\) 在 \\(i = j\\) 时为空。

我们可以基于该表示实现具有相同功能的二分查找算法:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search.py
def binary_search_lcro(nums: list[int], target: int) -> int:\n    \"\"\"二分查找(左闭右开区间)\"\"\"\n    # 初始化左闭右开区间 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1\n    i, j = 0, len(nums)\n    # 循环,当搜索区间为空时跳出(当 i = j 时为空)\n    while i < j:\n        m = (i + j) // 2  # 计算中点索引 m\n        if nums[m] < target:\n            i = m + 1  # 此情况说明 target 在区间 [m+1, j) 中\n        elif nums[m] > target:\n            j = m  # 此情况说明 target 在区间 [i, m) 中\n        else:\n            return m  # 找到目标元素,返回其索引\n    return -1  # 未找到目标元素,返回 -1\n
binary_search.cpp
/* 二分查找(左闭右开区间) */\nint binarySearchLCRO(vector<int> &nums, int target) {\n    // 初始化左闭右开区间 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1\n    int i = 0, j = nums.size();\n    // 循环,当搜索区间为空时跳出(当 i = j 时为空)\n    while (i < j) {\n        int m = i + (j - i) / 2; // 计算中点索引 m\n        if (nums[m] < target)    // 此情况说明 target 在区间 [m+1, j) 中\n            i = m + 1;\n        else if (nums[m] > target) // 此情况说明 target 在区间 [i, m) 中\n            j = m;\n        else // 找到目标元素,返回其索引\n            return m;\n    }\n    // 未找到目标元素,返回 -1\n    return -1;\n}\n
binary_search.java
/* 二分查找(左闭右开区间) */\nint binarySearchLCRO(int[] nums, int target) {\n    // 初始化左闭右开区间 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1\n    int i = 0, j = nums.length;\n    // 循环,当搜索区间为空时跳出(当 i = j 时为空)\n    while (i < j) {\n        int m = i + (j - i) / 2; // 计算中点索引 m\n        if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j) 中\n            i = m + 1;\n        else if (nums[m] > target) // 此情况说明 target 在区间 [i, m) 中\n            j = m;\n        else // 找到目标元素,返回其索引\n            return m;\n    }\n    // 未找到目标元素,返回 -1\n    return -1;\n}\n
binary_search.cs
/* 二分查找(左闭右开区间) */\nint BinarySearchLCRO(int[] nums, int target) {\n    // 初始化左闭右开区间 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1\n    int i = 0, j = nums.Length;\n    // 循环,当搜索区间为空时跳出(当 i = j 时为空)\n    while (i < j) {\n        int m = i + (j - i) / 2;   // 计算中点索引 m\n        if (nums[m] < target)      // 此情况说明 target 在区间 [m+1, j) 中\n            i = m + 1;\n        else if (nums[m] > target) // 此情况说明 target 在区间 [i, m) 中\n            j = m;\n        else                       // 找到目标元素,返回其索引\n            return m;\n    }\n    // 未找到目标元素,返回 -1\n    return -1;\n}\n
binary_search.go
/* 二分查找(左闭右开区间) */\nfunc binarySearchLCRO(nums []int, target int) int {\n    // 初始化左闭右开区间 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1\n    i, j := 0, len(nums)\n    // 循环,当搜索区间为空时跳出(当 i = j 时为空)\n    for i < j {\n        m := i + (j-i)/2      // 计算中点索引 m\n        if nums[m] < target { // 此情况说明 target 在区间 [m+1, j) 中\n            i = m + 1\n        } else if nums[m] > target { // 此情况说明 target 在区间 [i, m) 中\n            j = m\n        } else { // 找到目标元素,返回其索引\n            return m\n        }\n    }\n    // 未找到目标元素,返回 -1\n    return -1\n}\n
binary_search.swift
/* 二分查找(左闭右开区间) */\nfunc binarySearchLCRO(nums: [Int], target: Int) -> Int {\n    // 初始化左闭右开区间 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1\n    var i = nums.startIndex\n    var j = nums.endIndex\n    // 循环,当搜索区间为空时跳出(当 i = j 时为空)\n    while i < j {\n        let m = i + (j - i) / 2 // 计算中点索引 m\n        if nums[m] < target { // 此情况说明 target 在区间 [m+1, j) 中\n            i = m + 1\n        } else if nums[m] > target { // 此情况说明 target 在区间 [i, m) 中\n            j = m\n        } else { // 找到目标元素,返回其索引\n            return m\n        }\n    }\n    // 未找到目标元素,返回 -1\n    return -1\n}\n
binary_search.js
/* 二分查找(左闭右开区间) */\nfunction binarySearchLCRO(nums, target) {\n    // 初始化左闭右开区间 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1\n    let i = 0,\n        j = nums.length;\n    // 循环,当搜索区间为空时跳出(当 i = j 时为空)\n    while (i < j) {\n        // 计算中点索引 m ,使用 parseInt() 向下取整\n        const m = parseInt(i + (j - i) / 2);\n        if (nums[m] < target)\n            // 此情况说明 target 在区间 [m+1, j) 中\n            i = m + 1;\n        else if (nums[m] > target)\n            // 此情况说明 target 在区间 [i, m) 中\n            j = m;\n        // 找到目标元素,返回其索引\n        else return m;\n    }\n    // 未找到目标元素,返回 -1\n    return -1;\n}\n
binary_search.ts
/* 二分查找(左闭右开区间) */\nfunction binarySearchLCRO(nums: number[], target: number): number {\n    // 初始化左闭右开区间 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1\n    let i = 0,\n        j = nums.length;\n    // 循环,当搜索区间为空时跳出(当 i = j 时为空)\n    while (i < j) {\n        // 计算中点索引 m\n        const m = Math.floor(i + (j - i) / 2);\n        if (nums[m] < target) {\n            // 此情况说明 target 在区间 [m+1, j) 中\n            i = m + 1;\n        } else if (nums[m] > target) {\n            // 此情况说明 target 在区间 [i, m) 中\n            j = m;\n        } else {\n            // 找到目标元素,返回其索引\n            return m;\n        }\n    }\n    return -1; // 未找到目标元素,返回 -1\n}\n
binary_search.dart
/* 二分查找(左闭右开区间) */\nint binarySearchLCRO(List<int> nums, int target) {\n  // 初始化左闭右开区间 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1\n  int i = 0, j = nums.length;\n  // 循环,当搜索区间为空时跳出(当 i = j 时为空)\n  while (i < j) {\n    int m = i + (j - i) ~/ 2; // 计算中点索引 m\n    if (nums[m] < target) {\n      // 此情况说明 target 在区间 [m+1, j) 中\n      i = m + 1;\n    } else if (nums[m] > target) {\n      // 此情况说明 target 在区间 [i, m) 中\n      j = m;\n    } else {\n      // 找到目标元素,返回其索引\n      return m;\n    }\n  }\n  // 未找到目标元素,返回 -1\n  return -1;\n}\n
binary_search.rs
/* 二分查找(左闭右开区间) */\nfn binary_search_lcro(nums: &[i32], target: i32) -> i32 {\n    // 初始化左闭右开区间 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1\n    let mut i = 0;\n    let mut j = nums.len() as i32;\n    // 循环,当搜索区间为空时跳出(当 i = j 时为空)\n    while i < j {\n        let m = i + (j - i) / 2; // 计算中点索引 m\n        if nums[m as usize] < target {\n            // 此情况说明 target 在区间 [m+1, j) 中\n            i = m + 1;\n        } else if nums[m as usize] > target {\n            // 此情况说明 target 在区间 [i, m) 中\n            j = m;\n        } else {\n            // 找到目标元素,返回其索引\n            return m;\n        }\n    }\n    // 未找到目标元素,返回 -1\n    return -1;\n}\n
binary_search.c
/* 二分查找(左闭右开区间) */\nint binarySearchLCRO(int *nums, int len, int target) {\n    // 初始化左闭右开区间 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1\n    int i = 0, j = len;\n    // 循环,当搜索区间为空时跳出(当 i = j 时为空)\n    while (i < j) {\n        int m = i + (j - i) / 2; // 计算中点索引 m\n        if (nums[m] < target)    // 此情况说明 target 在区间 [m+1, j) 中\n            i = m + 1;\n        else if (nums[m] > target) // 此情况说明 target 在区间 [i, m) 中\n            j = m;\n        else // 找到目标元素,返回其索引\n            return m;\n    }\n    // 未找到目标元素,返回 -1\n    return -1;\n}\n
binary_search.kt
/* 二分查找(左闭右开区间) */\nfun binarySearchLCRO(nums: IntArray, target: Int): Int {\n    // 初始化左闭右开区间 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1\n    var i = 0\n    var j = nums.size\n    // 循环,当搜索区间为空时跳出(当 i = j 时为空)\n    while (i < j) {\n        val m = i + (j - i) / 2 // 计算中点索引 m\n        if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j) 中\n            i = m + 1\n        else if (nums[m] > target) // 此情况说明 target 在区间 [i, m) 中\n            j = m\n        else  // 找到目标元素,返回其索引\n            return m\n    }\n    // 未找到目标元素,返回 -1\n    return -1\n}\n
binary_search.rb
### 二分查找(左闭右开区间) ###\ndef binary_search_lcro(nums, target)\n  # 初始化左闭右开区间 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1\n  i, j = 0, nums.length\n\n  # 循环,当搜索区间为空时跳出(当 i = j 时为空)\n  while i < j\n    # 计算中点索引 m\n    m = (i + j) / 2\n\n    if nums[m] < target\n      i = m + 1 # 此情况说明 target 在区间 [m+1, j) 中\n    elsif nums[m] > target\n      j = m - 1 # 此情况说明 target 在区间 [i, m) 中\n    else\n      return m  # 找到目标元素,返回其索引\n    end\n  end\n\n  -1  # 未找到目标元素,返回 -1\nend\n
可视化运行

全屏观看 >

如图 10-3 所示,在两种区间表示下,二分查找算法的初始化、循环条件和缩小区间操作皆有所不同。

由于“双闭区间”表示中的左右边界都被定义为闭区间,因此通过指针 \\(i\\) 和指针 \\(j\\) 缩小区间的操作也是对称的。这样更不容易出错,因此一般建议采用“双闭区间”的写法。

图 10-3   两种区间定义

","path":["第 10 章   搜索","10.1   二分查找"],"tags":[]},{"location":"chapter_searching/binary_search/#1012","level":2,"title":"10.1.2   优点与局限性","text":"

二分查找在时间和空间方面都有较好的性能。

然而,二分查找并非适用于所有情况,主要有以下原因。

","path":["第 10 章   搜索","10.1   二分查找"],"tags":[]},{"location":"chapter_searching/binary_search_edge/","level":1,"title":"10.3   二分查找边界","text":"","path":["第 10 章   搜索","10.3   二分查找边界"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1031","level":2,"title":"10.3.1   查找左边界","text":"

Question

给定一个长度为 \\(n\\) 的有序数组 nums ,其中可能包含重复元素。请返回数组中最左一个元素 target 的索引。若数组中不包含该元素,则返回 \\(-1\\) 。

回忆二分查找插入点的方法,搜索完成后 \\(i\\) 指向最左一个 target ,因此查找插入点本质上是在查找最左一个 target 的索引。

考虑通过查找插入点的函数实现查找左边界。请注意,数组中可能不包含 target ,这种情况可能导致以下两种结果。

当遇到以上两种情况时,直接返回 \\(-1\\) 即可。代码如下所示:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_edge.py
def binary_search_left_edge(nums: list[int], target: int) -> int:\n    \"\"\"二分查找最左一个 target\"\"\"\n    # 等价于查找 target 的插入点\n    i = binary_search_insertion(nums, target)\n    # 未找到 target ,返回 -1\n    if i == len(nums) or nums[i] != target:\n        return -1\n    # 找到 target ,返回索引 i\n    return i\n
binary_search_edge.cpp
/* 二分查找最左一个 target */\nint binarySearchLeftEdge(vector<int> &nums, int target) {\n    // 等价于查找 target 的插入点\n    int i = binarySearchInsertion(nums, target);\n    // 未找到 target ,返回 -1\n    if (i == nums.size() || nums[i] != target) {\n        return -1;\n    }\n    // 找到 target ,返回索引 i\n    return i;\n}\n
binary_search_edge.java
/* 二分查找最左一个 target */\nint binarySearchLeftEdge(int[] nums, int target) {\n    // 等价于查找 target 的插入点\n    int i = binary_search_insertion.binarySearchInsertion(nums, target);\n    // 未找到 target ,返回 -1\n    if (i == nums.length || nums[i] != target) {\n        return -1;\n    }\n    // 找到 target ,返回索引 i\n    return i;\n}\n
binary_search_edge.cs
/* 二分查找最左一个 target */\nint BinarySearchLeftEdge(int[] nums, int target) {\n    // 等价于查找 target 的插入点\n    int i = binary_search_insertion.BinarySearchInsertion(nums, target);\n    // 未找到 target ,返回 -1\n    if (i == nums.Length || nums[i] != target) {\n        return -1;\n    }\n    // 找到 target ,返回索引 i\n    return i;\n}\n
binary_search_edge.go
/* 二分查找最左一个 target */\nfunc binarySearchLeftEdge(nums []int, target int) int {\n    // 等价于查找 target 的插入点\n    i := binarySearchInsertion(nums, target)\n    // 未找到 target ,返回 -1\n    if i == len(nums) || nums[i] != target {\n        return -1\n    }\n    // 找到 target ,返回索引 i\n    return i\n}\n
binary_search_edge.swift
/* 二分查找最左一个 target */\nfunc binarySearchLeftEdge(nums: [Int], target: Int) -> Int {\n    // 等价于查找 target 的插入点\n    let i = binarySearchInsertion(nums: nums, target: target)\n    // 未找到 target ,返回 -1\n    if i == nums.endIndex || nums[i] != target {\n        return -1\n    }\n    // 找到 target ,返回索引 i\n    return i\n}\n
binary_search_edge.js
/* 二分查找最左一个 target */\nfunction binarySearchLeftEdge(nums, target) {\n    // 等价于查找 target 的插入点\n    const i = binarySearchInsertion(nums, target);\n    // 未找到 target ,返回 -1\n    if (i === nums.length || nums[i] !== target) {\n        return -1;\n    }\n    // 找到 target ,返回索引 i\n    return i;\n}\n
binary_search_edge.ts
/* 二分查找最左一个 target */\nfunction binarySearchLeftEdge(nums: Array<number>, target: number): number {\n    // 等价于查找 target 的插入点\n    const i = binarySearchInsertion(nums, target);\n    // 未找到 target ,返回 -1\n    if (i === nums.length || nums[i] !== target) {\n        return -1;\n    }\n    // 找到 target ,返回索引 i\n    return i;\n}\n
binary_search_edge.dart
/* 二分查找最左一个 target */\nint binarySearchLeftEdge(List<int> nums, int target) {\n  // 等价于查找 target 的插入点\n  int i = binarySearchInsertion(nums, target);\n  // 未找到 target ,返回 -1\n  if (i == nums.length || nums[i] != target) {\n    return -1;\n  }\n  // 找到 target ,返回索引 i\n  return i;\n}\n
binary_search_edge.rs
/* 二分查找最左一个 target */\nfn binary_search_left_edge(nums: &[i32], target: i32) -> i32 {\n    // 等价于查找 target 的插入点\n    let i = binary_search_insertion(nums, target);\n    // 未找到 target ,返回 -1\n    if i == nums.len() as i32 || nums[i as usize] != target {\n        return -1;\n    }\n    // 找到 target ,返回索引 i\n    i\n}\n
binary_search_edge.c
/* 二分查找最左一个 target */\nint binarySearchLeftEdge(int *nums, int numSize, int target) {\n    // 等价于查找 target 的插入点\n    int i = binarySearchInsertion(nums, numSize, target);\n    // 未找到 target ,返回 -1\n    if (i == numSize || nums[i] != target) {\n        return -1;\n    }\n    // 找到 target ,返回索引 i\n    return i;\n}\n
binary_search_edge.kt
/* 二分查找最左一个 target */\nfun binarySearchLeftEdge(nums: IntArray, target: Int): Int {\n    // 等价于查找 target 的插入点\n    val i = binarySearchInsertion(nums, target)\n    // 未找到 target ,返回 -1\n    if (i == nums.size || nums[i] != target) {\n        return -1\n    }\n    // 找到 target ,返回索引 i\n    return i\n}\n
binary_search_edge.rb
### 二分查找最左一个 target ###\ndef binary_search_left_edge(nums, target)\n  # 等价于查找 target 的插入点\n  i = binary_search_insertion(nums, target)\n\n  # 未找到 target ,返回 -1\n  return -1 if i == nums.length || nums[i] != target\n\n  i # 找到 target ,返回索引 i\nend\n
可视化运行

全屏观看 >

","path":["第 10 章   搜索","10.3   二分查找边界"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1032","level":2,"title":"10.3.2   查找右边界","text":"

那么如何查找最右一个 target 呢?最直接的方式是修改代码,替换在 nums[m] == target 情况下的指针收缩操作。代码在此省略,有兴趣的读者可以自行实现。

下面我们介绍两种更加取巧的方法。

","path":["第 10 章   搜索","10.3   二分查找边界"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#1","level":3,"title":"1.   复用查找左边界","text":"

实际上,我们可以利用查找最左元素的函数来查找最右元素,具体方法为:将查找最右一个 target 转化为查找最左一个 target + 1

如图 10-7 所示,查找完成后,指针 \\(i\\) 指向最左一个 target + 1(如果存在),而 \\(j\\) 指向最右一个 target ,因此返回 \\(j\\) 即可。

图 10-7   将查找右边界转化为查找左边界

请注意,返回的插入点是 \\(i\\) ,因此需要将其减 \\(1\\) ,从而获得 \\(j\\) :

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_edge.py
def binary_search_right_edge(nums: list[int], target: int) -> int:\n    \"\"\"二分查找最右一个 target\"\"\"\n    # 转化为查找最左一个 target + 1\n    i = binary_search_insertion(nums, target + 1)\n    # j 指向最右一个 target ,i 指向首个大于 target 的元素\n    j = i - 1\n    # 未找到 target ,返回 -1\n    if j == -1 or nums[j] != target:\n        return -1\n    # 找到 target ,返回索引 j\n    return j\n
binary_search_edge.cpp
/* 二分查找最右一个 target */\nint binarySearchRightEdge(vector<int> &nums, int target) {\n    // 转化为查找最左一个 target + 1\n    int i = binarySearchInsertion(nums, target + 1);\n    // j 指向最右一个 target ,i 指向首个大于 target 的元素\n    int j = i - 1;\n    // 未找到 target ,返回 -1\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // 找到 target ,返回索引 j\n    return j;\n}\n
binary_search_edge.java
/* 二分查找最右一个 target */\nint binarySearchRightEdge(int[] nums, int target) {\n    // 转化为查找最左一个 target + 1\n    int i = binary_search_insertion.binarySearchInsertion(nums, target + 1);\n    // j 指向最右一个 target ,i 指向首个大于 target 的元素\n    int j = i - 1;\n    // 未找到 target ,返回 -1\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // 找到 target ,返回索引 j\n    return j;\n}\n
binary_search_edge.cs
/* 二分查找最右一个 target */\nint BinarySearchRightEdge(int[] nums, int target) {\n    // 转化为查找最左一个 target + 1\n    int i = binary_search_insertion.BinarySearchInsertion(nums, target + 1);\n    // j 指向最右一个 target ,i 指向首个大于 target 的元素\n    int j = i - 1;\n    // 未找到 target ,返回 -1\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // 找到 target ,返回索引 j\n    return j;\n}\n
binary_search_edge.go
/* 二分查找最右一个 target */\nfunc binarySearchRightEdge(nums []int, target int) int {\n    // 转化为查找最左一个 target + 1\n    i := binarySearchInsertion(nums, target+1)\n    // j 指向最右一个 target ,i 指向首个大于 target 的元素\n    j := i - 1\n    // 未找到 target ,返回 -1\n    if j == -1 || nums[j] != target {\n        return -1\n    }\n    // 找到 target ,返回索引 j\n    return j\n}\n
binary_search_edge.swift
/* 二分查找最右一个 target */\nfunc binarySearchRightEdge(nums: [Int], target: Int) -> Int {\n    // 转化为查找最左一个 target + 1\n    let i = binarySearchInsertion(nums: nums, target: target + 1)\n    // j 指向最右一个 target ,i 指向首个大于 target 的元素\n    let j = i - 1\n    // 未找到 target ,返回 -1\n    if j == -1 || nums[j] != target {\n        return -1\n    }\n    // 找到 target ,返回索引 j\n    return j\n}\n
binary_search_edge.js
/* 二分查找最右一个 target */\nfunction binarySearchRightEdge(nums, target) {\n    // 转化为查找最左一个 target + 1\n    const i = binarySearchInsertion(nums, target + 1);\n    // j 指向最右一个 target ,i 指向首个大于 target 的元素\n    const j = i - 1;\n    // 未找到 target ,返回 -1\n    if (j === -1 || nums[j] !== target) {\n        return -1;\n    }\n    // 找到 target ,返回索引 j\n    return j;\n}\n
binary_search_edge.ts
/* 二分查找最右一个 target */\nfunction binarySearchRightEdge(nums: Array<number>, target: number): number {\n    // 转化为查找最左一个 target + 1\n    const i = binarySearchInsertion(nums, target + 1);\n    // j 指向最右一个 target ,i 指向首个大于 target 的元素\n    const j = i - 1;\n    // 未找到 target ,返回 -1\n    if (j === -1 || nums[j] !== target) {\n        return -1;\n    }\n    // 找到 target ,返回索引 j\n    return j;\n}\n
binary_search_edge.dart
/* 二分查找最右一个 target */\nint binarySearchRightEdge(List<int> nums, int target) {\n  // 转化为查找最左一个 target + 1\n  int i = binarySearchInsertion(nums, target + 1);\n  // j 指向最右一个 target ,i 指向首个大于 target 的元素\n  int j = i - 1;\n  // 未找到 target ,返回 -1\n  if (j == -1 || nums[j] != target) {\n    return -1;\n  }\n  // 找到 target ,返回索引 j\n  return j;\n}\n
binary_search_edge.rs
/* 二分查找最右一个 target */\nfn binary_search_right_edge(nums: &[i32], target: i32) -> i32 {\n    // 转化为查找最左一个 target + 1\n    let i = binary_search_insertion(nums, target + 1);\n    // j 指向最右一个 target ,i 指向首个大于 target 的元素\n    let j = i - 1;\n    // 未找到 target ,返回 -1\n    if j == -1 || nums[j as usize] != target {\n        return -1;\n    }\n    // 找到 target ,返回索引 j\n    j\n}\n
binary_search_edge.c
/* 二分查找最右一个 target */\nint binarySearchRightEdge(int *nums, int numSize, int target) {\n    // 转化为查找最左一个 target + 1\n    int i = binarySearchInsertion(nums, numSize, target + 1);\n    // j 指向最右一个 target ,i 指向首个大于 target 的元素\n    int j = i - 1;\n    // 未找到 target ,返回 -1\n    if (j == -1 || nums[j] != target) {\n        return -1;\n    }\n    // 找到 target ,返回索引 j\n    return j;\n}\n
binary_search_edge.kt
/* 二分查找最右一个 target */\nfun binarySearchRightEdge(nums: IntArray, target: Int): Int {\n    // 转化为查找最左一个 target + 1\n    val i = binarySearchInsertion(nums, target + 1)\n    // j 指向最右一个 target ,i 指向首个大于 target 的元素\n    val j = i - 1\n    // 未找到 target ,返回 -1\n    if (j == -1 || nums[j] != target) {\n        return -1\n    }\n    // 找到 target ,返回索引 j\n    return j\n}\n
binary_search_edge.rb
### 二分查找最右一个 target ###\ndef binary_search_right_edge(nums, target)\n  # 转化为查找最左一个 target + 1\n  i = binary_search_insertion(nums, target + 1)\n\n  # j 指向最右一个 target ,i 指向首个大于 target 的元素\n  j = i - 1\n\n  # 未找到 target ,返回 -1\n  return -1 if j == -1 || nums[j] != target\n\n  j # 找到 target ,返回索引 j\nend\n
可视化运行

全屏观看 >

","path":["第 10 章   搜索","10.3   二分查找边界"],"tags":[]},{"location":"chapter_searching/binary_search_edge/#2","level":3,"title":"2.   转化为查找元素","text":"

我们知道,当数组不包含 target 时,最终 \\(i\\) 和 \\(j\\) 会分别指向首个大于、小于 target 的元素。

因此,如图 10-8 所示,我们可以构造一个数组中不存在的元素,用于查找左右边界。

图 10-8   将查找边界转化为查找元素

代码在此省略,以下两点值得注意。

","path":["第 10 章   搜索","10.3   二分查找边界"],"tags":[]},{"location":"chapter_searching/binary_search_insertion/","level":1,"title":"10.2   二分查找插入点","text":"

二分查找不仅可用于搜索目标元素,还可用于解决许多变种问题,比如搜索目标元素的插入位置。

","path":["第 10 章   搜索","10.2   二分查找插入点"],"tags":[]},{"location":"chapter_searching/binary_search_insertion/#1021","level":2,"title":"10.2.1   无重复元素的情况","text":"

Question

给定一个长度为 \\(n\\) 的有序数组 nums 和一个元素 target ,数组不存在重复元素。现将 target 插入数组 nums 中,并保持其有序性。若数组中已存在元素 target ,则插入到其左方。请返回插入后 target 在数组中的索引。示例如图 10-4 所示。

图 10-4   二分查找插入点示例数据

如果想复用上一节的二分查找代码,则需要回答以下两个问题。

问题一:当数组中包含 target 时,插入点的索引是否是该元素的索引?

题目要求将 target 插入到相等元素的左边,这意味着新插入的 target 替换了原来 target 的位置。也就是说,当数组包含 target 时,插入点的索引就是该 target 的索引。

问题二:当数组中不存在 target 时,插入点是哪个元素的索引?

进一步思考二分查找过程:当 nums[m] < target 时 \\(i\\) 移动,这意味着指针 \\(i\\) 在向大于等于 target 的元素靠近。同理,指针 \\(j\\) 始终在向小于等于 target 的元素靠近。

因此二分结束时一定有:\\(i\\) 指向首个大于 target 的元素,\\(j\\) 指向首个小于 target 的元素。易得当数组不包含 target 时,插入索引为 \\(i\\) 。代码如下所示:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_insertion.py
def binary_search_insertion_simple(nums: list[int], target: int) -> int:\n    \"\"\"二分查找插入点(无重复元素)\"\"\"\n    i, j = 0, len(nums) - 1  # 初始化双闭区间 [0, n-1]\n    while i <= j:\n        m = (i + j) // 2  # 计算中点索引 m\n        if nums[m] < target:\n            i = m + 1  # target 在区间 [m+1, j] 中\n        elif nums[m] > target:\n            j = m - 1  # target 在区间 [i, m-1] 中\n        else:\n            return m  # 找到 target ,返回插入点 m\n    # 未找到 target ,返回插入点 i\n    return i\n
binary_search_insertion.cpp
/* 二分查找插入点(无重复元素) */\nint binarySearchInsertionSimple(vector<int> &nums, int target) {\n    int i = 0, j = nums.size() - 1; // 初始化双闭区间 [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // 计算中点索引 m\n        if (nums[m] < target) {\n            i = m + 1; // target 在区间 [m+1, j] 中\n        } else if (nums[m] > target) {\n            j = m - 1; // target 在区间 [i, m-1] 中\n        } else {\n            return m; // 找到 target ,返回插入点 m\n        }\n    }\n    // 未找到 target ,返回插入点 i\n    return i;\n}\n
binary_search_insertion.java
/* 二分查找插入点(无重复元素) */\nint binarySearchInsertionSimple(int[] nums, int target) {\n    int i = 0, j = nums.length - 1; // 初始化双闭区间 [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // 计算中点索引 m\n        if (nums[m] < target) {\n            i = m + 1; // target 在区间 [m+1, j] 中\n        } else if (nums[m] > target) {\n            j = m - 1; // target 在区间 [i, m-1] 中\n        } else {\n            return m; // 找到 target ,返回插入点 m\n        }\n    }\n    // 未找到 target ,返回插入点 i\n    return i;\n}\n
binary_search_insertion.cs
/* 二分查找插入点(无重复元素) */\nint BinarySearchInsertionSimple(int[] nums, int target) {\n    int i = 0, j = nums.Length - 1; // 初始化双闭区间 [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // 计算中点索引 m\n        if (nums[m] < target) {\n            i = m + 1; // target 在区间 [m+1, j] 中\n        } else if (nums[m] > target) {\n            j = m - 1; // target 在区间 [i, m-1] 中\n        } else {\n            return m; // 找到 target ,返回插入点 m\n        }\n    }\n    // 未找到 target ,返回插入点 i\n    return i;\n}\n
binary_search_insertion.go
/* 二分查找插入点(无重复元素) */\nfunc binarySearchInsertionSimple(nums []int, target int) int {\n    // 初始化双闭区间 [0, n-1]\n    i, j := 0, len(nums)-1\n    for i <= j {\n        // 计算中点索引 m\n        m := i + (j-i)/2\n        if nums[m] < target {\n            // target 在区间 [m+1, j] 中\n            i = m + 1\n        } else if nums[m] > target {\n            // target 在区间 [i, m-1] 中\n            j = m - 1\n        } else {\n            // 找到 target ,返回插入点 m\n            return m\n        }\n    }\n    // 未找到 target ,返回插入点 i\n    return i\n}\n
binary_search_insertion.swift
/* 二分查找插入点(无重复元素) */\nfunc binarySearchInsertionSimple(nums: [Int], target: Int) -> Int {\n    // 初始化双闭区间 [0, n-1]\n    var i = nums.startIndex\n    var j = nums.endIndex - 1\n    while i <= j {\n        let m = i + (j - i) / 2 // 计算中点索引 m\n        if nums[m] < target {\n            i = m + 1 // target 在区间 [m+1, j] 中\n        } else if nums[m] > target {\n            j = m - 1 // target 在区间 [i, m-1] 中\n        } else {\n            return m // 找到 target ,返回插入点 m\n        }\n    }\n    // 未找到 target ,返回插入点 i\n    return i\n}\n
binary_search_insertion.js
/* 二分查找插入点(无重复元素) */\nfunction binarySearchInsertionSimple(nums, target) {\n    let i = 0,\n        j = nums.length - 1; // 初始化双闭区间 [0, n-1]\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // 计算中点索引 m, 使用 Math.floor() 向下取整\n        if (nums[m] < target) {\n            i = m + 1; // target 在区间 [m+1, j] 中\n        } else if (nums[m] > target) {\n            j = m - 1; // target 在区间 [i, m-1] 中\n        } else {\n            return m; // 找到 target ,返回插入点 m\n        }\n    }\n    // 未找到 target ,返回插入点 i\n    return i;\n}\n
binary_search_insertion.ts
/* 二分查找插入点(无重复元素) */\nfunction binarySearchInsertionSimple(\n    nums: Array<number>,\n    target: number\n): number {\n    let i = 0,\n        j = nums.length - 1; // 初始化双闭区间 [0, n-1]\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // 计算中点索引 m, 使用 Math.floor() 向下取整\n        if (nums[m] < target) {\n            i = m + 1; // target 在区间 [m+1, j] 中\n        } else if (nums[m] > target) {\n            j = m - 1; // target 在区间 [i, m-1] 中\n        } else {\n            return m; // 找到 target ,返回插入点 m\n        }\n    }\n    // 未找到 target ,返回插入点 i\n    return i;\n}\n
binary_search_insertion.dart
/* 二分查找插入点(无重复元素) */\nint binarySearchInsertionSimple(List<int> nums, int target) {\n  int i = 0, j = nums.length - 1; // 初始化双闭区间 [0, n-1]\n  while (i <= j) {\n    int m = i + (j - i) ~/ 2; // 计算中点索引 m\n    if (nums[m] < target) {\n      i = m + 1; // target 在区间 [m+1, j] 中\n    } else if (nums[m] > target) {\n      j = m - 1; // target 在区间 [i, m-1] 中\n    } else {\n      return m; // 找到 target ,返回插入点 m\n    }\n  }\n  // 未找到 target ,返回插入点 i\n  return i;\n}\n
binary_search_insertion.rs
/* 二分查找插入点(无重复元素) */\nfn binary_search_insertion_simple(nums: &[i32], target: i32) -> i32 {\n    let (mut i, mut j) = (0, nums.len() as i32 - 1); // 初始化双闭区间 [0, n-1]\n    while i <= j {\n        let m = i + (j - i) / 2; // 计算中点索引 m\n        if nums[m as usize] < target {\n            i = m + 1; // target 在区间 [m+1, j] 中\n        } else if nums[m as usize] > target {\n            j = m - 1; // target 在区间 [i, m-1] 中\n        } else {\n            return m;\n        }\n    }\n    // 未找到 target ,返回插入点 i\n    i\n}\n
binary_search_insertion.c
/* 二分查找插入点(无重复元素) */\nint binarySearchInsertionSimple(int *nums, int numSize, int target) {\n    int i = 0, j = numSize - 1; // 初始化双闭区间 [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // 计算中点索引 m\n        if (nums[m] < target) {\n            i = m + 1; // target 在区间 [m+1, j] 中\n        } else if (nums[m] > target) {\n            j = m - 1; // target 在区间 [i, m-1] 中\n        } else {\n            return m; // 找到 target ,返回插入点 m\n        }\n    }\n    // 未找到 target ,返回插入点 i\n    return i;\n}\n
binary_search_insertion.kt
/* 二分查找插入点(无重复元素) */\nfun binarySearchInsertionSimple(nums: IntArray, target: Int): Int {\n    var i = 0\n    var j = nums.size - 1 // 初始化双闭区间 [0, n-1]\n    while (i <= j) {\n        val m = i + (j - i) / 2 // 计算中点索引 m\n        if (nums[m] < target) {\n            i = m + 1 // target 在区间 [m+1, j] 中\n        } else if (nums[m] > target) {\n            j = m - 1 // target 在区间 [i, m-1] 中\n        } else {\n            return m // 找到 target ,返回插入点 m\n        }\n    }\n    // 未找到 target ,返回插入点 i\n    return i\n}\n
binary_search_insertion.rb
### 二分查找插入点(无重复元素) ###\ndef binary_search_insertion_simple(nums, target)\n  # 初始化双闭区间 [0, n-1]\n  i, j = 0, nums.length - 1\n\n  while i <= j\n    # 计算中点索引 m\n    m = (i + j) / 2\n\n    if nums[m] < target\n      i = m + 1 # target 在区间 [m+1, j] 中\n    elsif nums[m] > target\n      j = m - 1 # target 在区间 [i, m-1] 中\n    else\n      return m  # 找到 target ,返回插入点 m\n    end\n  end\n\n  i # 未找到 target ,返回插入点 i\nend\n
可视化运行

全屏观看 >

","path":["第 10 章   搜索","10.2   二分查找插入点"],"tags":[]},{"location":"chapter_searching/binary_search_insertion/#1022","level":2,"title":"10.2.2   存在重复元素的情况","text":"

Question

在上一题的基础上,规定数组可能包含重复元素,其余不变。

假设数组中存在多个 target ,则普通二分查找只能返回其中一个 target 的索引,而无法确定该元素的左边和右边还有多少 target

题目要求将目标元素插入到最左边,所以我们需要查找数组中最左一个 target 的索引。初步考虑通过图 10-5 所示的步骤实现。

  1. 执行二分查找,得到任意一个 target 的索引,记为 \\(k\\) 。
  2. 从索引 \\(k\\) 开始,向左进行线性遍历,当找到最左边的 target 时返回。

图 10-5   线性查找重复元素的插入点

此方法虽然可用,但其包含线性查找,因此时间复杂度为 \\(O(n)\\) 。当数组中存在很多重复的 target 时,该方法效率很低。

现考虑拓展二分查找代码。如图 10-6 所示,整体流程保持不变,每轮先计算中点索引 \\(m\\) ,再判断 targetnums[m] 的大小关系,分为以下几种情况。

循环完成后,\\(i\\) 指向最左边的 target ,\\(j\\) 指向首个小于 target 的元素,因此索引 \\(i\\) 就是插入点。

<1><2><3><4><5><6><7><8>

图 10-6   二分查找重复元素的插入点的步骤

观察以下代码,判断分支 nums[m] > targetnums[m] == target 的操作相同,因此两者可以合并。

即便如此,我们仍然可以将判断条件保持展开,因为其逻辑更加清晰、可读性更好。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_insertion.py
def binary_search_insertion(nums: list[int], target: int) -> int:\n    \"\"\"二分查找插入点(存在重复元素)\"\"\"\n    i, j = 0, len(nums) - 1  # 初始化双闭区间 [0, n-1]\n    while i <= j:\n        m = (i + j) // 2  # 计算中点索引 m\n        if nums[m] < target:\n            i = m + 1  # target 在区间 [m+1, j] 中\n        elif nums[m] > target:\n            j = m - 1  # target 在区间 [i, m-1] 中\n        else:\n            j = m - 1  # 首个小于 target 的元素在区间 [i, m-1] 中\n    # 返回插入点 i\n    return i\n
binary_search_insertion.cpp
/* 二分查找插入点(存在重复元素) */\nint binarySearchInsertion(vector<int> &nums, int target) {\n    int i = 0, j = nums.size() - 1; // 初始化双闭区间 [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // 计算中点索引 m\n        if (nums[m] < target) {\n            i = m + 1; // target 在区间 [m+1, j] 中\n        } else if (nums[m] > target) {\n            j = m - 1; // target 在区间 [i, m-1] 中\n        } else {\n            j = m - 1; // 首个小于 target 的元素在区间 [i, m-1] 中\n        }\n    }\n    // 返回插入点 i\n    return i;\n}\n
binary_search_insertion.java
/* 二分查找插入点(存在重复元素) */\nint binarySearchInsertion(int[] nums, int target) {\n    int i = 0, j = nums.length - 1; // 初始化双闭区间 [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // 计算中点索引 m\n        if (nums[m] < target) {\n            i = m + 1; // target 在区间 [m+1, j] 中\n        } else if (nums[m] > target) {\n            j = m - 1; // target 在区间 [i, m-1] 中\n        } else {\n            j = m - 1; // 首个小于 target 的元素在区间 [i, m-1] 中\n        }\n    }\n    // 返回插入点 i\n    return i;\n}\n
binary_search_insertion.cs
/* 二分查找插入点(存在重复元素) */\nint BinarySearchInsertion(int[] nums, int target) {\n    int i = 0, j = nums.Length - 1; // 初始化双闭区间 [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // 计算中点索引 m\n        if (nums[m] < target) {\n            i = m + 1; // target 在区间 [m+1, j] 中\n        } else if (nums[m] > target) {\n            j = m - 1; // target 在区间 [i, m-1] 中\n        } else {\n            j = m - 1; // 首个小于 target 的元素在区间 [i, m-1] 中\n        }\n    }\n    // 返回插入点 i\n    return i;\n}\n
binary_search_insertion.go
/* 二分查找插入点(存在重复元素) */\nfunc binarySearchInsertion(nums []int, target int) int {\n    // 初始化双闭区间 [0, n-1]\n    i, j := 0, len(nums)-1\n    for i <= j {\n        // 计算中点索引 m\n        m := i + (j-i)/2\n        if nums[m] < target {\n            // target 在区间 [m+1, j] 中\n            i = m + 1\n        } else if nums[m] > target {\n            // target 在区间 [i, m-1] 中\n            j = m - 1\n        } else {\n            // 首个小于 target 的元素在区间 [i, m-1] 中\n            j = m - 1\n        }\n    }\n    // 返回插入点 i\n    return i\n}\n
binary_search_insertion.swift
/* 二分查找插入点(存在重复元素) */\nfunc binarySearchInsertion(nums: [Int], target: Int) -> Int {\n    // 初始化双闭区间 [0, n-1]\n    var i = nums.startIndex\n    var j = nums.endIndex - 1\n    while i <= j {\n        let m = i + (j - i) / 2 // 计算中点索引 m\n        if nums[m] < target {\n            i = m + 1 // target 在区间 [m+1, j] 中\n        } else if nums[m] > target {\n            j = m - 1 // target 在区间 [i, m-1] 中\n        } else {\n            j = m - 1 // 首个小于 target 的元素在区间 [i, m-1] 中\n        }\n    }\n    // 返回插入点 i\n    return i\n}\n
binary_search_insertion.js
/* 二分查找插入点(存在重复元素) */\nfunction binarySearchInsertion(nums, target) {\n    let i = 0,\n        j = nums.length - 1; // 初始化双闭区间 [0, n-1]\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // 计算中点索引 m, 使用 Math.floor() 向下取整\n        if (nums[m] < target) {\n            i = m + 1; // target 在区间 [m+1, j] 中\n        } else if (nums[m] > target) {\n            j = m - 1; // target 在区间 [i, m-1] 中\n        } else {\n            j = m - 1; // 首个小于 target 的元素在区间 [i, m-1] 中\n        }\n    }\n    // 返回插入点 i\n    return i;\n}\n
binary_search_insertion.ts
/* 二分查找插入点(存在重复元素) */\nfunction binarySearchInsertion(nums: Array<number>, target: number): number {\n    let i = 0,\n        j = nums.length - 1; // 初始化双闭区间 [0, n-1]\n    while (i <= j) {\n        const m = Math.floor(i + (j - i) / 2); // 计算中点索引 m, 使用 Math.floor() 向下取整\n        if (nums[m] < target) {\n            i = m + 1; // target 在区间 [m+1, j] 中\n        } else if (nums[m] > target) {\n            j = m - 1; // target 在区间 [i, m-1] 中\n        } else {\n            j = m - 1; // 首个小于 target 的元素在区间 [i, m-1] 中\n        }\n    }\n    // 返回插入点 i\n    return i;\n}\n
binary_search_insertion.dart
/* 二分查找插入点(存在重复元素) */\nint binarySearchInsertion(List<int> nums, int target) {\n  int i = 0, j = nums.length - 1; // 初始化双闭区间 [0, n-1]\n  while (i <= j) {\n    int m = i + (j - i) ~/ 2; // 计算中点索引 m\n    if (nums[m] < target) {\n      i = m + 1; // target 在区间 [m+1, j] 中\n    } else if (nums[m] > target) {\n      j = m - 1; // target 在区间 [i, m-1] 中\n    } else {\n      j = m - 1; // 首个小于 target 的元素在区间 [i, m-1] 中\n    }\n  }\n  // 返回插入点 i\n  return i;\n}\n
binary_search_insertion.rs
/* 二分查找插入点(存在重复元素) */\npub fn binary_search_insertion(nums: &[i32], target: i32) -> i32 {\n    let (mut i, mut j) = (0, nums.len() as i32 - 1); // 初始化双闭区间 [0, n-1]\n    while i <= j {\n        let m = i + (j - i) / 2; // 计算中点索引 m\n        if nums[m as usize] < target {\n            i = m + 1; // target 在区间 [m+1, j] 中\n        } else if nums[m as usize] > target {\n            j = m - 1; // target 在区间 [i, m-1] 中\n        } else {\n            j = m - 1; // 首个小于 target 的元素在区间 [i, m-1] 中\n        }\n    }\n    // 返回插入点 i\n    i\n}\n
binary_search_insertion.c
/* 二分查找插入点(存在重复元素) */\nint binarySearchInsertion(int *nums, int numSize, int target) {\n    int i = 0, j = numSize - 1; // 初始化双闭区间 [0, n-1]\n    while (i <= j) {\n        int m = i + (j - i) / 2; // 计算中点索引 m\n        if (nums[m] < target) {\n            i = m + 1; // target 在区间 [m+1, j] 中\n        } else if (nums[m] > target) {\n            j = m - 1; // target 在区间 [i, m-1] 中\n        } else {\n            j = m - 1; // 首个小于 target 的元素在区间 [i, m-1] 中\n        }\n    }\n    // 返回插入点 i\n    return i;\n}\n
binary_search_insertion.kt
/* 二分查找插入点(存在重复元素) */\nfun binarySearchInsertion(nums: IntArray, target: Int): Int {\n    var i = 0\n    var j = nums.size - 1 // 初始化双闭区间 [0, n-1]\n    while (i <= j) {\n        val m = i + (j - i) / 2 // 计算中点索引 m\n        if (nums[m] < target) {\n            i = m + 1 // target 在区间 [m+1, j] 中\n        } else if (nums[m] > target) {\n            j = m - 1 // target 在区间 [i, m-1] 中\n        } else {\n            j = m - 1 // 首个小于 target 的元素在区间 [i, m-1] 中\n        }\n    }\n    // 返回插入点 i\n    return i\n}\n
binary_search_insertion.rb
### 二分查找插入点(存在重复元素) ###\ndef binary_search_insertion(nums, target)\n  # 初始化双闭区间 [0, n-1]\n  i, j = 0, nums.length - 1\n\n  while i <= j\n    # 计算中点索引 m\n    m = (i + j) / 2\n\n    if nums[m] < target\n      i = m + 1 # target 在区间 [m+1, j] 中\n    elsif nums[m] > target\n      j = m - 1 # target 在区间 [i, m-1] 中\n    else\n      j = m - 1 # 首个小于 target 的元素在区间 [i, m-1] 中\n    end\n  end\n\n  i # 返回插入点 i\nend\n
可视化运行

全屏观看 >

Tip

本节的代码都是“双闭区间”写法。有兴趣的读者可以自行实现“左闭右开”写法。

总的来看,二分查找无非就是给指针 \\(i\\) 和 \\(j\\) 分别设定搜索目标,目标可能是一个具体的元素(例如 target ),也可能是一个元素范围(例如小于 target 的元素)。

在不断的循环二分中,指针 \\(i\\) 和 \\(j\\) 都逐渐逼近预先设定的目标。最终,它们或是成功找到答案,或是越过边界后停止。

","path":["第 10 章   搜索","10.2   二分查找插入点"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/","level":1,"title":"10.4   哈希优化策略","text":"

在算法题中,我们常通过将线性查找替换为哈希查找来降低算法的时间复杂度。我们借助一个算法题来加深理解。

Question

给定一个整数数组 nums 和一个目标元素 target ,请在数组中搜索“和”为 target 的两个元素,并返回它们的数组索引。返回任意一个解即可。

","path":["第 10 章   搜索","10.4   哈希优化策略"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/#1041","level":2,"title":"10.4.1   线性查找:以时间换空间","text":"

考虑直接遍历所有可能的组合。如图 10-9 所示,我们开启一个两层循环,在每轮中判断两个整数的和是否为 target ,若是,则返回它们的索引。

图 10-9   线性查找求解两数之和

代码如下所示:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby two_sum.py
def two_sum_brute_force(nums: list[int], target: int) -> list[int]:\n    \"\"\"方法一:暴力枚举\"\"\"\n    # 两层循环,时间复杂度为 O(n^2)\n    for i in range(len(nums) - 1):\n        for j in range(i + 1, len(nums)):\n            if nums[i] + nums[j] == target:\n                return [i, j]\n    return []\n
two_sum.cpp
/* 方法一:暴力枚举 */\nvector<int> twoSumBruteForce(vector<int> &nums, int target) {\n    int size = nums.size();\n    // 两层循环,时间复杂度为 O(n^2)\n    for (int i = 0; i < size - 1; i++) {\n        for (int j = i + 1; j < size; j++) {\n            if (nums[i] + nums[j] == target)\n                return {i, j};\n        }\n    }\n    return {};\n}\n
two_sum.java
/* 方法一:暴力枚举 */\nint[] twoSumBruteForce(int[] nums, int target) {\n    int size = nums.length;\n    // 两层循环,时间复杂度为 O(n^2)\n    for (int i = 0; i < size - 1; i++) {\n        for (int j = i + 1; j < size; j++) {\n            if (nums[i] + nums[j] == target)\n                return new int[] { i, j };\n        }\n    }\n    return new int[0];\n}\n
two_sum.cs
/* 方法一:暴力枚举 */\nint[] TwoSumBruteForce(int[] nums, int target) {\n    int size = nums.Length;\n    // 两层循环,时间复杂度为 O(n^2)\n    for (int i = 0; i < size - 1; i++) {\n        for (int j = i + 1; j < size; j++) {\n            if (nums[i] + nums[j] == target)\n                return [i, j];\n        }\n    }\n    return [];\n}\n
two_sum.go
/* 方法一:暴力枚举 */\nfunc twoSumBruteForce(nums []int, target int) []int {\n    size := len(nums)\n    // 两层循环,时间复杂度为 O(n^2)\n    for i := 0; i < size-1; i++ {\n        for j := i + 1; j < size; j++ {\n            if nums[i]+nums[j] == target {\n                return []int{i, j}\n            }\n        }\n    }\n    return nil\n}\n
two_sum.swift
/* 方法一:暴力枚举 */\nfunc twoSumBruteForce(nums: [Int], target: Int) -> [Int] {\n    // 两层循环,时间复杂度为 O(n^2)\n    for i in nums.indices.dropLast() {\n        for j in nums.indices.dropFirst(i + 1) {\n            if nums[i] + nums[j] == target {\n                return [i, j]\n            }\n        }\n    }\n    return [0]\n}\n
two_sum.js
/* 方法一:暴力枚举 */\nfunction twoSumBruteForce(nums, target) {\n    const n = nums.length;\n    // 两层循环,时间复杂度为 O(n^2)\n    for (let i = 0; i < n; i++) {\n        for (let j = i + 1; j < n; j++) {\n            if (nums[i] + nums[j] === target) {\n                return [i, j];\n            }\n        }\n    }\n    return [];\n}\n
two_sum.ts
/* 方法一:暴力枚举 */\nfunction twoSumBruteForce(nums: number[], target: number): number[] {\n    const n = nums.length;\n    // 两层循环,时间复杂度为 O(n^2)\n    for (let i = 0; i < n; i++) {\n        for (let j = i + 1; j < n; j++) {\n            if (nums[i] + nums[j] === target) {\n                return [i, j];\n            }\n        }\n    }\n    return [];\n}\n
two_sum.dart
/* 方法一: 暴力枚举 */\nList<int> twoSumBruteForce(List<int> nums, int target) {\n  int size = nums.length;\n  // 两层循环,时间复杂度为 O(n^2)\n  for (var i = 0; i < size - 1; i++) {\n    for (var j = i + 1; j < size; j++) {\n      if (nums[i] + nums[j] == target) return [i, j];\n    }\n  }\n  return [0];\n}\n
two_sum.rs
/* 方法一:暴力枚举 */\npub fn two_sum_brute_force(nums: &Vec<i32>, target: i32) -> Option<Vec<i32>> {\n    let size = nums.len();\n    // 两层循环,时间复杂度为 O(n^2)\n    for i in 0..size - 1 {\n        for j in i + 1..size {\n            if nums[i] + nums[j] == target {\n                return Some(vec![i as i32, j as i32]);\n            }\n        }\n    }\n    None\n}\n
two_sum.c
/* 方法一:暴力枚举 */\nint *twoSumBruteForce(int *nums, int numsSize, int target, int *returnSize) {\n    for (int i = 0; i < numsSize; ++i) {\n        for (int j = i + 1; j < numsSize; ++j) {\n            if (nums[i] + nums[j] == target) {\n                int *res = malloc(sizeof(int) * 2);\n                res[0] = i, res[1] = j;\n                *returnSize = 2;\n                return res;\n            }\n        }\n    }\n    *returnSize = 0;\n    return NULL;\n}\n
two_sum.kt
/* 方法一:暴力枚举 */\nfun twoSumBruteForce(nums: IntArray, target: Int): IntArray {\n    val size = nums.size\n    // 两层循环,时间复杂度为 O(n^2)\n    for (i in 0..<size - 1) {\n        for (j in i + 1..<size) {\n            if (nums[i] + nums[j] == target) return intArrayOf(i, j)\n        }\n    }\n    return IntArray(0)\n}\n
two_sum.rb
### 方法一:暴力枚举 ###\ndef two_sum_brute_force(nums, target)\n  # 两层循环,时间复杂度为 O(n^2)\n  for i in 0...(nums.length - 1)\n    for j in (i + 1)...nums.length\n      return [i, j] if nums[i] + nums[j] == target\n    end\n  end\n\n  []\nend\n
可视化运行

全屏观看 >

此方法的时间复杂度为 \\(O(n^2)\\) ,空间复杂度为 \\(O(1)\\) ,在大数据量下非常耗时。

","path":["第 10 章   搜索","10.4   哈希优化策略"],"tags":[]},{"location":"chapter_searching/replace_linear_by_hashing/#1042","level":2,"title":"10.4.2   哈希查找:以空间换时间","text":"

考虑借助一个哈希表,键值对分别为数组元素和元素索引。循环遍历数组,每轮执行图 10-10 所示的步骤。

  1. 判断数字 target - nums[i] 是否在哈希表中,若是,则直接返回这两个元素的索引。
  2. 将键值对 nums[i] 和索引 i 添加进哈希表。
<1><2><3>

图 10-10   辅助哈希表求解两数之和

实现代码如下所示,仅需单层循环即可:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby two_sum.py
def two_sum_hash_table(nums: list[int], target: int) -> list[int]:\n    \"\"\"方法二:辅助哈希表\"\"\"\n    # 辅助哈希表,空间复杂度为 O(n)\n    dic = {}\n    # 单层循环,时间复杂度为 O(n)\n    for i in range(len(nums)):\n        if target - nums[i] in dic:\n            return [dic[target - nums[i]], i]\n        dic[nums[i]] = i\n    return []\n
two_sum.cpp
/* 方法二:辅助哈希表 */\nvector<int> twoSumHashTable(vector<int> &nums, int target) {\n    int size = nums.size();\n    // 辅助哈希表,空间复杂度为 O(n)\n    unordered_map<int, int> dic;\n    // 单层循环,时间复杂度为 O(n)\n    for (int i = 0; i < size; i++) {\n        if (dic.find(target - nums[i]) != dic.end()) {\n            return {dic[target - nums[i]], i};\n        }\n        dic.emplace(nums[i], i);\n    }\n    return {};\n}\n
two_sum.java
/* 方法二:辅助哈希表 */\nint[] twoSumHashTable(int[] nums, int target) {\n    int size = nums.length;\n    // 辅助哈希表,空间复杂度为 O(n)\n    Map<Integer, Integer> dic = new HashMap<>();\n    // 单层循环,时间复杂度为 O(n)\n    for (int i = 0; i < size; i++) {\n        if (dic.containsKey(target - nums[i])) {\n            return new int[] { dic.get(target - nums[i]), i };\n        }\n        dic.put(nums[i], i);\n    }\n    return new int[0];\n}\n
two_sum.cs
/* 方法二:辅助哈希表 */\nint[] TwoSumHashTable(int[] nums, int target) {\n    int size = nums.Length;\n    // 辅助哈希表,空间复杂度为 O(n)\n    Dictionary<int, int> dic = [];\n    // 单层循环,时间复杂度为 O(n)\n    for (int i = 0; i < size; i++) {\n        if (dic.ContainsKey(target - nums[i])) {\n            return [dic[target - nums[i]], i];\n        }\n        dic.Add(nums[i], i);\n    }\n    return [];\n}\n
two_sum.go
/* 方法二:辅助哈希表 */\nfunc twoSumHashTable(nums []int, target int) []int {\n    // 辅助哈希表,空间复杂度为 O(n)\n    hashTable := map[int]int{}\n    // 单层循环,时间复杂度为 O(n)\n    for idx, val := range nums {\n        if preIdx, ok := hashTable[target-val]; ok {\n            return []int{preIdx, idx}\n        }\n        hashTable[val] = idx\n    }\n    return nil\n}\n
two_sum.swift
/* 方法二:辅助哈希表 */\nfunc twoSumHashTable(nums: [Int], target: Int) -> [Int] {\n    // 辅助哈希表,空间复杂度为 O(n)\n    var dic: [Int: Int] = [:]\n    // 单层循环,时间复杂度为 O(n)\n    for i in nums.indices {\n        if let j = dic[target - nums[i]] {\n            return [j, i]\n        }\n        dic[nums[i]] = i\n    }\n    return [0]\n}\n
two_sum.js
/* 方法二:辅助哈希表 */\nfunction twoSumHashTable(nums, target) {\n    // 辅助哈希表,空间复杂度为 O(n)\n    let m = {};\n    // 单层循环,时间复杂度为 O(n)\n    for (let i = 0; i < nums.length; i++) {\n        if (m[target - nums[i]] !== undefined) {\n            return [m[target - nums[i]], i];\n        } else {\n            m[nums[i]] = i;\n        }\n    }\n    return [];\n}\n
two_sum.ts
/* 方法二:辅助哈希表 */\nfunction twoSumHashTable(nums: number[], target: number): number[] {\n    // 辅助哈希表,空间复杂度为 O(n)\n    let m: Map<number, number> = new Map();\n    // 单层循环,时间复杂度为 O(n)\n    for (let i = 0; i < nums.length; i++) {\n        let index = m.get(target - nums[i]);\n        if (index !== undefined) {\n            return [index, i];\n        } else {\n            m.set(nums[i], i);\n        }\n    }\n    return [];\n}\n
two_sum.dart
/* 方法二: 辅助哈希表 */\nList<int> twoSumHashTable(List<int> nums, int target) {\n  int size = nums.length;\n  // 辅助哈希表,空间复杂度为 O(n)\n  Map<int, int> dic = HashMap();\n  // 单层循环,时间复杂度为 O(n)\n  for (var i = 0; i < size; i++) {\n    if (dic.containsKey(target - nums[i])) {\n      return [dic[target - nums[i]]!, i];\n    }\n    dic.putIfAbsent(nums[i], () => i);\n  }\n  return [0];\n}\n
two_sum.rs
/* 方法二:辅助哈希表 */\npub fn two_sum_hash_table(nums: &Vec<i32>, target: i32) -> Option<Vec<i32>> {\n    // 辅助哈希表,空间复杂度为 O(n)\n    let mut dic = HashMap::new();\n    // 单层循环,时间复杂度为 O(n)\n    for (i, num) in nums.iter().enumerate() {\n        match dic.get(&(target - num)) {\n            Some(v) => return Some(vec![*v as i32, i as i32]),\n            None => dic.insert(num, i as i32),\n        };\n    }\n    None\n}\n
two_sum.c
/* 哈希表 */\ntypedef struct {\n    int key;\n    int val;\n    UT_hash_handle hh; // 基于 uthash.h 实现\n} HashTable;\n\n/* 哈希表查询 */\nHashTable *find(HashTable *h, int key) {\n    HashTable *tmp;\n    HASH_FIND_INT(h, &key, tmp);\n    return tmp;\n}\n\n/* 哈希表元素插入 */\nvoid insert(HashTable **h, int key, int val) {\n    HashTable *t = find(*h, key);\n    if (t == NULL) {\n        HashTable *tmp = malloc(sizeof(HashTable));\n        tmp->key = key, tmp->val = val;\n        HASH_ADD_INT(*h, key, tmp);\n    } else {\n        t->val = val;\n    }\n}\n\n/* 方法二:辅助哈希表 */\nint *twoSumHashTable(int *nums, int numsSize, int target, int *returnSize) {\n    HashTable *hashtable = NULL;\n    for (int i = 0; i < numsSize; i++) {\n        HashTable *t = find(hashtable, target - nums[i]);\n        if (t != NULL) {\n            int *res = malloc(sizeof(int) * 2);\n            res[0] = t->val, res[1] = i;\n            *returnSize = 2;\n            return res;\n        }\n        insert(&hashtable, nums[i], i);\n    }\n    *returnSize = 0;\n    return NULL;\n}\n
two_sum.kt
/* 方法二:辅助哈希表 */\nfun twoSumHashTable(nums: IntArray, target: Int): IntArray {\n    val size = nums.size\n    // 辅助哈希表,空间复杂度为 O(n)\n    val dic = HashMap<Int, Int>()\n    // 单层循环,时间复杂度为 O(n)\n    for (i in 0..<size) {\n        if (dic.containsKey(target - nums[i])) {\n            return intArrayOf(dic[target - nums[i]]!!, i)\n        }\n        dic[nums[i]] = i\n    }\n    return IntArray(0)\n}\n
two_sum.rb
### 方法二:辅助哈希表 ###\ndef two_sum_hash_table(nums, target)\n  # 辅助哈希表,空间复杂度为 O(n)\n  dic = {}\n  # 单层循环,时间复杂度为 O(n)\n  for i in 0...nums.length\n    return [dic[target - nums[i]], i] if dic.has_key?(target - nums[i])\n\n    dic[nums[i]] = i\n  end\n\n  []\nend\n
可视化运行

全屏观看 >

此方法通过哈希查找将时间复杂度从 \\(O(n^2)\\) 降至 \\(O(n)\\) ,大幅提升运行效率。

由于需要维护一个额外的哈希表,因此空间复杂度为 \\(O(n)\\) 。尽管如此,该方法的整体时空效率更为均衡,因此它是本题的最优解法。

","path":["第 10 章   搜索","10.4   哈希优化策略"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/","level":1,"title":"10.5   重识搜索算法","text":"

搜索算法(searching algorithm)用于在数据结构(例如数组、链表、树或图)中搜索一个或一组满足特定条件的元素。

搜索算法可根据实现思路分为以下两类。

不难发现,这些知识点都已在前面的章节中介绍过,因此搜索算法对于我们来说并不陌生。在本节中,我们将从更加系统的视角切入,重新审视搜索算法。

","path":["第 10 章   搜索","10.5   重识搜索算法"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1051","level":2,"title":"10.5.1   暴力搜索","text":"

暴力搜索通过遍历数据结构的每个元素来定位目标元素。

暴力搜索的优点是简单且通用性好,无须对数据做预处理和借助额外的数据结构。

然而,此类算法的时间复杂度为 \\(O(n)\\) ,其中 \\(n\\) 为元素数量,因此在数据量较大的情况下性能较差。

","path":["第 10 章   搜索","10.5   重识搜索算法"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1052","level":2,"title":"10.5.2   自适应搜索","text":"

自适应搜索利用数据的特有属性(例如有序性)来优化搜索过程,从而更高效地定位目标元素。

此类算法的优点是效率高,时间复杂度可达到 \\(O(\\log n)\\) 甚至 \\(O(1)\\) 。

然而,使用这些算法往往需要对数据进行预处理。例如,二分查找需要预先对数组进行排序,哈希查找和树查找都需要借助额外的数据结构,维护这些数据结构也需要额外的时间和空间开销。

Tip

自适应搜索算法常被称为查找算法,主要用于在特定数据结构中快速检索目标元素。

","path":["第 10 章   搜索","10.5   重识搜索算法"],"tags":[]},{"location":"chapter_searching/searching_algorithm_revisited/#1053","level":2,"title":"10.5.3   搜索方法选取","text":"

给定大小为 \\(n\\) 的一组数据,我们可以使用线性搜索、二分查找、树查找、哈希查找等多种方法从中搜索目标元素。各个方法的工作原理如图 10-11 所示。

图 10-11   多种搜索策略

上述几种方法的操作效率与特性如表 10-1 所示。

表 10-1   查找算法效率对比

线性搜索 二分查找 树查找 哈希查找 查找元素 \\(O(n)\\) \\(O(\\log n)\\) \\(O(\\log n)\\) \\(O(1)\\) 插入元素 \\(O(1)\\) \\(O(n)\\) \\(O(\\log n)\\) \\(O(1)\\) 删除元素 \\(O(n)\\) \\(O(n)\\) \\(O(\\log n)\\) \\(O(1)\\) 额外空间 \\(O(1)\\) \\(O(1)\\) \\(O(n)\\) \\(O(n)\\) 数据预处理 / 排序 \\(O(n \\log n)\\) 建树 \\(O(n \\log n)\\) 建哈希表 \\(O(n)\\) 数据是否有序 无序 有序 有序 无序

搜索算法的选择还取决规模、搜索性能要求、数据查询与更新频率等。

线性搜索

二分查找

哈希查找

树查找

","path":["第 10 章   搜索","10.5   重识搜索算法"],"tags":[]},{"location":"chapter_searching/summary/","level":1,"title":"10.6   小结","text":"","path":["第 10 章   搜索","10.6   小结"],"tags":[]},{"location":"chapter_searching/summary/#1","level":3,"title":"1.   重点回顾","text":"","path":["第 10 章   搜索","10.6   小结"],"tags":[]},{"location":"chapter_sorting/","level":1,"title":"第 11 章   排序","text":"

Abstract

排序犹如一把将混乱变为秩序的魔法钥匙,使我们能以更高效的方式理解与处理数据。

无论是简单的升序,还是复杂的分类排列,排序都向我们展示了数据的和谐美感。

","path":["第 11 章   排序"],"tags":[]},{"location":"chapter_sorting/#_1","level":2,"title":"本章内容","text":"","path":["第 11 章   排序"],"tags":[]},{"location":"chapter_sorting/bubble_sort/","level":1,"title":"11.3   冒泡排序","text":"

冒泡排序(bubble sort)通过连续地比较与交换相邻元素实现排序。这个过程就像气泡从底部升到顶部一样,因此得名冒泡排序。

如图 11-4 所示,冒泡过程可以利用元素交换操作来模拟:从数组最左端开始向右遍历,依次比较相邻元素大小,如果“左元素 > 右元素”就交换二者。遍历完成后,最大的元素会被移动到数组的最右端。

<1><2><3><4><5><6><7>

图 11-4   利用元素交换操作模拟冒泡

","path":["第 11 章   排序","11.3   冒泡排序"],"tags":[]},{"location":"chapter_sorting/bubble_sort/#1131","level":2,"title":"11.3.1   算法流程","text":"

设数组的长度为 \\(n\\) ,冒泡排序的步骤如图 11-5 所示。

  1. 首先,对 \\(n\\) 个元素执行“冒泡”,将数组的最大元素交换至正确位置。
  2. 接下来,对剩余 \\(n - 1\\) 个元素执行“冒泡”,将第二大元素交换至正确位置。
  3. 以此类推,经过 \\(n - 1\\) 轮“冒泡”后,前 \\(n - 1\\) 大的元素都被交换至正确位置。
  4. 仅剩的一个元素必定是最小元素,无须排序,因此数组排序完成。

图 11-5   冒泡排序流程

示例代码如下:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby bubble_sort.py
def bubble_sort(nums: list[int]):\n    \"\"\"冒泡排序\"\"\"\n    n = len(nums)\n    # 外循环:未排序区间为 [0, i]\n    for i in range(n - 1, 0, -1):\n        # 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n        for j in range(i):\n            if nums[j] > nums[j + 1]:\n                # 交换 nums[j] 与 nums[j + 1]\n                nums[j], nums[j + 1] = nums[j + 1], nums[j]\n
bubble_sort.cpp
/* 冒泡排序 */\nvoid bubbleSort(vector<int> &nums) {\n    // 外循环:未排序区间为 [0, i]\n    for (int i = nums.size() - 1; i > 0; i--) {\n        // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // 交换 nums[j] 与 nums[j + 1]\n                // 这里使用了 std::swap() 函数\n                swap(nums[j], nums[j + 1]);\n            }\n        }\n    }\n}\n
bubble_sort.java
/* 冒泡排序 */\nvoid bubbleSort(int[] nums) {\n    // 外循环:未排序区间为 [0, i]\n    for (int i = nums.length - 1; i > 0; i--) {\n        // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // 交换 nums[j] 与 nums[j + 1]\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n            }\n        }\n    }\n}\n
bubble_sort.cs
/* 冒泡排序 */\nvoid BubbleSort(int[] nums) {\n    // 外循环:未排序区间为 [0, i]\n    for (int i = nums.Length - 1; i > 0; i--) {\n        // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // 交换 nums[j] 与 nums[j + 1]\n                (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);\n            }\n        }\n    }\n}\n
bubble_sort.go
/* 冒泡排序 */\nfunc bubbleSort(nums []int) {\n    // 外循环:未排序区间为 [0, i]\n    for i := len(nums) - 1; i > 0; i-- {\n        // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n        for j := 0; j < i; j++ {\n            if nums[j] > nums[j+1] {\n                // 交换 nums[j] 与 nums[j + 1]\n                nums[j], nums[j+1] = nums[j+1], nums[j]\n            }\n        }\n    }\n}\n
bubble_sort.swift
/* 冒泡排序 */\nfunc bubbleSort(nums: inout [Int]) {\n    // 外循环:未排序区间为 [0, i]\n    for i in nums.indices.dropFirst().reversed() {\n        // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n        for j in 0 ..< i {\n            if nums[j] > nums[j + 1] {\n                // 交换 nums[j] 与 nums[j + 1]\n                nums.swapAt(j, j + 1)\n            }\n        }\n    }\n}\n
bubble_sort.js
/* 冒泡排序 */\nfunction bubbleSort(nums) {\n    // 外循环:未排序区间为 [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // 交换 nums[j] 与 nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n            }\n        }\n    }\n}\n
bubble_sort.ts
/* 冒泡排序 */\nfunction bubbleSort(nums: number[]): void {\n    // 外循环:未排序区间为 [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // 交换 nums[j] 与 nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n            }\n        }\n    }\n}\n
bubble_sort.dart
/* 冒泡排序 */\nvoid bubbleSort(List<int> nums) {\n  // 外循环:未排序区间为 [0, i]\n  for (int i = nums.length - 1; i > 0; i--) {\n    // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n    for (int j = 0; j < i; j++) {\n      if (nums[j] > nums[j + 1]) {\n        // 交换 nums[j] 与 nums[j + 1]\n        int tmp = nums[j];\n        nums[j] = nums[j + 1];\n        nums[j + 1] = tmp;\n      }\n    }\n  }\n}\n
bubble_sort.rs
/* 冒泡排序 */\nfn bubble_sort(nums: &mut [i32]) {\n    // 外循环:未排序区间为 [0, i]\n    for i in (1..nums.len()).rev() {\n        // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n        for j in 0..i {\n            if nums[j] > nums[j + 1] {\n                // 交换 nums[j] 与 nums[j + 1]\n                nums.swap(j, j + 1);\n            }\n        }\n    }\n}\n
bubble_sort.c
/* 冒泡排序 */\nvoid bubbleSort(int nums[], int size) {\n    // 外循环:未排序区间为 [0, i]\n    for (int i = size - 1; i > 0; i--) {\n        // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                int temp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = temp;\n            }\n        }\n    }\n}\n
bubble_sort.kt
/* 冒泡排序 */\nfun bubbleSort(nums: IntArray) {\n    // 外循环:未排序区间为 [0, i]\n    for (i in nums.size - 1 downTo 1) {\n        // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n        for (j in 0..<i) {\n            if (nums[j] > nums[j + 1]) {\n                // 交换 nums[j] 与 nums[j + 1]\n                val temp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = temp\n            }\n        }\n    }\n}\n
bubble_sort.rb
### 冒泡排序 ###\ndef bubble_sort(nums)\n  n = nums.length\n  # 外循环:未排序区间为 [0, i]\n  for i in (n - 1).downto(1)\n    # 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # 交换 nums[j] 与 nums[j + 1]\n        nums[j], nums[j + 1] = nums[j + 1], nums[j]\n      end\n    end\n  end\nend\n
可视化运行

全屏观看 >

","path":["第 11 章   排序","11.3   冒泡排序"],"tags":[]},{"location":"chapter_sorting/bubble_sort/#1132","level":2,"title":"11.3.2   效率优化","text":"

我们发现,如果某轮“冒泡”中没有执行任何交换操作,说明数组已经完成排序,可直接返回结果。因此,可以增加一个标志位 flag 来监测这种情况,一旦出现就立即返回。

经过优化,冒泡排序的最差时间复杂度和平均时间复杂度仍为 \\(O(n^2)\\) ;但当输入数组完全有序时,可达到最佳时间复杂度 \\(O(n)\\) 。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby bubble_sort.py
def bubble_sort_with_flag(nums: list[int]):\n    \"\"\"冒泡排序(标志优化)\"\"\"\n    n = len(nums)\n    # 外循环:未排序区间为 [0, i]\n    for i in range(n - 1, 0, -1):\n        flag = False  # 初始化标志位\n        # 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n        for j in range(i):\n            if nums[j] > nums[j + 1]:\n                # 交换 nums[j] 与 nums[j + 1]\n                nums[j], nums[j + 1] = nums[j + 1], nums[j]\n                flag = True  # 记录交换元素\n        if not flag:\n            break  # 此轮“冒泡”未交换任何元素,直接跳出\n
bubble_sort.cpp
/* 冒泡排序(标志优化)*/\nvoid bubbleSortWithFlag(vector<int> &nums) {\n    // 外循环:未排序区间为 [0, i]\n    for (int i = nums.size() - 1; i > 0; i--) {\n        bool flag = false; // 初始化标志位\n        // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // 交换 nums[j] 与 nums[j + 1]\n                // 这里使用了 std::swap() 函数\n                swap(nums[j], nums[j + 1]);\n                flag = true; // 记录交换元素\n            }\n        }\n        if (!flag)\n            break; // 此轮“冒泡”未交换任何元素,直接跳出\n    }\n}\n
bubble_sort.java
/* 冒泡排序(标志优化) */\nvoid bubbleSortWithFlag(int[] nums) {\n    // 外循环:未排序区间为 [0, i]\n    for (int i = nums.length - 1; i > 0; i--) {\n        boolean flag = false; // 初始化标志位\n        // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // 交换 nums[j] 与 nums[j + 1]\n                int tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                flag = true; // 记录交换元素\n            }\n        }\n        if (!flag)\n            break; // 此轮“冒泡”未交换任何元素,直接跳出\n    }\n}\n
bubble_sort.cs
/* 冒泡排序(标志优化)*/\nvoid BubbleSortWithFlag(int[] nums) {\n    // 外循环:未排序区间为 [0, i]\n    for (int i = nums.Length - 1; i > 0; i--) {\n        bool flag = false; // 初始化标志位\n        // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // 交换 nums[j] 与 nums[j + 1]\n                (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);\n                flag = true;  // 记录交换元素\n            }\n        }\n        if (!flag) break;     // 此轮“冒泡”未交换任何元素,直接跳出\n    }\n}\n
bubble_sort.go
/* 冒泡排序(标志优化)*/\nfunc bubbleSortWithFlag(nums []int) {\n    // 外循环:未排序区间为 [0, i]\n    for i := len(nums) - 1; i > 0; i-- {\n        flag := false // 初始化标志位\n        // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n        for j := 0; j < i; j++ {\n            if nums[j] > nums[j+1] {\n                // 交换 nums[j] 与 nums[j + 1]\n                nums[j], nums[j+1] = nums[j+1], nums[j]\n                flag = true // 记录交换元素\n            }\n        }\n        if flag == false { // 此轮“冒泡”未交换任何元素,直接跳出\n            break\n        }\n    }\n}\n
bubble_sort.swift
/* 冒泡排序(标志优化)*/\nfunc bubbleSortWithFlag(nums: inout [Int]) {\n    // 外循环:未排序区间为 [0, i]\n    for i in nums.indices.dropFirst().reversed() {\n        var flag = false // 初始化标志位\n        for j in 0 ..< i {\n            if nums[j] > nums[j + 1] {\n                // 交换 nums[j] 与 nums[j + 1]\n                nums.swapAt(j, j + 1)\n                flag = true // 记录交换元素\n            }\n        }\n        if !flag { // 此轮“冒泡”未交换任何元素,直接跳出\n            break\n        }\n    }\n}\n
bubble_sort.js
/* 冒泡排序(标志优化)*/\nfunction bubbleSortWithFlag(nums) {\n    // 外循环:未排序区间为 [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        let flag = false; // 初始化标志位\n        // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // 交换 nums[j] 与 nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                flag = true; // 记录交换元素\n            }\n        }\n        if (!flag) break; // 此轮“冒泡”未交换任何元素,直接跳出\n    }\n}\n
bubble_sort.ts
/* 冒泡排序(标志优化)*/\nfunction bubbleSortWithFlag(nums: number[]): void {\n    // 外循环:未排序区间为 [0, i]\n    for (let i = nums.length - 1; i > 0; i--) {\n        let flag = false; // 初始化标志位\n        // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n        for (let j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                // 交换 nums[j] 与 nums[j + 1]\n                let tmp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = tmp;\n                flag = true; // 记录交换元素\n            }\n        }\n        if (!flag) break; // 此轮“冒泡”未交换任何元素,直接跳出\n    }\n}\n
bubble_sort.dart
/* 冒泡排序(标志优化)*/\nvoid bubbleSortWithFlag(List<int> nums) {\n  // 外循环:未排序区间为 [0, i]\n  for (int i = nums.length - 1; i > 0; i--) {\n    bool flag = false; // 初始化标志位\n    // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n    for (int j = 0; j < i; j++) {\n      if (nums[j] > nums[j + 1]) {\n        // 交换 nums[j] 与 nums[j + 1]\n        int tmp = nums[j];\n        nums[j] = nums[j + 1];\n        nums[j + 1] = tmp;\n        flag = true; // 记录交换元素\n      }\n    }\n    if (!flag) break; // 此轮“冒泡”未交换任何元素,直接跳出\n  }\n}\n
bubble_sort.rs
/* 冒泡排序(标志优化) */\nfn bubble_sort_with_flag(nums: &mut [i32]) {\n    // 外循环:未排序区间为 [0, i]\n    for i in (1..nums.len()).rev() {\n        let mut flag = false; // 初始化标志位\n        // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n        for j in 0..i {\n            if nums[j] > nums[j + 1] {\n                // 交换 nums[j] 与 nums[j + 1]\n                nums.swap(j, j + 1);\n                flag = true; // 记录交换元素\n            }\n        }\n        if !flag {\n            break; // 此轮“冒泡”未交换任何元素,直接跳出\n        };\n    }\n}\n
bubble_sort.c
/* 冒泡排序(标志优化)*/\nvoid bubbleSortWithFlag(int nums[], int size) {\n    // 外循环:未排序区间为 [0, i]\n    for (int i = size - 1; i > 0; i--) {\n        bool flag = false;\n        // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n        for (int j = 0; j < i; j++) {\n            if (nums[j] > nums[j + 1]) {\n                int temp = nums[j];\n                nums[j] = nums[j + 1];\n                nums[j + 1] = temp;\n                flag = true;\n            }\n        }\n        if (!flag)\n            break;\n    }\n}\n
bubble_sort.kt
/* 冒泡排序(标志优化) */\nfun bubbleSortWithFlag(nums: IntArray) {\n    // 外循环:未排序区间为 [0, i]\n    for (i in nums.size - 1 downTo 1) {\n        var flag = false // 初始化标志位\n        // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n        for (j in 0..<i) {\n            if (nums[j] > nums[j + 1]) {\n                // 交换 nums[j] 与 nums[j + 1]\n                val temp = nums[j]\n                nums[j] = nums[j + 1]\n                nums[j + 1] = temp\n                flag = true // 记录交换元素\n            }\n        }\n        if (!flag) break // 此轮“冒泡”未交换任何元素,直接跳出\n    }\n}\n
bubble_sort.rb
### 冒泡排序(标志优化)###\ndef bubble_sort_with_flag(nums)\n  n = nums.length\n  # 外循环:未排序区间为 [0, i]\n  for i in (n - 1).downto(1)\n    flag = false # 初始化标志位\n\n    # 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端\n    for j in 0...i\n      if nums[j] > nums[j + 1]\n        # 交换 nums[j] 与 nums[j + 1]\n        nums[j], nums[j + 1] = nums[j + 1], nums[j]\n        flag = true # 记录交换元素\n      end\n    end\n\n    break unless flag # 此轮“冒泡”未交换任何元素,直接跳出\n  end\nend\n
可视化运行

全屏观看 >

","path":["第 11 章   排序","11.3   冒泡排序"],"tags":[]},{"location":"chapter_sorting/bubble_sort/#1133","level":2,"title":"11.3.3   算法特性","text":"","path":["第 11 章   排序","11.3   冒泡排序"],"tags":[]},{"location":"chapter_sorting/bucket_sort/","level":1,"title":"11.8   桶排序","text":"

前述几种排序算法都属于“基于比较的排序算法”,它们通过比较元素间的大小来实现排序。此类排序算法的时间复杂度无法超越 \\(O(n \\log n)\\) 。接下来,我们将探讨几种“非比较排序算法”,它们的时间复杂度可以达到线性阶。

桶排序(bucket sort)是分治策略的一个典型应用。它通过设置一些具有大小顺序的桶,每个桶对应一个数据范围,将数据平均分配到各个桶中;然后,在每个桶内部分别执行排序;最终按照桶的顺序将所有数据合并。

","path":["第 11 章   排序","11.8   桶排序"],"tags":[]},{"location":"chapter_sorting/bucket_sort/#1181","level":2,"title":"11.8.1   算法流程","text":"

考虑一个长度为 \\(n\\) 的数组,其元素是范围 \\([0, 1)\\) 内的浮点数。桶排序的流程如图 11-13 所示。

  1. 初始化 \\(k\\) 个桶,将 \\(n\\) 个元素分配到 \\(k\\) 个桶中。
  2. 对每个桶分别执行排序(这里采用编程语言的内置排序函数)。
  3. 按照桶从小到大的顺序合并结果。

图 11-13   桶排序算法流程

代码如下所示:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby bucket_sort.py
def bucket_sort(nums: list[float]):\n    \"\"\"桶排序\"\"\"\n    # 初始化 k = n/2 个桶,预期向每个桶分配 2 个元素\n    k = len(nums) // 2\n    buckets = [[] for _ in range(k)]\n    # 1. 将数组元素分配到各个桶中\n    for num in nums:\n        # 输入数据范围为 [0, 1),使用 num * k 映射到索引范围 [0, k-1]\n        i = int(num * k)\n        # 将 num 添加进桶 i\n        buckets[i].append(num)\n    # 2. 对各个桶执行排序\n    for bucket in buckets:\n        # 使用内置排序函数,也可以替换成其他排序算法\n        bucket.sort()\n    # 3. 遍历桶合并结果\n    i = 0\n    for bucket in buckets:\n        for num in bucket:\n            nums[i] = num\n            i += 1\n
bucket_sort.cpp
/* 桶排序 */\nvoid bucketSort(vector<float> &nums) {\n    // 初始化 k = n/2 个桶,预期向每个桶分配 2 个元素\n    int k = nums.size() / 2;\n    vector<vector<float>> buckets(k);\n    // 1. 将数组元素分配到各个桶中\n    for (float num : nums) {\n        // 输入数据范围为 [0, 1),使用 num * k 映射到索引范围 [0, k-1]\n        int i = num * k;\n        // 将 num 添加进桶 bucket_idx\n        buckets[i].push_back(num);\n    }\n    // 2. 对各个桶执行排序\n    for (vector<float> &bucket : buckets) {\n        // 使用内置排序函数,也可以替换成其他排序算法\n        sort(bucket.begin(), bucket.end());\n    }\n    // 3. 遍历桶合并结果\n    int i = 0;\n    for (vector<float> &bucket : buckets) {\n        for (float num : bucket) {\n            nums[i++] = num;\n        }\n    }\n}\n
bucket_sort.java
/* 桶排序 */\nvoid bucketSort(float[] nums) {\n    // 初始化 k = n/2 个桶,预期向每个桶分配 2 个元素\n    int k = nums.length / 2;\n    List<List<Float>> buckets = new ArrayList<>();\n    for (int i = 0; i < k; i++) {\n        buckets.add(new ArrayList<>());\n    }\n    // 1. 将数组元素分配到各个桶中\n    for (float num : nums) {\n        // 输入数据范围为 [0, 1),使用 num * k 映射到索引范围 [0, k-1]\n        int i = (int) (num * k);\n        // 将 num 添加进桶 i\n        buckets.get(i).add(num);\n    }\n    // 2. 对各个桶执行排序\n    for (List<Float> bucket : buckets) {\n        // 使用内置排序函数,也可以替换成其他排序算法\n        Collections.sort(bucket);\n    }\n    // 3. 遍历桶合并结果\n    int i = 0;\n    for (List<Float> bucket : buckets) {\n        for (float num : bucket) {\n            nums[i++] = num;\n        }\n    }\n}\n
bucket_sort.cs
/* 桶排序 */\nvoid BucketSort(float[] nums) {\n    // 初始化 k = n/2 个桶,预期向每个桶分配 2 个元素\n    int k = nums.Length / 2;\n    List<List<float>> buckets = [];\n    for (int i = 0; i < k; i++) {\n        buckets.Add([]);\n    }\n    // 1. 将数组元素分配到各个桶中\n    foreach (float num in nums) {\n        // 输入数据范围为 [0, 1),使用 num * k 映射到索引范围 [0, k-1]\n        int i = (int)(num * k);\n        // 将 num 添加进桶 i\n        buckets[i].Add(num);\n    }\n    // 2. 对各个桶执行排序\n    foreach (List<float> bucket in buckets) {\n        // 使用内置排序函数,也可以替换成其他排序算法\n        bucket.Sort();\n    }\n    // 3. 遍历桶合并结果\n    int j = 0;\n    foreach (List<float> bucket in buckets) {\n        foreach (float num in bucket) {\n            nums[j++] = num;\n        }\n    }\n}\n
bucket_sort.go
/* 桶排序 */\nfunc bucketSort(nums []float64) {\n    // 初始化 k = n/2 个桶,预期向每个桶分配 2 个元素\n    k := len(nums) / 2\n    buckets := make([][]float64, k)\n    for i := 0; i < k; i++ {\n        buckets[i] = make([]float64, 0)\n    }\n    // 1. 将数组元素分配到各个桶中\n    for _, num := range nums {\n        // 输入数据范围为 [0, 1),使用 num * k 映射到索引范围 [0, k-1]\n        i := int(num * float64(k))\n        // 将 num 添加进桶 i\n        buckets[i] = append(buckets[i], num)\n    }\n    // 2. 对各个桶执行排序\n    for i := 0; i < k; i++ {\n        // 使用内置切片排序函数,也可以替换成其他排序算法\n        sort.Float64s(buckets[i])\n    }\n    // 3. 遍历桶合并结果\n    i := 0\n    for _, bucket := range buckets {\n        for _, num := range bucket {\n            nums[i] = num\n            i++\n        }\n    }\n}\n
bucket_sort.swift
/* 桶排序 */\nfunc bucketSort(nums: inout [Double]) {\n    // 初始化 k = n/2 个桶,预期向每个桶分配 2 个元素\n    let k = nums.count / 2\n    var buckets = (0 ..< k).map { _ in [Double]() }\n    // 1. 将数组元素分配到各个桶中\n    for num in nums {\n        // 输入数据范围为 [0, 1),使用 num * k 映射到索引范围 [0, k-1]\n        let i = Int(num * Double(k))\n        // 将 num 添加进桶 i\n        buckets[i].append(num)\n    }\n    // 2. 对各个桶执行排序\n    for i in buckets.indices {\n        // 使用内置排序函数,也可以替换成其他排序算法\n        buckets[i].sort()\n    }\n    // 3. 遍历桶合并结果\n    var i = nums.startIndex\n    for bucket in buckets {\n        for num in bucket {\n            nums[i] = num\n            i += 1\n        }\n    }\n}\n
bucket_sort.js
/* 桶排序 */\nfunction bucketSort(nums) {\n    // 初始化 k = n/2 个桶,预期向每个桶分配 2 个元素\n    const k = nums.length / 2;\n    const buckets = [];\n    for (let i = 0; i < k; i++) {\n        buckets.push([]);\n    }\n    // 1. 将数组元素分配到各个桶中\n    for (const num of nums) {\n        // 输入数据范围为 [0, 1),使用 num * k 映射到索引范围 [0, k-1]\n        const i = Math.floor(num * k);\n        // 将 num 添加进桶 i\n        buckets[i].push(num);\n    }\n    // 2. 对各个桶执行排序\n    for (const bucket of buckets) {\n        // 使用内置排序函数,也可以替换成其他排序算法\n        bucket.sort((a, b) => a - b);\n    }\n    // 3. 遍历桶合并结果\n    let i = 0;\n    for (const bucket of buckets) {\n        for (const num of bucket) {\n            nums[i++] = num;\n        }\n    }\n}\n
bucket_sort.ts
/* 桶排序 */\nfunction bucketSort(nums: number[]): void {\n    // 初始化 k = n/2 个桶,预期向每个桶分配 2 个元素\n    const k = nums.length / 2;\n    const buckets: number[][] = [];\n    for (let i = 0; i < k; i++) {\n        buckets.push([]);\n    }\n    // 1. 将数组元素分配到各个桶中\n    for (const num of nums) {\n        // 输入数据范围为 [0, 1),使用 num * k 映射到索引范围 [0, k-1]\n        const i = Math.floor(num * k);\n        // 将 num 添加进桶 i\n        buckets[i].push(num);\n    }\n    // 2. 对各个桶执行排序\n    for (const bucket of buckets) {\n        // 使用内置排序函数,也可以替换成其他排序算法\n        bucket.sort((a, b) => a - b);\n    }\n    // 3. 遍历桶合并结果\n    let i = 0;\n    for (const bucket of buckets) {\n        for (const num of bucket) {\n            nums[i++] = num;\n        }\n    }\n}\n
bucket_sort.dart
/* 桶排序 */\nvoid bucketSort(List<double> nums) {\n  // 初始化 k = n/2 个桶,预期向每个桶分配 2 个元素\n  int k = nums.length ~/ 2;\n  List<List<double>> buckets = List.generate(k, (index) => []);\n\n  // 1. 将数组元素分配到各个桶中\n  for (double _num in nums) {\n    // 输入数据范围为 [0, 1),使用 _num * k 映射到索引范围 [0, k-1]\n    int i = (_num * k).toInt();\n    // 将 _num 添加进桶 bucket_idx\n    buckets[i].add(_num);\n  }\n  // 2. 对各个桶执行排序\n  for (List<double> bucket in buckets) {\n    bucket.sort();\n  }\n  // 3. 遍历桶合并结果\n  int i = 0;\n  for (List<double> bucket in buckets) {\n    for (double _num in bucket) {\n      nums[i++] = _num;\n    }\n  }\n}\n
bucket_sort.rs
/* 桶排序 */\nfn bucket_sort(nums: &mut [f64]) {\n    // 初始化 k = n/2 个桶,预期向每个桶分配 2 个元素\n    let k = nums.len() / 2;\n    let mut buckets = vec![vec![]; k];\n    // 1. 将数组元素分配到各个桶中\n    for &num in nums.iter() {\n        // 输入数据范围为 [0, 1),使用 num * k 映射到索引范围 [0, k-1]\n        let i = (num * k as f64) as usize;\n        // 将 num 添加进桶 i\n        buckets[i].push(num);\n    }\n    // 2. 对各个桶执行排序\n    for bucket in &mut buckets {\n        // 使用内置排序函数,也可以替换成其他排序算法\n        bucket.sort_by(|a, b| a.partial_cmp(b).unwrap());\n    }\n    // 3. 遍历桶合并结果\n    let mut i = 0;\n    for bucket in buckets.iter() {\n        for &num in bucket.iter() {\n            nums[i] = num;\n            i += 1;\n        }\n    }\n}\n
bucket_sort.c
/* 桶排序 */\nvoid bucketSort(float nums[], int n) {\n    int k = n / 2;                                 // 初始化 k = n/2 个桶\n    int *sizes = malloc(k * sizeof(int));          // 记录每个桶的大小\n    float **buckets = malloc(k * sizeof(float *)); // 动态数组的数组(桶)\n    // 为每个桶预分配足够的空间\n    for (int i = 0; i < k; ++i) {\n        buckets[i] = (float *)malloc(n * sizeof(float));\n        sizes[i] = 0;\n    }\n    // 1. 将数组元素分配到各个桶中\n    for (int i = 0; i < n; ++i) {\n        int idx = (int)(nums[i] * k);\n        buckets[idx][sizes[idx]++] = nums[i];\n    }\n    // 2. 对各个桶执行排序\n    for (int i = 0; i < k; ++i) {\n        qsort(buckets[i], sizes[i], sizeof(float), compare);\n    }\n    // 3. 合并排序后的桶\n    int idx = 0;\n    for (int i = 0; i < k; ++i) {\n        for (int j = 0; j < sizes[i]; ++j) {\n            nums[idx++] = buckets[i][j];\n        }\n        // 释放内存\n        free(buckets[i]);\n    }\n}\n
bucket_sort.kt
/* 桶排序 */\nfun bucketSort(nums: FloatArray) {\n    // 初始化 k = n/2 个桶,预期向每个桶分配 2 个元素\n    val k = nums.size / 2\n    val buckets = mutableListOf<MutableList<Float>>()\n    for (i in 0..<k) {\n        buckets.add(mutableListOf())\n    }\n    // 1. 将数组元素分配到各个桶中\n    for (num in nums) {\n        // 输入数据范围为 [0, 1),使用 num * k 映射到索引范围 [0, k-1]\n        val i = (num * k).toInt()\n        // 将 num 添加进桶 i\n        buckets[i].add(num)\n    }\n    // 2. 对各个桶执行排序\n    for (bucket in buckets) {\n        // 使用内置排序函数,也可以替换成其他排序算法\n        bucket.sort()\n    }\n    // 3. 遍历桶合并结果\n    var i = 0\n    for (bucket in buckets) {\n        for (num in bucket) {\n            nums[i++] = num\n        }\n    }\n}\n
bucket_sort.rb
### 桶排序 ###\ndef bucket_sort(nums)\n  # 初始化 k = n/2 个桶,预期向每个桶分配 2 个元素\n  k = nums.length / 2\n  buckets = Array.new(k) { [] }\n\n  # 1. 将数组元素分配到各个桶中\n  nums.each do |num|\n    # 输入数据范围为 [0, 1),使用 num * k 映射到索引范围 [0, k-1]\n    i = (num * k).to_i\n    # 将 num 添加进桶 i\n    buckets[i] << num\n  end\n\n  # 2. 对各个桶执行排序\n  buckets.each do |bucket|\n    # 使用内置排序函数,也可以替换成其他排序算法\n    bucket.sort!\n  end\n\n  # 3. 遍历桶合并结果\n  i = 0\n  buckets.each do |bucket|\n    bucket.each do |num|\n      nums[i] = num\n      i += 1\n    end\n  end\nend\n
可视化运行

全屏观看 >

","path":["第 11 章   排序","11.8   桶排序"],"tags":[]},{"location":"chapter_sorting/bucket_sort/#1182","level":2,"title":"11.8.2   算法特性","text":"

桶排序适用于处理体量很大的数据。例如,输入数据包含 100 万个元素,由于空间限制,系统内存无法一次性加载所有数据。此时,可以将数据分成 1000 个桶,然后分别对每个桶进行排序,最后将结果合并。

","path":["第 11 章   排序","11.8   桶排序"],"tags":[]},{"location":"chapter_sorting/bucket_sort/#1183","level":2,"title":"11.8.3   如何实现平均分配","text":"

桶排序的时间复杂度理论上可以达到 \\(O(n)\\) ,关键在于将元素均匀分配到各个桶中,因为实际数据往往不是均匀分布的。例如,我们想要将淘宝上的所有商品按价格范围平均分配到 10 个桶中,但商品价格分布不均,低于 100 元的非常多,高于 1000 元的非常少。若将价格区间平均划分为 10 个,各个桶中的商品数量差距会非常大。

为实现平均分配,我们可以先设定一条大致的分界线,将数据粗略地分到 3 个桶中。分配完毕后,再将商品较多的桶继续划分为 3 个桶,直至所有桶中的元素数量大致相等。

如图 11-14 所示,这种方法本质上是创建一棵递归树,目标是让叶节点的值尽可能平均。当然,不一定要每轮将数据划分为 3 个桶,具体划分方式可根据数据特点灵活选择。

图 11-14   递归划分桶

如果我们提前知道商品价格的概率分布,则可以根据数据概率分布设置每个桶的价格分界线。值得注意的是,数据分布并不一定需要特意统计,也可以根据数据特点采用某种概率模型进行近似。

如图 11-15 所示,我们假设商品价格服从正态分布,这样就可以合理地设定价格区间,从而将商品平均分配到各个桶中。

图 11-15   根据概率分布划分桶

","path":["第 11 章   排序","11.8   桶排序"],"tags":[]},{"location":"chapter_sorting/counting_sort/","level":1,"title":"11.9   计数排序","text":"

计数排序(counting sort)通过统计元素数量来实现排序,通常应用于整数数组。

","path":["第 11 章   排序","11.9   计数排序"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1191","level":2,"title":"11.9.1   简单实现","text":"

先来看一个简单的例子。给定一个长度为 \\(n\\) 的数组 nums ,其中的元素都是“非负整数”,计数排序的整体流程如图 11-16 所示。

  1. 遍历数组,找出其中的最大数字,记为 \\(m\\) ,然后创建一个长度为 \\(m + 1\\) 的辅助数组 counter
  2. 借助 counter 统计 nums 中各数字的出现次数,其中 counter[num] 对应数字 num 的出现次数。统计方法很简单,只需遍历 nums(设当前数字为 num),每轮将 counter[num] 增加 \\(1\\) 即可。
  3. 由于 counter 的各个索引天然有序,因此相当于所有数字已经排序好了。接下来,我们遍历 counter ,根据各数字出现次数从小到大的顺序填入 nums 即可。

图 11-16   计数排序流程

代码如下所示:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby counting_sort.py
def counting_sort_naive(nums: list[int]):\n    \"\"\"计数排序\"\"\"\n    # 简单实现,无法用于排序对象\n    # 1. 统计数组最大元素 m\n    m = max(nums)\n    # 2. 统计各数字的出现次数\n    # counter[num] 代表 num 的出现次数\n    counter = [0] * (m + 1)\n    for num in nums:\n        counter[num] += 1\n    # 3. 遍历 counter ,将各元素填入原数组 nums\n    i = 0\n    for num in range(m + 1):\n        for _ in range(counter[num]):\n            nums[i] = num\n            i += 1\n
counting_sort.cpp
/* 计数排序 */\n// 简单实现,无法用于排序对象\nvoid countingSortNaive(vector<int> &nums) {\n    // 1. 统计数组最大元素 m\n    int m = 0;\n    for (int num : nums) {\n        m = max(m, num);\n    }\n    // 2. 统计各数字的出现次数\n    // counter[num] 代表 num 的出现次数\n    vector<int> counter(m + 1, 0);\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. 遍历 counter ,将各元素填入原数组 nums\n    int i = 0;\n    for (int num = 0; num < m + 1; num++) {\n        for (int j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
counting_sort.java
/* 计数排序 */\n// 简单实现,无法用于排序对象\nvoid countingSortNaive(int[] nums) {\n    // 1. 统计数组最大元素 m\n    int m = 0;\n    for (int num : nums) {\n        m = Math.max(m, num);\n    }\n    // 2. 统计各数字的出现次数\n    // counter[num] 代表 num 的出现次数\n    int[] counter = new int[m + 1];\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. 遍历 counter ,将各元素填入原数组 nums\n    int i = 0;\n    for (int num = 0; num < m + 1; num++) {\n        for (int j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
counting_sort.cs
/* 计数排序 */\n// 简单实现,无法用于排序对象\nvoid CountingSortNaive(int[] nums) {\n    // 1. 统计数组最大元素 m\n    int m = 0;\n    foreach (int num in nums) {\n        m = Math.Max(m, num);\n    }\n    // 2. 统计各数字的出现次数\n    // counter[num] 代表 num 的出现次数\n    int[] counter = new int[m + 1];\n    foreach (int num in nums) {\n        counter[num]++;\n    }\n    // 3. 遍历 counter ,将各元素填入原数组 nums\n    int i = 0;\n    for (int num = 0; num < m + 1; num++) {\n        for (int j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
counting_sort.go
/* 计数排序 */\n// 简单实现,无法用于排序对象\nfunc countingSortNaive(nums []int) {\n    // 1. 统计数组最大元素 m\n    m := 0\n    for _, num := range nums {\n        if num > m {\n            m = num\n        }\n    }\n    // 2. 统计各数字的出现次数\n    // counter[num] 代表 num 的出现次数\n    counter := make([]int, m+1)\n    for _, num := range nums {\n        counter[num]++\n    }\n    // 3. 遍历 counter ,将各元素填入原数组 nums\n    for i, num := 0, 0; num < m+1; num++ {\n        for j := 0; j < counter[num]; j++ {\n            nums[i] = num\n            i++\n        }\n    }\n}\n
counting_sort.swift
/* 计数排序 */\n// 简单实现,无法用于排序对象\nfunc countingSortNaive(nums: inout [Int]) {\n    // 1. 统计数组最大元素 m\n    let m = nums.max()!\n    // 2. 统计各数字的出现次数\n    // counter[num] 代表 num 的出现次数\n    var counter = Array(repeating: 0, count: m + 1)\n    for num in nums {\n        counter[num] += 1\n    }\n    // 3. 遍历 counter ,将各元素填入原数组 nums\n    var i = 0\n    for num in 0 ..< m + 1 {\n        for _ in 0 ..< counter[num] {\n            nums[i] = num\n            i += 1\n        }\n    }\n}\n
counting_sort.js
/* 计数排序 */\n// 简单实现,无法用于排序对象\nfunction countingSortNaive(nums) {\n    // 1. 统计数组最大元素 m\n    let m = Math.max(...nums);\n    // 2. 统计各数字的出现次数\n    // counter[num] 代表 num 的出现次数\n    const counter = new Array(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. 遍历 counter ,将各元素填入原数组 nums\n    let i = 0;\n    for (let num = 0; num < m + 1; num++) {\n        for (let j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
counting_sort.ts
/* 计数排序 */\n// 简单实现,无法用于排序对象\nfunction countingSortNaive(nums: number[]): void {\n    // 1. 统计数组最大元素 m\n    let m: number = Math.max(...nums);\n    // 2. 统计各数字的出现次数\n    // counter[num] 代表 num 的出现次数\n    const counter: number[] = new Array<number>(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. 遍历 counter ,将各元素填入原数组 nums\n    let i = 0;\n    for (let num = 0; num < m + 1; num++) {\n        for (let j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n}\n
counting_sort.dart
/* 计数排序 */\n// 简单实现,无法用于排序对象\nvoid countingSortNaive(List<int> nums) {\n  // 1. 统计数组最大元素 m\n  int m = 0;\n  for (int _num in nums) {\n    m = max(m, _num);\n  }\n  // 2. 统计各数字的出现次数\n  // counter[_num] 代表 _num 的出现次数\n  List<int> counter = List.filled(m + 1, 0);\n  for (int _num in nums) {\n    counter[_num]++;\n  }\n  // 3. 遍历 counter ,将各元素填入原数组 nums\n  int i = 0;\n  for (int _num = 0; _num < m + 1; _num++) {\n    for (int j = 0; j < counter[_num]; j++, i++) {\n      nums[i] = _num;\n    }\n  }\n}\n
counting_sort.rs
/* 计数排序 */\n// 简单实现,无法用于排序对象\nfn counting_sort_naive(nums: &mut [i32]) {\n    // 1. 统计数组最大元素 m\n    let m = *nums.iter().max().unwrap();\n    // 2. 统计各数字的出现次数\n    // counter[num] 代表 num 的出现次数\n    let mut counter = vec![0; m as usize + 1];\n    for &num in nums.iter() {\n        counter[num as usize] += 1;\n    }\n    // 3. 遍历 counter ,将各元素填入原数组 nums\n    let mut i = 0;\n    for num in 0..m + 1 {\n        for _ in 0..counter[num as usize] {\n            nums[i] = num;\n            i += 1;\n        }\n    }\n}\n
counting_sort.c
/* 计数排序 */\n// 简单实现,无法用于排序对象\nvoid countingSortNaive(int nums[], int size) {\n    // 1. 统计数组最大元素 m\n    int m = 0;\n    for (int i = 0; i < size; i++) {\n        if (nums[i] > m) {\n            m = nums[i];\n        }\n    }\n    // 2. 统计各数字的出现次数\n    // counter[num] 代表 num 的出现次数\n    int *counter = calloc(m + 1, sizeof(int));\n    for (int i = 0; i < size; i++) {\n        counter[nums[i]]++;\n    }\n    // 3. 遍历 counter ,将各元素填入原数组 nums\n    int i = 0;\n    for (int num = 0; num < m + 1; num++) {\n        for (int j = 0; j < counter[num]; j++, i++) {\n            nums[i] = num;\n        }\n    }\n    // 4. 释放内存\n    free(counter);\n}\n
counting_sort.kt
/* 计数排序 */\n// 简单实现,无法用于排序对象\nfun countingSortNaive(nums: IntArray) {\n    // 1. 统计数组最大元素 m\n    var m = 0\n    for (num in nums) {\n        m = max(m, num)\n    }\n    // 2. 统计各数字的出现次数\n    // counter[num] 代表 num 的出现次数\n    val counter = IntArray(m + 1)\n    for (num in nums) {\n        counter[num]++\n    }\n    // 3. 遍历 counter ,将各元素填入原数组 nums\n    var i = 0\n    for (num in 0..<m + 1) {\n        var j = 0\n        while (j < counter[num]) {\n            nums[i] = num\n            j++\n            i++\n        }\n    }\n}\n
counting_sort.rb
### 计数排序 ###\ndef counting_sort_naive(nums)\n  # 简单实现,无法用于排序对象\n  # 1. 统计数组最大元素 m\n  m = 0\n  nums.each { |num| m = [m, num].max }\n  # 2. 统计各数字的出现次数\n  # counter[num] 代表 num 的出现次数\n  counter = Array.new(m + 1, 0)\n  nums.each { |num| counter[num] += 1 }\n  # 3. 遍历 counter ,将各元素填入原数组 nums\n  i = 0\n  for num in 0...(m + 1)\n    (0...counter[num]).each do\n      nums[i] = num\n      i += 1\n    end\n  end\nend\n
可视化运行

全屏观看 >

计数排序与桶排序的联系

从桶排序的角度看,我们可以将计数排序中的计数数组 counter 的每个索引视为一个桶,将统计数量的过程看作将各个元素分配到对应的桶中。本质上,计数排序是桶排序在整型数据下的一个特例。

","path":["第 11 章   排序","11.9   计数排序"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1192","level":2,"title":"11.9.2   完整实现","text":"

细心的读者可能发现了,如果输入数据是对象,上述步骤 3. 就失效了。假设输入数据是商品对象,我们想按照商品价格(类的成员变量)对商品进行排序,而上述算法只能给出价格的排序结果。

那么如何才能得到原数据的排序结果呢?我们首先计算 counter 的“前缀和”。顾名思义,索引 i 处的前缀和 prefix[i] 等于数组前 i 个元素之和:

\\[ \\text{prefix}[i] = \\sum_{j=0}^i \\text{counter[j]} \\]

前缀和具有明确的意义,prefix[num] - 1 代表元素 num 在结果数组 res 中最后一次出现的索引。这个信息非常关键,因为它告诉我们各个元素应该出现在结果数组的哪个位置。接下来,我们倒序遍历原数组 nums 的每个元素 num ,在每轮迭代中执行以下两步。

  1. num 填入数组 res 的索引 prefix[num] - 1 处。
  2. 令前缀和 prefix[num] 减小 \\(1\\) ,从而得到下次放置 num 的索引。

遍历完成后,数组 res 中就是排序好的结果,最后使用 res 覆盖原数组 nums 即可。图 11-17 展示了完整的计数排序流程。

<1><2><3><4><5><6><7><8>

图 11-17   计数排序步骤

计数排序的实现代码如下所示:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby counting_sort.py
def counting_sort(nums: list[int]):\n    \"\"\"计数排序\"\"\"\n    # 完整实现,可排序对象,并且是稳定排序\n    # 1. 统计数组最大元素 m\n    m = max(nums)\n    # 2. 统计各数字的出现次数\n    # counter[num] 代表 num 的出现次数\n    counter = [0] * (m + 1)\n    for num in nums:\n        counter[num] += 1\n    # 3. 求 counter 的前缀和,将“出现次数”转换为“尾索引”\n    # 即 counter[num]-1 是 num 在 res 中最后一次出现的索引\n    for i in range(m):\n        counter[i + 1] += counter[i]\n    # 4. 倒序遍历 nums ,将各元素填入结果数组 res\n    # 初始化数组 res 用于记录结果\n    n = len(nums)\n    res = [0] * n\n    for i in range(n - 1, -1, -1):\n        num = nums[i]\n        res[counter[num] - 1] = num  # 将 num 放置到对应索引处\n        counter[num] -= 1  # 令前缀和自减 1 ,得到下次放置 num 的索引\n    # 使用结果数组 res 覆盖原数组 nums\n    for i in range(n):\n        nums[i] = res[i]\n
counting_sort.cpp
/* 计数排序 */\n// 完整实现,可排序对象,并且是稳定排序\nvoid countingSort(vector<int> &nums) {\n    // 1. 统计数组最大元素 m\n    int m = 0;\n    for (int num : nums) {\n        m = max(m, num);\n    }\n    // 2. 统计各数字的出现次数\n    // counter[num] 代表 num 的出现次数\n    vector<int> counter(m + 1, 0);\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. 求 counter 的前缀和,将“出现次数”转换为“尾索引”\n    // 即 counter[num]-1 是 num 在 res 中最后一次出现的索引\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. 倒序遍历 nums ,将各元素填入结果数组 res\n    // 初始化数组 res 用于记录结果\n    int n = nums.size();\n    vector<int> res(n);\n    for (int i = n - 1; i >= 0; i--) {\n        int num = nums[i];\n        res[counter[num] - 1] = num; // 将 num 放置到对应索引处\n        counter[num]--;              // 令前缀和自减 1 ,得到下次放置 num 的索引\n    }\n    // 使用结果数组 res 覆盖原数组 nums\n    nums = res;\n}\n
counting_sort.java
/* 计数排序 */\n// 完整实现,可排序对象,并且是稳定排序\nvoid countingSort(int[] nums) {\n    // 1. 统计数组最大元素 m\n    int m = 0;\n    for (int num : nums) {\n        m = Math.max(m, num);\n    }\n    // 2. 统计各数字的出现次数\n    // counter[num] 代表 num 的出现次数\n    int[] counter = new int[m + 1];\n    for (int num : nums) {\n        counter[num]++;\n    }\n    // 3. 求 counter 的前缀和,将“出现次数”转换为“尾索引”\n    // 即 counter[num]-1 是 num 在 res 中最后一次出现的索引\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. 倒序遍历 nums ,将各元素填入结果数组 res\n    // 初始化数组 res 用于记录结果\n    int n = nums.length;\n    int[] res = new int[n];\n    for (int i = n - 1; i >= 0; i--) {\n        int num = nums[i];\n        res[counter[num] - 1] = num; // 将 num 放置到对应索引处\n        counter[num]--; // 令前缀和自减 1 ,得到下次放置 num 的索引\n    }\n    // 使用结果数组 res 覆盖原数组 nums\n    for (int i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
counting_sort.cs
/* 计数排序 */\n// 完整实现,可排序对象,并且是稳定排序\nvoid CountingSort(int[] nums) {\n    // 1. 统计数组最大元素 m\n    int m = 0;\n    foreach (int num in nums) {\n        m = Math.Max(m, num);\n    }\n    // 2. 统计各数字的出现次数\n    // counter[num] 代表 num 的出现次数\n    int[] counter = new int[m + 1];\n    foreach (int num in nums) {\n        counter[num]++;\n    }\n    // 3. 求 counter 的前缀和,将“出现次数”转换为“尾索引”\n    // 即 counter[num]-1 是 num 在 res 中最后一次出现的索引\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. 倒序遍历 nums ,将各元素填入结果数组 res\n    // 初始化数组 res 用于记录结果\n    int n = nums.Length;\n    int[] res = new int[n];\n    for (int i = n - 1; i >= 0; i--) {\n        int num = nums[i];\n        res[counter[num] - 1] = num; // 将 num 放置到对应索引处\n        counter[num]--; // 令前缀和自减 1 ,得到下次放置 num 的索引\n    }\n    // 使用结果数组 res 覆盖原数组 nums\n    for (int i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
counting_sort.go
/* 计数排序 */\n// 完整实现,可排序对象,并且是稳定排序\nfunc countingSort(nums []int) {\n    // 1. 统计数组最大元素 m\n    m := 0\n    for _, num := range nums {\n        if num > m {\n            m = num\n        }\n    }\n    // 2. 统计各数字的出现次数\n    // counter[num] 代表 num 的出现次数\n    counter := make([]int, m+1)\n    for _, num := range nums {\n        counter[num]++\n    }\n    // 3. 求 counter 的前缀和,将“出现次数”转换为“尾索引”\n    // 即 counter[num]-1 是 num 在 res 中最后一次出现的索引\n    for i := 0; i < m; i++ {\n        counter[i+1] += counter[i]\n    }\n    // 4. 倒序遍历 nums ,将各元素填入结果数组 res\n    // 初始化数组 res 用于记录结果\n    n := len(nums)\n    res := make([]int, n)\n    for i := n - 1; i >= 0; i-- {\n        num := nums[i]\n        // 将 num 放置到对应索引处\n        res[counter[num]-1] = num\n        // 令前缀和自减 1 ,得到下次放置 num 的索引\n        counter[num]--\n    }\n    // 使用结果数组 res 覆盖原数组 nums\n    copy(nums, res)\n}\n
counting_sort.swift
/* 计数排序 */\n// 完整实现,可排序对象,并且是稳定排序\nfunc countingSort(nums: inout [Int]) {\n    // 1. 统计数组最大元素 m\n    let m = nums.max()!\n    // 2. 统计各数字的出现次数\n    // counter[num] 代表 num 的出现次数\n    var counter = Array(repeating: 0, count: m + 1)\n    for num in nums {\n        counter[num] += 1\n    }\n    // 3. 求 counter 的前缀和,将“出现次数”转换为“尾索引”\n    // 即 counter[num]-1 是 num 在 res 中最后一次出现的索引\n    for i in 0 ..< m {\n        counter[i + 1] += counter[i]\n    }\n    // 4. 倒序遍历 nums ,将各元素填入结果数组 res\n    // 初始化数组 res 用于记录结果\n    var res = Array(repeating: 0, count: nums.count)\n    for i in nums.indices.reversed() {\n        let num = nums[i]\n        res[counter[num] - 1] = num // 将 num 放置到对应索引处\n        counter[num] -= 1 // 令前缀和自减 1 ,得到下次放置 num 的索引\n    }\n    // 使用结果数组 res 覆盖原数组 nums\n    for i in nums.indices {\n        nums[i] = res[i]\n    }\n}\n
counting_sort.js
/* 计数排序 */\n// 完整实现,可排序对象,并且是稳定排序\nfunction countingSort(nums) {\n    // 1. 统计数组最大元素 m\n    let m = Math.max(...nums);\n    // 2. 统计各数字的出现次数\n    // counter[num] 代表 num 的出现次数\n    const counter = new Array(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. 求 counter 的前缀和,将“出现次数”转换为“尾索引”\n    // 即 counter[num]-1 是 num 在 res 中最后一次出现的索引\n    for (let i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. 倒序遍历 nums ,将各元素填入结果数组 res\n    // 初始化数组 res 用于记录结果\n    const n = nums.length;\n    const res = new Array(n);\n    for (let i = n - 1; i >= 0; i--) {\n        const num = nums[i];\n        res[counter[num] - 1] = num; // 将 num 放置到对应索引处\n        counter[num]--; // 令前缀和自减 1 ,得到下次放置 num 的索引\n    }\n    // 使用结果数组 res 覆盖原数组 nums\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
counting_sort.ts
/* 计数排序 */\n// 完整实现,可排序对象,并且是稳定排序\nfunction countingSort(nums: number[]): void {\n    // 1. 统计数组最大元素 m\n    let m: number = Math.max(...nums);\n    // 2. 统计各数字的出现次数\n    // counter[num] 代表 num 的出现次数\n    const counter: number[] = new Array<number>(m + 1).fill(0);\n    for (const num of nums) {\n        counter[num]++;\n    }\n    // 3. 求 counter 的前缀和,将“出现次数”转换为“尾索引”\n    // 即 counter[num]-1 是 num 在 res 中最后一次出现的索引\n    for (let i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. 倒序遍历 nums ,将各元素填入结果数组 res\n    // 初始化数组 res 用于记录结果\n    const n = nums.length;\n    const res: number[] = new Array<number>(n);\n    for (let i = n - 1; i >= 0; i--) {\n        const num = nums[i];\n        res[counter[num] - 1] = num; // 将 num 放置到对应索引处\n        counter[num]--; // 令前缀和自减 1 ,得到下次放置 num 的索引\n    }\n    // 使用结果数组 res 覆盖原数组 nums\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n
counting_sort.dart
/* 计数排序 */\n// 完整实现,可排序对象,并且是稳定排序\nvoid countingSort(List<int> nums) {\n  // 1. 统计数组最大元素 m\n  int m = 0;\n  for (int _num in nums) {\n    m = max(m, _num);\n  }\n  // 2. 统计各数字的出现次数\n  // counter[_num] 代表 _num 的出现次数\n  List<int> counter = List.filled(m + 1, 0);\n  for (int _num in nums) {\n    counter[_num]++;\n  }\n  // 3. 求 counter 的前缀和,将“出现次数”转换为“尾索引”\n  // 即 counter[_num]-1 是 _num 在 res 中最后一次出现的索引\n  for (int i = 0; i < m; i++) {\n    counter[i + 1] += counter[i];\n  }\n  // 4. 倒序遍历 nums ,将各元素填入结果数组 res\n  // 初始化数组 res 用于记录结果\n  int n = nums.length;\n  List<int> res = List.filled(n, 0);\n  for (int i = n - 1; i >= 0; i--) {\n    int _num = nums[i];\n    res[counter[_num] - 1] = _num; // 将 _num 放置到对应索引处\n    counter[_num]--; // 令前缀和自减 1 ,得到下次放置 _num 的索引\n  }\n  // 使用结果数组 res 覆盖原数组 nums\n  nums.setAll(0, res);\n}\n
counting_sort.rs
/* 计数排序 */\n// 完整实现,可排序对象,并且是稳定排序\nfn counting_sort(nums: &mut [i32]) {\n    // 1. 统计数组最大元素 m\n    let m = *nums.iter().max().unwrap() as usize;\n    // 2. 统计各数字的出现次数\n    // counter[num] 代表 num 的出现次数\n    let mut counter = vec![0; m + 1];\n    for &num in nums.iter() {\n        counter[num as usize] += 1;\n    }\n    // 3. 求 counter 的前缀和,将“出现次数”转换为“尾索引”\n    // 即 counter[num]-1 是 num 在 res 中最后一次出现的索引\n    for i in 0..m {\n        counter[i + 1] += counter[i];\n    }\n    // 4. 倒序遍历 nums ,将各元素填入结果数组 res\n    // 初始化数组 res 用于记录结果\n    let n = nums.len();\n    let mut res = vec![0; n];\n    for i in (0..n).rev() {\n        let num = nums[i];\n        res[counter[num as usize] - 1] = num; // 将 num 放置到对应索引处\n        counter[num as usize] -= 1; // 令前缀和自减 1 ,得到下次放置 num 的索引\n    }\n    // 使用结果数组 res 覆盖原数组 nums\n    nums.copy_from_slice(&res)\n}\n
counting_sort.c
/* 计数排序 */\n// 完整实现,可排序对象,并且是稳定排序\nvoid countingSort(int nums[], int size) {\n    // 1. 统计数组最大元素 m\n    int m = 0;\n    for (int i = 0; i < size; i++) {\n        if (nums[i] > m) {\n            m = nums[i];\n        }\n    }\n    // 2. 统计各数字的出现次数\n    // counter[num] 代表 num 的出现次数\n    int *counter = calloc(m, sizeof(int));\n    for (int i = 0; i < size; i++) {\n        counter[nums[i]]++;\n    }\n    // 3. 求 counter 的前缀和,将“出现次数”转换为“尾索引”\n    // 即 counter[num]-1 是 num 在 res 中最后一次出现的索引\n    for (int i = 0; i < m; i++) {\n        counter[i + 1] += counter[i];\n    }\n    // 4. 倒序遍历 nums ,将各元素填入结果数组 res\n    // 初始化数组 res 用于记录结果\n    int *res = malloc(sizeof(int) * size);\n    for (int i = size - 1; i >= 0; i--) {\n        int num = nums[i];\n        res[counter[num] - 1] = num; // 将 num 放置到对应索引处\n        counter[num]--;              // 令前缀和自减 1 ,得到下次放置 num 的索引\n    }\n    // 使用结果数组 res 覆盖原数组 nums\n    memcpy(nums, res, size * sizeof(int));\n    // 5. 释放内存\n    free(res);\n    free(counter);\n}\n
counting_sort.kt
/* 计数排序 */\n// 完整实现,可排序对象,并且是稳定排序\nfun countingSort(nums: IntArray) {\n    // 1. 统计数组最大元素 m\n    var m = 0\n    for (num in nums) {\n        m = max(m, num)\n    }\n    // 2. 统计各数字的出现次数\n    // counter[num] 代表 num 的出现次数\n    val counter = IntArray(m + 1)\n    for (num in nums) {\n        counter[num]++\n    }\n    // 3. 求 counter 的前缀和,将“出现次数”转换为“尾索引”\n    // 即 counter[num]-1 是 num 在 res 中最后一次出现的索引\n    for (i in 0..<m) {\n        counter[i + 1] += counter[i]\n    }\n    // 4. 倒序遍历 nums ,将各元素填入结果数组 res\n    // 初始化数组 res 用于记录结果\n    val n = nums.size\n    val res = IntArray(n)\n    for (i in n - 1 downTo 0) {\n        val num = nums[i]\n        res[counter[num] - 1] = num // 将 num 放置到对应索引处\n        counter[num]-- // 令前缀和自减 1 ,得到下次放置 num 的索引\n    }\n    // 使用结果数组 res 覆盖原数组 nums\n    for (i in 0..<n) {\n        nums[i] = res[i]\n    }\n}\n
counting_sort.rb
### 计数排序 ###\ndef counting_sort(nums)\n  # 完整实现,可排序对象,并且是稳定排序\n  # 1. 统计数组最大元素 m\n  m = nums.max\n  # 2. 统计各数字的出现次数\n  # counter[num] 代表 num 的出现次数\n  counter = Array.new(m + 1, 0)\n  nums.each { |num| counter[num] += 1 }\n  # 3. 求 counter 的前缀和,将“出现次数”转换为“尾索引”\n  # 即 counter[num]-1 是 num 在 res 中最后一次出现的索引\n  (0...m).each { |i| counter[i + 1] += counter[i] }\n  # 4. 倒序遍历 nums, 将各元素填入结果数组 res\n  # 初始化数组 res 用于记录结果\n  n = nums.length\n  res = Array.new(n, 0)\n  (n - 1).downto(0).each do |i|\n    num = nums[i]\n    res[counter[num] - 1] = num # 将 num 放置到对应索引处\n    counter[num] -= 1 # 令前缀和自减 1 ,得到下次放置 num 的索引\n  end\n  # 使用结果数组 res 覆盖原数组 nums\n  (0...n).each { |i| nums[i] = res[i] }\nend\n
可视化运行

全屏观看 >

","path":["第 11 章   排序","11.9   计数排序"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1193","level":2,"title":"11.9.3   算法特性","text":"","path":["第 11 章   排序","11.9   计数排序"],"tags":[]},{"location":"chapter_sorting/counting_sort/#1194","level":2,"title":"11.9.4   局限性","text":"

看到这里,你也许会觉得计数排序非常巧妙,仅通过统计数量就可以实现高效的排序。然而,使用计数排序的前置条件相对较为严格。

计数排序只适用于非负整数。若想将其用于其他类型的数据,需要确保这些数据可以转换为非负整数,并且在转换过程中不能改变各个元素之间的相对大小关系。例如,对于包含负数的整数数组,可以先给所有数字加上一个常数,将全部数字转化为正数,排序完成后再转换回去。

计数排序适用于数据量大但数据范围较小的情况。比如,在上述示例中 \\(m\\) 不能太大,否则会占用过多空间。而当 \\(n \\ll m\\) 时,计数排序使用 \\(O(m)\\) 时间,可能比 \\(O(n \\log n)\\) 的排序算法还要慢。

","path":["第 11 章   排序","11.9   计数排序"],"tags":[]},{"location":"chapter_sorting/heap_sort/","level":1,"title":"11.7   堆排序","text":"

Tip

阅读本节前,请确保已学完“堆”章节。

堆排序(heap sort)是一种基于堆数据结构实现的高效排序算法。我们可以利用已经学过的“建堆操作”和“元素出堆操作”实现堆排序。

  1. 输入数组并建立小顶堆,此时最小元素位于堆顶。
  2. 不断执行出堆操作,依次记录出堆元素,即可得到从小到大排序的序列。

以上方法虽然可行,但需要借助一个额外数组来保存弹出的元素,比较浪费空间。在实际中,我们通常使用一种更加优雅的实现方式。

","path":["第 11 章   排序","11.7   堆排序"],"tags":[]},{"location":"chapter_sorting/heap_sort/#1171","level":2,"title":"11.7.1   算法流程","text":"

设数组的长度为 \\(n\\) ,堆排序的流程如图 11-12 所示。

  1. 输入数组并建立大顶堆。完成后,最大元素位于堆顶。
  2. 将堆顶元素(第一个元素)与堆底元素(最后一个元素)交换。完成交换后,堆的长度减 \\(1\\) ,已排序元素数量加 \\(1\\) 。
  3. 从堆顶元素开始,从顶到底执行堆化操作(sift down)。完成堆化后,堆的性质得到修复。
  4. 循环执行第 2. 步和第 3. 步。循环 \\(n - 1\\) 轮后,即可完成数组排序。

Tip

实际上,元素出堆操作中也包含第 2. 步和第 3. 步,只是多了一个弹出元素的步骤。

<1><2><3><4><5><6><7><8><9><10><11><12>

图 11-12   堆排序步骤

在代码实现中,我们使用了与“堆”章节相同的从顶至底堆化 sift_down() 函数。值得注意的是,由于堆的长度会随着提取最大元素而减小,因此我们需要给 sift_down() 函数添加一个长度参数 \\(n\\) ,用于指定堆的当前有效长度。代码如下所示:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby heap_sort.py
def sift_down(nums: list[int], n: int, i: int):\n    \"\"\"堆的长度为 n ,从节点 i 开始,从顶至底堆化\"\"\"\n    while True:\n        # 判断节点 i, l, r 中值最大的节点,记为 ma\n        l = 2 * i + 1\n        r = 2 * i + 2\n        ma = i\n        if l < n and nums[l] > nums[ma]:\n            ma = l\n        if r < n and nums[r] > nums[ma]:\n            ma = r\n        # 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出\n        if ma == i:\n            break\n        # 交换两节点\n        nums[i], nums[ma] = nums[ma], nums[i]\n        # 循环向下堆化\n        i = ma\n\ndef heap_sort(nums: list[int]):\n    \"\"\"堆排序\"\"\"\n    # 建堆操作:堆化除叶节点以外的其他所有节点\n    for i in range(len(nums) // 2 - 1, -1, -1):\n        sift_down(nums, len(nums), i)\n    # 从堆中提取最大元素,循环 n-1 轮\n    for i in range(len(nums) - 1, 0, -1):\n        # 交换根节点与最右叶节点(交换首元素与尾元素)\n        nums[0], nums[i] = nums[i], nums[0]\n        # 以根节点为起点,从顶至底进行堆化\n        sift_down(nums, i, 0)\n
heap_sort.cpp
/* 堆的长度为 n ,从节点 i 开始,从顶至底堆化 */\nvoid siftDown(vector<int> &nums, int n, int i) {\n    while (true) {\n        // 判断节点 i, l, r 中值最大的节点,记为 ma\n        int l = 2 * i + 1;\n        int r = 2 * i + 2;\n        int ma = i;\n        if (l < n && nums[l] > nums[ma])\n            ma = l;\n        if (r < n && nums[r] > nums[ma])\n            ma = r;\n        // 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出\n        if (ma == i) {\n            break;\n        }\n        // 交换两节点\n        swap(nums[i], nums[ma]);\n        // 循环向下堆化\n        i = ma;\n    }\n}\n\n/* 堆排序 */\nvoid heapSort(vector<int> &nums) {\n    // 建堆操作:堆化除叶节点以外的其他所有节点\n    for (int i = nums.size() / 2 - 1; i >= 0; --i) {\n        siftDown(nums, nums.size(), i);\n    }\n    // 从堆中提取最大元素,循环 n-1 轮\n    for (int i = nums.size() - 1; i > 0; --i) {\n        // 交换根节点与最右叶节点(交换首元素与尾元素)\n        swap(nums[0], nums[i]);\n        // 以根节点为起点,从顶至底进行堆化\n        siftDown(nums, i, 0);\n    }\n}\n
heap_sort.java
/* 堆的长度为 n ,从节点 i 开始,从顶至底堆化 */\nvoid siftDown(int[] nums, int n, int i) {\n    while (true) {\n        // 判断节点 i, l, r 中值最大的节点,记为 ma\n        int l = 2 * i + 1;\n        int r = 2 * i + 2;\n        int ma = i;\n        if (l < n && nums[l] > nums[ma])\n            ma = l;\n        if (r < n && nums[r] > nums[ma])\n            ma = r;\n        // 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出\n        if (ma == i)\n            break;\n        // 交换两节点\n        int temp = nums[i];\n        nums[i] = nums[ma];\n        nums[ma] = temp;\n        // 循环向下堆化\n        i = ma;\n    }\n}\n\n/* 堆排序 */\nvoid heapSort(int[] nums) {\n    // 建堆操作:堆化除叶节点以外的其他所有节点\n    for (int i = nums.length / 2 - 1; i >= 0; i--) {\n        siftDown(nums, nums.length, i);\n    }\n    // 从堆中提取最大元素,循环 n-1 轮\n    for (int i = nums.length - 1; i > 0; i--) {\n        // 交换根节点与最右叶节点(交换首元素与尾元素)\n        int tmp = nums[0];\n        nums[0] = nums[i];\n        nums[i] = tmp;\n        // 以根节点为起点,从顶至底进行堆化\n        siftDown(nums, i, 0);\n    }\n}\n
heap_sort.cs
/* 堆的长度为 n ,从节点 i 开始,从顶至底堆化 */\nvoid SiftDown(int[] nums, int n, int i) {\n    while (true) {\n        // 判断节点 i, l, r 中值最大的节点,记为 ma\n        int l = 2 * i + 1;\n        int r = 2 * i + 2;\n        int ma = i;\n        if (l < n && nums[l] > nums[ma])\n            ma = l;\n        if (r < n && nums[r] > nums[ma])\n            ma = r;\n        // 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出\n        if (ma == i)\n            break;\n        // 交换两节点\n        (nums[ma], nums[i]) = (nums[i], nums[ma]);\n        // 循环向下堆化\n        i = ma;\n    }\n}\n\n/* 堆排序 */\nvoid HeapSort(int[] nums) {\n    // 建堆操作:堆化除叶节点以外的其他所有节点\n    for (int i = nums.Length / 2 - 1; i >= 0; i--) {\n        SiftDown(nums, nums.Length, i);\n    }\n    // 从堆中提取最大元素,循环 n-1 轮\n    for (int i = nums.Length - 1; i > 0; i--) {\n        // 交换根节点与最右叶节点(交换首元素与尾元素)\n        (nums[i], nums[0]) = (nums[0], nums[i]);\n        // 以根节点为起点,从顶至底进行堆化\n        SiftDown(nums, i, 0);\n    }\n}\n
heap_sort.go
/* 堆的长度为 n ,从节点 i 开始,从顶至底堆化 */\nfunc siftDown(nums *[]int, n, i int) {\n    for true {\n        // 判断节点 i, l, r 中值最大的节点,记为 ma\n        l := 2*i + 1\n        r := 2*i + 2\n        ma := i\n        if l < n && (*nums)[l] > (*nums)[ma] {\n            ma = l\n        }\n        if r < n && (*nums)[r] > (*nums)[ma] {\n            ma = r\n        }\n        // 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出\n        if ma == i {\n            break\n        }\n        // 交换两节点\n        (*nums)[i], (*nums)[ma] = (*nums)[ma], (*nums)[i]\n        // 循环向下堆化\n        i = ma\n    }\n}\n\n/* 堆排序 */\nfunc heapSort(nums *[]int) {\n    // 建堆操作:堆化除叶节点以外的其他所有节点\n    for i := len(*nums)/2 - 1; i >= 0; i-- {\n        siftDown(nums, len(*nums), i)\n    }\n    // 从堆中提取最大元素,循环 n-1 轮\n    for i := len(*nums) - 1; i > 0; i-- {\n        // 交换根节点与最右叶节点(交换首元素与尾元素)\n        (*nums)[0], (*nums)[i] = (*nums)[i], (*nums)[0]\n        // 以根节点为起点,从顶至底进行堆化\n        siftDown(nums, i, 0)\n    }\n}\n
heap_sort.swift
/* 堆的长度为 n ,从节点 i 开始,从顶至底堆化 */\nfunc siftDown(nums: inout [Int], n: Int, i: Int) {\n    var i = i\n    while true {\n        // 判断节点 i, l, r 中值最大的节点,记为 ma\n        let l = 2 * i + 1\n        let r = 2 * i + 2\n        var ma = i\n        if l < n, nums[l] > nums[ma] {\n            ma = l\n        }\n        if r < n, nums[r] > nums[ma] {\n            ma = r\n        }\n        // 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出\n        if ma == i {\n            break\n        }\n        // 交换两节点\n        nums.swapAt(i, ma)\n        // 循环向下堆化\n        i = ma\n    }\n}\n\n/* 堆排序 */\nfunc heapSort(nums: inout [Int]) {\n    // 建堆操作:堆化除叶节点以外的其他所有节点\n    for i in stride(from: nums.count / 2 - 1, through: 0, by: -1) {\n        siftDown(nums: &nums, n: nums.count, i: i)\n    }\n    // 从堆中提取最大元素,循环 n-1 轮\n    for i in nums.indices.dropFirst().reversed() {\n        // 交换根节点与最右叶节点(交换首元素与尾元素)\n        nums.swapAt(0, i)\n        // 以根节点为起点,从顶至底进行堆化\n        siftDown(nums: &nums, n: i, i: 0)\n    }\n}\n
heap_sort.js
/* 堆的长度为 n ,从节点 i 开始,从顶至底堆化 */\nfunction siftDown(nums, n, i) {\n    while (true) {\n        // 判断节点 i, l, r 中值最大的节点,记为 ma\n        let l = 2 * i + 1;\n        let r = 2 * i + 2;\n        let ma = i;\n        if (l < n && nums[l] > nums[ma]) {\n            ma = l;\n        }\n        if (r < n && nums[r] > nums[ma]) {\n            ma = r;\n        }\n        // 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出\n        if (ma === i) {\n            break;\n        }\n        // 交换两节点\n        [nums[i], nums[ma]] = [nums[ma], nums[i]];\n        // 循环向下堆化\n        i = ma;\n    }\n}\n\n/* 堆排序 */\nfunction heapSort(nums) {\n    // 建堆操作:堆化除叶节点以外的其他所有节点\n    for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i--) {\n        siftDown(nums, nums.length, i);\n    }\n    // 从堆中提取最大元素,循环 n-1 轮\n    for (let i = nums.length - 1; i > 0; i--) {\n        // 交换根节点与最右叶节点(交换首元素与尾元素)\n        [nums[0], nums[i]] = [nums[i], nums[0]];\n        // 以根节点为起点,从顶至底进行堆化\n        siftDown(nums, i, 0);\n    }\n}\n
heap_sort.ts
/* 堆的长度为 n ,从节点 i 开始,从顶至底堆化 */\nfunction siftDown(nums: number[], n: number, i: number): void {\n    while (true) {\n        // 判断节点 i, l, r 中值最大的节点,记为 ma\n        let l = 2 * i + 1;\n        let r = 2 * i + 2;\n        let ma = i;\n        if (l < n && nums[l] > nums[ma]) {\n            ma = l;\n        }\n        if (r < n && nums[r] > nums[ma]) {\n            ma = r;\n        }\n        // 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出\n        if (ma === i) {\n            break;\n        }\n        // 交换两节点\n        [nums[i], nums[ma]] = [nums[ma], nums[i]];\n        // 循环向下堆化\n        i = ma;\n    }\n}\n\n/* 堆排序 */\nfunction heapSort(nums: number[]): void {\n    // 建堆操作:堆化除叶节点以外的其他所有节点\n    for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i--) {\n        siftDown(nums, nums.length, i);\n    }\n    // 从堆中提取最大元素,循环 n-1 轮\n    for (let i = nums.length - 1; i > 0; i--) {\n        // 交换根节点与最右叶节点(交换首元素与尾元素)\n        [nums[0], nums[i]] = [nums[i], nums[0]];\n        // 以根节点为起点,从顶至底进行堆化\n        siftDown(nums, i, 0);\n    }\n}\n
heap_sort.dart
/* 堆的长度为 n ,从节点 i 开始,从顶至底堆化 */\nvoid siftDown(List<int> nums, int n, int i) {\n  while (true) {\n    // 判断节点 i, l, r 中值最大的节点,记为 ma\n    int l = 2 * i + 1;\n    int r = 2 * i + 2;\n    int ma = i;\n    if (l < n && nums[l] > nums[ma]) ma = l;\n    if (r < n && nums[r] > nums[ma]) ma = r;\n    // 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出\n    if (ma == i) break;\n    // 交换两节点\n    int temp = nums[i];\n    nums[i] = nums[ma];\n    nums[ma] = temp;\n    // 循环向下堆化\n    i = ma;\n  }\n}\n\n/* 堆排序 */\nvoid heapSort(List<int> nums) {\n  // 建堆操作:堆化除叶节点以外的其他所有节点\n  for (int i = nums.length ~/ 2 - 1; i >= 0; i--) {\n    siftDown(nums, nums.length, i);\n  }\n  // 从堆中提取最大元素,循环 n-1 轮\n  for (int i = nums.length - 1; i > 0; i--) {\n    // 交换根节点与最右叶节点(交换首元素与尾元素)\n    int tmp = nums[0];\n    nums[0] = nums[i];\n    nums[i] = tmp;\n    // 以根节点为起点,从顶至底进行堆化\n    siftDown(nums, i, 0);\n  }\n}\n
heap_sort.rs
/* 堆的长度为 n ,从节点 i 开始,从顶至底堆化 */\nfn sift_down(nums: &mut [i32], n: usize, mut i: usize) {\n    loop {\n        // 判断节点 i, l, r 中值最大的节点,记为 ma\n        let l = 2 * i + 1;\n        let r = 2 * i + 2;\n        let mut ma = i;\n        if l < n && nums[l] > nums[ma] {\n            ma = l;\n        }\n        if r < n && nums[r] > nums[ma] {\n            ma = r;\n        }\n        // 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出\n        if ma == i {\n            break;\n        }\n        // 交换两节点\n        nums.swap(i, ma);\n        // 循环向下堆化\n        i = ma;\n    }\n}\n\n/* 堆排序 */\nfn heap_sort(nums: &mut [i32]) {\n    // 建堆操作:堆化除叶节点以外的其他所有节点\n    for i in (0..nums.len() / 2).rev() {\n        sift_down(nums, nums.len(), i);\n    }\n    // 从堆中提取最大元素,循环 n-1 轮\n    for i in (1..nums.len()).rev() {\n        // 交换根节点与最右叶节点(交换首元素与尾元素)\n        nums.swap(0, i);\n        // 以根节点为起点,从顶至底进行堆化\n        sift_down(nums, i, 0);\n    }\n}\n
heap_sort.c
/* 堆的长度为 n ,从节点 i 开始,从顶至底堆化 */\nvoid siftDown(int nums[], int n, int i) {\n    while (1) {\n        // 判断节点 i, l, r 中值最大的节点,记为 ma\n        int l = 2 * i + 1;\n        int r = 2 * i + 2;\n        int ma = i;\n        if (l < n && nums[l] > nums[ma])\n            ma = l;\n        if (r < n && nums[r] > nums[ma])\n            ma = r;\n        // 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出\n        if (ma == i) {\n            break;\n        }\n        // 交换两节点\n        int temp = nums[i];\n        nums[i] = nums[ma];\n        nums[ma] = temp;\n        // 循环向下堆化\n        i = ma;\n    }\n}\n\n/* 堆排序 */\nvoid heapSort(int nums[], int n) {\n    // 建堆操作:堆化除叶节点以外的其他所有节点\n    for (int i = n / 2 - 1; i >= 0; --i) {\n        siftDown(nums, n, i);\n    }\n    // 从堆中提取最大元素,循环 n-1 轮\n    for (int i = n - 1; i > 0; --i) {\n        // 交换根节点与最右叶节点(交换首元素与尾元素)\n        int tmp = nums[0];\n        nums[0] = nums[i];\n        nums[i] = tmp;\n        // 以根节点为起点,从顶至底进行堆化\n        siftDown(nums, i, 0);\n    }\n}\n
heap_sort.kt
/* 堆的长度为 n ,从节点 i 开始,从顶至底堆化 */\nfun siftDown(nums: IntArray, n: Int, li: Int) {\n    var i = li\n    while (true) {\n        // 判断节点 i, l, r 中值最大的节点,记为 ma\n        val l = 2 * i + 1\n        val r = 2 * i + 2\n        var ma = i\n        if (l < n && nums[l] > nums[ma]) \n            ma = l\n        if (r < n && nums[r] > nums[ma]) \n            ma = r\n        // 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出\n        if (ma == i) \n            break\n        // 交换两节点\n        val temp = nums[i]\n        nums[i] = nums[ma]\n        nums[ma] = temp\n        // 循环向下堆化\n        i = ma\n    }\n}\n\n/* 堆排序 */\nfun heapSort(nums: IntArray) {\n    // 建堆操作:堆化除叶节点以外的其他所有节点\n    for (i in nums.size / 2 - 1 downTo 0) {\n        siftDown(nums, nums.size, i)\n    }\n    // 从堆中提取最大元素,循环 n-1 轮\n    for (i in nums.size - 1 downTo 1) {\n        // 交换根节点与最右叶节点(交换首元素与尾元素)\n        val temp = nums[0]\n        nums[0] = nums[i]\n        nums[i] = temp\n        // 以根节点为起点,从顶至底进行堆化\n        siftDown(nums, i, 0)\n    }\n}\n
heap_sort.rb
### 堆的长度为 n ,从节点 i 开始,从顶至底堆化 ###\ndef sift_down(nums, n, i)\n  while true\n    # 判断节点 i, l, r 中值最大的节点,记为 ma\n    l = 2 * i + 1\n    r = 2 * i + 2\n    ma = i\n    ma = l if l < n && nums[l] > nums[ma]\n    ma = r if r < n && nums[r] > nums[ma]\n    # 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出\n    break if ma == i\n    # 交换两节点\n    nums[i], nums[ma] = nums[ma], nums[i]\n    # 循环向下堆化\n    i = ma\n  end\nend\n\n### 堆排序 ###\ndef heap_sort(nums)\n  # 建堆操作:堆化除叶节点以外的其他所有节点\n  (nums.length / 2 - 1).downto(0) do |i|\n    sift_down(nums, nums.length, i)\n  end\n  # 从堆中提取最大元素,循环 n-1 轮\n  (nums.length - 1).downto(1) do |i|\n    # 交换根节点与最右叶节点(交换首元素与尾元素)\n    nums[0], nums[i] = nums[i], nums[0]\n    # 以根节点为起点,从顶至底进行堆化\n    sift_down(nums, i, 0)\n  end\nend\n
可视化运行

全屏观看 >

","path":["第 11 章   排序","11.7   堆排序"],"tags":[]},{"location":"chapter_sorting/heap_sort/#1172","level":2,"title":"11.7.2   算法特性","text":"","path":["第 11 章   排序","11.7   堆排序"],"tags":[]},{"location":"chapter_sorting/insertion_sort/","level":1,"title":"11.4   插入排序","text":"

插入排序(insertion sort)是一种简单的排序算法,它的工作原理与手动整理一副牌的过程非常相似。

具体来说,我们在未排序区间选择一个基准元素,将该元素与其左侧已排序区间的元素逐一比较大小,并将该元素插入到正确的位置。

图 11-6 展示了数组插入元素的操作流程。设基准元素为 base ,我们需要将从目标索引到 base 之间的所有元素向右移动一位,然后将 base 赋值给目标索引。

图 11-6   单次插入操作

","path":["第 11 章   排序","11.4   插入排序"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1141","level":2,"title":"11.4.1   算法流程","text":"

插入排序的整体流程如图 11-7 所示。

  1. 初始状态下,数组的第 1 个元素已完成排序。
  2. 选取数组的第 2 个元素作为 base ,将其插入到正确位置后,数组的前 2 个元素已排序。
  3. 选取第 3 个元素作为 base ,将其插入到正确位置后,数组的前 3 个元素已排序。
  4. 以此类推,在最后一轮中,选取最后一个元素作为 base ,将其插入到正确位置后,所有元素均已排序。

图 11-7   插入排序流程

示例代码如下:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby insertion_sort.py
def insertion_sort(nums: list[int]):\n    \"\"\"插入排序\"\"\"\n    # 外循环:已排序区间为 [0, i-1]\n    for i in range(1, len(nums)):\n        base = nums[i]\n        j = i - 1\n        # 内循环:将 base 插入到已排序区间 [0, i-1] 中的正确位置\n        while j >= 0 and nums[j] > base:\n            nums[j + 1] = nums[j]  # 将 nums[j] 向右移动一位\n            j -= 1\n        nums[j + 1] = base  # 将 base 赋值到正确位置\n
insertion_sort.cpp
/* 插入排序 */\nvoid insertionSort(vector<int> &nums) {\n    // 外循环:已排序区间为 [0, i-1]\n    for (int i = 1; i < nums.size(); i++) {\n        int base = nums[i], j = i - 1;\n        // 内循环:将 base 插入到已排序区间 [0, i-1] 中的正确位置\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // 将 nums[j] 向右移动一位\n            j--;\n        }\n        nums[j + 1] = base; // 将 base 赋值到正确位置\n    }\n}\n
insertion_sort.java
/* 插入排序 */\nvoid insertionSort(int[] nums) {\n    // 外循环:已排序区间为 [0, i-1]\n    for (int i = 1; i < nums.length; i++) {\n        int base = nums[i], j = i - 1;\n        // 内循环:将 base 插入到已排序区间 [0, i-1] 中的正确位置\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // 将 nums[j] 向右移动一位\n            j--;\n        }\n        nums[j + 1] = base;        // 将 base 赋值到正确位置\n    }\n}\n
insertion_sort.cs
/* 插入排序 */\nvoid InsertionSort(int[] nums) {\n    // 外循环:已排序区间为 [0, i-1]\n    for (int i = 1; i < nums.Length; i++) {\n        int bas = nums[i], j = i - 1;\n        // 内循环:将 base 插入到已排序区间 [0, i-1] 中的正确位置\n        while (j >= 0 && nums[j] > bas) {\n            nums[j + 1] = nums[j]; // 将 nums[j] 向右移动一位\n            j--;\n        }\n        nums[j + 1] = bas;         // 将 base 赋值到正确位置\n    }\n}\n
insertion_sort.go
/* 插入排序 */\nfunc insertionSort(nums []int) {\n    // 外循环:已排序区间为 [0, i-1]\n    for i := 1; i < len(nums); i++ {\n        base := nums[i]\n        j := i - 1\n        // 内循环:将 base 插入到已排序区间 [0, i-1] 中的正确位置\n        for j >= 0 && nums[j] > base {\n            nums[j+1] = nums[j] // 将 nums[j] 向右移动一位\n            j--\n        }\n        nums[j+1] = base // 将 base 赋值到正确位置\n    }\n}\n
insertion_sort.swift
/* 插入排序 */\nfunc insertionSort(nums: inout [Int]) {\n    // 外循环:已排序区间为 [0, i-1]\n    for i in nums.indices.dropFirst() {\n        let base = nums[i]\n        var j = i - 1\n        // 内循环:将 base 插入到已排序区间 [0, i-1] 中的正确位置\n        while j >= 0, nums[j] > base {\n            nums[j + 1] = nums[j] // 将 nums[j] 向右移动一位\n            j -= 1\n        }\n        nums[j + 1] = base // 将 base 赋值到正确位置\n    }\n}\n
insertion_sort.js
/* 插入排序 */\nfunction insertionSort(nums) {\n    // 外循环:已排序区间为 [0, i-1]\n    for (let i = 1; i < nums.length; i++) {\n        let base = nums[i],\n            j = i - 1;\n        // 内循环:将 base 插入到已排序区间 [0, i-1] 中的正确位置\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // 将 nums[j] 向右移动一位\n            j--;\n        }\n        nums[j + 1] = base; // 将 base 赋值到正确位置\n    }\n}\n
insertion_sort.ts
/* 插入排序 */\nfunction insertionSort(nums: number[]): void {\n    // 外循环:已排序区间为 [0, i-1]\n    for (let i = 1; i < nums.length; i++) {\n        const base = nums[i];\n        let j = i - 1;\n        // 内循环:将 base 插入到已排序区间 [0, i-1] 中的正确位置\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j]; // 将 nums[j] 向右移动一位\n            j--;\n        }\n        nums[j + 1] = base; // 将 base 赋值到正确位置\n    }\n}\n
insertion_sort.dart
/* 插入排序 */\nvoid insertionSort(List<int> nums) {\n  // 外循环:已排序区间为 [0, i-1]\n  for (int i = 1; i < nums.length; i++) {\n    int base = nums[i], j = i - 1;\n    // 内循环:将 base 插入到已排序区间 [0, i-1] 中的正确位置\n    while (j >= 0 && nums[j] > base) {\n      nums[j + 1] = nums[j]; // 将 nums[j] 向右移动一位\n      j--;\n    }\n    nums[j + 1] = base; // 将 base 赋值到正确位置\n  }\n}\n
insertion_sort.rs
/* 插入排序 */\nfn insertion_sort(nums: &mut [i32]) {\n    // 外循环:已排序区间为 [0, i-1]\n    for i in 1..nums.len() {\n        let (base, mut j) = (nums[i], (i - 1) as i32);\n        // 内循环:将 base 插入到已排序区间 [0, i-1] 中的正确位置\n        while j >= 0 && nums[j as usize] > base {\n            nums[(j + 1) as usize] = nums[j as usize]; // 将 nums[j] 向右移动一位\n            j -= 1;\n        }\n        nums[(j + 1) as usize] = base; // 将 base 赋值到正确位置\n    }\n}\n
insertion_sort.c
/* 插入排序 */\nvoid insertionSort(int nums[], int size) {\n    // 外循环:已排序区间为 [0, i-1]\n    for (int i = 1; i < size; i++) {\n        int base = nums[i], j = i - 1;\n        // 内循环:将 base 插入到已排序区间 [0, i-1] 中的正确位置\n        while (j >= 0 && nums[j] > base) {\n            // 将 nums[j] 向右移动一位\n            nums[j + 1] = nums[j];\n            j--;\n        }\n        // 将 base 赋值到正确位置\n        nums[j + 1] = base;\n    }\n}\n
insertion_sort.kt
/* 插入排序 */\nfun insertionSort(nums: IntArray) {\n    //外循环: 已排序元素为 1, 2, ..., n\n    for (i in nums.indices) {\n        val base = nums[i]\n        var j = i - 1\n        // 内循环:将 base 插入到已排序区间 [0, i-1] 中的正确位置\n        while (j >= 0 && nums[j] > base) {\n            nums[j + 1] = nums[j] // 将 nums[j] 向右移动一位\n            j--\n        }\n        nums[j + 1] = base        // 将 base 赋值到正确位置\n    }\n}\n
insertion_sort.rb
### 插入排序 ###\ndef insertion_sort(nums)\n  n = nums.length\n  # 外循环:已排序区间为 [0, i-1]\n  for i in 1...n\n    base = nums[i]\n    j = i - 1\n    # 内循环:将 base 插入到已排序区间 [0, i-1] 中的正确位置\n    while j >= 0 && nums[j] > base\n      nums[j + 1] = nums[j] # 将 nums[j] 向右移动一位\n      j -= 1\n    end\n    nums[j + 1] = base # 将 base 赋值到正确位置\n  end\nend\n
可视化运行

全屏观看 >

","path":["第 11 章   排序","11.4   插入排序"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1142","level":2,"title":"11.4.2   算法特性","text":"","path":["第 11 章   排序","11.4   插入排序"],"tags":[]},{"location":"chapter_sorting/insertion_sort/#1143","level":2,"title":"11.4.3   插入排序的优势","text":"

插入排序的时间复杂度为 \\(O(n^2)\\) ,而我们即将学习的快速排序的时间复杂度为 \\(O(n \\log n)\\) 。尽管插入排序的时间复杂度更高,但在数据量较小的情况下,插入排序通常更快。

这个结论与线性查找和二分查找的适用情况的结论类似。快速排序这类 \\(O(n \\log n)\\) 的算法属于基于分治策略的排序算法,往往包含更多单元计算操作。而在数据量较小时,\\(n^2\\) 和 \\(n \\log n\\) 的数值比较接近,复杂度不占主导地位,每轮中的单元操作数量起到决定性作用。

实际上,许多编程语言(例如 Java)的内置排序函数采用了插入排序,大致思路为:对于长数组,采用基于分治策略的排序算法,例如快速排序;对于短数组,直接使用插入排序。

虽然冒泡排序、选择排序和插入排序的时间复杂度都为 \\(O(n^2)\\) ,但在实际情况中,插入排序的使用频率显著高于冒泡排序和选择排序,主要有以下原因。

","path":["第 11 章   排序","11.4   插入排序"],"tags":[]},{"location":"chapter_sorting/merge_sort/","level":1,"title":"11.6   归并排序","text":"

归并排序(merge sort)是一种基于分治策略的排序算法,包含图 11-10 所示的“划分”和“合并”阶段。

  1. 划分阶段:通过递归不断地将数组从中点处分开,将长数组的排序问题转换为短数组的排序问题。
  2. 合并阶段:当子数组长度为 1 时终止划分,开始合并,持续地将左右两个较短的有序数组合并为一个较长的有序数组,直至结束。

图 11-10   归并排序的划分与合并阶段

","path":["第 11 章   排序","11.6   归并排序"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1161","level":2,"title":"11.6.1   算法流程","text":"

如图 11-11 所示,“划分阶段”从顶至底递归地将数组从中点切分为两个子数组。

  1. 计算数组中点 mid ,递归划分左子数组(区间 [left, mid] )和右子数组(区间 [mid + 1, right] )。
  2. 递归执行步骤 1. ,直至子数组区间长度为 1 时终止。

“合并阶段”从底至顶地将左子数组和右子数组合并为一个有序数组。需要注意的是,从长度为 1 的子数组开始合并,合并阶段中的每个子数组都是有序的。

<1><2><3><4><5><6><7><8><9><10>

图 11-11   归并排序步骤

观察发现,归并排序与二叉树后序遍历的递归顺序是一致的。

归并排序的实现如以下代码所示。请注意,nums 的待合并区间为 [left, right] ,而 tmp 的对应区间为 [0, right - left]

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby merge_sort.py
def merge(nums: list[int], left: int, mid: int, right: int):\n    \"\"\"合并左子数组和右子数组\"\"\"\n    # 左子数组区间为 [left, mid], 右子数组区间为 [mid+1, right]\n    # 创建一个临时数组 tmp ,用于存放合并后的结果\n    tmp = [0] * (right - left + 1)\n    # 初始化左子数组和右子数组的起始索引\n    i, j, k = left, mid + 1, 0\n    # 当左右子数组都还有元素时,进行比较并将较小的元素复制到临时数组中\n    while i <= mid and j <= right:\n        if nums[i] <= nums[j]:\n            tmp[k] = nums[i]\n            i += 1\n        else:\n            tmp[k] = nums[j]\n            j += 1\n        k += 1\n    # 将左子数组和右子数组的剩余元素复制到临时数组中\n    while i <= mid:\n        tmp[k] = nums[i]\n        i += 1\n        k += 1\n    while j <= right:\n        tmp[k] = nums[j]\n        j += 1\n        k += 1\n    # 将临时数组 tmp 中的元素复制回原数组 nums 的对应区间\n    for k in range(0, len(tmp)):\n        nums[left + k] = tmp[k]\n\ndef merge_sort(nums: list[int], left: int, right: int):\n    \"\"\"归并排序\"\"\"\n    # 终止条件\n    if left >= right:\n        return  # 当子数组长度为 1 时终止递归\n    # 划分阶段\n    mid = (left + right) // 2 # 计算中点\n    merge_sort(nums, left, mid)  # 递归左子数组\n    merge_sort(nums, mid + 1, right)  # 递归右子数组\n    # 合并阶段\n    merge(nums, left, mid, right)\n
merge_sort.cpp
/* 合并左子数组和右子数组 */\nvoid merge(vector<int> &nums, int left, int mid, int right) {\n    // 左子数组区间为 [left, mid], 右子数组区间为 [mid+1, right]\n    // 创建一个临时数组 tmp ,用于存放合并后的结果\n    vector<int> tmp(right - left + 1);\n    // 初始化左子数组和右子数组的起始索引\n    int i = left, j = mid + 1, k = 0;\n    // 当左右子数组都还有元素时,进行比较并将较小的元素复制到临时数组中\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j])\n            tmp[k++] = nums[i++];\n        else\n            tmp[k++] = nums[j++];\n    }\n    // 将左子数组和右子数组的剩余元素复制到临时数组中\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // 将临时数组 tmp 中的元素复制回原数组 nums 的对应区间\n    for (k = 0; k < tmp.size(); k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* 归并排序 */\nvoid mergeSort(vector<int> &nums, int left, int right) {\n    // 终止条件\n    if (left >= right)\n        return; // 当子数组长度为 1 时终止递归\n    // 划分阶段\n    int mid = left + (right - left) / 2;    // 计算中点\n    mergeSort(nums, left, mid);      // 递归左子数组\n    mergeSort(nums, mid + 1, right); // 递归右子数组\n    // 合并阶段\n    merge(nums, left, mid, right);\n}\n
merge_sort.java
/* 合并左子数组和右子数组 */\nvoid merge(int[] nums, int left, int mid, int right) {\n    // 左子数组区间为 [left, mid], 右子数组区间为 [mid+1, right]\n    // 创建一个临时数组 tmp ,用于存放合并后的结果\n    int[] tmp = new int[right - left + 1];\n    // 初始化左子数组和右子数组的起始索引\n    int i = left, j = mid + 1, k = 0;\n    // 当左右子数组都还有元素时,进行比较并将较小的元素复制到临时数组中\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j])\n            tmp[k++] = nums[i++];\n        else\n            tmp[k++] = nums[j++];\n    }\n    // 将左子数组和右子数组的剩余元素复制到临时数组中\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // 将临时数组 tmp 中的元素复制回原数组 nums 的对应区间\n    for (k = 0; k < tmp.length; k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* 归并排序 */\nvoid mergeSort(int[] nums, int left, int right) {\n    // 终止条件\n    if (left >= right)\n        return; // 当子数组长度为 1 时终止递归\n    // 划分阶段\n    int mid = left + (right - left) / 2; // 计算中点\n    mergeSort(nums, left, mid); // 递归左子数组\n    mergeSort(nums, mid + 1, right); // 递归右子数组\n    // 合并阶段\n    merge(nums, left, mid, right);\n}\n
merge_sort.cs
/* 合并左子数组和右子数组 */\nvoid Merge(int[] nums, int left, int mid, int right) {\n    // 左子数组区间为 [left, mid], 右子数组区间为 [mid+1, right]\n    // 创建一个临时数组 tmp ,用于存放合并后的结果\n    int[] tmp = new int[right - left + 1];\n    // 初始化左子数组和右子数组的起始索引\n    int i = left, j = mid + 1, k = 0;\n    // 当左右子数组都还有元素时,进行比较并将较小的元素复制到临时数组中\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j])\n            tmp[k++] = nums[i++];\n        else\n            tmp[k++] = nums[j++];\n    }\n    // 将左子数组和右子数组的剩余元素复制到临时数组中\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // 将临时数组 tmp 中的元素复制回原数组 nums 的对应区间\n    for (k = 0; k < tmp.Length; ++k) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* 归并排序 */\nvoid MergeSort(int[] nums, int left, int right) {\n    // 终止条件\n    if (left >= right) return;       // 当子数组长度为 1 时终止递归\n    // 划分阶段\n    int mid = left + (right - left) / 2;    // 计算中点\n    MergeSort(nums, left, mid);      // 递归左子数组\n    MergeSort(nums, mid + 1, right); // 递归右子数组\n    // 合并阶段\n    Merge(nums, left, mid, right);\n}\n
merge_sort.go
/* 合并左子数组和右子数组 */\nfunc merge(nums []int, left, mid, right int) {\n    // 左子数组区间为 [left, mid], 右子数组区间为 [mid+1, right]\n    // 创建一个临时数组 tmp ,用于存放合并后的结果\n    tmp := make([]int, right-left+1)\n    // 初始化左子数组和右子数组的起始索引\n    i, j, k := left, mid+1, 0\n    // 当左右子数组都还有元素时,进行比较并将较小的元素复制到临时数组中\n    for i <= mid && j <= right {\n        if nums[i] <= nums[j] {\n            tmp[k] = nums[i]\n            i++\n        } else {\n            tmp[k] = nums[j]\n            j++\n        }\n        k++\n    }\n    // 将左子数组和右子数组的剩余元素复制到临时数组中\n    for i <= mid {\n        tmp[k] = nums[i]\n        i++\n        k++\n    }\n    for j <= right {\n        tmp[k] = nums[j]\n        j++\n        k++\n    }\n    // 将临时数组 tmp 中的元素复制回原数组 nums 的对应区间\n    for k := 0; k < len(tmp); k++ {\n        nums[left+k] = tmp[k]\n    }\n}\n\n/* 归并排序 */\nfunc mergeSort(nums []int, left, right int) {\n    // 终止条件\n    if left >= right {\n        return\n    }\n    // 划分阶段\n    mid := left + (right - left) / 2\n    mergeSort(nums, left, mid)\n    mergeSort(nums, mid+1, right)\n    // 合并阶段\n    merge(nums, left, mid, right)\n}\n
merge_sort.swift
/* 合并左子数组和右子数组 */\nfunc merge(nums: inout [Int], left: Int, mid: Int, right: Int) {\n    // 左子数组区间为 [left, mid], 右子数组区间为 [mid+1, right]\n    // 创建一个临时数组 tmp ,用于存放合并后的结果\n    var tmp = Array(repeating: 0, count: right - left + 1)\n    // 初始化左子数组和右子数组的起始索引\n    var i = left, j = mid + 1, k = 0\n    // 当左右子数组都还有元素时,进行比较并将较小的元素复制到临时数组中\n    while i <= mid, j <= right {\n        if nums[i] <= nums[j] {\n            tmp[k] = nums[i]\n            i += 1\n        } else {\n            tmp[k] = nums[j]\n            j += 1\n        }\n        k += 1\n    }\n    // 将左子数组和右子数组的剩余元素复制到临时数组中\n    while i <= mid {\n        tmp[k] = nums[i]\n        i += 1\n        k += 1\n    }\n    while j <= right {\n        tmp[k] = nums[j]\n        j += 1\n        k += 1\n    }\n    // 将临时数组 tmp 中的元素复制回原数组 nums 的对应区间\n    for k in tmp.indices {\n        nums[left + k] = tmp[k]\n    }\n}\n\n/* 归并排序 */\nfunc mergeSort(nums: inout [Int], left: Int, right: Int) {\n    // 终止条件\n    if left >= right { // 当子数组长度为 1 时终止递归\n        return\n    }\n    // 划分阶段\n    let mid = left + (right - left) / 2 // 计算中点\n    mergeSort(nums: &nums, left: left, right: mid) // 递归左子数组\n    mergeSort(nums: &nums, left: mid + 1, right: right) // 递归右子数组\n    // 合并阶段\n    merge(nums: &nums, left: left, mid: mid, right: right)\n}\n
merge_sort.js
/* 合并左子数组和右子数组 */\nfunction merge(nums, left, mid, right) {\n    // 左子数组区间为 [left, mid], 右子数组区间为 [mid+1, right]\n    // 创建一个临时数组 tmp ,用于存放合并后的结果\n    const tmp = new Array(right - left + 1);\n    // 初始化左子数组和右子数组的起始索引\n    let i = left,\n        j = mid + 1,\n        k = 0;\n    // 当左右子数组都还有元素时,进行比较并将较小的元素复制到临时数组中\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j]) {\n            tmp[k++] = nums[i++];\n        } else {\n            tmp[k++] = nums[j++];\n        }\n    }\n    // 将左子数组和右子数组的剩余元素复制到临时数组中\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // 将临时数组 tmp 中的元素复制回原数组 nums 的对应区间\n    for (k = 0; k < tmp.length; k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* 归并排序 */\nfunction mergeSort(nums, left, right) {\n    // 终止条件\n    if (left >= right) return; // 当子数组长度为 1 时终止递归\n    // 划分阶段\n    let mid = Math.floor(left + (right - left) / 2); // 计算中点\n    mergeSort(nums, left, mid); // 递归左子数组\n    mergeSort(nums, mid + 1, right); // 递归右子数组\n    // 合并阶段\n    merge(nums, left, mid, right);\n}\n
merge_sort.ts
/* 合并左子数组和右子数组 */\nfunction merge(nums: number[], left: number, mid: number, right: number): void {\n    // 左子数组区间为 [left, mid], 右子数组区间为 [mid+1, right]\n    // 创建一个临时数组 tmp ,用于存放合并后的结果\n    const tmp = new Array(right - left + 1);\n    // 初始化左子数组和右子数组的起始索引\n    let i = left,\n        j = mid + 1,\n        k = 0;\n    // 当左右子数组都还有元素时,进行比较并将较小的元素复制到临时数组中\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j]) {\n            tmp[k++] = nums[i++];\n        } else {\n            tmp[k++] = nums[j++];\n        }\n    }\n    // 将左子数组和右子数组的剩余元素复制到临时数组中\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // 将临时数组 tmp 中的元素复制回原数组 nums 的对应区间\n    for (k = 0; k < tmp.length; k++) {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* 归并排序 */\nfunction mergeSort(nums: number[], left: number, right: number): void {\n    // 终止条件\n    if (left >= right) return; // 当子数组长度为 1 时终止递归\n    // 划分阶段\n    let mid = Math.floor(left + (right - left) / 2); // 计算中点\n    mergeSort(nums, left, mid); // 递归左子数组\n    mergeSort(nums, mid + 1, right); // 递归右子数组\n    // 合并阶段\n    merge(nums, left, mid, right);\n}\n
merge_sort.dart
/* 合并左子数组和右子数组 */\nvoid merge(List<int> nums, int left, int mid, int right) {\n  // 左子数组区间为 [left, mid], 右子数组区间为 [mid+1, right]\n  // 创建一个临时数组 tmp ,用于存放合并后的结果\n  List<int> tmp = List.filled(right - left + 1, 0);\n  // 初始化左子数组和右子数组的起始索引\n  int i = left, j = mid + 1, k = 0;\n  // 当左右子数组都还有元素时,进行比较并将较小的元素复制到临时数组中\n  while (i <= mid && j <= right) {\n    if (nums[i] <= nums[j])\n      tmp[k++] = nums[i++];\n    else\n      tmp[k++] = nums[j++];\n  }\n  // 将左子数组和右子数组的剩余元素复制到临时数组中\n  while (i <= mid) {\n    tmp[k++] = nums[i++];\n  }\n  while (j <= right) {\n    tmp[k++] = nums[j++];\n  }\n  // 将临时数组 tmp 中的元素复制回原数组 nums 的对应区间\n  for (k = 0; k < tmp.length; k++) {\n    nums[left + k] = tmp[k];\n  }\n}\n\n/* 归并排序 */\nvoid mergeSort(List<int> nums, int left, int right) {\n  // 终止条件\n  if (left >= right) return; // 当子数组长度为 1 时终止递归\n  // 划分阶段\n  int mid = left + (right - left) ~/ 2; // 计算中点\n  mergeSort(nums, left, mid); // 递归左子数组\n  mergeSort(nums, mid + 1, right); // 递归右子数组\n  // 合并阶段\n  merge(nums, left, mid, right);\n}\n
merge_sort.rs
/* 合并左子数组和右子数组 */\nfn merge(nums: &mut [i32], left: usize, mid: usize, right: usize) {\n    // 左子数组区间为 [left, mid], 右子数组区间为 [mid+1, right]\n    // 创建一个临时数组 tmp ,用于存放合并后的结果\n    let tmp_size = right - left + 1;\n    let mut tmp = vec![0; tmp_size];\n    // 初始化左子数组和右子数组的起始索引\n    let (mut i, mut j, mut k) = (left, mid + 1, 0);\n    // 当左右子数组都还有元素时,进行比较并将较小的元素复制到临时数组中\n    while i <= mid && j <= right {\n        if nums[i] <= nums[j] {\n            tmp[k] = nums[i];\n            i += 1;\n        } else {\n            tmp[k] = nums[j];\n            j += 1;\n        }\n        k += 1;\n    }\n    // 将左子数组和右子数组的剩余元素复制到临时数组中\n    while i <= mid {\n        tmp[k] = nums[i];\n        k += 1;\n        i += 1;\n    }\n    while j <= right {\n        tmp[k] = nums[j];\n        k += 1;\n        j += 1;\n    }\n    // 将临时数组 tmp 中的元素复制回原数组 nums 的对应区间\n    for k in 0..tmp_size {\n        nums[left + k] = tmp[k];\n    }\n}\n\n/* 归并排序 */\nfn merge_sort(nums: &mut [i32], left: usize, right: usize) {\n    // 终止条件\n    if left >= right {\n        return; // 当子数组长度为 1 时终止递归\n    }\n\n    // 划分阶段\n    let mid = left + (right - left) / 2; // 计算中点\n    merge_sort(nums, left, mid); // 递归左子数组\n    merge_sort(nums, mid + 1, right); // 递归右子数组\n\n    // 合并阶段\n    merge(nums, left, mid, right);\n}\n
merge_sort.c
/* 合并左子数组和右子数组 */\nvoid merge(int *nums, int left, int mid, int right) {\n    // 左子数组区间为 [left, mid], 右子数组区间为 [mid+1, right]\n    // 创建一个临时数组 tmp ,用于存放合并后的结果\n    int tmpSize = right - left + 1;\n    int *tmp = (int *)malloc(tmpSize * sizeof(int));\n    // 初始化左子数组和右子数组的起始索引\n    int i = left, j = mid + 1, k = 0;\n    // 当左右子数组都还有元素时,进行比较并将较小的元素复制到临时数组中\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j]) {\n            tmp[k++] = nums[i++];\n        } else {\n            tmp[k++] = nums[j++];\n        }\n    }\n    // 将左子数组和右子数组的剩余元素复制到临时数组中\n    while (i <= mid) {\n        tmp[k++] = nums[i++];\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++];\n    }\n    // 将临时数组 tmp 中的元素复制回原数组 nums 的对应区间\n    for (k = 0; k < tmpSize; ++k) {\n        nums[left + k] = tmp[k];\n    }\n    // 释放内存\n    free(tmp);\n}\n\n/* 归并排序 */\nvoid mergeSort(int *nums, int left, int right) {\n    // 终止条件\n    if (left >= right)\n        return; // 当子数组长度为 1 时终止递归\n    // 划分阶段\n    int mid = left + (right - left) / 2;    // 计算中点\n    mergeSort(nums, left, mid);      // 递归左子数组\n    mergeSort(nums, mid + 1, right); // 递归右子数组\n    // 合并阶段\n    merge(nums, left, mid, right);\n}\n
merge_sort.kt
/* 合并左子数组和右子数组 */\nfun merge(nums: IntArray, left: Int, mid: Int, right: Int) {\n    // 左子数组区间为 [left, mid], 右子数组区间为 [mid+1, right]\n    // 创建一个临时数组 tmp ,用于存放合并后的结果\n    val tmp = IntArray(right - left + 1)\n    // 初始化左子数组和右子数组的起始索引\n    var i = left\n    var j = mid + 1\n    var k = 0\n    // 当左右子数组都还有元素时,进行比较并将较小的元素复制到临时数组中\n    while (i <= mid && j <= right) {\n        if (nums[i] <= nums[j])\n            tmp[k++] = nums[i++]\n        else\n            tmp[k++] = nums[j++]\n    }\n    // 将左子数组和右子数组的剩余元素复制到临时数组中\n    while (i <= mid) {\n        tmp[k++] = nums[i++]\n    }\n    while (j <= right) {\n        tmp[k++] = nums[j++]\n    }\n    // 将临时数组 tmp 中的元素复制回原数组 nums 的对应区间\n    for (l in tmp.indices) {\n        nums[left + l] = tmp[l]\n    }\n}\n\n/* 归并排序 */\nfun mergeSort(nums: IntArray, left: Int, right: Int) {\n    // 终止条件\n    if (left >= right) return  // 当子数组长度为 1 时终止递归\n    // 划分阶段\n    val mid = left + (right - left) / 2 // 计算中点\n    mergeSort(nums, left, mid) // 递归左子数组\n    mergeSort(nums, mid + 1, right) // 递归右子数组\n    // 合并阶段\n    merge(nums, left, mid, right)\n}\n
merge_sort.rb
### 合并左子数组和右子数组 ###\ndef merge(nums, left, mid, right)\n  # 左子数组区间为 [left, mid], 右子数组区间为 [mid+1, right]\n  # 创建一个临时数组 tmp,用于存放合并后的结果\n  tmp = Array.new(right - left + 1, 0)\n  # 初始化左子数组和右子数组的起始索引\n  i, j, k = left, mid + 1, 0\n  # 当左右子数组都还有元素时,进行比较并将较小的元素复制到临时数组中\n  while i <= mid && j <= right\n    if nums[i] <= nums[j]\n      tmp[k] = nums[i]\n      i += 1\n    else\n      tmp[k] = nums[j]\n      j += 1\n    end\n    k += 1\n  end\n  # 将左子数组和右子数组的剩余元素复制到临时数组中\n  while i <= mid\n    tmp[k] = nums[i]\n    i += 1\n    k += 1\n  end\n  while j <= right\n    tmp[k] = nums[j]\n    j += 1\n    k += 1\n  end\n  # 将临时数组 tmp 中的元素复制回原数组 nums 的对应区间\n  (0...tmp.length).each do |k|\n    nums[left + k] = tmp[k]\n  end\nend\n\n### 归并排序 ###\ndef merge_sort(nums, left, right)\n  # 终止条件\n  # 当子数组长度为 1 时终止递归\n  return if left >= right\n  # 划分阶段\n  mid = left + (right - left) / 2 # 计算中点\n  merge_sort(nums, left, mid) # 递归左子数组\n  merge_sort(nums, mid + 1, right) # 递归右子数组\n  # 合并阶段\n  merge(nums, left, mid, right)\nend\n
可视化运行

全屏观看 >

","path":["第 11 章   排序","11.6   归并排序"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1162","level":2,"title":"11.6.2   算法特性","text":"","path":["第 11 章   排序","11.6   归并排序"],"tags":[]},{"location":"chapter_sorting/merge_sort/#1163","level":2,"title":"11.6.3   链表排序","text":"

对于链表,归并排序相较于其他排序算法具有显著优势,可以将链表排序任务的空间复杂度优化至 \\(O(1)\\) 。

具体实现细节比较复杂,有兴趣的读者可以查阅相关资料进行学习。

","path":["第 11 章   排序","11.6   归并排序"],"tags":[]},{"location":"chapter_sorting/quick_sort/","level":1,"title":"11.5   快速排序","text":"

快速排序(quick sort)是一种基于分治策略的排序算法,运行高效,应用广泛。

快速排序的核心操作是“哨兵划分”,其目标是:选择数组中的某个元素作为“基准数”,将所有小于基准数的元素移到其左侧,而大于基准数的元素移到其右侧。具体来说,哨兵划分的流程如图 11-8 所示。

  1. 选取数组最左端元素作为基准数,初始化两个指针 ij 分别指向数组的两端。
  2. 设置一个循环,在每轮中使用 ij)分别寻找第一个比基准数大(小)的元素,然后交换这两个元素。
  3. 循环执行步骤 2. ,直到 ij 相遇时停止,最后将基准数交换至两个子数组的分界线。
<1><2><3><4><5><6><7><8><9>

图 11-8   哨兵划分步骤

哨兵划分完成后,原数组被划分成三部分:左子数组、基准数、右子数组,且满足“左子数组任意元素 \\(\\leq\\) 基准数 \\(\\leq\\) 右子数组任意元素”。因此,我们接下来只需对这两个子数组进行排序。

快速排序的分治策略

哨兵划分的实质是将一个较长数组的排序问题简化为两个较短数组的排序问题。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
def partition(self, nums: list[int], left: int, right: int) -> int:\n    \"\"\"哨兵划分\"\"\"\n    # 以 nums[left] 为基准数\n    i, j = left, right\n    while i < j:\n        while i < j and nums[j] >= nums[left]:\n            j -= 1  # 从右向左找首个小于基准数的元素\n        while i < j and nums[i] <= nums[left]:\n            i += 1  # 从左向右找首个大于基准数的元素\n        # 元素交换\n        nums[i], nums[j] = nums[j], nums[i]\n    # 将基准数交换至两子数组的分界线\n    nums[i], nums[left] = nums[left], nums[i]\n    return i  # 返回基准数的索引\n
quick_sort.cpp
/* 哨兵划分 */\nint partition(vector<int> &nums, int left, int right) {\n    // 以 nums[left] 为基准数\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;                // 从右向左找首个小于基准数的元素\n        while (i < j && nums[i] <= nums[left])\n            i++;                // 从左向右找首个大于基准数的元素\n        swap(nums[i], nums[j]); // 交换这两个元素\n    }\n    swap(nums[i], nums[left]);  // 将基准数交换至两子数组的分界线\n    return i;                   // 返回基准数的索引\n}\n
quick_sort.java
/* 元素交换 */\nvoid swap(int[] nums, int i, int j) {\n    int tmp = nums[i];\n    nums[i] = nums[j];\n    nums[j] = tmp;\n}\n\n/* 哨兵划分 */\nint partition(int[] nums, int left, int right) {\n    // 以 nums[left] 为基准数\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // 从右向左找首个小于基准数的元素\n        while (i < j && nums[i] <= nums[left])\n            i++;          // 从左向右找首个大于基准数的元素\n        swap(nums, i, j); // 交换这两个元素\n    }\n    swap(nums, i, left);  // 将基准数交换至两子数组的分界线\n    return i;             // 返回基准数的索引\n}\n
quick_sort.cs
/* 元素交换 */\nvoid Swap(int[] nums, int i, int j) {\n    (nums[j], nums[i]) = (nums[i], nums[j]);\n}\n\n/* 哨兵划分 */\nint Partition(int[] nums, int left, int right) {\n    // 以 nums[left] 为基准数\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // 从右向左找首个小于基准数的元素\n        while (i < j && nums[i] <= nums[left])\n            i++;          // 从左向右找首个大于基准数的元素\n        Swap(nums, i, j); // 交换这两个元素\n    }\n    Swap(nums, i, left);  // 将基准数交换至两子数组的分界线\n    return i;             // 返回基准数的索引\n}\n
quick_sort.go
/* 哨兵划分 */\nfunc (q *quickSort) partition(nums []int, left, right int) int {\n    // 以 nums[left] 为基准数\n    i, j := left, right\n    for i < j {\n        for i < j && nums[j] >= nums[left] {\n            j-- // 从右向左找首个小于基准数的元素\n        }\n        for i < j && nums[i] <= nums[left] {\n            i++ // 从左向右找首个大于基准数的元素\n        }\n        // 元素交换\n        nums[i], nums[j] = nums[j], nums[i]\n    }\n    // 将基准数交换至两子数组的分界线\n    nums[i], nums[left] = nums[left], nums[i]\n    return i // 返回基准数的索引\n}\n
quick_sort.swift
/* 哨兵划分 */\nfunc partition(nums: inout [Int], left: Int, right: Int) -> Int {\n    // 以 nums[left] 为基准数\n    var i = left\n    var j = right\n    while i < j {\n        while i < j, nums[j] >= nums[left] {\n            j -= 1 // 从右向左找首个小于基准数的元素\n        }\n        while i < j, nums[i] <= nums[left] {\n            i += 1 // 从左向右找首个大于基准数的元素\n        }\n        nums.swapAt(i, j) // 交换这两个元素\n    }\n    nums.swapAt(i, left) // 将基准数交换至两子数组的分界线\n    return i // 返回基准数的索引\n}\n
quick_sort.js
/* 元素交换 */\nswap(nums, i, j) {\n    let tmp = nums[i];\n    nums[i] = nums[j];\n    nums[j] = tmp;\n}\n\n/* 哨兵划分 */\npartition(nums, left, right) {\n    // 以 nums[left] 为基准数\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j -= 1; // 从右向左找首个小于基准数的元素\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i += 1; // 从左向右找首个大于基准数的元素\n        }\n        // 元素交换\n        this.swap(nums, i, j); // 交换这两个元素\n    }\n    this.swap(nums, i, left); // 将基准数交换至两子数组的分界线\n    return i; // 返回基准数的索引\n}\n
quick_sort.ts
/* 元素交换 */\nswap(nums: number[], i: number, j: number): void {\n    let tmp = nums[i];\n    nums[i] = nums[j];\n    nums[j] = tmp;\n}\n\n/* 哨兵划分 */\npartition(nums: number[], left: number, right: number): number {\n    // 以 nums[left] 为基准数\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j -= 1; // 从右向左找首个小于基准数的元素\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i += 1; // 从左向右找首个大于基准数的元素\n        }\n        // 元素交换\n        this.swap(nums, i, j); // 交换这两个元素\n    }\n    this.swap(nums, i, left); // 将基准数交换至两子数组的分界线\n    return i; // 返回基准数的索引\n}\n
quick_sort.dart
/* 元素交换 */\nvoid _swap(List<int> nums, int i, int j) {\n  int tmp = nums[i];\n  nums[i] = nums[j];\n  nums[j] = tmp;\n}\n\n/* 哨兵划分 */\nint _partition(List<int> nums, int left, int right) {\n  // 以 nums[left] 为基准数\n  int i = left, j = right;\n  while (i < j) {\n    while (i < j && nums[j] >= nums[left]) j--; // 从右向左找首个小于基准数的元素\n    while (i < j && nums[i] <= nums[left]) i++; // 从左向右找首个大于基准数的元素\n    _swap(nums, i, j); // 交换这两个元素\n  }\n  _swap(nums, i, left); // 将基准数交换至两子数组的分界线\n  return i; // 返回基准数的索引\n}\n
quick_sort.rs
/* 哨兵划分 */\nfn partition(nums: &mut [i32], left: usize, right: usize) -> usize {\n    // 以 nums[left] 为基准数\n    let (mut i, mut j) = (left, right);\n    while i < j {\n        while i < j && nums[j] >= nums[left] {\n            j -= 1; // 从右向左找首个小于基准数的元素\n        }\n        while i < j && nums[i] <= nums[left] {\n            i += 1; // 从左向右找首个大于基准数的元素\n        }\n        nums.swap(i, j); // 交换这两个元素\n    }\n    nums.swap(i, left); // 将基准数交换至两子数组的分界线\n    i // 返回基准数的索引\n}\n
quick_sort.c
/* 元素交换 */\nvoid swap(int nums[], int i, int j) {\n    int tmp = nums[i];\n    nums[i] = nums[j];\n    nums[j] = tmp;\n}\n\n/* 哨兵划分 */\nint partition(int nums[], int left, int right) {\n    // 以 nums[left] 为基准数\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j--; // 从右向左找首个小于基准数的元素\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i++; // 从左向右找首个大于基准数的元素\n        }\n        // 交换这两个元素\n        swap(nums, i, j);\n    }\n    // 将基准数交换至两子数组的分界线\n    swap(nums, i, left);\n    // 返回基准数的索引\n    return i;\n}\n
quick_sort.kt
/* 元素交换 */\nfun swap(nums: IntArray, i: Int, j: Int) {\n    val temp = nums[i]\n    nums[i] = nums[j]\n    nums[j] = temp\n}\n\n/* 哨兵划分 */\nfun partition(nums: IntArray, left: Int, right: Int): Int {\n    // 以 nums[left] 为基准数\n    var i = left\n    var j = right\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--           // 从右向左找首个小于基准数的元素\n        while (i < j && nums[i] <= nums[left])\n            i++           // 从左向右找首个大于基准数的元素\n        swap(nums, i, j)  // 交换这两个元素\n    }\n    swap(nums, i, left)   // 将基准数交换至两子数组的分界线\n    return i              // 返回基准数的索引\n}\n
quick_sort.rb
### 哨兵划分 ###\ndef partition(nums, left, right)\n  # 以 nums[left] 为基准数\n  i, j = left, right\n  while i < j\n    while i < j && nums[j] >= nums[left]\n      j -= 1 # 从右向左找首个小于基准数的元素\n    end\n    while i < j && nums[i] <= nums[left]\n      i += 1 # 从左向右找首个大于基准数的元素\n    end\n    # 元素交换\n    nums[i], nums[j] = nums[j], nums[i]\n  end\n  # 将基准数交换至两子数组的分界线\n  nums[i], nums[left] = nums[left], nums[i]\n  i # 返回基准数的索引\nend\n
可视化运行

全屏观看 >

","path":["第 11 章   排序","11.5   快速排序"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1151","level":2,"title":"11.5.1   算法流程","text":"

快速排序的整体流程如图 11-9 所示。

  1. 首先,对原数组执行一次“哨兵划分”,得到未排序的左子数组和右子数组。
  2. 然后,对左子数组和右子数组分别递归执行“哨兵划分”。
  3. 持续递归,直至子数组长度为 1 时终止,从而完成整个数组的排序。

图 11-9   快速排序流程

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
def quick_sort(self, nums: list[int], left: int, right: int):\n    \"\"\"快速排序\"\"\"\n    # 子数组长度为 1 时终止递归\n    if left >= right:\n        return\n    # 哨兵划分\n    pivot = self.partition(nums, left, right)\n    # 递归左子数组、右子数组\n    self.quick_sort(nums, left, pivot - 1)\n    self.quick_sort(nums, pivot + 1, right)\n
quick_sort.cpp
/* 快速排序 */\nvoid quickSort(vector<int> &nums, int left, int right) {\n    // 子数组长度为 1 时终止递归\n    if (left >= right)\n        return;\n    // 哨兵划分\n    int pivot = partition(nums, left, right);\n    // 递归左子数组、右子数组\n    quickSort(nums, left, pivot - 1);\n    quickSort(nums, pivot + 1, right);\n}\n
quick_sort.java
/* 快速排序 */\nvoid quickSort(int[] nums, int left, int right) {\n    // 子数组长度为 1 时终止递归\n    if (left >= right)\n        return;\n    // 哨兵划分\n    int pivot = partition(nums, left, right);\n    // 递归左子数组、右子数组\n    quickSort(nums, left, pivot - 1);\n    quickSort(nums, pivot + 1, right);\n}\n
quick_sort.cs
/* 快速排序 */\nvoid QuickSort(int[] nums, int left, int right) {\n    // 子数组长度为 1 时终止递归\n    if (left >= right)\n        return;\n    // 哨兵划分\n    int pivot = Partition(nums, left, right);\n    // 递归左子数组、右子数组\n    QuickSort(nums, left, pivot - 1);\n    QuickSort(nums, pivot + 1, right);\n}\n
quick_sort.go
/* 快速排序 */\nfunc (q *quickSort) quickSort(nums []int, left, right int) {\n    // 子数组长度为 1 时终止递归\n    if left >= right {\n        return\n    }\n    // 哨兵划分\n    pivot := q.partition(nums, left, right)\n    // 递归左子数组、右子数组\n    q.quickSort(nums, left, pivot-1)\n    q.quickSort(nums, pivot+1, right)\n}\n
quick_sort.swift
/* 快速排序 */\nfunc quickSort(nums: inout [Int], left: Int, right: Int) {\n    // 子数组长度为 1 时终止递归\n    if left >= right {\n        return\n    }\n    // 哨兵划分\n    let pivot = partition(nums: &nums, left: left, right: right)\n    // 递归左子数组、右子数组\n    quickSort(nums: &nums, left: left, right: pivot - 1)\n    quickSort(nums: &nums, left: pivot + 1, right: right)\n}\n
quick_sort.js
/* 快速排序 */\nquickSort(nums, left, right) {\n    // 子数组长度为 1 时终止递归\n    if (left >= right) return;\n    // 哨兵划分\n    const pivot = this.partition(nums, left, right);\n    // 递归左子数组、右子数组\n    this.quickSort(nums, left, pivot - 1);\n    this.quickSort(nums, pivot + 1, right);\n}\n
quick_sort.ts
/* 快速排序 */\nquickSort(nums: number[], left: number, right: number): void {\n    // 子数组长度为 1 时终止递归\n    if (left >= right) {\n        return;\n    }\n    // 哨兵划分\n    const pivot = this.partition(nums, left, right);\n    // 递归左子数组、右子数组\n    this.quickSort(nums, left, pivot - 1);\n    this.quickSort(nums, pivot + 1, right);\n}\n
quick_sort.dart
/* 快速排序 */\nvoid quickSort(List<int> nums, int left, int right) {\n  // 子数组长度为 1 时终止递归\n  if (left >= right) return;\n  // 哨兵划分\n  int pivot = _partition(nums, left, right);\n  // 递归左子数组、右子数组\n  quickSort(nums, left, pivot - 1);\n  quickSort(nums, pivot + 1, right);\n}\n
quick_sort.rs
/* 快速排序 */\npub fn quick_sort(left: i32, right: i32, nums: &mut [i32]) {\n    // 子数组长度为 1 时终止递归\n    if left >= right {\n        return;\n    }\n    // 哨兵划分\n    let pivot = Self::partition(nums, left as usize, right as usize) as i32;\n    // 递归左子数组、右子数组\n    Self::quick_sort(left, pivot - 1, nums);\n    Self::quick_sort(pivot + 1, right, nums);\n}\n
quick_sort.c
/* 快速排序 */\nvoid quickSort(int nums[], int left, int right) {\n    // 子数组长度为 1 时终止递归\n    if (left >= right) {\n        return;\n    }\n    // 哨兵划分\n    int pivot = partition(nums, left, right);\n    // 递归左子数组、右子数组\n    quickSort(nums, left, pivot - 1);\n    quickSort(nums, pivot + 1, right);\n}\n
quick_sort.kt
/* 快速排序 */\nfun quickSort(nums: IntArray, left: Int, right: Int) {\n    // 子数组长度为 1 时终止递归\n    if (left >= right) return\n    // 哨兵划分\n    val pivot = partition(nums, left, right)\n    // 递归左子数组、右子数组\n    quickSort(nums, left, pivot - 1)\n    quickSort(nums, pivot + 1, right)\n}\n
quick_sort.rb
### 快速排序类 ###\ndef quick_sort(nums, left, right)\n  # 子数组长度不为 1 时递归\n  if left < right\n    # 哨兵划分\n    pivot = partition(nums, left, right)\n    # 递归左子数组、右子数组\n    quick_sort(nums, left, pivot - 1)\n    quick_sort(nums, pivot + 1, right)\n  end\n  nums\nend\n
可视化运行

全屏观看 >

","path":["第 11 章   排序","11.5   快速排序"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1152","level":2,"title":"11.5.2   算法特性","text":"","path":["第 11 章   排序","11.5   快速排序"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1153","level":2,"title":"11.5.3   快速排序为什么快","text":"

从名称上就能看出,快速排序在效率方面应该具有一定的优势。尽管快速排序的平均时间复杂度与“归并排序”和“堆排序”相同,但通常快速排序的效率更高,主要有以下原因。

","path":["第 11 章   排序","11.5   快速排序"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1154","level":2,"title":"11.5.4   基准数优化","text":"

快速排序在某些输入下的时间效率可能降低。举一个极端例子,假设输入数组是完全倒序的,由于我们选择最左端元素作为基准数,那么在哨兵划分完成后,基准数被交换至数组最右端,导致左子数组长度为 \\(n - 1\\)、右子数组长度为 \\(0\\) 。如此递归下去,每轮哨兵划分后都有一个子数组的长度为 \\(0\\) ,分治策略失效,快速排序退化为“冒泡排序”的近似形式。

为了尽量避免这种情况发生,我们可以优化哨兵划分中的基准数的选取策略。例如,我们可以随机选取一个元素作为基准数。然而,如果运气不佳,每次都选到不理想的基准数,效率仍然不尽如人意。

需要注意的是,编程语言通常生成的是“伪随机数”。如果我们针对伪随机数序列构建一个特定的测试样例,那么快速排序的效率仍然可能劣化。

为了进一步改进,我们可以在数组中选取三个候选元素(通常为数组的首、尾、中点元素),并将这三个候选元素的中位数作为基准数。这样一来,基准数“既不太小也不太大”的概率将大幅提升。当然,我们还可以选取更多候选元素,以进一步提高算法的稳健性。采用这种方法后,时间复杂度劣化至 \\(O(n^2)\\) 的概率大大降低。

示例代码如下:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
def median_three(self, nums: list[int], left: int, mid: int, right: int) -> int:\n    \"\"\"选取三个候选元素的中位数\"\"\"\n    l, m, r = nums[left], nums[mid], nums[right]\n    if (l <= m <= r) or (r <= m <= l):\n        return mid  # m 在 l 和 r 之间\n    if (m <= l <= r) or (r <= l <= m):\n        return left  # l 在 m 和 r 之间\n    return right\n\ndef partition(self, nums: list[int], left: int, right: int) -> int:\n    \"\"\"哨兵划分(三数取中值)\"\"\"\n    # 以 nums[left] 为基准数\n    med = self.median_three(nums, left, (left + right) // 2, right)\n    # 将中位数交换至数组最左端\n    nums[left], nums[med] = nums[med], nums[left]\n    # 以 nums[left] 为基准数\n    i, j = left, right\n    while i < j:\n        while i < j and nums[j] >= nums[left]:\n            j -= 1  # 从右向左找首个小于基准数的元素\n        while i < j and nums[i] <= nums[left]:\n            i += 1  # 从左向右找首个大于基准数的元素\n        # 元素交换\n        nums[i], nums[j] = nums[j], nums[i]\n    # 将基准数交换至两子数组的分界线\n    nums[i], nums[left] = nums[left], nums[i]\n    return i  # 返回基准数的索引\n
quick_sort.cpp
/* 选取三个候选元素的中位数 */\nint medianThree(vector<int> &nums, int left, int mid, int right) {\n    int l = nums[left], m = nums[mid], r = nums[right];\n    if ((l <= m && m <= r) || (r <= m && m <= l))\n        return mid; // m 在 l 和 r 之间\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l 在 m 和 r 之间\n    return right;\n}\n\n/* 哨兵划分(三数取中值) */\nint partition(vector<int> &nums, int left, int right) {\n    // 选取三个候选元素的中位数\n    int med = medianThree(nums, left, (left + right) / 2, right);\n    // 将中位数交换至数组最左端\n    swap(nums[left], nums[med]);\n    // 以 nums[left] 为基准数\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;                // 从右向左找首个小于基准数的元素\n        while (i < j && nums[i] <= nums[left])\n            i++;                // 从左向右找首个大于基准数的元素\n        swap(nums[i], nums[j]); // 交换这两个元素\n    }\n    swap(nums[i], nums[left]);  // 将基准数交换至两子数组的分界线\n    return i;                   // 返回基准数的索引\n}\n
quick_sort.java
/* 选取三个候选元素的中位数 */\nint medianThree(int[] nums, int left, int mid, int right) {\n    int l = nums[left], m = nums[mid], r = nums[right];\n    if ((l <= m && m <= r) || (r <= m && m <= l))\n        return mid; // m 在 l 和 r 之间\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l 在 m 和 r 之间\n    return right;\n}\n\n/* 哨兵划分(三数取中值) */\nint partition(int[] nums, int left, int right) {\n    // 选取三个候选元素的中位数\n    int med = medianThree(nums, left, (left + right) / 2, right);\n    // 将中位数交换至数组最左端\n    swap(nums, left, med);\n    // 以 nums[left] 为基准数\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // 从右向左找首个小于基准数的元素\n        while (i < j && nums[i] <= nums[left])\n            i++;          // 从左向右找首个大于基准数的元素\n        swap(nums, i, j); // 交换这两个元素\n    }\n    swap(nums, i, left);  // 将基准数交换至两子数组的分界线\n    return i;             // 返回基准数的索引\n}\n
quick_sort.cs
/* 选取三个候选元素的中位数 */\nint MedianThree(int[] nums, int left, int mid, int right) {\n    int l = nums[left], m = nums[mid], r = nums[right];\n    if ((l <= m && m <= r) || (r <= m && m <= l))\n        return mid; // m 在 l 和 r 之间\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l 在 m 和 r 之间\n    return right;\n}\n\n/* 哨兵划分(三数取中值) */\nint Partition(int[] nums, int left, int right) {\n    // 选取三个候选元素的中位数\n    int med = MedianThree(nums, left, (left + right) / 2, right);\n    // 将中位数交换至数组最左端\n    Swap(nums, left, med);\n    // 以 nums[left] 为基准数\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--;          // 从右向左找首个小于基准数的元素\n        while (i < j && nums[i] <= nums[left])\n            i++;          // 从左向右找首个大于基准数的元素\n        Swap(nums, i, j); // 交换这两个元素\n    }\n    Swap(nums, i, left);  // 将基准数交换至两子数组的分界线\n    return i;             // 返回基准数的索引\n}\n
quick_sort.go
/* 选取三个候选元素的中位数 */\nfunc (q *quickSortMedian) medianThree(nums []int, left, mid, right int) int {\n    l, m, r := nums[left], nums[mid], nums[right]\n    if (l <= m && m <= r) || (r <= m && m <= l) {\n        return mid // m 在 l 和 r 之间\n    }\n    if (m <= l && l <= r) || (r <= l && l <= m) {\n        return left // l 在 m 和 r 之间\n    }\n    return right\n}\n\n/* 哨兵划分(三数取中值)*/\nfunc (q *quickSortMedian) partition(nums []int, left, right int) int {\n    // 以 nums[left] 为基准数\n    med := q.medianThree(nums, left, (left+right)/2, right)\n    // 将中位数交换至数组最左端\n    nums[left], nums[med] = nums[med], nums[left]\n    // 以 nums[left] 为基准数\n    i, j := left, right\n    for i < j {\n        for i < j && nums[j] >= nums[left] {\n            j-- //从右向左找首个小于基准数的元素\n        }\n        for i < j && nums[i] <= nums[left] {\n            i++ //从左向右找首个大于基准数的元素\n        }\n        //元素交换\n        nums[i], nums[j] = nums[j], nums[i]\n    }\n    //将基准数交换至两子数组的分界线\n    nums[i], nums[left] = nums[left], nums[i]\n    return i //返回基准数的索引\n}\n
quick_sort.swift
/* 选取三个候选元素的中位数 */\nfunc medianThree(nums: [Int], left: Int, mid: Int, right: Int) -> Int {\n    let l = nums[left]\n    let m = nums[mid]\n    let r = nums[right]\n    if (l <= m && m <= r) || (r <= m && m <= l) {\n        return mid // m 在 l 和 r 之间\n    }\n    if (m <= l && l <= r) || (r <= l && l <= m) {\n        return left // l 在 m 和 r 之间\n    }\n    return right\n}\n\n/* 哨兵划分(三数取中值) */\nfunc partitionMedian(nums: inout [Int], left: Int, right: Int) -> Int {\n    // 选取三个候选元素的中位数\n    let med = medianThree(nums: nums, left: left, mid: left + (right - left) / 2, right: right)\n    // 将中位数交换至数组最左端\n    nums.swapAt(left, med)\n    return partition(nums: &nums, left: left, right: right)\n}\n
quick_sort.js
/* 选取三个候选元素的中位数 */\nmedianThree(nums, left, mid, right) {\n    let l = nums[left],\n        m = nums[mid],\n        r = nums[right];\n    // m 在 l 和 r 之间\n    if ((l <= m && m <= r) || (r <= m && m <= l)) return mid;\n    // l 在 m 和 r 之间\n    if ((m <= l && l <= r) || (r <= l && l <= m)) return left;\n    return right;\n}\n\n/* 哨兵划分(三数取中值) */\npartition(nums, left, right) {\n    // 选取三个候选元素的中位数\n    let med = this.medianThree(\n        nums,\n        left,\n        Math.floor((left + right) / 2),\n        right\n    );\n    // 将中位数交换至数组最左端\n    this.swap(nums, left, med);\n    // 以 nums[left] 为基准数\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) j--; // 从右向左找首个小于基准数的元素\n        while (i < j && nums[i] <= nums[left]) i++; // 从左向右找首个大于基准数的元素\n        this.swap(nums, i, j); // 交换这两个元素\n    }\n    this.swap(nums, i, left); // 将基准数交换至两子数组的分界线\n    return i; // 返回基准数的索引\n}\n
quick_sort.ts
/* 选取三个候选元素的中位数 */\nmedianThree(\n    nums: number[],\n    left: number,\n    mid: number,\n    right: number\n): number {\n    let l = nums[left],\n        m = nums[mid],\n        r = nums[right];\n    // m 在 l 和 r 之间\n    if ((l <= m && m <= r) || (r <= m && m <= l)) return mid;\n    // l 在 m 和 r 之间\n    if ((m <= l && l <= r) || (r <= l && l <= m)) return left;\n    return right;\n}\n\n/* 哨兵划分(三数取中值) */\npartition(nums: number[], left: number, right: number): number {\n    // 选取三个候选元素的中位数\n    let med = this.medianThree(\n        nums,\n        left,\n        Math.floor((left + right) / 2),\n        right\n    );\n    // 将中位数交换至数组最左端\n    this.swap(nums, left, med);\n    // 以 nums[left] 为基准数\n    let i = left,\n        j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left]) {\n            j--; // 从右向左找首个小于基准数的元素\n        }\n        while (i < j && nums[i] <= nums[left]) {\n            i++; // 从左向右找首个大于基准数的元素\n        }\n        this.swap(nums, i, j); // 交换这两个元素\n    }\n    this.swap(nums, i, left); // 将基准数交换至两子数组的分界线\n    return i; // 返回基准数的索引\n}\n
quick_sort.dart
/* 选取三个候选元素的中位数 */\nint _medianThree(List<int> nums, int left, int mid, int right) {\n  int l = nums[left], m = nums[mid], r = nums[right];\n  if ((l <= m && m <= r) || (r <= m && m <= l))\n    return mid; // m 在 l 和 r 之间\n  if ((m <= l && l <= r) || (r <= l && l <= m))\n    return left; // l 在 m 和 r 之间\n  return right;\n}\n\n/* 哨兵划分(三数取中值) */\nint _partition(List<int> nums, int left, int right) {\n  // 选取三个候选元素的中位数\n  int med = _medianThree(nums, left, (left + right) ~/ 2, right);\n  // 将中位数交换至数组最左端\n  _swap(nums, left, med);\n  // 以 nums[left] 为基准数\n  int i = left, j = right;\n  while (i < j) {\n    while (i < j && nums[j] >= nums[left]) j--; // 从右向左找首个小于基准数的元素\n    while (i < j && nums[i] <= nums[left]) i++; // 从左向右找首个大于基准数的元素\n    _swap(nums, i, j); // 交换这两个元素\n  }\n  _swap(nums, i, left); // 将基准数交换至两子数组的分界线\n  return i; // 返回基准数的索引\n}\n
quick_sort.rs
/* 选取三个候选元素的中位数 */\nfn median_three(nums: &mut [i32], left: usize, mid: usize, right: usize) -> usize {\n    let (l, m, r) = (nums[left], nums[mid], nums[right]);\n    if (l <= m && m <= r) || (r <= m && m <= l) {\n        return mid; // m 在 l 和 r 之间\n    }\n    if (m <= l && l <= r) || (r <= l && l <= m) {\n        return left; // l 在 m 和 r 之间\n    }\n    right\n}\n\n/* 哨兵划分(三数取中值) */\nfn partition(nums: &mut [i32], left: usize, right: usize) -> usize {\n    // 选取三个候选元素的中位数\n    let med = Self::median_three(nums, left, (left + right) / 2, right);\n    // 将中位数交换至数组最左端\n    nums.swap(left, med);\n    // 以 nums[left] 为基准数\n    let (mut i, mut j) = (left, right);\n    while i < j {\n        while i < j && nums[j] >= nums[left] {\n            j -= 1; // 从右向左找首个小于基准数的元素\n        }\n        while i < j && nums[i] <= nums[left] {\n            i += 1; // 从左向右找首个大于基准数的元素\n        }\n        nums.swap(i, j); // 交换这两个元素\n    }\n    nums.swap(i, left); // 将基准数交换至两子数组的分界线\n    i // 返回基准数的索引\n}\n
quick_sort.c
/* 选取三个候选元素的中位数 */\nint medianThree(int nums[], int left, int mid, int right) {\n    int l = nums[left], m = nums[mid], r = nums[right];\n    if ((l <= m && m <= r) || (r <= m && m <= l))\n        return mid; // m 在 l 和 r 之间\n    if ((m <= l && l <= r) || (r <= l && l <= m))\n        return left; // l 在 m 和 r 之间\n    return right;\n}\n\n/* 哨兵划分(三数取中值) */\nint partitionMedian(int nums[], int left, int right) {\n    // 选取三个候选元素的中位数\n    int med = medianThree(nums, left, (left + right) / 2, right);\n    // 将中位数交换至数组最左端\n    swap(nums, left, med);\n    // 以 nums[left] 为基准数\n    int i = left, j = right;\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--; // 从右向左找首个小于基准数的元素\n        while (i < j && nums[i] <= nums[left])\n            i++;          // 从左向右找首个大于基准数的元素\n        swap(nums, i, j); // 交换这两个元素\n    }\n    swap(nums, i, left); // 将基准数交换至两子数组的分界线\n    return i;            // 返回基准数的索引\n}\n
quick_sort.kt
/* 选取三个候选元素的中位数 */\nfun medianThree(nums: IntArray, left: Int, mid: Int, right: Int): Int {\n    val l = nums[left]\n    val m = nums[mid]\n    val r = nums[right]\n    if ((m in l..r) || (m in r..l))\n        return mid  // m 在 l 和 r 之间\n    if ((l in m..r) || (l in r..m))\n        return left // l 在 m 和 r 之间\n    return right\n}\n\n/* 哨兵划分(三数取中值) */\nfun partitionMedian(nums: IntArray, left: Int, right: Int): Int {\n    // 选取三个候选元素的中位数\n    val med = medianThree(nums, left, (left + right) / 2, right)\n    // 将中位数交换至数组最左端\n    swap(nums, left, med)\n    // 以 nums[left] 为基准数\n    var i = left\n    var j = right\n    while (i < j) {\n        while (i < j && nums[j] >= nums[left])\n            j--                      // 从右向左找首个小于基准数的元素\n        while (i < j && nums[i] <= nums[left])\n            i++                      // 从左向右找首个大于基准数的元素\n        swap(nums, i, j)             // 交换这两个元素\n    }\n    swap(nums, i, left)              // 将基准数交换至两子数组的分界线\n    return i                         // 返回基准数的索引\n}\n
quick_sort.rb
### 选取三个候选元素的中位数 ###\ndef median_three(nums, left, mid, right)\n  # 选取三个候选元素的中位数\n  _l, _m, _r = nums[left], nums[mid], nums[right]\n  # m 在 l 和 r 之间\n  return mid if (_l <= _m && _m <= _r) || (_r <= _m && _m <= _l)\n  # l 在 m 和 r 之间\n  return left if (_m <= _l && _l <= _r) || (_r <= _l && _l <= _m)\n  return right\nend\n\n### 哨兵划分(三数取中值)###\ndef partition(nums, left, right)\n  ### 以 nums[left] 为基准数\n  med = median_three(nums, left, (left + right) / 2, right)\n  # 将中位数交换至数组最左断\n  nums[left], nums[med] = nums[med], nums[left]\n  i, j = left, right\n  while i < j\n    while i < j && nums[j] >= nums[left]\n      j -= 1 # 从右向左找首个小于基准数的元素\n    end\n    while i < j && nums[i] <= nums[left]\n      i += 1 # 从左向右找首个大于基准数的元素\n    end\n    # 元素交换\n    nums[i], nums[j] = nums[j], nums[i]\n  end\n  # 将基准数交换至两子数组的分界线\n  nums[i], nums[left] = nums[left], nums[i]\n  i # 返回基准数的索引\nend\n
可视化运行

全屏观看 >

","path":["第 11 章   排序","11.5   快速排序"],"tags":[]},{"location":"chapter_sorting/quick_sort/#1155","level":2,"title":"11.5.5   递归深度优化","text":"

在某些输入下,快速排序可能占用空间较多。以完全有序的输入数组为例,设递归中的子数组长度为 \\(m\\) ,每轮哨兵划分操作都将产生长度为 \\(0\\) 的左子数组和长度为 \\(m - 1\\) 的右子数组,这意味着每一层递归调用减少的问题规模非常小(只减少一个元素),递归树的高度会达到 \\(n - 1\\) ,此时需要占用 \\(O(n)\\) 大小的栈帧空间。

为了防止栈帧空间的累积,我们可以在每轮哨兵排序完成后,比较两个子数组的长度,仅对较短的子数组进行递归。由于较短子数组的长度不会超过 \\(n / 2\\) ,因此这种方法能确保递归深度不超过 \\(\\log n\\) ,从而将最差空间复杂度优化至 \\(O(\\log n)\\) 。代码如下所示:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby quick_sort.py
def quick_sort(self, nums: list[int], left: int, right: int):\n    \"\"\"快速排序(递归深度优化)\"\"\"\n    # 子数组长度为 1 时终止\n    while left < right:\n        # 哨兵划分操作\n        pivot = self.partition(nums, left, right)\n        # 对两个子数组中较短的那个执行快速排序\n        if pivot - left < right - pivot:\n            self.quick_sort(nums, left, pivot - 1)  # 递归排序左子数组\n            left = pivot + 1  # 剩余未排序区间为 [pivot + 1, right]\n        else:\n            self.quick_sort(nums, pivot + 1, right)  # 递归排序右子数组\n            right = pivot - 1  # 剩余未排序区间为 [left, pivot - 1]\n
quick_sort.cpp
/* 快速排序(递归深度优化) */\nvoid quickSort(vector<int> &nums, int left, int right) {\n    // 子数组长度为 1 时终止\n    while (left < right) {\n        // 哨兵划分操作\n        int pivot = partition(nums, left, right);\n        // 对两个子数组中较短的那个执行快速排序\n        if (pivot - left < right - pivot) {\n            quickSort(nums, left, pivot - 1); // 递归排序左子数组\n            left = pivot + 1;                 // 剩余未排序区间为 [pivot + 1, right]\n        } else {\n            quickSort(nums, pivot + 1, right); // 递归排序右子数组\n            right = pivot - 1;                 // 剩余未排序区间为 [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.java
/* 快速排序(递归深度优化) */\nvoid quickSort(int[] nums, int left, int right) {\n    // 子数组长度为 1 时终止\n    while (left < right) {\n        // 哨兵划分操作\n        int pivot = partition(nums, left, right);\n        // 对两个子数组中较短的那个执行快速排序\n        if (pivot - left < right - pivot) {\n            quickSort(nums, left, pivot - 1); // 递归排序左子数组\n            left = pivot + 1; // 剩余未排序区间为 [pivot + 1, right]\n        } else {\n            quickSort(nums, pivot + 1, right); // 递归排序右子数组\n            right = pivot - 1; // 剩余未排序区间为 [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.cs
/* 快速排序(递归深度优化) */\nvoid QuickSort(int[] nums, int left, int right) {\n    // 子数组长度为 1 时终止\n    while (left < right) {\n        // 哨兵划分操作\n        int pivot = Partition(nums, left, right);\n        // 对两个子数组中较短的那个执行快速排序\n        if (pivot - left < right - pivot) {\n            QuickSort(nums, left, pivot - 1);  // 递归排序左子数组\n            left = pivot + 1;  // 剩余未排序区间为 [pivot + 1, right]\n        } else {\n            QuickSort(nums, pivot + 1, right); // 递归排序右子数组\n            right = pivot - 1; // 剩余未排序区间为 [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.go
/* 快速排序(递归深度优化)*/\nfunc (q *quickSortTailCall) quickSort(nums []int, left, right int) {\n    // 子数组长度为 1 时终止\n    for left < right {\n        // 哨兵划分操作\n        pivot := q.partition(nums, left, right)\n        // 对两个子数组中较短的那个执行快速排序\n        if pivot-left < right-pivot {\n            q.quickSort(nums, left, pivot-1) // 递归排序左子数组\n            left = pivot + 1                 // 剩余未排序区间为 [pivot + 1, right]\n        } else {\n            q.quickSort(nums, pivot+1, right) // 递归排序右子数组\n            right = pivot - 1                 // 剩余未排序区间为 [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.swift
/* 快速排序(递归深度优化) */\nfunc quickSortTailCall(nums: inout [Int], left: Int, right: Int) {\n    var left = left\n    var right = right\n    // 子数组长度为 1 时终止\n    while left < right {\n        // 哨兵划分操作\n        let pivot = partition(nums: &nums, left: left, right: right)\n        // 对两个子数组中较短的那个执行快速排序\n        if (pivot - left) < (right - pivot) {\n            quickSortTailCall(nums: &nums, left: left, right: pivot - 1) // 递归排序左子数组\n            left = pivot + 1 // 剩余未排序区间为 [pivot + 1, right]\n        } else {\n            quickSortTailCall(nums: &nums, left: pivot + 1, right: right) // 递归排序右子数组\n            right = pivot - 1 // 剩余未排序区间为 [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.js
/* 快速排序(递归深度优化) */\nquickSort(nums, left, right) {\n    // 子数组长度为 1 时终止\n    while (left < right) {\n        // 哨兵划分操作\n        let pivot = this.partition(nums, left, right);\n        // 对两个子数组中较短的那个执行快速排序\n        if (pivot - left < right - pivot) {\n            this.quickSort(nums, left, pivot - 1); // 递归排序左子数组\n            left = pivot + 1; // 剩余未排序区间为 [pivot + 1, right]\n        } else {\n            this.quickSort(nums, pivot + 1, right); // 递归排序右子数组\n            right = pivot - 1; // 剩余未排序区间为 [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.ts
/* 快速排序(递归深度优化) */\nquickSort(nums: number[], left: number, right: number): void {\n    // 子数组长度为 1 时终止\n    while (left < right) {\n        // 哨兵划分操作\n        let pivot = this.partition(nums, left, right);\n        // 对两个子数组中较短的那个执行快速排序\n        if (pivot - left < right - pivot) {\n            this.quickSort(nums, left, pivot - 1); // 递归排序左子数组\n            left = pivot + 1; // 剩余未排序区间为 [pivot + 1, right]\n        } else {\n            this.quickSort(nums, pivot + 1, right); // 递归排序右子数组\n            right = pivot - 1; // 剩余未排序区间为 [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.dart
/* 快速排序(递归深度优化) */\nvoid quickSort(List<int> nums, int left, int right) {\n  // 子数组长度为 1 时终止\n  while (left < right) {\n    // 哨兵划分操作\n    int pivot = _partition(nums, left, right);\n    // 对两个子数组中较短的那个执行快速排序\n    if (pivot - left < right - pivot) {\n      quickSort(nums, left, pivot - 1); // 递归排序左子数组\n      left = pivot + 1; // 剩余未排序区间为 [pivot + 1, right]\n    } else {\n      quickSort(nums, pivot + 1, right); // 递归排序右子数组\n      right = pivot - 1; // 剩余未排序区间为 [left, pivot - 1]\n    }\n  }\n}\n
quick_sort.rs
/* 快速排序(递归深度优化) */\npub fn quick_sort(mut left: i32, mut right: i32, nums: &mut [i32]) {\n    // 子数组长度为 1 时终止\n    while left < right {\n        // 哨兵划分操作\n        let pivot = Self::partition(nums, left as usize, right as usize) as i32;\n        // 对两个子数组中较短的那个执行快速排序\n        if pivot - left < right - pivot {\n            Self::quick_sort(left, pivot - 1, nums); // 递归排序左子数组\n            left = pivot + 1; // 剩余未排序区间为 [pivot + 1, right]\n        } else {\n            Self::quick_sort(pivot + 1, right, nums); // 递归排序右子数组\n            right = pivot - 1; // 剩余未排序区间为 [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.c
/* 快速排序(递归深度优化) */\nvoid quickSortTailCall(int nums[], int left, int right) {\n    // 子数组长度为 1 时终止\n    while (left < right) {\n        // 哨兵划分操作\n        int pivot = partition(nums, left, right);\n        // 对两个子数组中较短的那个执行快速排序\n        if (pivot - left < right - pivot) {\n            // 递归排序左子数组\n            quickSortTailCall(nums, left, pivot - 1);\n            // 剩余未排序区间为 [pivot + 1, right]\n            left = pivot + 1;\n        } else {\n            // 递归排序右子数组\n            quickSortTailCall(nums, pivot + 1, right);\n            // 剩余未排序区间为 [left, pivot - 1]\n            right = pivot - 1;\n        }\n    }\n}\n
quick_sort.kt
/* 快速排序(递归深度优化) */\nfun quickSortTailCall(nums: IntArray, left: Int, right: Int) {\n    // 子数组长度为 1 时终止\n    var l = left\n    var r = right\n    while (l < r) {\n        // 哨兵划分操作\n        val pivot = partition(nums, l, r)\n        // 对两个子数组中较短的那个执行快速排序\n        if (pivot - l < r - pivot) {\n            quickSort(nums, l, pivot - 1) // 递归排序左子数组\n            l = pivot + 1 // 剩余未排序区间为 [pivot + 1, right]\n        } else {\n            quickSort(nums, pivot + 1, r) // 递归排序右子数组\n            r = pivot - 1 // 剩余未排序区间为 [left, pivot - 1]\n        }\n    }\n}\n
quick_sort.rb
### 快速排序(递归深度优化)###\ndef quick_sort(nums, left, right)\n  # 子数组长度不为 1 时递归\n  while left < right\n    # 哨兵划分\n    pivot = partition(nums, left, right)\n    # 对两个子数组中较短的那个执行快速排序\n    if pivot - left < right - pivot\n      quick_sort(nums, left, pivot - 1)\n      left = pivot + 1 # 剩余未排序区间为 [pivot + 1, right]\n    else\n      quick_sort(nums, pivot + 1, right)\n      right = pivot - 1 # 剩余未排序区间为 [left, pivot - 1]\n    end\n  end\nend\n
可视化运行

全屏观看 >

","path":["第 11 章   排序","11.5   快速排序"],"tags":[]},{"location":"chapter_sorting/radix_sort/","level":1,"title":"11.10   基数排序","text":"

上一节介绍了计数排序,它适用于数据量 \\(n\\) 较大但数据范围 \\(m\\) 较小的情况。假设我们需要对 \\(n = 10^6\\) 个学号进行排序,而学号是一个 \\(8\\) 位数字,这意味着数据范围 \\(m = 10^8\\) 非常大,使用计数排序需要分配大量内存空间,而基数排序可以避免这种情况。

基数排序(radix sort)的核心思想与计数排序一致,也通过统计个数来实现排序。在此基础上,基数排序利用数字各位之间的递进关系,依次对每一位进行排序,从而得到最终的排序结果。

","path":["第 11 章   排序","11.10   基数排序"],"tags":[]},{"location":"chapter_sorting/radix_sort/#11101","level":2,"title":"11.10.1   算法流程","text":"

以学号数据为例,假设数字的最低位是第 \\(1\\) 位,最高位是第 \\(8\\) 位,基数排序的流程如图 11-18 所示。

  1. 初始化位数 \\(k = 1\\) 。
  2. 对学号的第 \\(k\\) 位执行“计数排序”。完成后,数据会根据第 \\(k\\) 位从小到大排序。
  3. 将 \\(k\\) 增加 \\(1\\) ,然后返回步骤 2. 继续迭代,直到所有位都排序完成后结束。

图 11-18   基数排序算法流程

下面剖析代码实现。对于一个 \\(d\\) 进制的数字 \\(x\\) ,要获取其第 \\(k\\) 位 \\(x_k\\) ,可以使用以下计算公式:

\\[ x_k = \\lfloor\\frac{x}{d^{k-1}}\\rfloor \\bmod d \\]

其中 \\(\\lfloor a \\rfloor\\) 表示对浮点数 \\(a\\) 向下取整,而 \\(\\bmod \\: d\\) 表示对 \\(d\\) 取模(取余)。对于学号数据,\\(d = 10\\) 且 \\(k \\in [1, 8]\\) 。

此外,我们需要小幅改动计数排序代码,使之可以根据数字的第 \\(k\\) 位进行排序:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby radix_sort.py
def digit(num: int, exp: int) -> int:\n    \"\"\"获取元素 num 的第 k 位,其中 exp = 10^(k-1)\"\"\"\n    # 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n    return (num // exp) % 10\n\ndef counting_sort_digit(nums: list[int], exp: int):\n    \"\"\"计数排序(根据 nums 第 k 位排序)\"\"\"\n    # 十进制的位范围为 0~9 ,因此需要长度为 10 的桶数组\n    counter = [0] * 10\n    n = len(nums)\n    # 统计 0~9 各数字的出现次数\n    for i in range(n):\n        d = digit(nums[i], exp)  # 获取 nums[i] 第 k 位,记为 d\n        counter[d] += 1  # 统计数字 d 的出现次数\n    # 求前缀和,将“出现个数”转换为“数组索引”\n    for i in range(1, 10):\n        counter[i] += counter[i - 1]\n    # 倒序遍历,根据桶内统计结果,将各元素填入 res\n    res = [0] * n\n    for i in range(n - 1, -1, -1):\n        d = digit(nums[i], exp)\n        j = counter[d] - 1  # 获取 d 在数组中的索引 j\n        res[j] = nums[i]  # 将当前元素填入索引 j\n        counter[d] -= 1  # 将 d 的数量减 1\n    # 使用结果覆盖原数组 nums\n    for i in range(n):\n        nums[i] = res[i]\n\ndef radix_sort(nums: list[int]):\n    \"\"\"基数排序\"\"\"\n    # 获取数组的最大元素,用于判断最大位数\n    m = max(nums)\n    # 按照从低位到高位的顺序遍历\n    exp = 1\n    while exp <= m:\n        # 对数组元素的第 k 位执行计数排序\n        # k = 1 -> exp = 1\n        # k = 2 -> exp = 10\n        # 即 exp = 10^(k-1)\n        counting_sort_digit(nums, exp)\n        exp *= 10\n
radix_sort.cpp
/* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nint digit(int num, int exp) {\n    // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n    return (num / exp) % 10;\n}\n\n/* 计数排序(根据 nums 第 k 位排序) */\nvoid countingSortDigit(vector<int> &nums, int exp) {\n    // 十进制的位范围为 0~9 ,因此需要长度为 10 的桶数组\n    vector<int> counter(10, 0);\n    int n = nums.size();\n    // 统计 0~9 各数字的出现次数\n    for (int i = 0; i < n; i++) {\n        int d = digit(nums[i], exp); // 获取 nums[i] 第 k 位,记为 d\n        counter[d]++;                // 统计数字 d 的出现次数\n    }\n    // 求前缀和,将“出现个数”转换为“数组索引”\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // 倒序遍历,根据桶内统计结果,将各元素填入 res\n    vector<int> res(n, 0);\n    for (int i = n - 1; i >= 0; i--) {\n        int d = digit(nums[i], exp);\n        int j = counter[d] - 1; // 获取 d 在数组中的索引 j\n        res[j] = nums[i];       // 将当前元素填入索引 j\n        counter[d]--;           // 将 d 的数量减 1\n    }\n    // 使用结果覆盖原数组 nums\n    for (int i = 0; i < n; i++)\n        nums[i] = res[i];\n}\n\n/* 基数排序 */\nvoid radixSort(vector<int> &nums) {\n    // 获取数组的最大元素,用于判断最大位数\n    int m = *max_element(nums.begin(), nums.end());\n    // 按照从低位到高位的顺序遍历\n    for (int exp = 1; exp <= m; exp *= 10)\n        // 对数组元素的第 k 位执行计数排序\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // 即 exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n}\n
radix_sort.java
/* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nint digit(int num, int exp) {\n    // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n    return (num / exp) % 10;\n}\n\n/* 计数排序(根据 nums 第 k 位排序) */\nvoid countingSortDigit(int[] nums, int exp) {\n    // 十进制的位范围为 0~9 ,因此需要长度为 10 的桶数组\n    int[] counter = new int[10];\n    int n = nums.length;\n    // 统计 0~9 各数字的出现次数\n    for (int i = 0; i < n; i++) {\n        int d = digit(nums[i], exp); // 获取 nums[i] 第 k 位,记为 d\n        counter[d]++;                // 统计数字 d 的出现次数\n    }\n    // 求前缀和,将“出现个数”转换为“数组索引”\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // 倒序遍历,根据桶内统计结果,将各元素填入 res\n    int[] res = new int[n];\n    for (int i = n - 1; i >= 0; i--) {\n        int d = digit(nums[i], exp);\n        int j = counter[d] - 1; // 获取 d 在数组中的索引 j\n        res[j] = nums[i];       // 将当前元素填入索引 j\n        counter[d]--;           // 将 d 的数量减 1\n    }\n    // 使用结果覆盖原数组 nums\n    for (int i = 0; i < n; i++)\n        nums[i] = res[i];\n}\n\n/* 基数排序 */\nvoid radixSort(int[] nums) {\n    // 获取数组的最大元素,用于判断最大位数\n    int m = Integer.MIN_VALUE;\n    for (int num : nums)\n        if (num > m)\n            m = num;\n    // 按照从低位到高位的顺序遍历\n    for (int exp = 1; exp <= m; exp *= 10) {\n        // 对数组元素的第 k 位执行计数排序\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // 即 exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n    }\n}\n
radix_sort.cs
/* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nint Digit(int num, int exp) {\n    // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n    return (num / exp) % 10;\n}\n\n/* 计数排序(根据 nums 第 k 位排序) */\nvoid CountingSortDigit(int[] nums, int exp) {\n    // 十进制的位范围为 0~9 ,因此需要长度为 10 的桶数组\n    int[] counter = new int[10];\n    int n = nums.Length;\n    // 统计 0~9 各数字的出现次数\n    for (int i = 0; i < n; i++) {\n        int d = Digit(nums[i], exp); // 获取 nums[i] 第 k 位,记为 d\n        counter[d]++;                // 统计数字 d 的出现次数\n    }\n    // 求前缀和,将“出现个数”转换为“数组索引”\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // 倒序遍历,根据桶内统计结果,将各元素填入 res\n    int[] res = new int[n];\n    for (int i = n - 1; i >= 0; i--) {\n        int d = Digit(nums[i], exp);\n        int j = counter[d] - 1; // 获取 d 在数组中的索引 j\n        res[j] = nums[i];       // 将当前元素填入索引 j\n        counter[d]--;           // 将 d 的数量减 1\n    }\n    // 使用结果覆盖原数组 nums\n    for (int i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n\n/* 基数排序 */\nvoid RadixSort(int[] nums) {\n    // 获取数组的最大元素,用于判断最大位数\n    int m = int.MinValue;\n    foreach (int num in nums) {\n        if (num > m) m = num;\n    }\n    // 按照从低位到高位的顺序遍历\n    for (int exp = 1; exp <= m; exp *= 10) {\n        // 对数组元素的第 k 位执行计数排序\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // 即 exp = 10^(k-1)\n        CountingSortDigit(nums, exp);\n    }\n}\n
radix_sort.go
/* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nfunc digit(num, exp int) int {\n    // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n    return (num / exp) % 10\n}\n\n/* 计数排序(根据 nums 第 k 位排序) */\nfunc countingSortDigit(nums []int, exp int) {\n    // 十进制的位范围为 0~9 ,因此需要长度为 10 的桶数组\n    counter := make([]int, 10)\n    n := len(nums)\n    // 统计 0~9 各数字的出现次数\n    for i := 0; i < n; i++ {\n        d := digit(nums[i], exp) // 获取 nums[i] 第 k 位,记为 d\n        counter[d]++             // 统计数字 d 的出现次数\n    }\n    // 求前缀和,将“出现个数”转换为“数组索引”\n    for i := 1; i < 10; i++ {\n        counter[i] += counter[i-1]\n    }\n    // 倒序遍历,根据桶内统计结果,将各元素填入 res\n    res := make([]int, n)\n    for i := n - 1; i >= 0; i-- {\n        d := digit(nums[i], exp)\n        j := counter[d] - 1 // 获取 d 在数组中的索引 j\n        res[j] = nums[i]    // 将当前元素填入索引 j\n        counter[d]--        // 将 d 的数量减 1\n    }\n    // 使用结果覆盖原数组 nums\n    for i := 0; i < n; i++ {\n        nums[i] = res[i]\n    }\n}\n\n/* 基数排序 */\nfunc radixSort(nums []int) {\n    // 获取数组的最大元素,用于判断最大位数\n    max := math.MinInt\n    for _, num := range nums {\n        if num > max {\n            max = num\n        }\n    }\n    // 按照从低位到高位的顺序遍历\n    for exp := 1; max >= exp; exp *= 10 {\n        // 对数组元素的第 k 位执行计数排序\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // 即 exp = 10^(k-1)\n        countingSortDigit(nums, exp)\n    }\n}\n
radix_sort.swift
/* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nfunc digit(num: Int, exp: Int) -> Int {\n    // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n    (num / exp) % 10\n}\n\n/* 计数排序(根据 nums 第 k 位排序) */\nfunc countingSortDigit(nums: inout [Int], exp: Int) {\n    // 十进制的位范围为 0~9 ,因此需要长度为 10 的桶数组\n    var counter = Array(repeating: 0, count: 10)\n    // 统计 0~9 各数字的出现次数\n    for i in nums.indices {\n        let d = digit(num: nums[i], exp: exp) // 获取 nums[i] 第 k 位,记为 d\n        counter[d] += 1 // 统计数字 d 的出现次数\n    }\n    // 求前缀和,将“出现个数”转换为“数组索引”\n    for i in 1 ..< 10 {\n        counter[i] += counter[i - 1]\n    }\n    // 倒序遍历,根据桶内统计结果,将各元素填入 res\n    var res = Array(repeating: 0, count: nums.count)\n    for i in nums.indices.reversed() {\n        let d = digit(num: nums[i], exp: exp)\n        let j = counter[d] - 1 // 获取 d 在数组中的索引 j\n        res[j] = nums[i] // 将当前元素填入索引 j\n        counter[d] -= 1 // 将 d 的数量减 1\n    }\n    // 使用结果覆盖原数组 nums\n    for i in nums.indices {\n        nums[i] = res[i]\n    }\n}\n\n/* 基数排序 */\nfunc radixSort(nums: inout [Int]) {\n    // 获取数组的最大元素,用于判断最大位数\n    var m = Int.min\n    for num in nums {\n        if num > m {\n            m = num\n        }\n    }\n    // 按照从低位到高位的顺序遍历\n    for exp in sequence(first: 1, next: { m >= ($0 * 10) ? $0 * 10 : nil }) {\n        // 对数组元素的第 k 位执行计数排序\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // 即 exp = 10^(k-1)\n        countingSortDigit(nums: &nums, exp: exp)\n    }\n}\n
radix_sort.js
/* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nfunction digit(num, exp) {\n    // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n    return Math.floor(num / exp) % 10;\n}\n\n/* 计数排序(根据 nums 第 k 位排序) */\nfunction countingSortDigit(nums, exp) {\n    // 十进制的位范围为 0~9 ,因此需要长度为 10 的桶数组\n    const counter = new Array(10).fill(0);\n    const n = nums.length;\n    // 统计 0~9 各数字的出现次数\n    for (let i = 0; i < n; i++) {\n        const d = digit(nums[i], exp); // 获取 nums[i] 第 k 位,记为 d\n        counter[d]++; // 统计数字 d 的出现次数\n    }\n    // 求前缀和,将“出现个数”转换为“数组索引”\n    for (let i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // 倒序遍历,根据桶内统计结果,将各元素填入 res\n    const res = new Array(n).fill(0);\n    for (let i = n - 1; i >= 0; i--) {\n        const d = digit(nums[i], exp);\n        const j = counter[d] - 1; // 获取 d 在数组中的索引 j\n        res[j] = nums[i]; // 将当前元素填入索引 j\n        counter[d]--; // 将 d 的数量减 1\n    }\n    // 使用结果覆盖原数组 nums\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n\n/* 基数排序 */\nfunction radixSort(nums) {\n    // 获取数组的最大元素,用于判断最大位数\n    let m = Math.max(... nums);\n    // 按照从低位到高位的顺序遍历\n    for (let exp = 1; exp <= m; exp *= 10) {\n        // 对数组元素的第 k 位执行计数排序\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // 即 exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n    }\n}\n
radix_sort.ts
/* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nfunction digit(num: number, exp: number): number {\n    // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n    return Math.floor(num / exp) % 10;\n}\n\n/* 计数排序(根据 nums 第 k 位排序) */\nfunction countingSortDigit(nums: number[], exp: number): void {\n    // 十进制的位范围为 0~9 ,因此需要长度为 10 的桶数组\n    const counter = new Array(10).fill(0);\n    const n = nums.length;\n    // 统计 0~9 各数字的出现次数\n    for (let i = 0; i < n; i++) {\n        const d = digit(nums[i], exp); // 获取 nums[i] 第 k 位,记为 d\n        counter[d]++; // 统计数字 d 的出现次数\n    }\n    // 求前缀和,将“出现个数”转换为“数组索引”\n    for (let i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // 倒序遍历,根据桶内统计结果,将各元素填入 res\n    const res = new Array(n).fill(0);\n    for (let i = n - 1; i >= 0; i--) {\n        const d = digit(nums[i], exp);\n        const j = counter[d] - 1; // 获取 d 在数组中的索引 j\n        res[j] = nums[i]; // 将当前元素填入索引 j\n        counter[d]--; // 将 d 的数量减 1\n    }\n    // 使用结果覆盖原数组 nums\n    for (let i = 0; i < n; i++) {\n        nums[i] = res[i];\n    }\n}\n\n/* 基数排序 */\nfunction radixSort(nums: number[]): void {\n    // 获取数组的最大元素,用于判断最大位数\n    let m: number = Math.max(... nums);\n    // 按照从低位到高位的顺序遍历\n    for (let exp = 1; exp <= m; exp *= 10) {\n        // 对数组元素的第 k 位执行计数排序\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // 即 exp = 10^(k-1)\n        countingSortDigit(nums, exp);\n    }\n}\n
radix_sort.dart
/* 获取元素 _num 的第 k 位,其中 exp = 10^(k-1) */\nint digit(int _num, int exp) {\n  // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n  return (_num ~/ exp) % 10;\n}\n\n/* 计数排序(根据 nums 第 k 位排序) */\nvoid countingSortDigit(List<int> nums, int exp) {\n  // 十进制的位范围为 0~9 ,因此需要长度为 10 的桶数组\n  List<int> counter = List<int>.filled(10, 0);\n  int n = nums.length;\n  // 统计 0~9 各数字的出现次数\n  for (int i = 0; i < n; i++) {\n    int d = digit(nums[i], exp); // 获取 nums[i] 第 k 位,记为 d\n    counter[d]++; // 统计数字 d 的出现次数\n  }\n  // 求前缀和,将“出现个数”转换为“数组索引”\n  for (int i = 1; i < 10; i++) {\n    counter[i] += counter[i - 1];\n  }\n  // 倒序遍历,根据桶内统计结果,将各元素填入 res\n  List<int> res = List<int>.filled(n, 0);\n  for (int i = n - 1; i >= 0; i--) {\n    int d = digit(nums[i], exp);\n    int j = counter[d] - 1; // 获取 d 在数组中的索引 j\n    res[j] = nums[i]; // 将当前元素填入索引 j\n    counter[d]--; // 将 d 的数量减 1\n  }\n  // 使用结果覆盖原数组 nums\n  for (int i = 0; i < n; i++) nums[i] = res[i];\n}\n\n/* 基数排序 */\nvoid radixSort(List<int> nums) {\n  // 获取数组的最大元素,用于判断最大位数\n  // dart 中 int 的长度是 64 位的\n  int m = -1 << 63;\n  for (int _num in nums) if (_num > m) m = _num;\n  // 按照从低位到高位的顺序遍历\n  for (int exp = 1; exp <= m; exp *= 10)\n    // 对数组元素的第 k 位执行计数排序\n    // k = 1 -> exp = 1\n    // k = 2 -> exp = 10\n    // 即 exp = 10^(k-1)\n    countingSortDigit(nums, exp);\n}\n
radix_sort.rs
/* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nfn digit(num: i32, exp: i32) -> usize {\n    // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n    return ((num / exp) % 10) as usize;\n}\n\n/* 计数排序(根据 nums 第 k 位排序) */\nfn counting_sort_digit(nums: &mut [i32], exp: i32) {\n    // 十进制的位范围为 0~9 ,因此需要长度为 10 的桶数组\n    let mut counter = [0; 10];\n    let n = nums.len();\n    // 统计 0~9 各数字的出现次数\n    for i in 0..n {\n        let d = digit(nums[i], exp); // 获取 nums[i] 第 k 位,记为 d\n        counter[d] += 1; // 统计数字 d 的出现次数\n    }\n    // 求前缀和,将“出现个数”转换为“数组索引”\n    for i in 1..10 {\n        counter[i] += counter[i - 1];\n    }\n    // 倒序遍历,根据桶内统计结果,将各元素填入 res\n    let mut res = vec![0; n];\n    for i in (0..n).rev() {\n        let d = digit(nums[i], exp);\n        let j = counter[d] - 1; // 获取 d 在数组中的索引 j\n        res[j] = nums[i]; // 将当前元素填入索引 j\n        counter[d] -= 1; // 将 d 的数量减 1\n    }\n    // 使用结果覆盖原数组 nums\n    nums.copy_from_slice(&res);\n}\n\n/* 基数排序 */\nfn radix_sort(nums: &mut [i32]) {\n    // 获取数组的最大元素,用于判断最大位数\n    let m = *nums.into_iter().max().unwrap();\n    // 按照从低位到高位的顺序遍历\n    let mut exp = 1;\n    while exp <= m {\n        counting_sort_digit(nums, exp);\n        exp *= 10;\n    }\n}\n
radix_sort.c
/* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nint digit(int num, int exp) {\n    // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n    return (num / exp) % 10;\n}\n\n/* 计数排序(根据 nums 第 k 位排序) */\nvoid countingSortDigit(int nums[], int size, int exp) {\n    // 十进制的位范围为 0~9 ,因此需要长度为 10 的桶数组\n    int *counter = (int *)malloc((sizeof(int) * 10));\n    memset(counter, 0, sizeof(int) * 10); // 初始化为 0 以支持后续内存释放\n    // 统计 0~9 各数字的出现次数\n    for (int i = 0; i < size; i++) {\n        // 获取 nums[i] 第 k 位,记为 d\n        int d = digit(nums[i], exp);\n        // 统计数字 d 的出现次数\n        counter[d]++;\n    }\n    // 求前缀和,将“出现个数”转换为“数组索引”\n    for (int i = 1; i < 10; i++) {\n        counter[i] += counter[i - 1];\n    }\n    // 倒序遍历,根据桶内统计结果,将各元素填入 res\n    int *res = (int *)malloc(sizeof(int) * size);\n    for (int i = size - 1; i >= 0; i--) {\n        int d = digit(nums[i], exp);\n        int j = counter[d] - 1; // 获取 d 在数组中的索引 j\n        res[j] = nums[i];       // 将当前元素填入索引 j\n        counter[d]--;           // 将 d 的数量减 1\n    }\n    // 使用结果覆盖原数组 nums\n    for (int i = 0; i < size; i++) {\n        nums[i] = res[i];\n    }\n    // 释放内存\n    free(res);\n    free(counter);\n}\n\n/* 基数排序 */\nvoid radixSort(int nums[], int size) {\n    // 获取数组的最大元素,用于判断最大位数\n    int max = INT32_MIN;\n    for (int i = 0; i < size; i++) {\n        if (nums[i] > max) {\n            max = nums[i];\n        }\n    }\n    // 按照从低位到高位的顺序遍历\n    for (int exp = 1; max >= exp; exp *= 10)\n        // 对数组元素的第 k 位执行计数排序\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // 即 exp = 10^(k-1)\n        countingSortDigit(nums, size, exp);\n}\n
radix_sort.kt
/* 获取元素 num 的第 k 位,其中 exp = 10^(k-1) */\nfun digit(num: Int, exp: Int): Int {\n    // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n    return (num / exp) % 10\n}\n\n/* 计数排序(根据 nums 第 k 位排序) */\nfun countingSortDigit(nums: IntArray, exp: Int) {\n    // 十进制的位范围为 0~9 ,因此需要长度为 10 的桶数组\n    val counter = IntArray(10)\n    val n = nums.size\n    // 统计 0~9 各数字的出现次数\n    for (i in 0..<n) {\n        val d = digit(nums[i], exp) // 获取 nums[i] 第 k 位,记为 d\n        counter[d]++                // 统计数字 d 的出现次数\n    }\n    // 求前缀和,将“出现个数”转换为“数组索引”\n    for (i in 1..9) {\n        counter[i] += counter[i - 1]\n    }\n    // 倒序遍历,根据桶内统计结果,将各元素填入 res\n    val res = IntArray(n)\n    for (i in n - 1 downTo 0) {\n        val d = digit(nums[i], exp)\n        val j = counter[d] - 1 // 获取 d 在数组中的索引 j\n        res[j] = nums[i]       // 将当前元素填入索引 j\n        counter[d]--           // 将 d 的数量减 1\n    }\n    // 使用结果覆盖原数组 nums\n    for (i in 0..<n)\n        nums[i] = res[i]\n}\n\n/* 基数排序 */\nfun radixSort(nums: IntArray) {\n    // 获取数组的最大元素,用于判断最大位数\n    var m = Int.MIN_VALUE\n    for (num in nums) if (num > m) m = num\n    var exp = 1\n    // 按照从低位到高位的顺序遍历\n    while (exp <= m) {\n        // 对数组元素的第 k 位执行计数排序\n        // k = 1 -> exp = 1\n        // k = 2 -> exp = 10\n        // 即 exp = 10^(k-1)\n        countingSortDigit(nums, exp)\n        exp *= 10\n    }\n}\n
radix_sort.rb
### 获取元素 num 的第 k 位,其中 exp = 10^(k-1) ###\ndef digit(num, exp)\n  # 转入 exp 而非 k 可以避免在此重复执行昂贵的次方计算\n  (num / exp) % 10\nend\n\n### 计数排序(根据 nums 第 k 位排序)###\ndef counting_sort_digit(nums, exp)\n  # 十进制的位范围为 0~9 ,因此需要长度为 10 的桶数组\n  counter = Array.new(10, 0)\n  n = nums.length\n  # 统计 0~9 各数字的出现次数\n  for i in 0...n\n    d = digit(nums[i], exp) # 获取 nums[i] 第 k 位,记为 d\n    counter[d] += 1 # 统计数字 d 的出现次数\n  end\n  # 求前缀和,将“出现个数”转换为“数组索引”\n  (1...10).each { |i| counter[i] += counter[i - 1] }\n  # 倒序遍历,根据桶内统计结果,将各元素填入 res\n  res = Array.new(n, 0)\n  for i in (n - 1).downto(0)\n    d = digit(nums[i], exp)\n    j = counter[d] - 1 # 获取 d 在数组中的索引 j\n    res[j] = nums[i] # 将当前元素填入索引 j\n    counter[d] -= 1 # 将 d 的数量减 1\n  end\n  # 使用结果覆盖原数组 nums\n  (0...n).each { |i| nums[i] = res[i] }\nend\n\n### 基数排序 ###\ndef radix_sort(nums)\n  # 获取数组的最大元素,用于判断最大位数\n  m = nums.max\n  # 按照从低位到高位的顺序遍历\n  exp = 1\n  while exp <= m\n    # 对数组元素的第 k 位执行计数排序\n    # k = 1 -> exp = 1\n    # k = 2 -> exp = 10\n    # 即 exp = 10^(k-1)\n    counting_sort_digit(nums, exp)\n    exp *= 10\n  end\nend\n
可视化运行

全屏观看 >

为什么从最低位开始排序?

在连续的排序轮次中,后一轮排序会覆盖前一轮排序的结果。举例来说,如果第一轮排序结果 \\(a < b\\) ,而第二轮排序结果 \\(a > b\\) ,那么第二轮的结果将取代第一轮的结果。由于数字的高位优先级高于低位,因此应该先排序低位再排序高位。

","path":["第 11 章   排序","11.10   基数排序"],"tags":[]},{"location":"chapter_sorting/radix_sort/#11102","level":2,"title":"11.10.2   算法特性","text":"

相较于计数排序,基数排序适用于数值范围较大的情况,但前提是数据必须可以表示为固定位数的格式,且位数不能过大。例如,浮点数不适合使用基数排序,因为其位数 \\(k\\) 过大,可能导致时间复杂度 \\(O(nk) \\gg O(n^2)\\) 。

","path":["第 11 章   排序","11.10   基数排序"],"tags":[]},{"location":"chapter_sorting/selection_sort/","level":1,"title":"11.2   选择排序","text":"

选择排序(selection sort)的工作原理非常简单:开启一个循环,每轮从未排序区间选择最小的元素,将其放到已排序区间的末尾。

设数组的长度为 \\(n\\) ,选择排序的算法流程如图 11-2 所示。

  1. 初始状态下,所有元素未排序,即未排序(索引)区间为 \\([0, n-1]\\) 。
  2. 选取区间 \\([0, n-1]\\) 中的最小元素,将其与索引 \\(0\\) 处的元素交换。完成后,数组前 1 个元素已排序。
  3. 选取区间 \\([1, n-1]\\) 中的最小元素,将其与索引 \\(1\\) 处的元素交换。完成后,数组前 2 个元素已排序。
  4. 以此类推。经过 \\(n - 1\\) 轮选择与交换后,数组前 \\(n - 1\\) 个元素已排序。
  5. 仅剩的一个元素必定是最大元素,无须排序,因此数组排序完成。
<1><2><3><4><5><6><7><8><9><10><11>

图 11-2   选择排序步骤

在代码中,我们用 \\(k\\) 来记录未排序区间内的最小元素:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby selection_sort.py
def selection_sort(nums: list[int]):\n    \"\"\"选择排序\"\"\"\n    n = len(nums)\n    # 外循环:未排序区间为 [i, n-1]\n    for i in range(n - 1):\n        # 内循环:找到未排序区间内的最小元素\n        k = i\n        for j in range(i + 1, n):\n            if nums[j] < nums[k]:\n                k = j  # 记录最小元素的索引\n        # 将该最小元素与未排序区间的首个元素交换\n        nums[i], nums[k] = nums[k], nums[i]\n
selection_sort.cpp
/* 选择排序 */\nvoid selectionSort(vector<int> &nums) {\n    int n = nums.size();\n    // 外循环:未排序区间为 [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // 内循环:找到未排序区间内的最小元素\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // 记录最小元素的索引\n        }\n        // 将该最小元素与未排序区间的首个元素交换\n        swap(nums[i], nums[k]);\n    }\n}\n
selection_sort.java
/* 选择排序 */\nvoid selectionSort(int[] nums) {\n    int n = nums.length;\n    // 外循环:未排序区间为 [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // 内循环:找到未排序区间内的最小元素\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // 记录最小元素的索引\n        }\n        // 将该最小元素与未排序区间的首个元素交换\n        int temp = nums[i];\n        nums[i] = nums[k];\n        nums[k] = temp;\n    }\n}\n
selection_sort.cs
/* 选择排序 */\nvoid SelectionSort(int[] nums) {\n    int n = nums.Length;\n    // 外循环:未排序区间为 [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // 内循环:找到未排序区间内的最小元素\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // 记录最小元素的索引\n        }\n        // 将该最小元素与未排序区间的首个元素交换\n        (nums[k], nums[i]) = (nums[i], nums[k]);\n    }\n}\n
selection_sort.go
/* 选择排序 */\nfunc selectionSort(nums []int) {\n    n := len(nums)\n    // 外循环:未排序区间为 [i, n-1]\n    for i := 0; i < n-1; i++ {\n        // 内循环:找到未排序区间内的最小元素\n        k := i\n        for j := i + 1; j < n; j++ {\n            if nums[j] < nums[k] {\n                // 记录最小元素的索引\n                k = j\n            }\n        }\n        // 将该最小元素与未排序区间的首个元素交换\n        nums[i], nums[k] = nums[k], nums[i]\n\n    }\n}\n
selection_sort.swift
/* 选择排序 */\nfunc selectionSort(nums: inout [Int]) {\n    // 外循环:未排序区间为 [i, n-1]\n    for i in nums.indices.dropLast() {\n        // 内循环:找到未排序区间内的最小元素\n        var k = i\n        for j in nums.indices.dropFirst(i + 1) {\n            if nums[j] < nums[k] {\n                k = j // 记录最小元素的索引\n            }\n        }\n        // 将该最小元素与未排序区间的首个元素交换\n        nums.swapAt(i, k)\n    }\n}\n
selection_sort.js
/* 选择排序 */\nfunction selectionSort(nums) {\n    let n = nums.length;\n    // 外循环:未排序区间为 [i, n-1]\n    for (let i = 0; i < n - 1; i++) {\n        // 内循环:找到未排序区间内的最小元素\n        let k = i;\n        for (let j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k]) {\n                k = j; // 记录最小元素的索引\n            }\n        }\n        // 将该最小元素与未排序区间的首个元素交换\n        [nums[i], nums[k]] = [nums[k], nums[i]];\n    }\n}\n
selection_sort.ts
/* 选择排序 */\nfunction selectionSort(nums: number[]): void {\n    let n = nums.length;\n    // 外循环:未排序区间为 [i, n-1]\n    for (let i = 0; i < n - 1; i++) {\n        // 内循环:找到未排序区间内的最小元素\n        let k = i;\n        for (let j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k]) {\n                k = j; // 记录最小元素的索引\n            }\n        }\n        // 将该最小元素与未排序区间的首个元素交换\n        [nums[i], nums[k]] = [nums[k], nums[i]];\n    }\n}\n
selection_sort.dart
/* 选择排序 */\nvoid selectionSort(List<int> nums) {\n  int n = nums.length;\n  // 外循环:未排序区间为 [i, n-1]\n  for (int i = 0; i < n - 1; i++) {\n    // 内循环:找到未排序区间内的最小元素\n    int k = i;\n    for (int j = i + 1; j < n; j++) {\n      if (nums[j] < nums[k]) k = j; // 记录最小元素的索引\n    }\n    // 将该最小元素与未排序区间的首个元素交换\n    int temp = nums[i];\n    nums[i] = nums[k];\n    nums[k] = temp;\n  }\n}\n
selection_sort.rs
/* 选择排序 */\nfn selection_sort(nums: &mut [i32]) {\n    if nums.is_empty() {\n        return;\n    }\n    let n = nums.len();\n    // 外循环:未排序区间为 [i, n-1]\n    for i in 0..n - 1 {\n        // 内循环:找到未排序区间内的最小元素\n        let mut k = i;\n        for j in i + 1..n {\n            if nums[j] < nums[k] {\n                k = j; // 记录最小元素的索引\n            }\n        }\n        // 将该最小元素与未排序区间的首个元素交换\n        nums.swap(i, k);\n    }\n}\n
selection_sort.c
/* 选择排序 */\nvoid selectionSort(int nums[], int n) {\n    // 外循环:未排序区间为 [i, n-1]\n    for (int i = 0; i < n - 1; i++) {\n        // 内循环:找到未排序区间内的最小元素\n        int k = i;\n        for (int j = i + 1; j < n; j++) {\n            if (nums[j] < nums[k])\n                k = j; // 记录最小元素的索引\n        }\n        // 将该最小元素与未排序区间的首个元素交换\n        int temp = nums[i];\n        nums[i] = nums[k];\n        nums[k] = temp;\n    }\n}\n
selection_sort.kt
/* 选择排序 */\nfun selectionSort(nums: IntArray) {\n    val n = nums.size\n    // 外循环:未排序区间为 [i, n-1]\n    for (i in 0..<n - 1) {\n        var k = i\n        // 内循环:找到未排序区间内的最小元素\n        for (j in i + 1..<n) {\n            if (nums[j] < nums[k])\n                k = j // 记录最小元素的索引\n        }\n        // 将该最小元素与未排序区间的首个元素交换\n        val temp = nums[i]\n        nums[i] = nums[k]\n        nums[k] = temp\n    }\n}\n
selection_sort.rb
### 选择排序 ###\ndef selection_sort(nums)\n  n = nums.length\n  # 外循环:未排序区间为 [i, n-1]\n  for i in 0...(n - 1)\n    # 内循环:找到未排序区间内的最小元素\n    k = i\n    for j in (i + 1)...n\n      if nums[j] < nums[k]\n        k = j # 记录最小元素的索引\n      end\n    end\n    # 将该最小元素与未排序区间的首个元素交换\n    nums[i], nums[k] = nums[k], nums[i]\n  end\nend\n
可视化运行

全屏观看 >

","path":["第 11 章   排序","11.2   选择排序"],"tags":[]},{"location":"chapter_sorting/selection_sort/#1121","level":2,"title":"11.2.1   算法特性","text":"

图 11-3   选择排序非稳定示例

","path":["第 11 章   排序","11.2   选择排序"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/","level":1,"title":"11.1   排序算法","text":"

排序算法(sorting algorithm)用于对一组数据按照特定顺序进行排列。排序算法有着广泛的应用,因为有序数据通常能够被更高效地查找、分析和处理。

如图 11-1 所示,排序算法中的数据类型可以是整数、浮点数、字符或字符串等。排序的判断规则可根据需求设定,如数字大小、字符 ASCII 码顺序或自定义规则。

图 11-1   数据类型和判断规则示例

","path":["第 11 章   排序","11.1   排序算法"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/#1111","level":2,"title":"11.1.1   评价维度","text":"

运行效率:我们期望排序算法的时间复杂度尽量低,且总体操作数量较少(时间复杂度中的常数项变小)。对于大数据量的情况,运行效率显得尤为重要。

就地性:顾名思义,原地排序通过在原数组上直接操作实现排序,无须借助额外的辅助数组,从而节省内存。通常情况下,原地排序的数据搬运操作较少,运行速度也更快。

稳定性:稳定排序在完成排序后,相等元素在数组中的相对顺序不发生改变。

稳定排序是多级排序场景的必要条件。假设我们有一个存储学生信息的表格,第 1 列和第 2 列分别是姓名和年龄。在这种情况下,非稳定排序可能导致输入数据的有序性丧失:

# 输入数据是按照姓名排序好的\n# (name, age)\n  ('A', 19)\n  ('B', 18)\n  ('C', 21)\n  ('D', 19)\n  ('E', 23)\n\n# 假设使用非稳定排序算法按年龄排序列表,\n# 结果中 ('D', 19) 和 ('A', 19) 的相对位置改变,\n# 输入数据按姓名排序的性质丢失\n  ('B', 18)\n  ('D', 19)\n  ('A', 19)\n  ('C', 21)\n  ('E', 23)\n

自适应性:自适应排序能够利用输入数据已有的顺序信息来减少计算量,达到更优的时间效率。自适应排序算法的最佳时间复杂度通常优于平均时间复杂度。

是否基于比较:基于比较的排序依赖比较运算符(\\(<\\)、\\(=\\)、\\(>\\))来判断元素的相对顺序,从而排序整个数组,理论最优时间复杂度为 \\(O(n \\log n)\\) 。而非比较排序不使用比较运算符,时间复杂度可达 \\(O(n)\\) ,但其通用性相对较差。

","path":["第 11 章   排序","11.1   排序算法"],"tags":[]},{"location":"chapter_sorting/sorting_algorithm/#1112","level":2,"title":"11.1.2   理想排序算法","text":"

运行快、原地、稳定、自适应、通用性好。显然,迄今为止尚未发现兼具以上所有特性的排序算法。因此,在选择排序算法时,需要根据具体的数据特点和问题需求来决定。

接下来,我们将共同学习各种排序算法,并基于上述评价维度对各个排序算法的优缺点进行分析。

","path":["第 11 章   排序","11.1   排序算法"],"tags":[]},{"location":"chapter_sorting/summary/","level":1,"title":"11.11   小结","text":"","path":["第 11 章   排序","11.11   小结"],"tags":[]},{"location":"chapter_sorting/summary/#1","level":3,"title":"1.   重点回顾","text":"

图 11-19   排序算法对比

","path":["第 11 章   排序","11.11   小结"],"tags":[]},{"location":"chapter_sorting/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q:排序算法稳定性在什么情况下是必需的?

在现实中,我们有可能基于对象的某个属性进行排序。例如,学生有姓名和身高两个属性,我们希望实现一个多级排序:先按照姓名进行排序,得到 (A, 180) (B, 185) (C, 170) (D, 170) ;再对身高进行排序。由于排序算法不稳定,因此可能得到 (D, 170) (C, 170) (A, 180) (B, 185)

可以发现,学生 D 和 C 的位置发生了交换,姓名的有序性被破坏了,而这是我们不希望看到的。

Q:哨兵划分中“从右往左查找”与“从左往右查找”的顺序可以交换吗?

不行,当我们以最左端元素为基准数时,必须先“从右往左查找”再“从左往右查找”。这个结论有些反直觉,我们来剖析一下原因。

哨兵划分 partition() 的最后一步是交换 nums[left]nums[i] 。完成交换后,基准数左边的元素都 <= 基准数,这就要求最后一步交换前 nums[left] >= nums[i] 必须成立。假设我们先“从左往右查找”,那么如果找不到比基准数更大的元素,则会在 i == j 时跳出循环,此时可能 nums[j] == nums[i] > nums[left]。也就是说,此时最后一步交换操作会把一个比基准数更大的元素交换至数组最左端,导致哨兵划分失败。

举个例子,给定数组 [0, 0, 0, 0, 1] ,如果先“从左向右查找”,哨兵划分后数组为 [1, 0, 0, 0, 0] ,这个结果是不正确的。

再深入思考一下,如果我们选择 nums[right] 为基准数,那么正好反过来,必须先“从左往右查找”。

Q:关于快速排序的递归深度优化,为什么选短的数组能保证递归深度不超过 \\(\\log n\\) ?

递归深度就是当前未返回的递归方法的数量。每轮哨兵划分我们将原数组划分为两个子数组。在递归深度优化后,向下递归的子数组长度最大为原数组长度的一半。假设最差情况,一直为一半长度,那么最终的递归深度就是 \\(\\log n\\) 。

回顾原始的快速排序,我们有可能会连续地递归长度较大的数组,最差情况下为 \\(n\\)、\\(n - 1\\)、\\(\\dots\\)、\\(2\\)、\\(1\\) ,递归深度为 \\(n\\) 。递归深度优化可以避免这种情况出现。

Q:当数组中所有元素都相等时,快速排序的时间复杂度是 \\(O(n^2)\\) 吗?该如何处理这种退化情况?

是的。对于这种情况,可以考虑通过哨兵划分将数组划分为三个部分:小于、等于、大于基准数。仅向下递归小于和大于的两部分。在该方法下,输入元素全部相等的数组,仅一轮哨兵划分即可完成排序。

Q:桶排序的最差时间复杂度为什么是 \\(O(n^2)\\) ?

最差情况下,所有元素被分至同一个桶中。如果我们采用一个 \\(O(n^2)\\) 算法来排序这些元素,则时间复杂度为 \\(O(n^2)\\) 。

","path":["第 11 章   排序","11.11   小结"],"tags":[]},{"location":"chapter_stack_and_queue/","level":1,"title":"第 5 章   栈与队列","text":"

Abstract

栈如同叠猫猫,而队列就像猫猫排队。

两者分别代表先入后出和先入先出的逻辑关系。

","path":["第 5 章   栈与队列"],"tags":[]},{"location":"chapter_stack_and_queue/#_1","level":2,"title":"本章内容","text":"","path":["第 5 章   栈与队列"],"tags":[]},{"location":"chapter_stack_and_queue/deque/","level":1,"title":"5.3   双向队列","text":"

在队列中,我们仅能删除头部元素或在尾部添加元素。如图 5-7 所示,双向队列(double-ended queue)提供了更高的灵活性,允许在头部和尾部执行元素的添加或删除操作。

图 5-7   双向队列的操作

","path":["第 5 章   栈与队列","5.3   双向队列"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#531","level":2,"title":"5.3.1   双向队列常用操作","text":"

双向队列的常用操作如表 5-3 所示,具体的方法名称需要根据所使用的编程语言来确定。

表 5-3   双向队列操作效率

方法名 描述 时间复杂度 push_first() 将元素添加至队首 \\(O(1)\\) push_last() 将元素添加至队尾 \\(O(1)\\) pop_first() 删除队首元素 \\(O(1)\\) pop_last() 删除队尾元素 \\(O(1)\\) peek_first() 访问队首元素 \\(O(1)\\) peek_last() 访问队尾元素 \\(O(1)\\)

同样地,我们可以直接使用编程语言中已实现的双向队列类:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby deque.py
from collections import deque\n\n# 初始化双向队列\ndeq: deque[int] = deque()\n\n# 元素入队\ndeq.append(2)      # 添加至队尾\ndeq.append(5)\ndeq.append(4)\ndeq.appendleft(3)  # 添加至队首\ndeq.appendleft(1)\n\n# 访问元素\nfront: int = deq[0]  # 队首元素\nrear: int = deq[-1]  # 队尾元素\n\n# 元素出队\npop_front: int = deq.popleft()  # 队首元素出队\npop_rear: int = deq.pop()       # 队尾元素出队\n\n# 获取双向队列的长度\nsize: int = len(deq)\n\n# 判断双向队列是否为空\nis_empty: bool = len(deq) == 0\n
deque.cpp
/* 初始化双向队列 */\ndeque<int> deque;\n\n/* 元素入队 */\ndeque.push_back(2);   // 添加至队尾\ndeque.push_back(5);\ndeque.push_back(4);\ndeque.push_front(3);  // 添加至队首\ndeque.push_front(1);\n\n/* 访问元素 */\nint front = deque.front(); // 队首元素\nint back = deque.back();   // 队尾元素\n\n/* 元素出队 */\ndeque.pop_front();  // 队首元素出队\ndeque.pop_back();   // 队尾元素出队\n\n/* 获取双向队列的长度 */\nint size = deque.size();\n\n/* 判断双向队列是否为空 */\nbool empty = deque.empty();\n
deque.java
/* 初始化双向队列 */\nDeque<Integer> deque = new LinkedList<>();\n\n/* 元素入队 */\ndeque.offerLast(2);   // 添加至队尾\ndeque.offerLast(5);\ndeque.offerLast(4);\ndeque.offerFirst(3);  // 添加至队首\ndeque.offerFirst(1);\n\n/* 访问元素 */\nint peekFirst = deque.peekFirst();  // 队首元素\nint peekLast = deque.peekLast();    // 队尾元素\n\n/* 元素出队 */\nint popFirst = deque.pollFirst();  // 队首元素出队\nint popLast = deque.pollLast();    // 队尾元素出队\n\n/* 获取双向队列的长度 */\nint size = deque.size();\n\n/* 判断双向队列是否为空 */\nboolean isEmpty = deque.isEmpty();\n
deque.cs
/* 初始化双向队列 */\n// 在 C# 中,将链表 LinkedList 看作双向队列来使用\nLinkedList<int> deque = new();\n\n/* 元素入队 */\ndeque.AddLast(2);   // 添加至队尾\ndeque.AddLast(5);\ndeque.AddLast(4);\ndeque.AddFirst(3);  // 添加至队首\ndeque.AddFirst(1);\n\n/* 访问元素 */\nint peekFirst = deque.First.Value;  // 队首元素\nint peekLast = deque.Last.Value;    // 队尾元素\n\n/* 元素出队 */\ndeque.RemoveFirst();  // 队首元素出队\ndeque.RemoveLast();   // 队尾元素出队\n\n/* 获取双向队列的长度 */\nint size = deque.Count;\n\n/* 判断双向队列是否为空 */\nbool isEmpty = deque.Count == 0;\n
deque_test.go
/* 初始化双向队列 */\n// 在 Go 中,将 list 作为双向队列使用\ndeque := list.New()\n\n/* 元素入队 */\ndeque.PushBack(2)      // 添加至队尾\ndeque.PushBack(5)\ndeque.PushBack(4)\ndeque.PushFront(3)     // 添加至队首\ndeque.PushFront(1)\n\n/* 访问元素 */\nfront := deque.Front() // 队首元素\nrear := deque.Back()   // 队尾元素\n\n/* 元素出队 */\ndeque.Remove(front)    // 队首元素出队\ndeque.Remove(rear)     // 队尾元素出队\n\n/* 获取双向队列的长度 */\nsize := deque.Len()\n\n/* 判断双向队列是否为空 */\nisEmpty := deque.Len() == 0\n
deque.swift
/* 初始化双向队列 */\n// Swift 没有内置的双向队列类,可以把 Array 当作双向队列来使用\nvar deque: [Int] = []\n\n/* 元素入队 */\ndeque.append(2) // 添加至队尾\ndeque.append(5)\ndeque.append(4)\ndeque.insert(3, at: 0) // 添加至队首\ndeque.insert(1, at: 0)\n\n/* 访问元素 */\nlet peekFirst = deque.first! // 队首元素\nlet peekLast = deque.last! // 队尾元素\n\n/* 元素出队 */\n// 使用 Array 模拟时 popFirst 的复杂度为 O(n)\nlet popFirst = deque.removeFirst() // 队首元素出队\nlet popLast = deque.removeLast() // 队尾元素出队\n\n/* 获取双向队列的长度 */\nlet size = deque.count\n\n/* 判断双向队列是否为空 */\nlet isEmpty = deque.isEmpty\n
deque.js
/* 初始化双向队列 */\n// JavaScript 没有内置的双端队列,只能把 Array 当作双端队列来使用\nconst deque = [];\n\n/* 元素入队 */\ndeque.push(2);\ndeque.push(5);\ndeque.push(4);\n// 请注意,由于是数组,unshift() 方法的时间复杂度为 O(n)\ndeque.unshift(3);\ndeque.unshift(1);\n\n/* 访问元素 */\nconst peekFirst = deque[0];\nconst peekLast = deque[deque.length - 1];\n\n/* 元素出队 */\n// 请注意,由于是数组,shift() 方法的时间复杂度为 O(n)\nconst popFront = deque.shift();\nconst popBack = deque.pop();\n\n/* 获取双向队列的长度 */\nconst size = deque.length;\n\n/* 判断双向队列是否为空 */\nconst isEmpty = size === 0;\n
deque.ts
/* 初始化双向队列 */\n// TypeScript 没有内置的双端队列,只能把 Array 当作双端队列来使用\nconst deque: number[] = [];\n\n/* 元素入队 */\ndeque.push(2);\ndeque.push(5);\ndeque.push(4);\n// 请注意,由于是数组,unshift() 方法的时间复杂度为 O(n)\ndeque.unshift(3);\ndeque.unshift(1);\n\n/* 访问元素 */\nconst peekFirst: number = deque[0];\nconst peekLast: number = deque[deque.length - 1];\n\n/* 元素出队 */\n// 请注意,由于是数组,shift() 方法的时间复杂度为 O(n)\nconst popFront: number = deque.shift() as number;\nconst popBack: number = deque.pop() as number;\n\n/* 获取双向队列的长度 */\nconst size: number = deque.length;\n\n/* 判断双向队列是否为空 */\nconst isEmpty: boolean = size === 0;\n
deque.dart
/* 初始化双向队列 */\n// 在 Dart 中,Queue 被定义为双向队列\nQueue<int> deque = Queue<int>();\n\n/* 元素入队 */\ndeque.addLast(2);  // 添加至队尾\ndeque.addLast(5);\ndeque.addLast(4);\ndeque.addFirst(3); // 添加至队首\ndeque.addFirst(1);\n\n/* 访问元素 */\nint peekFirst = deque.first; // 队首元素\nint peekLast = deque.last;   // 队尾元素\n\n/* 元素出队 */\nint popFirst = deque.removeFirst(); // 队首元素出队\nint popLast = deque.removeLast();   // 队尾元素出队\n\n/* 获取双向队列的长度 */\nint size = deque.length;\n\n/* 判断双向队列是否为空 */\nbool isEmpty = deque.isEmpty;\n
deque.rs
/* 初始化双向队列 */\nlet mut deque: VecDeque<u32> = VecDeque::new();\n\n/* 元素入队 */\ndeque.push_back(2);  // 添加至队尾\ndeque.push_back(5);\ndeque.push_back(4);\ndeque.push_front(3); // 添加至队首\ndeque.push_front(1);\n\n/* 访问元素 */\nif let Some(front) = deque.front() { // 队首元素\n}\nif let Some(rear) = deque.back() {   // 队尾元素\n}\n\n/* 元素出队 */\nif let Some(pop_front) = deque.pop_front() { // 队首元素出队\n}\nif let Some(pop_rear) = deque.pop_back() {   // 队尾元素出队\n}\n\n/* 获取双向队列的长度 */\nlet size = deque.len();\n\n/* 判断双向队列是否为空 */\nlet is_empty = deque.is_empty();\n
deque.c
// C 未提供内置双向队列\n
deque.kt
/* 初始化双向队列 */\nval deque = LinkedList<Int>()\n\n/* 元素入队 */\ndeque.offerLast(2)  // 添加至队尾\ndeque.offerLast(5)\ndeque.offerLast(4)\ndeque.offerFirst(3) // 添加至队首\ndeque.offerFirst(1)\n\n/* 访问元素 */\nval peekFirst = deque.peekFirst() // 队首元素\nval peekLast = deque.peekLast()   // 队尾元素\n\n/* 元素出队 */\nval popFirst = deque.pollFirst() // 队首元素出队\nval popLast = deque.pollLast()   // 队尾元素出队\n\n/* 获取双向队列的长度 */\nval size = deque.size\n\n/* 判断双向队列是否为空 */\nval isEmpty = deque.isEmpty()\n
deque.rb
# 初始化双向队列\n# Ruby 没有内直的双端队列,只能把 Array 当作双端队列来使用\ndeque = []\n\n# 元素如队\ndeque << 2\ndeque << 5\ndeque << 4\n# 请注意,由于是数组,Array#unshift 方法的时间复杂度为 O(n)\ndeque.unshift(3)\ndeque.unshift(1)\n\n# 访问元素\npeek_first = deque.first\npeek_last = deque.last\n\n# 元素出队\n# 请注意,由于是数组, Array#shift 方法的时间复杂度为 O(n)\npop_front = deque.shift\npop_back = deque.pop\n\n# 获取双向队列的长度\nsize = deque.length\n\n# 判断双向队列是否为空\nis_empty = size.zero?\n
可视化运行

全屏观看 >

","path":["第 5 章   栈与队列","5.3   双向队列"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#532","level":2,"title":"5.3.2   双向队列实现 *","text":"

双向队列的实现与队列类似,可以选择链表或数组作为底层数据结构。

","path":["第 5 章   栈与队列","5.3   双向队列"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#1","level":3,"title":"1.   基于双向链表的实现","text":"

回顾上一节内容,我们使用普通单向链表来实现队列,因为它可以方便地删除头节点(对应出队操作)和在尾节点后添加新节点(对应入队操作)。

对于双向队列而言,头部和尾部都可以执行入队和出队操作。换句话说,双向队列需要实现另一个对称方向的操作。为此,我们采用“双向链表”作为双向队列的底层数据结构。

如图 5-8 所示,我们将双向链表的头节点和尾节点视为双向队列的队首和队尾,同时实现在两端添加和删除节点的功能。

<1><2><3><4><5>

图 5-8   基于链表实现双向队列的入队出队操作

实现代码如下所示:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linkedlist_deque.py
class ListNode:\n    \"\"\"双向链表节点\"\"\"\n\n    def __init__(self, val: int):\n        \"\"\"构造方法\"\"\"\n        self.val: int = val\n        self.next: ListNode | None = None  # 后继节点引用\n        self.prev: ListNode | None = None  # 前驱节点引用\n\nclass LinkedListDeque:\n    \"\"\"基于双向链表实现的双向队列\"\"\"\n\n    def __init__(self):\n        \"\"\"构造方法\"\"\"\n        self._front: ListNode | None = None  # 头节点 front\n        self._rear: ListNode | None = None  # 尾节点 rear\n        self._size: int = 0  # 双向队列的长度\n\n    def size(self) -> int:\n        \"\"\"获取双向队列的长度\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"判断双向队列是否为空\"\"\"\n        return self._size == 0\n\n    def push(self, num: int, is_front: bool):\n        \"\"\"入队操作\"\"\"\n        node = ListNode(num)\n        # 若链表为空,则令 front 和 rear 都指向 node\n        if self.is_empty():\n            self._front = self._rear = node\n        # 队首入队操作\n        elif is_front:\n            # 将 node 添加至链表头部\n            self._front.prev = node\n            node.next = self._front\n            self._front = node  # 更新头节点\n        # 队尾入队操作\n        else:\n            # 将 node 添加至链表尾部\n            self._rear.next = node\n            node.prev = self._rear\n            self._rear = node  # 更新尾节点\n        self._size += 1  # 更新队列长度\n\n    def push_first(self, num: int):\n        \"\"\"队首入队\"\"\"\n        self.push(num, True)\n\n    def push_last(self, num: int):\n        \"\"\"队尾入队\"\"\"\n        self.push(num, False)\n\n    def pop(self, is_front: bool) -> int:\n        \"\"\"出队操作\"\"\"\n        if self.is_empty():\n            raise IndexError(\"双向队列为空\")\n        # 队首出队操作\n        if is_front:\n            val: int = self._front.val  # 暂存头节点值\n            # 删除头节点\n            fnext: ListNode | None = self._front.next\n            if fnext is not None:\n                fnext.prev = None\n                self._front.next = None\n            self._front = fnext  # 更新头节点\n        # 队尾出队操作\n        else:\n            val: int = self._rear.val  # 暂存尾节点值\n            # 删除尾节点\n            rprev: ListNode | None = self._rear.prev\n            if rprev is not None:\n                rprev.next = None\n                self._rear.prev = None\n            self._rear = rprev  # 更新尾节点\n        self._size -= 1  # 更新队列长度\n        return val\n\n    def pop_first(self) -> int:\n        \"\"\"队首出队\"\"\"\n        return self.pop(True)\n\n    def pop_last(self) -> int:\n        \"\"\"队尾出队\"\"\"\n        return self.pop(False)\n\n    def peek_first(self) -> int:\n        \"\"\"访问队首元素\"\"\"\n        if self.is_empty():\n            raise IndexError(\"双向队列为空\")\n        return self._front.val\n\n    def peek_last(self) -> int:\n        \"\"\"访问队尾元素\"\"\"\n        if self.is_empty():\n            raise IndexError(\"双向队列为空\")\n        return self._rear.val\n\n    def to_array(self) -> list[int]:\n        \"\"\"返回数组用于打印\"\"\"\n        node = self._front\n        res = [0] * self.size()\n        for i in range(self.size()):\n            res[i] = node.val\n            node = node.next\n        return res\n
linkedlist_deque.cpp
/* 双向链表节点 */\nstruct DoublyListNode {\n    int val;              // 节点值\n    DoublyListNode *next; // 后继节点指针\n    DoublyListNode *prev; // 前驱节点指针\n    DoublyListNode(int val) : val(val), prev(nullptr), next(nullptr) {\n    }\n};\n\n/* 基于双向链表实现的双向队列 */\nclass LinkedListDeque {\n  private:\n    DoublyListNode *front, *rear; // 头节点 front ,尾节点 rear\n    int queSize = 0;              // 双向队列的长度\n\n  public:\n    /* 构造方法 */\n    LinkedListDeque() : front(nullptr), rear(nullptr) {\n    }\n\n    /* 析构方法 */\n    ~LinkedListDeque() {\n        // 遍历链表删除节点,释放内存\n        DoublyListNode *pre, *cur = front;\n        while (cur != nullptr) {\n            pre = cur;\n            cur = cur->next;\n            delete pre;\n        }\n    }\n\n    /* 获取双向队列的长度 */\n    int size() {\n        return queSize;\n    }\n\n    /* 判断双向队列是否为空 */\n    bool isEmpty() {\n        return size() == 0;\n    }\n\n    /* 入队操作 */\n    void push(int num, bool isFront) {\n        DoublyListNode *node = new DoublyListNode(num);\n        // 若链表为空,则令 front 和 rear 都指向 node\n        if (isEmpty())\n            front = rear = node;\n        // 队首入队操作\n        else if (isFront) {\n            // 将 node 添加至链表头部\n            front->prev = node;\n            node->next = front;\n            front = node; // 更新头节点\n        // 队尾入队操作\n        } else {\n            // 将 node 添加至链表尾部\n            rear->next = node;\n            node->prev = rear;\n            rear = node; // 更新尾节点\n        }\n        queSize++; // 更新队列长度\n    }\n\n    /* 队首入队 */\n    void pushFirst(int num) {\n        push(num, true);\n    }\n\n    /* 队尾入队 */\n    void pushLast(int num) {\n        push(num, false);\n    }\n\n    /* 出队操作 */\n    int pop(bool isFront) {\n        if (isEmpty())\n            throw out_of_range(\"队列为空\");\n        int val;\n        // 队首出队操作\n        if (isFront) {\n            val = front->val; // 暂存头节点值\n            // 删除头节点\n            DoublyListNode *fNext = front->next;\n            if (fNext != nullptr) {\n                fNext->prev = nullptr;\n                front->next = nullptr;\n            }\n            delete front;\n            front = fNext; // 更新头节点\n        // 队尾出队操作\n        } else {\n            val = rear->val; // 暂存尾节点值\n            // 删除尾节点\n            DoublyListNode *rPrev = rear->prev;\n            if (rPrev != nullptr) {\n                rPrev->next = nullptr;\n                rear->prev = nullptr;\n            }\n            delete rear;\n            rear = rPrev; // 更新尾节点\n        }\n        queSize--; // 更新队列长度\n        return val;\n    }\n\n    /* 队首出队 */\n    int popFirst() {\n        return pop(true);\n    }\n\n    /* 队尾出队 */\n    int popLast() {\n        return pop(false);\n    }\n\n    /* 访问队首元素 */\n    int peekFirst() {\n        if (isEmpty())\n            throw out_of_range(\"双向队列为空\");\n        return front->val;\n    }\n\n    /* 访问队尾元素 */\n    int peekLast() {\n        if (isEmpty())\n            throw out_of_range(\"双向队列为空\");\n        return rear->val;\n    }\n\n    /* 返回数组用于打印 */\n    vector<int> toVector() {\n        DoublyListNode *node = front;\n        vector<int> res(size());\n        for (int i = 0; i < res.size(); i++) {\n            res[i] = node->val;\n            node = node->next;\n        }\n        return res;\n    }\n};\n
linkedlist_deque.java
/* 双向链表节点 */\nclass ListNode {\n    int val; // 节点值\n    ListNode next; // 后继节点引用\n    ListNode prev; // 前驱节点引用\n\n    ListNode(int val) {\n        this.val = val;\n        prev = next = null;\n    }\n}\n\n/* 基于双向链表实现的双向队列 */\nclass LinkedListDeque {\n    private ListNode front, rear; // 头节点 front ,尾节点 rear\n    private int queSize = 0; // 双向队列的长度\n\n    public LinkedListDeque() {\n        front = rear = null;\n    }\n\n    /* 获取双向队列的长度 */\n    public int size() {\n        return queSize;\n    }\n\n    /* 判断双向队列是否为空 */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* 入队操作 */\n    private void push(int num, boolean isFront) {\n        ListNode node = new ListNode(num);\n        // 若链表为空,则令 front 和 rear 都指向 node\n        if (isEmpty())\n            front = rear = node;\n        // 队首入队操作\n        else if (isFront) {\n            // 将 node 添加至链表头部\n            front.prev = node;\n            node.next = front;\n            front = node; // 更新头节点\n        // 队尾入队操作\n        } else {\n            // 将 node 添加至链表尾部\n            rear.next = node;\n            node.prev = rear;\n            rear = node; // 更新尾节点\n        }\n        queSize++; // 更新队列长度\n    }\n\n    /* 队首入队 */\n    public void pushFirst(int num) {\n        push(num, true);\n    }\n\n    /* 队尾入队 */\n    public void pushLast(int num) {\n        push(num, false);\n    }\n\n    /* 出队操作 */\n    private int pop(boolean isFront) {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        int val;\n        // 队首出队操作\n        if (isFront) {\n            val = front.val; // 暂存头节点值\n            // 删除头节点\n            ListNode fNext = front.next;\n            if (fNext != null) {\n                fNext.prev = null;\n                front.next = null;\n            }\n            front = fNext; // 更新头节点\n        // 队尾出队操作\n        } else {\n            val = rear.val; // 暂存尾节点值\n            // 删除尾节点\n            ListNode rPrev = rear.prev;\n            if (rPrev != null) {\n                rPrev.next = null;\n                rear.prev = null;\n            }\n            rear = rPrev; // 更新尾节点\n        }\n        queSize--; // 更新队列长度\n        return val;\n    }\n\n    /* 队首出队 */\n    public int popFirst() {\n        return pop(true);\n    }\n\n    /* 队尾出队 */\n    public int popLast() {\n        return pop(false);\n    }\n\n    /* 访问队首元素 */\n    public int peekFirst() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return front.val;\n    }\n\n    /* 访问队尾元素 */\n    public int peekLast() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return rear.val;\n    }\n\n    /* 返回数组用于打印 */\n    public int[] toArray() {\n        ListNode node = front;\n        int[] res = new int[size()];\n        for (int i = 0; i < res.length; i++) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_deque.cs
/* 双向链表节点 */\nclass ListNode(int val) {\n    public int val = val;       // 节点值\n    public ListNode? next = null; // 后继节点引用\n    public ListNode? prev = null; // 前驱节点引用\n}\n\n/* 基于双向链表实现的双向队列 */\nclass LinkedListDeque {\n    ListNode? front, rear; // 头节点 front, 尾节点 rear\n    int queSize = 0;      // 双向队列的长度\n\n    public LinkedListDeque() {\n        front = null;\n        rear = null;\n    }\n\n    /* 获取双向队列的长度 */\n    public int Size() {\n        return queSize;\n    }\n\n    /* 判断双向队列是否为空 */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* 入队操作 */\n    void Push(int num, bool isFront) {\n        ListNode node = new(num);\n        // 若链表为空,则令 front 和 rear 都指向 node\n        if (IsEmpty()) {\n            front = node;\n            rear = node;\n        }\n        // 队首入队操作\n        else if (isFront) {\n            // 将 node 添加至链表头部\n            front!.prev = node;\n            node.next = front;\n            front = node; // 更新头节点                           \n        }\n        // 队尾入队操作\n        else {\n            // 将 node 添加至链表尾部\n            rear!.next = node;\n            node.prev = rear;\n            rear = node;  // 更新尾节点\n        }\n\n        queSize++; // 更新队列长度\n    }\n\n    /* 队首入队 */\n    public void PushFirst(int num) {\n        Push(num, true);\n    }\n\n    /* 队尾入队 */\n    public void PushLast(int num) {\n        Push(num, false);\n    }\n\n    /* 出队操作 */\n    int? Pop(bool isFront) {\n        if (IsEmpty())\n            throw new Exception();\n        int? val;\n        // 队首出队操作\n        if (isFront) {\n            val = front?.val; // 暂存头节点值\n            // 删除头节点\n            ListNode? fNext = front?.next;\n            if (fNext != null) {\n                fNext.prev = null;\n                front!.next = null;\n            }\n            front = fNext;   // 更新头节点\n        }\n        // 队尾出队操作\n        else {\n            val = rear?.val;  // 暂存尾节点值\n            // 删除尾节点\n            ListNode? rPrev = rear?.prev;\n            if (rPrev != null) {\n                rPrev.next = null;\n                rear!.prev = null;\n            }\n            rear = rPrev;    // 更新尾节点\n        }\n\n        queSize--; // 更新队列长度\n        return val;\n    }\n\n    /* 队首出队 */\n    public int? PopFirst() {\n        return Pop(true);\n    }\n\n    /* 队尾出队 */\n    public int? PopLast() {\n        return Pop(false);\n    }\n\n    /* 访问队首元素 */\n    public int? PeekFirst() {\n        if (IsEmpty())\n            throw new Exception();\n        return front?.val;\n    }\n\n    /* 访问队尾元素 */\n    public int? PeekLast() {\n        if (IsEmpty())\n            throw new Exception();\n        return rear?.val;\n    }\n\n    /* 返回数组用于打印 */\n    public int?[] ToArray() {\n        ListNode? node = front;\n        int?[] res = new int?[Size()];\n        for (int i = 0; i < res.Length; i++) {\n            res[i] = node?.val;\n            node = node?.next;\n        }\n\n        return res;\n    }\n}\n
linkedlist_deque.go
/* 基于双向链表实现的双向队列 */\ntype linkedListDeque struct {\n    // 使用内置包 list\n    data *list.List\n}\n\n/* 初始化双端队列 */\nfunc newLinkedListDeque() *linkedListDeque {\n    return &linkedListDeque{\n        data: list.New(),\n    }\n}\n\n/* 队首元素入队 */\nfunc (s *linkedListDeque) pushFirst(value any) {\n    s.data.PushFront(value)\n}\n\n/* 队尾元素入队 */\nfunc (s *linkedListDeque) pushLast(value any) {\n    s.data.PushBack(value)\n}\n\n/* 队首元素出队 */\nfunc (s *linkedListDeque) popFirst() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Front()\n    s.data.Remove(e)\n    return e.Value\n}\n\n/* 队尾元素出队 */\nfunc (s *linkedListDeque) popLast() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Back()\n    s.data.Remove(e)\n    return e.Value\n}\n\n/* 访问队首元素 */\nfunc (s *linkedListDeque) peekFirst() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Front()\n    return e.Value\n}\n\n/* 访问队尾元素 */\nfunc (s *linkedListDeque) peekLast() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Back()\n    return e.Value\n}\n\n/* 获取队列的长度 */\nfunc (s *linkedListDeque) size() int {\n    return s.data.Len()\n}\n\n/* 判断队列是否为空 */\nfunc (s *linkedListDeque) isEmpty() bool {\n    return s.data.Len() == 0\n}\n\n/* 获取 List 用于打印 */\nfunc (s *linkedListDeque) toList() *list.List {\n    return s.data\n}\n
linkedlist_deque.swift
/* 双向链表节点 */\nclass ListNode {\n    var val: Int // 节点值\n    var next: ListNode? // 后继节点引用\n    weak var prev: ListNode? // 前驱节点引用\n\n    init(val: Int) {\n        self.val = val\n    }\n}\n\n/* 基于双向链表实现的双向队列 */\nclass LinkedListDeque {\n    private var front: ListNode? // 头节点 front\n    private var rear: ListNode? // 尾节点 rear\n    private var _size: Int // 双向队列的长度\n\n    init() {\n        _size = 0\n    }\n\n    /* 获取双向队列的长度 */\n    func size() -> Int {\n        _size\n    }\n\n    /* 判断双向队列是否为空 */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* 入队操作 */\n    private func push(num: Int, isFront: Bool) {\n        let node = ListNode(val: num)\n        // 若链表为空,则令 front 和 rear 都指向 node\n        if isEmpty() {\n            front = node\n            rear = node\n        }\n        // 队首入队操作\n        else if isFront {\n            // 将 node 添加至链表头部\n            front?.prev = node\n            node.next = front\n            front = node // 更新头节点\n        }\n        // 队尾入队操作\n        else {\n            // 将 node 添加至链表尾部\n            rear?.next = node\n            node.prev = rear\n            rear = node // 更新尾节点\n        }\n        _size += 1 // 更新队列长度\n    }\n\n    /* 队首入队 */\n    func pushFirst(num: Int) {\n        push(num: num, isFront: true)\n    }\n\n    /* 队尾入队 */\n    func pushLast(num: Int) {\n        push(num: num, isFront: false)\n    }\n\n    /* 出队操作 */\n    private func pop(isFront: Bool) -> Int {\n        if isEmpty() {\n            fatalError(\"双向队列为空\")\n        }\n        let val: Int\n        // 队首出队操作\n        if isFront {\n            val = front!.val // 暂存头节点值\n            // 删除头节点\n            let fNext = front?.next\n            if fNext != nil {\n                fNext?.prev = nil\n                front?.next = nil\n            }\n            front = fNext // 更新头节点\n        }\n        // 队尾出队操作\n        else {\n            val = rear!.val // 暂存尾节点值\n            // 删除尾节点\n            let rPrev = rear?.prev\n            if rPrev != nil {\n                rPrev?.next = nil\n                rear?.prev = nil\n            }\n            rear = rPrev // 更新尾节点\n        }\n        _size -= 1 // 更新队列长度\n        return val\n    }\n\n    /* 队首出队 */\n    func popFirst() -> Int {\n        pop(isFront: true)\n    }\n\n    /* 队尾出队 */\n    func popLast() -> Int {\n        pop(isFront: false)\n    }\n\n    /* 访问队首元素 */\n    func peekFirst() -> Int {\n        if isEmpty() {\n            fatalError(\"双向队列为空\")\n        }\n        return front!.val\n    }\n\n    /* 访问队尾元素 */\n    func peekLast() -> Int {\n        if isEmpty() {\n            fatalError(\"双向队列为空\")\n        }\n        return rear!.val\n    }\n\n    /* 返回数组用于打印 */\n    func toArray() -> [Int] {\n        var node = front\n        var res = Array(repeating: 0, count: size())\n        for i in res.indices {\n            res[i] = node!.val\n            node = node?.next\n        }\n        return res\n    }\n}\n
linkedlist_deque.js
/* 双向链表节点 */\nclass ListNode {\n    prev; // 前驱节点引用 (指针)\n    next; // 后继节点引用 (指针)\n    val; // 节点值\n\n    constructor(val) {\n        this.val = val;\n        this.next = null;\n        this.prev = null;\n    }\n}\n\n/* 基于双向链表实现的双向队列 */\nclass LinkedListDeque {\n    #front; // 头节点 front\n    #rear; // 尾节点 rear\n    #queSize; // 双向队列的长度\n\n    constructor() {\n        this.#front = null;\n        this.#rear = null;\n        this.#queSize = 0;\n    }\n\n    /* 队尾入队操作 */\n    pushLast(val) {\n        const node = new ListNode(val);\n        // 若链表为空,则令 front 和 rear 都指向 node\n        if (this.#queSize === 0) {\n            this.#front = node;\n            this.#rear = node;\n        } else {\n            // 将 node 添加至链表尾部\n            this.#rear.next = node;\n            node.prev = this.#rear;\n            this.#rear = node; // 更新尾节点\n        }\n        this.#queSize++;\n    }\n\n    /* 队首入队操作 */\n    pushFirst(val) {\n        const node = new ListNode(val);\n        // 若链表为空,则令 front 和 rear 都指向 node\n        if (this.#queSize === 0) {\n            this.#front = node;\n            this.#rear = node;\n        } else {\n            // 将 node 添加至链表头部\n            this.#front.prev = node;\n            node.next = this.#front;\n            this.#front = node; // 更新头节点\n        }\n        this.#queSize++;\n    }\n\n    /* 队尾出队操作 */\n    popLast() {\n        if (this.#queSize === 0) {\n            return null;\n        }\n        const value = this.#rear.val; // 存储尾节点值\n        // 删除尾节点\n        let temp = this.#rear.prev;\n        if (temp !== null) {\n            temp.next = null;\n            this.#rear.prev = null;\n        }\n        this.#rear = temp; // 更新尾节点\n        this.#queSize--;\n        return value;\n    }\n\n    /* 队首出队操作 */\n    popFirst() {\n        if (this.#queSize === 0) {\n            return null;\n        }\n        const value = this.#front.val; // 存储尾节点值\n        // 删除头节点\n        let temp = this.#front.next;\n        if (temp !== null) {\n            temp.prev = null;\n            this.#front.next = null;\n        }\n        this.#front = temp; // 更新头节点\n        this.#queSize--;\n        return value;\n    }\n\n    /* 访问队尾元素 */\n    peekLast() {\n        return this.#queSize === 0 ? null : this.#rear.val;\n    }\n\n    /* 访问队首元素 */\n    peekFirst() {\n        return this.#queSize === 0 ? null : this.#front.val;\n    }\n\n    /* 获取双向队列的长度 */\n    size() {\n        return this.#queSize;\n    }\n\n    /* 判断双向队列是否为空 */\n    isEmpty() {\n        return this.#queSize === 0;\n    }\n\n    /* 打印双向队列 */\n    print() {\n        const arr = [];\n        let temp = this.#front;\n        while (temp !== null) {\n            arr.push(temp.val);\n            temp = temp.next;\n        }\n        console.log('[' + arr.join(', ') + ']');\n    }\n}\n
linkedlist_deque.ts
/* 双向链表节点 */\nclass ListNode {\n    prev: ListNode; // 前驱节点引用 (指针)\n    next: ListNode; // 后继节点引用 (指针)\n    val: number; // 节点值\n\n    constructor(val: number) {\n        this.val = val;\n        this.next = null;\n        this.prev = null;\n    }\n}\n\n/* 基于双向链表实现的双向队列 */\nclass LinkedListDeque {\n    private front: ListNode; // 头节点 front\n    private rear: ListNode; // 尾节点 rear\n    private queSize: number; // 双向队列的长度\n\n    constructor() {\n        this.front = null;\n        this.rear = null;\n        this.queSize = 0;\n    }\n\n    /* 队尾入队操作 */\n    pushLast(val: number): void {\n        const node: ListNode = new ListNode(val);\n        // 若链表为空,则令 front 和 rear 都指向 node\n        if (this.queSize === 0) {\n            this.front = node;\n            this.rear = node;\n        } else {\n            // 将 node 添加至链表尾部\n            this.rear.next = node;\n            node.prev = this.rear;\n            this.rear = node; // 更新尾节点\n        }\n        this.queSize++;\n    }\n\n    /* 队首入队操作 */\n    pushFirst(val: number): void {\n        const node: ListNode = new ListNode(val);\n        // 若链表为空,则令 front 和 rear 都指向 node\n        if (this.queSize === 0) {\n            this.front = node;\n            this.rear = node;\n        } else {\n            // 将 node 添加至链表头部\n            this.front.prev = node;\n            node.next = this.front;\n            this.front = node; // 更新头节点\n        }\n        this.queSize++;\n    }\n\n    /* 队尾出队操作 */\n    popLast(): number {\n        if (this.queSize === 0) {\n            return null;\n        }\n        const value: number = this.rear.val; // 存储尾节点值\n        // 删除尾节点\n        let temp: ListNode = this.rear.prev;\n        if (temp !== null) {\n            temp.next = null;\n            this.rear.prev = null;\n        }\n        this.rear = temp; // 更新尾节点\n        this.queSize--;\n        return value;\n    }\n\n    /* 队首出队操作 */\n    popFirst(): number {\n        if (this.queSize === 0) {\n            return null;\n        }\n        const value: number = this.front.val; // 存储尾节点值\n        // 删除头节点\n        let temp: ListNode = this.front.next;\n        if (temp !== null) {\n            temp.prev = null;\n            this.front.next = null;\n        }\n        this.front = temp; // 更新头节点\n        this.queSize--;\n        return value;\n    }\n\n    /* 访问队尾元素 */\n    peekLast(): number {\n        return this.queSize === 0 ? null : this.rear.val;\n    }\n\n    /* 访问队首元素 */\n    peekFirst(): number {\n        return this.queSize === 0 ? null : this.front.val;\n    }\n\n    /* 获取双向队列的长度 */\n    size(): number {\n        return this.queSize;\n    }\n\n    /* 判断双向队列是否为空 */\n    isEmpty(): boolean {\n        return this.queSize === 0;\n    }\n\n    /* 打印双向队列 */\n    print(): void {\n        const arr: number[] = [];\n        let temp: ListNode = this.front;\n        while (temp !== null) {\n            arr.push(temp.val);\n            temp = temp.next;\n        }\n        console.log('[' + arr.join(', ') + ']');\n    }\n}\n
linkedlist_deque.dart
/* 双向链表节点 */\nclass ListNode {\n  int val; // 节点值\n  ListNode? next; // 后继节点引用\n  ListNode? prev; // 前驱节点引用\n\n  ListNode(this.val, {this.next, this.prev});\n}\n\n/* 基于双向链表实现的双向对列 */\nclass LinkedListDeque {\n  late ListNode? _front; // 头节点 _front\n  late ListNode? _rear; // 尾节点 _rear\n  int _queSize = 0; // 双向队列的长度\n\n  LinkedListDeque() {\n    this._front = null;\n    this._rear = null;\n  }\n\n  /* 获取双向队列长度 */\n  int size() {\n    return this._queSize;\n  }\n\n  /* 判断双向队列是否为空 */\n  bool isEmpty() {\n    return size() == 0;\n  }\n\n  /* 入队操作 */\n  void push(int _num, bool isFront) {\n    final ListNode node = ListNode(_num);\n    if (isEmpty()) {\n      // 若链表为空,则令 _front 和 _rear 都指向 node\n      _front = _rear = node;\n    } else if (isFront) {\n      // 队首入队操作\n      // 将 node 添加至链表头部\n      _front!.prev = node;\n      node.next = _front;\n      _front = node; // 更新头节点\n    } else {\n      // 队尾入队操作\n      // 将 node 添加至链表尾部\n      _rear!.next = node;\n      node.prev = _rear;\n      _rear = node; // 更新尾节点\n    }\n    _queSize++; // 更新队列长度\n  }\n\n  /* 队首入队 */\n  void pushFirst(int _num) {\n    push(_num, true);\n  }\n\n  /* 队尾入队 */\n  void pushLast(int _num) {\n    push(_num, false);\n  }\n\n  /* 出队操作 */\n  int? pop(bool isFront) {\n    // 若队列为空,直接返回 null\n    if (isEmpty()) {\n      return null;\n    }\n    final int val;\n    if (isFront) {\n      // 队首出队操作\n      val = _front!.val; // 暂存头节点值\n      // 删除头节点\n      ListNode? fNext = _front!.next;\n      if (fNext != null) {\n        fNext.prev = null;\n        _front!.next = null;\n      }\n      _front = fNext; // 更新头节点\n    } else {\n      // 队尾出队操作\n      val = _rear!.val; // 暂存尾节点值\n      // 删除尾节点\n      ListNode? rPrev = _rear!.prev;\n      if (rPrev != null) {\n        rPrev.next = null;\n        _rear!.prev = null;\n      }\n      _rear = rPrev; // 更新尾节点\n    }\n    _queSize--; // 更新队列长度\n    return val;\n  }\n\n  /* 队首出队 */\n  int? popFirst() {\n    return pop(true);\n  }\n\n  /* 队尾出队 */\n  int? popLast() {\n    return pop(false);\n  }\n\n  /* 访问队首元素 */\n  int? peekFirst() {\n    return _front?.val;\n  }\n\n  /* 访问队尾元素 */\n  int? peekLast() {\n    return _rear?.val;\n  }\n\n  /* 返回数组用于打印 */\n  List<int> toArray() {\n    ListNode? node = _front;\n    final List<int> res = [];\n    for (int i = 0; i < _queSize; i++) {\n      res.add(node!.val);\n      node = node.next;\n    }\n    return res;\n  }\n}\n
linkedlist_deque.rs
/* 双向链表节点 */\npub struct ListNode<T> {\n    pub val: T,                                 // 节点值\n    pub next: Option<Rc<RefCell<ListNode<T>>>>, // 后继节点指针\n    pub prev: Option<Rc<RefCell<ListNode<T>>>>, // 前驱节点指针\n}\n\nimpl<T> ListNode<T> {\n    pub fn new(val: T) -> Rc<RefCell<ListNode<T>>> {\n        Rc::new(RefCell::new(ListNode {\n            val,\n            next: None,\n            prev: None,\n        }))\n    }\n}\n\n/* 基于双向链表实现的双向队列 */\n#[allow(dead_code)]\npub struct LinkedListDeque<T> {\n    front: Option<Rc<RefCell<ListNode<T>>>>, // 头节点 front\n    rear: Option<Rc<RefCell<ListNode<T>>>>,  // 尾节点 rear\n    que_size: usize,                         // 双向队列的长度\n}\n\nimpl<T: Copy> LinkedListDeque<T> {\n    pub fn new() -> Self {\n        Self {\n            front: None,\n            rear: None,\n            que_size: 0,\n        }\n    }\n\n    /* 获取双向队列的长度 */\n    pub fn size(&self) -> usize {\n        return self.que_size;\n    }\n\n    /* 判断双向队列是否为空 */\n    pub fn is_empty(&self) -> bool {\n        return self.que_size == 0;\n    }\n\n    /* 入队操作 */\n    fn push(&mut self, num: T, is_front: bool) {\n        let node = ListNode::new(num);\n        // 队首入队操作\n        if is_front {\n            match self.front.take() {\n                // 若链表为空,则令 front 和 rear 都指向 node\n                None => {\n                    self.rear = Some(node.clone());\n                    self.front = Some(node);\n                }\n                // 将 node 添加至链表头部\n                Some(old_front) => {\n                    old_front.borrow_mut().prev = Some(node.clone());\n                    node.borrow_mut().next = Some(old_front);\n                    self.front = Some(node); // 更新头节点\n                }\n            }\n        }\n        // 队尾入队操作\n        else {\n            match self.rear.take() {\n                // 若链表为空,则令 front 和 rear 都指向 node\n                None => {\n                    self.front = Some(node.clone());\n                    self.rear = Some(node);\n                }\n                // 将 node 添加至链表尾部\n                Some(old_rear) => {\n                    old_rear.borrow_mut().next = Some(node.clone());\n                    node.borrow_mut().prev = Some(old_rear);\n                    self.rear = Some(node); // 更新尾节点\n                }\n            }\n        }\n        self.que_size += 1; // 更新队列长度\n    }\n\n    /* 队首入队 */\n    pub fn push_first(&mut self, num: T) {\n        self.push(num, true);\n    }\n\n    /* 队尾入队 */\n    pub fn push_last(&mut self, num: T) {\n        self.push(num, false);\n    }\n\n    /* 出队操作 */\n    fn pop(&mut self, is_front: bool) -> Option<T> {\n        // 若队列为空,直接返回 None\n        if self.is_empty() {\n            return None;\n        };\n        // 队首出队操作\n        if is_front {\n            self.front.take().map(|old_front| {\n                match old_front.borrow_mut().next.take() {\n                    Some(new_front) => {\n                        new_front.borrow_mut().prev.take();\n                        self.front = Some(new_front); // 更新头节点\n                    }\n                    None => {\n                        self.rear.take();\n                    }\n                }\n                self.que_size -= 1; // 更新队列长度\n                old_front.borrow().val\n            })\n        }\n        // 队尾出队操作\n        else {\n            self.rear.take().map(|old_rear| {\n                match old_rear.borrow_mut().prev.take() {\n                    Some(new_rear) => {\n                        new_rear.borrow_mut().next.take();\n                        self.rear = Some(new_rear); // 更新尾节点\n                    }\n                    None => {\n                        self.front.take();\n                    }\n                }\n                self.que_size -= 1; // 更新队列长度\n                old_rear.borrow().val\n            })\n        }\n    }\n\n    /* 队首出队 */\n    pub fn pop_first(&mut self) -> Option<T> {\n        return self.pop(true);\n    }\n\n    /* 队尾出队 */\n    pub fn pop_last(&mut self) -> Option<T> {\n        return self.pop(false);\n    }\n\n    /* 访问队首元素 */\n    pub fn peek_first(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.front.as_ref()\n    }\n\n    /* 访问队尾元素 */\n    pub fn peek_last(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.rear.as_ref()\n    }\n\n    /* 返回数组用于打印 */\n    pub fn to_array(&self, head: Option<&Rc<RefCell<ListNode<T>>>>) -> Vec<T> {\n        let mut res: Vec<T> = Vec::new();\n        fn recur<T: Copy>(cur: Option<&Rc<RefCell<ListNode<T>>>>, res: &mut Vec<T>) {\n            if let Some(cur) = cur {\n                res.push(cur.borrow().val);\n                recur(cur.borrow().next.as_ref(), res);\n            }\n        }\n\n        recur(head, &mut res);\n        res\n    }\n}\n
linkedlist_deque.c
/* 双向链表节点 */\ntypedef struct DoublyListNode {\n    int val;                     // 节点值\n    struct DoublyListNode *next; // 后继节点\n    struct DoublyListNode *prev; // 前驱节点\n} DoublyListNode;\n\n/* 构造函数 */\nDoublyListNode *newDoublyListNode(int num) {\n    DoublyListNode *new = (DoublyListNode *)malloc(sizeof(DoublyListNode));\n    new->val = num;\n    new->next = NULL;\n    new->prev = NULL;\n    return new;\n}\n\n/* 析构函数 */\nvoid delDoublyListNode(DoublyListNode *node) {\n    free(node);\n}\n\n/* 基于双向链表实现的双向队列 */\ntypedef struct {\n    DoublyListNode *front, *rear; // 头节点 front ,尾节点 rear\n    int queSize;                  // 双向队列的长度\n} LinkedListDeque;\n\n/* 构造函数 */\nLinkedListDeque *newLinkedListDeque() {\n    LinkedListDeque *deque = (LinkedListDeque *)malloc(sizeof(LinkedListDeque));\n    deque->front = NULL;\n    deque->rear = NULL;\n    deque->queSize = 0;\n    return deque;\n}\n\n/* 析构函数 */\nvoid delLinkedListdeque(LinkedListDeque *deque) {\n    // 释放所有节点\n    for (int i = 0; i < deque->queSize && deque->front != NULL; i++) {\n        DoublyListNode *tmp = deque->front;\n        deque->front = deque->front->next;\n        free(tmp);\n    }\n    // 释放 deque 结构体\n    free(deque);\n}\n\n/* 获取队列的长度 */\nint size(LinkedListDeque *deque) {\n    return deque->queSize;\n}\n\n/* 判断队列是否为空 */\nbool empty(LinkedListDeque *deque) {\n    return (size(deque) == 0);\n}\n\n/* 入队 */\nvoid push(LinkedListDeque *deque, int num, bool isFront) {\n    DoublyListNode *node = newDoublyListNode(num);\n    // 若链表为空,则令 front 和 rear 都指向node\n    if (empty(deque)) {\n        deque->front = deque->rear = node;\n    }\n    // 队首入队操作\n    else if (isFront) {\n        // 将 node 添加至链表头部\n        deque->front->prev = node;\n        node->next = deque->front;\n        deque->front = node; // 更新头节点\n    }\n    // 队尾入队操作\n    else {\n        // 将 node 添加至链表尾部\n        deque->rear->next = node;\n        node->prev = deque->rear;\n        deque->rear = node;\n    }\n    deque->queSize++; // 更新队列长度\n}\n\n/* 队首入队 */\nvoid pushFirst(LinkedListDeque *deque, int num) {\n    push(deque, num, true);\n}\n\n/* 队尾入队 */\nvoid pushLast(LinkedListDeque *deque, int num) {\n    push(deque, num, false);\n}\n\n/* 访问队首元素 */\nint peekFirst(LinkedListDeque *deque) {\n    assert(size(deque) && deque->front);\n    return deque->front->val;\n}\n\n/* 访问队尾元素 */\nint peekLast(LinkedListDeque *deque) {\n    assert(size(deque) && deque->rear);\n    return deque->rear->val;\n}\n\n/* 出队 */\nint pop(LinkedListDeque *deque, bool isFront) {\n    if (empty(deque))\n        return -1;\n    int val;\n    // 队首出队操作\n    if (isFront) {\n        val = peekFirst(deque); // 暂存头节点值\n        DoublyListNode *fNext = deque->front->next;\n        if (fNext) {\n            fNext->prev = NULL;\n            deque->front->next = NULL;\n        }\n        delDoublyListNode(deque->front);\n        deque->front = fNext; // 更新头节点\n    }\n    // 队尾出队操作\n    else {\n        val = peekLast(deque); // 暂存尾节点值\n        DoublyListNode *rPrev = deque->rear->prev;\n        if (rPrev) {\n            rPrev->next = NULL;\n            deque->rear->prev = NULL;\n        }\n        delDoublyListNode(deque->rear);\n        deque->rear = rPrev; // 更新尾节点\n    }\n    deque->queSize--; // 更新队列长度\n    return val;\n}\n\n/* 队首出队 */\nint popFirst(LinkedListDeque *deque) {\n    return pop(deque, true);\n}\n\n/* 队尾出队 */\nint popLast(LinkedListDeque *deque) {\n    return pop(deque, false);\n}\n\n/* 打印队列 */\nvoid printLinkedListDeque(LinkedListDeque *deque) {\n    int *arr = malloc(sizeof(int) * deque->queSize);\n    // 拷贝链表中的数据到数组\n    int i;\n    DoublyListNode *node;\n    for (i = 0, node = deque->front; i < deque->queSize; i++) {\n        arr[i] = node->val;\n        node = node->next;\n    }\n    printArray(arr, deque->queSize);\n    free(arr);\n}\n
linkedlist_deque.kt
/* 双向链表节点 */\nclass ListNode(var _val: Int) {\n    // 节点值\n    var next: ListNode? = null // 后继节点引用\n    var prev: ListNode? = null // 前驱节点引用\n}\n\n/* 基于双向链表实现的双向队列 */\nclass LinkedListDeque {\n    private var front: ListNode? = null // 头节点 front\n    private var rear: ListNode? = null // 尾节点 rear\n    private var queSize: Int = 0 // 双向队列的长度\n\n    /* 获取双向队列的长度 */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* 判断双向队列是否为空 */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* 入队操作 */\n    fun push(num: Int, isFront: Boolean) {\n        val node = ListNode(num)\n        // 若链表为空,则令 front 和 rear 都指向 node\n        if (isEmpty()) {\n            rear = node\n            front = rear\n            // 队首入队操作\n        } else if (isFront) {\n            // 将 node 添加至链表头部\n            front?.prev = node\n            node.next = front\n            front = node // 更新头节点\n            // 队尾入队操作\n        } else {\n            // 将 node 添加至链表尾部\n            rear?.next = node\n            node.prev = rear\n            rear = node // 更新尾节点\n        }\n        queSize++ // 更新队列长度\n    }\n\n    /* 队首入队 */\n    fun pushFirst(num: Int) {\n        push(num, true)\n    }\n\n    /* 队尾入队 */\n    fun pushLast(num: Int) {\n        push(num, false)\n    }\n\n    /* 出队操作 */\n    fun pop(isFront: Boolean): Int {\n        if (isEmpty()) \n            throw IndexOutOfBoundsException()\n        val _val: Int\n        // 队首出队操作\n        if (isFront) {\n            _val = front!!._val // 暂存头节点值\n            // 删除头节点\n            val fNext = front!!.next\n            if (fNext != null) {\n                fNext.prev = null\n                front!!.next = null\n            }\n            front = fNext // 更新头节点\n            // 队尾出队操作\n        } else {\n            _val = rear!!._val // 暂存尾节点值\n            // 删除尾节点\n            val rPrev = rear!!.prev\n            if (rPrev != null) {\n                rPrev.next = null\n                rear!!.prev = null\n            }\n            rear = rPrev // 更新尾节点\n        }\n        queSize-- // 更新队列长度\n        return _val\n    }\n\n    /* 队首出队 */\n    fun popFirst(): Int {\n        return pop(true)\n    }\n\n    /* 队尾出队 */\n    fun popLast(): Int {\n        return pop(false)\n    }\n\n    /* 访问队首元素 */\n    fun peekFirst(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return front!!._val\n    }\n\n    /* 访问队尾元素 */\n    fun peekLast(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return rear!!._val\n    }\n\n    /* 返回数组用于打印 */\n    fun toArray(): IntArray {\n        var node = front\n        val res = IntArray(size())\n        for (i in res.indices) {\n            res[i] = node!!._val\n            node = node.next\n        }\n        return res\n    }\n}\n
linkedlist_deque.rb
=begin\nFile: linkedlist_deque.rb\nCreated Time: 2024-04-06\nAuthor: Xuan Khoa Tu Nguyen (ngxktuzkai2000@gmail.com)\n=end\n\n### 双向链表节点\nclass ListNode\n  attr_accessor :val\n  attr_accessor :next # 后继节点引用\n  attr_accessor :prev # 前躯节点引用\n\n  ### 构造方法 ###\n  def initialize(val)\n    @val = val\n  end\nend\n\n### 基于双向链表实现的双向队列 ###\nclass LinkedListDeque\n  ### 获取双向队列的长度 ###\n  attr_reader :size\n\n  ### 构造方法 ###\n  def initialize\n    @front = nil  # 头节点 front\n    @rear = nil   # 尾节点 rear\n    @size = 0     # 双向队列的长度\n  end\n\n  ### 判断双向队列是否为空 ###\n  def is_empty?\n    size.zero?\n  end\n\n  ### 入队操作 ###\n  def push(num, is_front)\n    node = ListNode.new(num)\n    # 若链表为空, 则令 front 和 rear 都指向 node\n    if is_empty?\n      @front = @rear = node\n    # 队首入队操作\n    elsif is_front\n      # 将 node 添加至链表头部\n      @front.prev = node\n      node.next = @front\n      @front = node # 更新头节点\n    # 队尾入队操作\n    else\n      # 将 node 添加至链表尾部\n      @rear.next = node\n      node.prev = @rear\n      @rear = node # 更新尾节点\n    end\n    @size += 1 # 更新队列长度\n  end\n\n  ### 队首入队 ###\n  def push_first(num)\n    push(num, true)\n  end\n\n  ### 队尾入队 ###\n  def push_last(num)\n    push(num, false)\n  end\n\n  ### 出队操作 ###\n  def pop(is_front)\n    raise IndexError, '双向队列为空' if is_empty?\n\n    # 队首出队操作\n    if is_front\n      val = @front.val # 暂存头节点值\n      # 删除头节点\n      fnext = @front.next\n      unless fnext.nil?\n        fnext.prev = nil\n        @front.next = nil\n      end\n      @front = fnext # 更新头节点\n    # 队尾出队操作\n    else\n      val = @rear.val # 暂存尾节点值\n      # 删除尾节点\n      rprev = @rear.prev\n      unless rprev.nil?\n        rprev.next = nil\n        @rear.prev = nil\n      end\n      @rear = rprev # 更新尾节点\n    end\n    @size -= 1 # 更新队列长度\n\n    val\n  end\n\n  ### 队首出队 ###\n  def pop_first\n    pop(true)\n  end\n\n  ### 队首出队 ###\n  def pop_last\n    pop(false)\n  end\n\n  ### 访问队首元素 ###\n  def peek_first\n    raise IndexError, '双向队列为空' if is_empty?\n\n    @front.val\n  end\n\n  ### 访问队尾元素 ###\n  def peek_last\n    raise IndexError, '双向队列为空' if is_empty?\n\n    @rear.val\n  end\n\n  ### 返回数组用于打印 ###\n  def to_array\n    node = @front\n    res = Array.new(size, 0)\n    for i in 0...size\n      res[i] = node.val\n      node = node.next\n    end\n    res\n  end\nend\n
","path":["第 5 章   栈与队列","5.3   双向队列"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#2","level":3,"title":"2.   基于数组的实现","text":"

如图 5-9 所示,与基于数组实现队列类似,我们也可以使用环形数组来实现双向队列。

<1><2><3><4><5>

图 5-9   基于数组实现双向队列的入队出队操作

在队列的实现基础上,仅需增加“队首入队”和“队尾出队”的方法:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_deque.py
class ArrayDeque:\n    \"\"\"基于环形数组实现的双向队列\"\"\"\n\n    def __init__(self, capacity: int):\n        \"\"\"构造方法\"\"\"\n        self._nums: list[int] = [0] * capacity\n        self._front: int = 0\n        self._size: int = 0\n\n    def capacity(self) -> int:\n        \"\"\"获取双向队列的容量\"\"\"\n        return len(self._nums)\n\n    def size(self) -> int:\n        \"\"\"获取双向队列的长度\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"判断双向队列是否为空\"\"\"\n        return self._size == 0\n\n    def index(self, i: int) -> int:\n        \"\"\"计算环形数组索引\"\"\"\n        # 通过取余操作实现数组首尾相连\n        # 当 i 越过数组尾部后,回到头部\n        # 当 i 越过数组头部后,回到尾部\n        return (i + self.capacity()) % self.capacity()\n\n    def push_first(self, num: int):\n        \"\"\"队首入队\"\"\"\n        if self._size == self.capacity():\n            print(\"双向队列已满\")\n            return\n        # 队首指针向左移动一位\n        # 通过取余操作实现 front 越过数组头部后回到尾部\n        self._front = self.index(self._front - 1)\n        # 将 num 添加至队首\n        self._nums[self._front] = num\n        self._size += 1\n\n    def push_last(self, num: int):\n        \"\"\"队尾入队\"\"\"\n        if self._size == self.capacity():\n            print(\"双向队列已满\")\n            return\n        # 计算队尾指针,指向队尾索引 + 1\n        rear = self.index(self._front + self._size)\n        # 将 num 添加至队尾\n        self._nums[rear] = num\n        self._size += 1\n\n    def pop_first(self) -> int:\n        \"\"\"队首出队\"\"\"\n        num = self.peek_first()\n        # 队首指针向后移动一位\n        self._front = self.index(self._front + 1)\n        self._size -= 1\n        return num\n\n    def pop_last(self) -> int:\n        \"\"\"队尾出队\"\"\"\n        num = self.peek_last()\n        self._size -= 1\n        return num\n\n    def peek_first(self) -> int:\n        \"\"\"访问队首元素\"\"\"\n        if self.is_empty():\n            raise IndexError(\"双向队列为空\")\n        return self._nums[self._front]\n\n    def peek_last(self) -> int:\n        \"\"\"访问队尾元素\"\"\"\n        if self.is_empty():\n            raise IndexError(\"双向队列为空\")\n        # 计算尾元素索引\n        last = self.index(self._front + self._size - 1)\n        return self._nums[last]\n\n    def to_array(self) -> list[int]:\n        \"\"\"返回数组用于打印\"\"\"\n        # 仅转换有效长度范围内的列表元素\n        res = []\n        for i in range(self._size):\n            res.append(self._nums[self.index(self._front + i)])\n        return res\n
array_deque.cpp
/* 基于环形数组实现的双向队列 */\nclass ArrayDeque {\n  private:\n    vector<int> nums; // 用于存储双向队列元素的数组\n    int front;        // 队首指针,指向队首元素\n    int queSize;      // 双向队列长度\n\n  public:\n    /* 构造方法 */\n    ArrayDeque(int capacity) {\n        nums.resize(capacity);\n        front = queSize = 0;\n    }\n\n    /* 获取双向队列的容量 */\n    int capacity() {\n        return nums.size();\n    }\n\n    /* 获取双向队列的长度 */\n    int size() {\n        return queSize;\n    }\n\n    /* 判断双向队列是否为空 */\n    bool isEmpty() {\n        return queSize == 0;\n    }\n\n    /* 计算环形数组索引 */\n    int index(int i) {\n        // 通过取余操作实现数组首尾相连\n        // 当 i 越过数组尾部后,回到头部\n        // 当 i 越过数组头部后,回到尾部\n        return (i + capacity()) % capacity();\n    }\n\n    /* 队首入队 */\n    void pushFirst(int num) {\n        if (queSize == capacity()) {\n            cout << \"双向队列已满\" << endl;\n            return;\n        }\n        // 队首指针向左移动一位\n        // 通过取余操作实现 front 越过数组头部后回到尾部\n        front = index(front - 1);\n        // 将 num 添加至队首\n        nums[front] = num;\n        queSize++;\n    }\n\n    /* 队尾入队 */\n    void pushLast(int num) {\n        if (queSize == capacity()) {\n            cout << \"双向队列已满\" << endl;\n            return;\n        }\n        // 计算队尾指针,指向队尾索引 + 1\n        int rear = index(front + queSize);\n        // 将 num 添加至队尾\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* 队首出队 */\n    int popFirst() {\n        int num = peekFirst();\n        // 队首指针向后移动一位\n        front = index(front + 1);\n        queSize--;\n        return num;\n    }\n\n    /* 队尾出队 */\n    int popLast() {\n        int num = peekLast();\n        queSize--;\n        return num;\n    }\n\n    /* 访问队首元素 */\n    int peekFirst() {\n        if (isEmpty())\n            throw out_of_range(\"双向队列为空\");\n        return nums[front];\n    }\n\n    /* 访问队尾元素 */\n    int peekLast() {\n        if (isEmpty())\n            throw out_of_range(\"双向队列为空\");\n        // 计算尾元素索引\n        int last = index(front + queSize - 1);\n        return nums[last];\n    }\n\n    /* 返回数组用于打印 */\n    vector<int> toVector() {\n        // 仅转换有效长度范围内的列表元素\n        vector<int> res(queSize);\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[index(j)];\n        }\n        return res;\n    }\n};\n
array_deque.java
/* 基于环形数组实现的双向队列 */\nclass ArrayDeque {\n    private int[] nums; // 用于存储双向队列元素的数组\n    private int front; // 队首指针,指向队首元素\n    private int queSize; // 双向队列长度\n\n    /* 构造方法 */\n    public ArrayDeque(int capacity) {\n        this.nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* 获取双向队列的容量 */\n    public int capacity() {\n        return nums.length;\n    }\n\n    /* 获取双向队列的长度 */\n    public int size() {\n        return queSize;\n    }\n\n    /* 判断双向队列是否为空 */\n    public boolean isEmpty() {\n        return queSize == 0;\n    }\n\n    /* 计算环形数组索引 */\n    private int index(int i) {\n        // 通过取余操作实现数组首尾相连\n        // 当 i 越过数组尾部后,回到头部\n        // 当 i 越过数组头部后,回到尾部\n        return (i + capacity()) % capacity();\n    }\n\n    /* 队首入队 */\n    public void pushFirst(int num) {\n        if (queSize == capacity()) {\n            System.out.println(\"双向队列已满\");\n            return;\n        }\n        // 队首指针向左移动一位\n        // 通过取余操作实现 front 越过数组头部后回到尾部\n        front = index(front - 1);\n        // 将 num 添加至队首\n        nums[front] = num;\n        queSize++;\n    }\n\n    /* 队尾入队 */\n    public void pushLast(int num) {\n        if (queSize == capacity()) {\n            System.out.println(\"双向队列已满\");\n            return;\n        }\n        // 计算队尾指针,指向队尾索引 + 1\n        int rear = index(front + queSize);\n        // 将 num 添加至队尾\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* 队首出队 */\n    public int popFirst() {\n        int num = peekFirst();\n        // 队首指针向后移动一位\n        front = index(front + 1);\n        queSize--;\n        return num;\n    }\n\n    /* 队尾出队 */\n    public int popLast() {\n        int num = peekLast();\n        queSize--;\n        return num;\n    }\n\n    /* 访问队首元素 */\n    public int peekFirst() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return nums[front];\n    }\n\n    /* 访问队尾元素 */\n    public int peekLast() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        // 计算尾元素索引\n        int last = index(front + queSize - 1);\n        return nums[last];\n    }\n\n    /* 返回数组用于打印 */\n    public int[] toArray() {\n        // 仅转换有效长度范围内的列表元素\n        int[] res = new int[queSize];\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[index(j)];\n        }\n        return res;\n    }\n}\n
array_deque.cs
/* 基于环形数组实现的双向队列 */\nclass ArrayDeque {\n    int[] nums;  // 用于存储双向队列元素的数组\n    int front;   // 队首指针,指向队首元素\n    int queSize; // 双向队列长度\n\n    /* 构造方法 */\n    public ArrayDeque(int capacity) {\n        nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* 获取双向队列的容量 */\n    int Capacity() {\n        return nums.Length;\n    }\n\n    /* 获取双向队列的长度 */\n    public int Size() {\n        return queSize;\n    }\n\n    /* 判断双向队列是否为空 */\n    public bool IsEmpty() {\n        return queSize == 0;\n    }\n\n    /* 计算环形数组索引 */\n    int Index(int i) {\n        // 通过取余操作实现数组首尾相连\n        // 当 i 越过数组尾部后,回到头部\n        // 当 i 越过数组头部后,回到尾部\n        return (i + Capacity()) % Capacity();\n    }\n\n    /* 队首入队 */\n    public void PushFirst(int num) {\n        if (queSize == Capacity()) {\n            Console.WriteLine(\"双向队列已满\");\n            return;\n        }\n        // 队首指针向左移动一位\n        // 通过取余操作实现 front 越过数组头部后回到尾部\n        front = Index(front - 1);\n        // 将 num 添加至队首\n        nums[front] = num;\n        queSize++;\n    }\n\n    /* 队尾入队 */\n    public void PushLast(int num) {\n        if (queSize == Capacity()) {\n            Console.WriteLine(\"双向队列已满\");\n            return;\n        }\n        // 计算队尾指针,指向队尾索引 + 1\n        int rear = Index(front + queSize);\n        // 将 num 添加至队尾\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* 队首出队 */\n    public int PopFirst() {\n        int num = PeekFirst();\n        // 队首指针向后移动一位\n        front = Index(front + 1);\n        queSize--;\n        return num;\n    }\n\n    /* 队尾出队 */\n    public int PopLast() {\n        int num = PeekLast();\n        queSize--;\n        return num;\n    }\n\n    /* 访问队首元素 */\n    public int PeekFirst() {\n        if (IsEmpty()) {\n            throw new InvalidOperationException();\n        }\n        return nums[front];\n    }\n\n    /* 访问队尾元素 */\n    public int PeekLast() {\n        if (IsEmpty()) {\n            throw new InvalidOperationException();\n        }\n        // 计算尾元素索引\n        int last = Index(front + queSize - 1);\n        return nums[last];\n    }\n\n    /* 返回数组用于打印 */\n    public int[] ToArray() {\n        // 仅转换有效长度范围内的列表元素\n        int[] res = new int[queSize];\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[Index(j)];\n        }\n        return res;\n    }\n}\n
array_deque.go
/* 基于环形数组实现的双向队列 */\ntype arrayDeque struct {\n    nums        []int // 用于存储双向队列元素的数组\n    front       int   // 队首指针,指向队首元素\n    queSize     int   // 双向队列长度\n    queCapacity int   // 队列容量(即最大容纳元素数量)\n}\n\n/* 初始化队列 */\nfunc newArrayDeque(queCapacity int) *arrayDeque {\n    return &arrayDeque{\n        nums:        make([]int, queCapacity),\n        queCapacity: queCapacity,\n        front:       0,\n        queSize:     0,\n    }\n}\n\n/* 获取双向队列的长度 */\nfunc (q *arrayDeque) size() int {\n    return q.queSize\n}\n\n/* 判断双向队列是否为空 */\nfunc (q *arrayDeque) isEmpty() bool {\n    return q.queSize == 0\n}\n\n/* 计算环形数组索引 */\nfunc (q *arrayDeque) index(i int) int {\n    // 通过取余操作实现数组首尾相连\n    // 当 i 越过数组尾部后,回到头部\n    // 当 i 越过数组头部后,回到尾部\n    return (i + q.queCapacity) % q.queCapacity\n}\n\n/* 队首入队 */\nfunc (q *arrayDeque) pushFirst(num int) {\n    if q.queSize == q.queCapacity {\n        fmt.Println(\"双向队列已满\")\n        return\n    }\n    // 队首指针向左移动一位\n    // 通过取余操作实现 front 越过数组头部后回到尾部\n    q.front = q.index(q.front - 1)\n    // 将 num 添加至队首\n    q.nums[q.front] = num\n    q.queSize++\n}\n\n/* 队尾入队 */\nfunc (q *arrayDeque) pushLast(num int) {\n    if q.queSize == q.queCapacity {\n        fmt.Println(\"双向队列已满\")\n        return\n    }\n    // 计算队尾指针,指向队尾索引 + 1\n    rear := q.index(q.front + q.queSize)\n    // 将 num 添加至队尾\n    q.nums[rear] = num\n    q.queSize++\n}\n\n/* 队首出队 */\nfunc (q *arrayDeque) popFirst() any {\n    num := q.peekFirst()\n    if num == nil {\n        return nil\n    }\n    // 队首指针向后移动一位\n    q.front = q.index(q.front + 1)\n    q.queSize--\n    return num\n}\n\n/* 队尾出队 */\nfunc (q *arrayDeque) popLast() any {\n    num := q.peekLast()\n    if num == nil {\n        return nil\n    }\n    q.queSize--\n    return num\n}\n\n/* 访问队首元素 */\nfunc (q *arrayDeque) peekFirst() any {\n    if q.isEmpty() {\n        return nil\n    }\n    return q.nums[q.front]\n}\n\n/* 访问队尾元素 */\nfunc (q *arrayDeque) peekLast() any {\n    if q.isEmpty() {\n        return nil\n    }\n    // 计算尾元素索引\n    last := q.index(q.front + q.queSize - 1)\n    return q.nums[last]\n}\n\n/* 获取 Slice 用于打印 */\nfunc (q *arrayDeque) toSlice() []int {\n    // 仅转换有效长度范围内的列表元素\n    res := make([]int, q.queSize)\n    for i, j := 0, q.front; i < q.queSize; i++ {\n        res[i] = q.nums[q.index(j)]\n        j++\n    }\n    return res\n}\n
array_deque.swift
/* 基于环形数组实现的双向队列 */\nclass ArrayDeque {\n    private var nums: [Int] // 用于存储双向队列元素的数组\n    private var front: Int // 队首指针,指向队首元素\n    private var _size: Int // 双向队列长度\n\n    /* 构造方法 */\n    init(capacity: Int) {\n        nums = Array(repeating: 0, count: capacity)\n        front = 0\n        _size = 0\n    }\n\n    /* 获取双向队列的容量 */\n    func capacity() -> Int {\n        nums.count\n    }\n\n    /* 获取双向队列的长度 */\n    func size() -> Int {\n        _size\n    }\n\n    /* 判断双向队列是否为空 */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* 计算环形数组索引 */\n    private func index(i: Int) -> Int {\n        // 通过取余操作实现数组首尾相连\n        // 当 i 越过数组尾部后,回到头部\n        // 当 i 越过数组头部后,回到尾部\n        (i + capacity()) % capacity()\n    }\n\n    /* 队首入队 */\n    func pushFirst(num: Int) {\n        if size() == capacity() {\n            print(\"双向队列已满\")\n            return\n        }\n        // 队首指针向左移动一位\n        // 通过取余操作实现 front 越过数组头部后回到尾部\n        front = index(i: front - 1)\n        // 将 num 添加至队首\n        nums[front] = num\n        _size += 1\n    }\n\n    /* 队尾入队 */\n    func pushLast(num: Int) {\n        if size() == capacity() {\n            print(\"双向队列已满\")\n            return\n        }\n        // 计算队尾指针,指向队尾索引 + 1\n        let rear = index(i: front + size())\n        // 将 num 添加至队尾\n        nums[rear] = num\n        _size += 1\n    }\n\n    /* 队首出队 */\n    func popFirst() -> Int {\n        let num = peekFirst()\n        // 队首指针向后移动一位\n        front = index(i: front + 1)\n        _size -= 1\n        return num\n    }\n\n    /* 队尾出队 */\n    func popLast() -> Int {\n        let num = peekLast()\n        _size -= 1\n        return num\n    }\n\n    /* 访问队首元素 */\n    func peekFirst() -> Int {\n        if isEmpty() {\n            fatalError(\"双向队列为空\")\n        }\n        return nums[front]\n    }\n\n    /* 访问队尾元素 */\n    func peekLast() -> Int {\n        if isEmpty() {\n            fatalError(\"双向队列为空\")\n        }\n        // 计算尾元素索引\n        let last = index(i: front + size() - 1)\n        return nums[last]\n    }\n\n    /* 返回数组用于打印 */\n    func toArray() -> [Int] {\n        // 仅转换有效长度范围内的列表元素\n        (front ..< front + size()).map { nums[index(i: $0)] }\n    }\n}\n
array_deque.js
/* 基于环形数组实现的双向队列 */\nclass ArrayDeque {\n    #nums; // 用于存储双向队列元素的数组\n    #front; // 队首指针,指向队首元素\n    #queSize; // 双向队列长度\n\n    /* 构造方法 */\n    constructor(capacity) {\n        this.#nums = new Array(capacity);\n        this.#front = 0;\n        this.#queSize = 0;\n    }\n\n    /* 获取双向队列的容量 */\n    capacity() {\n        return this.#nums.length;\n    }\n\n    /* 获取双向队列的长度 */\n    size() {\n        return this.#queSize;\n    }\n\n    /* 判断双向队列是否为空 */\n    isEmpty() {\n        return this.#queSize === 0;\n    }\n\n    /* 计算环形数组索引 */\n    index(i) {\n        // 通过取余操作实现数组首尾相连\n        // 当 i 越过数组尾部后,回到头部\n        // 当 i 越过数组头部后,回到尾部\n        return (i + this.capacity()) % this.capacity();\n    }\n\n    /* 队首入队 */\n    pushFirst(num) {\n        if (this.#queSize === this.capacity()) {\n            console.log('双向队列已满');\n            return;\n        }\n        // 队首指针向左移动一位\n        // 通过取余操作实现 front 越过数组头部后回到尾部\n        this.#front = this.index(this.#front - 1);\n        // 将 num 添加至队首\n        this.#nums[this.#front] = num;\n        this.#queSize++;\n    }\n\n    /* 队尾入队 */\n    pushLast(num) {\n        if (this.#queSize === this.capacity()) {\n            console.log('双向队列已满');\n            return;\n        }\n        // 计算队尾指针,指向队尾索引 + 1\n        const rear = this.index(this.#front + this.#queSize);\n        // 将 num 添加至队尾\n        this.#nums[rear] = num;\n        this.#queSize++;\n    }\n\n    /* 队首出队 */\n    popFirst() {\n        const num = this.peekFirst();\n        // 队首指针向后移动一位\n        this.#front = this.index(this.#front + 1);\n        this.#queSize--;\n        return num;\n    }\n\n    /* 队尾出队 */\n    popLast() {\n        const num = this.peekLast();\n        this.#queSize--;\n        return num;\n    }\n\n    /* 访问队首元素 */\n    peekFirst() {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        return this.#nums[this.#front];\n    }\n\n    /* 访问队尾元素 */\n    peekLast() {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        // 计算尾元素索引\n        const last = this.index(this.#front + this.#queSize - 1);\n        return this.#nums[last];\n    }\n\n    /* 返回数组用于打印 */\n    toArray() {\n        // 仅转换有效长度范围内的列表元素\n        const res = [];\n        for (let i = 0, j = this.#front; i < this.#queSize; i++, j++) {\n            res[i] = this.#nums[this.index(j)];\n        }\n        return res;\n    }\n}\n
array_deque.ts
/* 基于环形数组实现的双向队列 */\nclass ArrayDeque {\n    private nums: number[]; // 用于存储双向队列元素的数组\n    private front: number; // 队首指针,指向队首元素\n    private queSize: number; // 双向队列长度\n\n    /* 构造方法 */\n    constructor(capacity: number) {\n        this.nums = new Array(capacity);\n        this.front = 0;\n        this.queSize = 0;\n    }\n\n    /* 获取双向队列的容量 */\n    capacity(): number {\n        return this.nums.length;\n    }\n\n    /* 获取双向队列的长度 */\n    size(): number {\n        return this.queSize;\n    }\n\n    /* 判断双向队列是否为空 */\n    isEmpty(): boolean {\n        return this.queSize === 0;\n    }\n\n    /* 计算环形数组索引 */\n    index(i: number): number {\n        // 通过取余操作实现数组首尾相连\n        // 当 i 越过数组尾部后,回到头部\n        // 当 i 越过数组头部后,回到尾部\n        return (i + this.capacity()) % this.capacity();\n    }\n\n    /* 队首入队 */\n    pushFirst(num: number): void {\n        if (this.queSize === this.capacity()) {\n            console.log('双向队列已满');\n            return;\n        }\n        // 队首指针向左移动一位\n        // 通过取余操作实现 front 越过数组头部后回到尾部\n        this.front = this.index(this.front - 1);\n        // 将 num 添加至队首\n        this.nums[this.front] = num;\n        this.queSize++;\n    }\n\n    /* 队尾入队 */\n    pushLast(num: number): void {\n        if (this.queSize === this.capacity()) {\n            console.log('双向队列已满');\n            return;\n        }\n        // 计算队尾指针,指向队尾索引 + 1\n        const rear: number = this.index(this.front + this.queSize);\n        // 将 num 添加至队尾\n        this.nums[rear] = num;\n        this.queSize++;\n    }\n\n    /* 队首出队 */\n    popFirst(): number {\n        const num: number = this.peekFirst();\n        // 队首指针向后移动一位\n        this.front = this.index(this.front + 1);\n        this.queSize--;\n        return num;\n    }\n\n    /* 队尾出队 */\n    popLast(): number {\n        const num: number = this.peekLast();\n        this.queSize--;\n        return num;\n    }\n\n    /* 访问队首元素 */\n    peekFirst(): number {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        return this.nums[this.front];\n    }\n\n    /* 访问队尾元素 */\n    peekLast(): number {\n        if (this.isEmpty()) throw new Error('The Deque Is Empty.');\n        // 计算尾元素索引\n        const last = this.index(this.front + this.queSize - 1);\n        return this.nums[last];\n    }\n\n    /* 返回数组用于打印 */\n    toArray(): number[] {\n        // 仅转换有效长度范围内的列表元素\n        const res: number[] = [];\n        for (let i = 0, j = this.front; i < this.queSize; i++, j++) {\n            res[i] = this.nums[this.index(j)];\n        }\n        return res;\n    }\n}\n
array_deque.dart
/* 基于环形数组实现的双向队列 */\nclass ArrayDeque {\n  late List<int> _nums; // 用于存储双向队列元素的数组\n  late int _front; // 队首指针,指向队首元素\n  late int _queSize; // 双向队列长度\n\n  /* 构造方法 */\n  ArrayDeque(int capacity) {\n    this._nums = List.filled(capacity, 0);\n    this._front = this._queSize = 0;\n  }\n\n  /* 获取双向队列的容量 */\n  int capacity() {\n    return _nums.length;\n  }\n\n  /* 获取双向队列的长度 */\n  int size() {\n    return _queSize;\n  }\n\n  /* 判断双向队列是否为空 */\n  bool isEmpty() {\n    return _queSize == 0;\n  }\n\n  /* 计算环形数组索引 */\n  int index(int i) {\n    // 通过取余操作实现数组首尾相连\n    // 当 i 越过数组尾部后,回到头部\n    // 当 i 越过数组头部后,回到尾部\n    return (i + capacity()) % capacity();\n  }\n\n  /* 队首入队 */\n  void pushFirst(int _num) {\n    if (_queSize == capacity()) {\n      throw Exception(\"双向队列已满\");\n    }\n    // 队首指针向左移动一位\n    // 通过取余操作实现 _front 越过数组头部后回到尾部\n    _front = index(_front - 1);\n    // 将 _num 添加至队首\n    _nums[_front] = _num;\n    _queSize++;\n  }\n\n  /* 队尾入队 */\n  void pushLast(int _num) {\n    if (_queSize == capacity()) {\n      throw Exception(\"双向队列已满\");\n    }\n    // 计算队尾指针,指向队尾索引 + 1\n    int rear = index(_front + _queSize);\n    // 将 _num 添加至队尾\n    _nums[rear] = _num;\n    _queSize++;\n  }\n\n  /* 队首出队 */\n  int popFirst() {\n    int _num = peekFirst();\n    // 队首指针向右移动一位\n    _front = index(_front + 1);\n    _queSize--;\n    return _num;\n  }\n\n  /* 队尾出队 */\n  int popLast() {\n    int _num = peekLast();\n    _queSize--;\n    return _num;\n  }\n\n  /* 访问队首元素 */\n  int peekFirst() {\n    if (isEmpty()) {\n      throw Exception(\"双向队列为空\");\n    }\n    return _nums[_front];\n  }\n\n  /* 访问队尾元素 */\n  int peekLast() {\n    if (isEmpty()) {\n      throw Exception(\"双向队列为空\");\n    }\n    // 计算尾元素索引\n    int last = index(_front + _queSize - 1);\n    return _nums[last];\n  }\n\n  /* 返回数组用于打印 */\n  List<int> toArray() {\n    // 仅转换有效长度范围内的列表元素\n    List<int> res = List.filled(_queSize, 0);\n    for (int i = 0, j = _front; i < _queSize; i++, j++) {\n      res[i] = _nums[index(j)];\n    }\n    return res;\n  }\n}\n
array_deque.rs
/* 基于环形数组实现的双向队列 */\nstruct ArrayDeque<T> {\n    nums: Vec<T>,    // 用于存储双向队列元素的数组\n    front: usize,    // 队首指针,指向队首元素\n    que_size: usize, // 双向队列长度\n}\n\nimpl<T: Copy + Default> ArrayDeque<T> {\n    /* 构造方法 */\n    pub fn new(capacity: usize) -> Self {\n        Self {\n            nums: vec![T::default(); capacity],\n            front: 0,\n            que_size: 0,\n        }\n    }\n\n    /* 获取双向队列的容量 */\n    pub fn capacity(&self) -> usize {\n        self.nums.len()\n    }\n\n    /* 获取双向队列的长度 */\n    pub fn size(&self) -> usize {\n        self.que_size\n    }\n\n    /* 判断双向队列是否为空 */\n    pub fn is_empty(&self) -> bool {\n        self.que_size == 0\n    }\n\n    /* 计算环形数组索引 */\n    fn index(&self, i: i32) -> usize {\n        // 通过取余操作实现数组首尾相连\n        // 当 i 越过数组尾部后,回到头部\n        // 当 i 越过数组头部后,回到尾部\n        ((i + self.capacity() as i32) % self.capacity() as i32) as usize\n    }\n\n    /* 队首入队 */\n    pub fn push_first(&mut self, num: T) {\n        if self.que_size == self.capacity() {\n            println!(\"双向队列已满\");\n            return;\n        }\n        // 队首指针向左移动一位\n        // 通过取余操作实现 front 越过数组头部后回到尾部\n        self.front = self.index(self.front as i32 - 1);\n        // 将 num 添加至队首\n        self.nums[self.front] = num;\n        self.que_size += 1;\n    }\n\n    /* 队尾入队 */\n    pub fn push_last(&mut self, num: T) {\n        if self.que_size == self.capacity() {\n            println!(\"双向队列已满\");\n            return;\n        }\n        // 计算队尾指针,指向队尾索引 + 1\n        let rear = self.index(self.front as i32 + self.que_size as i32);\n        // 将 num 添加至队尾\n        self.nums[rear] = num;\n        self.que_size += 1;\n    }\n\n    /* 队首出队 */\n    fn pop_first(&mut self) -> T {\n        let num = self.peek_first();\n        // 队首指针向后移动一位\n        self.front = self.index(self.front as i32 + 1);\n        self.que_size -= 1;\n        num\n    }\n\n    /* 队尾出队 */\n    fn pop_last(&mut self) -> T {\n        let num = self.peek_last();\n        self.que_size -= 1;\n        num\n    }\n\n    /* 访问队首元素 */\n    fn peek_first(&self) -> T {\n        if self.is_empty() {\n            panic!(\"双向队列为空\")\n        };\n        self.nums[self.front]\n    }\n\n    /* 访问队尾元素 */\n    fn peek_last(&self) -> T {\n        if self.is_empty() {\n            panic!(\"双向队列为空\")\n        };\n        // 计算尾元素索引\n        let last = self.index(self.front as i32 + self.que_size as i32 - 1);\n        self.nums[last]\n    }\n\n    /* 返回数组用于打印 */\n    fn to_array(&self) -> Vec<T> {\n        // 仅转换有效长度范围内的列表元素\n        let mut res = vec![T::default(); self.que_size];\n        let mut j = self.front;\n        for i in 0..self.que_size {\n            res[i] = self.nums[self.index(j as i32)];\n            j += 1;\n        }\n        res\n    }\n}\n
array_deque.c
/* 基于环形数组实现的双向队列 */\ntypedef struct {\n    int *nums;       // 用于存储队列元素的数组\n    int front;       // 队首指针,指向队首元素\n    int queSize;     // 尾指针,指向队尾 + 1\n    int queCapacity; // 队列容量\n} ArrayDeque;\n\n/* 构造函数 */\nArrayDeque *newArrayDeque(int capacity) {\n    ArrayDeque *deque = (ArrayDeque *)malloc(sizeof(ArrayDeque));\n    // 初始化数组\n    deque->queCapacity = capacity;\n    deque->nums = (int *)malloc(sizeof(int) * deque->queCapacity);\n    deque->front = deque->queSize = 0;\n    return deque;\n}\n\n/* 析构函数 */\nvoid delArrayDeque(ArrayDeque *deque) {\n    free(deque->nums);\n    free(deque);\n}\n\n/* 获取双向队列的容量 */\nint capacity(ArrayDeque *deque) {\n    return deque->queCapacity;\n}\n\n/* 获取双向队列的长度 */\nint size(ArrayDeque *deque) {\n    return deque->queSize;\n}\n\n/* 判断双向队列是否为空 */\nbool empty(ArrayDeque *deque) {\n    return deque->queSize == 0;\n}\n\n/* 计算环形数组索引 */\nint dequeIndex(ArrayDeque *deque, int i) {\n    // 通过取余操作实现数组首尾相连\n    // 当 i 越过数组尾部时,回到头部\n    // 当 i 越过数组头部后,回到尾部\n    return ((i + capacity(deque)) % capacity(deque));\n}\n\n/* 队首入队 */\nvoid pushFirst(ArrayDeque *deque, int num) {\n    if (deque->queSize == capacity(deque)) {\n        printf(\"双向队列已满\\r\\n\");\n        return;\n    }\n    // 队首指针向左移动一位\n    // 通过取余操作实现 front 越过数组头部回到尾部\n    deque->front = dequeIndex(deque, deque->front - 1);\n    // 将 num 添加到队首\n    deque->nums[deque->front] = num;\n    deque->queSize++;\n}\n\n/* 队尾入队 */\nvoid pushLast(ArrayDeque *deque, int num) {\n    if (deque->queSize == capacity(deque)) {\n        printf(\"双向队列已满\\r\\n\");\n        return;\n    }\n    // 计算队尾指针,指向队尾索引 + 1\n    int rear = dequeIndex(deque, deque->front + deque->queSize);\n    // 将 num 添加至队尾\n    deque->nums[rear] = num;\n    deque->queSize++;\n}\n\n/* 访问队首元素 */\nint peekFirst(ArrayDeque *deque) {\n    // 访问异常:双向队列为空\n    assert(empty(deque) == 0);\n    return deque->nums[deque->front];\n}\n\n/* 访问队尾元素 */\nint peekLast(ArrayDeque *deque) {\n    // 访问异常:双向队列为空\n    assert(empty(deque) == 0);\n    int last = dequeIndex(deque, deque->front + deque->queSize - 1);\n    return deque->nums[last];\n}\n\n/* 队首出队 */\nint popFirst(ArrayDeque *deque) {\n    int num = peekFirst(deque);\n    // 队首指针向后移动一位\n    deque->front = dequeIndex(deque, deque->front + 1);\n    deque->queSize--;\n    return num;\n}\n\n/* 队尾出队 */\nint popLast(ArrayDeque *deque) {\n    int num = peekLast(deque);\n    deque->queSize--;\n    return num;\n}\n\n/* 返回数组用于打印 */\nint *toArray(ArrayDeque *deque, int *queSize) {\n    *queSize = deque->queSize;\n    int *res = (int *)calloc(deque->queSize, sizeof(int));\n    int j = deque->front;\n    for (int i = 0; i < deque->queSize; i++) {\n        res[i] = deque->nums[j % deque->queCapacity];\n        j++;\n    }\n    return res;\n}\n
array_deque.kt
/* 构造方法 */\nclass ArrayDeque(capacity: Int) {\n    private var nums: IntArray = IntArray(capacity) // 用于存储双向队列元素的数组\n    private var front: Int = 0 // 队首指针,指向队首元素\n    private var queSize: Int = 0 // 双向队列长度\n\n    /* 获取双向队列的容量 */\n    fun capacity(): Int {\n        return nums.size\n    }\n\n    /* 获取双向队列的长度 */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* 判断双向队列是否为空 */\n    fun isEmpty(): Boolean {\n        return queSize == 0\n    }\n\n    /* 计算环形数组索引 */\n    private fun index(i: Int): Int {\n        // 通过取余操作实现数组首尾相连\n        // 当 i 越过数组尾部后,回到头部\n        // 当 i 越过数组头部后,回到尾部\n        return (i + capacity()) % capacity()\n    }\n\n    /* 队首入队 */\n    fun pushFirst(num: Int) {\n        if (queSize == capacity()) {\n            println(\"双向队列已满\")\n            return\n        }\n        // 队首指针向左移动一位\n        // 通过取余操作实现 front 越过数组头部后回到尾部\n        front = index(front - 1)\n        // 将 num 添加至队首\n        nums[front] = num\n        queSize++\n    }\n\n    /* 队尾入队 */\n    fun pushLast(num: Int) {\n        if (queSize == capacity()) {\n            println(\"双向队列已满\")\n            return\n        }\n        // 计算队尾指针,指向队尾索引 + 1\n        val rear = index(front + queSize)\n        // 将 num 添加至队尾\n        nums[rear] = num\n        queSize++\n    }\n\n    /* 队首出队 */\n    fun popFirst(): Int {\n        val num = peekFirst()\n        // 队首指针向后移动一位\n        front = index(front + 1)\n        queSize--\n        return num\n    }\n\n    /* 队尾出队 */\n    fun popLast(): Int {\n        val num = peekLast()\n        queSize--\n        return num\n    }\n\n    /* 访问队首元素 */\n    fun peekFirst(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return nums[front]\n    }\n\n    /* 访问队尾元素 */\n    fun peekLast(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        // 计算尾元素索引\n        val last = index(front + queSize - 1)\n        return nums[last]\n    }\n\n    /* 返回数组用于打印 */\n    fun toArray(): IntArray {\n        // 仅转换有效长度范围内的列表元素\n        val res = IntArray(queSize)\n        var i = 0\n        var j = front\n        while (i < queSize) {\n            res[i] = nums[index(j)]\n            i++\n            j++\n        }\n        return res\n    }\n}\n
array_deque.rb
### 基于环形数组实现的双向队列 ###\nclass ArrayDeque\n  ### 获取双向队列的长度 ###\n  attr_reader :size\n\n  ### 构造方法 ###\n  def initialize(capacity)\n    @nums = Array.new(capacity, 0)\n    @front = 0\n    @size = 0\n  end\n\n  ### 获取双向队列的容量 ###\n  def capacity\n    @nums.length\n  end\n\n  ### 判断双向队列是否为空 ###\n  def is_empty?\n    size.zero?\n  end\n\n  ### 队首入队 ###\n  def push_first(num)\n    if size == capacity\n      puts '双向队列已满'\n      return\n    end\n\n    # 队首指针向左移动一位\n    # 通过取余操作实现 front 越过数组头部后回到尾部\n    @front = index(@front - 1)\n    # 将 num 添加至队首\n    @nums[@front] = num\n    @size += 1\n  end\n\n  ### 队尾入队 ###\n  def push_last(num)\n    if size == capacity\n      puts '双向队列已满'\n      return\n    end\n\n    # 计算队尾指针,指向队尾索引 + 1\n    rear = index(@front + size)\n    # 将 num 添加至队尾\n    @nums[rear] = num\n    @size += 1\n  end\n\n  ### 队首出队 ###\n  def pop_first\n    num = peek_first\n    # 队首指针向后移动一位\n    @front = index(@front + 1)\n    @size -= 1\n    num\n  end\n\n  ### 队尾出队 ###\n  def pop_last\n    num = peek_last\n    @size -= 1\n    num\n  end\n\n  ### 访问队首元素 ###\n  def peek_first\n    raise IndexError, '双向队列为空' if is_empty?\n\n    @nums[@front]\n  end\n\n  ### 访问队尾元素 ###\n  def peek_last\n    raise IndexError, '双向队列为空' if is_empty?\n\n    # 计算尾元素索引\n    last = index(@front + size - 1)\n    @nums[last]\n  end\n\n  ### 返回数组用于打印 ###\n  def to_array\n    # 仅转换有效长度范围内的列表元素\n    res = []\n    for i in 0...size\n      res << @nums[index(@front + i)]\n    end\n    res\n  end\n\n  private\n\n  ### 计算环形数组索引 ###\n  def index(i)\n    # 通过取余操作实现数组首尾相连\n    # 当 i 越过数组尾部后,回到头部\n    # 当 i 越过数组头部后,回到尾部\n    (i + capacity) % capacity\n  end\nend\n
","path":["第 5 章   栈与队列","5.3   双向队列"],"tags":[]},{"location":"chapter_stack_and_queue/deque/#533","level":2,"title":"5.3.3   双向队列应用","text":"

双向队列兼具栈与队列的逻辑,因此它可以实现这两者的所有应用场景,同时提供更高的自由度。

我们知道,软件的“撤销”功能通常使用栈来实现:系统将每次更改操作 push 到栈中,然后通过 pop 实现撤销。然而,考虑到系统资源的限制,软件通常会限制撤销的步数(例如仅允许保存 \\(50\\) 步)。当栈的长度超过 \\(50\\) 时,软件需要在栈底(队首)执行删除操作。但栈无法实现该功能,此时就需要使用双向队列来替代栈。请注意,“撤销”的核心逻辑仍然遵循栈的先入后出原则,只是双向队列能够更加灵活地实现一些额外逻辑。

","path":["第 5 章   栈与队列","5.3   双向队列"],"tags":[]},{"location":"chapter_stack_and_queue/queue/","level":1,"title":"5.2   队列","text":"

队列(queue)是一种遵循先入先出规则的线性数据结构。顾名思义,队列模拟了排队现象,即新来的人不断加入队列尾部,而位于队列头部的人逐个离开。

如图 5-4 所示,我们将队列头部称为“队首”,尾部称为“队尾”,将把元素加入队尾的操作称为“入队”,删除队首元素的操作称为“出队”。

图 5-4   队列的先入先出规则

","path":["第 5 章   栈与队列","5.2   队列"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#521","level":2,"title":"5.2.1   队列常用操作","text":"

队列的常见操作如表 5-2 所示。需要注意的是,不同编程语言的方法名称可能会有所不同。我们在此采用与栈相同的方法命名。

表 5-2   队列操作效率

方法名 描述 时间复杂度 push() 元素入队,即将元素添加至队尾 \\(O(1)\\) pop() 队首元素出队 \\(O(1)\\) peek() 访问队首元素 \\(O(1)\\)

我们可以直接使用编程语言中现成的队列类:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby queue.py
from collections import deque\n\n# 初始化队列\n# 在 Python 中,我们一般将双向队列类 deque 当作队列使用\n# 虽然 queue.Queue() 是纯正的队列类,但不太好用,因此不推荐\nque: deque[int] = deque()\n\n# 元素入队\nque.append(1)\nque.append(3)\nque.append(2)\nque.append(5)\nque.append(4)\n\n# 访问队首元素\nfront: int = que[0]\n\n# 元素出队\npop: int = que.popleft()\n\n# 获取队列的长度\nsize: int = len(que)\n\n# 判断队列是否为空\nis_empty: bool = len(que) == 0\n
queue.cpp
/* 初始化队列 */\nqueue<int> queue;\n\n/* 元素入队 */\nqueue.push(1);\nqueue.push(3);\nqueue.push(2);\nqueue.push(5);\nqueue.push(4);\n\n/* 访问队首元素 */\nint front = queue.front();\n\n/* 元素出队 */\nqueue.pop();\n\n/* 获取队列的长度 */\nint size = queue.size();\n\n/* 判断队列是否为空 */\nbool empty = queue.empty();\n
queue.java
/* 初始化队列 */\nQueue<Integer> queue = new LinkedList<>();\n\n/* 元素入队 */\nqueue.offer(1);\nqueue.offer(3);\nqueue.offer(2);\nqueue.offer(5);\nqueue.offer(4);\n\n/* 访问队首元素 */\nint peek = queue.peek();\n\n/* 元素出队 */\nint pop = queue.poll();\n\n/* 获取队列的长度 */\nint size = queue.size();\n\n/* 判断队列是否为空 */\nboolean isEmpty = queue.isEmpty();\n
queue.cs
/* 初始化队列 */\nQueue<int> queue = new();\n\n/* 元素入队 */\nqueue.Enqueue(1);\nqueue.Enqueue(3);\nqueue.Enqueue(2);\nqueue.Enqueue(5);\nqueue.Enqueue(4);\n\n/* 访问队首元素 */\nint peek = queue.Peek();\n\n/* 元素出队 */\nint pop = queue.Dequeue();\n\n/* 获取队列的长度 */\nint size = queue.Count;\n\n/* 判断队列是否为空 */\nbool isEmpty = queue.Count == 0;\n
queue_test.go
/* 初始化队列 */\n// 在 Go 中,将 list 作为队列来使用\nqueue := list.New()\n\n/* 元素入队 */\nqueue.PushBack(1)\nqueue.PushBack(3)\nqueue.PushBack(2)\nqueue.PushBack(5)\nqueue.PushBack(4)\n\n/* 访问队首元素 */\npeek := queue.Front()\n\n/* 元素出队 */\npop := queue.Front()\nqueue.Remove(pop)\n\n/* 获取队列的长度 */\nsize := queue.Len()\n\n/* 判断队列是否为空 */\nisEmpty := queue.Len() == 0\n
queue.swift
/* 初始化队列 */\n// Swift 没有内置的队列类,可以把 Array 当作队列来使用\nvar queue: [Int] = []\n\n/* 元素入队 */\nqueue.append(1)\nqueue.append(3)\nqueue.append(2)\nqueue.append(5)\nqueue.append(4)\n\n/* 访问队首元素 */\nlet peek = queue.first!\n\n/* 元素出队 */\n// 由于是数组,因此 removeFirst 的复杂度为 O(n)\nlet pool = queue.removeFirst()\n\n/* 获取队列的长度 */\nlet size = queue.count\n\n/* 判断队列是否为空 */\nlet isEmpty = queue.isEmpty\n
queue.js
/* 初始化队列 */\n// JavaScript 没有内置的队列,可以把 Array 当作队列来使用\nconst queue = [];\n\n/* 元素入队 */\nqueue.push(1);\nqueue.push(3);\nqueue.push(2);\nqueue.push(5);\nqueue.push(4);\n\n/* 访问队首元素 */\nconst peek = queue[0];\n\n/* 元素出队 */\n// 底层是数组,因此 shift() 方法的时间复杂度为 O(n)\nconst pop = queue.shift();\n\n/* 获取队列的长度 */\nconst size = queue.length;\n\n/* 判断队列是否为空 */\nconst empty = queue.length === 0;\n
queue.ts
/* 初始化队列 */\n// TypeScript 没有内置的队列,可以把 Array 当作队列来使用\nconst queue: number[] = [];\n\n/* 元素入队 */\nqueue.push(1);\nqueue.push(3);\nqueue.push(2);\nqueue.push(5);\nqueue.push(4);\n\n/* 访问队首元素 */\nconst peek = queue[0];\n\n/* 元素出队 */\n// 底层是数组,因此 shift() 方法的时间复杂度为 O(n)\nconst pop = queue.shift();\n\n/* 获取队列的长度 */\nconst size = queue.length;\n\n/* 判断队列是否为空 */\nconst empty = queue.length === 0;\n
queue.dart
/* 初始化队列 */\n// 在 Dart 中,队列类 Qeque 是双向队列,也可作为队列使用\nQueue<int> queue = Queue();\n\n/* 元素入队 */\nqueue.add(1);\nqueue.add(3);\nqueue.add(2);\nqueue.add(5);\nqueue.add(4);\n\n/* 访问队首元素 */\nint peek = queue.first;\n\n/* 元素出队 */\nint pop = queue.removeFirst();\n\n/* 获取队列的长度 */\nint size = queue.length;\n\n/* 判断队列是否为空 */\nbool isEmpty = queue.isEmpty;\n
queue.rs
/* 初始化双向队列 */\n// 在 Rust 中使用双向队列作为普通队列来使用\nlet mut deque: VecDeque<u32> = VecDeque::new();\n\n/* 元素入队 */\ndeque.push_back(1);\ndeque.push_back(3);\ndeque.push_back(2);\ndeque.push_back(5);\ndeque.push_back(4);\n\n/* 访问队首元素 */\nif let Some(front) = deque.front() {\n}\n\n/* 元素出队 */\nif let Some(pop) = deque.pop_front() {\n}\n\n/* 获取队列的长度 */\nlet size = deque.len();\n\n/* 判断队列是否为空 */\nlet is_empty = deque.is_empty();\n
queue.c
// C 未提供内置队列\n
queue.kt
/* 初始化队列 */\nval queue = LinkedList<Int>()\n\n/* 元素入队 */\nqueue.offer(1)\nqueue.offer(3)\nqueue.offer(2)\nqueue.offer(5)\nqueue.offer(4)\n\n/* 访问队首元素 */\nval peek = queue.peek()\n\n/* 元素出队 */\nval pop = queue.poll()\n\n/* 获取队列的长度 */\nval size = queue.size\n\n/* 判断队列是否为空 */\nval isEmpty = queue.isEmpty()\n
queue.rb
# 初始化队列\n# Ruby 内置的队列(Thread::Queue) 没有 peek 和遍历方法,可以把 Array 当作队列来使用\nqueue = []\n\n# 元素入队\nqueue.push(1)\nqueue.push(3)\nqueue.push(2)\nqueue.push(5)\nqueue.push(4)\n\n# 访问队列元素\npeek = queue.first\n\n# 元素出队\n# 清注意,由于是数组,Array#shift 方法时间复杂度为 O(n)\npop = queue.shift\n\n# 获取队列的长度\nsize = queue.length\n\n# 判断队列是否为空\nis_empty = queue.empty?\n
可视化运行

全屏观看 >

","path":["第 5 章   栈与队列","5.2   队列"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#522","level":2,"title":"5.2.2   队列实现","text":"

为了实现队列,我们需要一种数据结构,可以在一端添加元素,并在另一端删除元素,链表和数组都符合要求。

","path":["第 5 章   栈与队列","5.2   队列"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#1","level":3,"title":"1.   基于链表的实现","text":"

如图 5-5 所示,我们可以将链表的“头节点”和“尾节点”分别视为“队首”和“队尾”,规定队尾仅可添加节点,队首仅可删除节点。

<1><2><3>

图 5-5   基于链表实现队列的入队出队操作

以下是用链表实现队列的代码:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linkedlist_queue.py
class LinkedListQueue:\n    \"\"\"基于链表实现的队列\"\"\"\n\n    def __init__(self):\n        \"\"\"构造方法\"\"\"\n        self._front: ListNode | None = None  # 头节点 front\n        self._rear: ListNode | None = None  # 尾节点 rear\n        self._size: int = 0\n\n    def size(self) -> int:\n        \"\"\"获取队列的长度\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"判断队列是否为空\"\"\"\n        return self._size == 0\n\n    def push(self, num: int):\n        \"\"\"入队\"\"\"\n        # 在尾节点后添加 num\n        node = ListNode(num)\n        # 如果队列为空,则令头、尾节点都指向该节点\n        if self._front is None:\n            self._front = node\n            self._rear = node\n        # 如果队列不为空,则将该节点添加到尾节点后\n        else:\n            self._rear.next = node\n            self._rear = node\n        self._size += 1\n\n    def pop(self) -> int:\n        \"\"\"出队\"\"\"\n        num = self.peek()\n        # 删除头节点\n        self._front = self._front.next\n        self._size -= 1\n        return num\n\n    def peek(self) -> int:\n        \"\"\"访问队首元素\"\"\"\n        if self.is_empty():\n            raise IndexError(\"队列为空\")\n        return self._front.val\n\n    def to_list(self) -> list[int]:\n        \"\"\"转化为列表用于打印\"\"\"\n        queue = []\n        temp = self._front\n        while temp:\n            queue.append(temp.val)\n            temp = temp.next\n        return queue\n
linkedlist_queue.cpp
/* 基于链表实现的队列 */\nclass LinkedListQueue {\n  private:\n    ListNode *front, *rear; // 头节点 front ,尾节点 rear\n    int queSize;\n\n  public:\n    LinkedListQueue() {\n        front = nullptr;\n        rear = nullptr;\n        queSize = 0;\n    }\n\n    ~LinkedListQueue() {\n        // 遍历链表删除节点,释放内存\n        freeMemoryLinkedList(front);\n    }\n\n    /* 获取队列的长度 */\n    int size() {\n        return queSize;\n    }\n\n    /* 判断队列是否为空 */\n    bool isEmpty() {\n        return queSize == 0;\n    }\n\n    /* 入队 */\n    void push(int num) {\n        // 在尾节点后添加 num\n        ListNode *node = new ListNode(num);\n        // 如果队列为空,则令头、尾节点都指向该节点\n        if (front == nullptr) {\n            front = node;\n            rear = node;\n        }\n        // 如果队列不为空,则将该节点添加到尾节点后\n        else {\n            rear->next = node;\n            rear = node;\n        }\n        queSize++;\n    }\n\n    /* 出队 */\n    int pop() {\n        int num = peek();\n        // 删除头节点\n        ListNode *tmp = front;\n        front = front->next;\n        // 释放内存\n        delete tmp;\n        queSize--;\n        return num;\n    }\n\n    /* 访问队首元素 */\n    int peek() {\n        if (size() == 0)\n            throw out_of_range(\"队列为空\");\n        return front->val;\n    }\n\n    /* 将链表转化为 Vector 并返回 */\n    vector<int> toVector() {\n        ListNode *node = front;\n        vector<int> res(size());\n        for (int i = 0; i < res.size(); i++) {\n            res[i] = node->val;\n            node = node->next;\n        }\n        return res;\n    }\n};\n
linkedlist_queue.java
/* 基于链表实现的队列 */\nclass LinkedListQueue {\n    private ListNode front, rear; // 头节点 front ,尾节点 rear\n    private int queSize = 0;\n\n    public LinkedListQueue() {\n        front = null;\n        rear = null;\n    }\n\n    /* 获取队列的长度 */\n    public int size() {\n        return queSize;\n    }\n\n    /* 判断队列是否为空 */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* 入队 */\n    public void push(int num) {\n        // 在尾节点后添加 num\n        ListNode node = new ListNode(num);\n        // 如果队列为空,则令头、尾节点都指向该节点\n        if (front == null) {\n            front = node;\n            rear = node;\n        // 如果队列不为空,则将该节点添加到尾节点后\n        } else {\n            rear.next = node;\n            rear = node;\n        }\n        queSize++;\n    }\n\n    /* 出队 */\n    public int pop() {\n        int num = peek();\n        // 删除头节点\n        front = front.next;\n        queSize--;\n        return num;\n    }\n\n    /* 访问队首元素 */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return front.val;\n    }\n\n    /* 将链表转化为 Array 并返回 */\n    public int[] toArray() {\n        ListNode node = front;\n        int[] res = new int[size()];\n        for (int i = 0; i < res.length; i++) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_queue.cs
/* 基于链表实现的队列 */\nclass LinkedListQueue {\n    ListNode? front, rear;  // 头节点 front ,尾节点 rear \n    int queSize = 0;\n\n    public LinkedListQueue() {\n        front = null;\n        rear = null;\n    }\n\n    /* 获取队列的长度 */\n    public int Size() {\n        return queSize;\n    }\n\n    /* 判断队列是否为空 */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* 入队 */\n    public void Push(int num) {\n        // 在尾节点后添加 num\n        ListNode node = new(num);\n        // 如果队列为空,则令头、尾节点都指向该节点\n        if (front == null) {\n            front = node;\n            rear = node;\n            // 如果队列不为空,则将该节点添加到尾节点后\n        } else if (rear != null) {\n            rear.next = node;\n            rear = node;\n        }\n        queSize++;\n    }\n\n    /* 出队 */\n    public int Pop() {\n        int num = Peek();\n        // 删除头节点\n        front = front?.next;\n        queSize--;\n        return num;\n    }\n\n    /* 访问队首元素 */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return front!.val;\n    }\n\n    /* 将链表转化为 Array 并返回 */\n    public int[] ToArray() {\n        if (front == null)\n            return [];\n\n        ListNode? node = front;\n        int[] res = new int[Size()];\n        for (int i = 0; i < res.Length; i++) {\n            res[i] = node!.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_queue.go
/* 基于链表实现的队列 */\ntype linkedListQueue struct {\n    // 使用内置包 list 来实现队列\n    data *list.List\n}\n\n/* 初始化队列 */\nfunc newLinkedListQueue() *linkedListQueue {\n    return &linkedListQueue{\n        data: list.New(),\n    }\n}\n\n/* 入队 */\nfunc (s *linkedListQueue) push(value any) {\n    s.data.PushBack(value)\n}\n\n/* 出队 */\nfunc (s *linkedListQueue) pop() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Front()\n    s.data.Remove(e)\n    return e.Value\n}\n\n/* 访问队首元素 */\nfunc (s *linkedListQueue) peek() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Front()\n    return e.Value\n}\n\n/* 获取队列的长度 */\nfunc (s *linkedListQueue) size() int {\n    return s.data.Len()\n}\n\n/* 判断队列是否为空 */\nfunc (s *linkedListQueue) isEmpty() bool {\n    return s.data.Len() == 0\n}\n\n/* 获取 List 用于打印 */\nfunc (s *linkedListQueue) toList() *list.List {\n    return s.data\n}\n
linkedlist_queue.swift
/* 基于链表实现的队列 */\nclass LinkedListQueue {\n    private var front: ListNode? // 头节点\n    private var rear: ListNode? // 尾节点\n    private var _size: Int\n\n    init() {\n        _size = 0\n    }\n\n    /* 获取队列的长度 */\n    func size() -> Int {\n        _size\n    }\n\n    /* 判断队列是否为空 */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* 入队 */\n    func push(num: Int) {\n        // 在尾节点后添加 num\n        let node = ListNode(x: num)\n        // 如果队列为空,则令头、尾节点都指向该节点\n        if front == nil {\n            front = node\n            rear = node\n        }\n        // 如果队列不为空,则将该节点添加到尾节点后\n        else {\n            rear?.next = node\n            rear = node\n        }\n        _size += 1\n    }\n\n    /* 出队 */\n    @discardableResult\n    func pop() -> Int {\n        let num = peek()\n        // 删除头节点\n        front = front?.next\n        _size -= 1\n        return num\n    }\n\n    /* 访问队首元素 */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"队列为空\")\n        }\n        return front!.val\n    }\n\n    /* 将链表转化为 Array 并返回 */\n    func toArray() -> [Int] {\n        var node = front\n        var res = Array(repeating: 0, count: size())\n        for i in res.indices {\n            res[i] = node!.val\n            node = node?.next\n        }\n        return res\n    }\n}\n
linkedlist_queue.js
/* 基于链表实现的队列 */\nclass LinkedListQueue {\n    #front; // 头节点 #front\n    #rear; // 尾节点 #rear\n    #queSize = 0;\n\n    constructor() {\n        this.#front = null;\n        this.#rear = null;\n    }\n\n    /* 获取队列的长度 */\n    get size() {\n        return this.#queSize;\n    }\n\n    /* 判断队列是否为空 */\n    isEmpty() {\n        return this.size === 0;\n    }\n\n    /* 入队 */\n    push(num) {\n        // 在尾节点后添加 num\n        const node = new ListNode(num);\n        // 如果队列为空,则令头、尾节点都指向该节点\n        if (!this.#front) {\n            this.#front = node;\n            this.#rear = node;\n            // 如果队列不为空,则将该节点添加到尾节点后\n        } else {\n            this.#rear.next = node;\n            this.#rear = node;\n        }\n        this.#queSize++;\n    }\n\n    /* 出队 */\n    pop() {\n        const num = this.peek();\n        // 删除头节点\n        this.#front = this.#front.next;\n        this.#queSize--;\n        return num;\n    }\n\n    /* 访问队首元素 */\n    peek() {\n        if (this.size === 0) throw new Error('队列为空');\n        return this.#front.val;\n    }\n\n    /* 将链表转化为 Array 并返回 */\n    toArray() {\n        let node = this.#front;\n        const res = new Array(this.size);\n        for (let i = 0; i < res.length; i++) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_queue.ts
/* 基于链表实现的队列 */\nclass LinkedListQueue {\n    private front: ListNode | null; // 头节点 front\n    private rear: ListNode | null; // 尾节点 rear\n    private queSize: number = 0;\n\n    constructor() {\n        this.front = null;\n        this.rear = null;\n    }\n\n    /* 获取队列的长度 */\n    get size(): number {\n        return this.queSize;\n    }\n\n    /* 判断队列是否为空 */\n    isEmpty(): boolean {\n        return this.size === 0;\n    }\n\n    /* 入队 */\n    push(num: number): void {\n        // 在尾节点后添加 num\n        const node = new ListNode(num);\n        // 如果队列为空,则令头、尾节点都指向该节点\n        if (!this.front) {\n            this.front = node;\n            this.rear = node;\n            // 如果队列不为空,则将该节点添加到尾节点后\n        } else {\n            this.rear!.next = node;\n            this.rear = node;\n        }\n        this.queSize++;\n    }\n\n    /* 出队 */\n    pop(): number {\n        const num = this.peek();\n        if (!this.front) throw new Error('队列为空');\n        // 删除头节点\n        this.front = this.front.next;\n        this.queSize--;\n        return num;\n    }\n\n    /* 访问队首元素 */\n    peek(): number {\n        if (this.size === 0) throw new Error('队列为空');\n        return this.front!.val;\n    }\n\n    /* 将链表转化为 Array 并返回 */\n    toArray(): number[] {\n        let node = this.front;\n        const res = new Array<number>(this.size);\n        for (let i = 0; i < res.length; i++) {\n            res[i] = node!.val;\n            node = node!.next;\n        }\n        return res;\n    }\n}\n
linkedlist_queue.dart
/* 基于链表实现的队列 */\nclass LinkedListQueue {\n  ListNode? _front; // 头节点 _front\n  ListNode? _rear; // 尾节点 _rear\n  int _queSize = 0; // 队列长度\n\n  LinkedListQueue() {\n    _front = null;\n    _rear = null;\n  }\n\n  /* 获取队列的长度 */\n  int size() {\n    return _queSize;\n  }\n\n  /* 判断队列是否为空 */\n  bool isEmpty() {\n    return _queSize == 0;\n  }\n\n  /* 入队 */\n  void push(int _num) {\n    // 在尾节点后添加 _num\n    final node = ListNode(_num);\n    // 如果队列为空,则令头、尾节点都指向该节点\n    if (_front == null) {\n      _front = node;\n      _rear = node;\n    } else {\n      // 如果队列不为空,则将该节点添加到尾节点后\n      _rear!.next = node;\n      _rear = node;\n    }\n    _queSize++;\n  }\n\n  /* 出队 */\n  int pop() {\n    final int _num = peek();\n    // 删除头节点\n    _front = _front!.next;\n    _queSize--;\n    return _num;\n  }\n\n  /* 访问队首元素 */\n  int peek() {\n    if (_queSize == 0) {\n      throw Exception('队列为空');\n    }\n    return _front!.val;\n  }\n\n  /* 将链表转化为 Array 并返回 */\n  List<int> toArray() {\n    ListNode? node = _front;\n    final List<int> queue = [];\n    while (node != null) {\n      queue.add(node.val);\n      node = node.next;\n    }\n    return queue;\n  }\n}\n
linkedlist_queue.rs
/* 基于链表实现的队列 */\n#[allow(dead_code)]\npub struct LinkedListQueue<T> {\n    front: Option<Rc<RefCell<ListNode<T>>>>, // 头节点 front\n    rear: Option<Rc<RefCell<ListNode<T>>>>,  // 尾节点 rear\n    que_size: usize,                         // 队列的长度\n}\n\nimpl<T: Copy> LinkedListQueue<T> {\n    pub fn new() -> Self {\n        Self {\n            front: None,\n            rear: None,\n            que_size: 0,\n        }\n    }\n\n    /* 获取队列的长度 */\n    pub fn size(&self) -> usize {\n        return self.que_size;\n    }\n\n    /* 判断队列是否为空 */\n    pub fn is_empty(&self) -> bool {\n        return self.que_size == 0;\n    }\n\n    /* 入队 */\n    pub fn push(&mut self, num: T) {\n        // 在尾节点后添加 num\n        let new_rear = ListNode::new(num);\n        match self.rear.take() {\n            // 如果队列不为空,则将该节点添加到尾节点后\n            Some(old_rear) => {\n                old_rear.borrow_mut().next = Some(new_rear.clone());\n                self.rear = Some(new_rear);\n            }\n            // 如果队列为空,则令头、尾节点都指向该节点\n            None => {\n                self.front = Some(new_rear.clone());\n                self.rear = Some(new_rear);\n            }\n        }\n        self.que_size += 1;\n    }\n\n    /* 出队 */\n    pub fn pop(&mut self) -> Option<T> {\n        self.front.take().map(|old_front| {\n            match old_front.borrow_mut().next.take() {\n                Some(new_front) => {\n                    self.front = Some(new_front);\n                }\n                None => {\n                    self.rear.take();\n                }\n            }\n            self.que_size -= 1;\n            old_front.borrow().val\n        })\n    }\n\n    /* 访问队首元素 */\n    pub fn peek(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.front.as_ref()\n    }\n\n    /* 将链表转化为 Array 并返回 */\n    pub fn to_array(&self, head: Option<&Rc<RefCell<ListNode<T>>>>) -> Vec<T> {\n        let mut res: Vec<T> = Vec::new();\n\n        fn recur<T: Copy>(cur: Option<&Rc<RefCell<ListNode<T>>>>, res: &mut Vec<T>) {\n            if let Some(cur) = cur {\n                res.push(cur.borrow().val);\n                recur(cur.borrow().next.as_ref(), res);\n            }\n        }\n\n        recur(head, &mut res);\n\n        res\n    }\n}\n
linkedlist_queue.c
/* 基于链表实现的队列 */\ntypedef struct {\n    ListNode *front, *rear;\n    int queSize;\n} LinkedListQueue;\n\n/* 构造函数 */\nLinkedListQueue *newLinkedListQueue() {\n    LinkedListQueue *queue = (LinkedListQueue *)malloc(sizeof(LinkedListQueue));\n    queue->front = NULL;\n    queue->rear = NULL;\n    queue->queSize = 0;\n    return queue;\n}\n\n/* 析构函数 */\nvoid delLinkedListQueue(LinkedListQueue *queue) {\n    // 释放所有节点\n    while (queue->front != NULL) {\n        ListNode *tmp = queue->front;\n        queue->front = queue->front->next;\n        free(tmp);\n    }\n    // 释放 queue 结构体\n    free(queue);\n}\n\n/* 获取队列的长度 */\nint size(LinkedListQueue *queue) {\n    return queue->queSize;\n}\n\n/* 判断队列是否为空 */\nbool empty(LinkedListQueue *queue) {\n    return (size(queue) == 0);\n}\n\n/* 入队 */\nvoid push(LinkedListQueue *queue, int num) {\n    // 尾节点处添加 node\n    ListNode *node = newListNode(num);\n    // 如果队列为空,则令头、尾节点都指向该节点\n    if (queue->front == NULL) {\n        queue->front = node;\n        queue->rear = node;\n    }\n    // 如果队列不为空,则将该节点添加到尾节点后\n    else {\n        queue->rear->next = node;\n        queue->rear = node;\n    }\n    queue->queSize++;\n}\n\n/* 访问队首元素 */\nint peek(LinkedListQueue *queue) {\n    assert(size(queue) && queue->front);\n    return queue->front->val;\n}\n\n/* 出队 */\nint pop(LinkedListQueue *queue) {\n    int num = peek(queue);\n    ListNode *tmp = queue->front;\n    queue->front = queue->front->next;\n    free(tmp);\n    queue->queSize--;\n    return num;\n}\n\n/* 打印队列 */\nvoid printLinkedListQueue(LinkedListQueue *queue) {\n    int *arr = malloc(sizeof(int) * queue->queSize);\n    // 拷贝链表中的数据到数组\n    int i;\n    ListNode *node;\n    for (i = 0, node = queue->front; i < queue->queSize; i++) {\n        arr[i] = node->val;\n        node = node->next;\n    }\n    printArray(arr, queue->queSize);\n    free(arr);\n}\n
linkedlist_queue.kt
/* 基于链表实现的队列 */\nclass LinkedListQueue(\n    // 头节点 front ,尾节点 rear\n    private var front: ListNode? = null,\n    private var rear: ListNode? = null,\n    private var queSize: Int = 0\n) {\n\n    /* 获取队列的长度 */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* 判断队列是否为空 */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* 入队 */\n    fun push(num: Int) {\n        // 在尾节点后添加 num\n        val node = ListNode(num)\n        // 如果队列为空,则令头、尾节点都指向该节点\n        if (front == null) {\n            front = node\n            rear = node\n            // 如果队列不为空,则将该节点添加到尾节点后\n        } else {\n            rear?.next = node\n            rear = node\n        }\n        queSize++\n    }\n\n    /* 出队 */\n    fun pop(): Int {\n        val num = peek()\n        // 删除头节点\n        front = front?.next\n        queSize--\n        return num\n    }\n\n    /* 访问队首元素 */\n    fun peek(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return front!!._val\n    }\n\n    /* 将链表转化为 Array 并返回 */\n    fun toArray(): IntArray {\n        var node = front\n        val res = IntArray(size())\n        for (i in res.indices) {\n            res[i] = node!!._val\n            node = node.next\n        }\n        return res\n    }\n}\n
linkedlist_queue.rb
### 基于链表头现的队列 ###\nclass LinkedListQueue\n  ### 获取队列的长度 ###\n  attr_reader :size\n\n  ### 构造方法 ###\n  def initialize\n    @front = nil  # 头节点 front\n    @rear = nil   # 尾节点 rear\n    @size = 0\n  end\n\n  ### 判断队列是否为空 ###\n  def is_empty?\n    @front.nil?\n  end\n\n  ### 入队 ###\n  def push(num)\n    # 在尾节点后添加 num\n    node = ListNode.new(num)\n\n    # 如果队列为空,则令头,尾节点都指向该节点\n    if @front.nil?\n      @front = node\n      @rear = node\n    # 如果队列不为空,则令该节点添加到尾节点后\n    else\n      @rear.next = node\n      @rear = node\n    end\n\n    @size += 1\n  end\n\n  ### 出队 ###\n  def pop\n    num = peek\n    # 删除头节点\n    @front = @front.next\n    @size -= 1\n    num\n  end\n\n  ### 访问队首元素 ###\n  def peek\n    raise IndexError, '队列为空' if is_empty?\n\n    @front.val\n  end\n\n  ### 将链表为 Array 并返回 ###\n  def to_array\n    queue = []\n    temp = @front\n    while temp\n      queue << temp.val\n      temp = temp.next\n    end\n    queue\n  end\nend\n
可视化运行

全屏观看 >

","path":["第 5 章   栈与队列","5.2   队列"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#2","level":3,"title":"2.   基于数组的实现","text":"

在数组中删除首元素的时间复杂度为 \\(O(n)\\) ,这会导致出队操作效率较低。然而,我们可以采用以下巧妙方法来避免这个问题。

我们可以使用一个变量 front 指向队首元素的索引,并维护一个变量 size 用于记录队列长度。定义 rear = front + size ,这个公式计算出的 rear 指向队尾元素之后的下一个位置。

基于此设计,数组中包含元素的有效区间为 [front, rear - 1],各种操作的实现方法如图 5-6 所示。

可以看到,入队和出队操作都只需进行一次操作,时间复杂度均为 \\(O(1)\\) 。

<1><2><3>

图 5-6   基于数组实现队列的入队出队操作

你可能会发现一个问题:在不断进行入队和出队的过程中,frontrear 都在向右移动,当它们到达数组尾部时就无法继续移动了。为了解决此问题,我们可以将数组视为首尾相接的“环形数组”。

对于环形数组,我们需要让 frontrear 在越过数组尾部时,直接回到数组头部继续遍历。这种周期性规律可以通过“取余操作”来实现,代码如下所示:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_queue.py
class ArrayQueue:\n    \"\"\"基于环形数组实现的队列\"\"\"\n\n    def __init__(self, size: int):\n        \"\"\"构造方法\"\"\"\n        self._nums: list[int] = [0] * size  # 用于存储队列元素的数组\n        self._front: int = 0  # 队首指针,指向队首元素\n        self._size: int = 0  # 队列长度\n\n    def capacity(self) -> int:\n        \"\"\"获取队列的容量\"\"\"\n        return len(self._nums)\n\n    def size(self) -> int:\n        \"\"\"获取队列的长度\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"判断队列是否为空\"\"\"\n        return self._size == 0\n\n    def push(self, num: int):\n        \"\"\"入队\"\"\"\n        if self._size == self.capacity():\n            raise IndexError(\"队列已满\")\n        # 计算队尾指针,指向队尾索引 + 1\n        # 通过取余操作实现 rear 越过数组尾部后回到头部\n        rear: int = (self._front + self._size) % self.capacity()\n        # 将 num 添加至队尾\n        self._nums[rear] = num\n        self._size += 1\n\n    def pop(self) -> int:\n        \"\"\"出队\"\"\"\n        num: int = self.peek()\n        # 队首指针向后移动一位,若越过尾部,则返回到数组头部\n        self._front = (self._front + 1) % self.capacity()\n        self._size -= 1\n        return num\n\n    def peek(self) -> int:\n        \"\"\"访问队首元素\"\"\"\n        if self.is_empty():\n            raise IndexError(\"队列为空\")\n        return self._nums[self._front]\n\n    def to_list(self) -> list[int]:\n        \"\"\"返回列表用于打印\"\"\"\n        res = [0] * self.size()\n        j: int = self._front\n        for i in range(self.size()):\n            res[i] = self._nums[(j % self.capacity())]\n            j += 1\n        return res\n
array_queue.cpp
/* 基于环形数组实现的队列 */\nclass ArrayQueue {\n  private:\n    int *nums;       // 用于存储队列元素的数组\n    int front;       // 队首指针,指向队首元素\n    int queSize;     // 队列长度\n    int queCapacity; // 队列容量\n\n  public:\n    ArrayQueue(int capacity) {\n        // 初始化数组\n        nums = new int[capacity];\n        queCapacity = capacity;\n        front = queSize = 0;\n    }\n\n    ~ArrayQueue() {\n        delete[] nums;\n    }\n\n    /* 获取队列的容量 */\n    int capacity() {\n        return queCapacity;\n    }\n\n    /* 获取队列的长度 */\n    int size() {\n        return queSize;\n    }\n\n    /* 判断队列是否为空 */\n    bool isEmpty() {\n        return size() == 0;\n    }\n\n    /* 入队 */\n    void push(int num) {\n        if (queSize == queCapacity) {\n            cout << \"队列已满\" << endl;\n            return;\n        }\n        // 计算队尾指针,指向队尾索引 + 1\n        // 通过取余操作实现 rear 越过数组尾部后回到头部\n        int rear = (front + queSize) % queCapacity;\n        // 将 num 添加至队尾\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* 出队 */\n    int pop() {\n        int num = peek();\n        // 队首指针向后移动一位,若越过尾部,则返回到数组头部\n        front = (front + 1) % queCapacity;\n        queSize--;\n        return num;\n    }\n\n    /* 访问队首元素 */\n    int peek() {\n        if (isEmpty())\n            throw out_of_range(\"队列为空\");\n        return nums[front];\n    }\n\n    /* 将数组转化为 Vector 并返回 */\n    vector<int> toVector() {\n        // 仅转换有效长度范围内的列表元素\n        vector<int> arr(queSize);\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            arr[i] = nums[j % queCapacity];\n        }\n        return arr;\n    }\n};\n
array_queue.java
/* 基于环形数组实现的队列 */\nclass ArrayQueue {\n    private int[] nums; // 用于存储队列元素的数组\n    private int front; // 队首指针,指向队首元素\n    private int queSize; // 队列长度\n\n    public ArrayQueue(int capacity) {\n        nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* 获取队列的容量 */\n    public int capacity() {\n        return nums.length;\n    }\n\n    /* 获取队列的长度 */\n    public int size() {\n        return queSize;\n    }\n\n    /* 判断队列是否为空 */\n    public boolean isEmpty() {\n        return queSize == 0;\n    }\n\n    /* 入队 */\n    public void push(int num) {\n        if (queSize == capacity()) {\n            System.out.println(\"队列已满\");\n            return;\n        }\n        // 计算队尾指针,指向队尾索引 + 1\n        // 通过取余操作实现 rear 越过数组尾部后回到头部\n        int rear = (front + queSize) % capacity();\n        // 将 num 添加至队尾\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* 出队 */\n    public int pop() {\n        int num = peek();\n        // 队首指针向后移动一位,若越过尾部,则返回到数组头部\n        front = (front + 1) % capacity();\n        queSize--;\n        return num;\n    }\n\n    /* 访问队首元素 */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return nums[front];\n    }\n\n    /* 返回数组 */\n    public int[] toArray() {\n        // 仅转换有效长度范围内的列表元素\n        int[] res = new int[queSize];\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[j % capacity()];\n        }\n        return res;\n    }\n}\n
array_queue.cs
/* 基于环形数组实现的队列 */\nclass ArrayQueue {\n    int[] nums;  // 用于存储队列元素的数组\n    int front;   // 队首指针,指向队首元素\n    int queSize; // 队列长度\n\n    public ArrayQueue(int capacity) {\n        nums = new int[capacity];\n        front = queSize = 0;\n    }\n\n    /* 获取队列的容量 */\n    int Capacity() {\n        return nums.Length;\n    }\n\n    /* 获取队列的长度 */\n    public int Size() {\n        return queSize;\n    }\n\n    /* 判断队列是否为空 */\n    public bool IsEmpty() {\n        return queSize == 0;\n    }\n\n    /* 入队 */\n    public void Push(int num) {\n        if (queSize == Capacity()) {\n            Console.WriteLine(\"队列已满\");\n            return;\n        }\n        // 计算队尾指针,指向队尾索引 + 1\n        // 通过取余操作实现 rear 越过数组尾部后回到头部\n        int rear = (front + queSize) % Capacity();\n        // 将 num 添加至队尾\n        nums[rear] = num;\n        queSize++;\n    }\n\n    /* 出队 */\n    public int Pop() {\n        int num = Peek();\n        // 队首指针向后移动一位,若越过尾部,则返回到数组头部\n        front = (front + 1) % Capacity();\n        queSize--;\n        return num;\n    }\n\n    /* 访问队首元素 */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return nums[front];\n    }\n\n    /* 返回数组 */\n    public int[] ToArray() {\n        // 仅转换有效长度范围内的列表元素\n        int[] res = new int[queSize];\n        for (int i = 0, j = front; i < queSize; i++, j++) {\n            res[i] = nums[j % this.Capacity()];\n        }\n        return res;\n    }\n}\n
array_queue.go
/* 基于环形数组实现的队列 */\ntype arrayQueue struct {\n    nums        []int // 用于存储队列元素的数组\n    front       int   // 队首指针,指向队首元素\n    queSize     int   // 队列长度\n    queCapacity int   // 队列容量(即最大容纳元素数量)\n}\n\n/* 初始化队列 */\nfunc newArrayQueue(queCapacity int) *arrayQueue {\n    return &arrayQueue{\n        nums:        make([]int, queCapacity),\n        queCapacity: queCapacity,\n        front:       0,\n        queSize:     0,\n    }\n}\n\n/* 获取队列的长度 */\nfunc (q *arrayQueue) size() int {\n    return q.queSize\n}\n\n/* 判断队列是否为空 */\nfunc (q *arrayQueue) isEmpty() bool {\n    return q.queSize == 0\n}\n\n/* 入队 */\nfunc (q *arrayQueue) push(num int) {\n    // 当 rear == queCapacity 表示队列已满\n    if q.queSize == q.queCapacity {\n        return\n    }\n    // 计算队尾指针,指向队尾索引 + 1\n    // 通过取余操作实现 rear 越过数组尾部后回到头部\n    rear := (q.front + q.queSize) % q.queCapacity\n    // 将 num 添加至队尾\n    q.nums[rear] = num\n    q.queSize++\n}\n\n/* 出队 */\nfunc (q *arrayQueue) pop() any {\n    num := q.peek()\n    if num == nil {\n        return nil\n    }\n\n    // 队首指针向后移动一位,若越过尾部,则返回到数组头部\n    q.front = (q.front + 1) % q.queCapacity\n    q.queSize--\n    return num\n}\n\n/* 访问队首元素 */\nfunc (q *arrayQueue) peek() any {\n    if q.isEmpty() {\n        return nil\n    }\n    return q.nums[q.front]\n}\n\n/* 获取 Slice 用于打印 */\nfunc (q *arrayQueue) toSlice() []int {\n    rear := (q.front + q.queSize)\n    if rear >= q.queCapacity {\n        rear %= q.queCapacity\n        return append(q.nums[q.front:], q.nums[:rear]...)\n    }\n    return q.nums[q.front:rear]\n}\n
array_queue.swift
/* 基于环形数组实现的队列 */\nclass ArrayQueue {\n    private var nums: [Int] // 用于存储队列元素的数组\n    private var front: Int // 队首指针,指向队首元素\n    private var _size: Int // 队列长度\n\n    init(capacity: Int) {\n        // 初始化数组\n        nums = Array(repeating: 0, count: capacity)\n        front = 0\n        _size = 0\n    }\n\n    /* 获取队列的容量 */\n    func capacity() -> Int {\n        nums.count\n    }\n\n    /* 获取队列的长度 */\n    func size() -> Int {\n        _size\n    }\n\n    /* 判断队列是否为空 */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* 入队 */\n    func push(num: Int) {\n        if size() == capacity() {\n            print(\"队列已满\")\n            return\n        }\n        // 计算队尾指针,指向队尾索引 + 1\n        // 通过取余操作实现 rear 越过数组尾部后回到头部\n        let rear = (front + size()) % capacity()\n        // 将 num 添加至队尾\n        nums[rear] = num\n        _size += 1\n    }\n\n    /* 出队 */\n    @discardableResult\n    func pop() -> Int {\n        let num = peek()\n        // 队首指针向后移动一位,若越过尾部,则返回到数组头部\n        front = (front + 1) % capacity()\n        _size -= 1\n        return num\n    }\n\n    /* 访问队首元素 */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"队列为空\")\n        }\n        return nums[front]\n    }\n\n    /* 返回数组 */\n    func toArray() -> [Int] {\n        // 仅转换有效长度范围内的列表元素\n        (front ..< front + size()).map { nums[$0 % capacity()] }\n    }\n}\n
array_queue.js
/* 基于环形数组实现的队列 */\nclass ArrayQueue {\n    #nums; // 用于存储队列元素的数组\n    #front = 0; // 队首指针,指向队首元素\n    #queSize = 0; // 队列长度\n\n    constructor(capacity) {\n        this.#nums = new Array(capacity);\n    }\n\n    /* 获取队列的容量 */\n    get capacity() {\n        return this.#nums.length;\n    }\n\n    /* 获取队列的长度 */\n    get size() {\n        return this.#queSize;\n    }\n\n    /* 判断队列是否为空 */\n    isEmpty() {\n        return this.#queSize === 0;\n    }\n\n    /* 入队 */\n    push(num) {\n        if (this.size === this.capacity) {\n            console.log('队列已满');\n            return;\n        }\n        // 计算队尾指针,指向队尾索引 + 1\n        // 通过取余操作实现 rear 越过数组尾部后回到头部\n        const rear = (this.#front + this.size) % this.capacity;\n        // 将 num 添加至队尾\n        this.#nums[rear] = num;\n        this.#queSize++;\n    }\n\n    /* 出队 */\n    pop() {\n        const num = this.peek();\n        // 队首指针向后移动一位,若越过尾部,则返回到数组头部\n        this.#front = (this.#front + 1) % this.capacity;\n        this.#queSize--;\n        return num;\n    }\n\n    /* 访问队首元素 */\n    peek() {\n        if (this.isEmpty()) throw new Error('队列为空');\n        return this.#nums[this.#front];\n    }\n\n    /* 返回 Array */\n    toArray() {\n        // 仅转换有效长度范围内的列表元素\n        const arr = new Array(this.size);\n        for (let i = 0, j = this.#front; i < this.size; i++, j++) {\n            arr[i] = this.#nums[j % this.capacity];\n        }\n        return arr;\n    }\n}\n
array_queue.ts
/* 基于环形数组实现的队列 */\nclass ArrayQueue {\n    private nums: number[]; // 用于存储队列元素的数组\n    private front: number; // 队首指针,指向队首元素\n    private queSize: number; // 队列长度\n\n    constructor(capacity: number) {\n        this.nums = new Array(capacity);\n        this.front = this.queSize = 0;\n    }\n\n    /* 获取队列的容量 */\n    get capacity(): number {\n        return this.nums.length;\n    }\n\n    /* 获取队列的长度 */\n    get size(): number {\n        return this.queSize;\n    }\n\n    /* 判断队列是否为空 */\n    isEmpty(): boolean {\n        return this.queSize === 0;\n    }\n\n    /* 入队 */\n    push(num: number): void {\n        if (this.size === this.capacity) {\n            console.log('队列已满');\n            return;\n        }\n        // 计算队尾指针,指向队尾索引 + 1\n        // 通过取余操作实现 rear 越过数组尾部后回到头部\n        const rear = (this.front + this.queSize) % this.capacity;\n        // 将 num 添加至队尾\n        this.nums[rear] = num;\n        this.queSize++;\n    }\n\n    /* 出队 */\n    pop(): number {\n        const num = this.peek();\n        // 队首指针向后移动一位,若越过尾部,则返回到数组头部\n        this.front = (this.front + 1) % this.capacity;\n        this.queSize--;\n        return num;\n    }\n\n    /* 访问队首元素 */\n    peek(): number {\n        if (this.isEmpty()) throw new Error('队列为空');\n        return this.nums[this.front];\n    }\n\n    /* 返回 Array */\n    toArray(): number[] {\n        // 仅转换有效长度范围内的列表元素\n        const arr = new Array(this.size);\n        for (let i = 0, j = this.front; i < this.size; i++, j++) {\n            arr[i] = this.nums[j % this.capacity];\n        }\n        return arr;\n    }\n}\n
array_queue.dart
/* 基于环形数组实现的队列 */\nclass ArrayQueue {\n  late List<int> _nums; // 用于储存队列元素的数组\n  late int _front; // 队首指针,指向队首元素\n  late int _queSize; // 队列长度\n\n  ArrayQueue(int capacity) {\n    _nums = List.filled(capacity, 0);\n    _front = _queSize = 0;\n  }\n\n  /* 获取队列的容量 */\n  int capaCity() {\n    return _nums.length;\n  }\n\n  /* 获取队列的长度 */\n  int size() {\n    return _queSize;\n  }\n\n  /* 判断队列是否为空 */\n  bool isEmpty() {\n    return _queSize == 0;\n  }\n\n  /* 入队 */\n  void push(int _num) {\n    if (_queSize == capaCity()) {\n      throw Exception(\"队列已满\");\n    }\n    // 计算队尾指针,指向队尾索引 + 1\n    // 通过取余操作实现 rear 越过数组尾部后回到头部\n    int rear = (_front + _queSize) % capaCity();\n    // 将 _num 添加至队尾\n    _nums[rear] = _num;\n    _queSize++;\n  }\n\n  /* 出队 */\n  int pop() {\n    int _num = peek();\n    // 队首指针向后移动一位,若越过尾部,则返回到数组头部\n    _front = (_front + 1) % capaCity();\n    _queSize--;\n    return _num;\n  }\n\n  /* 访问队首元素 */\n  int peek() {\n    if (isEmpty()) {\n      throw Exception(\"队列为空\");\n    }\n    return _nums[_front];\n  }\n\n  /* 返回 Array */\n  List<int> toArray() {\n    // 仅转换有效长度范围内的列表元素\n    final List<int> res = List.filled(_queSize, 0);\n    for (int i = 0, j = _front; i < _queSize; i++, j++) {\n      res[i] = _nums[j % capaCity()];\n    }\n    return res;\n  }\n}\n
array_queue.rs
/* 基于环形数组实现的队列 */\nstruct ArrayQueue<T> {\n    nums: Vec<T>,      // 用于存储队列元素的数组\n    front: i32,        // 队首指针,指向队首元素\n    que_size: i32,     // 队列长度\n    que_capacity: i32, // 队列容量\n}\n\nimpl<T: Copy + Default> ArrayQueue<T> {\n    /* 构造方法 */\n    fn new(capacity: i32) -> ArrayQueue<T> {\n        ArrayQueue {\n            nums: vec![T::default(); capacity as usize],\n            front: 0,\n            que_size: 0,\n            que_capacity: capacity,\n        }\n    }\n\n    /* 获取队列的容量 */\n    fn capacity(&self) -> i32 {\n        self.que_capacity\n    }\n\n    /* 获取队列的长度 */\n    fn size(&self) -> i32 {\n        self.que_size\n    }\n\n    /* 判断队列是否为空 */\n    fn is_empty(&self) -> bool {\n        self.que_size == 0\n    }\n\n    /* 入队 */\n    fn push(&mut self, num: T) {\n        if self.que_size == self.capacity() {\n            println!(\"队列已满\");\n            return;\n        }\n        // 计算队尾指针,指向队尾索引 + 1\n        // 通过取余操作实现 rear 越过数组尾部后回到头部\n        let rear = (self.front + self.que_size) % self.que_capacity;\n        // 将 num 添加至队尾\n        self.nums[rear as usize] = num;\n        self.que_size += 1;\n    }\n\n    /* 出队 */\n    fn pop(&mut self) -> T {\n        let num = self.peek();\n        // 队首指针向后移动一位,若越过尾部,则返回到数组头部\n        self.front = (self.front + 1) % self.que_capacity;\n        self.que_size -= 1;\n        num\n    }\n\n    /* 访问队首元素 */\n    fn peek(&self) -> T {\n        if self.is_empty() {\n            panic!(\"index out of bounds\");\n        }\n        self.nums[self.front as usize]\n    }\n\n    /* 返回数组 */\n    fn to_vector(&self) -> Vec<T> {\n        let cap = self.que_capacity;\n        let mut j = self.front;\n        let mut arr = vec![T::default(); cap as usize];\n        for i in 0..self.que_size {\n            arr[i as usize] = self.nums[(j % cap) as usize];\n            j += 1;\n        }\n        arr\n    }\n}\n
array_queue.c
/* 基于环形数组实现的队列 */\ntypedef struct {\n    int *nums;       // 用于存储队列元素的数组\n    int front;       // 队首指针,指向队首元素\n    int queSize;     // 当前队列的元素数量\n    int queCapacity; // 队列容量\n} ArrayQueue;\n\n/* 构造函数 */\nArrayQueue *newArrayQueue(int capacity) {\n    ArrayQueue *queue = (ArrayQueue *)malloc(sizeof(ArrayQueue));\n    // 初始化数组\n    queue->queCapacity = capacity;\n    queue->nums = (int *)malloc(sizeof(int) * queue->queCapacity);\n    queue->front = queue->queSize = 0;\n    return queue;\n}\n\n/* 析构函数 */\nvoid delArrayQueue(ArrayQueue *queue) {\n    free(queue->nums);\n    free(queue);\n}\n\n/* 获取队列的容量 */\nint capacity(ArrayQueue *queue) {\n    return queue->queCapacity;\n}\n\n/* 获取队列的长度 */\nint size(ArrayQueue *queue) {\n    return queue->queSize;\n}\n\n/* 判断队列是否为空 */\nbool empty(ArrayQueue *queue) {\n    return queue->queSize == 0;\n}\n\n/* 访问队首元素 */\nint peek(ArrayQueue *queue) {\n    assert(size(queue) != 0);\n    return queue->nums[queue->front];\n}\n\n/* 入队 */\nvoid push(ArrayQueue *queue, int num) {\n    if (size(queue) == capacity(queue)) {\n        printf(\"队列已满\\r\\n\");\n        return;\n    }\n    // 计算队尾指针,指向队尾索引 + 1\n    // 通过取余操作实现 rear 越过数组尾部后回到头部\n    int rear = (queue->front + queue->queSize) % queue->queCapacity;\n    // 将 num 添加至队尾\n    queue->nums[rear] = num;\n    queue->queSize++;\n}\n\n/* 出队 */\nint pop(ArrayQueue *queue) {\n    int num = peek(queue);\n    // 队首指针向后移动一位,若越过尾部,则返回到数组头部\n    queue->front = (queue->front + 1) % queue->queCapacity;\n    queue->queSize--;\n    return num;\n}\n\n/* 返回数组用于打印 */\nint *toArray(ArrayQueue *queue, int *queSize) {\n    *queSize = queue->queSize;\n    int *res = (int *)calloc(queue->queSize, sizeof(int));\n    int j = queue->front;\n    for (int i = 0; i < queue->queSize; i++) {\n        res[i] = queue->nums[j % queue->queCapacity];\n        j++;\n    }\n    return res;\n}\n
array_queue.kt
/* 基于环形数组实现的队列 */\nclass ArrayQueue(capacity: Int) {\n    private val nums: IntArray = IntArray(capacity) // 用于存储队列元素的数组\n    private var front: Int = 0 // 队首指针,指向队首元素\n    private var queSize: Int = 0 // 队列长度\n\n    /* 获取队列的容量 */\n    fun capacity(): Int {\n        return nums.size\n    }\n\n    /* 获取队列的长度 */\n    fun size(): Int {\n        return queSize\n    }\n\n    /* 判断队列是否为空 */\n    fun isEmpty(): Boolean {\n        return queSize == 0\n    }\n\n    /* 入队 */\n    fun push(num: Int) {\n        if (queSize == capacity()) {\n            println(\"队列已满\")\n            return\n        }\n        // 计算队尾指针,指向队尾索引 + 1\n        // 通过取余操作实现 rear 越过数组尾部后回到头部\n        val rear = (front + queSize) % capacity()\n        // 将 num 添加至队尾\n        nums[rear] = num\n        queSize++\n    }\n\n    /* 出队 */\n    fun pop(): Int {\n        val num = peek()\n        // 队首指针向后移动一位,若越过尾部,则返回到数组头部\n        front = (front + 1) % capacity()\n        queSize--\n        return num\n    }\n\n    /* 访问队首元素 */\n    fun peek(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return nums[front]\n    }\n\n    /* 返回数组 */\n    fun toArray(): IntArray {\n        // 仅转换有效长度范围内的列表元素\n        val res = IntArray(queSize)\n        var i = 0\n        var j = front\n        while (i < queSize) {\n            res[i] = nums[j % capacity()]\n            i++\n            j++\n        }\n        return res\n    }\n}\n
array_queue.rb
### 基于环形数组实现的队列 ###\nclass ArrayQueue\n  ### 获取队列的长度 ###\n  attr_reader :size\n\n  ### 构造方法 ###\n  def initialize(size)\n    @nums = Array.new(size, 0) # 用于存储队列元素的数组\n    @front = 0 # 队首指针,指向队首元素\n    @size = 0 # 队列长度\n  end\n\n  ### 获取队列的容量 ###\n  def capacity\n    @nums.length\n  end\n\n  ### 判断队列是否为空 ###\n  def is_empty?\n    size.zero?\n  end\n\n  ### 入队 ###\n  def push(num)\n    raise IndexError, '队列已满' if size == capacity\n\n    # 计算队尾指针,指向队尾索引 + 1\n    # 通过取余操作实现 rear 越过数组尾部后回到头部\n    rear = (@front + size) % capacity\n    # 将 num 添加至队尾\n    @nums[rear] = num\n    @size += 1\n  end\n\n  ### 出队 ###\n  def pop\n    num = peek\n    # 队首指针向后移动一位,若越过尾部,则返回到数组头部\n    @front = (@front + 1) % capacity\n    @size -= 1\n    num\n  end\n\n  ### 访问队首元素 ###\n  def peek\n    raise IndexError, '队列为空' if is_empty?\n\n    @nums[@front]\n  end\n\n  ### 返回列表用于打印 ###\n  def to_array\n    res = Array.new(size, 0)\n    j = @front\n\n    for i in 0...size\n      res[i] = @nums[j % capacity]\n      j += 1\n    end\n\n    res\n  end\nend\n
可视化运行

全屏观看 >

以上实现的队列仍然具有局限性:其长度不可变。然而,这个问题不难解决,我们可以将数组替换为动态数组,从而引入扩容机制。有兴趣的读者可以尝试自行实现。

两种实现的对比结论与栈一致,在此不再赘述。

","path":["第 5 章   栈与队列","5.2   队列"],"tags":[]},{"location":"chapter_stack_and_queue/queue/#523","level":2,"title":"5.2.3   队列典型应用","text":"","path":["第 5 章   栈与队列","5.2   队列"],"tags":[]},{"location":"chapter_stack_and_queue/stack/","level":1,"title":"5.1   栈","text":"

栈(stack)是一种遵循先入后出逻辑的线性数据结构。

我们可以将栈类比为桌面上的一摞盘子,规定每次只能移动一个盘子,那么想取出底部的盘子,则需要先将上面的盘子依次移走。我们将盘子替换为各种类型的元素(如整数、字符、对象等),就得到了栈这种数据结构。

如图 5-1 所示,我们把堆叠元素的顶部称为“栈顶”,底部称为“栈底”。将把元素添加到栈顶的操作叫作“入栈”,删除栈顶元素的操作叫作“出栈”。

图 5-1   栈的先入后出规则

","path":["第 5 章   栈与队列","5.1   栈"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#511","level":2,"title":"5.1.1   栈的常用操作","text":"

栈的常用操作如表 5-1 所示,具体的方法名需要根据所使用的编程语言来确定。在此,我们以常见的 push()pop()peek() 命名为例。

表 5-1   栈的操作效率

方法 描述 时间复杂度 push() 元素入栈(添加至栈顶) \\(O(1)\\) pop() 栈顶元素出栈 \\(O(1)\\) peek() 访问栈顶元素 \\(O(1)\\)

通常情况下,我们可以直接使用编程语言内置的栈类。然而,某些语言可能没有专门提供栈类,这时我们可以将该语言的“数组”或“链表”当作栈来使用,并在程序逻辑上忽略与栈无关的操作。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby stack.py
# 初始化栈\n# Python 没有内置的栈类,可以把 list 当作栈来使用\nstack: list[int] = []\n\n# 元素入栈\nstack.append(1)\nstack.append(3)\nstack.append(2)\nstack.append(5)\nstack.append(4)\n\n# 访问栈顶元素\npeek: int = stack[-1]\n\n# 元素出栈\npop: int = stack.pop()\n\n# 获取栈的长度\nsize: int = len(stack)\n\n# 判断是否为空\nis_empty: bool = len(stack) == 0\n
stack.cpp
/* 初始化栈 */\nstack<int> stack;\n\n/* 元素入栈 */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* 访问栈顶元素 */\nint top = stack.top();\n\n/* 元素出栈 */\nstack.pop(); // 无返回值\n\n/* 获取栈的长度 */\nint size = stack.size();\n\n/* 判断是否为空 */\nbool empty = stack.empty();\n
stack.java
/* 初始化栈 */\nStack<Integer> stack = new Stack<>();\n\n/* 元素入栈 */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* 访问栈顶元素 */\nint peek = stack.peek();\n\n/* 元素出栈 */\nint pop = stack.pop();\n\n/* 获取栈的长度 */\nint size = stack.size();\n\n/* 判断是否为空 */\nboolean isEmpty = stack.isEmpty();\n
stack.cs
/* 初始化栈 */\nStack<int> stack = new();\n\n/* 元素入栈 */\nstack.Push(1);\nstack.Push(3);\nstack.Push(2);\nstack.Push(5);\nstack.Push(4);\n\n/* 访问栈顶元素 */\nint peek = stack.Peek();\n\n/* 元素出栈 */\nint pop = stack.Pop();\n\n/* 获取栈的长度 */\nint size = stack.Count;\n\n/* 判断是否为空 */\nbool isEmpty = stack.Count == 0;\n
stack_test.go
/* 初始化栈 */\n// 在 Go 中,推荐将 Slice 当作栈来使用\nvar stack []int\n\n/* 元素入栈 */\nstack = append(stack, 1)\nstack = append(stack, 3)\nstack = append(stack, 2)\nstack = append(stack, 5)\nstack = append(stack, 4)\n\n/* 访问栈顶元素 */\npeek := stack[len(stack)-1]\n\n/* 元素出栈 */\npop := stack[len(stack)-1]\nstack = stack[:len(stack)-1]\n\n/* 获取栈的长度 */\nsize := len(stack)\n\n/* 判断是否为空 */\nisEmpty := len(stack) == 0\n
stack.swift
/* 初始化栈 */\n// Swift 没有内置的栈类,可以把 Array 当作栈来使用\nvar stack: [Int] = []\n\n/* 元素入栈 */\nstack.append(1)\nstack.append(3)\nstack.append(2)\nstack.append(5)\nstack.append(4)\n\n/* 访问栈顶元素 */\nlet peek = stack.last!\n\n/* 元素出栈 */\nlet pop = stack.removeLast()\n\n/* 获取栈的长度 */\nlet size = stack.count\n\n/* 判断是否为空 */\nlet isEmpty = stack.isEmpty\n
stack.js
/* 初始化栈 */\n// JavaScript 没有内置的栈类,可以把 Array 当作栈来使用\nconst stack = [];\n\n/* 元素入栈 */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* 访问栈顶元素 */\nconst peek = stack[stack.length-1];\n\n/* 元素出栈 */\nconst pop = stack.pop();\n\n/* 获取栈的长度 */\nconst size = stack.length;\n\n/* 判断是否为空 */\nconst is_empty = stack.length === 0;\n
stack.ts
/* 初始化栈 */\n// TypeScript 没有内置的栈类,可以把 Array 当作栈来使用\nconst stack: number[] = [];\n\n/* 元素入栈 */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* 访问栈顶元素 */\nconst peek = stack[stack.length - 1];\n\n/* 元素出栈 */\nconst pop = stack.pop();\n\n/* 获取栈的长度 */\nconst size = stack.length;\n\n/* 判断是否为空 */\nconst is_empty = stack.length === 0;\n
stack.dart
/* 初始化栈 */\n// Dart 没有内置的栈类,可以把 List 当作栈来使用\nList<int> stack = [];\n\n/* 元素入栈 */\nstack.add(1);\nstack.add(3);\nstack.add(2);\nstack.add(5);\nstack.add(4);\n\n/* 访问栈顶元素 */\nint peek = stack.last;\n\n/* 元素出栈 */\nint pop = stack.removeLast();\n\n/* 获取栈的长度 */\nint size = stack.length;\n\n/* 判断是否为空 */\nbool isEmpty = stack.isEmpty;\n
stack.rs
/* 初始化栈 */\n// 把 Vec 当作栈来使用\nlet mut stack: Vec<i32> = Vec::new();\n\n/* 元素入栈 */\nstack.push(1);\nstack.push(3);\nstack.push(2);\nstack.push(5);\nstack.push(4);\n\n/* 访问栈顶元素 */\nlet top = stack.last().unwrap();\n\n/* 元素出栈 */\nlet pop = stack.pop().unwrap();\n\n/* 获取栈的长度 */\nlet size = stack.len();\n\n/* 判断是否为空 */\nlet is_empty = stack.is_empty();\n
stack.c
// C 未提供内置栈\n
stack.kt
/* 初始化栈 */\nval stack = Stack<Int>()\n\n/* 元素入栈 */\nstack.push(1)\nstack.push(3)\nstack.push(2)\nstack.push(5)\nstack.push(4)\n\n/* 访问栈顶元素 */\nval peek = stack.peek()\n\n/* 元素出栈 */\nval pop = stack.pop()\n\n/* 获取栈的长度 */\nval size = stack.size\n\n/* 判断是否为空 */\nval isEmpty = stack.isEmpty()\n
stack.rb
# 初始化栈\n# Ruby 没有内置的栈类,可以把 Array 当作栈来使用\nstack = []\n\n# 元素入栈\nstack << 1\nstack << 3\nstack << 2\nstack << 5\nstack << 4\n\n# 访问栈顶元素\npeek = stack.last\n\n# 元素出栈\npop = stack.pop\n\n# 获取栈的长度\nsize = stack.length\n\n# 判断是否为空\nis_empty = stack.empty?\n
可视化运行

全屏观看 >

","path":["第 5 章   栈与队列","5.1   栈"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#512","level":2,"title":"5.1.2   栈的实现","text":"

为了深入了解栈的运行机制,我们来尝试自己实现一个栈类。

栈遵循先入后出的原则,因此我们只能在栈顶添加或删除元素。然而,数组和链表都可以在任意位置添加和删除元素,因此栈可以视为一种受限制的数组或链表。换句话说,我们可以“屏蔽”数组或链表的部分无关操作,使其对外表现的逻辑符合栈的特性。

","path":["第 5 章   栈与队列","5.1   栈"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#1","level":3,"title":"1.   基于链表的实现","text":"

使用链表实现栈时,我们可以将链表的头节点视为栈顶,尾节点视为栈底。

如图 5-2 所示,对于入栈操作,我们只需将元素插入链表头部,这种节点插入方法被称为“头插法”。而对于出栈操作,只需将头节点从链表中删除即可。

<1><2><3>

图 5-2   基于链表实现栈的入栈出栈操作

以下是基于链表实现栈的示例代码:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby linkedlist_stack.py
class LinkedListStack:\n    \"\"\"基于链表实现的栈\"\"\"\n\n    def __init__(self):\n        \"\"\"构造方法\"\"\"\n        self._peek: ListNode | None = None\n        self._size: int = 0\n\n    def size(self) -> int:\n        \"\"\"获取栈的长度\"\"\"\n        return self._size\n\n    def is_empty(self) -> bool:\n        \"\"\"判断栈是否为空\"\"\"\n        return self._size == 0\n\n    def push(self, val: int):\n        \"\"\"入栈\"\"\"\n        node = ListNode(val)\n        node.next = self._peek\n        self._peek = node\n        self._size += 1\n\n    def pop(self) -> int:\n        \"\"\"出栈\"\"\"\n        num = self.peek()\n        self._peek = self._peek.next\n        self._size -= 1\n        return num\n\n    def peek(self) -> int:\n        \"\"\"访问栈顶元素\"\"\"\n        if self.is_empty():\n            raise IndexError(\"栈为空\")\n        return self._peek.val\n\n    def to_list(self) -> list[int]:\n        \"\"\"转化为列表用于打印\"\"\"\n        arr = []\n        node = self._peek\n        while node:\n            arr.append(node.val)\n            node = node.next\n        arr.reverse()\n        return arr\n
linkedlist_stack.cpp
/* 基于链表实现的栈 */\nclass LinkedListStack {\n  private:\n    ListNode *stackTop; // 将头节点作为栈顶\n    int stkSize;        // 栈的长度\n\n  public:\n    LinkedListStack() {\n        stackTop = nullptr;\n        stkSize = 0;\n    }\n\n    ~LinkedListStack() {\n        // 遍历链表删除节点,释放内存\n        freeMemoryLinkedList(stackTop);\n    }\n\n    /* 获取栈的长度 */\n    int size() {\n        return stkSize;\n    }\n\n    /* 判断栈是否为空 */\n    bool isEmpty() {\n        return size() == 0;\n    }\n\n    /* 入栈 */\n    void push(int num) {\n        ListNode *node = new ListNode(num);\n        node->next = stackTop;\n        stackTop = node;\n        stkSize++;\n    }\n\n    /* 出栈 */\n    int pop() {\n        int num = top();\n        ListNode *tmp = stackTop;\n        stackTop = stackTop->next;\n        // 释放内存\n        delete tmp;\n        stkSize--;\n        return num;\n    }\n\n    /* 访问栈顶元素 */\n    int top() {\n        if (isEmpty())\n            throw out_of_range(\"栈为空\");\n        return stackTop->val;\n    }\n\n    /* 将 List 转化为 Array 并返回 */\n    vector<int> toVector() {\n        ListNode *node = stackTop;\n        vector<int> res(size());\n        for (int i = res.size() - 1; i >= 0; i--) {\n            res[i] = node->val;\n            node = node->next;\n        }\n        return res;\n    }\n};\n
linkedlist_stack.java
/* 基于链表实现的栈 */\nclass LinkedListStack {\n    private ListNode stackPeek; // 将头节点作为栈顶\n    private int stkSize = 0; // 栈的长度\n\n    public LinkedListStack() {\n        stackPeek = null;\n    }\n\n    /* 获取栈的长度 */\n    public int size() {\n        return stkSize;\n    }\n\n    /* 判断栈是否为空 */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* 入栈 */\n    public void push(int num) {\n        ListNode node = new ListNode(num);\n        node.next = stackPeek;\n        stackPeek = node;\n        stkSize++;\n    }\n\n    /* 出栈 */\n    public int pop() {\n        int num = peek();\n        stackPeek = stackPeek.next;\n        stkSize--;\n        return num;\n    }\n\n    /* 访问栈顶元素 */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return stackPeek.val;\n    }\n\n    /* 将 List 转化为 Array 并返回 */\n    public int[] toArray() {\n        ListNode node = stackPeek;\n        int[] res = new int[size()];\n        for (int i = res.length - 1; i >= 0; i--) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_stack.cs
/* 基于链表实现的栈 */\nclass LinkedListStack {\n    ListNode? stackPeek;  // 将头节点作为栈顶\n    int stkSize = 0;   // 栈的长度\n\n    public LinkedListStack() {\n        stackPeek = null;\n    }\n\n    /* 获取栈的长度 */\n    public int Size() {\n        return stkSize;\n    }\n\n    /* 判断栈是否为空 */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* 入栈 */\n    public void Push(int num) {\n        ListNode node = new(num) {\n            next = stackPeek\n        };\n        stackPeek = node;\n        stkSize++;\n    }\n\n    /* 出栈 */\n    public int Pop() {\n        int num = Peek();\n        stackPeek = stackPeek!.next;\n        stkSize--;\n        return num;\n    }\n\n    /* 访问栈顶元素 */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return stackPeek!.val;\n    }\n\n    /* 将 List 转化为 Array 并返回 */\n    public int[] ToArray() {\n        if (stackPeek == null)\n            return [];\n\n        ListNode? node = stackPeek;\n        int[] res = new int[Size()];\n        for (int i = res.Length - 1; i >= 0; i--) {\n            res[i] = node!.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_stack.go
/* 基于链表实现的栈 */\ntype linkedListStack struct {\n    // 使用内置包 list 来实现栈\n    data *list.List\n}\n\n/* 初始化栈 */\nfunc newLinkedListStack() *linkedListStack {\n    return &linkedListStack{\n        data: list.New(),\n    }\n}\n\n/* 入栈 */\nfunc (s *linkedListStack) push(value int) {\n    s.data.PushBack(value)\n}\n\n/* 出栈 */\nfunc (s *linkedListStack) pop() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Back()\n    s.data.Remove(e)\n    return e.Value\n}\n\n/* 访问栈顶元素 */\nfunc (s *linkedListStack) peek() any {\n    if s.isEmpty() {\n        return nil\n    }\n    e := s.data.Back()\n    return e.Value\n}\n\n/* 获取栈的长度 */\nfunc (s *linkedListStack) size() int {\n    return s.data.Len()\n}\n\n/* 判断栈是否为空 */\nfunc (s *linkedListStack) isEmpty() bool {\n    return s.data.Len() == 0\n}\n\n/* 获取 List 用于打印 */\nfunc (s *linkedListStack) toList() *list.List {\n    return s.data\n}\n
linkedlist_stack.swift
/* 基于链表实现的栈 */\nclass LinkedListStack {\n    private var _peek: ListNode? // 将头节点作为栈顶\n    private var _size: Int // 栈的长度\n\n    init() {\n        _size = 0\n    }\n\n    /* 获取栈的长度 */\n    func size() -> Int {\n        _size\n    }\n\n    /* 判断栈是否为空 */\n    func isEmpty() -> Bool {\n        size() == 0\n    }\n\n    /* 入栈 */\n    func push(num: Int) {\n        let node = ListNode(x: num)\n        node.next = _peek\n        _peek = node\n        _size += 1\n    }\n\n    /* 出栈 */\n    @discardableResult\n    func pop() -> Int {\n        let num = peek()\n        _peek = _peek?.next\n        _size -= 1\n        return num\n    }\n\n    /* 访问栈顶元素 */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"栈为空\")\n        }\n        return _peek!.val\n    }\n\n    /* 将 List 转化为 Array 并返回 */\n    func toArray() -> [Int] {\n        var node = _peek\n        var res = Array(repeating: 0, count: size())\n        for i in res.indices.reversed() {\n            res[i] = node!.val\n            node = node?.next\n        }\n        return res\n    }\n}\n
linkedlist_stack.js
/* 基于链表实现的栈 */\nclass LinkedListStack {\n    #stackPeek; // 将头节点作为栈顶\n    #stkSize = 0; // 栈的长度\n\n    constructor() {\n        this.#stackPeek = null;\n    }\n\n    /* 获取栈的长度 */\n    get size() {\n        return this.#stkSize;\n    }\n\n    /* 判断栈是否为空 */\n    isEmpty() {\n        return this.size === 0;\n    }\n\n    /* 入栈 */\n    push(num) {\n        const node = new ListNode(num);\n        node.next = this.#stackPeek;\n        this.#stackPeek = node;\n        this.#stkSize++;\n    }\n\n    /* 出栈 */\n    pop() {\n        const num = this.peek();\n        this.#stackPeek = this.#stackPeek.next;\n        this.#stkSize--;\n        return num;\n    }\n\n    /* 访问栈顶元素 */\n    peek() {\n        if (!this.#stackPeek) throw new Error('栈为空');\n        return this.#stackPeek.val;\n    }\n\n    /* 将链表转化为 Array 并返回 */\n    toArray() {\n        let node = this.#stackPeek;\n        const res = new Array(this.size);\n        for (let i = res.length - 1; i >= 0; i--) {\n            res[i] = node.val;\n            node = node.next;\n        }\n        return res;\n    }\n}\n
linkedlist_stack.ts
/* 基于链表实现的栈 */\nclass LinkedListStack {\n    private stackPeek: ListNode | null; // 将头节点作为栈顶\n    private stkSize: number = 0; // 栈的长度\n\n    constructor() {\n        this.stackPeek = null;\n    }\n\n    /* 获取栈的长度 */\n    get size(): number {\n        return this.stkSize;\n    }\n\n    /* 判断栈是否为空 */\n    isEmpty(): boolean {\n        return this.size === 0;\n    }\n\n    /* 入栈 */\n    push(num: number): void {\n        const node = new ListNode(num);\n        node.next = this.stackPeek;\n        this.stackPeek = node;\n        this.stkSize++;\n    }\n\n    /* 出栈 */\n    pop(): number {\n        const num = this.peek();\n        if (!this.stackPeek) throw new Error('栈为空');\n        this.stackPeek = this.stackPeek.next;\n        this.stkSize--;\n        return num;\n    }\n\n    /* 访问栈顶元素 */\n    peek(): number {\n        if (!this.stackPeek) throw new Error('栈为空');\n        return this.stackPeek.val;\n    }\n\n    /* 将链表转化为 Array 并返回 */\n    toArray(): number[] {\n        let node = this.stackPeek;\n        const res = new Array<number>(this.size);\n        for (let i = res.length - 1; i >= 0; i--) {\n            res[i] = node!.val;\n            node = node!.next;\n        }\n        return res;\n    }\n}\n
linkedlist_stack.dart
/* 基于链表类实现的栈 */\nclass LinkedListStack {\n  ListNode? _stackPeek; // 将头节点作为栈顶\n  int _stkSize = 0; // 栈的长度\n\n  LinkedListStack() {\n    _stackPeek = null;\n  }\n\n  /* 获取栈的长度 */\n  int size() {\n    return _stkSize;\n  }\n\n  /* 判断栈是否为空 */\n  bool isEmpty() {\n    return _stkSize == 0;\n  }\n\n  /* 入栈 */\n  void push(int _num) {\n    final ListNode node = ListNode(_num);\n    node.next = _stackPeek;\n    _stackPeek = node;\n    _stkSize++;\n  }\n\n  /* 出栈 */\n  int pop() {\n    final int _num = peek();\n    _stackPeek = _stackPeek!.next;\n    _stkSize--;\n    return _num;\n  }\n\n  /* 访问栈顶元素 */\n  int peek() {\n    if (_stackPeek == null) {\n      throw Exception(\"栈为空\");\n    }\n    return _stackPeek!.val;\n  }\n\n  /* 将链表转化为 List 并返回 */\n  List<int> toList() {\n    ListNode? node = _stackPeek;\n    List<int> list = [];\n    while (node != null) {\n      list.add(node.val);\n      node = node.next;\n    }\n    list = list.reversed.toList();\n    return list;\n  }\n}\n
linkedlist_stack.rs
/* 基于链表实现的栈 */\n#[allow(dead_code)]\npub struct LinkedListStack<T> {\n    stack_peek: Option<Rc<RefCell<ListNode<T>>>>, // 将头节点作为栈顶\n    stk_size: usize,                              // 栈的长度\n}\n\nimpl<T: Copy> LinkedListStack<T> {\n    pub fn new() -> Self {\n        Self {\n            stack_peek: None,\n            stk_size: 0,\n        }\n    }\n\n    /* 获取栈的长度 */\n    pub fn size(&self) -> usize {\n        return self.stk_size;\n    }\n\n    /* 判断栈是否为空 */\n    pub fn is_empty(&self) -> bool {\n        return self.size() == 0;\n    }\n\n    /* 入栈 */\n    pub fn push(&mut self, num: T) {\n        let node = ListNode::new(num);\n        node.borrow_mut().next = self.stack_peek.take();\n        self.stack_peek = Some(node);\n        self.stk_size += 1;\n    }\n\n    /* 出栈 */\n    pub fn pop(&mut self) -> Option<T> {\n        self.stack_peek.take().map(|old_head| {\n            self.stack_peek = old_head.borrow_mut().next.take();\n            self.stk_size -= 1;\n\n            old_head.borrow().val\n        })\n    }\n\n    /* 访问栈顶元素 */\n    pub fn peek(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {\n        self.stack_peek.as_ref()\n    }\n\n    /* 将 List 转化为 Array 并返回 */\n    pub fn to_array(&self) -> Vec<T> {\n        fn _to_array<T: Sized + Copy>(head: Option<&Rc<RefCell<ListNode<T>>>>) -> Vec<T> {\n            if let Some(node) = head {\n                let mut nums = _to_array(node.borrow().next.as_ref());\n                nums.push(node.borrow().val);\n                return nums;\n            }\n            return Vec::new();\n        }\n\n        _to_array(self.peek())\n    }\n}\n
linkedlist_stack.c
/* 基于链表实现的栈 */\ntypedef struct {\n    ListNode *top; // 将头节点作为栈顶\n    int size;      // 栈的长度\n} LinkedListStack;\n\n/* 构造函数 */\nLinkedListStack *newLinkedListStack() {\n    LinkedListStack *s = malloc(sizeof(LinkedListStack));\n    s->top = NULL;\n    s->size = 0;\n    return s;\n}\n\n/* 析构函数 */\nvoid delLinkedListStack(LinkedListStack *s) {\n    while (s->top) {\n        ListNode *n = s->top->next;\n        free(s->top);\n        s->top = n;\n    }\n    free(s);\n}\n\n/* 获取栈的长度 */\nint size(LinkedListStack *s) {\n    return s->size;\n}\n\n/* 判断栈是否为空 */\nbool isEmpty(LinkedListStack *s) {\n    return size(s) == 0;\n}\n\n/* 入栈 */\nvoid push(LinkedListStack *s, int num) {\n    ListNode *node = (ListNode *)malloc(sizeof(ListNode));\n    node->next = s->top; // 更新新加节点指针域\n    node->val = num;     // 更新新加节点数据域\n    s->top = node;       // 更新栈顶\n    s->size++;           // 更新栈大小\n}\n\n/* 访问栈顶元素 */\nint peek(LinkedListStack *s) {\n    if (s->size == 0) {\n        printf(\"栈为空\\n\");\n        return INT_MAX;\n    }\n    return s->top->val;\n}\n\n/* 出栈 */\nint pop(LinkedListStack *s) {\n    int val = peek(s);\n    ListNode *tmp = s->top;\n    s->top = s->top->next;\n    // 释放内存\n    free(tmp);\n    s->size--;\n    return val;\n}\n
linkedlist_stack.kt
/* 基于链表实现的栈 */\nclass LinkedListStack(\n    private var stackPeek: ListNode? = null, // 将头节点作为栈顶\n    private var stkSize: Int = 0 // 栈的长度\n) {\n\n    /* 获取栈的长度 */\n    fun size(): Int {\n        return stkSize\n    }\n\n    /* 判断栈是否为空 */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* 入栈 */\n    fun push(num: Int) {\n        val node = ListNode(num)\n        node.next = stackPeek\n        stackPeek = node\n        stkSize++\n    }\n\n    /* 出栈 */\n    fun pop(): Int? {\n        val num = peek()\n        stackPeek = stackPeek?.next\n        stkSize--\n        return num\n    }\n\n    /* 访问栈顶元素 */\n    fun peek(): Int? {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return stackPeek?._val\n    }\n\n    /* 将 List 转化为 Array 并返回 */\n    fun toArray(): IntArray {\n        var node = stackPeek\n        val res = IntArray(size())\n        for (i in res.size - 1 downTo 0) {\n            res[i] = node?._val!!\n            node = node.next\n        }\n        return res\n    }\n}\n
linkedlist_stack.rb
### 基于链表实现的栈 ###\nclass LinkedListStack\n  attr_reader :size\n\n  ### 构造方法 ###\n  def initialize\n    @size = 0\n  end\n\n  ### 判断栈是否为空 ###\n  def is_empty?\n    @peek.nil?\n  end\n\n  ### 入栈 ###\n  def push(val)\n    node = ListNode.new(val)\n    node.next = @peek\n    @peek = node\n    @size += 1\n  end\n\n  ### 出栈 ###\n  def pop\n    num = peek\n    @peek = @peek.next\n    @size -= 1\n    num\n  end\n\n  ### 访问栈顶元素 ###\n  def peek\n    raise IndexError, '栈为空' if is_empty?\n\n    @peek.val\n  end\n\n  ### 将链表转化为 Array 并反回 ###\n  def to_array\n    arr = []\n    node = @peek\n    while node\n      arr << node.val\n      node = node.next\n    end\n    arr.reverse\n  end\nend\n
可视化运行

全屏观看 >

","path":["第 5 章   栈与队列","5.1   栈"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#2","level":3,"title":"2.   基于数组的实现","text":"

使用数组实现栈时,我们可以将数组的尾部作为栈顶。如图 5-3 所示,入栈与出栈操作分别对应在数组尾部添加元素与删除元素,时间复杂度都为 \\(O(1)\\) 。

<1><2><3>

图 5-3   基于数组实现栈的入栈出栈操作

由于入栈的元素可能会源源不断地增加,因此我们可以使用动态数组,这样就无须自行处理数组扩容问题。以下为示例代码:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_stack.py
class ArrayStack:\n    \"\"\"基于数组实现的栈\"\"\"\n\n    def __init__(self):\n        \"\"\"构造方法\"\"\"\n        self._stack: list[int] = []\n\n    def size(self) -> int:\n        \"\"\"获取栈的长度\"\"\"\n        return len(self._stack)\n\n    def is_empty(self) -> bool:\n        \"\"\"判断栈是否为空\"\"\"\n        return self.size() == 0\n\n    def push(self, item: int):\n        \"\"\"入栈\"\"\"\n        self._stack.append(item)\n\n    def pop(self) -> int:\n        \"\"\"出栈\"\"\"\n        if self.is_empty():\n            raise IndexError(\"栈为空\")\n        return self._stack.pop()\n\n    def peek(self) -> int:\n        \"\"\"访问栈顶元素\"\"\"\n        if self.is_empty():\n            raise IndexError(\"栈为空\")\n        return self._stack[-1]\n\n    def to_list(self) -> list[int]:\n        \"\"\"返回列表用于打印\"\"\"\n        return self._stack\n
array_stack.cpp
/* 基于数组实现的栈 */\nclass ArrayStack {\n  private:\n    vector<int> stack;\n\n  public:\n    /* 获取栈的长度 */\n    int size() {\n        return stack.size();\n    }\n\n    /* 判断栈是否为空 */\n    bool isEmpty() {\n        return stack.size() == 0;\n    }\n\n    /* 入栈 */\n    void push(int num) {\n        stack.push_back(num);\n    }\n\n    /* 出栈 */\n    int pop() {\n        int num = top();\n        stack.pop_back();\n        return num;\n    }\n\n    /* 访问栈顶元素 */\n    int top() {\n        if (isEmpty())\n            throw out_of_range(\"栈为空\");\n        return stack.back();\n    }\n\n    /* 返回 Vector */\n    vector<int> toVector() {\n        return stack;\n    }\n};\n
array_stack.java
/* 基于数组实现的栈 */\nclass ArrayStack {\n    private ArrayList<Integer> stack;\n\n    public ArrayStack() {\n        // 初始化列表(动态数组)\n        stack = new ArrayList<>();\n    }\n\n    /* 获取栈的长度 */\n    public int size() {\n        return stack.size();\n    }\n\n    /* 判断栈是否为空 */\n    public boolean isEmpty() {\n        return size() == 0;\n    }\n\n    /* 入栈 */\n    public void push(int num) {\n        stack.add(num);\n    }\n\n    /* 出栈 */\n    public int pop() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return stack.remove(size() - 1);\n    }\n\n    /* 访问栈顶元素 */\n    public int peek() {\n        if (isEmpty())\n            throw new IndexOutOfBoundsException();\n        return stack.get(size() - 1);\n    }\n\n    /* 将 List 转化为 Array 并返回 */\n    public Object[] toArray() {\n        return stack.toArray();\n    }\n}\n
array_stack.cs
/* 基于数组实现的栈 */\nclass ArrayStack {\n    List<int> stack;\n    public ArrayStack() {\n        // 初始化列表(动态数组)\n        stack = [];\n    }\n\n    /* 获取栈的长度 */\n    public int Size() {\n        return stack.Count;\n    }\n\n    /* 判断栈是否为空 */\n    public bool IsEmpty() {\n        return Size() == 0;\n    }\n\n    /* 入栈 */\n    public void Push(int num) {\n        stack.Add(num);\n    }\n\n    /* 出栈 */\n    public int Pop() {\n        if (IsEmpty())\n            throw new Exception();\n        var val = Peek();\n        stack.RemoveAt(Size() - 1);\n        return val;\n    }\n\n    /* 访问栈顶元素 */\n    public int Peek() {\n        if (IsEmpty())\n            throw new Exception();\n        return stack[Size() - 1];\n    }\n\n    /* 将 List 转化为 Array 并返回 */\n    public int[] ToArray() {\n        return [.. stack];\n    }\n}\n
array_stack.go
/* 基于数组实现的栈 */\ntype arrayStack struct {\n    data []int // 数据\n}\n\n/* 初始化栈 */\nfunc newArrayStack() *arrayStack {\n    return &arrayStack{\n        // 设置栈的长度为 0,容量为 16\n        data: make([]int, 0, 16),\n    }\n}\n\n/* 栈的长度 */\nfunc (s *arrayStack) size() int {\n    return len(s.data)\n}\n\n/* 栈是否为空 */\nfunc (s *arrayStack) isEmpty() bool {\n    return s.size() == 0\n}\n\n/* 入栈 */\nfunc (s *arrayStack) push(v int) {\n    // 切片会自动扩容\n    s.data = append(s.data, v)\n}\n\n/* 出栈 */\nfunc (s *arrayStack) pop() any {\n    val := s.peek()\n    s.data = s.data[:len(s.data)-1]\n    return val\n}\n\n/* 获取栈顶元素 */\nfunc (s *arrayStack) peek() any {\n    if s.isEmpty() {\n        return nil\n    }\n    val := s.data[len(s.data)-1]\n    return val\n}\n\n/* 获取 Slice 用于打印 */\nfunc (s *arrayStack) toSlice() []int {\n    return s.data\n}\n
array_stack.swift
/* 基于数组实现的栈 */\nclass ArrayStack {\n    private var stack: [Int]\n\n    init() {\n        // 初始化列表(动态数组)\n        stack = []\n    }\n\n    /* 获取栈的长度 */\n    func size() -> Int {\n        stack.count\n    }\n\n    /* 判断栈是否为空 */\n    func isEmpty() -> Bool {\n        stack.isEmpty\n    }\n\n    /* 入栈 */\n    func push(num: Int) {\n        stack.append(num)\n    }\n\n    /* 出栈 */\n    @discardableResult\n    func pop() -> Int {\n        if isEmpty() {\n            fatalError(\"栈为空\")\n        }\n        return stack.removeLast()\n    }\n\n    /* 访问栈顶元素 */\n    func peek() -> Int {\n        if isEmpty() {\n            fatalError(\"栈为空\")\n        }\n        return stack.last!\n    }\n\n    /* 将 List 转化为 Array 并返回 */\n    func toArray() -> [Int] {\n        stack\n    }\n}\n
array_stack.js
/* 基于数组实现的栈 */\nclass ArrayStack {\n    #stack;\n    constructor() {\n        this.#stack = [];\n    }\n\n    /* 获取栈的长度 */\n    get size() {\n        return this.#stack.length;\n    }\n\n    /* 判断栈是否为空 */\n    isEmpty() {\n        return this.#stack.length === 0;\n    }\n\n    /* 入栈 */\n    push(num) {\n        this.#stack.push(num);\n    }\n\n    /* 出栈 */\n    pop() {\n        if (this.isEmpty()) throw new Error('栈为空');\n        return this.#stack.pop();\n    }\n\n    /* 访问栈顶元素 */\n    top() {\n        if (this.isEmpty()) throw new Error('栈为空');\n        return this.#stack[this.#stack.length - 1];\n    }\n\n    /* 返回 Array */\n    toArray() {\n        return this.#stack;\n    }\n}\n
array_stack.ts
/* 基于数组实现的栈 */\nclass ArrayStack {\n    private stack: number[];\n    constructor() {\n        this.stack = [];\n    }\n\n    /* 获取栈的长度 */\n    get size(): number {\n        return this.stack.length;\n    }\n\n    /* 判断栈是否为空 */\n    isEmpty(): boolean {\n        return this.stack.length === 0;\n    }\n\n    /* 入栈 */\n    push(num: number): void {\n        this.stack.push(num);\n    }\n\n    /* 出栈 */\n    pop(): number | undefined {\n        if (this.isEmpty()) throw new Error('栈为空');\n        return this.stack.pop();\n    }\n\n    /* 访问栈顶元素 */\n    top(): number | undefined {\n        if (this.isEmpty()) throw new Error('栈为空');\n        return this.stack[this.stack.length - 1];\n    }\n\n    /* 返回 Array */\n    toArray() {\n        return this.stack;\n    }\n}\n
array_stack.dart
/* 基于数组实现的栈 */\nclass ArrayStack {\n  late List<int> _stack;\n  ArrayStack() {\n    _stack = [];\n  }\n\n  /* 获取栈的长度 */\n  int size() {\n    return _stack.length;\n  }\n\n  /* 判断栈是否为空 */\n  bool isEmpty() {\n    return _stack.isEmpty;\n  }\n\n  /* 入栈 */\n  void push(int _num) {\n    _stack.add(_num);\n  }\n\n  /* 出栈 */\n  int pop() {\n    if (isEmpty()) {\n      throw Exception(\"栈为空\");\n    }\n    return _stack.removeLast();\n  }\n\n  /* 访问栈顶元素 */\n  int peek() {\n    if (isEmpty()) {\n      throw Exception(\"栈为空\");\n    }\n    return _stack.last;\n  }\n\n  /* 将栈转化为 Array 并返回 */\n  List<int> toArray() => _stack;\n}\n
array_stack.rs
/* 基于数组实现的栈 */\nstruct ArrayStack<T> {\n    stack: Vec<T>,\n}\n\nimpl<T> ArrayStack<T> {\n    /* 初始化栈 */\n    fn new() -> ArrayStack<T> {\n        ArrayStack::<T> {\n            stack: Vec::<T>::new(),\n        }\n    }\n\n    /* 获取栈的长度 */\n    fn size(&self) -> usize {\n        self.stack.len()\n    }\n\n    /* 判断栈是否为空 */\n    fn is_empty(&self) -> bool {\n        self.size() == 0\n    }\n\n    /* 入栈 */\n    fn push(&mut self, num: T) {\n        self.stack.push(num);\n    }\n\n    /* 出栈 */\n    fn pop(&mut self) -> Option<T> {\n        self.stack.pop()\n    }\n\n    /* 访问栈顶元素 */\n    fn peek(&self) -> Option<&T> {\n        if self.is_empty() {\n            panic!(\"栈为空\")\n        };\n        self.stack.last()\n    }\n\n    /* 返回 &Vec */\n    fn to_array(&self) -> &Vec<T> {\n        &self.stack\n    }\n}\n
array_stack.c
/* 基于数组实现的栈 */\ntypedef struct {\n    int *data;\n    int size;\n} ArrayStack;\n\n/* 构造函数 */\nArrayStack *newArrayStack() {\n    ArrayStack *stack = malloc(sizeof(ArrayStack));\n    // 初始化一个大容量,避免扩容\n    stack->data = malloc(sizeof(int) * MAX_SIZE);\n    stack->size = 0;\n    return stack;\n}\n\n/* 析构函数 */\nvoid delArrayStack(ArrayStack *stack) {\n    free(stack->data);\n    free(stack);\n}\n\n/* 获取栈的长度 */\nint size(ArrayStack *stack) {\n    return stack->size;\n}\n\n/* 判断栈是否为空 */\nbool isEmpty(ArrayStack *stack) {\n    return stack->size == 0;\n}\n\n/* 入栈 */\nvoid push(ArrayStack *stack, int num) {\n    if (stack->size == MAX_SIZE) {\n        printf(\"栈已满\\n\");\n        return;\n    }\n    stack->data[stack->size] = num;\n    stack->size++;\n}\n\n/* 访问栈顶元素 */\nint peek(ArrayStack *stack) {\n    if (stack->size == 0) {\n        printf(\"栈为空\\n\");\n        return INT_MAX;\n    }\n    return stack->data[stack->size - 1];\n}\n\n/* 出栈 */\nint pop(ArrayStack *stack) {\n    int val = peek(stack);\n    stack->size--;\n    return val;\n}\n
array_stack.kt
/* 基于数组实现的栈 */\nclass ArrayStack {\n    // 初始化列表(动态数组)\n    private val stack = mutableListOf<Int>()\n\n    /* 获取栈的长度 */\n    fun size(): Int {\n        return stack.size\n    }\n\n    /* 判断栈是否为空 */\n    fun isEmpty(): Boolean {\n        return size() == 0\n    }\n\n    /* 入栈 */\n    fun push(num: Int) {\n        stack.add(num)\n    }\n\n    /* 出栈 */\n    fun pop(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return stack.removeAt(size() - 1)\n    }\n\n    /* 访问栈顶元素 */\n    fun peek(): Int {\n        if (isEmpty()) throw IndexOutOfBoundsException()\n        return stack[size() - 1]\n    }\n\n    /* 将 List 转化为 Array 并返回 */\n    fun toArray(): Array<Any> {\n        return stack.toTypedArray()\n    }\n}\n
array_stack.rb
### 基于数组实现的栈 ###\nclass ArrayStack\n  ### 构造方法 ###\n  def initialize\n    @stack = []\n  end\n\n  ### 获取栈的长度 ###\n  def size\n    @stack.length\n  end\n\n  ### 判断栈是否为空 ###\n  def is_empty?\n    @stack.empty?\n  end\n\n  ### 入栈 ###\n  def push(item)\n    @stack << item\n  end\n\n  ### 出栈 ###\n  def pop\n    raise IndexError, '栈为空' if is_empty?\n\n    @stack.pop\n  end\n\n  ### 访问栈顶元素 ###\n  def peek\n    raise IndexError, '栈为空' if is_empty?\n\n    @stack.last\n  end\n\n  ### 返回列表用于打印 ###\n  def to_array\n    @stack\n  end\nend\n
可视化运行

全屏观看 >

","path":["第 5 章   栈与队列","5.1   栈"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#513","level":2,"title":"5.1.3   两种实现对比","text":"

支持操作

两种实现都支持栈定义中的各项操作。数组实现额外支持随机访问,但这已超出了栈的定义范畴,因此一般不会用到。

时间效率

在基于数组的实现中,入栈和出栈操作都在预先分配好的连续内存中进行,具有很好的缓存本地性,因此效率较高。然而,如果入栈时超出数组容量,会触发扩容机制,导致该次入栈操作的时间复杂度变为 \\(O(n)\\) 。

在基于链表的实现中,链表的扩容非常灵活,不存在上述数组扩容时效率降低的问题。但是,入栈操作需要初始化节点对象并修改指针,因此效率相对较低。不过,如果入栈元素本身就是节点对象,那么可以省去初始化步骤,从而提高效率。

综上所述,当入栈与出栈操作的元素是基本数据类型时,例如 intdouble ,我们可以得出以下结论。

空间效率

在初始化列表时,系统会为列表分配“初始容量”,该容量可能超出实际需求;并且,扩容机制通常是按照特定倍率(例如 2 倍)进行扩容的,扩容后的容量也可能超出实际需求。因此,基于数组实现的栈可能造成一定的空间浪费。

然而,由于链表节点需要额外存储指针,因此链表节点占用的空间相对较大。

综上,我们不能简单地确定哪种实现更加节省内存,需要针对具体情况进行分析。

","path":["第 5 章   栈与队列","5.1   栈"],"tags":[]},{"location":"chapter_stack_and_queue/stack/#514","level":2,"title":"5.1.4   栈的典型应用","text":"","path":["第 5 章   栈与队列","5.1   栈"],"tags":[]},{"location":"chapter_stack_and_queue/summary/","level":1,"title":"5.4   小结","text":"","path":["第 5 章   栈与队列","5.4   小结"],"tags":[]},{"location":"chapter_stack_and_queue/summary/#1","level":3,"title":"1.   重点回顾","text":"","path":["第 5 章   栈与队列","5.4   小结"],"tags":[]},{"location":"chapter_stack_and_queue/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q:浏览器的前进后退是否是双向链表实现?

浏览器的前进后退功能本质上是“栈”的体现。当用户访问一个新页面时,该页面会被添加到栈顶;当用户点击后退按钮时,该页面会从栈顶弹出。使用双向队列可以方便地实现一些额外操作,这个在“双向队列”章节有提到。

Q:在出栈后,是否需要释放出栈节点的内存?

如果后续仍需要使用弹出节点,则不需要释放内存。若之后不需要用到,JavaPython 等语言拥有自动垃圾回收机制,因此不需要手动释放内存;在 CC++ 中需要手动释放内存。

Q:双向队列像是两个栈拼接在了一起,它的用途是什么?

双向队列就像是栈和队列的组合或两个栈拼在了一起。它表现的是栈 + 队列的逻辑,因此可以实现栈与队列的所有应用,并且更加灵活。

Q:撤销(undo)和反撤销(redo)具体是如何实现的?

使用两个栈,栈 A 用于撤销,栈 B 用于反撤销。

  1. 每当用户执行一个操作,将这个操作压入栈 A ,并清空栈 B
  2. 当用户执行“撤销”时,从栈 A 中弹出最近的操作,并将其压入栈 B
  3. 当用户执行“反撤销”时,从栈 B 中弹出最近的操作,并将其压入栈 A
","path":["第 5 章   栈与队列","5.4   小结"],"tags":[]},{"location":"chapter_tree/","level":1,"title":"第 7 章   树","text":"

Abstract

参天大树充满生命力,根深叶茂,分枝扶疏。

它为我们展现了数据分治的生动形态。

","path":["第 7 章   树"],"tags":[]},{"location":"chapter_tree/#_1","level":2,"title":"本章内容","text":"","path":["第 7 章   树"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/","level":1,"title":"7.3   二叉树数组表示","text":"

在链表表示下,二叉树的存储单元为节点 TreeNode ,节点之间通过指针相连接。上一节介绍了链表表示下的二叉树的各项基本操作。

那么,我们能否用数组来表示二叉树呢?答案是肯定的。

","path":["第 7 章   树","7.3   二叉树数组表示"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#731","level":2,"title":"7.3.1   表示完美二叉树","text":"

先分析一个简单案例。给定一棵完美二叉树,我们将所有节点按照层序遍历的顺序存储在一个数组中,则每个节点都对应唯一的数组索引。

根据层序遍历的特性,我们可以推导出父节点索引与子节点索引之间的“映射公式”:若某节点的索引为 \\(i\\) ,则该节点的左子节点索引为 \\(2i + 1\\) ,右子节点索引为 \\(2i + 2\\) 。图 7-12 展示了各个节点索引之间的映射关系。

图 7-12   完美二叉树的数组表示

映射公式的角色相当于链表中的节点引用(指针)。给定数组中的任意一个节点,我们都可以通过映射公式来访问它的左(右)子节点。

","path":["第 7 章   树","7.3   二叉树数组表示"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#732","level":2,"title":"7.3.2   表示任意二叉树","text":"

完美二叉树是一个特例,在二叉树的中间层通常存在许多 None 。由于层序遍历序列并不包含这些 None ,因此我们无法仅凭该序列来推测 None 的数量和分布位置。这意味着存在多种二叉树结构都符合该层序遍历序列。

如图 7-13 所示,给定一棵非完美二叉树,上述数组表示方法已经失效。

图 7-13   层序遍历序列对应多种二叉树可能性

为了解决此问题,我们可以考虑在层序遍历序列中显式地写出所有 None 。如图 7-14 所示,这样处理后,层序遍历序列就可以唯一表示二叉树了。示例代码如下:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
# 二叉树的数组表示\n# 使用 None 来表示空位\ntree = [1, 2, 3, 4, None, 6, 7, 8, 9, None, None, 12, None, None, 15]\n
/* 二叉树的数组表示 */\n// 使用 int 最大值 INT_MAX 标记空位\nvector<int> tree = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};\n
/* 二叉树的数组表示 */\n// 使用 int 的包装类 Integer ,就可以使用 null 来标记空位\nInteger[] tree = { 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 };\n
/* 二叉树的数组表示 */\n// 使用 int? 可空类型 ,就可以使用 null 来标记空位\nint?[] tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
/* 二叉树的数组表示 */\n// 使用 any 类型的切片, 就可以使用 nil 来标记空位\ntree := []any{1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15}\n
/* 二叉树的数组表示 */\n// 使用 Int? 可空类型 ,就可以使用 nil 来标记空位\nlet tree: [Int?] = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15]\n
/* 二叉树的数组表示 */\n// 使用 null 来表示空位\nlet tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
/* 二叉树的数组表示 */\n// 使用 null 来表示空位\nlet tree: (number | null)[] = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
/* 二叉树的数组表示 */\n// 使用 int? 可空类型 ,就可以使用 null 来标记空位\nList<int?> tree = [1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15];\n
/* 二叉树的数组表示 */\n// 使用 None 来标记空位\nlet tree = [Some(1), Some(2), Some(3), Some(4), None, Some(6), Some(7), Some(8), Some(9), None, None, Some(12), None, None, Some(15)];\n
/* 二叉树的数组表示 */\n// 使用 int 最大值标记空位,因此要求节点值不能为 INT_MAX\nint tree[] = {1, 2, 3, 4, INT_MAX, 6, 7, 8, 9, INT_MAX, INT_MAX, 12, INT_MAX, INT_MAX, 15};\n
/* 二叉树的数组表示 */\n// 使用 null 来表示空位\nval tree = arrayOf( 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 )\n
### 二叉树的数组表示 ###\n# 使用 nil 来表示空位\ntree = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15]\n

图 7-14   任意类型二叉树的数组表示

值得说明的是,完全二叉树非常适合使用数组来表示。回顾完全二叉树的定义,None 只出现在最底层且靠右的位置,因此所有 None 一定出现在层序遍历序列的末尾。

这意味着使用数组表示完全二叉树时,可以省略存储所有 None ,非常方便。图 7-15 给出了一个例子。

图 7-15   完全二叉树的数组表示

以下代码实现了一棵基于数组表示的二叉树,包括以下几种操作。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby array_binary_tree.py
class ArrayBinaryTree:\n    \"\"\"数组表示下的二叉树类\"\"\"\n\n    def __init__(self, arr: list[int | None]):\n        \"\"\"构造方法\"\"\"\n        self._tree = list(arr)\n\n    def size(self):\n        \"\"\"列表容量\"\"\"\n        return len(self._tree)\n\n    def val(self, i: int) -> int | None:\n        \"\"\"获取索引为 i 节点的值\"\"\"\n        # 若索引越界,则返回 None ,代表空位\n        if i < 0 or i >= self.size():\n            return None\n        return self._tree[i]\n\n    def left(self, i: int) -> int | None:\n        \"\"\"获取索引为 i 节点的左子节点的索引\"\"\"\n        return 2 * i + 1\n\n    def right(self, i: int) -> int | None:\n        \"\"\"获取索引为 i 节点的右子节点的索引\"\"\"\n        return 2 * i + 2\n\n    def parent(self, i: int) -> int | None:\n        \"\"\"获取索引为 i 节点的父节点的索引\"\"\"\n        return (i - 1) // 2\n\n    def level_order(self) -> list[int]:\n        \"\"\"层序遍历\"\"\"\n        self.res = []\n        # 直接遍历数组\n        for i in range(self.size()):\n            if self.val(i) is not None:\n                self.res.append(self.val(i))\n        return self.res\n\n    def dfs(self, i: int, order: str):\n        \"\"\"深度优先遍历\"\"\"\n        if self.val(i) is None:\n            return\n        # 前序遍历\n        if order == \"pre\":\n            self.res.append(self.val(i))\n        self.dfs(self.left(i), order)\n        # 中序遍历\n        if order == \"in\":\n            self.res.append(self.val(i))\n        self.dfs(self.right(i), order)\n        # 后序遍历\n        if order == \"post\":\n            self.res.append(self.val(i))\n\n    def pre_order(self) -> list[int]:\n        \"\"\"前序遍历\"\"\"\n        self.res = []\n        self.dfs(0, order=\"pre\")\n        return self.res\n\n    def in_order(self) -> list[int]:\n        \"\"\"中序遍历\"\"\"\n        self.res = []\n        self.dfs(0, order=\"in\")\n        return self.res\n\n    def post_order(self) -> list[int]:\n        \"\"\"后序遍历\"\"\"\n        self.res = []\n        self.dfs(0, order=\"post\")\n        return self.res\n
array_binary_tree.cpp
/* 数组表示下的二叉树类 */\nclass ArrayBinaryTree {\n  public:\n    /* 构造方法 */\n    ArrayBinaryTree(vector<int> arr) {\n        tree = arr;\n    }\n\n    /* 列表容量 */\n    int size() {\n        return tree.size();\n    }\n\n    /* 获取索引为 i 节点的值 */\n    int val(int i) {\n        // 若索引越界,则返回 INT_MAX ,代表空位\n        if (i < 0 || i >= size())\n            return INT_MAX;\n        return tree[i];\n    }\n\n    /* 获取索引为 i 节点的左子节点的索引 */\n    int left(int i) {\n        return 2 * i + 1;\n    }\n\n    /* 获取索引为 i 节点的右子节点的索引 */\n    int right(int i) {\n        return 2 * i + 2;\n    }\n\n    /* 获取索引为 i 节点的父节点的索引 */\n    int parent(int i) {\n        return (i - 1) / 2;\n    }\n\n    /* 层序遍历 */\n    vector<int> levelOrder() {\n        vector<int> res;\n        // 直接遍历数组\n        for (int i = 0; i < size(); i++) {\n            if (val(i) != INT_MAX)\n                res.push_back(val(i));\n        }\n        return res;\n    }\n\n    /* 前序遍历 */\n    vector<int> preOrder() {\n        vector<int> res;\n        dfs(0, \"pre\", res);\n        return res;\n    }\n\n    /* 中序遍历 */\n    vector<int> inOrder() {\n        vector<int> res;\n        dfs(0, \"in\", res);\n        return res;\n    }\n\n    /* 后序遍历 */\n    vector<int> postOrder() {\n        vector<int> res;\n        dfs(0, \"post\", res);\n        return res;\n    }\n\n  private:\n    vector<int> tree;\n\n    /* 深度优先遍历 */\n    void dfs(int i, string order, vector<int> &res) {\n        // 若为空位,则返回\n        if (val(i) == INT_MAX)\n            return;\n        // 前序遍历\n        if (order == \"pre\")\n            res.push_back(val(i));\n        dfs(left(i), order, res);\n        // 中序遍历\n        if (order == \"in\")\n            res.push_back(val(i));\n        dfs(right(i), order, res);\n        // 后序遍历\n        if (order == \"post\")\n            res.push_back(val(i));\n    }\n};\n
array_binary_tree.java
/* 数组表示下的二叉树类 */\nclass ArrayBinaryTree {\n    private List<Integer> tree;\n\n    /* 构造方法 */\n    public ArrayBinaryTree(List<Integer> arr) {\n        tree = new ArrayList<>(arr);\n    }\n\n    /* 列表容量 */\n    public int size() {\n        return tree.size();\n    }\n\n    /* 获取索引为 i 节点的值 */\n    public Integer val(int i) {\n        // 若索引越界,则返回 null ,代表空位\n        if (i < 0 || i >= size())\n            return null;\n        return tree.get(i);\n    }\n\n    /* 获取索引为 i 节点的左子节点的索引 */\n    public Integer left(int i) {\n        return 2 * i + 1;\n    }\n\n    /* 获取索引为 i 节点的右子节点的索引 */\n    public Integer right(int i) {\n        return 2 * i + 2;\n    }\n\n    /* 获取索引为 i 节点的父节点的索引 */\n    public Integer parent(int i) {\n        return (i - 1) / 2;\n    }\n\n    /* 层序遍历 */\n    public List<Integer> levelOrder() {\n        List<Integer> res = new ArrayList<>();\n        // 直接遍历数组\n        for (int i = 0; i < size(); i++) {\n            if (val(i) != null)\n                res.add(val(i));\n        }\n        return res;\n    }\n\n    /* 深度优先遍历 */\n    private void dfs(Integer i, String order, List<Integer> res) {\n        // 若为空位,则返回\n        if (val(i) == null)\n            return;\n        // 前序遍历\n        if (\"pre\".equals(order))\n            res.add(val(i));\n        dfs(left(i), order, res);\n        // 中序遍历\n        if (\"in\".equals(order))\n            res.add(val(i));\n        dfs(right(i), order, res);\n        // 后序遍历\n        if (\"post\".equals(order))\n            res.add(val(i));\n    }\n\n    /* 前序遍历 */\n    public List<Integer> preOrder() {\n        List<Integer> res = new ArrayList<>();\n        dfs(0, \"pre\", res);\n        return res;\n    }\n\n    /* 中序遍历 */\n    public List<Integer> inOrder() {\n        List<Integer> res = new ArrayList<>();\n        dfs(0, \"in\", res);\n        return res;\n    }\n\n    /* 后序遍历 */\n    public List<Integer> postOrder() {\n        List<Integer> res = new ArrayList<>();\n        dfs(0, \"post\", res);\n        return res;\n    }\n}\n
array_binary_tree.cs
/* 数组表示下的二叉树类 */\nclass ArrayBinaryTree(List<int?> arr) {\n    List<int?> tree = new(arr);\n\n    /* 列表容量 */\n    public int Size() {\n        return tree.Count;\n    }\n\n    /* 获取索引为 i 节点的值 */\n    public int? Val(int i) {\n        // 若索引越界,则返回 null ,代表空位\n        if (i < 0 || i >= Size())\n            return null;\n        return tree[i];\n    }\n\n    /* 获取索引为 i 节点的左子节点的索引 */\n    public int Left(int i) {\n        return 2 * i + 1;\n    }\n\n    /* 获取索引为 i 节点的右子节点的索引 */\n    public int Right(int i) {\n        return 2 * i + 2;\n    }\n\n    /* 获取索引为 i 节点的父节点的索引 */\n    public int Parent(int i) {\n        return (i - 1) / 2;\n    }\n\n    /* 层序遍历 */\n    public List<int> LevelOrder() {\n        List<int> res = [];\n        // 直接遍历数组\n        for (int i = 0; i < Size(); i++) {\n            if (Val(i).HasValue)\n                res.Add(Val(i)!.Value);\n        }\n        return res;\n    }\n\n    /* 深度优先遍历 */\n    void DFS(int i, string order, List<int> res) {\n        // 若为空位,则返回\n        if (!Val(i).HasValue)\n            return;\n        // 前序遍历\n        if (order == \"pre\")\n            res.Add(Val(i)!.Value);\n        DFS(Left(i), order, res);\n        // 中序遍历\n        if (order == \"in\")\n            res.Add(Val(i)!.Value);\n        DFS(Right(i), order, res);\n        // 后序遍历\n        if (order == \"post\")\n            res.Add(Val(i)!.Value);\n    }\n\n    /* 前序遍历 */\n    public List<int> PreOrder() {\n        List<int> res = [];\n        DFS(0, \"pre\", res);\n        return res;\n    }\n\n    /* 中序遍历 */\n    public List<int> InOrder() {\n        List<int> res = [];\n        DFS(0, \"in\", res);\n        return res;\n    }\n\n    /* 后序遍历 */\n    public List<int> PostOrder() {\n        List<int> res = [];\n        DFS(0, \"post\", res);\n        return res;\n    }\n}\n
array_binary_tree.go
/* 数组表示下的二叉树类 */\ntype arrayBinaryTree struct {\n    tree []any\n}\n\n/* 构造方法 */\nfunc newArrayBinaryTree(arr []any) *arrayBinaryTree {\n    return &arrayBinaryTree{\n        tree: arr,\n    }\n}\n\n/* 列表容量 */\nfunc (abt *arrayBinaryTree) size() int {\n    return len(abt.tree)\n}\n\n/* 获取索引为 i 节点的值 */\nfunc (abt *arrayBinaryTree) val(i int) any {\n    // 若索引越界,则返回 null ,代表空位\n    if i < 0 || i >= abt.size() {\n        return nil\n    }\n    return abt.tree[i]\n}\n\n/* 获取索引为 i 节点的左子节点的索引 */\nfunc (abt *arrayBinaryTree) left(i int) int {\n    return 2*i + 1\n}\n\n/* 获取索引为 i 节点的右子节点的索引 */\nfunc (abt *arrayBinaryTree) right(i int) int {\n    return 2*i + 2\n}\n\n/* 获取索引为 i 节点的父节点的索引 */\nfunc (abt *arrayBinaryTree) parent(i int) int {\n    return (i - 1) / 2\n}\n\n/* 层序遍历 */\nfunc (abt *arrayBinaryTree) levelOrder() []any {\n    var res []any\n    // 直接遍历数组\n    for i := 0; i < abt.size(); i++ {\n        if abt.val(i) != nil {\n            res = append(res, abt.val(i))\n        }\n    }\n    return res\n}\n\n/* 深度优先遍历 */\nfunc (abt *arrayBinaryTree) dfs(i int, order string, res *[]any) {\n    // 若为空位,则返回\n    if abt.val(i) == nil {\n        return\n    }\n    // 前序遍历\n    if order == \"pre\" {\n        *res = append(*res, abt.val(i))\n    }\n    abt.dfs(abt.left(i), order, res)\n    // 中序遍历\n    if order == \"in\" {\n        *res = append(*res, abt.val(i))\n    }\n    abt.dfs(abt.right(i), order, res)\n    // 后序遍历\n    if order == \"post\" {\n        *res = append(*res, abt.val(i))\n    }\n}\n\n/* 前序遍历 */\nfunc (abt *arrayBinaryTree) preOrder() []any {\n    var res []any\n    abt.dfs(0, \"pre\", &res)\n    return res\n}\n\n/* 中序遍历 */\nfunc (abt *arrayBinaryTree) inOrder() []any {\n    var res []any\n    abt.dfs(0, \"in\", &res)\n    return res\n}\n\n/* 后序遍历 */\nfunc (abt *arrayBinaryTree) postOrder() []any {\n    var res []any\n    abt.dfs(0, \"post\", &res)\n    return res\n}\n
array_binary_tree.swift
/* 数组表示下的二叉树类 */\nclass ArrayBinaryTree {\n    private var tree: [Int?]\n\n    /* 构造方法 */\n    init(arr: [Int?]) {\n        tree = arr\n    }\n\n    /* 列表容量 */\n    func size() -> Int {\n        tree.count\n    }\n\n    /* 获取索引为 i 节点的值 */\n    func val(i: Int) -> Int? {\n        // 若索引越界,则返回 null ,代表空位\n        if i < 0 || i >= size() {\n            return nil\n        }\n        return tree[i]\n    }\n\n    /* 获取索引为 i 节点的左子节点的索引 */\n    func left(i: Int) -> Int {\n        2 * i + 1\n    }\n\n    /* 获取索引为 i 节点的右子节点的索引 */\n    func right(i: Int) -> Int {\n        2 * i + 2\n    }\n\n    /* 获取索引为 i 节点的父节点的索引 */\n    func parent(i: Int) -> Int {\n        (i - 1) / 2\n    }\n\n    /* 层序遍历 */\n    func levelOrder() -> [Int] {\n        var res: [Int] = []\n        // 直接遍历数组\n        for i in 0 ..< size() {\n            if let val = val(i: i) {\n                res.append(val)\n            }\n        }\n        return res\n    }\n\n    /* 深度优先遍历 */\n    private func dfs(i: Int, order: String, res: inout [Int]) {\n        // 若为空位,则返回\n        guard let val = val(i: i) else {\n            return\n        }\n        // 前序遍历\n        if order == \"pre\" {\n            res.append(val)\n        }\n        dfs(i: left(i: i), order: order, res: &res)\n        // 中序遍历\n        if order == \"in\" {\n            res.append(val)\n        }\n        dfs(i: right(i: i), order: order, res: &res)\n        // 后序遍历\n        if order == \"post\" {\n            res.append(val)\n        }\n    }\n\n    /* 前序遍历 */\n    func preOrder() -> [Int] {\n        var res: [Int] = []\n        dfs(i: 0, order: \"pre\", res: &res)\n        return res\n    }\n\n    /* 中序遍历 */\n    func inOrder() -> [Int] {\n        var res: [Int] = []\n        dfs(i: 0, order: \"in\", res: &res)\n        return res\n    }\n\n    /* 后序遍历 */\n    func postOrder() -> [Int] {\n        var res: [Int] = []\n        dfs(i: 0, order: \"post\", res: &res)\n        return res\n    }\n}\n
array_binary_tree.js
/* 数组表示下的二叉树类 */\nclass ArrayBinaryTree {\n    #tree;\n\n    /* 构造方法 */\n    constructor(arr) {\n        this.#tree = arr;\n    }\n\n    /* 列表容量 */\n    size() {\n        return this.#tree.length;\n    }\n\n    /* 获取索引为 i 节点的值 */\n    val(i) {\n        // 若索引越界,则返回 null ,代表空位\n        if (i < 0 || i >= this.size()) return null;\n        return this.#tree[i];\n    }\n\n    /* 获取索引为 i 节点的左子节点的索引 */\n    left(i) {\n        return 2 * i + 1;\n    }\n\n    /* 获取索引为 i 节点的右子节点的索引 */\n    right(i) {\n        return 2 * i + 2;\n    }\n\n    /* 获取索引为 i 节点的父节点的索引 */\n    parent(i) {\n        return Math.floor((i - 1) / 2); // 向下整除\n    }\n\n    /* 层序遍历 */\n    levelOrder() {\n        let res = [];\n        // 直接遍历数组\n        for (let i = 0; i < this.size(); i++) {\n            if (this.val(i) !== null) res.push(this.val(i));\n        }\n        return res;\n    }\n\n    /* 深度优先遍历 */\n    #dfs(i, order, res) {\n        // 若为空位,则返回\n        if (this.val(i) === null) return;\n        // 前序遍历\n        if (order === 'pre') res.push(this.val(i));\n        this.#dfs(this.left(i), order, res);\n        // 中序遍历\n        if (order === 'in') res.push(this.val(i));\n        this.#dfs(this.right(i), order, res);\n        // 后序遍历\n        if (order === 'post') res.push(this.val(i));\n    }\n\n    /* 前序遍历 */\n    preOrder() {\n        const res = [];\n        this.#dfs(0, 'pre', res);\n        return res;\n    }\n\n    /* 中序遍历 */\n    inOrder() {\n        const res = [];\n        this.#dfs(0, 'in', res);\n        return res;\n    }\n\n    /* 后序遍历 */\n    postOrder() {\n        const res = [];\n        this.#dfs(0, 'post', res);\n        return res;\n    }\n}\n
array_binary_tree.ts
/* 数组表示下的二叉树类 */\nclass ArrayBinaryTree {\n    #tree: (number | null)[];\n\n    /* 构造方法 */\n    constructor(arr: (number | null)[]) {\n        this.#tree = arr;\n    }\n\n    /* 列表容量 */\n    size(): number {\n        return this.#tree.length;\n    }\n\n    /* 获取索引为 i 节点的值 */\n    val(i: number): number | null {\n        // 若索引越界,则返回 null ,代表空位\n        if (i < 0 || i >= this.size()) return null;\n        return this.#tree[i];\n    }\n\n    /* 获取索引为 i 节点的左子节点的索引 */\n    left(i: number): number {\n        return 2 * i + 1;\n    }\n\n    /* 获取索引为 i 节点的右子节点的索引 */\n    right(i: number): number {\n        return 2 * i + 2;\n    }\n\n    /* 获取索引为 i 节点的父节点的索引 */\n    parent(i: number): number {\n        return Math.floor((i - 1) / 2); // 向下整除\n    }\n\n    /* 层序遍历 */\n    levelOrder(): number[] {\n        let res = [];\n        // 直接遍历数组\n        for (let i = 0; i < this.size(); i++) {\n            if (this.val(i) !== null) res.push(this.val(i));\n        }\n        return res;\n    }\n\n    /* 深度优先遍历 */\n    #dfs(i: number, order: Order, res: (number | null)[]): void {\n        // 若为空位,则返回\n        if (this.val(i) === null) return;\n        // 前序遍历\n        if (order === 'pre') res.push(this.val(i));\n        this.#dfs(this.left(i), order, res);\n        // 中序遍历\n        if (order === 'in') res.push(this.val(i));\n        this.#dfs(this.right(i), order, res);\n        // 后序遍历\n        if (order === 'post') res.push(this.val(i));\n    }\n\n    /* 前序遍历 */\n    preOrder(): (number | null)[] {\n        const res = [];\n        this.#dfs(0, 'pre', res);\n        return res;\n    }\n\n    /* 中序遍历 */\n    inOrder(): (number | null)[] {\n        const res = [];\n        this.#dfs(0, 'in', res);\n        return res;\n    }\n\n    /* 后序遍历 */\n    postOrder(): (number | null)[] {\n        const res = [];\n        this.#dfs(0, 'post', res);\n        return res;\n    }\n}\n
array_binary_tree.dart
/* 数组表示下的二叉树类 */\nclass ArrayBinaryTree {\n  late List<int?> _tree;\n\n  /* 构造方法 */\n  ArrayBinaryTree(this._tree);\n\n  /* 列表容量 */\n  int size() {\n    return _tree.length;\n  }\n\n  /* 获取索引为 i 节点的值 */\n  int? val(int i) {\n    // 若索引越界,则返回 null ,代表空位\n    if (i < 0 || i >= size()) {\n      return null;\n    }\n    return _tree[i];\n  }\n\n  /* 获取索引为 i 节点的左子节点的索引 */\n  int? left(int i) {\n    return 2 * i + 1;\n  }\n\n  /* 获取索引为 i 节点的右子节点的索引 */\n  int? right(int i) {\n    return 2 * i + 2;\n  }\n\n  /* 获取索引为 i 节点的父节点的索引 */\n  int? parent(int i) {\n    return (i - 1) ~/ 2;\n  }\n\n  /* 层序遍历 */\n  List<int> levelOrder() {\n    List<int> res = [];\n    for (int i = 0; i < size(); i++) {\n      if (val(i) != null) {\n        res.add(val(i)!);\n      }\n    }\n    return res;\n  }\n\n  /* 深度优先遍历 */\n  void dfs(int i, String order, List<int?> res) {\n    // 若为空位,则返回\n    if (val(i) == null) {\n      return;\n    }\n    // 前序遍历\n    if (order == 'pre') {\n      res.add(val(i));\n    }\n    dfs(left(i)!, order, res);\n    // 中序遍历\n    if (order == 'in') {\n      res.add(val(i));\n    }\n    dfs(right(i)!, order, res);\n    // 后序遍历\n    if (order == 'post') {\n      res.add(val(i));\n    }\n  }\n\n  /* 前序遍历 */\n  List<int?> preOrder() {\n    List<int?> res = [];\n    dfs(0, 'pre', res);\n    return res;\n  }\n\n  /* 中序遍历 */\n  List<int?> inOrder() {\n    List<int?> res = [];\n    dfs(0, 'in', res);\n    return res;\n  }\n\n  /* 后序遍历 */\n  List<int?> postOrder() {\n    List<int?> res = [];\n    dfs(0, 'post', res);\n    return res;\n  }\n}\n
array_binary_tree.rs
/* 数组表示下的二叉树类 */\nstruct ArrayBinaryTree {\n    tree: Vec<Option<i32>>,\n}\n\nimpl ArrayBinaryTree {\n    /* 构造方法 */\n    fn new(arr: Vec<Option<i32>>) -> Self {\n        Self { tree: arr }\n    }\n\n    /* 列表容量 */\n    fn size(&self) -> i32 {\n        self.tree.len() as i32\n    }\n\n    /* 获取索引为 i 节点的值 */\n    fn val(&self, i: i32) -> Option<i32> {\n        // 若索引越界,则返回 None ,代表空位\n        if i < 0 || i >= self.size() {\n            None\n        } else {\n            self.tree[i as usize]\n        }\n    }\n\n    /* 获取索引为 i 节点的左子节点的索引 */\n    fn left(&self, i: i32) -> i32 {\n        2 * i + 1\n    }\n\n    /* 获取索引为 i 节点的右子节点的索引 */\n    fn right(&self, i: i32) -> i32 {\n        2 * i + 2\n    }\n\n    /* 获取索引为 i 节点的父节点的索引 */\n    fn parent(&self, i: i32) -> i32 {\n        (i - 1) / 2\n    }\n\n    /* 层序遍历 */\n    fn level_order(&self) -> Vec<i32> {\n        self.tree.iter().filter_map(|&x| x).collect()\n    }\n\n    /* 深度优先遍历 */\n    fn dfs(&self, i: i32, order: &'static str, res: &mut Vec<i32>) {\n        if self.val(i).is_none() {\n            return;\n        }\n        let val = self.val(i).unwrap();\n        // 前序遍历\n        if order == \"pre\" {\n            res.push(val);\n        }\n        self.dfs(self.left(i), order, res);\n        // 中序遍历\n        if order == \"in\" {\n            res.push(val);\n        }\n        self.dfs(self.right(i), order, res);\n        // 后序遍历\n        if order == \"post\" {\n            res.push(val);\n        }\n    }\n\n    /* 前序遍历 */\n    fn pre_order(&self) -> Vec<i32> {\n        let mut res = vec![];\n        self.dfs(0, \"pre\", &mut res);\n        res\n    }\n\n    /* 中序遍历 */\n    fn in_order(&self) -> Vec<i32> {\n        let mut res = vec![];\n        self.dfs(0, \"in\", &mut res);\n        res\n    }\n\n    /* 后序遍历 */\n    fn post_order(&self) -> Vec<i32> {\n        let mut res = vec![];\n        self.dfs(0, \"post\", &mut res);\n        res\n    }\n}\n
array_binary_tree.c
/* 数组表示下的二叉树结构体 */\ntypedef struct {\n    int *tree;\n    int size;\n} ArrayBinaryTree;\n\n/* 构造函数 */\nArrayBinaryTree *newArrayBinaryTree(int *arr, int arrSize) {\n    ArrayBinaryTree *abt = (ArrayBinaryTree *)malloc(sizeof(ArrayBinaryTree));\n    abt->tree = malloc(sizeof(int) * arrSize);\n    memcpy(abt->tree, arr, sizeof(int) * arrSize);\n    abt->size = arrSize;\n    return abt;\n}\n\n/* 析构函数 */\nvoid delArrayBinaryTree(ArrayBinaryTree *abt) {\n    free(abt->tree);\n    free(abt);\n}\n\n/* 列表容量 */\nint size(ArrayBinaryTree *abt) {\n    return abt->size;\n}\n\n/* 获取索引为 i 节点的值 */\nint val(ArrayBinaryTree *abt, int i) {\n    // 若索引越界,则返回 INT_MAX ,代表空位\n    if (i < 0 || i >= size(abt))\n        return INT_MAX;\n    return abt->tree[i];\n}\n\n/* 层序遍历 */\nint *levelOrder(ArrayBinaryTree *abt, int *returnSize) {\n    int *res = (int *)malloc(sizeof(int) * size(abt));\n    int index = 0;\n    // 直接遍历数组\n    for (int i = 0; i < size(abt); i++) {\n        if (val(abt, i) != INT_MAX)\n            res[index++] = val(abt, i);\n    }\n    *returnSize = index;\n    return res;\n}\n\n/* 深度优先遍历 */\nvoid dfs(ArrayBinaryTree *abt, int i, char *order, int *res, int *index) {\n    // 若为空位,则返回\n    if (val(abt, i) == INT_MAX)\n        return;\n    // 前序遍历\n    if (strcmp(order, \"pre\") == 0)\n        res[(*index)++] = val(abt, i);\n    dfs(abt, left(i), order, res, index);\n    // 中序遍历\n    if (strcmp(order, \"in\") == 0)\n        res[(*index)++] = val(abt, i);\n    dfs(abt, right(i), order, res, index);\n    // 后序遍历\n    if (strcmp(order, \"post\") == 0)\n        res[(*index)++] = val(abt, i);\n}\n\n/* 前序遍历 */\nint *preOrder(ArrayBinaryTree *abt, int *returnSize) {\n    int *res = (int *)malloc(sizeof(int) * size(abt));\n    int index = 0;\n    dfs(abt, 0, \"pre\", res, &index);\n    *returnSize = index;\n    return res;\n}\n\n/* 中序遍历 */\nint *inOrder(ArrayBinaryTree *abt, int *returnSize) {\n    int *res = (int *)malloc(sizeof(int) * size(abt));\n    int index = 0;\n    dfs(abt, 0, \"in\", res, &index);\n    *returnSize = index;\n    return res;\n}\n\n/* 后序遍历 */\nint *postOrder(ArrayBinaryTree *abt, int *returnSize) {\n    int *res = (int *)malloc(sizeof(int) * size(abt));\n    int index = 0;\n    dfs(abt, 0, \"post\", res, &index);\n    *returnSize = index;\n    return res;\n}\n
array_binary_tree.kt
/* 数组表示下的二叉树类 */\nclass ArrayBinaryTree(val tree: MutableList<Int?>) {\n    /* 列表容量 */\n    fun size(): Int {\n        return tree.size\n    }\n\n    /* 获取索引为 i 节点的值 */\n    fun _val(i: Int): Int? {\n        // 若索引越界,则返回 null ,代表空位\n        if (i < 0 || i >= size()) return null\n        return tree[i]\n    }\n\n    /* 获取索引为 i 节点的左子节点的索引 */\n    fun left(i: Int): Int {\n        return 2 * i + 1\n    }\n\n    /* 获取索引为 i 节点的右子节点的索引 */\n    fun right(i: Int): Int {\n        return 2 * i + 2\n    }\n\n    /* 获取索引为 i 节点的父节点的索引 */\n    fun parent(i: Int): Int {\n        return (i - 1) / 2\n    }\n\n    /* 层序遍历 */\n    fun levelOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        // 直接遍历数组\n        for (i in 0..<size()) {\n            if (_val(i) != null)\n                res.add(_val(i))\n        }\n        return res\n    }\n\n    /* 深度优先遍历 */\n    fun dfs(i: Int, order: String, res: MutableList<Int?>) {\n        // 若为空位,则返回\n        if (_val(i) == null)\n            return\n        // 前序遍历\n        if (\"pre\" == order)\n            res.add(_val(i))\n        dfs(left(i), order, res)\n        // 中序遍历\n        if (\"in\" == order)\n            res.add(_val(i))\n        dfs(right(i), order, res)\n        // 后序遍历\n        if (\"post\" == order)\n            res.add(_val(i))\n    }\n\n    /* 前序遍历 */\n    fun preOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        dfs(0, \"pre\", res)\n        return res\n    }\n\n    /* 中序遍历 */\n    fun inOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        dfs(0, \"in\", res)\n        return res\n    }\n\n    /* 后序遍历 */\n    fun postOrder(): MutableList<Int?> {\n        val res = mutableListOf<Int?>()\n        dfs(0, \"post\", res)\n        return res\n    }\n}\n
array_binary_tree.rb
### 数组表示下的二叉树类 ###\nclass ArrayBinaryTree\n  ### 构造方法 ###\n  def initialize(arr)\n    @tree = arr.to_a\n  end\n\n  ### 列表容量 ###\n  def size\n    @tree.length\n  end\n\n  ### 获取索引为 i 节点的值 ###\n  def val(i)\n    # 若索引越界,则返回 nil ,代表空位\n    return if i < 0 || i >= size\n\n    @tree[i]\n  end\n\n  ### 获取索引为 i 节点的左子节点的索引 ###\n  def left(i)\n    2 * i + 1\n  end\n\n  ### 获取索引为 i 节点的右子节点的索引 ###\n  def right(i)\n    2 * i + 2\n  end\n\n  ### 获取索引为 i 节点的父节点的索引 ###\n  def parent(i)\n    (i - 1) / 2\n  end\n\n  ### 层序遍历 ###\n  def level_order\n    @res = []\n\n    # 直接遍历数组\n    for i in 0...size\n      @res << val(i) unless val(i).nil?\n    end\n\n    @res\n  end\n\n  ### 深度优先遍历 ###\n  def dfs(i, order)\n    return if val(i).nil?\n    # 前序遍历\n    @res << val(i) if order == :pre\n    dfs(left(i), order)\n    # 中序遍历\n    @res << val(i) if order == :in\n    dfs(right(i), order)\n    # 后序遍历\n    @res << val(i) if order == :post\n  end\n\n  ### 前序遍历 ###\n  def pre_order\n    @res = []\n    dfs(0, :pre)\n    @res\n  end\n\n  ### 中序遍历 ###\n  def in_order\n    @res = []\n    dfs(0, :in)\n    @res\n  end\n\n  ### 后序遍历 ###\n  def post_order\n    @res = []\n    dfs(0, :post)\n    @res\n  end\nend\n
可视化运行

全屏观看 >

","path":["第 7 章   树","7.3   二叉树数组表示"],"tags":[]},{"location":"chapter_tree/array_representation_of_tree/#733","level":2,"title":"7.3.3   优点与局限性","text":"

二叉树的数组表示主要有以下优点。

然而,数组表示也存在一些局限性。

","path":["第 7 章   树","7.3   二叉树数组表示"],"tags":[]},{"location":"chapter_tree/avl_tree/","level":1,"title":"7.5   AVL 树 *","text":"

在“二叉搜索树”章节中我们提到,在多次插入和删除操作后,二叉搜索树可能退化为链表。在这种情况下,所有操作的时间复杂度将从 \\(O(\\log n)\\) 劣化为 \\(O(n)\\) 。

如图 7-24 所示,经过两次删除节点操作,这棵二叉搜索树便会退化为链表。

图 7-24   AVL 树在删除节点后发生退化

再例如,在图 7-25 所示的完美二叉树中插入两个节点后,树将严重向左倾斜,查找操作的时间复杂度也随之劣化。

图 7-25   AVL 树在插入节点后发生退化

1962 年 G. M. Adelson-Velsky 和 E. M. Landis 在论文“An algorithm for the organization of information”中提出了 AVL 树。论文中详细描述了一系列操作,确保在持续添加和删除节点后,AVL 树不会退化,从而使得各种操作的时间复杂度保持在 \\(O(\\log n)\\) 级别。换句话说,在需要频繁进行增删查改操作的场景中,AVL 树能始终保持高效的数据操作性能,具有很好的应用价值。

","path":["第 7 章   树","7.5   AVL 树 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#751-avl","level":2,"title":"7.5.1   AVL 树常见术语","text":"

AVL 树既是二叉搜索树,也是平衡二叉树,同时满足这两类二叉树的所有性质,因此是一种平衡二叉搜索树(balanced binary search tree)。

","path":["第 7 章   树","7.5   AVL 树 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#1","level":3,"title":"1.   节点高度","text":"

由于 AVL 树的相关操作需要获取节点高度,因此我们需要为节点类添加 height 变量:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
class TreeNode:\n    \"\"\"AVL 树节点类\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val                 # 节点值\n        self.height: int = 0                # 节点高度\n        self.left: TreeNode | None = None   # 左子节点引用\n        self.right: TreeNode | None = None  # 右子节点引用\n
/* AVL 树节点类 */\nstruct TreeNode {\n    int val{};          // 节点值\n    int height = 0;     // 节点高度\n    TreeNode *left{};   // 左子节点\n    TreeNode *right{};  // 右子节点\n    TreeNode() = default;\n    explicit TreeNode(int x) : val(x){}\n};\n
/* AVL 树节点类 */\nclass TreeNode {\n    public int val;        // 节点值\n    public int height;     // 节点高度\n    public TreeNode left;  // 左子节点\n    public TreeNode right; // 右子节点\n    public TreeNode(int x) { val = x; }\n}\n
/* AVL 树节点类 */\nclass TreeNode(int? x) {\n    public int? val = x;    // 节点值\n    public int height;      // 节点高度\n    public TreeNode? left;  // 左子节点引用\n    public TreeNode? right; // 右子节点引用\n}\n
/* AVL 树节点结构体 */\ntype TreeNode struct {\n    Val    int       // 节点值\n    Height int       // 节点高度\n    Left   *TreeNode // 左子节点引用\n    Right  *TreeNode // 右子节点引用\n}\n
/* AVL 树节点类 */\nclass TreeNode {\n    var val: Int // 节点值\n    var height: Int // 节点高度\n    var left: TreeNode? // 左子节点\n    var right: TreeNode? // 右子节点\n\n    init(x: Int) {\n        val = x\n        height = 0\n    }\n}\n
/* AVL 树节点类 */\nclass TreeNode {\n    val; // 节点值\n    height; //节点高度\n    left; // 左子节点指针\n    right; // 右子节点指针\n    constructor(val, left, right, height) {\n        this.val = val === undefined ? 0 : val;\n        this.height = height === undefined ? 0 : height;\n        this.left = left === undefined ? null : left;\n        this.right = right === undefined ? null : right;\n    }\n}\n
/* AVL 树节点类 */\nclass TreeNode {\n    val: number;            // 节点值\n    height: number;         // 节点高度\n    left: TreeNode | null;  // 左子节点指针\n    right: TreeNode | null; // 右子节点指针\n    constructor(val?: number, height?: number, left?: TreeNode | null, right?: TreeNode | null) {\n        this.val = val === undefined ? 0 : val;\n        this.height = height === undefined ? 0 : height;\n        this.left = left === undefined ? null : left;\n        this.right = right === undefined ? null : right;\n    }\n}\n
/* AVL 树节点类 */\nclass TreeNode {\n  int val;         // 节点值\n  int height;      // 节点高度\n  TreeNode? left;  // 左子节点\n  TreeNode? right; // 右子节点\n  TreeNode(this.val, [this.height = 0, this.left, this.right]);\n}\n
use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* AVL 树节点结构体 */\nstruct TreeNode {\n    val: i32,                               // 节点值\n    height: i32,                            // 节点高度\n    left: Option<Rc<RefCell<TreeNode>>>,    // 左子节点\n    right: Option<Rc<RefCell<TreeNode>>>,   // 右子节点\n}\n\nimpl TreeNode {\n    /* 构造方法 */\n    fn new(val: i32) -> Rc<RefCell<Self>> {\n        Rc::new(RefCell::new(Self {\n            val,\n            height: 0,\n            left: None,\n            right: None\n        }))\n    }\n}\n
/* AVL 树节点结构体 */\ntypedef struct TreeNode {\n    int val;\n    int height;\n    struct TreeNode *left;\n    struct TreeNode *right;\n} TreeNode;\n\n/* 构造函数 */\nTreeNode *newTreeNode(int val) {\n    TreeNode *node;\n\n    node = (TreeNode *)malloc(sizeof(TreeNode));\n    node->val = val;\n    node->height = 0;\n    node->left = NULL;\n    node->right = NULL;\n    return node;\n}\n
/* AVL 树节点类 */\nclass TreeNode(val _val: Int) {  // 节点值\n    val height: Int = 0          // 节点高度\n    val left: TreeNode? = null   // 左子节点\n    val right: TreeNode? = null  // 右子节点\n}\n
### AVL 树节点类 ###\nclass TreeNode\n  attr_accessor :val    # 节点值\n  attr_accessor :height # 节点高度\n  attr_accessor :left   # 左子节点引用\n  attr_accessor :right  # 右子节点引用\n\n  def initialize(val)\n    @val = val\n    @height = 0\n  end\nend\n

“节点高度”是指从该节点到它的最远叶节点的距离,即所经过的“边”的数量。需要特别注意的是,叶节点的高度为 \\(0\\) ,而空节点的高度为 \\(-1\\) 。我们将创建两个工具函数,分别用于获取和更新节点的高度:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def height(self, node: TreeNode | None) -> int:\n    \"\"\"获取节点高度\"\"\"\n    # 空节点高度为 -1 ,叶节点高度为 0\n    if node is not None:\n        return node.height\n    return -1\n\ndef update_height(self, node: TreeNode | None):\n    \"\"\"更新节点高度\"\"\"\n    # 节点高度等于最高子树高度 + 1\n    node.height = max([self.height(node.left), self.height(node.right)]) + 1\n
avl_tree.cpp
/* 获取节点高度 */\nint height(TreeNode *node) {\n    // 空节点高度为 -1 ,叶节点高度为 0\n    return node == nullptr ? -1 : node->height;\n}\n\n/* 更新节点高度 */\nvoid updateHeight(TreeNode *node) {\n    // 节点高度等于最高子树高度 + 1\n    node->height = max(height(node->left), height(node->right)) + 1;\n}\n
avl_tree.java
/* 获取节点高度 */\nint height(TreeNode node) {\n    // 空节点高度为 -1 ,叶节点高度为 0\n    return node == null ? -1 : node.height;\n}\n\n/* 更新节点高度 */\nvoid updateHeight(TreeNode node) {\n    // 节点高度等于最高子树高度 + 1\n    node.height = Math.max(height(node.left), height(node.right)) + 1;\n}\n
avl_tree.cs
/* 获取节点高度 */\nint Height(TreeNode? node) {\n    // 空节点高度为 -1 ,叶节点高度为 0\n    return node == null ? -1 : node.height;\n}\n\n/* 更新节点高度 */\nvoid UpdateHeight(TreeNode node) {\n    // 节点高度等于最高子树高度 + 1\n    node.height = Math.Max(Height(node.left), Height(node.right)) + 1;\n}\n
avl_tree.go
/* 获取节点高度 */\nfunc (t *aVLTree) height(node *TreeNode) int {\n    // 空节点高度为 -1 ,叶节点高度为 0\n    if node != nil {\n        return node.Height\n    }\n    return -1\n}\n\n/* 更新节点高度 */\nfunc (t *aVLTree) updateHeight(node *TreeNode) {\n    lh := t.height(node.Left)\n    rh := t.height(node.Right)\n    // 节点高度等于最高子树高度 + 1\n    if lh > rh {\n        node.Height = lh + 1\n    } else {\n        node.Height = rh + 1\n    }\n}\n
avl_tree.swift
/* 获取节点高度 */\nfunc height(node: TreeNode?) -> Int {\n    // 空节点高度为 -1 ,叶节点高度为 0\n    node?.height ?? -1\n}\n\n/* 更新节点高度 */\nfunc updateHeight(node: TreeNode?) {\n    // 节点高度等于最高子树高度 + 1\n    node?.height = max(height(node: node?.left), height(node: node?.right)) + 1\n}\n
avl_tree.js
/* 获取节点高度 */\nheight(node) {\n    // 空节点高度为 -1 ,叶节点高度为 0\n    return node === null ? -1 : node.height;\n}\n\n/* 更新节点高度 */\n#updateHeight(node) {\n    // 节点高度等于最高子树高度 + 1\n    node.height =\n        Math.max(this.height(node.left), this.height(node.right)) + 1;\n}\n
avl_tree.ts
/* 获取节点高度 */\nheight(node: TreeNode): number {\n    // 空节点高度为 -1 ,叶节点高度为 0\n    return node === null ? -1 : node.height;\n}\n\n/* 更新节点高度 */\nupdateHeight(node: TreeNode): void {\n    // 节点高度等于最高子树高度 + 1\n    node.height =\n        Math.max(this.height(node.left), this.height(node.right)) + 1;\n}\n
avl_tree.dart
/* 获取节点高度 */\nint height(TreeNode? node) {\n  // 空节点高度为 -1 ,叶节点高度为 0\n  return node == null ? -1 : node.height;\n}\n\n/* 更新节点高度 */\nvoid updateHeight(TreeNode? node) {\n  // 节点高度等于最高子树高度 + 1\n  node!.height = max(height(node.left), height(node.right)) + 1;\n}\n
avl_tree.rs
/* 获取节点高度 */\nfn height(node: OptionTreeNodeRc) -> i32 {\n    // 空节点高度为 -1 ,叶节点高度为 0\n    match node {\n        Some(node) => node.borrow().height,\n        None => -1,\n    }\n}\n\n/* 更新节点高度 */\nfn update_height(node: OptionTreeNodeRc) {\n    if let Some(node) = node {\n        let left = node.borrow().left.clone();\n        let right = node.borrow().right.clone();\n        // 节点高度等于最高子树高度 + 1\n        node.borrow_mut().height = std::cmp::max(Self::height(left), Self::height(right)) + 1;\n    }\n}\n
avl_tree.c
/* 获取节点高度 */\nint height(TreeNode *node) {\n    // 空节点高度为 -1 ,叶节点高度为 0\n    if (node != NULL) {\n        return node->height;\n    }\n    return -1;\n}\n\n/* 更新节点高度 */\nvoid updateHeight(TreeNode *node) {\n    int lh = height(node->left);\n    int rh = height(node->right);\n    // 节点高度等于最高子树高度 + 1\n    if (lh > rh) {\n        node->height = lh + 1;\n    } else {\n        node->height = rh + 1;\n    }\n}\n
avl_tree.kt
/* 获取节点高度 */\nfun height(node: TreeNode?): Int {\n    // 空节点高度为 -1 ,叶节点高度为 0\n    return node?.height ?: -1\n}\n\n/* 更新节点高度 */\nfun updateHeight(node: TreeNode?) {\n    // 节点高度等于最高子树高度 + 1\n    node?.height = max(height(node?.left), height(node?.right)) + 1\n}\n
avl_tree.rb
### 获取节点高度 ###\ndef height(node)\n  # 空节点高度为 -1 ,叶节点高度为 0\n  return node.height unless node.nil?\n\n  -1\nend\n\n### 更新节点高度 ###\ndef update_height(node)\n  # 节点高度等于最高子树高度 + 1\n  node.height = [height(node.left), height(node.right)].max + 1\nend\n
","path":["第 7 章   树","7.5   AVL 树 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#2","level":3,"title":"2.   节点平衡因子","text":"

节点的平衡因子(balance factor)定义为节点左子树的高度减去右子树的高度,同时规定空节点的平衡因子为 \\(0\\) 。我们同样将获取节点平衡因子的功能封装成函数,方便后续使用:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def balance_factor(self, node: TreeNode | None) -> int:\n    \"\"\"获取平衡因子\"\"\"\n    # 空节点平衡因子为 0\n    if node is None:\n        return 0\n    # 节点平衡因子 = 左子树高度 - 右子树高度\n    return self.height(node.left) - self.height(node.right)\n
avl_tree.cpp
/* 获取平衡因子 */\nint balanceFactor(TreeNode *node) {\n    // 空节点平衡因子为 0\n    if (node == nullptr)\n        return 0;\n    // 节点平衡因子 = 左子树高度 - 右子树高度\n    return height(node->left) - height(node->right);\n}\n
avl_tree.java
/* 获取平衡因子 */\nint balanceFactor(TreeNode node) {\n    // 空节点平衡因子为 0\n    if (node == null)\n        return 0;\n    // 节点平衡因子 = 左子树高度 - 右子树高度\n    return height(node.left) - height(node.right);\n}\n
avl_tree.cs
/* 获取平衡因子 */\nint BalanceFactor(TreeNode? node) {\n    // 空节点平衡因子为 0\n    if (node == null) return 0;\n    // 节点平衡因子 = 左子树高度 - 右子树高度\n    return Height(node.left) - Height(node.right);\n}\n
avl_tree.go
/* 获取平衡因子 */\nfunc (t *aVLTree) balanceFactor(node *TreeNode) int {\n    // 空节点平衡因子为 0\n    if node == nil {\n        return 0\n    }\n    // 节点平衡因子 = 左子树高度 - 右子树高度\n    return t.height(node.Left) - t.height(node.Right)\n}\n
avl_tree.swift
/* 获取平衡因子 */\nfunc balanceFactor(node: TreeNode?) -> Int {\n    // 空节点平衡因子为 0\n    guard let node = node else { return 0 }\n    // 节点平衡因子 = 左子树高度 - 右子树高度\n    return height(node: node.left) - height(node: node.right)\n}\n
avl_tree.js
/* 获取平衡因子 */\nbalanceFactor(node) {\n    // 空节点平衡因子为 0\n    if (node === null) return 0;\n    // 节点平衡因子 = 左子树高度 - 右子树高度\n    return this.height(node.left) - this.height(node.right);\n}\n
avl_tree.ts
/* 获取平衡因子 */\nbalanceFactor(node: TreeNode): number {\n    // 空节点平衡因子为 0\n    if (node === null) return 0;\n    // 节点平衡因子 = 左子树高度 - 右子树高度\n    return this.height(node.left) - this.height(node.right);\n}\n
avl_tree.dart
/* 获取平衡因子 */\nint balanceFactor(TreeNode? node) {\n  // 空节点平衡因子为 0\n  if (node == null) return 0;\n  // 节点平衡因子 = 左子树高度 - 右子树高度\n  return height(node.left) - height(node.right);\n}\n
avl_tree.rs
/* 获取平衡因子 */\nfn balance_factor(node: OptionTreeNodeRc) -> i32 {\n    match node {\n        // 空节点平衡因子为 0\n        None => 0,\n        // 节点平衡因子 = 左子树高度 - 右子树高度\n        Some(node) => {\n            Self::height(node.borrow().left.clone()) - Self::height(node.borrow().right.clone())\n        }\n    }\n}\n
avl_tree.c
/* 获取平衡因子 */\nint balanceFactor(TreeNode *node) {\n    // 空节点平衡因子为 0\n    if (node == NULL) {\n        return 0;\n    }\n    // 节点平衡因子 = 左子树高度 - 右子树高度\n    return height(node->left) - height(node->right);\n}\n
avl_tree.kt
/* 获取平衡因子 */\nfun balanceFactor(node: TreeNode?): Int {\n    // 空节点平衡因子为 0\n    if (node == null) return 0\n    // 节点平衡因子 = 左子树高度 - 右子树高度\n    return height(node.left) - height(node.right)\n}\n
avl_tree.rb
### 获取平衡因子 ###\ndef balance_factor(node)\n  # 空节点平衡因子为 0\n  return 0 if node.nil?\n\n  # 节点平衡因子 = 左子树高度 - 右子树高度\n  height(node.left) - height(node.right)\nend\n

Tip

设平衡因子为 \\(f\\) ,则一棵 AVL 树的任意节点的平衡因子皆满足 \\(-1 \\le f \\le 1\\) 。

","path":["第 7 章   树","7.5   AVL 树 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#752-avl","level":2,"title":"7.5.2   AVL 树旋转","text":"

AVL 树的特点在于“旋转”操作,它能够在不影响二叉树的中序遍历序列的前提下,使失衡节点重新恢复平衡。换句话说,旋转操作既能保持“二叉搜索树”的性质,也能使树重新变为“平衡二叉树”。

我们将平衡因子绝对值 \\(> 1\\) 的节点称为“失衡节点”。根据节点失衡情况的不同,旋转操作分为四种:右旋、左旋、先右旋后左旋、先左旋后右旋。下面详细介绍这些旋转操作。

","path":["第 7 章   树","7.5   AVL 树 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#1_1","level":3,"title":"1.   右旋","text":"

如图 7-26 所示,节点下方为平衡因子。从底至顶看,二叉树中首个失衡节点是“节点 3”。我们关注以该失衡节点为根节点的子树,将该节点记为 node ,其左子节点记为 child ,执行“右旋”操作。完成右旋后,子树恢复平衡,并且仍然保持二叉搜索树的性质。

<1><2><3><4>

图 7-26   右旋操作步骤

如图 7-27 所示,当节点 child 有右子节点(记为 grand_child )时,需要在右旋中添加一步:将 grand_child 作为 node 的左子节点。

图 7-27   有 grand_child 的右旋操作

“向右旋转”是一种形象化的说法,实际上需要通过修改节点指针来实现,代码如下所示:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def right_rotate(self, node: TreeNode | None) -> TreeNode | None:\n    \"\"\"右旋操作\"\"\"\n    child = node.left\n    grand_child = child.right\n    # 以 child 为原点,将 node 向右旋转\n    child.right = node\n    node.left = grand_child\n    # 更新节点高度\n    self.update_height(node)\n    self.update_height(child)\n    # 返回旋转后子树的根节点\n    return child\n
avl_tree.cpp
/* 右旋操作 */\nTreeNode *rightRotate(TreeNode *node) {\n    TreeNode *child = node->left;\n    TreeNode *grandChild = child->right;\n    // 以 child 为原点,将 node 向右旋转\n    child->right = node;\n    node->left = grandChild;\n    // 更新节点高度\n    updateHeight(node);\n    updateHeight(child);\n    // 返回旋转后子树的根节点\n    return child;\n}\n
avl_tree.java
/* 右旋操作 */\nTreeNode rightRotate(TreeNode node) {\n    TreeNode child = node.left;\n    TreeNode grandChild = child.right;\n    // 以 child 为原点,将 node 向右旋转\n    child.right = node;\n    node.left = grandChild;\n    // 更新节点高度\n    updateHeight(node);\n    updateHeight(child);\n    // 返回旋转后子树的根节点\n    return child;\n}\n
avl_tree.cs
/* 右旋操作 */\nTreeNode? RightRotate(TreeNode? node) {\n    TreeNode? child = node?.left;\n    TreeNode? grandChild = child?.right;\n    // 以 child 为原点,将 node 向右旋转\n    child.right = node;\n    node.left = grandChild;\n    // 更新节点高度\n    UpdateHeight(node);\n    UpdateHeight(child);\n    // 返回旋转后子树的根节点\n    return child;\n}\n
avl_tree.go
/* 右旋操作 */\nfunc (t *aVLTree) rightRotate(node *TreeNode) *TreeNode {\n    child := node.Left\n    grandChild := child.Right\n    // 以 child 为原点,将 node 向右旋转\n    child.Right = node\n    node.Left = grandChild\n    // 更新节点高度\n    t.updateHeight(node)\n    t.updateHeight(child)\n    // 返回旋转后子树的根节点\n    return child\n}\n
avl_tree.swift
/* 右旋操作 */\nfunc rightRotate(node: TreeNode?) -> TreeNode? {\n    let child = node?.left\n    let grandChild = child?.right\n    // 以 child 为原点,将 node 向右旋转\n    child?.right = node\n    node?.left = grandChild\n    // 更新节点高度\n    updateHeight(node: node)\n    updateHeight(node: child)\n    // 返回旋转后子树的根节点\n    return child\n}\n
avl_tree.js
/* 右旋操作 */\n#rightRotate(node) {\n    const child = node.left;\n    const grandChild = child.right;\n    // 以 child 为原点,将 node 向右旋转\n    child.right = node;\n    node.left = grandChild;\n    // 更新节点高度\n    this.#updateHeight(node);\n    this.#updateHeight(child);\n    // 返回旋转后子树的根节点\n    return child;\n}\n
avl_tree.ts
/* 右旋操作 */\nrightRotate(node: TreeNode): TreeNode {\n    const child = node.left;\n    const grandChild = child.right;\n    // 以 child 为原点,将 node 向右旋转\n    child.right = node;\n    node.left = grandChild;\n    // 更新节点高度\n    this.updateHeight(node);\n    this.updateHeight(child);\n    // 返回旋转后子树的根节点\n    return child;\n}\n
avl_tree.dart
/* 右旋操作 */\nTreeNode? rightRotate(TreeNode? node) {\n  TreeNode? child = node!.left;\n  TreeNode? grandChild = child!.right;\n  // 以 child 为原点,将 node 向右旋转\n  child.right = node;\n  node.left = grandChild;\n  // 更新节点高度\n  updateHeight(node);\n  updateHeight(child);\n  // 返回旋转后子树的根节点\n  return child;\n}\n
avl_tree.rs
/* 右旋操作 */\nfn right_rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc {\n    match node {\n        Some(node) => {\n            let child = node.borrow().left.clone().unwrap();\n            let grand_child = child.borrow().right.clone();\n            // 以 child 为原点,将 node 向右旋转\n            child.borrow_mut().right = Some(node.clone());\n            node.borrow_mut().left = grand_child;\n            // 更新节点高度\n            Self::update_height(Some(node));\n            Self::update_height(Some(child.clone()));\n            // 返回旋转后子树的根节点\n            Some(child)\n        }\n        None => None,\n    }\n}\n
avl_tree.c
/* 右旋操作 */\nTreeNode *rightRotate(TreeNode *node) {\n    TreeNode *child, *grandChild;\n    child = node->left;\n    grandChild = child->right;\n    // 以 child 为原点,将 node 向右旋转\n    child->right = node;\n    node->left = grandChild;\n    // 更新节点高度\n    updateHeight(node);\n    updateHeight(child);\n    // 返回旋转后子树的根节点\n    return child;\n}\n
avl_tree.kt
/* 右旋操作 */\nfun rightRotate(node: TreeNode?): TreeNode {\n    val child = node!!.left\n    val grandChild = child!!.right\n    // 以 child 为原点,将 node 向右旋转\n    child.right = node\n    node.left = grandChild\n    // 更新节点高度\n    updateHeight(node)\n    updateHeight(child)\n    // 返回旋转后子树的根节点\n    return child\n}\n
avl_tree.rb
### 右旋操作 ###\ndef right_rotate(node)\n  child = node.left\n  grand_child = child.right\n  # 以 child 为原点,将 node 向右旋转\n  child.right = node\n  node.left = grand_child\n  # 更新节点高度\n  update_height(node)\n  update_height(child)\n  # 返回旋转后子树的根节点\n  child\nend\n
","path":["第 7 章   树","7.5   AVL 树 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#2_1","level":3,"title":"2.   左旋","text":"

相应地,如果考虑上述失衡二叉树的“镜像”,则需要执行图 7-28 所示的“左旋”操作。

图 7-28   左旋操作

同理,如图 7-29 所示,当节点 child 有左子节点(记为 grand_child )时,需要在左旋中添加一步:将 grand_child 作为 node 的右子节点。

图 7-29   有 grand_child 的左旋操作

可以观察到,右旋和左旋操作在逻辑上是镜像对称的,它们分别解决的两种失衡情况也是对称的。基于对称性,我们只需将右旋的实现代码中的所有的 left 替换为 right ,将所有的 right 替换为 left ,即可得到左旋的实现代码:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def left_rotate(self, node: TreeNode | None) -> TreeNode | None:\n    \"\"\"左旋操作\"\"\"\n    child = node.right\n    grand_child = child.left\n    # 以 child 为原点,将 node 向左旋转\n    child.left = node\n    node.right = grand_child\n    # 更新节点高度\n    self.update_height(node)\n    self.update_height(child)\n    # 返回旋转后子树的根节点\n    return child\n
avl_tree.cpp
/* 左旋操作 */\nTreeNode *leftRotate(TreeNode *node) {\n    TreeNode *child = node->right;\n    TreeNode *grandChild = child->left;\n    // 以 child 为原点,将 node 向左旋转\n    child->left = node;\n    node->right = grandChild;\n    // 更新节点高度\n    updateHeight(node);\n    updateHeight(child);\n    // 返回旋转后子树的根节点\n    return child;\n}\n
avl_tree.java
/* 左旋操作 */\nTreeNode leftRotate(TreeNode node) {\n    TreeNode child = node.right;\n    TreeNode grandChild = child.left;\n    // 以 child 为原点,将 node 向左旋转\n    child.left = node;\n    node.right = grandChild;\n    // 更新节点高度\n    updateHeight(node);\n    updateHeight(child);\n    // 返回旋转后子树的根节点\n    return child;\n}\n
avl_tree.cs
/* 左旋操作 */\nTreeNode? LeftRotate(TreeNode? node) {\n    TreeNode? child = node?.right;\n    TreeNode? grandChild = child?.left;\n    // 以 child 为原点,将 node 向左旋转\n    child.left = node;\n    node.right = grandChild;\n    // 更新节点高度\n    UpdateHeight(node);\n    UpdateHeight(child);\n    // 返回旋转后子树的根节点\n    return child;\n}\n
avl_tree.go
/* 左旋操作 */\nfunc (t *aVLTree) leftRotate(node *TreeNode) *TreeNode {\n    child := node.Right\n    grandChild := child.Left\n    // 以 child 为原点,将 node 向左旋转\n    child.Left = node\n    node.Right = grandChild\n    // 更新节点高度\n    t.updateHeight(node)\n    t.updateHeight(child)\n    // 返回旋转后子树的根节点\n    return child\n}\n
avl_tree.swift
/* 左旋操作 */\nfunc leftRotate(node: TreeNode?) -> TreeNode? {\n    let child = node?.right\n    let grandChild = child?.left\n    // 以 child 为原点,将 node 向左旋转\n    child?.left = node\n    node?.right = grandChild\n    // 更新节点高度\n    updateHeight(node: node)\n    updateHeight(node: child)\n    // 返回旋转后子树的根节点\n    return child\n}\n
avl_tree.js
/* 左旋操作 */\n#leftRotate(node) {\n    const child = node.right;\n    const grandChild = child.left;\n    // 以 child 为原点,将 node 向左旋转\n    child.left = node;\n    node.right = grandChild;\n    // 更新节点高度\n    this.#updateHeight(node);\n    this.#updateHeight(child);\n    // 返回旋转后子树的根节点\n    return child;\n}\n
avl_tree.ts
/* 左旋操作 */\nleftRotate(node: TreeNode): TreeNode {\n    const child = node.right;\n    const grandChild = child.left;\n    // 以 child 为原点,将 node 向左旋转\n    child.left = node;\n    node.right = grandChild;\n    // 更新节点高度\n    this.updateHeight(node);\n    this.updateHeight(child);\n    // 返回旋转后子树的根节点\n    return child;\n}\n
avl_tree.dart
/* 左旋操作 */\nTreeNode? leftRotate(TreeNode? node) {\n  TreeNode? child = node!.right;\n  TreeNode? grandChild = child!.left;\n  // 以 child 为原点,将 node 向左旋转\n  child.left = node;\n  node.right = grandChild;\n  // 更新节点高度\n  updateHeight(node);\n  updateHeight(child);\n  // 返回旋转后子树的根节点\n  return child;\n}\n
avl_tree.rs
/* 左旋操作 */\nfn left_rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc {\n    match node {\n        Some(node) => {\n            let child = node.borrow().right.clone().unwrap();\n            let grand_child = child.borrow().left.clone();\n            // 以 child 为原点,将 node 向左旋转\n            child.borrow_mut().left = Some(node.clone());\n            node.borrow_mut().right = grand_child;\n            // 更新节点高度\n            Self::update_height(Some(node));\n            Self::update_height(Some(child.clone()));\n            // 返回旋转后子树的根节点\n            Some(child)\n        }\n        None => None,\n    }\n}\n
avl_tree.c
/* 左旋操作 */\nTreeNode *leftRotate(TreeNode *node) {\n    TreeNode *child, *grandChild;\n    child = node->right;\n    grandChild = child->left;\n    // 以 child 为原点,将 node 向左旋转\n    child->left = node;\n    node->right = grandChild;\n    // 更新节点高度\n    updateHeight(node);\n    updateHeight(child);\n    // 返回旋转后子树的根节点\n    return child;\n}\n
avl_tree.kt
/* 左旋操作 */\nfun leftRotate(node: TreeNode?): TreeNode {\n    val child = node!!.right\n    val grandChild = child!!.left\n    // 以 child 为原点,将 node 向左旋转\n    child.left = node\n    node.right = grandChild\n    // 更新节点高度\n    updateHeight(node)\n    updateHeight(child)\n    // 返回旋转后子树的根节点\n    return child\n}\n
avl_tree.rb
### 左旋操作 ###\ndef left_rotate(node)\n  child = node.right\n  grand_child = child.left\n  # 以 child 为原点,将 node 向左旋转\n  child.left = node\n  node.right = grand_child\n  # 更新节点高度\n  update_height(node)\n  update_height(child)\n  # 返回旋转后子树的根节点\n  child\nend\n
","path":["第 7 章   树","7.5   AVL 树 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#3","level":3,"title":"3.   先左旋后右旋","text":"

对于图 7-30 中的失衡节点 3 ,仅使用左旋或右旋都无法使子树恢复平衡。此时需要先对 child 执行“左旋”,再对 node 执行“右旋”。

图 7-30   先左旋后右旋

","path":["第 7 章   树","7.5   AVL 树 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#4","level":3,"title":"4.   先右旋后左旋","text":"

如图 7-31 所示,对于上述失衡二叉树的镜像情况,需要先对 child 执行“右旋”,再对 node 执行“左旋”。

图 7-31   先右旋后左旋

","path":["第 7 章   树","7.5   AVL 树 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#5","level":3,"title":"5.   旋转的选择","text":"

图 7-32 展示的四种失衡情况与上述案例逐个对应,分别需要采用右旋、先左旋后右旋、先右旋后左旋、左旋的操作。

图 7-32   AVL 树的四种旋转情况

如下表所示,我们通过判断失衡节点的平衡因子以及较高一侧子节点的平衡因子的正负号,来确定失衡节点属于图 7-32 中的哪种情况。

表 7-3   四种旋转情况的选择条件

失衡节点的平衡因子 子节点的平衡因子 应采用的旋转方法 \\(> 1\\) (左偏树) \\(\\geq 0\\) 右旋 \\(> 1\\) (左偏树) \\(<0\\) 先左旋后右旋 \\(< -1\\) (右偏树) \\(\\leq 0\\) 左旋 \\(< -1\\) (右偏树) \\(>0\\) 先右旋后左旋

为了便于使用,我们将旋转操作封装成一个函数。有了这个函数,我们就能对各种失衡情况进行旋转,使失衡节点重新恢复平衡。代码如下所示:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def rotate(self, node: TreeNode | None) -> TreeNode | None:\n    \"\"\"执行旋转操作,使该子树重新恢复平衡\"\"\"\n    # 获取节点 node 的平衡因子\n    balance_factor = self.balance_factor(node)\n    # 左偏树\n    if balance_factor > 1:\n        if self.balance_factor(node.left) >= 0:\n            # 右旋\n            return self.right_rotate(node)\n        else:\n            # 先左旋后右旋\n            node.left = self.left_rotate(node.left)\n            return self.right_rotate(node)\n    # 右偏树\n    elif balance_factor < -1:\n        if self.balance_factor(node.right) <= 0:\n            # 左旋\n            return self.left_rotate(node)\n        else:\n            # 先右旋后左旋\n            node.right = self.right_rotate(node.right)\n            return self.left_rotate(node)\n    # 平衡树,无须旋转,直接返回\n    return node\n
avl_tree.cpp
/* 执行旋转操作,使该子树重新恢复平衡 */\nTreeNode *rotate(TreeNode *node) {\n    // 获取节点 node 的平衡因子\n    int _balanceFactor = balanceFactor(node);\n    // 左偏树\n    if (_balanceFactor > 1) {\n        if (balanceFactor(node->left) >= 0) {\n            // 右旋\n            return rightRotate(node);\n        } else {\n            // 先左旋后右旋\n            node->left = leftRotate(node->left);\n            return rightRotate(node);\n        }\n    }\n    // 右偏树\n    if (_balanceFactor < -1) {\n        if (balanceFactor(node->right) <= 0) {\n            // 左旋\n            return leftRotate(node);\n        } else {\n            // 先右旋后左旋\n            node->right = rightRotate(node->right);\n            return leftRotate(node);\n        }\n    }\n    // 平衡树,无须旋转,直接返回\n    return node;\n}\n
avl_tree.java
/* 执行旋转操作,使该子树重新恢复平衡 */\nTreeNode rotate(TreeNode node) {\n    // 获取节点 node 的平衡因子\n    int balanceFactor = balanceFactor(node);\n    // 左偏树\n    if (balanceFactor > 1) {\n        if (balanceFactor(node.left) >= 0) {\n            // 右旋\n            return rightRotate(node);\n        } else {\n            // 先左旋后右旋\n            node.left = leftRotate(node.left);\n            return rightRotate(node);\n        }\n    }\n    // 右偏树\n    if (balanceFactor < -1) {\n        if (balanceFactor(node.right) <= 0) {\n            // 左旋\n            return leftRotate(node);\n        } else {\n            // 先右旋后左旋\n            node.right = rightRotate(node.right);\n            return leftRotate(node);\n        }\n    }\n    // 平衡树,无须旋转,直接返回\n    return node;\n}\n
avl_tree.cs
/* 执行旋转操作,使该子树重新恢复平衡 */\nTreeNode? Rotate(TreeNode? node) {\n    // 获取节点 node 的平衡因子\n    int balanceFactorInt = BalanceFactor(node);\n    // 左偏树\n    if (balanceFactorInt > 1) {\n        if (BalanceFactor(node?.left) >= 0) {\n            // 右旋\n            return RightRotate(node);\n        } else {\n            // 先左旋后右旋\n            node!.left = LeftRotate(node!.left);\n            return RightRotate(node);\n        }\n    }\n    // 右偏树\n    if (balanceFactorInt < -1) {\n        if (BalanceFactor(node?.right) <= 0) {\n            // 左旋\n            return LeftRotate(node);\n        } else {\n            // 先右旋后左旋\n            node!.right = RightRotate(node!.right);\n            return LeftRotate(node);\n        }\n    }\n    // 平衡树,无须旋转,直接返回\n    return node;\n}\n
avl_tree.go
/* 执行旋转操作,使该子树重新恢复平衡 */\nfunc (t *aVLTree) rotate(node *TreeNode) *TreeNode {\n    // 获取节点 node 的平衡因子\n    // Go 推荐短变量,这里 bf 指代 t.balanceFactor\n    bf := t.balanceFactor(node)\n    // 左偏树\n    if bf > 1 {\n        if t.balanceFactor(node.Left) >= 0 {\n            // 右旋\n            return t.rightRotate(node)\n        } else {\n            // 先左旋后右旋\n            node.Left = t.leftRotate(node.Left)\n            return t.rightRotate(node)\n        }\n    }\n    // 右偏树\n    if bf < -1 {\n        if t.balanceFactor(node.Right) <= 0 {\n            // 左旋\n            return t.leftRotate(node)\n        } else {\n            // 先右旋后左旋\n            node.Right = t.rightRotate(node.Right)\n            return t.leftRotate(node)\n        }\n    }\n    // 平衡树,无须旋转,直接返回\n    return node\n}\n
avl_tree.swift
/* 执行旋转操作,使该子树重新恢复平衡 */\nfunc rotate(node: TreeNode?) -> TreeNode? {\n    // 获取节点 node 的平衡因子\n    let balanceFactor = balanceFactor(node: node)\n    // 左偏树\n    if balanceFactor > 1 {\n        if self.balanceFactor(node: node?.left) >= 0 {\n            // 右旋\n            return rightRotate(node: node)\n        } else {\n            // 先左旋后右旋\n            node?.left = leftRotate(node: node?.left)\n            return rightRotate(node: node)\n        }\n    }\n    // 右偏树\n    if balanceFactor < -1 {\n        if self.balanceFactor(node: node?.right) <= 0 {\n            // 左旋\n            return leftRotate(node: node)\n        } else {\n            // 先右旋后左旋\n            node?.right = rightRotate(node: node?.right)\n            return leftRotate(node: node)\n        }\n    }\n    // 平衡树,无须旋转,直接返回\n    return node\n}\n
avl_tree.js
/* 执行旋转操作,使该子树重新恢复平衡 */\n#rotate(node) {\n    // 获取节点 node 的平衡因子\n    const balanceFactor = this.balanceFactor(node);\n    // 左偏树\n    if (balanceFactor > 1) {\n        if (this.balanceFactor(node.left) >= 0) {\n            // 右旋\n            return this.#rightRotate(node);\n        } else {\n            // 先左旋后右旋\n            node.left = this.#leftRotate(node.left);\n            return this.#rightRotate(node);\n        }\n    }\n    // 右偏树\n    if (balanceFactor < -1) {\n        if (this.balanceFactor(node.right) <= 0) {\n            // 左旋\n            return this.#leftRotate(node);\n        } else {\n            // 先右旋后左旋\n            node.right = this.#rightRotate(node.right);\n            return this.#leftRotate(node);\n        }\n    }\n    // 平衡树,无须旋转,直接返回\n    return node;\n}\n
avl_tree.ts
/* 执行旋转操作,使该子树重新恢复平衡 */\nrotate(node: TreeNode): TreeNode {\n    // 获取节点 node 的平衡因子\n    const balanceFactor = this.balanceFactor(node);\n    // 左偏树\n    if (balanceFactor > 1) {\n        if (this.balanceFactor(node.left) >= 0) {\n            // 右旋\n            return this.rightRotate(node);\n        } else {\n            // 先左旋后右旋\n            node.left = this.leftRotate(node.left);\n            return this.rightRotate(node);\n        }\n    }\n    // 右偏树\n    if (balanceFactor < -1) {\n        if (this.balanceFactor(node.right) <= 0) {\n            // 左旋\n            return this.leftRotate(node);\n        } else {\n            // 先右旋后左旋\n            node.right = this.rightRotate(node.right);\n            return this.leftRotate(node);\n        }\n    }\n    // 平衡树,无须旋转,直接返回\n    return node;\n}\n
avl_tree.dart
/* 执行旋转操作,使该子树重新恢复平衡 */\nTreeNode? rotate(TreeNode? node) {\n  // 获取节点 node 的平衡因子\n  int factor = balanceFactor(node);\n  // 左偏树\n  if (factor > 1) {\n    if (balanceFactor(node!.left) >= 0) {\n      // 右旋\n      return rightRotate(node);\n    } else {\n      // 先左旋后右旋\n      node.left = leftRotate(node.left);\n      return rightRotate(node);\n    }\n  }\n  // 右偏树\n  if (factor < -1) {\n    if (balanceFactor(node!.right) <= 0) {\n      // 左旋\n      return leftRotate(node);\n    } else {\n      // 先右旋后左旋\n      node.right = rightRotate(node.right);\n      return leftRotate(node);\n    }\n  }\n  // 平衡树,无须旋转,直接返回\n  return node;\n}\n
avl_tree.rs
/* 执行旋转操作,使该子树重新恢复平衡 */\nfn rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc {\n    // 获取节点 node 的平衡因子\n    let balance_factor = Self::balance_factor(node.clone());\n    // 左偏树\n    if balance_factor > 1 {\n        let node = node.unwrap();\n        if Self::balance_factor(node.borrow().left.clone()) >= 0 {\n            // 右旋\n            Self::right_rotate(Some(node))\n        } else {\n            // 先左旋后右旋\n            let left = node.borrow().left.clone();\n            node.borrow_mut().left = Self::left_rotate(left);\n            Self::right_rotate(Some(node))\n        }\n    }\n    // 右偏树\n    else if balance_factor < -1 {\n        let node = node.unwrap();\n        if Self::balance_factor(node.borrow().right.clone()) <= 0 {\n            // 左旋\n            Self::left_rotate(Some(node))\n        } else {\n            // 先右旋后左旋\n            let right = node.borrow().right.clone();\n            node.borrow_mut().right = Self::right_rotate(right);\n            Self::left_rotate(Some(node))\n        }\n    } else {\n        // 平衡树,无须旋转,直接返回\n        node\n    }\n}\n
avl_tree.c
/* 执行旋转操作,使该子树重新恢复平衡 */\nTreeNode *rotate(TreeNode *node) {\n    // 获取节点 node 的平衡因子\n    int bf = balanceFactor(node);\n    // 左偏树\n    if (bf > 1) {\n        if (balanceFactor(node->left) >= 0) {\n            // 右旋\n            return rightRotate(node);\n        } else {\n            // 先左旋后右旋\n            node->left = leftRotate(node->left);\n            return rightRotate(node);\n        }\n    }\n    // 右偏树\n    if (bf < -1) {\n        if (balanceFactor(node->right) <= 0) {\n            // 左旋\n            return leftRotate(node);\n        } else {\n            // 先右旋后左旋\n            node->right = rightRotate(node->right);\n            return leftRotate(node);\n        }\n    }\n    // 平衡树,无须旋转,直接返回\n    return node;\n}\n
avl_tree.kt
/* 执行旋转操作,使该子树重新恢复平衡 */\nfun rotate(node: TreeNode): TreeNode {\n    // 获取节点 node 的平衡因子\n    val balanceFactor = balanceFactor(node)\n    // 左偏树\n    if (balanceFactor > 1) {\n        if (balanceFactor(node.left) >= 0) {\n            // 右旋\n            return rightRotate(node)\n        } else {\n            // 先左旋后右旋\n            node.left = leftRotate(node.left)\n            return rightRotate(node)\n        }\n    }\n    // 右偏树\n    if (balanceFactor < -1) {\n        if (balanceFactor(node.right) <= 0) {\n            // 左旋\n            return leftRotate(node)\n        } else {\n            // 先右旋后左旋\n            node.right = rightRotate(node.right)\n            return leftRotate(node)\n        }\n    }\n    // 平衡树,无须旋转,直接返回\n    return node\n}\n
avl_tree.rb
### 执行旋转操作,使该子树重新恢复平衡 ###\ndef rotate(node)\n  # 获取节点 node 的平衡因子\n  balance_factor = balance_factor(node)\n  # 左遍树\n  if balance_factor > 1\n    if balance_factor(node.left) >= 0\n      # 右旋\n      return right_rotate(node)\n    else\n      # 先左旋后右旋\n      node.left = left_rotate(node.left)\n      return right_rotate(node)\n    end\n  # 右遍树\n  elsif balance_factor < -1\n    if balance_factor(node.right) <= 0\n      # 左旋\n      return left_rotate(node)\n    else\n      # 先右旋后左旋\n      node.right = right_rotate(node.right)\n      return left_rotate(node)\n    end\n  end\n  # 平衡树,无须旋转,直接返回\n  node\nend\n
","path":["第 7 章   树","7.5   AVL 树 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#753-avl","level":2,"title":"7.5.3   AVL 树常用操作","text":"","path":["第 7 章   树","7.5   AVL 树 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#1_2","level":3,"title":"1.   插入节点","text":"

AVL 树的节点插入操作与二叉搜索树在主体上类似。唯一的区别在于,在 AVL 树中插入节点后,从该节点到根节点的路径上可能会出现一系列失衡节点。因此,我们需要从这个节点开始,自底向上执行旋转操作,使所有失衡节点恢复平衡。代码如下所示:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def insert(self, val):\n    \"\"\"插入节点\"\"\"\n    self._root = self.insert_helper(self._root, val)\n\ndef insert_helper(self, node: TreeNode | None, val: int) -> TreeNode:\n    \"\"\"递归插入节点(辅助方法)\"\"\"\n    if node is None:\n        return TreeNode(val)\n    # 1. 查找插入位置并插入节点\n    if val < node.val:\n        node.left = self.insert_helper(node.left, val)\n    elif val > node.val:\n        node.right = self.insert_helper(node.right, val)\n    else:\n        # 重复节点不插入,直接返回\n        return node\n    # 更新节点高度\n    self.update_height(node)\n    # 2. 执行旋转操作,使该子树重新恢复平衡\n    return self.rotate(node)\n
avl_tree.cpp
/* 插入节点 */\nvoid insert(int val) {\n    root = insertHelper(root, val);\n}\n\n/* 递归插入节点(辅助方法) */\nTreeNode *insertHelper(TreeNode *node, int val) {\n    if (node == nullptr)\n        return new TreeNode(val);\n    /* 1. 查找插入位置并插入节点 */\n    if (val < node->val)\n        node->left = insertHelper(node->left, val);\n    else if (val > node->val)\n        node->right = insertHelper(node->right, val);\n    else\n        return node;    // 重复节点不插入,直接返回\n    updateHeight(node); // 更新节点高度\n    /* 2. 执行旋转操作,使该子树重新恢复平衡 */\n    node = rotate(node);\n    // 返回子树的根节点\n    return node;\n}\n
avl_tree.java
/* 插入节点 */\nvoid insert(int val) {\n    root = insertHelper(root, val);\n}\n\n/* 递归插入节点(辅助方法) */\nTreeNode insertHelper(TreeNode node, int val) {\n    if (node == null)\n        return new TreeNode(val);\n    /* 1. 查找插入位置并插入节点 */\n    if (val < node.val)\n        node.left = insertHelper(node.left, val);\n    else if (val > node.val)\n        node.right = insertHelper(node.right, val);\n    else\n        return node; // 重复节点不插入,直接返回\n    updateHeight(node); // 更新节点高度\n    /* 2. 执行旋转操作,使该子树重新恢复平衡 */\n    node = rotate(node);\n    // 返回子树的根节点\n    return node;\n}\n
avl_tree.cs
/* 插入节点 */\nvoid Insert(int val) {\n    root = InsertHelper(root, val);\n}\n\n/* 递归插入节点(辅助方法) */\nTreeNode? InsertHelper(TreeNode? node, int val) {\n    if (node == null) return new TreeNode(val);\n    /* 1. 查找插入位置并插入节点 */\n    if (val < node.val)\n        node.left = InsertHelper(node.left, val);\n    else if (val > node.val)\n        node.right = InsertHelper(node.right, val);\n    else\n        return node;     // 重复节点不插入,直接返回\n    UpdateHeight(node);  // 更新节点高度\n    /* 2. 执行旋转操作,使该子树重新恢复平衡 */\n    node = Rotate(node);\n    // 返回子树的根节点\n    return node;\n}\n
avl_tree.go
/* 插入节点 */\nfunc (t *aVLTree) insert(val int) {\n    t.root = t.insertHelper(t.root, val)\n}\n\n/* 递归插入节点(辅助函数) */\nfunc (t *aVLTree) insertHelper(node *TreeNode, val int) *TreeNode {\n    if node == nil {\n        return NewTreeNode(val)\n    }\n    /* 1. 查找插入位置并插入节点 */\n    if val < node.Val.(int) {\n        node.Left = t.insertHelper(node.Left, val)\n    } else if val > node.Val.(int) {\n        node.Right = t.insertHelper(node.Right, val)\n    } else {\n        // 重复节点不插入,直接返回\n        return node\n    }\n    // 更新节点高度\n    t.updateHeight(node)\n    /* 2. 执行旋转操作,使该子树重新恢复平衡 */\n    node = t.rotate(node)\n    // 返回子树的根节点\n    return node\n}\n
avl_tree.swift
/* 插入节点 */\nfunc insert(val: Int) {\n    root = insertHelper(node: root, val: val)\n}\n\n/* 递归插入节点(辅助方法) */\nfunc insertHelper(node: TreeNode?, val: Int) -> TreeNode? {\n    var node = node\n    if node == nil {\n        return TreeNode(x: val)\n    }\n    /* 1. 查找插入位置并插入节点 */\n    if val < node!.val {\n        node?.left = insertHelper(node: node?.left, val: val)\n    } else if val > node!.val {\n        node?.right = insertHelper(node: node?.right, val: val)\n    } else {\n        return node // 重复节点不插入,直接返回\n    }\n    updateHeight(node: node) // 更新节点高度\n    /* 2. 执行旋转操作,使该子树重新恢复平衡 */\n    node = rotate(node: node)\n    // 返回子树的根节点\n    return node\n}\n
avl_tree.js
/* 插入节点 */\ninsert(val) {\n    this.root = this.#insertHelper(this.root, val);\n}\n\n/* 递归插入节点(辅助方法) */\n#insertHelper(node, val) {\n    if (node === null) return new TreeNode(val);\n    /* 1. 查找插入位置并插入节点 */\n    if (val < node.val) node.left = this.#insertHelper(node.left, val);\n    else if (val > node.val)\n        node.right = this.#insertHelper(node.right, val);\n    else return node; // 重复节点不插入,直接返回\n    this.#updateHeight(node); // 更新节点高度\n    /* 2. 执行旋转操作,使该子树重新恢复平衡 */\n    node = this.#rotate(node);\n    // 返回子树的根节点\n    return node;\n}\n
avl_tree.ts
/* 插入节点 */\ninsert(val: number): void {\n    this.root = this.insertHelper(this.root, val);\n}\n\n/* 递归插入节点(辅助方法) */\ninsertHelper(node: TreeNode, val: number): TreeNode {\n    if (node === null) return new TreeNode(val);\n    /* 1. 查找插入位置并插入节点 */\n    if (val < node.val) {\n        node.left = this.insertHelper(node.left, val);\n    } else if (val > node.val) {\n        node.right = this.insertHelper(node.right, val);\n    } else {\n        return node; // 重复节点不插入,直接返回\n    }\n    this.updateHeight(node); // 更新节点高度\n    /* 2. 执行旋转操作,使该子树重新恢复平衡 */\n    node = this.rotate(node);\n    // 返回子树的根节点\n    return node;\n}\n
avl_tree.dart
/* 插入节点 */\nvoid insert(int val) {\n  root = insertHelper(root, val);\n}\n\n/* 递归插入节点(辅助方法) */\nTreeNode? insertHelper(TreeNode? node, int val) {\n  if (node == null) return TreeNode(val);\n  /* 1. 查找插入位置并插入节点 */\n  if (val < node.val)\n    node.left = insertHelper(node.left, val);\n  else if (val > node.val)\n    node.right = insertHelper(node.right, val);\n  else\n    return node; // 重复节点不插入,直接返回\n  updateHeight(node); // 更新节点高度\n  /* 2. 执行旋转操作,使该子树重新恢复平衡 */\n  node = rotate(node);\n  // 返回子树的根节点\n  return node;\n}\n
avl_tree.rs
/* 插入节点 */\nfn insert(&mut self, val: i32) {\n    self.root = Self::insert_helper(self.root.clone(), val);\n}\n\n/* 递归插入节点(辅助方法) */\nfn insert_helper(node: OptionTreeNodeRc, val: i32) -> OptionTreeNodeRc {\n    match node {\n        Some(mut node) => {\n            /* 1. 查找插入位置并插入节点 */\n            match {\n                let node_val = node.borrow().val;\n                node_val\n            }\n            .cmp(&val)\n            {\n                Ordering::Greater => {\n                    let left = node.borrow().left.clone();\n                    node.borrow_mut().left = Self::insert_helper(left, val);\n                }\n                Ordering::Less => {\n                    let right = node.borrow().right.clone();\n                    node.borrow_mut().right = Self::insert_helper(right, val);\n                }\n                Ordering::Equal => {\n                    return Some(node); // 重复节点不插入,直接返回\n                }\n            }\n            Self::update_height(Some(node.clone())); // 更新节点高度\n\n            /* 2. 执行旋转操作,使该子树重新恢复平衡 */\n            node = Self::rotate(Some(node)).unwrap();\n            // 返回子树的根节点\n            Some(node)\n        }\n        None => Some(TreeNode::new(val)),\n    }\n}\n
avl_tree.c
/* 插入节点 */\nvoid insert(AVLTree *tree, int val) {\n    tree->root = insertHelper(tree->root, val);\n}\n\n/* 递归插入节点(辅助函数) */\nTreeNode *insertHelper(TreeNode *node, int val) {\n    if (node == NULL) {\n        return newTreeNode(val);\n    }\n    /* 1. 查找插入位置并插入节点 */\n    if (val < node->val) {\n        node->left = insertHelper(node->left, val);\n    } else if (val > node->val) {\n        node->right = insertHelper(node->right, val);\n    } else {\n        // 重复节点不插入,直接返回\n        return node;\n    }\n    // 更新节点高度\n    updateHeight(node);\n    /* 2. 执行旋转操作,使该子树重新恢复平衡 */\n    node = rotate(node);\n    // 返回子树的根节点\n    return node;\n}\n
avl_tree.kt
/* 插入节点 */\nfun insert(_val: Int) {\n    root = insertHelper(root, _val)\n}\n\n/* 递归插入节点(辅助方法) */\nfun insertHelper(n: TreeNode?, _val: Int): TreeNode {\n    if (n == null)\n        return TreeNode(_val)\n    var node = n\n    /* 1. 查找插入位置并插入节点 */\n    if (_val < node._val)\n        node.left = insertHelper(node.left, _val)\n    else if (_val > node._val)\n        node.right = insertHelper(node.right, _val)\n    else\n        return node // 重复节点不插入,直接返回\n    updateHeight(node) // 更新节点高度\n    /* 2. 执行旋转操作,使该子树重新恢复平衡 */\n    node = rotate(node)\n    // 返回子树的根节点\n    return node\n}\n
avl_tree.rb
### 插入节点 ###\ndef insert(val)\n  @root = insert_helper(@root, val)\nend\n\n### 递归插入节点(辅助方法)###\ndef insert_helper(node, val)\n  return TreeNode.new(val) if node.nil?\n  # 1. 查找插入位置并插入节点\n  if val < node.val\n    node.left = insert_helper(node.left, val)\n  elsif val > node.val\n    node.right = insert_helper(node.right, val)\n  else\n    # 重复节点不插入,直接返回\n    return node\n  end\n  # 更新节点高度\n  update_height(node)\n  # 2. 执行旋转操作,使该子树重新恢复平衡\n  rotate(node)\nend\n
","path":["第 7 章   树","7.5   AVL 树 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#2_2","level":3,"title":"2.   删除节点","text":"

类似地,在二叉搜索树的删除节点方法的基础上,需要从底至顶执行旋转操作,使所有失衡节点恢复平衡。代码如下所示:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby avl_tree.py
def remove(self, val: int):\n    \"\"\"删除节点\"\"\"\n    self._root = self.remove_helper(self._root, val)\n\ndef remove_helper(self, node: TreeNode | None, val: int) -> TreeNode | None:\n    \"\"\"递归删除节点(辅助方法)\"\"\"\n    if node is None:\n        return None\n    # 1. 查找节点并删除\n    if val < node.val:\n        node.left = self.remove_helper(node.left, val)\n    elif val > node.val:\n        node.right = self.remove_helper(node.right, val)\n    else:\n        if node.left is None or node.right is None:\n            child = node.left or node.right\n            # 子节点数量 = 0 ,直接删除 node 并返回\n            if child is None:\n                return None\n            # 子节点数量 = 1 ,直接删除 node\n            else:\n                node = child\n        else:\n            # 子节点数量 = 2 ,则将中序遍历的下个节点删除,并用该节点替换当前节点\n            temp = node.right\n            while temp.left is not None:\n                temp = temp.left\n            node.right = self.remove_helper(node.right, temp.val)\n            node.val = temp.val\n    # 更新节点高度\n    self.update_height(node)\n    # 2. 执行旋转操作,使该子树重新恢复平衡\n    return self.rotate(node)\n
avl_tree.cpp
/* 删除节点 */\nvoid remove(int val) {\n    root = removeHelper(root, val);\n}\n\n/* 递归删除节点(辅助方法) */\nTreeNode *removeHelper(TreeNode *node, int val) {\n    if (node == nullptr)\n        return nullptr;\n    /* 1. 查找节点并删除 */\n    if (val < node->val)\n        node->left = removeHelper(node->left, val);\n    else if (val > node->val)\n        node->right = removeHelper(node->right, val);\n    else {\n        if (node->left == nullptr || node->right == nullptr) {\n            TreeNode *child = node->left != nullptr ? node->left : node->right;\n            // 子节点数量 = 0 ,直接删除 node 并返回\n            if (child == nullptr) {\n                delete node;\n                return nullptr;\n            }\n            // 子节点数量 = 1 ,直接删除 node\n            else {\n                delete node;\n                node = child;\n            }\n        } else {\n            // 子节点数量 = 2 ,则将中序遍历的下个节点删除,并用该节点替换当前节点\n            TreeNode *temp = node->right;\n            while (temp->left != nullptr) {\n                temp = temp->left;\n            }\n            int tempVal = temp->val;\n            node->right = removeHelper(node->right, temp->val);\n            node->val = tempVal;\n        }\n    }\n    updateHeight(node); // 更新节点高度\n    /* 2. 执行旋转操作,使该子树重新恢复平衡 */\n    node = rotate(node);\n    // 返回子树的根节点\n    return node;\n}\n
avl_tree.java
/* 删除节点 */\nvoid remove(int val) {\n    root = removeHelper(root, val);\n}\n\n/* 递归删除节点(辅助方法) */\nTreeNode removeHelper(TreeNode node, int val) {\n    if (node == null)\n        return null;\n    /* 1. 查找节点并删除 */\n    if (val < node.val)\n        node.left = removeHelper(node.left, val);\n    else if (val > node.val)\n        node.right = removeHelper(node.right, val);\n    else {\n        if (node.left == null || node.right == null) {\n            TreeNode child = node.left != null ? node.left : node.right;\n            // 子节点数量 = 0 ,直接删除 node 并返回\n            if (child == null)\n                return null;\n            // 子节点数量 = 1 ,直接删除 node\n            else\n                node = child;\n        } else {\n            // 子节点数量 = 2 ,则将中序遍历的下个节点删除,并用该节点替换当前节点\n            TreeNode temp = node.right;\n            while (temp.left != null) {\n                temp = temp.left;\n            }\n            node.right = removeHelper(node.right, temp.val);\n            node.val = temp.val;\n        }\n    }\n    updateHeight(node); // 更新节点高度\n    /* 2. 执行旋转操作,使该子树重新恢复平衡 */\n    node = rotate(node);\n    // 返回子树的根节点\n    return node;\n}\n
avl_tree.cs
/* 删除节点 */\nvoid Remove(int val) {\n    root = RemoveHelper(root, val);\n}\n\n/* 递归删除节点(辅助方法) */\nTreeNode? RemoveHelper(TreeNode? node, int val) {\n    if (node == null) return null;\n    /* 1. 查找节点并删除 */\n    if (val < node.val)\n        node.left = RemoveHelper(node.left, val);\n    else if (val > node.val)\n        node.right = RemoveHelper(node.right, val);\n    else {\n        if (node.left == null || node.right == null) {\n            TreeNode? child = node.left ?? node.right;\n            // 子节点数量 = 0 ,直接删除 node 并返回\n            if (child == null)\n                return null;\n            // 子节点数量 = 1 ,直接删除 node\n            else\n                node = child;\n        } else {\n            // 子节点数量 = 2 ,则将中序遍历的下个节点删除,并用该节点替换当前节点\n            TreeNode? temp = node.right;\n            while (temp.left != null) {\n                temp = temp.left;\n            }\n            node.right = RemoveHelper(node.right, temp.val!.Value);\n            node.val = temp.val;\n        }\n    }\n    UpdateHeight(node);  // 更新节点高度\n    /* 2. 执行旋转操作,使该子树重新恢复平衡 */\n    node = Rotate(node);\n    // 返回子树的根节点\n    return node;\n}\n
avl_tree.go
/* 删除节点 */\nfunc (t *aVLTree) remove(val int) {\n    t.root = t.removeHelper(t.root, val)\n}\n\n/* 递归删除节点(辅助函数) */\nfunc (t *aVLTree) removeHelper(node *TreeNode, val int) *TreeNode {\n    if node == nil {\n        return nil\n    }\n    /* 1. 查找节点并删除 */\n    if val < node.Val.(int) {\n        node.Left = t.removeHelper(node.Left, val)\n    } else if val > node.Val.(int) {\n        node.Right = t.removeHelper(node.Right, val)\n    } else {\n        if node.Left == nil || node.Right == nil {\n            child := node.Left\n            if node.Right != nil {\n                child = node.Right\n            }\n            if child == nil {\n                // 子节点数量 = 0 ,直接删除 node 并返回\n                return nil\n            } else {\n                // 子节点数量 = 1 ,直接删除 node\n                node = child\n            }\n        } else {\n            // 子节点数量 = 2 ,则将中序遍历的下个节点删除,并用该节点替换当前节点\n            temp := node.Right\n            for temp.Left != nil {\n                temp = temp.Left\n            }\n            node.Right = t.removeHelper(node.Right, temp.Val.(int))\n            node.Val = temp.Val\n        }\n    }\n    // 更新节点高度\n    t.updateHeight(node)\n    /* 2. 执行旋转操作,使该子树重新恢复平衡 */\n    node = t.rotate(node)\n    // 返回子树的根节点\n    return node\n}\n
avl_tree.swift
/* 删除节点 */\nfunc remove(val: Int) {\n    root = removeHelper(node: root, val: val)\n}\n\n/* 递归删除节点(辅助方法) */\nfunc removeHelper(node: TreeNode?, val: Int) -> TreeNode? {\n    var node = node\n    if node == nil {\n        return nil\n    }\n    /* 1. 查找节点并删除 */\n    if val < node!.val {\n        node?.left = removeHelper(node: node?.left, val: val)\n    } else if val > node!.val {\n        node?.right = removeHelper(node: node?.right, val: val)\n    } else {\n        if node?.left == nil || node?.right == nil {\n            let child = node?.left ?? node?.right\n            // 子节点数量 = 0 ,直接删除 node 并返回\n            if child == nil {\n                return nil\n            }\n            // 子节点数量 = 1 ,直接删除 node\n            else {\n                node = child\n            }\n        } else {\n            // 子节点数量 = 2 ,则将中序遍历的下个节点删除,并用该节点替换当前节点\n            var temp = node?.right\n            while temp?.left != nil {\n                temp = temp?.left\n            }\n            node?.right = removeHelper(node: node?.right, val: temp!.val)\n            node?.val = temp!.val\n        }\n    }\n    updateHeight(node: node) // 更新节点高度\n    /* 2. 执行旋转操作,使该子树重新恢复平衡 */\n    node = rotate(node: node)\n    // 返回子树的根节点\n    return node\n}\n
avl_tree.js
/* 删除节点 */\nremove(val) {\n    this.root = this.#removeHelper(this.root, val);\n}\n\n/* 递归删除节点(辅助方法) */\n#removeHelper(node, val) {\n    if (node === null) return null;\n    /* 1. 查找节点并删除 */\n    if (val < node.val) node.left = this.#removeHelper(node.left, val);\n    else if (val > node.val)\n        node.right = this.#removeHelper(node.right, val);\n    else {\n        if (node.left === null || node.right === null) {\n            const child = node.left !== null ? node.left : node.right;\n            // 子节点数量 = 0 ,直接删除 node 并返回\n            if (child === null) return null;\n            // 子节点数量 = 1 ,直接删除 node\n            else node = child;\n        } else {\n            // 子节点数量 = 2 ,则将中序遍历的下个节点删除,并用该节点替换当前节点\n            let temp = node.right;\n            while (temp.left !== null) {\n                temp = temp.left;\n            }\n            node.right = this.#removeHelper(node.right, temp.val);\n            node.val = temp.val;\n        }\n    }\n    this.#updateHeight(node); // 更新节点高度\n    /* 2. 执行旋转操作,使该子树重新恢复平衡 */\n    node = this.#rotate(node);\n    // 返回子树的根节点\n    return node;\n}\n
avl_tree.ts
/* 删除节点 */\nremove(val: number): void {\n    this.root = this.removeHelper(this.root, val);\n}\n\n/* 递归删除节点(辅助方法) */\nremoveHelper(node: TreeNode, val: number): TreeNode {\n    if (node === null) return null;\n    /* 1. 查找节点并删除 */\n    if (val < node.val) {\n        node.left = this.removeHelper(node.left, val);\n    } else if (val > node.val) {\n        node.right = this.removeHelper(node.right, val);\n    } else {\n        if (node.left === null || node.right === null) {\n            const child = node.left !== null ? node.left : node.right;\n            // 子节点数量 = 0 ,直接删除 node 并返回\n            if (child === null) {\n                return null;\n            } else {\n                // 子节点数量 = 1 ,直接删除 node\n                node = child;\n            }\n        } else {\n            // 子节点数量 = 2 ,则将中序遍历的下个节点删除,并用该节点替换当前节点\n            let temp = node.right;\n            while (temp.left !== null) {\n                temp = temp.left;\n            }\n            node.right = this.removeHelper(node.right, temp.val);\n            node.val = temp.val;\n        }\n    }\n    this.updateHeight(node); // 更新节点高度\n    /* 2. 执行旋转操作,使该子树重新恢复平衡 */\n    node = this.rotate(node);\n    // 返回子树的根节点\n    return node;\n}\n
avl_tree.dart
/* 删除节点 */\nvoid remove(int val) {\n  root = removeHelper(root, val);\n}\n\n/* 递归删除节点(辅助方法) */\nTreeNode? removeHelper(TreeNode? node, int val) {\n  if (node == null) return null;\n  /* 1. 查找节点并删除 */\n  if (val < node.val)\n    node.left = removeHelper(node.left, val);\n  else if (val > node.val)\n    node.right = removeHelper(node.right, val);\n  else {\n    if (node.left == null || node.right == null) {\n      TreeNode? child = node.left ?? node.right;\n      // 子节点数量 = 0 ,直接删除 node 并返回\n      if (child == null)\n        return null;\n      // 子节点数量 = 1 ,直接删除 node\n      else\n        node = child;\n    } else {\n      // 子节点数量 = 2 ,则将中序遍历的下个节点删除,并用该节点替换当前节点\n      TreeNode? temp = node.right;\n      while (temp!.left != null) {\n        temp = temp.left;\n      }\n      node.right = removeHelper(node.right, temp.val);\n      node.val = temp.val;\n    }\n  }\n  updateHeight(node); // 更新节点高度\n  /* 2. 执行旋转操作,使该子树重新恢复平衡 */\n  node = rotate(node);\n  // 返回子树的根节点\n  return node;\n}\n
avl_tree.rs
/* 删除节点 */\nfn remove(&self, val: i32) {\n    Self::remove_helper(self.root.clone(), val);\n}\n\n/* 递归删除节点(辅助方法) */\nfn remove_helper(node: OptionTreeNodeRc, val: i32) -> OptionTreeNodeRc {\n    match node {\n        Some(mut node) => {\n            /* 1. 查找节点并删除 */\n            if val < node.borrow().val {\n                let left = node.borrow().left.clone();\n                node.borrow_mut().left = Self::remove_helper(left, val);\n            } else if val > node.borrow().val {\n                let right = node.borrow().right.clone();\n                node.borrow_mut().right = Self::remove_helper(right, val);\n            } else if node.borrow().left.is_none() || node.borrow().right.is_none() {\n                let child = if node.borrow().left.is_some() {\n                    node.borrow().left.clone()\n                } else {\n                    node.borrow().right.clone()\n                };\n                match child {\n                    // 子节点数量 = 0 ,直接删除 node 并返回\n                    None => {\n                        return None;\n                    }\n                    // 子节点数量 = 1 ,直接删除 node\n                    Some(child) => node = child,\n                }\n            } else {\n                // 子节点数量 = 2 ,则将中序遍历的下个节点删除,并用该节点替换当前节点\n                let mut temp = node.borrow().right.clone().unwrap();\n                loop {\n                    let temp_left = temp.borrow().left.clone();\n                    if temp_left.is_none() {\n                        break;\n                    }\n                    temp = temp_left.unwrap();\n                }\n                let right = node.borrow().right.clone();\n                node.borrow_mut().right = Self::remove_helper(right, temp.borrow().val);\n                node.borrow_mut().val = temp.borrow().val;\n            }\n            Self::update_height(Some(node.clone())); // 更新节点高度\n\n            /* 2. 执行旋转操作,使该子树重新恢复平衡 */\n            node = Self::rotate(Some(node)).unwrap();\n            // 返回子树的根节点\n            Some(node)\n        }\n        None => None,\n    }\n}\n
avl_tree.c
/* 删除节点 */\n// 由于引入了 stdio.h ,此处无法使用 remove 关键词\nvoid removeItem(AVLTree *tree, int val) {\n    TreeNode *root = removeHelper(tree->root, val);\n}\n\n/* 递归删除节点(辅助函数) */\nTreeNode *removeHelper(TreeNode *node, int val) {\n    TreeNode *child, *grandChild;\n    if (node == NULL) {\n        return NULL;\n    }\n    /* 1. 查找节点并删除 */\n    if (val < node->val) {\n        node->left = removeHelper(node->left, val);\n    } else if (val > node->val) {\n        node->right = removeHelper(node->right, val);\n    } else {\n        if (node->left == NULL || node->right == NULL) {\n            child = node->left;\n            if (node->right != NULL) {\n                child = node->right;\n            }\n            // 子节点数量 = 0 ,直接删除 node 并返回\n            if (child == NULL) {\n                return NULL;\n            } else {\n                // 子节点数量 = 1 ,直接删除 node\n                node = child;\n            }\n        } else {\n            // 子节点数量 = 2 ,则将中序遍历的下个节点删除,并用该节点替换当前节点\n            TreeNode *temp = node->right;\n            while (temp->left != NULL) {\n                temp = temp->left;\n            }\n            int tempVal = temp->val;\n            node->right = removeHelper(node->right, temp->val);\n            node->val = tempVal;\n        }\n    }\n    // 更新节点高度\n    updateHeight(node);\n    /* 2. 执行旋转操作,使该子树重新恢复平衡 */\n    node = rotate(node);\n    // 返回子树的根节点\n    return node;\n}\n
avl_tree.kt
/* 删除节点 */\nfun remove(_val: Int) {\n    root = removeHelper(root, _val)\n}\n\n/* 递归删除节点(辅助方法) */\nfun removeHelper(n: TreeNode?, _val: Int): TreeNode? {\n    var node = n ?: return null\n    /* 1. 查找节点并删除 */\n    if (_val < node._val)\n        node.left = removeHelper(node.left, _val)\n    else if (_val > node._val)\n        node.right = removeHelper(node.right, _val)\n    else {\n        if (node.left == null || node.right == null) {\n            val child = if (node.left != null)\n                node.left\n            else\n                node.right\n            // 子节点数量 = 0 ,直接删除 node 并返回\n            if (child == null)\n                return null\n            // 子节点数量 = 1 ,直接删除 node\n            else\n                node = child\n        } else {\n            // 子节点数量 = 2 ,则将中序遍历的下个节点删除,并用该节点替换当前节点\n            var temp = node.right\n            while (temp!!.left != null) {\n                temp = temp.left\n            }\n            node.right = removeHelper(node.right, temp._val)\n            node._val = temp._val\n        }\n    }\n    updateHeight(node) // 更新节点高度\n    /* 2. 执行旋转操作,使该子树重新恢复平衡 */\n    node = rotate(node)\n    // 返回子树的根节点\n    return node\n}\n
avl_tree.rb
### 删除节点 ###\ndef remove(val)\n  @root = remove_helper(@root, val)\nend\n\n### 递归删除节点(辅助方法)###\ndef remove_helper(node, val)\n  return if node.nil?\n  # 1. 查找节点并删除\n  if val < node.val\n    node.left = remove_helper(node.left, val)\n  elsif val > node.val\n    node.right = remove_helper(node.right, val)\n  else\n    if node.left.nil? || node.right.nil?\n      child = node.left || node.right\n      # 子节点数量 = 0 ,直接删除 node 并返回\n      return if child.nil?\n      # 子节点数量 = 1 ,直接删除 node\n      node = child\n    else\n      # 子节点数量 = 2 ,则将中序遍历的下个节点删除,并用该节点替换当前节点\n      temp = node.right\n      while !temp.left.nil?\n        temp = temp.left\n      end\n      node.right = remove_helper(node.right, temp.val)\n      node.val = temp.val\n    end\n  end\n  # 更新节点高度\n  update_height(node)\n  # 2. 执行旋转操作,使该子树重新恢复平衡\n  rotate(node)\nend\n
","path":["第 7 章   树","7.5   AVL 树 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#3_1","level":3,"title":"3.   查找节点","text":"

AVL 树的节点查找操作与二叉搜索树一致,在此不再赘述。

","path":["第 7 章   树","7.5   AVL 树 *"],"tags":[]},{"location":"chapter_tree/avl_tree/#754-avl","level":2,"title":"7.5.4   AVL 树典型应用","text":"","path":["第 7 章   树","7.5   AVL 树 *"],"tags":[]},{"location":"chapter_tree/binary_search_tree/","level":1,"title":"7.4   二叉搜索树","text":"

如图 7-16 所示,二叉搜索树(binary search tree)满足以下条件。

  1. 对于根节点,左子树中所有节点的值 \\(<\\) 根节点的值 \\(<\\) 右子树中所有节点的值。
  2. 任意节点的左、右子树也是二叉搜索树,即同样满足条件 1.

图 7-16   二叉搜索树

","path":["第 7 章   树","7.4   二叉搜索树"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#741","level":2,"title":"7.4.1   二叉搜索树的操作","text":"

我们将二叉搜索树封装为一个类 BinarySearchTree ,并声明一个成员变量 root ,指向树的根节点。

","path":["第 7 章   树","7.4   二叉搜索树"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#1","level":3,"title":"1.   查找节点","text":"

给定目标节点值 num ,可以根据二叉搜索树的性质来查找。如图 7-17 所示,我们声明一个节点 cur ,从二叉树的根节点 root 出发,循环比较节点值 cur.valnum 之间的大小关系。

<1><2><3><4>

图 7-17   二叉搜索树查找节点示例

二叉搜索树的查找操作与二分查找算法的工作原理一致,都是每轮排除一半情况。循环次数最多为二叉树的高度,当二叉树平衡时,使用 \\(O(\\log n)\\) 时间。示例代码如下:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_tree.py
def search(self, num: int) -> TreeNode | None:\n    \"\"\"查找节点\"\"\"\n    cur = self._root\n    # 循环查找,越过叶节点后跳出\n    while cur is not None:\n        # 目标节点在 cur 的右子树中\n        if cur.val < num:\n            cur = cur.right\n        # 目标节点在 cur 的左子树中\n        elif cur.val > num:\n            cur = cur.left\n        # 找到目标节点,跳出循环\n        else:\n            break\n    return cur\n
binary_search_tree.cpp
/* 查找节点 */\nTreeNode *search(int num) {\n    TreeNode *cur = root;\n    // 循环查找,越过叶节点后跳出\n    while (cur != nullptr) {\n        // 目标节点在 cur 的右子树中\n        if (cur->val < num)\n            cur = cur->right;\n        // 目标节点在 cur 的左子树中\n        else if (cur->val > num)\n            cur = cur->left;\n        // 找到目标节点,跳出循环\n        else\n            break;\n    }\n    // 返回目标节点\n    return cur;\n}\n
binary_search_tree.java
/* 查找节点 */\nTreeNode search(int num) {\n    TreeNode cur = root;\n    // 循环查找,越过叶节点后跳出\n    while (cur != null) {\n        // 目标节点在 cur 的右子树中\n        if (cur.val < num)\n            cur = cur.right;\n        // 目标节点在 cur 的左子树中\n        else if (cur.val > num)\n            cur = cur.left;\n        // 找到目标节点,跳出循环\n        else\n            break;\n    }\n    // 返回目标节点\n    return cur;\n}\n
binary_search_tree.cs
/* 查找节点 */\nTreeNode? Search(int num) {\n    TreeNode? cur = root;\n    // 循环查找,越过叶节点后跳出\n    while (cur != null) {\n        // 目标节点在 cur 的右子树中\n        if (cur.val < num) cur =\n            cur.right;\n        // 目标节点在 cur 的左子树中\n        else if (cur.val > num)\n            cur = cur.left;\n        // 找到目标节点,跳出循环\n        else\n            break;\n    }\n    // 返回目标节点\n    return cur;\n}\n
binary_search_tree.go
/* 查找节点 */\nfunc (bst *binarySearchTree) search(num int) *TreeNode {\n    node := bst.root\n    // 循环查找,越过叶节点后跳出\n    for node != nil {\n        if node.Val.(int) < num {\n            // 目标节点在 cur 的右子树中\n            node = node.Right\n        } else if node.Val.(int) > num {\n            // 目标节点在 cur 的左子树中\n            node = node.Left\n        } else {\n            // 找到目标节点,跳出循环\n            break\n        }\n    }\n    // 返回目标节点\n    return node\n}\n
binary_search_tree.swift
/* 查找节点 */\nfunc search(num: Int) -> TreeNode? {\n    var cur = root\n    // 循环查找,越过叶节点后跳出\n    while cur != nil {\n        // 目标节点在 cur 的右子树中\n        if cur!.val < num {\n            cur = cur?.right\n        }\n        // 目标节点在 cur 的左子树中\n        else if cur!.val > num {\n            cur = cur?.left\n        }\n        // 找到目标节点,跳出循环\n        else {\n            break\n        }\n    }\n    // 返回目标节点\n    return cur\n}\n
binary_search_tree.js
/* 查找节点 */\nsearch(num) {\n    let cur = this.root;\n    // 循环查找,越过叶节点后跳出\n    while (cur !== null) {\n        // 目标节点在 cur 的右子树中\n        if (cur.val < num) cur = cur.right;\n        // 目标节点在 cur 的左子树中\n        else if (cur.val > num) cur = cur.left;\n        // 找到目标节点,跳出循环\n        else break;\n    }\n    // 返回目标节点\n    return cur;\n}\n
binary_search_tree.ts
/* 查找节点 */\nsearch(num: number): TreeNode | null {\n    let cur = this.root;\n    // 循环查找,越过叶节点后跳出\n    while (cur !== null) {\n        // 目标节点在 cur 的右子树中\n        if (cur.val < num) cur = cur.right;\n        // 目标节点在 cur 的左子树中\n        else if (cur.val > num) cur = cur.left;\n        // 找到目标节点,跳出循环\n        else break;\n    }\n    // 返回目标节点\n    return cur;\n}\n
binary_search_tree.dart
/* 查找节点 */\nTreeNode? search(int _num) {\n  TreeNode? cur = _root;\n  // 循环查找,越过叶节点后跳出\n  while (cur != null) {\n    // 目标节点在 cur 的右子树中\n    if (cur.val < _num)\n      cur = cur.right;\n    // 目标节点在 cur 的左子树中\n    else if (cur.val > _num)\n      cur = cur.left;\n    // 找到目标节点,跳出循环\n    else\n      break;\n  }\n  // 返回目标节点\n  return cur;\n}\n
binary_search_tree.rs
/* 查找节点 */\npub fn search(&self, num: i32) -> OptionTreeNodeRc {\n    let mut cur = self.root.clone();\n    // 循环查找,越过叶节点后跳出\n    while let Some(node) = cur.clone() {\n        match num.cmp(&node.borrow().val) {\n            // 目标节点在 cur 的右子树中\n            Ordering::Greater => cur = node.borrow().right.clone(),\n            // 目标节点在 cur 的左子树中\n            Ordering::Less => cur = node.borrow().left.clone(),\n            // 找到目标节点,跳出循环\n            Ordering::Equal => break,\n        }\n    }\n\n    // 返回目标节点\n    cur\n}\n
binary_search_tree.c
/* 查找节点 */\nTreeNode *search(BinarySearchTree *bst, int num) {\n    TreeNode *cur = bst->root;\n    // 循环查找,越过叶节点后跳出\n    while (cur != NULL) {\n        if (cur->val < num) {\n            // 目标节点在 cur 的右子树中\n            cur = cur->right;\n        } else if (cur->val > num) {\n            // 目标节点在 cur 的左子树中\n            cur = cur->left;\n        } else {\n            // 找到目标节点,跳出循环\n            break;\n        }\n    }\n    // 返回目标节点\n    return cur;\n}\n
binary_search_tree.kt
/* 查找节点 */\nfun search(num: Int): TreeNode? {\n    var cur = root\n    // 循环查找,越过叶节点后跳出\n    while (cur != null) {\n        // 目标节点在 cur 的右子树中\n        cur = if (cur._val < num)\n            cur.right\n        // 目标节点在 cur 的左子树中\n        else if (cur._val > num)\n            cur.left\n        // 找到目标节点,跳出循环\n        else\n            break\n    }\n    // 返回目标节点\n    return cur\n}\n
binary_search_tree.rb
### 查找节点 ###\ndef search(num)\n  cur = @root\n\n  # 循环查找,越过叶节点后跳出\n  while !cur.nil?\n    # 目标节点在 cur 的右子树中\n    if cur.val < num\n      cur = cur.right\n    # 目标节点在 cur 的左子树中\n    elsif cur.val > num\n      cur = cur.left\n    # 找到目标节点,跳出循环\n    else\n      break\n    end\n  end\n\n  cur\nend\n
可视化运行

全屏观看 >

","path":["第 7 章   树","7.4   二叉搜索树"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#2","level":3,"title":"2.   插入节点","text":"

给定一个待插入元素 num ,为了保持二叉搜索树“左子树 < 根节点 < 右子树”的性质,插入操作流程如图 7-18 所示。

  1. 查找插入位置:与查找操作相似,从根节点出发,根据当前节点值和 num 的大小关系循环向下搜索,直到越过叶节点(遍历至 None )时跳出循环。
  2. 在该位置插入节点:初始化节点 num ,将该节点置于 None 的位置。

图 7-18   在二叉搜索树中插入节点

在代码实现中,需要注意以下两点。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_tree.py
def insert(self, num: int):\n    \"\"\"插入节点\"\"\"\n    # 若树为空,则初始化根节点\n    if self._root is None:\n        self._root = TreeNode(num)\n        return\n    # 循环查找,越过叶节点后跳出\n    cur, pre = self._root, None\n    while cur is not None:\n        # 找到重复节点,直接返回\n        if cur.val == num:\n            return\n        pre = cur\n        # 插入位置在 cur 的右子树中\n        if cur.val < num:\n            cur = cur.right\n        # 插入位置在 cur 的左子树中\n        else:\n            cur = cur.left\n    # 插入节点\n    node = TreeNode(num)\n    if pre.val < num:\n        pre.right = node\n    else:\n        pre.left = node\n
binary_search_tree.cpp
/* 插入节点 */\nvoid insert(int num) {\n    // 若树为空,则初始化根节点\n    if (root == nullptr) {\n        root = new TreeNode(num);\n        return;\n    }\n    TreeNode *cur = root, *pre = nullptr;\n    // 循环查找,越过叶节点后跳出\n    while (cur != nullptr) {\n        // 找到重复节点,直接返回\n        if (cur->val == num)\n            return;\n        pre = cur;\n        // 插入位置在 cur 的右子树中\n        if (cur->val < num)\n            cur = cur->right;\n        // 插入位置在 cur 的左子树中\n        else\n            cur = cur->left;\n    }\n    // 插入节点\n    TreeNode *node = new TreeNode(num);\n    if (pre->val < num)\n        pre->right = node;\n    else\n        pre->left = node;\n}\n
binary_search_tree.java
/* 插入节点 */\nvoid insert(int num) {\n    // 若树为空,则初始化根节点\n    if (root == null) {\n        root = new TreeNode(num);\n        return;\n    }\n    TreeNode cur = root, pre = null;\n    // 循环查找,越过叶节点后跳出\n    while (cur != null) {\n        // 找到重复节点,直接返回\n        if (cur.val == num)\n            return;\n        pre = cur;\n        // 插入位置在 cur 的右子树中\n        if (cur.val < num)\n            cur = cur.right;\n        // 插入位置在 cur 的左子树中\n        else\n            cur = cur.left;\n    }\n    // 插入节点\n    TreeNode node = new TreeNode(num);\n    if (pre.val < num)\n        pre.right = node;\n    else\n        pre.left = node;\n}\n
binary_search_tree.cs
/* 插入节点 */\nvoid Insert(int num) {\n    // 若树为空,则初始化根节点\n    if (root == null) {\n        root = new TreeNode(num);\n        return;\n    }\n    TreeNode? cur = root, pre = null;\n    // 循环查找,越过叶节点后跳出\n    while (cur != null) {\n        // 找到重复节点,直接返回\n        if (cur.val == num)\n            return;\n        pre = cur;\n        // 插入位置在 cur 的右子树中\n        if (cur.val < num)\n            cur = cur.right;\n        // 插入位置在 cur 的左子树中\n        else\n            cur = cur.left;\n    }\n\n    // 插入节点\n    TreeNode node = new(num);\n    if (pre != null) {\n        if (pre.val < num)\n            pre.right = node;\n        else\n            pre.left = node;\n    }\n}\n
binary_search_tree.go
/* 插入节点 */\nfunc (bst *binarySearchTree) insert(num int) {\n    cur := bst.root\n    // 若树为空,则初始化根节点\n    if cur == nil {\n        bst.root = NewTreeNode(num)\n        return\n    }\n    // 待插入节点之前的节点位置\n    var pre *TreeNode = nil\n    // 循环查找,越过叶节点后跳出\n    for cur != nil {\n        if cur.Val == num {\n            return\n        }\n        pre = cur\n        if cur.Val.(int) < num {\n            cur = cur.Right\n        } else {\n            cur = cur.Left\n        }\n    }\n    // 插入节点\n    node := NewTreeNode(num)\n    if pre.Val.(int) < num {\n        pre.Right = node\n    } else {\n        pre.Left = node\n    }\n}\n
binary_search_tree.swift
/* 插入节点 */\nfunc insert(num: Int) {\n    // 若树为空,则初始化根节点\n    if root == nil {\n        root = TreeNode(x: num)\n        return\n    }\n    var cur = root\n    var pre: TreeNode?\n    // 循环查找,越过叶节点后跳出\n    while cur != nil {\n        // 找到重复节点,直接返回\n        if cur!.val == num {\n            return\n        }\n        pre = cur\n        // 插入位置在 cur 的右子树中\n        if cur!.val < num {\n            cur = cur?.right\n        }\n        // 插入位置在 cur 的左子树中\n        else {\n            cur = cur?.left\n        }\n    }\n    // 插入节点\n    let node = TreeNode(x: num)\n    if pre!.val < num {\n        pre?.right = node\n    } else {\n        pre?.left = node\n    }\n}\n
binary_search_tree.js
/* 插入节点 */\ninsert(num) {\n    // 若树为空,则初始化根节点\n    if (this.root === null) {\n        this.root = new TreeNode(num);\n        return;\n    }\n    let cur = this.root,\n        pre = null;\n    // 循环查找,越过叶节点后跳出\n    while (cur !== null) {\n        // 找到重复节点,直接返回\n        if (cur.val === num) return;\n        pre = cur;\n        // 插入位置在 cur 的右子树中\n        if (cur.val < num) cur = cur.right;\n        // 插入位置在 cur 的左子树中\n        else cur = cur.left;\n    }\n    // 插入节点\n    const node = new TreeNode(num);\n    if (pre.val < num) pre.right = node;\n    else pre.left = node;\n}\n
binary_search_tree.ts
/* 插入节点 */\ninsert(num: number): void {\n    // 若树为空,则初始化根节点\n    if (this.root === null) {\n        this.root = new TreeNode(num);\n        return;\n    }\n    let cur: TreeNode | null = this.root,\n        pre: TreeNode | null = null;\n    // 循环查找,越过叶节点后跳出\n    while (cur !== null) {\n        // 找到重复节点,直接返回\n        if (cur.val === num) return;\n        pre = cur;\n        // 插入位置在 cur 的右子树中\n        if (cur.val < num) cur = cur.right;\n        // 插入位置在 cur 的左子树中\n        else cur = cur.left;\n    }\n    // 插入节点\n    const node = new TreeNode(num);\n    if (pre!.val < num) pre!.right = node;\n    else pre!.left = node;\n}\n
binary_search_tree.dart
/* 插入节点 */\nvoid insert(int _num) {\n  // 若树为空,则初始化根节点\n  if (_root == null) {\n    _root = TreeNode(_num);\n    return;\n  }\n  TreeNode? cur = _root;\n  TreeNode? pre = null;\n  // 循环查找,越过叶节点后跳出\n  while (cur != null) {\n    // 找到重复节点,直接返回\n    if (cur.val == _num) return;\n    pre = cur;\n    // 插入位置在 cur 的右子树中\n    if (cur.val < _num)\n      cur = cur.right;\n    // 插入位置在 cur 的左子树中\n    else\n      cur = cur.left;\n  }\n  // 插入节点\n  TreeNode? node = TreeNode(_num);\n  if (pre!.val < _num)\n    pre.right = node;\n  else\n    pre.left = node;\n}\n
binary_search_tree.rs
/* 插入节点 */\npub fn insert(&mut self, num: i32) {\n    // 若树为空,则初始化根节点\n    if self.root.is_none() {\n        self.root = Some(TreeNode::new(num));\n        return;\n    }\n    let mut cur = self.root.clone();\n    let mut pre = None;\n    // 循环查找,越过叶节点后跳出\n    while let Some(node) = cur.clone() {\n        match num.cmp(&node.borrow().val) {\n            // 找到重复节点,直接返回\n            Ordering::Equal => return,\n            // 插入位置在 cur 的右子树中\n            Ordering::Greater => {\n                pre = cur.clone();\n                cur = node.borrow().right.clone();\n            }\n            // 插入位置在 cur 的左子树中\n            Ordering::Less => {\n                pre = cur.clone();\n                cur = node.borrow().left.clone();\n            }\n        }\n    }\n    // 插入节点\n    let pre = pre.unwrap();\n    let node = Some(TreeNode::new(num));\n    if num > pre.borrow().val {\n        pre.borrow_mut().right = node;\n    } else {\n        pre.borrow_mut().left = node;\n    }\n}\n
binary_search_tree.c
/* 插入节点 */\nvoid insert(BinarySearchTree *bst, int num) {\n    // 若树为空,则初始化根节点\n    if (bst->root == NULL) {\n        bst->root = newTreeNode(num);\n        return;\n    }\n    TreeNode *cur = bst->root, *pre = NULL;\n    // 循环查找,越过叶节点后跳出\n    while (cur != NULL) {\n        // 找到重复节点,直接返回\n        if (cur->val == num) {\n            return;\n        }\n        pre = cur;\n        if (cur->val < num) {\n            // 插入位置在 cur 的右子树中\n            cur = cur->right;\n        } else {\n            // 插入位置在 cur 的左子树中\n            cur = cur->left;\n        }\n    }\n    // 插入节点\n    TreeNode *node = newTreeNode(num);\n    if (pre->val < num) {\n        pre->right = node;\n    } else {\n        pre->left = node;\n    }\n}\n
binary_search_tree.kt
/* 插入节点 */\nfun insert(num: Int) {\n    // 若树为空,则初始化根节点\n    if (root == null) {\n        root = TreeNode(num)\n        return\n    }\n    var cur = root\n    var pre: TreeNode? = null\n    // 循环查找,越过叶节点后跳出\n    while (cur != null) {\n        // 找到重复节点,直接返回\n        if (cur._val == num)\n            return\n        pre = cur\n        // 插入位置在 cur 的右子树中\n        cur = if (cur._val < num)\n            cur.right\n        // 插入位置在 cur 的左子树中\n        else\n            cur.left\n    }\n    // 插入节点\n    val node = TreeNode(num)\n    if (pre?._val!! < num)\n        pre.right = node\n    else\n        pre.left = node\n}\n
binary_search_tree.rb
### 插入节点 ###\ndef insert(num)\n  # 若树为空,则初始化根节点\n  if @root.nil?\n    @root = TreeNode.new(num)\n    return\n  end\n\n  # 循环查找,越过叶节点后跳出\n  cur, pre = @root, nil\n  while !cur.nil?\n    # 找到重复节点,直接返回\n    return if cur.val == num\n\n    pre = cur\n    # 插入位置在 cur 的右子树中\n    if cur.val < num\n      cur = cur.right\n    # 插入位置在 cur 的左子树中\n    else\n      cur = cur.left\n    end\n  end\n\n  # 插入节点\n  node = TreeNode.new(num)\n  if pre.val < num\n    pre.right = node\n  else\n    pre.left = node\n  end\nend\n
可视化运行

全屏观看 >

与查找节点相同,插入节点使用 \\(O(\\log n)\\) 时间。

","path":["第 7 章   树","7.4   二叉搜索树"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#3","level":3,"title":"3.   删除节点","text":"

先在二叉树中查找到目标节点,再将其删除。与插入节点类似,我们需要保证在删除操作完成后,二叉搜索树的“左子树 < 根节点 < 右子树”的性质仍然满足。因此,我们根据目标节点的子节点数量,分 0、1 和 2 三种情况,执行对应的删除节点操作。

如图 7-19 所示,当待删除节点的度为 \\(0\\) 时,表示该节点是叶节点,可以直接删除。

图 7-19   在二叉搜索树中删除节点(度为 0 )

如图 7-20 所示,当待删除节点的度为 \\(1\\) 时,将待删除节点替换为其子节点即可。

图 7-20   在二叉搜索树中删除节点(度为 1 )

当待删除节点的度为 \\(2\\) 时,我们无法直接删除它,而需要使用一个节点替换该节点。由于要保持二叉搜索树“左子树 \\(<\\) 根节点 \\(<\\) 右子树”的性质,因此这个节点可以是右子树的最小节点或左子树的最大节点。

假设我们选择右子树的最小节点(中序遍历的下一个节点),则删除操作流程如图 7-21 所示。

  1. 找到待删除节点在“中序遍历序列”中的下一个节点,记为 tmp
  2. tmp 的值覆盖待删除节点的值,并在树中递归删除节点 tmp
<1><2><3><4>

图 7-21   在二叉搜索树中删除节点(度为 2 )

删除节点操作同样使用 \\(O(\\log n)\\) 时间,其中查找待删除节点需要 \\(O(\\log n)\\) 时间,获取中序遍历后继节点需要 \\(O(\\log n)\\) 时间。示例代码如下:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_search_tree.py
def remove(self, num: int):\n    \"\"\"删除节点\"\"\"\n    # 若树为空,直接提前返回\n    if self._root is None:\n        return\n    # 循环查找,越过叶节点后跳出\n    cur, pre = self._root, None\n    while cur is not None:\n        # 找到待删除节点,跳出循环\n        if cur.val == num:\n            break\n        pre = cur\n        # 待删除节点在 cur 的右子树中\n        if cur.val < num:\n            cur = cur.right\n        # 待删除节点在 cur 的左子树中\n        else:\n            cur = cur.left\n    # 若无待删除节点,则直接返回\n    if cur is None:\n        return\n\n    # 子节点数量 = 0 or 1\n    if cur.left is None or cur.right is None:\n        # 当子节点数量 = 0 / 1 时, child = null / 该子节点\n        child = cur.left or cur.right\n        # 删除节点 cur\n        if cur != self._root:\n            if pre.left == cur:\n                pre.left = child\n            else:\n                pre.right = child\n        else:\n            # 若删除节点为根节点,则重新指定根节点\n            self._root = child\n    # 子节点数量 = 2\n    else:\n        # 获取中序遍历中 cur 的下一个节点\n        tmp: TreeNode = cur.right\n        while tmp.left is not None:\n            tmp = tmp.left\n        # 递归删除节点 tmp\n        self.remove(tmp.val)\n        # 用 tmp 覆盖 cur\n        cur.val = tmp.val\n
binary_search_tree.cpp
/* 删除节点 */\nvoid remove(int num) {\n    // 若树为空,直接提前返回\n    if (root == nullptr)\n        return;\n    TreeNode *cur = root, *pre = nullptr;\n    // 循环查找,越过叶节点后跳出\n    while (cur != nullptr) {\n        // 找到待删除节点,跳出循环\n        if (cur->val == num)\n            break;\n        pre = cur;\n        // 待删除节点在 cur 的右子树中\n        if (cur->val < num)\n            cur = cur->right;\n        // 待删除节点在 cur 的左子树中\n        else\n            cur = cur->left;\n    }\n    // 若无待删除节点,则直接返回\n    if (cur == nullptr)\n        return;\n    // 子节点数量 = 0 or 1\n    if (cur->left == nullptr || cur->right == nullptr) {\n        // 当子节点数量 = 0 / 1 时, child = nullptr / 该子节点\n        TreeNode *child = cur->left != nullptr ? cur->left : cur->right;\n        // 删除节点 cur\n        if (cur != root) {\n            if (pre->left == cur)\n                pre->left = child;\n            else\n                pre->right = child;\n        } else {\n            // 若删除节点为根节点,则重新指定根节点\n            root = child;\n        }\n        // 释放内存\n        delete cur;\n    }\n    // 子节点数量 = 2\n    else {\n        // 获取中序遍历中 cur 的下一个节点\n        TreeNode *tmp = cur->right;\n        while (tmp->left != nullptr) {\n            tmp = tmp->left;\n        }\n        int tmpVal = tmp->val;\n        // 递归删除节点 tmp\n        remove(tmp->val);\n        // 用 tmp 覆盖 cur\n        cur->val = tmpVal;\n    }\n}\n
binary_search_tree.java
/* 删除节点 */\nvoid remove(int num) {\n    // 若树为空,直接提前返回\n    if (root == null)\n        return;\n    TreeNode cur = root, pre = null;\n    // 循环查找,越过叶节点后跳出\n    while (cur != null) {\n        // 找到待删除节点,跳出循环\n        if (cur.val == num)\n            break;\n        pre = cur;\n        // 待删除节点在 cur 的右子树中\n        if (cur.val < num)\n            cur = cur.right;\n        // 待删除节点在 cur 的左子树中\n        else\n            cur = cur.left;\n    }\n    // 若无待删除节点,则直接返回\n    if (cur == null)\n        return;\n    // 子节点数量 = 0 or 1\n    if (cur.left == null || cur.right == null) {\n        // 当子节点数量 = 0 / 1 时, child = null / 该子节点\n        TreeNode child = cur.left != null ? cur.left : cur.right;\n        // 删除节点 cur\n        if (cur != root) {\n            if (pre.left == cur)\n                pre.left = child;\n            else\n                pre.right = child;\n        } else {\n            // 若删除节点为根节点,则重新指定根节点\n            root = child;\n        }\n    }\n    // 子节点数量 = 2\n    else {\n        // 获取中序遍历中 cur 的下一个节点\n        TreeNode tmp = cur.right;\n        while (tmp.left != null) {\n            tmp = tmp.left;\n        }\n        // 递归删除节点 tmp\n        remove(tmp.val);\n        // 用 tmp 覆盖 cur\n        cur.val = tmp.val;\n    }\n}\n
binary_search_tree.cs
/* 删除节点 */\nvoid Remove(int num) {\n    // 若树为空,直接提前返回\n    if (root == null)\n        return;\n    TreeNode? cur = root, pre = null;\n    // 循环查找,越过叶节点后跳出\n    while (cur != null) {\n        // 找到待删除节点,跳出循环\n        if (cur.val == num)\n            break;\n        pre = cur;\n        // 待删除节点在 cur 的右子树中\n        if (cur.val < num)\n            cur = cur.right;\n        // 待删除节点在 cur 的左子树中\n        else\n            cur = cur.left;\n    }\n    // 若无待删除节点,则直接返回\n    if (cur == null)\n        return;\n    // 子节点数量 = 0 or 1\n    if (cur.left == null || cur.right == null) {\n        // 当子节点数量 = 0 / 1 时, child = null / 该子节点\n        TreeNode? child = cur.left ?? cur.right;\n        // 删除节点 cur\n        if (cur != root) {\n            if (pre!.left == cur)\n                pre.left = child;\n            else\n                pre.right = child;\n        } else {\n            // 若删除节点为根节点,则重新指定根节点\n            root = child;\n        }\n    }\n    // 子节点数量 = 2\n    else {\n        // 获取中序遍历中 cur 的下一个节点\n        TreeNode? tmp = cur.right;\n        while (tmp.left != null) {\n            tmp = tmp.left;\n        }\n        // 递归删除节点 tmp\n        Remove(tmp.val!.Value);\n        // 用 tmp 覆盖 cur\n        cur.val = tmp.val;\n    }\n}\n
binary_search_tree.go
/* 删除节点 */\nfunc (bst *binarySearchTree) remove(num int) {\n    cur := bst.root\n    // 若树为空,直接提前返回\n    if cur == nil {\n        return\n    }\n    // 待删除节点之前的节点位置\n    var pre *TreeNode = nil\n    // 循环查找,越过叶节点后跳出\n    for cur != nil {\n        if cur.Val == num {\n            break\n        }\n        pre = cur\n        if cur.Val.(int) < num {\n            // 待删除节点在右子树中\n            cur = cur.Right\n        } else {\n            // 待删除节点在左子树中\n            cur = cur.Left\n        }\n    }\n    // 若无待删除节点,则直接返回\n    if cur == nil {\n        return\n    }\n    // 子节点数为 0 或 1\n    if cur.Left == nil || cur.Right == nil {\n        var child *TreeNode = nil\n        // 取出待删除节点的子节点\n        if cur.Left != nil {\n            child = cur.Left\n        } else {\n            child = cur.Right\n        }\n        // 删除节点 cur\n        if cur != bst.root {\n            if pre.Left == cur {\n                pre.Left = child\n            } else {\n                pre.Right = child\n            }\n        } else {\n            // 若删除节点为根节点,则重新指定根节点\n            bst.root = child\n        }\n        // 子节点数为 2\n    } else {\n        // 获取中序遍历中待删除节点 cur 的下一个节点\n        tmp := cur.Right\n        for tmp.Left != nil {\n            tmp = tmp.Left\n        }\n        // 递归删除节点 tmp\n        bst.remove(tmp.Val.(int))\n        // 用 tmp 覆盖 cur\n        cur.Val = tmp.Val\n    }\n}\n
binary_search_tree.swift
/* 删除节点 */\nfunc remove(num: Int) {\n    // 若树为空,直接提前返回\n    if root == nil {\n        return\n    }\n    var cur = root\n    var pre: TreeNode?\n    // 循环查找,越过叶节点后跳出\n    while cur != nil {\n        // 找到待删除节点,跳出循环\n        if cur!.val == num {\n            break\n        }\n        pre = cur\n        // 待删除节点在 cur 的右子树中\n        if cur!.val < num {\n            cur = cur?.right\n        }\n        // 待删除节点在 cur 的左子树中\n        else {\n            cur = cur?.left\n        }\n    }\n    // 若无待删除节点,则直接返回\n    if cur == nil {\n        return\n    }\n    // 子节点数量 = 0 or 1\n    if cur?.left == nil || cur?.right == nil {\n        // 当子节点数量 = 0 / 1 时, child = null / 该子节点\n        let child = cur?.left ?? cur?.right\n        // 删除节点 cur\n        if cur !== root {\n            if pre?.left === cur {\n                pre?.left = child\n            } else {\n                pre?.right = child\n            }\n        } else {\n            // 若删除节点为根节点,则重新指定根节点\n            root = child\n        }\n    }\n    // 子节点数量 = 2\n    else {\n        // 获取中序遍历中 cur 的下一个节点\n        var tmp = cur?.right\n        while tmp?.left != nil {\n            tmp = tmp?.left\n        }\n        // 递归删除节点 tmp\n        remove(num: tmp!.val)\n        // 用 tmp 覆盖 cur\n        cur?.val = tmp!.val\n    }\n}\n
binary_search_tree.js
/* 删除节点 */\nremove(num) {\n    // 若树为空,直接提前返回\n    if (this.root === null) return;\n    let cur = this.root,\n        pre = null;\n    // 循环查找,越过叶节点后跳出\n    while (cur !== null) {\n        // 找到待删除节点,跳出循环\n        if (cur.val === num) break;\n        pre = cur;\n        // 待删除节点在 cur 的右子树中\n        if (cur.val < num) cur = cur.right;\n        // 待删除节点在 cur 的左子树中\n        else cur = cur.left;\n    }\n    // 若无待删除节点,则直接返回\n    if (cur === null) return;\n    // 子节点数量 = 0 or 1\n    if (cur.left === null || cur.right === null) {\n        // 当子节点数量 = 0 / 1 时, child = null / 该子节点\n        const child = cur.left !== null ? cur.left : cur.right;\n        // 删除节点 cur\n        if (cur !== this.root) {\n            if (pre.left === cur) pre.left = child;\n            else pre.right = child;\n        } else {\n            // 若删除节点为根节点,则重新指定根节点\n            this.root = child;\n        }\n    }\n    // 子节点数量 = 2\n    else {\n        // 获取中序遍历中 cur 的下一个节点\n        let tmp = cur.right;\n        while (tmp.left !== null) {\n            tmp = tmp.left;\n        }\n        // 递归删除节点 tmp\n        this.remove(tmp.val);\n        // 用 tmp 覆盖 cur\n        cur.val = tmp.val;\n    }\n}\n
binary_search_tree.ts
/* 删除节点 */\nremove(num: number): void {\n    // 若树为空,直接提前返回\n    if (this.root === null) return;\n    let cur: TreeNode | null = this.root,\n        pre: TreeNode | null = null;\n    // 循环查找,越过叶节点后跳出\n    while (cur !== null) {\n        // 找到待删除节点,跳出循环\n        if (cur.val === num) break;\n        pre = cur;\n        // 待删除节点在 cur 的右子树中\n        if (cur.val < num) cur = cur.right;\n        // 待删除节点在 cur 的左子树中\n        else cur = cur.left;\n    }\n    // 若无待删除节点,则直接返回\n    if (cur === null) return;\n    // 子节点数量 = 0 or 1\n    if (cur.left === null || cur.right === null) {\n        // 当子节点数量 = 0 / 1 时, child = null / 该子节点\n        const child: TreeNode | null =\n            cur.left !== null ? cur.left : cur.right;\n        // 删除节点 cur\n        if (cur !== this.root) {\n            if (pre!.left === cur) pre!.left = child;\n            else pre!.right = child;\n        } else {\n            // 若删除节点为根节点,则重新指定根节点\n            this.root = child;\n        }\n    }\n    // 子节点数量 = 2\n    else {\n        // 获取中序遍历中 cur 的下一个节点\n        let tmp: TreeNode | null = cur.right;\n        while (tmp!.left !== null) {\n            tmp = tmp!.left;\n        }\n        // 递归删除节点 tmp\n        this.remove(tmp!.val);\n        // 用 tmp 覆盖 cur\n        cur.val = tmp!.val;\n    }\n}\n
binary_search_tree.dart
/* 删除节点 */\nvoid remove(int _num) {\n  // 若树为空,直接提前返回\n  if (_root == null) return;\n  TreeNode? cur = _root;\n  TreeNode? pre = null;\n  // 循环查找,越过叶节点后跳出\n  while (cur != null) {\n    // 找到待删除节点,跳出循环\n    if (cur.val == _num) break;\n    pre = cur;\n    // 待删除节点在 cur 的右子树中\n    if (cur.val < _num)\n      cur = cur.right;\n    // 待删除节点在 cur 的左子树中\n    else\n      cur = cur.left;\n  }\n  // 若无待删除节点,直接返回\n  if (cur == null) return;\n  // 子节点数量 = 0 or 1\n  if (cur.left == null || cur.right == null) {\n    // 当子节点数量 = 0 / 1 时, child = null / 该子节点\n    TreeNode? child = cur.left ?? cur.right;\n    // 删除节点 cur\n    if (cur != _root) {\n      if (pre!.left == cur)\n        pre.left = child;\n      else\n        pre.right = child;\n    } else {\n      // 若删除节点为根节点,则重新指定根节点\n      _root = child;\n    }\n  } else {\n    // 子节点数量 = 2\n    // 获取中序遍历中 cur 的下一个节点\n    TreeNode? tmp = cur.right;\n    while (tmp!.left != null) {\n      tmp = tmp.left;\n    }\n    // 递归删除节点 tmp\n    remove(tmp.val);\n    // 用 tmp 覆盖 cur\n    cur.val = tmp.val;\n  }\n}\n
binary_search_tree.rs
/* 删除节点 */\npub fn remove(&mut self, num: i32) {\n    // 若树为空,直接提前返回\n    if self.root.is_none() {\n        return;\n    }\n    let mut cur = self.root.clone();\n    let mut pre = None;\n    // 循环查找,越过叶节点后跳出\n    while let Some(node) = cur.clone() {\n        match num.cmp(&node.borrow().val) {\n            // 找到待删除节点,跳出循环\n            Ordering::Equal => break,\n            // 待删除节点在 cur 的右子树中\n            Ordering::Greater => {\n                pre = cur.clone();\n                cur = node.borrow().right.clone();\n            }\n            // 待删除节点在 cur 的左子树中\n            Ordering::Less => {\n                pre = cur.clone();\n                cur = node.borrow().left.clone();\n            }\n        }\n    }\n    // 若无待删除节点,则直接返回\n    if cur.is_none() {\n        return;\n    }\n    let cur = cur.unwrap();\n    let (left_child, right_child) = (cur.borrow().left.clone(), cur.borrow().right.clone());\n    match (left_child.clone(), right_child.clone()) {\n        // 子节点数量 = 0 or 1\n        (None, None) | (Some(_), None) | (None, Some(_)) => {\n            // 当子节点数量 = 0 / 1 时, child = nullptr / 该子节点\n            let child = left_child.or(right_child);\n            let pre = pre.unwrap();\n            // 删除节点 cur\n            if !Rc::ptr_eq(&cur, self.root.as_ref().unwrap()) {\n                let left = pre.borrow().left.clone();\n                if left.is_some() && Rc::ptr_eq(left.as_ref().unwrap(), &cur) {\n                    pre.borrow_mut().left = child;\n                } else {\n                    pre.borrow_mut().right = child;\n                }\n            } else {\n                // 若删除节点为根节点,则重新指定根节点\n                self.root = child;\n            }\n        }\n        // 子节点数量 = 2\n        (Some(_), Some(_)) => {\n            // 获取中序遍历中 cur 的下一个节点\n            let mut tmp = cur.borrow().right.clone();\n            while let Some(node) = tmp.clone() {\n                if node.borrow().left.is_some() {\n                    tmp = node.borrow().left.clone();\n                } else {\n                    break;\n                }\n            }\n            let tmp_val = tmp.unwrap().borrow().val;\n            // 递归删除节点 tmp\n            self.remove(tmp_val);\n            // 用 tmp 覆盖 cur\n            cur.borrow_mut().val = tmp_val;\n        }\n    }\n}\n
binary_search_tree.c
/* 删除节点 */\n// 由于引入了 stdio.h ,此处无法使用 remove 关键词\nvoid removeItem(BinarySearchTree *bst, int num) {\n    // 若树为空,直接提前返回\n    if (bst->root == NULL)\n        return;\n    TreeNode *cur = bst->root, *pre = NULL;\n    // 循环查找,越过叶节点后跳出\n    while (cur != NULL) {\n        // 找到待删除节点,跳出循环\n        if (cur->val == num)\n            break;\n        pre = cur;\n        if (cur->val < num) {\n            // 待删除节点在 root 的右子树中\n            cur = cur->right;\n        } else {\n            // 待删除节点在 root 的左子树中\n            cur = cur->left;\n        }\n    }\n    // 若无待删除节点,则直接返回\n    if (cur == NULL)\n        return;\n    // 判断待删除节点是否存在子节点\n    if (cur->left == NULL || cur->right == NULL) {\n        /* 子节点数量 = 0 or 1 */\n        // 当子节点数量 = 0 / 1 时, child = nullptr / 该子节点\n        TreeNode *child = cur->left != NULL ? cur->left : cur->right;\n        // 删除节点 cur\n        if (pre->left == cur) {\n            pre->left = child;\n        } else {\n            pre->right = child;\n        }\n        // 释放内存\n        free(cur);\n    } else {\n        /* 子节点数量 = 2 */\n        // 获取中序遍历中 cur 的下一个节点\n        TreeNode *tmp = cur->right;\n        while (tmp->left != NULL) {\n            tmp = tmp->left;\n        }\n        int tmpVal = tmp->val;\n        // 递归删除节点 tmp\n        removeItem(bst, tmp->val);\n        // 用 tmp 覆盖 cur\n        cur->val = tmpVal;\n    }\n}\n
binary_search_tree.kt
/* 删除节点 */\nfun remove(num: Int) {\n    // 若树为空,直接提前返回\n    if (root == null)\n        return\n    var cur = root\n    var pre: TreeNode? = null\n    // 循环查找,越过叶节点后跳出\n    while (cur != null) {\n        // 找到待删除节点,跳出循环\n        if (cur._val == num)\n            break\n        pre = cur\n        // 待删除节点在 cur 的右子树中\n        cur = if (cur._val < num)\n            cur.right\n        // 待删除节点在 cur 的左子树中\n        else\n            cur.left\n    }\n    // 若无待删除节点,则直接返回\n    if (cur == null)\n        return\n    // 子节点数量 = 0 or 1\n    if (cur.left == null || cur.right == null) {\n        // 当子节点数量 = 0 / 1 时, child = null / 该子节点\n        val child = if (cur.left != null)\n            cur.left\n        else\n            cur.right\n        // 删除节点 cur\n        if (cur != root) {\n            if (pre!!.left == cur)\n                pre.left = child\n            else\n                pre.right = child\n        } else {\n            // 若删除节点为根节点,则重新指定根节点\n            root = child\n        }\n        // 子节点数量 = 2\n    } else {\n        // 获取中序遍历中 cur 的下一个节点\n        var tmp = cur.right\n        while (tmp!!.left != null) {\n            tmp = tmp.left\n        }\n        // 递归删除节点 tmp\n        remove(tmp._val)\n        // 用 tmp 覆盖 cur\n        cur._val = tmp._val\n    }\n}\n
binary_search_tree.rb
### 删除节点 ###\ndef remove(num)\n  # 若树为空,直接提前返回\n  return if @root.nil?\n\n  # 循环查找,越过叶节点后跳出\n  cur, pre = @root, nil\n  while !cur.nil?\n    # 找到待删除节点,跳出循环\n    break if cur.val == num\n\n    pre = cur\n    # 待删除节点在 cur 的右子树中\n    if cur.val < num\n      cur = cur.right\n    # 待删除节点在 cur 的左子树中\n    else\n      cur = cur.left\n    end\n  end\n  # 若无待删除节点,则直接返回\n  return if cur.nil?\n\n  # 子节点数量 = 0 or 1\n  if cur.left.nil? || cur.right.nil?\n    # 当子节点数量 = 0 / 1 时, child = null / 该子节点\n    child = cur.left || cur.right\n    # 删除节点 cur\n    if cur != @root\n      if pre.left == cur\n        pre.left = child\n      else\n        pre.right = child\n      end\n    else\n      # 若删除节点为根节点,则重新指定根节点\n      @root = child\n    end\n  # 子节点数量 = 2\n  else\n    # 获取中序遍历中 cur 的下一个节点\n    tmp = cur.right\n    while !tmp.left.nil?\n      tmp = tmp.left\n    end\n    # 递归删除节点 tmp\n    remove(tmp.val)\n    # 用 tmp 覆盖 cur\n    cur.val = tmp.val\n  end\nend\n
可视化运行

全屏观看 >

","path":["第 7 章   树","7.4   二叉搜索树"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#4","level":3,"title":"4.   中序遍历有序","text":"

如图 7-22 所示,二叉树的中序遍历遵循“左 \\(\\rightarrow\\) 根 \\(\\rightarrow\\) 右”的遍历顺序,而二叉搜索树满足“左子节点 \\(<\\) 根节点 \\(<\\) 右子节点”的大小关系。

这意味着在二叉搜索树中进行中序遍历时,总是会优先遍历下一个最小节点,从而得出一个重要性质:二叉搜索树的中序遍历序列是升序的。

利用中序遍历升序的性质,我们在二叉搜索树中获取有序数据仅需 \\(O(n)\\) 时间,无须进行额外的排序操作,非常高效。

图 7-22   二叉搜索树的中序遍历序列

","path":["第 7 章   树","7.4   二叉搜索树"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#742","level":2,"title":"7.4.2   二叉搜索树的效率","text":"

给定一组数据,我们考虑使用数组或二叉搜索树存储。观察表 7-2 ,二叉搜索树的各项操作的时间复杂度都是对数阶,具有稳定且高效的性能。只有在高频添加、低频查找删除数据的场景下,数组比二叉搜索树的效率更高。

表 7-2   数组与搜索树的效率对比

无序数组 二叉搜索树 查找元素 \\(O(n)\\) \\(O(\\log n)\\) 插入元素 \\(O(1)\\) \\(O(\\log n)\\) 删除元素 \\(O(n)\\) \\(O(\\log n)\\)

在理想情况下,二叉搜索树是“平衡”的,这样就可以在 \\(\\log n\\) 轮循环内查找任意节点。

然而,如果我们在二叉搜索树中不断地插入和删除节点,可能导致二叉树退化为图 7-23 所示的链表,这时各种操作的时间复杂度也会退化为 \\(O(n)\\) 。

图 7-23   二叉搜索树退化

","path":["第 7 章   树","7.4   二叉搜索树"],"tags":[]},{"location":"chapter_tree/binary_search_tree/#743","level":2,"title":"7.4.3   二叉搜索树常见应用","text":"","path":["第 7 章   树","7.4   二叉搜索树"],"tags":[]},{"location":"chapter_tree/binary_tree/","level":1,"title":"7.1   二叉树","text":"

二叉树(binary tree)是一种非线性数据结构,代表“祖先”与“后代”之间的派生关系,体现了“一分为二”的分治逻辑。与链表类似,二叉树的基本单元是节点,每个节点包含值、左子节点引用和右子节点引用。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby
class TreeNode:\n    \"\"\"二叉树节点类\"\"\"\n    def __init__(self, val: int):\n        self.val: int = val                # 节点值\n        self.left: TreeNode | None = None  # 左子节点引用\n        self.right: TreeNode | None = None # 右子节点引用\n
/* 二叉树节点结构体 */\nstruct TreeNode {\n    int val;          // 节点值\n    TreeNode *left;   // 左子节点指针\n    TreeNode *right;  // 右子节点指针\n    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}\n};\n
/* 二叉树节点类 */\nclass TreeNode {\n    int val;         // 节点值\n    TreeNode left;   // 左子节点引用\n    TreeNode right;  // 右子节点引用\n    TreeNode(int x) { val = x; }\n}\n
/* 二叉树节点类 */\nclass TreeNode(int? x) {\n    public int? val = x;    // 节点值\n    public TreeNode? left;  // 左子节点引用\n    public TreeNode? right; // 右子节点引用\n}\n
/* 二叉树节点结构体 */\ntype TreeNode struct {\n    Val   int\n    Left  *TreeNode\n    Right *TreeNode\n}\n/* 构造方法 */\nfunc NewTreeNode(v int) *TreeNode {\n    return &TreeNode{\n        Left:  nil, // 左子节点指针\n        Right: nil, // 右子节点指针\n        Val:   v,   // 节点值\n    }\n}\n
/* 二叉树节点类 */\nclass TreeNode {\n    var val: Int // 节点值\n    var left: TreeNode? // 左子节点引用\n    var right: TreeNode? // 右子节点引用\n\n    init(x: Int) {\n        val = x\n    }\n}\n
/* 二叉树节点类 */\nclass TreeNode {\n    val; // 节点值\n    left; // 左子节点指针\n    right; // 右子节点指针\n    constructor(val, left, right) {\n        this.val = val === undefined ? 0 : val;\n        this.left = left === undefined ? null : left;\n        this.right = right === undefined ? null : right;\n    }\n}\n
/* 二叉树节点类 */\nclass TreeNode {\n    val: number;\n    left: TreeNode | null;\n    right: TreeNode | null;\n\n    constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {\n        this.val = val === undefined ? 0 : val; // 节点值\n        this.left = left === undefined ? null : left; // 左子节点引用\n        this.right = right === undefined ? null : right; // 右子节点引用\n    }\n}\n
/* 二叉树节点类 */\nclass TreeNode {\n  int val;         // 节点值\n  TreeNode? left;  // 左子节点引用\n  TreeNode? right; // 右子节点引用\n  TreeNode(this.val, [this.left, this.right]);\n}\n
use std::rc::Rc;\nuse std::cell::RefCell;\n\n/* 二叉树节点结构体 */\nstruct TreeNode {\n    val: i32,                               // 节点值\n    left: Option<Rc<RefCell<TreeNode>>>,    // 左子节点引用\n    right: Option<Rc<RefCell<TreeNode>>>,   // 右子节点引用\n}\n\nimpl TreeNode {\n    /* 构造方法 */\n    fn new(val: i32) -> Rc<RefCell<Self>> {\n        Rc::new(RefCell::new(Self {\n            val,\n            left: None,\n            right: None\n        }))\n    }\n}\n
/* 二叉树节点结构体 */\ntypedef struct TreeNode {\n    int val;                // 节点值\n    int height;             // 节点高度\n    struct TreeNode *left;  // 左子节点指针\n    struct TreeNode *right; // 右子节点指针\n} TreeNode;\n\n/* 构造函数 */\nTreeNode *newTreeNode(int val) {\n    TreeNode *node;\n\n    node = (TreeNode *)malloc(sizeof(TreeNode));\n    node->val = val;\n    node->height = 0;\n    node->left = NULL;\n    node->right = NULL;\n    return node;\n}\n
/* 二叉树节点类 */\nclass TreeNode(val _val: Int) {  // 节点值\n    val left: TreeNode? = null   // 左子节点引用\n    val right: TreeNode? = null  // 右子节点引用\n}\n
### 二叉树节点类 ###\nclass TreeNode\n  attr_accessor :val    # 节点值\n  attr_accessor :left   # 左子节点引用\n  attr_accessor :right  # 右子节点引用\n\n  def initialize(val)\n    @val = val\n  end\nend\n

每个节点都有两个引用(指针),分别指向左子节点(left-child node)和右子节点(right-child node),该节点被称为这两个子节点的父节点(parent node)。当给定一个二叉树的节点时,我们将该节点的左子节点及其以下节点形成的树称为该节点的左子树(left subtree),同理可得右子树(right subtree)。

在二叉树中,除叶节点外,其他所有节点都包含子节点和非空子树。如图 7-1 所示,如果将“节点 2”视为父节点,则其左子节点和右子节点分别是“节点 4”和“节点 5”,左子树是“节点 4 及其以下节点形成的树”,右子树是“节点 5 及其以下节点形成的树”。

图 7-1   父节点、子节点、子树

","path":["第 7 章   树","7.1   二叉树"],"tags":[]},{"location":"chapter_tree/binary_tree/#711","level":2,"title":"7.1.1   二叉树常见术语","text":"

二叉树的常用术语如图 7-2 所示。

图 7-2   二叉树的常用术语

Tip

请注意,我们通常将“高度”和“深度”定义为“经过的边的数量”,但有些题目或教材可能会将其定义为“经过的节点的数量”。在这种情况下,高度和深度都需要加 1 。

","path":["第 7 章   树","7.1   二叉树"],"tags":[]},{"location":"chapter_tree/binary_tree/#712","level":2,"title":"7.1.2   二叉树基本操作","text":"","path":["第 7 章   树","7.1   二叉树"],"tags":[]},{"location":"chapter_tree/binary_tree/#1","level":3,"title":"1.   初始化二叉树","text":"

与链表类似,首先初始化节点,然后构建引用(指针)。

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree.py
# 初始化二叉树\n# 初始化节点\nn1 = TreeNode(val=1)\nn2 = TreeNode(val=2)\nn3 = TreeNode(val=3)\nn4 = TreeNode(val=4)\nn5 = TreeNode(val=5)\n# 构建节点之间的引用(指针)\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
binary_tree.cpp
/* 初始化二叉树 */\n// 初始化节点\nTreeNode* n1 = new TreeNode(1);\nTreeNode* n2 = new TreeNode(2);\nTreeNode* n3 = new TreeNode(3);\nTreeNode* n4 = new TreeNode(4);\nTreeNode* n5 = new TreeNode(5);\n// 构建节点之间的引用(指针)\nn1->left = n2;\nn1->right = n3;\nn2->left = n4;\nn2->right = n5;\n
binary_tree.java
// 初始化节点\nTreeNode n1 = new TreeNode(1);\nTreeNode n2 = new TreeNode(2);\nTreeNode n3 = new TreeNode(3);\nTreeNode n4 = new TreeNode(4);\nTreeNode n5 = new TreeNode(5);\n// 构建节点之间的引用(指针)\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
binary_tree.cs
/* 初始化二叉树 */\n// 初始化节点\nTreeNode n1 = new(1);\nTreeNode n2 = new(2);\nTreeNode n3 = new(3);\nTreeNode n4 = new(4);\nTreeNode n5 = new(5);\n// 构建节点之间的引用(指针)\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
binary_tree.go
/* 初始化二叉树 */\n// 初始化节点\nn1 := NewTreeNode(1)\nn2 := NewTreeNode(2)\nn3 := NewTreeNode(3)\nn4 := NewTreeNode(4)\nn5 := NewTreeNode(5)\n// 构建节点之间的引用(指针)\nn1.Left = n2\nn1.Right = n3\nn2.Left = n4\nn2.Right = n5\n
binary_tree.swift
// 初始化节点\nlet n1 = TreeNode(x: 1)\nlet n2 = TreeNode(x: 2)\nlet n3 = TreeNode(x: 3)\nlet n4 = TreeNode(x: 4)\nlet n5 = TreeNode(x: 5)\n// 构建节点之间的引用(指针)\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
binary_tree.js
/* 初始化二叉树 */\n// 初始化节点\nlet n1 = new TreeNode(1),\n    n2 = new TreeNode(2),\n    n3 = new TreeNode(3),\n    n4 = new TreeNode(4),\n    n5 = new TreeNode(5);\n// 构建节点之间的引用(指针)\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
binary_tree.ts
/* 初始化二叉树 */\n// 初始化节点\nlet n1 = new TreeNode(1),\n    n2 = new TreeNode(2),\n    n3 = new TreeNode(3),\n    n4 = new TreeNode(4),\n    n5 = new TreeNode(5);\n// 构建节点之间的引用(指针)\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
binary_tree.dart
/* 初始化二叉树 */\n// 初始化节点\nTreeNode n1 = new TreeNode(1);\nTreeNode n2 = new TreeNode(2);\nTreeNode n3 = new TreeNode(3);\nTreeNode n4 = new TreeNode(4);\nTreeNode n5 = new TreeNode(5);\n// 构建节点之间的引用(指针)\nn1.left = n2;\nn1.right = n3;\nn2.left = n4;\nn2.right = n5;\n
binary_tree.rs
// 初始化节点\nlet n1 = TreeNode::new(1);\nlet n2 = TreeNode::new(2);\nlet n3 = TreeNode::new(3);\nlet n4 = TreeNode::new(4);\nlet n5 = TreeNode::new(5);\n// 构建节点之间的引用(指针)\nn1.borrow_mut().left = Some(n2.clone());\nn1.borrow_mut().right = Some(n3);\nn2.borrow_mut().left = Some(n4);\nn2.borrow_mut().right = Some(n5);\n
binary_tree.c
/* 初始化二叉树 */\n// 初始化节点\nTreeNode *n1 = newTreeNode(1);\nTreeNode *n2 = newTreeNode(2);\nTreeNode *n3 = newTreeNode(3);\nTreeNode *n4 = newTreeNode(4);\nTreeNode *n5 = newTreeNode(5);\n// 构建节点之间的引用(指针)\nn1->left = n2;\nn1->right = n3;\nn2->left = n4;\nn2->right = n5;\n
binary_tree.kt
// 初始化节点\nval n1 = TreeNode(1)\nval n2 = TreeNode(2)\nval n3 = TreeNode(3)\nval n4 = TreeNode(4)\nval n5 = TreeNode(5)\n// 构建节点之间的引用(指针)\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
binary_tree.rb
# 初始化二叉树\n# 初始化节点\nn1 = TreeNode.new(1)\nn2 = TreeNode.new(2)\nn3 = TreeNode.new(3)\nn4 = TreeNode.new(4)\nn5 = TreeNode.new(5)\n# 构建节点之间的引用(指针)\nn1.left = n2\nn1.right = n3\nn2.left = n4\nn2.right = n5\n
可视化运行

全屏观看 >

","path":["第 7 章   树","7.1   二叉树"],"tags":[]},{"location":"chapter_tree/binary_tree/#2","level":3,"title":"2.   插入与删除节点","text":"

与链表类似,在二叉树中插入与删除节点可以通过修改指针来实现。图 7-3 给出了一个示例。

图 7-3   在二叉树中插入与删除节点

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree.py
# 插入与删除节点\np = TreeNode(0)\n# 在 n1 -> n2 中间插入节点 P\nn1.left = p\np.left = n2\n# 删除节点 P\nn1.left = n2\n
binary_tree.cpp
/* 插入与删除节点 */\nTreeNode* P = new TreeNode(0);\n// 在 n1 -> n2 中间插入节点 P\nn1->left = P;\nP->left = n2;\n// 删除节点 P\nn1->left = n2;\n// 释放内存\ndelete P;\n
binary_tree.java
TreeNode P = new TreeNode(0);\n// 在 n1 -> n2 中间插入节点 P\nn1.left = P;\nP.left = n2;\n// 删除节点 P\nn1.left = n2;\n
binary_tree.cs
/* 插入与删除节点 */\nTreeNode P = new(0);\n// 在 n1 -> n2 中间插入节点 P\nn1.left = P;\nP.left = n2;\n// 删除节点 P\nn1.left = n2;\n
binary_tree.go
/* 插入与删除节点 */\n// 在 n1 -> n2 中间插入节点 P\np := NewTreeNode(0)\nn1.Left = p\np.Left = n2\n// 删除节点 P\nn1.Left = n2\n
binary_tree.swift
let P = TreeNode(x: 0)\n// 在 n1 -> n2 中间插入节点 P\nn1.left = P\nP.left = n2\n// 删除节点 P\nn1.left = n2\n
binary_tree.js
/* 插入与删除节点 */\nlet P = new TreeNode(0);\n// 在 n1 -> n2 中间插入节点 P\nn1.left = P;\nP.left = n2;\n// 删除节点 P\nn1.left = n2;\n
binary_tree.ts
/* 插入与删除节点 */\nconst P = new TreeNode(0);\n// 在 n1 -> n2 中间插入节点 P\nn1.left = P;\nP.left = n2;\n// 删除节点 P\nn1.left = n2;\n
binary_tree.dart
/* 插入与删除节点 */\nTreeNode P = new TreeNode(0);\n// 在 n1 -> n2 中间插入节点 P\nn1.left = P;\nP.left = n2;\n// 删除节点 P\nn1.left = n2;\n
binary_tree.rs
let p = TreeNode::new(0);\n// 在 n1 -> n2 中间插入节点 P\nn1.borrow_mut().left = Some(p.clone());\np.borrow_mut().left = Some(n2.clone());\n// 删除节点 p\nn1.borrow_mut().left = Some(n2);\n
binary_tree.c
/* 插入与删除节点 */\nTreeNode *P = newTreeNode(0);\n// 在 n1 -> n2 中间插入节点 P\nn1->left = P;\nP->left = n2;\n// 删除节点 P\nn1->left = n2;\n// 释放内存\nfree(P);\n
binary_tree.kt
val P = TreeNode(0)\n// 在 n1 -> n2 中间插入节点 P\nn1.left = P\nP.left = n2\n// 删除节点 P\nn1.left = n2\n
binary_tree.rb
# 插入与删除节点\n_p = TreeNode.new(0)\n# 在 n1 -> n2 中间插入节点 _p\nn1.left = _p\n_p.left = n2\n# 删除节点\nn1.left = n2\n
可视化运行

全屏观看 >

Tip

需要注意的是,插入节点可能会改变二叉树的原有逻辑结构,而删除节点通常意味着删除该节点及其所有子树。因此,在二叉树中,插入与删除通常是由一套操作配合完成的,以实现有实际意义的操作。

","path":["第 7 章   树","7.1   二叉树"],"tags":[]},{"location":"chapter_tree/binary_tree/#713","level":2,"title":"7.1.3   常见二叉树类型","text":"","path":["第 7 章   树","7.1   二叉树"],"tags":[]},{"location":"chapter_tree/binary_tree/#1_1","level":3,"title":"1.   完美二叉树","text":"

如图 7-4 所示,完美二叉树(perfect binary tree)所有层的节点都被完全填满。在完美二叉树中,叶节点的度为 \\(0\\) ,其余所有节点的度都为 \\(2\\) ;若树的高度为 \\(h\\) ,则节点总数为 \\(2^{h+1} - 1\\) ,呈现标准的指数级关系,反映了自然界中常见的细胞分裂现象。

Tip

请注意,在中文社区中,完美二叉树常被称为满二叉树。

图 7-4   完美二叉树

","path":["第 7 章   树","7.1   二叉树"],"tags":[]},{"location":"chapter_tree/binary_tree/#2_1","level":3,"title":"2.   完全二叉树","text":"

如图 7-5 所示,完全二叉树(complete binary tree)仅允许最底层的节点不完全填满,且最底层的节点必须从左至右依次连续填充。请注意,完美二叉树也是一棵完全二叉树。

图 7-5   完全二叉树

","path":["第 7 章   树","7.1   二叉树"],"tags":[]},{"location":"chapter_tree/binary_tree/#3","level":3,"title":"3.   完满二叉树","text":"

如图 7-6 所示,完满二叉树(full binary tree)除了叶节点之外,其余所有节点都有两个子节点。

图 7-6   完满二叉树

","path":["第 7 章   树","7.1   二叉树"],"tags":[]},{"location":"chapter_tree/binary_tree/#4","level":3,"title":"4.   平衡二叉树","text":"

如图 7-7 所示,平衡二叉树(balanced binary tree)中任意节点的左子树和右子树的高度之差的绝对值不超过 1 。

图 7-7   平衡二叉树

","path":["第 7 章   树","7.1   二叉树"],"tags":[]},{"location":"chapter_tree/binary_tree/#714","level":2,"title":"7.1.4   二叉树的退化","text":"

图 7-8 展示了二叉树的理想结构与退化结构。当二叉树的每层节点都被填满时,达到“完美二叉树”;而当所有节点都偏向一侧时,二叉树退化为“链表”。

图 7-8   二叉树的最佳结构与最差结构

如表 7-1 所示,在最佳结构和最差结构下,二叉树的叶节点数量、节点总数、高度等达到极大值或极小值。

表 7-1   二叉树的最佳结构与最差结构

完美二叉树 链表 第 \\(i\\) 层的节点数量 \\(2^{i-1}\\) \\(1\\) 高度为 \\(h\\) 的树的叶节点数量 \\(2^h\\) \\(1\\) 高度为 \\(h\\) 的树的节点总数 \\(2^{h+1} - 1\\) \\(h + 1\\) 节点总数为 \\(n\\) 的树的高度 \\(\\log_2 (n+1) - 1\\) \\(n - 1\\)","path":["第 7 章   树","7.1   二叉树"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/","level":1,"title":"7.2   二叉树遍历","text":"

从物理结构的角度来看,树是一种基于链表的数据结构,因此其遍历方式是通过指针逐个访问节点。然而,树是一种非线性数据结构,这使得遍历树比遍历链表更加复杂,需要借助搜索算法来实现。

二叉树常见的遍历方式包括层序遍历、前序遍历、中序遍历和后序遍历等。

","path":["第 7 章   树","7.2   二叉树遍历"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#721","level":2,"title":"7.2.1   层序遍历","text":"

如图 7-9 所示,层序遍历(level-order traversal)从顶部到底部逐层遍历二叉树,并在每一层按照从左到右的顺序访问节点。

层序遍历本质上属于广度优先遍历(breadth-first traversal),也称广度优先搜索(breadth-first search, BFS),它体现了一种“一圈一圈向外扩展”的逐层遍历方式。

图 7-9   二叉树的层序遍历

","path":["第 7 章   树","7.2   二叉树遍历"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#1","level":3,"title":"1.   代码实现","text":"

广度优先遍历通常借助“队列”来实现。队列遵循“先进先出”的规则,而广度优先遍历则遵循“逐层推进”的规则,两者背后的思想是一致的。实现代码如下:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree_bfs.py
def level_order(root: TreeNode | None) -> list[int]:\n    \"\"\"层序遍历\"\"\"\n    # 初始化队列,加入根节点\n    queue: deque[TreeNode] = deque()\n    queue.append(root)\n    # 初始化一个列表,用于保存遍历序列\n    res = []\n    while queue:\n        node: TreeNode = queue.popleft()  # 队列出队\n        res.append(node.val)  # 保存节点值\n        if node.left is not None:\n            queue.append(node.left)  # 左子节点入队\n        if node.right is not None:\n            queue.append(node.right)  # 右子节点入队\n    return res\n
binary_tree_bfs.cpp
/* 层序遍历 */\nvector<int> levelOrder(TreeNode *root) {\n    // 初始化队列,加入根节点\n    queue<TreeNode *> queue;\n    queue.push(root);\n    // 初始化一个列表,用于保存遍历序列\n    vector<int> vec;\n    while (!queue.empty()) {\n        TreeNode *node = queue.front();\n        queue.pop();              // 队列出队\n        vec.push_back(node->val); // 保存节点值\n        if (node->left != nullptr)\n            queue.push(node->left); // 左子节点入队\n        if (node->right != nullptr)\n            queue.push(node->right); // 右子节点入队\n    }\n    return vec;\n}\n
binary_tree_bfs.java
/* 层序遍历 */\nList<Integer> levelOrder(TreeNode root) {\n    // 初始化队列,加入根节点\n    Queue<TreeNode> queue = new LinkedList<>();\n    queue.add(root);\n    // 初始化一个列表,用于保存遍历序列\n    List<Integer> list = new ArrayList<>();\n    while (!queue.isEmpty()) {\n        TreeNode node = queue.poll(); // 队列出队\n        list.add(node.val);           // 保存节点值\n        if (node.left != null)\n            queue.offer(node.left);   // 左子节点入队\n        if (node.right != null)\n            queue.offer(node.right);  // 右子节点入队\n    }\n    return list;\n}\n
binary_tree_bfs.cs
/* 层序遍历 */\nList<int> LevelOrder(TreeNode root) {\n    // 初始化队列,加入根节点\n    Queue<TreeNode> queue = new();\n    queue.Enqueue(root);\n    // 初始化一个列表,用于保存遍历序列\n    List<int> list = [];\n    while (queue.Count != 0) {\n        TreeNode node = queue.Dequeue(); // 队列出队\n        list.Add(node.val!.Value);       // 保存节点值\n        if (node.left != null)\n            queue.Enqueue(node.left);    // 左子节点入队\n        if (node.right != null)\n            queue.Enqueue(node.right);   // 右子节点入队\n    }\n    return list;\n}\n
binary_tree_bfs.go
/* 层序遍历 */\nfunc levelOrder(root *TreeNode) []any {\n    // 初始化队列,加入根节点\n    queue := list.New()\n    queue.PushBack(root)\n    // 初始化一个切片,用于保存遍历序列\n    nums := make([]any, 0)\n    for queue.Len() > 0 {\n        // 队列出队\n        node := queue.Remove(queue.Front()).(*TreeNode)\n        // 保存节点值\n        nums = append(nums, node.Val)\n        if node.Left != nil {\n            // 左子节点入队\n            queue.PushBack(node.Left)\n        }\n        if node.Right != nil {\n            // 右子节点入队\n            queue.PushBack(node.Right)\n        }\n    }\n    return nums\n}\n
binary_tree_bfs.swift
/* 层序遍历 */\nfunc levelOrder(root: TreeNode) -> [Int] {\n    // 初始化队列,加入根节点\n    var queue: [TreeNode] = [root]\n    // 初始化一个列表,用于保存遍历序列\n    var list: [Int] = []\n    while !queue.isEmpty {\n        let node = queue.removeFirst() // 队列出队\n        list.append(node.val) // 保存节点值\n        if let left = node.left {\n            queue.append(left) // 左子节点入队\n        }\n        if let right = node.right {\n            queue.append(right) // 右子节点入队\n        }\n    }\n    return list\n}\n
binary_tree_bfs.js
/* 层序遍历 */\nfunction levelOrder(root) {\n    // 初始化队列,加入根节点\n    const queue = [root];\n    // 初始化一个列表,用于保存遍历序列\n    const list = [];\n    while (queue.length) {\n        let node = queue.shift(); // 队列出队\n        list.push(node.val); // 保存节点值\n        if (node.left) queue.push(node.left); // 左子节点入队\n        if (node.right) queue.push(node.right); // 右子节点入队\n    }\n    return list;\n}\n
binary_tree_bfs.ts
/* 层序遍历 */\nfunction levelOrder(root: TreeNode | null): number[] {\n    // 初始化队列,加入根节点\n    const queue = [root];\n    // 初始化一个列表,用于保存遍历序列\n    const list: number[] = [];\n    while (queue.length) {\n        let node = queue.shift() as TreeNode; // 队列出队\n        list.push(node.val); // 保存节点值\n        if (node.left) {\n            queue.push(node.left); // 左子节点入队\n        }\n        if (node.right) {\n            queue.push(node.right); // 右子节点入队\n        }\n    }\n    return list;\n}\n
binary_tree_bfs.dart
/* 层序遍历 */\nList<int> levelOrder(TreeNode? root) {\n  // 初始化队列,加入根节点\n  Queue<TreeNode?> queue = Queue();\n  queue.add(root);\n  // 初始化一个列表,用于保存遍历序列\n  List<int> res = [];\n  while (queue.isNotEmpty) {\n    TreeNode? node = queue.removeFirst(); // 队列出队\n    res.add(node!.val); // 保存节点值\n    if (node.left != null) queue.add(node.left); // 左子节点入队\n    if (node.right != null) queue.add(node.right); // 右子节点入队\n  }\n  return res;\n}\n
binary_tree_bfs.rs
/* 层序遍历 */\nfn level_order(root: &Rc<RefCell<TreeNode>>) -> Vec<i32> {\n    // 初始化队列,加入根节点\n    let mut que = VecDeque::new();\n    que.push_back(root.clone());\n    // 初始化一个列表,用于保存遍历序列\n    let mut vec = Vec::new();\n\n    while let Some(node) = que.pop_front() {\n        // 队列出队\n        vec.push(node.borrow().val); // 保存节点值\n        if let Some(left) = node.borrow().left.as_ref() {\n            que.push_back(left.clone()); // 左子节点入队\n        }\n        if let Some(right) = node.borrow().right.as_ref() {\n            que.push_back(right.clone()); // 右子节点入队\n        };\n    }\n    vec\n}\n
binary_tree_bfs.c
/* 层序遍历 */\nint *levelOrder(TreeNode *root, int *size) {\n    /* 辅助队列 */\n    int front, rear;\n    int index, *arr;\n    TreeNode *node;\n    TreeNode **queue;\n\n    /* 辅助队列 */\n    queue = (TreeNode **)malloc(sizeof(TreeNode *) * MAX_SIZE);\n    // 队列指针\n    front = 0, rear = 0;\n    // 加入根节点\n    queue[rear++] = root;\n    // 初始化一个列表,用于保存遍历序列\n    /* 辅助数组 */\n    arr = (int *)malloc(sizeof(int) * MAX_SIZE);\n    // 数组指针\n    index = 0;\n    while (front < rear) {\n        // 队列出队\n        node = queue[front++];\n        // 保存节点值\n        arr[index++] = node->val;\n        if (node->left != NULL) {\n            // 左子节点入队\n            queue[rear++] = node->left;\n        }\n        if (node->right != NULL) {\n            // 右子节点入队\n            queue[rear++] = node->right;\n        }\n    }\n    // 更新数组长度的值\n    *size = index;\n    arr = realloc(arr, sizeof(int) * (*size));\n\n    // 释放辅助数组空间\n    free(queue);\n    return arr;\n}\n
binary_tree_bfs.kt
/* 层序遍历 */\nfun levelOrder(root: TreeNode?): MutableList<Int> {\n    // 初始化队列,加入根节点\n    val queue = LinkedList<TreeNode?>()\n    queue.add(root)\n    // 初始化一个列表,用于保存遍历序列\n    val list = mutableListOf<Int>()\n    while (queue.isNotEmpty()) {\n        val node = queue.poll()      // 队列出队\n        list.add(node?._val!!)       // 保存节点值\n        if (node.left != null)\n            queue.offer(node.left)   // 左子节点入队\n        if (node.right != null)\n            queue.offer(node.right)  // 右子节点入队\n    }\n    return list\n}\n
binary_tree_bfs.rb
### 层序遍历 ###\ndef level_order(root)\n  # 初始化队列,加入根节点\n  queue = [root]\n  # 初始化一个列表,用于保存遍历序列\n  res = []\n  while !queue.empty?\n    node = queue.shift # 队列出队\n    res << node.val # 保存节点值\n    queue << node.left unless node.left.nil? # 左子节点入队\n    queue << node.right unless node.right.nil? # 右子节点入队\n  end\n  res\nend\n
可视化运行

全屏观看 >

","path":["第 7 章   树","7.2   二叉树遍历"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#2","level":3,"title":"2.   复杂度分析","text":"","path":["第 7 章   树","7.2   二叉树遍历"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#722","level":2,"title":"7.2.2   前序、中序、后序遍历","text":"

相应地,前序、中序和后序遍历都属于深度优先遍历(depth-first traversal),也称深度优先搜索(depth-first search, DFS),它体现了一种“先走到尽头,再回溯继续”的遍历方式。

图 7-10 展示了对二叉树进行深度优先遍历的工作原理。深度优先遍历就像是绕着整棵二叉树的外围“走”一圈,在每个节点都会遇到三个位置,分别对应前序遍历、中序遍历和后序遍历。

图 7-10   二叉搜索树的前序、中序、后序遍历

","path":["第 7 章   树","7.2   二叉树遍历"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#1_1","level":3,"title":"1.   代码实现","text":"

深度优先搜索通常基于递归实现:

PythonC++JavaC#GoSwiftJSTSDartRustCKotlinRuby binary_tree_dfs.py
def pre_order(root: TreeNode | None):\n    \"\"\"前序遍历\"\"\"\n    if root is None:\n        return\n    # 访问优先级:根节点 -> 左子树 -> 右子树\n    res.append(root.val)\n    pre_order(root=root.left)\n    pre_order(root=root.right)\n\ndef in_order(root: TreeNode | None):\n    \"\"\"中序遍历\"\"\"\n    if root is None:\n        return\n    # 访问优先级:左子树 -> 根节点 -> 右子树\n    in_order(root=root.left)\n    res.append(root.val)\n    in_order(root=root.right)\n\ndef post_order(root: TreeNode | None):\n    \"\"\"后序遍历\"\"\"\n    if root is None:\n        return\n    # 访问优先级:左子树 -> 右子树 -> 根节点\n    post_order(root=root.left)\n    post_order(root=root.right)\n    res.append(root.val)\n
binary_tree_dfs.cpp
/* 前序遍历 */\nvoid preOrder(TreeNode *root) {\n    if (root == nullptr)\n        return;\n    // 访问优先级:根节点 -> 左子树 -> 右子树\n    vec.push_back(root->val);\n    preOrder(root->left);\n    preOrder(root->right);\n}\n\n/* 中序遍历 */\nvoid inOrder(TreeNode *root) {\n    if (root == nullptr)\n        return;\n    // 访问优先级:左子树 -> 根节点 -> 右子树\n    inOrder(root->left);\n    vec.push_back(root->val);\n    inOrder(root->right);\n}\n\n/* 后序遍历 */\nvoid postOrder(TreeNode *root) {\n    if (root == nullptr)\n        return;\n    // 访问优先级:左子树 -> 右子树 -> 根节点\n    postOrder(root->left);\n    postOrder(root->right);\n    vec.push_back(root->val);\n}\n
binary_tree_dfs.java
/* 前序遍历 */\nvoid preOrder(TreeNode root) {\n    if (root == null)\n        return;\n    // 访问优先级:根节点 -> 左子树 -> 右子树\n    list.add(root.val);\n    preOrder(root.left);\n    preOrder(root.right);\n}\n\n/* 中序遍历 */\nvoid inOrder(TreeNode root) {\n    if (root == null)\n        return;\n    // 访问优先级:左子树 -> 根节点 -> 右子树\n    inOrder(root.left);\n    list.add(root.val);\n    inOrder(root.right);\n}\n\n/* 后序遍历 */\nvoid postOrder(TreeNode root) {\n    if (root == null)\n        return;\n    // 访问优先级:左子树 -> 右子树 -> 根节点\n    postOrder(root.left);\n    postOrder(root.right);\n    list.add(root.val);\n}\n
binary_tree_dfs.cs
/* 前序遍历 */\nvoid PreOrder(TreeNode? root) {\n    if (root == null) return;\n    // 访问优先级:根节点 -> 左子树 -> 右子树\n    list.Add(root.val!.Value);\n    PreOrder(root.left);\n    PreOrder(root.right);\n}\n\n/* 中序遍历 */\nvoid InOrder(TreeNode? root) {\n    if (root == null) return;\n    // 访问优先级:左子树 -> 根节点 -> 右子树\n    InOrder(root.left);\n    list.Add(root.val!.Value);\n    InOrder(root.right);\n}\n\n/* 后序遍历 */\nvoid PostOrder(TreeNode? root) {\n    if (root == null) return;\n    // 访问优先级:左子树 -> 右子树 -> 根节点\n    PostOrder(root.left);\n    PostOrder(root.right);\n    list.Add(root.val!.Value);\n}\n
binary_tree_dfs.go
/* 前序遍历 */\nfunc preOrder(node *TreeNode) {\n    if node == nil {\n        return\n    }\n    // 访问优先级:根节点 -> 左子树 -> 右子树\n    nums = append(nums, node.Val)\n    preOrder(node.Left)\n    preOrder(node.Right)\n}\n\n/* 中序遍历 */\nfunc inOrder(node *TreeNode) {\n    if node == nil {\n        return\n    }\n    // 访问优先级:左子树 -> 根节点 -> 右子树\n    inOrder(node.Left)\n    nums = append(nums, node.Val)\n    inOrder(node.Right)\n}\n\n/* 后序遍历 */\nfunc postOrder(node *TreeNode) {\n    if node == nil {\n        return\n    }\n    // 访问优先级:左子树 -> 右子树 -> 根节点\n    postOrder(node.Left)\n    postOrder(node.Right)\n    nums = append(nums, node.Val)\n}\n
binary_tree_dfs.swift
/* 前序遍历 */\nfunc preOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // 访问优先级:根节点 -> 左子树 -> 右子树\n    list.append(root.val)\n    preOrder(root: root.left)\n    preOrder(root: root.right)\n}\n\n/* 中序遍历 */\nfunc inOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // 访问优先级:左子树 -> 根节点 -> 右子树\n    inOrder(root: root.left)\n    list.append(root.val)\n    inOrder(root: root.right)\n}\n\n/* 后序遍历 */\nfunc postOrder(root: TreeNode?) {\n    guard let root = root else {\n        return\n    }\n    // 访问优先级:左子树 -> 右子树 -> 根节点\n    postOrder(root: root.left)\n    postOrder(root: root.right)\n    list.append(root.val)\n}\n
binary_tree_dfs.js
/* 前序遍历 */\nfunction preOrder(root) {\n    if (root === null) return;\n    // 访问优先级:根节点 -> 左子树 -> 右子树\n    list.push(root.val);\n    preOrder(root.left);\n    preOrder(root.right);\n}\n\n/* 中序遍历 */\nfunction inOrder(root) {\n    if (root === null) return;\n    // 访问优先级:左子树 -> 根节点 -> 右子树\n    inOrder(root.left);\n    list.push(root.val);\n    inOrder(root.right);\n}\n\n/* 后序遍历 */\nfunction postOrder(root) {\n    if (root === null) return;\n    // 访问优先级:左子树 -> 右子树 -> 根节点\n    postOrder(root.left);\n    postOrder(root.right);\n    list.push(root.val);\n}\n
binary_tree_dfs.ts
/* 前序遍历 */\nfunction preOrder(root: TreeNode | null): void {\n    if (root === null) {\n        return;\n    }\n    // 访问优先级:根节点 -> 左子树 -> 右子树\n    list.push(root.val);\n    preOrder(root.left);\n    preOrder(root.right);\n}\n\n/* 中序遍历 */\nfunction inOrder(root: TreeNode | null): void {\n    if (root === null) {\n        return;\n    }\n    // 访问优先级:左子树 -> 根节点 -> 右子树\n    inOrder(root.left);\n    list.push(root.val);\n    inOrder(root.right);\n}\n\n/* 后序遍历 */\nfunction postOrder(root: TreeNode | null): void {\n    if (root === null) {\n        return;\n    }\n    // 访问优先级:左子树 -> 右子树 -> 根节点\n    postOrder(root.left);\n    postOrder(root.right);\n    list.push(root.val);\n}\n
binary_tree_dfs.dart
/* 前序遍历 */\nvoid preOrder(TreeNode? node) {\n  if (node == null) return;\n  // 访问优先级:根节点 -> 左子树 -> 右子树\n  list.add(node.val);\n  preOrder(node.left);\n  preOrder(node.right);\n}\n\n/* 中序遍历 */\nvoid inOrder(TreeNode? node) {\n  if (node == null) return;\n  // 访问优先级:左子树 -> 根节点 -> 右子树\n  inOrder(node.left);\n  list.add(node.val);\n  inOrder(node.right);\n}\n\n/* 后序遍历 */\nvoid postOrder(TreeNode? node) {\n  if (node == null) return;\n  // 访问优先级:左子树 -> 右子树 -> 根节点\n  postOrder(node.left);\n  postOrder(node.right);\n  list.add(node.val);\n}\n
binary_tree_dfs.rs
/* 前序遍历 */\nfn pre_order(root: Option<&Rc<RefCell<TreeNode>>>) -> Vec<i32> {\n    let mut result = vec![];\n\n    fn dfs(root: Option<&Rc<RefCell<TreeNode>>>, res: &mut Vec<i32>) {\n        if let Some(node) = root {\n            // 访问优先级:根节点 -> 左子树 -> 右子树\n            let node = node.borrow();\n            res.push(node.val);\n            dfs(node.left.as_ref(), res);\n            dfs(node.right.as_ref(), res);\n        }\n    }\n    dfs(root, &mut result);\n\n    result\n}\n\n/* 中序遍历 */\nfn in_order(root: Option<&Rc<RefCell<TreeNode>>>) -> Vec<i32> {\n    let mut result = vec![];\n\n    fn dfs(root: Option<&Rc<RefCell<TreeNode>>>, res: &mut Vec<i32>) {\n        if let Some(node) = root {\n            // 访问优先级:左子树 -> 根节点 -> 右子树\n            let node = node.borrow();\n            dfs(node.left.as_ref(), res);\n            res.push(node.val);\n            dfs(node.right.as_ref(), res);\n        }\n    }\n    dfs(root, &mut result);\n\n    result\n}\n\n/* 后序遍历 */\nfn post_order(root: Option<&Rc<RefCell<TreeNode>>>) -> Vec<i32> {\n    let mut result = vec![];\n\n    fn dfs(root: Option<&Rc<RefCell<TreeNode>>>, res: &mut Vec<i32>) {\n        if let Some(node) = root {\n            // 访问优先级:左子树 -> 右子树 -> 根节点\n            let node = node.borrow();\n            dfs(node.left.as_ref(), res);\n            dfs(node.right.as_ref(), res);\n            res.push(node.val);\n        }\n    }\n\n    dfs(root, &mut result);\n\n    result\n}\n
binary_tree_dfs.c
/* 前序遍历 */\nvoid preOrder(TreeNode *root, int *size) {\n    if (root == NULL)\n        return;\n    // 访问优先级:根节点 -> 左子树 -> 右子树\n    arr[(*size)++] = root->val;\n    preOrder(root->left, size);\n    preOrder(root->right, size);\n}\n\n/* 中序遍历 */\nvoid inOrder(TreeNode *root, int *size) {\n    if (root == NULL)\n        return;\n    // 访问优先级:左子树 -> 根节点 -> 右子树\n    inOrder(root->left, size);\n    arr[(*size)++] = root->val;\n    inOrder(root->right, size);\n}\n\n/* 后序遍历 */\nvoid postOrder(TreeNode *root, int *size) {\n    if (root == NULL)\n        return;\n    // 访问优先级:左子树 -> 右子树 -> 根节点\n    postOrder(root->left, size);\n    postOrder(root->right, size);\n    arr[(*size)++] = root->val;\n}\n
binary_tree_dfs.kt
/* 前序遍历 */\nfun preOrder(root: TreeNode?) {\n    if (root == null) return\n    // 访问优先级:根节点 -> 左子树 -> 右子树\n    list.add(root._val)\n    preOrder(root.left)\n    preOrder(root.right)\n}\n\n/* 中序遍历 */\nfun inOrder(root: TreeNode?) {\n    if (root == null) return\n    // 访问优先级:左子树 -> 根节点 -> 右子树\n    inOrder(root.left)\n    list.add(root._val)\n    inOrder(root.right)\n}\n\n/* 后序遍历 */\nfun postOrder(root: TreeNode?) {\n    if (root == null) return\n    // 访问优先级:左子树 -> 右子树 -> 根节点\n    postOrder(root.left)\n    postOrder(root.right)\n    list.add(root._val)\n}\n
binary_tree_dfs.rb
### 前序遍历 ###\ndef pre_order(root)\n  return if root.nil?\n\n  # 访问优先级:根节点 -> 左子树 -> 右子树\n  $res << root.val\n  pre_order(root.left)\n  pre_order(root.right)\nend\n\n### 中序遍历 ###\ndef in_order(root)\n  return if root.nil?\n\n  # 访问优先级:左子树 -> 根节点 -> 右子树\n  in_order(root.left)\n  $res << root.val\n  in_order(root.right)\nend\n\n### 后序遍历 ###\ndef post_order(root)\n  return if root.nil?\n\n  # 访问优先级:左子树 -> 右子树 -> 根节点\n  post_order(root.left)\n  post_order(root.right)\n  $res << root.val\nend\n
可视化运行

全屏观看 >

Tip

深度优先搜索也可以基于迭代实现,有兴趣的读者可以自行研究。

图 7-11 展示了前序遍历二叉树的递归过程,其可分为“递”和“归”两个逆向的部分。

  1. “递”表示开启新方法,程序在此过程中访问下一个节点。
  2. “归”表示函数返回,代表当前节点已经访问完毕。
<1><2><3><4><5><6><7><8><9><10><11>

图 7-11   前序遍历的递归过程

","path":["第 7 章   树","7.2   二叉树遍历"],"tags":[]},{"location":"chapter_tree/binary_tree_traversal/#2_1","level":3,"title":"2.   复杂度分析","text":"","path":["第 7 章   树","7.2   二叉树遍历"],"tags":[]},{"location":"chapter_tree/summary/","level":1,"title":"7.6   小结","text":"","path":["第 7 章   树","7.6   小结"],"tags":[]},{"location":"chapter_tree/summary/#1","level":3,"title":"1.   重点回顾","text":"","path":["第 7 章   树","7.6   小结"],"tags":[]},{"location":"chapter_tree/summary/#2-q-a","level":3,"title":"2.   Q & A","text":"

Q:对于只有一个节点的二叉树,树的高度和根节点的深度都是 \\(0\\) 吗?

是的,因为高度和深度通常定义为“经过的边的数量”。

Q:二叉树中的插入与删除一般由一套操作配合完成,这里的“一套操作”指什么呢?可以理解为资源的子节点的资源释放吗?

拿二叉搜索树来举例,删除节点操作要分三种情况处理,其中每种情况都需要进行多个步骤的节点操作。

Q:为什么 DFS 遍历二叉树有前、中、后三种顺序,分别有什么用呢?

与顺序和逆序遍历数组类似,前序、中序、后序遍历是三种二叉树遍历方法,我们可以使用它们得到一个特定顺序的遍历结果。例如在二叉搜索树中,由于节点大小满足 左子节点值 < 根节点值 < 右子节点值 ,因此我们只要按照“左 \\(\\rightarrow\\) 根 \\(\\rightarrow\\) 右”的优先级遍历树,就可以获得有序的节点序列。

Q:右旋操作是处理失衡节点 nodechildgrand_child 之间的关系,那 node 的父节点和 node 原来的连接不需要维护吗?右旋操作后岂不是断掉了?

我们需要从递归的视角来看这个问题。右旋操作 right_rotate(root) 传入的是子树的根节点,最终 return child 返回旋转之后的子树的根节点。子树的根节点和其父节点的连接是在该函数返回后完成的,不属于右旋操作的维护范围。

Q:在 C++ 中,函数被划分到 privatepublic 中,这方面有什么考量吗?为什么要将 height() 函数和 updateHeight() 函数分别放在 publicprivate 中呢?

主要看方法的使用范围,如果方法只在类内部使用,那么就设计为 private 。例如,用户单独调用 updateHeight() 是没有意义的,它只是插入、删除操作中的一步。而 height() 是访问节点高度,类似于 vector.size() ,因此设置成 public 以便使用。

Q:如何从一组输入数据构建一棵二叉搜索树?根节点的选择是不是很重要?

是的,构建树的方法已在二叉搜索树代码中的 build_tree() 方法中给出。至于根节点的选择,我们通常会将输入数据排序,然后将中点元素作为根节点,再递归地构建左右子树。这样做可以最大程度保证树的平衡性。

Q:在 Java 中,字符串对比是否一定要用 equals() 方法?

在 Java 中,对于基本数据类型,== 用于对比两个变量的值是否相等。对于引用类型,两种符号的工作原理是不同的。

因此,如果要对比值,我们应该使用 equals() 。然而,通过 String a = \"hi\"; String b = \"hi\"; 初始化的字符串都存储在字符串常量池中,它们指向同一个对象,因此也可以用 a == b 来比较两个字符串的内容。

Q:广度优先遍历到最底层之前,队列中的节点数量是 \\(2^h\\) 吗?

是的,例如高度 \\(h = 2\\) 的满二叉树,其节点总数 \\(n = 7\\) ,则底层节点数量 \\(4 = 2^h = (n + 1) / 2\\) 。

","path":["第 7 章   树","7.6   小结"],"tags":[]}]}