首先我想寫這篇文章的原因是我用google搜索,但是沒有找到很多有關哨兵在鏈表中的使用,如果有的話,也只是輕描淡寫的寫了寫代碼,沒有明確說明使用哨兵的好處,這篇文章說明了用哨兵實現有序和無序鏈表的好處。
首先定義一個節點的結構.
struct Node { int value; Node* next; Node() : value(0), next(0) {} Node(int v, Node* n) : value(v), next(n) {} };
通常我們寫鏈表插入函數的時候,要分兩種情況:
(1)鏈表頭爲空
(2)鏈表頭不爲空
如果爲空的話,函數就需要改變Head指向的節點,因此,如果insert函數如果有head參數的數的話,那就得必須是head的引用(Node* &head)或指向head的指針(Node** head),這樣才能處理鏈表爲空的情況(當然,如果你把head指針設置爲全局的話,那就可以不必傳參了,一般也不會這樣做,否則要爲每個鏈表寫個insert函數),如果不用哨兵的話,通常的代碼是這樣的。
bool insert(Node** head, int value) { if (*head == NULL) { *head = new Node(value, 0); return true; } Node** current = head; while ((*current)->next != NULL) { current = &(*current)->next; } (*current)->next = new Node(value, 0); return true; }
開頭你就得判斷head是不是爲NULL,當要插入的數據很多的時候,這無疑是一筆很大的開銷。如果不爲空的話,傳入的head也就可以不必爲引用了,上面的例子是鏈表爲空和鏈表不爲空了都是用引用(這裏就不區別指針和引用了)處理的。看下面不用head引用來處理鏈表非空情況:
bool insert(Node** head, int value) { if (*head == NULL) { *head = new Node(value, 0); return true; } Node* current = *head; while (current->next != NULL) { current = current->next; } current->next = new Node(value, 0); return true; }
這裏之所以行得通,是因爲 current->next = new Node(value, 0) , 這種形式,如果while循環中的條件改爲 while (current != NULL) ,後面改爲 current = new Node(value, 0) 這樣就不行了(其實很多人還是會寫出這種代碼的,我也寫過,結果就是找不出問題在哪裏)。但是鏈表非空用head引用去這樣處理是沒問題的,也就是while條件爲 while(*current != NULL) ,後面 *current = new Node(value, 0) ,原因是因爲這裏是引用,可以改變next指針的值。對於指針可能這裏還是比較難理解,舉個例子吧:
int* a = new int(2); int* b = a; b = new int(3);
這個a地址存放的值是2,而不是3,沒有改變a地址存放的值
int* a = new int(2); int** b = &a; *b = new int(3);
這個a地址存放的值就是3,而不是原來的2了,懂了這個,也就可以明白上面那個是怎麼回事了。
扯了好久,都沒有扯上哨兵,這裏引入哨兵的目的就是要解決每次判斷head是否爲空的問題,這樣想吧,如果我讓一個空的鏈表含有一個元素(哨兵元素),這樣是不就不用判斷head爲空的情況了,那麼哨兵的初始值爲多少好呢?如果是無序的鏈表的話,當然就不用考慮哨兵初始值的問題,插入到隊尾就行了(如果無序的話,插入到開始不是更好嗎?我也不知道原因,這裏就用鏈表看作是隊列吧,隊列就是插入到尾部)。看代碼
bool insert(Node* head, int value) { while (head->next != NULL) { head = head->next; } head->next = new Node(value, 0); return true; }
這樣head也就不需要傳引用了,但是初始化head的時候必須讓他指向一個哨兵節點,如果爲head指向爲空insert會崩掉。
下面在說說有序鏈表的問題,有序鏈表的話,插入新節點的位置就不一定是在末尾了,可能在頭部,中間,尾部,似乎要分很多情況,我自己嘗試着不用哨兵節點,結果開頭的空指針判斷不可少,while循環還得加一個判斷條件類似 (current->next != NULL && current->value < value) 這樣子,這樣的while循環多了一個條件無疑增加了開銷,這時哨兵的可以派上用場了,一開始我就讓空的鏈表含有兩個元素分別是:Node* MAX = new Node(10000, 0), Node* MIN = new Node(-10000, MAX), (用10000表示數很大), head = MIN。insert函數爲:
bool insert(Node* head, int value) { Node* current = head; Node* pre = head; while (current->value < value) { pre = current; current = current->next; } pre->next = new Node(value, current); return true; }
這樣的代碼可謂是簡潔明瞭吧,這就是哨兵的好處。如果你要寫print函數的話,根據你用的哨兵的情況的來寫就好了。
其實這種尾部放哨兵的方法還可以用於數組,這裏也附帶說一下,如果要在含n個數的數組array中找個數value,一般是這樣寫:
for (int i = 0; i < n; ++i) { if (value == array[i]) return i; }
用了哨兵的方式爲:令 array[n] = value;然後代碼爲:
for (int i = 0;; i++) { if (value == array[i]) { if (i != n) return i; } }
好處在哪顯而易見。