上一篇介紹了一維數據結構的關系,算是開篇了數據結構這個系列。這篇詳細說一下鏈表的實現。
鏈表實現也是我面試時喜歡出的一道題,因為有層次,能提現區分度。看似簡單的三個操作,需要處理的異常和邊界情況并不少,能一次完美寫對并不容易。
而且這里還有一個哨兵
的概念,合理運用能減少邊界處理,是一個遞進的層次,可為候選人增加區分度。
鏈表的操作:
(1)SEARCH(x):鏈表的搜索
(2)INSERT(i,x):鏈表的插入,在第i個位置插入x
(3)DELETE(x):鏈表的刪除
哨兵(sentinel):
為了減少邊界條件的判斷(是否為空鏈表等等),引入哨兵,使得鏈表永遠不為“空”。
下面就細講一下鏈表實現
插入
上面說了為了減少邊界,我們使用了一個哨兵
的概念,那么都有哪些邊界呢?
1.不用哨兵
插入的核心操作如下:
// pre: 前置節點; p: 當前第i個節點
// item: 待插入節點
pre.next -> item;
item.next -> p;
進行插入,就需要知道兩個節點指針,這時就出現了兩個邊界情況,是不存在前置節點
的:
- 鏈表為空
- 插入位置是0
需要對兩種情況做特殊處理,直接看代碼
void insert(i,item) {
// 空鏈表的處理
if (head == NULL) {
head == item;
}
// 插入到第一個位置的情況
if (i == 0) {
// 插入
head.next -> item;
item.next -> head;
head = item;
} else {
// 第一個節點
Node *pre = head;
Node *p = head -> next;
int idx = 0;
// 添加 p 非空條件,處理 i > length 情況
while (idx < i && p != NULL) {
pre = pre -> next;
p = p -> next;
}
// 插入
pre.next -> item;
item.next -> p;
}
}
處理下來27行,進行了兩次if-else
判斷
2.使用哨兵
但如果引入哨兵
概念,會怎么樣呢?
void insert(i,item) {
// 獲取哨兵和實際上第一個節點
Node *pre = sentinel;
Node *p = sentinel -> next;
int idx = 0;
// 添加 p 非空條件,處理 i > length 情況
while (idx < i && p != NULL) {
pre = pre -> next;
p = p -> next;
}
// 插入
pre.next -> item;
item.next -> p;
}
可以看到,整個操作簡單歸一,14行就搞定了,沒有額外邊界處理邏輯處理,也不需要再進行頭結點變更,哨兵的內存開銷也不大。是一個優雅的解決方式。
再看看剩下兩個方法的實現細節
搜索
比較簡單,沒有對前置節點
的依賴,所以無需做特殊處理。
Node* search(x) {
Node *p = head;
// Node *p = sentinel -> next;
while (p != NULL) {
if (*p.data == x) {
break;
}
p = p -> next;
}
return p;
}
刪除
先簡單介紹下兩種鏈表刪除操作
1.依賴前置節點pre
1>將 pre->next 指向 found->next
2>去除 found->next指向
3>刪除found節點
但是對于單鏈表操作,拿到前置節點非常困難,為了避免使用前置節點,我們可以使用一次拷貝,將刪除節點從found變成found->next,比如:
2.不依賴前置節點 pre
1>將next節點內容copy到found
2>found->next指向next->next
3>刪除next
順利避免了對前置節點依賴,還可以復用上面的 search 函數。但是這種設計,存在一個問題:就是當刪除的是最后一個節點時,依然有對前置節點的依賴。這時引入一個末尾哨兵,保證我們的 next 節點永遠不為 NULL,讓我們避免這種情況。
需要對插入、查找做修改
3.引入末尾哨兵
init() {
head_sentinel = new Node();
tail_sentinel = new Node();
head_sentinel->next = tail_sentinel;
tail_sentinel->next=NULL:
}
void insert(i,item) {
// 獲取頭部哨兵和實際上第一個節點
Node *pre = head_sentinel;
Node *p = head_sentinel -> next;
int idx = 0;
// 添加 p ≠ tail_sentinel,處理 i > length 情況
while (idx < i && p != tail_sentinel) {
pre = pre -> next;
p = p -> next;
}
// 插入
pre.next -> item;
item.next -> p;
}
Node* search(x) {
Node *p = head_sentinel -> next;
// 結束條件,變為不等于末尾哨兵
while (p != tail_sentinel) {
if (*p.data == x) {
break;
}
p = p -> next;
}
return p;
}
void delete(x) {
Node *found = search(x);
if (found == NULL) return;
Node *next = found->next;
*found.data = copy(*next.data);
// 因為有末尾哨兵的存在,next永遠不為NULL
found->next = next->next;
next->next = NULL;
delete(next);
}
小結
綜上,哨兵
的作用就是保持鏈表非空,將所有操作去差異化,減少邊界條件的處理。從廣義上來說,鏈表的操作過程中產生了對“鄰居節點”的依賴,當這種依賴是不穩定的時候,我們就可以使用哨兵
,將這種依賴補齊,或者是轉化為對“哨兵”的穩定依賴。
此外,在刪除過程中,因為單鏈表獲取前置節點
困難,所以針對該點進行優化,這也是很多數據結構和算法調優的思路。熟悉后可以舉一反三。
參考
http://www.lxweimin.com/p/afbfc784238a
https://www.zhihu.com/question/27155932