Notes & software courses · Free to learn
Aph's Blog
On this page

ข้อ 10 · LC283 Move Zeroes 🟢

👋 อ่านฟรีทั้งหมดบน Aph's Blog — เนื้อหาภาษาไทย ทำตามทีละหน้าใน sidebar ได้เลย หากมีข้อเสนอแนะหรืออยากให้เพิ่มหัวข้อไหน บอกได้เสมอ

ดัน 0 ทุกตัวไปท้ายแถว โดยห้ามสร้างแถวใหม่ และตัวที่ไม่ใช่ 0 ต้องเรียงลำดับเดิม — ความลับของข้อนี้คือ อย่าไปขยับ 0 เลย ให้ขยับตัวที่ไม่ใช่ 0 แล้ว 0 จะไปกองท้ายเอง

ให้ array (ลิสต์) ตัวเลข nums จงย้ายเลข 0 ทุกตัวไปอยู่ท้ายแถว โดยมีเงื่อนไขสองข้อ: ตัวที่ไม่ใช่ 0 ต้องยังเรียงตามลำดับเดิม และต้องแก้ลงในแถวเดิม ห้ามสร้างแถวใหม่มาแทน

Example 1
Input:
nums = [0,1,0,3,12]
Output:
[1,3,12,0,0]
Explanation:
ตัวที่ไม่ใช่ 0 คือ 1, 3, 12 ยังเรียงลำดับเดิม แล้วต่อท้ายด้วย 0 สองตัว จำนวนช่องเท่าเดิมคือ 5 ช่อง
Example 2
Input:
nums = [0]
Output:
[0]
Explanation:
มีตัวเดียวและเป็น 0 อยู่แล้ว ไม่มีอะไรต้องทำ
Example 3
Input:
nums = [1,2,3]
Output:
[1,2,3]
Explanation:
ไม่มี 0 เลย โค้ดต้องไม่พังและไม่ทำอะไรเสียหาย
Example 4
Input:
nums = [0,0,1]
Output:
[1,0,0]
Explanation:
เคสนี้จำไว้ให้ดี เดี๋ยวจะใช้จับบั๊กที่คนพลาดกันมากที่สุด
Constraints (ข้อจำกัด)
  • 1 <= nums.length <= 10^4
  • -2^31 <= nums[i] <= 2^31 - 1
  • ต้องแก้ใน array เดิม (in-place) ห้าม return array ใหม่
  • ท้าทาย: ทำให้จำนวนครั้งที่เขียนลง array น้อยที่สุด
  • ค่าติดลบมีได้ → เงื่อนไขที่ต้องเช็คคือ "ไม่ใช่ 0" ไม่ใช่ "มากกว่า 0"
โจทย์นี้ถามอะไรจริง ๆ

อ่านให้ชัดว่ามันไม่ได้บอกให้ "ลบ 0 ออก" แต่บอกให้ "ดัน 0 ไปท้าย" — จำนวนช่องในแถวต้องเท่าเดิมตลอด และคำว่า in-place กับ คงลำดับเดิม คือสองกฎที่ตัดวิธีง่าย ๆ หลายวิธีออกไป ถ้าไม่มีสองกฎนี้ ข้อนี้จะกลายเป็นโจทย์ระดับหนึ่งบรรทัด

พื้นฐานที่ต้องมีก่อน 4 อย่าง

สี่หัวข้อนี้คือของที่ต้องรู้ก่อนเริ่มเขียน ไม่ใช่ของที่ควรไปรู้ทีหลัง แต่ละอันมีโค้ดให้รันดูจริง เพราะทั้งสี่อย่างเป็นเรื่องที่อ่านคำอธิบายแล้วคิดว่าเข้าใจ แต่พอเขียนโค้ดจริงจะพลาด

พื้นฐานที่ 1 — in-place หมายถึงอะไร (และห้ามพลาดตรงนี้ใน Python)

in-place แปลว่าแก้ข้อมูลในที่เดิม ไม่สร้างที่เก็บใหม่ที่โตตามขนาดข้อมูล จุดที่ต้องเข้าใจให้ชัด: การมีตัวแปรเพิ่มไม่กี่ตัว (เช่นตัวนับ ตัวชี้ตำแหน่ง) ยังนับว่า in-place อยู่ เพราะจำนวนตัวแปรไม่โตตามความยาวแถว ที่ห้ามคือการสร้าง list ใหม่ทั้งก้อน

และมีกับดักของ Python ที่ทำให้โค้ดดูเหมือนทำงานแต่จริง ๆ ไม่มีอะไรเปลี่ยนเลย

nums = ... กับ nums[:] = ... ต่างกันคนละเรื่องpython
def wrong(nums):
    nums = [x for x in nums if x != 0] + [0] * nums.count(0)   # สร้างลิสต์ใหม่แล้วตั้งชื่อทับ


def right(nums):
    kept = [x for x in nums if x != 0]
    nums[:] = kept + [0] * (len(nums) - len(kept))             # เขียนทับ "ข้างใน" ลิสต์เดิม


a = [0, 1, 0, 3, 12]
wrong(a)
print("หลังเรียก wrong :", a, "<- ไม่มีอะไรเปลี่ยนเลย")

b = [0, 1, 0, 3, 12]
right(b)
print("หลังเรียก right :", b, "<- เปลี่ยนจริง")
Output
หลังเรียก wrong : [0, 1, 0, 3, 12] <- ไม่มีอะไรเปลี่ยนเลย
หลังเรียก right : [1, 3, 12, 0, 0] <- เปลี่ยนจริง

เหตุผล: บรรทัด nums = ... ไม่ได้แก้ลิสต์ มันแค่เอาชื่อ nums ไปแปะกับลิสต์ก้อนใหม่ ส่วนลิสต์เดิมของคนที่เรียกฟังก์ชันยังอยู่เหมือนเดิมไม่ถูกแตะเลย ตรงข้ามกับ nums[:] = ... ที่สั่งว่า "เอาของชุดนี้ไปเขียนทับทุกช่องของลิสต์เดิม" ซึ่งแก้ก้อนเดิมจริง

แต่ nums[:] = ... ก็ยังผิดกติกาข้อนี้

โค้ดฝั่ง right ข้างบนตอบถูกและผ่านระบบตรวจของ LeetCode ได้ แต่มันสร้างลิสต์ kept ขึ้นมาก่อน ซึ่งใหญ่ตามความยาวข้อมูล จึงไม่ใช่ O(1) space ตามที่โจทย์ต้องการ ยกมาให้ดูเพื่อสอนความต่างของ nums = กับ nums[:] = เท่านั้น ของจริงที่เราจะเขียนคือแก้ทีละช่องด้วย nums[i] = ...

พื้นฐานที่ 2 — ทำไมห้ามใช้ pop(0) หรือ remove(0)

ความคิดแรกของเกือบทุกคนคือ "เจอ 0 ก็ลบทิ้ง แล้วไปต่อท้ายใหม่" ฟังดูตรงไปตรงมาที่สุด แต่วิธีนี้ช้ามากด้วยเหตุผลที่มองไม่เห็นจากโค้ด: การลบของกลางลิสต์ ต้องเลื่อนของที่อยู่ข้างหลังมันทั้งหมด มาข้างหน้าหนึ่งช่อง

ต้นทุนที่ซ่อนอยู่ของการลบของออกจากลิสต์python
nums = [10, 20, 30, 40, 50]
print("ก่อน :", nums)
nums.pop(0)                      # เอาช่องแรกออก
print("หลัง pop(0) :", nums, "<- ทุกตัวเลื่อนซ้าย 1 ช่อง และลิสต์สั้นลง")

# นับว่าการเลื่อนเกิดขึ้นกี่ครั้งจริง ๆ ถ้าลบ 0 ทีละตัวแบบนี้
def count_shifts(n_zeros, n_total):
    shifts = 0
    length = n_total
    for _ in range(n_zeros):
        shifts += length - 1     # ลบหนึ่งตัว = เลื่อนของที่เหลือทั้งหมด
        length -= 1
    return shifts

print("แถวยาว 100 มี 0 อยู่ 50 ตัว -> เลื่อนของทั้งหมด", count_shifts(50, 100), "ครั้ง")
print("แถวยาว 1000 มี 0 อยู่ 500 ตัว -> เลื่อน", count_shifts(500, 1000), "ครั้ง")
print("สังเกต: แถวยาวขึ้น 10 เท่า แต่งานเพิ่มขึ้นประมาณ 100 เท่า")
Output
ก่อน : [10, 20, 30, 40, 50]
หลัง pop(0) : [20, 30, 40, 50] <- ทุกตัวเลื่อนซ้าย 1 ช่อง และลิสต์สั้นลง
แถวยาว 100 มี 0 อยู่ 50 ตัว -> เลื่อนของทั้งหมด 3725 ครั้ง
แถวยาว 1000 มี 0 อยู่ 500 ตัว -> เลื่อน 374750 ครั้ง
สังเกต: แถวยาวขึ้น 10 เท่า แต่งานเพิ่มขึ้นประมาณ 100 เท่า

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

กฎที่ใช้ได้ตลอดชีวิตการเขียนโปรแกรม

การลบหรือแทรกของ กลาง ลิสต์แพงเสมอ เพราะต้องเลื่อนของที่เหลือ ส่วนการ อ่านหรือเขียนทับ ช่องเดิม (nums[i] = ...) ถูกและเร็วเสมอ เพราะไม่ต้องขยับใคร โจทย์แนวจัดของใน array แทบทุกข้อจึงแก้ด้วยการเขียนทับช่อง ไม่ใช่การลบและแทรก

พื้นฐานที่ 3 — การสลับค่าสองตัวในบรรทัดเดียว

Python สลับค่าตัวแปรได้ในบรรทัดเดียวด้วย a, b = b, a เหตุผลที่ทำได้คือ Python คำนวณ ฝั่งขวาให้เสร็จทั้งหมดก่อน (ได้เป็นคู่ค่า) แล้วจึงค่อยจ่ายไปให้ฝั่งซ้าย จึงไม่ต้องมีตัวแปรพักเหมือนภาษาอื่น

ทำไมสลับสองบรรทัดไม่ได้python
a, b = 10, 20
print("ก่อน  : a =", a, ", b =", b)
a, b = b, a                     # Python คำนวณฝั่งขวาให้เสร็จก่อน แล้วค่อยจ่ายให้ฝั่งซ้าย
print("หลัง  : a =", a, ", b =", b)

print("--- ถ้าแยกเป็นสองบรรทัดจะพัง")
x, y = 10, 20
x = y                           # x กลายเป็น 20 ทับค่า 10 ทิ้งไปแล้ว
y = x                           # y ได้ 20 กลับมา ไม่ใช่ 10
print("x =", x, ", y =", y, "<- ค่า 10 หายไปเลย ไม่ได้สลับ")
Output
ก่อน  : a = 10 , b = 20
หลัง  : a = 20 , b = 10
--- ถ้าแยกเป็นสองบรรทัดจะพัง
x = 20 , y = 20 <- ค่า 10 หายไปเลย ไม่ได้สลับ

เรื่องนี้ใช้กับช่องใน array ได้เหมือนกัน คือ nums[i], nums[j] = nums[j], nums[i] และเราจะใช้มันในเฉลยของข้อนี้

พื้นฐานที่ 4 — เช็ค != 0 ไม่ใช่ > 0

constraint บอกว่าค่าในแถวติดลบได้ ถ้าเผลอเขียนเงื่อนไขว่า "มากกว่า 0" เลขติดลบจะถูกเหมาโดนดันไปท้ายเหมือนเป็น 0 ด้วย ซึ่งเป็นบั๊กที่ตัวอย่างในโจทย์จับไม่ได้เพราะตัวอย่างไม่มีเลขติดลบเลย

เลขติดลบเปิดโปงบั๊กนี้python
def wrong_gt(nums):
    insert = 0
    for i in range(len(nums)):
        if nums[i] > 0:            # ผิด: เลขติดลบถูกมองว่าเป็นศูนย์
            nums[insert], nums[i] = nums[i], nums[insert]
            insert += 1
    return nums


def right_ne(nums):
    insert = 0
    for i in range(len(nums)):
        if nums[i] != 0:
            nums[insert], nums[i] = nums[i], nums[insert]
            insert += 1
    return nums


print("แบบ > 0  :", wrong_gt([-1, 0, -2, 3]), "<- เลขติดลบถูกดันไปท้าย และ 0 ค้างกลางแถว")
print("แบบ != 0 :", right_ne([-1, 0, -2, 3]), "<- ถูกต้อง")
Output
แบบ > 0  : [3, 0, -2, -1] <- เลขติดลบถูกดันไปท้าย และ 0 ค้างกลางแถว
แบบ != 0 : [-1, -2, 3, 0] <- ถูกต้อง
นิสัยที่ควรติดตัว

ทุกครั้งที่เขียนเงื่อนไข ให้ย้อนไปอ่าน constraint ว่าค่าที่เป็นไปได้มีอะไร แล้วลองแทนค่าสุดขั้วเข้าไป (ติดลบ, ศูนย์, ตัวเดียว, ยาวสุด) ตัวอย่างในโจทย์ถูกคัดมาให้เข้าใจโจทย์ ไม่ได้คัดมาให้จับบั๊ก

ลองแบบตรงไปตรงมาก่อน: สร้างแถวใหม่

วิธีที่คิดออกง่ายสุดคือเก็บตัวที่ไม่ใช่ 0 ไปไว้แถวใหม่ แล้วต่อท้ายด้วย 0 ให้ครบจำนวน วิธีนี้ ตอบถูก และเข้าใจง่ายที่สุด ให้เขียนให้ได้ก่อนเสมอ เพราะมันคือหลักฐานว่าเราเข้าใจโจทย์แล้ว

python
def build_new(nums):
    kept = [x for x in nums if x != 0]          # เก็บตัวที่ไม่ใช่ 0 ตามลำดับเดิม
    zeros = len(nums) - len(kept)
    return kept + [0] * zeros                   # ต่อท้ายด้วย 0 ให้ครบ

print(build_new([0, 1, 0, 3, 12]))   # [1, 3, 12, 0, 0]

แต่วิธีนี้ผิดกติกาข้อ in-place เพราะลิสต์ kept โตตามจำนวนข้อมูล ถ้าแถวยาวหนึ่งหมื่น kept ก็อาจยาวหนึ่งหมื่นตาม เราจึงต้องหาวิธีทำงานเดียวกันนี้โดยไม่สร้างที่เก็บใหม่ ทั้งหน้าที่เหลือคือการไล่จากวิธีนี้ไปหาวิธีที่ไม่กินพื้นที่เพิ่ม

ความคิดที่ดูถูกแต่ผิด: เจอ 0 แล้วเลื่อนของหลังมาข้างหน้า

นี่คือความคิดที่คนคิดออกเป็นอันดับสองและเป็นกับดักที่ลึกที่สุดของข้อนี้ ไอเดียคือ "เจอ 0 ที่ช่องไหน ก็เลื่อนของที่อยู่หลังมันมาทับ แล้วเอา 0 ไปวางท้ายสุด" ฟังดูสมเหตุสมผลทุกคำ และมันจะ ผ่านตัวอย่างที่ 1 ของโจทย์ ด้วย ทำให้คนเชื่อว่าถูกแล้ว

รันดูว่าพังตอนไหนpython
def shift_left(nums):
    """ความคิดตรงไปตรงมา: เจอ 0 ที่ไหน ก็เลื่อนของหลังมันมาข้างหน้า แล้วยัด 0 ไว้ท้าย"""
    for i in range(len(nums)):
        if nums[i] == 0:
            for j in range(i, len(nums) - 1):
                nums[j] = nums[j + 1]      # เลื่อนของหลังมาทับ
            nums[-1] = 0                   # เอา 0 ไปไว้ท้ายสุด
    return nums


print(shift_left([0, 1, 0, 3, 12]), "<- เคสนี้ดูเหมือนถูก")
print(shift_left([0, 0, 1]), "<- ควรได้ [1, 0, 0]  แต่ได้ผิด!")
print(shift_left([0, 0, 0, 1]), "<- ควรได้ [1, 0, 0, 0]")
Output
[1, 3, 12, 0, 0] <- เคสนี้ดูเหมือนถูก
[0, 1, 0] <- ควรได้ [1, 0, 0]  แต่ได้ผิด!
[0, 1, 0, 0] <- ควรได้ [1, 0, 0, 0]

สาเหตุที่พัง: เมื่อเลื่อนของมาทับช่องที่ i แล้ว ช่องที่ i อาจกลายเป็น 0 ตัวใหม่ (เพราะตัวที่เลื่อนมาเป็น 0) แต่ลูป for พาเราไปช่อง i+1 แล้ว จึงข้าม 0 ตัวนั้นไปเลยไม่มีวันย้อนกลับมาดู เคส [0,0,1] จึงเหลือ 0 ค้างอยู่หน้าแถว

แล้วถ้าแก้ด้วยการเขียน i -= 1 เพื่อให้ย้อนกลับมาดูช่องเดิมอีกครั้ง จะเจอปัญหาใหม่คือ for loop ของ Python จะไม่สนใจการแก้ค่า i ข้างในลูปเลย (เรื่องนี้อธิบายไว้แล้วในหน้า "เลือก for หรือ while ให้ถูก" ของคอร์สพื้นฐาน) และถ้าเปลี่ยนไปใช้ while แล้วเขียน i -= 1 จริง โปรแกรมจะวนไม่จบเพราะ i เด้งไปกลับที่เดิมตลอด

บทเรียนที่ใหญ่กว่าตัวโจทย์

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

จุดที่ควรสังเกตอีกอย่าง: วิธีเลื่อนนี้ต้องเลื่อนของทั้งแถวทุกครั้งที่เจอ 0 ซึ่งคือปัญหาความช้าเดียวกับ pop(0) ในพื้นฐานที่ 2 เป๊ะ ๆ นั่นเป็นสัญญาณว่าเรากำลังคิดผิดทิศ ทางที่ถูกต้องไม่ควรมีการเลื่อนของทั้งแถวเลย

แนวคิดหลัก: อย่าไปขยับ 0

นี่คือประโยคเดียวที่พลิกข้อนี้จากยากเป็นง่าย: เลิกคิดเรื่องย้าย 0 ไปท้าย ให้คิดแค่ว่า เอาตัวที่ไม่ใช่ 0 ไปเรียงชิดซ้ายให้หมด แล้ว 0 จะไปกองท้ายเองโดยที่เราไม่ต้องสั่งเลย

เปรียบเทียบกับการจัดชั้นหนังสือที่มีช่องว่างแทรกอยู่: เราไม่ต้องเก็บช่องว่างไปไว้ทางขวา เราแค่เลื่อนหนังสือทุกเล่มไปชิดซ้ายเท่าที่ชิดได้ ช่องว่างจะไปรวมกันอยู่ทางขวาเองเป็นผลพลอยได้

แปลเป็นโค้ดต้องมีตัวแปรสองตัวคนละหน้าที่ ตรงนี้คือจุดที่คนติดกันมากที่สุด เพราะเคยชินกับการมี index ตัวเดียวในลูป

ตัวแปรหน้าที่ขยับเมื่อไหร่
i (ตัวอ่าน)ไล่ดูทุกช่องตั้งแต่ต้นจนจบ เพื่อหาตัวที่ไม่ใช่ 0ทุกรอบของลูป — ไม่มีเงื่อนไข
insert (ตัวเขียน)ชี้ช่องว่างถัดไปที่จะวางตัวไม่ใช่ 0 ลงไปเฉพาะตอนที่วางของลงไปจริง ๆ
ชื่อที่ต่างกันแต่เป็นเรื่องเดียวกัน

ตัวแปรคู่นี้มีชื่อเรียกหลายแบบตามแต่ละแหล่ง: slow กับ fast, read กับ write, insert กับ i ทั้งหมดคือของอย่างเดียวกัน คือ ตัวหนึ่งอ่านตามจังหวะของข้อมูล อีกตัวเขียนตามจังหวะของผลลัพธ์ ถ้าเคยอ่านข้อ 9 String Compression มาแล้วจะจำได้ว่าเป็นท่าเดียวกันเป๊ะ

ทางที่ 1 — เขียนทับ แล้วเติม 0 (สองรอบ)

ทางแรกทำตรงตามแนวคิดเลย: รอบแรกเอาตัวที่ไม่ใช่ 0 เขียนทับลงช่องต้น ๆ ตามลำดับ รอบสองช่องที่เหลือเติม 0 ให้ครบ ลองรันดูโดยให้มันพิมพ์สภาพแถวหลังจบรอบแรกให้เห็น

สภาพแถวระหว่างทาง — ตรงนี้สำคัญมากpython
nums = [0, 1, 0, 3, 12]
insert = 0

print("รอบที่ 1 — ดันตัวที่ไม่ใช่ศูนย์มาชิดซ้าย")
for x in nums:
    if x != 0:
        nums[insert] = x
        insert += 1
        print("   จด", x, "ลงช่อง", insert - 1, "-> nums =", nums)

print("สภาพแถวหลังจบรอบที่ 1 :", nums, " <- ท้ายแถวเป็นขยะ (3, 12 ค้างอยู่)")
print("จำนวนตัวที่ไม่ใช่ศูนย์ =", insert)

print("รอบที่ 2 — ช่องที่เหลือเติม 0")
for j in range(insert, len(nums)):
    nums[j] = 0
print("ผลลัพธ์สุดท้าย :", nums)
Output
รอบที่ 1 — ดันตัวที่ไม่ใช่ศูนย์มาชิดซ้าย
   จด 1 ลงช่อง 0 -> nums = [1, 1, 0, 3, 12]
   จด 3 ลงช่อง 1 -> nums = [1, 3, 0, 3, 12]
   จด 12 ลงช่อง 2 -> nums = [1, 3, 12, 3, 12]
สภาพแถวหลังจบรอบที่ 1 : [1, 3, 12, 3, 12]  <- ท้ายแถวเป็นขยะ (3, 12 ค้างอยู่)
จำนวนตัวที่ไม่ใช่ศูนย์ = 3
รอบที่ 2 — ช่องที่เหลือเติม 0
ผลลัพธ์สุดท้าย : [1, 3, 12, 0, 0]

บรรทัด [1, 3, 12, 3, 12] คือสิ่งที่ต้องเห็นให้ได้ คนที่ไม่เคยเห็นสภาพนี้แล้วไปลอง print กลางลูปเองจะคิดว่าโค้ดพัง เพราะเลข 3 กับ 12 โผล่สองที่ ความจริงคือของเดิมที่ยังไม่ถูกทับ ซึ่งเป็นเรื่องปกติของการแก้ในที่เดิม รอบที่สองจะไปกลบมันด้วย 0 ทีหลัง

และคำถามที่ควรสงสัยตรงนี้คือ: บรรทัด nums[insert] = x เขียนทับข้อมูลที่ยังกวาดไม่จบ มันจะไปทับตัวที่ยังไม่ได้อ่านไหม คำตอบอยู่ในหัวข้อถัดไป

ทางที่ 2 — สลับที่ (รอบเดียว) และเหตุผลว่าทำไมปลอดภัย

ทางที่สองรวมสองรอบให้เป็นรอบเดียว โดยเปลี่ยนจาก "เขียนทับ" เป็น "สลับที่" ข้อดีคือไม่มีขยะค้างท้ายแถว เพราะการสลับไม่ทำให้ของหายไปไหน มันแค่ย้ายที่ — 0 ที่ถูกเตะออกจากช่องหน้าก็ไปโผล่ที่ช่องหลังพอดี เท่ากับได้การเติม 0 ท้ายแถวมาฟรี ๆ

ตลอดการทำงาน แถวถูกแบ่งเป็น 3 โซนแบบนี้เสมอ:

  [ 1 , 3 | 0 , 0 | 12 , ... ]
    ↑        ↑       ↑
    0        insert  i

  ช่วง [0, insert)   = ตัวไม่ใช่ 0 ที่จัดเรียบร้อยแล้ว เรียงตามลำดับเดิม
  ช่วง [insert, i)   = กอง 0 ที่รอโดนเตะไปท้าย
  ช่วง [i, จบ)       = ส่วนที่ยังไม่ได้อ่าน

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

ทำไมการสลับหนึ่งครั้งไม่ทำลาย invariant นี้: ตอนที่เจอ nums[i] ที่ไม่ใช่ 0 ช่องที่ตำแหน่ง insert ต้องเป็น 0 อยู่แล้วแน่นอน เพราะมันอยู่ในโซนกลางซึ่งเป็น 0 ล้วนตามบรรทัดที่สอง การสลับจึงเท่ากับเอา "0 ตัวหน้าสุดของโซนกลาง" ไปแลกกับ "ตัวไม่ใช่ 0 ตัวใหม่" พอดี ผลคือโซนซ้ายได้สมาชิกเพิ่มต่อท้าย และโซนกลางยังเป็น 0 ล้วนเหมือนเดิม ไม่มีตัวไหนหายและไม่มีลำดับไหนสลับ

ส่วนคำถามของทางที่ 1 ว่า "เขียนทับแล้วข้อมูลหายไหม" ก็ตอบด้วยข้อเท็จจริงเดียวกัน: insert ไม่เคยแซง i เพราะ insert ขยับเฉพาะตอนที่ i เจอตัวไม่ใช่ 0 ซึ่งเท่ากับ i ขยับไปแล้วอย่างน้อยเท่ากัน ดังนั้นช่องที่เราเขียนลงไปเป็นช่องที่ i อ่านผ่านไปแล้วเสมอ ไม่มีข้อมูลที่ยังต้องใช้หายไปเลย

และเหตุผลที่ลำดับเดิมไม่เพี้ยน: เราเอาตัวไม่ใช่ 0 ต่อท้าย โซนซ้ายเสมอ ตามลำดับที่ i เดินเจอ ซึ่ง i เดินจากซ้ายไปขวาไม่ย้อน ตัวที่อยู่ซ้ายกว่าในแถวเดิมจึงถูกจัดเข้าโซนซ้ายก่อนเสมอ

แล้วกรณี i == insert ล่ะ

ถ้ายังไม่เคยเจอ 0 มาก่อนเลย โซนกลางจะว่าง ทำให้ i กับ insert ชี้ช่องเดียวกัน การสลับตอนนั้นคือสลับตัวมันเองกับตัวมันเอง ค่าไม่เปลี่ยน invariant ยังจริง เราจึงไม่ต้องเขียน if แยกกรณีนี้ให้โค้ดยาวขึ้น — แต่ถ้าสนใจเรื่องจำนวนครั้งที่เขียนลงแถว กรณีนี้มีเรื่องต้องคุยต่อในหัวข้อ challenge ด้านล่าง

ไล่ทีละสเต็ปด้วยมือ (dry run)

ไล่ nums = [0, 1, 0, 3, 12] แบบสลับที่ ให้ดูคอลัมน์สุดท้ายเป็นหลัก เพราะมันคือ invariant ที่เพิ่งพิสูจน์ไป

inums[i]ไม่ใช่ 0?ทำอะไรnums หลังรอบนี้insertจัดแล้ว | รอโดนเตะ | ยังไม่ดู
00ไม่ปล่อยผ่าน[0, 1, 0, 3, 12]0(ว่าง) | 0 | 1,0,3,12
11ใช่สลับช่อง 0 กับ 1[1, 0, 0, 3, 12]11 | 0 | 0,3,12
20ไม่ปล่อยผ่าน[1, 0, 0, 3, 12]11 | 0,0 | 3,12
33ใช่สลับช่อง 1 กับ 3[1, 3, 0, 0, 12]21,3 | 0,0 | 12
412ใช่สลับช่อง 2 กับ 4[1, 3, 12, 0, 0]31,3,12 | 0,0 | (ว่าง)

สามอย่างที่ควรอ่านออกจากตารางนี้: หนึ่ง โซนกลางเป็น 0 ล้วนทุกแถวไม่มีข้อยกเว้น สอง 1, 3, 12 เข้าโซนซ้ายตามลำดับที่เจอ จึงคงลำดับเดิม สาม insert จบที่ 3 ซึ่งเท่ากับจำนวนตัวที่ไม่ใช่ 0 พอดี — เราไม่เคยสั่งย้าย 0 เลยแม้แต่ครั้งเดียว มันไปกองท้ายเองจากการถูกสลับออกไปทีละตัว

กับดักที่คนพลาดบ่อยที่สุด

กับดักที่ 1 — ใช้ตัวชี้หัวกับท้ายเดินเข้าหากัน

คนที่เพิ่งอ่านหน้าแนวคิด Two Pointers มาแล้วมักหยิบแบบ opposite ends มาใช้ทันที คือเจอ 0 ที่หน้าก็เอาตัวท้ายมาถม วิธีนี้เร็วกว่าและเขียนของน้อยกว่าด้วย แต่มันทำลายลำดับเดิม ซึ่งโจทย์ห้ามไว้

เร็วกว่าจริง แต่ตอบผิดpython
def opposite_ends(nums):
    """เอาตัวท้ายมาถมช่องที่เป็น 0 — เร็ว แต่ทำลายลำดับเดิม"""
    left, n = 0, len(nums)
    while left < n:
        if nums[left] == 0:
            n -= 1
            nums[left], nums[n] = nums[n], nums[left]
        else:
            left += 1
    return nums


print(opposite_ends([0, 1, 0, 3, 12]), "<- ควรได้ [1, 3, 12, 0, 0]")
print("ตัวที่ไม่ใช่ศูนย์เรียงผิดลำดับเดิมไปแล้ว")
Output
[12, 1, 3, 0, 0] <- ควรได้ [1, 3, 12, 0, 0]
ตัวที่ไม่ใช่ศูนย์เรียงผิดลำดับเดิมไปแล้ว

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

กับดักที่ 2 — ลืมรอบที่สอง

ถ้าเลือกทางที่ 1 (เขียนทับ) แล้วลืมลูปเติม 0 คำตอบจะเป็น [1, 3, 12, 3, 12] คือมีขยะค้างท้ายแถว บั๊กนี้หาง่ายถ้าเคยเห็นสภาพระหว่างทางมาก่อน ซึ่งเป็นเหตุผลที่หน้านี้ยกมันมาโชว์ตั้งแต่ต้น

กับดักที่ 3 — สับสนกับโจทย์พี่น้อง

ข้อ 26 Remove Duplicates from Sorted Array ใช้โครงเดียวกันแต่ความหมายของตัวเขียนต่างกันหนึ่งช่อง: ที่ข้อนี้ insert หมายถึง "ช่องว่างถัดไปที่จะวาง" แต่ที่ LC26 ตัวเขียนหมายถึง "ช่องของตัวที่เก็บล่าสุด" คนที่ทำข้อนี้ได้แล้วไปทำ LC26 ต่อจึงมักพลาดเรื่อง off-by-one จำไว้ว่าต้องถามตัวเองทุกครั้งว่าตัวเขียน "ชี้ช่องว่าง" หรือ "ชี้ตัวล่าสุด"

ลองเองก่อน 10–15 นาที

python
def move_zeroes(nums: list[int]) -> None:
    """แก้ nums ในที่เดิม ไม่ต้อง return อะไร"""
    # เขียนโค้ดของคุณที่นี่
    pass


cases = [
    ([0, 1, 0, 3, 12], [1, 3, 12, 0, 0]),
    ([0], [0]),
    ([1, 2, 3], [1, 2, 3]),
    ([0, 0, 0], [0, 0, 0]),
    ([0, 0, 1], [1, 0, 0]),
    ([-1, 0, -2, 3], [-1, -2, 3, 0]),
]
for nums, expected in cases:
    got = nums[:]
    move_zeroes(got)
    print(got == expected, got)
💡 ใบ้ขั้น 1 — ตอบสามคำถามนี้ในหัวก่อน
  1. ถ้าเขียนคำตอบลงแถวใหม่ได้ คุณจะไล่เก็บอะไรก่อน — 0 หรือตัวที่ไม่ใช่ 0
  2. ถ้าห้ามสร้างแถวใหม่ แต่ต้องเขียนคำตอบทับลงแถวเดิม คุณต้องรู้อะไรเพิ่มอีกหนึ่งอย่าง
  3. ตัวแปรที่จำสิ่งนั้น เป็นตัวเดียวกับ index ของลูปที่กำลังไล่อ่านอยู่หรือเปล่า

ถ้าตอบข้อ 3 ว่า "ไม่ใช่ตัวเดียวกัน" คุณเจอไอเดียหลักของข้อนี้แล้ว เหลือแค่แปลเป็นโค้ด

💡 ใบ้ขั้น 2 — โครงโค้ดที่เติมให้เกือบครบ
python
insert = 0                          # ช่องว่างถัดไปที่จะวางตัวไม่ใช่ 0
for i in range(len(nums)):          # i ไล่อ่านทุกช่อง
    if ______________:              # (1) nums[i] น่าเก็บไหม
        ______________              # (2) เอา nums[i] ไปไว้ช่อง insert
        insert += 1                 # โซนจัดแล้วโตขึ้นหนึ่งช่อง

# ช่อง (2) ทำได้สองแบบ เลือกอันไหนก็ได้:
#   แบบสลับ     : nums[insert], nums[i] = nums[i], nums[insert]   (จบในรอบเดียว)
#   แบบเขียนทับ : nums[insert] = nums[i]   แล้วต้องมีลูปเติม 0 ต่อท้ายอีกรอบ
อย่าทำสิ่งนี้

อย่าใช้ nums.pop(), nums.remove(), หรือ nums.insert() เพราะทั้งสามอย่างต้องเลื่อนของทั้งแถวและเปลี่ยนความยาวแถว (เหตุผลอยู่ในพื้นฐานที่ 2) และอย่าเขียน nums = ... เพราะจะไม่มีอะไรเปลี่ยนเลย (พื้นฐานที่ 1)

💡 ใบ้ขั้น 3 — เช็คลิสต์ก่อนกดส่ง
  • [0, 0, 1] ต้องได้ [1, 0, 0] — เคสนี้จับบั๊ก "เลื่อนของทั้งแถว" ได้
  • [1, 2, 3] ต้องได้ [1, 2, 3] และโค้ดไม่พัง
  • [-1, 0, -2, 3] ต้องได้ [-1, -2, 3, 0] — เคสนี้จับบั๊ก > 0 ได้
  • [0] ตัวเดียวต้องได้ [0]
  • ความยาวแถวต้องเท่าเดิมทุกเคส
  • ฟังก์ชันไม่ return อะไร แต่แถวของคนเรียกต้องเปลี่ยนจริง
🔓 เปิดเฉลยเต็ม (ลองเองก่อนนะ)พับไว้ด้านใน — คลิกเมื่อพร้อมดู

ไอเดียหนึ่งบรรทัด: ให้ insert ชี้ช่องว่างถัดไป แล้วทุกครั้งที่ตัวอ่านเจอเลขไม่ใช่ 0 ก็สลับมันขึ้นมาข้างหน้า — 0 จะถูกเตะไปท้ายเอง

คำตอบสำหรับวางใน LeetCode — slow/fast แบบสลับที่ รอบเดียวpython
class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        insert = 0                          # (1) slow — ช่องว่างถัดไปของตัวที่ไม่ใช่ 0
        for i in range(len(nums)):          # (2) fast — ไล่ดูทุกช่อง
            if nums[i] != 0:                # (3) เจอตัวที่ต้องเก็บ
                nums[insert], nums[i] = nums[i], nums[insert]   # (4) สลับขึ้นมาข้างหน้า
                insert += 1                 # (5) โซนที่จัดแล้วโตขึ้นหนึ่งช่อง
ทำไมเฉลยหน้าตาแบบนี้

LeetCode ให้เราเติมโค้ดในคลาสชื่อ Solution และชื่อเมธอดต้องตรงตามที่โจทย์กำหนด (moveZeroes ไม่ใช่ move_zeroes) · ข้อนี้ประเภทผลลัพธ์เป็น None เพราะโจทย์ให้แก้ในลิสต์เดิม ไม่ใช่คืนลิสต์ใหม่ · ส่วน List[int] มาจาก typing ซึ่ง LeetCode import ไว้ให้แล้ว จึงไม่ต้องเขียน import เอง

บรรทัดโค้ดทำอะไร / ทำไมต้องมี
(1)insert = 0ตัวเขียน เริ่มที่ช่องแรกเพราะยังไม่มีอะไรถูกจัด ค่านี้เท่ากับ "จำนวนตัวไม่ใช่ 0 ที่จัดแล้ว" ตลอดเวลา
(2)for i in range(len(nums))ตัวอ่าน ไล่ทุกช่องรอบเดียว ไม่ย้อนกลับ ข้อนี้ใช้ for ได้เพราะตัวอ่านเดินทีละหนึ่งช่องเสมอ ไม่ต้องกระโดด
(3)if nums[i] != 0กรองตัวที่ต้องเก็บ ต้องใช้ != 0 ไม่ใช่ > 0 เพราะค่าติดลบมีได้ (พื้นฐานที่ 4)
(4)nums[insert], nums[i] = nums[i], nums[insert]สลับสองช่องพร้อมกัน ตัวไม่ใช่ 0 ขึ้นมาหน้า และ 0 ที่เคยอยู่หน้าไปโผล่ที่ช่อง i ปลอดภัยเพราะช่อง insert เป็น 0 อยู่แล้วแน่นอนตาม invariant
(5)insert += 1โซนจัดแล้วโตขึ้นหนึ่งช่อง ถ้าลืมบรรทัดนี้ ทุกตัวจะเขียนทับช่องเดิมและได้คำตอบผิด
วิธีรอบที่วนพื้นที่เพิ่มคงลำดับเดิมใช้ได้ไหม
สร้างแถวใหม่1O(n) ❌ได้ตอบถูกแต่ผิดกติกา in-place
pop/remove เจอ 0 แล้วลบโตเป็นกำลังสอง ❌O(1)ได้ช้าเกินและความยาวแถวเปลี่ยน
เลื่อนของทั้งแถวโตเป็นกำลังสอง ❌O(1)ได้ตอบผิดด้วย ([0,0,1])
opposite ends1 ✅O(1) ✅ไม่ได้ ❌ผิดกติกาข้อลำดับ
เขียนทับ 2 รอบ2 ✅O(1) ✅ได้ ✅ใช้ได้
สลับที่ 1 รอบ (เฉลยนี้)1 ✅O(1) ✅ได้ ✅ใช้ได้ และเขียนสั้นสุด

หมายเหตุเรื่อง "1 รอบ กับ 2 รอบ": อย่าเข้าใจผิดว่าแบบ 2 รอบช้ากว่าเท่าตัว เพราะรอบที่สองไปแตะแค่ช่องท้ายที่เหลือ ไม่ได้วนทั้งแถวอีกครั้ง จำนวนช่องที่ถูกแตะรวมกันจึงเท่ากันทั้งสองแบบ แกนที่ทั้งสองแบบต่างกันจริงคือ จำนวนครั้งที่เขียนลงแถว ซึ่งเป็นหัวข้อถัดไป

ตอบ challenge ของโจทย์: เขียนลงแถวให้น้อยที่สุด

โจทย์แถมมาว่า "ทำให้จำนวนครั้งที่เขียนลง array น้อยที่สุด" คำถามนี้ตอบด้วยความรู้สึกไม่ได้ ต้องนับจริง มาสามแบบเทียบกัน: สลับทุกครั้ง, สลับแต่ข้ามตอน i == insert, และเขียนทับสองรอบ

นับจำนวนครั้งที่เขียนลงแถวจริง ๆpython
def writes_swap(nums):
    w = insert = 0
    for i in range(len(nums)):
        if nums[i] != 0:
            nums[insert], nums[i] = nums[i], nums[insert]
            w += 2                      # สลับหนึ่งครั้ง = เขียน 2 ช่อง
            insert += 1
    return w


def writes_swap_skip(nums):
    w = insert = 0
    for i in range(len(nums)):
        if nums[i] != 0:
            if i != insert:             # ข้ามการสลับตัวเองกับตัวเอง
                nums[insert], nums[i] = nums[i], nums[insert]
                w += 2
            insert += 1
    return w


def writes_overwrite(nums):
    w = insert = 0
    for x in nums:
        if x != 0:
            nums[insert] = x
            w += 1
            insert += 1
    for j in range(insert, len(nums)):
        nums[j] = 0
        w += 1
    return w


cases = [
    ("0 เยอะ   [0,0,0,0,1]", [0, 0, 0, 0, 1]),
    ("0 น้อย   [1,2,3,4,0]", [1, 2, 3, 4, 0]),
    ("ไม่มี 0  [1,2,3,4,5]", [1, 2, 3, 4, 5]),
]
print("เคส                    | สลับ | สลับ+ข้าม | เขียนทับ")
for name, base in cases:
    print(f"{name} | {writes_swap(base[:]):4} | {writes_swap_skip(base[:]):9} | {writes_overwrite(base[:]):8}")
Output
เคส                    | สลับ | สลับ+ข้าม | เขียนทับ
0 เยอะ   [0,0,0,0,1] |    2 |         2 |        5
0 น้อย   [1,2,3,4,0] |    8 |         0 |        5
ไม่มี 0  [1,2,3,4,5] |   10 |         0 |        5

อ่านตารางแล้วจะเห็นว่าแบบสลับล้วน แพ้ แบบเขียนทับในสองเคสหลัง (8 กับ 10 เทียบกับ 5) เพราะมันเสีย 2 ครั้งต่อหนึ่งตัวไม่ใช่ 0 แม้ในตอนที่สลับตัวเองกับตัวเองซึ่งไม่ได้ย้ายอะไรจริงเลย ส่วนแบบเขียนทับเสียคงที่เท่ากับความยาวแถว เพราะไล่เขียนทุกช่อง

คำตอบของ challenge คือแบบกลาง: เติม if i != insert เข้าไปบรรทัดเดียว เพื่อ ข้ามการสลับที่ไม่ได้ย้ายอะไร ผลคือได้ 0 ครั้งในเคสที่แถวเป็นคำตอบอยู่แล้ว (ถูกต้อง เพราะไม่ต้องแก้อะไรเลย) และไม่เคยแย่กว่าอีกสองแบบในเคสไหน

จุดที่ต้องรู้: LeetCode เขียนผิดตรงนี้

editorial ของ LeetCode เองบอกว่าแบบสลับเขียนลงแถวเท่ากับ "จำนวนตัวที่ไม่ใช่ 0" ซึ่งคลาดไปครึ่งหนึ่ง เพราะการสลับหนึ่งครั้งคือการเขียน 2 ช่อง ไม่ใช่ 1 ช่อง และข้อความนั้นไม่ได้นับการสลับตัวเองกับตัวเองด้วย ข้อความนี้ถูกคัดลอกต่อไปในบล็อกและสรุปย่อจำนวนมากทั่วอินเทอร์เน็ต ถ้าเจอที่ไหนให้รู้ว่ามันคลาด และให้เชื่อตัวเลขที่นับเองจากโค้ดข้างบน

บทเรียนที่ใหญ่กว่าตัวโจทย์

คำถาม "วิธีไหนดีที่สุด" ตอบไม่ได้ถ้าไม่ระบุว่าวัดด้วยอะไร วัดด้วยจำนวนช่องที่ถูกแตะ ทั้งสามแบบเท่ากันหมด แต่วัดด้วยจำนวนครั้งที่เขียน คำตอบต่างกันชัดเจน เวลา interviewer ถามว่า "ดีขึ้นได้อีกไหม" สิ่งแรกที่ควรถามกลับคือ "ดีขึ้นในแง่ไหน"

ทริคและคำถามต่อยอดที่มักถูกถาม

คำถามต่อยอดคำตอบที่ควรตอบได้
ถ้าไม่ต้องคงลำดับเดิมล่ะเปลี่ยนไปใช้ opposite ends: เจอ 0 ที่หน้าก็เอาตัวท้ายมาถมแล้วหุบขอบขวาเข้ามา เขียนลงแถวน้อยกว่ามากเพราะแตะแค่ช่องที่เป็น 0 จริง ๆ (นี่คือคำตอบของ LC27 พอดี)
ถ้าให้ดัน 0 ไปไว้ หน้า แถวแทนกลับทิศทั้งหมด: ตัวอ่านเดินจากขวามาซ้าย ตัวเขียนเริ่มที่ช่องสุดท้าย เงื่อนไขยังเป็น != 0 เหมือนเดิม หลักการคือตัวเขียนต้องเดินตามหลังตัวอ่านในทิศเดียวกันเสมอ
ถ้าเป็น linked list ไม่ใช่ arrayสลับตำแหน่งด้วย index ไม่ได้แล้ว ให้กวาดรอบเดียวสร้างสองสาย (สายที่ไม่ใช่ 0 และสาย 0) โดยเก็บตัวท้ายของแต่ละสายไว้ แล้วต่อสาย 0 ไว้ท้ายสายแรก
ถ้ามีสามค่าไม่ใช่สองค่า (เช่น 0, 1, 2)กลายเป็น LC75 Sort Colors ใช้สามตัวชี้แบ่งเป็นสามโซน หลักคิดเดียวกันแต่โซนเพิ่มจากสองเป็นสาม และมีกับดักว่าหลังสลับกับฝั่งขวา ห้ามขยับตัวกลาง เพราะของที่เพิ่งสลับมายังไม่ถูกตรวจ
พิสูจน์ว่าถูกได้ไหมยก invariant สามโซนขึ้นมา แสดงว่ามันจริงตอนเริ่ม จริงหลังทุกรอบ และตอนจบ (i = ความยาวแถว) มันให้ผลลัพธ์ตรงตามโจทย์พอดี
แม่แบบที่ยกไปใช้ได้อีกหลายข้อ

โจทย์ LC283, LC26, LC27, LC80 ใช้โค้ดโครงเดียวกันห้าบรรทัด ต่างกันแค่สามจุด: หนึ่ง เงื่อนไขว่า "ตัวนี้เก็บไหม" สอง ตัวเขียนหมายถึง "ช่องว่างถัดไป" หรือ "ช่องของตัวล่าสุด" สาม จบแล้วต้องทำอะไรกับส่วนที่เหลือ (เติม 0 / return ความยาว / ไม่ทำอะไร) จำสามคำถามนี้แล้วจะแก้ได้ทั้งกลุ่ม

💻 แบบฝึกหัด

ระดับ 1 — ทำตามที่เรียนมา

  1. เขียน move_zeroes แบบเขียนทับสองรอบ ให้ผ่านเคสทั้งหมดในเช็คลิสต์ใบ้ขั้น 3
  2. เขียนแบบสลับที่รอบเดียว แล้วเทียบผลกับข้อ 1 ว่าตรงกันทุกเคส
  3. เติม if i != insert ลงในแบบสลับ แล้วนับจำนวนครั้งที่เขียนด้วยตัวแปร w เทียบกับตารางในหน้านี้

ระดับ 2 — ดัดแปลงโจทย์

  1. เขียนฟังก์ชันดัน 0 ไปไว้ หน้า แถว โดยยังคงลำดับเดิมของตัวที่ไม่ใช่ 0
  2. เขียน LC27 Remove Element: ให้ค่า val มา ให้ลบทุกตัวที่เท่ากับ val แล้ว return ความยาวใหม่ (ไม่สนลำดับของส่วนที่เหลือ) ลองทำสองแบบคือ slow/fast และ opposite ends แล้วนับจำนวนครั้งที่เขียนเทียบกัน
  3. เขียนฟังก์ชันที่ดันตัวที่ หารด้วย 3 ลงตัว ไปท้ายแถว โดยคงลำดับเดิม — ข้อนี้ต้องเปลี่ยนแค่บรรทัดเดียวจากเฉลย

ระดับ 3 — ท้าทาย

  1. เขียน LC26 Remove Duplicates from Sorted Array แล้วอธิบายเป็นข้อความว่าตัวเขียนของข้อนั้นหมายถึงอะไร ต่างจาก insert ของข้อนี้อย่างไร
  2. เขียนโค้ดสุ่มทดสอบ: สุ่มแถวขึ้นมา 2000 ชุด เทียบผลของเฉลยกับวิธีสร้างแถวใหม่ (ซึ่งถูกแน่นอน) ว่าตรงกันทุกชุด
  3. เขียน LC75 Sort Colors (มีแค่ค่า 0, 1, 2) ด้วยการกวาดรอบเดียวและใช้สามตัวชี้ แล้วเขียน invariant ของสี่โซนออกมาเป็นข้อความ
💡 สรุป pattern

slow/fast pointer สำหรับกรองของในที่เดิม: ตัวอ่านไล่ดูทุกช่องตามจังหวะข้อมูล ตัวเขียนตามหลังมาบันทึกเฉพาะของที่เก็บ ตามจังหวะผลลัพธ์ invariant ที่ทำให้ปลอดภัยคือ ตัวเขียนไม่เคยแซงตัวอ่าน จึงเขียนทับได้เฉพาะช่องที่อ่านผ่านไปแล้ว และหัวใจของข้อนี้คือประโยคเดียว: อย่าไปขยับสิ่งที่ไม่ต้องการ ให้ขยับสิ่งที่ต้องการ แล้วสิ่งที่ไม่ต้องการจะไปกองท้ายเอง

ต่อยอด (โจทย์พี่น้องกัน)

LC27 Remove Element (ไม่สนลำดับ ใช้ opposite ends ได้ เขียนน้อยกว่า), LC26 Remove Duplicates from Sorted Array (ตัวเขียนหมายถึงตัวล่าสุด ไม่ใช่ช่องว่าง ระวัง off-by-one), LC80 Remove Duplicates II (เงื่อนไขมองย้อนไป 2 ช่อง), LC75 Sort Colors (สามโซนแทนสองโซน), LC1089 Duplicate Zeros (ต้องกวาดจากขวามาซ้าย เพราะกวาดจากซ้ายจะทับข้อมูลที่ยังไม่อ่าน — เป็นตัวอย่างที่ดีว่า invariant พังเมื่อไหร่)