栈(stack)又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。
特点:先进后出,后进先出。(浏览器的前进后退就是基于栈实现的)
栈 - 图1

栈实现

核心方法

  • push进栈
  • pop出栈
  • peek取栈顶元素

栈 - 图2

数组实现栈

  1. public class YKDStack {
  2. String[] data;
  3. int size;
  4. // 栈顶位置
  5. int top = -1;
  6. // 初始化栈,默认大小为20
  7. public YKDStack() {
  8. this.initData(20);
  9. }
  10. // 初始化栈,并传入栈空间大小
  11. public YKDStack(int size) {
  12. this.initData(size);
  13. }
  14. // 初始化数组
  15. private void initData(int size) {
  16. this.size = size;
  17. this.data = new String[this.size];
  18. }
  19. // 添加元素
  20. public boolean push(String value) {
  21. // 数组越界了
  22. if (this.top >= this.size - 1) {
  23. return false;
  24. }
  25. // top 栈顶 +1
  26. this.top++;
  27. // 设置栈顶元素
  28. this.data[this.top] = value;
  29. return true;
  30. }
  31. // 弹出栈顶元素
  32. public String pop() {
  33. if (this.top == -1) {
  34. return null;
  35. }
  36. String item = this.data[this.top];
  37. this.top--;
  38. return item;
  39. }
  40. // 获取栈顶元素
  41. public String peek() {
  42. if (this.top == -1) {
  43. return null;
  44. }
  45. return this.data[this.top];
  46. }
  47. public static void main(String[] args) {
  48. YKDStack stack = new YKDStack();
  49. System.out.println(stack.peek());
  50. System.out.println(stack.pop());
  51. stack.push("hello");
  52. System.out.println(stack.peek());
  53. stack.push("you");
  54. System.out.println(stack.pop());
  55. System.out.println(stack.peek());
  56. }
  57. }

链表实现栈

  1. //节点
  2. public class Node {
  3. private String content;
  4. private Node next;
  5. public Node() {
  6. }
  7. public Node(String content) {
  8. this.content = content;
  9. }
  10. public Node(String content, Node next) {
  11. this.content = content;
  12. this.next = next;
  13. }
  14. //getter and setter
  15. }
  1. public class YKDStack {
  2. Node header;
  3. public YKDStack() {
  4. }
  5. // 添加元素
  6. public boolean push(String value) {
  7. if (this.header == null) {
  8. // 当前为空的时候,添加header
  9. this.header = new Node(value);
  10. } else {
  11. // 当前不为空的时候,构建一个新的头部节点
  12. this.header = new Node(value, this.header);
  13. }
  14. return true;
  15. }
  16. // 弹出栈顶元素
  17. public String pop() {
  18. if (this.header == null) {
  19. return null;
  20. }
  21. // 删除链表头部节点
  22. Node node = this.header;
  23. this.header = node.getNext();
  24. node.setNext(null);
  25. return node.getContent();
  26. }
  27. // 获取栈顶元素
  28. public String peek() {
  29. if(this.header == null){
  30. return null;
  31. }
  32. return this.header.getContent();
  33. }
  34. public static void main(String[] args) {
  35. YKDStack stack = new YKDStack();
  36. System.out.println(stack.peek());
  37. System.out.println(stack.pop());
  38. stack.push("hello");
  39. System.out.println(stack.peek());
  40. stack.push("you");
  41. System.out.println(stack.pop());
  42. System.out.println(stack.peek());
  43. }
  44. }

常见算法题

括号匹配

栈的实现代码这里省略。

  1. package algorithm;
  2. import java.util.Stack;
  3. /**
  4. * @ClassName Test
  5. * @Author 刘正星
  6. * @Date 2021/6/18 下午8:44
  7. * @Description
  8. */
  9. public class Test {
  10. static boolean match(String input){
  11. Stack<Character> strings = new Stack<>();
  12. for (int i = 0; i < input.length(); i++) {
  13. char c = input.charAt(i);
  14. if (c=='('||c=='{'||c=='['){
  15. strings.push(c);
  16. // continue;
  17. }
  18. if (c==')'){
  19. if (strings.peek()!=null&&strings.peek()=='('){
  20. strings.pop();
  21. }else {
  22. return false;
  23. }
  24. }
  25. if (c=='}') {
  26. if (strings.peek() != null && strings.peek() == '{') {
  27. strings.pop();
  28. }else {
  29. return false;
  30. }
  31. }
  32. if (c==']'){
  33. if (strings.peek()!=null&&strings.peek()=='['){
  34. strings.pop();
  35. }else {
  36. return false;
  37. }
  38. }
  39. }
  40. return true;
  41. }
  42. public static void main(String [] args){
  43. System.out.println(match("[{{(())}}]"));
  44. System.out.println(match("[{{(()}}"));
  45. }
  46. }

标准答案

  1. public class Demo {
  2. // 判断代码括号是否匹配
  3. public static boolean match(String code) {
  4. YKDStack<Character> stack = new YKDStack<>();
  5. for (int i = 0; i < code.length(); i++) {
  6. char c = code.charAt(i);
  7. if (c == '(' || c == '{' || c == '[') {
  8. stack.push(c);
  9. continue;
  10. }
  11. if (c == ')') {
  12. if (stack.peek() != null && stack.peek() == '(') {
  13. stack.pop();
  14. } else {
  15. return false;
  16. }
  17. }
  18. if (c == '}') {
  19. if (stack.peek() != null && stack.peek() == '{') {
  20. stack.pop();
  21. } else {
  22. return false;
  23. }
  24. }
  25. if (c == ']') {
  26. if (stack.peek() != null && stack.peek() == '[') {
  27. stack.pop();
  28. } else {
  29. return false;
  30. }
  31. }
  32. }
  33. return true;
  34. }
  35. public static void main(String[] args) {
  36. boolean result = match("()({()})");
  37. System.out.println("()({()}): " + result);
  38. result = match("{((}))");
  39. System.out.println("{((})): " + result);
  40. }
  41. }

行编辑器

image.png
标准答案

  1. public class Demo {
  2. // 行编辑器的输入
  3. public static String input(String source) {
  4. YKDStack<Character> stack = new YKDStack<>();
  5. // 遍历每个字符
  6. for (int i = 0; i < source.length(); i++) {
  7. char c = source.charAt(i);
  8. // 遇到 # 则 pop
  9. if (c == '#') {
  10. if (stack.peek() != null) {
  11. stack.pop();
  12. }
  13. continue;
  14. }
  15. // 遇到 @ 则清空,相当于重新 new YKDStack()。
  16. if (c == '@') {
  17. stack = new YKDStack<>();
  18. continue;
  19. }
  20. stack.push(c);
  21. }
  22. // 将栈中元素依次pop出来,拼接在字符串前面
  23. String result = "";
  24. while(stack.peek() != null){
  25. result = stack.pop() + result;
  26. }
  27. return result;
  28. }
  29. public static void main(String[] args) {
  30. System.out.println(input("abc#dd"));
  31. System.out.println(input("abc@dd"));
  32. System.out.println(input("a#b#d#ce"));
  33. System.out.println(input("ab##dce"));
  34. }
  35. }
  1. public class Demo {
  2. // 行编辑器的输入
  3. public static String input(String source) {
  4. YKDStack<String> stack = new YKDStack();
  5. String [] strs = source.split("");
  6. for (String str:strs ){
  7. if (str.equals("#")){
  8. stack.pop();
  9. continue;
  10. }
  11. if(str.equals("@")){
  12. while(stack.peek()!=null){
  13. stack.pop();
  14. }
  15. continue;
  16. }
  17. stack.push(str);
  18. }
  19. String back = "";
  20. while(stack.peek()!=null){
  21. back=stack.pop()+back;
  22. }
  23. return back;
  24. }
  25. public static void main(String[] args) {
  26. System.out.println(input("abc#dd"));
  27. System.out.println(input("abc@dd"));
  28. System.out.println(input("a#b#d#ce"));
  29. System.out.println(input("ab##dce"));
  30. }
  31. }

简单寻路算法

很多游戏里面都有自动寻路的场景。
栈 - 图4

自动寻路

地图简化三要素:可移动区域,不可以动区域,起点和终点
把地图抽象成二维方格。
栈 - 图5
栈 - 图6
#表示不可移动区域,空格表示可移动区域,*表示移动路径。

思路

  1. 如何存储整个地图信息,包括障碍物,可移动区域。
  2. 如何存储寻找自动寻路路径。

    地图信息

    可以用二维数组存储。
    栈 - 图7
    1. char[][] map = {
    2. {'#', '#', '#', '#', '#', '#', '#', '#', '#', '#'},
    3. {'#', ' ', ' ', '#', ' ', ' ', ' ', '#', ' ', '#'},
    4. {'#', ' ', ' ', '#', ' ', ' ', ' ', '#', ' ', '#'},
    5. {'#', ' ', ' ', ' ', ' ', '#', '#', ' ', ' ', '#'},
    6. {'#', ' ', '#', '#', '#', ' ', ' ', ' ', ' ', '#'},
    7. {'#', ' ', ' ', ' ', '#', ' ', ' ', ' ', ' ', '#'},
    8. {'#', ' ', '#', ' ', ' ', ' ', '#', ' ', ' ', '#'},
    9. {'#', ' ', '#', '#', '#', ' ', '#', '#', ' ', '#'},
    10. {'#', '#', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '#'},
    11. {'#', '#', '#', '#', '#', '#', '#', '#', '#', '#'}
    12. };
    获取第三行第四列信息。 ```java int row = 2; int column = 3;

// 第 3 行,第 4 列信息 char content = map[row][column]

  1. 整个游戏用`Game`类模拟,一个`map`属性保存地图信息。<br />`Point`类来存储位置信息。两个属性`row``column`
  2. <a name="IEWM7"></a>
  3. #### 方向
  4. 我们站在某一个方格,都有四个方向可以进行探索:上下左右<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/316618/1598949873675-8ce98895-a745-4583-8012-5c84dfe6a1d5.png#height=133&id=GfaKu&margin=%5Bobject%20Object%5D&name=image.png&originHeight=133&originWidth=643&originalType=binary&ratio=1&size=17266&status=done&style=none&width=643)<br />用向量去模拟方向。<br />![](https://cdn.nlark.com/yuque/0/2020/svg/316618/1598949892861-1a47628f-fd53-4bd3-9414-a98a424fead1.svg#height=321&id=nht0w&originHeight=321&originWidth=416&originalType=binary&ratio=1&size=0&status=done&style=none&width=416)<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/316618/1598949934791-74a2f704-a928-40a2-863f-cf6debdb633a.png#height=193&id=ComAN&margin=%5Bobject%20Object%5D&name=image.png&originHeight=193&originWidth=635&originalType=binary&ratio=1&size=19249&status=done&style=none&width=635)
  5. ```java
  6. // 存储4个方向
  7. private int[][] directions = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};

利用栈去储存路径,还要方便回退。

怎么标记方格是否访问过,方格是否是死路?

用@标记方格为死路
新建一个Title对象,表示在栈中的信息,包括方格位置,和遍历的方向。

  1. public class Tile {
  2. // 方格位置
  3. private Point point;
  4. // 方格当前遍历的方向
  5. private int direction = 0;
  6. }

实战过程

第一步

站在起点,起点位置{1,1} 加入到step中,并将元素方向设置为-1,表示还未探索过。
栈 - 图8

第二步

  • 获取栈顶元素,也就是{1,1}方格,将方格方向+1,也就是第一个的探索方向右
  • 探索右侧方格{1,2},加入到steps 栈中,方向设置为-1。


栈 - 图9

第三步

  • 获取栈顶元素,也就是{1,2}方格,将方格方向+1,
  • 但是这里遇到#也就是障碍物,则调整方向继续+1,向下探索,
  • 探索下册方格{2,2}加入steps中,方向设置为-1。

栈 - 图10
以此类推,可以达到以下情况。

栈 - 图11
方格{1,4}是死路,上下左右方向都不能继续探索了,我们将该方格设置为@,并弹出pop()方格。
继续观察方格{1,5}也是死路,同样上述操作。
直到如下场景:
栈 - 图12
获取栈顶元素{3,2},继续探索。
最终可以找到终点
栈 - 图13

路径回溯

栈中储存的就是起点到终点的路径,依次pop栈顶元素,然后倒叙即可。

完整代码

Game

  1. import java.util.ArrayList;
  2. public class Game {
  3. private char[][] map;
  4. // 存储探索的路径节点
  5. private YKDStack<Tile> steps = new YKDStack<>();
  6. // 存储4个方向
  7. private int[][] directions = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
  8. public Game(char[][] map) {
  9. this.map = map;
  10. }
  11. // 寻找路径
  12. public ArrayList<Point> findPath(Point start, Point end) {
  13. ArrayList<Point> path = new ArrayList<>();
  14. // 每次寻路之前将栈清空
  15. this.steps = new YKDStack<>();
  16. // 将起点添加到栈中,并将其标记为已访问
  17. this.steps.push(new Tile(start, -1));
  18. this.map[start.getRow()][start.getColumn()] = '*';
  19. // 循环到栈中元素结束,则表示从起点开始未能找到结束点
  20. while (this.steps.peek() != null) {
  21. // 获取栈顶元素
  22. Tile tile = this.steps.peek();
  23. Point tilePoint = tile.getPoint();
  24. // 判断是不是结束节点
  25. if (this.isSame(tilePoint, end)) {
  26. break;
  27. }
  28. // 修改栈顶元素的方向,准备进行探索
  29. tile.setDirection(tile.getDirection() + 1);
  30. // 当方向为 4 的时候,表示所有方向都已经遍历过了,所以标记为死路@,并弹出栈顶元素
  31. if (tile.getDirection() == 4) {
  32. this.map[tilePoint.getRow()][tilePoint.getColumn()] = '@';
  33. this.steps.pop();
  34. } else {
  35. // 否则探索目标方向的方格
  36. int[] direction = directions[tile.getDirection()];
  37. // 根据方向向量获取新方格的位置信息
  38. Point newPoint = new Point(tilePoint.getRow() + direction[0],
  39. tilePoint.getColumn() + direction[1]);
  40. // 如果可以通行,则将新探索的方格添加到栈中,并标记为已访问
  41. if (this.canStep(newPoint)) {
  42. Tile newTile = new Tile(newPoint, -1);
  43. this.map[newPoint.getRow()][newPoint.getColumn()] = '*';
  44. this.steps.push(newTile);
  45. }
  46. // 如果不可以通行,则不处理,再次获取栈顶元素,改变方向。
  47. }
  48. }
  49. // 从栈中以此回退方格信息,也就是整个访问路径信息
  50. while (this.steps.peek() != null) {
  51. path.add(0, this.steps.pop().getPoint());
  52. }
  53. return path;
  54. }
  55. private boolean isSame(Point point1, Point point2) {
  56. return point1.getRow() == point2.getRow() && point1.getColumn() == point2.getColumn();
  57. }
  58. private boolean canStep(Point point) {
  59. return this.map[point.getRow()][point.getColumn()] == ' ';
  60. }
  61. public static void main(String[] args) {
  62. char[][] map = {
  63. {'#', '#', '#', '#', '#', '#', '#', '#', '#', '#'},
  64. {'#', ' ', ' ', '#', ' ', ' ', ' ', '#', ' ', '#'},
  65. {'#', ' ', ' ', '#', ' ', ' ', ' ', '#', ' ', '#'},
  66. {'#', ' ', ' ', ' ', ' ', '#', '#', ' ', ' ', '#'},
  67. {'#', ' ', '#', '#', '#', ' ', ' ', ' ', ' ', '#'},
  68. {'#', ' ', ' ', ' ', '#', ' ', ' ', ' ', ' ', '#'},
  69. {'#', ' ', '#', ' ', ' ', ' ', '#', ' ', ' ', '#'},
  70. {'#', ' ', '#', '#', '#', ' ', '#', '#', ' ', '#'},
  71. {'#', '#', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '#'},
  72. {'#', '#', '#', '#', '#', '#', '#', '#', '#', '#'}
  73. };
  74. Game game = new Game(map);
  75. Point start = new Point(1, 1);
  76. Point end = new Point(8, 8);
  77. ArrayList<Point> path = game.findPath(start, end);
  78. for (Point point : path) {
  79. System.out.println(point.getRow() + " - " + point.getColumn());
  80. }
  81. }
  82. }

Node

  1. public class Node<T> {
  2. private T content;
  3. private Node<T> next;
  4. public Node() {
  5. }
  6. public Node(T content) {
  7. this.content = content;
  8. }
  9. public Node(T content, Node<T> next) {
  10. this.content = content;
  11. this.next = next;
  12. }
  13. // getter and setter
  14. }

Point

  1. public class Point {
  2. //行
  3. private int row = 0;
  4. //列
  5. private int column = 0;
  6. public Point(int row, int column) {
  7. this.row = row;
  8. this.column = column;
  9. }
  10. //getter and setter
  11. }

Title

  1. public class Tile {
  2. // 方格位置
  3. private Point point;
  4. // 方格当前遍历的方向
  5. private int direction = 0;
  6. public Tile(Point point, int direction) {
  7. this.point = point;
  8. this.direction = direction;
  9. }
  10. // getter and setter
  11. }

YKDStack

  1. public class YKDStack<T> {
  2. Node<T> header;
  3. public YKDStack() {
  4. }
  5. // 添加元素
  6. public boolean push(T value) {
  7. if (this.header == null) {
  8. // 当前为空的时候,添加header
  9. this.header = new Node<T>(value);
  10. } else {
  11. // 当前不为空的时候,构建一个新的头部节点
  12. this.header = new Node<T>(value, this.header);
  13. }
  14. return true;
  15. }
  16. // 弹出栈顶元素
  17. public T pop() {
  18. if (this.header == null) {
  19. return null;
  20. }
  21. // 删除链表头部节点
  22. Node<T> node = this.header;
  23. this.header = node.getNext();
  24. node.setNext(null);
  25. return node.getContent();
  26. }
  27. // 获取栈顶元素
  28. public T peek() {
  29. if (this.header == null) {
  30. return null;
  31. }
  32. return this.header.getContent();
  33. }
  34. }

拓展

如果可以八个方向探索。代码怎么修改?

栈 - 图14

  1. // 存储8个方向
  2. private int[][] directions = {{0, 1},{1,1}, {1, 0},{1,-1}, {0, -1},{-1,-1}, {-1, 0},{-1,1}};
  1. // 当方向为 8 的时候,表示所有方向都已经遍历过了,所以标记为死路@,并弹出栈顶元素
  2. if (tile.getDirection() == 8) {
  3. this.map[tilePoint.getRow()][tilePoint.getColumn()] = '@';
  4. this.steps.pop();