การเรียกซ้ำ - คือ“ แบ่งและพิชิต” หรือ“ นำรหัสมาใช้ใหม่”


11

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

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

ดังนั้นคำถามของฉันคือสิ่งนี้ ...

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

- อัปเดต -

คำตอบมากมายอ้างถึง "ปัญหาที่แท้จริง" ในฐานะการสำรวจต้นไม้, แฟคทอเรียล ฯลฯ ฉันต้องการ "ปัญหาที่แท้จริง" - ให้ฉันยกตัวอย่างให้คุณ ...

เรามีข้อความขนาดใหญ่ (ข้อความประมาณ 30 MB เป็นรายการที่เชื่อมโยงstructs) และเราจำเป็นต้องสร้างดัชนีสำหรับการค้นหาข้อความแบบเต็ม เราต้องการเก็บดัชนีทั้งหมดในหน่วยความจำและจัดทำดัชนีข้อความใหม่ทุกๆ 10 นาที

ทุก ๆ 10 นาทีเราจะเปรียบเทียบข้อความทั้งหมด (รายการที่เชื่อมโยงสองรายการ, บรรทัดต่อบรรทัด) กับกลุ่มข้อความที่สร้างขึ้นใหม่ - เพื่อดูว่าบรรทัดใดที่มีการเปลี่ยนแปลง - จากนั้นเราจะจัดทำดัชนีเฉพาะบรรทัดนั้นอีกครั้ง เราสามารถหลีกเลี่ยงการทำดัชนีข้อความทั้งหมดอีกครั้ง ข้อควรจำ - เราต้องค้นหาจุดต่างระหว่างสองรายการที่เชื่อมโยง 30 MB

หนึ่งในเพื่อนร่วมงานของฉันเกิดขึ้นกับโปรแกรมที่ยอดเยี่ยมซึ่งใช้การเรียกซ้ำแบบเฮฟวี่เพื่อเปรียบเทียบบรรทัด - แล้วรวบรวมตำแหน่งที่ chucks แตกต่างกันในอาร์เรย์ - ใช่ฉันรู้ว่ามันฟังดูงงงวย - การเรียกซ้ำช่วยได้อย่างไร - มันทำ.

ประเด็นคือ - เขาจะเห็นได้อย่างไรว่าปัญหานี้สามารถแก้ไขได้อย่างชาญฉลาดด้วยการใช้การเรียกซ้ำอย่างหนัก


วันนี้มีขนาดใหญ่จริงๆหรือ 30 MB ที่คอมพิวเตอร์ส่วนใหญ่มี GB of RAM และ TB ของพื้นที่ฮาร์ดไดรฟ์
JB King

30 MB อาจไม่ใหญ่ - แต่เมื่อพิจารณาโครงสร้างของข้อมูลที่ข้อความของเราอัดแน่น - มันเป็นข้อความขนาดใหญ่ที่นำไปสู่กระบวนการ - และ DIFF
treecoder

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

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

4
แต่การแวะผ่านต้นไม้เป็น "ปัญหาที่แท้จริง" ซึ่งหลายคนพบเจอเกือบทุกวัน
Falcon

คำตอบ:


16
  • อะไรคือ "รูปแบบปัญหา" ที่เรียกร้องให้แก้ปัญหาการเรียกซ้ำ

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

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

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

  • เรียกใช้ซ้ำในรูปแบบของกลยุทธ์ "แบ่ง & พิชิต" หรือรูปแบบของ "การใช้รหัสซ้ำ" - หรือเป็นรูปแบบการออกแบบในสิทธิ์ของตนเอง

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

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

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


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

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

อาใช่คุณพูดถูก ฉันควรจะพูดว่า "ในการใช้งานภาษา / คอมไพเลอร์ภาษาที่ใช้หน่วยความจำแบบสแต็ก"
JAB

โดยทั่วไปแล้วคุณพูดถูกด้วย ฉันไม่ต้องการที่จะปรากฏ nitpicking
Falcon

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

8

เรียกใช้ซ้ำในรูปแบบของกลยุทธ์ "แบ่ง & พิชิต" หรือรูปแบบของ "การใช้รหัสซ้ำ" - หรือเป็นรูปแบบการออกแบบในสิทธิ์ของตนเอง

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

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

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

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

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


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

1
@ Steve314 ฉันยอมรับว่าการใช้งานจริงของ DP (ไม่ว่าจะในโปรแกรมคอมพิวเตอร์หรือด้วยตนเอง) ไม่ค่อยใช้การเรียกซ้ำ แต่ความคิดนั้นมีพื้นฐานมาจากความสัมพันธ์ที่เกิดซ้ำ - ซึ่งเป็นเพียงสูตรแบบเรียกซ้ำ! - และกรณีฐาน
Konrad Rudolph

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

4
what are the "problem patterns" that call for the solution of recursion

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

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

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


1
+1 สำหรับ"ปัญหาสามารถแบ่งออกเป็นปัญหาย่อยที่มีโครงสร้างเดียวกับปัญหาหลัก"
treecoder

+1 และการถอดความ: ที่การแก้ปัญหานำไปใช้กับชั้นเด็ก ตัวอย่างโลกแห่งความจริงของฉันคือการค้นหาการเรียกเก็บเงินจากบัตรเครดิตที่นำไปสู่ ​​"แบตช์" ซอฟต์แวร์บัญชีจะมีค่าใช้จ่ายส่วนบุคคลและเงินฝากชุดในบัญชีตรวจสอบ กรณีของฉันอาจกลายเป็นคำถามที่นี่เนื่องจาก stackoverflow ไม่ได้คมชัดเกินไป stackoverflow.com/questions/14719806
Chris K

3

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

อะไรคือ "รูปแบบปัญหา" ที่เรียกร้องให้แก้ปัญหาการเรียกซ้ำ

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

เรียกใช้ซ้ำในรูปแบบของกลยุทธ์ "แบ่ง & พิชิต" หรือรูปแบบของ "การใช้รหัสซ้ำ" - หรือเป็นรูปแบบการออกแบบในสิทธิ์ของตนเอง

ไม่มี. หารและพิชิตใช้การเรียกซ้ำ แต่สามารถนำไปใช้กับกองซ้อนได้ การใช้รหัสซ้ำหมายถึงสิ่งอื่น รูปแบบการออกแบบนั้นซับซ้อนกว่าการเรียกซ้ำแบบง่าย

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

การแยกและทุกอย่างที่เกี่ยวข้องกับโครงสร้างต้นไม้ แม้แต่โครงสร้างของต้นไม้โดยปริยาย


3

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

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

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


2

อะไรคือ "รูปแบบปัญหา" ที่เรียกร้องให้แก้ปัญหาการเรียกซ้ำ

ในแง่ทั่วไปมาก recursion เรียกว่าสำหรับเมื่อคุณกำลังแก้ปัญหาที่f (x) = f (g (x)) ยกเว้นกรณีที่คุณโอเคกับการเรียกซ้ำอนันต์กรัม (x)ไม่ควรประเมินx

เรียกใช้ซ้ำในรูปแบบของกลยุทธ์ "แบ่ง & พิชิต" หรือรูปแบบของ "การใช้รหัสซ้ำ" - หรือเป็นรูปแบบการออกแบบในสิทธิ์ของตนเอง

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

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

Factorials, ลำดับ Fibonacci, Traversal ต้นไม้และปัญหาอื่น ๆ อีกมากมายสามารถแก้ไขได้ด้วย recusrion การเรียกใช้ซ้ำในแง่ของฟังก์ชันที่เรียกตัวเองไม่ได้เป็นวิธีที่ดีที่สุดในการใช้สิ่งต่าง ๆ เหล่านั้น มีวิธีอื่นเพื่อให้ได้ผลเช่นเดียวกัน (เช่น stack และ loop) ที่อาจเป็นที่ต้องการมากกว่า


-1

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

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

ตัวอย่างเช่นพิจารณาFactorialอัลกอริทึม

  • กำหนดฐาน: แฟคทอเรียล (1) = 1;
  • กำหนดแฟคทอเรียล n: แฟคทอเรียล (n) = n * แฟคทอเรียล (n-1);

จากนั้นเมื่อคุณพบปัญหาคุณควรคิดว่าคุณสามารถกำหนดมันซ้ำหรือไม่ถ้าคุณสามารถกำหนดมันซ้ำคุณเกือบจะได้แก้ไขมัน

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

ตัวอย่างคือMergeSortสิ่งที่mergeอาจเป็นขั้นตอนที่จำเป็นในการเชื่อมโยงคำจำกัดความหรือวิธีแก้ปัญหาของการเรียงลำดับอาร์เรย์ทั้งหมดกับการเรียงลำดับของอาร์เรย์ย่อย

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