Tải bản đầy đủ (.pdf) (79 trang)

tổ chức dữ liệu cho lớp thuật toán chia để trị và ứng dụng

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (1.61 MB, 79 trang )


Số hóa bởi Trung tâm Học liệu

1
ĐẠI HỌC THÁI NGUYÊN
TRƢỜNG ĐẠI HỌC CÔNG NGHỆ THÔNG TIN&TRUYỀN THÔNG



Đỗ Tuấn Anh


TỔ CHỨC DỮ LIỆU CHO LỚP THUẬT TOÁN
CHIA ĐỂ TRỊ VÀ ỨNG DỤNG
Chuyên ngành: Khoa học máy tính
Mã số: 60.48.01




TÓM TẮT LUẬN VĂN THẠC SĨ KHOA HỌC MÁY TÍNH







Thái Nguyên - 2014

Số hóa bởi Trung tâm Học liệu



2
LỜI CAM ĐOAN

Tôi xin cam đoan đây là công trình nghiên cứu của bản thân, được xuất phát từ yêu
cầu phát sinh trong công việc để hình thành hướng nghiên cứu. Các số liệu có nguồn gốc
rõ ràng tuân thủ đúng nguyên tắc và kết quả trình bày trong luận văn được thu thập được
trong quá trình nghiên cứu là trung thực chưa từng được ai công bố trước đây.

Thái Nguyên, ngày 19 tháng 5 năm 2014
Học viên thực hiên


Đỗ Tuấn Anh




Số hóa bởi Trung tâm Học liệu

3



LỜI CẢM ƠN

Đầu tiên, em xin gửi lời cảm ơn sâu sắc nhất đến cán bộ hướng dẫn khoa học, thầy
giáo, PGS.TSKH Nguyễn Xuân Huy, người đã truyền cho em nguồn cảm hứng nghiên cứu
khoa học, người đã định hướng cho em đến với lĩnh vực nghiên cứu này.
Em xin bày tỏ lời cảm ơn tới các thầy giáo, cô giáo đã giảng dạy em trong suốt hai

năm học qua. Em cũng muốn gửi lời cảm ơn tới những thành viên lớp đã có những góp ý
chuyên môn cũng như sự động viên về tinh thần rất đáng trân trọng.
Cuối cùng, em xin gửi lời cảm ơn sâu sắc tới tất cả người thân trong gia đình và
những bạn bè em với những động viên dành cho em trong công việc và trong cuộc sống.

Học viên thực hiện luận văn



Đỗ Tuấn Anh

Số hóa bởi Trung tâm Học liệu

4


MỤC LỤC
Trang
Lời cam đoan
Lời cảm ơn
Mục lục iii
Danh mục các bảng v
Danh mục các hình vẽ v

MỞ ĐẦU 1
CHƢƠNG 1. CÁC CHIẾN LƢỢC THIẾT KẾ THUẬT TOÁN 2
1.1 Các bước cơ bản khi giải bài toán trên máy tính 2
1.2 Phân tích thời gian thực hiện thuật toán 6
1.2.1 Độ phức tạp thuật toán 6
1.2.2 Xác định độ phức tạp của thuật toán 9

1.2.3 Ký hiệu Big-O và biểu diễn thời gian chạy của thuật toán 10
1.2.4 Độ phức tạp thuật toán với tình trạng dữ liệu vào 13
1.2.5 Chi phí thực hiện thuật toán 13
CHƢƠNG 2. TỔ CHỨC DỮ LIỆU CHO LỚP THUẬT TOÁN CHIA ĐỂ TRỊ 14
2.1 Chiến lược chia để trị 14
2.2 Tổ chức dữ liệu cho lớp thuật toán chia để trị 15
2.3 Định lý tổng quát tính độ phức tạp các thuật toán chia để trị 16
2.4 Một số lớp bài toán điển hình 17
2.4.1 Lớp bài toán tìm kiếm 18
2.4.1.1 Thuật toán tìm kiếm nhị phân 18
2.4.1.2 Bài toán tìm Max và min 20
2.4.2 Lớp bài toán sắp xếp 22
2.4.2.1 Thuật toán sắp xếp trộn (Merge Sort) 22
2.4.2.2 Thuật toán sắp xếp nhanh (Quick Sort) 24
2.4.3 Lớp bài toán tối ưu 27
2.4.3.1 Bài toán dãy con dài nhất 27
2.3.3.2 Bài toán tháp Hà Nội 29
2.3.3.5 Bài toán xếp lịch thi đấu 30
CHƢƠNG 3. ỨNG DỤNG THUẬT TOÁN CHIA ĐỂ TRỊ GIẢI BÀI TOÁN
NHÂN HAI SỐ NGUYÊN LỚN 32
3.1 Mô tả bài toán 32
3.2 Thuật toán nhân tự nhiên 32
3.3 Thuật toán nhân cơ bản 33

Số hóa bởi Trung tâm Học liệu

5
3.4 Thuật toán nhân Karatsuba-Ofman 35
3.5 Thuật toán nhân dựa trên biến đổi Fourier nhanh 37
3.6 Thuật toán nhân chia để trị 40

3.6.1 Ý tưởng chung 40
3.6.2 Phân tích thuật toán 41
3.6.3 Mô hình thuật toán chia để trị cho bài toán nhân hai số nguyên lớn 44
3.6.4 So sánh độ phức tạp giữa các thuật toán 46
3.7 Tổ chức dữ liệu cho thuật toán chia để trị 46
3.7.1 Biểu diễn dưới dạng bit 46
3.7.2 Biểu diễn dùng mảng và xâu 47
3.8 Thực nghiệm và đánh giá 51
3.8.1 Cài đặt trên C 51
3.8.2 Cài đặt trên C# 59
KẾT LUẬN VÀ HƢỚNG PHÁT TRIỂN 64
TÀI LIỆU THAM KHẢO 65



Số hóa bởi Trung tâm Học liệu

6


DANH MỤC CÁC BẢNG
Bảng 1.1 Các lớp độ phức tạp tính toán 11
Bảng 1.2 Thời gian chạy của các lớp thuật toán 12
Bảng 2.1 Độ phức tạp của thuật toán tìm kiếm nhị phân 20
Bảng 2.2 Độ phức tạp của thuật toán sắp xếp nhanh 26
Bảng 3.1 So sánh độ phức tạp tính toán của các thuật toán nhân 46

DANH MỤC CÁC HÌNH
Hình 2.1 Thuật toán chia để trị 14
Hình 2.2 Tổ chức dữ liệu cho lớp bài toán chia để trị 15

Hình 2.3 Ví dụ thuật toán sắp xếp trộn 23
Hình 3.1 Thuật toán nhân Brute-force 33
Hình 3.2 Thuật toán nhân chuẩn 34
Hình 3.3 Thuật toán nhân SRMA 35
Hình 3.4 Thuật toán nhân Karatsuba-Ofman 37
Hình 3.5 Thuật toán nhân FFT 39
Hình 3.6 Thuật toán nhân chia để trị 45
Hình 3.7 Phép nhân chia để trị tổ chức dưới dạng bit 46
Hình 3.8 Thuật toán nhân chia để trị biểu diễn bit 47
Hình 3.9 Ví dụ về phép chia Ấn Độ







Số hóa bởi Trung tâm Học liệu

7
MỞ ĐẦU

.Ngày nay phương pháp này vẫn còn được áp dụng trong nhiều lĩnh vực của đời
sống. Đặc biệt, phương pháp này rất hiệu quả khi thiết kế thuật toán giải các bài toán lớn,
phức tạp. Với bài toán đầu vào rất lớn ta chia thành những phần nhỏ hơn và tìm lời giải
cho các bài toán nhỏ riêng biệt này, rồi sau đó tổng hợp các nghiệm riêng rẽ thành nghiệm
bài toán toàn cục.
Trong luận văn này, tôi sẽ tập trung phân tích việc tổ chức dữ liệu cho lớp thuật toán
chia để trị và cách đánh giá độ phức tạp đối với các thuật toán chia để trị.Với mục tiêu
chính là áp dụng thiết kế thuật toán chia để trị để giải quyết bài toán nhân hai số nguyên

lớn, luận văn được trình bày trong 3 chương với bố cục như sau:
Chƣơng 1: Các chiến lƣợc thiết kế thuật toán. Giới thiệu tổng quan về các bước
giải bài toán trên máy tính và phân tích đánh giá thời gian thực hiện thuật toán cùng các
chiến lược thiết kế thuật toán cơ bản.
Chƣơng 2: Tổ chức dữ liệu cho lớp thuật toán chia để trị.Trình bày ý tưởng, cơ sở
khoa học của thuật toán chia để trị và cách thức tổ chức dữ liệu cho thuật toán chia để trị
với các bài toán kinh điển.
Chƣơng 3: Ứng dụng thuật toán chia để trị giải bài toán nhân hai số nguyên lớn.
Tập trung phân tích các cách tiếp cận giải bài toán nhân hai số nguyên lớn. Từ đó đề xuất
thuật toán dựa trên tư tưởng chia để trị để giải quyết và thực nghiệm so sánh với các cách
tiếp cận trước đó.
Cuối cùng là kết luận và hƣớng phát triển:Tóm tắtnhững kết quả đạt được, những
hạn chế và nêu lên các hướng phát triển trong tương lai.

Số hóa bởi Trung tâm Học liệu

8


CHƢƠNG 1. CÁC CHIẾN LƢỢC THIẾT KẾ THUẬT TOÁN

1.1 Các bƣớc cơ bản khi giải bài toán trên máy tính
Một thuật toán một thủ tục tính toán được định nghĩa chính xác, mà lấy một giá trị
hoặc một tập các giá trị, được gọi là đầu vào hay dữ liệu vào và tạo ra một giá trị, hoặc
một tập các giá trị, và gọi là đầu ra. Miêu tả một vấn đề thường được xác định nói
chungqua quan hệ đầu vào/đầu ra. Một thuật toán là một dãy bước xác định để chuyển đổi
dữ liệu đầu vào thành dữ liệu đầu ra. Chúng ta có thể xem một thuật toán như một công cụ
để giải quyết một vấn đề tính toán. Việc trình bày rõ ràng một vấn đề nói chung hình
thành mối quan hệ mong muốn đầu vào/đầu ra. Một thuật toán mô tả chính xác một thủ tục
tính toán để đạt được mối liên hệ giữa dữ liệu đầu vào và dữ liệu đầu ra.

1.1.1 Xác định bài toán
Việc xác định bài toán tức là phải xác định xem ta phải giải quyết vấn đề gì? với giả
thiết nào đã cho và lời giải cần phải đạt những yêu cầu nào. Khác với bài toán thuần tuý
toán học chỉ cần xác định rõ giả thiết và kết luận chứ không cần xác định yêu cầu về lời
giải, đôi khi những bài toán tin học ứng dụng trong thực tế chỉ cần tìm lời giải tốt tới mức
nào đó, thậm chí là tồi ở mức chấp nhận được. Bởi lời giải tốt nhất đòi hỏi quá nhiều thời
gian và chi phí.
Input → Process → Output
(Dữ liệu vào →Xử lý →Kết quả ra)
Ví dụ: Khi cài đặt các hàm số phức tạp trên máy tính. Nếu tính bằng cách khai triển
chuỗi vô hạn thì độ chính xác cao hơn nhưng thời gian chậm hơn hàng tỉ lần so với
phương pháp xấp xỉ. Trên thực tế việc tính toán luôn luôn cho phép chấp nhận một sai số
nào đó nên các hàm số trong máy tính đều được tính bằng phương pháp xấp xỉ của giải
tích số.
Xác định đúng yêu cầu bài toán là rất quan trọng bởi nó ảnh hưởng tới cách thức giải
quyết và chất lượng của lời giải. Một bài toán thực tế thường cho bởi những thông tin khá
mơ hồ và hình thức, ta phải phát biểu lại một cách chính xác và chặt chẽ để hiểu đúng bài
toán. Trên thực tế, ta nên xét một vài trường hợp cụ thể để thông qua đó hiểu được bài
toán rõ hơn và thấy được các thao tác cần phải tiến hành. Đối với những bài toán đơn giản,
đôi khi chỉ cần qua ví dụ là ta đã có thể đưa về một bài toán quen thuộc để giải.
1.1.2 Tìm cấu trúc dữ liệu biểu diễn bài toán

Số hóa bởi Trung tâm Học liệu

9
Khi giải một bài toán, ta cần phải định nghĩa tập hợp dữ liệu để biểu diễn tình trạng
cụ thể. Việc lựa chọn này tuỳ thuộc vào vấn đề cần giải quyết và những thao tác sẽ tiến
hành trên dữ liệu vào. Có những thuật toán chỉ thích ứng với một cách tổ chức dữ liệu nhất
định, đối với những cách tổ chức dữ liệu khác thì sẽ kém hiệu quả hoặc không thể thực
hiện được. Chính vì vậy nên bước xây dựng cấu trúc dữ liệu không thể tách rời bước tìm

kiếm thuật toán giải quyết vấn đề.
Các tiêu chuẩn khi lựa chọn cấu trúc dữ liệu:
- Phải biểu diễn được đầy đủ các thông tin nhập và xuất của bài toán
- Phù hợp với các thao tác của thuật toán mà ta lựa chọn để giải quyết bài toán.
- Phải cài đặt được trên máy tính với ngôn ngữlập trình đang sửdụng.
Đối với một sốbài toán, trước khi tổchức dữliệu ta phải viết một đoạn chương trình
nhỏ để khảosátxem dữliệu cần lưu trữlớn tới mức độnào.
1.1.3 Xây dựng thuật toán
Thuật toán là một hệ thống chặt chẽ và rõ ràng các quy tắc nhằm xác định một dãy
thao tác trên cấu trúc dữ liệu sao cho: Với một bộ dữ liệu vào, sau một số hữu hạn bước
thực hiện các thao tác đã chỉ ra, ta đạt được mục tiêu đã định.Các đặc trưng của thuật toán:
1. Tính đơn định: Ở mỗi bước của thuật toán, các thao tác phải hết sức rõ ràng,
không gây nên sự nhập nhằng, lộn xộn, tuỳ tiện, đa nghĩa. Thực hiện đúng các
bước của thuật toán thì với một dữ liệu vào, chỉ cho duy nhất một kết quả ra.
2. Tính dừng: Thuật toán không được rơi vào quá trình vô hạn, phải dừng lại và
cho kết quả sau một số hữu hạn bước.
3. Tính đúng: Sau khi thực hiện tất cả các bước của thuật toán theo đúng quá trình
đã định, ta phải được kết quả mong muốn với mọi bộ dữ liệu đầu vào. Kết quả
đó được kiểm chứng bằng yêu cầu bài toán.
4. Tính phổ dụng: Thuật toán phải dễ sửa đổi để thích ứng được với bất kỳ bài toán
nào trong một lớp các bài toán và có thể làm việc trên các dữ liệu khác nhau.
5. Tính khả thi:Đối với một bài toán, có thể có nhiều thuật toán nhưng chúng phải
cho cùng một output đối với một input. Tuy nhiên chúng có thể khác nhau về
hiệu quả. Hiệu quả thời gian là tốc độ xử lý là nhanh hay chậm. Ta có thể đánh
giá căn cứ vào số bước thực hiện. Hiệu quả không gian là không gian lưu trữ
theo số các đối tượng dùng để ghi nhớ các kết quả (kể cả kết quả trung gian).
Trong Tin học có cả một ngành chuyên đánh giá độ phức tạp của giải thuật, chủ yếu
là đánh giá về hiệu quả thời gian. Thực tế sử dụng cho thấy thách thức về không gian lưu
trữ có thể giải quyết dễ hơn thách thức về thời gian thực hiện.


Số hóa bởi Trung tâm Học liệu

10
1.1.4 Lập trình
Sau khi đã có thuật toán, ta phải tiến hành lập trình thể hiện thuật toán đó. Muốn lập
trình đạt hiệu quả cao, cần phải có kỹ thuật lập trình tốt. Kỹ thuật lập trình tốt thể hiện ở
kỹ năng viết chương trình, khả năng gỡ rối và thao tác nhanh. Lập trình tốt không phải chỉ
cần nắm vững ngôn ngữ lập trình là đủ mà phải biết cách viết chương trình uyển chuyển,
phát triển từng bước để chuyển các ý tưởng ra thành chương trình hoàn chỉnh. Kinh
nghiệm cho thấy một thuật toán hay nhưng do cài đặt vụng về nên khi chạy lại cho kết quả
sai hoặc tốc độ chậm.
Thông thường, chúng ta không nên cụ thể hoá ngay toàn bộ chương trình mà nên tiến
hành theo phương pháp tinh chế từng bước (Stepwiserefinement):
- Ban đầu, chương trình được thể hiện bằng ngôn ngữ tự nhiên, thể hiện thuật
toán với các bước tổng thể, mỗi bước nêu lên một công việc phải thực hiện.
- Một công việc đơn giản hoặc là một đoạn chương trình đã được học thuộc thì
ta tiến hành viết mã lệnh ngay bằng ngôn ngữ lập trình.
- Một công việc phức tạp thì ta lại chia ra thành những công việc nhỏ hơn để lại
tiếp tục với những công việc nhỏ hơn đó.
Trong quá trình tinh chế từng bước, ta phải đưa ra những biểu diễn dữ liệu. Như vậy
cùng với sự tinh chế các công việc, dữ liệu cũng được tinh chế dần, có cấu trúc hơn, thể
hiện rõ hơn mối liên hệ giữa các dữ liệu.Phương pháp tinh chếtừng bước là một thểhiện
của tưduy giải quyết vấn đềtừtrên xuống, giúpcho người lập trình có được một định hướng
thểhiện trong phong cách viết chương trình. Tránhviệc mò mẫm, xoá đi viết lại nhiều lần,
biến chương trình thành tờgiấy nháp.
1.1.5 Chạy và kiểm thử
1.1.5.1 Chạy thử và tìm lỗi
Chương trình là do con người viết ra, mà đã là con người thì ai cũng có thể nhầm lẫn.
Một chương trình viết xong chưa chắc đã chạy được ngay trên máy tính để cho ra kết quả
mong muốn. Kỹ năng tìm lỗi, sửa lỗi, điều chỉnh lại chương trình cũng là một kỹ năng

quan trọng của người lập trình. Kỹ năng này chỉ có được bằng kinh nghiệm tìm và sửa
chữa lỗi của chính mình.Có ba loại lỗi:
- Lỗi cú pháp: Lỗi này hay gặp nhất nhưng lại dễ sửa nhất, chỉ cần nắm vững
ngôn ngữ lập trình là đủ. Một người được coi là không biết lập trình nếu không
biết sửa lỗi cú pháp.
- Lỗi cài đặt: Việc cài đặt thể hiện không đúng thuật toán đã định, đối với lỗi này
thì phải xem lại tổng thể chương trình, kết hợp với các chức năng gỡ rối để sửa
lại cho đúng.

Số hóa bởi Trung tâm Học liệu

11
- Lỗi thuật toán: Lỗi này ít gặp nhất nhưng nguy hiểm nhất, nếu nhẹ thì phải điều
chỉnh lại thuật toán, nếu nặng thì có khi phải loại bỏ hoàn toàn thuật toán sai và
làm lại từ đầu.
1.1.5.2 Xây dựng các bộ dữ liệu kiểm tra
Có nhiều chương trình rất khó kiểm tra tính đúng đắn. Nhất là khi ta không biết kết
quả đúng là thế nào? Vì vậy nếu như chương trình vẫn chạy ra kết quả thì việc tìm lỗi rất
khó khăn. Khi đó ta nên làm các bộ dữ liệu test để thử chương trình của mình. Kinh
nghiệm khi xây dựng các bộ dữ liệu test là:
- Bắt đầu với một bộ test nhỏ, đơn giản, làm bằng tay cũng có được đáp số để so
sánh với kết quả chương trình chạy ra.
- Tiếp theo vẫn là các bộ test nhỏ, nhưng chứa các giá trị đặc biệt hoặc tầm
thường. Kinh nghiệm cho thấy đây là những test dễ sai nhất.
- Các bộ test phải đa dạng, tránh sự lặp đi lặp lại các bộ test tương tự.
- Có một vài test lớn chỉ để kiểm tra tính chịu đựng của chương trình mà thôi. Kết
quả có đúng hay không thì trong đa số trường hợp, ta không thể kiểm chứng
được với test này.
Lưu ý rằng chương trình chạy qua được hết các test không có nghĩa là chương trình
đó đã đúng. Bởi có thể ta chưa xây dựng được bộ test làm cho chương trình chạy sai. Vì

vậy nếu có thể, ta nên tìm cách chứng minh tính đúng đắn của thuật toán và chương trình,
điều này thường rất khó.
1.1.6 Tối ƣu chƣơng trình
Một chương trình đã chạy đúng không có nghĩa là việc lập trình đã xong, ta phải tiếp
tục cải tiến cấu trúc dữ liệu sửa đổi lại một vài chi tiết để có thể chạy nhanh hơn, hiệu quả
hơn. Thông thường, trước khi kiểm thử thì ta nên đặt mục tiêu viết chương trình sao cho
đơn giản, miễn sao chạy ra kết quả đúng là được, sau đó khi tối ưu chương trình, ta xem
lại những chỗ nào viết chưa tốt thì tối ưu lại mã lệnh để chương trình ngắn hơn, chạy
nhanh hơn. Không nên viết tới đâu tối ưu mã đến đó, bởi chương trình có mã lệnh tối ưu
thường phức tạp và khó kiểm soát.Ta nên tối ưu chương trình theo các tiêu chuẩn sau:
1. Tính tin cậy: Chương trình phải chạy đúng như dự định, mô tả đúng một giải
thuật đúng. Thông thường khi viết chương trình, ta luôn có thói quen kiểm tra
tính đúng đắn của các bước mỗi khi có thể.
2. Tính uyển chuyển: Chương trình phải dễ sửa đổi. Bởi ít có chương trình nào viết
ra đã hoàn hảo ngay được mà vẫn cần phải sửa đổi lại. Chương trình viết dễ sửa
đổi sẽ làm giảm bớt công sức của lập trình viên khi phát triển chương trình.

Số hóa bởi Trung tâm Học liệu

12
3. Tính trong sáng: Chương trình viết ra phải dễ đọc dễ hiểu, để sau một thời gian
dài, khi đọc lại còn hiểu mình làm cái gì? Để nếu có điều kiện thì còn có thể sửa
sai (nếu phát hiện lỗi mới), cải tiến hay biến đổi để được chương trình giải quyết
bài toán khác. Tính trong sáng của chương trình phụ thuộc rất nhiều vào công cụ
lập trình và phong cách lập trình.
4. Tính hữu hiệu: Chương trình phải chạy nhanh và ít tốn bộ nhớ, tức là tiết kiệm
được cả về không gian và thời gian. Để có một chương trình hữu hiệu, cần phải
có giải thuật tốt và những tiểu xảo khi lập trình. Tuy nhiên, việc áp dụng quá
nhiều tiểu xảo có thể khiến chương trình trở nên rối rắm, khó hiểu khi sửa đổi.
Tiêu chuẩn hữu hiệu nên dừng lại ở mức chấp nhận được, không quan trọng

bằng ba tiêu chuẩn trên. Bởi phần cứng phát triển rất nhanh, yêu cầu hữu hiệu
không cần phải đặt ra quá nặng.
Từ những phân tích ở trên, chúng ta nhận thấy rằng việc làm ra một chương trình đòi
hỏi rất nhiều công đoạn và tiêu tốn khá nhiều công sức. Chỉ một công đoạn không hợp lý
sẽ làm tăng chi phí viết chương trình. Nghĩ ra cách giải quyết vấn đề đã khó, biến ý tưởng
đó thành hiện thực cũng không dễ chút nào.
1.2 Phân tích thời gian thực hiện thuật toán
1.2.1 Độ phức tạp thuật toán
Với một vấn đề đặt ra có thể có nhiều thuật toán giải, chẳng hạn người ta đã tìm ra rất
nhiều thuật toán sắp xếp một mảng dữ liệu. Trong các trường hợp như thế, khi cần sử dụng
thuật toán người ta thường chọn thuật toán có thời gian thực hiện ít hơn các thuật toán
khác. Mặt khác, khi bạn đưa ra một thuật toán để giải quyết một vấn đề thì một câu hỏi đặt
ra là thuật toán đó có ý nghĩa thực tế không? Nếu thuật toán đó có thời gian thực hiện quá
lớn chẳng hạn hàng năm, hàng thế kỉ thì đương nhiên không thể áp dụng thuật toán này
trong thực tế. Như vậy chúng ta cần đánh giá thời gian thực hiện thuật toán. Phân tích
thuật toán, đánh giá thời gian chạy của thuật toán là một lĩnh vực nghiên cứu quan trong
của khoa học máy tính. Trong chương này, chúng ta sẽ nghiên cứu phương pháp đánh giá
thời gian chạy của thuật toán bằng cách sử dụng ký hiệu ô lớn, và chỉ ra cách đánh giá thời
gian chạy thuật toán bằng ký hiệu ô lớn. Trước khi đi tới mục tiêu trên, chúng ta sẽ thảo
luận ngắn gọn một số vấn đề liên quan đến thuật toán và tính hiệu quả của thuật toán.
1.2.1.1 Tính hiệu quả của thuật toán
Như đã phân tích ở trên, chúng ta thường xem xét thuật toán, lựa chọn thuật toán để
áp dụng dựa vào các tiêu chí sau:
1. Thuật toán đơn giản, dễ hiểu.
2. Thuật toán dễ cài đặt (dễ viết chương trình)
3. Thuật toán cần ít bộ nhớ

Số hóa bởi Trung tâm Học liệu

13

4. Thuật toán chạy nhanh
Khi cài đặt thuật toán chỉ để sử dụng một số ít lần, người ta thường lựa chọn thuật
toán theo tiêu chí 1 và 2. Tuy nhiên, có những thuật toán được sử dụng rất nhiều lần, trong
nhiều chương trình, chẳng hạn các thuật toán sắp xếp, các thuật toán tìm kiếm, các thuật
toán đồ thị…Trong các trường hợp như thế người ta lựa chọn thuật toán để sử dụng theo
tiêu chí 3 và 4. Hai tiêu chí này được nói tới như là tính hiệu quả của thuật toán. Tính hiệu
quả của thuật toán gồm hai yếu tố: dung lượng bộ nhớ mà thuật toán đòi hỏi và thời gian
thực hiện thuật toán. Dung lượng bộ nhớ gồm bộ nhớ dùng để lưu dữ liệu vào, dữ liệu ra,
và các kết quả trung gian khi thực hiện thuật toán; dung lượng bộ nhớ mà thuật toán đòi
hỏi còn được gọi là độ phức tạp không gian của thuật toán. Thời gian thực hiện thuật toán
được nói tới như là thời gian chạy (Running time) hoặc độ phức tạp thời gian của thuật
toán. Sau này chúng ta chỉ quan tâm tới đánh giá thời gian chạy của thuật toán.
Đánh giá thời gian chạy của thuật toán bằng cách nào? Với cách tiếp cận thực
nghiệm chúng ta có thể cài đặt thuật toán và cho chạy chương trình trên một máy tính nào
đó với một số dữ liệu vào. Thời gian chạy mà ta thu được sẽ phụ thuộc vào nhiều nhân tố:
- Kỹ năng của người lập trình
- Chương trình dịch
- Tốc độ thực hiện các phép toán của máy tính
- Dữ liệu vào
Vì vậy, trong cách tiếp cận thực nghiệm, ta không thể nói thời gian chạy của thuật
toán là bao nhiêu đơn vị thời gian. Chẳng hạn câu nói “thời gian chạy của thuật toán là 30
giây” là không thể chấp nhận được. Nếu có hai thuật toán A và B giải quyết cùng một vấn
đề, ta cũng không thể dùng phương pháp thực nghiệm để kết luận thuật toán nào chạy
nhanh hơn, bởi vì ta mới chỉ chạy chương trình với một số dữ liệu vào.Một cách tiếp cận
khác để đánh giá thời gian chạy của thuật toán là phương pháp phân tích sử dụng các công
cụ toán học. Chúng ta mong muốn có kết luận về thời gian chạy của một thuật toán mà nó
không phụ thuộc vào sự cài đặt của thuật toán, không phụ thuộc vào máy tính mà trên đó
thuật toán được thực hiện.
Để phân tích thuật toán chúng ta cần sử dụng khái niệm cỡ (size) của dữ liệu vào. Cỡ
của dữ liệu vào được xác định phụ thuộc vào từng thuật toán. Ví dụ, trong thuật toán tính

định thức của ma trận vuông cấp n, ta có thể chọn cỡ của dữ liệu vào là cấp n của ma trận;
còn đối với thuật toán sắp xếp mảng cỡ n thì cỡ của dữ liệu vào chính là cỡ n của mảng.
Đương nhiên là có vô số dữ liệu vào cùng một cỡ. Nói chung trong phần lớn các thuật
toán, cỡ của dữ liệu vào là một số nguyên dương n. Thời gian chạy của thuật toán phụ
thuộc vào cỡ của dữ liệu vào; chẳng hạn tính định thức của ma trận cấp 20 đòi hỏi thời
gian chạy nhiều hơn tính định thức của ma trận cấp 10. Nói chung, cỡ của dữ liệu càng lớn

Số hóa bởi Trung tâm Học liệu

14
thì thời gian thực hiện thuật toán càng lớn. Nhưng thời gian thực hiện thuật toán không chỉ
phụ thuộc vào cỡ của dữ liệu vào mà còn phụ thuộc vào chính dữ liệu vào.
Trong số các dữ liệu vào cùng một cỡ, thời gian chạy của thuật toán cũng thay đổi.
Chẳng hạn, xét bài toán tìm xem đối tượng a có mặt trong danh sách (a
1
,

,a
i
,

,a
n
) hay
không. Thuật toán được sử dụng là thuật toán tìm kiếm tuần tự: Xem xét lần lượt từng
phần tử của danh sách cho tới khi phát hiện ra đối tượng cần tìm thì dừng lại, hoặc đi hết
danh sách mà không gặp phần tử nào bằng a. Ở đây cỡ của dữ liệu vào là n, nếu một danh
sách với a là phần tử đầu tiên, ta chỉ cần một lần so sánh và đây là trường hợp tốt nhất,
nhưng nếu một danh sách mà a xuất hiện ở vị trí cuối cùng hoặc a không có trong danh
sách, ta cần n lần so sánh a với từng a

i
(i=1,2,…,n), trường hợp này là trường hợp xấu
nhất. Vì vậy, chúng ta cần đưa vào khái niệm thời gian chạy trong trường hợp xấu nhất và
thời gian chạy trung bình.
Thời gian chạy trong trường hợp xấu nhất (Worst-case running time) của một thuật
toán là thời gian chạy lớn nhất của thuật toán đó trên tất cả các dữ liệu vào cùng cỡ. Chúng
ta sẽ ký hiệu thời gian chạy trong trường hợp xấu nhất là T(n), trong đó n là cỡ của dữ liệu
vào. Sau này khi nói tới thời gian chạy của thuật toán chúng ta cần hiểu đó là thời gian
chạy trong trường hợp xấu nhất. Sử dụng thời gian chạy trong trường hợp xấu nhất để biểu
thị thời gian chạy của thuật toán có nhiều ưu điểm. Trước hết, nó đảm bảo rằng, thuật toán
không khi nào tiêu tốn nhiều thời gian hơn thời gian chạy đó. Hơn nữa, trong các áp dụng,
trường hợp xấu nhất cũng thường xuyên xảy ra.
Chúng ta xác định thời gian chạy trung bình (Average running time) của thuật toán là
số trung bình cộng của thời gian chạy của thuật toán đó trên tất cả các dữ liệu vào cùng cỡ
n. Thời gian chạy trung bình của thuật toán sẽ được ký hiệu là T
tb
(n). Đánh giá thời gian
chạy trung bình của thuật toán là công việc rất khó khăn, cần phải sử dụng các công cụ của
xác suất, thống kê và cần phải biết được phân phối xác suất của các dữ liệu vào. Rất khó
biết được phân phối xác suất của các dữ liệu vào. Các phân tích thường phải dựa trên giả
thiết các dữ liệu vào có phân phối xác suất đều. Do đó, sau này ít khi ta đánh giá thời gian
chạy trung bình.Để có thể phân tích đưa ra kết luận về thời gian chạy của thuật toán độc
lập với sự cài đặt thuật toán trong một ngôn ngữ lập trình, độc lập với máy tính được sử
dụng để thực hiện thuật toán, chúng ta đo thời gian chạy của thuật toán bởi số phép toán sơ
cấp cần phải thực hiện khi ta thực hiện thuật toán. Cần chú ý rằng, các phép toán sơ cấp là
các phép toán số học, các phép toán logic, các phép toán so sánh,…, nói chung, các phép
toán sơ cấp cần được hiểu là các phép toán mà khi thực hiện chỉ đòi hỏi một thời gian cố
định nào đó (thời gian này nhiều hay ít là phụ thuộc vào tốc độ của máy tính). Như vậy
chúng ta xác định thời gian chạy T(n) là số phép toán sơ cấp mà thuật toán đòi hỏi, khi
thực hiện thuật toán trên dữ liệu vào cỡ n. Tính ra biểu thức mô tả hàm T(n) được xác định

như trên là không đơn giản, và biểu thức thu được có thể rất phức tạp.

Số hóa bởi Trung tâm Học liệu

15
Do đó, chúng ta sẽ chỉ quan tâm tớitốc độ tăng(Rate of growth) của hàm T(n), tức là
tốc độ tăng của thời gian chạy khi cỡ dữ liệu vào tăng. Ví dụ, giả sử thời gian chạy của
thuật toán là T(n) = 3n
2
+ 7n + 5 (phép toán sơ cấp). Khi cỡ n tăng, hạng thức 3n
2
quyết
định tốc độ tăng của hàm T(n), nên ta có thể bỏ qua các hạng thức khác và có thể nói rằng
thời gian chạy của thuật toán tỉ lệ với bình phương của cỡ dữ liệu vào.
1.2.1.2 Kí pháp để đánh giá độ phức tạp thuật toán
Giả sử T(n) là thời gian thực hiện một thuật toán nào đó và f(n), g(n), h(n) là các hàm
xác định dương với mọi n. Khi đó ta có độ phức tạp của thuật toán là:
- Hàm Theta lớn:Θ(g(n)) nếu tồn tại các hằng số dương c
1
và c
2
và n
0
sao cho:
c
1
g(n) ≤ T(n) ≤ c
2
g(n) và gọi kí pháp chữ Θ theta lớn, hàm g(n) gọi là giới hạn
chặt của hàm T(n).

- Hàm Omega lớn: Ω(g(n)) nếu tồn tại các hàng số c và n
0
sao cho T(n) ≥c.g(n)
với mọi n ≥ n
0
và gọi là kí pháp chữ Ω lớn, hàm g(n) được gọi là giới hạn dưới
của hàm T(n).
- Hàm O lớn: O(g(n)) nếu tồn tại các hàng số c và n0 sao cho T(n) ≤c.g(n) với
mọi n ≥ n
0
và gọi là kí pháp chữ O lớn, hàm g(n) được gọi là giới hạn trên của
hàm T(n).
1.2.1.3 Các tính chất
(i). Tính bắc cầu: Tất cả các kí pháp trên đều có tính bắc cầu. Nếu f(n)=O(g(n)) và
g(n)= O(h(n)) thì f(n)= O(h(n))
(ii). Tính phản xạ: Tất cả các kí pháp trên đều có tính phản xạf(n)=O(f(n))
1.2.2 Xác định độ phức tạp của thuật toán
Quy tắc hằng số: Nếu đoạn chương trình P có thời gian thực hiện T(n)= O(c
1
f(n))
với c
1
là một hằng số dương thì có thể coi đoạn chương trình P có độ phức tạp tính toán là
O(f(n)).
Chứng minh: T(n)= O(c
1
f(n)) nên tồn tại c
0
>0 và n
0

>0 để T(n) ≤ c
0
.c
1
f(n) với mọi
n≥ n
0
.Đặt c=c
0
.c
1
ta có điều cần chứng minh.
Quy tắc lấy Max: Nếu đoạn chương trình P có thời gian thực hiện T(n)=O(f(n)+g(n))
thì có thể coi đoạn chương trình đó có độ phức tạp tính toán là O(max( f(n), g(n))).
Chứng minh: T(n)=O(f(n)+g(n)) nên tồn tại n
0
>0 và c>0 để T(n) ≤cf(n) + cg(n), với
mọi n ≥ n
0
vậy T(n) ≤cf(n) +cg(n) ≤ 2cmax (f(n),g(n)) với mọi n ≥ n
0
. Từ đó suy điều cần
chứng minh.
Quy tắc cộng: Nếu P
1
có thời gian thực hiện là T
1
(n)=O(f(n)) và P
2
có thời gian thực

hiện T
2
(n)=O(g(n)), khi đó: T
1
(n) +T
2
(n) = O(f(n) +g(n)).

Số hóa bởi Trung tâm Học liệu

16
Chứng minh: Vì T
1
(n)=O(f(n)) nên tồn tại các hàng số c
1
và n
1
sao cho T(n) ≤ c
1
.f(n)
với mọi n ≥ n
1.
Vì T
2
(n)=O(g(n)) nên tồn tại các hàng số c
2
và n
2
sao cho T(n) ≤ c
1

.g(n) với
mọi n ≥ n
2.
Chọn c=max (c
1
,c
2)
và n
0
=max (n
1
,n
2
) ta có với mọi n ≥ n
0
:
T(n)=T
1
(n) + T
2
(n) ≤ c
1
f(n) + c
2
g(n) ≤ cf(n) +cg(n) = c(f(n) +g(n)).
Như vậy ta có điều cần chứng minh.
Quy tắc nhân:Nếu đoạn chương trình P có thời gian thực hiện T(n)=O(f(n)). Khi đó
nếu thực hiện k(n) lần đoạn chương trình P với k(n)=O(g(n)) thì độ phức tạp tính toán sẽ
là: O(f(n) g(n)).
Chứng minh: Thời gian thực hiện k(n) lần đoạn chương trình P sẽ là k(n)*T(n), theo

định nghĩa:
- Tồn tại c
k
≥ 0 và n
k
>0 để k(n) ≤ c
k(
g(n)) với mọi n ≥ n
k

- Tồn tại c
T
≥ 0 và n
T
>0 để T(n) ≤ c
T
f(n) với mọi n ≥ n
T

Vậy với mọi n ≥ max(n
T,
n
k)
ta có k(n)T(n) ≤ c
k
c
T
(f(n)g(n)). Từ đó suy ra điều cần
chứng minh.
1.2.3 Ký hiệu Big-O và biểu diễn thời gian chạy của thuật toán

1.2.3.1 Định nghĩa ký hiệu Big-O
Bây giờ chúng ta đưa ra định nghĩa khái niệm một hàm là “ô lớn” của một hàm khác.
Định nghĩa. Giả sử f(n) và g(n) là các hàm thực không âm của đối số nguyên không
âm n. Ta nói “f(n) là ô lớn của g(n)” và viết là f(n)=O( g(n) ) nếu tồn tại các hằng số
dương c và n
0
sao cho f(n) ≤ cg(n) với mọi n ≥ n
0
.[2]
Như vậy, f(n)=O(g(n)) có nghĩa là hàm f(n) bị chặn trên bởi hàm g(n) với một nhân
tử hằng nào đó khi n đủ lớn. Muốn chứng minh được f(n)=O(g(n)), chúng ta cần chỉ ra
nhân tử hằng c , số nguyên dương n
0
và chứng minh được f(n) ≤ cg(n) với mọi n ≥ n
o
.
Ví dụ. Giả sử f(n) = 5n
3
+ 2n
2
+ 13n + 6, ta có:
f(n) = 5n
3
+ 2n
2
+ 13n + 6 ≤ 5n
3
+ 2n
3
+ 13n

3
+ 6n
3
= 26n
3

Bất đẳng thức trên đúng với mọi n≥ 1, và ta có n
0
=1, c=26. Do đó, ta có thể nói
f(n)=O(n
3
). Tổng quát nếu f(n) là một đa thức bậc k của n:
f(n) = a
k
n
k
+ a
k-1
n
k-1
+ + a
1
n + a
0
thì f(n)=O(n
k
)
Sau đây chúng ta đưa ra một số hệ quả từ định nghĩa ký hiệu ô lớn, nó giúp chúng ta
hiểu rõ bản chất ký hiệu ô lớn. (Lưu ý, các hàm mà ta nói tới đều là các hàm thực không
âm của đối số nguyên dương)

- Nếu f(n) = g(n) + g
1
(n) + + g
k
(n), trong đó các hàm g
i
(n) (i=1, ,k) tăng
chậm hơn hàm g(n) (tức là g
i
(n)/g(n) 0, khi n 0) thì f(n) = O(g(n))

Số hóa bởi Trung tâm Học liệu

17
- Nếu f(n)=O(g(n)) thì f(n)=O(d.g(n)), trong đó d là hằng số dương bất kỳ
- Nếu f(n)=O(g(n)) và g(n)=O(h(n)) thì f(n)=O(h(n)) (tính bắc cầu)
Các kết luận trên dễ dàng được chứng minh dựa vào định nghĩa của ký hiệu ô lớn.
Đến đây, ta thấy rằng, chẳng hạn nếu f(n)=O(n
2
) thì f(n)=O(75n
2
), f(n)=O(0,01n
2
),
f(n)=O(n
2
+ 7n + logn), f(n)=O(n
3
), , tức là có vô số hàm là cận trên (với một nhân tử
hằng nào đó) của hàm f(n).

Một nhận xét quan trọng nữa là, ký hiệu O(g(n)) xác định một tập hợp vô hạn các
hàm bị chặn trên bởi hàm g(n), cho nên ta viết f(n)=O(g(n)) chỉ có nghĩa f(n) là một trong
các hàm đó.
1.2.3.2 Biểu diễn thời gian chạy của thuật toán
Thời gian chạy của thuật toán là một hàm của cỡ dữ liệu vào: hàm T(n). Chúng ta sẽ
biểu diễn thời gian chạy của thuật toán bởi ký hiệu ô lớn: T(n)=O(f(n)), biểu diễn này có
nghĩa là thời gian chạy T(n) bị chặn trên bởi hàm f(n). Thế nhưng như ta đã nhận xét, một
hàm có vô số cận trên. Trong số các cận trên của thời gian chạy, chúng ta sẽ lấy cận trên
chặt (Tight bound) để biểu diễn thời gian chạy của thuật toán.
Định nghĩa. Ta nói f(n) là cận trên chặt của T(n) nếu T(n)=O(f(n)), và nếu
T(n)=O(g(n)) thì f(n)=O(g(n)).
Nói một cách khác, f(n) là cận trên chặt của T(n) nếu nó là cận trên của T(n) và ta
không thể tìm được một hàm g(n) là cận trên của T(n) mà lại tăng chậm hơn hàm f(n). Sau
này khi nói thời gian chạy của thuật toán là O(f(n)), chúng ta cần hiểu f(n) là cận trên chặt
của thời gian chạy.
Nếu T(n)=O(1) thì điều này có nghĩa là thời gian chạy của thuật toán bị chặn trên bởi
một hằng số nào đó, và ta thường nói thuật toán có thời gian chạy hằng. Nếu T(n)=O(n),
thì thời gian chạy của thuật toán bị chặn trên bởi hàm tuyến tính, và do đó ta nói thời gian
chạy của thuật toán là tuyến tính. Các cấp độ thời gian chạy của thuật toán và tên gọi của
chúng được liệt kê trong bảng sau:
Bảng 1.3Các lớp độ phức tạp tính toán
Ký hiệu
Tên gọi
O(1)
O(logn)
O(n)
O(nlogn)
O(n
2
)

O(n
3
)
O(2
n
)
hằng
logarit
tuyến tính
nlogn
bình phương
lập phương


Số hóa bởi Trung tâm Học liệu

18
Hằng số: Hầu hết các chỉ thị của các chương trình đều được thực hiện một lần hay
nhiều nhất chỉ một vài lần. Nếu tất cả các chỉ thị của cùng một chương trình có tính chất
này thì chúng ta sẽ nói rằng thời gian chạy của nó là hằng số. Điều này hiển nhiên là điều
mà ta phấn đấu để đạt được trong việc thiết kế thuật toán.
LogN: Khi thời gian chạy của chương trình là logarit tức là thời gian chạy chương
trình tiến chậm khi N lớn dần. Thời gian chạy thuộc loại này xuất hiện trong các chương
trình mà giải một bài toán lớn bằng cách chuyển nó thành một bài toán nhỏ hơn, bằng cách
cắt bớt kích thước một hằng số nào đó. Với mục đích của chúng ta, thời gian chạy có được
xem như nhỏ hơn một hằng số “lớn“. Cơ số của logarit làm thay đổi hằng số đó nhưng
không nhiều: Khi N là 1000 thì logN là 3 nếu cơ số là 10, là 10 nếu cơ số là 2; khi N là một
triệu, logN được nhân gấp đôi. bất cứ khi nào N được nhân đôi, logN tăng lên thêm một
hằng số.
N: Khi thời gian chạy của một chương trình là tuyến tính, nói chung đây là trường

hợp mà một số lượng nhỏ các xử lý được làm cho mỗi phần tử dữ liệu nhập. Khi N là một
triệu thì thời gian chạy cũng cỡ như vậy. Khi N được nhân gấp đôi thì thời gian chạy cũng
được nhân gấp đôi. Đây là tình huống tối ưu cho một thuật toán mà phải xử lý N dữ liệu
nhập (hay sản sinh ra N dữ liệu xuất).
NlogN: Đây là thời gian chạy tăng dần lên cho các thuật toán mà giải một bài toán
bằng cách tách nó thành các bài toán con nhỏ hơn, kế đến giải quyết chúng một cách độc
lập và sau đó tổ hợp các lời giải. Chúng ta nói rằng thời gian chạy của thuật toán như thế
là “NlogN”.
N
2
: Khi thời gian chạy của một thuật toán là bậc hai, trường hợp này chỉ có ý nghĩa
thực tế cho các bài toán tương đối nhỏ. Thời gian bình phương thường tăng dần lên trong
các thuật toán mà xử lý tất cả các phần tử dữ liệu (có thể là hai vòng lặp lồng nhau). Khi N
là một ngàn thì thời gian chạy là 1 triệu. Khi N được nhân đôi thì thời gian chạy tăng lên
gấp 4 lần.
N
3
: Tương tự, một thuật toán mà xử lý các bộ ba của các phần tử dữ liệu (có thể là 3
vòng lặp lồng nhau) có thời gian chạy bậc ba và cũng chí ý nghĩa thực tế trong các bài
toán nhỏ. Khi N là một trăm thì thời gian chạy là một triệu. Khi N được nhân đôi thì thời
gian chạy tăng lên gấp 8 lần.
2
N
: Một số ít thuật toán có thời gian chạy lũy thừa lại thích hợp trong một số trường
hợp thực tế. Khi N là hai mươi thì thời gian chạy là 1 triệu. Khi N tăng gấp đôi thì thời
gian chạy được nâng lên luỹ thừa hai
Đối với một thuật toán, chúng ta sẽ đánh giá thời gian chạy của nó thuộc cấp độ nào
trong các cấp độ đã liệt kê trên. Trong bảng trên, chúng ta đã sắp xếp các cấp độ thời gian
chạy theo thứ tự tăng dần, chẳng hạn thuật toán có thời gian chạy là O(logn) chạy nhanh
hơn thuật toán có thời gian chạy là O(n), Các thuật toán có thời gian chạy là O(n

k
), với

Số hóa bởi Trung tâm Học liệu

19
k=1,2,3, , được gọi là các thuật toán thời gian chạy đa thức (Polynomial-time algorithm).
Để so sánh thời gian chạy của các thuật toán thời gian đa thức và các thuật toán thời gian
mũ, chúng ta hãy xem xét bảng sau:
Bảng 1.4Thời gian chạy của các lớp thuật toán
Thời
gian
chạy
Cỡ dữ liệu vào
10
20
30
40
50
60
n
n
2
n
3
n
5
0,00001 giây
0,0001 giây
0,001 giây

0,1 giây
0,00002 giây
0,0004 giây
0,008 giây
3,2 giây
0,00003 giây
0,0009 giây
0,027 giây
24,3 giây
0,00004 giây
0,0016 giây
0,064 giây
1,7 phút
0,00005 giây
0,0025 giây
0,125 giây
5,2 phút
0,00006 giây
0,0036 giây
0,216 giây
13 phút
2
n

3
n
0,001 giây
0,059 giây
1,0 giây
58 phút

17,9 phút
6,5 năm
12,7 ngày
3855 thế kỷ
35,7 năm
2.10
8
thế kỷ
366 thế kỷ
1,3. 10
13
thế kỷ
Trong bảng trên, giả sử mỗi phép toán sơ cấp cần 1 micro giây để thực hiện. Thuật
toán có thời gian chạy n
2
, với cỡ dữ liệu vào n=20 thì thời gian chạy là 20
2
x10
-6
=0,004
giây. Đối với thuật toán hàm mũ, thời gian chạy là chấp nhận được chỉ với các dữ liệu vào
có cỡ rất khiêm tốn, n < 30; khi cỡ dữ liệu vào tăng, thời gian chạy sẽ tăng lên rất nhanh
và trở thành con số khổng lồ. Chẳng hạn, thuật toán với thời gian chạy 3
n
, với dữ liệu vào
cỡ 60, nó đòi hỏi thời gian là 1,3x10
13
thế kỉ! Vì vậy nghiên cứu tìm ra các thuật toán hiệu
quả (chạy nhanh) cho các vấn đề có nhiều ứng dụng trong thực tiễn luôn luôn là sự mong
muốn của các nhà tin học.

1.2.4 Độ phức tạp thuật toán với tình trạng dữ liệu vào
Trong nhiều trường hợp, thời gian thực hiện giải thuật không phải chỉ phụ thuộc vào
kích thước dữ liệu mà còn phụ thuộc vào tình trạng của dữ liệu. Chẳng hạn thời gian sắp
xếp một dãy số theo thứ tự tăng dần mà dãy đưa vào chưa có thứ tự sẽ khác với thời gian
sắp xếp một dãy số đã sắp xếp rồi hoặc đã sắp xếp theo thứ tự ngược lại. Lúc này, khi
phân tích thời gian thực hiện giải thuật ta sẽ phải xét tới trường hợp tốt nhất, trường hợp
trung bình và trường hợp xấu nhất. Khó khăn trong việc xác định độ phức tạp tính toán
trong trường hợp trung bình (bởi việc xác định T(n) trung bình thường phải dùng tới
những công cụ toán phức tạp), nên ta thường đánh giá độ phức tạp tính toán trong trường
hợp xấu nhất.
1.2.5 Chi phí thực hiện thuật toán
Độ phức tạp tính toán đặt ra là để đánh giá chi phí thực hiện một giải thuật về mặt
thời gian. Nhưng chi phí thực hiện giải thuật còn có rất nhiều yếu tố khác nữa: không gian
bộ nhớ phải sử dụng là một ví dụ. Tuy nhiên, trên phương diện phân tích lý thuyết, ta chỉ
có thể xét tới vấn đề thời gian bởi việc xác định các chi phí khác nhiều khi rất mơ hồvà
phức tạp. Đối với người lập trình thì khác, một thuật toán với độ phức tạp dù rất thấp cũng
sẽ là vô dụng nếu như không thể cài đặt được trên máy tính, chính vì vậy khi bắt tay cài
đặt một thuật toán, ta phải biết cách tổ chức dữ liệu một cách khoa học, tránh lãng phí bộ

Số hóa bởi Trung tâm Học liệu

20
nhớ không cần thiết. Có một quy luật tương đối khi tổ chức dữ liệu: Tiết kiệm được bộ nhớ
thì thời gian thực hiện thường sẽ chậm hơn và ngược lại.




Số hóa bởi Trung tâm Học liệu


21


CHƢƠNG 2. TỔ CHỨC DỮ LIỆU CHO LỚP
THUẬT TOÁN CHIA ĐỂ TRỊ

Trong khoa học máy tính, chia để trị là một mô hình thiết kế thuật toán quan trọng
dựa trên đệ quy với nhiều phân nhánh. Thuật toán chia để trị hoạt động bằng cách chia bài
toán thành nhiều bài toán nhỏ hơn thuộc cùng thể loại, cứ như vậy lặp lại nhiều lần, cho
đến khi bài toán thu được đủ đơn giản để có thể giải quyết trực tiếp. Sau đó lời giải của các
bài toán nhỏ được tổng hợp lại thành lời giải cho bài toán ban đầu. Kĩ thuật này là cơ sở
cho nhiều thuật toán hiệu quả. Tuy nhiên, khả năng hiểu và thiết kế thuật toán chia để trị là
một kĩ năng đòi hỏi nhiều thời gian để làm chủ. Trong chương này, tôi sẽ trình bày cách tổ
chức dữ liệu cho lớp thuật toán chia để trị và các bài toán điển hình được giải quyết theo
tiếp cận chia để trị.
2.1 Chiến lƣợc chia để trị
Ý tưởng của chiến lược này như sau: Chia vấn đề cần giải thành một số vấn đề con
cùng dạng với vấn đề đã cho, chỉ khác là cỡ của chúng nhỏ hơn. Mỗi vấn đề con được giải
quyết độc lập. Sau đó, ta kết hợp nghiệm của các vấn đề con để nhận được nghiệm của
vấn đề đã cho. Nếu vấn đề con là đủ nhỏ có thể dễ dàng tính được nghiệm, thì ta giải
quyết nó, nếu không vấn đề con được giải quyết bằng cách áp dụng đệ quy thủ tục trên
(tức là lại tiếp tục chia nó thành các vấn đề con nhỏ hơn,…) [1]. Do đó, các thuật toán
được thiết kế bằng chiến lược chia-để-trị sẽ là các thuật toán đệ quy.
Sau đây là lược đồ của kỹ thuật chia-để-trị:
DivideConquer (A,x)// Tìm nghiệm x của bài toán A.

if (A đủ nhỏ)
Solve (A);
else
Chia bài toán A thành các bài toán con A

1
,A
2
,…,A
m
;
for (i = 1; i <= m ; i ++)
DivideConquer (A
i
, x
i
);
Kết hợp các nghiệm x
i
của các bài toán con A
i
(i=1,…,m)
để nhận được nghiệm x của bài toán A;


Hình 2.3Thuật toán chia để trị

Số hóa bởi Trung tâm Học liệu

22
“Chia một bài toán thành các bài toán con” cần được hiểu là ta thực hiện các phép
biến đổi, các tính toán cần thiết để đưa việc giải quyết bài toán đã cho về việc giải quyết
các bài toán con cỡ nhỏ hơn.
2.2 Tổ chức dữ liệu cho lớp thuật toán chia để trị
Chiến lược "chia để trị" được áp dụng cho các thuật toán quy bài toán ban đầu về

đúng một bài toán nhỏ hơn, chẳng hạn như thuật toán tìm kiếm nhị phân, dùng cho việc
tìm khóa trong một danh sách đã sắp xếp. Khi thiết kế thuật toán giải quyết một vấn đề
bằng kỹ thuật chia-để-trị thì thuật toán chúng ta thu được là thuật toán đệ quy. Thuật toán
đệ quy được biểu diễn trong các ngôn ngữ lập trình bậc cao bởi các hàm đệ quy. Đó là các
hàm chứa các lời gọi hàm đến chính nó. Trong mục này chúng ta sẽ nêu lên các đặc điểm
của thuật toán đệ quy và phân tích hiệu quả (về không gian và thời gian) của thuật toán đệ
quy là cơ sở cho thuật toán chia đê trị. Đệ quy là một kỹ thuật đặc biệt quan trọng để giải
quyết vấn đề. Có những vấn đề rất phức tạp, nhưng chúng ta có thể đưa ra thuật toán đệ
quy rất đơn giản, sáng sủa và dễ hiểu. Cần phải hiểu rõ các đặc điểm của thuật toán đệ quy
để có thể đưa ra các thuật toán đệ quy đúng đắn.
Khi đó, tổ chức dữ liệu cho lớp bài toán chia để trị được mô tả như sau:

Hình 2.4Tổ chức dữ liệu cho lớp bài toán chia để trị
Trong đó:
 Bài toán ban đầu được chia thành k bài toán con.
 Đầu vào của bài toán ban đầu n, m, …được phân nhỏ thành đầu vào lần lượt
của các bài toán con là n
i
, m
i
(i=1 k)
 Đầu ra bài toán ban đầu o được chia thành các đầu ra o
i
(i=1 k)
Giải thuật đệ quy cho một vấn đề cần phải thoả mãn các đòi hỏi sau:
1. Chứa lời giải cho các trường hợp đơn giản nhất của vấn đề. Các trường hợp này
được gọi là các trường hợp cơ sở hay các trường hợp dừng.

Số hóa bởi Trung tâm Học liệu


23
2. Chứa các lời gọi đệ quy giải quyết các vấn đề con với cỡ nhỏ hơn.
3. Các lời gọi đệ quy sinh ra các lời gọi đệ quy khác và về tiềm năng các lời gọi đệ
quy phải dẫn tới các trường hợp cơ sở.
Tính chất 3 là đặc biệt quan trọng, nếu không thoả mãn, hàm đệ quy sẽ chạy mãi
không dừng. Đối với một vấn đề, có thể có hai cách giải: giải thuật đệ quy và giải thuật
dùng phép lặp. Giải thuật đệ quy được mô tả bởi hàm đệ quy, còn giải thuật dùng phép lặp
được mô tả bởi hàm chứa các lệnh lặp, để phân biệt với hàm đệ quy ta sẽ gọi là hàm lặp.
Ưu điểm nổi bật của đệ quy so với phép lặp là đệ quy cho phép ta đưa ra giải thuật rất đơn
giản, dễ hiểu ngay cả đối với những vấn đề phức tạp. Trong khi đó, nếu không sử dụng đệ
quy mà dùng phép lặp thì thuật toán thu được thường là phức tạp hơn, khó hiểu hơn.
Các nhân tố có thể làm cho thuật toán đệ quy kém hiệu quả. Trước hết, ta cần biết
cơ chế máy tính thực hiện một lời gọi hàm. Khi gặp một lời gọi hàm, máy tính tạo ra một
bản ghi hoạt động (Activation record) ở ngăn xếp thời gian chạy (Run-time stack) trong bộ
nhớ của máy tính. Bản ghi hoạt động chứa vùng nhớ cấp cho các tham biến và các biến địa
phương của hàm. Ngoài ra, nó còn chứa các thông tin để máy tính trở lại tiếp tục hiện
chương trình đúng vị trí sau khi nó đã thực hiện xong lời gọi hàm. Khi hoàn thành thực
hiện lời gọi hàm thì bản ghi hoạt động sẽ bị loại bỏ khỏi ngăn xếp thời gian chạy. Khi thực
hiện một hàm đệ quy, một dãy các lời gọi hàm được sinh ra. Hậu quả là một dãy bản ghi
hoạt động được tạo ra trong ngăn xếp thời gian chạy. Cần chú ý rằng, một lời gọi hàm chỉ
được thực hiện xong khi mà các lời gọi hàm mà nó sinh ra đã được thực hiện xong và do
đó rất nhiều bản ghi hoạt động đồng thời tồn tại trong ngăn xếp thời gian chạy, chỉ khi một
lời gọi hàm được thực hiện xong thì bản ghi hoạt động cấp cho nó mới được loại ngăn xếp
thời gian chạy. Vì vậy, việc thực hiện hàm đệ quy có thể đòi hỏi rất nhiều không gian nhớ
trong ngăn xếp thời gian chạy, thậm chí có thể vượt quá khả năng của ngăn xếp thời gian
chạy trong bộ nhớ của máy tính. Một nhân tố khác làm cho các thuật toán đệ quy kém hiệu
quả là các lời gọi đệ quy có thể dẫn đến phải tính nghiệm của cùng một bài toán con rất
nhiều lần.
2.3 Định lý tổng quát tính độ phức tạp các thuật toán chia để trị
Các thuật toán chia để trị thường chuyển bài toán lớn về các bài toán nhỏ rồi kết hợp

lời giải các bài toán nhỏ để tạo ra kết quả của bài toán ban đầu. Để tính độ phức tạp của
các thuật toán chia để trị, người ta thường sử dụng định lý tổng quát sau:
Định lý (Mastertheorem): Cho
n
T n aT f n
b
, ta có
1. Nếu
n
af cf n
b
với c> 1 thì
log
b
a
T n n
.

Số hóa bởi Trung tâm Học liệu

24
2. Nếu
n
af cf n
b
với c< 1 thì
T n f n
.
3. Nếu
n

af cf n
b
thì
log
b
T n f n n
.
Nhận xét: Có thể hiểu là ta chia bài toán lớn ra thành a bài toán có kích thước
n
b
để
giải rồi dùng f(n) phép tính để kết hợp lời giải các bài toán con lại.
Chứng minh:Giả sử
k
nb
(không mất tính tổng quát do ta có thể chọn k nhỏ nhất
sao cho
k
bn
), đồng thời giả sử ở trường hợp cơ sở n = 1, ta có T(1) = f(1).
Khai triển ra ta được:
2
2
2
2


k
k
T n f n aT n b f n af n b a T n b

f n af n b a f n b a f n b

Trường hợp 1: Nếu
n
af cf n
b
với c> 1, ta có:
2
2
1
1
1
1

1
k
k
k
k
fn
c af n b
af n b
c a f n b
a f n b
c a f n b

Như vậy, tổng trên bằng tổng dãy cấp số nhân (hệ số lũy tiến bằng c) có số hạng lớn
nhất là
kk
a f n b

. Như vậy, ta có:
log log
bb
na
kk
T n a f n b a n

Trường hợp 2: Nếu
n
af cf n
b
với c< 1 tương tự ta cũng có tổng dãy cấp số
nhân với hệ số lũy tiến c< 1 và số hạng lớn nhất là f(n). Như vậy, ta có:
T n f n

Trường hợp 3: Nếu
n
af cf n
b
, các số hạng của tổng trên đều bằng nhau và
bằng f(n), vậy :
1 log
b
T n k f n f n n
.
2.4 Một số lớp bài toán điển hình

Số hóa bởi Trung tâm Học liệu

25

Một ví dụ lâu đời của thuật toán chia để trị là thuật toán Cooley-Tukey [3] cho biến
đổi Fourier rời rạc. Thuật toán này được phát hiện bởi Gauss năm 1805 nhưng ông không
phân tích số phép tính của thuật toán và thuật toán này chỉ trở nên phổ biến khi được phát
hiện lại hơn một thế kỉ sau đó. Trong toán học, phép biến đổi Fourier rời rạc, đôi khi còn
được gọi là biến đổi Fourier hữu hạn, là một biến đổi trong giải tích Fourier cho các tín
hiệu thời gian rời rạc. Đầu vào của biến đổi này là một chuỗi hữu hạn các số thực hoặc số
phức, làm biến đổi này là một công cụ lý tưởng để xử lý thông tin trên các máy tính. Đặc
biệt, biến đổi này được sử dụng rộng rãi trong xử lý tín hiệu và các ngành liên quan đến
phân tích tần số chứa trong một tín hiệu, để giải phương trình đạo hàm riêng, và để làm
các phép như tích chập. Biến đổi này có thể được tính nhanh bởi thuật toán biến đổi
Fourier nhanh [2, 3, 4].
2.4.1 Lớp bài toán tìm kiếm
2.4.1.1 Thuật toán tìm kiếm nhị phân
a) Ý tƣởng: Thuật toán tìm kiếm nhị phân là thuật toán được thiết kế dựa trên chiến
lược chia-để-trị. Cho mảng A cỡ n được sắp xếp theo thứ tự tăng dần: A[0] ≤…≤ A[n-1].
Với x cho trước, ta cần tìm xem x có chứa trong mảng A hay không, tức là có hay không
chỉ số 0 ≤ i ≤ n-1 sao cho A[i] = x[3].
Kỹ thuật chia-để-trị gợi ý ta: Chia mảng A[0…n-1] thành 2 mảng con cỡ n/2 là
A[0…k-1] và A[k+1…n-1], trong đó k là chỉ số đứng giữa mảng. So sánh x với A[k]. Nếu
x = A[k] thì mảng A chứa x và i = k. Nếu không, do tính được sắp của mảng A, nếu x
A[k] ta tìm x trong mảng A[0…k-1], còn nếu x A[k] ta tìm x trong mảng A[k+1…n-1].
Áp dụng cho bài toán sau:
- Input: Dãy gồm N số nguyên k
1
, k
2
, , k
N
đôi mộ
nguyên x.

- Output: Chỉ số i mà k
i
= x hoặc thông báo không có số hạng nào của dãy có
giá trị trùng với x.
- Ý tưởng:
Giua
Giua=[ (N+1)/2].
Khi đó, chỉ xảy ra một trong ba trường hợp sau:
- Nếu k
Giua
= x thì Giua là chỉ số cần tìm. Việc tìm kiếm kết thúc.
- Nếu k
Giua
> k thì do dãy khoa là dãy đã được sắp xếp nên việc tìm kiếm tiếp
theo chỉ xét trên dãy k
1
, ka
2
, , k
Giua–1
(phạm vi tìm kiếm mới bằng khoảng
một nửa phạm vi tìm kiếm cũ).

×