diff --git a/chapter_array_and_linkedlist/array/index.html b/chapter_array_and_linkedlist/array/index.html
index 2fcce81e7..cc07d48b5 100644
--- a/chapter_array_and_linkedlist/array/index.html
+++ b/chapter_array_and_linkedlist/array/index.html
@@ -3763,8 +3763,8 @@
可视化运行
-
-全屏观看 >
数组元素被存储在连续的内存空间中,这意味着计算数组元素的内存地址非常容易。给定数组内存地址(首元素内存地址)和某个元素的索引,我们可以使用图 4-2 所示的公式计算得到该元素的内存地址,从而直接访问该元素。
@@ -3911,8 +3911,8 @@数组元素在内存中是“紧挨着的”,它们之间没有空间再存放任何数据。如图 4-3 所示,如果想在数组中间插入一个元素,则需要将该元素之后的所有元素都向后移动一位,之后再把元素赋值给该索引。
@@ -4069,8 +4069,8 @@同理,如图 4-4 所示,若想删除索引 \(i\) 处的元素,则需要把索引 \(i\) 之后的元素都向前移动一位。
@@ -4204,8 +4204,8 @@总的来看,数组的插入与删除操作有以下缺点。
在数组中查找指定元素需要遍历数组,每轮判断元素值是否匹配,若匹配则输出对应索引。
@@ -4551,8 +4551,8 @@在复杂的系统环境中,程序难以保证数组之后的内存空间是可用的,从而无法安全地扩展数组容量。因此在大多数编程语言中,数组的长度是不可变的。
@@ -4738,8 +4738,8 @@数组存储在连续的内存空间内,且元素类型相同。这种做法包含丰富的先验信息,系统可以利用这些信息来优化数据结构的操作效率。
diff --git a/chapter_array_and_linkedlist/linked_list/index.html b/chapter_array_and_linkedlist/linked_list/index.html index 0adb936f1..b764dcc5f 100644 --- a/chapter_array_and_linkedlist/linked_list/index.html +++ b/chapter_array_and_linkedlist/linked_list/index.html @@ -3999,8 +3999,8 @@数组整体是一个变量,比如数组 nums 包含元素 nums[0] 和 nums[1] 等,而链表是由多个独立的节点对象组成的。我们通常将头节点当作链表的代称,比如以上代码中的链表可记作链表 n0 。
如图 4-7 所示,在链表中删除节点也非常方便,只需改变一个节点的引用(指针)即可。
@@ -4289,8 +4289,8 @@在链表中访问节点的效率较低。如上一节所述,我们可以在 \(O(1)\) 时间下访问数组中的任意元素。链表则不然,程序需要从头节点出发,逐个向后遍历,直至找到目标节点。也就是说,访问链表的第 \(i\) 个节点需要循环 \(i - 1\) 轮,时间复杂度为 \(O(n)\) 。
@@ -4446,8 +4446,8 @@遍历链表,查找其中值为 target 的节点,输出该节点在链表中的索引。此过程也属于线性查找。代码如下所示:
表 4-1 总结了数组和链表的各项特点并对比了操作效率。由于它们采用两种相反的存储策略,因此各种性质和操作效率也呈现对立的特点。
diff --git a/chapter_array_and_linkedlist/list/index.html b/chapter_array_and_linkedlist/list/index.html index 956825c93..9b46fbed9 100644 --- a/chapter_array_and_linkedlist/list/index.html +++ b/chapter_array_and_linkedlist/list/index.html @@ -3746,8 +3746,8 @@列表本质上是数组,因此可以在 \(O(1)\) 时间内访问和更新元素,效率很高。
@@ -3848,8 +3848,8 @@相较于数组,列表可以自由地添加与删除元素。在列表尾部添加元素的时间复杂度为 \(O(1)\) ,但插入和删除元素的效率仍与数组相同,时间复杂度为 \(O(n)\) 。
@@ -4061,8 +4061,8 @@与数组一样,列表可以根据索引遍历,也可以直接遍历各元素。
@@ -4227,8 +4227,8 @@给定一个新列表 nums1 ,我们可以将其拼接到原列表的尾部。
完成列表排序后,我们便可以使用在数组类算法题中经常考查的“二分查找”和“双指针”算法。
@@ -4381,8 +4381,8 @@许多编程语言内置了列表,例如 Java、C++、Python 等。它们的实现比较复杂,各个参数的设定也非常考究,例如初始容量、扩容倍数等。感兴趣的读者可以查阅源码进行学习。
@@ -5624,8 +5624,8 @@图 13-1 在前序遍历中搜索节点
@@ -4013,8 +4013,8 @@在每次“尝试”中,我们通过将当前节点添加进 path 来记录路径;而在“回退”前,我们需要将该节点从 path 中弹出,以恢复本次尝试之前的状态。
观察图 13-2 所示的过程,我们可以将尝试和回退理解为“前进”与“撤销”,两个操作互为逆向。
@@ -4304,8 +4304,8 @@“剪枝”是一个非常形象的名词。如图 13-3 所示,在搜索过程中,我们“剪掉”了不满足约束条件的搜索分支,避免许多无意义的尝试,从而提高了搜索效率。
@@ -5139,8 +5139,8 @@根据题意,我们在找到值为 \(7\) 的节点后应该继续搜索,因此需要将记录解之后的 return 语句删除。图 13-4 对比了保留或删除 return 语句的搜索过程。
逐行放置 \(n\) 次,考虑列约束,则从第一行到最后一行分别有 \(n\)、\(n-1\)、\(\dots\)、\(2\)、\(1\) 个选择,因此时间复杂度为 \(O(n!)\) 。实际上,根据对角线约束的剪枝也能够大幅缩小搜索空间,因而搜索效率往往优于以上时间复杂度。
数组 state 使用 \(O(n^2)\) 空间,数组 cols、diags1 和 diags2 皆使用 \(O(n)\) 空间。最大递归深度为 \(n\) ,使用 \(O(n)\) 栈帧空间。因此,空间复杂度为 \(O(n^2)\) 。
假设元素两两之间互不相同,则 \(n\) 个元素共有 \(n!\) 种排列(阶乘);在记录结果时,需要复制长度为 \(n\) 的列表,使用 \(O(n)\) 时间。因此时间复杂度为 \(O(n!n)\) 。
最大递归深度为 \(n\) ,使用 \(O(n)\) 栈帧空间。selected 使用 \(O(n)\) 空间。同一时刻最多共有 \(n\) 个 duplicated ,使用 \(O(n^2)\) 空间。因此空间复杂度为 \(O(n^2)\) 。
向以上代码输入数组 \([3, 4, 5]\) 和目标元素 \(9\) ,输出结果为 \([3, 3, 3], [4, 5], [5, 4]\) 。虽然成功找出了所有和为 \(9\) 的子集,但其中存在重复的子集 \([4, 5]\) 和 \([5, 4]\) 。
这是因为搜索过程是区分选择顺序的,然而子集不区分选择顺序。如图 13-10 所示,先选 \(4\) 后选 \(5\) 与先选 \(5\) 后选 \(4\) 是不同的分支,但对应同一个子集。
@@ -4486,8 +4486,8 @@图 13-12 所示为将数组 \([3, 4, 5]\) 和目标元素 \(9\) 输入以上代码后的整体回溯过程。
@@ -4980,8 +4980,8 @@图 13-14 展示了数组 \([4, 4, 5]\) 和目标元素 \(9\) 的回溯过程,共包含四种剪枝操作。请你将图示与代码注释相结合,理解整个搜索过程,以及每种剪枝操作是如何工作的。
diff --git a/chapter_computational_complexity/iteration_and_recursion/index.html b/chapter_computational_complexity/iteration_and_recursion/index.html index 80a08bdfa..7e7ac2422 100644 --- a/chapter_computational_complexity/iteration_and_recursion/index.html +++ b/chapter_computational_complexity/iteration_and_recursion/index.html @@ -3819,8 +3819,8 @@图 2-1 是该求和函数的流程框图。
@@ -4004,8 +4004,8 @@while 循环比 for 循环的自由度更高。在 while 循环中,我们可以自由地设计条件变量的初始化和更新步骤。
例如在以下代码中,条件变量 \(i\) 每轮进行两次更新,这种情况就不太方便用 for 循环实现:
总的来说,for 循环的代码更加紧凑,while 循环更加灵活,两者都可以实现迭代结构。选择使用哪一个应该根据特定问题的需求来决定。
图 2-2 是该嵌套循环的流程框图。
@@ -4586,8 +4586,8 @@图 2-3 展示了该函数的递归过程。
@@ -4759,8 +4759,8 @@尾递归的执行过程如图 2-5 所示。对比普通递归和尾递归,两者的求和操作的执行点是不同的。
观察以上代码,我们在函数内递归调用了两个函数,这意味着从一个调用产生了两个调用分支。如图 2-6 所示,这样不断递归调用下去,最终将产生一棵层数为 \(n\) 的「递归树 recursion tree」。
@@ -5267,8 +5267,8 @@观察以上代码,当递归转化为迭代后,代码变得更加复杂了。尽管迭代和递归在很多情况下可以互相转化,但不一定值得这样做,有以下两点原因。
线性阶常见于元素数量与 \(n\) 成正比的数组、链表、栈、队列等:
@@ -4862,8 +4862,8 @@ O(1) < O(\log n) < O(n) < O(n^2) < O(2^n) \newline如图 2-17 所示,此函数的递归深度为 \(n\) ,即同时存在 \(n\) 个未返回的 linear_recur() 函数,使用 \(O(n)\) 大小的栈帧空间:
图 2-17 递归函数产生的线性阶空间复杂度
@@ -5189,8 +5189,8 @@ O(1) < O(\log n) < O(n) < O(n^2) < O(2^n) \newline如图 2-18 所示,该函数的递归深度为 \(n\) ,在每个递归函数中都初始化了一个数组,长度分别为 \(n\)、\(n-1\)、\(\dots\)、\(2\)、\(1\) ,平均长度为 \(n / 2\) ,因此总体占用 \(O(n^2)\) 空间:
图 2-18 递归函数产生的平方阶空间复杂度
@@ -5487,8 +5487,8 @@ O(1) < O(\log n) < O(n) < O(n^2) < O(2^n) \newline图 2-19 满二叉树产生的指数阶空间复杂度
diff --git a/chapter_computational_complexity/time_complexity/index.html b/chapter_computational_complexity/time_complexity/index.html index 173a86d96..a8421b4e0 100644 --- a/chapter_computational_complexity/time_complexity/index.html +++ b/chapter_computational_complexity/time_complexity/index.html @@ -4761,8 +4761,8 @@ O(1) < O(\log n) < O(n) < O(n \log n) < O(n^2) < O(2^n) < O(n!线性阶的操作数量相对于输入数据大小 \(n\) 以线性级别增长。线性阶通常出现在单层循环中:
@@ -4896,8 +4896,8 @@ O(1) < O(\log n) < O(n) < O(n \log n) < O(n^2) < O(2^n) < O(n!遍历数组和遍历链表等操作的时间复杂度均为 \(O(n)\) ,其中 \(n\) 为数组或链表的长度:
值得注意的是,输入数据大小 \(n\) 需根据输入数据的类型来具体确定。比如在第一个示例中,变量 \(n\) 为输入数据大小;在第二个示例中,数组长度 \(n\) 为数据大小。
图 2-10 对比了常数阶、线性阶和平方阶三种时间复杂度。
@@ -5489,8 +5489,8 @@ O(1) < O(\log n) < O(n) < O(n \log n) < O(n^2) < O(2^n) < O(n!生物学的“细胞分裂”是指数阶增长的典型例子:初始状态为 \(1\) 个细胞,分裂一轮后变为 \(2\) 个,分裂两轮后变为 \(4\) 个,以此类推,分裂 \(n\) 轮后有 \(2^n\) 个细胞。
@@ -5699,8 +5699,8 @@ O(1) < O(\log n) < O(n) < O(n \log n) < O(n^2) < O(2^n) < O(n!图 2-11 指数阶的时间复杂度
@@ -5817,8 +5817,8 @@ O(1) < O(\log n) < O(n) < O(n \log n) < O(n^2) < O(2^n) < O(n!指数阶增长非常迅速,在穷举法(暴力搜索、回溯等)中比较常见。对于数据规模较大的问题,指数阶是不可接受的,通常需要使用动态规划或贪心算法等来解决。
图 2-12 对数阶的时间复杂度
@@ -6093,8 +6093,8 @@ O(1) < O(\log n) < O(n) < O(n \log n) < O(n^2) < O(2^n) < O(n!对数阶常出现于基于分治策略的算法中,体现了“一分为多”和“化繁为简”的算法思想。它增长缓慢,是仅次于常数阶的理想的时间复杂度。
图 2-13 展示了线性对数阶的生成方式。二叉树的每一层的操作总数都为 \(n\) ,树共有 \(\log_2 n + 1\) 层,因此时间复杂度为 \(O(n \log n)\) 。
@@ -6450,8 +6450,8 @@ n! = n \times (n - 1) \times (n - 2) \times \dots \times 2 \times 1图 2-14 阶乘阶的时间复杂度
@@ -6793,8 +6793,8 @@ n! = n \times (n - 1) \times (n - 2) \times \dots \times 2 \times 1值得说明的是,我们在实际中很少使用最佳时间复杂度,因为通常只有在很小概率下才能达到,可能会带来一定的误导性。而最差时间复杂度更为实用,因为它给出了一个效率安全值,让我们可以放心地使用算法。
从上述示例可以看出,最差时间复杂度和最佳时间复杂度只出现于“特殊的数据分布”,这些情况的出现概率可能很小,并不能真实地反映算法运行效率。相比之下,平均时间复杂度可以体现算法在随机输入数据下的运行效率,用 \(\Theta\) 记号来表示。
diff --git a/chapter_data_structure/basic_data_types/index.html b/chapter_data_structure/basic_data_types/index.html index 39fad384d..b42a669ef 100644 --- a/chapter_data_structure/basic_data_types/index.html +++ b/chapter_data_structure/basic_data_types/index.html @@ -3645,8 +3645,8 @@图 12-8 展示了构建二叉树的递归过程,各个节点是在向下“递”的过程中建立的,而各条边(引用)是在向上“归”的过程中建立的。
如图 12-15 所示,汉诺塔问题形成一棵高度为 \(n\) 的递归树,每个节点代表一个子问题,对应一个开启的 dfs() 函数,因此时间复杂度为 \(O(2^n)\) ,空间复杂度为 \(O(n)\) 。
图 14-7 展示了以上代码的动态规划过程。
@@ -3998,8 +3998,8 @@ dp[i] = \min(dp[i-1], dp[i-2]) + cost[i]无后效性是动态规划能够有效解决问题的重要特性之一,其定义为:给定一个确定的状态,它的未来发展只与当前状态有关,而与过去经历的所有状态无关。
@@ -4303,8 +4303,8 @@ dp[i, 2] = dp[i-2, 1] + dp[i-2, 2]在上面的案例中,由于仅需多考虑前面一个状态,因此我们仍然可以通过扩展状态定义,使得问题重新满足无后效性。然而,某些问题具有非常严重的“有后效性”。
图 14-14 给出了以 \(dp[2, 1]\) 为根节点的递归树,其中包含一些重叠子问题,其数量会随着网格 grid 的尺寸变大而急剧增多。
从本质上看,造成重叠子问题的原因为:存在多条路径可以从左上角到达某一单元格。
@@ -4223,8 +4223,8 @@ dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j]如图 14-15 所示,在引入记忆化后,所有子问题的解只需计算一次,因此时间复杂度取决于状态总数,即网格尺寸 \(O(nm)\) 。
@@ -4551,8 +4551,8 @@ dp[i, j] = \min(dp[i-1, j], dp[i, j-1]) + grid[i, j]图 14-16 展示了最小路径和的状态转移过程,其遍历了整个网格,因此时间复杂度为 \(O(nm)\) 。
数组 dp 大小为 \(n \times m\) ,因此空间复杂度为 \(O(nm)\) 。
如图 14-30 所示,编辑距离问题的状态转移过程与背包问题非常类似,都可以看作填写一个二维网格的过程。
回溯算法通常并不显式地对问题进行拆解,而是将求解问题看作一系列决策步骤,通过试探和剪枝,搜索所有可能的解。
@@ -4136,8 +4136,8 @@ dp[i] = dp[i-1] + dp[i-2]图 14-3 展示了暴力搜索形成的递归树。对于问题 \(dp[n]\) ,其递归树的深度为 \(n\) ,时间复杂度为 \(O(2^n)\) 。指数阶属于爆炸式增长,如果我们输入一个比较大的 \(n\) ,则会陷入漫长的等待之中。
@@ -4452,8 +4452,8 @@ dp[i] = dp[i-1] + dp[i-2]观察图 14-4 ,经过记忆化处理后,所有重叠子问题都只需计算一次,时间复杂度优化至 \(O(n)\) ,这是一个巨大的飞跃。
@@ -4685,8 +4685,8 @@ dp[i] = dp[i-1] + dp[i-2]图 14-5 模拟了以上代码的执行过程。
@@ -4883,8 +4883,8 @@ dp[i] = dp[i-1] + dp[i-2]观察以上代码,由于省去了数组 dp 占用的空间,因此空间复杂度从 \(O(n)\) 降至 \(O(1)\) 。
在动态规划问题中,当前状态往往仅与前面有限个状态有关,这时我们可以只保留必要的状态,通过“降维”来节省内存空间。这种空间优化技巧被称为“滚动变量”或“滚动数组”。
diff --git a/chapter_dynamic_programming/knapsack_problem/index.html b/chapter_dynamic_programming/knapsack_problem/index.html index f563d7815..b0e08aff0 100644 --- a/chapter_dynamic_programming/knapsack_problem/index.html +++ b/chapter_dynamic_programming/knapsack_problem/index.html @@ -3826,8 +3826,8 @@ dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1])如图 14-18 所示,由于每个物品都会产生不选和选两条搜索分支,因此时间复杂度为 \(O(2^n)\) 。
观察递归树,容易发现其中存在重叠子问题,例如 \(dp[1, 10]\) 等。而当物品较多、背包容量较大,尤其是相同重量的物品较多时,重叠子问题的数量将会大幅增多。
@@ -4143,8 +4143,8 @@ dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1])图 14-19 展示了在记忆化搜索中被剪掉的搜索分支。
@@ -4443,8 +4443,8 @@ dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1])如图 14-20 所示,时间复杂度和空间复杂度都由数组 dp 大小决定,即 \(O(n \times cap)\) 。
由于当前状态是从左边和上边的状态转移而来的,因此空间优化后应该对 \(dp\) 表中的每一行进行正序遍历。
@@ -4353,8 +4353,8 @@ dp[i, c] = \max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1])背包问题是一大类动态规划问题的代表,其拥有很多变种,例如零钱兑换问题。
@@ -4738,8 +4738,8 @@ dp[i, a] = \min(dp[i-1, a], dp[i, a - coins[i-1]] + 1)图 14-25 展示了零钱兑换的动态规划过程,和完全背包问题非常相似。
空间优化处理方式相同,删除硬币维度即可:
@@ -5739,8 +5739,8 @@ dp[i, a] = dp[i-1, a] + dp[i, a - coins[i-1]]设无向图的顶点总数为 \(n\)、边总数为 \(m\) ,则可根据图 9-8 所示的方法实现各种操作。
@@ -5564,8 +5564,8 @@设图中共有 \(n\) 个顶点和 \(m\) 条边,表 9-2 对比了邻接矩阵和邻接表的时间效率和空间效率。
diff --git a/chapter_graph/graph_traversal/index.html b/chapter_graph/graph_traversal/index.html index 2e282a3e9..2eb404c28 100644 --- a/chapter_graph/graph_traversal/index.html +++ b/chapter_graph/graph_traversal/index.html @@ -4000,8 +4000,8 @@代码相对抽象,建议对照图 9-10 来加深理解。
深度优先遍历的算法流程如图 9-12 所示。
除排序之外,在最差情况下,需要遍历整个物品列表,因此时间复杂度为 \(O(n)\) ,其中 \(n\) 为物品数量。
由于初始化了一个 Item 对象列表,因此空间复杂度为 \(O(n)\) 。
你可能会不由地发出感叹:So clean !贪心算法仅用约十行代码就解决了零钱兑换问题。
之所以贪心比穷举更快,是因为每轮的贪心选择都会“跳过”一些状态。
diff --git a/chapter_greedy/max_product_cutting_problem/index.html b/chapter_greedy/max_product_cutting_problem/index.html index 7559e4146..20fba8d62 100644 --- a/chapter_greedy/max_product_cutting_problem/index.html +++ b/chapter_greedy/max_product_cutting_problem/index.html @@ -3840,8 +3840,8 @@ n = 3 a + b图 15-16 最大切分乘积的计算方法
diff --git a/chapter_hashing/hash_algorithm/index.html b/chapter_hashing/hash_algorithm/index.html index 422b999ba..c97ea4373 100644 --- a/chapter_hashing/hash_algorithm/index.html +++ b/chapter_hashing/hash_algorithm/index.html @@ -4084,8 +4084,8 @@观察发现,每种哈希算法的最后一步都是对大质数 \(1000000007\) 取模,以确保哈希值在合适的范围内。值得思考的是,为什么要强调对质数取模,或者说对合数取模的弊端是什么?这是一个有趣的问题。
先抛出结论:使用大质数作为模数,可以最大化地保证哈希值的均匀分布。因为质数不与其他数字存在公约数,可以减少因取模操作而产生的周期性模式,从而避免哈希冲突。
@@ -4399,8 +4399,8 @@在许多编程语言中,只有不可变对象才可作为哈希表的 key 。假如我们将列表(动态数组)作为 key ,当列表的内容发生变化时,它的哈希值也随之改变,我们就无法在哈希表中查询到原先的 value 了。
虽然自定义对象(比如链表节点)的成员变量是可变的,但它是可哈希的。这是因为对象的哈希值通常是基于内存地址生成的,即使对象的内容发生了变化,但它的内存地址不变,哈希值仍然是不变的。
diff --git a/chapter_hashing/hash_collision/index.html b/chapter_hashing/hash_collision/index.html index 674bd12b4..cab85563e 100644 --- a/chapter_hashing/hash_collision/index.html +++ b/chapter_hashing/hash_collision/index.html @@ -4888,8 +4888,8 @@值得注意的是,当链表很长时,查询效率 \(O(n)\) 很差。此时可以将链表转换为“AVL 树”或“红黑树”,从而将查询操作的时间复杂度优化至 \(O(\log n)\) 。
哈希表有三种常用的遍历方式:遍历键值对、遍历键和遍历值。示例代码如下:
我们先考虑最简单的情况,仅用一个数组来实现哈希表。在哈希表中,我们将数组中的每个空位称为「桶 bucket」,每个桶可存储一个键值对。因此,查询操作就是找到 key 对应的桶,并在桶中获取 value 。
从本质上看,哈希函数的作用是将所有 key 构成的输入空间映射到数组所有索引构成的输出空间,而输入空间往往远大于输出空间。因此,理论上一定存在“多个输入对应相同输出”的情况。
下面,我们来尝试推算第二种建堆方法的时间复杂度。
diff --git a/chapter_heap/heap/index.html b/chapter_heap/heap/index.html index 17922881c..406c3bffb 100644 --- a/chapter_heap/heap/index.html +++ b/chapter_heap/heap/index.html @@ -3967,8 +3967,8 @@下文实现的是大顶堆。若要将其转换为小顶堆,只需将所有大小逻辑判断取逆(例如,将 \(\geq\) 替换为 \(\leq\) )。感兴趣的读者可以自行实现。
@@ -4278,8 +4278,8 @@给定元素 val ,我们首先将其添加到堆底。添加之后,由于 val 可能大于堆中其他元素,堆的成立条件可能已被破坏,因此需要修复从插入节点到根节点的路径上的各个节点,这个操作被称为「堆化 heapify」。
堆顶元素是二叉树的根节点,即列表首元素。如果我们直接从列表中删除首元素,那么二叉树中所有节点的索引都会发生变化,这将使得后续使用堆化进行修复变得困难。为了尽量减少元素索引的变动,我们采用以下操作步骤。
@@ -5137,8 +5137,8 @@总共执行了 \(n\) 轮入堆和出堆,堆的最大长度为 \(k\) ,因此时间复杂度为 \(O(n \log k)\) 。该方法的效率很高,当 \(k\) 较小时,时间复杂度趋向 \(O(n)\) ;当 \(k\) 较大时,时间复杂度不会超过 \(O(n \log n)\) 。
另外,该方法适用于动态数据流的使用场景。在不断加入数据时,我们可以持续维护堆内的元素,从而实现最大的 \(k\) 个元素的动态更新。
diff --git a/chapter_searching/binary_search/index.html b/chapter_searching/binary_search/index.html index 60eefcfe2..5b63d5ac1 100644 --- a/chapter_searching/binary_search/index.html +++ b/chapter_searching/binary_search/index.html @@ -3827,8 +3827,8 @@时间复杂度为 \(O(\log n)\) :在二分循环中,区间每轮缩小一半,循环次数为 \(\log_2 n\) 。
空间复杂度为 \(O(1)\) :指针 \(i\) 和 \(j\) 使用常数大小空间。
@@ -4098,8 +4098,8 @@如图 10-3 所示,在两种区间表示下,二分查找算法的初始化、循环条件和缩小区间操作皆有所不同。
由于“双闭区间”表示中的左右边界都被定义为闭区间,因此通过指针 \(i\) 和指针 \(j\) 缩小区间的操作也是对称的。这样更不容易出错,因此一般建议采用“双闭区间”的写法。
diff --git a/chapter_searching/binary_search_edge/index.html b/chapter_searching/binary_search_edge/index.html index 6d4cda6a9..816e96a9d 100644 --- a/chapter_searching/binary_search_edge/index.html +++ b/chapter_searching/binary_search_edge/index.html @@ -3737,8 +3737,8 @@那么如何查找最右一个 target 呢?最直接的方式是修改代码,替换在 nums[m] == target 情况下的指针收缩操作。代码在此省略,有兴趣的读者可以自行实现。
我们知道,当数组不包含 target 时,最终 \(i\) 和 \(j\) 会分别指向首个大于、小于 target 的元素。
Tip
diff --git a/chapter_searching/replace_linear_by_hashing/index.html b/chapter_searching/replace_linear_by_hashing/index.html index ed80ed9d0..c3338552e 100644 --- a/chapter_searching/replace_linear_by_hashing/index.html +++ b/chapter_searching/replace_linear_by_hashing/index.html @@ -3717,8 +3717,8 @@此方法的时间复杂度为 \(O(n^2)\) ,空间复杂度为 \(O(1)\) ,在大数据量下非常耗时。
此方法通过哈希查找将时间复杂度从 \(O(n^2)\) 降至 \(O(n)\) ,大幅提升运行效率。
由于需要维护一个额外的哈希表,因此空间复杂度为 \(O(n)\) 。尽管如此,该方法的整体时空效率更为均衡,因此它是本题的最优解法。
diff --git a/chapter_sorting/bubble_sort/index.html b/chapter_sorting/bubble_sort/index.html index ddd7cc6f9..9e716cd02 100644 --- a/chapter_sorting/bubble_sort/index.html +++ b/chapter_sorting/bubble_sort/index.html @@ -3787,8 +3787,8 @@我们发现,如果某轮“冒泡”中没有执行任何交换操作,说明数组已经完成排序,可直接返回结果。因此,可以增加一个标志位 flag 来监测这种情况,一旦出现就立即返回。
桶排序适用于处理体量很大的数据。例如,输入数据包含 100 万个元素,由于空间限制,系统内存无法一次性加载所有数据。此时,可以将数据分成 1000 个桶,然后分别对每个桶进行排序,最后将结果合并。
diff --git a/chapter_sorting/counting_sort/index.html b/chapter_sorting/counting_sort/index.html index bb7589724..56555b992 100644 --- a/chapter_sorting/counting_sort/index.html +++ b/chapter_sorting/counting_sort/index.html @@ -3846,8 +3846,8 @@计数排序与桶排序的联系
@@ -4286,8 +4286,8 @@快速排序的整体流程如图 11-9 所示。
@@ -4118,8 +4118,8 @@在某些输入下,快速排序可能占用空间较多。以完全有序的输入数组为例,设递归中的子数组长度为 \(m\) ,每轮哨兵划分操作都将产生长度为 \(0\) 的左子数组和长度为 \(m - 1\) 的右子数组,这意味着每一层递归调用减少的问题规模非常小(只减少一个元素),递归树的高度会达到 \(n - 1\) ,此时需要占用 \(O(n)\) 大小的栈帧空间。
@@ -4794,8 +4794,8 @@为什么从最低位开始排序?
diff --git a/chapter_sorting/selection_sort/index.html b/chapter_sorting/selection_sort/index.html index dd0449ebc..178e2b75b 100644 --- a/chapter_sorting/selection_sort/index.html +++ b/chapter_sorting/selection_sort/index.html @@ -3761,8 +3761,8 @@双向队列的实现与队列类似,可以选择链表或数组作为底层数据结构。
diff --git a/chapter_stack_and_queue/queue/index.html b/chapter_stack_and_queue/queue/index.html index f531ac0c2..a02017d72 100644 --- a/chapter_stack_and_queue/queue/index.html +++ b/chapter_stack_and_queue/queue/index.html @@ -3889,8 +3889,8 @@为了实现队列,我们需要一种数据结构,可以在一端添加元素,并在另一端删除元素,链表和数组都符合要求。
@@ -4762,8 +4762,8 @@在数组中删除首元素的时间复杂度为 \(O(n)\) ,这会导致出队操作效率较低。然而,我们可以采用以下巧妙方法来避免这个问题。
@@ -5656,8 +5656,8 @@以上实现的队列仍然具有局限性:其长度不可变。然而,这个问题不难解决,我们可以将数组替换为动态数组,从而引入扩容机制。有兴趣的读者可以尝试自行实现。
两种实现的对比结论与栈一致,在此不再赘述。
diff --git a/chapter_stack_and_queue/stack/index.html b/chapter_stack_and_queue/stack/index.html index 4a044b652..eb382d8b6 100644 --- a/chapter_stack_and_queue/stack/index.html +++ b/chapter_stack_and_queue/stack/index.html @@ -3900,8 +3900,8 @@为了深入了解栈的运行机制,我们来尝试自己实现一个栈类。
@@ -4649,8 +4649,8 @@使用数组实现栈时,我们可以将数组的尾部作为栈顶。如图 5-3 所示,入栈与出栈操作分别对应在数组尾部添加元素与删除元素,时间复杂度都为 \(O(1)\) 。
@@ -5241,8 +5241,8 @@支持操作
diff --git a/chapter_tree/array_representation_of_tree/index.html b/chapter_tree/array_representation_of_tree/index.html index 1c9d48dad..9482ad578 100644 --- a/chapter_tree/array_representation_of_tree/index.html +++ b/chapter_tree/array_representation_of_tree/index.html @@ -4636,8 +4636,8 @@二叉树的数组表示主要有以下优点。
diff --git a/chapter_tree/binary_search_tree/index.html b/chapter_tree/binary_search_tree/index.html index e11675817..4d464a15c 100644 --- a/chapter_tree/binary_search_tree/index.html +++ b/chapter_tree/binary_search_tree/index.html @@ -3908,8 +3908,8 @@给定一个待插入元素 num ,为了保持二叉搜索树“左子树 < 根节点 < 右子树”的性质,插入操作流程如图 7-18 所示。
与查找节点相同,插入节点使用 \(O(\log n)\) 时间。
如图 7-22 所示,二叉树的中序遍历遵循“左 \(\rightarrow\) 根 \(\rightarrow\) 右”的遍历顺序,而二叉搜索树满足“左子节点 \(<\) 根节点 \(<\) 右子节点”的大小关系。
diff --git a/chapter_tree/binary_tree/index.html b/chapter_tree/binary_tree/index.html index 023e5b1a1..514142376 100644 --- a/chapter_tree/binary_tree/index.html +++ b/chapter_tree/binary_tree/index.html @@ -4043,8 +4043,8 @@与链表类似,在二叉树中插入与删除节点可以通过修改指针来实现。图 7-3 给出了一个示例。
@@ -4168,8 +4168,8 @@Note
diff --git a/chapter_tree/binary_tree_traversal/index.html b/chapter_tree/binary_tree_traversal/index.html index 6e174f733..2a4278db4 100644 --- a/chapter_tree/binary_tree_traversal/index.html +++ b/chapter_tree/binary_tree_traversal/index.html @@ -3905,8 +3905,8 @@Tip
diff --git a/en/404.html b/en/404.html index bf4a9c06e..e550e549d 100644 --- a/en/404.html +++ b/en/404.html @@ -1065,6 +1065,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +Array elements are tightly packed in memory, with no space available to accommodate additional data between them. Illustrated in Figure below, inserting an element in the middle of an array requires shifting all subsequent elements back by one position to create room for the new element.
@@ -1777,8 +1931,8 @@Similarly, as depicted in the Figure 4-4 , to delete an element at index \(i\), all elements following index \(i\) must be moved forward by one position.
@@ -1912,8 +2066,8 @@In summary, the insertion and deletion operations in arrays present the following disadvantages:
Locating a specific element within an array involves iterating through the array, checking each element to determine if it matches the desired value.
@@ -2259,8 +2413,8 @@In complex system environments, ensuring the availability of memory space after an array for safe capacity extension becomes challenging. Consequently, in most programming languages, the length of an array is immutable.
@@ -2446,8 +2600,8 @@Arrays are stored in contiguous memory spaces and consist of elements of the same type. This approach provides substantial prior information that systems can leverage to optimize the efficiency of data structure operations.
diff --git a/en/chapter_array_and_linkedlist/index.html b/en/chapter_array_and_linkedlist/index.html index fb8750d71..32f2a6069 100644 --- a/en/chapter_array_and_linkedlist/index.html +++ b/en/chapter_array_and_linkedlist/index.html @@ -1089,6 +1089,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +As shown below, deleting a node in a linked list is also very convenient, requiring only the change of one node's reference (pointer).
@@ -1997,8 +2151,8 @@Accessing nodes in a linked list is less efficient. As mentioned earlier, any element in an array can be accessed in \(O(1)\) time. However, in a linked list, the program needs to start from the head node and traverse each node sequentially until it finds the target node. That is, accessing the \(i\)-th node of a linked list requires \(i - 1\) iterations, with a time complexity of \(O(n)\).
@@ -2154,8 +2308,8 @@Traverse the linked list to find a node with a value equal to target, and output the index of that node in the linked list. This process also falls under linear search. The code is as follows:
The following table summarizes the characteristics of arrays and linked lists and compares their operational efficiencies. Since they employ two opposite storage strategies, their properties and operational efficiencies also show contrasting features.
diff --git a/en/chapter_array_and_linkedlist/list/index.html b/en/chapter_array_and_linkedlist/list/index.html index fa90e0bf3..3687bcee3 100644 --- a/en/chapter_array_and_linkedlist/list/index.html +++ b/en/chapter_array_and_linkedlist/list/index.html @@ -1207,6 +1207,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +The flowchart below represents this sum function.
@@ -1717,8 +1871,8 @@The while loop is more flexible than the for loop. In a while loop, we can freely design the initialization and update steps of the condition variable.
For example, in the following code, the condition variable \(i\) is updated twice in each round, which would be inconvenient to implement with a for loop:
Overall, for loops are more concise, while while loops are more flexible. Both can implement iterative structures. Which one to use should be determined based on the specific requirements of the problem.
The flowchart below represents this nested loop.
@@ -2299,8 +2453,8 @@The Figure 2-3 shows the recursive process of this function.
@@ -2472,8 +2626,8 @@The execution process of tail recursion is shown in the following figure. Comparing regular recursion and tail recursion, the point of the summation operation is different.
Observing the above code, we see that it recursively calls two functions within itself, meaning that one call generates two branching calls. As illustrated below, this continuous recursive calling eventually creates a "recursion tree" with a depth of \(n\).
@@ -2980,8 +3134,8 @@Observing the above code, when recursion is transformed into iteration, the code becomes more complex. Although iteration and recursion can often be transformed into each other, it's not always advisable to do so for two reasons:
Linear order is common in arrays, linked lists, stacks, queues, etc., where the number of elements is proportional to \(n\):
@@ -2576,8 +2730,8 @@ O(1) < O(\log n) < O(n) < O(n^2) < O(2^n) \newlineAs shown below, this function's recursive depth is \(n\), meaning there are \(n\) instances of unreturned linear_recur() function, using \(O(n)\) size of stack frame space:
Figure 2-17 Recursive Function Generating Linear Order Space Complexity
@@ -2903,8 +3057,8 @@ O(1) < O(\log n) < O(n) < O(n^2) < O(2^n) \newlineAs shown below, the recursive depth of this function is \(n\), and in each recursive call, an array is initialized with lengths \(n\), \(n-1\), \(\dots\), \(2\), \(1\), averaging \(n/2\), thus overall occupying \(O(n^2)\) space:
Figure 2-18 Recursive Function Generating Quadratic Order Space Complexity
@@ -3201,8 +3355,8 @@ O(1) < O(\log n) < O(n) < O(n^2) < O(2^n) \newlineFigure 2-19 Full Binary Tree Generating Exponential Order Space Complexity
diff --git a/en/chapter_computational_complexity/summary/index.html b/en/chapter_computational_complexity/summary/index.html index 276f2873e..a16da58a8 100644 --- a/en/chapter_computational_complexity/summary/index.html +++ b/en/chapter_computational_complexity/summary/index.html @@ -1147,6 +1147,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +Linear order indicates the number of operations grows linearly with the input data size \(n\). Linear order commonly appears in single-loop structures:
@@ -2607,8 +2761,8 @@ O(1) < O(\log n) < O(n) < O(n \log n) < O(n^2) < O(2^n) < O(n!Operations like array traversal and linked list traversal have a time complexity of \(O(n)\), where \(n\) is the length of the array or list:
It's important to note that the input data size \(n\) should be determined based on the type of input data. For example, in the first example, \(n\) represents the input data size, while in the second example, the length of the array \(n\) is the data size.
The following image compares constant order, linear order, and quadratic order time complexities.
@@ -3200,8 +3354,8 @@ O(1) < O(\log n) < O(n) < O(n \log n) < O(n^2) < O(2^n) < O(n!Biological "cell division" is a classic example of exponential order growth: starting with one cell, it becomes two after one division, four after two divisions, and so on, resulting in \(2^n\) cells after \(n\) divisions.
@@ -3410,8 +3564,8 @@ O(1) < O(\log n) < O(n) < O(n \log n) < O(n^2) < O(2^n) < O(n!Figure 2-11 Exponential Order Time Complexity
@@ -3528,8 +3682,8 @@ O(1) < O(\log n) < O(n) < O(n \log n) < O(n^2) < O(2^n) < O(n!Exponential order growth is extremely rapid and is commonly seen in exhaustive search methods (brute force, backtracking, etc.). For large-scale problems, exponential order is unacceptable, often requiring dynamic programming or greedy algorithms as solutions.
Figure 2-12 Logarithmic Order Time Complexity
@@ -3804,8 +3958,8 @@ O(1) < O(\log n) < O(n) < O(n \log n) < O(n^2) < O(2^n) < O(n!Logarithmic order is typical in algorithms based on the divide-and-conquer strategy, embodying the "split into many" and "simplify complex problems" approach. It's slow-growing and is the most ideal time complexity after constant order.
The image below demonstrates how linear-logarithmic order is generated. Each level of a binary tree has \(n\) operations, and the tree has \(\log_2 n + 1\) levels, resulting in a time complexity of \(O(n \log n)\).
@@ -4161,8 +4315,8 @@ n! = n \times (n - 1) \times (n - 2) \times \dots \times 2 \times 1Figure 2-14 Factorial Order Time Complexity
@@ -4504,8 +4658,8 @@ n! = n \times (n - 1) \times (n - 2) \times \dots \times 2 \times 1It's important to note that the best-case time complexity is rarely used in practice, as it is usually only achievable under very low probabilities and might be misleading. The worst-case time complexity is more practical as it provides a safety value for efficiency, allowing us to confidently use the algorithm.
From the above example, it's clear that both the worst-case and best-case time complexities only occur under "special data distributions," which may have a small probability of occurrence and may not accurately reflect the algorithm's run efficiency. In contrast, the average time complexity can reflect the algorithm's efficiency under random input data, denoted by the \(\Theta\) notation.
diff --git a/en/chapter_data_structure/basic_data_types/index.html b/en/chapter_data_structure/basic_data_types/index.html index 613dda2ff..d96fcc907 100644 --- a/en/chapter_data_structure/basic_data_types/index.html +++ b/en/chapter_data_structure/basic_data_types/index.html @@ -1099,6 +1099,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +In a regular queue, we can only delete elements from the head or add elements to the tail. As shown in the figure below, a "double-ended queue (deque)" offers more flexibility, allowing the addition or removal of elements at both the head and the tail.
- -The common operations in a double-ended queue are listed below, and the specific method names depend on the programming language used.
- Table
In a regular queue, we can only delete elements from the head or add elements to the tail. As shown in the Figure 5-7 , a "double-ended queue (deque)" offers more flexibility, allowing the addition or removal of elements at both the head and the tail.
+ +Figure 5-7 Operations in Double-Ended Queue
+The common operations in a double-ended queue are listed below, and the specific method names depend on the programming language used.
+Table 5-3 Efficiency of Double-Ended Queue Operations
+ +Similarly, we can directly use the double-ended queue classes implemented in programming languages:
The implementation of a double-ended queue is similar to that of a regular queue, with the choice of either linked lists or arrays as the underlying data structure.
-Recall from the previous section that we used a regular singly linked list to implement a queue, as it conveniently allows for deleting the head node (corresponding to dequeue operation) and adding new nodes after the tail node (corresponding to enqueue operation).
For a double-ended queue, both the head and the tail can perform enqueue and dequeue operations. In other words, a double-ended queue needs to implement another symmetric direction of operations. For this, we use a "doubly linked list" as the underlying data structure of the double-ended queue.
-As shown in the figure below, we treat the head and tail nodes of the doubly linked list as the front and rear of the double-ended queue, respectively, and implement the functionality to add and remove nodes at both ends.
+As shown in the Figure 5-8 , we treat the head and tail nodes of the doubly linked list as the front and rear of the double-ended queue, respectively, and implement the functionality to add and remove nodes at both ends.
+Figure 5-8 Implementing Double-Ended Queue with Doubly Linked List for Enqueue and Dequeue Operations
+The implementation code is as follows:
- -As shown in the figure below, similar to implementing a queue with an array, we can also use a circular array to implement a double-ended queue.
-The implementation only needs to add methods for "front enqueue" and "rear dequeue":
-[file]{array_deque}-[func]{}
+linkedlist_deque.pyclass ListNode:
+ """双向链表节点"""
+
+ def __init__(self, val: int):
+ """构造方法"""
+ self.val: int = val
+ self.next: ListNode | None = None # 后继节点引用
+ self.prev: ListNode | None = None # 前驱节点引用
+
+class LinkedListDeque:
+ """基于双向链表实现的双向队列"""
+
+ def __init__(self):
+ """构造方法"""
+ self._front: ListNode | None = None # 头节点 front
+ self._rear: ListNode | None = None # 尾节点 rear
+ self._size: int = 0 # 双向队列的长度
+
+ def size(self) -> int:
+ """获取双向队列的长度"""
+ return self._size
+
+ def is_empty(self) -> bool:
+ """判断双向队列是否为空"""
+ return self.size() == 0
+
+ def push(self, num: int, is_front: bool):
+ """入队操作"""
+ node = ListNode(num)
+ # 若链表为空,则令 front 和 rear 都指向 node
+ if self.is_empty():
+ self._front = self._rear = node
+ # 队首入队操作
+ elif is_front:
+ # 将 node 添加至链表头部
+ self._front.prev = node
+ node.next = self._front
+ self._front = node # 更新头节点
+ # 队尾入队操作
+ else:
+ # 将 node 添加至链表尾部
+ self._rear.next = node
+ node.prev = self._rear
+ self._rear = node # 更新尾节点
+ self._size += 1 # 更新队列长度
+
+ def push_first(self, num: int):
+ """队首入队"""
+ self.push(num, True)
+
+ def push_last(self, num: int):
+ """队尾入队"""
+ self.push(num, False)
+
+ def pop(self, is_front: bool) -> int:
+ """出队操作"""
+ if self.is_empty():
+ raise IndexError("双向队列为空")
+ # 队首出队操作
+ if is_front:
+ val: int = self._front.val # 暂存头节点值
+ # 删除头节点
+ fnext: ListNode | None = self._front.next
+ if fnext != None:
+ fnext.prev = None
+ self._front.next = None
+ self._front = fnext # 更新头节点
+ # 队尾出队操作
+ else:
+ val: int = self._rear.val # 暂存尾节点值
+ # 删除尾节点
+ rprev: ListNode | None = self._rear.prev
+ if rprev != None:
+ rprev.next = None
+ self._rear.prev = None
+ self._rear = rprev # 更新尾节点
+ self._size -= 1 # 更新队列长度
+ return val
+
+ def pop_first(self) -> int:
+ """队首出队"""
+ return self.pop(True)
+
+ def pop_last(self) -> int:
+ """队尾出队"""
+ return self.pop(False)
+
+ def peek_first(self) -> int:
+ """访问队首元素"""
+ if self.is_empty():
+ raise IndexError("双向队列为空")
+ return self._front.val
+
+ def peek_last(self) -> int:
+ """访问队尾元素"""
+ if self.is_empty():
+ raise IndexError("双向队列为空")
+ return self._rear.val
+
+ def to_array(self) -> list[int]:
+ """返回数组用于打印"""
+ node = self._front
+ res = [0] * self.size()
+ for i in range(self.size()):
+ res[i] = node.val
+ node = node.next
+ return res
-Applications of Double-Ended Queue¶
+/* 双向链表节点 */
+struct DoublyListNode {
+ int val; // 节点值
+ DoublyListNode *next; // 后继节点指针
+ DoublyListNode *prev; // 前驱节点指针
+ DoublyListNode(int val) : val(val), prev(nullptr), next(nullptr) {
+ }
+};
+
+/* 基于双向链表实现的双向队列 */
+class LinkedListDeque {
+ private:
+ DoublyListNode *front, *rear; // 头节点 front ,尾节点 rear
+ int queSize = 0; // 双向队列的长度
+
+ public:
+ /* 构造方法 */
+ LinkedListDeque() : front(nullptr), rear(nullptr) {
+ }
+
+ /* 析构方法 */
+ ~LinkedListDeque() {
+ // 遍历链表删除节点,释放内存
+ DoublyListNode *pre, *cur = front;
+ while (cur != nullptr) {
+ pre = cur;
+ cur = cur->next;
+ delete pre;
+ }
+ }
+
+ /* 获取双向队列的长度 */
+ int size() {
+ return queSize;
+ }
+
+ /* 判断双向队列是否为空 */
+ bool isEmpty() {
+ return size() == 0;
+ }
+
+ /* 入队操作 */
+ void push(int num, bool isFront) {
+ DoublyListNode *node = new DoublyListNode(num);
+ // 若链表为空,则令 front 和 rear 都指向 node
+ if (isEmpty())
+ front = rear = node;
+ // 队首入队操作
+ else if (isFront) {
+ // 将 node 添加至链表头部
+ front->prev = node;
+ node->next = front;
+ front = node; // 更新头节点
+ // 队尾入队操作
+ } else {
+ // 将 node 添加至链表尾部
+ rear->next = node;
+ node->prev = rear;
+ rear = node; // 更新尾节点
+ }
+ queSize++; // 更新队列长度
+ }
+
+ /* 队首入队 */
+ void pushFirst(int num) {
+ push(num, true);
+ }
+
+ /* 队尾入队 */
+ void pushLast(int num) {
+ push(num, false);
+ }
+
+ /* 出队操作 */
+ int pop(bool isFront) {
+ if (isEmpty())
+ throw out_of_range("队列为空");
+ int val;
+ // 队首出队操作
+ if (isFront) {
+ val = front->val; // 暂存头节点值
+ // 删除头节点
+ DoublyListNode *fNext = front->next;
+ if (fNext != nullptr) {
+ fNext->prev = nullptr;
+ front->next = nullptr;
+ delete front;
+ }
+ front = fNext; // 更新头节点
+ // 队尾出队操作
+ } else {
+ val = rear->val; // 暂存尾节点值
+ // 删除尾节点
+ DoublyListNode *rPrev = rear->prev;
+ if (rPrev != nullptr) {
+ rPrev->next = nullptr;
+ rear->prev = nullptr;
+ delete rear;
+ }
+ rear = rPrev; // 更新尾节点
+ }
+ queSize--; // 更新队列长度
+ return val;
+ }
+
+ /* 队首出队 */
+ int popFirst() {
+ return pop(true);
+ }
+
+ /* 队尾出队 */
+ int popLast() {
+ return pop(false);
+ }
+
+ /* 访问队首元素 */
+ int peekFirst() {
+ if (isEmpty())
+ throw out_of_range("双向队列为空");
+ return front->val;
+ }
+
+ /* 访问队尾元素 */
+ int peekLast() {
+ if (isEmpty())
+ throw out_of_range("双向队列为空");
+ return rear->val;
+ }
+
+ /* 返回数组用于打印 */
+ vector<int> toVector() {
+ DoublyListNode *node = front;
+ vector<int> res(size());
+ for (int i = 0; i < res.size(); i++) {
+ res[i] = node->val;
+ node = node->next;
+ }
+ return res;
+ }
+};
+/* 双向链表节点 */
+class ListNode {
+ int val; // 节点值
+ ListNode next; // 后继节点引用
+ ListNode prev; // 前驱节点引用
+
+ ListNode(int val) {
+ this.val = val;
+ prev = next = null;
+ }
+}
+
+/* 基于双向链表实现的双向队列 */
+class LinkedListDeque {
+ private ListNode front, rear; // 头节点 front ,尾节点 rear
+ private int queSize = 0; // 双向队列的长度
+
+ public LinkedListDeque() {
+ front = rear = null;
+ }
+
+ /* 获取双向队列的长度 */
+ public int size() {
+ return queSize;
+ }
+
+ /* 判断双向队列是否为空 */
+ public boolean isEmpty() {
+ return size() == 0;
+ }
+
+ /* 入队操作 */
+ private void push(int num, boolean isFront) {
+ ListNode node = new ListNode(num);
+ // 若链表为空,则令 front 和 rear 都指向 node
+ if (isEmpty())
+ front = rear = node;
+ // 队首入队操作
+ else if (isFront) {
+ // 将 node 添加至链表头部
+ front.prev = node;
+ node.next = front;
+ front = node; // 更新头节点
+ // 队尾入队操作
+ } else {
+ // 将 node 添加至链表尾部
+ rear.next = node;
+ node.prev = rear;
+ rear = node; // 更新尾节点
+ }
+ queSize++; // 更新队列长度
+ }
+
+ /* 队首入队 */
+ public void pushFirst(int num) {
+ push(num, true);
+ }
+
+ /* 队尾入队 */
+ public void pushLast(int num) {
+ push(num, false);
+ }
+
+ /* 出队操作 */
+ private int pop(boolean isFront) {
+ if (isEmpty())
+ throw new IndexOutOfBoundsException();
+ int val;
+ // 队首出队操作
+ if (isFront) {
+ val = front.val; // 暂存头节点值
+ // 删除头节点
+ ListNode fNext = front.next;
+ if (fNext != null) {
+ fNext.prev = null;
+ front.next = null;
+ }
+ front = fNext; // 更新头节点
+ // 队尾出队操作
+ } else {
+ val = rear.val; // 暂存尾节点值
+ // 删除尾节点
+ ListNode rPrev = rear.prev;
+ if (rPrev != null) {
+ rPrev.next = null;
+ rear.prev = null;
+ }
+ rear = rPrev; // 更新尾节点
+ }
+ queSize--; // 更新队列长度
+ return val;
+ }
+
+ /* 队首出队 */
+ public int popFirst() {
+ return pop(true);
+ }
+
+ /* 队尾出队 */
+ public int popLast() {
+ return pop(false);
+ }
+
+ /* 访问队首元素 */
+ public int peekFirst() {
+ if (isEmpty())
+ throw new IndexOutOfBoundsException();
+ return front.val;
+ }
+
+ /* 访问队尾元素 */
+ public int peekLast() {
+ if (isEmpty())
+ throw new IndexOutOfBoundsException();
+ return rear.val;
+ }
+
+ /* 返回数组用于打印 */
+ public int[] toArray() {
+ ListNode node = front;
+ int[] res = new int[size()];
+ for (int i = 0; i < res.length; i++) {
+ res[i] = node.val;
+ node = node.next;
+ }
+ return res;
+ }
+}
+/* 双向链表节点 */
+class ListNode(int val) {
+ public int val = val; // 节点值
+ public ListNode? next = null; // 后继节点引用
+ public ListNode? prev = null; // 前驱节点引用
+}
+
+/* 基于双向链表实现的双向队列 */
+class LinkedListDeque {
+ ListNode? front, rear; // 头节点 front, 尾节点 rear
+ int queSize = 0; // 双向队列的长度
+
+ public LinkedListDeque() {
+ front = null;
+ rear = null;
+ }
+
+ /* 获取双向队列的长度 */
+ public int Size() {
+ return queSize;
+ }
+
+ /* 判断双向队列是否为空 */
+ public bool IsEmpty() {
+ return Size() == 0;
+ }
+
+ /* 入队操作 */
+ void Push(int num, bool isFront) {
+ ListNode node = new(num);
+ // 若链表为空,则令 front 和 rear 都指向 node
+ if (IsEmpty()) {
+ front = node;
+ rear = node;
+ }
+ // 队首入队操作
+ else if (isFront) {
+ // 将 node 添加至链表头部
+ front!.prev = node;
+ node.next = front;
+ front = node; // 更新头节点
+ }
+ // 队尾入队操作
+ else {
+ // 将 node 添加至链表尾部
+ rear!.next = node;
+ node.prev = rear;
+ rear = node; // 更新尾节点
+ }
+
+ queSize++; // 更新队列长度
+ }
+
+ /* 队首入队 */
+ public void PushFirst(int num) {
+ Push(num, true);
+ }
+
+ /* 队尾入队 */
+ public void PushLast(int num) {
+ Push(num, false);
+ }
+
+ /* 出队操作 */
+ int? Pop(bool isFront) {
+ if (IsEmpty())
+ throw new Exception();
+ int? val;
+ // 队首出队操作
+ if (isFront) {
+ val = front?.val; // 暂存头节点值
+ // 删除头节点
+ ListNode? fNext = front?.next;
+ if (fNext != null) {
+ fNext.prev = null;
+ front!.next = null;
+ }
+ front = fNext; // 更新头节点
+ }
+ // 队尾出队操作
+ else {
+ val = rear?.val; // 暂存尾节点值
+ // 删除尾节点
+ ListNode? rPrev = rear?.prev;
+ if (rPrev != null) {
+ rPrev.next = null;
+ rear!.prev = null;
+ }
+ rear = rPrev; // 更新尾节点
+ }
+
+ queSize--; // 更新队列长度
+ return val;
+ }
+
+ /* 队首出队 */
+ public int? PopFirst() {
+ return Pop(true);
+ }
+
+ /* 队尾出队 */
+ public int? PopLast() {
+ return Pop(false);
+ }
+
+ /* 访问队首元素 */
+ public int? PeekFirst() {
+ if (IsEmpty())
+ throw new Exception();
+ return front?.val;
+ }
+
+ /* 访问队尾元素 */
+ public int? PeekLast() {
+ if (IsEmpty())
+ throw new Exception();
+ return rear?.val;
+ }
+
+ /* 返回数组用于打印 */
+ public int?[] ToArray() {
+ ListNode? node = front;
+ int?[] res = new int?[Size()];
+ for (int i = 0; i < res.Length; i++) {
+ res[i] = node?.val;
+ node = node?.next;
+ }
+
+ return res;
+ }
+}
+/* 基于双向链表实现的双向队列 */
+type linkedListDeque struct {
+ // 使用内置包 list
+ data *list.List
+}
+
+/* 初始化双端队列 */
+func newLinkedListDeque() *linkedListDeque {
+ return &linkedListDeque{
+ data: list.New(),
+ }
+}
+
+/* 队首元素入队 */
+func (s *linkedListDeque) pushFirst(value any) {
+ s.data.PushFront(value)
+}
+
+/* 队尾元素入队 */
+func (s *linkedListDeque) pushLast(value any) {
+ s.data.PushBack(value)
+}
+
+/* 队首元素出队 */
+func (s *linkedListDeque) popFirst() any {
+ if s.isEmpty() {
+ return nil
+ }
+ e := s.data.Front()
+ s.data.Remove(e)
+ return e.Value
+}
+
+/* 队尾元素出队 */
+func (s *linkedListDeque) popLast() any {
+ if s.isEmpty() {
+ return nil
+ }
+ e := s.data.Back()
+ s.data.Remove(e)
+ return e.Value
+}
+
+/* 访问队首元素 */
+func (s *linkedListDeque) peekFirst() any {
+ if s.isEmpty() {
+ return nil
+ }
+ e := s.data.Front()
+ return e.Value
+}
+
+/* 访问队尾元素 */
+func (s *linkedListDeque) peekLast() any {
+ if s.isEmpty() {
+ return nil
+ }
+ e := s.data.Back()
+ return e.Value
+}
+
+/* 获取队列的长度 */
+func (s *linkedListDeque) size() int {
+ return s.data.Len()
+}
+
+/* 判断队列是否为空 */
+func (s *linkedListDeque) isEmpty() bool {
+ return s.data.Len() == 0
+}
+
+/* 获取 List 用于打印 */
+func (s *linkedListDeque) toList() *list.List {
+ return s.data
+}
+/* 双向链表节点 */
+class ListNode {
+ var val: Int // 节点值
+ var next: ListNode? // 后继节点引用
+ weak var prev: ListNode? // 前驱节点引用
+
+ init(val: Int) {
+ self.val = val
+ }
+}
+
+/* 基于双向链表实现的双向队列 */
+class LinkedListDeque {
+ private var front: ListNode? // 头节点 front
+ private var rear: ListNode? // 尾节点 rear
+ private var queSize: Int // 双向队列的长度
+
+ init() {
+ queSize = 0
+ }
+
+ /* 获取双向队列的长度 */
+ func size() -> Int {
+ queSize
+ }
+
+ /* 判断双向队列是否为空 */
+ func isEmpty() -> Bool {
+ size() == 0
+ }
+
+ /* 入队操作 */
+ private func push(num: Int, isFront: Bool) {
+ let node = ListNode(val: num)
+ // 若链表为空,则令 front 和 rear 都指向 node
+ if isEmpty() {
+ front = node
+ rear = node
+ }
+ // 队首入队操作
+ else if isFront {
+ // 将 node 添加至链表头部
+ front?.prev = node
+ node.next = front
+ front = node // 更新头节点
+ }
+ // 队尾入队操作
+ else {
+ // 将 node 添加至链表尾部
+ rear?.next = node
+ node.prev = rear
+ rear = node // 更新尾节点
+ }
+ queSize += 1 // 更新队列长度
+ }
+
+ /* 队首入队 */
+ func pushFirst(num: Int) {
+ push(num: num, isFront: true)
+ }
+
+ /* 队尾入队 */
+ func pushLast(num: Int) {
+ push(num: num, isFront: false)
+ }
+
+ /* 出队操作 */
+ private func pop(isFront: Bool) -> Int {
+ if isEmpty() {
+ fatalError("双向队列为空")
+ }
+ let val: Int
+ // 队首出队操作
+ if isFront {
+ val = front!.val // 暂存头节点值
+ // 删除头节点
+ let fNext = front?.next
+ if fNext != nil {
+ fNext?.prev = nil
+ front?.next = nil
+ }
+ front = fNext // 更新头节点
+ }
+ // 队尾出队操作
+ else {
+ val = rear!.val // 暂存尾节点值
+ // 删除尾节点
+ let rPrev = rear?.prev
+ if rPrev != nil {
+ rPrev?.next = nil
+ rear?.prev = nil
+ }
+ rear = rPrev // 更新尾节点
+ }
+ queSize -= 1 // 更新队列长度
+ return val
+ }
+
+ /* 队首出队 */
+ func popFirst() -> Int {
+ pop(isFront: true)
+ }
+
+ /* 队尾出队 */
+ func popLast() -> Int {
+ pop(isFront: false)
+ }
+
+ /* 访问队首元素 */
+ func peekFirst() -> Int? {
+ isEmpty() ? nil : front?.val
+ }
+
+ /* 访问队尾元素 */
+ func peekLast() -> Int? {
+ isEmpty() ? nil : rear?.val
+ }
+
+ /* 返回数组用于打印 */
+ func toArray() -> [Int] {
+ var node = front
+ var res = Array(repeating: 0, count: size())
+ for i in res.indices {
+ res[i] = node!.val
+ node = node?.next
+ }
+ return res
+ }
+}
+/* 双向链表节点 */
+class ListNode {
+ prev; // 前驱节点引用 (指针)
+ next; // 后继节点引用 (指针)
+ val; // 节点值
+
+ constructor(val) {
+ this.val = val;
+ this.next = null;
+ this.prev = null;
+ }
+}
+
+/* 基于双向链表实现的双向队列 */
+class LinkedListDeque {
+ #front; // 头节点 front
+ #rear; // 尾节点 rear
+ #queSize; // 双向队列的长度
+
+ constructor() {
+ this.#front = null;
+ this.#rear = null;
+ this.#queSize = 0;
+ }
+
+ /* 队尾入队操作 */
+ pushLast(val) {
+ const node = new ListNode(val);
+ // 若链表为空,则令 front 和 rear 都指向 node
+ if (this.#queSize === 0) {
+ this.#front = node;
+ this.#rear = node;
+ } else {
+ // 将 node 添加至链表尾部
+ this.#rear.next = node;
+ node.prev = this.#rear;
+ this.#rear = node; // 更新尾节点
+ }
+ this.#queSize++;
+ }
+
+ /* 队首入队操作 */
+ pushFirst(val) {
+ const node = new ListNode(val);
+ // 若链表为空,则令 front 和 rear 都指向 node
+ if (this.#queSize === 0) {
+ this.#front = node;
+ this.#rear = node;
+ } else {
+ // 将 node 添加至链表头部
+ this.#front.prev = node;
+ node.next = this.#front;
+ this.#front = node; // 更新头节点
+ }
+ this.#queSize++;
+ }
+
+ /* 队尾出队操作 */
+ popLast() {
+ if (this.#queSize === 0) {
+ return null;
+ }
+ const value = this.#rear.val; // 存储尾节点值
+ // 删除尾节点
+ let temp = this.#rear.prev;
+ if (temp !== null) {
+ temp.next = null;
+ this.#rear.prev = null;
+ }
+ this.#rear = temp; // 更新尾节点
+ this.#queSize--;
+ return value;
+ }
+
+ /* 队首出队操作 */
+ popFirst() {
+ if (this.#queSize === 0) {
+ return null;
+ }
+ const value = this.#front.val; // 存储尾节点值
+ // 删除头节点
+ let temp = this.#front.next;
+ if (temp !== null) {
+ temp.prev = null;
+ this.#front.next = null;
+ }
+ this.#front = temp; // 更新头节点
+ this.#queSize--;
+ return value;
+ }
+
+ /* 访问队尾元素 */
+ peekLast() {
+ return this.#queSize === 0 ? null : this.#rear.val;
+ }
+
+ /* 访问队首元素 */
+ peekFirst() {
+ return this.#queSize === 0 ? null : this.#front.val;
+ }
+
+ /* 获取双向队列的长度 */
+ size() {
+ return this.#queSize;
+ }
+
+ /* 判断双向队列是否为空 */
+ isEmpty() {
+ return this.#queSize === 0;
+ }
+
+ /* 打印双向队列 */
+ print() {
+ const arr = [];
+ let temp = this.#front;
+ while (temp !== null) {
+ arr.push(temp.val);
+ temp = temp.next;
+ }
+ console.log('[' + arr.join(', ') + ']');
+ }
+}
+/* 双向链表节点 */
+class ListNode {
+ prev: ListNode; // 前驱节点引用 (指针)
+ next: ListNode; // 后继节点引用 (指针)
+ val: number; // 节点值
+
+ constructor(val: number) {
+ this.val = val;
+ this.next = null;
+ this.prev = null;
+ }
+}
+
+/* 基于双向链表实现的双向队列 */
+class LinkedListDeque {
+ private front: ListNode; // 头节点 front
+ private rear: ListNode; // 尾节点 rear
+ private queSize: number; // 双向队列的长度
+
+ constructor() {
+ this.front = null;
+ this.rear = null;
+ this.queSize = 0;
+ }
+
+ /* 队尾入队操作 */
+ pushLast(val: number): void {
+ const node: ListNode = new ListNode(val);
+ // 若链表为空,则令 front 和 rear 都指向 node
+ if (this.queSize === 0) {
+ this.front = node;
+ this.rear = node;
+ } else {
+ // 将 node 添加至链表尾部
+ this.rear.next = node;
+ node.prev = this.rear;
+ this.rear = node; // 更新尾节点
+ }
+ this.queSize++;
+ }
+
+ /* 队首入队操作 */
+ pushFirst(val: number): void {
+ const node: ListNode = new ListNode(val);
+ // 若链表为空,则令 front 和 rear 都指向 node
+ if (this.queSize === 0) {
+ this.front = node;
+ this.rear = node;
+ } else {
+ // 将 node 添加至链表头部
+ this.front.prev = node;
+ node.next = this.front;
+ this.front = node; // 更新头节点
+ }
+ this.queSize++;
+ }
+
+ /* 队尾出队操作 */
+ popLast(): number {
+ if (this.queSize === 0) {
+ return null;
+ }
+ const value: number = this.rear.val; // 存储尾节点值
+ // 删除尾节点
+ let temp: ListNode = this.rear.prev;
+ if (temp !== null) {
+ temp.next = null;
+ this.rear.prev = null;
+ }
+ this.rear = temp; // 更新尾节点
+ this.queSize--;
+ return value;
+ }
+
+ /* 队首出队操作 */
+ popFirst(): number {
+ if (this.queSize === 0) {
+ return null;
+ }
+ const value: number = this.front.val; // 存储尾节点值
+ // 删除头节点
+ let temp: ListNode = this.front.next;
+ if (temp !== null) {
+ temp.prev = null;
+ this.front.next = null;
+ }
+ this.front = temp; // 更新头节点
+ this.queSize--;
+ return value;
+ }
+
+ /* 访问队尾元素 */
+ peekLast(): number {
+ return this.queSize === 0 ? null : this.rear.val;
+ }
+
+ /* 访问队首元素 */
+ peekFirst(): number {
+ return this.queSize === 0 ? null : this.front.val;
+ }
+
+ /* 获取双向队列的长度 */
+ size(): number {
+ return this.queSize;
+ }
+
+ /* 判断双向队列是否为空 */
+ isEmpty(): boolean {
+ return this.queSize === 0;
+ }
+
+ /* 打印双向队列 */
+ print(): void {
+ const arr: number[] = [];
+ let temp: ListNode = this.front;
+ while (temp !== null) {
+ arr.push(temp.val);
+ temp = temp.next;
+ }
+ console.log('[' + arr.join(', ') + ']');
+ }
+}
+/* 双向链表节点 */
+class ListNode {
+ int val; // 节点值
+ ListNode? next; // 后继节点引用
+ ListNode? prev; // 前驱节点引用
+
+ ListNode(this.val, {this.next, this.prev});
+}
+
+/* 基于双向链表实现的双向对列 */
+class LinkedListDeque {
+ late ListNode? _front; // 头节点 _front
+ late ListNode? _rear; // 尾节点 _rear
+ int _queSize = 0; // 双向队列的长度
+
+ LinkedListDeque() {
+ this._front = null;
+ this._rear = null;
+ }
+
+ /* 获取双向队列长度 */
+ int size() {
+ return this._queSize;
+ }
+
+ /* 判断双向队列是否为空 */
+ bool isEmpty() {
+ return size() == 0;
+ }
+
+ /* 入队操作 */
+ void push(int _num, bool isFront) {
+ final ListNode node = ListNode(_num);
+ if (isEmpty()) {
+ // 若链表为空,则令 _front 和 _rear 都指向 node
+ _front = _rear = node;
+ } else if (isFront) {
+ // 队首入队操作
+ // 将 node 添加至链表头部
+ _front!.prev = node;
+ node.next = _front;
+ _front = node; // 更新头节点
+ } else {
+ // 队尾入队操作
+ // 将 node 添加至链表尾部
+ _rear!.next = node;
+ node.prev = _rear;
+ _rear = node; // 更新尾节点
+ }
+ _queSize++; // 更新队列长度
+ }
+
+ /* 队首入队 */
+ void pushFirst(int _num) {
+ push(_num, true);
+ }
+
+ /* 队尾入队 */
+ void pushLast(int _num) {
+ push(_num, false);
+ }
+
+ /* 出队操作 */
+ int? pop(bool isFront) {
+ // 若队列为空,直接返回 null
+ if (isEmpty()) {
+ return null;
+ }
+ final int val;
+ if (isFront) {
+ // 队首出队操作
+ val = _front!.val; // 暂存头节点值
+ // 删除头节点
+ ListNode? fNext = _front!.next;
+ if (fNext != null) {
+ fNext.prev = null;
+ _front!.next = null;
+ }
+ _front = fNext; // 更新头节点
+ } else {
+ // 队尾出队操作
+ val = _rear!.val; // 暂存尾节点值
+ // 删除尾节点
+ ListNode? rPrev = _rear!.prev;
+ if (rPrev != null) {
+ rPrev.next = null;
+ _rear!.prev = null;
+ }
+ _rear = rPrev; // 更新尾节点
+ }
+ _queSize--; // 更新队列长度
+ return val;
+ }
+
+ /* 队首出队 */
+ int? popFirst() {
+ return pop(true);
+ }
+
+ /* 队尾出队 */
+ int? popLast() {
+ return pop(false);
+ }
+
+ /* 访问队首元素 */
+ int? peekFirst() {
+ return _front?.val;
+ }
+
+ /* 访问队尾元素 */
+ int? peekLast() {
+ return _rear?.val;
+ }
+
+ /* 返回数组用于打印 */
+ List<int> toArray() {
+ ListNode? node = _front;
+ final List<int> res = [];
+ for (int i = 0; i < _queSize; i++) {
+ res.add(node!.val);
+ node = node.next;
+ }
+ return res;
+ }
+}
+/* 双向链表节点 */
+pub struct ListNode<T> {
+ pub val: T, // 节点值
+ pub next: Option<Rc<RefCell<ListNode<T>>>>, // 后继节点指针
+ pub prev: Option<Rc<RefCell<ListNode<T>>>>, // 前驱节点指针
+}
+
+impl<T> ListNode<T> {
+ pub fn new(val: T) -> Rc<RefCell<ListNode<T>>> {
+ Rc::new(RefCell::new(ListNode {
+ val,
+ next: None,
+ prev: None,
+ }))
+ }
+}
+
+/* 基于双向链表实现的双向队列 */
+#[allow(dead_code)]
+pub struct LinkedListDeque<T> {
+ front: Option<Rc<RefCell<ListNode<T>>>>, // 头节点 front
+ rear: Option<Rc<RefCell<ListNode<T>>>>, // 尾节点 rear
+ que_size: usize, // 双向队列的长度
+}
+
+impl<T: Copy> LinkedListDeque<T> {
+ pub fn new() -> Self {
+ Self {
+ front: None,
+ rear: None,
+ que_size: 0,
+ }
+ }
+
+ /* 获取双向队列的长度 */
+ pub fn size(&self) -> usize {
+ return self.que_size;
+ }
+
+ /* 判断双向队列是否为空 */
+ pub fn is_empty(&self) -> bool {
+ return self.size() == 0;
+ }
+
+ /* 入队操作 */
+ pub fn push(&mut self, num: T, is_front: bool) {
+ let node = ListNode::new(num);
+ // 队首入队操作
+ if is_front {
+ match self.front.take() {
+ // 若链表为空,则令 front 和 rear 都指向 node
+ None => {
+ self.rear = Some(node.clone());
+ self.front = Some(node);
+ }
+ // 将 node 添加至链表头部
+ Some(old_front) => {
+ old_front.borrow_mut().prev = Some(node.clone());
+ node.borrow_mut().next = Some(old_front);
+ self.front = Some(node); // 更新头节点
+ }
+ }
+ }
+ // 队尾入队操作
+ else {
+ match self.rear.take() {
+ // 若链表为空,则令 front 和 rear 都指向 node
+ None => {
+ self.front = Some(node.clone());
+ self.rear = Some(node);
+ }
+ // 将 node 添加至链表尾部
+ Some(old_rear) => {
+ old_rear.borrow_mut().next = Some(node.clone());
+ node.borrow_mut().prev = Some(old_rear);
+ self.rear = Some(node); // 更新尾节点
+ }
+ }
+ }
+ self.que_size += 1; // 更新队列长度
+ }
+
+ /* 队首入队 */
+ pub fn push_first(&mut self, num: T) {
+ self.push(num, true);
+ }
+
+ /* 队尾入队 */
+ pub fn push_last(&mut self, num: T) {
+ self.push(num, false);
+ }
+
+ /* 出队操作 */
+ pub fn pop(&mut self, is_front: bool) -> Option<T> {
+ // 若队列为空,直接返回 None
+ if self.is_empty() {
+ return None
+ };
+ // 队首出队操作
+ if is_front {
+ self.front.take().map(|old_front| {
+ match old_front.borrow_mut().next.take() {
+ Some(new_front) => {
+ new_front.borrow_mut().prev.take();
+ self.front = Some(new_front); // 更新头节点
+ }
+ None => {
+ self.rear.take();
+ }
+ }
+ self.que_size -= 1; // 更新队列长度
+ Rc::try_unwrap(old_front).ok().unwrap().into_inner().val
+ })
+
+ }
+ // 队尾出队操作
+ else {
+ self.rear.take().map(|old_rear| {
+ match old_rear.borrow_mut().prev.take() {
+ Some(new_rear) => {
+ new_rear.borrow_mut().next.take();
+ self.rear = Some(new_rear); // 更新尾节点
+ }
+ None => {
+ self.front.take();
+ }
+ }
+ self.que_size -= 1; // 更新队列长度
+ Rc::try_unwrap(old_rear).ok().unwrap().into_inner().val
+ })
+ }
+ }
+
+ /* 队首出队 */
+ pub fn pop_first(&mut self) -> Option<T> {
+ return self.pop(true);
+ }
+
+ /* 队尾出队 */
+ pub fn pop_last(&mut self) -> Option<T> {
+ return self.pop(false);
+ }
+
+ /* 访问队首元素 */
+ pub fn peek_first(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {
+ self.front.as_ref()
+ }
+
+ /* 访问队尾元素 */
+ pub fn peek_last(&self) -> Option<&Rc<RefCell<ListNode<T>>>> {
+ self.rear.as_ref()
+ }
+
+ /* 返回数组用于打印 */
+ pub fn to_array(&self, head: Option<&Rc<RefCell<ListNode<T>>>>) -> Vec<T> {
+ if let Some(node) = head {
+ let mut nums = self.to_array(node.borrow().next.as_ref());
+ nums.insert(0, node.borrow().val);
+ return nums;
+ }
+ return Vec::new();
+ }
+}
+/* 双向链表节点 */
+typedef struct DoublyListNode {
+ int val; // 节点值
+ struct DoublyListNode *next; // 后继节点
+ struct DoublyListNode *prev; // 前驱节点
+} DoublyListNode;
+
+/* 构造函数 */
+DoublyListNode *newDoublyListNode(int num) {
+ DoublyListNode *new = (DoublyListNode *)malloc(sizeof(DoublyListNode));
+ new->val = num;
+ new->next = NULL;
+ new->prev = NULL;
+ return new;
+}
+
+/* 析构函数 */
+void delDoublyListNode(DoublyListNode *node) {
+ free(node);
+}
+
+/* 基于双向链表实现的双向队列 */
+typedef struct {
+ DoublyListNode *front, *rear; // 头节点 front ,尾节点 rear
+ int queSize; // 双向队列的长度
+} LinkedListDeque;
+
+/* 构造函数 */
+LinkedListDeque *newLinkedListDeque() {
+ LinkedListDeque *deque = (LinkedListDeque *)malloc(sizeof(LinkedListDeque));
+ deque->front = NULL;
+ deque->rear = NULL;
+ deque->queSize = 0;
+ return deque;
+}
+
+/* 析构函数 */
+void delLinkedListdeque(LinkedListDeque *deque) {
+ // 释放所有节点
+ for (int i = 0; i < deque->queSize && deque->front != NULL; i++) {
+ DoublyListNode *tmp = deque->front;
+ deque->front = deque->front->next;
+ free(tmp);
+ }
+ // 释放 deque 结构体
+ free(deque);
+}
+
+/* 获取队列的长度 */
+int size(LinkedListDeque *deque) {
+ return deque->queSize;
+}
+
+/* 判断队列是否为空 */
+bool empty(LinkedListDeque *deque) {
+ return (size(deque) == 0);
+}
+
+/* 入队 */
+void push(LinkedListDeque *deque, int num, bool isFront) {
+ DoublyListNode *node = newDoublyListNode(num);
+ // 若链表为空,则令 front 和 rear 都指向node
+ if (empty(deque)) {
+ deque->front = deque->rear = node;
+ }
+ // 队首入队操作
+ else if (isFront) {
+ // 将 node 添加至链表头部
+ deque->front->prev = node;
+ node->next = deque->front;
+ deque->front = node; // 更新头节点
+ }
+ // 队尾入队操作
+ else {
+ // 将 node 添加至链表尾部
+ deque->rear->next = node;
+ node->prev = deque->rear;
+ deque->rear = node;
+ }
+ deque->queSize++; // 更新队列长度
+}
+
+/* 队首入队 */
+void pushFirst(LinkedListDeque *deque, int num) {
+ push(deque, num, true);
+}
+
+/* 队尾入队 */
+void pushLast(LinkedListDeque *deque, int num) {
+ push(deque, num, false);
+}
+
+/* 访问队首元素 */
+int peekFirst(LinkedListDeque *deque) {
+ assert(size(deque) && deque->front);
+ return deque->front->val;
+}
+
+/* 访问队尾元素 */
+int peekLast(LinkedListDeque *deque) {
+ assert(size(deque) && deque->rear);
+ return deque->rear->val;
+}
+
+/* 出队 */
+int pop(LinkedListDeque *deque, bool isFront) {
+ if (empty(deque))
+ return -1;
+ int val;
+ // 队首出队操作
+ if (isFront) {
+ val = peekFirst(deque); // 暂存头节点值
+ DoublyListNode *fNext = deque->front->next;
+ if (fNext) {
+ fNext->prev = NULL;
+ deque->front->next = NULL;
+ delDoublyListNode(deque->front);
+ }
+ deque->front = fNext; // 更新头节点
+ }
+ // 队尾出队操作
+ else {
+ val = peekLast(deque); // 暂存尾节点值
+ DoublyListNode *rPrev = deque->rear->prev;
+ if (rPrev) {
+ rPrev->next = NULL;
+ deque->rear->prev = NULL;
+ delDoublyListNode(deque->rear);
+ }
+ deque->rear = rPrev; // 更新尾节点
+ }
+ deque->queSize--; // 更新队列长度
+ return val;
+}
+
+/* 队首出队 */
+int popFirst(LinkedListDeque *deque) {
+ return pop(deque, true);
+}
+
+/* 队尾出队 */
+int popLast(LinkedListDeque *deque) {
+ return pop(deque, false);
+}
+
+/* 打印队列 */
+void printLinkedListDeque(LinkedListDeque *deque) {
+ int *arr = malloc(sizeof(int) * deque->queSize);
+ // 拷贝链表中的数据到数组
+ int i;
+ DoublyListNode *node;
+ for (i = 0, node = deque->front; i < deque->queSize; i++) {
+ arr[i] = node->val;
+ node = node->next;
+ }
+ printArray(arr, deque->queSize);
+ free(arr);
+}
+// 双向链表节点
+fn ListNode(comptime T: type) type {
+ return struct {
+ const Self = @This();
+
+ val: T = undefined, // 节点值
+ next: ?*Self = null, // 后继节点指针
+ prev: ?*Self = null, // 前驱节点指针
+
+ // Initialize a list node with specific value
+ pub fn init(self: *Self, x: i32) void {
+ self.val = x;
+ self.next = null;
+ self.prev = null;
+ }
+ };
+}
+
+// 基于双向链表实现的双向队列
+fn LinkedListDeque(comptime T: type) type {
+ return struct {
+ const Self = @This();
+
+ front: ?*ListNode(T) = null, // 头节点 front
+ rear: ?*ListNode(T) = null, // 尾节点 rear
+ que_size: usize = 0, // 双向队列的长度
+ mem_arena: ?std.heap.ArenaAllocator = null,
+ mem_allocator: std.mem.Allocator = undefined, // 内存分配器
+
+ // 构造函数(分配内存+初始化队列)
+ pub fn init(self: *Self, allocator: std.mem.Allocator) !void {
+ if (self.mem_arena == null) {
+ self.mem_arena = std.heap.ArenaAllocator.init(allocator);
+ self.mem_allocator = self.mem_arena.?.allocator();
+ }
+ self.front = null;
+ self.rear = null;
+ self.que_size = 0;
+ }
+
+ // 析构函数(释放内存)
+ pub fn deinit(self: *Self) void {
+ if (self.mem_arena == null) return;
+ self.mem_arena.?.deinit();
+ }
+
+ // 获取双向队列的长度
+ pub fn size(self: *Self) usize {
+ return self.que_size;
+ }
+
+ // 判断双向队列是否为空
+ pub fn isEmpty(self: *Self) bool {
+ return self.size() == 0;
+ }
+
+ // 入队操作
+ pub fn push(self: *Self, num: T, is_front: bool) !void {
+ var node = try self.mem_allocator.create(ListNode(T));
+ node.init(num);
+ // 若链表为空,则令 front 和 rear 都指向 node
+ if (self.isEmpty()) {
+ self.front = node;
+ self.rear = node;
+ // 队首入队操作
+ } else if (is_front) {
+ // 将 node 添加至链表头部
+ self.front.?.prev = node;
+ node.next = self.front;
+ self.front = node; // 更新头节点
+ // 队尾入队操作
+ } else {
+ // 将 node 添加至链表尾部
+ self.rear.?.next = node;
+ node.prev = self.rear;
+ self.rear = node; // 更新尾节点
+ }
+ self.que_size += 1; // 更新队列长度
+ }
+
+ // 队首入队
+ pub fn pushFirst(self: *Self, num: T) !void {
+ try self.push(num, true);
+ }
+
+ // 队尾入队
+ pub fn pushLast(self: *Self, num: T) !void {
+ try self.push(num, false);
+ }
+
+ // 出队操作
+ pub fn pop(self: *Self, is_front: bool) T {
+ if (self.isEmpty()) @panic("双向队列为空");
+ var val: T = undefined;
+ // 队首出队操作
+ if (is_front) {
+ val = self.front.?.val; // 暂存头节点值
+ // 删除头节点
+ var fNext = self.front.?.next;
+ if (fNext != null) {
+ fNext.?.prev = null;
+ self.front.?.next = null;
+ }
+ self.front = fNext; // 更新头节点
+ // 队尾出队操作
+ } else {
+ val = self.rear.?.val; // 暂存尾节点值
+ // 删除尾节点
+ var rPrev = self.rear.?.prev;
+ if (rPrev != null) {
+ rPrev.?.next = null;
+ self.rear.?.prev = null;
+ }
+ self.rear = rPrev; // 更新尾节点
+ }
+ self.que_size -= 1; // 更新队列长度
+ return val;
+ }
+
+ // 队首出队
+ pub fn popFirst(self: *Self) T {
+ return self.pop(true);
+ }
+
+ // 队尾出队
+ pub fn popLast(self: *Self) T {
+ return self.pop(false);
+ }
+
+ // 访问队首元素
+ pub fn peekFirst(self: *Self) T {
+ if (self.isEmpty()) @panic("双向队列为空");
+ return self.front.?.val;
+ }
+
+ // 访问队尾元素
+ pub fn peekLast(self: *Self) T {
+ if (self.isEmpty()) @panic("双向队列为空");
+ return self.rear.?.val;
+ }
+
+ // 返回数组用于打印
+ pub fn toArray(self: *Self) ![]T {
+ var node = self.front;
+ var res = try self.mem_allocator.alloc(T, self.size());
+ @memset(res, @as(T, 0));
+ var i: usize = 0;
+ while (i < res.len) : (i += 1) {
+ res[i] = node.?.val;
+ node = node.?.next;
+ }
+ return res;
+ }
+ };
+}
+As shown in the Figure 5-9 , similar to implementing a queue with an array, we can also use a circular array to implement a double-ended queue.
+ +Figure 5-9 Implementing Double-Ended Queue with Array for Enqueue and Dequeue Operations
+ +The implementation only needs to add methods for "front enqueue" and "rear dequeue":
+ +The double-ended queue combines the logic of both stacks and queues, thus it can implement all the application scenarios of these two, while offering greater flexibility.
We know that the "undo" feature in software is typically implemented using a stack: the system pushes each change operation onto the stack, and then pops to implement undoing. However, considering the limitations of system resources, software often restricts the number of undo steps (for example, only allowing the last 50 steps). When the length of the stack exceeds 50, the software needs to perform a deletion operation at the bottom of the stack (the front of the queue). But a regular stack cannot perform this function, which is where a double-ended queue becomes necessary. Note that the core logic of "undo" still follows the Last-In-First-Out principle of a stack, but a double-ended queue can more flexibly implement some additional logic.