本文首發於我的Github博客
本文是數據結構與算法複習的第二篇博文,複習
- 鏈表的概念
- 常見的鏈表類型和設計取捨
- 鏈表的反轉操作
鏈表的概念
鏈表可以定義爲:
- 空
- 擁有一個節點,該節點有兩個屬性
- val,本節點的值
- next,另一個鏈表
首先,這個定義是單鏈表的定義,但是雙向鏈表也是類似的
其次,從這個定義可以看到,鏈表是可以遞歸定義的
常見的鏈表類型和設計取捨
比較常見的鏈表類型有
- 單鏈表
- 雙向鏈表
所謂的設計取捨主要是考慮:
- 選擇單鏈表還是雙向鏈表?
- 是否需要衛士節點(sentinel node)?
單鏈表與雙向鏈表
雙向鏈表與單鏈表的區別就在於
- 單鏈表每個節點只有一個next指向額外的鏈表(也就是後繼元素)
- 雙鏈表每個節點有兩個屬性指向額外的鏈表(也就是前驅和後繼)
所以雙向鏈表的使用更加靈活
如何選擇
對於單鏈表和雙鏈表的選擇,主要是考慮兩點:
- 空間,雙鏈表由於多了一個屬性,消耗空間更多
- 時間,這裏是指
- 如果有訪問前驅節點的需求,可以考慮
- 雙鏈表
- 如果需要訪問的前驅結點是固定的,比如固定是前一個節點或者前兩個節點
- 就可以直接使用額外的一個節點指針進行存儲
- 如果有訪問前驅節點的需求,可以考慮
是否需要衛士節點
衛士節點就是指
對於任意的鏈表,都向其中添加一個無用的節點,使得鏈表不會是0節點的鏈表
是否需要衛士節點主要是看個人的使用習慣
- 使用衛士節點可以在一定程度上規避空指針的問題
- 畢竟鏈表中一定會有節點
- 但是會多出一個節點的空間消耗,且編程邏輯和無衛士節點的編程邏輯不同(
廢話,當然不同)
鏈表的翻轉操作
鏈表考的最多的就是翻轉(淡然還有單鏈表的快排,不過那個可以留到排序的時候再說)
鏈表的翻轉有兩種實現:
- 循環實現
- 遞歸實現
其實就是邏輯煩了點,實在不行代碼背下來
循環實現鏈表翻轉
如下代碼的關鍵在於循環的不變式
每次循環開始前,
head
指向的是還未反轉的鏈表頭,prev
指向的是已經反轉的鏈表頭
class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
class Solution {
public ListNode reverseList(ListNode head) {
ListNode prev = null, next = null;
while (head != null) {
next = head.next;
head.next = prev;
prev = head;
head = next;
}
return prev;
}
}
遞歸實現鏈表翻轉
以下代碼的關鍵在於函數聲明
reverseList(headA, headB)
會將headA
的鏈表反轉,然後將headB
的鏈表拼接到反轉的headA
鏈表後
class Solution {
public ListNode reverseList(ListNode head) {
return reverseList(head, null);
}
private ListNode reverseList(ListNode headA, ListNode headB) {
if (headA == null) {
return headB;
}
ListNode next = headA.next;
headA.next = headB;
return reverseList(next, headA);
}
}