มีอะไรที่สามารถทำได้ด้วยการเรียกซ้ำที่ไม่สามารถทำได้ด้วยการวนซ้ำ?


126

มีบางครั้งที่การใช้การเรียกซ้ำนั้นดีกว่าการใช้การวนซ้ำและเวลาที่การใช้การวนซ้ำนั้นดีกว่าการใช้การวนซ้ำ การเลือก "สิทธิ" สามารถบันทึกทรัพยากรและ / หรือส่งผลให้มีรหัสบรรทัดน้อยลง

มีกรณีใดบ้างที่ภารกิจสามารถทำได้โดยใช้การเรียกซ้ำเท่านั้นแทนที่จะวนซ้ำหรือไม่?


13
ฉันสงสัยอย่างจริงจัง การเรียกซ้ำเป็นลูปที่มีการสรรเสริญ
การแข่งขัน Lightness ใน Orbit

6
การเห็นทิศทางที่แตกต่างกันซึ่งคำตอบนั้นเกิดขึ้น (และทำให้ตัวเองล้มเหลวในการให้คำแนะนำที่ดีกว่า) คุณอาจทำใครก็ตามที่พยายามตอบคำตอบถ้าคุณให้ภูมิหลังเพิ่มขึ้นอีกเล็กน้อย คุณต้องการการพิสูจน์เชิงทฤษฎีสำหรับเครื่องจักรสมมุติ (ที่มีพื้นที่เก็บข้อมูลไม่ จำกัด และเวลาทำงาน) หรือไม่? หรือตัวอย่างการปฏิบัติ? (ที่“ จะซับซ้อนอย่างน่าขัน” อาจมีคุณสมบัติว่า“ ทำไม่ได้”) หรือมีอะไรที่แตกต่างออกไป?
5gon12eder

8
@LightnessRacesinOrbit สำหรับหูที่ไม่ใช่เจ้าของภาษาอังกฤษของฉัน "Recursion is a glorified loop" ฟังคุณหมายถึง "คุณอาจใช้โครงสร้างลูปแทนการเรียกซ้ำทุกที่และแนวคิดไม่สมควรได้รับชื่อของตัวเอง" . บางทีฉันอาจตีความสำนวนที่ "ยกย่องบางสิ่ง" ผิดไป
hyde

13
ฟังก์ชั่นของ Ackermann คืออะไร? en.wikipedia.org/wiki/Ackermann_functionไม่มีประโยชน์อย่างยิ่ง แต่เป็นไปไม่ได้ที่จะทำผ่านการวนซ้ำ (คุณอาจต้องการตรวจสอบวิดีโอนี้youtube.com/watch?v=i7sm9dzFtEIโดย Computerphile)
WizardOfMenlo

8
@WizardOfMenlo รหัส befunge เป็นการใช้งานโซลูชันERRE (ซึ่งเป็นโซลูชันแบบโต้ตอบ ... ด้วยสแต็ก) วิธีการวนซ้ำด้วยสแต็กสามารถเลียนแบบการเรียกซ้ำ ในการเขียนโปรแกรมใด ๆ ที่มีประสิทธิภาพเหมาะสมสร้างหนึ่งวนสามารถใช้เพื่อเลียนแบบอื่น เครื่องลงทะเบียนคำแนะนำINC (r), JZDEC (r, z)สามารถใช้เครื่องทัวริง มันไม่มี 'การเรียกซ้ำ' - นั่นเป็นการกระโดดถ้าไม่มีศูนย์อื่นลดลง ถ้าฟังก์ชั่น Ackermann คำนวณได้ (เป็น) เครื่องลงทะเบียนสามารถทำได้

คำตอบ:


164

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

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

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


120
โดยพื้นฐานแล้วการทำลูปแทนการเรียกซ้ำหมายถึงการจัดการสแต็กด้วยตนเอง
Silviu Burcea

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

35
Therefore, the one thing recursion can do that loops can't is make some tasks super easy. และสิ่งหนึ่งที่ลูปสามารถทำได้นั้นการเรียกซ้ำไม่สามารถทำให้งานบางอย่างง่ายสุด ๆ คุณเคยเห็นสิ่งที่น่าเกลียดและไม่ได้ใช้งานง่ายที่คุณต้องทำเพื่อแปลงปัญหาที่เกิดขึ้นซ้ำ ๆ ตามธรรมชาติมากที่สุดจากการเรียกซ้ำแบบไร้เดียงสาไปจนถึงการเรียกซ้ำแบบหางเพื่อที่พวกเขาจะไม่ระเบิดกองซ้อน?
Mason Wheeler

10
@MasonWheeler 99% ของเวลา "สิ่ง" เหล่านั้นสามารถห่อหุ้มได้ดีกว่าในตัวดำเนินการเรียกซ้ำmapหรือfold(ในความเป็นจริงถ้าคุณเลือกที่จะพิจารณาแบบดั้งเดิมฉันคิดว่าคุณสามารถใช้fold/ unfoldเป็นทางเลือกที่สามในการวนซ้ำหรือเรียกซ้ำ) นอกจากว่าคุณกำลังเขียนรหัสไลบรารีมีหลายกรณีที่คุณควรกังวลเกี่ยวกับการใช้การวนซ้ำมากกว่างานที่ควรจะทำสำเร็จ - ในทางปฏิบัตินั่นหมายถึงลูปที่ชัดเจนและการเรียกซ้ำอย่างชัดเจนมีทั้งที่ไม่เท่าเทียมกัน abstractions ที่ควรหลีกเลี่ยงในระดับสูงสุด
Leushenko

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

78

เลขที่

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

การเขียนโปรแกรมภาษาใด ๆ ที่สามารถใช้เครื่องทัวริงเรียกว่าทัวริงสมบูรณ์ และมีภาษามากมายที่ทำให้สมบูรณ์

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

สิ่งนี้ทำให้เดือดมากเหลือน้อย:

  • สิ่งใด ๆ ที่คำนวณได้นั้นสามารถคำนวณได้บนเครื่องทัวริง
  • ภาษาใด ๆ ที่สามารถนำเครื่องทัวริง (เรียกว่าทัวริงสมบูรณ์) สามารถคำนวณอะไรก็ได้ที่ภาษาอื่นสามารถทำได้
  • เนื่องจากมีเครื่องจักรทัวริงในภาษาที่ขาด recursion (และมีคนอื่น ๆ ที่เพียง แต่มีการเรียกซ้ำเมื่อคุณได้รับในบางส่วนของ esolangs อื่น ๆ ) ก็จำเป็นต้องเป็นจริงว่ามีอะไรที่คุณสามารถทำได้ด้วยการเรียกซ้ำว่าคุณไม่สามารถทำอะไรกับ วนรอบ (และคุณไม่สามารถทำอะไรกับวนซ้ำที่คุณไม่สามารถทำได้ด้วยการเรียกซ้ำ)

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

และในขณะที่ฉันนำสิ่งนี้ไปที่ 'esolang' สุดขีด (ส่วนใหญ่เป็นเพราะคุณสามารถค้นหาสิ่งที่ทัวริงสมบูรณ์และนำไปใช้ในรูปแบบที่ค่อนข้างแปลก) นี้ไม่ได้หมายความว่า esolangs จะเป็นทางเลือก มีรายการทั้งหมดที่ทัวริงเสร็จสมบูรณ์โดยไม่ตั้งใจรวมถึง Magic the Gathering, Sendmail, MediaWiki template และระบบชนิด Scala สิ่งเหล่านี้จำนวนมากอยู่ไกลจากความเหมาะสมเมื่อพูดถึงการทำสิ่งต่าง ๆ ที่เป็นจริงเพียงแค่คุณสามารถคำนวณสิ่งที่คำนวณได้โดยใช้เครื่องมือเหล่านี้


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

หากคุณมีสมมุติว่าเป็นแฟกทอเรียลที่เขียนเป็น:

int fact(int n) {
    return fact(n, 1);
}

int fact(int n, int accum) {
    if(n == 0) { return 1; }
    if(n == 1) { return accum; }
    return fact(n-1, n * accum);
}

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

นอกจากนี้ยังมีเวลาที่แปลงห่วงง่ายในการเรียกหางโทร recursive สามารถที่ซับซ้อนและมากขึ้นยากที่จะเข้าใจ


หากคุณต้องการเข้าสู่ด้านทฤษฎีของมันให้ดูที่วิทยานิพนธ์ทัวริสทัวริส คุณอาจพบว่าการทำวิทยานิพนธ์ของคริสตจักรใน CS.SE มีประโยชน์เช่นกัน


29
ทัวริงสมบูรณ์ถูกโยนไปรอบ ๆ มากเกินไปเหมือนมันเป็นเรื่องสำคัญ มีหลายสิ่งที่ทัวริงเสร็จสมบูรณ์ ( เช่น Magic the Gathering ) แต่นั่นไม่ได้หมายความว่ามันเหมือนกับสิ่งอื่น ๆ ที่เป็นทัวริงที่สมบูรณ์ อย่างน้อยก็ไม่ได้อยู่ในระดับที่สำคัญ ฉันไม่ต้องการเดินต้นไม้ด้วย Magic the Gathering
Scant Roger

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

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

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

7
"สามารถ" ไม่มากเหมือน "ดีที่สุด" เมื่อคุณเข้าใจผิดว่า "ไม่ดีที่สุด" สำหรับ "ไม่สามารถ" คุณจะเป็นอัมพาตเพราะไม่ว่าคุณจะทำอะไรบางอย่างมันก็มีวิธีที่ดีกว่าเสมอ
Steven Burnap

31

มีกรณีใดบ้างที่ภารกิจสามารถทำได้โดยใช้การเรียกซ้ำเท่านั้นแทนที่จะวนซ้ำหรือไม่?

คุณสามารถเปลี่ยนอัลกอริธึมวนซ้ำเป็นวงวนซึ่งใช้โครงสร้างข้อมูล Last-In-First-Out (AKA stack) เพื่อจัดเก็บสถานะชั่วคราวเนื่องจากการเรียกซ้ำซ้ำนั้นเป็นสิ่งที่แน่นอนโดยจัดเก็บสถานะปัจจุบันไว้ในกองดำเนินการตามขั้นตอนวิธี จากนั้นจึงเรียกคืนสถานะในภายหลัง คำตอบสั้น ๆ คือ: ไม่ไม่มีกรณีเช่นนี้

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

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


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

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


2
เมื่อต้องจัดการกับแอสเซมบลีสำหรับโปรเซสเซอร์ที่ไม่มีสแต็ค
Joshua

@Joshua แน่นอน! มันเป็นเรื่องของระดับของสิ่งที่เป็นนามธรรม ถ้าคุณลดลงไปหนึ่งหรือสองระดับมันก็เป็นเพียงประตูลอจิก
ไฮด์

2
ไม่ถูกต้องนัก หากต้องการจำลองการวนซ้ำด้วยการวนซ้ำคุณต้องมีสแต็กที่สามารถเข้าถึงแบบสุ่มได้ สแต็คเดี่ยวที่ไม่มีการเข้าถึงแบบสุ่มรวมถึงหน่วยความจำที่เข้าถึงได้โดยตรงจำนวน จำกัด คือ PDA ซึ่งไม่ได้รับการทำให้สมบูรณ์
Gilles

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

20

ขึ้นอยู่กับว่าคุณกำหนด "การเรียกซ้ำ" อย่างเข้มงวดเพียงใด

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

แต่ให้พิจารณากรณีที่เราโทรซ้ำและใช้ผลการโทรซ้ำสำหรับการโทรซ้ำนั้น

public static BigInteger Ackermann(BigInteger m, BigInteger n)
{
  if (m == 0)
    return  n+1;
  if (n == 0)
    return Ackermann(m - 1, 1);
  else
    return Ackermann(m - 1, Ackermann(m, n - 1));
}

การโทรซ้ำแบบเรียกซ้ำครั้งแรกทำได้ง่าย:

public static BigInteger Ackermann(BigInteger m, BigInteger n)
{
restart:
  if (m == 0)
    return  n+1;
  if (n == 0)
  {
    m--;
    n = 1;
    goto restart;
  }
  else
    return Ackermann(m - 1, Ackermann(m, n - 1));
}

จากนั้นเราสามารถทำความสะอาดลบgotoเพื่อกำจัดvelociraptorsและร่มเงาของ Dijkstra:

public static BigInteger Ackermann(BigInteger m, BigInteger n)
{
  while(m != 0)
  {
    if (n == 0)
    {
      m--;
      n = 1;
    }
    else
      return Ackermann(m - 1, Ackermann(m, n - 1));
  }
  return  n+1;
}

แต่หากต้องการลบการโทรแบบเรียกซ้ำอื่น ๆ เราจะต้องเก็บค่าการโทรบางส่วนไว้ในสแต็ก:

public static BigInteger Ackermann(BigInteger m, BigInteger n)
{
  Stack<BigInteger> stack = new Stack<BigInteger>();
  stack.Push(m);
  while(stack.Count != 0)
  {
    m = stack.Pop();
    if(m == 0)
      n = n + 1;
    else if(n == 0)
    {
      stack.Push(m - 1);
      n = 1;
    }
    else
    {
      stack.Push(m - 1);
      stack.Push(m);
      --n;
    }
  }
  return n;
}

ตอนนี้เมื่อเราพิจารณาซอร์สโค้ดเราได้เปลี่ยนวิธีการเรียกซ้ำของเราให้เป็นซ้ำอย่างแน่นอน

เมื่อพิจารณาถึงสิ่งที่ถูกคอมไพล์เราได้เปลี่ยนรหัสที่ใช้ call stack เพื่อนำ recursion มาใช้เป็นโค้ดที่ไม่ (และในการทำโค้ดหันที่จะโยนข้อยกเว้น stack-overflow สำหรับค่าที่ค่อนข้างเล็กลงในโค้ดที่จะ ใช้เวลานานอย่างเลือดตาแทบกระเด็นเพื่อส่งคืน [ดูฉันจะป้องกันไม่ให้ฟังก์ชั่น Ackerman ของฉันล้นสแต็คได้อย่างไรสำหรับการเพิ่มประสิทธิภาพเพิ่มเติมที่ทำให้มันกลับมาจริงสำหรับอินพุตที่เป็นไปได้มากขึ้น])

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

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

แน่นอนทั้งหมดนี้ถือว่าคุณมีทางเลือก มีทั้งภาษาที่ห้ามการโทรซ้ำและภาษาที่ไม่มีโครงสร้างการวนซ้ำที่จำเป็นสำหรับการวนซ้ำ


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

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

@gerrit และแคบลงก็หลีกเลี่ยงได้ ในที่สุดมันก็ลงมาที่ขอบของสิ่งที่เราทำหรือไม่ใช้ป้ายกำกับที่มีประโยชน์นี้ที่เราใช้สำหรับรหัสบางอย่าง
Jon Hanna

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

9

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


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

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

โอเคทีนี้เราลองเอาเท้าข้างหนึ่งไปวางในแนวปฏิบัติเพื่อยึดเท้าอีกฝั่งไว้ในทฤษฏี


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

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

mergesortสมมติว่าคุณกำลังดำเนินการไม่มีทาง คุณอาจมีการforวนซ้ำที่ผ่านแต่ละNเซ็กเมนต์เรียกmergesortพวกมันแยกกันจากนั้นผสานผลลัพธ์

คุณจะทำให้มันขนานกับ OpenMP ได้อย่างไร?

ในขอบเขตแบบวนซ้ำมันง่ายมาก: แค่#pragma omp parallel forวนลูปของคุณที่เปลี่ยนจาก 1 เป็น N แล้วคุณก็ทำเสร็จแล้ว ในขอบเขตการวนซ้ำคุณไม่สามารถทำได้ คุณต้องวางไข่เธรดด้วยตนเองและส่งผ่านข้อมูลที่เหมาะสมด้วยตนเองเพื่อให้พวกเขารู้ว่าต้องทำอะไร

ในทางกลับกันมีเครื่องมืออื่น ๆ (เช่น vectorizers อัตโนมัติเช่น#pragma vector) ที่ทำงานกับลูป แต่ไร้ประโยชน์อย่างเต็มที่กับการเรียกซ้ำ

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

เช่น: เครื่องมือสำหรับกระบวนทัศน์หนึ่งจะไม่แปลเป็นกระบวนทัศน์อื่นโดยอัตโนมัติ

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


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

8

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

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

เพื่อสรุป:

  • กรณีการเรียกซ้ำ = การควบคุมการไหล + สแต็ก (+ ฮีป)
  • กรณีลูป = การควบคุมการไหล + กอง

สมมติว่าส่วนการควบคุมการไหลนั้นทรงพลังพอสมควรความแตกต่างเพียงอย่างเดียวก็คือในประเภทหน่วยความจำที่มีอยู่ ดังนั้นเราจะเหลือ 4 กรณี (กำลังแสดงอยู่ในวงเล็บ):

  1. ไม่มีสแต็คไม่มีฮีป: การเรียกซ้ำและโครงสร้างแบบไดนามิกเป็นไปไม่ได้ (เรียกซ้ำ = วนซ้ำ)
  2. สแต็คไม่มีฮีป: การเรียกซ้ำเป็นปกติโครงสร้างแบบไดนามิกเป็นไปไม่ได้ (การเรียกซ้ำ> ลูป)
  3. ไม่มีสแต็คฮีป: การเรียกซ้ำเป็นไปไม่ได้โครงสร้างแบบไดนามิกนั้นใช้ได้ (เรียกซ้ำ = วนซ้ำ)
  4. สแต็คฮีป: การเรียกซ้ำและโครงสร้างแบบไดนามิกนั้นใช้ได้ (เรียกซ้ำ = วนซ้ำ)

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

  1. ไม่มีสแต็คไม่มีฮีป: การเรียกซ้ำและโครงสร้างแบบไดนามิกเป็นไปไม่ได้ (เรียกซ้ำ <ลูป)
  2. สแต็คไม่มีฮีป: การเรียกซ้ำเป็นปกติโครงสร้างแบบไดนามิกเป็นไปไม่ได้ (การเรียกซ้ำ> ลูป)
  3. ไม่มีสแต็คฮีป: การเรียกซ้ำเป็นไปไม่ได้โครงสร้างแบบไดนามิกนั้นใช้ได้ (เรียกซ้ำ <ลูป)
  4. สแต็คฮีป: การเรียกซ้ำและโครงสร้างแบบไดนามิกนั้นใช้ได้ (เรียกซ้ำ = วนซ้ำ)

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


2

ใช่. มีงานทั่วไปหลายอย่างที่ง่ายต่อการทำให้สำเร็จโดยใช้การเรียกซ้ำ แต่เป็นไปไม่ได้ด้วยการวนซ้ำ:

  • ก่อให้เกิดการล้นสแต็ค
  • โปรแกรมเมอร์เริ่มต้นสับสนโดยสิ้นเชิง
  • การสร้างฟังก์ชั่นที่ดูรวดเร็วซึ่งจริงๆแล้วคือ O (n ^ n)

3
ได้โปรดพวกมันง่ายมากกับลูปฉันเห็นพวกมันตลอดเวลา เฮ้ด้วยความพยายามเล็กน้อยคุณไม่จำเป็นต้องมีลูป แม้ว่าการเรียกซ้ำนั้นง่ายกว่า
AviD

1
จริงๆแล้ว A (0, n) = n + 1; A (m, 0) = A (m-1,1) ถ้า m> 0; A (m, n) = A (m-1, A (m, n-1)) ถ้า m> 0, n> 0 เติบโตได้เร็วกว่า O (n ^ n) (สำหรับ m = n) :)
John Donn

1
@JohnDonn ยิ่งกว่านั้นมันยิ่งใหญ่มาก สำหรับ n = 3 n ^ n ^ n สำหรับ n = 4 n ^ n ^ n ^ n ^ n และอื่น ๆ n กำลัง n กำลัง n คูณ
Aaron McMillin

1

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

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


3
ฉันไม่แน่ใจว่าสิ่งนี้ใช้ได้กับคำถามข้างต้นอย่างไร คุณช่วยทำให้การเชื่อมต่อนั้นชัดเจนยิ่งขึ้นได้ไหม?
Yakk

1
แทนที่ "loop" ที่ไม่แน่ชัดด้วยความแตกต่างที่สำคัญระหว่าง "loop ที่มีจำนวนการทำซ้ำ จำกัด " และ "loop with count iteration ไม่ จำกัด " ซึ่งฉันคิดว่าทุกคนจะรู้จาก CS 101
gnasher729

แน่นอน แต่มันยังใช้ไม่ได้กับคำถาม คำถามเกี่ยวกับการวนซ้ำและการเรียกซ้ำไม่ใช่การเรียกซ้ำแบบดั้งเดิมและการเรียกซ้ำ ลองนึกภาพว่ามีคนถามถึงความแตกต่างของ C / C ++ และคุณตอบเกี่ยวกับความแตกต่างระหว่าง K&R C กับ Ansi C. แน่นอนว่ามันทำให้สิ่งต่าง ๆ มีความแม่นยำมากขึ้น แต่ก็ไม่ตอบคำถาม
Yakk

1

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


0
  • ถ้าการเรียกซ้ำเป็นคำสั่งแรกสุดหรือคำสั่งสุดท้าย (ไม่รวมการตรวจสอบเงื่อนไข) ของฟังก์ชันเรียกซ้ำมันง่ายที่จะแปลเป็นโครงสร้างลูป
  • แต่ถ้าฟังก์ชั่นทำบางอย่างก่อนและหลังการเรียกซ้ำแล้วมันจะยุ่งยากในการแปลงเป็นลูป
  • หากฟังก์ชั่นนี้มีการเรียกซ้ำหลายครั้งการแปลงเป็นรหัสที่ใช้การวนซ้ำจะเป็นไปไม่ได้ สแต็กบางอย่างจะต้องติดตามข้อมูล ในการเรียกซ้ำสแต็กการเรียกเองจะทำงานเป็น data-stack

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

@CodesInChaos แก้ไขแล้ว
Gulshan

-6

ฉันเห็นด้วยกับคำถามอื่น ๆ ไม่มีอะไรที่คุณสามารถทำได้ด้วยการเรียกซ้ำคุณไม่สามารถทำอะไรกับลูปได้

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


2
การใช้ตัวเลขแบบฟีโบนัชชีแบบเรียกซ้ำด้วยการเรียกซ้ำจะเรียกใช้ "หมดเวลา" ก่อนที่จะหมดพื้นที่สแต็ก ฉันเดาว่ามีปัญหาอื่น ๆ ที่ดีกว่าสำหรับตัวอย่างนี้ นอกจากนี้สำหรับปัญหาหลายอย่างรุ่นลูปมีผลกระทบต่อหน่วยความจำเดียวกับแบบวนซ้ำโดยเฉพาะในฮีปแทนที่จะเป็นสแต็ก
Paŭlo Ebermann

6
วนอาจเป็น "อันตรายมาก" ถ้าคุณลืมที่จะเพิ่มตัวแปรวน ...
h22

2
ดังนั้นการสร้าง stack overflow อย่างจงใจนั้นเป็นงานที่ยุ่งยากมากโดยไม่ต้องเรียกซ้ำ
5gon12eder

@ 5gon12eder ซึ่งนำเราไปยังวิธีการใดที่มีเพื่อหลีกเลี่ยงการล้นกองในอัลกอริทึมแบบเรียกซ้ำ? - การเขียนเพื่อมีส่วนร่วม TCO หรือการมีส่วนร่วมอาจเป็นประโยชน์ Iterative vs. Recursive Approachก็น่าสนใจเพราะมันเกี่ยวข้องกับวิธีการแบบเรียกซ้ำสองวิธีที่แตกต่างกันสำหรับ Fibonacci

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