การทำความเข้าใจว่าฟังก์ชันแบบเรียกซ้ำทำงานอย่างไร


115

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

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

นี่คือฟังก์ชั่น (ไวยากรณ์ถูกเขียนด้วย Swift):

func sumInts(a: Int, b: Int) -> Int {
    if (a > b) {
        return 0
    } else {
        return a + sumInts(a: a + 1, b: b)
    }
}

เราจะใช้ 2 และ 5 เป็นอาร์กิวเมนต์ของเรา:

println(sumInts(a: 2, b: 5))

เห็นได้ชัดว่าคำตอบคือ 14 แต่ฉันไม่ชัดเจนว่าค่านั้นบรรลุได้อย่างไร

นี่คือ 2 แฮงก์ของฉัน:

  1. ฟังก์ชันถูกเรียกซ้ำจนกว่าจะตรงตามเงื่อนไข เงื่อนไขนั้นคือ a> b เมื่อตรงตามเงื่อนไขนี้ให้ส่งคืน 0 เมื่อดูแวบแรกฉันคาดว่าค่าที่ส่งคืนจะเป็น 0 ซึ่งไม่ถูกต้องอย่างเห็นได้ชัด

  2. การพิมพ์ค่าของ 'a' ในการทำซ้ำแต่ละครั้งจะให้ค่าที่ฉันคาดหวัง: 2, 3, 4, 5 (ที่จุด 5 + ​​1> b ซึ่งตรงตามเงื่อนไขแรก: a> b) แต่ฉันก็ยังไม่ ' มองไม่เห็นว่าค่า 14 ได้มาอย่างไร

ความคิดแรกของฉันคือสิ่งที่คล้ายกับสิ่งต่อไปนี้กำลังเกิดขึ้นอย่างน่าอัศจรรย์:

var answer = a;
answer += a+1 until a > b;
return answer;   

ฉันไม่ได้รับบางสิ่งบางอย่าง ฉันชอบที่จะเข้าใจสิ่งที่เกิดขึ้นมากกว่าแค่โดยปริยาย

หากมีใครสามารถอธิบายได้ว่าเกิดอะไรขึ้นในทางเทคนิคระหว่างฟังก์ชันประเภทนี้และทำไมผลลัพธ์ถึงไม่เป็น 0 และในที่สุดa + sumInts(a: a + 1, b: b) = 14ฉันก็จะเป็นหนี้ของคุณตลอดไป


7
การเรียกซ้ำเป็นหนึ่งในแนวคิดการเขียนโปรแกรมที่เข้าใจง่ายกว่าในรูปแบบรหัส มีคำจำกัดความที่ดีที่นี่
blgt

5
LearnYouARecursion, โจทย์ครบชุดจากศาสตราจารย์ระดับโลก!
recursion.ninja

15
ฉันแค่ ต้องขอให้คุณพิมพ์ "การเรียกซ้ำ" ในช่องค้นหาของ Google หนึ่งในไข่อีสเตอร์ ฉันจะไม่ทำให้คุณประหลาดใจ
Floris

7
อาจซ้ำกันของ stackoverflow.com/questions/25676961/…
Neil McGuigan

คำตอบ:


107

ฉันคิดว่าความสับสนเกิดจากการคิดว่ามันเป็น "ฟังก์ชันเดียวกัน" ที่ถูกเรียกหลายครั้ง หากคุณคิดว่ามันเป็น "หลายสำเนาของฟังก์ชันเดียวกันที่ถูกเรียก" มันอาจจะชัดเจนกว่านี้:

มีเพียงสำเนาเดียวของฟังก์ชันที่คืนค่า 0 และไม่ใช่ฟังก์ชันแรก (เป็นฟังก์ชันสุดท้าย) ดังนั้นผลลัพธ์ของการเรียกตัวแรกจึงไม่ใช่ 0

สำหรับความสับสนเล็กน้อยฉันคิดว่าการสะกดคำซ้ำเป็นภาษาอังกฤษจะง่ายกว่า อ่านบรรทัดนี้:

return a + sumInts(a + 1, b: b)

เป็น "ส่งคืนค่าของ 'a' บวก (ค่าส่งคืนของสำเนาอื่นของฟังก์ชันซึ่งเป็นค่าของสำเนาของ 'a' บวก (ค่าส่งคืนของสำเนาอื่นของฟังก์ชันซึ่งเป็นค่าสำเนาที่สองของ ' a 'plus (... "โดยแต่ละสำเนาของฟังก์ชันจะวางสำเนาใหม่ของตัวเองโดยเพิ่มขึ้น 1 จนกว่าจะตรงตามเงื่อนไข a> b

เมื่อคุณไปถึงเงื่อนไข a> b ที่เป็นจริงคุณจะมีสำเนาของฟังก์ชันจำนวนมาก (ที่อาจเกิดขึ้นเองโดยพลการ) ในช่วงกลางของการทำงานทั้งหมดรอผลของสำเนาถัดไปเพื่อค้นหาสิ่งที่พวกเขา ควรเพิ่มใน "a"

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


7
Catfish_Man: ฉันคิดว่าคุณตอกมัน! การคิดว่ามันเป็น "สำเนา" ของฟังก์ชันเดียวกันหลาย ๆ ฉันยังคงห่อหัวอยู่ แต่ฉันคิดว่าคุณส่งฉันไปถูกทางแล้ว! ขอขอบคุณที่สละเวลาว่างจากวันที่วุ่นวายเพื่อช่วยเหลือเพื่อนโปรแกรมเมอร์! ฉันจะทำเครื่องหมายคำตอบของคุณว่าเป็นคำตอบที่ถูกต้อง มีวันที่ดี!
Jason Elwood

13
นี่เป็นการเปรียบเทียบที่ดีแม้ว่าโปรดระวังอย่าใช้มันมากเกินไปเนื่องจาก "สำเนา" แต่ละรายการเป็นรหัสเดียวกันทุกประการ สิ่งที่แตกต่างกันสำหรับแต่ละสำเนาคือข้อมูลทั้งหมดที่ทำงานอยู่
Tim B

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

5
คำศัพท์ที่ถูกต้องคือมีการเรียกใช้ฟังก์ชันต่างๆ การเรียกแต่ละครั้งมีอินสแตนซ์ของตัวแปรaและb.
Theodore Norvell

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

130

1. ฟังก์ชันนี้เรียกซ้ำจนกว่าจะตรงตามเงื่อนไข เงื่อนไขนั้นคือa > b. เมื่อตรงตามเงื่อนไขนี้ให้ส่งคืน 0 เมื่อดูแวบแรกฉันคาดว่าค่าที่ส่งคืนจะเป็น 0 ซึ่งไม่ถูกต้องอย่างเห็นได้ชัด

นี่คือสิ่งที่คอมพิวเตอร์คอมพิวเตอร์sumInts(2,5)จะคิดหากสามารถ:

I want to compute sumInts(2, 5)
for this, I need to compute sumInts(3, 5)
and add 2 to the result.
  I want to compute sumInts(3, 5)
  for this, I need to compute sumInts(4, 5)
  and add 3 to the result.
    I want to compute sumInts(4, 5)
    for this, I need to compute sumInts(5, 5)
    and add 4 to the result.
      I want to compute sumInts(5, 5)
      for this, I need to compute sumInts(6, 5)
      and add 5 to the result.
        I want to compute sumInts(6, 5)
        since 6 > 5, this is zero.
      The computation yielded 0, therefore I shall return 5 = 5 + 0.
    The computation yielded 5, therefore I shall return 9 = 4 + 5.
  The computation yielded 9, therefore I shall return 12 = 3 + 9.
The computation yielded 12, therefore I shall return 14 = 2 + 12.

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

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

2. การพิมพ์ค่า 'a' ในการทำซ้ำแต่ละครั้งจะให้ค่าที่ฉันคาดหวัง: 2, 3, 4, 5 (ที่จุด 5 + ​​1> b ซึ่งตรงตามเงื่อนไขแรก: a> b) แต่ฉันก็ยัง ไม่เห็นว่าค่าของ 14 ได้มาอย่างไร

เนื่องจากค่าที่ส่งคืนไม่ใช่aตัวมันเอง แต่เป็นผลรวมของมูลค่าของaและค่าที่ส่งคืนโดยการเรียกซ้ำ


3
ขอขอบคุณที่สละเวลาเขียนคำตอบที่ยอดเยี่ยมนี้ Michael! +1!
Jason Elwood

9
@JasonElwood อาจจะมีประโยชน์ถ้าคุณปรับเปลี่ยนsumIntsเพื่อให้มันเขียน "ความคิดของคอมพิวเตอร์" ลงไป เมื่อคุณเขียนฟังก์ชันดังกล่าวได้แล้วคุณอาจจะต้อง“ เข้าใจ”!
Michael Le Barbier Grünewald

4
นี่เป็นคำตอบที่ดีแม้ว่าฉันจะทราบว่าไม่มีข้อกำหนดว่าการเปิดใช้งานฟังก์ชันจะเกิดขึ้นกับโครงสร้างข้อมูลที่เรียกว่า "the stack" การเรียกซ้ำสามารถทำได้โดยใช้รูปแบบการส่งต่อแบบต่อเนื่องซึ่งในกรณีนี้จะไม่มีสแต็กเลย สแต็กเป็นเพียงหนึ่งเดียว - มีประสิทธิภาพโดยเฉพาะดังนั้นในการใช้งานทั่วไป - การสร้างแนวคิดเรื่องความต่อเนื่อง
Eric Lippert

1
@EricLippert ขณะที่เทคนิคที่ใช้ในการดำเนินการ recursivity เป็นหัวข้อที่น่าสนใจต่อ seผมไม่แน่ใจว่ามันจะเป็นประโยชน์สำหรับ OP-ที่ต้องการที่จะเข้าใจ“วิธีการทำงาน” -to ได้สัมผัสกับความหลากหลายของกลไกที่ใช้ ในขณะที่ความต่อเนื่องผ่านรูปแบบหรือภาษาตามการขยายตัว (เช่นเท็กซ์และ M4) จะไม่ยิ่งหนักกว่ากระบวนทัศน์การเขียนโปรแกรมที่พบบ่อยมากขึ้นผมจะไม่กระทำผิดกฎหมายทุกคนโดยการติดฉลากเหล่านี้“ธรรมดา” และน้อยโกหกสีขาวเช่น“ก็มักจะเกิดขึ้นในสแต็ค” ควร ช่วย OP ให้เข้าใจแนวคิด (และประเภทของสแต็คมีส่วนเกี่ยวข้องเสมอ)
Michael Le Barbier Grünewald

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

48

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

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

ให้ฉันแสดงขั้นตอน:

sumInts(a: 2, b: 5) will return: 2 + sumInts(a: 3, b: 5)
sumInts(a: 3, b: 5) will return: 3 + sumInts(a: 4, b: 5)
sumInts(a: 4, b: 5) will return: 4 + sumInts(a: 5, b: 5)
sumInts(a: 5, b: 5) will return: 5 + sumInts(a: 6, b: 5)
sumInts(a: 6, b: 5) will return: 0

เมื่อ sumInts (a: 6, b: 5) ได้ดำเนินการแล้วผลลัพธ์สามารถคำนวณได้ดังนั้นการสำรองข้อมูลเชนด้วยผลลัพธ์ที่คุณได้รับ:

 sumInts(a: 6, b: 5) = 0
 sumInts(a: 5, b: 5) = 5 + 0 = 5
 sumInts(a: 4, b: 5) = 4 + 5 = 9
 sumInts(a: 3, b: 5) = 3 + 9 = 12
 sumInts(a: 2, b: 5) = 2 + 12 = 14.

อีกวิธีหนึ่งในการแสดงโครงสร้างของการเรียกซ้ำ:

 sumInts(a: 2, b: 5) = 2 + sumInts(a: 3, b: 5)
 sumInts(a: 2, b: 5) = 2 + 3 + sumInts(a: 4, b: 5)  
 sumInts(a: 2, b: 5) = 2 + 3 + 4 + sumInts(a: 5, b: 5)  
 sumInts(a: 2, b: 5) = 2 + 3 + 4 + 5 + sumInts(a: 6, b: 5)
 sumInts(a: 2, b: 5) = 2 + 3 + 4 + 5 + 0
 sumInts(a: 2, b: 5) = 14 

2
ใส่ได้ดีมาก Rob คุณได้วางไว้อย่างชัดเจนและเข้าใจง่าย ขอบคุณที่สละเวลา!
Jason Elwood

3
นี่คือการแสดงที่ชัดเจนที่สุดของสิ่งที่เกิดขึ้นโดยไม่ต้องเข้าไปดูรายละเอียดทางทฤษฎีและทางเทคนิคของมันแสดงให้เห็นแต่ละขั้นตอนของการดำเนินการอย่างชัดเจน
Bryan

2
ฉันดีใจ. :) ไม่ใช่เรื่องง่ายเสมอไปที่จะอธิบายสิ่งเหล่านี้ ขอบคุณสำหรับคำชม.
Rob

1
+1 นี่คือวิธีที่ฉันจะอธิบายโดยเฉพาะกับตัวอย่างสุดท้ายของโครงสร้าง การคลายภาพสิ่งที่เกิดขึ้นเป็นประโยชน์
KChaloux

40

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

รหัสที่คุณระบุที่นี่ช่วยแก้ปัญหาต่อไปนี้: คุณต้องการทราบผลรวมของจำนวนเต็มทั้งหมดจาก a ถึง b รวม ตัวอย่างเช่นคุณต้องการผลรวมของตัวเลขตั้งแต่ 2 ถึง 5 รวมกันซึ่งก็คือ

2 + 3 + 4 + 5

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

2 + (3 + 4 + 5)

ที่นี่ (3 + 4 + 5) เป็นผลรวมของจำนวนเต็มทั้งหมดระหว่าง 3 ถึง 5 กล่าวอีกนัยหนึ่งถ้าคุณต้องการทราบผลรวมของจำนวนเต็มทั้งหมดระหว่าง 2 ถึง 5 ให้เริ่มต้นด้วยการคำนวณผลรวมของจำนวนเต็มทั้งหมดระหว่าง 3 ถึง 5 จากนั้นจึงเพิ่ม 2

แล้วคุณจะคำนวณผลรวมของจำนวนเต็มทั้งหมดระหว่าง 3 ถึง 5 ได้อย่างไร? ผลรวมนั้นคือ

3 + 4 + 5

ซึ่งสามารถคิดแทนได้ว่า

3 + (4 + 5)

ในที่นี้ (4 + 5) คือผลรวมของจำนวนเต็มทั้งหมดระหว่าง 4 ถึง 5 ดังนั้นหากคุณต้องการคำนวณผลรวมของตัวเลขทั้งหมดระหว่าง 3 ถึง 5 รวมคุณต้องคำนวณผลรวมของจำนวนเต็มทั้งหมดระหว่าง 4 ถึง 5 แล้วบวก 3

มีรูปแบบที่นี่! หากคุณต้องการคำนวณผลรวมของจำนวนเต็มระหว่าง a และ b รวมคุณสามารถดำเนินการดังต่อไปนี้ ขั้นแรกให้คำนวณผลรวมของจำนวนเต็มระหว่าง a + 1 และ b รวม จากนั้นเพิ่ม a ในผลรวมนั้น คุณจะสังเกตเห็นว่า "คำนวณผลรวมของจำนวนเต็มระหว่าง a + 1 และ b รวม" เป็นปัญหาเดียวกันกับที่เรากำลังพยายามแก้ไขอยู่ แต่มีพารามิเตอร์ที่แตกต่างกันเล็กน้อย แทนที่จะคำนวณจาก a ถึง b เรากำลังคำนวณจาก a + 1 ถึง b นั่นคือขั้นตอนที่เกิดซ้ำ - ในการแก้ปัญหาที่ใหญ่กว่า ("ผลรวมจาก a ถึง b รวม") เราลดปัญหาให้เล็กลง ("ผลรวมจาก a + 1 ถึง b รวม")

หากคุณดูรหัสที่คุณมีด้านบนคุณจะสังเกตเห็นว่ามีขั้นตอนนี้อยู่:

return a + sumInts(a + 1, b: b)

รหัสนี้เป็นเพียงการแปลตรรกะข้างต้น - หากคุณต้องการรวมจาก a ถึง b ให้รวมเริ่มต้นด้วยการรวม a + 1 ถึง b รวม (นั่นคือการเรียกซ้ำไปยังsumInts) จากนั้นเพิ่มaแล้วเพิ่ม

แน่นอนวิธีนี้ใช้ไม่ได้จริง ตัวอย่างเช่นคุณจะคำนวณผลรวมของจำนวนเต็มทั้งหมดระหว่าง 5 ถึง 5 ได้อย่างไร เมื่อใช้ตรรกะปัจจุบันของเราคุณจะต้องคำนวณผลรวมของจำนวนเต็มทั้งหมดระหว่าง 6 ถึง 5 รวมแล้วบวก 5 แล้วคุณจะคำนวณผลรวมของจำนวนเต็มทั้งหมดระหว่าง 6 ถึง 5 ได้อย่างไร? เมื่อใช้ตรรกะปัจจุบันของเราคุณจะต้องคำนวณผลรวมของจำนวนเต็มทั้งหมดระหว่าง 7 ถึง 5 รวมแล้วบวก 6 คุณจะสังเกตเห็นปัญหาที่นี่ - สิ่งนี้จะดำเนินต่อไปเรื่อย ๆ !

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

แล้วกรณีพื้นฐานของปัญหานี้คืออะไร? เมื่อคุณรวมจำนวนเต็มจาก a ถึง b รวมถ้า a มีค่ามากกว่า b คำตอบคือ 0 - ไม่มีตัวเลขใด ๆ ในช่วง! ดังนั้นเราจะจัดโครงสร้างโซลูชันของเราดังนี้:

  1. ถ้า a> b คำตอบคือ 0
  2. มิฉะนั้น (a ≤ b) จะได้รับคำตอบดังนี้:
    1. คำนวณผลรวมของจำนวนเต็มระหว่าง a + 1 และ b
    2. เพิ่มเพื่อรับคำตอบ

ตอนนี้เปรียบเทียบรหัสเทียมนี้กับรหัสจริงของคุณ:

func sumInts(a: Int, b: Int) -> Int {
    if (a > b) {
        return 0
    } else {
        return a + sumInts(a + 1, b: b)
    }
}

สังเกตว่าแทบจะมีแผนที่แบบหนึ่งต่อหนึ่งระหว่างโซลูชันที่ระบุไว้ในรหัสเทียมและรหัสจริงนี้ ขั้นตอนแรกคือกรณีฐาน - ในกรณีที่คุณขอผลรวมของช่วงว่างของตัวเลขคุณจะได้ 0 มิฉะนั้นให้คำนวณผลรวมระหว่าง a + 1 และ b จากนั้นเพิ่ม a

จนถึงตอนนี้ฉันได้ให้แนวคิดระดับสูงที่อยู่เบื้องหลังโค้ด แต่คุณมีอีกสองคำถามที่ดีมาก ก่อนอื่นทำไมสิ่งนี้ไม่ส่งกลับ 0 เสมอเนื่องจากฟังก์ชันบอกว่าให้ส่งคืน 0 ถ้า a> b? ประการที่สอง 14 ที่แท้จริงมาจากไหน? ลองดูสิ่งเหล่านี้ในทางกลับกัน

มาลองใช้กรณีที่เรียบง่ายมาก จะเกิดอะไรขึ้นถ้าคุณโทรมาsumInts(6, 5)? ในกรณีนี้การติดตามโค้ดคุณจะเห็นว่าฟังก์ชันจะส่งกลับ 0 นั่นเป็นสิ่งที่ถูกต้องที่จะทำเพื่อ - ไม่มีตัวเลขใด ๆ ในช่วง ตอนนี้ลองสิ่งที่ยากขึ้น จะเกิดอะไรขึ้นเมื่อคุณโทรsumInts(5, 5)? นี่คือสิ่งที่เกิดขึ้น:

  1. คุณโทรsumInts(5, 5). เราตกอยู่ในelseสาขาซึ่งส่งคืนค่าของ `a + sumInts (6, 5)
  2. ในsumInts(5, 5)การพิจารณาว่าอะไรsumInts(6, 5)คืออะไรเราจำเป็นต้องหยุดสิ่งที่เรากำลังทำและโทรหาsumInts(6, 5)เป็นเราต้องหยุดสิ่งที่เรากำลังทำและโทรออกไปยัง
  3. sumInts(6, 5)ถูกเรียก มันเข้าสู่ifสาขาและกลับ0มา อย่างไรก็ตามอินสแตนซ์นี้sumIntsถูกเรียกโดยsumInts(5, 5)ดังนั้นค่าที่ส่งคืนจะถูกสื่อสารกลับไปsumInts(5, 5)ไม่ใช่ไปยังผู้โทรระดับบนสุด
  4. sumInts(5, 5)ตอนนี้สามารถคำนวณ5 + sumInts(6, 5)เพื่อกลับมาได้5แล้ว จากนั้นส่งกลับไปยังผู้โทรระดับบนสุด

สังเกตว่าค่า 5 ถูกสร้างขึ้นที่นี่อย่างไร sumIntsเราเริ่มต้นด้วยการเรียกใช้งานหนึ่งไปยัง ที่ยิงออกโทร recursive sumInts(5, 5)อื่นและค่าส่งกลับโดยการโทรที่การสื่อสารข้อมูลกลับไปยัง sumInts(5, 5)ในทางกลับกันการเรียกไปยังทำการคำนวณและส่งคืนค่ากลับไปยังผู้โทร

หากคุณลองทำsumInts(4, 5)สิ่งนี้จะเกิดขึ้น:

  • sumInts(4, 5)4 + sumInts(5, 5)พยายามที่จะกลับมา การทำเช่นนั้นเรียกsumInts(5, 5)ว่า
    • sumInts(5, 5)5 + sumInts(6, 5)พยายามที่จะกลับมา การทำเช่นนั้นเรียกsumInts(6, 5)ว่า
    • sumInts(6, 5)คืนค่า 0 กลับไปเป็นsumInts(5, 5).</li> <li>sumInts (5, 5) now has a value forsumInts (6, 5) , namely 0. It then returns5 + 0 = 5`
  • sumInts(4, 5)ตอนนี้มีความคุ้มค่าsumInts(5, 5)คือ 5. 4 + 5 = 9จากนั้นผลตอบแทน

ในคำอื่น ๆ ค่าที่ส่งกลับจะเกิดขึ้นจากข้อสรุปค่าหนึ่งครั้งในแต่ละครั้งการหนึ่งค่าส่งกลับโดยโทร recursive โดยเฉพาะและการเพิ่มมูลค่าปัจจุบันของsumInts aเมื่อพื้นการเรียกซ้ำออกมาการเรียกที่ลึกที่สุดจะส่งกลับ 0 อย่างไรก็ตามค่านั้นจะไม่ออกจากห่วงโซ่การเรียกซ้ำในทันที แต่เพียงส่งค่ากลับไปที่การเรียกแบบเรียกซ้ำหนึ่งชั้นที่อยู่เหนือค่านั้น ด้วยวิธีนี้การเรียกซ้ำแต่ละครั้งจะเพิ่มหมายเลขอีกหนึ่งหมายเลขและส่งกลับให้สูงขึ้นในห่วงโซ่โดยปิดท้ายด้วยผลรวมทั้งหมด ลองติดตามสิ่งนี้เพื่อเป็นการออกกำลังกายsumInts(2, 5)ซึ่งเป็นสิ่งที่คุณต้องการเริ่มต้นด้วย

หวังว่านี่จะช่วยได้!


3
ขอขอบคุณที่สละเวลาจากวันที่วุ่นวายเพื่อแบ่งปันคำตอบที่ครอบคลุม! มีข้อมูลที่ยอดเยี่ยมมากมายที่นี่ซึ่งช่วยให้ฉันเข้าใจเกี่ยวกับฟังก์ชันการเรียกซ้ำและจะช่วยให้ผู้อื่นสะดุดกับโพสต์นี้ในอนาคตอย่างแน่นอน ขอขอบคุณอีกครั้งและขอให้เป็นวันที่ดี!
Jason Elwood

22

คุณมีคำตอบที่ดีที่นี่แล้ว แต่ฉันจะเพิ่มอีกหนึ่งคำตอบที่แตกต่างออกไป

ก่อนอื่นฉันได้เขียนบทความมากมายเกี่ยวกับอัลกอริธึมการเรียกซ้ำอย่างง่ายที่คุณอาจคิดว่าน่าสนใจ ดู

http://ericlippert.com/tag/recursion/

http://blogs.msdn.com/b/ericlippert/archive/tags/recursion/

สิ่งเหล่านี้อยู่ในลำดับใหม่ล่าสุดจากด้านบนดังนั้นให้เริ่มจากด้านล่าง

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

ขอฉันเขียนฟังก์ชันของคุณใหม่ให้กะทัดรัดกว่านี้เล็กน้อย อย่าคิดว่าสิ่งนี้เป็นภาษาใดภาษาหนึ่ง

s = (a, b) => a > b ? 0 : a + s(a + 1, b)

ฉันหวังว่ามันจะสมเหตุสมผล หากคุณไม่คุ้นเคยกับโอเปอเรเตอร์เงื่อนไขเป็นแบบฟอร์มcondition ? consequence : alternativeและความหมายของมันจะชัดเจน

ตอนนี้เราต้องการประเมินs(2,5) เราทำได้โดยทำการแทนที่ข้อความของการเรียกด้วยตัวฟังก์ชันจากนั้นแทนที่aด้วย2และbด้วย5:

s(2, 5) 
---> 2 > 5 ? 0 : 2 + s(2 + 1, 5)

ตอนนี้ประเมินเงื่อนไข เรา textually แทนที่ด้วย2 > 5false

---> false ? 0 : 2 + s(2 + 1, 5)

ตอนนี้แทนที่เงื่อนไขเท็จทั้งหมดด้วยข้อความทางเลือกและเงื่อนไขจริงทั้งหมดด้วยผลลัพธ์ เรามีเงื่อนไขที่เป็นเท็จเท่านั้นดังนั้นเราจึงแทนที่นิพจน์นั้นทางข้อความด้วยทางเลือก:

---> 2 + s(2 + 1, 5)

ตอนนี้เพื่อช่วยฉันที่ต้องพิมพ์+เครื่องหมายเหล่านั้นทั้งหมดข้อความแทนที่เลขคณิตคงที่ด้วยค่าของมัน (นี่เป็นการโกงเล็กน้อย แต่ฉันไม่ต้องการที่จะต้องติดตามวงเล็บทั้งหมด!)

---> 2 + s(3, 5)

ค้นหาและแทนที่คราวนี้ด้วยเนื้อความสำหรับการโทร3สำหรับaและ5สำหรับ b เราจะแทนที่การโทรในวงเล็บ:

---> 2 + (3 > 5 ? 0 : 3 + s(3 + 1, 5))

และตอนนี้เราก็ทำตามขั้นตอนการแทนที่ข้อความเดียวกันต่อไป:

---> 2 + (false ? 0 : 3 + s(3 + 1, 5))  
---> 2 + (3 + s(3 + 1, 5))                
---> 2 + (3 + s(4, 5))                     
---> 2 + (3 + (4 > 5 ? 0 : 4 + s(4 + 1, 5)))
---> 2 + (3 + (false ? 0 : 4 + s(4 + 1, 5)))
---> 2 + (3 + (4 + s(4 + 1, 5)))
---> 2 + (3 + (4 + s(5, 5)))
---> 2 + (3 + (4 + (5 > 5 ? 0 : 5 + s(5 + 1, 5))))
---> 2 + (3 + (4 + (false ? 0 : 5 + s(5 + 1, 5))))
---> 2 + (3 + (4 + (5 + s(5 + 1, 5))))
---> 2 + (3 + (4 + (5 + s(6, 5))))
---> 2 + (3 + (4 + (5 + (6 > 5 ? 0 : s(6 + 1, 5)))))
---> 2 + (3 + (4 + (5 + (true ? 0 : s(6 + 1, 5)))))
---> 2 + (3 + (4 + (5 + 0)))
---> 2 + (3 + (4 + 5))
---> 2 + (3 + 9)
---> 2 + 12
---> 14

ทั้งหมดที่เราทำที่นี่เป็นเพียงตรงไปตรงมาเกี่ยวกับใจเปลี่ยนตัว จริงๆแล้วฉันไม่ควรเปลี่ยนตัว "3" สำหรับ "2 + 1" ไปเรื่อย ๆ จนกว่าฉันจะต้องทำ แต่ในแง่การสอนมันคงอ่านยาก

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

แน่นอนว่าภาษาส่วนใหญ่ไม่ได้ใช้การเปิดใช้งานเป็นการแทนที่ข้อความ แต่มีเหตุผลนั่นคือสิ่งที่เป็นอยู่

แล้วการเรียกซ้ำที่ไม่มีขอบเขตคืออะไร? การเรียกซ้ำที่การแทนที่ข้อความไม่หยุด! สังเกตว่าในที่สุดเราก็มาถึงขั้นตอนที่ไม่มีอะไรsให้แทนที่ได้อีกแล้วเราก็ใช้กฎสำหรับเลขคณิตได้


ตัวอย่างที่ดี แต่มันทำให้ใจคุณสลายเมื่อคุณดำเนินการคำนวณที่ซับซ้อนมากขึ้น เช่น. การค้นหาบรรพบุรุษร่วมกันใน Binary Tree
CodeYogi

11

วิธีที่ฉันมักจะหาว่าฟังก์ชันเรียกซ้ำทำงานอย่างไรโดยดูที่ตัวพิมพ์ฐานและทำงานย้อนกลับ นี่คือเทคนิคที่ใช้กับฟังก์ชันนี้

อันดับแรกกรณีฐาน:

sumInts(6, 5) = 0

จากนั้นการโทรที่อยู่เหนือสิ่งนั้นในกองการโทร :

sumInts(5, 5) == 5 + sumInts(6, 5)
sumInts(5, 5) == 5 + 0
sumInts(5, 5) == 5

จากนั้นการโทรที่อยู่เหนือสิ่งนั้นในกองการโทร:

sumInts(4, 5) == 4 + sumInts(5, 5)
sumInts(4, 5) == 4 + 5
sumInts(4, 5) == 9

และอื่น ๆ :

sumInts(3, 5) == 3 + sumInts(4, 5)
sumInts(3, 5) == 3 + 9
sumInts(3, 5) == 12

และอื่น ๆ :

sumInts(2, 5) == 2 + sumInts(3, 5)
sumInts(4, 5) == 2 + 12
sumInts(4, 5) == 14

สังเกตว่าเรามาถึงการเรียกใช้ฟังก์ชันเดิม sumInts(2, 5) == 14

ลำดับที่ดำเนินการเรียกเหล่านี้:

sumInts(2, 5)
sumInts(3, 5)
sumInts(4, 5)
sumInts(5, 5)
sumInts(6, 5)

ลำดับที่การเรียกเหล่านี้ส่งกลับ:

sumInts(6, 5)
sumInts(5, 5)
sumInts(4, 5)
sumInts(3, 5)
sumInts(2, 5)

โปรดทราบว่าเรามาถึงข้อสรุปเกี่ยวกับวิธีการทำงานการดำเนินงานโดยการติดตามการโทรในการสั่งซื้อที่พวกเขากลับมา


5

ฉันจะให้มันไป

ดำเนินการสมการ a + sumInts (a + 1, b) ฉันจะแสดงให้เห็นว่าคำตอบสุดท้ายคือ 14 อย่างไร

//the sumInts function definition
func sumInts(a: Int, b: Int) -> Int {
    if (a > b) {
        return 0
    } else {
        return a + sumInts(a + 1, b)
    }
}

Given: a = 2 and b = 5

1) 2 + sumInts(2+1, 5)

2) sumInts(3, 5) = 12
   i) 3 + sumInts(3+1, 5)
   ii) 4 + sumInts(4+1, 5)
   iii) 5 + sumInts(5+1, 5)
   iv) return 0
   v) return 5 + 0
   vi) return 4 + 5
   vii) return 3 + 9

3) 2 + 12 = 14.

โปรดแจ้งให้เราทราบหากคุณมีคำถามเพิ่มเติม

นี่คืออีกตัวอย่างของฟังก์ชันเรียกซ้ำในตัวอย่างต่อไปนี้

ชายคนหนึ่งเพิ่งเรียนจบวิทยาลัย

t คือจำนวนเวลาในหน่วยปี

จำนวนปีที่ทำงานจริงทั้งหมดก่อนที่จะเกษียณสามารถคำนวณได้ดังนี้:

public class DoIReallyWantToKnow 
{
    public int howLongDoIHaveToWork(int currentAge)
    {
      const int DESIRED_RETIREMENT_AGE = 65;
      double collectedMoney = 0.00; //remember, you just graduated college
      double neededMoneyToRetire = 1000000.00

      t = 0;
      return work(t+1);
    }

    public int work(int time)
    {
      collectedMoney = getCollectedMoney();

      if(currentAge >= DESIRED_RETIREMENT_AGE 
          && collectedMoney == neededMoneyToRetire
      {
        return time;
      }

      return work(time + 1);
    }
}

และนั่นก็น่าจะเพียงพอแล้วที่จะทำให้ทุกคนหดหู่ฮ่า ๆ ;-P


5

recursion ในการเรียกซ้ำของวิทยาการคอมพิวเตอร์ครอบคลุมในเชิงลึกภายใต้หัวข้อ Finite Automata

ในรูปแบบที่ง่ายที่สุดคือการอ้างอิงตัวเอง ตัวอย่างเช่นการพูดว่า "my car is a car" เป็นคำสั่งซ้ำ ปัญหาคือคำสั่งเป็นการวนซ้ำที่ไม่มีที่สิ้นสุดซึ่งจะไม่มีวันสิ้นสุด คำจำกัดความในคำว่า "รถ" คือมันคือ "รถ" จึงอาจใช้แทนกันได้ อย่างไรก็ตามไม่มีที่สิ้นสุดเพราะในกรณีของการเปลี่ยนตัวยังคงกลายเป็น "รถของฉันคือรถ"

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

การทดแทนที่แท้จริงเป็นกฎการผลิต ระบุว่าคำสั่งแสดงด้วย S และรถคันนั้นเป็นตัวแปรที่สามารถเป็น "เบนท์ลีย์" คำสั่งนี้สามารถสร้างขึ้นใหม่ได้

S -> "my"S | " "S | CS | "is"S | "blue"S | ε
C -> "bentley"

สิ่งนี้สามารถสร้างได้หลายวิธีเนื่องจากแต่ละ|วิธีมีทางเลือก Sสามารถแทนที่ได้ด้วยตัวเลือกใดตัวเลือกหนึ่งและ S จะเริ่มว่างเปล่าเสมอ εหมายถึงการยุติการผลิต เช่นเดียวกับที่Sสามารถแทนที่ได้ตัวแปรอื่น ๆ ก็สามารถทำได้เช่นกัน (มีเพียงตัวเดียวและจะเป็นCตัวแทนของ "เบนท์ลีย์")

ดังนั้นการเริ่มต้นด้วยSการว่างเปล่าและแทนที่ด้วยตัวเลือกแรก"my"S Sจะกลายเป็น

"my"S

Sยังคงสามารถแทนที่ได้เนื่องจากแสดงถึงตัวแปร เราสามารถเลือก "ของฉัน" อีกครั้งหรือεเพื่อสิ้นสุด แต่ขอให้ดำเนินการต่อไป เราเลือกช่องว่างที่Sถูกแทนที่ด้วย" "S

"my "S

ถัดไปให้เลือก C

"my "CS

และ C มีทางเลือกเดียวเท่านั้นสำหรับการเปลี่ยน

"my bentley"S

และเว้นวรรคอีกครั้งสำหรับ S

"my bentley "S

และอื่น ๆ"my bentley is"S, "my bentley is "S, "my bentley is blue"S,"my bentley is blue" (เปลี่ยน S สำหรับεปิดให้บริการการผลิต) และเราได้สร้างซ้ำคำสั่งของเรา "เบนท์ลีย์ของฉันคือสีฟ้า"

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

S -> 2 + A
A -> 3 + B
B -> 4 + C
C -> 5 + D
D -> 0

สิ่งนี้จะกลายเป็น

2 + A
2 + 3 + B
2 + 3 + 4 + C
2 + 3 + 4 + 5 + D
2 + 3 + 4 + 5 + 0
14

ฉันไม่แน่ใจว่าออโตมาตาที่ จำกัด หรือไวยากรณ์ที่ไม่มีบริบทเป็นตัวอย่างที่ดีที่สุดซึ่งสามารถช่วยให้คนสร้างสัญชาตญาณแรก ๆ เกี่ยวกับการเรียกซ้ำได้ เป็นตัวอย่างที่ดี แต่อาจจะไม่ค่อยคุ้นเคยกับโปรแกรมเมอร์ที่ไม่มีพื้นฐาน CS มาก่อน
ไค

4

ฉันคิดว่าวิธีที่ดีที่สุดในการทำความเข้าใจฟังก์ชันแบบเรียกซ้ำคือการตระหนักว่าพวกเขาสร้างขึ้นเพื่อประมวลผลโครงสร้างข้อมูลแบบเรียกซ้ำ แต่ในฟังก์ชันเดิมของคุณsumInts(a: Int, b: Int)ที่คำนวณผลรวมของตัวเลขจากaไปbยังแบบวนซ้ำดูเหมือนว่าจะไม่ใช่โครงสร้างข้อมูลแบบเรียกซ้ำ ... ลองใช้เวอร์ชันsumInts(a: Int, n: Int)ที่แก้ไขเล็กน้อยโดยที่nคุณจะเพิ่มจำนวนตัวเลข

ตอนนี้ sumInts วนซ้ำnเป็นจำนวนธรรมชาติ ยังไม่ใช่ข้อมูลซ้ำใช่ไหม? จำนวนธรรมชาติอาจถือได้ว่าเป็นโครงสร้างข้อมูลแบบวนซ้ำโดยใช้สัจพจน์ Peano:

enum Natural = {
    case Zero
    case Successor(Natural)
}

ดังนั้น 0 = ศูนย์ 1 = Succesor (ศูนย์) 2 = Succesor (Succesor (Zero)) และอื่น ๆ

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

// sums n numbers beginning from a
func sumInts(a: Int, n: Int) -> Int {
    if (n == 0) {
        // non recursive case
    } else {
        // recursive case. We use sumInts(..., n - 1)
    }
}

ตอนนี้ฟังก์ชันเรียกซ้ำง่ายกว่าในการเขียนโปรแกรม ประการแรกกรณีฐานn=0 . เราควรคืนค่าอะไรถ้าเราไม่ต้องการเพิ่มตัวเลข? คำตอบคือแน่นอน 0

แล้วกรณีเรียกซ้ำล่ะ? หากเราต้องการเพิ่มnตัวเลขที่ขึ้นต้นด้วยaและเรามีsumIntsฟังก์ชันการทำงานที่เหมาะกับn-1? ดีเราต้องเพิ่มaแล้ววิงวอนsumIntsด้วยa + 1ดังนั้นเราจึงจบลงด้วย:

// sums n numbers beginning from a
func sumInts(a: Int, n: Int) -> Int {
    if (n == 0) {
        return 0
    } else {
        return a + sumInts(a + 1, n - 1)
    }
}

สิ่งที่ดีคือตอนนี้คุณไม่จำเป็นต้องคิดในระดับต่ำของการเรียกซ้ำ คุณเพียงแค่ต้องตรวจสอบว่า:

  • สำหรับกรณีพื้นฐานของข้อมูลที่เรียกซ้ำจะคำนวณคำตอบโดยไม่ใช้การเรียกซ้ำ
  • สำหรับกรณีที่เกิดซ้ำของข้อมูลที่เรียกซ้ำจะคำนวณคำตอบโดยใช้การเรียกซ้ำในข้อมูลที่ถูกทำลาย

4

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

เพื่อแนะนำคุณเกี่ยวกับการใช้งานฟังก์ชัน: พิจารณารหัสเครื่องเสมือนต่อไปนี้:

ใส่คำอธิบายภาพที่นี่

หาก Swift คอมไพล์เป็นภาษาเครื่องเสมือนนี้บล็อกของรหัส Swift ต่อไปนี้:

mult(a: 2, b: 3) - 4

จะรวบรวมลงไปที่

push constant 2  // Line 1
push constant 3  // Line 2
call mult        // Line 3
push constant 4  // Line 4
sub              // Line 5

ภาษาเครื่องเสมือนได้รับการออกแบบรอบสแต็คระดับโลกpush constant nดันจำนวนเต็มไปยังสแต็กส่วนกลางนี้

หลังจากดำเนินการบรรทัดที่ 1 และ 2 สแต็กจะมีลักษณะดังนี้:

256:  2  // Argument 0
257:  3  // Argument 1

256และ257เป็นที่อยู่หน่วยความจำ

call mult ดันหมายเลขบรรทัดส่งกลับ (3) ไปยังสแต็กและจัดสรรพื้นที่สำหรับตัวแปรโลคัลของฟังก์ชัน

256:  2  // argument 0
257:  3  // argument 1
258:  3  // return line number
259:  0  // local 0

... function multและมันจะไปต่อฉลาก รหัสภายในmultจะถูกเรียกใช้งาน อันเป็นผลมาจากการเรียกใช้รหัสนั้นเราจะคำนวณผลคูณของ 2 และ 3 ซึ่งเก็บไว้ในตัวแปรท้องถิ่นที่ 0 ของฟังก์ชัน

256:  2  // argument 0
257:  3  // argument 1
258:  3  // return line number
259:  6  // local 0

ก่อนreturning จาก mult คุณจะสังเกตเห็นบรรทัด:

push local 0  // push result

เราจะดันผลิตภัณฑ์ลงบนกอง

256:  2  // argument 0
257:  3  // argument 1
258:  3  // return line number
259:  6  // local 0
260:  6  // product

เมื่อเรากลับมาสิ่งต่อไปนี้จะเกิดขึ้น:

  • ใส่ค่าสุดท้ายบนสแต็กไปยังที่อยู่หน่วยความจำของอาร์กิวเมนต์ที่ 0 (256 ในกรณีนี้) สิ่งนี้เป็นจุดที่สะดวกที่สุดในการวาง
  • ทิ้งทุกอย่างในสแต็กจนถึงที่อยู่ของอาร์กิวเมนต์ที่ 0
  • ไปที่หมายเลขบรรทัดส่งกลับ (3 ในกรณีนี้) แล้วเลื่อนไปข้างหน้า

หลังจากกลับมาเราพร้อมที่จะดำเนินการบรรทัดที่ 4 และสแต็คของเรามีลักษณะดังนี้:

256:  6  // product that we just returned

ตอนนี้เราดัน 4 เข้าสู่สแต็ก

256:  6
257:  4

subเป็นฟังก์ชันดั้งเดิมของภาษาเครื่องเสมือน ใช้สองอาร์กิวเมนต์และส่งคืนผลลัพธ์ในที่อยู่ปกตินั่นคืออาร์กิวเมนต์ที่ 0

ตอนนี้เรามี

256:  2  // 6 - 4 = 2

ตอนนี้คุณรู้แล้วว่าการเรียกใช้ฟังก์ชันทำงานอย่างไรจึงค่อนข้างง่ายที่จะเข้าใจว่าการเรียกซ้ำทำงานอย่างไร ไม่มีเวทมนตร์แค่กอง

ฉันได้ใช้งานsumIntsฟังก์ชันของคุณในภาษาเครื่องเสมือนนี้:

function sumInts 0     // `0` means it has no local variables.
  label IF
    push argument 0
    push argument 1
    lte              
    if-goto ELSE_CASE
    push constant 0
    return
  label ELSE_CASE
    push constant 2
    push argument 0
    push constant 1
    add
    push argument 1
    call sumInts       // Line 15
    add                // Line 16
    return             // Line 17
// End of function

ตอนนี้ฉันจะเรียกมันว่า:

push constant 2
push constant 5
call sumInts           // Line 21

รหัสดำเนินการและเราไปถึงจุดหยุดที่lteจะกลับfalseมา นี่คือลักษณะของสแต็ก ณ จุดนี้:

// First invocation
256:  2   // argument 0
257:  5   // argument 1
258:  21  // return line number
259:  2   // augend
// Second
260:  3   // argument 0
261:  5   // argument 1
262:  15  // return line number
263:  3   // augend
// Third
264:  4   // argument 0
265:  5   // argument 1
266:  15  // return line number
267:  4   // augend
// Fourth
268:  5   // argument 0
269:  5   // argument 1
270:  15  // return line number
271:  5   // augend
// Fifth
272:  6   // argument 0
273:  5   // argument 1
274:  15  // return line number
275:  0   // return value

ทีนี้มา "คลาย" การเรียกซ้ำของเรา return0 และไปที่บรรทัดที่ 15 และเลื่อนไป

271:  5
272:  0

บรรทัดที่ 16: add

271:  5

บรรทัดที่ 17: return5 และไปยังบรรทัดที่ 15 และไปข้างหน้า

267:  4
268:  5

บรรทัดที่ 16: add

267:  9

บรรทัดที่ 17: return9 และไปยังบรรทัดที่ 15 และไปข้างหน้า

263:  3
264:  9

บรรทัดที่ 16: add

263:  12

บรรทัดที่return17:12 และไปยังบรรทัดที่ 15 และไปข้างหน้า

259:  2
260:  12

บรรทัดที่ 16: add

259:  14

บรรทัดที่ 17: return14 และไปที่สาย 21 และไปข้างหน้า

256:  14

ที่นั่นคุณมี recursion: gotoสดุดี


4

เคล็ดลับที่ดีอย่างหนึ่งที่ฉันได้พบในการเรียนรู้และทำความเข้าใจกับการเรียกซ้ำจริงๆคือการใช้เวลาเรียนรู้ภาษาที่ไม่มีรูปแบบของการสร้างลูปใด ๆ นอกจากการเรียกซ้ำ ด้วยวิธีนี้คุณจะรู้สึกดีมากในการใช้การเรียกซ้ำผ่านการฝึกฝน

ฉันติดตามhttp://www.htdp.org/ซึ่งรวมถึงการสอน Scheme ด้วยแล้วยังเป็นคำแนะนำที่ดีเยี่ยมเกี่ยวกับวิธีการออกแบบโปรแกรมในแง่ของสถาปัตยกรรมและการออกแบบ

แต่โดยพื้นฐานแล้วคุณต้องลงทุนเวลา หากไม่มีความเข้าใจในอัลกอริทึมบางอย่างที่เรียกซ้ำอย่าง 'มั่นคง' เช่นการย้อนรอยจะดูเหมือน 'ยาก' หรือแม้แต่ 'เวทมนตร์' สำหรับคุณเสมอ ดังนั้นจงอดทน :-D

ฉันหวังว่านี่จะช่วยได้และขอให้โชคดี!


3

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

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


3

ฉันรู้ว่านอกประเด็นไปหน่อย แต่ ... ลองค้นหาการเรียกซ้ำใน Google ... คุณจะเห็นตัวอย่างว่ามันหมายถึงอะไร :-)


Google เวอร์ชันก่อนหน้านี้ส่งคืนข้อความต่อไปนี้ (อ้างจากหน่วยความจำ):

recursion

ดูการเรียกซ้ำ

เมื่อวันที่ 10 กันยายน 2014 เรื่องตลกเกี่ยวกับการเรียกซ้ำได้รับการอัปเดต:

recursion

คุณหมายถึง: การเรียกซ้ำ


สำหรับคำตอบอื่นโปรดดูคำตอบนี้


3

คิดว่าการเรียกซ้ำเป็นโคลนหลาย ๆทำสิ่งเดียวกัน ...

คุณขอให้โคลน [1]: "รวมตัวเลขระหว่าง 2 ถึง 5"

+ clone[1]               knows that: result is 2 + "sum numbers between 3 and 5". so he asks to clone[2] to return: "sum numbers between 3 and 5"
|   + clone[2]           knows that: result is 3 + "sum numbers between 4 and 5". so he asks to clone[3] to return: "sum numbers between 4 and 5"
|   |   + clone[3]       knows that: result is 4 + "sum numbers between 5 and 5". so he asks to clone[4] to return: "sum numbers between 5 and 5"
|   |   |   + clone[4]   knows that: result is 5 + "sum numbers between 6 and 5". so he asks to clone[5] to return: "sum numbers between 6 and 5"
|   |   |   |   clone[5] knows that: he can't sum, because 6 is larger than 5. so he returns 0 as result.
|   |   |   + clone[4]   gets the result from clone[5] (=0)  and sums: 5 + 0,  returning 5
|   |   + clone[3]       gets the result from clone[4] (=5)  and sums: 4 + 5,  returning 9
|   + clone[2]           gets the result from clone[3] (=9)  and sums: 3 + 9,  returning 12
+ clone[1]               gets the result from clone[2] (=12) and sums: 2 + 12, returning 14

และvoilá !!


2

หลาย ๆ คำตอบข้างบนดีมาก เทคนิคที่มีประโยชน์ในการแก้การเรียกซ้ำคือการสะกดสิ่งที่เราต้องการทำก่อนและเขียนโค้ดในฐานะมนุษย์จะแก้ได้ ในกรณีข้างต้นเราต้องการสรุปลำดับของจำนวนเต็มต่อเนื่องกัน (โดยใช้ตัวเลขจากด้านบน):

2, 3, 4, 5  //adding these numbers would sum to 14

ตอนนี้โปรดทราบว่าบรรทัดเหล่านี้สับสน (ไม่ผิด แต่สับสน)

if (a > b) {
    return 0 
}

ทำไมต้องทดสอบa>bและทำไมreturn 0

มาเปลี่ยนรหัสเพื่อให้สะท้อนถึงสิ่งที่มนุษย์ทำมากขึ้น

func sumInts(a: Int, b: Int) -> Int {
  if (a == b) {
    return b // When 'a equals b' I'm at the most Right integer, return it
  }
  else {
    return a + sumInts(a: a + 1, b: b)
  }
}

เราจะทำเหมือนมนุษย์ได้มากกว่านี้ไหม? ใช่ โดยปกติเราจะสรุปจากซ้ายไปขวา (2 + 3 + ... ) แต่การเรียกซ้ำข้างต้นกำลังสรุปจากขวาไปซ้าย (... + 4 + 5) เปลี่ยนรหัสเพื่อสะท้อน ( -อาจเป็นเรื่องที่น่ากลัวเล็กน้อย แต่ไม่มาก)

func sumInts(a: Int, b: Int) -> Int {
  if (a == b) {
    return b // When I'm at the most Left integer, return it
  }
  else {
    return sumInts(a: a, b: b - 1) + b
  }
}

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


2

ฉันมีความยากลำบากในการทำความเข้าใจการเรียกซ้ำจากนั้นฉันก็พบบล็อกนี้และฉันเห็นคำถามนี้แล้วดังนั้นฉันคิดว่าฉันต้องแบ่งปัน คุณต้องอ่านบล็อกนี้ฉันพบว่าสิ่งนี้มีประโยชน์อย่างมากซึ่งอธิบายด้วยสแต็กและแม้จะอธิบายว่าการเรียกซ้ำสองครั้งทำงานกับสแต็กทีละขั้นตอนอย่างไร ฉันขอแนะนำให้คุณทำความเข้าใจก่อนว่าสแต็กทำงานอย่างไรซึ่งอธิบายได้เป็นอย่างดีที่นี่: journey-to-the-stack

then now you will understand how recursion works now take a look of this post: ทำความเข้าใจการเรียกซ้ำทีละขั้นตอน

ใส่คำอธิบายภาพที่นี่

เป็นโปรแกรม:

def hello(x):
    if x==1:
        return "op"
    else:
        u=1
        e=12
        s=hello(x-1)
        e+=1
        print(s)
        print(x)
        u+=1
    return e

hello(3)

ใส่คำอธิบายภาพที่นี่ ใส่คำอธิบายภาพที่นี่


2

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


0

ให้ฉันบอกคุณด้วยตัวอย่างของอนุกรมฟีโบนักชีฟีโบนักชีคือ

เสื้อ (n) = เสื้อ (n - 1) + n;

ถ้า n = 0 แล้ว 1

เพื่อให้เห็นวิธีการทำงานของการเรียกซ้ำผมเพียงแค่เปลี่ยนnในt(n)ด้วยn-1และอื่น ๆ ดูเหมือน:

เสื้อ (n-1) = เสื้อ (n - 2) + n + 1;

เสื้อ (n-1) = เสื้อ (n - 3) + n + 1 + n;

เสื้อ (n-1) = เสื้อ (n - 4) + n + 1 + n + 2 + n;

.

.

.

เสื้อ (n) = เสื้อ (nk) + ... + (nk-3) + (nk-2) + (nk-1) + n;

เรารู้ว่าถ้าt(0)=(n-k)เท่ากับ1แล้วn-k=0ดังนั้นn=kเราแทนที่kด้วยn:

เสื้อ (n) = เสื้อ (nn) + ... + (n-n + 3) + (n-n + 2) + (n-n + 1) + n;

ถ้าเราละเว้นn-n:

เสื้อ (n) = เสื้อ (0) + ... + 3 + 2 + 1 + (n-1) + n;

เพื่อให้3+2+1+(n-1)+nเป็นจำนวนธรรมชาติ มันคำนวณเป็นΣ3+2+1+(n-1)+n = n(n+1)/2 => n²+n/2

ผลลัพธ์ของ fib คือ: O(1 + n²) = O(n²)

วิธีนี้เป็นวิธีที่ดีที่สุดในการทำความเข้าใจความสัมพันธ์แบบเรียกซ้ำ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.