假设我们要求输入类似这样一个表达式:9+(3-1)*3+10/2,输出结果。我们知道先括号,再乘除,最后加减,中学时候使用的科学计算器,是允许输入这样的表达式计算结果的,那么计算机怎么知道这个串里面先算括号再算乘除呢?我们先来介绍下栈这种数据结构,再来解决这个问题。
前面已经说过数组的连表,现在来说另外一种线性表的数据结构---栈。
举个比较形象的例子,洗盘子的时候,是不是一个一个往上面堆着放,拿的时候也从上面一个一个的拿,最先放的在最下面,最后放的在最上面,拿的时候第一个拿到。这就是典型的栈结构。先进后出First In Last Out(FILO).
怎么来实现一个栈结构呢,栈也是一种线性表,前面也有提到两种很基础的线性表结构的数据结构数组和链表。栈其实就是第一个特殊的链表或者数组。可以基于数组或者链表来实现,成为数组栈或者链栈,与之具有数组和链表相关特点。
栈的特殊点在于先进去的元素放在栈低,后进的在栈顶。向栈中插入一个元素叫入栈、进栈、压栈都行,插入的数据会被放在栈顶。从栈中取出一个元素叫出栈、退栈都行,取出之后,原本栈顶的这个元素就会被删掉,让它下面的那个元素成为新的栈顶元素。
数组栈一般栈低是索引开始的元素,压栈就往索引增长方向走;链栈一般栈低是头结点,栈顶是尾结点。
既然都是用数组或链表来实现,为什么还单独拎出来一个数据结构呢。数组和链表暴露了太多了的操作。就会更容易出错。针对性的封装出来的栈这种结构,在某些场景会更加适合。想象一下我们浏览器的的前进后退,是不是就很像两个栈的数据在互相交换操作,一个前进栈,一个后退栈。点后退,把后退栈的栈顶弹出,放进前进栈的栈顶;再点前进,是不是就是压进前进栈顶的后退栈的栈顶元素。就这样互相交替着。
想象一个程序的调用流程是不是也是一个栈结构。最后调用的方法最先执行。
Java里面的Stack也是基于数组实现的,它继承了Vector。我们用数组实现一个简单栈的基本操作:
package com.nijunyang.algorithm.stack; /** * Description: * Created by nijunyang on 2020/4/1 23:48 */ public class MyStack{ private static final int DEFAULT_SIZE = 10; private Object[] elements; private int size; public MyStack() { this(DEFAULT_SIZE); } public MyStack(int capacity) { this.elements = new Object[capacity]; } /** * 入栈 * @param e */ public void push(E e) { //弹性伸缩,扩容/收缩释放内存空间 if (size >= elements.length) { resize(size * 2); } else if (size > 0 && size < elements.length / 2) { resize(elements.length / 2); } elements[size++] = e; } /** * 出栈 */ public E pop() { if (isEmpty()) { return null; } E e = (E) elements[--size]; //size是5,那么最后一个元素就是4也就是--size elements[size] = null; //现在size已经是4了,弹出就是4这个元素的位置置为空 return e; } public boolean isEmpty() { return size == 0; } public int size() { return size; } /** * 扩容/收缩 */ private void resize(int newCapacity) { Object[] temp = new Object[newCapacity]; for(int i = 0 ; i < size; i ++){ temp[i] = elements[i]; } elements = temp; } public static void main(String[] args){ MyStack myStack = new MyStack(5); myStack.push(1); myStack.push(2); myStack.push(3); myStack.push(4); myStack.push(5); myStack.push(6); System.out.println(myStack.size); Integer e = myStack.pop(); System.out.println(e); e = myStack.pop(); System.out.println(e); e = myStack.pop(); System.out.println(e); e = myStack.pop(); System.out.println(e); e = myStack.pop(); System.out.println(e); e = myStack.pop(); System.out.println(e); e = myStack.pop(); System.out.println(e); } }
现在用我们看看怎么用栈来解决9+(3-1)*3+10/2这个计算问题
首先我们要怎么来处理括号和运算符号的优先级呢
这里先说一下中缀表达式和后缀表达式,像这个表达式9+(3-1)*3+10/2就是中缀表达式,如果我们转换成9 3 1 - 3 * + 10 2 / + 这个就是后缀表达式,后缀表达式也叫逆波兰,可以可以自行百度或者google,后缀表达式就是操作符号在两个操作数的后面,而中缀表达式就是操作符号在两个操作数的中间。
看下后缀表达式是怎么操作的,就是遇到操作符号就把前面两个数进行符号运算:
9 3 1 - 3 * + 10 2 / + 这个表达式的操作如下:
9 3 1 - 这个时候就把3和1 相减得到2 => 9 2 3 * 这个时候就把2和3相乘 得到6 =>
9 6 + => 15 10 2 / =>15 5 + => 20
大致就是这么个流程,这个过程是不是很像栈的操作,遇到数字就入栈,遇到符号就把数字前面两个数字出栈进行计算,然后将结果入栈,直到表达式结束。
现在我们只要把中缀表达式转换成后缀表达式就可以进行计算了。看下百度的转换流程
简单来说就是用一个栈来存放符号,然后从左到右遍历中缀表达式的数字和字符,若是数字就输出,若是符号则判断和栈顶符号的优先级,如果是括号或优先级低于栈顶元素,则依次出栈并输出,将当前符号进栈,直到最后结束。
9+(3-1)*3+10/2
先初始化一个栈stack,然后依次遍历我们的中缀表达式,操作逻辑如下:
- 9 输出 => 9
- + 栈空的直接进栈:stack:+
- ( 未配对的 直接进栈:stack:+ (
- 3 数字直接输出:9 3
- - 前面是( 直接进栈:stack + ( -
- 1 直接输出: 9 3 1
- ) 将前面的符号弹出输出,直到匹配到第一个(为止:9 3 1 - stack: +
- * 优先级高于 + 进栈: stack: + *
- 3 输出 9 3 1 - 3
- + 优先级低于栈顶的* 将栈顶弹出输出 继续判断之后栈顶是否比+优先级低(同级也弹出,直到有限比栈顶高,或者空栈为止),这里就会连续弹出 * + 然后将当前的 + 入栈:9 3 1 - 3 * + stack: +
- 10 输出:9 3 1 - 3 * + 10
- / 优先级高于栈顶 + 直接入栈:stack: + /
- 2 直接输出: 9 3 1 - 3 * + 10 2
- 最后符号依次出输出:9 3 1 - 3 * + 10 2 / +
从上述逻辑中可以看到,不管是最后的计算,还是中缀表达式转后缀表达式中都用到栈这种数据结构。