Giải thuật : Đệ Quy1Cách đệ quy hoạt động12Tại sao đệ quy hoạt động43Phương pháp truy suất64Ví dụ về một số hàm đệ quy124.1Tìm phần tử lớn nhất của mảng124.2Nhận diện số nguyên không dấu ( Unsigned)134.3Ước chung lớn nhất135Tuần tự trong đệ quy145.1Xử lí đầu vào của mảng145.2Toà tháp Hà Nội15Bài viết này mô tả việc sử dụng đệ quy trong lâp trình. Đệ quy tương đương với sức mạnh của phương pháp lặp do đó, một ngôn ngữ lập trình không cần cả hai, nhưng đệ quy làm nhiều tác vụ dễ dàng hơn để lập trình và các ngôn ngữ chủ yếu dựa vào các vòng lặp cũng luôn cung cấp đệ quy. Sức mạnh đến từ khả năng giải quyết vấn đề đệ quy sẽ thay đổi cách bạn tiếp cận nhiều vấn đề của lập trình.Đệ quy là kỹ thuật mô tả và giải quyết các vấn đề bằng cách đưa ra các vấn đề nhỏ hơn tương tự. Phương pháp đệ quy là những phương pháp có thể tự gọi, trực tiếp hoặc gián tiếp. Đệ quy là một phương pháp thay thế cho phép lặp; một ngôn ngữ lập trình với các vòng lặp không mạnh mẽ hơn bằng cách thêm các phương thức đệ quy, tương tự thêm các vòng lặp vào một ngôn ngữ có đệ quy không làm cho nó mạnh hơn. Nhưng các kỹ thuật giải quyết vấn đề đệ quy thường khá khác so với các kỹ thuật lặp và thường đơn giản và rõ ràng hơn nhiều, và do đó dễ lập trình hơn.1Cách đệ quy hoạt độngĐể hiểu cách các hàm đệ quy hoạt động, cần phải hiểu các định nghĩa đệ quy và chúng ta sẽ bắt đầu với các định nghĩa đệ quy của các hàm toán học. Hàm factorial (luỹ thừa) thường được biểu thị bằng dấu chấm than , Có thể được định nghĩa đại khái như sau:0 == 1n == n(n1)(n2)...321 với số nguyên n > 0.Luỹ thừa chỉ được xác định cho các số nguyên lớn hơn hoặc bằng 0 và tạo ra các số nguyên lớn hơn 0.Do đó,0 == 11 == 12 == 21 == 23 == 321 == 64 == 4321 == 245 == 54321 == 120...10 == 10987654321 == 3,628,800Định nghĩa trên là sự lặp lại theo quy luật và chúng ta có thể sử dụng một vòng lặp để viết cách tính giá trị của n. Factorial: iterativepublic static int fact(int n){ PreconditionAssert.pre(n >= 0,argument must be >= 0); Postcondition: returned value == nint factValue = 1;for (int i = 1; i 0.Định nghĩa này bao gồm hai mệnh đề hay là hai phương trình. Mệnh đề thứ nhất, được gọi là mệnh đề cơ sở, đưa ra kết quả trực tiếp, trong trường hợp này, cho đối số 0. Phương trình thứ hai, được gọi là mệnh đề quy nạp hoặc đệ quy, đánh dấu hay thông báo rằng định nghĩa là định nghĩa đệ quy vì đối tượng được định nghĩa, (hàm giai thừa ), xuất hiện ở bên phải của phương trình cũng như bên trái. Phương thức hàm đệ quy để tính giai thừa như sau: Factorial: recursivepublic static int fact(int n){ PreconditionAssert.pre(n >= 0,argument must be >= 0); Postcondition: returned value == nif (n == 0)return 1;elsereturn n fact(n1);}Định nghĩa này có thể được nhận dạng là đệ quy vì định nghĩa của hàm chứa lệnh gọi hàm được định nghĩa.Có thể hơi trừu tượng và khó hiểu nhưng sau khi làm một số bài tập và đọc lại bạn sẽ hiểu. Định nghĩa tự nó cho thấy rất ít sự phức tạp hoặc tính toán; điều phức tạp nhất xảy ra là tham số n được nhân với một giá trị được trả về bởi một phương thức. Tại sao nó hoạt động? Nhờ vào các pre và postcondition Nếu phương thức này được truyền giá trị 0, nó trả về 1. Nếu nó được truyền giá trị lớn hơn, giả sử 5, thì nó nhân gấp 5 lần giá trị được trả về bởi thực tế (4). Do đó, kết quả là chính xác nếu là: thực tế (4) trả về 4. Nếu muốn, chúng ta có thể thấy thực tế (5) trả về là 5 4 3 2 1. Nhưng có một kỹ thuật mạnh mẽ hơn nhiều, và chúng ta sẽ tìm hiểu ngay.Phương pháp đệ quy thường thể hiện hai tính chất. Đầu tiên, đối với một số tập hợp đối số, phương thức tính toán câu trả lời là trực tiếp, không cần gọi đệ quy. Những trường hợp này tương ứng với các mệnh đề cơ bản của định nghĩa. Thông thường một số vấn đề nhỏ được giải quyết trực tiếp bằng các mệnh đề cơ bản; trong trường hợp giai thừa, giá trị của hàm cho đối số nhỏ nhất có thể, 0, được chỉ định rõ ràng trong hàm. Thuộc tính thứ hai của các phương thức đệ quy là nếu một đối số không được xử lý bởi mệnh đề cơ sở, thì phương thức sẽ tính kết quả bằng cách gọi đệ quy có đối số gần với mệnh đề cơ sở (thường là nhỏ hơn) so với đối số cho lần gọi ban đầu . Trong trường hợp giai thừa, nếu đối số n không bằng 0, thì một lần gọi đệ quy được thực hiện cho factorial, nhưng đối số được truyền là n1.Các vấn đề nhỏ được giải quyết trực tiếp bằng định nghĩa hàm đệ quy có thể nhiều hơn một lần và số lượng các lần gọi đệ quy được thực hiện trong một hàm cũng có thể nhiều hơn một. Một hàm toán học đệ quy kinh điển khác là hàm Fibonacci; như với Factorial ( giai thừa) , nó chỉ được định nghĩa cho các số nguyên không âm.F(0) = 0 F(1) = 1 F(n) = F(n1) + F(n2) vớ n>=2Chuỗi Fibonacci là chuỗi các số nguyên được tạo bởi dãy:0, 1, 1, 2, 3, 5, 8, 13, 21...Hai giá trị đầu tiên được tạo mặc định; mỗi phần tử khác là tổng của hai số trước. Việc thực hiện tính toán các số tiếp theo trong dãy theo phương pháp đệ quy được xây dựng trực tiếp từ định nghĩa toán học: Fibonaccipublic static int fibonacci(int n){ PreconditionAssert.pre(n >= 0,argument must be >= 0); Postcondition: returned value is F(n)if (n == 0 || n == 1) return n;elsereturn fibonacci(n1)+fibonacci(n2);}Định nghĩa này giải quyết trực tiếp hai trường hợp, cho các đối số 0 và 1 và có hai lệnh gọi đệ quy để tính F (n), cả hai đều truyền các đối số nhỏ hơn n.Sử dụng đệ quy không đảm bảo thành công 100%. Một định nghĩa đệ quy có thể không chính xác theo nhiều cách khác nhau và hàm đệ quy có thể được gọi không chính xác. Một số lỗi này sẽ khiến một lần gọi kéo dài mãi mãi không kết thúc. Các lần gọi đệ quy không kết thúc tương tự như các vòng lặp không kết thúc. Về lý thuyết, hàm Java hoàn toàn OK, sẽ không bao giờ chấm dứt bởi vì mỗi lần gọi hàm đều dẫn đến một lần gọi khác không có trường hợp cơ bản nào để dừng việc tính toán. Trên thực tế, nó sẽ chấm dứt khi không gian cần thiết cho thời gian chạy stack vượt quá khả năng của máy tính hoặc khi n1 quá nhỏ.public static int byebye(int n){return byebye(n1) + 1;}Hàm sau có trường hợp cơ bản, nhưng sẽ chỉ dừng lại đối với các đối số chẵn dương một đối số là số nguyên lẻ hoặc số âm sẽ dẫn đến một chuỗi các lệnh gọi hàm không bao giờ kết thúc.public static int evenOnly(int n) {if (n == 0) Truong hop co banreturn 0; else return soChan(n2) + 2;}Các hàm giai thừa và hàm Fibonaccinchỉ được xác định với các số nguyên dương, như đã nêu các điều kiện trên việc gọi một trong hai đối số âm sẽ dẫn đến việc tính toán không thể kết thúc.2Tại sao phương pháp đệ quy hoạt độngTrong phần trước, chúng ta đã thấy làm thế nào để suy luận về tính chính xác của code có chứa phép gán, phép lặp. Nhưng làm thế nào để chúng ta thuyết phục bản thân rằng phương pháp đệ quy hoạt động? Câu trả lời đơn giản, dựa trên nguyên tắc toán học, hoặc nguyên tắc tổng quát hơn, đệ quy. Trong phần này, mình giới thiệu ngắn gọn về phương pháp quy nạp trong toán học và sau đó cho thấy ứng dụng của nó đối với phương pháp đệ quy.Ví dụ: Giả sử ta muốn chỉ ra rằng với mọi số nguyên không âm n (0, 1, 2, ...) thì biểu thức (n5n) là bội số nguyên của 5. Có một số cách thức chứng minh. Ví dụ: chúng ta có thể chứng minh trực tiếp bằng cách hiển thị (kiểm tra 10 trường hợp khác nhau) rằng với mọi số tự nhiên n thì n5 và n có cùng một chữ số đơn vị. Do đó n5n luôn có kết quả là chữ số 0 ở vị trí đơn vị, và do đó chia hết cho 10.Giải thuật thay thế sử dụng bằng cách chứng minh quy nạp. Ở đây chúng ta bắt đầu với định nghĩa quy nạp của tập hợp và sau đó sử dụng định nghĩa này làm cơ sở chứng minh. Tập hợp các số nguyên không âm N có thể được mô tả đệ quy như sau:1. 0 thuộc N.2. Nếu n thuộc N, thì n+1 cũng thuộc N.3. Chỉ các số như điều 1 và 2 thì mới thuộc NQuy tắc đầu tiên thiết lập : tập N không trống; nó có thể nhiều hơn một phần tử. Đây là quy tắc cơ bản của định nghĩa một tập hợp. Quy tắc thứ hai là quy tắc quy nạp; nó luôn có dạng Nếu các phần tử này nằm trong tập hợp, thì phần tử khác này cũng nằm trong tập hợp. Quy tắc cuối cùng thường không được nêu ra, nhưng nó đóng vai trò loại trừ phần tử bất kỳ khỏi tập hợp ngoại trừ các phần tử được gộp là hệ quả của các quy tắc cơ bản và quy nạp.Mình đã đưa ra một VD quy nạp của tập N, ta thường có thể sử dụng chứng minh quy nạp để chứng minh một định lý có dạng: Với mọi n thuộc N, P(n); mọi phần tử của n đều có tính chất của P. Để hiển thị bằng quy nạp rằng tính chất của P(n) là chứa tất cả các số nguyên không âm, đối với tất cả các phần tử của N, chúng ta phải biểu diễn 2 điều: 1.P(0) là đúng. Nghĩa là P giữ trường hợp cơ sở. 2. Với mọi n thuộc N,P(n) => P(n+1). mọi n có thuộc tính của P thì n+1 cũng có thuộc tính của P.Bước đầu tiên được gọi là bước cơ sở chứng minh. Bước cơ sở xác định thuộc tính giữ tất cả các phần tử được đưa ra trong bước cơ sở định nghĩa của tập N. Bước thứ hai là bước quy nạp; bất cứ thứ gì có thể được xây dựng bằng cách sử dụng bước quy nạp của định nghĩa N đều có thuộc tính P. Lưu ý rằng một khi ta đã chỉ ra các bước cơ sở và quy nạp, ta có một công thức để chỉ ra rằng P(n) giữ bất kỳ n cụ thể nào. Vì P(0) giữ (cơ sở) , do đó là P(1) (P(n+1)); vì P(1) giữ, nên P(2); vì P(2) giữ, nên P(3) cũng vậy, v.v. Do đó, chúng ta có thể sử dụng công thức này để hiển thị trực tiếp nhưng rất dài dòng, có khi đến P(3625). Nhưng nếu chúng ta có thể chỉ ra phép tất suy, quy tắc suy luận toán học được gọi là Nguyên lý quy nạp toán học cho phép chúng ta kết luận.Với mọi n trong N, P (n).Để sử dụng quy nạp cho bài toán (n5n) ta quan sát như sau. Bước cơ sở là đúng bởi sự hiển nhiên:Với mọi n thuộc N, P(n). Thì:050 == 0và 0 chia hết cho 5.Bước quy nạp: Cách phổ biến nhất trình bày lập luận: Với mọi n, P(n) => Q(n)Lưu ý rằng phép tất suy này là đúng nếu P(n) là sai, do đó sự suy ra là đúng nếu chúng ta chỉ đơn giản chỉ ra rằng bất cứ khi nào P(n) là đúng, thì Q(n) là đúng. Trong trường hợp này, chúng tôi muốn chỉ ra rằng nếu n5 n chia hết cho 5, thì (n+1)5 (n+1) cũng chia hết cho 5. Để chứng minh điều này, trước tiên ta đánh giá (n+1)5 (n+1) và được:(n5 + 5n4 + 10n3 + 10n2 + 5n +1) (n + 1) Biến đổi ta được:(n5 n ) + 5(n4 + 2n3 + 2n2 + n) Biểu thức này là tổng của 2 biểu thức, biểu thức thứ 2 ta thấy hiển nhiên chia hết cho 5. Và nếu biểu thức bên trái đúng, thì biểu thức ban đầu chia hết cho 5 là đúng. Điều này chứng minh sự suy ra ở trên — nếu n5 n chia hết cho 5, thì (n+1)5 (n+1) cũng chia hết cho 5. Điều đó chứng minh cho suy luận: P(n) => P(n+1), đó chính xác là điều ta muốn chứng minh ở bước quy nạp, do đó ta có thể kết luận P(n) đúng với mọi n thuộc N. Đó là P(n)= n5 n chia hết cho 5 với mọi số nguyên n không âm.Nếu ta nắm được định nghĩa và cách thức hoạt động của đệ quy thì phương thức chứng minh bằng quy nạp vô cùng đơn giản. Để chứng minh hàm f đúng với mọi số nguyên n, đầu tiên ta chỉ ra rằng hàm f đúng với mỗi trường hợp cơ sở. Bước này đơn giản. Sau đó ta chỉ ra: nếu f đúng (nghĩa là, nếu nó thoả mãn điều kiện trước và sau) với mọi giá trị nhỏ hơn n, thì phương pháp cũng đúng với n. Do đó để chỉ ra rằng hàm f(n) luôn tính ra giá trị đúng, ta phải chỉ ra rằng nó cũng đúng với trường hợp cơ sở, và sau đó ta phải chỉ ra rằng giá trị được trả về bởi việc gọi hàm f(n) đúng nếu tất cả giá trị được trả về bởi việc gọi đến hàm f(k) là đúng, với điều kiện k < n. Ví dụ, để thấy được cách hàm factorial (hàm luỹ thừa) tính toán, đầu tiên ta phải chỉ ra kết quả được trả về bởi việc tính toán fact(0) là đúng, đó là, 0. Factorial: recursivepublic static int fact(int n){ Precondition: Dieu kien truocAssert.pre(n >= 0,argument must be >=0); Postcondition: returned value == n Dieu kien sauif (n == 0)return 1;elsereturn n fact(n1);}Nhìn vào code thấy nếu n == 0 thì hàm trả về 1, giá trị đúng cho 0. Để chỉ ra vế suy ra, ta chỉ cần chỉ ra nếu hàm factorial đúng với mọi giá trị cho đến n, thì nó cũng đúng với n+1. Ta thấy tiếp rằng nếu fact(n+1) là để gọi fact(n) thì n+1 phải lớn hơn 0. Do đó lệnh gọi đệ quy đến hàm fact(n)thì điều kiện đầu của hàm giữ. Hàm sau đó trả về tích của n+1 và kết quả của việc tính fact(n). Nếu fact(n) trả về n, thì fact(n+1) trả về (n+1), đúng theo như định nghĩa quy nạp của hàm luỹ thừa. Ta sẽ phải cẩn thận trong việc nêu điều kiện trước và sau cho hàm đệ quy cũng như ta nêu cho các vòng lặp.3Truy nguyên phương pháp đệ quyKỹ thuật truy nguyên chồng chéo là phương pháp tốt để theo dõi cách thức đệ quy hoạt động. Có một mẹo là bạn cứ nghĩ mỗi lần gọi đệ quy là một phương thức riêng biệt, do đó cần nhiều bản sao của phương thức đệ quy. Ví dụ, chương trình chính gọi fact(4).Tuần tự tiếp theo đó là truy nguyên ( lần theo lần gọi trước). Trong trường hợp này ta cần 5 bản sao của hàm factorial. Các tham số thực tế đã được thay thế cho các tham số chính thức trong hàm. 4Ví dụ về hàm đệ quyCấu trúc dữ liệu thường được định nghĩa theo cách đệ quy; ví dụ, một định nghĩa kinh điển của phép liệt kê là đệ quy.1.Một liệt kê rỗng chính là một liệt kê.2.Nếu a là một giá trị, và x là một liệt kê, thì a:x chính là một liệt kê. 3. Không có liệt kê nào khác.Tập hữu hạn được định nghĩa theo cách tương tự:1. Tập rỗng là tập hữu hạn.2. Nếu S là tập hữu hạn, và x là một phần tử, phép hợp S {x} là một tập hữu hạn.3. Ngoài ra không còn tập hữu hạn nào khác.Ta thường sẽ không đưa ra các định nghĩa đệ quy rõ ràng về các tập hợp và hàm bên dưới các phương thức đệ quy của chúng, nhưng điều này là cần thiết và khó khăn trong việc xây dựng các điều kiện trước và sau của hàm đệ quy thường phản ánh sự thiếu hiểu biết về các định nghĩa cơ bản. Trong phần này, mình sẽ đưa ra một số định nghĩa về các hàm đệ quy đơn giản được xác định trên các tập hợp con. Trong một số trường hợp, bước cơ sở của định nghĩa được khai báo cho các mảng con và trong các trường hợp khác, nó được định nghĩa trên các mảng con mà có một phần tử duy nhất. Đệ quy không thực sự phù hợp với hầu hết các ví dụ chúng tôi đưa ra trong phần này vì các vấn đề được giải quyết dễ dàng lặp đi lặp lại, nhưng việc sử dụng đệ quy làm cho cả chương trình và thuật toán cơ bản trở nên dễ hiểu.4.1Tìm số lớn nhất của mảng số nguyên nhập từ bàn phím.Hàm maxEntry trả về phần tử lớn nhất của mảng blo...hi được định nghĩa theo cách đệ quy. Trường hợp cơ sở là tập hợp các mảng con với một lần nhập đầu vào ; trường hợp quy nạp là tập hợp các mảng con với hai hay nhiều hơn số lần nhập đầu vào. Find the largest entry of an integer subarray.public static int maxEntry(int b, int lo, int hi){ PreconditionAssert.pre(0 P(n+1), xác điều ta muốn chứng minh bước quy nạp, ta kết luận P( n) với n thuộc N Đó P(n)= n5 - n chia hết cho với số nguyên n không âm Nếu ta nắm định nghĩa cách thức hoạt động đệ quy phương thức chứng minh quy nạp vô đơn giản Để chứng minh hàm f với số nguyên n, ta hàm f với trường hợp sở Bước đơn giản Sau ta ra: f (nghĩa là, thoả mãn điều kiện trước sau) với giá trị nhỏ n, phương pháp với n Do để hàm f(n) ln tính giá trị đúng, ta phải với trường hợp sở, sau ta phải giá trị trả việc gọi hàm f(n) tất giá trị trả việc gọi đến hàm f(k) đúng, với điều kiện k < n Ví dụ, để thấy cách hàm factorial (hàm luỹ thừa) tính tốn, ta phải kết trả việc tính tốn fact(0) đúng, là, 0! // Factorial: recursive public static int fact(int n) { // Precondition: Dieu kien truoc Assert.pre(n >= 0,"argument must be >=0"); // Postcondition: returned value == n! Dieu kien sau if (n == 0) return 1; else return n * fact(n1); } Nhìn vào code thấy n == hàm trả 1, giá trị cho 0! Để vế suy ra, ta cần hàm factorial với giá trị n, với n+1 Ta thấy tiếp fact(n+1) để gọi fact(n) n+1 phải lớn Do lệnh gọi đệ quy đến hàm fact(n)thì điều kiện đầu hàm giữ Hàm sau trả tích n+1 kết việc tính fact(n) Nếu fact(n) trả n!, fact(n+1) trả (n+1)!, theo định nghĩa quy nạp hàm luỹ thừa Ta phải cẩn thận việc nêu điều kiện trước sau cho hàm đệ quy ta nêu cho vòng lặp Truy nguyên phương pháp đệ quy Kỹ thuật truy nguyên chồng chéo phương pháp tốt để theo dõi cách thức đệ quy hoạt động Có mẹo bạn nghĩ lần gọi đệ quy phương thức riêng biệt, cần nhiều phương thức đệ quy Ví dụ, chương trình gọi fact(4).Tuần tự truy nguyên ( lần theo lần gọi trước) Trong trường hợp ta cần hàm factorial Các tham số thực tế thay cho tham số thức hàm // Main // Main public int fact(int 4) Call to fact(4) x = fact(4); x = { if (4==0) return 1; else return 4*fact(3); } // Main x = public int fact(int 4) { i e } public int fact(int 3) { if (3==0) return 1; else return 3*fact(2); Call to fact(2) } // Main public int fact(int 4) x = { i e } public int fact(int 3) { i e } public int fact(int 2) { if (2==0) return 1; else return 2*fact(1); } Call to fact(1) Call to fact(3) // Main public int fact(int 4) x = { i e } public int fact(int 3) { i e } public int fact(int 2) { i e } public int fact(int 1) { if (1==0) return 1; else return 1*fact(0); Call to fact(0) } // Main public int fact(int 4) x = { i e } public int fact(int 3) { i e } public int fact(int 2) { i e } public int fact(int 1) { i e } public int fact(int 0) { if (0==0) return 1; else return 0*fact(0); } Returns // Main public int fact(int 4) x = { i e } public int fact(int 3) { i e } public int fact(int 2) { i e } public int fact(int 1) { if (1==0) return 1; else return 1*1; Returns 1*1==1 } // Main public int fact(int 4) x = { i e } public int fact(int 3) { i e } public int fact(int 2) { if (2==0) return 1; else return 2*1; } Returns 2*1==2 // Main public int fact(int 4) x = { i e } public int fact(int 3) { if (3==0) return 1; Returns 3*2==6 else return 3*2; } // Main x = public int fact(int 4) { if (4==0) return 1; else return 4*6; Returns 4*6==24 } // Main x = 24; x is assigned 24 Ví dụ về hàm đệ quy Cấu trúc liệu thường định nghĩa theo cách đệ quy; ví dụ, định nghĩa kinh điển phép liệt kê đệ quy Một liệt kê rỗng liệt kê Nếu a giá trị, x liệt kê, a:x liệt kê Khơng có liệt kê khác Tập hữu hạn định nghĩa theo cách tương tự: Tập rỗng tập hữu hạn Nếu S tập hữu hạn, x phần tử, phép hợp S {x} tập hữu hạn Ngồi khơng tập hữu hạn khác Ta thường không đưa định nghĩa đệ quy rõ ràng tập hợp hàm bên phương thức đệ quy chúng, điều cần thiết khó khăn việc xây dựng điều kiện trước sau hàm đệ quy thường phản ánh thiếu hiểu biết định nghĩa Trong phần này, đưa số định nghĩa hàm đệ quy đơn giản xác định tập hợp Trong số trường hợp, bước sở định nghĩa khai báo cho mảng trường hợp khác, định nghĩa mảng mà có phần tử Đệ quy không thực phù hợp với hầu hết ví dụ chúng tơi đưa phần vấn đề giải dễ dàng lặp lặp lại, việc sử dụng đệ quy làm cho chương trình thuật tốn trở nên dễ hiểu 4.1 Tìm số lớn nhất của mảng số ngun nhập từ bàn phím Hàm maxEntry trả phần tử lớn mảng b[lo hi] được định nghĩa theo cách đệ quy Trường hợp sở tập hợp mảng với lần nhập đầu vào ; trường hợp quy nạp tập hợp mảng với hai hay nhiều số lần nhập đầu vào // Find the largest entry of an integer subarray public static int maxEntry(int[] b, int lo, int hi) { // Precondition Assert.pre(0