基础算法(11)——栈
1. 删除字符串中的所有相邻重复项
题目描述
算法思路:
如果使用 stack 来保存的话,最后还需要把结果从栈中取出来,所以不如直接用【数组模拟一个栈】,结构:在数组的尾部【尾插尾删】,实现栈的【进栈】【出栈】,最后数组存留的内容就是最后的结果
代码实现:
class Solution {public String removeDuplicates(String s) {StringBuffer str = new StringBuffer();char[] chs = s.toCharArray();for (char ch : chs) {if (str.length() > 0 && ch == str.charAt(str.length() - 1)) {str.deleteCharAt(str.length() - 1);} else {str.append(ch);}}return str.toString();}
}
2. 比较含退格的字符串
题目描述
算法思路
由于退格时需要知道【前面元素】的信息,而且退格也符合【后进先出】的特性,因此我们可以使用【栈】结构来模拟退格的过程
当遇到非 # 字符时,直接出栈
当遇到 # 字符时,栈顶元素出栈
为了方便,直接使用【数组】来模拟实现栈结构
代码实现:
class Solution {public boolean backspaceCompare(String s, String t) {return changeStr(s).equals(changeStr(t));}public String changeStr(String s) {StringBuffer ret = new StringBuffer();char[] tmp = s.toCharArray(); // 数组模拟栈结构for (int i = 0; i < tmp.length; i++) {if (tmp[i] != '#') {ret.append(tmp[i]); // 入栈} else {if (ret.length() > 0)ret.deleteCharAt(ret.length() - 1); // 出栈}}return ret.toString();}
}
3. 基本计算器II
题目描述
算法思路
由于表达式里面没有括号,因此我们只用处理【加减乘除】混合运算即可,根据四则运算的顺序,我们可以先计算乘除法,然后再计算加减法,由此得出以下结论:
- 当一个数前面是 '+' 号时,这一个数是否会被立即计算是【不确定】的,因此我们可以先入栈
- 当一个数前面是 '-' 号时,这一个数是否会被立即计算也是【不确定】的,并且这个数已经和前面的 '-' 号绑定了,所以我们可以将这个数的相反数入栈
- 当一个数前面是 '*' 号时,这一个数可以立即与前面的一个数相乘,所以此时我们让栈顶元素出栈与之相乘,将结果入栈
- 当一个数前面是 '/' 号时,这一个数可以立即与前面的一个数相乘,所以此时我们让栈顶元素出栈与之相除,将结果入栈
使用一个 int tmp 来暂存数字,这是为了应对数字不只一位的情况(如:123);使用栈来存放数字;使用 char op 来存放运算符,初始化为 '+'
当遍历完全部的表达式后,栈中剩余【元素之和】就是最终结果
代码实现
class Solution {public int calculate(String ss) {Deque<Integer> dq = new ArrayDeque<>();int n = ss.length();char[] ch = ss.toCharArray();int i = 0; // 用于遍历 chchar op = '+'; // 用于保存运算符while (i < n) {if (ch[i] == ' ') i++;else if (ch[i] >= '0' && ch[i] <= '9') {int tmp = 0;while (i < n && ch[i] >= '0' && ch[i] <= '9') {tmp *= 10;tmp += ch[i] - '0';i++;}if (op == '-') dq.push(-tmp);else if (op == '+') dq.push(tmp);else if (op == '*') dq.push(dq.pop() * tmp);else dq.push(dq.pop() / tmp);}else {op = ch[i];i++;}}int ret = 0;while (!dq.isEmpty()) {ret += dq.pop();}return ret; }
}
4. 字符串编码
题目描述
算法思路
题目要求解码 k[encoded_string]
格式的字符串,核心是嵌套处理重复逻辑(比如 3[a2[c]]
需先解内层 2[c]
再解外层 3[acc]
)。下面用 两个栈 分工:
string
栈(代码里是st
)存待拼接的字符串片段(比如内层解码结果、普通字符段 )int
栈(代码里是nums
)存重复次数k
分情况讨论:
1. 初始化栈与变量
Stack<StringBuffer> st = new Stack<>();
st.push(new StringBuffer()); // 初始化放空串,作为最外层“容器”
Stack<Integer> nums = new Stack<>();
int n = s.length();
char[] ch = s.toCharArray();
int i = 0;
st.push(new StringBuffer())
:提前放一个空StringBuffer
,后续所有解码结果都会拼接到这个基础容器里(比如示例3[a]2[bc]
,最终结果会逐步拼到这个初始空串 )。nums
栈:专门存解码需要的重复次数k
。ch
转数组 +i
指针:方便遍历字符串,逐个字符处理。
2. 遍历字符串:分情况处理逻辑
核心是 while (i < n)
里的 if-else
,对应笔记里的四种场景:
① 遇到数字:提取完整的 k
(处理连续数字,比如 12[abc]
里的 12
)
if (ch[i] >= '0' && ch[i] <= '9') {int tmp = 0;while (i < n && ch[i] >= '0' && ch[i] <= '9') {tmp = tmp * 10 + (ch[i] - '0'); // 拼接数字(如 '1','2' → 12)i++;}nums.push(tmp); // 存到数字栈,等后面遇到 ']' 时用
}
- 逻辑:数字可能是多位数(如
100[abc]
),所以用while
循环 “吃” 完连续数字,转成整数tmp
存入nums
栈。 - 关联笔记:对应 “遇到数字,提取放入数字栈”。
② 遇到 [
:准备存新的字符串片段
else if (ch[i] == '[') {i++;StringBuffer tmp = new StringBuffer();while (i < n && ch[i] >= 'a' && ch[i] <= 'z') {tmp.append(ch[i]); // 提取 `[` 后的普通字符(如 `3[a2[c]]` 里 `[` 后可能先有 `a`)i++;}st.push(tmp); // 存到字符串栈,等后面遇到 ']' 时拼接
}
- 逻辑:
[
是 “新片段开始” 的标记,先跳过[
(i++
),然后提取[
后面紧跟的普通字符(比如2[bc]
里的bc
会在这一步处理吗?不,这里处理的是[
后到]
前的 “非嵌套” 普通字符,嵌套的会在后续循环处理 ),存入st
栈。 - 关联笔记:对应 “遇到
[
,提取后面字符串放入字符串栈”。
③ 遇到 ]
:拼接重复片段(核心解码逻辑)
else if (ch[i] == ']') {StringBuffer tmp = st.pop(); // 取出待重复的片段(如 `2[c]` 里的 `c` 或内层解码后的 `acc`)int k = nums.pop(); // 取出重复次数(如 `2` 或 `3`)while (k-- != 0) {st.peek().append(tmp); // 拼接到栈顶的字符串(外层容器或上层片段)}i++;
}
- 逻辑:
]
是 “片段结束” 的标记,此时:- 从
st
弹出待重复的片段(比如a2[c]
里,内层]
对应弹出c
,外层]
对应弹出acc
)。 - 从
nums
弹出重复次数k
,循环k
次把tmp
拼接到栈顶剩余的字符串(因为st
弹出了当前片段,栈顶是外层容器或上层未闭合的片段 )。
- 从
- 关联笔记:对应 “遇到
]
,解析(弹出片段 + 次数),拼到栈顶字符串后面”。
④ 遇到普通字符(非数字、非括号):直接拼接到栈顶
else {StringBuffer tmp = new StringBuffer();while (i < n && ch[i] >= 'a' && ch[i] <= 'z') {tmp.append(ch[i]); // 提取连续普通字符(如 `abc3[cd]` 里的 `abc`)i++;}st.peek().append(tmp); // 直接拼到当前栈顶的字符串
}
- 逻辑:普通字符不需要重复,直接提取连续字母(比如
ef
或xyz
),拼接到st
栈顶的字符串里(因为栈顶始终是 “当前正在构建的片段” )。 - 关联笔记:对应 “遇到单独字符,提取拼到栈顶字符串后面”。
- 逻辑:整个解码过程中,所有片段最终都会拼接到最开始初始化的空
StringBuffer
里(因为每次]
拼接、普通字符拼接,都是往栈顶操作,而栈底初始空串会逐步累积结果 ),所以直接取栈顶(此时栈里只剩最终结果)转成字符串返回。
示例:
以示例
s = "3[a2[c]]"
为例,走一遍流程:
- 初始化:
st = [""]
(栈底空串),nums = []
,i=0
。- 遇到
'3'
:进入数字逻辑,提取3
→nums = [3]
,i
移到'['
后(i=1
)。- 遇到
'['
:进入[
逻辑,跳过[
(i=2
),提取'a'
→st = ["", "a"]
,i
移到'2'
后(i=3
)。- 遇到
'2'
:提取2
→nums = [3, 2]
,i
移到'['
后(i=4
)。- 遇到
'['
:跳过[
(i=5
),提取'c'
→st = ["", "a", "c"]
,i
移到']'
后(i=6
)。- 遇到
']'
:弹出'c'
(tmp="c"
),弹出2
(k=2
)→ 循环 2 次拼到栈顶a
→st = ["", "acc"]
,i=7
。- 遇到
']'
:弹出'acc'
(tmp="acc"
),弹出3
(k=3
)→ 循环 3 次拼到栈底空串 →st = ["accaccacc"]
,i=8
(结束循环)。- 返回
st.peek()
→"accaccacc"
,与示例结果一致。
代码实现
class Solution {public String decodeString(String s) {// 1. 初始化栈与变量Stack<StringBuffer> st = new Stack<>();st.push(new StringBuffer()); // 初始化放空串,作为最外层“容器”Stack<Integer> nums = new Stack<>();int n = s.length();char[] ch = s.toCharArray();int i = 0;//2. 遍历字符串,分情况处理逻辑while (i < n) {// ①. 遇到数字:提取完整的 k(连续处理数字,比如 12[abc]中的 12)if (ch[i] >= '0' && ch[i] <= '9') {int tmp = 0;while (i < n && ch[i] >= '0' && ch[i] <= '9') {tmp = tmp * 10 + (ch[i] - '0'); // 拼接数字(如 '1','2' → 12)i++;}nums.push(tmp); // 存到数字栈,等后面遇到 ']' 时用} else if (ch[i] == '[') { // ②. 遇到 [:准备存新的字符串片段i++;StringBuffer tmp = new StringBuffer();while (i < n && ch[i] >= 'a' && ch[i] <= 'z') {tmp.append(ch[i]); // 提取 `[` 后的普通字符(如 `3[a2[c]]` 里 `[` 后可能先有 `a`)i++;}st.push(tmp); // 存到字符串栈,等后面遇到 ']' 时拼接} else if (ch[i] == ']') { // ③. 遇到 ]:拼接重复片段(核心逻辑)StringBuffer tmp = st.pop(); // 取出待重复的片段(如 `2[c]` 里的 `c` 或内层解码后的 `acc`)int k = nums.pop(); // 取出重复次数(如 `2` 或 `3`)while (k-- != 0) {st.peek().append(tmp); // 拼接到栈顶的字符串(外层容器或上层片段)}i++;} else { // ④. 遇到普通字符(非数字,非括号):直接拼接到栈顶StringBuffer tmp = new StringBuffer();while (i < n && ch[i] >= 'a' && ch[i] <= 'z') {tmp.append(ch[i]); // 提取连续普通字符(如 `abc3[cd]` 里的 `abc`)i++;}st.peek().append(tmp); // 直接拼到当前栈顶的字符串}}return st.peek().toString();}
}
5. 验证栈序列
题目描述
算法思路:
用栈来模拟进栈出栈的流程
一直让元素进栈,进栈的同时判断是否需要出栈,当所有元素模拟完毕之后,如果栈中还有元素,那么就是一个非法的序列,否则就是一个合法的序列
代码实现
import java.util.Stack;class Solution {public boolean validateStackSequences(int[] pushed, int[] popped) {Stack<Integer> stack = new Stack<>();int k = 0;for (int num : pushed) {stack.push(num);// 栈不为空且栈顶元素等于 popped 对应位置元素时,循环弹出并移动 popped 索引while (!stack.isEmpty() && stack.peek() == popped[k]) {stack.pop();k++;}}// 若 k 遍历完 popped 数组,说明匹配成功,否则失败return k == popped.length;}
}