การเรียกซ้ำกับการทำซ้ำ


109

ถูกต้องหรือไม่ที่จะบอกว่าสามารถใช้การforวนซ้ำได้ทุกที่ และถ้าการเรียกซ้ำมักจะช้าลงเหตุผลทางเทคนิคที่เคยใช้การforวนซ้ำคืออะไร?

และถ้าเป็นไปได้เสมอที่จะแปลงการเรียกซ้ำเป็นforลูปจะมีกฎง่ายๆในการทำหรือไม่?


3
recursionVS iteration? iteration = for loopฉันคิด.
gongzhitaao

4
บล็อกของ Tom Moertel มีสี่โพสต์ที่ยอดเยี่ยมเกี่ยวกับการแปลงรหัสเรียกซ้ำเป็นรหัสซ้ำ: blog.moertel.com/tags/recursion.html
cjohnson318

คำตอบ:


148

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

การเพิ่มประสิทธิภาพบางอย่างเช่นการเพิ่มประสิทธิภาพการโทรหางทำให้การเรียกซ้ำเร็วขึ้น แต่ไม่สามารถทำได้เสมอไปและไม่สามารถใช้งานได้ในทุกภาษา

เหตุผลหลักในการใช้การเรียกซ้ำคือ

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

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


10
หากต้องการเพิ่ม - การเรียกซ้ำมีความเกี่ยวข้องอย่างใกล้ชิดกับเงื่อนไขการลดซึ่งมีบทบาทสำคัญในอัลกอริทึมต่างๆและใน CS โดยทั่วไป
SomeWittyUsername

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

@Yeikel เขียนฟังก์ชั่นf(n)ที่ส่งกลับที่ n จำนวนฟีโบนักชี
Matt

54

ถูกต้องหรือไม่ที่จะบอกว่าทุกที่สามารถใช้ a for loop ได้?

ใช่เนื่องจากการเรียกซ้ำในซีพียูส่วนใหญ่ถูกจำลองด้วยลูปและโครงสร้างข้อมูลสแต็ก

และถ้าการเรียกซ้ำมักจะช้าลงเหตุผลทางเทคนิคในการใช้คืออะไร?

ไม่ใช่ "โดยปกติช้ากว่า": เป็นการเรียกซ้ำที่ใช้อย่างไม่ถูกต้องซึ่งช้ากว่า ยิ่งไปกว่านั้นคอมไพเลอร์สมัยใหม่ยังสามารถแปลงการวนซ้ำเป็นลูปได้โดยไม่ต้องถาม

และถ้าเป็นไปได้เสมอที่จะแปลงการเรียกซ้ำเป็นสำหรับลูปจะมีกฎง่ายๆในการทำหรือไม่?

เขียนโปรแกรมซ้ำสำหรับอัลกอริทึมที่เข้าใจได้ดีที่สุดเมื่ออธิบายซ้ำ ๆ เขียนโปรแกรมแบบเรียกซ้ำสำหรับอัลกอริทึมที่อธิบายได้ดีที่สุดแบบเรียกซ้ำ

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


+ฉันยืมการเปรียบเทียบค้อนขนาดใหญ่จาก "วินัยในการเขียนโปรแกรม" ของ Dijkstra


7
การเรียกซ้ำมักจะแพงกว่า (หน่วยความจำช้าลง / มากกว่า) เนื่องจากการสร้างสแต็กเฟรมและอื่น ๆ ความแตกต่างอาจเล็กน้อยเมื่อใช้อย่างถูกต้องสำหรับปัญหาที่ซับซ้อนเพียงพอ แต่ก็ยังแพงกว่า มีข้อยกเว้นที่เป็นไปได้เช่นการเพิ่มประสิทธิภาพการเรียกซ้ำหาง
Bernhard Barker

ฉันไม่แน่ใจเกี่ยวกับsingle for loopในทุกกรณี พิจารณาการเรียกซ้ำที่ซับซ้อนมากขึ้นหรือการเรียกซ้ำที่มีตัวแปรมากกว่าตัวเดียว
SomeWittyUsername

@dasblinkenlight อาจเป็นไปได้ในทางทฤษฎีที่จะลดหลายลูปให้เหลือเพียงอันเดียว แต่ไม่แน่ใจเกี่ยวกับสิ่งนี้
SomeWittyUsername

@icepack ใช่มันเป็นไปได้ มันอาจจะไม่สวย แต่ก็เป็นไปได้
Bernhard Barker

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

29

คำถาม:

และถ้าการเรียกซ้ำมักจะช้าลงเหตุผลทางเทคนิคที่เคยใช้ซ้ำสำหรับการวนซ้ำคืออะไร?

คำตอบ:

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

สิ่งที่ดีอีกอย่างที่ควรลอง: พยายามเขียน Merge sort ซ้ำ ๆ จะต้องใช้เวลาพอสมควร

คำถาม:

ถูกต้องหรือไม่ที่จะบอกว่าทุกที่สามารถใช้ a for loop ได้?

คำตอบ:

ใช่. กระทู้นี้มีคำตอบที่ดีมากสำหรับเรื่องนี้

คำถาม:

และถ้าเป็นไปได้เสมอที่จะแปลงการเรียกซ้ำเป็นสำหรับลูปจะมีกฎง่ายๆในการทำหรือไม่?

คำตอบ:

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

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


3
ฉันชื่นชมความพยายามในการให้คำตอบที่เชื่อถือได้และฉันแน่ใจว่าผู้เขียนฉลาด แต่ "เชื่อใจฉัน" ไม่ใช่คำตอบที่เป็นประโยชน์สำหรับคำถามที่มีความหมายซึ่งคำตอบนั้นไม่ชัดเจนในทันที มีอัลกอริทึมที่ตรงไปตรงมามากสำหรับการค้นหาเชิงลึกครั้งแรกแบบวนซ้ำ ดูตัวอย่างที่ด้านล่างของหน้านี้สำหรับคำอธิบายของอัลกอริทึมใน pseudocode: csl.mtu.edu/cs2321/www/newLectures/26_Depth_First_Search.html
jdelman

3

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


3

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

ตัวอย่างเช่นในการหาจำนวนสามเหลี่ยมที่ n ของลำดับสามเหลี่ยม: 1 3 6 10 15 …โปรแกรมที่ใช้อัลกอริทึมแบบวนซ้ำเพื่อค้นหาจำนวนสามเหลี่ยมที่ n:

การใช้อัลกอริทึมซ้ำ:

//Triangular.java
import java.util.*;
class Triangular {
   public static int iterativeTriangular(int n) {
      int sum = 0;
      for (int i = 1; i <= n; i ++)
         sum += i;
      return sum;
   }
   public static void main(String args[]) {
      Scanner stdin = new Scanner(System.in);
      System.out.print("Please enter a number: ");
      int n = stdin.nextInt();
      System.out.println("The " + n + "-th triangular number is: " + 
                            iterativeTriangular(n));
   }
}//enter code here

การใช้อัลกอริทึมแบบเรียกซ้ำ:

//Triangular.java
import java.util.*;
class Triangular {
   public static int recursiveTriangular(int n) {
      if (n == 1)
     return 1;  
      return recursiveTriangular(n-1) + n; 
   }

   public static void main(String args[]) {
      Scanner stdin = new Scanner(System.in);
      System.out.print("Please enter a number: ");
      int n = stdin.nextInt();
      System.out.println("The " + n + "-th triangular number is: " + 
                             recursiveTriangular(n)); 
   }
}

1

คำตอบส่วนใหญ่ดูเหมือนจะสมมติว่าiterative= for loop. หาก for loop ของคุณไม่ถูก จำกัด ( a la C คุณสามารถทำอะไรก็ได้ที่คุณต้องการด้วยตัวนับลูปของคุณ) แสดงว่าถูกต้อง หากเป็นลูปจริง for (พูดในภาษา Python หรือภาษาที่ใช้งานได้ส่วนใหญ่ซึ่งคุณไม่สามารถแก้ไขตัวนับลูปได้ด้วยตนเอง) แสดงว่าไม่ถูกต้อง

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

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


1

ใช่ตามที่ธนกรณ์ตันดาวาสกล่าวไว้ ,

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

ตัวอย่างเช่นหอคอยแห่งฮานอย

  1. แหวน N ในขนาดที่เพิ่มขึ้น
  2. 3 เสา
  3. วงแหวนเริ่มซ้อนกันบนเสา 1 เป้าหมายคือการเลื่อนวงแหวนเพื่อให้ซ้อนกันบนเสา 3 ... แต่
    • สามารถย้ายแหวนได้ครั้งละหนึ่งวงเท่านั้น
    • ไม่สามารถใส่แหวนที่ใหญ่กว่าที่เล็กกว่าได้
  4. วิธีแก้ซ้ำนั้น“ ทรงพลัง แต่น่าเกลียด”; การแก้ปัญหาแบบวนซ้ำคือ "สง่างาม"

ตัวอย่างที่น่าสนใจ ฉันเดาว่าคุณรู้จักบทความโดย MC Er "The Towers of Hanoi and Binary Numerals" ได้รับการปฏิบัติในวิดีโอที่ยอดเยี่ยมโดย 3brown1blue
Andrestand

0

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

อย่างไรก็ตามในกรณีของโซลูชันการเรียกซ้ำขั้นสูงฉันไม่เชื่อว่าจะสามารถใช้งานได้โดยใช้การforวนซ้ำแบบธรรมดาเสมอไป


เป็นไปได้เสมอที่จะแปลงอัลกอริทึมแบบวนซ้ำเป็นแบบวนซ้ำ (โดยใช้สแต็ก) คุณอาจไม่ได้ลงเอยด้วยการวนซ้ำธรรมดา ๆ แต่ก็เป็นไปได้
Bernhard Barker

-4

การเรียกซ้ำ + การท่องจำอาจนำไปสู่วิธีการแก้ปัญหาที่มีประสิทธิภาพมากขึ้นเมื่อเปรียบเทียบกับวิธีการวนซ้ำที่บริสุทธิ์เช่นตรวจสอบสิ่งนี้: http://jsperf.com/fibonacci-memoized-vs-iterative-for-large-n


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

-6

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

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