Giới thiệu Chương này trình bày phương pháp luận lập trình bao gồm các giai đoạn: đặc tả và phân tích vấn đề, thiết kế giải thuật, xây dựng chiến lược kiểm thử, viết chương trình, thực h
Trang 1TRƯỜNG ĐẠI HỌC BÁCH KHOA
Trang 2LỜI NÓI ĐẦU 7
Trang 34.4 Danh sách liên kết đơn Header 122 4.5 Danh sách liên kết đơn vòng Header 143
4.7 Danh sách liên kết kép vòng Header 187
Trang 6Quyển sách Cấu trúc dữ liệu và Giải thuật trình bày cấu trúc dữ liệu
tuyến tính (mảng, danh sách liên kết) và cấu trúc dữ liệu phi tuyến (cây, đồ thị) và các giải thuật của các cấu trúc dữ liệu này
Các cấu trúc dữ liệu và giải thuật được trình bày theo kiểu lập trình có
cấu trúc (structured programming) (minh họa bằng ngôn ngữ lập trình C++)
và theo kiểu lập trình hướng đối tượng (object-oriented programming)
(minh họa bằng ngôn ngữ lập trình C#) Các phần phụ lục là các chương
trình được viết theo kiểu lập trình tổng quát (generic programming)
Quyển sách này bao gồm 12 chương và 9 phụ lục
Chương 1 Phương pháp luận lập trình Giới thiệu phương pháp luận lập
trình, lập trình có cấu trúc, giải thuật và độ phức tạp của giải thuật
Chương 2 Đệ quy Trình bày định nghĩa đệ quy, các loại đệ quy, đệ quy và
vòng lặp
Chương 3 Các cấu trúc dữ liệu cơ bản Trình bày các kiểu dữ liệu cơ bản,
kiểu cấu trúc, kiểu lớp và kiểu con trỏ
Chương 4 Danh sách Trình bày mảng (array), danh sách liên kết đơn
(linked list), danh sách liên kết kép và danh sách liên kết vòng
Chương 5 Chồng và hàng đợi Trình bày chồng (stack) và hàng đợi
(queue) được cài đặt bằng mảng và danh sách liên kết, ứng dụng của chồng
và hàng đợi
Chương 6 Cây Trình bày định nghĩa cây (tree) và các tác vụ của cây, đống
(heap) và các tác vụ của đống
Chương 7 Cây nhị phân tìm kiếm Trình bày cây nhị phân tìm kiếm (BST -
Binary Search Tree) và các tác vụ
Chương 8 Cây cân bằng Trình bày cây cân bằng (AVL) và các tác vụ Chương 9 Cây B Trình bày cây B (B-tree) và các tác vụ
Chương 10 Băm Trình bày định nghĩa băm (hasing), hàm băm, giải quyết
đụng độ bằng phương pháp kết nối và phương pháp địa chỉ mở
Trang 7Chương 11 Sắp thứ tự Trình bày các giải thuật sắp thứ tự Insertion Sort,
Shell Sort, Selection Sort, Heap Sort, Bubble Sort, Shaker Sort, Quick Sort, Merge Sort, Radix Sort
Chương 12 Đồ thị Trình bày định nghĩa đồ thị (graph), cách biểu diễn đồ
thị, các tác vụ của đồ thị, duyệt đồ thị theo thứ tự DFS và BFS, sắp thứ tự topo,
tìm đường đi ngắn nhất và tìm cây phủ tối thiểu
Phụ lục A đến Phụ lục I là các chương trình của các chương 4 đến
chương 12 được viết theo kiểu lập trình hướng đối tượng tổng quát dùng khuôn
mẫu (template) (minh họa bằng ngôn ngữ lập trình C++)
Quyển sách Cấu trúc dữ liệu và Giải thuật là tài liệu học tập cho sinh
viên chuyên ngành Công nghệ Thông tin, Khoa học Máy tính, Kỹ thuật Máy tính, Công nghệ Phần mềm của hệ đào tạo chính quy bậc đại học và bậc cao
đẳng và lập trình viên
Tác giả trân trọng cảm ơn Ban chủ nhiệm và các giảng viên của Khoa Khoa học và Kỹ thuật Máy tính, Ban Giáo trình của Trường Đại học Bách Khoa – Đại học Quốc gia TP HCM đã tạo điều kiện và giúp đỡ để quyển
sách này được xuất bản
Mặc dù tác giả đã nỗ lực biên soạn, tuy nhiên, quyển sách này không thể tránh khỏi những thiếu sót Tác giả rất mong nhận được những ý kiến đóng góp quý báu của độc giả và xin gửi về:
Khoa Khoa học và Kỹ thuật Máy tính, Trường Đại học Bách Khoa – Đại học Quốc gia TP Hồ Chí Minh, 268 Lý Thường Kiệt, Phường 14, Quận
10, TP Hồ Chí Minh
Email: nttruc@hcmut.edu.vn, truc@cse.hcmut.edu.vn
Tác giả NGUYỄN TRUNG TRỰC
Trang 9Giới thiệu
Chương này trình bày phương pháp luận lập trình bao gồm các giai đoạn: đặc tả và phân tích vấn đề, thiết kế giải thuật, xây dựng chiến lược kiểm thử, viết chương trình, thực hiện và kiểm tra chương trình, bảo trì chương trình
Chương này cũng giới thiệu lập trình có cấu trúc, là kiểu lập trình nhằm nâng cao tính trong sáng (dễ đọc, dễ hiểu), nâng cao chất lượng và giảm thời gian phát triển chương trình (dễ chẩn đoán lỗi, dễ sửa đổi chương trình)
1.1 Phương pháp luận lập trình
Phương pháp luận lập trình (programming methodology) giải quyết
các vấn đề về phân tích (analysis), thiết kế (design) và thực hiện (implementation) các chương trình
Phương pháp luận lập trình còn được gọi là phát triển chương trình
(program development)
Một phương pháp luận quan trọng là cách tiếp cận từ trên xuống
(top-down approach) để phân tích, thiết kế và thực hiện
Quy trình giải quyết vấn đề (problem-solving process) bằng máy tính
gồm có các giai đoạn (phase) quan trọng sau đây:
1 Đặc tả và phân tích vấn đề (problem analysis and specification)
2 Thiết kế giải thuật (algorithm design)
3 Xây dựng chiến lược kiểm thử (testing strategy development)
4 Viết chương trình (program coding)
5 Thực hiện và kiểm tra chương trình (program execution and testing)
6 Bảo trì chương trình (program maintenance)
Trong thực tế, các giai đoạn này có thể được thực hiện một cách tuần
tự theo mô hình thác nước (waterfall model) hoặc chúng được thực hiện lặp vòng nhiều lần theo mô hình xoắn ốc (spiral model)
Trang 101.1.1 Đặc tả và phân tích vấn đề
Đặc tả ban đầu của vấn đề có thể hơi mơ hồ và không chính xác
Do đó, giai đoạn đầu tiên của quy trình giải quyết vấn đề là xem xét lại
(review) vấn đề một cách cẩn thận để xác định phần nhập và phần xuất của vấn
đề Giai đoạn này còn được gọi là hiểu vấn đề (problem understanding) Giai đoạn này có thể là rất khó nhưng nó cũng là giai đoạn hoàn toàn quyết định
Đặc tả vấn đề (problem specification), còn được gọi là định nghĩa vấn đề (problem definition), bao gồm các định nghĩa chính xác của hai
thành phần chính là phần nhập và phần xuất và sự liên hệ giữa chúng
- Phần nhập (input) là thông tin gì được cho và những điều nào là
quan trọng để giải quyết vấn đề Phần nhập được gọi là giả thiết (supposition) của bài toán Phần nhập phải được xác định đầy đủ để
giải quyết vấn đề
- Phần xuất (output) là thông tin gì phải đạt được sau khi giải quyết
vấn đề Phần xuất được gọi là kết luận (conclusion) của bài toán
Việc xác định phần nhập và phần xuất của vấn đề giúp cho ta hiểu
vấn đề một cách rõ ràng và chính xác
Phân tích vấn đề (problem analysis) là một quá trình hiểu vấn đề, các
yêu cầu của người sử dụng và đề xuất các giải pháp (solution) để giải quyết
các yêu cầu này Mục tiêu của phân tích vấn đề là hiểu rõ vấn đề trước khi giải quyết nó
Người sử dụng nêu ra vấn đề và người giải quyết vấn đề (problem solver) phải cùng làm việc chung với nhau để bảo đảm cả hai cùng hiểu rõ
vấn đề
Các bước của phân tích vấn đề gồm có:
1 Đạt được sự chấp thuận (agreement) về định nghĩa vấn đề: Một cách
đơn giản nhất để có được sự chấp thuận là người giải quyết vấn đề phải ghi chép vấn đề rõ ràng và bàn luận với người sử dụng Các nguyên tắc
ghi chép là KISS (Keep It Simple Stupid) và SVDPI
(Subject-Verb-Direct-Preposition-Indirect)
2 Hiểu được các nguyên nhân cơ bản (root cause): Vấn đề ở đằng sau
vấn đề Phân tích các nguyên nhân cơ bản là một cách làm có hệ thống để làm rõ các nguyên nhân và những tiềm ẩn của vấn đề
3 Xác định những người sử dụng (user) và những người có liên quan (stakeholder) Hiểu được các yêu cầu của người sử dụng và người có
Trang 11liên quan là yếu tố chính để xây dựng giải pháp hiệu quả Người có liên quan là người chịu ảnh hưởng chủ yếu do việc thực hiện giải
quyết vấn đề bằng hệ thống (system) hoặc ứng dụng (application)
4 Xác định phạm vi của hệ thống (system boundary) Phạm vi của hệ thống là sự giao tiếp (interface) với môi trường ngoài hoặc với các hệ
thống khác
5 Xác định các ràng buộc (constraint) áp đặt cho giải pháp Ràng buộc là
sự hạn chế hoặc giới hạn cần phải tuân thủ khi đề xuất giải pháp
Ví dụ: Vấn đề ban đầu là giải phương trình bậc hai ax2 + bx + c = 0 trong trường số thực
Phần nhập: cho ba hệ số a , b , c là các số thực
Phần xuất: tìm nghiệm x là số thực
1.1.2 Thiết kế giải thuật
Giai đoạn thiết kế giải thuật (algorithm design) gồm có hai bước:
- Bước 1: Thiết kế giải pháp
- Bước 2: Tinh chế giải pháp
Trong bước thiết kế giải pháp (solution design), ta xác định giải pháp
sơ bộ Ta chia vấn đề ban đầu thành nhiều vấn đề con nhỏ hơn, dễ giải quyết hơn và cứ như thế cho đến khi các vấn đề con cuối cùng được giải quyết Khi đó, ta đã tìm ra được giải pháp cuối cùng
Các giải pháp từng phần để giải quyết các vấn đề con và giải pháp
cuối cùng để giải quyết vấn đề ban đầu đều được trình bày ở dạng ý tưởng (idea), chưa đề cập đến các yếu tố kỹ thuật cụ thể
Cách tiếp cận này là cách tiếp cận từ trên xuống (top-down approach) với chiến lược chia để trị (devide and conquer)
Các giải pháp cần phải có tính tổng quát Tuy nhiên, ta cũng cần phải lưu ý các khía cạnh riêng biệt (particular aspect) của vấn đề để tìm ra các
giải pháp thích hợp cho chúng
Ví dụ: Vấn đề ban đầu là giải phương trình bậc hai ax2 + bx + c = 0 trong trường số thực Vấn đề này được chia thành hai vấn đề con nhỏ hơn như sau:
Trang 12- Vấn đề ban đầu: Nếu a = 0 thì giải phương trình bậc nhất bx + c = 0 , ngược lại thì giải phương trình bậc hai đủ ax2+ bx + c = 0
- Vấn đề con 1: Giải phương trình bậc nhất bx + c = 0 , khi a = 0 Giải pháp từng phần là tìm nghiệm x (bậc nhất), nếu có
- Vấn đề con 2: Giải phương trình bậc hai đủ ax2+ bx + c = 0 , khi a ≠
0 Giải pháp từng phần là tìm nghiệm x (bậc hai), nếu có
Trong bước tinh chế giải pháp (solution refinement), một giải pháp
phải được mô tả chi tiết hơn bằng một chuỗi các hành động (action) cụ thể
không phân chia và được thực hiện trên dữ liệu của phần nhập để cho ra kết
quả của phần xuất và được gọi là giải thuật (algorithm)
Quá trình tinh chế (refinement process) thực hiện chi tiết hóa một
giải pháp cho đến khi đạt được mô tả giải thuật tương tự với chương trình
Quá trình này được gọi là tinh chế dần từng bước (stepwise refinement)
Chương trình thực hiện giải thuật phải được viết bằng một ngôn ngữ
mà máy tính hiểu được Ngôn ngữ này được gọi là ngôn ngữ lập trình (programming language)
Do đó, ta thường mô tả giải thuật bằng một ngôn ngữ tương tự với
ngôn ngữ lập trình, ngôn ngữ này được gọi là ngôn ngữ lập trình giả (pseudoprogramming language) và thường được gọi là mã giả (pseudocode)
Ngôn ngữ này là sự pha trộn giữa ngôn ngữ tự nhiên (natuaral language) và
ký hiệu toán học (mathematical notation) Mã giả không phải là một ngôn
ngữ lập trình được dùng cho máy tính
Không giống như ngôn ngữ lập trình mức cao (high-level programming language) như Pascal, C++, C#, Java, …, ta không có một tập các quy tắc nào
để định nghĩa một cách chính xác ngôn ngữ nào là mã giả, ngôn ngữ nào không
là mã giả Mã giả thay đổi từ nhóm lập trình này đến nhóm lập trình khác Sau đây là một số đặc điểm trong các mã giả khác nhau:
- Các ký hiệu máy tính thông thường được sử dụng cho các phép
toán số học (arithmetic operation): + là phép cộng (addition), − là
phép trừ (subtraction), * là phép nhân (multiplication) và / là phép
chia (division)
- Các tên ký hiệu (symbolic name) hoặc danh hiệu (identifier) được
dùng để biểu diễn các đại lượng được xử lý trong giải thuật
Trang 13- Các ghi chú (comment) được dùng để giải thích cho các bước, các
thành phần của giải thuật Các ghi chú này thường được ghi trong cặp
(* *) , cặp /* */ hoặc //
- Một số từ của ngôn ngữ tự nhiên được sử dụng làm từ khóa (keyword):
read , enter , input dùng để nhập dữ liệu, write , output ,
display , print dùng để xuất dữ liệu, compute dùng để tính toán
- Thụt đầu dòng (indentation) được dùng để trình bày các cấu trúc điều
khiển cho dễ đọc
Mô tả giải thuật bằng mã giả phải rõ ràng và cho thấy luận lý của
chương trình (program) thực hiện giải thuật này
Giải thuật có cấu trúc (structured algorithm) là một giải thuật được
thiết kế bằng cách sử dụng ba cấu trúc điều khiển sau đây:
- Cấu trúc tuần tự (sequence structure): chuỗi các hành động được
thực hiện một cách tuần tự, mỗi hành động được thực hiện đúng một lần
- Cấu trúc điều kiện (conditional structure): Một hoặc nhiều hành
động khác nhau được chọn và thực hiện
- Cấu trúc lặp (iterational structure): một hoặc nhiều hành động được
thực hiện lặp lại
Ba cấu trúc điều khiển này rất đơn giản Tuy nhiên, trên thực tế, ba cấu trúc điều khiển này được dùng để mô tả bất kỳ giải thuật nào
Cấu trúc của một giải thuật được trình bày bằng một hình vẽ, hình
này được gọi là sơ đồ cấu trúc (structure diagram)
Sơ đồ cấu trúc cho thấy các công việc khác nhau phải được thực hiện
và mối liên hệ giữa chúng Sơ đồ cấu trúc đặc biệt rất hữu ích để mô tả các giải thuật của các vấn đề rất phức tạp
Trang 14Giải phương trình bậc hai đủ ax2 + bx + c = 0;
- Vấn đề con 1: Giải phương trình bậc nhất bx + c = 0 , khi a = 0 Giải pháp từng phần là tìm nghiệm x (bậc nhất), nếu có
- Vấn đề con 2: Giải phương trình bậc hai đủ ax2+ bx + c = 0 , khi a ≠
0 Giải pháp từng phần là tìm nghiệm x (bậc hai), nếu có
Trang 15write "Nghiệm kép = ", x;
}
else
{
compute x1 = (-b – sqrt(delta)) / (2 * a);
compute x2 = (-b + sqrt(delta)) / (2 * a);
write "Nghiệm x1 = ", x1;
write "Nghiệm x2 = ", x2;
}
1.1.3 Xây dựng chiến lược kiểm thử
Giải thuật được dùng để viết chương trình cho máy tính thực hiện Ta cần phải chạy thử chương trình này với nhiều tổ hợp dữ liệu nhập khác nhau
để bảo đảm chương trình luôn luôn cho ra các kết quả đúng trong mọi trường hợp
Một tổ hợp dữ liệu nhập để chạy kiểm tra một chương trình được gọi
là trường hợp kiểm thử (test-case)
Để kiểm thử một chương trình một cách hoàn toàn kỹ lưỡng, ta phải phân loại các trường hợp kiểm thử sao cho chúng bao quát các giá trị nhập
thông thường (normal input value) và các giá trị nhập ở ngưỡng (extreme input value) để kiểm tra các giới hạn của chương trình Các tổ hợp đặc biệt
của dữ liệu nhập được dùng để kiểm tra chương trình thực hiện các khía cạnh riêng biệt của vấn đề
Lưu ý rằng đối với tất cả các trường hợp kiểm thử, các kết quả mong
đợi phải được xác định trước khi viết chương trình Việc hoàn thành các
trường hợp kiểm thử sẽ giúp cho việc kiểm tra các giải thuật và các chương trình được hiệu quả
Ví dụ: Các trường hợp kiểm thử để kiểm tra chương trình giải phương trình
bậc hai ax2+ bx + c = 0 như sau:
Bảng 1.1 Các trường hợp kiểm thử
Test-case a b c Kết quả mong đợi
TC1 0 0 0 Phương trình có vô số nghiệm
TC2 0 0 1 Phương trình bậc nhất vô nghiệm
TC3 0 1 2 Nghiệm bậc nhất x = -2
TC4 1 1 2 Phương trình bậc hai vô nghiệm
TC5 1 2 1 Nghiệm kép x = -1
TC6 1 -3 2 Hai nghiệm x1 = 1 và x2 = 2
Trang 161.1.4 Viết chương trình
Các giải thuật bằng mã giả không thể được thực hiện trực tiếp trên máy tính
Quá trình chuyển giải thuật bằng mã giả thành chương trình bằng ngôn
ngữ lập trình được gọi là viết chương trình hoặc lập trình (programming,
coding)
Nhiều ngôn ngữ lập trình đều có các giới hạn khi thực hiện các chương trình trên máy tính Do đó, việc lập trình sẽ gặp khó khăn khi các
giới hạn này xảy ra Ví dụ, trong toán học thì số nguyên (integer number) có
thể rất lớn, nhưng nhiều phần mềm ngôn ngữ lập trình chứa số nguyên trong
4 byte và có giá trị từ 0 đến 4.294.967.295 (số nguyên không dấu)
Ta phải chọn một phần mềm ngôn ngữ lập trình thích hợp với giải
thuật đã thiết kế để viết chương trình
Hiện tại có nhiều ngôn ngữ lập trình mức cao (high-level programming language) như Pascal, C++, C#, Java, … Mỗi ngôn ngữ lập trình đều có các
đặc điểm riêng biệt
Sau khi ta chọn ngôn ngữ lập trình, việc viết chương trình phải bảo
đảm chương trình chạy đúng (correct), dễ đọc (readable) và dễ hiểu
(understandable)
Các nguyên tắc viết chương trình là:
- Chương trình nên có cấu trúc tốt (well-structured)
- Mỗi chương trình nên được lập tài liệu (program documentation)
- Chương trình nên được định dạng (formatted)
a Chương trình có cấu trúc tốt
- Sử dụng cách tiếp cận từ trên xuống (top-down approach) khi phát
triển chương trình để giải quyết một vấn đề phức tạp Phân chia vấn
đề ban đầu thành nhiều vấn đề con ngày càng đơn giản hơn Sau đó,
ta viết các đơn thể chương trình riêng biệt để giải quyết các vấn đề
con này Cách làm này được gọi là lập trình đơn thể (modular
programming)
- Cố gắng đạt được tính đơn giản (simplicity) và tính trong sáng
(clarity) Nên tránh kiểu lập trình thông minh chỉ nhằm mục đích
chứng minh sự khéo léo của người lập trình hoặc viết đoạn mã lệnh
để chạy nhanh hơn một chút
Trang 17b Lập tài liệu cho đơn vị chương trình
- Mỗi đơn vị chương trình (program unit) nên có tài liệu mở (opening
documentation) Nên có các ghi chú (comment) ở đầu mỗi chương trình (thủ tục, hàm) để giải thích chương trình làm gì (what), làm như thế nào (how), các giải thuật (algorithm) được sử dụng, thông tin của
người lập trình, ngày viết và ngày cập nhật chương trình, các tài liệu tham khảo để bổ sung thông tin về chương trình, giải thích các biến được sử dụng trong chương trình
- Các đoạn chương trình (program segment) quan trọng nên có các ghi
chú Tuy nhiên, quá nhiều các ghi chú chi tiết và không cần thiết sẽ làm cho chương trình khó đọc và khó hiểu
- Nên sử dụng các danh hiệu có nghĩa (meaningful identifier) Ví dụ:
các danh hiệu có nghĩa found , firstName , …
c Định dạng chương trình
Chương trình nên được định dạng theo một kiểu mẫu (style) để làm
tăng tính dễ đọc (readability) Điều này được gọi là phong cách lập trình
(programming style)
- Sử dụng các khoảng trống (space) giữa các thành phần trong một phát biểu để dễ đọc Ví dụ: nên có khoảng trống trước và sau mỗi toán tử
- Thêm hàng trống (blank line) giữa các phần (section) của chương
trình và ở bất cứ chỗ nào thích hợp trong chuỗi các phát biểu để tạo thành các nhóm phát biểu
- Tuân theo một cách nghiêm ngặt các hướng dẫn về sắp thẳng hàng
(alignment) và thụt đầu dòng (indentation) để làm nổi bật mối liên hệ
giữa các phần khác nhau của chương trình
Ví dụ: Chương trình sau giải phương trình bậc hai ax2+ bx + c = 0
Trang 18void GiaiPTBac2(double a, double b, double c)
double x1 = (-b - sqrt(delta)) / (2 * a);
double x2 = (-b + sqrt(delta)) / (2 * a);
cout << "Nghiem x1 = " << x1 << endl;
cout << "Nghiem x2 = " << x2 << endl;
}
}
// - // Giải phương trình bậc hai ax2 + bx + c = 0
Trang 191.1.5 Thực hiện và kiểm thử chương trình
Sau khi viết xong chương trình, ta phải chạy chương trình này với các
trường hợp kiểm thử (test-case) đã được xác định trong giai đoạn xây dựng
chiến lược kiểm thử
a Các lỗi của chương trình
Khi chạy chương trình, ta có thể gặp các lỗi sau đây:
- Lỗi cú pháp (syntax error), hoặc lỗi thời gian biên dịch
(compile-time error), là một lỗi trong cú pháp của một chuỗi hoặc các token
của một ngôn ngữ lập trình cụ thể
Đối với trình biên dịch (compiler), lỗi cú pháp xảy ra trong thời gian
biên dịch (compile-time) Trình biên dịch sẽ thông báo các lỗi cú
pháp và người lập trình phải sửa các lỗi này Một chương trình sẽ được biên dịch xong khi tất cả các lỗi cú pháp đều được sửa
Đối với trình thông dịch (interpreter), không phải tất cả các lỗi cú
pháp đều được phát hiện cho đến khi chạy chương trình
Ví dụ: Trong C++, một lỗi cú pháp là cout Hello; viết đúng là
cout << "Hello";
- Lỗi thời gian chạy (run-time error) là một lỗi xảy ra trong thời gian
chạy chương trình Khi xảy ra lỗi thời gian chạy, chương trình bị dừng thực hiện (kết thúc bất thường) và trình biên dịch sẽ thông báo lỗi Để sửa lỗi này, người lập trình phải tìm nguyên nhân gây ra lỗi bằng cách lần ngược từ phát biểu bị dừng thực hiện
Ví dụ: Đoạn mã C++ sau đây:
- Lỗi luận lý (logic error, logical error, semantic error) là một lỗi
(bug) trong chương trình làm cho chương trình tạo ra kết quả không
mong đợi hoặc hành vi khác Thông thường, ta khó tìm ra lỗi luận lý
Trang 20Ví dụ: Một lỗi luận lý là delta = b * b + 4 * a * c; viết đúng là
delta = b * b - 4 * a * c;
Lỗi luận lý có thể xảy ra trong các trường hợp: thiết kế giải thuật bị sai, thiết kế chương trình chưa hoàn thiện, viết chương trình bị sai, tính toán các kết quả mong đợi của các trường hợp kiểm thử bị sai Quá trình viết chương trình và kiểm thử chương trình được gọi là
thực hiện chương trình (program implementation)
b Các cách thức chạy chương trình
Có hai cách thức chạy chương trình là:
- Cách thức tương tác (interactive mode): Người sử dụng tương tác
với chương trình trong quá trình chạy chương trình bằng cách người
sử dụng nhập vào dữ liệu và chương trình sẽ cho ra kết quả
- Xử lý theo bó (batch processing): Tập tin dữ liệu (data file) chứa các
dữ liệu Trong quá trình chạy, chương trình sẽ đọc dữ liệu từ các tập tin dữ liệu và cho ra kết quả mà không có sự tương tác của người sử
dụng (user interaction)
1.1.6 Bảo trì chương trình
Để bảo trì chương trình, ta cần phải lập tài liệu chương trình
Lập tài liệu chương trình (program documentation) bắt đầu từ giai
đoạn đầu tiên và tiếp tục trong suốt quá trình phát triển chương trình
Tài liệu cuối cùng của chương trình, được gọi là danh sách chương trình (program listing), gồm có các nội dung sau đây:
- Giải thích tất cả các giai đoạn của quá trình phát triển chương trình
- Các thiết kế chương trình
- Mô tả giải pháp giải quyết vấn đề
- Giải thích các chương trình phức tạp sao cho dễ hiểu
- Bản in mã nguồn của các chương trình
- Các vấn đề gặp phải trong quá trình viết và kiểm thử chương trình
Tài liệu này là tài liệu kỹ thuật (technical report) và được trình bày sao
cho dễ đọc, dễ hiểu và giúp ích cho việc bảo trì và phát triển chương trình
Trang 21Ngoài ra, tài liệu chương trình bao gồm tài liệu cung cấp cho người
sử dụng (user instruction), đây là tài liệu hướng dẫn người sử dụng không
quen với máy tính có thể sử dụng được các chương trình Sổ tay người sử dụng (user's manual) hướng dẫn sử dụng chương trình, chuẩn bị dữ liệu
nhập như thế nào, giải thích các kết quả Ta không nên sử dụng các thuật ngữ kỹ thuật trong tài liệu này
Sau một thời gian sử dụng, chương trình có thể bị thay đổi vì các lý
do sau đây, nhất là đối với các hệ thống phức tạp:
- Phát hiện thêm các lỗi (bug) mà chúng chưa được phát hiện trong giai
đoạn kiểm thử chương trình
- Thay đổi các yêu cầu nghiệp vụ (business requirement)
- Thay đổi các yêu cầu hệ thống (system requirement)
- Thêm các yêu cầu nghiệp vụ mới
Bảo trì chương trình (program maintenance) bao gồm tất cả các hoạt
động (activity) xảy ra sau khi vận hành chương trình, nghĩa là chương trình
đã được đưa vào sử dụng trong thực tế Người lập trình phải sửa lại các chương trình hiện tại và/hoặc viết thêm các chương trình mới
Các hoạt động của bảo trì chương trình gồm có:
- Loại bỏ các lỗi đã được phát hiện
- Sửa các chương trình hiện tại để đáp ứng các yêu cầu thay đổi về nghiệp vụ và về hệ thống
- Viết thêm các chương trình để thêm các yêu cầu nghiệp vụ mới
- Chỉnh sửa tài liệu chương trình
Tài liệu bảo trì (maintenance documentation) sẽ giúp ích cho những
người bảo trì (maintainer) hoàn thành các công việc của họ Tài liệu này
gồm có các nội dung:
- Tài liệu thiết kế
- Mã nguồn chương trình
- Thông tin về kiểm thử
Trong thực tế, chi phí (cost) bảo trì thường lớn hơn chi phí xây dụng
chương trình ban đầu
Trang 22Lưu ý
Chi phí bảo trì chương trình có thể giảm đáng kể Điều này phụ thuộc vào việc thiết kế và lập trình ban đầu sao cho dễ đọc, dễ hiểu, có cấu trúc tốt, dễ sửa đổi và được lập tài liệu
1.2 Lập trình goto và lập trình có cấu trúc
1.2.1 Lập trình goto
Phát biểu goto cho phép chuyển điều khiển đến một phát biểu bất kỳ nào đó trong một chương trình Điều này có thể dẫn đến chương trình hoàn
toàn không có cấu trúc và ta khó theo dõi dòng điều khiển (control flow) của
chương trình, khó bảo trì và khó phát triển chương trình
Kiểu lập trình này được gọi là lập trình goto (goto programming), là
kiểu lập trình không có cấu trúc
"Spaghetti code" (mã rối) là một thuật ngữ có nghĩa xấu dùng cho mã
nguồn có cấu trúc điều khiển phức tạp và lộn xộn, đặc biệt là sử dụng nhiều phát biểu goto (là phát biểu rẽ nhánh không điều kiện)
Có nhiều nguyên nhân gây ra mã rối, bao gồm:
- Những người lập trình chưa có kinh nghiệm, thiết kế giải thuật chưa tốt, chưa biết sử dụng các cấu trúc điều khiển một cách hợp lý để viết chương trình
- Nhiều người lập trình cùng hiệu chỉnh một chương trình trong một khoảng thời gian dài
- …
Lưu ý
Lập trình goto cho phép một chương trình có nhiều ngõ vào (entry point) và nhiều ngõ ra (exit point) và có thể gây ra mã rối
Lập trình có cấu trúc làm giảm đáng kể tầm ảnh hưởng của mã rối
Ví dụ: Chương trình sau đây minh họa mã rối dùng để hiển thị tổng của các
số bình phương của các số từ 1 đến 5
C++
int main()
Trang 23Lập trình goto cho phép người lập trình không chuyên viết chương
trình một cách tùy tiện, miễn sao chương trình cho ra kết quả đúng
Điều này giúp cho người lập trình tốn ít thời gian viết chương trình và chương trình chạy nhanh Tuy nhiên, điều này cũng làm cho ta khó theo dõi
dòng điều khiển của chương trình, khó hiểu luận lý của chương trình (program logic) tức là giải thuật (algorithm), khó bảo trì và khó phát triển
chương trình
Trang 24Người lập trình chuyên nghiệp không sử dụng lập trình goto mà sử
dụng lập trình có cấu trúc (structured programming) và lập trình hướng đối tượng (object-oriented programming).
1.2.2 Lập trình có cấu trúc
Lập trình có cấu trúc (structured programming) là một kiểu lập trình nhằm mục đích nâng cao tính trong sáng (dễ đọc, dễ hiểu), nâng cao chất lượng và giảm thời gian phát triển chương trình (dễ chẩn đoán lỗi, dễ sửa
đổi chương trình) bằng cách sử dụng các chương trình con, cấu trúc tuần tự,
cấu trúc điều kiện, cấu trúc lặp nhằm để loại bỏ kiểu lập trình kiểm tra và rẽ nhánh không điều kiện (thường được gọi là kiểu lập trình goto ) mà nó dẫn đến mã rối khó theo dõi dòng điều khiển của chương trình, khó bảo trì
và khó phát triển chương trình
Chương trình có cấu trúc (structured program) là một chương trình
chỉ bao gồm các cấu trúc tuần tự, cấu trúc điều kiện và cấu trúc lặp
Trong lập trình có cấu trúc, phương pháp để viết một chương trình là
sử dụng:
- Phân tích từ trên xuống (top-down analysis) để giải quyết vấn đề:
phân tích một vấn đề (công việc) phức tạp thành nhiều vấn đề con
(công việc con) đơn giản hơn Đây là chiến lược chia để trị (divide
and conquer) Sau đó, mỗi vấn đề con có thể được giải quyết một
cách độc lập, có thể tiếp tục sử dụng chiến lược chia để trị để chia
chúng thành các vấn đề con đơn giản hơn nữa Quá trình tinh chế
(refinement process) này tiếp tục cho đến khi đạt được các vấn đề
con đủ đơn giản mà ta có thể dễ dàng giải quyết chúng
- Đơn thể hóa (modularization) và lập trình đơn thể (modular
programming): tổ chức và cấu trúc của chương trình bao gồm các
đơn thể (module) và viết chương trình cho các đơn thể riêng biệt
- Cấu trúc hóa (structuralization): sử dụng các cấu trúc tuần tự, cấu
trúc điều kiện và cấu trúc lặp để viết chương trình cho các đơn thể Các quy tắc của lập trình có cấu trúc gồm:
Quy tắc 1: Phân tích từ trên xuống – Sử dụng cấu trúc tuần tự
Phân rã một công việc phức tạp thành nhiều công việc đơn giản hơn
(phân tích từ trên xuống) và chúng được thực hiện một cách tuần tự (sử dụng cấu trúc tuần tự)
Trang 25Thực hiện công việc phức tạp bằng cách thực hiện tuần tự các công
việc sau đây (xem Hình 1.2):
Công việc đơn giản 1
Công việc đơn giản 2
…
Công việc đơn giản n
Quy tắc 2: Đơn thể hóa – Lập trình đơn thể
Một công việc đơn giản có thể được thực hiện bởi một đơn thể
Trong lập trình đơn thể, đơn thể (module) là một chương trình con (subroutine, subprogram) có thể là một hàm (function) hoặc một thủ tục
(procedure) bao gồm một số ít phát biểu nhằm để thực hiện một công việc
Trang 26Quy tắc 3: Cấu trúc hóa – Lập trình có cấu trúc
Một công việc đơn giản có thể được thực hiện bởi:
- Một phát biểu thực thi: phát biểu gán, phát biểu nhập / xuất, phát
biểu gọi hàm (function call), … nhưng không là phát biểu rẽ nhánh
không điều kiện nhằm để loại bỏ goto ( goto elimination) Phát biểu thực thi thực hiện một hành động không phân chia được (indivisible action)
- Một cấu trúc điều kiện: cấu trúc if , cấu trúc switch ,
- Một cấu trúc lặp: cấu trúc for , cấu trúc while , cấu trúc do while
và thân của các cấu trúc này thực hiện tuần tự các công việc đơn giản hơn nữa
Hai cấu trúc điều khiển không bao giờ giao nhau (cắt nhau) và hai cấu trúc điều khiển có thể được kết hợp theo hai cách như sau:
- Tuần tự (sequential): cấu trúc điều khiển đứng trước được thực hiện
trước, cấu trúc điều khiển đứng sau được thực hiện sau
Ví dụ: Cấu trúc điều kiện if đứng trước cấu trúc lặp while
Ngõ ra
Trang 27- Lồng nhau (nested): thân của cấu trúc điều khiển này (cấu trúc nằm
ngoài) có chứa cấu trúc điều khiển khác (cấu trúc nằm trong)
Ví dụ: Cấu trúc lặp while chứa cấu trúc lặp for
break , continue ở trong thân của các cấu trúc lặp for , while ,
do while , các phát biểu return ở trong thân của một đơn thể để trở về đơn thể gọi.
Hình 1.4 Hai cấu trúc được thực hiện tuần tự
Trang 28Lưu ý rằng các phát biểu rẽ nhánh không điều kiện thường được sử dụng chung với các phát biểu if
Một ngoại lệ của lập trình có cấu trúc là cho phép sử dụng các phát biểu break trong cấu trúc điều kiện switch vì lý do ngữ nghĩa của cấu trúc này
Lập trình có cấu trúc chỉ cho phép một loại rẽ nhánh không điều kiện
là gọi đơn thể (module call) để thực hiện một đơn thể khác, tức là thực hiện
một công việc khác
Điều này thể hiện mục đích của lập trình có cấu trúc là loại bỏ goto
( goto elimination): mỗi cấu trúc chỉ có một ngõ vào (bắt đầu) và một ngõ
ra (thoát)
Ví dụ: Giải quyết bài toán tính diện tích của hình chữ nhật có chiều dài và
chiều rộng được nhập từ bàn phím: nếu chiều dài và chiều rộng là số dương thì hiển thị diện tích, ngược lại thì hiển thị lỗi sai do nhập dữ liệu
- Áp dụng Quy tắc 1 – Phân tích từ trên xuống, bài toán này được phân
rã thành các công việc được thực hiện một cách tuần tự (cấu trúc tuần tự) như sau:
CV1: Nhập chiều dài a từ bàn phím;
CV2: Nhập chiều rộng b từ bàn phím;
CV3: Kiểm tra dữ liệu hợp lệ và tính toán, xuất kết quả;
- Áp dụng Quy tắc 3 – Cấu trúc hóa, CV1 và CV2 được thực hiện bởi các phát biểu nhập và CV3 được thực hiện bởi một cấu trúc điều kiện
CV6: Hiển thị lỗi sai do nhập dữ liệu;
- Áp dụng Quy tắc 2 – Đơn thể hóa, CV4 được thực hiện bởi một phát biểu gọi hàm tính diện tích TinhDienTichCN(a, b) và gán cho biến dienTich :
Trang 29CV6: Hiển thị lỗi sai do nhập dữ liệu;
Hàm tính diện tích TinhDienTichCN(a, b) là một đơn thể và được viết bằng một cấu trúc tuần tự như sau:
CV6: cout << "Du lieu nhap bi sai" << endl;
- Áp dụng Quy tắc 3 – Cấu trúc hóa, CV7 được thực hiện bởi phát biểu gán và CV8 được thực hiện bởi phát biểu return :
Trang 30Sự lệch hướng phổ biến ở trong nhiều ngôn ngữ lập trình là sử dụng
các phát biểu rẽ nhánh không điều kiện:
- Phát biểu return được dùng để kết thúc sớm việc thực hiện một chương trình con
- Phát biểu break được dùng để kết thúc sớm việc thực hiện một cấu trúc
lặp for , while và do while
Điều này dẫn đến một chương trình con hoặc một cấu trúc lặp có thể
có nhiều ngõ ra (exit point) và cho phép việc lập trình được dễ dàng hơn so
với lập trình có cấu trúc
Tuy nhiên, người lập trình phải thật cẩn thận khi sử dụng các phát biểu return và break
Trang 31Nên nhớ rằng việc sử dụng quá đáng nhiều phát biểu break trong một cấu trúc lặp và nhiều phát biểu return trong một chương trình con sẽ dẫn đến " spagetti-code " (mã xấu): cấu trúc lặp có quá nhiều điều kiện thoát, chương trình con có quá nhiều điểm trở về Điều này sẽ làm cho ta khó đọc hiểu, khó chẩn đoán các lỗi và khó phát triển chương trình
Các phát biểu return và break thường ở trong các phát biểu if Kiểu lập trình if return và if break cũng là kiểu lập trình kiểm tra
và rẽ nhánh không điều kiện.
Ví dụ: Chương trình C++ sau đây cho biết phần tử của lần xuất hiện đầu tiên
có giá trị là 5 trong ma trận vuông a (mảng hai chiều)
Ta sử dụng hai cấu trúc lặp for lồng nhau Khi phát hiện phần tử
a[i][j] bằng 5, ta dùng phát biểu break để thoát khỏi vòng lặp for(j)
bên trong Tuy nhiên, điều khiển chương trình vẫn ở trong vòng lặp for(i)
bên ngoài Để điều khiển chương trình tiếp tục thoát khỏi vòng lặp for(i)
bằng phát biểu break , ta phải sử dụng thêm biến cờ found , nó được gán giá trị true trong vòng lặp for(j) và nó được kiểm tra trong vòng lặp
Trang 32// Cờ found để thoát vòng lặp while(i)
while ((i < n) && ! found)
{
j = 0;
// Cờ found để thoát vòng lặp while(j)
while ((j < n) && ! found)
Trang 33{
if (a[i][j] == 5)
// Biến cờ found dùng để thoát cùng lúc
// hai vòng lặp while(j) và while(i)
Trong trường hợp này, việc thoát khỏi cùng lúc hai cấu trúc lặp
while lồng nhau bằng biến cờ found một cách tự nhiên hơn so với việc thoát khỏi cùng lúc hai cấu trúc lặp for lồng nhau bằng phát biểu break và biến cờ found
Các ưu điểm của lập trình có cấu trúc
Nói chung, lập trình có cấu trúc tập trung vào việc giải quyết vấn đề
về độ phức tạp (complexity) trong lập trình
Lập trình có cấu trúc dựa vào các nguyên tắc:
- Phân tích từ trên xuống để giải quyết vấn đề
- Đơn thể hóa để thiết kế chương trình
- Cấu trúc hóa để lập trình
Do đó, lập trình có cấu trúc có các ưu điểm sau đây:
1 Viết các chương trình dễ dàng hơn (more easily) và nhanh hơn
(more quickly) Một công việc phức tạp phải được phân chia thành
các công việc con, sao cho viết chương trình một cách dễ dàng cho các công việc con này, mỗi chương trình là một đơn vị riêng biệt
Trang 342 Các chương trình có tính tin cậy cao hơn (greater reliability) Ít xảy
ra các lỗi luận lý trong các giai đoạn đầu tiên của quá trính phát triển
và viết chương trình
3 Tốn ít thời gian chẩn đoán lỗi (debug) và kiểm thử (test) các chương
trình, bởi vì ít lỗi xảy ra trong quá trình viết chương trình Các lỗi chỉ xảy ra trong các chương trình đơn giản và ta dễ dàng sửa các lỗi này
4 Dễ dàng bảo trì (maintain) các chương trình Khi các chương trình
được đưa vào sử dụng, các yêu cầu về thay đổi chương trình hoặc thêm các tính năng mới cho chương trình ngày càng gia tăng
Khảo sát thực tế cho thấy việc viết chương trình, chẩn đoán lỗi và kiểm thử chương trình tốn dưới 50% toàn bộ công sức lập trình Hơn 50% công sức lập trình dành cho việc bảo trì chương trình
Đối với lập trình có cấu trúc, công việc bảo trì trở nên dễ dàng hơn, đặc biệt đối với những người lập trình mới tham gia vào dự án, bởi vì chương trình dễ đọc hiểu, luận lý chương trình được thiết kế tốt, các đơn thể chương trình đơn giản
Khi có yêu cầu thay đổi, ta dễ dàng tìm ra các đơn thể chương trình nào cần phải thay đổi và thay đổi như thế nào Bởi vì các đơn thể là tách biệt, là các đơn vị chương trình độc lập, nên ta dễ dàng thay thế một đơn thể cũ bởi một đơn thể mới và không dẫn đến việc thay đổi lớn trong toàn bộ chương trình
Khi có yêu cầu thêm các tính năng mới, ta dễ dàng thêm các đơn thể chương trình mới vào các chương trình đang có sẵn
Các ưu điểm này làm tăng hiệu suất lập trình (programming
productivity) của những người lập trình
1.2.3 Tinh chế dần từng bước
Tinh chế dần từng bước (stepwise refinement) là một phương pháp
để phát triển một chương trình bằng cách trước tiên mô tả các chức năng tổng quát, sau đó mỗi chức năng sẽ được mô tả ở mức chi tiết hơn trong các bước tiếp theo cho đến khi toàn bộ chương trình đã được xác định Cách tiếp
cận này còn được gọi là thiết kế từ trên xuống (top-down design)
Tinh chế dần từng bước cho phép ta tập trung giải quyết các vấn đề
khó khăn nhất trong giai đoạn phát triển, nghĩa là tập trung vào các khía cạnh quan trọng nhất và bỏ qua các chi tiết cụ thể
Trang 35Để mô tả các chức năng trong quá trình tinh chế dần từng bước, ta sử dụng mã giả
Mã giả (pseudocode) là sự mô tả quen thuộc ở mức cao về nguyên lý
hoạt động của chương trình hoặc giải thuật
Mã giả được xây dựng từ các quy ước ngôn ngữ lập trình được kết hợp với ngôn ngữ tự nhiên và các ký hiệu toán học
Mục tiêu của việc sử dụng mã giả là để cho con người dễ đọc hiểu giải thuật, bỏ qua các chi tiết không cần thiết mà mã nguồn của ngôn ngữ lập trình quy định; ví dụ, khai báo các biến, các mã đặc trưng của hệ thống, các chương trình con của thư viện chương trình
Không có cú pháp chuẩn cho mã giả và chương trình được viết bằng
mã giả không phải là một chương trình thực thi (chương trình máy tính) Một số từ ngữ và các cấu trúc điều khiển được sử dụng trong mã giả như sau:
set reset increment compute process calculate add sum multiply generate
print display input output edit test
call call with(parameters) return return( )
if else switch for while do while
Quy trình lập trình mã giả (PPP – Pseudocode Programming
Process) là một phương pháp để phát triển chương trình bằng cách viết mã
giả thành các câu ghi chú trong chương trình, sau đó viết mã thật ở phía dưới các câu ghi chú này
Bước đầu tiên của quá trình tinh chế dần từng bước là phân chia một công việc phức tạp ban đầu thành nhiều công việc đơn giản hơn được thực hiện một cách tuần tự
Nhiều chương trình có thể được chia thành ba giai đoạn mà chúng được thực hiện tuần tự như sau:
Giai đoạn bắt đầu (initialization phase): Gán giá trị ban đầu cho các biến Giai đoạn xử lý (processing phase): Nhập dữ liệu và điều chỉnh các biến Giai đoạn kết thúc (termination phase): Tính toán và xuất kết quả
Các bước kế tiếp của quá trình tinh chế dần từng bước là mô tả chi tiết các công việc đơn giản của các giai đoạn trên bằng các cấu trúc điều
Trang 36khiển gồm có cấu trúc tuần tự, cấu trúc điều kiện và cấu trúc lặp, cho đến khi toàn bộ các công việc đã được xác định
Bước cuối cùng của quá trình tinh chế dần từng bước là thay thế mã giả thành mã thật của một ngôn ngữ lập trình cụ thể
Ví dụ: Tính giá trị trung bình của các số được nhập từ bàn phím, kết thúc
việc nhập khi nhập vào số 0
Bước 1: Phân chia công việc phức tạp
// CV1 (Initiation phase): set 0 to tongCong, dem
// CV2 (Processing phase): input x and compute tongCong, dem // CV3 (Termination phase): compute and output gtTrungBinh
Bước 2: Mô tả chi tiết cho công việc CV2
// CV1 (Initiation phase): set 0 to tongCong, dem
// CV2 (Processing phase): input x and compute tongCong, dem
// CV3 (Termination phase): compute and output gtTrungBinh
Bước 3: Mô tả chi tiết cho công việc CV3
// CV1 (Initiation phase): set 0 to tongCong, dem
// CV2 (Processing phase): input x and compute tongCong, dem
output "Khong tinh duoc gia tri trung binh"
Bước 4: Thay thế mã giả thành mã thật, ví dụ ngôn ngữ C++
C++
int main()
Trang 37double gtTrungBinh = tongCong / dem;
cout << "Gia tri trung binh = " << gtTrungBinh << endl; }
Ví dụ: Viết hàm Find(a, n, x) để tìm tuần tự x trong mảng a có n phần
tử Nếu tìm thấy x thì hàm trả về true ; ngược lại thì hàm trả về false
Cách 1: Sử dụng phát biểu return để thoát bất thường vòng lặp for và trở
Trang 38Cách viết này cho thấy:
- Vòng lặp for có hai ngõ ra là i ≥ n và return true
- Hàm Found() có hai ngõ ra là return true và return false
Cách 2: Áp dụng quy trình lập trình mã giả PPP Ta có thể diễn giải bình thường cách thức tìm kiếm x trong mảng a như sau:
// (1) Ban đầu chưa tìm thấy x trong mảng a
// (2) Bắt đầu từ phần tử đầu tiên
// (3) Trong khi xét phần tử hiện tại và chưa tìm thấy
// (4) Nếu phần tử hiện tại là x thì tìm thấy rồi
// (5) Ngược lại, ta xét phần tử kế tiếp
// (6) Trả về kết quả tìm kiếm
(1) Chưa tìm thấy x trong mảng a : ta gán false cho biến found
(2) Bắt đầu từ phần tử đầu tiên: ta gán 0 cho biến i
(3) Trong khi xét phần tử hiện tại và chưa tìm thấy: ta sử dụng vòng lặp
while với điều kiện i < n và not found
(4) Nếu phần tử hiện tại là x thì tìm thấy rồi: ta gán true cho biến found
(5) Ngược lại, ta xét phần tử kế tiếp: ta cho i tăng thêm 1
(6) Trả về kết quả tìm kiếm: trả về found
Ta có đoạn mã giả sau:
// (1) Ban đầu chưa tìm thấy x trong mảng a
found = true;
// (2) Bắt đầu từ phần tử đầu tiên
i = 0;
// (3) Trong khi xét phần tử hiện tại và chưa tìm thấy
while (i < n and not found)
// (4) Nếu phần tử hiện tại là x thì tìm thấy rồi
Trang 39Cách viết này cho thấy:
- Vòng lặp while chỉ có một ngõ ra khi điều kiện:
(i < n) && (! found)) là false
- Hàm Found() chỉ có một ngõ ra là return found và phát biểu này luôn luôn là phát biểu cuối cùng của hàm
1.3 Giải thuật
Giải thuật là một phương pháp để giải quyết một vấn đề
Giải thuật xác định một chuỗi các hành động chi tiết và rõ ràng để giải quyết một vấn đề cụ thể hoặc thực hiện một công việc cụ thể
Trong khoa học máy tính (computer science), giải thuật (algorithm)
là một chuỗi các hành động (action) không phân chia được, còn được gọi là quá trình (process), được thực hiện trên một tập dữ liệu nhập (input) và tạo
ra kết quả (output) trong một khoảng thời gian hữu hạn
Ta có thể mô tả một giải thuật bằng nhiều cách khác nhau:
- Ngôn ngữ tự nhiên (natural language)
- Mã giả (pseudocode)
- Sơ đồ tiến trình (flowchart) …
Quá trình(Xử lý)
Hình 1.6 Mô hình của giải thuật
Thời gian thực hiện
Trang 401.3.1 Mã giả
Mã giả (pseudocode) là sự mô tả quen thuộc ở mức cao về nguyên lý
hoạt động của chương trình máy tính hoặc giải thuật
Mã giả được xây dựng từ các quy ước ngôn ngữ lập trình được kết hợp với ngôn ngữ tự nhiên và các ký hiệu toán học
Mục tiêu của việc sử dụng mã giả là để cho con người dễ đọc hiểu giải thuật, bằng cách bỏ qua các chi tiết không cần thiết mà mã nguồn của ngôn ngữ lập trình quy định, ví dụ như khai báo các biến, các mã đặc trưng của hệ thống, các chương trình con của thư viện chương trình
Ta không có cú pháp chuẩn cho mã giả và chương trình được viết bằng
mã giả không phải là một chương trình thực thi (chương trình máy tính)
Ví dụ: Tìm ước số chung lớn nhất (GCD – Greatest Common Divisor) của hai
số nguyên dương a và b bằng giải thuật Euclid (Euclid's algorithm) Ta có thể
mô tả giải thuật này bằng ngôn ngữ tự nhiên và bằng mã giả sau đây:
Ngôn ngữ tự nhiên:
Bước 1: Nhập a và b
Bước 2: Nếu b = 0 thì a là ước số chung lớn nhất và kết thúc giải thuật
Bước 3: Nếu a > b thì a = a – b , ngược lại b = b – a Sau đó, đến Bước 2