ตัวชี้สองตัว: ช้าและเร็ว
ใช้รูปแบบตัวชี้ช้า–เร็วเพื่อลบค่าซ้ำในที่เดิม ย้ายศูนย์ และแบ่งอาร์เรย์รอบค่าหมุด
ตัวชี้สองตัว: ช้าและเร็ว เป็นบทเรียน Coding Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Coding Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
อธิบายตัวชี้ช้าและเร็ว
รูปแบบ ตัวชี้ช้า-เร็ว หรือที่เรียกอีกอย่างว่าเต่ากับกระต่าย ใช้ตัวชี้สองตัวเคลื่อนที่ด้วยความเร็วต่างกันผ่านลำดับเดียวกัน ต่างจากตัวชี้จากปลายตรงข้าม ตัวชี้ทั้งคู่เริ่มต้นจากจุดเริ่มต้น ตัวชี้ช้าเลื่อนไปครั้งละหนึ่งขั้น ส่วนตัวชี้เร็วเลื่อนไปครั้งละสองขั้นหรือมากกว่า ความเร็วที่ต่างกันทำให้เกิดค่าคงรูปที่เป็นประโยชน์: ตัวชี้ชาจะติดตาม “คำนำหน้าที่ถูกต้อง” ขณะที่ตัวชี้เร็วสแกนเงื่อนไขที่อยู่ถัดไป
# Slow pointer marks the write position;
# Fast pointer scans for next non-duplicate.
def remove_duplicates(nums):
if not nums: return 0
slow = 0 # next position to write a unique value
for fast in range(1, len(nums)):
if nums[fast] != nums[slow]:
slow += 1
nums[slow] = nums[fast]
return slow + 1 # new length
nums = [1, 1, 2, 3, 3, 3, 4]
k = remove_duplicates(nums)
print(nums[:k]) # [1, 2, 3, 4]ลบค่าซ้ำจากอาร์เรย์ที่เรียงลำดับ
ในอาร์เรย์ที่เรียงลำดับ ค่าซ้ำจะอยู่ติดกัน ตัวชี้ช้าจะติดตามค่าที่ ไม่ซ้ำ ค่าสุดท้ายที่ถูกเขียนไว้ ส่วนตัวชี้เร็วจะสแกนไปข้างหน้า เมื่อใดก็ตามที่ตัวชี้เร็วพบค่าที่แตกต่างจาก nums[slow] ให้เลื่อนตัวชี้ช้าแล้วคัดลอกค่าใหม่ อัลกอริทึมที่ทำงานในพื้นที่เดิมนี้ใช้เวลา O(n) และใช้พื้นที่เพิ่มเติม O(1) เป็นโจทย์สัมภาษณ์มาตรฐานที่ทดสอบความเข้าใจรูปแบบตัวชี้อ่าน-เขียน
def remove_duplicates_v2(nums):
slow = 0
for fast in range(len(nums)):
if nums[fast] != nums[slow]:
slow += 1
nums[slow] = nums[fast]
return slow + 1
# Allow at most 2 occurrences
def remove_duplicates_k2(nums):
slow = 0
for fast in range(len(nums)):
if slow < 2 or nums[fast] != nums[slow - 2]:
nums[slow] = nums[fast]
slow += 1
return slow
print(remove_duplicates_k2([1,1,1,2,2,3]))
# Result: 5, nums[:5] = [1,1,2,2,3]ย้ายเลขศูนย์ด้วยตัวชี้ช้า-เร็ว
ย้ายเลขศูนย์ทั้งหมดไปไว้ท้ายอาร์เรย์ โดยคงลำดับสัมพัทธ์ของสมาชิกที่ไม่ใช่ศูนย์ไว้ ตัวชี้ช้าจะระบุตำแหน่งถัดไปสำหรับสมาชิกที่ไม่ใช่ศูนย์ ส่วนตัวชี้เร็วจะสแกนหาค่าที่ไม่ใช่ศูนย์ เมื่อพบค่า ตัวชี้เร็วจะคัดลอกค่านั้นไปยังตำแหน่งของตัวชี้ช้า แล้วเลื่อนตัวชี้ทั้งคู่ หลังจากสแกนเสร็จ ให้เติมเลขศูนย์ตั้งแต่ตำแหน่งของตัวชี้ช้าจนถึงท้ายอาร์เรย์ ใช้เวลา O(n) และพื้นที่ O(1)
def move_zeroes(nums):
slow = 0 # next position for a non-zero
for fast in range(len(nums)):
if nums[fast] != 0:
nums[slow] = nums[fast]
slow += 1
# Fill rest with zeroes
while slow < len(nums):
nums[slow] = 0
slow += 1
nums = [0, 1, 0, 3, 12]
move_zeroes(nums)
print(nums) # [1, 3, 12, 0, 0]แบ่งอาร์เรย์รอบจุดหมุน
ขั้นตอนย่อยการแบ่งส่วนของการเรียงลำดับแบบเร็วจะจัดเรียงสมาชิกในพื้นที่เดิม เพื่อให้ค่าทั้งหมดที่ < จุดหมุนอยู่ก่อนค่าที่ >= จุดหมุน วิธีของโลมูโตใช้ตัวชี้ช้าเพื่อระบุตำแหน่งสุดท้ายของค่าน้อย และใช้ตัวชี้เร็วสแกนไปข้างหน้า เมื่อตัวชี้เร็วพบค่าน้อย ให้เพิ่มตัวชี้ช้าแล้วสลับค่า วิธีนี้ใช้เวลา O(n) และใช้พื้นที่เพิ่มเติม O(1)
def lomuto_partition(nums, low, high):
pivot = nums[high]
slow = low - 1 # last position of small element
for fast in range(low, high):
if nums[fast] <= pivot:
slow += 1
nums[slow], nums[fast] = nums[fast], nums[slow]
# Place pivot in final position
nums[slow+1], nums[high] = nums[high], nums[slow+1]
return slow + 1 # pivot's final index
arr = [3, 1, 4, 1, 5, 9, 2, 6]
p = lomuto_partition(arr, 0, len(arr)-1)
print(arr) # elements before p are <= pivotค้นหาโหนดกลางของรายการเชื่อมโยง
เมื่อใช้ตัวชี้ช้า-เร็วกับรายการเชื่อมโยง ตัวชี้เร็วจะเลื่อนไปสองโหนดต่อหนึ่งขั้น ส่วนตัวชี้ช้าจะเลื่อนไปหนึ่งโหนด เมื่อตัวชี้เร็วถึงท้ายรายการ ตัวชี้ช้าจะอยู่ตรงกลาง วิธีผ่านรายการเพียงรอบเดียวด้วยเวลา O(n) นี้สะอาดกว่าการนับจำนวนโหนดแล้วค่อยเดินไปครึ่งทางมาก วิธีนี้ใช้เป็นขั้นตอนย่อยในการเรียงลำดับแบบผสานสำหรับรายการเชื่อมโยง และการตรวจหาพาลินโดรมในรายการเชื่อมโยง
class Node:
def __init__(self, val, nxt=None):
self.val = val
self.next = nxt
def find_middle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow # slow is at middle
# Build 1->2->3->4->5
h = Node(1, Node(2, Node(3, Node(4, Node(5)))))
mid = find_middle(h)
print(mid.val) # 3 (middle of 5 nodes)การตรวจจับวัฏจักร: เต่ากับกระต่ายของฟลอยด์
การตรวจจับวัฏจักรของฟลอยด์จะวางตัวชี้ช้าและตัวชี้เร็วไว้ที่หัวของรายการเชื่อมโยง ตัวชี้ช้าจะเลื่อนไปหนึ่งโหนด ส่วนตัวชี้เร็วจะเลื่อนไปสองโหนด หากมีวัฏจักร ตัวชี้เร็วจะค่อย ๆ ไล่ทันตัวชี้ช้าและทั้งคู่จะพบกันภายในวัฏจักร หากตัวชี้เร็วไปถึงจุดสิ้นสุด ก็แปลว่าไม่มีวัฏจักร การพบกันนี้รับประกันได้ เพราะตัวชี้เร็วจะได้เปรียบตัวชี้ช้าหนึ่งขั้นในแต่ละรอบการทำงาน ดังนั้นในวัฏจักรที่มีความยาว k ทั้งคู่จะพบกันภายใน k ขั้นนับจากตัวชี้ช้าเข้าสู่วัฏจักร
class ListNode:
def __init__(self, val=0, nxt=None):
self.val = val
self.next = nxt
def has_cycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow is fast: # identity check (same object)
return True
return False
# 1->2->3->4->2 (cycle at node 2)
n1 = ListNode(1)
n2 = ListNode(2)
n3 = ListNode(3)
n4 = ListNode(4)
n1.next=n2; n2.next=n3; n3.next=n4; n4.next=n2
print(has_cycle(n1)) # Trueค้นหาจุดเริ่มต้นของวัฏจักร
หลังจากตรวจพบวัฏจักรแล้ว (slow == fast) ให้ย้ายตัวชี้ตัวหนึ่งกลับไปที่หัว จากนั้นเลื่อนตัวชี้ ทั้งคู่ ไปทีละหนึ่งขั้น ทั้งคู่จะพบกันที่จุดเริ่มต้นของวัฏจักร วิธีนี้อาศัยคุณสมบัติทางคณิตศาสตร์ที่ว่าระยะทางจากหัวถึงจุดเริ่มต้นของวัฏจักรเท่ากับระยะทางจากจุดที่พบกันถึงจุดเริ่มต้นของวัฏจักร เมื่อคิดแบบโมดูโลความยาวของวัฏจักร ผลลัพธ์ทางคณิตศาสตร์อันงดงามนี้ปรากฏอยู่บ่อยครั้งในโจทย์สัมภาษณ์ระดับยาก
def detect_cycle(head):
slow = fast = head
# Phase 1: detect
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
slow = head
while slow is not fast:
slow = slow.next
fast = fast.next
return slow # cycle entry node
# Using same cycled list as previous scene
print(detect_cycle(n1).val) # 2 (cycle entry)ตัวชี้ช้า-เร็วสำหรับเลขมีความสุข
ตัวชี้ช้า-เร็วไม่ได้ใช้เฉพาะกับรายการเชื่อมโยง แต่ใช้ได้กับกระบวนการใด ๆ ที่เกิดวัฏจักร เลขมีความสุขจะวนผ่านผลรวมกำลังสองของเลขแต่ละหลัก หาก n ไม่ใช่เลขมีความสุข ลำดับนั้นจะวนซ้ำในที่สุด ให้ตรวจจับการวนซ้ำด้วยตัวช้า ซึ่งหนึ่งขั้นเท่ากับผลรวมกำลังสองของเลขหนึ่งหลัก และตัวเร็ว ซึ่งเดินครั้งละสองขั้น หากทั้งคู่พบกันที่ 1 แสดงว่า n เป็นเลขมีความสุข มิฉะนั้น n จะติดอยู่ในวัฏจักรที่ไม่ใช่ 1 นี่คืออัลกอริทึมของฟลอยด์ที่นำมาใช้กับรายการเชื่อมโยงเสมือนของค่า
def is_happy(n):
def next_val(x):
total = 0
while x:
x, d = divmod(x, 10)
total += d * d
return total
slow = n
fast = next_val(n)
while fast != 1 and slow != fast:
slow = next_val(slow)
fast = next_val(next_val(fast))
return fast == 1
print(is_happy(19)) # True (1->9->...->1)
print(is_happy(2)) # False (enters a cycle)โหนดลำดับที่ n จากท้ายรายการ
ค้นหาโหนดลำดับที่ n จากท้ายรายการเชื่อมโยงโดยใช้ตัวชี้สองตัวและเดินผ่านรายการเพียงรอบเดียว เลื่อนตัวชี้เร็วล่วงหน้าไป n ขั้น จากนั้นเลื่อนตัวชี้ทั้งคู่ไปพร้อมกันจนตัวชี้เร็วถึงท้ายรายการ ขณะนั้นตัวชี้ช้าจะอยู่ที่โหนดลำดับที่ n จากท้าย หากต้องการลบโหนดนี้ ให้เก็บตัวชี้ก่อนหน้าซึ่งอยู่หลังตัวชี้ช้าหนึ่งขั้นไว้ วิธีนี้เป็นโจทย์รายการเชื่อมโยงแบบผ่านรอบเดียวที่ใช้กันทั่วไป และไม่ต้องนับความยาวทั้งหมดก่อน
def remove_nth_from_end(head, n):
dummy = ListNode(0)
dummy.next = head
fast = slow = dummy
# Advance fast n+1 steps
for _ in range(n + 1):
fast = fast.next
# Advance together
while fast:
slow = slow.next
fast = fast.next
# slow.next is the nth from end
slow.next = slow.next.next
return dummy.next
# Build 1->2->3->4->5, remove 2nd from end
h2 = ListNode(1,ListNode(2,ListNode(3,ListNode(4,ListNode(5)))))
result = remove_nth_from_end(h2, 2)
# Should give 1->2->3->5ตัวชี้ช้า-เร็วในโจทย์สตริง
แนวคิดตัวชี้ช้า-เร็วยังใช้กับโจทย์อาร์เรย์และสตริงได้ด้วย เมื่อต้องบีบอัดสตริงที่เข้ารหัสด้วยความยาวช่วง ตัวชี้ช้าจะระบุตำแหน่งเขียน ส่วนตัวชี้เร็วจะสแกนไปจนถึงท้ายของแต่ละช่วง เมื่ออักขระทั้งหมดในช่วงเท่ากับอักขระที่ตัวชี้ช้าชี้อยู่ ให้เลื่อนตัวชี้เร็วต่อไป มิฉะนั้นให้บันทึกช่วงนั้นแล้วปรับตำแหน่งตัวชี้ช้า วิธีนี้ทำงานได้ในเวลา O(n) ด้วยการผ่านเพียงรอบเดียวและใช้พื้นที่ O(1)
def compress(chars):
slow = fast = 0
while fast < len(chars):
char = chars[fast]
count = 0
# Count the run
while fast < len(chars) and chars[fast] == char:
fast += 1
count += 1
chars[slow] = char
slow += 1
if count > 1:
for c in str(count):
chars[slow] = c
slow += 1
return slow
chars = list('aabcccccaa')
print(compress(chars)) # 6
print(chars[:6]) # ['a','2','b','c','5','a']... wait
# Actually: ['a','2','b','c','5','a','2']การเลือกใช้ตัวชี้ช้า-เร็วหรือตัวชี้จากปลายตรงข้าม
ใช้ตัวชี้ จากปลายตรงข้าม เมื่อโจทย์เกี่ยวข้องกับคู่ที่มีผลรวมเท่ากับเป้าหมาย การตรวจสอบพาลินโดรม หรือการบีบช่วงจากทั้งสองด้าน ใช้ตัวชี้ ช้า-เร็ว เมื่อจำเป็นต้องมีตัวชี้เขียน เช่น การลบหรือย้ายสมาชิก เมื่อต้องประมวลผลโครงสร้างรายการเชื่อมโยง เช่น การหาโหนดกลางหรือวัฏจักร หรือเมื่อต้องตรวจจับวัฏจักรในลำดับค่าใด ๆ ทั้งสองวิธีช่วยกำจัดการวนซ้อนและทำงานได้ในเวลา O(n) ปัจจัยที่ใช้ตัดสินใจคือโครงสร้างของการเดินผ่านข้อมูล
# Pattern matcher:
# 1. Sorted array, target sum -> OPPOSITE ENDS
# 2. Remove/filter elements in-place -> SLOW-FAST (read-write)
# 3. Linked list middle/cycle -> SLOW-FAST (1x vs 2x speed)
# 4. Detect cycle in value sequence -> SLOW-FAST (Floyd)
# Example: given sorted array, remove val in-place
def remove_sorted(nums, val):
slow = 0
for fast in range(len(nums)):
if nums[fast] != val:
nums[slow] = nums[fast]
slow += 1
return slow
nums = [0,1,2,2,3,0,4,2]
print(remove_sorted(nums, 2)) # 5ตรวจสอบความเข้าใจ
ตรวจสอบความเข้าใจแนวคิดโครงสร้างข้อมูลและอัลกอริทึมสำหรับการเตรียมสัมภาษณ์การเขียนโปรแกรมจากบทเรียนนี้
สรุปบทเรียน
ในบทเรียนนี้คุณได้เรียนรู้ว่า รูปแบบตัวชี้ช้า-เร็ว (อ่าน-เขียน) จะรักษาตัวชี้เขียนไว้ที่ตำแหน่งที่ถูกต้องถัดไป ขณะที่ตัวชี้เร็วสแกนไปข้างหน้า ซึ่งเป็นแกนหลักของการลบ การลบค่าซ้ำ และการย้ายเลขศูนย์ในพื้นที่เดิม วิธีเต่ากับกระต่ายของฟลอยด์ตรวจจับวัฏจักรได้ในเวลา O(n) และใช้พื้นที่ O(1) โดยอาศัยความแตกต่างของความเร็วระหว่างตัวชี้สองตัว และ หลังตรวจพบวัฏจักร การย้ายตัวชี้ตัวหนึ่งกลับไปที่หัวแล้วเลื่อนทั้งคู่ด้วยความเร็วเท่ากันจะค้นพบจุดเริ่มต้นของวัฏจักร เนื่องจากระยะทางทั้งสองเท่ากันตามที่พิสูจน์ได้ ต่อไปเราจะศึกษา API สตริงของไพธอนสำหรับการสัมภาษณ์
คำถามที่พบบ่อย
บทเรียน “ตัวชี้สองตัว: ช้าและเร็ว” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “ตัวชี้สองตัว: ช้าและเร็ว” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Coding Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “ตัวชี้สองตัว: ช้าและเร็ว”
ใช้รูปแบบตัวชี้ช้า–เร็วเพื่อลบค่าซ้ำในที่เดิม ย้ายศูนย์ และแบ่งอาร์เรย์รอบค่าหมุด คุณปฏิบัติ Coding Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Coding Interview Prep หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน Coding Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน
บทเรียน “ตัวชี้สองตัว: ช้าและเร็ว” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน Coding Interview Prep นี้ได้ไหม
ได้ บทเรียน Coding Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- พื้นฐานอาร์เรย์และการดำเนินการในที่เดิม
- ผลรวมคำนำหน้าและยอดรวมสะสม
- ตัวชี้สองตัว: จากปลายตรงข้าม
- ตัวชี้สองตัว: ช้าและเร็ว