摘要:在改進(jìn)前使用數(shù)組的一個(gè)缺點(diǎn)是必須聲明數(shù)組的大小,所以棧有確定的容量。待解決的問(wèn)題建立一個(gè)能夠增長(zhǎng)或者縮短到任意大小的棧。下邊的圖是觀察時(shí)間開(kāi)銷(xiāo)的另一種方式,表示了入棧操作需要訪問(wèn)數(shù)組的次數(shù)。
前言
上一篇:算法分析
下一篇:基本排序
本篇內(nèi)容主要是棧,隊(duì)列 (和包)的基本數(shù)據(jù)類(lèi)型和數(shù)據(jù)結(jié)構(gòu)
文章里頭所有的對(duì)數(shù)函數(shù)都是以 2 為底
關(guān)于性能分析,可能還是需要一些數(shù)學(xué)知識(shí),有時(shí)間可以回一下
在很多應(yīng)用中,我們需要維護(hù)多個(gè)對(duì)象的集合,而對(duì)這個(gè)集合的操作也很簡(jiǎn)單
對(duì)象的集合
操作:
insert -- 向集合中添加新的對(duì)象
remove -- 去掉集合中的某個(gè)元素
iterate -- 遍歷集合中的元素并對(duì)他們執(zhí)行某種操作
test if empty -- 檢查集合是否為空
做插入和刪除操作時(shí)我們要明確以什么樣的形式去添加元素,或我們要?jiǎng)h除集合中的哪個(gè)元素。
處理這類(lèi)問(wèn)題有兩個(gè)經(jīng)典的基礎(chǔ)數(shù)據(jù)結(jié)構(gòu):棧(stack) 和隊(duì)列(queue)
兩者的區(qū)別在于去除元素的方式:
棧:去除最近加入的元素,遵循后進(jìn)先出原則(LIFO: last in first out)。
插入元素對(duì)應(yīng)的術(shù)語(yǔ)是入棧 -- push;去掉最近加入的元素叫出棧 -- pop
隊(duì)列:去除最開(kāi)始加入的元素,遵循先進(jìn)先出原則(FIFO: first in first out)。
關(guān)注最開(kāi)始加入隊(duì)列的元素,為了和棧的操作區(qū)分,隊(duì)列加入元素的操作叫做入隊(duì) -- enqueue;去除元素的操作叫出隊(duì) -- dequeue
此篇隱含的主題是模塊式編程,也是平時(shí)開(kāi)發(fā)需要遵守的原則
模塊化編程這一原則的思想是將接口與實(shí)現(xiàn)完全分離。比如我們精確定義了一些數(shù)據(jù)類(lèi)型和數(shù)據(jù)結(jié)構(gòu)(如棧,隊(duì)列等),我們想要的是把實(shí)現(xiàn)這些數(shù)據(jù)結(jié)構(gòu)的細(xì)節(jié)完全與客戶(hù)端分離。客戶(hù)端可以選擇數(shù)據(jù)結(jié)構(gòu)不同的實(shí)現(xiàn)方式,但是客戶(hù)端代碼只能執(zhí)行基本操作。
實(shí)現(xiàn)的部分無(wú)法知道客戶(hù)端需求的細(xì)節(jié),它所要做的只是實(shí)現(xiàn)這些操作,這樣,很多不同的客戶(hù)端都可以使用同一個(gè)實(shí)現(xiàn),這使得我們能夠用模塊式可復(fù)用的算法與數(shù)據(jù)結(jié)構(gòu)庫(kù)來(lái)構(gòu)建更復(fù)雜的算法和數(shù)據(jù)結(jié)構(gòu),并在必要的時(shí)候更關(guān)注算法的效率。
Separate client and implementation via API.
API:描述數(shù)據(jù)類(lèi)型特征的操作
Client:使用API??操作的客戶(hù)端程序。
Implementation:實(shí)現(xiàn)API操作的代碼。
下面具體看下這兩種數(shù)據(jù)結(jié)構(gòu)的實(shí)現(xiàn)
棧 Stack 棧 API假設(shè)我們有一個(gè)字符串集合,我們想要實(shí)現(xiàn)字符串集合的儲(chǔ)存,定期取出并且返回最后加入的字符串,并檢查集合是否為空。我們需要先寫(xiě)一個(gè)客戶(hù)端然后再看它的實(shí)現(xiàn)。
字符串?dāng)?shù)據(jù)類(lèi)型的棧
性能要求:所有操作都花費(fèi)常數(shù)時(shí)間
客戶(hù)端:從標(biāo)準(zhǔn)輸入讀取逆序的字符串序列
import edu.princeton.cs.algs4.StdIn; import edu.princeton.cs.algs4.StdOut; public static void main(String[] args) { StackOfStrings stack = new StackOfStrings(); while (!StdIn.isEmpty()) { //從標(biāo)準(zhǔn)輸入獲取一些字符串 String s = StdIn.readString(); //如果字符串為"-",則客戶(hù)端將棧頂?shù)淖址鰲?,并打印出棧的字符? if (s.equals("-")) StdOut.print(stack.pop()); //否則將字符串入棧到棧頂 else stack.push(s); } }
客戶(hù)端輸入輸出:
棧的實(shí)現(xiàn):鏈表鏈表(linked-list)連接待添加...
我們想保存一個(gè)有節(jié)點(diǎn)組成的,用來(lái)儲(chǔ)存字符串的鏈表。節(jié)點(diǎn)包含指向鏈表中下一個(gè)元素的引用(first).
維持指針 first 指向鏈表中的第一個(gè)節(jié)點(diǎn)
Push:入棧,在鏈表頭插入一個(gè)新的節(jié)點(diǎn)
Pop:出棧,去掉鏈表頭處第一個(gè)節(jié)點(diǎn)
Java 實(shí)現(xiàn)public class LinkedStackOfStrings { //棧中唯一的實(shí)例變量是鏈表中的第一個(gè)節(jié)點(diǎn)的引用 private Node first = null; //內(nèi)部類(lèi),節(jié)點(diǎn)對(duì)象,構(gòu)成鏈表中的元素,由一個(gè)字符串和指向另一個(gè)節(jié)點(diǎn)的引用組成 private class Node { private String item; private Node next; } public boolean isEmpty() { return first == null; } // public void push(String item) { //將指向鏈表頭的指針先保存 Node oldfirst = first; //創(chuàng)建新節(jié)點(diǎn):我們將要插入表頭的節(jié)點(diǎn) first = new Node(); first.item = item; //實(shí)例變量的next指針指向鏈表oldfirst元素,現(xiàn)在變成鏈表的第二個(gè)元素 first.next = oldfirst; } //出棧 public String pop() { //將鏈表中的第一個(gè)元素儲(chǔ)存在標(biāo)量 item 中 String item = first.item; //去掉第一個(gè)節(jié)點(diǎn):將原先指向第一個(gè)元素的指針指向下一個(gè)元素,然后第一個(gè)節(jié)點(diǎn)就等著被垃圾回收處理 first = first.next; //返回鏈表中原先保存的元素 return item; } }
圖示:
出棧:
入棧:
性能分析通過(guò)分析提供給客戶(hù)算法和數(shù)據(jù)結(jié)構(gòu)的性能信息,評(píng)估這個(gè)實(shí)現(xiàn)對(duì)以不同客戶(hù)端程序的資源使用量
Proposition 在最壞的情況下,每個(gè)操作只需要消耗常數(shù)時(shí)間(沒(méi)有循環(huán))。
Proposition 具有n個(gè)元素的棧使用 ~40n 個(gè)字節(jié)內(nèi)存
(沒(méi)有考慮字符串本身的內(nèi)存,因?yàn)檫@些空間的開(kāi)銷(xiāo)在客戶(hù)端上)
棧用鏈表是實(shí)現(xiàn)花費(fèi)常數(shù)的時(shí)間,但是棧還有更快的實(shí)現(xiàn)
另一種實(shí)現(xiàn)棧的 natural way 是使用數(shù)組儲(chǔ)存棧上的元素
將棧中的N個(gè)元素保存在數(shù)組中,索引為 n,n 對(duì)應(yīng)的數(shù)組位置即為棧頂?shù)奈恢?,即下一個(gè)元素加入的地方
使用數(shù)組 s[] 在棧上存儲(chǔ)n個(gè)元素。
push():在 s[n] 處添加新元素。
pop():從 s[n-1] 中刪除元素。
在改進(jìn)前使用數(shù)組的一個(gè)缺點(diǎn)是必須聲明數(shù)組的大小,所以棧有確定的容量。如果棧上的元素個(gè)數(shù)比棧的容量多,我們就必須處理這個(gè)問(wèn)題(調(diào)整數(shù)組)
Java 實(shí)現(xiàn)public class FixedCapacityStackOfStrings { private String[] s; //n 為棧的大小,棧中下一個(gè)開(kāi)放位置,也為下一個(gè)元素的索引 private int n = 0; //int capacity:看以下說(shuō)明 public FixedCapacityStackOfStrings(int capacity) { s = new String[capacity]; } public boolean isEmpty() { return n == 0; } public void push(String item) { //將元素放在 n 索引的位置,然后 n+1 s[n++] = item; } public String pop() { //然后返回?cái)?shù)組n-1的元素 return s[--n]; } }
int capacity: 在構(gòu)造函數(shù)中加入了容量的參數(shù),破壞了API,需要客戶(hù)端提供棧的容量。不過(guò)實(shí)際上我們不會(huì)這么做,因?yàn)榇蠖鄶?shù)情況下,客戶(hù)端也無(wú)法確定需要多大棧,而且客戶(hù)端也可能需要同時(shí)維護(hù)很多棧,這些棧又不同時(shí)間到達(dá)最大容量,同時(shí)還有其他因素的影響。這里只是為了簡(jiǎn)化。在調(diào)整數(shù)組中會(huì)處理可變?nèi)萘康膯?wèn)題,避免溢出
對(duì)于兩種實(shí)現(xiàn)的思考上述的實(shí)現(xiàn)中我們暫時(shí)沒(méi)有處理的問(wèn)題:
Overflow and underflow
Underflow :客戶(hù)端從空棧中出棧我們沒(méi)有拋出異常
Overflow :使用數(shù)組實(shí)現(xiàn),當(dāng)客戶(hù)端入棧超過(guò)容量發(fā)生棧溢出的問(wèn)題
Null item:客戶(hù)端是否能向數(shù)據(jù)結(jié)構(gòu)中插入空元素,上邊我們是允許的
Duplicate items: 客戶(hù)端是否能向數(shù)據(jù)結(jié)構(gòu)中重復(fù)入棧同一個(gè)元素,上邊我們是允許的
Loitering 對(duì)象游離:即在棧的數(shù)組中,我們有一個(gè)對(duì)象的引用,可是我們已經(jīng)不再使用這個(gè)引用了
數(shù)組中當(dāng)我們減小 n 時(shí),在數(shù)組中仍然有我們已經(jīng)出棧的對(duì)象的指針,盡管我們不再使用它,但是Java系統(tǒng)并不知道。所以為了避免這個(gè)問(wèn)題,有效地利用內(nèi)存,最好將去除元素對(duì)應(yīng)的項(xiàng)設(shè)為 null,這樣就不會(huì)剩下舊元素的引用指針,接下來(lái)就等著垃圾回收機(jī)制去回收這些內(nèi)存。這個(gè)問(wèn)題比較細(xì)節(jié)化,但是卻很重要。
public String pop() { String item = s[--n]; s[n] = null; return item; }調(diào)整數(shù)組
之前棧的基本數(shù)組實(shí)現(xiàn)需要客戶(hù)端提供棧的最大容量,現(xiàn)在我們來(lái)看解決這個(gè)問(wèn)題的技術(shù)。
待解決的問(wèn)題:建立一個(gè)能夠增長(zhǎng)或者縮短到任意大小的棧。
調(diào)整大小是一個(gè)挑戰(zhàn),而且要通過(guò)某種方式確保它不會(huì)頻繁地發(fā)生。
反復(fù)增倍法 (repeated doubling):當(dāng)數(shù)組被填滿(mǎn)時(shí),建立一個(gè)大小翻倍的新數(shù)組,然后將所有的元素復(fù)制過(guò)去。這樣我們就不會(huì)那么頻繁地創(chuàng)建新數(shù)組。
Java 實(shí)現(xiàn)public class ResizingArrayStackOfStrings { private String[] s; //n 為棧的大小,棧中下一個(gè)開(kāi)放位置,也為下一個(gè)元素的索引 private int n = 0; public ResizingArrayStackOfStrings(){ s = new String[2]; } public boolean isEmpty() { return n == 0; } /** * 從大小為1的數(shù)組開(kāi)始,如果發(fā)現(xiàn)數(shù)組被填滿(mǎn),那么就在插入元素之前,將數(shù)組長(zhǎng)度調(diào)整為原來(lái)的2倍 * @param item */ public void push(String item) { if (n == s.length) resize(2 * s.length); s[n++] = item; } /** * 調(diào)整數(shù)組方法 * 創(chuàng)建具有目標(biāo)容量的新數(shù)組,然后把當(dāng)前棧復(fù)制到新棧的前一半 * 然后重新設(shè)置和返回實(shí)例標(biāo)量 * @param capacity */ private void resize(int capacity) { System.out.println("resize when insert item "+ (n+1)); String[] copy = new String[capacity]; for (int i = 0; i < n; i++) copy[i] = s[i]; s = copy; } public String pop() { return s[--n]; } }性能分析
往棧中插入 n 個(gè)元素的時(shí)間復(fù)雜度是線(xiàn)性相近的,即與 n 成正比 ~n
Q. 假設(shè)我們從一個(gè)空的棧開(kāi)始,我們執(zhí)行 n 次入棧, 那么我們的 **resize()** 方法被調(diào)用了幾次? A. 是以 2 為底的對(duì)數(shù)次。因?yàn)槲覀冎挥性跅5拇笮〉扔?2 的冪函數(shù)的時(shí)候,即 2^1,2^2,2^3 ... 2^i 的時(shí)候才會(huì)調(diào)用 resize(). 在 1 到 n 之間,符合 2 的冪的數(shù)字(如 2,4,8,16...) 一共有 logn 個(gè),其中 log 以為 2 為底.
我們?cè)诓迦?2^i 個(gè)元素時(shí),需要復(fù)制數(shù)組 logn 次,需要花費(fèi)訪問(wèn)數(shù)組 n + (2 + 4 + 8 + ... + m) ~3n 的時(shí)間,其中 m = 2^logn = n
n : 無(wú)論數(shù)組翻不翻倍,對(duì)于每個(gè)新元素,入棧需要 Θ(1) 時(shí)間。因此,對(duì)于 n 個(gè)元素,它需要 Θ(n) 時(shí)間。即忽略常數(shù)項(xiàng),插入 n 個(gè) 就有 n 次入棧的操作,就訪問(wèn) n 次數(shù)組
(2 + 4 + 8 + ... + n):
如果 n = 2^i 個(gè)元素入棧,需要數(shù)組翻倍 lgn 次。
從技術(shù)上講,總和(2 + 4 + 8 + .. + m)是具有 logN 個(gè)元素的幾何級(jí)數(shù)
然后:(2 + 4 + 8 + .. + m)= 2 *(2 ^ log N - 1) = 2(N - 1) = 2N - 2 ~2N
=> N +(2 + 4 + 8 + .. + N)= N + 2N - 2 = 3N - 2 ~3N
舉個(gè)栗子~,如果我們往棧中插入 8 (2^3) 個(gè)元素,那么我們必須將數(shù)組翻倍 lg8 次,即3次。因此,8個(gè)元素入棧的開(kāi)銷(xiāo)為 8 +(2 + 4 + 8)= 22 次 ≈ 24 次
再舉個(gè)栗子~,如果插入 16 (2^4) 個(gè)元素,那么我們必須將數(shù)組翻倍 lg16 次,即4次。因此,16個(gè)元素入棧的開(kāi)銷(xiāo)為 16 +(2 + 4 + 8 + 16)= 46 次 ≈ 48 次
或者粗略想象一下,如果我們計(jì)算一下開(kāi)銷(xiāo),插入前 n (n = 2^i) 個(gè)元素,是對(duì) 2 的冪從1到N求和。 這樣,總的開(kāi)銷(xiāo)大約是3N。先要訪問(wèn)數(shù)組一次,對(duì)于復(fù)制要訪問(wèn)兩次。所以,要插入元素,大約需要訪問(wèn)數(shù)組三次。
下邊的圖是觀察時(shí)間開(kāi)銷(xiāo)的另一種方式,表示了入棧操作需要訪問(wèn)數(shù)組的次數(shù)。
每次遇到2的冪,需要進(jìn)行斜線(xiàn)上的數(shù)組訪問(wèn)時(shí)間,但是從宏觀上來(lái)看,是將那些元素入棧上花去了紅色直線(xiàn)那些時(shí)間 這叫做平攤分析??紤]開(kāi)銷(xiāo)時(shí)將總的開(kāi)銷(xiāo)平均給所有的操作。關(guān)于平攤分析就不再解釋了,有興趣可以自行了解...
怎樣縮小數(shù)組如果數(shù)組翻倍多次后,又有多次出棧,那么如果不調(diào)整數(shù)組大小,數(shù)組可能會(huì)變得太空。那數(shù)組在什么情況下去縮小,縮小多少才合適?
我們也許這么考慮,當(dāng)數(shù)組滿(mǎn)了的時(shí)候?qū)⑷萘糠叮敲串?dāng)它只有一半滿(mǎn)的時(shí)候,將容量縮減一半。但是這樣并不合理,因?yàn)橛幸环N現(xiàn)象叫做thrashing:即客戶(hù)端剛好反復(fù)交替入棧出棧入棧出棧...
如果數(shù)組滿(mǎn)了就會(huì)反復(fù)翻倍減半翻倍減半,并且每個(gè)操作都會(huì)新建數(shù)組,都要花掉正比與N的時(shí)間,這樣就會(huì)導(dǎo)致thrashing頻繁發(fā)生要花費(fèi)平方時(shí)間,這不是我們想要的。
有效的解決方案是直到數(shù)組變?yōu)?1/4 滿(mǎn)的時(shí)候才將容量減半
我們只要測(cè)試數(shù)組是否為 1/4 滿(mǎn),如果是,則調(diào)整大小使其為 1/2 滿(mǎn)。
不變式:數(shù)組總是介于25% 滿(mǎn)與全滿(mǎn)之間
public String pop() { String item = s[--n]; //解決之前說(shuō)的對(duì)象引用游離問(wèn)題 s[n] = null; if (n > 0 && n == s.length/4) resize(s.length/2); return item; }
這樣的好處:
因?yàn)槭前霛M(mǎn)的,既可以插入向棧插入元素,又可以從棧刪除元素,而不需要再次進(jìn)行調(diào)整數(shù)組大小的操作直到數(shù)組全滿(mǎn),或者再次1/4滿(mǎn)。
每次調(diào)整大小時(shí), 開(kāi)銷(xiāo)已經(jīng)在平攤給了每次入棧和出棧
下圖展示了上邊測(cè)試寫(xiě)的客戶(hù)端例子中數(shù)組上的操作
可以看到在開(kāi)始時(shí),數(shù)組大小從1倍增到2又到4,但一旦到8,數(shù)組的大小則維持一段時(shí)間不變,直到數(shù)組中只有2個(gè)元素時(shí)才縮小到4,等等。
算法分析 運(yùn)行時(shí)間數(shù)組調(diào)整大小并不經(jīng)常發(fā)生,但這是實(shí)現(xiàn)棧API的一種很有效的方式,客戶(hù)端不需要提供棧的最大容量,但依然保證了我們使用的內(nèi)存大小總是棧中實(shí)際元素個(gè)數(shù)的常數(shù)倍,所以分析說(shuō)明對(duì)于任意的操作序列,每個(gè)操作的平均運(yùn)行時(shí)間與常數(shù)成正比。
這里,存在最壞情況(worst case)。當(dāng)棧容量翻倍時(shí),需要正比于N的時(shí)間,所以性能不如我們想要的那么好,但是優(yōu)勢(shì)在于進(jìn)行入棧出棧操作時(shí)非常快,入棧只需要訪問(wèn)數(shù)組并移動(dòng)棧頂索引。對(duì)于大多數(shù)操作都很高效的。對(duì)于眾多的客戶(hù)端這是個(gè)很有效的權(quán)衡。
內(nèi)存使用棧的內(nèi)存用量實(shí)際上比鏈表使用更少的內(nèi)存。
給出的命題. 一個(gè) ResizingArrayStackOfStrings 內(nèi)存用量在 ~8n bytes 到 ~32n bytes 之間,取決于數(shù)組有多滿(mǎn)。
只看 “private String[] s;”
?~ 8n 當(dāng)數(shù)組滿(mǎn)時(shí). -- 棧的實(shí)際長(zhǎng)度 = n,所以?xún)?nèi)存占用是 8 乘以棧不為空的元素個(gè)數(shù)
?~ 32n 當(dāng)數(shù)組 1/4 滿(mǎn)時(shí). -- 棧的實(shí)際長(zhǎng)度 = 4n,所以?xún)?nèi)存占用是 8 乘以(4 乘以棧的有效元素個(gè)數(shù))
這里只是計(jì)算了 Java中數(shù)組占用的空間。同樣地,這個(gè)分析只針對(duì)棧本身 而不包括客戶(hù)端上的字符串。
調(diào)整數(shù)組實(shí)現(xiàn)VS鏈表實(shí)現(xiàn)那么使用可調(diào)整大小的數(shù)組與鏈表之間如何取舍呢?
這是兩種 API相同的不同的實(shí)現(xiàn),客戶(hù)端可以互換使用。
哪個(gè)更好呢?
很多情形中,我們會(huì)有同一API的多種實(shí)現(xiàn)。你需要根據(jù)客戶(hù)端的性質(zhì)選擇合適的實(shí)現(xiàn)。
Linked-list implementation.
對(duì)于鏈表,每個(gè)操作最壞情況下需要常數(shù)時(shí)間,這是有保障的
但是為了處理鏈接,我們需要一些額外的時(shí)間和空間。所以鏈表實(shí)現(xiàn)會(huì)慢一些
Resizing-array implementation.
可調(diào)大小的數(shù)組實(shí)現(xiàn)有很好的分?jǐn)倳r(shí)間,所以整個(gè)過(guò)程總的平均效率不錯(cuò)
浪費(fèi)更少的空間,對(duì)于每個(gè)操作也許有更快的實(shí)現(xiàn)
所以對(duì)于一些客戶(hù)端,也許會(huì)有區(qū)別。以下這樣的情形你不會(huì)想用可調(diào)大小數(shù)組實(shí)現(xiàn):你有一架飛機(jī)進(jìn)場(chǎng)等待降落,你不想系統(tǒng)突然間不能高效運(yùn)轉(zhuǎn);或者互聯(lián)網(wǎng)上的一個(gè)路由器,數(shù)據(jù)包以很高的速度涌進(jìn)來(lái),你不想因?yàn)槟硞€(gè)操作突然變得很慢而丟失一些數(shù)據(jù)。
客戶(hù)端就可以權(quán)衡,如果想要獲得保證每個(gè)操作能夠很快完成,就使用鏈表實(shí)現(xiàn);
如果不需要保證每個(gè)操作,只是關(guān)心總的時(shí)間,可能就是用可調(diào)大小數(shù)組實(shí)現(xiàn)。因?yàn)榭偟臅r(shí)間會(huì)小得多,如果不是最壞情況下單個(gè)操作非??臁?br>盡管只有這些簡(jiǎn)單的數(shù)據(jù)結(jié)構(gòu),我們都需要做很重要的權(quán)衡,在很多實(shí)際情形中真的會(huì)產(chǎn)生影響。
接下來(lái)我們簡(jiǎn)要考慮一下使用相同基本底層數(shù)據(jù)結(jié)構(gòu)的隊(duì)列的實(shí)現(xiàn)。
隊(duì)列的API這是字符串隊(duì)列對(duì)應(yīng)的API,實(shí)際上和棧的API是相同的,只是名字不一樣而已...
入棧換成了入隊(duì)(enqueue),出棧換成了出隊(duì)(dequeue)。語(yǔ)義是不同的。
入隊(duì)操作向隊(duì)尾添加元素,而出隊(duì)操作從隊(duì)首移除元素。
就像你排隊(duì)買(mǎi)票一樣,入隊(duì)時(shí)你排在隊(duì)列的最后,在隊(duì)列里待的最久的人是下一個(gè)離開(kāi)隊(duì)列的人。
數(shù)據(jù)結(jié)構(gòu)的性能要求:所有操作都花費(fèi)常數(shù)時(shí)間。
鏈表實(shí)現(xiàn)隊(duì)列的鏈表表示中,我們需要維護(hù)兩個(gè)指針引用。一個(gè)是鏈表中的第一個(gè)元素,另一個(gè)是鏈表最后一個(gè)元素。
插入的時(shí)候我們?cè)阪湵?strong>末端添加元素;移除元素的時(shí)候不變,依然從鏈表頭取出元素。
出隊(duì) dequeue
那么這就是出隊(duì)操作的實(shí)現(xiàn),和棧的出棧操作是一樣的。保存元素,前進(jìn)指針指向下一個(gè)節(jié)點(diǎn),這樣就刪除了第一個(gè)節(jié)點(diǎn),然后返回該元素。
入隊(duì) enqueue
入隊(duì)操作時(shí),向鏈表添加新節(jié)點(diǎn)。我們要把它放在鏈表末端,這樣它就是最后一個(gè)出隊(duì)的元素。
首先要做的是保存指向最后一個(gè)節(jié)點(diǎn)的指針,因?yàn)槲覀冃枰獙⑺赶蛳乱粋€(gè)節(jié)點(diǎn)的指針從null變?yōu)樾碌墓?jié)點(diǎn)。
然后給鏈表末端創(chuàng)建新的節(jié)點(diǎn)并對(duì)其屬性賦值,將舊的指針從null變?yōu)橹赶蛐鹿?jié)點(diǎn)。
實(shí)際上現(xiàn)在指針操作僅限于如棧和隊(duì)列這樣的少數(shù)幾個(gè)實(shí)現(xiàn)以及一些其他的基本數(shù)據(jù)結(jié)構(gòu)了?,F(xiàn)在很多操作鏈表的通用程序都封裝在了這樣的基本數(shù)據(jù)類(lèi)型里。
Java 實(shí)現(xiàn)
這里處理了當(dāng)隊(duì)列為空時(shí)的特殊情況。為了保證去除最后一個(gè)元素后隊(duì)列是空的,我們將last設(shè)為null,還保證first和last始終都是符合我們預(yù)想。
完整代碼: Queue.java 這里用到了泛型和迭代器的實(shí)現(xiàn)
用可調(diào)大小的數(shù)組實(shí)現(xiàn)并不難,但絕對(duì)是一個(gè)棘手的編程練習(xí)。
我們維護(hù)兩個(gè)指針,分別指向隊(duì)列中的第一個(gè)元素和隊(duì)尾,即下一個(gè)元素要加入的地方。
對(duì)于入隊(duì)操作在 tail 指向的地方加入新元素
出隊(duì)操作移除 head 指向的元素
棘手的地方是一旦指針的位置超過(guò)了數(shù)組的容量,必須重置指針回到0,這里需要多寫(xiě)一些代碼,而且和棧一樣實(shí)現(xiàn)數(shù)據(jù)結(jié)構(gòu)的時(shí)候你需要加上調(diào)整容量的方法。
完整代碼:ResizingArrayQueue.java
額外補(bǔ)充兩個(gè)棧實(shí)現(xiàn)隊(duì)列的數(shù)據(jù)結(jié)構(gòu)
實(shí)現(xiàn)具有兩個(gè)棧的隊(duì)列,以便每個(gè)隊(duì)列操作都是棧操作的常量次數(shù)(平攤次數(shù))。
提示:
1.如果將元素推入棧然后全部出棧,它們將以相反的順序出現(xiàn)。如果你重復(fù)這個(gè)過(guò)程,它們現(xiàn)在又恢復(fù)了正常的隊(duì)列順序。
2.為了避免不停的出棧入棧,可以加某個(gè)判定條件,比如當(dāng) dequeue 棧為空時(shí),將 enqueue 棧的元素出棧到 dequeue 棧,然后最后從dequeue 棧出棧,也就實(shí)現(xiàn)了出隊(duì)的操作。直到 dequeue 棧的元素都出棧了,再次觸發(fā)出隊(duì)操作時(shí),再?gòu)?enqueue 棧導(dǎo)入數(shù)據(jù)重復(fù)上邊的過(guò)程
實(shí)現(xiàn)參考:QueueWithTwoStacks.java
泛型 -- Generic接下來(lái)我們要處理的是前面實(shí)現(xiàn)里另一個(gè)根本性的缺陷。前面的實(shí)現(xiàn)只適用于字符串,如果想要實(shí)現(xiàn)其他類(lèi)型數(shù)據(jù)的隊(duì)列和棧怎么 StackOfURLs, StackOfInts... 嗯。。。
這個(gè)問(wèn)題就涉及泛型的話(huà)題了。
實(shí)際上很多編程環(huán)境中這一點(diǎn)都是不得不考慮的。
第一種方法:我們對(duì)每一個(gè)數(shù)據(jù)類(lèi)型都實(shí)現(xiàn)一個(gè)多帶帶的棧
這太雞肋了,我們要把代碼復(fù)制到需要實(shí)現(xiàn)棧的地方,然后把數(shù)據(jù)類(lèi)型改成這個(gè)型那個(gè)型,那么如果我們要處理上百個(gè)不同的數(shù)據(jù)類(lèi)型,我們就得有上百個(gè)不同的實(shí)現(xiàn),想想就很心累。
不幸的是Java 推出 1.5 版本前就是陷在這種模式里,并且非常多的編程語(yǔ)言都無(wú)法擺脫這樣的模式。所以我們需要采用一種現(xiàn)代模式,不用給每個(gè)類(lèi)型的數(shù)據(jù)都分別搞實(shí)現(xiàn)。
第二種方法:我們對(duì) Object 類(lèi)實(shí)現(xiàn)數(shù)據(jù)結(jié)構(gòu)
有一個(gè)廣泛采用的捷徑是使用強(qiáng)制類(lèi)型轉(zhuǎn)換對(duì)不同的數(shù)據(jù)類(lèi)型重用代碼。Java中所有的類(lèi)都是 Object 的子類(lèi),當(dāng)客戶(hù)端使用時(shí),就將結(jié)果轉(zhuǎn)換為對(duì)應(yīng)的類(lèi)型。但是這種解決方案并不令人滿(mǎn)意。
這個(gè)例子中我們有兩個(gè)棧:蘋(píng)果的棧和桔子的棧。接下來(lái),當(dāng)從蘋(píng)果棧出棧的時(shí)候需要客戶(hù)端將出棧元素強(qiáng)制轉(zhuǎn)換為蘋(píng)果類(lèi)型,這樣類(lèi)型檢查系統(tǒng)才不會(huì)報(bào)錯(cuò)。
這樣做的問(wèn)題在于:
必須客戶(hù)端完成強(qiáng)制類(lèi)型轉(zhuǎn)換,通過(guò)編譯檢查。
存在一個(gè)隱患,如果類(lèi)型不匹配,會(huì)發(fā)生運(yùn)行時(shí)錯(cuò)誤。
第三種方法:使用泛型
這種方法中客戶(hù)端程序不需要強(qiáng)制類(lèi)型轉(zhuǎn)換。在編譯時(shí)就能發(fā)現(xiàn)類(lèi)型不匹配的錯(cuò)誤,而不是在運(yùn)行時(shí)。
這個(gè)使用泛型的例子中棧的類(lèi)型有一個(gè)類(lèi)型參數(shù)(Apple),在代碼里這個(gè)尖括號(hào)中。
如果我們有一個(gè)蘋(píng)果棧,并且試圖入棧一個(gè)桔子,我們?cè)诰幾g時(shí)就會(huì)提示錯(cuò)誤,因?yàn)槁暶髦心莻€(gè)棧只包含蘋(píng)果,桔子禁止入內(nèi)。
優(yōu)秀的模塊化編程的指導(dǎo)原則就是我們應(yīng)當(dāng)歡迎編譯時(shí)錯(cuò)誤,避免運(yùn)行時(shí)錯(cuò)誤。
因?yàn)槿绻覀兡茉诰幾g時(shí)檢測(cè)到錯(cuò)誤,我們給客戶(hù)交付產(chǎn)品或者部署對(duì)一個(gè)API的實(shí)現(xiàn)時(shí),就有把握對(duì)于任何客戶(hù)端都是沒(méi)問(wèn)題的。 有些運(yùn)行時(shí)才會(huì)出現(xiàn)的錯(cuò)誤可能在某些客戶(hù)端的開(kāi)發(fā)中幾年之后才出現(xiàn),如果這樣,就必須部署我們的軟件,這對(duì)每個(gè)人都是很困難的。
實(shí)際上優(yōu)秀的泛型實(shí)現(xiàn)并不難。只需要把每處使用的字符串替換為泛型類(lèi)型名稱(chēng)。
鏈表?xiàng)5姆盒蛯?shí)現(xiàn)如這里的代碼所示,左邊是我們使用鏈表實(shí)現(xiàn)的字符串棧,右邊是泛型實(shí)現(xiàn)。
左邊每處用到字符串類(lèi)型的地方我們換成了item。在最上面類(lèi)聲明的地方我們用尖括號(hào)聲明 item 是我們要用的泛型類(lèi)型,這樣的實(shí)現(xiàn)非常直截了當(dāng),并且出色地解決了不同的數(shù)據(jù)類(lèi)型多帶帶實(shí)現(xiàn)的問(wèn)題。
數(shù)組棧的泛型實(shí)現(xiàn)基于數(shù)組的實(shí)現(xiàn),這種方法不管用。目前很多編程語(yǔ)言這方面都有問(wèn)題,而對(duì)Java尤其是個(gè)難題。我們想做的是用泛型名稱(chēng) item 直接聲明一個(gè)新的數(shù)組。比如這樣:
public class FixedCapacityStack- { private Item[] s; private int n = 0; public FixedCapacityStack(int capacity) //看這里看這里像這樣,但是實(shí)際我們?cè)趈ava當(dāng)中我卻不能這樣方便的實(shí)現(xiàn) { s = new Item[capacity]; } public boolean isEmpty() { return n == 0; } public void push(Item item) { s[n++] = item; } public Item pop() { return s[--n]; } }
如有備注的這行所示。其他部分都和之前的方法沒(méi)區(qū)別。不幸的是,Java不允許創(chuàng)建泛型數(shù)組。對(duì)于這個(gè)問(wèn)題有各種技術(shù)方面的原因,在網(wǎng)上關(guān)于這個(gè)問(wèn)題你能看到大量的爭(zhēng)論,這個(gè)不在我們討論的范圍之內(nèi)。關(guān)于協(xié)變的內(nèi)容,可以自行了解,嗯。。。我一會(huì)也去了解了解...
這里,要行得通我們需要加入強(qiáng)制類(lèi)型轉(zhuǎn)換。
我們創(chuàng)建 Object 數(shù)組,然后將類(lèi)型轉(zhuǎn)換為 item 數(shù)組。教授的觀點(diǎn)是優(yōu)秀的代碼應(yīng)該不用強(qiáng)制類(lèi)型轉(zhuǎn)換, 要盡量避免強(qiáng)制類(lèi)型轉(zhuǎn)換,因?yàn)樗_實(shí)在我們的實(shí)現(xiàn)中留下了隱患。但這個(gè)情況中我們必須加入這個(gè)強(qiáng)制類(lèi)型轉(zhuǎn)換。
當(dāng)我們編譯這個(gè)程序的時(shí)候,Java會(huì)發(fā)出警告信息說(shuō)我們?cè)谑褂梦唇?jīng)檢查或者不安全的操作,詳細(xì)信息需要使用-Xlint=unchecked 參數(shù)重新編譯。
我們加上這個(gè)參數(shù)重新編譯之后顯示你在代碼中加入了一個(gè)未經(jīng)檢查的強(qiáng)制類(lèi)型轉(zhuǎn)換,然后 java 就警告你不應(yīng)該加入未經(jīng)檢查的強(qiáng)制類(lèi)型轉(zhuǎn)換。但是這么做并不是我們的錯(cuò),因?yàn)槟悴辉试S我們聲明泛型數(shù)組,我們才不得不這么做。收到這個(gè)警告信息請(qǐng)不要認(rèn)為是你的代碼中有什么問(wèn)題。
自動(dòng)裝箱 (Autoboxing) 與拆箱 (Unboxing)接下來(lái),是個(gè)跟Java有關(guān)的細(xì)節(jié)問(wèn)題,
Q. 對(duì)基本數(shù)據(jù)類(lèi)型,我們?cè)鯓邮褂梅盒停?/strong>
我們用的泛型類(lèi)型是針對(duì) Object 及其子類(lèi)的。前面講過(guò),是從 Object 數(shù)組強(qiáng)制類(lèi)型轉(zhuǎn)換來(lái)的。為了處理基本類(lèi)型,我們需要使用Java的包裝對(duì)象類(lèi)型。
如大寫(xiě)的 Integer 是整型的包裝類(lèi)型等等。另外,有個(gè)過(guò)程叫自動(dòng)打包,自動(dòng)轉(zhuǎn)換基本類(lèi)型與包裝類(lèi)型,所以處理基本類(lèi)型這個(gè)問(wèn)題,基本上都是在后臺(tái)完成的.
Autoboxing:基本數(shù)據(jù)類(lèi)型到包裝類(lèi)型的自動(dòng)轉(zhuǎn)換。
unboxing:包裝器類(lèi)型到基本數(shù)據(jù)類(lèi)型的自動(dòng)轉(zhuǎn)換。
綜上所述,我們能定義適用于任何數(shù)據(jù)類(lèi)型的泛型棧的API,而且我們有基于鏈表和數(shù)組兩種實(shí)現(xiàn)。我們講過(guò)的使用可變大小數(shù)組或者鏈表,對(duì)于任何數(shù)據(jù)類(lèi)型都有非常好的性能。
額外補(bǔ)充在 Java 6, 必須在變量聲明(左側(cè))和構(gòu)造函數(shù)調(diào)用(右側(cè))中指定具體類(lèi)型。從Java 7 開(kāi)始,可以使用菱形運(yùn)算符:
Stack
Q. 為什么Java需要強(qiáng)制轉(zhuǎn)換(或反射)?
簡(jiǎn)短的回答: 向后兼容性。
詳細(xì)地回答:需要了解類(lèi)型擦除和協(xié)變數(shù)組
Q. 當(dāng)我嘗試創(chuàng)建泛型數(shù)組時(shí),為什么會(huì)出現(xiàn)“無(wú)法創(chuàng)建泛型數(shù)組”錯(cuò)誤?
public class ResizingArrayStack
A. 根本原因是Java中的數(shù)組是協(xié)變的,但泛型不是。換句話(huà)說(shuō),String [] 是 Object [] 的子類(lèi)型,但 Stack
Q. 那么,為什么數(shù)組是協(xié)變的呢?
A. 許多程序員(和編程語(yǔ)言理論家)認(rèn)為協(xié)變數(shù)組是 Java 類(lèi)型系統(tǒng)中的一個(gè)嚴(yán)重缺陷:它們會(huì)產(chǎn)生不必要的運(yùn)行時(shí)性能開(kāi)銷(xiāo)(例如,參見(jiàn)ArrayStoreException)并且可能導(dǎo)致細(xì)微的 BUG。在Java中引入了協(xié)變數(shù)組,以避免Java在其設(shè)計(jì)中最初沒(méi)有包含泛型的問(wèn)題,例如,實(shí)現(xiàn)Arrays.sort(Comparable [])并使用 String [] 類(lèi)型的輸入數(shù)組進(jìn)行調(diào)用。
Q. 我可以創(chuàng)建并返回參數(shù)化類(lèi)型的新數(shù)組,例如,為泛型隊(duì)列實(shí)現(xiàn) toArray() 方法嗎?
A. 不容易。如果客戶(hù)端將所需具體類(lèi)型的對(duì)象傳遞給 toArray(),則可以使用反射來(lái)執(zhí)行此操作。這是 Java 的 Collection Framework 采用的(笨拙)方法。
迭代器 IteratorsJava還提供了另一種能夠使客戶(hù)端代碼保持優(yōu)雅緊湊,絕對(duì)值得添加到我們的基本數(shù)據(jù)類(lèi)型的特性 -- 迭代器。
遍歷對(duì)于遍歷功能,大多數(shù)客戶(hù)端想做的只是遍歷集合中的元素,我們考慮的任何內(nèi)部表示,這對(duì)于客戶(hù)端是不相關(guān)的,他們并不關(guān)心集合的內(nèi)部實(shí)現(xiàn)。也就是說(shuō)我們?cè)试S客戶(hù)端遍歷集合中的元素,但不必讓客戶(hù)端知道我們是用數(shù)組還是鏈表。
Java提供了一個(gè)解決方式,就是實(shí)現(xiàn)遍歷機(jī)制,然后使用 foreach.
Foreach loop我們自找麻煩地要讓我們的數(shù)據(jù)類(lèi)型添加迭代器是因?yàn)椋绻覀兊臄?shù)據(jù)結(jié)構(gòu)是可遍歷的,在Java中我們就可以使用非常緊湊優(yōu)雅的客戶(hù)端代碼,即所謂的for-each語(yǔ)句來(lái)進(jìn)行集合的遍歷。
使用了迭代器后,以下兩種寫(xiě)法都可以不考慮底層的內(nèi)部實(shí)現(xiàn)而遍歷某個(gè)集合,兩種方法是等價(jià)的:
Stackstack; ... // "foreach" loop for (String s : stack) StdOut.println(s); ... // 與上邊的方法等價(jià) Iterator i = stack.iterator(); while (i.hasNext()) { String s = i.next(); StdOut.println(s); }
所以如果我們有一個(gè)棧 stack 可以寫(xiě)(for String s: stack) 表示對(duì)棧中每個(gè)字符串,執(zhí)行打印輸出。
我們也可以寫(xiě)成下邊這種完整形式的代碼,但不會(huì)有人這么做,因?yàn)樗蜕线叺暮?jiǎn)寫(xiě)形式是等價(jià)的。
不使用迭代器的話(huà)要實(shí)現(xiàn)遍歷客戶(hù)端代碼中就要執(zhí)行非常多不必要的入棧出棧操作。所以這是能夠讓遍歷數(shù)據(jù)結(jié)構(gòu)中的元素的客戶(hù)端代碼變得這么緊湊的關(guān)鍵所在。
要使用戶(hù)定義的集合支持 foreach 循環(huán):
數(shù)據(jù)類(lèi)型必須具有名為 iterator() 的方法
iterator() 方法返回一個(gè)對(duì)象,這個(gè)對(duì)象具有兩個(gè)核心方法:
hasNext() 方法, 當(dāng)不再遍歷到任何元素時(shí),返回false
next() 方法, 返回集合中的下一個(gè)元素
迭代器為了支持 foreach 循環(huán),Java 提供了兩個(gè)接口。
Iterator 接口:有 next() 和 hasNext() 方法。
Iterable 接口:iterator() 方法返回 一個(gè)迭代器 Iterator
兩者都應(yīng)該與泛型一起使用
Q. 什么是 Iterable ?
A. 在 Java 語(yǔ)言中 Iterable 是具有返回迭代器的方法的一種類(lèi)。
來(lái)源:jdk 8 java.lang.Iterable 接口
//此處T可以隨便寫(xiě)為任意標(biāo)識(shí),常見(jiàn)的如T、E、K、V等形式的參數(shù)常用于表示泛型 //在實(shí)例化泛型類(lèi)時(shí),必須指定T的具體類(lèi)型 public interface Iterable{ /** * Returns an iterator over elements of type {@code T}. * * @return an Iterator. */ Iterator iterator(); ... }
Q. 那么什么是迭代器 iterator ?
A. 迭代器是具有 hasNext() 和 next() 方法的類(lèi)。
來(lái)源:jdk 8 java.util.Iterator 接口
public interface Iterator{ boolean hasNext(); E next(); default void remove() default void forEachRemaining(Consumer super E> action) }
Java還允許 remove() 方法,但我們認(rèn)為這不是一個(gè)很好的特性,它有可能成為調(diào)試隱患,一般不用。
那么,只要有 hasNext() 和 next() 方法就使得數(shù)據(jù)結(jié)構(gòu)是可遍歷的,所以我們要實(shí)現(xiàn)這兩個(gè)方法。
下面我們要做的是看看如何使我們的棧、隊(duì)列和后面要講到的其他數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)所謂的 Iterable(可遍歷類(lèi))接口。
實(shí)例我們要給我們所有的基本數(shù)據(jù)結(jié)構(gòu)提供遍歷機(jī)制。實(shí)現(xiàn)這個(gè)功能并不特別難,而且絕對(duì)值得投入精力。這是基于棧的代碼。
棧實(shí)現(xiàn)迭代器: 鏈表實(shí)現(xiàn)接下來(lái)我們要實(shí)現(xiàn)Iterable接口。
實(shí)現(xiàn)Iterable接口意味著什么呢?這個(gè)類(lèi)需要有iterator()方法返回迭代器。
什么是迭代器呢?我們要用一個(gè)內(nèi)部類(lèi)。這個(gè)例子中,命名為 ListIterator 的內(nèi)部類(lèi)實(shí)現(xiàn) Iterator 接口,并且是泛化(generic)的。
ListIterator 這個(gè)類(lèi)主要完成的是實(shí)現(xiàn) hasNext() 和 next() 方法。從名字就能清楚知道它們的語(yǔ)義。
hasNext() 在完成遍歷之后會(huì)返回 false;如果還沒(méi)有完成,應(yīng)該返回 true
next() 方法提供要遍歷的下一個(gè)元素
所以如果是基于鏈表的數(shù)據(jù)結(jié)構(gòu),我們要從表頭 first 元素開(kāi)始,這是處于表頭的元素.
我們要維護(hù)迭代器中的實(shí)例變量 current 存儲(chǔ)當(dāng)前正在遍歷的元素。我們?nèi)〕鯿urrent元素,然后將 current 引用指向下一個(gè)元素,并返回之前儲(chǔ)存的item,也就將current 移動(dòng)到了下一個(gè)元素上。
客戶(hù)端會(huì)一直測(cè)試 hasNext(),所以當(dāng)current變成空指針,hasNext 返回 false 終止遍歷。
在我們的遍歷中,我們只需要關(guān)注實(shí)現(xiàn) next() 和 hasNext()方法,使用一個(gè)局部實(shí)例變量 current 就能完成。
如果遍歷已經(jīng)終止,客戶(hù)端還試圖調(diào)用 next() 或者試圖調(diào)用 remove() 時(shí)拋出異常,我們不提供remove()方法。
完整代碼:StackImpIterator
棧實(shí)現(xiàn)迭代器: 數(shù)組實(shí)現(xiàn)對(duì)于基于數(shù)組的實(shí)現(xiàn),就更簡(jiǎn)單了。使用迭代器我們能控制遍歷順序,使其符合語(yǔ)義和數(shù)據(jù)結(jié)構(gòu)。
遍歷棧時(shí)你要讓元素以出棧的順序返回,即對(duì)于數(shù)組是逆序的,那么這種情況下next() 就將索引減 1,返回下一個(gè)元素。
而我們的實(shí)例變量是數(shù)組的索引。
只要該變量為正,hasNext() 返回 true。要實(shí)現(xiàn)這個(gè)遍歷機(jī)制只需要寫(xiě)幾行Java代碼,以后遇到涉及對(duì)象集合的基本數(shù)據(jù)類(lèi)型中我們都會(huì)用這種編程范式。
完整代碼:ResizingArrayStack
實(shí)際上很多客戶(hù)端并不關(guān)心我們返回元素的順序。我們經(jīng)常做的是直接向集合中插入元素,接下來(lái)遍歷已有的元素。這樣的數(shù)據(jù)結(jié)構(gòu)叫做背包。
我們來(lái)看看它的API。
順序并不重要,所以我們想要能直接添加元素,也許還想知道集合大小。
我們想遍歷背包中所有的元素,這個(gè)API更簡(jiǎn)單,功能更少,但依然提供了幾個(gè)重要的操作。
使用這個(gè)API,我們已經(jīng)看過(guò)實(shí)現(xiàn)了,只需要將棧的出棧操作或者隊(duì)列的出隊(duì)操作去掉,就能獲得這個(gè)有用的數(shù)據(jù)結(jié)構(gòu)的良好的實(shí)現(xiàn)
完整代碼:Bag--ListIterator ;Bag--ArrayIterator
棧與隊(duì)列的應(yīng)用棧的應(yīng)用:
棧確實(shí)非?;A(chǔ),很多計(jì)算基于它運(yùn)行 因?yàn)樗軐?shí)現(xiàn)遞歸
·Java虛擬機(jī)
·解析編譯器 (處理編譯一種編程語(yǔ)言或者解釋為實(shí)際的計(jì)算)
·文字處理器中的撤消
·Web瀏覽器中的后退按鈕(當(dāng)你使用網(wǎng)頁(yè)瀏覽器上的后退按鈕時(shí),你去過(guò)的網(wǎng)頁(yè)就存儲(chǔ)在棧上)
·打印機(jī)的PostScript語(yǔ)言
·在編譯器中實(shí)現(xiàn)函數(shù)的方式 (當(dāng)有函數(shù)被調(diào)用時(shí),整個(gè)局部環(huán)境和返回地址入棧,之后函數(shù)返回時(shí), 返回地址和環(huán)境變量出棧. 有個(gè)棧包含全部信息,無(wú)論函數(shù)調(diào)用的是否是它本身。棧就包含了遞歸。實(shí)際上,你總能顯式地使用棧將遞歸程序非遞歸化。)
隊(duì)列的應(yīng)用:
應(yīng)用程序
·數(shù)據(jù)緩沖區(qū)(iPod,TiVo,聲卡...)
·異步數(shù)據(jù)傳輸(文件IO,sockets...)
·共享資源上分配請(qǐng)求(打印機(jī),處理器...)
...
模擬現(xiàn)實(shí)世界
·交通的流量分析
·呼叫中心客戶(hù)的等待時(shí)間
·確定在超市收銀員的數(shù)量...
前面的一些基本數(shù)據(jù)結(jié)構(gòu)和實(shí)現(xiàn)看起來(lái)相當(dāng)基礎(chǔ)和簡(jiǎn)單,但馬上我們就要涉及這些基本概念的一些非常復(fù)雜的應(yīng)用。
首先要提到的是我們實(shí)現(xiàn)的數(shù)據(jù)類(lèi)型和數(shù)據(jù)結(jié)構(gòu)往往能在 Java 庫(kù)中找到,很多編程環(huán)境都是如此。比如在 Java 庫(kù)中就能找到棧和隊(duì)列。
Java 對(duì)于集合有一個(gè)通用 API,就是所謂的List接口。具體請(qǐng)查看對(duì)應(yīng)版本 jdk 的源碼。
List接口包含從表尾添加,從表頭移除之類(lèi)的方法,而且它的實(shí)現(xiàn)使用的是可變大小數(shù)組。
在 Java 庫(kù)中
java.util.ArrayList 使用調(diào)整大小數(shù)組實(shí)現(xiàn)
java.util.LinkedList 使用雙向鏈表實(shí)現(xiàn)
我們考慮的很多原則其實(shí) Java 庫(kù)中 LinkedList 接口一樣考慮了。 但是我們目前還是要用我們自己的實(shí)現(xiàn)。
問(wèn)題在于這樣的庫(kù)一般是開(kāi)發(fā)組(committee phenomenon)設(shè)計(jì)的,里頭加入了越來(lái)越多的操作,API變得過(guò)寬和臃腫。
在API中擁有非常多的操作并不好,等成為有經(jīng)驗(yàn)的程序員以后知道自己在做什么了,就可以高效地使用一些集合庫(kù)。但是經(jīng)驗(yàn)不足的程序員使用庫(kù)經(jīng)常會(huì)遇到問(wèn)題。用這樣包含了那么多操作的,像瑞士軍刀一樣的實(shí)現(xiàn),很難知道你的客戶(hù)端需要的那組操作是否是高效實(shí)現(xiàn)的。所以這門(mén)算法課上堅(jiān)持的原則是我們?cè)谡n上實(shí)現(xiàn)之前,能表明你理解性能指標(biāo)前,先不要使用 Java 庫(kù).
Q. 在不運(yùn)行程序的情況下觀察下面一段將會(huì)打印什么?
int n = 50; Stackstack = new Stack (); while (n > 0) { stack.push(n % 2); n = n / 2; } for (int digit : stack) { StdOut.print(digit); } StdOut.println();
A. 110010
值得注意的是,如果使用的是上邊我們自己定義的 iterator 去遍歷,那么得到的就是符合棧后進(jìn)先出特點(diǎn)的答案,但是如果直接使用java.util.Stack 中的Stack,在重寫(xiě)遍歷方式前,他得到的就是先進(jìn)先出的答案,這不符合棧的數(shù)據(jù)類(lèi)型特點(diǎn)。
這是因?yàn)?JDK (以下以 jdk8 為例) 中的 Stack 繼承了 Vector 類(lèi)
package java.util; public class Stackextends Vector { ... }
而 Vector 這個(gè)類(lèi)中 Stack 實(shí)現(xiàn)的迭代器的默認(rèn)的遍歷方式是FIFO的,并不是棧特點(diǎn)的LIFO
狀態(tài)(已關(guān)閉,短期不會(huì)修復(fù)):讓 JDK 中的棧去繼承 Vector 類(lèi)并不是一個(gè)好的設(shè)計(jì),但是因?yàn)榧嫒菪缘膯?wèn)題所以不會(huì)去修復(fù)。
所以更印證了前面的提議,如果在沒(méi)有對(duì) JDK 底層數(shù)據(jù)結(jié)構(gòu)有熟悉的了解前,提交的作業(yè)不推薦使用 JDK 封裝的數(shù)據(jù)結(jié)構(gòu)!
編程練習(xí)Programming Assignment 2: Deques and Randomized Queues
原題地址:里頭有具體的 API 要求和數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)的性能要求。
使用泛型實(shí)現(xiàn)雙端隊(duì)列和隨機(jī)隊(duì)列。此作業(yè)的目標(biāo)是使用數(shù)組和鏈表實(shí)現(xiàn)基本數(shù)據(jù)結(jié)構(gòu),并加深泛型和迭代器的理解。
Dequeue: double-ended queue or deque (發(fā)音為“deck”) 是棧和隊(duì)列的概括,支持從數(shù)據(jù)結(jié)構(gòu)的前面或后面添加和刪除元素
性能要求:deque 的實(shí)現(xiàn)必須在最壞的情況下支持每個(gè)操作(包括構(gòu)造函數(shù))在最壞情況下花費(fèi)常量時(shí)間。一個(gè)包含 n 個(gè)元素的雙端隊(duì)列最多使用 48n + 192 個(gè)字節(jié)的內(nèi)存,并使用與雙端隊(duì)列當(dāng)前元素?cái)?shù)量成比例的空間。此外,迭代器實(shí)現(xiàn)必須支持在最壞的情況下每個(gè)操作(包括構(gòu)造函數(shù))都是用常數(shù)時(shí)間。
Randomized queue: 隨機(jī)隊(duì)列類(lèi)似于棧或隊(duì)列,除了從數(shù)據(jù)結(jié)構(gòu)中移除的元素是隨機(jī)均勻選擇的。
性能要求:隨機(jī)隊(duì)列的實(shí)現(xiàn)必須支持每個(gè)操作( 包括生成迭代器 )都花費(fèi)常量的平攤時(shí)間。
也就是說(shuō),對(duì)于某些常數(shù)c,任意 m 個(gè)隨機(jī)隊(duì)列操作序列(從空隊(duì)列開(kāi)始)在最壞情況下最多 c 乘以 m 步。包含n個(gè)元素的隨機(jī)隊(duì)列使用最多48n + 192 個(gè)字節(jié)的內(nèi)存。
此外,迭代器實(shí)現(xiàn)必須支持在最壞情況下 next()和 hasNext()操作花費(fèi)常量時(shí)間; 迭代器中的構(gòu)造函數(shù)花費(fèi)線(xiàn)性時(shí)間; 可能(并且將需要)為每個(gè)迭代器使用線(xiàn)性數(shù)量的額外內(nèi)存。
Permutation: 寫(xiě)一個(gè)叫 Permutation.java 的客戶(hù)端,將整數(shù) k 作為命令行參數(shù); 使用StdIn.readString() 讀取標(biāo)準(zhǔn)輸入的字符串序列; 并且隨機(jī)均勻地打印它們中的 k 個(gè)。每個(gè)元素從序列中最多打印一次。
比如在輸入端的序列是:
% more distinct.txt
A B C D E F G H I
那么打印的時(shí)候:
% java-algs4 Permutation 3 < distinct.txt
C
G
A
而絕對(duì)不出現(xiàn):
C
G
G
同個(gè)元素被多次打印的情況
命令行輸入:可以假設(shè) 0≤k≤n,其中 n 是標(biāo)準(zhǔn)輸入上的字符串的個(gè)數(shù)。
性能要求:客戶(hù)端的運(yùn)行時(shí)間必須與輸入的數(shù)據(jù)大小成線(xiàn)性關(guān)系??梢?xún)H使用 恒定數(shù)量的內(nèi)存 加上 一個(gè)最大大小為 n 的 Deque 或RandomizedQueue 對(duì)象的大小。(對(duì)于額外的挑戰(zhàn),最多只使用一個(gè)最大大小為 k 的 Deque 或 RandomizedQueue 對(duì)象)
每一次作業(yè)都會(huì)有一個(gè) bonus 的分?jǐn)?shù),就是類(lèi)似獎(jiǎng)勵(lì)的分?jǐn)?shù),本次的作業(yè)的額外加分是上分的括號(hào)內(nèi)容,同時(shí)還有內(nèi)存使用之類(lèi)
Test 3 (bonus): check that maximum size of any or Deque or RandomizedQueue object
created is equal to k
filename = tale.txt, n = 138653, k = 5
filename = tale.txt, n = 138653, k = 50
filename = tale.txt, n = 138653, k = 500
filename = tale.txt, n = 138653, k = 5000
filename = tale.txt, n = 138653, k = 50000
==> passed
Test 8 (bonus): Uses at most 40n + 40 bytes of memory
==> passed
Total: 3/2 tests passed!
附錄git 地址 100/100:在此
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/73673.html