数据结构与算法-栈
什么是栈
栈是一种“操作受限”的线性表,只允许在一端插入和删除数据。
相比数组和链表,栈带给我的只有限制,并没有任何优势。那我直接使用数组或者链表不就好了吗?为什么还要用这个“操作受限”的“栈”呢?
从功能上来说,数组或链表确实可以替代栈,但你要知道,特定的数据结构是对特定场景的抽象,而且,数组或链表暴露了太多的操作接口,操作上的确灵活自由,但使用时就比较不可控,自然也就更容易出错。
当某个数据集合只涉及在一端插入和删除数据,并且满足后进先出,这时我们就应该首选“栈”这种数据结构。
栈的几种实现方式
顺序栈
基于数组实现
代码例子
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 }
在入栈和出栈过程中,只需要一两个临时变量存储空间,所以空间复杂度是 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 }
其他
栈是一种常见的数据结构,其特点是后进先出(Last In First Out,LIFO)的顺序。栈常用于以下场景:
函数调用:栈可以用来保存函数调用时的现场信息,包括返回地址、局部变量等。每当一个函数被调用时,其现场信息就被压入栈中,函数执行完毕后再从栈中弹出。
表达式求值:在表达式求值过程中,栈可以用来保存运算符和操作数,以便按照正确的优先级进行计算。
括号匹配:栈可以用来判断括号是否匹配。遍历字符串,遇到左括号时将其压入栈中,遇到右括号时弹出栈顶元素并判断是否匹配。
浏览器前进后退:浏览器的前进和后退功能可以使用两个栈来实现。每当用户浏览一个新页面时,将该页面压入前进栈中;当用户点击后退按钮时,将当前页面弹出前进栈并压入后退栈中;当用户点击前进按钮时,将后退栈的栈顶元素弹出并压入前进栈中。
编译器中的语法分析:编译器在进行语法分析时,常常使用栈来存储语法规则和语法分析过程中的状态。
栈的方法
- push:将元素压入栈顶。
- pop:将栈顶元素弹出。
- top:返回栈顶元素,但不弹出。
- isEmpty:判断栈是否为空。
- size:返回栈中元素的个数。