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

Giáo trình kĩ thuật lập trình

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 (1.12 MB, 124 trang )

Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
MỤC LỤC
MỤC LỤC 1

2
Lời nói đầu 3
Chương 1 4
Một số kỹ thuật – phong cách lập trình tốt 4
0.1 Cách đặt tên cho biến hàm 4
0.2 Phong cách viết mã nguồn 6
0.3 Tối ưu sự thực thi mã nguồn 8
Kỹ thuật đệ quy 16
1.1 Kỹ thuật đệ quy 16
1.2 Xây dựng một chương trình đệ quy 20
1.3 Các ví dụ đệ quy 21
1.4 Khử đệ quy 27
1.4.1 Tìm hiểu cơ chế thực hiện hàm đệ quy 27
1.4.2 Các trường hợp khử đệ quy đơn giản 29
1.4.3 Khử đệ quy dùng stack 31
Bài toán liên quan tổ hợp 37
2.1 Phương pháp sinh 37
2.1.1 Bài toán sinh dãy nhị phân độ dài n 37
2.1.2 Bài toán liệt kê tập con k phần tử 39
2.1.3 Bài toán liệt kê các hoán vị 42
2.2 Thuật toán quay lui (Back Tracking) 45
2.2.1 Thuật toán quay lui liệt kê dãy nhị phân n 47
2.2.2 Thuật toán quay lui liệt kê tập con k phần tử 48
2.2.3 Thuật toán quay lui liệt kê hoán vị n phần tử 50
2.2.4 Bài toán sắp xếp quân Hậu 51
2.2.5 Bài toán mã đi tuần 57
Tìm kiếm và Sắp xếp 63


1.1 Tìm kiếm 63
1.1.1 Mô tả bài toán tìm kiếm trong tin học 63
1.1.2 Tìm kiếm tuyến tính 64
1.1.3 Tìm kiếm nhị phân 65
1.1.4 Kết luận 67
1.2 Bài toán sắp xếp 67
1.3 Một số phương pháp sắp xếp cơ bản 67
1.3.1 Phương pháp chọn 67
1.3.2 Phương pháp sắp xếp nổi bọt 68
1.3.3 Phương pháp sắp xếp chèn 68
1.3.4 Phương pháp đổi chỗ trực tiếp 69
1.3.5 Phương pháp ShellSort 76
1.3.6 Phương pháp phân đoạn QuickSort 79
1.3.7 Phương pháp cơ số RadixSort 83
Stack - Queue 87
1
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
2.1 Giới thiệu Stack – ngăn xếp 87
2.1.1 Cài đặt Stack dùng CTDL mảng 88
2.1.2 Các ứng dụng stack 90
2.1.3 Các ví dụ minh họa 91
2.2 Giới thiệu Queue – hàng đợi 106
2.2.1 Cài đặt Queue dùng CTDL mảng 108
2.2.2 Các ứng dụng Queue 109
BÀI TẬP 117
TÀI LIỆU THAM KHẢO 124
 
2
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
Lời nói đầu

Học phần kỹ thuật lập trình 2 được thiết kế dành cho sinh viên khoa công
nghệ thông tin ĐH Kỹ Thuật Công Nghệ, là phần tiếp nối với môn kỹ thuật lập
trình 1. Mục đích của môn học là bổ sung những kỹ thuật lập trình đệ quy, khử đệ
quy, các bài toán trên tập hợp, phương pháp sinh, kỹ thuật quay lui, tìm kiếm và
sắp xếp trên mảng, ngăn xếp và hàng đợi…Song song với phần lý thuyết là các ví
dụ minh họa cụ thể, cho phép sinh viên hiểu rõ vấn đề hơn.
Ngoài những kỹ thuật lập trình, giáo trình còn đề cập tới phương diện
phong cách lập trình trong chương 1. Việc sớm làm quen với phong cách lập trình
sẽ hỗ trợ sinh viên hoàn thiện kỹ năng viết chương trình.
Bài giảng được viết lần đầu tiên nên sẽ không tránh khỏi những sai sót.
Kính mong sự đóng góp của các giảng viên và sinh viên nhằm hoàn thiện phần bài
giảng này trong lần tái bản sau.
Tất cả những ý kiến đóng góp điều được trân trọng.
Xin chân thành cảm ơn!
Tác giả
3
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
Chương 1
Một số kỹ thuật – phong cách lập trình tốt
 
Một chương trình nguồn được xem là tốt không chỉ được đánh giá thông qua thuật
giải đúng và cấu trúc dữ liệu thích hợp. Mà còn phụ thuộc vào phong cách và kỹ thuật mã
hoá (coding) của người viết chương trình.
Nếu một người lập trình viết một chương trình tuy thực hiện đúng yêu cầu đặt ra
nhưng mã nguồn quá lộn xộn và phong cách lập trình cẩu thả, thì mã nguồn này sẽ gây
khó khăn cho chính người lập trình!
Đôi khi người mới lập trình không quan tâm đến vấn đề này do ban đầu chỉ làm
việc với chương trình nhỏ. Tuy nhiên, vấn đề phát sinh khi họ phải làm việc với dự án lớn
và chương trình lúc này không còn đơn giản vài chục dòng lệnh nữa. Nếu không rèn
luyện một phong cách và trang bị một số kỹ thuật lập trình tốt thì người lập trình đối mặt

với nhiều khó khăn…
Trong chương đầu tiên xin giới thiệu một số kỹ thuật và phong cách lập trình cơ
bản, ít nhiều giúp cho người học viết chương trình được tốt hơn.
0.1 Cách đặt tên cho biến hàm
Thông thường tùy theo ngôn ngữ và môi trường lập trình, người viết chương trình
thường chọn cho mình một phong cách nhất quán trong việc đặt tên các định danh. Một
số quy tắc cần quan tâm khi đặt tên như sau:
1. Tên của định danh phải thể hiện được ý nghĩa : thông thường các biến nguyên
như i, j, k dùng làm biến lặp; x, y dùng làm biến lưu tọa độ…Còn những biến
lưu trữ dữ liệu khác thì nên đặt gợi nhớ: biến đếm số lần dùng “count” hay
So_Luong, biến lưu trọng lượng “weight”, chiều cao “height”…Nếu đặt quá
ngắn gọn như c cho biến đếm, hay w cho khối lượng thì sau này khi nhìn vào
chương trình sẽ rất khó hiểu!
2. Tên phải xác định được kiểu dữ liệu lưu trữ : phong cách lập trình tốt là khi
người đọc nhìn vào một biến nào đó thì xác định ngay được kiểu dữ liệu mà
4
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
biến đó lưu trữ. Giả sử có biến đếm số lần thì ta có thể đặt iCount, trong đó i là
kiểu của dữ liệu, strContent là kiểu chuỗi…Có nhiều cú pháp quy ước đặt tên
biến, người lập trình có thể chọn cho mình một quy ước thích hợp. Có thể tham
khảo một số quy ước trong phần 3 bên dưới.
3. Theo một quy ước cụ thể :
a. Cú pháp Hungary : hình thức chung của cú pháp này là thêm tiền tố chứa
kiểu dữ liệu vào tên biến. Bảng 1.1 bên dưới là một số tiền tố quy ước
được nhiều lập trình viên sử dụng. Các công ty phần mềm thường có các
quy ước về cách đặt tên biến cho đội ngũ lập trình viên. Tuy nhiên đa số
các quy ước này đều dựa trên cú pháp Hungary.
Tiền tố Kiểu dữ liệu Minh họa
b boolean bool bStop
c char char cLetterGenre

str/s C++ string string strFirstName
si short integer short siTables
i/n integer int iCars
int nCars
li long integer long liStars
f floating point float fPercent
d Double precision floating point double dMiles
ld long double precision floating
point
long double ldPI
sz Null terminated string char szName[NAME_LEN]
if Input file stream ifstream ifNameFile
is Input stream istream isInput
of Output file stream ofstream ofNameFile
os Output stream ostream osOut
S Struct struct sPoint {…}
C Class class CStudent {…}
w Word word wChar
u Unsigned
m_ biến thành viên của hàm class CStudent
{
private:
string m_strName;
}
g_ biến toàn cục string g_strBuff
lp long pointer LPCTSTR lpszClassName
5
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
h handle trong windows HINSTANCE hInstance
Bảng 1.1: Minh họa tiền tố của cú pháp Hungary.

Đối với những hằng thì tất cả các ký tự đều viết hoa
#define MAXSIZE 100
const int MAXLENGTH 200
Cách đặt tên cho hàm: hàm bắt đầu với ký tự đầu tiên là ký tự hoa và không có
tiền tố. Tuy nhiên, điều này cũng không bắt buộc tuỳ theo ngôn ngữ lập trình. Nói chung
là hàm có chức năng thực hiện một chức năng nào đó, cho nên chúng thường bắt đầu
bằng động từ: get, set, do…
CString GetName(); // Microsoft VC++ standard
String setName(); // Sun Java standard
0.2 Phong cách viết mã nguồn
• Sử dụng tab để canh lề chương trình : khi soạn thảo mã nguồn nên dùng tab với kích
thước là 4 hay 8 để canh lề. Thói quen này giúp cho chương trình được rõ ràng và dễ
quản lý.
for (i = 0;i < N; i++)
{
if (Check(i))
{
Action1();
Action2();
}
else
Action3();
ActionMain();
}
for (i = 0; i < N; i++)
{
if (Check(i))
{
Action1();
Action2();

}
else
Action3();
ActionMain();
}
• Sử dụng khoảng trắng : chương trình sẽ dễ nhìn hơn.
int count;
for(count=0;count<10;count++)
{
printf(“%d”,count*count+count);
}
int count;
for (count = 0; count < 10; count++)
{
printf(“%d”, count * count + count);
}
• Tránh viết nhiều lệnh trên cùng một dòng :
if(a>5){b=a;a++;} if (a > 5)
{
6
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
b=a;
a++;
}
• Định nghĩa các hằng số : một thói quen là người lập trình không định nghĩa những
hằng số thường xuyên sử dụng. Dẫn đến những con số khó hiểu xuất hiện trong
chương trình, một số tài liệu lập trình gọi những con số này là “magic mumber”.

for(int i=0; i < 100; i ++)
A[i] = Rand(100);


k = InputNum();
j=0;
while (A[j] != k && j < 100)
j++;

#define MAX_LEN 100
#define MAX_NUM 100

for(int i=0; i < MAX_LEN; i++)
A[i] = Rand(MAX_NUM);

k = InputNum();
j=0;
while (A[j] != k && j < MAX_LEN)
j++;

Trong đoạn chương trình bên trái rất khó phân biệt giá trị 100 ở ba vị trí có mối
quan hệ gì với nhau. Tuy nhiên, trong đoạn bên phải ta dễ dàng thấy được ý nghĩa của
từng giá trị khi thay bằng định danh. Ngoài ra khi cần thay đổi giá trị của MAX_LEN,
MAX_NUM thì chỉ cần thay một lần trong phần định nghĩa. Do đó đoạn chương trình B
dễ nhìn hơn và dễ thay đổi hơn!
• Viết chú thích cho chương trình : biến, hàm khi định nghĩa nên viết chú thích ý nghĩa
và chức năng rõ ràng. Đôi khi các lệnh thực thi cũng cần có giải thích nếu chúng quá
phức tạp.
int CheckFactor(int n)
{
/*
Ý nghĩa: kiểm tra xem 1 số có phải là nguyên tố hay không
Tham số vào: n số cần kiểm tra

Tham số ra: giá trị trả về
0: không phải số nguyên tố
1: là số nguyên tố
*/
….// phần thực hiện của hàm
}
Ví dụ chú thích cho biến
byte Image; // buffer ảnh
int Rows, Cols; // số dòng, số cột
7
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
int r, c; // dòng cột hiện hành
int PixelCount; // tổng số pixel
Tuy nhiên không phải bất cứ lệnh nào cũng chú thích, việc chú thích tràn lan ngay
cả với câu lệnh đơn giản cũng không có ý nghĩa gì. Đôi khi còn làm cho chương trình
khó nhìn hơn!
• Nên viết biểu thức điều kiện mang tính tự nhiên : biểu thức nên viết dưới dạng khẳng
định, việc viết biểu thức dạng phủ định sẽ làm khó hiểu!
if ( !(iBlock < Block1 ) || !(iBlock >= Block2))

Mỗi biểu thức trong điều kiện được viết dưới dạng phủ định, ta nên viết lại dưới dạng
khẳng định cho dễ hiểu hơn:
if ( (iBlock >= Block1 ) || (iBlock < Block2))

• Dùng chính ngôn ngữ đó để tính kích thước của đối tượng : không nên dùng giá trị
tường minh cho kích thước của dữ liệu. Khi cần lấy kích thước của biến int, ta có thể
dùng sizeof(int) thay cho các giá trị 2 hay 4. Tương tự như vậy khi lấy kích thước của
phần tử trong một mảng int ta dùng sizeof(array[0]) thay cho sizeof(int). Sau này khi
mảng array có thay đổi kiểu dữ liệu thì cách viết sizeof(array[0]) cũng không ảnh
hưởng.

0.3 Tối ưu sự thực thi mã nguồn
Mã nguồn nếu được viết tốt sẽ làm cho tốc độ chương trình cải thiện đáng kể. Có
thể ngày nay năng lực xử lý của máy tính khá mạnh, do đó người lập trình không
quan tâm đến việc tối ưu mã nguồn. Nhưng cũng không vì thế mà bỏ qua kỹ thuật
này. Vậy thế nào là tối ưu mã nguồn? ở đây không đề cập đến giải thuật, vì chắc chắn
giải thuật tốt thì sẽ cho chương trình tối ưu. Tuy nhiên, việc cài đặt cũng cần phải có
kỹ thuật, nếu không thì chính khả năng cài đặt của lập trình viên làm hạn chế sự thực
thi của thuật giải hay chương trình.
Mục đích của việc tối ưu mã nguồn là nâng cao tốc độ xử lý và hạn chế không
gian bộ nhớ mà chương trình chiếm dụng. Thông thường có thể mâu thuẫn giữa tốc
độ và không gian lưu trữ, do đó tuỳ theo điều kiện cụ thể mà người lập trình có thể
lựa chọn thích hợp.
8
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
Trong phần dưới xin trình bày một số thủ thuật chọn lọc có thể giúp ích để hình
thành nên phong cách lập trình tốt cho người đọc.
• Thu gọn những biểu thức dùng nhiều lần : nếu một biểu thức tính toán được dùng
nhiều lần thì chúng ta nên tính kết quả một lần rồi lưu vào một biến và dùng lại.
Ví dụ:
F = sqrt(dx*dx+dy*dy) + (sqrt(dx*dx + dy*dy)*sqrt(dx*dx)-sqrt(dy*dy))…
Trong dãy biểu thức trên có sqrt(dx*dx+dy*dy), dx*dx, dy*dy được dùng nhiều
chỗ, ta có thể tính trước bên ngoài và lưu vào biến tạm để dùng lại sau này. Hạn chế
việc tính toán với cùng một biểu thức nhiều lần!
• Đưa những biểu thức không phụ thuộc vòng lặp ra ngoài : trong một số vòng lặp ta có
sử dụng biểu thức tính toán nhưng giá trị của biểu thức không phụ thuộc vào sự thay
đổi của vòng lặp thì có thể đưa biểu thức này ra ngoài.
Ví dụ:
for(i =0; i < strlen(str); i++)
….
chuyển thành:

int n = strlen(str)
for(i =0; i < n; i++)
….
• Thay thế một biểu thức bằng một biểu thức tương đương nhưng lợi về thực thi : một
số chương trình xử lý ảnh đòi hỏi tốc độ cao, thì người lập trình có thể thay thế các
phép nhân chia bằng phép dịch chuyển bit. Thay thế sử dụng chỉ mục trong mảng
C/C++ bằng con trỏ…
Ví dụ: khi so sánh khoảng cách của hai điểm ta thường làm như sau
if (sqrt(dx1*dx1+dy1*dy1) < sqrt(dx2*dx2+dy2*dy2))

Thay bằng
if ((dx1*dx1+dy1*dy1) < (dx2*dx2+dy2*dy2))

• Dùng số nguyên thay cho số thực : do việc xử lý số thực chậm hơn xử lý số nguyên
nên ta có thể dùng số nguyên thay cho số thực có phần lẻ nhỏ.
9
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
Ví dụ: điểm trung bình của sinh viên là số thực ta có thể thay bằng số nguyên: DTB là
8.72 thì lưu theo số nguyên 872, khi xuất ra thì chia cho 100.
• Loại bỏ vòng lặp : nếu thân vòng lặp đơn giản và số lần lặp cũng không nhiều, ta có
thể làm cho đoạn chương trình hiệu quả hơn bằng cách bỏ vòng lặp.
Ví dụ:
for(i =0; i < 3; i++)
A[i] = B[i] + C[i];
Thay bằng
A[1] = B[1] + C[1];
A[2] = B[2] + C[2];
A[3] = B[3] + C[3];
Đoạn chương trình thay thế loại bỏ vòng lặp, tức là lệnh rẽ nhánh, lệnh rẽ nhánh làm
chậm chương trình do ngắt luồng thực thi.

Nếu vòng lặp dài và cùng dạng biểu thức ta có thể cải tiến như ví dụ sau
for(i=0; i < 3*n; i++)
A[i] = B[i] + C[i];
Thay bằng
for(i=0; i < 3*n; i+=3)
{
A[i] = B[i] + C[i];
A[i+1] = B[i+1] + C[i+1];
A[i+2] = B[i+2] + C[i+2];
}
Ví dụ trên chỉ áp dụng khi chiều dài vòng lặp là bội số của bước nhảy!
• Loại bỏ câu lệnh rẽ nhánh trong vòng lặp : xem ví dụ sau
Chương trình A Chương trình B
for i to 1000 do
{
x[i] = x[i] + y[i];
if (w) then y[i] = 0;
}
if (w) then
for i to 1000 do
{
x[i] = x[i] + y[i];
y[i] = 0;
}
else
for i to 1000 do
x[i] = x[i] + y[i];
10
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
Trong chương trình A, mỗi lần lặp thì phải kiểm tra thêm điều kiện của w. Trong khi

chương trình B thì ta kiểm tra giá trị của w trước khi vào vòng lặp. Do đó B có hai vòng
lặp nhưng chỉ thực hiện một trong hai và chỉ kiểm tra giá trị w duy nhất 1 lần!
• Thoát khỏi vòng lặp sớm nhất : một số trường hợp không cần phải lặp hết toàn bộ
vòng lặp mà đã đạt được mục đích thì có thể thoát ra khỏi vòng lặp.
Ví dụ: chỉ cần xác định giá trị -99 có xuất hiện trong danh sách hay không ta có hai
chương trình A và B minh họa như sau:
Chương trình A Chương trình B
found = FALSE;
for(i=0;i<10000;i++)
{
if( list[i] == -99 )
{
found = TRUE;
}
}
if( found ) printf("Yes, there is a -99.");
found = FALSE;
for(i=0; i<10000; i++)
{
if( list[i] == -99 )
{
found = TRUE;
break;
}
}
if( found ) printf("Yes, there is a -99.");
Chương trình A khi tìm thấy thì vẫn cứ lặp cho đến hết, trong khi B thì sẽ thoát
ngay. Rõ ràng khi đã tìm thấy thì không cần phải lặp tiếp, khi đó B sẽ tối ưu hơn!
• Gom các vòng lặp : các vòng lặp cùng số lần lặp thì nên gom lại
Ví dụ:

for( int i=0; i<n; i++)
a[i]= 0;
for(i=0; i<n i++)
b[i]= 0;
Viết lại:
for(i=0; i<n i++)
a[i]= b[i]= 0;
• Sử dụng phép shift thay cho nhân chia :
11
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
o Shift trái 1 bit: nhân 2
o Shift phải 1 bit: chia 2
Ví dụ:
a *= 4 ⇒ a<<2
b /=8 ⇒ b>>3
a = 2*(b+c) ⇒ a = (b+c)<<1
• Sử dụng phép “&” : thay cho phép chia dư n, với n là 2
i
{2, 4, 8, 16, 32…}
Ví dụ:
m = n % 2 ⇒ m = n & 1 ⇔ m = n & 0x1
m = n % 8 ⇒ m = n & 7 ⇔ m = n & 0x7
m = n % 16 ⇒ m = n & 15 ⇔ m = n & 0xF
Lấy byte thấp:
m = n % 256 ⇒ m = n & 0xFF
• Cải tiến tính toán cho biến cờ :
if (x >y)
flag =1;
else
flag =0;

Cải tiến thành:
flag = x>y;
• Lưu tạm giá trị thường sử dụng : trong chương trình đôi khi một giá trị được tính toán
một lần nhưng lại thường được sử dụng mà ít có thay đổi giá trị. Khi đó ta có thể
dùng biến lưu trữ giá trị của biểu thức này, khi nào cần thì có thể sử dụng biến đó
thay vì phải tính toán lại.
Ví dụ: đoạn chương trình giải phương trình bậc hai.

12
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
if ((b*b)-4*a*c < 0)
printf(“Phuong trinh vo nghiem!”);
else if ((b*b)-4*a*c == 0)
printf(“Phuong trinh co nghiem kep”);

else
{
x1= (-b + sqrt((b*b)-4*a*c))/(2*a);
x2= (-b - sqrt((b*b)-4*a*c))/(2*a);

}
Trong đoạn chương trình trên delta được tính lại 4 lần, ta có thể cải tiến chỉ tính duy
nhất một lần!
delta = (b*b)-4*a*c;
if ( delta < 0)
printf(“Phuong trinh vo nghiem!”);
else if (delta == 0)
printf(“Phuong trinh co nghiem kep”);

else

{
x1= (-b + sqrt(delta))/(2*a);
x2= (-b - sqrt(delta))/(2*a);

13
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
}
• Tránh lãng phí bộ nhớ: bằng cách sử dụng kiểu dữ liệu nhỏ nhất có thể được để lưu
trữ: không gian bộ nhớ hiện tại có thể không còn eo hẹp như trước, nhưng không vì
thế mà người lập trình có thể tự do phung phí cấp cho chương trình. Việc sử dụng quá
nhiều tài nguyên hơn mức đòi hỏi của chương trình là thói quen xấu mà người lập
trình hay mắc phải. Hơn nữa tốc độ chương trình sẽ nhanh hơn khi sử dụng kiểu dữ
liệu nhỏ hơn.
• Khai báo biến cục bộ trong phạm vi gần nhất : đúng như tên gọi là biến cục bộ do đó
khi sử dụng nên khai báo gần với điểm sử dụng nhất. Việc khai báo ở phạm vị rộng
hơn chỉ làm lãng phí và khó kiểm soát.
• Sử dụng macro : một số hàm đơn giản và thường sử dụng có thể chuyển thành macro
để tăng tốc độ thực thi của chương trình. Do mỗi lần gọi hàm sẽ tốn chi phí cho việc
gọi và trả về từ hàm.
Ví dụ:
int max(int a, int b)
{
return a>b? a: b;
}
Chuyển thành macro:
#define max(a, b) ((a)>(b)) ? (a) : (b)
Hàm hoán chuyển giá trị 2 số nguyên
void swap(int &a, int &b)
{
int t;

t = a;
a = b;
b = t;
}
Chuyển thành macro swap
#define swap(a, b) {int t = a; a = b; b = t;}
14
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
• Giảm số lượng tham số truyền vào hàm : việc sử dụng hàm có quá nhiều tham số được
truyền vào có thể làm ảnh hưởng đến ngăn xếp dành cho việc gọi hàm. Nhất là trường
hợp tham số là kiểu dữ liệu cấu trúc. Sử dụng con trỏ hay tham chiếu trong trường
hợp này để đơn giản hoá.
Ví dụ :
void Print(struct Student s)
{
printf(“%d”, s.StudentCode);

}
Thay bằng:
void Print(const struct Student *s)
{
printf(“%d”, s->StudentCode);

}
 
15
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
Chương 1
Kỹ thuật đệ quy
 

1.1 Kỹ thuật đệ quy
Đệ quy là một thuật toán dùng để đơn giản hóa những bài toán phức tạp bằng
cách phân nhỏ phép toán đó thành nhiều phần đồng dạng. Qua việc giải những bài
toán được phân nhỏ này, những lời giải sẽ được kết hợp lại để giải quyết bài toán lớn
hơn.
Một số các ví dụ đệ quy
• Định nghĩa số tự nhiên
o 0 là số tự nhiên
o N là số tự nhiên n-1 là số tự nhiên
• Định nghĩa giai thừa của n
o 0! là 1
o Nếu n>0, n! = n *(n-1)!
Hàm đệ quy : Hàm đệ quy là một hàm trong đó có dùng lời gọi hàm đến chính bản
thân nó.
Ví dụ ta có hàm đệ quy như sau:
int Sum(int n)
{
if (n==0)
return 0;
else
return (n+Sum(n-1)); // gọi đệ quy đến chính bản thân hàm sum
}
Khi một hàm đệ quy gọi đến chính nó thì mỗi lần gọi máy sẽ tạo ra tập các biến
cục bộ mới hoàn toàn độc lập với biến cục bộ đã tạo ra trong lần gọi trước. Bao nhiêu
lần gọi hàm đệ quy thì tương ứng với bấy nhiêu lần thoát ra khỏi hàm, mỗi lần ra khỏi
hàm thì tập biến cục bộ bị xóa.
16
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
Có một sự tương ứng giữa các lời gọi hàm và lần thoát khỏi hàm theo thứ tự
ngược lại: lần ra khỏi hàm đầu tiên tương ứng với lần gọi hàm cuối cùng.

Ví dụ minh họa hàm đệ quy: tính giai thừa của n (tích của các số từ 1 đến n). Ta có
định nghĩa của giai thừa n như sau: n! = 1.2.3 (n-1).n
hoặc định nghĩa:
n! =



≥−
=
1)!.1(
01
nnn
n
Phương pháp thứ nhất là dùng vòng lặp:
long GT(int n)
{
long result = 1;
for(int i=1; i <= n; i++)
result *= i;
return result;
}
Phương pháp thứ hai là dùng hàm đệ quy:
long Giaithua(int n)
{
if (n == 0) return 1;
else return (n*Giaithua(n-1));
}
Phân tích chương trình thực hiện đệ quy:
Giả sử chương trình có lời gọi hàm như sau
long l = Giaithua(5);

17
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
Hình 2.1: Gọi đệ quy của hàm giai thừa.
Lưu ý: Hàm đệ quy dùng nhiều vùng nhớ trên ngăn xếp do đó có thể dẫn đến tràn
ngăn xếp. Do đó nếu một bài toán có thể dùng phương pháp lặp (không đệ quy) để
giải quyết thì nên sử dụng cách này.
Phân loại hàm đệ quy:
 Đệ quy trực tiếp : trong một hàm có lời gọi hàm đến chính bản thân hàm đó.
n = 5
return 5* Giaithua(4)
n = 4
return 4* Giaithua(3)
n = 3
return 3* Giaithua(2)
n = 2
return 2* Giaithua(1)
n = 1
return 1* Giaithua(0)
long l = Giaithua(5)
1
2
6
24
120
Giaithua(5)
Giaithua(4)
Giaithua(3)
Giaithua(2)
Giaithua(1)
n = 0

return 1
Giaithua(0)
1
18
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
- Đệ quy tuyến tính : thân hàm gọi một lần đến chính nó:
U
n
a, n =1
r + U
n-1
, n>1
double U(int n, double a, double r)
{
if (n == 1)
return a ;
return r + U(n-1, a, r) ;
}
- Đệ quy nhị phân : thân hàm có hai lần gọi chính nó
U
n
1, n =1, 2
U
n-2
+ U
n-1
, n>2
long Fibo(int n)
{
if (n<2 ) return 1 ;

return Fibo(n-1) + Fibo(n-1) ;
}
- Đệ quy phi tuyến : thân hàm gọi nhiều lần đến nó
U
n
n, n < 6
U
n-5
+ U
n-4
U
n-3
+ U
n-2
+ U
n-1
, n>=6
long U( int n)
{
if (n<6) return n;
long S= 0;
for (int i = 5; i>0; i )
S+= U(n-i);
return S;
}
- Đệ quy hỗ tương: hai hàm đệ quy gọi nhau
U
n
n, n <5
U

n-1
+ G
n-2
, n>=5
G
n
n-3, n <8
U
n-1
+ G
n-2
, n>=8
long G(int n);
19
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
long U( int n)
{
if (n<5)
return n;
return U(n-1) + G(n-2);
}
long G(int n)
{
if (n<8)
return n-3;
return U(n-1) + G(n-2);
}
 Đệ quy gián tiếp : trong một hàm có lời gọi hàm đến một hàm khác và bên
trong hàm này lại có lời gọi hàm đến hàm ban đầu. Ví dụ như hàm F
1

gọi hàm
F
2
và bên trong hàm F
2
lại có lời gọi hàm đến F
1
. Đây được gọi là sự đệ quy
gián tiếp.
Thông thường những dạng chương trình đệ quy gián tiếp thì khó theo dõi và gỡ rối,
nên khi xây dựng chương trình loại này phải hết sức cẩn thận.
1.2 Xây dựng một chương trình đệ quy
Phương pháp đệ quy thường được áp dụng cho những bài toán phụ thuộc tham số và
có các đặc điểm sau:
1. Bài toán dễ dàng giải quyết trong một số trường hợp riêng ứng với các giá trị đặc
biệt nào đó của tham số. Trường hợp này gọi là suy biến. Ví dụ như khi tính giai
thừa thì giai thừa của 0 là 1.
2. Trong trường hợp tổng quát, bài toán quy về cùng một dạng nhưng giá trị tham số
được thay đổi. Sau một số lần hữu hạn các bước biến đổi đệ quy thì bài toán trở
về trường hợp suy biến. Ví dụ như n! = (n-1)!. n, khi đó n giảm về 0 thì xảy ra
trường hợp suy biến.
Các hàm đệ quy thường có dạng tổng quát như sau:
if (Trường hợp đặc biệt, suy biến)
{
// giải theo cách suy biến, trường hợp này đã có lời giải
}
else // trường hợp tổng quát.
20
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
{

// gọi đệ quy với giá trị tham số khác (thay đổi tham số)
}
Ví dụ 1: Tính tổng các số nguyên từ 1 đến N.
∑∑∑



=
+−+=+=
2
1
1
1
)1(
N
i
N
i
N
i
iNNiNi
Ta phân tích như sau:
+ Trường hợp đặc biệt N=1 thì kết quả là 1
+ Trường hợp khác ta thực hiện đệ quy: N + Tong(N-1).
Ví dụ 2: tìm USCLN của hai số nguyên dương a, b.
+ Trường hợp đặc biệt khi a = b khi đó USCLN(a, b) = a
+ Trường hợp chung a và b khác nhau ta có thể thực hiện đệ quy như sau:
- USCLN(a, b) = USCLN(a-b, b) nếu a>b
- USCLN(a, b) = USCLN(a, b-a) nếu a<b.
Hàm tìm USCLN đệ quy được viết như sau:

int USCLN(int a, int b)
{
if (a==b)
return a;
else if (a>b)
return USCLN(a-b, b);
else
return USCLN(a, b-a);
}
Ví dụ 3: Tính a
n
.
+ Trường hợp đặc biệt n = 0, kết quả là 1
+ Trường hợp khác, kết quả là a * a
(n-1)
.
1.3 Các ví dụ đệ quy
Trong phần này chúng ta sẽ tìm hiểu một số chương trình đệ quy như sau:
 Tháp Hanoi (Tower of Hanoi) :
Cho 3 cột tháp được đặt tên là C
1
, C
2
, và C
3
. Có N đĩa có đường kính giảm dần và
được sắp như hình vẽ. Hãy dịch chuyển N đĩa đó sang cột C
2
, theo nguyên tắc sau:
mỗi lần chỉ dịch được một đĩa, không được để một đĩa có đường kính lớn nằm trên

đĩa có đường kính nhỏ. Ta phân tích cách thực hiện như sau:
21
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
Với N = 2: ta có cách làm như sau: chuyển đĩa bé nhất sang C
3
, chuyển đĩa lớn sang
C
2
, chuyển đĩa nhỏ từ C
3
sang C
2
.
Hình 2.2: Minh họa tháp Hanoi với n =2.
Với N = 3: ta thực hiện với giả thiết đã biết cách làm với N-1 đĩa (2 đĩa trong ví dụ
N=3): chuyển đĩa 1 và 2 sang cọc 3, chuyển đĩa 3 sang cọc 2, chuyển hai đĩa 1, 2 từ
cọc 3 sang cọc 2.
1 1 1 12 2 2 23 3
3 3
22
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
Hình 2.3: Minh họa trường hợp N = 3.
Trong trường hợp N = 3 như hình 2.3, thực hiện ba bước để đưa 3 đĩa về cọc 2: gồm B1,
B2 và B3. Với B2 thì đơn giản do chuyển 1 đĩa, còn bước B1 và B3 phải di chuyển nhiều
hơn 1 đĩa nên chúng sẽ bao gồm nhiều bước nhỏ trong đó. B1 gồm {B1.1, B1.2, B1.3} và
C
1
C
2
C

3
1, 2 qua cọc 3 1, 2 qua cọc 2
3 qua cọc 2
B1 B2 B3
C
1
C
2
C
3
C
1
C
2
C
3
C
1
C
2
C
3
C
1
C
2
C
3
C
1

C
2
C
3
C
1
C
2
C
3
C
1
C
2
C
3
C
1
C
2
C
3
B1.1
B1.2
B1.3 B3.1
B3.2
B3.3
23
C
1

C
2
C
3
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
B2 gồm {B2.1, B2.2, B2.3}. Cuối cùng cách thực hiện theo các bước: B1.1 ⇒ B1.2 ⇒
B1.3 ⇒ B2 ⇒ B3.1 ⇒ B3.1⇒ B3.3.
Hình 2.4: Tháp Hanoi với n = 4.
Chúng ta định nghĩa hàm DichChuyen chuyển N đĩa từ cọc nguồn, sang cọc đích
thông qua một cọc trung gian (cọc thứ 3 còn lại).
Hàm này định nghĩa như sau:
DichChuyen(N, Nguon, Dich, Trung gian);
Với N = 2 ta diễn tả lại như sau:
DichChuyen(1, C1, C3, C2)
DichChuyen(1, C1, C2, C3)
DichChuyen(1,C3, C2, C1)
Với N = 3 ta diễn tả như sau: thông qua dịch chuyển 2 đĩa
DichChuyen(2, C1, C3, C2)
DichChuyen(1, C1, C2, C3)
DichChuyen(2,C3, C2, C1)
Với N tổng quát ta có
DichChuyen(N-1, C1, C3, C2)
DichChuyen(1, C1, C2, C3)
DichChuyen(N-1,C3, C2, C1)
Trường hợp N =1 ta chỉ cần dịch từ cọc nguồn tới cọc đích không cần cọc trung gian.
Đoạn chương trình C/C++ minh họa như sau:
#include <stdio.h>
void DichChuyen(int N, int C1, int C2, int C3);
int main()
{

int N;
C
1
C
2
C
3
24
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
printf(“Nhap so dia: “); scanf(“%d”, &N);
DichChuyen(N, 1, 2, 3);
return 0;
}
void DichChuyen(int N, int C1, int C2, int C3)
{
if (N == 1) printf(“%d - > %d”, C1, C2);
else
{
DichChuyen(N-1, C1, C3, C2);
DichChuyen(1, C1, C2, C3);
DichChuyen(N-1, C3, C2, C1);
}
}
 Tìm phần tử lớn nhất trong mảng dùng đệ quy : cho mảng a[n], n > 1, hãy tìm phần tử
lớn nhất trong mảng a[n]. Ta thử phân tích như sau: ý tưởng là đi từ phần đuôi và so
sánh với phần tử cuối cùng của mảng với biến tạm m, chọn ra phần tử lớn nhất ⇒ lưu
lại vào m. Bước tiếp theo thực hiện tương tự nhưng lúc này mảng rút ngắn lại còn n-1
phần tử.
Hình 2.5 : Tìm phần tử lớn trong mảng dùng đệ quy
Hàm đệ quy tìm phần tử lớn nhất mô tả như sau: giả sử chỉ số mảng tính từ 1.

n =1
n = n-1
a
1
a
2
a
3
a
n-1
a
n
m = Max(m, a
n
)
a
1
a
2
a
3
a
n
m = Max(m, a
n
)
a
n
m = Max(m, a
n

)
25

×