การเรียกซ้ำหรือซ้ำซ้ำ


226

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


4
@Warrior ไม่เสมอไป ด้วยโปรแกรมหมากรุกเช่นอ่านการเรียกซ้ำได้ง่ายขึ้น รหัสหมากรุกเวอร์ชัน "ซ้ำ ๆ " จะไม่ช่วยเร่งความเร็วและอาจทำให้ซับซ้อนขึ้น
Mateen Ulhaq

12
ทำไมค้อนถึงได้รับการสนับสนุนมากกว่าเลื่อย ไขควงมากกว่าสว่านหรือไม่? สิ่วเหนือสว่าน?
Wayne Conrad

3
ไม่มีรายการโปรด พวกมันล้วนเป็นแค่เครื่องมือแต่ละอย่างมีจุดประสงค์ของตัวเอง ฉันจะถามว่า "ปัญหาซ้ำ ๆ แบบไหนดีกว่าการเรียกซ้ำและในทางกลับกัน"
Wayne Conrad

9
"มีอะไรดีเกี่ยวกับการเรียกซ้ำ?" ... มันเป็นการเรียกซ้ำที่เกิดขึ้น ; o)
Keng

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

คำตอบ:


181

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

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


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

15
Re: tail recursion is optimized by compilersแต่คอมไพเลอร์ทุกตัวไม่รองรับการเรียกซ้ำหาง ..
เควินเมเรดิ ธ

347

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


3
@ LeighCaldwell: ฉันคิดว่าสรุปความคิดของฉันอย่างแน่นอน สงสารเจ้าพ่อไม่ได้ upmod ฉันมี :)
Ande Turner

35
คุณรู้หรือไม่ว่าคุณถูกอ้างถึงในหนังสือเพราะคำตอบของคุณ? LOL amazon.com/Grokking-Algorithms-illustrated-programmers-curious/ …
Aipi

4
ฉันชอบคำตอบนี้ .. และฉันชอบหนังสือ "Grokking Algorithms")
สูงสุด

อย่างน้อยฉันและมนุษย์ 341 คนอ่านหนังสืออัลกอริทึมของ Grokking!
zzfima

78

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

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

นอกจากนี้อัลกอริทึมแบบเรียกซ้ำบางตัวยังใช้ "การประเมิน Lazy" ซึ่งทำให้พวกเขามีประสิทธิภาพมากกว่าพี่น้องแบบวนซ้ำ ซึ่งหมายความว่าพวกเขาทำการคำนวณราคาแพงในเวลาที่พวกเขาต้องการมากกว่าแต่ละครั้งที่วงวนทำงาน

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

ลิงค์ 1: Haskel vs PHP (การเรียกซ้ำ vs การวนซ้ำ)

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

http://blog.webspecies.co.uk/2011-05-31/lazy-evaluation-with-php.html

ลิงก์ 2: การเรียนรู้แบบเรียกซ้ำ

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

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

https://developer.ibm.com/articles/l-recurs/

ลิงก์ 3: การเรียกซ้ำเร็วกว่าการวนซ้ำหรือไม่ (ตอบ)

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

การเรียกซ้ำเป็นเร็วกว่าการวนซ้ำหรือไม่


4
ชอบการเปรียบเทียบไขควงจริงๆ
jh314

blog.webspecies.co.uk/2011-05-31/lazy-evaluation-with-php.htmlนั้นตาย แต่คุณสามารถค้นหาได้ที่นี่github.com/juokaz/blog.webspecies.co.uk/blob/master/_posts / …
Vladyslav Startsev

16

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

ถึงกระนั้นก็มีหลายกรณีที่การเรียกซ้ำเป็นธรรมชาติและอ่านได้ง่ายกว่าลูป - เหมือนเมื่อทำงานกับต้นไม้ ในกรณีเหล่านี้ฉันขอแนะนำให้ใช้การเรียกซ้ำ


5
คอมไพเลอร์ของคุณจะปรับการเรียกหางเช่น Scala
Ben Hardy

11

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

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

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


8

recursion จะดีกว่าย้ำสำหรับปัญหาที่สามารถแบ่งออกเป็นหลายชิ้นที่มีขนาดเล็ก

ตัวอย่างเช่นในการสร้างอัลกอริทึม Fibonnaci แบบเรียกซ้ำคุณแบ่งไฟเบอร์ (n) ออกเป็น Fib (n-1) และ fib (n-2) และคำนวณทั้งสองส่วน การวนซ้ำช่วยให้คุณสามารถทำซ้ำฟังก์ชั่นเดียวซ้ำแล้วซ้ำอีก

อย่างไรก็ตาม Fibonacci เป็นตัวอย่างที่ผิดและฉันคิดว่าการทำซ้ำมีประสิทธิภาพมากขึ้น โปรดสังเกตว่า fib (n) = fib (n-1) + fib (n-2) และ fib (n-1) = fib (n-2) + fib (n-3) ตอแหล (n-1) ได้รับการคำนวณสองครั้ง!

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

ดังนั้นการเรียกซ้ำนั้นดีกว่าการวนซ้ำสำหรับปัญหาที่สามารถแบ่งย่อยเป็นปัญหาที่มีขนาดเล็กลงเป็นอิสระและคล้ายกัน


1
การคำนวณสองครั้งสามารถหลีกเลี่ยงได้จริงผ่านการบันทึก
Siddhartha

7

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

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


ไม่เป็นความจริงเสมอไป การเรียกซ้ำอาจมีประสิทธิภาพเท่ากับการทำซ้ำสำหรับบางกรณีที่สามารถเพิ่มประสิทธิภาพการโทรหางได้ stackoverflow.com/questions/310974/…
Sid Kshatriya

6

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

http://lambda-the-ultimate.org/node/1333


มี JVM's สำหรับ Java ที่ปรับการเรียกซ้ำแบบหางให้เหมาะสม ibm.com/developerworks/java/library/j-diag8.html
Liran Orevi

5

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


5

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

int factorial ( int input )
{
  int x, fact = 1;
  for ( x = input; x > 1; x--)
     fact *= x;
  return fact;
}

ตอนนี้ให้พิจารณาโดยใช้ฟังก์ชั่นวนซ้ำ

int factorial ( int input )
{
  if (input == 0)
  {
     return 1;
  }
  return input * factorial(input - 1);
}

โดยการสังเกตสองสิ่งนี้เราจะเห็นว่าการเรียกซ้ำเป็นเรื่องง่ายที่จะเข้าใจ แต่ถ้ามันไม่ได้ใช้ด้วยความระมัดระวังก็อาจเกิดข้อผิดพลาดได้ง่ายเช่นกัน สมมติว่าถ้าเราพลาดif (input == 0)แล้วรหัสจะถูกดำเนินการบางครั้งและจบลงด้วยมักจะล้นสแต็ค


6
ฉันพบว่ามันซ้ำง่ายกว่าที่จะเข้าใจ ฉันคิดว่าสำหรับตัวเขาแต่ละคน
Maxpm

@Maxpm ที่สูงเพื่อแก้ปัญหา recursive มากดีกว่า: foldl (*) 1 [1..n]ที่มัน
SK-logic

5

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

การนำไปใช้ซ้ำแล้วซ้ำอีก

public static void sort(Comparable[] a)
{
    int N = a.length;
    aux = new Comparable[N];
    for (int sz = 1; sz < N; sz = sz+sz)
        for (int lo = 0; lo < N-sz; lo += sz+sz)
            merge(a, lo, lo+sz-1, Math.min(lo+sz+sz-1, N-1));
}

การใช้งานแบบเรียกซ้ำ

private static void sort(Comparable[] a, Comparable[] aux, int lo, int hi)
{
    if (hi <= lo) return;
    int mid = lo + (hi - lo) / 2;
    sort(a, aux, lo, mid);
    sort(a, aux, mid+1, hi);
    merge(a, aux, lo, mid, hi);
}

ป.ล. - นี่คือสิ่งที่ศาสตราจารย์เควินเวย์น (มหาวิทยาลัยพรินซ์ตัน) บอกเล่าเกี่ยวกับหลักสูตรอัลกอริธึมที่นำเสนอบน Coursera


4

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


1
ที่จริงแล้วฟังก์ชั่น Scala tail-recursive ที่คอมไพล์แล้วจะถูกรวบรวมลงในลูปในไบต์ถ้าคุณสนใจที่จะดู (แนะนำ) ไม่มีฟังก์ชั่นการโทรเหนือศีรษะ ประการที่สองฟังก์ชั่นแบบเรียกซ้ำมีความได้เปรียบในการไม่ต้องใช้ตัวแปร / ผลข้างเคียงที่ไม่แน่นอนหรือลูปอย่างชัดเจนทำให้การพิสูจน์ความถูกต้องง่ายขึ้น
Ben Hardy

4

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



3

หากคุณเป็นเพียงการวนซ้ำในรายการให้แน่ใจว่าวนซ้ำ

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

ลองดูวิธี "ค้นหา" ที่นี่: http://penguin.ewu.edu/cscd300/Topic/BSTintro/index.html


3

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

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


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

2

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


2

มันขึ้นอยู่กับ "ความลึกเรียกซ้ำ" ขึ้นอยู่กับจำนวนค่าใช้จ่ายในการเรียกใช้ฟังก์ชันที่มีผลต่อเวลาดำเนินการทั้งหมด

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

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


เห็นด้วยอย่างสมบูรณ์กับ ugasoft ที่นี่ ... มันขึ้นอยู่กับความลึกของการเรียกซ้ำ ... และความซับซ้อนของการใช้งานซ้ำ ๆ ... คุณต้องเปรียบเทียบทั้งสองและดูว่ามีประสิทธิภาพมากขึ้น ... ไม่มีกฎง่ายๆเช่นนี้ ..
rajya vardhan

2

recursion? ฉันจะเริ่มจากตรงไหนวิกิจะบอกคุณว่า“ มันเป็นกระบวนการของการทำซ้ำสิ่งต่าง ๆ ในลักษณะที่คล้ายกัน”

ย้อนกลับไปในวันที่ฉันทำ C, การเรียกซ้ำ C ++ เป็นสิ่งที่พระเจ้าส่งมาเช่น "การเรียกซ้ำแบบหาง" คุณจะพบอัลกอริทึมการเรียงลำดับจำนวนมากใช้การเรียกซ้ำ ตัวอย่างการจัดเรียงด่วน: http://alienryderflex.com/quicksort/

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


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

จุดยุติธรรมมันย้อนหลัง อย่างไรก็ตามฉันไม่แน่ใจว่ายังคงใช้กับการเรียกซ้ำหาง
Nickz

2

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

ในกรณีของฉันฉันกำลังพยายามใช้การยกกำลังแบบเมทริกซ์โดยการยกกำลังสองโดยใช้วัตถุอาร์มาดิลโล่ในแบบวนซ้ำและแบบวนซ้ำ อัลกอริทึมที่สามารถพบได้ที่นี่ ... https://en.wikipedia.org/wiki/Exponentiation_by_squaring ฟังก์ชั่นของฉันถูกเทมเพลตและผมได้คำนวณเมทริกซ์ยกอำนาจ1,000,000 12x12 10ฉันได้ผลลัพธ์ดังต่อไปนี้:

iterative + optimisation flag -O3 -> 2.79.. sec
recursive + optimisation flag -O3 -> 1.32.. sec

iterative + No-optimisation flag  -> 2.83.. sec
recursive + No-optimisation flag  -> 4.15.. sec

ผลลัพธ์เหล่านี้ได้รับโดยใช้ gcc-4.8 พร้อมด้วยธง c ++ 11 ( -std=c++11) และ Armadillo 6.1 พร้อม Intel mkl คอมไพเลอร์ของ Intel ยังแสดงผลลัพธ์ที่คล้ายกัน


1

ไมค์ถูกต้อง Tail recursion ไม่ได้รับการปรับให้เหมาะสมโดยคอมไพเลอร์ Java หรือ JVM คุณจะได้รับสแต็คล้นด้วยบางสิ่งดังนี้:

int count(int i) {
  return i >= 100000000 ? i : count(i+1);
}

3
ถ้าคุณไม่เขียนใน Scala ;-)
Ben Hardy

1

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


1

การเรียกซ้ำมีข้อเสียที่อัลกอริทึมที่คุณเขียนโดยใช้การเรียกซ้ำมีความซับซ้อนของพื้นที่ O (n) ในขณะที่ aproach ซ้ำมีความซับซ้อนของพื้นที่ของ O (1) นี่คือข้อดีของการใช้การทำซ้ำมากกว่าการเรียกซ้ำ แล้วทำไมเราถึงใช้การเรียกซ้ำ

ดูด้านล่าง

บางครั้งมันง่ายกว่าที่จะเขียนอัลกอริธึมโดยใช้การเรียกซ้ำในขณะที่ยากกว่าเล็กน้อยในการเขียนอัลกอริทึมเดียวกันโดยใช้การวนซ้ำในกรณีนี้ถ้าคุณเลือกที่จะทำตามวิธีการทำซ้ำ


1

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

ตัวอย่างเช่นในบางภาษามีการใช้งานการเรียงลำดับแบบผสานหลายเธรดแบบเรียกซ้ำ

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


0

เท่าที่ฉันรู้ Perl ไม่ได้เพิ่มประสิทธิภาพการโทรแบบเรียกซ้ำ แต่คุณสามารถปลอมมันได้

sub f{
  my($l,$r) = @_;

  if( $l >= $r ){
    return $l;
  } else {

    # return f( $l+1, $r );

    @_ = ( $l+1, $r );
    goto &f;

  }
}

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

โปรดทราบว่าไม่มี " my @_;" หรือ " local @_;" ถ้าคุณทำมันจะไม่ทำงานอีกต่อไป


0

ด้วยการใช้เพียง Chrome 45.0.2454.85 m การเรียกซ้ำก็น่าจะเร็วขึ้น

นี่คือรหัส:

(function recursionVsForLoop(global) {
    "use strict";

    // Perf test
    function perfTest() {}

    perfTest.prototype.do = function(ns, fn) {
        console.time(ns);
        fn();
        console.timeEnd(ns);
    };

    // Recursion method
    (function recur() {
        var count = 0;
        global.recurFn = function recurFn(fn, cycles) {
            fn();
            count = count + 1;
            if (count !== cycles) recurFn(fn, cycles);
        };
    })();

    // Looped method
    function loopFn(fn, cycles) {
        for (var i = 0; i < cycles; i++) {
            fn();
        }
    }

    // Tests
    var curTest = new perfTest(),
        testsToRun = 100;

    curTest.do('recursion', function() {
        recurFn(function() {
            console.log('a recur run.');
        }, testsToRun);
    });

    curTest.do('loop', function() {
        loopFn(function() {
            console.log('a loop run.');
        }, testsToRun);
    });

})(window);

ผล

// 100 รันโดยใช้มาตรฐานสำหรับลูป

100x สำหรับการวนซ้ำ เวลาที่จะเสร็จสมบูรณ์: 7.683ms

// การวิ่ง 100 ครั้งโดยใช้การเรียกใช้ซ้ำโดยใช้การเรียกซ้ำ

เรียกใช้การเรียกซ้ำ 100x เวลาที่จะเสร็จสมบูรณ์: 4.841ms

ในภาพหน้าจอด้านล่างการเรียกซ้ำชนะอีกครั้งด้วยอัตรากำไรที่มากกว่าเมื่อวิ่งที่ 300 รอบต่อการทดสอบ

การเรียกซ้ำชนะอีกครั้ง!


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

0

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

ในระยะสั้น: 1) การสำรวจเส้นทางการสั่งซื้อซ้ำซ้ำไม่ใช่เรื่องง่าย - ทำให้ DFT มีความซับซ้อนมากขึ้น 2) ตรวจสอบรอบได้ง่ายขึ้นด้วยการเรียกซ้ำ

รายละเอียด:

ในกรณีที่เกิดซ้ำมันง่ายต่อการสร้าง traversals ก่อนและหลัง:

ลองนึกภาพคำถามที่ค่อนข้างธรรมดา: "พิมพ์งานทั้งหมดที่ควรดำเนินการเพื่อเรียกใช้งาน 5 เมื่องานขึ้นอยู่กับงานอื่น"

ตัวอย่าง:

    //key-task, value-list of tasks the key task depends on
    //"adjacency map":
    Map<Integer, List<Integer>> tasksMap = new HashMap<>();
    tasksMap.put(0, new ArrayList<>());
    tasksMap.put(1, new ArrayList<>());

    List<Integer> t2 = new ArrayList<>();
    t2.add(0);
    t2.add(1);
    tasksMap.put(2, t2);

    List<Integer> t3 = new ArrayList<>();
    t3.add(2);
    t3.add(10);
    tasksMap.put(3, t3);

    List<Integer> t4 = new ArrayList<>();
    t4.add(3);
    tasksMap.put(4, t4);

    List<Integer> t5 = new ArrayList<>();
    t5.add(3);
    tasksMap.put(5, t5);

    tasksMap.put(6, new ArrayList<>());
    tasksMap.put(7, new ArrayList<>());

    List<Integer> t8 = new ArrayList<>();
    t8.add(5);
    tasksMap.put(8, t8);

    List<Integer> t9 = new ArrayList<>();
    t9.add(4);
    tasksMap.put(9, t9);

    tasksMap.put(10, new ArrayList<>());

    //task to analyze:
    int task = 5;


    List<Integer> res11 = getTasksInOrderDftReqPostOrder(tasksMap, task);
    System.out.println(res11);**//note, no reverse required**

    List<Integer> res12 = getTasksInOrderDftReqPreOrder(tasksMap, task);
    Collections.reverse(res12);//note reverse!
    System.out.println(res12);

    private static List<Integer> getTasksInOrderDftReqPreOrder(Map<Integer, List<Integer>> tasksMap, int task) {
         List<Integer> result = new ArrayList<>();
         Set<Integer> visited = new HashSet<>();
         reqPreOrder(tasksMap,task,result, visited);
         return result;
    }

private static void reqPreOrder(Map<Integer, List<Integer>> tasksMap, int task, List<Integer> result, Set<Integer> visited) {

    if(!visited.contains(task)) {
        visited.add(task);
        result.add(task);//pre order!
        List<Integer> children = tasksMap.get(task);
        if (children != null && children.size() > 0) {
            for (Integer child : children) {
                reqPreOrder(tasksMap,child,result, visited);
            }
        }
    }
}

private static List<Integer> getTasksInOrderDftReqPostOrder(Map<Integer, List<Integer>> tasksMap, int task) {
    List<Integer> result = new ArrayList<>();
    Set<Integer> visited = new HashSet<>();
    reqPostOrder(tasksMap,task,result, visited);
    return result;
}

private static void reqPostOrder(Map<Integer, List<Integer>> tasksMap, int task, List<Integer> result, Set<Integer> visited) {
    if(!visited.contains(task)) {
        visited.add(task);
        List<Integer> children = tasksMap.get(task);
        if (children != null && children.size() > 0) {
            for (Integer child : children) {
                reqPostOrder(tasksMap,child,result, visited);
            }
        }
        result.add(task);//post order!
    }
}

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

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

    List<Integer> res1 = getTasksInOrderDftStack(tasksMap, task);
    Collections.reverse(res1);//note reverse!
    System.out.println(res1);

    private static List<Integer> getTasksInOrderDftStack(Map<Integer, List<Integer>> tasksMap, int task) {
    List<Integer> result = new ArrayList<>();
    Set<Integer> visited = new HashSet<>();
    Stack<Integer> st = new Stack<>();


    st.add(task);
    visited.add(task);

    while(!st.isEmpty()){
        Integer node = st.pop();
        List<Integer> children = tasksMap.get(node);
        result.add(node);
        if(children!=null && children.size() > 0){
            for(Integer child:children){
                if(!visited.contains(child)){
                    st.add(child);
                    visited.add(child);
                }
            }
        }
        //If you put it here - it does not matter - it is anyway a pre-order
        //result.add(node);
    }
    return result;
}

ดูง่ายใช่มั้ย

แต่มันเป็นกับดักในการสัมภาษณ์

มันหมายถึงสิ่งต่อไปนี้: ด้วยวิธีการเรียกซ้ำคุณสามารถใช้การสำรวจเส้นทางแรกและเลือกลำดับที่คุณต้องการก่อนหรือหลังการโพสต์ (เพียงแค่เปลี่ยนตำแหน่งของ "การพิมพ์" ในกรณีของ "การเพิ่มลงในรายการผลลัพธ์" ) ด้วยซ้ำ (หนึ่งสแต็ค) วิธีการที่คุณสามารถได้อย่างง่ายดายทำเพียงสั่งซื้อล่วงหน้าสำรวจเส้นทางและดังนั้นในสถานการณ์เมื่อเด็กจำเป็นต้องได้รับการตีพิมพ์ครั้งแรก (สวยมากทุกสถานการณ์เมื่อคุณต้องการเริ่มต้นการพิมพ์จากโหนดล่างจะขึ้นไป) - คุณอยู่ใน ปัญหา. หากคุณมีปัญหานั้นคุณสามารถย้อนกลับได้ในภายหลัง แต่มันจะเป็นส่วนเสริมของอัลกอริทึมของคุณ และหากผู้สัมภาษณ์กำลังดูนาฬิกาของเขามันอาจเป็นปัญหาสำหรับคุณ มีวิธีการที่ซับซ้อนในการทำการสำรวจเส้นทางที่มีการโพสต์ซ้ำตามลำดับ แต่มันไม่ง่ายไม่ง่ายตัวอย่าง:https://www.geeksforgeeks.org/iterative-postorder-traversal-using-stack/

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

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

ข้อดีอีกอย่างของการเรียกซ้ำ - มันง่ายกว่าที่จะหลีกเลี่ยง / สังเกตวงจรในกราฟ

ตัวอย่าง (preudocode):

dft(n){
    mark(n)
    for(child: n.children){
        if(marked(child)) 
            explode - cycle found!!!
        dft(child)
    }
    unmark(n)
}

0

มันอาจจะสนุกที่จะเขียนมันเป็นการเรียกซ้ำหรือเป็นการฝึกฝน

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

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

ทุกครั้งที่อัลกอริทึมเกิดขึ้นซ้ำขนาดของข้อมูลหรือnลดลงเท่าใด

หากคุณลดขนาดของข้อมูลnลงครึ่งหนึ่งทุกครั้งที่คุณเรียกเก็บเงินจากนั้นโดยทั่วไปคุณไม่ต้องกังวลเกี่ยวกับการล้นสแต็ก สมมติว่าถ้ามันต้องมีความลึก 4,000 ระดับหรือลึก 10,000 ระดับสำหรับโปรแกรมที่จะโอเวอร์โฟลว์ขนาดของข้อมูลของคุณจะต้องประมาณ 2 4000สำหรับโปรแกรมของคุณในการสแต็คล้น อุปกรณ์จัดเก็บข้อมูลที่ใหญ่ที่สุดสามารถเก็บได้ 2 61ไบต์และหากคุณมีอุปกรณ์ดังกล่าว2 61คุณจะจัดการกับขนาดข้อมูล2 122เท่านั้น หากคุณกำลังมองดูอะตอมทั้งหมดในจักรวาลคาดว่ามันอาจจะน้อยกว่า 2 84. หากคุณต้องการจัดการกับข้อมูลทั้งหมดในเอกภพและรัฐของพวกเขาเป็นเวลาหนึ่งพันทุกวินาทีนับตั้งแต่การกำเนิดของเอกภพที่คาดไว้เมื่อประมาณ 14 พันล้านปีก่อนมันอาจจะเป็นเพียง 2 153เท่านั้น ดังนั้นหากโปรแกรมของคุณสามารถจัดการข้อมูล 2 4000หน่วยหรือnคุณสามารถจัดการข้อมูลทั้งหมดในจักรวาลและโปรแกรมจะไม่สแตกล้น หากคุณไม่ต้องการจัดการกับตัวเลขที่มีขนาดใหญ่เท่ากับ 2 4000 (จำนวนเต็ม 4000 บิต) โดยทั่วไปแล้วคุณไม่จำเป็นต้องกังวลเกี่ยวกับสแต็คล้น

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

ดังนั้นหากคุณมีความเป็นไปได้ที่จะสแต็กล้นให้ลองทำซ้ำวิธีแก้ปัญหา


-1

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

เราแนะนำประเภทสำหรับต้นไม้อย่างง่าย:

data Tree a = Branch (Tree a) (Tree a)
            | Leaf a
            deriving (Eq)

เราสามารถอ่านคำจำกัดความนี้ว่า "ต้นไม้เป็นสาขา (ซึ่งมีต้นไม้สองต้น) หรือเป็นใบไม้ (ซึ่งมีค่าข้อมูล)" ดังนั้นใบไม้จึงเป็นกรณีเล็ก ๆ หากต้นไม้ไม่ได้เป็นใบมันจะต้องเป็นต้นไม้ประสมที่มีต้นไม้สองต้น เหล่านี้เป็นกรณีเท่านั้น

มาสร้างต้นไม้กันเถอะ:

example :: Tree Int
example = Branch (Leaf 1) 
                 (Branch (Leaf 2) 
                         (Leaf 3))

ทีนี้สมมติว่าเราต้องการเพิ่ม 1 ในแต่ละค่าในทรี เราสามารถทำได้โดยโทร:

addOne :: Tree Int -> Tree Int
addOne (Branch a b) = Branch (addOne a) (addOne b)
addOne (Leaf a)     = Leaf (a + 1)

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

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

นอกจากนี้การเรียกซ้ำแบบนี้มักจะสามารถแยกออกได้ในแง่ของ "functor" เราสามารถสร้าง Trees ให้เป็น Functors โดยการกำหนด:

instance Functor Tree where fmap f (Leaf a)     = Leaf (f a)
                            fmap f (Branch a b) = Branch (fmap f a) (fmap f b)

และกำหนด:

addOne' = fmap (+1)

เราสามารถแยกแยะแผนการเรียกซ้ำอื่น ๆ เช่น catamorphism (หรือ fold) สำหรับประเภทข้อมูลเกี่ยวกับพีชคณิต ใช้ catamorphism เราสามารถเขียน:

addOne'' = cata go where
           go (Leaf a) = Leaf (a + 1)
           go (Branch a b) = Branch a b

-2

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

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

ภาษาระดับที่สูงขึ้นและแม้แต่ clang / cpp อาจนำมาใช้ในพื้นหลังเหมือนกัน


1
"Stack overflow จะเกิดขึ้นเฉพาะเมื่อคุณเขียนโปรแกรมในภาษาที่ไม่มีในการจัดการหน่วยความจำภายใน" - ไม่มีเหตุผล ภาษาส่วนใหญ่ใช้สแต็กที่มีขนาด จำกัด ดังนั้นการเรียกซ้ำจะทำให้เกิดความล้มเหลวในไม่ช้า
StaceyGirl
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.