- 逆波兰表达式求值
本题不难,但第一次做的话,会很难想到,所以先看视频,了解思路再去做题
题目链接/文章讲解/视频讲解:https://programmercarl.com/0150.%E9%80%86%E6%B3%A2%E5%85%B0%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B1%82%E5%80%BC.html
- 滑动窗口最大值 (有点难度,可能代码写不出来,但一刷至少需要理解思路)
之前讲的都是栈的应用,这次该是队列的应用了。
本题算比较有难度的,需要自己去构造单调队列,建议先看视频来理解。
题目链接/文章讲解/视频讲解:https://programmercarl.com/0239.%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E6%9C%80%E5%A4%A7%E5%80%BC.html
347.前 K 个高频元素 (有点难度,可能代码写不出来,一刷至少需要理解思路)
大/小顶堆的应用, 在C++中就是优先级队列
本题是 大数据中取前k值 的经典思路,了解想法之后,不算难。
题目链接/文章讲解/视频讲解:https://programmercarl.com/0347.%E5%89%8DK%E4%B8%AA%E9%AB%98%E9%A2%91%E5%85%83%E7%B4%A0.html
逆波兰表达式求值
class Solution {
public int evalRPN(String[] tokens) {
Deque<Integer> stack = new LinkedList();
for (String s : tokens) {
if ("+".equals(s)) { // leetcode 内置jdk的问题,不能使用==判断字符串是否相等
stack.push(stack.pop() + stack.pop()); // 注意 - 和/ 需要特殊处理
} else if ("-".equals(s)) {
stack.push(-stack.pop() + stack.pop());
} else if ("*".equals(s)) {
stack.push(stack.pop() * stack.pop());
} else if ("/".equals(s)) {
int temp1 = stack.pop();
int temp2 = stack.pop();
stack.push(temp2 / temp1);
} else {
stack.push(Integer.valueOf(s));
}
}
return stack.pop();
}
}
你提到的 波兰表达式(Polish Notation),也叫做 前缀表达式(Prefix Notation),是一种数学表达式的写法,在这种写法中,运算符放在操作数之前。
波兰表达式的基本规则:
- 运算符在操作数之前:例如,
+ 1 2
表示1 + 2
,而1 + 2
是普通的中缀表达式。 - 没有括号:运算符的顺序决定了运算的先后,不需要使用括号来明确优先级。
- 每个运算符都需要两个操作数。
波兰表达式的例子:
-
中缀表达式:
3 + 4
-
波兰表达式:
+ 3 4
-
中缀表达式:
(3 + 4) * 5
-
波兰表达式:
* + 3 4 5
如何逆波兰表达式(Reverse Polish Notation, RPN)进行求值:
逆波兰表达式(RPN)是波兰表达式的变种,其中运算符跟随在操作数之后。例如,3 4 +
表示 3 + 4
。
逆波兰表达式求值的算法:
- 使用栈:我们从左到右遍历表达式的每个元素:
- 如果是操作数(数字),就将其压入栈中。
- 如果是运算符(
+
,-
,*
,/
等),则从栈中弹出两个操作数,进行相应的计算,然后将结果压回栈中。
- 最终栈中会剩下一个元素,即为表达式的计算结果。
代码解析:
public int evalRPN(String[] tokens) {
// 使用双端队列作为栈来存储数字
Deque<Integer> stack = new LinkedList<>();
// 遍历 tokens 数组中的每个元素
for (String s : tokens) {
// 如果是加法运算符
if ("+".equals(s)) {
// 弹出两个数字并计算结果
stack.push(stack.pop() + stack.pop());
}
// 如果是减法运算符
else if ("-".equals(s)) {
// 弹出两个数字,注意顺序是要先弹出第二个数(用于减法)
stack.push(-stack.pop() + stack.pop());
}
// 如果是乘法运算符
else if ("*".equals(s)) {
// 弹出两个数字并计算结果
stack.push(stack.pop() * stack.pop());
}
// 如果是除法运算符
else if ("/".equals(s)) {
int temp1 = stack.pop(); // 除数
int temp2 = stack.pop(); // 被除数
stack.push(temp2 / temp1); // 计算并将结果入栈
}
// 如果是数字,将数字压入栈中
else {
stack.push(Integer.valueOf(s));
}
}
// 最终栈中会只有一个元素,即为最终结果
return stack.pop();
}
代码逐行解释:
Deque<Integer> stack = new LinkedList<>();
:使用双端队列(Deque
)来模拟栈。LinkedList
是Deque
的一个实现,它支持高效的头尾操作。- 遍历输入的每个元素
s
:- 如果
s
是加法、减法、乘法或除法运算符,就从栈中弹出两个元素进行计算,并将计算结果压回栈。 - 如果
s
是数字,将它转换为整数并压入栈中。
- 如果
stack.push()
:将元素压入栈。stack.pop()
:将栈顶元素弹出,用于运算操作。stack.pop()
和stack.push()
操作实现了数字和运算符的交换:每个操作符都会用两个栈中的数字进行计算,然后将结果压回栈中。- 最终,栈中只剩下一个元素,它就是逆波兰表达式的计算结果。
时间复杂度:
- 时间复杂度:O(n),其中
n
是tokens
数组的长度。每个元素最多进栈和出栈一次,所以时间复杂度是 O(n)。 - 空间复杂度:O(n),栈最多需要存储
n
个数字。
例子:
假设输入是:
String[] tokens = {"2", "1", "+", "3", "*"};
- 初始栈为空。
- 第一个元素
2
入栈:[2]
。 - 第二个元素
1
入栈:[2, 1]
。 - 遇到加法运算符
"+"
,弹出1
和2
,计算2 + 1 = 3
,结果入栈:[3]
。 - 第三个元素
3
入栈:[3, 3]
。 - 遇到乘法运算符
"*"
,弹出3
和3
,计算3 * 3 = 9
,结果入栈:[9]
。
最终栈中只有一个元素 9
,即为结果。
总结:
- 波兰表达式(前缀表达式)是运算符在操作数之前的一种写法,而 逆波兰表达式(后缀表达式)是操作数在运算符之后的一种写法。
- 通过栈来模拟逆波兰表达式的求值非常高效,利用栈的后进先出特性来处理运算符和操作数。
滑动窗口最大值
//解法二
//利用双端队列手动实现单调队列
/**
* 用一个单调队列来存储对应的下标,每当窗口滑动的时候,直接取队列的头部指针对应的值放入结果集即可
* 单调递减队列类似 (head -->) 3 --> 2 --> 1 --> 0 (--> tail) (左边为头结点,元素存的是下标)
*/
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
ArrayDeque<Integer> deque = new ArrayDeque<>();
int n = nums.length;
int[] res = new int[n - k + 1];
int idx = 0;
for(int i = 0; i < n; i++) {
// 根据题意,i为nums下标,是要在[i - k + 1, i] 中选到最大值,只需要保证两点
// 1.队列头结点需要在[i - k + 1, i]范围内,不符合则要弹出
while(!deque.isEmpty() && deque.peek() < i - k + 1){
deque.poll();
}
// 2.维护单调递减队列:新元素若大于队尾元素,则弹出队尾元素,直到满足单调性
while(!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
deque.pollLast();
}
deque.offer(i);
// 因为单调,当i增长到符合第一个k范围的时候,每滑动一步都将队列头节点放入结果就行了
if(i >= k - 1){
res[idx++] = nums[deque.peek()];
}
}
return res;
}
}
前 K 个高频元素
//解法2:基于小顶堆实现
public int[] topKFrequent2(int[] nums, int k) {
Map<Integer,Integer> map = new HashMap<>(); //key为数组元素值,val为对应出现次数
for (int num : nums) {
map.put(num, map.getOrDefault(num, 0) + 1);
}
//在优先队列中存储二元组(num, cnt),cnt表示元素值num在数组中的出现次数
//出现次数按从队头到队尾的顺序是从小到大排,出现次数最低的在队头(相当于小顶堆)
PriorityQueue<int[]> pq = new PriorityQueue<>((pair1, pair2) -> pair1[1] - pair2[1]);
for (Map.Entry<Integer, Integer> entry : map.entrySet()) { //小顶堆只需要维持k个元素有序
if (pq.size() < k) { //小顶堆元素个数小于k个时直接加
pq.add(new int[]{entry.getKey(), entry.getValue()});
} else {
if (entry.getValue() > pq.peek()[1]) { //当前元素出现次数大于小顶堆的根结点(这k个元素中出现次数最少的那个)
pq.poll(); //弹出队头(小顶堆的根结点),即把堆里出现次数最少的那个删除,留下的就是出现次数多的了
pq.add(new int[]{entry.getKey(), entry.getValue()});
}
}
}
int[] ans = new int[k];
for (int i = k - 1; i >= 0; i--) { //依次弹出小顶堆,先弹出的是堆的根,出现次数少,后面弹出的出现次数多
ans[i] = pq.poll()[0];
}
return ans;
}