1. 程式人生 > >佇列和棧的深度教學

佇列和棧的深度教學

佇列和棧(自己動手寫API系列一)

前言: 我的建議就是學完什麼真正可以讓你有收穫的東西 寫下來

記錄下

------------------------------------------------------------------------------------

首先是佇列和棧 我這裡用的是JAVA語言 因為馬上大二了

也不能只用C寫一些東西了 摘自<<"Thinking in java">>一書 一

個API 應該首先放的是成員變數 然後是public 的 方法 使用者讀

完public就不用在往下讀了

------------------------------------------------------------------------------------

 

一.佇列(Queue)

 

佇列是什麼? 是一種資料結構 對,但是這種資料結構在日常生活中經常用到

比如你排隊的時候 你總要有一個先來後到.

就是你先來了 你買完東西 你就先 離開了

對應到資料 你先進來 你就要 先出去.

 

接下來讓我們看一下佇列的方法摘要

public class Queue<Item>//類的宣告 Item的泛型

 

public int size() //返回佇列個數大小

 

public boolean isEmpty() //判斷當前是不是空佇列 是的話返回true

 

public void enqueue(Item Date)//向佇列新增一個數據

 

public Item dequeue()//向佇列刪除一個數據 並返回

 

public QueueLinkIterator Iterator() //返回自己手寫的一個迭代器

 

private class QueueLinkIterator implements Iterator<Item>

//內部類實現了迭代器

 

private class Node //內部類 連結串列的結點

 

好了 方法基本上看完了這裡我們選擇連結串列實現. 在JAVA中寫連結串列要比C語言簡單的多

 

想一想你生活中的排隊.

如果又有新人來排隊是不是排到了隊尾??

隊首的人處理完了 隊首的人先走?

所以我們必須得要有一個隊首 和 隊尾對嗎?

隊首負責出人 隊尾負責進人 你發現這樣就很簡單.

先來看一下我們連結串列結點的定義

private class Node {
        Item Date;
        Node next;
        Node() {}
        Node(Item Date, Node next) {
            this.Date = Date;
            this.next = next;
        }
    }

因為這是一個內部類 我們並不想讓外界知道所以用private 修飾

那兩個構造方法其實也可以不寫 我們一會說.

然後就是讓我們看一下成員變數

    private Node first; //隊首的結點

    private Node last; //隊尾的結點

    private int N = 0; // 個數

    private int number = 0;//這個是運算元 我們最後講

先來看 size() 和 isEmpty() 兩個超簡單的方法

size 代表的是什麼? 就是你的個數嘛 不久是你成員的N?

 public int size() { return N; }

那isEmpty()判斷的是你是不是空佇列 是返回true 想想這個條件 也很簡單

就是判斷隊首的位置有沒有人就行了

public boolean isEmpty() { return first == null; }

這兩個簡單的操作基本上搞定了.

然後就是進隊和出隊了.

我們先來講進隊吧.

public void enqueue(Item Date) //Item 是你要傳入的資料型別

讓我們看一場圖

我們第一個結點是這個樣子的

因為只有一個結點 所以 他既是頭結點又是尾結點

然後又來了一個結點

因為第一個結點 我們現在first 和last 都是他 但是因為我們進隊都是隊尾進隊

所以讓last的下一個等於這個進隊的人 然後讓last變成新進的人

之後的first都不動了

 

 

好了 讓我們看看怎麼實現

首先讓一個臨時結點儲存last結點

然後讓last變成新的結點

讓臨時結點的下一個指向了last 完成

        Node temp = last;
        last = new Node(Date, null);
        temp.next = last;

 

讓我們再看一下 Node的構造

 

Node(Item Date, Node next) {
            this.Date = Date;
            this.next = next;
        }

其實你也完全可以這樣寫

        Node temp = last;
        last = new Node();
        last.Date = Date;
        last = null;

看你個人喜好 我就用第一種方法了

然後 你想想怎麼放置頭結點呢? 是不是 這個佇列是空的

才讓first結點指向新結點?

public void enqueue(Item Date) {
        Node temp = last;
        last = new Node(Date, null);
        if(isEmpty()) first = last;
        else temp.next = last;
        N++; number++; //數量++ 運算元++
    }

OK 進隊操作完成 是不是其實很簡單的.

其實出隊更簡單.... 

出隊就是返回隊首的資料 然後讓第一個變成第二個

也就是讓隊首那個人出去 第二個人補上來變成隊首

first = first.next;

然後進隊的特殊處就是隊首 出隊也一樣 你一直在用隊首做操作

萬一這個隊沒人了呢? 想一想 你的隊尾還一直保持著最後一個人呢

所以 當隊為空 讓隊尾 變成 null就好了

if(isEmpty()) last = null;

public Item dequeue() {
        Item temp = first.Date;
        first = first.next;
        if(isEmpty()) last = null;
        N--; number++;
        return temp;
    }

就是這樣 出隊也完成了.是不是特別簡單?

public QueueLinkIterator Iterator()

最後這個公用的方法其實也特別簡單

public QueueLinkIterator Iterator() { return new QueueLinkIterator(); }

感覺自己想不想受騙了 哈哈哈

不鬧了 還是來看一下 實現的這個迭代器吧.

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

 

因為這個迭代器也就是支援先進先出的輸出 所以 我只要在迭代器中拿到隊首就好

 

private class QueueLinkIterator implements Iterator<Item> {
    private Node first = Queue.this.first;
}

內部類訪問外部類 要引用類名 不能用super

這點注意下 super引用的是這個內部類的父類.

我這裡因為沒有任何繼承 所以是Object

用super引用的就是Object類

 

內部類的好處就是對你外部類完全是隱藏的.而我可以拿你的東西

迭代器有一個hasNext()功能 判斷還有沒有值

其實 這裡的實現也特別簡單

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

這裡注意一下就是 這裡的first 是內部類中的 不是 Queue中的frist

因為我不希望在內部類中去改變外部類.

然後下面我想說說 之前說的number 運算元的問題

我們只有在進隊和出隊的時候讓運算元++了

而且都是++ 這裡我們要的是什麼?

我們用系統API的迭代器的時候遍歷時刪除 修改 或者新增物件了

都會丟擲個異常

其實 這裡也就是這樣的一個原因 所以 我在外部類中加個運算元

然後在內部中獲得了這個運算元而且一旦得到我就不希望改變了

所以我在next的時候判斷一下 我在建立物件的時候得到的這個運算元

和你當前的運算元是不是相等的 不一樣我就拋異常.

所以我們就在 內部的迭代類中有這樣一個欄位, 你也可以加final

private int key = number;

然後看一下next 的程式碼 其實 也特別簡單

public Item next() {
            if (key != number) throw new UnsupportedOperationException();
            Item temp = first.Date;
            first = first.next;
            return temp;
        }

還是要注意 這裡的frist 沒有加任何關鍵字 預設是內部類自己的

實現了Iterator藉口本來還要寫remove 方法 但是我們這裡沒必要

private class QueueLinkIterator implements Iterator<Item> {

        private int key = number;

        private Node first = Queue.this.first;

        @Override
        public boolean hasNext() {
            return first != null;
        }

        @Override
        public Item next() {
            if (key != number) throw new UnsupportedOperationException();
            Item temp = first.Date;
            first = first.next;
            return temp;
        }

        @Override
        public void remove() {

        }
    }

這就是內部類的程式碼了

 

完整程式碼如下:

public class Queue<Item> {

    private Node first;

    private Node last;

    private int N = 0;

    private int number = 0;

    private class Node {
        Item Date;
        Node next;
        Node() {}
        Node(Item Date, Node next) {
            this.Date = Date;
            this.next = next;
        }
    }

    public boolean isEmpty() { return first == null; }

    public int size() { return N; }

    public void enqueue(Item Date) {
        Node temp = last;
        last = new Node(Date, null);
        if(isEmpty()) first = last;
        else temp.next = last;
        N++; number++;
    }

    public Item dequeue() {
        Item temp = first.Date;
        first = first.next;
        if(isEmpty()) last = null;
        N--; number++;
        return temp;
    }

    public QueueLinkIterator Iterator() { return new QueueLinkIterator(); }

    private class QueueLinkIterator implements Iterator<Item> {

        private int key = number;

        private Node first = Queue.this.first;

        @Override
        public boolean hasNext() {
            return first != null;
        }

        @Override
        public Item next() {
            if (key != number) throw new UnsupportedOperationException();
            Item temp = first.Date;
            first = first.next;
            return temp;
        }

        @Override
        public void remove() {

        }
    }

}

 

 

二.棧

 

棧是一種先進後出的結構 就比如你的箱子

我現在有三本書 JAVA書 C++書 PHP書

一開始 箱子為空

我先放入了 JAVA書 

然後依次放入C++書和PHP書

你看見了箱子大小了吧.

我現在又想把JAVA書取出來怎麼辦.. 手伸不進去.

難道你要把箱子撕爛? 太野蠻了吧??

我們是不是要先把PHP書和 C++書依次先拿出來??

所以 JAVA書先進去 想拿出來 他是最後才出來

棧就是這樣的一種結構 先進後出

---------------------------------------------------------------------------------------

棧的方法摘要:

---------------------------------------------------------------------------------------

public class Stack<Item> 類宣告

 

Stack()//無參構造方法

 

Stack(int size)//指定大小的構造 這裡我們用陣列的 待會寫一版連結串列的

 

public boolean isEmpty()//熟悉嗎? 返回棧是不是空 是的話 返回 true

 

public int size()// 返回棧的大小

 

public boolean push(Item Date)//壓棧 也就是你放書的動作 

這裡的返回值也可以寫void 為什麼 待會說

 

public Item pop()//彈棧 也就是你拿書的動作

 

public Iterator<Item> iterator() //返回該棧的迭代器

 

private class StackArrayIterator implements Iterator<Item>//自己寫的迭代器

 

private void resize(int max) //改變陣列的大小 為了高效利用陣列空間

 

然後看一下 該類的成員變數

 

private Item[] elements; // 該泛型的陣列 


    private int size; 陣列空間大小


    private int number = 0; 運算元

 

    private int N = 0; 有多少資料

這裡的成員方法就比之前的簡單多了吧?

然後兩個構造方法 一個不帶引數 預設開10個空間的棧

帶引數 指定大小的空間

  Stack() {
        size = 10;
        elements = (Item[])new Object[size];
    }

    Stack(int size) {
        this.size = size;
        elements = (Item[])new Object[this.size];
    }

然後最簡單的兩個方法 isEmpty() 和 size() 

public boolean isEmpty() { return N == 0; }

    public int size() { return N; }

是不是簡單的不得了?

因為接下來有很多的操作 我在上面都講了 所以忘記了 上去看一下

先來看壓棧的操作push()

因為是陣列版本的 我們要保證陣列有一個動態增長 

所以這裡我們先看一下resize() 這個 改變陣列大小的函式

 private void resize(int max) {
        this.size = max;
        Item[] temp = (Item[])new Object[this.size];
        for (int i = 0; i < N; i++) {
            temp[i] = elements[i];
        }
        elements = temp;
    }

這裡我們開一個 指定大小的陣列 然後 把你原本的elements 陣列的資料複製進去

然後讓elements指向temp的引用

關於會不會多開空間呀 這個你放心 JAVA的虛擬機器有足夠好的機制幫你釋放垃圾

迴歸正題 push()操作

首先我們判斷 我們放入棧的個數 有沒有 超過 棧的大小

如果你學過其他語言 應該知道棧溢位這一危險詞吧?

所以 我們得判斷一下

    public boolean push(Item Date) {
        if(N == size) resize(size<<1);
}

解釋一下size<<1 就是size 的二進位制左移一位

其實就相當於乘2

你發現 N 到上線了 把這個陣列擴容為2倍

接下來就簡單了

public boolean push(Item Date) {
        if(N == size) resize(size<<1);
        elements[N++] = Date;
        number++;
        return true;
    }

還記得我之前說過為什麼說這裡的返回值可以寫void了吧

number是運算元別忘了

壓棧完了 彈棧怎麼做?

我們現在依次把資料壓入棧中了.

棧頂是什麼? 其實就是elements[N - 1]

所以 彈棧直接用N做操作就可以

public Item pop() {
        Item temp = elements[--N];
        elements[N] = null;
        if (N > 0 && N < size/4) resize(size>>1);
        number++;
        return temp;
    }

其實也特別簡單

把那個資料儲存起來 然後讓那個資料 變成null就好

然後解釋下後面的if() N>0好理解

N<size/4 就是 你現在這個棧都還沒用到我大小的1/4

那豈不是太浪費空間? 

我就給你給小點 size>>1就是右移一位 相當於除2

然後 後面number運算元++就好

然後就剩下迭代器的方法了

public Iterator<Item> iterator() { return new StackArrayIterator(); }

是不是很簡單 哈哈 還是看一下私有方法把 有了之前的基礎 再看接下來的方法相信你會輕鬆多

我先直接全部扔出來 你看一下

private class StackArrayIterator implements Iterator<Item> {
        private int n = N;

        private int key = number;

        public boolean hasNext() { return n > 0; }

        public Item next() {
            if(key != number) throw new UnsupportedOperationException();
            return elements[--n];
        }

        @Override
        public void remove() {

        }
    }

裡面的成員方法 拿的是你現在棧數量的大小 只要n>0的代表你還有資料

同樣的 key 獲取 你外部類中的運算元

我在遍歷的時候發現 兩個不相等 就丟擲異常

然後返回elements[--n] 就好了

好了 陣列版的講完了 再講個連結串列版本

和陣列版本不同的就是沒有那麼多構造方法 

然後成員變數不同

先看一下連結串列版的成員變數

 

private Node first; //頭結點


    private int N = 0; // 個數


    private int number = 0; //運算元
    


    private class Node { //內部類連結串列結點
        Item Date;
        Node next;
        Node(Item Date, Node next) {
            this.Date = Date;
            this.next = next;
        }
        Node() {}
    }

然後內部類的兩個兩個構造我不講應該可以吧.

因為其實和那個佇列的是一樣的

然後這裡我只講不同的push() 和 pop() 兩個方法

看一下圖 我用 integer型別做例子了

然後 又來了一個5

然後又來了個 4

也就是說讓first 一直維持你最新建立的.

我們的操作就是讓一個temp 保持之前的first

也就是比如你只有5 3 結點 然後又來了個4

你現在的first 本來在5上面

然後 你用一個臨時變數temp 儲存了 first 也就是5的結點

4結點來了 你讓first 結點變成了4

讓first的下一個連線temp

也就是4結點下一個連線5 是不是連起來了?

 public void push(Item Date) {
        Node temp = first;
        first = new Node(Date,temp);
        N++;
        number++;
    }

然後就是這個樣子

壓棧完了讓運算元++

然後看最後

我想拿3結點 是不是先拿4 然後 拿5

那就先返回4 然後 first 往後移就可以了對嗎?

public Item pop() {
        Item temp = first.Date;
        first = first.next;
        N--;
        number++;
        return temp;
    }

炒雞簡單對嗎?

迭代器的話我就直接扔了

private class StackLinkIterator implements Iterator<Item> {

        private Node first = Stack.this.first;
        
        private int key = number;

        @Override
        public boolean hasNext() {
            return first != null;
        }

        @Override
        public Item next() {
            if ( key != number ) throw new UnsupportedOperationException();
            Item temp = first.Date;
            first =  first.next;
            return temp;
        }

        @Override
        public void remove() {

        }
    }

你有沒有發現其實這裡的next其實也pop()幾乎一樣?

 

完整程式碼:


import java.util.Iterator;

public class Stack<Item> {

    private Node first;

    private int N = 0;

    private int number = 0;


    private class Node {
        Item Date;
        Node next;
        Node(Item Date, Node next) {
            this.Date = Date;
            this.next = next;
        }
        Node() {}
    }

    public boolean isEmpty() { return first == null; }

    public int size() { return N; }

    public void push(Item Date) {
        Node temp = first;
        first = new Node(Date,temp);
        N++;
        number++;
    }

    public Item pop() {
        Item temp = first.Date;
        first = first.next;
        N--;
        number++;
        return temp;
    }

    public Iterator<Item> iterator() { return new StackLinkIterator(); }

    private class StackLinkIterator implements Iterator<Item> {

        private Node first = Stack2.this.first;

        private int key = number;

        @Override
        public boolean hasNext() {
            return first != null;
        }

        @Override
        public Item next() {
            if ( key != number ) throw new UnsupportedOperationException();
            Item temp = first.Date;
            first =  first.next;
            return temp;
        }

        @Override
        public void remove() {

        }
    }

}