经典⾯试题 - 栈的基本操作
推荐刷题顺序:
LeetCode #⾯试题03.04 化栈为队
LeetCode #682 棒球⽐赛
LeetCode #844 ⽐较含退格的字符串
LeetCode #946 验证栈序列
1. LeetCode #⾯试题03.04 化栈为队
题目描述:
实现一个MyQueue类,该类用两个栈来实现一个队列。
解题思路:
利⽤两个栈来实现,⼀个输⼊栈、⼀个输出栈。
- 输⼊栈⽤于读⼊数据。当需要输出元素时,
若输出栈为空,则将输⼊栈的所有元素推送到输出栈
,然后取栈顶元素;若输出栈⾮空,则直接输出栈顶即可。 - 输出栈的作⽤是对已经被反转的序列进⾏⼆次反转。
创建两个栈,Stack1为输入栈、Stack2为输出栈
入队列直接就往Stack1入栈即可,假设依次入队1,2,3:
当碰到出队列动作时,需要先判断Stack2是否为空,
若Stack2为空
,则将Stack1此时所有元素
依次出栈,再依次入栈到Stack2,由于栈的特性,先进后出,后进先出,所以Stack1最先出栈的元素就是最后入队列的元素:
当Stack1全部元素都移到Stack2后,出队列元素就是出栈Stack2栈顶元素,如下出队列一个元素为1,满足队列先进先出性质:
假设再入队元素4,5,直接入栈Stack1即可:
当再出队时,再次判断Stack2是否为空,若不为空,直接出栈Stack2的元素,出队列元素2:
注意每次出队的时候,只有当Stack2为空的时候,再将Stack1里的元素移到Stack2中,否则会造成乱序
// 化栈为队
class MyQueue {
private LinkedList<Integer> inStack = new LinkedList<>();
private LinkedList<Integer> outStack = new LinkedList<>();
/** Initialize your data structure here. */
public MyQueue() {}
/** Push element x to the back of queue. */
public void push(int x) {
inStack.push(x);
}
/** Removes the element from in front of queue and returns that element. */
public int pop() {
transfer();
return outStack.pop();
}
private void transfer() {
if (outStack.isEmpty()) {
// 只有当输出栈为空的时候,再将输入栈的元素全部转移到输出栈
while (!inStack.isEmpty()) {
outStack.push(inStack.pop());
}
}
}
/** Get the front element. */
public int peek() {
transfer();
return outStack.peek();
}
/** Returns whether the queue is empty. */
public boolean empty() {
return inStack.isEmpty() && outStack.isEmpty();
}
}
/**
* 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();
* boolean param_4 = obj.empty();
*/
2. LeetCode #682 棒球⽐赛
题目描述:
你现在是一场采用特殊赛制棒球比赛的记录员。这场比赛由若干回合组成,过去几回合的得分可能会影响以后几回合的得分。
比赛开始时,记录是空白的。你会得到一个记录操作的字符串列表 ops,其中 ops[i] 是你需要记录的第 i 项操作,ops 遵循下述规则:
- 整数 x - 表示本回合新获得分数 x
- “+” - 表示本回合新获得的得分是前两次得分的总和。题目数据保证记录此操作时前面总是存在两个有效的分数。
- “D” - 表示本回合新获得的得分是前一次得分的两倍。题目数据保证记录此操作时前面总是存在一个有效的分数。
- “C” - 表示前一次得分无效,将其从记录中移除。题目数据保证记录此操作时前面总是存在一个有效的分数。
请你返回记录中所有得分的总和。
解题思路:
比较简单,用栈实现即可。
遍历完成后,最终求栈中所有元素和即可,15+10+5=30
class Solution {
public int calPoints(String[] ops) {
LinkedList<Integer> stack = new LinkedList<>();
for (String op : ops) {
switch (op) {
case "+":
// 表示本回合新获得的得分是前两次得分的总和
Integer pre1 = stack.pop();
Integer pre2 = stack.peek();
stack.push(pre1);
stack.push(pre1 + pre2);
break;
case "D":
// 表示本回合新获得的得分是前一次得分的两倍
stack.push(stack.peek() * 2);
break;
case "C":
// 表示前一次得分无效,将其从记录中移除
stack.pop();
break;
default:
stack.push(Integer.valueOf(op));
break;
}
}
int result = 0;
while (!stack.isEmpty()) {
result += stack.pop();
}
return result;
}
}
3. LeetCode #844 ⽐较含退格的字符串
题目描述:
给定 S 和 T 两个字符串,当它们分别被输入到空白的文本编辑器后,判断二者是否相等,并返回结果。 # 代表退格字符。
注意:如果对空文本输入退格字符,文本继续为空。
进阶:
- 你可以用 O(N) 的时间复杂度和 O(1) 的空间复杂度解决该问题吗?
解题思路:
思路一:直接将给定的字符串中的退格符和应当被删除的字符都去除,还原给定字符串的一般形式。然后直接比较两字符串是否相等即可。
具体地,我们用栈处理遍历过程,每次我们遍历到一个字符:
- 如果它是退格符,那么我们将栈顶弹出;
- 如果它是普通字符,那么我们将其压入栈中。
class Solution {
public boolean backspaceCompare(String S, String T) {
LinkedList<Character> s = new LinkedList<>();
LinkedList<Character> t = new LinkedList<>();
handle(S, s);
handle(T, t);
if (s.size() != t.size()) return false;
while (!s.isEmpty()) {
if (s.pop() != t.pop()) {
return false;
}
}
return true;
}
private void handle(String str, LinkedList<Character> stack) {
for (char c : str.toCharArray()) {
if ('#' == c && !stack.isEmpty()) {
stack.pop();
} else if ('#' != c) {
stack.push(c);
}
}
}
}
可以不用栈,直接利用StringBuilder实现:
class Solution {
public boolean backspaceCompare(String S, String T) {
return handle(S).equals(handle(T));
}
private String handle(String s) {
StringBuilder sb = new StringBuilder();
for (char c : s.toCharArray()) {
if (c == '#' && sb.length() > 0) {
sb.deleteCharAt(sb.length() - 1);
} else if (c != '#') {
sb.append(c);
}
}
return sb.toString();
}
}
思路二:双指针法,
一个字符是否会被删掉,只取决于该字符后面的退格符,而与该字符前面的退格符无关。因此当我们逆序地遍历字符串,就可以立即确定当前字符是否会被删掉。
具体地,我们定义 skip 表示当前待删除的字符的数量。每次我们遍历到一个字符:
- 若该字符为退格符,则我们需要多删除一个普通字符,我们让 skip 加 1;
- 若该字符为普通字符:
- 若 skip 为 0,则说明当前字符不需要删去(需要进行比较);
- 若 skip 不为 0,则说明当前字符需要删去(不需要比较),我们让 skip 减 1。
这样,我们定义两个指针,分别指向两字符串的末尾。每次我们让两指针逆序地遍历两字符串,直到两字符串能够各自确定一个字符,然后将这两个字符进行比较。重复这一过程直到找到的两个字符不相等,或遍历完字符串为止。
假设两个字符串S和T,分别逆序寻找“确定字符”进行比较:
以此类推,S下一个确定字符是’a’,T下一个确定字符是’a’,还是相等:
继续遍历,最终两个指针都指向-1,说明退格字符串相等。
class Solution {
public boolean backspaceCompare(String S, String T) {
// skip为允许跳过的字符数量
int skipS = 0;
int skipT = 0;
// i 是S的指针,j 是T的指针,只要有任何一个字符串遍历完就一直循环
for (int i = S.length() - 1, j = T.length() - 1; i >= 0 || j >= 0;) {
// 逆序找S中的确定字符
for (; i >= 0; i--) {
if (S.charAt(i) == '#') {
skipS++;
} else {
if (skipS > 0) {
skipS--;
} else {
// skipS = 0,说明此时i指向的是确定字符
break;
}
}
}
// 逆序找T中的确定字符
for (; j >= 0; j--) {
if (T.charAt(j) == '#') {
skipT++;
} else {
if (skipT > 0) {
skipT--;
} else {
// skipT = 0,说明此时j指向的是确定字符
break;
}
}
}
if (i >= 0 && j >= 0) {// 说明两个字符串都找到了确定字符,进行比较
if (S.charAt(i) != T.charAt(j)) {
return false;
}
} else {
// 如果有任何一个找到,一个没找到,肯定不相等
if (i >= 0 || j >= 0) {
return false;
}
}
i--;
j--;
}
return true;
}
}
4. LeetCode #946 验证栈序列
题目描述:
给定 pushed 和 popped 两个序列,每个序列中的 值都不重复,只有当它们可能是在最初空栈上进行的推入 push 和弹出 pop 操作序列的结果时,返回 true;否则,返回 false 。
解题思路:
由题意可知,pushed代表的是入栈的序列,popped代表的是出栈的序列,观察出栈序列popped思考:
- 第一个出栈元素,该元素在入栈序列中所在位置前面的元素肯定已经入栈了(出栈序列popped[0]=4,说明入栈序列pushed[0]=1,pushed[1]=2,pushed[2]=3已经入栈了)
- 第二个出栈元素,
要么是已经入栈的栈顶元素,要么是未来入栈的元素
(出栈4以后,栈顶元素为3,而出栈序列第二个元素popped[1]=5,其只可能是未来入栈的元素)
因此,我们可以创建一个栈,重点关注出栈的序列,模拟入栈和出栈:
- 如果当前栈顶元素不是出栈序列指定要出栈的元素(或者栈为空),则根据入栈序列入栈元素,直到和要出栈的元素相等或者入栈序列全部都入栈了
- 如果当前栈顶元素是出栈序列指定要出栈的元素,弹栈即可,出栈序列指针向后走一步,继续遍历处理下一个要弹栈的元素
当我们创建的栈能够按照popped序列指定元素顺序依次弹栈走完popped,说明栈序列是合法的,否则如果当遍历过程中,入栈序列pushed已经全部入栈了也不能弹栈,说明是非法的。
核心思想就是根据出栈顺序将入栈的栈里的值清空。
图解:
首先,看到出栈序列第一个出栈的元素是4,而我们创建的栈中是空的,所以我们先根据入栈序列入栈一个元素:
入栈之后入栈序列的指针向后移动一位,然后开始比较栈顶元素是否和出栈序列指针指向的元素相等,发现不等,重复入栈操作,直到相等:
当入栈序列第四个元素入栈的时候,栈顶元素和出栈序列指针指向的元素相等,此时允许弹栈:
弹栈以后,出栈序列指针向后移动一位,继续比较弹栈序列下一个元素:
此时发现栈顶元素3,与出栈序列第二个元素不相等,继续入栈操作:
将5入栈之后,栈顶和出栈序列指针指向的值相等,进行弹栈,之后从栈中依次弹出的值和出栈序列中的值顺序完全一致,图就不细画了,当poped出栈序列所有元素都处理完了,说明栈序列是合法的。
class Solution {
public boolean validateStackSequences(int[] pushed, int[] popped) {
LinkedList<Integer> stack = new LinkedList<>();
// 创建一个栈 模拟入栈出栈过程,按照出栈序列进行出栈,如果最终能清空该栈,说明合法
// i代表出栈序列索引、j为入栈序列索引
for (int i = 0, j = 0; i < popped.length; i++) {
// 当前出栈元素是poped[i]
// 如果我们创建的栈此时为空、或者栈顶元素不是出栈序列指定的出栈元素,则按照入栈序列一直入栈
// 因为要出栈的元素,只能是当前栈顶元素,或者是未来要入栈的元素
while ((stack.isEmpty() || stack.peek() != popped[i]) && j < pushed.length) {
stack.push(pushed[j++]);
}
//走到这要么是当前栈顶元素 等于 要出栈的元素,要么入栈序列已经遍历完了
if (stack.peek() != popped[i]) return false;//不等于说明入栈序列已经遍历完了,是非法的序列
stack.pop();//相等说明此时栈顶元素就是要出栈的元素,弹栈
}
return true;
}
}