栈:先进后出
队列:先进先出
用栈实现队列
核心思想:两个栈,一个负责入,一个负责出。
class MyQueue {
Stack<Integer> a, b;
public MyQueue() {
a = new Stack<>();
b = new Stack<>();
}
public void push(int x) {
a.push(x);
}
public int pop() {
if (!b.isEmpty()) return b.pop();
while (!a.isEmpty()) b.push(a.pop());
return b.pop();
}
public int peek() {
if (!b.isEmpty()) return b.peek();
while (!a.isEmpty()) b.push(a.pop());
return b.peek();
}
public boolean empty() {
return a.isEmpty() && b.isEmpty();
}
}
数组模拟队列参考:【AcWing 学习】数据结构 + STL
用队列实现栈
class MyStack {
// a - 输入队列, b - 输出队列
Queue<Integer> a, b;
public MyStack() {
a = new LinkedList<>();
b = new LinkedList<>();
}
public void push(int x) {
a.offer(x);
// 将 b 队列中元素全部转给 a
while (!b.isEmpty()) a.offer(b.poll());
// 交换 a 和 b, 使得 a 在没有 push() 时永远为空队列
Queue<Integer> tmp = a;
a = b;
b = tmp;
}
public int pop() {
return b.poll();
}
public int top() {
return b.peek();
}
public boolean empty() {
return b.isEmpty();
}
}
数组模拟栈参考:【AcWing 学习】数据结构 + STL
有效的括号
给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串 s
,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
输入:s = "()[]{}"
输出:true
public boolean isValid(String s) {
var map = Map.of('(', ')', '[', ']', '{', '}');
Stack<Character> st = new Stack<>();
for (char c : s.toCharArray()) {
// 左括号入栈
if (map.containsKey(c)) st.push(c);
// 右括号, 判断是否与栈顶匹配
else if (st.isEmpty() || c != map.get(st.pop())) return false;
}
return st.isEmpty();
}
删除字符串中的所有相邻重复项
给出由小写字母组成的字符串 S
,重复项删除操作会选择两个相邻且相同的字母,并删除它们。
在 S 上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
输入:"abbaca"
输出:"ca"
解释:
例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"。
利用栈模拟:
public String removeDuplicates(String s) {
Deque<Character> st = new LinkedList<>();
for (char c : s.toCharArray()) {
if (!st.isEmpty() && st.peek() == c) st.pop();
else st.push(c);
}
StringBuilder sb = new StringBuilder();
while (!st.isEmpty()) sb.append(st.pop());
return sb.reverse().toString();
}
利用 StringBuidler 实现栈的操作:
public String removeDuplicates(String s) {
StringBuilder sb = new StringBuilder();
for (char c : s.toCharArray()) {
int len = sb.length();
if (len > 0 && c == sb.charAt(len - 1)) sb.deleteCharAt(len - 1);
else sb.append(c);
}
return sb.toString();
}
逆波兰表达式求值
根据 逆波兰表示法,求表达式的值。
有效的算符包括 +
、-
、*
、/
。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
注意 两个整数之间的除法只保留整数部分。
可以保证给定的逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。
输入:tokens = ["2","1","+","3","*"]
输出:9
解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
没有括号参与的逆波兰表达式十分简单。
public int evalRPN(String[] tokens) {
Deque<Integer> st = new ArrayDeque<>();
for (String s : tokens) {
if ("+".equals(s)) st.push(st.pop() + st.pop());
else if ("-".equals(s)) st.push(-st.pop() + st.pop());
else if ("*".equals(s)) st.push(st.pop() * st.pop());
else if ("/".equals(s)) {
int num1 = st.pop(), num2 = st.pop();
st.push(num2 / num1);
} else st.push(Integer.parseInt(s));
}
return st.pop();
}
滑动窗口的最大值*
给你一个整数数组 nums
,有一个大小为 k
的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k
个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
单调队列:永远保证队列头节点是最大值(或最小值)
public int[] maxSlidingWindow(int[] nums, int k) {
int[] res = new int[nums.length - k + 1];
LinkedList<Integer> q = new LinkedList<>(); // 队列中存下标
int idx = 0;
for (int i = 0; i < nums.length; i++) {
// 队列头节点是最先放进来的, 需要检查在 [i - k + 1, i] 范围内
if (!q.isEmpty() && i - k + 1 > q.peek()) q.poll();
// 单调队列, 保证每次进去的数字比末尾的大
while (!q.isEmpty() && nums[i] > nums[q.peekLast()]) q.pollLast();
q.offer(i);
// 当 i 符合 k 范围的时候, 每次队列头节点放入结果
if (i >= k - 1) res[idx++] = nums[q.peek()];
}
return res;
}
前 K 个高频元素
给你一个整数数组 nums
和一个整数 k
,请你返回其中出现频率前 k
高的元素。你可以按 任意顺序 返回答案。
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
优先级队列<二元对<数字, 频率>>
大根堆
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
for (int n : nums) map.put(n, map.getOrDefault(n, 0) + 1);
PriorityQueue<Map.Entry<Integer, Integer>> pq = new PriorityQueue<>(
(o1, o2) -> o2.getValue() - o1.getValue()); // 大根堆
for (var entry : map.entrySet()) pq.offer(entry);
int[] res = new int[k];
for (int i = 0; i < k; i++) res[i] = pq.poll().getKey();
return res;
}
对 map 的 value 进行排序:
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
for (int n : nums) map.put(n, map.getOrDefault(n, 0) + 1);
// 对 map 的 value 进行排序
var list = new ArrayList<>(map.entrySet());
Collections.sort(list, (o1, o2) -> o2.getValue() - o1.getValue());
int[] res = new int[k];
for (int i = 0; i < k; i++) res[i] = list.get(i).getKey();
return res;
}
维护 size 为 k 的小根堆:
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
for (int n : nums) map.put(n, map.getOrDefault(n, 0) + 1);
PriorityQueue<Integer> pq = new PriorityQueue<>(
(a, b) -> map.get(a) - map.get(b)); // 小根堆
for (int n : map.keySet()) {
if (pq.size() < k) pq.add(n); // size < k 则直接添加
// size > k 则判断是否比堆顶元素大, 是则堆顶出堆, 将该元素入堆
else if (map.get(n) > map.get(pq.peek())) {
pq.poll();
pq.add(n);
}
}
int[] res = new int[k];
for (int i = 0; i < k; i++) res[i] = pq.poll();
return res;
}