ในภาษาอังกฤษธรรมดาการเรียกซ้ำคืออะไร?


74

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


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

38
ดูเพิ่มเติมที่: programmers.stackexchange.com/questions/25052/…
compman

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


1
เพื่อทำความเข้าใจกับการเรียกซ้ำคุณต้องเข้าใจการเรียกซ้ำ
Goerman

คำตอบ:


110

เพื่ออธิบายการเรียกซ้ำฉันใช้การผสมผสานของคำอธิบายที่ต่างกันโดยปกติแล้วทั้งสองพยายาม:

  • อธิบายแนวคิด
  • อธิบายว่าทำไมมันถึงสำคัญ
  • อธิบายวิธีรับมัน

สำหรับผู้เริ่มต้นWolfram | Alphaให้นิยามมันง่ายกว่าWikipedia :

นิพจน์เช่นนั้นแต่ละคำถูกสร้างขึ้นโดยการทำซ้ำการดำเนินการทางคณิตศาสตร์ที่เฉพาะเจาะจง


เลข

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

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

  • ฟังก์ชันทางคณิตศาสตร์ ...
  • ... ที่เรียกตัวเองเพื่อคำนวณค่าที่สอดคล้องกับองค์ประกอบ n-th ...
  • ... และสิ่งที่กำหนดขอบเขต

โดยปกติแล้วคุณจะได้รับ "huh huh, whatev" "ที่ดีที่สุดเพราะพวกเขายังไม่ได้ใช้หรืออาจเป็นเพียงกรนที่ลึกมาก


ตัวอย่างการเข้ารหัส

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

ในขั้นตอนนี้นักเรียนของฉันมักจะรู้วิธีพิมพ์บางอย่างบนหน้าจอ สมมติว่าเราจะใช้ C พวกเขารู้วิธีที่จะพิมพ์ถ่านเดียวโดยใช้หรือwrite printfพวกเขายังรู้เกี่ยวกับลูปควบคุม

ฉันมักจะใช้ปัญหาการเขียนโปรแกรมซ้ำ ๆ และเรียบง่ายไม่กี่จนกว่าพวกเขาจะได้รับ:

  • ปัจจัยในการดำเนินงาน
  • เครื่องพิมพ์ตัวอักษร
  • เครื่องพิมพ์ตัวอักษรย้อนกลับ
  • การดำเนินการยกกำลัง

factorial

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

นิยามแบบเรียกซ้ำของการดำเนินการแบบแฟกทอเรียล

ตัวอักษร

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

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

ยกกำลัง

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

รูปแบบที่เรียบง่าย:

รูปแบบที่เรียบง่ายของการดำเนินการยกกำลัง

สามารถแสดงเช่นนี้โดยการเกิดซ้ำ:

ความสัมพันธ์ที่เกิดซ้ำสำหรับการดำเนินการยกกำลัง

ยาก

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

  • Fibonacciตัวเลข
  • ตัวหารร่วมมาก ,
  • 8 Queensปัญหา
  • ทาวเวอร์ของฮานอยเกม
  • และถ้าคุณมีสภาพแวดล้อมแบบกราฟิก (หรือสามารถให้โค้ดสมบูรณ์สำหรับมันหรือสำหรับเทอร์มินัลเอาท์พุทหรือพวกเขาสามารถจัดการมันได้) สิ่งต่าง ๆ เช่น:
  • และสำหรับตัวอย่างภาคปฏิบัติลองพิจารณาเขียน:
    • อัลกอริธึมการสำรวจเส้นทางต้นไม้
    • ตัวแยกวิเคราะห์นิพจน์ทางคณิตศาสตร์อย่างง่าย
    • เกมเรือกวาดทุ่นระเบิด

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


ผู้ช่วย

เอกสารอ้างอิง

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

ระดับ / ลึก

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

แผนภาพสแต็ก - เป็น - ลิ้นชัก

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

ตัวย่อแบบเรียกซ้ำ

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

ซ้ำ:

  • GNU - "GNU ไม่ใช่ Unix"
  • Nagios - "Nagios ไม่ได้ยืนยันกับ Sainthood"
  • PHP - "PHP Hypertext Preprocessor" (และต้นฉบับทั้งหมด "หน้าแรกส่วนตัว")
  • ไวน์ - "ไวน์ไม่ใช่อีมูเลเตอร์"
  • Zile - "Zile คือ Emacs ที่สูญเสีย"

ซ้ำกัน:

  • HURD - "HIRD of Unix-Daemons การแทนที่" (โดย HIRD คือ "HURD ของอินเทอร์เฟซแทนความลึก")

ให้พวกเขาลองคิดด้วยตัวเอง

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


ข้อผิดพลาดและการเรียนรู้เพิ่มเติม

ปัญหาบางอย่างที่คนมักจะต่อสู้ด้วยและที่คุณต้องรู้คำตอบ

ทำไมโอ้พระเจ้าทำไม ???

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

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

มีอะไรอีก

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

สิ้นสุดเงื่อนไข

ฉันจะกำหนดเงื่อนไขสิ้นสุดได้อย่างไร ง่ายมากเพียงแค่ให้พวกเขาพูดเสียงดังออกมา ตัวอย่างเช่นสำหรับแฟคทอเรียลเริ่มจาก 5 จากนั้น 4 จากนั้น ... จนถึง 0

ปีศาจอยู่ในรายละเอียด

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

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

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

เรียกซ้ำกัน

เราได้เห็นแล้วว่าฟังก์ชั่นสามารถเรียกซ้ำได้และแม้กระทั่งว่าพวกเขาสามารถมีจุดโทรหลายจุด (8-ราชินี, ฮานอย, ฟีโบนักชีหรือแม้แต่อัลกอริธึมสำรวจสำหรับเรือกวาดทุ่นระเบิด) แต่สิ่งที่เกี่ยวกับการโทรซ้ำร่วมกัน ? เริ่มด้วยคณิตศาสตร์ที่นี่เช่นกัน f(x) = g(x) + h(x)ที่ไหนg(x) = f(x) + l(x)และhและlเพียงแค่ทำสิ่งต่าง ๆ

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

ลำดับของชายและหญิงของ Hofstadter

อย่างไรก็ตามในแง่ของรหัสก็เป็นที่น่าสังเกตว่าการดำเนินการของการแก้ปัญหาร่วมกัน recursive มักจะนำไปสู่รหัสซ้ำซ้อนและค่อนข้างควรจะคล่องตัวในรูปแบบ recursive เดียว (ดูปีเตอร์นอร์วิก 's แก้ปริศนาทุกซูโดกุ


5
ฉันกำลังอ่านคำตอบของคุณหลังจากที่เห็นมันหลังจากเกือบ 5 หรือ 6 ครั้ง มันดี แต่ยาวเกินไปสำหรับการดึงดูดผู้ใช้รายอื่นที่นี่ฉันคิดว่า ฉันเรียนรู้หลายสิ่งเกี่ยวกับการสอนการเรียกซ้ำที่นี่ ในฐานะครูคุณช่วยประเมินความคิดของฉันในการสอน recursion- programmers.stackexchange.com/questions/25052/…
Gulshan

9
@Gulshan ฉันคิดว่าคำตอบนี้เกี่ยวกับการครอบคลุมเท่าที่เป็นไปได้และ 'อ่านง่าย' โดยผู้อ่านทั่วไป ดังนั้นมันได้รับstatic unsigned int vote = 1;จากฉัน ให้อภัยอารมณ์ขันแบบคงที่ถ้าคุณจะ :) นี่คือคำตอบที่ดีที่สุดเพื่อให้ห่างไกล
Tim Post

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

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

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

58

การเรียกใช้ฟังก์ชันจากภายในฟังก์ชันเดียวกันนั้น


2
นี่คือคำอธิบายที่ดีที่สุดที่จะเริ่มจากจริงๆ ง่ายและตรงประเด็น; เมื่อคุณสร้างข้อมูลสรุปนี้แล้วให้ไปที่รายละเอียดการเดินทางทั้งหมด
jhocking

27

การเรียกซ้ำเป็นฟังก์ชันที่เรียกตัวเอง

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

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

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


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

@Haylem: คุณถูกต้องเกี่ยวกับคำตอบของ "วิธีการใช้เมื่อไรที่จะใช้และวิธีการหลีกเลี่ยงการออกแบบที่ไม่ดี .. " จะมีมากขึ้นในสิ่งที่ OP ขอ (ไม่ใช่แค่ลองด้วยตัวเอง) "ตามที่ฉันพูด) แต่นั่นต้องมีการบรรยายอย่างกว้างขวางเกี่ยวกับประเภทการสอนมากกว่าที่จะตอบคำถามอย่างรวดเร็วที่นี่ คุณได้งานที่ดีมากกับคำตอบของคุณแม้ว่า +1 สำหรับสิ่งนั้น ... ผู้ที่ต้องการเข้าใจแนวคิดที่ดีกว่าจะได้รับประโยชน์จากการอ่านคำตอบของคุณ
ความกลัว

ฟังก์ชั่นคู่หนึ่งที่เรียกซึ่งกันและกัน A สาย B ซึ่งเรียก A อีกครั้งจนกว่าจะถึงเงื่อนไขบางอย่าง สิ่งนี้จะยังคงถูกพิจารณาซ้ำอีกไหม?
santiagozky

ใช่ฟังก์ชั่นaยังคงเรียกตัวเองเพียงทางอ้อม (โดยการโทรb)
kindall

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

21

การเขียนโปรแกรมแบบเรียกซ้ำเป็นกระบวนการของการลดปัญหาอย่างต่อเนื่องเพื่อให้ง่ายต่อการแก้ไขรุ่นของตัวเอง

ทุกฟังก์ชั่นวนซ้ำมีแนวโน้มที่:

  1. ใช้รายการเพื่อดำเนินการหรือโครงสร้างอื่น ๆ หรือโดเมนปัญหา
  2. จัดการกับจุด / ขั้นตอนปัจจุบัน
  3. เรียกตัวเองในส่วนที่เหลือ / โดเมนย่อย
  4. รวมหรือใช้ผลการทำงานของโดเมนย่อย

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

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

   B
A     C

สั่งซื้อล่วงหน้า: BAC

traverse(tree):
    visit the node
    traverse(left)
    traverse(right)

ตามลำดับ: ABC

traverse(tree):
    traverse(left)
    visit the node
    traverse(right)

การสั่งซื้อภายหลัง: ACB

traverse(tree):
    traverse(left)
    traverse(right)
    visit the node

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


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

@ Barry Brown: ค่อนข้างถูกต้อง ดังนั้นคำสั่งของฉัน"... การลดปัญหาเพื่อง่ายต่อการแก้ไขรุ่นของตัวเอง"
Orbling

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

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

20

OP กล่าวว่าการเรียกซ้ำไม่มีอยู่ในโลกจริง แต่ฉันขอแตกต่างกัน

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

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

นี่คือตัวอย่างใน Ruby:

def cut_pizza (existing_slices, ที่ต้องการ_slices)
  ถ้า existing_slices! = ต้องการ_slices
    # เรามีชิ้นไม่เพียงพอที่จะเลี้ยงทุกคนดังนั้น
    # เราตัดพิซซ่าเป็นส่วน ๆ ดังนั้นเพิ่มจำนวนของพวกเขาเป็นสองเท่า
    new_slices = existing_slices * 2 
    # และนี่คือการเรียกซ้ำ
    cut_pizza (new_slices, ที่ต้องการ_slices)
  อื่น
    # เรามีจำนวนชิ้นที่ต้องการดังนั้นเราจึงกลับมา
    # ที่นี่แทนที่จะเรียกเก็บเงินต่อไป
    ส่งคืนค่าที่มีอยู่
  ปลาย
ปลาย

pizza = 1 # a พิซซ่าทั้งหมด 'one slice'
cut_pizza (pizza, 8) # => เราจะได้ 8

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

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

  • การคำนวณดอกเบี้ยทบต้นในช่วงหลายเดือน
  • ค้นหาไฟล์ในระบบไฟล์ (เพราะระบบไฟล์เป็นต้นไม้เพราะไดเรกทอรี)
  • ฉันเดาว่าอะไรก็ตามที่เกี่ยวข้องกับการทำงานกับต้นไม้

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

find_file_by_name(file_name_we_are_looking_for, path_to_look_in)

ดังนั้นคุณสามารถเรียกมันว่า:

find_file_by_name('httpd.conf', '/etc') # damn it i can never find apache's conf

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

เครดิตเสริมcut_pizzaตัวอย่างข้างต้นจะทำให้คุณมีระดับสแต็คข้อผิดพลาดลึกเกินไปถ้าคุณถามมันสำหรับจำนวนของชิ้นที่ไม่ได้เป็นอำนาจของ 2 (เช่น 2 หรือ 4 หรือ 8 หรือ 16) คุณสามารถปรับเปลี่ยนมันได้ไหมถ้ามีคนขอ 10 แผ่นมันจะไม่ทำงานตลอดไปหรือไม่?


16

โอเคฉันจะพยายามทำให้เรื่องนี้ง่ายและกระชับ

ฟังก์ชั่นวนซ้ำเป็นฟังก์ชั่นที่เรียกตัวเองว่า ฟังก์ชั่นวนซ้ำประกอบด้วยสามสิ่ง:

  1. ตรรกะ
  2. มีสายเรียกเข้าเอง
  3. เมื่อไหร่ที่จะยุติ

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

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

หากคุณได้รับเรื่องตลกที่คุณได้รับหมายถึงการเรียกซ้ำ


ดูคำตอบนี้: programmers.stackexchange.com/questions/25052/… (-:
Murph

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

6

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

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

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


ไม่ว่าจะช้าหรือต้องใช้หน่วยความจำมากขึ้นอยู่กับการใช้งานในมือ
Orbling

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

1
@ เดฟ: ฉันจะไม่โต้แย้ง แต่ฉันคิดว่าฟีโบนัชชีเป็นตัวอย่างที่ดีในการเริ่มต้น
Bryan Harrington

5

ฉันชอบที่จะใช้อันนี้:

คุณเดินไปที่ร้านได้อย่างไร

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

มันเป็นสิ่งสำคัญที่จะรวมสามด้าน:

  • กรณีฐานเล็กน้อย
  • การแก้ปัญหาชิ้นเล็ก ๆ
  • การแก้ปัญหาส่วนที่เหลือซ้ำ ๆ

เราใช้การเรียกซ้ำหลายครั้งในชีวิตประจำวัน เราแค่ไม่คิดอย่างนั้น


นั่นไม่ใช่การเรียกซ้ำ ถ้าคุณแบ่งมันเป็นสอง: เดินครึ่งทางไปที่ร้านเดินอีกครึ่ง recurse

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

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

3

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


2

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

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

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

ไม่ใช่คำอธิบายที่ดีที่สุด แต่หวังว่าจะช่วยได้


2

การ สอบถามซ้ำ - รูปแบบของการออกแบบอัลกอริทึมที่การดำเนินการถูกกำหนดในแง่ของตัวเอง

ตัวอย่างคลาสสิกคือการหาแฟกทอเรียลของตัวเลข, n! 0! = 1 และสำหรับหมายเลขธรรมชาติอื่น ๆ N แฟคทอเรียลของ N คือผลผลิตของจำนวนธรรมชาติทั้งหมดน้อยกว่าหรือเท่ากับ N ดังนั้น, 6! = 6 * 5 * 4 * 3 * 2 * 1 = 720 คำจำกัดความพื้นฐานนี้จะช่วยให้คุณสร้างโซลูชันที่วนซ้ำง่าย ๆ :

int Fact(int degree)
{
    int result = 1;
    for(int i=degree; i>1; i--)
       result *= i;

    return result;
}

อย่างไรก็ตามตรวจสอบการดำเนินการอีกครั้ง 6! = 6 * 5 * 4 * 3 * 2 * 1 ตามคำนิยามเดียวกัน 5! = 5 * 4 * 3 * 2 * 1 หมายความว่าเราสามารถพูดได้ 6! = 6 * (5!) ในทางกลับกัน 5! = 5 * (4!) และอื่น ๆ โดยการทำเช่นนี้เราลดปัญหาให้กับการดำเนินการที่ทำกับผลลัพธ์ของการดำเนินการก่อนหน้านี้ทั้งหมด ในที่สุดสิ่งนี้จะลดลงเป็นจุดที่เรียกว่าเคสฐานซึ่งผลลัพธ์เป็นที่รู้จักกันโดยนิยาม ในกรณีของเรา 0! = 1 (ในกรณีส่วนใหญ่เราสามารถพูดได้ว่า 1! = 1) ในการคำนวณเรามักได้รับอนุญาตให้กำหนดอัลกอริธึมในลักษณะที่คล้ายกันมากโดยให้วิธีการเรียกตัวเองและผ่านอินพุตขนาดเล็กซึ่งช่วยลดปัญหาผ่านการเรียกซ้ำหลายครั้งไปยังกรณีพื้นฐาน:

int Fact(int degree)
{
    if(degree==0) return 1; //the base case; 0! = 1 by definition
    else return degree * Fact(degree -1); //the recursive case; N! = N*(N-1)!
}

ในหลายภาษาสามารถทำได้ง่ายขึ้นโดยใช้โอเปอร์เรเตอร์ ternary (บางครั้งถูกมองว่าเป็นฟังก์ชัน Iif ในภาษาที่ไม่ได้ให้โอเปอเรเตอร์):

int Fact(int degree)
{
    //reads equivalently to the above, but is concise and often optimizable
    return degree==0 ? 1: degree * Fact(degree -1);
}

ข้อดี:

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

ข้อเสีย:

  • ต้องมีความเข้าใจ - คุณต้อง "เข้าใจ" แนวคิดของการเรียกซ้ำเพื่อให้เข้าใจว่าเกิดอะไรขึ้นดังนั้นจึงต้องเขียนและรักษาอัลกอริทึมแบบเรียกซ้ำ มิฉะนั้นมันก็ดูเหมือนว่าเวทมนตร์สีดำ
  • ขึ้นอยู่กับบริบท - ไม่ว่าการเรียกซ้ำเป็นความคิดที่ดีหรือไม่ขึ้นอยู่กับว่าอัลกอริธึมสามารถกำหนดได้อย่างสวยงามในแง่ของตัวเอง ในขณะที่มันเป็นไปได้ที่จะสร้างเช่น recursive SelectionSort, อัลกอริทึมซ้ำมักจะเข้าใจได้มากขึ้น
  • การเข้าถึง RAM สำหรับการโทรซ้อน - โดยทั่วไปแล้วการเรียกใช้ฟังก์ชั่นจะถูกกว่าการเข้าถึงแคชซึ่งสามารถเรียกซ้ำได้เร็วกว่าการวนซ้ำ แต่โดยทั่วไปจะมีข้อ จำกัด ด้านความลึกของ call stack ที่อาจทำให้เกิดการเรียกซ้ำไปยังข้อผิดพลาดที่อัลกอริทึมซ้ำจะทำงาน
  • การเรียกซ้ำแบบไม่สิ้นสุด - คุณต้องรู้ว่าจะหยุดเมื่อไหร่ การวนซ้ำแบบไม่สิ้นสุดเป็นไปได้เช่นกัน แต่โครงสร้างการวนรอบที่เกี่ยวข้องมักจะเข้าใจได้ง่ายขึ้นและเพื่อทำการดีบัก

1

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

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


0

การเรียกซ้ำสามารถใช้เพื่อแก้ปัญหาการนับจำนวนมาก ตัวอย่างเช่นสมมติว่าคุณมีกลุ่มคน n คนในงานปาร์ตี้ (n> 1) และทุกคนก็จับมือของคนอื่นเพียงครั้งเดียว มีการจับมือกันกี่ครั้ง คุณอาจรู้ว่าวิธีแก้ปัญหาคือ C (n, 2) = n (n-1) / 2 แต่คุณสามารถแก้ปัญหาแบบวนซ้ำดังนี้:

สมมติว่ามีเพียงสองคน จากนั้น (โดยการตรวจสอบ) คำตอบนั้นชัดเจน 1

สมมติว่าคุณมีสามคน แยกคนคนหนึ่งออกจากกันและสังเกตว่าเขา / เธอจับมือกับอีกสองคน หลังจากนั้นคุณต้องนับแค่การจับมือกันระหว่างคนอีกสองคน เราทำไปแล้วตอนนี้และก็คือ 1 ดังนั้นคำตอบคือ 2 + 1 = 3

สมมติว่าคุณมี n คน ทำตามตรรกะเดียวกับก่อนหน้านี้คือ (n-1) + (จำนวนการจับมือกันระหว่าง n-1 คน) กำลังขยายเราจะได้รับ (n-1) + (n-2) + ... + 1

แสดงว่าเป็นฟังก์ชันแบบเรียกซ้ำ

f (2) = 1
f (n) = n-1 + f (n-1), n> 2


0

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

การเรียกซ้ำจะเกิดขึ้นที่นี่ในโลกแม้ว่า มาก.

ตัวอย่างที่ดีคือ (เวอร์ชั่นย่อของ) วัฏจักรของน้ำ:

  • พระอาทิตย์อุ่นทะเลสาบ
  • น้ำขึ้นไปบนท้องฟ้าและก่อตัวเป็นเมฆ
  • เมฆล่องลอยไปบนภูเขา
  • ที่ภูเขาอากาศจะเย็นเกินไปจนความชื้นจะสะสม
  • ฝนตก
  • รูปแบบแม่น้ำ
  • น้ำในแม่น้ำไหลลงสู่ทะเลสาบ

นี่คือวงจรที่ทำให้ตัวเองเกิดขึ้นอีกครั้ง มันเกิดซ้ำ

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

จากสัญชาตญาณภาษาของสตีเวนพินเจอร์:

ถ้าเด็กผู้หญิงคนนั้นกินไอศครีมหรือเด็กผู้หญิงกินขนมแล้วเด็กก็กินฮอทดอก

นั่นคือประโยคทั้งหมดที่มีประโยคอื่นทั้งหมด:

หญิงสาวกินไอศกรีม

หญิงสาวกินขนม

เด็กชายกินฮอทดอก

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

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

สำหรับตัวอย่างฉันจะใช้ฟังก์ชันตัวหารร่วมมากหรือ gcd สั้น ๆ

คุณมีหมายเลขสองของคุณและa bในการค้นหา gcd ของพวกเขา (สมมติว่าไม่ใช่ 0) คุณต้องตรวจสอบว่าaสามารถแบ่งได้เท่า ๆ กันbหรือไม่ ถ้ามันเป็นแล้วbเป็น GCD มิฉะนั้นคุณต้องตรวจสอบ GCD ของและส่วนที่เหลือของba/b

คุณควรจะเห็นว่านี่เป็นฟังก์ชันแบบเรียกซ้ำเนื่องจากคุณมีฟังก์ชัน gcd ที่เรียกใช้ฟังก์ชัน gcd เพียงแค่ใช้ค้อนที่บ้านนี่คือใน c # (อีกครั้งโดยสมมติว่า 0 ไม่เคยผ่านเป็นพารามิเตอร์):

int gcd(int a, int b)
{   
    if (a % b == 0) //this is a stopping condition
    {
        return b;
    }

    return (gcd(b, a % b)); //the call to gcd here makes this function recursive
}

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

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


1
ฉันพบตัวอย่างวัฏจักรของน้ำซ้ำแล้วซ้ำอีก ตัวอย่างภาษาที่สองดูเหมือนจะแบ่งและพิชิตได้มากกว่าการเรียกซ้ำ
Gulshan

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

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

ไม่มีการโทรซ้ำ! ไม่ใช่ฟังก์ชั่น : D เกิดซ้ำเพราะมันทำให้ตัวมันเองกำเริบ น้ำจากทะเลสาบกลับมาที่ทะเลสาบและวัฏจักรก็เริ่มขึ้นอีกครั้ง หากระบบอื่น ๆ กำลังเติมน้ำลงไปในทะเลสาบมันจะเป็นการวนซ้ำ
Matt Ellen

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

0

นี่คือตัวอย่างโลกแห่งความเป็นจริงสำหรับการเรียกซ้ำ

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

ตอนนี้ให้พวกเขาจัดเรียงการ์ตูนกองใหญ่ที่ยังไม่เรียงด้วยความช่วยเหลือของคู่มือนี้:

Manual: How to sort a pile of comics

Check the pile if it is already sorted. If it is, then done.

As long as there are comics in the pile, put each one on another pile, 
ordered from left to right in ascending order:

    If your current pile contains different comics, pile them by comic.
    If not and your current pile contains different years, pile them by year.
    If not and your current pile contains different tenth digits, pile them 
    by this digit: Issue 1 to 9, 10 to 19, and so on.
    If not then "pile" them by issue number.

Refer to the "Manual: How to sort a pile of comics" to separately sort each
of the new piles.

Collect the piles back to a big pile from left to right.

Done.

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

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


-1
  • ยุติหากถึงเงื่อนไขทางออก
  • ทำอะไรสักอย่างเพื่อเปลี่ยนแปลงสถานะของสิ่งต่าง ๆ
  • ทำงานให้ทั่วเริ่มต้นจากสถานะปัจจุบันของสิ่งต่าง ๆ

การเรียกซ้ำเป็นวิธีที่รัดกุมมากในการแสดงสิ่งที่ต้องทำซ้ำจนกว่าจะถึงบางสิ่ง



-2

คำอธิบายที่ดีของการเรียกซ้ำคือการกระทำที่เกิดขึ้นเองจากภายใน "

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

ฟังก์ชัน paint () ของเขาเรียกตัวเองซ้ำแล้วซ้ำอีกเพื่อสร้างฟังก์ชัน paint_wall () ที่ใหญ่กว่าของเขา

หวังว่าจิตรกรผู้น่าสงสารคนนี้จะมีเงื่อนไขหยุดอยู่บ้าง :)


6
สำหรับฉันตัวอย่างดูเหมือนจะคล้ายกับขั้นตอนการทำซ้ำมากกว่าการเรียกซ้ำ
Gulshan

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