跳至主要内容

Linked List

Linked List 是一種線性資料結構,與 Array 不同,Linked List 在電腦記憶體裡不需要連續的空間。它是把資料分散在記憶體各處,並透過每個 Node 內附帶的「pointer 」指向下一個 Node 的位址。

白話理解

概念就像是一列「火車」,一節車廂連著一節車廂串接起來,車廂彼此不需要緊貼在一起,只要知道「下一節車廂在哪裡」就能一路連下去。要在車廂中間加掛一節新車廂,也只需要重新勾住前後兩節,不用把整列火車搬家。

核心組成: Node(Node)​

Linked List 由數個 Node 組成,每個 Node 通常包含兩個部分:

  • 資料(Data/Value):實際要儲存的內容。
  • 指標(Next):記憶體位址,指向下一個 Node。最後一個 Node 的 next 則指向 null。
class Node:
def __init__(self, val):
self.val = val
self.next = None

常見種類​

Singly Linked List​

每個 Node 只知道「下一個」是誰,只能從頭往後逛,不能回頭。

Singly Linked List

class SinglyLinkedList:
def __init__(self):
self.head = None # 指向「開頭」的 Node
self.tail = None # 指向「結尾」的 Node
self.length = 0 # 記錄目前 Node 總數

# 1. 尾端新增 Node:O(1)
def push(self, val):
new_node = Node(val)

if not self.head:
# 如果原先 Linked List 是空的,新增 Node 同時是頭也是尾
self.head = new_node
self.tail = self.head
else:
# Linked List 已有資料,新增 Node 接到目前尾巴的後面,並更新尾巴指標
self.tail.next = new_node
self.tail = new_node
self.length += 1
return self

# 2. 尾端刪除 Node:O(n) - 因為 Linked List 必須從頭走到倒數第二個 Node 來找新尾巴
def pop(self):
if self.length == 0:
return None

current_node = self.head # 用來跑迴圈走到最後
new_tail = current_node # 用來追蹤倒數第二個 Node(即將成為新尾巴)

# 當 current_node 後面還有 Node 時,繼續往後移
while current_node.next:
new_tail = current_node
current_node = current_node.next

self.tail = new_tail # 更新 tail 為倒數第二個 Node 上
self.tail.next = None # 斷開連結
self.length -= 1

# 如果刪除後 Linked List 變空了,要把頭尾指針都清空
if self.length == 0:
self.head = None
self.tail = None
return current_node # 回傳被刪除的 Node

# 3. 開頭刪除 Node:O(1)
def shift(self):
if self.length == 0:
return None

shifted_node = self.head # 暫存原本的頭
self.head = shifted_node.next
self.length -= 1

# 如果刪到變空 Linked List,尾巴指標也要清空
if self.length == 0:
self.tail = None
return shifted_node

# 4. 開頭新增 Node:O(1)
def unshift(self, val):
new_node = Node(val)

if not self.head:
self.head = new_node
self.tail = self.head
else:
new_node.next = self.head
self.head = new_node
self.length += 1
return self

# 5. 獲取指定位置的 Node:O(n)
def get(self, index):
# 檢查邊界
if index < 0 or index >= self.length:
return None

count = 0
current_node = self.head

while count < index:
count += 1
current_node = current_node.next
return current_node

# 6. 修改指定位置的 Node value:O(n) - 效能取決於 get()
def set(self, index, val):
set_node = self.get(index)
if not set_node:
return False

set_node.val = val
return True

# 7. 在指定位置插入新 Node:O(n) - 尋找前一個 Node 需要 O(n),但插入動作本身是 O(1)
def insert(self, index, val):
if index < 0 or index > self.length:
return False

# 如果要在最後面插入,直接用 push
if index == self.length:
return bool(self.push(val))

# 如果要在最前面插入,直接用 unshift
if index == 0:
return bool(self.unshift(val))

new_node = Node(val)
prev_node = self.get(index - 1)

new_node.next = prev_node.next
prev_node.next = new_node
self.length += 1
return True

# 8. 刪除指定位置的 Node:O(n)
def remove(self, index):
if index < 0 or index >= self.length:
return None
if index == self.length - 1:
return self.pop()
if index == 0:
return self.shift()

prev_node = self.get(index - 1) # 找到要刪除位置的「前一個 Node」
removed_node = prev_node.next # 暫存即將被刪除的 Node

prev_node.next = removed_node.next # 讓前一個 Node 跳過被刪除者,直接指向下下一個
self.length -= 1
return removed_node

Doubly Linked List​

每個 Node 同時記錄「上一個」與「下一個」,可以雙向穿梭,但比較佔記憶體。

Doubly Linked List

實作內容和 Singly Linked List 大同小異,只是雙向的 Node 多了一個指標。

class Node:
def __init__(self, val):
self.val = val
self.next = None
self.prev = None

class DoublyLinkedList:
def __init__(self):
self.head = None
self.tail = None
self.length = 0

# 1. 尾端新增 Node:O(1)
def push(self, val):
new_node = Node(val)
if not self.head:
self.head = new_node
self.tail = self.head
else:
self.tail.next = new_node
new_node.prev = self.tail
self.tail = new_node
self.length += 1
return self

# 2. 尾端刪除 Node:O(1) - 因為 Node 有前後指針,所以不需要寫迴圈來找到要刪除的 Node
def pop(self):
if not self.head:
return None
popped_node = self.tail
if self.length == 1:
self.head = None
self.tail = None
else:
self.tail = popped_node.prev
self.tail.next = None
popped_node.prev = None
self.length -= 1
return popped_node

# 3. 開頭刪除 Node:O(1)
def shift(self):
if not self.head:
return None
shifted_node = self.head
if self.length == 1:
self.head = None
self.tail = None
else:
self.head = shifted_node.next
self.head.prev = None
shifted_node.next = None
self.length -= 1
return shifted_node

# 4. 開頭新增 Node:O(1)
def unshift(self, val):
new_node = Node(val)
if not self.head:
self.head = new_node
self.tail = self.head
else:
self.head.prev = new_node
new_node.next = self.head
self.head = new_node
self.length += 1
return self

# 5. 獲取指定位置的 Node:O(n)
def get(self, index):
if index < 0 or index >= self.length:
return None

# 如果要找的位置靠近前半段,從「頭 (head)」出發往後找
if index <= self.length / 2:
count = 0
current_node = self.head
while count != index:
current_node = current_node.next
count += 1
# 如果要找的位置靠近後半段,從「尾 (tail)」出發往前找
else:
count = self.length - 1
current_node = self.tail
while count != index:
current_node = current_node.prev
count -= 1
return current_node

# 6. 修改指定位置的 Node 資料:O(n)
def set(self, index, val):
set_node = self.get(index)
if not set_node:
return False
set_node.val = val
return True

# 7. 在指定位置插入新 Node:O(n) - 插入本身的指針重連動作是 O(1)
def insert(self, index, val):
if index < 0 or index > self.length:
return False
if index == 0:
return bool(self.unshift(val))
if index == self.length:
return bool(self.push(val))
new_node = Node(val)
before_node = self.get(index - 1)
after_node = before_node.next

before_node.next = new_node
new_node.prev = before_node

new_node.next = after_node
after_node.prev = new_node

self.length += 1
return True

# 8. 刪除指定位置的 Node:O(n)
def remove(self, index):
if index < 0 or index >= self.length:
return None
if index == 0:
return self.shift()
if index == self.length - 1:
return self.pop()
removed_node = self.get(index)

removed_node.prev.next = removed_node.next
removed_node.next.prev = removed_node.prev
removed_node.prev = None
removed_node.next = None

self.length -= 1
return removed_node

複雜度​

操作種類時間複雜度原因與說明
開頭新增 / 刪除O(1)完美效能。只需更改 Head 的 pointer 指向,完全不影響其他 Node。
尾端新增 / 刪除O(1) 或 O(n)如果有記錄 Tail 就是 O(1);若沒有,必須從 Head 數到尾,則是 O(n)。
中間特定位置新增/刪除O(1)只要已知該 Node,把 next 指標斷開並重新連上即可,不需挪移資料。
讀取資料 (Access)O(n)不支援隨機存取。想看第 50 個 Node,必須從第一個一路連過去。
搜尋資料 (Search)O(n)必須從 Head 開始,一個一個比對資料值直到找到為止。

適用情況​

主要在以下 4 種情況下使用 Linked List:

1. 頻繁在一連串資料裡進行新增、刪除​

  • 情境:如果用 Array,在開頭插入一筆資料,則後面 100 萬筆資料都必須在記憶體中全部往後移一格(O(n)),極度消耗 CPU。
  • 應用:用 Linked List 只需要改動新增 Node 與前後 Node 的指標(O(1)),後面 100 萬筆資料完全不用動。

2. 資料量完全無法預測、變動極大時​

  • 情境:Array 在底層需要連續空間。當 Array 滿了,電腦必須在記憶體找一塊更大的「連續空地」,把舊資料全部複製搬家過去(動態擴容)。
  • 應用:Linked List 的記憶體是散落各處的。多一個資料就多申請一個小格子,不需要預先知道大小,也不需要搬動全部資料。

3. 作為其他進階資料結構的底層​

許多經典資料結構,為了追求新增與刪除的極致效能,底層都會採用 Linked List:

  • Queue 與 Stack:用 Linked List 實作可以確保新增和移除都是最佳效率的 O(1)。
  • 處理 Hash Collision:發生碰撞時,最常用的「Separate Chaining」,即在格子後面掛一個 Linked List,串起同個 hash value 的 key-value pairs。
  • Graph / Tree:Binary Tree 的左右子 Node、Graph 的 Adjacency List,本質上都是 Linked List 的變形。

4. 實際生活中的軟體功能​

  • 音樂播放器的「下一首 / 上一首」:通常會使用 Doubly Linked List,每首歌是一個 Node,記錄著前一首與下一首的歌是誰,還可以把最後一首連回第一首變成「循環播放」。
  • 圖片檢視器、投影片切換:按下左右鍵切換上一張、下一張圖,邏輯與音樂播放器完全相同。
  • 區塊鏈(Blockchain):每一個 Block 都包含資料,並透過 hash 指向前一個區塊,這在結構上就是一個只能往前追溯的單向 Linked List。

5. 搭配 Two Pointers 解題​

Linked List 相關的題目時常會搭配 Two Pointers 的「Fast & Slow Pointers」手法,用來判斷是否有環、找出中點,或是找出倒數第 k 個節點,這是刷題時非常常見的組合。

判斷是否有環、找出環的起點,可以參考 Floyd's Cycle Detection 的詳細說明。

常見誤區​

  • 斷開指標的順序寫錯,導致「斷了線」:例如在 insert 時,如果先把 prevNode.next 指向新節點,卻忘記事先把新節點的 next 接到原本 prevNode.next 的對象,後面的節點就會直接遺失,永遠找不回來。動手前建議先畫圖,確認「新指標接好之後,才能覆蓋舊指標」。
  • 忘記同步更新 head、tail 或 length:尤其是刪到只剩最後一個元素、或刪光整個 Linked List 時,很容易漏掉把 head/tail 設回 null,導致之後的操作出現意外行為。
  • 誤以為可以像 Array 一樣直接用 index 存取:Linked List 沒有隨機存取的能力,get(index) 必須從頭(或利用 Doubly Linked List 從尾)一步步走過去,是 O(n) 而不是 O(1),這也是它和 Array 最核心的取捨差異。