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

การแลกเปลี่ยนระหว่างแบบเรียกซ้ำกับแบบวนซ้ำ

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

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

ความเป็นคู่กันของการเรียกซ้ำและการวนซ้ำ

อัลกอริทึมทุกแบบที่เขียนเป็นแบบเรียกซ้ำได้ สามารถเขียนเป็นแบบวนซ้ำได้เช่นกัน และในทางกลับกัน เวอร์ชันแบบเรียกซ้ำมักสะท้อนนิยามทางคณิตศาสตร์ของปัญหาได้ใกล้เคียงกว่า ขณะที่เวอร์ชันแบบวนซ้ำให้คุณควบคุมหน่วยความจำได้โดยตรงและหลีกเลี่ยงความเสี่ยงจากสแตกโอเวอร์โฟลว์ การเลือกระหว่างสองแบบเป็นการตัดสินใจเชิงปฏิบัติที่พิจารณาจากความอ่านง่าย ขีดจำกัดด้านความลึก และข้อกำหนดด้านประสิทธิภาพ

ในการสัมภาษณ์ การสามารถนำเสนอทั้งสองแบบและอธิบายข้อแลกเปลี่ยนได้เป็นสัญญาณที่ชัดเจนว่าคุณเข้าใจเนื้อหาอย่างเชี่ยวชาญ

factorial: แบบเรียกซ้ำเทียบกับแบบวนซ้ำ

factorial เป็นตัวอย่างมาตรฐาน คำตอบแบบเรียกซ้ำเข้ารหัสนิยามทางคณิตศาสตร์ n! = n × (n-1)! โดยตรง และใช้พื้นที่สแตก O(n) เนื่องจากมีค่าส่งคืนที่รออยู่ n ค่า เวอร์ชันแบบวนซ้ำวนลูปตั้งแต่ 1 ถึง n โดยใช้พื้นที่ O(1) เมื่อ n = 1000 เวอร์ชันแบบเรียกซ้ำจะชนขีดจำกัดเริ่มต้นของไพธอน ส่วนเวอร์ชันแบบวนซ้ำรองรับ n ที่ใหญ่เพียงใดก็ได้

def factorial_rec(n):
    if n == 0:
        return 1
    return n * factorial_rec(n - 1)   # O(n) stack

def factorial_iter(n):
    result = 1
    for i in range(2, n + 1):
        result *= i                    # O(1) stack
    return result

print(factorial_rec(10))   # 3628800
print(factorial_iter(10))  # 3628800

# Large n: iterative works, recursive may overflow
print(factorial_iter(1000) > 0)  # True (Python handles big ints)

ฟีโบนักชี: เอ็กซ์โพเนนเชียลเทียบกับเชิงเส้น

ฟีโบนักชีแบบเรียกซ้ำพื้นฐานมี time เป็น O(2^n) — ช้ามากจนใช้งานไม่ได้สำหรับ n ที่มีค่ามาก เวอร์ชันแบบวนซ้ำมี time เป็น O(n) และใช้พื้นที่ O(1) การเรียกซ้ำที่จดจำผลลัพธ์ (ในบทเรียนถัดไป) ก็มี time เป็น O(n) แต่ใช้พื้นที่ O(n) เนื่องจากพจนานุกรมบันทึกผลและสแตก O(n) สำหรับฟีโบนักชี วิธีแบบวนซ้ำเหมาะสมที่สุดในทุกด้าน เมื่อ n = 50 การเรียกซ้ำแบบพื้นฐานใช้เวลาหลายวินาที แต่วิธีแบบวนซ้ำใช้เวลาเพียงไมโครวินาที

import time

def fib_rec(n):
    if n <= 1: return n
    return fib_rec(n-1) + fib_rec(n-2)   # O(2^n)

def fib_iter(n):
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a                              # O(n) time, O(1) space

# Timing comparison for n=35
start = time.time()
fib_rec(35)
print(f'Recursive n=35: {time.time()-start:.3f}s')

start = time.time()
fib_iter(35)
print(f'Iterative n=35: {time.time()-start:.6f}s')

print(fib_iter(100))  # handles large n

การท่องผ่านต้นไม้: แบบเรียกซ้ำเทียบกับแบบวนซ้ำ

การท่องผ่านต้นไม้แบบเรียกซ้ำนั้นอ่านเข้าใจง่ายโดยธรรมชาติ เพราะโครงสร้างต้นไม้สอดคล้องกับการเรียกซ้ำ แต่สำหรับต้นไม้ที่เอียงมากและลึก (ซึ่งโดยพื้นฐานแล้วคล้ายลิงก์ลิสต์) ความลึกของการเรียกซ้ำจะเท่ากับความสูงของต้นไม้ = O(n) จึงเสี่ยงทำให้สแตกเต็ม ส่วนแบบวนซ้ำที่ใช้สแตกอย่างชัดเจนจะไม่มีข้อจำกัดด้านความลึก และทำให้ขนาดสแตกขยายบนฮีปแทนที่จะใช้สแตกการเรียกใช้

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val   = val; self.left = left; self.right = right

def preorder_rec(root, result=None):
    if result is None: result = []
    if root:
        result.append(root.val)
        preorder_rec(root.left, result)
        preorder_rec(root.right, result)
    return result

def preorder_iter(root):
    if not root: return []
    result, stack = [], [root]
    while stack:
        node = stack.pop()
        result.append(node.val)
        if node.right: stack.append(node.right)
        if node.left:  stack.append(node.left)
    return result

root = TreeNode(1, TreeNode(2, TreeNode(4), TreeNode(5)), TreeNode(3))
print(preorder_rec(root))   # [1, 2, 4, 5, 3]
print(preorder_iter(root))  # [1, 2, 4, 5, 3]

การเรียงลำดับแบบผสาน: แบบเรียกซ้ำเทียบกับแบบวนซ้ำ (จากล่างขึ้นบน)

การเรียงลำดับแบบผสานเหมาะกับการเขียนแบบเรียกซ้ำโดยธรรมชาติ (แบ่ง เรียกซ้ำ แล้วผสาน) ส่วนการเรียงลำดับแบบผสานจากล่างขึ้นบนแบบวนซ้ำจะไม่ใช้การเรียกซ้ำเลย โดยเริ่มจากอาร์เรย์ย่อยขนาด 1 ผสานคู่ที่อยู่ติดกันให้เป็นอาร์เรย์ย่อยขนาด 2 จากนั้นเป็นขนาด 4 และต่อไปเรื่อย ๆ โดยเพิ่มขนาดอาร์เรย์ย่อยเป็นสองเท่าในแต่ละรอบ การเรียงลำดับแบบผสานจากล่างขึ้นบนใช้เวลา O(n log n) ใช้พื้นที่ O(n) (สำหรับบัฟเฟอร์การผสาน) และใช้พื้นที่สแตก O(1)

def merge_sort_iterative(arr):
    n = len(arr)
    size = 1
    while size < n:
        for start in range(0, n, 2 * size):
            mid   = min(start + size, n)
            end   = min(start + 2 * size, n)
            left  = arr[start:mid]
            right = arr[mid:end]
            # Merge
            i = j = 0
            for k in range(start, end):
                if i < len(left) and (j >= len(right) or left[i] <= right[j]):
                    arr[k] = left[i]; i += 1
                else:
                    arr[k] = right[j]; j += 1
        size *= 2
    return arr

print(merge_sort_iterative([5, 2, 4, 6, 1, 3]))  # [1,2,3,4,5,6]

เมื่อการเรียกซ้ำดีกว่าอย่างชัดเจน

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

# Recursion is clearest for JSON-like nested structures
def flatten(nested):
    result = []
    for item in nested:
        if isinstance(item, list):
            result.extend(flatten(item))  # recurse on sub-list
        else:
            result.append(item)
    return result

print(flatten([1, [2, [3, 4], 5], 6]))  # [1, 2, 3, 4, 5, 6]
print(flatten([]))                        # []
print(flatten([[1, [2]], [3, [4, [5]]]])) # [1, 2, 3, 4, 5]

เมื่อการวนซ้ำดีกว่าอย่างชัดเจน

ควรเลือกการวนซ้ำเมื่อ: ความลึกเป็น O(n) และ n มีค่ามาก (มากกว่า ~500 ในโค้ดไพธอนที่ปลอดภัย), แบบเรียกซ้ำและแบบวนซ้ำอ่านเข้าใจได้พอ ๆ กัน (ฟีโบนัชชี แฟกทอเรียล) หรือปัญหานั้นเป็นลำดับต่อเนื่องโดยพื้นฐานและไม่มีการแยกเป็นปัญหาย่อยอย่างเป็นธรรมชาติ ลูปง่าย ๆ ที่ประมวลผลอาร์เรย์จากซ้ายไปขวา — เช่น ผลรวมสะสม หน้าต่างเลื่อน และตัวชี้สองตัว — ควรใช้การวนซ้ำเสมอ

# Iterative is clearest for sequential array processing
def running_max(nums):
    result = []
    curr_max = float('-inf')
    for n in nums:
        curr_max = max(curr_max, n)
        result.append(curr_max)
    return result

print(running_max([3, 1, 4, 1, 5, 9, 2, 6]))  # [3,3,4,4,5,9,9,9]

# No natural recursion here — iteration is the only sensible choice

การเปลี่ยนการเรียกซ้ำของ DFS ให้เป็นการวนซ้ำ

แนวทางที่เป็นระบบคือ การเรียกซ้ำของ DFS ทุกแบบสามารถเปลี่ยนเป็นแบบวนซ้ำได้ด้วยการนำอาร์กิวเมนต์ของการเรียกซ้ำไปใส่ไว้ในสแตกอย่างชัดเจน แนวคิดสำคัญคือ การเรียกซ้ำ f(args) มีความหมายเทียบเท่ากับการใส่ args ลงในสแตกแล้ววนซ้ำ สำหรับการประมวลผลแบบหลังลำดับ (ซึ่งต้องใช้ผลลัพธ์จากลูกก่อนประมวลผลโหนดแม่) อาจต้องใช้แนวทางสองรอบหรือแฟล็กการเยี่ยมชม

# Post-order iterative using two stacks
class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val=val; self.left=left; self.right=right

def postorder_iter(root):
    if not root: return []
    s1, s2 = [root], []
    while s1:
        node = s1.pop()
        s2.append(node.val)
        if node.left:  s1.append(node.left)
        if node.right: s1.append(node.right)
    return s2[::-1]  # reverse gives post-order

root = TreeNode(1, TreeNode(2, TreeNode(4), TreeNode(5)), TreeNode(3))
print(postorder_iter(root))  # [4, 5, 2, 3, 1]

ค่าใช้จ่ายส่วนเกินของการเรียกซ้ำ

การเรียกซ้ำแต่ละครั้งในไพธอนมีค่าใช้จ่ายส่วนเกินไม่น้อย: จะมีการสร้างเฟรมใหม่ (ซึ่งจัดสรรหน่วยความจำบนฮีป) ตัวแปรเฉพาะที่จะถูกกำหนดค่าเริ่มต้น และมีการจัดเก็บตัวชี้ที่อยู่สำหรับการคืนกลับ การทดสอบประสิทธิภาพแสดงให้เห็นว่า ค่าใช้จ่ายของการเรียกใช้ฟังก์ชันในไพธอนอยู่ที่ประมาณ 100–200 นาโนวินาทีต่อครั้ง สำหรับความลึกของการเรียกซ้ำ 10^6 ครั้ง ค่าใช้จ่ายนี้จะรวมเป็น 0.1–0.2 วินาทีของค่าใช้จ่ายส่วนเกินล้วน ๆ โดยไม่ขึ้นกับงานของอัลกอริทึม ลูปแบบวนซ้ำจะหลีกเลี่ยงค่าใช้จ่ายนี้ได้ทั้งหมด

import time

def rec_sum(n):
    if n == 0: return 0
    return n + rec_sum(n - 1)

def iter_sum(n):
    total = 0
    for i in range(n + 1):
        total += i
    return total

import sys; sys.setrecursionlimit(10000)

n = 5000
start = time.time()
for _ in range(100): rec_sum(n)
print(f'Recursive sum({n}) x100: {(time.time()-start)*1000:.2f}ms')

start = time.time()
for _ in range(100): iter_sum(n)
print(f'Iterative sum({n}) x100: {(time.time()-start)*1000:.2f}ms')

การตัดสินใจในการสัมภาษณ์

ในการสัมภาษณ์การเขียนโปรแกรม หากเลือกได้ ให้ถามว่า: "ความลึกของการเรียกซ้ำมีขอบเขตเป็น O(log n) หรือไม่" หากใช่ การเรียกซ้ำก็เหมาะสม "ความลึกของการเรียกซ้ำเป็น O(n) หรือไม่" — ควรเลือกการวนซ้ำ หรือกล่าวว่าจะเปลี่ยนเป็นแบบวนซ้ำเมื่อนำไปใช้งานจริง "ปัญหานี้มีรูปทรงเป็นต้นไม้หรือเป็นการแบ่งแล้วพิชิตโดยธรรมชาติหรือไม่" — ให้เอนเอียงไปทางการเรียกซ้ำ "ปัญหานี้เป็นการสแกนตามลำดับหรือไม่" — ให้ใช้การวนซ้ำ

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

สรุป: ตารางข้อแลกเปลี่ยน

สรุปข้อแลกเปลี่ยน: โค้ดแบบเรียกซ้ำมักสั้นกว่าและสะท้อนโครงสร้างของปัญหา แต่ใช้พื้นที่สแตก O(ความลึก) และมีค่าใช้จ่ายจากการเรียกใช้ฟังก์ชัน โค้ดแบบวนซ้ำยาวกว่า แต่ใช้พื้นที่สแตก O(1) และหลีกเลี่ยงข้อจำกัดของการเรียกซ้ำ การเรียกซ้ำที่จดจำผลลัพธ์ (บทถัดไป) เป็นแนวทางตรงกลาง ซึ่งยังคงความชัดเจนของการเรียกซ้ำไว้พร้อมกำจัดการคำนวณซ้ำที่ไม่จำเป็น ควรระบุความซับซ้อนด้านพื้นที่ให้ชัดเจนเสมอ โดยรวมพื้นที่ของสแตกการเรียกใช้ไว้ด้วยเมื่อวิเคราะห์คำตอบของคุณ

rows = [
    ('Factorial',   'O(n) / O(1)', 'O(n) / O(1)', 'Same time; iter wins on space'),
    ('Fibonacci',   'O(2^n) / O(n)', 'O(n) / O(1)', 'Iter massively wins'),
    ('Binary search','O(log n) / O(log n)', 'O(log n) / O(1)', 'Iter wins on space'),
    ('Tree DFS',    'O(n) / O(h)',  'O(n) / O(h)', 'Equal; rec cleaner'),
    ('Merge sort',  'O(n log n) / O(log n)', 'O(n log n) / O(1)', 'BU-iter wins on stack'),
]
for name, rec, it, note in rows:
    print(f'{name:<15} rec={rec:<22} iter={it:<22} {note}')

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

ทดสอบความเข้าใจแนวคิดโครงสร้างข้อมูลและอัลกอริทึม — การเตรียมสัมภาษณ์การเขียนโปรแกรมจากบทเรียนนี้

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

ในบทเรียนนี้ คุณได้เรียนรู้ว่า: ควรใช้การเรียกซ้ำเมื่อความลึกเป็น O(log n) หรือเมื่อปัญหามีรูปทรงเป็นต้นไม้โดยธรรมชาติ และควรใช้การวนซ้ำเมื่อความลึกเป็น O(n) หรือเมื่อปัญหาเป็นลำดับต่อเนื่อง, ฟีโบนัชชีแบบเรียกซ้ำอย่างตรงไปตรงมามีเวลา O(2^n) — ส่วนแบบวนซ้ำใช้เวลา O(n) และพื้นที่ O(1) และ DFS แบบเรียกซ้ำทุกแบบสามารถเปลี่ยนเป็นแบบวนซ้ำได้ด้วยการจัดการสแตกอย่างชัดเจนบนฮีป ต่อไปเราจะใช้การจดจำผลลัพธ์เพื่อกำจัดการเรียกซ้ำที่ไม่จำเป็น

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

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

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

คุณจะเรียนรู้อะไรในบทเรียน “การแลกเปลี่ยนระหว่างแบบเรียกซ้ำกับแบบวนซ้ำ”

แปลงแฟกทอเรียลและฟีโบนัชชีแบบเรียกซ้ำเป็นลูปแบบวนซ้ำ และอธิบายว่าเมื่อใดขีดจำกัดการเรียกซ้ำและขนาดสแตกของ Python ทำให้ควรใช้การวนซ้ำ คุณปฏิบัติ 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. กรอบการเรียกซ้ำ: กรณีฐาน ความเชื่อมั่น การสร้าง
  2. การแสดงภาพสแตกการเรียก
  3. การแลกเปลี่ยนระหว่างแบบเรียกซ้ำกับแบบวนซ้ำ
  4. การจดจำผลลัพธ์: แคชผลลัพธ์จากการเรียกซ้ำ
← กลับไปที่ Coding Interview Prep