1. 程式人生 > >關於鏈表中哨兵結點問題的深入剖析

關於鏈表中哨兵結點問題的深入剖析

添加 剖析 引入 ont uri size ext 指針 結構

最近正在學習UC Berkeley的CS61B這門課,主要是采用Java語言去實現一些數據結構以及運用數據結構去做一些project。這門課不僅告訴你這個東西怎麽做,而且一步一步探尋為什麽要這樣做以及為什麽會有這些功能。我們有時在接觸某段代碼或功能的實現時,可能直接就看到了它最終的面貌,而不知道如何一步步演化而來,其實每一個功能的添加或優化都是對應一個問題的解決。下面就這門課中關於鏈表中哨兵結點的相關問題進行總結。

什麽是哨兵結點

哨兵顧名思義有巡邏、檢查的功能,在我們程序中通過增加哨兵結點往往能夠簡化邊界條件,從而防止對特殊條件的判斷,使代碼更為簡便優雅,在鏈表中應用最為典型。

單鏈表中的哨兵結點

首先討論哨兵結點在單鏈表中的運用,如果不加哨兵結點在進行頭尾刪除和插入時需要進行特殊判斷。比如在尾部插入結點的代碼如下:

void addLast(int x) {
    if (first == null) {
        first = new Node(x, null);
        return;
    }
    Node p = first;
    while (p.next != null) {
        p = p.next;
    }
    p.next = new Node(x, null);
}

如上所示需要對結點為空的特殊情況進行判斷,頭部加了一個哨兵結點後就可以不需要判斷了(不會為空)

技術分享圖片

雙鏈表中的哨兵結點

Version 1: 雙哨兵

在雙鏈表中需要能夠在頭部和尾部分別進行插入刪除操作(可以實現雙端隊列),為了能快速在尾部進行插入刪除,需要引入指向尾部的指針。截圖如下(圖片來自CS61B)

技術分享圖片

技術分享圖片

上述增加了一個指向尾部的last結點,從上圖可以看出一個問題,last結點有時指向哨兵結點,有時指向實際結點。這會導致特殊情況的出現,比如在進行addFirst操作時,last指向哨兵結點時插入後需要將last往後移動一個,而第二張圖指向實際結點時在頭部插入結點後並不需要改變last指針。這時需要在尾部後也引入一個哨兵結點,以使其一致。相應示意圖如下:

技術分享圖片

技術分享圖片

Version 2:循環雙鏈表

上述Version1需要兩個哨兵結點,可以對其進行改進。可以使用頭部結點的prev指針指向尾部,尾部結點的next指針指向哨兵,這樣就只需要一個哨兵結點,使鏈表變成循環鏈表,比Version1更為簡潔優雅。

技術分享圖片

技術分享圖片

在對如上所示進行插入和刪除操作時一定要格外註意,自己在寫的時候很容易就漏掉某個指針的關系設置,最好在紙上自己畫一遍。(對於要改變的連接可能會影響其他的,這時可將其暫存或最好設置)

在頭部插入的代碼如下:

   public void addFirst(Item item) {
        Node node = new Node(item);
        node.prev = sentinel;
        node.next = sentinel.next;
        sentinel.next.prev = node;
        sentinel.next = node;
        size++;
    }

尾部插入代碼如下:

    public void addLast(Item item) {
        Node node = new Node(item);
        node.prev = sentinel.prev;
        node.next = sentinel;
        sentinel.prev.next = node;
        sentinel.prev = node;
        size++;
    }

頭部刪除代碼如下:

   public Item removeFirst() {
        Item item = sentinel.next.item;
        sentinel.next = sentinel.next.next;
        sentinel.next.prev = sentinel;
        size--;
        return item;
    }

尾部刪除代碼如下

    public Item removeLast() {
        Item item = sentinel.prev.item;
        Node sl = sentinel.prev.prev;
        sl.next = sl.next.next;
        sl.next.prev = sl;
        size--;
        return item;
    }

總結與感想

(1)雖然看起來很小很簡單的事情,但實現起來卻有很多細小問題可以考慮,學會把一件小事做的很漂亮。(small but smart)

(2)學會分析一個東西的來龍去脈,為什麽會有這部分,以及怎麽改進的。

參考:

1.cs61b:https://joshhug.gitbooks.io/hug61b/content/chap2/chap23.html

2.算法導論10.2鏈表

關於鏈表中哨兵結點問題的深入剖析