ทำความเข้าใจกับการเรียกซ้ำ [ปิด]


225

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

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


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

40
การเรียกซ้ำ: ดูการเรียกซ้ำ
Loren Pechtel

36
@ พอล: ฉันได้รับเรื่องตลก แต่ฉันคิดเสมอว่ามันผิดทางเทคนิค เงื่อนไขพื้นฐานที่ทำให้อัลกอริทึมสิ้นสุดลงที่ใด นั่นเป็นสิ่งจำเป็นพื้นฐานสำหรับการเรียกซ้ำ =)
Sergio Acosta

70
ฉันจะให้มันช็อต: "เพื่อให้เข้าใจการเรียกซ้ำคุณต้องเข้าใจการเรียกซ้ำจนกว่าคุณจะเข้าใจ" =)
Sergio Acosta

91
มีลักษณะที่คำถามนี้มันอาจจะช่วยให้stackoverflow.com/questions/717725/understanding-recursion
Omar Kooheji

คำตอบ:


598

คุณจะล้างแจกันที่มีดอกไม้ห้าดอกได้อย่างไร

คำตอบ: ถ้าแจกันไม่ว่างคุณจะเอาดอกไม้หนึ่งดอกออกแล้วคุณจะทำแจกันที่มีดอกไม้สี่ดอกให้ว่าง

คุณจะล้างแจกันที่มีดอกไม้สี่ดอกได้อย่างไร

คำตอบ: ถ้าแจกันไม่ว่างคุณจะเอาดอกไม้หนึ่งดอกออกแล้วคุณจะล้างแจกันที่มีดอกไม้สามดอก

คุณจะล้างแจกันที่มีดอกไม้สามดอกได้อย่างไร

คำตอบ: ถ้าแจกันไม่ว่างคุณจะเอาดอกไม้หนึ่งดอกออกแล้วคุณจะล้างแจกันที่มีดอกไม้สองดอก

คุณจะล้างแจกันที่มีดอกไม้สองดอกได้อย่างไร

คำตอบ: ถ้าแจกันไม่ว่างคุณจะเอาดอกไม้หนึ่งดอกออกแล้วคุณจะทำแจกันที่มีดอกไม้หนึ่งดอกให้ว่าง

คุณจะล้างแจกันที่มีดอกไม้หนึ่งดอกได้อย่างไร

คำตอบ: ถ้าแจกันไม่ว่างคุณจะเอาดอกไม้หนึ่งดอกออกแล้วจากนั้นก็ให้คุณล้างแจกันที่ไม่มีดอกไม้

คุณจะล้างแจกันที่ไม่มีดอกไม้ได้อย่างไร

คำตอบ: ถ้าแจกันไม่ว่างคุณจะเอาดอกไม้หนึ่งดอกออกมา แต่แจกันนั้นว่างแล้วคุณก็ทำเสร็จแล้ว

มันซ้ำ ๆ มาพูดคุยกัน:

คุณจะล้างแจกันที่มีNดอกไม้ได้อย่างไร

คำตอบ: ถ้าแจกันไม่ว่างคุณจะเอาดอกไม้หนึ่งดอกออกแล้วจากนั้นคุณก็ล้างแจกันที่มีดอกไม้N-1

อืมเราจะเห็นว่าในรหัส?

void emptyVase( int flowersInVase ) {
  if( flowersInVase > 0 ) {
   // take one flower and
    emptyVase( flowersInVase - 1 ) ;

  } else {
   // the vase is empty, nothing to do
  }
}

อืมเราไม่ได้ทำแบบนั้นในการวนซ้ำหรือไม่?

ทำไมใช่การสอบถามซ้ำสามารถถูกแทนที่ด้วยการวนซ้ำ แต่บ่อยครั้งที่การเรียกซ้ำเป็นสง่ามากขึ้น

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

ลองนึกภาพว่าโหนดนอกเหนือจากลูกมันมีค่าตัวเลขและจินตนาการว่าเราต้องการรวมค่าทั้งหมดในต้นไม้บางต้น

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

ดังนั้นเมื่อรวมผลรวมของลูกซ้ายเราจะเพิ่มมูลค่าของโหนดลูกเองกับมูลค่าของลูกซ้ายของมันถ้ามีและมูลค่าของลูกที่ถูกต้องของมันถ้ามี

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

บางทีคุณคาดว่าฉันจะไปกับสิ่งนี้และต้องการที่จะเห็นรหัสบางส่วน? ตกลง:

struct node {
  node* left;
  node* right;
  int value;
} ;

int sumNode( node* root ) {
  // if there is no tree, its sum is zero
  if( root == null ) {
    return 0 ;

  } else { // there is a tree
    return root->value + sumNode( root->left ) + sumNode( root->right ) ;
  }
}

โปรดสังเกตว่าแทนที่จะทดสอบเด็ก ๆ อย่างชัดเจนเพื่อดูว่าพวกเขาเป็นโมฆะหรือโหนดเราเพียงแค่ทำให้ฟังก์ชัน recursive คืนค่าศูนย์สำหรับโหนดว่าง

สมมติว่าเรามีต้นไม้ที่มีลักษณะเช่นนี้ (ตัวเลขคือค่าเครื่องหมายสแลชชี้ไปที่เด็กและ @ หมายถึงตัวชี้ชี้เป็นโมฆะ):

     5
    / \
   4   3
  /\   /\
 2  1 @  @
/\  /\
@@  @@

หากเราเรียก sumNode บนรูท (โหนดที่มีค่า 5) เราจะส่งคืน:

return root->value + sumNode( root->left ) + sumNode( root->right ) ;
return 5 + sumNode( node-with-value-4 ) + sumNode( node-with-value-3 ) ;

ลองขยายมันให้เข้าที่ ทุกที่ที่เราเห็น sumNode เราจะแทนที่ด้วยการขยายคำสั่ง return:

sumNode( node-with-value-5);
return root->value + sumNode( root->left ) + sumNode( root->right ) ;
return 5 + sumNode( node-with-value-4 ) + sumNode( node-with-value-3 ) ;

return 5 + 4 + sumNode( node-with-value-2 ) + sumNode( node-with-value-1 ) 
 + sumNode( node-with-value-3 ) ;  

return 5 + 4 
 + 2 + sumNode(null ) + sumNode( null )
 + sumNode( node-with-value-1 ) 
 + sumNode( node-with-value-3 ) ;  

return 5 + 4 
 + 2 + 0 + 0
 + sumNode( node-with-value-1 ) 
 + sumNode( node-with-value-3 ) ; 

return 5 + 4 
 + 2 + 0 + 0
 + 1 + sumNode(null ) + sumNode( null )
 + sumNode( node-with-value-3 ) ; 

return 5 + 4 
 + 2 + 0 + 0
 + 1 + 0 + 0
 + sumNode( node-with-value-3 ) ; 

return 5 + 4 
 + 2 + 0 + 0
 + 1 + 0 + 0
 + 3 + sumNode(null ) + sumNode( null ) ; 

return 5 + 4 
 + 2 + 0 + 0
 + 1 + 0 + 0
 + 3 + 0 + 0 ;

return 5 + 4 
 + 2 + 0 + 0
 + 1 + 0 + 0
 + 3 ;

return 5 + 4 
 + 2 + 0 + 0
 + 1 
 + 3  ;

return 5 + 4 
 + 2 
 + 1 
 + 3  ;

return 5 + 4 
 + 3
 + 3  ;

return 5 + 7
 + 3  ;

return 5 + 10 ;

return 15 ;

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

How to sum a node:
 If a node is null 
   its sum is zero
 otherwise 
   its sum is its value 
   plus the sum of its left child node
   plus the sum of its right child node

นั่นคือพลังของการเรียกซ้ำ


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

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

ในความเป็นจริงลำดับที่เราเรียกว่า children และเพิ่มค่าของ node ปัจจุบันไม่สำคัญเลยเพราะการเพิ่มนั้นเป็น commutative

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

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

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

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

เรามาดูกันว่าในรหัส:

struct node {
  node* left;
  node* right;
  char value;
} ;

// don't worry about this code
class Printer {
  private ostream& out;
  Printer( ostream& o ) :out(o) {}
  void print( char c ) { out << c; }
}

// worry about this code
int printNode( node* root, Printer& printer ) {
  // if there is no tree, do nothing
  if( root == null ) {
    return ;

  } else { // there is a tree
    printNode( root->left, printer );
    printer.print( value );
    printNode( root->right, printer );
}

Printer printer( std::cout ) ;
node* root = makeTree() ; // this function returns a tree, somehow
printNode( root, printer );

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

ทีนี้ถ้าต้นไม้ของเราเป็นแบบนี้:

         k
        / \
       h   n
      /\   /\
     a  j @  @
    /\ /\
    @@ i@
       /\
       @@

เราจะพิมพ์อะไร

From k, we go left to
  h, where we go left to
    a, where we go left to 
      null, where we do nothing and so
    we return to a, where we print 'a' and then go right to
      null, where we do nothing and so
    we return to a and are done, so
  we return to h, where we print 'h' and then go right to
    j, where we go left to
      i, where we go left to 
        null, where we do nothing and so
      we return to i, where we print 'i' and then go right to
        null, where we do nothing and so
      we return to i and are done, so
    we return to j, where we print 'j' and then go right to
      null, where we do nothing and so
    we return to j and are done, so
  we return to h and are done, so
we return to k, where we print 'k' and then go right to
  n where we go left to 
    null, where we do nothing and so
  we return to n, where we print 'n' and then go right to
    null, where we do nothing and so
  we return to n and are done, so 
we return to k and are done, so we return to the caller

ดังนั้นถ้าเราดูที่บรรทัดที่เราพิมพ์:

    we return to a, where we print 'a' and then go right to
  we return to h, where we print 'h' and then go right to
      we return to i, where we print 'i' and then go right to
    we return to j, where we print 'j' and then go right to
we return to k, where we print 'k' and then go right to
  we return to n, where we print 'n' and then go right to

เราเห็นว่าเราพิมพ์ "ahijkn" ซึ่งแน่นอนตามลำดับตัวอักษร

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

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

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

void recurse() { doWeStop() || recurse(); } 

ความคิดเห็น Luc M:

ดังนั้นควรสร้างตราสำหรับคำตอบประเภทนี้ ขอแสดงความยินดี!

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

ดูความคิดเห็นของฉันที่นี่: /programming/128434/what-are-community-wiki-posts-in-stackoverflow/718699#718699


35

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

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

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

a) อ่านหน้าผลการค้นหาของ Google สำหรับ "การเรียกซ้ำ"
b) เมื่อคุณอ่านเสร็จแล้วให้ไปที่ลิงค์แรกบนมันและ ...
a.1) อ่านหน้าใหม่เกี่ยวกับการเรียกซ้ำ 
b.1) เมื่อคุณอ่านแล้วให้ไปที่ลิงค์แรกบนมันและ ...
a.2) อ่านหน้าใหม่เกี่ยวกับการเรียกซ้ำ 
b.2) เมื่อคุณอ่านแล้วให้ไปที่ลิงค์แรกที่มันและ ...

อย่างที่คุณเห็นคุณกำลังทำสิ่งที่วนซ้ำมาเป็นเวลานานโดยไม่มีปัญหาใด ๆ

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

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

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

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

"ค้นหาข้อมูลเพิ่มเติมเกี่ยวกับการเรียกซ้ำบนอินเทอร์เน็ตจนกว่าคุณจะเข้าใจหรือคุณอ่านสูงสุด 10 หน้าและเริ่มต้นที่ www.google.com/search?q=recursion "

ฉันขอแนะนำให้คุณลองหนังสือเล่มใดเล่มหนึ่งเหล่านี้:

  • Common Lisp: การแนะนำแบบอ่อนโยนต่อการคำนวณเชิงสัญลักษณ์ นี่คือคำอธิบายที่ไม่ใช่ทางคณิตศาสตร์ที่น่ารักที่สุดของการเรียกซ้ำ
  • เจ้าอุบายตัวน้อย

6
คำอุปมาของ "function = กล่องขนาดเล็กของ I / O" ทำงานร่วมกับการเรียกซ้ำตราบใดที่คุณยังนึกภาพว่ามีโรงงานผลิตอยู่ที่นั่นทำให้โคลนที่ไม่มีที่สิ้นสุดและกล่องขนาดเล็กของคุณสามารถกลืนกล่องขนาดเล็กอื่น ๆ
ephemient

2
น่าสนใจ .. ดังนั้นในอนาคตหุ่นยนต์จะทำอะไรบางอย่างและเรียนรู้ด้วยตัวเองโดยใช้ลิงก์ 10 ตัวแรก :) :)
kumar

2
@kumar Google ไม่ได้ทำกับอินเทอร์เน็ตแล้วหรือ?
TJ

1
หนังสือที่ดีขอบคุณสำหรับคำแนะนำ
Max Koretskyi

+1 สำหรับ "สมองของคุณระเบิดเพราะเข้าสู่การวนซ้ำแบบไม่มีที่สิ้นสุดนั่นเป็นความผิดพลาดเริ่มต้นทั่วไป"
Stack Underflow

26

เพื่อให้เข้าใจถึงการเรียกซ้ำสิ่งที่คุณต้องทำคือดูที่ฉลากขวดแชมพูของคุณ:

function repeat()
{
   rinse();
   lather();
   repeat();
}

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


6
ขอบคุณ dar7yl - มันทำให้ฉันรำคาญกับขวดแชมพูเสมอ (ฉันเดาว่าฉันมักจะถูกลิขิตให้เขียนโปรแกรมอยู่เสมอ) ถึงแม้ว่าผมจะวางเดิมพันคนที่ตัดสินใจที่จะเพิ่ม 'ซ้ำ" ในตอนท้ายของคำสั่งที่ทำล้าน บริษัท .
kenj0418

5
ฉันหวังว่าคุณrinse()หลังจากที่คุณlather()
CoderDennis

@JakeWilson หากใช้การเพิ่มประสิทธิภาพการโทรแบบหาง - แน่นอน แม้ว่ามันจะยืนอยู่ในขณะนี้ - มันเป็นการเรียกซ้ำที่ถูกต้องสมบูรณ์

1
@ dar7yl นั่นคือสาเหตุที่ขวดแชมพูของฉันว่างเปล่าเสมอ ...
Brandon Ling

11

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


จากนั้นเดินไปตามหนังสือที่เหลือของ Hofstadter โปรดของฉันในขณะนี้เป็นหนึ่งในการแปลของบทกวี: Le Beau ตันทำ Marot ไม่ใช่วิชา CS แต่เป็นประเด็นที่น่าสนใจเกี่ยวกับการแปลว่าอะไรและหมายถึงอะไร
RBerteig

9

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

พูดถึงการคูณลองคิดดู

คำถาม:

* b คืออะไร

ตอบ:

ถ้า b คือ 1 จะเป็น a มิฉะนั้นจะเป็น + a * (b-1)

* (b-1) คืออะไร ดูคำถามข้างต้นเพื่อหาวิธีแก้ไข


@Andrew Grimm: เป็นคำถามที่ดี คำจำกัดความนี้ใช้สำหรับจำนวนธรรมชาติไม่ใช่จำนวนเต็ม
S.Lott

9

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

function writeNumbers( aNumber ){
 write(aNumber);
 if( aNumber > 0 ){
  writeNumbers( aNumber - 1 );
 }
 else{
  return;
 }
}

ฟังก์ชันนี้จะพิมพ์ตัวเลขทั้งหมดจากหมายเลขแรกที่คุณจะป้อนจนถึง 0 ดังนั้น:

writeNumbers( 10 );
//This wil write: 10 9 8 7 6 5 4 3 2 1 0
//and then stop because aNumber is no longer larger then 0

สิ่งที่เกิดขึ้นอย่างเด็ดขาดคือ writeNumbers (10) จะเขียน 10 จากนั้นเรียก writeNumbers (9) ซึ่งจะเขียน 9 จากนั้นเรียกใช้ writeNumber (8) ฯลฯ จนกระทั่ง writeNumbers (1) เขียน 1 แล้วเรียก writeNumbers (0) ซึ่งจะเขียน 0 ชนจะไม่เรียกใช้ writeNumbers (-1);

รหัสนี้เป็นหลักเช่นเดียวกับ:

for(i=10; i>0; i--){
 write(i);
}

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

var nestedArray = Array('Im a string', 
                        Array('Im a string nested in an array', 'me too!'),
                        'Im a string again',
                        Array('More nesting!',
                              Array('nested even more!')
                              ),
                        'Im the last string');
function printArrayItems( stringOrArray ){
 if(typeof stringOrArray === 'Array'){
   for(i=0; i<stringOrArray.length; i++){ 
     printArrayItems( stringOrArray[i] );
   }
 }
 else{
   write( stringOrArray );
 }
}

printArrayItems( stringOrArray );
//this will write:
//'Im a string' 'Im a string nested in an array' 'me too' 'Im a string again'
//'More nesting' 'Nested even more' 'Im the last string'

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

for(i=0; i<nestedArray.length; i++){
 if(typeof nestedArray[i] == 'Array'){
  for(a=0; i<nestedArray[i].length; a++){
   if(typeof nestedArray[i][a] == 'Array'){
    for(b=0; b<nestedArray[i][a].length; b++){
     //This would be enough for the nestedAaray we have now, but you would have
     //to nest the for loops even more if you would nest the array another level
     write( nestedArray[i][a][b] );
    }//end for b
   }//endif typeod nestedArray[i][a] == 'Array'
   else{ write( nestedArray[i][a] ); }
  }//end for a
 }//endif typeod nestedArray[i] == 'Array'
 else{ write( nestedArray[i] ); }
}//end for i

อย่างที่คุณเห็นวิธีการเวียนเกิดนั้นดีกว่ามาก


1
LOL - เอาฉันวินาทีเพื่อตระหนักว่าคุณใช้ JavaScript! ฉันเห็น "ฟังก์ชั่น" และคิดว่า PHP รู้แล้วว่าตัวแปรไม่ได้ขึ้นต้นด้วย $ ฉันคิดว่า C # สำหรับการใช้คำ var - แต่วิธีการไม่ได้เรียกว่าฟังก์ชั่น!
ozzy432836

8

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


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

6

ฉันจะพยายามอธิบายด้วยตัวอย่าง

คุณรู้ไหมว่า n! หมายถึง? หากไม่ใช่: http://en.wikipedia.org/wiki/Factorial

3! = 1 * 2 * 3 = 6

นี่จะเป็นรหัสเทียมบางส่วน

function factorial(n) {
  if (n==0) return 1
  else return (n * factorial(n-1))
}

ดังนั้นลองทำดู

factorial(3)

คือ 0

ไม่!

ดังนั้นเราจึงขุดลึกลงไปด้วยการสอบถามซ้ำ:

3 * factorial(3-1)

3-1 = 2

คือ 2 == 0?

ไม่!

ดังนั้นเราจะไปให้ลึก! 3 * 2 * แฟกทอเรียล (2-1) 2-1 = 1

คือ 1 == 0?

ไม่!

ดังนั้นเราจะไปให้ลึก! 3 * 2 * 1 * factorial (1-1) 1-1 = 0

คือ 0 == 0

ใช่!

เรามีกรณีเล็ก ๆ น้อย ๆ

ดังนั้นเราจึงมี 3 * 2 * 1 * 1 = 6

ฉันหวังว่าจะช่วยคุณ


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

วิธีการทำความเข้าใจการเรียกซ้ำที่ดีกว่าจะเป็นอย่างไร ฉันไม่ได้บอกว่าคุณต้องดูฟังก์ชั่นวนซ้ำทุกครั้งด้วยวิธีนี้ แต่มันช่วยให้ฉันเข้าใจว่ามันทำงานอย่างไร
Zoran Zaric

1
[ฉันไม่ได้ลงคะแนน -1, BTW] คุณอาจคิดแบบนี้: เชื่อว่าแฟคทอเรียล (n-1) ให้อย่างถูกต้อง (n-1)! = (n-1) * ... * 2 * 1 จากนั้น n แฟคทอเรียล (n-1) ให้ n * (n-1) ... * 2 * 1 ซึ่งก็คือ n! หรืออะไรก็ตาม [หากคุณกำลังพยายามเรียนรู้วิธีเขียนฟังก์ชันแบบเรียกซ้ำด้วยตัวคุณเองไม่เพียง แต่จะเห็นว่าฟังก์ชั่นบางอย่างทำงานอย่างไร]
ShreevatsaR

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

5

recursion

วิธี A, โทรวิธี A โทรวิธี A ในที่สุดหนึ่งในวิธีการเหล่านี้จะไม่เรียกและออก แต่มันก็เรียกซ้ำเพราะสิ่งที่เรียกตัวเอง

ตัวอย่างการเรียกซ้ำที่ฉันต้องการพิมพ์ทุกชื่อโฟลเดอร์บนฮาร์ดไดรฟ์: (ใน c #)

public void PrintFolderNames(DirectoryInfo directory)
{
    Console.WriteLine(directory.Name);

    DirectoryInfo[] children = directory.GetDirectories();

    foreach(var child in children)
    {
        PrintFolderNames(child); // See we call ourself here...
    }
}

กรณีฐานในตัวอย่างนี้อยู่ที่ไหน
Kunal Mukherjee

4

คุณใช้หนังสือเล่มไหน

ตำรามาตรฐานเกี่ยวกับอัลกอริทึมที่ดีจริง ๆ คือ Cormen & Rivest ประสบการณ์ของฉันคือมันสอนการเรียกซ้ำได้ค่อนข้างดี

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

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


4

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


4

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

สำหรับวง:

public printBar(length)
{
  String holder = "";
  for (int index = 0; i < length; i++)
  {
    holder += "*"
  }
  return holder;
}

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

public String printBar(int Length) // Method, to call the recursive function
{
  printBar(length, 0);
}

public String printBar(int length, int index) //Overloaded recursive method
{
  // To get a better idea of how this works without a for loop
  // you can also replace this if/else with the for loop and
  // operationally, it should do the same thing.
  if (index >= length)
    return "";
  else
    return "*" + printBar(length, index + 1); // Make recursive call
}

เพื่อให้เนื้อเรื่องสั้นยาวการเรียกซ้ำเป็นวิธีที่ดีในการเขียนโค้ดให้น้อยลง ใน printBar หลังแจ้งให้ทราบว่าเรามีคำสั่ง if หากถึงเงื่อนไขของเราแล้วเราจะออกจากการสอบถามซ้ำและกลับสู่วิธีก่อนหน้าซึ่งกลับไปเป็นวิธีก่อนหน้า ฯลฯ หากฉันส่ง printBar (8) ฉันจะได้รับ ******** ฉันหวังว่าด้วยตัวอย่างของฟังก์ชั่นง่าย ๆ ที่ทำเช่นเดียวกันกับ for loop ซึ่งอาจช่วยได้ คุณสามารถฝึกฝนได้มากกว่านี้ที่ Java Bat


javabat.comเป็นเว็บไซต์ที่มีประโยชน์อย่างมากที่จะช่วยให้คิดได้อย่างซ้ำ ๆ ฉันขอแนะนำให้ไปที่นั่นและพยายามแก้ปัญหาแบบเรียกซ้ำด้วยตัวเอง
Paradius

3

วิธีการทางคณิตศาสตร์อย่างแท้จริงในการสร้างฟังก์ชั่นวนซ้ำจะเป็นดังนี้:

1: ลองจินตนาการว่าคุณมีฟังก์ชั่นที่ถูกต้องสำหรับ f (n-1) สร้าง f อย่างที่ f (n) ถูกต้อง 2: สร้าง f เช่นว่า f (1) ถูกต้อง

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

ตอนนี้เป็นตัวอย่าง "ง่าย" สร้างฟังก์ชั่นที่สามารถตรวจสอบว่ามีความเป็นไปได้ที่จะมีการรวมกันของเหรียญ 5 เซ็นต์และ 7 เซ็นต์เพื่อสร้าง x เซนต์ ตัวอย่างเช่นเป็นไปได้ที่จะมี 17 เซนต์คูณ 2x5 + 1x7 แต่เป็นไปไม่ได้ที่จะมี 16 เซ็นต์

ทีนี้ลองนึกว่าคุณมีฟังก์ชั่นที่จะบอกคุณว่าเป็นไปได้ในการสร้าง x cents หรือไม่ตราบใดที่ x <n เรียกใช้ฟังก์ชันนี้ can_create_coins_small มันควรจะค่อนข้างง่ายที่จะจินตนาการถึงวิธีการทำให้ฟังก์ชั่นสำหรับ n ตอนนี้สร้างฟังก์ชั่นของคุณ:

bool can_create_coins(int n)
{
    if (n >= 7 && can_create_coins_small(n-7))
        return true;
    else if (n >= 5 && can_create_coins_small(n-5))
        return true;
    else
        return false;
}

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

bool can_create_coins(int n)
{
    if (n >= 7 && can_create_coins(n-7))
        return true;
    else if (n >= 5 && can_create_coins(n-5))
        return true;
    else
        return false;
}

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

bool can_create_coins(int n)
{
    if (n == 0)
        return true;
    else if (n >= 7 && can_create_coins(n-7))
        return true;
    else if (n >= 5 && can_create_coins(n-5))
        return true;
    else
        return false;
}

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

ในการใช้ข้อมูลนี้เพื่อแก้ปัญหาหอคอยแห่งฮานอยฉันคิดว่าเคล็ดลับคือสมมติว่าคุณมีฟังก์ชั่นที่จะย้ายแท็บเล็ต n-1 จาก a ไป b (สำหรับ a / b ใด ๆ ) พยายามย้ายตาราง n จาก a ไป b .


3

ตัวอย่างแบบเรียกซ้ำง่าย ๆ ในCommon LISP :

MYMAP ใช้ฟังก์ชันกับแต่ละองค์ประกอบในรายการ

1)รายการว่างไม่มีองค์ประกอบดังนั้นเราจึงส่งคืนรายการว่าง - () และ NIL ทั้งสองเป็นรายการว่าง

2)ใช้ฟังก์ชั่นกับรายการแรกโทร MYMAP สำหรับรายการที่เหลือ (การเรียกซ้ำ) และรวมผลลัพธ์ทั้งสองเข้าไว้ในรายการใหม่

(DEFUN MYMAP (FUNCTION LIST)
  (IF (NULL LIST)
      ()
      (CONS (FUNCALL FUNCTION (FIRST LIST))
            (MYMAP FUNCTION (REST LIST)))))

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

ตัวอย่างนี้เรียกใช้ฟังก์ชัน SIN ในแต่ละหมายเลขในรายการ (1 2 3 4)

Command: (mymap 'sin '(1 2 3 4))

1 Enter MYMAP SIN (1 2 3 4)
| 2 Enter MYMAP SIN (2 3 4)
|   3 Enter MYMAP SIN (3 4)
|   | 4 Enter MYMAP SIN (4)
|   |   5 Enter MYMAP SIN NIL
|   |   5 Exit MYMAP NIL
|   | 4 Exit MYMAP (-0.75680256)
|   3 Exit MYMAP (0.14112002 -0.75680256)
| 2 Exit MYMAP (0.9092975 0.14112002 -0.75680256)
1 Exit MYMAP (0.841471 0.9092975 0.14112002 -0.75680256)

นี่คือผลลัพธ์ของเรา:

(0.841471 0.9092975 0.14112002 -0.75680256)

อะไรคือตัวพิมพ์ใหญ่ทั้งหมด? อย่างจริงจังแม้ว่าพวกเขาออกไปอย่างมีสไตล์ใน LISP เมื่อ 20 ปีก่อน
เซบาสเตียน Krog

ฉันเขียนลงใน Lisp Machine model ซึ่งตอนนี้อายุ 17 ปี ที่จริงฉันเขียนฟังก์ชั่นโดยไม่มีการจัดรูปแบบในการฟังทำการแก้ไขแล้วใช้ PPRINT เพื่อจัดรูปแบบ ที่เปลี่ยนรหัสเป็น CAPS
Rainer Joswig

3

หากต้องการอธิบายการเรียกซ้ำให้กับเด็กอายุหกขวบก่อนอื่นให้อธิบายแก่เด็กอายุห้าขวบก่อนจากนั้นจึงรอหนึ่งปี

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

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

ในความเป็นจริงนั่นเป็นคำจำกัดความซ้ำของวิธีแก้ปัญหาด้วยการสอบถามซ้ำ


3

เด็กใช้การเรียกซ้ำโดยปริยายเช่น:

เดินทางไปถนน Disney World

เรายังอยู่หรือไม่ (ไม่)

พวกเราอยู่ที่นั่นหรือไม่ (เร็ว ๆ นี้)

เราอยู่ที่นั่นหรือยัง (เกือบ ... )

พวกเรายังอยู่หรือเปล่า (SHHHH)

พวกเราอยู่ที่นั่นหรือยัง (!!!!!)

เมื่อถึงจุดที่เด็กหลับ ...

ฟังก์ชันนับถอยหลังนี้เป็นตัวอย่างง่ายๆ:

function countdown()
      {
      return (arguments[0] > 0 ?
        (
        console.log(arguments[0]),countdown(arguments[0] - 1)) : 
        "done"
        );
      }
countdown(10);

กฎหมายของ Hofstadter ที่ใช้กับโครงการซอฟต์แวร์ก็มีความเกี่ยวข้องเช่นกัน

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

อ้างอิง


2

เมื่อทำงานกับโซลูชันแบบเรียกซ้ำฉันพยายาม:

  • สร้างเคสพื้นฐานก่อนเช่นเมื่อ n = 1 ในโซลูชันเพื่อแฟคทอเรียล
  • ลองคิดกฎทั่วไปสำหรับกรณีอื่น ๆ

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

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

สำหรับการอ้างอิงฉันขอแนะนำอัลกอริทึมโดย Robert Sedgewick

หวังว่าจะช่วย โชคดี.


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

2

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


1

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

ไม่แน่ใจว่าคำอุปมานี้มีผล ... :-)

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

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

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


1

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

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

ตัวอย่างเช่นเราต้องการคำนวณความยาวของรายการ ให้เรียกฟังก์ชั่นของเรา magical_length และผู้ช่วยเวทมนต์ของเราด้วย magical_length เรารู้ว่าถ้าเราให้รายการย่อยที่ไม่มีองค์ประกอบแรกมันจะทำให้เรามีความยาวของรายการย่อยด้วยเวทมนตร์ แล้วสิ่งเดียวที่เราต้องคิดก็คือวิธีรวมข้อมูลนี้กับงานของเรา ความยาวขององค์ประกอบแรกคือ 1 และ magic_counter ให้ความยาวของรายการย่อย n-1 ดังนั้นความยาวทั้งหมดคือ (n-1) + 1 -> n

int magical_length( list )
  sublist = rest_of_the_list( list )
  sublist_length = magical_length( sublist ) // you can think this function as magical and given to you
  return 1 + sublist_length

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

int magical_length( list )
  if ( list is empty) then
    return 0
  else
    sublist_length = magical_length( sublist ) // you can think this function as magical and given to you
    return 1 + sublist_length
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.