124
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

Giao trinh ky thuat lap trinh 2

Embed Size (px)

DESCRIPTION

Giao trinh ky thuat lap trinh 2

Citation preview

Page 1: Giao trinh ky thuat lap trinh 2

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

Page 2: Giao trinh ky thuat lap trinh 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

Page 3: Giao trinh ky thuat lap trinh 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

Page 4: Giao trinh ky thuat lap trinh 2

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àmThô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

Page 5: Giao trinh ky thuat lap trinh 2

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ọab boolean bool bStopc char char cLetterGenrestr/s C++ string string strFirstNamesi short integer short siTablesi/n integer int iCars

int nCarsli long integer long liStarsf floating point float fPercentd Double precision floating point double dMilesld long double precision floating

point

long double ldPI

sz Null terminated string char szName[NAME_LEN]if Input file stream ifstream ifNameFileis Input stream istream isInputof Output file stream ofstream ofNameFileos Output stream ostream osOutS Struct struct sPoint {…}C Class class CStudent {…}w Word word wCharu 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_strBufflp long pointer LPCTSTR lpszClassName

5

Page 6: Giao trinh ky thuat lap trinh 2

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();}elseAction3();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

Page 7: Giao trinh ky thuat lap trinh 2

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ôngTham số vào: n số cần kiểm traTham 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 ảnhint Rows, Cols; // số dòng, số cột

7

Page 8: Giao trinh ky thuat lap trinh 2

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ànhint 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

Page 9: Giao trinh ky thuat lap trinh 2

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

Page 10: Giao trinh ky thuat lap trinh 2

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ằngfor(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

Page 11: Giao trinh ky thuat lap trinh 2

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

Page 12: Giao trinh ky thuat lap trinh 2

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

Page 13: Giao trinh ky thuat lap trinh 2

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

Page 14: Giao trinh ky thuat lap trinh 2

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

Page 15: Giao trinh ky thuat lap trinh 2

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

Page 16: Giao trinh ky thuat lap trinh 2

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

Page 17: Giao trinh ky thuat lap trinh 2

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

Page 18: Giao trinh ky thuat lap trinh 2

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 = 5return 5* Giaithua(4)

n = 4return 4* Giaithua(3)

n = 3return 3* Giaithua(2)

n = 2return 2* Giaithua(1)

n = 1return 1* Giaithua(0)

long l = Giaithua(5)

1

2

6

24

120

Giaithua(5)

Giaithua(4)

Giaithua(3)

Giaithua(2)

Giaithua(1)

n = 0return 1

Giaithua(0)1

18

Page 19: Giao trinh ky thuat lap trinh 2

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

Page 20: Giao trinh ky thuat lap trinh 2

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 đệ quyPhươ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

Page 21: Giao trinh ky thuat lap trinh 2

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);

elsereturn 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ụ đệ quyTrong 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

Page 22: Giao trinh ky thuat lap trinh 2

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

Page 23: Giao trinh ky thuat lap trinh 2

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 23 qua cọc 2

B1 B2 B3

C1

C2

C3

C1

C2 C

3 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

Page 24: Giao trinh ky thuat lap trinh 2

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

Page 25: Giao trinh ky thuat lap trinh 2

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

Page 26: Giao trinh ky thuat lap trinh 2

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 = a

1

26

Page 27: Giao trinh ky thuat lap trinh 2

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 đệ quyTạ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

Page 28: Giao trinh ky thuat lap trinh 2

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

Page 29: Giao trinh ky thuat lap trinh 2

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ảno 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

Page 30: Giao trinh ky thuat lap trinh 2

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

Page 31: Giao trinh ky thuat lap trinh 2

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

Page 32: Giao trinh ky thuat lap trinh 2

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

Page 33: Giao trinh ky thuat lap trinh 2

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

Page 34: Giao trinh ky thuat lap trinh 2

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

Page 35: Giao trinh ky thuat lap trinh 2

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

Page 36: Giao trinh ky thuat lap trinh 2

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

Page 37: Giao trinh ky thuat lap trinh 2

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 sinhPhươ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

Page 38: Giao trinh ky thuat lap trinh 2

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 7x 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ụcvoid Next_BS(int B[MAX], int n) // Hàm phát sinh chuỗi kế tiếp{

int i = n; // duyệt từ cuốiwhile (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à 0i--; // 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

Page 39: Giao trinh ky thuat lap trinh 2

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

elseB[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ạiNext_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

Page 40: Giao trinh ky thuat lap trinh 2

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

Page 41: Giao trinh ky thuat lap trinh 2

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ạnwhile (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ạiNext_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

Page 42: Giao trinh ky thuat lap trinh 2

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

Page 43: Giao trinh ky thuat lap trinh 2

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

Page 44: Giao trinh ky thuat lap trinh 2

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

Page 45: Giao trinh ky thuat lap trinh 2

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

Page 46: Giao trinh ky thuat lap trinh 2

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 x

1

và x2 đã chọn

46

Page 47: Giao trinh ky thuat lap trinh 2

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 nBiể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

Page 48: Giao trinh ky thuat lap trinh 2

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 = 1x

1 = 0

x2 = 0 x

2 = 1 x

2 = 0 x

2 = 1

x3 = 0 x

3 = 0 x

3 = 0 x

3 = 0x

3 = 1 x

3 = 1 x

3 = 1 x

3 = 1

001 010 011 100 101 110 111

48

Page 49: Giao trinh ky thuat lap trinh 2

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

Page 50: Giao trinh ky thuat lap trinh 2

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

Page 51: Giao trinh ky thuat lap trinh 2

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 jif (i == n)

result(P, n); // xuất kết quảelse

Try(i+1, P, B, n); // thử cho bước tiếp theoB[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ậuYê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

Page 52: Giao trinh ky thuat lap trinh 2

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

Page 53: Giao trinh ky thuat lap trinh 2

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

Page 54: Giao trinh ky thuat lap trinh 2

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

Page 55: Giao trinh ky thuat lap trinh 2

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 12void 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 jif (i==n-1)

ShowResult(b,n); // xuất 1 bộ kết quảelse{

55

Page 56: Giao trinh ky thuat lap trinh 2

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 jc1[(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 theor[j] = true; // bỏ chọn cột jc1[(i-j)+n-1] = true; // chéo tự doc2[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

Page 57: Giao trinh ky thuat lap trinh 2

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ầnYê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

Page 58: Giao trinh ky thuat lap trinh 2

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

Page 59: Giao trinh ky thuat lap trinh 2

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

Page 60: Giao trinh ky thuat lap trinh 2

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

Page 61: Giao trinh ky thuat lap trinh 2

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("\n\n"); }

}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

Page 62: Giao trinh ky thuat lap trinh 2

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

138

43

34

336

19

22

44

59

237

20

23

4 17

39

42

33

60

35

18

21

10

58

45

40

53

24

11

16

5

41

32

57

46

61

26

9 12

50

47

52

25

54

15

6 27

31

56

49

62

29

813

64

48

51

30

55

14

63

28

7

Hình 3.14: Một giải pháp cho bàn cờ 8x8.

62

Page 63: Giao trinh ky thuat lap trinh 2

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ếmTì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ọcTì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

Page 64: Giao trinh ky thuat lap trinh 2

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

Page 65: Giao trinh ky thuat lap trinh 2

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ấyelse

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

Page 66: Giao trinh ky thuat lap trinh 2

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ữaif (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

}

11Left = 1

X = X = 88

Right = 8Mid = 4

Đoạn tìm kiếm

22 44 55 66 88 1212 1515

11Left = 5

X = X = 88

Right = 8Mid = 6

Đoạn tìm kiếm

22 44 55 66 88 1212 1515

==

66

Page 67: Giao trinh ky thuat lap trinh 2

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ếpSắ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ảnTrong 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

Page 68: Giao trinh ky thuat lap trinh 2

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)

beginfor i:= 1 to n-1 dobegin

jmin := i;for j:=i+1 to n do

if a[j] < a[jmin] thenjmin = 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ọtTrong 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)

beginfor i:=2 to n do

for j:= n downto i doif (a[j] < a[j-1])

Swap(a[j],a[j-1])end.

1.3.3 Phương pháp sắp xếp chènXé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

Page 69: Giao trinh ky thuat lap trinh 2

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)

beginfor i:= 2 to n dobegin

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ốngj := j-1;

endk[j+1] = tmp; // chèn vào khoảng trống.

endend.

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

Page 70: Giao trinh ky thuat lap trinh 2

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

Page 71: Giao trinh ky thuat lap trinh 2

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

Page 72: Giao trinh ky thuat lap trinh 2

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

Page 73: Giao trinh ky thuat lap trinh 2

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

Page 74: Giao trinh ky thuat lap trinh 2

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

Page 75: Giao trinh ky thuat lap trinh 2

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

Page 76: Giao trinh ky thuat lap trinh 2

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 ShellSortTrong 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 16Dãy con 1 10 6 4Dãy con 2 3 2 16Dã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

Page 77: Giao trinh ky thuat lap trinh 2

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

Page 78: Giao trinh ky thuat lap trinh 2

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ảyfor(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 conx = a[i];

78

Page 79: Giao trinh ky thuat lap trinh 2

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 conj = 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 conj = 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

Page 80: Giao trinh ky thuat lap trinh 2

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

Bước 1 : Chọn tùy ý một phần tử a[k] trong dãy là giá trị mốc, left ≤ k ≤ right,

o Cho x = a[k], i = left, j = right.

Bước 2 : Tìm và hoán vị cặp phần tử a[i] và a[j] không đúng thứ tự đang sắp.

o Bước 2-1 : Trong khi a[i] < x ⇒ i++;

o Bước 2-2 : Trong khi a[j] > x ⇒ j--;

o Bước 2-3 : Nếu i < j ⇒ Swap(a[i], a[j]) // a[i], a[j] sai thứ tự

Bước 3 :

o Nếu i < j: ⇒ Bước 2; // chưa hết mảng dừng

o Nếu i ≥ j: ⇒ Dừng.

Giải thuật để sắp xếp dãy a[left], a[left+1],.., a[right]: được phát biểu theo cách đệ quy

như sau:

Bước 1 : Phân hoạch dãy a[left]...a[right] thành các dãy con:

o Dãy con 1: a[left]...a[j] < x

o Dãy con 2: a[j+1]...a[i-1] = x

o Dãy con 3: a[i]...a[right] > x

Bước 2 :

o Nếu (left < j) // dãy con 1 có nhiều hơn 1 phần tử

Phân hoạch dãy a[left]...a[j]

o Nếu (i < right) // dãy con 3 có nhiều hơn 1 phần tử

Phân hoạch dãy a[i]...a[right]

Ví dụ phân hoạch dãy sau: 10 3 7 6 2 5 4 16

Phân hoạch đoạn left = 1, right = 8, x = a[4] = 6

10 3 7 6 2 5 4 16

left = 1 right = 8

4 3 5 2 6 7 10 16

Phân hoạch đoạn left = 1, right = 4, x = a[2] = 3

80

Page 81: Giao trinh ky thuat lap trinh 2

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

4 3 5 2 6 7 10 16

left = 1 right = 4

2 3 5 4 6 7 10 16

Phân hoạch đoạn left = 3, right = 4, x = a[3] = 5

2 3 5 4 6 7 10 16

left = 3 right = 4

2 3 4 5 6 7 10 16

Phân hoạch đoạn left = 6, right = 8, x = a[7] = 10

2 3 4 5 6 7 10 16

left = 6 right = 8

2 3 4 5 6 7 10 16

QuickSort được cài đặt đệ quy như sau:

void QuickSort(int a[], int left, int right){

int i, j;

81

Page 82: Giao trinh ky thuat lap trinh 2

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

int x;x = a[(left+right)/2]; // chọn phần tử giữa làm gốci = left;j = right;do{

while (a[i] < x) i++; // lặp đến khi a[i] >= xwhile (a[j] > x) j--; // lặp đến khi a[i] <= xif ( i <= j) // nếu có 2 phần tử a[i] và a[j] ko theo thứ tự{

Swap(a[i], a[j]);i++; // qua phần tử kế tiếpj--; // qua phần tử đứng trước

}} while (i<j);if (left < j) // phân hoạch đoạn bên trái

QuickSort(a, left, j);if (right > i) // phân hoạch đoạn bên phải

QuickSort(a, i, right);}

82

Page 83: Giao trinh ky thuat lap trinh 2

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

1.3.7 Phương pháp cơ số RadixSortThuật giải sắp xếp theo phương pháp cơ số không quan tâm đến việc so sánh giá trị

của các phần tử như các thuật giải trước. RadixSort sử dụng cách thức phân loại các con

số trong dãy và thứ tự phân loại con con số này để tạo ra thứ tự cho các phần tử. Đây là

cách tiếp cận khác so với các phương pháp sắp xếp trước là so sánh các giá trị của các

phần tử.

Thuật giải dựa trên ý tưởng chính là sắp xếp từng con số của một dãy các số. Giả sử

chúng ta có dãy số như sau: 493 812 715 710 195 437 582 340 385.

Đầu tiên sắp xếp các số theo hàng đơn vị: 493 812 715 710 195 437 582 340

385. Ta được bảng kết quả minh họa như sau:

Số hàng đơn vị Dãy con0 710 3401 -2 812 5823 4934 -5 715 195 3856 -7 4378 -9 -

Bảng 4.1: Sắp theo hàng đơn vị

Lưu ý những phần tử này được đưa vào dãy con theo thứ tự tìm thấy, do đó chúng ta có

thể thấy là các dãy con chưa có thứ tự. Lúc này chúng ta thu được một danh sách gồm các

dãy con từ 0 → 9 như sau:

710 340 812 582 493 715 195 385 437

Tiếp tục chúng ta phân loại các phần tử của dãy trên theo con số của hàng chục:

710 340 812 582 493 715 195 385 437

Số hàng chục Dãy con0 -1 710 812 7152 -3 437

83

Page 84: Giao trinh ky thuat lap trinh 2

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

4 3405 -6 -7 -8 582 3859 493 195

Bảng 4.2: Sắp theo hàng chục

Lúc này chúng ta thu được danh sách như sau:

710 812 715 437 340 582 385 493 195

Thực hiện tiếp với phân loại các con số hàng trăm:

710 812 715 437 340 582 385 493 195

Số hàng trăm Dãy con0 -1 1952 -3 340 3854 437 4935 5826 -7 710 7158 8129 -

Bảng 4.3: Sắp theo hàng trăm

Thu được danh sách các phần tử từ dãy con được phân loại theo hàng trăm từ 0 → 9.

195 340 385 437 493 582 710 715 812

Như chúng ta thấy thì lúc này dãy đã được sắp!

Tóm lại để sắp xếp dãy a[1], a[2],..., a[n] giải thuật RadixSort thực hiện như sau:

Xem mỗi phần tử a[i] trong dãy a[1]...a[n] là một số nguyên có tối đa m chữ số

Lần lượt phân loại các chữ số theo hàng đơn vị, hàng chục, hàng trăm...Tại mỗi

bước phân loại ta sẽ nối các dãy con từ danh sách đã phân loại theo thứ tự 0 → 9.

Sau khi phân loại xong ở hàng thứ m cao nhất ta sẽ thu được danh sách các phần

tử được sắp.

Các bước thực hiện thuật giải:

Bước 1 : k = 0; // k là chữ số phân loại, k = 0 hàng đơn vị, k = 1 hàng chục...

Bước 2 : // Tạo các dãy chứa phần tử phân loại B[0]...B[9]

84

Page 85: Giao trinh ky thuat lap trinh 2

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

Khởi tạo B[0]...B[9] rỗng, chưa chứa phần tử nào, B[i] sẽ chứa các phần tử có chữ

số thứ k là i.

Bước 3 :

o For i=1 to n do

Đặt a[i] vào dãy B[j] với j là chữ số thứ k của a[i].

o Nối B[0], B[1],..., B[9] lại theo đúng trình tự thành a.

Bước 4 :

o k = k +1

o Nếu k < m: ⇒ Bước 2. // m là số lượng chữ số tối đa của các số

trong mảng

o Ngược lại: ⇒ Dừng.

Giải thuật sắp xếp theo cơ số RadixSort:

Mảng a[MAX] chứa các phần tử của mảng cần sắp tăng.

Mảng B[10][MAX] chứa các dãy số được phân tạm thời theo các

con số. Ví dụ B[0] chứa các phần tử có con số ở hàng đơn vị là 100, 210,

320...Khi đó với mỗi dòng của B thì sẽ phân các phần tử có con số ở hàng thứ i (i

từ 0 - 9), các giá trị cột j sẽ lần lượt chứa các phần tử có cùng con số ở hàng thứ i.

Mảng Len[10] chứa số lượng các phần tử của các dòng B[i]. Ví dụ

B[0] có 3 phần tử thì Len[0] = 3, B[5] có 2 phần tử thì B[5] = 2. Tại mỗi bước

trước khi phân các phần tử vào mảng B thì các Len[i] được khởi tạo là 0.

Cài đặt RadixSort:

void RadixSort(long a[], int n){

int i, j, d;int h = 10; // biến để lấy các con số, bắt đầu từ hàng đơn vịlong B[10][MAX]; // mảng hai chiều chứa các phần tử phân lôint Len[10]; // kích thước của từng mảng B[i]// MAXDIGIT là số con số tối đa của các phần tử a[i]for(d = 0; d < MAXDIGIT; d++){

// khởi tạo kích thước các dãy B[i] là 0for( i = 0; i < 10; i++)

Len[i] = 0;

85

Page 86: Giao trinh ky thuat lap trinh 2

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

// thực hiện phân lô các phần tử theo con số hàng thứ d tính từ cuốifor(i = 0; i < n; i++)// duyệt qua tất cả các phần tử của mảng{

digit = (a[i] % h) / (h/ 10); // lấy con số theo hàng h// đưa vào dãy (lô) B[digit] ở cột Len[digit]B[digit][Len[digit]++] = a[i];

}// end for i// thực hiện nối lại tuần tự từ B[0] – đến B[9] vào mảng a[] ban đầunum = 0; // chỉ số bắt đầu cho mảng a[]for(i = 0; i < 10; i++) // duyệt qua các dãy từ B[0] – đến B[9]{

// lấy từng phần tử của từng dãy B[i]for(j =0; j < Len[i]; j++)

a[num++] = B[i][j];}// end for ih *= 10; // qua hàng kế tiếp.

}// end for d}// end RadixSort

86

Page 87: Giao trinh ky thuat lap trinh 2

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

Chương 2

Stack - Queue

2.1 Giới thiệu Stack – ngăn xếpNgăn xếp là kiểu danh sách với hai thao tác đặc trưng bổ sung một phần tử vào cuối

danh sách và loại bỏ một phần tử cũng ở cuối danh sách. Nói một cách khác là thao tác

thêm phần tử và loại phần tử chỉ diễn ra ở một đầu của danh sách.

Hình 5.1: Minh họa Stack.

Chúng ta có thể hình dung một cách đơn giản như hình ảnh một chồng đĩa, đĩa nào

được đặt vào chồng sau cùng sẽ nằm trên tất cả đĩa khác và sẽ được lấy ra đầu tiên. Vì

nguyên tắc vào sau ra trước đó, Stack còn có tên gọi là danh sách kiểu LIFO (Last In

First Out). Vị trí cuối cùng được gọi là đỉnh (top) của ngăn xếp.

Hai thao tác chính trên Stack là:

1. Thao tác push: đưa một phần tử vào đỉnh của Stack

2. Thao tác pop: xoá đi một phần tử ở đỉnh của Stack.

Ví dụ: Cho Stack S và các thao tác như sau:

1. Khởi tạo S là rỗng như hình 5.2.1.

2. Thêm một phần tử vào Stack S: Push(S, A) có kết quả như hình 5.2.2

3. Thêm một phần tử vào Stack S: Push(S, B) có kết quả như hình 5.2.3.

87

Đưa vào (Push) Lấy ra (Pop)

Stack

Đỉnh stack (top)

Page 88: Giao trinh ky thuat lap trinh 2

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

4. Loại phần tử đầu Stack S: Pop(S) ta được Stack S như hình 5.2.4.

5. Thêm phần tử vào Stack S: Push(S, C) kết quả Stack S như hình 5.2.5.

ABA A

CA

Hình 1 Hình 2 Hình 3 Hình 4 Hình 5

Hình 5.2: Minh họa các thao tác trên Stack.

2.1.1 Cài đặt Stack dùng CTDL mảng

Trong phần này chúng ta sẽ cài đặt một Stack dùng cấu trúc dữ liệu mảng. Để cho

tổng quát ta gọi Data là kiểu dữ liệu được định nghĩa mà Stack lưu trữ, ví dụ Data có thể

là số nguyên, thực... hoặc một cấu trúc dữ liệu nào đó:

typedef int Data

typedef float Data

typedef struct {

int ID;

char Name[50];

float Salary;

...

} Data;

...

Khai báo cấu trúc dữ liệu Stack trên mảng:

#define MAX 100

typedef struct{

int top;

Data S[MAX];

} Stack;

88

Page 89: Giao trinh ky thuat lap trinh 2

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

CBA

Top

Hình 5.3: Minh họa Stack dùng mảng một chiều.

Trong cấu trúc Stack này các trường có ý nghĩa như sau:

top: chỉ đến đỉnh của Stack, top = -1 ⇒ Stack rỗng, top ≥ 0 ⇒ Stack có phần tử

S[MAX]: chứa dữ liệu của Stack lưu trữ, để truy cập đến phần tử đỉnh thì dùng

trường top.

Các thao tác trên Stack được mô tả như sau:

void Push(Stack &st, Data x): Đưa một phần tử x vào đỉnh của Stack.

Data Pop(Stack &st): Lấy một phần tử ở đỉnh ra khỏi Stack.

void InitStack(Stack &st): Hàm khởi tạo một Stack mới rỗng

int IsEmpty(Stack st): Kiểm tra xem Stack có rỗng hay không.

int IsFull(Stack st): Kiểm tra xem Stack đầy hay chưa.

Data Top(Stack st): Xem một phần tử ở đỉnh (không lấy ra).

Cài đặt của các thao tác:

#define TRUE 1#define FALSE 0void Push(Stack &st, Data x){

if (IsFull(st)){

printf(“\nStack full!”);}else

st.S[++st.top] = x;}Data Pop(Stack &st){

if (IsEmpty(st))printf(“\nStack empty!”);

elsereturn (st.S[st.top--]);

}

89

Page 90: Giao trinh ky thuat lap trinh 2

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

void InitStack(Stack &st){

st.top = -1;}int IsEmpty(Stack st){

if (st.top == -1) return TRUE;

elsereturn FALSE;

}int IsFull(Stack st){

if (st.top >= MAX)return TRUE;

elsereturn FALSE;

}Data Top(Stack st){

Data d;if (IsEmpty(st))

printf(“\n Stack empty!”);else

d = st.S[st.top];return d;

}

2.1.2 Các ứng dụng stack

Thông thường cấu trúc dữ liệu stack thích hợp cho việc lưu trữ dữ liệu mà trình tự

truy xuất ngược với trình tự lưu trữ. Có nghĩa là dữ liệu lưu trữ trước sẽ được lấy ra sau

những dữ liệu lưu sau (LIFO). Do đó stack có một số ứng dụng như sau:

Trong trình biên dịch, stack dùng để lưu trữ các lời gọi hàm, ví dụ như

hàm A gọi hàm B, hàm B lại gọi hàm C, khi hàm C thực hiện xong thì sự điều khiển

chương trình sẽ trở về thực hiện chương trình B. Rồi khi hàm B thực hiện xong thì sự

điều khiển chương trình sẽ được trả về cho A. Khi đó ta thấy là hàm C được gọi sau

cùng và chúng được thực hiện xong trước tiên rồi mới đến B và A. Đây là dạng thực

hiện sau và kết thúc trước.

90

Page 91: Giao trinh ky thuat lap trinh 2

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

Haøm A Haøm B Haøm C

Hình 5.4: Minh họa xử lý gọi hàm trong trình biên dịch

Lưu trữ dữ liệu để giải các bài toán lý thuyết đồ thị. Ví dụ như tìm chu

trình Euler, tìm đường đi...

Ngoài ra, stack còn được sử dụng để khử một số bài toán đệ quy.

Còn rất nhiều ứng dụng của Stack...

2.1.3 Các ví dụ minh họa

• Chương trình xuất chuỗi ký tự theo thứ tự ngược lại

Sử dụng stack chứa dữ liệu là các ký tự, khi đó ta sẽ push lần lượt các ký tự của chuỗi

vào stack và sau đó lấy (Pop) lần lượt các ký tự này ra. Kết quả là chuỗi được xuất dưới

dạng đảo ngược lại.

#include "stdio.h"#include "conio.h"#include "string.h"#define TRUE 1#define FALSE 0

#define MAX 100typedef char Data;typedef struct{

int top;Data S[MAX];

} Stack;

void Push(Stack & st, Data x);Data Pop(Stack &st);void InitStack(Stack &st);int IsEmpty(Stack st);int IsFull(Stack st);Data Top(Stack st);void Push(Stack & st, Data x){

if (IsFull(st))printf("\nStack is full!");

91

Page 92: Giao trinh ky thuat lap trinh 2

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

elsest.S[++st.top] = x;

}Data Pop(Stack &st){

if (IsEmpty(st))printf("\nStack is empty!");

elsereturn (st.S[st.top--]);

}void InitStack(Stack &st){

st.top = -1;}int IsEmpty(Stack st){

if (st.top == -1)return TRUE;

elsereturn FALSE;

}int IsFull(Stack st){

if (st.top >= MAX)return TRUE;

elsereturn FALSE;

}Data Top(Stack st){

Data d;if (IsEmpty(st))

printf("\n Stack is empty!");else

d = st.S[st.top];return d;

}void main(){ char s[MAX];

Stack st;clrscr();

92

Page 93: Giao trinh ky thuat lap trinh 2

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

printf("Nhap chuoi: ");gets(s);InitStack(st);for(int i=0; i < strlen(s); i++)

Push(st, s[i]);printf("\n Chuoi nguoc lai: ");while (!IsEmpty(st))

printf("%c", Pop(st));getch();

}

• Chương trình đổi số sang hệ cơ số bất kỳ .

#include "stdio.h"#include "conio.h"#include "string.h"#define MAX 100#define TRUE 1#define FALSE 0

typedef unsigned int Data;typedef struct{

int top;Data S[MAX];

} Stack;void Push(Stack & st, Data x);Data Pop(Stack &st);void InitStack(Stack &st);int IsEmpty(Stack st);int IsFull(Stack st);Data Top(Stack st);void Push(Stack & st, Data x){

if (IsFull(st))printf("\nStack is full!");

elsest.S[++st.top] = x;

}Data Pop(Stack &st){

if (IsEmpty(st))printf("\nStack is empty!");

else

93

Page 94: Giao trinh ky thuat lap trinh 2

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

return (st.S[st.top--]);}void InitStack(Stack &st){

st.top = -1;}int IsEmpty(Stack st){

if (st.top == -1)return TRUE;

elsereturn FALSE;

}int IsFull(Stack st){

if (st.top >= MAX)return TRUE;

elsereturn FALSE;

}Data Top(stack st){

Data d;if (IsEmpty(st))

printf("\n Stack is empty!");else

d = st.S[st.top];return d;

}void main(){

char s[MAX];int coso, so, du;stack st;clrscr();printf("Nhap co so can doi: ");scanf("%d", &coso);printf("Nhap so:");scanf("%d",&so);InitStack(st);while (so != 0)

{du = so % coso;

94

Page 95: Giao trinh ky thuat lap trinh 2

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

Push(st, du); so = so/coso;

}printf("\n Co so : ");while (!IsEmpty(st))printf("%3X", Pop(st));getch();

}

• Bài toán tháp Hanoi

Bài toán tháp Hanoi thường được giải bằng phương pháp đệ quy, trong chương trước

đã trình bày. Tuy nhiên có thể giải bằng cách dùng stack để khử đệ quy. Để thực hiện

việc lưu trữ tạm trong quá trình di chuyển chúng ta dùng một stack.

Trong đó mỗi phần tử của stack này chứa các thông tin gồm: số đĩa di chuyển (N), cột

nguồn bắt đầu di chuyển (Nguon) và cột đích là nơi cần di chuyển đến (Dich). Ở đây

không cần lưu cột trung gian vì có 3 cột đánh số là 1, 2 và 3 thì cột trung gian để di

chuyển là: 6 – (Nguon+Dich).

Đầu tiên đưa vào stack thông tin di chuyển {n, 1, 2}, tức là di chuyển n đĩa từ cột 1

sang cột thứ 2 qua cột trung gian là 6-(1+2) = 3.

Tại mỗi bước khi lấy trong stack ra một phần tử, thực hiện như sau:

Nếu N = 1: ⇒ di chuyển đĩa từ cột Nguon ⇒ cột Dich

Ngược lại (nếu N > 1):

• Xác định cột trung gian TrungGian = 6 – (Nguon+Dich)

• Push ⇒ stack thông tin di chuyển {N-1, TrungGian, Dich}

• Push ⇒ stack thông tin di chuyển {1, Nguon, Dich}

• Push ⇒ stack thông tin di chuyển {N-1, Nguon, TrungGian}

Quá trình còn thực hiện khi stack khác rỗng.

Nhận xét: lưu ý thứ tự khi đưa vào thông tin di chuyển vào stack. Trong phần trên thông

tin {N-1, Nguon, TrungGian} được đưa vào stack sau cùng nên chúng sẽ được lấy ra

trước tiên, kế đến là thông tin di chuyển {1, Nguon, Dich} và cuối cùng là thông tin di

chuyển {N-1, TrungGian, Dich}.

Chương trình minh họa:

#include "stdio.h"#include "conio.h"

95

Page 96: Giao trinh ky thuat lap trinh 2

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

#define MAX 100#define TRUE 1#define FALSE 0typedef struct{

int N; // số đĩa cần di chuyểnint Nguon; // cột bắt đầu di chuyểnint Dich; // cột đích đến

} Data;

typedef struct{int top;Data S[MAX];

} stack;

void Push(stack & st, Data x);Data Pop(stack &st);void InitStack(stack &st);int IsEmpty(stack st);int IsFull(stack st);Data Top(stack st);

void Push(stack & st, Data x){

if (IsFull(st))printf("\nStack full!");

elsest.S[++st.top] = x;

}Data Pop(stack &st){

if (IsEmpty(st))printf("\nStack empty!");

elsereturn (st.S[st.top--]);

}void InitStack(stack &st){

st.top = -1;}int IsEmpty(stack st){

96

Page 97: Giao trinh ky thuat lap trinh 2

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

if (st.top == -1)return TRUE;

elsereturn FALSE;

}int IsFull(stack st){

if (st.top >= MAX)return TRUE;

elsereturn FALSE;

}Data Top(stack st){

Data d;if (IsEmpty(st))

printf("\n Stack is empty!");else

d = st.S[st.top];return d;

}void Hanoi(int n){

stack st; // stack chứa thông tin di chuyểnInitStack(st); // khởi tạo stack

Data d,d1,d2,d3; // các biến tạm chứa thông tin di chuyển.int TrungGian; // cột trung gian

// đưa thông tin ban đầu vào stack {n, 1, 2} : di chuyển n đĩa từ cột 1 đến 2d.N = n;

d.Nguon = 1; d.Dich = 2;

Push(st, d); // đưa vào stack

while (!IsEmpty(st)) // lặp khi stack còn phần tử {

d = Pop(st); // lấy một phần tử đầu stack ra thực hiện if (d.N == 1) // nếu chỉ có một đĩa thì di chuyển Nguồn → Đích

printf("\nDi chuyen: %d -> %d", d.Nguon, d.Dich); else // nhiều hơn một đĩa {

97

Page 98: Giao trinh ky thuat lap trinh 2

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

TrungGian = 6 - (d.Nguon+d.Dich); // lấy cột Trung gian/* Đưa vào stack theo thứ tự ngược lại. Tức là đưa vào thông tin: di chuyển N-1 đĩa từ cọc trung gian đến cọc đích, di chuyển 1 đĩa từ nguồn sang đích, và cuối cùng đưa N-1 đĩa từ cọc nguồn sang trung gian. Khi đó lấy từ stack ra chúng ta sẽ có thứ tự ngược lại là: di chuyển N-1 đĩa từ cọc nguồn sang trung gian, di chuyển 1 đĩa từ cọc nguồn sang đích và cuối cùng di chuyển N-1 đĩa từ cọc trung gian sang đích. Đây chính là thứ tự mà chúng ta cần thực hiện chuyển N đĩa từ cọc nguồn sang đích. */

// đưa thông tin di chuyển N-1 đĩa từ Trung gian → Đích d1.N = d.N-1; d1.Nguon = TrungGian; d1.Dich = d.Dich; Push(st,d1);

// đưa thông tin di chuyển 1 đĩa từ Nguồn → Đích d2.N = 1;

d2.Nguon = d.Nguon; d2.Dich = d.Dich; Push(st, d2);

// đưa thông tin di chuyển N-1 đĩa từ Nguồn → Trung gian d3.N = d.N -1; d3.Nguon = d.Nguon; d3.Dich = TrungGian; Push(st,d3);

} } // end while stack <> ∅} // end Hanoi

int main(){

int n; clrscr(); printf("Nhap vao so cot: "); scanf("%d",&n); Hanoi(n); getch(); return 0;}

• Thuật toán QuickSort sử dụng stack

98

Page 99: Giao trinh ky thuat lap trinh 2

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

Như chúng ta đã biết thì thuật toán QuickSort thường được cài đặt bằng giải thuật đệ

quy. Tuy nhiên một cách khác là dùng cấu trúc dữ liệu stack để cài đặt cũng tốt. Cách

thực hiện là tạo một cấu trúc stack để lưu trữ hai biên trái và phải của một đoạn.

Ví dụ như khi phân đoạn left đến right, chúng ta được ba đoạn là [left...i] là các phần

tử nhỏ hơn x, đoạn [i+1..j-1] là các phần tử bằng x, và đoạn cuối là [j...right]. Khi đó

chúng ta sẽ đưa vào stack đoạn bên phải, nếu đoạn bên trái nhiều hơn một phần tử ta

cập nhật lại right = i, khi đó chúng ta sẽ lặp lại với đoạn [left...i] một cách tương tự, khi

đoạn bên trái hết thì chúng ta sẽ lấy từ trong stack ra những đoạn được lưu giữ để thực

hiện tiếp tục...quá trình thực hiện cho đến khi stack rỗng.

#include "stdio.h"#include "conio.h"#include "stdlib.h"#define TRUE 1#define FALSE 0

#define MAX 100typedef struct{

int left;int right;

} Data;

typedef struct{int top;Data S[MAX];

} stack;

void Push(stack & st, Data x);Data Pop(stack &st);void InitStack(stack &st);int IsEmpty(stack st);int IsFull(stack st);Data Top(stack st);void Swap(int &a, int &b){ int tmp =a; a = b; b = tmp;}

99

Page 100: Giao trinh ky thuat lap trinh 2

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

void Push(stack & st, Data x){

if (IsFull(st))printf("\nStack is full!");

elsest.S[++st.top] = x;

}Data Pop(stack &st){

if (IsEmpty(st))printf("\nStack is empty!");

elsereturn (st.S[st.top--]);

}void InitStack(stack &st){

st.top = -1;}int IsEmpty(stack st){

if (st.top == -1)return TRUE;

elsereturn FALSE;

}int IsFull(stack st){

if (st.top >= MAX)return TRUE;

elsereturn FALSE;

}Data Top(stack st){

Data d;if (IsEmpty(st))

printf("\n Stack is empty!");else

d = st.S[st.top];return d;

}void QuickSort(int a[MAX], int n)

100

Page 101: Giao trinh ky thuat lap trinh 2

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

{int i, j, l, r;int x;

l = 0;r = n-1;stack st;InitStack(st);

while (l < r ){

x = a[(l+r)/2];i = l;j = r;do{

while (a[i] < x) i++;while (a[j] > x) j--;if ( i <= j){

Swap(a[i], a[j]);i++;j--;

}} while (i<j);

if (r > i){

Data d;d.left = i;d.right = r;Push(st,d);

}if (l < j){

r = j;}else if (!IsEmpty(st)){

Data d = Pop(st);l = d.left;r = d.right;

}else

101

Page 102: Giao trinh ky thuat lap trinh 2

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

break; // thoat khoi vong lap -> while (l < r)}

}void main(){ int a[MAX] ; int n = 30; clrscr(); randomize(); printf("Mang truoc khi sap: \n"); for(int i=0; i < n;i++) {

a[i] = random(100);printf("%4d", a[i]);

} QuickSort(a,n); printf("\nMang sau khi sap:\n"); for(i=0; i <n; i++)

printf("%4d",a[i]); getch();}

• Bài toán chuyển biểu thức trung tố sang hậu tố.

Như chúng ta đã biết một biểu thức được biểu diễn dưới dạng hậu tố hay còn gọi là

ký pháp nghịch đảo Ba Lan RPN (Reverse Polish Notation) giúp ích rất nhiều cho việc

tính toán giá trị biểu thức tốt hơn là biểu thức dạng trung tố. Các trình biên dịch máy tính

thường chuyển những biểu thức trung tố sang hậu tố để dễ xử lý hơn. Trong phần này

chúng ta sẽ tìm hiểu thuật toán để chuyển một biểu thức dạng trung tố đơn giản sang biểu

thức hậu tố tương ứng.

Ví dụ một biểu thức trung tố như sau: (6 / 2 + 3) * (7 - 4) được chuyển thành biểu

thức hậu tố như sau: 6 2 / 3 + 7 4 - *. Chúng ta có thể thấy cách biểu diễn của trung tố

cần phải xử lý dấu ngoặc trong khi biểu thức hậu tố thì không cần sử dụng. Đây là ưu

điểm của biểu thức hậu tố.

Để chuyển đổi chúng ta sử dụng một Stack dùng lưu trữ các toán tử và dấu ngoặc mở.

Ngoài ra cũng quy định độ ưu tiên của các toán tử thông qua hàm Priority, trong đó phép

toán *, / có độ ưu tiên cao nhất là 2, phép toán +, - có độ ưu tiên ít hơn là 1, và cuối cùng

dấu ngoặc mở ‘(’ là 0.

Ý tưởng của thuật toán:

102

Page 103: Giao trinh ky thuat lap trinh 2

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

Duyệt qua từng phần tử trong biểu thức trung tố, gọi C là phần tử đang xét:

Nếu C là ‘(’ thì push vào stack

Nếu C là ‘)’ thì lấy trong stack ra cho đến khi gặp ‘(‘: xuất ra những phần tử

này.

Nếu C là toán tử ‘+’,’-‘,’*’,’/’: Lấy trong stack ra tất cả những toán tử có độ

ưu tiên cao hơn C, xuất những phần tử này ra ngoài. Sau đó đưa C vào stack.

Cuối cùng lấy tất cả những phần tử còn lại trong stack xuất ra ngoài.

Thuật toán chuyển đổi từ trung tố sang hậu tố:

Stack = {∅}; // khởi tạo stack rỗngfor < phần tử C được đọc từ biểu thức trung tố> do{

if ( C == ‘(‘ ) Push(Stack, C); // đưa vào stack

else if ( C== ‘)’){

do{

x = Pop(Stack); // lấy từ stack ra cho đến khi gặp ‘(‘if (c != ‘(‘)

printf(“%c”, x); } while ( x!= ‘(‘)

}else if (C == ‘+’ || C ==’-‘ || C == ‘*’ || C==’/’){

while ( !IsEmpty(Stack) && Priority(C) <= Priority(Top(Stack))) printf(“%c”, Pop(Stack)); // nếu độ ưu tiên toán tử trong

// stack lớn hơn thì lấy ra.

Push(Stack, C); // đưa toán tử mới vào stack}else // toán hạng

printf(“%c”,C);}while ( !IsEmpty(Stack)) // lặp để lấy tất cả các phần tử trong stack ra

printf(“%c”, Pop(Stack));

• Ví dụ khi thực hiện với biểu thức trung tố : (2 * 3 + 7 / 8) * (5 – 1)

Đọc Xử lý Stack Output

103

Page 104: Giao trinh ky thuat lap trinh 2

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

( Đẩy vào stack (2 Xuất ( 2

*Do ‘*’ ưu tiên hơn ‘(‘ ở đỉnh stack nên đưa ‘*’

vào stack( * 2

3 Xuất ( * 2 3

+

Do ‘+’ ưu tiên thấp hơn ‘*’ ở đỉnh stack nên ta

lấy ‘*’ ra.

Tiếp tục so sánh ‘+’ với ‘(‘ thì ‘+’ ưu tiên cao hơn

nên đưa vào stack

( + 2 3 *

7 Xuất ( + 2 3 * 7

/Do ‘/’ có độ ưu tiên cao hơn ‘+’ trên đỉnh stack

nên đưa ‘/’ vào stack.( + / 2 3 * 7

8 Xuất ( + / 2 3 * 7 8) Lấy trong stack ra cho đến khi gặp ngoặc (. 2 3 * 7 8 / +* Đưa vào stack * 2 3 * 7 8 / +( Đưa vào stack * ( 2 3 * 7 8 / +5 Xuất * ( 2 3 * 7 8 / + 5

-Độ ưu tiên của ‘-‘ cao hơn ‘(‘ trong đỉnh stack

nên đưa ‘-‘ vào stack* ( - 2 3 * 7 8 / + 5

1 Xuất * ( - 2 3 * 7 8 / + 5 1) Lấy trong stack ra cho đến khi gặp ngoặc đóng * 2 3 * 7 8 / + 5 1 -

Lấy những phần tử còn lại trong stack và hiển

thị

2 3 * 7 8 / + 5 1 -

*

• Bài toán tính giá trị biểu thức hậu tố .

Trong những năm đầu 1950 nhà logic học người Ba Lan Jan Lukasiewicz đã chứng

minh rằng: biểu thức hậu tố không cần có dấu ngoặc vẫn có thể tính đúng bằng cách đọc

lần lượt biểu thức từ trái qua phải và dùng một stack để lưu trữ kết quả trung gian.

Các bước thực hiện như sau:

Bước 1: Khởi tạo một stack = {∅}

Bước 2: Đọc lần lượt các phần tử của biểu thức hậu tố từ trái sang phải, với mỗi phần tử

đó thực hiện kiểm tra:

Nếu phần tử này là toán hạng thì đẩy nó vào stack

Nếu phần tử này là toán tử thì ta lấy hai toán hạng trong stack ra và thực hiện

phép toán với hai toán hạng đó. Kết quả tính được sẽ được lưu vào trong

stack.

104

Page 105: Giao trinh ky thuat lap trinh 2

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

Bước 3: Sau khi kết thúc bước hai thì toàn bộ biểu thức đã được đọc xong, trong stack

chỉ còn duy nhất một phần tử, phần tử đó chính là giá trị của biểu thức.

Ví dụ: tính giá trị biểu thức 10 2 / 3 + 7 4 - *, có biểu thức trung tố là: (10/2 + 3)*(7-4),

ta có các bước thực hiện như sau:

Đọc Xử lý Stack Output10 Đưa vào stack 102 Đưa vào stack 10 2

/

Lấy hai phần tử đầu stack là 2, 10 và thực hiện

phép toán 10/2 = 5. Sau đó lưu kết quả 5 vào

stack

5

3 Đưa 3 vào stack 5 3

+

Lấy hai giá trị 3, 5 ra khỏi stack, thực hiện phép

cộng của hai số đó, kết quả là 8 được đưa vào

lại stack

8

7 Đưa 7 vào stack 8 74 Đưa 4 vào stack 8 7 4

-Lấy hai giá trị 4, 7 ra khỏi stack, thực hiện phép

tính 7 – 4 = 3, kết quả 3 được đưa vào lại stack8 3

*Lấy hai giá trị 3, 8 ra khỏi stack, thực hiện phép

tính 8 * 3 = 24, lưu kết quả vào stack24

Lấy kết quả từ stack ⇒ đây chính là kết quả của

biểu thức.24

105

Page 106: Giao trinh ky thuat lap trinh 2

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

2.2 Giới thiệu Queue – hàng đợi

Hàng đợi Queue là kiểu danh sách với hai phép toán cơ bản là bổ sung một phần tử

vào cuối danh sách (Rear) và loại bỏ một phần tử ở đầu danh sách (Front). Nói tóm lại là

hai thao tác cơ bản đưa vào một phần tử và lấy ra một phần tử ở hai đầu của danh sách.

Danh sách hàng đợi làm việc theo cơ chế FIFO (First In First Out), nghĩa là việc

thêm một phần tử vào hàng đợi hay lấy một phần tử từ hàng đợi ra ngoài theo thứ tự

“Vào trước ra trước”.

Hình 5.5: Minh họa hàng đợi Queue.

• Hai thao tác chính trên hàng đợi :

1. Thao tác Insert : thêm một phần tử vào cuối danh sách.

2. Thao tác Remove : xoá một phần tử ở đầu danh sách.

• Ví dụ : Cho hàng đợi Q và các thao tác như sau:

1. Hàng đợi ban đầu có hai phần tử A và B hình 5.6

2. Thêm một phần tử C vào cuối Queue: Insert(Q, C) hình 5.7

3. Lấy một phần tử ở đầu Queue: Remove(Q) hình 5.8

4. Thêm một phần tử D vào cuối Queue: Insert(Q, D) hình 5.9

5. Lấy một phần tử ở đầu Queue: Remove(Q) hình 5.10

106

Lấy ra (Remove)

Thêm vào (Insert)

Đầu Queue (Front) Cuối Queue (Rear)

Queue

Page 107: Giao trinh ky thuat lap trinh 2

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

Hình 5.6: Hàng đợi có hai phần tử A và B.

Hình 5.7: Hàng đợi sau khi thêm phần tử C vào cuối.

Hình 5.8: Hàng đợi sau khi lấy phần tử ở đầu.

Hình 5.9: Hàng đợi sau khi thêm phần tử D vào cuối.

107

A B

Front Rear

A B C

RearFront

B C

RearFront

B C D

RearFront

C D

RearFront

Page 108: Giao trinh ky thuat lap trinh 2

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

Hình 5.10: Hàng đợi sau khi lấy phần tử ở đầu.

2.2.1 Cài đặt Queue dùng CTDL mảng

Để sử dụng cấu trúc dữ liệu mảng mô tả Queue người ta dùng hai chỉ số là Front và

Rear để đánh dấu vị trí đầu và cuối của hàng đợi trong mảng. Nếu mảng tính từ chỉ số 0

thì ban đầu chúng ta khởi tạo Rear = -1 và Front = 0.

• Để thêm một phần tử vào Queue, ta tăng Rear lên 1 và đưa nó vào phần tử thứ

Rear.

• Để loại một phần tử khỏi Queue, ta lấy giá trị ở vị trí Front và tăng chỉ số này

lên 1.

• Khi Rear tăng lên hết khoảng chỉ số của mảng thì mảng đầy, khi đó ta không thể

thêm vào mảng được nữa.

• Khi Front > Rear ⇒ Queue = {∅}.

Tương tự như phần Stack ở đây sẽ xây dựng một cấu trúc Queue chứa các phần tử có

kiểu dữ liệu là Data, Data là kiểu dữ liệu do người dùng định nghĩa có thể số nguyên,

thực, ký tự hoặc một cấu trúc dữ liệu nào đó tùy ý.

Khai báo cấu trúc Queue dùng mảng:

#define MAX 100typedef struct{

int Front; // chỉ đến vị trí đầu của queueint Rear; // chỉ đến vị trí cuối của queueData Q[MAX]; // mảng dữ liệu cần lưu trữ

} Queue;Trong cấu trúc này trường Front chỉ đến vị trí đầu của Queue còn Rear chỉ đến vị trí

cuối của Queue. Ban đầu khi Queue được khởi tạo rỗng thì Rear = -1 và Front = 0.

Các thao tác trên cấu trúc Queue:

void Insert(Queue &queue, Data x): Đưa phần tử x vào cuối Queue

Data Remove(Queue &queue): Lấy một phần tử ở đầu Queue

int IsEmpty(Queue queue): Kiểm tra xem Queue có rỗng hay không

int IsFull(Queue queue): Kiểm tra xem Queue đầy hay không.

void InitQueue(Queue &queue): Khởi tạo Queue.

Phần cài đặt các thao tác trên Queue:

108

Page 109: Giao trinh ky thuat lap trinh 2

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

#define TRUE 1#define FALSE 0void Insert(Queue &queue, Data x){

queue.Rear++; // tăng Rear lên 1if (IsFull(queue))

printf(“Queue full!”);else

queue.Q[queue.Rear] = x;}Data Remove(Queue &queue){

Data x;if (IsEmpty(queue))

printf(“Queue empty!”);else

x = queue.Q[queue.Front++];return x;

}int IsEmpty(Queue queue){

if (queue.Front > queue.Rear)return TRUE;

return FALSE;}int IsFull(Queue queue){

if (queue.Rear == MAX)return TRUE;

return FALSE;}void InitQueue(Queue &queue){

queue.Rear = -1;queue.Front = 0;

}

2.2.2 Các ứng dụng Queue

Thông thường các vấn đề liên quan đến cơ chế “vào trước ra trước” đều có thể dùng

cấu trúc dữ liệu Queue. Ví dụ như cơ chế sản xuất và tiêu thụ, hàng hóa sản xuất trước

được đưa vào kho và sẽ được xuất ra trước. Các ứng dụng đặt vé tàu lửa máy bay, hệ

109

Page 110: Giao trinh ky thuat lap trinh 2

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

thống rút tiền...Ngoài ra hàng đợi còn được dùng nhiều trong hệ điều hành: bộ đệm ứng

dụng, hàng đợi xử lý các sự kiện, hàng đợi xử lý phím nhấn, tiến trình...

• Bài toán quản lý kho hàng :

Đây là dạng bài toán sản xuất & tiêu dùng, mặt hàng được sản xuất ra sẽ được lưu

vào kho, hàng hóa từ kho này sẽ được xuất ra ngoài cho nhà phân phối. Khi đó những

mặt hàng nào đưa vào kho trước tiên sẽ ra được xuất kho trước. Đây là dạng FIFO nên

chúng ta có thể dùng cấu trúc dữ liệu Queue để minh họa cho nó.

#include "stdio.h"#include "conio.h"

#define MAX 100#define TRUE 1#define FALSE 0

typedef struct{ char MaSP[10]; // Ma san pham char TenSP[50]; // Ten san pham} Data;

typedef struct{ int Rear, Front; Data Q[MAX];}Queue;

void Insert(Queue &queue, Data x);Data Remove(Queue &queue);int IsEmpty(Queue queue);int IsFull(Queue queue);void InitQueue(Queue &queue);

void Insert(Queue &queue, Data x){

queue.Rear++; // tăng Rear lên 1if (IsFull(queue))

printf("Queue full!");else

queue.Q[queue.Rear] = x;}

110

Page 111: Giao trinh ky thuat lap trinh 2

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

Data Remove(Queue &queue){

Data x;if (IsEmpty(queue))

printf("Queue empty!");else

x = queue.Q[queue.Front++];return x;

}int IsEmpty(Queue queue){

if (queue.Front > queue.Rear)return TRUE;

return FALSE;}int IsFull(Queue queue){

if (queue.Rear == MAX)return TRUE;

return FALSE;}void InitQueue(Queue &queue){

queue.Rear = -1;queue.Front = 0;

}Data InputProduct() // đọc một sản phẩm{

Data sp;printf("\nMa san pham: "); scanf("%s",sp.MaSP);printf("Ten san pham: "); scanf("%s", sp.TenSP);return sp;

}void OutputProduct(Data sp) // xuất một sản phẩm{ printf("\nSan pham : MaSP: %s, TenSP: %s", sp.MaSP, sp.TenSP);}void ListProducts(Queue q) // liệt kê những sản phẩm trong kho{

if (!IsEmpty(q)) // nếu có sản phẩm trong kho{

printf("\n Danh sach san pham trong kho:\n");

111

Page 112: Giao trinh ky thuat lap trinh 2

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

for(int i= q.Front; i <= q.Rear; i++) // duyệt qua các phần tử{ printf("\nSan pham: MaSP: %s, TenSP: %s",q.Q[i].MaSP,

q.Q[i].TenSP);}

}}int main(){

Queue queue;InitQueue(queue); // khởi tạo hàng đợiclrscr();int i;do{

clrscr(); printf("CHUONG TRINH MINH HOA QUAN LY KHO\n"); printf("-----------------------------------------------\n"); printf("1. Them san pham vao kho.\n"); printf("2. Xuat san pham ra khoi kho.\n"); printf("3. Xem san pham chuan bi xuat kho.\n"); printf("4. Xem tat ca hang hoa trong kho. \n"); printf("5. Thoat.\n"); printf("Chon chuc nang: (1- 5): "); scanf("%d",&i);

switch (i) {

case 1:Insert(queue, InputProduct()); // thêm sp vào khobreak;

case 2:OutputProduct(Remove(queue)); // lấy sp khỏi khobreak;

case 3:if (!IsEmpty(queue)) // xem sp sắp lấy

OutputProduct(queue.Q[queue.Front]);break;

case 4: // xem tất cả spListProducts(queue);break;

} getch();

} while (i != 5);

112

Page 113: Giao trinh ky thuat lap trinh 2

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

return 0;}

• Duyệt cây theo chiều rộng trong đồ thị :

Đây là phương pháp duyệt hay tìm kiếm phổ biến trên đồ thị. Việc duyệt một đỉnh v

sẽ cho phép chúng ta đi duyệt tiếp với những đỉnh kề của nó sao cho thỏa thứ tự ưu tiên

theo chiều rộng, tức là đỉnh nào gần với v nhất sẽ được duyệt trước.

A

B C D

E F G

Duyeät sau ñænh B, C,...…D

Hình 5.11: Minh họa thứ tự duyệt theo chiều rộng.

Khi đó tại mỗi bước xét một đỉnh v ta có một danh sách các đỉnh kề của nó, những

đỉnh này chờ được duyệt do đó ta sẽ đưa những đỉnh này vào cuối danh sách chờ được

duyệt, khi đó những đỉnh nào vào danh sách chờ trước thì sẽ được duyệt trước. Với cấu

trúc dữ liệu này thì thích hợp cho việc sử dụng Queue.

Thuật toán: Breadth First Search

void BFS(int v) // thủ tục duyệt theo chiều rộng từ một đỉnh v{

Queue QList;InitQueue(QList); // khởi tạo hàng đợiInsert(QList, v); // đưa v vào hàng đợiXet[v] = TRUE; // đánh dấu v đã duyệtwhile (IsEmpty(QList) == 0){

p = Remove(QList); // lấy đỉnh trong hàng đợiVisit(p); // thăm đỉnh p, có thể xuất đỉnh ra ...for( i=1; i <= n; i++) // duyệt qua các đỉnh

if ( A[v][i] == 1 && Xet[i] == FALSE) // nếu i là đỉnh kề v và chưa xét{

Insert(QList, i); // đưa vào hàng đợiXet[i] = TRUE; // đánh dấu i đã xét.

113

Page 114: Giao trinh ky thuat lap trinh 2

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

}}

}Mảng A[][]: chứa ma trận kề của đồ thị (G)

Mảng Xet[]: đánh dấu một đỉnh v đã được duyệt hay chưa; 1: đã duyệt, 0: chưa duyệt. Tất

cả các phần tử của mảng Xet[] ban đầu được khởi tạo là 0.

Chương trình minh họa duyệt theo chiều rộng:

#include "stdio.h"#include "conio.h"#define MAX 100#define TRUE 1#define FALSE 0

typedef int Data;typedef struct{ int Rear, Front; Data Q[MAX];} Queue;void Insert(Queue &queue, Data x);Data Remove(Queue &queue);int IsEmpty(Queue queue);int IsFull(Queue queue);void InitQueue(Queue &queue);void Insert(Queue &queue, Data x){

queue.Rear++; // tăng Rear lên 1if (IsFull(queue))

printf("Queue full!");else

queue.Q[queue.Rear] = x;}Data Remove(Queue &queue){

Data x;if (IsEmpty(queue))

printf("Queue empty!");else

x = queue.Q[queue.Front++];return x;

}

114

Page 115: Giao trinh ky thuat lap trinh 2

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

int IsEmpty(Queue queue){

if (queue.Front > queue.Rear)return TRUE;

return FALSE;}int IsFull(Queue queue){

if (queue.Rear == MAX)return TRUE;

return FALSE;}void InitQueue(Queue &queue){

queue.Rear = -1;queue.Front = 0;

}

void BFS(int v, int a[][MAX], int xet[], int n){

int d; Queue queue; InitQueue(queue); Insert(queue, v); xet[v] = TRUE; while (!IsEmpty(queue)) { d = Remove(queue); printf("%d\t", d); for(int i=1; i <= n; i++) if (a[d][i]== 1 && xet[i]== FALSE) { Insert(queue, i); xet[i] = TRUE; } }

}int main(){

int a[MAX][MAX], xet[MAX], n;clrscr();char name[50];printf(“Nhap ten file ma tran ke cua (G): ”);

115

Page 116: Giao trinh ky thuat lap trinh 2

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

gets(name);FILE * f = fopen(name,"r");if (f == NULL){ printf("Loi doc file!"); return 1;

} fscanf(f,"%d",&n); for(int i=1; i <= n;i++)

for(int j=1; j <= n; j++) fscanf(f,"%d",&a[i][j]);

// danh dau cac dinh chua xetfor(i =1; i <= n; i++)

xet[i] = FALSE;// duyet qua cac dinh cua do thiprintf("Cac dinh cua do thi duoc duyet theo chieu rong\n");

for(i =1; i <= n; i++)if (xet[i] == FALSE)

BFS(i, a, xet, n);getch();return 0;

}

Tập tin ma trận kề có dạng sau:

N

A[1][1]… A[1][N]

A[N][1]… A[N][N]

Trong đó A[i][j] = 1 nếu (i, j) có cạnh nối và ngược lại A[i][j] = 0 nếu không có cạnh nối

Ví dụ tập tin ma trận kề minh họa đồ thị có 5 đỉnh.

50 1 1 1 11 0 1 1 1

1 1 0 1 11 1 1 0 11 1 1 1 0

116

Page 117: Giao trinh ky thuat lap trinh 2

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

BÀI TẬP

1. Tinh chỉnh lại đoạn chương trình sau:

for (int i=0; i < n; i++)

{

if ( flag)

b[i] = 0;

a[i] = a[i] + b[i];

c[i] = a[i] + 2*b[i];

}

2. Tinh chỉnh lại đoạn chương trình sau:

...

while ( sqrt( i*i) < sqrt (a*a) )

{

// đoạn chương trình

….

i++;

}

3. Tinh chỉnh lại đoạn chương trình sau:

...

flag = FALSE;

k =0;

while ( k < n)

{

if (a[i] == x)

flag = TRUE;

k++;

}

117

Page 118: Giao trinh ky thuat lap trinh 2

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

if (flag)

printf("Tim thay phan tu!");

4. Tinh chỉnh lại đoạn chương trình sau:

...

while ( i < n)

{

d = sqrt (a+b)*i;

printf("gia tri %.2f", d);

i++;

}

...

5. Tinh chỉnh lại đoạn chương trình sau:

...

for(int i=0; i < strlen(strBuff); i++)

{

….

}

6. Tinh chỉnh lại đoạn chương trình sau:

if (a==0 && b==0 && c==0)

Function1();

else if (a==0 && b==0)

Function2();

else if (a==0)

Function3();

else

Function4();

7. Tối ưu lại đoạn chương trình sau:

char S1[MAX], S2[MAX];

….

int i;

118

Page 119: Giao trinh ky thuat lap trinh 2

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

for (i=0;i<strlen(S1); i++)

{

if ( i<strlen(S1)/3 && S1[i] < ’M’ )

S2[i]= S1[i]+2;

else if ( i < strlen(S1)/2 && S[i] < ‘X’)

S2[i] = S1[i] +3;

else S2[i]= S1[i];

}

8. Tinh chỉnh đoạn chương trình sau

if (strcmp(s1, s2)==0)

printf(“equal”);

else if (strcmp(s1, s2) >0)

printf(“greater than”);

else

printf(“less than”);

9. Tinh chỉnh lại đoạn chương trình sau:

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

x2 = (-b - sqrt(b*b-4*a*c))/(2*a);

10. Viết chương trình với giao diện đồ hoạ minh họa cho bài toán tháp Hanoi.

Chương trình thể hiện từng bước sự chuyển các đĩa. Cho phép nhập vào số n và

thời gian di chuyển từng đĩa.

11. Viết hàm đệ quy tìm phần tử lớn nhất và nhỏ nhất trong mảng nguyên.

12. Viết hàm đệ quy tính tổng các phần tử trong mảng các số nguyên.

13. Viết chương trình tìm kiếm giá trị x trên ma trận vuông các số nguyên, dùng đệ

quy để tìm kiếm.

14. Viết chương trình chuyển đổi cơ số từ hệ thập phân sang hệ k, yêu cầu dùng hàm

đệ quy để giải.

119

Page 120: Giao trinh ky thuat lap trinh 2

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

15. Viết chương trình tháp Hanoi không dùng đệ quy.

16. Viết chương trình chuyển đổi số từ cơ số thập phân sang cơ số k bất kỳ, không

dùng đệ quy.

17. Viết chương trình liệt kê chuỗi nhị phân n bit. Dùng phương pháp sinh.

18. Cho tập các số tự nhiên {1, 2, .., n}, viết chương trình liệt kê tập con k phần tử.

Dùng phương pháp sinh.

19. Viết chương trình liệt kê hoán vị n phần tử. Dùng phương pháp sinh.

20. Nhập vào một chuỗi ký tự S, hãy liệt kê các hoán vị ký tự của chuỗi S.

Ví dụ: chuỗi S nhập vào là "abc" thì kết quả là abc, acb, bac, bca, cab, cba.

21. Tương tự như bài 1, liệt kê tổ hợp chập k ký tự của chuỗi S.

22. Một hoán vị hoàn toàn của tập{1, 2, .., n} là dãy hoán vị mà thoả mãn x[i] ≠ i, ∀i:

1≤ i ≤ n. Viết chương trình nhập vào giá trị n, sau đó xuất ra tất cả hoán vị hoàn

toàn của tập {1, 2, .., n}.

23. Viết chương trình nhập vào một số nguyên n, liệt kê tất cả cách chia số tự nhiên n

thành tổng các số nhỏ hơn.

Ví dụ: n = 4

Kết quả 3 1

2 2

2 1 1

1 1 1 1

24. Cho một bàn cờ tổng quát có kích thước nxn và quân mã. Viết chương trình cho

phép liệt kê ra hành trình của quân mã từ một vị trí (x ,y) đi qua tất cả các ô còn

lại của bàn cờ, mỗi ô đi qua một lần.

120

Page 121: Giao trinh ky thuat lap trinh 2

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

Hình: Các nước đi có thể có của quân mã

25. Cho bàn cờ vua nxn, hãy viết chương trình minh hoạ bàn cờ và cách sắp xếp n

quân hậu sao cho các quân hậu không thể ăn lẫn nhau.

Hình: Một cách bố trí các quân Hậu.

26. Nhập danh sách tên gồm n người, liệt kê ra tất cả cách chọn k người trong số n

người đó.

Ví dụ: n =6: {"Nam", "Hung", "Viet", "Hoa", "Lan", "Tien"},

Kết quả: k = 2 {"Nam", "Hung"}, {"Nam", "Viet"}, {"Nam", "Hoa"}….

121

Page 122: Giao trinh ky thuat lap trinh 2

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

27. Nhập danh sách tên gồm n người, xuất ra các cách xếp n người đó vào trong một

bàn tròn.

28. Nhập vào danh sách n bạn nam và n bạn nữ, hãy xuất ra danh sách cách xếp 2n

bạn đó vào một bàn tròn trong đó có sự xen lẫn giữa nam và nữ.

29. Viết chương trình liệt kê tất cả hoán vị của từ “HUTECH”

30. Viết chương trình liệt kê tất cả hoán vị chữ cái trong từ "MISSISSIPPI"

31. Viết chương trình mô phỏng từng bước thực hiện của các thuật toán sắp xếp sau:

a. Phương pháp chọn

b. Phương pháp nổi bọt

c. Phương pháp chèn

d. Phương pháp đổi chỗ trực tiếp

e. Phương pháp ShellSort

f. Phương pháp phân đoạn

g. Phương pháp cơ số

32. Cho một mảng nguyên n >100 phần tử, các phần tử phát sinh ngẫu nhiên. Viết

hàm tìm kiếm một phần tử trong mảng.

33. Tương tự như bài tập bên trên, nhưng hàm tìm kiếm theo dạng nhị phân. Sinh

viên có thể dùng một trong các phương pháp sắp xếp để sắp xếp lại mảng trước

khi thực hiện tìm kiếm nhị phân.

34. Viết chương trình đo thời gian thực hiện của các thuật toán sắp xếp bên trên.

Chương trình phát sinh ngẫu nhiên bộ dữ liệu test (mảng số nguyên, có kích thước

n >= 1000) , cho các thuật toán lần lượt chạy và ghi nhận lại thời gian thực hiện.

35. Viết chương trình không đệ quy cho thuật giải Quicksort, (áp dụng stack để khử

đệ quy).

36. Nhập vào một biểu thức trung tố, chuyển đổi thành biểu thực hậu tố và tính giá trị

của biểu thức. Lưu ý: toán hạng có thể nhiều hơn một con số hay số thực. Sinh

viên mở rộng với các toán tử khác...

Ví dụ: (20+5)*3+(10/5) => 20 5 + 3 * 10 5 / +

Kết quả: 77

37. Viết chương trình mô phỏng hàng đợi mua vé xem phim. Thông tin của việc đăng

ký gồm: họ tên, địa chỉ, tuổi và số ghế...

122

Page 123: Giao trinh ky thuat lap trinh 2

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

38. Viết chương trình mô phỏng hàng đợi mua vé xe lửa.

39. Viết chương trình sắp xếp theo cơ số (RadixSort), trong đó sử dụng cấu trúc dữ

liệu queue để lưu tạm trong quá trình sắp xếp. Hướng dẫn sử dụng 10 hàng đợi để

lưu tạm các con số. Gồm hàng đợi 0 đến 9, khi đó hàng đợi 0 sẽ chỉ lưu những

con số có số 0 ở các bước phân hàng đơn vị, hàng chục, hàng trăm tương ứng...

123

Page 124: Giao trinh ky thuat lap trinh 2

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

TÀI LIỆU THAM KHẢO

1. Brian W. Kernighan, Rob Pike, The Practice of Programming, Addison

Wesley, 1999.

2. Ellis Horowitz, Sartaj Sahni, Fundamentals of Data Structures, ebook, 1981.

3. R. Neapolitan, K. Naimipour , Foundations of Algorithms Using C++

Pseudocode, Jones and Bartlett Publishers , 2004.

4. Lê Hoài Bắc, Nguyễn Thanh Nghị, Kỹ năng lập trình, NXB KHKT, 2005.

5. Trần Hoàng Thọ, Giáo trình Kỹ thuật Lập trình Nâng cao, ĐH Đà Lạt, 2002.

6. Dương Anh Đức, Trần Hạnh Nhi, Nhập môn Cấu trúc dữ liệu và thuật toán,

ĐH KHTN, 2000.

7. Lê Hữu Lập, Nguyễn Duy Phương, Giáo trình kỹ thuật lập trình, NXB Bưu

Điện, 2002.

8. Lê Minh Hoàng, Giải thuật và lập trình, NXB ĐH Sư Phạm HN, 1999- 2002

124