鏈表
鏈表題一般常考
定義
單鏈表:一個節點 + 指向下一個節點的指針
頭指針:第一個節點,head
尾指針:最後一個節點,tail
雙向鏈表:單鏈表增加指向前繼結點的指針
特點
增加、刪除特別方便,複雜度:O(1)
查找、獲得第k個元素,複雜度: O(n)
實現
參考之前的文章: 用最容易的方式學會單鏈表(Python實現)
class ListNode:
"""鏈表結點定義
"""
def __init__(self, data=0, next_node=None):
self.data = data
self.next = next_node
操作
查找:
def search_list(L, key):
cur = L.head
while cur and cur.data != key:
cur = cur.next
return True # 做需要的操作
插入一個節點(後插法):
def insert_after(node, new_node):
new_node.next = node.next
node.next = new_node
# 做需要的操作
return ""
刪除節點:
def delete_after(node):
node.next = node.next.next
# 做需要的操作
return ""
鏈表問題常見套路:
通常來說,鏈表的問題從概念上講很簡單,更多時單純的考察編碼能力,而不是設計和解決算法。
套路一:設置虛擬頭節點(也稱哨兵節點)dummy head
。可以避免檢查空鏈表,極大簡化代碼,減少錯誤的發生。可參見下面的題目。
套路二:雙指針。單鏈表的快慢指針,要麼設置兩個指針指向不同的位置,要麼設置兩個指針走的步數不一樣。
鏈表常考題目
1. 合併兩個有序鏈表
* 例如:
* 輸入:1->2->4, 1->3->4->5
* 輸出:1->1->2->3->4->4->5
一個超級暴力解法的解法,把兩個鏈表append在一起,然後排序。但是此方法沒有利用到鏈表有序的特點。
更有效的方法是:遍歷兩個鏈表,總是選擇擁有最小元素的節點,並一直進行
問: 如果其中一個鏈表已經走完,另一個怎麼處理?
不能忘記把剩下的鏈表直接添加到末尾
class Solution:
def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
dummy_head = cur = ListNode() # 虛擬頭結點
while l1 and l2:
if l1.val < l2.val:
cur.next, l1 = l1, l1.next
else:
cur.next, l2 = l2, l2.next
cur = cur.next
# 添加剩下的鏈表,l1 或者 l2
cur.next = l1 or l2
return dummy_head.next
時間複雜度: O((n + m), n和m分別爲兩個鏈表的長度
空間複雜度: O(1) , 無額外空間
更多解法參考:合併兩個有序鏈表
2. 反轉鏈表
"""
# 反轉一個單鏈表。
#
# 示例:
#
# 輸入: 1->2->3->4->5->NULL
# 輸出: 5->4->3->2->1->NULL
#
# 進階:
# 你可以迭代或遞歸地反轉鏈表。你能否用兩種方法解決這道題?
"""
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
cur = head
reverseHead = None
while cur:
temp = cur.next
cur.next = reverseHead
reverseHead = cur
cur = temp
return reverseHead
變體1:反轉鏈表2
反轉從位置 m 到 n 的鏈表。請使用一趟掃描完成反轉。
說明:1 ≤ m ≤ n ≤ 鏈表長度。
示例:
輸入: 1->2->3->4->5->NULL, m = 2, n = 4
輸出: 1->4->3->2->5->NULL
解題思路:分離[m,n]的子鏈表,對子鏈表反轉,然後分割放回去。缺點是需要兩次處理子鏈表。
找到子鏈表[m, n],反轉它們,然後連接m和n+1, 連接n和m-1。
class Solution:
def reverseBetween(self, head: ListNode, m: int, n: int) -> ListNode:
if m == n:
return head
dummy_head = pre = ListNode(0)
dummy_head.next = head
for _ in range(m - 1):
pre = pre.next
# Reverse sublist[m,n]
cur = pre.next
reverse = None
for _ in range(n - m):
temp = cur.next
cur.next, reverse, cur = (reverse, cur, temp)
pre.next.next = cur
pre.next = reverse
return dummy_head.next
變體2: k個一組反轉鏈表
簡單版:兩兩反轉鏈表
3. 環形鏈表
- 空間換時間:哈希表法
這個問題有幾種解決方案。 如果空間不是問題,最簡單的方法是從頭開始通過下一個字段探索節點,並將訪問的節點存儲在哈希表中-僅當我們訪問哈希表中已經存在的節點時,存在一個循環。 如果不存在循環,則搜索在結尾處結束(通常通過將下一個字段設置爲null來表示)。 此解決方案需要O(n)空間,其中n是列表中的節點數。
- 暴力解法
不使用額外存儲空間且不修改列表的暴力方法是在兩個循環中遍歷該列表-外循環一遍遍遍歷節點,而內循環從頭開始並遍歷爲 到目前爲止,由於外循環已經經歷了許多節點。 如果外部循環訪問的節點被訪問兩次,則檢測到循環。 (如果外部循環遇到列表的末尾,則不存在循環。)此方法的複雜度爲。
- 快慢指針
可以使這種想法在線性時間內工作-使用慢指針和快指針遍歷列表。 在每次迭代中,將慢指針加1,將快指針加2。 當且僅當兩個指針相遇時,列表才具有循環。
原因如下:如果快指針跳過了慢指針,則在下一步中,慢指針將等於快指針。
class Solution:
def hasCycle(self, head: ListNode) -> bool:
# hash_set = set()
# while head:
# if head in hash_set:
# return True
# hash_set.add(head)
# head = head.next
# return False
fast = slow = head
while slow and fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow is fast:
return True
return False
擴展:找到環的入口節點