🔢 Algorithms✍️ Khoa📅 19/04/2026☕ 5 phút đọc

Algorithms: Two Pointers

1. Khi nào dùng Two Pointers?

Dấu hiệu nhận biết:

  • Array đã sorted (hoặc có thể sort)
  • Tìm cặp/bộ ba thỏa điều kiện tổng
  • Partition array (Dutch National Flag, Quick Sort)
  • Palindrome check, reverse in-place

Hai dạng chính:

Opposite ends:          Same direction:
[1, 2, 3, 4, 5]        [1, 2, 3, 4, 5]
 L           R           S  F
      →   ←                  →→

2. Opposite Ends Template

Dùng khi: tìm cặp thỏa điều kiện trong sorted array.

func twoSum(nums []int, target int) []int {
    left, right := 0, len(nums)-1
    for left < right {
        sum := nums[left] + nums[right]
        switch {
        case sum == target:
            return []int{left, right}
        case sum < target:
            left++   // cần tổng lớn hơn → tăng left
        default:
            right--  // cần tổng nhỏ hơn → giảm right
        }
    }
    return nil
}

3. Same Direction Template (Fast & Slow)

Dùng cho: remove duplicates, move zeros, cycle detection.

// Remove duplicates in-place từ sorted array
func removeDuplicates(nums []int) int {
    slow := 0
    for fast := 1; fast < len(nums); fast++ {
        if nums[fast] != nums[slow] {
            slow++
            nums[slow] = nums[fast]
        }
    }
    return slow + 1
}

4. Bài kinh điển

3Sum

Tìm tất cả bộ ba a + b + c = 0.

Sort trước: [-4, -1, -1, 0, 1, 2]
Fix a=-4: two pointers tìm b+c=4
Fix a=-1: two pointers tìm b+c=1
...
func threeSum(nums []int) [][]int {
    sort.Ints(nums)
    result := [][]int{}

    for i := 0; i < len(nums)-2; i++ {
        // Skip duplicate cho i
        if i > 0 && nums[i] == nums[i-1] {
            continue
        }
        left, right := i+1, len(nums)-1
        for left < right {
            sum := nums[i] + nums[left] + nums[right]
            if sum == 0 {
                result = append(result, []int{nums[i], nums[left], nums[right]})
                // Skip duplicates
                for left < right && nums[left] == nums[left+1] {
                    left++
                }
                for left < right && nums[right] == nums[right-1] {
                    right--
                }
                left++
                right--
            } else if sum < 0 {
                left++
            } else {
                right--
            }
        }
    }
    return result
}

Complexity: O(n²) — fix 1 pointer O(n), two pointer O(n)

Container With Most Water

[1, 8, 6, 2, 5, 4, 8, 3, 7]
 L                         R
area = min(height[L], height[R]) × (R - L)
→ move pointer có height nhỏ hơn
func maxArea(height []int) int {
    left, right := 0, len(height)-1
    maxWater := 0
    for left < right {
        h := min(height[left], height[right])
        water := h * (right - left)
        if water > maxWater {
            maxWater = water
        }
        if height[left] < height[right] {
            left++
        } else {
            right--
        }
    }
    return maxWater
}

Tại sao move pointer nhỏ hơn? Nếu move pointer lớn hơn, width giảm mà height không thể tăng (bị giới hạn bởi pointer nhỏ) → area chắc chắn giảm.

Trapping Rain Water

elevation: [0,1,0,2,1,0,1,3,2,1,2,1]
water:         1   1 2 1   1   1
               
Tại mỗi position i:
water[i] = min(maxLeft[i], maxRight[i]) - height[i]
func trap(height []int) int {
    left, right := 0, len(height)-1
    maxLeft, maxRight := 0, 0
    total := 0

    for left < right {
        if height[left] < height[right] {
            if height[left] >= maxLeft {
                maxLeft = height[left]
            } else {
                total += maxLeft - height[left]
            }
            left++
        } else {
            if height[right] >= maxRight {
                maxRight = height[right]
            } else {
                total += maxRight - height[right]
            }
            right--
        }
    }
    return total
}

Logic: Nếu height[left] < height[right], thì maxRight ít nhất bằng height[right] → water tại left chỉ phụ thuộc maxLeft.

Dutch National Flag (3-way partition)

Sort array chỉ có 3 giá trị [0, 1, 2] in-place.

[2, 0, 2, 1, 1, 0]
 lo=0, mid=0, hi=5

[0, 0, 1, 1, 2, 2]
func sortColors(nums []int) {
    lo, mid, hi := 0, 0, len(nums)-1
    for mid <= hi {
        switch nums[mid] {
        case 0:
            nums[lo], nums[mid] = nums[mid], nums[lo]
            lo++
            mid++
        case 1:
            mid++
        case 2:
            nums[mid], nums[hi] = nums[hi], nums[mid]
            hi-- // không tăng mid vì nums[hi] chưa được check
        }
    }
}

Linked List Cycle Detection (Floyd's)

Slow: 1 step/turn
Fast: 2 steps/turn
→ Nếu có cycle, fast sẽ gặp slow trong cycle
func hasCycle(head *ListNode) bool {
    slow, fast := head, head
    for fast != nil && fast.Next != nil {
        slow = slow.Next
        fast = fast.Next.Next
        if slow == fast {
            return true
        }
    }
    return false
}

5. Common mistakes

Lỗi Fix
Quên sort trước Two pointers chỉ work trên sorted array (opposite ends)
Không skip duplicate Thêm while left < right && nums[left] == nums[left+1]
Loop condition sai left < right không phải left <= right (tránh tính 1 element 2 lần)
Move cả 2 pointer khi tìm thấy Phải move cả 2 và skip duplicate

💡 Interview: Two pointers thường reduce O(n²) xuống O(n) hoặc O(n log n) (nếu cần sort). Khi thấy "find pair/triplet in sorted array", nghĩ two pointers ngay.