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

Recursion — ฟังก์ชันที่เรียกตัวเอง

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

แก้ปัญหาด้วยการแบ่งเป็นปัญหาย่อยที่เล็กลง — base case, recursive case และวิธีคิดให้ออก

Recursion คือฟังก์ชันที่เรียกตัวเองเพื่อแก้ปัญหาย่อยที่เล็กลงเรื่อย ๆ เป็นแนวคิดที่ทรงพลังและสวยงาม ใช้กับปัญหาที่มีโครงสร้างซ้ำตัวเอง เช่น tree, graph, การแบ่งย่อย เป็นพื้นฐานของหลายอัลกอริทึมขั้นสูง

สองส่วนที่ขาดไม่ได้

ทุกฟังก์ชัน recursion ต้องมี 2 ส่วนเสมอ ขาดข้อใดข้อหนึ่งจะวนไม่จบ

  • Base case — เงื่อนไขหยุด เคสเล็กสุดที่ตอบได้เลยโดยไม่ต้องเรียกตัวเองอีก
  • Recursive case — เรียกตัวเองด้วยปัญหาที่ "เล็กลง" เข้าใกล้ base case
python
# factorial: 5! = 5*4*3*2*1
def factorial(n):
    if n <= 1:               # base case — หยุด
        return 1
    return n * factorial(n - 1)  # recursive case (n เล็กลง)

print(factorial(5))  # 120
# ขยาย: 5*factorial(4) = 5*4*factorial(3) = ... = 5*4*3*2*1
ลืม base case = RecursionError

ถ้าไม่มี base case (หรือ recursive case ไม่เล็กลง) ฟังก์ชันจะเรียกตัวเองไม่รู้จบ Python จะหยุดด้วย RecursionError: maximum recursion depth exceeded ตรวจเสมอว่ามีจุดหยุดและปัญหาเล็กลงทุกครั้ง

วิธีคิด recursion ให้ออก

เคล็ดลับ: อย่าพยายามไล่ตามทุกชั้นในหัว ให้ "เชื่อ" ว่าฟังก์ชันแก้ปัญหาเล็กกว่าได้ถูกต้องแล้ว (เรียกว่า leap of faith) จากนั้นถามแค่ 2 ข้อ

  1. เคสเล็กสุดคืออะไร และตอบว่าอะไร? (base case)
  2. ถ้าฉันมีคำตอบของปัญหาที่เล็กกว่า 1 ขั้น ฉันจะประกอบเป็นคำตอบของปัญหาเต็มได้อย่างไร? (recursive case)
python
# ผลรวมของลิสต์แบบ recursion
def sum_list(nums):
    if not nums:             # base: ลิสต์ว่าง = 0
        return 0
    return nums[0] + sum_list(nums[1:])  # ตัวแรก + ผลรวมที่เหลือ

print(sum_list([1, 2, 3, 4]))  # 10

# กลับข้อความ
def reverse(s):
    if len(s) <= 1:
        return s
    return reverse(s[1:]) + s[0]
print(reverse("hello"))  # olleh

ตัวอย่างคลาสสิก: Fibonacci

python
def fib(n):
    if n <= 1:               # base case: fib(0)=0, fib(1)=1
        return n
    return fib(n - 1) + fib(n - 2)

print([fib(i) for i in range(10)])
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
ระวังช้า: ใช้ memoization

fib แบบนี้คำนวณค่าซ้ำมหาศาล (O(2ⁿ) ช้ามาก) แก้ได้ด้วยการจำผลที่เคยคำนวณไว้ (memoization) — ง่ายสุดใช้ @lru_cache ครอบฟังก์ชัน นี่คือสะพานไปสู่ Dynamic Programming ที่จะเจอในการเตรียมสัมภาษณ์

python
from functools import lru_cache

@lru_cache(maxsize=None)   # จำผลลัพธ์อัตโนมัติ
def fib(n):
    if n <= 1:
        return n
    return fib(n - 1) + fib(n - 2)

print(fib(50))  # 12586269025  (เร็วมากเพราะไม่คำนวณซ้ำ)

Recursion vs Loop

หลายปัญหาเขียนได้ทั้งสองแบบ loop มักเร็วกว่าและใช้ memory น้อยกว่า ส่วน recursion อ่านง่ายกว่าสำหรับปัญหาที่มีโครงสร้างซ้ำตัวเอง (tree, graph, การแบ่งย่อย) เลือกตามความเหมาะสม

สรุปหัวข้อนี้

  • Recursion = ฟังก์ชันเรียกตัวเอง ต้องมี base case (หยุด) และ recursive case (เล็กลง)
  • วิธีคิด: เชื่อว่าฟังก์ชันแก้ปัญหาเล็กกว่าได้ แล้วประกอบคำตอบ
  • ลืม base case → RecursionError
  • ปัญหาที่คำนวณซ้ำ (เช่น fibonacci) ใช้ memoization (@lru_cache) ช่วยให้เร็วขึ้นมาก
  • Recursion อ่านง่ายกับปัญหาโครงสร้างซ้ำตัวเอง, loop เร็วและประหยัด memory กว่า
แบบฝึกหัด

1) เขียน factorial แบบ recursion 2) เขียนฟังก์ชันหา fibonacci ตัวที่ n 3) เขียน recursion นับจำนวนสมาชิกในลิสต์ (ไม่ใช้ len) 4) เขียน recursion เช็คว่าข้อความเป็นพาลินโดรมไหม 5) เพิ่ม @lru_cache ให้ fib แล้วลองเรียก fib(50) เทียบความเร็ว