ขณะที่วนซ้ำภายในเรียกซ้ำ?


37

ฉันสงสัยว่าขณะที่ลูปนั้นวนซ้ำอยู่ภายในหรือไม่

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



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

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

6
สิ่งเดียวกันที่แท้จริงหมายถึงที่นี่? ในการเขียนโปรแกรมการใช้การเรียกซ้ำหมายถึงสิ่งที่เฉพาะเจาะจงซึ่งแตกต่างจากการวนซ้ำ (ลูป) ใน CS เมื่อคุณเข้าใกล้ด้านคณิตศาสตร์เชิงทฤษฎีของสิ่งต่างๆมากขึ้นสิ่งเหล่านี้จะเริ่มมีความหมายแตกต่างกันเล็กน้อย
hyde

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

คำตอบ:


116

ลูปเป็นอย่างมากไม่เรียกซ้ำ ในความเป็นจริงพวกเขาเป็นตัวอย่างที่สำคัญของฝั่งตรงข้ามกลไก: ซ้ำ

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

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

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


10
@Giorgio นั่นอาจเป็นจริง แต่มันเป็นความเห็นเกี่ยวกับการอ้างสิทธิ์คำตอบที่ไม่ได้ทำ "Arbitrently" ไม่ปรากฏในคำตอบนี้และจะเปลี่ยนความหมายอย่างมีนัยสำคัญ
hvd

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

15
@Giorgio "นี่เป็นสิ่งที่ recursion ใช้งานอยู่: เรียกตัวเองว่ามีอาร์กิวเมนต์ใหม่" - ยกเว้นการเรียก และข้อโต้แย้ง
ฮอบส์

12
@Giorgio คุณกำลังใช้คำจำกัดความที่แตกต่างกันของคำมากกว่าที่นี่มากที่สุด คำพูดที่คุณรู้ว่าเป็นพื้นฐานของการสื่อสาร นี่คือโปรแกรมเมอร์ไม่ใช่ CS Stack Exchange หากเราใช้คำเช่น "อาร์กิวเมนต์", "การเรียก", "ฟังก์ชั่น" ฯลฯ ตามที่คุณแนะนำมันจะเป็นไปไม่ได้ที่จะพูดคุยเกี่ยวกับรหัสจริง
hyde

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

37

การพูดว่า X นั้นเป็นเนื้อแท้ใน Y จะสมเหตุสมผลถ้าคุณมีระบบ (เป็นทางการ) อยู่ในใจว่าคุณกำลังแสดง X เข้ามาหากคุณนิยามความหมายของwhileคำว่าแลคด้าแคลคูลัสคุณอาจพูดถึงการเรียกซ้ำ *; หากคุณกำหนดไว้ในแง่ของเครื่องลงทะเบียนคุณอาจจะไม่

ในกรณีใดกรณีหนึ่งผู้คนอาจไม่เข้าใจคุณถ้าคุณเรียกใช้ฟังก์ชันแบบเรียกซ้ำเนื่องจากมันมีการวนรอบสักครู่

* foldแม้ว่าบางทีอาจจะเป็นเพียงทางอ้อมเช่นถ้าคุณกำหนดมันในแง่ของ


4
เพื่อความเป็นธรรมฟังก์ชั่นนี้ไม่ได้เรียกซ้ำในคำจำกัดความใด ๆ มันมีองค์ประกอบวนซ้ำวนซ้ำ
Luaan

@ Luaan: แน่นอนดังนั้น แต่เนื่องจากในภาษาที่มีการwhileเรียกซ้ำแบบสร้างโดยทั่วไปแล้วเป็นคุณสมบัติของฟังก์ชั่นฉันก็ไม่สามารถคิดอย่างอื่นที่จะอธิบายว่า "ซ้ำ" ในบริบทนี้
Anton Golov

36

ขึ้นอยู่กับมุมมองของคุณ

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

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

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

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

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


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

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

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

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

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


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

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

ฉันหมายถึงการไม่มีอะไรอื่นนอกจากตัวแปรแบบสแตติกหรือแบบอัตโนมัติอย่างง่ายนั่นคือไม่ได้เป็นแบบทัวริง ภาษาย้ำ - ย้ำจะถูก จำกัด อยู่ที่งานที่สามารถทำได้ด้วยการ จำกัด เครื่องจักรง่าย ๆ ขณะที่ภาษา recursive จะเพิ่มความสามารถในการปฏิบัติงานที่จะต้องมีอย่างน้อยที่สุดก็ต้อง จำกัด การกดเครื่องจักร จำกัด
supercat

1
หากภาษานั้นไม่สมบูรณ์แบบไม่มีจุดเริ่มต้น DFA ไม่สามารถทำซ้ำได้ตามอำเภอใจหรือเรียกซ้ำ btw
Polygnome

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

12

ความแตกต่างคือสแต็กโดยนัยและความหมาย

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

การเรียกใช้ซ้ำไม่สามารถทำได้หากไม่มีสแต็กนี้โดยนัยซึ่งจะจดจำสถานะของงานที่ทำก่อนหน้านี้

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

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

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


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

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

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

1
@Groo: ดูen.wikipedia.org/wiki/Continuation-passing_style
ruakh

@ruakh: CPS โดยตัวมันเองสร้างกองซ้อนที่เหมือนกันดังนั้นจึงต้องพึ่งพาการกำจัด tail call เพื่อให้เข้าใจได้ (ซึ่งทำให้ไม่สำคัญเนื่องจากวิธีที่มันถูกสร้างขึ้น) แม้แต่บทความวิกิพีเดียที่คุณเชื่อมโยงไว้ก็บอกว่าเหมือนกัน: การใช้ CPS โดยไม่มีการเพิ่มประสิทธิภาพการโทรหาง (TCO) จะทำให้ไม่เพียง แต่ความต่อเนื่องที่สร้างขึ้นเพื่อเติบโตในระหว่างการเรียกซ้ำ แต่ยังรวมถึงสแตกการโทรด้วย
Groo

7

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

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

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

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

บรรทัดล่างสุด: คุณต้องสามารถบอกความแตกต่างได้ไม่เช่นนั้นโปรแกรมของคุณจะพัง


3

whileลูปเป็นรูปแบบของการเรียกซ้ำดูตัวอย่างคำตอบที่ยอมรับสำหรับคำถามนี้ พวกเขาสอดคล้องกับμ-operator ในทฤษฎีการคำนวณ (ดูตัวอย่างที่นี่ )

รูปแบบทั้งหมดของforลูปที่ย้ำในช่วงของตัวเลข, คอลเลกชัน จำกัด อาร์เรย์และอื่น ๆ ที่สอดคล้องกับการเรียกซ้ำดั้งเดิมเช่นดูที่นี่และที่นี่ โปรดทราบว่าforลูปของ C, C ++, Java และอื่น ๆ เป็นน้ำตาลซินแทกติกสำหรับwhileลูปและดังนั้นจึงไม่สอดคล้องกับการเรียกซ้ำแบบดั้งเดิม Pascal forloop เป็นตัวอย่างของการเรียกซ้ำแบบดั้งเดิม

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

แก้ไข

คำอธิบายบางอย่างเกี่ยวกับความคิดเห็นและคำตอบอื่น ๆ "การเรียกซ้ำเกิดขึ้นเมื่อมีการกำหนดสิ่งต่าง ๆ ในแง่ของตัวเองหรือประเภทของมัน" (ดูวิกิพีเดีย ) ดังนั้น,

ขณะที่วนซ้ำภายในเรียกซ้ำ?

เนื่องจากคุณสามารถกำหนดwhileวงในแง่ของตัวเอง

while p do c := if p then (c; while p do c))

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

อีกคำถามหนึ่งที่สันนิษฐานโดยนัยโดยคำตอบและความคิดเห็นมากมายคือ

ในขณะที่ฟังก์ชั่นวนซ้ำและแบบเรียกซ้ำ?

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

ดังนั้นความจริงที่ว่า "a whileloop เป็นรูปแบบของการเรียกซ้ำ" ไม่ได้ขัดแย้งกับความจริงที่ว่า "บางฟังก์ชั่นที่เรียกซ้ำไม่สามารถแสดงออกได้ด้วยการwhileวนซ้ำ"


2
@morbidCode: การเรียกซ้ำแบบดั้งเดิมและμ-recursion เป็นรูปแบบของการเรียกซ้ำด้วยข้อ จำกัด ที่เฉพาะเจาะจง (หรือขาดดังกล่าว) ศึกษาเช่นในทฤษฎีการคำนวณ เมื่อปรากฎว่าภาษาที่มีเพียงFORลูปสามารถคำนวณฟังก์ชัน recursive ดั้งเดิมทั้งหมดได้และภาษาที่มีเพียงWHILEลูปสามารถคำนวณฟังก์ชัน functions-recursive ทั้งหมดได้อย่างแน่นอน (และปรากฎว่าฟังก์ชัน rec-recursive นั้นเป็นฟังก์ชันที่ เครื่องทัวริงสามารถคำนวณได้) หรือเพื่อทำให้สั้น: การเรียกซ้ำแบบดั้งเดิมและ rec-recursion เป็นศัพท์ทางเทคนิคจากคณิตศาสตร์ / ทฤษฎีการคำนวณ
Jörg W Mittag

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

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

2
@DavidGrinberg: การอ้างถึง: "C, C ++, Java สำหรับลูปไม่ใช่ตัวอย่างของการเรียกซ้ำแบบดั้งเดิมการเรียกซ้ำแบบดั้งเดิมหมายความว่าจำนวนการวนซ้ำสูงสุด / ความลึกการเรียกซ้ำถูกแก้ไขก่อนที่จะเริ่มวนรอบ" จอร์โจพูดถึงทฤษฎีการคำนวณแบบดั้งเดิม ไม่เกี่ยวข้องกับภาษาการเขียนโปรแกรม
Mooing Duck

3
ฉันต้องเห็นด้วยกับ Mooing Duck ในขณะที่ทฤษฎีการคำนวณอาจจะน่าสนใจในเชิงทฤษฎี CS ฉันคิดว่าทุกคนยอมรับว่า OP กำลังพูดถึงแนวคิดการเขียนโปรแกรมภาษา
Voo

2

โทรหาง (หรือโทร recursive หาง) จะดำเนินการว่าเป็น "โกโตะมีข้อโต้แย้ง" (โดยไม่ผลักดันใด ๆเพิ่มเติมกรอบการโทรบนสแต็คโทร ) และในภาษาที่ทำงานบางคน (Ocaml สะดุดตา) เป็นวิธีปกติของการวนลูป

ดังนั้นในขณะที่ห่วง (ในภาษาที่มีพวกเขา) สามารถมองเห็นว่าลงท้ายด้วยหางเรียกร่างกายของมัน (หรือทดสอบหัวของมัน)

ในทำนองเดียวกันการโทรซ้ำแบบธรรมดา (ไม่ใช่การโทรหาง) สามารถจำลองโดยการวนซ้ำ (ใช้สแต็กบางตัว)

อ่านยังเกี่ยวกับการและรูปแบบต่อเนื่องผ่าน

ดังนั้น "การเรียกซ้ำ" และ "การทำซ้ำ" จึงเทียบเท่ากันอย่างลึกซึ้ง


1

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

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

 typedef struct List List;
 struct List
 {
     List* next;
     int element;
 };

 void print_list_loop(List* l)
 {
     List* it = l;
     while(it != NULL)
     {
          printf("Element: %d\n", it->element);
          it = it->next;
     }
 }

 void print_list_rec(List* l)
 {
      if(l == NULL) return;
      printf("Element: %d\n", l->element);
      print_list_rec(l->next);
 }

ง่ายใช่มั้ย ทีนี้ลองทำการดัดแปลงนิดหน่อย: พิมพ์รายการในลำดับย้อนกลับ

สำหรับตัวแปรแบบเรียกซ้ำนี่คือการปรับเปลี่ยนเล็กน้อยสำหรับฟังก์ชั่นดั้งเดิม:

void print_list_reverse_rec(List* l)
{
    if (l == NULL) return;
    print_list_reverse_rec(l->next);
    printf("Element: %d\n", l->element);
}

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

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

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

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


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

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

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

0

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

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

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

ในตัวอย่างต่อไปนี้ชีวิตคือฟังก์ชั่นใช้สองพารามิเตอร์ "กฎ" และ "รัฐ" และรัฐใหม่จะถูกสร้างขึ้นในเห็บในครั้งต่อไป

life rules state = life rules new_state
    where new_state = construct_state_in_time rules state

[1]: การเพิ่มประสิทธิภาพการโทรแบบหางเป็นการเพิ่มประสิทธิภาพทั่วไปในภาษาโปรแกรมการทำงานเพื่อใช้ฟังก์ชั่นสแต็คที่มีอยู่ในการโทรซ้ำโดยไม่ต้องสร้างใหม่

[2]: โครงสร้างและการตีความของโปรแกรมคอมพิวเตอร์, MIT https://mitpress.mit.edu/books/structure-and-interpretation-computer-programs


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

1
บางทีอาจจะมองจากมุมมองที่เป็นนามธรรมมากกว่าสิ่งที่ดูเหมือนจะแตกต่างกันเป็นแนวคิดเดียวกัน ฉันคิดว่ามันน่าท้อใจและน่าเศร้าที่คิดว่าผู้คน downvote ตอบเพียงเพราะพวกเขาไม่ตรงกับความคาดหวังของพวกเขาแทนที่จะให้พวกเขาเป็นโอกาสที่จะเรียนรู้บางสิ่งบางอย่าง คำตอบทั้งหมดที่พยายามจะพูดว่า: "เฮ้ดูสิสิ่งเหล่านี้ดูแตกต่างบนพื้นผิว แต่จริงๆแล้วเหมือนกันในระดับที่เป็นนามธรรมมากกว่า" ได้ถูกลดระดับลง
Giorgio

3
@Georgio: จุดประสงค์ของเว็บไซต์นี้คือเพื่อรับคำตอบของคำถาม คำตอบที่เป็นประโยชน์และถูกต้องควรได้รับ upvotes, คำตอบที่สับสนและไม่สมควรได้รับ downvotes คำตอบที่ใช้คำจำกัดความที่แตกต่างกันของคำทั่วไปอย่างละเอียดโดยไม่ทำให้ชัดเจนว่าการใช้คำจำกัดความที่แตกต่างนั้นสับสนและไม่ช่วยเหลืออะไร คำตอบที่เหมาะสมถ้าคุณรู้คำตอบอยู่แล้วก็ไม่เป็นประโยชน์และทำหน้าที่เพื่อแสดงความเข้าใจคำศัพท์ที่ดีกว่าของนักเขียน
JacquesB

2
@JacquesB: "คำตอบที่เหมาะสมถ้าคุณรู้คำตอบอยู่แล้วก็ไม่มีประโยชน์ ... ": นี่อาจเป็นคำตอบที่ยืนยันได้ว่าผู้อ่านรู้หรือคิดอยู่แล้วเท่านั้น หากคำตอบแนะนำคำศัพท์ที่ไม่ชัดเจนเป็นไปได้ที่จะเขียนความคิดเห็นเพื่อขอรายละเอียดเพิ่มเติมก่อนที่จะทำการลงคะแนน
จอร์โจ

4
ลูปไม่ใช่การเรียกซ้ำแบบพิเศษ ดูที่ทฤษฎีการคำนวณและเช่นภาษา WHILE เชิงทฤษฎีและ calcul-แคลคูลัส ใช่บางภาษาใช้ลูปเป็นน้ำตาลประโยคจะจริงใช้ recursion เบื้องหลัง แต่พวกเขาสามารถทำเช่นนั้นเพราะการเรียกซ้ำและซ้ำมีการแสดงออกอย่างเท่าเทียมกันไม่ได้เพราะพวกเขาเป็นเดียวกัน
Polygnome

-1

ห่วงในขณะที่แตกต่างจากการเรียกซ้ำ

เมื่อเรียกใช้ฟังก์ชันจะมีการดำเนินการดังต่อไปนี้:

  1. เฟรมสแต็กถูกเพิ่มเข้ากับสแต็ก

  2. ตัวชี้รหัสย้ายไปที่จุดเริ่มต้นของฟังก์ชัน

เมื่อลูป while อยู่ที่ท้ายจะเกิดสิ่งต่อไปนี้:

  1. เงื่อนไขถามว่ามีอะไรจริงหรือไม่

  2. ถ้าเป็นเช่นนั้นรหัสจะกระโดดไปยังจุด

โดยทั่วไปห่วง while นั้นคล้ายกับรหัสเทียมต่อไปนี้:

 if (x)

 {

      Jump_to(y);

 }

สิ่งที่สำคัญที่สุดของทั้งหมดการเรียกซ้ำและการวนซ้ำมีการแทนรหัสแอสเซมบลีที่แตกต่างกันและการแสดงรหัสเครื่อง ซึ่งหมายความว่าพวกเขาจะไม่เหมือนกัน พวกเขาอาจมีผลลัพธ์เดียวกัน แต่รหัสเครื่องที่แตกต่างกันพิสูจน์ว่าพวกเขาไม่เหมือนกัน 100%


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

1
การเรียกการสอบถามซ้ำแบบอินไลน์ที่ดีที่สุดอาจสร้างแอสเซมบลีเดียวกันเช่นเดียวกับลูปธรรมดาทั้งนี้ขึ้นอยู่กับคอมไพเลอร์
hyde

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

@Giorgio Nah มันเป็นคำอธิบายที่สมเหตุสมผลไม่ใช่การติดตั้ง เรารู้ว่าสองสิ่งที่เทียบเท่า ; แต่ความเท่าเทียมไม่ได้เป็นตัวตนเพราะคำถาม (และคำตอบ) เป็นวิธีที่เราได้รับผลคือพวกเขาจำเป็นต้องมีคำอธิบายอัลกอริทึม (ซึ่งสามารถแสดงในแง่ของสแต็คและตัวแปร ฯลฯ )
ปีเตอร์ - Reinstate Monica

1
@ PeterA.Schneider ใช่ แต่คำตอบนี้ระบุว่า "สำคัญที่สุดของทั้งหมด ... รหัสการประกอบต่าง ๆ " ซึ่งไม่ถูกต้องนัก
ไฮด์

-1

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

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

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

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

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