การเรียกซ้ำแบบหางคืออะไร


52

ฉันรู้ว่าแนวคิดทั่วไปของการเรียกซ้ำ ฉันเจอแนวคิดเรื่องการเรียกซ้ำหางในขณะที่ศึกษาอัลกอริทึมแบบเร่งด่วน ในวิดีโอของอัลกอริทึมการเรียงลำดับด่วนจาก MITเวลา 18:30 วินาทีอาจารย์บอกว่านี่เป็นอัลกอริทึมแบบเรียกซ้ำ ไม่ชัดเจนสำหรับฉันว่าการเรียกซ้ำแบบหางหมายถึงอะไรจริงๆ

บางคนสามารถอธิบายแนวคิดด้วยตัวอย่างที่เหมาะสมได้หรือไม่

คำตอบบางอย่างให้โดยชุมชนดังนั้นที่นี่


บอกให้เราทราบเพิ่มเติมเกี่ยวกับบริบทที่คุณได้พบในระยะหาง recursion เชื่อมโยง? อ้างอิง?
A.Schulz

@ A.Schulz ฉันได้ใส่ลิงค์ไปยังบริบท
Geek

5
ดู " หาง recursion คืออะไร? " ใน StackOverflow
Vor

2
@ajmartin คำถามคือเส้นขอบบนStack Overflowแต่เน้นหัวข้อวิทยาศาสตร์คอมพิวเตอร์อย่างแน่นหนาดังนั้นในหลักการวิทยาศาสตร์คอมพิวเตอร์ควรให้คำตอบที่ดีกว่า มันไม่ได้เกิดขึ้นที่นี่ แต่ก็ยังโอเคที่จะถามคำถามใหม่ที่นี่ด้วยความหวังว่าจะได้คำตอบที่ดีกว่า เกินบรรยายคุณควรพูดถึงคำถามก่อนหน้านี้ของคุณใน SO ดังนั้นเพื่อให้ผู้คนไม่พูดซ้ำสิ่งที่พูดไปแล้ว
Gilles 'หยุดความชั่วร้าย'

1
นอกจากนี้คุณควรพูดว่าอะไรคือส่วนที่ไม่ชัดเจนหรือทำไมคุณไม่พอใจกับคำตอบก่อนหน้านี้ฉันคิดว่าผู้คนใน SO ให้คำตอบที่ดี แต่สิ่งที่ทำให้คุณถามอีกครั้ง

คำตอบ:


52

Tail recursion เป็นกรณีพิเศษของการเรียกซ้ำที่ฟังก์ชันการเรียกใช้ไม่มีการคำนวณเพิ่มเติมหลังจากทำการเรียกซ้ำ ตัวอย่างเช่นฟังก์ชั่น

int f (int x, int y) {
  ถ้า (y == 0) {
    ส่งคืน x
  }

  ส่งคืน f (x * y, y-1);
}

คือ tail recursive (เนื่องจากคำสั่งสุดท้ายคือ recursive call) ในขณะที่ฟังก์ชันนี้ไม่ใช่ tail recursive:

int g (int x) {
  ถ้า (x == 1) {
    คืน 1
  }

  int y = g (x-1);

  ส่งคืน x * y;
}

เนื่องจากมันทำการคำนวณบางอย่างหลังจากที่การเรียกซ้ำได้ส่งกลับ

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


2
คุณเขียน "นั่นหมายความว่าเราไม่จำเป็นต้องมี call stack เลยสำหรับการโทรซ้ำทั้งหมด" Call stack จะอยู่ที่นั่นเสมอไม่จำเป็นต้องเขียนที่อยู่ผู้ส่งคืนใน call stack ใช่ไหม
Geek

2
มันขึ้นอยู่กับรูปแบบการคำนวณของคุณในระดับหนึ่ง :) แต่ใช่บนคอมพิวเตอร์จริง ๆ call call ยังคงอยู่เราก็ไม่ได้ใช้มัน
Matt Lewis

เกิดอะไรขึ้นถ้ามันเป็นสายสุดท้าย แต่ในสำหรับวง ดังนั้นคุณทำการคำนวณทั้งหมดของคุณด้านบน แต่บางส่วนของพวกเขาในการวนรอบเช่นdef recurse(x): if x < 0 return 1; for i in range 100{ (do calculations) recurse(x)}
thed0ctor

13

กล่าวง่ายๆคือการเรียกซ้ำหางเป็นการเรียกซ้ำที่คอมไพเลอร์สามารถแทนที่การเรียกซ้ำด้วยคำสั่ง "goto" ดังนั้นเวอร์ชันที่คอมไพล์จะไม่ต้องเพิ่มความลึกของสแต็ก

บางครั้งการออกแบบฟังก์ชั่น tail-recursive คุณจำเป็นต้องสร้างฟังก์ชั่นตัวช่วยพร้อมพารามิเตอร์เพิ่มเติม

ตัวอย่างเช่นนี่ไม่ใช่ฟังก์ชันแบบเรียกซ้ำ:

int factorial(int x) {
    if (x > 0) {
        return x * factorial(x - 1);
    }
    return 1;
}

แต่นี่เป็นฟังก์ชั่นแบบเรียกซ้ำ:

int factorial(int x) {
    return tailfactorial(x, 1);
}

int tailfactorial(int x, int multiplier) {
    if (x > 0) {
        return tailfactorial(x - 1, x * multiplier);
    }
    return multiplier;
}

เพราะคอมไพเลอร์สามารถเขียนฟังก์ชัน recursive ไปยังฟังก์ชันที่ไม่ใช่แบบเรียกซ้ำโดยใช้บางสิ่งเช่นนี้ (pseudocode):

int tailfactorial(int x, int multiplier) {
    start:
    if (x > 0) {
        multiplier = x * multiplier;
        x--;
        goto start;
    }
    return multiplier;
}

กฎสำหรับคอมไพเลอร์นั้นง่ายมาก: เมื่อคุณพบ " return thisfunction(newparameters);" ให้แทนที่ด้วย " parameters = newparameters; goto start;" แต่สามารถทำได้เฉพาะเมื่อค่าที่ส่งคืนโดยการเรียกซ้ำเรียกคืนโดยตรง

หากการเรียกซ้ำทั้งหมดในฟังก์ชันสามารถเปลี่ยนได้เช่นนี้แสดงว่าเป็นฟังก์ชันแบบเรียกซ้ำ


13

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

วิธี A: กระบวนการวนซ้ำแบบเชิงเส้น

(define (factorial n)
 (if (= n 1)
  1
  (* n (factorial (- n 1)))))

รูปร่างของกระบวนการสำหรับApproach Aมีลักษณะดังนี้:

(factorial 5)
(* 5 (factorial 4))
(* 5 (* 4 (factorial 3)))
(* 5 (* 4 (* 3 (factorial 2))))
(* 5 (* 4 (* 3 (* 2 (factorial 1)))))
(* 5 (* 4 (* 3 (* 2 (* 1)))))
(* 5 (* 4 (* 3 (* 2))))
(* 5 (* 4 (* 6)))
(* 5 (* 24))
120

วิธีการ B: กระบวนการวนซ้ำเชิงเส้น

(define (factorial n)
 (fact-iter 1 1 n))

(define (fact-iter product counter max-count)
 (if (> counter max-count)
  product
  (fact-iter (* counter product)
             (+ counter 1)
             max-count)))

รูปร่างของกระบวนการสำหรับApproach Bมีลักษณะดังนี้:

(factorial 5)
(fact-iter 1 1 5)
(fact-iter 1 2 5)
(fact-iter 2 3 5)
(fact-iter 6 4 5)
(fact-iter 24 5 5)
(fact-iter 120 6 5)
120

กระบวนการวนซ้ำเชิงเส้น (Approach B) ทำงานในพื้นที่คงที่แม้ว่ากระบวนการนั้นจะเป็นกระบวนการแบบเรียกซ้ำ ควรสังเกตว่าในแนวทางนี้ตัวแปรชุดจะกำหนดสถานะของกระบวนการที่จุดใด ๆ {product, counter, max-count}. นี่เป็นเทคนิคที่การเรียกซ้ำแบบหางอนุญาตการปรับให้เหมาะสมของคอมไพเลอร์

ในวิธีการ A มีข้อมูลที่ซ่อนอยู่มากกว่าซึ่งล่ามเก็บรักษาซึ่งเป็นห่วงโซ่ของการปฏิบัติการรอการตัดบัญชี


5

Tail-recursion เป็นรูปแบบของการเรียกซ้ำซึ่งการเรียกซ้ำเป็นคำสั่งสุดท้ายในฟังก์ชั่น (นั่นคือที่ส่วนหางมาจาก) นอกจากนี้การเรียกซ้ำจะต้องไม่ประกอบด้วยการอ้างอิงไปยังเซลล์หน่วยความจำที่เก็บค่าก่อนหน้า (การอ้างอิงอื่นที่ไม่ใช่พารามิเตอร์ของฟังก์ชัน) ด้วยวิธีนี้เราไม่สนใจค่าก่อนหน้าและพอเพียงหนึ่งเฟรมสแต็กสำหรับการเรียกซ้ำทั้งหมด tail-recursion เป็นวิธีหนึ่งในการเพิ่มประสิทธิภาพอัลกอริทึมแบบเรียกซ้ำ ข้อดี / การเพิ่มประสิทธิภาพอื่น ๆ คือมีวิธีที่ง่ายในการแปลงอัลกอริทึมแบบเรียกซ้ำแบบหางเป็นแบบที่เทียบเท่าซึ่งใช้การวนซ้ำแทนการเรียกซ้ำ ใช่แล้วอัลกอริทึมสำหรับ quicksort เป็นแบบเรียกซ้ำได้แน่นอน

QUICKSORT(A, p, r)
    if(p < r)
    then
        q = PARTITION(A, p, r)
        QUICKSORT(A, p, q–1)
        QUICKSORT(A, q+1, r)

นี่คือรุ่นที่ซ้ำกัน:

QUICKSORT(A)
    p = 0, r = len(A) - 1
    while(p < r)
        q = PARTITION(A, p, r)
        r = q - 1

    p = 0, r = len(A) - 1
    while(p < r)
        q = PARTITION(A, p, r)
        p = q + 1
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.