Thuật toán là một khái niệm then chốt trong Tin học. Việc làm quen với các thuật toán cơ bản và khám phá ra những thuật toán là công việc quan trọng của người lập trình. Người lập trình không chỉ cần biết các thuật toán thông dụng mà còn phải biết tự tìm ra các thuật toán giải quyết các vấn đề cụ thể nảy sinh khi viết chương trình.
Trang 1HỒ ANH MINH
GIÁO TRÌNH
NHẬP MÔN THUẬT TOÁN
Trang 2ĐẠI HỌC SƯ PHẠM QUY NHƠN – 2003
Trang 3LỜI NÓI ĐẦU
Thuật toán là một khái niệm then chốt trong Tin học Việc làm quen với các thuậttoán cơ bản và khám phá ra những thuật toán là công việc quan trọng của người lậptrình Người lập trình không chỉ cần biết các thuật toán thông dụng mà còn phải biết tựtìm ra các thuật toán giải quyết các vấn đề cụ thể nảy sinh khi viết chương trình
Hiện nay có hai loại tài liệu liên quan tới thuật toán là các tài liệu về ngôn ngữ lậptrình và các tài liệu chuyên về thuật toán Các tài liệu về ngôn ngữ lập trình thường đưa
ra các chương trình dựa trên các thuật toán có sẵn Việc sử dụng các thuật toán có sẵntrong việc viết chương trình dễ tạo thành một thói quen sử dụng thuật toán một cách cảmtính, ít quan tâm tới tính đúng đắn cũng như hiệu quả của thuật toán đang sử dụng Trongkhi đó các tài liệu chuyên môn về thuật toán thì đòi hỏi một mức độ nhất định về kiếnthức toán học và tư duy, không thích hợp cho những người mới làm quen với thuật toán.Giáo trình Nhập môn thuật toán được biên soạn dựa trên bài giảng cho sinh viênngành Tin học Giáo trình tập trung vào khái niệm thuật toán mà không nhắc nhiều tớicấu trúc dữ liệu
Nội dung giáo trình gồm hai phần:
Phần 1 trình bày các khái niệm cơ bản và giới thiệu một số thuật toán sơ cấpthường dùng
Phần 2 giới thiệu một số kỹ thuật thiết kế thuật toán và các bài toán điển hình đượcgiải quyết nhờ áp dụng các kỹ thuật này
Mục đích của giáo trình nhằm giúp cho sinh viên làm quen với việc diễn đạt (mô tả)thuật toán, đánh giá thuật toán và một số kỹ thuật thiết kế thuật toán Sinh viên có thểvận dụng các yếu tố này vào việc viết chương trình bằng một ngôn ngữ lập trình đã biết
Vì là giáo trình nhập môn nên giáo trình không đi quá chi tiết vào việc sử dụngcông cụ Toán học để đánh giá độ phức tạp của thuật toán Các kỹ thuật thiết kế thuật toánđược lựa chọn phù hợp với trình độ của sinh viên năm thứ Hai và cũng là những kỹ thuật
Trang 4được sử dụng phổ biến Việc áp dụng các kỹ thuật thiết kế thuật toán tổng quát đượcminh họa thông qua nhiều bài toán cụ thể
Cuối mỗi phần đều có khá nhiều bài tập Những bài tập có dấu * có thể sử dụng nhưtài liệu tham khảo bồi dưỡng học sinh giỏi
Việc biên soạn không thể tránh khỏi những thiếu sót, chúng tôi rất mong được sựgóp ý, bổ sung của bạn đọc để chỉnh lý nhằm phục vụ ngày càng tốt hơn bạn đọc
Trang 5PHẦN 1
CÁC KHÁI NIỆM CƠ BẢN
Phần này nhắc lại các khái niệm cơ bản liên quan tới thuật toán và một số thuật toán
sơ cấp quen thuộc thường dùng
1 THUẬT TOÁN VÀ ĐỘ PHỨC TẠP TÍNH TOÁN.
1. Thuật toán, đặc trưng, mô tả thuật toán
Khái niệm thuật toán
Tùy theo từng góc độ mà khái niệm thuật toán có thể được hiểu theo nhiều cáchkhác nhau, chẳng hạn:
Thuật toán là phương pháp giải quyết vấn đề nào đó theo từng bước
Thuật toán là các qui tắc để tính toán
Thuật toán là phương pháp giải quyết vấn đề thích hợp cho cài đặt trên máy tínhTrong tài liệu này khái niệm thuật toán được định nghĩa như sau:
Thuật toán là một dãy hữu hạn các bước hành động xác định để giải quyết một vấn
đề, quá trình thực hiện các bước hành động này phải dừng và cho kết quả như mong muốn.
Các đặc trưng của thuật toán
quả ở đầu ra
ra kết quả sau một số hữu hạn hành động.
Trang 6Tính đơn trị: Kết quả của mỗi hành động chỉ phụ thuộc vào kết quả của các hành
động được thực hiện trước đó và dữ liệu đầu vào Nói một cách khác, với đầu vào nhưnhau thuật toán sẽ cho đầu ra như nhau
hoặc máy) có thể thực hiện được
hoặc một bài toán với các đầu vào cụ thể khác nhau
Ngoài ra đối với một thuật toán còn có các yêu cầu về tính đúng đắn và tính hiệu
quả.
Mô tả thuật toán
Mô tả thuật toán là việc nêu ra các bước hành động cũng như trình tự thực hiệncác bước Mô tả thuật toán là công việc cực kỳ quan trọng Một mô tả tốt sẽ giúp ngườilập trình hình dung rõ ràng và đúng đắn các việc phải làm
Để mô tả một thuật toán ta có thể áp dụng một trong các cách sau:
thường cách mô tả này giúp hình dung thuật toán ở mức bao quát
trúc điều khiển của ngôn ngữ lập trình (Pascal chẳng hạn) để mô tả Trong cách mô tảnày ta chỉ sử dụng các từ khóa và cấu trúc với ý nghĩa đã biết của ngôn ngữ lập trìnhtương ứng chứ không hoàn toàn tuân thủ cú pháp của ngôn ngữ Cách mô tả này chophép diễn đạt các hành động một cách chi tiết, cụ thể hơn Khi dùng giả mã, để mô tảgọn ta sẽ bỏ qua một số từ khóa không cần thiết và sẽ viết các hành động cùng mức với
lề thụt vào như nhau, các hành động ở mức sâu hơn sẽ được viết với lề lớn hơn
Ví dụ để mô tả thuật toán tìm số lớn nhất trong một dãy số ta có thể có các mô tảnhư sau:
- Mô tả 1:
Trang 7Đầu vào: dãy a1, a2, , an
Đầu ra: x là giá trị lớn nhất trong dãy
Thuật toán: Xét lần lượt từng phần tử trong dãy kể từ đầu đến cuối dãy, với mỗiphần tử trong dãy xác định x là giá trị lớn nhất (tạm thời) cho tới thời điểm đó
- Mô tả 2:
Đầu vào: dãy a[1], a[2], , a[n]
Đầu ra: x là giá trị lớn nhất trong dãy
thuật toán bằng một ngôn ngữ lập trình cụ thể
Ví dụ: Phần lớn các chương trình có dạng tổng quát sau:
Nhập một số dữ liệu đầu vào.
Thực hiện những tính toán nào đó.
Tạo ra một số đầu ra thích hợp.
Khi mô tả thuật toán ta thường coi dữ liệu đầu vào là có sẵn
Trong tài liệu này chúng ta sử dụng cách mô tả các thuật toán bằng giả mã kết hợpvới ngôn ngữ tự nhiên
Một điểm cần lưu ý là với một thuật toán ta có thể mô tả nó ở nhiều mức độ khácnhau tùy theo tình huống Chẳng hạn ta có thể mô tả một thuật toán bằng một vài hànhđộng mà mỗi một hành động lại tương ứng với một thuật toán khác nào đó Thông
Trang 8thường ta sẽ mô tả một thuật toán qua nhiều mức, ngày càng chi tiết hơn hoặc mô tả chitiết hơn một số hành động nào đó nếu cần Với một bản mô tả thuật toán đủ chi tiết thì
việc viết chương trình chỉ là việc chuyển đổi cách diễn đạt thuật toán hiện có sang một
ngôn ngữ lập trình thích hợp.
Cũng cần phân biệt thuật toán và chương trình: Một chương trình sẽ được viết bằngmột ngôn ngữ lập trình cụ thể nào đó, do vậy chương trình phụ thuộc vào ngôn ngữ lậptrình (chẳng hạn giới hạn phạm vi của các kiểu dữ liệu của ngôn ngữ) và phụ thuộc vàocác yêu cầu phần cứng cụ thể (chẳng hạn giới hạn của bộ nhớ có thể sử dụng được, v.v.).Trong khi đó một thuật toán không quan tâm tới ngôn ngữ lập trình cụ thể nào và chỉquan tâm tới việc diễn đạt các hành động cần thực hiện Chính vì vậy khi cài đặt mộtthuật toán ta phải lưu ý tới những hạn chế của ngôn ngữ lập trình được sử dụng
2 Độ phức tạp tính toán và phân tích thuật toán.
Có nhiều cách tiếp cận khác nhau để đánh giá hiệu quả của một thuật toán nhằmchọn lựa thuật toán thích hợp áp dụng trong thực tế Chẳng hạn có thể xem xét thuật toánđòi hỏi những gì về tài nguyên hệ thống (bộ nhớ) hoặc thuật toán có thể thực hiện trong
một khoảng thời gian chấp nhận được hay không Ở đây chúng ta chỉ quan tâm tới độ
phức tạp tính toán tức là đánh giá thời gian thực hiện của thuật toán Việc đánh giá thời
gian thực hiện của thuật toán được gọi là phân tích thuật toán Phân tích thuật toán là cần
thiết vì những lý do sau:
Việc phân tích thuật toán đáng tin cậy hơn là thực nghiệm Nếu ta thực nghiệm(tức là chạy thử chương trình), ta chỉ biết hành vi của một chương trình đối với nhữngtrường hợp riêng lẻ, trong khi đó phân tích thuật toán cho ta biết về hiệu quả cho mọi đầuvào
Phân tích thuật toán giúp lựa chọn cách giải quyết trong số nhiều cách giải quyếtđối với bài toán Một bài toán có thể có nhiều cách giải quyết khác nhau Phân tích và sosánh cẩn thận các thuật toán giúp quyết định thuật toán nào là thích hợp nhất với mụcđích của chúng ta mà không cần phải cài đặt và kiểm thử tất cả
Phân tích thuật toán giúp tiên đoán hiệu quả của một chương trình trước khi viết
Trang 9Điều này là rất quan trọng đối với những chương trình lớn và giúp chúng ta có thể pháthiện và tập trung khắc phục những vấn đề làm chương trình kém hiệu quả.
Khi phân tích thuật toán ta sẽ quan tâm tới mối liên hệ giữa dữ liệu đầu vào (mà ta
gọi là kích thước đầu vào) với số lượng các thao tác cần thực hiệân của thuật toán.
Kích thước đầu vào thường là một số nguyên dương n Tuỳ theo tình huống cụ thể
mà ta coi cái gì là kích thước đầu vào Chẳng hạn n có thể là số lượng các đối tượng của
dữ liệu đầu vào hoặc n có thể là kích thước của miền xác định của một đối tượng, v.v.
Chẳng hạn nếu dữ liệu đầu vào là một dãy n số nguyên thì ta có thể coi n là kích thướcđầu vào, nếu đầu vào là một ma trận 2 chiều mn thì kích thước đầu vào có thể coi là hai
số nguyên dương m, n
Một điều hiển nhiên là thuật toán phải thực hiện càng nhiều thao tác thì thời gian
thực hiện thuật toán càng lớn Do vậy, ta sẽ coi số lượng các thao tác cơ bản cần thực
hiện là một hàm của kích thước đầu vào, ký hiệu là f(n), gọi là hàm thời gian chạy của thuật toán (chương trình) Các thao tác cơ bản thường dùng là các phép toán số học và so
sánh, phép gán, thao tác đọc file và ghi file Tuy nhiên với từng thuật toán, tùy theo tìnhhuống ta sẽ quan tâm tới một số thao tác cơ bản nhất định
Việc tính chính xác hàm f(n) trong phần lớn trường hợp là rất khó và thực ra là
không cần thiết Ta sẽ quan tâm tới tốc độ tăng của hàm f khi n tăng, hay nói khác đi ta
muốn biết mức độ tăng thời gian thực hiện thuật toán khi kích thước đầu vào tăng Đặc
biệt ta quan tâm tới tình huống trường hợp tồi nhất (khi số lượng các thao tác cơ bản cần
thực hiện là nhiều nhất)
Ký pháp O: Giả sử f, g là hai hàm N N, ta nói f có bậc cao nhất là g, ký hiệu f(n)
= O(g(n)) nếu tồn tại hai hằng số C và k sao cho f(n) < Cg(n) với mọi n > k Khi đó tanói hàm f có bậc (hay tốc độ tăng) là g
Ví dụ 1: Hàm T(n)=3n3+2n2 là O(n3) với k=0 và C=5
Ta cũng có thể nói rằng T(n) là O(n4) nhưng phát biểu này yếu hơn
Ví dụ 2: Ta chứng minh rằng hàm 3n không là O(2n)
Trang 10Nhưng ta biết rằng (3/2)n tiến ra vô cùng khi n ra vô cùng Mâu thuẫn.
Khi sử dụng ký pháp O ta đã bỏ qua các hằng số của bậc, điều này ám chỉ rằng ta
quan tâm tới những trường hợp mà kích thước đầu vào đủ lớn
Giả sử ta có 2 thuật toán với 2 hàm thời gian chạy là f1 và f2 tương ứng, vàf1(n)=100n, f2(n)=2n Cả hai thuật toán của chúng ta giải quyết một trường hợp cụ thểmất 104 giây Giả sử nhờ cải thiện phần cứng ta có thể tăng tốc độ máy lên 10 lần Khi đóvới thuật toán f2 ta có thể giải quyết bài toán với kích thước đầu vào tăng 30% với thờigian như cũ, trong khi với thuật toán f1 ta có thể giải quyết bài toán với kích thước đầuvào tăng 1000% với thời gian như cũ Thực tế là máy tính ngày càng rẻ hơn và nhanhhơn, nhưng nhu cầu thực tế giải quyết các bài toán với kích thước ngày càng lớn và càngphức tạp cũng tăng lên Vì vậy việc tìm ra và sử dụng các thuật toán có độ phức tạp tăngchậm ngày càng trở nên quan trọng hơn
Một số tính chất của ký pháp O:
Qui tắc cộng: Giả sử T1(n) và T2(n) là thời gian chạy của 2 thuật toán P1 và P2,
T1(n)=O(f(n)) và T2(n)=O(g(n)) Khi đó thời gian chạy tuần tự 2 thuật toán P1 và P2 là
T1(n)+T2(n) = O(max(f(n),g(n)))
Thật vậy giả sử C1, k1, C2, k2 là các hằng số sao cho với mọi n>k1 ta cóT1(n)<C1.f(n) và với mọi n>k2 có T2(n)<C2.g(n) Gọi k0=max(k1,k2), khi đó với mọin>k0 ta có T1(n)<C1.f(n) và T2(n)<C2.g(n)
Suy ra T1(n) + T2(n) < (C1+C2) max(f(n),g(n))
Vậy T1(n) + T2(n) = O(max(f(n),g(n)))
Hay O(f(n))+O(g(n)) = O(max(f(n),g(n)))
Áp dụng qui tắc này khi đánh giá thời gian chạy của một thuật toán gồm các đoạn
thực hiện tuần tự ta có thể coi thời gian chạy (hay độ phức tạp tính toán) của thuật toán
bằng thời gian chạy của đoạn chương trình có thời gian chạy lớn nhất
Một nhận xét khác là nếu g(n) < f(n) với n đủ lớn thì O(f(n)+g(n))=O(f(n)), ví dụO(n3+n2)=O(n3) Vì vậy khi xem xét các hàm đánh giá thời gian chạy của thuật toán ta
Trang 11chỉ quan tâm tới hạng tử bậc cao nhất.
Qui tắc nhân: Nếu T1(n)=O(f(n)) và T2(n)=O(g(n)) thì T1(n)T2(n)=O(f(n)g(n)) Từ qui
tắc này ta có O(c.f(n))=O(f(n)) với c là một hằng số
Khi xác định hàm thời gian chạy của thuật toán ta thường ước tính số các thao tác cơbản như phép gán, thao tác đọc ghi hoặc các tính toán số học, các phép so sánh Mỗi thaotác cơ bản này thường được coi là thực hiện mất một đơn vị thời gian (mặc dù trong thực
tế việc thực các thao tác này đòi hỏi thời gian khác nhau, chẳng hạn các thao tác đọc ghimất nhiều thời gian hơn cả, thực hiện phép nhân mất nhiều thời gian hơn thực hiện phépcộng, )
Ví dụ: Đánh giá thời gian chạy của thuật toán sau
Trang 12hiện j lần, do đó với mỗi i (chạy từ 1 đến n-1) số lần thực hiện Writeln là (i+1)+(i+2)+ +n = (n+i+1).(n-i)/2
Vậy tổng số lần thực hiện Writeln là:
(n+2).(n-1)/2+(n+3)(n-2)/2+…+2n/2 = O(n3)
Trong một số tình huống khác, khi ta ước lượng được số tối đa các thao tác cơ bảncần thực hiện trong mỗi bước lặp, thì để cho việc tính độ phức tạp đơn giản hơn, ta cóthể tính hàm thời gian chạy bằng cách đếm số lần lặp
3 Vấn đề lựa chọn thuật toán để cài đặt.
Với một bài toán có thể áp dụng nhiều thuật toán khác nhau để giải quyết Khi đónảy sinh vấn đề nên lựa chọn sử dụng thuật toán nào
Có 2 tiêu chuẩn quan trọng để lựa chọn một thuật toán: tính đơn giản và tính
hiệu quả Thuật toán đơn giản là thuật toán dễ hiểu, dễ cài đặt và dễ bảo trì, tuy vậy các
thuật toán đơn giản thường kém hiệu quả Ngược lại một thuật toán hiệu quả thường làphức tạp, vì vậy khó cài đặt cũng như khó bảo trì
Khi viết một chương trình không đòi hỏi tính hiệu quả hoặc số lần sử dụng ít người
ta thường ưu tiên chọn những thuật toán đơn giản Trong những tình huống như thế nàylợi ích thực tế do tính hiệu quả mang lại có thể không đáng kể so với chi phí cài đặt hoặc
do số lần sử dụng quá ít
Một khi chương trình được sử dụng nhiều cũng như yêu cầu thực tế về hiệu quả(chẳng hạn đòi hỏi về thời gian thực hiện chương trình càng nhỏ càng tốt như thường gặpvới các ứng dụng thời gian thực) thì lúc này các thuật toán hiệu quả được ưu tiên lựachọn Khi đó những chi phí cài đặt sẽ được bù lại bởi lợi ích thu được mỗi lần chạychương trình nhân với số lần chạy chương trình (có thể rất lớn)
Ví dụ: Giả sử cần viết một chương trình dùng trong một thời gian ngắn (10 lầntrong 10 ngày chẳng hạn) Có hai thuật toán, thuật toán đơn giản có thể cài đặt trong 2giờ, mỗi lần thực hiện chương trình tương ứng chạy trong 1 giờ Thuật toán hiệu quả, dophức tạp (khó thể hiện thuật toán, mất nhiều thời gian sửa các lỗi phát sinh) nên thời gian
Trang 13cài đặt là 2 ngày (có thể lâu hơn), tuy nhiên mỗi lần chạy chương trình tương ứng mất 1giây Xét về thời gian dành cho viết và sử dụng chương trình, đối với thuật toán đơn giản
là 12 giờ còn đối với thuật toán hiệu quả là 2 ngày Nhìn ở góc độ này thì nên ưu tiênchọn thuật toán đơn giản Tuy nhiên nếu như chương trình được sử dụng nhiều hơn,chẳng hạn khoảng 2400 lần thì lúc này đối với thuật toán đơn giản thời gian dành choviết và sử dụng chương trình là khoảng 100 ngày, trong khi đó đối với chương trình hiệuquả chỉ là hơn 2 ngày Trong tình huống này nên ưu tiên chọn thuật toán hiệu quả
Trang 14Thuật toán phải thực hiện bao nhiêu phép cộng? Bao nhiêu phép nhân?
2. Dùng ký pháp O, tính thời gian chạy tồi nhất của các thuật toán sau:
Trang 15Có thể nói gì về giá trị của các biến x và y khi kết thúc thuật toán? Có cách nào làmgiảm độ phức tạp của thuật toán trên hay không?
Trang 162 MỘT SỐ THUẬT TOÁN CƠ BẢN.
5 Các thuật toán sắp xếp sơ cấp.
Sắp xếp là một bài toán thường gặp, ngoài ý nghĩa tổ chức lại dữ liệu theo một yêucầu nào đó, việc sắp xếp còn tạo thuận lợi cho việc tìm kiếm
Ta sẽ quan tâm tới việc sắp xếp các bản ghi trong một file có kích thước nhỏ, mỗi
bản ghi chứa một trường khóa key dùng làm tiêu chuẩn để sắp xếp Các bản ghi sẽ được
sắp xếp sao cho trường khóa của chúng được sắp xếp theo một thứ tự nào đó xác địnhtrước
Nếu kích thước file nhỏ ta có thể sao nó ra một mảng và thực hiện việc sắp xếp trên
mảng là sắp Sắp xếp mảng còn được gọi là sắp xếp trong Việc sắp xếp file trên đĩa được gọi xếp ngoài Khi sắp xếp mảng các bản ghi được truy nhập trực tiếp, còn với sắp
xếp ngoài các bản ghi được truy nhập tuần tự nên việc sắp xếp gặp nhiều khó khăn hơn
Ở đây ta sẽ xem xét các thuật toán sắp xếp mảng Các phương pháp sắp xếp ngoài sẽđược xem xét trong giáo trình Cấu trúc dữ liệu
Kích thước dữ liệu đầu vào cho các phương pháp sắp xếp mảng là kích thước n củamảng (tức là số phần tử mảng)
Để thuận tiện cho việc trình bày cũng như tập trung vào thuật toán ta sẽ làm việc vớimảng đơn giản gồm các số nguyên
Các phương pháp sắp xếp mảng cơ bản: Có thể phân loại các phương pháp sơ cấp sắp xếp mảng thành 3 phương pháp cơ bản là sắp xếp chèn, sắp xếp chọn và sắp xếp đổi chỗ Một đặc trưng cần lưu ý của các phương pháp sắp xếp là tính ổn định Một
phương pháp sắp xếp được gọi là ổn định nếu như nó giữ nguyên thứ tự ban đầu của cácphần tử giống nhau
Đối với các thuật toán được xét ở đây chúng ta sẽ quan tâm tới việc sắp xếp mảng
A[1 n] các số nguyên theo thứ tự tăng dần của trường khóa.
Trang 17a/ Sắp xếp chèn
Có thể mô tả ngắn gọn như sau
For i:=2 to n do
chèn A[i] vào vị trí thích hợp trong các phần tử A[1],…, A[i-1]
Trong phương pháp này, ở bước thứ i ta có các phần tử từ A[1] đến A[i-1] đã đượcsắp thứ tự Để chèn A[i] vào vị trí thích hợp trong các phần tử A[1],…, A[i-1] ta sẽ tìm
vị trí j nhỏ nhất thỏa mãn A[i] < A[j] và chèn A[i] vào vị trí j Khi đó các phần tử từ vị trí
j đến i-1 sẽ dịch sang phải 1 vị trí Trong quá trình tìm vị trí j ta đồng thời dịch các phần
tử lớn hơn A[i] sang phải một vị trí Mô tả chi tiết như sau:
Trang 18b/ Sắp xếp chọn: có thể mô tả ngắn gọn như sau
For i:=1 to n-1 do
Chọn phần tử nhỏ nhất chưa đúng vị trí và đặt nó vào vị trí i
Khi sử dụng phương pháp sắp xếp chọn ta giả thiết các vị trí từ 1 đến i-1 trong mảng
đã chọn được các phần tử đúng vị trí Ta chọn phần tử nhỏ nhất chưa đúng vị trí nằmtrong khoảng từ vị trí i đến vị trí n và sắp nó vào vị trí i
Mô tả chi tiết:
{phần tử tại vị trí min là nhỏ nhất chưa được sắp}
Hoán vị (A[i], A[min])
Ví dụ: Giả sử mảng đã cho là A = (3, 6, 2, 8, 4, 5) Trong mỗi dòng dưới đây là tìnhtrạng của mảng sau khi kết thúc một vòng lặp ứng với i
Trang 19Mô tả chi tiết:
tự, sau mỗi lần thực hiện vòng lặp For với i không còn phần tử nào nhỏ hơn A[i] đứngsau nó
Ví dụ: Giả sử mảng đã cho là A = (4, 6, 3, 8, 2, 5) Trong mỗi dòng dưới đây là tìnhtrạng của mảng sau khi kết thúc một vòng lặp ứng với i
Ngoài ra có phương pháp sắp xếp đếm , nội dung của phương pháp này là đếm số
phần tử nhỏ hơn hoặc bằng A[i] Nếu có j phần tử nhỏ hơn hoặc bằng A[i] thì A[i] sẽ có
vị trí thứ j+1 trong dãy đã sắp thứ tự Khi đếm các phần tử bằng A[i] ta chỉ đếm nhữngphần tử đi trước nó để đảm bảo các số đếm là khác nhau
Mô tả:
Procedure Sxdem;
For i:=1 to n do Count[i]:=0;
For i:=n downto 2 do
Trang 20If a[i] < a[j] then Count[j]:=Count[j]+1Else Count[i]:=Count[i]+1;
For i:=1 to n do s[Count[i]+1]:=a[i];
Trang 227 Các thuật toán tìm kiếm sơ cấp trên mảng.
Tìm kiếm là một thao tác đóng vai trò quan trọng trong nhiều tính toán và được áp
dụng nhiều trong thực tế Có nhiều yêu cầu tìm kiếm khác nhau cũng như có nhiều thuậttoán tìm kiếm khác nhau Ta sẽ quan tâm tới một số thuật toán tìm kiếm sơ cấp trênmảng Bài toán đặt ra là tìm một phần tử trong mảng A[1 n] cho trước có giá trị bằng Tcho trước Có thể xét bài toán tổng quát hơn là tìm một phần tử trong mảng A[1 n] chotrước có tính chất P cho trước
a Tìm tuần tự: Để tìm kiếm tuần tự một phần tử T cho trước trong mảng A ta sử
dụng một vòng lặp, tại mỗi bước của vòng lặp ta thao tác với một phần tử xác định củamảng, ở đây cụ thể là so sánh lần lượt mỗi phần tử của mảng với T (hay kiểm tra xem
phần tử tương ứng có tính chất P hay không) Thao tác này được gọi là duyệt mảng Việc
duyệt mảng được dừng lại khi bắt gặp một phần tử bằng T hoặc đã so sánh hết các phần
tử của mảng Khi gặp một phần tử bằng T (hay có tính chất P) ta sẽ ghi nhận sự kiện này
Mô tả: Duyệt (so sánh mỗi phần tử mảng với T) toàn bộ mảng cho tới khi gặp mộtphần tử bằng T hoặc đã duyệt hết mảng
Biến Found dùng đ ể ghi nhận phần tử trong mảng bằng Tcó
Trong trường hợp tổng quát, điều kiện
If A[i]=Tđược thay bằng
If A[i] có tính chất P
Trang 23b Tìm nhị phân: Trong tình huống A là mảng sắp thứ tự ta có thuật toán tìm kiếm
tốt hơn tìm tuần tự, đó là tìm kiếm nhị phân Ý tưởng của thuật toán tìm nhị phân là ta sosánh T với phần tử ở giữa mảng A, dựa vào kết quả so sánh mà kết thúc hoặc tiếp tục tìmkiếm bằng cách thu hẹp phạm vi tìm kiếm còn bằng một nửa phạm vi trước đó Quá trìnhnày được thực hiện lặp đi lặp lại cho tới khi gặp một phần tử bằng T hoặc phạm vi tìmkiếm là rỗng
Mô tả thuật toán: Giả sử A[1 n] là mảng sắp thứ tự tăng
Else d:=g+1;
{tiếp tục tìm ở nửa phải phạm vi }
If co then
Phần tử bằng T có tại vị trí g trong mảng Else Không có T trong mảng.
Nếu T < A[g] thì với mọi chỉ số k > g ta có T < A[g] < A[k] (do mảng sắp thứ tựtăng), do đó ta chỉ cần tìm T trong mảng ứng với các chỉ số k < g Lúc này ta thu hẹpphạm vi tìm kiếm bằng cách xác định lại vị trí cuối của phạm vi là g-1
Nếu T > A[g] thì ta thực hiện một hành động tương tự là tìm T trong mảng ứng vớicác chỉ số k > g Lúc này ta thu hẹp phạm vi tìm kiếm bằng cách xác định lại vị trí đầucủa phạm vi là g+1
Trang 24Kết luận giá trị T có trong mảng tại vị trí thứ 3.
- Với T = 50 ta có các bước tìm kiếm được thể hiện như sauTrước khi vào vòng lặp: d=1, c=9, co = False
Trang 258 BÀI TẬP
1. Đánh giá độ phức tạp của các thuật toán tìm kiếm sơ cấp
2. Mô tả thuật toán Tìm phần tử lớn thứ nhì trong một dãy tuỳ ý có n phần tử và
đánh giá độ phức tạp của thuật toán
3. Chạy từng bước thuật toán tìm nhị phân trên một mảng cụ thể có 16 phần tử,xét hai trường hợp: phần tử cần tìm có trong mảng và không có trong mảng
4. Mô tả thuật toán Tìm phần tử xuất hiện nhiều lần nhất trong một dãy tuỳ ý có n
phần tử và đánh giá độ phức tạp của thuật toán
Trang 269. Đệ qui
2.5.1 Thuật toán đệ qui.
Các thuật toán đệ qui đóng vai trò quan trọng trong việc giải quyết nhiều bài toán
Một thuật toán đệ qui là thuật toán có yêu cầu thực hiện lại chính thuật toán đó với mức
độ dữ liệu thấp hơn Các thuật toán đệ qui có liên quan chặt chẽ với các định nghĩa đệ
qui hoặc các quan hệ truy hồi
Một thuật toán đệ qui gồm 2 phần:
Phần cơ sở: là các trường hợp không cần thực hiện lại thuật toán.
Phần đệ qui: là các trường hợp yêu cầu thực hiện lại thuật toán.
2.5.2 Một số bài toán
Bài toán 1: Tìm ƯCLN của hai số tự nhiên a và b cho trước.
Có nhiều thuật toán khác nhau để giải bài toán này, ở đây ta sử dụng một tính chấtcủa ƯCLN là: nếu a > b thì ƯCLN(a,b)=ƯCLN(a-b,b) Thuật toán được mô tả như sau:Function Ucln(a,b); {gỉả thiết b<>0}
If (a=b) or (b=1) then ucln:=b (* phần cơ sở *)
Bài toán 2: Định nghĩa các số Fibbonacci như sau: f0=1, f1=1, fn=fn-1+fn-2 với n>1.
Tính fn với n cho trước
Thuật toán đệ qui sử dụng ngay định nghĩa đệ qui của các số Fibbonacci, vì thế thuậttoán chỉ là diễn đạt lại định nghĩa
Trang 27Function Fib(n);
If (n=0) or (n=1) then Fib:=1
Else Fib:=Fib(n-1)+Fib(n-2);
Bài toán 3: Tháp Hà Nội Có 3 cột 1, 2, 3 Trên cột 1 có n đĩa bán kính khác nhau
được xếp theo thứ tự đĩa lớn nằm dưới, đĩa nhỏ nằm trên Các đĩa có thể được chuyển từcột này sang cột khác Qui tắc chuyển như sau:
Mỗi lần chỉ được chuyển 1 đĩa Không được để đĩa lớn nằm trên đĩa nhỏ
Viết thuật toán chuyển n đĩa từ cột 1 sang cột 3 dùng cột 2 làm trung gian
Để giải quyết bài toán ta chỉ cần giả thiết đã chuyển được n -1 đĩa nhỏ nhất từ cột 1tới cột 2 Khi đó ta chỉ cần chuyển đĩa lớn nhất từ cột 1 tới cột 3 sau đó chuyển n -1 đĩa
từ cột 2 tới cột 3
Như vậy ta cần một thủ tục có thể chuyển m đĩa nhỏ nhất từ cột i tới cột j Thủ tụcnày sẽ làm việc như sau: đầu tiên chuyển m-1 đĩa từ cột i tới cột 6-i-j, sau đó chuyển 1đĩa từ cột i tới cột j, cuối cùng chuyển m-1 đĩa từ cột 6-i-j tới cột j
Mô tả thủ tục như sau
Trang 282.5.3 Khử đệ qui.
Mỗi ngôn ngữ lập trình cho phép sử dụng đệ qui đều dùng một ngăn xếp các bản ghi
kích hoạt để ghi lại các giá trị của các biến thuộc về một thủ tục đang kích hoạt (tức là
thủ tục đang được yêu cầu thực hiện) Khi một thủ tục P được gọi, một bản ghi kích hoạtmới cho P được đặt vào ngăn xếp mà không cần biết đã có bản ghi kích hoạt nào kháccủa P trong ngăn xếp hay chưa Khi P kết thúc, bản ghi kích hoạt của nó phải ở trên đỉnhcủa ngăn xếp vì P không thể kết thúc chừng nào mà tất cả các thủ tục mà nó đã gọi chưakết thúc Vì vậy ta có thể lấy bản ghi kích hoạt cho lời gọi P để kiểm soát điểm mà P
được gọi (điểm này còn được gọi là địa chỉ trả lại, lưu trong bản ghi kích hoạt của P khi
gọi P) Như vậy có một cơ chế có thể sử dụng để loại bỏ đệ qui bằng cách tạo ra mộtngăn xếp thích hợp
Việc sử dụng ngăn xếp để loại bỏ đệ qui sẽ được xem xét trong giáo trình Cấu trúc
dữ liệu Ở đây ta chỉ xem xét một số tình huống loại bỏ đệ qui khi lời gọi đệ qui nằm ở
cuối thủ tục (còn gọi là đệ qui đuôi)
Nếu thủ tục P(x) có lời gọi đệ qui P(y) ở cuối thủ tục thì ta có thể thay thể lời gọi nàybằng phép gán x:=y sau đó nhảy tới đầu thủ tục P Ở đây y có thể là một biểu thức,
nhưng x phải là tham số truyền theo giá trị, sao cho giá trị của nó được lưu ở vị trí cục
bộ riêng biệt với lời gọi P
Điều kiện dừng vòng lặp sẽ tương ứng với phần cơ sở trong thủ tục đệ qui
Ví dụ 1: Khử đệ qui của thuật toán tìm ƯCLN của hai số tự nhiên a và b.
Thuật toán được mô tả như sau:
Trang 29Ví dụ 2: Khử đệ qui của thuật toán tìm các số Fibbonacci trong bài toán 2.
Thuật toán tìm các số Fibbonacci trong bài toán 2 có dạng đệ qui đuôi, vì vậy ta cóthể khử đệ qui như sau:
Trang 3010 BÀI TẬP
1. Sử dụng tính chất sau của ƯCLN để đưa ra một thuật toán đệ qui tìmƯCLN(a,b): Nếu a>b thì ƯCLN(a,b)=ƯCLN(a mod b,b) và ƯCLN(a,0)=a với a>0 Khử
đệ qui của thuật toán này
2. Mô tả thuật toán sắp xếp chèn dạng đệ qui
3. Thuật toán sau có đạt được ý định tìm và in ra số lớn nhất trong n số haykhông? Giải thích
5. Viết 2 thuật toán đệ qui tính tổng các số hạng của một dãy số
6. Một robot có thể đi các bước 1m hoặc 2m
Trang 31a/ Tính số cách đi để robot đi được n mét
b/ Liệt kê tất cả các cách đi đó
Mô tả thuật toán tương ứng với các yêu cầu trên
7. Một robot có thể đi các bước 1m hoặc 2m hoặc 3m
a/ Tính số cách đi để robot đi được n mét
b*/ Liệt kê tất cả các cách đi đó
Mô tả thuật toán tương ứng với các yêu cầu trên
8. Đổi cơ số Để chuyển biểu diễn của một số từ cơ số b sang cơ số 10 ta có thểlàm như sau: Giả sử ta đã biết cách chuyển một số nguyên với n chữ số trong cơ số b và
ta muốn chuyển một số nguyên có n+1 chữ số Viết 2 thuật toán đệ qui theo cách tiếp cậnnày (một cách gọi đệ qui theo n chữ số đầu, cách kia gọi đệ qui theo n chữ số cuối)
9. Viết thuật toán đệ qui in ra chuỗi đảo ngược của một chuỗi cho trước
10.Cho dãy m số nguyên tuỳ ý Viết thuật toán đệ qui tìm và gán cho B giá trị lớnnhất trong dãy, đồng thời tìm và gán cho N giá trị lớn thứ nhì trong dãy
11.Hàm f(n) xác định trên tập các số nguyên không âm như sau:
f(0)=0, f(1)=1, f(2n)=f(n), f(2n+1)= f(n)+f(n+1) với n=1, 2,
Mô tả hai thuật toán đệ qui và không đệ qui tính f(N) với N cho trước
12.*Xét bài toán Josephus: Có một nhóm N người Để chọn một người đi làm mộtnhiệm vụ đặc biệt người ta cho N người này đứng thành vòng tròn Bắt đầu đếm từngngười từ một vị trí nào đó theo chiều kim đồng hồ Mỗi khi gặp người thứ M thì loạingười đó ra khỏi vòng tròn Tiếp tục quá trình đếm cho tới khi chỉ còn lại một người Đóchính là người được chọn Ví dụ với N=9, M=5 và đếm từ 1 Khi đó thứ tự người bị loại
ra lần lượt là: 5, 1, 7, 4, 3, 6, 9, 2 Người còn lại là người thứ 8
Mô tả một thuật toán đệ qui cho biết người được chọn
Có thể tìm được một thuật toán không đệ qui giải bài toán này không?
Trang 32PHẦN 2
MỘT SỐ KỸ THUẬT THIẾT KẾ THUẬT TOÁN
Phần này giới thiệu một số kỹ thuật thiết kế thuật toán như Chia để trị, Qui hoạchđộng, Thuật toán Tham lam, v.v đồng thời giới thiệu các bài toán điển hình có áp dụngcác kỹ thuật này
1 CHIA ĐỂ TRỊ
11. Mở đầu
Chia để trị là một kỹ thuật thiết kế thuật toán bằng cách chia bài toán đã cho thành
một số bài toán con hoàn toàn tương tự nhưng với kích thước đầu vào nhỏ hơn, giải
quyết lần lượt và độc lập các bài toán con này (có thể áp dụng kỹ thuật này đối với các
bài toán con), sau đó kết hợp các lời giải con để nhận được lời giải cho bài toán ban đầu.
Có thể mô tả một cách tổng quát kỹ thuật này như sau:
Function DQ(x);
{trả lại một lời giải với đầu vào x}
If x đủ nhỏ hoặc đơn giản then return ADHOC(x)
Else
Tách x thành các đầu vào nhỏ hơn x1, …, xk;
For i:=1 to k do yi:=DQ(xi);
Kết hợp các yi để thu được yReturn y;
Trong đó ADHOC(x) là một thuật toán cơ sở được dùng để giải quyết bài toán vớinhững đầu vào nhỏ
Số k trong mô tả thường nhỏ và cũng độc lập với đầu vào cụ thể Khi k=1 thì kỹ
Trang 33thuật này được gọi là kỹ thuật đơn giản hoá.
Để áp dụng được kỹ thuật chia để trị cần có một số điều kiện: phải có khả năng táchđầu vào thành những đầu vào nhỏ hơn và có khả năng kết hợp các lời giải con lại mộtcách hiệu quả Phải quyết định khi nào thì sử dụng thuật toán cơ sở
Các thuật toán chia để trị, do cách tiếp cận, thường có mô tả tự nhiên dạng đệ qui
Theo cách tiếp cận chia để trị ta sẽ chia phạm vi tìm kiếm thành 2 phần và xác định
sẽ tiếp tục tìm kiếm ở phần nào Do tính chất sắp thứ tự tăng của mảng nên ta thấy:
- Nếu với chỉ số i nào đó mà T[i] < x thì với mọi chỉ số j < i ta cũng có T[j] <
Nếu như x > T[g] thì phạm vi tìm kiếm tiếp tục sẽ là [g,c]
Ngược lại, nếu x T[g] thì phạm vi tìm kiếm tiếp tục sẽ là [d,g]
Thuật toán có thể mô tả như sau:
Procedure TimNP2(T,i,j,x);
{thủ tục này được gọi khi T[i] < x T[j] và ij }
If (i=j) and (T[i]= x) then vị trí:= i
Else
Trang 34g:=(i+j+1) div 2;
if x T[g] then TimNP2(T,i,g,x)else TimNP2(T,g,j,x);
bằng nhau và sắp xếp các phần này bằng lời gọi đệ qui, sau đó trộn các kết quả, lưu ý
bảo toàn thứ tự Công việc chủ yếu chỉ là trộn 2 mảng sắp thứ tự thành 1 mảng sắp thứ
tự Mô tả thuật toán như sau:
Trang 3514 Quicksort.
Khác với sắp xếp trộn, phần không đệ qui của thuật toán này không kết hợp các lời
giải con mà là đi xây dựng các bài toán con
Bước đầu tiên thuật toán chọn một phần tử trong mảng làm mốc (pivot) Sau đó mảng
được chia thành hai phần ở hai phía của mốc: các phần tử lớn hơn mốc được đưa sangphải và các phần tử không lớn hơn mốc được đưa sang trái mốc Nếu bây giờ mỗi phầnđược sắp thứ tự nhờ lời gọi đệ qui thì toàn bộ mảng sẽ được sắp thứ tự mà không cầntrộn
Lưu ý rằng có nhiều biến thể của thuật toán QuickSort, chủ yếu khác nhau ở phươngpháp chọn phần tử để đặt mốc Ở đây ta sẽ chọn phần tử đầu tiên của mảng làm mốc Mô tả thuật toán:
Trong đó thủ tục Datmoc sẽ chia mảng T thành 2 phần như mong muốn Ta sẽ thựchiện điều này bằng cách duyệt mảng T đồng thời từ hai đầu Dùng 2 biến k và h để duytrì 2 vị trí duyệt, khởi tạo k:=i và h:=j+1 Tăng k cho đến khi T[k] > p và giảm h cho tớikhi T[h] p Khi đó đổi chỗ T[k] và T[h] Quá trình này còn tiếp tục khi nào mà k<h.Cuối cùng đổi chỗ T[i] và T[h] để đặt mốc đúng chỗ
Trong thuật toán này ta đã chọn thuật toán sắp xếp chèn làm thuật toán sắp xếp cơsở
Thủ tục Datmoc có thể mô tả như sau:
Trang 36Khi đó theo mô tả của thuật toán ta có h(n) = 2h(n/2)+n
Ta có h(n) = 2(2h(n/4)+n/2)+n = 4h(n/4) + 2n = = nh(1)+kn =kn
Do đó ta có h(n) = O(n.logn)
15 Tính luỹ thừa
Nhập môn về Mật mã: vì một lý do nào đó An và Bình muốn thiết lập một bí mật
chung Họ chỉ có thể giao tiếp với nhau qua điện thoại Tuy nhiên họ biết chắc rằng mọi
cuộc điện thoại của họ đều bị C nghe lén (ta nói An và Bình giao tiếp qua một kênh
không an toàn)
Vấn đề đặt ra là làm thế nào An và Bình có thể thiết lập được một bí mật chung mà
C không thể biết được bí mật này mặc dù C có thể biết hết nội dung mọi cuộc trao đổi
giữa An và Bình? Nói khác đi là có hay không một nghi thức để An và Bình có thể công
khai tạo ra một khoá bí mật an toàn Một khoá bí mật được gọi là an toàn nếu thời gian
để tìm ra khoá là không chấp nhận được
Trang 37Trong Mật mã học có một yêu cầu khác liên quan tới việc tạo ra khoá bí mật là việctạo ra khoá phải tương đối đơn giản và không mất nhiều thời gian.
Vấn đề này lần đầu tiên được Diffie và Hellman giải quyết vào năm 1976 Cách làm
như sau: Đầu tiên An và Bình thoả thuận chọn một số nguyên p nào đó có hàng trăm
chữ số và một số nguyên g, 2<g<p-1 Tất nhiên là C biết rõ hai số này Tiếp theo An và
Bình, độc lập với nhau, mỗi người chọn ngẫu nhiên một số là A và B tương ứng
Sau đó An tính số a=gA mod p và gửi số a cho Bình, tương tự Bình tính và gửi cho
An số b=gB mod p Cuối cùng An tính x= bA mod p và Bình tính y=aB mod p
Dễ thấy x=y và không ai có thể kiểm soát trước được giá trị này Đây có thể coi là bímật chung của An và Bình mà họ hy vọng là C sẽ không biết được
Dễ dàng chứng minh được nếu A’ khác với A mà gA mod p = gA’ mod p thì bA’mod p = bA mod p với b=gB mod p Việc tính A’ từ p, g và a được gọi là vấn đềlogarithm rời rạc
Có một thuật toán hiển nhiên để tính A’ như sau:
Thuật toán trả lại giá trị 0 nếu không có A sao cho a=gA mod p Chẳng hạn không có
A sao cho 3=2A mod 7
Tuy nhiên thuật toán này đòi hỏi một thời gian không chấp nhận được vì trung bình
nó đòi hỏi khoảng p/2 lần thực hiện vòng lặp Nếu mỗi lần lặp được thực hiện trong 1micro giây và thậm chí nếu p chỉ có khoảng 30 chữ số thì thời gian thực hiện p/2 lần lặpcũng mất khoảng 4 tỉ năm Cho tới nay vẫn chưa có thuật toán nào có thể tính x từ p, g, a
Trang 38và b mà không phải tính logarithm rời rạc
Vì vậy hiện nay C vẫn không thể biết được bí mật của An và Bình mặc dù người tavẫn chưa thể chứng minh được điều này Nói cách khác độ an toàn của mật mã khoácông khai vẫn còn được đảm bảo chừng nào người ta còn chưa tìm ra được thuật toánhiệu quả hơn để tính x từ p, g, a và b mà không phải tính logarithm rời rạc
Nếu C sử dụng thuật toán nói trên để tìm A thì An và Bình cũng không thể sử dụngthuật toán tương tự như vậy để tính a, b, x và y Chẳng hạn như thuật toán sau
Trang 39While n>0 do
If n mod 2 =1 then x:=x*y mod p;
y:= y*y mod p;
leû A neáu
0 A
1
) 1 ( 1
0 )
(
div A h
A h A
h
Gọi s là độ dài của dãy nhị phân biểu diễn số A với A>1, khi đó dễ dàng chứng minhđược s h(A) 2s Như vậy với p, A, B có 200 chữ số An chỉ phải thực hiện khôngquá 3000 phép nhân các số có 200 chữ số và không quá 3000 phép nhân các số có 400chữ số Điều này là hoàn toàn có thể chấp nhận được
Ta minh họa qua một ví dụ: tính g35
Trang 4016 BÀI TẬP
1. Mô tả thuật toán trộn hai mảng sắp thứ tự thành một mảng sắp thứ tự Đánh giá
độ phức tạp của thuật toán Từ đó đánh giá độ phức tạp của thuật toán sắp xếp trộn
2. Mô tả thuật toán chèn một phần tử T vào một mảng A sắp thứ tự tăng chotrước sao cho mảng vẫn còn sắp thứ tự
3. Trong thuật toán sắp xếp trộn, thay vì chia mảng T thành 2 phần, ta sẽ chia nóthành 3 phần Hãy mô tả thuật toán tương ứng
1 0
Giả sử i, j là hai số nguyên Cái gì làtích của vectơ (i,j) và ma trận F? Điều gì xảy ra nếu i, j là hai số Fibbonacci liên tiếp?Dùng kỹ thuật chia để trị tính số Fibbonacci thứ n
Gợi ý: áp dụng kỹ thuật chia để trị để tính luỹ thừa.
5. Giả sử T[1 n] là mảng n phần tử Dễ dàng tìm phần tử lớn nhất của T bằngcách thực hiện n-1 phép so sánh như sau
If min > T[i] then min:=T[i];
Hãy đưa ra một thuật toán có thể tìm cả phần tử lớn nhất và phần tử nhỏ nhất củamảng gồm n phần tử bằng cách thực hiện ít hơn 2n-3 phép so sánh Có thể giả thiết n làluỹ thừa của 2