快慢指標是一個經典的解題技巧,它的概念是用一個一次走兩步的快指標,和一個一次走一步的慢指標,兩者同時在資料結構上移動,當快指標發生了先撞牆,或者領先慢指標一整圈,就代表你偵測出了特定的位置或性質。本篇將介紹如何透過讓快慢指標在 linked list 上移動,來偵測出中間節點,或者 linked list 是否有連成環狀。

當然,前述的兩個問題也有其他解法。以偵測出中間節點來說,你可以用一個指標先掃一遍,算出節點個數;然後再掃第二遍,移動到一半的距離,就等於找到中間節點。但若資料是用串流的方式產生,你就無法掃第二次;或者若 linked list 上面其實有環,那這個方法就會停不下來。

以下先示範如何用快慢指標,在沒有環的 linked list 上面找中間節點(若你想處理有環的狀況,可以自己試著跟再下面一個範例結合):

class ListNode(object):
	def __init__(self, val, next_node=None):
		self.val = val
		self.next = next_node


# Construct
head = ListNode(10)
ptr = head
for v in (20, 30, 40, 50):
	ptr.next = ListNode(v)
	ptr = ptr.next

fast = head
slow = head
while fast and fast.next:
	fast = fast.next.next
	slow = slow.next
print(slow.val)

在上述範例中,若令節點個數為 n,而 head 指向的節點為 index 0 時,我們要尋找的目標是 index 為 n // 2 的節點。找尋的概念也相當的簡單,因為快指標的速度是慢指標的兩倍,所以快指標走了 n 個節點以後,慢指標就會在 n / 2 的位置。

偵測 linked list 是否有環其實也有其他解法,但也各自有其缺點。例如你可以維護一個集合,把走過的節點位置記錄下來,如果有重複的話就是有環,但這個方法需要多用掉不少空間。你也可以把走過的節點上的 val 改成一個不可能出現的值,但若節點上的 val 沒有任何已知限制,或者沒有權限修改資料時,這個方法就會直接失效。

而要用快慢指標的方法,來判斷 linked list 中是否有環,則只需要看看快指標會撞牆還是會領先慢指標一整圈即可,如下:

class ListNode(object):
	def __init__(self, val, next_node=None):
		self.val = val
		self.next = next_node


def has_cycle(head):
	fast = head
	slow = head
	while fast and fast.next:
		for _ in range(2):
			fast = fast.next
			if fast == slow:
				return True
		slow = slow.next
	return False


# Construct
head = ListNode(10)
ptr = head
for v in (20, 30, 40, 50, 60):
	ptr.next = ListNode(v)
	ptr = ptr.next

print('Has cycle:', has_cycle(head))

# Add cycle
ptr_20 = head.next
ptr_50 = ptr_20.next.next.next
ptr_50.next = ptr_20

print('Has cycle:', has_cycle(head))

對於上述方法的時間複雜度分析,由於指標移動一次是 O(1),而最多只需要移動 O(n) 次即可完成演算法,因此整體的時間複雜度是 O(n)。