本文正在参加「金石计划」
栈
对于数据结构而言,栈的重要性不言而喻,往往是笔试/面试向中的重中之重。今天一起学习 道与栈相关的题目。
232. 用栈实现队列
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):
实现 MyQueue 类:
- void push(int x) 将元素 x 推到队列的末尾
- int pop() 从队列的开头移除并返回元素
- int peek() 返回队列开头的元素
- boolean empty() 如果队列为空,返回 true ;否则,返回 false
说明:
- 你只能使用标准的栈操作 —— 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。
- 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。
进阶:
- 你能否实现每个操作均摊时间复杂度为 O(1) 的队列?换句话说,执行 n 个操作的总时间复杂度为 O(n) ,即使其中一个操作可能花费较长时间。
示例:
输入:
["MyQueue", "push", "push", "peek", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 1, 1, false]
解释:
MyQueue myQueue = new MyQueue();
myQueue.push(1); // queue is: [1]
myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue)
myQueue.peek(); // return 1
myQueue.pop(); // return 1, queue is [2]
myQueue.empty(); // return false
提示:
- 1 <= x <= 9
- 最多调用 100 次 push、pop、peek 和 empty
- 假设所有操作都是有效的 (例如,一个空的队列不会调用 pop 或者 peek 操作)
基本思路
无论「用栈实现队列」还是「用队列实现栈」,思路都是类似的。
都可以通过使用两个栈/队列来解决。
我们创建两个栈,分别为 out 和 in,用作处理「输出」和「输入」操作。
其实就是两个栈来回「倒腾」。
而对于「何时倒腾」决定了是 O(n) 解法 还是 均摊 O(1) 解法。
O(n) 解法
我们创建两个栈,分别为 out 和 in:
in用作处理输入操作push(),使用in时需确保out为空out用作处理输出操作pop()和peek(),使用out时需确保in为空
class MyQueue {
Deque<Integer> out, in;
public MyQueue() {
in = new ArrayDeque<>();
out = new ArrayDeque<>();
}
public void push(int x) {
while (!out.isEmpty()) in.addLast(out.pollLast());
in.addLast(x);
}
public int pop() {
while (!in.isEmpty()) out.addLast(in.pollLast());
return out.pollLast();
}
public int peek() {
while (!in.isEmpty()) out.addLast(in.pollLast());
return out.peekLast();
}
public boolean empty() {
return out.isEmpty() && in.isEmpty();
}
}
- 时间复杂度:
- 空间复杂度:
均摊 O(1) 解法
事实上,我们不需要在每次的「入栈」和「出栈」操作中都进行「倒腾」。
我们只需要保证,输入的元素总是跟在前面的输入元素的后面,而输出元素总是最早输入的那个元素即可。
可以通过调整「倒腾」的时机来确保满足上述要求,但又不需要发生在每一次操作中:
- 只有在「输出栈」为空的时候,才发生一次性的「倒腾」
class MyQueue {
Deque<Integer> out, in;
public MyQueue() {
in = new ArrayDeque<>();
out = new ArrayDeque<>();
}
public void push(int x) {
in.addLast(x);
}
public int pop() {
if (out.isEmpty()) {
while (!in.isEmpty()) out.addLast(in.pollLast());
}
return out.pollLast();
}
public int peek() {
if (out.isEmpty()) {
while (!in.isEmpty()) out.addLast(in.pollLast());
}
return out.peekLast();
}
public boolean empty() {
return out.isEmpty() && in.isEmpty();
}
}
- 时间复杂度:
pop()和peek()操作都是均摊 - 空间复杂度:
155. 最小栈
设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
- push(x) —— 将元素 x 推入栈中。
- pop() —— 删除栈顶的元素。
- top() —— 获取栈顶元素。
- getMin() —— 检索栈中的最小元素。
示例:
输入:
["MinStack","push","push","push","getMin","pop","top","getMin"]
[[],[-2],[0],[-3],[],[],[],[]]
输出:
[null,null,null,null,-3,null,0,-2]
解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.getMin(); --> 返回 -2.
提示:
- pop、top 和 getMin 操作总是在 非空栈 上调用。
双栈解法
为了快速找到栈中最小的元素,我们可以使用一个辅助栈 help。
通过控制 help 的压栈逻辑来实现:help 栈顶中始终存放着栈内元素的最小值。
代码:
class MinStack {
Deque<Integer> data = new ArrayDeque<>();
Deque<Integer> help = new ArrayDeque<>();
public void push(int val) {
data.addLast(val);
if (help.isEmpty() || help.peekLast() >= val) {
help.addLast(val);
} else {
help.addLast(help.peekLast());
}
}
public void pop() {
data.pollLast();
help.pollLast();
}
public int top() {
return data.peekLast();
}
public int getMin() {
return help.peekLast();
}
}
- 时间复杂度:所有的操作均为
- 空间复杂度:
385. 迷你语法分析器
给定一个字符串 s 表示一个整数嵌套列表,实现一个解析它的语法分析器并返回解析的结果 NestedInteger 。
列表中的每个元素只可能是整数或整数嵌套列表
示例 1:
输入:s = "324",
输出:324
解释:你应该返回一个 NestedInteger 对象,其中只包含整数值 324。
提示:
s由数字、方括号"[]"、负号'-'、逗号','组成- 用例保证
s是可解析的NestedInteger - 输入中的所有值的范围是
栈
每个 [ 的出现意味着存在一个嵌套类型的 NestedInteger 实例,同时每个 NestedInteger 实例(无论是嵌套类型还是数值类型)都归属于其最近一个左边的嵌套类型的 NestedInteger 实例(即其左边最近一个 [),因此我们可以使用栈来处理。
对出现的几类字符进行简单分情况讨论:
,:跳过即可;-或数字:将连续段代表数值的字符串取出,创建数值型的NestedInteger实例并压入栈中;[:创建一个嵌套类型的NestedInteger实例并压入栈中,同时为了方便,同时压入一个起「标识」作用的NestedInteger对象;]:从栈中取出元素,直到遇到起「标识」作用的NestedInteger对象(说明找到与当前]成对的[),将[和]之间的所有元素添加到[所代指的嵌套NestedInteger实例中,然后将[所代指的嵌套NestedInteger实例重新放入栈中。
按照上述逻辑处理完整个 s,最终栈顶元素即是答案。
代码:
class Solution {
static NestedInteger ph = new NestedInteger(0);
public NestedInteger deserialize(String s) {
Deque<NestedInteger> d = new ArrayDeque<>();
char[] cs = s.toCharArray();
int n = cs.length, i = 0;
while (i < n) {
if (cs[i] == ',' && ++i >= 0) continue;
if (cs[i] == '-' || (cs[i] >= '0' && cs[i] <= '9')) {
int j = cs[i] == '-' ? i + 1 : i, num = 0;
while (j < n && (cs[j] >= '0' && cs[j] <= '9')) num = num * 10 + (cs[j++] - '0');
d.addLast(new NestedInteger(cs[i] == '-' ? -num : num));
i = j;
} else if (cs[i] == '[') {
d.addLast(new NestedInteger());
d.addLast(ph);
i++;
} else {
List<NestedInteger> list = new ArrayList<>();
while (!d.isEmpty()) {
NestedInteger poll = d.pollLast();
if (poll == ph) break;
list.add(poll);
}
for (int j = list.size() - 1; j >= 0; j--) d.peekLast().add(list.get(j));
i++;
}
}
return d.peekLast();
}
}
- 时间复杂度:
- 空间复杂度:
71. 简化路径
给你一个字符串 path,表示指向某一文件或目录的 Unix 风格 绝对路径 (以 '/' 开头),请你将其转化为更加简洁的规范路径。
在 Unix 风格的文件系统中,一个点(.)表示当前目录本身;此外,两个点 (..) 表示将目录切换到上一级(指向父目录);两者都可以是复杂相对路径的组成部分。任意多个连续的斜杠(即,'//')都被视为单个斜杠 '/' 。 对于此问题,任何其他格式的点(例如,'...')均被视为文件/目录名称。
请注意,返回的 规范路径 必须遵循下述格式:
- 始终以斜杠
'/'开头。 - 两个目录名之间必须只有一个斜杠
'/'。 - 最后一个目录名(如果存在)不能 以
'/'结尾。 - 此外,路径仅包含从根目录到目标文件或目录的路径上的目录(即,不含 '
.'或'..')。
返回简化后得到的 规范路径。
示例 1:
输入:path = "/home/"
输出:"/home"
解释:注意,最后一个目录名后面没有斜杠。
提示:
path由英文字母,数字,'.','/'或'_'组成。path是一个有效的Unix风格绝对路径。
模拟
根据题意,使用栈进行模拟即可。
具体的,从前往后处理 path,每次以 item 为单位进行处理(有效的文件名),根据 item 为何值进行分情况讨论:
item为有效值 :存入栈中;item为..:弹出栈顶元素(若存在);item为.:不作处理。
代码:
class Solution {
public String simplifyPath(String path) {
Deque<String> d = new ArrayDeque<>();
int n = path.length();
for (int i = 1; i < n; ) {
if (path.charAt(i) == '/' && ++i >= 0) continue;
int j = i + 1;
while (j < n && path.charAt(j) != '/') j++;
String item = path.substring(i, j);
if (item.equals("..")) {
if (!d.isEmpty()) d.pollLast();
} else if (!item.equals(".")) {
d.addLast(item);
}
i = j;
}
StringBuilder sb = new StringBuilder();
while (!d.isEmpty()) sb.append("/" + d.pollFirst());
return sb.length() == 0 ? "/" : sb.toString();
}
}
- 时间复杂度:
- 空间复杂度:
总结
与数组、链表等数据结构类型,栈同样是线性类型的数据结构。
其具有后进先出的特性,往往与先进先出的队列共同提及,栈作为基础数据结构往往出现在中小厂的面试/笔试题中,需要重点掌握。