0Pricing
Coding Interview Prep · บทเรียน

การเรียกซ้ำและวิธีต้นไม้การเรียกซ้ำ

ติดตามการเรียกซ้ำเป็นต้นไม้ ใช้ทฤษฎีบทมาสเตอร์ และหาความซับซ้อนด้านเวลาของการเรียงแบบผสาน แฟกทอเรียล และฟีโบนัชชีรูปแบบต่าง ๆ

การเรียกซ้ำและวิธีต้นไม้การเรียกซ้ำ เป็นบทเรียน Coding Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Coding Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน

การเรียกซ้ำและสแตกการเรียกฟังก์ชัน

เมื่อฟังก์ชันเรียกตัวเอง การเรียกแต่ละครั้งจะเพิ่ม เฟรมของสแตก ซึ่งสะสมต่อกันจนกว่าจะถึงกรณีฐาน แล้วจึงคลายกลับออกมา การจินตนาการภาพนี้คือขั้นตอนแรกในการวิเคราะห์การเรียกซ้ำ

def factorial(n):
    if n == 0:       # base case
        return 1
    return n * factorial(n - 1)  # recursive call

# Call chain: factorial(4)
#   4 * factorial(3)
#     3 * factorial(2)
#       2 * factorial(1)
#         1 * factorial(0) -> 1
# Unwinds: 1, 2, 6, 24
print(factorial(5))  # 120

ต้นไม้การเรียกซ้ำของฟีโบนัชชี

ต้นไม้การเรียกซ้ำจะแตกการเรียกแต่ละครั้งออกเป็นการเรียกย่อยของมัน การคำนวณฟีโบนัชชีแบบตรงไปตรงมาจะแตกออกเป็นสองกิ่งทุกครั้ง ทำให้ได้ต้นไม้ที่มีประมาณ 2^n โหนด — จึงมีความซับซ้อนเป็น O(2^n) ดูโค้ดได้เลย

call_count = [0]

def fib_naive(n):
    call_count[0] += 1
    if n <= 1:
        return n
    return fib_naive(n-1) + fib_naive(n-2)

for n in [5, 10, 15, 20]:
    call_count[0] = 0
    result = fib_naive(n)
    print(f'fib({n})={result}, calls={call_count[0]}')
# Calls roughly double each time n increases by 1

การระบุปัญหาย่อยที่เกิดซ้ำ

ในต้นไม้นั้น การเรียกเดิม ๆ เช่น fib(3) จะเกิดซ้ำในหลายกิ่ง ปัญหาย่อยที่ซ้อนทับกันเหล่านี้เป็นสัญญาณให้ใช้การจดจำผลลัพธ์ ซึ่งลดความซับซ้อนจาก O(2^n) เหลือ O(n)

# Memoised: each unique sub-problem computed once
def fib_memo(n, memo={}):
    if n in memo: return memo[n]
    if n <= 1:    return n
    memo[n] = fib_memo(n-1, memo) + fib_memo(n-2, memo)
    return memo[n]

call_count2 = [0]
def fib_counted(n, memo={}):
    call_count2[0] += 1
    if n in memo: return memo[n]
    if n <= 1:    return n
    memo[n] = fib_counted(n-1, memo) + fib_counted(n-2, memo)
    return memo[n]

fib_counted(20)
print(f'calls with memo: {call_count2[0]}')  # only 21

ต้นไม้การเรียกซ้ำของการเรียงลำดับแบบผสาน

ต้นไม้ของการเรียงลำดับแบบผสานมี log n ระดับ และแต่ละระดับทำงานรวมกันเป็น O(n) — สมาชิกทุกตัวถูกเข้าถึงครั้งเดียว จึงนำจำนวนระดับมาคูณกันได้เป็น O(n log n) ดูโค้ดได้เลย

# Merge sort: at each level, n total elements are merged
# Level 0:  1 merge of n elements    -> n work
# Level 1:  2 merges of n/2 each     -> n work
# Level 2:  4 merges of n/4 each     -> n work
# ...log(n) levels...
# Total: n * log(n)

# Verify with operation counter:
def merge_sort_counted(arr):
    ops = [0]
    def _sort(a):
        if len(a) <= 1: return a
        m = len(a) // 2
        l, r = _sort(a[:m]), _sort(a[m:])
        result, i, j = [], 0, 0
        while i < len(l) and j < len(r):
            ops[0] += 1
            if l[i] <= r[j]: result.append(l[i]); i+=1
            else:             result.append(r[j]); j+=1
        return result + l[i:] + r[j:]
    return _sort(arr), ops[0]

_, c = merge_sort_counted(list(range(64, 0, -1)))
print(f'Merge ops: {c}')  # ~384 ~ 64*log2(64)=384

ทฤษฎีบทมาสเตอร์

ทฤษฎีบทมาสเตอร์ใช้แก้สมการเวียนเกิด T(n) = a*T(n/b) + O(n^d) ได้ด้วยสามกรณี สำหรับการเรียงลำดับแบบผสาน (a=2, b=2, d=1) จะได้ O(n log n) จงจำทั้งสามกรณีไว้สำหรับการสอบ

# Merge sort: T(n) = 2*T(n/2) + O(n)
# a=2, b=2, d=1, log_b(a)=log2(2)=1=d  => O(n log n)

# Binary search: T(n) = 1*T(n/2) + O(1)
# a=1, b=2, d=0, log2(1)=0=d  => O(log n)

# Strassen matrix mult: T(n) = 7*T(n/2) + O(n^2)
# a=7, b=2, d=2, log2(7)~2.81 > 2 => O(n^log2(7)) ~ O(n^2.81)

import math
print('log2(7) =', math.log2(7))  # 2.807...

การวาดต้นไม้การเรียกซ้ำทีละขั้น

การวาด ต้นไม้การเรียกซ้ำทำได้ดังนี้: วาง T(n) ไว้ด้านบน แตกการเรียกแต่ละครั้งออกมา รวมงานในแต่ละระดับ แล้วคูณด้วยจำนวนระดับ ฝึกจนทำได้โดยอัตโนมัติ

# Factorial: T(n) = T(n-1) + O(1)
# Tree is a chain: n levels, O(1) each -> O(n)

# Fibonacci: T(n) = T(n-1) + T(n-2) + O(1)
# Binary tree of depth n, ~2^n nodes -> O(2^n)

# Merge sort: T(n) = 2*T(n/2) + O(n)
# Log levels, n work each -> O(n log n)

def count_recursive_calls(n, results=[]):
    if n <= 1:
        results.append(n)
        return n
    return count_recursive_calls(n-1, results) + count_recursive_calls(n-2, results)

results = []
count_recursive_calls(8, results)
print(f'fib(8) leaf calls: {len(results)}')

การเรียกซ้ำแบบเลขชี้กำลัง: เซตย่อย

การสร้าง เซตย่อยทั้งหมดมีความซับซ้อนเป็น O(2^n) — มีเซตย่อยอยู่ exactement 2^n ชุด จึงไม่มีทางทำให้เร็วกว่านี้ได้ แต่ละสมาชิกจะถูกเลือกให้เข้าหรือไม่เข้าก็ได้ ทำให้เกิดต้นไม้การเลือกแบบทวิภาค ดูโค้ดได้เลย

def subsets(nums):
    result = []
    def backtrack(start, current):
        result.append(list(current))  # O(n) copy
        for i in range(start, len(nums)):
            current.append(nums[i])
            backtrack(i + 1, current)
            current.pop()
    backtrack(0, [])
    return result

nums = [1, 2, 3]
ss = subsets(nums)
print(len(ss))  # 8 = 2^3
print(ss)

การเรียกซ้ำแบบหางและการเพิ่มประสิทธิภาพ

การเรียกซ้ำแบบหางคือการที่การเรียกซ้ำเป็นขั้นตอนสุดท้ายอย่างแท้จริง ภาษาบางภาษาจะนำเฟรมเดิมกลับมาใช้ใหม่ แต่ Python ไม่ทำเช่นนั้น — ดังนั้นการเรียกซ้ำที่ลึกมากยังคงทำให้เกิดการล้นของสแตกได้ ให้ใช้ลูปแทน

# Tail-recursive factorial (accumulator pattern)
def fact_tail(n, acc=1):
    if n == 0:
        return acc
    return fact_tail(n - 1, n * acc)  # tail call

# Python does NOT TCO, so this overflows for large n
# Instead, convert to iterative:
def fact_iter(n):
    acc = 1
    while n > 0:
        acc *= n
        n -= 1
    return acc

print(fact_tail(10))  # 3628800
print(fact_iter(10))  # 3628800

ความซับซ้อนด้านพื้นที่ของการเรียกซ้ำ

การเรียกซ้ำแต่ละครั้งเก็บเฟรมหนึ่งเฟรมไว้ ดังนั้นการเรียกซ้ำจึงใช้พื้นที่เป็น O(ความลึก) การเรียกซ้ำแบบเชิงเส้นมีความซับซ้อนเป็น O(n) ส่วนการค้นหาแบบ DFS บนต้นไม้สมดุลมีความซับซ้อนเป็น O(log n) หากเรียกลึกเกินไปจะพบ RecursionError

import sys
print(sys.getrecursionlimit())  # default 1000

# Increase limit for deep problems
sys.setrecursionlimit(10000)

# Track max depth manually
def max_depth_tracker(n, depth=0, max_seen=[0]):
    max_seen[0] = max(max_seen[0], depth)
    if n <= 0:
        return
    max_depth_tracker(n - 1, depth + 1, max_seen)
    return max_seen[0]

print(max_depth_tracker(50))  # 50  => O(n) stack frames

ต้นไม้การเรียกซ้ำของการเรียงลำดับแบบเร็ว

การเรียงลำดับแบบเร็วมีความซับซ้อนเป็น O(n log n) เมื่อเลือกจุดหมุนได้ดี แต่ถ้าเลือกจุดหมุนได้แย่กับข้อมูลเข้าที่เรียงอยู่แล้ว ความซับซ้อนจะเพิ่มเป็น O(n^2) นี่คือเหตุผลที่การสุ่มจุดหมุนมีความสำคัญ ดูโค้ดได้เลย

import random

def quick_sort(arr):
    if len(arr) <= 1:
        return arr
    pivot = random.choice(arr)  # randomised -> O(n log n) expected
    less    = [x for x in arr if x < pivot]
    equal   = [x for x in arr if x == pivot]
    greater = [x for x in arr if x > pivot]
    return quick_sort(less) + equal + quick_sort(greater)

print(quick_sort([3, 6, 8, 10, 1, 2, 1]))  # sorted

ฟังก์ชันยกกำลัง: การเรียกซ้ำแบบ log n

การคำนวณ x^n แบบตรงไปตรงมาต้องคูณ O(n) ครั้ง แต่การ ยกกำลังสองจะลดงานลงครึ่งหนึ่งในแต่ละขั้น: x^n = (x^(n/2))^2 จึงได้ O(log n) อย่างชัดเจน — เป็นการลดลงครึ่งหนึ่งที่เห็นได้จริง ดูโค้ดได้เลย

def fast_pow(x, n):
    if n == 0: return 1
    if n < 0:  return 1 / fast_pow(x, -n)
    if n % 2 == 0:
        half = fast_pow(x, n // 2)
        return half * half          # O(log n) calls
    return x * fast_pow(x, n - 1)

print(fast_pow(2, 10))   # 1024
print(fast_pow(3, 5))    # 243
# Only log2(10)=3-4 recursive calls for n=10

ตรวจสอบความเข้าใจอย่างรวดเร็ว

ตรวจสอบความเข้าใจอย่างรวดเร็ว — แสดงสิ่งที่วิธีต้นไม้การเรียกซ้ำสอนคุณ หนึ่งคำถาม ค่อย ๆ คิดได้เต็มที่ 🌳

ทบทวนบทเรียน

ทบทวน: ต้นไม้การเรียกซ้ำเผยให้เห็นงานทั้งหมด ทฤษฎีบทมาสเตอร์ใช้แก้สมการเวียนเกิดของการแบ่งแล้วพิชิต และการเรียกซ้ำใช้พื้นที่สแตกเป็น O(ความลึก)

คำถามที่พบบ่อย

บทเรียน “การเรียกซ้ำและวิธีต้นไม้การเรียกซ้ำ” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “การเรียกซ้ำและวิธีต้นไม้การเรียกซ้ำ” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Coding Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “การเรียกซ้ำและวิธีต้นไม้การเรียกซ้ำ”

ติดตามการเรียกซ้ำเป็นต้นไม้ ใช้ทฤษฎีบทมาสเตอร์ และหาความซับซ้อนด้านเวลาของการเรียงแบบผสาน แฟกทอเรียล และฟีโบนัชชีรูปแบบต่าง ๆ คุณปฏิบัติ Coding Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Coding Interview Prep หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน Coding Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน

บทเรียน “การเรียกซ้ำและวิธีต้นไม้การเรียกซ้ำ” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน Coding Interview Prep นี้ได้ไหม

ได้ บทเรียน Coding Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. สัญกรณ์ Big-O ตั้งแต่พื้นฐาน
  2. วิเคราะห์ลูปและลูปซ้อน
  3. การเรียกซ้ำและวิธีต้นไม้การเรียกซ้ำ
  4. ความซับซ้อนด้านพื้นที่และการแลกเปลี่ยน
← กลับไปที่ Coding Interview Prep