1 栈和队列
栈的特点是后入先出,根据这个特点可以临时保存一些数据,之后用到依次再弹出来,常用于 DFS 深度搜索。
队列的特点是先入先出,一般常用于 BFS 广度搜索,类似一层一层的搜索。
2 栈常见题目
2.1 (lee-155) 最小栈
设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。
- push(x) —— 将元素 x 推入栈中。
- pop() —— 删除栈顶的元素。
- top() —— 获取栈顶元素。
- getMin() —— 检索栈中的最小元素。
输入:
[“MinStack”,“push”,“push”,“push”,“getMin”,“pop”,“top”,“getMin”]
[[ ],[-2],[0],[-3],[ ],[ ],[ ],[ ]]
输出:
[null,null,null,null,-3,null,0,-2]
解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.getMin(); --> 返回 -2.
思路:用两个栈实现,一个最小栈始终保证最小值在顶部
public class MinStack {
private Deque<Integer> stack;
private Deque<Integer> minStack;
private int min;
public MinStack() { //创建最小栈
stack = new LinkedList<Integer>();
minStack = new LinkedList<Integer>();
min = Integer.MAX_VALUE;
}
public void push(int x) { //入栈
stack.offerFirst(x);
min = Math.min(min, x);
minStack.offerFirst(min);
}
public void pop() { //出栈
stack.pollFirst();
minStack.pollFirst();
min = minStack.isEmpty() ? Integer.MAX_VALUE : minStack.peekFirst();
}
public int top() { //返回栈顶
return stack.peekFirst();
}
public int getMin() { //返回最小元素
return minStack.peekFirst();
}
public static void main(String[] args) {
MinStack e = new MinStack();
e.push(1);
e.push(2);
e.push(3);
e.push(4);
e.pop();
int p = e.top();
System.out.println(p);
int q = e.getMin();
System.out.println(q);
}
}
2.2 (lee-150) 逆波兰表达式求值
根据 逆波兰表示法,求表达式的值。有效的算符包括 +、-、*、/ 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
说明:
整数除法只保留整数部分。
给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。
输入:tokens = [“2”,“1”,"+",“3”,"*"]
输出:9
解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
/**
* 使用栈实现后缀表达式
* @param tokens
* @return
*/
public int evalRPN(String[] tokens) {
Deque<Integer> stack = new LinkedList<Integer>();
int n = tokens.length;
for(int i = 0;i <n;i++) {
String token = tokens[i];
if(isNumber(token)) {
stack.push(Integer.parseInt(token));
}else {
int num2 = stack.pop();
int num1 = stack.pop();
switch(token) {
case "+":
stack.push(num1+num2);
break;
case "-":
stack.push(num1-num2);
break;
case "*":
stack.push(num1*num2);
break;
case "/":
stack.push(num1/num2);
break;
default:
}
}
}
return stack.pop();
}
private boolean isNumber(String token) {
return !("+".equals(token) || "-".equals(token) || "*".equals(token) || "/".equals(token));
}
/**
* 创建数组模拟栈
* @param tokens
* @return
*/
public int evalRPN(String[] tokens) {
int[] nums = new int[tokens.length/2+1]; //一个有效的逆波兰表达式,其长度n一定是奇数,且操作数的个数一定比运算符的个数多1个
int index = 0; //数组下标 0 的位置对应栈底
for (String s : tokens) {
switch (s) {
case "+": //如果遇到运算符,则将两个操作数出栈,然后将一个新操作数入栈,因此栈内元素先减少 2 个再增加 1 个,结果是栈内元素减少 1 个
nums[index - 2] += nums[--index];
break;
case "-":
nums[index - 2] -= nums[--index];
break;
case "*":
nums[index - 2] *= nums[--index];
break;
case "/":
nums[index - 2] /= nums[--index];
break;
default:
nums[index++] = Integer.parseInt(s); //如果遇到操作数,则将操作数入栈,因此栈内元素增加 1 个
break;
}
}
return nums[0];
}
2.3 (lee-394) 字符串解码
给定一个经过编码的字符串,返回它解码后的字符串。编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。
输入:s = “3[a]2[bc]”
输出:“aaabcbc”
输入:s = “3[a2[c]]”
输出:“accaccacc”
/**
* 思路:数字一个栈,字母一个栈,取出来计算就可以了。
* @param s
* @return
*/
public String decodeString(String s) {
StringBuilder res = new StringBuilder();
LinkedList<Integer> numStack = new LinkedList<Integer>();
LinkedList<StringBuilder> resStack = new LinkedList<StringBuilder>();
int num =0;
char[] c = s.toCharArray();
for(int i = 0;i <c.length;i++) {
if(c[i] >= '0' && c[i] <='9') {
num = 10* num + Integer.parseInt(c[i] +"");
}else if(c[i] == '[') {
numStack.addLast(num);
resStack.addLast(res);
num = 0;
res = new StringBuilder();
}else if(c[i] == ']') {
StringBuilder temp = resStack.removeLast();
int curNum = numStack.removeLast();
for(int j = 0;j <curNum;j++) {
temp.append(res);
}
res = temp;
}else {
res.append(c[i]);
}
}
return res.toString();
}
2.4 (lee-94) 二叉树的中序遍历
给定一个二叉树的根节点 root ,返回它的 中序 遍历。
输入:root = [1,null,2,3]
输出:[1,3,2]
class TreeNode{
int val;
TreeNode left;
TreeNode right;
TreeNode(int x){
val = x;
}
}
/**
* 给定一个二叉树,返回它的中序遍历。
* 思路:通过stack 保存已经访问的元素,用于原路返回
* @param root
* @return
*/
public List<Integer> inorderTraversal(TreeNode root){
if(root==null) {
return new LinkedList<>();
}
List<Integer> res = new LinkedList<>();
Deque<TreeNode> stack = new LinkedList<>();
TreeNode node = root;
while(node!= null || !stack.isEmpty()) {
while(node!= null) {
stack.addLast(node);
node = node.left;
}
node = stack.removeLast();
res.add(node.val);
node = node.right;
}
return res;
}
2.5 (lee-133) 克隆图
给你无向 连通 图中一个节点的引用,请你返回该图的 深拷贝(克隆)。
class Node {
public int val;
public List<Node> neighbors;
public Node() {
val = 0;
neighbors = new ArrayList<Node>();
}
public Node(int _val) {
val = _val;
neighbors = new ArrayList<Node>();
}
public Node(int _val, ArrayList<Node> _neighbors) {
val = _val;
neighbors = _neighbors;
}
}
/**
* 给你无向连通图中一个节点的引用,请你返回该图的深拷贝(克隆)
* 递归:深度优先遍历
*/
public Node cloneGraph(Node node) {
if(node==null) {
return node;
}
HashMap<Node,Node> visited = new HashMap<>();
if(visited.containsKey(node)) { // 如果visited存在node的克隆结点则返回克隆结点
return visited.get(node);
}
Node cloneNode = new Node(node.val,new ArrayList<>());
visited.put(node, cloneNode);
for (Node neighbor : node.neighbors) { // 设置好cloneNode结点的克隆结点
cloneNode.neighbors.add(cloneGraph(neighbor)); // 递归node邻居结点的克隆结点
}
return cloneNode;
}
/**
* 广度优先遍历
*/
public Node cloneGraph2(Node node) {
if(node==null) {
return node;
}
HashMap<Node,Node> visited = new HashMap<>();
LinkedList<Node> queue = new LinkedList<>(); //将原节点添加到队列
visited.put(node, new Node(node.val, new ArrayList<>())); //克隆第一个节点并存储到哈希表中
// 广度优先搜索
while(!queue.isEmpty()) {
Node n = queue.remove(); // 取出队列的头节点
for(Node neighbor: n .neighbors) { // 遍历该节点的邻居
if(!visited.containsKey(neighbor)) { // 如果没有被访问过,就克隆并存储在哈希表中
visited.put(neighbor, new Node(neighbor.val, new ArrayList<>()));
queue.add(neighbor); // 将邻居节点加入队列中
}
visited.get(n).neighbors.add(visited.get(neighbor)); // 更新当前节点的邻居列表
}
}
return visited.get(node);
}
2.6 (lee-84) 柱状图中最大的矩形
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。求在该柱状图中,能够勾勒出来的矩形的最大面积。
输入: [2,1,5,6,2,3]
输出: 10
2.6.1 枚举宽 O(n*2)
public int largestRectangleArea(int[] heights) {
int n = heights.length;
int ans = 0;
// 枚举左边界
for(int l = 0;l < n;l++) {
int minHeight = Integer.MAX_VALUE;
// 枚举右边界
for(int r = l;r < n;r++) {
minHeight = Math.min(minHeight, heights[r]);// 确定高度
ans = Math.max(ans, (r-l+1)*minHeight); // 计算面积
}
}
return ans;
}
2.6.2 枚举高 O(n*2)
public int largestRectangleArea2(int[] heights) {
int n = heights.length;
int ans = 0;
for(int mid = 0;mid < n;mid++) {
int height = heights[mid];// 枚举高
int left = mid;
int right = mid;
while(left-1 >= 0 && heights[left-1] >= height) { // 确定左边界
left--;
}
while(right+1 < n && heights[right+1] >= height) { // 确定右边界
right++;
}
ans = Math.max(ans, (right-left+1)*height);// 计算面积
}
return ans;
}
2.6.3 使用单调栈 O(N)
public int largestRectangleArea3(int[] heights) {
int n = heights.length;
int[] left = new int[n];
int[] right = new int[n];
Stack<Integer> mono_stack = new Stack<Integer>();
for (int i = 0; i < n; ++i) {
while (!mono_stack.isEmpty() && heights[mono_stack.peek()] >= heights[i]) {
mono_stack.pop();
}
left[i] = (mono_stack.isEmpty() ? -1 : mono_stack.peek());
mono_stack.push(i);
}
mono_stack.clear();
for (int i = n - 1; i >= 0; --i) {
while (!mono_stack.isEmpty() && heights[mono_stack.peek()] >= heights[i]) {
mono_stack.pop();
}
right[i] = (mono_stack.isEmpty() ? n : mono_stack.peek());
mono_stack.push(i);
}
int ans = 0;
for (int i = 0; i < n; ++i) {
ans = Math.max(ans, (right[i] - left[i] - 1) * heights[i]);
}
return ans;
}
2.6.4 使用deque 用时更少
public int largestRectangleArea4(int[] heights) {
int len = heights.length;
if (len == 0) {
return 0;
}
if (len == 1) {
return heights[0];
}
int[] newHeights = new int[len + 2]; //左右边界包含
for (int i = 0; i < len; ++i) {
newHeights[i + 1] = heights[i];
}
int maxArea = 0;
len += 2;
heights = newHeights;
Deque<Integer> stack = new LinkedList<>();
stack.addFirst(0);
for (int i = 1; i < len; ++i) {
while (heights[stack.peekFirst()] > heights[i]) {
int height = heights[stack.removeFirst()];
int width = i - stack.peekFirst() - 1;
maxArea = Math.max(maxArea, height * width);
}
stack.addFirst(i);
}
return maxArea;
}
2.7 (lee-85) 最大矩形
给定一个仅包含 0 和 1 的二维二进制矩阵,找出只包含 1 的最大矩形,并返回其面积。
输入:
[[“1”,“0”,“1”,“0”,“0”],
[“1”,“0”,“1”,“1”,“1”],
[“1”,“1”,“1”,“1”,“1”],
[“1”,“0”,“0”,“1”,“0”]]
输出: 6
思路:将该问题转化为84中柱状图中最大矩形的面积
第一层柱状图的高度[“1”,“0”,“1”,“0”,“0”],最大面积为1;
第二层柱状图的高度[“2”,“0”,“2”,“1”,“1”],最大面积为3;
第三层柱状图的高度[“3”,“1”,“3”,“2”,“2”],最大面积为6;
第四层柱状图的高度[“4”,“0”,“0”,“3”,“0”],最大面积为4;
public int maximalRectangle(char[][] matrix) {
if(matrix.length == 0) {
return 0;
}
int col = matrix.length;
int row = matrix[0].length;
int[] high = new int[row];
int res = 0;
for(int i = 0;i < col;i++) {
for(int j = 0;j < row;j++) {
if(matrix[i][j] == '1') {
high[j] += 1;
}else {
high[j] = 0;
}
}
res = Math.max(res, largestRectangleArea1(high));
}
return res;
}
接下来就是求84柱状图中最大矩形的面积,还有其他方法,这里只写了一种。
/*
* 单调栈 (困难)
*/
public int largestRectangleArea1(int[] heights) {
Deque<Integer> stack = new LinkedList<>();
stack.offerFirst(-1);
int maxArea = 0;
for(int i = 0;i < heights.length;i++) {
while(stack.peekFirst() != -1 && heights[stack.peekFirst()] >= heights[i]) {
maxArea = Math.max(maxArea, heights[stack.pollFirst()] * (i - stack.peekFirst() - 1));
}
stack.offerFirst(i);
}
while(stack.peekFirst() != -1) {
maxArea = Math.max(maxArea, heights[stack.pollFirst()] * (heights.length - stack.peekFirst() - 1));
}
return maxArea;
}
2.8 (lee-227)基本计算器2
给你一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值。整数除法仅保留整数部分(无括号)。
思路:我们可以用一个栈,保存这些(进行乘除运算后的)整数的值,对于加减号后的数字,将其直接压入栈中;对于乘除号后的数字,可以直接与栈顶元素计算,并替换栈顶元素为计算后的结果。
时间复杂度O(n)
空间复杂度O(n)
输入:s = “3+2*2”
输出:7
public int calculate(String s) {
Deque<Integer> stack = new LinkedList<Integer>();//数字栈
char preSign = '+'; //符号栈用一个变量preSigh代替,只存储上一个符号
int num = 0;
int n = s.length();
for (int i = 0; i < n; ++i) {
char cur = s.charAt(i);
if (cur >= '0') {
num = num * 10 - '0'+ cur;// 记录当前数字。先减,防溢出
}
if ((cur < '0' && cur != ' ' )|| i == n - 1) {
switch (preSign) {
case '+':
stack.push(num);
break;
case '-':
stack.push(-num);
break;
case '*':
stack.push(stack.pop() * num);
break;
default:
stack.push(stack.pop() / num);
}
preSign = s.charAt(i); //保存当前符号
num = 0; //数字清0
}
}
int ans = 0;
while (!stack.isEmpty()) {
ans += stack.pop();
}
return ans;
}
3 队列常见题目
3.1 (lee-232) 用栈实现队列
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty)
实现 MyQueue 类:
- void push(int x) 将元素 x 推到队列的末尾
- int pop() 从队列的开头移除并返回元素
- int peek() 返回队列开头的元素
- boolean empty() 如果队列为空,返回 true ;否则,返回 false
输入:
[“MyQueue”, “push”, “push”, “peek”, “pop”, “empty”]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 1, 1, false]
解释:
MyQueue myQueue = new MyQueue();
myQueue.push(1); // queue is: [1]
myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue)
myQueue.peek(); // return 1
myQueue.pop(); // return 1, queue is [2]
myQueue.empty(); // return false
public class QueueUseStack {
public QueueUseStack(){}
private Deque<Integer> stackA = new LinkedList<>();//栈A作为队列的入口,负责插入新元素
private Deque<Integer> stackB = new LinkedList<>();//栈B作为队列的出口,负责移除老元素
//入队操作
public void enQueue(int x) {
stackA.addFirst(x);
}
//出队操作
public Integer deQueue() {
if(stackB.isEmpty()) {
if(stackA.isEmpty()) {
return null;
}
transfer();
}
return stackB.removeFirst();
}
//栈A元素转移到栈B
private void transfer() {
while(!stackA.isEmpty()) {
stackB.addFirst(stackA.removeFirst());
}
}
public static void main(String[] args) {
QueueUseStack qus = new QueueUseStack();
qus.enQueue(1);
qus.enQueue(2);
qus.enQueue(3);
System.out.println(qus.deQueue());
System.out.println(qus.deQueue());
qus.enQueue(4);
System.out.println(qus.deQueue());
}
}
3.2 (lee-102) 二叉树的层序遍历
给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)
二叉树:[3,9,20,null,null,15,7],
[ [3],
[9,20],
[15,7] ]
class TreeNode{
int val;
TreeNode left;
TreeNode right;
TreeNode(int x){
val = x;
}
}
/**
* 二叉树层次遍历
* @param root
* @return
*/
public List<List<Integer>> levelOrder(TreeNode root){
if(root==null) {
return null;
}
List<List<Integer>> res = new LinkedList<>();
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while(!queue.isEmpty()) { //注意
int size = queue.size();
List<Integer> list = new ArrayList<>();
for(int i = 0;i <size;i++) {
TreeNode node = queue.remove();
list.add(node.val);
if(node.left != null) {
queue.offer(node.left);
}
if(node.right != null) {
queue.offer(node.right);
}
}
res.add(list);
}
return res;
}
3.3 (lee-542) 01矩阵
给定一个由 0 和 1 组成的矩阵,找出每个元素到最近的 0 的距离。两个相邻元素间的距离为 1。
输入:
[[0,0,0],
[0,1,0],
[0,0,0]]
输出:
[[0,0,0],
[0,1,0],
[0,0,0]]
动态规划
public int[][] updateMatrix(int[][] matrix){
int row = matrix.length;
int col = matrix[0].length;
int[][] dist = new int[row][col];
int MAX_TEMP = Integer.MAX_VALUE/2;
// 如果 (i, j) 的元素为 0,那么距离为 0,否则设置成一个很大的数
for(int i = 0;i < row;i++) {
for(int j = 0;j < col;j++) {
if(matrix[i][j]==0) {
dist[i][j] = 0;
}else {
dist[i][j] = MAX_TEMP;
}
}
}
// 水平向左移动 和 竖直向上移动
for(int i =0;i < row;i++) {
for(int j = 0;j < col;j++) {
if(i-1 >= 0) {
dist[i][j] = Math.min(dist[i][j], dist[i-1][j]+1);
}
if(j-1 >= 0) {
dist[i][j] = Math.max(dist[i][j], dist[i][j-1]+1);
}
}
}
// 水平向右移动 和 竖直向下移动
for(int i = row-1;i >= 0;i--) {
for(int j = col-1;j >= 0;j--) {
if(i+1 < row) {
dist[i][j] = Math.min(dist[i][j], dist[i + 1][j] + 1);
}
if(j+1 < col) {
dist[i][j] = Math.min(dist[i][j], dist[i][j+1] + 1);
}
}
}
return dist;
}
4 总结
- 熟悉栈的使用场景
- 后出先出,保存临时值
- 利用栈 DFS 深度搜索
- 熟悉队列的使用场景
- 利用队列 BFS 广度搜索