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

ตัวชี้สองตัว: จากปลายตรงข้าม

ใช้ตัวชี้ซ้ายและขวาเคลื่อนเข้าหากันเพื่อแก้โจทย์ผลรวมคู่ในอาร์เรย์ที่เรียงแล้ว พาลินโดรมที่ถูกต้อง และการกักเก็บน้ำฝน

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

แนวคิดตัวชี้สองตัว

เทคนิคตัวชี้สองตัว ใช้ตัวแปรดัชนีสองตัวที่เคลื่อนเข้าหากัน (หรือเคลื่อนไปในทิศทางเดียวกัน) เพื่อลดความจำเป็นในการใช้ลูปซ้อน แทนที่จะตรวจสอบทุกคู่ด้วยเวลา O(n²) ให้สร้างความคืบหน้าในทุกครั้งที่เปรียบเทียบ แล้วเสร็จสิ้นด้วยเวลา O(n) เทคนิคนี้แทบจะต้องใช้อาร์เรย์ที่ เรียงลำดับแล้ว ก่อนเสมอ เพราะการเรียงลำดับช่วยให้พิจารณาได้ว่าควรเลื่อนตัวชี้ไปในทิศทางใด โดยดูจากว่าผลรวมของคู่ปัจจุบันมากหรือน้อยเกินไป

# Without two pointers: O(n^2)
def two_sum_brute(nums, target):
    for i in range(len(nums)):
        for j in range(i+1, len(nums)):
            if nums[i] + nums[j] == target:
                return [i, j]
    return []

# With two pointers on sorted array: O(n)
def two_sum_sorted(nums, target):
    left, right = 0, len(nums) - 1
    while left < right:
        s = nums[left] + nums[right]
        if s == target: return [left, right]
        elif s < target: left  += 1
        else:           right -= 1
    return []

ผลรวมสองตัวในอาร์เรย์ที่เรียงลำดับแล้ว

เมื่ออาร์เรย์เรียงลำดับแล้ว ให้วางตัวชี้หนึ่งตัวไว้ที่ปลายซ้าย (ค่าน้อยที่สุด) และอีกตัวไว้ที่ปลายขวา (ค่ามากที่สุด) หากผลรวมเล็กเกินไป ให้เลื่อนตัวชี้ซ้ายไปทางขวาเพื่อเพิ่มผลรวม หากผลรวมใหญ่เกินไป ให้เลื่อนตัวชี้ขวาไปทางซ้ายเพื่อลดผลรวม ในแต่ละรอบจะมีตัวชี้อย่างน้อยหนึ่งตัวเคลื่อนที่ ดังนั้นลูปจึงทำงานไม่เกิน n ครั้ง หรือใช้เวลา O(n) โดยรวมหลังจากการเรียงลำดับ ที่สำคัญ การเลื่อนแต่ละครั้ง พิสูจน์ได้ว่าถูกต้อง เนื่องจากลำดับของอาร์เรย์ที่เรียงแล้ว

def two_sum_sorted(numbers, target):
    # numbers is 1-indexed per LeetCode 167
    left, right = 0, len(numbers) - 1
    while left < right:
        s = numbers[left] + numbers[right]
        if s == target:
            return [left + 1, right + 1]  # 1-indexed
        elif s < target:
            left  += 1  # need larger sum
        else:
            right -= 1  # need smaller sum
    return []

print(two_sum_sorted([2, 7, 11, 15], 9))   # [1, 2]
print(two_sum_sorted([2, 3, 4], 6))         # [1, 3]

การตรวจสอบข้อความที่อ่านกลับเหมือนเดิม

สตริงเป็นข้อความที่อ่านกลับเหมือนเดิม หากอ่านจากด้านหน้าและด้านหลังแล้วได้เหมือนกัน ให้ใช้ตัวชี้สองตัวเริ่มจากปลายทั้งสองด้านแล้วเคลื่อนเข้าหากัน: เปรียบเทียบอักขระ ข้ามอักขระที่ไม่ใช่ตัวอักษรหรือตัวเลข และหยุดเมื่อตัวชี้ข้ามกัน วิธีนี้ใช้เวลา O(n) และพื้นที่เพิ่มเติม O(1) ซึ่งสะอาดกว่าการกลับสตริงแล้วเปรียบเทียบอย่างมาก เพราะวิธีหลังจัดสรรหน่วยความจำเพิ่มเติม O(n)

def is_palindrome(s):
    left, right = 0, len(s) - 1
    while left < right:
        # Skip non-alphanumeric
        while left < right and not s[left].isalnum():
            left += 1
        while left < right and not s[right].isalnum():
            right -= 1
        if s[left].lower() != s[right].lower():
            return False
        left += 1
        right -= 1
    return True

print(is_palindrome('A man, a plan, a canal: Panama'))  # True
print(is_palindrome('race a car'))                       # False

ผลรวมสามตัว: sort + ตัวชี้สองตัว

โจทย์ผลรวมสามตัวต้องการชุดสามสมาชิกที่ไม่ซ้ำกันทั้งหมดซึ่งมีผลรวมเป็นศูนย์ ให้ใช้ sort กับอาร์เรย์ก่อน จากนั้นกำหนดสมาชิกแต่ละตัว nums[i] แล้วค้นหาด้วยตัวชี้สองตัวในช่วงย่อยที่เหลือ เพื่อหาคู่ที่มีผลรวมเป็น -nums[i] ข้ามสมาชิกที่ซ้ำกันทั้งในส่วนสมาชิกที่กำหนดและคู่ที่พบ เพื่อหลีกเลี่ยงชุดสามสมาชิกซ้ำกัน เวลารวมคือ O(n²) หลังจาก sort ที่ใช้เวลา O(n log n)

def three_sum(nums):
    nums.sort()
    result = []
    for i in range(len(nums) - 2):
        if i > 0 and nums[i] == nums[i-1]: continue  # skip dupe
        left, right = i + 1, len(nums) - 1
        while left < right:
            s = nums[i] + nums[left] + nums[right]
            if s == 0:
                result.append([nums[i], nums[left], nums[right]])
                while left < right and nums[left]  == nums[left+1]:  left  += 1
                while left < right and nums[right] == nums[right-1]: right -= 1
                left += 1; right -= 1
            elif s < 0: left  += 1
            else:       right -= 1
    return result

print(three_sum([-1, 0, 1, 2, -1, -4]))
# [[-1,-1,2],[-1,0,1]]

ภาชนะที่บรรจุน้ำได้มากที่สุด

เมื่อกำหนดความสูงของเส้นแนวตั้ง ให้หาเส้นสองเส้นที่สร้างภาชนะซึ่งบรรจุน้ำได้มากที่สุด พื้นที่ = min(height[left], height[right]) × (right - left) ให้เลื่อนตัวชี้ที่อยู่ตรงเส้นที่ สั้นกว่า เข้าด้านในแบบเลือกทางที่เหมาะสมที่สุด การเลื่อนเส้นที่สูงกว่าสามารถลดความกว้างลงได้เท่านั้น โดยไม่เพิ่มขอบเขตความสูง การเลือกนี้พิสูจน์ได้ว่าเหมาะสมที่สุดและใช้เวลา O(n)

def max_area(height):
    left, right = 0, len(height) - 1
    best = 0
    while left < right:
        h    = min(height[left], height[right])
        area = h * (right - left)
        best = max(best, area)
        # Move the shorter wall inward
        if height[left] < height[right]:
            left  += 1
        else:
            right -= 1
    return best

print(max_area([1, 8, 6, 2, 5, 4, 8, 3, 7]))  # 49

การยกกำลังสองของอาร์เรย์ที่เรียงลำดับแล้ว

ยกกำลังสองสมาชิกแต่ละตัวของอาร์เรย์ที่เรียงลำดับแล้ว ซึ่งอาจมีค่าติดลบ และส่งคืนผลลัพธ์ในลำดับที่เรียงแล้ว ค่ากำลังสองของจำนวนลบมีขนาดใหญ่ ส่วนค่ากำลังสองของจำนวนบวกมีขนาดเล็กเมื่ออยู่ใกล้ตรงกลาง วางตัวชี้สองตัวไว้ที่ปลายทั้งสองด้าน แล้วเติมอาร์เรย์ผลลัพธ์จากขวาไปซ้าย (จากค่ามากไปหาค่าน้อย) ใช้เวลา O(n) และพื้นที่ O(n) สำหรับผลลัพธ์ ซึ่งดีกว่าการยกกำลังสองแล้วเรียงลำดับที่ใช้เวลา O(n log n) อย่างมาก

def sorted_squares(nums):
    n = len(nums)
    result = [0] * n
    left, right = 0, n - 1
    pos = n - 1
    while left <= right:
        l_sq = nums[left]  ** 2
        r_sq = nums[right] ** 2
        if l_sq > r_sq:
            result[pos] = l_sq
            left += 1
        else:
            result[pos] = r_sq
            right -= 1
        pos -= 1
    return result

print(sorted_squares([-4, -1, 0, 3, 10]))
# [0, 1, 9, 16, 100]

การดักน้ำฝน

น้ำที่ขังอยู่ที่ดัชนี i มีค่าเท่ากับ min(max_left, max_right) - height[i] แนวทางตัวชี้สองตัวคือรักษาค่า max_left และ max_right ที่เป็นค่าสะสมไว้ เมื่อ max_left < max_right ด้านซ้ายเป็นคอขวด ให้ประมวลผลตัวชี้ซ้าย มิฉะนั้นให้ประมวลผลตัวชี้ขวา วิธีนี้ไม่จำเป็นต้องใช้อาร์เรย์ค่าสูงสุดทางซ้ายและทางขวาแยกกัน จึงใช้พื้นที่เพิ่มเติม O(1)

def trap(height):
    left, right = 0, len(height) - 1
    max_left = max_right = 0
    water = 0
    while left < right:
        if height[left] < height[right]:
            if height[left] >= max_left:
                max_left = height[left]
            else:
                water += max_left - height[left]
            left += 1
        else:
            if height[right] >= max_right:
                max_right = height[right]
            else:
                water += max_right - height[right]
            right -= 1
    return water

print(trap([0,1,0,2,1,0,1,3,2,1,2,1]))  # 6

เหตุใดการเลื่อนตัวชี้แบบเลือกทางที่เหมาะสมที่สุดจึงใช้ได้

คำถามต่อยอดที่พบบ่อยในการสัมภาษณ์คือ เหตุใดจึงปลอดภัยที่จะทิ้งตัวชี้ที่มีค่าน้อยกว่า โครงร่างการพิสูจน์สำหรับโจทย์ภาชนะที่บรรจุน้ำได้มากที่สุดมีดังนี้: สมมติว่า height[left] < height[right] คู่ทุกคู่ (left, j) สำหรับ j < right จะมีพื้นที่ ≤ height[left] × (j-left) < height[left] × (right-left) ≤ พื้นที่ปัจจุบัน ดังนั้นไม่มีคู่ใดที่เริ่มจาก 'left' และมีดัชนีขวาน้อยกว่า 'right' จะให้พื้นที่มากกว่าพื้นที่ปัจจุบัน เราจึงข้ามคู่เหล่านั้นได้อย่างปลอดภัยด้วยการเลื่อน left ไปข้างหน้า

# Correctness argument via contradiction:
# If left < right and height[left] < height[right],
# then for any j in (left, right):
#   area(left, j) <= min(h[left], h[j]) * (j - left)
#                 <= h[left] * (j - left)
#                 <= h[left] * (right - left)   [since j < right]
#                 = current area
# So no pair (left, j) for j < right can improve.
# Moving left inward is SAFE.

print('Proof verified: advance shorter pointer is optimal')

คู่ที่มีผลต่างน้อยที่สุดในอาร์เรย์ที่เรียงลำดับ

ค้นหาคู่ตัวเลขในอาร์เรย์ที่เรียงลำดับซึ่งมีผลต่างสัมบูรณ์น้อยที่สุด ใช้ตัวชี้สองตัวที่อยู่ติดกัน (ไม่ใช่ปลายตรงข้าม) สแกนไปพร้อมกัน โดยคำนวณ |nums[i] - nums[i+1]| สำหรับทุกคู่ที่อยู่ต่อเนื่องกัน ผลต่างต่ำสุดในอาร์เรย์ที่เรียงลำดับจะเกิดขึ้นระหว่างสมาชิกที่อยู่ติดกันเสมอ เพราะการเรียงลำดับจะจัดค่าที่ใกล้เคียงกันไว้ด้วยกัน การดำเนินการนี้ใช้เวลา O(n) หลังจากเรียงลำดับแล้ว

def min_diff_pair(nums):
    nums.sort()  # O(n log n)
    min_diff = float('inf')
    best = (nums[0], nums[1])
    for i in range(len(nums) - 1):
        diff = nums[i+1] - nums[i]  # sorted: always >= 0
        if diff < min_diff:
            min_diff = diff
            best = (nums[i], nums[i+1])
    return best, min_diff

pair, d = min_diff_pair([4, 2, 1, 6, 10, 8])
print(pair, d)  # (1, 2) 1

แม่แบบตัวชี้สองตัวจากปลายตรงข้าม

โจทย์ที่ใช้ตัวชี้สองตัวจากปลายตรงข้ามส่วนใหญ่มีโครงร่างเหมือนกัน การทำความเข้าใจแม่แบบนี้จะช่วยให้คุณปรับใช้ได้อย่างรวดเร็วเมื่อต้องทำงานแข่งกับเวลา การตัดสินใจสำคัญมีดังนี้: (1) เงื่อนไขใดทำให้เลื่อนตัวชี้ซ้าย (2) เงื่อนไขใดทำให้เลื่อนตัวชี้ขวา (3) สิ่งใดถือเป็นคำตอบ และ (4) จะจัดการกับค่าซ้ำอย่างไร ฝึกแปลงการตัดสินใจเหล่านี้เป็นขั้นตอนจากข้อความโจทย์ก่อนเขียนโค้ด

def two_pointer_template(arr, condition):
    """
    Generic opposite-ends two-pointer skeleton.
    Replace condition logic for each specific problem.
    """
    left, right = 0, len(arr) - 1
    result = []
    while left < right:
        current = arr[left] + arr[right]  # or some combination
        if current == condition:           # found a valid pair
            result.append((arr[left], arr[right]))
            left  += 1
            right -= 1
        elif current < condition:          # need to increase
            left  += 1
        else:                             # need to decrease
            right -= 1
    return result

การนับคู่ที่ถูกต้องด้วยตัวชี้สองตัว

ตัวชี้สองตัวยังช่วยนับคู่ได้อย่างมีประสิทธิภาพ สำหรับโจทย์ให้ “นับคู่ที่มีผลรวมน้อยกว่าเป้าหมาย” ในอาร์เรย์ที่เรียงลำดับ ให้ตรึงตัวชี้ซ้ายไว้ แล้วใช้ตัวชี้ขวาค้นหาดัชนีขวาสุดที่ยังถูกต้อง คู่ทั้งหมดตั้งแต่ (ซ้าย, ซ้าย+1 ถึงขวา) ล้วนถูกต้อง ให้เพิ่ม right - left ลงในจำนวนคู่ แล้วเลื่อนตัวชี้ซ้าย วิธีนี้นับคู่ที่ถูกต้องทั้งหมดได้ในเวลา O(n) แทนที่จะเป็น O(n²)

def count_pairs_less_than(nums, target):
    nums.sort()
    left, right = 0, len(nums) - 1
    count = 0
    while left < right:
        if nums[left] + nums[right] < target:
            count += right - left  # all (left, left+1..right) valid
            left  += 1
        else:
            right -= 1
    return count

print(count_pairs_less_than([1, 2, 3, 4, 5], 6))
# pairs: (1,2)(1,3)(1,4)(2,3)  -> 4

ตรวจสอบความเข้าใจ

ตรวจสอบความเข้าใจแนวคิดโครงสร้างข้อมูลและอัลกอริทึมสำหรับการเตรียมสัมภาษณ์การเขียนโปรแกรมจากบทเรียนนี้

สรุปบทเรียน

ในบทเรียนนี้คุณได้เรียนรู้ว่า ตัวชี้สองตัวจากปลายตรงข้ามช่วยแทนที่การไล่แจกแจงคู่แบบ O(n²) ด้วยการเลื่อนตัวชี้ซ้ายและขวาเข้าหากันแบบ O(n) บนอาร์เรย์ที่เรียงลำดับ การตัดสินใจว่าจะเลื่อนตัวชี้ใดเป็นผลจากคุณสมบัติแบบโมโนโทนิกของโจทย์ — ให้เลื่อนด้านที่กำลังจำกัดความคืบหน้าอยู่ และ โจทย์ผลรวมสามค่า ภาชนะที่บรรจุน้ำได้มากที่สุด การกักเก็บน้ำฝน และการตรวจสอบพาลินโดรม ล้วนลดรูปเหลือแม่แบบพื้นฐานเดียวกัน ต่อไปเราจะศึกษารูปแบบตัวชี้สองตัวแบบช้าและเร็ว

คำถามที่พบบ่อย

บทเรียน “ตัวชี้สองตัว: จากปลายตรงข้าม” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “ตัวชี้สองตัว: จากปลายตรงข้าม” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ 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 ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน

บทเรียน “ตัวชี้สองตัว: จากปลายตรงข้าม” ใช้เวลานานแค่ไหน

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

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

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

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

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