การตรวจจับวัฏจักรด้วยอัลกอริทึมของ Floyd
ตรวจจับวัฏจักรด้วยแนวทางตัวชี้ช้า–เร็ว ค้นหาจุดเริ่มต้นของวัฏจักร และพิสูจน์ความถูกต้องของอัลกอริทึมทางคณิตศาสตร์
การตรวจจับวัฏจักรด้วยอัลกอริทึมของ Floyd เป็นบทเรียน DSA Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน DSA Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
วงจรในรายการเชื่อมโยงคืออะไร
วงจรในรายการเชื่อมโยงเกิดขึ้นเมื่อโหนดหนึ่งมีตัวชี้ next ชี้ย้อนกลับไปยังโหนดที่เคยเยี่ยมชมแล้ว ทำให้เกิดลูปไม่รู้จบ การไล่ผ่านรายการลักษณะนี้ด้วยลูป while head จะทำงานไปตลอด การตรวจจับวงจรเป็นโจทย์คลาสสิกในการสัมภาษณ์งาน และเป็นพื้นฐานของอัลกอริทึมตัวชี้ขั้นสูง
วิธีตรงไปตรงมาคือเก็บโหนดที่เคยเยี่ยมชมทุกโหนดไว้ในเซต แล้วตรวจสอบการเป็นสมาชิก โดยใช้เวลา O(n) และพื้นที่ O(n) อัลกอริทึมของฟลอยด์แก้ปัญหาเดียวกันได้ในเวลา O(n) และพื้นที่ O(1) ซึ่งเป็นสิ่งที่ผู้สัมภาษณ์คาดหวัง
อัลกอริทึมตัวชี้ช้า-เร็วของฟลอยด์
การตรวจจับวงจรของฟลอยด์ (หรือที่เรียกว่า 'เต่ากับกระต่าย') ใช้ตัวชี้สองตัว: slow เคลื่อนที่ครั้งละหนึ่งขั้น ส่วน fast เคลื่อนที่ครั้งละสองขั้น หากไม่มีวงจร fast จะไปถึงค่าว่างก่อน หากมีวงจร ตัวชี้เร็วจะวนแซงตัวชี้ช้าภายในวงจรในที่สุด และทั้งสองจะพบกันที่โหนดเดียวกัน การพบกันนี้เป็นหลักฐานว่ามีวงจรอยู่
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def hasCycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow is fast:
return True
return False
# Build: 3 -> 2 -> 0 -> -4 -> (back to 2)
nodes = [ListNode(v) for v in [3, 2, 0, -4]]
for i in range(3):
nodes[i].next = nodes[i+1]
nodes[3].next = nodes[1] # cycle: -4 -> 2
print(hasCycle(nodes[0])) # Trueเหตุใดตัวชี้ช้าและเร็วจึงพบกันเสมอ
อธิบายอย่างไม่เป็นทางการได้ว่า เมื่อ ตัวชี้ทั้งสองเข้าสู่วงจรแล้ว ระยะห่างระหว่างตัวชี้จะเปลี่ยนไป 1 หน่วยต่อขั้น (ตัวชี้เร็วได้ระยะ 2 ส่วนตัวชี้ช้าได้ระยะ 1 ดังนั้นช่องว่างจึงลดลง 1 ในแต่ละรอบ) ในที่สุดช่องว่างจะเป็น 0 ซึ่งหมายความว่าทั้งสองอยู่ที่โหนดเดียวกัน อย่างเป็นทางการมากขึ้น หากวงจรมีความยาว C ช่องว่างสูงสุดภายในวงจรคือ C-1 และช่องว่างลดลง 1 ในแต่ละขั้น ดังนั้นทั้งสองจะพบกันภายใน C ขั้นหลังจากเข้าสู่วงจรครบทั้งคู่
จำนวนขั้นทั้งหมดก่อนพบกัน: มากที่สุด O(n + C) = O(n) เนื่องจาก C <= n
# Visualise convergence: simulate gap in cycle
cycle_length = 5
for start_gap in range(1, cycle_length + 1):
gap = start_gap
steps = 0
while gap != 0:
gap = (gap - 1) % cycle_length
steps += 1
print(f'Start gap {start_gap}: meet after {steps} step(s)')การหาจุดเริ่มต้นของวงจร
หลังจากตรวจพบวงจรแล้ว อัลกอริทึมของฟลอยด์ยังสามารถหาโหนดเริ่มต้นได้ด้วย ซึ่งเป็นตำแหน่งที่วงจรเริ่มต้น หลังจากตัวชี้ช้าและเร็วพบกันภายในวงจร ให้ย้ายตัวชี้ตัวหนึ่งกลับไปที่หัวรายการ และคงอีกตัวไว้ที่จุดที่พบกัน จากนั้นเลื่อนตัวชี้ทั้งสองครั้งละหนึ่งขั้น ทั้งสองจะพบกันพอดีที่โหนดเริ่มต้นของวงจร วิธีนี้ใช้ได้เพราะระยะจากหัวรายการถึงจุดเริ่มต้นเท่ากับระยะจากจุดที่พบกันถึงจุดเริ่มต้นเมื่อคิดแบบโมดูลาร์ด้วยความยาววงจร
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def detectCycle(head):
slow = fast = head
# Phase 1: detect meeting point
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow is fast:
break
else:
return None # no cycle
# Phase 2: find entry
pointer = head
while pointer is not slow:
pointer = pointer.next
slow = slow.next
return pointer # cycle entry node
nodes = [ListNode(v) for v in [3, 2, 0, -4]]
for i in range(3):
nodes[i].next = nodes[i+1]
nodes[3].next = nodes[1] # entry is nodes[1] (val=2)
entry = detectCycle(nodes[0])
print(entry.val) # 2การพิสูจน์ทางคณิตศาสตร์ของโหนดเริ่มต้น
ให้ F = ระยะจากหัวรายการถึงจุดเริ่มต้นของวงจร, C = ความยาววงจร และ a = ระยะจากจุดเริ่มต้นถึงจุดที่พบกันภายในวงจร เมื่อพบกัน ตัวชี้ช้าเดินทางเป็นระยะ F + a ขั้น ส่วนตัวชี้เร็วเดินทางเป็นระยะ F + a + n*C ขั้น (ล่วงหน้าอยู่ n รอบเต็ม) เนื่องจากระยะทางของตัวชี้เร็วเป็น 2 เท่าของตัวชี้ช้า: 2(F+a) = F+a+nC → F = nC - a นั่นหมายความว่าระยะจากหัวรายการถึงจุดเริ่มต้นเท่ากับระยะจากจุดที่พบกันถึงจุดเริ่มต้นเมื่อคิดแบบโมดูลาร์ด้วย C การย้ายตัวชี้ตัวหนึ่งไปที่หัวรายการแล้วเลื่อนทั้งสองครั้งละ 1 ขั้นจะทำให้ทั้งสองมาบรรจบกันที่โหนดเริ่มต้น
# Verify with our example: F=1 (head to node 2), C=3 (cycle: 2->0->-4->2), a=?
# Meeting inside cycle after F+a slow steps
# Let us measure a by counting from entry to meeting point
# In practice the code handles this automatically
F = 1 # head(3) to entry(2)
C = 3 # cycle length 2->0->-4
# n=1: F = 1*C - a => a = C - F = 3 - 1 = 2
a = C - F
print(f'F={F}, C={C}, a={a}')
print(f'After meeting, {F} more steps reach entry: {F == C - a or F % C == (C - a) % C}')การวัดความยาววงจร
เมื่อมีจุดที่พบกันภายในวงจรแล้ว (ระยะที่ 1 ของอัลกอริทึมฟลอยด์) คุณสามารถวัดความยาววงจรได้ โดยตรึงตัวชี้ตัวหนึ่งไว้กับที่ แล้วเลื่อนอีกตัวหนึ่งไปเรื่อย ๆ จนกลับมาพบกันอีกครั้ง จำนวนขั้นที่ใช้เท่ากับความยาววงจร วิธีนี้มีประโยชน์สำหรับโจทย์ที่ถามหาความยาววงจรโดยตรง
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def cycle_length(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow is fast: # found meeting point
length = 1
fast = fast.next
while fast is not slow:
fast = fast.next
length += 1
return length
return 0 # no cycle
nodes = [ListNode(v) for v in [1, 2, 3, 4, 5]]
for i in range(4):
nodes[i].next = nodes[i+1]
nodes[4].next = nodes[2] # cycle: 3->4->5->3, length=3
print(cycle_length(nodes[0])) # 3จำนวนแฮปปี (ตรวจจับวงจรโดยไม่ใช้รายการ)
อัลกอริทึมของฟลอยด์ไม่ได้จำกัดการใช้งานอยู่แค่กับรายการเชื่อมโยง LeetCode 202 เรื่อง 'จำนวนแฮปปี' ถามว่าการแทนค่า n ซ้ำ ๆ ด้วยผลรวมกำลังสองของเลขโดดจะไปถึง 1 ในที่สุดหรือไม่ หากเข้าสู่วงจรที่ไม่มี 1 อยู่ด้วย กระบวนการจะวนซ้ำตลอดไป คุณสามารถจำลองสิ่งนี้เป็นการไล่ผ่านรายการเชื่อมโยงเสมือน โดยให้ 'ค่าถัดไป' ของแต่ละโหนดเป็นค่าที่คำนวณได้ถัดไป แล้วใช้อัลกอริทึมของฟลอยด์เพื่อตรวจจับวงจร
def isHappy(n):
def next_val(x):
total = 0
while x:
x, d = divmod(x, 10)
total += d * d
return total
slow, fast = n, next_val(n)
while fast != 1 and slow != fast:
slow = next_val(slow)
fast = next_val(next_val(fast))
return fast == 1
print(isHappy(19)) # True (1->81+1=82->68->100->1)
print(isHappy(2)) # False (enters cycle)การตรวจจับแบบใช้เซตอย่างง่ายเทียบกับวิธีของฟลอยด์
วิธีใช้เซตจะเก็บโหนดแต่ละโหนดที่เยี่ยมชมไว้ในเซต และตรวจสอบการเป็นสมาชิกก่อนเยี่ยมชม ใช้เวลา O(n) และพื้นที่ O(n) ส่วนอัลกอริทึมของฟลอยด์ก็ใช้เวลา O(n) เช่นกัน แต่ใช้พื้นที่เพียง O(1) โดยไม่ต้องมีโครงสร้างข้อมูลเพิ่มเติม ในสภาพแวดล้อมที่มีหน่วยความจำจำกัด (เช่น ระบบฝังตัวและเคอร์เนลระบบปฏิบัติการ) การรับประกันการใช้พื้นที่ O(1) มีความสำคัญ ผู้สัมภาษณ์บางครั้งจะถามต่ออย่างชัดเจนให้ใช้พื้นที่ O(1) หลังจากที่คุณเสนอวิธีใช้เซต
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# Naive O(n) space approach
def hasCycle_set(head):
seen = set()
while head:
if id(head) in seen:
return True
seen.add(id(head))
head = head.next
return False
# Floyd's O(1) space approach
def hasCycle_floyd(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow is fast:
return True
return False
print('Both implementations give the same result')กรณีขอบสำหรับการตรวจจับวงจร
มีกรณีขอบสามกรณีที่ต้องจัดการ ประการแรก รายการว่าง: head is None — เงื่อนไขลูปของฟลอยด์ fast and fast.next จะออกจากลูปทันทีและคืนค่าเท็จ ประการที่สอง โหนดเดียวที่ไม่มีวงจร: fast.next เป็น None ลูปจึงจบลงและคืนค่าเท็จ ประการที่สาม โหนดเดียวที่มีวงจร: ตัวชี้ของโหนดชี้กลับมายังตัวมันเอง ตัวชี้ช้าและเร็วเริ่มต้นที่หัวรายการเดียวกัน หลังจากเลื่อนหนึ่งขั้น ตัวชี้เร็วจะกลับมายังหัวรายการอีกครั้ง ขณะที่ตัวชี้ช้าก็อยู่ที่หัวรายการเช่นกัน ดังนั้นตัวชี้ทั้งสองจึงตรงกันตั้งแต่การวนรอบครั้งแรก
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def hasCycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow is fast:
return True
return False
# Edge cases
print(hasCycle(None)) # False: empty
node = ListNode(1)
print(hasCycle(node)) # False: single, no cycle
node.next = node
print(hasCycle(node)) # True: single node cycleวงจรในรายการเชื่อมโยง II: LeetCode 142
LeetCode 142 เรื่อง 'วงจรในรายการเชื่อมโยง II' ให้หาโหนดที่วงจรเริ่มต้น (หรือค่าว่างหากไม่มีวงจร) นี่คือการประยุกต์ใช้อัลกอริทึมสองระยะของฟลอยด์โดยตรง ผู้สัมภาษณ์มักถามข้อนี้ต่อจากโจทย์การตรวจจับวงจรพื้นฐาน วิธีแก้ทั้งหมดคือ ระยะที่ 1 หาจุดที่พบกันภายในวงจร ระยะที่ 2 ย้ายตัวชี้ตัวหนึ่งไปที่หัวรายการ แล้วเลื่อนตัวชี้ทั้งสองไปข้างหน้าจนพบกัน จุดที่พบกันนั้นคือจุดเริ่มต้นของวงจร
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def detectCycle(head):
slow = fast = head
# Phase 1
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow is fast:
break
else:
return None
# Phase 2
ptr = head
while ptr is not slow:
ptr = ptr.next
slow = slow.next
return ptr
nodes = [ListNode(v) for v in [1, 2, 3, 4, 5]]
for i in range(4):
nodes[i].next = nodes[i+1]
nodes[4].next = nodes[2] # cycle entry: node with val=3
entry = detectCycle(nodes[0])
print(entry.val) # 3เหตุใดวิธีของฟลอยด์จึงดีกว่าวิธีใช้เซต
แม้ว่าทั้งสองวิธีจะใช้เวลา O(n) แต่ตัวคูณคงที่แตกต่างกันในการใช้งานจริง วิธีใช้เซตต้องคำนวณค่าแฮชของตัวชี้แต่ละโหนด (คำนวณแฮช ค้นช่องในตารางแฮช และจัดเก็บตัวชี้) ขณะที่อัลกอริทึมของฟลอยด์ทำเพียงการเข้าถึงค่าผ่านตัวชี้ ซึ่งมีต้นทุนต่อขั้นต่ำกว่ามาก ที่สำคัญกว่านั้น การรับประกันการใช้พื้นที่ O(1) หมายความว่าอัลกอริทึมของฟลอยด์สามารถทำงานกับรายการที่ยาวเท่าใดก็ได้โดยไม่เสี่ยงต่อหน่วยความจำไม่เพียงพอ
การกล่าวถึงข้อได้เปรียบด้านพื้นที่นี้ก่อนในการสัมภาษณ์งาน แสดงให้เห็นว่าคุณเข้าใจการแลกเปลี่ยนของอัลกอริทึมอย่างลึกซึ้ง ไม่ได้มองเพียงสัญกรณ์บิ๊กโอเท่านั้น
ตรวจสอบความเข้าใจอย่างรวดเร็ว
ทดสอบความเข้าใจแนวคิดเรื่องโครงสร้างข้อมูลและอัลกอริทึม — การเตรียมตัวสัมภาษณ์การเขียนโปรแกรมจากบทเรียนนี้
ทบทวนบทเรียน
ในบทเรียนนี้ คุณได้เรียนรู้ว่า: อัลกอริทึมตัวชี้ช้า-เร็วของฟลอยด์ตรวจจับวงจรได้ในเวลา O(n) และใช้พื้นที่ O(1) ระยะที่ 2 (ย้ายตัวชี้ตัวหนึ่งไปที่หัวรายการ แล้วเลื่อนทั้งสองครั้งละ 1 ขั้น) จะหาจุดเริ่มต้นของวงจรได้อย่างแม่นยำ และ เทคนิคเดียวกันนี้ใช้ได้นอกเหนือจากรายการเชื่อมโยงกับลำดับโดยนัยใด ๆ ที่ 'ค่าถัดไป' เป็นฟังก์ชัน ต่อไปเราจะพูดถึงการผสานรายการที่เรียงลำดับแล้ว การแบ่งรายการที่จุดกึ่งกลาง และการหาโหนดลำดับที่ n จากท้ายรายการ
คำถามที่พบบ่อย
บทเรียน “การตรวจจับวัฏจักรด้วยอัลกอริทึมของ Floyd” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “การตรวจจับวัฏจักรด้วยอัลกอริทึมของ Floyd” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส DSA Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “การตรวจจับวัฏจักรด้วยอัลกอริทึมของ Floyd”
ตรวจจับวัฏจักรด้วยแนวทางตัวชี้ช้า–เร็ว ค้นหาจุดเริ่มต้นของวัฏจักร และพิสูจน์ความถูกต้องของอัลกอริทึมทางคณิตศาสตร์ คุณปฏิบัติ DSA Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน DSA Interview Prep หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน DSA Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน
บทเรียน “การตรวจจับวัฏจักรด้วยอัลกอริทึมของ Floyd” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน DSA Interview Prep นี้ได้ไหม
ได้ บทเรียน DSA Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- คลาสโหนดและการสร้างลิสต์
- การกลับลำดับลิสต์เชื่อมโยง
- การตรวจจับวัฏจักรด้วยอัลกอริทึมของ Floyd
- ผสาน แบ่ง และค้นหาโหนดลำดับที่ n จากท้าย