數據結構與算法(三),棧與隊列

轉載請註明出處:http://www.cnblogs.com/wangyingli/p/5931782.htmlhtml

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

1、棧

一、基本概念

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

抽象數據類型:算法

棧同線性表同樣,通常包括插入、刪除等基本操做。其基於泛型的API接口代碼以下:數組

public interface Stack<E> {

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

棧的實現一般有兩種方式:數據結構

  • 基於數組的實現(順序存儲)
  • 基於鏈表的實現(鏈式存儲)

二、棧的順序存儲結構

棧的順序存儲結構實際上是線性表順序存儲結構的簡化,咱們能夠簡稱它爲「順序棧」。其存儲結構以下圖:ide

實現代碼以下:函數

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()操做有時會調整數組大小,這項操做的耗時和棧的大小成正比測試

三、兩棧共享空間

用一個數組來存儲兩個棧,讓一個棧的棧底爲數組的始端,即下標爲0,另外一個棧的棧底爲數組的末端,即下標爲 n-1。兩個棧若增長元素,棧頂都向中間延伸。其結構以下:

這種結構適合兩個棧有相同的數據類型而且空間需求具備相反的關係的狀況,即一個棧增加時另外一個棧縮短。如,買股票,有人買入,就必定有人賣出。

代碼:

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)+" ");
        }
    }
}

四、棧的鏈式存儲結構

棧的鏈式存儲結構,簡稱鏈棧。爲了操做方便,通常將棧頂放在單鏈表的頭部。一般對於鏈棧來講,不須要頭結點。

其存儲結構以下圖:

代碼實現以下:

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)。

缺點:每一個元素都有指針域,增長了內存的開銷。

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

五、棧的應用——遞歸

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

遞歸從狹義上來說,指的是計算機科學中(也就是像各位程序猿都熟悉的那樣),一個模塊的程序在其內部調用自身的技巧。若是咱們把這個效果視覺化就成爲了「德羅斯特效應」,即圖片的一部分包涵了圖片自己。

以下面這張圖,「先有書仍是先有封面 ?」

咱們把一個直接調用自身或經過一系列語句間接調用自身的函數,稱爲遞歸函數。每一個遞歸函數必須至少有一個結束條件,即不在引用自身而是返回值退出。不然程序將陷入無窮遞歸中。

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

\[ F(n)=\left\{ \begin{array}{lcl} 0, &{n = 0}\\ 1, &{n = 1}\\ F(n-1) + F(n-2),&{n > 1}\\ \end{array} \right. \]

遞歸實現:

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;
}

迭代與遞歸的區別:

  • 迭代使用的是循環結構,遞歸使用的是選擇結構。
  • 遞歸能使程序的結構更清晰、簡潔,更容易令人理解。可是大量的遞歸將消耗大量的內存和時間。

編譯器使用棧來實現遞歸。在前行階段,每一次遞歸,函數的局部變量、參數值及返回地址都被壓入棧中;退回階段,這些元素被彈出,以恢復調用的狀態。

2、隊列

一、基本概念

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

抽象數據類型:

隊列做爲一種特殊的線性表,它同樣包括插入、刪除等基本操做。其基於泛型的API接口代碼以下:

public interface Queue<E> {

    //隊列是否爲空
    boolean isEmpty();

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

    //入隊
    void enQueue(E element);

    //出隊
    E deQueue();
}

一樣的,隊列具備兩種存儲方式:順序存儲和鏈式存儲。

二、隊列的順序存儲結構

其存儲結構以下圖:

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

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

解決「假溢出」的辦法就是若數組未滿,但後面滿了,就從頭開始入隊。咱們把這種邏輯上首尾相連的順序存儲結構稱爲循環隊列

數組實現隊列的過程:

假設開始時數組長度爲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("出隊");
    }
}

三、隊列的鏈式存儲結構

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

存儲結構以下圖:

代碼以下:

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)。和線性表相同,在能夠肯定隊列長度的狀況下,建議使用循環隊列;沒法肯定隊列長度時使用鏈隊列。

3、總結

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

對於棧來講,若兩個棧數據類型相同,空間需求相反,則可使用共享數組空間的方法來實現,以提升空間利用率。對於隊列來講,爲避免插入刪除操做時數據的移動,同時避免「假溢出」現象,引入了循環隊列,使得隊列的基本操做的時間複雜度降爲 O(1)。

相關文章
相關標籤/搜索