Hiểu đệ quy [đã đóng]


225

Tôi đang gặp khó khăn lớn trong việc hiểu đệ quy ở trường. Bất cứ khi nào giáo sư nói về nó, tôi dường như có được nó nhưng ngay khi tôi tự mình thử nó, nó hoàn toàn thổi bay bộ não của tôi.

Tôi đã cố gắng giải quyết Tháp Hà Nội cả đêm và hoàn toàn làm tôi kinh ngạc. Sách giáo khoa của tôi chỉ có khoảng 30 trang đệ quy nên không quá hữu ích. Có ai biết về những cuốn sách hoặc tài nguyên có thể giúp làm rõ chủ đề này?


200
Để hiểu đệ quy, trước tiên bạn phải hiểu đệ quy.
Paul Tomblin

40
Đệ quy: Xem đệ quy
Loren Pechtel

36
@Paul: Tôi có một trò đùa, nhưng tôi luôn nghĩ rằng nó sai về mặt kỹ thuật. Trường hợp điều kiện cơ bản khiến thuật toán kết thúc? Đó là một điều kiện cơ bản cho đệ quy. =)
Sergio Acosta

70
Tôi sẽ cho nó một shot: "Để hiểu đệ quy bạn cần hiểu đệ quy, cho đến khi bạn hiểu nó." =)
Sergio Acosta

91
Hãy xem câu hỏi này có thể giúp stackoverflow.com/questions/717725/under
Hiểu -recursion

Câu trả lời:


598

Làm thế nào để bạn làm trống một chiếc bình chứa năm bông hoa?

Trả lời: nếu chiếc bình không rỗng, bạn lấy ra một bông hoa và sau đó bạn làm trống một chiếc bình chứa bốn bông hoa.

Làm thế nào để bạn làm trống một chiếc bình chứa bốn bông hoa?

Trả lời: nếu chiếc bình không rỗng, bạn lấy ra một bông hoa và sau đó bạn làm trống một chiếc bình chứa ba bông hoa.

Làm thế nào để bạn làm trống một chiếc bình chứa ba bông hoa?

Trả lời: nếu chiếc bình không rỗng, bạn lấy ra một bông hoa và sau đó bạn làm trống một chiếc bình chứa hai bông hoa.

Làm thế nào để bạn làm trống một chiếc bình chứa hai bông hoa?

Trả lời: nếu chiếc bình không rỗng, bạn lấy ra một bông hoa và sau đó bạn làm trống một chiếc bình chứa một bông hoa.

Làm thế nào để bạn làm trống một chiếc bình chứa một bông hoa?

Trả lời: nếu chiếc bình không rỗng, bạn lấy ra một bông hoa và sau đó bạn làm trống một chiếc bình không chứa hoa.

Làm thế nào để bạn làm trống một chiếc bình không chứa hoa?

Trả lời: nếu chiếc bình không rỗng, bạn lấy ra một bông hoa nhưng chiếc bình trống thì bạn đã hoàn thành.

Điều đó lặp đi lặp lại. Hãy khái quát nó:

Làm thế nào để bạn làm trống một chiếc bình chứa N hoa?

Trả lời: nếu chiếc bình không rỗng, bạn lấy ra một bông hoa và sau đó bạn làm trống một chiếc bình chứa hoa N-1 .

Hmm, chúng ta có thể thấy điều đó trong mã?

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

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

Hmm, chúng ta không thể làm điều đó trong một vòng lặp for sao?

Tại sao, vâng, đệ quy có thể được thay thế bằng phép lặp, nhưng thường đệ quy là thanh lịch hơn.

Hãy nói về cây. Trong khoa học máy tính, một cây là một cấu trúc được tạo thành từ các nút , trong đó mỗi nút có một số số con cũng là các nút hoặc null. Một cây nhị phân là một cây làm bằng nút đó có chính xác hai trẻ em, thường được gọi là "trái" và "quyền"; một lần nữa, các con có thể là các nút, hoặc null. Một gốc là một nút không phải là con của bất kỳ nút nào khác.

Hãy tưởng tượng rằng một nút, ngoài các con của nó, có một giá trị, một số và tưởng tượng rằng chúng ta muốn tổng hợp tất cả các giá trị trong một số cây.

Để tính tổng giá trị trong bất kỳ một nút nào, chúng ta sẽ thêm giá trị của chính nút đó vào giá trị của con trái của nó, nếu có và giá trị của con phải của nó, nếu có. Bây giờ hãy nhớ lại rằng những đứa trẻ, nếu chúng không null, cũng là các nút.

Vì vậy, để tính tổng con trái, chúng ta sẽ thêm giá trị của nút con vào giá trị của con trái của nó, nếu có và giá trị của con phải của nó, nếu có.

Vì vậy, để tính tổng giá trị của con trái bên trái, chúng ta sẽ thêm giá trị của nút con vào giá trị của con trái của nó, nếu có và giá trị của con phải của nó, nếu có.

Có lẽ bạn đã dự đoán nơi tôi sẽ đi với điều này và muốn xem một số mã? ĐỒNG Ý:

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 ) ;
  }
}

Lưu ý rằng thay vì kiểm tra rõ ràng các em để xem chúng là null hay các nút, chúng ta chỉ làm cho hàm đệ quy trả về 0 cho một nút null.

Vì vậy, giả sử chúng ta có một cây trông như thế này (các số là giá trị, dấu gạch chéo trỏ đến trẻ em và @ có nghĩa là con trỏ trỏ đến null):

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

Nếu chúng ta gọi sumNode trên thư mục gốc (nút có giá trị 5), chúng ta sẽ trả về:

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

Hãy mở rộng nó tại chỗ. Ở mọi nơi chúng ta thấy sumNode, chúng ta sẽ thay thế nó bằng việc mở rộng câu lệnh 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 ;

Bây giờ hãy xem cách chúng tôi chinh phục một cấu trúc có độ sâu và "nhánh" tùy ý, bằng cách xem nó như là ứng dụng lặp đi lặp lại của một mẫu tổng hợp? mỗi lần thông qua hàm sumNode của chúng tôi, chúng tôi chỉ xử lý một nút duy nhất, sử dụng một nhánh if / then và hai câu lệnh trả về đơn giản gần như đã viết chúng, trực tiếp từ đặc tả của chúng tôi?

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

Đó là sức mạnh của đệ quy.


Ví dụ về chiếc bình ở trên là một ví dụ về đệ quy đuôi . Tất cả các đệ quy đuôi đó có nghĩa là trong hàm đệ quy, nếu chúng ta đệ quy (nghĩa là, nếu chúng ta gọi lại hàm đó), đó là điều cuối cùng chúng ta đã làm.

Ví dụ về cây không phải là đệ quy đuôi, bởi vì mặc dù điều cuối cùng chúng ta đã làm là tái phát đứa trẻ bên phải, trước khi chúng ta làm điều đó, chúng ta đã đệ quy đứa trẻ bên trái.

Trên thực tế, thứ tự mà chúng tôi gọi là trẻ em và thêm giá trị của nút hiện tại hoàn toàn không thành vấn đề, bởi vì việc bổ sung là giao hoán.

Bây giờ hãy xem xét một hoạt động trong đó trật tự có vấn đề. Chúng ta sẽ sử dụng cây nhị phân của các nút, nhưng lần này giá trị được giữ sẽ là một ký tự, không phải là một số.

Cây của chúng ta sẽ có một thuộc tính đặc biệt, đối với bất kỳ nút nào, ký tự của nó xuất hiện sau (theo thứ tự bảng chữ cái) ký tự được giữ bởi con trái của nó và trước (theo thứ tự bảng chữ cái) ký tự được giữ bởi con phải của nó.

Những gì chúng ta muốn làm là in cây theo thứ tự bảng chữ cái. Điều đó thật dễ dàng để làm, với tài sản đặc biệt của cây. Chúng ta chỉ in con trái, rồi ký tự của nút, rồi đến con phải.

Chúng tôi không chỉ muốn in willy-nilly, vì vậy chúng tôi sẽ chuyển chức năng của chúng tôi để in. Đây sẽ là một đối tượng có chức năng in (char); chúng ta không cần phải lo lắng về cách thức hoạt động của nó, chỉ là khi in được gọi, nó sẽ in một cái gì đó, ở đâu đó.

Hãy xem mã đó:

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 );

Ngoài thứ tự các hoạt động hiện đang quan trọng, ví dụ này minh họa rằng chúng ta có thể chuyển mọi thứ vào một hàm đệ quy. Điều duy nhất chúng ta phải làm là đảm bảo rằng trên mỗi cuộc gọi đệ quy, chúng ta tiếp tục chuyển nó theo. Chúng tôi đã chuyển trong một con trỏ nút và một máy in cho hàm và trên mỗi cuộc gọi đệ quy, chúng tôi đã chuyển chúng "xuống".

Bây giờ nếu cây của chúng ta trông như thế này:

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

Chúng ta sẽ in cái gì?

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

Vì vậy, nếu chúng ta chỉ nhìn vào các dòng đã được in:

    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

Chúng tôi thấy chúng tôi đã in "ahijkn", đó thực sự là theo thứ tự bảng chữ cái.

Chúng tôi quản lý để in toàn bộ một cây, theo thứ tự bảng chữ cái, chỉ bằng cách biết cách in một nút theo thứ tự bảng chữ cái. Đó chỉ là (vì cây của chúng ta có thuộc tính đặc biệt là sắp xếp các giá trị ở bên trái các giá trị theo thứ tự chữ cái) biết in con trái trước khi in giá trị của nút và in con phải sau khi in giá trị của nút.

đó là sức mạnh của đệ quy: có thể làm toàn bộ mọi việc bằng cách chỉ biết cách thực hiện một phần của toàn bộ (và biết khi nào nên dừng đệ quy).

Nhắc lại rằng trong hầu hết các ngôn ngữ, toán tử | | ("hoặc") ngắn mạch khi toán hạng đầu tiên của nó là đúng, hàm đệ quy chung là:

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

Luc M bình luận:

SO nên tạo một huy hiệu cho loại câu trả lời này. Xin chúc mừng!

Cảm ơn, Lục! Nhưng, thực ra, vì tôi đã chỉnh sửa câu trả lời này hơn bốn lần (để thêm ví dụ cuối cùng, nhưng chủ yếu là sửa lỗi chính tả và đánh bóng nó - gõ trên bàn phím netbook nhỏ rất khó), tôi không thể có thêm điểm nào cho nó . Điều này phần nào làm tôi nản lòng khi đặt nhiều nỗ lực vào các câu trả lời trong tương lai.

Xem bình luận của tôi ở đây về điều đó: /programming/128434/what-are-community-wiki-posts-in-stackoverflow/718699#718699


35

Não của bạn nổ tung vì nó đi vào một đệ quy vô hạn. Đó là một lỗi phổ biến cho người mới bắt đầu.

Dù bạn có tin hay không, bạn đã hiểu đệ quy, bạn chỉ bị kéo xuống bởi một phép ẩn dụ thông thường, nhưng bị lỗi cho một chức năng: một hộp nhỏ với những thứ ra vào.

Hãy suy nghĩ thay vì một nhiệm vụ hoặc thủ tục, chẳng hạn như "tìm hiểu thêm về đệ quy trên mạng". Đó là đệ quy và bạn không có vấn đề với nó. Để hoàn thành nhiệm vụ này, bạn có thể:

a) Đọc trang kết quả của Google để biết "đệ quy"
b) Khi bạn đã đọc nó, hãy theo liên kết đầu tiên trên đó và ...
a.1) Đọc trang mới về đệ quy 
b.1) Khi bạn đã đọc nó, hãy theo liên kết đầu tiên trên đó và ...
a.2) Đọc trang mới về đệ quy 
b.2) Khi bạn đã đọc nó, hãy theo liên kết đầu tiên trên đó và ...

Như bạn có thể thấy, bạn đã làm công cụ đệ quy trong một thời gian dài mà không gặp vấn đề gì.

Bao lâu bạn sẽ tiếp tục làm nhiệm vụ đó? Mãi mãi cho đến khi não bạn nổ tung? Tất nhiên là không, bạn sẽ dừng lại ở một điểm nhất định, bất cứ khi nào bạn tin rằng bạn đã hoàn thành nhiệm vụ.

Không cần chỉ định điều này khi yêu cầu bạn "tìm hiểu thêm về đệ quy trên mạng", bởi vì bạn là một con người và bạn có thể tự suy luận điều đó.

Máy tính không thể suy ra jack, vì vậy bạn phải bao gồm một kết thúc rõ ràng: "tìm hiểu thêm về đệ quy trên mạng, UNTIL bạn hiểu nó hoặc bạn đã đọc tối đa 10 trang ".

Bạn cũng đã suy luận rằng bạn nên bắt đầu tại trang kết quả của Google để "đệ quy" và một lần nữa đó là điều mà máy tính không thể làm được. Mô tả đầy đủ về nhiệm vụ đệ quy của chúng tôi cũng phải bao gồm một điểm bắt đầu rõ ràng:

"tìm hiểu thêm về đệ quy trên mạng, UNTIL bạn hiểu nó hoặc bạn đã đọc tối đa 10 trangbắt đầu tại www.google.com.vn/search?q=recursion "

Để tìm hiểu toàn bộ, tôi khuyên bạn nên thử bất kỳ cuốn sách nào trong số này:

  • Lisp thường gặp: Giới thiệu nhẹ nhàng về tính toán tượng trưng. Đây là lời giải thích phi toán học dễ thương nhất của đệ quy.
  • Các sơ đồ nhỏ.

6
Phép ẩn dụ của "function = hộp nhỏ I / O" hoạt động với đệ quy miễn là bạn cũng tưởng tượng rằng có một nhà máy ngoài kia tạo ra các bản sao vô hạn và hộp nhỏ của bạn có thể nuốt các hộp nhỏ khác.
ephemient

2
Thật thú vị..Vậy, trong tương lai robot sẽ google một cái gì đó và tự học bằng 10 liên kết đầu tiên. :) :)
kumar

2
@kumar không phải google đã làm điều đó với internet ..?
TJ

1
những cuốn sách tuyệt vời, cảm ơn vì lời giới thiệu
Max Koretskyi

+1 cho "Bộ não của bạn đã nổ tung vì nó bị đệ quy vô hạn. Đó là một lỗi phổ biến cho người mới bắt đầu."
Stack Underflow

26

Để hiểu đệ quy, tất cả những gì bạn phải làm là nhìn vào nhãn của chai dầu gội của bạn:

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

Vấn đề với điều này là không có điều kiện chấm dứt, và đệ quy sẽ lặp lại vô thời hạn, hoặc cho đến khi bạn hết dầu gội hoặc nước nóng (điều kiện chấm dứt bên ngoài, tương tự như thổi chồng của bạn).


6
Cảm ơn bạn dar7yl - điều đó LUÔN LUÔN làm tôi khó chịu với những chai dầu gội đầu. (Tôi đoán rằng tôi luôn luôn được dành cho lập trình). Mặc dù tôi cá là anh chàng đã quyết định thêm 'Lặp lại "vào cuối hướng dẫn đã khiến công ty trở thành hàng triệu người.
Kenj0418

5
Tôi hy vọng bạn rinse()sau bạnlather()
CoderDennis

@JakeWilson nếu tối ưu hóa cuộc gọi đuôi được sử dụng - chắc chắn. như hiện tại, nó là một đệ quy hoàn toàn hợp lệ.

1
@ dar7yl vậy đó là lý do tại sao chai dầu gội đầu của tôi luôn trống rỗng ...
Brandon Ling

11

Nếu bạn muốn có một cuốn sách làm tốt công việc giải thích đệ quy bằng những thuật ngữ đơn giản, hãy xem Gôdel, Escher, Bach: An Eternal Golden Braid của Douglas Hofstadter, đặc biệt là Chương 5. Ngoài ra, việc giải thích nó là một công việc tốt để giải thích một số khái niệm phức tạp trong khoa học máy tính và toán học một cách dễ hiểu, với một giải thích dựa trên một giải thích khác. Nếu trước đây bạn chưa tiếp xúc nhiều với các loại khái niệm này, thì đây có thể là một cuốn sách hay.


Và sau đó đi lang thang qua phần còn lại của những cuốn sách của Hofstadter. Yêu thích của tôi tại thời điểm này là một trong những bản dịch thơ: Le Ton Beau do Marot . Không chính xác là một chủ đề CS, nhưng nó đặt ra những vấn đề thú vị về bản dịch thực sự là gì và có nghĩa là gì.
RBerteig

9

Đây là một khiếu nại nhiều hơn là một câu hỏi. Bạn có một câu hỏi cụ thể hơn về đệ quy? Giống như phép nhân, đó không phải là điều mọi người viết nhiều.

Nói về phép nhân, hãy nghĩ về điều này.

Câu hỏi:

* B là gì?

Câu trả lời:

Nếu b là 1, thì đó là a. Mặt khác, nó là + a * (b-1).

* (B-1) là gì? Xem câu hỏi trên để biết cách giải quyết.


@Andrew Grimm: Câu hỏi hay. Định nghĩa này là cho số tự nhiên, không phải số nguyên.
S.Lott

9

Tôi nghĩ rằng phương pháp rất đơn giản này sẽ giúp bạn hiểu đệ quy. Phương thức sẽ tự gọi cho đến khi một điều kiện nào đó là đúng và sau đó trả về:

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

Hàm này sẽ in ra tất cả các số từ số đầu tiên bạn sẽ cung cấp cho đến 0. Do đó:

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

Điều khó hiểu xảy ra là writeNumbers (10) sẽ viết 10 và sau đó gọi writeNumbers (9) sẽ viết 9 và sau đó gọi writeNumber (8), vv Cho đến khi writeNumbers (1) viết 1 và sau đó gọi writeNumbers (0) sẽ viết 0 mông sẽ không gọi writeNumbers (-1);

Mã này về cơ bản giống như:

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

Vậy thì tại sao lại sử dụng đệ quy mà bạn có thể hỏi, nếu một vòng lặp for về cơ bản giống nhau. Chà, bạn chủ yếu sử dụng đệ quy khi bạn sẽ phải làm tổ cho các vòng lặp nhưng sẽ không biết chúng được lồng sâu đến mức nào. Ví dụ: khi in ra các mục từ mảng lồng nhau:

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'

Hàm này có thể lấy một mảng có thể được lồng vào 100 cấp độ, trong khi bạn viết một vòng lặp for sau đó sẽ yêu cầu bạn lồng nó 100 lần:

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

Như bạn có thể thấy phương pháp đệ quy tốt hơn rất nhiều.


1
LOL - đưa tôi một giây để nhận ra bạn đang sử dụng JavaScript! Tôi đã thấy "hàm" và nghĩ rằng PHP sau đó nhận ra các biến không bắt đầu bằng $. Sau đó, tôi nghĩ C # để sử dụng từ var - nhưng các phương thức không được gọi là hàm!
ozzy432836

8

Trên thực tế, bạn sử dụng đệ quy để giảm sự phức tạp của vấn đề của bạn. Bạn áp dụng đệ quy cho đến khi bạn đạt được một trường hợp cơ bản đơn giản có thể được giải quyết dễ dàng. Với điều này, bạn có thể giải quyết bước đệ quy cuối cùng. Và với điều này, tất cả các bước đệ quy khác cho đến vấn đề ban đầu của bạn.


1
Tôi đồng ý với câu trả lời này. Bí quyết là xác định và giải quyết trường hợp cơ bản (đơn giản nhất). Và sau đó diễn đạt vấn đề theo trường hợp đơn giản nhất (mà bạn đã giải quyết).
Sergio Acosta

6

Tôi sẽ cố gắng giải thích nó với một ví dụ.

Bạn biết gì n! có nghĩa? Nếu không: http://en.wikipedia.org/wiki/Factorial

3! = 1 * 2 * 3 = 6

ở đây đi một số mã giả

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

Vì vậy, hãy thử nó:

factorial(3)

là n 0?

Không!

vì vậy chúng tôi đào sâu hơn với đệ quy của chúng tôi:

3 * factorial(3-1)

3-1 = 2

là 2 == 0?

Không!

để chúng ta đi sâu hơn 3 * 2 * giai thừa (2-1) 2-1 = 1

là 1 == 0?

Không!

để chúng ta đi sâu hơn 3 * 2 * 1 * giai thừa (1-1) 1-1 = 0

là 0 == 0?

Đúng!

chúng tôi có một trường hợp tầm thường

vậy ta có 3 * 2 * 1 * 1 = 6

tôi hy vọng sự giúp đỡ của bạn


Đây không phải là một cách hữu ích để suy nghĩ về đệ quy. Một người mới bắt đầu mắc lỗi là cố gắng tưởng tượng những gì xảy ra bên trong cuộc gọi bị từ chối, thay vì chỉ tin tưởng / chứng minh rằng nó sẽ trả lời đúng - và câu trả lời này dường như khuyến khích điều đó.
ShreevatsaR

một cách tốt hơn để hiểu đệ quy là gì? Tôi không nói bạn phải xem xét mọi chức năng đệ quy theo cách này. Nhưng nó đã giúp tôi hiểu làm thế nào nó hoạt động.
Zoran Zaric

1
[Tôi đã không bỏ phiếu -1, BTW.] Bạn có thể nghĩ như thế này: tin rằng giai thừa đó (n-1) đưa ra chính xác (n-1)! = (N-1) * ... * 2 * 1, sau đó n giai thừa (n-1) cho n * (n-1) ... * 2 * 1, đó là n!. Hay bất cứ cái gì. [Nếu bạn đang cố gắng tự học cách viết các hàm đệ quy, không chỉ xem một số chức năng làm gì.]
ShreevatsaR

Tôi đã sử dụng các yếu tố khi giải thích đệ quy và tôi nghĩ một trong những lý do phổ biến khiến nó thất bại là một ví dụ là vì người giải thích không thích toán học, và bị cuốn vào đó. (Có hay không một người không thích toán học nên mã hóa là một câu hỏi khác). Vì lý do đó, tôi thường cố gắng sử dụng một ví dụ phi toán học nếu có thể.
Tony Meyer

5

Đệ quy

Phương thức A, gọi Phương thức A gọi Phương thức A. Cuối cùng, một trong những phương thức này A sẽ không gọi và thoát, nhưng đó là đệ quy vì có gì đó tự gọi.

Ví dụ về đệ quy nơi tôi muốn in ra mọi tên thư mục trên ổ cứng: (trong 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...
    }
}

trường hợp cơ sở trong ví dụ này là ở đâu?
Kunal Mukherjee

4

Bạn đang sử dụng cuốn sách nào?

Sách giáo khoa tiêu chuẩn về các thuật toán thực sự tốt là Cormen & Rivest. Kinh nghiệm của tôi là nó dạy đệ quy khá tốt.

Đệ quy là một trong những phần khó hơn của lập trình để nắm bắt, và trong khi nó đòi hỏi bản năng, nó có thể được học. Nhưng nó cần một mô tả hay, ví dụ hay và minh họa tốt.

Ngoài ra, 30 trang nói chung là rất nhiều, 30 trang trong một ngôn ngữ lập trình là khó hiểu. Đừng cố học đệ quy trong C hoặc Java, trước khi bạn hiểu đệ quy nói chung từ một cuốn sách chung.


4

Hàm đệ quy đơn giản là một hàm tự gọi nó nhiều lần như nó cần để làm như vậy. Nó hữu ích nếu bạn cần xử lý một cái gì đó nhiều lần, nhưng bạn không chắc chắn sẽ thực sự cần bao nhiêu lần. Theo một cách nào đó, bạn có thể nghĩ về một hàm đệ quy như một kiểu vòng lặp. Tuy nhiên, giống như một vòng lặp, bạn sẽ cần chỉ định các điều kiện để quá trình bị phá vỡ nếu không nó sẽ trở thành vô hạn.


4

http://javabat.com là một nơi vui vẻ và thú vị để thực hành đệ quy. Các ví dụ của họ bắt đầu khá nhẹ nhàng và hoạt động rộng rãi (nếu bạn muốn đưa nó đi xa). Lưu ý: Cách tiếp cận của họ là học bằng cách thực hành. Đây là một hàm đệ quy mà tôi đã viết chỉ đơn giản là thay thế một vòng lặp for.

Vòng lặp for:

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

Đây là đệ quy để làm điều tương tự. (lưu ý chúng tôi quá tải phương thức đầu tiên để đảm bảo nó được sử dụng như trên). Chúng tôi cũng có một phương pháp khác để duy trì chỉ mục của chúng tôi (tương tự như cách câu lệnh for thực hiện cho bạn ở trên). Hàm đệ quy phải duy trì chỉ số riêng của chúng.

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
}

Để làm cho một câu chuyện dài ngắn, đệ quy là một cách tốt để viết ít mã. Trong thông báo printBar sau, chúng ta có một câu lệnh if. NẾU điều kiện của chúng tôi đã đạt được, chúng tôi sẽ thoát khỏi đệ quy và quay lại phương thức trước đó, trở lại phương thức trước đó, v.v. Nếu tôi gửi trong printBar (8), tôi nhận được ********. Tôi hy vọng rằng với một ví dụ về một hàm đơn giản thực hiện điều tương tự như một vòng lặp for có thể điều này sẽ giúp ích. Bạn có thể thực hành điều này nhiều hơn tại Java Bat.


javabat.com là một trang web cực kỳ hữu ích sẽ giúp người ta suy nghĩ đệ quy. Tôi đặc biệt khuyên bạn nên đến đó và cố gắng tự mình giải quyết các vấn đề đệ quy.
Paradius

3

Cách thực sự toán học để xem xét việc xây dựng một hàm đệ quy sẽ như sau:

1: Hãy tưởng tượng bạn có một hàm đúng với f (n-1), xây dựng f sao cho f (n) đúng. 2: Xây dựng f, sao cho f (1) đúng.

Đây là cách bạn có thể chứng minh rằng hàm này là chính xác, về mặt toán học và nó được gọi là Cảm ứng . Nó tương đương với các trường hợp cơ sở khác nhau, hoặc các hàm phức tạp hơn trên nhiều biến). Nó cũng tương đương với tưởng tượng rằng f (x) đúng với mọi x

Bây giờ cho một ví dụ "đơn giản". Xây dựng một chức năng có thể xác định xem có thể có sự kết hợp đồng xu 5 xu và 7 xu để tạo x xu không. Ví dụ: có thể có 17 xu bằng 2x5 + 1x7, nhưng không thể có 16 xu.

Bây giờ hãy tưởng tượng bạn có một hàm cho bạn biết nếu có thể tạo x xu, miễn là x <n. Gọi hàm này là can_create_coins_small. Nó khá đơn giản để tưởng tượng làm thế nào để thực hiện chức năng cho n. Bây giờ xây dựng chức năng của bạ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;
}

Mẹo ở đây là nhận ra rằng can_create_coins hoạt động cho n, có nghĩa là bạn có thể thay thế can_create_coins cho can_create_coins_small, đưa ra:

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;
}

Một điều cuối cùng cần làm là có một trường hợp cơ sở để ngăn chặn đệ quy vô hạn. Lưu ý rằng nếu bạn đang cố gắng tạo 0 xu, thì điều đó là có thể bằng cách không có tiền. Thêm điều kiện này cho:

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;
}

Có thể chứng minh rằng hàm này sẽ luôn quay trở lại, sử dụng một phương thức gọi là gốc vô hạn , nhưng điều đó không cần thiết ở đây. Bạn có thể tưởng tượng rằng f (n) chỉ gọi các giá trị thấp hơn của n và cuối cùng sẽ luôn đạt 0.

Để sử dụng thông tin này để giải quyết vấn đề Tháp Hà Nội của bạn, tôi nghĩ mẹo là giả sử bạn có chức năng di chuyển máy tính bảng n-1 từ a sang b (cho bất kỳ a / b) nào, cố gắng di chuyển n bảng từ a sang b .


3

Ví dụ đệ quy đơn giản trong Common Lisp :

MYMAP áp dụng một chức năng cho từng thành phần trong danh sách.

1) danh sách trống không có phần tử, vì vậy chúng tôi trả về danh sách trống - () và cả NIL đều là danh sách trống.

2) áp dụng chức năng cho danh sách đầu tiên, gọi MYMAP cho phần còn lại của danh sách (cuộc gọi đệ quy) và kết hợp cả hai kết quả vào một danh sách mới.

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

Chúng ta hãy xem thực hiện theo dõi. Khi ENTER một chức năng, các đối số được in. Khi EXITing một chức năng, kết quả được in. Đối với mỗi cuộc gọi đệ quy, đầu ra sẽ được thụt vào mức.

Ví dụ này gọi hàm SIN trên mỗi số trong danh sách (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)

Đây là kết quả của chúng tôi :

(0.841471 0.9092975 0.14112002 -0.75680256)

CÁI GÌ VỚI TẤT CẢ CÁC CAPS? Nghiêm túc, mặc dù, họ đã đi ra khỏi phong cách trong LISP khoảng 20 năm trước.
Sebastian Krog

Chà, tôi đã viết nó trên một mô hình Lisp Machine, hiện đã 17 tuổi. Trên thực tế tôi đã viết hàm mà không có định dạng trong trình nghe, thực hiện một số chỉnh sửa và sau đó sử dụng PPRINT để định dạng nó. Điều đó đã biến mã thành CAPS.
Rainer Joswig

3

Để giải thích đệ quy cho một đứa trẻ sáu tuổi, trước tiên hãy giải thích nó cho một đứa trẻ năm tuổi, và sau đó đợi một năm.

Trên thực tế, đây là một ví dụ phản biện hữu ích, bởi vì cuộc gọi đệ quy của bạn nên đơn giản hơn, không khó hơn. Sẽ còn khó hơn để giải thích đệ quy cho một đứa trẻ năm tuổi, và mặc dù bạn có thể dừng đệ quy ở mức 0, bạn không có giải pháp đơn giản nào để giải thích đệ quy cho một đứa trẻ 0 tuổi.

Để giải quyết vấn đề bằng cách sử dụng đệ quy, trước tiên, hãy chia nó thành một hoặc nhiều vấn đề đơn giản hơn mà bạn có thể giải quyết theo cùng một cách, và sau đó khi vấn đề đủ đơn giản để giải quyết mà không cần đệ quy thêm, bạn có thể quay trở lại mức cao hơn.

Trong thực tế, đó là một định nghĩa đệ quy về cách giải quyết vấn đề bằng đệ quy.


3

Trẻ em ngầm sử dụng đệ quy, ví dụ:

Chuyến đi đến Disney World

Chúng ta đã ở đó chưa? (Không)

Chúng ta đã ở đó chưa? (Sớm)

Chúng ta đã ở đó chưa? (Hầu như ...)

Chúng ta đã ở đó chưa? (SHHHH)

Chúng ta đã ở đó chưa?(!!!!!)

Lúc đó đứa trẻ ngủ thiếp đi ...

Hàm đếm ngược này là một ví dụ đơn giản:

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

Luật của Hofstadter áp dụng cho các dự án phần mềm cũng có liên quan.

Bản chất của ngôn ngữ loài người, theo Chomsky, khả năng của bộ não hữu hạn để tạo ra thứ mà ông coi là ngữ pháp vô hạn. Điều này có nghĩa là anh ta không chỉ không có giới hạn trên đối với những gì chúng ta có thể nói, mà còn không có giới hạn trên về số lượng câu mà ngôn ngữ của chúng tôi có, không có giới hạn trên về kích thước của bất kỳ câu cụ thể nào. Chomsky đã tuyên bố rằng công cụ cơ bản làm nền tảng cho tất cả sự sáng tạo này của ngôn ngữ loài người là đệ quy: khả năng cho một cụm từ tái hiện bên trong một cụm từ khác cùng loại. Nếu tôi nói "nhà của anh trai John", tôi có một danh từ "nhà", xuất hiện trong một cụm danh từ, "nhà của anh trai", và cụm danh từ đó xuất hiện trong một cụm danh từ khác, "nhà của anh trai John". Điều này rất có ý nghĩa, và nó '

Người giới thiệu


2

Khi làm việc với các giải pháp đệ quy, tôi luôn cố gắng:

  • Thiết lập trường hợp cơ sở đầu tiên tức là khi n = 1 trong một giải pháp cho giai thừa
  • Cố gắng đưa ra một quy tắc chung cho mọi trường hợp khác

Ngoài ra còn có các loại giải pháp đệ quy khác nhau, có cách tiếp cận phân chia và chinh phục hữu ích cho các fractals và nhiều phương pháp khác.

Nó cũng sẽ giúp nếu bạn có thể giải quyết các vấn đề đơn giản trước tiên chỉ để giải quyết vấn đề. Một số ví dụ đang giải quyết cho giai thừa và tạo ra số thứ n.

Để tham khảo, tôi đánh giá cao Thuật toán của Robert Sedgewick.

Mong rằng sẽ giúp. Chúc may mắn.


Tôi tự hỏi liệu có tốt hơn không khi đưa ra một quy tắc chung, cuộc gọi đệ quy, "đơn giản" hơn so với những gì bạn bắt đầu. Sau đó, trường hợp cơ sở sẽ trở nên rõ ràng dựa trên trường hợp đơn giản nhất. Đó là cách tôi có xu hướng nghĩ về việc giải quyết vấn đề một cách đệ quy.
dlaliberte

2

Ôi. Tôi đã cố gắng tìm ra Tháp Hà Nội năm ngoái. Điều khó khăn về TOH không phải là một ví dụ đơn giản về đệ quy - bạn đã thu hồi lồng nhau, điều này cũng thay đổi vai trò của các tháp trong mỗi cuộc gọi. Cách duy nhất tôi có thể khiến nó trở nên có ý nghĩa là hình dung theo nghĩa đen sự chuyển động của những chiếc nhẫn trong mắt tôi và diễn đạt bằng lời gọi cuộc gọi đệ quy sẽ là gì. Tôi sẽ bắt đầu với một vòng duy nhất, sau đó hai, rồi ba. Tôi thực sự đã đặt hàng các trò chơi trên internet. Tôi phải mất hai hoặc ba ngày để phá vỡ bộ não của mình để có được nó.


1

Hàm đệ quy giống như một lò xo bạn nén một chút trên mỗi cuộc gọi. Trên mỗi bước, bạn đặt một chút thông tin (bối cảnh hiện tại) vào một ngăn xếp. Khi bước cuối cùng đạt được, mùa xuân được giải phóng, thu thập tất cả các giá trị (bối cảnh) cùng một lúc!

Không chắc phép ẩn dụ này có hiệu quả ... :-)

Dù sao, ngoài các ví dụ cổ điển (giai thừa là ví dụ tồi tệ nhất vì nó không hiệu quả và dễ bị san phẳng, Fibonacci, Hà Nội ...) là một chút giả tạo (tôi hiếm khi, nếu sử dụng chúng trong các trường hợp lập trình thực), đó là thú vị để xem nơi nó thực sự được sử dụng.

Một trường hợp rất phổ biến là đi bộ một cây (hoặc biểu đồ, nhưng nói chung cây phổ biến hơn).
Ví dụ: phân cấp thư mục: để liệt kê các tệp, bạn lặp lại trên chúng. Nếu bạn tìm thấy một thư mục con, hàm liệt kê các tệp gọi chính nó với thư mục mới làm đối số. Khi trở về từ việc liệt kê thư mục mới này (và các thư mục con của nó!), Nó sẽ tiếp tục bối cảnh của nó, đến tệp tiếp theo (hoặc thư mục).
Một trường hợp cụ thể khác là khi vẽ một hệ thống phân cấp của các thành phần GUI: thông thường có các thùng chứa, như panes, để giữ các thành phần cũng có thể là pan, hoặc các thành phần hỗn hợp, v.v ... Quy trình vẽ thường gọi đệ quy chức năng vẽ của từng thành phần, gọi chức năng sơn của tất cả các thành phần mà nó giữ, v.v.

Không chắc chắn nếu tôi rất rõ ràng, nhưng tôi thích thể hiện việc sử dụng tài liệu giảng dạy trong thế giới thực, vì đó là điều mà tôi đã vấp ngã trong quá khứ.


1

Hãy suy nghĩ một con ong thợ. Nó cố gắng làm mật ong. Nó làm công việc của mình và hy vọng những con ong thợ khác sẽ làm phần còn lại của mật ong. Và khi tổ ong đầy, nó dừng lại.

Hãy nghĩ rằng đó là phép thuật. Bạn có một hàm có cùng tên với hàm bạn đang cố thực hiện và khi bạn đưa nó vào bài toán con, nó sẽ giải quyết nó cho bạn và điều duy nhất bạn cần làm là tích hợp giải pháp của phần của bạn với giải pháp đó cho bạn.

Ví dụ: chúng tôi muốn tính độ dài của danh sách. Hãy gọi hàm của chúng tôi là Magical_length và người trợ giúp ma thuật của chúng tôi bằng Magical_length Chúng tôi biết rằng nếu chúng tôi đưa ra danh sách phụ không có yếu tố đầu tiên, nó sẽ cho chúng tôi độ dài của danh sách phụ bằng phép thuật. Sau đó, điều duy nhất chúng ta cần nghĩ là làm thế nào để tích hợp thông tin này với công việc của chúng ta. Độ dài của phần tử đầu tiên là 1 và magic_count cho chúng ta độ dài của danh sách con n-1, do đó tổng chiều dài là (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

Tuy nhiên, câu trả lời này không đầy đủ vì chúng tôi đã không xem xét điều gì xảy ra nếu chúng tôi đưa ra một danh sách trống. Chúng tôi nghĩ rằng danh sách chúng tôi luôn có ít nhất một yếu tố. Do đó, chúng ta cần suy nghĩ xem câu trả lời nào sẽ là câu trả lời nếu chúng ta được cung cấp một danh sách trống và câu trả lời rõ ràng là 0. Vì vậy, hãy thêm thông tin này vào hàm của chúng ta và đây được gọi là điều kiện cơ sở / cạnh.

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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.