III. CÁC TRƯỜNG HỢP KHỬ ĐỆ QUY ĐƠN GIẢN
3. Khử đệ quy một số dạng thủ tục đệ quy thường gặp
a) Dẫn nhập.
Để thực hiện một chương trình con đệ quy thì hệ thống phải tổ chức vùng lưu tr. thỏa quy tắc LIFO (vùng Stack). Vì vậy chỉ những ngôn ngữ lập trình có khả năng tạo vùng nhớ stack mới cho phép tổ chức các chương trình con đệ quy. Thực hiện một chương trình con đệ quy theo cách mặc định thường tốn bộ nhớ vì cách tổ chức Stack một cách mặc định thích hợp cho mọi trường hợp thường là không tối ưu trong từng trường hợp cụ thể. Vì vậy sẻ rất có ích khi người lập trình chủ động tạo ra cấu trúc dữ liệu stack đặc dụng cho từng chương trình con đệ quy cụ thể .
Phần tiềp theo sẻ trình bày việc khử đệ quy một số dạng thủ tục đệ quy theo hướng thay giải thuật đệ quy bằng các vòng lặp và một cấu trúc dữ liệu kiểu stack thích hợp .
b) Thủ tục đệ qui chi có một lệnh gọi đệ 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)
1 - 46 -
else begin
end ;
Với :
X là một bién đơn hoặc biến véc tơ. C(X) là một biểu thức boolean của X .
A(X) , B(X) , D(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 thủ tục P(X) sẻ là : + Nếu C(X) đúng thì thực hiện D(X) .
+ Còn không ( 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 ) .
Gỉa sử qúa 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 k thao tác B theo thứ tự : B(fk-1(X)) , B(fk-2(X)) , . . . ,B(f(f(X))) ,B(f(X)),B(X).
Để thực hiện dãy thao tác { B(fi(X)) } theo thứ tự ngược với thứ tự phát sinh ta cần dãy dữ liệu {fi(X) } truy xuất theo nguyên tắc LIFO. Ta sẻ dùng một Stack để lưu trử dãy { fi(X) } = { X , f(X) , f(f(X)) , . . . , fi(X) , . . . , fk-1(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 = f(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=fk-1(X) )
C(U) = True D(U )
1 - 47 -
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 gía 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ó dạng : 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;
}
1 - 48 -
c) Nhiều lệnh gọi đệ quy trực tiếp.
c1) Thủ tục đệ quy với 2 lần gọi trực tiếp Thủ tục đệ quy 2 lần gọi trực tiếp có dạng :
P(X) = if C(X) then D(X) else begin A(X) ; P(f(X)) ; B(X) ; P(g(X)) ; end ; Qúa 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ì thự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 gía trị fi(X) ta con phải lưu vào stack các gía trị gi(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 gía 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 là ngoài dữ liệu X , con phải lưu vào ngăn xếp 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à :
{ Creat_Stact (S) : Push (S, (X,1)) ; Repeat
A(X) ; Push (S, (X,2)) ; X := f(X) ; end ; D(X) ; POP (S, (X,k)) ; if ( k <> 1) then begin B(X) ; X := g(X) ; end ; until ( k = 1 ) ; } 1 - 49 - Ví dụ : Khử đệ quy thủ tục Tháp Hà Nội . + Dạng đệ quy của thủ tục Tháp Hà Nội là : THN(n , X , Y, Z ) = if( n > 0 ) then begin THN ( n - 1 , X , Z , Y ) ;
Move ( X , Z ) ;
THN ( n - 1 , Y , X , Z ) ; end ;
Với n là số đĩa , X là cột đầu , Z là cột cuối , Y là cột giữa ,Move(X,Z) là thao tác chuyển 1 đĩa từ cột X tới cột Z .
Trong trường hợp này : Biến X là bộ ( n , X , Y , Z ) .
C(X) là biểu thức boolean ( n < = 0 ) . D(X) , A(X) là thao tác rỗng .
B(X) = B(n,X,Y,Z) là thao tác Move(X,Z) ; f(X) = f(n ,X ,Y ,Z) = (n - 1 , X , Z , Y) . g(X) = g(n ,X , Y, Z ) = (n - 1 , Y ,X , Z ) . Giải thuật không đệ quy tương đương là : { Creat_Stack (S) ; Push (S ,(n,X,Y,Z,1)) ; Repeat While ( n > 0 ) do begin Push (S ,(n,X,Y,Z,2)) ; n := n - 1 ;
Swap (Y,Z ) ; (* Swap(a,b) là thủ tục hoán end ; đổi nội dung 2 biến a ,b *)
POP (S,(n,X,Y,Z,k)) ; if ( k <> 1 ) then begin Move (X ,Z ) ; n := n - 1 ; Swap (X ,Y ) ; end ; until ( k = 1 ) ; }
c2) Trường hợp n lần gọi đệ quy trực tiếp . Thủ tục đệ quy trong trường hợp này có dạng :
1 - 50 - P(X) = if C(X) then D(X) else begin A1(X) ; P(f1(X)) ; A2(X) ; P(f2(X)) ; ... Ai(X) ; P(fi(X)) ; ... An(X) ; P(fn(X)) ; An+1(X) ; end ;
Cũng giống như trong trường hợp (3a) là khi quay trở lại sau khi thực hiện một lần đệ quy, cần biết đó là lệnh gọi thuộc nhóm thứ mấy trong dãy lệnh gọi để biết thao tác cần thực hiện tiếp. Vì vậy trong chồng cần giữ thêm vị trí nhóm lệnh gọi . Dạng lặp tương ứng là :
{ Creat_Stack (S) ; Push(S,(X,1)) ; Repeat
While (not C(X) ) do begin A1(X) ; Push (S,(X,2)) ; X := f1(X) ; end ; D(X) ; POP(S,(X,k)) ; While( k = n+1 ) do begin An+1 ; POP(S,(X,k)) ; end ; if ( k > 0 ) then begin Ak(X) ; Push (S,(X,k+1)); X := f k (X) end ; until (k = 1 ) ; }
Ví dụ : Khử đệ quy cho thủ tục hoán vị . + Thủ tục hoán vị dưới dạng đệ quy :
1 - 51 -
HVI(V ,n) = if (n = 1 ) then Print ( V ) else for i := n downto 1 do
Swap (V[n],V[i] ) ; HVI(V ,n - 1) : end ;
trong trường hợp này thì :
X là bộ (V ,n ) . (* vector V và số nguyên n *) C(X) là ( n = 1 ) .
D(X) là Print (V) . (* xuất vector V *)
Ai(X) là thủ tục Swap(V[n] ,V[i] ) ( i = 1 .. n ) . An+1 là thao tác rỗng . fi(X) = f(V, n ) = ( V, n - 1) .( với i = 1 . . n ) Dạng lặp của thủ tục là : { Creat_Stack (S) ; Push (S,(V ,n ,1)) ; Repeat While ( n > 1 ) do begin Swap(V[n] ,V[1] ; Push (S ,V , n ,2) ; n := n -1 ; end ; Print (V) ; POP (S ,(V ,n ,k)) ; While ( k = n +1 ) do POP(S ,(V ,n ,k) ; if(k <> 1 ) then begin
Swap(V[n] ,V[k]) ; Push (S ,(V ,n ,k+1) ; n := n - 1 ; end ; until(k = 1 ) ; 1 - 52 - PHẦN II KIỂM CHỨNG CHƯƠNG TRÌNH CHƯƠNG IV CÁC KHÁI NIỆM