diff --git a/docs/assets/avatar/avatar_shevkun.jpg b/docs/assets/avatar/avatar_shevkun.jpg new file mode 100644 index 000000000..aa660a246 Binary files /dev/null and b/docs/assets/avatar/avatar_shevkun.jpg differ diff --git a/docs/chapter_appendix/terminology.md b/docs/chapter_appendix/terminology.md index 5ff24b7cc..82fb6455c 100644 --- a/docs/chapter_appendix/terminology.md +++ b/docs/chapter_appendix/terminology.md @@ -1,137 +1,134 @@ # 术语表 -下表列出了书中出现的重要术语,值得注意以下几点。 - -- 建议记住名词的英文叫法,以便阅读英文文献。 -- 部分名词在简体中文和繁体中文下的叫法不同。 +下表列出了书中出现的重要术语。建议记住各个名词的英文叫法,以便阅读英文文献。

  数据结构与算法的重要名词

-| 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$ 记号 | 大 $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 树 | 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$ 问题 | 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$ 皇后问题 | $n$ 皇后問題 | -| dynamic programming | 动态规划 | 動態規劃 | -| initial state | 初始状态 | 初始狀態 | -| state-transition equation | 状态转移方程 | 狀態轉移方程 | -| knapsack problem | 背包问题 | 背包問題 | -| edit distance problem | 编辑距离问题 | 編輯距離問題 | -| greedy algorithm | 贪心算法 | 貪婪演算法 | +| 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 | 贪心算法 | diff --git a/docs/chapter_computational_complexity/space_complexity.md b/docs/chapter_computational_complexity/space_complexity.md index 156f146ff..13b48eb28 100755 --- a/docs/chapter_computational_complexity/space_complexity.md +++ b/docs/chapter_computational_complexity/space_complexity.md @@ -806,8 +806,8 @@ $$ \begin{aligned} -O(1) < O(\log n) < O(n) < O(n^2) < O(2^n) \newline -\text{常数阶} < \text{对数阶} < \text{线性阶} < \text{平方阶} < \text{指数阶} +& O(1) < O(\log n) < O(n) < O(n^2) < O(2^n) \newline +& \text{常数阶} < \text{对数阶} < \text{线性阶} < \text{平方阶} < \text{指数阶} \end{aligned} $$ diff --git a/docs/chapter_computational_complexity/time_complexity.md b/docs/chapter_computational_complexity/time_complexity.md index fe913c8ae..757638279 100755 --- a/docs/chapter_computational_complexity/time_complexity.md +++ b/docs/chapter_computational_complexity/time_complexity.md @@ -992,8 +992,8 @@ $$ $$ \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{阶乘阶} +& 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} $$ diff --git a/docs/chapter_data_structure/character_encoding.md b/docs/chapter_data_structure/character_encoding.md index 12f9a9df4..64fa4c3ff 100644 --- a/docs/chapter_data_structure/character_encoding.md +++ b/docs/chapter_data_structure/character_encoding.md @@ -24,11 +24,9 @@ 那个时代的研究人员就在想:**如果推出一个足够完整的字符集,将世界范围内的所有语言和符号都收录其中,不就可以解决跨语言环境和乱码问题了吗**?在这种想法的驱动下,一个大而全的字符集 Unicode 应运而生。 -Unicode 的中文名称为“统一码”,理论上能容纳 100 多万个字符。它致力于将全球范围内的字符纳入统一的字符集之中,提供一种通用的字符集来处理和显示各种语言文字,减少因为编码标准不同而产生的乱码问题。 +Unicode 的中文名称为“统一码”,理论上能容纳 100 多万个字符。它致力于将全球范围内的字符纳入统一的字符集之中,提供一种通用的字符集来处理和显示各种语言文字,减少因为编码标准不同而产生的乱码问题。自 1991 年发布以来,Unicode 不断扩充新的语言与字符。截至 2022 年 9 月,Unicode 已经包含 149186 个字符,包括各种语言的字符、符号甚至表情符号等。 -自 1991 年发布以来,Unicode 不断扩充新的语言与字符。截至 2022 年 9 月,Unicode 已经包含 149186 个字符,包括各种语言的字符、符号甚至表情符号等。Unicode 将每个字符映射为一个码点(字符编号),其取值范围为 0 至 1114111(即 U+0000 至 U+10FFFF),构成了统一的字符编号空间。 - -Unicode 是一种通用字符集,本质上是给每个字符分配一个编号(称为“码点”),**但它并没有规定在计算机中如何存储这些字符码点**。我们不禁会问:当多种长度的 Unicode 码点同时出现在一个文本中时,系统如何解析字符?例如给定一个长度为 2 字节的编码,系统如何确认它是一个 2 字节的字符还是两个 1 字节的字符? +Unicode 作为一种通用字符集,本质上是给每个字符分配唯一的“码点”(字符编号),其取值范围为 U+0000 至 U+10FFFF,构成了统一的字符编号空间。然而,**Unicode 并没有规定在计算机中如何存储这些字符码点**。我们不禁会问:当多种长度的 Unicode 码点同时出现在一个文本中时,系统如何解析字符?例如给定一个长度为 2 字节的编码,系统如何确认它是一个 2 字节的字符还是两个 1 字节的字符? 对于以上问题,**一种直接的解决方案是将所有字符存储为等长的编码**。如下图所示,“Hello”中的每个字符占用 1 字节,“算法”中的每个字符占用 2 字节。我们可以通过高位填 0 将“Hello 算法”中的所有字符都编码为 2 字节长度。这样系统就可以每隔 2 字节解析一个字符,恢复这个短语的内容了。 diff --git a/docs/chapter_preface/about_the_book.md b/docs/chapter_preface/about_the_book.md index 76311eefb..62573db46 100644 --- a/docs/chapter_preface/about_the_book.md +++ b/docs/chapter_preface/about_the_book.md @@ -30,13 +30,13 @@ ## 致谢 -本书在开源社区众多贡献者的共同努力下不断完善。感谢每一位投入时间与精力的撰稿人,他们是(按照 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、IsChristina、xBLACKICEx、guowei-gong、Cathay-Chen、pengchzn、QiLOL、magentaqin、hello-ikun、JoseHung、qualifier1024、thomasq0、sunshinesDL、L-Super、Guanngxu、Transmigration-zhou、WSL0809、Slone123c、lhxsm、yuan0221、what-is-me、Shyam-Chen、theNefelibatas、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、MolDuM、Nigh、Dr-XYZ、XC-Zero、reeswell、PXG-XPG、NI-SW、Horbin-Magician、Enlightenus、YangXuanyi、beatrix-chan、DullSword、xjr7670、jiaxianhua、qq909244296、iStig、boloboloda、hts0000、gledfish、wenjianmin、keshida、kilikilikid、lclc6、lwbaptx、linyejoe2、liuxjerry、llql1211、fbigm、echo1937、szu17dmy、dshlstarr、Yucao-cy、coderlef、czruby、bongbongbakudan、beintentional、ZongYangL、ZhongYuuu、ZhongGuanbin、hezhizhen、linzeyan、ZJKung、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、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、GaochaoZhu、hopkings2008、yang-le、realwujing、Evilrabbit520、Umer-Jahangir、Turing-1024-Lee、Suremotoo、paoxiaomooo、Chieko-Seren、Allen-Scai、ymmmas、Risuntsy、Richard-Zhang1019、RafaelCaso、qingpeng9802、primexiao、Urbaner3、zhongfq、nidhoggfgg、MwumLi、CreatorMetaSky、martinx、ZnYang2018、hugtyftg、logan-qiu、psychelzh、Keynman、KeiichiKasai 和 KawaiiAsh。 +本书在开源社区众多贡献者的共同努力下不断完善。感谢每一位投入时间与精力的撰稿人,他们是(按照 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 完成(按照首字母顺序排列)。感谢他们付出的时间与精力,正是他们确保了各语言代码的规范与统一。 -本书的繁体中文版由 Shyam-Chen 和 Dr-XYZ 审阅,英文版由 yuelinxin、K3v123、QiLOL、Phoenix0415、SamJin98、yanedie、RafaelCaso、pengchzn、thomasq0 和 magentaqin 审阅,日文版由 eltociear 审阅。正是因为他们的持续贡献,这本书才能够服务于更广泛的读者群体,感谢他们。 +本书的英文版由 yuelinxin、K3v123、magentaqin、QiLOL、Phoenix0415、SamJin98、yanedie、RafaelCaso、pengchzn 和 thomasq0 审阅;日文版由 eltociear 审阅;俄文版由 И. А. Шевкун 和 Yuyan Huang 审阅;繁体中文版由 Shyam-Chen 和 Dr-XYZ 审阅。正是因为他们的贡献,这本书才能够服务于更广泛的读者群体,感谢他们。 -本书的 ePub 电子书生成工具由 zhongfq 开发。感谢他的贡献,为读者提供了更加自由的阅读方式。 +本书的 ePub 电子书生成工具由 zhongfq 开发。感谢他的贡献,为读者提供了更灵活的阅读方式。 在本书的创作过程中,我得到了许多人的帮助。 @@ -45,7 +45,7 @@ - 感谢腾宝、琦宝、飞宝为本书起了一个富有创意的名字,唤起大家写下第一行代码“Hello World!”的美好回忆; - 感谢校铨在知识产权方面提供的专业帮助,这对本开源书的完善起到了重要作用; - 感谢苏潼为本书设计了精美的封面和 logo ,并在我的强迫症的驱使下多次耐心修改; -- 感谢 @squidfunk 提供的排版建议,以及他开发的开源文档主题 [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material/tree/master) 。 +- 感谢 @squidfunk 提供的排版建议,以及他开发的开源文档主题 [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material) 。 在写作过程中,我阅读了许多关于数据结构与算法的教材和文章。这些作品为本书提供了优秀的范本,确保了本书内容的准确性与品质。在此感谢所有老师和前辈的杰出贡献! diff --git a/docs/index.html b/docs/index.html index f1dec6193..af2d9038c 100644 --- a/docs/index.html +++ b/docs/index.html @@ -110,7 +110,7 @@
Cover
- 获取纸质书 + 了解纸质书 @@ -251,7 +251,7 @@
-

代码译者

+

代码审阅者

本书多语言代码版本由以下开发者协力完成,感谢他们的投入与贡献!

diff --git a/en/README.md b/en/README.md index 1d444db02..210a73076 100644 --- a/en/README.md +++ b/en/README.md @@ -6,7 +6,7 @@

hello-algo-typing-svg
- Data structures and algorithms crash course with animated illustrations and off-the-shelf code + Data structures and algorithms tutorial with animated illustrations and ready-to-run code

diff --git a/en/docs/chapter_appendix/contribution.md b/en/docs/chapter_appendix/contribution.md index 5a3846cd9..440894580 100644 --- a/en/docs/chapter_appendix/contribution.md +++ b/en/docs/chapter_appendix/contribution.md @@ -16,7 +16,7 @@ As shown in the figure below, there is an "edit icon" in the top-right corner of 1. Click the "edit icon". If you encounter a prompt asking you to "Fork this repository", please approve the operation. 2. Modify the content of the Markdown source file, verify the correctness of the content, and maintain consistent formatting as much as possible. -3. Fill in a description of your changes at the bottom of the page, then click the "Propose file change" button. After the page transitions, click the "Create pull request" button to submit your pull request. +3. Fill in a description of your changes at the bottom of the page, then click the "Propose file change" button. After the new page loads, click the "Create pull request" button to submit your pull request. ![Page edit button](contribution.assets/edit_markdown.png) @@ -27,14 +27,14 @@ Images cannot be directly modified. Please describe the issue by creating a new If you are interested in contributing to this open source project, including translating code into other programming languages or expanding article content, you will need to follow the Pull Request workflow below. 1. Log in to GitHub and Fork the book's [code repository](https://github.com/krahets/hello-algo) to your personal account. -2. Enter your forked repository webpage and use the `git clone` command to clone the repository to your local machine. +2. Go to your forked repository page and use the `git clone` command to clone the repository to your local machine. 3. Create content locally and conduct comprehensive tests to verify code correctness. 4. Commit your local changes and push them to the remote repository. 5. Refresh the repository webpage and click the "Create pull request" button to submit your pull request. ### Docker Deployment -From the root directory of `hello-algo`, run the following Docker script to access the project at `http://localhost:8000`: +From the root directory of `hello-algo`, run the following Docker command to access the project at `http://localhost:8000`: ```shell docker-compose up -d diff --git a/en/docs/chapter_appendix/installation.md b/en/docs/chapter_appendix/installation.md index 4b97078ca..eb62fac06 100644 --- a/en/docs/chapter_appendix/installation.md +++ b/en/docs/chapter_appendix/installation.md @@ -1,6 +1,6 @@ # Programming Environment Installation -## Installing Ide +## Installing IDE We recommend using the open-source and lightweight VS Code as the local integrated development environment (IDE). Visit the [VS Code official website](https://code.visualstudio.com/), and download and install the appropriate version of VS Code according to your operating system. @@ -14,11 +14,11 @@ VS Code has a powerful ecosystem of extensions that supports running and debuggi ### Python Environment -1. Download and install [Miniconda3](https://docs.conda.io/en/latest/miniconda.html), which requires Python 3.10 or newer. +1. Download and install [Miniconda3](https://docs.conda.io/en/latest/miniconda.html) with Python 3.10 or later. 2. Search for `python` in the VS Code extension marketplace and install the Python Extension Pack. 3. (Optional) Enter `pip install black` on the command line to install the code formatter. -### C/c++ Environment +### C/C++ Environment 1. Windows systems need to install [MinGW](https://sourceforge.net/projects/mingw-w64/files/) ([configuration tutorial](https://blog.csdn.net/qq_33698226/article/details/129031241)); macOS comes with Clang built-in and does not require installation. 2. Search for `c++` in the VS Code extension marketplace and install the C/C++ Extension Pack. @@ -26,12 +26,12 @@ VS Code has a powerful ecosystem of extensions that supports running and debuggi ### Java Environment -1. Download and install [OpenJDK](https://jdk.java.net/18/) (version must be > JDK 9). +1. Download and install [OpenJDK](https://jdk.java.net/18/) (version 10 or later). 2. Search for `java` in the VS Code extension marketplace and install the Extension Pack for Java. ### C# Environment -1. Download and install [.Net 8.0](https://dotnet.microsoft.com/en-us/download). +1. Download and install [.NET 8.0](https://dotnet.microsoft.com/en-us/download). 2. Search for `C# Dev Kit` in the VS Code extension marketplace and install C# Dev Kit ([configuration tutorial](https://code.visualstudio.com/docs/csharp/get-started)). 3. You can also use Visual Studio ([installation tutorial](https://learn.microsoft.com/zh-cn/visualstudio/install/install-visual-studio?view=vs-2022)). @@ -46,12 +46,12 @@ VS Code has a powerful ecosystem of extensions that supports running and debuggi 1. Download and install [Swift](https://www.swift.org/download/). 2. Search for `swift` in the VS Code extension marketplace and install [Swift for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=sswg.swift-lang). -### Javascript Environment +### JavaScript Environment 1. Download and install [Node.js](https://nodejs.org/en/). 2. (Optional) Search for `Prettier` in the VS Code extension marketplace and install the code formatter. -### Typescript Environment +### TypeScript Environment 1. Follow the same installation steps as the JavaScript environment. 2. Install [TypeScript Execute (tsx)](https://github.com/privatenumber/tsx?tab=readme-ov-file#global-installation). diff --git a/en/docs/chapter_appendix/terminology.md b/en/docs/chapter_appendix/terminology.md index eb6febfaf..b68472e73 100644 --- a/en/docs/chapter_appendix/terminology.md +++ b/en/docs/chapter_appendix/terminology.md @@ -1,137 +1,134 @@ -# Terminology Table +# Glossary -The following table lists important terms that appear in this book. It is worth noting the following points: - -- We recommend remembering the English names of terms to help with reading English literature. -- Some terms have different names in Simplified Chinese and Traditional Chinese. +The following table lists important terms that appear in this book.

Table   Important Terms in Data Structures and Algorithms

-| English | Simplified Chinese | Traditional Chinese | -| ------------------------------ | ------------------ | ------------------- | -| 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$ 记号 | 大 $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 树 | 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$ 问题 | 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$ 皇后问题 | $n$ 皇后問題 | -| dynamic programming | 动态规划 | 動態規劃 | -| initial state | 初始状态 | 初始狀態 | -| state-transition equation | 状态转移方程 | 狀態轉移方程 | -| knapsack problem | 背包问题 | 背包問題 | -| edit distance problem | 编辑距离问题 | 編輯距離問題 | -| greedy algorithm | 贪心算法 | 貪婪演算法 | +| 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 | +| 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 | +| 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 | +| 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 | +| dynamic programming | +| initial state | +| state-transition equation | +| knapsack problem | +| edit distance problem | +| greedy algorithm | diff --git a/en/docs/chapter_array_and_linkedlist/array.md b/en/docs/chapter_array_and_linkedlist/array.md index 082ec9f55..eed5bd3f8 100755 --- a/en/docs/chapter_array_and_linkedlist/array.md +++ b/en/docs/chapter_array_and_linkedlist/array.md @@ -8,7 +8,7 @@ An array is a linear data structure that stores elements of the same type ### Initializing Arrays -We can choose between two array initialization methods based on our needs: without initial values or with given initial values. When no initial values are specified, most programming languages will initialize array elements to $0$: +We can choose between two array initialization methods based on our needs: with or without initial values. When no initial values are specified, most programming languages initialize array elements to $0$: === "Python" @@ -146,11 +146,11 @@ Accessing elements in an array is highly efficient; we can randomly access any e ### Inserting Elements -Array elements are stored "tightly adjacent" in memory, with no space between them to store any additional data. As shown in the figure below, if we want to insert an element in the middle of an array, we need to shift all elements after that position backward by one position, and then assign the value to that index. +Array elements are packed tightly together in memory, with no extra space between them for additional data. As shown in the figure below, if we want to insert an element in the middle of an array, we need to shift all subsequent elements one position to the right and then assign the value at that index. ![Example of inserting an element into an array](array.assets/array_insert_element.png) -It is worth noting that since the length of an array is fixed, inserting an element will inevitably cause the element at the end of the array to be "lost". We will leave the solution to this problem for discussion in the "List" chapter. +It is worth noting that since the length of an array is fixed, inserting an element will inevitably push the last element out of the array. We will leave the solution to this problem for discussion in the "List" chapter. ```src [file]{array}-[class]{}-[func]{insert} @@ -162,7 +162,7 @@ Similarly, as shown in the figure below, to delete the element at index $i$, we ![Example of removing an element from an array](array.assets/array_remove_element.png) -Note that after the deletion is complete, the original last element becomes "meaningless", so we do not need to specifically modify it. +Note that after the deletion is complete, the original last element is no longer meaningful, so we do not need to modify it explicitly. ```src [file]{array}-[class]{}-[func]{remove} @@ -172,7 +172,7 @@ Overall, array insertion and deletion operations have the following drawbacks: - **High time complexity**: The average time complexity for both insertion and deletion in arrays is $O(n)$, where $n$ is the length of the array. - **Loss of elements**: Since the length of an array is immutable, after inserting an element, elements that exceed the array's length will be lost. -- **Memory waste**: We can initialize a relatively long array and only use the front portion, so that when inserting data, the lost elements at the end are "meaningless", but this causes some memory space to be wasted. +- **Memory waste**: We can initialize a relatively long array and use only the front portion, so that any overwritten tail elements are merely unused placeholders, but this wastes some memory space. ### Traversing Arrays diff --git a/en/docs/chapter_array_and_linkedlist/index.md b/en/docs/chapter_array_and_linkedlist/index.md index 709f53a7c..aaa29b4c2 100644 --- a/en/docs/chapter_array_and_linkedlist/index.md +++ b/en/docs/chapter_array_and_linkedlist/index.md @@ -1,9 +1,9 @@ -# Array and Linked List +# Arrays and Linked Lists -![Array and Linked List](../assets/covers/chapter_array_and_linkedlist.jpg) +![Arrays and Linked Lists](../assets/covers/chapter_array_and_linkedlist.jpg) !!! abstract The world of data structures is like a solid brick wall. - Array bricks are neatly arranged, tightly packed one by one. Linked list bricks are scattered everywhere, with connecting vines freely weaving through the gaps between bricks. + The bricks of an array are neatly aligned, each pressed tightly against the next. The bricks of a linked list are scattered about, with connecting vines weaving freely through the gaps between them. diff --git a/en/docs/chapter_array_and_linkedlist/linked_list.md b/en/docs/chapter_array_and_linkedlist/linked_list.md index 156399b2a..6c8faa52d 100755 --- a/en/docs/chapter_array_and_linkedlist/linked_list.md +++ b/en/docs/chapter_array_and_linkedlist/linked_list.md @@ -1,10 +1,10 @@ # Linked List -Memory space is a shared resource for all programs. In a complex system runtime environment, available memory space may be scattered throughout the memory. We know that the memory space for storing an array must be contiguous, and when the array is very large, the memory may not be able to provide such a large contiguous space. This is where the flexibility advantage of linked lists becomes apparent. +Memory is a shared resource for all programs. In a complex runtime environment, free memory may be scattered throughout the address space. We know that arrays require contiguous memory, and when an array is very large, the system may not be able to provide such a large contiguous block. This is where the flexibility of linked lists becomes apparent. A linked list is a linear data structure in which each element is a node object, and the nodes are connected through "references". A reference records the memory address of the next node, through which the next node can be accessed from the current node. -The design of linked lists allows nodes to be stored scattered throughout the memory, and their memory addresses do not need to be contiguous. +This design allows linked-list nodes to be stored in different locations in memory, and their addresses do not need to be contiguous. ![Linked list definition and storage method](linked_list.assets/linkedlist_definition.png) @@ -417,7 +417,7 @@ Building a linked list involves two steps: first, initializing each node object; https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E9%93%BE%E8%A1%A8%201%20-%3E%203%20-%3E%202%20-%3E%205%20-%3E%204%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%90%84%E4%B8%AA%E8%8A%82%E7%82%B9%0A%20%20%20%20n0%20%3D%20ListNode%281%29%0A%20%20%20%20n1%20%3D%20ListNode%283%29%0A%20%20%20%20n2%20%3D%20ListNode%282%29%0A%20%20%20%20n3%20%3D%20ListNode%285%29%0A%20%20%20%20n4%20%3D%20ListNode%284%29%0A%20%20%20%20%23%20%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%0A%20%20%20%20n0.next%20%3D%20n1%0A%20%20%20%20n1.next%20%3D%20n2%0A%20%20%20%20n2.next%20%3D%20n3%0A%20%20%20%20n3.next%20%3D%20n4&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false -An array is a single variable; for example, an array `nums` contains elements `nums[0]`, `nums[1]`, etc. A linked list, however, is composed of multiple independent node objects. **We typically use the head node as the reference to the linked list**; for example, the linked list in the above code can be referred to as linked list `n0`. +An array is a single variable; for example, an array `nums` contains elements `nums[0]`, `nums[1]`, and so on. A linked list, by contrast, is composed of multiple independent node objects. **We usually use the head node as a stand-in for the entire linked list**; for example, the linked list in the above code can be referred to as linked list `n0`. ### Inserting a Node @@ -478,7 +478,7 @@ The table below summarizes the characteristics of arrays and linked lists and co As shown in the figure below, there are three common types of linked lists: -- **Singly linked list**: This is the ordinary linked list introduced earlier. The nodes of a singly linked list contain a value and a reference to the next node. We call the first node the head node and the last node the tail node, which points to null `None`. +- **Singly linked list**: This is the ordinary linked list introduced earlier. The nodes of a singly linked list contain a value and a reference to the next node. We call the first node the head node and the last node the tail node; the tail node points to `None`. - **Circular linked list**: If we make the tail node of a singly linked list point to the head node (connecting the tail to the head), we get a circular linked list. In a circular linked list, any node can be viewed as the head node. - **Doubly linked list**: Compared to a singly linked list, a doubly linked list records references in both directions. The node definition of a doubly linked list includes references to both the successor node (next node) and the predecessor node (previous node). Compared to a singly linked list, a doubly linked list is more flexible and can traverse the linked list in both directions, but it also requires more memory space. diff --git a/en/docs/chapter_array_and_linkedlist/list.md b/en/docs/chapter_array_and_linkedlist/list.md index ce8065b20..6ed3c545f 100755 --- a/en/docs/chapter_array_and_linkedlist/list.md +++ b/en/docs/chapter_array_and_linkedlist/list.md @@ -2,20 +2,20 @@ A list is an abstract data structure concept that represents an ordered collection of elements, supporting operations such as element access, modification, insertion, deletion, and traversal, without requiring users to consider capacity limitations. Lists can be implemented based on linked lists or arrays. -- A linked list can naturally be viewed as a list, supporting element insertion, deletion, search, and modification operations, and can flexibly expand dynamically. -- An array also supports element insertion, deletion, search, and modification, but since its length is immutable, it can only be viewed as a list with length limitations. +- A linked list can naturally be viewed as a list: it supports insertion, deletion, search, and update, and can grow flexibly as needed. +- An array also supports insertion, deletion, search, and update, but because its length is fixed, it can only be regarded as a list with a capacity limit. -When implementing lists using arrays, **the immutable length property reduces the practicality of the list**. This is because we usually cannot determine in advance how much data we need to store, making it difficult to choose an appropriate list length. If the length is too small, it may fail to meet usage requirements; if the length is too large, it will waste memory space. +When a list is implemented with an array, **its fixed length makes it less practical**. This is because we usually cannot determine in advance how much data we need to store, making it difficult to choose an appropriate capacity. If the capacity is too small, it may fail to meet our needs; if it is too large, memory space will be wasted. -To solve this problem, we can use a dynamic array to implement a list. It inherits all the advantages of arrays and can dynamically expand during program execution. +To solve this problem, we can use a dynamic array to implement a list. It inherits all the advantages of arrays while supporting dynamic resizing during program execution. -In fact, **the lists provided in the standard libraries of many programming languages are implemented based on dynamic arrays**, such as `list` in Python, `ArrayList` in Java, `vector` in C++, and `List` in C#. In the following discussion, we will treat "list" and "dynamic array" as equivalent concepts. +In fact, **the list types provided by the standard libraries of many programming languages are implemented with dynamic arrays**, such as `list` in Python, `ArrayList` in Java, `vector` in C++, and `List` in C#. In the following discussion, we will treat "list" and "dynamic array" as equivalent concepts. ## Common List Operations ### Initialize a List -We typically use two initialization methods: "without initial values" and "with initial values": +We typically initialize a list in one of two ways: empty or with predefined values: === "Python" diff --git a/en/docs/chapter_array_and_linkedlist/ram_and_cache.md b/en/docs/chapter_array_and_linkedlist/ram_and_cache.md index 8462197ed..512862a09 100644 --- a/en/docs/chapter_array_and_linkedlist/ram_and_cache.md +++ b/en/docs/chapter_array_and_linkedlist/ram_and_cache.md @@ -1,6 +1,6 @@ # Random-Access Memory and Cache * -In the first two sections of this chapter, we explored arrays and linked lists, two fundamental and important data structures that represent "contiguous storage" and "distributed storage" as two physical structures, respectively. +In the first two sections of this chapter, we explored arrays and linked lists, two fundamental and important data structures that represent two physical layouts: "contiguous storage" and "distributed storage", respectively. In fact, **physical structure largely determines the efficiency with which programs utilize memory and cache**, which in turn affects the overall performance of algorithmic programs. @@ -16,9 +16,9 @@ Computers include three types of storage devices: hard disk, random-ac | Volatility | Data is not lost after power-off | Data is lost after power-off | Data is lost after power-off | | Capacity | Large, on the order of terabytes (TB) | Small, on the order of gigabytes (GB) | Very small, on the order of megabytes (MB) | | Speed | Slow, hundreds to thousands of MB/s | Fast, tens of GB/s | Very fast, tens to hundreds of GB/s | -| Cost (USD/GB) | Inexpensive, fractions of a dollar to a few dollars per GB | Expensive, tens to hundreds of dollars per GB | Very expensive, priced as part of the CPU package | +| Cost (CNY/GB) | Inexpensive, from a few tenths of a yuan to a few yuan per GB | Expensive, from tens to hundreds of yuan per GB | Very expensive, effectively bundled with the CPU package | -We can imagine the computer storage system as a pyramid structure as shown in the diagram below. Storage devices closer to the top of the pyramid are faster, have smaller capacity, and are more expensive. This multi-layered design is not by accident, but rather the result of careful consideration by computer scientists and engineers. +We can imagine the computer storage system as a pyramid, as shown in the diagram below. Storage devices closer to the top are faster, have smaller capacity, and are more expensive. This multi-layered design is deliberate, the result of careful consideration by computer scientists and engineers. - **Hard disk cannot be easily replaced by RAM**. First, data in memory is lost after power-off, making it unsuitable for long-term data storage. Second, memory is tens of times more expensive than hard disk, which makes it difficult to popularize in the consumer market. - **Cache cannot simultaneously achieve large capacity and high speed**. As the capacity of L1, L2, and L3 caches increases, their physical size becomes larger, and the physical distance between them and the CPU core increases, resulting in longer data transmission time and higher element access latency. With current technology, the multi-layered cache structure represents the best balance point between capacity, speed, and cost. @@ -29,9 +29,9 @@ We can imagine the computer storage system as a pyramid structure as shown in th The storage hierarchy of computers embodies a delicate balance among speed, capacity, and cost. In fact, such trade-offs are common across all industrial fields, requiring us to find the optimal balance point between different advantages and constraints. -In summary, **hard disk is used for long-term storage of large amounts of data, RAM is used for temporary storage of data being processed during program execution, and cache is used for storage of frequently accessed data and instructions**, to improve program execution efficiency. The three work together to ensure efficient operation of the computer system. +In summary, **hard disks are used for long-term storage of large amounts of data, RAM is used to temporarily store the data being processed during program execution, and cache is used to store frequently accessed data and instructions**, thereby improving program execution efficiency. The three work together to keep the computer system running efficiently. -As shown in the diagram below, during program execution, data is read from the hard disk into RAM for CPU computation. Cache can be viewed as part of the CPU, **it intelligently loads data from RAM**, providing the CPU with high-speed data reading, thereby significantly improving program execution efficiency and reducing reliance on slower RAM. +As shown in the diagram below, during program execution, data is read from the hard disk into RAM for CPU computation. Cache can be viewed as part of the CPU. **By intelligently loading data from RAM**, it provides the CPU with high-speed access to data, significantly improving program execution efficiency and reducing reliance on slower RAM. ![Data Flow Among Hard Disk, RAM, and Cache](ram_and_cache.assets/computer_storage_devices.png) @@ -56,9 +56,9 @@ To achieve the highest efficiency possible, cache employs the following data loa - **Spatial locality**: If a piece of data is accessed, nearby data may also be accessed in the near future. Therefore, when the cache loads a particular piece of data, it also loads nearby data to improve hit rate. - **Temporal locality**: If a piece of data is accessed, it is likely to be accessed again in the near future. Cache leverages this principle by retaining recently accessed data to improve hit rate. -In fact, **arrays and linked lists have different efficiencies in utilizing cache**, manifested in the following aspects. +In fact, **arrays and linked lists differ in how efficiently they utilize cache**, mainly in the following respects. -- **Space occupied**: Linked list elements occupy more space than array elements, resulting in fewer effective data in the cache. +- **Space occupied**: Linked-list elements occupy more space than array elements, so less useful data can fit in the cache. - **Cache lines**: Linked list data are scattered throughout memory, while cache loads "by lines," so the proportion of invalid data loaded is higher. - **Prefetching mechanism**: Arrays have more "predictable" data access patterns than linked lists, making it easier for the system to guess which data will be loaded next. - **Spatial locality**: Arrays are stored in centralized memory space, so data near loaded data is more likely to be accessed soon. diff --git a/en/docs/chapter_array_and_linkedlist/summary.md b/en/docs/chapter_array_and_linkedlist/summary.md index 53ab5a25d..59c678e7c 100644 --- a/en/docs/chapter_array_and_linkedlist/summary.md +++ b/en/docs/chapter_array_and_linkedlist/summary.md @@ -2,11 +2,11 @@ ### Key Review -- Arrays and linked lists are two fundamental data structures, representing two different ways data can be stored in computer memory: contiguous memory storage and scattered memory storage. The characteristics of the two complement each other. +- Arrays and linked lists are two fundamental data structures, representing two different ways data can be stored in computer memory: contiguous storage and scattered storage. Their strengths and weaknesses complement each other. - Arrays support random access and use less memory; however, inserting and deleting elements is inefficient, and the length is immutable after initialization. - Linked lists achieve efficient insertion and deletion of nodes by modifying references (pointers), and can flexibly adjust length; however, node access is inefficient and memory consumption is higher. Common linked list types include singly linked lists, circular linked lists, and doubly linked lists. - A list is an ordered collection of elements that supports insertion, deletion, search, and modification, typically implemented based on dynamic arrays. It retains the advantages of arrays while allowing flexible adjustment of length. -- The emergence of lists has greatly improved the practicality of arrays, but may result in some wasted memory space. +- The emergence of lists has greatly improved the practicality of arrays, but it may also waste some memory space. - During program execution, data is primarily stored in memory. Arrays provide higher memory space efficiency, while linked lists offer greater flexibility in memory usage. - Caches provide fast data access to the CPU through mechanisms such as cache lines, prefetching, and spatial and temporal locality, significantly improving program execution efficiency. - Because arrays have higher cache hit rates, they are generally more efficient than linked lists. When choosing a data structure, appropriate selection should be made based on specific requirements and scenarios. @@ -25,17 +25,17 @@ Arrays stored on the stack and on the heap are both stored in contiguous memory Linked lists are composed of nodes, with nodes connected through references (pointers), and each node can store different types of data, such as `int`, `double`, `string`, `object`, etc. -In contrast, array elements must be of the same type, so that the corresponding element position can be obtained by calculating the offset. For example, if an array contains both `int` and `long` types, with individual elements occupying 4 bytes and 8 bytes respectively, then the following formula cannot be used to calculate the offset, because the array contains two different "element lengths". +In contrast, array elements must be of the same type so that their positions can be determined by calculating offsets. For example, if an array contains both `int` and `long` types, with individual elements occupying 4 bytes and 8 bytes respectively, then the following formula cannot be used to calculate the offset, because the array contains two different "element sizes". ```shell -# Element Memory Address = Array Memory Address (first Element Memory address) + Element Length * Element Index +# element address = array base address (address of the first element) + element size * element index ``` **Q**: After deleting node `P`, do we need to set `P.next` to `None`? It is not necessary to modify `P.next`. From the perspective of the linked list, traversing from the head node to the tail node will no longer encounter `P`. This means that node `P` has been removed from the linked list, and it doesn't matter where node `P` points to at this time—it won't affect the linked list. -From a data structures and algorithms perspective (problem-solving), not disconnecting the pointer doesn't matter as long as the program logic is correct. From the perspective of standard libraries, disconnecting is safer and the logic is clearer. If not disconnected, assuming the deleted node is not properly reclaimed, it may affect the memory reclamation of its successor nodes. +From an algorithms-and-problem-solving perspective, leaving the pointer connected is fine as long as the program logic is correct. From a standard-library implementation perspective, explicitly disconnecting it is safer and clearer. If it is not disconnected and the deleted node is not reclaimed properly, it may affect the reclamation of successor nodes. **Q**: In a linked list, the time complexity of insertion and deletion operations is $O(1)$. However, both insertion and deletion require $O(n)$ time to find the element; why isn't the time complexity $O(n)$? @@ -79,8 +79,8 @@ If we want each `[0]` in the 2D list to be independent, we can use `res = [[0] f **Q**: Does the operation `res = [0] * n` create a list where each integer 0 is independent? -In this list, all integer 0s are references to the same object. This is because Python uses a caching mechanism for small integers (typically -5 to 256) to maximize object reuse and improve performance. +In this list, all the integer zeros reference the same object. This is because Python uses a caching mechanism for small integers (typically -5 to 256) to maximize object reuse and improve performance. -Although they point to the same object, we can still independently modify each element in the list. This is because Python integers are "immutable objects". When we modify an element, we are actually switching to a reference of another object, rather than changing the original object itself. +Although they all reference the same object, we can still modify each element in the list independently. This is because Python integers are "immutable objects". When we modify an element, we actually switch that element to reference a different object, rather than changing the original object itself. However, when list elements are "mutable objects" (such as lists, dictionaries, or class instances), modifying an element directly changes the object itself, and all elements referencing that object will have the same change. diff --git a/en/docs/chapter_backtracking/backtracking_algorithm.md b/en/docs/chapter_backtracking/backtracking_algorithm.md index 28bb41316..baea6c055 100644 --- a/en/docs/chapter_backtracking/backtracking_algorithm.md +++ b/en/docs/chapter_backtracking/backtracking_algorithm.md @@ -20,7 +20,7 @@ For this problem, we perform a preorder traversal of the tree and check whether **The reason it is called a backtracking algorithm is that it employs "attempt" and "backtrack" strategies when searching the solution space**. When the algorithm encounters a state where it cannot continue forward or cannot find a solution that satisfies the constraints, it will undo the previous choice, return to a previous state, and try other possible choices. -For Example 1, visiting each node represents an "attempt", while skipping over a leaf node or a function `return` from the parent node represents a "backtrack". +For Example 1, visiting each node represents an "attempt", while skipping over a leaf node or the `return` that brings the traversal back to the parent node represents a "backtrack". It is worth noting that **backtracking is not limited to function returns alone**. To illustrate this, let's extend Example 1 slightly. @@ -28,7 +28,7 @@ It is worth noting that **backtracking is not limited to function returns alone* In a binary tree, search all nodes with value $7$, **and return the paths from the root node to these nodes**. -Based on the code from Example 1, we need to use a list `path` to record the visited node path. When we reach a node with value $7$, we copy `path` and add it to the result list `res`. After traversal is complete, `res` contains all the solutions. The code is as follows: +Based on the code from Example 1, we need to use a list `path` to record the path of visited nodes. When we reach a node with value $7$, we copy `path` and add it to the result list `res`. After traversal is complete, `res` contains all the solutions. The code is as follows: ```src [file]{preorder_traversal_ii_compact}-[class]{}-[func]{pre_order} @@ -91,7 +91,7 @@ To satisfy the above constraints, **we need to add pruning operations**: during ## Framework Code -Next, we attempt to extract the main framework of backtracking's "attempt, backtrack, and pruning", to improve code generality. +Next, we attempt to extract a general framework centered on backtracking's "attempt, backtrack, and pruning" to improve code generality. In the following framework code, `state` represents the current state of the problem, and `choices` represents the choices available in the current state: @@ -439,7 +439,7 @@ As per the problem statement, we should continue searching after finding a node ![Comparison of search process with and without return statement](backtracking_algorithm.assets/backtrack_remove_return_or_not.png) -Compared to code based on preorder traversal, code based on the backtracking algorithm framework appears more verbose, but has better generality. In fact, **many backtracking problems can be solved within this framework**. We only need to define `state` and `choices` for the specific problem and implement each method in the framework. +Compared to code based on preorder traversal, code based on the backtracking algorithm framework appears more verbose, but is more general. In fact, **many backtracking problems can be solved within this framework**. We only need to define `state` and `choices` for the specific problem and implement each method in the framework. ## Common Terminology @@ -458,7 +458,7 @@ To analyze algorithmic problems more clearly, we summarize the meanings of commo !!! tip - The concepts of problem, solution, state, etc. are universal and are involved in divide-and-conquer, backtracking, dynamic programming, greedy and other algorithms. + The concepts of problem, solution, state, etc. are universal and appear in divide-and-conquer, backtracking, dynamic programming, greedy algorithms, and others. ## Advantages and Limitations @@ -466,7 +466,7 @@ The backtracking algorithm is essentially a depth-first search algorithm that tr However, when dealing with large-scale or complex problems, **the running efficiency of the backtracking algorithm may be unacceptable**. -- **Time**: The backtracking algorithm usually needs to traverse all possibilities in the solution space, and the time complexity can reach exponential or factorial order. +- **Time**: The backtracking algorithm usually needs to traverse all possibilities in the state space, and the time complexity can reach exponential or factorial order. - **Space**: During recursive calls, the current state needs to be saved (such as paths, auxiliary variables used for pruning, etc.), and when the depth is large, the space requirement can become very large. Nevertheless, **the backtracking algorithm is still the best solution for certain search problems and constraint satisfaction problems**. For these problems, since we cannot predict which choices will generate valid solutions, we must traverse all possible choices. In this case, **the key is how to optimize efficiency**. There are two common efficiency optimization methods. diff --git a/en/docs/chapter_backtracking/n_queens_problem.md b/en/docs/chapter_backtracking/n_queens_problem.md index 2c4122293..d3794a414 100644 --- a/en/docs/chapter_backtracking/n_queens_problem.md +++ b/en/docs/chapter_backtracking/n_queens_problem.md @@ -2,7 +2,7 @@ !!! question - According to the rules of chess, a queen can attack pieces that share the same row, column, or diagonal line. Given $n$ queens and an $n \times n$ chessboard, find a placement scheme such that no two queens can attack each other. + According to the rules of chess, a queen can attack any piece in the same row, column, or diagonal. Given $n$ queens and an $n \times n$ chessboard, find an arrangement such that no two queens can attack each other. As shown in the figure below, when $n = 4$, there are two solutions that can be found. From the perspective of the backtracking algorithm, an $n \times n$ chessboard has $n^2$ squares, which provide all the choices `choices`. During the process of placing queens one by one, the chessboard state changes continuously, and the chessboard at each moment represents the state `state`. @@ -14,11 +14,11 @@ The figure below illustrates the three constraints of this problem: **multiple q ### Row-By-Row Placement Strategy -Since both the number of queens and the number of rows on the chessboard are $n$, we can easily derive a conclusion: **each row of the chessboard allows and only allows exactly one queen to be placed**. +Since both the number of queens and the number of rows on the chessboard are $n$, we can easily derive a conclusion: **each row of the chessboard allows one and only one queen to be placed**. This means we can adopt a row-by-row placement strategy: starting from the first row, place one queen in each row until the last row is completed. -The figure below shows the row-by-row placement process for the 4-queens problem. Due to space limitations, the figure only expands one search branch of the first row, and all schemes that do not satisfy the column constraint and diagonal constraints are pruned. +The figure below shows the row-by-row placement process for the 4-queens problem. Due to space limitations, the figure only expands one search branch of the first row, and all schemes that violate the column or diagonal constraints are pruned. ![Row-by-row placement strategy](n_queens_problem.assets/n_queens_placing.png) @@ -42,7 +42,7 @@ Similarly, **for all squares on an anti-diagonal, the sum $row + col$ is a const ### Code Implementation -Please note that in an $n$-dimensional square matrix, the range of $row - col$ is $[-n + 1, n - 1]$, and the range of $row + col$ is $[0, 2n - 2]$. Therefore, the number of both main diagonals and anti-diagonals is $2n - 1$, meaning the length of both arrays `diags1` and `diags2` is $2n - 1$. +Please note that in an $n \times n$ square matrix, the range of $row - col$ is $[-n + 1, n - 1]$, and the range of $row + col$ is $[0, 2n - 2]$. Therefore, the number of both main diagonals and anti-diagonals is $2n - 1$, meaning the length of both arrays `diags1` and `diags2` is $2n - 1$. ```src [file]{n_queens}-[class]{}-[func]{n_queens} diff --git a/en/docs/chapter_backtracking/permutations_problem.md b/en/docs/chapter_backtracking/permutations_problem.md index a390950d0..e2ea59253 100644 --- a/en/docs/chapter_backtracking/permutations_problem.md +++ b/en/docs/chapter_backtracking/permutations_problem.md @@ -30,7 +30,7 @@ As shown in the figure below, we can unfold the search process into a recursion To ensure that each element is chosen only once, we consider introducing a boolean array `selected`, where `selected[i]` indicates whether `choices[i]` has been chosen. We implement the following pruning operation based on it. -- After making a choice `choice[i]`, we set `selected[i]` to $\text{True}$, indicating that it has been chosen. +- After making a choice `choices[i]`, we set `selected[i]` to $\text{True}$, indicating that it has been chosen. - When traversing the candidate list `choices`, we skip all nodes that have been chosen, which is pruning. As shown in the figure below, suppose we choose $1$ in the first round, $3$ in the second round, and $2$ in the third round. Then we need to prune the branch of element $1$ in the second round and prune the branches of elements $1$ and $3$ in the third round. @@ -55,13 +55,13 @@ After understanding the above information, we can fill in the blanks in the temp Suppose the input array is $[1, 1, 2]$. To distinguish the two duplicate elements $1$, we denote the second $1$ as $\hat{1}$. -As shown in the figure below, the method described above generates permutations where half are duplicates. +As shown in the figure below, half of the permutations generated by the above method are duplicates. ![Duplicate permutations](permutations_problem.assets/permutations_ii.png) So how do we remove duplicate permutations? The most direct approach is to use a hash set to directly deduplicate the permutation results. However, this is not elegant because **the search branches that generate duplicate permutations are unnecessary and should be identified and pruned early**, which can further improve algorithm efficiency. -### Pruning Duplicate Elements +### Pruning Equal Elements Observe the figure below. In the first round, choosing $1$ or choosing $\hat{1}$ is equivalent. All permutations generated under these two choices are duplicates. Therefore, we should prune $\hat{1}$. @@ -73,7 +73,7 @@ Essentially, **our goal is to ensure that multiple equal elements are chosen onl ### Code Implementation -Building on the code from the previous problem, we consider opening a hash set `duplicated` in each round of choices to record which elements have been tried in this round, and prune duplicate elements: +Building on the code from the previous problem, we initialize a hash set `duplicated` in each round of choices to record which elements have already been tried in that round, and prune equal elements: ```src [file]{permutations_ii}-[class]{}-[func]{permutations_ii} @@ -88,7 +88,7 @@ The maximum recursion depth is $n$, using $O(n)$ stack frame space. `selected` u Note that although both `selected` and `duplicated` are used for pruning, they have different objectives. - **Pruning duplicate choices**: There is only one `selected` throughout the entire search process. It records which elements are included in the current state, and its purpose is to prevent an element from appearing repeatedly in `state`. -- **Pruning duplicate elements**: Each round of choices (each `backtrack` function call) contains a `duplicated` set. It records which elements have been chosen in this round's iteration (the `for` loop), and its purpose is to ensure that equal elements are chosen only once. +- **Pruning equal elements**: Each round of choices (each `backtrack` function call) contains a `duplicated` set. It records which elements have been chosen in this round's iteration (the `for` loop), and its purpose is to ensure that equal elements are chosen only once. The figure below shows the effective scope of the two pruning conditions. Note that each node in the tree represents a choice, and the nodes on the path from the root to a leaf node form a permutation. diff --git a/en/docs/chapter_backtracking/subset_sum_problem.md b/en/docs/chapter_backtracking/subset_sum_problem.md index 821d12fa9..1d3cc2ebf 100644 --- a/en/docs/chapter_backtracking/subset_sum_problem.md +++ b/en/docs/chapter_backtracking/subset_sum_problem.md @@ -11,17 +11,17 @@ For example, given the set $\{3, 4, 5\}$ and target integer $9$, the solutions a - Elements in the input set can be selected repeatedly without limit. - Subsets do not distinguish element order; for example, $\{4, 5\}$ and $\{5, 4\}$ are the same subset. -### Reference to Full Permutation Solution +### Using the Permutation Solution as a Reference -Similar to the full permutation problem, we can imagine the process of generating subsets as a series of choices, and update the "sum of elements" in real-time during the selection process. When the sum equals `target`, we record the subset to the result list. +Similar to the permutation problem, we can view the process of generating subsets as the result of a series of choices and update the running sum during the selection process. When the sum equals `target`, we record the subset in the result list. -Unlike the full permutation problem, **elements in this problem's set can be selected unlimited times**, so we do not need to use a `selected` boolean list to track whether an element has been selected. We can make minor modifications to the full permutation code and initially obtain the solution: +Unlike the permutation problem, **elements in this problem can be selected any number of times**, so we do not need to use a `selected` boolean list to track whether an element has already been selected. With a few small changes to the permutation code, we obtain an initial solution: ```src [file]{subset_sum_i_naive}-[class]{}-[func]{subset_sum_i_naive} ``` -When we input array $[3, 4, 5]$ and target element $9$ to the above code, the output is $[3, 3, 3], [4, 5], [5, 4]$. **Although we successfully find all subsets that sum to $9$, there are duplicate subsets $[4, 5]$ and $[5, 4]$**. +Running the above code on array $[3, 4, 5]$ with target value $9$ produces $[3, 3, 3], [4, 5], [5, 4]$. **Although we successfully found all subsets that sum to $9$, there are duplicate subsets $[4, 5]$ and $[5, 4]$**. This is because the search process distinguishes the order of selections, but subsets do not distinguish selection order. As shown in the figure below, selecting 4 first and then 5 versus selecting 5 first and then 4 are different branches, but they correspond to the same subset. @@ -37,13 +37,13 @@ To eliminate duplicate subsets, **one straightforward idea is to deduplicate the **We consider deduplication through pruning during the search process**. Observing the figure below, duplicate subsets occur when array elements are selected in different orders, as in the following cases: 1. When the first and second rounds select $3$ and $4$ respectively, all subsets containing these two elements are generated, denoted as $[3, 4, \dots]$. -2. Afterward, when the first round selects $4$, **the second round should skip $3$**, because the subset $[4, 3, \dots]$ generated by this choice is completely duplicate with the subset generated in step `1.` +2. Afterward, when the first round selects $4$, **the second round should skip $3$**, because the subset $[4, 3, \dots]$ generated by this choice is an exact duplicate of the subset generated in step `1.` In the search process, each level's choices are tried from left to right, so the rightmost branches are pruned more. 1. The first two rounds select $3$ and $5$, generating subset $[3, 5, \dots]$. 2. The first two rounds select $4$ and $5$, generating subset $[4, 5, \dots]$. -3. If the first round selects $5$, **the second round should skip $3$ and $4$**, because subsets $[5, 3, \dots]$ and $[5, 4, \dots]$ are completely duplicate with the subsets described in steps `1.` and `2.` +3. If the first round selects $5$, **the second round should skip $3$ and $4$**, because subsets $[5, 3, \dots]$ and $[5, 4, \dots]$ are exact duplicates of the subsets described in steps `1.` and `2.` ![Different selection orders leading to duplicate subsets](subset_sum_problem.assets/subset_sum_i_pruning.png) @@ -62,7 +62,7 @@ In addition, we have made the following two optimizations to the code: [file]{subset_sum_i}-[class]{}-[func]{subset_sum_i} ``` -The figure below shows the complete backtracking process when array $[3, 4, 5]$ and target element $9$ are input to the above code. +The figure below shows the complete backtracking process produced by running the above code on array $[3, 4, 5]$ with target value $9$. ![Subset-sum I backtracking process](subset_sum_problem.assets/subset_sum_i.png) @@ -72,7 +72,7 @@ The figure below shows the complete backtracking process when array $[3, 4, 5]$ Given a positive integer array `nums` and a target positive integer `target`, find all possible combinations where the sum of elements in the combination equals `target`. **The given array may contain duplicate elements, and each element can be selected at most once**. Return these combinations in list form, where the list should not contain duplicate combinations. -Compared to the previous problem, **the input array in this problem may contain duplicate elements**, which introduces new challenges. For example, given array $[4, \hat{4}, 5]$ and target element $9$, the output of the existing code is $[4, 5], [\hat{4}, 5]$, which contains duplicate subsets. +Compared to the previous problem, **the input array in this problem may contain duplicate elements**, which introduces a new issue. For example, given array $[4, \hat{4}, 5]$ and target value $9$, the output of the existing code is $[4, 5], [\hat{4}, 5]$, which contains duplicate subsets. **The reason for this duplication is that equal elements are selected multiple times in a certain round**. In the figure below, the first round has three choices, two of which are $4$, creating two duplicate search branches that output duplicate subsets. Similarly, the two $4$'s in the second round also produce duplicate subsets. @@ -80,7 +80,7 @@ Compared to the previous problem, **the input array in this problem may contain ### Pruning Equal Elements -To solve this problem, **we need to limit equal elements to be selected only once in each round**. The implementation is quite clever: since the array is already sorted, equal elements are adjacent. This means that in a certain round of selection, if the current element equals the element to its left, it means this element has already been selected, so we skip the current element directly. +To solve this problem, **we need to limit equal elements to be selected only once in each round**. The implementation is quite clever: since the array is already sorted, equal elements are adjacent. This means that in a given round of selection, if the current element equals the element to its left, then the same value has already been chosen in this round, so we skip the current element directly. At the same time, **this problem specifies that each array element can only be selected once**. Fortunately, we can also use the variable `start` to satisfy this constraint: after making choice $x_{i}$, set the next round to start traversal from index $i + 1$ onwards. This both eliminates duplicate subsets and avoids selecting elements multiple times. @@ -90,6 +90,6 @@ At the same time, **this problem specifies that each array element can only be s [file]{subset_sum_ii}-[class]{}-[func]{subset_sum_ii} ``` -The figure below shows the backtracking process for array $[4, 4, 5]$ and target element $9$, which includes four types of pruning operations. Combine the illustration with the code comments to understand the entire search process and how each pruning operation works. +The figure below shows the backtracking process for array $[4, 4, 5]$ with target value $9$, which includes four types of pruning operations. Combine the illustration with the code comments to understand the entire search process and how each pruning operation works. ![Subset-sum II backtracking process](subset_sum_problem.assets/subset_sum_ii.png) diff --git a/en/docs/chapter_backtracking/summary.md b/en/docs/chapter_backtracking/summary.md index e13bd8c1f..3b4a52701 100644 --- a/en/docs/chapter_backtracking/summary.md +++ b/en/docs/chapter_backtracking/summary.md @@ -9,15 +9,15 @@ - The permutation problem aims to find all possible permutations of elements in a given set. We use an array to record whether each element has been selected, thereby pruning search branches that attempt to select the same element repeatedly, ensuring each element is selected exactly once. - In the permutation problem, if the set contains duplicate elements, the final result will contain duplicate permutations. We need to impose a constraint so that equal elements can only be selected once per round, which is typically achieved using a hash set. - The subset-sum problem aims to find all subsets of a given set that sum to a target value. Since the set is unordered but the search process outputs results in all orders, duplicate subsets are generated. We sort the data before backtracking and use a variable to indicate the starting point of each round's traversal, thereby pruning search branches that generate duplicate subsets. -- For the subset-sum problem, equal elements in the array produce duplicate sets. We leverage the precondition that the array is sorted by checking whether adjacent elements are equal to implement pruning, ensuring that equal elements can only be selected once per round. +- For the subset-sum problem, equal elements in the array produce duplicate subsets. We leverage the precondition that the array is sorted by checking whether adjacent elements are equal to implement pruning, ensuring that equal elements can only be selected once per round. - The $n$ queens problem aims to find placements of $n$ queens on an $n \times n$ chessboard such that no two queens can attack each other. The constraints of this problem include row constraints, column constraints, and main and anti-diagonal constraints. To satisfy row constraints, we adopt a row-by-row placement strategy, ensuring exactly one queen is placed in each row. - The handling of column constraints and diagonal constraints is similar. For column constraints, we use an array to record whether each column has a queen, thereby indicating whether a selected cell is valid. For diagonal constraints, we use two arrays to separately record whether queens exist on each main or anti-diagonal. The challenge lies in finding the row-column index pattern that characterizes cells on the same main (anti-)diagonal. ### Q & A -**Q**: How should we understand the relationship between backtracking and recursion? +**Q**: How can we understand the relationship between backtracking and recursion? -Overall, backtracking is an "algorithm strategy", while recursion is more like a "tool". +Overall, backtracking is an algorithmic strategy, while recursion is better viewed as a tool. -- The backtracking algorithm is typically implemented based on recursion. However, backtracking is one application scenario of recursion and represents the application of recursion in search problems. -- The structure of recursion embodies the "subproblem decomposition" problem-solving paradigm, commonly used to solve problems involving divide-and-conquer, backtracking, and dynamic programming (memoized recursion). +- Backtracking is typically implemented with recursion. However, backtracking is only one application of recursion, specifically its use in search problems. +- The structure of recursion reflects a problem-solving paradigm based on decomposing a problem into subproblems, and it is commonly used in divide-and-conquer, backtracking, and dynamic programming (memoized recursion). diff --git a/en/docs/chapter_computational_complexity/iteration_and_recursion.md b/en/docs/chapter_computational_complexity/iteration_and_recursion.md index 0c76de86b..305c32014 100644 --- a/en/docs/chapter_computational_complexity/iteration_and_recursion.md +++ b/en/docs/chapter_computational_complexity/iteration_and_recursion.md @@ -10,7 +10,7 @@ In algorithms, repeatedly executing a task is very common and closely related to The `for` loop is one of the most common forms of iteration, **suitable for use when the number of iterations is known in advance**. -The following function implements the summation $1 + 2 + \dots + n$ based on a `for` loop, with the sum result recorded using the variable `res`. Note that in Python, `range(a, b)` corresponds to a "left-closed, right-open" interval, with the traversal range being $a, a + 1, \dots, b-1$: +The following function implements the summation $1 + 2 + \dots + n$ using a `for` loop, with the result stored in the variable `res`. Note that in Python, `range(a, b)` corresponds to a "left-closed, right-open" interval, with the traversal range being $a, a + 1, \dots, b-1$: ```src [file]{iteration}-[class]{}-[func]{for_loop} @@ -56,7 +56,7 @@ The figure below shows the flowchart of this nested loop. In this case, the number of operations of the function is proportional to $n^2$, or the algorithm's running time has a "quadratic relationship" with the input data size $n$. -We can continue adding nested loops, where each nesting is a "dimension increase", raising the time complexity to "cubic relationship", "quartic relationship", and so on. +We can continue adding nested loops, where each additional level of nesting can be viewed as an increase in dimensionality, raising the time complexity to a "cubic relationship", a "quartic relationship", and so on. ## Recursion @@ -93,7 +93,7 @@ Taking the above summation function as an example, let the problem be $f(n) = 1 ### Call Stack -Each time a recursive function calls itself, the system allocates memory for the newly opened function to store local variables, call addresses, and other information. This leads to two consequences. +Each time a recursive function calls itself, the system allocates memory for the newly invoked function to store local variables, call addresses, and other information. This leads to two consequences. - The function's context data is stored in a memory area called "stack frame space", which is not released until the function returns. Therefore, **recursion usually consumes more memory space than iteration**. - Recursive function calls incur additional overhead. **Therefore, recursion is usually less time-efficient than loops**. @@ -106,7 +106,7 @@ In practice, the recursion depth allowed by programming languages is usually lim ### Tail Recursion -Interestingly, **if a function makes the recursive call as the very last step before returning**, the function can be optimized by the compiler or interpreter to have space efficiency comparable to iteration. This case is called tail recursion. +Interestingly, **if a function makes the recursive call as the very last step before returning**, the compiler or interpreter may optimize it so that its space efficiency is comparable to iteration. This case is called tail recursion. - **Regular recursion**: When a function returns to the previous level, it needs to continue executing code, so the system needs to save the context of the previous layer's call. - **Tail recursion**: The recursive call is the last operation before the function returns, meaning that after returning to the previous level, there is no need to continue executing other operations, so the system does not need to save the context of the previous layer's function. @@ -117,7 +117,7 @@ Taking the calculation of $1 + 2 + \dots + n$ as an example, we can set the resu [file]{recursion}-[class]{}-[func]{tail_recur} ``` -The execution process of tail recursion is shown in the figure below. Comparing regular recursion and tail recursion, the execution point of the summation operation is different. +The execution process of tail recursion is shown in the figure below. Comparing regular recursion and tail recursion, the summation operation is performed at different points. - **Regular recursion**: The summation operation is performed during the "ascending" process, requiring an additional summation operation after each layer returns. - **Tail recursion**: The summation operation is performed during the "descending" process; the "ascending" process only needs to return layer by layer. @@ -147,7 +147,7 @@ Following the recurrence relation to make recursive calls, with the first two nu [file]{recursion}-[class]{}-[func]{fib} ``` -Observing the above code, we recursively call two functions within the function, **meaning that one call produces two call branches**. As shown in the figure below, such continuous recursive calling will eventually produce a recursion tree with $n$ levels. +Observing the above code, we make two recursive calls within the function, **meaning that one call produces two call branches**. As shown in the figure below, this repeated recursive calling eventually produces a recursion tree with $n$ levels. ![Recursion tree of the Fibonacci sequence](iteration_and_recursion.assets/recursion_tree.png) diff --git a/en/docs/chapter_computational_complexity/performance_evaluation.md b/en/docs/chapter_computational_complexity/performance_evaluation.md index 83893114a..79583afda 100644 --- a/en/docs/chapter_computational_complexity/performance_evaluation.md +++ b/en/docs/chapter_computational_complexity/performance_evaluation.md @@ -16,17 +16,17 @@ Efficiency evaluation methods are mainly divided into two types: actual testing ## Actual Testing -Suppose we now have algorithm `A` and algorithm `B`, both of which can solve the same problem, and we need to compare the efficiency of these two algorithms. The most direct method is to find a computer, run these two algorithms, and monitor and record their running time and memory usage. This evaluation approach can reflect the real situation, but it also has considerable limitations. +Suppose we now have algorithm `A` and algorithm `B`, both of which can solve the same problem, and we need to compare their efficiency. The most direct method is to run them on a computer and measure their running time and memory usage. This evaluation approach can reflect real-world behavior, but it also has considerable limitations. -On one hand, **it is difficult to eliminate interference factors from the testing environment**. Hardware configuration affects the performance of algorithms. For example, if an algorithm has a high degree of parallelism, it is more suitable for running on multi-core CPUs; if an algorithm has intensive memory operations, it will perform better on high-performance memory. In other words, the test results of an algorithm on different machines may be inconsistent. This means we need to test on various machines and calculate average efficiency, which is impractical. +On one hand, **it is difficult to eliminate interference factors from the testing environment**. Hardware configuration affects algorithmic performance. For example, if an algorithm has a high degree of parallelism, it is more suitable for running on multi-core CPUs; if an algorithm performs memory-intensive operations, it will benefit more from high-performance memory. In other words, the test results of an algorithm on different machines may be inconsistent. This means we need to test on various machines and calculate average efficiency, which is impractical. On the other hand, **conducting complete testing is very resource-intensive**. As the input data volume changes, the algorithm will exhibit different efficiencies. For example, when the input data volume is small, the running time of algorithm `A` is shorter than algorithm `B`; but when the input data volume is large, the test results may be exactly the opposite. Therefore, to obtain convincing conclusions, we need to test input data of various scales, which requires a large amount of computational resources. ## Theoretical Estimation -Since actual testing has considerable limitations, we can consider evaluating algorithm efficiency through calculations alone. This estimation method is called asymptotic complexity analysis, or complexity analysis for short. +Since actual testing has considerable limitations, we can consider evaluating algorithm efficiency through theoretical calculation. This estimation method is called asymptotic complexity analysis, or complexity analysis for short. -Complexity analysis can reflect the relationship between the time and space resources required for algorithm execution and the input data scale. **It describes the growth trend of the time and space required for algorithm execution as the input data scale increases**. This definition is somewhat convoluted, so we can break it down into three key points to understand. +Complexity analysis can reflect the relationship between the time and space resources required for algorithm execution and the input data scale. **It describes the growth trend of the time and space required for algorithm execution as the input data scale increases**. This definition is a bit cumbersome, so we can break it down into three key points to understand. - "Time and space resources" correspond to time complexity and space complexity, respectively. - "As the input data scale increases" means that complexity reflects the relationship between algorithm running efficiency and input data scale. @@ -44,6 +44,6 @@ Complexity analysis can reflect the relationship between the time and space reso Complexity analysis provides us with a "ruler" for evaluating algorithm efficiency, allowing us to measure the time and space resources required to execute a certain algorithm and compare the efficiency between different algorithms. -Complexity is a mathematical concept that may be relatively abstract for beginners, with a relatively high learning difficulty. From this perspective, complexity analysis may not be very suitable as the first content to be introduced. However, when we discuss the characteristics of a certain data structure or algorithm, it is difficult to avoid analyzing its running speed and space usage. +Complexity is a mathematical concept that may feel abstract and challenging for beginners. From this perspective, complexity analysis may not be the most suitable topic to introduce first. However, when we discuss the characteristics of a certain data structure or algorithm, it is difficult to avoid analyzing its running speed and space usage. -In summary, it is recommended that before diving deep into data structures and algorithms, **you first establish a preliminary understanding of complexity analysis so that you can complete complexity analysis of simple algorithms**. +In summary, it is recommended that before diving deep into data structures and algorithms, **you first establish a preliminary understanding of complexity analysis so that you can analyze the complexity of simple algorithms**. diff --git a/en/docs/chapter_computational_complexity/space_complexity.md b/en/docs/chapter_computational_complexity/space_complexity.md index 1bab67259..b84296576 100644 --- a/en/docs/chapter_computational_complexity/space_complexity.md +++ b/en/docs/chapter_computational_complexity/space_complexity.md @@ -18,7 +18,7 @@ Temporary space can be further divided into three parts. - **Stack frame space**: Used to save the context data of called functions. The system creates a stack frame at the top of the stack each time a function is called, and the stack frame space is released after the function returns. - **Instruction space**: Used to save compiled program instructions, which are usually ignored in actual statistics. -When analyzing the space complexity of a program, **we usually count three parts: temporary data, stack frame space, and output data**, as shown in the following figure. +When analyzing the space complexity of a program, **we usually consider three parts: temporary data, stack frame space, and output data**, as shown in the following figure. ![Algorithm-related space](space_complexity.assets/space_types.png) @@ -363,11 +363,11 @@ The related code is as follows: ## Calculation Method -The calculation method for space complexity is roughly the same as for time complexity, except that the statistical object is changed from "number of operations" to "size of space used". +The calculation method for space complexity is roughly the same as for time complexity, except that what we measure changes from the "number of operations" to the "amount of space used". Unlike time complexity, **we usually only focus on the worst-case space complexity**. This is because memory space is a hard requirement, and we must ensure that sufficient memory space is reserved for all input data. -Observe the following code. The "worst case" in worst-case space complexity has two meanings. +Observe the following code. Here, "worst case" in worst-case space complexity has two meanings. 1. **Based on the worst input data**: When $n < 10$, the space complexity is $O(1)$; but when $n > 10$, the initialized array `nums` occupies $O(n)$ space, so the worst-case space complexity is $O(n)$. 2. **Based on the peak memory during algorithm execution**: For example, before executing the last line, the program occupies $O(1)$ space; when initializing the array `nums`, the program occupies $O(n)$ space, so the worst-case space complexity is $O(n)$. @@ -806,8 +806,8 @@ Let the input data size be $n$. The following figure shows common types of space $$ \begin{aligned} -O(1) < O(\log n) < O(n) < O(n^2) < O(2^n) \newline -\text{Constant} < \text{Logarithmic} < \text{Linear} < \text{Quadratic} < \text{Exponential} +& O(1) < O(\log n) < O(n) < O(n^2) < O(2^n) \newline +& \text{Constant} < \text{Logarithmic} < \text{Linear} < \text{Quadratic} < \text{Exponential} \end{aligned} $$ @@ -815,7 +815,7 @@ $$ ### Constant Order $O(1)$ -Constant order is common in constants, variables, and objects whose quantity is independent of the input data size $n$. +Constant order is common for constants, variables, and objects whose number is independent of the input data size $n$. It should be noted that memory occupied by initializing variables or calling functions in a loop is released when entering the next iteration, so it does not accumulate space, and the space complexity remains $O(1)$: @@ -875,6 +875,6 @@ Another example is converting a number to a string. Given a positive integer $n$ Ideally, we hope that both the time complexity and space complexity of an algorithm can reach optimal. However, in practice, optimizing both time complexity and space complexity simultaneously is usually very difficult. -**Reducing time complexity usually comes at the cost of increasing space complexity, and vice versa**. The approach of sacrificing memory space to improve algorithm execution speed is called "trading space for time"; conversely, it is called "trading time for space". +**Reducing time complexity usually comes at the cost of increasing space complexity, and vice versa**. Sacrificing memory space to improve execution speed is called "trading space for time"; the reverse is called "trading time for space". The choice of which approach depends on which aspect we value more. In most cases, time is more precious than space, so "trading space for time" is usually the more common strategy. Of course, when the data volume is very large, controlling space complexity is also very important. diff --git a/en/docs/chapter_computational_complexity/summary.md b/en/docs/chapter_computational_complexity/summary.md index 4671357f1..8ea91def0 100644 --- a/en/docs/chapter_computational_complexity/summary.md +++ b/en/docs/chapter_computational_complexity/summary.md @@ -6,16 +6,16 @@ - Time efficiency and space efficiency are the two primary evaluation metrics for measuring algorithm performance. - We can evaluate algorithm efficiency through actual testing, but it is difficult to eliminate the influence of the testing environment, and it consumes substantial computational resources. -- Complexity analysis can eliminate the drawbacks of actual testing, with results applicable to all running platforms, and it can reveal algorithm efficiency under different data scales. +- Complexity analysis can overcome the limitations of actual testing. Its results apply across running platforms, and it can reveal algorithm efficiency under different data scales. **Time Complexity** -- Time complexity is used to measure the trend of algorithm runtime as data volume increases. It can effectively evaluate algorithm efficiency, but may fail in certain situations, such as when the input data volume is small or when time complexities are identical, making it impossible to precisely compare algorithm efficiency. +- Time complexity is used to measure the trend of algorithm runtime as data volume increases. It can effectively evaluate algorithm efficiency, but it may be less informative in certain situations, such as when the input data volume is small or when time complexities are identical, making it impossible to precisely compare algorithm efficiency. - Worst-case time complexity is represented using Big $O$ notation, corresponding to the asymptotic upper bound of a function, reflecting the growth level of the number of operations $T(n)$ as $n$ approaches positive infinity. - Deriving time complexity involves two steps: first, counting the number of operations, then determining the asymptotic upper bound. - Common time complexities arranged from low to high include $O(1)$, $O(\log n)$, $O(n)$, $O(n \log n)$, $O(n^2)$, $O(2^n)$, and $O(n!)$. - The time complexity of some algorithms is not fixed, but rather depends on the distribution of input data. Time complexity is divided into worst-case, best-case, and average-case time complexity. Best-case time complexity is rarely used because input data generally needs to satisfy strict conditions to achieve the best case. -- Average time complexity reflects the algorithm's runtime efficiency under random data input, and is closest to the algorithm's performance in practical applications. Calculating average time complexity requires statistical analysis of input data distribution and the combined mathematical expectation. +- Average time complexity reflects the algorithm's runtime efficiency under random data input, and is closest to the algorithm's performance in practical applications. Calculating average time complexity requires analyzing the input data distribution and the resulting mathematical expectation. **Space Complexity** @@ -32,7 +32,7 @@ Theoretically, the space complexity of tail recursive functions can be optimized **Q**: What is the difference between the terms function and method? -A function can be executed independently, with all parameters passed explicitly. A method is associated with an object, is implicitly passed to the object that invokes it, and can operate on data contained in class instances. +A function can be executed independently, with all parameters passed explicitly. A method is associated with an object, is implicitly bound to the object that invokes it, and can operate on data contained in class instances. The following examples use several common programming languages for illustration. @@ -46,7 +46,7 @@ No, the diagram shows space complexity, which reflects growth trends rather than Assuming $n = 8$, you might find that the values of each curve do not correspond to the functions. This is because each curve contains a constant term used to compress the value range into a visually comfortable range. -In practice, because we generally do not know what the "constant term" complexity of each method is, we usually cannot select the optimal solution for $n = 8$ based on complexity alone. But for $n = 8^5$, the choice is straightforward, as the growth trend already dominates. +In practice, because we generally do not know the "constant-term" cost of each method, we usually cannot choose the optimal solution for cases like $n = 8$ based on complexity alone. But for $n = 8^5$, the choice is straightforward, because the growth trend already dominates. **Q**: Are there situations where algorithms are designed to sacrifice time (or space) based on actual use cases? diff --git a/en/docs/chapter_computational_complexity/time_complexity.md b/en/docs/chapter_computational_complexity/time_complexity.md index fb734d9fd..94d6aed6f 100644 --- a/en/docs/chapter_computational_complexity/time_complexity.md +++ b/en/docs/chapter_computational_complexity/time_complexity.md @@ -207,7 +207,7 @@ $$ 1 + 1 + 10 + (1 + 5) \times n = 6n + 12 $$ -In reality, however, **counting an algorithm's runtime is neither reasonable nor realistic**. First, we do not want to tie the estimated time to the running platform, because algorithms need to run on various different platforms. Second, it is difficult to know the runtime of each type of operation, which brings great difficulty to the estimation process. +In reality, however, **trying to count an algorithm's exact runtime is neither practical nor realistic**. First, we do not want to tie the estimated time to the running platform, because algorithms need to run on many different platforms. Second, it is difficult to know the runtime of each type of operation, which makes the estimation process extremely difficult. ## Counting Time Growth Trends @@ -495,7 +495,7 @@ The figure below shows the time complexity of the above three algorithm function Compared to directly counting the algorithm's runtime, what are the characteristics of time complexity analysis? - **Time complexity can effectively evaluate algorithm efficiency**. For example, the runtime of algorithm `B` grows linearly; when $n > 1$ it is slower than algorithm `A`, and when $n > 1000000$ it is slower than algorithm `C`. In fact, as long as the input data size $n$ is sufficiently large, an algorithm with "constant order" complexity will always be superior to one with "linear order" complexity, which is precisely the meaning of time growth trend. -- **The derivation method for time complexity is simpler**. Obviously, the running platform and the types of computational operations are both unrelated to the growth trend of the algorithm's runtime. Therefore, in time complexity analysis, we can simply treat the execution time of all computational operations as the same "unit time", thus simplifying "counting computational operation runtime" to "counting the number of computational operations", which greatly reduces the difficulty of estimation. +- **The derivation method for time complexity is simpler**. Obviously, the running platform and the types of computational operations are both unrelated to the growth trend of the algorithm's runtime. Therefore, in time complexity analysis, we can simply treat the execution time of all computational operations as the same "unit time", reducing "tracking the runtime of each operation" to "counting the number of operations", which greatly reduces the difficulty of estimation. - **Time complexity also has certain limitations**. For example, although algorithms `A` and `C` have the same time complexity, their actual runtimes differ significantly. Similarly, although algorithm `B` has a higher time complexity than `C`, when the input data size $n$ is small, algorithm `B` is clearly superior to algorithm `C`. In such cases, it is often difficult to judge the efficiency of algorithms based solely on time complexity. Of course, despite the above issues, complexity analysis remains the most effective and commonly used method for evaluating algorithm efficiency. ## Asymptotic Upper Bound of Functions @@ -705,7 +705,7 @@ As shown in the figure below, calculating the asymptotic upper bound is to find ## Derivation Method -The asymptotic upper bound has a bit of mathematical flavor. If you feel you haven't fully understood it, don't worry. We can first master the derivation method, and gradually grasp its mathematical meaning through continuous practice. +The idea of an asymptotic upper bound is somewhat mathematical. If you feel you haven't fully understood it, don't worry. We can first master the derivation method, and gradually grasp its mathematical meaning through continuous practice. According to the definition, after determining $f(n)$, we can obtain the time complexity $O(f(n))$. So how do we determine the asymptotic upper bound $f(n)$? Overall, it is divided into two steps: first count the number of operations, then determine the asymptotic upper bound. @@ -992,8 +992,8 @@ Let the input data size be $n$. Common time complexity types are shown in the fi $$ \begin{aligned} -O(1) < O(\log n) < O(n) < O(n \log n) < O(n^2) < O(2^n) < O(n!) \newline -\text{Constant order} < \text{Logarithmic order} < \text{Linear order} < \text{Linearithmic order} < \text{Quadratic order} < \text{Exponential order} < \text{Factorial order} +& O(1) < O(\log n) < O(n) < O(n \log n) < O(n^2) < O(2^n) < O(n!) \newline +& \text{Constant} < \text{Logarithmic} < \text{Linear} < \text{Linearithmic} < \text{Quadratic} < \text{Exponential} < \text{Factorial} \end{aligned} $$ @@ -1003,7 +1003,7 @@ $$ The number of operations in constant order is independent of the input data size $n$, meaning it does not change as $n$ changes. -In the following function, although the number of operations `size` may be large, since it is independent of the input data size $n$, the time complexity remains $O(1)$: +In the following function, although the value of `size` may be large, it is independent of the input data size $n$, so the time complexity remains $O(1)$: ```src [file]{time_complexity}-[class]{}-[func]{constant} @@ -1081,7 +1081,7 @@ Like exponential order, logarithmic order also commonly appears in recursive fun [file]{time_complexity}-[class]{}-[func]{log_recur} ``` -Logarithmic order commonly appears in algorithms based on the divide-and-conquer strategy, embodying the algorithmic thinking of "dividing into many" and "simplifying complexity". It grows slowly and is the ideal time complexity second only to constant order. +Logarithmic order commonly appears in algorithms based on the divide-and-conquer strategy, reflecting the idea of repeatedly splitting a problem and simplifying it. It grows slowly and is the ideal time complexity second only to constant order. !!! tip "What is the base of $O(\log n)$?" @@ -1140,7 +1140,7 @@ The "worst-case time complexity" corresponds to the function's asymptotic upper It is worth noting that we rarely use best-case time complexity in practice, because it can usually only be achieved with a very small probability and may be somewhat misleading. **The worst-case time complexity is more practical because it gives a safety value for efficiency**, allowing us to use the algorithm with confidence. -From the above example, we can see that both worst-case and best-case time complexities only occur under "special data distributions", which may have a very small probability of occurrence and may not truly reflect the algorithm's running efficiency. In contrast, **average time complexity can reflect the algorithm's running efficiency under random input data**, denoted using the $\Theta$ notation. +From the above example, we can see that both worst-case and best-case time complexities arise only under particular input distributions, which may occur with very low probability and may not truly reflect the algorithm's running efficiency. In contrast, **average time complexity can reflect the algorithm's running efficiency under random input data**, denoted using the $\Theta$ notation. For some algorithms, we can simply derive the average case under random data distribution. For example, in the above example, since the input array is shuffled, the probability of element $1$ appearing at any index is equal, so the algorithm's average number of loops is half the array length $n / 2$, giving an average time complexity of $\Theta(n / 2) = \Theta(n)$. diff --git a/en/docs/chapter_data_structure/basic_data_types.md b/en/docs/chapter_data_structure/basic_data_types.md index 8b866e7da..0658d3d1b 100644 --- a/en/docs/chapter_data_structure/basic_data_types.md +++ b/en/docs/chapter_data_structure/basic_data_types.md @@ -1,6 +1,6 @@ # Basic Data Types -When we talk about data in computers, we think of various forms such as text, images, videos, audio, 3D models, and more. Although these data are organized in different ways, they are all composed of various basic data types. +When we talk about data stored in computers, we think of various forms such as text, images, videos, audio, 3D models, and more. Although these kinds of data are organized in different ways, they are all composed of various basic data types. **Basic data types are types that the CPU can directly operate on**, and they are directly used in algorithms, mainly including the following. @@ -9,7 +9,7 @@ When we talk about data in computers, we think of various forms such as text, im - Character type `char`, used to represent letters, punctuation marks, and even emojis in various languages. - Boolean type `bool`, used to represent "yes" and "no" judgments. -**Basic data types are stored in binary form in computers**. One binary bit is $1$ bit. In most modern operating systems, $1$ byte consists of $8$ bits. +**Basic data types are stored in binary form in computers**. A binary digit is one bit. In most modern operating systems, $1$ byte consists of $8$ bits. The range of values for basic data types depends on the size of the space they occupy. Below is an example using Java. @@ -31,16 +31,16 @@ The following table lists the space occupied, value ranges, and default values o | Character | `char` | 2 bytes | $0$ | $2^{16} - 1$ | $0$ | | Boolean | `bool` | 1 byte | $\text{false}$ | $\text{true}$ | $\text{false}$ | -Please note that the above table is specific to Java's basic data types. Each programming language has its own data type definitions, and their space occupied, value ranges, and default values may vary. +Please note that the table above applies specifically to Java's basic data types. Each programming language has its own type definitions, and their space usage, value ranges, and default values may vary. - In Python, the integer type `int` can be of any size, limited only by available memory; the floating-point type `float` is double-precision 64-bit; there is no `char` type, a single character is actually a string `str` of length 1. - C and C++ do not explicitly specify the size of basic data types, which varies by implementation and platform. The above table follows the LP64 [data model](https://en.cppreference.com/w/cpp/language/types#Properties), which is used in Unix 64-bit operating systems including Linux and macOS. - The size of character `char` is 1 byte in C and C++, and in most programming languages it depends on the specific character encoding method, as detailed in the "Character Encoding" section. - Even though representing a boolean value requires only 1 bit ($0$ or $1$), it is usually stored as 1 byte in memory. This is because modern computer CPUs typically use 1 byte as the minimum addressable memory unit. -So, what is the relationship between basic data types and data structures? We know that data structures are ways of organizing and storing data in computers. The subject of this statement is "structure", not "data". +So, what is the relationship between basic data types and data structures? We know that data structures are ways of organizing and storing data in computers. Here, the emphasis is on the "structure", not the "data". -If we want to represent "a row of numbers", we naturally think of using an array. This is because the linear structure of an array can represent the adjacency and order relationships of numbers, but the content stored—whether integer `int`, floating-point `float`, or character `char`—is unrelated to the "data structure". +If we want to represent "a row of numbers", we naturally think of using an array. This is because the linear structure of an array can represent the adjacency and order relationships of numbers, but whether the stored content is integer `int`, floating-point `float`, or character `char` is unrelated to the "data structure". In other words, **basic data types provide the "content type" of data, while data structures provide the "organization method" of data**. For example, in the following code, we use the same data structure (array) to store and represent different basic data types, including `int`, `float`, `char`, `bool`, etc. diff --git a/en/docs/chapter_data_structure/character_encoding.md b/en/docs/chapter_data_structure/character_encoding.md index 3bd159b12..80e56582c 100644 --- a/en/docs/chapter_data_structure/character_encoding.md +++ b/en/docs/chapter_data_structure/character_encoding.md @@ -2,7 +2,7 @@ In computers, all data is stored in binary form, and character `char` is no exception. To represent characters, we need to establish a "character set" that defines a one-to-one correspondence between each character and binary numbers. With a character set, computers can convert binary numbers to characters by looking up the table. -## Ascii Character Set +## ASCII Character Set ASCII code is the earliest character set, with the full name American Standard Code for Information Interchange. It uses 7 binary bits (the lower 7 bits of one byte) to represent a character, and can represent a maximum of 128 different characters. As shown in the figure below, ASCII code includes uppercase and lowercase English letters, numbers 0 ~ 9, some punctuation marks, and some control characters (such as newline and tab). @@ -12,9 +12,9 @@ However, **ASCII code can only represent English**. With the globalization of co Worldwide, a batch of EASCII character sets suitable for different regions have appeared successively. The first 128 characters of these character sets are unified as ASCII code, and the last 128 characters are defined differently to adapt to the needs of different languages. -## Gbk Character Set +## GBK Character Set -Later, people found that **EASCII code still cannot meet the character quantity requirements of many languages**. For example, there are nearly one hundred thousand Chinese characters, and several thousand are used daily. In 1980, the China National Standardization Administration released the GB2312 character set, which included 6,763 Chinese characters, basically meeting the needs for computer processing of Chinese characters. +Later, people found that **EASCII still could not provide enough characters for many languages**. For example, there are nearly one hundred thousand Chinese characters, and several thousand are used in everyday life. In 1980, the China National Standardization Administration released the GB2312 character set, which included 6,763 Chinese characters, basically meeting the needs of computer processing for Chinese. However, GB2312 cannot handle some rare characters and traditional Chinese characters. The GBK character set is an extension based on GB2312, which includes a total of 21,886 Chinese characters. In the GBK encoding scheme, ASCII characters are represented using one byte, and Chinese characters are represented using two bytes. @@ -22,13 +22,11 @@ However, GB2312 cannot handle some rare characters and traditional Chinese chara With the vigorous development of computer technology, character sets and encoding standards flourished, which brought many problems. On the one hand, these character sets generally only define characters for specific languages and cannot work normally in multilingual environments. On the other hand, multiple character set standards exist for the same language, and if two computers use different encoding standards, garbled characters will appear during information transmission. -Researchers of that era thought: **If a sufficiently complete character set is released that includes all languages and symbols in the world, wouldn't it be possible to solve cross-language environment and garbled character problems**? Driven by this idea, a large and comprehensive character set, Unicode, was born. +Researchers of that era thought: **If a sufficiently complete character set were released to include all languages and symbols in the world, wouldn't that solve problems in cross-language environments and eliminate garbled text**? Driven by this idea, a large and comprehensive character set, Unicode, was born. -Unicode is called "统一码" (Unified Code) in Chinese and can theoretically accommodate over one million characters. It is committed to including characters from around the world into a unified character set, providing a universal character set to handle and display various language texts, reducing garbled character problems caused by different encoding standards. +Unicode, or Unified Code, can theoretically accommodate over one million characters. It is committed to including characters from around the world into a unified character set, providing a universal character set to handle and display various language texts, reducing garbled character problems caused by different encoding standards. Since its release in 1991, Unicode has continuously expanded to include new languages and characters. As of September 2022, Unicode has included 149,186 characters, including characters, symbols, and even emojis from various languages. -Since its release in 1991, Unicode has continuously expanded to include new languages and characters. As of September 2022, Unicode has included 149,186 characters, including characters, symbols, and even emojis from various languages. Unicode maps each character to a code point (a character identifier), whose values range from 0 to 1114111 (that is, U+0000 to U+10FFFF), forming a unified character numbering space. - -Unicode is a universal character set that essentially assigns a number (called a "code point") to each character, **but it does not specify how to store these character code points in computers**. We can't help but ask: when Unicode code points of multiple lengths appear simultaneously in a text, how does the system parse the characters? For example, given an encoding with a length of 2 bytes, how does the system determine whether it is one 2-byte character or two 1-byte characters? +As a universal character set, Unicode essentially assigns each character a unique "code point" (character identifier), whose range is U+0000 to U+10FFFF, forming a unified character numbering space. However, **Unicode does not specify how to store these character code points in computers**. We can't help but ask: when Unicode code points of multiple lengths appear simultaneously in a text, how does the system parse the characters? For example, given an encoding with a length of 2 bytes, how does the system determine whether it is one 2-byte character or two 1-byte characters? For the above problem, **a straightforward solution is to store all characters as equal-length encodings**. As shown in the figure below, each character in "Hello" occupies 1 byte, and each character in "算法" (algorithm) occupies 2 bytes. We can encode all characters in "Hello 算法" as 2 bytes in length by padding the high bits with 0. In this way, the system can parse one character every 2 bytes and restore the content of this phrase. @@ -36,7 +34,7 @@ For the above problem, **a straightforward solution is to store all characters a However, ASCII code has already proven to us that encoding English only requires 1 byte. If the above scheme is adopted, the size of English text will be twice that under ASCII encoding, which is very wasteful of memory space. Therefore, we need a more efficient Unicode encoding method. -## Utf-8 Encoding +## UTF-8 Encoding Currently, UTF-8 has become the most widely used Unicode encoding method internationally. **It is a variable-length encoding** that uses 1 to 4 bytes to represent a character, depending on the complexity of the character. ASCII characters only require 1 byte, Latin and Greek letters require 2 bytes, commonly used Chinese characters require 3 bytes, and some other rare characters require 4 bytes. @@ -45,7 +43,7 @@ The encoding rules of UTF-8 are not complicated and can be divided into the foll - For 1-byte characters, set the highest bit to $0$, and set the remaining 7 bits to the Unicode code point. It is worth noting that ASCII characters occupy the first 128 code points in the Unicode character set. That is to say, **UTF-8 encoding is backward compatible with ASCII code**. This means we can use UTF-8 to parse very old ASCII code text. - For characters with a length of $n$ bytes (where $n > 1$), set the highest $n$ bits of the first byte to $1$, and set the $(n + 1)$-th bit to $0$; starting from the second byte, set the highest 2 bits of each byte to $10$; use all remaining bits to fill in the Unicode code point of the character. -The figure below shows the UTF-8 encoding corresponding to "Hello算法". It can be observed that since the highest $n$ bits are all set to $1$, the system can parse the length of the character as $n$ by reading the number of highest bits that are $1$. +The figure below shows the UTF-8 encoding corresponding to "Hello 算法". It can be observed that since the highest $n$ bits are all set to $1$, the system can determine that the character length is $n$ by counting the leading $1$ bits. But why set the highest 2 bits of all other bytes to $10$? In fact, this $10$ can serve as a check symbol. Assuming the system starts parsing text from an incorrect byte, the $10$ at the beginning of the byte can help the system quickly determine an anomaly. @@ -64,7 +62,7 @@ From a compatibility perspective, UTF-8 has the best universality, and many tool ## Character Encoding in Programming Languages -For most past programming languages, strings during program execution use fixed-length encodings such as UTF-16 or UTF-32. Under fixed-length encoding, we can treat strings as arrays for processing, and this approach has the following advantages. +For many programming languages in the past, strings during program execution used internal encodings such as UTF-16 or UTF-32. Under these representations, we can often treat strings like arrays during processing, and this approach has the following advantages. - **Random access**: UTF-16 encoded strings can be easily accessed randomly. UTF-8 is a variable-length encoding. To find the $i$-th character, we need to traverse from the beginning of the string to the $i$-th character, which requires $O(n)$ time. - **Character counting**: Similar to random access, calculating the length of a UTF-16 encoded string is also an $O(1)$ operation. However, calculating the length of a UTF-8 encoded string requires traversing the entire string. diff --git a/en/docs/chapter_data_structure/classification_of_data_structure.md b/en/docs/chapter_data_structure/classification_of_data_structure.md index e260d9146..5a050683b 100644 --- a/en/docs/chapter_data_structure/classification_of_data_structure.md +++ b/en/docs/chapter_data_structure/classification_of_data_structure.md @@ -4,7 +4,7 @@ Common data structures include arrays, linked lists, stacks, queues, hash tables ## Logical Structure: Linear and Non-Linear -**Logical structure reveals the logical relationships between data elements**. In arrays and linked lists, data is arranged in a certain order, embodying the linear relationship between data; while in trees, data is arranged hierarchically from top to bottom, showing the derived relationship between "ancestors" and "descendants"; graphs are composed of nodes and edges, reflecting complex network relationships. +**Logical structure reveals the logical relationships between data elements**. In arrays and linked lists, data is arranged in a certain order, embodying linear relationships between elements; while in trees, data is arranged hierarchically from top to bottom, showing parent-descendant relationships; graphs are composed of nodes and edges, reflecting complex network relationships. As shown in the figure below, logical structures can be divided into two major categories: "linear" and "non-linear". Linear structures are more intuitive, indicating that data is linearly arranged in logical relationships; non-linear structures are the opposite, arranged non-linearly. @@ -28,11 +28,11 @@ Non-linear data structures can be further divided into tree structures and netwo !!! tip - It is worth noting that comparing memory to an Excel spreadsheet is a simplified analogy. The actual working mechanism of memory is quite complex, involving concepts such as address space, memory management, cache mechanisms, virtual memory, and physical memory. + It should be noted that comparing memory to an Excel spreadsheet is only a simplified analogy. The actual workings of memory are much more complex, involving concepts such as address space, memory management, cache mechanisms, virtual memory, and physical memory. Memory is a shared resource for all programs. When a block of memory is occupied by a program, it usually cannot be used by other programs at the same time. **Therefore, in the design of data structures and algorithms, memory resources are an important consideration**. For example, the peak memory occupied by an algorithm should not exceed the remaining free memory of the system; if there is a lack of contiguous large memory blocks, then the data structure chosen must be able to be stored in dispersed memory spaces. -As shown in the figure below, **physical structure reflects the way data is stored in computer memory**, and can be divided into contiguous space storage (arrays) and dispersed space storage (linked lists). The two physical structures exhibit complementary characteristics in terms of time efficiency and space efficiency. +As shown in the figure below, **physical structure reflects the way data is stored in computer memory**. It can be divided into contiguous-space storage (arrays) and dispersed-space storage (linked lists). At a low level, physical structure determines how data is accessed, updated, inserted, and deleted. These two physical structures exhibit complementary characteristics in terms of time efficiency and space efficiency. ![Contiguous space storage and dispersed space storage](classification_of_data_structure.assets/classification_phisical_structure.png) @@ -41,7 +41,7 @@ It is worth noting that **all data structures are implemented based on arrays, l - **Can be implemented based on arrays**: Stacks, queues, hash tables, trees, heaps, graphs, matrices, tensors (arrays with dimensions $\geq 3$), etc. - **Can be implemented based on linked lists**: Stacks, queues, hash tables, trees, heaps, graphs, etc. -After initialization, linked lists can still adjust their length during program execution, so they are also called "dynamic data structures". After initialization, the length of arrays cannot be changed, so they are also called "static data structures". It is worth noting that arrays can achieve length changes by reallocating memory, thus possessing a certain degree of "dynamism". +After initialization, linked lists can still adjust their length during program execution, so they are also called "dynamic data structures". After initialization, the length of arrays cannot be changed, so they are also called "static data structures". It is worth noting that arrays can change length by reallocating memory, thus retaining a limited degree of flexibility. !!! tip diff --git a/en/docs/chapter_data_structure/index.md b/en/docs/chapter_data_structure/index.md index 82ef70b96..2997f6b31 100644 --- a/en/docs/chapter_data_structure/index.md +++ b/en/docs/chapter_data_structure/index.md @@ -4,6 +4,6 @@ !!! abstract - Data structure is like a sturdy and diverse framework. + Data structures are like a sturdy and diverse framework. It provides a blueprint for the orderly organization of data, upon which algorithms come to life. diff --git a/en/docs/chapter_data_structure/number_encoding.md b/en/docs/chapter_data_structure/number_encoding.md index c1a74376f..0b813c9c1 100644 --- a/en/docs/chapter_data_structure/number_encoding.md +++ b/en/docs/chapter_data_structure/number_encoding.md @@ -6,7 +6,7 @@ ## Sign-Magnitude, 1's Complement, and 2's Complement -In the table from the previous section, we found that all integer types can represent one more negative number than positive numbers. For example, the `byte` range is $[-128, 127]$. This phenomenon is counterintuitive, and its underlying reason involves knowledge of sign-magnitude, 1's complement, and 2's complement. +In the table from the previous section, we found that all integer types can represent one more negative number than positive numbers. For example, the `byte` range is $[-128, 127]$. This phenomenon is counterintuitive, and its underlying cause lies in sign-magnitude, 1's complement, and 2's complement representations. First, it should be noted that **numbers are stored in computers in the form of "2's complement"**. Before analyzing the reasons for this, let's first define these three concepts. @@ -63,7 +63,7 @@ $$ Adding $1$ to the 1's complement of negative zero produces a carry, but since the `byte` type has a length of only 8 bits, the $1$ that overflows to the 9th bit is discarded. That is to say, **the 2's complement of negative zero is $0000 \; 0000$, which is the same as the 2's complement of positive zero**. This means that in 2's complement representation, there is only one zero, and the positive and negative zero ambiguity is thus resolved. -One last question remains: the range of the `byte` type is $[-128, 127]$, and how is the extra negative number $-128$ obtained? We notice that all integers in the interval $[-127, +127]$ have corresponding sign-magnitude, 1's complement, and 2's complement, and sign-magnitude and 2's complement can be converted to each other. +One last question remains: the range of the `byte` type is $[-128, 127]$, so where does the extra negative number $-128$ come from? We notice that all integers in the interval $[-127, +127]$ have corresponding sign-magnitude, 1's complement, and 2's complement, and sign-magnitude and 2's complement can be converted to each other. However, **the 2's complement $1000 \; 0000$ is an exception, and it does not have a corresponding sign-magnitude**. According to the conversion method, we get that the sign-magnitude of this 2's complement is $0000 \; 0000$. This is clearly contradictory because this sign-magnitude represents the number $0$, and its 2's complement should be itself. The computer specifies that this special 2's complement $1000 \; 0000$ represents $-128$. In fact, the result of calculating $(-1) + (-127)$ in 2's complement is $-128$. @@ -82,7 +82,7 @@ You may have noticed that all the above calculations are addition operations. Th Please note that this does not mean that computers can only perform addition. **By combining addition with some basic logical operations, computers can implement various other mathematical operations**. For example, calculating the subtraction $a - b$ can be converted to calculating the addition $a + (-b)$; calculating multiplication and division can be converted to calculating multiple additions or subtractions. -Now we can summarize the reasons why computers use 2's complement: based on 2's complement representation, computers can use the same circuits and operations to handle the addition of positive and negative numbers, without the need to design special hardware circuits to handle subtraction, and without the need to specially handle the ambiguity problem of positive and negative zero. This greatly simplifies hardware design and improves operational efficiency. +We can now summarize why computers use 2's complement: with 2's complement representation, computers can use the same circuits and operations to handle the addition of positive and negative numbers, without designing special hardware circuits for subtraction or separately handling the ambiguity of positive and negative zero. This greatly simplifies hardware design and improves efficiency. The design of 2's complement is very ingenious. Due to space limitations, we will stop here. Interested readers are encouraged to explore further. diff --git a/en/docs/chapter_data_structure/summary.md b/en/docs/chapter_data_structure/summary.md index a32225860..7e0a82c99 100644 --- a/en/docs/chapter_data_structure/summary.md +++ b/en/docs/chapter_data_structure/summary.md @@ -3,15 +3,15 @@ ### Key Review - Data structures can be classified from two perspectives: logical structure and physical structure. Logical structure describes the logical relationships between data elements, while physical structure describes how data is stored in computer memory. -- Common logical structures include linear, tree, and network structures. We typically classify data structures as linear (arrays, linked lists, stacks, queues) and non-linear (trees, graphs, heaps) based on their logical structure. The implementation of hash tables may involve both linear and non-linear data structures. +- Common logical structures include linear, tree-like, and network structures. We typically classify data structures as linear (arrays, linked lists, stacks, queues) and non-linear (trees, graphs, heaps) based on their logical structure. The implementation of hash tables may involve both linear and non-linear data structures. - When a program runs, data is stored in computer memory. Each memory space has a corresponding memory address, and the program accesses data through these memory addresses. - Physical structures are primarily divided into contiguous space storage (arrays) and dispersed space storage (linked lists). All data structures are implemented using arrays, linked lists, or a combination of both. - Basic data types in computers include integers `byte`, `short`, `int`, `long`, floating-point numbers `float`, `double`, characters `char`, and booleans `bool`. Their value ranges depend on the size of space they occupy and their representation method. - Sign-magnitude, 1's complement, and 2's complement are three methods for encoding numbers in computers, and they can be converted into each other. The most significant bit of sign-magnitude is the sign bit, and the remaining bits represent the value of the number. - Integers are stored in computers in 2's complement form. Under 2's complement representation, computers can treat the addition of positive and negative numbers uniformly, without needing to design special hardware circuits for subtraction, and there is no ambiguity of positive and negative zero. - The encoding of floating-point numbers consists of 1 sign bit, 8 exponent bits, and 23 fraction bits. Due to the exponent bits, the range of floating-point numbers is much larger than that of integers, at the cost of sacrificing precision. -- ASCII is the earliest English character set, with a length of 1 byte, containing a total of 127 characters. GBK is a commonly used Chinese character set, containing over 20,000 Chinese characters. Unicode is committed to providing a complete character set standard, collecting characters from various languages around the world, thereby solving the garbled text problem caused by inconsistent character encoding methods. -- UTF-8 is the most popular Unicode encoding method, with excellent universality. It is a variable-length encoding method with good scalability, effectively improving storage space efficiency. UTF-16 and UTF-32 are fixed-length encoding methods. When encoding Chinese characters, UTF-16 occupies less space than UTF-8. Programming languages such as Java and C# use UTF-16 encoding by default. +- ASCII is the earliest English character set, with a length of 1 byte, containing a total of 128 characters. GBK is a commonly used Chinese character set, containing over 20,000 Chinese characters. Unicode is committed to providing a complete character set standard, collecting characters from various languages around the world, thereby solving the garbled text problem caused by inconsistent character encoding methods. +- UTF-8 is the most popular Unicode encoding method and has excellent compatibility. It is a variable-length encoding method with good scalability, effectively improving storage space efficiency. UTF-16 and UTF-32 are common Unicode encoding methods. When encoding Chinese characters, UTF-16 occupies less space than UTF-8. Programming languages such as Java and C# use UTF-16 encoding by default. ### Q & A @@ -31,7 +31,7 @@ Stacks can indeed implement dynamic data operations, but the data structure is s **Q**: When constructing a stack (queue), its size is not specified. Why are they "static data structures"? -In high-level programming languages, we do not need to manually specify the initial capacity of a stack (queue); this work is automatically completed within the class. For example, the initial capacity of Java's `ArrayList` is typically 10. Additionally, the expansion operation is also automatically implemented. See the subsequent "List" section for details. +In high-level programming languages, we do not need to manually specify the initial capacity of a stack (queue); the class handles this automatically. For example, the initial capacity of Java's `ArrayList` is typically 10. Additionally, the expansion operation is also automatically implemented. See the subsequent "List" section for details. **Q**: The method of converting sign-magnitude to 2's complement is "first negate then add 1". So converting 2's complement to sign-magnitude should be the inverse operation "first subtract 1 then negate". However, 2's complement can also be converted to sign-magnitude through "first negate then add 1". Why is this? @@ -61,6 +61,6 @@ $$ In summary, both "first negate then add 1" and "first subtract 1 then negate" are computing the complement to $10000$, and they are equivalent. -Essentially, the "negate" operation is actually finding the complement to $1111$ (because `sign-magnitude + 1's complement = 1111` always holds); and adding 1 to the 1's complement yields the 2's complement, which is the complement to $10000$. +Essentially, the "negate" operation is actually finding the complement to $1111$ (because "sign-magnitude + 1's complement = 1111" always holds); and adding 1 to the 1's complement yields the 2's complement, which is the complement to $10000$. The above uses $n = 4$ as an example, and it can be generalized to binary numbers of any number of bits. diff --git a/en/docs/chapter_divide_and_conquer/binary_search_recur.md b/en/docs/chapter_divide_and_conquer/binary_search_recur.md index 8076fb6d0..969a2d6be 100644 --- a/en/docs/chapter_divide_and_conquer/binary_search_recur.md +++ b/en/docs/chapter_divide_and_conquer/binary_search_recur.md @@ -3,7 +3,7 @@ We have already learned that search algorithms are divided into two major categories. - **Brute-force search**: Implemented by traversing the data structure, with a time complexity of $O(n)$. -- **Adaptive search**: Utilizes unique data organization forms or prior information, with time complexity reaching $O(\log n)$ or even $O(1)$. +- **Adaptive search**: Leverages specific data organization or prior information, with time complexity reaching $O(\log n)$ or even $O(1)$. In fact, **search algorithms with time complexity of $O(\log n)$ are typically implemented based on the divide and conquer strategy**, such as binary search and trees. @@ -24,7 +24,7 @@ In previous sections, binary search was implemented based on iteration. Now we i !!! question - Given a sorted array `nums` of length $n$, where all elements are unique, find the element `target`. + Given a sorted array `nums` of length $n$, where all elements are unique, find `target`. From a divide and conquer perspective, we denote the subproblem corresponding to the search interval $[i, j]$ as $f(i, j)$. @@ -32,7 +32,7 @@ Starting from the original problem $f(0, n-1)$, perform binary search through th 1. Calculate the midpoint $m$ of the search interval $[i, j]$, and use it to eliminate half of the search interval. 2. Recursively solve the subproblem reduced by half in size, which could be $f(i, m-1)$ or $f(m+1, j)$. -3. Repeat steps `1.` and `2.` until `target` is found or the interval is empty and return. +3. Repeat steps `1.` and `2.` until `target` is found, or return when the interval is empty. The figure below shows the divide and conquer process of binary search for element $6$ in an array. diff --git a/en/docs/chapter_divide_and_conquer/build_binary_tree_problem.md b/en/docs/chapter_divide_and_conquer/build_binary_tree_problem.md index 5b01d72ee..df70242c9 100644 --- a/en/docs/chapter_divide_and_conquer/build_binary_tree_problem.md +++ b/en/docs/chapter_divide_and_conquer/build_binary_tree_problem.md @@ -33,7 +33,7 @@ Using the data from the figure above as an example, we can obtain the division r ### Describing Subtree Intervals Based on Variables -Based on the above division method, **we have obtained the index intervals of the root node, left subtree, and right subtree in `preorder` and `inorder`**. To describe these index intervals, we need to use several pointer variables. +Based on the above division method, **we have obtained the index intervals of the root node, left subtree, and right subtree in `preorder` and `inorder`**. To describe these index intervals, we need to use several index variables. - Denote the index of the current tree's root node in `preorder` as $i$. - Denote the index of the current tree's root node in `inorder` as $m$. diff --git a/en/docs/chapter_divide_and_conquer/divide_and_conquer.md b/en/docs/chapter_divide_and_conquer/divide_and_conquer.md index a00456170..4274a398a 100644 --- a/en/docs/chapter_divide_and_conquer/divide_and_conquer.md +++ b/en/docs/chapter_divide_and_conquer/divide_and_conquer.md @@ -1,6 +1,6 @@ # Divide and Conquer Algorithms -Divide and conquer is a very important and common algorithm strategy. Divide and conquer is typically implemented based on recursion, consisting of two steps: "divide" and "conquer". +Divide and conquer is a very important and common algorithmic strategy. Divide and conquer is typically implemented based on recursion, consisting of two steps: "divide" and "conquer". 1. **Divide (partition phase)**: Recursively divide the original problem into two or more subproblems until the smallest subproblem is reached. 2. **Conquer (merge phase)**: Starting from the smallest subproblems with known solutions, merge the solutions of subproblems from bottom to top to construct the solution to the original problem. @@ -28,13 +28,13 @@ Clearly, merge sort satisfies these three criteria. ## Improving Efficiency Through Divide and Conquer -**Divide and conquer can not only effectively solve algorithmic problems but often also improve algorithm efficiency**. In sorting algorithms, quick sort, merge sort, and heap sort are faster than selection, bubble, and insertion sort because they apply the divide and conquer strategy. +**Divide and conquer can not only effectively solve algorithmic problems, but can often also improve algorithmic efficiency**. In sorting algorithms, quick sort, merge sort, and heap sort are faster than selection, bubble, and insertion sort because they apply the divide and conquer strategy. This raises the question: **Why can divide and conquer improve algorithm efficiency, and what is the underlying logic**? In other words, why is dividing a large problem into multiple subproblems, solving the subproblems, and merging their solutions more efficient than directly solving the original problem? This question can be discussed from two aspects: operation count and parallel computation. ### Operation Count Optimization -Taking "bubble sort" as an example, processing an array of length $n$ requires $O(n^2)$ time. Suppose we divide the array into two subarrays from the midpoint as shown in the figure below, the division requires $O(n)$ time, sorting each subarray requires $O((n / 2)^2)$ time, and merging the two subarrays requires $O(n)$ time, resulting in an overall time complexity of: +Taking "bubble sort" as an example, processing an array of length $n$ requires $O(n^2)$ time. Suppose we divide the array at the midpoint into two subarrays, as shown in the figure below. The division requires $O(n)$ time, sorting each subarray requires $O((n / 2)^2)$ time, and merging the two subarrays requires $O(n)$ time, resulting in an overall time complexity of: $$ O(n + (\frac{n}{2})^2 \times 2 + n) = O(\frac{n^2}{2} + 2n) @@ -60,7 +60,7 @@ Thinking further, **what if we set multiple division points** and evenly divide ### Parallel Computation Optimization -We know that the subproblems generated by divide and conquer are independent of each other, **so they can typically be solved in parallel**. This means divide and conquer can not only reduce the time complexity of algorithms, **but also benefits from parallel optimization by operating systems**. +We know that the subproblems generated by divide and conquer are independent of each other, **so they can typically be solved in parallel**. This means divide and conquer can not only reduce the time complexity of algorithms, **but is also amenable to parallel optimization by the operating system**. Parallel optimization is particularly effective in multi-core or multi-processor environments, as the system can simultaneously handle multiple subproblems, making fuller use of computing resources and significantly reducing overall runtime. @@ -70,7 +70,7 @@ For example, in the "bucket sort" shown in the figure below, we evenly distribut ## Common Applications of Divide and Conquer -On one hand, divide and conquer can be used to solve many classic algorithmic problems. +On the one hand, divide and conquer can be used to solve many classic algorithmic problems. - **Finding the closest pair of points**: This algorithm first divides the point set into two parts, then finds the closest pair of points in each part separately, and finally finds the closest pair of points that spans both parts. - **Large integer multiplication**: For example, the Karatsuba algorithm, which decomposes large integer multiplication into several smaller integer multiplications and additions. @@ -80,12 +80,12 @@ On one hand, divide and conquer can be used to solve many classic algorithmic pr On the other hand, divide and conquer is widely applied in the design of algorithms and data structures. -- **Binary search**: Binary search divides a sorted array into two parts from the midpoint index, then decides which half to eliminate based on the comparison result between the target value and the middle element value, and performs the same binary operation on the remaining interval. +- **Binary search**: Binary search divides a sorted array into two parts from the midpoint index, then decides which half to eliminate based on the comparison result between the target value and the middle element value, and performs the same binary-search step on the remaining interval. - **Merge sort**: Already introduced at the beginning of this section, no further elaboration needed. - **Quick sort**: Quick sort selects a pivot value, then divides the array into two subarrays, one with elements smaller than the pivot and the other with elements larger than the pivot, then performs the same division operation on these two parts until the subarrays have only one element. - **Bucket sort**: The basic idea of bucket sort is to scatter data into multiple buckets, then sort the elements within each bucket, and finally extract the elements from each bucket in sequence to obtain a sorted array. - **Trees**: For example, binary search trees, AVL trees, red-black trees, B-trees, B+ trees, etc. Their search, insertion, and deletion operations can all be viewed as applications of the divide and conquer strategy. - **Heaps**: A heap is a special complete binary tree, and its various operations, such as insertion, deletion, and heapify, actually imply the divide and conquer idea. -- **Hash tables**: Although hash tables do not directly apply divide and conquer, some hash collision resolution solutions indirectly apply the divide and conquer strategy. For example, long linked lists in chaining may be converted to red-black trees to improve query efficiency. +- **Hash tables**: Although hash tables do not directly apply divide and conquer, some methods for resolving hash collisions indirectly apply the divide and conquer strategy. For example, long linked lists in chaining may be converted to red-black trees to improve lookup efficiency. -It can be seen that **divide and conquer is a "subtly pervasive" algorithmic idea**, embedded in various algorithms and data structures. +It can be seen that **divide and conquer is a "quietly pervasive" algorithmic idea**, embedded in various algorithms and data structures. diff --git a/en/docs/chapter_divide_and_conquer/hanota_problem.md b/en/docs/chapter_divide_and_conquer/hanota_problem.md index 57eb6affd..5bfff8421 100644 --- a/en/docs/chapter_divide_and_conquer/hanota_problem.md +++ b/en/docs/chapter_divide_and_conquer/hanota_problem.md @@ -94,4 +94,4 @@ As shown in the figure below, the hanota problem forms a recursion tree of heigh The hanota problem originates from an ancient legend. In a temple in ancient India, monks had three tall diamond pillars and $64$ golden discs of different sizes. The monks continuously moved the discs, believing that when the last disc was correctly placed, the world would come to an end. - However, even if the monks moved one disc per second, it would take approximately $2^{64} \approx 1.84×10^{19}$ seconds, which is about $5850$ billion years, far exceeding current estimates of the age of the universe. Therefore, if this legend is true, we should not need to worry about the end of the world. + However, even if the monks moved one disc per second, it would take approximately $2^{64} \approx 1.84×10^{19}$ seconds, which is about $585$ billion years, far exceeding current estimates of the age of the universe. Therefore, if this legend is true, we should not need to worry about the end of the world. diff --git a/en/docs/chapter_divide_and_conquer/index.md b/en/docs/chapter_divide_and_conquer/index.md index 66cab2877..0c7b6c72a 100644 --- a/en/docs/chapter_divide_and_conquer/index.md +++ b/en/docs/chapter_divide_and_conquer/index.md @@ -6,4 +6,4 @@ Difficult problems are decomposed layer by layer, with each decomposition making them simpler. - Divide and conquer reveals an important truth: start with simplicity, and nothing remains complex. + Divide and conquer reveals an important truth: start with what is simple, and nothing remains complex. diff --git a/en/docs/chapter_divide_and_conquer/summary.md b/en/docs/chapter_divide_and_conquer/summary.md index bd573986a..8008b96ac 100644 --- a/en/docs/chapter_divide_and_conquer/summary.md +++ b/en/docs/chapter_divide_and_conquer/summary.md @@ -2,11 +2,11 @@ ### Key Review -- Divide and conquer is a common algorithm design strategy, consisting of two phases: divide (partition) and conquer (merge), typically implemented based on recursion. +- Divide and conquer is a common algorithm design strategy consisting of two phases, divide (partition) and conquer (merge), and is typically implemented recursively. - The criteria for determining whether a problem is a divide and conquer problem include: whether the problem can be decomposed, whether subproblems are independent, and whether subproblems can be merged. - Merge sort is a typical application of the divide and conquer strategy. It recursively divides an array into two equal-length subarrays until only one element remains, then merges them layer by layer to complete the sorting. -- Introducing the divide and conquer strategy can often improve algorithm efficiency. On one hand, the divide and conquer strategy reduces the number of operations; on the other hand, it facilitates parallel optimization of the system after division. -- Divide and conquer can both solve many algorithmic problems and is widely applied in data structure and algorithm design, appearing everywhere. +- Introducing the divide and conquer strategy can often improve algorithm efficiency. On one hand, it reduces the number of operations; on the other hand, it makes parallel optimization by the system easier. +- Divide and conquer can solve many algorithmic problems and is also widely used in data structures and algorithm design, making it ubiquitous. - Compared to brute-force search, adaptive search is more efficient. Search algorithms with time complexity of $O(\log n)$ are typically implemented based on the divide and conquer strategy. - Binary search is another typical application of divide and conquer. It does not include the step of merging solutions of subproblems. We can implement binary search through recursive divide and conquer. - In the problem of building a binary tree, building the tree (original problem) can be divided into building the left subtree and right subtree (subproblems), which can be achieved by dividing the index intervals of the preorder and inorder traversals. diff --git a/en/docs/chapter_dynamic_programming/dp_problem_features.md b/en/docs/chapter_dynamic_programming/dp_problem_features.md index 83e5e2bed..8c5619671 100644 --- a/en/docs/chapter_dynamic_programming/dp_problem_features.md +++ b/en/docs/chapter_dynamic_programming/dp_problem_features.md @@ -14,7 +14,7 @@ We make a slight modification to the stair climbing problem to make it more suit !!! question "Climbing stairs with minimum cost" - Given a staircase, where you can climb $1$ or $2$ steps at a time, and each step has a non-negative integer representing the cost you need to pay at that step. Given a non-negative integer array $cost$, where $cost[i]$ represents the cost at the $i$-th step, and $cost[0]$ is the ground (starting point). What is the minimum cost required to reach the top? + Given a staircase, you can climb $1$ or $2$ steps at a time, and each step is labeled with a non-negative integer representing the cost of stepping on it. Given a non-negative integer array $cost$, where $cost[i]$ represents the cost of the $i$-th step and $cost[0]$ is the ground (starting point), what is the minimum cost required to reach the top? As shown in the figure below, if the costs of the $1$st, $2$nd, and $3$rd steps are $1$, $10$, and $1$ respectively, then climbing from the ground to the $3$rd step requires a minimum cost of $2$. @@ -60,7 +60,7 @@ However, if we add a constraint to the stair climbing problem, the situation cha Given a staircase with $n$ steps, where you can climb $1$ or $2$ steps at a time, **but you cannot jump $1$ step in two consecutive rounds**. How many ways are there to climb to the top? -As shown in the figure below, there are only $2$ feasible ways to climb to the $3$rd step. The way of jumping $1$ step three consecutive times does not satisfy the constraint and is therefore discarded. +As shown in the figure below, there are only $2$ feasible ways to climb to the $3$rd step. The path with three consecutive $1$-step jumps does not satisfy the constraint and is therefore discarded. ![Number of ways to climb to the 3rd step with constraint](dp_problem_features.assets/climbing_stairs_constraint_example.png) @@ -70,8 +70,8 @@ It is not difficult to see that this problem no longer satisfies no aftereffects For this reason, we need to expand the state definition: **state $[i, j]$ represents being on the $i$-th step with the previous round having jumped $j$ steps**, where $j \in \{1, 2\}$. This state definition effectively distinguishes whether the previous round was a jump of $1$ step or $2$ steps, allowing us to determine where the current state came from. -- When the previous round jumped $1$ step, the round before that could only choose to jump $2$ steps, i.e., $dp[i, 1]$ can only be transferred from $dp[i-1, 2]$. -- When the previous round jumped $2$ steps, the round before that could choose to jump $1$ step or $2$ steps, i.e., $dp[i, 2]$ can be transferred from $dp[i-2, 1]$ or $dp[i-2, 2]$. +- When the previous round jumped $1$ step, the round before that could only choose to jump $2$ steps, i.e., $dp[i, 1]$ can only transition from $dp[i-1, 2]$. +- When the previous round jumped $2$ steps, the round before that could choose to jump $1$ step or $2$ steps, i.e., $dp[i, 2]$ can transition from $dp[i-2, 1]$ or $dp[i-2, 2]$. As shown in the figure below, under this definition, $dp[i, j]$ represents the number of ways for state $[i, j]$. The state transition equation is then: @@ -94,8 +94,8 @@ In the above case, since we only need to consider one more preceding state, we c !!! question "Climbing stairs with obstacle generation" - Given a staircase with $n$ steps, where you can climb $1$ or $2$ steps at a time. **It is stipulated that when climbing to the $i$-th step, the system will automatically place an obstacle on the $2i$-th step, and thereafter no round is allowed to jump to the $2i$-th step**. For example, if the first two rounds jump to the $2$nd and $3$rd steps, then afterwards you cannot jump to the $4$th and $6$th steps. How many ways are there to climb to the top? + Given a staircase with $n$ steps, where you can climb $1$ or $2$ steps at a time. **Whenever you reach the $i$-th step, the system automatically places an obstacle on the $2i$-th step, and no subsequent round is allowed to jump to the $2i$-th step**. For example, if the first two rounds jump to the $2$nd and $3$rd steps, then afterwards you cannot jump to the $4$th and $6$th steps. How many ways are there to climb to the top? In this problem, the next jump depends on all past states, because each jump places obstacles on higher steps, affecting future jumps. For such problems, dynamic programming is often difficult to solve. -In fact, many complex combinatorial optimization problems (such as the traveling salesman problem) do not satisfy no aftereffects. For such problems, we usually choose to use other methods, such as heuristic search, genetic algorithms, reinforcement learning, etc., to obtain usable local optimal solutions within a limited time. +In fact, many complex combinatorial optimization problems (such as the traveling salesman problem) do not satisfy no aftereffects. For such problems, we usually use other methods, such as heuristic search, genetic algorithms, and reinforcement learning, to obtain usable locally optimal solutions within a limited time. diff --git a/en/docs/chapter_dynamic_programming/dp_solution_pipeline.md b/en/docs/chapter_dynamic_programming/dp_solution_pipeline.md index bdfe8c69a..d00b51e5f 100644 --- a/en/docs/chapter_dynamic_programming/dp_solution_pipeline.md +++ b/en/docs/chapter_dynamic_programming/dp_solution_pipeline.md @@ -5,7 +5,7 @@ The previous two sections introduced the main characteristics of dynamic program 1. How to determine whether a problem is a dynamic programming problem? 2. What is the complete process for solving a dynamic programming problem, and where should we start? -## Problem Determination +## Problem Identification Generally speaking, if a problem contains overlapping subproblems, optimal substructure, and satisfies no aftereffects, then it is usually suitable for solving with dynamic programming. However, it is difficult to directly extract these characteristics from the problem description. Therefore, we usually relax the conditions and **first observe whether the problem is suitable for solving with backtracking (exhaustive search)**. @@ -13,17 +13,17 @@ Generally speaking, if a problem contains overlapping subproblems, optimal subst In other words, if a problem contains an explicit concept of decisions, and the solution is generated through a series of decisions, then it satisfies the decision tree model and can usually be solved using backtracking. -On this basis, dynamic programming problems also have some "bonus points" for determination. +On this basis, dynamic programming problems also have some positive indicators. - The problem contains descriptions such as maximum (minimum) or most (least), indicating optimization. - The problem's state can be represented using a list, multi-dimensional matrix, or tree, and a state has a recurrence relation with its surrounding states. -Correspondingly, there are also some "penalty points". +Correspondingly, there are also some negative indicators. - The goal of the problem is to find all possible solutions, rather than finding the optimal solution. - The problem description has obvious permutation and combination characteristics, requiring the return of specific multiple solutions. -If a problem satisfies the decision tree model and has relatively obvious "bonus points", we can assume it is a dynamic programming problem and verify it during the solving process. +If a problem satisfies the decision tree model and has relatively obvious positive indicators, we can assume it is a dynamic programming problem and verify that assumption during the solving process. ## Problem-Solving Steps @@ -33,7 +33,7 @@ To illustrate the problem-solving steps more vividly, we use a classic problem " !!! question - Given an $n \times m$ two-dimensional grid `grid`, where each cell in the grid contains a non-negative integer representing the cost of that cell. A robot starts from the top-left cell and can only move down or right at each step until reaching the bottom-right cell. Return the minimum path sum from the top-left to the bottom-right. + Given an $n \times m$ two-dimensional grid `grid` in which each cell contains a non-negative integer representing its cost, a robot starts from the top-left cell and can only move down or right at each step until reaching the bottom-right cell. Return the minimum path sum from the top-left to the bottom-right. The figure below shows an example where the minimum path sum for the given grid is $13$. @@ -57,7 +57,7 @@ From this, we obtain the two-dimensional $dp$ matrix shown in the figure below, **Step 2: Identify the optimal substructure, and then derive the state transition equation** -For state $[i, j]$, it can only be transferred from the cell above $[i-1, j]$ or the cell to the left $[i, j-1]$. Therefore, the optimal substructure is: the minimum path sum to reach $[i, j]$ is determined by the smaller of the minimum path sums of $[i, j-1]$ and $[i-1, j]$. +For state $[i, j]$, it can only transition from the cell above $[i-1, j]$ or the cell to the left $[i, j-1]$. Therefore, the optimal substructure is: the minimum path sum to reach $[i, j]$ is determined by the smaller of the minimum path sums of $[i, j-1]$ and $[i-1, j]$. Based on the above analysis, the state transition equation shown in the figure below can be derived: @@ -77,13 +77,13 @@ $$ In this problem, states in the first row can only come from the state to their left, and states in the first column can only come from the state above them. Therefore, the first row $i = 0$ and first column $j = 0$ are boundary conditions. -As shown in the figure below, since each cell is transferred from the cell to its left and the cell above it, we use loops to traverse the matrix, with the outer loop traversing rows and the inner loop traversing columns. +As shown in the figure below, since each cell transitions from the cell to its left and the cell above it, we use loops to traverse the matrix, with the outer loop traversing rows and the inner loop traversing columns. ![Boundary conditions and state transition order](dp_solution_pipeline.assets/min_path_sum_solution_initial_state.png) !!! note - Boundary conditions in dynamic programming are used to initialize the $dp$ table, and in search are used for pruning. + Boundary conditions in dynamic programming are used to initialize the $dp$ table, while in search they are used for pruning. The core of state transition order is to ensure that when computing the solution to the current problem, all the smaller subproblems it depends on have already been computed correctly. @@ -91,7 +91,7 @@ Based on the above analysis, we can directly write the dynamic programming code. ### Method 1: Brute Force Search -Starting from state $[i, j]$, continuously decompose into smaller states $[i-1, j]$ and $[i, j-1]$. The recursive function includes the following elements. +Starting from state $[i, j]$, we continuously decompose it into smaller states $[i-1, j]$ and $[i, j-1]$. The recursive function includes the following elements. - **Recursive parameters**: state $[i, j]$. - **Return value**: minimum path sum from $[0, 0]$ to $[i, j]$, which is $dp[i, j]$. diff --git a/en/docs/chapter_dynamic_programming/edit_distance_problem.md b/en/docs/chapter_dynamic_programming/edit_distance_problem.md index 57906e4b1..3e16965fc 100644 --- a/en/docs/chapter_dynamic_programming/edit_distance_problem.md +++ b/en/docs/chapter_dynamic_programming/edit_distance_problem.md @@ -12,7 +12,7 @@ As shown in the figure below, transforming `kitten` into `sitting` requires 3 ed ![Example data for edit distance](edit_distance_problem.assets/edit_distance_example.png) -**The edit distance problem can be naturally explained using the decision tree model**. Strings correspond to tree nodes, and a round of decision (one edit operation) corresponds to an edge of the tree. +**The edit distance problem can be naturally explained using the decision tree model**. Strings correspond to tree nodes, and each edit operation corresponds to an edge in the tree. As shown in the figure below, without restricting operations, each node can branch into many edges, with each edge corresponding to one operation, meaning there are many possible paths to transform `hello` into `algo`. @@ -26,7 +26,7 @@ From the perspective of the decision tree, the goal of this problem is to find t Each round of decision involves performing one edit operation on string $s$. -We want the problem scale to gradually decrease during the editing process, which allows us to construct subproblems. Let the lengths of strings $s$ and $t$ be $n$ and $m$ respectively. We first consider the tail characters of the two strings, $s[n-1]$ and $t[m-1]$. +We want the problem size to gradually decrease during the editing process so that we can construct subproblems. Let the lengths of strings $s$ and $t$ be $n$ and $m$ respectively. We first consider the tail characters of the two strings, $s[n-1]$ and $t[m-1]$. - If $s[n-1]$ and $t[m-1]$ are the same, we can skip them and directly consider $s[n-2]$ and $t[m-2]$. - If $s[n-1]$ and $t[m-1]$ are different, we need to perform one edit on $s$ (insert, delete, or replace) to make the tail characters of the two strings the same, allowing us to skip them and consider a smaller-scale problem. @@ -47,7 +47,7 @@ Consider subproblem $dp[i, j]$, where the tail characters of the corresponding t ![State transition for edit distance](edit_distance_problem.assets/edit_distance_state_transfer.png) -Based on the above analysis, the optimal substructure can be obtained: the minimum number of edits for $dp[i, j]$ equals the minimum among the minimum edit steps of $dp[i, j-1]$, $dp[i-1, j]$, and $dp[i-1, j-1]$, plus the edit step $1$ for this time. The corresponding state transition equation is: +Based on the above analysis, we obtain the optimal substructure: the minimum number of edits for $dp[i, j]$ equals the minimum of $dp[i, j-1]$, $dp[i-1, j]$, and $dp[i-1, j-1]$, plus the current edit cost of $1$. The corresponding state transition equation is: $$ dp[i, j] = \min(dp[i, j-1], dp[i-1, j], dp[i-1, j-1]) + 1 @@ -71,7 +71,7 @@ Observing the state transition equation, the solution $dp[i, j]$ depends on solu [file]{edit_distance}-[class]{}-[func]{edit_distance_dp} ``` -As shown in the figure below, the state transition process for the edit distance problem is very similar to the knapsack problem and can both be viewed as the process of filling a two-dimensional grid. +As shown in the figure below, the state transition process for the edit distance problem is very similar to that of the knapsack problem; both can be viewed as the process of filling a two-dimensional grid. === "<1>" ![Dynamic programming process for edit distance](edit_distance_problem.assets/edit_distance_dp_step1.png) @@ -120,9 +120,9 @@ As shown in the figure below, the state transition process for the edit distance ### Space Optimization -Since $dp[i, j]$ is transferred from the solutions above $dp[i-1, j]$, to the left $dp[i, j-1]$, and to the upper-left $dp[i-1, j-1]$, forward traversal will lose the upper-left solution $dp[i-1, j-1]$, and reverse traversal cannot build $dp[i, j-1]$ in advance, so neither traversal order is feasible. +Since $dp[i, j]$ depends on the states above $dp[i-1, j]$, to the left $dp[i, j-1]$, and at the upper-left $dp[i-1, j-1]$, forward traversal will lose the upper-left state $dp[i-1, j-1]$, while reverse traversal cannot construct $dp[i, j-1]$ in advance, so neither traversal order is suitable. -For this reason, we can use a variable `leftup` to temporarily store the upper-left solution $dp[i-1, j-1]$, so we only need to consider the solutions to the left and above. This situation is the same as the unbounded knapsack problem, allowing for forward traversal. The code is as follows: +For this reason, we can use a variable `leftup` to temporarily store the upper-left solution $dp[i-1, j-1]$, so we only need to consider the solutions to the left and above. This situation is the same as in the unbounded knapsack problem, so we can use forward traversal. The code is as follows: ```src [file]{edit_distance}-[class]{}-[func]{edit_distance_dp_comp} diff --git a/en/docs/chapter_dynamic_programming/index.md b/en/docs/chapter_dynamic_programming/index.md index 44adcd642..1e62e80f2 100644 --- a/en/docs/chapter_dynamic_programming/index.md +++ b/en/docs/chapter_dynamic_programming/index.md @@ -4,6 +4,6 @@ !!! abstract - Streams converge into rivers, rivers converge into the sea. + Streams flow into rivers, rivers flow into the sea. - Dynamic programming gathers solutions to small problems into answers to large problems, step by step guiding us to the shore of problem-solving. + Dynamic programming combines solutions to small problems into the answer to a large problem, leading us step by step to the other shore of problem-solving. diff --git a/en/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md b/en/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md index 4435348cf..fc855daac 100644 --- a/en/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md +++ b/en/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md @@ -12,7 +12,7 @@ As shown in the figure below, for a $3$-step staircase, there are $3$ different ![Number of ways to reach the 3rd step](intro_to_dynamic_programming.assets/climbing_stairs_example.png) -The goal of this problem is to find the number of ways, **we can consider using backtracking to enumerate all possibilities**. Specifically, imagine climbing stairs as a multi-round selection process: starting from the ground, choosing to go up $1$ or $2$ steps in each round, incrementing the count by $1$ whenever the top of the stairs is reached, and pruning when exceeding the top. The code is as follows: +The goal of this problem is to determine the number of ways, so **we can consider using backtracking to enumerate all possibilities**. Specifically, imagine climbing stairs as a multi-round selection process: starting from the ground, choosing to go up $1$ or $2$ steps in each round, incrementing the count by $1$ whenever the top of the stairs is reached, and pruning when exceeding the top. The code is as follows: ```src [file]{climbing_stairs_backtrack}-[class]{}-[func]{climbing_stairs_backtrack} @@ -36,19 +36,19 @@ $$ dp[i] = dp[i-1] + dp[i-2] $$ -This means that in the stair climbing problem, there exists a recurrence relation among the subproblems, **the solution to the original problem can be constructed from the solutions to the subproblems**. The figure below illustrates this recurrence relation. +This means that in the stair climbing problem, there exists a recurrence relation among the subproblems, and **the solution to the original problem can be constructed from the solutions to the subproblems**. The figure below illustrates this recurrence relation. ![Recurrence relation for the number of ways](intro_to_dynamic_programming.assets/climbing_stairs_state_transfer.png) We can obtain a brute force search solution based on the recurrence formula. Starting from $dp[n]$, **recursively decompose a larger problem into the sum of two smaller problems**, until reaching the smallest subproblems $dp[1]$ and $dp[2]$ and returning. Among them, the solutions to the smallest subproblems are known, namely $dp[1] = 1$ and $dp[2] = 2$, representing $1$ and $2$ ways to climb to the $1$st and $2$nd steps, respectively. -Observe the following code, which, like standard backtracking code, belongs to depth-first search but is more concise: +Observe the following code: like standard backtracking code, it also uses depth-first search but is more concise: ```src [file]{climbing_stairs_dfs}-[class]{}-[func]{climbing_stairs_dfs} ``` -The figure below shows the recursion tree formed by brute force search. For the problem $dp[n]$, the depth of its recursion tree is $n$, with a time complexity of $O(2^n)$. Exponential order represents explosive growth; if we input a relatively large $n$, we will fall into a long wait. +The figure below shows the recursion tree formed by brute force search. For the problem $dp[n]$, the depth of its recursion tree is $n$, with a time complexity of $O(2^n)$. Exponential growth is explosive; if we input a relatively large $n$, the wait can be very long. ![Recursion tree for climbing stairs](intro_to_dynamic_programming.assets/climbing_stairs_dfs_tree.png) @@ -69,7 +69,7 @@ The code is as follows: [file]{climbing_stairs_dfs_mem}-[class]{}-[func]{climbing_stairs_dfs_mem} ``` -Observe the figure below, **after memoization, all overlapping subproblems only need to be computed once, optimizing the time complexity to $O(n)$**, which is a tremendous leap. +Observe the figure below: **after memoization, all overlapping subproblems need to be computed only once, reducing the time complexity to $O(n)$**, which is a tremendous leap. ![Recursion tree with memoization](intro_to_dynamic_programming.assets/climbing_stairs_dfs_memo_tree.png) @@ -99,12 +99,12 @@ Based on the above content, we can summarize the commonly used terminology in dy ## Space Optimization -Observant readers may have noticed that **since $dp[i]$ is only related to $dp[i-1]$ and $dp[i-2]$, we do not need to use an array `dp` to store the solutions to all subproblems**, but can simply use two variables to roll forward. The code is as follows: +Observant readers may have noticed that **since $dp[i]$ is only related to $dp[i-1]$ and $dp[i-2]$, we do not need to use an array `dp` to store the solutions to all subproblems**, and can instead use two variables that roll forward. The code is as follows: ```src [file]{climbing_stairs_dp}-[class]{}-[func]{climbing_stairs_dp_comp} ``` -Observing the above code, since the space occupied by the array `dp` is saved, the space complexity is reduced from $O(n)$ to $O(1)$. +As the above code shows, by eliminating the space occupied by the array `dp`, the space complexity is reduced from $O(n)$ to $O(1)$. In dynamic programming problems, the current state often depends only on a limited number of preceding states, allowing us to retain only the necessary states and save memory space through "dimension reduction". **This space optimization technique is called "rolling variable" or "rolling array"**. diff --git a/en/docs/chapter_dynamic_programming/knapsack_problem.md b/en/docs/chapter_dynamic_programming/knapsack_problem.md index 77ff86a84..d8a26a873 100644 --- a/en/docs/chapter_dynamic_programming/knapsack_problem.md +++ b/en/docs/chapter_dynamic_programming/knapsack_problem.md @@ -6,7 +6,7 @@ In this section, we will first solve the most common 0-1 knapsack problem. !!! question - Given $n$ items, where the weight of the $i$-th item is $wgt[i-1]$ and its value is $val[i-1]$, and a knapsack with capacity $cap$. Each item can only be selected once. What is the maximum value that can be placed in the knapsack within the capacity limit? + Given $n$ items and a knapsack with capacity $cap$, where the weight and value of the $i$-th item are $wgt[i-1]$ and $val[i-1]$, respectively. Each item can be selected at most once. What is the maximum value that can fit in the knapsack under the capacity limit? Observe the figure below. Since item number $i$ starts counting from $1$ and array indices start from $0$, item $i$ corresponds to weight $wgt[i-1]$ and value $val[i-1]$. @@ -31,7 +31,7 @@ After making the decision for item $i$, what remains is the subproblem of the fi - **Not putting item $i$**: The knapsack capacity remains unchanged, and the state changes to $[i-1, c]$. - **Putting item $i$**: The knapsack capacity decreases by $wgt[i-1]$, the value increases by $val[i-1]$, and the state changes to $[i-1, c-wgt[i-1]]$. -The above analysis reveals the optimal substructure of this problem: **the maximum value $dp[i, c]$ equals the larger value between not putting item $i$ and putting item $i$**. From this, the state transition equation can be derived: +The above analysis reveals the optimal substructure of this problem: **the maximum value $dp[i, c]$ equals the greater of the values obtained by not putting item $i$ into the knapsack and by putting it into the knapsack**. From this, the state transition equation can be derived: $$ dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1]) @@ -43,7 +43,7 @@ Note that if the weight of the current item $wgt[i - 1]$ exceeds the remaining k When there are no items or the knapsack capacity is $0$, the maximum value is $0$, i.e., the first column $dp[i, 0]$ and the first row $dp[0, c]$ are both equal to $0$. -The current state $[i, c]$ is transferred from the state above $[i-1, c]$ and the state in the upper-left $[i-1, c-wgt[i-1]]$, so the entire $dp$ table is traversed in order through two nested loops. +The current state $[i, c]$ transitions from the state above $[i-1, c]$ and the upper-left state $[i-1, c-wgt[i-1]]$, so we can traverse the entire $dp$ table in forward order using two nested loops. Based on the above analysis, we will next implement the brute force search, memoization, and dynamic programming solutions in order. @@ -53,14 +53,14 @@ The search code includes the following elements. - **Recursive parameters**: state $[i, c]$. - **Return value**: solution to the subproblem $dp[i, c]$. -- **Termination condition**: when the item number is out of bounds $i = 0$ or the remaining knapsack capacity is $0$, terminate recursion and return value $0$. +- **Termination condition**: when there are no items left ($i = 0$) or the remaining knapsack capacity is $0$, terminate the recursion and return value $0$. - **Pruning**: if the weight of the current item exceeds the remaining knapsack capacity, only the option of not putting it in is available. ```src [file]{knapsack}-[class]{}-[func]{knapsack_dfs} ``` -As shown in the figure below, since each item generates two search branches of not selecting and selecting, the time complexity is $O(2^n)$. +As shown in the figure below, since each item generates two search branches, excluding it and including it, the time complexity is $O(2^n)$. Observing the recursion tree, it is easy to see overlapping subproblems, such as $dp[1, 10]$. When there are many items, large knapsack capacity, and especially many items with the same weight, the number of overlapping subproblems will increase significantly. diff --git a/en/docs/chapter_dynamic_programming/summary.md b/en/docs/chapter_dynamic_programming/summary.md index f0683d734..f7b1b3968 100644 --- a/en/docs/chapter_dynamic_programming/summary.md +++ b/en/docs/chapter_dynamic_programming/summary.md @@ -1,6 +1,6 @@ # Summary -### Key Review +### Key Points - Dynamic programming decomposes problems and avoids redundant computation by storing the solutions to subproblems, thereby significantly improving computational efficiency. - Without considering time constraints, all dynamic programming problems can be solved using backtracking (brute force search), but the recursion tree contains a large number of overlapping subproblems, resulting in extremely low efficiency. By introducing a memo list, we can store the solutions to all computed subproblems, ensuring that overlapping subproblems are only computed once. @@ -8,12 +8,12 @@ - Subproblem decomposition is a general algorithmic approach, with different properties in divide and conquer, dynamic programming, and backtracking. - Dynamic programming problems have three major characteristics: overlapping subproblems, optimal substructure, and no aftereffects. - If the optimal solution to the original problem can be constructed from the optimal solutions to the subproblems, then it has optimal substructure. -- No aftereffects means that for a given state, its future development is only related to that state and has nothing to do with all past states. Many combinatorial optimization problems do not have no aftereffects and cannot be quickly solved using dynamic programming. +- No aftereffects means that for a given state, its future development is only related to that state and has nothing to do with all past states. Many combinatorial optimization problems do not satisfy this property and cannot be solved efficiently using dynamic programming. **Knapsack problem** - The knapsack problem is one of the most typical dynamic programming problems, with variants such as the 0-1 knapsack, unbounded knapsack, and multiple knapsack. -- The state definition for the 0-1 knapsack is the maximum value among the first $i$ items in a knapsack of capacity $c$. Based on the two decisions of not putting an item in the knapsack and putting it in, the optimal substructure can be identified and the state transition equation constructed. In space optimization, since each state depends on the state directly above and to the upper-left, the list needs to be traversed in reverse order to avoid overwriting the upper-left state. +- The state definition for the 0-1 knapsack is the maximum value achievable using the first $i$ items with a knapsack capacity of $c$. Based on the two decisions of not putting an item in the knapsack and putting it in, the optimal substructure can be identified and the state transition equation constructed. In space optimization, since each state depends on the state directly above and to the upper-left, the list needs to be traversed in reverse order to avoid overwriting the upper-left state. - The unbounded knapsack problem has no limit on the selection quantity of each type of item, so the state transition for choosing to put in an item differs from the 0-1 knapsack problem. Since the state depends on the state directly above and directly to the left, space optimization should use forward traversal. - The coin change problem is a variant of the unbounded knapsack problem. It changes from seeking the "maximum" value to seeking the "minimum" number of coins, so $\max()$ in the state transition equation should be changed to $\min()$. It changes from seeking "not exceeding" the knapsack capacity to seeking "exactly" making up the target amount, so $amt + 1$ is used to represent the invalid solution of "unable to make up the target amount". - Coin change problem II changes from seeking the "minimum number of coins" to seeking the "number of coin combinations", so the state transition equation correspondingly changes from $\min()$ to a summation operator. diff --git a/en/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md b/en/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md index 2b8afa524..1243fe9da 100644 --- a/en/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md +++ b/en/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md @@ -95,7 +95,7 @@ The two-dimensional $dp$ table has size $(n+1) \times (amt+1)$. This problem differs from the unbounded knapsack problem in the following two aspects regarding the state transition equation. - This problem seeks the minimum value, so the operator $\max()$ needs to be changed to $\min()$. -- The optimization target is the number of coins rather than item value, so when a coin is selected, simply execute $+1$. +- The optimization target is the number of coins rather than item value, so when a coin is selected, simply add $1$. $$ dp[i, a] = \min(dp[i-1, a], dp[i, a - coins[i-1]] + 1) @@ -109,7 +109,7 @@ When there are no coins, **it is impossible to make up any amount $> 0$**, which ### Code Implementation -Most programming languages do not provide a $+ \infty$ variable, and can only use the maximum value of integer type `int` as a substitute. However, this can lead to large number overflow: the $+ 1$ operation in the state transition equation may cause overflow. +Most programming languages do not provide a $+ \infty$ variable, and can only use the maximum value of integer type `int` as a substitute. However, this can lead to integer overflow: the $+ 1$ operation in the state transition equation may cause overflow. For this reason, we use the number $amt + 1$ to represent invalid solutions, because the maximum number of coins needed to make up $amt$ is at most $amt$. Before returning, check whether $dp[n, amt]$ equals $amt + 1$; if so, return $-1$, indicating that the target amount cannot be made up. The code is as follows: @@ -172,7 +172,7 @@ The space optimization for the coin change problem is handled in the same way as [file]{coin_change}-[class]{}-[func]{coin_change_dp_comp} ``` -## Coin Change Problem Ii +## Coin Change Problem II !!! question diff --git a/en/docs/chapter_graph/graph.md b/en/docs/chapter_graph/graph.md index 51cf1d3f1..a305f2dc5 100644 --- a/en/docs/chapter_graph/graph.md +++ b/en/docs/chapter_graph/graph.md @@ -10,7 +10,7 @@ G & = \{ V, E \} \newline \end{aligned} $$ -If we view vertices as nodes and edges as references (pointers) connecting the nodes, we can see graphs as a data structure extended from linked lists. As shown in the figure below, **compared to linear relationships (linked lists) and divide-and-conquer relationships (trees), network relationships (graphs) have a higher degree of freedom and are therefore more complex**. +If we view vertices as nodes and edges as references (pointers) connecting them, we can regard a graph as an extension of the linked list data structure. As shown in the figure below, **compared to linear relationships (linked lists) and divide-and-conquer relationships (trees), network relationships (graphs) have a higher degree of freedom and are therefore more complex**. ![Relationships among linked lists, trees, and graphs](graph.assets/linkedlist_tree_graph.png) @@ -18,8 +18,8 @@ If we view vertices as nodes and edges as references (pointers) connecting the n Graphs can be divided into undirected graphs and directed graphs based on whether edges have direction, as shown in the figure below. -- In undirected graphs, edges represent a "bidirectional" connection between two vertices, such as the "friend relationship" on WeChat or QQ. -- In directed graphs, edges have directionality, meaning edges $A \rightarrow B$ and $A \leftarrow B$ are independent of each other, such as the "follow" and "be followed" relationships on Weibo or TikTok. +- In undirected graphs, edges represent a "bidirectional" connection between two vertices, such as friendships on WeChat or QQ. +- In directed graphs, edges have directionality, meaning edges $A \rightarrow B$ and $A \leftarrow B$ are independent of each other, such as following and follower relationships on Weibo or TikTok. ![Directed and undirected graphs](graph.assets/directed_graph.png) @@ -30,7 +30,7 @@ Graphs can be divided into connected graphs and disconnected graphsweighted graphs as shown in the figure below. For example, in mobile games like "Honor of Kings", the system calculates the "intimacy" between players based on their shared game time, and such intimacy networks can be represented using weighted graphs. +We can also add a "weight" variable to edges, resulting in weighted graphs as shown in the figure below. For example, in mobile games like "Honor of Kings", the system calculates the "intimacy" between players based on how long they have played together, and such intimacy networks can be represented using weighted graphs. ![Weighted and unweighted graphs](graph.assets/weighted_graph.png) @@ -38,7 +38,7 @@ Graph data structures include the following commonly used terms. - Adjacency: When two vertices are connected by an edge, these two vertices are said to be "adjacent". In the figure above, the adjacent vertices of vertex 1 are vertices 2, 3, and 5. - Path: The sequence of edges from vertex A to vertex B is called a "path" from A to B. In the figure above, the edge sequence 1-5-2-4 is a path from vertex 1 to vertex 4. -- Degree: The number of edges a vertex has. For directed graphs, in-degree indicates how many edges point to the vertex, and out-degree indicates how many edges point out from the vertex. +- Degree: The number of edges a vertex has. For directed graphs, in-degree indicates how many edges point to the vertex, and out-degree indicates how many edges leave the vertex. ## Representation of Graphs @@ -56,7 +56,7 @@ Adjacency matrices have the following properties. - In simple graphs, vertices cannot connect to themselves, so the elements on the main diagonal of the adjacency matrix are meaningless. - For undirected graphs, edges in both directions are equivalent, so the adjacency matrix is symmetric about the main diagonal. -- Replacing the elements of the adjacency matrix from $1$ and $0$ to weights allows representation of weighted graphs. +- Replacing the $1$ and $0$ entries in the adjacency matrix with weights allows it to represent weighted graphs. When using adjacency matrices to represent graphs, we can directly access matrix elements to obtain edges, resulting in highly efficient addition, deletion, lookup, and modification operations, all with a time complexity of $O(1)$. However, the space complexity of the matrix is $O(n^2)$, which consumes significant memory. @@ -66,9 +66,9 @@ An adjacency list uses $n$ linked lists to represent a graph, with linked ![Adjacency list representation of a graph](graph.assets/adjacency_list.png) -Adjacency lists only store edges that actually exist, and the total number of edges is typically much less than $n^2$, making them more space-efficient. However, finding edges in an adjacency list requires traversing the linked list, so its time efficiency is inferior to that of adjacency matrices. +Adjacency lists only store edges that actually exist, and the total number of edges is typically much less than $n^2$, making them more space-efficient. However, finding edges in an adjacency list requires traversing the linked list, so it is less time-efficient than an adjacency matrix. -Observing the figure above, **the structure of adjacency lists is very similar to "chaining" in hash tables, so we can adopt similar methods to optimize efficiency**. For example, when linked lists are long, they can be converted to AVL trees or red-black trees, thereby optimizing time efficiency from $O(n)$ to $O(\log n)$; linked lists can also be converted to hash tables, thereby reducing time complexity to $O(1)$. +As shown in the figure above, **the structure of adjacency lists is very similar to separate chaining in hash tables, so we can use similar methods to improve efficiency**. For example, when a linked list becomes long, it can be converted into an AVL tree or red-black tree, improving the time complexity from $O(n)$ to $O(\log n)$; it can also be converted into a hash table, reducing the time complexity to $O(1)$. ## Common Applications of Graphs diff --git a/en/docs/chapter_graph/graph_operations.md b/en/docs/chapter_graph/graph_operations.md index 61cf5b536..ee6615909 100644 --- a/en/docs/chapter_graph/graph_operations.md +++ b/en/docs/chapter_graph/graph_operations.md @@ -1,6 +1,6 @@ # Basic Operations on Graphs -Basic operations on graphs can be divided into operations on "edges" and operations on "vertices". Under the two representation methods of "adjacency matrix" and "adjacency list", the implementation methods differ. +Basic operations on graphs can be divided into operations on "edges" and operations on "vertices". Their implementations differ depending on whether the graph is represented as an "adjacency matrix" or an "adjacency list". ## Implementation Based on Adjacency Matrix @@ -9,7 +9,7 @@ Given an undirected graph with $n$ vertices, the various operations are implemen - **Adding or removing an edge**: Directly modify the specified edge in the adjacency matrix, using $O(1)$ time. Since it is an undirected graph, both directions of the edge need to be updated simultaneously. - **Adding a vertex**: Add a row and a column at the end of the adjacency matrix and fill them all with $0$s, using $O(n)$ time. - **Removing a vertex**: Delete a row and a column in the adjacency matrix. The worst case occurs when removing the first row and column, requiring $(n-1)^2$ elements to be "moved up and to the left", thus using $O(n^2)$ time. -- **Initialization**: Pass in $n$ vertices, initialize a vertex list `vertices` of length $n$, using $O(n)$ time; initialize an adjacency matrix `adjMat` of size $n \times n$, using $O(n^2)$ time. +- **Initialization**: Given $n$ vertices, initialize a vertex list `vertices` of length $n$, using $O(n)$ time; initialize an adjacency matrix `adjMat` of size $n \times n$, using $O(n^2)$ time. === "<1>" ![Initialization, adding and removing edges, adding and removing vertices in adjacency matrix](graph_operations.assets/adjacency_matrix_step1_initialization.png) @@ -38,7 +38,7 @@ Given an undirected graph with a total of $n$ vertices and $m$ edges, the variou - **Adding an edge**: Add the edge at the end of the corresponding vertex's linked list, using $O(1)$ time. Since it is an undirected graph, edges in both directions need to be added simultaneously. - **Removing an edge**: Find and remove the specified edge in the corresponding vertex's linked list, using $O(m)$ time. In an undirected graph, edges in both directions need to be removed simultaneously. -- **Adding a vertex**: Add a linked list in the adjacency list and set the new vertex as the head node of the list, using $O(1)$ time. +- **Adding a vertex**: Add a linked list to the adjacency list, with the new vertex as the head node, using $O(1)$ time. - **Removing a vertex**: Traverse the entire adjacency list and remove all edges containing the specified vertex, using $O(n + m)$ time. - **Initialization**: Create $n$ vertices and $2m$ edges in the adjacency list, using $O(n + m)$ time. @@ -57,12 +57,12 @@ Given an undirected graph with a total of $n$ vertices and $m$ edges, the variou === "<5>" ![adjacency_list_remove_vertex](graph_operations.assets/adjacency_list_step5_remove_vertex.png) -The following is the adjacency list code implementation. Compared to the figure above, the actual code has the following differences. +The following code shows the adjacency list implementation. Compared with the figure above, the actual code differs in the following ways. - For convenience in adding and removing vertices, and to simplify the code, we use lists (dynamic arrays) instead of linked lists. - A hash table is used to store the adjacency list, where `key` is the vertex instance and `value` is the list (linked list) of adjacent vertices for that vertex. -Additionally, we use the `Vertex` class to represent vertices in the adjacency list. The reason for this is: if we used list indices to distinguish different vertices as with adjacency matrices, then to delete the vertex at index $i$, we would need to traverse the entire adjacency list and decrement all indices greater than $i$ by $1$, which is very inefficient. However, if each vertex is a unique `Vertex` instance, deleting a vertex does not require modifying other vertices. +Additionally, we use the `Vertex` class to represent vertices in the adjacency list for the following reason: if we used list indices to distinguish different vertices, as with adjacency matrices, then to delete the vertex at index $i$, we would need to traverse the entire adjacency list and decrement all indices greater than $i$ by $1$, which is very inefficient. However, if each vertex is a unique `Vertex` instance, deleting one vertex does not require modifying the others. ```src [file]{graph_adjacency_list}-[class]{graph_adj_list}-[func]{} @@ -70,7 +70,7 @@ Additionally, we use the `Vertex` class to represent vertices in the adjacency l ## Efficiency Comparison -Assuming the graph has $n$ vertices and $m$ edges, the table below compares the time efficiency and space efficiency of adjacency matrices and adjacency lists. Note that the adjacency list (linked list) corresponds to the implementation in this text, while the adjacency list (hash table) refers specifically to the implementation where all linked lists are replaced with hash tables. +Assuming the graph has $n$ vertices and $m$ edges, the table below compares the time efficiency and space efficiency of adjacency matrices and adjacency lists. Note that the adjacency list (linked list) corresponds to the implementation used in this section, while the adjacency list (hash table) refers specifically to the implementation where all linked lists are replaced with hash tables.

Table   Comparison of adjacency matrix and adjacency list

diff --git a/en/docs/chapter_graph/graph_traversal.md b/en/docs/chapter_graph/graph_traversal.md index ebed38f6d..1e89caf96 100644 --- a/en/docs/chapter_graph/graph_traversal.md +++ b/en/docs/chapter_graph/graph_traversal.md @@ -6,7 +6,7 @@ Both graphs and trees require the application of search algorithms to implement ## Breadth-First Search -**Breadth-first search is a near-to-far traversal method that, starting from a certain node, always prioritizes visiting the nearest vertices and expands outward layer by layer**. As shown in the figure below, starting from the top-left vertex, first traverse all adjacent vertices of that vertex, then traverse all adjacent vertices of the next vertex, and so on, until all vertices have been visited. +**Breadth-first search proceeds from near to far: starting from a given node, it always visits the nearest vertices first and expands outward layer by layer**. As shown in the figure below, starting from the top-left vertex, first traverse all adjacent vertices of that vertex, then traverse all adjacent vertices of the next vertex, and so on, until all vertices have been visited. ![Breadth-first search of a graph](graph_traversal.assets/graph_bfs.png) @@ -22,7 +22,7 @@ To prevent revisiting vertices, we use a hash set `visited` to record which node !!! tip - A hash set can be viewed as a hash table that stores only `key` without storing `value`. It can perform addition, deletion, lookup, and modification operations on `key` in $O(1)$ time complexity. Based on the uniqueness of `key`, hash sets are typically used for data deduplication and similar scenarios. + A hash set can be viewed as a hash table that stores only `key` without storing `value`. It supports insertion, deletion, lookup, and update operations on `key` in $O(1)$ time. Based on the uniqueness of `key`, hash sets are typically used for data deduplication and similar scenarios. ```src [file]{graph_bfs}-[class]{}-[func]{graph_bfs} @@ -90,9 +90,9 @@ This "go as far as possible then return" algorithm paradigm is typically impleme The algorithm flow of depth-first search is shown in the figure below. - **Straight dashed lines represent downward recursion**, indicating that a new recursive method has been initiated to visit a new vertex. -- **Curved dashed lines represent upward backtracking**, indicating that this recursive method has returned to the position where it was initiated. +- **Curved dashed lines represent upward backtracking**, indicating that this recursive call has returned to the point where it was made. -To deepen understanding, it is recommended to combine the figure below with the code to mentally simulate (or draw out) the entire DFS process, including when each recursive method is initiated and when it returns. +To deepen understanding, it is recommended to combine the figure below with the code to mentally simulate (or draw out) the entire DFS process, including when each recursive call begins and when it returns. === "<1>" ![Steps of depth-first search of a graph](graph_traversal.assets/graph_dfs_step1.png) @@ -129,7 +129,7 @@ To deepen understanding, it is recommended to combine the figure below with the !!! question "Is the depth-first traversal sequence unique?" - Similar to breadth-first search, the order of depth-first traversal sequences is also not unique. Given a certain vertex, exploring in any direction first is valid, meaning the order of adjacent vertices can be arbitrarily shuffled, all being depth-first search. + Similar to breadth-first search, depth-first traversal sequences are also not unique. Given a vertex, any exploration direction may be chosen first; that is, the order of adjacent vertices can be arbitrarily rearranged and still constitute depth-first search. Taking tree traversal as an example, "root $\rightarrow$ left $\rightarrow$ right", "left $\rightarrow$ root $\rightarrow$ right", and "left $\rightarrow$ right $\rightarrow$ root" correspond to pre-order, in-order, and post-order traversals, respectively. They represent three different traversal priorities, yet all three belong to depth-first search. diff --git a/en/docs/chapter_graph/summary.md b/en/docs/chapter_graph/summary.md index e28307f89..980b9fb48 100644 --- a/en/docs/chapter_graph/summary.md +++ b/en/docs/chapter_graph/summary.md @@ -3,16 +3,16 @@ ### Key Review - Graphs consist of vertices and edges and can be represented as a set of vertices and a set of edges. -- Compared to linear relationships (linked lists) and divide-and-conquer relationships (trees), network relationships (graphs) have a higher degree of freedom and are therefore more complex. -- Directed graphs have edges with directionality, connected graphs have all vertices reachable from any vertex, and weighted graphs have edges that each contain a weight variable. +- Compared with the linear relationships modeled by linked lists and the divide-and-conquer relationships modeled by trees, the network relationships modeled by graphs offer much greater flexibility and are therefore more complex. +- In directed graphs, edges have direction; in connected graphs, every vertex is reachable from any other vertex; and in weighted graphs, each edge carries a weight. - Adjacency matrices use matrices to represent graphs, where each row (column) represents a vertex, and matrix elements represent edges, using $1$ or $0$ to indicate whether two vertices have an edge or not. Adjacency matrices are highly efficient for addition, deletion, lookup, and modification operations, but consume significant space. -- Adjacency lists use multiple linked lists to represent graphs, where the $i$-th linked list corresponds to vertex $i$ and stores all adjacent vertices of that vertex. Adjacency lists are more space-efficient than adjacency matrices, but have lower time efficiency because they require traversing linked lists to find edges. +- Adjacency lists use multiple linked lists to represent a graph: the $i$-th linked list corresponds to vertex $i$ and stores all vertices adjacent to it. Compared with adjacency matrices, adjacency lists use less space, but edge lookups are less efficient because the linked list must be traversed. - When linked lists in adjacency lists become too long, they can be converted to red-black trees or hash tables, thereby improving lookup efficiency. - From an algorithmic perspective, adjacency matrices embody "trading space for time", while adjacency lists embody "trading time for space". - Graphs can be used to model various real-world systems, such as social networks and subway lines. - Trees are a special case of graphs, and tree traversal is a special case of graph traversal. -- Breadth-first search of graphs is a near-to-far, layer-by-layer expansion search method, typically implemented using a queue. -- Depth-first search of graphs is a search method that prioritizes going as far as possible and backtracks when no path remains, commonly implemented using recursion. +- Breadth-first search in graphs explores from near to far, expanding layer by layer, and is typically implemented with a queue. +- Depth-first search in graphs follows a path as deep as possible and backtracks when it can go no farther, and is commonly implemented with recursion. ### Q & A @@ -24,8 +24,8 @@ In this text, a path is viewed as a sequence of edges, not a sequence of vertice **Q**: In a disconnected graph, will there be unreachable vertices? -In a disconnected graph, starting from a certain vertex, at least one vertex cannot be reached. Traversing a disconnected graph requires setting multiple starting points to traverse all connected components of the graph. +In a disconnected graph, if you start from one vertex, at least one other vertex will be unreachable. To traverse a disconnected graph, you need multiple starting points so that all connected components are covered. -**Q**: In an adjacency list, is there a requirement for the order of "all vertices connected to that vertex"? +**Q**: In an adjacency list, is there any required ordering for the vertices adjacent to a given vertex? -It can be in any order. However, in practical applications, it may be necessary to sort according to specified rules, such as the order in which vertices were added, or the order of vertex values, which helps quickly find vertices "with certain extreme values". +They can appear in any order. In practice, however, they may need to be sorted according to specific rules, such as the order in which vertices were added or the order of vertex values, which helps when quickly finding a vertex with some extreme value. diff --git a/en/docs/chapter_greedy/fractional_knapsack_problem.md b/en/docs/chapter_greedy/fractional_knapsack_problem.md index 1004b6dcb..5f267f3f8 100644 --- a/en/docs/chapter_greedy/fractional_knapsack_problem.md +++ b/en/docs/chapter_greedy/fractional_knapsack_problem.md @@ -2,13 +2,13 @@ !!! question - Given $n$ items, where the weight of the $i$-th item is $wgt[i-1]$ and its value is $val[i-1]$, and a knapsack with capacity $cap$. Each item can be selected only once, **but a portion of an item can be selected, with the value calculated based on the proportion of weight selected**, what is the maximum value of items in the knapsack under the limited capacity? An example is shown in the figure below. + Given $n$ items, where the weight of the $i$-th item is $wgt[i-1]$ and its value is $val[i-1]$, and a knapsack with capacity $cap$. Each item can be selected only once, **but a fraction of an item may be selected, with its value proportional to the selected weight**. What is the maximum total value that can be placed in the knapsack under the capacity constraint? An example is shown in the figure below. ![Example data for the fractional knapsack problem](fractional_knapsack_problem.assets/fractional_knapsack_example.png) The fractional knapsack problem is very similar overall to the 0-1 knapsack problem, with states including the current item $i$ and capacity $c$, and the goal being to maximize value under the limited knapsack capacity. -The difference is that this problem allows selecting only a portion of an item. As shown in the figure below, **we can arbitrarily split items and calculate the corresponding value based on the weight proportion**. +The difference is that this problem allows selecting only a fraction of an item. As shown in the figure below, **we can split an item arbitrarily and compute its value in proportion to the selected weight**. 1. For item $i$, its value per unit weight is $val[i-1] / wgt[i-1]$, referred to as unit value. 2. Suppose we put a portion of item $i$ with weight $w$ into the knapsack, then the value added to the knapsack is $w \times val[i-1] / wgt[i-1]$. @@ -17,7 +17,7 @@ The difference is that this problem allows selecting only a portion of an item. ### Greedy Strategy Determination -Maximizing the total value of items in the knapsack **is essentially maximizing the value per unit weight of items**. From this, we can derive the greedy strategy shown in the figure below. +Maximizing the total value in the knapsack **essentially means prioritizing items with higher value per unit weight**. From this observation, we can derive the greedy strategy shown in the figure below. 1. Sort items by unit value from high to low. 2. Iterate through all items, **greedily selecting the item with the highest unit value in each round**. @@ -27,13 +27,13 @@ Maximizing the total value of items in the knapsack **is essentially maximizing ### Code Implementation -We created an `Item` class to facilitate sorting items by unit value. We loop to make greedy selections, breaking when the knapsack is full and returning the solution: +We define an `Item` class so that items can be sorted by unit value. We then iterate through the sorted items greedily, stopping once the knapsack is full and returning the result: ```src [file]{fractional_knapsack}-[class]{}-[func]{fractional_knapsack} ``` -The time complexity of built-in sorting algorithms is usually $O(\log n)$, and the space complexity is usually $O(\log n)$ or $O(n)$, depending on the specific implementation of the programming language. +Built-in sorting algorithms usually take $O(n \log n)$ time, and their space complexity is usually $O(\log n)$ or $O(n)$, depending on the specific implementation of the programming language. Apart from sorting, in the worst case the entire item list needs to be traversed, **therefore the time complexity is $O(n)$**, where $n$ is the number of items. @@ -41,12 +41,12 @@ Since an `Item` object list is initialized, **the space complexity is $O(n)$**. ### Correctness Proof -Using proof by contradiction. Suppose item $x$ has the highest unit value, and some algorithm yields a maximum value of `res`, but this solution does not include item $x$. +We use proof by contradiction. Suppose item $x$ has the highest unit value, and some algorithm produces an optimal value `res`, but the resulting solution does not include item $x$. -Now remove a unit weight of any item from the knapsack and replace it with a unit weight of item $x$. Since item $x$ has the highest unit value, the total value after replacement will definitely be greater than `res`. **This contradicts the assumption that `res` is the optimal solution, proving that the optimal solution must include item $x$**. +Now remove one unit of weight from any item in the knapsack and replace it with one unit of weight from item $x$. Since item $x$ has the highest unit value, the total value after the replacement must be greater than `res`. **This contradicts the assumption that `res` is optimal, proving that any optimal solution must include item $x$**. -For other items in this solution, we can also construct the above contradiction. In summary, **items with greater unit value are always better choices**, which proves that the greedy strategy is effective. +We can construct the same contradiction for the other items in the solution as well. In summary, **items with higher unit value are always the better choice**, which proves that the greedy strategy is effective. -As shown in the figure below, if we view item weight and item unit value as the horizontal and vertical axes of a two-dimensional chart respectively, then the fractional knapsack problem can be transformed into "finding the maximum area enclosed within a limited horizontal axis range". This analogy can help us understand the effectiveness of the greedy strategy from a geometric perspective. +As shown in the figure below, if we treat item weight and unit value as the horizontal and vertical axes of a two-dimensional chart, then the fractional knapsack problem can be viewed as "finding the maximum area enclosed within a bounded interval on the horizontal axis." This analogy helps explain the effectiveness of the greedy strategy from a geometric perspective. ![Geometric representation of the fractional knapsack problem](fractional_knapsack_problem.assets/fractional_knapsack_area_chart.png) diff --git a/en/docs/chapter_greedy/greedy_algorithm.md b/en/docs/chapter_greedy/greedy_algorithm.md index 545a2fb31..e8c9d04ba 100644 --- a/en/docs/chapter_greedy/greedy_algorithm.md +++ b/en/docs/chapter_greedy/greedy_algorithm.md @@ -1,19 +1,19 @@ # Greedy Algorithm -Greedy algorithm is a common algorithm for solving optimization problems. Its basic idea is to make the seemingly best choice at each decision stage of the problem, that is, to greedily make locally optimal decisions in hopes of obtaining a globally optimal solution. Greedy algorithms are simple and efficient, and are widely applied in many practical problems. +Greedy algorithm is a common approach to solving optimization problems. Its basic idea is to choose the option that appears best at each decision stage, that is, to greedily make locally optimal decisions in the hope of obtaining a globally optimal solution. Greedy algorithms are simple and efficient, and are widely used in many practical problems. Greedy algorithms and dynamic programming are both commonly used to solve optimization problems. They share some similarities, such as both relying on the optimal substructure property, but they work differently. - Dynamic programming considers all previous decisions when making the current decision, and uses solutions to past subproblems to construct the solution to the current subproblem. - Greedy algorithms do not consider past decisions, but instead make greedy choices moving forward, continually reducing the problem size until the problem is solved. -We will first understand how greedy algorithms work through the example problem "coin change". This problem has already been introduced in the "Complete Knapsack Problem" chapter, so I believe you are not unfamiliar with it. +We will first understand how greedy algorithms work through the example problem "coin change." This problem was already introduced in the "Complete Knapsack Problem" chapter, so it should already be familiar to you. !!! question - Given $n$ types of coins, where the denomination of the $i$-th type of coin is $coins[i - 1]$, and the target amount is $amt$, with each type of coin available for repeated selection, what is the minimum number of coins needed to make up the target amount? If it is impossible to make up the target amount, return $-1$. + Given $n$ types of coins, where the denomination of the $i$-th type is $coins[i - 1]$, a target amount $amt$, and an unlimited number of coins of each type, what is the minimum number of coins needed to make up the target amount? If the target amount cannot be made up, return $-1$. -The greedy strategy adopted for this problem is shown in the figure below. Given a target amount, **we greedily select the coin that is not greater than and closest to it**, and continuously repeat this step until the target amount is reached. +The greedy strategy for this problem is shown in the figure below. Given a target amount, **we greedily choose the coin that does not exceed it and is closest to it**, repeating this step until the target amount is made up. ![Greedy strategy for coin change](greedy_algorithm.assets/coin_change_greedy_strategy.png) @@ -23,26 +23,26 @@ The implementation code is as follows: [file]{coin_change_greedy}-[class]{}-[func]{coin_change_greedy} ``` -You might exclaim: So clean! The greedy algorithm solves the coin change problem in about ten lines of code. +You may find yourself exclaiming, "So clean!" The greedy algorithm solves the coin change problem in only about ten lines of code. ## Advantages and Limitations of Greedy Algorithms -**Greedy algorithms are not only straightforward and simple to implement, but are also usually very efficient**. In the code above, if the smallest coin denomination is $\min(coins)$, the greedy choice loops at most $amt / \min(coins)$ times, giving a time complexity of $O(amt / \min(coins))$. This is an order of magnitude smaller than the time complexity of the dynamic programming solution $O(n \times amt)$. +**Greedy algorithms are not only straightforward to apply and easy to implement, but are also usually very efficient**. In the code above, if the smallest coin denomination is $\min(coins)$, the greedy selection loop runs at most $amt / \min(coins)$ times, giving a time complexity of $O(amt / \min(coins))$. This is an order of magnitude lower than the time complexity of the dynamic programming solution, $O(n \times amt)$. -However, **for certain coin denomination combinations, greedy algorithms cannot find the optimal solution**. The figure below provides two examples. +However, **for some coin denomination sets, greedy algorithms cannot find the optimal solution**. The figure below shows two examples. -- **Positive example $coins = [1, 5, 10, 20, 50, 100]$**: With this coin combination, given any $amt$, the greedy algorithm can find the optimal solution. -- **Negative example $coins = [1, 20, 50]$**: Suppose $amt = 60$, the greedy algorithm can only find the combination $50 + 1 \times 10$, totaling $11$ coins, but dynamic programming can find the optimal solution $20 + 20 + 20$, requiring only $3$ coins. -- **Negative example $coins = [1, 49, 50]$**: Suppose $amt = 98$, the greedy algorithm can only find the combination $50 + 1 \times 48$, totaling $49$ coins, but dynamic programming can find the optimal solution $49 + 49$, requiring only $2$ coins. +- **Positive example $coins = [1, 5, 10, 20, 50, 100]$**: With this coin set, the greedy algorithm can find the optimal solution for any $amt$. +- **Counterexample $coins = [1, 20, 50]$**: Suppose $amt = 60$. The greedy algorithm can only find the combination $50 + 1 \times 10$, using $11$ coins in total, whereas dynamic programming can find the optimal solution $20 + 20 + 20$ using only $3$ coins. +- **Counterexample $coins = [1, 49, 50]$**: Suppose $amt = 98$. The greedy algorithm can only find the combination $50 + 1 \times 48$, using $49$ coins in total, whereas dynamic programming can find the optimal solution $49 + 49$ using only $2$ coins. ![Examples where greedy algorithms cannot find the optimal solution](greedy_algorithm.assets/coin_change_greedy_vs_dp.png) -In other words, for the coin change problem, greedy algorithms cannot guarantee finding the global optimal solution, and may even find very poor solutions. It is better suited for solving with dynamic programming. +In other words, for the coin change problem, greedy algorithms cannot guarantee a globally optimal solution and may even produce very poor results. This problem is better solved with dynamic programming. -Generally, the applicability of greedy algorithms falls into the following two situations. +In general, greedy algorithms are applicable in the following two situations. -1. **Can guarantee finding the optimal solution**: In this situation, greedy algorithms are often the best choice, because they tend to be more efficient than backtracking and dynamic programming. -2. **Can find an approximate optimal solution**: Greedy algorithms are also applicable in this situation. For many complex problems, finding the global optimal solution is very difficult, and being able to find a suboptimal solution with high efficiency is also very good. +1. **The optimal solution can be guaranteed**: In this case, greedy algorithms are often the best choice because they tend to be more efficient than backtracking and dynamic programming. +2. **An approximately optimal solution can be found**: Greedy algorithms are also useful in this case. For many complex problems, finding the global optimal solution is very difficult, so efficiently finding a suboptimal solution is already a very good outcome. ## Characteristics of Greedy Algorithms @@ -57,30 +57,30 @@ Optimal substructure has already been introduced in the "Dynamic Programming" ch We mainly explore methods for determining the greedy choice property. Although its description seems relatively simple, **in practice, for many problems, proving the greedy choice property is not easy**. -For example, in the coin change problem, although we can easily provide counterexamples to disprove the greedy choice property, proving it is quite difficult. If asked: **what conditions must a coin combination satisfy to be solvable using a greedy algorithm**? We often can only rely on intuition or examples to give an ambiguous answer, and find it difficult to provide a rigorous mathematical proof. +For example, in the coin change problem, although we can easily provide counterexamples to disprove the greedy choice property, proving that it holds is much harder. If asked, **under what conditions can a coin set be solved using a greedy algorithm**? We often can only rely on intuition or examples to give a vague answer, and it is difficult to provide a rigorous mathematical proof. !!! quote - There is a paper that presents an algorithm with $O(n^3)$ time complexity for determining whether a coin combination can use a greedy algorithm to find the optimal solution for any amount. + There is a paper that presents an $O(n^3)$ algorithm for determining whether a coin set can be solved optimally by a greedy algorithm for any amount. Pearson, D. A polynomial-time algorithm for the change-making problem[J]. Operations Research Letters, 2005, 33(3): 231-234. ## Steps for Solving Problems with Greedy Algorithms -The problem-solving process for greedy problems can generally be divided into the following three steps. +The general process for solving greedy problems can be divided into the following three steps. -1. **Problem analysis**: Sort out and understand the problem characteristics, including state definition, optimization objectives, and constraints, etc. This step is also involved in backtracking and dynamic programming. -2. **Determine the greedy strategy**: Determine how to make greedy choices at each step. This strategy should be able to reduce the problem size at each step, ultimately solving the entire problem. -3. **Correctness proof**: It is usually necessary to prove that the problem has both greedy choice property and optimal substructure. This step may require mathematical proofs, such as mathematical induction or proof by contradiction. +1. **Problem analysis**: Sort out and understand the characteristics of the problem, including state definitions, optimization objectives, and constraints. This step also appears in backtracking and dynamic programming. +2. **Determine the greedy strategy**: Decide how to make a greedy choice at each step. This strategy should reduce the problem size step by step and ultimately solve the entire problem. +3. **Correctness proof**: It is usually necessary to prove that the problem has both greedy choice property and optimal substructure. This step may require mathematical tools such as induction or proof by contradiction. -Determining the greedy strategy is the core step in solving the problem, but it may not be easy to implement, mainly for the following reasons. +Determining the greedy strategy is the core step in solving such problems, but it may not be easy in practice, mainly for the following reasons. -- **Greedy strategies differ greatly between different problems**. For many problems, the greedy strategy is relatively straightforward, and we can derive it through some general thinking and attempts. However, for some complex problems, the greedy strategy may be very elusive, which really tests one's problem-solving experience and algorithmic ability. -- **Some greedy strategies are highly misleading**. When we confidently design a greedy strategy, write the solution code and submit it for testing, we may find that some test cases cannot pass. This is because the designed greedy strategy is only "partially correct", as exemplified by the coin change problem discussed above. +- **Greedy strategies vary greatly from problem to problem**. For many problems, the greedy strategy is fairly intuitive and can be derived through rough reasoning and experimentation. For some complex problems, however, the greedy strategy may be deeply hidden, which strongly tests one's problem-solving experience and algorithmic ability. +- **Some greedy strategies are highly deceptive**. We may confidently design a greedy strategy, write the solution code, and submit it, only to find that some test cases fail. This is because the designed greedy strategy is only "partially correct," as exemplified by the coin change problem discussed above. -To ensure correctness, we should rigorously mathematically prove the greedy strategy, **usually using proof by contradiction or mathematical induction**. +To ensure correctness, we should give a rigorous mathematical proof of the greedy strategy, **usually using proof by contradiction or mathematical induction**. -However, correctness proofs may also not be easy. If we have no clue, we usually choose to debug the code based on test cases, step by step modifying and verifying the greedy strategy. +However, correctness proofs can also be difficult. If we have no clear direction, we usually resort to debugging against test cases, revising and validating the greedy strategy step by step. ## Typical Problems Solved by Greedy Algorithms diff --git a/en/docs/chapter_greedy/index.md b/en/docs/chapter_greedy/index.md index f773db26a..5bc36c372 100644 --- a/en/docs/chapter_greedy/index.md +++ b/en/docs/chapter_greedy/index.md @@ -4,6 +4,6 @@ !!! abstract - Sunflowers turn toward the sun, constantly pursuing the maximum potential for their own growth. + Sunflowers turn toward the sun, always seeking the fullest growth possible. - Through rounds of simple choices, greedy strategies gradually lead to the best answer. + Through successive simple choices, greedy strategies gradually lead to the optimal solution. diff --git a/en/docs/chapter_greedy/max_capacity_problem.md b/en/docs/chapter_greedy/max_capacity_problem.md index 9c1eff0f3..ceac5d127 100644 --- a/en/docs/chapter_greedy/max_capacity_problem.md +++ b/en/docs/chapter_greedy/max_capacity_problem.md @@ -2,48 +2,48 @@ !!! question - Input an array $ht$, where each element represents the height of a vertical partition. Any two partitions in the array, along with the space between them, can form a container. + Given an array $ht$, where each element represents the height of a vertical partition. Any two partitions in the array, together with the space between them, can form a container. - The capacity of the container equals the product of height and width (area), where the height is determined by the shorter partition, and the width is the difference in array indices between the two partitions. + The capacity of the container equals the product of its height and width (that is, its area), where the height is determined by the shorter partition and the width is the difference between the array indices of the two partitions. - Please select two partitions in the array such that the capacity of the formed container is maximized, and return the maximum capacity. An example is shown in the figure below. + Select two partitions in the array such that the capacity of the resulting container is maximized, and return that maximum capacity. An example is shown in the figure below. ![Example data for the max capacity problem](max_capacity_problem.assets/max_capacity_example.png) -The container is formed by any two partitions, **therefore the state of this problem is the indices of two partitions, denoted as $[i, j]$**. +The container is formed by any two partitions, **so the state of this problem is the indices of the two partitions, denoted by $[i, j]$**. -According to the problem description, capacity equals height multiplied by width, where height is determined by the shorter partition, and width is the difference in array indices between the two partitions. Let the capacity be $cap[i, j]$, then the calculation formula is: +According to the problem statement, capacity equals height multiplied by width, where the height is determined by the shorter partition and the width is the difference between the array indices of the two partitions. Let the capacity be $cap[i, j]$; then we obtain the following formula: $$ cap[i, j] = \min(ht[i], ht[j]) \times (j - i) $$ -Let the array length be $n$, then the number of combinations of two partitions (total number of states) is $C_n^2 = \frac{n(n - 1)}{2}$. Most directly, **we can exhaustively enumerate all states** to find the maximum capacity, with time complexity $O(n^2)$. +Let the array length be $n$. Then the number of ways to choose two partitions (that is, the total number of states) is $C_n^2 = \frac{n(n - 1)}{2}$. The most straightforward approach is to **exhaustively enumerate all states** to find the maximum capacity, which has a time complexity of $O(n^2)$. ### Greedy Strategy Determination -This problem has a more efficient solution. As shown in the figure below, select a state $[i, j]$ where index $i < j$ and height $ht[i] < ht[j]$, meaning $i$ is the short partition and $j$ is the long partition. +This problem has a more efficient solution. As shown in the figure below, consider a state $[i, j]$ where $i < j$ and $ht[i] < ht[j]$. In this case, $i$ is the shorter partition and $j$ is the taller partition. ![Initial state](max_capacity_problem.assets/max_capacity_initial_state.png) -As shown in the figure below, **if we now move the long partition $j$ closer to the short partition $i$, the capacity will definitely decrease**. +As shown in the figure below, **if we now move the taller partition $j$ inward toward the shorter partition $i$, the capacity will definitely decrease**. -This is because after moving the long partition $j$, the width $j-i$ definitely decreases; and since height is determined by the short partition, the height can only remain unchanged ($i$ is still the short partition) or decrease (the moved $j$ becomes the short partition). +This is because after moving the taller partition $j$, the width $j-i$ definitely decreases. Since the height is determined by the shorter partition, the height can only stay the same ($i$ remains the shorter partition) or decrease ($j$ becomes the shorter partition after being moved). ![State after moving the long partition inward](max_capacity_problem.assets/max_capacity_moving_long_board.png) -Conversely, **we can only possibly increase capacity by contracting the short partition $i$ inward**. Because although width will definitely decrease, **height may increase** (the moved short partition $i$ may become taller). For example, in the figure below, the area increases after moving the short partition. +Conversely, **only by moving the shorter partition $i$ inward can the capacity possibly increase**. Although the width will definitely decrease, **the height may increase** (the moved partition at $i$ may be taller). For example, in the figure below, the area increases after moving the shorter partition. ![State after moving the short partition inward](max_capacity_problem.assets/max_capacity_moving_short_board.png) -From this we can derive the greedy strategy for this problem: initialize two pointers at both ends of the container, and in each round contract the pointer corresponding to the short partition inward, until the two pointers meet. +From this, we can derive the greedy strategy for this problem: initialize two pointers at the two ends, and in each round move the pointer corresponding to the shorter partition inward until the two pointers meet. The figure below shows the execution process of the greedy strategy. 1. In the initial state, pointers $i$ and $j$ are at both ends of the array. 2. Calculate the capacity of the current state $cap[i, j]$, and update the maximum capacity. -3. Compare the heights of partition $i$ and partition $j$, and move the short partition inward by one position. -4. Loop through steps `2.` and `3.` until $i$ and $j$ meet. +3. Compare the heights of partitions $i$ and $j$, and move the pointer corresponding to the shorter partition inward by one position. +4. Repeat steps `2.` and `3.` until $i$ and $j$ meet. === "<1>" ![Greedy process for the max capacity problem](max_capacity_problem.assets/max_capacity_greedy_step1.png) @@ -74,9 +74,9 @@ The figure below shows the execution process of the greedy strategy. ### Code Implementation -The code loops at most $n$ rounds, **therefore the time complexity is $O(n)$**. +The code runs for at most $n$ rounds, **so the time complexity is $O(n)$**. -Variables $i$, $j$, and $res$ use a constant amount of extra space, **therefore the space complexity is $O(1)$**. +Variables $i$, $j$, and $res$ use only a constant amount of extra space, **so the space complexity is $O(1)$**. ```src [file]{max_capacity}-[class]{}-[func]{max_capacity} @@ -86,7 +86,7 @@ Variables $i$, $j$, and $res$ use a constant amount of extra space, **therefore The reason greedy is faster than exhaustive enumeration is that each round of greedy selection "skips" some states. -For example, in state $cap[i, j]$ where $i$ is the short partition and $j$ is the long partition, if we greedily move the short partition $i$ inward by one position, the states shown in the figure below will be "skipped". **This means that the capacities of these states cannot be verified later**. +For example, in state $cap[i, j]$, suppose $i$ is the shorter partition and $j$ is the taller partition. If we greedily move the shorter partition $i$ inward by one position, the states shown in the figure below will be "skipped." **This means that their capacities can no longer be checked later**. $$ cap[i, i+1], cap[i, i+2], \dots, cap[i, j-2], cap[i, j-1] @@ -94,6 +94,6 @@ $$ ![States skipped by moving the short partition](max_capacity_problem.assets/max_capacity_skipped_states.png) -Observing carefully, **these skipped states are actually all the states obtained by moving the long partition $j$ inward**. We have already proven that moving the long partition inward will definitely decrease capacity. That is, the skipped states cannot possibly be the optimal solution, **skipping them will not cause us to miss the optimal solution**. +A closer look shows that **these skipped states are exactly the states obtained by moving the taller partition $j$ inward**. We have already proven that moving the taller partition inward will definitely decrease the capacity. Therefore, none of the skipped states can be the optimal solution, **so skipping them does not cause us to miss the optimum**. -The above analysis shows that the operation of moving the short partition is "safe", and the greedy strategy is effective. +The above analysis shows that moving the shorter partition is a "safe" operation, and that the greedy strategy is effective. diff --git a/en/docs/chapter_greedy/max_product_cutting_problem.md b/en/docs/chapter_greedy/max_product_cutting_problem.md index 60f72dc14..771ad8c69 100644 --- a/en/docs/chapter_greedy/max_product_cutting_problem.md +++ b/en/docs/chapter_greedy/max_product_cutting_problem.md @@ -1,8 +1,8 @@ -# Max Product Cutting Problem +# Maximum Product Cutting Problem !!! question - Given a positive integer $n$, split it into the sum of at least two positive integers, and find the maximum product of all integers after splitting, as shown in the figure below. + Given a positive integer $n$, split it into the sum of at least two positive integers and find the maximum product of the resulting integers, as shown in the figure below. ![Problem definition of max product cutting](max_product_cutting_problem.assets/max_product_cutting_definition.png) @@ -18,11 +18,11 @@ $$ \max(\prod_{i=1}^{m}n_i) $$ -We need to think about: how large should the splitting count $m$ be, and what should each $n_i$ be? +We need to determine how many parts $m$ there should be and what each $n_i$ should be. -### Greedy Strategy Determination +### Determining the Greedy Strategy -Based on experience, the product of two integers is often greater than their sum. Suppose we split out a factor of $2$ from $n$, then their product is $2(n-2)$. We compare this product with $n$: +As a rule of thumb, the product of two integers is often greater than their sum. Suppose we split off a factor of $2$ from $n$; the resulting product is $2(n-2)$. We compare this product with $n$: $$ \begin{aligned} @@ -34,7 +34,7 @@ $$ As shown in the figure below, when $n \geq 4$, splitting out a $2$ will increase the product, **which indicates that integers greater than or equal to $4$ should all be split**. -**Greedy strategy one**: If the splitting scheme includes factors $\geq 4$, then they should continue to be split. The final splitting scheme should only contain factors $1$, $2$, and $3$. +**Greedy strategy one**: If the splitting scheme contains a factor $\geq 4$, it should be split further. The final splitting scheme should contain only the factors $1$, $2$, and $3$. ![Splitting causes product to increase](max_product_cutting_problem.assets/max_product_cutting_greedy_infer1.png) @@ -42,7 +42,7 @@ Next, consider which factor is optimal. Among the three factors $1$, $2$, and $3 As shown in the figure below, when $n = 6$, we have $3 \times 3 > 2 \times 2 \times 2$. **This means that splitting out $3$ is better than splitting out $2$**. -**Greedy strategy two**: In the splitting scheme, there should be at most two $2$s. Because three $2$s can always be replaced by two $3$s to obtain a larger product. +**Greedy strategy two**: In the splitting scheme, there should be at most two $2$s, because three $2$s can always be replaced by two $3$s to obtain a larger product. ![Optimal splitting factor](max_product_cutting_problem.assets/max_product_cutting_greedy_infer2.png) @@ -50,12 +50,12 @@ In summary, the following greedy strategies can be derived. 1. Input integer $n$, continuously split out factor $3$ until the remainder is $0$, $1$, or $2$. 2. When the remainder is $0$, it means $n$ is a multiple of $3$, so no further action is needed. -3. When the remainder is $2$, do not continue splitting, keep it. -4. When the remainder is $1$, since $2 \times 2 > 1 \times 3$, the last $3$ should be replaced with $2$. +3. When the remainder is $2$, do not split it further; keep it as is. +4. When the remainder is $1$, since $2 \times 2 > 1 \times 3$, replace the final $3$ and the remaining $1$ with two $2$s. ### Code Implementation -As shown in the figure below, we don't need to use loops to split the integer, but can use integer division to get the count of $3$s as $a$, and modulo operation to get the remainder as $b$, at which point we have: +As shown in the figure below, we do not need loops to split the integer. Instead, we use integer division to obtain the number of $3$s, denoted by $a$, and the modulo operation to obtain the remainder $b$, giving: $$ n = 3 a + b @@ -69,7 +69,7 @@ Please note that for the edge case of $n \leq 3$, a $1$ must be split out, with ![Calculation method for max product cutting](max_product_cutting_problem.assets/max_product_cutting_greedy_calculation.png) -**The time complexity depends on the implementation of the exponentiation operation in the programming language**. Taking Python as an example, there are three commonly used power calculation functions. +**The time complexity depends on how exponentiation is implemented in the programming language**. Taking Python as an example, there are three commonly used ways to compute powers. - Both the operator `**` and the function `pow()` have time complexity $O(\log⁡ a)$. - The function `math.pow()` internally calls the C library's `pow()` function, which performs floating-point exponentiation, with time complexity $O(1)$. @@ -78,8 +78,8 @@ Variables $a$ and $b$ use a constant amount of extra space, **therefore the spac ### Correctness Proof -Using proof by contradiction, only analyzing the case where $n \geq 4$. +We use proof by contradiction and consider only the case where $n \geq 4$. -1. **All factors $\leq 3$**: Suppose the optimal splitting scheme includes a factor $x \geq 4$, then it can definitely continue to be split into $2(x-2)$ to obtain a larger (or equal) product. This contradicts the assumption. -2. **The splitting scheme does not contain $1$**: Suppose the optimal splitting scheme includes a factor of $1$, then it can definitely be merged into another factor to obtain a larger product. This contradicts the assumption. -3. **The splitting scheme contains at most two $2$s**: Suppose the optimal splitting scheme includes three $2$s, then they can definitely be replaced by two $3$s for a larger product. This contradicts the assumption. +1. **All factors $\leq 3$**: Suppose the optimal splitting scheme includes a factor $x \geq 4$. Then it can be further split into $2(x-2)$ to obtain a larger (or equal) product. This contradicts the assumption. +2. **The splitting scheme does not contain $1$**: Suppose the optimal splitting scheme includes a factor of $1$. Then it can be merged into another factor to obtain a larger product. This contradicts the assumption. +3. **The splitting scheme contains at most two $2$s**: Suppose the optimal splitting scheme includes three $2$s. Then they can be replaced by two $3$s, yielding a larger product. This contradicts the assumption. diff --git a/en/docs/chapter_greedy/summary.md b/en/docs/chapter_greedy/summary.md index cbdca517a..7d21b338b 100644 --- a/en/docs/chapter_greedy/summary.md +++ b/en/docs/chapter_greedy/summary.md @@ -8,7 +8,7 @@ - In the coin change problem, for certain coin combinations, greedy algorithms can guarantee finding the optimal solution; for other coin combinations, however, greedy algorithms may find very poor solutions. - Problems suitable for solving with greedy algorithms have two major properties: greedy choice property and optimal substructure. The greedy choice property represents the effectiveness of the greedy strategy. - For some complex problems, proving the greedy choice property is not simple. Relatively speaking, disproving it is easier, such as in the coin change problem. -- Solving greedy problems mainly consists of three steps: problem analysis, determining the greedy strategy, and correctness proof. Among these, determining the greedy strategy is the core step, and correctness proof is often the difficult point. -- The fractional knapsack problem, based on the 0-1 knapsack problem, allows selecting a portion of items, and therefore can be solved using greedy algorithms. The correctness of the greedy strategy can be proven using proof by contradiction. -- The max capacity problem can be solved using exhaustive enumeration with time complexity $O(n^2)$. By designing a greedy strategy to move the short partition inward in each round, the time complexity can be optimized to $O(n)$. +- Solving greedy problems mainly consists of three steps: problem analysis, determining the greedy strategy, and correctness proof. Among these, determining the greedy strategy is the core step, and correctness proof is often the main difficulty. +- The fractional knapsack problem, based on the 0-1 knapsack problem, allows selecting fractions of items, and therefore can be solved using greedy algorithms. The correctness of the greedy strategy can be proven using proof by contradiction. +- The max capacity problem can be solved using exhaustive enumeration with time complexity $O(n^2)$. By designing a greedy strategy to move the shorter side inward in each round, the time complexity can be optimized to $O(n)$. - In the max product cutting problem, we successively derive two greedy strategies: integers $\geq 4$ should all continue to be split, and the optimal splitting factor is $3$. The code includes exponentiation operations, and the time complexity depends on the implementation method of exponentiation, typically being $O(1)$ or $O(\log n)$. diff --git a/en/docs/chapter_hashing/hash_algorithm.md b/en/docs/chapter_hashing/hash_algorithm.md index bdf5569bc..7f0c9767e 100644 --- a/en/docs/chapter_hashing/hash_algorithm.md +++ b/en/docs/chapter_hashing/hash_algorithm.md @@ -6,7 +6,7 @@ If hash collisions occur too frequently, the performance of the hash table will ![Ideal and worst cases of hash collisions](hash_algorithm.assets/hash_collision_best_worst_condition.png) -**The distribution of key-value pairs is determined by the hash function**. Recalling the calculation steps of the hash function, first compute the hash value, then take the modulo by the array length: +**The distribution of key-value pairs is determined by the hash function**. Recall the steps of the hash function: first compute the hash value, then take it modulo the array length: ```shell index = hash(key) % capacity @@ -18,7 +18,7 @@ This means that, to reduce the probability of hash collisions, we should focus o ## Goals of Hash Algorithms -To achieve a "fast and stable" hash table data structure, hash algorithms should have the following characteristics: +To build a hash table that is both fast and robust, a hash algorithm should have the following properties: - **Determinism**: For the same input, the hash algorithm should always produce the same output. Only then can the hash table be reliable. - **High efficiency**: The process of computing the hash value should be fast enough. The smaller the computational overhead, the more practical the hash table. @@ -29,7 +29,7 @@ In fact, hash algorithms are not only used to implement hash tables but are also - **Password storage**: To protect the security of user passwords, systems usually do not store the plaintext passwords but rather the hash values of the passwords. When a user enters a password, the system calculates the hash value of the input and compares it with the stored hash value. If they match, the password is considered correct. - **Data integrity check**: The data sender can calculate the hash value of the data and send it along; the receiver can recalculate the hash value of the received data and compare it with the received hash value. If they match, the data is considered intact. -For cryptographic applications, to prevent reverse engineering such as deducing the original password from the hash value, hash algorithms need higher-level security features. +For cryptographic applications, hash algorithms need stronger security properties to prevent reverse engineering, such as inferring the original password from a hash value. - **Unidirectionality**: It should be impossible to deduce any information about the input data from the hash value. - **Collision resistance**: It should be extremely difficult to find two different inputs that produce the same hash value. @@ -42,7 +42,7 @@ Note that **"uniform distribution" and "collision resistance" are two independen The design of hash algorithms is a complex issue that requires consideration of many factors. However, for some less demanding scenarios, we can also design some simple hash algorithms. - **Additive hash**: Add up the ASCII codes of each character in the input and use the total sum as the hash value. -- **Multiplicative hash**: Utilize the non-correlation of multiplication, multiplying each round by a constant, accumulating the ASCII codes of each character into the hash value. +- **Multiplicative hash**: Leverage the low correlation introduced by multiplication: multiply by a constant at each step and accumulate the ASCII codes of the characters into the hash value. - **XOR hash**: Accumulate the hash value by XORing each element of the input data. - **Rotating hash**: Accumulate the ASCII code of each character into a hash value, performing a rotation operation on the hash value before each accumulation. @@ -50,9 +50,9 @@ The design of hash algorithms is a complex issue that requires consideration of [file]{simple_hash}-[class]{}-[func]{rot_hash} ``` -It is observed that the last step of each hash algorithm is to take the modulus of the large prime number $1000000007$ to ensure that the hash value is within an appropriate range. It is worth pondering why emphasis is placed on modulo a prime number, or what are the disadvantages of modulo a composite number? This is an interesting question. +We can observe that the final step of each hash algorithm is to take the result modulo the large prime $1000000007$, ensuring that the hash value stays within a suitable range. This naturally raises a question: why emphasize using a prime modulus, and what are the drawbacks of using a composite modulus? -To conclude: **Using a large prime number as the modulus can maximize the uniform distribution of hash values**. Since a prime number does not share common factors with other numbers, it can reduce the periodic patterns caused by the modulo operation, thus avoiding hash collisions. +In short: **using a large prime as the modulus helps maximize the uniformity of hash values**. Because a prime shares no common factors with other numbers, it can reduce periodic patterns introduced by the modulo operation and thus mitigate hash collisions. For example, suppose we choose the composite number $9$ as the modulus, which can be divided by $3$, then all `key` divisible by $3$ will be mapped to hash values $0$, $3$, $6$. @@ -64,7 +64,7 @@ $$ \end{aligned} $$ -If the input `key` happens to have this kind of arithmetic sequence distribution, then the hash values will cluster, thereby exacerbating hash collisions. Now, suppose we replace `modulus` with the prime number $13$, since there are no common factors between `key` and `modulus`, the uniformity of the output hash values will be significantly improved. +If the input `key` values happen to follow this kind of arithmetic progression, the hash values will cluster, worsening hash collisions. Now suppose we replace `modulus` with the prime number $13$. Because `key` and `modulus` share no common factors, the output hash values become much more evenly distributed. $$ \begin{aligned} @@ -80,7 +80,7 @@ In summary, we usually choose a prime number as the modulus, and this prime numb ## Common Hash Algorithms -It is not hard to see that the simple hash algorithms mentioned above are quite "fragile" and far from reaching the design goals of hash algorithms. For example, since addition and XOR obey the commutative law, additive hash and XOR hash cannot distinguish strings with the same content but in different order, which may exacerbate hash collisions and cause security issues. +It is easy to see that the simple hash algorithms introduced above are fairly "fragile" and fall far short of the design goals of hash algorithms. For example, because addition and XOR are commutative, additive hash and XOR hash cannot distinguish strings with the same characters in a different order, which may worsen hash collisions and introduce security risks. In practice, we usually use some standard hash algorithms, such as MD5, SHA-1, SHA-2, and SHA-3. They can map input data of any length to a fixed-length hash value. @@ -100,14 +100,14 @@ Over the past century, hash algorithms have been in a continuous process of upgr | Security Level | Low, has been successfully attacked | Low, has been successfully attacked | High | High | | Applications | Abandoned, still used for data integrity checks | Abandoned | Cryptocurrency transaction verification, digital signatures, etc. | Can be used to replace SHA-2 | -# Hash Values in Data Structures +## Hash Values in Data Structures -We know that the keys in a hash table can be of various data types such as integers, decimals, or strings. Programming languages usually provide built-in hash algorithms for these data types to calculate the bucket indices in the hash table. Taking Python as an example, we can use the `hash()` function to compute the hash values for various data types. +We know that hash table keys can be integers, floating-point numbers, strings, and other data types. Programming languages usually provide built-in hash algorithms for these types to compute bucket indices in a hash table. Taking Python as an example, we can call the `hash()` function to compute hash values for various data types. - The hash values of integers and booleans are their own values. - The calculation of hash values for floating-point numbers and strings is more complex, and interested readers are encouraged to study this on their own. -- The hash value of a tuple is a combination of the hash values of each of its elements, resulting in a single hash value. -- The hash value of an object is generated based on its memory address. By overriding the hash method of an object, hash values can be generated based on content. +- The hash value of a tuple is obtained by hashing each of its elements and combining those results into a single hash value. +- An object's hash value is typically generated from its memory address. By overriding the object's hash method, it can instead be generated from the object's contents. !!! tip diff --git a/en/docs/chapter_hashing/hash_collision.md b/en/docs/chapter_hashing/hash_collision.md index bf61008d2..a21c08c2f 100644 --- a/en/docs/chapter_hashing/hash_collision.md +++ b/en/docs/chapter_hashing/hash_collision.md @@ -7,19 +7,19 @@ Hash collisions can lead to incorrect query results, severely impacting the usab 1. Improve the hash table data structure so that **the hash table can function normally when hash collisions occur**. 2. Only expand when necessary, that is, only when hash collisions are severe. -The main methods for improving the structure of hash tables include "separate chaining" and "open addressing". +The main approaches to improving a hash table's structure are separate chaining and open addressing. ## Separate Chaining -In the original hash table, each bucket can store only one key-value pair. Separate chaining converts a single element into a linked list, treating key-value pairs as linked list nodes and storing all colliding key-value pairs in the same linked list. The figure below shows an example of a separate chaining hash table. +In the original hash table, each bucket can store only one key-value pair. Separate chaining replaces the single element in each bucket with a linked list, treating each key-value pair as a node and storing all colliding key-value pairs in the same list. The figure below shows an example of a separate chaining hash table. ![Separate chaining hash table](hash_collision.assets/hash_table_chaining.png) -The operations of a hash table implemented with separate chaining have changed as follows: +In a hash table implemented with separate chaining, the basic operations work as follows: -- **Querying elements**: Input `key`, obtain the bucket index through the hash function, then access the head node of the linked list, then traverse the linked list and compare `key` to find the target key-value pair. -- **Adding elements**: First access the linked list head node through the hash function, then append the node (key-value pair) to the linked list. -- **Deleting elements**: Access the head of the linked list based on the result of the hash function, then traverse the linked list to find the target node and delete it. +- **Querying elements**: Input `key`, compute the bucket index using the hash function, access the head of the corresponding linked list, and traverse the list while comparing keys until the target key-value pair is found. +- **Adding elements**: First use the hash function to locate the corresponding linked list, then insert the node (key-value pair) into the list. +- **Deleting elements**: Use the hash function to locate the corresponding linked list, then traverse it to find and delete the target node. Separate chaining has the following limitations: @@ -35,36 +35,36 @@ The code below provides a simple implementation of a separate chaining hash tabl [file]{hash_map_chaining}-[class]{hash_map_chaining}-[func]{} ``` -It's worth noting that when the linked list is very long, the query efficiency $O(n)$ is poor. **In this case, the list can be converted to an "AVL tree" or "Red-Black tree"** to optimize the time complexity of the query operation to $O(\log n)$. +It's worth noting that when the linked list becomes very long, the query time $O(n)$ is poor. **In this case, the linked list can be converted into an AVL tree or a red-black tree**, reducing the time complexity of lookups to $O(\log n)$. ## Open Addressing -Open addressing does not introduce additional data structures but instead handles hash collisions through "multiple probes". The probing methods mainly include linear probing, quadratic probing, and double hashing. +Open addressing does not introduce additional data structures. Instead, it handles hash collisions through repeated probing. Common probing strategies include linear probing, quadratic probing, and multiple hashing. Let's use linear probing as an example to introduce the mechanism of open addressing hash tables. ### Linear Probing -Linear probing uses a fixed-step linear search for probing, and its operation method differs from ordinary hash tables. +Linear probing uses a fixed step size to probe sequentially, so its operations differ somewhat from those of an ordinary hash table. -- **Inserting elements**: Calculate the bucket index using the hash function. If the bucket already contains an element, linearly traverse forward from the conflict position (usually with a step size of $1$) until an empty bucket is found, then insert the element. -- **Searching for elements**: If a hash collision is encountered, use the same step size to linearly traverse forward until the corresponding element is found and return `value`; if an empty bucket is encountered, it means the target element is not in the hash table, so return `None`. +- **Inserting elements**: Compute the bucket index using the hash function. If the bucket is already occupied, continue probing forward from the collision position with a fixed step size (usually $1$) until an empty bucket is found, then insert the element there. +- **Searching for elements**: If a collision occurs, continue probing forward with the same step size until the corresponding element is found and return its `value`; if an empty bucket is encountered, the target element is not in the hash table, so return `None`. -The figure below shows the distribution of key-value pairs in an open addressing (linear probing) hash table. According to this hash function, keys with the same last two digits will be mapped to the same bucket. Through linear probing, they are stored sequentially in that bucket and the buckets below it. +The figure below shows the distribution of key-value pairs in an open-addressing hash table that uses linear probing. Under this hash function, keys with the same last two digits are mapped to the same bucket. Linear probing then places them in that bucket and the subsequent buckets. ![Distribution of key-value pairs in open addressing (linear probing) hash table](hash_collision.assets/hash_table_linear_probing.png) -However, **linear probing is prone to create "clustering"**. Specifically, the longer the continuously occupied positions in the array, the greater the probability of hash collisions occurring in these continuous positions, further promoting clustering growth at that position, forming a vicious cycle, and ultimately leading to degraded efficiency of insertion, deletion, query, and update operations. +However, **linear probing is prone to clustering**. Specifically, the longer a contiguous occupied region in the array becomes, the more likely new collisions are to occur within that region. This in turn makes the cluster grow even further, creating a vicious cycle that gradually degrades the efficiency of insertion, deletion, lookup, and update operations. -It's important to note that **we cannot directly delete elements in an open addressing hash table**. Deleting an element creates an empty bucket `None` in the array. When searching for elements, if linear probing encounters this empty bucket, it will return, making the elements below this empty bucket inaccessible. The program may incorrectly assume these elements do not exist, as shown in the figure below. +It's important to note that **we cannot directly delete elements from an open-addressing hash table**. Deleting an element creates an empty bucket `None` in the array. During lookup, once linear probing reaches that empty bucket, it stops, which means any elements stored farther along the probe sequence become unreachable. As a result, the program may incorrectly conclude that those elements do not exist, as shown in the figure below. ![Query issues caused by deletion in open addressing](hash_collision.assets/hash_table_open_addressing_deletion.png) -To solve this problem, we can adopt the lazy deletion mechanism: instead of directly removing elements from the hash table, **use a constant `TOMBSTONE` to mark the bucket**. In this mechanism, both `None` and `TOMBSTONE` represent empty buckets and can hold key-value pairs. However, when linear probing encounters `TOMBSTONE`, it should continue traversing since there may still be key-value pairs below it. +To solve this problem, we can adopt lazy deletion: instead of directly removing an element from the hash table, **use a constant `TOMBSTONE` to mark the bucket**. Under this mechanism, both `None` and `TOMBSTONE` denote buckets that can accept key-value pairs. The difference is that when linear probing encounters `TOMBSTONE`, it must continue probing, because key-value pairs may still exist farther along the sequence. -However, **lazy deletion may accelerate the performance degradation of the hash table**. Every deletion operation produces a deletion mark, and as `TOMBSTONE` increases, the search time will also increase because linear probing may need to skip multiple `TOMBSTONE` to find the target element. +However, **lazy deletion may accelerate hash-table performance degradation**. Each deletion leaves behind a marker, and as the number of `TOMBSTONE` entries grows, search time increases as well, because linear probing may need to skip over multiple tombstones before finding the target element. -To address this, consider recording the index of the first encountered `TOMBSTONE` during linear probing and swapping the searched target element with that `TOMBSTONE`. The benefit of doing this is that each time an element is queried or added, the element will be moved to a bucket closer to its ideal position (the starting point of probing), thereby optimizing query efficiency. +To address this, we can record the index of the first `TOMBSTONE` encountered during linear probing and swap the found target element into that position. The benefit is that each query or insertion can move elements closer to their ideal positions, that is, closer to where probing begins, which improves lookup efficiency. The code below implements an open addressing (linear probing) hash table with lazy deletion. To make better use of the hash table space, we treat the hash table as a "circular array". When going beyond the end of the array, we return to the beginning and continue traversing. @@ -86,18 +86,18 @@ However, quadratic probing is not perfect: - Clustering still exists, i.e., some positions are more likely to be occupied than others. - Due to the growth of squares, quadratic probing may not probe the entire hash table, meaning that even if there are empty buckets in the hash table, quadratic probing may not be able to access them. -### Double Hashing +### Multiple Hashing -As the name suggests, the double hashing method uses multiple hash functions $f_1(x)$, $f_2(x)$, $f_3(x)$, $\dots$ for probing. +As the name suggests, multiple hashing uses multiple hash functions $f_1(x)$, $f_2(x)$, $f_3(x)$, $\dots$ for probing. - **Inserting elements**: If hash function $f_1(x)$ encounters a conflict, try $f_2(x)$, and so on, until an empty position is found and the element is inserted. - **Searching for elements**: Search in the same order of hash functions until the target element is found and return it; if an empty position is encountered or all hash functions have been tried, it indicates the element is not in the hash table, then return `None`. -Compared to linear probing, the double hashing method is less prone to clustering, but multiple hash functions introduce additional computational overhead. +Compared with linear probing, multiple hashing is less prone to clustering, but using multiple hash functions introduces additional computational overhead. !!! tip - Please note that open addressing (linear probing, quadratic probing, and double hashing) hash tables all have the problem of "cannot directly delete elements". + Please note that hash tables based on open addressing, including linear probing, quadratic probing, and multiple hashing, all have the problem that elements cannot be deleted directly. ## Choice of Programming Languages diff --git a/en/docs/chapter_hashing/hash_map.md b/en/docs/chapter_hashing/hash_map.md index 714d0392b..3f2a0a696 100755 --- a/en/docs/chapter_hashing/hash_map.md +++ b/en/docs/chapter_hashing/hash_map.md @@ -1,8 +1,8 @@ # Hash Table -A hash table, also known as a hash map, establishes a mapping between keys `key` and values `value`, enabling efficient element retrieval. Specifically, when we input a key `key` into a hash table, we can retrieve the corresponding value `value` in $O(1)$ time. +A hash table, also known as a hash map, stores mappings from keys `key` to values `value`, enabling efficient lookups. Specifically, given a key `key`, we can retrieve the corresponding value `value` from a hash table in $O(1)$ time. -As shown in the figure below, given $n$ students, each with two pieces of data: "name" and "student ID". If we want to implement a query function that "inputs a student ID and returns the corresponding name", we can use the hash table shown below. +As shown below, suppose we have $n$ students, each with two pieces of information: a name and a student ID. If we want to support the query "given a student ID, return the corresponding name," we can use the hash table shown below. ![Abstract representation of a hash table](hash_map.assets/hash_table_lookup.png) @@ -20,7 +20,7 @@ In addition to hash tables, arrays and linked lists can also implement query fun | Add element | $O(1)$ | $O(1)$ | $O(1)$ | | Delete element | $O(n)$ | $O(n)$ | $O(1)$ | -As observed, **the time complexity for insertion, deletion, search, and modification operations in a hash table is $O(1)$**, which is very efficient. +As we can see, **insertion, deletion, lookup, and update operations in a hash table all have time complexity $O(1)$**, making hash tables highly efficient. ## Common Hash Table Operations @@ -249,7 +249,7 @@ Common operations on hash tables include: initialization, query operations, addi map.insert(12836, "XiaoHa".to_string()); map.insert(15937, "XiaoLuo".to_string()); map.insert(16750, "XiaoSuan".to_string()); - map.insert(13279, "XiaoFa".to_string()); + map.insert(13276, "XiaoFa".to_string()); map.insert(10583, "XiaoYa".to_string()); /* Query operation */ @@ -540,22 +540,22 @@ There are three common ways to traverse a hash table: traversing key-value pairs ## Simple Hash Table Implementation -Let's first consider the simplest case: **implementing a hash table using only an array**. In a hash table, each empty position in the array is called a bucket, and each bucket can store a key-value pair. Therefore, the query operation is to find the bucket corresponding to `key` and retrieve the `value` from the bucket. +Let's start with the simplest case: **implementing a hash table with just an array**. In a hash table, each empty slot in the array is called a bucket, and each bucket can store one key-value pair. A lookup therefore consists of finding the bucket for `key` and reading the `value` stored there. -So how do we locate the corresponding bucket based on `key`? This is achieved through a hash function. The role of the hash function is to map a larger input space to a smaller output space. In a hash table, the input space is all `key`s, and the output space is all buckets (array indices). In other words, given a `key`, **we can use the hash function to obtain the storage location of the key-value pair corresponding to that `key` in the array**. +So how do we find the right bucket for a given `key`? We do this with a hash function. A hash function maps a larger input space to a smaller output space. In a hash table, the input space is the set of all `key`s, and the output space is the set of all buckets (array indices). In other words, given a `key`, **the hash function tells us where the corresponding key-value pair should be stored in the array**. -When inputting a `key`, the hash function's calculation process consists of the following two steps: +Given a `key`, computing the bucket index involves the following two steps: -1. Calculate the hash value through a hash algorithm `hash()`. -2. Take the modulo of the hash value by the number of buckets (array length) `capacity` to obtain the bucket (array index) `index` corresponding to that `key`. +1. Use a hash algorithm `hash()` to compute a hash value. +2. Take that hash value modulo the number of buckets (array length), `capacity`, to obtain the bucket (array index) `index` corresponding to the `key`. ```shell index = hash(key) % capacity ``` -Subsequently, we can use `index` to access the corresponding bucket in the hash table and retrieve the `value`. +We can then use `index` to access the corresponding bucket in the hash table and retrieve the `value`. -Assuming the array length is `capacity = 100` and the hash algorithm is `hash(key) = key`, the hash function becomes `key % 100`. The figure below shows the working principle of the hash function using `key` as student ID and `value` as name. +Suppose the array length is `capacity = 100` and the hash algorithm is `hash(key) = key`. Then the hash function is `key % 100`. The figure below illustrates how this hash function works, using student ID as `key` and name as `value`. ![Working principle of hash function](hash_map.assets/hash_function.png) @@ -567,7 +567,7 @@ The following code implements a simple hash table. Here, we encapsulate `key` an ## Hash Collision and Resizing -Fundamentally, the role of a hash function is to map the input space consisting of all `key`s to the output space consisting of all array indices, and the input space is often much larger than the output space. Therefore, **theoretically there must be cases where "multiple inputs correspond to the same output"**. +Fundamentally, a hash function maps the input space consisting of all `key`s to the output space consisting of all array indices, and the input space is often much larger than the output space. Therefore, **in theory, different inputs must sometimes map to the same output**. For the hash function in the above example, when the input `key`s have the same last two digits, the hash function produces the same output. For example, when querying two students with IDs 12836 and 20336, we get: @@ -576,7 +576,7 @@ For the hash function in the above example, when the input `key`s have the same 20336 % 100 = 36 ``` -As shown in the figure below, two student IDs point to the same name, which is obviously incorrect. We call this situation where multiple inputs correspond to the same output a hash collision. +As shown below, two student IDs now point to the same name, which is clearly incorrect. We call this situation, where multiple inputs map to the same output, a hash collision. ![Hash collision example](hash_map.assets/hash_collision.png) @@ -586,6 +586,6 @@ As shown in the figure below, before expansion, the key-value pairs `(136, A)` a ![Hash table resizing](hash_map.assets/hash_table_reshash.png) -Similar to array expansion, hash table expansion requires migrating all key-value pairs from the original hash table to the new hash table, which is very time-consuming. Moreover, since the hash table capacity `capacity` changes, we need to recalculate the storage locations of all key-value pairs through the hash function, further increasing the computational overhead of the expansion process. For this reason, programming languages typically reserve a sufficiently large hash table capacity to prevent frequent expansion. +Like resizing an array, resizing a hash table requires migrating all key-value pairs from the original table to the new table, which is expensive. In addition, because the hash table capacity `capacity` changes, we must recompute the storage location of every key-value pair using the hash function, which further increases the cost of resizing. For this reason, programming languages typically reserve a sufficiently large hash table capacity to avoid frequent resizing. -The load factor is an important concept for hash tables. It is defined as the number of elements in the hash table divided by the number of buckets, and is used to measure the severity of hash collisions. **It is also commonly used as a trigger condition for hash table expansion**. For example, in Java, when the load factor exceeds $0.75$, the system will expand the hash table to $2$ times its original size. +The load factor is an important concept in hash tables. It is defined as the number of elements in the hash table divided by the number of buckets and is used to measure the severity of hash collisions. **It is also commonly used as a threshold for triggering hash table resizing**. For example, in Java, when the load factor exceeds $0.75$, the system expands the hash table to twice its original size. diff --git a/en/docs/chapter_hashing/index.md b/en/docs/chapter_hashing/index.md index 135c681df..793b3ad05 100644 --- a/en/docs/chapter_hashing/index.md +++ b/en/docs/chapter_hashing/index.md @@ -6,4 +6,4 @@ In the world of computing, a hash table is like a clever librarian. - They know how to calculate call numbers, enabling them to quickly locate the target book. + It knows how to compute call numbers, allowing it to quickly locate the desired book. diff --git a/en/docs/chapter_hashing/summary.md b/en/docs/chapter_hashing/summary.md index e4bc7a805..4891ec570 100644 --- a/en/docs/chapter_hashing/summary.md +++ b/en/docs/chapter_hashing/summary.md @@ -8,7 +8,7 @@ - Two different keys may end up with the same array index after hashing, leading to erroneous query results. This phenomenon is known as hash collision. - The larger the capacity of the hash table, the lower the probability of hash collisions. Therefore, hash table expansion can mitigate hash collisions. Similar to array expansion, hash table expansion is costly. - The load factor, defined as the number of elements divided by the number of buckets, reflects the severity of hash collisions and is often used as a condition to trigger hash table expansion. -- Separate chaining addresses hash collisions by converting each element into a linked list, storing all colliding elements in the same linked list. However, excessively long linked lists can reduce query efficiency, which can be improved by converting the linked lists into red-black trees. +- Separate chaining addresses hash collisions by storing all colliding elements in the same linked list. However, excessively long linked lists can reduce query efficiency, which can be improved by further converting the linked lists into red-black trees. - Open addressing handles hash collisions through multiple probing. Linear probing uses a fixed step size but cannot delete elements and is prone to clustering. Double hashing uses multiple hash functions for probing, which reduces clustering compared to linear probing but increases computational overhead. - Different programming languages adopt various hash table implementations. For example, Java's `HashMap` uses separate chaining, while Python's `dict` employs open addressing. - In hash tables, we desire hash algorithms with determinism, high efficiency, and uniform distribution. In cryptography, hash algorithms should also possess collision resistance and the avalanche effect. @@ -45,3 +45,7 @@ During the search process, the hash function points to the corresponding bucket **Q**: Why can expanding a hash table alleviate hash collisions? The last step of a hash function often involves taking the modulo of the array length $n$, to keep the output within the array index range. When expanding, the array length $n$ changes, and the indices corresponding to the keys may also change. Keys that were previously mapped to the same bucket might be distributed across multiple buckets after expansion, thereby mitigating hash collisions. + +**Q**: If the goal is efficient access, why not just use an array directly? + +When the `key` values are continuous integers within a small range, an array is indeed a simple and efficient choice. But when the `key` is of another type, such as a string, we need a hash function to map the `key` to an array index and then store the element in a bucket array. That structure is precisely what a hash table is. diff --git a/en/docs/chapter_heap/build_heap.md b/en/docs/chapter_heap/build_heap.md index 693aa849c..cd1313fe9 100644 --- a/en/docs/chapter_heap/build_heap.md +++ b/en/docs/chapter_heap/build_heap.md @@ -4,7 +4,7 @@ In some cases, we want to build a heap using all elements of a list, and this pr ## Implementing with Element Insertion -We first create an empty heap, then iterate through the list, performing the "element insertion operation" on each element in sequence. This means adding the element to the bottom of the heap and then performing "bottom-to-top" heapify on that element. +We first create an empty heap, then iterate through the list, performing the "element insertion operation" on each element in sequence. This means appending the element to the end of the heap and then performing "bottom-to-top" heapify on that element. Each time an element is inserted into the heap, the heap's length increases by one. Since nodes are added to the binary tree sequentially from top to bottom, the heap is constructed "from top to bottom." @@ -19,9 +19,9 @@ In fact, we can implement a more efficient heap construction method in two steps **After heapifying a node, the subtree rooted at that node becomes a valid sub-heap**. Since we traverse in reverse order, the heap is constructed "from bottom to top." -The reason for choosing reverse order traversal is that it ensures the subtree below the current node is already a valid sub-heap, making the heapification of the current node effective. +The reason for choosing reverse-order traversal is that it ensures the subtrees beneath the current node are already valid sub-heaps, so heapifying the current node is effective. -It's worth noting that **since leaf nodes have no children, they are naturally valid sub-heaps and do not require heapification**. As shown in the code below, the last non-leaf node is the parent of the last node; we start from it and traverse in reverse order to perform heapification: +It's worth noting that **since leaf nodes have no children, they are naturally valid sub-heaps and do not require heapification**. As shown in the code below, the last non-leaf node is the parent of the last node; we start from that node and heapify while traversing in reverse order: ```src [file]{my_heap}-[class]{max_heap}-[func]{__init__} @@ -32,21 +32,21 @@ It's worth noting that **since leaf nodes have no children, they are naturally v Next, let's attempt to derive the time complexity of this second heap construction method. - Assuming the complete binary tree has $n$ nodes, then the number of leaf nodes is $(n + 1) / 2$, where $/$ is floor division. Therefore, the number of nodes that need heapification is $(n - 1) / 2$. -- In the top-to-bottom heapify process, each node is heapified at most to the leaf nodes, so the maximum number of iterations is the binary tree height $\log n$. +- In the top-to-bottom heapify process, each node can sink at most to a leaf node, so the maximum number of iterations is the height of the binary tree, $\log n$. Multiplying these two together, we get a time complexity of $O(n \log n)$ for the heap construction process. **However, this estimate is not accurate because it doesn't account for the property that binary trees have far more nodes at lower levels than at upper levels**. -Let's perform a more accurate calculation. To reduce calculation difficulty, assume a "perfect binary tree" with $n$ nodes and height $h$; this assumption does not affect the correctness of the result. +Let's perform a more accurate calculation. To simplify the analysis, assume a "perfect binary tree" with $n$ nodes and height $h$; this assumption does not affect the correctness of the result. ![Node count at each level of a perfect binary tree](build_heap.assets/heapify_operations_count.png) -As shown in the figure above, the maximum number of iterations for a node's "top-to-bottom heapify" equals the distance from that node to the leaf nodes, which is precisely the "node height." Therefore, we can sum the "number of nodes $\times$ node height" at each level to **obtain the total number of heapify iterations for all nodes**. +As shown in the figure above, the maximum number of iterations for a node's "top-to-bottom heapify" equals the distance from that node to a leaf node, which is precisely the node's height. Therefore, we can sum the "number of nodes $\times$ node height" at each level to **obtain the total number of heapify iterations for all nodes**. $$ T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \dots + 2^{(h-1)}\times1 $$ -To simplify the above expression, we need to use sequence knowledge from high school. First, multiply $T(h)$ by $2$ to get: +Simplifying the expression above requires some high-school sequence algebra. First, multiply $T(h)$ by $2$ to get: $$ \begin{aligned} @@ -55,7 +55,7 @@ T(h) & = 2^0h + 2^1(h-1) + 2^2(h-2) + \dots + 2^{h-1}\times1 \newline \end{aligned} $$ -Using the method of differences, subtract the first equation $T(h)$ from the second equation $2 T(h)$ to get: +Using subtraction of shifted sums, subtract the first equation $T(h)$ from the second equation $2 T(h)$ to get: $$ 2T(h) - T(h) = T(h) = -2^0h + 2^1 + 2^2 + \dots + 2^{h-1} + 2^h diff --git a/en/docs/chapter_heap/heap.md b/en/docs/chapter_heap/heap.md index 0d39ce5b3..8c615f8c4 100644 --- a/en/docs/chapter_heap/heap.md +++ b/en/docs/chapter_heap/heap.md @@ -15,7 +15,7 @@ As a special case of a complete binary tree, heaps have the following characteri ## Common Heap Operations -It should be noted that many programming languages provide a priority queue, which is an abstract data structure defined as a queue with priority sorting. +It should be noted that many programming languages provide a priority queue, an abstract data structure defined as a queue whose elements are ordered by priority. In fact, **heaps are typically used to implement priority queues, with max heaps corresponding to priority queues where elements are dequeued in descending order**. From a usage perspective, we can regard "priority queue" and "heap" as equivalent data structures. Therefore, this book does not make a special distinction between the two and uniformly refers to them as "heap." @@ -414,13 +414,13 @@ Similar to "ascending order" and "descending order" in sorting algorithms, we ca ## Implementation of the Heap -The following implementation is of a max heap. To convert it to a min heap, simply invert all size logic comparisons (for example, replace $\geq$ with $\leq$). Interested readers are encouraged to implement this on their own. +The following implementation is for a max heap. To convert it to a min heap, simply reverse all comparison logic related to ordering (for example, replace $\geq$ with $\leq$). Interested readers are encouraged to implement this on their own. ### Heap Storage and Representation As mentioned in the "Binary Tree" chapter, complete binary trees are well-suited for array representation. Since heaps are a type of complete binary tree, **we will use arrays to store heaps**. -When representing a binary tree with an array, elements represent node values, and indexes represent node positions in the binary tree. **Node pointers are implemented through index mapping formulas**. +When representing a binary tree with an array, elements represent node values, and indexes represent node positions in the binary tree. **Parent-child relationships are represented through index-mapping formulas**. As shown in the figure below, given an index $i$, the index of its left child is $2i + 1$, the index of its right child is $2i + 2$, and the index of its parent is $(i - 1) / 2$ (floor division). When an index is out of bounds, it indicates a null node or that the node does not exist. @@ -442,9 +442,9 @@ The heap top element is the root node of the binary tree, which is also the firs ### Inserting an Element Into the Heap -Given an element `val`, we first add it to the bottom of the heap. After addition, since `val` may be larger than other elements in the heap, the heap's property may be violated. **Therefore, it's necessary to repair the path from the inserted node to the root node**. This operation is called heapify. +Given an element `val`, we first add it to the bottom of the heap. After insertion, because `val` may be larger than other elements in the heap, the heap property may be violated. **Therefore, we need to restore the heap property along the path from the inserted node to the root**. This operation is called heapify. -Starting from the inserted node, **perform heapify from bottom to top**. As shown in the figure below, we compare the inserted node with its parent node, and if the inserted node is larger, swap them. Then continue this operation, repairing nodes in the heap from bottom to top until we pass the root node or encounter a node that does not need swapping. +Starting from the inserted node, **perform heapify from bottom to top**. As shown in the figure below, we compare the inserted node with its parent, and if the inserted node is larger, we swap them. We continue this process from bottom to top until we move past the root or reach a node that no longer needs to be swapped. === "<1>" ![Steps of inserting an element into the heap](heap.assets/heap_push_step1.png) @@ -527,6 +527,6 @@ Similar to the element insertion operation, the time complexity of the heap top ## Common Applications of Heaps -- **Priority queue**: Heaps are typically the preferred data structure for implementing priority queues, with both enqueue and dequeue operations having a time complexity of $O(\log n)$, and the heap construction operation having $O(n)$, all of which are highly efficient. +- **Priority queue**: Heaps are typically the preferred data structure for implementing priority queues. The time complexity of both enqueue and dequeue operations is $O(\log n)$, and heap construction has a time complexity of $O(n)$, making these operations highly efficient. - **Heap sort**: Given a set of data, we can build a heap with them and then continuously perform element removal operations to obtain sorted data. However, we usually use a more elegant approach to implement heap sort, as detailed in the "Heap Sort" chapter. -- **Getting the largest $k$ elements**: This is a classic algorithm problem and also a typical application, such as selecting the top 10 trending news for Weibo hot search, selecting the top 10 best-selling products, etc. +- **Getting the largest $k$ elements**: This is a classic algorithm problem and also a typical application, such as selecting the top 10 trending news items for Weibo Hot Search or the top 10 best-selling products. diff --git a/en/docs/chapter_heap/index.md b/en/docs/chapter_heap/index.md index 405cfc4a1..9dfaf0525 100644 --- a/en/docs/chapter_heap/index.md +++ b/en/docs/chapter_heap/index.md @@ -4,6 +4,6 @@ !!! abstract - Heaps are like mountain peaks, layered and undulating, each with its unique form. + Heaps are like mountain peaks, rising layer upon layer, each with a distinct shape. The peaks rise and fall at varying heights, yet the tallest peak always catches the eye first. diff --git a/en/docs/chapter_heap/summary.md b/en/docs/chapter_heap/summary.md index bea6dbf1d..61e69a239 100644 --- a/en/docs/chapter_heap/summary.md +++ b/en/docs/chapter_heap/summary.md @@ -2,16 +2,16 @@ ### Key Review -- A heap is a complete binary tree that can be categorized as a max heap or min heap based on its property. The heap top element of a max heap (min heap) is the largest (smallest). -- A priority queue is defined as a queue with priority sorting, typically implemented using heaps. -- Common heap operations and their corresponding time complexities include: element insertion $O(\log n)$, heap top element removal $O(\log n)$, and accessing the heap top element $O(1)$. +- A heap is a complete binary tree. Depending on the property it satisfies, it can be classified as either a max heap or a min heap. The top element of a max heap (min heap) is the largest (smallest) element. +- A priority queue is a queue in which elements are dequeued according to priority, and it is typically implemented using a heap. +- Common heap operations and their corresponding time complexities include inserting an element $O(\log n)$, removing the top element $O(\log n)$, and accessing the top element $O(1)$. - Complete binary trees are well-suited for array representation, so we typically use arrays to store heaps. - Heapify operations are used to maintain the heap property and are employed in both element insertion and removal operations. -- The time complexity of building a heap with $n$ input elements can be optimized to $O(n)$, which is highly efficient. -- Top-k is a classic algorithm problem that can be efficiently solved using the heap data structure, with a time complexity of $O(n \log k)$. +- Building a heap from $n$ input elements can be optimized to $O(n)$, which is highly efficient. +- Top-k is a classic algorithmic problem that can be solved efficiently using a heap, with a time complexity of $O(n \log k)$. ### Q & A -**Q**: Are the "heap" in data structures and the "heap" in memory management the same concept? +**Q**: Does the term "heap" in data structures mean the same thing as "heap" in memory management? -The two are not the same concept; they just happen to share the name "heap." The heap in computer system memory is part of dynamic memory allocation, where programs can use it to store data during runtime. Programs can request a certain amount of heap memory to store complex structures such as objects and arrays. When this data is no longer needed, the program needs to release this memory to prevent memory leaks. Compared to stack memory, heap memory management and usage require more caution, as improper use can lead to issues such as memory leaks and dangling pointers. +They are not the same concept; they simply share the same name. In computer systems, the heap is part of dynamic memory allocation, and programs can use it to store data at runtime. A program can request a certain amount of heap memory to store complex structures such as objects and arrays. When the data is no longer needed, the program must release that memory to prevent memory leaks. Compared with stack memory, heap memory requires more careful management and use; improper handling can lead to problems such as memory leaks and dangling pointers. diff --git a/en/docs/chapter_heap/top_k.md b/en/docs/chapter_heap/top_k.md index c4a800ff5..08ace4d22 100644 --- a/en/docs/chapter_heap/top_k.md +++ b/en/docs/chapter_heap/top_k.md @@ -1,16 +1,16 @@ -# Top-K Problem +# Top-k Problem !!! question Given an unordered array `nums` of length $n$, return the largest $k$ elements in the array. -For this problem, we'll first introduce two solutions with relatively straightforward approaches, then introduce a more efficient heap-based solution. +For this problem, we will first introduce two relatively straightforward solutions, followed by a more efficient heap-based solution. ## Method 1: Iterative Selection We can perform $k$ rounds of traversal as shown in the figure below, extracting the $1^{st}$, $2^{nd}$, $\dots$, $k^{th}$ largest elements in each round, with a time complexity of $O(nk)$. -This method is only suitable when $k \ll n$, because when $k$ is close to $n$, the time complexity approaches $O(n^2)$, which is very time-consuming. +This method is only suitable when $k \ll n$, because when $k$ is close to $n$, the time complexity approaches $O(n^2)$, making it very inefficient. ![Traversing to find the largest k elements](top_k.assets/top_k_traversal.png) @@ -22,13 +22,13 @@ This method is only suitable when $k \ll n$, because when $k$ is close to $n$, t As shown in the figure below, we can first sort the array `nums`, then return the rightmost $k$ elements, with a time complexity of $O(n \log n)$. -Clearly, this method "overachieves" the task, as we only need to find the largest $k$ elements, without needing to sort the other elements. +Clearly, this method does more work than necessary, because we only need to find the largest $k$ elements rather than sort the other elements. ![Sorting to find the largest k elements](top_k.assets/top_k_sorting.png) ## Method 3: Heap -We can solve the Top-k problem more efficiently using heaps, with the process shown in the figure below. +We can solve the Top-k problem more efficiently with a heap, as shown in the figure below. 1. Initialize a min heap, where the heap top element is the smallest. 2. First, insert the first $k$ elements of the array into the heap in sequence. @@ -70,4 +70,4 @@ Example code is as follows: A total of $n$ rounds of heap insertions and removals are performed, with the heap's maximum length being $k$, so the time complexity is $O(n \log k)$. This method is very efficient; when $k$ is small, the time complexity approaches $O(n)$; when $k$ is large, the time complexity does not exceed $O(n \log n)$. -Additionally, this method is suitable for dynamic data stream scenarios. By continuously adding data, we can maintain the elements in the heap, thus achieving dynamic updates of the largest $k$ elements. +Additionally, this method is well suited to dynamic data streams. As new data arrives, we can continuously maintain the elements in the heap, enabling dynamic updates to the largest $k$ elements. diff --git a/en/docs/chapter_hello_algo/index.md b/en/docs/chapter_hello_algo/index.md index 7d398ed55..e86f62e1a 100644 --- a/en/docs/chapter_hello_algo/index.md +++ b/en/docs/chapter_hello_algo/index.md @@ -3,17 +3,17 @@ comments: true icon: material/rocket-launch-outline --- -# Before Starting +# Preface A few years ago, I shared the "Sword for Offer" problem solutions on LeetCode, receiving encouragement and support from many readers. During interactions with readers, the most frequently asked question I encountered was "how to get started with algorithms." Gradually, I developed a keen interest in this question. Diving straight into problem-solving seems to be the most popular approach—it's simple, direct, and effective. However, problem-solving is like playing Minesweeper: those with strong self-learning abilities can successfully defuse the mines one by one, while those with insufficient foundations may end up bruised and battered, retreating step by step in frustration. Reading through textbooks is also a common practice, but for job seekers, graduation theses, resume submissions, and preparations for written tests and interviews have already consumed most of their energy, making working through thick books an arduous challenge. -If you're facing similar struggles, then it's fortunate that this book has "found" you. This book is my answer to this question—even if it may not be the optimal solution, it is at least a positive attempt. While this book alone won't directly land you a job offer, it will guide you to explore the "knowledge map" of data structures and algorithms, help you understand the shapes, sizes, and distributions of different "mines," and enable you to master various "mine-clearing methods." With these skills, I believe you can tackle problems and read technical literature more confidently, gradually building a complete knowledge system. +If you're facing similar struggles, then it's fortunate that this book has "found" you. This book is my answer to this question—even if it may not be the optimal solution, it is at least a positive attempt. While this book alone won't directly land you a job offer, it will guide you through the "landscape" of data structures and algorithms, help you understand the shapes, sizes, and distributions of different "mines," and enable you to master various "mine-clearing methods." With these skills, I believe you can tackle problems and read technical literature more confidently, gradually building a complete knowledge system. I deeply agree with Professor Feynman's words: "Knowledge isn't free. You have to pay attention." In this sense, this book is not entirely "free." In order to live up to the precious "attention" you invest in this book, I will do my utmost and devote my greatest "attention" to completing this work. -I'm acutely aware of my limited knowledge and shallow expertise. Although the content of this book has been refined over a period of time, there are certainly still many errors, and I sincerely welcome critiques and corrections from teachers and fellow students. +I'm keenly aware of the limits of my knowledge and experience. Although the content of this book has been refined over a period of time, there are certainly still many errors, and I sincerely welcome critiques and corrections from teachers and fellow students. ![Hello Algorithms](../assets/covers/chapter_hello_algo.jpg){ class="cover-image" } @@ -21,10 +21,10 @@ I'm acutely aware of my limited knowledge and shallow expertise. Although the co

Hello, Algorithms!

-The advent of computers has brought tremendous changes to the world. With their high-speed computing capabilities and excellent programmability, they have become the ideal medium for executing algorithms and processing data. Whether it's the realistic graphics in video games, the intelligent decision-making in autonomous driving, AlphaGo's brilliant Go matches, or ChatGPT's natural interactions, these applications are all exquisite interpretations of algorithms on computers. +The advent of computers has brought tremendous changes to the world. With their high-speed computing capabilities and excellent programmability, they have become the ideal medium for executing algorithms and processing data. Whether it's the realistic graphics in video games, the intelligent decision-making in autonomous driving, AlphaGo's brilliant Go matches, or ChatGPT's natural interactions, these applications are all striking demonstrations of algorithms at work on computers. In fact, before the advent of computers, algorithms and data structures already existed in every corner of the world. Early algorithms were relatively simple, such as ancient counting methods and tool-making procedures. As civilization progressed, algorithms gradually became more refined and complex. From the ingenious craftsmanship of master artisans, to industrial products that liberate productive forces, to the scientific laws governing the operation of the universe, behind almost every ordinary or astonishing thing lies ingenious algorithmic thinking. Similarly, data structures are everywhere: from large-scale social networks to small subway systems, many systems can be modeled as "graphs"; from a nation to a family, the primary organizational forms of society exhibit characteristics of "trees"; winter clothing is like a "stack," where the first item put on is the last to be taken off; a badminton tube is like a "queue," with items inserted at one end and retrieved from the other; a dictionary is like a "hash table," enabling quick lookup of target entries. -This book aims to help readers understand the core concepts of algorithms and data structures through clear and accessible animated illustrations and runnable code examples, and to implement them through programming. Building on this foundation, the book endeavors to reveal the vivid manifestations of algorithms in the complex world and showcase the beauty of algorithms. I hope this book can be of help to you! +This book aims to help readers understand the core concepts of algorithms and data structures through clear and accessible animated illustrations and runnable code examples, and to implement them in code. Building on this foundation, the book endeavors to reveal the vivid manifestations of algorithms in the complex world and showcase the beauty of algorithms. I hope this book can be of help to you! diff --git a/en/docs/chapter_introduction/algorithms_are_everywhere.md b/en/docs/chapter_introduction/algorithms_are_everywhere.md index b6f7008e1..659d760e6 100644 --- a/en/docs/chapter_introduction/algorithms_are_everywhere.md +++ b/en/docs/chapter_introduction/algorithms_are_everywhere.md @@ -2,11 +2,11 @@ When we hear the term "algorithm," we naturally think of mathematics. However, many algorithms do not involve complex mathematics but rely more on basic logic, which can be seen everywhere in our daily lives. -Before we start discussing about algorithms officially, there's an interesting fact worth sharing: **you've learned many algorithms unconsciously and are used to applying them in your daily life**. Here, I will give a few specific examples to prove this point. +Before we formally explore algorithms, here's an interesting fact worth sharing: **you have already learned many algorithms without realizing it, and you are used to applying them in daily life**. Let me give a few specific examples to illustrate this point. **Example 1: Looking Up a Dictionary**. In an English dictionary, words are listed alphabetically. Assuming we're searching for a word that starts with the letter $r$, this is typically done in the following way: -1. Open the dictionary to about halfway and check the first vocabulary of the page, let's say the letter starts with $m$. +1. Open the dictionary to about halfway and check the first word on that page; suppose it starts with the letter $m$. 2. Since $r$ comes after $m$ in the alphabet, the first half can be ignored and the search space is narrowed down to the second half. 3. Repeat steps `1.` and `2.` until you find the page where the word starts with $r$. @@ -27,7 +27,7 @@ Before we start discussing about algorithms officially, there's an interesting f Looking up a dictionary, an essential skill for elementary school students is actually the famous "Binary Search" algorithm. From a data structure perspective, we can consider the dictionary as a sorted "array"; from an algorithmic perspective, the series of actions taken to look up a word in the dictionary can be viewed as the algorithm "Binary Search." -**Example 2: Organizing Card Deck**. When playing cards, we need to arrange the cards in our hands in ascending order, as shown in the following process. +**Example 2: Organizing Playing Cards**. When playing cards, we need to arrange the cards in our hands in ascending order, as shown in the following process. 1. Divide the playing cards into "ordered" and "unordered" sections, assuming initially the leftmost card is already in order. 2. Take out a card from the unordered section and insert it into the correct position in the ordered section; after this, the leftmost two cards are in order. @@ -35,11 +35,11 @@ Looking up a dictionary, an essential skill for elementary school students is ac ![Process of sorting a deck of cards](algorithms_are_everywhere.assets/playing_cards_sorting.png) -The above method of organizing playing cards is practically the "Insertion Sort" algorithm, which is very efficient for small datasets. Many programming languages' sorting functions include the insertion sort. +The above method of organizing playing cards is essentially the "Insertion Sort" algorithm, which is very efficient for small datasets. Many programming languages' built-in sorting implementations use insertion sort internally. **Example 3: Making Change**. Assume making a purchase of $69$ at a supermarket. If you give the cashier $100$, they will need to provide you with $31$ in change. This process can be clearly understood as illustrated in the figure below. -1. The options are currencies valued below $31$, including $1$, $5$, $10$, and $20$. +1. The available denominations smaller than $31$ are $1$, $5$, $10$, and $20$. 2. Take out the largest $20$ from the options, leaving $31 - 20 = 11$. 3. Take out the largest $10$ from the remaining options, leaving $11 - 10 = 1$. 4. Take out the largest $1$ from the remaining options, leaving $1 - 1 = 0$. @@ -47,10 +47,10 @@ The above method of organizing playing cards is practically the "Insertion Sort" ![Process of making change](algorithms_are_everywhere.assets/greedy_change.png) -In the steps described, we choose the best option at each stage by utilizing the largest denomination available, which leads to an effective change-making strategy. From a data structures and algorithms perspective, this approach is known as a "Greedy" algorithm. +In the steps above, we choose what seems to be the best option at each stage by using the largest denomination available, which leads to an effective way to make change. From a data structures and algorithms perspective, this approach is known as a "Greedy" algorithm. From cooking a meal to interstellar travel, almost all problem-solving involves algorithms. The advent of computers allows us to store data structures in memory and write code to call the CPU and GPU to execute algorithms. In this way, we can transfer real-life problems to computers and solve various complex issues in a more efficient way. !!! tip - If you are still confused about concepts like data structures, algorithms, arrays, and binary searches, I encourage you to keep reading. This book will gently guide you into the realm of understanding data structures and algorithms. + If concepts such as data structures, algorithms, arrays, and binary search still feel only half-familiar, keep reading. This book will guide you into the world of data structures and algorithms. diff --git a/en/docs/chapter_introduction/summary.md b/en/docs/chapter_introduction/summary.md index 75258b946..62bea4d3c 100644 --- a/en/docs/chapter_introduction/summary.md +++ b/en/docs/chapter_introduction/summary.md @@ -2,13 +2,13 @@ ### Key Review -- Algorithms are ubiquitous in daily life and are not distant, esoteric knowledge. In fact, we have already learned many algorithms unconsciously and use them to solve problems big and small in life. +- Algorithms are ubiquitous in daily life and are not some distant, esoteric body of knowledge. In fact, we have already learned many algorithms unconsciously and use them to solve problems big and small in life. - The principle of looking up a dictionary is consistent with the binary search algorithm. Binary search embodies the important algorithmic idea of divide and conquer. - The process of organizing playing cards is very similar to the insertion sort algorithm. Insertion sort is suitable for sorting small datasets. - The steps of making change are essentially a greedy algorithm, where the best choice is made at each step based on the current situation. -- An algorithm is a set of instructions or operational steps that solves a specific problem within a finite amount of time, while a data structure is the way computers organize and store data. +- An algorithm is a set of instructions or operational steps that solves a specific problem within a finite amount of time, while a data structure is a way of organizing and storing data in a computer. - Data structures and algorithms are closely connected. Data structures are the foundation of algorithms, and algorithms breathe life into data structures. -- We can compare data structures and algorithms to assembling building blocks. The blocks represent data, the shape and connection method of the blocks represent the data structure, and the steps to assemble the blocks correspond to the algorithm. +- We can compare data structures and algorithms to assembling building blocks. The blocks represent data, the way they are shaped and connected represents the data structure, and the steps used to assemble them correspond to the algorithm. ### Q & A @@ -16,9 +16,9 @@ If we compare specific work skills to "techniques" in martial arts, then fundamental subjects should be more like "internal skills". -I believe the significance of learning algorithms (and other fundamental subjects) is not to implement them from scratch at work, but rather to be able to make professional reactions and judgments when solving problems based on the knowledge learned, thereby improving the overall quality of work. Here is a simple example. Every programming language has a built-in sorting function: +I believe the significance of learning algorithms (and other fundamental subjects) is not that you will need to implement them from scratch at work, but that the knowledge you gain enables you to make sound professional judgments when solving problems, thereby improving the overall quality of your work. Here is a simple example. Every programming language has a built-in sorting function: - If we have not studied data structures and algorithms, we might simply feed any given data to this sorting function. It runs smoothly with good performance, and there doesn't seem to be any problem. - But if we have studied algorithms, we would know that the time complexity of the built-in sorting function is $O(n \log n)$. However, if the given data consists of integers with a fixed number of digits (such as student IDs), we can use the more efficient "radix sort", reducing the time complexity to $O(nk)$, where $k$ is the number of digits. When the data volume is very large, the saved running time can create significant value (reduced costs, improved experience, etc.). -In the field of engineering, a large number of problems are difficult to reach optimal solutions, and many problems are only solved "approximately". The difficulty of a problem depends on one hand on the nature of the problem itself, and on the other hand on the knowledge reserve of the person observing the problem. The more complete a person's knowledge and the more experience they have, the deeper their analysis of the problem will be, and the more elegantly the problem can be solved. +In engineering, many problems are difficult to solve optimally, and many others are only solved "well enough." The difficulty of a problem depends, on the one hand, on the nature of the problem itself and, on the other hand, on the knowledge of the person examining it. The more complete a person's knowledge and the more experience they have, the deeper their analysis will be, and the more elegantly the problem can be solved. diff --git a/en/docs/chapter_introduction/what_is_dsa.md b/en/docs/chapter_introduction/what_is_dsa.md index fdf24966f..dbd43ab51 100644 --- a/en/docs/chapter_introduction/what_is_dsa.md +++ b/en/docs/chapter_introduction/what_is_dsa.md @@ -5,12 +5,12 @@ An algorithm is a set of instructions or operational steps that solves a specific problem within a finite amount of time. It has the following characteristics. - The problem is well-defined, with clear input and output definitions. -- It is feasible and can be completed within a finite number of steps, time, and memory space. +- It is feasible and can be completed with finite steps, time, and memory. - Each step has a definite meaning, and under the same input and operating conditions, the output is always the same. ## Data Structure Definition -A data structure is a way of organizing and storing data, covering the data content, relationships between data, and methods for data operations. It has the following design objectives. +A data structure is a way of organizing and storing data, including the data itself, the relationships between data elements, and the methods used to operate on them. It has the following design objectives. - Occupy as little space as possible to save computer memory. - Data operations should be as fast as possible, covering data access, addition, deletion, update, etc. @@ -46,7 +46,7 @@ The detailed correspondence between the two is shown in the table below. | Algorithm | A series of operational steps to assemble the blocks into the target form | | Output data | Building block model | -It is worth noting that data structures and algorithms are independent of programming languages. For this reason, this book is able to provide implementations based on multiple programming languages. +It is worth noting that data structures and algorithms are independent of programming languages. That is why this book can provide implementations in multiple programming languages. !!! tip "Conventional abbreviation" diff --git a/en/docs/chapter_preface/about_the_book.md b/en/docs/chapter_preface/about_the_book.md index eb2b14c5a..76520a92c 100644 --- a/en/docs/chapter_preface/about_the_book.md +++ b/en/docs/chapter_preface/about_the_book.md @@ -2,53 +2,53 @@ This project aims to create an open-source, free, beginner-friendly introductory tutorial on data structures and algorithms. -- The entire book uses animated illustrations, with clear and easy-to-understand content and a smooth learning curve, guiding beginners to explore the knowledge map of data structures and algorithms. +- The entire book uses animated illustrations, with clear and easy-to-understand content and a smooth learning curve, guiding beginners through the landscape of data structures and algorithms. - The source code can be run with one click, helping readers improve their programming skills through practice and understand how algorithms work and the underlying implementation of data structures. - We encourage readers to learn from each other, and everyone is welcome to ask questions and share insights in the comments section, making progress together through discussion and exchange. ## Target Audience -If you are an algorithm beginner who has never been exposed to algorithms, or if you already have some problem-solving experience and have a vague understanding of data structures and algorithms, oscillating between knowing and not knowing, then this book is tailor-made for you! +If you are an algorithm beginner who has never studied algorithms, or if you already have some problem-solving experience but only a hazy understanding of data structures and algorithms, then this book is tailor-made for you! If you have already accumulated a certain amount of problem-solving experience and are familiar with most question types, this book can help you review and organize your algorithm knowledge system, and the repository's source code can be used as a "problem-solving toolkit" or "algorithm dictionary." -If you are an algorithm "expert," we look forward to receiving your valuable suggestions, or [participating in creation together](https://www.hello-algo.com/chapter_appendix/contribution/). +If you are an algorithm "expert," we look forward to receiving your valuable suggestions, or [joining us as a contributor](https://www.hello-algo.com/chapter_appendix/contribution/). !!! success "Prerequisites" - You need to have at least a programming foundation in any language, and be able to read and write simple code. + You need basic programming knowledge in at least one language and the ability to read and write simple code. ## Content Structure The main content of this book is shown in the figure below. - **Complexity analysis**: Evaluation dimensions and methods for data structures and algorithms. Methods for calculating time complexity and space complexity, common types, examples, etc. -- **Data structures**: Classification methods for basic data types and data structures. The definition, advantages and disadvantages, common operations, common types, typical applications, implementation methods, etc. of data structures such as arrays, linked lists, stacks, queues, hash tables, trees, heaps, and graphs. +- **Data structures**: Classification methods for basic data types and data structures. Definitions, advantages and disadvantages, common operations, common types, typical applications, implementation methods, and more for data structures such as arrays, linked lists, stacks, queues, hash tables, trees, heaps, and graphs. - **Algorithms**: The definition, advantages and disadvantages, efficiency, application scenarios, problem-solving steps, and example problems of algorithms such as searching, sorting, divide and conquer, backtracking, dynamic programming, and greedy algorithms. ![Main content of this book](about_the_book.assets/hello_algo_mindmap.png) ## Acknowledgements -This book has been continuously improved through the joint efforts of many contributors in the open-source community. Thanks to every contributor who invested time and effort, they are (in the order automatically generated by 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, IsChristina, xBLACKICEx, guowei-gong, Cathay-Chen, pengchzn, QiLOL, magentaqin, hello-ikun, JoseHung, qualifier1024, thomasq0, sunshinesDL, L-Super, Guanngxu, Transmigration-zhou, WSL0809, Slone123c, lhxsm, yuan0221, what-is-me, Shyam-Chen, theNefelibatas, 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, MolDuM, Nigh, Dr-XYZ, XC-Zero, reeswell, PXG-XPG, NI-SW, Horbin-Magician, Enlightenus, YangXuanyi, beatrix-chan, DullSword, xjr7670, jiaxianhua, qq909244296, iStig, boloboloda, hts0000, gledfish, wenjianmin, keshida, kilikilikid, lclc6, lwbaptx, linyejoe2, liuxjerry, llql1211, fbigm, echo1937, szu17dmy, dshlstarr, Yucao-cy, coderlef, czruby, bongbongbakudan, beintentional, ZongYangL, ZhongYuuu, ZhongGuanbin, hezhizhen, linzeyan, ZJKung, 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, 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, GaochaoZhu, hopkings2008, yang-le, realwujing, Evilrabbit520, Umer-Jahangir, Turing-1024-Lee, Suremotoo, paoxiaomooo, Chieko-Seren, Allen-Scai, ymmmas, Risuntsy, Richard-Zhang1019, RafaelCaso, qingpeng9802, primexiao, Urbaner3, zhongfq, nidhoggfgg, MwumLi, CreatorMetaSky, martinx, ZnYang2018, hugtyftg, logan-qiu, psychelzh, Keynman, KeiichiKasai, and KawaiiAsh. +This book has been continuously improved through the joint efforts of many contributors in the open-source community. Thanks to every contributor who invested time and effort, they are (in the order automatically generated by 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, and KeiichiKasai. -The code review work for this book was completed by coderonion, curtishd, Gonglja, gvenusleo, hpstory, justin-tse, khoaxuantu, krahets, night-cruise, nuomi1, Reanon and rongyi (in alphabetical order). Thanks to them for the time and effort they put in, it is they who ensure the standardization and unity of code in various languages. +The code review work for this book was completed by coderonion, curtishd, Gonglja, gvenusleo, hpstory, justin-tse, khoaxuantu, krahets, night-cruise, nuomi1, Reanon and rongyi (in alphabetical order). Thanks to them for the time and effort they put in; they helped keep the code consistent and standardized across the different language versions. -The Traditional Chinese version of this book was reviewed by Shyam-Chen and Dr-XYZ, the English version was reviewed by yuelinxin, K3v123, QiLOL, Phoenix0415, SamJin98, yanedie, RafaelCaso, pengchzn, thomasq0 and magentaqin, and the Japanese edition was reviewed by eltociear. It is because of their continuous contributions that this book can serve a wider readership, and we thank them. +The English version of this book was reviewed by yuelinxin, K3v123, magentaqin, QiLOL, Phoenix0415, SamJin98, yanedie, RafaelCaso, pengchzn and thomasq0; the Japanese version was reviewed by eltociear; the Russian version was reviewed by И. А. Шевкун and Yuyan Huang; and the Traditional Chinese version was reviewed by Shyam-Chen and Dr-XYZ. Thanks to their contributions, this book is able to serve a broader readership, and we are deeply grateful to them. The ePub ebook generation tool for this book was developed by zhongfq. We thank him for his contribution, which provides readers with a more flexible way to read. During the creation of this book, I received help from many people. - Thanks to my mentor at the company, Dr. Li Xi, who encouraged me to "take action quickly" during a conversation, strengthening my determination to write this book; -- Thanks to my girlfriend Bubble as the first reader of this book, who provided many valuable suggestions from the perspective of an algorithm beginner, making this book more suitable for novices to read; +- Thanks to my girlfriend Bubble, the first reader of this book, who provided many valuable suggestions from the perspective of an algorithm beginner, making this book more approachable for beginners; - Thanks to Tengbao, Qibao, and Feibao for coming up with a creative name for this book, evoking everyone's fond memories of writing their first line of code "Hello World!"; - Thanks to Xiaoquan for providing professional help in intellectual property rights, which played an important role in the improvement of this open-source book; -- Thanks to Sutong for designing the beautiful cover and logo for this book, and for patiently making revisions multiple times driven by my obsessive-compulsive disorder; -- Thanks to @squidfunk for the typesetting suggestions, as well as for developing the open-source documentation theme [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material/tree/master). +- Thanks to Sutong for designing the beautiful cover and logo for this book, and for patiently revising them many times at my perfectionist insistence; +- Thanks to @squidfunk for the typesetting suggestions, as well as for developing the open-source documentation theme [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material). -During the writing process, I read many textbooks and articles on data structures and algorithms. These works provided excellent examples for this book and ensured the accuracy and quality of the book's content. I would like to thank all the teachers and predecessors for their outstanding contributions! +During the writing process, I read many textbooks and articles on data structures and algorithms. These works served as excellent models for this book and helped ensure the accuracy and quality of its content. I would like to thank all the teachers and predecessors for their outstanding contributions! -This book advocates a learning method that combines hands and brain, and in this regard I was deeply inspired by [Dive into Deep Learning](https://github.com/d2l-ai/d2l-zh). I highly recommend this excellent work to all readers. +This book advocates a hands-on approach to learning, and in this respect I was deeply inspired by [Dive into Deep Learning](https://github.com/d2l-ai/d2l-zh). I highly recommend this excellent work to all readers. -**Heartfelt thanks to my parents, it is your support and encouragement that has given me the opportunity to do this interesting thing**. +**Heartfelt thanks to my parents. It is your support and encouragement that gave me the opportunity to pursue this enjoyable project**. diff --git a/en/docs/chapter_preface/suggestions.md b/en/docs/chapter_preface/suggestions.md index b21ed2f1d..10e9d82b8 100644 --- a/en/docs/chapter_preface/suggestions.md +++ b/en/docs/chapter_preface/suggestions.md @@ -6,12 +6,12 @@ ## Writing Style Conventions -- Titles marked with `*` are optional sections with relatively difficult content. If you have limited time, you can skip them first. -- Technical terms will be in bold (in paper and PDF versions) or underlined (in web versions), such as array. It is recommended to memorize them for reading literature. +- Sections marked with `*` after the title are optional and somewhat more challenging. If you're short on time, you can skip them on your first pass. +- Technical terms are shown in bold (in the print and PDF editions) or underlined (in the web edition), such as array. They are worth remembering, as they will help when reading technical literature. - Key content and summary statements will be **bolded**, and such text deserves special attention. - Words and phrases with specific meanings will be marked with "quotation marks" to avoid ambiguity. -- When it comes to nouns that are inconsistent between programming languages, this book uses Python as the standard, for example, using `None` to represent "null". -- This book partially abandons the comment conventions of programming languages in favor of more compact content layout. Comments are mainly divided into three types: title comments, content comments, and multi-line comments. +- When terminology differs across programming languages, this book follows Python conventions; for example, it uses `None` to represent "null". +- This book partially relaxes conventional programming-language comment styles in favor of a more compact layout. Comments are mainly divided into three types: title comments, content comments, and multi-line comments. === "Python" @@ -180,9 +180,9 @@ ## Learning Efficiently with Animated Illustrations -Compared to text, videos and images have higher information density and structural organization, making them easier to understand. In this book, **key and difficult knowledge will mainly be presented in the form of animated illustrations**, with text serving as explanation and supplement. +Compared with plain text, videos and images have higher information density and a clearer structure, making them easier to understand. In this book, **key concepts and challenging topics are presented mainly through animated illustrations**, with text serving as explanation and supplement. -If you find that a section of content provides animated illustrations as shown in the figure below while reading this book, **please focus on the illustrations first, with text as a supplement**, and combine the two to understand the content. +If, while reading this book, you encounter an animated illustration like the one shown below, **treat the illustration as primary and the text as supplementary**, and use both together to understand the content. ![Example of animated illustrations](../index.assets/animation.gif) @@ -192,13 +192,13 @@ The accompanying code for this book is hosted in the [GitHub repository](https:/ If time permits, **it is recommended that you type out the code yourself**. If you have limited study time, please at least read through and run all the code. -Compared to reading code, the process of writing code often brings more rewards. **Learning by doing is the real learning**. +Compared with simply reading code, writing it yourself often brings greater rewards. **Hands-on practice is where real learning happens**. ![Example of running code](../index.assets/running_code.gif) -The preliminary work for running code is mainly divided into three steps. +Getting the code running mainly involves three preliminary steps. -**Step 1: Install the local programming environment**. Please follow the [tutorial](https://www.hello-algo.com/chapter_appendix/installation/) shown in the appendix for installation. If already installed, you can skip this step. +**Step 1: Install the local programming environment**. Please follow the [tutorial](https://www.hello-algo.com/chapter_appendix/installation/) in the appendix. If it is already installed, you can skip this step. **Step 2: Clone or download the code repository**. Visit the [GitHub repository](https://github.com/krahets/hello-algo). If you have already installed [Git](https://git-scm.com/downloads), you can clone this repository with the following command: @@ -206,7 +206,7 @@ The preliminary work for running code is mainly divided into three steps. git clone https://github.com/krahets/hello-algo.git ``` -Of course, you can also click the "Download ZIP" button at the location shown in the figure below to directly download the code compressed package, and then extract it locally. +Alternatively, you can click the "Download ZIP" button shown below to download a ZIP archive of the repository directly and then extract it locally. ![Clone repository and download code](suggestions.assets/download_code.png) @@ -214,24 +214,24 @@ Of course, you can also click the "Download ZIP" button at the location shown in ![Code blocks and corresponding source code files](suggestions.assets/code_md_to_repo.png) -In addition to running code locally, **the web version also supports visual running of Python code** (implemented based on [pythontutor](https://pythontutor.com/)). As shown in the figure below, you can click "Visual Run" below the code block to expand the view and observe the execution process of the algorithm code; you can also click "Full Screen View" for a better viewing experience. +In addition to running code locally, **the web version also supports visual execution of Python code** (implemented based on [pythontutor](https://pythontutor.com/)). As shown in the figure below, you can click "Visual Run" below the code block to expand the view and observe the execution process of the algorithm code; you can also click "Full Screen View" for a better viewing experience. ![Visual running of Python code](suggestions.assets/pythontutor_example.png) ## Growing Together Through Questions and Discussions -When reading this book, please do not easily skip knowledge points that you have not learned well. **Feel free to ask your questions in the comments section**, and my friends and I will do our best to answer you, and generally reply within two days. +When reading this book, please do not skip over points that you still do not fully understand. **Feel free to ask your questions in the comments section**, and my friends and I will do our best to answer them, usually within two days. -As shown in the figure below, the web version has a comments section at the bottom of each chapter. I hope you will pay more attention to the content of the comments section. On the one hand, you can learn about the problems that everyone encounters, thus checking for omissions and stimulating deeper thinking. On the other hand, I hope you can generously answer other friends' questions, share your insights, and help others progress. +As shown in the figure below, the web version has a comments section at the bottom of each chapter. I encourage you to pay close attention to the discussions there. On the one hand, you can learn about the problems that others encounter, thereby filling gaps in your own understanding and prompting deeper thought. On the other hand, I hope you will generously answer other readers' questions, share your insights, and help others improve. ![Example of comments section](../index.assets/comment.gif) ## Algorithm Learning Roadmap -From an overall perspective, we can divide the process of learning data structures and algorithms into three stages. +Overall, we can divide the process of learning data structures and algorithms into three stages. 1. **Stage 1: Algorithm introduction**. We need to familiarize ourselves with the characteristics and usage of various data structures, and learn the principles, processes, uses, and efficiency of different algorithms. -2. **Stage 2: Practice algorithm problems**. It is recommended to start with popular problems, and accumulate at least 100 problems first, to familiarize yourself with mainstream algorithm problems. When first practicing problems, "knowledge forgetting" may be a challenge, but rest assured, this is very normal. We can review problems according to the "Ebbinghaus forgetting curve", and usually after 3-5 rounds of repetition, we can firmly remember them. For recommended problem lists and practice plans, please see this [GitHub repository](https://github.com/krahets/LeetCode-Book). +2. **Stage 2: Practice algorithm problems**. It is recommended to start with popular problems and solve at least 100 of them first, so that you become familiar with mainstream algorithm questions. When you first begin practicing problems, "knowledge forgetting" may feel like a challenge, but rest assured, this is very normal. We can review problems according to the "Ebbinghaus forgetting curve", and after 3-5 rounds of repetition, they usually stick firmly in memory. For recommended problem lists and practice plans, please see this [GitHub repository](https://github.com/krahets/LeetCode-Book). 3. **Stage 3: Building a knowledge system**. In terms of learning, we can read algorithm column articles, problem-solving frameworks, and algorithm textbooks to continuously enrich our knowledge system. In terms of practicing problems, we can try advanced problem-solving strategies, such as categorization by topic, one problem multiple solutions, one solution multiple problems, etc. Related problem-solving insights can be found in various communities. As shown in the figure below, the content of this book mainly covers "Stage 1", aiming to help you more efficiently carry out Stage 2 and Stage 3 learning. diff --git a/en/docs/chapter_preface/summary.md b/en/docs/chapter_preface/summary.md index 766d47d7c..f3f756955 100644 --- a/en/docs/chapter_preface/summary.md +++ b/en/docs/chapter_preface/summary.md @@ -2,9 +2,9 @@ ### Key Review -- The main audience of this book is algorithm beginners. If you already have a certain foundation, this book can help you systematically review algorithm knowledge, and the source code in the book can also be used as a "problem-solving toolkit." +- The main audience of this book is algorithm beginners. If you already have some background, this book can help you systematically review algorithm knowledge, and the source code in the book can also be used as a "problem-solving toolkit." - The content of the book mainly includes three parts: complexity analysis, data structures, and algorithms, covering most topics in this field. - For algorithm novices, reading an introductory book during the initial learning stage is crucial, as it can help you avoid many detours. -- The animated illustrations in the book are usually used to introduce key and difficult knowledge. When reading this book, you should pay more attention to these contents. +- The animated illustrations in the book are usually used to introduce key concepts and challenging topics. When reading this book, you should pay more attention to these topics. - Practice is the best way to learn programming. It is strongly recommended to run the source code and type the code yourself. - The web version of this book has a comments section for each chapter, where you are welcome to share your questions and insights at any time. diff --git a/en/docs/chapter_reference/index.md b/en/docs/chapter_reference/index.md index 2c015705f..1f9dfc05f 100644 --- a/en/docs/chapter_reference/index.md +++ b/en/docs/chapter_reference/index.md @@ -16,7 +16,7 @@ icon: material/bookshelf [6] Mark Allen Weiss, translated by Chen Yue. Data Structures and Algorithm Analysis in Java (Third Edition). -[7] Cheng Jie. Conversational Data Structures. +[7] Cheng Jie. Data Structures in Plain Language. [8] Wang Zheng. The Beauty of Data Structures and Algorithms. diff --git a/en/docs/chapter_searching/binary_search.md b/en/docs/chapter_searching/binary_search.md index 2a5e5570c..4276793cc 100644 --- a/en/docs/chapter_searching/binary_search.md +++ b/en/docs/chapter_searching/binary_search.md @@ -1,6 +1,6 @@ # Binary Search -Binary search is an efficient searching algorithm based on the divide-and-conquer strategy. It leverages the orderliness of data to reduce the search range by half in each round until the target element is found or the search interval becomes empty. +Binary search is an efficient search algorithm based on the divide-and-conquer strategy. It leverages the sorted order of the data to reduce the search range by half in each round until the target element is found or the search interval becomes empty. !!! question @@ -18,7 +18,7 @@ Next, perform the following two steps in a loop: 2. When `nums[m] > target`, it indicates that `target` is in the interval $[i, m - 1]$, so execute $j = m - 1$. 3. When `nums[m] = target`, it indicates that `target` has been found, so return index $m$. -If the array does not contain the target element, the search interval will eventually shrink to empty. In this case, return $-1$. +If the array does not contain the target element, the search interval will eventually become empty. In this case, return $-1$. === "<1>" ![Binary search process](binary_search.assets/binary_search_step1.png) @@ -41,7 +41,7 @@ If the array does not contain the target element, the search interval will event === "<7>" ![binary_search_step7](binary_search.assets/binary_search_step7.png) -It's worth noting that since both $i$ and $j$ are of `int` type, **$i + j$ may exceed the range of the `int` type**. To avoid large number overflow, we typically use the formula $m = \lfloor {i + (j - i) / 2} \rfloor$ to calculate the midpoint. +It's worth noting that since both $i$ and $j$ are of `int` type, **$i + j$ may exceed the range of the `int` type**. To avoid integer overflow, we typically use the formula $m = \lfloor {i + (j - i) / 2} \rfloor$ to calculate the midpoint. The code is shown below: @@ -49,13 +49,13 @@ The code is shown below: [file]{binary_search}-[class]{}-[func]{binary_search} ``` -**Time complexity is $O(\log n)$**: In the binary loop, the interval is reduced by half each round, so the number of loops is $\log_2 n$. +**Time complexity is $O(\log n)$**: In the binary search loop, the interval is reduced by half each round, so the number of iterations is $\log_2 n$. **Space complexity is $O(1)$**: Pointers $i$ and $j$ use constant-size space. ## Interval Representation Methods -In addition to the closed interval mentioned above, another common interval representation is the "left-closed right-open" interval, defined as $[0, n)$, meaning the left boundary includes itself while the right boundary does not. Under this representation, the interval $[i, j)$ is empty when $i = j$. +In addition to the closed interval mentioned above, another common interval representation is the "left-closed right-open" interval, defined as $[0, n)$, meaning that the left boundary is inclusive while the right boundary is exclusive. Under this representation, the interval $[i, j)$ is empty when $i = j$. We can implement a binary search algorithm with the same functionality based on this representation: @@ -71,13 +71,13 @@ Since both the left and right boundaries in the "closed interval" representation ## Advantages and Limitations -Binary search performs well in both time and space aspects. +Binary search offers good performance in both time and space. -- Binary search has high time efficiency. With large data volumes, the logarithmic time complexity has significant advantages. For example, when the data size $n = 2^{20}$, linear search requires $2^{20} = 1048576$ loop rounds, while binary search only needs $\log_2 2^{20} = 20$ rounds. +- Binary search has high time efficiency. With large data volumes, the logarithmic time complexity has significant advantages. For example, when the data size $n = 2^{20}$, linear search requires $2^{20} = 1048576$ iterations, while binary search only needs $\log_2 2^{20} = 20$ iterations. - Binary search requires no extra space. Compared to searching algorithms that require additional space (such as hash-based search), binary search is more space-efficient. However, binary search is not suitable for all situations, mainly for the following reasons: -- Binary search is only applicable to sorted data. If the input data is unsorted, sorting specifically to use binary search would be counterproductive, as sorting algorithms typically have a time complexity of $O(n \log n)$, which is higher than both linear search and binary search. For scenarios with frequent element insertions, maintaining array orderliness requires inserting elements at specific positions with a time complexity of $O(n)$, which is also very expensive. -- Binary search is only applicable to arrays. Binary search requires jump-style (non-contiguous) element access, and jump-style access has low efficiency in linked lists, making it unsuitable for linked lists or data structures based on linked list implementations. +- Binary search is only applicable to sorted data. If the input data is unsorted, sorting specifically to use binary search would be counterproductive, as sorting algorithms typically have a time complexity of $O(n \log n)$, which is higher than both linear search and binary search. For scenarios with frequent element insertions, keeping the array sorted requires inserting elements at specific positions with a time complexity of $O(n)$, which is also very expensive. +- Binary search is only applicable to arrays. Binary search requires non-contiguous, jump-style access to elements, and this kind of access is inefficient in linked lists, making it unsuitable for linked lists or linked-list-based data structures. - For small data volumes, linear search performs better. In linear search, each round requires only 1 comparison operation; while in binary search, it requires 1 addition, 1 division, 1-3 comparison operations, and 1 addition (subtraction), totaling 4-6 unit operations. Therefore, when the data volume $n$ is small, linear search is actually faster than binary search. diff --git a/en/docs/chapter_searching/binary_search_edge.md b/en/docs/chapter_searching/binary_search_edge.md index acdf0a2d0..f3ef0d3dc 100644 --- a/en/docs/chapter_searching/binary_search_edge.md +++ b/en/docs/chapter_searching/binary_search_edge.md @@ -1,10 +1,10 @@ -# Binary Search Edge Cases +# Binary Search Boundaries ## Finding the Left Boundary !!! question - Given a sorted array `nums` of length $n$ that may contain duplicate elements, return the index of the leftmost element `target` in the array. If the array does not contain the element, return $-1$. + Given a sorted array `nums` of length $n$ that may contain duplicate elements, return the index of the leftmost occurrence of `target`. If the array does not contain `target`, return $-1$. Recall the method for finding the insertion point with binary search. After the search completes, $i$ points to the leftmost `target`, **so finding the insertion point is essentially finding the index of the leftmost `target`**. @@ -27,9 +27,9 @@ Below we introduce two more clever methods. ### Reusing Left Boundary Search -In fact, we can use the function for finding the leftmost element to find the rightmost element. The specific method is: **Convert finding the rightmost `target` into finding the leftmost `target + 1`**. +In fact, we can use the function for finding the leftmost `target` to find the rightmost `target`. The specific method is: **convert finding the rightmost `target` into finding the leftmost `target + 1`**. -As shown in the figure below, after the search completes, pointer $i$ points to the leftmost `target + 1` (if it exists), while $j$ points to the rightmost `target`, **so we can simply return $j$**. +As shown in the figure below, after the search completes, the pointer $i$ points to the leftmost `target + 1` (if it exists), while $j$ points to the rightmost `target`, **so we can return $j$**. ![Converting right boundary search to left boundary search](binary_search_edge.assets/binary_search_right_edge_by_left_edge.png) @@ -45,12 +45,12 @@ We know that when the array does not contain `target`, $i$ and $j$ will eventual Therefore, as shown in the figure below, we can construct an element that does not exist in the array to find the left and right boundaries. -- Finding the leftmost `target`: Can be converted to finding `target - 0.5` and returning pointer $i$. -- Finding the rightmost `target`: Can be converted to finding `target + 0.5` and returning pointer $j$. +- Finding the leftmost `target`: This can be converted to finding `target - 0.5` and returning the pointer $i$. +- Finding the rightmost `target`: This can be converted to finding `target + 0.5` and returning the pointer $j$. ![Converting boundary search to element search](binary_search_edge.assets/binary_search_edge_by_element.png) The code is omitted here, but the following two points are worth noting: -- Since the given array does not contain decimals, we don't need to worry about how to handle equal cases. +- Since the given array does not contain decimal values, we do not need to worry about how to handle equality. - Because this method introduces decimals, the variable `target` in the function needs to be changed to a floating-point type (Python does not require this change). diff --git a/en/docs/chapter_searching/binary_search_insertion.md b/en/docs/chapter_searching/binary_search_insertion.md index 38e0e649d..07262dc8d 100644 --- a/en/docs/chapter_searching/binary_search_insertion.md +++ b/en/docs/chapter_searching/binary_search_insertion.md @@ -1,12 +1,12 @@ # Binary Search Insertion Point -Binary search can not only be used to search for target elements but also to solve many variant problems, such as searching for the insertion position of a target element. +Binary search can be used not only to search for target elements, but also to solve many variant problems, such as finding the insertion position of a target element. ## Case Without Duplicate Elements !!! question - Given a sorted array `nums` of length $n$ and an element `target`, where the array contains no duplicate elements. Insert `target` into the array `nums` while maintaining its sorted order. If the array already contains the element `target`, insert it to its left. Return the index of `target` in the array after insertion. An example is shown in the figure below. + Given a sorted array `nums` of length $n$ and an element `target`, where the array contains no duplicate elements, insert `target` into `nums` while maintaining its sorted order. If `target` already exists in the array, insert it to its left. Return the index of `target` after insertion. An example is shown below. ![Binary search insertion point example data](binary_search_insertion.assets/binary_search_insertion_example.png) @@ -18,9 +18,9 @@ The problem requires inserting `target` to the left of equal elements, which mea **Question 2**: When the array does not contain `target`, what is the insertion point index? -Further consider the binary search process: When `nums[m] < target`, $i$ moves, which means pointer $i$ is approaching elements greater than or equal to `target`. Similarly, pointer $j$ is always approaching elements less than or equal to `target`. +To analyze this further, consider the binary search process: when `nums[m] < target`, $i$ moves, meaning that pointer $i$ is approaching elements greater than or equal to `target`. Similarly, pointer $j$ is always approaching elements less than or equal to `target`. -Therefore, when the binary search ends, we must have: $i$ points to the first element greater than `target`, and $j$ points to the first element less than `target`. **It's easy to see that when the array does not contain `target`, the insertion index is $i$**. The code is shown below: +Therefore, when the binary search ends, $i$ must point to the first element greater than `target`, and $j$ must point to the first element less than `target`. **It follows that when the array does not contain `target`, the insertion index is $i$**. The code is shown below: ```src [file]{binary_search_insertion}-[class]{}-[func]{binary_search_insertion_simple} @@ -34,7 +34,7 @@ Therefore, when the binary search ends, we must have: $i$ points to the first el Suppose there are multiple `target` elements in the array. Ordinary binary search can only return the index of one `target`, **and cannot determine how many `target` elements are to the left and right of that element**. -The problem requires inserting the target element at the leftmost position, **so we need to find the index of the leftmost `target` in the array**. Initially, consider implementing this through the steps shown in the figure below: +The problem requires inserting the target element at the leftmost position, **so we need to find the index of the leftmost `target` in the array**. A straightforward initial approach is to follow the steps shown in the figure below: 1. Perform binary search to obtain the index of any `target`, denoted as $k$. 2. Starting from index $k$, perform linear traversal to the left, and return when the leftmost `target` is found. @@ -43,10 +43,10 @@ The problem requires inserting the target element at the leftmost position, **so Although this method works, it includes linear search, resulting in a time complexity of $O(n)$. When the array contains many duplicate `target` elements, this method is very inefficient. -Now consider extending the binary search code. As shown in the figure below, the overall process remains unchanged: calculate the midpoint index $m$ in each round, then compare `target` with `nums[m]`, divided into the following cases: +Now consider extending the binary search code. As shown in the figure below, the overall process remains unchanged: in each iteration, we first compute the midpoint index $m$, then compare `target` with `nums[m]`, leading to the following cases: -- When `nums[m] < target` or `nums[m] > target`, it means `target` has not been found yet, so use the ordinary binary search interval narrowing operation to **make pointers $i$ and $j$ approach `target`**. -- When `nums[m] == target`, it means elements less than `target` are in the interval $[i, m - 1]$, so use $j = m - 1$ to narrow the interval, thereby **making pointer $j$ approach elements less than `target`**. +- When `nums[m] < target` or `nums[m] > target`, it means `target` has not been found yet, so use the standard interval-shrinking operation of binary search to **move pointers $i$ and $j$ closer to `target`**. +- When `nums[m] == target`, it means elements less than `target` are in the interval $[i, m - 1]$, so use $j = m - 1$ to shrink the interval, thereby **moving pointer $j$ closer to elements less than `target`**. After the loop completes, $i$ points to the leftmost `target`, and $j$ points to the first element less than `target`, **so index $i$ is the insertion point**. @@ -74,7 +74,7 @@ After the loop completes, $i$ points to the leftmost `target`, and $j$ points to === "<8>" ![binary_search_insertion_step8](binary_search_insertion.assets/binary_search_insertion_step8.png) -Observe the following code: the operations for branches `nums[m] > target` and `nums[m] == target` are the same, so the two can be merged. +Observe the following code: the branches `nums[m] > target` and `nums[m] == target` perform the same operation, so they can be merged. Even so, we can still keep the conditional branches expanded, as the logic is clearer and more readable. @@ -84,8 +84,8 @@ Even so, we can still keep the conditional branches expanded, as the logic is cl !!! tip - The code in this section all uses the "closed interval" approach. Interested readers can implement the "left-closed right-open" approach themselves. + The code in this section uses the "closed interval" approach throughout. Interested readers can implement the "left-closed, right-open" approach themselves. -Overall, binary search is simply about setting search targets for pointers $i$ and $j$ separately. The target could be a specific element (such as `target`) or a range of elements (such as elements less than `target`). +Overall, binary search is simply a matter of setting separate search targets for pointers $i$ and $j$. The target may be a specific element (such as `target`) or a range of elements (such as elements less than `target`). -Through continuous binary iterations, both pointers $i$ and $j$ gradually approach their preset targets. Ultimately, they either successfully find the answer or stop after crossing the boundaries. +With each iteration of binary search, pointers $i$ and $j$ gradually approach their preset targets. Ultimately, they either find the answer or stop after crossing the boundary. diff --git a/en/docs/chapter_searching/replace_linear_by_hashing.md b/en/docs/chapter_searching/replace_linear_by_hashing.md index 5a2f76964..82edbc291 100644 --- a/en/docs/chapter_searching/replace_linear_by_hashing.md +++ b/en/docs/chapter_searching/replace_linear_by_hashing.md @@ -4,11 +4,11 @@ In algorithm problems, **we often reduce the time complexity of algorithms by re !!! question - Given an integer array `nums` and a target element `target`, search for two elements in the array whose "sum" equals `target`, and return their array indices. Any solution will do. + Given an integer array `nums` and a target value `target`, find two elements in the array whose sum is `target`, and return their indices. Any solution will do. ## Linear Search: Trading Time for Space -Consider directly traversing all possible combinations. As shown in the figure below, we open a two-layer loop and judge in each round whether the sum of two integers equals `target`. If so, return their indices. +Consider directly traversing all possible combinations. As shown in the figure below, we use nested loops and check in each iteration whether the sum of two integers is `target`. If so, return their indices. ![Linear search solution for two sum](replace_linear_by_hashing.assets/two_sum_brute_force.png) @@ -18,11 +18,11 @@ The code is shown below: [file]{two_sum}-[class]{}-[func]{two_sum_brute_force} ``` -This method has a time complexity of $O(n^2)$ and a space complexity of $O(1)$, which is very time-consuming with large data volumes. +This method has a time complexity of $O(n^2)$ and a space complexity of $O(1)$, making it very time-consuming on large inputs. ## Hash-Based Search: Trading Space for Time -Consider using a hash table where key-value pairs are array elements and element indices respectively. Loop through the array, performing the steps shown in the figure below in each round: +Consider using a hash table whose keys are array elements and whose values are their indices. Traverse the array and perform the steps shown in the figure below in each iteration: 1. Check if the number `target - nums[i]` is in the hash table. If so, directly return the indices of these two elements. 2. Add the key-value pair `nums[i]` and index `i` to the hash table. @@ -36,7 +36,7 @@ Consider using a hash table where key-value pairs are array elements and element === "<3>" ![two_sum_hashtable_step3](replace_linear_by_hashing.assets/two_sum_hashtable_step3.png) -The implementation code is shown below, requiring only a single loop: +The implementation is shown below and requires only a single loop: ```src [file]{two_sum}-[class]{}-[func]{two_sum_hash_table} @@ -44,4 +44,4 @@ The implementation code is shown below, requiring only a single loop: This method reduces the time complexity from $O(n^2)$ to $O(n)$ through hash-based search, greatly improving runtime efficiency. -Since an additional hash table needs to be maintained, the space complexity is $O(n)$. **Nevertheless, this method achieves a more balanced overall time-space efficiency, making it the optimal solution for this problem**. +Since an additional hash table needs to be maintained, the space complexity is $O(n)$. **Nevertheless, this method offers a more balanced overall time-space trade-off, making it the optimal solution to this problem**. diff --git a/en/docs/chapter_searching/searching_algorithm_revisited.md b/en/docs/chapter_searching/searching_algorithm_revisited.md index adc3d4cc8..914c351e6 100644 --- a/en/docs/chapter_searching/searching_algorithm_revisited.md +++ b/en/docs/chapter_searching/searching_algorithm_revisited.md @@ -5,9 +5,9 @@ Searching algorithms can be divided into the following two categories based on their implementation approach: - **Locating target elements by traversing the data structure**, such as traversing arrays, linked lists, trees, and graphs. -- **Achieving efficient element search by utilizing data organization structure or prior information contained in the data**, such as binary search, hash-based search, and binary search tree search. +- **Achieving efficient element lookup by leveraging the way data is organized or prior information about the data**, such as binary search, hash-based search, and binary search tree search. -It's not hard to see that these topics have all been covered in previous chapters, so searching algorithms are not unfamiliar to us. In this section, we will approach from a more systematic perspective and re-examine searching algorithms. +As these topics have already been introduced in earlier chapters, searching algorithms should already be familiar to us. In this section, we revisit them from a more systematic perspective. ## Brute-Force Search @@ -22,11 +22,11 @@ However, **the time complexity of such algorithms is $O(n)$**, where $n$ is the ## Adaptive Search -Adaptive search utilizes the unique properties of data (such as orderliness) to optimize the search process, thereby locating target elements more efficiently. +Adaptive search leverages properties of the data itself (such as sorted order) to optimize the search process and locate target elements more efficiently. - "Binary search" uses the orderliness of data to achieve efficient searching, applicable only to arrays. -- "Hash-based search" uses hash tables to establish key-value pair mappings between search data and target data, thereby achieving query operations. -- "Tree search" in specific tree structures (such as binary search trees), quickly eliminates nodes based on comparing node values to locate target elements. +- "Hash-based search" uses hash tables to store searchable data as key-value pairs, thereby enabling efficient queries. +- "Tree search" operates on specific tree structures (such as binary search trees), quickly ruling out nodes by comparing node values to locate the target element. The advantage of such algorithms is high efficiency, **with time complexity reaching $O(\log n)$ or even $O(1)$**. @@ -42,7 +42,7 @@ Given a dataset of size $n$, we can use linear search, binary search, tree searc ![Multiple search strategies](searching_algorithm_revisited.assets/searching_algorithms.png) -The operational efficiency and characteristics of the above methods are as follows: +The efficiency and characteristics of these methods are summarized in the table below.

Table   Comparison of search algorithm efficiency

@@ -59,26 +59,26 @@ The choice of search algorithm also depends on data volume, search performance r **Linear search** -- Good generality, requiring no data preprocessing operations. If we only need to query the data once, the data preprocessing time for the other three methods would be longer than linear search. +- Good generality, requiring no data preprocessing operations. If we need to query the data only once, the preprocessing required by the other three methods can take longer than the linear search itself. - Suitable for small data volumes, where time complexity has less impact on efficiency. - Suitable for scenarios with high data update frequency, as this method does not require any additional data maintenance. **Binary search** -- Suitable for large data volumes with stable efficiency performance, worst-case time complexity of $O(\log n)$. +- Suitable for large datasets, with stable performance and a worst-case time complexity of $O(\log n)$. - Data volume cannot be too large, as storing arrays requires contiguous memory space. - Not suitable for scenarios with frequent data insertion and deletion, as maintaining a sorted array has high overhead. **Hash-based search** - Suitable for scenarios with high query performance requirements, with an average time complexity of $O(1)$. -- Not suitable for scenarios requiring ordered data or range searches, as hash tables cannot maintain data orderliness. +- Not suitable for scenarios requiring ordered data or range searches, as hash tables cannot maintain the data in sorted order. - High dependence on hash functions and hash collision handling strategies, with significant risk of performance degradation. - Not suitable for excessively large data volumes, as hash tables require extra space to minimize collisions and thus provide good query performance. **Tree search** -- Suitable for massive data, as tree nodes are stored dispersedly in memory. -- Suitable for scenarios requiring maintained ordered data or range searches. +- Suitable for massive datasets, as tree nodes are stored non-contiguously in memory. +- Suitable for scenarios that require maintaining ordered data or performing range searches. - During continuous node insertion and deletion, binary search trees may become skewed, degrading time complexity to $O(n)$. -- If using AVL trees or red-black trees, all operations can run stably at $O(\log n)$ efficiency, but operations to maintain tree balance add extra overhead. +- If AVL trees or red-black trees are used, all operations can consistently run in $O(\log n)$ time, though maintaining tree balance adds extra overhead. diff --git a/en/docs/chapter_searching/summary.md b/en/docs/chapter_searching/summary.md index 3d8b31c0a..c3f8ae2db 100644 --- a/en/docs/chapter_searching/summary.md +++ b/en/docs/chapter_searching/summary.md @@ -2,9 +2,9 @@ ### Key Review -- Binary search relies on data orderliness and progressively reduces the search interval by half through loops. It requires input data to be sorted and is only applicable to arrays or data structures based on array implementations. -- Brute-force search locates data by traversing the data structure. Linear search is applicable to arrays and linked lists, while breadth-first search and depth-first search are applicable to graphs and trees. Such algorithms have good generality and require no data preprocessing, but have a relatively high time complexity of $O(n)$. +- Binary search relies on ordered data and searches by repeatedly halving the search interval. It requires the input data to be sorted and applies only to arrays or array-based data structures. +- Brute-force search locates data by traversing the data structure. Linear search applies to arrays and linked lists, while breadth-first search and depth-first search apply to graphs and trees. These algorithms are broadly applicable and require no data preprocessing, but their relatively high time complexity is $O(n)$. - Hash-based search, tree search, and binary search are efficient search methods that can quickly locate target elements in specific data structures. Such algorithms are highly efficient with time complexity reaching $O(\log n)$ or even $O(1)$, but typically require additional data structures. - In practice, we need to analyze factors such as data scale, search performance requirements, and data query and update frequency to choose the appropriate search method. -- Linear search is suitable for small-scale or frequently updated data; binary search is suitable for large-scale, sorted data; hash-based search is suitable for data with high query efficiency requirements and no need for range queries; tree search is suitable for large-scale dynamic data that needs to maintain order and support range queries. +- Linear search is suitable for small datasets or data that is updated frequently; binary search is suitable for large sorted datasets; hash-based search is suitable when high query efficiency is required and range queries are unnecessary; tree search is suitable for large dynamic datasets that must maintain order and support range queries. - Replacing linear search with hash-based search is a commonly used strategy to optimize runtime, reducing time complexity from $O(n)$ to $O(1)$. diff --git a/en/docs/chapter_sorting/bubble_sort.md b/en/docs/chapter_sorting/bubble_sort.md index 2fdbf0260..17a560260 100644 --- a/en/docs/chapter_sorting/bubble_sort.md +++ b/en/docs/chapter_sorting/bubble_sort.md @@ -1,11 +1,11 @@ # Bubble Sort -Bubble sort (bubble sort) achieves sorting by continuously comparing and swapping adjacent elements. This process is like bubbles rising from the bottom to the top, hence the name bubble sort. +Bubble sort sorts an array by continuously comparing and swapping adjacent elements. This process resembles bubbles rising from the bottom to the top, hence the name bubble sort. -As shown in the figure below, the bubbling process can be simulated using element swap operations: starting from the leftmost end of the array and traversing to the right, compare the size of adjacent elements, and if "left element > right element", swap them. After completing the traversal, the largest element will be moved to the rightmost end of the array. +As shown in the figure below, the bubbling process can be simulated using element swaps: starting from the leftmost end of the array and traversing to the right, compare each pair of adjacent elements, and if "left element > right element", swap them. After the traversal is complete, the largest element is moved to the rightmost end of the array. === "<1>" - ![Simulating bubble using element swap operation](bubble_sort.assets/bubble_operation_step1.png) + ![Simulating bubble sort using element swaps](bubble_sort.assets/bubble_operation_step1.png) === "<2>" ![bubble_operation_step2](bubble_sort.assets/bubble_operation_step2.png) @@ -44,9 +44,9 @@ Example code is as follows: ## Efficiency Optimization -We notice that if no swap operations are performed during a certain round of "bubbling", it means the array has already completed sorting and can directly return the result. Therefore, we can add a flag `flag` to monitor this situation and return immediately once it occurs. +We can observe that if no swaps occur during a round of "bubbling", the array is already sorted and the algorithm can return immediately. Therefore, we can add a flag `flag` to detect this situation and terminate as soon as it occurs. -After optimization, the worst-case time complexity and average time complexity of bubble sort remain $O(n^2)$; but when the input array is completely ordered, the best-case time complexity can reach $O(n)$. +After this optimization, the worst-case and average-case time complexities of bubble sort remain $O(n^2)$; however, when the input array is already sorted, the best-case time complexity becomes $O(n)$. ```src [file]{bubble_sort}-[class]{}-[func]{bubble_sort_with_flag} @@ -54,6 +54,6 @@ After optimization, the worst-case time complexity and average time complexity o ## Algorithm Characteristics -- **Time complexity of $O(n^2)$, adaptive sorting**: The array lengths traversed in each round of "bubbling" are $n - 1$, $n - 2$, $\dots$, $2$, $1$, totaling $(n - 1) n / 2$. After introducing the `flag` optimization, the best-case time complexity can reach $O(n)$. +- **Time complexity is $O(n^2)$; adaptive**: In successive rounds of "bubbling", the traversed portion of the array has lengths $n - 1$, $n - 2$, $\dots$, $2$, $1$, for a total of $(n - 1) n / 2$. After introducing the `flag` optimization, the best-case time complexity can reach $O(n)$. - **Space complexity of $O(1)$, in-place sorting**: Pointers $i$ and $j$ use a constant amount of extra space. -- **Stable sorting**: Since equal elements are not swapped during "bubbling". +- **Stable sorting**: Equal elements are not swapped during "bubbling". diff --git a/en/docs/chapter_sorting/bucket_sort.md b/en/docs/chapter_sorting/bucket_sort.md index aca0fe479..af7b3e694 100644 --- a/en/docs/chapter_sorting/bucket_sort.md +++ b/en/docs/chapter_sorting/bucket_sort.md @@ -1,8 +1,8 @@ # Bucket Sort -The several sorting algorithms mentioned earlier all belong to "comparison-based sorting algorithms", which achieve sorting by comparing the size of elements. The time complexity of such sorting algorithms cannot exceed $O(n \log n)$. Next, we will explore several "non-comparison sorting algorithms", whose time complexity can reach linear order. +The sorting algorithms discussed earlier are all comparison-based sorting algorithms, which sort by comparing the relative order of elements. The time complexity of such algorithms cannot beat $O(n \log n)$. Next, we will explore several non-comparison sorting algorithms, whose time complexity can be linear. -Bucket sort (bucket sort) is a typical application of the divide-and-conquer strategy. It works by setting up buckets with size order, each bucket corresponding to a data range, evenly distributing data to each bucket; then, sorting within each bucket separately; finally, merging all data in the order of the buckets. +Bucket sort is a typical application of the divide-and-conquer strategy. It works by creating a sequence of ordered buckets, each corresponding to a data range, and distributing the data evenly among them. The elements within each bucket are then sorted separately. Finally, all buckets are merged in order. ## Algorithm Flow @@ -22,23 +22,23 @@ The code is as follows: ## Algorithm Characteristics -Bucket sort is suitable for processing very large data volumes. For example, if the input data contains 1 million elements and system memory cannot load all the data at once, the data can be divided into 1000 buckets, each bucket sorted separately, and then the results merged. +Bucket sort is suitable for processing very large datasets. For example, suppose the input contains 1 million elements, and limited memory prevents the system from loading all of them at once. In that case, the data can be divided into 1000 buckets, each bucket can be sorted separately, and the results can then be merged. -- **Time complexity of $O(n + k)$**: Assuming the elements are evenly distributed among the buckets, then the number of elements in each bucket is $\frac{n}{k}$. Assuming sorting a single bucket uses $O(\frac{n}{k} \log\frac{n}{k})$ time, then sorting all buckets uses $O(n \log\frac{n}{k})$ time. **When the number of buckets $k$ is relatively large, the time complexity approaches $O(n)$**. Merging results requires traversing all buckets and elements, taking $O(n + k)$ time. In the worst case, all data is distributed into one bucket, and sorting that bucket uses $O(n^2)$ time. -- **Space complexity of $O(n + k)$, non-in-place sorting**: Additional space is required for $k$ buckets and a total of $n$ elements. +- **Time complexity is $O(n + k)$**: Assuming the elements are evenly distributed across the buckets, each bucket contains $\frac{n}{k}$ elements. If sorting a single bucket takes $O(\frac{n}{k} \log\frac{n}{k})$ time, then sorting all buckets takes $O(n \log\frac{n}{k})$ time. **When the number of buckets $k$ is relatively large, the time complexity approaches $O(n)$**. Merging the results requires traversing all buckets and elements, which takes $O(n + k)$ time. In the worst case, all data is placed into a single bucket, and sorting that bucket takes $O(n^2)$ time. +- **Space complexity is $O(n + k)$, and bucket sort is not in-place**: It requires extra space for $k$ buckets and a total of $n$ elements. - Whether bucket sort is stable depends on whether the algorithm for sorting elements within buckets is stable. ## How to Achieve Even Distribution -Theoretically, bucket sort can achieve $O(n)$ time complexity. **The key is to evenly distribute elements to each bucket**, because real data is often not evenly distributed. For example, if we want to evenly distribute all products on Taobao into 10 buckets by price range, there may be very many products below 100 yuan and very few above 1000 yuan. If the price intervals are evenly divided into 10, the difference in the number of products in each bucket will be very large. +In theory, bucket sort can achieve $O(n)$ time complexity. **The key is to distribute the elements evenly across the buckets**, because real-world data is often not uniformly distributed. For example, suppose we want to divide all products on Taobao evenly into 10 buckets by price range, but the price distribution is uneven: there are many products priced below 100 yuan and very few priced above 1000 yuan. If the price range is divided evenly into 10 intervals, the numbers of products in the buckets will differ greatly. -To achieve even distribution, we can first set an approximate dividing line to roughly divide the data into 3 buckets. **After distribution is complete, continue dividing buckets with more products into 3 buckets until the number of elements in all buckets is roughly equal**. +To achieve a more even distribution, we can first choose a rough boundary and partition the data into 3 buckets. **After that, buckets containing more products can be further divided into 3 buckets until the numbers of elements in all buckets are roughly equal**. -As shown in the figure below, this method essentially creates a recursion tree, with the goal of making the values of leaf nodes as even as possible. Of course, it is not necessary to divide the data into 3 buckets every round; the specific division method can be flexibly chosen according to data characteristics. +As shown in the figure below, this method essentially builds a recursion tree whose goal is to make the leaf nodes as balanced as possible. Of course, the data does not have to be split into 3 buckets in every round; the specific partitioning strategy can be chosen flexibly based on the characteristics of the data. ![Recursively dividing buckets](bucket_sort.assets/scatter_in_buckets_recursively.png) -If we know the probability distribution of product prices in advance, **we can set the price dividing line for each bucket based on the data probability distribution**. It is worth noting that the data distribution does not necessarily need to be specifically calculated, but can also be approximated using a certain probability model based on data characteristics. +If we know the probability distribution of product prices in advance, **we can set the price boundaries for each bucket according to that distribution**. Notably, the data distribution does not need to be measured exactly; it can also be approximated with a probability model chosen to fit the characteristics of the data. As shown in the figure below, we assume that product prices follow a normal distribution, which allows us to reasonably set price intervals to evenly distribute products to each bucket. diff --git a/en/docs/chapter_sorting/counting_sort.md b/en/docs/chapter_sorting/counting_sort.md index 5ab813678..800c44f33 100644 --- a/en/docs/chapter_sorting/counting_sort.md +++ b/en/docs/chapter_sorting/counting_sort.md @@ -1,14 +1,14 @@ # Counting Sort -Counting sort (counting sort) achieves sorting by counting the number of elements, typically applied to integer arrays. +Counting sort sorts by counting the occurrences of elements and is typically applied to integer arrays. ## Simple Implementation Let's start with a simple example. Given an array `nums` of length $n$, where the elements are all "non-negative integers", the overall flow of counting sort is shown in the figure below. 1. Traverse the array to find the largest number, denoted as $m$, and then create an auxiliary array `counter` of length $m + 1$. -2. **Use `counter` to count the number of occurrences of each number in `nums`**, where `counter[num]` corresponds to the number of occurrences of the number `num`. The counting method is simple: just traverse `nums` (let the current number be `num`), and increase `counter[num]` by $1$ in each round. -3. **Since each index of `counter` is naturally ordered, this is equivalent to all numbers being sorted**. Next, we traverse `counter` and fill in `nums` in ascending order based on the number of occurrences of each number. +2. **Use `counter` to count how many times each number appears in `nums`**, where `counter[num]` stores the number of occurrences of `num`. This is simple: traverse `nums` (denote the current number by `num`) and increment `counter[num]` by $1$ each time. +3. **Because the indices of `counter` are naturally ordered, the numbers are effectively already sorted**. Next, traverse `counter` and write the numbers back into `nums` in ascending order according to their occurrence counts. ![Counting sort flow](counting_sort.assets/counting_sort_overview.png) @@ -20,21 +20,21 @@ The code is as follows: !!! note "Connection between counting sort and bucket sort" - From the perspective of bucket sort, we can regard each index of the counting array `counter` in counting sort as a bucket, and the process of counting quantities as distributing each element to the corresponding bucket. Essentially, counting sort is a special case of bucket sort for integer data. + From the perspective of bucket sort, each index of the counting array `counter` can be viewed as a bucket, and the counting process can be seen as distributing elements into their corresponding buckets. Essentially, counting sort is a special case of bucket sort for integer data. ## Complete Implementation -Observant readers may have noticed that **if the input data is objects, step `3.` above becomes invalid**. Suppose the input data is product objects, and we want to sort the products by price (a member variable of the class), but the above algorithm can only give the sorting result of prices. +Observant readers may have noticed that **if the input consists of objects, step `3.` above no longer works**. Suppose the input consists of product objects and we want to sort them by price (a member variable of the class); the above algorithm can only produce the sorted order of the prices themselves. -So how can we obtain the sorting result of the original data? We first calculate the "prefix sum" of `counter`. As the name suggests, the prefix sum at index `i`, `prefix[i]`, equals the sum of the first `i` elements of the array: +So how can we obtain the sorted order of the original data? We first compute the prefix sums of `counter`. As the name suggests, the prefix sum at index `i`, `prefix[i]`, equals the sum of the elements from index `0` through `i`: $$ \text{prefix}[i] = \sum_{j=0}^i \text{counter[j]} $$ -**The prefix sum has a clear meaning: `prefix[num] - 1` represents the index of the last occurrence of element `num` in the result array `res`**. This information is very critical because it tells us where each element should appear in the result array. Next, we traverse each element `num` of the original array `nums` in reverse order, performing the following two steps in each iteration. +**The prefix sum has a clear interpretation: `prefix[num] - 1` gives the index of the last occurrence of element `num` in the result array `res`**. This information is crucial because it tells us where each element should be placed in the result array. Next, we traverse the original array `nums` in reverse, and for each element `num`, perform the following two steps. -1. Fill `num` into the array `res` at index `prefix[num] - 1`. +1. Place `num` at index `prefix[num] - 1` of the array `res`. 2. Decrease the prefix sum `prefix[num]` by $1$ to get the index for the next placement of `num`. After the traversal is complete, the array `res` contains the sorted result, and finally `res` is used to overwrite the original array `nums`. The complete counting sort flow is shown in the figure below. @@ -63,7 +63,7 @@ After the traversal is complete, the array `res` contains the sorted result, and === "<8>" ![counting_sort_step8](counting_sort.assets/counting_sort_step8.png) -The implementation code of counting sort is as follows: +The counting sort implementation is shown below: ```src [file]{counting_sort}-[class]{}-[func]{counting_sort} @@ -71,14 +71,14 @@ The implementation code of counting sort is as follows: ## Algorithm Characteristics -- **Time complexity of $O(n + m)$, non-adaptive sorting**: Involves traversing `nums` and traversing `counter`, both using linear time. Generally, $n \gg m$, and time complexity tends toward $O(n)$. +- **Time complexity is $O(n + m)$, and counting sort is non-adaptive**: Traversing `nums` and `counter` both takes linear time. In general, when $n \gg m$, the time complexity approaches $O(n)$. - **Space complexity of $O(n + m)$, non-in-place sorting**: Uses arrays `res` and `counter` of lengths $n$ and $m$ respectively. - **Stable sorting**: Since elements are filled into `res` in a "right-to-left" order, traversing `nums` in reverse can avoid changing the relative positions of equal elements, thereby achieving stable sorting. In fact, traversing `nums` in forward order can also yield correct sorting results, but the result would be unstable. ## Limitations -By this point, you might think counting sort is very clever, as it can achieve efficient sorting just by counting quantities. However, the prerequisites for using counting sort are relatively strict. +At this point, you might think counting sort is quite ingenious because it achieves efficient sorting simply by counting occurrences. However, the prerequisites for using counting sort are fairly restrictive. -**Counting sort is only suitable for non-negative integers**. If you want to apply it to other types of data, you need to ensure that the data can be converted to non-negative integers without changing the relative size relationships between elements. For example, for an integer array containing negative numbers, you can first add a constant to all numbers to convert them all to positive numbers, and then convert them back after sorting is complete. +**Counting sort is only applicable to non-negative integers**. To apply it to other types of data, you must ensure that they can be converted to non-negative integers without changing the relative ordering of the elements. For example, for an integer array containing negative numbers, you can first add a constant to every number to shift them into the non-negative range, and then shift them back after sorting. -**Counting sort is suitable for situations where the data volume is large but the data range is small**. For example, in the above example, $m$ cannot be too large, otherwise it will occupy too much space. And when $n \ll m$, counting sort uses $O(m)$ time, which may be slower than $O(n \log n)$ sorting algorithms. +**Counting sort is well suited to cases with many elements but a small value range**. For example, in the above scenario, $m$ cannot be too large; otherwise, it consumes too much space. And when $n \ll m$, counting sort takes $O(m)$ time, which may be slower than sorting algorithms with $O(n \log n)$ time complexity. diff --git a/en/docs/chapter_sorting/heap_sort.md b/en/docs/chapter_sorting/heap_sort.md index 9fc79a86f..44e49fb46 100644 --- a/en/docs/chapter_sorting/heap_sort.md +++ b/en/docs/chapter_sorting/heap_sort.md @@ -4,10 +4,10 @@ Before reading this section, please ensure you have completed the "Heap" chapter. -Heap sort (heap sort) is an efficient sorting algorithm based on the heap data structure. We can use the "build heap operation" and "element out-heap operation" that we have already learned to implement heap sort. +Heap sort is an efficient sorting algorithm based on the heap data structure. We can implement heap sort using the heap construction and element removal operations introduced earlier. 1. Input the array and build a min-heap, at which point the smallest element is at the heap top. -2. Continuously perform the out-heap operation, record the out-heap elements in sequence, and an ascending sorted sequence can be obtained. +2. Continuously perform element removal operations and record the removed elements in order to obtain a sequence sorted in ascending order. Although the above method is feasible, it requires an additional array to save the popped elements, which is quite wasteful of space. In practice, we usually use a more elegant implementation method. @@ -17,12 +17,12 @@ Assume the array length is $n$. The flow of heap sort is shown in the figure bel 1. Input the array and build a max-heap. After completion, the largest element is at the heap top. 2. Swap the heap top element (first element) with the heap bottom element (last element). After the swap is complete, reduce the heap length by $1$ and increase the count of sorted elements by $1$. -3. Starting from the heap top element, perform top-to-bottom heapify operation (sift down). After heapify is complete, the heap property is restored. -4. Loop through steps `2.` and `3.` After looping $n - 1$ rounds, the array sorting can be completed. +3. Starting from the heap top element, perform a top-to-bottom heapify operation (sift down). After heapify is complete, the heap property is restored. +4. Repeat steps `2.` and `3.` After $n - 1$ rounds, the array is sorted. !!! tip - In fact, the element out-heap operation also includes steps `2.` and `3.`, with just an additional step to pop the element. + In fact, the element removal operation also includes steps `2.` and `3.`, with the additional step of removing the element. === "<1>" ![Heap sort steps](heap_sort.assets/heap_sort_step1.png) @@ -60,7 +60,7 @@ Assume the array length is $n$. The flow of heap sort is shown in the figure bel === "<12>" ![heap_sort_step12](heap_sort.assets/heap_sort_step12.png) -In the code implementation, we use the same top-to-bottom heapify function `sift_down()` from the "Heap" chapter. It is worth noting that since the heap length will decrease as the largest element is extracted, we need to add a length parameter $n$ to the `sift_down()` function to specify the current effective length of the heap. The code is as follows: +In the code below, we use the same `sift_down()` function for top-to-bottom heapify as in the "Heap" chapter. It is worth noting that since the heap length decreases as the largest element is extracted, we need to add a length parameter $n$ to `sift_down()` to specify the current effective length of the heap. The code is as follows: ```src [file]{heap_sort}-[class]{}-[func]{heap_sort} @@ -68,6 +68,6 @@ In the code implementation, we use the same top-to-bottom heapify function `sift ## Algorithm Characteristics -- **Time complexity of $O(n \log n)$, non-adaptive sorting**: The build heap operation uses $O(n)$ time. Extracting the largest element from the heap has a time complexity of $O(\log n)$, looping a total of $n - 1$ rounds. -- **Space complexity of $O(1)$, in-place sorting**: A few pointer variables use $O(1)$ space. Element swapping and heapify operations are both performed on the original array. -- **Non-stable sorting**: When swapping the heap top element and heap bottom element, the relative positions of equal elements may change. +- **Time complexity is $O(n \log n)$; heap sort is non-adaptive**: Heap construction takes $O(n)$ time. Extracting the largest element from the heap takes $O(\log n)$ time, and this is repeated for a total of $n - 1$ rounds. +- **Space complexity is $O(1)$; heap sort is in-place**: A few pointer variables use $O(1)$ space. Element swapping and heapify are both performed on the original array. +- **Unstable sorting**: When swapping the heap top element and heap bottom element, the relative positions of equal elements may change. diff --git a/en/docs/chapter_sorting/index.md b/en/docs/chapter_sorting/index.md index 80fca2401..980965aef 100644 --- a/en/docs/chapter_sorting/index.md +++ b/en/docs/chapter_sorting/index.md @@ -6,4 +6,4 @@ Sorting is like a magic key that transforms chaos into order, enabling us to understand and process data more efficiently. - Whether it's simple ascending order or complex categorized arrangements, sorting demonstrates the harmonious beauty of data. + From simple ascending order to more complex classification schemes, sorting reveals the harmonious beauty of data. diff --git a/en/docs/chapter_sorting/insertion_sort.md b/en/docs/chapter_sorting/insertion_sort.md index e954ae5a9..3d1150329 100644 --- a/en/docs/chapter_sorting/insertion_sort.md +++ b/en/docs/chapter_sorting/insertion_sort.md @@ -1,10 +1,10 @@ # Insertion Sort -Insertion sort (insertion sort) is a simple sorting algorithm that works very similarly to the process of manually organizing a deck of cards. +Insertion sort is a simple sorting algorithm that works very similarly to the process of manually sorting a deck of cards. -Specifically, we select a base element from the unsorted interval, compare the element with elements in the sorted interval to its left one by one, and insert the element into the correct position. +Specifically, we select a base element from the unsorted portion, compare it one by one with the elements in the sorted portion to its left, and insert it into the correct position. -The figure below shows the operation flow of inserting an element into the array. Let the base element be `base`. We need to move all elements from the target index to `base` one position to the right, and then assign `base` to the target index. +The figure below illustrates how an element is inserted into an array. Let the base element be `base`. We need to shift all elements between the target index and `base` one position to the right, and then assign `base` to the target index. ![Single insertion operation](insertion_sort.assets/insertion_operation.png) @@ -12,7 +12,7 @@ The figure below shows the operation flow of inserting an element into the array The overall flow of insertion sort is shown in the figure below. -1. Initially, the first element of the array has completed sorting. +1. Initially, the first element of the array is already sorted. 2. Select the second element of the array as `base`, and after inserting it into the correct position, **the first 2 elements of the array are sorted**. 3. Select the third element as `base`, and after inserting it into the correct position, **the first 3 elements of the array are sorted**. 4. And so on. In the last round, select the last element as `base`, and after inserting it into the correct position, **all elements are sorted**. @@ -27,20 +27,20 @@ Example code is as follows: ## Algorithm Characteristics -- **Time complexity of $O(n^2)$, adaptive sorting**: In the worst case, each insertion operation requires loops of $n - 1$, $n-2$, $\dots$, $2$, $1$, summing to $(n - 1) n / 2$, so the time complexity is $O(n^2)$. When encountering ordered data, the insertion operation will terminate early. When the input array is completely ordered, insertion sort achieves the best-case time complexity of $O(n)$. +- **Time complexity of $O(n^2)$, adaptive sorting**: In the worst case, the insertion operations require $n - 1$, $n-2$, $\dots$, $2$, and $1$ iterations, respectively, summing to $(n - 1) n / 2$, so the time complexity is $O(n^2)$. When the data is already sorted, each insertion operation terminates early. When the input array is completely sorted, insertion sort achieves its best-case time complexity of $O(n)$. - **Space complexity of $O(1)$, in-place sorting**: Pointers $i$ and $j$ use a constant amount of extra space. -- **Stable sorting**: During the insertion operation process, we insert elements to the right of equal elements, without changing their order. +- **Stable sorting**: During insertion, we place elements to the right of equal elements, so their relative order is unchanged. ## Advantages of Insertion Sort -The time complexity of insertion sort is $O(n^2)$, while the time complexity of quick sort, which we will learn about next, is $O(n \log n)$. Although insertion sort has a higher time complexity, **insertion sort is usually faster for smaller data volumes**. +The time complexity of insertion sort is $O(n^2)$, while the time complexity of quick sort, which we will learn about next, is $O(n \log n)$. Although insertion sort has a higher time complexity, **it is usually faster on small datasets**. -This conclusion is similar to the applicable situations of linear search and binary search. Algorithms like quick sort with $O(n \log n)$ complexity are sorting algorithms based on divide-and-conquer strategy and often contain more unit computation operations. When the data volume is small, $n^2$ and $n \log n$ are numerically close, and complexity does not dominate; the number of unit operations per round plays a decisive role. +This conclusion is similar to the one about when linear search and binary search are applicable. Algorithms such as quick sort, with $O(n \log n)$ complexity, are divide-and-conquer sorting algorithms and often involve more primitive operations. When the dataset is small, the values of $n^2$ and $n \log n$ are relatively close, so asymptotic complexity does not dominate; instead, the number of primitive operations per round becomes the deciding factor. -In fact, the built-in sorting functions in many programming languages (such as Java) adopt insertion sort. The general approach is: for long arrays, use sorting algorithms based on divide-and-conquer strategy, such as quick sort; for short arrays, directly use insertion sort. +In fact, the built-in sorting functions of many programming languages (such as Java) use insertion sort. The general idea is: for large arrays, use divide-and-conquer sorting algorithms such as quick sort; for short arrays, use insertion sort directly. Although bubble sort, selection sort, and insertion sort all have a time complexity of $O(n^2)$, in actual situations, **insertion sort is used significantly more frequently than bubble sort and selection sort**, mainly for the following reasons. -- Bubble sort is based on element swapping, requiring the use of a temporary variable, involving 3 unit operations; insertion sort is based on element assignment, requiring only 1 unit operation. Therefore, **the computational overhead of bubble sort is usually higher than that of insertion sort**. +- Bubble sort is implemented through element swaps, which require a temporary variable and involve 3 primitive operations; insertion sort is implemented through element assignment and requires only 1 primitive operation. Therefore, **bubble sort usually has higher computational overhead than insertion sort**. - Selection sort has a time complexity of $O(n^2)$ in any case. **If given a set of partially ordered data, insertion sort is usually more efficient than selection sort**. - Selection sort is unstable and cannot be applied to multi-level sorting. diff --git a/en/docs/chapter_sorting/merge_sort.md b/en/docs/chapter_sorting/merge_sort.md index f4cf8d3fc..075371c31 100644 --- a/en/docs/chapter_sorting/merge_sort.md +++ b/en/docs/chapter_sorting/merge_sort.md @@ -1,9 +1,9 @@ # Merge Sort -Merge sort (merge sort) is a sorting algorithm based on the divide-and-conquer strategy, which includes the "divide" and "merge" phases shown in the figure below. +Merge sort is a sorting algorithm based on a divide-and-conquer strategy, consisting of the "divide" and "merge" phases shown in the figure below. -1. **Divide phase**: Recursively split the array from the midpoint, transforming the sorting problem of a long array into the sorting problems of shorter arrays. -2. **Merge phase**: When the sub-array length is 1, terminate the division and start merging, continuously merging two shorter sorted arrays into one longer sorted array until the process is complete. +1. **Divide phase**: Recursively split the array at the midpoint, reducing the problem of sorting a long array to the problem of sorting shorter arrays. +2. **Merge phase**: When a sub-array has length 1, stop dividing and start merging, continuously combining the shorter sorted sub-arrays on the left and right into a longer sorted array until the process is complete. ![Divide and merge phases of merge sort](merge_sort.assets/merge_sort_overview.png) @@ -12,9 +12,9 @@ As shown in the figure below, the "divide phase" recursively splits the array from the midpoint into two sub-arrays from top to bottom. 1. Calculate the array midpoint `mid`, recursively divide the left sub-array (interval `[left, mid]`) and right sub-array (interval `[mid + 1, right]`). -2. Recursively execute step `1.` until the sub-array interval length is 1, then terminate. +2. Repeat step `1.` recursively until a sub-array has length 1. -The "merge phase" merges the left sub-array and right sub-array into a sorted array from bottom to top. Note that merging starts from sub-arrays of length 1, and each sub-array in the merge phase is sorted. +The "merge phase" merges the left and right sub-arrays into a sorted array from bottom to top. Note that merging starts from sub-arrays of length 1, so every sub-array involved in this phase is already sorted. === "<1>" ![Merge sort steps](merge_sort.assets/merge_sort_step1.png) @@ -46,7 +46,7 @@ The "merge phase" merges the left sub-array and right sub-array into a sorted ar === "<10>" ![merge_sort_step10](merge_sort.assets/merge_sort_step10.png) -It can be observed that the recursive order of merge sort is consistent with the post-order traversal of a binary tree. +The recursive order of merge sort is consistent with the post-order traversal of a binary tree. - **Post-order traversal**: First recursively traverse the left subtree, then recursively traverse the right subtree, and finally process the root node. - **Merge sort**: First recursively process the left sub-array, then recursively process the right sub-array, and finally perform the merge. @@ -59,15 +59,15 @@ The implementation of merge sort is shown in the code below. Note that the inter ## Algorithm Characteristics -- **Time complexity of $O(n \log n)$, non-adaptive sorting**: The division produces a recursion tree of height $\log n$, and the total number of merge operations at each level is $n$, so the overall time complexity is $O(n \log n)$. -- **Space complexity of $O(n)$, non-in-place sorting**: The recursion depth is $\log n$, using $O(\log n)$ size of stack frame space. The merge operation requires the aid of an auxiliary array, using $O(n)$ size of additional space. -- **Stable sorting**: In the merge process, the order of equal elements remains unchanged. +- **Time complexity is $O(n \log n)$; merge sort is non-adaptive**: The divide phase produces a recursion tree of height $\log n$, and the total number of operations performed during merging at each level is $n$, so the overall time complexity is $O(n \log n)$. +- **Space complexity is $O(n)$; merge sort is not in-place**: The recursion depth is $\log n$, which uses $O(\log n)$ stack-frame space. The merge operation requires an auxiliary array, which uses $O(n)$ additional space. +- **Stable sort**: During merging, the relative order of equal elements remains unchanged. ## Linked List Sorting -For linked lists, merge sort has significant advantages over other sorting algorithms, **and can optimize the space complexity of linked list sorting tasks to $O(1)$**. +For linked lists, merge sort has significant advantages over other sorting algorithms, **and it can reduce the space complexity of the sorting task to $O(1)$**. -- **Divide phase**: "Iteration" can be used instead of "recursion" to implement linked list division work, thus saving the stack frame space used by recursion. -- **Merge phase**: In linked lists, node insertion and deletion operations can be achieved by just changing references (pointers), so there is no need to create additional linked lists during the merge phase (merging two short ordered linked lists into one long ordered linked list). +- **Divide phase**: Iteration can be used instead of recursion to split the linked list, thereby eliminating the stack-frame space used by recursion. +- **Merge phase**: In linked lists, node insertion and deletion require only pointer updates, so the merge phase (merging two short sorted linked lists into one longer sorted linked list) does not require creating an additional linked list. The specific implementation details are quite complex, and interested readers can consult related materials for learning. diff --git a/en/docs/chapter_sorting/quick_sort.md b/en/docs/chapter_sorting/quick_sort.md index 51def23c1..da7f8e707 100644 --- a/en/docs/chapter_sorting/quick_sort.md +++ b/en/docs/chapter_sorting/quick_sort.md @@ -1,12 +1,12 @@ # Quick Sort -Quick sort (quick sort) is a sorting algorithm based on the divide-and-conquer strategy, which operates efficiently and is widely applied. +Quick sort is an efficient and widely used sorting algorithm based on the divide-and-conquer strategy. -The core operation of quick sort is "sentinel partitioning", which aims to: select a certain element in the array as the "pivot", move all elements smaller than the pivot to its left, and move elements larger than the pivot to its right. Specifically, the flow of sentinel partitioning is shown in the figure below. +The core operation of quick sort is "sentinel partitioning", whose goal is to select an element as the "pivot", move all elements smaller than the pivot to its left, and move all elements larger than the pivot to its right. Specifically, the process is shown in the figure below. -1. Select the leftmost element of the array as the pivot, and initialize two pointers `i` and `j` pointing to the two ends of the array. -2. Set up a loop in which `i` (`j`) is used in each round to find the first element larger (smaller) than the pivot, and then swap these two elements. -3. Loop through step `2.` until `i` and `j` meet, and finally swap the pivot to the boundary line of the two sub-arrays. +1. Select the leftmost element as the pivot, and initialize two pointers `i` and `j` at the two ends of the array. +2. Enter a loop. In each round, use `i` (`j`) to find the first element larger (smaller) than the pivot, and then swap the two elements. +3. Repeat step `2.` until `i` and `j` meet, then swap the pivot into the boundary position between the two sub-arrays. === "<1>" ![Sentinel partitioning steps](quick_sort.assets/pivot_division_step1.png) @@ -35,7 +35,7 @@ The core operation of quick sort is "sentinel partitioning", which aims to: sele === "<9>" ![pivot_division_step9](quick_sort.assets/pivot_division_step9.png) -After sentinel partitioning is complete, the original array is divided into three parts: left sub-array, pivot, right sub-array, satisfying "any element in left sub-array $\leq$ pivot $\leq$ any element in right sub-array". Therefore, we next only need to sort these two sub-arrays. +After sentinel partitioning, the original array is divided into three parts: the left sub-array, the pivot, and the right sub-array, such that "any element in the left sub-array $\leq$ the pivot $\leq$ any element in the right sub-array". Therefore, we only need to sort the two sub-arrays next. !!! note "Divide-and-conquer strategy of quick sort" @@ -61,27 +61,27 @@ The overall flow of quick sort is shown in the figure below. ## Algorithm Characteristics -- **Time complexity of $O(n \log n)$, non-adaptive sorting**: In the average case, the number of recursive levels of sentinel partitioning is $\log n$, and the total number of loops at each level is $n$, using $O(n \log n)$ time overall. In the worst case, each round of sentinel partitioning divides an array of length $n$ into two sub-arrays of length $0$ and $n - 1$, at which point the number of recursive levels reaches $n$, the number of loops at each level is $n$, and the total time used is $O(n^2)$. +- **Time complexity of $O(n \log n)$, non-adaptive sorting**: On average, sentinel partitioning produces $\log n$ recursive levels, and the total number of loop iterations across each level is $n$, so the overall time complexity is $O(n \log n)$. In the worst case, each round of sentinel partitioning splits an array of length $n$ into sub-arrays of lengths $0$ and $n - 1$. The recursion depth then reaches $n$, with $n$ loop iterations at each level, yielding an overall time complexity of $O(n^2)$. - **Space complexity of $O(n)$, in-place sorting**: In the case where the input array is completely reversed, the worst recursive depth reaches $n$, using $O(n)$ stack frame space. The sorting operation is performed on the original array without the aid of an additional array. -- **Non-stable sorting**: In the last step of sentinel partitioning, the pivot may be swapped to the right of equal elements. +- **Unstable sorting**: In the last step of sentinel partitioning, the pivot may be swapped to the right of an equal element. ## Why Is Quick Sort Fast -From the name, we can see that quick sort should have certain advantages in terms of efficiency. Although the average time complexity of quick sort is the same as "merge sort" and "heap sort", quick sort is usually more efficient, mainly for the following reasons. +As the name suggests, quick sort has clear efficiency advantages. Although its average time complexity is the same as that of "merge sort" and "heap sort", quick sort is usually faster in practice for the following reasons. -- **The probability of the worst case occurring is very low**: Although the worst-case time complexity of quick sort is $O(n^2)$, which is not as stable as merge sort, in the vast majority of cases, quick sort can run with a time complexity of $O(n \log n)$. -- **High cache utilization**: When performing sentinel partitioning operations, the system can load the entire sub-array into the cache, so element access efficiency is relatively high. Algorithms like "heap sort" require jump-style access to elements, thus lacking this characteristic. -- **Small constant coefficient of complexity**: Among the three algorithms mentioned above, quick sort has the smallest total number of operations such as comparisons, assignments, and swaps. This is similar to the reason why "insertion sort" is faster than "bubble sort". +- **The worst case is unlikely to occur**: Although the worst-case time complexity of quick sort is $O(n^2)$ and its performance is less predictable than that of merge sort, quick sort runs in $O(n \log n)$ time in the vast majority of cases. +- **High cache efficiency**: During sentinel partitioning, the system can load the entire sub-array into cache, so accessing elements is relatively efficient. By contrast, algorithms such as "heap sort" require non-contiguous access to elements and therefore do not enjoy this advantage. +- **Small constant factors**: Among the three algorithms above, quick sort performs the fewest comparisons, assignments, and swaps in total. This is similar to why "insertion sort" is faster than "bubble sort". ## Pivot Optimization -**Quick sort may have reduced time efficiency for certain inputs**. Take an extreme example: suppose the input array is completely reversed. Since we select the leftmost element as the pivot, after sentinel partitioning is complete, the pivot is swapped to the rightmost end of the array, causing the left sub-array length to be $n - 1$ and the right sub-array length to be $0$. If we recurse down like this, each round of sentinel partitioning will have a sub-array length of $0$, the divide-and-conquer strategy fails, and quick sort degrades to a form approximate to "bubble sort". +**Quick sort can become less time-efficient for certain inputs**. Consider an extreme example in which the input array is in completely descending order. Because we choose the leftmost element as the pivot, once sentinel partitioning is complete, the pivot is swapped to the far right of the array, leaving a left sub-array of length $n - 1$ and a right sub-array of length $0$. If this continues recursively, each round of sentinel partitioning produces one sub-array of length $0$, the divide-and-conquer strategy breaks down, and quick sort degenerates into an approximation of "bubble sort". -To avoid this situation as much as possible, **we can optimize the pivot selection strategy in sentinel partitioning**. For example, we can randomly select an element as the pivot. However, if luck is not good and we select a non-ideal pivot every time, efficiency is still not satisfactory. +To reduce the chance of this happening, **we can optimize the pivot selection strategy used in sentinel partitioning**. For example, we can choose a pivot at random. However, if we are unlucky and repeatedly pick poor pivots, performance can still be unsatisfactory. -It should be noted that programming languages usually generate "pseudo-random numbers". If we construct a specific test case for a pseudo-random number sequence, the efficiency of quick sort may still degrade. +It should be noted that programming languages usually generate "pseudo-random numbers". If we construct a specific test case against a pseudo-random sequence, quick sort can still suffer degraded performance. -For further improvement, we can select three candidate elements in the array (usually the first, last, and middle elements of the array), **and use the median of these three candidate elements as the pivot**. In this way, the probability that the pivot is "neither too small nor too large" will be greatly increased. Of course, we can also select more candidate elements to further improve the robustness of the algorithm. After adopting this method, the probability of time complexity degrading to $O(n^2)$ is greatly reduced. +To improve further, we can choose three candidate elements from the array, usually the first, last, and middle elements, **and use the median of the three as the pivot**. This greatly increases the chance that the pivot is "neither too small nor too large". We can also choose more candidate elements to further improve the robustness of the algorithm. With this method, the probability that the time complexity degrades to $O(n^2)$ is significantly reduced. Example code is as follows: @@ -91,9 +91,9 @@ Example code is as follows: ## Recursive Depth Optimization -**For certain inputs, quick sort may occupy more space**. Taking a completely ordered input array as an example, let the length of the sub-array in recursion be $m$. Each round of sentinel partitioning will produce a left sub-array of length $0$ and a right sub-array of length $m - 1$, which means that the problem scale reduced per recursive call is very small (only one element is reduced), and the height of the recursion tree will reach $n - 1$, at which point $O(n)$ size of stack frame space is required. +**Quick sort may also use more space for certain inputs**. Consider a fully sorted input array. Let the length of the current sub-array in the recursion be $m$. Each round of sentinel partitioning produces a left sub-array of length $0$ and a right sub-array of length $m - 1$, which means each recursive call reduces the problem size by only one element. The recursion tree can therefore reach a height of $n - 1$, requiring $O(n)$ stack-frame space. -To prevent the accumulation of stack frame space, we can compare the lengths of the two sub-arrays after each round of sentinel sorting is complete, **and only recurse on the shorter sub-array**. Since the length of the shorter sub-array will not exceed $n / 2$, this method can ensure that the recursion depth does not exceed $\log n$, thus optimizing the worst-case space complexity to $O(\log n)$. The code is as follows: +To prevent stack frames from accumulating, we can compare the lengths of the two sub-arrays after each round of sentinel partitioning, **and recurse only on the shorter one**. Because the shorter sub-array has length at most $n / 2$, this method ensures that the recursion depth does not exceed $\log n$, reducing the worst-case space complexity to $O(\log n)$. The code is shown below: ```src [file]{quick_sort}-[class]{quick_sort_tail_call}-[func]{quick_sort} diff --git a/en/docs/chapter_sorting/radix_sort.md b/en/docs/chapter_sorting/radix_sort.md index d150b69ed..d6a322fc7 100644 --- a/en/docs/chapter_sorting/radix_sort.md +++ b/en/docs/chapter_sorting/radix_sort.md @@ -1,8 +1,8 @@ # Radix Sort -The previous section introduced counting sort, which is suitable for situations where the data volume $n$ is large but the data range $m$ is small. Suppose we need to sort $n = 10^6$ student IDs, and the student ID is an 8-digit number, which means the data range $m = 10^8$ is very large. Using counting sort would require allocating a large amount of memory space, whereas radix sort can avoid this situation. +The previous section introduced counting sort, which is suitable when the number of items $n$ is large but the value range $m$ is small. Suppose we need to sort $n = 10^6$ student IDs, each of which is an 8-digit number. Then the value range $m = 10^8$ is very large. Using counting sort would require a large amount of memory, whereas radix sort avoids this problem. -Radix sort (radix sort) has a core idea consistent with counting sort, which also achieves sorting by counting quantities. Building on this, radix sort utilizes the progressive relationship between the digits of numbers, sorting each digit in turn to obtain the final sorting result. +Radix sort is based on the same core idea as counting sort: it also sorts by counting occurrences. Building on this, radix sort exploits the positional relationship among digits and sorts them one digit at a time to obtain the final result. ## Algorithm Flow @@ -14,13 +14,13 @@ Taking student ID data as an example, assume the lowest digit is the $1$st digit ![Radix sort algorithm flow](radix_sort.assets/radix_sort_overview.png) -Below we analyze the code implementation. For a $d$-base number $x$, to get its $k$th digit $x_k$, the following calculation formula can be used: +Next, let us look at the code. For a number $x$ in base $d$, its $k$th digit $x_k$ can be obtained with the following formula: $$ x_k = \lfloor\frac{x}{d^{k-1}}\rfloor \bmod d $$ -Where $\lfloor a \rfloor$ denotes rounding down the floating-point number $a$, and $\bmod \: d$ denotes taking the modulo (remainder) with respect to $d$. For student ID data, $d = 10$ and $k \in [1, 8]$. +Here, $\lfloor a \rfloor$ denotes rounding the floating-point number $a$ down, and $\bmod \: d$ denotes taking the remainder modulo $d$. For student ID data, $d = 10$ and $k \in [1, 8]$. Additionally, we need to slightly modify the counting sort code to make it sort based on the $k$th digit of the number: @@ -30,12 +30,12 @@ Additionally, we need to slightly modify the counting sort code to make it sort !!! question "Why start sorting from the lowest digit?" - In successive sorting rounds, the result of a later round will override the result of an earlier round. For example, if the first round result is $a < b$, while the second round result is $a > b$, then the second round's result will replace the first round's result. Since higher-order digits have higher priority than lower-order digits, we should sort the lower digits first and then sort the higher digits. + In successive sorting passes, a later pass overrides the result of an earlier one. For example, if the first pass yields $a < b$ but the second yields $a > b$, then the result of the second pass prevails. Because higher-order digits have higher priority than lower-order digits, we should sort the lower digits first and then the higher digits. ## Algorithm Characteristics -Compared to counting sort, radix sort is suitable for larger numerical ranges, **but the prerequisite is that the data must be representable in a fixed number of digits, and the number of digits should not be too large**. For example, floating-point numbers are not suitable for radix sort because their number of digits $k$ may be too large, potentially leading to time complexity $O(nk) \gg O(n^2)$. +Compared with counting sort, radix sort is suitable for larger value ranges, **but only when the data can be represented with a fixed number of digits and that digit count is not too large**. For example, floating-point numbers are not well suited to radix sort because the digit count $k$ can be too large, potentially leading to time complexity $O(nk) \gg O(n^2)$. -- **Time complexity of $O(nk)$, non-adaptive sorting**: Let the data volume be $n$, the data be in base $d$, and the maximum number of digits be $k$. Then performing counting sort on a certain digit uses $O(n + d)$ time, and sorting all $k$ digits uses $O((n + d)k)$ time. Typically, both $d$ and $k$ are relatively small, and the time complexity approaches $O(n)$. +- **Time complexity of $O(nk)$, non-adaptive sorting**: Let the number of items be $n$, let the values be represented in base $d$, and let the maximum number of digits be $k$. Counting sort on one digit takes $O(n + d)$ time, so sorting all $k$ digits takes $O((n + d)k)$ time. In practice, $d$ and $k$ are usually relatively small, so the overall time complexity approaches $O(n)$. - **Space complexity of $O(n + d)$, non-in-place sorting**: Same as counting sort, radix sort requires auxiliary arrays `res` and `counter` of lengths $n$ and $d$. -- **Stable sorting**: When counting sort is stable, radix sort is also stable; when counting sort is unstable, radix sort cannot guarantee obtaining correct sorting results. +- **Stable sort**: When counting sort is stable, radix sort is also stable; when counting sort is unstable, radix sort cannot guarantee correct sorting results. diff --git a/en/docs/chapter_sorting/selection_sort.md b/en/docs/chapter_sorting/selection_sort.md index eb02a8e17..c1605ac55 100644 --- a/en/docs/chapter_sorting/selection_sort.md +++ b/en/docs/chapter_sorting/selection_sort.md @@ -1,14 +1,14 @@ # Selection Sort -Selection sort (selection sort) works very simply: it opens a loop, and in each round, selects the smallest element from the unsorted interval and places it at the end of the sorted interval. +Selection sort works very simply: in each round, it selects the smallest element from the unsorted interval and places it at the end of the sorted interval. -Assume the array has length $n$. The algorithm flow of selection sort is shown in the figure below. +Assume the array has length $n$. The procedure of selection sort is shown in the figure below. 1. Initially, all elements are unsorted, i.e., the unsorted (index) interval is $[0, n-1]$. 2. Select the smallest element in the interval $[0, n-1]$ and swap it with the element at index $0$. After completion, the first element of the array is sorted. 3. Select the smallest element in the interval $[1, n-1]$ and swap it with the element at index $1$. After completion, the first 2 elements of the array are sorted. 4. And so on. After $n - 1$ rounds of selection and swapping, the first $n - 1$ elements of the array are sorted. -5. The only remaining element must be the largest element, requiring no sorting, so the array sorting is complete. +5. The only remaining element must be the largest, so no further sorting is needed and the array is sorted. === "<1>" ![Selection sort steps](selection_sort.assets/selection_sort_step1.png) @@ -43,7 +43,7 @@ Assume the array has length $n$. The algorithm flow of selection sort is shown i === "<11>" ![selection_sort_step11](selection_sort.assets/selection_sort_step11.png) -In the code, we use $k$ to record the smallest element within the unsorted interval: +In the code, we use $k$ to track the smallest element within the unsorted interval: ```src [file]{selection_sort}-[class]{}-[func]{selection_sort} @@ -51,8 +51,8 @@ In the code, we use $k$ to record the smallest element within the unsorted inter ## Algorithm Characteristics -- **Time complexity of $O(n^2)$, non-adaptive sorting**: The outer loop has $n - 1$ rounds in total. The length of the unsorted interval in the first round is $n$, and the length of the unsorted interval in the last round is $2$. That is, each round of the outer loop contains $n$, $n - 1$, $\dots$, $3$, $2$ inner loop iterations, summing to $\frac{(n - 1)(n + 2)}{2}$. -- **Space complexity of $O(1)$, in-place sorting**: Pointers $i$ and $j$ use a constant amount of extra space. -- **Non-stable sorting**: As shown in the figure below, element `nums[i]` may be swapped to the right of an element equal to it, causing a change in their relative order. +- **Time complexity $O(n^2)$, non-adaptive sorting**: The outer loop has $n - 1$ rounds in total. The length of the unsorted interval in the first round is $n$, and the length of the unsorted interval in the last round is $2$. That is, the rounds of the outer loop contain inner loops with $n$, $n - 1$, $\dots$, $3$, and $2$ iterations, summing to $\frac{(n - 1)(n + 2)}{2}$. +- **Space complexity $O(1)$, in-place sorting**: Pointers $i$ and $j$ use a constant amount of extra space. +- **Unstable sorting**: As shown in the figure below, element `nums[i]` may be swapped to the right of an element equal to it, causing a change in their relative order. ![Selection sort non-stability example](selection_sort.assets/selection_sort_instability.png) diff --git a/en/docs/chapter_sorting/sorting_algorithm.md b/en/docs/chapter_sorting/sorting_algorithm.md index f3d3591b7..1ee758867 100644 --- a/en/docs/chapter_sorting/sorting_algorithm.md +++ b/en/docs/chapter_sorting/sorting_algorithm.md @@ -1,8 +1,8 @@ # Sorting Algorithm -Sorting algorithm (sorting algorithm) is used to arrange a group of data in a specific order. Sorting algorithms have extensive applications because ordered data can usually be searched, analyzed, and processed more efficiently. +A sorting algorithm arranges a set of data in a specific order. Sorting algorithms have extensive applications because ordered data can usually be searched, analyzed, and processed more efficiently. -As shown in the figure below, data types in sorting algorithms can be integers, floating-point numbers, characters, or strings, etc. The sorting criterion can be set according to requirements, such as numerical size, character ASCII code order, or custom rules. +As shown in the figure below, the data being sorted can be integers, floating-point numbers, characters, strings, and so on. The sorting rule can be defined as needed, such as numerical order, ASCII order, or a custom rule. ![Data type and criterion examples](sorting_algorithm.assets/sorting_examples.png) @@ -17,7 +17,7 @@ As shown in the figure below, data types in sorting algorithms can be integers, Stable sorting is a necessary condition for multi-level sorting scenarios. Suppose we have a table storing student information, where column 1 and column 2 are name and age, respectively. In this case, unstable sorting may cause the ordered nature of the input data to be lost: ```shell -# Input Data Is Sorted by Name +# The input data is sorted by name # (name, age) ('A', 19) ('B', 18) @@ -25,9 +25,9 @@ Stable sorting is a necessary condition for multi-level sorting scenarios. Suppo ('D', 19) ('E', 23) -# Assuming We Use an Unstable Sorting Algorithm to Sort the List by Age, -# In the Result, the Relative Positions of ('D', 19) and ('A', 19) Are Changed, -# And the Property That the Input Data Is Sorted by Name Is Lost +# Suppose we use an unstable sorting algorithm to sort the list by age. +# In the result, the relative positions of ('D', 19) and ('A', 19) change, +# so the property that the input data is sorted by name is lost. ('B', 18) ('D', 19) ('A', 19) @@ -37,10 +37,10 @@ Stable sorting is a necessary condition for multi-level sorting scenarios. Suppo **Adaptability**: Adaptive sorting can utilize the existing order information in the input data to reduce the amount of computation, achieving better time efficiency. The best-case time complexity of adaptive sorting algorithms is typically better than the average time complexity. -**Comparison-based or not**: Comparison-based sorting relies on comparison operators ($<$, $=$, $>$) to determine the relative order of elements, thereby sorting the entire array, with a theoretical optimal time complexity of $O(n \log n)$. Non-comparison sorting does not use comparison operators and can achieve a time complexity of $O(n)$, but its versatility is relatively limited. +**Comparison-based or non-comparison**: Comparison-based sorting relies on comparison operators ($<$, $=$, $>$) to determine the relative order of elements, thereby sorting the entire array, with a theoretical optimal time complexity of $O(n \log n)$. Non-comparison sorting does not use comparison operators and can achieve a time complexity of $O(n)$, but its versatility is relatively limited. ## Ideal Sorting Algorithm -**Fast execution, in-place, stable, adaptive, good versatility**. Clearly, no sorting algorithm has been discovered to date that combines all of these characteristics. Therefore, when selecting a sorting algorithm, it is necessary to decide based on the specific characteristics of the data and the requirements of the problem. +**Fast, in-place, stable, adaptive, and broadly applicable**. Clearly, no sorting algorithm has been discovered to date that combines all of these characteristics. Therefore, when selecting a sorting algorithm, it is necessary to decide based on the specific characteristics of the data and the requirements of the problem. -Next, we will learn about various sorting algorithms together and analyze the advantages and disadvantages of each sorting algorithm based on the above evaluation dimensions. +Next, we will examine various sorting algorithms and analyze their advantages and disadvantages based on the evaluation dimensions above. diff --git a/en/docs/chapter_sorting/summary.md b/en/docs/chapter_sorting/summary.md index 566f8ffc0..f57f7a5f0 100644 --- a/en/docs/chapter_sorting/summary.md +++ b/en/docs/chapter_sorting/summary.md @@ -3,13 +3,13 @@ ### Key Review - Bubble sort achieves sorting by swapping adjacent elements. By adding a flag to enable early return, we can optimize the best-case time complexity of bubble sort to $O(n)$. -- Insertion sort completes sorting by inserting elements from the unsorted interval into the correct position in the sorted interval each round. Although the time complexity of insertion sort is $O(n^2)$, it is very popular in small data volume sorting tasks because it involves relatively few unit operations. -- Quick sort is implemented based on sentinel partitioning operations. In sentinel partitioning, it is possible to select the worst pivot every time, causing the time complexity to degrade to $O(n^2)$. Introducing median pivot or random pivot can reduce the probability of such degradation. By preferentially recursing on the shorter sub-interval, the recursion depth can be effectively reduced, optimizing the space complexity to $O(\log n)$. +- In each round, insertion sort inserts an element from the unsorted portion into its correct position in the sorted portion. Although insertion sort has a time complexity of $O(n^2)$, it remains very popular for small sorting tasks because each operation is relatively lightweight. +- Quick sort relies on sentinel partitioning. In sentinel partitioning, repeatedly choosing the worst possible pivot can degrade the time complexity to $O(n^2)$. Choosing a median-based pivot or a random pivot can reduce the probability of this degradation. By recursing on the shorter subarray first, we can effectively reduce the recursion depth and optimize the space complexity to $O(\log n)$. - Merge sort includes two phases: divide and merge, which typically embody the divide-and-conquer strategy. In merge sort, sorting an array requires creating auxiliary arrays, with a space complexity of $O(n)$; however, the space complexity of sorting a linked list can be optimized to $O(1)$. - Bucket sort consists of three steps: distributing data into buckets, sorting within buckets, and merging results. It also embodies the divide-and-conquer strategy and is suitable for very large data volumes. The key to bucket sort is distributing data evenly. - Counting sort is a special case of bucket sort, which achieves sorting by counting the number of occurrences of data. Counting sort is suitable for situations where the data volume is large but the data range is limited, and requires that data can be converted to positive integers. - Radix sort achieves data sorting by sorting digit by digit, requiring that data can be represented as fixed-digit numbers. -- Overall, we hope to find a sorting algorithm that is efficient, stable, in-place, and adaptive, with good versatility. However, just like other data structures and algorithms, no sorting algorithm has been found so far that simultaneously possesses all these characteristics. In practical applications, we need to select the appropriate sorting algorithm based on the specific characteristics of the data. +- Overall, we hope to find a sorting algorithm that is efficient, stable, in-place, and adaptive. However, as with other data structures and algorithms, no sorting algorithm can satisfy all of these criteria at the same time. In practice, we need to choose the appropriate sorting algorithm based on the characteristics of the data. - The figure below compares mainstream sorting algorithms in terms of efficiency, stability, in-place property, and adaptability. ![Sorting algorithm comparison](summary.assets/sorting_algorithms_comparison.png) @@ -20,7 +20,7 @@ In reality, we may sort based on a certain attribute of objects. For example, students have two attributes: name and height. We want to implement multi-level sorting: first sort by name to get `(A, 180) (B, 185) (C, 170) (D, 170)`; then sort by height. Because the sorting algorithm is unstable, we may get `(D, 170) (C, 170) (A, 180) (B, 185)`. -It can be seen that the positions of students D and C have been swapped, and the orderliness of names has been disrupted, which is something we don't want to see. +We can see that students D and C have swapped positions, destroying the ordering by name, which is not what we want. **Q**: Can the order of "searching from right to left" and "searching from left to right" in sentinel partitioning be swapped? @@ -30,17 +30,17 @@ The last step of sentinel partitioning `partition()` is to swap `nums[left]` and For example, given the array `[0, 0, 0, 0, 1]`, if we first "search from left to right", the array after sentinel partitioning is `[1, 0, 0, 0, 0]`, which is incorrect. -Thinking deeper, if we select `nums[right]` as the pivot, then it's exactly the opposite - we must first "search from left to right". +By the same reasoning, if we select `nums[right]` as the pivot, the order is reversed: we must first "search from left to right". **Q**: Regarding the optimization of recursion depth in quick sort, why can selecting the shorter array ensure that the recursion depth does not exceed $\log n$? -The recursion depth is the number of currently unreturned recursive methods. Each round of sentinel partitioning divides the original array into two sub-arrays. After recursion depth optimization, the length of the sub-array to be recursively processed is at most half of the original array length. Assuming the worst case is always half the length, the final recursion depth will be $\log n$. +Recursion depth is the number of recursive calls that have not yet returned. Each round of sentinel partitioning divides the original array into two sub-arrays. After this optimization, the sub-array selected for further recursion is at most half the length of the original array. In the worst case, if it is always half as long, the final recursion depth is $\log n$. Reviewing the original quick sort, we may continuously recurse on the longer array. In the worst case, it would be $n$, $n - 1$, $\dots$, $2$, $1$, with a recursion depth of $n$. Recursion depth optimization can avoid this situation. **Q**: When all elements in the array are equal, is the time complexity of quick sort $O(n^2)$? How should this degenerate case be handled? -Yes. For this situation, consider partitioning the array into three parts through sentinel partitioning: less than, equal to, and greater than the pivot. Only recursively process the less than and greater than parts. Under this method, an array where all input elements are equal can complete sorting in just one round of sentinel partitioning. +Yes. In this case, the array can be partitioned into three parts through sentinel partitioning: less than, equal to, and greater than the pivot. We then recurse only on the less-than and greater-than parts. With this approach, an array whose elements are all equal can be sorted in just one round of sentinel partitioning. **Q**: Why is the worst-case time complexity of bucket sort $O(n^2)$? diff --git a/en/docs/chapter_stack_and_queue/deque.md b/en/docs/chapter_stack_and_queue/deque.md index af6ea5187..d948438c1 100644 --- a/en/docs/chapter_stack_and_queue/deque.md +++ b/en/docs/chapter_stack_and_queue/deque.md @@ -1,6 +1,6 @@ # Deque -In a queue, we can only remove elements from the front or add elements at the rear. As shown in the figure below, a double-ended queue (deque) provides greater flexibility, allowing the addition or removal of elements at both the front and rear. +In a queue, we can only remove elements from the front or add elements at the rear. As shown in the figure below, a double-ended queue (deque) provides greater flexibility, allowing elements to be added or removed at both the front and the rear. ![Operations of deque](deque.assets/deque_operations.png) @@ -19,7 +19,7 @@ The common operations on a deque are shown in the table below. The specific meth | `peek_first()` | Access front element | $O(1)$ | | `peek_last()` | Access rear element | $O(1)$ | -Similarly, we can directly use the deque classes already implemented in programming languages: +Similarly, we can directly use the deque classes provided by the programming language: === "Python" diff --git a/en/docs/chapter_stack_and_queue/index.md b/en/docs/chapter_stack_and_queue/index.md index 461d82a27..a59e20b6c 100644 --- a/en/docs/chapter_stack_and_queue/index.md +++ b/en/docs/chapter_stack_and_queue/index.md @@ -1,9 +1,9 @@ -# Stack and Queue +# Stacks and Queues -![Stack and Queue](../assets/covers/chapter_stack_and_queue.jpg) +![Stacks and Queues](../assets/covers/chapter_stack_and_queue.jpg) !!! abstract - Stacks are like stacking cats, while queues are like cats lining up. + A stack is like cats piled on top of one another, while a queue is like cats lining up. - They represent LIFO (Last In First Out) and FIFO (First In First Out) logic, respectively. + They represent the logical relationships of LIFO (Last In, First Out) and FIFO (First In, First Out), respectively. diff --git a/en/docs/chapter_stack_and_queue/queue.md b/en/docs/chapter_stack_and_queue/queue.md index f4cefc09b..cab140396 100755 --- a/en/docs/chapter_stack_and_queue/queue.md +++ b/en/docs/chapter_stack_and_queue/queue.md @@ -1,6 +1,6 @@ # Queue -A queue is a linear data structure that follows the First In First Out (FIFO) rule. As the name suggests, a queue simulates the phenomenon of lining up, where newcomers continuously join the end of the queue, while people at the front of the queue leave one by one. +A queue is a linear data structure that follows the First In, First Out (FIFO) rule. As the name suggests, it models people lining up: newcomers continuously join the rear of the queue, while the people at the front leave one by one. As shown in the figure below, we call the front of the queue the "front" and the end the "rear." The operation of adding an element to the rear is called "enqueue," and the operation of removing the front element is called "dequeue." @@ -8,7 +8,7 @@ As shown in the figure below, we call the front of the queue the "front" and the ## Common Queue Operations -The common operations on a queue are shown in the table below. Note that method names may vary across different programming languages. We adopt the same naming convention as for stacks here. +The common operations on a queue are shown in the table below. Note that method names may vary across programming languages. Here, we use the same naming convention as for stacks.

Table   Efficiency of Queue Operations

@@ -18,7 +18,7 @@ The common operations on a queue are shown in the table below. Note that method | `pop()` | Dequeue front element | $O(1)$ | | `peek()` | Access front element | $O(1)$ | -We can directly use the ready-made queue classes in programming languages: +We can directly use the queue classes provided by the programming language: === "Python" diff --git a/en/docs/chapter_stack_and_queue/stack.md b/en/docs/chapter_stack_and_queue/stack.md index b3513480a..b06c0dc27 100755 --- a/en/docs/chapter_stack_and_queue/stack.md +++ b/en/docs/chapter_stack_and_queue/stack.md @@ -1,10 +1,10 @@ # Stack -A stack is a linear data structure that follows the Last In First Out (LIFO) logic. +A stack is a linear data structure that follows the Last In, First Out (LIFO) principle. We can compare a stack to a pile of plates on a table. If we specify that only one plate can be moved at a time, then to get the bottom plate, we must first remove the plates above it one by one. If we replace the plates with various types of elements (such as integers, characters, objects, etc.), we get the stack data structure. -As shown in the figure below, we call the top of the stacked elements the "top" and the bottom the "base." The operation of adding an element to the top is called "push," and the operation of removing the top element is called "pop." +As shown in the figure below, we call the top of the stacked elements the "top" and the bottom the "bottom." The operation of adding an element to the top is called "push," and the operation of removing the top element is called "pop." ![LIFO rule of stack](stack.assets/stack_operations.png) @@ -20,7 +20,7 @@ The common operations on a stack are shown in the table below. The specific meth | `pop()` | Pop top element from stack | $O(1)$ | | `peek()` | Access top element | $O(1)$ | -Typically, we can directly use the built-in stack class provided by the programming language. However, some languages may not provide a dedicated stack class. In these cases, we can use the language's "array" or "linked list" as a stack and ignore operations unrelated to the stack in the program logic. +Typically, we can directly use the built-in stack class provided by the programming language. However, some languages may not provide a dedicated stack class. In such cases, we can use the language's "array" or "linked list" as a stack and simply avoid using operations unrelated to stack behavior. === "Python" diff --git a/en/docs/chapter_stack_and_queue/summary.md b/en/docs/chapter_stack_and_queue/summary.md index ff8551353..8ab9b6e9b 100644 --- a/en/docs/chapter_stack_and_queue/summary.md +++ b/en/docs/chapter_stack_and_queue/summary.md @@ -3,7 +3,7 @@ ### Key Review - A stack is a data structure that follows the LIFO principle and can be implemented using arrays or linked lists. -- In terms of time efficiency, the array implementation of a stack has higher average efficiency, but during expansion, the time complexity of a single push operation degrades to $O(n)$. In contrast, the linked list implementation of a stack provides more stable efficiency performance. +- In terms of time efficiency, the array implementation of a stack has higher average efficiency, but during expansion, the time complexity of a single push operation degrades to $O(n)$. In contrast, the linked-list implementation of a stack offers more stable performance. - In terms of space efficiency, the array implementation of a stack may lead to some degree of space wastage. However, it should be noted that the memory space occupied by linked list nodes is larger than that of array elements. - A queue is a data structure that follows the FIFO principle and can also be implemented using arrays or linked lists. The conclusions regarding time efficiency and space efficiency comparisons for queues are similar to those for stacks mentioned above. - A deque is a queue with greater flexibility that allows adding and removing elements at both ends. @@ -12,7 +12,7 @@ **Q**: Is the browser's forward and backward functionality implemented with a doubly linked list? -The forward and backward functionality of a browser is essentially a manifestation of a "stack." When a user visits a new page, that page is added to the top of the stack; when the user clicks the back button, that page is popped from the top of the stack. Using a deque can conveniently implement some additional operations, as mentioned in the "Deque" section. +The browser's forward and backward behavior is essentially an application of a "stack." When a user visits a new page, that page is added to the top of the stack; when the user clicks the back button, that page is popped from the top of the stack. A deque can conveniently support some additional operations, as mentioned in the "Deque" section. **Q**: After popping from the stack, do we need to free the memory of the popped node? @@ -20,7 +20,7 @@ If the popped node will still be needed later, then memory does not need to be f **Q**: A deque seems like two stacks joined together. What is its purpose? -A deque is like a combination of a stack and a queue, or two stacks joined together. It exhibits the logic of both stack and queue, so it can implement all applications of stacks and queues, and is more flexible. +A deque is like a combination of a stack and a queue, or two stacks joined together. It combines the logic of both, so it can support all applications of stacks and queues while offering greater flexibility. **Q**: How are undo and redo specifically implemented? diff --git a/en/docs/chapter_tree/array_representation_of_tree.md b/en/docs/chapter_tree/array_representation_of_tree.md index 7ab08e0ad..f31a718ca 100644 --- a/en/docs/chapter_tree/array_representation_of_tree.md +++ b/en/docs/chapter_tree/array_representation_of_tree.md @@ -1,6 +1,6 @@ # Array Representation of Binary Trees -Under the linked list representation, the storage unit of a binary tree is a node `TreeNode`, and nodes are connected by pointers. The previous section introduced the basic operations of binary trees under the linked list representation. +In the linked-list representation, the storage unit of a binary tree is a node `TreeNode`, and nodes are connected by pointers. The previous section introduced the basic operations of binary trees in this representation. So, can we use an array to represent a binary tree? The answer is yes. @@ -22,7 +22,7 @@ As shown in the figure below, given a non-perfect binary tree, the above method ![Level-order traversal sequence corresponds to multiple binary tree possibilities](array_representation_of_tree.assets/array_representation_without_empty.png) -To solve this problem, **we can consider explicitly writing out all `None` values in the level-order traversal sequence**. As shown in the figure below, after this treatment, the level-order traversal sequence can uniquely represent a binary tree. Example code is as follows: +To solve this problem, **we can explicitly write out all `None` values in the level-order traversal sequence**. As shown in the figure below, once we do this, the level-order traversal sequence can uniquely represent a binary tree. Example code is as follows: === "Python" @@ -128,7 +128,7 @@ To solve this problem, **we can consider explicitly writing out all `None` value tree = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15] ``` -![Array representation of any type of binary tree](array_representation_of_tree.assets/array_representation_with_empty.png) +![Array representation of an arbitrary binary tree](array_representation_of_tree.assets/array_representation_with_empty.png) It's worth noting that **complete binary trees are very well-suited for array representation**. Recalling the definition of a complete binary tree, `None` only appears at the bottom level and towards the right, **meaning all `None` values must appear at the end of the level-order traversal sequence**. @@ -136,9 +136,9 @@ This means that when using an array to represent a complete binary tree, it's po ![Array representation of a complete binary tree](array_representation_of_tree.assets/array_representation_complete_binary_tree.png) -The following code implements a binary tree based on array representation, including the following operations: +The following code implements a binary tree using an array representation, including the following operations: -- Given a certain node, obtain its value, left (right) child node, and parent node. +- Given a node, obtain its value, left (right) child node, and parent node. - Obtain the preorder, inorder, postorder, and level-order traversal sequences. ```src diff --git a/en/docs/chapter_tree/avl_tree.md b/en/docs/chapter_tree/avl_tree.md index 767aeb50b..ecabb9489 100644 --- a/en/docs/chapter_tree/avl_tree.md +++ b/en/docs/chapter_tree/avl_tree.md @@ -1,4 +1,4 @@ -# Avl Tree * +# AVL Tree * In the "Binary Search Tree" section, we mentioned that after multiple insertion and removal operations, a binary search tree may degenerate into a linked list. In this case, the time complexity of all operations degrades from $O(\log n)$ to $O(n)$. @@ -10,9 +10,9 @@ For example, in the perfect binary tree shown in the figure below, after inserti ![Degradation of an AVL tree after inserting nodes](avl_tree.assets/avltree_degradation_from_inserting_node.png) -In 1962, G. M. Adelson-Velsky and E. M. Landis proposed the AVL tree in their paper "An algorithm for the organization of information". The paper described in detail a series of operations ensuring that after continuously adding and removing nodes, the AVL tree does not degenerate, thus keeping the time complexity of various operations at the $O(\log n)$ level. In other words, in scenarios requiring frequent insertions, deletions, searches, and modifications, the AVL tree can always maintain efficient data operation performance, making it very valuable in applications. +In 1962, G. M. Adelson-Velsky and E. M. Landis proposed the AVL tree in their paper "An algorithm for the organization of information". The paper describes a series of operations that prevent an AVL tree from degenerating as nodes are inserted and removed, thereby keeping the time complexity of various operations at $O(\log n)$. In other words, in scenarios that require frequent insertion, deletion, lookup, and update operations, AVL trees can maintain consistently efficient performance and therefore have strong practical value. -## Common Terminology in Avl Trees +## Common Terminology in AVL Trees An AVL tree is both a binary search tree and a balanced binary tree, simultaneously satisfying all the properties of these two types of binary trees, hence it is a balanced binary search tree. @@ -228,7 +228,7 @@ Since the operations related to AVL trees require obtaining node heights, we nee end ``` -The "node height" refers to the distance from that node to its farthest leaf node, i.e., the number of "edges" passed. It is important to note that the height of a leaf node is $0$, and the height of a null node is $-1$. We will create two utility functions for getting and updating the height of a node: +The "node height" refers to the distance from that node to its farthest leaf node, i.e., the number of edges on the path. It is important to note that the height of a leaf node is $0$, and the height of a null node is $-1$. We will create two utility functions for getting and updating the height of a node: ```src [file]{avl_tree}-[class]{avl_tree}-[func]{update_height} @@ -246,11 +246,11 @@ The balance factor of a node is defined as the height of the node's left Let the balance factor be $f$, then the balance factor of any node in an AVL tree satisfies $-1 \le f \le 1$. -## Rotations in Avl Trees +## Rotations in AVL Trees The characteristic of AVL trees lies in the "rotation" operation, which can restore balance to unbalanced nodes without affecting the inorder traversal sequence of the binary tree. In other words, **rotation operations can both maintain the property of a "binary search tree" and make the tree return to a "balanced binary tree"**. -We call nodes with a balance factor absolute value $> 1$ "unbalanced nodes". Depending on the imbalance situation, rotation operations are divided into four types: right rotation, left rotation, left rotation then right rotation, and right rotation then left rotation. Below we describe these rotation operations in detail. +We call nodes with a balance factor absolute value $> 1$ "unbalanced nodes". Depending on the imbalance situation, rotation operations are divided into four types: right rotation, left rotation, right rotation then left rotation, and left rotation then right rotation. Below we describe these rotation operations in detail. ### Right Rotation @@ -329,7 +329,7 @@ For ease of use, we encapsulate the rotation operations into a function. **With [file]{avl_tree}-[class]{avl_tree}-[func]{rotate} ``` -## Common Operations in Avl Trees +## Common Operations in AVL Trees ### Node Insertion @@ -351,7 +351,7 @@ Similarly, on the basis of the binary search tree's node removal method, rotatio The node search operation in AVL trees is consistent with that in binary search trees, and will not be elaborated here. -## Typical Applications of Avl Trees +## Typical Applications of AVL Trees - Organizing and storing large-scale data, suitable for scenarios with high-frequency searches and low-frequency insertions and deletions. - Used to build index systems in databases. diff --git a/en/docs/chapter_tree/binary_search_tree.md b/en/docs/chapter_tree/binary_search_tree.md index 182fd1838..eddc63d64 100755 --- a/en/docs/chapter_tree/binary_search_tree.md +++ b/en/docs/chapter_tree/binary_search_tree.md @@ -13,7 +13,7 @@ We encapsulate the binary search tree as a class `BinarySearchTree` and declare ### Searching for a Node -Given a target node value `num`, we can search according to the properties of the binary search tree. As shown in the figure below, we declare a node `cur` and start from the binary tree's root node `root`, looping to compare the node value `cur.val` with `num`. +Given a target node value `num`, we can search according to the properties of the binary search tree. As shown in the figure below, we declare a node `cur` and start from the binary search tree's root node `root`, looping to compare `cur.val` with `num`. - If `cur.val < num`, it means the target node is in `cur`'s right subtree, thus execute `cur = cur.right`. - If `cur.val > num`, it means the target node is in `cur`'s left subtree, thus execute `cur = cur.left`. @@ -31,7 +31,7 @@ Given a target node value `num`, we can search according to the properties of th === "<4>" ![bst_search_step4](binary_search_tree.assets/bst_search_step4.png) -The search operation in a binary search tree works on the same principle as the binary search algorithm, both eliminating half of the cases in each round. The number of loop iterations is at most the height of the binary tree. When the binary tree is balanced, it uses $O(\log n)$ time. The example code is as follows: +The search operation in a binary search tree follows the same principle as binary search: each round rules out half of the remaining cases. The number of loop iterations is at most the height of the tree. When the tree is balanced, the search takes $O(\log n)$ time. The example code is as follows: ```src [file]{binary_search_tree}-[class]{binary_search_tree}-[func]{search} @@ -42,13 +42,13 @@ The search operation in a binary search tree works on the same principle as the Given an element `num` to be inserted, in order to maintain the property of the binary search tree "left subtree < root node < right subtree," the insertion process is as shown in the figure below. 1. **Finding the insertion position**: Similar to the search operation, start from the root node and loop downward searching according to the size relationship between the current node value and `num`, until passing the leaf node (traversing to `None`) and then exit the loop. -2. **Insert the node at that position**: Initialize node `num` and place it at the `None` position. +2. **Insert the node at that position**: Create a node for `num` and place it at the `None` position. ![Inserting a node into a binary search tree](binary_search_tree.assets/bst_insert.png) In the code implementation, note the following two points: -- Binary search trees do not allow duplicate nodes; otherwise, it would violate its definition. Therefore, if the node to be inserted already exists in the tree, the insertion is not performed and it returns directly. +- Binary search trees do not allow duplicate nodes; otherwise, the tree would no longer satisfy its definition. Therefore, if the node to be inserted already exists in the tree, the insertion is skipped and the function returns directly. - To implement the node insertion, we need to use node `pre` to save the node from the previous loop iteration. This way, when traversing to `None`, we can obtain its parent node, thereby completing the node insertion operation. ```src @@ -59,7 +59,7 @@ Similar to searching for a node, inserting a node uses $O(\log n)$ time. ### Removing a Node -First, find the target node in the binary tree, then remove it. Similar to node insertion, we need to ensure that after the removal operation is completed, the binary search tree's property of "left subtree $<$ root node $<$ right subtree" is still maintained. Therefore, depending on the number of child nodes the target node has, we divide it into 0, 1, and 2 three cases, and execute the corresponding node removal operations. +First, find the target node in the binary search tree, then remove it. Similar to node insertion, we need to ensure that after the removal operation is completed, the binary search tree's property of "left subtree $<$ root node $<$ right subtree" is still maintained. Therefore, depending on the number of child nodes the target node has, we consider three cases: degree $0$, degree $1$, and degree $2$, and perform the corresponding removal operation. As shown in the figure below, when the degree of the node to be removed is $0$, it means the node is a leaf node and can be directly removed. @@ -71,7 +71,7 @@ As shown in the figure below, when the degree of the node to be removed is $1$, When the degree of the node to be removed is $2$, we cannot directly remove it; instead, we need to use a node to replace it. To maintain the binary search tree's property of "left subtree $<$ root node $<$ right subtree," **this node can be either the smallest node in the right subtree or the largest node in the left subtree**. -Assuming we choose the smallest node in the right subtree (the next node in the inorder traversal), the removal process is as shown in the figure below. +Assuming we choose the smallest node in the right subtree, that is, the inorder successor, the removal process is as shown in the figure below. 1. Find the next node of the node to be removed in the "inorder traversal sequence," denoted as `tmp`. 2. Replace the value of the node to be removed with the value of `tmp`, and recursively remove node `tmp` in the tree. @@ -116,7 +116,7 @@ Given a set of data, we consider using an array or a binary search tree for stor | Insert element | $O(1)$ | $O(\log n)$ | | Remove element | $O(n)$ | $O(\log n)$ | -In the ideal case, a binary search tree is "balanced," such that any node can be found within $\log n$ loop iterations. +In the ideal case, a binary search tree is balanced, so any node can be found within $O(\log n)$ loop iterations. However, if we continuously insert and remove nodes in a binary search tree, it may degenerate into a linked list as shown in the figure below, where the time complexity of various operations also degrades to $O(n)$. diff --git a/en/docs/chapter_tree/binary_tree.md b/en/docs/chapter_tree/binary_tree.md index 5f42796c3..03252e987 100644 --- a/en/docs/chapter_tree/binary_tree.md +++ b/en/docs/chapter_tree/binary_tree.md @@ -1,6 +1,6 @@ # Binary Tree -A binary tree is a non-linear data structure that represents the derivation relationship between "ancestors" and "descendants" and embodies the divide-and-conquer logic of "one divides into two". Similar to a linked list, the basic unit of a binary tree is a node, and each node contains a value, a reference to its left child node, and a reference to its right child node. +A binary tree is a non-linear data structure that models the hierarchical relationship between "ancestors" and "descendants" and embodies a divide-and-conquer pattern in which each split branches into two. Similar to a linked list, the basic unit of a binary tree is a node, and each node contains a value, a reference to its left child node, and a reference to its right child node. === "Python" @@ -203,7 +203,7 @@ A binary tree is a non-linear data structure that represents the derivati Each node has two references (pointers), pointing respectively to the left-child node and right-child node. This node is called the parent node of these two child nodes. When given a node of a binary tree, we call the tree formed by this node's left child and all nodes below it the left subtree of this node. Similarly, the right subtree can be defined. -**In a binary tree, except leaf nodes, all other nodes contain child nodes and non-empty subtrees.** As shown in the figure below, if "Node 2" is regarded as a parent node, its left and right child nodes are "Node 4" and "Node 5" respectively. The left subtree is formed by "Node 4" and all nodes beneath it, while the right subtree is formed by "Node 5" and all nodes beneath it. +**In a binary tree, every non-leaf node has child nodes and therefore non-empty subtrees.** As shown in the figure below, if "Node 2" is regarded as a parent node, its left and right child nodes are "Node 4" and "Node 5" respectively. The left subtree is formed by "Node 4" and all nodes beneath it, while the right subtree is formed by "Node 5" and all nodes beneath it. ![Parent Node, child Node, subtree](binary_tree.assets/binary_tree_definition.png) @@ -224,7 +224,7 @@ The commonly used terminology of binary trees is shown in the figure below. !!! tip - Please note that we usually define "height" and "depth" as "the number of edges traversed", but some questions or textbooks may define them as "the number of nodes traversed". In this case, both height and depth need to be incremented by 1. + We usually define "height" and "depth" as the number of edges traversed, but some textbooks and problem statements define them as the number of nodes on the path. In that case, both values are larger by 1. ## Basic Operations of Binary Trees @@ -617,13 +617,13 @@ Similar to a linked list, inserting and removing nodes in a binary tree can be a !!! tip - It should be noted that inserting nodes may change the original logical structure of the binary tree, while removing nodes typically involves removing the node and all its subtrees. Therefore, in a binary tree, insertion and removal are usually performed through a set of operations to achieve meaningful outcomes. + Keep in mind that inserting a node can alter the original logical structure of a binary tree, while deleting a node usually entails removing that node together with its entire subtree. In practice, insertion and deletion in binary trees are therefore typically implemented as coordinated sequences of operations to achieve a meaningful result. ## Common Types of Binary Trees ### Perfect Binary Tree -As shown in the figure below, a perfect binary tree has all levels completely filled with nodes. In a perfect binary tree, leaf nodes have a degree of $0$, while all other nodes have a degree of $2$. If the tree height is $h$, the total number of nodes is $2^{h+1} - 1$, exhibiting a standard exponential relationship that reflects the common phenomenon of cell division in nature. +As shown in the figure below, a perfect binary tree has every level completely filled. In a perfect binary tree, leaf nodes have a degree of $0$, while all other nodes have a degree of $2$. If the tree height is $h$, the total number of nodes is $2^{h+1} - 1$, following a standard exponential pattern that mirrors the common phenomenon of cell division in nature. !!! tip @@ -651,9 +651,9 @@ As shown in the figure below, in a balanced binary tree, the absolute dif ## Degeneration of Binary Trees -The figure below shows the ideal and degenerate structures of binary trees. When every level of a binary tree is filled, it reaches the "perfect binary tree" state; when all nodes are biased toward one side, the binary tree degenerates into a "linked list". +The figure below contrasts the ideal and degenerate structures of binary trees. When every level is filled, the tree becomes a "perfect binary tree"; when all nodes skew to one side, the binary tree degenerates into a "linked list". -- A perfect binary tree is the ideal case, fully leveraging the "divide and conquer" advantage of binary trees. +- A perfect binary tree is the ideal case, fully leveraging the divide-and-conquer advantages of binary trees. - A linked list represents the other extreme, where all operations become linear operations with time complexity degrading to $O(n)$. ![The Best and Worst Structures of Binary Trees](binary_tree.assets/binary_tree_best_worst_cases.png) diff --git a/en/docs/chapter_tree/binary_tree_traversal.md b/en/docs/chapter_tree/binary_tree_traversal.md index 5d553d7c6..08d946e40 100755 --- a/en/docs/chapter_tree/binary_tree_traversal.md +++ b/en/docs/chapter_tree/binary_tree_traversal.md @@ -8,7 +8,7 @@ The common traversal methods for binary trees include level-order traversal, pre As shown in the figure below, level-order traversal traverses the binary tree from top to bottom, layer by layer. Within each level, it visits nodes from left to right. -Level-order traversal is essentially breadth-first traversal, also known as breadth-first search (BFS), which embodies a "expanding outward circle by circle" layer-by-layer traversal method. +Level-order traversal is essentially breadth-first traversal, also known as breadth-first search (BFS), which proceeds outward level by level. ![Level-order traversal of a binary tree](binary_tree_traversal.assets/binary_tree_bfs.png) @@ -27,7 +27,7 @@ Breadth-first traversal is typically implemented with the help of a "queue". The ## Preorder, Inorder, and Postorder Traversal -Correspondingly, preorder, inorder, and postorder traversals all belong to depth-first traversal, also known as depth-first search (DFS), which embodies a "first go to the end, then backtrack and continue" traversal method. +Correspondingly, preorder, inorder, and postorder traversals all belong to depth-first traversal, also known as depth-first search (DFS), which goes as deep as possible before backtracking. The figure below shows how depth-first traversal works on a binary tree. **Depth-first traversal is like "walking" around the perimeter of the entire binary tree**, encountering three positions at each node, corresponding to preorder, inorder, and postorder traversal. @@ -43,12 +43,12 @@ Depth-first search is usually implemented based on recursion: !!! tip - Depth-first search can also be implemented based on iteration, interested readers can study this on their own. + Depth-first search can also be implemented iteratively, and interested readers can explore this on their own. -The figure below shows the recursive process of preorder traversal of a binary tree, which can be divided into two opposite parts: "recursion" and "return". +The figure below shows the recursive process of preorder traversal of a binary tree, which can be divided into two opposite phases: "descending" and "returning". -1. "Recursion" means opening a new method, where the program accesses the next node in this process. -2. "Return" means the function returns, indicating that the current node has been fully visited. +1. "Descending" means making a new recursive call, during which the program visits the next node. +2. "Returning" means the function call returns, indicating that the current node has been fully processed. === "<1>" ![The recursive process of preorder traversal](binary_tree_traversal.assets/preorder_step1.png) diff --git a/en/docs/chapter_tree/index.md b/en/docs/chapter_tree/index.md index f641a4a14..96098cd4d 100644 --- a/en/docs/chapter_tree/index.md +++ b/en/docs/chapter_tree/index.md @@ -4,6 +4,6 @@ !!! abstract - Towering trees are full of vitality, with deep roots and lush leaves, spreading branches and flourishing. + Towering trees are full of vitality, with deep roots, lush foliage, and sprawling branches. - They show us the vivid form of divide and conquer in data. \ No newline at end of file + They offer a vivid illustration of divide-and-conquer in data structures. diff --git a/en/docs/chapter_tree/summary.md b/en/docs/chapter_tree/summary.md index 2bcbf3881..54f9b1ed4 100644 --- a/en/docs/chapter_tree/summary.md +++ b/en/docs/chapter_tree/summary.md @@ -2,23 +2,23 @@ ### Key Review -- A binary tree is a non-linear data structure that embodies the divide-and-conquer logic of "one divides into two". Each binary tree node contains a value and two pointers, which respectively point to its left and right child nodes. +- A binary tree is a non-linear data structure that embodies the divide-and-conquer logic of splitting into two. Each binary tree node contains a value and two pointers, which point to its left and right child nodes. - For a certain node in a binary tree, the tree formed by its left (right) child node and all nodes below is called the left (right) subtree of that node. - Related terminology of binary trees includes root node, leaf node, level, degree, edge, height, and depth. - The initialization, node insertion, and node removal operations of binary trees are similar to those of linked lists. -- Common types of binary trees include perfect binary trees, complete binary trees, full binary trees, and balanced binary trees. The perfect binary tree is the ideal state, while the linked list is the worst state after degradation. +- Common types of binary trees include perfect binary trees, complete binary trees, full binary trees, and balanced binary trees. A perfect binary tree is the ideal form, while a linked list represents the worst degenerate case. - A binary tree can be represented using an array by arranging node values and empty slots in level-order traversal sequence, and implementing pointers based on the index mapping relationship between parent and child nodes. -- Level-order traversal of a binary tree is a breadth-first search method, embodying a layer-by-layer traversal approach of "expanding outward circle by circle", typically implemented using a queue. -- Preorder, inorder, and postorder traversals all belong to depth-first search, embodying a traversal approach of "first go to the end, then backtrack and continue", typically implemented using recursion. +- Level-order traversal of a binary tree is a breadth-first search method that proceeds level by level, typically implemented using a queue. +- Preorder, inorder, and postorder traversals all belong to depth-first search, which proceeds by going as deep as possible before backtracking, typically using recursion. - A binary search tree is an efficient data structure for element searching, with search, insertion, and removal operations all having time complexity of $O(\log n)$. When a binary search tree degenerates into a linked list, all time complexities degrade to $O(n)$. - An AVL tree, also known as a balanced binary search tree, ensures the tree remains balanced after continuous node insertions and removals through rotation operations. -- Rotation operations in AVL trees include right rotation, left rotation, left rotation then right rotation, and right rotation then left rotation. After inserting or removing nodes, AVL trees perform rotation operations from bottom to top to restore the tree to balance. +- Rotation operations in AVL trees include right rotation, left rotation, right rotation followed by left rotation, and left rotation followed by right rotation. After inserting or removing nodes, AVL trees perform rotations from bottom to top to restore balance. ### Q & A **Q**: For a binary tree with only one node, are both the height of the tree and the depth of the root node $0$? -Yes, because height and depth are typically defined as "the number of edges passed." +Yes, because height and depth are typically defined as the number of edges on the path. **Q**: The insertion and removal in a binary tree are generally accomplished by a set of operations. What does "a set of operations" refer to here? Does it imply releasing the resources of the child nodes? diff --git a/en/docs/index.html b/en/docs/index.html index 6e4c29ecb..9f50f3e9d 100644 --- a/en/docs/index.html +++ b/en/docs/index.html @@ -14,15 +14,15 @@ - Complexity analysis + Complexity Analysis - Array and linked list + Array and Linked List - Stack and queue + Stack and Queue @@ -50,7 +50,7 @@ - Divide and conquer + Divide and Conquer @@ -58,7 +58,7 @@ - Dynamic programming + Dynamic Programming @@ -71,13 +71,13 @@

- Data structures and algorithms crash course with animated illustrations and off-the-shelf code + Data structures and algorithms tutorial with animated illustrations and ready-to-run code

- Dive in + Get Started @@ -105,25 +105,13 @@ -
-

500 animated illustrations, 14 programming languages, and 3000 community Q&As to help you dive into data structures and algorithms.

+

500 animated illustrations, 14 programming languages, and 3,000 community Q&As to help you get started with data structures and algorithms.

@@ -152,12 +140,12 @@

Endorsements

-

“An easy-to-understand book on data structures and algorithms, which guides readers to learn by minds-on and hands-on. Strongly recommended for algorithm beginners!”

-

—— Junhui Deng, Professor, Department of computer science and technology, Tsinghua University

+

“An easy-to-understand introduction to data structures and algorithms that encourages readers to engage both mind and hands. Highly recommended for beginners!”

+

—— Junhui Deng, Professor, Department of Computer Science and Technology, Tsinghua University

“If I had 'Hello Algo' when I was learning data structures and algorithms, it would have been 10 times easier!”

-

—— Mu Li, Senior principal scientist, Amazon

+

—— Mu Li, Senior Principal Scientist, Amazon

@@ -173,7 +161,7 @@ -

Animated illustrations

+

Animated Illustrations

It’s crafted for a smooth learning experience.

"A picture is worth a thousand words."

@@ -190,9 +178,9 @@ -

Off-the-shelf code

+

One-Click Runnable Code

-

One click to run code in multiple languages.

+

Run code in multiple languages with one click.

"Talk is cheap. Show me the code."

@@ -205,9 +193,9 @@ -

Learning together

+

Learn Together

-

Don’t hesitate to ask or share your thoughts.

+

Ask questions, discuss ideas, and learn with others.

"Learning by teaching."

@@ -221,7 +209,7 @@

- Special thanks + Special Thanks

Translators

-

The English version of this book was reviewed by the following translators. We thank them for their time and effort!

-
-

Code translators

-

The multilingual code versions of this book were made possible by the following translators. We appreciate their efforts and contributions!

+

Code Reviewers

+

The multilingual code versions of this book were made possible by the following developers. We appreciate their efforts and contributions!

diff --git a/en/docs/index.md b/en/docs/index.md index 59eb61d7a..36db754bc 100644 --- a/en/docs/index.md +++ b/en/docs/index.md @@ -1,5 +1,5 @@ # Hello Algo -Data structures and algorithms crash course with animated illustrations and off-the-shelf code +Data structures and algorithms tutorial with animated illustrations and ready-to-run code. -[Dive in](chapter_hello_algo/) +[Hello Algo](chapter_hello_algo/) diff --git a/en/mkdocs.yml b/en/mkdocs.yml index 0a39c9f37..f88473aaf 100644 --- a/en/mkdocs.yml +++ b/en/mkdocs.yml @@ -4,7 +4,7 @@ INHERIT: ../mkdocs.yml # Project information site_name: Hello Algo site_url: https://www.hello-algo.com/en/ -site_description: "Data Structures and Algorithms Crash Course with Animated Illustrations and Off-the-Shelf Code" +site_description: "Data structures and algorithms tutorial with animated illustrations and ready-to-run code" docs_dir: ../build/en/docs site_dir: ../site/en # Repository @@ -45,7 +45,7 @@ nav: - 0.1 About This Book: chapter_preface/about_the_book.md - 0.2 How to Use This Book: chapter_preface/suggestions.md - 0.3 Summary: chapter_preface/summary.md - - Chapter 1. Encounter With Algorithms: + - Chapter 1. Encounter with Algorithms: # [icon: material/calculator-variant-outline] - chapter_introduction/index.md - 1.1 Algorithms Are Everywhere: chapter_introduction/algorithms_are_everywhere.md @@ -67,20 +67,20 @@ nav: - 3.3 Number Encoding *: chapter_data_structure/number_encoding.md - 3.4 Character Encoding *: chapter_data_structure/character_encoding.md - 3.5 Summary: chapter_data_structure/summary.md - - Chapter 4. Array and Linked List: + - Chapter 4. Arrays and Linked Lists: # [icon: material/view-list-outline] - chapter_array_and_linkedlist/index.md - 4.1 Array: chapter_array_and_linkedlist/array.md - 4.2 Linked List: chapter_array_and_linkedlist/linked_list.md - 4.3 List: chapter_array_and_linkedlist/list.md - - 4.4 Memory and Cache *: chapter_array_and_linkedlist/ram_and_cache.md + - 4.4 Random-Access Memory and Cache *: chapter_array_and_linkedlist/ram_and_cache.md - 4.5 Summary: chapter_array_and_linkedlist/summary.md - - Chapter 5. Stack and Queue: + - Chapter 5. Stacks and Queues: # [icon: material/stack-overflow] - chapter_stack_and_queue/index.md - 5.1 Stack: chapter_stack_and_queue/stack.md - 5.2 Queue: chapter_stack_and_queue/queue.md - - 5.3 Double-Ended Queue: chapter_stack_and_queue/deque.md + - 5.3 Deque: chapter_stack_and_queue/deque.md - 5.4 Summary: chapter_stack_and_queue/summary.md - Chapter 6. Hashing: # [icon: material/table-search] @@ -94,7 +94,7 @@ nav: - chapter_tree/index.md - 7.1 Binary Tree: chapter_tree/binary_tree.md - 7.2 Binary Tree Traversal: chapter_tree/binary_tree_traversal.md - - 7.3 Array Representation of Tree: chapter_tree/array_representation_of_tree.md + - 7.3 Array Representation of Binary Trees: chapter_tree/array_representation_of_tree.md - 7.4 Binary Search Tree: chapter_tree/binary_search_tree.md - 7.5 AVL Tree *: chapter_tree/avl_tree.md - 7.6 Summary: chapter_tree/summary.md @@ -102,8 +102,8 @@ nav: # [icon: material/family-tree] - chapter_heap/index.md - 8.1 Heap: chapter_heap/heap.md - - 8.2 Building a Heap: chapter_heap/build_heap.md - - 8.3 Top-K Problem: chapter_heap/top_k.md + - 8.2 Heap Construction Operation: chapter_heap/build_heap.md + - 8.3 Top-k Problem: chapter_heap/top_k.md - 8.4 Summary: chapter_heap/summary.md - Chapter 9. Graph: # [icon: material/graphql] @@ -116,15 +116,15 @@ nav: # [icon: material/text-search] - chapter_searching/index.md - 10.1 Binary Search: chapter_searching/binary_search.md - - 10.2 Binary Search Insertion: chapter_searching/binary_search_insertion.md - - 10.3 Binary Search Edge Cases: chapter_searching/binary_search_edge.md + - 10.2 Binary Search Insertion Point: chapter_searching/binary_search_insertion.md + - 10.3 Binary Search Boundaries: chapter_searching/binary_search_edge.md - 10.4 Hash Optimization Strategy: chapter_searching/replace_linear_by_hashing.md - - 10.5 Search Algorithms Revisited: chapter_searching/searching_algorithm_revisited.md + - 10.5 Searching Algorithms Revisited: chapter_searching/searching_algorithm_revisited.md - 10.6 Summary: chapter_searching/summary.md - Chapter 11. Sorting: # [icon: material/sort-ascending] - chapter_sorting/index.md - - 11.1 Sorting Algorithms: chapter_sorting/sorting_algorithm.md + - 11.1 Sorting Algorithm: chapter_sorting/sorting_algorithm.md - 11.2 Selection Sort: chapter_sorting/selection_sort.md - 11.3 Bubble Sort: chapter_sorting/bubble_sort.md - 11.4 Insertion Sort: chapter_sorting/insertion_sort.md @@ -141,7 +141,7 @@ nav: - 12.1 Divide and Conquer Algorithms: chapter_divide_and_conquer/divide_and_conquer.md - 12.2 Divide and Conquer Search Strategy: chapter_divide_and_conquer/binary_search_recur.md - 12.3 Building a Binary Tree Problem: chapter_divide_and_conquer/build_binary_tree_problem.md - - 12.4 Hanoi Tower Problem: chapter_divide_and_conquer/hanota_problem.md + - 12.4 Hanota Problem: chapter_divide_and_conquer/hanota_problem.md - 12.5 Summary: chapter_divide_and_conquer/summary.md - Chapter 13. Backtracking: # [icon: material/map-marker-path] @@ -174,6 +174,6 @@ nav: - chapter_appendix/index.md - 16.1 Programming Environment Installation: chapter_appendix/installation.md - 16.2 Contributing Together: chapter_appendix/contribution.md - - 16.3 Terminology Table: chapter_appendix/terminology.md + - 16.3 Glossary: chapter_appendix/terminology.md - References: - chapter_reference/index.md diff --git a/ja/docs/chapter_appendix/terminology.md b/ja/docs/chapter_appendix/terminology.md index e967b2540..c456875ba 100644 --- a/ja/docs/chapter_appendix/terminology.md +++ b/ja/docs/chapter_appendix/terminology.md @@ -1,137 +1,134 @@ # 用語集 -以下の表は、本書に登場する重要な用語を一覧にしたものです。特に次の点に注意してください。 - -- 名詞の英語表現も覚えておくと、英語文献を読む際に役立ちます。 -- 一部の名詞は、簡体字中国語と繁体字中国語で呼び方が異なります。 +以下の表は、本書に登場する重要な用語を一覧にしたものです。英語文献を読む際に役立つよう、各名詞の英語表現も覚えておくことをおすすめします。

  データ構造とアルゴリズムの重要用語

-| 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 | ビッグオー記法 | ビッグオー記法 | -| asymptotic upper bound | 漸近上界 | 漸近上界 | -| sign-magnitude | 符号絶対値表現 | 符号絶対値表現 | -| 1’s complement | 1の補数 | 1の補数 | -| 2’s complement | 2の補数 | 2の補数 | -| 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 木 | 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$ 問題 | 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$ クイーン問題 | $n$ クイーン問題 | -| dynamic programming | 動的計画法 | 動的計画法 | -| initial state | 初期状態 | 初期状態 | -| state-transition equation | 状態遷移方程式 | 状態遷移方程式 | -| knapsack problem | ナップサック問題 | ナップサック問題 | -| edit distance problem | 編集距離問題 | 編集距離問題 | -| greedy algorithm | 貪欲法 | 貪欲法 | +| 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 | ビッグオー記法 | +| asymptotic upper bound | 漸近上界 | +| sign-magnitude | 符号絶対値表現 | +| 1’s complement | 1の補数 | +| 2’s complement | 2の補数 | +| 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 | 貪欲法 | diff --git a/ja/docs/chapter_computational_complexity/space_complexity.md b/ja/docs/chapter_computational_complexity/space_complexity.md index 8809b3e33..315bddc7d 100644 --- a/ja/docs/chapter_computational_complexity/space_complexity.md +++ b/ja/docs/chapter_computational_complexity/space_complexity.md @@ -806,8 +806,8 @@ $$ \begin{aligned} -O(1) < O(\log n) < O(n) < O(n^2) < O(2^n) \newline -\text{定数階} < \text{対数階} < \text{線形階} < \text{平方階} < \text{指数階} +& O(1) < O(\log n) < O(n) < O(n^2) < O(2^n) \newline +& \text{定数階} < \text{対数階} < \text{線形階} < \text{平方階} < \text{指数階} \end{aligned} $$ diff --git a/ja/docs/chapter_computational_complexity/time_complexity.md b/ja/docs/chapter_computational_complexity/time_complexity.md index 126a3eccb..b2567aef2 100644 --- a/ja/docs/chapter_computational_complexity/time_complexity.md +++ b/ja/docs/chapter_computational_complexity/time_complexity.md @@ -992,8 +992,8 @@ $$ $$ \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{階乗階} +& 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} $$ diff --git a/ja/docs/chapter_data_structure/character_encoding.md b/ja/docs/chapter_data_structure/character_encoding.md index 11504bafe..6aaa4e5e9 100644 --- a/ja/docs/chapter_data_structure/character_encoding.md +++ b/ja/docs/chapter_data_structure/character_encoding.md @@ -24,11 +24,9 @@ 当時の研究者たちはこう考えました。**十分に完全な文字セットを打ち出して、世界中のあらゆる言語と記号をそこに収録すれば、多言語環境や文字化けの問題を解決できるのではないか**。この発想に後押しされて、大規模で包括的な文字セット Unicode が誕生しました。 -Unicode の中国語名は「統一コード」であり、理論上は 100 万を超える文字を収容できます。Unicode は世界中の文字を 1 つの文字セットに統合することを目指し、さまざまな言語の文字を処理・表示できる汎用文字セットを提供することで、符号化規格の違いによる文字化けを減らそうとしています。 +Unicode の中国語名は「統一コード」であり、理論上は 100 万を超える文字を収容できます。Unicode は世界中の文字を 1 つの文字セットに統合することを目指し、さまざまな言語の文字を処理・表示できる汎用文字セットを提供することで、符号化規格の違いによる文字化けを減らそうとしています。1991 年の公開以来、Unicode は新しい言語と文字を継続的に拡充してきました。2022 年 9 月時点で、Unicode にはすでに 149186 文字が含まれており、各種言語の文字、記号、さらには絵文字まで収録されています。 -1991 年の公開以来、Unicode は新しい言語と文字を継続的に拡充してきました。2022 年 9 月時点で、Unicode にはすでに 149186 文字が含まれており、各種言語の文字、記号、さらには絵文字まで収録されています。Unicode は各文字をコードポイント(文字番号)に対応付けており、その値の範囲は 0 から 1114111(すなわち U+0000 から U+10FFFF)で、統一された文字番号空間を構成しています。 - -Unicode は汎用文字セットであり、本質的には各文字に番号(「コードポイント」)を割り当てるものですが、**それらのコードポイントをコンピュータ内でどのように保存するかまでは規定していません**。ここで疑問が生じます。長さの異なる Unicode コードポイントが同じテキストに現れたとき、システムはどのように文字を解析するのでしょうか。たとえば長さ 2 バイトの符号が与えられたとき、それが 2 バイトの 1 文字なのか、1 バイトの 2 文字なのかをどう判定するのでしょうか。 +Unicode は汎用文字セットとして、本質的には各文字に固有の「コードポイント」(文字番号)を割り当てており、その範囲は U+0000 から U+10FFFF までで、統一された文字番号空間を構成しています。しかし、**Unicode はそれらのコードポイントをコンピュータ内でどのように保存するかまでは規定していません**。ここで疑問が生じます。長さの異なる Unicode コードポイントが同じテキストに現れたとき、システムはどのように文字を解析するのでしょうか。たとえば長さ 2 バイトの符号が与えられたとき、それが 2 バイトの 1 文字なのか、1 バイトの 2 文字なのかをどう判定するのでしょうか。 この問題に対して、**すべての文字を固定長の符号として保存する**という直接的な解決策があります。下図のように、「Hello」の各文字は 1 バイト、「アルゴリズム」の各文字は 2 バイトを占めます。上位ビットを 0 で埋めることで、「Hello アルゴリズム」のすべての文字を 2 バイト長にエンコードできます。こうすれば、システムは 2 バイトごとに 1 文字を解析して、この語句の内容を復元できます。 diff --git a/ja/docs/chapter_paperbook/index.md b/ja/docs/chapter_paperbook/index.md deleted file mode 100644 index efc58cbde..000000000 --- a/ja/docs/chapter_paperbook/index.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -comments: true -icon: material/book-open-page-variant -status: new ---- - -# 紙の書籍 - -長い時間をかけて磨き上げた『Hello アルゴリズム』の紙の書籍が、ついに発売されました!今の気持ちは、次の一節で表せます: - -

風を追い月を追って立ち止まるな、草原の果てには春の山がある。

- -![](index.assets/paper_book_overview.jpg){ class="animation-figure" } - -以下の動画では紙の書籍を紹介しており、私の考えもいくつか含まれています: - -- データ構造とアルゴリズムを学ぶ重要性。 -- なぜ紙の書籍で Python を選んだのか。 -- 知識共有に対する理解。 - -> 新人 UP 主ですので、ぜひ応援と高評価・チャンネル登録をお願いします~ありがとうございます! - -
- -
- -紙の書籍のスナップショット: - -![](index.assets/paper_book_chapter_heap.jpg){ class="animation-figure" } - -![](index.assets/paper_book_avl_tree.jpg){ class="animation-figure" } - -## 長所と短所 - -紙の書籍ならではの魅力を、簡単にまとめると次のとおりです: - -- フルカラー印刷を採用し、本書の「アニメーション図解」の強みをそのまま活かせます。 -- 紙の素材にもこだわり、色彩を高い精度で再現しつつ、紙の書籍ならではの質感も残しています。 -- 紙の書籍版は Web 版よりも書式が整っており、たとえば図中の数式には斜体を用いています。 -- 価格を上げずに、マインドマップの折り込みページやしおりも付属します。 -- 紙の書籍、Web 版、PDF 版で内容は同期しており、自由に切り替えて読めます。 - -!!! tip - - 紙の書籍と Web 版を同期させるのは難しいため、細かな違いが生じる場合があります。ご了承ください! - -もちろん、購入前に検討しておくべき点もいくつかあります: - -- Python 言語を使用しているため、あなたの主言語と合わない可能性があります(Python は疑似コードと捉え、考え方の理解を重視してください)。 -- フルカラー印刷は図解やコードの読みやすさを大きく高める一方で、白黒印刷より価格はやや高くなります。 - -!!! tip - - 「印刷品質」と「価格」は、アルゴリズムにおける「時間効率」と「空間効率」のようなもので、両立は容易ではありません。そして私は、「印刷品質」は「時間効率」に当たるため、より重視すべきだと考えています。 - -## 購入リンク - -紙の書籍に興味があれば、ぜひ一冊ご検討ください。新刊の 5 割引を用意していただきましたので、[こちらのリンク](https://3.cn/1X-qmTD3)をご覧いただくか、以下の QR コードをスキャンしてください: - -![](index.assets/book_jd_link.jpg){ class="animation-figure" } - -## あとがき - -当初、私は紙の書籍出版に必要な作業量を甘く見ていて、オープンソースプロジェクトをきちんと保守していれば、紙の書籍版も何らかの自動化手段で生成できると思っていました。実際には、紙の書籍の制作フローとオープンソースプロジェクトの更新の仕組みには大きな違いがあり、その間をつなぐには多くの追加作業が必要でした。 - -一冊の本の初稿と出版基準を満たす完成稿との間には、なお大きな隔たりがあります。出版社(企画、編集、デザイン、マーケティングなど)と著者が力を合わせ、長い時間をかけて磨き上げていく必要があります。ここで、図霊の企画編集者である王軍花さん、そして人民郵電出版社と図霊コミュニティで本書の出版工程に携わってくださったすべての皆さまに感謝いたします! - -この本があなたの助けになれば幸いです! diff --git a/ja/docs/chapter_preface/about_the_book.md b/ja/docs/chapter_preface/about_the_book.md index b671987f2..58ef8ae48 100644 --- a/ja/docs/chapter_preface/about_the_book.md +++ b/ja/docs/chapter_preface/about_the_book.md @@ -30,13 +30,13 @@ ## 謝辞 -本書は、オープンソースコミュニティの多くの貢献者による共同の努力のもとで、継続的に改善されています。時間と労力を注いでくださったすべての執筆者の皆さんに感謝します。お名前は次のとおりです(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、IsChristina、xBLACKICEx、guowei-gong、Cathay-Chen、pengchzn、QiLOL、magentaqin、hello-ikun、JoseHung、qualifier1024、thomasq0、sunshinesDL、L-Super、Guanngxu、Transmigration-zhou、WSL0809、Slone123c、lhxsm、yuan0221、what-is-me、Shyam-Chen、theNefelibatas、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、MolDuM、Nigh、Dr-XYZ、XC-Zero、reeswell、PXG-XPG、NI-SW、Horbin-Magician、Enlightenus、YangXuanyi、beatrix-chan、DullSword、xjr7670、jiaxianhua、qq909244296、iStig、boloboloda、hts0000、gledfish、wenjianmin、keshida、kilikilikid、lclc6、lwbaptx、linyejoe2、liuxjerry、llql1211、fbigm、echo1937、szu17dmy、dshlstarr、Yucao-cy、coderlef、czruby、bongbongbakudan、beintentional、ZongYangL、ZhongYuuu、ZhongGuanbin、hezhizhen、linzeyan、ZJKung、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、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、GaochaoZhu、hopkings2008、yang-le、realwujing、Evilrabbit520、Umer-Jahangir、Turing-1024-Lee、Suremotoo、paoxiaomooo、Chieko-Seren、Allen-Scai、ymmmas、Risuntsy、Richard-Zhang1019、RafaelCaso、qingpeng9802、primexiao、Urbaner3、zhongfq、nidhoggfgg、MwumLi、CreatorMetaSky、martinx、ZnYang2018、hugtyftg、logan-qiu、psychelzh、Keynman、KeiichiKasai と KawaiiAsh。 +本書は、オープンソースコミュニティの多くの貢献者による共同の努力のもとで、継続的に改善されています。時間と労力を注いでくださったすべての執筆者の皆さんに感謝します。お名前は次のとおりです(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 によって行われました(アルファベット順)。彼らが費やしてくれた時間と労力に感謝します。各言語のコードの規範性と統一性が保たれているのは、まさに彼らのおかげです。 -本書の繁体字中国語版は Shyam-Chen と Dr-XYZ がレビューし、英語版は yuelinxin、K3v123、QiLOL、Phoenix0415、SamJin98、yanedie、RafaelCaso、pengchzn、thomasq0 と magentaqin がレビューし、日本語版は eltociear がレビューしました。彼らの継続的な貢献があってこそ、本書はより幅広い読者に届けられています。感謝いたします。 +本書の英語版は yuelinxin、K3v123、magentaqin、QiLOL、Phoenix0415、SamJin98、yanedie、RafaelCaso、pengchzn と thomasq0 がレビューし、日本語版は eltociear がレビューし、ロシア語版は И. А. Шевкун と Yuyan Huang がレビューし、繁体字中国語版は Shyam-Chen と Dr-XYZ がレビューしました。彼らの貢献があってこそ、本書はより幅広い読者に届けられています。感謝いたします。 -本書の ePub 電子書籍生成ツールは zhongfq によって開発されました。彼の貢献に感謝します。読者により自由な読書方法を提供してくれました。 +本書の ePub 電子書籍生成ツールは zhongfq によって開発されました。彼の貢献に感謝します。読者により柔軟な読書方法を提供してくれました。 本書の執筆過程で、私は多くの方々の助けを得ました。 @@ -45,7 +45,7 @@ - 腾宝、琦宝、飞宝が本書に創造性あふれる名前を付けてくれたことに感謝します。みんなが最初のコード行「Hello World!」を書いた美しい記憶を呼び起こしてくれました; - 校铨が知的財産の面で専門的な支援をしてくれたことに感謝します。これは本オープンソース書籍の改善に重要な役割を果たしました; - 苏潼が本書の美しい表紙と logo をデザインし、私の完璧主義につき合って何度も辛抱強く修正してくれたことに感謝します; -- @squidfunk が組版に関する助言を提供してくれたこと、そして彼が開発したオープンソースのドキュメントテーマ [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material/tree/master) に感謝します。 +- @squidfunk が組版に関する助言を提供してくれたこと、そして彼が開発したオープンソースのドキュメントテーマ [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material) に感謝します。 執筆の過程で、私はデータ構造とアルゴリズムに関する多くの教材や記事を読みました。これらの作品は本書に優れた手本を与え、本書の内容の正確性と品質を支えてくれました。ここに、すべての先生方と先人たちの卓越した貢献に感謝します! diff --git a/ja/docs/index.html b/ja/docs/index.html index 68aef3153..da2524efc 100644 --- a/ja/docs/index.html +++ b/ja/docs/index.html @@ -105,18 +105,6 @@ -
@@ -265,7 +253,7 @@
-

コード翻訳者

+

コードレビュアー

本書の多言語コード版は、以下の開発者の協力によって実現しました。ご尽力に感謝します!

diff --git a/overrides/partials/comments.html b/overrides/partials/comments.html index e56e60213..24f227b7d 100644 --- a/overrides/partials/comments.html +++ b/overrides/partials/comments.html @@ -43,27 +43,21 @@