ข้อ 1 · LC1768 Merge Strings Alternately 🟢
รวมสตริงสองตัวโดยหยิบตัวอักษรสลับกันทีละตัว เริ่มจาก word1 — ถ้าฝั่งหนึ่งหมดก่อน ให้ต่อส่วนที่เหลือของอีกฝั่งท้าย
ให้สตริงสองตัวคือ word1 กับ word2 จงรวมเข้าด้วยกันโดยหยิบตัวอักษรสลับกันทีละตัว เริ่มจาก word1 เสมอ
ถ้าสตริงหนึ่งหมดก่อน ให้นำส่วนที่เหลือของอีกสตริงมาต่อท้าย แล้ว return สตริงผลลัพธ์
- Input:
- word1 = "abc", word2 = "pqr"
- Output:
- "apbqcr"
- Explanation:
- สองฝั่งยาว 3 เท่ากันพอดี จึงสลับกันได้ครบทุกตัวโดยไม่มีใครเหลือ: หยิบ a (word1 ตัวที่ 0) แล้ว p (word2 ตัวที่ 0) แล้ว b แล้ว q แล้ว c แล้ว r
- Input:
- word1 = "ab", word2 = "pqrs"
- Output:
- "apbqrs"
- Explanation:
- สลับกันได้แค่ 2 คู่ (a-p และ b-q) เพราะ word1 มีแค่ 2 ตัว พอ word1 หมด ก็ไม่มีอะไรให้สลับด้วยอีกแล้ว ตัวที่เหลือของ word2 คือ r กับ s จึงถูกต่อท้ายติดกันรวดเดียวเป็น rs ไม่ใช่แทรกสลับกับอะไร
- Input:
- word1 = "abcd", word2 = "pq"
- Output:
- "apbqcd"
- Explanation:
- กลับด้านกับเคสก่อน คราวนี้ word2 หมดก่อน เหลือ c กับ d ของ word1 ต่อท้ายเป็น cd — สังเกตว่ากติกาไม่ได้สนใจว่าใครยาวกว่า สนใจแค่ว่า "ใครหมดก่อน" แล้วที่เหลือของอีกฝั่งไปต่อท้าย
- Input:
- word1 = "a", word2 = ""
- Output:
- "a"
- Explanation:
- เคสสุดขั้วที่ควรลองเอง (ไม่ได้อยู่ใน constraint จริงของ LeetCode แต่เป็นเคสที่คนเขียนโค้ดพังบ่อย): ฝั่งหนึ่งว่างเปล่า แปลว่าสลับกันได้ 0 คู่ ผลลัพธ์คืออีกฝั่งทั้งก้อน โค้ดต้องไม่ error และต้องไม่คืนสตริงว่าง
- 1 <= word1.length, word2.length <= 100
- word1 และ word2 เป็นตัวอักษรอังกฤษพิมพ์เล็ก
อ่านโจทย์กับตัวอย่างข้างบนให้ครบ แล้วลองเขียนเองก่อน ถ้าติดให้เปิดใบ้ทีละขั้น ยังไม่ต้องเปิดกล่องสอนหรือเฉลย ตรงที่ติดคือตรงที่จะได้เรียนรู้มากที่สุด
💡 ใบ้ขั้นที่ 1 — คำถามที่ต้องตอบก่อน
ถ้าสองสตริงยาวเท่ากัน ข้อนี้ง่ายมาก วนตามความยาวแล้วหยิบสลับกันก็จบ
คำถามคือ ถ้ายาวไม่เท่ากัน ลูปควรหยุดตอนไหน และหลังลูปหยุด ยังมีงานเหลือให้ทำอีกไหม
💡 ใบ้ขั้นที่ 2 — โครงที่เติมให้เกือบครบ
res = []
i = j = 0
while i < len(word1) and ___: # ต้องเช็คทั้งสองฝั่ง
res.append(word1[i]); i += 1
res.append(word2[j]); j += 1
# ลูปจบแล้วยังไม่จบงาน — ยังมีฝั่งที่เหลืออยู่
return "".join(res) + ___ + ___💡 ใบ้ขั้นที่ 3 — กุญแจของข้อนี้
ลูปใช้ and จึงหยุดทันทีที่ฝั่งใดฝั่งหนึ่งหมด ตอนนั้น i กับ j จะค้างอยู่ที่ตำแหน่งที่ยังไม่ได้หยิบพอดี
เราจึงต่อ word1[i:] และ word2[j:] ท้ายผลลัพธ์ได้เลย ฝั่งที่หมดแล้วจะให้สตริงว่างซึ่งไม่กระทบอะไร จึงไม่ต้องเขียน if แยกว่าใครหมดก่อน
🧭 กล่องสอน — ไล่ตั้งแต่ศูนย์ทีละขั้น (เปิดเมื่อพร้อม)
ขั้นที่ 1 · โจทย์นี้ขออะไรจริง ๆ
แปลโจทย์เป็นภาพก่อน ให้นึกภาพคนสองแถวยืนรอเข้าประตูเดียวกัน แถวซ้ายคือ word1 แถวขวาคือ word2
ประตูปล่อยคนทีละคน สลับแถวไปเรื่อย ๆ ซ้าย ขวา ซ้าย ขวา ลำดับที่คนเดินออกมาคือคำตอบ
กติกาที่ต้องอ่านให้แตกคือประโยคสุดท้าย: สองแถวอาจยาวไม่เท่ากัน พอแถวหนึ่งหมดคน คนที่เหลือของอีกแถวต้องเดินออกรวดเดียวจนหมด ไม่ต้องรอสลับกับใครอีก
สังเกตว่ากติกาไม่สนใจว่าแถวไหนยาวกว่า สนใจแค่ว่าแถวไหนหมดก่อน
ขั้นที่ 2 · ลองทำด้วยมือก่อนเขียนโค้ด
หยิบเคส word1 = ab กับ word2 = pqrs มาไล่ด้วยมือ
รอบแรกหยิบ a แล้ว p ได้ ap · รอบสองหยิบ b แล้ว q ได้ apbq · ถึงตรงนี้แถวซ้ายหมดแล้ว เหลือ r กับ s ในแถวขวา
r กับ s จึงต่อท้ายติดกันเป็น apbqrs ไม่ได้แทรกสลับกับอะไรอีก เพราะไม่มีใครให้สลับด้วยแล้ว
การไล่ด้วยมือก่อนสองสามรอบทำให้เราเห็นเองว่างานมีสองช่วง คือช่วงที่สลับกันได้ และช่วงที่เหลือเศษ ถ้ากระโดดไปเขียนโค้ดเลยมักจะเขียนได้แค่ช่วงแรกแล้วลืมช่วงที่สอง
ขั้นที่ 3 · วิธีแรกที่คนส่วนใหญ่คิดออก แล้วมันพอไหม
วิธีแรกที่คิดออกมักเป็น วนตามความยาวของตัวที่สั้นกว่า แล้วหยิบสลับกัน ซึ่งถูกครึ่งเดียว
ลองดูว่าถ้าหยุดแค่นั้นจะเกิดอะไรขึ้น
def wrong(word1, word2):
res = []
i = j = 0
while i < len(word1) and j < len(word2):
res.append(word1[i]); res.append(word2[j])
i += 1; j += 1
return "".join(res) # ลืมต่อส่วนที่เหลือ
def right(word1, word2):
res = []
i = j = 0
while i < len(word1) and j < len(word2):
res.append(word1[i]); res.append(word2[j])
i += 1; j += 1
return "".join(res) + word1[i:] + word2[j:]
for a, b in [("abc","pqr"), ("ab","pqrs"), ("abcd","pq")]:
w, r = wrong(a,b), right(a,b)
print(f"a={a!r} b={b!r} | ลืมเก็บเศษ -> {w!r} | ถูก -> {r!r} | ตรงกันไหม: {w==r}")a='abc' b='pqr' | ลืมเก็บเศษ -> 'apbqcr' | ถูก -> 'apbqcr' | ตรงกันไหม: True
a='ab' b='pqrs' | ลืมเก็บเศษ -> 'apbq' | ถูก -> 'apbqrs' | ตรงกันไหม: False
a='abcd' b='pq' | ลืมเก็บเศษ -> 'apbq' | ถูก -> 'apbqcd' | ตรงกันไหม: Falseสังเกตบรรทัดแรก: เคสที่ยาวเท่ากันให้ผลตรงกันทั้งสองวิธี นี่คือสาเหตุที่หลายคนเทสต์ด้วยเคสแรกแล้วคิดว่าโค้ดถูกแล้ว
แต่พอสองฝั่งยาวไม่เท่ากัน คำตอบขาดท้ายไปเลย และไม่มี error ขึ้นให้เห็น
ขั้นที่ 4 · กุญแจที่ปลดล็อกข้อนี้
กุญแจอยู่ที่การใช้ตัวชี้สองตัวคือ i กับ j แล้วปล่อยให้มันค้างค่าไว้หลังลูปจบ
เพราะเงื่อนไข while ใช้คำว่า and ลูปจะหยุดทันทีที่ฝั่งใดฝั่งหนึ่งหมด ตอนนั้น i กับ j ชี้อยู่ที่ตำแหน่งแรกที่ยังไม่ได้หยิบพอดี
เราจึงเขียน word1[i:] + word2[j:] ต่อท้ายได้เลย โดยไม่ต้องเขียน if เช็คว่าใครหมดก่อน เพราะฝั่งที่หมดแล้ว slice จะให้สตริงว่างซึ่งบวกเข้าไปก็ไม่เปลี่ยนอะไร
การใช้ slice ให้คืนสตริงว่างแทนการเขียน if แยกเคส เป็นวิธีลดเงื่อนไขที่ใช้ได้บ่อยมาก ยิ่งเงื่อนไขน้อย โอกาสเขียนพลาดยิ่งน้อย
ขั้นที่ 5 · เดินตามโค้ดทีละสเต็ป
โค้ดข้างล่างพิมพ์สถานะทุกรอบให้ดู ให้ดูค่า i กับ j ตอนลูปจบเป็นพิเศษ เพราะมันคือหัวใจของข้อนี้
def merge(word1, word2):
res = []
i = j = 0
while i < len(word1) and j < len(word2):
res.append(word1[i]); res.append(word2[j])
print(f" i={i} j={j} -> หยิบ {word1[i]!r} แล้ว {word2[j]!r} | ได้ {''.join(res)!r}")
i += 1; j += 1
print(f" ลูปจบ: i={i} j={j} | เหลือ word1[{i}:]={word1[i:]!r} word2[{j}:]={word2[j:]!r}")
return "".join(res) + word1[i:] + word2[j:]
print("merge('ab','pqrs') =", merge("ab", "pqrs")) i=0 j=0 -> หยิบ 'a' แล้ว 'p' | ได้ 'ap'
i=1 j=1 -> หยิบ 'b' แล้ว 'q' | ได้ 'apbq'
ลูปจบ: i=2 j=2 | เหลือ word1[2:]='' word2[2:]='rs'
merge('ab','pqrs') = apbqrsบรรทัดสุดท้ายก่อนคำตอบคือจุดสำคัญ: word1[2:] ได้สตริงว่างเพราะหมดแล้ว ส่วน word2[2:] ได้ rs ซึ่งคือเศษที่ต้องเอาไปต่อท้าย
ขั้นที่ 6 · ทำไมต้อง join ไม่ใช่ +=
สังเกตว่าโค้ดเก็บตัวอักษรลง list ชื่อ res แล้วค่อย join ตอนท้าย ไม่ได้เขียน res += word1[i] ไปเรื่อย ๆ
เหตุผลคือเรื่องเดียวกับส่วนที่ 4 ของหน้าแนวคิด: string แก้ในที่เดิมไม่ได้ การต่อ string ทีละตัวในลูปจึงเป็นการสร้างสตริงใหม่ทุกรอบ
ข้อนี้ n แค่ 100 จะเขียนแบบไหนก็ผ่าน แต่การฝึกท่าที่ถูกตั้งแต่ข้อง่ายจะช่วยเราตอนเจอข้อที่ n เป็นแสน
🔓 เฉลยเต็ม พร้อมอธิบายทีละบรรทัด (ลองเองก่อนนะ)พับไว้ด้านใน — คลิกเมื่อพร้อมดู
class Solution:
def mergeAlternately(self, word1: str, word2: str) -> str:
res = [] # เก็บชิ้นส่วนไว้ก่อน ค่อย join ตอนท้าย
i = j = 0 # ตัวชี้ประจำแต่ละสตริง
while i < len(word1) and j < len(word2): # and -> หยุดทันทีที่ฝั่งใดฝั่งหนึ่งหมด
res.append(word1[i])
i += 1
res.append(word2[j])
j += 1
# ลูปจบแล้ว i กับ j ค้างอยู่ที่ตำแหน่งแรกที่ยังไม่ได้หยิบ
# ฝั่งที่หมดแล้วจะได้สตริงว่าง จึงไม่ต้องเขียน if แยกเคส
return "".join(res) + word1[i:] + word2[j:]อ่านโค้ดทีละส่วน
- res = [] เก็บตัวอักษรลง list ก่อน เพราะการต่อ string ในลูปมีต้นทุนแอบแฝง
- i = j = 0 ตั้งตัวชี้ให้แต่ละสตริงหนึ่งตัว เริ่มที่ตำแหน่งแรกทั้งคู่
- เงื่อนไข while ใช้ and เพื่อให้หยุดทันทีที่ฝั่งใดฝั่งหนึ่งหมด ป้องกันการอ่านตำแหน่งที่ไม่มีอยู่
- ในลูปหยิบ word1 ก่อนเสมอ ตามที่โจทย์กำหนด แล้วขยับตัวชี้ทีละตัว
- หลังลูปต่อ word1[i:] และ word2[j:] ท้ายผลลัพธ์ ฝั่งที่หมดแล้วให้สตริงว่างซึ่งไม่กระทบอะไร
ต้นทุน
เวลา O(n + m) เพราะแตะตัวอักษรแต่ละตัวครั้งเดียว · หน่วยความจำ O(n + m) สำหรับผลลัพธ์ ซึ่งเลี่ยงไม่ได้เพราะโจทย์ขอสตริงใหม่
เช็คลิสต์ก่อนกดส่ง
- หยิบ word1 ก่อน word2 ในทุกรอบ ไม่ใช่สลับกัน
- เงื่อนไข while ใช้ and ไม่ใช่ or (ถ้าใช้ or จะอ่านตำแหน่งเกินขอบแล้ว IndexError)
- มีบรรทัดต่อส่วนที่เหลือหลังลูปจบ ไม่ได้ return แค่ join(res)
- ทดสอบด้วยเคสที่สองฝั่งยาวไม่เท่ากันทั้งสองทิศ คือฝั่งซ้ายยาวกว่า และฝั่งขวายาวกว่า