資料結構與演算法(三),棧與佇列

weixin_34087301發表於2016-10-10

轉載請註明出處:http://www.jianshu.com/p/462b42344098

上一篇《資料結構與演算法(二),線性表》中介紹了資料結構中線性表的兩種不同實現——順序表與連結串列。這一篇主要介紹線性表中比較特殊的兩種資料結構——棧與佇列。首先必須明確一點,棧和佇列都是線性表,它們中的元素都具有線性關係,即前驅後繼關係。

目錄:

  • 一、棧

    • 1、基本概念
    • 2、棧的順序儲存結構
    • 3、兩棧共享空間
    • 4、棧的鏈式儲存結構
    • 5、棧的應用——遞迴
  • 二、佇列

    • 1、基本概念
    • 2、佇列的順序儲存結構
    • 3、佇列的鏈式儲存結構
  • 三、總結

一、棧

1、基本概念

(也稱下壓棧,堆疊)是僅允許在表尾進行插入和刪除操作的線性表。我們把允許插入和刪除的一端稱為棧頂(top),另一端稱為棧底(bottom)。棧是一種後進先出(Last In First Out)的線性表,簡稱(LIFO)結構。棧的一個典型應用是在集合中儲存元素的同時顛倒元素的相對順序

抽象資料型別:

棧同線性表一樣,一般包括插入、刪除等基本操作。其基於泛型的API介面程式碼如下:

public interface Stack<E> {

    //棧是否為空
    boolean isEmpty();
    //棧的大小
    int size();
    //入棧
    void push(E element);
    //出棧
    E pop();
    //返回棧頂元素
    E peek();
}

棧的實現通常有兩種方式:

  • 基於陣列的實現(順序儲存)
  • 基於連結串列的實現(鏈式儲存)

2、棧的順序儲存結構

棧的順序儲存結構其實是線性表順序儲存結構的簡化,我們可以簡稱它為「順序棧」。其儲存結構如下圖:

2959073-1e166da9b9a83a09.png
棧的順序儲存結構

實現程式碼如下:

import java.util.Iterator;
/**
 * 能動態調整陣列大小的棧
 */
public class ArrayStack<E> implements Iterable<E>, Stack<E> {

    private E[] elements;
    private int size=0;
    
    @SuppressWarnings("unchecked")
    public ArrayStack() {
        elements = (E[])new Object[1]; //注意:java不允許建立泛型陣列
    }
    
    @Override public int size() {return size;}
    
    @Override public boolean isEmpty() {return size == 0;}

    //返回棧頂元素
    @Override public E peek() {return elements[size-1];}

    //調整陣列大小
    public void resizingArray(int num) {
        @SuppressWarnings("unchecked")
        E[] temp = (E[])new Object[num];
        for(int i=0;i<size;i++) {
            temp[i] = elements[i];
        }
        elements = temp;
    }

    //入棧
    @Override public void push(E element) {
        if(size == elements.length) {
            resizingArray(2*size);//若陣列已滿將長度加倍
        }
        elements[size++] = element;
    }

    //出棧
    @Override public E pop() {
        E element = elements[--size];
        elements[size] = null;     //注意:避免物件遊離
        if(size > 0 && size == elements.length/4) {
            resizingArray(elements.length/2);//小於陣列1/4,將陣列減半
        }
        return element;
    }

    //實現迭代器, Iterable介面在java.lang中,但Iterator在java.util中
    public Iterator<E> iterator() {
        return new Iterator<E>() {
            private int num = size;
            public boolean hasNext() {
                return num > 0;
            }
            public E next() {
                return elements[--num];
            }
        };
    }

    //測試
    public static void main(String[] args) {
        int[] a = {1,2,3,4,new Integer(5),6};//測試陣列
        ArrayStack<Integer> stack = new ArrayStack<Integer>();
        System.out.print("入棧順序:");
        for(int i=0;i<a.length;i++) {
            System.out.print(a[i]+" ");
            stack.push(a[i]);
        }
        System.out.println();
        System.out.print("出棧順序陣列實現:");
        //迭代
        for (Integer s : stack) {
            System.out.print(s+" ");
        }
    }
}

優點:

  1. 每項操作的用時都與集合大小無關
  2. 空間需求總是不超過集合大小乘以一個常數

缺點:push()和pop()操作有時會調整陣列大小,這項操作的耗時和棧的大小成正比

3、兩棧共享空間

用一個陣列來儲存兩個棧,讓一個棧的棧底為陣列的始端,即下標為0,另一個棧的棧底為陣列的末端,即下標為 n-1。兩個棧若增加元素,棧頂都向中間延伸。其結構如下:

2959073-b9cdeadb7e77293e.png
兩棧共享空間

這種結構適合兩個棧有相同的資料型別並且空間需求具有相反的關係的情況,即一個棧增長時另一個棧縮短。如,買股票,有人買入,就一定有人賣出。

程式碼:

public class SqDoubleStack<E> {

    private static final int MAXSIZE = 20;
    private E[] elements;
    private int leftSize=0;
    private int rightSize= MAXSIZE - 1;
    
    //標記是哪個棧
    enum EnumStack {LEFT, RIGHT}

    @SuppressWarnings("unchecked")
    public SqDoubleStack() {
        elements = (E[])new Object[MAXSIZE]; //注意:java不允許建立泛型陣列
    }
    

    //入棧
    public void push(E element, EnumStack es) {

        if(leftSize - 1 == rightSize)
            throw new RuntimeException("棧已滿,無法新增"); 
        if(es == EnumStack.LEFT) {
            elements[leftSize++] = element;
        } else {
            elements[rightSize--] = element;
        }
    }

    //出棧
    public E pop(EnumStack es ) {

        if(es == EnumStack.LEFT) {
            if(leftSize <= 0)
                throw new RuntimeException("棧為空,無法刪除"); 
            E element = elements[--leftSize];
            elements[leftSize] = null;     //注意:避免物件遊離
            return element;
        } else {
            if(rightSize >= MAXSIZE - 1)
                throw new RuntimeException("棧為空,無法刪除"); 
            E element = elements[++rightSize];
            elements[rightSize] = null;     //注意:避免物件遊離
            return element;
        }
    }

    //測試
    public static void main(String[] args) {
        int[] a = {1,2,3,4,new Integer(5),6};//測試陣列
        SqDoubleStack<Integer> stack = new SqDoubleStack<Integer>();
        System.out.print("入棧順序:");
        for(int i=0;i<a.length;i++) {
            System.out.print(a[i]+" ");
            stack.push(a[i], EnumStack.RIGHT);
        }
        System.out.println();
        System.out.print("出棧順序陣列實現:");
        //迭代
        for(int i=0;i<a.length;i++) {
            System.out.print(stack.pop(EnumStack.RIGHT)+" ");
        }
    }
}

4、棧的鏈式儲存結構

棧的鏈式儲存結構,簡稱鏈棧。為了操作方便,一般將棧頂放在單連結串列的頭部。通常對於鏈棧來說,不需要頭結點。

其儲存結構如下圖:

2959073-c4c988f2f8c51085.png
棧的鏈式儲存結構

程式碼實現如下:

import java.util.Iterator;
public class LinkedStack<E> implements Stack<E>, Iterable<E> {
    private int size = 0;
    private Node head = null;//棧頂

    private class Node {
        E element;
        Node next;
        Node(E element, Node next) {
            this.element = element;
            this.next = next;
        }
    }

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

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

    @Override public E peek() {return head.element;}

    @Override public void push(E element) {
        Node node = new Node(element, head);
        head = node;
        size++;
    }

    @Override public E pop() {
        E element = head.element;
        head = head.next;
        size--;
        return element;
    }
    //迭代器
    public Iterator<E> iterator() {
        return new Iterator<E>() {
            private Node current = head;

        public boolean hasNext() {
                return current != null;
            }

            public E next() {
                E element = current.element;
                current = current.next;
                return element;
            }
        };
    }

    public static void main(String[] args) {
        int[] a = {1,2,3,4,new Integer(5),6};//測試陣列
        LinkedStack<Integer> stack = new LinkedStack<Integer>();
        System.out.print("入棧順序:");
        for(int i=0;i<a.length;i++) {
            System.out.print(a[i]+" ");
            stack.push(a[i]);
        }
        System.out.println();
        System.out.print("出棧順序連結串列實現:");
        for (Integer s : stack) {
            System.out.print(s+" ");
        }
    }
}

注意:私有巢狀類(內部類Node)的一個特點是隻有包含他的類能夠直接訪問他的例項變數,無需將他的例項變數(element)宣告為public或private。即使將變數element宣告為private,外部類依然可以通過Node.element的形式呼叫。

優點:

  1. 所需空間和集合的大小成正比
  2. 操作時間和集合的大小無關
  3. 鏈棧的push和pop操作的時間複雜度都為 O(1)。

缺點:每個元素都有指標域,增加了記憶體的開銷。

順序棧與鏈棧的選擇和線性表一樣,若棧的元素變化不可預料,有時很大,有時很小,那最好使用鏈棧。反之,若它的變化在可控範圍內,使用順序棧會好一些。

5、棧的應用——遞迴

棧的一個最重要的應用就是遞迴。那麼什麼是遞迴呢?借用《哥德爾、艾舍爾、巴赫——集異璧之大成》中的話:

遞迴從狹義上來講,指的是電腦科學中(也就是像各位程式猿都熟悉的那樣),一個模組的程式在其內部呼叫自身的技巧。如果我們把這個效果視覺化就成為了「德羅斯特效應」,即圖片的一部分包涵了圖片本身。

如下面這張圖,「先有書還是先有封面 ?」

2959073-ed326020d3d366af.jpg
德羅斯特效應

我們把一個直接呼叫自身或通過一系列語句間接呼叫自身的函式,稱為遞迴函式。每個遞迴函式必須至少有一個結束條件,即不在引用自身而是返回值退出。否則程式將陷入無窮遞迴中。

一個遞迴的例子:斐波那契數列(Fibonacci)

2959073-bf59bdab97a4b72f.png
斐波那契數列

遞迴實現:

public int fibonacci(int num) {
    if(num < 2)
        return num == 0 ? 0 : 1;
    return fibonacci(num - 1) + fibonacci(num - 2);
}

迭代實現:

public int fibonacci(int num) {
    if(num < 2)
        return num == 0 ? 0 : 1;
    int temp1 = 0;
    int temp2 = 1;
    int result = 0;
    for(int i=2; i < num; i++) {
        result = temp1 + temp2;
        temp1 = temp2;
        temp2 = result;
    }
    return result;
}

迭代與遞迴的區別:

  • 迭代使用的是迴圈結構,遞迴使用的是選擇結構。
  • 遞迴能使程式的結構更清晰、簡潔,更容易使人理解。但是大量的遞迴將消耗大量的記憶體和時間。

編譯器使用棧來實現遞迴。在前行階段,每一次遞迴,函式的區域性變數、引數值及返回地址都被壓入棧中;退回階段,這些元素被彈出,以恢復呼叫的狀態。

二、佇列

1、基本概念

佇列是隻允許在一端進行插入操作,在另一端進行刪除操作的線性表。它是一種基於先進先出(First In First Out,簡稱FIFO)策略的集合型別。允許插入的一端稱為隊尾,允許刪除的一端稱為隊頭。

抽象資料型別:

佇列作為一種特殊的線性表,它一樣包括插入、刪除等基本操作。其基於泛型的API介面程式碼如下:

public interface Queue<E> {

    //佇列是否為空
    boolean isEmpty();

    //佇列的大小
    int size();

    //入隊
    void enQueue(E element);

    //出隊
    E deQueue();
}

同樣的,佇列具有兩種儲存方式:順序儲存和鏈式儲存。

2、佇列的順序儲存結構

其儲存結構如下圖:

2959073-63edc783dc0dfab1.png
佇列的順序儲存

與棧不同的是,佇列元素的出列是在隊頭,即下表為0的位置。為保證隊頭不為空,每次出隊後佇列中的所有元素都得向前移動,此時時間複雜度為 O(n)。此時佇列的實現和線性表的順序儲存結構完全相同,不在詳述。

若不限制佇列的元素必須儲存在陣列的前n個單元,出隊的效能就能大大提高。但這種結構可能產生「假溢位」現象,即陣列末尾元素已被佔用,如果繼續向後就會產生下標越界,而前面為空。如下圖:

2959073-712c4a842466da3c.png
假溢位

解決「假溢位」的辦法就是若陣列未滿,但後面滿了,就從頭開始入隊。我們把這種邏輯上首尾相連的順序儲存結構稱為迴圈佇列

陣列實現佇列的過程:

2959073-7c87cf5ecc34816e.png
迴圈佇列實現過程

假設開始時陣列長度為5,如圖,當f入隊時,此時陣列末尾元素已被佔用,如果繼續向後就會產生下標越界,但此時陣列未滿,將從頭開始入隊。當陣列滿(h入隊)時,將陣列的長度加倍。

程式碼如下:

import java.util.Iterator;
/**
 * 能動態調整陣列大小的迴圈佇列
 */
public class CycleArrayQueue<E> implements Queue<E>, Iterable<E> {
    private int size; //記錄佇列大小
    
    private int first; //first表示頭元素的索引
    private int last; //last表示尾元素後一個的索引
    private E[] elements;

    @SuppressWarnings("unchecked")
    public CycleArrayQueue() {
        elements = (E[])new Object[1];
    }
    
    @Override public int size() {return size;}
    @Override public boolean isEmpty(){return size == 0;}

    //調整陣列大小
    public void resizingArray(int num) {
        @SuppressWarnings("unchecked")
        E[] temp = (E[])new Object[num];
        for(int i=0; i<size; i++) {
            temp[i] = elements[(first+i) % elements.length];
        }
        elements = temp;
        first = 0;//陣列調整後first,last位置
        last =  size;
    }

    @Override public void enQueue(E element){
        //當佇列滿時,陣列長度加倍
        if(size == elements.length) 
            resizingArray(2*size);
        elements[last] = element;
        last = (last+1) % elements.length;//【關鍵】
        size++;
    }
    
    @Override public E deQueue() {
        if(isEmpty()) 
            return null;
        E element = elements[first];
        first = (first+1) % elements.length;//【關鍵】
        size--;
        //當佇列長度小於陣列1/4時,陣列長度減半
        if(size > 0 && size < elements.length/4) 
            resizingArray(2*size);
        return element;
    }

    //實現迭代器
    public Iterator<E> iterator() {
        return new Iterator<E>() {
            private int num = size;
            private int current = first;
            public boolean hasNext() {
                return num > 0;
            }
            public E next() {
                E element = elements[current++];
                num--;
                return element;
            }                
        };
    }

    public static void main(String[] args) {
        int[] a = {1,2,4,6,new Integer(10),5};
        CycleArrayQueue<Integer> queue = new CycleArrayQueue<Integer>();

        for(int i=0;i<a.length;i++) {
            queue.enQueue(a[i]);
            System.out.print(a[i]+" ");
        }    
        System.out.println("入隊");

        for (Integer s : queue) {
            System.out.print(s+" ");
        }
        System.out.println("出隊");
    }
}

3、佇列的鏈式儲存結構

佇列的鏈式儲存結構,其實就是線性表的單連結串列,只不過它只能尾進頭出而已,我們簡稱為「鏈佇列」。

儲存結構如下圖:

2959073-4792802b936f2bb4.png
佇列的鏈式儲存

程式碼如下:

import java.util.Iterator;
/**
 * 佇列的鏈式儲存結構,不帶頭結點的實現
 */
public class LinkedQueue<E> implements Queue<E>, Iterable<E> {
    private Node first; //頭結點
    private Node last; //尾結點
    private int size = 0;

    private class Node {
        E element;
        Node next;
        Node(E element) {
            this.element = element;
        }
    }
    
    @Override public int size() {return size;}
    @Override public boolean isEmpty(){return size == 0;}

    
    //入隊
    @Override public void enQueue(E element) {
        Node oldLast = last;
        last = new Node(element);
        if(isEmpty()) {
            first = last;//【要點】
        }else {
            oldLast.next = last;
        }
        size++;
    }
    //出隊
    @Override public E deQueue() {
        E element = first.element;
        first = first.next;
        size--;
        if(isEmpty()) 
            last = null;//【要點】
        return element;
    }
    //實現迭代器
    public Iterator<E> iterator() {
        return new Iterator<E>() {
            private Node current = first;

            public boolean hasNext() {
                return current != null;
            }

            public E next(){
                E element = current.element;
                current = current.next;
                return element;
            }
        };
    }

    public static void main(String[] args) {
        int[] a = {1,2,4,6,new Integer(10),5};
        LinkedQueue<Integer> queue = new LinkedQueue<Integer>();

        for(int i=0;i<a.length;i++) {
            queue.enQueue(a[i]);
            System.out.print(a[i]+" ");
        }    
        System.out.println("入隊");

        for (Integer s : queue) {
            System.out.print(s+" ");
        }
        System.out.println("出隊");
    }
}

迴圈佇列與鏈佇列,它們的基本操作的時間複雜度都為 O(1)。和線性表相同,在可以確定佇列長度的情況下,建議使用迴圈佇列;無法確定佇列長度時使用鏈佇列。

三、總結

棧與佇列,它們都是特殊的線性表,只是對插入和刪除操作做了限制。棧限定僅能在棧頂進行插入和刪除操作,而佇列限定只能在隊尾插入,在隊頭刪除。它們都可以使用順序儲存結構和鏈式儲存結構兩種方式來實現。

對於棧來說,若兩個棧資料型別相同,空間需求相反,則可以使用共享陣列空間的方法來實現,以提高空間利用率。對於佇列來說,為避免插入刪除操作時資料的移動,同時避免「假溢位」現象,引入了迴圈佇列,使得佇列的基本操作的時間複雜度降為 O(1)。

相關文章