Như chúng ta đã biết phương pháp đệ quy không phải bao giờ cũng là giải pháp hữu hiệu nhất. Cứ mỗi lần gọi đệ quy máy phải dành một số vùng nhớ để lưu trữ các trị, các thông số và biến cục bộ. Do đó, đệ quy tốn nhiều vùng nhớ, thời gian truyền đối mục, thiết lập vùng nhớ trung gian và trả về kết quả Vậy để khắc phục hạn chế đó người ta nói đến khử đệ quy. Khử đệ quy ở đây có nghĩa là biến một thủ tục đệ quy thành một thủ tục chỉ chứa vòng lặp mà không ảnh hưởng gì đến các yếu tố khác, chứ không phải là thay đổi thuật toán. Ví dụ như trong các hàm đệ quy tính n! và số Fibonaci F(n) ta có thể thay bằng một vòng lặp để tính. Trong trường hợp tổng quát, khử đệ quy là một việc làm khá phức tạp và khó khăn. Có thể bạn sẽ nói rằng, vậy thì cứ sử dụng đệ quy, vừa ngắn gọn, dễ hiểu, vừa dễ cài đặt. Nhưng có đôi khi, sự hạn hẹp của bộ nhớ dành cho chương trình con không cho phép chúng ta làm điều đó, hoặc chúng ta biết rằng, ngôn ngữ máy không có đệ quy, vì vậy các trình biên dịch đều phải có nhiệm vụ khử đệ quy. Và một lí do nữa là bạn có thể thực sự gặp rắc rối với thủ tục đệ quy của mình khi trong một môi trường lập trình mà không cung cấp khả năng gọi đệ quy
41 trang |
Chia sẻ: maiphuongdc | Lượt xem: 12466 | Lượt tải: 1
Bạn đang xem trước 20 trang tài liệu Đề tài Đệ quy và cách khử đệ quy, để xem tài liệu hoàn chỉnh bạn click vào nút DOWNLOAD ở trên
giải thuật đệ quy có thể dẫn tới một tiến trình gọi đệ quy không kết thúc khi đó không có khả năng gặp trường hợp neo, vì vậy quan tâm đến điều kiện dừng của một giải thuật đệ quy luôn được đặt ra. Để kiểm soát quá trình gọi đệ quy của giải thuật đệ quy P người ta thường gắn thao tác gọi P với việc kiểm tra một điều kiện B xác định và biến đổi qua mỗi lần gọi P, quá trình gọi P sẽ dừng khi P không còn thỏa mãn.
Mô hình tổng quát của một giải thuật đệ quy với sự quan tâm đến điều kiện dừng sẽ là:
P if B then P [ S, P ] hoặc P P [ S, if B then P ]
Thông thường với giải thuật đệ quy P để đảm bảo P sẽ dừng sau n lần gọi ta chọn B là ( n > 0). Mô hình giải thuật đệ quy khi đó có dạng:
P ( n ) if ( n > 0 ) then P [ S , P ( n -1 ) ]; hoặc
P ( n ) P [ S , if ( n > 0) then P ( n – 1 ) ] ;
Trong các ứng dụng thực tế số lần gọi đệ quy ( độ sâu đệ quy ) không những phải hữu hạn mà còn phải đủ nhỏ, bởi vì mỗi lần gọi đệ quy sẽ cần một vùng nhớ mới trong khi vùng nhớ cũ vẫn phải duy trì.
1.2.2 Ví dụ:
Xét bài toán tìm một từ trong từ điển :
Ta có thuật toán dưới dạng giả mã như sau:
BEGIN
IF ( từ điển chỉ có một trang ) THEN ( tìm từ trong trang này )
ELSE
BEGIN
+ ( chia đôi từ điển );
+ ( xác định xem nửa nào của từ điển chứa từ cần tìm)
IF ( từ đó nằm ở nửa trước của từ điển )
THEN ( tìm từ đó trong nửa trước )
ELSE ( tìm từ đó trong nửa sau );
END;
END.
Ta thấy sau mỗi lần từ điển được tách đôi thì một nửa thích hợp sẽ lại được tìm kiếm bằng một cách như đã dùng trước đó. Có một trường hợp đặc biệt, khác với mọi trường hợp trước, sẽ đạt được sau nhiều lần tách đôi, đó là trường hợp từ điển chỉ còn duy nhất một trang. Lúc đó việc tách đôi ngừng lại và bài toán đã đủ nhỏ để ta có thể giải quyết trực tiếp bằng cách tìm từ mong muốn trên trang đó, chẳng hạn bằng cách tìm tuần tự. Trường hợp đặc biệt này gọi là trường hợp suy biến.
1.3 Ưu nhược điểm của đệ quy:
1.3.1 Ưu điểm:
Công cụ mạnh, diễn đạt tư duy rất rõ ràng, chặt chẽ.
Ngắn gọn, có khả năng định nghĩa một tập hợp rất lớn các đối tượng bằng một số các câu lệnh hữu hạn.
Làm chương trình trông đẹp mắt, dễ đọc, dễ hiểu và chương trình được nêu bật rõ ràng hơn.
1.3.2 Nhược điểm:
Đệ quy tốn bộ nhớ nhiều hơn khi không đệ quy vì cứ mỗi lần gọi đệ quy lại cần thêm một vùng nhớ mới trong khi vùng nhớ cũ vẫn được duy trì.
Tốc độ thực hiện chương trình chậm hơn khi không đệ quy.
Chương II : CHƯƠNG TRÌNH CON ĐỆ QUY
2.1 Các phương pháp xây dựng chương trình con đệ quy:
2.1.1 Định nghĩa chương trình con đệ quy:
Định nghĩa:
Một chương trình con (hàm hoặc thủ tục) được gọi là đệ quy nếu trong quá trình thực hiện nó có phần phải gọi đến chính nó.
2.1.2 Cấu trúc của một chương trình con đệ quy:
Cấu trúc chính của một chương trình con đệ quy gồm hai phần: Phần cơ sở và phần đệ quy.
Phần cơ sở: Chứa tác động của hàm hoặc thủ tục với một số giá trị cụ thển ban đầu của tham số.Nó bao gồm các trường hợp dừng mà có thể được giải quyết ngay (trường hợp duy biến).
Phần đệ quy: Định nghĩa giá trị cần tác động cho giá trị hiện thời các tham số bằng các tác động đã được định nghĩa trước đây với các kích thước tham số nhỏ hơn (Là phần trong thuật toán có yêu cầu gọi đệ quy,tức là yêu yêu cầu thực hiện lại thuật toán ở cấp độ nhỏ hơn (điều kiện kiểm soát đệ quy)).
- Ví dụ : Hàm tính giai thừa của số tự nhiên n ( tính n! ).
Ta có chương trình con đệ quy sau:
Function gt ( n: byte ) : longint;
Begin
If n= 0 then gt:= 1 ( phần cơ sở )
Else gt:= n* gt ( n - 1); ( phần đệ quy )
End;
Trong ví dụ trên, qui trình thực hiện như sau:
Khi có lệnh gọi hàm, chẳng hạn:
x := gt(3);
thì máy sẽ ghi nhớ là:
gt(3) := 3 * gt(2); và đi tính gt(2)
kế tiếp máy lại ghi nhớ:
gt(2) := 2 * gt(1); và đi tính gt(1)
Theo định nghĩa của hàm thì:
gt(1) := 1;
Máy sẽ quay ngược lại:
gt(2) := 2 * 1; cho kết quả là 2
Tiếp tục:
gt(3) := 3 * 2; cho kết quả là 6
Như vậy kết quả cuối cùng trả về là 6. Ta có: 3! = 6.
Một ví dụ khác như sau: Ta xây dựng chương trình con tính số fibonaci.
Function Fibo ( n: integer) : integer ;
Begin
If ( n = 1) or ( n = 2 ) then Fibo := 1
( phần cơ sở)
Else Fibo := Fibo ( n - 1) + Fibo ( n - 2) ;
( phần đệ quy);
End;
2.2 Phương pháp xây dựng chương trình con đệ quy:
Để xây dựng giải thuật một bài toán có tính đệ quy bằng phương pháp đệ quy ta cần thực hiện tuần tự 3 nội dung sau:
Thông số hóa bài toán.
Tìm các trường hợp neo cùng giải thuật giải tương ứng.
Tìm giải thuật giải trong trường hợp tổng quát bằng phân rã bài toán theo kiểu đệ quy.
Thông số hoá bài toán.
Tổng quát hóa bài toán cụ thể cần giải thành bài toán tổng quat (một họ các bài toán chúa bài toán cần giải), tìm ra các thông số cho bài toán tổng quat đặc biệt là nhóm các thông số biểu thị kích thước của bài toán-các thông số điểu khiển (các thông số mà độ lớn của chúng đặc trưng cho độ phức tạp của bài toán, và giảm đi qua mỗi lần gọi đệ quy).
Ví dụ: n trong hàm FAC(n); a,b trong hàm USCLN(a,b).
Phát hiện các trường hợp suy biến (neo) và tìm giải thuật cho các trường hợp này.
Đây là các trường hợp suy biến của bài toán tổng quat, là các trường hợp tương ứng với các giá trị biên cuả các biến điều khiển (trường hợp kích thước bài toán nhỏ nhất), mà giải thuật giải không đệ qui (thường rất đơn giản).
Ví dụ:
FAC(1)=1, USCLN(a,0)=a
Phân rã bài toán tổng quát theo phương thức đệ quy.
Tìm phương án (giải thuật) giải bài toán trong trường hợp tổng quat bằng cách phân chia nó thành các thành phần mà hoặc có giải thuật không đệ quy hoặc là bài toán trên nhưng có kích thước nhỏ hơn.
Ví dụ: FAC(n)=n*FAC(n-1).
Đệ quy là một công cụ cho phép người lập trình tập trung vào bước chính yếu của giải thuật mà không phải lo lắng tại thới điểm khởi đầu về cách kết nối bước chính yếu này với các bước khác. Khi cần giải quyết một vấn đề, bước tiếp cận đầu tiên nên làm thường là xem xét một vài ví dụ đơn giản, và chỉ sau khi đã hiểu được chúng một cách kỹ lưỡng, chúng ta mới thử cố gắng xây dựng một phương pháp tổng quát hơn. Một vài điểm quan trọng trong việc thiết kế một giải thuật đệ quy được liệt kê sau đây:
Tìm bước chính yếu. Hãy bắt đầu bằng câu hỏi “Bài toán này có thể được chia nhỏ như thế nào?” hoặc “Bước chính yếu trong giai đọan giữa sẽ được thực hiện như thế nào?”. Nên đảm bảo rằng câu trả lời của bạn đơn giản nhưng có tính tổng quat. Không nên đi từ điểm khởi đầu hay điểm kết thúc của bài toán lớn, hoặc sa vào quá nhiểu trường hợp đặc biệt (do chúng chỉ phù hờp vói các bài toán nhỏ). Khi đã có được một bước nhỏ và đơn giản để hướng tới lời giải, hay tự hỏi rằng những khúc mắc còn lại của bài toán có thể được giải quyết bằng cách tương tự hay không, để sủa lại phương pháp của bạn cho tổng quát hơn, nếu cần thiết. Ngoại trừ những định nghĩa toán học thể hiện sự đệ quy quá rõ ràng, một điều thú vị mà chúng ta sẽ lần lượt gặp trong những chương sau la, khi những bài toán cần được giải quyết trên những cấy trúc dữ liệu mà định nghĩa mang tính chất đệ quy nhu danh sách, chuỗi ký tự biểu diễn biểu thức số học, cây, hay đồ thi… thì giải pháp hướng tới một giải thuật đệ quy là rất dễ nhình thấy.
Tìm điều kiện dừng. Điều kiện dừng chỉ ra rằng bài toán hoặc một phần nào đó của bài toán đã được giải quyết. Điều kiện dừng thường là trường hợp nhỏ, đặc biệt, có thể được giải quyết một cách dễ dàng không cần đệ quy.
Phác thảo giải thuật. Kết hợp điểu kiện dừng với bước chính yếu của bài toán, sử dụng lệnh if để chọn lụa giũa chúng. Đến đây thì chúng ta có thể viết hàm đệ quy, trong đó mô tá cách mà bước chính yếu được tiến hành cho đến khi gặp được điểu kiện dùng. Mỗi lần gọi đệ quy hoặc là phải giải quyết một phần của bài toán khi gặp một trong các điều kiện dừng, hoặc là phải giảm kích thước bài toán hướng dần đến điều kiện dừng.
Kiểm tra sự kết thúc. Kế tiếp, và cũng là điều tối quan trọng, là phải chắc chắn việc gọi đệ quy sẽ không bị lặp vô tận. Bắt đầu từ một trường hợp chung, qua một số bước hữu hạn, chúng ta cần kiểm tra liệu điều kiện dừng có khả năng xảy ra để quá trình đệ quy kết thúc hay không. Trong bất kỳ một giải thuật nào, khi một lần gọi hàm không phải làm gì, nó thường quay về một cách êm thấm. Đối với giải thuật đệ quy, điều này rất thường xảy ra, do việc gọi hàm mà không phải làm gì thường là một điều kiện dừng. Do đó, cần lưu ý rằng việc gọi hàm mà không làm gì thường không phải là một lỗi trong trường hợp của hàm đệ quy.
Kiểm tra lại mọi trường hợp đặc biệt. Cuối cùng chúng ta cũng cần bảo đảm rằng giải thuật của chúng ta luôn đáp ứng mọi trường hợp đặc biệt.
Vẽ cây đệ quy. Công cụ chính để phân tích các giải thuật đệ quy là cây đệ quy. Như chúng ta đã thấy trong bài toán Tháp Hà Nội, chiểu cao của cây đệ quy kiên quan mật thiết đến tổng dung lượng bộ nhớ mà chương trình cần đến, và kích thước tổng cộng của cây phản ánh số lần thực hiện bước chính yếu và cũng là tổng thời gian chay chương trình. Thông thường chúng ta nên vẽ cây đệ quy cho một hoặc hai trường hợp đơn giản của bài toán của chúng ta vì nó sẽ chỉ dẫn cho chúng ta nhiều điều
2.3 Cách thực hiện của đệ quy:
Khi nghiên cứu về phần này chúng ta phải lưu ý câu hỏi về cách thực hiện một chương trình đệ quy trong máy tính cần được tách rời khỏi câu hỏi về sử dụng đệ quy để thiết kế giải thuật.
Trong giai đoạn thiết kế, chúng ta nên sử dụng mọi phương pháp giải quyết vấn đề mà chúng tỏ ra thích hợp với bài toán, đệ quy là một trong các công cụ hiệu quả và linh hoạt này.
Trong giai đoạn thực hiện, chúng ta cần tìm xem phương pháp nào trong số các phương pháp sẽ là tốt nhất so với từng tình huống. Có ít nhất hai cách để hiện thực đệ quy trong hệ thống máy tính. Quan điểm chính của chúng ta khi xem xét hai cách hiện thực khác nhau dưới đây là, cho dù có sự hạn chế về không gian và thời gian chúng cũng nên được tách riêng ra khỏi quá trình thiết kế giải thuật, các loại thiết bị tính toán khác nhau. Chúng ta sẽ tìm hiểu hai cách hiện thực đa xử lý và đơn xử lý dưới đây.
2.3.1 Hiện thực đa xử lý:xử lý đồng thời.
Có lẽ rằng cách suy nghĩ tự nhiên về quá trình hiện thực của đệ quy là các hàm không chiếm những phần riêng trong cùng một máy tính, mà chúng sẽ được thực hiện trên những máy khác nhau, Bằng cách này, khi một hàm cần gọi một hàm khác, nó khởi động chiếc tương ứng, và khi máy này kết thúc công việc, nó sẽ trả về chiếc máy ban đầu kết quả tính được để chiếc máy ban đầu có thể tiếp tục công việc. Nếu một hàm gọi đệ quy chính nó hai lần, đơn giản nó chỉ cần khởi động hai chiếc máy khác để thực hiện những dòng lệnh y như những dòng lệnh mà nó đang thực hiện. Khi hai máy hoàn tất công việc chúng trả kết quả về cho máy gọi chúng. Nếu chúng cần gọi đệ quy, dĩ nhiên chúng cũng khởi động những chiếc máy khác nữa.
Thông thường bộ xử lý trung ương là thành phần đắt nhất trong hệ thống máy tính, nên bất kỳ ý nghĩ nào về một hệ thống có nhiều hơn một bộ xử lý cũng cần phải xem xét đến xự lãng phí song song sẽ trở nên bình thường.
Với đa xử lý, nhưng người lập trình sẽ không còn xem xét các giải thuật chỉ như một chuỗi tuyến tính các hành động, thay vào đó, cần phải nhận ra một số phần của giải thuật có thể thực hiện song song. Cách xử lý này còn được gọi là xử lý đồng thời (concurrent). Việc nghiên cứu về xử lý đồng thời và các phương pháp kết nối giữa chúng hiện tại là một đề tài nghiên cứu trong khoa học máy tính, một điều ,chắc chắn là nó sẽ cải tiến cách mà giải thuật sẽ được mô ta và hiện thực trong nhiều năm tới.
2.3.2 Hiện thực đơn xử lý: vấn đề vùng nhớ.
Để xem xét làm cách nào mà đệ quy có thể được thực hiện trong một hệ thống chỉ có một bộ xử lý chúng ta nhớ lại cơ cấu ngăn xếp của các lần gọi hàm đã được giới thiệu ơ đầu chương. Một hàm khi được gọi phải có một vùng nhớ riêng để chứa các biến cục bộ và các tham trị của nó, kể cả các trị trong các thanh ghi và địa chỉ quay về khi nó chuẩn bị gọi một hàm khác . Sau khi hàm kết thúc , nó sẽ không còn cần đến bất kỳ thứ gì trong vùng nhớ dành riêng cho nó nữa. Thực sự là không có sự khác nhau giữa việc gọi một hàm đệ quy và việc gọi một hàm không đệ quy. Khi một hàm chưa kết thúc , vùng nhớ của nó sẽ bất khả xâm phạm. Một lần gọi hàm đệ quy cũng chưa là một lần gọi hàm riêng biệt . Chúng ta cần chú ý rằng hai lần gọi đệ quy là hoàn toàn khac nhau , để chung ta không lẫn vùng nhớ của chúng khi chúng chưa kết thúc. Đối với hàm đệ quy , những thông tin lưu trữ dành cho lần gọi ngoài cần giữ được cho đến khi nó kết thúc , như vậy một lần gọi bên trong phải sử dụng một vùng nhớ khác làm vùng nhớ riêng của nó .
Đối với một hàm không đệ quy , vùng nhớ có thể là vùng nhớ cố định và được dành cho lâu dài, do chúng không biết rằng một lần gọi hàm sẽ được trả về trước khi hàm có thể lại được gọi lần nữa,và sau khi gọi trước được trả về , các thông tin trong vùng nhớ của nó không cần thiết nữa. Vùng nhớ lâu dài được dành sẵn cho các hàm không đệ quy có thể gây lẵng phí lớn, do nhưng khi hàm không được yêu cầu thực hiện , vùng nhớ đó không thể được sử dụng vào mục đích khác. Đó cũng là cách quản lý vùng nhớ dành cho các hàm của các phiên bản cũ cuả các ngôn ngữ như FORTRAN và COBOL, và chính điều này cũng là lý do mà các ngôn ngữ này không cho phép đệ quy.
Sau đây là một số chương trình con đệ quy được minh họa như sau:
BÀI TOÁN THÁP HÀ NỘI
Thuật giải đệ quy
đặt tên các cọc là A, B, C -- những tên này có thể chuyển ở các bước khác nhau (ở đây: A = Cột Nguồn, B = Cột Đích, C = Cột Trung Gian)
gọi n là tổng số đĩa
đánh số đĩa từ 1 (nhỏ nhất, trên cùng) đến n (lớn nhất, dưới cùng)
Để chuyển n đĩa từ cọc A sang cọc B thì cần:
chuyển n-1 đĩa từ A sang C. Chỉ còn lại đĩa #n trên cọc A
chuyển đĩa #n từ A sang B
chuyển n-1 đĩa từ C sang B cho chúng nằm trên đĩa #n
Phương pháp trên được gọi là thuật giải đệ quy: để tiến hành bước 1 và 3, áp dụng lại thuật giải cho n-1. Toàn bộ quá trình là một số hữu hạn các bước, vì đến một lúc nào đó thuật giải sẽ áp dụng cho n = 1. Bước này chỉ đơn giản là chuyển một đĩa duy nhất từ cọc A sang cọc C.
Ngôn ngữ Pascal biểu diễn giải thuật trên là:
VAR n: Integer;
Procedure chuyen(sodia: Integer; CotNguon: Char; CotDich: Char; CotTG: Char);
Begin
If sodia>0 then begin
chuyen(sodia-1, CotNguon, CotTG, CotDich);
Writeln(CotNguon,'->',CotDich); { Dia lon nhat hien tai }
chuyen(sodia-1, CotTG, CotDich, CotNguon)
End;
End;
BEGIN
Write('Hay nhap so dia: '); Readln(n);
chuyen(n,'A','C','B');//Thực hiện chuyển từ cột A sang cột C với cột B làm trung gian
Readln;
END.
3.1-Vấn đề khử đệ quy:
Như chúng ta đã biết phương pháp đệ quy không phải bao giờ cũng là giải pháp hữu hiệu nhất. Cứ mỗi lần gọi đệ quy máy phải dành một số vùng nhớ để lưu trữ các trị, các thông số và biến cục bộ. Do đó, đệ quy tốn nhiều vùng nhớ, thời gian truyền đối mục, thiết lập vùng nhớ trung gian và trả về kết quả…Vậy để khắc phục hạn chế đó người ta nói đến khử đệ quy. Khử đệ quy ở đây có nghĩa là biến một thủ tục đệ quy thành một thủ tục chỉ chứa vòng lặp mà không ảnh hưởng gì đến các yếu tố khác, chứ không phải là thay đổi thuật toán. Ví dụ như trong các hàm đệ quy tính n! và số Fibonaci F(n) ta có thể thay bằng một vòng lặp để tính. Trong trường hợp tổng quát, khử đệ quy là một việc làm khá phức tạp và khó khăn. Có thể bạn sẽ nói rằng, vậy thì cứ sử dụng đệ quy, vừa ngắn gọn, dễ hiểu, vừa dễ cài đặt. Nhưng có đôi khi, sự hạn hẹp của bộ nhớ dành cho chương trình con không cho phép chúng ta làm điều đó, hoặc chúng ta biết rằng, ngôn ngữ máy không có đệ quy, vì vậy các trình biên dịch đều phải có nhiệm vụ khử đệ quy. Và một lí do nữa là bạn có thể thực sự gặp rắc rối với thủ tục đệ quy của mình khi trong một môi trường lập trình mà không cung cấp khả năng gọi đệ quy. Khử đệ quy giúp bạn vẫn giữ được nguyên bản thuật toán đệ quy của mình mà không hề có lời gọi đệ quy, và như thế chương trình có thể chạy được trong bất kỳ môi trường lập trình nào.
Khử đệ quy thực chất là chúng ta phải làm công việc của một trình biên dịch đối với một thủ tục, đó là: Đặt tất cả các giá trị của các biến cục bộ và địa chỉ của chỉ thị kế tiếp vào ngăn xếp (Stack), quy định các giá trị tham số cho thủ tục và chuyển tới vị trí bắt đầu thủ tục, thực hiện lần lượt từng câu lệnh. Sau khi thủ tục hoàn tất thì nó phải lấy ra khỏi ngăn xếp địa chỉ trả về và các giá trị của các biến cục bộ, khôi phục các biến và chuyển tới địa chỉ trả về.
Các bước gợi ý trong việc khử đệ quy một cách tổng quát:
Chúng ta có thể tạo một ngăn xếp để chứa các bản ghi, lệnh gọi đệ quy và lệnh trả về từ hàm đệ quy có thể được thay thế như sau, mỗi lệnh gọi đệ quy có thể được thay bởi:
Đưa vào ngăn xếp một bản ghi chứa các biến cục bộ, các thông số và vị trí dòng lệnh ngay sau lệnh gọi đệ quy.
Gán mọi thông số về các trị mới thích hợp.
Trở về thực hiện dòng lệnh đầu tiên trong giải thuật đệ quy.
Mỗi lệnh trả về của hàm đệ quy được thay bởi:
Lấy từ ngăn xếp để khôi phục mọi biến cục bộ và thông số.
Bắt đầu thực hiện dòng lệnh tại vị trí mà trước đó đã được cất trong ngăn xếp.
3.2 Một số phương pháp khử đệ quy:
Trong thực tế các bài toán về đệ quy rất phong phú , không phải bài toán đệ quy nào cũng đều có thể đưa về dạng khử đệ quy một cách chuẩn mực. Nên việc cài đặt các giải thuật khử đệ quy đòi hỏi phải hiểu rõ được tính chất đệ quy của bài toán, trên cơ sở đó lựa chọn ra giải thuật phù hợp.
Để thực hiện giải thuật khử đệ quy trong kĩ thuật lập trình ta có thể sử dụng thuật lặp, nhưng phổ biến là dùng cấu trúc dữ liệu kho đẩy (stack) để ghi nhớ được các bước xử lí, nhờ đó có thể phục hồi các bước xử lí trước. Việc sử dụng stack để thể hiện giải thuật khử đệ quy cũng y hệt như công việc mà một trình biên dịch phải làm khi dịch một chương trình đệ quy thành ngôn ngữ máy.
3.2.1 Khử đệ quy bằng vòng lặp:
3.2.1.1 Hàm tính giá trị của dãy dữ liệu mô tả bằng hồi quy:
Ý tưởng:
Xét một vòng lặp trong đó sử dụng một tập hợp biến W = ( V , U ) gồm tập hợp U các biến bị thay đổi trong vòng lặp và V là các biến còn lại.
Dạng tổng quát của vòng lặp là: W:=Wo ; {Wo = (Uo, Vo) }
While C(U) do U:=g(W) (1)
Gọi Uo là trạng thái của U ngay trước vòng lặp, Uk với k>0 là trạng thái của U sau lần lặp thứ k (giả sử còn lặp đến lần k). Ta có:
Uo mang các giá trị được gán ban đầu.
Uk =g(W) =g(Uk-1 ,Vo) =f( Uk-1 ) với k=1..n (2)
Với n là lần lặp cuối cùng, tức C(Uk) đúng với mọi k<n, C(Un) sai.
Sau vòng lặp W mang nội dung (Un,Vo).
Ta thấy để tính giá trị dãy được định nghĩa bởi quan hệ hồi quy dạng (2) ta có thể dùng giải thuật lặp mô tả bởi đoạn lệnh (1).
Giải thuật tính giá trị của dãy hồi quy thường gặp dạng:
= g(n, f(n-1)) khi n > no
Vi dụ:
Xây dựng hàm FAC(n) = n! không đệ quy:
Function FAC (n: integer) : longint ;
Var k: integer ;
F: longint;
Begin
F:=1; k:=0;
While ( k<n ) do
begin
k :=k+1;
F :=F*k ;
end;
FAC :=F ;
End;
Hoặc:
Function FAC ( n: integer): longint;
Var k: integer;
F:longint;
Begin
F: =1;
For k: =1 to n do
F: =F *k;
FAC: =F;
End;
3.2.1.2 Các thủ tục khử đệ quy dạng đệ quy đuôi:
Xét thủ tục P dạng:
P(X) if B(X) then D(X)
Else
{ A(X) ;
P(f(X));
}
Trong đó: X là tập biến (một hoặc một bộ nhiều biến).
P(X) là thủ tục đệ quy phụ thuộc X.
A(X),D(X) là các nhóm thao tac ( lệnh ) không đệ quy.
f(X) là hàm biến đổi X.
Xét quá trình thi hành P(X):
Gọi Po là lần gọi P thứ 0(đầu tiên) P(X)
P1 là lần gọi P thư 1( lần hai) P(f(X))
Pi là lần gọi P thứ i (lần i+1) P(f(f(…(f(X)..)))
P(fi(X)) hợp i lần hàm f.
Trong lần gọi Pi nếu B(fi(X)) không đúng (false) thì thi hành lệnh A và gọi Pi+1,nếu B(fi(X)) đúng (true) thì thi hành lệnh D và kết thúc quá trình gọi .
Giả sử P được gọi đúng n+1 lần.khi đó ở trong lần gọi cuối cùng (thứ n) Pn thì B(fn(X)) đúng,lệnh D được thi hành và chấm dứt thao tac gọi thủ tục P.
Sơ đồ khối quá trình thực hiện lệnh gọi thủ tục P(X) có dạng:
Tương ứng với vòng lặp sau:
While ( not B(X) ) do
Begin
A(X) ;
X := f(X) ;
End;
D(X) ;
Ví dụ: tìm ƯSCLN của hai số nguyên dựa vào thuật toán Euclide.
Ta có : giải thuật đệ quy (dưới dạng thủ tục) tìm ƯSCLN(m,n) bằng thuật toán Euclide là: ƯSCLN( m,n,var us) if ( n=0) then us:= m
else ƯSCLN(n,m mod n,us) ;
Trong trường hợp này thì: X là (m , n, us )
P(X) là USCL(m, n, us)
B(X) là n = 0
D(X) là lệnh gán us := m
A(X) là lệnh rỗng
F(X) là f(m, n, us) = (n, m mod n, us)
Đoạn lệnh lặp tương ứng là:
While (n 0 ) do begin
Sd := m mod n;
m := n;
n := sd;
end;
us := n;
*Thủ tục không đệ quy tương ứng trong Pascal
Procedure USCL (m, n : integer ; var us : integer );
Var sd : integer ;
Begin
While ( n 0 ) do begin
Sd := m mod n ;
m := n ;
n := sd;
end;
us := m ;
end;
3.2.1.3 Các hàm khử đệ quy dạng đệ quy đuôi ( tail-recusive ).
Xét hàm đệ quy dạng :
F(X) = - f(g(X)) khi C (X) đúng
- a(X) khi C(X) sai
Tức là :
F(X) if (C(X)) then return ( f(g(X)))
Else return (a(X))
Ví dụ : Tìm ƯSCLN của hai số m và n nguyên (m,n>=0):
Với m, n >= 0 ta có hàm đệ quy tính USCLN (m, n) là:
USCLN (m, n) if (m 0 ) then return (USCLN (abs(m – n),(m, n);
Else return n;
Trong trường hợp này :
X là (m, n) ;
C(X) = C(m, n) là m 0;
g(X) = g(m, n) = (abs(m – n) , (m, n));
a(X) = a(m, n) = n;
Đoạn chương trình tính USCLN (a,b) là:
m := a; n :=b;
while m 0 do begin
t1 := m;
t2 := n;
m := abs(t1 – t2);
n := min (t1, t2);
end;
USCLN := n;
Ta có hàm không đệ quy tương ứng trong Pascal.
Function USCLN ( m, n : integer ) : integer;
Var t1, t2 : integer;
Begin
While (n 0 ) do begin t1 := m ; t2 := n ;
m := abs( t1 – t2 );
if ( t1 < t2 ) then n := t1
else n := t2 ;
end;
USCLN := m;
Khử đệ quy một số dạng thủ tục đệ quy thường gặp:
Thủ tục đệ quy chỉ có một lệnh gọi là đệ quy trực tiếp.
Mô hình tổng quát của thủ tục đệ quy chỉ có một lệnh gọi đệ quy trực tiếp là:
P(X) if C(X) then D(X)
Else begin
A(X);
P(f(X)) ;
B(X);
End;
Với:
+ X là một biến đơn hoặc là một biến véc tơ.
+ C(X) là một biểu thức boolean của X.
+ A(X), P(f(X), B(X) là các nhóm lệnh không đệ quy (không chứa lệnh gọi đến P).
+ f(X) là hàm của X.
* Tiến trình thực hiện của P(X) sẽ là:
- Nếu C(X) đúng thì thực hiện D(X).
-Nếu ( C(X) sai) thì thực hiện A(X); gọi P(f(X)) thực hiện B(X).(B(X) chỉ được thực hiện khi P(f(X)) thực hiện xong).
Mỗi lần thành phần đệ quy P(Y) được gọi thì thông tin giải thuật B(Y) lại được sinh ra (nhưng chưa thực hiện).
Giả sử quá trình đệ quy kết thúc sau k lần gọi đệ quy thì ta phải thực hiện một dãy các thao tác B theo thứ tự : B(f(X)), B(f(X))…. B(f(f(X))),B(f(X)), B(X).
Để thực hiện dãy thao tác { B(f(X))} theo thứ tự ngược với thứ tự phát sinh ta cần dãy dữ liệu {f(X)} truy xuất theo nguyên tắc LIFO (last in first out). Ta sẽ dùng một stack để lưu trữ dãy {f(X)} { X, f(f(X)), … f(X) ….f(X) }
Trình tự thực hiện P(X) được diễn tả bằng mô hình sau :
P(X)
C(X) = false A(X); Push(S,X); U:=f(X);P(U); POP(S,U); B(U)
( U = X )
C(U) = False A(U); Push(S,U); U := f(U); P(U) ; POP(S,U);B(U)
(U = X)
C(U) = False A(U); Push(S,U); U:=f(U)); P(U); POP(S,U);B(U)
---------------------------------------------------------------------------------------------------
C(U) = False A(U) ; Push(S,U); U:= f(U)); P(U); POP(S,U);B(U)
(U = f(X))
C(U) = True D(U)
Giải thuật thực hiện P(X) với việc sử dụng Stack có dạng :
P(X) { Creat_Stack (S) ; ( tạo stack S )
While(not(C(X)) do begin A(X);
Push (S,X) ; ( cất giá trị X vào stack S)
X := f(X) ;
End;
D(X) ;
While (not(EmptyS(S))) do begin
Pop(S,X) ; (lấy dữ liệu từ S)
B(X);
End;
}
Ví dụ :
Thủ tục đệ quy chuyển biểu diễn số từ cơ số thập phân sang nhị phân có dang:
Binary(m) if(m>0) then begin
Binary ( m div 2 );
write ( m mod 2 );
end;
Trong trường hợp này:
X là m.
P(X) là Binary(m).
A(X); D(X) là lệnh rỗng.
B(X) là lệnh write(m mod 2 ) ;
C(X) là ( m <= 0 ).
f(X) = f(m) = m div 2.
Giải thuật thực hiện Binary(m) không đệ quy là :
Binary(m) { Creat_Stack (S) ;
while (m > 0 ) do begin
Sdu := m mod 2 ;
Push (S,sdu);
m := m div 2 ;
end;
while ( not (emptyS(S)) do begin
POP(S,sdu);
Write(sdu);
End;
}
Nhiều lệnh gọi đệ quy trực tiếp:
Thủ tục đệ quy với hai lần gọi trực tiếp:
P(X) if C(X) then D(X)
Else begin
A(X); P(f(X));
B(X); P(g(X));
end;
Quá trình thực hiện thủ tục P(X) sẽ là :
-Nếu C(X) đúng thì thực hiện D(X).
-Nếu C(X) sai thì tực hiện A(X), gọi P(f(X)) ;thực hiện B(X); gọi P(g(X)),khi gọi P(g(X)) thì lại phát sinh lệnh A(g(X)),như vậy ngoài việc phải lưu vào stack các giá trị f i(X) ta còn phải lưu vào các giá trị g i(X) tương ứng. khi ta lấy dữ liệu từ stack để thực hiện lệnh B(U) mà chưa gặp điều kiện kết thúc thì ta thực hiện P(g(U)) và lại phải lưu giá trị g(U) vào stack …Điều kiện dừng là khi truy xuất tới phần tử lưu đầu tiên trong stack.
Như vậy,ngoài dữ liệu X còn phải lưu vào stack thêm thứ tự lần gọi (cụm gọi).
*Thuật toán khử đệ quy tương ứng với thủ tục đệ quy P(X) là:
Begin
Creat _stack (S);
Push (S,(X,1)) ;
Repeat
While ( not C(X)) do
Begi
Các file đính kèm theo tài liệu này:
- de_tai_toan_roi_rac_5425.doc