Tải bản đầy đủ (.pdf) (100 trang)

Giáo trình toán rời rạc phần 2 TS đỗ văn nhơn (biên soạn)

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (554.15 KB, 100 trang )

Chương 4:
I.

THUẬT TOÁN

Thuật toán và cách biểu diễn thuật toán

1.1 Khái niệm thuật toán
Thuật toán là một khái niệm cơ bản của Toán học và Tin học.
Khi viết một chương trình máy tính, người ta thường cài đặt một
phương pháp đã được nghó ra trước đó để giải quyết một vấn đề. Từ
“thuật toán” được dùng trong khoa học máy tính để để chỉ sự mô tả
một phương pháp giải bài toán thích hợp cho việc cài đặt thành các
chương trình nhờ các ngôn ngữ lập trình. Một thuật toán thường được
thể hiện bởi một thủ tục gồm một dãy hữu hạn bước mà theo đó ta sẽ
đạt đến lời giải cho bài toán. Người ta có thể trình bày thuật toán
bằng cách liệt kê ra các bước của thuật toán sử dụng ngôn ngữ tự
nhiên hay một ngôn ngữ qui ước nào đó chẳng hạn sử dụng một ngôn
ngữ lập trình nào đó gần với ngôn ngữ tự nhiên.
Ví dụ 1: thuật toán tìm phần tử lớn nhất trong một dãy hữu hạn
các số nguyên.
Bài toán tìm phần tử lớn nhất trong một dãy hữu hạn tương đối
tầm thường. Tuy nhiên đây là một trong những ví dụ khá tốt để minh
họa cho khái niệm về thuật toán. Có nhiều vấn đề mà trong đó đòi
hỏi phải tìm số nguyên lớn nhất trong một dãy số. Chẳng hạn như
việc tìm ra một học sinh có điểm cao nhất trong một kỳ thi, hay tìm
ra một nhân viên có năng suất cao nhất trong một xí nghiệp, v.v....

-120-



Chúng ta có nhiều cách để giải bài toán nầy. Một trong những
phương pháp để tìm phần tử lớn nhất trong một dãy số nguyên là
thực hiện một thủ tục theo các bước sau đây:
1. Trước hết ta đặt cho giá trò lớn nhất tạm thời bằng số nguyên
đầu tiên. (Giá trò lớn nhất tạm thời này chính là giá trò lớn nhất
ở mỗi giai đoạn của thủ tục.)
2. So sánh số nguyên kế tiếp trong dãy với giá trò lớn nhất tạm
thời, và nếu nó lớn hơn giá trò lớn nhất tạm thời thì đặt cho giá
trò lớn nhất tạm thời bằng số nguyên nầy.
3. Lặp lại bước 2 nếu còn số nguyên trong dãy chưa được xét tới.
4. Dừng nếu không còn số nguyên nào trong dãy chưa được xét.
Giá trò lớn nhất tạm thời lúc nầy chính là giá trò lớn nhất trong
dãy số.
Ví dụ 2: Thuật toán tính nghiệm của phương trình bậc hai: ax2 +
bx + c = 0 khi biết 3 hệ số a, b, c (a  0).
Bước 1: Tính giá trò  theo công thức
 = b2 - 4ac
Bước 2: Xét dấu , ta có kết quả tùy thuộc một trong 3 trường
hợp sau đây:
- Trường hợp  > 0: Phương trình có 2 nghiệm được tính theo
công thức
-b 
x =
2a
- Trường hợp  = 0: Phương trình có nghiệm kép được tính
theo công thức
-b
x =
2a
-121-



- Trường hợp  < 0: Phương trình vô nghiệm.
1.2 Biểu diễn thuật toán
Để trình bày một thuật toán hay biểu diễn một thuật toán, ta có thể
sử dụng các phương pháp biểu diễn thuật toán sau đây:
1. Dùng ngôn ngữ tự nhiên.
2. Dùng lưu đồ hay sơ đồ khối.
3. Dùng mã giả.
-

Lưu đồ:

Ngôn ngữ lưu đồ hay sơ đồ khối là một công cụ rất trực quan để
diễn đạt các thuật toán. Biểu diễn bằng lưu đồ sẽ giúp ta có được một
cái nhìn tổng quan về toàn cảnh của quá trình xử lý theo thuật toán.
Lưu đồ là một hệ thống các nút có hình dạng khác nhau, thể hiện
các chức năng khác nhau và được nối với nhau bởi các cung. Lưu đồ
được tạo thành bởi 4 thành phần chủ yếu sau đây:
1/ Nút giới hạn: được biểu diễn bởi hình ôvan có ghi chữ bên
trong như :

BẮT ĐẦU

KẾT THÚC

Các nút trên còn được gọi là nút đầu và nút cuối của lưu đồ.

-122-



2/ Nút thao tác: là một hình chữ nhật có ghi các lệnh cần thực
hiện. Ví dụ:

tăng k

3/ Nút điều kiện: thường là một hình thoi có ghi điều kiện cần
kiểm tra. Trong các cung nối với nút nầy có 2 cung ra chỉ hướng đi
theo 2 trường hợp: điều kiện đúng và điều kiện sai. Ví dụ:

4/ Cung: là các đường nối từ nút nầy đến nút khác của lưu đồ.
Hoạt động của thuật toán theo lưu đồ được bắt đầu từ nút đầu
tiên. Sau khi thực hiện các thao tác hoặc kiểm tra điều kiện ở mỗi
nút thì bộ xử lý sẽ theo một cung để đến nút khác. Quá trình thực
hiện thuật toán dừng khi gặp nút kết thúc hay nút cuối.
Trong giáo trình nầy chúng ta chủ yếu là sử dụng ngôn ngữ tự nhiên
và mã giả để trình bày thuật toán. Trong cách sử dụng ngôn ngữ tự
nhiên ta sẽ liệt kê các bước thực hiện các thao tác hay công việc nào
đó của thuật toán bằng ngôn ngữ mà con người sử dụng một cách phổ
thông hàng ngày. Các thuật toán được trình bày trong hai ví dụ trên
-123-


chính là cách biểu diễn thuật toán dùng ngôn ngữ tự nhiên. Mặc dù
cách biểu diễn nầy khá tự nhiên và không đòi hỏi người viết thuật
toán phải biết nhiều quy ước khác, nhưng nó không thể hiện rõ tính
cấu trúc của thuật toán nên không thuận lợi cho việc thiết kế và cài
đặt những thuật toán phức tạp. Hơn nữa trong nhiều trường hợp việc
biểu diễn thuật toán bằng ngôn ngữ tự nhiên tỏ ra dài dòng và dẽ gây
ra sự nhầm lẫn đối với người đọc. Còn việc sử dụng lưu đồ sẽ rất

cồng kềnh đối với các thuật toán phức tạp.
-

Mã giả:

Để biểu diễn thuật toán một cách hiệu quả, người ta thường dùng mã
giả (pseudocode). Theo cách nầy, ta sẽ sử dụng một số qui ước của
một ngôn ngữ lập trình, chẳng hạn là ngông ngữ lập trình PASCAL,
nhất là các cấu trúc điều khiển của ngôn ngữ lập trình như các cấu
trúc chọn, các cấu trúc lặp.
Trong mã giả ta còn sử dụng cả các ký hiệu toán học, các biến,
và đôi khi cả cấu trúc kiểu thủ tục. Cấu trúc thuật toán kiểu thủ tục
thường được sử dụng để trình bãy các thuật toán đệ qui hay các thuật
toán quá phức tạp cần phải được trình bày thành nhiều cấp độ.
Cùng với việc sử dụng các biến, trong thuật toán rất thường gặp
một phát biểu hành động đặt (hay gán) một giá trò cho một biến. Ví
du:ï hành động tăng biết i lên 1 có thể được viết như sau:
i := i + 1
hay
ii+1

-124-


Các cấu thường được sử dụng trong mã giả dựa theo ngôn ngữ lập
trình PASCAL gồm:
1/ Cấu trúc chọn:
if (điều kiện) then (hành động)
if (điều kiện) then (hành động)
else (hành động)

2/ Cấu trúc lặp:
while (điều kiện) do (hành động)
Repeat
(hành động)
Until (điều kiện)

động)

for (biến) := (giá trò đầu) to (giá trò cuối) do (hành

for (biến) := (giá trò đầu) downto (giá trò cuối) do
(hành động)
3/ Cấu trúc nhảy goto. Ngoài ra người ta còn sử dụng lệnh
ngắt vòng lặp break.
Dưới đây là các thuật toán được biểu diễn bằng mã giả (sử dụng các
cấu trúc điều khiển của ngôn ngữ lập trình PASCAL). Trước khi viết
-125-


các bước thực hiện thuật toán ta thường ghi rõ những gì được cho
trước (phần nhập) và kết quả cần đạt được (phần xuất).
-

Thuật toán tìm phần tử lớn nhất trong một dãy hữu hạn các số
nguyên:
Nhập: dãy số a1, a2, . . ., an
Xuất: max là giá trò lớn nhất trong dãy số đã cho trong input.
Thuật toán:
1. max := a1
2. for i := 2 to n do

if max < a1 then max := a1
3. max là giá trò lớn nhất trong dãy số.

-

Thuật toán giải phương trình bậc hai ax2 + bx + c = 0 (a  0):
Nhập : 3 hệ số a, b, c
Điều kiện : a  0
Xuất : nghiệm của phương trình
Thuật toán:
1. delta := b2 - 4*a*c
2. if delta > 0 then
begin
x1 := (-b - sqrt(delta)) / (2*a);
x2 := (-b+sqrt(delta)) / (2*a);
Xuất kết quả: phương trình có hai nghiệm là x1 và x2;
end
3. esle if delta = 0 then
Xuất kết quả: phương trình có nghiệm kép là -b / (2*a)
4. else trường hợp delta < 0
-126-


Xuất kết quả: phương trình vô nghiệm;
(Trong thuật toán nầy, ký hiệu sqrt(delta) dùng để chỉ căn bậc
hai dương của delta)
1.3 Các tính chất của thuật toán
Thuật toán có vai trò rất quan trọng trong khoa học máy tính. Để có
thể lập trình giải bài toán trên máy tính, ta cần có một thuật toán bảo
đảm những tính chất nhất đònh. Khi mô tả một thuật toán chúng ta

cần chú ý đến các tính chất sau đây:











Nhập (input): Các thuật giải có các giá trò nhập (input values)
từ một tập hợp nhất đònh nào đó.
Xuất (output): Từ mỗi tập hợp các giá trò được nhập một thuật
toán thường tạo ra những giá trò xuất (output values) thuộc
một tập hợp nhất đònh nào đó thể hiện lời giải cho bài toán.
Tính xác đònh (definiteness): Các bước trong thuật toán phải
chính xác rõ ràng.
Tính hữu hạn (finiteness): Thuật toán phải cho ra lời giải (hay
kết quả) sau một số hữu hạn bước.
Tính hiệu quả (về thời gian): Thuật toán cần phải được thực
hiện một cách chính xác và trong một khoảng thời gian cho
phép.
Tính tổng quát. Thuật toán phải áp dụng được cho tất cả các
bài toán có dạng như mong muốn, chứ không phải chỉ áp dụng
được cho một số trường hợp đặc biệt nào đó.
Tình đúng: Thuật toán phải cho kết quả như mong muốn.

-127-



Trong các tính chất trên, 3 tính chất cơ bản của thuật toán đòi hỏi
phải được thỏa mãn là tính xác đònh, tính hữu hạn và tính đúng.
Các thuật toán trong hai ví dụ 1 và 2 được trình bày ở trên đều thỏa
mãn các tính chất nêu trên.
Dưới đây chúng ta xét thêm một số ví dụ về các thuật toán.
Ví dụ 3: Thuật toán tìm kiếm tuyến tính (Linear Search)
Bài toán được đặt ra là xác đònh xem một phần tử x có trong
một dãy a1, a2, . . ., an hay không? Lời giải của bài toán nầy là
giá trò chỉ vò trí (hay chỉ số) của một phần tử trong dãy bằng
phần tử x, hoặc là 0 nếu x không có trong dãy.
Một thuật toán đơn giản để giải bài toán nầy là thuật toán tìm
kiếm tuyến tính (hay còn gọi là tìm kiếm tuần tự). Thuật toán
bắt đầu bằng việc so sánh x với a1, và nếu x = a1 thì lời giải là
vò trí của a1(tức là 1). Khi x  a1, ta tiếp tục so sánh x với a2.
Nếu x = a2, thì lời giải là vò trí của a2(tức là 2). Khi x  a2, ta
tiếp tục so sánh x với a3. Cứ tiếp tục quá trình nầy: lần lượt so
sánh x với từng phần tử của dãy cho tới khi gặp một phần tử
bằng x hoặc là cho tới khi đạt đến cuối dãy. Lời giải là vò trí
của phần tử trong dãy bằng x; hoặc là 0 nếu không có phần tử
nào trong dãy bằng x. Thuật toán nầy có thể được viết dưới
dạng mã giả như dưới đây.
Thuật toán: Tìm kiếm tuyến tính (hay tuần tự)
Nhập : dãy a1, a2, . . ., an, và phần tử x.
Xuất : vò trí của x trong dãy (chỉ số của phần tử trong dãy bằng
với x), hoặc 0
-128-



Thuật toán:
1. i := 1
2. while ( i  n and x  ai ) do
i := i + 1;
3. if i  n then location := i
else location := 0
4. location là một lời giải (ví trí cần tìm).
Trong thuật toán nầy từ "location" là một biến nguyên.
Ghi chú : Trong trường hợp dãy a1, a2, . . ., an có thứ tự thì ta có thể
tìm kiếm theo thuật toán tìm kiếm nhò phân (binary search). Ta có
thể tham khảo thuật toán nầy trong các sách về "cấu trúc dữ liệu
và thuật toán".
Ví dụ 4: thuật toán kiểm tra tính đối xứng của một ma trận.
Nhập : ma trận M cấp n.
Xuất : Yes nếu ma trận M là ma trận đối xứng.
No nếu M không đối xứng.
Thuật toán:
1. for i := 1 to n-1 do
2.
for j := i + 1 to n do
3.
if Mij  Mij then Kết xuất “No”, và dừng thuật
toán.
4. Kết xuất “Yes”.

-129-


II.


Độ phức tạp của thuật toán

2.1 Khái niệm độ phức tạp của thuật toán
Một chương trình máy tính thường được cài đặt dựa trên một thuật
toán để giải bài toán hay vấn đề đặt ra. Một đòi hỏi đương nhiên là
thuật toán phải đúng. Tuy nhiên, ngay cả khi thuật toán đúng, chương
trình vẫn có thể là không sử dụng được đối với một số dữ liệu nhập
nào đó bởi vì thời gian cần thiết để chạy chương trình hay vùng nhớ
cần thiết để lưu trữ dữ liệu (như các biến trong chương trình, các file
lưu trữ, ...) quá lớn.
Thuật ngữ phân tích thuật toán đề cập đến một quá trình tìm ra
một đánh giá về thời gian và không gian cần thiết để thực hiện thuật
toán. Độ phức tạp của thuật toán được thể hiện qua khối lượng thời
gian và không gian để thực hiện thuật toán. Không gian ở đây được
hiểu là các yêu cầu về bộ nhớ, thiết bò lưu trữ, … của máy tính để
thuật toán có thể làm việc được. Việc xem xét độ phức tạp về không
gian của thuật toán phụ thuộc phần lớn vào cấu trúc dữ liệu được sử
dụng trong cài đặt thuật toán. Trong phần nầy chúng ta chỉ đề cập
đến độ phức tạp về thời gian của thuật toán.
Chúng ta cũng có thể đạt được những thông tin rất hữu ích khi
phân tích độ phức tạp thời gian của thuật toán cơ sở của một chương
trình máy tính. Đánh giá một cách chính xác thời gian thực hiện một
chương trình phụ thuộc vào rất nhiều yếu tố và là một công việc rất
khó khăn. Tuy nhiên các nhà toán học đã phân tích cho chúng ta hầu
độ phức tạp của hầu hết các thuật toán thường được sử dụng như các
thuật toán sắp xếp, các thuật toán tìm kiếm, các thuật toán số học,
v.v…
-130-



Độ phức tạp thời gian của thuật toán thường được đánh giá dựa
vào số lượng thao tác được sử dụng trong thuật toán và số lượng thao
tác nầy phụ thuộc vào cở (size) của dữ liệu nhập. Ta còn gọi độ phức
tạp thời gian của thuật toán là độ phức tạp tính toán. Các thao tác
được sử dụng để đo độ phức tạp của thuật toán có thể là phép so sánh
2 số nguyên, cộng 2 số nguyên, nhân 2 số nguyên, chia 2 số nguyên,
hay bất kỳ thao tác cơ bản nào khác. Như thế ta có thể xem thời gian
thực hiện thuật toán là một hàm phụ thuộc vào dữ liệu nhập (thường
là cở của dữ liệu nhập). Nếu gọi cở dữ liệu nhập là n thì độ phức tạp
có thể được xem là một hàm theo n.
Chúng ta có thể đặt ra câu hỏi về thời gian thực hiện thuật toán
nhỏ nhất đối với các dữ liệu nhập có cở n. Ta có thể nêu lên một số
bài toán có dữ liệu nhập có cở n như: sắp xếp dãy n số nguyên, tìm
số nhỏ nhất trong dãy n số nguyên, v.v.... Thời gian nhỏ nhất nầy
được gọi là thời gian thực hiện thuật toán trong trường hợp tốt nhất
đối với dữ liệu nhập có cở n. Tương tự ta cũng thường đề cập đến
thời gian thực hiện thuật toán lớn nhất đối với các dữ liệu nhập có cở
n, và gọi là thời gian thực hiện thuật toán trong trường hợp xấu nhất
đối với dữ liệu nhập có cở n. Ngoài ra, đối với thuật toán có dữ liệu
nhập có cở n trong một tập hữu hạn nào đó, ta còn muốn tính ra thời
gian trung bình để thực hiện thuật toán.
Ví dụ 1: Thuật toán tìm giá trò lớn nhất trong dãy gồm n số
nguyên (xem ví dụ 1, mục I). Trong thuật toán nầy nếu ta xem
thời gian thực hiện thuật toán là số lần thực hiện phép so sánh
hay phép gán thì thời gian thực hiện thuật toán trong trường
hợp xấu nhất là:
t(n) = 1 + 2*(n-1) = 2n+1
và thời gian thực hiện thuật toán trong trường hợp tốt nhất là:
T(n) = 1 + (n-1) = n.
-131-



2.2 Ký hiệu O
Việc tính toán độ phức tạp (về thời gian hay về tính toán) của
thuật toán sẽ giúp ta có thể đánh giá và so sánh các thuật toán. Tuy
nhiên có những trường hợp mà 2 thuật toán khác nhau để giải quyết
cùng một bài toán có số lượng thao tác cơ bản là f(n) và g(n), với n là
cở dữ liệu nhập, rất khó so sánh đánh giá hơn kém theo sự so sánh
lớn bé thông thường. Hơn nữa trong hầu hết các thuật toán như thuật
toán sắp xếp, thuật toán tìm kiếm, … ta không thể tính ra được số
lượng thao tác f(n) theo n.
Thông thường ta ít chú ý tới con số chính xác về thời gian thực
hiện thuật toán trong trường hợp xấu nhất và trong trường hợp tốt
nhất. Điều mà chúng ta thường quan tâm hơn khi đánh giá độ phức
tạp thời gian của thuật toán là mức độ tăng lên của thời gian thực
hiện thuật toán khi cở của dữ liệu nhập tăng lên. Chẳng hạn, một
thuật toán đang được xem xét nào đó có thời gian thực hiện trong
trường hợp xấu nhất và trong trường hợp tốt nhất lần lượt là:
t(n) = 20n2 + 5n + 1,
T(n) = n2 + 10n + 1.
Như thế, nếu như n rất lớn thì ta có thể xấp xỉ t(n) và T(n) với
20n2 và n2. Có thể nói rằng t(n) và T(n) tăng giống như n2 khi n tăng.
Để diễn đạt điều nầy, người ta đònh nghóa và sử dụng ký hiệu O được
đònh nghóa như dưới đây.
-

Đònh nghóa: Cho 2 hàm thực f và g có miền xác đònh trong tập số
tự nhiên N. Ta viết:
f(n)  O(g(n))
-132-



và nói là f(n) có cấp cao nhất là g(n), hay f(n) thuộc lớp O(g(n)),
khi có một hằng số dương C sao cho:
f(n) C . | g(n) |,
với “hầu hết” n thuộc miền xác đònh của các hàm f và g. Từ
“hầu hết” ở đây ý nói là “với mọi chỉ trừ một số hữu hạn”,
hay nói một cách chính xác là
 C > 0,  k  N,  n  N, n  k  f(n) C . | g(n) |
Ví dụ:
1. Với t(n) = 20n2 + 5n + 1 và T(n) = n2 + 10n + 1. Ta có thể
chứng minh được rằng nói t(n) và T(n) có cấp cao nhất là n2,
tức là t(n)  O(n2) và T(n)  O(n2).
2. Xét f(n) = log (n!). Ta có
n! = 1.2. . .n  n.n. . .n  nn
 log(n!)  log (nn) = n.log(n)
Suy ra
log(n!)  O(n log n)
-

Đònh lý: Nếu f(n) là một đa thức bậc k theo n, tức là f(n) có dạng
f(n) = aknk + ak-1nk-1 + . . . + a1n + a0, với ak  0,
thì ta có f(n) thuộc lớp O(nk).

-

Ngoài ra ta còn có các tính chất sau đây:
 Giả sử rằng f1(n)  O(g1(n)) và f2(n)  O(g2(n)). Khi ấy ta có
f1(n) + f2(n)  O ( max(g1(n), g2(n)) )
-133-



Hệ quả là nếu f1(n) và f2(n) đều thuộc O(g(n)) thì ta cũng có
f1(n) + f2(n)  O(g(n))


Giả sử rằng f1(n)  O(g1(n)) và f2(n)  O(g2(n)). Khi ấy ta có
f1(n).f2(n)  O ( g1(n).g2(n) )

Ví dụ 2: Đánh giá độ phức tạp (thời gian) của thuật toán tìm kiếm
tuyến tính (xem ví dụ 3 ở mục I)
Đối với thuật toán nầy, trong trường hợp tốt nhất (phần tử cần
tìm nằm ngay tại vò trí đầu tiên của dãy) thời gian thực hiện
thuật toán là 1. Ta viết thời gian thực hiện thuật toán trong
trường hợp tốt nhất là: O(1).
Ta cũng có thể tính toán ra được thời gian thực hiện thuật toán
trong trường hợp xấu nhất và thời gian thực hiện thuật toán
trung bình đều là O(n).
2.3 Một số lớp độ phức tạp
Liên quan đến độ phức tạp của thuật toán ta có một số thuật ngữ
thường dùng trong sự phân lớp các độ phức tạp của thuật toán được
liệt kê dưới đây:
- độ phức tạp hằng:
- độ phức tạp logarith:
- độ phức tạp tuyến tính:
- độ phức tạp n log n:

O(1).
O(log n).
O(n).

O(n log n).
-134-


O(nb).
O(bn), trong đó b > 1.
O(n!).

- độ phức tạp đa thức:
- độ phức tạp lũy thừa:
- độ phức tạp giai thừa:
III.

Thuật toán đệ quy

3.1 Khái niệm đệ quy
Thuật toán đệ qui là một trong những sự mở rộng của khái niệm
thuật toán. Như đã biết, một thuật toán được đòi hỏi phải thỏa mãn
các tính chất:
- Tính xác đònh.
- Tính hữu hạn hay tính dừng.
- Tính đúng.
Tuy nhiên có những trường hợp việc tìm ra một thuật toán với
những tính chất đòi hỏi như trên rất khó khăn nhưng có cách giải có
thể vi phạm các tính chất của thuật toán nhưng lại khá đơn giản và
được chấp nhận. Ví dụ những trường hợp bài toán có thể được phân
tích và đưa tới việc giải một bài toán cùng loại nhưng cấp độ thấp
hơn, chẳng hạn cở dữ liệu nhập nhỏ hơn, giá trò cần tính toán nhỏ
hơn, v.v.... Ta cũng thường thấy những đònh nghóa về những đối
tượng, những khái niệm dựa trên chính những đối tượng, những khái

niệm đó như những ví dụ dưới đây.
Ví dụ 1: Đònh nghóa giai thừa.
Giai thừa của một số tự nhiên n, ký hiệu là n!, được đònh
nghóa bằng cách qui nạp như sau:
-135-


0! = 1,
n! = (n-1)!*n, với mọi n > 0.
Ví dụ 2: Đònh nghóa dãy số Fibonacci  f1, f2, . . ., fn, ...  :
f0 = 1,
f1 = 1,
fn = fn-1 + fn-2 , vớ mọi n > 1.
Thuật toán để giải các bài toán trong những trường hợp như trên
thường được viết dựa trên chính nó, tức là trong các bước của thuật
toán có thể có trường hợp thực hiện lại chính thuật toán đó (nhưng
thường là với dữ liệu nhập có cở thấp hơn, hay có cấp độ thấp hơn).
Những thuật toán loại nầy được gọi là những thuật toán đệ quy.
Dưới đây là các thuật toán đệ quy tính giai thừa của một số tự
nhiên n và tính số hạng thứ n của dãy số Fibonacci.
Thuật toán đệ quy tính giai thừa của một số tự nhiên.
Input : số tự nhiên n.
Output : F (n) bằng n!.
Thuật toán :
1. F := 1
2. if n > 0 then
F := F(n-1) * n; Tính (n-1)! rồi nhân với n sẽ được giá
trò F
3. Output F.
Thuật toán đệ quy tính số hạng thứ n của dãy số Fibonacci.

Input : số nguyên dương n.
Output : F (n) bằng số hạng thứ n của dãy Fibonacci.
-136-


Thuật toán :
1. if n=0 or n=1 then
F := 1;
2. if n > 1 then
F := F(n-1) + F(n-2)
tức là tính F(n-1) và F(n-2) rồi tính tổng số của các giá
trò nầy để gán cho F
3. Output F.
Trong thuật toán tính giai thừa n ở trên, có một bước mà ta tính
(n-1)! để từ đó tính ra kết quả. Đó là bước 2 trong trường hợp n > 0.
Chính bước tính (n-1)! nầy trong thuật toán làm cho thuật toán trở
thành thuật toán đệ quy. Ta còn gọi bước nầy là bước thực hiện đệ
quy. Trong thuật toán tính số hạng thứ n, ký hiệu F(n), của dãy số
Fibonacci ở trên ta phải tính F(n-1) và F(n-2) nếu n > 1 trong bước 2.
Bước tính F(n-1) và F(n-2) nầy chính là bước đệ quy của thuật toán.
Thuật toán đệ quy đã vi phạm tính xác đònh và rõ ràng của thuật toán
vì ở các bước đệ quy của nó. Tuy nhiên ta vẫn chấp nhận các thuật
toán đệ quy vì nó tiện lợi trong nhiều trường hợp chẳng hạn như việc
cài đặt các đònh nghóa đệ quy hay trong những trường hợp bài toán có
thể được đưa về bài toán cùng loại ở mức độ thấp hơn. Hơn nữa các
ngôn ngữ lập trình đều cho phép ta viết các chương trình con (thủ tục
hay hàm) dưới dạng đệ quy.
Ưu thế của thuật toán đệ quy là khi suy nghó về phương pháp giải bài
toán bằng cách phân chia trường hợp mà trong đó có những trường
hợp bài toán được thu gọn về bài toán cùng loại với cấp độ thấp hơn,

và những trường hợp nầy chính là những trường hợp mà ta phải thực
hiện bước đệ quy. Còn những trường hợp khác thì ta có thể giải quyết
trực tiếp một cách dễ dàng, và ta gọi những trường hợp nầy là những
-137-


trường hợp dừng đệ quy. Ví dụ như trong việc tính số hạng F(n) ứng
với chỉ số n của dãy Fibonacci thì trường hợp n = 0 và trường hợp n =
1 là các trường hợp dừng đệ quy, còn trường hợp n > 1 là trường hợp
mà ta phải thực hiện các bước đệ quy: tính các số hạng F(n-1) và F(n2) để suy ra số hạng F(n).
3.2 Cấu trúc của thuật toán đệ quy




Trong thuật giải đệ qui thường gồm 2 phần: phần cơ sở và phần
đệ quy.
Phần cơ sở gồm các trường hợp không cần thực hiện lại thuật
toán, tức là các trường hợp dừng mà ta có thể trực tiếp giải quyết
được bài toán (hay không có yêu cầu gọi đệ qui). Trong thuật
toán tìm số hạng thứ n của dãy Fibonacci ở trên, bước 1 trong
thuật toán là phần cơ sở của thuật giải đệ qui.
Phần đệ quy là phần trong thuật toán có yêu cầu gọi đệ quy, tức
là yêu cầu thực hiện lại thuật toán ở cấp độ thấp hơn. Trong thuật
toán tìm số hạng thứ n của dãy Fibonacci ở trên, bước 2 trong
thuật toán là phần đệ quy. Trong phần đệ quy, yêu cầu gọi đệ qui
thường được đặt trong một điều kiện kiểm tra việc gọi đệ quy.

Cần lưu ý rằng phần cơ sở luôn luôn phải có hay nói cách khác là
phần đệ quy luôn luôn phải nằm trong điều kiện kiểm soát dừng đệ

quy, vì nếu không thì thuật toán sẽ bò lặp vô hạn do việc gọi đệ quy
luôn được thực hiện.
-

Về mặt cài đặt, nếu như có sử dụng biến cục bộ trong thủ tục hay
hàm đệ quy thì các biến nầy được tạo ra và được đặt trong vùng
nhớ “STACK”. Do đó quá trình gọi đệ quy dễ gây ra tình trạng
tràn stack (stack overflow). Trong nhiều trường hợp có thể được
-138-


người ta tìm cách viết lại thuật toán đệ quy dưới dạng lặp. Thuật
toán đệ qui để tìm số hạng thứ n của dãy số Fibonacci và thuật
toán tính n! có thể được viết lại dưới dạng lặp như sau:
Thuật toán lặp tính số hạng thứ n của dãy số Fibonacci.
Input : số nguyên dương n.
Output : F (n) bằng số hạng thứ n của dãy Fibonacci.
Thuật toán :
1. a := 1
2. F := 1
3. for i:=3 to n do
begin
temp := a + F;
a := F;
F := temp;
end;
4. Output F.
Thuật toán lặp tính giai thừa của một số tự nhiên.
Input : số tự nhiên n.
Output : F (n) bằng n!.

Thuật toán :
1. F := 1
2. for i := 2 to n do
F := F * i
3. Output F.

-139-


3.3 Trình bày (hay viết) thuật toán đệ quy
Để tiện trình bày thuật toán đệ quy, nhất là ở các bước gọi đệ quy
(hay thực hiện đệ quy), ta thường đặt tên cho thuật toán có đi kèm
các tham biến chính liên quan đến bài toán cũng giống như ta khai
báo thủ tục hay hàm trong các chương trình máy tính. Dưới đây ta xét
2 ví dụ khác về các bài toán mà ta có thể giải bằng phương pháp đệ
quy.
Ví dụ 1: Tính tổ hợp n chọn k, ký hiệu là C(n,k). Nhắc lại một số
tính chất của C(n,k):
C(n,0) = C(n,n) = 1, và
C(n,k) = C(n-1,k) + C(n-1,k-1)
nếu 0 < k < n.
Các tính chất trên của phép tính tổ hợp cho ta một cách tính tổ
hợp theo phương pháp đệ quy. Đặt tên cho thuật toán tính tổ hợp
n chọn k là Tohop(n,k) ta có thuật toán đệ quy để tính tổ hợp sau
đây:
Thuật toán tính tổ hợp n chọn k:

Tohop(n,k)

If (k = 0) or (k = n) then

Tohop := 1;
If (0 < k) and (k < n) then
Tohop := Tohop(n-1, k) + Tohop(n-1, k-1);
Ví dụ 2: Tính ước số chung lớn nhất của 2 số tự nhiên a và b, ký
hiệu là USCLN(a,b).

-140-


Từ các tính chất dưới đây (cho các số nguyên tùy ý) của phép tính
ước số chung lớn nhất:
USCLN(a, 0) = USCLN(0, a) = a,
USCLN(a, b) = USCLN(a-b, b), và
USCLN(a, b) = USCLN(a, b-a)
Ta có ngay một cách tính USCLN theo phương pháp đệ quy. Đặt
tên cho thuật toán tính USCLN của 2 số tự nhiên a và b là
USCLN(a, b) ta có thuật toán đệ quy sau đây:
Thuật toán tính USCLN của a và b: USCLN(a,b)
If (a = 0) or (b = 0) then
USCLN := a+b;
Else If (a > b) then
USCLN := USCLN(a-b, b);
Else
USCLN := USCLN(a, b -a);
Ghi chú: Một ví dụ khá tốt để minh họa cho thuật toán đệ quy cũng
thường được đề cập tới là bài toán "Tháp Hà Nội". Chúng ta có
thể tham khảo về bài toán nầy trong [5] trang 227.
IV.

Một số thuật toán số học


Trong phần nầy chúng ta sẽ giới thiệu một số thuật toán đơn giản về
số học. Việc giới thiệu nầy nhằm giúp cho học viên cũng cố thêm
những kiến thức về thuật toán đã được trình bày ở các phần trên và
cũng là những chất liệu cần thiết về toán làm những bài tập cơ bản
cho việc học lập trình.
-141-


-

Thuật toán kiểm tra số nguyên tố.

Vấn đề đặt ra là : cho một số nguyên dương p. Làm thế nào để
biết được p có phải là số nguyên tố hay không?
Cách đơn giản nhất để biết p có nguyên tố hay không là dựa
vào đònh nghóa của số nguyên tố. Trước hết ta xem xét điều kiện p 
1. Nếu p = 1 thì p không nguyên tố. Nếu đúng là p  1 thì tiếp tục
kiểm tra xem trong các số từ 2 đến p-1 có ước số của p hay không?
Nếu có thì p không nguyên tố; ngược lại thì p nguyên tố. Thuật toán
kiểm tra số nguyên tố theo cách nầy có thể được viết như sau:
Thuật toán 1: Kiểm tra tính nguyên tố của một số nguyên dương
Nhập: p nguyên dương
Xuất : kết luận về tính nguyên tố của p.
Thuật toán :
1. if p = 1 then
begin
kết luận : p không nguyên tố;
Dừng thuật toán;
end

2. flag := TRUE (gán cho cờ hiệu “flag” giá trò RUE)
3. for k := 2 to p-1 do
if (k là ước số của p) then
begin
flag := FALSE;
break;
(ngắt vòng lặp for)
end

-142-


4. if flag = TRUE then
kết luận: p là số nguyên tố
else
kết luận: p không là số nguyên tố
Nhờ vào mệnh đề dưới đây, thuật toán trên còn có thể được cải tiến
để giảm bớt thao tác trong quá trình thực hiện thuật toán. Không làm
mất tính tổng quát, ta chỉ cần viết thuật toán kiểm tra tính nguyên tố
của một số nguyên dương n trong trường hợp n > 1.
Mệnh đề. Nếu số nguyên n > 1 không phải là một số nguyên tố
thì n có một ước số nguyên tố (dương)  n .
Thuật toán 2. Thuật toán kiểm tra số nguyên tố
Nhập: n nguyên dương
Điều kiện : n > 1
Xuất : kết luận về tính nguyên tố của n.
Thuật toán :
1. p := 2
(p sẽ được kiểm tra xem có phải là một
ước số của n hay không)

2. flag := TRUE
3. while p  n and flag do
if (p là ước số của n) then
flag := FALSE
else
p := p+1
4. if flag = TRUE then
kết luận: p là số nguyên tố
else
kết luận: p không là số nguyên tố
-143-


-

Thuật toán tính ước số chung lớn nhất của 2 số nguyên.

Trong mục nầy chúng ta phát biểu một số tính chất liên quan đến
ước số chung lớn nhất của hai số nguyên. Từ đó rút ra một thuật toán
để tìm ước số chung lớn nhất của hai số nguyên.
Mệnh đề.
(1) Nếu a là một ước số của b thì (a,b) = a.
(2) (a,b) = (a, a  b).
(3) Giả sử a là một số nguyên tùy ý, b là một số nguyên khác
0. Gọi r là số dư khi chia a cho b. Khi đó ta có :
(a,b)
= (b, r). Tức là ước số chung lớn nhất của a và b bằng ước
số chung lớn nhất của b và r.
Từ các tính chất trên ta có một cách để tìm ước số chung lớn nhất
của hai số nguyên a và b (không đồng thời bằng 0) như sau:

Thuật toán 3. Tìm ước số chung lớn nhất của hai số nguyên.
Nhập : m, n là 2 số nguyên.
Điều kiện : m và n không đồng thời bằng 0.
Xuất : d là ước số chung lớn nhất của m và n.
Thuật toán :
1. if n = 0 then
begin
d := m;
Dừng thuật toán
end
2. a := m; b := n
3. r := a mod b (gán cho r dư số trong phép chia a cho b)
4. while r  0 do
-144-


×