Đệ quy là gì và khi nào tôi nên sử dụng nó?


121

Một trong những chủ đề dường như xuất hiện thường xuyên trên các danh sách gửi thư và các cuộc thảo luận trực tuyến là giá trị (hoặc thiếu) của việc làm Bằng Khoa học Máy tính. Một lập luận dường như xuất hiện nhiều lần cho bên phủ định là họ đã viết mã trong một số năm và họ chưa bao giờ sử dụng đệ quy.

Vì vậy, câu hỏi là:

  1. Đệ quy là gì?
  2. Khi nào tôi sử dụng đệ quy?
  3. Tại sao mọi người không sử dụng đệ quy?

9
Và có thể điều này sẽ giúp ích: stackoverflow.com/questions/126756/…
kennytm

3
Điều này có thể giúp nắm bắt các khái niệm: điều hướng đến các liên kết được cung cấp trên nhận xét thứ hai của câu hỏi trên trang này và làm những gì các ý kiến nói làm: stackoverflow.com/questions/3021/...
dtmland

Câu trả lời:


86

Có một số giải thích tốt về đệ quy trong chuỗi này, câu trả lời này là về lý do tại sao bạn không nên sử dụng nó trong hầu hết các ngôn ngữ. * Trong phần lớn các triển khai ngôn ngữ mệnh lệnh chính (tức là mọi triển khai chính của C, C ++, Basic, Python , Ruby, Java và C #) lặp lại rất thích hợp hơn đệ quy.

Để biết lý do tại sao, hãy làm theo các bước mà các ngôn ngữ trên sử dụng để gọi một hàm:

  1. khoảng trống được tạo ra trên ngăn xếp cho các đối số của hàm và các biến cục bộ
  2. các đối số của hàm được sao chép vào không gian mới này
  3. điều khiển nhảy đến chức năng
  4. mã của chức năng chạy
  5. kết quả của hàm được sao chép thành giá trị trả về
  6. ngăn xếp được quấn lại vị trí cũ của nó
  7. điều khiển nhảy trở lại nơi hàm được gọi

Thực hiện tất cả các bước này cần thời gian, thường nhiều hơn một chút so với việc lặp qua một vòng lặp. Tuy nhiên, vấn đề thực sự là ở bước số 1. Khi nhiều chương trình khởi động, chúng phân bổ một đoạn bộ nhớ duy nhất cho ngăn xếp của chúng và khi chúng hết bộ nhớ đó (thường xuyên, nhưng không phải lúc nào cũng do đệ quy), chương trình bị treo do tràn ngăn xếp. .

Vì vậy, trong các ngôn ngữ này, việc đệ quy chậm hơn và nó khiến bạn dễ bị rơi. Tuy nhiên, vẫn có một số đối số để sử dụng nó. Nói chung, mã được viết đệ quy ngắn hơn và thanh lịch hơn một chút, một khi bạn biết cách đọc nó.

Có một kỹ thuật mà người triển khai ngôn ngữ có thể sử dụng được gọi là tối ưu hóa cuộc gọi đuôi có thể loại bỏ một số lớp tràn ngăn xếp. Nói một cách ngắn gọn: nếu biểu thức trả về của một hàm chỉ đơn giản là kết quả của một lệnh gọi hàm, thì bạn không cần phải thêm một cấp mới vào ngăn xếp, bạn có thể sử dụng lại cấp hiện tại cho hàm đang được gọi. Thật không may, một số triển khai ngôn ngữ bắt buộc có tích hợp tính năng tối ưu hóa cuộc gọi đuôi.

* Tôi thích đệ quy. Ngôn ngữ tĩnh yêu thích của tôi hoàn toàn không sử dụng vòng lặp, đệ quy là cách duy nhất để làm điều gì đó lặp đi lặp lại. Tôi không nghĩ rằng đệ quy nói chung là một ý tưởng hay trong các ngôn ngữ không được điều chỉnh cho nó.

** Nhân tiện Mario, tên tiêu biểu cho hàm Sắp xếp của bạn là "tham gia", và tôi sẽ rất ngạc nhiên nếu ngôn ngữ bạn chọn chưa có cách triển khai nó.


1
Tốt để xem giải thích về chi phí vốn có của đệ quy. Tôi cũng đã đề cập đến điều đó trong câu trả lời của mình. Nhưng với tôi, sức mạnh lớn với đệ quy là những gì bạn có thể làm với ngăn xếp cuộc gọi. Bạn có thể viết một thuật toán ngắn gọn với đệ quy phân nhánh lặp lại, cho phép bạn thực hiện những việc như thu thập thông tin phân cấp (quan hệ cha / con) một cách dễ dàng. Xem câu trả lời của tôi cho một ví dụ.
Steve Wortham

7
Rất thất vọng khi tìm thấy câu trả lời hàng đầu cho câu hỏi có tiêu đề "Đệ quy là gì và khi nào tôi nên sử dụng nó?" không thực sự trả lời một trong hai điều đó, đừng bận tâm đến cảnh báo cực kỳ thiên vị chống lại đệ quy, mặc dù nó được sử dụng rộng rãi trong hầu hết các ngôn ngữ bạn đã đề cập (không có gì sai cụ thể về những gì bạn nói, nhưng bạn dường như đang phóng đại vấn đề và phóng đại quá mức Tính hữu ích).
Bernhard Barker

2
Có lẽ bạn đúng @Dukeling. Đối với ngữ cảnh, khi tôi viết câu trả lời này, có rất nhiều lời giải thích tuyệt vời về đệ quy đã được viết sẵn và tôi viết câu này với mục đích là một phần bổ trợ cho thông tin đó, không phải câu trả lời hàng đầu. Trong thực tế, khi tôi cần đi dạo một cái cây hoặc xử lý bất kỳ cấu trúc dữ liệu lồng nhau nào khác, tôi thường chuyển sang đệ quy và tôi vẫn chưa gặp phải sự cố tràn ngăn xếp do chính tôi tạo ra.
Peter Burns

63

Ví dụ tiếng anh đơn giản về đệ quy.

A child couldn't sleep, so her mother told her a story about a little frog,
    who couldn't sleep, so the frog's mother told her a story about a little bear,
         who couldn't sleep, so the bear's mother told her a story about a little weasel... 
            who fell asleep.
         ...and the little bear fell asleep;
    ...and the little frog fell asleep;
...and the child fell asleep.

1
lên + cho chạm vào trái tim :)
Suhail Mumtaz Awan

Có một câu chuyện tương tự như thế này dành cho những đứa trẻ không ngủ quên trong truyện dân gian Trung Quốc, tôi chỉ nhớ câu chuyện đó, và nó nhắc tôi nhớ cách thức hoạt động của đệ quy trong thế giới thực.
Harvey Lin

49

Theo nghĩa khoa học máy tính cơ bản nhất, đệ quy là một hàm gọi chính nó. Giả sử bạn có cấu trúc danh sách liên kết:

struct Node {
    Node* next;
};

Và bạn muốn tìm hiểu xem một danh sách được liên kết dài bao lâu, bạn có thể thực hiện việc này với đệ quy:

int length(const Node* list) {
    if (!list->next) {
        return 1;
    } else {
        return 1 + length(list->next);
    }
}

(Tất nhiên, điều này cũng có thể được thực hiện với vòng lặp for, nhưng rất hữu ích như một minh họa cho khái niệm)


@Christopher: Đây là một ví dụ hay và đơn giản về đệ quy. Cụ thể đây là một ví dụ về đệ quy đuôi. Tuy nhiên, như Andreas đã nói, nó có thể dễ dàng được viết lại (hiệu quả hơn) bằng vòng lặp for. Như tôi đã giải thích trong câu trả lời của mình, có nhiều cách sử dụng tốt hơn cho đệ quy.
Steve Wortham

2
bạn có thực sự cần một tuyên bố khác ở đây không?
Adrien Be

1
Không, nó chỉ ở đó để rõ ràng.
Andreas Brinck

@SteveWortham: Đây không phải là đuôi đệ quy như đã viết; length(list->next)vẫn cần quay lại để length(list)sau này có thể thêm 1 vào kết quả. Nó được viết để vượt qua quãng thời gian dài cho đến nay, chỉ khi đó chúng ta mới có thể quên đi sự tồn tại của người gọi. Thích int length(const Node* list, int count=0) { return (!list) ? count : length(list->next, count + 1); }.
cHao

46

Bất cứ khi nào một hàm gọi chính nó, tạo ra một vòng lặp, thì đó là đệ quy. Như với bất cứ điều gì, có cách sử dụng tốt và cách sử dụng xấu cho đệ quy.

Ví dụ đơn giản nhất là đệ quy đuôi trong đó dòng cuối cùng của hàm là lời gọi đến chính nó:

int FloorByTen(int num)
{
    if (num % 10 == 0)
        return num;
    else
        return FloorByTen(num-1);
}

Tuy nhiên, đây là một ví dụ khập khiễng, gần như vô nghĩa vì nó có thể dễ dàng được thay thế bằng cách lặp hiệu quả hơn. Rốt cuộc, đệ quy gặp phải chi phí gọi hàm, trong ví dụ trên có thể là đáng kể so với hoạt động bên trong chính hàm.

Vì vậy, toàn bộ lý do để thực hiện đệ quy thay vì lặp lại phải là tận dụng ngăn xếp cuộc gọi để thực hiện một số công việc thông minh. Ví dụ: nếu bạn gọi một hàm nhiều lần với các tham số khác nhau bên trong cùng một vòng lặp thì đó là một cách để thực hiện phân nhánh . Một ví dụ cổ điển là tam giác Sierpinski .

nhập mô tả hình ảnh ở đây

Bạn có thể vẽ một trong những thứ đó rất đơn giản với đệ quy, trong đó lệnh gọi phân nhánh theo 3 hướng:

private void BuildVertices(double x, double y, double len)
{
    if (len > 0.002)
    {
        mesh.Positions.Add(new Point3D(x, y + len, -len));
        mesh.Positions.Add(new Point3D(x - len, y - len, -len));
        mesh.Positions.Add(new Point3D(x + len, y - len, -len));
        len *= 0.5;
        BuildVertices(x, y + len, len);
        BuildVertices(x - len, y - len, len);
        BuildVertices(x + len, y - len, len);
    }
}

Nếu bạn cố gắng làm điều tương tự với sự lặp lại, tôi nghĩ bạn sẽ thấy cần nhiều mã hơn để hoàn thành.

Các trường hợp sử dụng phổ biến khác có thể bao gồm phân cấp chuyển ngang, ví dụ: trình thu thập thông tin trang web, so sánh thư mục, v.v.

Phần kết luận

Về mặt thực tế, đệ quy có ý nghĩa nhất bất cứ khi nào bạn cần phân nhánh lặp đi lặp lại.


27

Đệ quy là một phương pháp giải quyết vấn đề dựa trên tâm lý chia và chinh phục. Ý tưởng cơ bản là bạn lấy vấn đề ban đầu và chia nó thành các trường hợp nhỏ hơn (dễ giải quyết hơn) của chính nó, giải các trường hợp nhỏ hơn đó (thường bằng cách sử dụng lại cùng một thuật toán) và sau đó tập hợp chúng lại thành giải pháp cuối cùng.

Ví dụ chính tắc là một quy trình tạo Giai thừa của n. Giai thừa của n được tính bằng cách nhân tất cả các số từ 1 đến n. Một giải pháp lặp lại trong C # trông như thế này:

public int Fact(int n)
{
  int fact = 1;

  for( int i = 2; i <= n; i++)
  {
    fact = fact * i;
  }

  return fact;
}

Không có gì đáng ngạc nhiên về giải pháp lặp lại và nó sẽ có ý nghĩa đối với bất kỳ ai quen thuộc với C #.

Giải pháp đệ quy được tìm thấy bằng cách nhận ra rằng Giai thừa thứ n là n * Fact (n-1). Hay nói một cách khác, nếu bạn biết một số Giai thừa cụ thể là bao nhiêu, bạn có thể tính số tiếp theo. Đây là giải pháp đệ quy trong C #:

public int FactRec(int n)
{
  if( n < 2 )
  {
    return 1;
  }

  return n * FactRec( n - 1 );
}

Phần đầu tiên của hàm này được gọi là Trường hợp cơ sở (hoặc đôi khi là Điều khoản bảo vệ) và là thứ ngăn thuật toán chạy mãi mãi. Nó chỉ trả về giá trị 1 bất cứ khi nào hàm được gọi với giá trị 1 hoặc nhỏ hơn. Phần thứ hai thú vị hơn và được gọi là Bước đệ quy . Ở đây chúng ta gọi phương thức tương tự với một tham số được sửa đổi một chút (chúng ta giảm nó đi 1) và sau đó nhân kết quả với bản sao của n.

Khi lần đầu tiên gặp phải, điều này có thể hơi khó hiểu vì vậy bạn nên kiểm tra cách nó hoạt động khi chạy. Hãy tưởng tượng rằng chúng ta gọi là FactRec (5). Chúng tôi đi vào quy trình, không bị trường hợp cơ sở chọn và vì vậy chúng tôi kết thúc như thế này:

// In FactRec(5)
return 5 * FactRec( 5 - 1 );

// which is
return 5 * FactRec(4);

Nếu chúng ta nhập lại phương thức với tham số 4, một lần nữa chúng ta không bị chặn lại bởi mệnh đề bảo vệ và vì vậy chúng ta kết thúc ở:

// In FactRec(4)
return 4 * FactRec(3);

Nếu chúng tôi thay thế giá trị trả về này thành giá trị trả về ở trên, chúng tôi nhận được

// In FactRec(5)
return 5 * (4 * FactRec(3));

Điều này sẽ cung cấp cho bạn manh mối về cách đưa ra giải pháp cuối cùng, vì vậy chúng tôi sẽ theo dõi nhanh và hiển thị từng bước trên đường đi:

return 5 * (4 * FactRec(3));
return 5 * (4 * (3 * FactRec(2)));
return 5 * (4 * (3 * (2 * FactRec(1))));
return 5 * (4 * (3 * (2 * (1))));

Sự thay thế cuối cùng đó xảy ra khi trường hợp cơ sở được kích hoạt. Tại thời điểm này, chúng tôi có một công thức đại số đơn giản để giải quyết, nó tương đương trực tiếp với định nghĩa của thừa số ở vị trí đầu tiên.

Cần lưu ý rằng mọi cuộc gọi vào phương thức đều dẫn đến một trường hợp cơ sở được kích hoạt hoặc một cuộc gọi đến cùng một phương thức trong đó các tham số gần với trường hợp cơ sở hơn (thường được gọi là lời gọi đệ quy). Nếu không đúng như vậy thì phương thức sẽ chạy mãi mãi.


2
Giải thích tốt, nhưng tôi nghĩ điều quan trọng cần lưu ý rằng đây chỉ đơn giản là đệ quy đuôi và không mang lại lợi thế nào so với giải pháp lặp lại. Nó gần như bằng cùng một lượng mã và sẽ chạy chậm hơn do chi phí gọi hàm.
Steve Wortham

1
@SteveWortham: Đây không phải là đệ quy đuôi. Trong bước đệ quy, kết quả của FactRec()phải được nhân với ntrước khi trả về.
rvighne 19/07/2016

12

Đệ quy là giải quyết một vấn đề với một hàm gọi chính nó. Một ví dụ điển hình về điều này là một hàm giai thừa. Giai thừa là một bài toán trong đó giai thừa của 5, ví dụ, là 5 * 4 * 3 * 2 * 1. Hàm này giải quyết vấn đề này trong C # cho các số nguyên dương (không được kiểm tra - có thể có lỗi).

public int Factorial(int n)
{
    if (n <= 1)
        return 1;

    return n * Factorial(n - 1);
}

9

Đệ quy đề cập đến một phương pháp giải quyết một vấn đề bằng cách giải một phiên bản nhỏ hơn của vấn đề và sau đó sử dụng kết quả đó cộng với một số phép tính khác để hình thành câu trả lời cho vấn đề ban đầu. Thông thường, trong quá trình giải quyết phiên bản nhỏ hơn, phương pháp sẽ giải quyết một phiên bản nhỏ hơn của vấn đề, và cứ tiếp tục như vậy, cho đến khi nó đạt đến một "trường hợp cơ sở" không thể giải quyết được.

Ví dụ, để tính giai thừa cho một số X, người ta có thể biểu diễn nó dưới dạng X times the factorial of X-1. Do đó, phương thức "đệ quy" để tìm giai thừa của X-1, sau đó nhân bất cứ thứ gì mà nó nhận được Xđể đưa ra câu trả lời cuối cùng. Tất nhiên, để tìm giai thừa của X-1, trước tiên nó sẽ tính giai thừa của X-2, v.v. Trường hợp cơ sở sẽ là khi nào Xlà 0 hoặc 1, trong trường hợp đó nó biết sẽ trả về 1kể từ đó 0! = 1! = 1.


1
Tôi nghĩ rằng những gì bạn đang đề cập đến không phải là đệ quy mà là nguyên tắc thiết kế thuật toán <a href=" en.wikipedia.org/wiki/… và Conquer</p>. Hãy xem ví dụ tại <a href = " en.wikipedia. org / wiki / Ackermann_osystem "> Hàm Ackermans </a>.
Gabriel Ščerbák

2
Không, tôi không đề cập đến D&C. D&C ngụ ý rằng có 2 hoặc nhiều bài toán con tồn tại, riêng đệ quy thì không (ví dụ: ví dụ giai thừa được đưa ra ở đây không phải là D&C - nó hoàn toàn tuyến tính). D&C về cơ bản là một tập con của đệ quy.
Amber

3
Trích dẫn từ bài viết chính xác mà bạn đã liên kết: "Thuật toán chia và chinh phục hoạt động bằng cách chia nhỏ một cách đệ quy một vấn đề thành hai hoặc nhiều vấn đề con cùng loại (hoặc có liên quan)"
Amber

Tôi không nghĩ rằng đó là một lời giải thích tuyệt vời, vì nói đúng ra đệ quy không phải giải quyết vấn đề gì cả. Bạn chỉ có thể gọi cho mình (Và tràn).
UK-AL

Tôi đang sử dụng lời giải thích của bạn trong một bài báo tôi đang viết cho PHP Master mặc dù tôi không thể quy nó cho bạn. Mong bạn không phiền.
frostymarvelous

9

Hãy xem xét một vấn đề cũ, nổi tiếng :

Trong toán học, ước chung lớn nhất (gcd)… của hai hoặc nhiều số nguyên khác 0, là số nguyên dương lớn nhất chia các số không có dư.

Định nghĩa của gcd rất đơn giản:

định nghĩa gcd

trong đó mod là toán tử modulo (nghĩa là phần còn lại sau khi chia số nguyên).

Trong tiếng Anh, định nghĩa này nói ước chung lớn nhất của bất kỳ số nào và số 0 là số đó, và ước chung lớn nhất của hai số mn là ước chung lớn nhất của n và số còn lại sau khi chia m cho n .

Nếu bạn muốn biết tại sao điều này hoạt động, hãy xem bài viết trên Wikipedia về thuật toán Euclide .

Hãy tính gcd (10, 8) làm ví dụ. Mỗi bước bằng với bước ngay trước nó:

  1. gcd (10, 8)
  2. gcd (10, 10 mod 8)
  3. gcd (8, 2)
  4. gcd (8, 8 mod 2)
  5. gcd (2, 0)
  6. 2

Trong bước đầu tiên, 8 không bằng 0, vì vậy áp dụng phần thứ hai của định nghĩa. 10 mod 8 = 2 vì 8 chuyển thành 10 một lần với phần dư là 2. Ở bước 3, áp dụng phần thứ hai một lần nữa, nhưng lần này 8 mod 2 = 0 vì 2 chia 8 không có dư. Ở bước 5, đối số thứ hai là 0, vì vậy câu trả lời là 2.

Bạn có nhận thấy rằng gcd xuất hiện ở cả bên trái và bên phải của dấu bằng không? Một nhà toán học có thể nói định nghĩa này là đệ quy vì sự biểu hiện bạn đang xác định tái phát bên trong định nghĩa của nó.

Các định nghĩa đệ quy có xu hướng thanh lịch. Ví dụ, một định nghĩa đệ quy cho tổng của một danh sách là

sum l =
    if empty(l)
        return 0
    else
        return head(l) + sum(tail(l))

đâu headlà phần tử đầu tiên trong danh sách và taillà phần còn lại của danh sách. Lưu ý rằng sumlặp lại bên trong định nghĩa của nó ở cuối.

Có thể bạn muốn giá trị lớn nhất trong danh sách thay vào đó:

max l =
    if empty(l)
        error
    elsif length(l) = 1
        return head(l)
    else
        tailmax = max(tail(l))
        if head(l) > tailmax
            return head(l)
        else
            return tailmax

Bạn có thể định nghĩa phép nhân các số nguyên không âm một cách đệ quy để biến nó thành một loạt các phép cộng:

a * b =
    if b = 0
        return 0
    else
        return a + (a * (b - 1))

Nếu phần biến đổi phép nhân thành một loạt phép cộng không có ý nghĩa, hãy thử mở rộng một vài ví dụ đơn giản để xem nó hoạt động như thế nào.

Merge sort có một định nghĩa đệ quy đáng yêu:

sort(l) =
    if empty(l) or length(l) = 1
        return l
    else
        (left,right) = split l
        return merge(sort(left), sort(right))

Các định nghĩa đệ quy đều có sẵn nếu bạn biết những gì cần tìm. Lưu ý rằng tất cả các định nghĩa này đều có các trường hợp cơ sở rất đơn giản, ví dụ: gcd (m, 0) = m. Các trường hợp đệ quy xoay quanh vấn đề để đi đến câu trả lời dễ dàng.

Với sự hiểu biết này, bây giờ bạn có thể đánh giá cao các thuật toán khác trong bài viết của Wikipedia về đệ quy !


8
  1. Một hàm tự gọi
  2. Khi một chức năng có thể được (dễ dàng) phân rã thành một hoạt động đơn giản cộng với chức năng tương tự trên một số phần nhỏ hơn của vấn đề. Tôi nên nói, đúng hơn, điều này làm cho nó trở thành một ứng cử viên tốt cho đệ quy.
  3. Họ làm!

Ví dụ chính tắc là giai thừa trông giống như sau:

int fact(int a) 
{
  if(a==1)
    return 1;

  return a*fact(a-1);
}

Nói chung, đệ quy không nhất thiết phải nhanh (chi phí cuộc gọi hàm có xu hướng cao vì các hàm đệ quy có xu hướng nhỏ, xem ở trên) và có thể gặp một số vấn đề (tràn ngăn xếp bất kỳ ai?). Một số người nói rằng họ có xu hướng khó 'đúng' trong những trường hợp không phải tầm thường nhưng tôi không thực sự quan tâm đến điều đó. Trong một số tình huống, đệ quy có ý nghĩa nhất và là cách tốt nhất và rõ ràng nhất để viết một hàm cụ thể. Cần lưu ý rằng một số ngôn ngữ ưu tiên các giải pháp đệ quy và tối ưu hóa chúng nhiều hơn (LISP có ý kiến).


6

Một hàm đệ quy là một hàm gọi chính nó. Lý do phổ biến nhất mà tôi tìm thấy để sử dụng nó là đi ngang qua một cấu trúc cây. Ví dụ: nếu tôi có TreeView với các hộp kiểm (nghĩ rằng cài đặt chương trình mới, trang "chọn tính năng để cài đặt"), tôi có thể muốn có nút "kiểm tra tất cả" sẽ giống như sau (mã giả):

function cmdCheckAllClick {
    checkRecursively(TreeView1.RootNode);
}

function checkRecursively(Node n) {
    n.Checked = True;
    foreach ( n.Children as child ) {
        checkRecursively(child);
    }
}

Vì vậy, bạn có thể thấy rằng checkRecursently đầu tiên kiểm tra nút mà nó được truyền qua, sau đó tự gọi cho từng nút con của nút đó.

Bạn cần phải cẩn thận một chút với đệ quy. Nếu bạn vướng vào vòng lặp đệ quy vô hạn, bạn sẽ nhận được ngoại lệ Stack Overflow :)

Tôi không thể nghĩ ra lý do tại sao mọi người không nên sử dụng nó, khi thích hợp. Nó hữu ích trong một số trường hợp, và không hữu ích trong những trường hợp khác.

Tôi nghĩ rằng bởi vì đó là một kỹ thuật thú vị, một số lập trình viên có lẽ sẽ sử dụng nó thường xuyên hơn họ nên mà không có sự biện minh thực sự. Điều này đã tạo cho đệ quy một tên xấu trong một số vòng kết nối.


5

Đệ quy là một biểu thức tham chiếu trực tiếp hoặc gián tiếp đến chính nó.

Hãy xem xét các từ viết tắt đệ quy như một ví dụ đơn giản:

  • GNU là viết tắt của GNU's Not Unix
  • PHP là viết tắt của PHP: Hypertext Preprocessor
  • YAML là viết tắt của YAML Ain’t Markup Language
  • WINE là viết tắt của Wine Is Not an Emulator
  • VISA là viết tắt của Visa International Service Association

Các ví dụ khác trên Wikipedia


4

Đệ quy hoạt động tốt nhất với cái mà tôi thích gọi là "các vấn đề fractal", nơi bạn đang giải quyết một việc lớn được tạo ra từ các phiên bản nhỏ hơn của việc lớn đó, mỗi phiên bản thậm chí là phiên bản nhỏ hơn của việc lớn, v.v. Nếu bạn đã từng phải đi qua hoặc tìm kiếm thông qua một cái gì đó như một cái cây hoặc các cấu trúc giống hệt nhau được lồng vào nhau, bạn đã gặp một vấn đề có thể là một ứng cử viên tốt cho đệ quy.

Mọi người tránh đệ quy vì một số lý do:

  1. Hầu hết mọi người (bao gồm cả tôi) đều không thích lập trình theo thủ tục hoặc lập trình hướng đối tượng thay vì lập trình hàm. Đối với những người như vậy, phương pháp lặp lại (thường sử dụng các vòng lặp) cảm thấy tự nhiên hơn.

  2. Những người trong chúng ta, những người đã học lập trình theo thủ tục hoặc lập trình hướng đối tượng thường được yêu cầu tránh đệ quy vì nó dễ bị lỗi.

  3. Chúng tôi thường nói rằng đệ quy chậm. Việc gọi và quay lại từ một thói quen lặp đi lặp lại liên quan đến nhiều lần đẩy và bật ngăn xếp, chậm hơn so với lặp lại. Tôi nghĩ rằng một số ngôn ngữ xử lý điều này tốt hơn những ngôn ngữ khác và những ngôn ngữ đó rất có thể không phải là những ngôn ngữ mà mô hình thống trị là thủ tục hoặc hướng đối tượng.

  4. Đối với ít nhất một vài ngôn ngữ lập trình mà tôi đã sử dụng, tôi nhớ đã nghe các khuyến nghị không sử dụng đệ quy nếu nó vượt quá một độ sâu nhất định vì ngăn xếp của nó không sâu như vậy.


4

Một câu lệnh đệ quy là một câu lệnh trong đó bạn xác định quá trình phải làm gì tiếp theo dưới dạng kết hợp của các đầu vào và những gì bạn đã làm.

Ví dụ, lấy giai thừa:

factorial(6) = 6*5*4*3*2*1

Nhưng dễ dàng nhận thấy giai thừa (6) cũng là:

6 * factorial(5) = 6*(5*4*3*2*1).

Vì vậy, nói chung:

factorial(n) = n*factorial(n-1)

Tất nhiên, điều khó khăn về đệ quy là nếu bạn muốn xác định mọi thứ về những gì bạn đã làm, cần phải có một số điểm để bắt đầu.

Trong ví dụ này, chúng ta chỉ tạo một trường hợp đặc biệt bằng cách xác định giai thừa (1) = 1.

Bây giờ chúng ta thấy nó từ dưới lên:

factorial(6) = 6*factorial(5)
                   = 6*5*factorial(4)
                   = 6*5*4*factorial(3) = 6*5*4*3*factorial(2) = 6*5*4*3*2*factorial(1) = 6*5*4*3*2*1

Vì chúng ta đã xác định giai thừa (1) = 1, chúng ta đạt đến "đáy".

Nói chung, thủ tục đệ quy có hai phần:

1) Phần đệ quy, xác định một số thủ tục về đầu vào mới kết hợp với những gì bạn đã "thực hiện" thông qua cùng một thủ tục. (tức là factorial(n) = n*factorial(n-1))

2) Một phần cơ sở, đảm bảo rằng quá trình không lặp lại mãi mãi bằng cách cho nó một số nơi để bắt đầu (tức là factorial(1) = 1 )

Thoạt đầu, bạn có thể hơi bối rối khi tìm hiểu kỹ, nhưng chỉ cần xem xét một loạt các ví dụ và tất cả sẽ kết hợp lại với nhau. Nếu bạn muốn hiểu sâu hơn về khái niệm này, hãy nghiên cứu quy nạp toán học. Ngoài ra, hãy lưu ý rằng một số ngôn ngữ tối ưu hóa cho các cuộc gọi đệ quy trong khi những ngôn ngữ khác thì không. Khá dễ dàng để tạo ra các hàm đệ quy cực kỳ chậm nếu bạn không cẩn thận, nhưng cũng có những kỹ thuật để làm cho chúng hoạt động hiệu quả trong hầu hết các trường hợp.

Hi vọng điêu nay co ich...


4

Tôi thích định nghĩa này:
Trong đệ quy, một quy trình tự giải quyết một phần nhỏ của vấn đề, chia vấn đề thành các phần nhỏ hơn, và sau đó gọi chính nó để giải từng phần nhỏ hơn.

Tôi cũng thích cuộc thảo luận của Steve McConnells về đệ quy trong Code Complete, nơi ông chỉ trích các ví dụ được sử dụng trong sách Khoa học Máy tính về Đệ quy.

Không sử dụng đệ quy cho giai thừa hoặc số Fibonacci

Một vấn đề với sách giáo khoa khoa học máy tính là chúng đưa ra những ví dụ ngớ ngẩn về phép đệ quy. Các ví dụ điển hình là tính toán giai thừa hoặc tính toán chuỗi Fibonacci. Đệ quy là một công cụ mạnh mẽ và thực sự ngớ ngẩn khi sử dụng nó trong một trong hai trường hợp đó. Nếu một lập trình viên làm việc cho tôi sử dụng đệ quy để tính giai thừa, tôi sẽ thuê một người khác.

Tôi nghĩ đây là một điểm rất thú vị để nêu ra và có thể là lý do tại sao đệ quy thường bị hiểu nhầm.

CHỈNH SỬA: Đây không phải là câu trả lời của Dav - Tôi đã không thấy câu trả lời đó khi tôi đăng bài này


6
Hầu hết lý do tại sao chuỗi thừa kế hoặc chuỗi fibonacci được sử dụng làm ví dụ là vì chúng là các mục phổ biến được xác định theo cách đệ quy và do đó chúng tự cho mình các ví dụ về đệ quy để tính toán chúng một cách tự nhiên - ngay cả khi đó thực sự không phải là phương pháp tốt nhất theo quan điểm CS.
Amber

Tôi đồng ý - tôi chỉ thấy khi đọc cuốn sách rằng đó là một điểm thú vị cần nêu ra ở giữa phần về đệ quy
Robben_Ford_Fan_boy

4

1.) Một phương thức là đệ quy nếu nó có thể gọi chính nó; hoặc trực tiếp:

void f() {
   ... f() ... 
}

hoặc gián tiếp:

void f() {
    ... g() ...
}

void g() {
   ... f() ...
}

2.) Khi nào sử dụng đệ quy

Q: Does using recursion usually make your code faster? 
A: No.
Q: Does using recursion usually use less memory? 
A: No.
Q: Then why use recursion? 
A: It sometimes makes your code much simpler!

3.) Người ta chỉ sử dụng đệ quy khi viết mã lặp rất phức tạp. Ví dụ, các kỹ thuật duyệt cây như đặt hàng trước, đặt hàng sau có thể được thực hiện cả lặp lại và đệ quy. Nhưng thông thường chúng ta sử dụng đệ quy vì tính đơn giản của nó.


Điều gì về việc giảm độ phức tạp khi phân chia và chinh phục các perfs liên quan?
mfrachet

4

Đây là một ví dụ đơn giản: có bao nhiêu phần tử trong một tập hợp. (có nhiều cách tốt hơn để đếm mọi thứ, nhưng đây là một ví dụ đệ quy đơn giản hay.)

Đầu tiên, chúng ta cần hai quy tắc:

  1. nếu tập hợp trống, số lượng mục trong tập hợp là 0 (duh!).
  2. nếu tập hợp không trống, số đếm là một cộng với số mục trong tập hợp sau khi một mục được xóa.

Giả sử bạn có một tập hợp như sau: [xxx]. hãy đếm xem có bao nhiêu mục.

  1. tập hợp là [xxx] không trống, vì vậy chúng tôi áp dụng quy tắc 2. số mục là một cộng với số mục trong [xx] (tức là chúng tôi đã xóa một mục).
  2. tập hợp là [xx], vì vậy chúng tôi áp dụng lại quy tắc 2: một + số mục trong [x].
  3. tập hợp là [x], vẫn phù hợp với quy tắc 2: một + số mục trong [].
  4. Bây giờ tập hợp là [], phù hợp với quy tắc 1: số đếm bằng 0!
  5. Bây giờ chúng ta đã biết câu trả lời ở bước 4 (0), chúng ta có thể giải quyết bước 3 (1 + 0)
  6. Tương tự như vậy, bây giờ chúng ta đã biết câu trả lời ở bước 3 (1), chúng ta có thể giải quyết bước 2 (1 + 1)
  7. Và cuối cùng bây giờ chúng ta đã biết câu trả lời ở bước 2 (2), chúng ta có thể giải quyết bước 1 (1 + 2) và nhận được số lượng các mục trong [xxx], là 3. Hoan hô!

Chúng tôi có thể trình bày điều này là:

count of [x x x] = 1 + count of [x x]
                 = 1 + (1 + count of [x])
                 = 1 + (1 + (1 + count of []))
                 = 1 + (1 + (1 + 0)))
                 = 1 + (1 + (1))
                 = 1 + (2)
                 = 3

Khi áp dụng một giải pháp đệ quy, bạn thường có ít nhất 2 quy tắc:

  • cơ sở, trường hợp đơn giản cho biết điều gì sẽ xảy ra khi bạn đã "sử dụng hết" tất cả dữ liệu của mình. Đây thường là một số biến thể của "nếu bạn không có dữ liệu để xử lý, câu trả lời của bạn là X"
  • quy tắc đệ quy, cho biết điều gì sẽ xảy ra nếu bạn vẫn có dữ liệu. Đây thường là một số loại quy tắc có nội dung "làm điều gì đó để làm cho tập dữ liệu của bạn nhỏ hơn và áp dụng lại các quy tắc của bạn cho tập dữ liệu nhỏ hơn."

Nếu chúng tôi dịch phần trên sang mã giả, chúng tôi nhận được:

numberOfItems(set)
    if set is empty
        return 0
    else
        remove 1 item from set
        return 1 + numberOfItems(set)

Có rất nhiều ví dụ hữu ích hơn (ví dụ như đi ngang qua một cái cây) mà tôi chắc rằng những người khác sẽ đề cập đến.


3

Đó là một định nghĩa khá tốt mà bạn có. Và wikipedia cũng có một định nghĩa tốt. Vì vậy, tôi sẽ thêm một định nghĩa khác (có thể tệ hơn) cho bạn.

Khi mọi người đề cập đến "đệ quy", họ thường nói về một hàm mà họ đã viết, hàm này gọi chính nó nhiều lần cho đến khi hoàn thành công việc của nó. Đệ quy có thể hữu ích khi duyệt qua cấu trúc phân cấp trong cấu trúc dữ liệu.


3

Ví dụ: Định nghĩa đệ quy về cầu thang là: Cầu thang bao gồm: - một bậc duy nhất và một bậc cầu thang (đệ quy) - hoặc chỉ một bậc duy nhất (đoạn cuối)


2

Để lặp lại một vấn đề đã được giải quyết: không làm gì cả, bạn đã hoàn thành.
Để lặp lại một vấn đề mở: thực hiện bước tiếp theo, sau đó lặp lại phần còn lại.


2

Bằng tiếng Anh đơn giản: Giả sử bạn có thể làm 3 điều:

  1. Lấy một quả táo
  2. Viết ra các điểm kiểm đếm
  3. Đếm điểm kiểm đếm

Bạn có rất nhiều quả táo ở phía trước trên bàn và bạn muốn biết có bao nhiêu quả táo.

start
  Is the table empty?
  yes: Count the tally marks and cheer like it's your birthday!
  no:  Take 1 apple and put it aside
       Write down a tally mark
       goto start

Quá trình lặp lại cùng một thứ cho đến khi bạn hoàn thành được gọi là đệ quy.

Tôi hy vọng đây là câu trả lời "tiếng Anh đơn giản" mà bạn đang tìm kiếm!


1
Chờ đã, tôi có rất nhiều điểm kiểm đếm trước mặt tôi trên bàn, và bây giờ tôi muốn biết có bao nhiêu điểm kiểm đếm. Tôi có thể bằng cách nào đó sử dụng táo cho việc này không?
Christoffer Hammarström

Nếu bạn lấy một quả táo trên mặt đất (khi bạn đã đặt chúng ở đó trong suốt quá trình) và đặt nó trên bàn mỗi khi bạn cào một dấu kiểm đếm của danh sách cho đến khi không còn dấu kiểm đếm nào, tôi khá chắc chắn rằng bạn kết thúc với số lượng táo trên bàn bằng với số điểm kiểm đếm mà bạn có. Bây giờ chỉ cần đếm những quả táo để thành công ngay lập tức! (lưu ý: quá trình này không còn là đệ quy nữa mà là một vòng lặp vô hạn)
Bastiaan Linders

2

Một hàm đệ quy là một hàm chứa lời gọi đến chính nó. Một cấu trúc đệ quy là một cấu trúc chứa một thể hiện của chính nó. Bạn có thể kết hợp cả hai như một lớp đệ quy. Phần quan trọng của một mục đệ quy là nó chứa một thể hiện / lời gọi của chính nó.

Xét hai gương đối diện nhau. Chúng tôi đã thấy hiệu ứng vô cực gọn gàng mà họ tạo ra. Mỗi phản xạ là một thể hiện của gương, được chứa trong một thể hiện khác của gương, v.v. Gương có chứa phản xạ của chính nó là đệ quy.

Một cây tìm kiếm nhị phân là một ví dụ lập trình tốt của đệ quy. Cấu trúc là đệ quy với mỗi Node chứa 2 thể hiện của một Node. Các hàm hoạt động trên cây tìm kiếm nhị phân cũng là hàm đệ quy.


2

Đây là một câu hỏi cũ, nhưng tôi muốn thêm câu trả lời từ quan điểm hậu cần (nghĩa là không phải từ quan điểm tính đúng thuật toán hoặc quan điểm hiệu suất).

Tôi sử dụng Java cho công việc và Java không hỗ trợ hàm lồng nhau. Như vậy, nếu tôi muốn thực hiện đệ quy, tôi có thể phải xác định một hàm bên ngoài (chỉ tồn tại vì mã của tôi chống lại quy tắc quan liêu của Java) hoặc tôi có thể phải cấu trúc lại mã hoàn toàn (điều mà tôi thực sự không thích làm).

Do đó, tôi thường tránh đệ quy và thay vào đó sử dụng hoạt động ngăn xếp, vì bản thân đệ quy về cơ bản là một hoạt động ngăn xếp.


1

Bạn muốn sử dụng nó bất cứ lúc nào bạn có cấu trúc cây. Nó rất hữu ích trong việc đọc XML.


1

Đệ quy áp dụng cho lập trình về cơ bản là gọi một hàm từ bên trong định nghĩa của chính nó (bên trong chính nó), với các tham số khác nhau để hoàn thành một tác vụ.


1

"Nếu tôi có một cái búa, hãy biến mọi thứ giống như một cái đinh."

Đệ quy là một chiến lược giải quyết vấn đề cho các vấn đề lớn , trong đó ở mỗi bước, mỗi bước chỉ cần "biến 2 việc nhỏ thành một việc lớn hơn" với cùng một chiếc búa.

Thí dụ

Giả sử bàn làm việc của bạn được bao phủ bởi 1024 tờ giấy lộn xộn vô tổ chức. Làm thế nào để bạn tạo một chồng giấy gọn gàng, sạch sẽ khỏi đống hỗn độn, bằng cách sử dụng đệ quy?

  1. Chia: Trải tất cả các tờ giấy ra, vì vậy bạn chỉ có một tờ trong mỗi "chồng".
  2. Chinh phục:
    1. Đi xung quanh, đặt mỗi tờ lên trên một tờ khác. Bây giờ bạn có 2 ngăn xếp.
    2. Đi xung quanh, đặt mỗi chồng 2 lên trên 2 chồng khác. Bây giờ bạn có 4 ngăn xếp.
    3. Đi xung quanh, đặt từng chồng 4 lên trên 4 ngăn xếp khác. Bây giờ bạn có 8 ngăn xếp.
    4. ... tiếp tục ...
    5. Bây giờ bạn có một chồng 1024 tờ lớn!

Lưu ý rằng điều này khá trực quan, ngoài việc đếm mọi thứ (không cần thiết lắm). Trên thực tế, bạn có thể không xuống hết các ngăn xếp 1 tờ, nhưng bạn có thể và nó sẽ vẫn hoạt động. Phần quan trọng là cái búa: Với cánh tay của bạn, bạn luôn có thể đặt chồng này lên chồng kia để tạo thành một chồng lớn hơn và không quan trọng (trong lý do) chồng lớn bao nhiêu.


6
Bạn đang mô tả sự phân chia và chinh phục. Mặc dù đây là một ví dụ về đệ quy, nhưng nó không phải là ví dụ duy nhất.
Konrad Rudolph

Tốt rồi. Tôi không cố gắng nắm bắt [thế giới đệ quy] [1] trong một câu, ở đây. Tôi muốn một lời giải thích trực quan. [1]: facebook.com/pages/Recursion-Fairy/269711978049
Andres Jaan Tack

1

Đệ quy là quá trình mà một phương thức tự gọi để có thể thực hiện một tác vụ nhất định. Nó làm giảm sự dư thừa của mã. Hầu hết các hàm hoặc phương thức đệ quy phải có một condifiton để ngắt lời gọi đệ quy, tức là ngăn nó tự gọi nếu một điều kiện được đáp ứng - điều này ngăn cản việc tạo ra một vòng lặp vô hạn. Không phải tất cả các hàm đều phù hợp để sử dụng một cách đệ quy.


1

Này, xin lỗi nếu ý kiến ​​của tôi đồng ý với ai đó, tôi chỉ đang cố gắng giải thích đệ quy bằng tiếng Anh đơn giản.

giả sử bạn có ba người quản lý - Jack, John và Morgan. Jack quản lý 2 lập trình viên, John - 3 tuổi và Morgan - 5. Bạn sẽ chia cho mỗi người quản lý 300 đô la và muốn biết chi phí đó là bao nhiêu. Câu trả lời là hiển nhiên - nhưng nếu 2 nhân viên của Morgan cũng là quản lý thì sao?

Ở đây có đệ quy. bạn bắt đầu từ đầu cấu trúc phân cấp. chi phí tổng kết là 0 $. bạn bắt đầu với Jack, Sau đó kiểm tra xem anh ta có người quản lý nào là nhân viên không. nếu bạn tìm thấy bất kỳ người nào trong số họ, hãy kiểm tra xem họ có người quản lý nào là nhân viên không, v.v. Thêm 300 đô la vào chi phí tổng kết mỗi khi bạn tìm được người quản lý. khi bạn kết thúc với Jack, hãy đến John, các nhân viên của anh ấy và sau đó là Morgan.

Bạn sẽ không bao giờ biết, bạn sẽ đi bao nhiêu chu kỳ trước khi nhận được câu trả lời, mặc dù bạn biết bạn có bao nhiêu người quản lý và bạn có thể chi bao nhiêu Ngân sách.

Đệ quy là một cái cây, có cành và lá, lần lượt được gọi là cha mẹ và con cái. Khi bạn sử dụng thuật toán đệ quy, bạn ít nhiều có ý thức đang xây dựng một cây từ dữ liệu.


1

Trong tiếng Anh đơn giản, đệ quy có nghĩa là lặp đi lặp lại nhiều lần.

Trong lập trình, một ví dụ là gọi hàm trong chính nó.

Hãy xem ví dụ sau về tính giai thừa của một số:

public int fact(int n)
{
    if (n==0) return 1;
    else return n*fact(n-1)
}

1
Trong tiếng Anh đơn giản, lặp đi lặp lại một điều gì đó được gọi là lặp lại.
toon81

1

Bất kỳ thuật toán nào cũng hiển thị đệ quy cấu trúc trên một kiểu dữ liệu nếu về cơ bản bao gồm một câu lệnh chuyển đổi với một trường hợp cho mỗi trường hợp của kiểu dữ liệu.

ví dụ, khi bạn đang làm việc trên một loại

  tree = null 
       | leaf(value:integer) 
       | node(left: tree, right:tree)

một thuật toán đệ quy cấu trúc sẽ có dạng

 function computeSomething(x : tree) =
   if x is null: base case
   if x is leaf: do something with x.value
   if x is node: do something with x.left,
                 do something with x.right,
                 combine the results

đây thực sự là cách rõ ràng nhất để viết bất kỳ thuật toán nào hoạt động trên cấu trúc dữ liệu.

bây giờ, khi bạn nhìn vào các số nguyên (tốt, các số tự nhiên) được định nghĩa bằng cách sử dụng tiên đề Peano

 integer = 0 | succ(integer)

bạn thấy rằng một thuật toán đệ quy cấu trúc trên số nguyên trông như thế này

 function computeSomething(x : integer) =
   if x is 0 : base case
   if x is succ(prev) : do something with prev

hàm giai thừa quá nổi tiếng là về ví dụ đơn giản nhất của dạng này.


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.