算道浅行
引言
题还是得时常刷刷。时间一久,许多思路就淡了,手感也会生疏。
这次重新来一遍,参照的是《代码随想录》的顺序。 之前那次是按照题号一路刷下来,虽然也学到了不少,但是体系松散,难以融会贯通。有些题只是记住了解法,却不太能灵活应用。
于是想再刷一遍,理清思路,补全结构,也让手感回来一些。
数组
数组是一段在虚拟地址空间中连续存储、可通过索引直接访问的同类型元素集合。
int arr = ;
二分查找
写二分查找时常见两种写法,主要区别在于搜索区间的开闭方式:
一种是左闭右闭 [left, right],另一种是左闭右开 [left, right)。
不同的区间定义会影响在更新子区间时的处理方式,因此保持区间定义的一致性非常重要。 如果采用左闭右闭,就要始终维持这一不变式(invariant)。
对我来说,第一种写法更自然一些。
;
// time: O(log n)
// space: O(1)
也来看看循环结束的状态。以闭合区间的写法 [left, right], 在循环结束时如果target存在, left会最终指向这个target。如果target不存在,left会最终指向第一个大于target的元素(即插入点)。
Example: nums = [1, 3, 5, 6], target = 4
Final state after loop:
nums: [ 1, 3, 5, 6 ]
index: 0 1 2 3
↑ ↑
right left
right → last element < target (nums[1] = 3)
left → first element >= target (nums[2] = 5, insertion position)
换言之,当循环结束时:
right指向最后一个<target的元素left指向第一个≥target的元素left > right,left = right + 1left即插入位置
... [ right ] | [ left ] ...
< t ≥ t
;
// time: O(log n)
// space: O(1)
#34. Find First and Last Position of Element in Sorted Array
如果数组中的元素不唯一,我们最终会找到的是一个区间,而不是单一索引。 思路基本相同,只是在遇到相等元素时, 取决于我们如何更新搜索范围(向左还是向右继续查找), 从而决定我们找到的是左边界还是右边界。
;
// time: O(log n)
// space: O(1)
这次我们需要向下取整,也即是找下边界right。
... [ right ] | [ left ] ...
< t ≥ t
相等的情况(如果存在)经常需要单独处理,所以具体一点是这样的。
... [ right ] mid [ left ] ...
< t = > t
// for x < 2, sqrt(x) equals x itself
// for x >=2, search range is [1, x/2] (inclusive)
;
// time: O(log n)
// space: O(1)
这题与sqrt(x)基本相同。
;
// time: O(log n)
// space: O(1)
移除元素
这里用到了双指针。 双指针是一种常用的遍历与操作序列的技巧,通过同时维护两个下标或指针在数组或链表上移动,以更高效的解决问题。常见于去重、分区、反转、查找区间等场景。根据用途不同,又可分为快慢指针(一个遍历,一个构建或判断) 和左右指针(从两端向中间收缩)两种常见形式。
// i: next position for valid element
// j: next element to check
// time: O(n)
// space: O(1)
#26. Remove Duplicates from Sorted Array
这次用快慢指针来做本地去重,两个指针根据应用场景含义发生了变化,但是大致处理结构相同。
// i: last unique element
// j: next element to check
;
// time: O(n)
// space: O(1)
同样的技巧,可以观察到操作后元素的相对位置是保持不变的,将剩余元素置零即可。
// i: next position for valid element
// j: next element to check
;
// time: O(n)
// space: O(1)
#844. Backspace String Compare
逆向双指针结合跳过逻辑:
这里指针不是从前往后扫描,而是从后往前。这在处理删除、退格、合并排序结果、字符串等对齐场景非常常见。
同时代码中通过跳过逻辑(skip logic),用计数器来跳过被退格的字符。这种控制结构本质上是在双指针基础上加上了状态机逻辑,常见于带有“无效区段”或“需过滤元素”的问题。
代码层面我们也可以把它总结为双指针加嵌套跳过循环(two pointers with inner skip loop)。这种写法有几个特点:
- 外层
while (i >= 0 || j >= 0)同步推进两个指针。 - 内层
while用于跳过特定状态(这里是退格后的字符)。 - 退出内层循环后,比较两边的有效字符。
;
// time: O(m + n) where m = len(s), n = len(t)
// space: O(1)
算法层面没有显示判断i < 0 && j < 0返回true的情况,但逻辑上是隐式判断了。只要有一个指针还在范围内,就继续循环。 当两个指针都小于 0(即 i < 0 && j < 0)时,循环退出并返回true。
有序数组的平方
#977. Square of a Sorted Array
双指针从两端向中间靠拢:平方后负数会变大,因此平方后最大值一定在两端。
;
// time: O(n)
// space: O(n)
长度最小的字数组
#209. Minimum Size Subarray Sum
滑动窗口(sliding window)是数组与字符串题里常用的思想之一。滑动窗口维护一个动态区间[left, right],
不断地扩展右边界来满足条件,再收缩左边界来优化结果。
常见于在连续序列中,寻找满足某种条件的最短/最长/固定长度区间。 left和right单调向前移动,因此大多能做到O(n)的时间复杂度。
;
// time: O(n)
// space: O(1)
同样是滑动窗口的移动方式:
- 右指针
right一格一格前进 → 扩大窗口; - 左指针
left有时连续前进多步 → 缩小窗口以恢复合法状态; - 每次扩张后都要判断“当前窗口是否合法”;
- 不合法则继续收缩;
- 合法则计算/更新答案。
;
// time: O(n)
// space: O(1)
滑动窗口配合计数:
right扩张窗口加入字符,统计匹配数量matched;- 当
matched == need.size()说明窗口已包含全部字符; - 然后尝试收缩
left以最短化, 每次收缩时更新最短区间; - 若窗口不再满足条件,继续右扩。
;
// time: O(m + n), where m = len(s), n = len(t)
// space: O(m + n)
螺旋矩阵II
一道模拟过程的题目。模拟生成矩阵的过程时,可以很自然地分成四个方向依次遍历。 每一轮循环填充一层“外圈”。在开始每轮循环前,要先想清楚当前的边界是否仍然有效。
每一轮的填充顺序为:
- 从左到右,填充当前最上方一行;
- 从上到下,填充当前最右侧一列;
- 从右到左,填充当前最下方一行;
- 从下到上,填充当前最左侧一列。
当进入循环时,我们已经确保 left <= right 且 top <= bottom,
因此第 1、2 步总是在边界内可以安全执行。
但在执行完它们后,我们更新了边界:top++, right--。
此时再进入第 3、4 步前,就需要重新检查是否仍在有效范围内。
这一步实际上是为了避免在 n 为奇数时中心点被重复填充或越界。
对于二维矩阵,通常约定:
i表示行索引,从上到下增加;j表示列索引,从左到右增加。
;
// time: O(n^2)
// space: O(n^2)
与上一题基本相同,这次不是写而是读,也是要时常注意边界条件(是否会越界)。
;
区间和
前缀和(Prefix Sum or presum)是一种通过“累计”预处理来实现快速区间求和的技巧。
对于数组 $nums=[a_0, a_1, a_2, \ldots, a_{n-1}]$
定义前缀和数组 $p[i]$ 表示前i个数的总和。
即:
p[0] = 0p[1] = nums[0]p[2] = nums[0] + nums[1]p[3] = nums[0] + nums[1] + nums[2]p[i] = nums[0] + nums[1] + ... + nums[i-1]
于是任意闭合区间[l, r]的和就可以写为:sum(l, r) = p[r+1] - p[l]。
前缀和有两种常见的写法(是否在前面多加一个零)。上述是第一种写法,区间求和公式比较自然。
特别在l = 0时sum(0, r) = p[r+1] - p[0] = p[r+1]。
而如果是第二种写法我们不在前面多加一个零, 此时p[0] = nums[0]。求和时就需要对l = 0单独判断了: sum(l, r) = (l == 0 ? p[r] : p[r] - p[l-1])。
using namespace std;
int
// time: O(n)
// space: O(n)
#303. Range Sum Query - Immutable
同样也用前缀和数组。
;
/**
* Your NumArray object will be instantiated and called as such:
* NumArray* obj = new NumArray(nums);
* int param_1 = obj->sumRange(left,right);
*/
// time: O(n)
// space: O(n) = construct prefix sum array O(n) + range sum query O(1)
开发商购买土地
虽然没有直接构造二维前缀和数组,但本题依然体现了前缀和的思想。
通过提前计算每行、每列的总和,在两个方向上各自求一次前缀和,从而避免了重复累加,降低了复杂度。
using namespace std;
int
// time: O(m x n)
// space: O(m + n)
链表
链表是一种由节点顺次连接而成的线性结构,每个节点保存数据和指向下一个节点的指针,因此不需要连续内存。它的主要优点是插入、删除某个已知位置的节点可以在 O(1) 时间完成,结构灵活、内存利用弹性高。缺点是无法随机访问,访问任意位置都必须从头依次遍历,缓存局部性差、整体速度通常不如数组。
链表常用于需要频繁插入删除的场景,比如实现队列、LRU 缓存内部的双向链、或操作系统中的任务调度队列。
// node in a singly linked list
移除链表元素
#203. Remove Linked List Elements
在删除链表节点时,如果删除的是头节点,需要单独处理 head 的更新。
如果删除的是中间节点,只需让前驱节点 prev->next = cur->next。为了避免对头节点特判,我们通常在 head 前加一个 dummy 节点,把所有删除操作统一成“删除某节点的下一节点”,这是链表中常用的技巧。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
;
// time: O(n)
// space: O(1)
LeetCode 环境下一般会省略 delete,真实工程需显式释放被删除的节点。
设计链表
插入或删除一个节点都必须先找到它的前驱节点。引入 dummy node 后,链表的头节点也拥有“前驱”,从而统一了所有位置的操作逻辑。size 则用于 O(1) 判断越界,使边界处理更简单可靠。以下是单链表的实现,省略了析构函数。
;
/**
* Your MyLinkedList object will be instantiated and called as such:
* MyLinkedList* obj = new MyLinkedList();
* int param_1 = obj->get(index);
* obj->addAtHead(val);
* obj->addAtTail(val);
* obj->addAtIndex(index,val);
* obj->deleteAtIndex(index);
*/
翻转链表
# Reverse Linked List (A → B → C → D → NULL)
----------------------------------------------------
NULL A → B → C → D → NULL
^ ^
prev cur
----------------------------------------------------
NULL ← A B → C → D → NULL
^ ^
prev cur
----------------------------------------------------
NULL ← A ← B C → D → NULL
^ ^
prev cur
----------------------------------------------------
NULL ← A ← B ← C D → NULL
^ ^
prev cur
----------------------------------------------------
NULL ← A ← B ← C ← D NULL
^ ^
prev curr
(new head) (stop)
反转链表时,我们用 prev 指向已经反转好的前一节点,用 cur 指向当前处理的节点。每一步先保存 cur 的下一个节点避免链表断开,然后让 cur->next 指向 prev 完成指针反转,接着把 prev 移到 cur,cur 再移动到原来的下一个节点。这样 prev 不断向右推进、链表指针不断从右向左翻转,直到 cur 变为 NULL,此时 prev 就是整个链表反转后的新头节点。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
;
// time: O(n)
// space: O(1)
两两交换链表中的节点
prev → first → second → next
- 让
first指向second之后的节点:first → next - 把
second放到first前面:second → first - 把交换好的这一对接回链表:
prev → second
curr → second → first → next
然后把 prev 前进到 first,继续下一对处理。先操作第一步以防链表结构断掉。
由于交换操作需要访问每一对节点的前驱,因此同样要使用 dummy 节点,使头节点也拥有统一的前驱。prev 始终指向已完成交换部分的最后一个节点,用来连接下一对将要交换的节点。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
;
// time: O(n)
// space: O(1)
删除链表的倒数第N个节点
#19. Remove Nth Node From End of List
采用 dummy 节点 + 快慢指针。让 fast 先走 n 步,形成间隔;然后让 fast 和 slow 一起走,当 fast 到达链表末尾时,slow 刚好停在目标节点的前一个位置。此时将 slow->next 指向下下个节点即可完成删除。使用 dummy 能统一处理删除头节点等情况,使逻辑更加简洁稳定。
dummy → 1 → 2 → 3 → 4 → 5, n = 2
^slow
^fast
1) fast moves n steps:
dummy → 1 → 2 → 3 → 4 → 5
^slow ^fast
2) fast and slow move together until fast hits the end:
dummy → 1 → 2 → 3 → 4 → 5
^slow ^fast
3) slow->next is the target, skip it:
dummy → 1 → 2 → 3 → 5
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
;
// time: O(n)
// space: O(1)
链表相交
#160. Intersection of Two Linked Lists
双指针交换走法, 两人走一样长的路,必在交点相遇。
A: a1 → a2 → a3 → c1 → c2
B: b1 → b2 → b3 → b4 → c1 → c2
让两个指针:
- A 走完后去 B
- B 走完后去 A
最终路径:
pA: A → C → B
pB: B → C → A
两者走过的总长度都是a + b + c 若有交点则必在 c1 同步到达。
若无交点, 则都会走到 nullptr。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
;
// time: O(m + n), where m = len(a), n = len(b)
// space: O(1)
环形链表II
Floyd判圈 (弗洛伊德判环算法或Floyd Cycle Detection Algorithm)
阶段一:用两个指针(slow、fast)检测链表是否有环:
slow每次走 1 步fast每次走 2 步- 如果链表中没有环:
fast会先走到nullptr,不会相遇 - 如果链表中有环:
fast必然会追上slow,最终两者会相遇
- 如果链表中没有环:
阶段二:寻找环入口
- 让一个指针从
head出发 - 另一指针从相遇点出发
- 两者都每次走 1 步
- 最终相遇的位置就是环的入口
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
;
// time: O(n)
// space: O(1)
从 slow 的角度,它始终以 1 步/次向前移动。无环时 slow 最多走完整个链表一次;有环时 slow 到达环入口后在环内最多再走一圈就会被 fast 追上,而环长也不超过链表长度。因此 slow 的总步数始终在链表节点数量级内,整体时间复杂度为 O(n)。
哈希表
哈希表利用哈希函数把 key 映射到数组的某个位置,只要分布足够均匀,插入、删除、查找通常都能做到平均 O(1)。
核心做法就是先把 key 哈希成一个下标,再用这个下标快速定位到对应的桶(bucket),如果发生冲突则通过链表或开放寻址处理。只要负载因子不过高、冲突不集中,整体性能依然接近常数级,因此哈希表也成为工程和算法中使用最广泛的基础结构之一,C++ 的 unordered_map / unordered_set 就是典型实现。
| 操作 | 平均复杂度 | 最坏复杂度 | 说明 |
|---|---|---|---|
| 插入 | O(1) | O(n) | rehash 或大量冲突时 |
| 查找 | O(1) | O(n) | 所有元素落在一个桶里 |
| 删除 | O(1) | O(n) | 同查找一样 |
| 扩容(rehash) | armotized O(1) | O(n) | 需要重分布全部 key |
// hash set
// stores unique elements. provides O(1) average-time lookup
unordered_set<int> set = ;
// hash map
// key value mapping implemented with a hash table
unordered_map<string, int> freq = ;
// perfect hash (array counting)
// when the key space is small and continuous (e.g. 'a' to 'z')
// an array can be used as a perfect hash table
// no collisions, fastest lookup
int cnt = ;
有效的字母异位词
判断两个字符串是否由相同字符频次组成。最优方法是用大小为 26 的计数数组做频率差值,通过一次遍历解决,时间 O(n)、空间 O(1)。若字符集未知,则用哈希表。排序是最简单但不是最优的通用解法。
;
// time: O(n)
// space: O(1)
;
// time: O(n x k) where k = average length of strs
// space: O(n x k)
采用固定格式的 key(如带分隔符的 26 项编码 #1#0#3#0#0#...)更稳定也更安全:结构固定、无歧义,不会因为省略 0 而让 key 长度不一致,避免 C++ 的隐式转换问题,方便调试和扩展到更多字符集,同时在哈希表中能得到更均匀的分布。
#438. Find All Anagrams in a String
;
// time: O(|s| + |t|) => O(|s|) since |t| <= |s|
// space: O(1)
算法对字符串 s 做一次滑动窗口遍历。 每一步只更新固定大小(26)的频次数组,检查相等也是常数时间。 因此总时间复杂度为 O(|s|)。
两个数组的交集
#349. Intersection of Two Arrays
// time: O(m + n) where m = len(nums1), n = len(nums2)
// space: O(m + n)
#350. Intersection of Two Arrays II
;
// time: O(m + n) where m = len(nums1), n = len(nums2)
// space: O(m + n)
快乐数
;
// time: O(log n) need some math
// space: O(log n)
两数之和
;
// time: O(n)
// space: O(n)
四数相加II
数组长度 n ≤ 200 时,四重循环是 n⁴ ≈ 1.6e9,必然超时。 常用优化是把四个数组拆成两组:先枚举 A+B 的所有和并计数,再在 C+D 中查找对应的相反数。这样把 O(n⁴) 降为 O(n²),n²=4×10⁴,轻松通过。这是典型的 k-sum 降维技巧。
;
// time: O(n^2)
// space: O(n^2)
赎金信
;
// time: O(m + n) where m = len(s1), n = len(s2)
// space: O(1)
三数之和
3Sum 的核心是「排序 + 去重 + 双指针」。排序确保数组有序,可以自然地跳过重复元素。固定第一个数后,在右侧通过双指针根据 sum 的大小移动指针,以线性时间找到所有二元组。哈希查补数在 3Sum 中也能使用,但相比双指针较难控制去重,并且无法利用有序性减少搜索空间,因此属于次优策略。
双指针做法:
// two pointers (optimal)
;
// time: O(n^2)
// space: O(1)
哈希表做法(不推荐):
// hash set (sub-optimal)
;
// time: O(n^2) but much slower in practice
// space: O(n^2)
四数之和
承接之前的三数之和「排序 + 去重 + 双指针」,加上了一重循环。此时哈希表已经不合适了。
;
// time: O(n^3)
// space: O(1)
字符串
string s = "hello";
反转字符串
;
// time: O(n)
// space: O(1)
反转字符串II
// time: O(n)
// space: O(1)
替换数字
std::string 底层是动态数组,当容量不够时会重新分配更大的缓冲区,并把已有内容全部拷贝过去。
如果不断追加而又没有提前预留空间,就会发生多次扩容和拷贝,从而导致最坏情况下的 O(n²) 行为。
using namespace std;
int
// time: O(n)
// space: O(n)
翻转字符串里的单词
#151. Reverse Words in a String
原地解法(O(1)空间):
- 原字符串: " the sky is blue "
- 去掉首尾空格: "the sky is blue"
- 整体反转: "eulb si yks eht"
- 对每个单词单独反转:"blue is sky the"
;
// time: O(n)
// space: O(1)
右旋字符串
三次反转法: 符串的旋转,本质都是把字符串分成两段,然后交换顺序。
适用于:
- 左旋 k(把前 k 个字符移到后面)
- 右旋 k(把后 k 个字符移到前面)
时间复杂度 O(n),空间 O(1)。
== 左旋 k ==
原始:
a b c d e f g
1. reverse whole
g f e d c b a
2. reverse the first n-k elements
g f e d c | b a
↓ ↓ ↓ ↓ ↓
c d e f g | b a
3. reverse the last k elements
c d e f g | b a
↓ ↓
c d e f g | a b
== 右旋 k ==
原始:
a b c d e f g
1. reverse whole
g f e d c b a
2. reverse the first k elements
g f | e d c b a
↓ ↓
f g | e d c b a
3. reverse the last n-k elements
f g | e d c b a
↓ ↓ ↓ ↓ ↓
f g | a b c d e
// rotate right
using namespace std;
int
// time: O(n)
// space: O(1)
实现strStr()
#28. Find the Index of the First Occurrence in a String
瞻仰一下 KMP, 建议看看相关视频讲解。(代码随想录 - KMP 理论)
KMP 是一种高效的子串匹配算法(substring search)。
普通暴力算法每次失败都会让文本串回退,导致时间复杂度 O(n x m)。
KMP 利用模式串自身的信息,使匹配只向前,做到线性时间 O(n + m)。
核心思想:失败时不回退文本串,而是利用 LPS 表回退模式串的位置,继续匹配。
前缀:从第一个字符开始,但不能包含最后一个字符的所有连续子串。
例:"ababa" 的前缀(prefix)有:
a
ab
aba
abab
后缀: 以最后一个字符结束,但不能包含第一个字符的所有连续子串。
例:"ababa" 的后缀(suffix)有:
baba
aba
ba
a
LPS: Longest Prefix Suffix 即"最长前后缀"是 KMP 的核心概念。
lps[i] 表示:
pattern[0..i] 这个子串中,最长的 “相等前缀 = 相等后缀” 的长度。
例: pattern = "aabaa"
| i | 子串 | 最长相等前后缀 | lps[i] |
|---|---|---|---|
| 0 | "a" | 无 | 0 |
| 1 | "aa" | "a" | 1 |
| 2 | "aab" | 无 | 0 |
| 3 | "aaba" | "a" | 1 |
| 4 | "aabaa" | "aa" | 2 |
lps = [0, 1, 0, 1, 2]
实现
构建 LPS(前缀表)和搜索阶段使用 LPS 的方式,是完全相同的逻辑。
它们遵循同一个 loop invariant(循环不变式):
j 始终表示当前已经匹配的“最长前后缀长度”也同时指向“下一次应该尝试匹配的位置”。
在 LPS 构建阶段: i 表示正在分析的模式串下标。
扫描 pattern 本身,用来计算 pattern[0..i] 的最长前后缀。
在搜索阶段:i 表示正在比较的文本串下标。
扫描 haystack,是 KMP 的主驱动。
无论在哪个阶段,i 永远只前进不后退。
所有回退都由 j 与 LPS 表完成。
;
// time: O(m + n) where m = len(haystack), n = len(needle)
// space: O(n)
#459. Repeated Substring Pattern
- 构建 LPS 数组,
lps[i]表示最长前后缀长度。 - 若
lps[n-1] == 0→ 一定不是重复串。 - 最小重复单元长度 =
n - lps[n-1]。 - 若
n % (n - lps[n-1]) == 0→ 是重复串。
若 l == 0, 说明没有任何非空前后缀相等所以不可能是重复构成。(n - l) 是最小重复单元长度,
如果 n % (n - l) == 0 字符串长度必须能整除单元长度,满足时说明字符串由该单元反复拼接而成。
"ababab"
n = 6
l = 4
n - l = 2 → 子串 "ab"
6 % 2 == 0 → ✔
;
// time: O(n)
// space: O(n)
双指针法
大部分题目在以上其实已经总结过便不再赘述,刷一遍练手。(~ 表示重复项)
移除元素
;
// time: O(n)
// space: O(n)
反转字符串
;
// time: O(n)
// space: O(1)
栈与队列
栈
C++ 的 std::stack 是一个典型的后进先出(LIFO)容器适配器,默认使用 deque 作为底层实现,只允许从栈顶进行 push、pop 以及读取 top,这些操作都在 O(1) 时间内完成。
stack<int> s;
s.;
s.;
int x = s.; // 20
s.;
队列
C++ 的 std::queue 是一种先进先出(FIFO)的容器适配器,默认使用 deque 作为底层结构,只允许从队尾 push、从队头 pop,并通过 front() 与 back() 访问两端元素;这些操作同样在 O(1) 时间内完成。
queue<int> q;
q.; // push at tail
q.;
q.;
// Queue: head [10, 20, 30] tail
cout << q. << endl; // 10 (head)
cout << q. << endl; // 30 (tail)
q.; // pop from head (removes 10)
// Queue: head [20, 30] tail
cout << q. << endl; // 20
cout << q. << endl; // 30
用栈实现队列
#232. Implement Queue using Stacks
用两个栈:in 负责入队,out 负责出队;当 out 空时,把 in 全部倒进去。
这个实现的核心在于用两个栈分别承担入队和出队,把顺序反转一次就能得到队列的先入先出行为。绝大多数操作都是常数时间,只有在出队栈为空时才会把入队栈整体倒过去,但每个元素只经历一次倒转,因此整体仍保持接近 O(1) 的平均效率。
push 1: in=[1] out=[]
push 2: in=[1,2] out=[]
push 3: in=[1,2,3] out=[]
pop → shift:
in=[] out=[3,2,1] → return 1
push 4: in=[4] out=[3,2]
pop: in=[4] out=[3,2] → return 2
;
/**
* Your MyQueue object will be instantiated and called as such:
* MyQueue* obj = new MyQueue();
* obj->push(x);
* int param_2 = obj->pop();
* int param_3 = obj->peek();
* bool param_4 = obj->empty();
*/
// time: see each method
// space: O(n)
用队列实现栈
#225. Implement Stack using Queues
用一个队列即可:每次 push 时先把元素入队,然后把它之前的所有元素依次出队再入队,让新元素被“旋转”到队头, 这样队头始终是最新压入的元素,于是 pop 就是直接弹队头,top 直接查看队头,就像在用栈一样。
push(1): [1]
push(2): [1,2] → rotate → [2,1]
push(3): [2,1,3] → rotate → [3,2,1]
pop(): 3
;
/**
* Your MyStack object will be instantiated and called as such:
* MyStack* obj = new MyStack();
* obj->push(x);
* int param_2 = obj->pop();
* int param_3 = obj->top();
* bool param_4 = obj->empty();
*/
// time: see each method
// space: O(n)