關於連結串列中哨兵結點問題的深入剖析
最近正在學習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)學會分析一個東西的來龍去脈,為什麼會有這部分,以及怎麼改進的。
參考:
2.演算法導論10.2連結串列