Professional Documents
Culture Documents
HÀ NỘI 6/2021
(Lưu hành nội bộ)
Cấu trúc dữ liệu và giải thuật
2
Cấu trúc dữ liệu và giải thuật
MỤC LỤC
Chương 1. CÁC KHÁI NIỆM CƠ BẢN ...................................................... 12
1.1 Các khái niệm cơ bản ............................................................................ 12
1.2 Các bước phân tích thuật toán ............................................................... 17
1.3 Biểu diễn giải thuật ................................................................................ 18
1.3.1 Ngôn ngữ tự nhiên – liệt kê theo bước ........................................... 18
1.3.2 Sơ đồ khối ....................................................................................... 19
1.3.3 Mã giả ............................................................................................. 22
1.4 Phân tích – Đánh giá giải thuật.............................................................. 23
1.4.1 Độ phức tạp không gian ................................................................. 24
1.4.2 Độ phức tạp thời gian ..................................................................... 24
1.4.3 Ước lượng thời gian thực hiện chương trình .................................. 25
1.4.4 Nguyên tắc tính toán thời gian chạy thực hiện chương trình ......... 26
1.5 Độ phức tạp tính toán với tình trạng dữ liệu vào ................................... 28
1.6 Phân lớp bài toán ................................................................................... 28
1.7 Đệ quy ................................................................................................... 30
1.7.1 Giới thiệu ........................................................................................ 30
1.7.2 Giải thuật đệ quy ............................................................................ 30
1.7.3 Ví dụ minh họa ............................................................................... 32
1.8 Tổng kết chương và câu hỏi ôn tập ....................................................... 35
1.8.1 Tổng kết chương ............................................................................. 35
1.8.2 Câu hỏi ôn tập ................................................................................. 36
1.9 Bài tập áp dụng ...................................................................................... 36
Chương 2. DANH SÁCH ............................................................................. 40
2.1 Danh sách .............................................................................................. 40
2.1.1 Khái niệm ....................................................................................... 40
2.1.2 Các phép toán trên danh sách ......................................................... 41
2.2 Cài đặt danh sách bằng mảng ................................................................ 42
2.2.1 Cài đặt .......................................................................................... 42
2.2.2 Bài toán tìm kiếm trên danh sách................................................. 45
2.2.3 Bài toán sắp xếp trên danh sách ................................................... 48
2.3 Cài đặt danh sách bằng danh sách liên kết ............................................ 66
3
Cấu trúc dữ liệu và giải thuật
4
Cấu trúc dữ liệu và giải thuật
5
Cấu trúc dữ liệu và giải thuật
6
Cấu trúc dữ liệu và giải thuật
7
Cấu trúc dữ liệu và giải thuật
8
Cấu trúc dữ liệu và giải thuật
- Nắm vững khái niệm kiểu dữ liệu, kiểu dữ liệu trừu tượng, mô
hình dữ liệu, giải thuật và cấu trúc dữ liệu.
- Nắm vững và cài đặt được các mô hình dữ liệu, kiểu dữ liệu trừu
tượng cơ bản như danh sách, ngăn xếp, hàng đợi, cây, tập hợp, bảng
băm, đồ thị bằng một ngôn ngữ lập trình căn bản.
- Vận dụng được các kiểu dữ liệu trừu tượng, các mô hình dữ liệu để
giải quyết bài toán đơn giản trong thực tế.
- Sinh viên chuyên ngành công nghệ thông tin (môn bắt buộc)
9
Cấu trúc dữ liệu và giải thuật
những bài toán cần duyệt danh sách. Chương này cũng trình bày các cài đặt
chi tiết để các bạn sinh viên có thể tiếp cận thực hành.
Chương 3:
Chương này trình bày hai dạng danh sách đặc biệt là Ngăn xếp và
Hàng đợi. Chúng tôi cũng trình bày một số bài toán ứng dụng ngăn xếp và
hàng đợi trong thực tế.
Chương 4:
Chương này chúng tôi tập trung trình bày về kiểu dữ liệu trừu tượng
tập hợp đặc biệt là bảng băm, hàm băm. Trình bày về các loại hàm băm và
bảng băm cơ bản.
Chương 5:
Chương này giới thiệu về kiểu dữ liệu trừu tượng cây, khái niệm cây
tổng quát, các phép duyệt cây và cài đặt cây. Kế đến chúng tôi trình bày về
cây nhị phân, các cách cài đặt cây nhị phân và ứng dụng cây nhị phân. Cuối
cùng, chúng tôi trình bày cây tìm kiếm nhị phân như là một ứng dụng của
cây nhị phân để lưu trữ và tìm kiếm dữ liệu.
Chương 6:
Chương này trình bày mô hình dữ liệu đồ thị, các cách biểu diễn đồ
thị. Ở đây chúng tôi cũng trình bày các phép duyệt đồ thị bao gồm duyệt
theo chiều rộng và duyệt theo chiều sâu và đề cập một số bài toán thường
gặp trên đồ thị như là bài toán tìm đường đi ngắn nhất, bài toán tìm cây
khung tối thiểu. Do hạn chế về thời lượng lên lớp nên chương này chúng
tôi chỉ giới thiệu để sinh viên tham khảo thêm về cách cài đặt đồ thị và các
bài toán trên đồ thị.
10
Cấu trúc dữ liệu và giải thuật
[2] Đỗ Xuân Lôi, Cấu trúc dữ liệu và Giải thuật, Nhà xuất bản Khoa học và Kỹ
thuật, 1998.
[3] Nguyễn Xuân Huy, Thuật toán, Nhà xuất bản thống kê, 1988.
[4] Nicklaus Wirth, Algorithms + Data Structure = Program, Bản dịch tiếng Việt,
Nhà xuất bản Khoa học và Kỹ thuật, 1993.
11
Cấu trúc dữ liệu và giải thuật
Trong chương này tập trung trình bày các khái niệm về giải thuật và
phương pháp biểu diễn giải thuật hiện nay. Chương này cũng nêu phương
pháp phân tích và đánh giá một thuật toán, các khái niệm liên quan đến
việc tính toán thời gian thực hiện của một chương trình.
1.1 Các khái niệm cơ bản
Xây dựng một đề án tin học thực chất là chuyển bài toán thực tế thành
một bài toán có thể giải quyết trên máy tính. Mà một bài toán thực tế bất kỳ
đều bao gồm các đối tượng dữ liệu và các yêu cầu xử lý trên các đối tượng
đó. Như vậy, để xây dựng một mô hình tin học phản ánh được bài toán thực
tế cần chú trọng đến hai vấn đề:
*Tổ chức biểu diễn các đối tượng thực tế:
Các đối tượng dữ liệu thực tế rất đa dạng, phong phú và thường chứa
đựng những quan hệ nào đó với nhau, do đó trong mô hình tin học của bài
toán, cần phải tổ chức, xây dựng các cấu trúc thích hợp sao cho vừa có thể
phản ánh chính xác các dữ liệu thực tế đó, vừa có thể dễ dàng dùng máy
tính để xử lý. Công việc này được gọi là xây dựng cấu trúc dữ liệu cho bài
toán.
*Xây dựng các thao tác xử lý dữ liệu:
Từ những yêu cầu xử lý thực tế, cần tìm ra các giải thuật tương ứng để
xác định trình tự các thao tác máy tính phải tác động lên dữ liệu để cho ra
kết quả mong muốn, đây là bước xây dựng giải thuật cho bài toán.
Trên thực tế khi giải quyết một bài toán trên máy tính chúng ta thường
có khuynh hướng chỉ chú trọng đến việc xây dựng giải thuật mà quên đi
tầm quan trọng của việc tổ chức dữ liệu trong bài toán. Cần nhớ rằng: Giải
thuật phản ánh các phép xử lý, còn đối tượng xử lý của giải thuật lại là dữ
liệu, chính dữ liệu chứa đựng các thông tin cần thiết để thực hiện giải thuật.
Vì vậy để xác định được giải thuật phù hợp cần phải biết nó tác động đến
loại dữ liệu nào (ví dụ để làm nhuyễn các hạt đậu, người ta dùng cách xay
chứ không băm bằng dao, vì đậu sẽ văng ra ngoài và sẽ mất thời gian hơn
12
Cấu trúc dữ liệu và giải thuật
nhiều) và khi chọn lựa cấu trúc dữ liệu cũng cần phải hiểu rõ những thao
tác nào sẽ tác động đến nó (ví dụ để biểu diễn điểm số của sinh viên người
ta dùng số thực thay vì chuỗi ký tự vì còn phải thực hiện thao tác tính trung
bình từ những điểm số đó). Như vậy trong một đề án tin học, giải thuật và
cấu trúc dữ liệu có mối quan hệ với nhau.
Với một cấu trúc dữ liệu đã chọn, sẽ có những giải thuật tương ứng, phù
hợp. Khi cấu trúc dữ liệu thay đổi thường giải thuật cũng phải thay đổi theo
để tránh việc xử lý gượng ép, thiếu tự nhiên trên một cấu trúc không phù
hợp. Hơn nữa, một cấu trúc dữ liệu tốt sẽ giúp giải thuật xử lý trên đó có
thể phát huy tác dụng tốt hơn, vừa đáp ứng nhanh vừa tiết kiệm tài nguyên,
đồng thời giải thuật cũng dễ hiểu và đơn giản hơn.
Ví dụ 1.1 Một chương trình quản lý điểm thi của sinh viên cần lưu trữ
các điểm số của 4 sinh viên. Do mỗi sinh viên có 3 điểm số ứng với 3 môn
học khác nhau nên dữ liệu có dạng bảng như sau:
SV1 8 6 4
SV2 9 5 3
SV3 6 7 2
SV4 5 6 5
Chỉ xét thao tác xử lý là xuất điểm số các môn học của từng sinh viên.
Giả sử có các phương án tổ chức lưu trữ sau:
Phương án 1: Sử dụng mảng một chiều
Có tất cả 4(Sv) x 4(môn) = 12 điểm số cần lưu trữ, do đó khai báo mảng
diem như sau:
13
Cấu trúc dữ liệu và giải thuật
5 6 5 };
Khi đó trong mảng diem các phần tử sẽ được lưu trữ như sau:
Và truy xuất điểm số môn j của sinh viên i – (là phần tử tại dòng i, cột j
S chỉ số tương ứng trong
trong bảng) - phải sử dụng một công thức xác định
mảng diem: bảngđiểm(dòng i, cột j) tương đương với diem[((i-1)*số cột)
+j]
Ngược lại, với một phần tử bất kỳ trong mảng, muốn biết đó là điểm
số của sinh viên nào, môn gì, phải dùng công thức xác định như sau:
diem[i] tương đương với diem[((i-1)*số cột) + j]
Ở phương án này, thao tác xử lý được cài đặt như sau:
14
Cấu trúc dữ liệu và giải thuật
Như vậy truy xuất điểm số môn j của sinh viên i là phần tử tại dòng i cột
j trong bảng – cũng chính là phần tử nằm ở vị trí dòng i cột j trong mảng:
bảngđiểm(dòng i, cột j) tương đương với diem[i][j]
Ở phương án này, thao tác xử lý được cài đặt như sau:
Nhận xét:
Ta có thể thấy rằng phương án 2 cung cấp một cấu trúc dữ liệu phù hợp
với dữ liệu thực tế hơn phương án 1, do đó giải thuật xử lý trên cấu trúc dữ
liệu của phương án 2 cũng đơn giản và tự nhiên hơn.
Qua phần trên ta đã thấy được vai trò và tầm quan trọng của việc lựa
chọn một phương án tổ chức dữ liệu thích hợp trong một chương trình hay
một đề án tin học. Một cấu trúc dữ liệu tốt phải thoả mãn các tiêu chuẩn
sau:
*Phản ảnh đúng thực tế: đây là tiêu chuẩn quan trọng nhất, quyết định
tính đúng đắn của toàn bộ bài toán. Cần xem xét kỹ lưỡng cũng như dự trù
các trạng thái biến đổi của dữ liệu trong chu trình sống để có thể chọn cấu
trúc dữ liệu lưu trữ thể hiện chính xác đối tượng thực tế.
Ví dụ 1.2 Một số trường hợp chọn cấu trúc dữ liệu sai
15
Cấu trúc dữ liệu và giải thuật
- Chọn một số nguyên int để lưu trữ điểm trung bình của sinh viên (được
tính theo công thức trung bình cộng của các môn học có hệ số), như vậy sẽ
làm tròn mọi điểm số của sinh viên gây ra việc đánh giá sinh viên không
chính xác qua điểm số. Trong trường hợp này phải sử dụng biến số thực để
phản ảnh đúng kết quả của công thức tính thực tế cũng như phản ảnh chính
xác kết quả học tập của sinh viên.
- Trong trường phổ thông, một lớp có 50 học sinh, mỗi tháng đóng quỹ
lớp 1.000 đồng. Nếu chọn một biến số kiểu unsigned int (khả năng lưu trữ
0 – 65535) để lưu trữ tổng tiền quỹ của lớp học trong tháng, nếu xảy ra
trường hợp trong hai tháng liên tiếp không có chi hoặc tăng tiền đóng quỹ
của mỗi học sinh lên 2.000 đồng thì tổng quỹ lớp thu được là 100.000
đồng, vượt khỏi khả năng lưu trữ của biến đã chọn, gây nên tình trạng tràn,
sai lệnh. Như vậy khi chọn biến dữ liệu ta phải tính đến các trường hợp
phát triển của đại lượng chứa trong biến để chọn liệu dữ liệu thích hợp.
Trong trường hợp trên ta có thể chọn kiểu long (có kích thước 4 bytes, khả
năng lưu trữ là -2147483648 ! 2147483647) để lưu trữ tổng tiền quỹ lớp.
*Phù hợp với các thao tác xử lý: tiêu chuẩn này giúp tăng tính hiệu quả
của đề án: phát triển các thuật toán đơn giản, tự nhiên hơn; chương trình
đạt hiệu quả cao hơn về tốc độ xử lý.
Ví dụ 1.3 Một số trường hợp chọn cấu trúc dữ liệu không phù hợp
Khi cần xây dựng một chương trình soạn thảo văn bản, các thao tác xử lý
thường xảy ra là chèn, xoá, sửa các ký tự trên văn bản. Trong thời gian xử
lý văn bản, nếu chọn cấu trúc lưu trữ văn bản trực tiếp lên tập tin thì sẽ gây
khó khăn khi xây dựng các giải thuật cập nhật văn bản và làm chậm tốc độ
xử lý của chương trình vì phải làm việc trên bộ nhớ ngoài. Trường hợp này
nên tìm một cấu trúc dữ liệu có thể tổ chức có thể tổ chức ở bộ nhớ trong
để lưu trữ văn bản suốt thời gian soạn thảo.
*Tiết kiệm tài nguyên hệ thống: cấu trúc dữ liệu chỉ nên sử dụng tài
nguyên hệ thống vừa đủ để đảm nhiệm được chức năng của nó. Thông
thường có hai loại tài nguyên cần lưu tâm nhất là CPU và bộ nhớ. Tiêu
chuẩn này nên cân nhắc tuỳ vào tình huống cụ thể khi thực hiện đề án. Nếu
tổ chức sử dụng đề án cần có những xử lý nhanh thì chọn cấu trúc dữ liệu
16
Cấu trúc dữ liệu và giải thuật
có yếu tố tiết kiệm thời gian xử lý ưu tiên hơn tiêu chuẩn sử dụng tối ưu bộ
nhớ, và ngược lại.
Ví dụ 1.4 Một số trường hợp chọn cấu trúc dữ liệu gây lãng phí
- Sử dụng biến int (2 bytes) để lưu trữ một giá trị thông tin về ngày trong
tháng. Vì một tháng chỉ có thể nhận các giá trị từ 1-31 nên chỉ cần sử dụng
biến char (1 byte) là đủ.
- Để lưu trữ danh sách nhân viên trong công ty mà sử dụng mảng 1000
phần tử. Nếu số lượng nhân viên thật sự ít hơn 1000 (bị giảm hoặc biên chế
không đủ) thì gây lãng phí. Trường hợp này cần có một cấu trúc dữ liệu
linh động hơn mảng – ví dụ danh sách liên kết.
1.2 Các bước phân tích thuật toán
Bước đầu tiên trong việc phân tích một thuật toán là xác định đặc trưng
dữ liệu sẽ được dùng làm dữ liệu nhập của thuật toán và quyết định phân
tích nào là thích hợp. Về mặt lý tưởng, chúng ta muốn rằng với một phân
bố tùy ý được cho của dữ liệu nhập, sẽ có sự phân bố tương ứng về thời
gian hoạt động của thuật toán. Chúng ta không thể đạt tới điều lý tưởng này
cho bất kỳ một thuật toán không tầm thường nào, vì vậy chúng ta chỉ quan
tâm đến bao đóng của thống kê về tính năng của thuật toán bằng cách cố
gắng chứng minh thời gian chạy luôn luôn nhỏ hơn một “chặn trên” bất
chấp dữ liệu nhập như thế nào và cố gắng tính được thời gian chạy trung
bình cho dữ liệu nhập “ngẫu nhiên”.
Bước thứ hai trong phân tích một thuật toán là nhận ra các thao tác trừu
tượng của thuật toán để tách biệt sự phân tích với sự cài đặt. Ví dụ, chúng
ta tách biệt sự nghiên cứu có bao nhiêu phép so sánh trong một thuật toán
sắp xếp khỏi sự xác định cần bao nhiêu micro giây trên một máy tính cụ
thể; yếu tố thứ nhất được xác định bởi tính chất của thuật toán, yếu tố thứ
hai lại được xác định bởi tính chất của máy tính. Sự tách biệt này cho phép
chúng ta so sánh các thuật toán một cách độc lập với sự cài đặt cụ thể hay
độc lập với một máy tính cụ thể.
Bước thứ ba trong quá trình phân tích thuật toán là sự phân tích về mặt
toán học, với mục đích tìm ra các giá trị trung bình và trường hợp xấu nhất
cho mỗi đại lượng cơ bản. Chúng ta sẽ không gặp khó khăn khi tìm một
17
Cấu trúc dữ liệu và giải thuật
chận trên cho thời gian chạy chương trình, vấn đề là phải tìm ra một chận
trên tốt nhất, tức là thời gian chạy chương trình khi gặp dữ liệu nhập của
trường hợp xấu nhất. Trường hợp trung bình thông thường đòi hỏi một
phân tích toán học tinh vi hơn trường hợp xấu nhất. Mỗi khi đã hoàn thành
một quá trình phân tích thuật toán dựa vào các đại lượng cơ bản, nếu thời
gian kết hợp với mỗi đại lượng được xác định rõ thì ta sẽ có các biểu thức
để tính thời gian chạy.
Nói chung, về mặt lý thuyết, có thể phân tích chính xác tính năng của
một thuật toán dựa vào khả năng của máy tính cụ thể và dựa vào bản chất
toán học của một số đại lượng trừu tượng. Tuy nhiên, thay vì phân tích quá
cụ thể, người ta thường đưa ra các ước lượng để tránh sa vào chi tiết.
1.3 Biểu diễn giải thuật
Khi chứng minh hoặc giải một bài toán trong toán học, ta thường dùng
những ngôn từ toán học: "ta có", "điều phải chứng minh", "giả thuyết", ...
và sử dụng những phép suy luận toán học như phép suy ra, tương đương, ...
Thuật toán là một phương pháp thể hiện lời giải bài toán nên cũng phải
tuân theo một số quy tắc nhất định. Để có thể truyền đạt thuật toán cho
người khác hay chuyển thuật toán thành chương trình máy tính, ta phải có
phương pháp biểu diễn thuật toán. Có 3 phương pháp biểu diễn thuật toán
sau:
1.3.1 Ngôn ngữ tự nhiên – liệt kê theo bước
Trong cách biểu diễn thuật toán theo ngôn ngữ tự nhiên, người ta sử
dụng ngôn ngữ thường ngày để liệt kê các bước của thuật toán. Phương
pháp biểu diễn bằng ngôn ngữ tự nhiên không yêu cầu người viết thuật toán
cũng như người đọc thuật toán phải tuân thủ các quy tắc cụ thể nhất định
nào đó. Tuy vậy, cách biểu diễn này thường dài dòng, không thể hiện rõ
cấu trúc của thuật toán, đôi lúc gây hiểu lầm hoặc khó hiểu cho người đọc.
Gần như không có một quy tắc cố định nào trong việc thể hiện thuật toán
bằng ngôn ngữ tự nhiên. Tuy vậy, để dễ đọc, ta nên viết các bước con lùi
vào bên phải và đánh số bước theo quy tắc phân cấp như 1, 1.1, 1.1.1, ...
Ví dụ 1.5 Thuật toán giải phương trình bậc hai
Bước 1: Nhập ba hệ số a, b, c (giả sử a khác 0)
18
Cấu trúc dữ liệu và giải thuật
a=b D>0
- Thao tác xử lý – process: được biểu diễn bằng một hình chữ nhật, bên
trong chứa nội dung xử lý tương ứng
19
Cấu trúc dữ liệu và giải thuật
i=i+1 x = -b/(2a)
Thao tác xử lý
- Đường đi – router: sử dụng cung tên có mũi tên để chỉ hướng thực hiện
của hai bước kế tiếp nhau. Từ thao tác chọn lựa có thể có hai hướng đi, một
hướng ứng với điều kiện thỏa và một hướng ứng với điều kiện không thỏa.
Do vậy, ta dùng hai cung xuất phát từ các đỉnh hình thoi, trên mỗi cung có
ký hiệu Đúng (Yes) để chỉ hướng đi ứng với điều kiện thỏa và ký hiệu Sai
(No) để chỉ hướng đi ứng với điều kiện không thỏa.
S
D>0 D=0
Có 2 nghiệm
phân biệt x1, x2
Đường đi
- Điểm cuối – terminator: là điểm khởi đầu và kết thúc của một thuật
toán, được biểu diễn bằng hình oval, bên trong có ghi chữ Bắt đầu (Begin)
hoặc Kết thúc (End). Điểm cuối chỉ có cung đi ra (điểm khởi đầu) hoặc
cung đi vào (điểm kết thúc)
20
Cấu trúc dữ liệu và giải thuật
Điểm cuối
- Điểm nối – connector: được dùng để nối các phần khác nhau của một
lưu đồ lại với nhau. Bên trong điểm nối, ta đặt ký hiệu để biết sự liên hệ
giữa các điểm nối, ký hiệu thường đánh số đồng nhất nhau
- Điểm nối sang trang – Off page connector: tương tự như điểm nối
nhưng được dùng khi lưu đồ quá lớn phải vẽ trên nhiều trang. Bên trong
điểm nối sang trang đặt đồng nhất ký hiệu để biết được sự liên hệ giữa
điểm nối của các trang.
21
Cấu trúc dữ liệu và giải thuật
Hình 1.1 Lưu đồ thuật toán giải phương trình bậc hai
1.3.3 Mã giả
Tuy sơ đồ khối thể hiện rõ quá trình xử lý và sự phân cấp các trường hợp
của thuật toán nhưng lại cồng kềnh. Để mô tả một thuật toán nhỏ ta phải
dùng một không gian rất lớn. Hơn nữa, lưu đồ chỉ phân biệt hai thao tác là
rẽ nhánh (chọn lựa có điều kiện) và xử lý mà trong thực tế, các thuật toán
còn có thêm các thao tác lặp.
Khi thể hiện thuật toán bằng mã giả, ta sẽ vay mượn các cú pháp của một
ngôn ngữ lập trình nào đó để thể hiện thuật toán. Tất nhiên, mọi ngôn ngữ
lập trình đều có những thao tác cơ bản là xử lý, rẽ nhánh và lặp. Dùng mã
giả vừa tận dụng được các khái niệm trong ngôn ngữ lập trình, vừa giúp
22
Cấu trúc dữ liệu và giải thuật
người cài đặt dễ dàng nắm bắt nội dung thuật toán. Tất nhiên là trong mã
giả ta vẫn dùng một phần ngôn ngữ tự nhiên. Một khi đã vay mượn cú pháp
và khái niệm của ngôn ngữ lập trình thì chắc chắn mã giả sẽ bị phụ thuộc
vào ngôn ngữ lập trình đó.
Ví dụ 1.6 Đoạn mã giả giải phương trình bậc hai
if (Delta > 0)
{
x1 = (-b - sqrt(delta))/(2*a)
x2 = (-b + sqrt(delta))/(2*a)
xuất kết quả: phương trình có hai nghiệm là x1 và x2
}
else if (delta == 0)
xuất kết quả: phương trình có nghiệm kép là -b/(2*a)
else
xuất kết quả: phương trình vô nghiệm
1.4 Phân tích – Đánh giá giải thuật
Với mỗi vấn đề cần giải quyết, ta có thể tìm ra nhiều thuật toán khác
nhau. Có những thuật toán được thiết kế đơn giản, dễ hiểu, dễ lập trình và
sửa lỗi, tuy nhiên thời gian thực hiện lại lớn và tiêu tốn nhiều tài nguyên
máy tính. Ngược lại, có những thuật toán được thiết kế và lập trình rất phức
tạp nhưng lại cho thời gian chạy nhanh hơn, sử dụng tài nguyên của máy
tính hiệu quả hơn. Đồng thời cũng một vấn đề đặt ra có thể đưa ra nhiều
nhiều cách giải khác nhau. Khi đó, câu hỏi đặt ra là ta nên lựa chọn giải
thuật nào để thực hiện.
Đối với những chương trình chỉ được thực hiện một vài lần thì thời gian
chạy không phải là tiêu chí quan trọng nhất. Đối với những bài toán kiểu
này, thời gian để lập trình xây dựng và hoàn thiện thuật toán đáng quan tâm
hơn thời gian để chạy của chương trình vì vậy khi đó ta ưu tiên chọn những
giải thuật đơn giản về mặt thiết kế và xây dựng.
23
Cấu trúc dữ liệu và giải thuật
Tuy nhiên, đối với các chương trình được thực hiện nhiều lần thì thời
gian chạy của chương trình đáng quan tâm hơn rất nhiều so với thời gian
dùng để thiết kế và xây dựng được giải thuật đó. Khi đó, ta lại ưu tiên lựa
chọn một giải thuật có thời gian chạy nhanh hơn.
Các tiêu chí lựa chọn thuật toán để áp dụng:
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ớ
4. Thuật toán chạy nhanh
Hiện nay, để đánh giá một thuật toán thông thường người ta dựa trên
việc đánh giá chi phí về mặt không gian và chi phí về mặt thời gian dành
cho thuật toán đó. Vì vậy ta có hai tiêu chí đánh giá thuật toán: độ phức tạp
không gian và độ phức tạp thời gian
1.4.1 Độ phức tạp không gian
Độ phức tạp về mặt không gian của thuật toán được tính là tổng số chi
phí về mặt không gian (bộ nhớ) cần thiết sử dụng cho thuật toán.
Chi phí về mặt bộ nhớ được tính dựa trên
- Bộ nhớ lưu trữ dữ liệu đầu vào
- Bộ nhớ lưu trữ dữ liệu đầu ra
- Bộ nhớ lưu trữ các kết quả trung gian
1.4.2 Độ phức tạp thời gian
Độ phức tạp thời gian được tính là tổng thời gian cần thiết để hoàn thành
thuật toán, được đánh giá dựa vào số lượng các thao tác được sử dụng trong
thuật toán dựa trên bộ dữ liệu đầu vào
Các thao tác được xem xét khi đánh giá độ phức tạp thời gian:
- Phép so sánh hai đại lượng
- Phép cộng, trừ, nhân và chia
- Phép gán, phép thay đổi
…
24
Cấu trúc dữ liệu và giải thuật
25
Cấu trúc dữ liệu và giải thuật
f(n) = O(n3).
Một số quy tắc chung trong việc phân tích và tính toán thời gian thực
hiện chương trình
- Thời gian thực hiện các lệnh gán, đọc, ghi, … luôn luôn là O(1)
- Thời gian thực hiện chuỗi tuần tự các lệnh được xác định theo quy tắc
cộng cấp độ tăng. Có nghĩa là thời gian thực hiện của cả nhóm lệnh tuần tự
được tính là thời gian thực hiện của lệnh lớn nhất.
- Thời gian thực hiện lệnh rẽ nhánh if được tính bằng thời gian thực hiện
các lệnh khi điều kiện kiểm tra được thỏa mãn và thời gian thực hiện việc
kiểm tra điều kiện. Thời gian thực hiện kiểm tra điều kiện luôn là O(1).
- Thời gian thực hiện một vòng lặp được tính là tổng thời gian thực hiện
các lệnh ở thân vòng lặp qua tất cả các bước lặp và thời gian để kiểm tra
điều kiện dừng (thường là O(1)). Thời gian thực hiện này được tính theo
quy tắc nhân cấp độ tăng số lần thực hiện bước lặp và thời gian thực hiện
các lệnh ở thân vòng lặp. Các vòng lặp phải được tính thời gian thực hiện
một cách riêng rẽ.
1.4.4 Nguyên tắc tính toán thời gian chạy thực hiện chương trình
Việc xác định độ phức tạp tính toán của một giải thuật bất kỳ có thể rất
phức tạp. Tuy nhiên, trong thực tế, đối với một số giải thuật ta có thể phân
tích bằng một số quy tắc đơn giản sau:
a. Quy tắc bỏ hằng số
Nếu đoạn chương trình P có thời gian thực hiện là T(n) = O(c1.f(n)) với
c1 là một hằng số dương thì có thể coi đoạn chương trình đó có độ phức tạp
tính toán là O(f(n)).
Chứng minh:
T(n) = O(c1.f(n)) nến ∃c0>0 và ∃n0>0 để T(n≤c0.c1.f(n) với ∀n≥n0. Đặt
C=c0.c1 và dùng định nghĩa ta có T(n) = O(f(n)).
b. 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)).
26
Cấu trúc dữ liệu và giải thuật
Chứng minh:
T(n) = O(f(n) + g(n)) nên ∃C>0 và ∃n0>0 để T(n) ≤ C.f(n) + C.g(n),
∀n≥n0.
Vậy T(n) ≤ C.f(n) + C.g(n) ≤ 2C.max(f(n).g(n)) ∀n≥n0
Từ định nghĩa ta suy ra T(n) = O(max(f(n),g(n))
c. Quy tắc cộng
Nếu đoạn chương trình P1 có thời gian thực hiện T1(n) = O(f(n)) và đoạn
chương trình P2 có thời gian thực hiện T2(n) = O(g(n)) thì thời gian thực
hiện P1 rồi đến P2 tiếp theo sẽ là: T1(n) + T2(n) = O( max( f(n), g(n) ))
Chứng minh:
T1(n) = O(f(n)) nên ∃n1 và c1 để T1(n) ≤ c1.f(n) với ∀n≥n1
T2(n) = O(g(n)) nên ∃n2 và c2 để T2(n) ≤ c2.g(n) với ∀n≥n2
Chọn n0 = max(n1, n2) và c = max( c1, c2). Ta có: ∀n≥n0 thì
T1(n) + T2(n) ≤ c1.f(n) + c2.g(n) ≤ c.f(n) + c.g(n)
≤ c(f(n) + g(n)) ≤ 2c. max(f(n), g(n))
Vậy, T1(n) + T2(n) = O( max( f(n), g(n) ))
d. Quy tắc nhân
Nếu đoạn chương trình P có thời gian thực hiện là 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 sẽ là O(g(n).f(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). Theo định
nghĩa:
∃nk và ck để k(n)≤ck.g(n) với ∀n≥nk
∃nT và cT để T(n)≤cT.f(n) với ∀n≥nT
Vậy với ∀n≥max(nT, nk) ta có k(n).T(n)≤cT.ck(g(n).f(n))
27
Cấu trúc dữ liệu và giải thuật
1.5 Độ phức tạp tính toán với tình trạng dữ liệu vào
Có nhiều trường hợp, thời gian thực hiện giải thuật không 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 đó nữa.
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 rồi nhưng 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.
" Phân tích thời gian thực hiện giải thuật trong trường hợp xấu
nhất (worst-case analysis): với một kích thước dữ liệu n, tìm T(n) là thời
gian lớn nhất khi thực hiện giải thuật trên mọi bộ dữ liệu kích thước n và
phân tích thời gian thực hiện giải thuật dựa trên hàm T(n).
" Phân tích thời gian thực hiện trong trường hợp tốt nhất (best-
case analysis): với một kích thước dữ liệu n, tìm T(n) là thời gian ít nhất
thực hiện trên mọi bộ dữ liệu kích thước n và phân tích thời gian thực hiện
giải thuật dựa trên hàm T(n).
" Phân tích thời gian trung bình thực hiện giải thuật (average-case
analysis): giải sử rằng dữ liệu vào tuân theo một phân phối xác suất nào đó
(chẳng hạn phân bố đều nghĩa là khả năng chọn mọi bộ dữ liệu vào là như
nhau) và tính toán giá trị kỳ vọng (trung bình) của thời gian chạy cho mỗi
kích thước dữ liệu n (T(n)), sau đó phân tích thời gian thực hiện giải thuật
dựa trên hàm T(n).
1.6 Phân lớp bài toán
Như đã được chú ý ở trên, hầu hết các thuật toán đều có một tham số
chính là N, thông thường đó là số lượng các phần tử dữ liệu được xử lý.
Tham số này ảnh hưởng rất nhiều tới thời gian chạy. Tham số N có thể là
bậc của một đa thức, kích thước của một tập tin được sắp xếp hay tìm kiếm,
số nút trong một đồ thị, v.v, … Hầu hết tất cả các thuật toán trong giáo
trình này có thời gian chạy tiệm cận tới một trong các hàm sau:
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ó
28
Cấu trúc dữ liệu và giải thuật
là hằng số. Điều này hiển nhiên là hoàn cảnh 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ỏ kích thước bớt 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. Bởi vì thiếu
một tính từ tốt hơn (có lẽ là “tuyến tính logarit”?), chúng ta nói rằng thời
gian chạy của thuật toán như thế là “NlogN”. Khi N là một triệu, NlogN có
lẽ khoảng 20 triệu. khi N được nhân gấp đôi, thời gian chạy bị nhân lên
nhiều hơn gấp đôi (nhưng không nhiều lắm).
N2: 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 tăng gấp đôi thì thời gian chạy
tăng lên gấp 4 lần.
N3: 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í ý
29
Cấu trúc dữ liệu và giải thuật
nghĩa thực tế trong các bài toán nhỏ. Khi N tăng gấp đô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ế, mặc dù các thuật toán như thế là “sự ép buộc
thô bạo” để giải các bài toán. Khi N tăng gấp đôi thì thời gian chạy được
nâng lên luỹ thừa hai!
Thời gian chạy của một chương trình cụ thể đôi khi là một hệ số hằng
nhân với các số hạng nói trên (“số hạng dẫn đầu”) cộng thêm một số hạng
nhỏ hơn. Giá trị của hệ số hằng và các số hạng phụ thuộc vào kết quả của
sự phân tích và các chi tiết cài đặt. Hệ số của số hạng dẫn đầu liên quan tới
số chỉ thị bên trong vòng lặp: Ở một tầng tùy ý của thiết kế thuật toán thì
phải cẩn thận giới hạn số chỉ thị như thế. Với N lớn thì các số hạng dẫn đầu
đóng vai trò chủ chốt; với N nhỏ thì các số hạng cùng đóng góp vào sự so
sánh các thuật toán sẽ khó khăn hơn. Trong hầu hết các trường hợp, chúng
ta sẽ gặp các chương trình có thời gian chạy là “tuyến tính”, “NlogN”, “bậc
ba”,… với hiểu ngầm là các phân tích hay nghiên cứu thực tế phải được
làm trong trường hợp mà tính hiệu quả là rất quan trọng.
1.7 Đệ quy
1.7.1 Giới thiệu
Đối tượng được gọi là đệ quy nếu được định nghĩa qua chính nó hoặc
một đối tượng khác cùng dạng với chính nó bằng quy nạp.
Ví dụ 1.9 Ta đặt hai chiếc dương đối diện nhau, sau đó đặt một vật thể
bất kỳ vào giữa hai chiếc gương, khi đó ta thấy trong gương này cũng có
hình ảnh chiếc gương và vật thể kia và ngược lại.
Hoặc trong trường hợp toán học ta gặp định nghĩa đệ quy như bài toán
tính giai thừa của số n. Giai thừa được tính theo nguyên tắc: nếu n = 0 thì
giai thừa bằng 1 ngược lại giai thừa (n) = n * giai thừa (n-1).
1.7.2 Giải thuật đệ quy
Giải thuật đệ quy là lời giải một bài toán P được thực hiện bằng lời giải
của bài toán P’ khác có dạng giống như P với P’ phải “nhỏ” hơn P.
30
Cấu trúc dữ liệu và giải thuật
Tùy thuộc cách diễn đạt tác vụ đệ quy mà có các loại đệ quy sau: Đệ quy
tuyến tính là trường hợp trong thân hàm có một lời gọi đến chính nó, Đệ
quy nhị phân là trường hợp trong thân hàm có 2 lời gọi đến chính nó, Đệ
quy phi tuyến là trường hợp trong thân hàm lặp lời gọi nhiều lần đến chính
nó, Đệ quy hỗ tương là trường hợp có hai hàm đệ quy khác nhau mà mỗi
thân hàm đệ quy đều có lời gọi đến các hàm đệ quy khác.
Để định nghĩa một hàm đệ quy hay thủ tục đệ quy phải tiến hành xác
định hai phần sau:
• Phần neo (anchor):
o Phần này chỉ ra lời giải của bài toán thực hiện trong những
trường hợp đơn giản,
o Có thể giải trực tiếp chứ không cần phải nhờ đến một bài toán
con nào khác.
• Phần đệ quy:
o Xác định những bài toán con đã có và bài toán cần giải thực hiện
gọi đệ quy giải những bài toán con đó,
o Chỉ ra để giải bài toán đó thì phối hợp các các bài toán con đã
có lời giải lại với nhau theo một nguyên tắc nào đó.
Ví dụ 1.10 Tính giai thừa của n với n>=0
- Phần tử neo: n = 0 thì Giai thừa = 1
- Phần đệ quy: Giai thừa (n) = n * Giai thừa (n-1)
Các bước để giải quyết một bài toán bằng lời giải đệ quy:
• Thông số hóa bài toán,
• Tìm các điều kiện biên (chặn) – lời giải với giá trị cụ thể. Có nghĩa
là tìm giải thuật cho các tình huống này, xác định phần tử neo của bài toán,
• Tìm giải thuật tổng quát theo hướng đệ quy lui dần về tình huống bị
chặn.
Ví dụ 1.11 Tính tổng của n phần tử trong mảng a các số nguyên
Thông số hóa: int *a, int n
31
Cấu trúc dữ liệu và giải thuật
32
Cấu trúc dữ liệu và giải thuật
Dãy số Fibonaci bắt nguồn từ bài toán cổ về sinh sản của các cặp thỏ.
Bài toán phát biểu như sau:
1. Các con thỏ không bao giờ chết
2. Hai tháng sau khi ra đời mỗi cặp thỏi sẽ sinh ra một cặp thỏ con mới
(một con đực và một con cái)
3. Khi đã sinh con rồi thì cứ tiếp tục mỗi tháng chúng lại sinh được một
cặp con mới
Câu hỏi đặt ra, ban đầu chỉ có một cặp mới ra đời thì đến tháng thứ n sẽ
có thêm bao nhiêu cặp thỏ nữa.
Ví dụ 1.12 Cho n = 5, Tính các cặp thỏ sinh sản ở tháng n tương ứng.
Với n = 5, ta có:
Tháng thứ 1: 1 cặp (cặp ban đầu)
Tháng thứ 2: 1 cặp (cặp ban đầu chưa sinh thêm)
Tháng thứ 3: 2 cặp (có thêm một cặp mới)
Tháng thứ 4: 3 cặp (cặp ban đầu tiếp tục đẻ thêm và cặp mới chưa đẻ
thêm)
Tháng thứ 5: 5 cặp (cặp ban đầu tiếp tục để và cặp con mới đầu tiên bắt
đầu đẻ mới)
33
Cấu trúc dữ liệu và giải thuật
Tháng thứ 1:
Tháng thứ 2:
Tháng thứ 3:
Tháng thứ 4:
Tháng thứ 5:
Như vậy, để tính được số cặp thỏ ở tháng thứ n trở về bài toán tính
Fibonaci(n) với nguyên tắc tính Fibonaci(n) = Fibonaci(n-2) + Fibonaci(n-
1).
* Công thức đệ quy:
Fibonaci(n) = 1 nếu n = 1 hoặc n = 2
Fibonaci(n-2) + Fibonaci(n-1) với n>2
* Cài đặt đệ quy
35
Cấu trúc dữ liệu và giải thuật
- Để biểu thị cấp độ tăng của hàm, ta sử dụng ký hiệu O(n). Cấp độ tăng
về thời gian thực hiện của chương trình cho phép ta xác định độ lớn của bài
toán mà ta có thể giải quyết.
- Để tính độ phức tạp của một bài toán ta áp dụng quy tắc cộng và quy
tắc nhân để xác định.
- Một số giải thuật cơ sở thường gặp.
1.8.2 Câu hỏi ôn tập
1. Trình bày khái niệm thuật toán? Các đặc điểm của thuật toán?
2. Thời gian thực hiện một chương trình thường phụ thuộc vào các yếu
tố nào? Phân tích cụ thể từng yếu tố?
3. Nói thời gian thực hiện của chương trình là T(n) = O(f(n)) có nghĩa là
gì? Cho ví dụ minh họa?
4. Hãy nêu quy tắc cộng và nhân cấp độ tăng của hàm và đưa ví dụ minh
họa.
1.9 Bài tập áp dụng
Bài 1. Giả sử quy tắc tổ chức quản lý nhân viên của một công ty như
sau:
*Thông tin về một nhân viên bao gồm lý lịch và bảng chấm công:
+ Lý lịch nhân viên:
- Mã nhân viên: chuỗi 8 ký tự
- Họ, Tên nhân viên: chuỗi 30 ký tự
- Tình trạng gia đình: 1 ký tự (M = Married, S = Single)
- Số con: số nguyên ≤ 20
- Trình độ văn hoá: chuỗi 2 ký tự, trình độ được nhập với nguyên tắc sau
(C1 = cấp 1; C2 = cấp 2; C3 = cấp 3; ĐH = đại học, CH = cao học)
- Lương căn bản: số ≤ 1.000.000
+ Chấm công nhân viên:
- Số ngày nghỉ có phép trong tháng: số ≤ 28
36
Cấu trúc dữ liệu và giải thuật
37
Cấu trúc dữ liệu và giải thuật
n = n - 1;
printf("%5d", n+k );
}
c. k = 10; n = 1;
do {
n = n + 1;
printf("%5d", k-n );
if (n%2==1)
k = k-5;
} while ( k>n );
d. m = 10; n = 20;
while ( m<n )
{
m++;
printf("%5d", m-n );
if ( m%2==1 )
n = n-2;
}
Bài 3. Áp dụng tìm điểm dừng và giải thuật đệ quy lập trình giải các bài
toán sau với dữ liệu nhập vào từ bàn phím và kết quả đưa ra màn hình:
m
- Nhập các số nguyên n, m. Tính và đưa ra giá trị C n (0 ≤ m ≤ n ≤ 31).
- Tìm giá trị lớn nhất trong m số đầu tiên của mảng a[1..m]?
- Tìm giá trị ước số chung lớn nhất của hai số nguyên a,b (với a,b>0)
- Tính biểu thức: S=(x - (x - (x - ....(x-1)2....)2)2)2 - với n lần bình phương
- Tính biểu thức sau: S = sin( x + sin( x + … sin(x) …)) - với n
hàm sin
- Tính tổng n số đầu tiên trong mảng a[1..n]
38
Cấu trúc dữ liệu và giải thuật
- Bài toán bội số chung nhỏ nhất của hai số nguyên a,b (với a, b>0)
- Chuyển số n trong hệ cơ số 10 sang hệ cơ số 2
39
Cấu trúc dữ liệu và giải thuật
Khi đó mảng s là một danh sách có kiểu cấu trúc Sinhvien, chứa tối đa
100 sinh viên, mỗi sinh viên có các thông tin cá nhân gồm họ tên và điểm.
Phép chèn một phần tử vào mảng
42
Cấu trúc dữ liệu và giải thuật
Thủ tục trên thực hiện thêm một sinh viên x vào vị trí vt của danh sách
và thủ tục chỉ thực hiện được khi mà danh sách đó chưa đầy.
Phép xóa một phần tử khỏi danh sách
Mảng ban đầu:
43
Cấu trúc dữ liệu và giải thuật
Muốn xóa phần tử thứ p của mảng mà vẫn giữ nguyên thứ tự của các
phần tử còn lại, ta thực hiện:
- Dồn các phần tử từ vị trí thứ p+1 tới n lên trước một vị trí
Thủ tục trên thực hiện xóa phần tử tại vị trí vt khỏi danh sách. Phép toán
chỉ thực hiện được khi danh sách không rỗng.
Nhận xét về phương pháp cài đặt danh sách bởi mảng:
44
Cấu trúc dữ liệu và giải thuật
Như chúng ta đã biết, dùng mảng để lưu trữ các phần tử của danh sách
cho phép ta truy cập trực tiếp đến từng phần tử tại vị trí bất kì trong danh
sách. Tuy nhiên phương pháp này có hạn chế lớn đó là không gian nhớ
không đổi xác định bởi kích cỡ của mảng, chúng ta không thể thực hiện
thêm mới vượt quá khả năng lưu trữ của mảng.
2.2.2 Bài toán tìm kiếm trên danh sách
Tìm kiếm thông tin là một bài toán rất quan trọng trong đời sống. Ví dụ
muốn tra cứu thông tin tuyển sinh và bạn biết số báo danh, bạn cần có các
thông tin về họ tên thí sinh, điểm thành phần, điểm tổng cũng như các
thông tin khác gắn với số báo danh đó. Thông thường trong tin học các
thông tin về một đối tượng được biểu diễn dưới dạng bản ghi/cấu trúc, các
thuộc tính của đối tượng sẽ là các trường của cấu trúc. Khi tìm kiếm chúng
ta sẽ dựa trên các thuộc tính đã biết về đối tượng, các thuộc tính đó gọi là
khóa tìm kiếm. Như vậy, khóa tìm kiếm có thể là một hoặc một số trường
của cấu trúc. Với một giá trị cho trước của khóa có thể có nhiều bản ghi có
khóa đó hoặc không có bản ghi nào.
Người ta chia làm 2 loại tìm kiếm: tìm kiếm trong và tìm kiếm ngoài.
Tìm kiếm ngoài là việc tìm kiếm dựa trên các tệp dữ liệu được lưu ở bộ
nhớ ngoài máy tính như ổ đĩa cứng, băng từ, USB Flash disk,…Khi dữ liệu
được lưu ở bộ nhớ trong (RAM) thì đó là bài toán tìm kiếm trong. Chúng ta
chỉ đề cập đến phương pháp tìm kiếm trong.
a. Tìm kiếm tuần tự
Tìm kiếm tuần tự là phương pháp lần lượt duyệt qua toàn bộ các phần tử
trong danh sách một cách tuần tự. Tại mỗi bước khóa của bản ghi sẽ được
so sánh với giá trị cần tìm. Quá trình tìm kiếm sẽ kết thúc khi tìm thấy bản
ghi có khóa thỏa mãn hoặc khi duyệt hết danh sách.
Hàm thực hiện tìm kiếm trên một mảng số nguyên như sau:
45
Cấu trúc dữ liệu và giải thuật
Hàm trên duyệt từ đầu mảng, nếu tại vị trí nào đó giá trị phần tử bằng
với giá trị cần tìm thì hàm trả về chỉ số tương ứng của phần tử trong mảng.
Nếu không tìm thấy giá trị trong toàn bộ mảng thì hàm trả về giá trị -1.
Thuật toán tìm kiếm tuần tự có thời gian thực hiện O(n), trong trường
hợp xấu nhất thuật toán mất n lần thực hiện phép so sánh.
b. Tìm kiếm nhị phân
Trong trường hợp số bản ghi cần tìm rất lớn, việc tìm kiếm tuần tự có thể
là 1 giải pháp không hiệu quả về mặt thời gian. Một giải pháp tìm kiếm
khác hiệu quả hơn có thể được sử dụng dựa trên mô hình “chia để trị” như
sau: Chia tập cần tìm làm 2 nửa, xác định nửa chứa bản ghi cần tìm và tập
trung tìm kiếm trên nửa đó. Để làm được điều này, tập các phần tử cần phải
được sắp, và sử dụng chỉ số của mảng để xác định nửa cần tìm. Đầu tiên, so
sánh giá trị cần tìm với giá trị của phần tử ở giữa. Nếu nó nhỏ hơn, tiến
hành tìm ở nửa đầu dãy, ngược lại, tiến hành tìm ở nửa sau của dãy. Quá
trình được lặp lại tương tự cho nửa dãy vừa được xác định này.
Ví dụ 2.5 Tìm kiếm phần tử có giá trị x từ vị trí left đến vị trí right trong
dãy đã sắp xếp tăng.
- Tìm phần tử giữa: middle = (left+right)/2
- Nếu x < a[middle] thì ta thực hiện tìm kiếm trên đoạn [left, middle -1]
(nửa đầu)
46
Cấu trúc dữ liệu và giải thuật
- Nếu x > a[middle] thì ta thực hiện tìm trên đoạn [middle+1, right] (nửa
sau)
47
Cấu trúc dữ liệu và giải thuật
Hàm thực hiện tìm giá trị x trong dãy a, biến left và right lưu vị trí đầu
và cuối của danh sách con cần tiếp tục tìm. Biến middle lưu vị trí giữa của
mỗi danh sách con. Quá trình tìm được thực hiện bởi vòng lặp do … while.
Mỗi lần lặp x sẽ so sánh với giá trị của phần tử giữa danh sách. Nếu bằng
nhau thì dừng và trả lại vị trí tìm thấy. Nếu x nhỏ hơn, ta tiếp tục tìm ở nửa
đầu danh sách con đang xét (đặt lại right = middle - 1), ngược lại tìm ở nửa
cuối danh sách left = middle+1.
Thuật toán tìm kiếm nhị phân có thời gian thực hiện O(log2n). Thuật
toán này đòi hỏi phải sắp xếp các phần tử trước khi tìm kiếm.
2.2.3 Bài toán sắp xếp trên danh sách
Sắp xếp là quá trình biến đổi một danh sách các đối tượng theo một thứ
tự xác định nào đó. Sắp xếp có vai trò quan trọng trong tìm kiếm dữ liệu.
Ví dụ muốn tra cứu từ điển nếu các từ không được sắp xếp theo trật tự thì
việc tra cứu sẽ rất khó khăn.
Các giải thuật sắp xếp chia làm 2 loại: cài đặt đơn giản nhưng không
hiệu quả phải sử dụng nhiều thao tác, một loại cài đặt phức tạp nhưng hiệu
quả tốc độ xử lý nhanh hơn. Đối với bài toán sắp với số lượng ít các phần
tử thì nên cài đặt theo cách đơn giản, ngược lại dùng cách 2 hiệu quả hơn.
48
Cấu trúc dữ liệu và giải thuật
Thông thường các đối tượng cần sắp được biểu diễn bởi cấu trúc gồm
một số trường. Một trong các trường đó gọi là khóa sắp xếp, khóa sắp xếp
có kiểu dữ liệu cơ bản (nguyên, thực, …)
a. Sắp xếp bằng lựa chọn – Selection Sort
Đây là thuật toán đơn giản nhất. Ý tưởng của thuật toán như sau:
• Tại mỗi bước chọn ra phần tử nhỏ nhất trong số phần tử chưa xét và
đưa vào vị trí thích hợp, cố định phần tử này không xét lại nữa.
• Dãy ban đầu có n phần tử, thuật toán thực hiện n-1 lượt để đưa phần
tử nhỏ nhất trong dãy hiện hành về vị trí đúng ở đầu dãy.
Ví dụ 2.6 Các bước thực hiện sắp xếp chọn dãy số bên dưới như sau:
Bước 1: Chọn được phần tử nhỏ nhất là 06 trong dãy ban đầu chưa xét,
đổi chỗ cho 32, cố định lại và không xét phần tử này trong bước tiếp theo.
Bước 2: Chọn được phần tử nhỏ nhất là 17, vị trí của 17 đã phù hợp nên
giữ nguyên.
49
Cấu trúc dữ liệu và giải thuật
Bước 3: Chọn được phần tử nhỏ nhất tiếp theo là 25, đổi chỗ cho 49.
Bước 4: Chọn được phần tử nhỏ nhất trong dãy chưa xét là 32, đổi chỗ
cho 98.
Bước 5: Chọn được phần tử nhỏ nhất trong dãy chưa xét còn lại là 49,
đổi chỗ cho 98.
Bước 6: Chọn được phần tử nhỏ nhất trong dãy còn lại là 53, đổi chỗ cho
98.
50
Cấu trúc dữ liệu và giải thuật
Bước 7: Chọn được phần tử nhỏ nhất trong số 2 phần tử còn lại là 61, đổi
chỗ cho 98.
Trong thủ tục trên, vòng lặp đầu tiên duyệt từ đầu đến cuối dãy. Tại mỗi
vị trí i, tiến hành duyệt tiếp từ i tới cuối dãy để chọn ra phần tử nhỏ thứ i và
đổi chỗ cho phần tử ở vị trí i.
2
Thời gian thực hiện thuật toán tỷ lệ với N , vì vòng lặp ngoài (biến chạy
i) duyệt qua N phần tử, và vòng lặp trong duyệt trung bình N/2 phần tử. Do
2 2
đó, độ phức tạp trung bình của thuật toán là O(N * N/2) = O(N /2) = O(N ).
b. Sắp xếp bằng xen vào- Insertion Sort
Ý tưởng của thuật toán là: với dãy ban đầu a1, a2,…an ta có thể xem như
đã có đoạn gồm một phần tử a1 đã được sắp, sau đó thêm a2 vào đoạn a1 sẽ
có đoạn a1, a2 được sắp; tiếp tục thêm a3 vào đoạn a1, a2 để có đoạn a1, a2, a3
được sắp; tiếp tục cho đến khi thêm xong an vào đoạn a1, a2, ..., an-1 sẽ có
dãy a1, a2,…an được sắp.
Ví dụ 2.7 Xét lại ví dụ trên với thuật toán sắp chèn trực tiếp
Dãy ban đầu
Bước 1: Chèn phần tử đầu của nửa chưa sắp là 32 vào nửa đã sắp. Do
nửa đã sắp là trống nên có thể chèn vào vị trí bất kỳ.
52
Cấu trúc dữ liệu và giải thuật
Bước 2: Chèn phần tử 17 vào nửa đã sắp. Dịch chuyển 32 sang phải 1 vị
trí và đưa 17 vào vị trí trống.
Bước 5: Chèn phần tử 06 vào nửa đã sắp. Dịch chuyển các phần tử 17,
32, 49, 98 sang phải 1 vị trí và đưa 06 vào vị trí trống.
Bước 6: Chèn phần tử 25 vào nửa đã sắp. Dịch chuyển các phần tử 32,
49, 98 sang phải 1 vị trí và đưa 25 vào vị trí trống.
Bước 7: Chèn phần tử 53 vào nửa đã sắp. Dịch chuyển phần tử 98 sang
phải 1 vị trí và đưa 53 vào vị trí trống.
53
Cấu trúc dữ liệu và giải thuật
Bước 8: Chèn phần tử cuối cùng 61 vào nửa đã sắp. Dịch chuyển phần tử
98 sang phải 1 vị trí và đưa 61 vào vị trí trống.
54
Cấu trúc dữ liệu và giải thuật
Trong trường hợp xấu nhất dãy cần sắp có thứ tự ngược với trật tự cần
sắp do đó ở lượt thứ i cần i-1 phép so sánh và tổng số phép so sánh là:
(n-1) + (n-2) + … + 2 + 1 = n*(n-1)/2
Vậy thủ tục sắp xếp chèn trực tiếp có độ phức tạp O(n2)
c. Sắp xếp nổi bọt - Buble Sort
Giải thuật sắp xếp nổi bọt được thực hiện theo nguyên tắc: Duyệt nhiều
lần từ cuối lên đầu dãy, tiến hành đổi chỗ 2 phần tử liên tiếp nếu chúng
ngược thứ tự. Đến một bước nào đó, khi không có phép đổi chỗ nào xảy ra
thì toàn bộ dãy đã được sắp.
Trở lại với ví dụ trên, khi thực hiện sắp xếp bằng thuật toán nổi bọt ta có
các bước thực hiện như sau:
Bước 1: Tại bước này, khi duyệt từ cuối dãy lên, lần lượt xuất hiện các
cặp ngược thứ tự là (06, 98), (06, 49), (06, 17), (06, 32). Phần tử 06 “nổi”
lên đầu dãy.
55
Cấu trúc dữ liệu và giải thuật
Bước 2: Duyệt từ cuối dãy lên, lần lượt xuất hiện các cặp ngược thứ tự là
(25, 98), (25, 49), (17, 32). Phần tử 17 nổi lên vị trí thứ 2.
Bước 3: Duyệt từ cuối dãy lên, lần lượt xuất hiện các cặp ngược thứ tự là
(53, 98), (25, 32). Phần tử 25 nổi lên vị trí thứ 3.
56
Cấu trúc dữ liệu và giải thuật
Bước 4: Duyệt từ cuối dãy lên, xuất hiện cặp ngược thứ tự là (61, 98).
Bước 5: Duyệt từ cuối dãy lên, không còn xuất hiện cặp ngược nào.
Toàn bộ dãy đã được sắp
57
Cấu trúc dữ liệu và giải thuật
58
Cấu trúc dữ liệu và giải thuật
Thủ tục sắp nổi bọt có độ phức tạp O(n2) vì ở lần duyệt đầu tiên cần
khoảng n-1 phép so sánh và đổi chỗ để làm nổi phần tử nhỏ nhất lên đầu.
Lần duyệt thứ 2 cần khoảng n-2 phép toán, .v.v. Tổng cộng, số phép so
sánh cần thực hiện là:
(n-1) + (n-2)+ … + 2+1 = n*(n-1)/2
d. Quick Sort
QuickSort là thuật toán sắp xếp tốt nhất dù dãy khóa cần sắp có kiểu dữ
liệu thuộc thứ tự nào thì thuật toán cũng có thể sắp xếp được.
Ý tưởng của thuật toán là: Chọn một phần tử ngẫu nhiên nào đó của dãy
làm “chốt” thông thường chọn phần tử giữa (left + right)/2 làm chốt, mọi
phần tử nhỏ hơn “chốt” được xếp vào vị trí trước và mọi phần tử lớn hơn
“chốt” được xếp vào vị trí sau => Các phần tử của dãy được so sánh với
“chốt” và đổi chỗ cho nhau, hoặc cho “chốt” nếu nó lớn hơn chốt mà nằm
trước hoặc nhỏ hơn chốt mà nằm sau. Sau lượt đầu tiên thì dãy được chia
thành 2 đoạn: 1 đoạn bao gồm các phần tử nhỏ hơn chốt, đoạn còn lại bao
gồm các phần tử lớn hơn chốt. Và vấn đề là sắp xếp 2 đoạn vừa tạo thành
có độ dài nhỏ hơn độ dài đoạn ban đầu bằng phương pháp tương tự (gọi đệ
quy)
Ví dụ 2.8 Cho dãy 10, 5, 7, 3, 9, 2, 15, 1. Mô tả từng bước sắp xếp tăng
dần bằng QuickSort
Dãy ban đầu:
59
Cấu trúc dữ liệu và giải thuật
Quá trình duyệt, làm nhiệm vụ đưa các phần tử nhỏ hơn hoặc bằng 3 lên
nửa trước và đưa các phần tử lớn hơn phần tử 3 về nửa sau. Như vậy, sau
lần duyệt thứ nhất dãy được chia thành 2 đoạn, kết quả:
Đoạn 1: từ vị trí 0 đến 2, các phần tử nhỏ hơn hoặc bằng 3
Đoạn 2: từ vị trí 3 đến 7, các phần tử lớn hơn 3
60
Cấu trúc dữ liệu và giải thuật
Bước 2: Xét khoảng con từ vị trí 3 đến vị trí 7 (Left = 3 và Right = 7).
Phần tử chốt x = a[mid] = 5
61
Cấu trúc dữ liệu và giải thuật
Quá trình duyệt, làm nhiệm vụ đưa các phần tử nhỏ hơn hoặc bằng 5 lên
nửa trước và đứa các phần tử lớn hơn phần tử 5 về nửa sau. Tiếp tục, chia
khoảng được xét thành 2 đoạn, kết quả:
Đoạn 1: từ vị trí 3 đến vị trí 3, các phần tử nhỏ hơn hoặc bằng 5
Đoạn 2: từ vị trí 4 đến vị trí 7, các phần tử lớn hơn 5
62
Cấu trúc dữ liệu và giải thuật
Quá trình duyệt và đổi chỗ lại lặp lại với các đoạn vừa tạo thành và tiếp
tục cho đến khi toàn bộ dãy được sắp. Kết quả thu được:
• Bước 2:
o Nếu (L<j) Phân hoạch dãy aL … aj
o Nếu (i<R) Phân hoạch dãy ai … aR
*Phân hoạch bài toán
• Bước 1: chọn tùy ý phần tử chốt x = a[k] trong dãy aL, a2, …, aR.
(thông thường lấy chính giữa k = (L+R)/2 )
o i = L; j = R;
• Bước 2: phát hiện và điều chỉnh các phần tử a[i] và a[j] sai vị trí:
o trong khi a[i]<x thì i++
o trong khi a[j]>x thì j- -
o nếu i<j thì
# hoán vị a[i] và a[j]
# i++; j--;
• Bước 3:
o Nếu i<j: thì lặp lại bước 2
o Ngược lại i>=j thì dừng phân hoạch
*Thủ tục sắp Quick Sort được cài đặt như sau:
64
Cấu trúc dữ liệu và giải thuật
Trong trường hợp xấu nhất cần phải mất n lần gọi đệ quy và mỗi lần chỉ
loại được 1 phần tử. Thời gian thực hiện thuật toán trong trường hợp xấu
2 2
nhất này là khoảng N /2, có nghĩa là O(N ). Trong trường hợp tốt nhất, mỗi
lần phân chia sẽ được 2 nửa dãy bằng nhau, khi đó thời gian thực hiện thuật
toán T(N) sẽ được tính là:
T(N) = 2T(N/2) + N
Khi đó, T(N) ≈ NlogN.
e. Sắp xếp bằng đổi chỗ trực tiếp – Interchange Sort
Ý tưởng: Xuất phát từ dãy đầu a0, a1, …, ai xét các phần tử sau đó từ ai+1
đến an xem có phần tử nào nhỏ hơn ai không thì hoán đổi vị trí => Sau mỗi
lần luôn được dãy a0, a1, …, ai đã được sắp thứ tự
Mô tả thuật toán
*Đầu vào:
n – số phần tử mảng
a – mảng chứa các phần tử bất kỳ
*Đầu ra:
a - mảng đã được sắp xếp tăng dần
*Các bước:
• Bước 1: i = 1
• Bước 2: tính các giá trị j = i + 1
• Bước 3: Trong khi j<n thực hiện
o nếu a[j] < a[i] thì hoán đổi a[i] với a[j]
o j = j + 1;
• Bước 4: i = i +1
o nếu i<n thì lặp lại bước 2
o ngược lại thì dừng
*Thủ tục cài đặt như sau:
65
Cấu trúc dữ liệu và giải thuật
Thủ tục sắp chèn trực tiếp có độ phức tạp O(n2). Trong trường hợp tốt
nhất dãy đã được sắp thì ta cần khoảng n*(n-1)/2 phép so sánh và không
phải thực hiện đổi chỗ. Trong trường hợp xấu nhất thì cần n*(n-1)/2 phép
so sánh và n*(n-1)/2 phép đổi chỗ các phần tử ngược thứ tự.
2.3 Cài đặt danh sách bằng danh sách liên kết
2.3.1 Khái niệm
Danh sách liên kết là tập các phần tử liên kết móc nối liên tiếp với nhau,
có kiểu truy cập tuần tự. Mỗi một phần tử là một nút.
Mỗi nút gồm hai phần:
- Dữ liệu (Data): là các thành phần dữ liệu mà một nút đó lưu trữ
- Liên kết (Linked): là con trỏ kiểu nút đang định nghĩa được dùng để
liên kết với các nút khác
Để khai báo cấu trúc dữ liệu dạng danh sách liên kết đơn thực hiện:
- Khai báo cấu trúc dữ liệu của thông tin được lưu trữ - Data
- Khai báo cấu trúc của một nút – Node
struct Node
{
Data Infor;
struct Node *Next;
66
Cấu trúc dữ liệu và giải thuật
};
- Khai báo danh sách - LIST: mỗi danh sách được xác định bởi phần tử
đầu (Head) và phần tử cuối (Tail)
struct LIST
{
Node Head;
Node Tail;
};
LIST Q;
Ví dụ 2.9 Cho thông tin của sinh viên gồm: Mã sinh viên (số nguyên),
Họ tên (chuỗi), Tuổi (số nguyên), Điểm trung bình (số thực). Khai báo cấu
trúc dữ liệu dạng danh sách liên kết đơn để lưu trữ danh sách các sinh viên
- Khai báo cấu trúc Sinh viên
struct SinhVien
{
int MaSV;
char HoTen[20];
int Tuoi;
float DTB;
};
- Khai báo cấu trúc một Node
struct Node
{
SinhVien Infor; // khai báo dữ liệu
struct Node *Next; //khai báo liên kết
};
- Khai báo danh sách liên kết đơn
struct LIST
67
Cấu trúc dữ liệu và giải thuật
{
Node *Head;
Node *Tail;
};
LIST Q;
2.3.2 Các phép toán trên danh sách liên kết
a. Khởi tạo danh sách rỗng
Danh sách rỗng là danh sách chưa có chứa bất kỳ phần tử nào, có nghĩa
là phần tử đầu và phần tử cuối của danh sách trỏ đến địa chỉ NULL.
68
Cấu trúc dữ liệu và giải thuật
- Chèn thêm một nút vào sau một nút nào đó trong danh sách
*Thêm vào đầu
Giả sử ta có 1 danh sách mà đầu của danh sách được trỏ tới bởi con trỏ
q.Head, cuối danh sách được trỏ bởi con trỏ q.Tail.
Các bước để chèn 1 nút mới vào đầu danh sách như sau:
Hình 2.1 Minh họa thêm phần tử mới vào đầu danh sách
Mô tả thuật toán:
• Nếu danh sách rỗng thì:
o Phần tử đầu là phần tử mới chèn vào
o Phần tử cuối cũng là phần tử đầu
• Ngược lại (danh sách khác rỗng):
o Phần tử mới trỏ tới phần tử đầu
o Phần tử đầu là phần tử mới chèn vào
Thủ tục được cài đặt như sau:
69
Cấu trúc dữ liệu và giải thuật
Hình 2.2 Chèn thêm phần tử mới vào cuối danh sách
Cho con trỏ tiếp của nút cuối q.Tail trỏ đến nút mới tạo là p, và cho con
trỏ tiếp của p trỏ tới NULL.
Thuật toán:
• Nếu danh sách rỗng thì:
o Phần tử đầu là phần tử mới chèn vào
o Phần tử cuối cũng là phần tử đầu
• Ngược lại (danh sách khác rỗng):
o Phần tử cuối trỏ tới phần tử mới chèn vào
o Phần tử cuối là phần tử mới chèn vào
Thủ tục được cài như sau:
70
Cấu trúc dữ liệu và giải thuật
Hình 2.3 Chèn thêm phần tử mới vào sau phần tử q xác định
Cho con trỏ tiếp của p trỏ tới phần tử đứng sau q, con trỏ tiếp của q trỏ
tới p
Thuật toán:
• Nếu phần tử q rỗng $ không chèn được
• Nếu phần tử q khác rỗng $ có tồn tại phần tử q
o Phần tử mới trỏ tới phần tử đứng sau phần tử q
o Phần tử q trỏ tới phần tử mới chèn vào
Thủ tục cài đặt như sau:
71
Cấu trúc dữ liệu và giải thuật
72
Cấu trúc dữ liệu và giải thuật
Hình 2.5 Minh họa xóa phần tử đứng sau phần tử q xác định
*Thủ tục cài đặt
73
Cấu trúc dữ liệu và giải thuật
e. Tìm kiếm
Danh sách liên kết đơn chỉ cho phép truy cập tuần tự đến từng phần tử
nên chỉ có thể áp dụng thuật toán tìm kiếm tuần tự để tìm xem một phần tử
có giá trị k có tồn tại trong danh sách hay không.
Để duyệt danh sách ta sử dụng một con trỏ p, ban đầu con trỏ p trỏ đến
đầu danh sách, sau đó con trỏ p lần lượt duyệt qua từng phần tử của danh
sách. Trong quá trình duyệt so sánh giá trị của mỗi phần tử với giá trị cần
tìm k, nếu không bằng thì đi tiếp đến hết ngược lại trả về địa chỉ nút tìm
thấy.
*Các bước thuật toán:
• Bước 1: p = Q.Head; //p trỏ từ đầu danh sách
• Bước 2: Kiểm tra danh sách còn phần tử và nếu chưa tìm thấy phần
tử
Lặp trong khi (p!=NULL) và (p->Infor !=k) thì
p = p -> Next;
• Bước 3:
74
Cấu trúc dữ liệu và giải thuật
75
Cấu trúc dữ liệu và giải thuật
Với thuật toán sắp xếp bằng cách thay đổi mối liên kết của các phần tử,
ta sẽ xây dựng một danh sách mới được sắp từ việc tìm ra các phần tử có
giá trị lớn nhất/nhỏ nhất trong dãy ban đầu rồi đưa vào danh sách mới này.
Các bước của thuật toán có thể mô tả như sau:
- Bước 1: Khởi tạo danh sách mới Result là rỗng;
- Bước 2: Tìm trong danh sách cũ Q(Head, Tail) phần tử min là phần tử
nhỏ nhất;
- Bước 3: Tách min khỏi danh sách cũ Q(Head, Tail);
- Bước 4: Chèn min vào cuối danh sách Result;
- Bước 5: Lặp lại bước 2 khi chưa hết danh sách cũ Q(Head, Tail);
2.3.3 So sánh cài đặt danh sách theo hai phương pháp
Như vậy chúng ta đã trình bày hai phương pháp cài đặt danh sách: bởi
mảng và bởi danh sách liên kết. Vậy trong 2 phương pháp này phương
pháp nào tốt hơn? Chỉ có thể nói rằng mỗi phương pháp đều có những ưu
điểm và hạn chế, việc lựa chọn cài đặt bằng phương pháp nào tùy thuộc
vào bài toán áp dụng.
- Mảng có thể được truy cập ngẫu nhiên thông qua chỉ số, còn danh sách
chỉ có thể truy cập tuần tự. Trong danh sách liên kết, muốn truy cập tới một
phần từ phải bắt đầu từ đầu danh sách sau đó lần lượt qua các phần tử kế
tiếp cho tới khi đến phần tử cần truy cập.
76
Cấu trúc dữ liệu và giải thuật
- Việc sắp đặt lại trình tự các phần tử trong một danh sách liên kết đơn
giản hơn nhiều so với mảng. Bởi vì đối với danh sách liên kết, để thay đổi
vị trí của một phần tử, ta chỉ cần thay đổi các liên kết của một số phần tử có
liên quan, còn trong mảng, ta thường phải thay đổi vị trí của rất nhiều phần
tử.
- Do bản chất động của danh sách liên kết, kích thước của danh sách liên
kết có thể linh hoạt hơn nhiều so với mảng. Kích thước của danh sách
không cần phải khai báo trước, bất kỳ lúc nào có thể tạo mới một phần tử
và thêm vào vị trí bất kỳ trong danh sách. Nói cách khác, mảng là một tập
có số lượng cố định các phần tử, còn danh sách liên kết là một tập có số
lượng phần tử không cố định.
- Với cài đặt bằng mảng, các phép toán truy cập đến mỗi phần tử được
thực hiện với thời gian hằng, các phép bổ sung và loại bỏ đòi hỏi thời gian
tỉ lệ với độ dài của danh sách. Trong khi đó với danh sách liên kết các phép
bổ sung và loại bỏ lại thực hiện với thời gian hằng, còn các phép xử lý khác
lại thực hiện với thời gian tuyến tính. Do đó, dựa vào bài toán cần xem
phép xử lý nào được sử dụng nhiều nhất để lựa chọn phương pháp biểu
diễn cho thích hợp.
2.3.4 Các dạng danh sách liên kết khác
a. Danh sách vòng tròn
Danh sách liên kết vòng là danh sách mà con trỏ của phần tử cuối cùng
của danh sách không trỏ tới NULL mà trỏ đến phần tử đầu tiên của danh
sách, tạo thành một vòng tròn.
77
Cấu trúc dữ liệu và giải thuật
Từ một nút bất kỳ, ta có thể tiến hành duyệt qua toàn bộ các phần tử của
danh sách mà không cần trở về nút đầu tiên như trong danh sách liên kết
thông thường.
Tuy nhiên, nhược điểm của danh sách loại này là có thể không biết khi
nào thì đã duyệt qua toàn bộ phần tử của danh sách. Điều này dẫn đến 1
quá trình duyệt vô hạn, không có điểm dừng.
Để khắc phục nhược điểm này, trong quá trình duyệt luôn phải kiểm tra
xem đã trở về nút ban đầu hay chưa. Việc kiểm tra này có thể dựa trên giá
trị phần tử hoặc bằng cách thêm vào 1 nút đặc biệt.
b. Danh sách liên kết đôi
Danh sách liên kết đôi là danh sách mà mỗi nút được nối với nhau theo
hai chiều. Mỗi nút là một cấu trúc gồm 3 trường:
- Trường thứ nhất lưu giá trị dữ liệu của nút đó – Infor
- Trường thứ hai là một con trỏ, trỏ đến phần tử kế tiếp - Next
- Trường thứ ba là một con trỏ, trỏ đến phần tử liền trước - prev
78
Cấu trúc dữ liệu và giải thuật
79
Cấu trúc dữ liệu và giải thuật
80
Cấu trúc dữ liệu và giải thuật
81
Cấu trúc dữ liệu và giải thuật
82
Cấu trúc dữ liệu và giải thuật
int Key;
Node *Next;
};
Trong đó, khai báo Node *Next; dùng để mô tả
A. Con trỏ tới địa chỉ vùng nhớ của phần tử trước đó trong danh sách
liên kết đơn,
B. Con trỏ trỏ tới phần dữ liệu,
C. Vùng liên kết quản lý địa chỉ phần tử kế tiếp,
D. Con trỏ tới địa chỉ vùng nhớ của phần tử đầu tiên trong danh sách liên
kết đơn.
Câu 3. Đoạn mã để tạo ra nút mới có thành phần là x trong danh sách
liên kết đơn với mỗi nút gồm hai thành phần (Infor, Next) sau:
Node* get_Node( Data x ){
Node *p;
……………………..
if ( p == NULL )
{
printf("Ko du bo nho");
exit(1);
}
p -> Infor = x;
p -> Next = NULL;
return p;
}
Điền phần còn thiếu vào chỗ …………..
A. p = (Node*)malloc(Node));
B. p = (Node*)malloc(sizeof(Node));
C. p = malloc(sizeof(Node));
83
Cấu trúc dữ liệu và giải thuật
D. p = malloc(Node);
Câu 4. Các trường hợp thực hiện hủy phần tử khỏi danh sách liên kết
đơn gồm:
A. Hủy phần tử đầu danh sách và hủy phần tử đứng sau phần tử q,
B. Hủy phần tử có giá trị xác định k và hủy phần tử đứng sau phần tử q,
C. Hủy phần tử đầu danh sách, hủy phần tử đứng sau phần tử q và hủy
phần tử có giá trị xác định k,
D. Hủy phần tử đầu danh sách và hủy phần tử có giá trị xác định k.
Câu 5. Để tiến hành tìm kiếm một phần tử trong danh sách liên kết đơn
sử dụng phương pháp tìm kiếm gì?
A. Tìm kiếm tuyến tính và tìm kiếm nhị phân,
B. Tìm kiếm tuyến tính,
C. Tìm kiếm nhị phân ,
D. Cả ba phát biểu đều đúng.
Câu 6. Đoạn mã cài đặt chèn thêm một phần tử mới vào đầu của danh
sách liên kết đơn:
void insertFirst ( LIST &Q, Node *new_element )
{
if ( Q.Head == NULL ) //nếu danh sách rỗng
{
Q.Head = new_element;
Q.Tail = Q.Head;
}
else //danh sách không rỗng
{
[1] ……………
[2] ……………
}
84
Cấu trúc dữ liệu và giải thuật
}
Đoạn mã còn thiếu để đặt vào dòn số [1] và [2].
A. Q.Head = new_element;
new_element -> Next = Q.Head;
B. new_element -> Next = Q.Head;
Q.Head = new_element;
C. new_element -> Next = NULL;
Q.Head -> Next = new_element;
D. new_element -> Next = Q.Head;
Q.Head -> Next = new_element;
Câu 7. Tổ chức của danh sách liên kết kép gồm có mấy thành phần:
A. 4 thành phần,
B. 5 thành phần,
C. 2 thành phần,
D. 3 thành phần.
Câu 8. Để sắp xếp các phần tử của danh sách liên kết đơn có mấy
phương án sử dụng:
A. 4 phương án,
B. 3 phương án,
C. 2 phương án,
D. 5 phương án.
Câu 9. Tổ chức cấu trúc dữ liệu cho danh sách liên kết đơn:
struct OneNode {
int Data;
Node *Link;
};
OneNode *SLLPointer;
85
Cấu trúc dữ liệu và giải thuật
Mã giả thuật toán thêm một phần tử có giá trị thành phần là NewData
vào trong danh sách liên kết đơn SLList vào ngay sau nút có địa chỉ
InsNode:
B1: NewNode = new OneNode
B2: if (NewNode = NULL)
Thực hiện BKT
B3: NewNode -> Link = NULL
B4: NewNode -> Data = NewData
B5: if (InsNode -> Link = NULL)
B5.1: InsNode -> Link = NewNode
B5.2: Thực hiện BKT
//Nối các nút kế sau InsNode vào sau NewNode
B6: ……………………………………………
//Chuyển mối liên kết giữa InsNode với nút kế của nó về NewNode
B7: ……………………………………………
BKT: Kết thúc
B6 và B7 dùng để nối nút kế sau InsNode vào sau NewNode và chuyển
mối liên kết giữa InsNode với nút kế nó về NewNode. Hãy chọn câu đúng
nhất cho B6 và B7
A. B6: InsNode -> Link = NewNode -> Link
B7: InsNode -> Link = NewNode
B. B6: NewNode -> Link = InsNode -> Link
B7: NewNode = InsNode -> Link
C. B6: NewNode -> Link = InsNode -> Link
B7: InsNode -> Link = NewNode
D. B6: InsNode -> Link = NewNode -> Link
B7: NewNode = InsNode -> Link
Câu 10. Danh sách được cài đặt bằng cách nào:
86
Cấu trúc dữ liệu và giải thuật
87
Cấu trúc dữ liệu và giải thuật
A. Để lưu trữ địa chỉ của nút kế tiếp hoặc giá trị NULL nếu không liên
kết đến phần tử nào,
B. Cả hai phát biểu trên đều đúng,
C. Cả hai phát biểu trên đều sai,
D. Để lưu trữ hay mô tả thông tin được lưu trữ trong nút của danh sách.
Câu 13. Đoạn mã cài đặt hủy bỏ một phần tử đứng sau một phần tử q
trong danh sách liên kết đơn:
void RemoveAfter ( LIST &Q , Node *q ){
Node *p;
if (q != NULL)
{
p = q -> Next;
if (p != NULL)
{
if (p == Q.Tail)
{
q->Next = NULL;
Q.Tail = q;
}
[1] ………………….
free(p);
}
}
else RemoveHead(Q);
}
Dòng lệnh cần thiết được đặt vào chỗ trống tại dòng số [1]:
A. q = p;
88
Cấu trúc dữ liệu và giải thuật
89
Cấu trúc dữ liệu và giải thuật
Câu 17. Để chèn thêm một phần tử mới vào danh sách liên kết đơn có
mấy trường hợp:
A. 6 trường hợp,
B. 4 trường hợp,
C. 5 trường hợp,
D. 3 trường hợp.
Câu 18. Đoạn mã cài đặt hủy phần tử đầu của danh sách liên kết đơn:
void RemoveHead ( LIST &Q ){
Node *p;
if (Q.Head != NULL)
{
p = Q.Head;
[1] …………………..
free(p);
if ( Q.Head == NULL )
Q.Tail = NULL;
}
}
Dòng lệnh cần thiết được đặt vào chỗ trống tại dòng số [1]:
A. Q.Head -> Next = Q.Head;
B. Q.Head = NULL;
C. Q.Head -> Next = NULL;
D. Q.Head = Q.Head -> Next;
Câu 19. Các thao tác cơ bản trên danh sách gồm thao tác gì:
A. Tách, ghép, …
B. Tất cả các thao tác trên,
C. Tìm kiếm, sắp xếp, sao chép,
90
Cấu trúc dữ liệu và giải thuật
91
Cấu trúc dữ liệu và giải thuật
Cấu trúc dữ liệu quản lý hàng đợi bằng hai phần tử đầu (Font) và cuối
(Rear):
struct QQUEUE {
QOneElement Font;
QOneElement Rear;
};
SQUEUE SQList;
Thêm phần tử vào sau phần tử Rear. Giả sử dữ liệu vào hàng đợi là
NewData, mã giả được mô tả như sau, Chọn câu đúng nhất cho B4, B5
B1: NewElement = Khởi tạo nút mới có thành phần NewData
B2: if (NewElement == NULL)
Thực hiện BKT;
B3: if (SQList.Font == NULL) //hàng đợi đang rỗng
B3.1. SQList.Font = SQList.Rear = NewElement
B3.2. Thực hiện BKT
B4: …………………………….
B5: …………………………….
BKT: Kết thúc
A. B4: NewElement = SQList.Rear -> Next
B5: SQList.Rear = NewElement
B. B4: NewElement = SQList.Font -> Next
B5: SQList.Font = NewElement
C. B4: SQList.Font -> Next = NewElement
B5: SQList.Font = NewElement
D. B4: SQList.Rear -> Next = NewElement
B5: SQList.Rear = NewElement
Câu 22. Đoạn mã cài đặt chèn thêm một phần tử mới vào đầu của danh
sách liên kết đơn:
92
Cấu trúc dữ liệu và giải thuật
93
Cấu trúc dữ liệu và giải thuật
Câu 24. Đoạn mã chèn thêm một phần tử mới vào sau phần tử q trong
danh sách liên kết đơn:
void InsertAfter( LIST &Q, Node *q, Node *new_element ){
if( q != NULL)
{
[1] ………….
[2] ………….
if (q == Q.Tail)
Q.Tail = new_element;
}
}
Đoạn mã còn thiếu để đặt vào dòng số [1] và [2]
A. q -> Next = new_element;
new_element -> Next = NULL;
B. new_element -> Next = q -> Next;
q -> Next = NULL;
C. new_element -> Next = q -> Next;
q -> Next = new_element;
D. q -> Next = new_element;
new_element -> Next = q -> Next;
Câu 25. Các thành phần của danh sách gồm:
A. Dữ liệu (data) và liên kết (link),
B. Số phần tử của danh sách (number),
C. Liên kết (link),
D. Dữ liệu (data) .
94
Cấu trúc dữ liệu và giải thuật
3 D 6 M 9 T 12
-5 8 9 1 6 7 -3
Câu 5. Trình bày ý tưởng, mô tả từng bước thuật toán và cài đặt giải
thuật chọn trực tiếp (SelectionSort) tăng dần, giảm dần cho một mảng một
chiều có các phần tử thuộc loại nguyên và minh hoạ các bước sắp xếp dãy
số nguyên dưới đây theo thứ tự tăng dần bằng phương pháp sắp xếp chèn
trực tiếp (SelectionSort).
-5 8 9 1 6 7 -3
Câu 6. Trình bày ý tưởng, mô tả từng bước thuật toán và viết hàm(hoặc
thủ tục) của thuật toán sắp xếp mảng một chiều gồm N số nguyên tăng dần,
giảm dần bằng phương pháp sắp xếp nổi bọt (BubbleSort). Đánh giá độ
phức tạp của thuật toán. Thực hiện chạy từng bước khi sắp xếp dãy sau
tăng dần, giảm dần theo giải thuật sắp xếp nổi bọt (BubbleSort)
-5 8 9 1 6 7 -3
95
Cấu trúc dữ liệu và giải thuật
Câu 7. Trình bày ý tưởng, mô tả từng bước thuật toán và cài đặt giải
thuật đổi chỗ trực tiếp (Interchange Sort) tăng dần, giảm dần cho một mảng
một chiều nguyên. Minh hoạ các bước sắp xếp dãy số nguyên dưới đây
theo thứ tự tăng dần bằng phương pháp sắp xếp đổi chỗ trực tiếp
(Interchange Sort).
-5 8 9 1 6 7 -3
Câu 8. Trình bày ý tưởng, mô tả từng bước thuật toán và cài đặt giải
thuật QuickSort tăng dần, giảm dần cho một mảng một chiều nguyên. Minh
họa các bước sắp xếp dãy số nguyên dưới đây theo thứ tự tăng dần bằng
phương pháp sắp xếp QuickSort
-5 8 9 1 6 7 -3
Câu 9. Thông tin về sinh viên gồm: mã số sinh viên (MaSV), họ tên
(HoTen), ngày sinh (NS), điểm toán, điểm văn, điểm tổng kết (ĐTK =
(Toán + Văn)/2 ). Thực hiện các yêu cầu sau (bằng ngôn ngữ lập trình C):
a. Khai báo cấu trúc danh sách liên kết đơn để quản lý danh sách sinh
viên. (Kiểu danh sách là ListSV lưu các sinh viên có kiểu là SV),
b. Viết hàm in ra màn hình thông tin của các sinh viên xếp loại giỏi (là
những sinh viên có điểm tổng kết ≥5.0),
c. Viết hàm kiểm tra sinh viên có mã sinh viên là k có trong danh sách
hay không?
d. Xây dựng hàm đếm các sinh viên có điểm tổng kết loại khá
(8>ĐTB≥7),
e. Viết hàm in đầy đủ các thông tin của cả danh sách,
f. Viết hàm liệt kê các sinh viên sinh vào tháng 10,
g. Liệt kê các sinh viên bị thi lại (có điểm một trong hai môn dưới 5),
h. Xây dựng hàm đếm các sinh viên bị học lại (có điểm của cả hai môn
dưới 4 điểm).
96
Cấu trúc dữ liệu và giải thuật
97
Cấu trúc dữ liệu và giải thuật
98
Cấu trúc dữ liệu và giải thuật
99
Cấu trúc dữ liệu và giải thuật
100
Cấu trúc dữ liệu và giải thuật
Nhược điểm của phương pháp cài đặt bằng mảng là phải xác định được
số phần tử tối đa mà ngăn xếp sẽ lưu trữ, điều này không phải lúc nào cũng
làm được nếu ta chọn quá lớn thì sẽ lãng phí bộ nhớ còn nếu thiếu thì
chương trình không chạy được. Để hạn chế nhược điểm này người ta sử
dụng phương pháp cài đặt bằng danh sách liên kết.
3.1.3 Cài đặt bằng danh sách liên kết
Sử dụng danh sách liên kết để cài đặt ta dùng một danh sách liên kết
đơn, trong đó phần tử đầu danh sách là đỉnh, phần tử cuối danh sách là đáy
danh sách. Với cách cài đặt này kích thước của ngăn xếp là vô hạn trừ khi
không còn bộ nhớ để cấp phát lưu trữ.
Hình 3.1 Minh họa sử dụng danh sách liên kết cài đặt cho ngăn xếp
Khai báo cấu trúc ngăn xếp bằng danh sách liên kết như sau:
struct Node
{
data Infor;
struct Node *Next;
};
struct Stack
{
101
Cấu trúc dữ liệu và giải thuật
Node top;
};
Các thao tác được cài đặt như sau:
*Khởi tạo danh sách rỗng
Hình 3.2 Minh họa thao tác Push để bổ sung phần tử vào Stack
102
Cấu trúc dữ liệu và giải thuật
Hình 3.3 Minh họa thao tác Pop để lấy phần tử khỏi Stack
103
Cấu trúc dữ liệu và giải thuật
- Duyệt từ đầu xâu đến cuối xâu. Lần lượt cho các ký tự duyệt được vào
ngăn xếp, cho hết các ký tự vào ngăn xếp
- Lần lượt lấy các phần tử từ ngăn xếp và in ra cho đến khi ngăn xếp
rỗng
Cài đặt:
104
Cấu trúc dữ liệu và giải thuật
điểm đầu và điểm cuối này. Để lấy ra một phần tử của hàng đợi thì điểm
đầu tăng lên 1. Để bổ sung 1 phần tử vào hàng đợi thì điểm cuối tăng lên 1.
Khai báo cấu trúc một hàng đợi:
#define MAX 100
struct Queue
{
int Head, Tail, Count;
Data Node[MAX];
};
*Khởi tạo hàng đợi rỗng
107
Cấu trúc dữ liệu và giải thuật
3.2.3 Cài đặt hàng đợi bằng danh sách liên kết
Để cài đặt hàng đợi bằng danh sách liên kết, ta cũng sử dụng 1 danh sách
liên kết đơn và 2 con trỏ Head và Tail lưu giữ nút đầu và nút cuối của danh
sách. Việc bổ sung phần tử mới sẽ được tiến hành ở cuối danh sách và việc
lấy phần tử ra sẽ được tiến hành ở đầu danh sách.
Khai báo cấu trúc hàng đợi bằng danh sách liên kết như sau:
struct Node
{
Data item;
struct Node *Next;
};
struct Queue
{
Node Head;
Node Tail;
};
Các thao tác trên hàng đợi được cài đặt như sau:
*Thao tác khởi tạo hàng đợi
Thao tác này thực hiện việc gán giá trị NULL cho nút đầu và cuối của
hàng đợi, cho biết hàng đợi đang ở trạng thái rỗng.
108
Cấu trúc dữ liệu và giải thuật
Để thêm phần tử vào cuối hàng đợi, tạo và cấp phát bộ nhớ cho 1 nút
mới. Gán giá trị thích hợp cho nút này, sau đó cho con trỏ tiếp của nút cuối
hàng đợi trỏ đến nó. Nút này bây giờ trở thành nút cuối của hàng đợi. Nếu
hàng đợi chưa có phần tử nào thì nó cũng chính là nút đầu của hàng đợi.
*Lấy phần tử ra khỏi hàng đợi
Để lấy phần tử ra khỏi hàng đợi, tiến hành lấy phần tử tại vị trí nút đầu
và cho nút đầu chuyển về nút kế tiếp. Tuy nhiên, trước khi làm các thao tác
này, ta phải kiểm tra xem hàng đợi có rỗng hay không.
109
Cấu trúc dữ liệu và giải thuật
P ( x ) = a0 + a1x + a2 x 2 + ... + an x n
Để biểu diễn đa thức, đơn giản nhất là dùng mảng a[i] cất giữ hệ số của
i
x . Các phép toán với các đa thức như: cộng hai đa thức, nhân hai đa thức,
… Khi các đa thức được biểu diễn bởi mảng, có thể cài đặt một cách đơn
giản.
Tuy nhiên, khi đa thức với nhiều hệ số bằng 0, cách biểu diễn dưới dạng
mảng là tốn kém bộ nhớ. Chẳng hạn, việc biểu diễn đa thức x1000 – 1, đòi
hỏi mảng chứa 1001 phần tử.
Biểu diễn đa thức bằng danh sách liên kết, ta chỉ xây dựng danh sách các
hệ số khác 0 cùng số mũ tương ứng. Tuy nhiên, việc cài đặt các phép toán
lại phức tập hơn.
Ví dụ 3.4 Bài toán cộng hai đa thức
110
Cấu trúc dữ liệu và giải thuật
Ta có thể sử dụng hàng đợi để biểu diễn đa thức khi đó mỗi phần tử của
hàng sẽ gồm 3 thành phần: hệ số, số mũ và con trỏ Next để trỏ đến phần tử
tiếp theo của hàng đợi.
Khi đó phép cộng hai đa thức được biểu diễn như hình:
111
Cấu trúc dữ liệu và giải thuật
là phần tử đã được lưu trong hàng đợi lâu nhất. Quy luật của hàng đợi là
vào trước ra trước (FIFO - First In First Out).
Hàng đợi cũng có thể được cài đặt bằng mảng hoặc danh sách liên kết.
Các thao tác cơ bản cũng bao gồm: Khởi tạo hàng đợi, kiểm tra hàng đợi
rỗng (đầy), thêm 1 phần tử vào hàng đợi, loại bỏ 1 phần tử khỏi hàng đợi.
3.4 Bài tập
Bài 1. Cho cấu trúc dữ liệu về hàng đợi (Queue) dưới đây:
struct KhachHang {
char maKH[10], tenKH[10];
float soluong;
};
struct item {
KhachHang Infor;
};
item Queue[100];
int Head;
1. Viết hàm thêm một khách hàng x vào hàng đợi Queue.
2. Viết hàm loại bỏ một khách hàng ra khỏi hàng đợi Queue.
3. Viết hàm cho biết thông tin của các khách hàng trong hàng đợi Queue.
Bài 2. Cho ngăn xếp (Stack) có cấu trúc dữ liệu dưới đây:
Struct item {
int Infor;
};
item Stack[100];
int sp;
1. Viết hàm thêm một phần tử mới x vào ngăn xếp Stack.
2. Viết hàm lấy một phần tử ra khỏi ngăn xếp Stack.
112
Cấu trúc dữ liệu và giải thuật
3. Viết hàm chuyển đổi một số nguyên a ở hệ đếm cơ số mười thành một
số ở hệ đếm cơ số hai.
113
Cấu trúc dữ liệu và giải thuật
Một hàm băm tốt là một hàm băm thoả mãn ba điều kiện:
% Tính toán nhanh.
% Các khoá sau khi thực hiện băm được phân bố đều trong bảng.
% Ít xảy ra đụng độ giữa các giá trị khoá sau khi thực hiện băm.
% Xử lý được các loại khoá có kiểu dữ liệu khác nhau.
Khoá để thực hiện băm có thể là dạng số hoặc dạng chuỗi. Trong trường
hợp khoá là dạng chuỗi thì thực hiện biến đổi chuỗi thành các số nguyên
tương ứng trước khi thực hiện băm.
4.2 Các loại hàm băm
4.2.1 Hàm băm sử dụng phương pháp chia
- Dùng số dư: h(k) = k mod m = k % m
114
Cấu trúc dữ liệu và giải thuật
- Trong đó:
+ k là khóa để thực hiện mã hoá
+ m là kích thước của bảng để thực hiện mã hoá, m thường là nguyên tố
gần nhất với số địa chỉ muốn lưu. Giá trị của m phải được lựa chọn sao cho
hạn chế ảnh hưởng đến h(k). Ví dụ: m không nên là bội của 2; m không nên
là bội của 10; …
- Ví dụ: Cho các khóa: 5, 7, 25, 49, 64, 19, 45. Hãy băm các khóa trên
bằng phương pháp chia với số địa chỉ m = 10 và chỉ ra sự xung đột.
5 5 5
7 7 7
49 9 9
64 4 4
115
Cấu trúc dữ liệu và giải thuật
5 5 5
7 9 9
49 1 1
64 6 6
116
Cấu trúc dữ liệu và giải thuật
- Ví dụ: Cho gia trị khoá k = 7985241, thực hiện băm bằng cách gập 3
phần tử
=> Thực hiện gập 3 ta được các số: 798, 425 và 001. Tính tổng các số ta
thu được giá trị: 1224. Vậy kết quả trích lấy 3 phần tử đầu là 122 và trích
lấy 3 giá trị cuối là 224.
4.3 Bảng băm
4.3.1 Giới thiệu
Giả sử ta có:
- K: là tập các giá trị khoa cần thực hiện băm
- M: tập các địa chỉ có để thực hiện lưu kết quả sau khi băm
- HF(k): là bảng băm dùng để ánh xạ tập giá trị các khoá K thành các địa
chị tương ứng trong tập M
117
Cấu trúc dữ liệu và giải thuật
*Thêm mới phần tử (Insert): Thêm một phần tử vào bảng băm. Sau
khi thêm số phần tử hiện có của bảng băm tăng thêm một đơn vị.
*Loại bỏ (Remove): Loại bỏ một phần tử ra khỏi bảng băm, và số phần
tử sẽ giảm đi một.
*Sao chép (Copy): Tạo một bảng băm mới tử một bảng băm cũ đã có.
*Duyệt (Traverse): duyệt bảng băm theo thứ tự địa chỉ từ nhỏ đến
lớn.
4.3.2 Các bảng băm cơ bản
*Bảng băm với phương pháp kết nối trực tiếp: là bảng băm mà tại
mỗi địa chỉ của nó chứa một thùng các khóa, các khóa trong một thùng
khóa liên kết với nhau dưới dạng danh sách liên kết đơn. Vì vậy, các khóa
có cùng địa chỉ được lưu dưới cùng một danh sách
Bảng băm với phương pháp kết nối trực tiếp thường được cài đặt bằng
danh sách liên kết, các phần tử trên bảng băm được tạo thành từ m danh
sách liên kết tương ứng với địa chỉ từ 0 đến m-1. Các phần tử xung đột
được lưu trữ trên cũng một danh sách liên kết tại cùng một địa chỉ.
Trong bảng băm với phương pháp kết nối trực tiếp việc tìm kiếm phần tử
có giá trị k thực chất là xác định phần tử đó tại địa chỉ trong khoảng từ 0
đến m-1. Chính vì vậy trong bảng băm với phương pháp kết nối trực tiếp sử
dụng thuật toán tìm kiếm tuyến tính để tìm kiếm một phần tử trên danh
sách liên kết. Tốc độ truy xuất thực hiện tìm kiếm phụ thuộc và lựa chọn
hàm băm để thực hiện băm. Nếu giá trị m (địa chỉ) càng lớn thì tốc độ thực
hiện tìm kiếm nhanh nhưng dẫn đến không hiệu quả về bộ nhớ.
Ví dụ: Cho danh sách phần tử sau: 37, 25, 74, 67, 24, 14, 98, 27. Yêu
câu: thực hiện lưu trữ các khóa trên vào bảng băm bằng phương pháp kết
nối trực tiếp sử dụng hàm băm bằng phương pháp chia với m = 10.
& K1 = 37 => h(k1) = 37 % 10 = 7 ' Lưu tại địa chỉ 7
& K2 = 25 => h(k2) = 25 % 10 = 5 ' Lưu tại địa chỉ 5
& K3 = 74 => h(k3) = 74 % 10 = 4 ' Lưu tại địa chỉ 4
& K4 = 67 => h(k4) = 67 % 10 = 7 ' Lưu tại địa chỉ 7
118
Cấu trúc dữ liệu và giải thuật
*Bảng băm với phương pháp dò tuyến tính: là bảng băm mà khi phần
tử khoá phía sau được băm có cùng địa chỉ với phần tử khoá trước đã lưu
thì thực hiện dò đến địa chỉ còn trống để lưu trữ nó. Như vậy, số khoá cần
lưu trữ phải đảm bảo nhỏ hơn hoặc bằng địa chỉ có để lưu trữ.
Khi thực hiện dò lại địa chỉ để lưu trữ cho một phần tử khi gặp xung đột
được thực hiện bằng cách tiến hành băm lại theo nguyên tắc:
h(k)mới = ( h(k) + i ) mod m = ( h(k) + i ) % m
Trong đó:
- h(k): là giá trị sau băm bị đụng độ
- i: là số lần băm lại
- m: là số địa chỉ cần lưu trữ
Ví dụ: Cho các khóa sau: 12, 94, 52, 76, 43, 79, 19. Yêu cầu: hãy lưu trữ
các khóa trên bằng phương pháp dò tuyến tính với hàm băm bằng phương
pháp chia m = 10.
& K1 = 12 => h(k1) = 12 % 10 = 2 ' Lưu trữ tại địa chỉ 2
119
Cấu trúc dữ liệu và giải thuật
*Bảng băm với phương pháp dò bậc 2: là bảng băm mà khi phần tử
khoá phía sau được băm có cùng địa chỉ với phần tử đã được lưu trữ thì ta
thực hiện dò để tìm địa chỉ mới theo nguyên tắc sau:
h(k)mới = ( h(k) + i2 ) mod m = ( h(k) + i2 ) % m
Trong đó:
- h(k): là giá trị sau băm bị đụng độ
- i: là số lần băm lại
120
Cấu trúc dữ liệu và giải thuật
121
Cấu trúc dữ liệu và giải thuật
*Bảng băm với phương pháp dò hằng số: là bảng băm mà khi phần tử
khoá phía sau được băm có cùng địa chỉ với phần tử đã được lưu trữ thì ta
thực hiện dò để tìm địa chỉ mới theo nguyên tắc sau:
h(k)mới = ( h(k) + c*i ) % m
Trong đó:
- h(k): là giá trị sau băm bị đụng độ
- i: là số lần băm lại
- c: là hằng số
- m: là số địa chỉ cần lưu trữ
Ví dụ: cho các khóa sau: 47, 28, 15, 52, 49, 6, 72, 94, 14. Hãy lưu trữ các
khóa trên vào bảng băm bằng phương pháp dò hằng số với c = 3 và dùng
hàm băm bằng phương pháp chia m = 11.
& K1=47 => h(k1) = 47%11 = 3 ' Lưu trữ tại địa chỉ 3
& K2=28 => h(k2) = 28%11 = 6 ' Lưu trữ tại địa chỉ 6
& K3=15 => h(k3) = 15%11 = 4 ' Lưu trữ tại địa chỉ 4
& K4=52 => h(k4) = 52%11 = 8 ' Lưu trữ tại địa chỉ 8
& K5=49 => h(k5) = 49%11 = 5 ' Lưu trữ tại địa chỉ 5
& K6=6 => h(k6) = 6%11 = 6 ' Xung đột K2
– Băm lại lần 1: h(k6) = (6 + 3*1)%11 = 9 => Lưu trữ tại địa chỉ 9
& K7=72 => h(k7) = 72%11 = 6 ' Xung đột K2
– Băm lại lần 1: h(k7) = (6+3*1)%11 = 9 => Xung đột K6
– Băm lại lần 2: h(k7) = (6+3*2)%11=1 => Lưu trữ tại địa chỉ 1
& K8=94 => h(k8) = 94%11 = 6 ' xung đột k2
– Băm lại lần 1: h(k8) = (6+3*1)%11 = 9 ' Xung đột K6
– Băm lại lần 2: h(k8) = (6+3*2)%11 = 1 ' Xung đột K7
– Băm lại lần 3: h(k8) = (6+3*3)%11 = 4 ' Xung đột K2
– Băm lại lần 4: h(k8) = (6+3*4)%11 = 7 ' Lưu trữ tại địa chỉ 7
& K9=14 => h(k2) = 14%11 = 3 ' Xung đột K1
122
Cấu trúc dữ liệu và giải thuật
123
Cấu trúc dữ liệu và giải thuật
*Băm trong định danh dữ liệu: Sử dụng giá trị băm để định dạng một
tập tin nào đó trong lưu trữ dữ liệu cũng được coi là một giải pháp đáng tin
cậy. Hiện nay một số hệ thống quản lý mã nguồn, sử dụng dụng gía trị
sha1sum (giá trị được sinh ra của một chương trình máy tính để xác định
định danh của một file tài nguyên) của nội dung tệp dữ liệu, cây thư mục,
thông tin thư mục gốc, … trong mục đích định dạng tệp tài nguyên đó. Giá
trị sha1sum cũng được sử dụng để xác định các tệp trên các mạng chia sẻ
ngan hàng nhằm cung cấp đầy đủ thông tin cho phép định vụ nguồn gốc
của tệp, đồng thời cho phép xác minh nội dung tệp tải xuống.
124
Cấu trúc dữ liệu và giải thuật
125
Cấu trúc dữ liệu và giải thuật
126
Cấu trúc dữ liệu và giải thuật
A. 7
B. 10
C. 37
D. Không xác định được
Câu 6. Cho danh sách các khóa sau: 12, 94, 52, 76, 43. Cho biết địa chỉ
trong bảng băm để lưu phần tử 52 bằng phương pháp dò tuyến tính với hàm
băm bằng phương pháp chia m = 10;
A. 2
B. 4
C. 3
D. 6
Câu 7: Cho danh sách các khoá: 47, 28, 15, 52, 49, 6, 72, 94, 14. Cho
biết vị trí để lưu trữ phần tử 6 vào bảng băm bằng phương pháp dò bậc 2
với hàm băm sử dụng phương pháp chia m=10?
A. 5
B. 6
C. 4
D. 7
Câu 8: Cho danh sách các khoá: 47, 28, 15, 52, 49, 6, 72, 94, 14. Cho
biết vị trí để lưu trữ phần tử 6 vào bảng băm bằng phương pháp dò hằng số
với c = 3 và dùng hàm băm phương pháp chia m = 11?
A. 9
B. 6
C. 3
D. 4
4.7 Bài tập áp dụng
Bài 1. Cho danh sách phần tử sau: 39, 23, 76, 65, 26, 12, 96, 29. Yêu
cầu: thực hiện lưu trữ các khóa trên vào bảng băm bằng phương pháp kết
nối trực tiếp sử dụng hàm băm bằng phương pháp chia với m = 10.
127
Cấu trúc dữ liệu và giải thuật
Bài 2. Cho các khóa sau: 10, 96, 50, 78, 41, 81, 17. Yêu cầu: thực hiện
lưu trữ các khóa trên bằng phương pháp dò tuyến tính với hàm băm bằng
phương pháp chia m = 10.
Bài 3. Cho các khóa sau: 5, 28, 19, 15, 20, 33, 12, 17, 10. Hãy lưu trữ
các khóa trên vào bảng băm bằng phương pháp dò bậc 2 với hàm băm bằng
phương pháp chia m = 10.
Bài 4. Cho các khóa sau: 49, 26, 17, 50, 51, 8, 70, 96, 12. Hãy lưu trữ
các khóa trên vào bảng băm bằng phương pháp dò hằng số với c = 3 và
dùng hàm băm bằng phương pháp chia m = 11.
Bài 5: Viết chương trình tạo ra 10 giá trị ngẫu nhiên trong phạm vi từ 1
đến 999. Tạo bảng băm chứa các giá trị bằng phương pháp kết nối trực tiếp
sử dụng hàm băm bằng phương pháp chia với m = 10.
128
Cấu trúc dữ liệu và giải thuật
129
Cấu trúc dữ liệu và giải thuật
130
Cấu trúc dữ liệu và giải thuật
- Nút 1 là nút gốc của toàn bộ cây, là nút cha của nút 2, 3, 4
- Nút 2 là nút gốc của nhánh có 3 phần tử: 2, 5, 6
- Nút 4 là nút gốc của nhánh có các phần tử: 4, 7, 8, 9, 10, 11
Cấp (Bậc) của nút: Số các nút con của một nút được gọi là cấp của nút
đo. Ví dụ nút 1 có cấp là 3, nút 2 có cấp là 2, nút 3 có cấp là 0, …
131
Cấu trúc dữ liệu và giải thuật
Nút lá (Leaf): Nút có bậc bằng 0 (Không có bất kỳ nhánh con nào) được
gọi là nút lá hay nút tận cùng.
Xét cây dưới:
132
Cấu trúc dữ liệu và giải thuật
133
Cấu trúc dữ liệu và giải thuật
- Hàm PARENT(n,T): cho biết nút cha của nút n trên cây T, nếu n là
nút gốc thì cho giá trị là NULL,
- Hàm LeftMost_Child(n,T): cho nút con trái nhất của nút n trên
cây T, nếu n là nút lá thì cho giá trị là NULL,
- Hàm Right_SibLing(n,T): cho nút anh em ruột bên phải của
nút n trên cây T, nếu n là lá thì cho giá trị NULL,
- Hàm Label_Node(n,T): cho nhãn tại nút n của cây T,
- Hàm Root(T): trả ra nút gốc của cây T. Nếu cây T rỗng thì hàm trả
về giá trị NULL.
5.2 Cài đặt cây
5.2.1 Cài đặt cây bằng danh sách kế tiếp – mảng các nút cha
Giả sử ta cần cài đặt một cây có n nút với các nút nhận giá trị lần lượt là
1, 2, 3, …, n. Khi đó để biểu diễn cây bằng mảng ta sử dụng mảng A để lưu
trữ các nút cha của các nút trong cây với A[i] = j nếu j là nút cha của
nút i. Nếu i là nút gốc thì gán A[i] = 0.
Cây được biểu diễn dưới dạng mảng theo tính chất: mỗi nút trong cây
chỉ có duy nhất một nút cha. Để tìm đường đi từ một nút lên gốc, ta tìm nút
cha của nút đó, rồi tìm nút cha của nút vừa tìm được, … cho tới khi đi
ngược lên nút gốc.
Ví dụ 5.1 Cho cây như hình dưới đây
134
Cấu trúc dữ liệu và giải thuật
Trong trường hợp giá trị của các nút trong cây không giá trị theo thứ tự
1, 2, 3, … ta sẽ phải mã hóa và chuyển đổi tương ứng giá trị của các nút
thành các con số tương ứng 1, 2, 3, … sau đó mới tiền hành lưu trữ trong
mảng được. Với cây trên ta có được kết quả lưu trữ mảng như sau:
135
Cấu trúc dữ liệu và giải thuật
Với phương pháp biểu diễn theo nguyên tắc này ta có thể tìm được nút
cha của một nút trên cây, nhưng để tìm nút con của một nút là khá phức
tạp, đặc biệt muốn xác định được tất cả các con của một nút là sẽ rất phức
tạp. Đồng thời với cách biểu diễn này thì rất khó xác định được thứ tự của
các nút con của một nút nào đó.
Có một cách khác để lưu trữ cây tổng quát bằng mảng. Trước hết, ta
đánh số các nút trên cây bắt đầu từ 1 theo một thứ tự tùy ý. Để lưu trữ cây
ta dùng 3 mảng: Infor, Children và Head. Giả sử cây có n nút khi đó ta
có:
- Mảng Infor[1...n] với Infor[i] lưu giá trị tương ứng với nút
thứ i của cây,
- Mảng Children được chia làm n đoạn, đoạn thứ i gồm một dãy liên
tiếp các phần tử là chỉ số chứa giá trị các nút con của nút i. Như vậy mảng
Children sẽ chứa tất cả chỉ số của mọi nút con trên cây ngoại trừ nút gốc
nên mảng sẽ gồm n-1 phần tử. Việc chia mảng Children làm n đoạn sẽ
có những đoạn rỗng (tương ứng với danh sách nút con của nút lá),
- Mảng Head[1…n+1] để đánh dấu vị trí cắt đoạn trong mảng
Children; Head[i] chứa vị trí đầu của đoạn thứ i để chứa các phần tử
là con của nút thứ i, hay nói chính xác thì các phần tử của mảng
Children từ vị trí Head[i] đến Head[i+1]–1 là chỉ số các nút con
của nút thứ i. Khi Head[i] = Head[i+1] có nghĩa là đoạn thứ i rỗng.
Quy ước: Head[n+1] = n.
Ví dụ 5.3 Cho cây như hình dưới đây
136
Cấu trúc dữ liệu và giải thuật
Ở đây trong mảng Children để mỗi khoảng gồm tối đa 3 phần tử.
Infor 1 2 3 4 5 6 7 8 9 10 11
Chỉ số 1 4 7 1 1 1
0 3 6
Childre 2 3 4 5 6 7 8 9
n
19 22 25 28 31
10 11
Head 1 4 7 10 13 16 19 22 25 28 31
5.2.2 Lưu trữ móc nối – danh sách các nút con
Cây có thể được cài đặt để làm việc tốt hơn bằng sách tạo ra một danh
sách các nút của cây. Có nhiều cách tổ chức danh sách tuy nhiên do số nút
137
Cấu trúc dữ liệu và giải thuật
con của mỗi nút là không được xác định trước nên ta dùng danh sách liên
kết chứa các nút con để biểu diễn.
Với cách tổ chức lưu trữ dưới dạng danh sách liên kết thì mỗi danh sách
sẽ chứa lần lượt các nút con của một nút nào đó nên dễ dàng cho phép
duyệt cây và logic hơn trong việc thực hiện các thao tác trên cây. Xuất phát
từ gốc, ta tìm các nút con của nút gốc, rồi tìm nút con của các nút vừa tìm
được, … thực hiện liên tiếp cho đến khi đến nút lá.
Có thể sử dụng các danh sách liên kết khác nhau để lưu trữ. Giả sử dùng
danh sách liên kết đơn để lưu trữ thì ta có n danh sách liên kết đơn, mỗi
danh sách tương ứng với một nút trong cây, xét với cây có n đỉnh khác
nhau. Mỗi một nút của cây gồm hai thành phần: Infor và next
Ví dụ 5.4 Cho cây như hình dưới đây, biểu diễn cây dưới dạng danh
sách móc nối
138
Cấu trúc dữ liệu và giải thuật
Hình 5.3 Minh họa cây tổng quát bằng danh sách con móc nối
Khai báo cấu trúc dữ liệu dạng DSLK để lưu trữ cây:
struct NodeTree
{
int Infor;
struct NodeTree *next;
};
NodeTree Tree[n];
139
Cấu trúc dữ liệu và giải thuật
140
Cấu trúc dữ liệu và giải thuật
Cây nhị phân đầy đủ: là cây phị phân mà mỗi nút không phải nút lá đều
có đúng 2 nút con và các nút có cùng độ sâu.
- Nút thứ i (i>=1) của cây có hai nút con tương ứng là nút thứ 2i và 2i+1
(thống nhất vị trí 2i chứa nút nhánh trái và vị trí 2i+1 chứa nút nhánh
phải), điều này có nghĩa là nút cha của nút i là nút ở vị trí [i/2] trong mảng.
Ví dụ 5.5 Cho cây sau, biểu diễn cây dưới dạng mảng
Trong trường hợp tổng quát ta có thể dễ dàng đánh số cho các nút trên
cây theo thứ tự lần lượt từ 1 trở đi, hết mức này đến mức khác và từ trái
sang phải đối với các nút trong cùng mức trước khi tiến hành lưu trữ bằng
mảng.
Ví dụ 5.6 Biểu diễn cách lưu trữ cây nhị phân sau
142
Cấu trúc dữ liệu và giải thuật
Ta có kết quả lưu trữ tương ứng với cách đánh chỉ số trên như sau:
Trong trường hợp nếu cây không đầy đủ (mỗi nút không có đủ cả nút trái
và nút phải) thì ta có thể thêm vào một số nút giả để được cây nhị phân đầy
đủ và gán những giá trị đặc biệt cho các phần tử trong mảng tương ứng.
Ví dụ 5.7 Cây lệch trái
Vị trí 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
Ví dụ 5.8 Cho cây zich zắc, biểu diễn cách lưu trữ của cây tương ứng
143
Cấu trúc dữ liệu và giải thuật
Vị trí 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
Đối với cây nhị phân không cân bằng, do số nút con của một nút có thể
<2 nên dùng cách biểu diễn cây dưới dạng mảng là không thích hợp vì tốn
chi phi bộ nhớ mảng. Khi đó để giải quyết vấn đề ta dùng mảng các nút,
mỗi nút này có chứa ba thông tin:
-Infor: chứa giá trị lưu trữ tại nút
-Leftchild: chứa giá trị của nút con bên trái
-Rightchild: chứa giá trị của nút con bên phải
Ví dụ 5.9 Cho cây như sau
144
Cấu trúc dữ liệu và giải thuật
15 13 18 11 14 16 20
Khi đó, khai báo cấu trúc của một nút như sau:
struct NodeTree
{
Data Infor;
Data Leftchild;
Data Rightchild;
};
Định nghĩa cây:
Node Tree[];
145
Cấu trúc dữ liệu và giải thuật
hoặc
146
Cấu trúc dữ liệu và giải thuật
147
Cấu trúc dữ liệu và giải thuật
Hình 5.5 Kết quả chuyển từ cây tổng quát sang cây nhị phân
5.6 Các thao tác trên cây nhị phân tìm kiếm
5.6.1 Các phép duyệt cây
Phép xử lý các nút trên cây mà ta gọi chung là phép thăm (visit) các nút
một cách đầy đủ sao cho mỗi nút được thăm duy nhất một lần được gọi là
phép duyệt cây. Hiện nay có 3 phương pháp duyệt cây cơ bản áp dụng với
cây nhị phân nói chung
a. Duyệt theo thứ tự trước – preorder traversal
Trong phương pháp duyệt theo thứ tự trước thì giá trị trong mỗi nút bất
kỳ được liệt kê trước giá trị lưu trong hai nút con của nó, nguyên tắc như
sau: trước tiên thăm nút gốc, sau đó thăm tất cả các nút bên nhánh trái,
cuối cùng thăm tất cả các nút bên nhánh phải. Phương pháp này còn có
tên là phương pháp duyệt NLR(Node-Left-Right).
Thuật toán giả mã:
void DuyetTruoc( Node Root)
{
if ( Root != NULL)
148
Cấu trúc dữ liệu và giải thuật
{
<Thăm nút Root>;
DuyetTruoc( Root -> Left );
DuyetTruoc( Root -> Right );
}
}
Ví dụ 5.12 Cho cây
150
Cấu trúc dữ liệu và giải thuật
151
Cấu trúc dữ liệu và giải thuật
Hình 5.6 Minh họa tìm kiếm phần tử trên cây NPTK
Cài đặt:
Node *SearchTree ( Tree Root , Data x )
{
Node *p;
p = Root;
while( p != NULL )
{
if (x = = p -> Infor )
return p;
else if ( x < p -> Infor )
SearchTree( Root->Next, x);
else
SearchTree( Root->Right, x);
}
return NULL;
}
5.6.3 Chèn thêm phần tử vào cây
Chèn thêm phần tử vào cây là thao tác lần lượt bổ sung từng phần tử vào
cây nhị phân tìm kiếm đã có sao cho nguyên tắc của cây nhị phân tìm kiếm
vẫn được đảm bảo.
152
Cấu trúc dữ liệu và giải thuật
Ý tưởng
Để chèn thêm phần tử vào cây nhị phân tìm kiếm, đầu tiên tiến hành quá
trình tìm kiếm nút cần chèn trong cây theo như các bước của quá trình tìm
kiếm. Nếu tìm thấy nút trong cây có nghĩa là cây đã tồn tại và không tiến
hành chèn thêm nút nữa. Nếu không tìm thấy nút trong cây thì tiến hành
chèn nút vào điểm kết thúc của quá trình tìm kiếm.
Các bước bổ sung phần tử x vào cây:
% Bắt đầu từ gốc của cây,
% So sánh giá trị phần tử x với thành phần Infor của nút đang xét. Có
ba khả năng sau:
( Nếu x = = Infor thì đã có phần tử x trong cây. Vì vậy dừng
không bổ sung phần tử x vào cây nữa,
( Nếu x > Infor thì phần tử x mới sẽ được bổ sung vào nhánh
phải của nút chứa x,
( Nếu x < Infor thì phần tử x mới sẽ được bổ sung vào nhánh
trái của nút đang xét,
% Quá trình trên được thực hiện liên tiếp đến khi bổ sung thêm được
phần tử x vào cây nhị phân tìm kiếm. Phần tử x được bổ sung vào
luôn là nút lá mới của cây.
Ví dụ 5.16 Thêm phần tử 50 vào cây đã có
153
Cấu trúc dữ liệu và giải thuật
Thêm
154
Cấu trúc dữ liệu và giải thuật
p->Infor = X;
p->Left = NULL;
p->Right = NULL;
}
}
}
Ví dụ 5.17 Cho tập hợp các số nguyên: 30, 10, 20, 50, 40, 45, 25, 15.
Áp dụng giải thuật thêm một phần tử vào cây nhị phân tìm kiếm, hãy biểu
diễn hình ảnh cây nhị phân tìm kiếm khi thêm tuần tự các phần tử có các
khoá trên vào cây.
Bước 1: Phần tử đầu tiên là phần tử gốc của cây:
Bước 2: Chèn thêm 10: So sánh 10 với nút gốc 30. Do 10 nhỏ hơn 30
nên chèn thêm 10 vào cây con trái của 30
Bước 3: Chèn thêm 20: So sánh 20 với nút gốc 30. Do 20 nhỏ hơn 30
nên chèn thêm vào cây con trái. So sánh 20 với nút tiếp theo 10. Do 20 lớn
hơn 10 nên chèn vào nhánh phải của 10
155
Cấu trúc dữ liệu và giải thuật
Bước 4: Chèn thêm 50: So sánh 50 với nút gốc 30. Do 50 lớn hơn 30
nên chèn thêm vào nhánh phải của 30.
Bước 5: Chèn thêm 40: So sánh 40 với nút gốc 30. Do 40 lớn hơn 30
nên chèn thêm 40 vào nhánh phải của 30. So sánh tiếp 40 với 50, do 40 nhỏ
hơn 50 nên chèn thêm 40 vào nhanh trái của 50.
156
Cấu trúc dữ liệu và giải thuật
Bước 6: Chèn thêm 45: So sánh 45 với 30. Do 45 lớn hơn 30 nên chèn
thêm 45 vào nhánh phải của 30. So sánh tiếp 45 với 50, do 45 nhỏ hơn 50
nên chèn thêm 45 vào nhánh trái của 50. So sánh tiếp 45 với 40, do 45 lớn
hơn 40 nên chèn thêm 45 vào nhánh phải của 40.
Bước 7: Chèn thêm 25: So sánh 25 với 30, chèn thêm 25 vào bên nhánh
trái của 30. So sánh tiếp 25 với 10, chèn thêm 25 vào nhánh phải của 10. So
sánh tiếp 25 với 20, chèn thêm 25 vào nhánh phải của 20
157
Cấu trúc dữ liệu và giải thuật
Bước 8: Chèn thêm 15: So sánh 15 với 30, chèn thêm 15 vào nhánh trái
của 30. So sánh 15 với 10, chèn thêm 15 vào nhánh phải của 10. So sánh 15
với 20, chèn thêm 15 vào nhánh trái của 20
158
Cấu trúc dữ liệu và giải thuật
159
Cấu trúc dữ liệu và giải thuật
160
Cấu trúc dữ liệu và giải thuật
Sử dụng các phương pháp duyệt khác nhau trên cây biểu thức ta thu
được các dạng biểu diễn khác nhau của biểu thức, thông thường có 3 dạng
sau:
Duyệt theo thứ tự trước (NLR-Node Left Right), cách gọi khác là
biểu thức dạng tiền tố. Trong dạng biểu diễn này toán tử được viết trước
sau đó đến hai toán hạng tương ứng, còn được gọi với tên là Ký pháp
Balan.
Nguyên tắc: Phép toán – Toán hạng 1 – Toán hạng 2
Ví dụ 5.18
Cho cây biểu thức:
162
Cấu trúc dữ liệu và giải thuật
163
Cấu trúc dữ liệu và giải thuật
( (
( ( (
3 3
+ ( ( +
164
Cấu trúc dữ liệu và giải thuật
4 3, 4
) 3, 4, +
* ( *
( ( * (
2 3, 4, +, 2
- ( * ( -
6 3, 4, +, 2, 6
) 3, 4, +, 2, 6, -
) 3, 4, +, 2, 6, -, *
165
Cấu trúc dữ liệu và giải thuật
dụng: một Stack rỗng chứa tạm thời các toán hạng và các nút tạo được
từng bước lặp.
Thuật toán được thực hiện như sau:
*Duyệt lần lượt qua từng phần tử từ đầu đến cuối chuỗi hậu tố
- Tạo ra một đối tượng nút kiểu BinaryTreeNode (nút của cây biểu
thức) chứa giá trị của phần tử được xét,
- Nếu phần tử được xét đến là toán hạng thì đẩy vào một Stack,
- Nếu phần tử được xét là toán tử thì:
+ Lấy ra một phần tử toán hạng từ Stack và đặt làm nhánh con
phải của nút được tạo (RightChild ),
+ Lấy tiếp ra phần tử toán hạng tiếp theo từ Stack và đặt làm
nhánh con trái của nút được tạo (LeftChild),
+ Sau đó đẩy nút vừa tạo vào Stack.
*Quá trình lặp liên tục kết thúc và phần tử cuối cùng còn lại trong Stack
chính là nút gốc của cây biểu thức.
Ví dụ 5.22 Cho biểu thức dưới dạng hậu tố: 2, 3, 4, *, 5, 6, /, -, +
Xây dựng cây biểu thức tương ứng.
166
Cấu trúc dữ liệu và giải thuật
167
Cấu trúc dữ liệu và giải thuật
168
Cấu trúc dữ liệu và giải thuật
- Stack NodeStack để chứa các nút tạo nên cấu trúc cây, nút gốc của các
cây con được xây dựng từ dưới lên.
Trước khi thực hiện thuật toán ta phải xây dựng một phương thức (hàm)
có tên là CreateSubTree có nhiệm vụ tạo một cây biểu thức gồm 3 nút, với
nút gốc là toán tử lấy từ OperatorStack ra, hai nút lá là toán hạng lần lượt
lấy từ NodeStack ra. Cuối cùng đưa nút gốc vào lại NodeStack.
Các bước thực hiện thuật toán:
*Lần lượt duyệt qua từng phần tử trong biểu thức trung tố từ đầu đến
cuối:
- Nếu gặp được toán hạng thì đẩy phần tử đọc được vào trong
NodeStack,
- Nếu gặp được dấu mở ngoặc “(“ thì đẩy vào trong OperatorStack,
- Nếu gặp được dấu đóng ngoặc “)” thì:
+ Lặp liên tục cho đến khi lấy được dấu ngoặc “(“ trong ngăn
xếp OpearotorStack, mỗi lần lặp gọi phương thức CreateSubTree,
+ Lấy dấu mở ngoặc ra khỏi OperatorStack,
- Nếu gặp toán tử:
+ Lặp cho đến khi OperatorStack rỗng hoặc độ ưu tiên của toán
tử ở đỉnh OpeatorStack nhỏ hơn độ ưu tiên của toán tử hiện đại.
Mỗi lần lặp gọi phương thức CreateSubTree,
+ Đẩy toán tử đọc được vào OpeatorStack.
*Khi hết vòng lặp nếu OperatorStack còn phần tử thì gọi phương thức
CreateSubTree cho đến khi OperatorStack rỗng
*Nút cuối cùng nằm trong NodeStack là nút gốc của cây.
Ví dụ 5.23 Chuyển biểu thức (a+b) * c - d/e thành cây biểu thức
170
Cấu trúc dữ liệu và giải thuật
Như vậy cuối cùng còn lại nút “-” ở trong NodeStack, đây chính là nút
gốc của cây biểu thức cần tạo.
Vậy cây biểu thức thu được là:
171
Cấu trúc dữ liệu và giải thuật
Đọc Kết quả cây nhị phân biểu thức được tạo
172
Cấu trúc dữ liệu và giải thuật
173
Cấu trúc dữ liệu và giải thuật
174
Cấu trúc dữ liệu và giải thuật
tính 1 + 2 được kết quả là 3 sau rồi mới đem cộng với 4 để ra bằng 7 chứ
không thể thực hiện cùng lúc 3 số cộng lại với nhau.
1 + 2 + 4
3 + 4
Hình 5.12 Nguyên tắc thực hiện tính toán của máy tính
Khi tổ chức một biểu thức dưới dạng cây nhị phân biểu thức ta có thể
nhận thấy mỗi nhánh con của cây mô tả một biểu thức trung gian mà máy
tính cần xử lý của biểu thức lớn.
Ví dụ 5.25 Cho cây nhị phân biểu thức
Với cây biểu thức trên, để tính giá trị thì máy phải tính hai biểu thức con
(6 / 2) + 5 và 7 - 4 trước khi làm phép nhân cuối cùng. Tương tự để tính (6 /
2) + 5 thì máy phải tính 6 / 2 trước khi đem cộng với 5.
Như vậy việc tính giá trị biểu thức lưu trữ trong cây nhị phân tìm kiếm
với nút gốc bắt đầu là Root thì thực hiện như một hàm đệ quy như sau
175
Cấu trúc dữ liệu và giải thuật
176
Cấu trúc dữ liệu và giải thuật
Đọc Xử lý Stack
177
Cấu trúc dữ liệu và giải thuật
178
Cấu trúc dữ liệu và giải thuật
35 20 52 101 9 28 56 64
Bước 1: Chèn thêm 20 vào Heap (Chèn sang bên trái của Heap)
Heap này vi phạm vì phần tử 52 lớn hơn phần tử gốc 35, tiến hành hoán
đổi 35 và 52, thu được Heap
Bước 3: Chèn phần tử 101 vào Heap, chèn vào bên trái của 20
179
Cấu trúc dữ liệu và giải thuật
Cây này vi phạm định nghĩa của Heap do 101 lớn hơn 20 nên hoán đổi
101 với 20
Cây mới lại vi phạm định nghĩa của Heap, phần tử 101 lớn hơn 52 vì vậy
hoán đổi 101 với 52
180
Cấu trúc dữ liệu và giải thuật
181
Cấu trúc dữ liệu và giải thuật
Cây này vi phạm Heap vì 64 lớn hơn 20, hoán đổi 64 và 20.
182
Cấu trúc dữ liệu và giải thuật
Cây này vi phạm Heap vì 64 lớn hơn 52, hoán đổi 64 và 52.
Vậy, Cây Heap cuối cùng thu được:
101
64 56
52 09 28 35
20
183
Cấu trúc dữ liệu và giải thuật
Lấy phần tử gốc 101 ra khỏi Heap và thay thế bởi nút cuối 20, ta được
cây Heap mới
Cây Heap này vi phạm vì 20 nhỏ hơn cả hai nút con 64 và 56. Tiến hành
đổi chỗ 20 cho nút con lớn hơn là 64
184
Cấu trúc dữ liệu và giải thuật
Heap vẫn tiếp tục vi phạm do 20 nhỏ hơn 52, hoán đổi 20 và 52
Ta có được cây Heap hoàn chỉnh. Quá trình lại tiếp tục tại mỗi bước ta
đưa phần tử lớn nhất về làm gốc của cây Heap.
5.8.2 Cây 2-3-4
Cây 2-3-4 là một dạng cây cân bằng đều mà mỗi nút trên cây thỏa mãn
tính chất sau:
-Mỗi nút có thể có tối đa là bốn nhánh con và chứa ba mục dữ liệu.
-Không có nút rỗng (nút không có trường dữ liệu nào cả).
-Đối với các node không phải là nút lá, có thể có 3 cách tổ chức: Node
với một mục dữ liệu thì luôn luôn có 2 con; Node với hai mục dữ liệu thì
luôn luôn có 3 con; Node với ba mục dữ liệu thì luôn luôn có 4 con.
185
Cấu trúc dữ liệu và giải thuật
Trong cây 2-3-4, một nút bất kỳ không phải nút lá thì số nhánh con của
nút đó luông nhiều hơn 1 so với số giá trị chứa trong nút đó.
*Nút có một mục giá trị:
186
Cấu trúc dữ liệu và giải thuật
187
Cấu trúc dữ liệu và giải thuật
c. Viết hàm in ra màn hình các phần tử được sắp xếp theo thứ tự tăng
dần.
void PhanTuTangDan( Root Node)
{
if (Root != NULL)
{
PhantuTangDan( Root -> Left );
printf(“%6f“, Root -> value );
PhantuTangDan( Root -> Right );
}
}
d. Cho biết kết quả in ra màn hình của các phần tử trên cây khi áp dụng
các phép duyệt như: duyệt trước, duyệt sau, duyệt giữa
- Duyệt trước: 50, 13, 8, 7, 23, 18, 43, 58, 55, 60
- Duyệt sau: 7, 8, 18, 43, 23, 13, 55, 60, 58, 50
- Duyệt giữa: 7, 8, 13, 18, 23, 43, 50, 55, 58, 60
e. Hãy biểu diễn hình ảnh của cây khi ta xoá nút có khoá 50 và 13.
* Xóa 50: chọn phần tử thế mạng là phần tử lớn nhất bên nhánh trái $
chọn phần tử 43
188
Cấu trúc dữ liệu và giải thuật
* Xóa 13: chọn phần tử thế mạng là nhỏ nhất bên nhánh phải $ chọn
phần tử 18
f. Hãy biểu diễn hình ảnh của cây khi thêm nút có khóa 13 vào cây
189
Cấu trúc dữ liệu và giải thuật
Yêu cầu:
a. Cho biết các dạng biểu diễn của biểu thức tương ứng với cây trên
- Trung tố: (3 + 4) * ((8 – 2) * 6)
- Hậu tố: 3 4 + 8 2 – 6 * *
- Tiền tố: * + 3 4 * – 8 2 6
b. Tính giá trị của biểu thức
Dạng hậu tố: 3 4 + 8 2 – 6 * *
190
Cấu trúc dữ liệu và giải thuật
Đọc Xử lý Stack
2. Với cây nhị phân bên dưới hãy biểu thị cây theo phương pháp mảng
và danh sách liên kết
3. Cho biết thứ tự thăm các nút của cây trên khi tiến hành duyệt theo thứ
tự trước, thứ tự giữa và thứ tự sau.
5.11 Một số câu hỏi trắc nghiệm ôn tập
Câu 1. Cho dãy số sau: 30, 18, 35, 17, 40, 16, 32, 31, 43, 19. Cho biết
kết quả khi duyệt cây được tạo lần lượt từ các phần tử trên bằng phương
pháp duyệt LRN (Left Right Node ):
A. 30, 18, 35, 17, 40, 16, 32, 31, 43, 19
B. 30, 18, 17, 16, 19, 35, 32, 31, 40, 43
C. 30, 35, 40, 43, 32, 31, 18, 19, 17, 16
D. 16, 17, 19, 18, 31, 32, 43, 40, 35, 30
Câu 2. Phần tử thế mạng có thể được dùng khi xóa nút trong trường hợp
nút có hai nhánh con là gì?
A. Cả hai phát biểu đều đúng,
B. là phần tử nhỏ nhất trong số các phần tử bên nhánh phải,
C. Cả hai phát biểu đều sai,
D. là phần tử lớn nhất trong số các phần tử bên nhánh trái.
Câu 3. Bậc của nút trong cây có nghĩa là gì?
A. Là số nhánh con của nút đó,
192
Cấu trúc dữ liệu và giải thuật
Câu 8. Các trường hợp có thể xảy ra khi xóa một phần tử khỏi cây
NPTK gồm:
A. Nút xóa là nút lá, nút xóa có một nhánh con và nút xóa có hai nhánh
con,
B. Nút xóa có một nhánh con và nút xóa có hai nhánh con,
C. Nút xóa là nút lá và nút xóa có hai nhánh con,
D. Nút xóa là nút lá và nút xóa có một nhánh con.
Câu 9. Các thao tác cơ bản thực hiện trên cây NPTK?
A. Thêm một phần tử vào cây,
B. Thêm một phần tử vào cây, duyệt cây, tìm kiếm một phần tử và hủy
một phần tử khỏi cây,
C. Hủy một phần tử khỏi cây,
D. Duyệt các phần tử của cây.
Câu 10. Cho dãy số sau: 30, 18, 35, 17, 40, 16, 32, 31, 43, 19. Cho biết
kết quả khi duyệt cây được tạo lần lượt từ các phần tử trên bằng phương
pháp duyệt RNL(Right Node Left):
A. 16, 17, 19, 18, 31, 32, 43, 40, 35, 30
B. 43, 40, 35, 32, 31, 30, 19, 18, 17, 16
C. 30, 18, 35, 17, 40, 16, 32, 31, 43, 19
D. 30, 35, 40, 43, 32, 31, 18, 19, 17, 16
Câu 11. Cho cây NPTK, chọn biểu thức tương ứng với cây:
194
Cấu trúc dữ liệu và giải thuật
A.(3+4)*((8-2)*6) B.(3+4)*((8-2)*6)
C.(3+4)*(8-2*6) D.(3+4*8-2*6)
Câu 12. Khi xóa một phần tử khỏi cây NPTK thì có mấy trường hợp có
thể xảy ra?
A. 4 trường hợp, B. 2 trường hợp,
C. 3 trường hợp, D. 5 trường hợp.
Câu 13. Cho dãy số sau: 30, 18, 35, 17, 40, 16, 32, 31, 43, 19. Cho biết
kết quả khi duyệt cây được tạo lần lượt từ các phần tử trên bằng phương
pháp duyệt RLN(Right Left Node):
A. 30, 18, 35, 17, 40, 16, 32, 31, 43, 19
B. 30, 35, 40, 43, 32, 31, 18, 19, 17, 16
C. 43, 40, 31, 32, 35, 19, 16, 17, 18, 30
D. 16, 17, 19, 18, 31, 32, 43, 40, 35, 30
Câu 14. Trong cây có mấy loại nút?
A. 2 loại, B. 3 loại,
C. 5 loại, D. 4 loại.
Câu 15. Các nút của cây được phân thành những loại gì?
A. Nút gốc, nút lá và nút nhánh, B. Nút gốc và nút lá,
C. Nút lá và nút gốc, D. Nút lá và nút nhánh.
Câu 16. Độ dài đường đi từ gốc đến một nút là gì?
195
Cấu trúc dữ liệu và giải thuật
196
Cấu trúc dữ liệu và giải thuật
197
Cấu trúc dữ liệu và giải thuật
C. Cây NPTK là cây nhị phân trong đó tại mỗi nút thì giá trị khóa của
các nút thuộc nhánh bên trái đều lớn hơn giá trị khóa của nút đang xét và
các giá trị khóa của nút trong nhánh phải đều nhỏ hơn giá trị khóa của nút
đang xét,
D. Cả hai phát biểu đều đúng.
Câu 24. Phương pháp duyệt LNR là phương pháp duyệt gì?
A. Left - Node – Right, B. Node - Left – Right,
C. Left - Right - Node , D. Cả ba phát biểu trên đều
sai.
Câu 25. Thao tác thêm một phần tử vào cây khi so sánh giá trị của phần
tử cần thêm vào so với nút đang xét nếu phần tử cần thêm vào lớn hơn thì
được thêm vào vị trí nào?
A. Cả hai phát biểu trên đều đúng,
B. Cả hai phát biểu trên đều sai,
C. Phần tử mới được bổ sung vào nhánh trái của nút đang xét,
D. Phần tử mới được bổ sung vào nhánh phải của nút đang xét.
Câu 26. Phương pháp duyệt NRL là phương pháp duyệt gì?
A. Node - Right - Left B. Right - Left – Node,
C. Cả ba phát biểu trên đều đúng, D. Right - Node – Left.
Câu 27. Cho cây NPTK, chọn dạng biểu diễn dưới dạng ký pháp Balan
đảo ngược của cây NPTK:
198
Cấu trúc dữ liệu và giải thuật
A. *, +, 3, 4, *, -, 8, 2, 6 B. 3, +, 4, *, 8, -, 2, *, 6
C. 3, 4, +, 8, 2, -, 6, *, * D. Không có đáp án đúng
Câu 28. Cho đoạn mã cài đặt phương pháp duyệt NLR:
void NLR( Tree Root )
{
if ( Root != NULL )
{
< Xử lý Root >;
[1]…………
NLR ( Root -> Right);
}
}
Đoạn mã điền vào phần trống ở dòng số [1]
A. NLR ( Root -> Left ), B. LRN ( Root -> Right ),
C. LRN ( Root -> Left ), D. NLR ( Root -> Right ).
Câu 29. Cho cây NPTK, Cho biết kết quả duyệt cây theo thứ tự RNL là:
199
Cấu trúc dữ liệu và giải thuật
200
Cấu trúc dữ liệu và giải thuật
B. Là cây nhị phân để biểu diễn biểu thức toán học, trong đó các nút lá
biểu thị các hằng hay các biến (các toán hạng) còn các nút không là lá biểu
thị cho toán tử (phép toán),
C. Cả hai phát biểu đều sai,
D. Cả hai phát biểu đều đúng.
Câu 32. Cây nhị phân được biểu diễn bằng danh sách liên kết. Mỗi nút
của danh sách gồm có mấy thành phần:
A. 3 thành phần, B. 2 thành phần,
C. 4 thành phần, D. 5 thành phần.
Câu 33. Phương pháp duyệt RNL là phương pháp duyệt gì?
A. Node - Right – Left,
B. Right - Node – Left,
C. Right - Left - Node ,
D. Cả ba phát biểu trên đều đúng.
Câu 34. Cho đoạn mã cài đặt thao tác duyệt cây bằng phương pháp LRN
void LRN( Tree Root )
{
if ( Root != NULL )
{
LRN ( Root -> Left);
[2]…………
< Xử lý Root >;
}
}
Đoạn mã cần điền vào phần trống tại dòng [2]
A. RLN ( Root -> Left ), B. RLN ( Root -> Right ),
C. LRN ( Root -> Right ), D. LRN ( Root -> Left ).
Câu 35. Cho các phần tử sau: 31, 19, 36, 20, 41, 17, 33, 32. Tạo cây
NPTK từ các phần tử trên. Hãy cho biết sau khi xóa phần tử 33 trên cây sau
201
Cấu trúc dữ liệu và giải thuật
đó áp dụng phương pháp duyệt LRN thì kết quả thu được thứ tự các phần
tử là như thế nào?
A. 31, 19, 17, 20, 36, 32, 41 B. 17, 20, 19, 32, 41, 36, 31
C. 17, 19, 20, 31, 32, 36, 41 D. 31, 19, 36, 20, 41, 17, 32
5.12 Bài tập áp dụng
Bài 1. Cho tập hợp các số nguyên sau: 55, 60, 15, 25, 45, 40, 50, 20, 10,
30
a. Khai báo cấu trúc cây nhị phân tìm kiếm để lưu các phần tử có khoá là
số nguyên,
b. Áp dụng giải thuật thêm một phần tử vào cây nhị phân tìm kiếm, hãy
biểu diễn hình ảnh cây nhị phân tìm kiếm khi thêm tuần tự các phần tử có
các khoá trên vào cây,
c. Viết hàm in ra màn hình các phần tử được sắp xếp theo thứ tự tăng
dần,
d. Viết hàm đếm số nút trong cây nhị phân tìm kiếm trên,
e. Cho biết kết quả in ra màn hình của các phần tử trên cây khi áp dụng
các phép duyệt như: duyệt trước, duyệt sau, duyệt giữa,
f. Hãy biểu diễn hình ảnh của cây khi ta xoá nút có khoá 50 và 15,
g. Hãy biểu diễn hình ảnh của cây khi thêm nút có khóa 18 vào cây.
Bài 2. Thông tin của một sinh viên bao gồm : mã sinh viên (kiểu xâu kí
tự), điểm tổng kết (kiểu thực). Cho danh sách sinh viên sau: (K,6.5), (H,
4.5), (C,3.5), (G,4.0), (F,7.0), (E,8.0), (D,5.5), (B,7.5), (A,8.5). Thực hiện
các yêu cầu sau (bằng ngôn ngữ lập trình C):
a. Khai báo cấu trúc dữ liệu cây nhị phân tìm kiếm (dạng con trỏ) để lưu
danh sách sinh viên với khoá là điểm tổng kết,
b. Áp dụng giải thuật thêm một phần tử vào cây, hãy biểu diễn hình ảnh
của cây nhị phân tìm kiếm khi thêm tuần tự các sinh viên trên vào cây,
c. Cài đặt giải thuật tìm một sinh viên có điểm tổng kết x trong cây nhị
phân,
202
Cấu trúc dữ liệu và giải thuật
d. Thực hiện chạy từng bước tìm sinh viên có điểm tổng kết là 4.0 theo
giải thuật tìm kiếm đã viết,
e. Hãy biểu diễn hình ảnh của cây khi thêm nút (H,3) vào cây.
Bài 3. Cho cây tổng quát.
203
Cấu trúc dữ liệu và giải thuật
Bài 6. Viết các hàm xác định các thông tin của cây nhị phân T:
a. Số nút lá,
b. Số nút có đúng 1 cây con,
c. Số nút có đúng 2 cây con,
204
Cấu trúc dữ liệu và giải thuật
205
Cấu trúc dữ liệu và giải thuật
}
}
void CreateTree_mang(Tree &T)
{
int x;
int n=7;
int a[] = { 8, 6, 10, 4, 9, 7, 11};
for(int i=0;i<n;i++)
{
int check = insertNode(T, a[i]);
if (check == -1) printf("\n Node da ton tai!");
else if (check == 0) printf("\n Khong du bo nho");
}
}
void LNR(Tree T)
{
if(T!=NULL)
{
LNR(T->Left);
printf("%7d",T->key);
LNR(T->Right);
}
}
void NLR(Tree T)
{
if(T!=NULL)
{
printf("%7d",T->key);
NLR(T->Left);
206
Cấu trúc dữ liệu và giải thuật
NLR(T->Right);
}
}
void LRN(Tree T)
{
if(T!=NULL)
{
LRN(T->Left);
LRN(T->Right);
printf("%7d",T->key);
}
}
Node* searchKey(Tree T, int x) // tim nut co key x
{
if (T!=NULL)
{
if (T->key == x) { Node *P = T; return P;}
if (T->key > x) return searchKey(T->Left, x);
if (T->key < x) return searchKey(T->Right, x);
}
return NULL;
}
int delKey(Tree &T, int x) // xoa nut co key x
{
if (T==NULL) return 0;
else if (T->key > x) return delKey(T->Left, x);
else if (T->key < x) return delKey(T->Right, x);
else // T->key == x
{
Node *P = T;
if (T->Left == NULL) T = T->Right; // Node chi co
cay con phai
else if (T->Right == NULL) T = T->Left; // Node chi
co cay con trai
else // Node co ca 2 con
{
Node *S = T, *Q = S->Left;
// S la cha cua Q, Q la Node phai nhat cua cay con
trai cua P
while (Q->Right != NULL)
{
207
Cấu trúc dữ liệu và giải thuật
S = Q;
Q = Q->Right;
}
P->key = Q->key;
S->Right = Q->Left;
delete Q;
}
}
return 1;
}
int main()
{
Tree T;
T=NULL; //Tao cay rong
Node *P;
int x;
printf("Nhap vao key can tim: ");
scanf("%d", &x);
P = searchKey(T, x);
if (P != NULL) printf("\n Tim thay key %d ", P->key);
else printf("\n Key %d khong co trong cay ", x);
return 0;
}
Bài 2: Thực hiện đếm số nút lá, tìm giá trị lớn nhất, nhỏ nhất, …
#include <iostream>
208
Cấu trúc dữ liệu và giải thuật
#include <cstdlib>
#include <fstream>
#include <ctime>
using namespace std;
struct node{
int Data;
struct node *nLeft, *nRight;
};
//Theo NRL
void NRL(Tree T)
{
if(T != NULL)
209
Cấu trúc dữ liệu và giải thuật
{
printf("%5d",T->Data);
NRL(T->nRight);
NRL(T->nLeft);
}
}
//Theo LNR
void LNR(Tree T)
{
if(T != NULL)
{
LNR(T->nLeft);
printf("%5d",T->Data);
LNR(T->nRight);
}
}
//Theo LRN
void LRN(Tree T)
{
if(T != NULL)
{
LRN(T->nLeft);
LRN(T->nRight);
printf("%5d",T->Data);
}
}
//Theo RNL
void RNL(Tree T)
{
if(T != NULL)
{
RNL(T->nRight);
printf("%5d",T->Data);
RNL(T->nLeft);
}
}
//Theo RLN
void RLN(Tree T)
{
if(T != NULL)
{
210
Cấu trúc dữ liệu và giải thuật
RLN(T->nRight);
RLN(T->nLeft);
printf("%5d",T->Data);
}
}
void printTreeFull(Tree T)
{
cout << "\n======+Xuat du lieu+======";
printf("\nNLR: ");
NLR(T);
printf("\nNRL: ");
NRL(T);
printf("\nLNR: ");
LNR(T);
printf("\nLRN: ");
LRN(T);
printf("\nRNL: ");
RNL(T);
printf("\nRLN: ");
RLN(T);
}
//Ham doc du lieu
void createTree(Tree &T)
{
int n;
printf("\n so phan tu:");
scanf("%d",&n);
for(int i = 0; i<=n; i++)
{
int x;
scanf("%d",&x);
insertTree(T,x);
}
printTreeFull(T);
}
//Ham dem node la
void countLeaf(Tree &T, int &count)
{
if(T != NULL)
{
211
Cấu trúc dữ liệu và giải thuật
countLeaf(T->nLeft, count);
if(T->nLeft == NULL && T->nRight == NULL)
count++;
countLeaf(T->nRight, count);
}
}
//Ham tim kiem
Node* search(Tree T, int x)
{
if(T == NULL)
return NULL;
if(T->Data > x)
Search(T->nLeft, x);
else if(T->Data <x)
Search(T->nRight, x);
else return T;
}
int timMax(Tree T)
{
if(T->nRight != NULL)
timMax(T->nRight);
else
return T->Data;
}
int timMin(Tree T)
{
if(T->nLeft != NULL)
timMin(T->nLeft);
else
return T->Data;
}
int Sum(Tree T)
{
if (T == NULL)
return 0;
int a = Sum(T->nLeft);
int b = Sum(T->nRight);
return a + b + T->Data;
}
212
Cấu trúc dữ liệu và giải thuật
int main(){
Tree T;
initTree(T);
createTree(T);
int count = 0;
countLeaf(t,count);
printf("\nSo node la co trong cay nhi phan la:
%5d",count);
int x;
printf("\nNhap vao phan tu can tim kiem: ");
scanf("%d",&x);
Node *q = search(T,x);
if(q==NULL)
printf("\nPhan %d Khong ton tai trong cay nhi
phan!",x);
else
printf("\nPhan tu %d co ton tai trong cay nhi
phan!",x);
213
Cấu trúc dữ liệu và giải thuật
214
Cấu trúc dữ liệu và giải thuật
215
Cấu trúc dữ liệu và giải thuật
216
Cấu trúc dữ liệu và giải thuật
217
Cấu trúc dữ liệu và giải thuật
218
Cấu trúc dữ liệu và giải thuật
219
Cấu trúc dữ liệu và giải thuật
0 1 1 0 1 0
1 0 1 1 0 0
1 1 0 1 1 0
0 1 1 0 1 1
1 0 1 1 0 1
0 0 0 1 1 1
b. Đồ thị có hướng
0 1 0 0 1 0
0 0 0 1 0 0
1 1 0 0 0 0
0 0 0 1 0 1
0 0 1 1 0 1
220
Cấu trúc dữ liệu và giải thuật
0 0 0 0 0 0
221
Cấu trúc dữ liệu và giải thuật
a. Đồ thị vô hướng
0 2 4 0 7 0
2 0 2 5 0 0
4 2 0 3 1 0
0 5 3 0 4 7
7 0 1 4 0 4
0 0 0 7 4 0
b. Đồ thị có hướng
0 2 0 0 4 0
0 0 0 2 0 0
3 1 0 0 0 0
0 0 6 0 0 5
0 0 5 4 0 3
222
Cấu trúc dữ liệu và giải thuật
0 0 0 0 0 0
Cách lưu trữ đồ thị bằng ma trận kề cho phép kiểm tra một cách trực tiếp
hai đỉnh nào đó có kề nhau hay không? Tuy nhiên sẽ mất thời gian để duyệt
qua toàn bộ mảng hai chiều để xác định tập các cạnh của đồ thị. Thời gian
để thực hiện thao tác này hoàn toàn độc lập với số cạnh và số đỉnh của đồ
thị, trong trường hợp số cạnh của đồ thị rất nhỏ ta cũng phải cần một mảng
nxn phần tử để lưu trữ. Do vậy, nếu ta thường xuyên làm việc xử lý với các
cạnh của đồ thị thì ta phải dùng cách lưu trữ khác để phù hợp, thông thường
ta sử dụng danh sách đỉnh kê hoặc danh sách cạnh.
6.2.2 Lưu trữ đồ thị bằng danh sách đỉnh kề
Trong cách lưu trữ này, ta sẽ lưu trữ các đỉnh kề tương ứng của một đỉnh
i nào đó bằng một danh sách liên kết theo thứ tự. Như vậy để lưu trữ đồ thị
n đỉnh ta cần một mảng có n phần tử để lưu trữ đồ thị, mỗi phần tử mảng
trỏ tới danh sách các đỉnh kề với đỉnh i nào đó.
Ví dụ 6.4 Cho đồ thị sau, lưu trữ đồ thị dưới dạng danh sách kề
223
Cấu trúc dữ liệu và giải thuật
224
Cấu trúc dữ liệu và giải thuật
Ví dụ 6.6 Cho đồ thị sau, cho kết quả thứ tự duyệt cây theo chiều sâu
225
Cấu trúc dữ liệu và giải thuật
Thứ tự duyệt các đỉnh của đồ thị như sau: bắt đầu từ đỉnh a, a có các
đỉnh kề là b, c, d; theo thứ tự đó đỉnh b được duyệt. Đỉnh b lại có đỉnh kề
là f chưa được duyệt nên đỉnh f được duyệt. Đỉnh f có các đỉnh kề chưa
duyệt là d, g nên ta duyệt đỉnh d. Đỉnh d có đỉnh kề chưa duyệt là c, e, g
nên ta duyệt đỉnh c. Các đỉnh của c đã được duyệt nên giải thuật quay lại
bắt đầu với đỉnh e, đỉnh e có một đỉnh kề chưa duyệt là g nên ta duyệt đỉnh
g. Lúc này tất cả các đỉnh đã được duyệt và thứ tự các đỉnh được duyệt là:
a, b, f, d, c, e, g.
Ta thấy, trong trường hợp đồ thị được lưu trữ bởi một danh sách lân cận
thì đỉnh w lân cận của v sẽ được xác định bằng cách dựa vào danh sách
móc nối ứng với v. Vì giải thuật DFS chỉ xem xét mỗi nút trong một danh
sách lân cận nhiều nhất một lần thôi mà lại có 2*e nút danh sách (giả sử đồ
thị có e cạnh) nên thời gian để hoàn thành phép tìm kiếm là O(e).
Còn ngược lại, nếu đồ thị được lưu trữ bằng ma trận kề thì thời gian để
xác định lân cận của một đỉnh v nào đó là O(n). Vì có tối đa n đỉnh được
thăm, nên thời gian tìm kiếm tổng quát hết O(n2).
6.3.2 Duyệt theo chiều rộng
Giả sử ta có đồ thị G với các đỉnh được đánh dấu chưa duyệt. Từ một
đỉnh v nào đó ta bắt đầu duyệt như sau: đánh dấu v đã được duyệt, kế đến
là duyệt tất cả các đỉnh kề với đỉnh v. Khi ta duyệt một đỉnh v rồi đến đỉnh
w thì các đỉnh kề của của v được duyệt trước các đỉnh kề của w, vì vậy ta
dùng một hàng đợi để lưu trữ các nút theo thứ tự được duyệt để có thể
226
Cấu trúc dữ liệu và giải thuật
duyệt các đỉnh kề với chúng. Ta cũng dùng một mảng một chiều Mark để
đánh dấu một nút nào đó đã được duyệt hay chưa.
Ví dụ 6.7 Cho đồ thị sau, cho kết quả duyệt cây theo chiều rộng
Trên đường đi từ đỉnh s đến đỉnh t có đỉnh v là đỉnh trước đỉnh t trong
đường đi thì ta có d(s,t) = d(s,v) + w(v,t).
Có nhiều thuật toán tìm đường đi ngắn nhất khác nhau như: Floyd-
Warshall, Dijkstra, Ford Bellman, …
a. Thuật toán Floyd - Warshall
227
Cấu trúc dữ liệu và giải thuật
Thuật toán Floyd-Warshall hay còn gọi là Floyd, là thuật toán tìm
đường đi ngắn nhất giữa mọi cặp đỉnh bất kỳ trong đồ thị có trong số được
công bố năm 1962 bởi Robert Floyd.
Ý tưởng: nếu có đường đi từ đỉnh i đến đỉnh k và từ đỉnh k đến đỉnh j
mà nhỏ hơn đường đi hiện tại từ đỉnh i đến đỉnh j thì ta sẽ cập nhật đường
đi mới cho đường đi từ i đến j thành đường đi từ i tới k cộng với từ k tới j.
Ta gọi k là đỉnh trung gian của đỉnh i và j. Khi đó, thuật toán thực hiện sẽ
sinh ra một số cạnh ảo là cạnh không nối trực tiếp giữa hai đỉnh.
Các bước thực hiện thuật toán:
*Ký hiệu:
- A: là ma trận trọng số của đồ thị,
- Pi: là ma trận lưu đỉnh trước của một đỉnh trong đường đi,
- Di: là ma trận lưu độ dài đường đi ngắn nhất.
* Các bước thực hiện:
- Bước 0: Khởi tạo
Ma trận D0 nhận giá trị tương ứng từ ma trận A, là đường đi trực tiếp
không qua nút nào cả.
Ma trận P0 điền các giá trị theo nguyên tắc: P0[i,j] = i, có nghĩa là đỉnh
xuất phát của đỉnh i chính là đỉnh i
- Bước 1:
+ Tính giá trị cho ma trận D1 theo nguyên tắc sau:
D1[i,j] = min{D0[i,j]; D0[i,1] + D0[1,j]}
+ Thay đổi giá trị cho ma trận P1 theo nguyên tắc sau:
P1[i,j] = P0[i,j] nếu D1[i,j] = D0[i,j]
1 nếu D1[i,j] = D0[i,1] + D0[1,j]
- Bước 2:
+ Tính giá trị cho ma trận D2 theo nguyên tắc sau:
D2[i,j] = min{ D1[i,j]; D1[i,2] + D1[2,j]}
+ Thay đổi giá trị cho ma trận P2 theo nguyên tắc sau:
228
Cấu trúc dữ liệu và giải thuật
229
Cấu trúc dữ liệu và giải thuật
⎡ ∞ 10 6 2 ⎤ ⎡1 1 1 1⎤
⎢10 ∞ 5 3 ⎥ ⎢2 2 2 2⎥⎥
D2 = ⎢ ⎥ và P2 = ⎢
⎢ 6 5 ∞ 1⎥ ⎢3 3 3 3⎥
⎢ ⎥ ⎢ ⎥
⎣ 2 3 1 ∞⎦ ⎣4 4 4 4⎦
230
Cấu trúc dữ liệu và giải thuật
Ví dụ 6.9 Tìm được đi ngắn nhất từ đinh a đến đỉnh z của đồ thị
231
Cấu trúc dữ liệu và giải thuật
Để thực hiện tìm đường đi ngắn nhất bằng cách áp dụng thuật toán
Dijktra thông thường ta xây dựng bảng để lưu lại từng bước thực hiện,
bảng được xây dựng theo nguyên tắc sau:
- Bảng gồm n + 1 cột với n là số đỉnh của đồ thị
- Bảng gồm tối đa n + 1 hàng
- Tại mỗi phần tử của bảng ghi giá trị (x,d) với x là đỉnh trước của đỉnh
tại cột đang xét hình thành đường đi ngắn nhất và d là độ dài đường đi ngắn
nhất từ đỉnh xuất phát đến đỉnh tại cột đang xét.
Theo nguyên tắc trên ta có bảng tương ứng của đồ thị trên như sau:
Bước a b c d e z
5 - - - - - (e, 13)*
Theo định nghĩa thì ta thấy một đồ thị G có thể có rất nhiều cây khung
khác nhau tùy theo việc lựa chọn cạnh bổ sung vào cây khung. Một trong
các ứng dụng cơ bản của khung đó là tìm cây khung nhỏ nhất hay gọi là
cây khung có giá trị cực tiểu trên đồ thị có trong số. Ví dụ, nếu ta có đồ thị
được biểu diễn với các đỉnh là các thành phố và các cung biểu diễn đường
nối giữa các thành phố với trọng số biểu diễn giá tiền xây dựng hoặc độ dài
đường đi giữa các thành phố … thì việc chọn một cây khung nhỏ nhất
tương ứng với việc xây dựng một mạng đường giao thông có khả năng liên
thông mọi thành phố với chi phí tổng xây dựng là ít nhất hoặc tổng độ dài
đường đi giữa các thành phố là ít nhất, …
Cây khung nhỏ nhất trong đồ thị liên thông có trọng số là một cây khung
có tổng trọng số trên các cạnh của cây khung là nhỏ nhất. Hiện nay có
nhiều giải thuật khác nhau để tìm cây khung nhỏ nhất của đồ thị trong đó
hai thuật toán thường hay được giới thiệu là thuật toán Kruskal và thuật
toán Prim.
a. Thuật toán Prim
Ý tưởng: thuật toán được bắt đầu bằng việc chọn một đỉnh bất kỳ đặt vào
cây khung, sau đó lần lượt ghép vào cây các cạnh có trọng số nhỏ nhất
233
Cấu trúc dữ liệu và giải thuật
chưa xét mà liên thuộc với đỉnh của cây sao cho không tạo thành chu trình.
Quá trình thực hiện liên tiếp khi đưa n-1 cạnh vào cây khung.
Để thực hiện thuật toán Prim ta sử dụng tập Tk và Sk, với Tk là tập chứa
các cạnh tạo thành cây khung và Sk là tập các đỉnh của cây khung tại bước
lặp thứ k.
Thuật toán thực hiện các bước lặp sau:
" Bước 0: Chọn một đỉnh bất kỳ bổ sung vào cây khung
" Bước 1: Chọn cạnh có trọng số nhỏ nhất liên thuộc với đỉnh đã xét
đưa vào tập T1 (tập chứa cây khung bước thứ 1)
" Bước k:
# Xây dựng tập Tk dựa trên tập Tk-1 theo nguyên tắc như sau:
chọn cạnh (u,v) có giá trị trọng số nhỏ nhất chưa được xét sao
cho (u,v) liên thuộc với đỉnh bất kỳ trong Sk-1 mà không tạo
thành chu trình. Sau đó, bổ sung cạnh (u,v) vào tập Tk và đỉnh u,v
vào tập Sk.
# Quá trình lặp liên tiếp khi k = n –1. Có nghĩa là chọn được n-1
cạnh vào cây khung.
Ví dụ 6.11 Cho đồ thị sau yêu cầu tìm cây khung nhỏ nhất của đồ thị
234
Cấu trúc dữ liệu và giải thuật
Bước 1: có hai đỉnh u1 và u2 kề với đỉnh u0 đã xét. Đánh chỉ số của các
đỉnh và chọn đỉnh có chỉ số nhỏ nhất đưa vào cây khung. Vì vậy chọn cạnh
(u0, u1) đưa vào cây khung.
Bước 2: Xét tiếp các đỉnh kề với đỉnh đã xét. Có hai cạnh (u1, u3) và
(u1, u2) thỏa mãn. Theo nguyên tắc, gán nhãn cho đỉnh u2 và u3 như hình
235
Cấu trúc dữ liệu và giải thuật
vẽ. Thực hiện, chọn đỉnh có nhãn nhỏ nhất đưa vào cây khung vì vậy chọn
cạnh (u1, u3) đưa vào cây khung.
Bước 3: Có các cạnh (u3, u2), (u3, u4), (u3, u5) kề với các đỉnh đã xét
của cây khung. Theo nguyên tắc, đánh chỉ số cho các đỉnh và chọn ra đỉnh
có chỉ số nhỏ nhất đưa vào cây khung. Ta chọn cạnh (u3, u4) đưa vào cây
khung .
236
Cấu trúc dữ liệu và giải thuật
Vậy kết quả được cây khung T3 = {(u0, u1), (u1, u3), (u3, u4)}
Bước 4: Có hai cạnh (u3, u2) và (u4, u5) kề với các đỉnh đã xét của cây
khung. Theo nguyên tắc, xét chỉ số của các đỉnh để chọn ra cạnh đưa vào
cây khung . Ta chọn cạnh (u3, u2) đưa vào cây khung.
Vậy kết quả cây khung T4 = {(u0, u1), (u1, u3), (u3, u4), (u3, u2)}
Bước 5: còn lại đỉnh u5 chưa xét kề với các đỉnh của cây khung, đánh chỉ
số của đỉnh u5. Theo nguyên tắc chọn cạnh đưa vào cây khung. Chọn cạnh
237
Cấu trúc dữ liệu và giải thuật
(u4, u5) đưa vào cây khung, kết quả cây khung của đồ thị. Vậy thuật toán
dừng vì đủ n-1 cạnh.
Để thực hiện thuật toán Prim thông thường xây dựng một bảng để lưu lại
quá trình thực hiện từng bước, bảng xây dựng để lưu các thông tin sau:
- Cạnh dự định được xét tại mỗi bước,
- Chỉ số trọng số của mỗi cạnh được xét tương ứng.
Trong mỗi bước xem xét cạnh có chỉ số nhỏ nhất thì được xét và đánh
dấu lại.
Theo nguyên tắc trên với đồ thị đã cho ta có bảng được xây dựng như
sau:
Lặp U0 U1 U2 U3 U4 U5
0 0, (u0,u0) 5, (u0,u1)* 30,(u0,u1) ∞,(u0,u3) ∞,(u0,u4) ∞,(u0,u5)
238
Cấu trúc dữ liệu và giải thuật
239
Cấu trúc dữ liệu và giải thuật
tiếp cho đến khi bổ sung n - 1 cạnh vào cây khung. Tập Tk cuối cùng chứa
các cạnh tạo thành cây khung.
Ví dụ 6.12: Cho đồ thị sau, tìm cây khung nhỏ nhất bằng thuật toán
Kruskal
Bước 2: Chọn cạnh có trọng số nhỏ nhất (a, d) với trọng số là 2 và đảm
bảo không tạo thành chu trình bổ sung vào cây khung.
240
Cấu trúc dữ liệu và giải thuật
Bước 3: Chọn cạnh có trọng số nhỏ nhất (h, i) với trọng số là 2 và đảm
bảo không tạo thành chu trình bổ sung vào cây khung.
Bước 4: Chọn cạnh có trọng số nhỏ nhất (d, b) với trọng số là 3 và đảm
bảo không tạo thành chu trình bổ sung vào cây khung.
241
Cấu trúc dữ liệu và giải thuật
Bước 5: Chọn cạnh có trọng số nhỏ nhất (c, f) với trọng số là 3 và đảm
bảo không tạo thành chu trình bổ sung vào cây khung.
Bước 6: Chọn cạnh có trọng số nhỏ nhất (d, g) với trọng số là 3 và đảm
bảo không tạo thành chu trình bổ sung vào cây khung.
Bước 7: Chọn cạnh có trọng số nhỏ nhất (e, h) với trọng số là 3 và đảm
bảo không tạo thành chu trình bổ sung vào cây khung.
242
Cấu trúc dữ liệu và giải thuật
Bước 8: Chọn cạnh có trọng số nhỏ nhất (b, c) với trọng số là 4 và đảm
bảo không tạo thành chu trình bổ sung vào cây khung.
243
Cấu trúc dữ liệu và giải thuật
244
Cấu trúc dữ liệu và giải thuật
245
Cấu trúc dữ liệu và giải thuật
246
Cấu trúc dữ liệu và giải thuật
Quy tắc 4: Trong quá trình xây dựng chu trình Hamilton, sau khi đã lấy
2 cạnh tới một đỉnh v đặt vào chu trình Hamilton rồi thì không thể lấy thêm
cạnh nào tới v nữa $ xoá mọi cạnh còn lại tới v.
Bài toán người đưa thư
Một nhân viên bưu điện, xuất phát từ trạm bưu điện mà anh ta đang làm
việc, cần chuyển đi n bức thư khác nhau đến n địa chỉ khác nhau, với yêu
cầu mỗi nơi chỉ đến 1 lần rồi trở về trạm bưu điện, hãy tìm một hành trình
ngắn nhất giúp nhân viên bưu điện hoàn thành công việc. Ta thấy ngay,
mỗi hành trình như trên chính là một chu trình Halminton. Theo định lý
Dirac nếu bậc của tất cả các đỉnh đều >n/2 thì bài toán luôn có lời giải. Tuy
nhiêu, đây chỉ là điều kiện đủ, nếu có một đỉnh nào đó có bậc <n/2 thì chưa
thể khẳng định được rằng đồ thị không phải là đồ thị Halminton.
6.4.4 Sắp xếp Tôpô
Thông thường khi gặp một công việc lớn bao giờ ta cũng chia nhỏ công
việc đó ra để dễ dàng thực hiện. Khi đó sẽ có nhiều công việc nhỏ khác
nhau, sẽ có những phần việc có thể thực hiện được độc lập nhưng cũng sẽ
có những phần việc chỉ thực hiện được khi một số phần việc con khác đã
được làm xong.
Ví dụ 6.13 Với một sinh viên học tại khoa công nghệ thông tin
theo hình thức đào tạo tín chỉ thì để có thể học một môn nào đó
sinh viên phải hoàn thành một số môn học tiên quyết bắt buộc,
chẳng hạn:
M2 Giải tích 1
M3 Kỹ thuật điện tử số
M4 Anh văn 1
247
Cấu trúc dữ liệu và giải thuật
M7 Giải tích 2 M2
M8 Anh văn 2 M4
M10 Cơ sở dữ liệu M5
Rõ ràng, môn “Tin đại cương”, môn “Giải tích 1” hoặc “Anh văn 1”
hoàn toàn có thể kết thúc môn học một cách độc lập nhưng môn “Cơ sở lập
trình” không thể bắt đầu nếu sinh viên chưa học môn “Tin đại cương” và
“Kỹ thuật điện tử số”, hay môn “Giải tích 1” sẽ không được học nếu sinh
viên chưa hoàn thành môn “Giải tích 1”. Như vậy, giữa môn “Cơ sở lập
trình”, “Kỹ thuật điện tử số” có mối quan hệ, Môn “Kỹ thuật điện tử số”
“được học trước” môn “Cơ sở lập trình”. Hay giữa môn “Giải tích 1”, “Giải
tích 2” có mối quan hệ, môn “Giải tích 1” “được học trước” môn “Giải tích
2”. Một cách tổng quát thì quan hệ đó được gọi là một “thứ tự bộ phận”.
Đó được coi là một quan hệ giữa các phần tử của một tập S, ký hiệu là
đọc là “đường trước” và quan hệ này thỏa mãn các tính chất sau đối với
các phần tử phân biệt x, y, z của S như sau:
1. Nếu x y và y z thì x z (tính bắc cầu)
2. Nếu x y thì không có y x (tính phản xứng)
3. Không có x x (tính không phản xứng)
Do ý nghĩa thực tế ta luôn giả thiết S là một tập hữu hạn. Một thứ tự bộ
phận có thể minh họa bằng một đồ thị có hướng trong đó các đỉnh đồ thị
tương ứng với các phần tử, còn các cung tương ứng với môn quan hệ thứ tự
giữa các phần tử.
Khi đó một sinh viên, trong một khoảng thời gian nhất định chỉ có thể
hoàn thành được một môn học (để kiểm tra và thi kết thúc) thì sinh viên đó
phải sắp xếp các môn học theo một thứ tự tuyến tính nhất định để sao cho
248
Cấu trúc dữ liệu và giải thuật
khi học một môn nào đó, thì các môn học cần trước đó đã phải hoàn thành
rồi. Chẳng hạn anh ta có thể học theo trình tự sau:
M1, M2, M3, M4, M5, M6, M7, M8, M9, M10
Hoặc
M1, M3, M5, M6, M10, M9, M2, M7, M4, M8
Một thứ tự tuyến tính với đặc điểm như trên được gọi là thứ tự Tôpô và
cách sắp xếp một tập các đối tượng có thứ tự bộ phận thành thứ tự Tôpô
được gọi là sắp xếp Tôpô.
Như vậy đối với một đồ thị định hướng, không có chu trình thì sắp xếp
Tôpô là quá trình định ra thứ tự tuyến tính cho các đỉnh của đồ thị sao cho
nếu có một cung từ đỉnh i đến đỉnh j thì j cũng xuất hiện trước j trong thứ
tự tuyến tính đó.
Để sắp xếp Tôpô sử dụng phương pháp sau: bắt đầu chọn một đỉnh mà
không có cung nào đi tới nó (hay nói cách khác không có đỉnh nào “trước”
nó), đỉnh này sẽ được đưa ra. Sau đó nó cùng các cung xuất phát từ nó sẽ bị
loại khỏi đồ thị, quá trình này được lặp với phần còn lại của đồ thị cho tới
khi các định của đồ thị được chọn ra hết, trong quá trình thực hiện nếu có
cùng một lúc nhiều đỉnh đều không có cung tới đỉnh đó thì việc chọn đỉnh
nào trong số đỉnh đó là tùy ý.
Ví dụ 6.14 Cho đồ thị như sau
249
Cấu trúc dữ liệu và giải thuật
1 3 5
4 6
250
Cấu trúc dữ liệu và giải thuật
6 Hết
1 2 3 4 5 6
251
Cấu trúc dữ liệu và giải thuật
Đồ thị có thể tổ chức lưu trữ theo nhiều cách khác nhau tuỳ theo mục
đích lưu trữ và khai thác tương ứng, như: ma trận kề, ma trận trọng số,
danh sách cạnh và danh sách kề.
Các bài toán cơ bản thực hiện trên đồ thi gồm: duyệt đồ thị, cây khung,
cây khung nhỏ nhất, đường đi ngắn nhất, …
6.5.2 Câu hỏi ôn tập
1. Định nghĩa đồ thị có hướng liên thông? Hãy vẽ 2 ví dụ minh họa về đồ
thị có hướng liên thông mạnh và liên thông yếu?
2. Chứng minh rằng trong đồ thị vô hướng thì số đỉnh bậc lẻ là một số
chẵn.
n(n − 1)
3. Chứng minh rằng đồ thị đầy đủ Kn với n đỉnh có số cạnh bằng 2
.
6.6 Một số câu hỏi trắc nghiệm ôn tập
Câu 1: Cho khai báo cấu trúc đồ thị dạng ma trận trọng số như
sau:
struct DoThi
{
int n;
float C[max][max];
};
Cho biết đoạn chương trình con sau thực hiện gì?
void XuLy(DoThi G)
{
printf("\n Ma tran trong so la:\n");
for(int i =1; i<=G.n; i++)
{
for(int j=1;j<=G.n;j++)
printf("%8.1f",G.C[i][j]);
printf("\n");
}
}
252
Cấu trúc dữ liệu và giải thuật
scanf("%d",&G.n);
for(i =1; i<=G.n; i++)
for(j=1; j<=G.n; j++)
G.C[i][j]=0;
}
C.
void XuLy(DoThi G, int k)
{
int i,j;
printf("\n Cac dinh ke cua %d la:",k);
for(i=1;i<=G.n;i++)
if(G.C[k][i]>0)
printf("%7d",i);
}
D.
void XuLy(DoThi G)
{
printf("\n Ma tran trong so la:\n");
for(int i =1; i<=G.n; i++)
{
for(int j=1;j<=G.n;j++)
if (G.C[i][j]>0)
printf("%8.1f",G.C[i][j]);
printf("\n");
}
}
Câu 4: Cho đồ thị sau:
A.
0 5 30 0 0 0
0 0 20 10 0 0
0 0 0 10 15 0
0 0 0 0 5 20
0 0 0 0 0 15
0 0 0 0 0 0
B.
0 0 0 0 0 0
5 0 0 0 0 0
30 20 0 0 0 0
0 10 10 0 0 0
0 0 15 5 0 0
0 0 0 20 15 0
C.
0 1 1 0 0 0
1 0 1 1 0 0
1 1 0 1 1 0
0 1 1 0 1 1
0 0 1 1 0 1
0 0 0 1 1 0
255
Cấu trúc dữ liệu và giải thuật
D.
0 5 30 0 0 0
5 0 20 10 0 0
30 20 0 10 15 0
0 10 10 0 5 20
0 0 15 5 0 15
0 0 0 20 15 0
A. B.
C. D.
Câu 9: Cho đồ thị G = <V,E> dưới dạng ma trận trọng số. Hãy cho biết
đâu là tập cạnh của cây khung nhỏ nhất được xây dựng theo thuật toán
Kruskal
A. T = {(1, 2), (1, 4), (2, 4), (2, 6), (4, 5), (6, 7)}
B. T = { (2, 3), (1, 3), (4, 5), (4, 6), (3, 5) }
C. T = {(1, 2), (1, 4), (2, 3), (2, 6), (6, 3), (6, 7)}
D. T={(1, 2), (1, 4), (2, 3), (4, 5), (2, 6), (6, 7)}
Câu 10: Tổng các phần tử hàng i, cột j của ma trận kề đồ thị có hướng G
=<V,E> đúng bằng:
257
Cấu trúc dữ liệu và giải thuật
A. Bán đỉnh bậc vào của đỉnh i, bán đỉnh bậc ra đỉnh j
B. Bán đỉnh bậc ra của đỉnh i, bán đỉnh bậc ra đỉnh j.
C. Bán đỉnh bậc ra của đỉnh i, bán đỉnh bậc vào đỉnh j.
D. Bán đỉnh bậc vào của đỉnh i, bán đỉnh bậc vào đỉnh j.
6.7 Bài tập áp dụng
Bài 1. Cho đồ thị như hình sau
258
Cấu trúc dữ liệu và giải thuật
259
Cấu trúc dữ liệu và giải thuật
Bài 3. Dùng thuật toán Kruskal và thuật toán Prim để tìm cây khung nhỏ
nhất của các đồ thị được cho dưới đây
1. Đồ thị vô hướng G được lưu trữ bằng ma trận kề như sau:
⎡0 2 43⎤ 4
⎢2 0 13⎥⎥1
⎢
⎢4 1 0 ∞ 2⎥
a. ⎢ ⎥;
⎢4 1 ∞ 0 2⎥
⎢⎣3 3 2 2 0⎥⎦
⎡0 14 3 15 28 35⎤
⎢14 0 11 29 24 16 ⎥⎥
⎢
⎢3 11 0 27 12 11⎥
b. ⎢ ⎥
⎢15 29 27 0 28 14 ⎥
⎢28 24 12 28 0 13 ⎥
⎢ ⎥
⎢⎣35 16 11 14 13 0 ⎥⎦
260
Cấu trúc dữ liệu và giải thuật
a.
Bài 4: Bài tập áp dụng Áp dụng thuật toán Floyd để tìm đường đi ngắn
nhất giữa hai đỉnh bất kỳ trong đồ thị sau
#include <stdio.h>
#include <conio.h>
#include <stdlib.h>
#define max 20
//Khai bao CTDL Ma tran Trong so
struct DoThi
{
int n;
float C[max][max];
};
//Nhap lien tiep den khi nhap dinh = 0 thi dung
void nhapDoThi(DoThi &G)
{
int dd,dc;
float ts;
printf("\n Nhap so dinh cua do thi:");
scanf("%d", &G.n);
for(int i =1; i<=G.n;i++)
for(int j=1;j<=G.n;j++)
G.C[i][j] = 0;
do
{
printf("\n Nhap gia tri 0 de dung!\n ");
printf("Nhap dinh dau:"); scanf("%d", &dd);
printf("Nhap dinh cuoi:"); scanf("%d", &dc);
printf("Nhap trong so:"); scanf("%f", &ts);
G.C[dd][dc] = ts;
G.C[dc][dd] = ts;
} while(dd!=0);
}
void inDoThi(DoThi G)
{
printf("\n Ma tran trong so la \n");
for(int i=1;i<=G.n; i++)
{
for(int j=1;j<=G.n; j++)
printf("%7.2f", G.C[i][j]);
printf("\n");
}
262
Cấu trúc dữ liệu và giải thuật
int main()
{
DoThi G;
nhapDoThi(G);
inDoThi(G);
getch();
}
Bài 2: Chương trình lưu trữ đồ thị dưới dạng Danh sách cạnh
#include<stdio.h>
#include<conio.h>
#include<stdlib.h>
#define max 20
//Khai bao CTDL dang ma tran
struct DoThiMaTran
{
int n;
float C[max][max];
};
//--------------------
struct Canh
{
int dd,dc;
float ts;
};
struct DoThiCanh
{
int m;
Canh ds[max];
};
scanf("%d",&G.m);
for(i =1; i<=G.m; i++)
{
printf("Nhap dinh dau:"); scanf("%d",&d);
printf("Nhap dinh cuoi:"); scanf("%d",&c);
printf("Nhap trong so:"); scanf("%f",&ts);
G.ds[i].dd = d;
G.ds[i].dc = c;
G.ds[i].ts = ts;
}
}
void inDoThi_MaTran(DoThiMaTran G)
{
printf("\n\n Ma tran trong so la:\n");
for(int i =1; i<=G.n; i++)
{
for(int j=1;j<=G.n;j++)
printf("%8.1f",G.C[i][j]);
printf("\n");
}
}
264
Cấu trúc dữ liệu và giải thuật
int main()
{
DoThiCanh G;
DoThiMaTran GG;
265
Cấu trúc dữ liệu và giải thuật
int k;
nhapDoThi(G);
inDoThi(G);
DSCanh_MaTran(G, GG);
inDoThi_MaTran(GG);
getch();
}
Bài 3: Chương trình lưu trữ đồ thị dưới dạng Danh sách kề
#include<stdio.h>
#include<conio.h>
#include<stdlib.h>
#define max 20
struct DoThiMaTran
{
int n;
float C[max][max];
};
//--------------------
struct Canh
{
int dd,dc;
float ts;
};
struct DoThiCanh
{
int m;
Canh ds[max];
};
//-------------------
struct DinhKe
{
int dc;
float ts;
};
struct DoThiDinhKe
{
int n;
266
Cấu trúc dữ liệu và giải thuật
DinhKe dske[max];
};
//------------------
void nhapDoThi(DoThiCanh &G)
{
int d,c;
int i;
float ts;
printf("Nhap so canh cua do thi:");
scanf("%d",&G.m);
for(i =1; i<=G.m; i++)
{
printf("\n Nhap gia tri 0 de dung!\n");
printf("Nhap dinh dau:"); scanf("%d",&d);
printf("Nhap dinh cuoi:"); scanf("%d",&c);
printf("Nhap trong so:"); scanf("%f",&ts);
G.ds[i].dd = d;
G.ds[i].dc = c;
G.ds[i].ts = ts;
}
}
void inDoThi(DoThiCanh G)
{
printf("\n Danh sach cac canh cua do thi la: \n");
for(int i = 1; i<=G.m; i++)
printf("\n %d -> %d: %7.1f",
G.ds[i].dd,G.ds[i].dc, G.ds[i].ts);
}
void inDoThi_MaTran(DoThiMaTran G)
{
printf("\n\n Ma tran trong so la:\n");
for(int i =1; i<=G.n; i++)
{
for(int j=1;j<=G.n;j++)
printf("%8.1f",G.C[i][j]);
printf("\n");
}
}
//Liet ke cac dinh ke cua dinh k trong do thi G
void lietKeDinhKe_MaTran(DoThiMaTran G,int k)
267
Cấu trúc dữ liệu và giải thuật
{
int i,j;
printf("\n Cac dinh ke cua dinh %d la:",k);
for(i=1;i<=G.n;i++)
if(G.C[k][i]>0)
printf("%7d",i);
}
void DSCanh_MaTran(DoThiCanh G, DoThiMaTran &GG)
{
int i,j;
//Tinh so dinh cua do thi
GG.n = 0;
for(i=1; i<=G.m; i++)
{
if(G.ds[i].dd > GG.n)
GG.n = G.ds[i].dd;
if(G.ds[i].dc > GG.n)
GG.n = G.ds[i].dc;
}
//printf("\n Dinh cua do thi la:%d", GG.n);
for(i=1; i<=GG.n; i++)
for(j=1; j<=GG.n; j++)
GG.C[i][j] = 0;
268
Cấu trúc dữ liệu và giải thuật
}
int main()
{
DoThiCanh G;
DoThiMaTran GG;
int k;
nhapDoThi(G);
inDoThi(G);
DSCanh_MaTran(G, GG);
inDoThi_MaTran(GG);
printf("\n\n Nhap dinh can xac dinh dinh ke:");
scanf("%d",&k);
//lietKeDinhKe(G,k);
lietKeDinhKe_DSCanh(G,k);
getch();
}
269