SlideShare a Scribd company logo
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à 2i
{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ó:
Un a, n =1
r + Un-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ó
Un 1, n =1, 2
Un-2 + Un-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ó
Un n, n < 6
Un-5 + Un-4 Un-3 + Un-2+ Un-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
Un n, n <5
Un-1 + Gn-2, n>=5
Gn n-3, n <8
Un-1 + Gn-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 F1 gọi hàm
F2 và bên trong hàm F2 lại có lời gọi hàm đến F1. Đâ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 an
.
+ 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à C1, C2, và C3. 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 C2, 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 C3, chuyển đĩa lớn sang
C2, chuyển đĩa nhỏ từ C3 sang C2.
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à
C1
C2
C3
1, 2 qua cọc 3 1, 2 qua cọc 2
3 qua cọc 2
B1 B2 B3
C1
C2
C3
C1
C2
C3 C1
C2
C3
C1
C2
C3
C1
C2
C3
C1
C2
C3
C1
C2
C3
C1
C2
C3
B1.1
B1.2
B1.3 B3.1
B3.2
B3.3
23
C1 C2 C3
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;
C1
C2
C3
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
a1
a2
a3
an-1
an
m = Max(m, an
)
a1
a2
a3
an
m = Max(m, an
)
an
m = Max(m, an
)
25
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
DeQuyMax(int a[N], int n, int &max)// Gỉa sử n > 0
 if ( n ==1) {max = a[1] ; return;}
 if (max < a[n]) max = a[n];
 DeQuyMax(a, n-1, max);
 Tính tổng các phần tử trong mảng dùng đệ quy: cho dãy a[1:n], gọi hàm Sum là hàm
đệ quy tính tổng, khi đó tổng của dãy a[1:n] là Sum(a[1:n])
Sum(a[1:n]) = Sum(a[1:n-1]) + a[n]
Và Sum(a[m:m]) = a[m], trường hợp m=1 thì Sum(a[1:1]) = a[1]
Hình 2.6: Tổng các phần tử trong mảng.
Hàm đệ quy mô tả bằng mã giả như sau:
Sum(int a[], int n)
- if ( n == 1) Sum = a[1];
- else
 Sum = Sum(a, n-1) + a[n];
Trả về
a1
a2
a3
an-1
an
Sum(n) = an
+ Sum(n-1)
n = n-1
a1
a2
a3
an
Sum(n) = an
+ Sum(n-1)
n = 1
an
Sum(n) = an
= a1
26
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
1.4 Khử đệ quy
1.4.1 Tìm hiểu cơ chế thực hiện hàm đệ quy
Tại mỗi thời điểm của hàm đệ quy được đặc trưng bởi: nội dung các biến và các
lệnh cần thực hiện tiếp theo. Do đó tại mỗi thời điểm trong tiến trình xử lý của hàm
đệ quy cần phải lưu trữ cả các trạng thái xử lý dang dở.
Ví dụ trong hàm đệ quy tính giai thừa n,
GT(n):
if (n == 0) return 1;
else return (n* GT(n-1));
Trong trường hợp n = 3
Hình 2.7: Gọi đệ quy hàm GT.
Khi thực hiện lời gọi GT(3) thì sẽ phát sinh lời gọi hàm đến GT(2) và đồng thời
phải lưu giữ thông tin trạng thái xử lý còn dang dở GT(3) = 3 * GT(2). Đến lượt hàm
GT(2) sẽ phát sinh lời gọi hàm đến GT(1) và lưu giữ thông tin trạng thái còn dang dở
GT(2) = 2 * GT(1)…Quá trình cứ thực hiện tương tự cho tới khi gặp trường hợp suy
biến GT(0) = 1.
GT(3) = 3 * GT(2)
GT(2) = 2 * GT(1)
GT(1) = 1 * GT(0)
GT(0) = 1
27
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
Kết thúc quá trình gọi đệ quy là quá trình xử lý ngược được thực hiện:
Giá trị của GT(0) được dùng để tính GT(1) theo quá trình lưu trữ
Dùng giá trị GT(1) để tính GT(2) theo quá trình tương tự
Dùng giá trị GT(2) để tính GT(3) để ra kết quả cuối cùng
Song song với quá trình xử lý ngược là xóa bỏ thông tin lưu trữ trong những lần gọi
hàm tương ứng.
Ví dụ hàm đệ quy tính giá trị dãy Fibonacci
Fibo(n)
if (n ==0) || (n == 1) return 1;
else
return (Fibo(n-1) + Fibo(n-2));
Hình 2.8: Hàm đệ quy tính dãy Fibonacci.
Do đặc điểm của quá trình xử lý một hàm đệ quy: việc thực thi lời gọi đệ quy sinh
ra lời gọi đệ quy mới cho đến khi gặp trường hợp suy biến, do đó cần phải có cơ chế
lưu trữ thông tin thoả yêu cầu:
o Ở mỗi lần gọi phải lưu trữ thông tin trạng thái con còn đang xử lý dang dở,
số trạng thái này bằng với số lần gọi chưa hoàn tất.
o Sau khi thực hiện xong một lần gọi thứ k, cần khôi phục lại toàn bộ thông
tin trạng thái của lần gọi trước đó là lần gọi k-1.
Fibo(4) = Fibo(2) + Fibo(3)
Fibo(2) = Fibo(1) + Fibo(0) Fibo(3) = Fibo(2) + Fibo(1)
Fibo(1) = 1 Fibo(0) = 1
Fibo(2) = Fibo(1) + Fibo(0) Fibo(1) = 1
Fibo(1) = 1 Fibo(0) = 1
28
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
o Lệnh gọi cuối cùng (trường hợp suy biến) sẽ được hoàn tất trước tiên. Các
lệnh gọi sau sẽ hoàn thành trước, do đó dãy thông tin trạng thái được hồi
phục theo thứ tự ngược với thứ tự lưu trữ.
Cấu trúc dữ liệu ngăn xếp lưu trữ theo kiểu Last In First Out thoả các yêu cầu trên
nên được sử dụng để lưu trữ thông tin trạng thái của quá trình xử lý đệ quy.
Thông thường đệ quy là phương pháp giúp chúng ta tìm giải thuật cho những bài
toán khó. Kết quả của giải thuật đệ quy thường rất gọn gàng, dễ hiểu và dễ chuyển
thành các chương trình trên các ngôn ngữ lập trình. Tuy nhiên, việc xử lý giải
thuật đệ quy cũng gây khó khăn cho máy về không gian lưu trữ và thời gian xử lý.
Vì vậy việc thay thế một chương trình đệ quy bằng một chương trình không đệ
quy cũng được quan tâm rất nhiều.
Thông thường khi gặp một bài toán khó giải quyết theo hướng không đệ quy thì
người ta thực hiện quá trình như sau:
o Dùng quan niệm đệ quy để tìm giải thuật cho bài toán
o Mã hoá giải thuật đệ quy
o Khử đệ quy để có một chương trình không đệ quy.
Quá trình trên gọi là khử đệ quy, đôi khi việc khử đệ quy cũng không dễ dàng gì,
nên nhiều khi cũng phải chấp nhận chương trình đệ quy!
1.4.2 Các trường hợp khử đệ quy đơn giản
o Hàm tính giá trị của dãy dữ liệu mô tả bằng hồi quy:
Ví dụ 1: hàm tính giai thừa không đệ quy
long int GiaiThua( int n)
{
long int F =1;
for (int k = 1; k <= n; k++)
F = k*F;
return (F);
}
Ví dụ 2: hàm tính Sn không đệ quy
int Sn(int n)
{
int k = 1;
int tg = 1;
while ( k < n )
{
29
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
k++;
if ( k % 2 )
tg += 2*k -1;
else
tg -= 2*k + 1;
}
}
o Dạng đệ quy đuôi
Một hàm đệ quy đuôi P có dạng như sau:
P(X)
{
if B(X) D(X)
else
{
A(X)
P(f(X))
}
}
Trong đó:
X: là biến (một hay nhiều biến)
P(X): là hàm đệ quy phụ thuộc X
A(X) và D(X): là các nhóm lệnh không đệ quy
f(X): là hàm biến đổi x
trong lần gọi thứ Pi nếu B(fi(X)) không đúng thì thực hiện lệnh X và gọi
Pi+1, ngược lại B(fi(X)) đúng thì thực hiện D(X) và kết thúc quá trình gọi (Pi ko
gọi thêm hàm đệ quy khác).
Ví dụ: Tìm USCLN của hai số dựa vào thuật toán Euclide
Giải thuật đệ quy USCLN(m ,n) bằng Euclide như sau :
void USCLN( int m, int n, int & kq)
{
if ( n ==0) kq = m ;
else
USCLN(n, m %n, kq) ;
}
Trong trường hợp này:
30
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
X là m, n và kq
P(X) : USCLN(m, n, kq)
B(X) : n ==0
D(X) : kq = m ;
A(X) : không có
f(x): USCLN(n, m %n, kq)
Hàm USCLN không đệ quy được thể hiện như sau:
void USCLN(int m, int n, int & kq)
{
int temp;
while (n !=0)
{
temp = m %n;
m = n;
n = temp;
}
kq = m;
}
1.4.3 Khử đệ quy dùng stack
Để thực hiện một chương trình con đệ quy thì hệ thống phải tổ chức vùng
nhớ lưu trữ theo quy tắc LIFO. Các ngôn ngữ lập trình cấp cao đều có khả năng
tạo vùng nhớ stack mới cho phép tổ chức các chương trình đệ 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ớ. Do cách tổ chức
stack mặc định thích hợp cho mọi trường hợp nên sẽ không tối ưu trong từng
trường hợp cụ thể. Do đó sẽ tốt khi người lập trình chủ động tạo cấu trúc dữ liệu
stack đặc dụng cho từng chương trình đệ quy cụ thể.
Giả sử thủ tục đệ quy trực tiếp có cấu trúc như sau :
P(X)
{
if C(X) D(X) ;
else
A(X) ;
P(f(X)) ;
B(X) ;
31
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
}
Trong đó
X : là một hay nhiều biến
C(X) : biểu thức điều kiện theo X
A(X), B(X) và D(X) : nhóm lệnh không đệ quy
f(X) : là hàm của X
Quá trình thực hiện thủ tục P(X) như sau:
Nếu C(X) đúng thì thực hiện D(X)
Ngược lại thực hiện A(X), gọi P(f(X)), thực hiện B(X) sau khi hoàn thành
P(f(X)).
Mỗi lần P(Y) được gọi thì thông tin của B(Y) lại được sinh ra nhưng chưa thực
hiện.
Giả sử quá trình đệ quy kết thúc sau k lần gọi đệ quy thì chương trình phải thực
hiện 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 trên ta cần xây dựng stack để lưu trữ tạm.
Giải thuật thực hiện P(X) với việc sử dụng stack có dạng:
P(X)
{
CreateStack(S) ;
while ( ! C(X))
{
A(X) ;
Push(S, X) ;
X = f(X) ;
}
D(X) ;
while ( !Empty(S))
{
Pop(S, X) ;
B(X) ;
32
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
}
}
Ví dụ: thủ tục đệ quy biểu diễn số thập phân sang nhị phân có dạng:
void Binary(int m)
{
if (m >0)
{
Binary( m / 2);
printf("%d", m % 2);
}
}
Trong đó:
X là m
P(X) là Binary(X)
A(X) và D(X) không có
B(X) là lệnh printf("%d", m % 2) ;
C(X) là m ≤ 0
f(X) = f(m) = m / 2 ;
Giải thuật không đệ quy như sau:
void Binary( int m)
{
int temp;
CreateStack(S);
while (m > 0)
{
temp = m % 2;
Push(S, temp);
m = m / 2;
}
while (! Empty(S))
{
Pop(S, temp);
printf(“%d”, temp);
}
33
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
}
Lệnh gọi đệ quy với hai lần gọi trực tiếp:
Thủ tục đệ quy có dạng như sau:
P(X)
{
if C(X)
D(X)
else
{
A(X);
P(f(X));
B(X);
P(g(X));
}
}
Quá trình thực hiện thủ tục đệ quy P(X) như sau:
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) và gọi P(g(X)); khi
đó ngoài việc lưu giá trị fi(X) tương ứng chương trình còn phải lưu thêm các giá
trị gi(X) phát sinh tương ứng…
Do đó ngoài dữ liệu X, chương trình còn phải lưu vào ngăn xếp thêm thứ tự lần
gọi.
Thủ tục khử đệ quy dùng stack trong trường hợp này có dạng như sau:
P(X)
{
CreateStack(S);
Push(S, (X, 1));
do
{
while ( !C(X))
{
34
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
A(X);
Push(S, (X, 2));
X = f(X);
}// end while
D(X);
Pop(S, (X, k));
if ( k != 1)
{
B(X);
X = g(X);
}// end if
} while (k > 1);
}
Ví dụ: khử đệ quy của thủ tục tháp Hanoi
Dạng thủ tục đệ quy của tháp Hanoi như sau:
Hanoi(n, a, b, c)
{
if (n>0)
{
Hanoi(n-1, a, c, b);
Move(a, c);
Hanoi(n-1, b, a, c);
}
}
Trong đó n là số đĩa, a là cột đầu tiên, b là cột trung gian, và c là cột cuối cùng,
Move(x, y) là thao tác chuyển 1 đĩa từ cột x sang y.
Trong trường hợp này:
X là bộ (n, a, b, c);
C(X) là (n ≤ 0)
A(X) và D(X) là rỗng
B(X) là B(n,a, b, c) = Move(a, c)
f(X) là f(n, a, b, c) = Hanoi(n-1, a, c, b)
35
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
g(X) là g(n, a, b, c) = Hanoi(n-1, b, a, c)
Giải thuật không đệ quy tương ứng như sau:
Create_Stack(S) ;
Push(S, (n, a, b, c, 1));
do
{
while (n > 0)
{
Push(S, (n, a, b, c, 2));
n = n-1;
Swap(b, c);
}
Pop(S, (n, a, b, c, k));
if ( k != 1)
{
Move(a, c);
n = n-1;
Swap(a, b);
}
} while (k>1);
 
36
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
Chương 2
Bài toán liên quan tổ hợp
 
2.1 Phương pháp sinh
Phương pháp sinh được áp dụng để giải quyết bài toán liệt kê của lý thuyết tổ hợp. Để
áp dụng được phương pháp này thì bài toán phải thoả mãn hai điều kiện sau:
o Có thể xác định được thứ tự trên tập các cấu hình tổ hợp cần liệt kê. Từ đó có
thể xác định được cấu hình đầu tiên và cấu hình cuối cùng trong thứ tự đó.
o Xây dựng được một thuật toán cho phép từ một cấu hình chưa phải cấu hình
cuối, sinh ra được cấu hình kế tiếp của nó.
Phương pháp sinh có thể được mô tả tổng quát như sau:
<Xây dựng cấu hình đầu tiên>
Do
<Đưa ra cấu hình đang có>
<Từ cấu hình đang có sinh ra cấu hình kế tiếp>
While <Còn cấu hình or khác cấu hình cuối>
2.1.1 Bài toán sinh dãy nhị phân độ dài n
 Bài toán: một tập hợp hữu hạn có n phần tử có thể được biểu diễn tương đương
với tập các số tự nhiên 1, 2, .., n.
Bài toán đặt ra là: cho một tập hợp gồm n phần tử X = {X1, X2,.., Xn} hãy liệt kê tất cả
các tập con của tập này.
Để biểu diễn tập con Y của X ta dùng xâu nhị phân Bn = {B1, B2,.., Bn}, sao cho nếu
Bi = 0 thì Xi∉ Y, ngược lại Bi = 1 thì Xi ∈ Y.
Ví dụ như dãy 0011 của tập hợp gồm n thể hiện cho tập Y = {X3, X4} do phần tử B3
và B4 có giá trị là 1.
Khi đó ta quy về bài toán liệt kê tất cả xâu nhị phân có kích thước n. Số các xâu nhị
phân là 2n
.
37
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
Một dãy nhị phân x độ dài n là biểu diễn một số nguyên p(x) nào đó trong đoạn [0,
2n
-1]. Do đó số các dãy nhị phân độ dài n = số các số nguyên ∈ [0, 2n
-1] = 2n
.
Mục tiêu là lập một chương trình liệt kê các dãy nhị phân n phần tử theo thứ tự từ
điển, có nghĩa là liệt kê dãy nhị phân biểu diễn các số nguyên theo thứ tự 0, 1,.., 2n
-1.
Khi n =3, các độ dài 3 được liệt kê như sau:
p(x) 0 1 2 3 4 5 6 7
x 000 001 010 011 100 101 110 111
Khi đó dãy đầu tiên là: 000 và dãy cuối cùng là 111. Nhận thấy rằng nếu x là dãy
đang có và phải là dãy cuối cùng thì dãy tiếp theo cần liệt kê chính là x cộng thêm 1
đơn vị trong hệ nhị phân!
Ví dụ n = 6:
Dãy đang có: 010000 Dãy đang có: 010111
Cộng thêm 1: +1 Cộng thêm 1: +1
______ ______
Dãy mới: 010001 Dãy mới: 011000
Kỹ thuật sinh kế tiếp từ cấu hình hiện tại có thể mô tả như sau: xét từ cuối dãy lên từ
hàng đơn vị tìm số 0 đầu tiên.
 Nếu tìm thấy thì thay số 0 bằng số 1 và đặt tất cả phần tử phía sau
vị trí đó bằng 0.
 Nếu không tìm thấy thì toàn là dãy chứa 1, đây là cấu hình cuối
cùng.
Chương trình minh họa 1: chương trình C/C++ liệt kê chuỗi nhị phân n bit.
int Stop; // biến toàn cục
void Next_BS(int B[MAX], int n) // Hàm phát sinh chuỗi kế tiếp
{
int i = n; // duyệt từ cuối
while (i>0 && B[i]) // lặp khi chưa tìm thấy B[i] ==0
{
B[i] = 0; // gán các bit sau là 0
i--; // giảm về trước
}
if (i==0 )
Stop = 1; // cấu hình cuối nên không tìm được B[i] = 0 -> dừng
38
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
else
B[i] = 1; // gán 1 cho B[i]
}
void Generate(int B[MAX], int n) // Hàm sinh chuỗi nhị phân
{
Stop = 0;
while (! Stop)
{
Result(B,n); // xuất chuỗi nhị phân hiện tại
Next_BS(B,n); // chuỗi nhị phân tiếp theo.
}
}
void Result(int B[MAX], int n)
{
static int count=0;
printf(“n Xau nhi phan thu %d”, ++count);
for(int i=0; i < n;i++)
printf(“%3d”, B[i]);
}
int main()
{
int i, B[MAX], n;
printf(“Nhap n: ”); scanf(“%d”,&n);
for(i=0; i< n; i++)
B[i] =0;
Generate(b, n);
getch();
return 0;
}
2.1.2 Bài toán liệt kê tập con k phần tử
 Phát biểu: Cho tập hợp X = {1, 2,.., n}. Hãy liệt kê tất cả tập con k phần tử của X.
Mỗi tập con k phần tử của X cho thể biểu diễn như bộ thứ tự:
a = (a1, a2,.., ak) thỏa mãn 1 ≤ a1 ≤ a2 ≤ ... ≤ ak ≤ n. Trên tập con k phần tử của X, ta
định nghĩa thứ tự của các tập con như sau:
Ta nói tập a = (a1, a2,.., ak) có thứ tự trước tập a’ = (a’1, a’2,.., a’k) theo thứ tự từ điển
và ký hiệu là a < a’ nếu tìm được j sao cho: a1 = a’1, a2 = a’2..., aj-1 = a’j-1 và aj < a’j.
Ví dụ với n = 5, k = 3, ta liệt kê 10 tập con của nó như sau:
{{1,2,3},{1,2,4}{1,2,5}{1,3,4}{1,3,5}{1,4,5}{2,3,4}{2,3,5}{2,4,5}{3,4,5}}
39
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
+ Ta thấy cấu hình đầu tiên là {1, 2..., k}
+ Cấu hình kết thúc là {n-k+1, n-k+2,.., n}.
Nhận xét: chúng ta sẽ in ra tập con với các phần tử của nó theo thứ tự tăng dần. Biểu
diễn tập con là một dãy a{a1, a2,..., ak} trong đó a1< a2 <...<ak. Ta nhận thấy giới hạn
trên của ak là n, của ak-1 là n-1, của ak-2 là n-2.
Tổng quát giới hạn trên của ai = n-k+i.
Còn giới hạn dưới của của ai (giá trị nhỏ nhất ai có thể nhận) là ai-1 + 1.
Như vậy nếu ta đang có một dãy x đại diện cho tập con, nếu x là cấu hình kết thúc thì
có nghĩa tất cả các phần tử trong x đều đạt tới giới hạn trên thì quá trình sinh kết thúc.
Nếu không thì phải phát sinh một dãy x tăng dần thỏa mãn đủ lớn hơn dãy x và không
có dãy nào chen vào giữa hai dãy theo thứ tự từ điển.
Ví dụ: n = 9, k = 6, cấu hình đang có <1, 2, 6, 7, 8, 9>, các phần tử a3 ⇒a6 đã đạt đến
giới hạn nên ta không thể tăng các phần tử này được, ta phải tăng a2 từ 2 lên thành 3.
Được cấu hình mới là <1, 3, 6, 7, 8, 9> cấu hình này thoả mãn lớn hơn cấu hình cũ,
nhưng chưa thoả mãn tính chất vừa đủ lớn do đó ta phải thay a3, a4, a5, a6 bằng giới
hạn dưới của nó như sau:
a3 = a(3-1= 2) + 1 = 3 + 1 = 4
a4 = a(4-1= 3) + 1 = 4 + 1 = 5
a5 = a(5-1= 4) + 1 = 5 + 1 = 6
a6 = a(6-1= 5) + 1 = 6 + 1 = 7
Vậy cấu hình tiếp theo <1, 3, 4, 5, 6, 7> là cấu hình cần tìm. Do đó muốn xác định
cấu hình tiếp ta thấy a6 = 7 chưa đạt đến giới hạn ta chỉ cần tăng a6 lên một là được
cấu hình tiếp theo: <1, 3, 4, 5, 6, 8>.
Vậy kỹ thuật sinh tập con kế tiếp từ tập x đã có có thể xây dựng như sau:
Tìm từ cuối lên đầu dãy cho tới khi gặp phần tử ai chưa đạt đến giới hạn n-k+i.
 Nếu tìm thấy:
o Tăng ai đó lên 1.
o Đặt tất cả phần tử phía sau ai bằng giới hạn dưới.
 Nếu không tìm thấy tức là phần tử đã đạt giới hạn trên, đây là cấu hình cuối
cùng. Kết thúc thuật toán.
Chương trình minh họa 2: liệt kê tập con k phần tử của n.
40
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
int A[MAX], Stop, n ,k;
void Next_SubSet()
{
int i,j;
i = k; // duyệt từ cuối dãy
// lặp khi chưa tìm được phần tử chưa tới giới hạn
while (i >0 && A[i] == n-k+i)
i--; // duyệt về đầu
if ( i > 0)
{
A[i] = A[i] +1; // tăng một đơn vị
// cho các phần tử còn lại qua giới hạn dưới
for(j = i+1; j <= k; j++)
A[j] = A[j-1]+ 1
}
else
Stop = 1; // kết thúc phát sinh cấu hình
}
void GenerateSet()
{
Stop = 0;
while (!Stop)
{
Result(); // xuất cấu hình hiện tại
Next_SubSet(); // qua cấu hình khác
}
}
void Result()
{
static int count=0;
printf(“Tap con thu %d”, ++count);
for(i=1; i <=k; i++)
printf(“%3d”, A[i]);
}
int main()
{
printf(“Nhap n: ”); scanf(“%d”, &n);
printf(“Nhap k: ”); scanf(“%d”, &k);
for(int i=1; i <= n;i++)
A[i] = i;
GenerateSet();
getch();
return 0;
41
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
}
2.1.3 Bài toán liệt kê các hoán vị
 Bài toán: Cho tập hợp X = {1, 2, ..., n}, hãy liệt kê tất cả hoán vị của X. Mỗi hoán
vị n phần tử của tập X có thể biểu diễn bởi bộ có thứ tự gồm n thành phần a = {a1,
a2,.., an} thoả ai ∈ X; i = 1, 2, .., n; ap ≠ aq nếu p ≠ q. Trên các tập hoán vị của X ta
định nghĩa thứ tự của các hoán vị như sau:
a = (a1, a2,..., an) được gọi là có thứ tự trước hoán vị a’=(a’1,a’2,..,a’n). Có ký hiệu a <
a’ nếu tìm được chỉ số k sao cho.
a1 = a’1, a2 = a’2,..., ak-1 = a’k-1, ak <a’k.
Ví dụ X = {1, 2, 3, 4} khi đó thứ tự hoán vị n = 4 được liệt kê như sau:
{{1, 2, 3, 4}, {1, 2, 4, 3}, {1, 3, 2, 4} {1, 3, 4, 2} {1, 4, 2, 3} {1, 4, 3, 2}
{2, 1, 3, 4}, {2, 1, 4, 3}, {2, 3, 1, 4} {2, 3, 4, 1} {2, 4, 1, 3} {2, 4, 3, 1}
{3, 1, 2, 4}, {3, 1, 4, 2}, {3, 2, 1, 4} {3, 2, 4, 1} {3, 4, 1, 2} {3, 4, 2, 1}
{4, 1, 2, 3}, {4, 1, 3, 2}, {4, 2, 1, 3} {4, 2, 3, 1} {4, 3, 1, 2} {4, 3, 2, 1}}
Hoán vị đầu tiên là: {1, 2, ..., n-1, n} và hoán vị cuối cùng là {n, n-1,..,2, 1}.
Khi đó hoán vị kế tiếp sinh ra phải lớn hơn hoán vị hiện tại, và hơn nữa nó phải đủ
lớn hơn hoán vị hiện tại theo nghĩa không có hoán vị nào khác chen vào giữa nó khi
sắp theo thứ tự từ điển.
Giả sử có hoán vị sau: <3, 2, 6, 5, 4, 1>, ta xét 4 phần tử cuối cùng, do chúng được
sắp theo thứ tự giảm dần. Khi đó ta hoán vị 4 giá trị này thì cũng chỉ được hoán vị
nhỏ hơn hoán vị hiện tại. Như vậy ta phải xét đến a2 = 2, ta phải thay giá trị này,
nhưng thay giá trị nào? ta không thể thay bằng 1 vì nếu như vậy sẽ được hoán vị nhỏ
hơn, không thể thay bằng 3 vì giá trị này đã có rồi a1 = 3 (phần tử sau không được
chọn vào những giá trị xuất hiện ở phần tử trước). Chỉ còn lại giá trị 4, 5, 6. Vì cần
một hoán vị đủ lớn nên ta chọn a2 = 4. Còn các giá trị a3, a4, a5, a6 sẽ lấy trong tập {2,
6, 5, 1}. Cũng do tính chất vừa đủ lớn nên ta sẽ tìm biểu diễn nhỏ nhất của 4 số này
để gán cho a3, a4, a5, a6, là <1, 2, 5, 6> vậy ta được hoán vị mới là <3, 4, 1, 2, 5, 6>
Nhận xét: đoạn cuối của hoán vị hiện tại được sắp giảm dần. số a5 là 4 là số nhỏ nhất
trong đoạn cuối lớn hơn a2 = 2. Nếu đổi chỗ a5 cho a2 thì ta được a2 = 4 và đoạn cuối
vẫn được xếp giảm dần là <6, 5, 2, 1> khi đó muốn biểu diễn nhỏ nhất cho các giá trị
trong đoạn cuối thì ta chỉ cần đảo ngược đoạn cuối.
42
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
Ví dụ trong hoán vị hiện tại <2, 1, 3, 4> có hoán vị kế tiếp là <2, 1, 4, 3>. Ta có thể
xem <2, 1, 3, 4> có đoạn cuối giảm dần là một phần tử <4>.
Vậy kỹ thuật sinh hoán vị kế tiếp từ hoán vị hiện tại có thể xây dựng như sau:
Xác định đoạn cuối giảm dần dài nhất, tìm phần tử ai đứng trước đoạn cuối
đó. Điều này đồng nghĩa với việc tìm từ vị trí sát cuối dãy lên đầu, gặp chỉ số i
đầu tiên thoả mãn ai < ai+1.
 Nếu tìm thấy chỉ số i như trên: trong đoạn cuối giảm dần, tìm phần tử ak nhỏ
nhất thoả mãn ak > ai. Do đoạn cuối giảm dần nên thực hiện bằng cách từ cuối
dãy lên đầu gặp chỉ số k đầu tiên thoả ak > ai.
o Đảo giá trị ak và ai.
o Lật ngược thứ tự đoạn cuối giảm dần (ai+1 đến ak) trở thành tăng dần
 Nếu không tìm thấy tức là dãy giảm dần, đây là cấu hình cuối cùng.
Chương trình minh họa 3: Liệt kê hoán vị n phần tử.
int n, P[MAX], Stop;
void Next_Permutation()
{
int j, k;
j = n -1;
while (j>0 && P[j]> P[j+1]) j--;
if (j == 0)
Stop = 1;
else
{
k = n;
while (P[j] > P[k]) k--;
Swap(P[j], P[k]);
l = j+1;
r = n;
while (l < r)
{
Swap(P[l], P[r]);
l++;
r--;
}
}// end else
}
void Permutation()
43
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
{
Stop = 0;
while (! Stop)
{
Result();
Next_Permutation();
}
}
void Result()
{
static int count=0;
printf(“n Hoan vi thu %d”, ++count);
for(int i=1; i <= n; i++)
printf(”%3d”, P[i]);
}
int main()
{
printf(“Nhap n: ”); scanf(“%d”, &n);
for(int i=1; i <= n; i++)
P[i] = i;
return 0;
}
44
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
2.2 Thuật toán quay lui (Back Tracking)
Thuật toán quay lui dùng để giải quyết các bài toán liệt kê các cấu hình. Phương
pháp sinh trong phần trước cũng được giải quyết cho các bài toán liệt kê khi nhận biết
được cấu hình đầu tiên của bài toán.
Tuy nhiên, không phải bất cứ cấu hình sinh kế tiếp nào cũng có thể sinh một cách
đơn giản từ cấu hình hiện tại. Do đó thuật toán sinh kế tiếp chỉ giải quyết được cái bài
toán liệt kê đơn giản. Để giải quyết những bài toán tổ hợp phức tạp, người ta dùng
thuật toán quay lui.
Nội dung chính của thuật toán quay lui:
Xây dựng dần dần các thành phần của cấu hình bằng cách thử tất cả các khả năng có
thể xảy ra. Giả sử cấu hình cần liệt kê có dạng x = (x1, x2,..,xn) khi đó thuật toán quay
lui thực hiện qua các bước:
1. Xét tất cả những giá trị có thể có của x1, thử cho x1 nhận lần lượt các giá trị
đó. Với mỗi giá trị thử gán cho x1 ta sẽ làm tiếp như sau:
2. Xét tất cả giá trị x2 có thể nhận, lại thử cho x2 nhận lần lượt các giá trị đó. Với
mỗi giá trị x2 ta lại xét lần lượt những giá trị của x3... cứ tiếp tục như vậy cho
đến bước n.
3. Xét giá trị có thể nhận cho xn, thử cho xn lần lượt nhận những giá trị đó, thông
báo những cấu hình tìm được như (x1, x2,..., xn).
Tóm lại thuật toán quay lui liệt kê các cấu hình n phần tử dạng x = (x1, x2,.., xn)
bằng cách thử cho x1 nhận lần lượt các giá trị có thể được. Với mỗi giá trị thử gán cho
x1 thì bài toán trở thành liệt kê tiếp cấu hình n-1 phần tử x = (x2, x3,.., xn).
45
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
Hình 3.1: Liệt kê các lời giải theo thuật toán quay lui.
Mô hình chung của thuật toán quay lui xác định thành phần thứ i được mô tả tổng
quát như sau: (thuật toán này thử cho xi nhận lần lượt những giá trị mà nó có thể
nhận).
void Try(int i)
{
for <mọi giá trị v có thể gán cho x[i]> do
{
<Thử cho x[i] bằng giá trị v>
if <x[i] là phần tử cuối cùng trong cấu hình hoặc i==n> then
<Thông báo cấu hình tìm được>
else
{
<Ghi nhận việc cho x[i] nhận giá trị v (nếu cần thiết)>
Try(i+1); // gọi đệ quy cho tiếp chi x[i+1].
<Nếu cần thì bỏ ghi nhận việc thử x[i]:= v để thử giá trị khác>
}
}
}
Thuật toán quay lui sẽ bắt đầu bằng lời gọi Try(1).
Gốc
Khả năng chọn x1
Khả năng chọn x2
với x1
đã chọn
Khả năng chọn x3
với x1
và x2
đã chọn
46
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
2.2.1 Thuật toán quay lui liệt kê dãy nhị phân n
Biểu diễn dãy nhị phân độ dài n dưới dạng x = (x1, x2,..., xn) trong đó xi nhận các
giá trị là {0, 1}. Với mỗi giá trị gán cho xi ta lại thử gán các giá trị có thể có cho xi+1.
Thuật toán quay lui được viết như sau:
void Try(int i, int B[MAX], int n)
{
int j;
for(j=0; j <= 1; j++)
{
B[i] = j;
if (i == n)
Result(B, n);
else
Try(i+1, B, n);
}
}
void Result(int B[MAX], int n)
{
int i;
printf(“n”);
for(i=1; i <= n; i++)
printf(“%3d”, B[i]);
}
int main()
{
int n, B[MAX];
printf(“Nhap n: ”);
scanf(“%d”, &n);
for(int i=1; i <= n; i++) // khởi tạo cho mảng B
B[i] = 0;
Try(1, B, n); // gọi thuật toán quay lui
return 0;
}
Khi n = 3, cây tìm kiếm quay lui như sau:
47
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
Hình 3.2: Cây tìm kiếm quay lui trong bài toán liệt kê dãy nhị phân.
2.2.2 Thuật toán quay lui liệt kê tập con k phần tử
Để liệt kê tập con k phần tử của tập S = {1, 2, ..., n} ta có thể đưa về liệt kê các
cấu hình x = (x1, x2,.., xn) ở đây xi ∈ S và x1 < x2 < ...< xn. Từ đó giá trị được chọn cho
xi là xi-1 + 1 cho đến n –k+i (1 ≤ i ≤ k ), giả thiết có thêm số x0 = 0 khi xét i =1.
Như vậy xét tất cả cách chọn x1 từ 1 (x0 +1) đến n-k+1, với mỗi giá trị đó, xét tiếp tất
cả cách chọn x2 từ x1+1 đến n-k+2...cứ như vậy khi chọn được xk thì ta có cấu hình
cần liệt kê.
Với trường hợp n = 5 {1, 2, 3, 4, 5} và k = 3 thuật toán quay lui liệt kê tập con k phần
tử được minh họa như sau:
Try(1)
Try(2) Try(2)
Try(3) Try(3) Try(3) Try(3)
000
x1
= 1x1
= 0
x2
= 0 x2
= 1 x2
= 0 x2
= 1
x3
= 0 x3
= 0 x3
= 0 x3
= 0x3
= 1 x3
= 1 x3
= 1 x3
= 1
001 010 011 100 101 110 111
48
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
Hình 3.3: Cây liệt kê tập con 3 phần tử với n = 5.
Chương trình quay lui liệt kê tập k phần tử:
void Try(int i, int B[MAX], int k, int n)
{
int j;
for(j = B[i-1] + 1; j <= (n-k+i); j++)
{
B[i] = j;
if (i == k) Result(B, k);
else
Try(i+1, B, k, n);
}
}
void Result(int B[MAX], int k)
{
static int count=0;
printf(“Tap con thu %d: ”, ++count);
for(i=1; i <= k; i++)
printf(“%3d”, B[i]);
}
int main()
{
int n, k, B[MAX];
printf(“Nhap n: ”); scanf(“%d”,&n);
printf(“Nhap k: ”); scanf(“%d”, &k);
B[0] = 0;
1
2
3 4 5 4 5
3 4
5
3 4
2 3
4
4 5 5 5
N = 5; k = 3
49
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
Try(1, B, k, n);
return 0;
}
2.2.3 Thuật toán quay lui liệt kê hoán vị n phần tử
Biểu diễn hoán vị dưới dạng p1, p2,.., pn, trong đó pi nhận giá trị từ 1 đến n và pi ≠
pj với i ≠ j. Các giá trị từ 1 đến n được đề cử cho pi, trong đó giá trị j được chấp nhận
nếu nó chưa được dùng trước đó. Do đó cần phải ghi nhớ xem giá trị j đã được dùng
chưa. Ta thực hiện điều này bằng một mảng B, trong đó Bj = true nếu j chưa được
dùng và ngược lại. Đầu tiên các giá trị trong B này phải được khởi tạo là true, sau khi
gán j cho xi thì ghi nhận Bj = false, sau khi gọi xong thủ tục Try(i+1) thì thiết lập lại
Bj = true, để đánh dấu nó chưa được dùng để cho bước thử tiếp theo.
Hình 3.4: Cây liệt kê hoán vị 3 phần tử
Chương trình quay lui liệt kê hoán vị m phần tử:
void Try(int i, int P[MAX], int B[MAX], int n)
{
int j;
for(j = 1; j <= n; j++)
if (B[j] == 1)
1 2 3
n = 3
2 3 1 3 1 2
3 2 3 1 2 1
50
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
{
P[i] = j;
B[j] = 0; // đánh dấu đã sử dụng j
if (i == n)
result(P, n); // xuất kết quả
else
Try(i+1, P, B, n); // thử cho bước tiếp theo
B[j] = 1; // bỏ đánh dấu phần đã sử dụng j
}
}
void Result(int P[MAX], int n)
{
static int count=0;
printf(“Hoan vi thu %d”, ++count);
for(int i=1; i<= n; i++)
printf(”%3d”, P[i]);
}
int main()
{
int P[MAX], B[MAX], n;
printf(“Nhap n: ”); scanf(“%d”, &n);
for(int i=1; i <=n; i++)
B[i] = 1;
Try(1, P, B, n);
return 0;
}
2.2.4 Bài toán sắp xếp quân Hậu
Yêu cầu: cho một bàn cờ vua nxn, hãy liệt kê cách sắp xếp n quân hậu sao cho các
quân hậu không ăn được nhau! Quân hậu trên bàn cờ có thể ăn quân khác trên cùng
hàng, cùng cột hoặc cùng đường chéo.
Phân tích: các quân hậu sẽ được sắp trên các dòng khác nhau do chúng có thể ăn
theo hàng ngang. Để dễ phân tích ta mô tả quân hậu theo dòng; quân hậu 1 ở dòng 1,
quân hậu i ở dòng i…Do mỗi quân hậu chắc chắn nằm trên các dòng khác nhau nên ta
chỉ cần xác định vị trí cột của mỗi quân hậu là xong.
Tiếp theo ta xét những ràng buộc theo đường chéo, có hai đường chéo:
o Chéo “”: theo hướng Trên Trái - Dưới Phải
o Chéo “/”: theo hướng Trên Phải - Dưới Trái
51
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
Hình 3.5: Các nước đi của quân hậu có thể có.
Hình 3.6: Một cách sắp xếp 8 hậu trên bàn cờ 8x8
Các đường chéo Trên Trái - Dưới Phải như hình vẽ dưới, mỗi đường chéo này sẽ đi qua
các ô, các ô này có tính chất: dòng - cột = C (hằng số). Do đó với mỗi đường chéo ta có 1
hằng số C và 1-n ≤ C ≤ n-1 xác định duy nhất đường chéo đó.
52
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
Hình 3.7: Các đường chéo Trên Trái - Dưới Phải
Các đường chéo Trên Phải - Dưới Trái: mỗi đường chéo này sẽ đi qua các ô có tính chất
sau: dòng + cột = C (hằng số). Do đó với mỗi đường chéo ta có một hằng số C và 2 ≤ C
≤ 2n.
Hình 3.8: Các đường chéo Trên Phải - Dưới Trái.
1
2
3
4
5
6
7
8
1 2 3 4 5 6 7 8
0
-1
-7
7
4
1
2
3
4
5
6
7
8
1 2 3 4 5 6 7 8
16
14
92
53
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
Hình 3.9: Vị trí của quân hậu ảnh hưởng đến 2 đường chéo.
Cài đặt:
o Cấu trúc dữ liệu:
o Mảng R[N]: lưu theo cột, R[i] = true ⇒cột i còn tự do, ngược lại cột i đã
bị quân hậu khống chế.
o Mảng C1[2*N-1]: lưu đường chéo TT-DP, do các dường chéo này có chỉ
số từ 1-n ⇒n-1 nên ánh xạ chỉ số này vào mảng C1 bằng cách cộng thêm
(n-1). Khi đó đường chéo 1-n sẽ có chỉ số là 0 trong mảng C1…
o Mảng C2[2*N+1]: lưu đường chéo TP-DT, các đường chéo này có chỉ số
từ 2- 2n nên ta đánh chỉ số C2 từ 2- 2n luôn cho tiện (hai phần tử C2[0] và
C2[1] ta không dùng đến).
o Các phần tử của 3 mảng R, C1 và C2 được gán giá trị True khi bắt đầu!
o Thuật toán quay lui:
o Ý tưởng chính như sau: xét tất cả các cột, thử đặt quân hậu 1 vào 1 cột,
với mỗi cách đặt quân hậu 1, xét tất cả các đặt quân hậu 2 sao cho quân
hậu 1 không ăn được nó, thử đặt quân hậu 2 vào ô có thể…rồi xét tiếp đến
quân hậu 3 đến quân hậu n. Với mỗi cách đặt quân hậu n sẽ cho ta một kết
quả! Khi xét hết tất cả các giá trị có thể có gán cho quân hậu thứ i thì thuật
toán sẽ quay lên xét những giá trị còn lại của quân hậu thứ i-1.
1
2
3
4
5
6
7
8
1 2 3 4 5 6 7 8
Đừng chéo TT-
DP có chỉ số 0
Đừng chéo TP-
DT có chỉ số 10
Ô ( 5, 5)
54
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
o Khi chọn vị trí j cho quân hậu thứ i, thì ô (i, j) không bị quân hậu đặt trước
đó ăn. Do vậy ô (i, j) phải thoả điều kiện:
 Cột j còn tự do.
 Đường chéo TT-DP có chỉ số (i-j) không bị bất kỳ quân hậu nào
khống chế.
 Đường chéo TP-DT có chỉ số (i+j) cũng không bị các quân hậu
trước đó khống chế.
o Sau khi đặt quân hậu thứ i vào cột j, nếu i = n tức là đặt xong quân hậu
cuối cùng ⇒được một bộ kết quả. Ngược lại
 Đánh dấu 2 đường chéo TT-DP (i-j) và đường TP-DT(i+j) và cột j
đã bị khống chế. Tiếp tục gọi đệ quy cho quân thứ i+1.
 Sau khi gọi đệ quy cho quân hậu i+1, ta phải thử vị trí khác cho
quân hậu thứ i trong số những giá trị j có thể nhận được. Do đó ta
phải bỏ việc đánh dấu cột j và 2 đường chéo, lúc này cột j và 2
đường chéo đó sẽ tự do. Thao tác này cho phép quân hậu khác có
thể đặt ở vị trí đó ở những bước tiếp sau.
Chương trình C/C++ minh họa bài toán n-Hậu:
#define MAX 12
void ShowResult(int b[MAX], int n)
{
/*Xuat ket qua theo dong*/
for(int i=0; i < n; i++)
printf("(%d, %d)t", i+1, b[i]+1);
printf("n");
}
void Try(int *r,int *b, int n, int i, int *c1, int *c2)
{
for(int j=0; j < n; j++) //tìm vị trí cột
{
if (r[j] && c1[(i-j)+n-1] && c2[i+j]) //kiểm tra cột và 2 chéo
{
b[i] = j; // chọn cột j
if (i==n-1)
ShowResult(b,n); // xuất 1 bộ kết quả
else
{
55
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
r[j] = false; // đánh dấu chọn cột j
c1[(i-j)+n-1] = false; //chéo bị hậu khống chế
c2[i+j] = false; //chéo bị hậu khống chế
Try(r, b, n, i+1, c1, c2); // đặt hậu tiếp theo
r[j] = true; // bỏ chọn cột j
c1[(i-j)+n-1] = true; // chéo tự do
c2[i+j] = true; // chéo tự do
}
}
}
}
int main(int argc, char* argv[])
{
int b[MAX],
r[MAX];
int c1[2*MAX-1],
c2[2*MAX-1];
int n;
printf("doc n (<12): ");
scanf("%d",&n);
for(int i=0; i < n;i++)
r[i] = true;
for(i=0; i < 2*MAX-1; i++)
{
c1[i] = c2[i] = true;
}
Try(r, b, n, 0, c1, c2);
return 0;
}
Kết quả khi n = 5
(1, 1) (2, 3) (3, 5) (4, 2) (5, 4)
(1, 1) (2, 4) (3, 2) (4, 5) (5, 3)
(1, 2) (2, 4) (3, 1) (4, 3) (5, 5)
(1, 2) (2, 5) (3, 3) (4, 1) (5, 4)
(1, 3) (2, 1) (3, 4) (4, 2) (5, 5)
(1, 3) (2, 5) (3, 2) (4, 4) (5, 1)
(1, 4) (2, 1) (3, 3) (4, 5) (5, 2)
(1, 4) (2, 2) (3, 5) (4, 3) (5, 1)
56
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
(1, 5) (2, 2) (3, 4) (4, 1) (5, 3)
(1, 5) (2, 3) (3, 1) (4, 4) (5, 2)
2.2.5 Bài toán mã đi tuần
Yêu cầu: Cho một bàn cờ tổng quát dạng nxn, hãy chỉ ra một hành trình của một
quân Mã, xuất phát từ một vị trí bắt đầu đi qua tất cả các ô còn lại của bàn cờ, mỗi ô
đi đúng một lần.
Ý tưởng cơ bản: dùng thuật toán quay lui; xuất phát từ 1 ô, gọi số nước đi là t=1,
ta cho quân mã thử đi tiếp 1 ô (có 8 nước đi có thể), nếu ô đi tiếp này chưa đi qua thì
chọn làm bước đi tiếp theo. Tại mỗi nước đi kiểm tra xem tổng số nước đi bằng n*n
chưa, nếu bằng thì mã đã đi qua tất cả các ô ⇒dừng (do chỉ cần tìm một giải pháp).
Trường hợp ngược lại, gọi đệ quy để chọn nước đi tiếp theo. Ngoài ra, nếu tại một
bước tìm đường đi, nếu không tìm được đường đi tiếp thì thuật toán sẽ quay lui lại
nước đi trước và tìm đường đi khác…
Hình 3.10: Minh họa tám nước đi tiếp của quân mã.
57
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
Hình 3.11: Đường đi của quân mã trong bàn cờ 5x5
Cài đặt:
o Cấu trúc dữ liệu:
o Mảng board[MAX][MAX]: lưu bàn cờ, trong đó board[i][j] là ô (i, j); giá
trị của board[i][j] là 0 khi quân mã chưa đi qua, và >0 khi quân mã đã đi
qua, giá trị board[i][j] lúc này chính là thứ tự nước đi trên hành trình. Thật
sự cài đặt mảng board như vậy là đủ, nhưng nếu bổ sung thêm một tí thì sẽ
tăng tốc độ thực hiện. Vấn đề bổ sung liên quan đến đường biên, do mỗi
lần di chuyển quân mã ta phải kiểm tra xem nước đi có ra ngoài biên hay
không. Ta có thể mở rộng mảng board để không cần phải kiểm tra bằng
cách mở rộng hai ô về bốn hướng trên dưới trái phải. Khi đó chỉ cần gán
giá trị cho các ô ngoài biên là -1.
58
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
Hình 3.12 : Mảng board cho bàn cờ 8x8 ⇒12x12.
o Mảng dr[8], dc[8]: lưu các độ dời của bước đi kế tiếp, có tám nước đi có
thể cho vị trí quân mã hiện tại. Do đó để đi nước thứ i ta chỉ cần cộng
thêm dr[i] cho dòng và dc[i] cho cột!
Hình 3.13: Thứ tự tám nước đi theo chiều kim đồng hồ.
Mảng dr[] = {-2, -1, 1, 2, 2, 1, -1, -2}
dc[] = {1, 2, 2, 1, -1, -2, -2, 1}
o Thuật giải đệ quy:
Tại mỗi bước lần lượt cho quân mã thử tất cả các nước đi kế tiếp (tám nước đi kế
tiếp). Với mỗi bước đi, kiểm tra xem nếu nước đi hợp lệ (chưa đi qua và ở trong
(-2,
1)
(-1, 2)
(1, 2)
(2, 1)(2, -1)
(1, -2)
(-1,
-2)
(-2,
-1)
59
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
bàn cờ) thì thử đi nước này. Nếu quân mã đã đi qua hết bàn cờ thì xuất kết quả.
Ngược lại thì gọi đệ quy tiếp cho vị trí mới thử trên. Lưu ý là mỗi khi vị trí đã đi
qua được đánh dấu chính bằng chính thứ tự nước đi trên bàn cờ. Sau khi không
thử vị trí này thì phải bỏ đánh dấu để chọn giải pháp khác (trường hợp quay lui).
Minh họa hàm Try với step là thứ tự của nước đi, i và j là vị trí của quân mã hiện
tại.
Try( int step, int i, j)
{
+ Với mỗi nước đi kế tiếp (ii, jj) từ (i, j)
+ Nếu (ii,jj) hợp lệ
chọn (ii, jj) làm nước đi kế tiếp
+ nếu đi hết bàn cờ
 xuất 1 kết quả
+ ngược lại
Gọi đệ quy Try(step +1, ii, jj)
Không chọn (ii, jj) là nước đi kế tiếp
}
Chương trình C/C++ minh họa cho trường hợp bàn cờ 8x8.
#include "stdafx.h"
#include "conio.h"
#include "stdlib.h"
#define MAX 12 // trường hợp bàn cờ 8x8
void Show(int board[MAX][MAX]);
void Init(int board[MAX][MAX])
{
for(int i=0;i<MAX;i++)
for(int j=0;j<MAX;j++)
if(i>=2 && i<=MAX-3 && j>=2 && j<=MAX-3)
board[i][j]=0; // đánh dấu chưa đi qua
else
board[i][j]=-1; // đánh dấu biên
}
void Try(int step, int i, int j, int board[MAX][MAX], int *dr, int *dc)
{
60
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
for(int k=0; k<7; k++) //duyệt qua các nước đi kế tiếp
{
if( board[i+dr[k]][j+dc[k]]==0 ) // nếu vị trí này chưa đi qua
{
Board[i+dr[k]][j+dc[k]]= step+1; // đánh dấu chọn vị trí
if(step+1==64) //hoàn tất một kết quả
{
Show(board);
printf("Nhan <ENTER> de tiep tuc tim loi 
giai ke. Nhan <ESC> de thoat");
char c;
if(c = getch() == 27)
exit(1);
}
else // gọi đệ quy cho nước kế tiếp
Try(step+1, i+dr[k], j+ dc[k], board, dr, dc);
Board[i+dr[k]][j+dc[k]]= 0;// trả tự do cho vị trí vừa chọn
}// end if
}//end for
}
void Show(int board[MAX][MAX])
{
for(int i=0;i<MAX;i++)
{
for(int j=0;j<MAX;j++)
printf("%4d",board[i][j]);
printf("nn");
}
}
void main()
{
int board[MAX][MAX];
int dr[8]={-2,-1,1, 2, 2, 1,-1,-2};
int dc[8]={1, 2, 2, 1,-1,-2,-2,-1};
Init(board);
board[2][2]=1; // chọn vị trí đầu tiên
Show(board);
Try(1, 2, 2, board, dr, dc);
}
Một kết quả của chương trình như sau:
61
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
1
3
8
4
3
3
4
3
3
6
1
9
22
4
4
5
9
2
3
7
2
0
2
3
4 17
3
9
4
2
3
3
6
0
3
5
1
8
2
1
10
5
8
4
5
4
0
5
3
2
4
1
1
1
6
5
4
1
3
2
5
7
4
6
6
1
2
6
9 12
5
0
4
7
5
2
2
5
5
4
1
5
6 27
3
1
5
6
4
9
6
2
2
9
8
1
3
64
4
8
5
1
3
0
5
5
1
4
6
3
2
8
7
Hình 3.14: Một giải pháp cho bàn cờ 8x8.
 
62
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
Chương 1
Tìm kiếm và Sắp xếp
 
1.1 Tìm kiếm
Tìm kiếm là thao tác cơ bản, thường xuyên và quan trọng trong tin học. Ví dụ như
tìm kiếm nhân viên trong danh sách nhân viên, tìm kiếm một sinh viên trong danh
sách lớp học…Các hệ thống thông tin thường lưu trữ khối lượng dữ liệu lớn, nên
thuật toán tìm kiếm tốt sẽ có nhiều lợi ích.
Tuy nhiên, thao tác tìm kiếm còn phụ thuộc rất nhiều đến dữ liệu được tổ chức
như thế nào; nếu dữ liệu được tổ chức tốt thì việc tìm kiếm sẽ tiến hành nhanh chóng
và hiệu quả hơn. Giả sử sách được sắp theo chủ đề, thể loại thì dễ tìm kiếm hơn là
không được sắp. Hoặc danh sách tên người được sắp theo thứ tự alphabet cũng dễ cho
việc tìm kiếm…
1.1.1 Mô tả bài toán tìm kiếm trong tin học
Tìm kiếm là quá trình xác định một đối tượng nào đó trong một tập các đối tượng.
Kết quả trả về là đối tượng tìm được hoặc một chỉ số (nếu có) xác định vị trí của đối
tượng trong tập đó.
Việc tìm kiếm dựa theo một trường nào đó của đối tượng, trường này là khóa
(key) của việc tìm kiếm.
Ví dụ: đối tượng sinh viên có các dữ liệu {MaSV, HoTen, DiaChi,…}. Khi đó tìm
kiếm trên danh sách sinh viên thì khóa thường chọn là MaSV hoặc HoTen.
Thông thường người ta phân làm hai loại tìm kiếm: tìm kiếm tuyến tính hay còn
gọi là tuần tự cho tập dữ liệu bất kỳ; tìm kiếm nhị phân cho tập dữ liệu đã được sắp
xếp.
Bài toán tìm kiếm được mô tả như sau:
• Tập dữ liệu được lưu trữ là dãy a1, a2,..,an. Giả sử chọn cấu trúc dữ liệu mảng
để lưu trữ dãy số này trong bộ nhớ chính, có khai báo: int a[n];
63
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
• Khoá cần tìm là x có kiểu nguyên : int x.
Hình 4.1: Phân loại phương pháp tìm kiếm
1.1.2 Tìm kiếm tuyến tính
Ý tưởng chính: duyệt tuần tự từ phần tử đầu tiên, lần lượt so sánh khóa tìm kiếm
với khoá tương ứng của các phần tử trong danh sách (trong trường hợp mô tả trên là
so sánh x và a[i]). Cho đến khi gặp phần tử cần tìm hoặc đến khi duyệt hết danh sách.
Các bước tiến hành như sau :
Bước 1: i = 1 ;
Bước 2: so sánh a[i] với x, có hai khả năng
i. a[i] = x: tìm thấy ⇒dừng
ii. a[i] <> x: sang bước 3
Bước 3: i = i +1, kiểm tra chỉ số i và kích thước mảng n
i. nếu i>n: hết mảng, không tìm thấy ⇒dừng
ii. ngược lại: quay lại bước 2
Hàm tìm kiếm tuyến tính đơn giản minh họa bằng ngôn ngữ C/C++.
Tìm kiếmTìm kiếm
Tìm kiếm tuyến tínhTìm kiếm tuyến tính Tìm kiếm nhị phânTìm kiếm nhị phân
Tập DL
bất kỳ
Tập DL
được
sắp
64
x ?
a1 a2 a3 an-1 an
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
int Search(int a[], int n, int key)
{
int i =0;
while (i<n) && (key != a[i])
i++;
if (i >= n)
return -1; // tìm không thấy
else
return i; // tìm thấy tại vị trí i
}
1.1.3 Tìm kiếm nhị phân
Phương pháp tìm kiếm nhị phân được áp dụng cho dãy khoá đã có thứ tự: k[1] ≤
k[2] ≤ ... ≤ k[n].
Ý tưởng của phương pháp này như sau:
Giả sử ta cần tìm trong đoạn a[left...right] với khoá tìm kiếm là x, trước hết ta xét
phần tử giữa a[mid], với mid = (left + right)/2.
• Nếu a[mid] < x thì có nghĩa là đoạn a[left] đến a[right] chỉ chứa khóa < x,
ta tiến hành tìm kiếm từ a[mid+1] đến a[right].
• Nếu a[mid] > x thì có nghĩa là đoạn a[m] đến a[right] chỉ chứa khoá > x, ta
tiến hành tìm kiếm từ a[left] đến a[mid-1].
• Nếu a[mid] = x thì việc tìm kiếm thành công.
• Quá trình tìm kiếm thất bại nếu left > right.
Các bước tiến hành như sau:
Bước 1: left =1, right = n // tìm kiếm trên tất cả phần tử.
Bước 2: mid = (left + right)/2 // lấy mốc so sánh
So sánh a[mid] với x có 3 khả năng:
- a[mid] = x, tìm thấy ⇒dừng
- a[mid]> x, tìm tiếp trong dãy a[left].. a[mid-1]
right = mid -1;
- a[mid] < x, tìm tiếp trong dãy a[mid+1].. a[right]
left = mid +1;
Bước 3:
Nếu left ≤ right; còn phần tử ⇒tìm tiếp ⇒bước 2
65
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
Ngược lại: dừng, đã xét hết phần tử ⇒không tìm thấy.
Ví dụ: cho dãy số gồm 8 phần tử {1, 2, 4, 5, 6, 8, 12, 15} và x = 8
Hình 4.2: Tìm kiếm nhị phân.
Hàm C minh họa cài đặt thuật toán tìm kiếm nhị phân
int BinarySearch(int key)
{
int left = 0, right = n-1, mid;
while (left <= right)
{
mid = (left + right)/ 2; // lấy điểm giữa
if (a[mid] == key) // nếu tìm được
return mid;
if (a[mid] < x) // tìm đoạn bên phải mid
left = mid+1;
else
right = mid-1; // tìm đoạn bên trái mid
}
return -1; // không tìm được
}
11
Left = 1
X =X =
88
Right = 8Mid = 4
Đoạn tìm kiếm
22 44 55 66 88 1212 1515
11
Left = 5
X =X =
88
Right = 8Mid = 6
Đoạn tìm kiếm
22 44 55 66 88 1212 1515
==
66
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
1.1.4 Kết luận
• Giải thuật tìm kiếm tuyến tính không phụ thuộc vào thứ tự của các phần tử
trong mảng, do vậy đây là phương pháp tổng quát nhất để tìm kiếm trên một
dãy bất kỳ.
• Thuật giải nhị phân dựa vào quan hệ giá trị của các phần tử trong mảng để
định hướng trong quá trình tìm kiếm, do vậy chỉ áp dụng được với dãy đã có
thứ tự.
• Thuật giải nhị phân tìm kiếm nhanh hơn tìm kiếm tuyến tính.
• Tuy nhiên khi áp dụng thuật giải nhị phân thì cần phải quan tâm đến chi phí
cho việc sắp xếp mảng. Vì khi mảng được sắp thứ tự rồi thì mới tìm kiếm nhị
phân.
1.2 Bài toán sắp xếp
Sắp xếp là quá trình bố trí lại các phần tử của một tập đối tượng nào đó theo một
thứ tự nhất định. Ví dụ như: tăng dần, giảm dần với một dãy số, thứ tự từ điển với các
từ...Việc sắp xếp là một bài toán thường thấy trong tin học, do các yêu cầu tìm kiếm
thuận lợi, sắp xếp kết quả các bảng biểu...
Dữ liệu thường được tổ chức thành mảng các mẫu tin dữ liệu, mỗi mẫu tin thường
có một số các trường dữ liệu khác nhau. Không phải toàn bộ các trường đều tham gia
quá trình sắp xếp mà chỉ có một trường nào đó (hoặc một vài trường) được quan tâm.
Người ta gọi trường này là khoá, việc sắp xếp sẽ được tiến hành dựa vào giá trị khoá
này.
Ví dụ: sắp xếp một mảng các số nguyên tăng dần, sắp xếp một danh sách học sinh với
điểm thi giảm dần...
1.3 Một số phương pháp sắp xếp cơ bản
Trong phần này giới thiệu một số phương pháp sắp xếp cơ bản thường được dùng
để sắp xếp một danh sách, mảng dữ liệu.
1.3.1 Phương pháp chọn
Đây là một trong những thuật toán sắp xếp đơn giản nhất. Ý tưởng cơ bản của
phương pháp này được thể hiện như sau:
67
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
1. Ở lượt thứ nhất, ta chọn trong dãy khoá k[1..n] ra khoá nhỏ nhất và đổi giá trị
nó với k[1], khi đó k[1] sẽ trở thành khoá nhỏ nhất.
2. Ở lượt thứ hai, ta chọn trong dãy khoá k[2..n] ra khóa nhỏ nhất và đổi giá trị
nó cho k[2].
3. ...
4. Ở lượt thứ i, ta chọn trong dãy khóa k[i..n] ra khóa nhỏ nhất và đổi giá trị nó
cho k[i].
5. Tới lượt k-1, ta chọn giá trị nhỏ nhất trong k[n-1] và k[n] ra khoá nhỏ nhất và
đổi cho giá trị cho k[n-1].
Thuật giải SelectionSort: (mã giả, chỉ số 1 là đầu mảng)
begin
for i:= 1 to n-1 do
begin
jmin := i;
for j:=i+1 to n do
if a[j] < a[jmin] then
jmin = j;
if ( jmin <> i)
Swap(a[i], a[jmin])
end.
end.
1.3.2 Phương pháp sắp xếp nổi bọt
Trong thuật toán sắp xếp nổi bọt, dãy khóa sẽ được duyệt từ cuối lên đầu dãy, nếu
gặp hai khóa kế cận ngược thứ tự thì đổi chỗ cho nhau. Sau lần duyệt như vậy, khóa
nhỏ nhất trong dãy khóa sẽ được chuyển về vị trí đầu tiên và vấn đề trở thành sắp xếp
dãy khoá từ k[n] đến k[2].
Thuật giải bubblesort: (mả giả, chỉ số 1 là đầu mảng)
begin
for i:=2 to n do
for j:= n downto i do
if (a[j] < a[j-1])
Swap(a[j],a[j-1])
end.
1.3.3 Phương pháp sắp xếp chèn
Xét dãy khóa k[1..n], ta thấy dãy con chỉ gồm mỗi một khoá là k[1] có thể coi là
đã sắp xếp rồi. Xét thêm k[2], ta so sánh nó với k[1], nếu thấy k[2] < k[1] thì chèn nó
vào trước k[1]. Đối với k[3], ta chỉ xét dãy chỉ gồm hai khoá k[1] và k[2] đã sắp xếp
68
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
và tìm cách chèn k[3] vào dãy khóa đó để được thứ tự sắp xếp. Một cách tổng quát, ta
sẽ sắp xếp dãy k[1..i] trong điều kiện dãy k[1..i-1] đã sắp xếp rồi bằng cách chèn k[i]
vào dãy đó tại vị trí đúng khi sắp xếp.
Thuật giải InsertionSort: (mả giả, chỉ số 1 là đầu mảng)
begin
for i:= 2 to n do
begin
tmp = a[i];
j = i-1;
while (j>0) and (tmp < a[j])
begin
a[j+1] = a[j];// đẩy lùi giá trị k[i] về sau -> tạo khoảng trống
j := j-1;
end
k[j+1] = tmp; // chèn vào khoảng trống.
end
end.
1.3.4 Phương pháp đổi chỗ trực tiếp
Ý tưởng chính: xuất phát từ đầu dãy, tìm những phần tử còn lại không thoả thứ tự sắp
xếp với phần tử đang xét, hoán vị các phần tử tương ứng để thỏa thứ tự. Lặp lại tương tự
với các phần tử tiếp theo của dãy.
Các bước tiến hành như sau:
 Bước 1: i = 1; // xuất phát từ đầu dãy
 Bước 2: j = i+1; // tìm các phần tử phía sau i
 Bước 3:
o While j ≤ n do
 Nếu a[j]< a[i] ⇒Swap(a[i], a[j]);
 j = j+1;
 Bước 4: i = i+1;
o Nếu i < n: ⇒Bước 2
o Ngược lại ⇒Kết thúc
Ví dụ: cho dãy số a: 10 3 7 6 2 5 4 16
69
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
10 3 7 6 2 5 4 16
i = 1 j = 2
3 10 7 6 2 5 4 16
i = 1 j = 5
70
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
2 10 7 6 3 5 4 16
i = 2 j = 3
2 7 10 6 3 5 4 16
i = 2 j = 4
2 6 10 7 3 5 4 16
i = 2 j = 5
71
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
2 3 10 7 6 5 4 16
i = 2 j = 5
2 3 10 7 6 5 4 16
i = 3 j = 4
72
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
2 3 7 10 6 5 4 16
i = 3 j = 5
2 3 6 10 7 5 4 16
i = 3 j = 6
2 3 5 10 7 6 4 16
i = 3 j = 7
73
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
2 3 4 10 7 6 5 16
i = 4 j = 5
2 3 4 7 10 6 5 16
i = 4 j = 6
2 3 4 6 10 7 5 16
i = 4 j = 7
74
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
2 3 4 5 10 7 6 16
i = 5 j = 6
2 3 4 5 7 10 6 16
i = 5 j = 7
2 3 4 5 6 10 7 16
i = 6 j = 7
2 3 4 5 6 7 10 16
i = 7
75
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
2 3 4 5 6 7 10 16
Hình 4.3: Minh họa đổi chỗ trực tiếp.
1.3.5 Phương pháp ShellSort
Trong phương pháp sắp xếp kiểu chèn nếu ta luôn phải chèn một khóa vào vị trí
đầu dãy thì dẫn đến hạn chế của thuật toán này. Để khắc phục trong trường hợp này thì
người ta đưa ra một phương pháp sắp xếp là ShellSort.
Ý tưởng chính: xét một dãy a[1]...a[n], cho một số nguyên h (1 ≤ h ≤ n), ta có thể chia
dãy đó thành h dãy con như sau:
 Dãy con 1: a[1], a[1+ h], a[1+2h]...
 Dãy con 2: a[2], a[2+h], a[2+2h]...
 Dãy con 3: a[3], a[3+h], a[3+2h]...
 ...
 Dãy con h: a[h], a[2h], a[3h]...
Ví dụ cho dãy:
10 3 7 6 2 5 4 16 n = 8, h = 3. Ta có
dãy con sau:
Dãy chính 10 3 7 6 2 5 4 16
Dãy con 1 10 6 4
Dãy con 2 3 2 16
Dãy con 3 7 5
Những dãy này được coi là những dãy con xếp theo độ dài bước h. Tư tưởng
chính của thuật toán ShellSort là: với mỗi bước h, áp dụng thuật toán sắp xếp kiểu chèn
từng dãy con độc lập để làm mịn dần các phần tử trong dãy chính. Tiếp tục làm tương tự
đối với bước (h div 2)... cho đến khi h = 1 thì ta được dãy phần tử được sắp.
Xét trong ví dụ trên, nếu chúng ta dùng phương pháp chèn thì với phần tử a[5] = 2
là phần tử nhỏ nhất trong dãy, do đó nó phải chèn vào vị trí thứ 1, tức là phải chèn trước
4 phần tử trước nó. Nhưng nếu chúng ta xem 2 là phần tử của dãy 2 thì ta chỉ cần chèn
trước một phần tử là 3. Đây chính là nguyên nhân thuật toán ShellSort thực hiện hiệu quả
hơn sắp xếp chèn. Khi đó khóa nhỏ nhanh chóng đưa về gần vị trí đúng của nó.
76
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
Các bước thực hiện chính như sau:
 Bước 1: chọn k khoảng cách h[1], h[2],.., h[k], và i = 1.
 Bước 2: Chia dãy ban đầu thành các dãy con có bước nhảy là h[i]. Thực hiện sắp xếp
từng dãy con bằng phương pháp chèn trực tiếp.
 Bước 3: i = i+1
o Nếu i > k: ⇒Dừng
o Ngược lại: ⇒Bước 2.
Ví dụ: cho dãy bên dưới với n = 8, h = {5, 3, 1}.
10 3 7 6 2 5 4 16
Ta có minh họa như sau:
10
3
7
6
2
5
4
16
h = 5
Daõy 1
Daõy 2
Daõy 3
Daõy 4
Daõy 5
77
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
5
3
7
6
2
10
4
16
h = 3
Daõy 1
Daõy 2
Daõy 3
5 2 7 4 3 10 6 16
h = 1
2 3 4 5 6 7 10 16
Daõy 1(daõy chính)
Hình 4.4: Minh hoạ ShellSort.
Cài đặt ShellSort: sắp xếp dãy a[] tăng, với h[] là mảng chứa các độ dài (bước nhảy) đã
chọn sẵn:
void ShellSort(int a[], int n, int h[], int k)
{
int step, i, j;
int x, len;
for(step = 0; step < k; step++) // duyệt qua từng bước nhảy
{
len = h[step]; // chiều dài của bước nhảy
for(i = len; i < n; i++) // duyệt các dãy con
{
// lưu phần tử cuối để tìm vị trí thích hợp trong dãy con
x = a[i];
78
Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN
// a[j] đứng trước a[i] trong cùng dãy con
j = i – len;
while ((x < a[j]) && (j>= 0))
// sắp xếp dãy con chứa x dùng pp chèn
{
a[j+len] = a[j]; // dời về sau theo dãy con
j = j – len; // qua phần tử trước trong dãy con
}
a[j+len] = x;// đưa x vào vị trí thích hợp trong dãy con
}
}
}
1.3.6 Phương pháp phân đoạn QuickSort
Đây là một phương pháp sắp xếp tốt do C.A.R Hoare đề xuất. Thuật toán này có
tốc độ trung bình nhanh hơn các thuật toán sắp xếp tổng quát khác. Do đó Hoare dùng
chữ “Quick” để đặt tên cho thuật toán này.
Ý tưởng chính: Để sắp dãy a[1] ... a[n], ta thực hiện sắp xếp dãy a từ chỉ số 1 đến chỉ số
n. QuickSort dựa trên phân hoạch dãy ban đầu thành hai phần dựa vào giá trị x, x là giá
trị của một phần tử tùy ý trong dãy ban đầu:
 Dãy thứ 1: gồm các phần tử a[1]..a[i] có giá trị không lớn hơn x.
 Dãy thứ 2: gồm các phần tử a[i]..a[n] có giá trị không nhỏ hơn x.
Sau khi phân hoạch thì dãy ban đầu được phân thành ba phần:
1. a[k] < x, với k = 1..i
2. a[k] = x, với k = i..j
3. a[k] > x, với k = j..n
a[k] < x a[k] = x a[k] > x
Ta có nhận xét khi đó dãy con thứ 2 đã có thứ tự, nếu dãy con 1 và dãy con 3 có
một phần tử thì chúng cũng đã có thứ tự, khi đó dãy ban đầu đã được sắp. Ngược lại,
nếu dãy con 1 và 3 có nhiều hơn một phần tử thì dãy ban đầu có thứ tự khi dãy con 1
và 3 được sắp. Để sắp xếp dãy con 1 và 3, ta lần lượt tiến hành việc phân hoạch từng
dãy con theo cùng phương pháp vừa trình bày.
Giải thuật phân hoạch dãy a[left], a[left+1],.., a[right] thành hai dãy con:
79
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2
Giao trinh ky thuat lap trinh 2

More Related Content

What's hot

Lap trinh huong doi tuong
Lap trinh huong doi tuongLap trinh huong doi tuong
Lap trinh huong doi tuong
Duc Nguyen
 
Cau truc may tinh
Cau truc may tinhCau truc may tinh
Cau truc may tinh
Duy Vọng
 
Luận văn: Chuyển ngữ tự động từ tiếng việt sang tiếng Nhật, HAY
Luận văn: Chuyển ngữ tự động từ tiếng việt sang tiếng Nhật, HAYLuận văn: Chuyển ngữ tự động từ tiếng việt sang tiếng Nhật, HAY
Luận văn: Chuyển ngữ tự động từ tiếng việt sang tiếng Nhật, HAY
Dịch vụ viết bài trọn gói ZALO 0917193864
 
Learning C# (Vietnamese)
Learning C# (Vietnamese)Learning C# (Vietnamese)
Learning C# (Vietnamese)
Nguyen Hung
 
Giáo trình lắp ráp và cài đặt máy tính
Giáo trình lắp ráp và cài đặt máy tínhGiáo trình lắp ráp và cài đặt máy tính
Giáo trình lắp ráp và cài đặt máy tính
jackjohn45
 
Luận văn: Ứng dụng công nghệ IoT cho giám sát môi trường, HAY
Luận văn: Ứng dụng công nghệ IoT cho giám sát môi trường, HAYLuận văn: Ứng dụng công nghệ IoT cho giám sát môi trường, HAY
Luận văn: Ứng dụng công nghệ IoT cho giám sát môi trường, HAY
Dịch vụ viết bài trọn gói ZALO 0917193864
 
Xây dựng hệ thống phân tích hình ảnh trên ros cho robot hướng dẫn viên
Xây dựng hệ thống phân tích hình ảnh trên ros cho robot hướng dẫn viênXây dựng hệ thống phân tích hình ảnh trên ros cho robot hướng dẫn viên
Xây dựng hệ thống phân tích hình ảnh trên ros cho robot hướng dẫn viên
Man_Ebook
 
Luan van xay dung Chatbot
Luan van xay dung ChatbotLuan van xay dung Chatbot
Luan van xay dung Chatbot
leeminhpark
 
đO nhiệt độ dùng cảm biến ds18 b20 sử dụng board arduino, hiển thị trên lcd, ...
đO nhiệt độ dùng cảm biến ds18 b20 sử dụng board arduino, hiển thị trên lcd, ...đO nhiệt độ dùng cảm biến ds18 b20 sử dụng board arduino, hiển thị trên lcd, ...
đO nhiệt độ dùng cảm biến ds18 b20 sử dụng board arduino, hiển thị trên lcd, ...
Man_Ebook
 
Thi nghiem xlths
Thi nghiem xlthsThi nghiem xlths
Thi nghiem xlths
baquanbachkhoa
 
Nghiên cứu thiết kế hệ thống scada cho hệ thống cung cấp nước sạch tại xí ngh...
Nghiên cứu thiết kế hệ thống scada cho hệ thống cung cấp nước sạch tại xí ngh...Nghiên cứu thiết kế hệ thống scada cho hệ thống cung cấp nước sạch tại xí ngh...
Nghiên cứu thiết kế hệ thống scada cho hệ thống cung cấp nước sạch tại xí ngh...
Man_Ebook
 
User Guide for TV BOOK.
User Guide for TV BOOK. User Guide for TV BOOK.
User Guide for TV BOOK.
Bùi Việt Hà
 
Thiết kế, chế tạo thiết bị đo và cảnh báo nồng độ cồn trong hơi thở
Thiết kế, chế tạo thiết bị đo và cảnh báo nồng độ cồn trong hơi thởThiết kế, chế tạo thiết bị đo và cảnh báo nồng độ cồn trong hơi thở
Thiết kế, chế tạo thiết bị đo và cảnh báo nồng độ cồn trong hơi thở
Man_Ebook
 
Bài giảng công nghệ phần mềm PTIT
Bài giảng công nghệ phần mềm PTITBài giảng công nghệ phần mềm PTIT
Bài giảng công nghệ phần mềm PTIT
NguynMinh294
 
Luận văn: Phát hiện xâm nhập theo thời gian thực trong internet
Luận văn: Phát hiện xâm nhập theo thời gian thực trong internetLuận văn: Phát hiện xâm nhập theo thời gian thực trong internet
Luận văn: Phát hiện xâm nhập theo thời gian thực trong internet
Dịch vụ viết bài trọn gói ZALO 0917193864
 
Nghiên cứu phương pháp tổng hợp cảm biến dùng cho kỹ thuật dẫn đường các robo...
Nghiên cứu phương pháp tổng hợp cảm biến dùng cho kỹ thuật dẫn đường các robo...Nghiên cứu phương pháp tổng hợp cảm biến dùng cho kỹ thuật dẫn đường các robo...
Nghiên cứu phương pháp tổng hợp cảm biến dùng cho kỹ thuật dẫn đường các robo...
Man_Ebook
 
Luận văn: Xây dựng mô hình cánh tay robot 5 bậc tự do, HAY
Luận văn: Xây dựng mô hình cánh tay robot 5 bậc tự do, HAYLuận văn: Xây dựng mô hình cánh tay robot 5 bậc tự do, HAY
Luận văn: Xây dựng mô hình cánh tay robot 5 bậc tự do, HAY
Dịch vụ viết bài trọn gói ZALO 0917193864
 
ĐIều khiển thông minh
ĐIều khiển thông minhĐIều khiển thông minh
ĐIều khiển thông minh
Man_Ebook
 

What's hot (18)

Lap trinh huong doi tuong
Lap trinh huong doi tuongLap trinh huong doi tuong
Lap trinh huong doi tuong
 
Cau truc may tinh
Cau truc may tinhCau truc may tinh
Cau truc may tinh
 
Luận văn: Chuyển ngữ tự động từ tiếng việt sang tiếng Nhật, HAY
Luận văn: Chuyển ngữ tự động từ tiếng việt sang tiếng Nhật, HAYLuận văn: Chuyển ngữ tự động từ tiếng việt sang tiếng Nhật, HAY
Luận văn: Chuyển ngữ tự động từ tiếng việt sang tiếng Nhật, HAY
 
Learning C# (Vietnamese)
Learning C# (Vietnamese)Learning C# (Vietnamese)
Learning C# (Vietnamese)
 
Giáo trình lắp ráp và cài đặt máy tính
Giáo trình lắp ráp và cài đặt máy tínhGiáo trình lắp ráp và cài đặt máy tính
Giáo trình lắp ráp và cài đặt máy tính
 
Luận văn: Ứng dụng công nghệ IoT cho giám sát môi trường, HAY
Luận văn: Ứng dụng công nghệ IoT cho giám sát môi trường, HAYLuận văn: Ứng dụng công nghệ IoT cho giám sát môi trường, HAY
Luận văn: Ứng dụng công nghệ IoT cho giám sát môi trường, HAY
 
Xây dựng hệ thống phân tích hình ảnh trên ros cho robot hướng dẫn viên
Xây dựng hệ thống phân tích hình ảnh trên ros cho robot hướng dẫn viênXây dựng hệ thống phân tích hình ảnh trên ros cho robot hướng dẫn viên
Xây dựng hệ thống phân tích hình ảnh trên ros cho robot hướng dẫn viên
 
Luan van xay dung Chatbot
Luan van xay dung ChatbotLuan van xay dung Chatbot
Luan van xay dung Chatbot
 
đO nhiệt độ dùng cảm biến ds18 b20 sử dụng board arduino, hiển thị trên lcd, ...
đO nhiệt độ dùng cảm biến ds18 b20 sử dụng board arduino, hiển thị trên lcd, ...đO nhiệt độ dùng cảm biến ds18 b20 sử dụng board arduino, hiển thị trên lcd, ...
đO nhiệt độ dùng cảm biến ds18 b20 sử dụng board arduino, hiển thị trên lcd, ...
 
Thi nghiem xlths
Thi nghiem xlthsThi nghiem xlths
Thi nghiem xlths
 
Nghiên cứu thiết kế hệ thống scada cho hệ thống cung cấp nước sạch tại xí ngh...
Nghiên cứu thiết kế hệ thống scada cho hệ thống cung cấp nước sạch tại xí ngh...Nghiên cứu thiết kế hệ thống scada cho hệ thống cung cấp nước sạch tại xí ngh...
Nghiên cứu thiết kế hệ thống scada cho hệ thống cung cấp nước sạch tại xí ngh...
 
User Guide for TV BOOK.
User Guide for TV BOOK. User Guide for TV BOOK.
User Guide for TV BOOK.
 
Thiết kế, chế tạo thiết bị đo và cảnh báo nồng độ cồn trong hơi thở
Thiết kế, chế tạo thiết bị đo và cảnh báo nồng độ cồn trong hơi thởThiết kế, chế tạo thiết bị đo và cảnh báo nồng độ cồn trong hơi thở
Thiết kế, chế tạo thiết bị đo và cảnh báo nồng độ cồn trong hơi thở
 
Bài giảng công nghệ phần mềm PTIT
Bài giảng công nghệ phần mềm PTITBài giảng công nghệ phần mềm PTIT
Bài giảng công nghệ phần mềm PTIT
 
Luận văn: Phát hiện xâm nhập theo thời gian thực trong internet
Luận văn: Phát hiện xâm nhập theo thời gian thực trong internetLuận văn: Phát hiện xâm nhập theo thời gian thực trong internet
Luận văn: Phát hiện xâm nhập theo thời gian thực trong internet
 
Nghiên cứu phương pháp tổng hợp cảm biến dùng cho kỹ thuật dẫn đường các robo...
Nghiên cứu phương pháp tổng hợp cảm biến dùng cho kỹ thuật dẫn đường các robo...Nghiên cứu phương pháp tổng hợp cảm biến dùng cho kỹ thuật dẫn đường các robo...
Nghiên cứu phương pháp tổng hợp cảm biến dùng cho kỹ thuật dẫn đường các robo...
 
Luận văn: Xây dựng mô hình cánh tay robot 5 bậc tự do, HAY
Luận văn: Xây dựng mô hình cánh tay robot 5 bậc tự do, HAYLuận văn: Xây dựng mô hình cánh tay robot 5 bậc tự do, HAY
Luận văn: Xây dựng mô hình cánh tay robot 5 bậc tự do, HAY
 
ĐIều khiển thông minh
ĐIều khiển thông minhĐIều khiển thông minh
ĐIều khiển thông minh
 

Similar to Giao trinh ky thuat lap trinh 2

Giáo trình Lập trình PLC theo ngôn ngữ bậc thang.pdf
Giáo trình Lập trình PLC theo ngôn ngữ bậc thang.pdfGiáo trình Lập trình PLC theo ngôn ngữ bậc thang.pdf
Giáo trình Lập trình PLC theo ngôn ngữ bậc thang.pdf
Man_Ebook
 
03 - LUANVAN_NopQuyen.pdf
03 - LUANVAN_NopQuyen.pdf03 - LUANVAN_NopQuyen.pdf
03 - LUANVAN_NopQuyen.pdf
Nguyễn Thái
 
Giáo trình môn Cơ sở dữ liệu trường Đại học CNTP TP.HCM
Giáo trình môn Cơ sở dữ liệu trường Đại học CNTP TP.HCMGiáo trình môn Cơ sở dữ liệu trường Đại học CNTP TP.HCM
Giáo trình môn Cơ sở dữ liệu trường Đại học CNTP TP.HCM
Tai Thỏ
 
Giáo Trình Phương Pháp Luận Lập Trình ICTU
Giáo Trình Phương Pháp Luận Lập Trình ICTUGiáo Trình Phương Pháp Luận Lập Trình ICTU
Giáo Trình Phương Pháp Luận Lập Trình ICTU
Ngô Doãn Tình
 
Giaotrinh pppthh v10
Giaotrinh pppthh v10Giaotrinh pppthh v10
Giaotrinh pppthh v10luuguxd
 
Luận văn: Tìm hiểu về đối sánh lược đồ và xây dựng ứng dụng VNMATCH
Luận văn: Tìm hiểu về đối sánh lược đồ và xây dựng ứng dụng VNMATCHLuận văn: Tìm hiểu về đối sánh lược đồ và xây dựng ứng dụng VNMATCH
Luận văn: Tìm hiểu về đối sánh lược đồ và xây dựng ứng dụng VNMATCH
Dịch Vụ Viết Thuê Khóa Luận Zalo/Telegram 0917193864
 
Luận văn thạc sĩ kỹ thuật Nghiên cứu thiết kế bộ điều khiển PID mờ.docx
Luận văn thạc sĩ kỹ thuật Nghiên cứu thiết kế bộ điều khiển PID mờ.docxLuận văn thạc sĩ kỹ thuật Nghiên cứu thiết kế bộ điều khiển PID mờ.docx
Luận văn thạc sĩ kỹ thuật Nghiên cứu thiết kế bộ điều khiển PID mờ.docx
Dịch vụ viết thuê đề tài trọn gói 🥳🥳 Liên hệ ZALO/TELE: 0917.193.864 ❤❤
 
Đồ Án Tốt Nghiệp Về Xây Dựng Chương Trình Hỗ Trợ Học Asp.Net.doc
Đồ Án Tốt Nghiệp Về Xây Dựng Chương Trình Hỗ Trợ Học Asp.Net.docĐồ Án Tốt Nghiệp Về Xây Dựng Chương Trình Hỗ Trợ Học Asp.Net.doc
Đồ Án Tốt Nghiệp Về Xây Dựng Chương Trình Hỗ Trợ Học Asp.Net.doc
mokoboo56
 
Đề tài: Xây dựng website nộp đồ án trực tuyến, HAY
Đề tài: Xây dựng website nộp đồ án trực tuyến, HAYĐề tài: Xây dựng website nộp đồ án trực tuyến, HAY
Đề tài: Xây dựng website nộp đồ án trực tuyến, HAY
Viết thuê trọn gói ZALO 0934573149
 
Đề tài: Bài toán kết cấu dàn, khung phẳng có biên phức tạp, HOT
Đề tài: Bài toán kết cấu dàn, khung phẳng có biên phức tạp, HOTĐề tài: Bài toán kết cấu dàn, khung phẳng có biên phức tạp, HOT
Đề tài: Bài toán kết cấu dàn, khung phẳng có biên phức tạp, HOT
Dịch vụ viết bài trọn gói ZALO: 0909232620
 
Thinghiemxlths 121102232414-phpapp02
Thinghiemxlths 121102232414-phpapp02Thinghiemxlths 121102232414-phpapp02
Thinghiemxlths 121102232414-phpapp02KUTY UIT - VNU HCM
 
Giao trinh ky thuat lap trinh
Giao trinh ky thuat lap trinhGiao trinh ky thuat lap trinh
Giao trinh ky thuat lap trinh
Thanh Ngo
 
Đề tài: Cài đặt một web server đơn giản trên một board vi xử lý
Đề tài: Cài đặt một web server đơn giản trên một board vi xử lýĐề tài: Cài đặt một web server đơn giản trên một board vi xử lý
Đề tài: Cài đặt một web server đơn giản trên một board vi xử lý
Dịch Vụ Viết Bài Trọn Gói ZALO 0917193864
 
Thiết Kế Bộ Điều Khiển Mờ Theo Đại Số Gia Tử Cho Robot 2 Bậc Tự Do.doc
Thiết Kế Bộ Điều Khiển Mờ Theo Đại Số Gia Tử Cho Robot 2 Bậc Tự Do.docThiết Kế Bộ Điều Khiển Mờ Theo Đại Số Gia Tử Cho Robot 2 Bậc Tự Do.doc
Thiết Kế Bộ Điều Khiển Mờ Theo Đại Số Gia Tử Cho Robot 2 Bậc Tự Do.doc
DV Viết Luận văn luanvanmaster.com ZALO 0973287149
 
Đề tài: Quản lí kho, HAY
Đề tài: Quản lí kho, HAYĐề tài: Quản lí kho, HAY
Đề tài: Quản lí kho, HAY
Viết thuê trọn gói ZALO 0934573149
 
Bai giang_Co so du lieu.pdf
Bai giang_Co so du lieu.pdfBai giang_Co so du lieu.pdf
Bai giang_Co so du lieu.pdf
cQun22
 
Đề tài: Tính toán phân tán và ứng dụng, HAY
Đề tài: Tính toán phân tán và ứng dụng, HAYĐề tài: Tính toán phân tán và ứng dụng, HAY
Đề tài: Tính toán phân tán và ứng dụng, HAY
Dịch vụ viết bài trọn gói ZALO 0917193864
 
Kiểm chứng các chương trình phần mềm hướng khía cạnh, HAY
Kiểm chứng các chương trình phần mềm hướng khía cạnh, HAYKiểm chứng các chương trình phần mềm hướng khía cạnh, HAY
Kiểm chứng các chương trình phần mềm hướng khía cạnh, HAY
Dịch vụ viết bài trọn gói ZALO: 0909232620
 
He dieu-hanh tu-minh-phuong-giao-trinh-hdh-cuuduongthancong.com
He dieu-hanh tu-minh-phuong-giao-trinh-hdh-cuuduongthancong.comHe dieu-hanh tu-minh-phuong-giao-trinh-hdh-cuuduongthancong.com
He dieu-hanh tu-minh-phuong-giao-trinh-hdh-cuuduongthancong.com
ntrungduc228
 
Luận văn: Tính toán khoảng giải các ràng buộc không tuyến tính
Luận văn: Tính toán khoảng giải các ràng buộc không tuyến tínhLuận văn: Tính toán khoảng giải các ràng buộc không tuyến tính
Luận văn: Tính toán khoảng giải các ràng buộc không tuyến tính
Dịch vụ viết bài trọn gói ZALO 0917193864
 

Similar to Giao trinh ky thuat lap trinh 2 (20)

Giáo trình Lập trình PLC theo ngôn ngữ bậc thang.pdf
Giáo trình Lập trình PLC theo ngôn ngữ bậc thang.pdfGiáo trình Lập trình PLC theo ngôn ngữ bậc thang.pdf
Giáo trình Lập trình PLC theo ngôn ngữ bậc thang.pdf
 
03 - LUANVAN_NopQuyen.pdf
03 - LUANVAN_NopQuyen.pdf03 - LUANVAN_NopQuyen.pdf
03 - LUANVAN_NopQuyen.pdf
 
Giáo trình môn Cơ sở dữ liệu trường Đại học CNTP TP.HCM
Giáo trình môn Cơ sở dữ liệu trường Đại học CNTP TP.HCMGiáo trình môn Cơ sở dữ liệu trường Đại học CNTP TP.HCM
Giáo trình môn Cơ sở dữ liệu trường Đại học CNTP TP.HCM
 
Giáo Trình Phương Pháp Luận Lập Trình ICTU
Giáo Trình Phương Pháp Luận Lập Trình ICTUGiáo Trình Phương Pháp Luận Lập Trình ICTU
Giáo Trình Phương Pháp Luận Lập Trình ICTU
 
Giaotrinh pppthh v10
Giaotrinh pppthh v10Giaotrinh pppthh v10
Giaotrinh pppthh v10
 
Luận văn: Tìm hiểu về đối sánh lược đồ và xây dựng ứng dụng VNMATCH
Luận văn: Tìm hiểu về đối sánh lược đồ và xây dựng ứng dụng VNMATCHLuận văn: Tìm hiểu về đối sánh lược đồ và xây dựng ứng dụng VNMATCH
Luận văn: Tìm hiểu về đối sánh lược đồ và xây dựng ứng dụng VNMATCH
 
Luận văn thạc sĩ kỹ thuật Nghiên cứu thiết kế bộ điều khiển PID mờ.docx
Luận văn thạc sĩ kỹ thuật Nghiên cứu thiết kế bộ điều khiển PID mờ.docxLuận văn thạc sĩ kỹ thuật Nghiên cứu thiết kế bộ điều khiển PID mờ.docx
Luận văn thạc sĩ kỹ thuật Nghiên cứu thiết kế bộ điều khiển PID mờ.docx
 
Đồ Án Tốt Nghiệp Về Xây Dựng Chương Trình Hỗ Trợ Học Asp.Net.doc
Đồ Án Tốt Nghiệp Về Xây Dựng Chương Trình Hỗ Trợ Học Asp.Net.docĐồ Án Tốt Nghiệp Về Xây Dựng Chương Trình Hỗ Trợ Học Asp.Net.doc
Đồ Án Tốt Nghiệp Về Xây Dựng Chương Trình Hỗ Trợ Học Asp.Net.doc
 
Đề tài: Xây dựng website nộp đồ án trực tuyến, HAY
Đề tài: Xây dựng website nộp đồ án trực tuyến, HAYĐề tài: Xây dựng website nộp đồ án trực tuyến, HAY
Đề tài: Xây dựng website nộp đồ án trực tuyến, HAY
 
Đề tài: Bài toán kết cấu dàn, khung phẳng có biên phức tạp, HOT
Đề tài: Bài toán kết cấu dàn, khung phẳng có biên phức tạp, HOTĐề tài: Bài toán kết cấu dàn, khung phẳng có biên phức tạp, HOT
Đề tài: Bài toán kết cấu dàn, khung phẳng có biên phức tạp, HOT
 
Thinghiemxlths 121102232414-phpapp02
Thinghiemxlths 121102232414-phpapp02Thinghiemxlths 121102232414-phpapp02
Thinghiemxlths 121102232414-phpapp02
 
Giao trinh ky thuat lap trinh
Giao trinh ky thuat lap trinhGiao trinh ky thuat lap trinh
Giao trinh ky thuat lap trinh
 
Đề tài: Cài đặt một web server đơn giản trên một board vi xử lý
Đề tài: Cài đặt một web server đơn giản trên một board vi xử lýĐề tài: Cài đặt một web server đơn giản trên một board vi xử lý
Đề tài: Cài đặt một web server đơn giản trên một board vi xử lý
 
Thiết Kế Bộ Điều Khiển Mờ Theo Đại Số Gia Tử Cho Robot 2 Bậc Tự Do.doc
Thiết Kế Bộ Điều Khiển Mờ Theo Đại Số Gia Tử Cho Robot 2 Bậc Tự Do.docThiết Kế Bộ Điều Khiển Mờ Theo Đại Số Gia Tử Cho Robot 2 Bậc Tự Do.doc
Thiết Kế Bộ Điều Khiển Mờ Theo Đại Số Gia Tử Cho Robot 2 Bậc Tự Do.doc
 
Đề tài: Quản lí kho, HAY
Đề tài: Quản lí kho, HAYĐề tài: Quản lí kho, HAY
Đề tài: Quản lí kho, HAY
 
Bai giang_Co so du lieu.pdf
Bai giang_Co so du lieu.pdfBai giang_Co so du lieu.pdf
Bai giang_Co so du lieu.pdf
 
Đề tài: Tính toán phân tán và ứng dụng, HAY
Đề tài: Tính toán phân tán và ứng dụng, HAYĐề tài: Tính toán phân tán và ứng dụng, HAY
Đề tài: Tính toán phân tán và ứng dụng, HAY
 
Kiểm chứng các chương trình phần mềm hướng khía cạnh, HAY
Kiểm chứng các chương trình phần mềm hướng khía cạnh, HAYKiểm chứng các chương trình phần mềm hướng khía cạnh, HAY
Kiểm chứng các chương trình phần mềm hướng khía cạnh, HAY
 
He dieu-hanh tu-minh-phuong-giao-trinh-hdh-cuuduongthancong.com
He dieu-hanh tu-minh-phuong-giao-trinh-hdh-cuuduongthancong.comHe dieu-hanh tu-minh-phuong-giao-trinh-hdh-cuuduongthancong.com
He dieu-hanh tu-minh-phuong-giao-trinh-hdh-cuuduongthancong.com
 
Luận văn: Tính toán khoảng giải các ràng buộc không tuyến tính
Luận văn: Tính toán khoảng giải các ràng buộc không tuyến tínhLuận văn: Tính toán khoảng giải các ràng buộc không tuyến tính
Luận văn: Tính toán khoảng giải các ràng buộc không tuyến tính
 

More from Hồ Lợi

Tóm tắt các hàm chuẩn của c
Tóm tắt các hàm chuẩn của cTóm tắt các hàm chuẩn của c
Tóm tắt các hàm chuẩn của cHồ Lợi
 
Lect04 functions
Lect04 functionsLect04 functions
Lect04 functionsHồ Lợi
 
Ky thuatkhudequy
Ky thuatkhudequyKy thuatkhudequy
Ky thuatkhudequyHồ Lợi
 
Itt epc assignment
Itt epc assignmentItt epc assignment
Itt epc assignmentHồ Lợi
 
Huong danontapc
Huong danontapcHuong danontapc
Huong danontapcHồ Lợi
 
H hai epc_baitap
H hai epc_baitapH hai epc_baitap
H hai epc_baitapHồ Lợi
 
Giaotrinhbaitapkythuatlaptrinh
GiaotrinhbaitapkythuatlaptrinhGiaotrinhbaitapkythuatlaptrinh
GiaotrinhbaitapkythuatlaptrinhHồ Lợi
 
Giao trinh c c++
Giao trinh c c++Giao trinh c c++
Giao trinh c c++Hồ Lợi
 
Epc assignment
Epc assignmentEpc assignment
Epc assignmentHồ Lợi
 
Epc test practical
Epc test practicalEpc test practical
Epc test practicalHồ Lợi
 
De thic++ --th
De thic++ --thDe thic++ --th
De thic++ --thHồ Lợi
 

More from Hồ Lợi (20)

Xu ly chuoi
Xu ly chuoiXu ly chuoi
Xu ly chuoi
 
Tóm tắt các hàm chuẩn của c
Tóm tắt các hàm chuẩn của cTóm tắt các hàm chuẩn của c
Tóm tắt các hàm chuẩn của c
 
T4
T4T4
T4
 
Nguyen lyoop
Nguyen lyoopNguyen lyoop
Nguyen lyoop
 
Lect04 functions
Lect04 functionsLect04 functions
Lect04 functions
 
Ky thuatkhudequy
Ky thuatkhudequyKy thuatkhudequy
Ky thuatkhudequy
 
Itt epc assignment
Itt epc assignmentItt epc assignment
Itt epc assignment
 
Huong danontapc
Huong danontapcHuong danontapc
Huong danontapc
 
H hai epc_baitap
H hai epc_baitapH hai epc_baitap
H hai epc_baitap
 
Gtrinh oop
Gtrinh oopGtrinh oop
Gtrinh oop
 
Giaotrinhbaitapkythuatlaptrinh
GiaotrinhbaitapkythuatlaptrinhGiaotrinhbaitapkythuatlaptrinh
Giaotrinhbaitapkythuatlaptrinh
 
Giao trinh c c++
Giao trinh c c++Giao trinh c c++
Giao trinh c c++
 
File trong c_
File trong c_File trong c_
File trong c_
 
Epc assignment
Epc assignmentEpc assignment
Epc assignment
 
Epc test practical
Epc test practicalEpc test practical
Epc test practical
 
De thic++ --th
De thic++ --thDe thic++ --th
De thic++ --th
 
Dethi c++ -lt
Dethi c++ -ltDethi c++ -lt
Dethi c++ -lt
 
Debug trong c
Debug trong cDebug trong c
Debug trong c
 
D05 stl
D05 stlD05 stl
D05 stl
 
Cpl test3
Cpl test3Cpl test3
Cpl test3
 

Giao trinh ky thuat lap trinh 2

  • 1. 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
  • 2. 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
  • 3. 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
  • 4. 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
  • 5. 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
  • 6. 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
  • 7. 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
  • 8. 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
  • 9. 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
  • 10. 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
  • 11. 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
  • 12. 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à 2i {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
  • 13. 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
  • 14. 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
  • 15. 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
  • 16. 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
  • 17. 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
  • 18. 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
  • 19. 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ó: Un a, n =1 r + Un-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ó Un 1, n =1, 2 Un-2 + Un-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ó Un n, n < 6 Un-5 + Un-4 Un-3 + Un-2+ Un-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 Un n, n <5 Un-1 + Gn-2, n>=5 Gn n-3, n <8 Un-1 + Gn-2, n>=8 long G(int n); 19
  • 20. 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 F1 gọi hàm F2 và bên trong hàm F2 lại có lời gọi hàm đến F1. Đâ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
  • 21. 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 an . + 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à C1, C2, và C3. 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 C2, 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
  • 22. 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 C3, chuyển đĩa lớn sang C2, chuyển đĩa nhỏ từ C3 sang C2. 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
  • 23. 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à C1 C2 C3 1, 2 qua cọc 3 1, 2 qua cọc 2 3 qua cọc 2 B1 B2 B3 C1 C2 C3 C1 C2 C3 C1 C2 C3 C1 C2 C3 C1 C2 C3 C1 C2 C3 C1 C2 C3 C1 C2 C3 B1.1 B1.2 B1.3 B3.1 B3.2 B3.3 23 C1 C2 C3
  • 24. 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; C1 C2 C3 24
  • 25. 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 a1 a2 a3 an-1 an m = Max(m, an ) a1 a2 a3 an m = Max(m, an ) an m = Max(m, an ) 25
  • 26. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN DeQuyMax(int a[N], int n, int &max)// Gỉa sử n > 0  if ( n ==1) {max = a[1] ; return;}  if (max < a[n]) max = a[n];  DeQuyMax(a, n-1, max);  Tính tổng các phần tử trong mảng dùng đệ quy: cho dãy a[1:n], gọi hàm Sum là hàm đệ quy tính tổng, khi đó tổng của dãy a[1:n] là Sum(a[1:n]) Sum(a[1:n]) = Sum(a[1:n-1]) + a[n] Và Sum(a[m:m]) = a[m], trường hợp m=1 thì Sum(a[1:1]) = a[1] Hình 2.6: Tổng các phần tử trong mảng. Hàm đệ quy mô tả bằng mã giả như sau: Sum(int a[], int n) - if ( n == 1) Sum = a[1]; - else  Sum = Sum(a, n-1) + a[n]; Trả về a1 a2 a3 an-1 an Sum(n) = an + Sum(n-1) n = n-1 a1 a2 a3 an Sum(n) = an + Sum(n-1) n = 1 an Sum(n) = an = a1 26
  • 27. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN 1.4 Khử đệ quy 1.4.1 Tìm hiểu cơ chế thực hiện hàm đệ quy Tại mỗi thời điểm của hàm đệ quy được đặc trưng bởi: nội dung các biến và các lệnh cần thực hiện tiếp theo. Do đó tại mỗi thời điểm trong tiến trình xử lý của hàm đệ quy cần phải lưu trữ cả các trạng thái xử lý dang dở. Ví dụ trong hàm đệ quy tính giai thừa n, GT(n): if (n == 0) return 1; else return (n* GT(n-1)); Trong trường hợp n = 3 Hình 2.7: Gọi đệ quy hàm GT. Khi thực hiện lời gọi GT(3) thì sẽ phát sinh lời gọi hàm đến GT(2) và đồng thời phải lưu giữ thông tin trạng thái xử lý còn dang dở GT(3) = 3 * GT(2). Đến lượt hàm GT(2) sẽ phát sinh lời gọi hàm đến GT(1) và lưu giữ thông tin trạng thái còn dang dở GT(2) = 2 * GT(1)…Quá trình cứ thực hiện tương tự cho tới khi gặp trường hợp suy biến GT(0) = 1. GT(3) = 3 * GT(2) GT(2) = 2 * GT(1) GT(1) = 1 * GT(0) GT(0) = 1 27
  • 28. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN Kết thúc quá trình gọi đệ quy là quá trình xử lý ngược được thực hiện: Giá trị của GT(0) được dùng để tính GT(1) theo quá trình lưu trữ Dùng giá trị GT(1) để tính GT(2) theo quá trình tương tự Dùng giá trị GT(2) để tính GT(3) để ra kết quả cuối cùng Song song với quá trình xử lý ngược là xóa bỏ thông tin lưu trữ trong những lần gọi hàm tương ứng. Ví dụ hàm đệ quy tính giá trị dãy Fibonacci Fibo(n) if (n ==0) || (n == 1) return 1; else return (Fibo(n-1) + Fibo(n-2)); Hình 2.8: Hàm đệ quy tính dãy Fibonacci. Do đặc điểm của quá trình xử lý một hàm đệ quy: việc thực thi lời gọi đệ quy sinh ra lời gọi đệ quy mới cho đến khi gặp trường hợp suy biến, do đó cần phải có cơ chế lưu trữ thông tin thoả yêu cầu: o Ở mỗi lần gọi phải lưu trữ thông tin trạng thái con còn đang xử lý dang dở, số trạng thái này bằng với số lần gọi chưa hoàn tất. o Sau khi thực hiện xong một lần gọi thứ k, cần khôi phục lại toàn bộ thông tin trạng thái của lần gọi trước đó là lần gọi k-1. Fibo(4) = Fibo(2) + Fibo(3) Fibo(2) = Fibo(1) + Fibo(0) Fibo(3) = Fibo(2) + Fibo(1) Fibo(1) = 1 Fibo(0) = 1 Fibo(2) = Fibo(1) + Fibo(0) Fibo(1) = 1 Fibo(1) = 1 Fibo(0) = 1 28
  • 29. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN o Lệnh gọi cuối cùng (trường hợp suy biến) sẽ được hoàn tất trước tiên. Các lệnh gọi sau sẽ hoàn thành trước, do đó dãy thông tin trạng thái được hồi phục theo thứ tự ngược với thứ tự lưu trữ. Cấu trúc dữ liệu ngăn xếp lưu trữ theo kiểu Last In First Out thoả các yêu cầu trên nên được sử dụng để lưu trữ thông tin trạng thái của quá trình xử lý đệ quy. Thông thường đệ quy là phương pháp giúp chúng ta tìm giải thuật cho những bài toán khó. Kết quả của giải thuật đệ quy thường rất gọn gàng, dễ hiểu và dễ chuyển thành các chương trình trên các ngôn ngữ lập trình. Tuy nhiên, việc xử lý giải thuật đệ quy cũng gây khó khăn cho máy về không gian lưu trữ và thời gian xử lý. Vì vậy việc thay thế một chương trình đệ quy bằng một chương trình không đệ quy cũng được quan tâm rất nhiều. Thông thường khi gặp một bài toán khó giải quyết theo hướng không đệ quy thì người ta thực hiện quá trình như sau: o Dùng quan niệm đệ quy để tìm giải thuật cho bài toán o Mã hoá giải thuật đệ quy o Khử đệ quy để có một chương trình không đệ quy. Quá trình trên gọi là khử đệ quy, đôi khi việc khử đệ quy cũng không dễ dàng gì, nên nhiều khi cũng phải chấp nhận chương trình đệ quy! 1.4.2 Các trường hợp khử đệ quy đơn giản o Hàm tính giá trị của dãy dữ liệu mô tả bằng hồi quy: Ví dụ 1: hàm tính giai thừa không đệ quy long int GiaiThua( int n) { long int F =1; for (int k = 1; k <= n; k++) F = k*F; return (F); } Ví dụ 2: hàm tính Sn không đệ quy int Sn(int n) { int k = 1; int tg = 1; while ( k < n ) { 29
  • 30. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN k++; if ( k % 2 ) tg += 2*k -1; else tg -= 2*k + 1; } } o Dạng đệ quy đuôi Một hàm đệ quy đuôi P có dạng như sau: P(X) { if B(X) D(X) else { A(X) P(f(X)) } } Trong đó: X: là biến (một hay nhiều biến) P(X): là hàm đệ quy phụ thuộc X A(X) và D(X): là các nhóm lệnh không đệ quy f(X): là hàm biến đổi x trong lần gọi thứ Pi nếu B(fi(X)) không đúng thì thực hiện lệnh X và gọi Pi+1, ngược lại B(fi(X)) đúng thì thực hiện D(X) và kết thúc quá trình gọi (Pi ko gọi thêm hàm đệ quy khác). Ví dụ: Tìm USCLN của hai số dựa vào thuật toán Euclide Giải thuật đệ quy USCLN(m ,n) bằng Euclide như sau : void USCLN( int m, int n, int & kq) { if ( n ==0) kq = m ; else USCLN(n, m %n, kq) ; } Trong trường hợp này: 30
  • 31. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN X là m, n và kq P(X) : USCLN(m, n, kq) B(X) : n ==0 D(X) : kq = m ; A(X) : không có f(x): USCLN(n, m %n, kq) Hàm USCLN không đệ quy được thể hiện như sau: void USCLN(int m, int n, int & kq) { int temp; while (n !=0) { temp = m %n; m = n; n = temp; } kq = m; } 1.4.3 Khử đệ quy dùng stack Để thực hiện một chương trình con đệ quy thì hệ thống phải tổ chức vùng nhớ lưu trữ theo quy tắc LIFO. Các ngôn ngữ lập trình cấp cao đều có khả năng tạo vùng nhớ stack mới cho phép tổ chức các chương trình đệ 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ớ. Do cách tổ chức stack mặc định thích hợp cho mọi trường hợp nên sẽ không tối ưu trong từng trường hợp cụ thể. Do đó sẽ tốt khi người lập trình chủ động tạo cấu trúc dữ liệu stack đặc dụng cho từng chương trình đệ quy cụ thể. Giả sử thủ tục đệ quy trực tiếp có cấu trúc như sau : P(X) { if C(X) D(X) ; else A(X) ; P(f(X)) ; B(X) ; 31
  • 32. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN } Trong đó X : là một hay nhiều biến C(X) : biểu thức điều kiện theo X A(X), B(X) và D(X) : nhóm lệnh không đệ quy f(X) : là hàm của X Quá trình thực hiện thủ tục P(X) như sau: Nếu C(X) đúng thì thực hiện D(X) Ngược lại thực hiện A(X), gọi P(f(X)), thực hiện B(X) sau khi hoàn thành P(f(X)). Mỗi lần P(Y) được gọi thì thông tin của B(Y) lại được sinh ra nhưng chưa thực hiện. Giả sử quá trình đệ quy kết thúc sau k lần gọi đệ quy thì chương trình phải thực hiện 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 trên ta cần xây dựng stack để lưu trữ tạm. Giải thuật thực hiện P(X) với việc sử dụng stack có dạng: P(X) { CreateStack(S) ; while ( ! C(X)) { A(X) ; Push(S, X) ; X = f(X) ; } D(X) ; while ( !Empty(S)) { Pop(S, X) ; B(X) ; 32
  • 33. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN } } Ví dụ: thủ tục đệ quy biểu diễn số thập phân sang nhị phân có dạng: void Binary(int m) { if (m >0) { Binary( m / 2); printf("%d", m % 2); } } Trong đó: X là m P(X) là Binary(X) A(X) và D(X) không có B(X) là lệnh printf("%d", m % 2) ; C(X) là m ≤ 0 f(X) = f(m) = m / 2 ; Giải thuật không đệ quy như sau: void Binary( int m) { int temp; CreateStack(S); while (m > 0) { temp = m % 2; Push(S, temp); m = m / 2; } while (! Empty(S)) { Pop(S, temp); printf(“%d”, temp); } 33
  • 34. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN } Lệnh gọi đệ quy với hai lần gọi trực tiếp: Thủ tục đệ quy có dạng như sau: P(X) { if C(X) D(X) else { A(X); P(f(X)); B(X); P(g(X)); } } Quá trình thực hiện thủ tục đệ quy P(X) như sau: 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) và gọi P(g(X)); khi đó ngoài việc lưu giá trị fi(X) tương ứng chương trình còn phải lưu thêm các giá trị gi(X) phát sinh tương ứng… Do đó ngoài dữ liệu X, chương trình còn phải lưu vào ngăn xếp thêm thứ tự lần gọi. Thủ tục khử đệ quy dùng stack trong trường hợp này có dạng như sau: P(X) { CreateStack(S); Push(S, (X, 1)); do { while ( !C(X)) { 34
  • 35. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN A(X); Push(S, (X, 2)); X = f(X); }// end while D(X); Pop(S, (X, k)); if ( k != 1) { B(X); X = g(X); }// end if } while (k > 1); } Ví dụ: khử đệ quy của thủ tục tháp Hanoi Dạng thủ tục đệ quy của tháp Hanoi như sau: Hanoi(n, a, b, c) { if (n>0) { Hanoi(n-1, a, c, b); Move(a, c); Hanoi(n-1, b, a, c); } } Trong đó n là số đĩa, a là cột đầu tiên, b là cột trung gian, và c là cột cuối cùng, Move(x, y) là thao tác chuyển 1 đĩa từ cột x sang y. Trong trường hợp này: X là bộ (n, a, b, c); C(X) là (n ≤ 0) A(X) và D(X) là rỗng B(X) là B(n,a, b, c) = Move(a, c) f(X) là f(n, a, b, c) = Hanoi(n-1, a, c, b) 35
  • 36. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN g(X) là g(n, a, b, c) = Hanoi(n-1, b, a, c) Giải thuật không đệ quy tương ứng như sau: Create_Stack(S) ; Push(S, (n, a, b, c, 1)); do { while (n > 0) { Push(S, (n, a, b, c, 2)); n = n-1; Swap(b, c); } Pop(S, (n, a, b, c, k)); if ( k != 1) { Move(a, c); n = n-1; Swap(a, b); } } while (k>1);   36
  • 37. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN Chương 2 Bài toán liên quan tổ hợp   2.1 Phương pháp sinh Phương pháp sinh được áp dụng để giải quyết bài toán liệt kê của lý thuyết tổ hợp. Để áp dụng được phương pháp này thì bài toán phải thoả mãn hai điều kiện sau: o Có thể xác định được thứ tự trên tập các cấu hình tổ hợp cần liệt kê. Từ đó có thể xác định được cấu hình đầu tiên và cấu hình cuối cùng trong thứ tự đó. o Xây dựng được một thuật toán cho phép từ một cấu hình chưa phải cấu hình cuối, sinh ra được cấu hình kế tiếp của nó. Phương pháp sinh có thể được mô tả tổng quát như sau: <Xây dựng cấu hình đầu tiên> Do <Đưa ra cấu hình đang có> <Từ cấu hình đang có sinh ra cấu hình kế tiếp> While <Còn cấu hình or khác cấu hình cuối> 2.1.1 Bài toán sinh dãy nhị phân độ dài n  Bài toán: một tập hợp hữu hạn có n phần tử có thể được biểu diễn tương đương với tập các số tự nhiên 1, 2, .., n. Bài toán đặt ra là: cho một tập hợp gồm n phần tử X = {X1, X2,.., Xn} hãy liệt kê tất cả các tập con của tập này. Để biểu diễn tập con Y của X ta dùng xâu nhị phân Bn = {B1, B2,.., Bn}, sao cho nếu Bi = 0 thì Xi∉ Y, ngược lại Bi = 1 thì Xi ∈ Y. Ví dụ như dãy 0011 của tập hợp gồm n thể hiện cho tập Y = {X3, X4} do phần tử B3 và B4 có giá trị là 1. Khi đó ta quy về bài toán liệt kê tất cả xâu nhị phân có kích thước n. Số các xâu nhị phân là 2n . 37
  • 38. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN Một dãy nhị phân x độ dài n là biểu diễn một số nguyên p(x) nào đó trong đoạn [0, 2n -1]. Do đó số các dãy nhị phân độ dài n = số các số nguyên ∈ [0, 2n -1] = 2n . Mục tiêu là lập một chương trình liệt kê các dãy nhị phân n phần tử theo thứ tự từ điển, có nghĩa là liệt kê dãy nhị phân biểu diễn các số nguyên theo thứ tự 0, 1,.., 2n -1. Khi n =3, các độ dài 3 được liệt kê như sau: p(x) 0 1 2 3 4 5 6 7 x 000 001 010 011 100 101 110 111 Khi đó dãy đầu tiên là: 000 và dãy cuối cùng là 111. Nhận thấy rằng nếu x là dãy đang có và phải là dãy cuối cùng thì dãy tiếp theo cần liệt kê chính là x cộng thêm 1 đơn vị trong hệ nhị phân! Ví dụ n = 6: Dãy đang có: 010000 Dãy đang có: 010111 Cộng thêm 1: +1 Cộng thêm 1: +1 ______ ______ Dãy mới: 010001 Dãy mới: 011000 Kỹ thuật sinh kế tiếp từ cấu hình hiện tại có thể mô tả như sau: xét từ cuối dãy lên từ hàng đơn vị tìm số 0 đầu tiên.  Nếu tìm thấy thì thay số 0 bằng số 1 và đặt tất cả phần tử phía sau vị trí đó bằng 0.  Nếu không tìm thấy thì toàn là dãy chứa 1, đây là cấu hình cuối cùng. Chương trình minh họa 1: chương trình C/C++ liệt kê chuỗi nhị phân n bit. int Stop; // biến toàn cục void Next_BS(int B[MAX], int n) // Hàm phát sinh chuỗi kế tiếp { int i = n; // duyệt từ cuối while (i>0 && B[i]) // lặp khi chưa tìm thấy B[i] ==0 { B[i] = 0; // gán các bit sau là 0 i--; // giảm về trước } if (i==0 ) Stop = 1; // cấu hình cuối nên không tìm được B[i] = 0 -> dừng 38
  • 39. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN else B[i] = 1; // gán 1 cho B[i] } void Generate(int B[MAX], int n) // Hàm sinh chuỗi nhị phân { Stop = 0; while (! Stop) { Result(B,n); // xuất chuỗi nhị phân hiện tại Next_BS(B,n); // chuỗi nhị phân tiếp theo. } } void Result(int B[MAX], int n) { static int count=0; printf(“n Xau nhi phan thu %d”, ++count); for(int i=0; i < n;i++) printf(“%3d”, B[i]); } int main() { int i, B[MAX], n; printf(“Nhap n: ”); scanf(“%d”,&n); for(i=0; i< n; i++) B[i] =0; Generate(b, n); getch(); return 0; } 2.1.2 Bài toán liệt kê tập con k phần tử  Phát biểu: Cho tập hợp X = {1, 2,.., n}. Hãy liệt kê tất cả tập con k phần tử của X. Mỗi tập con k phần tử của X cho thể biểu diễn như bộ thứ tự: a = (a1, a2,.., ak) thỏa mãn 1 ≤ a1 ≤ a2 ≤ ... ≤ ak ≤ n. Trên tập con k phần tử của X, ta định nghĩa thứ tự của các tập con như sau: Ta nói tập a = (a1, a2,.., ak) có thứ tự trước tập a’ = (a’1, a’2,.., a’k) theo thứ tự từ điển và ký hiệu là a < a’ nếu tìm được j sao cho: a1 = a’1, a2 = a’2..., aj-1 = a’j-1 và aj < a’j. Ví dụ với n = 5, k = 3, ta liệt kê 10 tập con của nó như sau: {{1,2,3},{1,2,4}{1,2,5}{1,3,4}{1,3,5}{1,4,5}{2,3,4}{2,3,5}{2,4,5}{3,4,5}} 39
  • 40. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN + Ta thấy cấu hình đầu tiên là {1, 2..., k} + Cấu hình kết thúc là {n-k+1, n-k+2,.., n}. Nhận xét: chúng ta sẽ in ra tập con với các phần tử của nó theo thứ tự tăng dần. Biểu diễn tập con là một dãy a{a1, a2,..., ak} trong đó a1< a2 <...<ak. Ta nhận thấy giới hạn trên của ak là n, của ak-1 là n-1, của ak-2 là n-2. Tổng quát giới hạn trên của ai = n-k+i. Còn giới hạn dưới của của ai (giá trị nhỏ nhất ai có thể nhận) là ai-1 + 1. Như vậy nếu ta đang có một dãy x đại diện cho tập con, nếu x là cấu hình kết thúc thì có nghĩa tất cả các phần tử trong x đều đạt tới giới hạn trên thì quá trình sinh kết thúc. Nếu không thì phải phát sinh một dãy x tăng dần thỏa mãn đủ lớn hơn dãy x và không có dãy nào chen vào giữa hai dãy theo thứ tự từ điển. Ví dụ: n = 9, k = 6, cấu hình đang có <1, 2, 6, 7, 8, 9>, các phần tử a3 ⇒a6 đã đạt đến giới hạn nên ta không thể tăng các phần tử này được, ta phải tăng a2 từ 2 lên thành 3. Được cấu hình mới là <1, 3, 6, 7, 8, 9> cấu hình này thoả mãn lớn hơn cấu hình cũ, nhưng chưa thoả mãn tính chất vừa đủ lớn do đó ta phải thay a3, a4, a5, a6 bằng giới hạn dưới của nó như sau: a3 = a(3-1= 2) + 1 = 3 + 1 = 4 a4 = a(4-1= 3) + 1 = 4 + 1 = 5 a5 = a(5-1= 4) + 1 = 5 + 1 = 6 a6 = a(6-1= 5) + 1 = 6 + 1 = 7 Vậy cấu hình tiếp theo <1, 3, 4, 5, 6, 7> là cấu hình cần tìm. Do đó muốn xác định cấu hình tiếp ta thấy a6 = 7 chưa đạt đến giới hạn ta chỉ cần tăng a6 lên một là được cấu hình tiếp theo: <1, 3, 4, 5, 6, 8>. Vậy kỹ thuật sinh tập con kế tiếp từ tập x đã có có thể xây dựng như sau: Tìm từ cuối lên đầu dãy cho tới khi gặp phần tử ai chưa đạt đến giới hạn n-k+i.  Nếu tìm thấy: o Tăng ai đó lên 1. o Đặt tất cả phần tử phía sau ai bằng giới hạn dưới.  Nếu không tìm thấy tức là phần tử đã đạt giới hạn trên, đây là cấu hình cuối cùng. Kết thúc thuật toán. Chương trình minh họa 2: liệt kê tập con k phần tử của n. 40
  • 41. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN int A[MAX], Stop, n ,k; void Next_SubSet() { int i,j; i = k; // duyệt từ cuối dãy // lặp khi chưa tìm được phần tử chưa tới giới hạn while (i >0 && A[i] == n-k+i) i--; // duyệt về đầu if ( i > 0) { A[i] = A[i] +1; // tăng một đơn vị // cho các phần tử còn lại qua giới hạn dưới for(j = i+1; j <= k; j++) A[j] = A[j-1]+ 1 } else Stop = 1; // kết thúc phát sinh cấu hình } void GenerateSet() { Stop = 0; while (!Stop) { Result(); // xuất cấu hình hiện tại Next_SubSet(); // qua cấu hình khác } } void Result() { static int count=0; printf(“Tap con thu %d”, ++count); for(i=1; i <=k; i++) printf(“%3d”, A[i]); } int main() { printf(“Nhap n: ”); scanf(“%d”, &n); printf(“Nhap k: ”); scanf(“%d”, &k); for(int i=1; i <= n;i++) A[i] = i; GenerateSet(); getch(); return 0; 41
  • 42. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN } 2.1.3 Bài toán liệt kê các hoán vị  Bài toán: Cho tập hợp X = {1, 2, ..., n}, hãy liệt kê tất cả hoán vị của X. Mỗi hoán vị n phần tử của tập X có thể biểu diễn bởi bộ có thứ tự gồm n thành phần a = {a1, a2,.., an} thoả ai ∈ X; i = 1, 2, .., n; ap ≠ aq nếu p ≠ q. Trên các tập hoán vị của X ta định nghĩa thứ tự của các hoán vị như sau: a = (a1, a2,..., an) được gọi là có thứ tự trước hoán vị a’=(a’1,a’2,..,a’n). Có ký hiệu a < a’ nếu tìm được chỉ số k sao cho. a1 = a’1, a2 = a’2,..., ak-1 = a’k-1, ak <a’k. Ví dụ X = {1, 2, 3, 4} khi đó thứ tự hoán vị n = 4 được liệt kê như sau: {{1, 2, 3, 4}, {1, 2, 4, 3}, {1, 3, 2, 4} {1, 3, 4, 2} {1, 4, 2, 3} {1, 4, 3, 2} {2, 1, 3, 4}, {2, 1, 4, 3}, {2, 3, 1, 4} {2, 3, 4, 1} {2, 4, 1, 3} {2, 4, 3, 1} {3, 1, 2, 4}, {3, 1, 4, 2}, {3, 2, 1, 4} {3, 2, 4, 1} {3, 4, 1, 2} {3, 4, 2, 1} {4, 1, 2, 3}, {4, 1, 3, 2}, {4, 2, 1, 3} {4, 2, 3, 1} {4, 3, 1, 2} {4, 3, 2, 1}} Hoán vị đầu tiên là: {1, 2, ..., n-1, n} và hoán vị cuối cùng là {n, n-1,..,2, 1}. Khi đó hoán vị kế tiếp sinh ra phải lớn hơn hoán vị hiện tại, và hơn nữa nó phải đủ lớn hơn hoán vị hiện tại theo nghĩa không có hoán vị nào khác chen vào giữa nó khi sắp theo thứ tự từ điển. Giả sử có hoán vị sau: <3, 2, 6, 5, 4, 1>, ta xét 4 phần tử cuối cùng, do chúng được sắp theo thứ tự giảm dần. Khi đó ta hoán vị 4 giá trị này thì cũng chỉ được hoán vị nhỏ hơn hoán vị hiện tại. Như vậy ta phải xét đến a2 = 2, ta phải thay giá trị này, nhưng thay giá trị nào? ta không thể thay bằng 1 vì nếu như vậy sẽ được hoán vị nhỏ hơn, không thể thay bằng 3 vì giá trị này đã có rồi a1 = 3 (phần tử sau không được chọn vào những giá trị xuất hiện ở phần tử trước). Chỉ còn lại giá trị 4, 5, 6. Vì cần một hoán vị đủ lớn nên ta chọn a2 = 4. Còn các giá trị a3, a4, a5, a6 sẽ lấy trong tập {2, 6, 5, 1}. Cũng do tính chất vừa đủ lớn nên ta sẽ tìm biểu diễn nhỏ nhất của 4 số này để gán cho a3, a4, a5, a6, là <1, 2, 5, 6> vậy ta được hoán vị mới là <3, 4, 1, 2, 5, 6> Nhận xét: đoạn cuối của hoán vị hiện tại được sắp giảm dần. số a5 là 4 là số nhỏ nhất trong đoạn cuối lớn hơn a2 = 2. Nếu đổi chỗ a5 cho a2 thì ta được a2 = 4 và đoạn cuối vẫn được xếp giảm dần là <6, 5, 2, 1> khi đó muốn biểu diễn nhỏ nhất cho các giá trị trong đoạn cuối thì ta chỉ cần đảo ngược đoạn cuối. 42
  • 43. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN Ví dụ trong hoán vị hiện tại <2, 1, 3, 4> có hoán vị kế tiếp là <2, 1, 4, 3>. Ta có thể xem <2, 1, 3, 4> có đoạn cuối giảm dần là một phần tử <4>. Vậy kỹ thuật sinh hoán vị kế tiếp từ hoán vị hiện tại có thể xây dựng như sau: Xác định đoạn cuối giảm dần dài nhất, tìm phần tử ai đứng trước đoạn cuối đó. Điều này đồng nghĩa với việc tìm từ vị trí sát cuối dãy lên đầu, gặp chỉ số i đầu tiên thoả mãn ai < ai+1.  Nếu tìm thấy chỉ số i như trên: trong đoạn cuối giảm dần, tìm phần tử ak nhỏ nhất thoả mãn ak > ai. Do đoạn cuối giảm dần nên thực hiện bằng cách từ cuối dãy lên đầu gặp chỉ số k đầu tiên thoả ak > ai. o Đảo giá trị ak và ai. o Lật ngược thứ tự đoạn cuối giảm dần (ai+1 đến ak) trở thành tăng dần  Nếu không tìm thấy tức là dãy giảm dần, đây là cấu hình cuối cùng. Chương trình minh họa 3: Liệt kê hoán vị n phần tử. int n, P[MAX], Stop; void Next_Permutation() { int j, k; j = n -1; while (j>0 && P[j]> P[j+1]) j--; if (j == 0) Stop = 1; else { k = n; while (P[j] > P[k]) k--; Swap(P[j], P[k]); l = j+1; r = n; while (l < r) { Swap(P[l], P[r]); l++; r--; } }// end else } void Permutation() 43
  • 44. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN { Stop = 0; while (! Stop) { Result(); Next_Permutation(); } } void Result() { static int count=0; printf(“n Hoan vi thu %d”, ++count); for(int i=1; i <= n; i++) printf(”%3d”, P[i]); } int main() { printf(“Nhap n: ”); scanf(“%d”, &n); for(int i=1; i <= n; i++) P[i] = i; return 0; } 44
  • 45. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN 2.2 Thuật toán quay lui (Back Tracking) Thuật toán quay lui dùng để giải quyết các bài toán liệt kê các cấu hình. Phương pháp sinh trong phần trước cũng được giải quyết cho các bài toán liệt kê khi nhận biết được cấu hình đầu tiên của bài toán. Tuy nhiên, không phải bất cứ cấu hình sinh kế tiếp nào cũng có thể sinh một cách đơn giản từ cấu hình hiện tại. Do đó thuật toán sinh kế tiếp chỉ giải quyết được cái bài toán liệt kê đơn giản. Để giải quyết những bài toán tổ hợp phức tạp, người ta dùng thuật toán quay lui. Nội dung chính của thuật toán quay lui: Xây dựng dần dần các thành phần của cấu hình bằng cách thử tất cả các khả năng có thể xảy ra. Giả sử cấu hình cần liệt kê có dạng x = (x1, x2,..,xn) khi đó thuật toán quay lui thực hiện qua các bước: 1. Xét tất cả những giá trị có thể có của x1, thử cho x1 nhận lần lượt các giá trị đó. Với mỗi giá trị thử gán cho x1 ta sẽ làm tiếp như sau: 2. Xét tất cả giá trị x2 có thể nhận, lại thử cho x2 nhận lần lượt các giá trị đó. Với mỗi giá trị x2 ta lại xét lần lượt những giá trị của x3... cứ tiếp tục như vậy cho đến bước n. 3. Xét giá trị có thể nhận cho xn, thử cho xn lần lượt nhận những giá trị đó, thông báo những cấu hình tìm được như (x1, x2,..., xn). Tóm lại thuật toán quay lui liệt kê các cấu hình n phần tử dạng x = (x1, x2,.., xn) bằng cách thử cho x1 nhận lần lượt các giá trị có thể được. Với mỗi giá trị thử gán cho x1 thì bài toán trở thành liệt kê tiếp cấu hình n-1 phần tử x = (x2, x3,.., xn). 45
  • 46. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN Hình 3.1: Liệt kê các lời giải theo thuật toán quay lui. Mô hình chung của thuật toán quay lui xác định thành phần thứ i được mô tả tổng quát như sau: (thuật toán này thử cho xi nhận lần lượt những giá trị mà nó có thể nhận). void Try(int i) { for <mọi giá trị v có thể gán cho x[i]> do { <Thử cho x[i] bằng giá trị v> if <x[i] là phần tử cuối cùng trong cấu hình hoặc i==n> then <Thông báo cấu hình tìm được> else { <Ghi nhận việc cho x[i] nhận giá trị v (nếu cần thiết)> Try(i+1); // gọi đệ quy cho tiếp chi x[i+1]. <Nếu cần thì bỏ ghi nhận việc thử x[i]:= v để thử giá trị khác> } } } Thuật toán quay lui sẽ bắt đầu bằng lời gọi Try(1). Gốc Khả năng chọn x1 Khả năng chọn x2 với x1 đã chọn Khả năng chọn x3 với x1 và x2 đã chọn 46
  • 47. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN 2.2.1 Thuật toán quay lui liệt kê dãy nhị phân n Biểu diễn dãy nhị phân độ dài n dưới dạng x = (x1, x2,..., xn) trong đó xi nhận các giá trị là {0, 1}. Với mỗi giá trị gán cho xi ta lại thử gán các giá trị có thể có cho xi+1. Thuật toán quay lui được viết như sau: void Try(int i, int B[MAX], int n) { int j; for(j=0; j <= 1; j++) { B[i] = j; if (i == n) Result(B, n); else Try(i+1, B, n); } } void Result(int B[MAX], int n) { int i; printf(“n”); for(i=1; i <= n; i++) printf(“%3d”, B[i]); } int main() { int n, B[MAX]; printf(“Nhap n: ”); scanf(“%d”, &n); for(int i=1; i <= n; i++) // khởi tạo cho mảng B B[i] = 0; Try(1, B, n); // gọi thuật toán quay lui return 0; } Khi n = 3, cây tìm kiếm quay lui như sau: 47
  • 48. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN Hình 3.2: Cây tìm kiếm quay lui trong bài toán liệt kê dãy nhị phân. 2.2.2 Thuật toán quay lui liệt kê tập con k phần tử Để liệt kê tập con k phần tử của tập S = {1, 2, ..., n} ta có thể đưa về liệt kê các cấu hình x = (x1, x2,.., xn) ở đây xi ∈ S và x1 < x2 < ...< xn. Từ đó giá trị được chọn cho xi là xi-1 + 1 cho đến n –k+i (1 ≤ i ≤ k ), giả thiết có thêm số x0 = 0 khi xét i =1. Như vậy xét tất cả cách chọn x1 từ 1 (x0 +1) đến n-k+1, với mỗi giá trị đó, xét tiếp tất cả cách chọn x2 từ x1+1 đến n-k+2...cứ như vậy khi chọn được xk thì ta có cấu hình cần liệt kê. Với trường hợp n = 5 {1, 2, 3, 4, 5} và k = 3 thuật toán quay lui liệt kê tập con k phần tử được minh họa như sau: Try(1) Try(2) Try(2) Try(3) Try(3) Try(3) Try(3) 000 x1 = 1x1 = 0 x2 = 0 x2 = 1 x2 = 0 x2 = 1 x3 = 0 x3 = 0 x3 = 0 x3 = 0x3 = 1 x3 = 1 x3 = 1 x3 = 1 001 010 011 100 101 110 111 48
  • 49. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN Hình 3.3: Cây liệt kê tập con 3 phần tử với n = 5. Chương trình quay lui liệt kê tập k phần tử: void Try(int i, int B[MAX], int k, int n) { int j; for(j = B[i-1] + 1; j <= (n-k+i); j++) { B[i] = j; if (i == k) Result(B, k); else Try(i+1, B, k, n); } } void Result(int B[MAX], int k) { static int count=0; printf(“Tap con thu %d: ”, ++count); for(i=1; i <= k; i++) printf(“%3d”, B[i]); } int main() { int n, k, B[MAX]; printf(“Nhap n: ”); scanf(“%d”,&n); printf(“Nhap k: ”); scanf(“%d”, &k); B[0] = 0; 1 2 3 4 5 4 5 3 4 5 3 4 2 3 4 4 5 5 5 N = 5; k = 3 49
  • 50. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN Try(1, B, k, n); return 0; } 2.2.3 Thuật toán quay lui liệt kê hoán vị n phần tử Biểu diễn hoán vị dưới dạng p1, p2,.., pn, trong đó pi nhận giá trị từ 1 đến n và pi ≠ pj với i ≠ j. Các giá trị từ 1 đến n được đề cử cho pi, trong đó giá trị j được chấp nhận nếu nó chưa được dùng trước đó. Do đó cần phải ghi nhớ xem giá trị j đã được dùng chưa. Ta thực hiện điều này bằng một mảng B, trong đó Bj = true nếu j chưa được dùng và ngược lại. Đầu tiên các giá trị trong B này phải được khởi tạo là true, sau khi gán j cho xi thì ghi nhận Bj = false, sau khi gọi xong thủ tục Try(i+1) thì thiết lập lại Bj = true, để đánh dấu nó chưa được dùng để cho bước thử tiếp theo. Hình 3.4: Cây liệt kê hoán vị 3 phần tử Chương trình quay lui liệt kê hoán vị m phần tử: void Try(int i, int P[MAX], int B[MAX], int n) { int j; for(j = 1; j <= n; j++) if (B[j] == 1) 1 2 3 n = 3 2 3 1 3 1 2 3 2 3 1 2 1 50
  • 51. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN { P[i] = j; B[j] = 0; // đánh dấu đã sử dụng j if (i == n) result(P, n); // xuất kết quả else Try(i+1, P, B, n); // thử cho bước tiếp theo B[j] = 1; // bỏ đánh dấu phần đã sử dụng j } } void Result(int P[MAX], int n) { static int count=0; printf(“Hoan vi thu %d”, ++count); for(int i=1; i<= n; i++) printf(”%3d”, P[i]); } int main() { int P[MAX], B[MAX], n; printf(“Nhap n: ”); scanf(“%d”, &n); for(int i=1; i <=n; i++) B[i] = 1; Try(1, P, B, n); return 0; } 2.2.4 Bài toán sắp xếp quân Hậu Yêu cầu: cho một bàn cờ vua nxn, hãy liệt kê cách sắp xếp n quân hậu sao cho các quân hậu không ăn được nhau! Quân hậu trên bàn cờ có thể ăn quân khác trên cùng hàng, cùng cột hoặc cùng đường chéo. Phân tích: các quân hậu sẽ được sắp trên các dòng khác nhau do chúng có thể ăn theo hàng ngang. Để dễ phân tích ta mô tả quân hậu theo dòng; quân hậu 1 ở dòng 1, quân hậu i ở dòng i…Do mỗi quân hậu chắc chắn nằm trên các dòng khác nhau nên ta chỉ cần xác định vị trí cột của mỗi quân hậu là xong. Tiếp theo ta xét những ràng buộc theo đường chéo, có hai đường chéo: o Chéo “”: theo hướng Trên Trái - Dưới Phải o Chéo “/”: theo hướng Trên Phải - Dưới Trái 51
  • 52. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN Hình 3.5: Các nước đi của quân hậu có thể có. Hình 3.6: Một cách sắp xếp 8 hậu trên bàn cờ 8x8 Các đường chéo Trên Trái - Dưới Phải như hình vẽ dưới, mỗi đường chéo này sẽ đi qua các ô, các ô này có tính chất: dòng - cột = C (hằng số). Do đó với mỗi đường chéo ta có 1 hằng số C và 1-n ≤ C ≤ n-1 xác định duy nhất đường chéo đó. 52
  • 53. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN Hình 3.7: Các đường chéo Trên Trái - Dưới Phải Các đường chéo Trên Phải - Dưới Trái: mỗi đường chéo này sẽ đi qua các ô có tính chất sau: dòng + cột = C (hằng số). Do đó với mỗi đường chéo ta có một hằng số C và 2 ≤ C ≤ 2n. Hình 3.8: Các đường chéo Trên Phải - Dưới Trái. 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 0 -1 -7 7 4 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 16 14 92 53
  • 54. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN Hình 3.9: Vị trí của quân hậu ảnh hưởng đến 2 đường chéo. Cài đặt: o Cấu trúc dữ liệu: o Mảng R[N]: lưu theo cột, R[i] = true ⇒cột i còn tự do, ngược lại cột i đã bị quân hậu khống chế. o Mảng C1[2*N-1]: lưu đường chéo TT-DP, do các dường chéo này có chỉ số từ 1-n ⇒n-1 nên ánh xạ chỉ số này vào mảng C1 bằng cách cộng thêm (n-1). Khi đó đường chéo 1-n sẽ có chỉ số là 0 trong mảng C1… o Mảng C2[2*N+1]: lưu đường chéo TP-DT, các đường chéo này có chỉ số từ 2- 2n nên ta đánh chỉ số C2 từ 2- 2n luôn cho tiện (hai phần tử C2[0] và C2[1] ta không dùng đến). o Các phần tử của 3 mảng R, C1 và C2 được gán giá trị True khi bắt đầu! o Thuật toán quay lui: o Ý tưởng chính như sau: xét tất cả các cột, thử đặt quân hậu 1 vào 1 cột, với mỗi cách đặt quân hậu 1, xét tất cả các đặt quân hậu 2 sao cho quân hậu 1 không ăn được nó, thử đặt quân hậu 2 vào ô có thể…rồi xét tiếp đến quân hậu 3 đến quân hậu n. Với mỗi cách đặt quân hậu n sẽ cho ta một kết quả! Khi xét hết tất cả các giá trị có thể có gán cho quân hậu thứ i thì thuật toán sẽ quay lên xét những giá trị còn lại của quân hậu thứ i-1. 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 Đừng chéo TT- DP có chỉ số 0 Đừng chéo TP- DT có chỉ số 10 Ô ( 5, 5) 54
  • 55. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN o Khi chọn vị trí j cho quân hậu thứ i, thì ô (i, j) không bị quân hậu đặt trước đó ăn. Do vậy ô (i, j) phải thoả điều kiện:  Cột j còn tự do.  Đường chéo TT-DP có chỉ số (i-j) không bị bất kỳ quân hậu nào khống chế.  Đường chéo TP-DT có chỉ số (i+j) cũng không bị các quân hậu trước đó khống chế. o Sau khi đặt quân hậu thứ i vào cột j, nếu i = n tức là đặt xong quân hậu cuối cùng ⇒được một bộ kết quả. Ngược lại  Đánh dấu 2 đường chéo TT-DP (i-j) và đường TP-DT(i+j) và cột j đã bị khống chế. Tiếp tục gọi đệ quy cho quân thứ i+1.  Sau khi gọi đệ quy cho quân hậu i+1, ta phải thử vị trí khác cho quân hậu thứ i trong số những giá trị j có thể nhận được. Do đó ta phải bỏ việc đánh dấu cột j và 2 đường chéo, lúc này cột j và 2 đường chéo đó sẽ tự do. Thao tác này cho phép quân hậu khác có thể đặt ở vị trí đó ở những bước tiếp sau. Chương trình C/C++ minh họa bài toán n-Hậu: #define MAX 12 void ShowResult(int b[MAX], int n) { /*Xuat ket qua theo dong*/ for(int i=0; i < n; i++) printf("(%d, %d)t", i+1, b[i]+1); printf("n"); } void Try(int *r,int *b, int n, int i, int *c1, int *c2) { for(int j=0; j < n; j++) //tìm vị trí cột { if (r[j] && c1[(i-j)+n-1] && c2[i+j]) //kiểm tra cột và 2 chéo { b[i] = j; // chọn cột j if (i==n-1) ShowResult(b,n); // xuất 1 bộ kết quả else { 55
  • 56. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN r[j] = false; // đánh dấu chọn cột j c1[(i-j)+n-1] = false; //chéo bị hậu khống chế c2[i+j] = false; //chéo bị hậu khống chế Try(r, b, n, i+1, c1, c2); // đặt hậu tiếp theo r[j] = true; // bỏ chọn cột j c1[(i-j)+n-1] = true; // chéo tự do c2[i+j] = true; // chéo tự do } } } } int main(int argc, char* argv[]) { int b[MAX], r[MAX]; int c1[2*MAX-1], c2[2*MAX-1]; int n; printf("doc n (<12): "); scanf("%d",&n); for(int i=0; i < n;i++) r[i] = true; for(i=0; i < 2*MAX-1; i++) { c1[i] = c2[i] = true; } Try(r, b, n, 0, c1, c2); return 0; } Kết quả khi n = 5 (1, 1) (2, 3) (3, 5) (4, 2) (5, 4) (1, 1) (2, 4) (3, 2) (4, 5) (5, 3) (1, 2) (2, 4) (3, 1) (4, 3) (5, 5) (1, 2) (2, 5) (3, 3) (4, 1) (5, 4) (1, 3) (2, 1) (3, 4) (4, 2) (5, 5) (1, 3) (2, 5) (3, 2) (4, 4) (5, 1) (1, 4) (2, 1) (3, 3) (4, 5) (5, 2) (1, 4) (2, 2) (3, 5) (4, 3) (5, 1) 56
  • 57. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN (1, 5) (2, 2) (3, 4) (4, 1) (5, 3) (1, 5) (2, 3) (3, 1) (4, 4) (5, 2) 2.2.5 Bài toán mã đi tuần Yêu cầu: Cho một bàn cờ tổng quát dạng nxn, hãy chỉ ra một hành trình của một quân Mã, xuất phát từ một vị trí bắt đầu đi qua tất cả các ô còn lại của bàn cờ, mỗi ô đi đúng một lần. Ý tưởng cơ bản: dùng thuật toán quay lui; xuất phát từ 1 ô, gọi số nước đi là t=1, ta cho quân mã thử đi tiếp 1 ô (có 8 nước đi có thể), nếu ô đi tiếp này chưa đi qua thì chọn làm bước đi tiếp theo. Tại mỗi nước đi kiểm tra xem tổng số nước đi bằng n*n chưa, nếu bằng thì mã đã đi qua tất cả các ô ⇒dừng (do chỉ cần tìm một giải pháp). Trường hợp ngược lại, gọi đệ quy để chọn nước đi tiếp theo. Ngoài ra, nếu tại một bước tìm đường đi, nếu không tìm được đường đi tiếp thì thuật toán sẽ quay lui lại nước đi trước và tìm đường đi khác… Hình 3.10: Minh họa tám nước đi tiếp của quân mã. 57
  • 58. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN Hình 3.11: Đường đi của quân mã trong bàn cờ 5x5 Cài đặt: o Cấu trúc dữ liệu: o Mảng board[MAX][MAX]: lưu bàn cờ, trong đó board[i][j] là ô (i, j); giá trị của board[i][j] là 0 khi quân mã chưa đi qua, và >0 khi quân mã đã đi qua, giá trị board[i][j] lúc này chính là thứ tự nước đi trên hành trình. Thật sự cài đặt mảng board như vậy là đủ, nhưng nếu bổ sung thêm một tí thì sẽ tăng tốc độ thực hiện. Vấn đề bổ sung liên quan đến đường biên, do mỗi lần di chuyển quân mã ta phải kiểm tra xem nước đi có ra ngoài biên hay không. Ta có thể mở rộng mảng board để không cần phải kiểm tra bằng cách mở rộng hai ô về bốn hướng trên dưới trái phải. Khi đó chỉ cần gán giá trị cho các ô ngoài biên là -1. 58
  • 59. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN Hình 3.12 : Mảng board cho bàn cờ 8x8 ⇒12x12. o Mảng dr[8], dc[8]: lưu các độ dời của bước đi kế tiếp, có tám nước đi có thể cho vị trí quân mã hiện tại. Do đó để đi nước thứ i ta chỉ cần cộng thêm dr[i] cho dòng và dc[i] cho cột! Hình 3.13: Thứ tự tám nước đi theo chiều kim đồng hồ. Mảng dr[] = {-2, -1, 1, 2, 2, 1, -1, -2} dc[] = {1, 2, 2, 1, -1, -2, -2, 1} o Thuật giải đệ quy: Tại mỗi bước lần lượt cho quân mã thử tất cả các nước đi kế tiếp (tám nước đi kế tiếp). Với mỗi bước đi, kiểm tra xem nếu nước đi hợp lệ (chưa đi qua và ở trong (-2, 1) (-1, 2) (1, 2) (2, 1)(2, -1) (1, -2) (-1, -2) (-2, -1) 59
  • 60. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN bàn cờ) thì thử đi nước này. Nếu quân mã đã đi qua hết bàn cờ thì xuất kết quả. Ngược lại thì gọi đệ quy tiếp cho vị trí mới thử trên. Lưu ý là mỗi khi vị trí đã đi qua được đánh dấu chính bằng chính thứ tự nước đi trên bàn cờ. Sau khi không thử vị trí này thì phải bỏ đánh dấu để chọn giải pháp khác (trường hợp quay lui). Minh họa hàm Try với step là thứ tự của nước đi, i và j là vị trí của quân mã hiện tại. Try( int step, int i, j) { + Với mỗi nước đi kế tiếp (ii, jj) từ (i, j) + Nếu (ii,jj) hợp lệ chọn (ii, jj) làm nước đi kế tiếp + nếu đi hết bàn cờ  xuất 1 kết quả + ngược lại Gọi đệ quy Try(step +1, ii, jj) Không chọn (ii, jj) là nước đi kế tiếp } Chương trình C/C++ minh họa cho trường hợp bàn cờ 8x8. #include "stdafx.h" #include "conio.h" #include "stdlib.h" #define MAX 12 // trường hợp bàn cờ 8x8 void Show(int board[MAX][MAX]); void Init(int board[MAX][MAX]) { for(int i=0;i<MAX;i++) for(int j=0;j<MAX;j++) if(i>=2 && i<=MAX-3 && j>=2 && j<=MAX-3) board[i][j]=0; // đánh dấu chưa đi qua else board[i][j]=-1; // đánh dấu biên } void Try(int step, int i, int j, int board[MAX][MAX], int *dr, int *dc) { 60
  • 61. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN for(int k=0; k<7; k++) //duyệt qua các nước đi kế tiếp { if( board[i+dr[k]][j+dc[k]]==0 ) // nếu vị trí này chưa đi qua { Board[i+dr[k]][j+dc[k]]= step+1; // đánh dấu chọn vị trí if(step+1==64) //hoàn tất một kết quả { Show(board); printf("Nhan <ENTER> de tiep tuc tim loi giai ke. Nhan <ESC> de thoat"); char c; if(c = getch() == 27) exit(1); } else // gọi đệ quy cho nước kế tiếp Try(step+1, i+dr[k], j+ dc[k], board, dr, dc); Board[i+dr[k]][j+dc[k]]= 0;// trả tự do cho vị trí vừa chọn }// end if }//end for } void Show(int board[MAX][MAX]) { for(int i=0;i<MAX;i++) { for(int j=0;j<MAX;j++) printf("%4d",board[i][j]); printf("nn"); } } void main() { int board[MAX][MAX]; int dr[8]={-2,-1,1, 2, 2, 1,-1,-2}; int dc[8]={1, 2, 2, 1,-1,-2,-2,-1}; Init(board); board[2][2]=1; // chọn vị trí đầu tiên Show(board); Try(1, 2, 2, board, dr, dc); } Một kết quả của chương trình như sau: 61
  • 62. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN 1 3 8 4 3 3 4 3 3 6 1 9 22 4 4 5 9 2 3 7 2 0 2 3 4 17 3 9 4 2 3 3 6 0 3 5 1 8 2 1 10 5 8 4 5 4 0 5 3 2 4 1 1 1 6 5 4 1 3 2 5 7 4 6 6 1 2 6 9 12 5 0 4 7 5 2 2 5 5 4 1 5 6 27 3 1 5 6 4 9 6 2 2 9 8 1 3 64 4 8 5 1 3 0 5 5 1 4 6 3 2 8 7 Hình 3.14: Một giải pháp cho bàn cờ 8x8.   62
  • 63. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN Chương 1 Tìm kiếm và Sắp xếp   1.1 Tìm kiếm Tìm kiếm là thao tác cơ bản, thường xuyên và quan trọng trong tin học. Ví dụ như tìm kiếm nhân viên trong danh sách nhân viên, tìm kiếm một sinh viên trong danh sách lớp học…Các hệ thống thông tin thường lưu trữ khối lượng dữ liệu lớn, nên thuật toán tìm kiếm tốt sẽ có nhiều lợi ích. Tuy nhiên, thao tác tìm kiếm còn phụ thuộc rất nhiều đến dữ liệu được tổ chức như thế nào; nếu dữ liệu được tổ chức tốt thì việc tìm kiếm sẽ tiến hành nhanh chóng và hiệu quả hơn. Giả sử sách được sắp theo chủ đề, thể loại thì dễ tìm kiếm hơn là không được sắp. Hoặc danh sách tên người được sắp theo thứ tự alphabet cũng dễ cho việc tìm kiếm… 1.1.1 Mô tả bài toán tìm kiếm trong tin học Tìm kiếm là quá trình xác định một đối tượng nào đó trong một tập các đối tượng. Kết quả trả về là đối tượng tìm được hoặc một chỉ số (nếu có) xác định vị trí của đối tượng trong tập đó. Việc tìm kiếm dựa theo một trường nào đó của đối tượng, trường này là khóa (key) của việc tìm kiếm. Ví dụ: đối tượng sinh viên có các dữ liệu {MaSV, HoTen, DiaChi,…}. Khi đó tìm kiếm trên danh sách sinh viên thì khóa thường chọn là MaSV hoặc HoTen. Thông thường người ta phân làm hai loại tìm kiếm: tìm kiếm tuyến tính hay còn gọi là tuần tự cho tập dữ liệu bất kỳ; tìm kiếm nhị phân cho tập dữ liệu đã được sắp xếp. Bài toán tìm kiếm được mô tả như sau: • Tập dữ liệu được lưu trữ là dãy a1, a2,..,an. Giả sử chọn cấu trúc dữ liệu mảng để lưu trữ dãy số này trong bộ nhớ chính, có khai báo: int a[n]; 63
  • 64. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN • Khoá cần tìm là x có kiểu nguyên : int x. Hình 4.1: Phân loại phương pháp tìm kiếm 1.1.2 Tìm kiếm tuyến tính Ý tưởng chính: duyệt tuần tự từ phần tử đầu tiên, lần lượt so sánh khóa tìm kiếm với khoá tương ứng của các phần tử trong danh sách (trong trường hợp mô tả trên là so sánh x và a[i]). Cho đến khi gặp phần tử cần tìm hoặc đến khi duyệt hết danh sách. Các bước tiến hành như sau : Bước 1: i = 1 ; Bước 2: so sánh a[i] với x, có hai khả năng i. a[i] = x: tìm thấy ⇒dừng ii. a[i] <> x: sang bước 3 Bước 3: i = i +1, kiểm tra chỉ số i và kích thước mảng n i. nếu i>n: hết mảng, không tìm thấy ⇒dừng ii. ngược lại: quay lại bước 2 Hàm tìm kiếm tuyến tính đơn giản minh họa bằng ngôn ngữ C/C++. Tìm kiếmTìm kiếm Tìm kiếm tuyến tínhTìm kiếm tuyến tính Tìm kiếm nhị phânTìm kiếm nhị phân Tập DL bất kỳ Tập DL được sắp 64 x ? a1 a2 a3 an-1 an
  • 65. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN int Search(int a[], int n, int key) { int i =0; while (i<n) && (key != a[i]) i++; if (i >= n) return -1; // tìm không thấy else return i; // tìm thấy tại vị trí i } 1.1.3 Tìm kiếm nhị phân Phương pháp tìm kiếm nhị phân được áp dụng cho dãy khoá đã có thứ tự: k[1] ≤ k[2] ≤ ... ≤ k[n]. Ý tưởng của phương pháp này như sau: Giả sử ta cần tìm trong đoạn a[left...right] với khoá tìm kiếm là x, trước hết ta xét phần tử giữa a[mid], với mid = (left + right)/2. • Nếu a[mid] < x thì có nghĩa là đoạn a[left] đến a[right] chỉ chứa khóa < x, ta tiến hành tìm kiếm từ a[mid+1] đến a[right]. • Nếu a[mid] > x thì có nghĩa là đoạn a[m] đến a[right] chỉ chứa khoá > x, ta tiến hành tìm kiếm từ a[left] đến a[mid-1]. • Nếu a[mid] = x thì việc tìm kiếm thành công. • Quá trình tìm kiếm thất bại nếu left > right. Các bước tiến hành như sau: Bước 1: left =1, right = n // tìm kiếm trên tất cả phần tử. Bước 2: mid = (left + right)/2 // lấy mốc so sánh So sánh a[mid] với x có 3 khả năng: - a[mid] = x, tìm thấy ⇒dừng - a[mid]> x, tìm tiếp trong dãy a[left].. a[mid-1] right = mid -1; - a[mid] < x, tìm tiếp trong dãy a[mid+1].. a[right] left = mid +1; Bước 3: Nếu left ≤ right; còn phần tử ⇒tìm tiếp ⇒bước 2 65
  • 66. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN Ngược lại: dừng, đã xét hết phần tử ⇒không tìm thấy. Ví dụ: cho dãy số gồm 8 phần tử {1, 2, 4, 5, 6, 8, 12, 15} và x = 8 Hình 4.2: Tìm kiếm nhị phân. Hàm C minh họa cài đặt thuật toán tìm kiếm nhị phân int BinarySearch(int key) { int left = 0, right = n-1, mid; while (left <= right) { mid = (left + right)/ 2; // lấy điểm giữa if (a[mid] == key) // nếu tìm được return mid; if (a[mid] < x) // tìm đoạn bên phải mid left = mid+1; else right = mid-1; // tìm đoạn bên trái mid } return -1; // không tìm được } 11 Left = 1 X =X = 88 Right = 8Mid = 4 Đoạn tìm kiếm 22 44 55 66 88 1212 1515 11 Left = 5 X =X = 88 Right = 8Mid = 6 Đoạn tìm kiếm 22 44 55 66 88 1212 1515 == 66
  • 67. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN 1.1.4 Kết luận • Giải thuật tìm kiếm tuyến tính không phụ thuộc vào thứ tự của các phần tử trong mảng, do vậy đây là phương pháp tổng quát nhất để tìm kiếm trên một dãy bất kỳ. • Thuật giải nhị phân dựa vào quan hệ giá trị của các phần tử trong mảng để định hướng trong quá trình tìm kiếm, do vậy chỉ áp dụng được với dãy đã có thứ tự. • Thuật giải nhị phân tìm kiếm nhanh hơn tìm kiếm tuyến tính. • Tuy nhiên khi áp dụng thuật giải nhị phân thì cần phải quan tâm đến chi phí cho việc sắp xếp mảng. Vì khi mảng được sắp thứ tự rồi thì mới tìm kiếm nhị phân. 1.2 Bài toán sắp xếp Sắp xếp là quá trình bố trí lại các phần tử của một tập đối tượng nào đó theo một thứ tự nhất định. Ví dụ như: tăng dần, giảm dần với một dãy số, thứ tự từ điển với các từ...Việc sắp xếp là một bài toán thường thấy trong tin học, do các yêu cầu tìm kiếm thuận lợi, sắp xếp kết quả các bảng biểu... Dữ liệu thường được tổ chức thành mảng các mẫu tin dữ liệu, mỗi mẫu tin thường có một số các trường dữ liệu khác nhau. Không phải toàn bộ các trường đều tham gia quá trình sắp xếp mà chỉ có một trường nào đó (hoặc một vài trường) được quan tâm. Người ta gọi trường này là khoá, việc sắp xếp sẽ được tiến hành dựa vào giá trị khoá này. Ví dụ: sắp xếp một mảng các số nguyên tăng dần, sắp xếp một danh sách học sinh với điểm thi giảm dần... 1.3 Một số phương pháp sắp xếp cơ bản Trong phần này giới thiệu một số phương pháp sắp xếp cơ bản thường được dùng để sắp xếp một danh sách, mảng dữ liệu. 1.3.1 Phương pháp chọn Đây là một trong những thuật toán sắp xếp đơn giản nhất. Ý tưởng cơ bản của phương pháp này được thể hiện như sau: 67
  • 68. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN 1. Ở lượt thứ nhất, ta chọn trong dãy khoá k[1..n] ra khoá nhỏ nhất và đổi giá trị nó với k[1], khi đó k[1] sẽ trở thành khoá nhỏ nhất. 2. Ở lượt thứ hai, ta chọn trong dãy khoá k[2..n] ra khóa nhỏ nhất và đổi giá trị nó cho k[2]. 3. ... 4. Ở lượt thứ i, ta chọn trong dãy khóa k[i..n] ra khóa nhỏ nhất và đổi giá trị nó cho k[i]. 5. Tới lượt k-1, ta chọn giá trị nhỏ nhất trong k[n-1] và k[n] ra khoá nhỏ nhất và đổi cho giá trị cho k[n-1]. Thuật giải SelectionSort: (mã giả, chỉ số 1 là đầu mảng) begin for i:= 1 to n-1 do begin jmin := i; for j:=i+1 to n do if a[j] < a[jmin] then jmin = j; if ( jmin <> i) Swap(a[i], a[jmin]) end. end. 1.3.2 Phương pháp sắp xếp nổi bọt Trong thuật toán sắp xếp nổi bọt, dãy khóa sẽ được duyệt từ cuối lên đầu dãy, nếu gặp hai khóa kế cận ngược thứ tự thì đổi chỗ cho nhau. Sau lần duyệt như vậy, khóa nhỏ nhất trong dãy khóa sẽ được chuyển về vị trí đầu tiên và vấn đề trở thành sắp xếp dãy khoá từ k[n] đến k[2]. Thuật giải bubblesort: (mả giả, chỉ số 1 là đầu mảng) begin for i:=2 to n do for j:= n downto i do if (a[j] < a[j-1]) Swap(a[j],a[j-1]) end. 1.3.3 Phương pháp sắp xếp chèn Xét dãy khóa k[1..n], ta thấy dãy con chỉ gồm mỗi một khoá là k[1] có thể coi là đã sắp xếp rồi. Xét thêm k[2], ta so sánh nó với k[1], nếu thấy k[2] < k[1] thì chèn nó vào trước k[1]. Đối với k[3], ta chỉ xét dãy chỉ gồm hai khoá k[1] và k[2] đã sắp xếp 68
  • 69. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN và tìm cách chèn k[3] vào dãy khóa đó để được thứ tự sắp xếp. Một cách tổng quát, ta sẽ sắp xếp dãy k[1..i] trong điều kiện dãy k[1..i-1] đã sắp xếp rồi bằng cách chèn k[i] vào dãy đó tại vị trí đúng khi sắp xếp. Thuật giải InsertionSort: (mả giả, chỉ số 1 là đầu mảng) begin for i:= 2 to n do begin tmp = a[i]; j = i-1; while (j>0) and (tmp < a[j]) begin a[j+1] = a[j];// đẩy lùi giá trị k[i] về sau -> tạo khoảng trống j := j-1; end k[j+1] = tmp; // chèn vào khoảng trống. end end. 1.3.4 Phương pháp đổi chỗ trực tiếp Ý tưởng chính: xuất phát từ đầu dãy, tìm những phần tử còn lại không thoả thứ tự sắp xếp với phần tử đang xét, hoán vị các phần tử tương ứng để thỏa thứ tự. Lặp lại tương tự với các phần tử tiếp theo của dãy. Các bước tiến hành như sau:  Bước 1: i = 1; // xuất phát từ đầu dãy  Bước 2: j = i+1; // tìm các phần tử phía sau i  Bước 3: o While j ≤ n do  Nếu a[j]< a[i] ⇒Swap(a[i], a[j]);  j = j+1;  Bước 4: i = i+1; o Nếu i < n: ⇒Bước 2 o Ngược lại ⇒Kết thúc Ví dụ: cho dãy số a: 10 3 7 6 2 5 4 16 69
  • 70. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN 10 3 7 6 2 5 4 16 i = 1 j = 2 3 10 7 6 2 5 4 16 i = 1 j = 5 70
  • 71. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN 2 10 7 6 3 5 4 16 i = 2 j = 3 2 7 10 6 3 5 4 16 i = 2 j = 4 2 6 10 7 3 5 4 16 i = 2 j = 5 71
  • 72. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN 2 3 10 7 6 5 4 16 i = 2 j = 5 2 3 10 7 6 5 4 16 i = 3 j = 4 72
  • 73. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN 2 3 7 10 6 5 4 16 i = 3 j = 5 2 3 6 10 7 5 4 16 i = 3 j = 6 2 3 5 10 7 6 4 16 i = 3 j = 7 73
  • 74. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN 2 3 4 10 7 6 5 16 i = 4 j = 5 2 3 4 7 10 6 5 16 i = 4 j = 6 2 3 4 6 10 7 5 16 i = 4 j = 7 74
  • 75. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN 2 3 4 5 10 7 6 16 i = 5 j = 6 2 3 4 5 7 10 6 16 i = 5 j = 7 2 3 4 5 6 10 7 16 i = 6 j = 7 2 3 4 5 6 7 10 16 i = 7 75
  • 76. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN 2 3 4 5 6 7 10 16 Hình 4.3: Minh họa đổi chỗ trực tiếp. 1.3.5 Phương pháp ShellSort Trong phương pháp sắp xếp kiểu chèn nếu ta luôn phải chèn một khóa vào vị trí đầu dãy thì dẫn đến hạn chế của thuật toán này. Để khắc phục trong trường hợp này thì người ta đưa ra một phương pháp sắp xếp là ShellSort. Ý tưởng chính: xét một dãy a[1]...a[n], cho một số nguyên h (1 ≤ h ≤ n), ta có thể chia dãy đó thành h dãy con như sau:  Dãy con 1: a[1], a[1+ h], a[1+2h]...  Dãy con 2: a[2], a[2+h], a[2+2h]...  Dãy con 3: a[3], a[3+h], a[3+2h]...  ...  Dãy con h: a[h], a[2h], a[3h]... Ví dụ cho dãy: 10 3 7 6 2 5 4 16 n = 8, h = 3. Ta có dãy con sau: Dãy chính 10 3 7 6 2 5 4 16 Dãy con 1 10 6 4 Dãy con 2 3 2 16 Dãy con 3 7 5 Những dãy này được coi là những dãy con xếp theo độ dài bước h. Tư tưởng chính của thuật toán ShellSort là: với mỗi bước h, áp dụng thuật toán sắp xếp kiểu chèn từng dãy con độc lập để làm mịn dần các phần tử trong dãy chính. Tiếp tục làm tương tự đối với bước (h div 2)... cho đến khi h = 1 thì ta được dãy phần tử được sắp. Xét trong ví dụ trên, nếu chúng ta dùng phương pháp chèn thì với phần tử a[5] = 2 là phần tử nhỏ nhất trong dãy, do đó nó phải chèn vào vị trí thứ 1, tức là phải chèn trước 4 phần tử trước nó. Nhưng nếu chúng ta xem 2 là phần tử của dãy 2 thì ta chỉ cần chèn trước một phần tử là 3. Đây chính là nguyên nhân thuật toán ShellSort thực hiện hiệu quả hơn sắp xếp chèn. Khi đó khóa nhỏ nhanh chóng đưa về gần vị trí đúng của nó. 76
  • 77. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN Các bước thực hiện chính như sau:  Bước 1: chọn k khoảng cách h[1], h[2],.., h[k], và i = 1.  Bước 2: Chia dãy ban đầu thành các dãy con có bước nhảy là h[i]. Thực hiện sắp xếp từng dãy con bằng phương pháp chèn trực tiếp.  Bước 3: i = i+1 o Nếu i > k: ⇒Dừng o Ngược lại: ⇒Bước 2. Ví dụ: cho dãy bên dưới với n = 8, h = {5, 3, 1}. 10 3 7 6 2 5 4 16 Ta có minh họa như sau: 10 3 7 6 2 5 4 16 h = 5 Daõy 1 Daõy 2 Daõy 3 Daõy 4 Daõy 5 77
  • 78. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN 5 3 7 6 2 10 4 16 h = 3 Daõy 1 Daõy 2 Daõy 3 5 2 7 4 3 10 6 16 h = 1 2 3 4 5 6 7 10 16 Daõy 1(daõy chính) Hình 4.4: Minh hoạ ShellSort. Cài đặt ShellSort: sắp xếp dãy a[] tăng, với h[] là mảng chứa các độ dài (bước nhảy) đã chọn sẵn: void ShellSort(int a[], int n, int h[], int k) { int step, i, j; int x, len; for(step = 0; step < k; step++) // duyệt qua từng bước nhảy { len = h[step]; // chiều dài của bước nhảy for(i = len; i < n; i++) // duyệt các dãy con { // lưu phần tử cuối để tìm vị trí thích hợp trong dãy con x = a[i]; 78
  • 79. Giáo trình Kỹ thuật lập trình 2 Khoa CNTT – ĐH KTCN // a[j] đứng trước a[i] trong cùng dãy con j = i – len; while ((x < a[j]) && (j>= 0)) // sắp xếp dãy con chứa x dùng pp chèn { a[j+len] = a[j]; // dời về sau theo dãy con j = j – len; // qua phần tử trước trong dãy con } a[j+len] = x;// đưa x vào vị trí thích hợp trong dãy con } } } 1.3.6 Phương pháp phân đoạn QuickSort Đây là một phương pháp sắp xếp tốt do C.A.R Hoare đề xuất. Thuật toán này có tốc độ trung bình nhanh hơn các thuật toán sắp xếp tổng quát khác. Do đó Hoare dùng chữ “Quick” để đặt tên cho thuật toán này. Ý tưởng chính: Để sắp dãy a[1] ... a[n], ta thực hiện sắp xếp dãy a từ chỉ số 1 đến chỉ số n. QuickSort dựa trên phân hoạch dãy ban đầu thành hai phần dựa vào giá trị x, x là giá trị của một phần tử tùy ý trong dãy ban đầu:  Dãy thứ 1: gồm các phần tử a[1]..a[i] có giá trị không lớn hơn x.  Dãy thứ 2: gồm các phần tử a[i]..a[n] có giá trị không nhỏ hơn x. Sau khi phân hoạch thì dãy ban đầu được phân thành ba phần: 1. a[k] < x, với k = 1..i 2. a[k] = x, với k = i..j 3. a[k] > x, với k = j..n a[k] < x a[k] = x a[k] > x Ta có nhận xét khi đó dãy con thứ 2 đã có thứ tự, nếu dãy con 1 và dãy con 3 có một phần tử thì chúng cũng đã có thứ tự, khi đó dãy ban đầu đã được sắp. Ngược lại, nếu dãy con 1 và 3 có nhiều hơn một phần tử thì dãy ban đầu có thứ tự khi dãy con 1 và 3 được sắp. Để sắp xếp dãy con 1 và 3, ta lần lượt tiến hành việc phân hoạch từng dãy con theo cùng phương pháp vừa trình bày. Giải thuật phân hoạch dãy a[left], a[left+1],.., a[right] thành hai dãy con: 79