1. Trang chủ
  2. » Giáo án - Bài giảng

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

124 410 4

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Định dạng
Số trang 124
Dung lượng 1,12 MB

Các công cụ chuyển đổi và chỉnh sửa cho tài liệu này

Nội dung

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

Trang 1

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

Trang 2

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

 

Trang 3

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ả

Trang 4

Chương 1 Một số kỹ thuật – phong cách lập trình tốt

 

Một chương trình nguồn được xem là tốt không chỉ được đánh giá thông qua thuật giải đúng và cấu trúc dữ liệu thích hợp Mà còn phụ thuộc vào phong cách và kỹ thuật mã hoá (coding) của người viết chương trình

Nếu một người lập trình viết một chương trình tuy thực hiện đúng yêu cầu đặt ra nhưng mã nguồn quá lộn xộn và phong cách lập trình cẩu thả, thì mã nguồn này sẽ gây khó khăn cho chính người lập trình!

Đôi khi người mới lập trình không quan tâm đến vấn đề này do ban đầu chỉ làm việc với chương trình nhỏ Tuy nhiên, vấn đề phát sinh khi họ phải làm việc với dự án lớn

và chương trình lúc này không còn đơn giản vài chục dòng lệnh nữa Nếu không rèn luyện một phong cách và trang bị một số kỹ thuật lập trình tốt thì người lập trình đối mặt với nhiều khó khăn…

Trong chương đầu tiên xin giới thiệu một số kỹ thuật và phong cách lập trình cơ bản, ít nhiều giúp cho người học viết chương trình được tốt hơn

0.1 Cách đặt tên cho biến hàm

Thông thường tùy theo ngôn ngữ và môi trường lập trình, người viết chương trình thường chọn cho mình một phong cách nhất quán trong việc đặt tên các định danh Một

số quy tắc cần quan tâm khi đặt tên như sau:

1 Tên của định danh phải thể hiện được ý nghĩa : thông thường các biến nguyên như i, j, k dùng làm biến lặp; x, y dùng làm biến lưu tọa độ…Còn những biến

lưu trữ dữ liệu khác thì nên đặt gợi nhớ: biến đếm số lần dùng “count” hay So_Luong, biến lưu trọng lượng “weight”, chiều cao “height”…Nếu đặt quá

ngắn gọn như c cho biến đếm, hay w cho khối lượng thì sau này khi nhìn vào chương trình sẽ rất khó hiểu!

2 Tên phải xác định được kiểu dữ liệu lưu trữ : phong cách lập trình tốt là khi người đọc nhìn vào một biến nào đó thì xác định ngay được kiểu dữ liệu mà

Trang 5

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

str/s C++ string string strFirstName

int nCars

d Double precision floating point double dMiles

ld long double precision floating

point

long double ldPI

sz Null terminated string char szName[NAME_LEN]

if Input file stream ifstream ifNameFile

of Output file stream ofstream ofNameFile

Trang 6

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

const int MAXLENGTH 200

Cách đặt tên cho hàm: hàm bắt đầu với ký tự đầu tiên là ký tự hoa và không có tiền tố Tuy nhiên, điều này cũng không bắt buộc tuỳ theo ngôn ngữ lập trình Nói chung

là hàm có chức năng thực hiện một chức năng nào đó, cho nên chúng thường bắt đầu bằng động từ: get, set, do…

CString GetName(); // Microsoft VC++ standard

String setName(); // Sun Java standard

0.2 Phong cách viết mã nguồn

• Sử dụng tab để canh lề chương trình : khi soạn thảo mã nguồn nên dùng tab với kích thước là 4 hay 8 để canh lề Thói quen này giúp cho chương trình được rõ ràng và dễ quản lý

for (i = 0;i < N; i++)

if (Check(i)) {

Action1();

Action2();

} else Action3();

Trang 7

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

Tham số ra: giá trị trả về

0: không phải số nguyên tố 1: là số nguyên tố

*/

….// phần thực hiện của hàm

}

Ví dụ chú thích cho biến

byte Image; // buffer ảnh

int Rows, Cols; // số dòng, số cột

Trang 8

int r, c; // dòng cột hiện hành

int PixelCount; // tổng số pixel

Tuy nhiên không phải bất cứ lệnh nào cũng chú thích, việc chú thích tràn lan ngay

cả với câu lệnh đơn giản cũng không có ý nghĩa gì Đôi khi còn làm cho chương trình khó nhìn hơn!

• Nên viết biểu thức điều kiện mang tính tự nhiên : biểu thức nên viết dưới dạng khẳng định, việc viết biểu thức dạng phủ định sẽ làm khó hiểu!

if ( !(iBlock < Block1 ) || !(iBlock >= Block2))

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

Trang 9

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

• 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ỏ

Trang 10

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

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

x[i] = x[i] + y[i];

y[i] = 0;

} else for i to 1000 do x[i] = x[i] + y[i];

Trang 11

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:

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

Trang 12

o Shift trái 1 bit: nhân 2

o Shift phải 1 bit: chia 2

Ví dụ: đoạn chương trình giải phương trình bậc hai

Trang 14

• 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

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

Chuyển thành macro swap

#define swap(a, b) {int t = a; a = b; b = t;}

Trang 15

• 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á.

Trang 17

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

1 (

0 1

n n n

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

Trang 18

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 = 2 return 2* Giaithua(1)

n = 1 return 1* Giaithua(0)

long l = Giaithua(5)

1 2 6 24

120 Giaithua(5)

Giaithua(0)

1

Trang 19

- Đệ 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>1double 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>2long 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>=6long U( int n)

Trang 20

long U( int n) {

if (n<5)

return n;

return U(n-1) + G(n-2);

} long G(int n) {

1.2 Xây dựng một chương trình đệ quy

Phương pháp đệ quy thường được áp dụng cho những bài toán phụ thuộc tham số và

có các đặc điểm sau:

1 Bài toán dễ dàng giải quyết trong một số trường hợp riêng ứng với các giá trị đặc

biệt nào đó của tham số Trường hợp này gọi là suy biến Ví dụ như khi tính giai

thừa thì giai thừa của 0 là 1

2 Trong trường hợp tổng quát, bài toán quy về cùng một dạng nhưng giá trị tham số được thay đổi Sau một số lần hữu hạn các bước biến đổi đệ quy thì bài toán trở

về trường hợp suy biến Ví dụ như n! = (n-1)! n, khi đó n giảm về 0 thì xảy ra trường hợp suy biến

Các hàm đệ quy thường có dạng tổng quát như sau:

if (Trường hợp đặc biệt, suy biến)

Trang 21

N i N

i

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)

Trong phần này chúng ta sẽ tìm hiểu một số chương trình đệ quy như sau:

 Tháp Hanoi (Tower of Hanoi) :

Cho 3 cột tháp được đặt tên là C1, C2, và C3 Có N đĩa có đường kính giảm dần và được sắp như hình vẽ Hãy dịch chuyển N đĩa đó sang cột C2, theo nguyên tắc sau: mỗi lần chỉ dịch được một đĩa, không được để một đĩa có đường kính lớn nằm trên đĩa có đường kính nhỏ Ta phân tích cách thực hiện như sau:

Trang 22

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

Trang 23

Hình 2.3: Minh họa trường hợp N = 3.

Trong trường hợp N = 3 như hình 2.3, thực hiện ba bước để đưa 3 đĩa về cọc 2: gồm B1, B2 và B3 Với B2 thì đơn giản do chuyển 1 đĩa, còn bước B1 và B3 phải di chuyển nhiều hơn 1 đĩa nên chúng sẽ bao gồm nhiều bước nhỏ trong đó B1 gồm {B1.1, B1.2, B1.3} và

C 1 C 2 C 3

3 qua cọc 2 B1 B2 B3

C 1 C 2 C 3

Trang 24

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:

Trang 25

printf(“Nhap so dia: “); scanf(“%d”, &N);

Hình 2.5 : Tìm phần tử lớn trong mảng dùng đệ quyHà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

Trang 26

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)

Trang 27

1.4 Khử đệ quy

Tại mỗi thời điểm của hàm đệ quy được đặc trưng bởi: nội dung các biến và các lệnh cần thực hiện tiếp theo Do đó tại mỗi thời điểm trong tiến trình xử lý của hàm

đệ quy cần phải lưu trữ cả các trạng thái xử lý dang dở

Ví dụ trong hàm đệ quy tính giai thừa n,

GT(3) = 3 * GT(2)

GT(2) = 2 * GT(1)

GT(1) = 1 * GT(0)

GT(0) = 1

Trang 28

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

Trang 29

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!

o Hàm tính giá trị của dãy dữ liệu mô tả bằng hồi quy:

Ví dụ 1: hàm tính giai thừa không đệ quy

long int GiaiThua( int n) {

int k = 1;

int tg = 1;

while ( k < n ) {

Trang 30

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 XA(X) và D(X): là các nhóm lệnh không đệ quyf(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:

Trang 31

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

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

Trang 32

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

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

Trang 33

} }

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

} }

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

}

Trang 34

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

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:

while ( !C(X)) {

Trang 35

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:

Trang 36

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

 

Trang 37

Chương 2 Bài toán liên quan tổ hợp

 

2.1 Phương pháp sinh

Phương pháp sinh được áp dụng để giải quyết bài toán liệt kê của lý thuyết tổ hợp Để

áp dụng được phương pháp này thì bài toán phải thoả mãn hai điều kiện sau:

o Có thể xác định được thứ tự trên tập các cấu hình tổ hợp cần liệt kê Từ đó có thể xác định được cấu hình đầu tiên và cấu hình cuối cùng trong thứ tự đó

o Xây dựng được một thuật toán cho phép từ một cấu hình chưa phải cấu hình cuối, sinh ra được cấu hình kế tiếp của nó

Phương pháp sinh có thể được mô tả tổng quát như sau:

<Xây dựng cấu hình đầu tiên>

Do

<Đưa ra cấu hình đang có>

<Từ cấu hình đang có sinh ra cấu hình kế tiếp>

While <Còn cấu hình or khác cấu hình cuối>

2.1.1 Bài toán sinh dãy nhị phân độ dài n

 Bài toán: một tập hợp hữu hạn có n phần tử có thể được biểu diễn tương đương với tập các số tự nhiên 1, 2, , n

Bài toán đặt ra là: cho một tập hợp gồm n phần tử X = {X1, X2, , Xn} hãy liệt kê tất cả các tập con của tập này

Để biểu diễn tập con Y của X ta dùng xâu nhị phân Bn = {B1, B2, , Bn}, sao cho nếu

Trang 38

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:

x 000 001 010 011 100 101 110 111Khi đó 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:

Cộng thêm 1: +1 Cộng thêm 1: +1

Kỹ thuật sinh kế tiếp từ cấu hình hiện tại có thể mô tả như sau: xét từ cuối dãy lên từ hàng đơn vị tìm số 0 đầu tiên

 Nếu tìm thấy thì thay số 0 bằng số 1 và đặt tất cả phần tử phía sau

vị trí đó bằng 0

 Nếu không tìm thấy thì toàn là dãy chứa 1, đây là cấu hình cuối cùng

Chương trình minh họa 1: chương trình C/C++ liệt kê chuỗi nhị phân n bit

int Stop; // biến toàn cục

void Next_BS(int B[MAX], int n) // Hàm phát sinh chuỗi kế tiếp

{

int i = n; // duyệt từ cuối

while (i>0 && B[i]) // lặp khi chưa tìm thấy B[i] ==0

{

B[i] = 0; // gán các bit sau là 0 i ; // giảm về trước }

if (i==0 )

Stop = 1; // cấu hình cuối nên không tìm được B[i] = 0 -> dừng

Trang 39

Result(B,n); // xuất chuỗi nhị phân hiện tại Next_BS(B,n); // chuỗi nhị phân tiếp theo.

} }

void Result(int B[MAX], int n)

{

static int count=0;

printf(“\n Xau nhi phan thu %d”, ++count);

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

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}}

Trang 40

+ 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 a i = 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à a i-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:

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

Ngày đăng: 31/10/2014, 18:00

Nguồn tham khảo

Tài liệu tham khảo Loại Chi tiết
1. Brian W. Kernighan, Rob Pike, The Practice of Programming, Addison Wesley, 1999 Sách, tạp chí
Tiêu đề: The Practice of Programming
2. Ellis Horowitz, Sartaj Sahni, Fundamentals of Data Structures, ebook, 1981 Sách, tạp chí
Tiêu đề: Fundamentals of Data Structures
3. R. Neapolitan, K. Naimipour , Foundations of Algorithms Using C++ Pseudocode, Jones and Bartlett Publishers , 2004 Sách, tạp chí
Tiêu đề: Foundations of Algorithms Using C++ "Pseudocode
4. Lê Hoài Bắc, Nguyễn Thanh Nghị, Kỹ năng lập trình, NXB KHKT, 2005 Sách, tạp chí
Tiêu đề: Kỹ năng lập trình
Nhà XB: NXB KHKT
5. Trần Hoàng Thọ, Giáo trình Kỹ thuật Lập trình Nâng cao, ĐH Đà Lạt, 2002 Sách, tạp chí
Tiêu đề: Giáo trình Kỹ thuật Lập trình Nâng cao
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 Sách, tạp chí
Tiêu đề: Nhập môn Cấu trúc dữ liệu và thuật toán
Tác giả: Dương Anh Đức, Trần Hạnh Nhi
Nhà XB: ĐH KHTN
Năm: 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 Sách, tạp chí
Tiêu đề: Giáo trình kỹ thuật lập trình
Nhà XB: NXB Bưu Điện
8. Lê Minh Hoàng, Giải thuật và lập trình, NXB ĐH Sư Phạm HN, 1999- 2002 Sách, tạp chí
Tiêu đề: Giải thuật và lập trình
Nhà XB: NXB ĐH Sư Phạm HN

HÌNH ẢNH LIÊN QUAN

Bảng 1.1: Minh họa tiền tố của cú pháp Hungary. - Giáo trình kĩ thuật lập trình
Bảng 1.1 Minh họa tiền tố của cú pháp Hungary (Trang 6)
Hình 2.1: Gọi đệ quy của hàm giai thừa. - Giáo trình kĩ thuật lập trình
Hình 2.1 Gọi đệ quy của hàm giai thừa (Trang 18)
Hình 2.3: Minh họa trường hợp N = 3. - Giáo trình kĩ thuật lập trình
Hình 2.3 Minh họa trường hợp N = 3 (Trang 23)
Hình 2.6: Tổng các phần tử trong mảng. - Giáo trình kĩ thuật lập trình
Hình 2.6 Tổng các phần tử trong mảng (Trang 26)
Hình 2.8: Hàm đệ quy tính dãy Fibonacci. - Giáo trình kĩ thuật lập trình
Hình 2.8 Hàm đệ quy tính dãy Fibonacci (Trang 28)
Hình 3.1: Liệt kê các lời giải theo thuật toán quay lui. - Giáo trình kĩ thuật lập trình
Hình 3.1 Liệt kê các lời giải theo thuật toán quay lui (Trang 46)
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. - Giáo trình kĩ thuật lập trình
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 (Trang 48)
Hình 3.3: Cây liệt kê tập con 3 phần tử với n = 5. - Giáo trình kĩ thuật lập trình
Hình 3.3 Cây liệt kê tập con 3 phần tử với n = 5 (Trang 49)
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ử: - Giáo trình kĩ thuật lập trình
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ử: (Trang 50)
Hình 3.8: Các đường chéo Trên Phải - Dưới Trái. - Giáo trình kĩ thuật lập trình
Hình 3.8 Các đường chéo Trên Phải - Dưới Trái (Trang 53)
Hình 3.9: Vị trí của quân hậu ảnh hưởng đến 2 đường chéo. - Giáo trình kĩ thuật lập trình
Hình 3.9 Vị trí của quân hậu ảnh hưởng đến 2 đường chéo (Trang 54)
Hình 3.14: Một giải pháp cho bàn cờ 8x8. - Giáo trình kĩ thuật lập trình
Hình 3.14 Một giải pháp cho bàn cờ 8x8 (Trang 62)
Hình 4.4: Minh hoạ ShellSort. - Giáo trình kĩ thuật lập trình
Hình 4.4 Minh hoạ ShellSort (Trang 78)
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: - Giáo trình kĩ thuật lập trình
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: (Trang 84)
Hình 5.1: Minh họa Stack. - Giáo trình kĩ thuật lập trình
Hình 5.1 Minh họa Stack (Trang 87)

TỪ KHÓA LIÊN QUAN

w