0Pricing
DSA Interview Prep · บทเรียน

ตัวชี้สองตัว: ช้าและเร็ว

ใช้รูปแบบตัวชี้ช้า–เร็วเพื่อลบค่าซ้ำในที่เดิม ย้ายศูนย์ และแบ่งอาร์เรย์รอบค่าหมุด

ตัวชี้สองตัว: ช้าและเร็ว เป็นบทเรียน DSA Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน DSA Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส DSA 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) และปลดล็อคส่วนที่เหลือของคอร์ส DSA Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “ตัวชี้สองตัว: ช้าและเร็ว”

ใช้รูปแบบตัวชี้ช้า–เร็วเพื่อลบค่าซ้ำในที่เดิม ย้ายศูนย์ และแบ่งอาร์เรย์รอบค่าหมุด คุณปฏิบัติ DSA Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน DSA Interview Prep หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน DSA Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน

บทเรียน “ตัวชี้สองตัว: ช้าและเร็ว” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน DSA Interview Prep นี้ได้ไหม

ได้ บทเรียน DSA Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. พื้นฐานอาร์เรย์และการดำเนินการในที่เดิม
  2. ผลรวมคำนำหน้าและยอดรวมสะสม
  3. ตัวชี้สองตัว: จากปลายตรงข้าม
  4. ตัวชี้สองตัว: ช้าและเร็ว
← กลับไปที่ DSA Interview Prep