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.
Và đó 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