นากกายต้นฉบับกำลังจับก้อนตัวเลขสามก้อนจากรางเรียงลำดับให้สมดุลที่จุดกึ่งกลาง

LeetCode 15 3Sum: sort แล้วบีบสองฝั่งเพื่อคุมชุดซ้ำ

พี่โชว์Engineering

โจทย์ 3Sum ขอชุดสามจำนวนที่รวมเป็นศูนย์โดยห้ามผลลัพธ์ซ้ำครับ ความยากจึงมีสองชั้น: หาให้ครบ และกำจัด duplicate โดยไม่เสียเวลาเกินจำเป็น

วิธีที่ 1: sort + two pointers

เรียง array แล้วตรึงค่าตัวแรก จากนั้นเลื่อน left/right ตามผลรวม หากรวมต่ำไปขยับซ้าย สูงไปขยับขวา และข้ามค่าซ้ำหลังพบคำตอบ

  • เวลา O(n²) หลัง sort O(n log n)
  • memory เพิ่ม O(1) หาก sort in place หรือ O(n) หาก copy เพื่อรักษา input
  • duplicate handling ชัดและเป็น best default

วิธีที่ 2: hash set ต่อ pivot

ตรึงค่าแรกแล้วเดินค่าที่สอง เก็บค่าที่เห็นเพื่อหา complement วิธีนี้ยัง O(n²) และใช้ O(n) memory โดยไม่ต้องเรียง แต่ต้อง canonicalize tuple เพื่อ deduplicate ผลลัพธ์

วิธีที่ 3: brute force เป็น oracle

ลองทุกสาม index ใช้เวลา O(n³) แต่สั้นและเชื่อถือได้สำหรับ array เล็ก จึงมีประโยชน์มากใน property test เปรียบเทียบ implementation เร็ว

import unittest


def three_sum_two_pointers(nums):
    values = sorted(nums)
    result = []
    for index in range(len(values) - 2):
        if index and values[index] == values[index - 1]:
            continue
        if values[index] > 0:
            break
        left, right = index + 1, len(values) - 1
        while left < right:
            total = values[index] + values[left] + values[right]
            if total < 0:
                left += 1
            elif total > 0:
                right -= 1
            else:
                result.append((values[index], values[left], values[right]))
                left += 1
                right -= 1
                while left < right and values[left] == values[left - 1]:
                    left += 1
                while left < right and values[right] == values[right + 1]:
                    right -= 1
    return set(result)


def three_sum_hash(nums):
    result = set()
    for index, first in enumerate(nums):
        seen = set()
        for second in nums[index + 1:]:
            third = -first - second
            if third in seen:
                result.add(tuple(sorted((first, second, third))))
            seen.add(second)
    return result


def three_sum_bruteforce(nums):
    result = set()
    for i in range(len(nums)):
        for j in range(i + 1, len(nums)):
            for k in range(j + 1, len(nums)):
                if nums[i] + nums[j] + nums[k] == 0:
                    result.add(tuple(sorted((nums[i], nums[j], nums[k]))))
    return result


class ThreeSumTests(unittest.TestCase):
    def check(self, nums, expected):
        self.assertEqual(three_sum_two_pointers(nums), expected)
        self.assertEqual(three_sum_hash(nums), expected)
        self.assertEqual(three_sum_bruteforce(nums), expected)

    def test_examples_and_duplicates(self):
        self.check([-1, 0, 1, 2, -1, -4], {(-1, -1, 2), (-1, 0, 1)})
        self.check([0, 0, 0, 0], {(0, 0, 0)})
        self.check([1, 2, -2, -1], set())


if __name__ == "__main__":
    unittest.main()

Decision table

สถานการณ์Sort + two pointersHash per pivotBrute force
input ใหญ่ดีที่สุดทั่วไปใช้ได้แต่ memory เพิ่มไม่เหมาะ
memory จำกัดดีถ้ายอม sort in placeO(n)O(1) แต่ช้า
readabilityduplicate rule ชัดcomplement คุ้นเคยง่ายสุด
input เรียงอยู่แล้วได้เปรียบมากไม่ได้ใช้ลำดับไม่ได้ใช้ลำดับ
ห้ามแก้ inputcopy ก่อน sortผ่านผ่าน
oracle สำหรับ testดีดีดีที่สุดเมื่อ n เล็ก

Best default: sort copy แล้วใช้ two pointers เพราะเวลา O(n²) และควบคุม duplicate โดยไม่พึ่ง result set ขนาดใหญ่

Edge cases

  • จำนวนศูนย์มากกว่าสามตัวต้องคืนชุดเดียว
  • ค่าซ้ำทั้ง pivot และ pointer ต้องข้ามให้ครบ
  • น้อยกว่าสามสมาชิกคืนว่าง
  • ภาษา fixed-width ต้องใช้ชนิดที่รวมสามค่าไม่ overflow

Recap

  • sort ช่วยให้ผลรวมบอกทิศทาง pointer
  • ข้าม duplicate ทั้ง pivot และหลังพบคำตอบ
  • copy ก่อน sort หาก input order เป็นของผู้เรียก
  • ใช้ brute force เป็น oracle กับเคสเล็ก