1. 程式人生 > >JAVA資料結構和演算法:第三章(棧和佇列)

JAVA資料結構和演算法:第三章(棧和佇列)

棧是限制僅在一個位置上進行插入和刪除的線性表。允許插入和刪除的一端為末端,稱為棧頂。另一端稱為棧底。不含任何資料元素的棧稱為空棧。棧又成為後進先出(LIFO)表,後進入的元素最先出來。

首先,棧是一個線性表,元素之間具有線性關係,即前驅後繼關係,其次,它是一種特殊的線性表,只能在表尾進行插入和刪除操作。棧的插入操作,叫作進棧(push),刪除操作,叫作出棧(pop).

這裡寫圖片描述

由於棧是一個線性表,所以我們上一章說的順序結構和鏈式結構對棧來說同樣適用。

棧的實現

棧的順序結構儲存其實是線性表結構儲存的簡化,我們稱為順序棧。我們前面知道,順序結構線性表是用陣列來實現的,那麼對於棧這種只能在棧頂進行插入和刪除的結構來說,陣列的哪一端作棧底比較好呢?沒錯,讓陣列下標為0的一端作棧底比較好,這樣在棧頂進行刪除的話其他元素不用移動。

我們在設計棧時,一般定義一個top變數來指向棧頂元素,標識棧頂元素在陣列中的位置,top可以來回移動,但是不能超出棧的長度,例如:當棧中有一個元素時,top應該為0。因為經常把判斷空棧的條件設定為top=-1。

這裡寫圖片描述

實現順序結構的棧

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

規則:從左到右遍歷表示式的每個數字和符號,遇到數字就輸出,若是符號,則判斷其與棧頂符號的的優先順序,是右括號或者優先順序低於棧頂符號的,則棧頂符號依次出棧並輸出,並將當前符號入棧,一直到輸出最終字尾表示式為止。

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

看完兩個應用之後,是不是覺得棧在計算機中有很大的作用,前人的智慧真的是令人讚歎。

佇列

佇列是隻允許在一端進行插入操作,而在另一端進行刪除操作的線性表。佇列是一種先進先出(FIFO)的線性表,允許插入的一端稱為隊尾,允許刪除的一端稱為隊頭。

這裡寫圖片描述

順序迴圈佇列的實現

線性表有順序儲存和鏈式儲存,佇列作為一種特殊的線性表,也同樣存在這兩種儲存方式。

我們先來看看佇列的順序結構,假設我們佇列有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時佇列滿。

方法二是當佇列滿時,我們修改條件,我們保留一個元素空間,也就是說,佇列滿時陣列中還有一個保留單元。

這裡寫圖片描述

出現上圖這種情況,我們就認為佇列滿了,可是我們怎麼判斷呢?由於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 + ")";
        }
}