佇列和棧的深度教學
佇列和棧(自己動手寫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() {
}
}
}