简介

限定仅在表尾进行插入和删除操作的线性表。允许插入和删除的一端成为栈顶,另一端成为栈低,不含任何元素的栈成为空栈,栈又称为先进先出的线性表,简称LIFO结构。

 

栈的插入操作,叫做进栈,也称压栈,入栈。

栈的删除操作,也叫出战,也有的叫做弹栈。

栈的附加功能

  • Peep 窥视:返回堆栈的栈顶元素(不删除)
  • isEmpty:检查堆栈是否为空。
  • isFull:检查堆栈是否已经满了。 

栈的存储表示方法:

  • 顺序栈:利用顺序存储结构实现的栈(①利用一组地址连续的存储单元一次存放从栈底到栈顶的数据元素;②附设指针top指向栈顶元素的位置,base指针指向栈底元素位置;③采用动态分配原则)  

    • 初始化:为顺序栈分配一个数组空间(stacksize,栈的最大容量),base和top同时指向栈底,表示空栈。
    • 入栈:判断栈是否已满,满则报错,否则将元素压入栈顶,top加一。
    • 出栈:判断栈是否为空,空则报错,否则将top减一,栈元素出栈。  
  • 链栈:采用链式存储结构实现的栈,通常用单链表表示(无头节点)。 

    • 初始化:构造一个空栈,无头结点,将栈顶指针置为空。
    • 入栈:为新的栈顶元素分配结点空间,将新结点插入到栈顶,修改栈顶指针。 
    • 出栈:判断栈是否为空,空则报错,否则取栈顶元素,修改栈顶指针,释放原栈顶元素空间。

顺序栈与链栈的优缺点:

  • 顺序栈受到最大空间容量的限制,在满员时扩容工作量较大,在无法估计栈可能达到的最大容量时应使用链栈。   

栈的应用:

  • 表达式评估。
  • 递归编程中实现函数调用。    

入栈

入栈操作

出栈

出栈操作

实现一个栈

栈主要包含两个操作,入栈和出栈,也就是在栈顶插入一个数据和从栈顶删除一个数据。栈既可以用数组来实现,也可以用链表来实现。用数组实现的栈,我们叫作顺序栈,用链表实现的栈,我们叫作链式栈。 根据上一节我们实现的数组代码我们来实现一个自己的栈。

栈的抽象接口

public interface Stack<E> {

    /**
     * 栈的容量
     * @return
     */
    int getSize();

    /**
     * 栈是否为空
     * @return
     */
    boolean isEmpty();

    /**
     * 向栈中添加元素
     * @param e
     */
    void push(E e);

    /**
     * 向栈中取出元素
     * @return
     */
    E pop();

    /**
     * 查看栈顶的元素
     * @return
     */
    E peek();
}

数组栈

public class ArrayStack<E> implements Stack<E> {

    private E[] data;

    private int size;

    public ArrayStack(){
        this(10);
    }

    public ArrayStack(int capacity){
        this.data = (E[]) new Object[capacity];
        this.size = 0;
    }

    @Override
    public int getSize() {
        return size;
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * 获取栈的容量
     * @return
     */
    public int getCapacity(){
        return data.length;
    }

    @Override
    public void push(E e) {
        addLast(e);
    }

    @Override
    public E pop() {
        return removeLast();
    }

    @Override
    public E peek() {
        return get(size - 1);
    }

    /**
     * 获取指定索引位置的值
     * @param index
     * @return
     */
    private E get(int index){
        if(index < 0 || index >= size){
            throw new IllegalArgumentException("Get failed. index is illegal.");
        }
        return data[index];
    }

    /**
     * 数组尾部新增元素
     * @param e
     */
    private void addLast(E e){
        add(size, e);
    }

    /**
     * 在指定位置插入元素
     * @param index
     * @param e
     */
    private void add(int index, E e){
        if(index < 0 || index > size){
            throw new IllegalArgumentException("AddLast failed. require index >=0 and index <= size");
        }
        if(size == data.length){
            //扩容
            resize(2 * data.length);
        }

        for(int i = size - 1; i >= index; i --){
            data[i + 1] = data[i];
        }
        data[index] = e;
        size ++;
    }

    /**
     * 数组扩容
     * @param newCapacity
     */
    private void resize(int newCapacity){
        E[] newData = (E[])new Object[newCapacity];
        for (int i = 0; i < size; i++) {
            newData[i] = data[i];
        }
        data = newData;
    }

    /**
     * 删除栈中最后一个元素
     * @return
     */
    private E removeLast(){
        return remove(size - 1);
    }

    /**
     * 删除栈数组中index位置的元素, 并返回删除的元素
     * @param index
     * @return
     */
    private E remove(int index){
        if(index < 0 || index >= size){
            throw new IllegalArgumentException("Remove failed. index is illegal.");
        }
        E ret = data[index];
        for (int i = index; i < size - 1; i++) {
            data[i] = data[i + 1];
        }
        size --;
        data[size] = null;
        if(size == data.length / 4 && data.length / 2 != 0){
            //当数组长度缩小为原数组的4分之一的时候才进行数组的缩容,
            //缩小为原数组的2分之一,预留空间,防止有数据添加导致扩容浪费性能
            resize(data.length / 2);
        }
        return ret;
    }

    @Override
    public String toString(){
        StringBuilder sb = new StringBuilder();
        sb.append("Stack: ");
        sb.append("[");
        for (int i = 0; i < size; i++) {
            sb.append(data[i]);
            if(i != size - 1){
                sb.append(", ");
            }
        }
        sb.append("] top");
        return sb.toString();
    }
}

链表栈

public class LinkedListStack<E> implements Stack<E> {

    private class Node<E>{
        public E e;

        public Node<E> next;

        public Node(E e){
            this.e = e;
        }

        @Override
        public String toString() {
            return e.toString();
        }
    }

    private Node<E> dummyHead;

    private Integer size;

    public LinkedListStack(){
        dummyHead = new Node<>(null);
        size = 0;
    }

    @Override
    public int getSize() {
        return size;
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    @Override
    public void push(E e) {
        Node<E> node = new Node<>(e);
        node.next = dummyHead.next;
        dummyHead.next = node;
        size++;
    }

    @Override
    public E pop() {
        if(isEmpty()){
            throw new IllegalArgumentException("Cannot pop from an empty stack.");
        }
        Node<E> prev = dummyHead;
        Node<E> retNode = prev.next;
        prev.next = retNode.next;
        retNode.next = null;
        size -- ;
        return retNode.e;
    }

    @Override
    public E peek() {
        if(isEmpty()){
            throw new IllegalArgumentException("Cannot pop from an empty stack.");
        }
        return dummyHead.next.e;
    }

    @Override
    public String toString(){
        StringBuilder sb = new StringBuilder();
        sb.append("Stack: top ");
        Node<E> cur = dummyHead.next;
        while (cur != null){
            sb.append(cur + "->");
            cur = cur.next;
        }
        sb.append("NULL");
        return sb.toString();
    }
}

栈应用

函数应用

操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成“栈”这种结构, 用来存储函数调用时的临时变量。每进入一个函数,就会将临时变量作为一个栈帧入栈,当被调用函数执行完成,返回之后,将这个函数对应的栈帧出栈。

栈的应用函数调用

表达式求值

我将算术表达式简化为只包含加减乘除四则运算,比如:34+13*9+44-12/3。对于这个四则运算编译器就是通过两个栈来实现的。其中一个保存操作数的栈,另一个是保存运算符的栈。我们从左向右遍历表达式,当遇到数字,我们就直接压入操作数栈;当遇到运算符,就与运算符栈的栈顶元素进行比较。

 

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

栈的表达式

括号匹配

我们假设表达式中只包含三种括号,圆括号 ()、方括号 [] 和花括号{},并且它们可以任意嵌套。比如,{[{}]}或 [{()}([])] 等都为合法格式,而{[}()] 或 [({)] 为不合法的格式。在给你一个包含三种括号的表达式字符串。我们用栈来保存未匹配的左括号,从左到右依次扫描字符串。当扫描到左括号时,则将其压入栈中;当扫描到右括号时,从栈顶取出一个左括号。如果能够匹配,比 如“(”跟“)”匹配,“[”跟“]”匹配,“{”跟“}”匹配,则继续扫描剩下的字符串。如果扫描的过程中,遇到不 能配对的右括号,或者栈中没有数据,则说明为非法格式。当所有的括号都扫描完成之后,如果栈为空,则说明字符串为合法格式;否则,说明有未匹配的左括号,为非法格式。

/**
 * @Author: huangyibo
 * @Date: 2021/12/26 18:58
 * @Description:
 * 给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。
 *
 * 有效字符串需满足:
 *
 * 左括号必须用相同类型的右括号闭合。
 * 左括号必须以正确的顺序闭合。
 * 注意空字符串可被认为是有效字符串。
 */
public class LeetCodeQuestion {

    public boolean isValid(String s) {
        Stack<Character> stack = new Stack<Character>();
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if(c == '(' || c == '{' || c == '{'){
                stack.push(c);
            }else {
                if(stack.isEmpty()){
                    return false;
                }
                Character pop = stack.pop();
                if(c == ')' && pop != '('){
                    return false;
                }
                if(c == '}' && pop != '{'){
                    return false;
                }
                if(c == ']' && pop != '['){
                    return false;
                }
            }
        }
        //如果栈里面还有字符的话,那么也不行,因为意味着还有字符没办法匹配
        return stack.isEmpty();
    }
}

参考: https://blog.csdn.net/weixin_39084521/article/details/89820132

https://www.cnblogs.com/smallzhen/p/14152557.html