文章目录
一、概述
在数组中,可以通过索引访问随机元素。但是,在某些情况下,可能需要限制处理的顺序。
这本LeetBook将介绍两种不同的处理顺序,先入先出和后入先出;以及两个相应的线性数据结构:队列和栈。
我们需要:
- 了解FIFO和LIFO处理顺序的原理
- 实现这两个数据结构
- 熟悉内置的队列和栈结构
- 解决基本的队列相关问题,尤其是BFS
- 解决基本的栈相关问题
- 理解在使用DFS和其他递归算法来解决问题时,系统栈是如何发挥作用的
二、队列:先入先出的数据结构
先入先出FIFO
- 理解FIFO和队列的定义
- 能够自己实现队列
- 熟悉内置队列结构
- 使用队列来解决简单问题
1.先入先出的数据结构
在FIFO数据结构中,首先处理添加到队列中的第一个元素。
如上图,队列是典型的FIFO数据结构。插入操作也被称为入队enqueue,新元素始终被添加在队列的末尾。删除操作也被称为出队dequeue。只能移除第一个元素。
实现队列,可以使用动态数组和指向队列头部的索引。
// "static void main" must be defined in a public class.
class MyQueue {
// store elements
private List<Integer> data;
// a pointer to indicate the start position
private int p_start;
public MyQueue() {
data = new ArrayList<Integer>();
p_start = 0;
}
/** Insert an element into the queue. Return true if the operation is successful. */
public boolean enQueue(int x) {
data.add(x);
return true;
};
/** Delete an element from the queue. Return true if the operation is successful. */
public boolean deQueue() {
if (isEmpty() == true) {
return false;
}
p_start++;
return true;
}
/** Get the front item from the queue. */
public int Front() {
return data.get(p_start);
}
/** Checks whether the queue is empty or not. */
public boolean isEmpty() {
return p_start >= data.size();
}
};
public class Main {
public static void main(String[] args) {
MyQueue q = new MyQueue();
q.enQueue(5);
q.enQueue(3);
if (q.isEmpty() == false) {
System.out.println(q.Front());
}
q.deQueue();
if (q.isEmpty() == false) {
System.out.println(q.Front());
}
q.deQueue();
if (q.isEmpty() == false) {
System.out.println(q.Front());
}
}
}
2.设计循环队列
- 使用一维数组保存数据,head为队列头索引,tail为队列尾索引,size是队列长度,也就是数组长度,构造方法初始化队列长度和设置head和tail的默认索引为-1
- 入队第一步检查是否队列已满,然后未满的话就进行入队操作,入队时检查队列是否为空,为空要设置好head = 0标明队列头,不然出队的时候会直接判断为空出不了队
- 出队第一步检查是否队列是空,然后未空的话就进行出队操作,出队前检查队列是不是只剩一个元素,即(head == tail),是的话就初始化头尾索引为-1并return true,不是的话就正常出队
public class MyCircularQueue {
private int[] data;
private int head;
private int tail;
private int size;
/**
* Initialize your data structure here. Set the size of the queue to be k.
*/
public MyCircularQueue(int k) {
data = new int[k];
head = -1;
tail = -1;
size = k;
}
/**
* Insert an element into the circular queue. Return true if the operation is successful.
*/
public boolean enQueue(int value) {
if (isFull()) {// 满了
return false;
}
if (isEmpty()) {// 空的情况,要初始化head为0
head = 0;
}
tail = (tail + 1) % size;
data[tail] = value;
return true;
}
/**
* Delete an element from the circular queue. Return true if the operation is successful.
*/
public boolean deQueue() {
if (isEmpty()) {//空空如也
return false;
}
if (head == tail) {
head = -1;
tail = -1;
return true;
}
head = (head + 1) % size;
return true;
}
/**
* Get the front item from the queue.
*/
public int Front() {
if (isEmpty()) {
return -1;
}
return data[head];
}
/**
* Get the last item from the queue.
*/
public int Rear() {
if (isEmpty()) {
return -1;
}
return data[tail];
}
/**
* Checks whether the circular queue is empty or not.
*/
public boolean isEmpty() {
return head == -1;
}
/**
* Checks whether the circular queue is full or not.
*/
public boolean isFull() {
return ((tail + 1) % size) == head;
}
}
3.队列用法
Java内置队列库,通常用LinkedList
Queue<Integer> q = new LinkedList();
// 2. Get the first element - return null if queue is empty.
System.out.println("The first element is: " + q.peek());
// 3. Push new element.
q.offer(5);
q.offer(13);
q.offer(8);
q.offer(6);
// 4. Pop an element.
q.poll();
// 5. Get the first element.
System.out.println("The first element is: " + q.peek());
// 7. Get the size of the queue.
System.out.println("The size is: " + q.size());
4.数据流中的移动平均值
LeetCode346:
给定一个整数数据流和一个窗口大小,根据该滑动窗口的大小,计算其所有整数的移动平均值。
示例:
MovingAverage m = new MovingAverage(3);
m.next(1) = 1
m.next(10) = (1 + 10) / 2
m.next(3) = (1 + 10 + 3) / 3
m.next(5) = (10 + 3 + 5) / 3
滑动窗口的size就是队列的size(),next整数数据流就是入队操作,也是要注意isFull,isFull的话就先把行头的peek元素减去,然后出队,整数进队。
class MovingAverage {
private int size;
private double sum;
private Queue<Integer> queue;
public MovingAverage(int size) {
this.size = size;
sum = 0;
queue = new LinkedList<Integer>();
}
private double getRes(int num) {
sum += num;
return sum / queue.size();
}
public double next(int num) {
if (queue.size() == size) {
sum -= queue.peek();
queue.poll();
queue.offer(num);
return getRes(num);
}
queue.offer(num);
return getRes(num);
}
}
三、队列和广度优先搜索BFS
广度优先搜索(BFS)是一种遍历或搜索数据结构(如树或图)的算法。
广度优先搜索(BFS)的一个常见应用是找出从根结点到目标结点的最短路径。
BFS 的两个主要方案:遍历或找出最短路径。
模板 I
伪代码:
/**
* Return the length of the shortest path between root and target node.
*/
int BFS(Node root, Node target) {
Queue<Node> queue; // store all nodes which are waiting to be processed
int step = 0; // number of steps neeeded from root to current node
// initialize
add root to queue;
// BFS
while (queue is not empty) {
step = step + 1;
// iterate the nodes which are already in the queue
int size = queue.size();
for (int i = 0; i < size; ++i) {
Node cur = the first node in queue;
return step if cur is target;
for (Node next : the neighbors of cur) {
add next to queue;
}
remove the first node from queue;
}
}
return -1; // there is no path from root to target
}
- 如代码所示,在每一轮中,队列中的结点是等待处理的结点。
- 在每个更外一层的 while 循环之后,我们距离根结点更远一步。变量 step 指示从根结点到我们正在访问的当前结点的距离。
模板 II
有时,确保我们永远不会访问一个结点两次很重要。否则,我们可能陷入无限循环。如果是这样,我们可以在上面的代码中添加一个哈希集来解决这个问题。
/**
* Return the length of the shortest path between root and target node.
*/
int BFS(Node root, Node target) {
Queue<Node> queue; // store all nodes which are waiting to be processed
Set<Node> used; // store all the used nodes
int step = 0; // number of steps neeeded from root to current node
// initialize
add root to queue;
add root to used;
// BFS
while (queue is not empty) {
step = step + 1;
// iterate the nodes which are already in the queue
int size = queue.size();
for (int i = 0; i < size; ++i) {
Node cur = the first node in queue;
return step if cur is target;
for (Node next : the neighbors of cur) {
if (next is not in used) {
add next to queue;
add next to used;
}
}
remove the first node from queue;
}
}
return -1; // there is no path from root to target
}
有两种情况你不需要使用哈希集:
你完全确定没有循环,例如,在树遍历中;
你确实希望多次将结点添加到队列中。
1.墙与门
private static final int INF = Integer.MAX_VALUE;
private static final List<int[]> DIRECTIONS = Arrays.asList(
new int[]{-1, 0},//up
new int[]{1, 0},//down
new int[]{0, -1},//left
new int[]{0, 1}//right
);
public void wallsAndGates(int[][] rooms) {
int m = rooms.length;
int n = rooms[0].length;
Queue<int[]> queue = new LinkedList<>();//存坐标
for (int row = 0; row < m; row++) {
for (int col = 0; col < n; col++) {
if (rooms[row][col] == 0) {
queue.offer(new int[]{row, col});//门
}
}
}
// BFS
while (!queue.isEmpty()) {
int[] point = queue.poll();
int row = point[0];
int col = point[1];
for (int[] dir : DIRECTIONS) {
int r = row + dir[0];
int c = col + dir[1];
if (r < 0 || c < 0 || r >= m || c >= n || rooms[r][c] != INF) {
continue;
}
rooms[r][c] = rooms[row][col] + 1;
queue.offer(new int[]{r, c});
}
}
}
public void wallsAndGates1(int[][] rooms) {
int m = rooms.length;
int n = rooms[0].length;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (rooms[i][j] == 0) {
dfs1(rooms, i, j, 0);
}
}
}
}
private void dfs1(int[][] rooms, int i, int j, int val) {
int m = rooms.length;
int n = rooms[0].length;
if (i < 0 || j < 0 || i >= m || j >= n || rooms[i][j] != INF) {
return;
}
rooms[i][j] = val;
dfs1(rooms, i - 1, j, val + 1);
dfs1(rooms, i + 1, j, val + 1);
dfs1(rooms, i, j - 1, val + 1);
dfs1(rooms, i, j + 1, val + 1);
}
2.岛屿数量
200. 岛屿数量
模版Ⅰ
private static final List<int[]> DIRECTIONS = Arrays.asList(
new int[]{-1, 0},//up
new int[]{1, 0},//down
new int[]{0, -1},//left
new int[]{0, 1}//right
);
public int numIslands(char[][] grid) {
int m = grid.length;
int n = grid[0].length;
int islandsNum = 0;
Queue<int[]> queue = new LinkedList<>();
for (int row = 0; row < m; row++) {
for (int col = 0; col < n; col++) {
if (grid[row][col] == '1') {
islandsNum++;
queue.offer(new int[]{row, col});
// BFS
while (!queue.isEmpty()) {
int[] point = queue.poll();
for (int[] dir : DIRECTIONS) {
int r = point[0] + dir[0];
int c = point[1] + dir[1];
if (r < 0 || c < 0 || r >= m || c >= n || grid[r][c] != '1') {
continue;
}
grid[r][c] = '0';
queue.offer(new int[]{r, c});
}
}
}
}
}
return islandsNum;
}
模版Ⅱ
一开始用的是HashSet,但这里保存的是int[],是引用类型,所以等于没用。
后面想了想决定建个二维数组来保存visited过的坐标。当然,也可以在入队的时候把坐标转换成整数形式入队,这样就能用HashSet了。
public int numIslands(char[][] grid) {
int m = grid.length;
int n = grid[0].length;
int[][] directions = {{-1,0},{1,0},{0,-1},{0,1}};
int islandsNum = 0;
Queue<int[]> queue = new LinkedList<>();
// Set<int[]> seen = new HashSet<>();
boolean[][] visited = new boolean[m][n];
for (int row = 0; row < m; row++) {
for (int col = 0; col < n; col++) {
if (grid[row][col] == '1') {
islandsNum++;
// BFS
queue.offer(new int[]{row, col});
// seen.add(new int[]{row, col});
visited[row][col] = true;
while (!queue.isEmpty()) {
int[] point = queue.poll();
for (int[] dir : directions) {
int r = point[0] + dir[0];
int c = point[1] + dir[1];
if (r < 0 || c < 0 || r >= m || c >= n || grid[r][c] != '1') {
continue;
}
int[] next = {r, c};
// if (!seen.contains(next)) {
if (!visited[r][c]) {
grid[r][c] = '0';
queue.offer(next);
visited[r][c] = true;
// seen.add(next);
}
}
}
}
}
}
return islandsNum;
}
虽然做出来了,但思路是有点乱。看了看大佬的,发现有很规范的写法,这里复制一下。
class Solution {
public int numIslands(char[][] grid) {
int count = 0;
for(int i = 0; i < grid.length; i++) {
for(int j = 0; j < grid[0].length; j++) {
if(grid[i][j] == '1'){
bfs(grid, i, j);
count++;
}
}
}
return count;
}
private void bfs(char[][] grid, int i, int j){
Queue<int[]> list = new LinkedList<>();
list.add(new int[] { i, j });
while(!list.isEmpty()){
int[] cur = list.remove();
i = cur[0]; j = cur[1];
if(0 <= i && i < grid.length && 0 <= j && j < grid[0].length && grid[i][j] == '1') {
grid[i][j] = '0';
list.add(new int[] { i + 1, j });
list.add(new int[] { i - 1, j });
list.add(new int[] { i, j + 1 });
list.add(new int[] { i, j - 1 });
}
}
}
}
作者:jyd
链接:https://leetcode-cn.com/problems/number-of-islands/solution/number-of-islands-shen-du-you-xian-bian-li-dfs-or-/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
3.打开转盘锁
public int openLock(String[] deadends, String target) {
Set<String> dead = new HashSet<>(Arrays.asList(deadends));
Queue<String> queue = new LinkedList<>();
Set<String> seen = new HashSet<>();
queue.offer("0000");
seen.add("0000");
int res = 0;
// BFS
while (!queue.isEmpty()) {
int sz = queue.size();
for (int i = 0; i < sz; i++) {
String cur = queue.poll();
if (dead.contains(cur)) {
continue;
}
if (cur.equals(target)) {
return res;
}
for (int j = 0; j < 4; j++) {
String up = plusOne(cur, j);
if (!seen.contains(up)) {
queue.offer(up);
seen.add(up);
}
String down = minusOne(cur, j);
if (!seen.contains(down)) {
queue.offer(down);
seen.add(down);
}
}
}
res++;
}
return -1;
}
String plusOne(String s, int j) {
char[] ch = s.toCharArray();
if (ch[j] == '9') {
ch[j] = '0';
} else {
ch[j] += 1;
}
return new String(ch);
}
String minusOne(String s, int j) {
char[] ch = s.toCharArray();
if (ch[j] == '0') {
ch[j] = '9';
} else {
ch[j] -= 1;
}
return new String(ch);
}
4.完全平方数
public int numSquares(int n) {
Queue<Integer> queue = new LinkedList<>();
Set<Integer> visited = new HashSet<>();
int res = 1;
queue.offer(n);
while (!queue.isEmpty()) {
int sz = queue.size();
for (int i = 0; i < sz; i++) {
int cur = queue.poll();
for (int j = 0; j * j <= cur; j++) {
int next = cur - j*j;
if (next == 0) {
return res;
}
if (!visited.contains(next)){
queue.offer(next);
visited.add(next);
}
}
}
res++;
}
return 0;
}
5.二叉树的最小深度
public int minDepth(TreeNode root) {
if (root == null) return 0;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
int depth = 1;
while (!queue.isEmpty()) {
int size = queue.size();// 记得在for循环前获取原队列的长度。
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
if (node.left == null && node.right == null) {
return depth;
}
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
depth++;
}
return depth;
}
四、栈:后入先出的数据结构
简单用ArrayList实现:
// "static void main" must be defined in a public class.
class MyStack {
private List<Integer> data; // store elements
public MyStack() {
data = new ArrayList<>();
}
/** Insert an element into the stack. */
public void push(int x) {
data.add(x);
}
/** Checks whether the queue is empty or not. */
public boolean isEmpty() {
return data.isEmpty();
}
/** Get the top item from the queue. */
public int top() {
return data.get(data.size() - 1);
}
/** Delete an element from the queue. Return true if the operation is successful. */
public boolean pop() {
if (isEmpty()) {
return false;
}
data.remove(data.size() - 1);
return true;
}
};
public class Main {
public static void main(String[] args) {
MyStack s = new MyStack();
s.push(1);
s.push(2);
s.push(3);
for (int i = 0; i < 4; ++i) {
if (!s.isEmpty()) {
System.out.println(s.top());
}
System.out.println(s.pop());
}
}
}
内置栈库:
// "static void main" must be defined in a public class.
public class Main {
public static void main(String[] args) {
// 1. Initialize a stack.
Stack<Integer> s = new Stack<>();
// 2. Push new element.
s.push(5);
s.push(13);
s.push(8);
s.push(6);
// 3. Check if stack is empty.
if (s.empty() == true) {
System.out.println("Stack is empty!");
return;
}
// 4. Pop an element.
s.pop();
// 5. Get the top element.
System.out.println("The top element is: " + s.peek());// 8
// 6. Get the size of the stack.
System.out.println("The size is: " + s.size());
}
}
当你想首先处理最后一个元素时,栈将是最合适的数据结构。
1.最小栈
155. 最小栈
把两个次最小值放在最小值下面。
class MinStack1 {
private int min = Integer.MAX_VALUE;
private Stack<Integer> stack;
/**
* initialize your data structure here.
*/
public MinStack1() {
stack = new Stack<>();
}
public void push(int x) {
if (min >= x) {
stack.push(min);
min = x;
}
stack.push(x);
}
public void pop() {
if (stack.pop() == min) {
min = stack.pop();
}
}
public int top() {
return stack.peek();
}
public int getMin() {
return min;
}
}
2.有效的括号
{[()]},后进先出。
public boolean isValid(String s) {
if (s.length() == 0) {
return true;
}
Stack<Character> stack = new Stack<>();
for (char c : s.toCharArray()) {
if (c == '(') {
stack.push(')');
} else if (c == '{') {
stack.push('}');
} else if (c == '[') {
stack.push(']');
} else if (stack.empty() || c != stack.pop()) {
return false;
}
}
return stack.empty();
}
3.每日温度
739. 每日温度
这里提供两种方法,第一种是典型的j = i + 1指针加条件判断,思路比较清晰,也不容易犯错。
第2种是利用栈,栈存放的是索引。
temperatures = [73, 74, 75, 71, 69, 72, 76, 73],
你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。
public int[] dailyTemperatures(int[] T) {
Stack<Integer> stack = new Stack<>();
int[] res = new int[T.length];
for (int i = 0; i < T.length; i++) {
while (!stack.empty() && T[i] > T[stack.peek()]) {
int temp = stack.pop();
res[temp] = i - temp;
}
stack.push(i);
}
return res;
// int[] res = new int[T.length];
// for (int i = 0; i < T.length; i++) {
// for (int j = i + 1; j < T.length; j++) {
// if (T[j] > T[i]) {
// res[i] = j - i;
// break;
// }
// }
// }
// return res;
}
4.逆波兰表达式求值
150. 逆波兰表达式求值
做了好几道题,应该感受到栈在获取最后一个元素的强大之处了。
public static int evalRPN(String[] tokens) {
Stack<Integer> stack = new Stack<>();
Integer a, b;
for (String s : tokens) {
switch (s) {
case "+":
b = stack.pop();
a = stack.pop();
stack.push(a + b);
break;
case "-":
b = stack.pop();
a = stack.pop();
stack.push(a - b);
break;
case "*":
b = stack.pop();
a = stack.pop();
stack.push(a * b);
break;
case "/":
b = stack.pop();
a = stack.pop();
stack.push(a / b);
break;
default:
stack.push(Integer.parseInt(s));
break;
}
}
return stack.pop();
}
五、栈和深度优先搜索DFS
模版Ⅰ
/*
* Return true if there is a path from cur to target.
*/
boolean DFS(Node cur, Node target, Set<Node> visited) {
return true if cur is target;
for (next : each neighbor of cur) {
if (next is not in visited) {
add next to visted;
return true if DFS(next, target, visited) == true;
}
}
return false;
}
当我们递归地实现 DFS 时,似乎不需要使用任何栈。但实际上,我们使用的是由系统提供的隐式栈,也称为调用栈(Call Stack)。
每个元素都需要固定的空间。栈的大小正好是 DFS 的深度。因此,在最坏的情况下,维护系统栈需要 O(h),其中 h 是 DFS 的最大深度。在计算空间复杂度时,永远不要忘记考虑系统栈。
1.岛屿数量
200.岛屿数量
发现用dfs写简单好多啊。
class Solution {
void dfs(char[][] grid, int r, int c) {
int m = grid.length;
int n = grid[0].length;
if (r < 0 || c < 0 || r >= m || c >= n || grid[r][c] == '0') {
return;
}
grid[r][c] = '0';
dfs(grid, r - 1, c);
dfs(grid, r + 1, c);
dfs(grid, r, c - 1);
dfs(grid, r, c + 1);
}
public int numIslands(char[][] grid) {
if (grid == null || grid.length == 0)
return 0;
int m = grid.length;
int n = grid[0].length;
int islandsNum = 0;
for (int r = 0; r < m; r++) {
for (int c = 0; c < n; c++) {
if (grid[r][c] == '1') {
islandsNum++;
dfs(grid, r, c);
}
}
}
return islandsNum;
}
}
2.克隆图
133.克隆图
这是我们第二次接触深拷贝(克隆)了对吗?跳到4.复制带随机指针的链表
我们上一次复制链表的时候,法1是直接在原链表进行复制结点(自己new一个val值相同的结点),然后我们再想办法复制它的next和random属性,最后把新链表拆出来,复原原链表就成功了。法二是创建了一个哈希表,再遍历原链表,遍历的同时再不断创建新节点(原节点作为key,新节点作为value), 设置完两个属性后直接提取新链表,用map.get方法。
所以我们这次也来用HashMap并结合dfs来做。
为了避免在深拷贝时陷入死循环,我们需要理解图的结构。对于一张无向图,任何给定的无向边都可以表示为两个有向边,即如果节点 A 和节点 B 之间存在无向边,则表示该图具有从节点 A 到节点 B 的有向边和从节点 B 到节点 A 的有向边。
public Node cloneGraph(Node node) {
Map<Node, Node> visited = new HashMap<>();
return dfs(node, visited);
}
private Node dfs(Node node, Map<Node, Node> visited) {
if (node == null)
return null;
if (visited.containsKey(node))
return visited.get(node);
Node clone = new Node(node.val);
visited.put(node, clone);
for (Node neighbor : node.neighbors) {
clone.neighbors.add(dfs(neighbor, visited));
}
return clone;
}
3.目标和
494. 目标和
法一:递归地枚举所有情况:
class Solution {
int count = 0;
public int findTargetSumWays(int[] nums, int S) {
calculate(nums, 0, 0, S);
return count;
}
public void calculate(int[] nums, int i, int sum, int S) {
if (i == nums.length) {
if (sum == S)
count++;
return;
}
calculate(nums, i + 1, sum + nums[i], S);
calculate(nums, i + 1, sum - nums[i], S);
}
}
法二:动态规划
模版Ⅱ
递归解决方案的优点是它更容易实现。 但是,存在一个很大的缺点:如果递归的深度太高,你将遭受堆栈溢出。 在这种情况下,您可能会希望使用 BFS,或使用显式栈实现 DFS。
这里我们提供了一个使用显式栈的模板:
/*
* Return true if there is a path from cur to target.
*/
boolean DFS(int root, int target) {
Set<Node> visited;
Stack<Node> s;
add root to s;
while (s is not empty) {
Node cur = the top element in s;
return true if cur is target;
for (Node next : the neighbors of cur) {
if (next is not in visited) {
add next to s;
add next to visited;
}
}
remove cur from s;
}
return false;
}
4.二叉树的中序遍历
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
while (root != null || !stack.empty()) {
while (root != null) {
stack.push(root);
root = root.left;
}
root = stack.pop();
list.add(root.val);
root = root.right;
}
return list;
}
六、小结
1.用栈实现队列
232.用栈实现队列
用了官方文档推荐的Deque,每次push都是通过addLast推进栈1,peek方法是如果栈2为空,把栈1的所有元素都出栈然后进入栈2,这样栈2的Last位置就还是队列的头。弹出的话先peek,以防栈2空了。
Stack,ArrayDeque,LinkedList的区别
class MyQueue {
Deque<Integer> stack1;
Deque<Integer> stack2;
/** Initialize your data structure here. */
public MyQueue() {
stack1 = new LinkedList<>();
stack2 = new LinkedList<>();
}
/** Push element x to the back of queue. */
public void push(int x) {
stack1.addLast(x);
}
/** Removes the element from in front of queue and returns that element. */
public int pop() {
peek();
return stack2.removeLast();
}
/** Get the front element. */
public int peek() {
if (stack2.isEmpty()){
while (!stack1.isEmpty()){
stack2.addLast(stack1.removeLast());
}
}
return stack2.peekLast();
}
/** Returns whether the queue is empty. */
public boolean empty() {
return stack1.isEmpty() && stack2.isEmpty();
}
}
2.用队列实现栈
class MyStack {
private Queue<Integer> queue1;
private Queue<Integer> queue2;
/**
* Initialize your data structure here.
*/
public MyStack() {
queue1 = new LinkedList<>();
queue2 = new LinkedList<>();
}
/**
* Push element x onto stack.
*/
public void push(int x) {
queue1.offer(x);
while (!queue2.isEmpty()) {
queue1.offer(queue2.poll());
}
// 交换队列1和队列2,保证1都是空的,故每次push x始终在1的队列头
// 队列2就始终保持先进后出的性质
Queue temp = queue1;
queue1 = queue2;
queue2 = temp;
}
/**
* Removes the element on top of the stack and returns that element.
*/
public int pop() {
return queue2.poll();
}
/**
* Get the top element.
*/
public int top() {
return queue2.peek();
}
/**
* Returns whether the stack is empty.
*/
public boolean empty() {
return queue2.isEmpty();
}
}
3.字符串解码
输入:s = “3[a2[c]]”
输出:“accaccacc”
还记得<有效的括号>那题吗,涉及到这种括号题很多都是用栈来做,套路就是创建stack来保存结果,然后在把字符串变成字符数组再for each循环,循环体里就是根据题目要求来设置if else if等判断语句。
上次只有括号信息要保存,所以只用了一个栈,这道题明显要保存的有数字和字符串,所以得用两个栈(整型栈和字符串栈)。其实,这种题就列出字符c有几种输入的类型,然后分别做对应的处理就行了。
- 数字,用multi来临时保存,要注意数字可能是多位数,所以得做处理。char类型转换成int型有一个窍门就是:
c - '0'
,当然如果你没想到的话,用Integer.parseInt(c+"")也行 - 英文字符,用sb对象res的append方法来拼接保存
- [ 遇到左括号,就先保存好我们上次的multi和res变量,通过入对应的栈,然后将它们格式化,继续保存后续的字符
- ] 遇到右括号,说明要把上次保存(最近一次)的结果整合好存进res里,通过一个sb对象tmp来保存这一次的整合结果,然后拼接在上一次的整合结果里(即拼接在字符串栈removeLast后)。
public String decodeString(String s) {
StringBuilder res = new StringBuilder();
int multi = 0;
Deque<Integer> stack_multi = new LinkedList<>();
Deque<String> stack_res = new LinkedList<>();
for (Character c : s.toCharArray()) {
if (c == '[') {
stack_multi.addLast(multi);
stack_res.addLast(res.toString());
multi = 0;
res = new StringBuilder();
} else if (c == ']') {
StringBuilder tmp = new StringBuilder();
int cur_multi = stack_multi.removeLast();
for (int i = 0; i < cur_multi; i++)
tmp.append(res);
res = new StringBuilder(stack_res.removeLast() + tmp);
} else if (c >= '0' && c <= '9')
multi = multi * 10 + c - '0';
else res.append(c);
}
return res.toString();
}
4.图像渲染
733.图像渲染
这道题以前做的岛屿数量很像,看该题对应的网站名,后缀是flood-fill,查了一下,这个叫泛洪算法。
泛洪填充算三个参数:起始节点(start node),目标颜色(target color)和替换颜色(replacement color)。 该算法查找阵列中通过目标颜色的路径连接到起始节点的所有节点,并将它们更改为替换颜色。 可以通过多种方式构建泛洪填充算法,但它们都明确地或隐式地使用队列或堆栈数据结构。
输入:
image = [[1,1,1],[1,1,0],[1,0,1]]
sr = 1, sc = 1, newColor = 2
输出: [[2,2,2],[2,2,0],[2,0,1]]
岛屿题可能会有多个start node,target color应该是1吧,然后replacement color是0。
这道题,star node是(1,1),tatget color是1,replacement color是2。
DFS做一做看看。
// DFS
public int[][] floodFill(int[][] image, int sr, int sc, int newColor) {
int oldColor = image[sr][sc];
if (oldColor != newColor) {
dfs(image, sr, sc, newColor, oldColor);
}
return image;
}
void dfs(int[][] image, int sr, int sc, int newColor, int oldColor) {
if (sr < 0 || sc < 0 || sr >= image.length || sc >= image[0].length || image[sr][sc] != oldColor) {
return;
}
image[sr][sc] = newColor;
dfs(image, sr - 1, sc, newColor, oldColor);
dfs(image, sr + 1, sc, newColor, oldColor);
dfs(image, sr, sc - 1, newColor, oldColor);
dfs(image, sr, sc + 1, newColor, oldColor);
}
下面是官方题解:
class Solution {
int[] dx = {1, 0, 0, -1};
int[] dy = {0, 1, -1, 0};
public int[][] floodFill(int[][] image, int sr, int sc, int newColor) {
int currColor = image[sr][sc];
if (currColor != newColor) {
dfs(image, sr, sc, currColor, newColor);
}
return image;
}
public void dfs(int[][] image, int x, int y, int color, int newColor) {
if (image[x][y] == color) {
image[x][y] = newColor;
for (int i = 0; i < 4; i++) {
int mx = x + dx[i], my = y + dy[i];
if (mx >= 0 && mx < image.length && my >= 0 && my < image[0].length) {
dfs(image, mx, my, color, newColor);
}
}
}
}
}
我也想到用for循环,但我想的是这样:
void dfs(int[][] image, int sr, int sc, int newColor, int oldColor) {
if (sr >= 0 && sc >= 0 && sr < image.length && sc < image[0].length && image[sr][sc] == oldColor) {
image[sr][sc] = newColor;
for (int i = 0; i < 4; i++) {
int mx = sr + dx[i], my = sc + dy[i];
dfs(image, mx, my, newColor, oldColor);
}
}
}
其实都差不多,不过我建议还是新手还是别用for循环,容易出错。
再练练BFS吧:
public int[][] floodFill(int[][] image, int sr, int sc, int newColor) {
int oldColor = image[sr][sc];
if (oldColor != newColor) {
// dfs(image, sr, sc, newColor, oldColor);
bfs(image, sr, sc, newColor, oldColor);
}
return image;
}
void bfs(int[][] image, int sr, int sc, int newColor, int oldColor) {
Queue<int[]> queue = new LinkedList<>();
queue.offer(new int[]{sr, sc});
while (!queue.isEmpty()) {
int[] cur = queue.poll();
sr = cur[0];
sc = cur[1];
if (sr < 0 || sc < 0 || sr >= image.length || sc >= image[0].length || image[sr][sc] != oldColor) {
continue;
}
image[sr][sc] = newColor;
queue.offer(new int[]{sr - 1, sc});
queue.offer(new int[]{sr + 1, sc});
queue.offer(new int[]{sr, sc - 1});
queue.offer(new int[]{sr, sc + 1});
}
}
当然,if continue看着不习惯也可以换掉:
if (sr >= 0 && sc >= 0 && sr < image.length && sc < image[0].length && image[sr][sc] == oldColor) {
image[sr][sc] = newColor;
queue.offer(new int[]{sr - 1, sc});
queue.offer(new int[]{sr + 1, sc});
queue.offer(new int[]{sr, sc - 1});
queue.offer(new int[]{sr, sc + 1});
}
这样也是可以的。
这里提一嘴offer和add的区别,offer:Inserts the specified element into this queue if it is possible to do so immediately without violating capacity restrictions. When using a capacity-restricted queue, this method is generally preferable to add, which can fail to insert an element only by throwing an exception.所以我们还是用offer好点。
5. 01矩阵
542.01矩阵
这道题跟286.墙与门相似,都是求距离,不过显然更简单。0是门,都是计算跟0的距离。不过286题多了一个-1墙,遇到-1就穿不过去。来了来了,既然我们上一题已经接触了flood-fill算法,来套一套:
起始结点(一般都是起点坐标):起始坐标,开始是0的坐标
目标颜色(要搜索坐标的初始值):1
替换颜色(要搜索坐标的终值):目标坐标到最近0的距离
我发现啊,这套教程,在最后的小结一般都会出现跟以前相似但更简单的题目,可能是为了加强大家的自信心吧hhh。
第一次尝试:
public int[][] updateMatrix(int[][] matrix) {
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[0].length; j++) {
if (matrix[i][j] == 0) {
bfs(matrix, i, j);
}
}
}
return matrix;
}
private void bfs(int[][] matrix, int i, int j) {
Queue<int[]> queue = new LinkedList<>();
queue.offer(new int[]{i, j});
while (!queue.isEmpty()) {
int[] cur = queue.poll();
int r = cur[0];
int c = cur[1];
if (r >= 0 && r < matrix.length && c >= 0 && c < matrix[0].length && matrix[r][c] == 1) {
matrix[r][c] = matrix[i][j] + 1;
queue.offer(new int[]{r - 1, c});
queue.offer(new int[]{r + 1, c});
queue.offer(new int[]{r, c - 1});
queue.offer(new int[]{r, c + 1});
}
}
}
当然,我这样做是错的,因为在图像渲染那题,第一个传进bfs的坐标对应的颜色已经是目标颜色了,但这里不是。那意味着,我们最好在if前就把坐标移动变换先做了。而且,得先把所有0弹进栈里。这里我借鉴了墙与门的代码思路:
第二次尝试:
int[] dx = {-1, 1, 0, 0};
int[] dy = {0, 0, -1, 1};
private static final int INF = Integer.MAX_VALUE;
public int[][] updateMatrix(int[][] matrix) {
Queue<int[]> queue = new LinkedList<>();
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[0].length; j++) {
if (matrix[i][j] == 0) {
queue.offer(new int[]{i, j});
// bfs(matrix, i, j);
}
if (matrix[i][j] == 1) {
matrix[i][j] = INF;
}
}
}
bfs(matrix, queue);
return matrix;
}
private void bfs(int[][] matrix, Queue<int[]> queue) {
while (!queue.isEmpty()) {
int[] cur = queue.poll();
int r = cur[0];
int c = cur[1];
for (int k = 0; k < 4; k++) {
int mr = r + dx[k];
int mc = c + dy[k];
if (mr >= 0 && mr < matrix.length && mc >= 0 && mc < matrix[0].length && matrix[mr][mc] == INF) {
matrix[mr][mc] = matrix[r][c] + 1;
queue.offer(new int[]{mr, mc});
}
}
}
}
这次是成功了。这道题的麻烦之处在于,原本的1会影响到后续的加操作,所以这里模仿墙与门先把所有的1都设置成INF,这样就不会让加过的1重新再加了。
但这样显然是不够好的,可读性也不高。思考一下还有其他办法能做吗?当然了,为了不走回头路,DFS经常都会搭配一个哈希集seen来验证。但是这里的坐标可是引用对象,所以不能用哈希集,用boolean[][]
吧。然后我还做了一些优化,比如变量名称尽量规范易懂,还有不用特意定义然后调用bfs函数,感觉多此一举而且容易出错。最后考虑到不污染matrix数组,新建了res数组来保存结果。
class Solution {
int[] dx = {-1, 1, 0, 0};
int[] dy = {0, 0, -1, 1};
// private static final int INF = Integer.MAX_VALUE;
public int[][] updateMatrix(int[][] matrix) {
Queue<int[]> queue = new LinkedList<>();
boolean[][] seen = new boolean[matrix.length][matrix[0].length];
int[][] res = new int[matrix.length][matrix[0].length];
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[0].length; j++) {
if (matrix[i][j] == 0) {
queue.offer(new int[]{i, j});
seen[i][j] = true;
}
}
}
while (!queue.isEmpty()) {
int[] cur = queue.poll();
int x = cur[0];
int y = cur[1];
for (int k = 0; k < 4; k++) {
int mx = x + dx[k];
int my = y + dy[k];
if (mx >= 0 && mx < matrix.length && my >= 0 && my < matrix[0].length && !seen[mx][my]) {
res[mx][my] = res[x][y] + 1;
queue.offer(new int[]{mx, my});
seen[mx][my] = true;
}
}
}
return res;
}
}
我们只要努力,都能一步一步变好的,对不对?
6.钥匙和房间
841.钥匙和房间
法一:BFS
public boolean canVisitAllRooms(List<List<Integer>> rooms) {
Queue<Integer> queue = new LinkedList<>();
Set<Integer> visited = new HashSet<>();
queue.offer(0);
visited.add(0);
while (!queue.isEmpty()) {
int cur = queue.poll();
for (int key : rooms.get(cur)) {
if (!visited.contains(key)) {
queue.offer(key);
visited.add(key);
}
}
}
return visited.size() == rooms.size();
}
法二:DFS
// DFS
public boolean canVisitAllRooms1(List<List<Integer>> rooms) {
Set<Integer> visited = new HashSet<>();
visited.add(0);
dfs(rooms, 0,visited);
return visited.size() == rooms.size();
}
private void dfs(List<List<Integer>> rooms, int n, Set<Integer> visited) {
for (int key : rooms.get(n)) {
if (!visited.contains(key)){
visited.add(key);
dfs(rooms,key,visited);
}
}
}
终于搞完了,四大线性数据结构(数组、链表、栈、队列)暂时先告一段落了,接下来需要快速学习非线性数据结构,也是四个:树、堆、散列表、图。先学树吧!
我大概已经把chandos唱片公司的梅特纳钢琴作品全集听了个遍,我在思考我最近沉迷他的原因。大家都说他的音乐里有德意志的根源和俄罗斯的精神,这些都对。最让我着迷的是他的很多音乐里有一种恐怖惊慌的感觉却又带有美丽诡异的旋律,飘动的音符在坚韧的信念与个人无奈的沉思间徘徊,神话般的音乐元素绽放着一种孩童的稚嫩与天真。真的很耐听,这里推荐他的成名作第一钢协和他最著名的第五钢琴奏鸣曲。
Lucas Debargue plays Medtner’s sonata in g minor, op.22, 2019.05.27, Moscow
http://music.163.com/song?id=546403285&userid=88476473