Hiểu cách hoạt động của các hàm đệ quy


115

Như tiêu đề đã giải thích, tôi có một câu hỏi lập trình rất cơ bản mà tôi chưa thể tìm hiểu. Lọc ra tất cả (cực kỳ thông minh) "Để hiểu đệ quy, trước tiên bạn phải hiểu đệ quy." trả lời từ các chủ đề trực tuyến khác nhau Tôi vẫn chưa hiểu lắm.

Hiểu rằng khi đối mặt với việc không biết những gì chúng ta không biết, chúng ta có thể có xu hướng đặt câu hỏi sai hoặc đặt câu hỏi đúng sai. Tôi sẽ chia sẻ những gì tôi "nghĩ" câu hỏi của tôi với hy vọng rằng ai đó có cùng quan điểm có thể chia sẻ một số một chút kiến ​​thức sẽ giúp bật bóng đèn đệ quy cho tôi!

Đây là hàm (cú pháp được viết bằng Swift):

func sumInts(a: Int, b: Int) -> Int {
    if (a > b) {
        return 0
    } else {
        return a + sumInts(a: a + 1, b: b)
    }
}

Chúng tôi sẽ sử dụng 2 và 5 làm đối số của mình:

println(sumInts(a: 2, b: 5))

Rõ ràng câu trả lời là 14. Nhưng tôi không rõ về cách đạt được giá trị đó.

Đây là 2 lần treo máy của tôi:

  1. Hàm được gọi đệ quy cho đến khi một điều kiện được đáp ứng. Điều kiện đó là a> b. Khi điều kiện này được đáp ứng, trả về 0. Thoạt nhìn, tôi mong đợi giá trị trả về là 0, rõ ràng là không chính xác.

  2. Việc in ra giá trị của 'a' trên mỗi lần lặp sẽ mang lại giá trị mà tôi mong đợi: 2, 3, 4, 5 (tại điểm 5 + 1> b đáp ứng điều kiện đầu tiên: a> b) nhưng tôi vẫn không ' t xem giá trị của 14 đạt được như thế nào.

Suy nghĩ đầu tiên của tôi là điều gì đó tương tự như sau đang xảy ra một cách kỳ diệu:

var answer = a;
answer += a+1 until a > b;
return answer;   

Vì vậy, loại trừ ma thuật, tôi chỉ không nhận được thứ gì đó. Tôi muốn hiểu những gì đang xảy ra hơn là chỉ mặc nhiên.

Nếu ai đó có thể vui lòng giải thích những gì về mặt kỹ thuật xảy ra trong loại chức năng này và tại sao kết quả không phải là 0 và làm thế nào, cuối cùng a + sumInts(a: a + 1, b: b) = 14, tôi sẽ mãi mãi mắc nợ bạn.


7
Đệ quy là một trong những khái niệm lập trình dễ hiểu hơn về mặt toán học so với mã; có một định nghĩa hay ở đây
blgt

5
LearnYouARecursion, bộ bài tập hoàn chỉnh từ giáo sư đẳng cấp thế giới!
recursion.ninja

15
Tôi chỉ để thúc giục bạn gõ "Recursion" trong hộp tìm kiếm của Google. Một trong những quả trứng Phục sinh. Tôi sẽ không làm hỏng bất ngờ cho bạn.
Floris


Câu trả lời:


107

Tôi nghĩ rằng sự nhầm lẫn bắt nguồn từ việc nghĩ về nó như là "cùng một chức năng" được gọi nhiều lần. Nếu bạn nghĩ nó là "nhiều bản sao của cùng một hàm được gọi", thì có thể rõ ràng hơn:

Chỉ một bản sao của hàm trả về 0 và nó không phải là bản đầu tiên (là bản cuối cùng). Vì vậy, kết quả của việc gọi đầu tiên không phải là 0.

Đối với chút nhầm lẫn thứ hai, tôi nghĩ sẽ dễ dàng hơn để đánh vần đệ quy bằng tiếng Anh. Đọc dòng này:

return a + sumInts(a + 1, b: b)

là "trả về giá trị của 'a' cộng (giá trị trả về của một bản sao khác của hàm, là giá trị của bản sao của 'a' cộng (giá trị trả về của bản sao khác của hàm, là giá trị của bản sao thứ hai của ' a 'plus (... ", với mỗi bản sao của hàm sinh ra một bản sao mới của chính nó với giá trị tăng thêm 1, cho đến khi điều kiện a> b được đáp ứng.

Vào thời điểm bạn đạt đến điều kiện a> b là true, bạn có một chồng dài (có thể tùy ý) các bản sao của hàm đang được chạy, tất cả đều đang chờ kết quả của bản sao tiếp theo để tìm ra chúng. nên thêm vào 'a'.

(sửa: ngoài ra, một điều cần lưu ý là chồng các bản sao của hàm mà tôi đề cập là một thứ thực chiếm bộ nhớ thực và sẽ làm hỏng chương trình của bạn nếu nó quá lớn. Trình biên dịch có thể tối ưu hóa nó trong một số trường hợp, nhưng việc cạn kiệt không gian ngăn xếp là một hạn chế đáng kể và đáng tiếc của các hàm đệ quy trong nhiều ngôn ngữ)


7
Catfish_Man: Tôi nghĩ bạn đã đóng đinh nó! Nghĩ về nó như một số "bản sao" của cùng một chức năng hoàn toàn có ý nghĩa. Tôi vẫn đang quấn lấy nó nhưng tôi nghĩ bạn đã đưa tôi đi đúng con đường! Cảm ơn bạn đã dành thời gian trong ngày bận rộn của mình để giúp đỡ một lập trình viên đồng nghiệp! Tôi sẽ đánh dấu câu trả lời của bạn là câu trả lời chính xác. Có một ngày tuyệt vời!
Jason Elwood

13
Đây là một phép loại suy tốt - mặc dù hãy cẩn thận đừng hiểu nó quá theo nghĩa đen vì mỗi "bản sao" thực sự là cùng một mã. Điều khác biệt đối với mỗi bản sao là tất cả dữ liệu mà nó đang hoạt động.
Tim B

2
Tôi không quá hài lòng với việc coi nó như một bản sao. Tôi thấy rằng một lời giải thích trực quan hơn là phân biệt bản thân hàm (mã, chức năng của nó) và lệnh gọi hàm (khởi tạo hàm đó) mà khung ngăn xếp / ngữ cảnh thực thi được liên kết với nhau. Hàm không sở hữu các biến cục bộ của nó, chúng được khởi tạo khi hàm được gọi (được gọi). Nhưng tôi đoán điều này sẽ làm như một phần giới thiệu về đệ quy
Thomas

5
Thuật ngữ chính xác là có một số lệnh gọi của hàm. Mỗi lời gọi có các trường hợp riêng của các biến ab.
Theodore Norvell

6
Có, có một lượng chính xác đáng kể có thể được thêm vào câu trả lời này. Tôi cố tình bỏ qua sự phân biệt giữa "các trường hợp của một hàm" và "bản ghi kích hoạt của các lệnh gọi của một hàm duy nhất", bởi vì đó là tải trọng khái niệm bổ sung không thực sự giúp hiểu vấn đề. Nó giúp hiểu các vấn đề khác , vì vậy nó vẫn là thông tin hữu ích, chỉ ở những nơi khác. Những nhận xét này có vẻ như là một nơi tốt cho nó :)
Catfish_Man

130

1. Hàm được gọi đệ quy cho đến khi một điều kiện được đáp ứng. Đó là điều kiện a > b. Khi điều kiện này được đáp ứng, trả về 0. Thoạt nhìn, tôi mong đợi giá trị trả về là 0, rõ ràng là không chính xác.

Đây là những gì máy tính máy tính sumInts(2,5)sẽ nghĩ nếu nó có thể:

I want to compute sumInts(2, 5)
for this, I need to compute sumInts(3, 5)
and add 2 to the result.
  I want to compute sumInts(3, 5)
  for this, I need to compute sumInts(4, 5)
  and add 3 to the result.
    I want to compute sumInts(4, 5)
    for this, I need to compute sumInts(5, 5)
    and add 4 to the result.
      I want to compute sumInts(5, 5)
      for this, I need to compute sumInts(6, 5)
      and add 5 to the result.
        I want to compute sumInts(6, 5)
        since 6 > 5, this is zero.
      The computation yielded 0, therefore I shall return 5 = 5 + 0.
    The computation yielded 5, therefore I shall return 9 = 4 + 5.
  The computation yielded 9, therefore I shall return 12 = 3 + 9.
The computation yielded 12, therefore I shall return 14 = 2 + 12.

Như bạn thấy, một số lệnh gọi đến hàm sumIntsthực sự trả về 0 tuy nhiên đây không phải là giá trị cuối cùng vì máy tính vẫn phải thêm 5 vào 0 đó, sau đó 4 vào kết quả, rồi 3, rồi 2, như được mô tả trong bốn câu cuối cùng của suy nghĩ của máy tính của chúng tôi. Lưu ý rằng trong đệ quy, máy tính không chỉ phải tính toán lời gọi đệ quy mà nó còn phải nhớ những việc cần làm với giá trị được trả về bởi lời gọi đệ quy. Có một vùng đặc biệt của bộ nhớ máy tính được gọi là ngăn xếp nơi loại thông tin này được lưu, không gian này bị giới hạn và các chức năng quá đệ quy có thể làm cạn kiệt ngăn xếp: đây là tràn ngăn xếp. đặt tên cho trang web được yêu thích nhất của chúng ta.

Tuyên bố của bạn dường như tạo ra giả định ngầm rằng máy tính quên những gì nó đã xảy ra khi thực hiện một cuộc gọi đệ quy, nhưng không phải vậy, đây là lý do tại sao kết luận của bạn không phù hợp với quan sát của bạn.

2. In ra giá trị của 'a' trên mỗi lần lặp sẽ mang lại giá trị mà tôi mong đợi: 2, 3, 4, 5 (tại điểm 5 + 1> b đáp ứng điều kiện đầu tiên: a> b) nhưng tôi vẫn không thấy giá trị của 14 đạt được như thế nào.

Điều này là do giá trị trả về không phải là achính nó mà là tổng của giá trị của avà giá trị được trả về bởi lời gọi đệ quy.


3
Cảm ơn đã dành thời gian viết câu trả lời tuyệt vời này Michael! +1!
Jason Elwood

9
@JasonElwood Có lẽ sẽ hữu ích nếu bạn sửa đổi sumIntsđể nó thực sự viết ra “suy nghĩ của máy tính”. Một khi bạn đã viết một tay các hàm như vậy, bạn có thể sẽ “hiểu được”!
Michael Le Barbier Grünewald

4
Đây là một câu trả lời hay, mặc dù tôi lưu ý rằng không có yêu cầu nào về việc kích hoạt chức năng diễn ra trên cấu trúc dữ liệu được gọi là "ngăn xếp". Đệ quy có thể được thực hiện bằng kiểu truyền tiếp tục, trong trường hợp này không có ngăn xếp nào cả. Ngăn xếp chỉ là một - đặc biệt hiệu quả, và do đó được sử dụng phổ biến - là sự cải tiến khái niệm về sự tiếp tục.
Eric Lippert

1
@EricLippert Trong khi các kỹ thuật sử dụng để thực hiện đệ quy là một chủ đề thú vị cho mỗi gia nhập , tôi không chắc chắn nếu nó sẽ hữu ích cho các OP-ai muốn hiểu “làm thế nào nó hoạt động” -để được tiếp xúc với sự đa dạng của các cơ chế sử dụng. Trong khi tiếp tục đi theo phong cách hoặc các ngôn ngữ dựa mở rộng (ví dụ như TeX và m4) không được bản chất khó khăn hơn so với mô hình lập trình phổ biến hơn, tôi sẽ không vi phạm bất cứ ai bằng cách gắn nhãn những “kỳ lạ” và một chút lời nói dối trắng như “nó luôn luôn xảy ra trên các ngăn xếp” nên giúp OP hiểu khái niệm. (Và một loại chồng luôn là tham gia.)
Michael Lê Barbier Grunewald

1
Phải có một số cách để phần mềm ghi nhớ những gì nó đang làm, gọi hàm một cách đệ quy, và sau đó trở lại trạng thái ban đầu khi nó quay trở lại. Cơ chế này hoạt động giống như ngăn xếp, vì vậy rất tiện lợi khi gọi nó là ngăn xếp, ngay cả khi một số cấu trúc dữ liệu khác được sử dụng.
Barmar

48

Để hiểu đệ quy, bạn phải nghĩ vấn đề theo một cách khác. Thay vì một chuỗi lớn các bước hợp lý có ý nghĩa tổng thể, bạn thay vào đó thực hiện một vấn đề lớn và chia thành các vấn đề nhỏ hơn và giải quyết chúng, khi bạn có câu trả lời cho các vấn đề phụ, bạn kết hợp kết quả của các vấn đề phụ để tạo ra giải pháp cho vấn đề lớn hơn. Hãy nghĩ xem bạn và bạn bè của bạn cần đếm số viên bi trong một cái thùng lớn. Mỗi bạn lấy một nhóm nhỏ hơn và đếm từng nhóm đó và khi bạn hoàn thành, bạn cộng các tổng lại với nhau .. Giờ nếu mỗi bạn tìm được một người bạn nào đó và chia nhóm thêm, thì bạn chỉ cần đợi những người bạn khác này tính ra tổng của chúng, mang nó trở lại cho mỗi bạn, bạn cộng lại. Và như thế.

Bạn phải nhớ mỗi khi hàm tự gọi đệ quy, nó tạo ra một ngữ cảnh mới với một tập con của vấn đề, khi phần đó được giải quyết, nó sẽ được trả về để lần lặp trước đó có thể hoàn thành.

Hãy để tôi chỉ cho bạn các bước:

sumInts(a: 2, b: 5) will return: 2 + sumInts(a: 3, b: 5)
sumInts(a: 3, b: 5) will return: 3 + sumInts(a: 4, b: 5)
sumInts(a: 4, b: 5) will return: 4 + sumInts(a: 5, b: 5)
sumInts(a: 5, b: 5) will return: 5 + sumInts(a: 6, b: 5)
sumInts(a: 6, b: 5) will return: 0

khi sumInts (a: 6, b: 5) đã được thực thi, kết quả có thể được tính toán để sao lưu chuỗi với kết quả bạn nhận được:

 sumInts(a: 6, b: 5) = 0
 sumInts(a: 5, b: 5) = 5 + 0 = 5
 sumInts(a: 4, b: 5) = 4 + 5 = 9
 sumInts(a: 3, b: 5) = 3 + 9 = 12
 sumInts(a: 2, b: 5) = 2 + 12 = 14.

Một cách khác để biểu diễn cấu trúc của đệ quy:

 sumInts(a: 2, b: 5) = 2 + sumInts(a: 3, b: 5)
 sumInts(a: 2, b: 5) = 2 + 3 + sumInts(a: 4, b: 5)  
 sumInts(a: 2, b: 5) = 2 + 3 + 4 + sumInts(a: 5, b: 5)  
 sumInts(a: 2, b: 5) = 2 + 3 + 4 + 5 + sumInts(a: 6, b: 5)
 sumInts(a: 2, b: 5) = 2 + 3 + 4 + 5 + 0
 sumInts(a: 2, b: 5) = 14 

2
Tốt lắm, Rob. Bạn đã diễn đạt theo cách rất rõ ràng và dễ hiểu. Cảm ơn đã dành thời gian!
Jason Elwood

3
Đây là sự trình bày rõ ràng nhất về những gì đang diễn ra, không cần đi sâu vào lý thuyết và chi tiết kỹ thuật của nó, cho thấy rõ ràng từng bước thực hiện.
Bryan

2
Tôi rất vui. :) không phải lúc nào cũng dễ dàng giải thích những điều này. Cảm ơn bạn đã khen.
Rob

1
+1. Đây là cách tôi mô tả nó, cụ thể là với ví dụ cuối cùng của bạn về cấu trúc. Sẽ rất hữu ích khi xem những gì đang xảy ra một cách trực quan.
KChaloux

40

Đệ quy là một chủ đề khó hiểu và tôi không nghĩ rằng mình có thể làm điều đó một cách công bằng ở đây. Thay vào đó, tôi sẽ cố gắng tập trung vào đoạn mã cụ thể mà bạn có ở đây và cố gắng mô tả cả trực giác về lý do tại sao giải pháp hoạt động và cơ chế của cách mã tính toán kết quả của nó.

Đoạn mã bạn đưa ra ở đây giải quyết vấn đề sau: bạn muốn biết tổng của tất cả các số nguyên từ a đến b, bao gồm cả. Ví dụ: bạn muốn tổng các số từ 2 đến 5, bao gồm

2 + 3 + 4 + 5

Khi cố gắng giải quyết một vấn đề một cách đệ quy, một trong những bước đầu tiên nên là tìm cách chia vấn đề thành một bài toán nhỏ hơn với cùng cấu trúc. Vì vậy, giả sử rằng bạn muốn tổng hợp các số từ 2 đến 5, bao gồm cả. Một cách để đơn giản hóa điều này là lưu ý rằng tổng trên có thể được viết lại thành

2 + (3 + 4 + 5)

Ở đây, (3 + 4 + 5) là tổng của tất cả các số nguyên từ 3 đến 5, bao gồm cả. Nói cách khác, nếu bạn muốn biết tổng của tất cả các số nguyên từ 2 đến 5, hãy bắt đầu bằng cách tính tổng của tất cả các số nguyên từ 3 đến 5, sau đó cộng 2.

Vậy làm cách nào để tính tổng của tất cả các số nguyên từ 3 đến 5, bao gồm cả? Chà, tổng đó là

3 + 4 + 5

thay vào đó có thể được coi là

3 + (4 + 5)

Ở đây, (4 + 5) là tổng của tất cả các số nguyên từ 4 đến 5, bao gồm cả. Vì vậy, nếu bạn muốn tính tổng của tất cả các số từ 3 đến 5, bao gồm cả, bạn sẽ tính tổng của tất cả các số nguyên từ 4 đến 5, sau đó cộng 3.

Có một mẫu ở đây! Nếu bạn muốn tính tổng các số nguyên giữa a và b, bao gồm cả, bạn có thể thực hiện như sau. Đầu tiên, tính tổng các số nguyên giữa a + 1 và b, bao gồm cả. Tiếp theo, thêm a vào tổng số đó. Bạn sẽ nhận thấy rằng "tính tổng các số nguyên giữa a + 1 và b, inclusive" xảy ra khá giống loại vấn đề mà chúng tôi đang cố gắng giải quyết, nhưng với các tham số hơi khác. Thay vì tính toán từ a đến b, bao gồm, chúng tôi tính toán từ a + 1 đến b, bao gồm cả. Đó là bước đệ quy - để giải quyết vấn đề lớn hơn ("tổng từ a đến b, bao gồm"), chúng tôi giảm vấn đề thành một phiên bản nhỏ hơn của chính nó ("tổng từ a + 1 đến b, bao gồm.").

Nếu bạn xem mã bạn có ở trên, bạn sẽ nhận thấy rằng có bước này trong đó:

return a + sumInts(a + 1, b: b)

Mã này chỉ đơn giản là một bản dịch của logic ở trên - nếu bạn muốn tính tổng từ a đến b, bao gồm, hãy bắt đầu bằng cách tổng từ a + 1 đến b, bao gồm (đó là lệnh gọi đệ quy cho sumInts), sau đó thêm a.

Tất nhiên, bản thân cách tiếp cận này sẽ không thực sự hiệu quả. Ví dụ, bạn sẽ tính tổng của tất cả các số nguyên từ 5 đến 5 bằng cách nào? Vâng, sử dụng logic hiện tại của chúng tôi, bạn sẽ tính tổng của tất cả các số nguyên từ 6 đến 5, bao gồm, sau đó cộng 5. Vậy làm cách nào để tính tổng của tất cả các số nguyên từ 6 đến 5, bao gồm cả? Chà, sử dụng logic hiện tại của chúng tôi, bạn sẽ tính tổng của tất cả các số nguyên từ 7 đến 5, bao gồm, sau đó thêm 6. Bạn sẽ nhận thấy một vấn đề ở đây - điều này cứ tiếp tục và liên tục!

Trong giải quyết vấn đề đệ quy, cần phải có một số cách để ngừng đơn giản hóa vấn đề và thay vào đó chỉ đi giải quyết nó một cách trực tiếp. Thông thường, bạn sẽ tìm một trường hợp đơn giản mà câu trả lời có thể được xác định ngay lập tức, sau đó cấu trúc giải pháp của bạn để giải quyết các trường hợp đơn giản trực tiếp khi chúng phát sinh. Đây thường được gọi là trường hợp cơ sở hoặc cơ sở đệ quy .

Vậy trường hợp cơ bản trong vấn đề cụ thể này là gì? Khi bạn tính tổng các số nguyên từ a đến b, nếu a lớn hơn b, thì câu trả lời là 0 - không có bất kỳ số nào trong phạm vi! Do đó, chúng tôi sẽ cấu trúc giải pháp của mình như sau:

  1. Nếu a> b, thì câu trả lời là 0.
  2. Nếu không (a ≤ b), nhận được câu trả lời như sau:
    1. Tính tổng các số nguyên giữa a + 1 và b.
    2. Thêm a để có câu trả lời.

Bây giờ, hãy so sánh mã giả này với mã thực của bạn:

func sumInts(a: Int, b: Int) -> Int {
    if (a > b) {
        return 0
    } else {
        return a + sumInts(a + 1, b: b)
    }
}

Lưu ý rằng hầu như có một bản đồ 1-1 giữa giải pháp được nêu trong mã giả và mã thực tế này. Bước đầu tiên là trường hợp cơ sở - trong trường hợp bạn yêu cầu tổng của một dãy số trống, bạn sẽ nhận được 0. Nếu không, hãy tính tổng giữa a + 1 và b, sau đó cộng a.

Cho đến nay, tôi chỉ đưa ra một ý tưởng cấp cao đằng sau mã. Nhưng bạn có hai câu hỏi khác, rất hay. Đầu tiên, tại sao điều này không luôn trả về 0, vì hàm nói rằng trả về 0 nếu a> b? Thứ hai, số 14 thực sự đến từ đâu? Chúng ta hãy lần lượt xem xét những điều này.

Hãy thử một trường hợp rất, rất đơn giản. Điều gì xảy ra nếu bạn gọi sumInts(6, 5)? Trong trường hợp này, truy tìm mã, bạn thấy rằng hàm chỉ trả về 0. Đó là điều đúng đắn cần làm, vì - không có bất kỳ số nào trong phạm vi. Bây giờ, hãy thử điều gì đó khó hơn. Điều gì xảy ra khi bạn gọi sumInts(5, 5)? Chà, đây là những gì sẽ xảy ra:

  1. Bạn gọi sumInts(5, 5). Chúng tôi rơi vàoelse nhánh trả về giá trị của `a + sumInts (6, 5).
  2. Để sumInts(5, 5)xác định điều gì sumInts(6, 5)đang xảy ra, chúng ta cần tạm dừng công việc đang làm và thực hiện cuộc gọi đến sumInts(6, 5).
  3. sumInts(6, 5)được gọi. Nó đi vào ifchi nhánh và quay trở lại 0. Tuy nhiên, trường hợp này của sumIntsđược gọi bởi sumInts(5, 5), do đó, giá trị trả về được truyền trở lại sumInts(5, 5), không phải cho người gọi cấp cao nhất.
  4. sumInts(5, 5)bây giờ có thể tính toán 5 + sumInts(6, 5)để lấy lại 5. Sau đó, nó sẽ trả lại cho người gọi cấp cao nhất.

Lưu ý cách giá trị 5 được hình thành ở đây. Chúng tôi đã bắt đầu với một cuộc gọi hiện hoạt tới sumInts. Điều đó đã kích hoạt một cuộc gọi đệ quy khác và giá trị được trả về bởi cuộc gọi đó đã truyền thông tin trở lại sumInts(5, 5). Cuộc gọi đếnsumInts(5, 5)Đến lượt nó, thực hiện một số tính toán và trả về một giá trị cho người gọi.

Nếu bạn thử điều này với sumInts(4, 5), đây là những gì sẽ xảy ra:

  • sumInts(4, 5)cố gắng trở lại 4 + sumInts(5, 5). Để làm điều đó, nó kêu gọi sumInts(5, 5).
    • sumInts(5, 5)cố gắng trở lại 5 + sumInts(6, 5). Để làm điều đó, nó kêu gọi sumInts(6, 5).
    • sumInts(6, 5)trả về 0 trở lại sumInts(5, 5).</li> <li>sumInts (5, 5) now has a value forsumInts (6, 5) , namely 0. It then returns5 + 0 = 5`.
  • sumInts(4, 5)bây giờ có một giá trị cho sumInts(5, 5), cụ thể là 5. Sau đó nó trả về 4 + 5 = 9.

Nói cách khác, giá trị được trả về được hình thành bằng cách tính tổng các giá trị tại một thời điểm, mỗi lần lấy một giá trị được trả về bởi một lệnh gọi đệ quy cụ thể sumIntsvà thêm vào giá trị hiện tại của a. Khi đệ quy chạm đáy, lệnh gọi sâu nhất trả về 0. Tuy nhiên, giá trị đó không thoát ngay khỏi chuỗi lệnh gọi đệ quy; thay vào đó, nó chỉ đưa giá trị trở lại lệnh gọi đệ quy một lớp phía trên nó. Theo cách đó, mỗi lệnh gọi đệ quy chỉ thêm một số nữa và trả về số đó cao hơn trong chuỗi, lên đến đỉnh điểm là tổng thể. Như một bài tập, hãy thử theo dõi điều này đểsumInts(2, 5) , đó là điều bạn muốn bắt đầu.

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


3
Cảm ơn bạn đã dành thời gian trong ngày bận rộn để chia sẻ câu trả lời toàn diện như vậy! Có rất nhiều thông tin tuyệt vời ở đây giúp tôi tìm hiểu về các hàm đệ quy và chắc chắn sẽ giúp ích cho những người khác gặp phải bài đăng này trong tương lai. cám ơn một lần nữa và chúc một ngày tốt lành.
Jason Elwood

22

Cho đến nay, bạn đã có một số câu trả lời hay ở đây, nhưng tôi sẽ thêm một câu trả lời nữa mà có cách giải quyết khác.

Trước hết, tôi đã viết nhiều bài báo về các thuật toán đệ quy đơn giản mà bạn có thể thấy thú vị; xem

http://ericlippert.com/tag/recursion/

http://blogs.msdn.com/b/ericlippert/archive/tags/recursion/

Đó là thứ tự mới nhất trên đầu trang, vì vậy hãy bắt đầu từ dưới cùng.

Thứ hai, cho đến nay tất cả các câu trả lời đã mô tả ngữ nghĩa đệ quy bằng cách xem xét kích hoạt hàm . Rằng mỗi, mỗi cuộc gọi thực hiện một kích hoạt mới và cuộc gọi đệ quy thực thi trong ngữ cảnh của kích hoạt này. Đó là một cách hay để nghĩ về nó, nhưng có một cách khác, tương đương: tìm kiếm văn bản thông minh-và-thay thế .

Hãy để tôi viết lại hàm của bạn thành một dạng nhỏ gọn hơn một chút; đừng nghĩ về điều này là bằng bất kỳ ngôn ngữ cụ thể nào.

s = (a, b) => a > b ? 0 : a + s(a + 1, b)

Tôi hy vọng điều đó đúng. Nếu bạn không quen với toán tử điều kiện, nó có dạngcondition ? consequence : alternative và ý nghĩa của nó sẽ trở nên rõ ràng.

Bây giờ chúng tôi muốn đánh giá s(2,5) Chúng tôi làm như vậy bằng cách thực hiện thay thế bằng văn bản của lời gọi bằng phần thân hàm, sau đó thay thế abằng 2bbằng 5:

s(2, 5) 
---> 2 > 5 ? 0 : 2 + s(2 + 1, 5)

Bây giờ đánh giá điều kiện. Chúng tôi thay thế bằng văn bản 2 > 5bằng false.

---> false ? 0 : 2 + s(2 + 1, 5)

Bây giờ thay thế bằng văn bản tất cả các điều kiện sai bằng thay thế và tất cả các điều kiện đúng bằng hệ quả. Chúng tôi chỉ có điều kiện sai, vì vậy chúng tôi thay thế biểu thức đó bằng văn bản thay thế:

---> 2 + s(2 + 1, 5)

Bây giờ, để đỡ phải nhập tất cả các +dấu hiệu đó, hãy thay thế hằng số số học bằng giá trị của nó. (Đây là một chút gian lận, nhưng tôi không muốn phải theo dõi tất cả các dấu ngoặc đơn!)

---> 2 + s(3, 5)

Bây giờ tìm kiếm và thay thế, lần này là phần nội dung cho cuộc gọi, 3choa5cho b. Chúng tôi sẽ đặt thay thế cho cuộc gọi trong dấu ngoặc đơn:

---> 2 + (3 > 5 ? 0 : 3 + s(3 + 1, 5))

Và bây giờ chúng tôi chỉ tiếp tục thực hiện các bước thay thế bằng văn bản đó:

---> 2 + (false ? 0 : 3 + s(3 + 1, 5))  
---> 2 + (3 + s(3 + 1, 5))                
---> 2 + (3 + s(4, 5))                     
---> 2 + (3 + (4 > 5 ? 0 : 4 + s(4 + 1, 5)))
---> 2 + (3 + (false ? 0 : 4 + s(4 + 1, 5)))
---> 2 + (3 + (4 + s(4 + 1, 5)))
---> 2 + (3 + (4 + s(5, 5)))
---> 2 + (3 + (4 + (5 > 5 ? 0 : 5 + s(5 + 1, 5))))
---> 2 + (3 + (4 + (false ? 0 : 5 + s(5 + 1, 5))))
---> 2 + (3 + (4 + (5 + s(5 + 1, 5))))
---> 2 + (3 + (4 + (5 + s(6, 5))))
---> 2 + (3 + (4 + (5 + (6 > 5 ? 0 : s(6 + 1, 5)))))
---> 2 + (3 + (4 + (5 + (true ? 0 : s(6 + 1, 5)))))
---> 2 + (3 + (4 + (5 + 0)))
---> 2 + (3 + (4 + 5))
---> 2 + (3 + 9)
---> 2 + 12
---> 14

Tất cả những gì chúng tôi làm ở đây chỉ là thay thế bằng văn bản đơn giản . Thực sự tôi không nên thay thế "3" cho "2 + 1", v.v. cho đến khi tôi phải làm vậy, nhưng về mặt sư phạm thì nó sẽ khó đọc.

Kích hoạt hàm không gì khác ngoài việc thay thế lời gọi hàm bằng phần thân của lệnh gọi và thay thế các tham số chính thức bằng các đối số tương ứng của chúng. Bạn phải cẩn thận trong việc giới thiệu dấu ngoặc đơn một cách thông minh, nhưng ngoài ra, nó chỉ là sự thay thế văn bản.

Tất nhiên, hầu hết các ngôn ngữ không thực sự triển khai kích hoạt thay thế văn bản, nhưng về mặt logic thì đó là điều.

Vậy thì một đệ quy không bị ràng buộc là gì? Một đệ quy mà sự thay thế văn bản không dừng lại! Lưu ý rằng cuối cùng chúng ta đã đi đến một bước mà không còn cách nào khác sđể thay thế, và sau đó chúng ta có thể chỉ cần áp dụng các quy tắc cho số học.


Ví dụ tốt nhưng nó làm bạn đau lòng khi bạn thực hiện các phép tính phức tạp hơn. Ví dụ. Tìm thấy tổ tiên chung trong Cây nhị phân.
CodeYogi

11

Cách mà tôi thường tìm ra cách hoạt động của một hàm đệ quy là xem xét trường hợp cơ sở và làm việc ngược lại. Đây là kỹ thuật được áp dụng cho chức năng này.

Đầu tiên là trường hợp cơ sở:

sumInts(6, 5) = 0

Sau đó, cuộc gọi ngay trên đó trong ngăn xếp cuộc gọi :

sumInts(5, 5) == 5 + sumInts(6, 5)
sumInts(5, 5) == 5 + 0
sumInts(5, 5) == 5

Sau đó, cuộc gọi ngay trên đó trong ngăn xếp cuộc gọi:

sumInts(4, 5) == 4 + sumInts(5, 5)
sumInts(4, 5) == 4 + 5
sumInts(4, 5) == 9

Và như thế:

sumInts(3, 5) == 3 + sumInts(4, 5)
sumInts(3, 5) == 3 + 9
sumInts(3, 5) == 12

Và như thế:

sumInts(2, 5) == 2 + sumInts(3, 5)
sumInts(4, 5) == 2 + 12
sumInts(4, 5) == 14

Lưu ý rằng chúng tôi đã đến cuộc gọi ban đầu của chúng tôi tới hàm sumInts(2, 5) == 14

Thứ tự thực hiện các lệnh gọi này:

sumInts(2, 5)
sumInts(3, 5)
sumInts(4, 5)
sumInts(5, 5)
sumInts(6, 5)

Thứ tự mà các cuộc gọi này trả về:

sumInts(6, 5)
sumInts(5, 5)
sumInts(4, 5)
sumInts(3, 5)
sumInts(2, 5)

Lưu ý rằng chúng tôi đã đi đến kết luận về cách hàm hoạt động bằng cách theo dõi các lệnh gọi theo thứ tự chúng trả về .


5

Tôi sẽ thử.

Thực hiện phương trình a + sumInts (a + 1, b), tôi sẽ chỉ ra câu trả lời cuối cùng là 14 như thế nào.

//the sumInts function definition
func sumInts(a: Int, b: Int) -> Int {
    if (a > b) {
        return 0
    } else {
        return a + sumInts(a + 1, b)
    }
}

Given: a = 2 and b = 5

1) 2 + sumInts(2+1, 5)

2) sumInts(3, 5) = 12
   i) 3 + sumInts(3+1, 5)
   ii) 4 + sumInts(4+1, 5)
   iii) 5 + sumInts(5+1, 5)
   iv) return 0
   v) return 5 + 0
   vi) return 4 + 5
   vii) return 3 + 9

3) 2 + 12 = 14.

Cho chúng tôi biết nếu bạn có bất kỳ câu hỏi thêm.

Đây là một ví dụ khác về các hàm đệ quy trong ví dụ sau.

Một người đàn ông vừa tốt nghiệp đại học.

t là lượng thời gian tính bằng năm.

Tổng số năm thực tế làm việc trước khi nghỉ hưu được tính như sau:

public class DoIReallyWantToKnow 
{
    public int howLongDoIHaveToWork(int currentAge)
    {
      const int DESIRED_RETIREMENT_AGE = 65;
      double collectedMoney = 0.00; //remember, you just graduated college
      double neededMoneyToRetire = 1000000.00

      t = 0;
      return work(t+1);
    }

    public int work(int time)
    {
      collectedMoney = getCollectedMoney();

      if(currentAge >= DESIRED_RETIREMENT_AGE 
          && collectedMoney == neededMoneyToRetire
      {
        return time;
      }

      return work(time + 1);
    }
}

Và điều đó chỉ đủ để làm bất cứ ai chán nản, lol. ;-P


5

Đệ quy. Trong Khoa học Máy tính, đệ quy được đề cập sâu trong chủ đề của Dữ liệu tự động hữu hạn.

Ở dạng đơn giản nhất, nó là một tham chiếu tự. Ví dụ, nói rằng "my car is a car" là một câu lệnh đệ quy. Vấn đề là câu lệnh là một đệ quy vô hạn trong đó nó sẽ không bao giờ kết thúc. Định nghĩa trong câu lệnh của "car" là nó là một "car" nên nó có thể được thay thế. Tuy nhiên, không có hồi kết vì trong trường hợp thay thế vẫn trở thành "xe của tôi là xe".

Điều này có thể khác nếu tuyên bố là "my car is a b Bentley. My car is blue." Trong trường hợp đó, sự thay thế trong tình huống thứ hai cho ô tô có thể là "b Bentley", dẫn đến "b Bentley của tôi có màu xanh lam". Các loại thay thế này được giải thích bằng toán học trong Khoa học Máy tính thông qua Ngữ pháp không theo ngữ cảnh .

Thay thế thực tế là một quy luật sản xuất. Cho rằng câu lệnh được đại diện bởi S và chiếc xe đó là một biến có thể là "b Bentley", câu lệnh này có thể được tái tạo một cách đệ quy.

S -> "my"S | " "S | CS | "is"S | "blue"S | ε
C -> "bentley"

Điều này có thể được xây dựng theo nhiều cách, vì mỗi |cách có nghĩa là có một sự lựa chọn. Scó thể được thay thế bằng bất kỳ một trong những lựa chọn đó và S luôn bắt đầu trống. Các εphương tiện để chấm dứt sản xuất. Cũng như Scó thể được thay thế, các biến khác cũng vậy (chỉ có một và nó làC sẽ đại diện cho "b Bentley").

Vì vậy, bắt đầu với Sviệc trống rỗng và thay thế nó bằng lựa chọn đầu tiên "my"S Strở thành

"my"S

Svẫn có thể được thay thế vì nó đại diện cho một biến. Chúng ta có thể chọn lại "của tôi" hoặc ε để kết thúc, nhưng chúng ta hãy tiếp tục đưa ra tuyên bố ban đầu của chúng ta. Chúng tôi chọn không gian có nghĩa Slà được thay thế bằng" "S

"my "S

Tiếp theo hãy chọn C

"my "CS

Và C chỉ có một sự lựa chọn để thay thế

"my bentley"S

Và không gian một lần nữa cho S

"my bentley "S

Và vân vân "my bentley is"S, "my bentley is "S, "my bentley is blue"S,"my bentley is blue" (thay thế S cho ε kết thúc sản xuất) và chúng tôi đã đệ quy được xây dựng tuyên bố của chúng tôi "bentley của tôi là màu xanh".

Hãy nghĩ về đệ quy như những sản phẩm và thay thế này. Mỗi bước trong quy trình thay thế bước trước của nó để tạo ra kết quả cuối cùng. Trong ví dụ chính xác về tổng đệ quy từ 2 đến 5, bạn kết thúc với sản xuất

S -> 2 + A
A -> 3 + B
B -> 4 + C
C -> 5 + D
D -> 0

Điều này trở thành

2 + A
2 + 3 + B
2 + 3 + 4 + C
2 + 3 + 4 + 5 + D
2 + 3 + 4 + 5 + 0
14

Tôi không chắc rằng tự động dữ liệu trạng thái hữu hạn hoặc ngữ pháp không có ngữ cảnh là những ví dụ tốt nhất có thể giúp người ta xây dựng một số trực giác đầu tiên về đệ quy. Chúng là những ví dụ hay, nhưng có lẽ hơi xa lạ đối với các lập trình viên không có nền tảng CS trước đây.
chi

4

Tôi nghĩ rằng cách tốt nhất để hiểu các hàm đệ quy là nhận ra rằng chúng được tạo ra để xử lý các cấu trúc dữ liệu đệ quy. Nhưng trong hàm ban đầu của bạn sumInts(a: Int, b: Int)tính toán đệ quy tổng các số từ ađến b, nó có vẻ không phải là một cấu trúc dữ liệu đệ quy ... Hãy thử một phiên bản sửa đổi một chút sumInts(a: Int, n: Int), nxem bạn sẽ thêm bao nhiêu số.

Bây giờ, sumInts là nmột số tự nhiên đệ quy . Vẫn không phải là một dữ liệu đệ quy, phải không? Chà, một số tự nhiên có thể được coi là một cấu trúc dữ liệu đệ quy bằng cách sử dụng tiên đề Peano:

enum Natural = {
    case Zero
    case Successor(Natural)
}

Vì vậy, 0 = Zero, 1 = Succesor (Zero), 2 = Succesor (Succesor (Zero)), v.v.

Khi bạn có cấu trúc dữ liệu đệ quy aa, bạn có mẫu cho hàm. Đối với mỗi trường hợp không đệ quy, bạn có thể tính giá trị trực tiếp. Đối với các trường hợp đệ quy, bạn giả sử rằng hàm đệ quy đã hoạt động và sử dụng nó để tính toán trường hợp đó, nhưng giải cấu trúc đối số. Trong trường hợp Tự nhiên, nó có nghĩa là thay vì Succesor(n)chúng tôi sẽ sử dụng nhoặc tương đương, thay vì nchúng tôi sẽ sử dụng n - 1.

// sums n numbers beginning from a
func sumInts(a: Int, n: Int) -> Int {
    if (n == 0) {
        // non recursive case
    } else {
        // recursive case. We use sumInts(..., n - 1)
    }
}

Bây giờ hàm đệ quy được lập trình đơn giản hơn. Đầu tiên, trường hợp cơ sở,n=0 . Chúng ta nên trả về những gì nếu chúng ta không muốn thêm số? Tất nhiên câu trả lời là 0.

Còn trường hợp đệ quy thì sao? Nếu chúng ta muốn thêm các nsố bắt đầu bằng avà chúng ta đã có một sumIntshàm làm việc hoạt động cho n-1? Chà, chúng ta cần thêm avà sau đó gọi sumIntsvới a + 1, vì vậy chúng ta kết thúc bằng:

// sums n numbers beginning from a
func sumInts(a: Int, n: Int) -> Int {
    if (n == 0) {
        return 0
    } else {
        return a + sumInts(a + 1, n - 1)
    }
}

Điều tốt đẹp là bây giờ bạn không cần phải suy nghĩ về mức độ đệ quy thấp. Bạn chỉ cần xác minh rằng:

  • Đối với các trường hợp cơ sở của dữ liệu đệ quy, nó tính toán câu trả lời mà không cần sử dụng đệ quy.
  • Đối với các trường hợp đệ quy của dữ liệu đệ quy, nó sẽ tính toán câu trả lời bằng cách sử dụng đệ quy trên dữ liệu bị hủy.

4

Bạn có thể quan tâm đến việc triển khai các chức năng của Nisan và Schocken . Bản pdf được liên kết là một phần của khóa học trực tuyến miễn phí. Nó mô tả phần thứ hai của việc triển khai máy ảo, trong đó học sinh phải viết một trình biên dịch ngôn ngữ máy-ngôn ngữ-máy-ảo. Việc triển khai hàm mà họ đề xuất có khả năng đệ quy vì nó dựa trên ngăn xếp.

Để giới thiệu cho bạn cách triển khai chức năng: Hãy xem xét mã máy ảo sau:

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

Nếu Swift được biên dịch sang ngôn ngữ máy ảo này, thì khối mã Swift sau:

mult(a: 2, b: 3) - 4

sẽ biên dịch xuống

push constant 2  // Line 1
push constant 3  // Line 2
call mult        // Line 3
push constant 4  // Line 4
sub              // Line 5

Ngôn ngữ máy ảo được thiết kế xung quanh một ngăn xếp toàn cục .push constant nđẩy một số nguyên vào ngăn xếp toàn cục này.

Sau khi thực hiện dòng 1 và 2, ngăn xếp trông giống như sau:

256:  2  // Argument 0
257:  3  // Argument 1

256257là các địa chỉ bộ nhớ.

call mult đẩy số dòng trả về (3) vào ngăn xếp và phân bổ không gian cho các biến cục bộ của hàm.

256:  2  // argument 0
257:  3  // argument 1
258:  3  // return line number
259:  0  // local 0

... và nó đi đến nhãn function mult. Mã bên trong multđược thực thi. Kết quả của việc thực thi mã đó, chúng tôi tính tích số 2 và 3, được lưu trữ trong biến cục bộ thứ 0 của hàm.

256:  2  // argument 0
257:  3  // argument 1
258:  3  // return line number
259:  6  // local 0

Ngay trước khi nhập returntừ mult, bạn sẽ thấy dòng:

push local 0  // push result

Chúng tôi sẽ đẩy sản phẩm lên ngăn xếp.

256:  2  // argument 0
257:  3  // argument 1
258:  3  // return line number
259:  6  // local 0
260:  6  // product

Khi chúng tôi quay trở lại, những điều sau sẽ xảy ra:

  • Đưa giá trị cuối cùng trên ngăn xếp vào địa chỉ bộ nhớ của đối số thứ 0 (256 trong trường hợp này). Đây là nơi thuận tiện nhất để đặt nó.
  • Loại bỏ mọi thứ trên ngăn xếp đến địa chỉ của đối số thứ 0.
  • Chuyển đến số dòng trả lại (3 trong trường hợp này) và sau đó chuyển tiếp.

Sau khi quay lại, chúng tôi đã sẵn sàng thực hiện dòng 4 và ngăn xếp của chúng tôi trông như thế này:

256:  6  // product that we just returned

Bây giờ chúng ta đẩy 4 vào ngăn xếp.

256:  6
257:  4

sublà một chức năng nguyên thủy của ngôn ngữ máy ảo. Nó nhận hai đối số và trả về kết quả của nó ở địa chỉ thông thường: địa chỉ của đối số thứ 0.

Bây giờ chúng tôi có

256:  2  // 6 - 4 = 2

Bây giờ bạn đã biết cách hoạt động của một lời gọi hàm, nó tương đối đơn giản để hiểu cách hoạt động của đệ quy. Không có phép thuật , chỉ là một ngăn xếp.

Tôi đã triển khai sumIntschức năng của bạn bằng ngôn ngữ máy ảo này:

function sumInts 0     // `0` means it has no local variables.
  label IF
    push argument 0
    push argument 1
    lte              
    if-goto ELSE_CASE
    push constant 0
    return
  label ELSE_CASE
    push constant 2
    push argument 0
    push constant 1
    add
    push argument 1
    call sumInts       // Line 15
    add                // Line 16
    return             // Line 17
// End of function

Bây giờ tôi sẽ gọi nó là:

push constant 2
push constant 5
call sumInts           // Line 21

Mã thực thi và chúng ta đi đến điểm dừng nơi ltetrả về false. Đây là những gì ngăn xếp trông như thế này tại thời điểm này:

// First invocation
256:  2   // argument 0
257:  5   // argument 1
258:  21  // return line number
259:  2   // augend
// Second
260:  3   // argument 0
261:  5   // argument 1
262:  15  // return line number
263:  3   // augend
// Third
264:  4   // argument 0
265:  5   // argument 1
266:  15  // return line number
267:  4   // augend
// Fourth
268:  5   // argument 0
269:  5   // argument 1
270:  15  // return line number
271:  5   // augend
// Fifth
272:  6   // argument 0
273:  5   // argument 1
274:  15  // return line number
275:  0   // return value

Bây giờ chúng ta hãy "giải phóng" đệ quy của chúng ta. return0 và goto dòng 15 và tiến lên.

271:  5
272:  0

Dòng 16: add

271:  5

Dòng 17: return5 và goto dòng 15 và tiến lên.

267:  4
268:  5

Dòng 16: add

267:  9

Dòng 17: return9 và goto dòng 15 và tiến lên.

263:  3
264:  9

Dòng 16: add

263:  12

Dòng 17: return12 và goto dòng 15 và tiến lên.

259:  2
260:  12

Dòng 16: add

259:  14

Dòng 17: return14 và goto dòng 21 và tiến lên.

256:  14

Đây là bạn có nó. Đệ quy: Glorified goto.


4

Một mẹo thực sự hay mà tôi đã gặp khi học và thực sự hiểu về đệ quy là dành thời gian học một ngôn ngữ không có bất kỳ dạng cấu trúc vòng lặp nào khác ngoài thông qua đệ quy. Bằng cách đó, bạn sẽ có cảm giác tuyệt vời về cách SỬ DỤNG đệ quy thông qua thực hành.

Tôi đã theo dõi http://www.htdp.org/ , đây cũng như là một hướng dẫn về Đề án, cũng là một giới thiệu tuyệt vời về cách thiết kế các chương trình về mặt kiến ​​trúc và thiết kế.

Nhưng về cơ bản, bạn cần đầu tư một thời gian. Nếu không có sự nắm bắt 'chắc chắn' về đệ quy, các thuật toán nhất định, chẳng hạn như backtracking, sẽ luôn có vẻ 'khó' hoặc thậm chí là 'ma thuật' đối với bạn. Vì vậy, hãy kiên trì. :-D

Tôi hy vọng điều này có thể giúp ích, chúc bạn may mắn!


3

Đã có rất nhiều câu trả lời hay. Tôi vẫn đang thử.
Khi được gọi, một hàm nhận được một không gian bộ nhớ được phân bổ, được xếp chồng lên không gian bộ nhớ của hàm người gọi. Trong vùng nhớ này, hàm giữ các tham số được truyền cho nó, các biến và giá trị của chúng. Không gian bộ nhớ này biến mất cùng với lệnh trả về kết thúc của hàm. Khi ý tưởng về ngăn xếp hình thành, không gian bộ nhớ của hàm người gọi bây giờ trở nên hoạt động.

Đối với các cuộc gọi đệ quy, cùng một hàm nhận được nhiều không gian bộ nhớ xếp chồng lên nhau. Đó là tất cả. Ý tưởng đơn giản về cách ngăn xếp hoạt động trong bộ nhớ của máy tính sẽ giúp bạn hiểu được cách thức đệ quy xảy ra trong quá trình thực thi.


3

Tôi biết một chút lạc đề, nhưng ... hãy thử tra cứu đệ quy trong Google ... Bạn sẽ thấy nó có nghĩa là gì :-)


Các phiên bản trước của Google đã trả lại văn bản sau (trích dẫn từ bộ nhớ):

Đệ quy

Xem đệ quy

Vào ngày 10 tháng 9 năm 2014, trò đùa về đệ quy đã được cập nhật:

Đệ quy

Ý của bạn là: Đệ quy


Để biết câu trả lời khác, hãy xem câu trả lời này .


3

Hãy nghĩ đệ quy giống như nhiều bản sao cùng làm một việc ...

Bạn yêu cầu sao chép [1]: "tổng các số từ 2 đến 5"

+ clone[1]               knows that: result is 2 + "sum numbers between 3 and 5". so he asks to clone[2] to return: "sum numbers between 3 and 5"
|   + clone[2]           knows that: result is 3 + "sum numbers between 4 and 5". so he asks to clone[3] to return: "sum numbers between 4 and 5"
|   |   + clone[3]       knows that: result is 4 + "sum numbers between 5 and 5". so he asks to clone[4] to return: "sum numbers between 5 and 5"
|   |   |   + clone[4]   knows that: result is 5 + "sum numbers between 6 and 5". so he asks to clone[5] to return: "sum numbers between 6 and 5"
|   |   |   |   clone[5] knows that: he can't sum, because 6 is larger than 5. so he returns 0 as result.
|   |   |   + clone[4]   gets the result from clone[5] (=0)  and sums: 5 + 0,  returning 5
|   |   + clone[3]       gets the result from clone[4] (=5)  and sums: 4 + 5,  returning 9
|   + clone[2]           gets the result from clone[3] (=9)  and sums: 3 + 9,  returning 12
+ clone[1]               gets the result from clone[2] (=12) and sums: 2 + 12, returning 14

và Voila!!


2

Nhiều câu trả lời ở trên rất hay. Mặc dù vậy, một kỹ thuật hữu ích để giải quyết đệ quy là viết trước những gì chúng ta muốn làm và viết mã như một con người sẽ giải quyết nó. Trong trường hợp trên, chúng ta muốn tính tổng một chuỗi các số nguyên liên tiếp (sử dụng các số từ trên):

2, 3, 4, 5  //adding these numbers would sum to 14

Bây giờ, lưu ý rằng những dòng này là khó hiểu (không sai, nhưng khó hiểu).

if (a > b) {
    return 0 
}

Tại sao phải kiểm tra a>b?, và tại saoreturn 0

Hãy thay đổi mã để phản ánh kỹ hơn những gì con người làm

func sumInts(a: Int, b: Int) -> Int {
  if (a == b) {
    return b // When 'a equals b' I'm at the most Right integer, return it
  }
  else {
    return a + sumInts(a: a + 1, b: b)
  }
}

Chúng ta có thể làm điều đó giống như con người hơn không? Đúng! Thông thường chúng ta tính tổng từ trái qua phải (2 + 3 + ...). Nhưng đệ quy trên là tính tổng từ phải sang trái (... + 4 + 5). Thay đổi mã để phản ánh nó (Có -thể hơi đáng sợ, nhưng không nhiều)

func sumInts(a: Int, b: Int) -> Int {
  if (a == b) {
    return b // When I'm at the most Left integer, return it
  }
  else {
    return sumInts(a: a, b: b - 1) + b
  }
}

Một số người có thể thấy hàm này khó hiểu hơn vì chúng ta đang bắt đầu từ điểm cuối 'xa', nhưng thực hành có thể khiến nó cảm thấy tự nhiên (và đó là một kỹ thuật 'tư duy' tốt khác: Thử 'cả hai' khi giải một đệ quy). Và một lần nữa, hàm phản ánh những gì một con người (hầu hết?) Làm: Lấy tổng của tất cả các số nguyên bên trái và thêm số nguyên bên phải 'tiếp theo'.


2

Tôi đã rất khó hiểu về đệ quy, sau đó tôi tìm thấy blog này và tôi đã thấy câu hỏi này nên tôi nghĩ tôi phải chia sẻ. Bạn phải đọc blog này, tôi thấy điều này cực kỳ hữu ích, nó giải thích với ngăn xếp và thậm chí nó giải thích cách hai đệ quy hoạt động với ngăn xếp theo từng bước. Trước tiên, tôi khuyên bạn nên hiểu cách hoạt động của ngăn xếp mà nó giải thích rất rõ ở đây: hành trình đến ngăn xếp

then now you will understand how recursion works now take a look of this post: Hiểu đệ quy từng bước

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

Nó là một chương trình:

def hello(x):
    if x==1:
        return "op"
    else:
        u=1
        e=12
        s=hello(x-1)
        e+=1
        print(s)
        print(x)
        u+=1
    return e

hello(3)

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


2

Đệ quy bắt đầu có ý nghĩa đối với tôi khi tôi ngừng đọc những gì người khác nói về nó hoặc coi nó là thứ tôi có thể tránh và chỉ viết mã. Tôi đã tìm thấy vấn đề với một giải pháp và cố gắng sao chép giải pháp mà không cần tìm. Tôi chỉ nhìn vào giải pháp khi tôi bế tắc bất lực. Sau đó, tôi quay lại cố gắng sao chép nó. Tôi đã làm điều này một lần nữa trên nhiều bài toán cho đến khi tôi phát triển sự hiểu biết và ý thức của riêng mình về cách xác định một vấn đề đệ quy và giải quyết nó. Khi tôi đạt đến cấp độ này, tôi bắt đầu tạo ra các vấn đề và giải quyết chúng. Điều đó đã giúp tôi nhiều hơn. Đôi khi, chỉ có thể học được mọi thứ bằng cách tự mình thử và đấu tranh; cho đến khi bạn "hiểu được".


0

Hãy để tôi cho bạn biết với một ví dụ về chuỗi Fibonacci, Fibonacci là

t (n) = t (n - 1) + n;

nếu n = 0 thì 1

vì vậy hãy xem làm thế nào các công trình đệ quy, tôi chỉ cần thay thế nt(n)với n-1và vân vân. nó trông:

t (n-1) = t (n - 2) + n + 1;

t (n-1) = t (n - 3) + n + 1 + n;

t (n-1) = t (n - 4) + n + 1 + n + 2 + n;

.

.

.

t (n) = t (nk) + ... + (nk-3) + (nk-2) + (nk-1) + n;

chúng tôi biết nếu t(0)=(n-k)bằng 1thì n-k=0nên n=kchúng tôi thay thế kbằng n:

t (n) = t (nn) + ... + (n-n + 3) + (n-n + 2) + (n-n + 1) + n;

nếu chúng ta bỏ qua n-nthì:

t (n) = t (0) + ... + 3 + 2 + 1 + (n-1) + n;

3+2+1+(n-1)+nsố tự nhiên cũng vậy . nó tính toán làΣ3+2+1+(n-1)+n = n(n+1)/2 => n²+n/2

kết quả cho fib là: O(1 + n²) = O(n²)

Đây là cách tốt nhất để hiểu mối quan hệ đệ quy

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.