T i li u tham kh o à ệ ả
môn h c c u trúc dọ ấ ữ
li uệ
- 1 -
LỜI NÓI ĐẦU
Giáo trình này được viết cho sinh viên các trường Cao đẳng Đại học
Để học tốt giáo trình này sinh viên cần có kiến thức về tin học cơ sở và đã biết một ngôn
ngữ lập trình bậc cao như Pascal, Delphi, C hay C
++
...
Giáo trình được viết nhằm những mục tiêu sau đây:
• Giới thiệu cho học sinh về khái niệm về kiểu dữ liệu trừu tượng.
• Cung cấp cho học sinh kiến thức cơ bản về những kiểu dữ liệu trừu tượng, cấu
trúc dữ liệu thông dụng, nâng cao và những thao tác trên các cấu trúc đó.
• Cung cấp một số thuật toán cơ bản và rèn luyện một số kĩ năng phân tích thuật
toán cho học sinh.
• Rèn luyện cho học sinh cách áp dụng các cấu trúc dữ liệu đã học và tư duy thuật
toán để có thể thiết kế và cài đặt một số chương trình bằng một ngôn ngữ bậc
cao.
Để có thể học tốt các chương sau, sinh viên cần đọc kĩ chương 1 và chương 2. Chương
1 giới thiệu những khái niệm cơ bản về thuật giải. Chương 2 giới thiệu những khái
niệm cơ bản về kiểu dữ liệu trừu tượng. Đây là khái niệm mới đối với học sinh. Một
kiểu dữ liệu trừu tượng là một tập hợp các đối tượng và được xác định hoàn toàn bởi
các phép toán có thể biểu diễn trên các đối tượng đó. Người sử dụng được phép tìm
hiểu các đối tượng và thực hiện các thao tác trên các đối tượng của kiểu dữ liệu thông
qua các phép toán đó mà không cần quan tâm đến việc đối tượng được cài như thế nào
trong ngôn ngữ lập trình. Chúng tôi có đưa ra một vài ví dụ đơn giản về kiểu dữ liệu
trừu tượng, cách đặc tả và xây dựng chúng để định hướng cho việc đặc tả và xây dựng
những kiểu dữ liệu trừu tượng trong những chương sau.
Trong khi trình bày mỗi kiểu dữ liệu trừu tượng ở mỗi chương sau, chúng tôi cố gắng
khuyến khích sinh viên trước hết phải hiểu một cách khái quát về kiểu dữ đó trước khi
quan tâm đến việc cài đặt chi tiết bên trong. Điều đó không có nghĩa là chúng tôi không
coi trọng việc cài đặt. Việc tách riêng đặc tả bên ngoài và bên trong cho phép ta nhìn rõ
trước hết vào bản chất của một kiểu dữ liệu trừu tượng là tập các thao tác trên các đối
- 2 -
tượng của kiểu dữ liệu đó. Và chỉ khi đã hiểu rõ bản chất của các thao tác trên một kiểu
dữ liệu trừu tượng chúng ta mới chuyển tới việc cài đặt những thao tác đó bằng một
ngôn ngữ bậc cao nào đó. Công cụ cho phép ta bóc tách một cách khái niệm hình thức
bên ngoài và chi tiết bên trong đó chính là kiểu dữ liệu trừu tượng.
Từ chương 3 đến chương 6 dành cho việc nghiên cứu một số kiểu dữ liệu trừu tượng cơ
bản tuyến tính và phi tuyến. Đó là các kiểu dữ liệu trừu tượng ngăn xếp, hàng đợi, cây,
bảng tìm kiếm, tập hợp và đồ thị... Với mỗi kiểu dữ liệu trừu tượng đưa ra, chúng đều
được đặc tả bởi các thao tác cơ bản và hướng dẫn cài đặt một số thao tác. Những thao
tác đưa ra là những thao tác cơ bản nhất. Tuy nhiên, tuỳ theo điều kiện và hoàn cảnh,
học sinh có thể bổ sung thêm một số những thao tác khác. Trong việc xây dựng các
kiểu dữ liệu trừu tượng, chúng tôi rất nhấn mạnh việc quan tâm đầu tiên là đặc tả được
các thao tác cần thiết (xây dựng mô đun ngoài), sau đó mới đến việc cài đặt các thao tác
(xây dựng mô đun trong). Ngoài ra, chúng tôi cũng quan tâm đến việc hướng dẫn sử
dụng các kiểu dữ liệu trừu tượng thông qua những ví dụ về những mô đun ứng dụng.
Sinh viên ắt hẳn đã được giới thiệu một vài thuật toán sắp xếp đơn giản trên mảng từ
phần Tin học đại cương. Tuy nhiên, bài toán sắp xếp và tìm kiếm là bài toán hết sức cơ
bản đối với mỗi sinh viên Cao đẳng và Đại học nên chúng tôi dành riêng chương 7 để
nói về một số giải thuật sắp xếp đơn giản cũng như một số giải thuật sắp xếp nâng cao,
cần đến một chút kiến thức về kiểu dữ liệu trừu tượng vừa học trong các chương trước.
Một số phép so sánh các giải thuật sắp xếp cũng được đề cập trong chương này.
Để giúp học sinh có thêm một số kiến thức về thuật giải, trong chương 8, chúng tôi trình
bày một số phương pháp thiết kế thuật giải như đệ qui, quay lui, chia dể trị, tham lam,
qui hoạch động. Đây là những phương pháp rất cơ bản và thông dụng trong khoa học
máy tính. Bạn đọc có thể tìm thấy nhiều ví dụ minh hoạ cho những phương pháp này.
Các ví dụ đều được cài đặt cụ thể bằng ngôn ngữ lập trình Pascal.
Cuối mỗi chương đều có hệ thống bài tập để học sinh làm và tự kiểm tra kiến thức của
mình.
Cuối cùng là phần phụ lục, trong đó chúng tôi có đưa ra một số bài tập lớn, để học sinh
áp dụng những kiến thức đã học tập giải quyết những vấn đề phức tạp hơn so với bài tập
sau mỗi chương. Với những bài tập lớn này, để nâng cao hiệu quả, yêu cầu học sinh cần
- 3 -
đặc tả rõ bài toán, phân tích và thiết kế thuật giải, sau đó cài đặt trên một ngôn ngữ cụ
thể. Tiếp theo học sinh cần phải kiểm thử phần mềm mình vừa cài đặt và có những
nhận xét về độ phức tạp của thuật toán mình thiết kế, về các hướng cần mở rộng cho các
bài tập này. Chỉ khi nào học sinh làm tốt các bài tập lớn đó mới có thể nói là họ đã
thành công trong việc học tập môn học này.
Chủ biên và hiệu đính toàn bộ nội dung bản thảo:
Chương 1: Mở đầu
chương 2: .
Chương 3: .
Chương 4,
Chương 6:
Chương 7:
Chương 8:
- 4 -
CHƯƠNG I: MỞ ĐẦU
Chương này trình bày một số khái niệm cơ bản đầu tiên về bài toán theo quan điểm Tin
học, thuật giải, cấu trúc dữ liệu, mối quan hệ giữa cấu trúc dữ liệu và thuật giải. Những
khái niệm về độ phức tạp của thuật giải và phân tích thuật giải cũng được giới thiệu
trong chương.
1.1. Khái niệm về cấu trúc dữ liệu và thuật giải
1.1.1. Khái niệm bài toán
Trong phạm vi Tin học, ta có thể quan niệm bài toán là bất cứ việc gì ta muốn máy tính
thực hiện. Như vậy những việc như viết một dòng chữ ra màn hình, giải phương trình
bậc hai, giải hệ phương trình bậc nhất hai ẩn số, quản lý các cán bộ trong một cơ quan...
đều là các ví dụ về bài toán.
Khi dùng máy tính giải bài toán, ta chỉ cần quan tâm đến hai yếu tố: đưa vào máy thông
tin gì (thông tin vào) và cần lấy ra thông tin gì (thông tin ra), Thông tin vào còn được
gọi là Input, thông tin ra còn được gọi là Output. Như vậy để phát biểu một bài toán, ta
chỉ cần đặc tả Input và Output của bài toán.
Ví dụ 1. Bài toán tìm ước chung lớn nhất
Input. Hai số nguyên dương M và N
Output. Ước chung lớn nhất của M và N
Ví dụ 2. Bài toán tìm nghiệm của đa thức một biến:
Input: số nguyên dương n; các số thực a
0
, a
1
, . . ., a
n
;
Output: Trả lời câu hỏi có bao nhiêu số thực x khác nhau và giá trị của chúng
(nếu có) sao cho
a
0
+ a
1
x +...+ a
n
x
n
= 0
Ví dụ 3. Bài toán phân tích số:
- 5 -
Input: Số nguyên dương N≥2;
Output: Các số nguyên tố p
1
,..., p
k
; các số nguyên dương α
1
,...,α
k
; sao cho
n = p
1
α
1
...p
k
α
k
;
Ví dụ 4. Bài toán kiểm tra tính nguyên tố:
Input: Số nguyên dương n;
Output: Trả lời câu hỏi n là số nguyên tố hay không?.
Ví dụ 5. Bài toán quản lý hồ sơ cán bộ:
Input: Hồ sơ gốc của các cán bộ trong cơ quan;
Output: Những biểu bảng thống kê, phân loại cán bộ, các quy trình lưu trữ, quản
lý hồ sơ cán bộ.
Ví dụ 6. Bài toán thứ 10 của Hilbert:
Input: P là đa thức (nhiều ẩn) với hệ số nguyên;
Output: Trả lời câu hỏi P có nghiệm nguyên hay không?
Qua các ví dụ trên, ta thấy các bài toán được cấu tạo bởi hai thành phần cơ bản:
- Thông tin vào (input): Cho ta biết những thông tin đã có (các giả thiết);
- Thông tin ra (output): Các thông tin cần tìm từ input (kết luận);
1.1.2. Khái niệm thuật giải
Như vậy trong giáo trình này, việc cho một bài toán có nghĩa là cho input và mô tả
output cần tìm. Vấn đề đặt ra là thế nào là giải một bài toán?
Trước hết cần lưu ý rằng trong toán học có một xu hướng nghiên cứu định tính các bài
toán. Theo xu hướng này, khi xem xét các bài toán, người ta chỉ cần chứng tỏ sự tồn tại
của output khi cho input và nếu có thể, xét xem có bao nhiêu "lời giải" và nghiên cứu
dáng điệu của chúng. Trong các nghiên cứu như vậy, nhiều khi ta không cần tìm ra lời
giải một cách tường minh nhưng bằng cách dùng các công cụ toán học khác nhau một
cách thích hợp ta vẫn có thể chứng minh chặt chẽ các điều khẳng định liên quan đến lời
- 6 -
giải. Mục tiêu của xu hướng nghiên cứu này là chỉ ra điều kiện tồn tại và nếu có thể, sự
duy nhất của lời giải. Sự tồn tại lời giải theo nghĩa này không luôn cho ta cách thức thực
tế để tìm ra lời giải.
Trong khuôn khổ của Tin học, việc giải bài toán có nghĩa trao được cho máy tính cách
giải. Để giao bài toán cho máy tính, ta cần hướng dẫn cho máy các thao tác mà về
nguyên tắc máy có thể thực hiện được. Một cách giải bài toán như vậy được gọi là một
thuật giải (còn gọi là thuật toán hay giải thuật).
Nói một cách đầy đủ hơn,
Thuật giải A để giải bài toán P là một dãy hữu hạn các thao tác đơn giản được
sắp xếp theo một trình tự xác định sao cho sau khi thực hiện dãy thao tác đó, từ
Input của bài toán, ta nhận được Output cần tìm.
Chính quan niệm về việc giải bài toán như vậy là cốt lõi để ta có thể chuyển giao công
việc đó cho máy tính. Như vậy, khác với quan niệm về việc giải một bài toán theo toán
học truyền thống, việc giải bài toán theo quan niệm của Tin học là quá trình thực hiện
một dãy hữu hạn các thao tác đơn giản để có thể từ Input dẫn ra Output một cách tường
minh.
Trong định nghĩa nêu trên, thao tác đơn giản được hiểu là thao tác mà máy tính có thể
thực hiện được. Ví dụ các phép toán số học, các phép so sánh hai giá trị số là các thao
tác đơn giản.
1.1.3. Mô tả thuật giải
Ngay từ khi học môn Tin học cơ sở học sinh đã được làm quen với khái niệm thuật giải
và được học cách mô tả hay diễn đạt những thuật giải đơn giản. Thuật giải cho những
bài toán nhỏ hay thuật giải cho những bài toán lớn khi được thiết kế ở mức thô hay được
mô tả bằng sơ đồ khối. Cách dùng sơ đồ khối để minh họa thuật giải có lợi thế là rất
trực quan, dễ hiểu. Tuy nhiên, với những bài toán lớn, phức tạp, việc dùng sơ đồ khối để
mô tả thuật giải có những hạn chế nhất định. Hạn chế này là có thể là do khuôn khổ
giấy hay màn hình ta dùng để vẽ sơ đồ hay tầm bao quát, theo dõi của mắt ta trên sơ đồ.
- 7 -
Hạn chế này cũng nảy sinh khi bài toán của ta phức tạp, có nhiều vòng lặp lồng nhau,
khi đó việc trình bày sơ đồ có rất nhiều hạn chế.
Ta cũng hay dùng phương pháp liệt kê các bước giải để mô tả thuật giải. Phương pháp
này cũng rất dễ hiểu đối với các bài toán nhỏ. Tuy nhiên, đối với các bài toán lớn, việc
liệt kê sẽ trở nên khó khăn và nếu được thì cũng gây khó khó theo dõi, nhất là đối với
vòng lặp. Hơn thế nữa việc liệt kê các bước giải ta hay phải dùng ngôn ngữ tự nhiên để
mô tả các thao tác nên việc diễn đạt nhiều khi rất khó chính xác.
Phương pháp thứ ba hay được dùng để mô tả thuật giải là dùng giả mã hay ngôn ngữ
phỏng trình. Việc dùng ngôn ngữ phỏng trình khắc phục được những nhiều nhược điểm
của hai phương pháp trên, đặc biệt đối với những bài toán lớn, phức tạp. Cũng có ý
kiến là ta nên chọn một ngôn ngữ lập trình thông dụng làm công cụ để diễn đạt thuật
giải. Tuy nhiên, ai cũng biết là thuật giải cần phải độc lập với ngôn ngữ lập trình. Khi
thuật giải được viết tốt nó có thể được cài đặt bằng bất cứ ngôn ngữ lập trình nào, tùy
theo tính thích hợp của bài toán và ý muốn của người lập trình. Hơn thế nữa, thiết kế
thuật giải là một trong những công việc quan trọng nhất của người lập trình. Nếu ta
chọn một ngôn ngữ lập trình để mô tả thuật giải thì lúc nào ta cũng phải chú ý tuân thủ
các tiểu tiết cú pháp của ngôn ngữ. Điều đó sẽ gây mất thì giờ và ảnh hưởng lớn đến sự
tập trung vào công việc chính là thiết kế và mô tả thuật giải.
Sau đây là ví dụ về mô tả thuật giải tìm ước chung lớn nhất của hai số nguyên m và n.
Cách 1: Dùng sơ đồ khối
- 8 -
t ← m
m ← n mod m
n ← t
False
True
Begin
End
m>0
Write n
Hình 1.1: Sơ đồ khối diễn tả thuật giải tìm ước chung lớn nhất của hai số nguyên
không âm
--------------------------------------
Cách thứ hai: Dùng phương pháp liệt kê các bước giải, ta có thể viết như sau:.
Bước 1. Khi m > 0 thực hiện
t ← m
m ← n mod m
n ← t
Bước 2. Kiểm tra xem nếu m > 0, quay lại bước 1
Bước 3. In kết quả là n và kết thúc
Cách thứ ba: Dùng ngôn ngữ phỏng trình.
Function Ucln(m,n)
var t
Begin
While m > 0 do
t ← m
m ← n mod m
n ← t
Return n
-----------------------------
Trong thực tế, tùy theo từng giai đoạn thiết kế thuật giải, tùy theo mức độ, và đối tượng
trình bày mà người lập trình thường hay chọn phương pháp thích hợp nhất trong ba
phương tiện nêu trên.
- 9 -
Như vậy, việc diễn tả một thuật giải giải một bài toán nào đó có nghĩa là trình bày trình
tự các thao tác cần thực hiện. Tuy nhiên, đó chỉ là cách diễn tả thuật giải giữa người với
người.
Ngay khi thuật giải đã được diễn tả như vậy, con người có thể dùng để giải bài toán với
mỗi bộ dữ liệu vào. Tuy nhiên, theo cách diễn tả này, máy tính vẫn chưa thể thực hiện
các thao tác trong thuật giải dù các thao tác đó rất đơn giản. Thuật giải cần được diễn tả
thành một chương trình gồm những dòng lệnh. Mỗi lệnh là một việc nào đó mà ta đòi
hỏi máy tính thực hiện.
Ngôn ngữ dùng để viết chương trình được gọi là ngôn ngữ lập trình.
Phần đọc thêm
Khái niệm thuật giải trình bày ở trên đủ để ta có thể hình dung để có thể chuyển giao cho
máy tính việc giải một bài toán ta cần diễn tả cách giải như thế nào và cũng là cách nhận
biết thế nào là thuật giải đối với một bài toán nào đó.
Trong quá trình nghiên cứu cách giải bài toán theo nghĩa nêu trên,trong toán học đã diễn
ra một quá trình phát triển đầy kịch tính. Cho tới đầu thế kỷ 20, với lòng tin tiên nghiệm
vào việc mọi bài toán được phát biểu đúng đắn đều có thuật giải giải, nhiều nhà toán học
đầy tài năng và tâm huyết đã tiến công vào các bài toán đang tồn tại như những lời thách
đố trí tuệ con người.
Vấn đề then chốt nhất của lý thuyết tính toán phát sinh ở đây. Để chứng minh một bài toán
nào đó có thuật giải giải nó, ta chỉ cần đề ra một thuật giải theo quan niệm trực giác của
khái niệm này và kiểm định tính đúng đắn của nó đối với bài toán đã cho. Tuy nhiên, khi
cần chứng minh việc không có thuật giải giải một bài toán nào đó, quan niệm trực quan về
thuật giải không thể là cơ sở bảo đảm cho một chứng minh toán học chặt chẽ được. Do đó,
muốn chứng minh những điều khẳng định "không có thuật giải giải một bài toán nào dó",
ta cần có một định nghĩa toán học cho khái niệm thuật giải.
Vào khoảng những năm 1930-1936, lần lượt các nhà toán học K.Gõdel, S. Kleene,
A.Church, A.Turing đã đề ra một số định nghĩa khác nhau cho khái niệm thuật giải.
Trong số các định nghĩa toán học khác nhau (nhưng tương đương) về thuật giải, các khái
niệm Máy Turing (1936) và Hàm đệ quy (1931-1936) được sử dụng rộng rãi hơn vì có
nhiều thuận tiện cho các nghiên cứu cả về lý thuyết lẫn thực hành. Ta có thể xem máy
Turing là một mô hình toán học của máy tính.
- 10 -
Chính nhờ có được định nghĩa toán học về thuật giải, người ta đã chứng minh được có
những bài toán không có thuật giải giải. Chẳng hạn, bài toán thứ 10 của Hilbert (Ví dụ 5
nêu trên) không có thuật giải đểgiải.
1.1.4. Một số ví dụ về thuật giải
Ví dụ 1. Một thuật giải tìm ước chung lớn nhất (viết tắt là UC) của hai số
nguyên dương M và N.(Bạn đọc có thể thấy sự khác nhau chút ít giữa thuật
toán này và thuật toán trình bày ở phần trên).
Bước 1. Nếu M=N thì UC=M và kết thúc, nếu không thì chuyển đến bước 2.
Bước 2. Nếu M<N thì thay N bằng N-M và chuyển đến bước 1, nếu không thì thay M
bằng M-N và chuyển đến bước 1.
Dãy thao tác trong thuật giải diễn ra trong nhiều nhất Max(M,N) bước.
Tuy nhiên, với cách diễn tả thuật giải trên, máy chưa có khả năng trực tiếp thực hiện. Ta
cần diễn tả thuật giải đó bằng một ngôn ngữ mà máy tính có thể “hiểu” và thực hiện
được. Cách diễn tả một thuật giải như vậy được gọi là một chương trình, ngôn ngữ dùng
để viết chương trình được gọi là ngôn ngữ lập trình.
Ví dụ 2. Thuật giải tìm giá trị lớn nhất và nhỏ nhất của một dãy số
Ta xét bài toán sau:
Input: Số nguyên dương n và dãy n số a
1
, . . ., a
n
.
Output: Giá trị lớn nhất Max và giá trị nhỏ nhất Min của dãy số
Sau đây là thuật giải đối với bài toán này:
Bước 1. Đặt Max= a
1
, Min= a
1
, i=2.
Bước 2. Nếu i<N thì thực hiện bước 3, nếu không thì kết thúc.
Bước 3
3.1. Nếu a
i
>Max thì Max= a
i
.
3.2. Nếu a
i
<Min thì Min= a
i
.
3.3. Tăng i một đơn vị và quay về bước 2.
- 11 -
Rõ ràng số thao tác cần tiến hành không quá n.
Ví dụ 3. Thuật giải để sắp xếp một dãy số cho trước thành một dãy đơn điệu không
tăng.
Ta xét bài toán sau:
Input: Số nguyên dương n và dãy n số a
1
, . . ., a
n
.
Output: Dãy số trên được sắp xếp lại thành một dãy số không tăng tức là số hạng
trước lớn hơn hay bằng số hạng sau.
Một thuật giải giải bài toán này có thể nói vắn tắt như sau: với mỗi chỉ số i, 1≤i≤n-1, xét
các chỉ số j đứng sau i, i+1≤j≤n, nếu a
i
<a
j
thì ta tráo đổi hai số hạng này cho nhau.
Một cách cụ thể hơn:
Bước 1. i=1;
Bước 2. Nếu i=n thì kết thúc, nếu không ( tức là i<n) thì
Bước 2.1. j=i+1;
Bước 2.2. Nếu (j≤n) và (a
i
<a
j
) thì tráo đổi giá trị của hai số này
Bước 2.3. Nếu j<n thì (tăng J một đơn vị và quay lại Bước 2.2), nếu không ( tức
là j=n), chuyển sang bước 3.
Bước 3. Tăng i một đơn vị và quay lại Bước 2.
Rõ ràng số bước thao tác cần tiến hành không quá n(n+1).
1.1.5 Khái niệm về cấu trúc dữ liệu và mối quan hệ với thuật giải
Ở trên ta đã nêu rõ khái niệm thuật giải. Đối tượng của thuật giải chính là dữ liệu hay
thuật giải bao giờ cũng phải tác động lên dữ liệu để có kết quả của bài toán. Một tập dữ
liệu có thể gồm những phần tử rời rạc, chẳng có quan hệ gì với nhau, nhưng cũng có thể
gồm những phần tử có quan hệ, ràng buộc nhất định. Khi những thành phần của tập dữ
liệu có mối quan hệ với nhau, chúng cần được tổ chức theo những phương thức nhất
định thành những cấu trúc dữ liệu để thuật giải tác động lên đó được hiệu quả.
- 12 -
Trong giáo trình Tin học cơ sở, ta đã làm quen với một số cấu trúc dữ liệu đơn giản như
bản ghi, mảng...Trong giáo trình này ta sẽ lần lượt nghiên cứu những cấu trúc dữ liệu
phức tạp hơn cả tuyến tính và phi tuyến tính.
Khi lập chương trình cho máy tính, công việc tổ chức dữ liệu và thiết kế những thuật
giải tương thích, hiệu quả là rất quan trọng. Chính vì thế cấu trúc dữ liệu và thuật giải là
hai thành tố quan trọng của chương trình máy tính và đều là những đối tượng nghiên
cứu quan trọng của khoa học máy tính. Hơn nữa, chúng có quan hệ mật thiết với nhau,
ảnh hưởng và điều phối lẫn nhau để tạo ra lời giải cho bài toán. Trong phạm vi của giáo
trình này, ta nghiên cứu những cấu trúc dữ liệu cơ bản và những thuật giải hay thao tác
thường thực hiện trên các cấu trúc dữ liệu đó.
1.2. Khái niệm về độ phức tạp của thuật giải
Mỗi thuật giải chỉ giải một bài toán nào đó nhưng có thể có nhiều thuật giải khác nhau
giải cùng một bài toán. Ví dụ, để sắp xếp một dãy số bất kỳ thành một dãy đơn điệu, có
rất nhiều thuật giải khác nhau.
Cùng một bài toán, có thể có nhiều thuật giải khác nhau. Việc lựa chọn một thuật giải
thích hợp nhất hay tốt nhất cho một bài toán luôn là mối quan tâm của người lập trình.
Vậy làm thế nào để chọn được thuật giải thích hợp nhất trong những thuật giải của một
bài toán? Nói cụ thể hơn, những tiêu chí nào là quan trọng để đánh giá một thuật giải?
Thuật giải càng trong sáng, có cấu trúc tốt thì mã hóa càng nhanh, bảo trì chương trình
càng dễ dàng và do đó giảm được giá thành của phần mềm. Tuy nhiên, có những thuật
giải rất trong sáng, dễ hiểu nhưng lại không khả thi về mặt thời gian thực hiện hay
không gian nhớ cần thiết khi thực hiện nó. Vậy làm thế nào để đánh giá được thời gian
thực hiện và không gian nhớ dành cho một thuật giải? Cách đơn giản nhất là ta cài đặt
thuật giải rồi cho máy chạy chưong trình với một tập dữ liệu vào nào đó và đo thời gian
thực hiện chương trình cũng như tính không gian nhớ cần thiết cho nó và ta có kết quả
về thời gian và không gian cần thiết cho thuật giải. Tuy nhiên, cách tính này chỉ mới áp
dụng trên một tập dữ liệu đầu vào cụ thể. Sẽ là không thích hợp nếu ta dùng kết quả này
để áp dụng chung cho các tập dữ liệu đầu vào khác. Nếu bài toán đơn giản và đầu vào
- 13 -
chỉ gồm một số ít phần tử thì ta cũng chẳng cần quan tâm nhiều đến việc lựa chọn thuật
giải làm gì. Ví dụ như bài toán sau: Ta cần chọn ra số lớn nhất trong 3 số nguyên hay
thậm chí trong vài chục số nguyên đã cho. Bạn có thể dùng bất cứ thuật giải nào bạn
nghĩ ra hay bạn thích, miễn là nó đúng, có lẽ thời gian thực hiện chúng cũng không khác
nhau bao nhiêu. Một thuật toán tìm kiếm tuần tự để tìm một số điện thoại trong một
danh sách điện thoại gồm 50 số thì có thể chấp nhận được nhưng nếu áp dụng thuật toán
đó cho một danh sách gồm hàng chục ngàn số điện thoại trong một thành phố, hẳn là
không thể chấp nhận được.
Như vậy, vấn đề đặt ra rất tự nhiên là khi giải một bài toán, ta muốn chọn một thuật giải
tốt. Chúng ta cần tìm ra một phương pháp nào đó để có thể cho phép ta đánh giá được
thuật giải này tốt hơn thuật giải kia với một tập dữ liệu vào bất kì. Ví dụ, khi một danh
bạ điện thoại gồm từ một trăm số điện thoại trở lên, cách tổ chức thành thư mục và việc
tìm kiếm theo thư mục chắc chắn sẽ giúp ta tìm kiếm tốt hơn là việc tìm kiếm tuần tự.
Nhưng thế nào là thuật giải tốt. Đây là nội dung nghiên cứu của lý thuyết độ phức tạp
tính toán. Lý thuyết này có thể được xây dựng trên một nền tảng toán học chặt chẽ từ
một hệ tiên đề. Tuy nhiên, trong phạm vi giáo trình này, chúng tôi chỉ giới thiệu một số
vấn đề đơn giản .
Khi dùng máy tính thực hiện một chương trình thể hiện một thuật giải nào đó, hệ điều
hành cần cung cấp cho chương trình đó các tài nguyên như giờ CPU, bộ nhớ, . . . Độ
phức tạp của một thuật giải được đo bằng số lượng các tài nguyên cần dùng để thực
hiện chương trình đó. Khi so sánh hai thuật giải cùng giải một bài toán, một thuật giải
này được xem là tốt hơn thuật giải kia nếu nó dùng ít tài nguyên hơn.
Trong các tài nguyên cần dùng để chạy chương trình, hiện nay người ta quan tâm nhiều
nhất đến thời gian vì đó là dạng tài nguyên không tái tạo được. Một thuật giải được xem
là tốt nếu chương trình tương ứng chạy nhanh hay chính xác hơn, chạy trong thời gian
chấp nhận được. Do đó, để đánh giá độ phức tạp của một thuật giải, ta thường ước tính
số thao tác cơ bản cần dùng để thực hiện thuật giải. Thao tác cơ bản của một thuật giải
là thao tác mà số lần thựchiện nó không ít hơn số lần thực hiện các thao tác khác.
- 14 -
Ví dụ về thao tác cơ bản:
Bài toán Thao tác cơ bản
Tìm phần tử x trong một danh sách So sánh x với một phần tử của danh sách
Nhân hai ma trận thực Phép nhân hai số thực, phép cộng hai số
thực
Sắp xếp một danh sách So sánh hai phần tử của danh sách
Với mỗi bài toán, ta xét kích thước đầu vào hay gọi đơn giản là kích thước của bài toán.
Kích thước đầu vào một bài toán được đo bằng số lượng dữ liệu vào cần xử lý. Ví dụ
nếu dữ liệu vào là một dãy số có N số hạng thì kích thước bài toán dược xem bằng N,
nếu dữ liệu vào là một đồ thị có N đỉnh, kích thước bài toán bằng N, nếu dữ liệu vào là
một xâu ký tự độ dài L thì kích thước bài toán bằng L, nếu dữ liệu vào là một bảng có M
dòng và N cột thì kích thước bài toán bằng MxN. . .
Khi đó, số các thao tác cơ bản mà một thuật giải sẽ được biểu thị như một hàm f(K) của
biến số K là kích thước của bài toán. Khi đánh giá hàm f(K) người ta thường dùng ký
hiệu O hay chính xác hơn là khái niệm Ω của giải tích có nghĩa là cùng bậc. Ví dụ, giải
thuật tìm Min-Max của một dãy số trong Mục 8.1 có độ phức tạp 2N, ta nói độ phức tạp
đó cỡ Ω(N) có nghĩa là cùng bậc với N khi N tiến đến vô cùng; tương tự, giải thuật sắp
xếp dãy số có độ phức tạp cở Ω(N
2
).
Việc dùng ký hiệu Ω để thể hiện độ phức tạp của giải thuật cho ta một đánh giá tổng thể
không bị sa vào những tính toán tỉ mỉ hơn nhưng không thật cần thiết. Ví dụ, cùng là hai
phép toán cộng và nhân hai số, rõ ràng phép cộng thực hiện nhanh hơn nhiều so với
phép nhân nhưng sự khác biệt đó chỉ thể hiện bằng việc thời gian tính toán sai khác một
hằng số nhân. Do đó, nếu dùng ký hiệu Ω, ta có thể bỏ qua sự khác biệt đó.
Khi nói về độ phức tạp về thời gian, có một số bậc được sử dụng thường xuyên và
người ta đã đặt tên cho chúng. Một cách vắn tắt, nếu một thuật toán có độ phức tạp là t
- 15 -
và t < cn, với n > n
0
và c là một hằng số dương, còn n là kích cỡ đầu vào thì người ta gọi
thuật toán đó có độ phức tạp tuyến tính. Nếu t < cn
2
thì t được gọi là có độ phức tạp
bình phương. Tương tự t được gọi là có độ phức tạp lập phương, đa thức hay mũ nếu
nó có bậc lần lượt của các hàm n
3
, n
k
hay a
n
, với k và a là những hằng số nào đó.
Để dễ hình dung về sự tăng của một số hàm với n là kích thước của bài toán, dưới đây ta
phác họa một số đồ thị của độ phức tạp loga, độ phức tạp tuyến tính, độ phức tạp bình
phương, độ phức tạp lập phương và độ phức tạp mũ.
Hình 2.1: Phác hoạ các đường biểu diễn độ phức tạp của một số thuật giải
-----------------------------------------
1.3. Phân tích thuật toán
1.3.1. Khái niệm về phân tích thuật toán
Phân tích một giải thuật có nghĩa là đánh giá độ phức tạp của giải thuật đó.
- 16 -
O(log(n)
O(n)
O(n
2
)
O(n
3
)
O(a
n
)
Thực ra không có một cẩm nang để phân tích thuật toán mà chủ yếu là ta sử dụng kiến
thức toán học, sự trực quan, suy luận, và một số kĩ thuật cơ bản để tính độ phức tạp của
một thuật giải.
Có một số cấu trúc phổ biến trong các thuật giải: cấu trúc tuần tự, cấu trúc lặp và cấu
trúc đệ quy.
Với cấu trúc tuần tự: Nếu ta có hai đoạn trình thực hiện tuần tự là P
1
và P
2
, độ phức tạp
của chúng lần lượt là t
1
= O(f(n)), t
2
= O(g(n)) thì độ phức tạp của cả hai đoạn P
1
và P
2
chính là O(max(f(n), g(n)).
Ví dụ: Khi ta thực hiện tuần tự hai đoạn trình P
1
và P
2
, P
1
có
độ phức tạp là O(n
2
) và P
2
có độ phức tạp là O(n
3
) thì hợp hai đoạn P
1
và P
2
sẽ có độ phức tạp là O(n
3
).
Tương tự, nếu P
1
có độ phức tạp là O(2
n
) còn P
2
có độ phức tạp là O(n
5
) thì độ phức tạp
của đoạn trình hợp là O(2
n
).
Để ý rằng, với n đủ lớn ta luôn có: n! > a
n
> n
k
> logn.
Cấu trúc lặp thường có một trong các dạng sau
For i:=a to b do <nhóm lệnh>; (1)
While <Điều kiện lô gic> Do <Nhóm lệnh>; (2)
Repeat <Nhóm lệnh> Until <Điều kiện lô gic>; (3)
Số thao tác cần thực hiện trong một cấu trúc lặp có thể đễ dàng ước tính được sau khi đã
tính được các thao tác cần thực hiện trong <Nhóm lệnh>. Đối với cấu trúc lặp loại (1),
nếu số thao tác trong <Nhóm lệnh> là S thì số thao tác cần thực hiện tổng cộng không
lớn hơn S(b-a).
Ví dụ: trong phép nhân hai ma trận vuông A và B cấp n ta có:
Procedure Nhanmatran(A,B)
Var C;
Begin
for i ← 1 to n do
for j ← 1 to n do
Begin
- 17 -
C[i,j] ← 0
for k ← 1 to n do
C[i,j] ← C[i,j] + a[i,k] * b[k,j]
End;
Return C;
End
Ta thấy ngay là vòng lặp trong cùng (k là biến chạy) có độ phức tạp O(n), vòng lặp
trong tiếp theo (j là biến chạy) cũng có độ phức tạp O(n) và vòng lặp ngoài cùng (với i
là biến chạy) với độ phức tạp O(n). Do đó toàn bộ chương trình của ta có độ phức tạp
O(n
3
).
Đối với cấu trúc lặp loại (2) và (3), căn cứ vào điều kiện lô gic, ta có thể ước lượng
được số lần lặp tối đa do đó có thể tính được số tối đa các thao tác cần thực hiện.
Đối với các cấu trúc đệ quy, việc ước lượng số thao tác khó khăn hơn vì nói ta phải tiến
hành tính số thao tác theo một công thức truy hồi.
Sau đây ta sẽ xét một số ví dụ nữa.
Ví dụ 1. Xét bài toán
Input. Mảng một chiều A[1..N] gồm các số nguyên;
Output. Giá trị nhỏ nhất Min và giá trị lớn nhất Max của các phần tử của mảng;
Xét thuật giải có tên FindMin-Max(1,N) sau:
Bước 1. Nếu N=1 thì nếu Min = Max = A[1] và kết thúc nếu không thì chuyển
sang Bước 2.
Bước 2. Nếu N=2 thì (A[1]≤A[2] thì Min = A[1] và Max = A[2] nếu không thì
Min = A[2] và Max = A[1] và kết thúc nếu không thì chuyển sang Bước 3.
Bước 3. Gọi FindMin1-Max1(1,N Div 2) và FindMin2-Max2(N div 2 +1,N); nếu
Min1≤Min2 thì Min = Min1 nếu không thì Min=Min2 và nếu Max1≤Max2 thì
Max = Max1 nếu không thì Max=Max2; kết thúc.
Nếu dộ phức tạp tính theo số lần so sánh C(N) các số thì ta có
- 18 -
C(2) = 1;
C(2K) = 2C(K) +2 với K>1;
C(2K+1) = C(K) + C(K+1) + 2;
Từ đó ta có thể tính được C(N) = 3N/2 - 2.
Ví dụ 2. Bài toán Tháp Hà Nội
Ta sẽ diễn tả bài toán này bằng ngôn ngữ thông thường. Có N đĩa tròn đường kính khác
nhau với lỗ thủng ở tâm và ba cọc 1, 2, 3. Ban đầu N đĩa đặt trên cọc 1, đĩa lớn hơn nằm
dưới đĩa nhỏ hơn. Một di chuyển đĩa thực hiện bằng cách chuyển đĩa trên cùng ở cọc A
(nếu có) đặt lên trên cùng một cọc B khác với điều kiện đĩa đó nhỏ hơn đĩa (nếu có)
đang nằm trên cùng đĩa ở cọc B. Cần tiến hành một dãy các di chuyển đĩa sao cho cuối
cùng, N đĩa được chuyển sang cọc 2.
Bài toán này gắn với một giai thoại về sự thử thách của Thượng đế đối với lòng kiên trì
của con người. Nếu số đĩa bằng 64, mỗi di chuyển đĩa thực hiện trong 1 giây và một
người thực hiện một dãy các di chuyển đúng đắn thì người đó phải mất 500 tỷ năm mới
di chuyển xong.
Việc cần chuyển N đĩa từ cọc 1 sang cọc 2 một cách hợp lệ và tốt nhất có thể xem là
trường hợp riêng của việc chuyển M đĩa nhỏ nhất từ cọc i sang cọc j ,1≤i≤3, 1≤j≤3, i≠j,
một cách hợp lệ và tốt nhất có thể. Nếu ta thể hiện việc đó bằng một thủ tục Procedure
Hanoi(m,i,j); Việc ta cần làm chính là Hanoi(N,1,2);
Để làm việc đó, do quy định của một di chuyển đĩa, ta cần chuyển m-1 đĩa nhỏ nhất từ
cọc i sang cọc thứ ba, đó là cọc k = 6-i-j, chuyển đĩa thứ M từ cọc i sang cọc j và sau đó
chuyển M-1 đĩa từ cọc k sang cọc j. Nếu viết bằng Pascal, ta có
Procedure Hanoi(M,i,j: Byte);
Begin
If m=0 then Exit;
Hanoi(M-1,i,6-i-j);
Writeln(i,’ – ‘,j);
Hanoi(M-1,6-i-j,j);
End;
- 19 -
Nếu ký hiệu T(M) là số di chuyển cần thực hiện (xem như một thao tác cơ bản), ta có
T(M) = 2.T(M-1) - 1 (*)
Như vậy nếu xem N là kích thước của bài toán thì độ phức tạp tính toán của thuật giải
trên là T(N). Từ hệ thức (*), ta có thể suy ra T(N) = 2
N
- 1.
Hệ thức (*) là một hệ thức truy hồi.
1.3.2. Một số quan điểm phân tích thuật toán
Cách phân tích thuật toán trong mục 8.2.2 còn được gọi là phân tích theo tình huống
xấu nhất (Worst-Case Analysis). Với mỗi thuật giải đối với bài toán kích thước K, ta
luôn đánh giá số tối đa các thao tác cơ bản cần tiến hành là bao nhiêu.
Có nhiều cách khác để phân tích thuật giải như phân tích trung bình (Average Analysis),
phân tích tiệm cận (Asymptotic Analysis).
Ta sẽ giới thiệu cách phân tích trung bình. Câu chuyện bắt đầu từ việc xem xét thuật giải
đơn hình đối với bài toán Quy hoạch tuyến tính. Bài toán này có thể phát biểu trong
ngôn ngữ đại số tuyến tính như sau:
I. Ma trận A gồm M dòng, N cột; Véc tơ cột B M chiều; véc tơ dòng C N
chiều;
O. Cần tìm véc tơ cột N chiều X sao cho AX
≤
B và CX đạt giá trị nhỏ nhất.
Theo cách đánh giá xấu nhất, độ phức tạp của thuật toán đơn hình cỡ hàm mũ. Tuy
nhiên, trải qua mấy chục năm dùng thuật giải này để giải rất nhiều bài toán phát sinh
trong nhiều lĩnh vực khác nhau, các chương trình vẫn chạy trong thời gian chấp nhận
được. Công trình nghiên cứu của nhà toán học S.. Smale cho kết quả độ phức tạp trung
bình của thuật toán đơn hình là đa thức (thậm chí bậc thấp) đối với kích thước bài toán.
Sau kết quả này, người ta cũng chứng tỏ được có nhiều thuât giải không tốt theo cách
đánh giá xấu nhất nhưng tính trung bình vẫn tốt.
Về đại thể, để đánh giá độ phức tạp trung bình một thuật giải, ta giả sử mọi dữ liệu đều
có xác xuất xuất hiện như nhau, trên cơ sở đó, độ phức tạp trung bình được tính như
trung bình xác suất của các số bước tính toán đối với các tình huống của dữ liệu. Chi
tiết về vấn đề này cũng sẽ được làm rõ trong môn lý thuyết độ phức tạp tính toán.
- 20 -
BÀI TẬP
Hãy phát biểu theo quan niệm của Tin học về bài toán và trình bày thuật giải cho mỗi
một trong các bài toán sau. Đánh giá độ phức tạp của từng thuật giải.
Bài 1. Giải và biện luận phương trình bậc nhất tổng quát: aX + b = 0.
Bài 2. Giải và biện luận phương trình bậc hai tổng quát: aX
2
+ bX + c = 0.
Bài 3. Giải và biện luận hệ phương trình bậc nhất hai ẩn số tổng quát.
Bài 4. Giải và biện luận phương trình trùng phương tổng quát: aX
4
+ bX
2
+ c = 0.
Bài 5. Giải và biện luận hệ hai phương trình bậc hai a
1
X
2
+ b
1
X + c
1
= 0 và a
2
X
2
+ b
2
X +
c
2
= 0.
Bài 6. Cho một hình chữ nhật ABCD với hai cạnh nguyên dương AB = M và BC = N.
Hình chữ nhật được chia thành các ô vuông đơn vị. Hãy tìm số ô vuông có điểm chung
với đường chéo AC.
Bài 7. Cho một hình chữ nhật ABCD với hai cạnh nguyên dương AB=M và BC= N.
Hình chữ nhật được chia thành các ô vuông đơn vị. Hãy tìm số ô vuông có điểm trong
chung với đường chéo AC.
Bài 8. Xét dãy vô hạn các chữ số A nhận được bằng cách viết liên tiếp các số nguyên
dương liên tiếp tăng dần bắt đầu từ 1, 2, 3, 4, . . . .
1234567891011121314151617181920212223.....
Cho số nguyên dương N. Hãy tìm chữ số thứ N của dãy A.
Ví dụ với N =13, chữ số thứ N của dãy trên là 1.
Bài 9. Cho số nguyên dương N. Lập dãy B các chữ số nhận được bằng cách viết các số
nguyên dương liên tiếp tăng dần bắt đầu từ 1, 2, 3, 4, . . . cho tới N. Hãy tìm chữ số thứ
N tính từ chữ số cuối cùng của dãy B.
Bài 10. Xét tập A các số nguyên dương được xác định như sau:
1. 1∈A.
- 21 -
2. Nếu K∈A thì 2K+1 và 3K+1 ∈A.
3. A gồm và chỉ gồm các số thoả mãn một trong hai điều kiên 1 và 2.
Cho một số nguyên dương N không lớn hơn 60000. Hãy tìm số các số thuộc A không
lớn hơn N.
Bài 11. Hai người A và B chơi trò chơi sau: A bí mật chọn một số nguyên dương N và
nói rõ N không lớn một số M nguyên dương và B đoán xem A chọn số nào. B được
quyền hỏi A một số câu hỏi và A sẽ chỉ trả lời đúng hay sai và mọi câu trả lời của A là
chính xác. Hãy chọn cho B một số ít nhất câu hỏi để sau khi hỏi xong, B đoán đúng
được số N.
Bài 12. Để tổng kết điểm cuối học kỳ và tiến hành phân loại kết quả học tập của các học
sinh trong một lớp học, giáo viên chủ nhiệm có sổ điểm của lớp. Cần tính điểm trung
bình từng môn học và tính điểm trung bình toàn học kỳ cho từng học sinh.
Với mỗi môn học, các điểm kiểm tra miệng, kiểm tra 15 phút tính hệ số 1, các điểm
kiểm tra 1 tiết tính hệ số 2, điểm kiểm tra học kỳ tính hệ số 3. Khi tính điểm trung bình
toàn học kỳ, các điểm trung bình môn của Văn, Toán tính hệ số 3, của Lý, Hoá, Ngoại
ngữ - hệ số 2, các môn còn lại - hệ số 1.
Có năm mức phân loại học tập: kém, trung bình, khá, giỏi và xuất sắc tương ứng với
điểm trung bình toàn học kỳ thuộc các miền: <5, [5,7), [7,8), [8,9) và [9,10]. Hãy viết ra
cho giáo viên chủ nhiệm các bước tiến hành công việc này.
Bài 13. Trên mặt phẳng toạ độ cho toạ độ của bốn điểm A, B, C, D trong đó không có
ba điểm nào thẳng hàng. Hãy cho biết hai đoạn AB và CD có điểm chung hay không.
Bài 14. Trên mặt phẳng toạ độ cho toạ độ của bốn điểm A, B, C, D trong đó không có
ba điểm nào thẳng hàng. Hãy cho biết hai đoạn AB và CD có điểm trong chung hay
không.
Bài 15. Cho hình chữ nhật ABCD và một điểm M trên mặt phẳng toạ độ. Biết toạ độ
của A, B, C, D, M. Hãy cho biết khả năng nào trong hai khả năng sau xảy ra:
1. M thuộc hình chữ nhật ABCD.
2. M không thuộc hình chữ nhật ABCD.
- 22 -
Bài 16. Cho hình chữ nhật ABCD và hai điểm khác nhau M, N trên mặt phẳng toạ độ.
Biết toạ độ của A, B, C, D, M, N. Hãy cho biết khả năng nào trong ba khả năng sau xảy
ra:
1. Đoạn MN nằm trong (có thể có điểm trên biên) hình chữ nhật ABCD.
2. Đoạn MN nằm ngoài hình chữ nhật ABCD.
3. Đoạn MN có điểm nằm ngoài hình chữ nhật ABCD và có điểm nằm trong hình chữ
nhật ABCD.
Bài 17. Trên mặt phẳng toạ độ cho đa giác P
1
. . .P
N
. Đa giác được gọi là tự cắt nếu có
hai cạnh P
I
P
I+1
và P
J
P
J+1
với I<J-1 (quy ước đỉnh P
N+1
là đỉnh P
1
) có điểm chung. Đa giác
được gọi là lồi nếu với bất kỳ cạnh nào của đa giác, toàn bộ đa giác nằm về một phía
của đường thẳng đi qua cạnh đó. Biết toạ độ các đỉnh của đa giác. Hãy cho biết đa giác
có tự cắt không? Nếu đa giác không tự cắt, hãy cho biết đa giác có lồi không?. Nếu đa
giác không lồi, hãy chọn một số ít nhất các đỉnh sao cho mọi đỉnh P
1
, . .,P
N
đều thuộc đa
giác lồi có các đỉnh là các đỉnh được chọn (đa giác này được gọi là bao lồi của tập điểm
P
1
, . .,P
N
).
- 23 -
CHƯƠNG 2: TỔNG QUAN VỀ KIỂU DỮ LIỆU TRỪU TƯỢNG
Chương này giới thiệu về kiểu dữ liệu trừu tượng, đó là một khái niệm rất quan trọng
trong lập trình. Khái niệm này cho phép ta tạo ra những đơn vị chương trình dễ bảo trì,
an toàn và có thể được dùng bởi nhiều chương trình khác nhau. Để hiểu tốt từ chương 3
đến chương 6, bạn đọc cần dành thời gian đọc kĩ chương này.
2.1. Khái niệm về kiểu dữ liệu trừu tượng
Khi lập trình và giải quyết các bài toán trên máy tính, một trong những ý tưởng có ý
nghĩa nhất, có sức mạnh nhất là khái niệm trừu tượng hoá. Đó chính là cách nhìn bài
toán một cách khái quát, tạm thời bỏ qua tất cả những chi tiết cụ thể. Cũng có thể mô tả
sự trừu tượng hoá sự vật là việc nhìn những nét bao quát bên ngoài mà chưa để ý đến
những chi tiết bên trong. Trong công việc nói chung, nếu không có khái niệm trừu tượng
hoá thì khó có thể hiểu và quản lý được những hệ thống lớn và phức tạp. Chẳng hạn, ta
hãy hình dung tổng giám đốc của một nhà máy không quản lý nhà máy đó thông qua các
phân xưởng mà lại trực tiếp quản lý từng công nhân, từng dây chuyền sản xuất, từng
đầu máy thì sự việc sẽ khó biết chừng nào.
Khi học phần đại cương về lập trình, học sinh đã được làm quen với những ví dụ điển
hình về trừu tượng hoá: Trừu tượng hoá thao tác, đó chính là việc xây dựng và sử dụng
các chương trình con. Mỗi chương trình con thực hiện một tác vụ xác định nào đó và
trong chương trình chính, có thể xem nó như một thao tác mặc dù trong thực tế nó có
thể được cài đặt bằng một dãy gồm rất nhiều thao tác. Chẳng hạn, dùng cơ chế thủ tục
và hàm trong ngôn ngữ Pascal, học sinh đã được học cách xây dựng những đơn vị
chương trình và đưa nó vào thư viện để sử dụng. Ví dụ, ta có chương trình con sắp xếp
nhanh Quicksort như sau:
Procedure Quicksort (A,n), sắp sếp một mảng A gồm n số thực theo chiều tăng dần. Ta
có thể cài đặt và đưa nó vào thư viện và sau đó nó được xem như là một bộ phận của
ngôn ngữ. Ở giai đoạn thiết kế thuật toán, ta chỉ quan tâm đến việc mà thủ tục sẽ thực
- 24 -
hiện mà không cần quan tâm nó thực hiện việc đó như thế nào. Chẳng hạn, nếu ta viết
đoạn chương trình sau đây
For i:=1 to 300 do
Read (DiemThi[i]);
Quicksort (DiemThi, 300);
Người đọc sẽ thấy hoàn toàn dễ hiểu cho dù họ chưa cần biết chi tiết bên trong thủ tục
Quicksort, tức là chưa cần biết nó sắp xếp các phần tử của mảng như thế nào. Việc giấu
được những chi tiết cài đặt bên trong chương trình con làm cho cơ chế sử dụng chương
trình con trở thành một trong những phần quan trọng nhất của bất kì một ngôn ngữ lập
trình bậc cao nào. Cách trừu tượng hoá ta vừa nêu chính là trừu tượng hoá thao tác.
Chọn những thao tác thích hợp thực hiện trên các cấu trúc dữ liệu hợp lý là một công
việc hết sức quan trọng của lập trình. Cũng như việc cần thiết phải trừu tượng hoá thao
tác, bây giờ ta mở rộng ý tưởng trừu tượng hoá cho cấu trúc dữ liệu. Chúng ta sẽ xây
dựng khái niệm kiểu dữ liệu trừu tượng. Để giải thích khái niệm này, trước hết ta xét
sơ đồ sau về các mức cấu trúc dữ liệu:
Mức C.
Mức B.
- 25 -
Kiểu dữ liệu được xây dựng bởi người lập
trình đề giải những bài toán nào đó.
Kiểu dữ liệu được cung cấp bởi các ngôn
ngữ lập trình bậc cao.