成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

棧和隊列 - Algorithms, Part I, week 2 STACKS AND QUEUE

Stardustsky / 3099人閱讀

摘要:在改進前使用數組的一個缺點是必須聲明數組的大小,所以棧有確定的容量。待解決的問題建立一個能夠增長或者縮短到任意大小的棧。下邊的圖是觀察時間開銷的另一種方式,表示了入棧操作需要訪問數組的次數。

前言

上一篇:算法分析
下一篇:基本排序

本篇內容主要是棧,隊列 (和包)的基本數據類型和數據結構
文章里頭所有的對數函數都是以 2 為底
關于性能分析,可能還是需要一些數學知識,有時間可以回一下
在很多應用中,我們需要維護多個對象的集合,而對這個集合的操作也很簡單

基本數據類型

對象的集合

操作:

insert -- 向集合中添加新的對象

remove -- 去掉集合中的某個元素

iterate -- 遍歷集合中的元素并對他們執(zhí)行某種操作

test if empty -- 檢查集合是否為空

做插入和刪除操作時我們要明確以什么樣的形式去添加元素,或我們要刪除集合中的哪個元素。

處理這類問題有兩個經典的基礎數據結構:棧(stack) 和隊列(queue)

兩者的區(qū)別在于去除元素的方式:

棧:去除最近加入的元素,遵循后進先出原則(LIFO: last in first out)。

插入元素對應的術語是入棧 -- push;去掉最近加入的元素叫出棧 -- pop

隊列:去除最開始加入的元素,遵循先進先出原則(FIFO: first in first out)。

關注最開始加入隊列的元素,為了和棧的操作區(qū)分,隊列加入元素的操作叫做入隊 -- enqueue;去除元素的操作叫出隊 -- dequeue

此篇隱含的主題是模塊式編程,也是平時開發(fā)需要遵守的原則

模塊化編程

這一原則的思想是將接口與實現完全分離。比如我們精確定義了一些數據類型和數據結構(如棧,隊列等),我們想要的是把實現這些數據結構的細節(jié)完全與客戶端分離??蛻舳丝梢赃x擇數據結構不同的實現方式,但是客戶端代碼只能執(zhí)行基本操作。

實現的部分無法知道客戶端需求的細節(jié),它所要做的只是實現這些操作,這樣,很多不同的客戶端都可以使用同一個實現,這使得我們能夠用模塊式可復用的算法與數據結構庫來構建更復雜的算法和數據結構,并在必要的時候更關注算法的效率。

Separate client and implementation via API.

API:描述數據類型特征的操作
Client:使用API??操作的客戶端程序。
Implementation:實現API操作的代碼。

下面具體看下這兩種數據結構的實現

棧 Stack 棧 API

假設我們有一個字符串集合,我們想要實現字符串集合的儲存,定期取出并且返回最后加入的字符串,并檢查集合是否為空。我們需要先寫一個客戶端然后再看它的實現。

字符串數據類型的棧

性能要求:所有操作都花費常數時間
客戶端:從標準輸入讀取逆序的字符串序列

測試客戶端
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())
    {
    //從標準輸入獲取一些字符串
    String s = StdIn.readString();
    //如果字符串為"-",則客戶端將棧頂的字符串出棧,并打印出棧的字符串
    if (s.equals("-")) StdOut.print(stack.pop());
    //否則將字符串入棧到棧頂
    else stack.push(s);
    }
}

客戶端輸入輸出:

棧的實現:鏈表

鏈表(linked-list)連接待添加...

我們想保存一個有節(jié)點組成的,用來儲存字符串的鏈表。節(jié)點包含指向鏈表中下一個元素的引用(first).

維持指針 first 指向鏈表中的第一個節(jié)點

Push:入棧,在鏈表頭插入一個新的節(jié)點

Pop:出棧,去掉鏈表頭處第一個節(jié)點

Java 實現
public class LinkedStackOfStrings
{
     //棧中唯一的實例變量是鏈表中的第一個節(jié)點的引用
     private Node first = null;
     
     //內部類,節(jié)點對象,構成鏈表中的元素,由一個字符串和指向另一個節(jié)點的引用組成
     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é)點:我們將要插入表頭的節(jié)點
         first = new Node();
         first.item = item;
         //實例變量的next指針指向鏈表oldfirst元素,現在變成鏈表的第二個元素
         first.next = oldfirst;
     }
     
     //出棧
     public String pop()
     {
         //將鏈表中的第一個元素儲存在標量 item 中
         String item = first.item;
         //去掉第一個節(jié)點:將原先指向第一個元素的指針指向下一個元素,然后第一個節(jié)點就等著被垃圾回收處理
         first = first.next;
         //返回鏈表中原先保存的元素
         return item;
     }
}

圖示

出棧

入棧

性能分析

通過分析提供給客戶算法和數據結構的性能信息,評估這個實現對以不同客戶端程序的資源使用量

Proposition 在最壞的情況下,每個操作只需要消耗常數時間(沒有循環(huán))。
Proposition 具有n個元素的棧使用 ~40n 個字節(jié)內存
(沒有考慮字符串本身的內存,因為這些空間的開銷在客戶端上)

棧的實現:數組

棧用鏈表是實現花費常數的時間,但是棧還有更快的實現

另一種實現棧的 natural way 是使用數組儲存棧上的元素
將棧中的N個元素保存在數組中,索引為 n,n 對應的數組位置即為棧頂的位置,即下一個元素加入的地方

使用數組 s[] 在棧上存儲n個元素。

push():在 s[n] 處添加新元素。

pop():從 s[n-1] 中刪除元素。

在改進前使用數組的一個缺點是必須聲明數組的大小,所以棧有確定的容量。如果棧上的元素個數比棧的容量多,我們就必須處理這個問題(調整數組)

Java 實現
public class FixedCapacityStackOfStrings
{
     private String[] s;
     //n 為棧的大小,棧中下一個開放位置,也為下一個元素的索引
     private int n = 0;
     
    //int capacity:看以下說明
     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()
     { 
         //然后返回數組n-1的元素
         return s[--n]; 
     }
}

int capacity: 在構造函數中加入了容量的參數,破壞了API,需要客戶端提供棧的容量。不過實際上我們不會這么做,因為大多數情況下,客戶端也無法確定需要多大棧,而且客戶端也可能需要同時維護很多棧,這些棧又不同時間到達最大容量,同時還有其他因素的影響。這里只是為了簡化。在調整數組中會處理可變容量的問題,避免溢出

對于兩種實現的思考

上述的實現中我們暫時沒有處理的問題:

Overflow and underflow

Underflow :客戶端從空棧中出棧我們沒有拋出異常

Overflow :使用數組實現,當客戶端入棧超過容量發(fā)生棧溢出的問題

Null item:客戶端是否能向數據結構中插入空元素,上邊我們是允許的

Duplicate items: 客戶端是否能向數據結構中重復入棧同一個元素,上邊我們是允許的

Loitering 對象游離:即在棧的數組中,我們有一個對象的引用,可是我們已經不再使用這個引用了

數組中當我們減小 n 時,在數組中仍然有我們已經出棧的對象的指針,盡管我們不再使用它,但是Java系統并不知道。所以為了避免這個問題,有效地利用內存,最好將去除元素對應的項設為 null,這樣就不會剩下舊元素的引用指針,接下來就等著垃圾回收機制去回收這些內存。這個問題比較細節(jié)化,但是卻很重要。

public String pop()
{
 String item = s[--n];
 s[n] = null;
 return item;
} 
調整數組

之前棧的基本數組實現需要客戶端提供棧的最大容量,現在我們來看解決這個問題的技術。

待解決的問題:建立一個能夠增長或者縮短到任意大小的棧。
調整大小是一個挑戰(zhàn),而且要通過某種方式確保它不會頻繁地發(fā)生。

怎樣加長數組

反復增倍法 (repeated doubling):當數組被填滿時,建立一個大小翻倍的新數組,然后將所有的元素復制過去。這樣我們就不會那么頻繁地創(chuàng)建新數組。

Java 實現
public class ResizingArrayStackOfStrings {

    private String[] s;

    //n 為棧的大小,棧中下一個開放位置,也為下一個元素的索引
    private int n = 0;

    public ResizingArrayStackOfStrings(){
        s = new String[2];
    }

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

    /**
     * 從大小為1的數組開始,如果發(fā)現數組被填滿,那么就在插入元素之前,將數組長度調整為原來的2倍
     * @param item
     */
    public void push(String item) {
        if (n == s.length) resize(2 * s.length);
        s[n++] = item;
    }

    /**
     * 調整數組方法
     * 創(chuàng)建具有目標容量的新數組,然后把當前棧復制到新棧的前一半
     * 然后重新設置和返回實例標量
     * @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 個元素的時間復雜度是線性相近的,即與 n 成正比 ~n

Q. 假設我們從一個空的棧開始,我們執(zhí)行 n 次入棧, 那么我們的 **resize()** 方法被調用了幾次?
A. 是以 2 為底的對數次。因為我們只有在棧的大小等于 2 的冪函數的時候,即 2^1,2^2,2^3 ... 2^i 的時候才會調用 resize().
   在 1 到 n 之間,符合 2 的冪的數字(如 2,4,8,16...) 一共有 logn 個,其中 log 以為 2 為底.

我們在插入 2^i 個元素時,需要復制數組 logn 次,需要花費訪問數組 n + (2 + 4 + 8 + ... + m) ~3n 的時間,其中 m = 2^logn = n

n : 無論數組翻不翻倍,對于每個新元素,入棧需要 Θ(1) 時間。因此,對于 n 個元素,它需要 Θ(n) 時間。即忽略常數項,插入 n 個 就有 n 次入棧的操作,就訪問 n 次數組

(2 + 4 + 8 + ... + n):

如果 n = 2^i 個元素入棧,需要數組翻倍 lgn 次。
從技術上講,總和(2 + 4 + 8 + .. + m)是具有 logN 個元素的幾何級數
然后:(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

舉個栗子~,如果我們往棧中插入 8 (2^3) 個元素,那么我們必須將數組翻倍 lg8 次,即3次。因此,8個元素入棧的開銷為 8 +(2 + 4 + 8)= 22 次 ≈ 24 次

再舉個栗子~,如果插入 16 (2^4) 個元素,那么我們必須將數組翻倍 lg16 次,即4次。因此,16個元素入棧的開銷為 16 +(2 + 4 + 8 + 16)= 46 次 ≈ 48 次

或者粗略想象一下,如果我們計算一下開銷,插入前 n (n = 2^i) 個元素,是對 2 的冪從1到N求和。 這樣,總的開銷大約是3N。先要訪問數組一次,對于復制要訪問兩次。所以,要插入元素,大約需要訪問數組三次。
下邊的圖是觀察時間開銷的另一種方式,表示了入棧操作需要訪問數組的次數。

每次遇到2的冪,需要進行斜線上的數組訪問時間,但是從宏觀上來看,是將那些元素入棧上花去了紅色直線那些時間 這叫做平攤分析??紤]開銷時將總的開銷平均給所有的操作。關于平攤分析就不再解釋了,有興趣可以自行了解...

怎樣縮小數組

如果數組翻倍多次后,又有多次出棧,那么如果不調整數組大小,數組可能會變得太空。那數組在什么情況下去縮小,縮小多少才合適?
我們也許這么考慮,當數組滿了的時候將容量翻倍,那么當它只有一半滿的時候,將容量縮減一半。但是這樣并不合理,因為有一種現象叫做thrashing:即客戶端剛好反復交替入棧出棧入棧出棧...

如果數組滿了就會反復翻倍減半翻倍減半,并且每個操作都會新建數組,都要花掉正比與N的時間,這樣就會導致thrashing頻繁發(fā)生要花費平方時間,這不是我們想要的。

有效的解決方案是直到數組變?yōu)?1/4 滿的時候才將容量減半
我們只要測試數組是否為 1/4 滿,如果是,則調整大小使其為 1/2 滿。

不變式:數組總是介于25% 滿與全滿之間

 public String pop()
 {
     String item = s[--n];
     //解決之前說的對象引用游離問題
     s[n] = null;
     if (n > 0 && n == s.length/4) resize(s.length/2);
     return item;
 }

這樣的好處:

因為是半滿的,既可以插入向棧插入元素,又可以從棧刪除元素,而不需要再次進行調整數組大小的操作直到數組全滿,或者再次1/4滿。

每次調整大小時, 開銷已經在平攤給了每次入棧和出棧

下圖展示了上邊測試寫的客戶端例子中數組上的操作

可以看到在開始時,數組大小從1倍增到2又到4,但一旦到8,數組的大小則維持一段時間不變,直到數組中只有2個元素時才縮小到4,等等。

算法分析 運行時間

數組調整大小并不經常發(fā)生,但這是實現棧API的一種很有效的方式,客戶端不需要提供棧的最大容量,但依然保證了我們使用的內存大小總是棧中實際元素個數的常數倍,所以分析說明對于任意的操作序列,每個操作的平均運行時間與常數成正比。

這里,存在最壞情況(worst case)。當棧容量翻倍時,需要正比于N的時間,所以性能不如我們想要的那么好,但是優(yōu)勢在于進行入棧出棧操作時非???,入棧只需要訪問數組并移動棧頂索引。對于大多數操作都很高效的。對于眾多的客戶端這是個很有效的權衡。

內存使用

棧的內存用量實際上比鏈表使用更少的內存。

給出的命題. 一個 ResizingArrayStackOfStrings 內存用量在 ~8n bytes~32n bytes 之間,取決于數組有多滿。

只看 “private String[] s;”

?~ 8n 當數組滿時. -- 棧的實際長度 = n,所以內存占用是 8 乘以棧不為空的元素個數
?~ 32n 當數組 1/4 滿時. -- 棧的實際長度 = 4n,所以內存占用是 8 乘以(4 乘以棧的有效元素個數)

這里只是計算了 Java中數組占用的空間。同樣地,這個分析只針對棧本身 而不包括客戶端上的字符串。

調整數組實現VS鏈表實現

那么使用可調整大小的數組與鏈表之間如何取舍呢?

這是兩種 API相同的不同的實現,客戶端可以互換使用。

哪個更好呢?

很多情形中,我們會有同一API的多種實現。你需要根據客戶端的性質選擇合適的實現。

Linked-list implementation.

對于鏈表,每個操作最壞情況下需要常數時間,這是有保障的

但是為了處理鏈接,我們需要一些額外的時間和空間。所以鏈表實現會慢一些

Resizing-array implementation.

可調大小的數組實現有很好的分攤時間,所以整個過程總的平均效率不錯

浪費更少的空間,對于每個操作也許有更快的實現

所以對于一些客戶端,也許會有區(qū)別。以下這樣的情形你不會想用可調大小數組實現:你有一架飛機進場等待降落,你不想系統突然間不能高效運轉;或者互聯網上的一個路由器,數據包以很高的速度涌進來,你不想因為某個操作突然變得很慢而丟失一些數據。

客戶端就可以權衡,如果想要獲得保證每個操作能夠很快完成,就使用鏈表實現;
如果不需要保證每個操作,只是關心總的時間,可能就是用可調大小數組實現。因為總的時間會小得多,如果不是最壞情況下單個操作非常快。
盡管只有這些簡單的數據結構,我們都需要做很重要的權衡,在很多實際情形中真的會產生影響。

隊列 Queue

接下來我們簡要考慮一下使用相同基本底層數據結構的隊列的實現。

隊列的API

這是字符串隊列對應的API,實際上和棧的API是相同的,只是名字不一樣而已...
入棧換成了入隊(enqueue),出棧換成了出隊(dequeue)。語義是不同的。
入隊操作向隊尾添加元素,而出隊操作從隊首移除元素。
就像你排隊買票一樣,入隊時你排在隊列的最后,在隊列里待的最久的人是下一個離開隊列的人。

數據結構的性能要求:所有操作都花費常數時間。

鏈表實現

隊列的鏈表表示中,我們需要維護兩個指針引用。一個是鏈表中的第一個元素,另一個是鏈表最后一個元素。

插入的時候我們在鏈表末端添加元素;移除元素的時候不變,依然從鏈表頭取出元素。

出隊 dequeue

那么這就是出隊操作的實現,和棧的出棧操作是一樣的。保存元素,前進指針指向下一個節(jié)點,這樣就刪除了第一個節(jié)點,然后返回該元素。

入隊 enqueue

入隊操作時,向鏈表添加新節(jié)點。我們要把它放在鏈表末端,這樣它就是最后一個出隊的元素。
首先要做的是保存指向最后一個節(jié)點的指針,因為我們需要將它指向下一個節(jié)點的指針從null變?yōu)樾碌墓?jié)點。
然后給鏈表末端創(chuàng)建新的節(jié)點并對其屬性賦值,將舊的指針從null變?yōu)橹赶蛐鹿?jié)點。

實際上現在指針操作僅限于如棧和隊列這樣的少數幾個實現以及一些其他的基本數據結構了。現在很多操作鏈表的通用程序都封裝在了這樣的基本數據類型里。

Java 實現

這里處理了當隊列為空時的特殊情況。為了保證去除最后一個元素后隊列是空的,我們將last設為null,還保證first和last始終都是符合我們預想。
完整代碼: Queue.java 這里用到了泛型和迭代器的實現

數組實現

用可調大小的數組實現并不難,但絕對是一個棘手的編程練習。

我們維護兩個指針,分別指向隊列中的第一個元素和隊尾,即下一個元素要加入的地方。

對于入隊操作在 tail 指向的地方加入新元素

出隊操作移除 head 指向的元素

棘手的地方是一旦指針的位置超過了數組的容量,必須重置指針回到0,這里需要多寫一些代碼,而且和棧一樣實現數據結構的時候你需要加上調整容量的方法。

完整代碼:ResizingArrayQueue.java

額外補充

兩個棧實現隊列的數據結構
實現具有兩個棧的隊列,以便每個隊列操作都是棧操作的常量次數(平攤次數)。
提示:
1.如果將元素推入棧然后全部出棧,它們將以相反的順序出現。如果你重復這個過程,它們現在又恢復了正常的隊列順序。
2.為了避免不停的出棧入棧,可以加某個判定條件,比如當 dequeue 棧為空時,將 enqueue 棧的元素出棧到 dequeue 棧,然后最后從dequeue 棧出棧,也就實現了出隊的操作。直到 dequeue 棧的元素都出棧了,再次觸發(fā)出隊操作時,再從 enqueue 棧導入數據重復上邊的過程

實現參考:QueueWithTwoStacks.java

泛型 -- Generic

接下來我們要處理的是前面實現里另一個根本性的缺陷。前面的實現只適用于字符串,如果想要實現其他類型數據的隊列和棧怎么 StackOfURLs, StackOfInts... 嗯。。。
這個問題就涉及泛型的話題了。

泛型的引出

實際上很多編程環(huán)境中這一點都是不得不考慮的。

第一種方法:我們對每一個數據類型都實現一個多帶帶的棧

這太雞肋了,我們要把代碼復制到需要實現棧的地方,然后把數據類型改成這個型那個型,那么如果我們要處理上百個不同的數據類型,我們就得有上百個不同的實現,想想就很心累。

不幸的是Java 推出 1.5 版本前就是陷在這種模式里,并且非常多的編程語言都無法擺脫這樣的模式。所以我們需要采用一種現代模式,不用給每個類型的數據都分別搞實現。

第二種方法:我們對 Object 類實現數據結構
有一個廣泛采用的捷徑是使用強制類型轉換對不同的數據類型重用代碼。Java中所有的類都是 Object 的子類,當客戶端使用時,就將結果轉換為對應的類型。但是這種解決方案并不令人滿意。
這個例子中我們有兩個棧:蘋果的棧和桔子的棧。接下來,當從蘋果棧出棧的時候需要客戶端將出棧元素強制轉換為蘋果類型,這樣類型檢查系統才不會報錯。

這樣做的問題在于:

必須客戶端完成強制類型轉換,通過編譯檢查。

存在一個隱患,如果類型不匹配,會發(fā)生運行時錯誤。

第三種方法:使用泛型

這種方法中客戶端程序不需要強制類型轉換。在編譯時就能發(fā)現類型不匹配的錯誤,而不是在運行時。

這個使用泛型的例子中棧的類型有一個類型參數(Apple),在代碼里這個尖括號中。

如果我們有一個蘋果棧,并且試圖入棧一個桔子,我們在編譯時就會提示錯誤,因為聲明中那個棧只包含蘋果,桔子禁止入內。

優(yōu)秀的模塊化編程的指導原則就是我們應當歡迎編譯時錯誤,避免運行時錯誤。

因為如果我們能在編譯時檢測到錯誤,我們給客戶交付產品或者部署對一個API的實現時,就有把握對于任何客戶端都是沒問題的。
有些運行時才會出現的錯誤可能在某些客戶端的開發(fā)中幾年之后才出現,如果這樣,就必須部署我們的軟件,這對每個人都是很困難的。

實際上優(yōu)秀的泛型實現并不難。只需要把每處使用的字符串替換為泛型類型名稱。

鏈表棧的泛型實現

如這里的代碼所示,左邊是我們使用鏈表實現的字符串棧,右邊是泛型實現。

左邊每處用到字符串類型的地方我們換成了item。在最上面類聲明的地方我們用尖括號聲明 item 是我們要用的泛型類型,這樣的實現非常直截了當,并且出色地解決了不同的數據類型多帶帶實現的問題。

數組棧的泛型實現

基于數組的實現,這種方法不管用。目前很多編程語言這方面都有問題,而對Java尤其是個難題。我們想做的是用泛型名稱 item 直接聲明一個新的數組。比如這樣:

public class FixedCapacityStack
{
 private Item[] s;
 private int n = 0;

     public FixedCapacityStack(int capacity)
     //看這里看這里像這樣,但是實際我們在java當中我卻不能這樣方便的實現
     { s = new Item[capacity]; }

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

     public void push(Item item)
     { s[n++] = item; }

     public Item pop()
     { return s[--n]; }
}

如有備注的這行所示。其他部分都和之前的方法沒區(qū)別。不幸的是,Java不允許創(chuàng)建泛型數組。對于這個問題有各種技術方面的原因,在網上關于這個問題你能看到大量的爭論,這個不在我們討論的范圍之內。關于協變的內容,可以自行了解,嗯。。。我一會也去了解了解...

這里,要行得通我們需要加入強制類型轉換。
我們創(chuàng)建 Object 數組,然后將類型轉換為 item 數組。教授的觀點是優(yōu)秀的代碼應該不用強制類型轉換, 要盡量避免強制類型轉換,因為它確實在我們的實現中留下了隱患。但這個情況中我們必須加入這個強制類型轉換。

當我們編譯這個程序的時候,Java會發(fā)出警告信息說我們在使用未經檢查或者不安全的操作,詳細信息需要使用-Xlint=unchecked 參數重新編譯。

我們加上這個參數重新編譯之后顯示你在代碼中加入了一個未經檢查的強制類型轉換,然后 java 就警告你不應該加入未經檢查的強制類型轉換。但是這么做并不是我們的錯,因為你不允許我們聲明泛型數組,我們才不得不這么做。收到這個警告信息請不要認為是你的代碼中有什么問題。

自動裝箱 (Autoboxing) 與拆箱 (Unboxing)

接下來,是個跟Java有關的細節(jié)問題,
Q. 對基本數據類型,我們怎樣使用泛型?
我們用的泛型類型是針對 Object 及其子類的。前面講過,是從 Object 數組強制類型轉換來的。為了處理基本類型,我們需要使用Java的包裝對象類型。

如大寫的 Integer 是整型的包裝類型等等。另外,有個過程叫自動打包,自動轉換基本類型與包裝類型,所以處理基本類型這個問題,基本上都是在后臺完成的.

Autoboxing:基本數據類型到包裝類型的自動轉換。
unboxing:包裝器類型到基本數據類型的自動轉換。

綜上所述,我們能定義適用于任何數據類型的泛型棧的API,而且我們有基于鏈表和數組兩種實現。我們講過的使用可變大小數組或者鏈表,對于任何數據類型都有非常好的性能。

額外補充

在 Java 6, 必須在變量聲明(左側)和構造函數調用(右側)中指定具體類型。從Java 7 開始,可以使用菱形運算符:
Stack stack = new Stack<>();

Q. 為什么Java需要強制轉換(或反射)?
簡短的回答: 向后兼容性。
詳細地回答:需要了解類型擦除協變數組

Q. 當我嘗試創(chuàng)建泛型數組時,為什么會出現“無法創(chuàng)建泛型數組”錯誤?
public class ResizingArrayStack {Item[] a = new Item[1];}

A. 根本原因是Java中的數組是協變的,但泛型不是。換句話說,String [] 是 Object [] 的子類型,但 Stack 不是 Stack 的子類型。

Q. 那么,為什么數組是協變的呢?

A. 許多程序員(和編程語言理論家)認為協變數組是 Java 類型系統中的一個嚴重缺陷:它們會產生不必要的運行時性能開銷(例如,參見ArrayStoreException)并且可能導致細微的 BUG。在Java中引入了協變數組,以避免Java在其設計中最初沒有包含泛型的問題,例如,實現Arrays.sort(Comparable [])并使用 String [] 類型的輸入數組進行調用。

Q. 我可以創(chuàng)建并返回參數化類型的新數組,例如,為泛型隊列實現 toArray() 方法嗎?

A. 不容易。如果客戶端將所需具體類型的對象傳遞給 toArray(),則可以使用反射來執(zhí)行此操作。這是 Java 的 Collection Framework 采用的(笨拙)方法。

迭代器 Iterators

Java還提供了另一種能夠使客戶端代碼保持優(yōu)雅緊湊,絕對值得添加到我們的基本數據類型的特性 -- 迭代器。

遍歷

對于遍歷功能,大多數客戶端想做的只是遍歷集合中的元素,我們考慮的任何內部表示,這對于客戶端是不相關的,他們并不關心集合的內部實現。也就是說我們允許客戶端遍歷集合中的元素,但不必讓客戶端知道我們是用數組還是鏈表。

Java提供了一個解決方式,就是實現遍歷機制,然后使用 foreach.

Foreach loop

我們自找麻煩地要讓我們的數據類型添加迭代器是因為,如果我們的數據結構是可遍歷的,在Java中我們就可以使用非常緊湊優(yōu)雅的客戶端代碼,即所謂的for-each語句來進行集合的遍歷。
使用了迭代器后,以下兩種寫法都可以不考慮底層的內部實現而遍歷某個集合,兩種方法是等價的:

Stack stack;
...

// "foreach" loop
for (String s : stack)
    StdOut.println(s);
    
...
// 與上邊的方法等價
Iterator i = stack.iterator();
while (i.hasNext())
{
    String s = i.next();
    StdOut.println(s);
}    

所以如果我們有一個棧 stack 可以寫(for String s: stack) 表示對棧中每個字符串,執(zhí)行打印輸出。
我們也可以寫成下邊這種完整形式的代碼,但不會有人這么做,因為它和上邊的簡寫形式是等價的。
不使用迭代器的話要實現遍歷客戶端代碼中就要執(zhí)行非常多不必要的入棧出棧操作。所以這是能夠讓遍歷數據結構中的元素的客戶端代碼變得這么緊湊的關鍵所在。

要使用戶定義的集合支持 foreach 循環(huán):

數據類型必須具有名為 iterator() 的方法

iterator() 方法返回一個對象,這個對象具有兩個核心方法:

hasNext() 方法, 當不再遍歷到任何元素時,返回false

next() 方法, 返回集合中的下一個元素

迭代器

為了支持 foreach 循環(huán),Java 提供了兩個接口。

Iterator 接口:有 next() 和 hasNext() 方法。

Iterable 接口:iterator() 方法返回 一個迭代器 Iterator

兩者都應該與泛型一起使用

Q. 什么是 Iterable ?
A. 在 Java 語言中 Iterable 是具有返回迭代器的方法的一種類。

來源:jdk 8 java.lang.Iterable 接口

//此處T可以隨便寫為任意標識,常見的如T、E、K、V等形式的參數常用于表示泛型
//在實例化泛型類時,必須指定T的具體類型
public interface Iterable {
    /**
     * Returns an iterator over elements of type {@code T}.
     *
     * @return an Iterator.
     */
    Iterator iterator();
    ...
}

Q. 那么什么是迭代器 iterator ?
A. 迭代器是具有 hasNext() 和 next() 方法的類。

來源:jdk 8 java.util.Iterator 接口

public interface Iterator {
    boolean hasNext();
    E next();
    default void remove()
    default void forEachRemaining(Consumer action)
}

Java還允許 remove() 方法,但我們認為這不是一個很好的特性,它有可能成為調試隱患,一般不用。
那么,只要有 hasNext() 和 next() 方法就使得數據結構是可遍歷的,所以我們要實現這兩個方法。

下面我們要做的是看看如何使我們的棧、隊列和后面要講到的其他數據結構實現所謂的 Iterable(可遍歷類)接口。

實例

我們要給我們所有的基本數據結構提供遍歷機制。實現這個功能并不特別難,而且絕對值得投入精力。這是基于棧的代碼。

棧實現迭代器: 鏈表實現

接下來我們要實現Iterable接口。
實現Iterable接口意味著什么呢?這個類需要有iterator()方法返回迭代器。
什么是迭代器呢?我們要用一個內部類。這個例子中,命名為 ListIterator 的內部類實現 Iterator 接口,并且是泛化(generic)的。
ListIterator 這個類主要完成的是實現 hasNext() 和 next() 方法。從名字就能清楚知道它們的語義。

hasNext() 在完成遍歷之后會返回 false;如果還沒有完成,應該返回 true

next() 方法提供要遍歷的下一個元素

所以如果是基于鏈表的數據結構,我們要從表頭 first 元素開始,這是處于表頭的元素.
我們要維護迭代器中的實例變量 current 存儲當前正在遍歷的元素。我們取出current元素,然后將 current 引用指向下一個元素,并返回之前儲存的item,也就將current 移動到了下一個元素上。
客戶端會一直測試 hasNext(),所以當current變成空指針,hasNext 返回 false 終止遍歷。
在我們的遍歷中,我們只需要關注實現 next() 和 hasNext()方法,使用一個局部實例變量 current 就能完成。
如果遍歷已經終止,客戶端還試圖調用 next() 或者試圖調用 remove() 時拋出異常,我們不提供remove()方法。

完整代碼:StackImpIterator

棧實現迭代器: 數組實現

對于基于數組的實現,就更簡單了。使用迭代器我們能控制遍歷順序,使其符合語義和數據結構。
遍歷棧時你要讓元素以出棧的順序返回,即對于數組是逆序的,那么這種情況下next() 就將索引減 1,返回下一個元素。
而我們的實例變量是數組的索引。
只要該變量為正,hasNext() 返回 true。要實現這個遍歷機制只需要寫幾行Java代碼,以后遇到涉及對象集合的基本數據類型中我們都會用這種編程范式。

完整代碼:ResizingArrayStack

實際上很多客戶端并不關心我們返回元素的順序。我們經常做的是直接向集合中插入元素,接下來遍歷已有的元素。這樣的數據結構叫做背包。

我們來看看它的API。

順序并不重要,所以我們想要能直接添加元素,也許還想知道集合大小。
我們想遍歷背包中所有的元素,這個API更簡單,功能更少,但依然提供了幾個重要的操作。
使用這個API,我們已經看過實現了,只需要將棧的出棧操作或者隊列的出隊操作去掉,就能獲得這個有用的數據結構的良好的實現

完整代碼:Bag--ListIterator ;Bag--ArrayIterator

棧與隊列的應用

棧的應用

棧確實非?;A,很多計算基于它運行 因為它能實現遞歸
·Java虛擬機
·解析編譯器 (處理編譯一種編程語言或者解釋為實際的計算)
·文字處理器中的撤消
·Web瀏覽器中的后退按鈕(當你使用網頁瀏覽器上的后退按鈕時,你去過的網頁就存儲在棧上)
·打印機的PostScript語言
·在編譯器中實現函數的方式 (當有函數被調用時,整個局部環(huán)境和返回地址入棧,之后函數返回時, 返回地址和環(huán)境變量出棧. 有個棧包含全部信息,無論函數調用的是否是它本身。棧就包含了遞歸。實際上,你總能顯式地使用棧將遞歸程序非遞歸化。)

隊列的應用

應用程序
·數據緩沖區(qū)(iPod,TiVo,聲卡...)
·異步數據傳輸(文件IO,sockets...)
·共享資源上分配請求(打印機,處理器...)
...

模擬現實世界
·交通的流量分析
·呼叫中心客戶的等待時間
·確定在超市收銀員的數量...

前面的一些基本數據結構和實現看起來相當基礎和簡單,但馬上我們就要涉及這些基本概念的一些非常復雜的應用。
首先要提到的是我們實現的數據類型和數據結構往往能在 Java 庫中找到,很多編程環(huán)境都是如此。比如在 Java 庫中就能找到棧和隊列。
Java 對于集合有一個通用 API,就是所謂的List接口。具體請查看對應版本 jdk 的源碼。
List接口包含從表尾添加,從表頭移除之類的方法,而且它的實現使用的是可變大小數組。

在 Java 庫中

java.util.ArrayList 使用調整大小數組實現

java.util.LinkedList 使用雙向鏈表實現

我們考慮的很多原則其實 Java 庫中 LinkedList 接口一樣考慮了。 但是我們目前還是要用我們自己的實現。
問題在于這樣的庫一般是開發(fā)組(committee phenomenon)設計的,里頭加入了越來越多的操作,API變得過寬和臃腫。
在API中擁有非常多的操作并不好,等成為有經驗的程序員以后知道自己在做什么了,就可以高效地使用一些集合庫。但是經驗不足的程序員使用庫經常會遇到問題。用這樣包含了那么多操作的,像瑞士軍刀一樣的實現,很難知道你的客戶端需要的那組操作是否是高效實現的。所以這門算法課上堅持的原則是我們在課上實現之前,能表明你理解性能指標前,先不要使用 Java 庫.

Q. 在不運行程序的情況下觀察下面一段將會打印什么?

int n = 50;

Stack stack = new Stack();
while (n > 0) {
    stack.push(n % 2);
    n = n / 2;
}

for (int digit : stack) {
    StdOut.print(digit);
}

StdOut.println();

A. 110010

值得注意的是,如果使用的是上邊我們自己定義的 iterator 去遍歷,那么得到的就是符合棧后進先出特點的答案,但是如果直接使用java.util.Stack 中的Stack,在重寫遍歷方式前,他得到的就是先進先出的答案,這不符合棧的數據類型特點。

這是因為 JDK (以下以 jdk8 為例) 中的 Stack 繼承了 Vector 類

package java.util;

public class Stack extends Vector {
    ...
}

而 Vector 這個類中 Stack 實現的迭代器的默認的遍歷方式是FIFO的,并不是棧特點的LIFO

狀態(tài)(已關閉,短期不會修復):讓 JDK 中的棧去繼承 Vector 類并不是一個好的設計,但是因為兼容性的問題所以不會去修復。

所以更印證了前面的提議,如果在沒有對 JDK 底層數據結構有熟悉的了解前,提交的作業(yè)不推薦使用 JDK 封裝的數據結構!

編程練習

Programming Assignment 2: Deques and Randomized Queues

原題地址:里頭有具體的 API 要求和數據結構實現的性能要求。

使用泛型實現雙端隊列和隨機隊列。此作業(yè)的目標是使用數組和鏈表實現基本數據結構,并加深泛型和迭代器的理解。

Dequeue: double-ended queue or deque (發(fā)音為“deck”) 是棧和隊列的概括,支持從數據結構的前面或后面添加和刪除元素

性能要求:deque 的實現必須在最壞的情況下支持每個操作(包括構造函數)在最壞情況下花費常量時間。一個包含 n 個元素的雙端隊列最多使用 48n + 192 個字節(jié)的內存,并使用與雙端隊列當前元素數量成比例的空間。此外,迭代器實現必須支持在最壞的情況下每個操作(包括構造函數)都是用常數時間。

Randomized queue: 隨機隊列類似于?;蜿犃?,除了從數據結構中移除的元素是隨機均勻選擇的。

性能要求:隨機隊列的實現必須支持每個操作( 包括生成迭代器 )都花費常量的平攤時間。
也就是說,對于某些常數c,任意 m 個隨機隊列操作序列(從空隊列開始)在最壞情況下最多 c 乘以 m 步。包含n個元素的隨機隊列使用最多48n + 192 個字節(jié)的內存。
此外,迭代器實現必須支持在最壞情況下 next()和 hasNext()操作花費常量時間; 迭代器中的構造函數花費線性時間; 可能(并且將需要)為每個迭代器使用線性數量的額外內存。

Permutation: 寫一個叫 Permutation.java 的客戶端,將整數 k 作為命令行參數; 使用StdIn.readString() 讀取標準輸入的字符串序列; 并且隨機均勻地打印它們中的 k 個。每個元素從序列中最多打印一次。

比如在輸入端的序列是:
% more distinct.txt
A B C D E F G H I

那么打印的時候:
% java-algs4 Permutation 3 < distinct.txt
C
G
A

而絕對不出現:
C
G
G
同個元素被多次打印的情況

命令行輸入:可以假設 0≤k≤n,其中 n 是標準輸入上的字符串的個數。

性能要求:客戶端的運行時間必須與輸入的數據大小成線性關系??梢詢H使用 恒定數量的內存 加上 一個最大大小為 n 的 Deque 或RandomizedQueue 對象的大小。(對于額外的挑戰(zhàn),最多只使用一個最大大小為 k 的 Deque 或 RandomizedQueue 對象)

每一次作業(yè)都會有一個 bonus 的分數,就是類似獎勵的分數,本次的作業(yè)的額外加分是上分的括號內容,同時還有內存使用之類

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:在此

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://systransis.cn/yun/73673.html

相關文章

  • Stack & Queue 棧和隊列的學習筆記

    摘要:的前部分內容講的是棧和隊列的實現。學習環(huán)境在學習這門課之前,先引入的概念,即抽象數據類型。鏈表實現學習,鏈表實現簡單的數組實現鏈表實現簡單的數組實現解決使用?;蛘哧犃袝r,的數據類型指定問題。 Week2 的前部分內容講的是棧和隊列的Java實現。學習環(huán)境:mac, inteliJ, java version 1.8.0_77 在學習這門課之前,先引入Abstract Data Type...

    peixn 評論0 收藏0
  • LeetCode 232:用棧實現隊列 Implement Queue using Stacks

    摘要:題目使用棧實現隊列的下列操作將一個元素放入隊列的尾部。用棧實現隊列,可以用兩個棧完成題解。入隊列時用存入節(jié)點,出隊列時內節(jié)點順序出棧壓入中。這類編程語言就壓根不需要用隊列實現棧或用棧實現隊列這種問題,因為棧和隊列本身就必須借助實現。 題目: 使用棧實現隊列的下列操作: push(x) -- 將一個元素放入隊列的尾部。 pop() -- 從隊列首部移除元素。 peek() -- 返回隊...

    cloud 評論0 收藏0
  • 算法面試:棧實現隊列的方案

    摘要:解決方案二入隊都在中進行,用于隊列,同時保證所有元素都在一個棧里,且遵循以下約束入隊不管空與否,都將中的所有元素壓入中出隊若中不為空,則直接從中彈出元素若為空,則說明隊列為空,不能出隊。 聲明:碼字不易,轉載請注明出處,歡迎文章下方討論交流。 前言:Java數據結構與算法專題會不定時更新,歡迎各位讀者監(jiān)督。本篇文章介紹一個有趣的問題:用兩個棧實現一個隊列。這道題來自互聯網公司的算法面試...

    韓冰 評論0 收藏0
  • Java多線程進階(三五)—— J.U.C之collections框架:SynchronousQue

    摘要:三總結主要用于線程之間的數據交換,由于采用無鎖算法,其性能一般比單純的其它阻塞隊列要高。它的最大特點時不存儲實際元素,而是在內部通過?;蜿犃薪Y構保存阻塞線程。 showImg(https://segmentfault.com/img/bVbgOsh?w=900&h=900); 本文首發(fā)于一世流云專欄:https://segmentfault.com/blog... 一、Synchro...

    missonce 評論0 收藏0
  • leetcode225 implement stack using queues

    摘要:題目要求使用隊列來模擬實現一個棧。棧是指先進后出的數據結構,而隊列則是先進先出的數據結構。隊列的包括在隊列尾插入數據,輸出隊列頭的數據,查看隊列的長度,隊列是否為空。 題目要求 Implement the following operations of a queue using stacks. push(x) -- Push element x to the back of que...

    binta 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<