数据结构与算法-栈

什么是栈

 

栈是一种“操作受限”的线性表,只允许在一端插入和删除数据。

相比数组和链表,栈带给我的只有限制,并没有任何优势。那我直接使用数组或者链表不就好了吗?为什么还要用这个“操作受限”的“栈”呢?

从功能上来说,数组或链表确实可以替代栈,但你要知道,特定的数据结构是对特定场景的抽象,而且,数组或链表暴露了太多的操作接口,操作上的确灵活自由,但使用时就比较不可控,自然也就更容易出错。

当某个数据集合只涉及在一端插入和删除数据,并且满足后进先出,这时我们就应该首选“栈”这种数据结构。

栈的几种实现方式

顺序栈

基于数组实现

代码例子

 1 // 基于数组实现的顺序栈
 2 public class ArrayStack {
 3   private String[] items;  // 数组
 4   private int count;       // 栈中元素个数
 5   private int n;           //栈的大小
 6 
 7   // 初始化数组,申请一个大小为n的数组空间
 8   public ArrayStack(int n) {
 9     this.items = new String[n];
10     this.n = n;
11     this.count = 0;
12   }
13 
14   // 入栈操作
15   public boolean push(String item) {
16     // 数组空间不够了,直接返回false,入栈失败。
17     if (count == n) return false;
18     // 将item放到下标为count的位置,并且count加一
19     items[count] = item;
20     ++count;
21     return true;
22   }
23   
24   // 出栈操作
25   public String pop() {
26     // 栈为空,则直接返回null
27     if (count == 0) return null;
28     // 返回下标为count-1的数组元素,并且栈中元素个数count减一
29     String tmp = items[count-1];
30     --count;
31     return tmp;
32   }
33 }
View Code

在入栈和出栈过程中,只需要一两个临时变量存储空间,所以空间复杂度是 O(1)。

这里存储数据需要一个大小为 n 的数组,并不是说空间复杂度就是 O(n)。因为,这 n 个空间是必须的,无法省掉。所以我们说空间复杂度的时候,是指除了原本的数据存储空间外,算法运行还需要额外的存储空间。

链式栈

基于链表实现

支持动态扩容的栈

链式栈天然支持扩容,顺序栈需要动态扩容数组

链式栈缺点就是使用的是链表。需要存储后驱节点指针

顺序栈是基于数组,扩容阶段的时候会拷贝一份到新数组,造成扩容的时候内存占用双份。同事还会预留一些闲置空间

关于栈的应用场景

函数调用栈

栈在表达式求值中的应用

34+13*9+44-12/3。对于这个四则运算,我们人脑可以很快求解出答案,但是对于计算机来说,理解这个表达式本身就是个挺难的事儿。如果换作你,让你来实现这样一个表达式求值的功能,你会怎么做呢?

实际上,编译器就是通过两个栈来实现的。其中一个保存操作数的栈,另一个是保存运算符的栈。

我们从左向右遍历表达式,当遇到数字,我们就直接压入操作数栈;

当遇到运算符,就与运算符栈的栈顶元素进行比较。

如果比运算符栈顶元素的优先级高,就将当前运算符压入栈;

如果比运算符栈顶元素的优先级低或者相同,从运算符栈中取栈顶运算符,从操作数栈的栈顶取 2 个操作数,然后进行计算,再把计算完的结果压入操作数栈,继续比较。

我将 3+5*8-6 这个表达式的计算过程画成了一张图,你可以结合图来理解我刚讲的计算过程。

参考代码

  1 package org.easy.util;
  2 
  3 import java.util.Scanner;
  4 import java.util.Stack;
  5 
  6 //import java.util.Stack;
  7 public class Expression {
  8 
  9     //运算符之间的优先级,其顺序是+、-、*、/、(、),其中大于号表示优先级高
 10     //,小于号表示优先级低,等号表示优先级相同,感叹号表示没有优先关系
 11     public static final char[][] relation = {{'>','>','<','<','<','>','>'},
 12             {'>','>','<','<','<','>','>'},{'>','>','>','>','<','>','>'},
 13             {'>','>','>','>','<','>','>'},{'<','<','<','<','<','=','!'},
 14             {'>','>','>','>','!','>','>'},{'<','<','<','<','<','!','='}};
 15 
 16     public static void main(String[] args) {
 17         Scanner input = new Scanner(System.in);
 18         while(true){
 19             try{
 20                 System.out.println("请输入要计算的表达式:");
 21                 String exp = input.next();
 22                 System.out.println(calc(exp + "#"));
 23             }catch(ArithmeticException e1){
 24                 System.out.println("表达式中的分母不能为0");
 25                 e1.printStackTrace();
 26             }
 27         }
 28     }
 29     /**
 30      *
 31      * @param exp 要计算的表达式
 32      * @return 计算的结果
 33      */
 34     private static int calc(String exp) {
 35         //操作数栈
 36         Stack<Integer> num = new Stack<Integer>();
 37         //操作符栈
 38         Stack<Character> op = new Stack<Character>();
 39 
 40         op.push('#');
 41         int i = 0;
 42         char ch = exp.charAt(i);
 43         boolean flag = false;//判断连续的几个字符是否是数字,若是,就处理成一个数字。这样就能处理多位数的运算了。
 44         while(ch != '#' || op.peek() != '#') {//peek()查看栈顶对象但不移除。
 45             if(ch >= '0' && ch <= '9') {
 46                 if(flag) {
 47                     int tmp = num.pop();
 48                     //原来是数字,现在要先*10,如131,第一次压入1。第二次取出1*10+3=13,第三次去取出13*10+1=131
 49                     num.push(tmp * 10 + Integer.parseInt(ch + ""));
 50                 } else {
 51                     num.push(Integer.parseInt(ch + ""));
 52                 }
 53                 flag = true;
 54                 i++;
 55             } else {
 56                 flag = false;
 57                 //比较优先级
 58                 switch(precede(op.peek(), ch)) {
 59                     case '<':
 60                         op.push(ch);
 61                         i++;
 62                         break;
 63                     case '=':
 64                         op.pop();
 65                         i++;
 66                         break;
 67                     case '>':
 68                         int num2 = num.pop();
 69                         int num1 = num.pop();
 70                         int result = operate(num1, op.pop(), num2);
 71                         num.push(result);
 72                         break;
 73                     case '!':
 74                         System.out.println("输入的表达式错误!");
 75                         return -1;
 76                 }
 77             }
 78             ch = exp.charAt(i);
 79         }
 80         return num.peek();
 81     }
 82     private static char precede(char peek, char ch) {
 83         return relation[getIndex(peek)][getIndex(ch)];
 84     }
 85 
 86     /**
 87      *
 88      * @param ch 操作符
 89      * @return 操作符的索引,按照+、-、*、/、(、)的顺序
 90      */
 91     private static int getIndex(char ch) {
 92         int index = -1;
 93         switch(ch) {
 94             case '+':
 95                 index = 0;
 96                 break;
 97             case '-':
 98                 index = 1;
 99                 break;
100             case '*':
101                 index = 2;
102                 break;
103             case '/':
104                 index = 3;
105                 break;
106             case '(':
107                 index = 4;
108                 break;
109             case ')':
110                 index = 5;
111                 break;
112             case '#':
113                 index = 6;
114                 break;
115         }
116         return index;
117     }
118 
119     /**
120      *
121      * @param num1  第一个运算数
122      * @param ch 运算符
123      * @param num2 第二个运算数
124      * @return 运算结果
125      */
126     private static int operate(int num1, char ch, int num2) {
127         int result = 0;
128         switch(ch) {
129             case '+':
130                 result = num1 + num2;
131                 break;
132             case '-':
133                 result = num1 - num2;
134                 break;
135             case '*':
136                 result = num1 * num2;
137                 break;
138             case '/':
139                 result = num1 / num2;
140                 break;
141         }
142         return result;
143     }
144 }
View Code

其他

栈是一种常见的数据结构,其特点是后进先出(Last In First Out,LIFO)的顺序。栈常用于以下场景:

函数调用:栈可以用来保存函数调用时的现场信息,包括返回地址、局部变量等。每当一个函数被调用时,其现场信息就被压入栈中,函数执行完毕后再从栈中弹出。

表达式求值:在表达式求值过程中,栈可以用来保存运算符和操作数,以便按照正确的优先级进行计算。

括号匹配:栈可以用来判断括号是否匹配。遍历字符串,遇到左括号时将其压入栈中,遇到右括号时弹出栈顶元素并判断是否匹配。

浏览器前进后退:浏览器的前进和后退功能可以使用两个栈来实现。每当用户浏览一个新页面时,将该页面压入前进栈中;当用户点击后退按钮时,将当前页面弹出前进栈并压入后退栈中;当用户点击前进按钮时,将后退栈的栈顶元素弹出并压入前进栈中。

编译器中的语法分析:编译器在进行语法分析时,常常使用栈来存储语法规则和语法分析过程中的状态。

 

栈的方法

  1. push:将元素压入栈顶。
  2. pop:将栈顶元素弹出。
  3. top:返回栈顶元素,但不弹出。
  4. isEmpty:判断栈是否为空。
  5. size:返回栈中元素的个数。
posted @ 2023-11-07 11:52  意犹未尽  阅读(29)  评论(0编辑  收藏  举报