Kỹ thuật khử đệ quy Trần Đức Thiện Đệ quy tim nghiên cứu lý thuyết thực hành tính toán, thể nhiều sức mạnh có ưu điểm nhiều toán Tuy nhiên lại ngược với công việc thường làm: khử đệ quy, vấn đề có nhiều thú vị đáng để xem xét Khử đệ quy biến thủ tục đệ quy thành thủ tục chứa vòng lặp mà không ảnh hưởng đến yếu tố khác, thay đổi thuật toán Ví dụ hàm đệ quy tính n! số Fibonaci F(n) ta thay vòng lặp để tính; Đó phương pháp khử đệ quy mà muốn nói Trong trường hợp tổng quát, khử đệ quy việc làm phức tạp khó khăn hàm n! hay F(n) ta dùng thuật toán không đệ quy, số toán, đệ quy bắt buộc Bạn nói rằng, sử dụng đệ quy, vừa ngắn gọn dễ hiểu, vừa dễ cài đặt Nhưng có đôi khi, hạn hẹp nhớ dành cho chương trình không cho phép làm điều đó; biết rằng, ngôn ngữ máy đệ quy, trình biên dịch phải có nhiệm vụ khử đệ quy Và bạn thực gặp rắc rối với thủ tục đệ quy môi trường lập trình mà không cung cấp khả gọi đệ quy Khử đệ quy giúp bạn giữ nguyên thuật toán đệ quy mà lời gọi đệ quy, chương trình chạy môi trường lập trình Khử đệ quy thực chất phải làm công việc trình biên dịch thủ tục, là: Đặt tất giá trị biến cục địa thị vào ngăn xếp (Stack), quy định giá trị tham số cho thủ tục chuyển tới vị trí bắt đầu thủ tục, thực câu lệnh Sau thủ tục hoàn tất phải lấy khỏi ngăn xếp địa trả giá trị biến cục bộ, khôi phục biến chuyển tới địa trả Để dễ theo dõi lấy ví dụ với toán cụ thể toán duyệt Giả sử có nhị phân lưu trữ biến động t định nghĩa: type pnode = ^node; node = record inf : variable; { truong luu tru thong tin } l,r : pnode; end; var t : pnode; Xuất phát từ nút gốc t, cần duyệt qua hết theo thứ tự từ trái qua phải Chương trình đệ quy sau: procedure Try(t : pnode); begin if t nil then begin visit(t); Try(t^.l); Try(t^.r); end; end; Trước hết thấy lệnh gọi đệ quy thứ hai khử dễ dàng mã lệnh theo sau Khi lệnh thực thủ tục Try( ) gọi với tham số t^.r lệnh gọi kết thúc thủ tục Try hành kết thúc Chương trình viết lại sau dùng goto: procedure try(t : pnode); label 0; begin : if t = nil then exit; visit(t); try(t^.l); t := t^.r; goto 0; end; Đó kỹ thuật tiếng gọi khử đệ quy phần cuối Việc khử lần gọi đệ quy lại đòi hỏi phải làm nhiều việc Giống trình biên dịch phải tổ chức ngăn xếp (Stack) để lưu trữ biến cục bộ, tham số, sử dụng thủ tục: Push(t): Đặt biến t vào đỉnh Stack; Hàm pop: lấy giá trị đỉnh stack Hàm stackempty: Báo hiệu Stack rỗng giá trị trả có biến cục t nên nạp vào stack chưa xử lý bước lấy biến đỉnh stack để xử lý nút Chương trình khử lời gọi đệ quy thứ hai sau: procedure try(t : pnode); label 0,1,2; begin 0: if t = nil then goto 1; visit(t); push(t); t := t^.l; goto 0; : t := t^.r; goto 0; : if stackempty then exit; t := pop; goto 2; end; Thủ tục diễn giải thô ý tưởng để bạn dễ hiểu, thị goto rườm ra, viết lại cách có cấu trúc sau: procedure try(t : pnode); label 0; begin 0: while t nil begin visit(t); push(t^.r); t := t^.l end; if stackempty then exit; t := pop; goto 0; end; Bây giờ, loại bỏ hoàn toàn thị goto tránh trường hợp nạp nút rỗng vào stack ta có thủ tục duyệt không đệ quy chuẩn sau, bạn thấy chất không khác thủ tục đệ quy mấy: procedure try(t : pnode); begin push(t); repeat t := pop; visit(t); if t^.l nil then push(t^.l); if t^.r nil then push(t^.r); until stackempty; end; Để minh hoạ cụ thể cho kỹ thuật này, xin trình bày với bạn chương trình xếp nhanh(QuickSort) khử đệ quy: Program Quick_sort_Khu_de_quy_Th; const inp = 'FileName.inp'; out = 'FileName.out'; maxstack = 1000; maxn = 1000; type it = longint; var a : array[1 maxn] of it; sl,sr : array[1 maxstack] of word; n,top : it; f : text; procedure push(l,r : word); begin inc(top); sl[top] := l; sr[top] := r; end; procedure pop(var l,r : word); begin l := sl[top]; r := sr[top]; dec(top); end; function stackEmpty : boolean; begin stackempty := top = 0; end; procedure init; begin top := 0; end; procedure nhap; var i : it; begin assign(f,inp); reset(f); readln(f,n); for i := to n read(f,a[i]); close(f); end; procedure sort(l1,r1 : word); var l,r,i,j : word; t,tg : it; begin push(l1,r1); repeat pop(l,r); i := l; j := r; t := a[(l+r) div 2]; repeat while a[i] < t inc(i); while t < a[j] dec(j); if i j; if i < r then push(i,r); if l < j then push(l,j); until stackEmpty; end; procedure xuat; var i : it; begin assign(f,out); rewrite(f); for i := to n write(f,a[i],' '); close(f); end; BEGIN nhap; init; sort(1,n); xuat; END Trong lúc khảo sát tính hiệu việc khử đệ quy Còn bạn trung thành với thủ tục Try đệ quy mình, thực ngắn gọn, dễ hiểu dễ cài đặt Dù không dùng đến nghiên cứu thêm việc không thừa Biết đâu sau bạn trở thành người viết chương trình dịch sao, − viết bổ ích cho bạn Chào thân hẹn gặp lại ... procedure try(t : pnode); label 0; begin : if t = nil then exit; visit(t); try(t^.l); t := t^.r; goto 0; end; Đó kỹ thuật tiếng gọi khử đệ quy phần cuối Việc khử lần gọi đệ quy lại đòi hỏi phải... hợp nạp nút rỗng vào stack ta có thủ tục duyệt không đệ quy chuẩn sau, bạn thấy chất không khác thủ tục đệ quy mấy: procedure try(t : pnode); begin push(t); repeat t := pop; visit(t); if t^.l... cho kỹ thuật này, xin trình bày với bạn chương trình xếp nhanh(QuickSort) khử đệ quy: Program Quick_sort_Khu _de_ quy_ Th; const inp = 'FileName.inp'; out = 'FileName.out'; maxstack = 1000; maxn