栈是限制仅在一个位置上进行插入和删除的线性表。允许插入和删除的一端为末端,称为栈顶。另一端称为栈底。不含任何数据元素的栈称为空栈。栈又成为后进先出(LIFO)表,后进入的元素最先出来。

首先,栈是一个线性表,元素之间具有线性关系,即前驱后继关系,其次,它是一种特殊的线性表,只能在表尾进行插入和删除操作。栈的插入操作,叫作进栈(push),删除操作,叫作出栈(pop).

java 入栈出栈示例_java

由于栈是一个线性表,所以我们上一章说的顺序结构和链式结构对栈来说同样适用。

栈的实现

栈的顺序结构存储其实是线性表结构存储的简化,我们称为顺序栈。我们前面知道,顺序结构线性表是用数组来实现的,那么对于栈这种只能在栈顶进行插入和删除的结构来说,数组的哪一端作栈底比较好呢?没错,让数组下标为0的一端作栈底比较好,这样在栈顶进行删除的话其他元素不用移动。

我们在设计栈时,一般定义一个top变量来指向栈顶元素,标识栈顶元素在数组中的位置,top可以来回移动,但是不能超出栈的长度,例如:当栈中有一个元素时,top应该为0。因为经常把判断空栈的条件设置为top=-1。

java 入栈出栈示例_数据结构_02

实现顺序结构的栈

public class Stack<T> { 
    //存放数据的数组
    private Object[] elements;
    //标识栈顶元素在数组中的位置
    private int top;

    public Stack(int size) {
        elements=new Object[size];
        top=-1;
    }

    public Stack() {
        this(16);
    }

    //判断是否为空栈
    public boolean isEmpty() {
        return this.top==-1;
    }

    //进栈操作
    public void push(T obj) {
        if(obj==null) {
            return ;
        } 
        //数组扩容
        if(this.top==elements.length-1) {
            Object[] temp=new Object[elements.length*2];
            for(int i=0;i<elements.length;i++) {
                temp[i]=elements[i];
            }
            elements=temp;
        }
        top++;
        elements[top]=obj;
    }

    //出栈操作
    public T pop() {
        if(this.top==-1) {
            return null;
        }
        return (T) elements[top--];
    }

    //打印栈中元素
    public String toString() {
        String str="(";
        for(int i=this.top;i>=0;i--) {
            str=str+elements[i];
            if(i!=0) {
                str=str+",";
            }
        }
        return str+")";
    }
}

实现链式结构的栈

栈的链式存储结构,简称为链栈。

//创建结点类

public class StackNode<T> {
    //保存数据
    public T data;

    //地址域,引用后继结点
    public StackNode<T> next;

    //构造方法
    public StackNode(T data,StackNode<T> next){
        this.data=data;
        this.next=next;
    }

    public StackNode() {
        this(null,null);
    }

    public String toString() {
        return this.data.toString();
    }


}
//实现链栈

public class LinkedStack<T> {
    //栈顶结点
    private StackNode<T> top; 

    public LinkedStack() {
          this.top = null;
    }

    //判断是否为空栈
    public boolean isEmpty() {        
        return this.top == null;
    }

    //进栈操作
    public void push(T x) {
        //头插入,x结点作为新的栈顶结点
        if (x != null) { 
            //保持新结点为栈顶结点
            this.top = new StackNode(x, this.top);
        }
    } 

    //出栈操作
    public T pop() {
        if (this.top == null)
            return null;
        //取栈顶结点元素
        T temp = this.top.data;
        //删除栈顶结点
        this.top = this.top.next;
        return temp;
    } 
    //打印栈中元素
     public String toString() {
            String str = "(";
            for (StackNode<T> p = this.top; p != null; p = p.next) {
                str += p.data.toString();
                //不是最后一个结点时后加分隔符
                if (p.next != null) {
                    str += ", ";
                }
            }
            return str + ") ";
      }

}

我们前面实现了顺序结构的栈和链式结构的栈,可是大家应该有个很大的疑惑,有什么用啊,直接用线性表和链表不就可以了吗。接下来我们来看栈的常用应用。

栈的应用

递归

栈的很重要的一个应用就是递归的实现,递归在调用下一次函数的过程中,将函数的局部变量、参数、返回地址都压入栈中,当前面一层的函数执行完后,位于栈顶的局部变量、参数、返回地址被弹出,恢复该方法的调用时状态。

四则运算

在我们做四则运算时,小学老师告诉我们“先乘除,后加减,从左到右,先算括号内再算括号外”。
相信以各位的智商,算这种简单的四则运算不在话下。但是如果是计算机来做呢?让你编写一个程序来处理这种有四则运算,你会怎样去处理呢?仔细想一想,你就会发现这个问题似乎有一些棘手,乘除在加减后面我们要先计算乘除,碰到括号还要先计算括号里面的,这个看似简单的四则运算问题在计算机中变得复杂起来。

20世纪50年代,波兰科学家想到了一种不需要括号的后缀表达法,我们把它称为逆波兰。这种后缀表达法,非常巧妙地解决了程序进行四则运算的难题。

例如:9+(3-1)*3+10/2

后缀表达式为:9 3 1 - 3 * + 10 2 / +

规则:从左到右遍历表达式的每个数字和符号,遇到是数字就进栈,遇到符号就将处于栈顶的两个数字出栈进行运算,然后将运算结果压入栈,继续计算,直到算出最终结果。

这个计算的问题解决了,但是这个后缀表达式是怎么得出来的,如果让我们手动的将每个算式转换为逆波兰,再输入计算机,那我相信大多数人都会放弃计算机,还不如自己手算。所以我们得来解决一下怎么让计算机把我们正常的算式转换为逆波兰。

我们平时所用的标准四则运算表达式叫做中缀表达式,因为所有的运算符都在数字之间,现在我们来解决一下中缀表达式到后缀表达式的转换。

还是上面的例子:9+(3-1)*3+10/2

规则:从左到右遍历表达式的每个数字和符号,遇到数字就输出,若是符号,则判断其与栈顶符号的的优先级,是右括号或者优先级低于栈顶符号的,则栈顶符号依次出栈并输出,并将当前符号入栈,一直到输出最终后缀表达式为止。

java 入栈出栈示例_算法_03

java 入栈出栈示例_java 入栈出栈示例_04

java 入栈出栈示例_算法_05

java 入栈出栈示例_数据结构_06

java 入栈出栈示例_栈_07

看完两个应用之后,是不是觉得栈在计算机中有很大的作用,前人的智慧真的是令人赞叹。

队列

队列是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。队列是一种先进先出(FIFO)的线性表,允许插入的一端称为队尾,允许删除的一端称为队头。

java 入栈出栈示例_算法_08

顺序循环队列的实现

线性表有顺序存储和链式存储,队列作为一种特殊的线性表,也同样存在这两种存储方式。

我们先来看看队列的顺序结构,假设我们队列有n个元素,则需要创建一个大于n的数组,然后将队列中的元素存储在数组的前n个单元中,下标为0的一端为队头,当我们执行插入操作时,就是在队尾增加一个元素,时间复杂度为O(1),而当我们执行删除操作时,也就是下标为0的元素出列,这时候所有后面的元素都需要向钱移动,时间复杂度为O(n)。那么我们如何解决这一问题呢?

仔细想一下,为什么我们元素出队列一定要全部的向前移动?如果我们不限制队列的元素必须存储在数组的前n个单元这一条件,出队列的性能就会大大增加。也就是说,我们可不可以队头是可以变化的,队头出列了,下一个元素就变为了队头。这样不就解决了我们的问题么。

我们可以指定两个指针,一个指向队头head,一个指向队尾的下一个元素rear,如果head=rear那么不就说明该队列变为了空队列。但是这种方式还有一些问题,例如我们前面移除了好几个元素,但是后面添加的元素却超过了数组的容量怎么办? 我们可以将元素再从头开始存放。

我们把队列这种头尾相接的顺序存储结构称为循环队列。

可是这时又存在一个问题,我们前面说过head=rear时,证明为空队列,但当存满时,rear也等于head,那么如何判断当前的队列究竟是空队列还是满队列呢?有两个解决办法
方法一是我们设置一个标志变量flag,当head==rear且flag==0时队列为空,当head==head且flag==1时队列满。

方法二是当队列满时,我们修改条件,我们保留一个元素空间,也就是说,队列满时数组中还有一个保留单元。

java 入栈出栈示例_java_09

出现上图这种情况,我们就认为队列满了,可是我们怎么判断呢?由于rear可能比front大也可能小,如果我们条件是它们俩差值为1的话,那么可能是队列满也可能是还差了一圈。所以我们需要别的判断方法,假设当前队列长度为size,队列满的条件就是(rear+1)%size==front。

public class CircleQueue<T> { 
    //存放数据的数组
    private Object[] elements; 
    //指向头和尾元素后一个的坐标
    private int front,rear;

    //构造方法
    public CircleQueue(int length) {
        this.elements=new Object[length];
        this.front=0;
        this.rear=0;
    } 

    public CircleQueue() {
        this(32);
    }  

    //判断是否为空队列
    public boolean isEmpty() {
        return this.front==this.rear;
    }

    //入队列操作
    public void enqueue(T obj) {
        if(obj==null) {
            return;
        }

        //判断队列是否满
        if((this.rear+1)%this.elements.length==this.front) {
            Object[] temp = this.elements;
            // 重新申请一个容量更大的数组
            this.elements = new Object[temp.length * 2];
            int j = 0;
            // 按照队列元素顺序复制数组元素,从当前头开始,然后再循环回去复制
            for (int i = this.front; i != this.rear; i = (i + 1) % temp.length) {
                this.elements[j++] = temp[i];
            }
            this.front = 0;
            this.rear = j;
        }
        this.elements[this.rear] = obj;
        this.rear = (this.rear + 1) % this.elements.length;
    }
    //出队列操作
     public T dequeue() {
            // 若队列空返回null
            if (isEmpty())
                return null;
            // 取得队头元素
            T temp = (T) this.elements[this.front];
            this.front = (this.front + 1) % this.elements.length;
            return temp;
        }

        /**
         * 返回队列所有元素的描述字符串,形式为“(,)”,按照队列元素次序
         */
        @Override
        public String toString() {
            String str = "(";
            if (!isEmpty()) {
                str += this.elements[this.front].toString();
                int i = (this.front + 1) % this.elements.length;
                while (i != this.rear) {
                    str += ", " + this.elements[i].toString();
                    i = (i + 1) % this.elements.length;
                }
            }
            return str + ")";
        }

}

链式队列的实现

链式存储结构的队列空间肯定够,所以就不需要循环。

public class LinkedQueue<T> {
    //指向头结点和尾结点
    private Node<T> front,rear; 

    public LinkedQueue() {
        this.front=this.rear=null;
    } 

    //判断当前队列是否为空
    public boolean isEmpty() {
        return this.front == null && this.rear == null;
    }

    //进队列操作 
     public void enqueue(T x) {
            if (x == null)
                return;
            Node<T> q = new Node<T>(x, null);
            if (this.front == null) {
                this.front = q;
            } else {
                // 插入在队列之尾
                this.rear.next = q;
            }
            this.rear = q;
        }

        /**
         * 出队,返回队头元素,若队列空返回null
         */

        public T dequeue() {
            if (isEmpty())
                return null;
            // 取得队头元素
            T temp = this.front.data;
            // 删除队头节点
            this.front = this.front.next;
            //判断接下来是否还有元素
            if (this.front == null)
                this.rear = null;
            return temp;
        }

        /**
         * 返回队列所有元素的描述字符串,形式为“(,)” 算法同不带头结点的单链表
         */
        @Override
        public String toString() {
            String str = "(";
            for (Node<T> p = this.front; p != null; p = p.next) {
                str += p.data.toString();
                if (p.next != null) {
                    // 不是最后一个结点时后加分隔符
                    str += ", ";
                }
            }
            // 空表返回()
            return str + ")";
        }
}