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