ต้องมี Base Case เสมอ — ไม่งั้นไม่หยุด
Base case คือการ์ดป้องกัน infinite recursion — เขียนเป็นบรรทัดแรก ตรวจสอบก่อนทำ recursive call เสมอ
Iteration & Recursion
เรียนรู้ Recursion ตั้งแต่แนวคิดการย่อยปัญหา Base Case/Recursive Case ความซับซ้อน O(n)/O(n) ไปจนถึงข้อควรระวัง Stack Overflow และการเลือกใช้ให้เหมาะ
Recursion (การเรียกซ้ำ) คือเทคนิคที่ฟังก์ชันเรียกตัวเองเพื่อแก้ปัญหาเดิมในขนาดที่เล็กลง — จนถึงจุดที่แก้ได้ทันที (base case) แล้วค่อยรวมคำตอบย้อนกลับขึ้นมา
มองแบบง่าย: "ย่อยปัญหาใหญ่ให้เล็กลงเรื่อย ๆ จนแก้ได้ทันที แล้วค่อยประกอบคำตอบกลับ" — กระบวนการนี้เรียกว่า divide and conquer ในระดับแนวคิด
หัวใจของ recursion คือการมองปัญหาผ่านมุมมองที่ "ปัญหาเดิม" = "ปัญหาที่เล็กลง + อีกนิดหน่อย" — เช่น factorial(5) = 5 × factorial(4) และ factorial(1) = 1 (แก้ได้ทันที)
วิธีคิด recursive: 1. ถามตัวเอง: กรณีไหนแก้ได้ทันทีโดยไม่ต้องเรียกซ้ำ? → นั่นคือ base case 2. ถามตัวเอง: กรณีทั่วไปสามารถเขียนในรูปของตัวเองแต่ขนาดเล็กลงได้อย่างไร? → นั่นคือ recursive case 3. เช็กว่า recursive case นั้นพาเข้าใกล้ base case จริง (ขนาดปัญหาลดลงทุกครั้ง)
ทุก recursion function มี 2 ส่วนที่ขาดไม่ได้ — ถ้าขาดส่วนใดส่วนหนึ่ง ฟังก์ชันจะทำงานผิดพลาด
กฎเหล็ก: ถ้าไม่มี base case → recursion เรียกไม่หยุด (stack overflow) ถ้า recursive case ไม่พาเข้าใกล้ base case → recursion เรียกไม่หยุดเช่นกัน
⚠️ Base Case ต้องมาก่อน Recursive Case ในโค้ด
ตรวจสอบ base case เป็นบรรทัดแรกเสมอ — ถ้าถึง base case แล้ว ให้ return ทันที ไม่ต้องคิด recursive case ต่อ การวาง base case ไว้ก่อนช่วยให้โค้ดอ่านง่ายและป้องกันการเรียกซ้ำที่ไม่จำเป็น
countdown เป็นตัวอย่าง recursion ที่เรียบง่ายที่สุด — ไม่มีการคืนค่าซับซ้อน ไม่ต้อง unwind หลายชั้น แค่พิมพ์ค่าแล้วเรียกตัวเองต่อด้วยค่าน้อยลง
Base case = n === 0, Recursive case = countdown(n - 1)
function countdown(n) {
if (n === 0) {
console.log("จบการนับถอยหลัง");
return;
}
console.log(n);
countdown(n - 1);
}
countdown(3);
// 3
// 2
// 1
// จบการนับถอยหลังลำดับขั้นของ countdown(3):
factorial เป็นตัวอย่างคลาสสิกที่แสดงการ unwind — เราเรียกซ้ำจนถึง base case แล้วค่อยคูณค่าย้อนกลับขึ้นมาทีละชั้น
Base case = n <= 1 (return 1), Recursive case = n * factorial(n - 1)
function factorial(n) {
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
}
console.log(factorial(4)); // 24ลำดับขั้นของ factorial(4):
สังเกต: ฟังก์ชันของเราหยุดรอผลลัพธ์จาก recursive call ทุกครั้ง — กระบวนการย้อนกลับแบบนี้เรียกว่า unwind (คลี่คลาย)
Call Stack คือ "กองงาน" ของฟังก์ชันที่กำลังทำงานอยู่ — ฟังก์ชันเรียกใหม่ก็วางกองเพิ่มด้านบน (push) ฟังก์ชันทำงานเสร็จก็ยกออกจากด้านบน (pop) ทำงานแบบ LIFO: Last In, First Out
เมื่อถึง base case แล้ว ระบบจะ unwind — คืนค่ากลับทีละชั้นจากบนลงล่าง จนกระทั่งฟังก์ชันแรก (ด้านล่างสุด) ได้ผลลัพธ์
ข้อสังเกตจาก Code Trace: แต่ละครั้งที่เรียก `factorial(n - 1)` ฟังก์ชันปัจจุบันจะ "หยุดรอ" — state ของ n ในแต่ละชั้นถูกเก็บไว้ใน call stack จนกว่า recursive call จะ return ค่ากลับมา
Space Complexity = O(n) — ฟังก์ชัน recursive แต่ละครั้งสร้าง frame ใหม่บน call stack — factorial(1000) ต้องซ้อน stack 1,000 ชั้น! นี่คือข้อเสียใหญ่ที่สุดของ recursion
⚠️ Stack Overflow — เมื่อ recursion ลึกเกินไป
JavaScript engine แต่ละตัวมีขีดจำกัด call stack (ปกติ ~10,000 เฟรม) — ถ้า n ใหญ่เกินไปจะเกิด stack overflow และโปรแกรมหยุดทำงานทันที นี่คือเหตุผลที่งานข้อมูลปริมาณมากควรใช้ iteration แทน
หัวใจสำคัญของ Recursion ที่ต้องจำให้ขึ้นใจ
Base case คือการ์ดป้องกัน infinite recursion — เขียนเป็นบรรทัดแรก ตรวจสอบก่อนทำ recursive call เสมอ
ทุกครั้งที่เรียกตัวเอง input ต้องเล็กลงในทางใดทางหนึ่ง — n - 1, array.slice(1), node.left — เพื่อให้ถึง base case ได้
Recursion ใช้ memory O(n) จาก call stack — ถ้า n > ~10,000 อาจ overflow ต้องประเมินก่อนใช้กับ input ขนาดใหญ่
หลักการ recursive leap of faith: ถ้า base case ถูก และ recursive case เรียกด้วย input ที่เล็กลง → recursive call จะทำงานถูกต้อง
งานเชิงเส้นด้วย recursion มักมี Time O(n) และ Space O(n) — ต่างจาก iteration ที่ Space O(1) แต่ recursion อ่านง่ายกว่าในปัญหาแบบต้นไม้
ทดสอบความเข้าใจก่อนเริ่มทำแบบฝึกหัด
คิดถึงกลไก call stack และเงื่อนไขหยุด
แบบฝึกหัด 3 ข้อนี้จะช่วยให้คุณเขียน Recursion ได้คล่อง — เริ่มจาก countdown คืนอาเรย์ ไปจนถึง sumTo และหาค่าสูงสุดในอาเรย์