ทรัพยากรสำหรับการปรับปรุงความเข้าใจในการเรียกซ้ำของคุณ? [ปิด]


13

ฉันรู้ว่าการเรียกซ้ำคืออะไร (เมื่อ patten reoccurs อยู่ภายในตัวเองโดยทั่วไปแล้วฟังก์ชั่นที่เรียกตัวเองว่าเป็นหนึ่งในสายของมันหลังจากการแบ่งแบบมีเงื่อนไข ... ใช่ไหม?) และฉันสามารถเข้าใจฟังก์ชั่นแบบเรียกซ้ำได้ ปัญหาของฉันคือเมื่อฉันเห็นตัวอย่างใหม่ฉันมักสับสนอยู่เสมอ ถ้าฉันเห็นการวนซ้ำหรือการแมปการซิปการทำรังการเรียก polymorphic และอื่น ๆ ฉันรู้ว่าจะเกิดอะไรขึ้นโดยดูที่มัน เมื่อฉันเห็นรหัสซ้ำกระบวนการคิดของฉันมักจะเป็น 'wtf นี่คืออะไร' ตามด้วย 'oh it recursive' ตามด้วย 'ฉันเดาว่ามันต้องใช้งานถ้าพวกเขาบอกว่าทำ'

คุณมีเคล็ดลับ / แผน / ทรัพยากรเพื่อสร้างทักษะในด้านนี้หรือไม่? การเรียกซ้ำเป็นแนวคิดแปลก ๆ ดังนั้นฉันจึงคิดวิธีจัดการกับมันอาจจะแปลกและไม่เท่าเทียมกันเท่ากัน


28
เพื่อให้เข้าใจถึงการเรียกซ้ำคุณต้องเข้าใจการเรียกซ้ำ
Andreas Johansson

1
'The Cat in Hat Comes Back' โดยดร. Seuss นี่อาจไม่เป็นประโยชน์อย่างสิ้นเชิง แต่การโทรซ้ำกับแมวจะช่วยขจัดคราบที่น่ารำคาญ :-) นอกจากนี้ยังมีประโยชน์ในการอ่านอย่างรวดเร็วมาก!
DKnight

2
ฝึกฝนฝึกฝนฝึกฝน
David Thornley

20
ตอบแล้วในคำถามนี้: programmers.stackexchange.com/questions/57243/…
Graham Borland

3
@ Graham Borland: นั่นเป็นตัวอย่างของการเรียกซ้ำแบบไม่สิ้นสุด ในโปรแกรมส่วนใหญ่การขาดเคสพื้นฐานมักส่งผลให้เกิดข้อผิดพลาดสแตกล้นหรือหน่วยความจำไม่เพียงพอ สำหรับผู้ใช้เว็บไซต์ก็อาจทำให้เกิดความสับสน ;)
FrustratedWithFormsDesigner

คำตอบ:


10

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

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


1
+1 นี่คือวิธีที่ฉันคลานมัน เช่นถ้าคุณกำลังใช้ OO ทำให้บางคลาสมีความสัมพันธ์กับผู้ปกครองเด็กจากนั้นลองทำฟังก์ชัน / วิธีที่ตรวจสอบว่าวัตถุมีบรรพบุรุษที่เฉพาะเจาะจงหรือไม่
Alb

5

ฉันขอแนะนำ Scheme โดยใช้หนังสือ The Little Lisper เมื่อเสร็จแล้วคุณจะเข้าใจการเรียกซ้ำลึกลงไป เกือบจะรับประกัน


1
+1 หนังสือเล่มนี้ทำเพื่อฉันจริงๆ แต่มันถูกเปลี่ยนชื่อเป็น "The Little Schemer"
mike30

4

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

ถนนอีกสายหนึ่งที่ไม่เกี่ยวข้องกับการเขียนโปรแกรมอย่างเคร่งครัดคือการอ่านGödel, Escher, Bach: Eternal Golden Braidโดย Hofstadter เมื่อคุณได้รับรางแล้วการเรียกซ้ำจะมีลักษณะเป็นธรรมชาติเหมือนเลขคณิต นอกจากนี้คุณจะมั่นใจได้ว่า P = nP และคุณจะต้องการสร้างเครื่องคิด - แต่มันเป็นผลข้างเคียงที่เล็กมากเมื่อเทียบกับผลประโยชน์


GEBก็คุ้มค่าที่จะอ่านต่อไป แม้ว่าบางสิ่งที่เขาพูดถึงจะล้าสมัยนิดหน่อย (ความคืบหน้าของการวิจัย CS ขั้นพื้นฐานได้เกิดขึ้นในรอบ 40 ปีที่ผ่านมา) ความเข้าใจพื้นฐานไม่ใช่
Donal Fellows

2

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

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

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

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


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

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

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

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

ดังนั้นนี่คือความพยายามของฉันที่แฟคทอเรียล เพื่อความเป็นธรรมที่ฉันเคยเห็นมาก่อนและแม้ว่าฉันจะเขียนตั้งแต่ต้นไม่ใช่หน่วยความจำ แต่ก็อาจจะง่ายกว่าที่ควรจะเป็น พยายามใน JS แต่มีข้อผิดพลาดในการแยกวิเคราะห์ แต่ทำงานใน Python def factorial(number): """return factorial of number""" if number == 0: return 0 elif number == 1: return 1 else: return number * factorial(number - 1)
Andrew M

2

โดยส่วนตัวแล้วฉันคิดว่าทางออกที่ดีที่สุดของคุณคือผ่านการฝึกฝน

ฉันเรียนรู้การเรียกซ้ำด้วยโลโก้ คุณสามารถใช้ LISP การเรียกซ้ำเป็นเรื่องธรรมดาในภาษาเหล่านั้น มิฉะนั้นคุณสามารถเปรียบมันกับการศึกษาของชุดทางคณิตศาสตร์และชุดที่คุณแสดงสิ่งต่อไปขึ้นอยู่กับสิ่งที่มาก่อนเช่น u (n + 1) = f (u (n)) หรือชุดที่ซับซ้อนมากขึ้นที่คุณมีหลายตัวแปรและ การอ้างอิงหลายรายการเช่น u (n) = g (u (n-1), u (n-2), v (n), v (n-1)); v (n) = h (u (n-1), u (n-2), v (n), v (n-1)) ...

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

"ปัญหา" แบบกราฟิกฉันพบว่าทำให้มองเห็นได้ง่ายขึ้น ดังนั้นลองดูเกล็ดของ Koch, Fibonacci, เส้นโค้งมังกรและแฟร็กทัลโดยทั่วไป แต่ยังดูอัลกอริทึมการเรียงลำดับอย่างรวดเร็ว ...

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


2

โครงสร้างและการตีความโปรแกรมคอมพิวเตอร์

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


0

เท่าที่ฉันชอบSICPและGödel, Escher, Bach: Eternal Golden Braid , LISPของ Touretzky : การแนะนำอย่างนุ่มนวลเกี่ยวกับการคำนวณเชิงสัญลักษณ์ยังทำงานได้ดีในการแนะนำการเรียกซ้ำ

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

สำหรับตัวอย่างที่น่าเกลียดมาก ๆ มีฟังก์ชั่นของ Ackermann A (m, n)

A(0,n) = n+1.                                   This is the terminal case.
A(m,0) = A(m-1,1) if m > 0.                     This is a simple recursion.
A(m,n) = A(m-1, A(m, n-1)) if m > 0 and n > 0.  This one is ugly.

0

ฉันแนะนำให้เล่นกับภาษาที่ใช้งานใน ML สไตล์อย่าง OCaml หรือ Haskell ฉันพบว่าไวยากรณ์รูปแบบจับคู่มันช่วยให้ฉันเข้าใจการทำงาน recursive แม้แต่ซับซ้อนค่อนข้างแน่นอนดีกว่าโครงการของifและcondงบ (ฉันเรียนรู้ Haskell และ Scheme ในเวลาเดียวกัน)

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

(define (fib n)
   (cond [(= n 0) 0]
         [(= n 1) 1]
         [else (+ (fib (- n 1)) (fib (- n 2)))]))

และด้วยการจับคู่รูปแบบ:

fib 0 = 0
fib 1 = 1
fib n = fib (n - 1) + fib (n - 2)

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

ฉันแนะนำ Haskell เป็นพิเศษเพราะเป็นภาษาที่ง่ายและมีไวยากรณ์ที่ดีมาก นอกจากนี้ยังช่วยให้ทำงานกับแนวคิดขั้นสูงเพิ่มเติมได้ง่ายขึ้นเช่นการตรวจสอบข้อมูล :

fibs = 0 : 1 : zipWith (+) fibs (drop 1 fibs)
fib n = fibs !! n

(คุณจะไม่เข้าใจโค้ดด้านบนจนกว่าคุณจะเล่นกับ Haskell เล็กน้อย แต่โปรดมั่นใจได้ว่ามันเป็นเวทย์มนตร์: P.) แน่นอนว่าคุณสามารถทำแบบเดียวกันกับสตรีมใน Scheme แต่มันเป็นธรรมชาติมากขึ้นใน Haskell


0

มันออกมาจากการพิมพ์ แต่ถ้าคุณสามารถหาได้ "Recursive Algorithms" โดย Richard Lorentz ไม่เกี่ยวกับการเรียกซ้ำ มันครอบคลุมพื้นฐานของการเรียกซ้ำเช่นเดียวกับอัลกอริทึมซ้ำที่เฉพาะเจาะจง

ตัวอย่างอยู่ในภาษาปาสคาล แต่ไม่ใหญ่จนเลือกภาษาได้น่ารำคาญ

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