这部分的内容相对来说简单一些,只需要记住栈和队列的结构特点就比较好做了。
栈:先进后出
队列:先进先出
栈与队列的题目类型,关键是看选择哪一种数据结构。
一、栈模拟队列
由于栈和队列只是出入的顺序不同,所以相互之间是可以转换的。
使用两个栈来模拟队列,就要保证栈顶的元素一定是最先加入的元素,这样就可以保证栈顶弹出时的元素是模拟队列的队头元素。
代码如下
class MyQueue {
public Stack<Integer> s1;
public Stack<Integer> s2;
public MyQueue() {
s1 = new Stack();
s2 = new Stack();
}
public void push(int x) {
// 始终保持新加入的元素在栈底,先加入的元素在栈顶
while (!s1.isEmpty()) s2.push(s1.pop());
s1.push(x);
while (!s2.isEmpty())s1.push(s2.pop());
}
public int pop() {
return s1.pop();
}
public int peek() {
return s1.peek();
}
public boolean empty() {
return s1.isEmpty() ? true : false;
}
}
二、队列模拟栈
队列模拟栈,可以使用两个队列来模拟,也可以直接使用双向队列来模拟。
使用两个队列来模拟,就要保证队头的元素是最后加入的元素,这样可以保证队头弹出的元素是模拟栈的栈顶元素。
代码如下
class MyStack {
Queue<Integer> que1;
Queue<Integer> que2;
public MyStack() {
que1 = new LinkedList();
que2 = new LinkedList();
}
public void push(int x) {
while (!que1.isEmpty()) que2.add(que1.remove());
que1.add(x);
while (!que2.isEmpty()) que1.add(que2.remove());
}
public int pop() {
return que1.remove();
}
public int top() {
return que1.peek();
}
public boolean empty() {
return que1.isEmpty() ? true : false;
}
}
也可以使用双向队列来实现,逻辑会更简单一些,队尾进队尾出就行。
代码如下
class MyStack {
private Deque<Integer> queue;
public MyStack() {
queue = new ArrayDeque<>();
}
public void push(int x) {
queue.addLast(x);
}
public int pop() {
return queue.removeLast();
}
public int top() {
return queue.getLast();
}
public boolean empty() {
return queue.isEmpty();
}
}
三、栈经典题目
栈的经典题目,那首先肯定是括号匹配了。
一旦栈顶的符号与遍历的符号匹配上了,那么就弹出栈顶元素,否则就将该符号添加入栈中。
最后看栈是否为空就可以了。
代码如下
class Solution {
public boolean isValid(String s) {
Stack<Character> st = new Stack();
char[] cs = s.toCharArray();
for (char c : cs) {
if (!st.isEmpty()) {
if (st.peek() == '(' && c == ')') {
st.pop();
continue;
}
else if (st.peek() == '[' && c == ']') {
st.pop();
continue;
}
else if (st.peek() == '{' && c == '}') {
st.pop();
continue;
}
}
st.push(c);
}
return st.isEmpty();
}
}
以及逆波兰表达式
逆波兰表达式是后缀表达式,与 x + y这种中缀表达式不同,后缀表达式更有利于计算机计算数据。
由于题目说了,表达式一定是有效的,所以不用考虑数字或者运算符的多和少情况。
一旦遇到运算符,那么就把栈顶的两个元素都取出来做运算,运算完后将结果再放回去栈中就可以了;如果不是运算符,那么就直接添加入栈中。
代码如下
class Solution {
public int evalRPN(String[] tokens) {
Stack<Integer> stack = new Stack();
for (String s : tokens) {
String res = s;
if (!stack.isEmpty() && s.equals("+") || s.equals("-") || s.equals("*") || s.equals("/")) {
int second = stack.pop();
int first = stack.pop();
if ("+".equals(s)) {
res = String.valueOf(first + second);
}else if ("-".equals(s)) {
res = String.valueOf(first - second);
}else if ("*".equals(s)) {
res = String.valueOf(first * second);
}else if ("/".equals(s)){
res = String.valueOf(first / second);
}
}
stack.push(Integer.valueOf(res));
}
return stack.pop();
}
}
四、单调队列
单调队列的特点就是队列中的元素是有序的。
以LC_239滑动窗口最大值为例。
这道题是一道困难题,每次都要选择出窗口中的最大值。
那么很容易想到,假如我们维护一个数据结构,它每次弹出的元素都是窗口最大值,那么就好解决了。
因为遍历元素时的滑动窗口是头进尾出,即先进先出的结构,符合队列的特点,那么就要使用队列来作为基础数据结构来实现。
要保证队头的元素一定是窗口最大值,就要对进出队列的元素进行处理。
- 如果说即将进入窗口的元素大于队尾的元素,那么就要一直将队尾元素弹出,直到队列为空,或者当前元素小于等于队尾元素,才将该元素加入队列。
- 如果说窗口要移除的元素等于队头元素,才将队头元素弹出,否则不作任何操作。
这样就可以保证,队列是一个有序队列,且是单调递减队列,队头元素一定是最大的元素。
窗口要移除的元素是不能出现在队列里的,因此当窗口要移除元素与队头元素相等,就要将队头元素弹出。
这样我们的数据结构就搭建好了。
由于需要将队尾元素弹出,所以要使用双向队列来实现。
// 单调队列
public class MyQueue {
public Deque<Integer> que;
public MyQueue() {
que = new ArrayDeque();
}
// 队头的一定是最大的元素
public int peek() {
if (!que.isEmpty()) return que.peekFirst();
return -1;
}
public void remove(int x) {
// 队头的元素等于窗口即将要移除的元素 才移除
if (!que.isEmpty() && this.peek() == x) que.removeFirst();
}
public void add(int x) {
// 要加入窗口的元素如果大于队尾元素 则一直删除 直到为空或者小于等于为止
while (!que.isEmpty() && x > que.peekLast()) {
que.removeLast();
}
que.addLast(x);
}
}
那么之后就是遍历数组中的元素了,一旦遍历数组的长度等于窗口大小k,之后就要执行添加、移除、和获取最大值的操作。
代码如下
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
MyQueue queue = new MyQueue();
int[] res = new int[nums.length - k + 1];
// 先把前k - 1个加进去 注意不一定全加进去
for (int i = 0; i < k - 1; i++) {
queue.add(nums[i]);
}
for (int i = 0; i < res.length; i++) {
queue.add(nums[i + k - 1]); // 第一个窗口的最后一个元素
res[i] = queue.peek(); // 返回队头元素 一定是最大的
queue.remove(nums[i]); // 移除队头 注意有可能不变
}
return res;
}
}
五、优先队列
优先队列顾名思义,队列中的元素是有优先级的,优先级的不同会影响元素在队列中的位置。
以LC_347为例。
要求前k大的频率的数字。
说到频率,首先想到是用map来保存每一个数字的频率,因为要保存的数值为2个,<数,频率>。
那么要前k大频率,如果让频率大的entry(map中的<k,v>对)排在队列前面,那么要前k大的,直接取前k个就行了。
1、用map保存频率:
// 使用哈希表保存频率
HashMap<Integer, Integer> map = new HashMap();
for (int n : nums) {
map.put(n, map.getOrDefault(n, 0) + 1);
}
2、创建优先级队列,并指定优先级的规则,entry中的value大的优先级高,排在前面。
// 使用优先队列对表进行排序
// entry是map中的每一个<k,v>对
Set<Map.Entry<Integer, Integer>> entrySet = map.entrySet();
// 参数为排序规则 要new PriorityQueue<>, new PriorityQueue是会报错的
// (o1, o2) -> o2.getValue() - o1.getValue()为大顶堆,降序排列
PriorityQueue<Map.Entry<Integer, Integer>> que = new PriorityQueue<>((o1, o2) -> o2.getValue() - o1.getValue());
// 将entry放入队列中
for (Map.Entry<Integer, Integer> e : entrySet) {
que.add(e);
}
3、取频率前k大的数字
int[] res = new int[k];
for (int i = 0; i < res.length; i++) {
res[i] = que.remove().getKey();
}
return res;
完整代码如下
class Solution {
public int[] topKFrequent(int[] nums, int k) {
// 使用哈希表保存频率
HashMap<Integer, Integer> map = new HashMap();
for (int n : nums) {
map.put(n, map.getOrDefault(n, 0) + 1);
}
// 使用优先队列对表进行排序
Set<Map.Entry<Integer, Integer>> entrySet = map.entrySet();
// 参数为排序规则 要new PriorityQueue<>, new PriorityQueue是会报错的
// (o1, o2) -> o2.getValue() - o1.getValue()为大顶堆,降序排列
PriorityQueue<Map.Entry<Integer, Integer>> que = new PriorityQueue<>((o1, o2) -> o2.getValue() - o1.getValue());
// 将entry放入队列中
for (Map.Entry<Integer, Integer> e : entrySet) {
que.add(e);
}
int[] res = new int[k];
for (int i = 0; i < res.length; i++) {
res[i] = que.remove().getKey();
}
return res;
}
}