Stack详解

# Stack详解

基于JDK8

# 1、栈数据结构动画演示

mixureSecure

从动画中可以看出,栈这种数据结构 只能从同一头进,从同一头出。
比较正式的说法叫: 后进先出(LIFO, Last In First Out)。

# 2、Stack的继承体系

public class Stack<E> extends Vector<E>
mixureSecure

可以看到JDK提供的 Stack 是利用 Vector实现的。所以Stack 类也是线程安全的。

# 3、Stack的push (入栈)方法

public E push(E item) {
    // 调用 addElement 方法将 item 添加到 Vector 的末尾
    addElement(item);

    // 返回被添加的元素 item
    return item;
}
public synchronized void addElement(E obj) {
    // 修改计数器增加,用于记录 Vector 被修改的次数
    modCount++;

    // 确保 Vector 有足够的容量来容纳新元素
    ensureCapacityHelper(elementCount + 1);

    // 将新元素添加到 elementData 数组的 elementCount 位置
    elementData[elementCount++] = obj;
}

private void ensureCapacityHelper(int minCapacity) {
    // 检查是否需要扩容,如果当前容量小于 minCapacity 则进行扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}


protected Object[] elementData;

private void grow(int minCapacity) {
    // 当前数组的容量
    int oldCapacity = elementData.length;

    // 新的容量,如果 capacityIncrement 大于 0 则按此增量扩容,否则容量翻倍
    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);
}

上面只有push方法是Stack提供的,其余方法都是Vector类提供的。

# 4、Stack的pop (出栈)方法

public synchronized E pop() {
    E obj;
    int len = size();

    // 获取栈顶元素
    obj = peek();

    // 移除栈顶元素
    removeElementAt(len - 1);

    // 返回被移除的栈顶元素
    return obj;
}

public synchronized E peek() {
    int len = size();

    // 如果栈为空,抛出 EmptyStackException 异常
    if (len == 0)
        throw new EmptyStackException();

    // 返回栈顶元素(位于 elementData 数组的最后一个有效位置)
    return elementAt(len - 1);
}

public synchronized void removeElementAt(int index) {
    // 增加修改计数器
    modCount++;

    // 如果索引超出当前元素数量,抛出 ArrayIndexOutOfBoundsException 异常
    if (index >= elementCount) {
        throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);
    }
    // 如果索引为负数,抛出 ArrayIndexOutOfBoundsException 异常
    else if (index < 0) {
        throw new ArrayIndexOutOfBoundsException(index);
    }

    // 计算需要移动的元素个数
    int j = elementCount - index - 1;

    // 如果有需要移动的元素
    if (j > 0) {
        // 将 elementData 数组中 index + 1 到 elementCount - 1 的元素左移一位
        System.arraycopy(elementData, index + 1, elementData, index, j);
    }

    // 减少元素数量
    elementCount--;

    // 将最后一个元素设为 null 以便垃圾回收
    elementData[elementCount] = null;
}

pop (出栈)方法利用到了peek(获取栈顶元素)方法。

# 5、Stack是如何利用Vector实现栈数据结构的?

上面已经用动画演示了栈数据结构的特点:栈这种数据结构 只能从同一头进,从同一头出。

栈的主要操作包括以下几种:

  • 压栈(push):将元素添加到栈的顶部。
  • 出栈(pop):移除并返回栈顶元素。
  • 查看栈顶元素(peek):返回栈顶元素但不移除它。

Stack 类通过继承 Vector,并添加自己的方法,实现了这些操作。
push 方法
push 方法将元素压入栈顶。它实际上调用了 Vector 的 addElement 方法,将元素添加到 Vector 的末尾

pop 方法
pop 方法移除并返回栈顶元素。它先调用 peek 方法获取栈顶元素,然后调用 removeElementAt 方法移除该元素。

peek 方法
peek 方法返回栈顶元素但不移除它。它直接访问 Vector 的最后一个元素。

Vector和ArrayList类似,底层都是使用Object数组来存储元素的。
Stack通过操作数组的末尾,比如push只把元素添加到数组的末尾,pop只移除数组末尾的元素。

mixureSecure

# 6、自己实现栈(不借助JDK提供的集合)

主要利用Object 数组和Arrays.copyOf方法

class MyStack<T>{

    // 默认容量
    private final Integer defaultCapacity = 10;
    // 存储元素的数组
    private Object[] elementData;
    // 真正存储的元素个数
    private int elementCount;

    public MyStack() {
        this.elementData = new Object[10];
    }


    public void push(T element){
        // 元素个数大于默认容量就复制数组
        if(gtDefaultCapacity(elementCount + 1)){
            // 创建一个新的数组,长度为当前数组长度加1
            Object[] newElements = Arrays.copyOf(elementData, elementCount + 1);
            newElements[elementCount++] = element;
            elementData = newElements;
        }else {
            // 否则就直接添加元素到数组末尾
            this.elementData[elementCount++] = element;
        }
    }

    // 获取栈顶元素
    @SuppressWarnings(value = "unchecked")
    public T peek(){
        if(elementCount<=0){
            throw new RuntimeException("当前栈为空!");
        }
        return (T)elementData[elementCount-1];
    }

    // 删除栈顶元素
    @SuppressWarnings(value = "unchecked")
    public T pop(){
        if(elementCount<=0){
            throw new RuntimeException("当前栈为空!");
        }
        Object temp = elementData[elementCount - 1];
        elementData = Arrays.copyOf(elementData, elementCount - 1);
        elementCount--;
        return (T) temp;
    }


    // 判断是否大于默认容量
    private boolean gtDefaultCapacity(int length){
        return length > defaultCapacity;
    }

    // 获取栈大小
    public int size(){
        return elementCount;
    }

    // 判断栈是否为空
    public boolean isEmpty(){
        return elementCount <= 0;
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        for (int i =elementCount - 1 ; i >= 0; i--) {
            builder.append(elementData[i].toString()).append("  ");
        }
        return builder.toString();
    }

}

测试:

public static void main(String[] args) throws Exception {
        MyStack<String> myStack = new MyStack<>();
        myStack.push("秀逗");
        myStack.push("四眼");
        myStack.push("大黄");

        System.out.println(myStack.peek());  //  大黄
        System.out.println(myStack.pop());  // 大黄
        System.out.println(myStack.size()); // 2
        System.out.println(myStack.isEmpty()); // false
		System.out.println(myStack);  // 四眼  秀逗

        System.out.println(myStack.pop()); //  四眼  
        System.out.println(myStack.pop());//  秀逗 
        System.out.println(myStack.pop()); // java.lang.RuntimeException: 当前栈为空!
       
    }

我们利用Object 数组和Arrays.copyOf方法实现了一个简单的栈,这个栈包含基本的 push、peek、pop、size、isEmpty方法,并模拟出栈顺序重写了 toString。

# 7、自己实现栈(借助JDK提供的集合)

上面我们自己利用Object 数组和Arrays.copyOf方法实现了简单的栈,我们默认设置了10的容量,如果超过10个元素,
我们利用Arrays.copyOf完成数组的复制。 这样做是非常耗费性能的,并且我们自己实现的栈是非线程安全的。
下面我们利用JDK现有的集合来实现栈。

# 利用 ArrayDeque 实现高性能的非线程安全的栈。

class MyStack<T>{
	// 使用 ArrayDeque 作为底层数据结构来存储栈元素
    private ArrayDeque<T> arrayDeque;

	// 构造方法,初始化 ArrayDeque
    public MyStack() {
        this.arrayDeque = new ArrayDeque<T>();
    }
    
	// 将元素压入栈顶,使用 addFirst 方法将元素添加到双端队列的头部
    public void push(T elementData){
        arrayDeque.addFirst(elementData);
    }


    public void peek(){
        arrayDeque.getFirst();
    }

    public T pop(){
       return arrayDeque.removeFirst();
    }

    public int size(){
        return arrayDeque.size();
    }

    public boolean isEmpty(){
        return arrayDeque.isEmpty();
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        // 这里不需要反序迭代  因为我们的push是往头部添加元素 直接正序迭代就是出栈顺序
        Iterator<T> iterator = arrayDeque.iterator();
        while (iterator.hasNext()){
            builder.append(iterator.next().toString()).append("  ");
        }
        return builder.toString();
    }
}

# 利用 ConcurrentLinkedDeque实现高性能的线程安全的栈。

class MyStack<T>{

    private ConcurrentLinkedDeque<T> concurrentLinkedDeque;

    public MyStack() {
        this.concurrentLinkedDeque = new ConcurrentLinkedDeque<T>();
    }

    public void push(T elementData){
        concurrentLinkedDeque.addFirst(elementData);
    }

    public void peek(){
        concurrentLinkedDeque.getFirst();
    }

    public T pop(){
       return concurrentLinkedDeque.removeFirst();
    }

    public int size(){
        return concurrentLinkedDeque.size();
    }

    public boolean isEmpty(){
        return concurrentLinkedDeque.isEmpty();
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        // 这里不需要反序迭代  因为我们的push是往头部添加元素 直接正序迭代就是出栈顺序
        Iterator<T> iterator = concurrentLinkedDeque.iterator();
        while (iterator.hasNext()){
            builder.append(iterator.next().toString()).append("  ");
        }
        return builder.toString();
    }
}

ConcurrentLinkedDeque 的高性能线程安全设计,涉及到乐观并发控制策略比如CAS,延迟回收,分段锁等机制。
后面总结并发编程知识的时候再详细分析下ConcurrentLinkedDeque 的设计。

# 8、栈的应用场景

栈虽然是一种后进先出(LIFO, Last In First Out)的数据结构,但是却有广泛的应用场景。
下面是几个常见的例子:

# ①、浏览器的前进、后退功能实现

mixureSecure

可以用两个栈分别存储前进和后退的页面
代码示例:

import java.util.Stack;

public class TestA {
    public static void main(String[] args) {
        Browser browser = new Browser();
        browser.visit("百度");
        browser.visit("谷歌");

        browser.back();  //  百度
        browser.forward();  // 谷歌
    }
}


class Browser {
    // 保存前进页面的栈,用于实现前进功能
    private Stack<String> forwardStack;
    // 保存后退页面的栈,用于实现后退功能
    private Stack<String> backStack;
    // 当前浏览的页面
    private String currentPage;

    // 初始化前进栈、后退栈和当前页面
    public Browser() {
        this.forwardStack = new Stack<>();
        this.backStack = new Stack<>();
        // 初始化默认页面为 "Home"
        this.currentPage = "Home";
    }

    /**
     * 访问新页面
     * @param page 新访问的页面
     */
    public void visit(String page) {
        if (page == null) {
            throw new RuntimeException("页面不能为空");
        }
        // 将当前页面加入后退栈,因为我们将访问新的页面
        backStack.push(currentPage);
        // 更新当前页面为新访问的页面
        currentPage = page;
        // 打印当前浏览页面
        System.out.println("当前浏览页面:" + currentPage);
        // 清空前进栈,因为访问新页面时前进栈失效
        forwardStack.clear();
    }

    /**
     * 前进到下一页面
     */
    public void forward() {
        System.out.println("前进");
        // 如果前进栈不为空
        if (!forwardStack.isEmpty()) {
            // 将当前页面加入后退栈
            backStack.push(currentPage);
            // 从前进栈弹出页面作为当前页面
            currentPage = forwardStack.pop();
            // 打印当前浏览页面
            System.out.println("当前浏览页面:" + currentPage);
        } else {
            // 前进栈为空,无法前进
            System.out.println("当前无前进页面");
        }
    }

    /**
     * 后退到上一页面
     */
    public void back() {
        System.out.println("后退");
        // 如果后退栈不为空
        if (!backStack.isEmpty()) {
            // 将当前页面加入前进栈
            forwardStack.push(currentPage);
            // 从后退栈弹出页面作为当前页面
            currentPage = backStack.pop();
            // 打印当前浏览页面
            System.out.println("当前浏览页面:" + currentPage);
        } else {
            // 后退栈为空,无法后退
            System.out.println("当前无后退页面");
        }
    }

    /**
     * 获取当前浏览的页面
     * @return 当前页面
     */
    public String getCurrentPage() {
        return currentPage;
    }
}

运行结果:

当前浏览页面:百度
当前浏览页面:谷歌
后退
当前浏览页面:百度
前进
当前浏览页面:谷歌

动画示例

mixureSecure

# ②、表达式求值 (实现简单计算器功能)

栈常用于表达式求值,如中缀表达式转后缀表达式(逆波兰表达式)和逆波兰表达式求值。

先了解几个表达式的基本概念:
一般来说,数学表达式的表示方法主要有以下三种:

中缀表达式 (Infix Notation)
前缀表达式 (Prefix Notation) 又叫 波兰式 (Polish Notation)
后缀表达式 (Postfix Notation) 又叫 逆波兰式 (Reverse Polish Notation)

表达式类型 示例 直观性 优先级标识 计算机实现 别名
中缀表达式 (A + B) * C 需要括号 复杂
前缀表达式 * + A B C 不需要 简单 波兰式
后缀表达式 A B + C * 不需要 简单 逆波兰式

特点比较:

  • 中缀表达式 是我们日常使用的表达方式,运算符位于操作数之间,直观但需要括号来明确优先级,计算机实现较复杂。
  • 前缀表达式(波兰式)运算符位于操作数之前,不需要括号来确定优先级,适合树结构计算,计算机实现简单但不直观。
  • 后缀表达式(逆波兰式)运算符位于操作数之后,同样不需要括号来确定优先级,适合栈结构计算,计算机实现简单但不直观。

我们可以利用栈实现计算器功能 (实际上就是中缀表达式转后缀表达式,再利用栈实现后缀表达式的计算):

import java.util.Scanner;
import java.util.Stack;

public class TestA {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.print("请输入一个中缀表达式 (输入 'exit' 退出): ");
            String infixExpression = scanner.nextLine();

            if (infixExpression.equalsIgnoreCase("exit")) {
                break; // 输入 'exit' 退出循环
            }

            try {
                String postfixExpression = infixToPostfix(infixExpression);
                System.out.println("后缀表达式: " + postfixExpression);
                double result = evaluatePostfix(postfixExpression);
                if (result % 1 == 0) { // 判断结果是否为整数
                    System.out.println("计算结果: " + (int) result); // 整数结果去掉小数部分
                } else {
                    System.out.println("计算结果: " + result);
                }
            } catch (Exception e) {
                System.out.println("表达式无效: " + e.getMessage());
            }
        }
        scanner.close();
    }

    // 中缀表达式转后缀表达式
    public static String infixToPostfix(String infix) {
        Stack<Character> stack = new Stack<>();
        StringBuilder postfix = new StringBuilder();

        for (int i = 0; i < infix.length(); i++) {
            char ch = infix.charAt(i);

            if (Character.isDigit(ch)) {
                // 处理多位数字和小数
                while (i < infix.length() && (Character.isDigit(infix.charAt(i)) || infix.charAt(i) == '.')) {
                    postfix.append(infix.charAt(i++));
                }
                postfix.append(' ');
                i--; // 调整索引,以便下一次循环从正确位置开始
            } else if (ch == '(') {
                stack.push(ch);
            } else if (ch == ')') {
                // 将栈中的元素弹出直到遇到 '('
                while (!stack.isEmpty() && stack.peek() != '(') {
                    postfix.append(stack.pop()).append(' ');
                }
                stack.pop(); // 弹出 '('
            } else if (isOperator(ch)) {
                // 当栈不为空且当前运算符的优先级小于等于栈顶运算符的优先级时,弹出栈顶运算符并加入后缀表达式
                while (!stack.isEmpty() && precedence(stack.peek()) >= precedence(ch)) {
                    postfix.append(stack.pop()).append(' ');
                }
                stack.push(ch); // 将当前运算符入栈
            }
        }

        // 将栈中剩余的运算符全部加入后缀表达式
        while (!stack.isEmpty()) {
            postfix.append(stack.pop()).append(' ');
        }

        return postfix.toString().trim(); // 返回后缀表达式
    }

    // 计算后缀表达式
    public static double evaluatePostfix(String postfix) {
        Stack<Double> stack = new Stack<>();

        for (int i = 0; i < postfix.length(); i++) {
            char ch = postfix.charAt(i);

            if (Character.isDigit(ch)) {
                // 处理多位数字和小数
                StringBuilder number = new StringBuilder();
                while (i < postfix.length() && (Character.isDigit(postfix.charAt(i)) || postfix.charAt(i) == '.')) {
                    number.append(postfix.charAt(i++));
                }
                stack.push(Double.parseDouble(number.toString())); // 将数字入栈
                i--; // 调整索引,以便下一次循环从正确位置开始
            } else if (isOperator(ch)) {
                double b = stack.pop();
                double a = stack.pop();
                // 根据运算符进行计算并将结果入栈
                switch (ch) {
                    case '+': stack.push(a + b); break;
                    case '-': stack.push(a - b); break;
                    case '*': stack.push(a * b); break;
                    case '/': stack.push(a / b); break;
                }
            }
        }

        return stack.pop(); // 返回计算结果
    }

    // 判断是否是运算符
    public static boolean isOperator(char ch) {
        return ch == '+' || ch == '-' || ch == '*' || ch == '/';
    }

    // 返回运算符优先级
    public static int precedence(char ch) {
        switch (ch) {
            case '+':
            case '-': return 1;
            case '*':
            case '/': return 2;
        }
        return -1; // 如果不是运算符,则返回 -1
    }
}

输入:3*(1+2)
输出:

后缀表达式: 3 1 2 + *
计算结果: 9

栈在计算后缀表达式结果时的步骤:

  • 1.创建一个空栈,用于存储操作数。
  • 2.从左到右遍历后缀表达式的每个字符。
  • 3.如果当前字符是操作数(数字),则将其转换为对应的数值,并压入栈中。
  • 4.如果当前字符是运算符(+、-、*、/):
          从栈中弹出两个操作数,分别记为 a 和 b(注意:b 是栈顶元素,a 是次顶元素)。
          根据当前运算符进行计算:根据后缀表达式的特性,b 是操作符 a 的右操作数,a 是左操作数。
  • 5.将计算结果压入栈中。
  • 6.遍历完后缀表达式后,栈中剩余的唯一元素即为最终的计算结果。

计算过程动画演示:

mixureSecure

# ③、语法解析

括号匹配

import java.util.Stack;

public class TestA {

    public static void main(String[] args) {
        String s = "{[(1234)]}";
        // 调用isValid方法并输出结果,应该输出true,因为输入的括号字符串是有效的
        System.out.println(isValid(s)); // 输出: true
    }

    // 判断括号字符串是否有效的方法
    public static boolean isValid(String s) {
        // 创建一个栈来存放左括号
        Stack<Character> stack = new Stack<>();

        // 遍历输入的字符串中的每一个字符
        for (char c : s.toCharArray()) {
            // 如果是左括号,则压入栈中
            if (c == '(' || c == '{' || c == '[') {
                stack.push(c);
            } else if (c == ')' || c == '}' || c == ']') { // 只处理右括号
                // 如果是右括号且栈为空,说明没有匹配的左括号,返回false
                if (stack.isEmpty()) return false;

                // 弹出栈顶的左括号
                char top = stack.pop();
                // 判断栈顶的左括号和当前的右括号是否匹配,如果不匹配,返回false
                if (c == ')' && top != '(' || c == '}' && top != '{' || c == ']' && top != '[') {
                    return false;
                }
            }
        }

        // 如果栈为空,说明所有的括号都匹配,返回true;否则返回false
        return stack.isEmpty();
    }
}

# ④、栈排序

用一个栈对数据进行排序

import java.util.*;

public class TestA {

    public static void main(String[] args) {
        // 创建一个LinkedList并添加一些整数元素
        List<Integer> intCollection = new ArrayList<>();
        intCollection.add(5);
        intCollection.add(6);
        intCollection.add(3);
        intCollection.add(1);
        intCollection.add(2);
        intCollection.add(4);

        // 输出排序前的整数集合
        System.out.println("整数集合排序前: " + intCollection);
        // 调用sortCollection方法对集合进行排序
        Collection<Integer> sortedIntCollection = sortCollection(intCollection);
        // 输出排序后的整数集合
        System.out.println("整数集合排序后: " + sortedIntCollection);

    }

    // 对集合进行排序的方法,使用泛型并要求元素实现Comparable接口
    public static <T extends Comparable<T>> Collection<T> sortCollection(Collection<T> collection) {
        // 创建一个栈用于排序
        Stack<T> stack = new Stack<>();
        // 将集合中的元素压入栈中
        for (T element : collection) {
            stack.push(element);
        }

        // 创建一个临时栈用于辅助排序
        Stack<T> tempStack = new Stack<>();
        // 当输入栈不为空时进行排序
        while (!stack.isEmpty()) {
            // 弹出输入栈的栈顶元素
            T temp = stack.pop();
            // 将临时栈中所有大于temp的元素弹出并压入输入栈
            while (!tempStack.isEmpty() && tempStack.peek().compareTo(temp) < 0) {
                stack.push(tempStack.pop());
            }
            // 将temp压入临时栈
            tempStack.push(temp);
        }

        // 将临时栈中的元素全部弹出并存入一个新集合
        Collection<T> sortedCollection = new LinkedList<>();
        while (!tempStack.isEmpty()) {
            sortedCollection.add(tempStack.pop());
        }

        return sortedCollection;
    }
}

# ⑤、实现特殊算法

深度优先搜索(DFS)
二叉树的非递归前序遍历

回溯算法
例如求解迷宫问题、N皇后问题等。
算法类的暂时先不在这边篇文章内写了。后续会单独再写一些相关的内容。

# 补充: Stack的方法区别

方法 描述 能否添加 null 会否抛出异常
add 向栈中添加元素 不会抛出异常
push 将元素压入栈顶 不会抛出异常
peek 查看栈顶元素但不移除 - 如果栈为空,抛出 EmptyStackException
pop 移除并返回栈顶元素 - 如果栈为空,抛出 EmptyStackException