Tải bản đầy đủ (.doc) (75 trang)

PHÂN TÍCH THIẾT KẾ THUẬT TOÁN VÀ TĂNG TRƯỞNG CỦA HÀM

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 (776.88 KB, 75 trang )

ĐẠI HỌC HUẾ

  

 !"#$%&'(
Đề tài: )*+,,-
./012/

Giáo viên hướng dẫn: 34'!56%75
Học viên thực hiện (Nhóm 6):
89:3;
5%:<=%>;5
5%:<?)>;55
8?%9:
@?7#%
 Lớp: Khoa học máy tính (2014)
Huế, 10 – 2014
1
AA
ĐẠI HỌC HUẾ 1
LỜI NÓI ĐẦU 3
CHƯƠNG 2 4
MỞ ĐẦU 4
2.1 Thuật toán Sắp xếp chèn (Insertion sort) 4
2.2 Phân tích thuật toán 7
2.3 Thiết kế thuật toán 16
CHƯƠNG 3 33
TĂNG TRƯỞNG CỦA HÀM 33
Tổng quan 33
3.1 Hệ ký hiệu tiệm cận 33
3.2 Khái niệm chuẩn và các hàm thông dụng 44


2
BC
Thiết kế và phân tích thuật toán là môn học rất quan trọng và cần thiết cho
người làm CNTT. Là nền tảng cơ bản nhất giúp người lập trình hiểu được sâu
hơn về hệ thống và có thể tính toán độ phức tạp để từ đó quyết định chọn một
phương pháp tối ưu cho hệ thống phần mềm.
Nội dung của tiểu luận này là cơ sở lý thuyết ban đầu để phục vụ việc thiết
kế phân tích thuật toán và sử dụng các ký hiệu để trình bày độ phức tạp tính toán
của chương trình một cách rõ ràng nhất.
Nội dung trình bày gồm 2 chương theo tài liệu dịch:
D'E%''F5'DGHI3J'EKE'
Thomas H. Cormen
Charles E. Leiserson
Ronald L. Rivest
Clifford Stein
The MIT Press
Cambridge , Massachusetts London, England
McGraw-Hill Book Company
Boston Burr Ridge , IL Dubuque , IA Madison , WI New York San Francisco St.
Louis
Montréal Toronto
Chương 2: Mở đầu
Chương 3: Tăng trưởng của hàm
Mặc dù đã rất cố gắng nhưng tiểu luận này chắc không tránh khỏi những
sai sót. Nhóm chúng em rất mong nhận được các ý kiến góp ý của thầy hướng
dẫn và các bạn. Xin chân thành cảm ơn TS. Hoàng Quang đã tận tình hướng dẫn
và tạo điều kiện cho chúng em hoàn thành môn học này.
3
LM
1C

Chương này là nền tảng cho phép bạn làm quen và dùng nó để thiết kế và
phân tích thuật toán. Ta bắt đầu bằng việc nghiên cứu thuật toán sắp xếp chèn.
Thuật toán này giải quyết các bài toán sắp xếp được giới thiệu trong Chương 1.
Chúng ta định nghĩa “pseudecode” (mã giả) là từ ngữ quen thuộc với người đọc
và lập trình trên máy tính hoặc sử dụng nó để trình bày trong thuật toán. Có
những thuật toán đặc biệt mà chúng ta đã chấp nhận nó đã được sắp xếp rất
chính xác và sau đó phân tích thời gian chạy của nó. Việc phân tích đã chỉ ra
phương pháp làm tăng thời gian với một số lượng các chỉ số đã được sắp xếp.
Thuật toán chèn dưới đây chúng ta đã sử dụng phương pháp “Divide-and-
conquer” (divide-and-conquer) để thiết kế một thuật toán và sử dụng nó để phát
triển thành một thuật toán gọi là thuật toán sắp xếp trộn. Cuối cùng là việc phân
tích thời gian chạy của thuật toán sắp xếp trộn.
M4N%&'(3O"P"QRHJD'H'DS
Thuật toán đầu tiên là thuật toán sắp xếp chèn nhằm giải quyết các bài
toán sắp xếp với thông số:
Input: Cho một dãy n số (a
1
, a
2
… a
n
)
Output: Phép hoán vị (a
1

, a
2

… a
n


) thỏa mãn a
1

<= a
2

<=….<= a
n


Ở đây, những con số mà chúng ta sắp xếp được gọi là khoá. Trong chương
này, điển hình chúng ta mô tả các thuật toán như là chương trình được viết bằng
ngôn ngữ mô phỏng tương tự như ngôn ngữ C, pascal, Java. Sự khác nhau giữa
mã giả và mã thật là gì? Chúng ta nhận thấy rằng cho dù ý nghĩa phương pháp
hầu hết là trong sáng và xúc tích để đưa ra một thuật toán. Nhiều khi phương
pháp trong sáng nhất là Tiếng Anh, vì vậy không phải ngạc nhiên nếu bạn dùng
một mệnh đề hoặc một câu Tiếng Anh mà không có từ “real”. Một sự khác nhau
giữa mã giả và mã thật là mã giả không được sự quan tâm điển hình với việc cấp
phát bộ nhớ của kỹ sư phần mềm. Việc cấp phát bộ nhớ của dữ liệu được hiểu
4
ngầm, các đơn vị đo và xử lý lỗi được đặt ra để diễn đạt thuật toán một cách xúc
tích hơn.
Chúng ta bắt đầu với thuật toán sắp xếp chèn mà kết quả của thuật toán
này là sắp xếp một số phần tử nhỏ. Thuật toán này làm việc như là cách người ta
sắp xếp những con bài trên tay. Bắt đầu là tay trái rỗng và các con bài được úp
xuống bàn. Sau đó, trong cùng một thời gian chúng ta di chuyển một con bài từ
bàn và chèn vào vị trí chính xác trên bàn tay trái. Để tìm vị trí chính xác cho một
con bài chúng ta so sánh nó với một con bài đã được sắp xếp sẵn sàng trên tay từ
phải qua trái. Tại cùng một thời điểm, những con bài đã được đặt trên bài tay trái

được sắp xếp và những con bài này là khởi đầu của những con bài trên đỉnh của
bàn.
Hình 2.1
Đoạn chương trình mô phỏng dưới đây cho ta giải thuật sắp xếp chèn. Nó
thực hiện các thông số đầu vào là một mảng gồm n phần tử tức là chứa đựng n
độ dài cần được sắp xếp. Đầu vào của mảng A là một dãy chưa được sắp xếp và
sau khi thuật toán kết thúc cho ta một mảng A đã được sắp xếp.
.TU"HJ%EJ'EJV7W9X
INSERTION-SORT(A)
1 for i ← 2 to length[A]
2 do m ← A[i] {m là khóa }
3 Insert A[i] into the sorted sequence A[1 i - 1].
4 j ← i - 1
5
5 while j > 0 and A[j] > m
6 do A[j + 1] ← A[j]
7 j ← j - 1
8 A[j + 1] ← m
Y<5Z([>TF\"97%&'(HO"P"Q
Ở hình bên dưới đây cho ta một mảng A với dãy số chưa được sắp xếp
A = (5, 2, 4, 6, 1, 3)
Đầu tiên ta xem chỉ số i như là con bài hiện tại đang ở trên tay. Cho vòng lặp
For chạy bởi i nó sẽ chia mảng A thành 2 đoạn A[1 j - 1] đây là đoạn cần được
sắp xếp và A[j + 1 n] là số con bài còn lại ở trên bàn chưa được đưa vào. Kết
quả các bước sắp xếp cho ta một dãy tăng dần như sau:
Hình 2.2
(H]EX5G^5Z
1- Thụt đầu dòng để nêu rõ cấu trúc khối.
2- Cấu trúc vòng lặp _FJ, `'D và DJ"J7 và những cấu trúc có điều kiện
như if, then và else được thể hiện giống nhau như trong pascal.

3- Ký tự "►" để ghi chú hoặc giải thích một đoạn lệnh.
4- Phép gán nhiều biến cho một giá trị nào đó như: i ← j ← e tức là cả hai
biến i và j là có giá trị e nó nên được xem là tương đương như là phép gán j ← e
theo sau phép gán i ← j.
5- Biến (như là i, j và key) là biến cục bộ trong thủ tục đã cho trên. Chúng
ta không nên sử dụng biến toàn cục với những mục đích không rõ.
6
6- Các thành phần mảng được truy cập bằng cách đặc tả tên mảng theo sau
là chỉ số trong các dấu ngoặc vuông. Ví dụ A[i] nêu rõ thành phần thứ i trong
mảng A.
7- Dữ liệu phức hợp thường được tổ chức thành các đối tượng bao hàm
các thuộc tính hoặc các trường. Để truy cập một trường cụ thể ta dùng tên trường
theo sau là tên đối tượng của nó trong các dấu ngoặc vuông.
Ví dụ ta xem một mảng như một đối tượng có thuộc tính length nêu rõ số
lượng thành phần mà nó chứa. Để đặc tả số lượng thành phần trong mảng A ta
viết Length[A].
8- Việc cấp phát bộ nhớ sẽ được hiểu ngầm.
a!&"M4N4N
Sắp xếp mảng A =〈31, 41, 59, 26, 41, 58> theo thứ tự tăng dần bằng mô
hình chuyển đổi thứ tự theo phương pháp chèn
a!&"M4N4M
Viết Thủ tục sắp xếp chèn theo thứ tự giảm dần
a!&"M4N4b
Viết mã giả cho việc tìm kiếm giá trị v trong mảng A theo phương pháp
tìm kiếm tuần tự.
M4M)#$%&'(
Phân tích một thuật toán nghĩa là dự đoán các tài nguyên (resource) mà
thuật toán yêu cầu. Đôi khi, các tài nguyên như là bộ nhớ, băng thông mạng, hay
là phần cứng máy tính đều là mối quan tâm chính, nhưng thường thì chính thời
gian tính toán mới là yếu tố mà chúng ta cần đo lường. Nhìn chung, nhờ việc

phân tích một vài thuật toán cho một vấn đề, ta có thể nhận ra thuật toán nào là
hiệu quả nhất. Việc phân tích có thể chỉ ra nhiều hơn một thuật toán thích hợp có
thể làm được, nhưng một vài thuật toán sử dụng nhiều tài nguyên hơn thường bị
loại bỏ ra khỏi quá trình tiến hành.
Trước khi chúng ta có thể phân tích một thuật toán, ta phải có một mô
hình của kỹ thuật thực hiện mà sẽ được sử dụng, bao gồm các mô hình của
7
những tài nguyên kỹ thuật và các chi phí thực hiện nó. Trong hầu hết cuốn sách
này, chúng ta sẽ thừa nhận một vi xử lý có đặc điểm chung, mô hình máy truy
cập ngẫu nhiên (RAM), một bộ xử lý chung, làm công nghệ thực thi và và ngầm
hiểu rằng các thuật toán của chúng sẽ được thực thi dưới dạng các chương trình
máy tính. Trong mô hình RAM, các câu lệnh được thực hiện lần lượt, không
cùng thực thi đồng thời. Tuy nhiên, ở chương sau, chúng sẽ có dịp nghiên cứu
các mô hình của phần cứng số.
Nói đúng ra, một thuật toán ('JS nên có định nghĩa chính xác các câu
lệnh của mô hình RAM và các chi phí của chúng. Tuy nhiên, nếu làm như vậy,
sẽ trở nên dài dòng và sinh ra chút sự hiểu biết bên trong thiết kế và phân tích
thuật toán. Lúc đó, chúng ta phải cẩn thận việc lạm dụng mô hình RAM. Ví dụ,
RAM có câu lệnh sắp xếp thì sao? Thì chúng ta có thể sắp xếp trong vừa một câu
lệnh. Nhưng RAM sẽ không tồn tại, nếu các máy tính thực không có các câu
lệnh. Vì vậy, một gợi ý là làm thế nào các máy tính thực được thiết kế. Mô hình
RAM chứa các câu lệnh thường được tìm thấy trong các máy tính thực: số học
(cộng, trừ, nhân, chia, chia không có số dư, số thấp nhất, số cao nhất), thao tác
dữ liệu (nạp, lưu trữ, sao chép), và điều khiển (tách có điều kiện và không có
điều kiện, gọi thủ tục con và gọi lại). Mỗi câu lệnh như thế mất một khoảng thời
gian cố định.
Các kiểu dữ liệu trong mô hình RAM là số nguyên và dấu phẩy động
(floating point). Mặc dù chúng ta không tự nắm rõ một cách đúng đắn độ chính
xác trong cuốn sách này (của kiểu dữ liệu), ứng dụng đòi hỏi có sự chính xác về
kiểu dữ liệu. Chúng ta còn thừa nhận giới hạn của kích cỡ mỗi từ dữ liệu. Ví dụ,

khi làm việc với những đầu vào kích cỡ n, chúng ta thừa nhận rằng các số
nguyên được đại diện bởi c*log
2
n bit với hằng số c>=1. Chúng ta yêu cầu c>=1
để mỗi từ (bit) có thể lưu giữ giá trị của n, cho phép chúng ta đưa vào các phần
tử đầu vào riêng lẻ, và chúng ta giới hạn c là hằng số để kích cỡ từ không phát
triển tùy tiện (nếu kích cỡ từ có thể phát triển tùy tiện, trong thời gian cố định,
chúng ta có thể lưu trữ một số lượng lớn dữ liệu trong một từ và vận hành trên nó
– rõ ràng là phi thực tế).
8
Một số máy tính thực chứa các câu lệnh không được liệt kê ở trên, và một
số câu lệnh miêu tả vùng xám của mô hình RAM. Ví dụ, có phải thời gian cố
định của câu lệnh là mũ hóa? Trong trường hợp tổng quát, không mất nhiều câu
lệnh để tính x
y
khi x và y là các số thực. Tuy nhiên, trong trạng thái giới hạn, hàm
mũ là sự vận hành hằng số thời gian. Một số máy tính có câu lệnh “shift left” (di
chuyển trái), ở đó hằng số thời gian đổi chỗ các bit số nguyên bởi k vị trí sang
bên trái. Trong hầu hết các máy tính, việc đổi chỗ các bit số nguyên bằng một vị
trí ở bên trái là tương đương nhân với 2. Đổi chỗ các bit k vị trí sang bên trái là
tương đương phép nhân 2
k
. Vì thế, một số máy tính có thể tính 2
k
trong một câu
lệnh hằng số thời gian bằng cách đổi chỗ số nguyên 1 qua k vị trí sang trái, ngay
khi k không nhiều hơn số các bit trong kí tự máy tính. Chúng ta sẽ nỗ lực để
tránh những vùng xám trong mô hình RAM, nhưng chúng ta sẽ xem xét phép
tính 2
k

như phép tính hằng số thời gian khi k là số nguyên dương đủ nhỏ.
Trong mô hình RAM, chúng ta không cố gắng mô hình hóa thứ tự bộ nhớ
mà rất phổ biến ở các máy tính cùng thời. Chúng ta không mô hình hóa các khối
nhớ hoặc bộ nhớ ảo (mà thường được bổ sung với phân trang theo yêu cầu). Một
vài mô hình tính toán cố gắng giải thích tác dụng của thứ tự bộ nhớ. Một ít các
vấn đề trong sách này nghiên cứu tác dụng thứ tự bộ nhớ, nhưng ở một số phần,
sự phân tích trong sách này sẽ không xem xét đến. Các mô hình bao gồm thứ tự
bộ nhớ là một bit phức tạp hơn mô hình RAM, để mà chúng có thể rất khó khăn
khi làm việc với chúng. Ngoài ra, việc phân tích mô hình RAM thường là dụng
cụ dự báo tuyệt vời hiệu suất của các máy tính thực.
Phân tích thậm chí một thuật toán đơn giản trong mô hình RAM có thể
được xem là thách thức. Các công cụ tính toán yêu cầu bao gồm các phép tính tổ
hợp, lý thuyết xác suất, phương trình đại số và khả năng nhận dạng hầu hết số
hạng quan trọng trong công thức. Bởi vì các giải quyết một thuật toán có lẽ khác
nhau ở từng đầu vào, nghĩa là chúng ta cần có biện pháp để tóm lược thành các
công thức đơn giản, dễ hiểu.
Thậm chí chúng ta chọn điển hình chỉ một mô hình máy để phân tích thuật
toán được đưa ra, chúng ta còn quay về nhiều sự lựa chọn trong việc quyết định
làm thế nào để mô tả phân tích của chúng ta. Chúng ta muốn một cách thật đơn
9
giản để viết, tính toán, và trình bày các đặc tính quan trọng của yêu cầu đầu vào
của thuật toán, và các chi tiết dài dòng bị bỏ.
)#$c%&HO"P"Q
Thời gian thực hiện thủ tục ISERTION-SORT phụ thuộc vào đầu vào: tiến
trình sắp xếp hàng ngàn con sẽ số lâu hơn sắp xếp ba con số. Hơn nữa,
INSERTION-SORT có thể mất vài khoảng thời gian khác nhau để sắp xếp hai
chuỗi đầu vào của cùng một kích cỡ giống nhau, phụ thuộc vào mức độ sắp xếp
sẵn của chúng. Nói chung, thời gian thực hiện một thuật toán thường tăng theo
kích cỡ đầu vào, do đó theo cách truyền thống, để mô tả thời gian thực hiện
chương trình như một hàm kích cỡ đầu vào của chương trình. Để làm được như

vậy, chúng ta cần định nghĩa các thuật ngữ: thời gian thực hiện (running time) và
kích cỡ đầu vào cẩn thận hơn.
Quan điểm tốt nhất của kích cỡ đầu vào phục thuộc vào bài toán đang
nghiên cứu. Đối với một số bài toán, như là sắp xếp hay tính toán rời rạc các
phép biến đổi Fourier, số đo tự nhiên nhất đó là số lượng các mục trong đầu vào
– ví dụ, kích cỡ mảng là n để sắp xếp. Trong một số các bài toán khác, như nhân
hai số nguyên, số đo tốt nhất của kích cỡ đầu vào là tổng số các bit để biểu thị
cho đầu vào theo ký hiệu nhị phân thông thường. Thỉnh thoảng, việc mô tả kích
cỡ đầu vào bằng hai số nguyên thích hợp hơn là một số. Ví dụ, nếu đầu vào của
thuật toán là một đồ thị, kích cỡ đầu vào có thể được mô tả bằng số các đỉnh và
cạnh của đồ thị. Chúng ta có thể nêu rõ kiểu đo kích cỡ đầu vào nào được sử
dụng với mỗi bài toán mà chúng ta nghiên cứu.
Thời gian thực hiện một thuật toán trên một đầu vào cụ thể chính là số
lượng các phép toán nguyên tố và các bước thực hiện. Sẽ tiện dụng hơn nếu ta
định nghĩa khái niệm “bước” để nó càng độc lập với máy càng tốt. Quan trọng,
hãy chấp nhận quan điểm sau. Cần có một khoảng thời gian cố định để thực thi
từng dòng giải mã của chúng ta. Một dòng có thể mất một khoảng thời gian khác
so với dòng kia, nhưng chúng ta sẽ thừa nhận rằng mỗi lệnh thực thi của dòng
thứ i mất thời gian c
i
, mà c
i
là một hằng số. Quan điểm này là phù hợp với mô
hình RAM và nó còn phản ánh cách thực thi giải mã trên hầu hết các máy tính
hiện nay.
(*)
10
Trong đoạn mô tả dưới đây, cách diễn tả về thời gian thực hiện của
INSERTION-SORT sẽ suy ra từ công thức mờ mà nó sử dụng trong tất cả các
hao phí c

i
thành một ký hiệu đơn giản hơn mà ngắn gọn hơn và dễ dàng tính toán
hơn. Ký hiệu đơn giản này sẽ giúp ta dễ dàng xác định một thuật toán có hiệu lực
hơn thuật toán khác hay không.
Chúng ta bắt đầu bằng việc đưa ra thủ tục INSERT-SORT với chi phí thời
gian của mỗi câu lệnh và số lượng thời gian thực thi mỗi câu lệnh. Với mỗi
j=2,3,…,n mà n = length[A], chúng ta cho t
j
là số thời gian thực hiện vòng lặp
while ở dòng 5 theo giá trị j đó. Khi vòng lặp for hoặc while kết thúc theo cách
thông thường (nhờ có kiểm tra vòng lặp đầu trước), kiểm tra được thực thi một
thời gian hơn phần thân vòng lặp. Chúng ta thừa nhận rằng các chú giải không
phải là các câu lệnh thi hành, và chúng không mất thời gian với nó.
INSERTION-SORT(A) cost times
1 for j ← 2 to length[A] c
1
n
2 do m ← A[j] {m là khóa } c
2
n - 1
3 ► Chèn A[j] vào chuỗi có sắp xếp A[1 i - 1] n - 1
4 i ← j - 1 c
4
n - 1
5 while i > 0 and A[i] > m c
5


=
n

j
jt
2
6 do A[i + 1] ← A[i] c
6


=

n
j
jt
2
)1(
7 i ← i - 1 c
7


=

n
j
jt
2
1
8 A[i + 1] ← m c
8
n - 1
Thời gian thực hiện của thuật toán là tổng thời gian thực thi mỗi câu lệnh
được thi hành; một câu lệnh mất c

i
bước để thực hiện và được thực hiện trong n
lần sẽ thành c
i
n tổng thời gian thực hiện
(**)
. Để tính T(n), thời gian thực hiện của
INSERTION-SORT, chúng ta cộng các kết quả các cột chi phí và thời gian, thu
được:
T(n) = c
1
n+c
2
(n – 1) + c
4
(n – 1)+c
5

=
n
j
jt
2
+ c
6

=

n
j

jt
2
)1(
+c
7

=

n
j
jt
2
1
+c
8
(n - 1)
Thậm chí với các đầu vào có một kích cỡ nhất định, thời gian thực hiện
một thuật toán có thể phụ thuộc vào việc cho đầu vào nào có cùng kích cỡ đó. Ví
11
dụ, trong INSERTION-SORT, trường hợp tốt nhất xảy ra nếu mảng được sắp
xếp sẵn rồi. Với mỗi j=2,3,….,n, chúng ta thấy A[i]<=key ở dòng 5 khi i có giá
trị ban đầu là j-1. Như vậy, t
j
= 1 với j = 2,3,…,n và thời gian thực thi tốt nhất là:
T(n) = c
1
n + c
2
(n-1)+ c
4

(n-1)+ c
5
(n-1)+ c
8
(n-1)
= (c
1
+ c
2
+ c
4
+ c
5
+ c
8
)n - (c
1
+ c
2
+ c
4
+ c
5
+ c
8
)
Thời gian thực hiện này có thể được mô tả như an+b với các hằng số a và
b phụ thuộc vào hao phí câu lệnh c
i
; như vậy, nó là một hàm tuyến tính của n.

Nếu mảng được sắp xếp theo thứ tự đảo ngược – nghĩa là theo thứ tự giảm
– trường hợp xấu nhất sẽ xảy ra. Chúng ta phải so sánh mỗi giá trị A[j] với mỗi
giá trị trong toàn bộ mảng con A[1 j-1] đã sắp xếp và t
j
= j trong đó j=2,3, ,n.
Lưu ý rằng:
1
2
)1(
2

+
=

=
nn
j
n
j


=

=−
n
j
nn
j
2
2

)1(
)1(
(Chương 3 sẽ đề cập lại các phép tổng này), ta thấy rằng trong trường hợp
xấu nhất, thời gian thực hiện của INSERTION-SORT là
T(n) = c
1
n + c
2
(n – 1)+c
4
(n – 1)+c
5







+
1
2
)1(nn
+
c
6







+
2
)1(nn
+
c
7






+
2
)1(nn
+
c
8
(n – 1)
=






++
222

765 ccc
n
2
+






+−−+++ 8
765
421
222
c
ccc
ccc
n – (c
2
+ c
4
+ c
5
+ c
8
)
Thời gian thực hiện trong trường hợp xấu nhất này có thể được diễn tả
bằng công thức an
2
+ bn + c với a,b và c là hằng số phụ thuộc vào giá trị thực

hiện lệnh c
j
; do đó nó là hàm bậc hai của n.
Thông thường như trong trường hợp sắp xếp theo kiểu chèn (insertion
sort), thời gian thực thi một thuật toán được cố định với một đầu vào được cho,
mặc dù trong các chương sau, chúng ta có thể thấy một số thuật toán ngẫu nhiên
mà cách hoạt động của nó có thể biến thiên ngay cả giá trị đầu vào cố định.
12
)#$D>d5e"Pf%f !D>d5e"D%5[g
Trong việc phân tích sắp xếp theo kiểu chèn ở trên, chúng ta xem xét cả
cách tốt nhất, mà mảng đầu vào đã được sắp xếp sẵn, và trong trường hợp xấu
nhất, mà mảng đầu vào đã được sắp xếp đảo ngược. Tuy nhiên, phần còn lại của
cuốn sách này, chúng ta thường tập trung vào việc chỉ tìm kiếm thời gian thực
hiện xấu nhất, nghĩa là, thời gian thực hiện là dài nhất cho bất kỳ một đầu vào
nào có kích cỡ là n. Sở dĩ như vậy vì ba lý do như sau:
* Thời gian thực hiện xấu nhất của một thuật toán là cận trên đối với thời
gian thực hiện của bất kỳ đầu vào nào. Biết rằng nó cho chúng ta sự đảm bảo
rằng thuật toán sẽ không bao giờ kéo dài lâu hơn nữa. Chúng ta không cần phải
suy đoán này nọ về thời gian thực hiện và hy vọng rằng nó không trở nên có thể
xấu hơn thế.
* Đối với một số thuật toán, trường hợp xấu nhất xuất hiện khá thường
xuyên. Chẳng hạn như trong việc tìm kiếm một dữ liệu cụ thể trong một cơ sở dữ
liệu, trường hợp xấu nhất của thuật toán thường xảy ra khi thông tin đó không
hiện diện trong cơ sở dữ liệu. Trong một số ứng dụng tìm kiếm, ta thường gặp
các trường hợp tìm kiếm thông tin không có mặt.
* Trường hợp trung bình cũng tệ hại tương tự như trường hợp không xấu
nhất. Giả sử là chúng ta chọn ngẫu nhiên n con số và áp dụng kỹ thuật tìm kiếm
theo kiểu sắp xếp chèn. Sẽ mất bao lâu để xác định vị trí trong mảng con A[1 j-
1] để chèn số hạng A[j]? Tính trung bình, một nửa các số hạng trong A[1 j-1] là
nhỏ hơn A[j] và một nửa số hạng là lớn hơn. Như vậy, tính trung bình, chúng ta

kiểm tra được một nửa mảng con A[1 j-1], nên t
j
=j/2. Nếu chúng ta tính ra kết
quả là trường hợp trung bình thời gian thực thi, nó thành ra trở thành một hàm
bậc 2 của kích cỡ đầu vào, hệt như thời gian thực hiện trường hợp xấu nhất.
Trong một số trường hợp cụ thể, chúng ta sẽ quan tâm đến thời gian thực
hiện mong đợi hoặc trường hợp trung bình của thuật toán, trong Chương 5 chúng
ta sẽ thấy kỹ thuật phân tích theo xác suất, bằng cách này, chúng ta xác định thời
gian thực thi được mong đợi. Một bài toán có thể tiến hành phân tích trường hợp
trung bình, đó là khi không nắm rõ nội dung tạo thành một đầu vào “trung bình”
cho một bài toán cụ thể. Thường thì chúng ta sẽ thừa nhận rằng tất cả các đầu
13
vào có cùng một kích cỡ nhất định có thể xảy ra như nhau. Trong thực tế, giả
thiết này có lẽ bị trái ngược, song đôi lúc, chúng ta buộc phải sử dụng thuật toán
ngẫu nhiên, mà nó xây dựng các lựa chọn ngẫu nhiên, để cho phép các phân tích
theo xác suất.
f"h5D>i5
Chúng ta đã từng sử dụng một số khái niệm trừu tượng được đơn giản hóa
để tạo thuận lợi cho tiến trình phân tích thủ tục INSERTION-SORT. Đầu tiên,
chúng ta đã bỏ qua các chi phí thực tế của mỗi câu lệnh, sử dụng hằng số c
i
để
đại diện cho chi phí của nó. Sau đó, chúng ta quan sát ngay cả các hằng số của
chúng cho ta biết chi tiết hơn ta cần: thời gian thực hiện xấu nhất là an
2
+ bn + c
với hằng số a, b và c phụ thuộc vào giá trị câu lệnh c
i
. Vì thế chúng ta bỏ qua
không chỉ chi phí câu lệnh thực tế mà còn bỏ qua chi phí trừu tượng c

i
.
Bây giờ chúng ta sẽ làm nhiều hơn một khái niệm trừu tượng được đơn
giản hóa. Nó là tỷ lệ phát triển hay cấp tăng trưởng của thời gian thực thi mà ta
thực sự quan tâm. Vì thế chúng ta xem như chỉ giới hạn chủ đạo của công thức
(ví dụ an
2
) từ giới hạn thứ tự thấp hơn là tương đối tầm thường với n lớn. chúng
ta còn bỏ qua hệ số hằng số giới hạn chủ đạo, từ khi thừa số hằng số ít có ý nghĩa
hơn tỷ lệ phát triển trong việc xác định hiệu suất tính toán với đầu vào lớn. Vì
vậy, chúng ta viết sắp xếp chèn, ví dụ có thời gian thực thi xấu nhất của Θ(n
2
)
(đọc là têta của n bình phương). Chúng ta sẽ sử dụng luôn ký hiệu Θ trong
chương này, nó sẽ được định nghĩa chính xác ở chương 3.
Chúng ta thường xem một thuật toán trở nên có hiệu quả hơn thuật toán
khác nếu thời gian thực hiện xấu nhất của nó có một cấp tăng trưởng thấp hơn.
Nhờ có các nhân tố hằng số và số hạng thứ tự thấp hơn, việc ước lượng có lẽ bị
lỗi nếu các đầu vào nhỏ. Nhưng với đầu vào đủ lớn, ví dụ, thuật toán Θ(n
2
) sẽ
chạy nhanh hơn trong trường hợp xấu nhất hơn thuật toán Θ(n
3
).
(*)
: Có một sự khôn khéo ở đây. Các bước thực hiện tính toán mà ta nêu ra
bằng tiếng Anh thường là các thủ tục được yêu cầu một khoảng thời gian thực
hiện thay đổi. Ví dụ, về phía cuối của sách này, chúng ta nói: sắp xếp các điểm
bằng điều phối biến x như chúng ta đã biết, mất hơn một khoảng thời gian không
đổi. Ngoài ra, cần chú ý rằng các câu lệnh được gọi là thủ tục con sẽ mất một

14
khoảng thời gian không đổi để thực thi, thông qua các thủ tục con này, dẫn
chứng cho thấy có thể mất thời gian nhiều hơn. Vì thế, chúng ta tách rời các tiến
trình của cái được gọi là các tham số thủ tục con với nó từ tiến trình của việc
thực thi thủ tục con.
(**)
: Các đặc tính này không cần thiết để lưu giữ tài nguyên như bộ nhớ.
Câu lệnh mà nó tham chiếu m ký tự của bộ nhớ và được thực thi trong n lần
không thực sự cần thiết phải dùng hết m*n ký tự trong tổng bộ nhớ.
a!&"M4MjN
Biễu diễn hàm n
3
/1000 – 100n
2
– 100n + 3 trong số hạng ký hiệu Θ.
a!&"M4MjM
Xem như việc sắp xếp n số lưu trong mảng A bằng việc tìm kiếm phần tử
nhỏ nhất đầu tiên của A và hoán đổi nó với phẩn tử A[1]. Sau đó tìm phần tử nhỏ
nhất thứ hai trong A và hoán đổi nó với A[2]. Tiếp tục như thế với n-1 số hạng
đầu tiên trong A. Viết giải mã cho thuật toán này với kiểu sắp xếp lựa chọn.
Thuật toán này duy trì vòng lặp không đổi nào? Tại sao cần chạy chỉ n-1 phần tử
đầu tiên hơn là tất cả n phần tử? Cho thời gian thực thi tốt nhất và xấu nhất của
cách sắp xếp lựa chọn này trong ký hiệu Θ.
a!&"M4Mjb
Xem như việc tìm kiếm tuyến tính lần nữa (bài 2.1-3). Có trung bình bao
nhiêu phần tử đầu vào cần được kiểm tra, biết rằng phần tử đang được tìm kiếm
là tương đương với phần tử khác trong mảng? Trường hợp xấu nhất thì như thế
nào? Thời gian thực thi trong trường hợp trung bình và không tối ưu của tìm
kiếm tuyến tính trong ký hiệu Θ là gì?
a!&"M4Mjk

Chúng ta bổ sung thuật toán như thế nào để có thời gian thực thi tối ưu
tốt?
15
M4b%&'(
Có nhiều cách để thiết kế một thuật toán: Ví dụ như sắp xếp chèn sử dụng
cách tiếp cận tăng dần (incremental) để sắp xếp mảng A[1 j-1], chúng ta chèn
một thành phần A[j] đơn lẻ vào nơi thích hợp, vùng đã sắp xếp mảng A[1 j]
Trong phần này chúng ta nghiên cứu một phương pháp thiết kế khác, được
hiểu như là: “divide-and-conquer” (chia để trị). Chúng ta sử dụng “divide-and-
conquer” để thiết kế một thuật toán sắp xếp mà trường hợp xấu nhất của phương
pháp này là thời gian thực hiện là ít hơn so với thuật toán sắp xếp chèn. Thuận
lợi của thuật toán “divide-and-conquer” là thời gian thực hiện của nó dễ dàng
quyết định việc sử dụng phương pháp và nó được giới thiệu trong Chương 4.
M4b4N5%:@FlUY EJj7Ej'm%JDVR7noD?S
Những thuật toán hữu ích đó là cấu trúc đệ quy (recursive): để giải quyết
một bài toán được đưa ra, chúng gọi đệ quy đến chúng một hoặc nhiều lần để xử
lý các bài toán con liên quan với nhau. Nguyên lý “divide-and-conquer” được
hiểu như sau: Chúng ngắt những bài toán thành nhiều bài toán con tương tự với
bài toán ban đầu nhưng kích thước nhỏ hơn, xử lý những bài toán con đệ quy và
sau đó kết nối những cách xử lý này để tạo ra một phương pháp giải quyết cho
bài toán gốc.
Nguyên lý “divide-and-conquer” bao gồm 3 bước tại mỗi mức của đệ
quy:
jY EJ Chia bài toán thành một số bài toán con
j'm%JD: Xâm chiếm vào những bài toán con bằng việc giải quyết nó đệ
quy. Nếu kích thước bài toán này là đủ nhỏ, giải quyết bài toán này bằng cách
tiến thẳng.
j'G[J: Kết hợp nhiều phương pháp giải quyết các bài toán con thành
phương pháp giải quyết cho bài toán gốc
* §ng dụng nguyên lý “divide-and-conquer” vào thuật toán sắp xếp hòa

nhập:
Y EJ: chia dãy n đã được sắp xếp thành hai dãy con thành phần n/2
16
'm%JD: Sắp xếp hai dãy con bằng cách đệ quy sử dụng phương pháp
sắp xếp trộn.
'G[J Trộn hai dãy con đã được sắp xếp để tạo thành một dãy và sắp
xếp.
Đệ quy U[''GH'%V(dưới lên) khi sắp xếp một dãy có chiều dài là 1,
trong trường hợp dãy đó không làm gì cả thì cứ mỗi dãy có chiều dài bằng 1 là
được sắp xếp thứ tự.
Chìa khoá của việc thực hiện thuật toán sắp xếp hòa nhập là hòa nhập hai
dãy đã được sắp xếp thành “combine” bước (bước kết hợp). Để thực hiện việc
hòa nhập chúng ta sử dụng một thủ tục con Merge (A,p,q,r) mà ở đó A là mảng
và p,q,r là các chỉ số phần tử của mảng, và p≤ q<r. Thủ tục này chỉ ra rằng mảng
con A[p q] và mảng con A[q+1 r] là được sắp xếp thứ tự. Nó hòa nhập hai
mảng con này để thiết lập mảng con đã được sắp xếp đơn lẻ thay thế cho mảng
A[p r] hiện thời.
Thủ tục MERGE này tiêu tốn thời gian là Θ(n) mà n=r-p+1số lần hòa
nhập và nó làm việc như sau: Trở lại với bài toán chơi bài, chúng ta có hai lá bài
lật ngửa ở trên bàn. Mỗi lá bài là đã sắp xếp, lấy những lá bài nhỏ nhất ở trên
đỉnh, chúng ta hòa nhập hai lá bài này thành một lá bài đã sắp xếp riêng lẻ xuất
ra mà nó đã được úp lại trên bàn. Ta muốn hòa nhập hai lá bài thành một lá bài
xuất ra và đã được sắp xếp mà mỗi lá bài là úp lại trên mặt bàn. Bước cơ bản của
ta là chọn hai lá bài nhỏ hơn ở trên đỉnh của những lá lật ngửa, di chuyển nó từ
những lá (phơi bày ra một đỉnh bài mới) và vị trí của lá bài này úp trên lá bài
xuất ra. Chúng ta lặp lại bước này cho đến khi một lá bài nhập vào là trống, tại
thời điểm này chúng ta chỉ còn lại lá bài nhập vào và vị trí của lá bài này úp lên
lá bài xuất ra.Theo như ước tính, mỗi bước cơ bản mất một hằng số lần. Khi đó
ta chỉ kiểm tra hai lá bài ở trên đỉnh. Lúc đó ta chỉ thực hiện n bước cơ bản. Việc
hòa nhập mất thời gian Θ(n) lần.

Theo sau những ý tưởng giả mã, với sự cộng thêm vòng xoắn để tránh
khỏi kiểm tra, mặc dù mỗi lá bài là trống trong mỗi bước cơ bản. Ý tưởng này là
đặt lên phía dưới của mỗi lá bài là một con bài HJJF (đứng canh) mà chứa giá
17
trị đặc biệt nào đó để đơn giản cho việc viết mã. Ở đây, chúng ta sử dụng ∞ như
là một giá trị đứng canh, vì vậy bất cứ khi nào một con bài ∞ được mở ra, nó
không thể nhỏ hơn trừ khi tất cả các lá bài có con bài đứng canh được mở ra.
Nhưng một lần nữa điều này đã xảy ra, tất cả các con bài không đứng gác phải
sẵn sàng ở vị trí trên lá bài xuất ra. Từ đó ta thấy là có chính xác r-p+1 lá bài sẽ ở
vị trí trên lá bài xuất ra, chúng ta phải ngừng một lần khi chúng ta thực hiện nó
trong nhiều bước cơ bản.
MERGE (A,p,q,r)
1 n
1
:= q - p + 1
2 n
2
:= r - q
3 create arrays L[1 n
1
+ 1] and R[1 n
2
+ 1]
4 for i := 1 to n
1
5 do L[i] := A[p + i - 1]
6 for j := 1 to n
2
7 do R[j] := A[q + j]
8 L[n

1
+ 1] := ∞
9 R[n
2
+ 1] := ∞
10 i := 1
11 j := 1
12 for k := p to r
13 do if L[i] ≤ R[j]
14 then A[k] := L[i]
15 i := i + 1
16 else A[k] := R[j]
17 j := j + 1
Từ thuật toán trên, thủ tục MERGE làm việc như sau: Dòng 1 của máy
tính có chiều dài n
1
của mảng con A[p q] và dòng 2 thì có chiều dài n
2
của mảng
con A[q+1 r]. Chúng ta tạo ra mảng L và R (“left” và “right”) có chiều dài n
1
+1
và n
2
+1 được nêu cụ thể trong dòng 3. Vòng lặp for của dòng 4 và 5 đã gán
mảng A[p q] cho mảng L[1 n] và vòng lặp for ở dòng 6, 7 gán mảng A[q+1 r]
thành mảng R[1 n
2
]. Dòng 8, 9 đặt lá bài đứng canh cuối của mảng Lvà R.
Dòng 10, 17 được minh hoạ trong Hình 2.3 thực hiện r-p+1 bước cơ bản bằng

việc duy trì theo sau biến vòng lặp. Bắt đầu sự lặp lại của vòng lặp for của dòng
18
12-17, mảng A[p k-1] chứa k-p phần tử nhỏ nhất của mảng L[1 n
1
+1] và mảng
R[1 n
2
+1] được sắp xếp thứ tự. Tuy nhiên, mảng L[i] và R[j] là phần tử nhỏ
nhất của mảng mà không được gán cho mảng A.
Hình 2.3
Quá trình hoạt động của dòng 10-17 trong lời gọi thủ tục
MERGE(A,9,12,16), khi mảng con A[9 16] chứa dãy (2,4,5,7,1,2,4,5,6). Sau
khi đã gán và chèn con bài đứng canh, mảng L chứa (2,4,5,7,∞) và mảng R chứa
(1,2,3,6,∞). Những vùng đã được tô bóng sáng trong mảng A chứa giá trị cuối
cùng của mảng và những vị trí đã được tô bóng sáng trong mảng L và R chứa giá
trị mà đã được gán cho mảng A. Lấy cùng thời điểm đó những vị trí đã được tô
bóng sáng luôn bao gồm những giá trị gốc trong mảng A[9 16] dọc theo với hai
con bài đứng canh. Những vị trí đã được tô bóng trong mảng A chứa những giá
trị sẽ được sao chép ở trên và những vị trí đã được tô bóng trong mảng L và R
chứa những giá trị mà sẵn sàng đã được gán cho mảng A. (a) - (h). Các mảng A,
L và R và những chỉ số k, i, j của chúng và chỉ số j trước sự lặp lại của vòng lặp
từ dòng 12-17.(i) các mảng và chỉ số tại thời điểm kết thúc. Tại thời điểm này
mảng con A[9 16] là được sắp xếp và hai con bài đứng canh trong mảng L và R
là chỉ hai phần tử trong mảng này mà không được gán cho mảng A.
Chúng ta phải thấy rằng biến vòng lặp này giữ ưu tiên của phép lặp for
đầu tiên từ dòng 12-17. Tại mỗi phép lặp của vòng lặp, biến được duy trì và biến
này cung cấp một thuộc tính hữu ích khi vòng lặp kết thúc.
19
7Fp7' (Khởi tạo): Trước sự lặp lại của vòng lặp đầu tiên, chúng ta
có k=p vì vậy mảng con A[p k-1] là trống. Mảng con trống này chứa k-p=0 phần

tử nhỏ nhất của mảng L và R và khi i=j=1,cả L[i] và R[j] là phần tử nhỏ nhất của
mảng mà không được gán từ mảng A.
Hình 2.3
7J7J(Sự duy trì): Để thấy rằng duy trì sự lặp đi lặp lại của mỗi
biến vòng lặp, đầu tiên chúng ta biết là L[i]

R[j]. Sau đó L[i] là phần tử nhỏ
nhất chưa được gán từ mảng A, bởi vì mảng A[p k-1] chứa k-p phần tử nhỏ nhất,
sau đó dòng 14 gán mảng L[i] cho A[k]. Mảng con A[p k] sẽ chứa k-p+1 phần tử
nhỏ nhất. Tăng biến k (cập nhật lại vòng lặp for) thiết lập lại biến vòng lặp cho
sự lặp lại kế tiếp. Nếu L[i]>R[j] thì dòng 16, 17 thực hiện hoạt động duy trì
biến vòng lặp.
JDG7' (Sự kết thúc): Tại thời điểm kết thúc k=r+1.Vì biến vòng
lặp của mảng con A[p k-1], mà mảng A[p r] chứa k-p=r-p+1 phần tử nhỏ nhất
của mảng L[1 n
1
+1] và mảng R[1 n
2
+1] được sắp xếp thứ tự. Mảng L và R cùng
20
chứa n
1
+n
2
+2=r-p+3 phần tử. Hai phần tử lớn được sao chép từ mảng A và hai
phần tử lớn này là hai lá bài đứng gác.
Ta thấy rằng thủ tục MERGE chạy với thời gian Θ(n) lần mà n=r-p+1,
quan sát điều này ta thấy từ dòng 1-3 và dòng 8-11 mất thời gian là một hằng số.
Vòng lặp for của dòng 4-7 mất Θ(n
1

+n
2
) = Θ(n) lần
(6)
, từ dòng 12-17 vòng lặp
for lặp n lần, mà mỗi lần mất thời gian là một hằng số lần.
Bây giờ chúng ta sử dụng thủ tục MERGE như là một thủ tục con trong
thuật toán sắp xếp trộn. Thủ tục MERGE-SORT (A,p,r) sắp xếp những phần tử
trong mảng con A[p r]. Nếu p>=r thì hầu như các mảng con có một phần tử đã
được sắp xếp. Mặt khác bước chia đơn giản của một máy tính một chỉ số q mà
phân vùng của mảng A[p r] thành hai mảng con: A[p q] chứa n/2 phần tử và
A[q+1 r] chứa n/2 phần tử
(7)
MERGE-SORT(A, p, r)
1 if p < r
2 then q ← ⌊(p + r)/2⌋
3 MERGE-SORT(A, p, q)
4 MERGE-SORT(A, q + 1, r)
5 MERGE(A, p, q, r)
Để sắp xếp toàn bộ dãy A(A[1], A[n]), ban đầu chúng ta phải gọi thủ
tục MERGE SORT(A,1,length[A]). Mà ở đó chiều dài của mảng A=n.
Hình 2.4 Minh hoạ việc thực hiện thủ tục “bottom-up” khi n là luỹ thừa
của 2. Thuật toán này thực hiện việc trộn những cặp của một khoảng dãy để hình
thành những dãy đã được sắp xếp có chiều dài 2. Và sau đó trộn những cặp của
những dãy có chiều dài là 2 thành những dãy đã được sắp xếp có chiều dài 4. Và
tiếp tục như vậy cho đến khi hai dãy đã được sắp xếp có chiều dài n/2 được trộn
lại thành một dãy cuối cùng đã được sắp xếp có chiều dài n.
Hình 2.4 Thực hiện việc sắp xếp hòa nhập trên một mảng
A=(5,2,4,7,1,3,2,6). Chiều dài của dãy đã được sắp xếp và hòa nhập lại tăng lên
khi thuật toán tăng dần từ đáy lên đỉnh.

21
Hình 2.4
M4b4M)#$%&'(E EJ7E'm%JD
Khi một thuật toán chứa lời gọi đệ quy đến chính nó. Thời gian thực hiện
của nó có thể được mô tả bởi một công thức truy hồi cân bằng hoặc công thức
truy hồi và nó mô tả hầu hết thời gian thực hiện một bài toán có kích cỡ n cùng
thời gian thực hiện nhỏ hơn đầu vào. Sau đó chúng ta có thể sử dụng các phương
pháp toán học để giải quyết công thức truy hồi và cung cấp những giới hạn trong
việc thực hiện các thuật toán.
Thời gian thực hiện công thức truy hồi của nguyên lý divide and conquer
là dựa trên 3 bước cơ bản. Trước hết ta cho T(n) là thời gian thực hiện bài toán
với kích thước n. Nếu kích thước của bài toán là đủ nhỏ, ta nói n<=c (c là hằng
số) . Giải pháp tiến thẳng mất thời gian là một hằng số mà ta viết là T(1).Việc
chia bài toán thành các bài toán con mà mỗi bài toán con có kích thước là 1/b bài
toán ban đầu (Ví dụ như thuật toán trộn cả a và b đều cùng bằng 2, nhưng ta thấy
rằng trong nhiều thuật toán divide-and-conquer thì a và b là khác nhau). Nếu ta
mất D(n) lần để chia bài toán thành các bài toán con và C(n) lần để kết hợp các
cách giải quyết bài toán con thành cách giải quyết cho bài toán ban đầu. Chúng
ta có công thức truy hồi như sau:
22
(1)
( )
( / ) ( ) ( )
if n c
T n
aT n b D n C n otherwise
Θ <=

=


+ +

Trong Chương 4 (của tài liệu dịch) chúng ta sẽ thấy được cách giải
quyết chung chung cho mẫu công thức truy hồi này.
)#$%&'(HO"P"q7&"
Mặc dầu mã giả của thuật toán MERGE SORT viết đúng khi một số phần
tử là không thể xảy ra, thì cơ sở của công thức truy hồi này phân tích thật đơn
giản. Nếu chúng ta cho rằng kích cỡ của bài toán gốc là luỹ thừa của 2. Cứ mỗi
lần chia thì vùng của hai dãy con có kích thước chính xác là n/2. Trong Chương
4 chúng ta sẽ thấy rằng giả định này không có hiệu quả thứ tự phát triển giải
pháp của công thức truy hồi.
Ta lý luận để thành lập công thức truy hồi cho T(n) như sau: Trường hợp
xấu nhất thời gian chạy của thuật toán MERGE SORT chỉ có n số. MERGE
SORT chỉ là một phần tử nên nó chỉ mất là hằng số lần. Khi chúng ta có n>1
phần tử, chúng ta sẽ chia thời gian thực hiện như sau:
Y EJ: Chia từng bước để thực hiện phần tử giữa mảng con mà mỗi lần
chia mất thời gian là một hằng số. Do đó, D(n)= Θ(1).
'm%JD: Đệ quy để giải quyết hai bài toán con, mỗi bài có kích thước là
n/2 và thời gian chạy của nó là 2T(n/2).
'G[J: Thủ tục MERGE của một mảng con n phần tử tiêu tốn thời gian
là Θ(n), vậy C(n)= Θ(n).
Khi ta thêm vào hàm D(n) và C(n) cho việc phân tích thuật toán MERGE
SORT là ta đang thêm một hàm Θ(n) và một hàm là Θ(1).
Tổng của hai hàm này là một hàm có chiều dài n và thời gian thực hiện là
Θ(n). Việc thêm nó vào để có một thuật ngữ là 2T(n/2) từ bước “conquer” là ta
có một công thức truy hồi cho thuật toán MERGE SORT mà trường hợp xấu
nhất là thời gian chạy bằng T(n).

23
(2.1)

(1) 1
( )
2 ( / 2) ( ) 1
if n
T n
T n n if n
Θ =

=

+ Θ >

Trong Chương 4, ta nắm vững nguyên tắc để ta có thể biểu diễn T(n) là
Θ(nlgn) mà lgn bằng log
2
n. Bởi vì hàm logarit phát triển chậm hơn so với bất kỳ
tuyến hàm nào khác. Đối với thuật toán hòa nhập, dữ liệu nhập vào lớn và thời
gian thực hiện của nó là Θ(nlgn) và nó làm tốt hơn thuật toán sắp xếp chèn.
Trong trường hợp xấu nhất, thời gian chạy của nó là Θ(n
2
)
Ta không cần phải nắm vững nguyên tắc tại sao công thức truy hồi (2.1) là
T(n)= Θ(nlgn). Viết lại công thức truy hồi (2.1) như sau:

1
( )
2 ( / 2) 1
C if n
T n
T n C if n

=

=

+ >

Và hằng số c mô tả thời gian yêu cầu giải quyết bài toán có kích thước là 1
tốt như là thời gian cho mỗi phần tử mảng của từng bước chia và kết hợp.
Hình 2.5 Mô tả cách giải quyết công thức truy hồi (2.2). Ta thấy rằng n là
luỹ thừa của 2. Phần (a) biểu diễn T(n), mà trong phần (b) đã mở rộng ra thành
một cây tương đương biểu diễn công thức truy hồi. cn là gốc (giá trị tại mức
đỉnh của đệ quy) và hai cây con của nó là hai công thức truy hồi T(n/2) nhỏ hơn.
Phần (c) chỉ ra quá trình thực hiện từng bước để mở rộng cây T(n/2). Giá trị cho
hai nút con tại mức thứ hai của phép đệ quy là cn/2. Ta tiếp tục mở rộng các nút
trên cây bằng cách chẽ nó ra thành những cây thành phần bằng công thức truy
hồi cho đến khi kích cỡ của bài toán giảm xuống 1 mà mỗi nhánh là một giá trị c.
Phần (d) biểu diễn cây kết quả.
24
Hình 2.5
Hình 2.5 Cấu trúc của một cây đệ quy cho ta một công thức truy hồi
T(n)=2T(n/2)+cn. Phần (a) chỉ ra công thức truy hồi T(n) và nó tăng mở rộng
25

×