文章结构

  1. 栈是什么
  2. Java中的Stack源码分析
  3. 什么时候使用栈
  4. 应用实例:使用栈来解决表达式计算问题

1、栈是什么

栈是一种操作受限的线性表,只能在一端插入和删除数据,栈中的元素遵循先入后出,后入先出的原则。

用一个形象的比方就是我们平时叠盘子,从下往上一个一个的叠,然后取的时候从上往下一个一个的取。

栈示意图

2、Java中的Stack源码分析

Stack继承自Vector,Stack中数据存储是使用数组来实现的。下面我们一一分析Stack的初始化,入栈、出栈和扩容操作

2.1 Stack的初始化

Stack只有一个无参构造函数

/**
 * Creates an empty Stack.
 */
public Stack() {
}

但是无参构造函数默认会去调用父类的无参构造函数,Vector的无参构造函数如下

public Vector() {
    this(10);
}
public Vector(int initialCapacity) {
    this(initialCapacity, 0);
}

public Vector(int initialCapacity, int capacityIncrement) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    this.elementData = new Object[initialCapacity];
    this.capacityIncrement = capacityIncrement;
}

最后我们看到Stack在Vector的构造函数中创建了一个大小为10的elementData数组用来存放添加的元素。

2.2 入栈操作

Stack的入栈操作方法

public E push(E item) {
    addElement(item);
    return item;
}

push()中调用父类Vector中的addElement()方法向elementData数组中添加元素,addElement()源码如下

public synchronized void addElement(E obj) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = obj;
}

其中ensureCapacityHelper(elementCount + 1)是用来判断是否需要对
elementData数组进行扩容的,我们放到后面的扩容里面去分析

2.3 出栈操作

Stack定义了pop()方法来返回栈顶元素

public synchronized E pop() {
    E obj;
    int len = size();
    obj = peek();//获取栈顶元素
    removeElementAt(len - 1);//删除栈顶元素
    return obj;
}

获取栈顶元素,注意的时候在获取栈顶元素的时候,要先判断栈是否为空,如果栈为空的情况下去获取栈顶元素,会抛出EmptyStackException异常

public synchronized E peek() {
    int len = size();
    if (len == 0)
        throw new EmptyStackException();
    return elementAt(len - 1);//从elementData取栈顶元素
}

删除栈顶元素

public synchronized void removeElementAt(int index) {
    modCount++;
/**
*下面的注释是我加的,栈中删除元素index==elementcount-1;这段代码不会执行
*/
//        if (index >= elementCount) {
//            throw new ArrayIndexOutOfBoundsException(index + " >= " +
//                    elementCount);
//        } else if (index < 0) {
//            throw new ArrayIndexOutOfBoundsException(index);
//        }
//        int j = elementCount - index - 1;
//        if (j > 0) {
//            System.arraycopy(elementData, index + 1, elementData, index, j);
//        }

    elementCount--;
    elementData[elementCount] = null; /* to let gc do its work */
}

2.4 扩容

ensureCapacityHelper(elementCount + 1)调用传入需要的最小容量,然后拿最小容量和现在的elementData数组大小比较,如果需要的最小容量比elementData数组大,则调用grow(minCapacity)方法进行扩容

private void ensureCapacityHelper(int minCapacity) {
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

我们看一下grow()方法具体是怎么实现扩容的

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    //计算扩容之后的大小
    int newCapacity = oldCapacity + ((capacityIncrement > 0) ?capacityIncrement : oldCapacity);
    
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    //防止扩容超过数组的最大限制
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}

capacityIncrement表示每次扩容的大小,默认是0,可以自己设置。如果capacityIncrement>0,则按照capacityIncrement的大小来扩容;如果capacityIncrement是0的时候,则执行newCapacity = oldCapacity +oldCapacity,也就是将elementData扩大一倍。但是我们要防止扩容之后的大小超过了数组的最大大小限制

顺序栈入栈和扩容示意图

3、什么时候使用栈

当我们需要这种先进后出,后进先出的操作特性的数据结构的时候,我们就可以想到使用栈。比如说,我们在用非递归方式去实现二叉树的遍历的时候,如中序遍历的非递归实现

private static void traverseMide2(StringBuilder builder, Node root) {
    Stack<Node> stack = new Stack<>();//缓存要回溯的节点
    Node temp = root;
    while (temp != null || !stack.isEmpty()) {
        while (temp != null) {
            stack.push(temp);
            temp = temp.left;
        }
        if (!stack.isEmpty()) {
            temp = stack.pop();
            builder.append(temp.value + ",");//打印节点
            if (temp.right != null) {
                temp = temp.right;
            } else {
                temp = null;
            }
        }
    }
}

另外方法调用本身也是一种栈的形式

4、应用实例:使用栈来解决表达式计算问题

如何编写代码计算由括号和加减乘除组成的表达式,比方计算下面的表达式的值

2*5+30/(2+3)-10/2

分析

首先我们来看一下运算规则

  1. 从左向右运算
  2. 先计算括号里面的
  3. 再计算乘除法
  4. 再计算加减法

我们可以将括号作为一个子表达式,将计算过程分解为两类

  1. 加减乘除的运算实现
  2. 括号的运算实现

1. 加减乘除的运算实现

  1. 定义操作符的优先级:加和减优先级为10;乘和除优先级为20;(加减的优先级低于乘除的优先级)
  2. 使用两个栈来实现计算过程:一个栈用来保存操作数,一个栈用来保存操作符
  3. 计算过程:从左向右遍历表达式,
    1. 当遇到数字的时候,我们就直接压入操作数栈;
    2. 当遇到运算符的时候,我们就把运算符和运算符栈顶的元素进行比较:如果比运算符栈顶元素的优先级高,就将当前运算符入栈;如果比运算符栈顶元素的优先级低或者相同,则从运算符栈中取栈顶运算符,从操作数栈的栈顶取两个操作数进行计算,再把计算完的结果压入操作数栈,然后拿这个操作符继续和运算符栈顶元素比较。
计算过程演示

2. 括号的运算实现

  1. 定义括号的优先级:括号优先级为0;(低于加减乘除的优先级,这样不会破坏上面的计算过程)
  2. 遇到左括号:将括号添加到运算符栈顶
  3. 遇到右括号:计算栈中数据,直到操作符栈顶为左括号;将左括号出栈,继续遍历

代码实现

public class ExpressionCalculate {

    public static int calculate(String expression) {
        Stack<Integer> stackValue = new Stack<>();
        Stack<Character> stackOperator = new Stack<>();
        if (expression == null || expression.trim().length() == 0) {
            return Integer.MIN_VALUE;
        }

        char[] array = expression.toCharArray();
        for (int i = 0; i < array.length; i++) {
            char ch = array[i];
            if (ch >= '0' && ch <= '9') {
                int value = ch - '0';
                while (i + 1 < array.length && array[i + 1] >= '0' && array[i + 1] <= '9') {//获取操作数
                    value = value * 10 + array[i + 1] - '0';
                    i++;
                }
                stackValue.add(value);
            } else if (ch == '+' || ch == '-' || ch == '*' || ch == '/') {
                int level = getLevel(ch);
                while (!stackOperator.isEmpty() && level <= getLevel(stackOperator.peek())) {//当前操作符比操作符栈顶的操作符级别低或者相等
                    calculate(stackValue, stackOperator);//执行计算:操作符栈顶元素出栈,从操作数
                }
                stackOperator.push(ch);//操作符入栈
            } else if (ch == '(') {//将左括号入栈
                stackOperator.push(ch);
            } else if (ch == ')') {//右括号,将括号内的表达式计算完毕
                while (!stackOperator.isEmpty() && stackOperator.peek() != '(') {
                    calculate(stackValue, stackOperator);
                }
                stackOperator.pop();//括号计算完之后,左括号出栈
            }
        }
        while (!stackOperator.isEmpty()) {//表达式已结束,将栈中所有数据计算完毕
            calculate(stackValue, stackOperator);
        }
        return stackValue.pop();
    }

    private static void calculate(Stack<Integer> stackValue, Stack<Character> stackOperator) {
        int num2 = stackValue.pop();
        char operator = stackOperator.pop();
        int num1 = stackValue.pop();
        switch (operator) {
            case '+':
                num2 = num1 + num2;
                break;
            case '-':
                num2 = num1 - num2;
                break;
            case '*':
                num2 = num1 * num2;
                break;
            case '/':
                num2 = num1 / num2;
                break;
        }
        stackValue.push(num2);
    }
    /**
    * 获取操作符级别
    */
    private static int getLevel(char ch) {
        switch (ch) {
            case '+':
            case '-':
                return 10;
            case '*':
            case '/':
                return 20;
            case '(':
            case ')':
                return 0;
        }
        return -1;
    }
}

完整的源码和测试用例请查看github之ExpressionCalculate和ExpressionCalculateTest

说明

文中图片来源:极客时间,王争《数据结构与算法之美》

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351