不要纠结,干就完事了,熟练度很重要!!!多练习,多总结!!!
LeetCode 20. 有效的括号
解题思路
遇到左括号就入栈,遇到右括号就去栈中寻找最近的左括号,看是否匹配,最后记得检查栈是否清空了,这才算匹配完成。
代码实现
class Solution {
public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
for(char c:s.toCharArray()){
if(c == '(' || c == '{' || c == '['){
stack.push(c);
}else if(!stack.isEmpty() && leftOf(c) == stack.peek()){
stack.pop();
}else {
return false;
}
}
return stack.isEmpty();
}
public char leftOf(char c){
if(c == ')'){
return '(';
}else if(c == ']'){
return '[';
}else if(c == '}'){
return '{';
}
return ' ';
}
}
LeetCode 921. 使括号有效的最少添加
解题思路
核心思路是以左括号为基准,通过维护对右括号的需求数need,来计算最小的插入次数。需要注意两个地方:
- 当need == -1的时候意味着什么?
因为只有遇到右括号)的时候才会need–,need == -1意味着右括号太多了,所以需要插入左括号。
比如说s = “))“这种情况,需要插入 2 个左括号,使得s变成”()()”,才是一个合法括号串。
- 算法为什么返回res + need?
因为res记录的左括号的插入次数,need记录了右括号的需求,当 for 循环结束后,若need不为 0,那么就意味着右括号还不够,需要插入。
比如说s = “))(“这种情况,插入 2 个左括号之后,还要再插入 1 个右括号,使得s变成”()()()”,才是一个合法括号串。
代码实现
class Solution {
public int minAddToMakeValid(String s) {
int res = 0, need = 0;
for(char c: s.toCharArray()){
if(c == '('){
need++;
}else if(c == ')'){
need--;
if(need == -1){
res++;
need = 0;
}
}
}
return res+need;
}
}
LeetCode 1541. 平衡括号字符串的最少插入次数
解题思路
- 当need == -1时,意味着我们遇到一个多余的右括号,显然需要插入一个左括号。
if (s[i] == ')') {
need--;
// 说明右括号太多了
if (need == -1) {
// 需要插入一个左括号
res++;
// 同时,对右括号的需求变为 1
need = 1;
}
}
- 当遇到左括号时,若对右括号的需求量为奇数,需要插入 1 个右括号。因为一个左括号需要两个右括号嘛,右括号的需求必须是偶数。(记住res和need代表含义不同,一加一减不可抵消,在need==-1时还有额外判断逻辑)
if (s[i] == '(') {
need += 2;
if (need % 2 == 1) {
// 插入一个右括号
res++;
// 对右括号的需求减一
need--;
}
}
代码实现
class Solution {
public int minInsertions(String s) {
int res = 0, need = 0;
for(char c :s.toCharArray()){
if(c == '('){
need+=2;
if (need % 2 == 1) {
res++;
need--;
}
}else if(c == ')'){
need--;
if(need == -1){
res++;
need = 1;
}
}
}
return res+need;
}
}
LeetCode 232. 用栈实现队列
解题思路
将一个栈当作输入栈,用于压入 push 传入数据;另一个栈当作输出栈,用于 pop 和 peek 操作。
每次 pop 或 peek 时,若输出栈为空则将输入栈的全部数据依次弹出并压入输出栈,这样输出栈从栈顶往栈底的顺序就是队列从队首往队尾的顺序。
代码实现
class MyQueue {
private Stack<Integer> s1, s2;
public MyQueue() {
s1 = new Stack<Integer>();
s2 = new Stack<Integer>();
}
public void push(int x) {
s1.push(x);
}
public int pop() {
peek();
return s2.pop();
}
public int peek() {
if(s2.isEmpty()){
while(!s1.isEmpty()){
s2.push(s1.pop());
}
}
return s2.peek();
}
public boolean empty() {
return s1.isEmpty() && s2.isEmpty();
}
}
LeetCode 155. 最小栈
解题思路
使得每个元素 a 与其相应的最小值 m 时刻保持一一对应。因此我们可以使用一个辅助栈,与元素栈同步插入与删除,用于存储与每个元素对应的最小值。
- 当一个元素要入栈时,我们取当前辅助栈的栈顶存储的最小值,与当前元素比较得出最小值,将这个最小值插入辅助栈中;
- 当一个元素要出栈时,我们把辅助栈的栈顶元素也一并弹出;
- 在任意一个时刻,栈内元素的最小值就存储在辅助栈的栈顶元素中。
代码实现
class MinStack {
Stack<Integer> stack;
Stack<Integer> minStack;
/** initialize your data structure here. */
public MinStack() {
stack=new Stack<>();
minStack=new Stack<>();
}
public void push(int x) {
stack.push(x);
if(minStack.isEmpty()){
minStack.push(x);
}else {
int tmp=minStack.peek();
if (tmp<x){
minStack.push(tmp);
}else {
minStack.push(x);
}
}
}
public void pop() {
if(!stack.isEmpty()&&!minStack.isEmpty()){
stack.pop();
minStack.pop();
}
}
public int top() {
return stack.peek();
}
public int getMin() {
return minStack.peek();
}
}
LeetCode 71. 简化路径
解题思路
代码实现
class Solution {
public String simplifyPath(String path) {
String[] names = path.split("/");
Deque<String> stack = new ArrayDeque<String>();
for (String name : names) {
if ("..".equals(name)) {
if (!stack.isEmpty()) {
stack.pollLast();
}
} else if (name.length() > 0 && !".".equals(name)) {
stack.offerLast(name);
}
}
StringBuffer ans = new StringBuffer();
if (stack.isEmpty()) {
ans.append('/');
} else {
while (!stack.isEmpty()) {
ans.append('/');
ans.append(stack.pollFirst());
}
}
return ans.toString();
}
}
LeetCode 394. 字符串解码
解题思路
countStack存储重复次数,stringStack暂存上一次处理结果,currentString作为返回结果,从stringStack提取上次结果后结合最新一次操作即可获取最终答案。
代码实现
class Solution {
public String decodeString(String s) {
Deque<Integer> countStack = new ArrayDeque<>(); // 存储数字
Deque<String> stringStack = new ArrayDeque<>(); // 存储字符串
String currentString = ""; // 当前解码字符串
int k = 0; // 当前的倍数
for (char ch : s.toCharArray()) {
if (Character.isDigit(ch)) {
k = k * 10 + (ch - '0'); // 处理多位数
} else if (ch == '[') {
// 遇到 '[',将当前的字符串和数字推入各自的栈
countStack.push(k);
stringStack.push(currentString);
currentString = ""; // 重置当前字符串
k = 0; // 重置倍数
} else if (ch == ']') {
// 遇到 ']',解码
StringBuilder temp = new StringBuilder(stringStack.pop());
int repeatTimes = countStack.pop();
for (int i = 0; i < repeatTimes; i++) {
temp.append(currentString); // 重复当前字符串
}
currentString = temp.toString(); // 更新当前字符串
} else {
// 如果是字母,直接加到当前字符串
currentString += ch;
}
}
return currentString;
}
}
单调栈
输入一个数组nums = [2,1,2,4,3],你返回数组[4,2,4,-1,-1]。因为第一个 2 后面比 2 大的数是 4; 1 后面比 1 大的数是 2;第二个 2 后面比 2 大的数是 4; 4 后面没有比 4 大的数,填 -1;3 后面没有比 3 大的数,填 -1。
int[] nextGreaterElement(int[] nums) {
int n = nums.length;
// 存放答案的数组
int[] res = new int[n];
Stack<Integer> s = new Stack<>();
// 倒着往栈里放
for (int i = n - 1; i >= 0; i--) {
// 判定个子高矮
while (!s.isEmpty() && s.peek() <= nums[i]) {
// 矮个起开,反正也被挡着了。。。
s.pop();
}
// nums[i] 身后的更大元素
res[i] = s.isEmpty() ? -1 : s.peek();
s.push(nums[i]);
}
return res;
}
LeetCode 496. 下一个更大元素 I
解题思路
题目说nums1是nums2的子集,那么我们先把nums2中每个元素的下一个更大元素算出来存到一个映射里,然后再让nums1中的元素去查表即可。
代码实现
class Solution {
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
int[] greater = nextGreater(nums2);
Map<Integer, Integer> map = new HashMap<>();
for(int i = 0;i < greater.length;i++){
map.put(nums2[i], greater[i]);
}
int[] res = new int[nums1.length];
for(int i = 0;i < nums1.length;i++){
res[i] = map.get(nums1[i]);
}
return res;
}
public int[] nextGreater(int[] nums){
int n = nums.length;
int[] res = new int[n];
Stack<Integer> stack = new Stack<>();
for(int i = n-1;i >= 0;i--){
while(!stack.isEmpty() && stack.peek() < nums[i]){
stack.pop();
}
res[i] = stack.isEmpty()? -1 : stack.peek();
stack.push(nums[i]);
}
return res;
}
}
LeetCode 739. 每日温度
解题思路
该题思路同《LeetCode 496. 下一个更大元素 I》不同点在于要求的是当前值与之后最大值的间隔。
代码实现
用Stack 还是Dequeue没区别,本质是为了保存一个遍历过程中的单调数据。
本题要求右侧的第一个最大元素,那么有两种遍历顺序:
1.倒序遍历,res数据也是倒序求值,另外注意res逻辑在while循环外
class Solution {
public int[] dailyTemperatures(int[] temperatures) {
int n = temperatures.length;
int[] res = new int[n];
Stack<Integer> stack = new Stack<>();
for(int i = n-1;i >=0 ;i--){
while(!stack.isEmpty() && temperatures[stack.peek()] <= temperatures[i]){
stack.pop();
}
res[i] = stack.isEmpty()? 0 : (stack.peek()-i);
stack.push(i);
}
return res;
}
}
2.正序遍历,res数据也是正序求值,另外注意res逻辑在while循环那
class Solution {
public int[] dailyTemperatures(int[] temperatures) {
int n = temperatures.length;
int[] res = new int[n];
Stack<Integer> stack = new Stack<>();
for(int i=0;i<n;i++){
while(!stack.isEmpty()&&temperatures[i]>temperatures[stack.peek()]){
res[stack.peek()]=i-stack.peek();
stack.pop();
}
stack.push(i);
}
return res;
}
}
LeetCode 503. 下一个更大元素 II
解题思路
题目要求环形数组,对于这种需求,常用套路就是将数组长度翻倍:
代码实现
class Solution {
public int[] nextGreaterElements(int[] nums) {
int n = nums.length;
int[] res = new int[n];
Stack<Integer> stack = new Stack<>();
for(int i = 2*n-1;i >= 0;i--){
while(!stack.isEmpty() && stack.peek() <= nums[i%n]){
stack.pop();
}
res[i%n] = stack.isEmpty() ? -1: stack.peek();
stack.push(nums[i%n]);
}
return res;
}
}
单调队列
如果每个元素被加入时都这样操作,最终单调队列中的元素大小就会保持一个单调递减的顺序。
/* 单调队列的实现 */
class MonotonicQueue {
LinkedList<Integer> maxq = new LinkedList<>();
public void push(int n) {
// 将小于 n 的元素全部删除
while (!maxq.isEmpty() && maxq.getLast() < n) {
maxq.pollLast();
}
// 然后将 n 加入尾部
maxq.addLast(n);
}
public int max() {
return maxq.getFirst();
}
public void pop(int n) {
if (n == maxq.getFirst()) {
maxq.pollFirst();
}
}
}
为啥要发明「单调队列」这种结构呢,主要是为了解决下面这个场景:
给你一个数组window,已知其最值为A,如果给window中添加一个数B,那么比较一下A和B就可以立即算出新的最值;但如果要从window数组中减少一个数,就不能直接得到最值了,因为如果减少的这个数恰好是A,就需要遍历window中的所有元素重新寻找新的最值。
如果单纯地维护最值的话,优先级队列很专业,队头元素就是最值。但优先级队列无法满足标准队列结构「先进先出」的时间顺序,因为优先级队列底层利用二叉堆对元素进行动态排序,元素的出队顺序是元素的大小顺序,和入队的先后顺序完全没有关系。
与双指针的滑动窗口不同 ,每当窗口扩大(right++)和窗口缩小(left++)时,你单凭移出和移入窗口的元素即可决定是否更新答案。
但就本文开头说的那个判断一个窗口中最值的例子,你就无法单凭移出窗口的那个元素更新窗口的最值,除非重新遍历所有元素,但这样的话时间复杂度就上来了。
LeetCode 239. 滑动窗口最大值
解题思路
代码实现
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
List<Integer> list = new ArrayList<>();
MaxQueue queue = new MaxQueue();
for(int i = 0;i < nums.length;i++){
if(i < k-1){
queue.push(nums[i]);
}else{
queue.push(nums[i]);
list.add(queue.max());
queue.pop(nums[i-k+1]);
}
}
int[] res = new int[list.size()];
for(int i = 0; i < list.size();i++){
res[i] = list.get(i);
}
return res;
}
}
class MaxQueue{
LinkedList<Integer> queue = new LinkedList<>();
public void push(int n){
while(!queue.isEmpty() && queue.getLast() < n){
queue.pollLast();
}
queue.addLast(n);
}
public int max(){
return queue.getFirst();
}
public void pop(int n){
if(!queue.isEmpty() && queue.getFirst() == n){
queue.pollFirst();
}
}
}
数组去重
LeetCode 316. 去除重复字母
解题思路
- 要求一:要去重。
- 要求二:去重字符串中的字符顺序不能打乱s中字符出现的相对顺序。
- 要求三: 在所有符合上一条要求的去重字符串中,字典序最小的作为最终结果。
在stk.peek() > c时才会 pop 元素,其实这时候应该分两种情况:
情况一、如果stk.peek()这个字符之后还会出现,那么可以把它 pop 出去,反正后面还有嘛,后面再 push 到栈里,刚好符合字典序的要求。
情况二、如果stk.peek()这个字符之后不会出现了,前面也说了栈中不会存在重复的元素,那么就不能把它 pop 出去,否则你就永远失去了这个字符。
代码实现
单调栈:
class Solution {
public String removeDuplicateLetters(String s) {
Stack<Character> stack = new Stack<>();
boolean[] flag = new boolean[256];
int[] count = new int[256];
for(char c : s.toCharArray()){
count[c]++;
}
for(char c : s.toCharArray()){
count[c]--;
if(flag[c]){
continue;
}
while(!stack.isEmpty() && stack.peek() > c){
if(count[stack.peek()] == 0){
break;
}
flag[stack.pop()]=false;
}
stack.push(c);
flag[c]=true;
}
StringBuilder sb = new StringBuilder();
while(!stack.isEmpty()){
sb.append(stack.pop());
}
return sb.reverse().toString();
}
}
单调队列:
class Solution {
public String removeDuplicateLetters(String s) {
LinkedList<Character> queue = new LinkedList<>();
boolean[] flag = new boolean[256];
int[] count = new int[256];
for(char c : s.toCharArray()){
count[c]++;
}
for(char c : s.toCharArray()){
count[c]--;
if(flag[c]){
continue;
}
while(!queue.isEmpty() && queue.getLast() > c){
if(count[queue.getLast()] == 0){
break;
}
flag[queue.pollLast()]=false;
}
queue.addLast(c);
flag[c]=true;
}
StringBuilder sb = new StringBuilder();
while(!queue.isEmpty()){
sb.append(queue.pollFirst());
}
return sb.toString();
}
}
中位数
LeetCode 295. 数据流的中位数
解题思路
这个小倒三角形相当于一个从小到大的有序数组,这个梯形相当于一个从大到小的有序数组。
小倒三角不就是个大顶堆嘛,梯形不就是个小顶堆嘛,中位数可以通过它们的堆顶元素算出来。
假设元素总数是n,如果n是偶数,我们希望两个堆的元素个数是一样的,这样把两个堆的堆顶元素拿出来求个平均数就是中位数;如果n是奇数,那么我们希望两个堆的元素个数分别是n/2 + 1和n/2,这样元素多的那个堆的堆顶元素就是中位数。
代码实现
class MedianFinder {
PriorityQueue<Integer> small;
PriorityQueue<Integer> large;
public MedianFinder() {
small = new PriorityQueue<>((a, b) -> {
return b-a;
});
large = new PriorityQueue<>();
}
public void addNum(int num) {
if(small.size() >= large.size()){
small.offer(num);
large.offer(small.poll());
}else{
large.offer(num);
small.offer(large.poll());
}
}
public double findMedian() {
if(large.size() > small.size()){
return large.peek();
}else if(large.size() < small.size()){
return small.peek();
}
return (small.peek()+large.peek())/2.0;
}
}
实现计算器
LeetCode 224. 基本计算器
LeetCode 227. 基本计算器 II
解题思路
- 当i走到了算式的尽头(i == s.size() - 1),也应该将前面的数字入栈.
- 乘除法优先于加减法体现在,乘除法可以和栈顶的数结合,而加减法只能把自己放入栈。
- 括号包含的算式,我们直接视为一个数字就行了。递归的开始条件和结束条件是什么?遇到(开始递归,遇到)结束递归.
注:
每次循环走到符号判断分支!Character.isDigit( c )时,处理的是上一位的运算数字,即(3+5/2)中的3,因为在首次运算前,先将sign赋值为+,而sign的更新和num的重置是在switch的符号分支逻辑之后的进行的。这也是为什么最后一位数字(i == s.length() - 1)要特殊处理,sign符号已经上次更新过了,且遍历到字符串尾部,不会再有后续switch的逻辑了,所以这次就要进行计算。
代码实现
class Solution {
public int calculate(String s) {
return helper(s);
}
int i = 0;
public int helper(String s){
int num = 0;
char sign = '+';
Deque<Integer> stack = new LinkedList<>();
for(;i < s.length();i++){
char c = s.charAt(i);
if(Character.isDigit(c)){
num = 10*num+(c-'0');
}else if(c == '('){
i++;
num = helper(s);
}
if((!Character.isDigit(c) && c != ' ') || i == s.length() - 1){
switch(sign){
case('+'):
stack.addFirst(num);
break;
case('-'):
stack.addFirst(-num);
break;
case('*'):
stack.addFirst(stack.removeFirst()*num);
break;
case('/'):
stack.addFirst(stack.removeFirst()/num);
break;
}
sign = c;
num = 0;
}
if(c == ')'){
break;
}
}
int sum = 0;
while(!stack.isEmpty()){
sum+=stack.removeFirst();
}
return sum;
}
}
LeetCode 150. 逆波兰表达式求值
解题思路
逆波兰表达式严格遵循「从左到右」的运算。计算逆波兰表达式的值时,使用一个栈存储操作数,从左到右遍历逆波兰表达式,进行如下操作:
- 如果遇到操作数,则将操作数入栈;
- 如果遇到运算符,则将两个操作数出栈,其中先出栈的是右操作数,后出栈的是左操作数,使用运算符对两个操作数进行运算,将运算得到的新操作数入栈。
整个逆波兰表达式遍历完毕之后,栈内只有一个元素,该元素即为逆波兰表达式的值。
代码实现
class Solution {
public int evalRPN(String[] tokens) {
Stack<Integer> stack=new Stack<>();
for (String s:tokens){
if (s.equals("+")){
stack.push(stack.pop()+stack.pop());
}else if (s.equals("-")){
stack.push(-stack.pop()+stack.pop());
}else if (s.equals("*")){
stack.push(stack.pop()*stack.pop());
}else if (s.equals("/")){
int temp=stack.pop();
stack.push(stack.pop()/temp);
}else {
stack.push(Integer.parseInt(s));
}
}
return stack.pop();
}
}
总结
本题来源于Leetcode中 归属于队列、栈类型题目。
同许多在算法道路上不断前行的人一样,不断练习,修炼自己!
如有博客中存在的疑问或者建议,可以在下方留言一起交流,感谢各位!
觉得本博客有用的客官,可以给个点赞+收藏哦! 嘿嘿
喜欢本系列博客的可以关注下,以后除了会继续更新面试手撕代码文章外,还会出其他系列的文章!