1 ) 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ữ LT nào cĩ khả năng tạo vùng nhớ stack thì mới cĩ thể 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 nguyên thuỷ rất 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 stack cho từng chương trình con đệ quy .
Trong phần tiềp theo ta sẻ khử đệ quy cho một số dạng thủ tục đệ quy theo hướng này .
2 ) 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) ≡ else { A(X) ; P(f(X)) ; B(X) ; } Endif ; (III.2.1) Với : X là 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à 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ì B(Y) lại được sinh ra . 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 : B(X) , B(f(X)) , B(f(f(X))) , . . . , B(fi(X)), . . . , B(fk -1(X)) theo thứ tự B(fi(X) trước B(fi -1(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) ; ---> P(f(X)) ; Push(S,X) POP(S,X) ; B(X)
C(f(X)) = False A(f(X)) ;---> P(f(f(X)) ; Push(S,U=f(X)) POP(S,U) ; B(U)
C(f(U)) = False A(f(U)) ;---> P(f(f(U)) : Push(S,U=f(U)) POP(S,U) ; B(U)
--- -
--- -
C(f(U)) = True D(f(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 { A(X) ;
Push(S,X) ; (* cất gía trị X vào stack S *) X := f(X) ;
} Endwhile ;
D(X) ;
While(not(EmptyS(S))) do { POP(S,X) ; (* lấy dữ liệu từ S *) B(X) ;
} Endwhile ;
}
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 { Binary( m div 2 ) ; ≡
write( m mod 2 ) ; }
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 { sdu := m mod 2 ; Push(S,sdu) ;
m := m div 2 ; }
While( not(EmptyS(S)) do { POP(S,sdu) ; Write(sdu) ; }
}
3 ) Nhiều lệnh gọi đệ quy trực tiếp
3a) 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 { A(X) ; P(f(X)) ; B(X) ; P(g(X)) ; }
endif ; ( III.3a - 1)
Qúa trình thực hiện thủ tục (III.3a - 1) 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 ngồ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à ngồ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 tốn khử đệ quy tương ứng với thủ tục đệ quy (III.3a) là : { Creat_Stact (S) :
Push (S, (X,1)) ; Repeat
While ( not C(X) ) do { A(X) ;
Push (S, (X,2)) ; X := f(X) ; } D(X) ; POP (S, (X,k)) ; If ( k <> 1) then { B(X) ; X := g(X) ; } until ( k = 1 ) ;
}
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 { THN ( n - 1 , X , Z , Y ) ; ≡
Move ( X , Z ) ;
THN ( n - 1 , Y , X , Z ) ; }
Endif ;
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 { Push (S ,(n,X,Y,Z,2)) ; n := n - 1 ;
Swap (Y,Z ) ; (* Swap(a,b) là thủ tục hốn đổi } nội dung 2 biến a ,b *)
POP (S,(n,X,Y,Z,k)) ; If ( k <> 1 ) then { Move (X ,Z ) ; n := n - 1 ; Swap (X ,Y ) ; } until ( k = 1 ) ; }
3b) 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 : P(X) if C(X) then D(X) ≡
else { A1 (X) ; P(f1 (X)) ; A2 (X) ; P(f2 (X)) ; ... Ai (X) ; P(fi (X)) ;
... An (X) ; P(fn (X)) ; An +1 (X) ;
}
Endif ; (III.3b)
Cũng giống như trong trường hợp (III.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 { A1 (X) ; Push (S,(X,2)) ; X := f1 (X) ; } D(X) ; POP(S,(X,k)) ; While( k = n+1 ) do { An +1 ; POP(S,(X,k)) ; } If ( k > 0 ) then { Ak (X) ; Push (S,(X,k+1)); X := fk (X) } until (k = 1 ) ; } Ví du ï : Khử đệ quy cho thủ tục hốn vị . + Thủ tục hốn vị dưới dạng đệ quy :
HVI(V ,n) ≡ If (n = 1 ) then Print ( V )
else for i := 1 to n do
{ Swap (V[n],V[i] ) ; HVI(V ,n - 1) : }
Endif ; 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 .
Dạng lặp của thủ tục là : { Creat_Stack (S) ; Push (S,(V ,n ,1)) ; Repeat While ( n > 1 ) do { Swap(V[n] ,V[1] ; Push (S ,V , n ,2) ; n := n -1 ; } Print (V) ; POP (S ,(V ,n ,k)) ; While ( k = n +1 ) do POP(S ,(V ,n ,k) ; If(k <> 1 ) then { Swap(V[n] ,V[k]) ; Push (S ,(V ,n ,k+1) ; n := n - 1 ; } until(k = 1 ) ; } ____________________________________ $ 5 . BAØI TẬP 1) Tính các đệ qui sau : a) Hàm ACKERMANN : A(m,n) = if ( m = 0 ) then n+1
else if (n = 0 ) then A(m -1,1)
else A(m-1,A(m,n-1)) tính A(0,10) , A(4,0) , A(3,5)
b) f(n,b) = if (n = 0 ) then 0
else 10 * f(n div b,b) + n mod b Tính f(1,2) , f(6,7) , f(6,2) .
2) Tìm miền giá trị của các hàm trên.
3) Chuyển các hàm sau đây sang chương trình PASCAL và C++ : a) Hàm Eckermann
b) Hàm tính MAX của một dãy số thực c) Hàm tính SUM(tổng) của một dãy số thực
4) Hãy cho biết kết quả in ra của chương trình sau : program De_qui ;
uses crt ;
procedure second ( i : integer ) ; forward ; procedure first ( N : integer );
var j , k : integer ; begin for j := 1 to N do begin writeln('= ', j) ; k := n -2*j ; second (k) end end ; procedure second; begin if ( i > 0 ) then begin writeln ( ' i = ' , i ); first ( i -1 ) end end ; begin clrscr ; first(5) end . 5) Cho hàm sau :
function CQD(x,y : integer) : integer ; begin
if (x = 0 ) then CQD := 1
else CQD := x * CQD(x-1,CQD(x,y+1)) end ;
a) Tìm miền xác định của hàm .
b) Hãy so sánh với hàm sau dùng x như là biến tồn bộ: function CQD1 (y:integer) : integer ;
begin if (x = 0 ) then CQD1 := 1 else begin x := x -1; CQD1 := (x+1) * CQD1(y+1) end end
6) Hàm sau hoạt động thế nào : function count : integer ; var letter : char; begin
read (letter) ;
write letter < > '.' do begin
count := count +1 ; read(letter)
end end;
7) Hãy lập các định nghĩa hàm đệ qui trong ngơn ngữ PASCAL hoặc C++ và chương trình tương ứng bằng vịng lặp cho mỗi bài tốn sau
a) Tính số hạng thứ n rồi tổng n số hạng đầu tiên của các dãy số sau : a1) 1 , 4 , 7 , 10 , 13 , . . .
a2) 20 , 22 , 26 , 28 , 32 , . . . a3) 3 , 9 , 27 , 81 , . . .
a4) 25 , 5 , 16 , 4 , 9 , . . .
b) Cũng như bài a. Nhưng cho các dãy số được định nghĩa hồi qui như sau : b1) Tính gần đúng 1/x với 0< x < 1 bằng dãy số : Ao = 1 , Co = 1 - x Ai = Ai -1 * (1 + Ci -1 ) i > 0 Ci = Ci -1 * Ci - 1 i > 0 b2) Tính sqrt(x) với 0<x<2 bằng dãy số : ao = x , co = 1 - x ai = ai -1 * (1 + ci -1 /2) i > 0 ci = ci -1 * (3 + ci -1 )/4
( Thì lim an = sqrt(x) khi n dẫn tới vơ hạn và cn đại diện sai số ). c) Tính xấp xỉ sin(x) bằng :
sin(x) ~ x - x3 /3! + x5 /5! ... + (-1) 2*i -1 * xi -1 ) /(2*i-1)! ... d) Tính xấp xỉ cos(x) bằng :
cos(x) ~ 1 - x2/2! + x4 /4! ... e) Để tính giá trị của đa thức
F(x) = an xn + . . . + a2 x2 + a1 x + ao tại xo với ít phép tính ,người ta thường trình theo sơ đồ Horner:
F(xo) = (...((an * xo + an -1 ) * xo + an -2 ) * xo + . . . + a1 ) * xo + ao 8) Với mỗi bài tốn sau , hãy dùng quan niệm đệ qui để giải bài tốn rồi chuyển thành vịng lặp while .
a) cĩ 2 array a , b : array [1..n ] of 1..k -1 đang chứa các ký số của 1 số biểu diễn dưới dạng k phân với ký số thấp nhất ở vị trí chỉ số cao nhất .
Hãy cộng a với b rồi chứa trong c (hay chính a) . b) cũng như câu a nhưng là tốn nhân
c) array a , kèm với một số k biểu thị đa thức .
A(x) = a[0] + a[1] * x + a[2] * x2 + . . . + a[k] * xk Hãy tính đạo hàm bậc m của a(x) (chứa trong a)
d) cĩ hai đa thức A(x) , B(x) được biểu diễn như ở câu c . Hãy tính thương và dư Q(x) và R(x) trong phép chia A(x) cho B(x) .
Tức là : A(x) = Q(x) B(x) + R(x) với bậc R < bậc B 9). Hãy chuyển thành vịng lặp thủ tục sau :
procedure P(a : integer ; var b :real) ; begin if a > 0 then begin b := b+2 ; P(a -1,b) end end
10). Loại bỏ lệnh gọi đệ qui cuối trong bài tốn Tháp hà nội .
11). Loại bỏ lệnh gọi đệ qui cuối trong thủ tục sau (quicksort) nhằm xắp xếp thứ tự một mảng T
procedure quicksort (inf,sup : integer) ; var i : integer ;
begin
if (inf < = sup ) then begin
i := segment (inf,sup) ; quicksort (inf, i -1) ; quicksort (i+1, sup) end
end
Vĩi segment là một hàm tác động lên mảng T và cho về một giá trị chỉ số. Hàm này khơng cĩ lệnh gọi đệ qui .
12) Với mỗi trường hợp dưới đây,mỗi hàm sẽ được cho bằng cách nhận xét tính đệ qui của nĩ.Hãy viết định nghĩa hàm đệ qui tương ứng,nhận định tính chất của mỗi hàm này
Viết chương trình đê qui tương ứng bằng ngơn ngữ PASCAL (hoặc C++) , và sau đĩ viết chương trình dưới dạng khơng đệ qui tương ứng :
a) Phép nhân :
= a + TICH(a,b -1) nếu b>0 cách 2 : TICH(a,b) = 0 nếu b=0
= 2 * TICH(a,b div 2) + a * (b mod 2) nếu b>0 b) Tính USCLN của 2 số : hàm USCLN với hai đĩi số nguyên khơng âm a,b . cách 1 : USCLN(a,b) = USCLN(b,a)
= a nếu b = 0 = USCLN (b,a - b) nếu a > b cách 2 : USCLN(a,b) = USCLN (b,a)
= a nếu b = 0 = USCLN(b,a mod b) nếu a > b>0 13) Khử đệ quy cho thủ tục dạng a) P(x) begin ≡ if cd(x) then A(x) else begin B(x) ; P ; C(x) end; D(x) ; end b) P begin ≡ I(x) ; if cd(x) then A(x) else begin B(x) ; P ; C(x) end ; D(x) end ; c) P if cd(x) then begin ≡ B(x) ; P ; C(x) end; d) P if cd(x) then begin ≡ P ; C(x) end else A(x) ; e) P if ( i > 0 ) then repeat ≡ A ;
P ; B ; until cd ; else C ;
14) Khử đệ quy cho bài tốn tháp Hà nội 15) Khử đệ quy cho bài tốn SortMerge ________________________________
PHẦN III : KIỂM CHỨNG CHƯƠNG TRÌNH
$1 . CÁC GIAI ĐOẠN TRONG CUỘC SỐNG CỦA MỘT PHẦN MỀM
Việc sử dụng máy tính để giải quyết một bài tốn thực tế thường bao gồm nhiều việc. Trong các cơng việc đĩ cơng việc mà ta quan tâm nhất là việc xây dựng các hệ thống phần mềm (các hệ thống chương trình giải bài tốn ).
Để xây dựng một hệ thống phần mềm , người ta thường thực hiện trình tự các bước sau :
Bước 1 : Đặc tả bài tốn :
Gồm việc phân tích để nắm bắt rõ yêu cầu của bài tốn và diễn đạt chính xác lại bài tốn bằng ngơn ngữ thích hợp vừa thích ứng với chuyên ngành tin học vừa cĩ tính đại chúng ( dễ hiểu đối với nhiều người).
Bước 2 : Xây dựng hệ thống :
Trong bước này sẻ tuần tự thực hiện các cơng việc sau :
- Thiết kế : Xây dựng một mơ hình một hệ thống phần mềm cần cĩ. Trong bước này, cơng việc chủ yếu là phân chia hệ thống thành các module chức năng và xác định rõ chức năng cũng như mối tương tác của mỗi module với các module khác. Chức năng của mỗi module sẽ được định rõ bởi đặc tả của module.
- Triển khai từng module và thử nghiệm :
Viết chương trình thực hiện "đúng" các đặc tả đã được đặt ra, thường thì tính đúng này được thuyết phục bằng việc thử chương trình trên nhiều bộ dữ liệu thử. Để cĩ tính thuyết phục cao, người ta cần thử nghiệm càng nhiều lần càng tốt. Khi thử nếu cĩ sai thì phải sửa lại chương trình . Giai đoạn thử nghiệm này thường rất tốn thời gian và cơng sức.
Sau khi từng module hoạt động tốt, ngưịi ta cần thử sự hoạt động phối hợp của nhiều module, thư nghiệm tồn bộ hệ thống phần mềm.
Bước 3 : Sử dụng và bảo trì hệ thống :
Sau khi hệ thống phần mềm hoạt động ổn định, người ta đưa nĩ vào sử dụng. Trong quá trình sử dụng cĩ thể cĩ những điều chỉnh trong yêu cầu của bài tốn, hay phát hiện lỗi sai của chương trình. Khi đĩ cần xem lại chương trình và sửa đổi chúng.
Từ mơ tả trên, ta rút ra được các yêu cầu sau cho qúa trình xây dựng phần mềm: a) Cần xây dựng các chương trình dễ đọc, dễ hiểu và dễ sửa đổi. Điều này cần địi hỏi một phương pháp tốt khi xây dựng các hệ phần mềm : cần chia cắt tốt hệ thống; cần sử dụng các cấu trúc đơn giản, chuẩn và cĩ hệ thống khi viết chương trình; cần cĩ sưu liệu đầy đủ.
b) Cần đảm bảo tính đúng. Làm thế nào để xây dựng một chương trình "đúng" ?
Một chân lý đơn giản là :
- Phép thử chương trình chỉ cho khả năng phát hiện chương trình sai chứ khơng chứng minh được chương trình đúng .
- Khơng thể thử hết được mọi trường hợp .Ngay cả khi chương trình đơn giản .
Người ta luơn mong muốn chứng minh chương trình đúng bằng logic thay vì thử nghiệm chương trình.
Cĩ 2 cách chính được sử dụng để đảm bảo tính đúng của phần mềm trong quá trình xây dựng hệ thống (bước 2) :
- Viết chương trình rồi chứng minh chương trình đúng. - Vừa xây dựng vừa chứng minh tính đúng của hệ thống.
Việc tìm kiếm những phương pháp xây dựng tốt để cĩ thể vừa xây dựng vừa kiểm chứng tính đúng luơn là một chủ đề suy nghĩ của những người lập trình .
$2. ĐẶC TẢ
I . Đặc tả bài tốn :
1 . Khái niệm .
Khi cĩ một vấn đề ( một bài tốn) cần được giải quyết , người ta phát biểu bài tốn bằng một văn bản gọi là đặc tả bài tốn (problem specification).
Các bài tốn đặt ra cho những người làm cơng tác tin học thường cĩ dạng sau : Xây dựng một hệ thống xử lý thơng tin mà hoạt động của nĩ :
- Dựa trên các dữ kiện nhập thoả mãn những điều kiện nhất định. - Xẩy ra trong một khung cảnh mơi trường được hạn chế nhất định.
- Sẽ sản sinh ra một tập hợp dữ kiện xuất được quy định trước về cấu trúc và cĩ mối quan hệ với dữ kiện nhập và mơi trường được xác định trước .
Những khía cạnh trên được thể hiện trong đặc tả bài tốn (ĐTBT) . 2 . Tác dụng của đặc tả bài tốn .
- Là cơ sở để đặt vấn đề, để truyền thơng giữa những người đặt bài tốn và những người giải bài tốn .
- Là cơ sở để người giải bài tốn triển khai các giải pháp của mình . - Là cơ sở để người giải bài tốn kiểm chứng kết quả .