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

ข้อ 1 · LC1768 Merge Strings Alternately 🟢

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

รวมสตริงสองตัวโดยหยิบตัวอักษรสลับกันทีละตัว เริ่มจาก word1 — ถ้าฝั่งหนึ่งหมดก่อน ให้ต่อส่วนที่เหลือของอีกฝั่งท้าย

ให้สตริงสองตัวคือ word1 กับ word2 จงรวมเข้าด้วยกันโดยหยิบตัวอักษรสลับกันทีละตัว เริ่มจาก word1 เสมอ

ถ้าสตริงหนึ่งหมดก่อน ให้นำส่วนที่เหลือของอีกสตริงมาต่อท้าย แล้ว return สตริงผลลัพธ์

Example 1
Input:
word1 = "abc", word2 = "pqr"
Output:
"apbqcr"
Explanation:
สองฝั่งยาว 3 เท่ากันพอดี จึงสลับกันได้ครบทุกตัวโดยไม่มีใครเหลือ: หยิบ a (word1 ตัวที่ 0) แล้ว p (word2 ตัวที่ 0) แล้ว b แล้ว q แล้ว c แล้ว r
Example 2
Input:
word1 = "ab", word2 = "pqrs"
Output:
"apbqrs"
Explanation:
สลับกันได้แค่ 2 คู่ (a-p และ b-q) เพราะ word1 มีแค่ 2 ตัว พอ word1 หมด ก็ไม่มีอะไรให้สลับด้วยอีกแล้ว ตัวที่เหลือของ word2 คือ r กับ s จึงถูกต่อท้ายติดกันรวดเดียวเป็น rs ไม่ใช่แทรกสลับกับอะไร
Example 3
Input:
word1 = "abcd", word2 = "pq"
Output:
"apbqcd"
Explanation:
กลับด้านกับเคสก่อน คราวนี้ word2 หมดก่อน เหลือ c กับ d ของ word1 ต่อท้ายเป็น cd — สังเกตว่ากติกาไม่ได้สนใจว่าใครยาวกว่า สนใจแค่ว่า "ใครหมดก่อน" แล้วที่เหลือของอีกฝั่งไปต่อท้าย
Example 4
Input:
word1 = "a", word2 = ""
Output:
"a"
Explanation:
เคสสุดขั้วที่ควรลองเอง (ไม่ได้อยู่ใน constraint จริงของ LeetCode แต่เป็นเคสที่คนเขียนโค้ดพังบ่อย): ฝั่งหนึ่งว่างเปล่า แปลว่าสลับกันได้ 0 คู่ ผลลัพธ์คืออีกฝั่งทั้งก้อน โค้ดต้องไม่ error และต้องไม่คืนสตริงว่าง
Constraints (ข้อจำกัด)
  • 1 <= word1.length, word2.length <= 100
  • word1 และ word2 เป็นตัวอักษรอังกฤษพิมพ์เล็ก
⏸ ลองเองก่อน 10 นาที

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

💡 ใบ้ขั้นที่ 1 — คำถามที่ต้องตอบก่อน

ถ้าสองสตริงยาวเท่ากัน ข้อนี้ง่ายมาก วนตามความยาวแล้วหยิบสลับกันก็จบ

คำถามคือ ถ้ายาวไม่เท่ากัน ลูปควรหยุดตอนไหน และหลังลูปหยุด ยังมีงานเหลือให้ทำอีกไหม

💡 ใบ้ขั้นที่ 2 — โครงที่เติมให้เกือบครบ
python
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 · วิธีแรกที่คนส่วนใหญ่คิดออก แล้วมันพอไหม

วิธีแรกที่คิดออกมักเป็น วนตามความยาวของตัวที่สั้นกว่า แล้วหยิบสลับกัน ซึ่งถูกครึ่งเดียว

ลองดูว่าถ้าหยุดแค่นั้นจะเกิดอะไรขึ้น

เทียบวิธีที่ลืมเก็บส่วนที่เหลือ กับวิธีที่ถูกpython
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}")
Output
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 ตอนลูปจบเป็นพิเศษ เพราะมันคือหัวใจของข้อนี้

ไล่ทีละรอบ พร้อมดูว่าเหลือเศษอะไรหลังลูปจบpython
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"))
Output
  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 เป็นแสน

🔓 เฉลยเต็ม พร้อมอธิบายทีละบรรทัด (ลองเองก่อนนะ)พับไว้ด้านใน — คลิกเมื่อพร้อมดู
คำตอบpython
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:]

อ่านโค้ดทีละส่วน

  1. res = [] เก็บตัวอักษรลง list ก่อน เพราะการต่อ string ในลูปมีต้นทุนแอบแฝง
  2. i = j = 0 ตั้งตัวชี้ให้แต่ละสตริงหนึ่งตัว เริ่มที่ตำแหน่งแรกทั้งคู่
  3. เงื่อนไข while ใช้ and เพื่อให้หยุดทันทีที่ฝั่งใดฝั่งหนึ่งหมด ป้องกันการอ่านตำแหน่งที่ไม่มีอยู่
  4. ในลูปหยิบ word1 ก่อนเสมอ ตามที่โจทย์กำหนด แล้วขยับตัวชี้ทีละตัว
  5. หลังลูปต่อ word1[i:] และ word2[j:] ท้ายผลลัพธ์ ฝั่งที่หมดแล้วให้สตริงว่างซึ่งไม่กระทบอะไร

ต้นทุน

เวลา O(n + m) เพราะแตะตัวอักษรแต่ละตัวครั้งเดียว · หน่วยความจำ O(n + m) สำหรับผลลัพธ์ ซึ่งเลี่ยงไม่ได้เพราะโจทย์ขอสตริงใหม่

เช็คลิสต์ก่อนกดส่ง

  • หยิบ word1 ก่อน word2 ในทุกรอบ ไม่ใช่สลับกัน
  • เงื่อนไข while ใช้ and ไม่ใช่ or (ถ้าใช้ or จะอ่านตำแหน่งเกินขอบแล้ว IndexError)
  • มีบรรทัดต่อส่วนที่เหลือหลังลูปจบ ไม่ได้ return แค่ join(res)
  • ทดสอบด้วยเคสที่สองฝั่งยาวไม่เท่ากันทั้งสองทิศ คือฝั่งซ้ายยาวกว่า และฝั่งขวายาวกว่า