Nhƣ đã nĩi trên một chƣơng trình con trong Pascal cĩ thể gọi về chính nĩ. Một lời gọi nhƣ thế gọi là một lời gọi đệ qui (recursion). Gọi đệ qui là một kỹ thuật lập trình rất quan trọng vì nĩ thƣờng ngắn gọn và thƣờng … phù hợp với suy nghĩ tự nhiên về nhiều cách giải bài tốn. Thậm chí nhiều bài tốn hầu nhƣ chỉ cĩ thể dùng đệ qui. Tuy nhiên xét về tốc độ giải thuật cũng nhƣ tối ƣu khơng gian bộ nhớ thì đệ qui thƣờng khơng phải là một giải pháp tốt. Ngƣời ta thƣờng cố gắng khắc phục đệ qui bằng cách dùng vịng lặp và sử dụng stack nhƣng đĩ là cơng việc khơng mấy dễ dàng.
Ví dụ 1:
Định nghĩa giai thừa của một số nguyên khơng âm m nhƣ sau: 1 if (m=0) or (m=1) !
m*(m-1)! if (m 2)
m
Lập trình để tính giai thừa của một số nguyên khơng âm nhập từ bàn phím. Cách 1: Dùng đệ qui.
Function GT(m: Integer): Longint; Begin If ( m = 0 ) or ( m = 1 ) then GT := 1 Else GT := m * GT( m-1 ); End;
Rõ ràng cách viết đệ qui là “phù hợp một cách tự nhiên” với định nghĩa của giai thừa. Việc thực thi một lời gọi đệ qui diễn ra tƣơng tự nhƣ sau:
Ví dụ ta truyền vào giá trị m = 4, tức gọi GT(4). GT(4) m = 4 Tính 4 * GT(4-1) gọi GT(3) GT(3) m = 3 Tính 3 * GT(3-1) gọi GT(2) GT(2) m = 2 Tính 2 * GT(2-1) gọi GT(1) GT(1) m = 1 Gán GT(1):=1
Cuối cùng một quá trình “tính ngƣợc” sẽ cho phép trả về giá trị của GT(4): GT(4) 4 * (3 * (2 * GT(1))).
Cách 2: Dùng vịng lặp.
Function GiaiThua(m: longint): longint; Var Tam, Dem:Longint;
BEGIN
IF (M<0) THEN Begin
Write(‘Khong tinh duoc’); HALT(1); End
ELSE Begin Tam:=1;
For Dem:=1 to m do Tam:=Tam*Dem; GiaiThua:=Tam;
End; END;
Lưu ý: Một chƣơng trình con đệ qui bao giờ cũng cĩ ít nhất hai phần:
- Phần gọi đệ qui. Trong ví dụ trên là GT:=m*GT(m-1).
- Phần “neo”. Trong ví dụ trên là IF (m=0) or (m=1) THEN GT:=1. Phần này rất quan trọng vì nĩ đảm bảo quá trình đệ qui phải dừng sau một số hữu hạn bƣớc. Quên phần này sẽ xảy ra lỗi làm tràn bộ nhớ stack (stack overflow) khi xảy ra quá trình đệ qui. Ví dụ 2:
Số Fibonacci đƣợc định nghĩa nhƣ sau:
Fibo(n)= 1 if (n=1) or (n=2)
( 1) ( 1) if (n 3)
Fibo n Fibo n
Chúng ta thấy bản thân định nghĩa số Fibonacci đã chứa một biểu thức truy hồi, tức về mặt lập trình đã dẫn tới một gợi ý lời gọi đệ qui. Chúng ta cĩ thể xây dựng một hàm tính số Fibonacci nhƣ sau:
Cách 1: (Dùng đệ qui)
FUNCTION FIBO(n: word): word; BEGIN IF (n=1) or (n=2) THEN FIBO:=1 ELSE FIBO := FIBO(n-1)+FIBO(n-2); END;
Trong cách này việc xây dựng hàm tính số Fibonacci tƣơng đối dễ dàng vì cách viết hồn tồn đồng nhất với định nghĩa tốn học. Ví dụ thứ hai này phức tạp hơn ví dụ thứ nhất vì lời gọi đệ qui chia làm hai nhánh.
Cách 2: (Dùng chỗ lưu trữ tạm) FUNCTION FIBO(n:word):word; Var Counter,F1,F2:word; BEGIN F1:=1; F2:=1; Fibo:=1; FOR Counter:=3 TO n DO Begin Fibo:=F1+F2; F1:=F2; F2:=Fibo; End; END;
Trong cách 2 này việc khử đệ qui khơng cịn dễ dàng nữa vì cách đĩ khơng chứa rõ ràng một qui tắc tổng quát cho phép xử lí.
Ví dụ 3:
Bài tốn tháp Hà Nội:
Cĩ 3 cái cọc, đánh dấu A, B, C, và N cái đĩa. Mỗi đĩa đều cĩ một lỗ chính giữa để đặt xuyên qua cọc, các đĩa đều cĩ kích thƣớc khác nhau. Ban đầu tất cả đĩa đều đƣợc đặt ở cọc thứ nhất theo thứ tự đĩa nhỏ hơn ở trên.
Trang 35
Yêu cầu: Chuyển tất cả các đĩa từ cọc A qua cọc C với ba ràng buộc nhƣ sau: 1. Mỗi lần chỉ chuyển đƣợc một đĩa.
2. Trong quá trình chuyển đĩa cĩ thể dùng cọc cịn lại để làm cọc trung gian. 3. Chỉ cho phép đặt đĩa cĩ bán kính nhỏ hơn lên đĩa cĩ bán kính lớn hơn.
Trong bài tốn trên hình dung một lời giải tổng quát cho trƣờng hợp tổng quát N đĩa là khơng dễ dàng. Hãy bắt đầu với các trƣờng hợp đơn giản.
N = 1. Lời giải trở thành tầm thƣờng (nhƣng khơng kém phần quan trọng đâu!). Đơn giản là chuyển đĩa này từ cọc A qua cọc C là xong.
N = 2. Để đảm bảo ràng buộc thứ hai ta bắt buộc chuyển đĩa trên cùng từ cọc A qua cọc B. Chuyển tiếp đĩa cịn lại từ cọc A qua cọc C. Chuyển tiếp đĩa đang ở cọc B sang cọc C. N=3. Ta phải thực hiện 7 bƣớc nhƣ sau:
Trạng thái ban dầu ---- --
---
Bƣớc 1: Chuyển một đĩa từ A qua C. --- ---- --
Bƣớc 2: Chuyển một đĩa từ A qua B. --- ---- --
Bƣớc 3: Chuyển một đĩa từ C qua B. --- ---- --
Bƣớc 4: Chuyển một đĩa từ A qua C. ------ ---
Bƣớc 5: Chuyển một đĩa từ B qua A. -- ---- ---
Bƣớc 6: Chuyển một đĩa từ B qua C. -- --- ----
Bƣớc 7: Chuyển một đĩa từ A qua C. --- ---- --
Hãy quan sát kết quả ở bƣớc thứ ba. Đây là một kết quả quan trọng vì nĩ cho ta thấy từ trƣờng hợp N=3 bài tốn đã đƣợc phân chia thành hai bài tốn với kích thƣớc nhỏ hơn: đĩ là bài tốn chuyển 1 đĩa từ cọc A qua cọc C lấy cọc B làm trung gian và bài tốn chuyển 2 đĩa (dời) từ cọc B sang cọc C lấy cọc A làm trung gian. Hai bài tốn con này đã biết cách giải (trƣờng hợp N=1 và trƣờng hợp N=2).
Nhận xét đĩ cho ta gợi ý trong trƣờng hợp tổng quát:
Bƣớc 1: Dời (N-1) đĩa trên cùng từ cọc A sang cọc B lấy cọc C làm trung gian.
Bƣớc 2: Chuyển 1 đĩa dƣới cùng từ cọc A sang cọc C.
Bƣớc 3: Dời (N-1) đĩa đang ở cọc B sang cọc C lấy cọc A làm trung gian.
Bài tốn đối với N đĩa nhƣ vậy đƣợc “đệ qui” về hai bài tốn (N-1) đĩa và bài tốn 1 đĩa. Quá trình đệ qui sẽ dừng lại khi N=0 (khơng cịn đĩa để dời hoặc chuyển).
PROGRAM ThapHanoi; Uses crt; TYPE Cot = Char; {---} Procedure Chuyen(X,Y:Cot); BEGIN Writeln(X,‟ -> „,Y); END; {---} Procedure Doi(N:byte; A,B,C:Cot);
{Dời N đĩa từ cọc A sang cọc C lấy cọc B làm trung gian} BEGIN
IF (N>0) THEN Begin
Doi(N-1,A,C,B); {Dời N-1 đĩa từ cọc A sang cọc B lấy cọc C làm trung gian} Chuyen(A,C);
Doi(N-1,B,A,C); {Dời N-1 đĩa từ cọc B sang cọc C lấy cọc A làm trung gian} End;
END;
{---} BEGIN
Clrscr;
Write(„Cho biet so dia :‟); Readln(Sodia); Writeln(„Cac buoc thuc hien:‟); Doi(Sodia,‟A‟,‟B‟,‟C‟);
Writeln; Writeln(„Thuc hien xong!‟); READLN; END.
Nếu áp dụng chƣơng trình này cho trƣờng hợp N=3 ta cĩ quá trình gọi đệ qui nhƣ sau:
Doi(0,A,C,B) Doi(1,A,B,C) Chuyen(A,C) Doi(0,B,A,C) Doi(2,A,C,B) Chuyen(A,B) Doi(0,C,B,A) Doi(1,C,A,B) Chuyen(C,B) Doi(0,A,C,B) Doi(3,A,B,C) Chuyen(A,C) Doi(0,B,A,C) Doi(1,B,C,A) Chuyen(B,A) Doi(0,C,B,A) Doi(2,B,A,C) Chuyen(B,C) Doi(0,A,C,B) Doi(1,A,B,C) Chuyen(A,C) Doi(0,B,A,C)
Ví dụ này cho thấy việc kết xuất ở các câu lệnh Chuyen(X,Y) chỉ xảy ra khi tồn bộ các lời gọi đệ qui đã đƣợc thực hiện và cũng cho thấy thứ tự các lời gọi đệ qui lần cuối cùng. Nhận xét này rất quan trọng khi bạn viết thủ tục đệ qui vì lẽ bạn cần phải hình dung trƣớc thứ tự các kết xuất nhất là khi lời gọi đệ qui cĩ rất nhiều nhánh.
Trang 37
Chƣơng 5: UNIT