You are on page 1of 269

BỘ GIÁO DỤC VÀ ĐÀO TẠO

TRƯỜNG ĐẠI HỌC MỞ HÀ NỘI

CẤU TRÚC DỮ LIỆU & GIẢI THUẬT

Trương Tiến Tùng – Nguyễn Thị Tâm - Trịnh Thị Xuân

HÀ NỘI 6/2021
(Lưu hành nội bộ)
Cấu trúc dữ liệu và giải thuật

LỜI NÓI ĐẦU


Để đáp ứng nhu cầu học tập của các bạn sinh viên đặc biệt là sinh
viên chuyên ngành Công nghệ thông tin, Khoa Công nghệ thông tin
Trường Đại học Mở Hà Nội, chúng tôi đã biên soạn giáo trình, bài giảng
của chương trình đào tạo theo hệ thống tín chỉ. Bài giảng môn Cấu trúc dữ
liệu và thuật toán này được biên soạn dựa trên quyển "Cấu trúc dữ liệu và
thuật toán" của tác giả Đinh Mạnh Tường, Nhà xuất bản Khoa học và Kỹ
thuật và quyển "Algorithms + Data Structure = Program" của Nicklaus
Wirth, Bản dịch tiếng Việt, Nhà xuất bản Khoa học và Kỹ thuật, 1993.
Giáo trình này cũng được biên soạn dựa trên kinh nghiệm giảng dạy nhiều
năm môn Cấu trúc dữ liệu và Giải thuật của chúng tôi.
Giáo trình được biên soạn theo đề cương chi tiết môn Cấu trúc dữ
liệu và Giải thuật của sinh viên chuyên ngành Công nghệ thông tin Khoa
Công nghệ Thông tin Trường Đại học Mở Hà Nội. Mục tiêu nhằm giúp các
bạn sinh viên có một tài liệu dùng làm tài liệu học tập song các đối tượng
khác cũng có thể tham khảo. Chúng tôi cho rằng các bạn sinh viên không
chuyên tin và những người quan tâm tới cấu trúc dữ liệu và giải thuật sẽ
tìm được nhiều điều bổ ích.
Mặc dù đã rất cố gắng trong quá trình biên soạn giáo trình nhưng
chắc chán giáo trình sẽ còn nhiều thiếu sót và hạn chế. Nhóm tác giả rất
mong nhận được sự đóng góp ý kiến của các bạn sinh viên và người đọc để
giáo trình ngày một hoàn thiện hơn.
Hà Nội, tháng 6 năm 2021
Nhóm tác giả
Trương Tiến Tùng
Nguyễn Thị Tâm
Trịnh Thị Xuân

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

2.3.1 Khái niệm ....................................................................................... 66


2.3.2 Các phép toán trên danh sách liên kết ............................................ 68
2.3.3 So sánh cài đặt danh sách theo hai phương pháp ........................... 76
2.3.4 Các dạng danh sách liên kết khác ................................................... 77
2.4 Bài tập có hướng dẫn ............................................................................. 78
2.5 Tổng kết chương .................................................................................... 82
2.6 Câu hỏi trắc nghiệm............................................................................... 82
2.7 Câu hỏi và bài tập .................................................................................. 95
Chương 3. NGĂN XẾP – HÀNG ĐỢI ......................................................... 97
3.1 Ngăn xếp - Stack ................................................................................... 97
3.1.1 Khái niệm ....................................................................................... 97
3.1.2 Cài đặt bằng mảng .......................................................................... 98
3.1.3 Cài đặt bằng danh sách liên kết .................................................... 101
3.1.4 Ứng dụng của ngăn xếp ............................................................. 103
3.2 Hàng đợi – Queue ................................................................................ 106
3.2.1 Khái niệm ..................................................................................... 106
3.2.2 Cài đặt bằng mảng ........................................................................ 106
3.2.3 Cài đặt hàng đợi bằng danh sách liên kết ..................................... 108
3.2.4 Ứng dụng hàng đợi để biểu diễn đa thức ...................................... 110
3.3 Tổng kết chương .................................................................................. 111
3.4 Bài tập .................................................................................................. 112
Chương 4. HÀM BĂM – BẢNG BĂM...................................................... 114
4.1 Khái niệm hàm băm – phép băm ......................................................... 114
4.2 Các loại hàm băm ................................................................................ 114
4.2.1 Hàm băm sử dụng phương pháp chia ........................................... 114
4.2.2 Hàm băm sử dụng phương pháp nhân .......................................... 115
4.2.3 Hàm băm sử dụng phương pháp trích .......................................... 116
4.2.4 Hàm băm sử dụng phương tách .................................................... 116
4.2.5 Hàm băm sử dụng phương gập ..................................................... 116
4.3 Bảng băm ............................................................................................. 117
4.3.1 Giới thiệu ...................................................................................... 117
4.3.2 Các bảng băm cơ bản.................................................................... 118

4
Cấu trúc dữ liệu và giải thuật

4.4 Ứng dụng hàm băm ............................................................................. 123


4.5 Tổng kết chương và câu hỏi ôn tập ..................................................... 125
4.5.1 Tổng kết chương ........................................................................... 125
4.5.2 Câu hỏi ôn tập ............................................................................... 125
4.6 Một số câu hỏi trắc nghiệm ôn tập ...................................................... 126
4.7 Bài tập áp dụng .................................................................................... 127
Chương 5. DANH SÁCH PHI TUYẾN DẠNG CÂY - TREE .................. 129
5.1 Các khái niệm – Định nghĩa ................................................................ 129
5.2 Cài đặt cây ........................................................................................... 134
5.2.1 Cài đặt cây bằng danh sách kế tiếp – mảng các nút cha ............... 134
5.2.2 Lưu trữ móc nối – danh sách các nút con ..................................... 137
5.3 Cây nhị phân – cây nhị phân tìm kiếm ................................................ 139
5.4 Lưu trữ cây nhị phân ........................................................................... 141
5.4.1 Lưu trữ kế tiếp dưới dạng mảng ................................................... 141
5.4.2 Lưu trữ móc nối ............................................................................ 145
5.5 Biểu diễn cây tổng quát bằng cây nhị phân ......................................... 147
5.6 Các thao tác trên cây nhị phân tìm kiếm ............................................. 148
5.6.1 Các phép duyệt cây ....................................................................... 148
5.6.2 Tìm kiếm trên cây nhị phân tìm kiếm .......................................... 151
5.6.3 Chèn thêm phần tử vào cây .......................................................... 152
5.6.4 Xóa phần tử khỏi cây .................................................................... 159
5.7 Cây biểu thức ....................................................................................... 161
5.7.1 Định nghĩa và các cách biểu diễn biểu thức ................................. 161
5.7.2 Chuyển từ biểu thức sang ký pháp Balan đảo ngược ................... 164
5.7.3 Xây dựng cây nhị phân biểu thức ................................................. 165
5.7.4 Tính giá trị của biểu thức .............................................................. 174
5.8 Bài toán áp dụng .................................................................................. 178
5.8.1 Sắp xếp danh sách theo phương pháp vun đống – Heap Sort ...... 178
5.8.2 Cây 2-3-4 ...................................................................................... 185
5.9 Ví dụ tổng hợp ..................................................................................... 187
5.10 Tổng kết chương và câu hỏi ôn tập ................................................... 191
5.10.1 Tổng kết chương ......................................................................... 191

5
Cấu trúc dữ liệu và giải thuật

5.10.2 Câu hỏi ôn tập ............................................................................. 191


5.11 Một số câu hỏi trắc nghiệm ôn tập .................................................... 192
5.12 Bài tập áp dụng .................................................................................. 202
5.13 Bài tập có hướng dẫn ......................................................................... 205
Chương 6. ĐỒ THỊ - GRAPH .................................................................... 214
6.1 Các khái niệm – Định nghĩa ................................................................ 214
6.1.1 Giới thiệu ...................................................................................... 214
6.1.2 Các định nghĩa cơ bản .................................................................. 215
6.1.3 Các thuật ngữ cơ bản .................................................................... 218
6.2 Lưu trữ đồ thị ....................................................................................... 219
6.2.1 Lưu trữ đồ thị bằng ma trận kề - Ma trận trọng số ....................... 219
6.2.2 Lưu trữ đồ thị bằng danh sách đỉnh kề ......................................... 223
6.2.3 Lưu trữ đồ thị bằng danh sách cạnh ............................................. 224
6.3 Các phép duyệt đồ thị và ứng dụng ..................................................... 225
6.3.1 Duyệt theo chiều sâu .................................................................... 225
6.3.2 Duyệt theo chiều rộng .................................................................. 226
6.4 Một số bài toán trên đồ thị ................................................................... 227
6.4.1 Tìm đường đi ngắn nhất và ứng dụng .......................................... 227
6.4.2 Tìm cây khung nhỏ nhất và ứng dụng .......................................... 233
6.4.3 Tìm chu trình ................................................................................ 244
6.4.4 Sắp xếp Tôpô ................................................................................ 247
6.5 Tổng kết chương và câu hỏi ôn tập ..................................................... 251
6.5.1 Tổng kết chương ........................................................................... 251
6.5.2 Câu hỏi ôn tập ............................................................................... 252
6.6 Một số câu hỏi trắc nghiệm ôn tập ...................................................... 252
6.7 Bài tập áp dụng .................................................................................... 258
6.8 Bài tập có hướng dẫn: .......................................................................... 261

6
Cấu trúc dữ liệu và giải thuật

DANH MỤC HÌNH VẼ


Hình 2.1 Minh họa thêm phần tử mới vào đầu danh sách ............................... 69
Hình 2.2 Chèn thêm phần tử mới vào cuối danh sách ..................................... 70
Hình 2.3 Chèn thêm phần tử mới vào sau phần tử q xác định ........................ 71
Hình 2.4 Minh họa xóa phần tử đầu danh sách ............................................... 72
Hình 2.5 Minh họa xóa phần tử đứng sau phần tử q xác định ......................... 73
Hình 2.6 Danh sách liên kết vòng đơn ............................................................ 77
Hình 2.7 Danh sách liên kết vòng đôi ............................................................. 78
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 ........... 101
Hình 3.2 Minh họa thao tác Push để bổ sung phần tử vào Stack .................. 102
Hình 3.3 Minh họa thao tác Pop để lấy phần tử khỏi Stack .......................... 103
Hình 3.4 Minh họa chuyển sang cơ số 8 ....................................................... 105
Hình 3.5 Minh họa hàng đợi Queue .............................................................. 106
Hình 3.6 Minh họa tính tổng hai đa thức ....................................................... 111
Hình 4.1 Hàm băm ........................................................................................ 114
Hình 4.2 Bảng băm ........................................................................................ 117
Hình 5.1 Sơ đồ tổ chức trường đại học.......................................................... 130
Hình 5.2 Sơ đồ tổ chức cây thư mục trong máy tính ..................................... 130
Hình 5.3 Minh họa cây tổng quát bằng danh sách con móc nối .................... 139
Hình 5.4 Sơ đồ cấu trúc một nút .................................................................... 146
Hình 5.5 Kết quả chuyển từ cây tổng quát sang cây nhị phân ...................... 148
Hình 5.6 Minh họa tìm kiếm phần tử trên cây NPTK ................................... 152
Hình 5.7 Minh họa thêm phần tử vào cây NPTK .......................................... 154
Hình 5.8 Minh họa xóa nút lá khỏi cây NPTK .............................................. 159
Hình 5.9 Minh họa xóa nút có một nhánh con .............................................. 160
Hình 5.10 Minh họa xóa nút có 2 nhánh con ................................................ 161
Hình 5.11 Cây biểu thức (6/2 + 3) * (7 - 4) ................................................... 161
Hình 5.12 Nguyên tắc thực hiện tính toán của máy tính ............................... 175
Hình 6.1 Đồ thị có hướng .............................................................................. 215
Hình 6.2 Đồ thị vô hướng .............................................................................. 215
Hình 6.3 Đơn đồ thị vô hướng....................................................................... 216
Hình 6.4 Đa đồ thị vô hướng ......................................................................... 216

7
Cấu trúc dữ liệu và giải thuật

Hình 6.5 Giả đồ thị ........................................................................................ 217


Hình 6.6 Đơn đồ thị có hướng ....................................................................... 217
Hình 6.7 Đa đồ thị có hướng ......................................................................... 218
Hình 6.8 Danh sách đỉnh kề lưu trữ đồ thị .................................................... 224
Hình 6.9 Danh sách cạnh lưu trữ đồ thị ......................................................... 225
Hình 6.10 Minh họa đồ thị Euler (H1, H2, H3) ............................................ 244
Hình 6.11 Minh họa đồ thị Hamilton (G1, G2, G3) ...................................... 246
Hình 6.12 Đồ thị minh họa sắp xếp Tôpô ..................................................... 251

8
Cấu trúc dữ liệu và giải thuật

PHẦN TỔNG QUAN


Mục đích yêu cầu
Môn học Cấu trúc dữ liệu và giải thuật cung cấp cho sinh viên một khối
lượng lớn các kiến thức cơ bản về các cấu trúc dữ liệu và các phép toán
trên từng cấu trúc đó. Sau khi học xong môn này, sinh viên cần phải:

- 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ế.

Đối tượng sử dụng


Môn học Cấu trúc dữ liệu và giải thuật được dùng để giảng dạy cho các
sinh viên sau:

- Sinh viên chuyên ngành công nghệ thông tin (môn bắt buộc)

Nội dung chính


Nội dung giáo trình gồm 6 chương và được trình bày trong 60 tiết
cho sinh viên bao gồm lý thuyết và bài tập mà giáo viên sẽ hướng dẫn cho
sinh viên trên lớp. Nội dung giáo trình chú trọng trình bày về các cấu trúc
dữ liệu và các giải thuật trên các cấu trúc dữ liệu đó.
Chương 1:
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.
Chương 2:
Trình bày mô hình dữ liệu danh sách, các cấu trúc dữ liệu để cài đặt
danh sách, chúng tôi tập trung trình bày cấu trúc danh sách liên kết đơn cho

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ị.

Kiến thức tiên quyết


Để học tốt môn học Cấu trúc dữ liệu và giải thuật này, sinh viên cần có
các kiến thức cơ bản:
• Kiến thức và kỹ năng lập trình cơ bản

Danh mục tài liệu tham khảo


[1] Đinh Mạnh Tường, Cấu trúc dữ liệu và Giải thuật, Nhà xuất bản Khoa học và
Kỹ thuật, 2001.

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

Chương 1. CÁC KHÁI NIỆM CƠ BẢN

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:

Sinh viên Môn 1 Môn 2 Môn 3

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:

int diem [12] = { 8 6 4


9 5 3
6 7 2

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:

Phương án 2: Sử dụng mảng hai chiều


Khai báo mảng 2 chiều diem có kích thước 3 cột * 4 dòng như sau:
int diem [12] = { {8 6 4},
{9 5 3},
{6 7 2},
{5 6 5} };
Khi đó trong mảng diem các phần tử sẽ được lưu trữ như sau:

Cột 0 Cột 1 Cột 2

14
Cấu trúc dữ liệu và giải thuật

Dòng 0 diem[0][0]=8 diem[0][1]=6 diem[0][2]=4

Dòng 1 diem[1][0]=9 diem[1][1]=5 diem[1][2]=3

Dòng 2 diem[2][0]=6 diem[2][1]=7 diem[2][2]=2

Dòng 3 diem[3][0]=5 diem[3][1]=6 diem[3][2]=5

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

Bước 2: Tính delta = b2 – 4ac


Bước 3: Xét dấu của delta để kết luận nghiệm
3.1: Nếu delta < 0 thì “Phương trình vô nghiệm”
3.2: Nếu delta = 0 thì “Phương trình có nghiệm kép x = -b/2a”
3.3: Nếu delta>0
Tính x1 = (-b + sqrt(delta))/(2a)
Tính x2 = (-b - sqrt(delta))/(2a)
Kết luận phương trình có hai nghiệm phân biệt x1 và x2
1.3.2 Sơ đồ khối
Lưu đồ hay sơ đồ khối (flow - chart) là một công cụ trực quan để diễn
đạt các thuật toán. Biểu diễn thuật toán bằng lưu đồ sẽ giúp người đọc theo
dõi được sự phân cấp các trường hợp và quá trình xử lý của thuật toán.
Phương pháp lưu đồ thường được dùng trong những thuật toán có tính rắc
rối, khó theo dõi được quá trình xử lý.
Ðể biểu diễn thuật toán theo sơ đồ khối, ta phải phân biệt hai loại thao
tác. Một thao tác là thao tác chọn lựa dựa theo một điều kiện nào đó. Chẳng
hạn: thao tác “nếu a = b thì thực hiện thao tác B2, ngược lại thực hiện B4”
là thao tác chọn lựa. Các thao tác còn lại không thuộc loại chọn lựa được
xếp vào loại hành động. Chẳng hạn, “Nhập vào hệ số a của phương trình
bậc hai” là một thao tác thuộc loại hành động.
- Thao tác chọn lựa – decision: được biểu diễn bằng một hình thoi bên
trong chứa biểu thức điều kiện (biểu thức so sánh)

a=b D>0

Thao tác lựa chọn

- 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

Bắt đầu Kết thúc

Đ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

1.4.3 Ước lượng thời gian thực hiện chương trình


Thời gian chạy của một chương trình phụ thuộc vào các yếu tố sau:
- Dữ liệu đầu vào
- Chất lượng của mã máy được tạo ra bởi chương trình dịch
- Tốc độ thực thi lệnh của máy
- Độ phức tạp về thời gian của thuật toán
Thông thường, thời gian chạy của chương trình không phụ thuộc vào giá
trị dữ liệu đầu vào mà phụ thuộc vào số lượng dữ liệu cần nhập (kích thước
dữ liệu đầu vào). Kích thước dữ liệu càng lớn thì thời gian xử ý cũng càng
lớn theo, chẳng hạn như thời gian sắp xếp một dãy số chịu ảnh hưởng của
số lượng các số thuộc dãy số đó. Vì thế, thời gian chạy của chương trình
nên được định nghĩa như là một hàm có tham số là kích thước của dữ liệu
đầu vào. Giả sử T là hàm ước lượng thời gian chạy của chương trình. Khi
đó với dữ liệu đầu vào có kích thước n thì thời gian chạy của chương trình
là T(n). Đơn vị của hàm T(n) là không xác định, tuy nhiên ta có thể xem
như T(n) là tổng số lệnh được thực hiện trên một máy tính lý tưởng.
Ký hiệu O(n)
Để biểu thị cấp độ tăng của hàm T(n) người ta sử dụng ký hiệu O(n). Giả
sử T(n) và g(n) là các hàm của đối số nguyên n. Ta nói “T(n) là ô lớn của
g(n)” và viết là: T(n) = O(g(n)) nếu tồn tại các hằng số dương c và n0 sao
cho T(n) <= c.g(n) với mọi n≥n0.
Nghĩa là nếu xét những giá trị n ≥ n0 thì hàm f(n) sẽ bị chặn trên bởi một
hằng số nhân với g(n). Khi đó, nếu f(n)là thời gian thực hiện của một giải
thuật thì ta nói giải thuật đó có cấp là g(n) hay độ phức tạp tính toán là
O(g(n)).
Ví dụ 1.7 Xét hàm T(n) = (n+1)2.
Ta có thể thấy T(n) là O(n2) với c = 4 và n0 = 1 vì ta có
T(n) = (n+1)2 = n2 + 2n + 1 < 4n2 với mọi n>0.
Ví dụ 1.8 Xét hàm, f(n) = 5n3 + 2n2 + 13n + 6, ta có:
f(n) = 5n3 + 2n2 + 13n + 6 <= 5n3 + 2n3 + 13n3 + 6n3 <= 26n3

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

Điều kiện biên: Mảng 0 phần tử thì tổng bằng 0.


Giải thuật chung:
Sum(a,n) = a[0] + a[1] + a[2] + ... + a[n-2] + a[n-1]
= Sum(a, n-1) + a[n-1]
Vậy công thức đệ quy là:
Sum (a,n) = 0 với n=0
a[n-1] + Sum(a, n-1) với n>0
1.7.3 Ví dụ minh họa
a. Tính n!
* Cách tính giá trị:
n = 0: 0! = 1
n = 1: 1! = 1
n = 2: 2! = 1 * 2 = 1! * 2
n = 3: 3! = 1 * 2 * 3 = 2! * 3
n = 4: 4! = 1 * 2 * 3 * 4 = 3! * 4
n = 5: 5! = 1 * 2 * 3 * 4 * 5 = 4! * 5
* Công thức đệ quy:
Giaithua(n) = 1 với n = 0
Giaithua(n-1) * n với n>0
* Cài đặt đệ quy
long Giaithua( int n )
{
if (n==0) return 1;
else return Giaithua(n-1)* n;
}
b. Dãy số Fibonaci
* Cách tính giá trị:

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:

Hình 1.1. Số Fibonacci

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

c. Tính tổng các phần tử mảng


* Cách tính giá trị:
Giả sử cho mảng a gồm n các số khác nhau, để tính tổng các phần tử của
mảng có thể áp dụng theo nguyên tắc sau:
n = 0: Tong(0) = 0
n = 1: Tong(1) = a[0] = Tong(0) + a[0]
n = 2: Tong(2) = a[0] + a[1] = Tong(1) + a[1]
34
Cấu trúc dữ liệu và giải thuật

n = 3: Tong(3) = a[0] + a[1] + a[2] = Tong(2) + a[2]


n = 4: Tong(4) = a[0] + a[1] + a[2] + a[3] = Tong(3) + a[3]

Vậy khi xét tính tổng các phần tử của mảng a có n phần tử được thực
hiện theo công thức TongMang(a,n) = TongMang(a,n-1) + a[n-1]
* Công thức đệ quy:
TongMang( a, n ) = 0 với n = 0
TongMang( a, n-1 ) + a[n-1]
* Cài đặt đệ quy:

1.8 Tổng kết chương và câu hỏi ôn tập


1.8.1 Tổng kết chương
Các kiến thức trọng tâm cần lưu ý:
- Thuật toán là một chuỗi hữu hạn các lệnh. Mỗi lệnh có một ngữ nghĩa
rõ ràng và có thể được thực hiện với một lượng hữu hạn tài nguyên trong
một khoảng hữu hạn thời gian.
- Thuật toán thường được mô tả bằng các ngôn ngữ diễn đạt giải thuật
gần với ngôn ngữ tự nhiên. Các mô tả này sẽ được tinh chỉnh dần dần để
đạt tới mức ngôn ngữ lập trình.
- Thời gian thực hiện của thuật toán thường được coi như là một hàm của
kích thước dữ liệu đầu vào.
- Thời gian thực hiện thuật toán được tính trong các trường hợp tốt nhất,
xấu nhất hoặc trung bình.

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

- Số ngày nghĩ không phép trong tháng: số ≤ 28


- Số ngày làm thêm trong tháng: số ≤ 28
- Kết quả công việc: chuỗi 2 ký tự (TO = Tốt; BT = đạt ; KE = Kém)
- Lương thực lĩnh trong tháng: số ≤ 2.000.000
*Quy tắc lĩnh lương: Lương thực lĩnh = Lương căn bản + Phụ trội
Trong đó nếu:
- số con > 2 : Phụ trội = +5% Lương căn bản
- trình độ văn hoá = CH: Phụ trội = +10% lương căn bản
- làm thêm: Phụ trội = +4% lương căn bản / ngày
- nghĩ không phép : Phụ trội = -5% lương căn bản / ngày
*Chức năng yêu cầu:
- Cập nhật lý lịch, bảng chấm công cho nhân viên (thêm, xoá, sửa)
- Xem bảng lương hàng tháng
- Tìm thông tin của một nhân viên
Tổ chức cấu trúc dữ liệu thích hợp để biểu diễn các thông tin trên, và cài
đặt chương trình theo các chức năng đã mô tả.
Lưu ý: Số lượng nhân viên tối đa là 50 người.
Bài 2. Cho các đoạn mã sau. Yêu cầu: vẽ sơ đồ khối thực hiện đoạn mã,
xác định độ phức tạp tính toán của thuật toán và xác định ký hiệu O-lớn của
thuật toán
a. t = 0; n = 20;
for ( i =1; i<n; i++ )
if ( i%2==1) t = t + 2*i;
printf(" Gia tri t la : %d", t );
b. k = 0; n = 20;
while ( k<n )
{

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

Chương 2. DANH SÁCH


Trong chương này chúng ta sẽ nghiên cứu về cấu trúc dữ liệu trừu tượng
Danh sách liên kết, các thuật toán xử lý dữ liệu cài đặt bằng danh sách liên
kết như thêm, sửa, xóa, tìm kiếm, sắp xếp, tính toán,... Sau khi học xong
chương này sinh viên sẽ nắm vững các khái niệm, vận dụng cài đặt giải
quyết các bài toán thực tế với cấu trúc dữ liệu danh sách.
2.1 Danh sách
2.1.1 Khái niệm
Danh sách là một dãy hữu hạn các phần tử thuộc cùng một lớp đối tượng
có kiểu giống nhau. Ví dụ như danh sách các sinh viên của một lớp, danh
sách các số nguyên, …
Giả sử L là danh sách có n phần tử (n>=0):
L = (a1, a2, …., an)
Khi đó: n là độ dài của danh sách
- n = 0: danh sách rỗng
- n >= 1 thì a1 gọi là phần tử đầu tiên của danh sách, an là phần tử cuối
cùng của danh sách.
- Các phần tử của danh sách được sắp tuyến tính theo vị trí xuất hiện:
nếu n > 1 thì phần tử ai đứng trước phần tử ai+1 hay phần tử ai+1 đứng sau ai
với i=1, 2…n-1. Ta cũng nói ai là phần tử ở vị trí thứ i của danh sách.
- Các phần tử trong danh sách có thể xuất hiện nhiều lần.
Ví dụ 2.1 danh sách các lớp của trường tiểu học:
Lớp 1A
Lớp 1B
Lớp 1C
Lớp 2A
Lớp 2B
Lớp 2C

40
Cấu trúc dữ liệu và giải thuật

Danh sách con


Nếu L = (a1, a2, …., an) là một danh sách thì một đoạn bất kì chứa các
phần tử liên tiếp nhau của L được gọi là một danh sách con.
Danh sách rỗng là danh sách con của một danh sách bất kì.
Danh sách con gồm các phần tử bắt đầu từ phần tử đầu tiên của danh
sách được gọi là phần đầu của danh sách (prefix), một danh sách kết thúc
bởi phần tử cuối cùng gọi là phần cuối của danh sách (postfix).
Dãy con
Một danh sách mà loại bỏ một số phần tử từ danh sách ban đầu L thì
được gọi là dãy con của danh sách L.
Ví dụ 2.2 Xét danh sách
L = (đỏ, cam, vàng, lục, lam, chàm, tím)
Khi đó:
L1 = (cam, vàng, lục, lam) là một danh sách con của L
L2 = (cam, lục, chàm) là một dãy con của L
L3 = (đỏ, cam, vàng, lục, lam) là phần đầu của danh sách
L4 = (lục, lam, chàm, tím) là phần cuối của danh sách.
2.1.2 Các phép toán trên danh sách
Một số phép toán chính trên danh sách gồm:
- Tạo danh sách
- Kiểm tra danh sách rỗng/ đầy
- Phép thêm mới một phần tử vào danh sách
- Phép loại bỏ
- Xác định vị trí của một phần tử trong danh sách
- Tìm kiếm
- Sắp xếp
- Duyệt danh sách

41
Cấu trúc dữ liệu và giải thuật

2.2 Cài đặt danh sách bằng mảng


2.2.1 Cài đặt
Sử dụng cấu trúc dữ liệu kiểu mảng để cài đặt danh sách khi đó mỗi
thành phần của mảng sẽ lưu một phần tử của danh sách, các phần tử kế tiếp
nhau trong danh sách được lưu trong các thành phần kế tiếp nhau của
mảng.
N là số phần tử tối đa trong danh sách, với cách cài đặt này chúng ta phải
xác định số lượng phần tử tối đa mà danh sách có thể lưu trữ.
Ví dụ 2.3 Khai báo một danh sách các số nguyên
int a[100]; // khai báo một mảng số nguyên a tối đa gồm 100 phần tử.
Với những dữ liệu phức tạp gồm nhiều thành phần thông tin có thể sử
dụng kiểu dữ liệu cấu trúc để khai báo.
Ví dụ 2.4 Danh sách sinh viên với các thông tin: họ tên, điểm

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

Dãy ban đầu

Muốn chèn phần tử V vào vị trí p của mảng ta phải:

42
Cấu trúc dữ liệu và giải thuật

- Dồn tất cả các phần tử từ vị trí p đến vị trí n về sau 1 vị trí

Dãy sau khi dồn chỗ

- Đặt V vào vị trí p

Chèn phần tử mới

- Tăng kích thước của mảng lên 1 đơn vị n = n + 1


Thủ tục được cài đặt như sau:

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

Dãy ban đầu

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í

Dãy sau khi dồn chỗ

- Giảm kích thước n đi 1 đơn vị

Dãy kết quả

Thủ tục được cài đặt như sau:

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)

- Nếu x = a[middle] hoặc right > left thì dừng.


Mô tả thuật toán tìm kiếm nhị phân như sau: (giả sử dãy a đã sắp
tăng)
*Đầu vào:
x – là giá trị cần tìm
n – số phần tử mảng
a – mảng chứa các phần tử đã được sắp xếp tăng dần
left, right – là chỉ số đầu và chỉ số cuối của mảng thực hiện
tìm
*Đầu ra: vị trí chứa phần tử x (nếu có)
*Các bước thuật toán:
• Bước 1: Khởi đầu tìm kiếm trên tất cả các phần tử của dãy ! left =
0 và right = n - 1
• Bước 2: Tính middle = (left + right)/2. So sánh a[middle] với x. Có
3 khả năng:
o a[middle] = x ⇒ Tìm thấy => Dừng
o a[middle] > x ⇒ tiếp tục tìm x trong dãy con mới với right =
middle – 1 (tìm trong nửa đầu)
o a[middle] < x ⇒ tiếp tục tìm x trong dãy con mới với left =
middle + 1 (tìm trong nửa cuối)
• Bước 3:
o Nếu left ≤ right ⇒ dãy còn phần tử, tiếp tục quay lại bước 2 để
tìm kiếm tiếp
o Ngược lại ⇒ Dãy hiện hành hết phần tử và dừng thuật toán

47
Cấu trúc dữ liệu và giải thuật

*Thủ tục được cài đặt như sau:

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.

Như vậy, toàn bộ dãy đã được sắp.


Thuật toán được mô tả như sau:
*Đầ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 thuật toán:
• Bước 1: i = 0 (bắt đầu từ vị trí đầu tiên)
• Bước 2: tìm chỉ số phần tử min nhỏ nhất trong dãy hiện hành từ a[i]
đến a[n-1]
• Bước 3: Hoán vị a[i] với a[min]
• Bước 4:
o nếu i<n-1 thì i = i+1 và lặp lại bước 2
o ngược lại thì n-1 phần tử đã được sắp xếp => Dừng thuật toán
51
Cấu trúc dữ liệu và giải thuật

*Thủ tục cài đặt như sau:

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 3, 4: Lần lượt có phần tử 49, 98 vào đoạn đã sắp

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.

Thuật toán được mô tả như sau:


*Đầ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 thuật toán:
• Bước 1: i = 1 //giả sử a[0] đã được sắp xếp
• Bước 2: x=a[i], tìm vị trí pos thích hợp trong đoạn từ a[0] đến a[i-1]
để chèn a[i] vào
• Bước 3: đổi chỗ các phần tử từ a[pos] đến a[i-1] sang phải một vị trí
để được vị trí chèn a[i] vào
• Bước 4: chèn a[i] vào vị trí pos vừa tìm được bằng cách gán a[pos]
=x
• Bước 5: Tính i = i+1
o Nếu i<n thì lặp lại bước 2
o Ngược lại thì dừng thuật toán
*Thủ tục được cài đặt như sau:

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

Thuật toán được mô tả như sau:


*Đầ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 thuật toán:
• Bước 1: i = 0
• Bước 2: j = n - 1 //duyệt từ cuối đến phần tử thứ i
Trong khi j>i thực hiện
o nếu a[j] < a[j-1] thì hoán đổi hai phần tử
o j=j-1
• Bước 3: i = i + 1
o Nếu i > n-1 thì Hết dãy và dừng thuật toán
o Ngược lại lặp lại bước 2
*Thủ tục được cài đặt như sau:

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

Bước 1: Xét khoảng các phần tử từ vị trí 0 đến 7 (Left = 0 và Right=7).


Phần tử chốt x = a[mid] = 3.

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

Bước 3: Xét khoảng các phần tử từ vị trí 4 đến 7 (Left = 4 và Right=7).


Phần tử chốt x = a[mid] = 7.

Bước 4: Xét khoảng các phần tử từ vị trí 5 đến 7 (Left = 5 và Right=7).


Phần tử chốt x = a[mid] = 15.

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:

Thuật toán được mô tả như sau:


*Đầ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 của thuật toán
• Bước 1: Phân hoạch dãy aL … aR thành các dãy con:
o Dãy con 1: aL … aj < x
o Dãy con 2: aj+1 … ai-1 =x
o Dãy con 3: ai … aR > x
63
Cấu trúc dữ liệu và giải thuật

• 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.

b. Tạo nút mới với thành phần dữ liệu x


Sử dụng để tạo ra một nút có thể chứa thông tin của một dữ liệu x nào
đó.

c. Thêm một nút vào danh sách


Để thêm một nút vào danh sách liên kết đơn đã có, ta có các trường hợp
sau:
- Chèn thêm một nút vào đầu danh sách
- Chèn thêm một nút vào cuối danh sách

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

*Thêm cuối danh sách


Các bước để chèn 1 nút mới vào cuối danh sách như sau:

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

*Thêm vào sau phần tử đã biết


Các bước để chèn một nút mới vào sau một phần tử của danh sách, giả
sử là q, thực hiện:

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:

d. Xóa một nút trong danh sách


Nếu như khi thêm chúng ta phải cấp phát bộ nhớ cho nút cần thêm thì
với xóa một nút chúng ta cần giải phóng bộ nhớ cho nút bị xóa bằng lệnh
free().

71
Cấu trúc dữ liệu và giải thuật

Để xóa một nút trong danh sách có các khả năng:


- Xóa phần tử đầu danh sách
- Xóa phần tử sau phần tử xác định nào đó
- Xóa phần tử có giá trị k nào đó
*Xóa phần tử đầu danh sách
Các bước thực hiện nhau sau:
- Kiểm tra danh sách không rỗng
- Lưu phần tử đầu tạm thời vào p
- Chuyển phần tử đầu tới phần tử tiếp theo
- Xóa phần tử đầu đã được lưu tạm – xóa p
- Kiểm tra: Nếu danh sách chỉ có một phần tử, khi xóa phần tử đi thì
phần tử cuối cùng không còn

Hình 2.4 Minh họa xóa phần tử đầu danh sách


Thủ tục cài đặt:

*Xóa phần tử đứng sau phần tử q của danh sách

72
Cấu trúc dữ liệu và giải thuật

Các bước thực hiện:


- Nếu có phần tử q: Lưu phần tử đứng sau phần tử q – lưu vào p
- Nếu có phần tử p (q không phải là phần tử cuối)
- Tách phần tử p ra khỏi danh sách
- Giải phóng phần tử p

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

*Xóa phần tử có khóa bằng k


Thuật toán:
Bước 1: Tìm phần tử p có khóa k và phần tử q đứng trước nó
Bước 2: Nếu tìm thấy phần tử có khóa là k thì hủy p ra khỏi xâu tương
tự hủy phần tử sau q;
Ngược lại thông báo không có k;

73
Cấu trúc dữ liệu và giải thuật

Thủ tục cài đặt như sau:

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

o Nếu p != NULL thì p trỏ đến phần tử cần tìm


o Ngược lại thì không tìm thấy phần tử cần tìm
*Thủ tục cài đặt như sau:

f. Sắp xếp danh sách


Danh sách có thứ tự là danh sách mà các phần tử của danh sách được sắp
xếp theo một thứ tự nào đó dựa vào thành phần khóa trên toàn bộ dữ liệu.
Để sắp xếp có hai phương án:
- Hoán vị nội dung của phần tử: là phương pháp thay đổi trực tiếp thành
phần dữ liệu – Infor trong mỗi nút còn thứ tự liên kết của các nút là không
thay đổi
- Thay đổi mối liên kết của phần tử: là thực hiện thay đổi trực tiếp thành
phần Next trong mỗi nút. Vì vậy thứ tự liên kết của các nút bị thay đổi.
Tạo ra một danh sách mới là danh sách có thứ tự từ danh sách cũ (đồng thời
huỷ danh sách cũ ).
Cài đặt thuật toán sắp hoán vị nội dung của phần tử:
Thuật toán sử dụng 2 con trỏ p, q để duyệt và so sánh với nhau. Ban đầu
con trỏ p trỏ đến đầu danh sách. Con trỏ q trỏ đến phần tử sau p. So sánh
giá trị của phần tử p và q nếu không đúng trật tự thì hoán đổi giá trị của p
và q cho nhau. Quá trình trên được thực hiện lặp lại cho đến khi hết toàn bộ
dãy và thu được dãy đã sắp.

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.

Hình 2.6 Danh sách liên kết vòng đơn


Đặc điểm của danh sách liên kết vòng là các phần tử đều có vai trò như
nhau, mỗi phần tử đều có phần tử đứng trước và phần tử đứng sau.

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

Hình 2.7 Danh sách liên kết vòng đôi


Trong danh sách liên kết đôi nút đầu tiên Head và nút cuối cùng là Tail.
Để duyệt danh sách liên kết đôi ta có 2 cách: hoặc bắt đầu từ Head và dựa
vào liên kết Next để đi đến phần tử cuối hoặc bắt đầu từ phần tử cuối Tail
và dựa vào liên kết prev để đi về phần tử đầu tiên.
2.4 Bài tập có hướng dẫn
Xây dựng chương trình cho phép quản lý danh sách CANBO với các thông tin
bao gồm: Mã cán bộ, Họ tên, Lương. Cài đặt bằng danh sách liên kết đơn cho
phép: thêm cán bộ, hiện danh sách cán bộ, tìm kiếm, sắp xếp, ...
Hướng dẫn: Áp dụng các thuật toán trên danh sách liên kết đơn để triển khai.

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

2.5 Tổng kết chương


Nội dung của chương này là xem xét các vấn đề liên quan tới tổ chức và
xử lý danh sách.
Danh sách tổ chức dưới dạng mảng có ưu điểm là dễ sử dụng, tốc độ truy
cập cao. Tuy nhiên, mảng có nhược điểm là không linh hoạt về kích thước
và phức tạp khi bố trí lại các phần tử.
Danh sách liên kết là một cấu trúc dữ liệu bao gồm một tập các phần tử,
trong đó mỗi phần tử là một phần của một nút có chứa một liên kết tới nút
kế tiếp. Danh sách liên kết có kiểu truy cập tuần tự, có kích thước linh hoạt
và dễ dàng trong việc bố trí lại các phần tử.
Các thao tác cơ bản trên danh sách bao gồm: Khởi tạo danh sách, chèn
một phần tử vào đầu, cuối, giữa danh sách, xoá một phần tử khỏi đầu, cuối,
giữa danh sách, duyệt qua toàn bộ danh sách, tìm kiếm và sắp xếp.
Phần nội dung quan trọng của chương này là các thuật toán tìm kiếm
tuần tự và nhị phân trong danh sách, các thuật toán sắp xếp từ đơn giản đến
phức tạp như Selection Sort, Insertion Sort, Bubble Sort, Quick Sort,..
Ngoài danh sách liên kết đơn trong chương này còn đề cập tới một số
loại danh sách liên kết khác như danh sách vòng, danh sách liên kết kép
v.v…
2.6 Câu hỏi trắc nghiệm
Câu 1. Các trường hợp chèn thêm một phần tử mới vào danh sách liên
kết đơn gồm:
A. Chèn thêm vào đầu danh sách và vào cuối danh sách,
B. Chèn thêm vào đầu danh sách và vào sau một phần tử q đã biết,
C. Chèn thêm vào cuối danh sách và vào sau một phần tử q đã biết,
D. Chèn thêm vào đầu danh sách, vào cuối danh sách và vào sau một
phần tử q đã biết.
Câu 2. Định nghĩa cấu trúc dữ liệu của danh sách liên kết đơn được mô
tả như sau:
struct Node{

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

A. Cả hai đáp án đều đúng,


B. Cài đặt bằng mảng,
C. Cả hai đáp án đều sai,
D. Cài đặt bằng danh sách liên kết.
Câu 11. Khi cần thêm một phần tử có giá trị thành phần dữ liệu là
NewData (là một số nguyên) vào đầu của danh sách liên kết đơn dùng thuật
toán có mã giả được mô tả như dưới đây:
struct OneNode {
int Data;
Node *NextNode;
};
OneNode *SLLPointer;
SLLPointer SSList;
B1: NewNode = new OneNode;
B2: if (NewNode = NULL) thực hiện BKT;
B3: NewNode -> NextNode = NULL;
B4: NewNode -> Data = NewData;
B5: NewNode -> NextNode = SLLList;
B6: SLLList = NewNode
BKT: Kết thúc
Tìm mô tả chính xác cho bước 5 (B5)
A. Nối NewNode vào sau SLLList,
B. Chuyển vai trò đứng đầu của NewNode cho SLLList,
C.Nối SLLList vào sau NewNode,
D. Chuyển vai trò đứng đầu của SLLList cho NewNode.
Câu 12. Trong một nút của danh sách liên kết đơn, thành phần Infor là
thành phần gì?

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

B. p -> Next = q -> Next;


C. p = q;
D. q -> Next = p -> Next;
Câu 14. Chọn định nghĩa đúng nhất về hàng đợi (Queue)
A. Hàng đợi là một danh sách mà trong đó thao tác thêm 1 phần tử vào
trong danh sách được thực hiện 1 đầu này và lấy 1 phần tử trong danh sách
lại thực hiện bởi đầu kia,
B. Hàng đợi là một danh sách mà trong đó thao tác thêm 1 phần tử hay
hủy một phần tử trong danh sách được thực hiện 1 đầu,
C. Hàng đợi còn được gọi là danh sách FIFO và cấu trúc dữ liệu này còn
được gọi là cấu trúc FIFO (First In Fast Out),
D. Hàng đợi phải là một danh sách liên kết đơn.
Câu 15. Các loại danh sách liên kết gồm:
A. Danh sách liên kết đơn và danh sách liên kết kép,
B. Danh sách liên kết kép và danh sách liên kết vòng,
C. Danh sách liên kết đơn, danh sách liên kết kép và danh sách liên kết
vòng,
D. Danh sách liên kết đơn và danh sách liên kết vòng.
Câu 16. Lựa chọn câu đúng nhất về danh sách liên kết đôi (Doubly
Linked List)
A. Vùng liên kết của một phần tử trong danh sách liên đôi có 02 mối liên
kết với phần tử đầu và cuối danh sách,
B. Vùng liên kết của một phần tử trong danh sách liên đôi có 01 mối liên
kết với 02 phần tử khác trong danh sách,
C. Vùng liên kết của một phần tử trong danh sách đôi có 02 mối liên kết
với 01 phần tử trong danh sách ,
D. Vùng liên kết của một phần tử trong danh sách liên kết đôi có 02 mối
liên kết với 02 trước và sau nó trong danh sách.

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

D. Bổ sung, loại bỏ, cập nhật.


Câu 20. Đ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;
p = (Node*)malloc(sizeof(Node));
if ( p == NULL )
{
printf("Ko du bo nho");
exit(1);
}
p -> Infor = ……;
p -> Next = NULL;
return p;
}
Điền phần còn thiếu vào chỗ …………..
A. 0
B. Data
C. NULL
D. x
Câu 21. Cấu trúc dữ liệu biểu diễn hàng đợi bằng danh sách liên kết
struct QOneElement {
T Key;
QElement *Next;
};
QOneElement *QType;

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

void insertFirst ( LIST &Q, Node *new_element )


{
if ( Q.Head == NULL ) //nếu danh sách rỗng
{
[1] ……..
[2] ……..
}
else //danh sách không rỗng
{
new_element -> Next = Q.Head;
Q.Head = new_element;
}
}
Đoạn mã còn thiếu để đặt vào dòn số [1] và [2].
A. Q.Tail = NULL;
Q.Head = NULL;
B. Q.Head = Q.Head;
Q.Tail = new_element;
C. Q.Head = new_element;
Q.Tail = Q.Head;
D. Q.Tail = Q.Head;
Q.Head = new_element;
Câu 23. Có mấy loại danh sách liên kết?
A. Có 4 loại,
B. Có 2 loại,
C. Có 5 loại,
D. Có 3 loại.

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

2.7 Câu hỏi và bài tập


Câu 1. Hãy nêu các ưu và nhược điểm của danh sách liên kết so với
mảng. Nêu các bước để thêm một nút vào đầu, giữa, và cuối danh sách liên
kết đơn.
Câu 2. Nêu các bước để xoá một nút ở đầu, giữa, và cuối danh sách liên
kết đơn.
Câu 3. Cho biết kết quả chạy từng bước của khi thực hiện tìm x = 6
trong dãy số dưới đây theo giải thuật tìm kiếm nhị phân và tuần tự

3 D 6 M 9 T 12

Trong đó: D là ngày sinh, M là tháng sinh, T là tuổi


Câu 4. 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 (InsertionSort) 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 (InsertionSort).

-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

Chương 3. NGĂN XẾP – HÀNG ĐỢI


Trong chương này chúng ta sẽ tìm hiểu về hai dạng đặc biệt của danh
sách đó là Ngăn xếp và Hàng đợi. Đồng thời chúng ta cũng tìm hiểu các
ứng dụng thực tế có thể giải quyết bằng Ngăn xếp, Hàng đợi từ đó sinh
viên có thể vận dụng giải quyết bài toán thực tế.
3.1 Ngăn xếp - Stack
3.1.1 Khái niệm
Ngăn xếp là dạng danh sách đặc biệt trong đó các phép toán thêm vào
một phần tử mới hoặc loại bỏ một phần tử trong danh sách chỉ được phép
thực hiện ở một đầu của danh sách. Đầu này gọi là đỉnh của ngăn xếp. Ta
có thể hình dung ngăn xếp như một chồng đĩa, ta chỉ có thể thêm đĩa mới
lên trên cùng hoặc lấy đĩa trên cùng ra khỏi chồng mà thôi. Như vậy chiếc
đĩa đặt vào chồng sau cùng khi lấy ra sẽ được lấy đầu tiên nên ngăn xếp
còn được gọi là danh sách LIFO (Last In First Out), vào sau ra trước.
Ta xét một ví dụ minh họa sự thay đổi của ngăn xếp thông qua các thao
tác bổ sung và loại bỏ đỉnh trong ngăn xếp. Giả sử ta có một Stack S lưu trữ
các kí tự. Ban đầu, ngăn xếp ở trạng thái rỗng:
Khi thực hiện lệnh bổ xung lần lướt phần tử A - Push(S,A) và phần tử B
– Push(S,B), ngăn xếp có dạng:

Lệnh Pop(S) loại bỏ phần tử nằm trên là B ra khỏi ngăn xếp:

97
Cấu trúc dữ liệu và giải thuật

Các phép toán cơ bản trên ngăn xếp:


- InitStack(Stack): Khởi tạo Stack rỗng
- Push(Stack,Item): Đẩy một phần tử Item vào Stack
- Pop(Stack): Hủy bỏ một phần tử khỏi Stack
- Top(Stack): Xem nội dung của phần tử đầu tiên
- isEmpty(Stack): Kiểm tra Stack có rỗng hay không?
- isFull(Stack): Kiểm tra danh sách đầy hay không?
3.1.2 Cài đặt bằng mảng
Để cài đặt ngăn xếp bằng mảng, ta sử dụng mảng một chiều s để biểu
diễn ngăn xếp. Thiết lập phần tử đầu tiên của mảng, s[0], làm đáy ngăn
xếp. Các phần tử tiếp theo được đưa vào ngăn xếp sẽ lần lượt được lưu tại
các vị trí s[1], s[2], … Nếu hiện tại ngăn xếp có n phần tử thì s[n-1] sẽ là
phần tử mới nhất được đưa vào ngăn xếp. Để lưu giữ đỉnh hiện tại của ngăn
xếp, ta sử dụng một con trỏ top. Chẳng hạn, nếu ngăn xếp có n phần tử thì
top sẽ có giá trị bằng n-1. Còn khi ngăn xếp chưa có phần tử nào thì ta quy
ước top sẽ có giá trị bằng -1.
Khai báo cấu trúc một ngăn xếp bằng mảng như sau:
struct Stack
{
int top;
Data nut[max];
};
Ví dụ: ngăn xếp chứa các số nguyên tối đa gồm 100 phần tử:

98
Cấu trúc dữ liệu và giải thuật

#define max 100


struct Stack
{
int top;
int nut[max];
};
Khi đó, các thao tác trên ngăn xếp được cài đặt như sau:
*Thao tác khởi tạo ngăn xếp
Thao tác này thực hiện việc gán giá trị -1 cho biến top, cho biết ngăn xếp
đang ở trạng thái rỗng.

*Thao tác kiểm tra ngăn xếp rỗng


Nếu ngăn xếp chưa có phần tử nào được gọi là ngăn xếp rỗng. Kết quả
trả lại bằng 1 nếu ngăn xếp có phần tử và trả lại bằng 0 nếu ngăn xếp không
chứa phần tử nào.

*Thao tác kiểm tra ngăn xếp đầy


Ngăn xếp đầy là trường hợp số phần tử chứa trong ngăn xếp bằng số
phần tử tối đa cho phép lưu trữ của mảng. Thao tác trả lại giá trị bằng 1 nếu
phần tử top ở vị trí max-1 và ngược lại trả lại giá trị bằng 0.

99
Cấu trúc dữ liệu và giải thuật

*Thao tác bổ sung một phần tử vào ngăn xếp


Thực hiện bổ sung một phần tử x vào đỉnh của Stack. Phần tử mới luôn
bổ sung vào vị trí top+1. Thao tác bổ sung một phần tử chỉ được thực hiện
nếu Stack chưa đầy.

*Lấy một phần tử ra khỏi danh sách


Hủy bỏ phần tử ở vị trí top của Stack và đồng thời lưu giữ được giá trị
của phần tử vừa được lấy ra. Thao tác lấy một phần tử ra khỏi danh sách
chỉ được thực hiện nếu Stack không rỗng.

*Lấy nội dung phần tử đầu ngăn xếp


Thực hiện đưa ra giá trị của phần tử top của Stack nhưng không loại bỏ
phần tử ở vị trí top khỏi Stack. Thao tác cũng chỉ thực hiện được nếu Stack
không rỗng.

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

Kiểm tra ngăn xếp rỗng

Thêm phần tử vào danh sách


Đưa thành phần Infor vào phần tử đầu của Stack.

Hình 3.2 Minh họa thao tác Push để bổ sung phần tử vào Stack

Lấy một phần tử ra khỏi danh sách


Lấy phần tử đầu tiên khỏi danh sách và thành phần Infor được lưu lại.

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

Lấy nội dung phần tử đầu danh sách

3.1.4 Ứng dụng của ngăn xếp


a. Bài toán đảo ngược xâu ký tự
Bài toán: cho một chuỗi ký tự, yêu cầu hiển thị các ký tự của xâu theo
chiều ngược lại. Có nghĩa là, ký tự cuối hiển thị trước, …, ký tự đầu hiển
thị cuối cùng.
Ví dụ 3.1 Cho chuỗi “Stack”. Tìm chuỗi đảo ngược tương ứng
Chuỗi ban đầu: Stack,
Chuỗi đảo ngược: kcats.
Giải quyết:

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:

b. Bài toán chuyển đổi cơ số


Chuyển một số từ hệ thập phân sang hệ cơ số bất kỳ, người ta thực hiện
lấy số cần chuyển chia cho cơ số và lấy số dư. Tiếp tục lấy kết quả của
phép chia đó chia cho cơ số cho đến khi kết quả bằng không thì dừng.
Ví dụ 3.2 Chuyển 28 sang hệ cơ số 8, chuyển 72 sang cơ số 4 và chuyển
53 sang cơ số 5.
Ta có kết quả như sau:
2810 = 3.81 + 4.80 = 348
7210 = 1.43 + 0.42 + 2.41 + 0.40 = 10204
5310 = 1.25 + 1.24 + 0.23 + 1.22 + 0.21 + 1.20 = 1101012
Ví dụ 3.3 Chuyển số 3553 sang cơ số 8 bằng biểu diễn bằng Stack

104
Cấu trúc dữ liệu và giải thuật

Hình 3.4 Minh họa chuyển sang cơ số 8

Các bước thực hiện chuyển số thập phân 3553 sang hệ cơ số 8:


- Ban đầu Stack rỗng, thực hiện phép chia n = 3553 cho 8 được 444 dư
1. Đẩy phần dư là 1 vào Stack. Và n được gán lại giá trị mới chính là kết
quả của phép chia vừa thực hiện n = 444.
- Lặp lại việc chia n cho cơ số 8 ta được 55 dư 4, đẩy 4 vào Stack, n gán
bằng 55.
- Thực hiện phép chia 55 cho 8 được 6 dư 7, đẩy 7 vào Stack, n gán
bằng 6
- Tiếp tục, thực hiện lấy 6 chia 7 được 0 dư 6, n = 0 thuật toán dừng.
Stack chứa các phần từ: 1, 4, 7, 6
- Lần lượt lấy các giá trị trong Stack ra ta có cách biểu diễn số 3553
sang hệ cơ số 8 là:
67418 = 6.83 + 7.82 + 4.81 + 1.80
Thuật toán thực hiện chuyển một số sang hệ cơ số bất kì như sau:
1. Chữ số bên phải nhất của kết quả = n % b. Đẩy vào Stack,
2. Thay n = n/ b (để tìm các số tiếp theo),
3. Lặp lại bước1 – bước 2 cho đến khi n = 0,
4. Rút lần lượt các chữ số lưu trong Stack, chuyển sang dạng ký tự
tương ứng với hệ cơ số trước khi in ra kết quả.
105
Cấu trúc dữ liệu và giải thuật

3.2 Hàng đợi – Queue


3.2.1 Khái niệm
Hàng đợi là một kiểu danh sách trong đó được trang bị hai phép toán bổ
sung một phần tử vào cuối danh sách và loại bỏ một phần tử ở đầu danh
sách.
Trong cuộc sống chúng ta thường xuyên gặp hàng, ví dụ hàng người xếp
chờ mua vé xem phim. Người ta chỉ có thể đi vào hàng ở cuối hàng và
người được phục vụ đi ra khỏi hàng là người ở đầu hàng, ai vào trước sẽ
được phục vụ trước. Vì vậy hàng đợi còn gọi là danh sách FIFO (First In
First Out), vào trước ra trước.

Hình 3.5 Minh họa hàng đợi Queue

Các phép toán trên hàng:


- Khởi tạo hàng rỗng: InitQueue(Queue),
- Kiểm tra hàng rỗng: isEmpty(Queue),
- Thêm một phần tử vào hàng: Put(Queue,item),
- Lấy một phần tử ra khỏi hàng: Get(Queue).
3.2.2 Cài đặt bằng mảng
Ta có thể cài đặt hàng bởi mảng và sử dụng hai biến Head và Tail để
lưu điểm đầu và điểm cuối của hàng, đồng thời sử dụng Count để xác định
số phần tử đang có trong hàng đợi. Các phần tử của hàng đợi nằm giữa
106
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

*Kiểm tra hàng đợi rỗng

*Thêm một phần tử vào hàng

*Lấy một phần tử ra khỏi hà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

*Thao tác kiểm tra hàng đợi rỗng


Hàng đợi rỗng nếu nút đầu trỏ đến NULL.

*Thao tác thêm 1 phần tử vào hàng đợi

Để 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

3.2.4 Ứng dụng hàng đợi để biểu diễn đa thức


Cho đa thức dạng:

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:

Hình 3.6 Minh họa tính tổng hai đa thức


3.3 Tổng kết chương
Ngăn xếp là một dạng đặc biệt của danh sách mà việc bổ sung hay loại
bỏ một phần tử đều được thực hiện ở 1 đầu của danh sách. Ngăn xếp còn
được gọi là kiểu dữ liệu có nguyên tắc LIFO (Last In First Out - Vào sau ra
trước).
Ngăn xếp 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 trên ngăn xếp bao gồm: Khởi tạo ngăn xếp, kiểm tra
ngăn xếp rỗng (đầy), thêm 1 phần tử vào ngăn xếp, loại bỏ 1 phần tử khỏi
ngăn xếp.
Hàng đợi là một cấu trúc dữ liệu gần giống với ngăn xếp, nhưng phần tử
được lấy ra khỏi hàng đợi không phải là phần tử mới nhất được đưa vào mà

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

Chương 4. HÀM BĂM – BẢNG BĂM


Những nội dung được giới thiệu trong chương này gồm các nội dung
sau:
- Phép băm – hàm băm
- Các loại hàm băm
- Bảng băm
- Cách giải quyết xung đột khi thực hiện băm
4.1 Khái niệm hàm băm – phép băm
Hàm băm (hash function) là hàm biến đổi giá trị khoá của một phần tử
thành một số nguyên và dùng số nguyên này để truy xuất xác định vị trí.
Hàm băm là hàm biến đổi khoá của một phần tử thành địa chỉ trên bảng
băm. Như vậy, hàm băm là một giải thuật cho phép tạo ra các giá trị khoá
(băm) mới cho từng khối dữ liệu. Giá trị băm được dùng như là một khoá
để phân biệt các khối dữ liệu.

Hình 4.1 Hàm băm

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.

k h(k) Địa chỉ lưu Nhận xét

5 5 5

7 7 7

25 5 5 Xung đột với giá trị k = 5

49 9 9

64 4 4

19 9 9 Xung đột với giá trị k = 49

45 5 5 Xung đột với giá trị k = 5 và k = 25

4.2.2 Hàm băm sử dụng phương pháp nhân


- Dùng số dư: h(k) = k2 mod m = k2 % m
- 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
- 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 nhân với số địa chỉ m = 10 và chỉ ra sự xung đột.

k h(k) Địa chỉ lưu Nhận xét

115
Cấu trúc dữ liệu và giải thuật

5 5 5

7 9 9

25 5 5 Xung đột với giá trị k = 5

49 1 1

64 6 6

19 1 1 Xung đột với giá trị k = 49

45 5 5 Xung đột với giá trị k = 5 và k = 25

4.2.3 Hàm băm sử dụng phương pháp trích


- “Phương pháp trích m tại n” tức là lấy ra m phần tử từ vị trị thứ n của
khoá k (trong đó vị trí đầu bắt đầu từ vị trí 1)
- Ví dụ: Cho gia trị khoá k = 7985241, thực hiện băm bằng cách trích 3
tại 2
=> Lấy ra 3 phần tử từ vị trí thứ 2 nên ta có h(k) = 985
4.2.4 Hàm băm sử dụng phương tách
- “Phương pháp trích m” tức là ta chia khóa k thành những đoạn gồm m
phần tử, sau đó tính tổng các đoạn này lại và thực hiện trích lấy m giá trị
đầu và m giá trị cuối của tổng.
- Ví dụ: Cho gia trị khoá k = 7985241, thực hiện băm bằng cách tách 3
=> Thực hiện tách 3 ta được các số: 789, 524 và 1. Thực hiện tính tổng
các số ta thu được: 1323. Vậy kết quả trích 3 phần tử đầu là 132 và trích 3
phần tử cuối là 323.
4.2.5 Hàm băm sử dụng phương gập
- “Phương pháp gập m phần tử” tức là thực hiện gập khóa thành từng
đoạn gồm m phần tử, sau đó tính tổng các đoạn và thực hiện trích đầu và
trích cuối m phần tử

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

Hình 4.2 Bảng băm

Các phép toán trên bảng băm gồm:


*Khởi tạo (Initialize): Khởi tạo bảng băm, cấp phát vùng nhớ hay
qui định số phần tử (kích thước) của bảng băm
*Kiểm tra rỗng (Empty): kiểm tra bảng băm có rỗng hay không?
*Lấy kích thước của bảng băm (Size): Cho biết số phần tử hiện có
trong bảng băm
*Tìm kiếm (Search): Tìm kiếm một phần tử trong bảng băm theo khoá
k chỉ định trước.

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

& K5 = 24 => h(k5) = 24 % 10 = 4 ' Lưu tại địa chỉ 4


& K6 = 44 => h(k6) = 44 % 10 = 4 ' Lưu tại địa chỉ 4
& K7 = 98 => h(k7) = 98 % 10 = 8 ' Lưu tại địa chỉ 8
& K8 = 27 => h(k8) = 27 % 10 = 7 ' Lưu tại địa chỉ 7
Kết quả ta có bảng băm như sau:

*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

& K2 = 94 => h(k2) = 94 % 10 = 4 ' Lưu trữ tại địa chỉ 4


& K3 = 52 => h(k3) = 52 % 10 = 2 ' Xung đột với K1
– Băm lại lần 1: h(k3) = ( 2 + 1) % 10 = 3 ' Lưu trữ tại địa chỉ 3
& K4 = 76 => h(k4) = 76 %10 = 6 ' Lưu trữ tại địa chỉ 6
& K5 = 43 => h(k5) = 43 %10 = 3 ' Xung đột K3
– Băm lại lần 1: h(k5) = (3+1)%10 = 4 ' Xung đột K2
– Băm lại lần 2: h(k5) = (3+2)%10 = 5 ' Lưu trữ tại địa chỉ 5
& K6 = 79 => h(k6) = 79%10 = 9 ' Lưu trữ tại địa chỉ 9
& K7 = 19 => h(k7) = 19%9 = 9 ' Xung đột K6
– Băm lại lần 1: h(k7) = (9+1)%10 = 0 ' Lưu trữ tại địa chỉ 0
Kết quả ta có bảng băm như sau:

*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

- 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ò bậc 2 với hàm băm bằng
phương pháp chia m = 10.
& 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 + 12)%11 = 7 => Lưu trữ tại địa chỉ 7
& K7=72 => h(k7) = 72%11 = 6 ' Xung đột K2
– Băm lại lần 1: h(k7) = (6+12)%11 = 7 => Xung đột k6
– Băm lại lần 2: h(k7) = (6+22)%11=10 => Lưu trữ tại địa chỉ 10
& K8=94 => h(k8) = 94%11 = 6 ' Xung đột k2
– Băm lại lần 1: h(k8) = (6+12)%11 = 7 ' Xung đột K6
– Băm lại lần 2: h(k8) = (6+22)%11 = 10 ' Xung đột K7
– Băm lại lần 3: h(k8) = (6+32)%11 = 4 ' Xung đột K3
– Băm lại lần 4: h(k8) = (6+42)%11 = 0 ' Lưu trữ tại địa chỉ 0
& K9=14 => h(k2) = 14%11 = 3 ' Xung đột k1
– Băm lại lần 1: h(k9) = (3+12)%11 = 4 ' Xung đột K3
– Băm lại lần 2: h(k9) = (3+22)%11 = 7 ' Xung đột K6
– Băm lại lần 3: h(k9) = (3+32)%11 = 1 => lưu tại 1
Kết quả ta có bảng băm như sau:

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

– Băm lại lần 1: h(k9) = (3+3*1)%11 = 6 ' Xung đột K2


– Băm lại lần 2: h(k9) = (3+3*2)%11 = 9 ' Xung đột K6
– Băm lại lần 3: h(k9) = (3+3*3)%11 = 1 ' Xung đột K7
– Băm lại lần 4: h(k9) = (3+3*4)%11 = 4 ' Xung đột K3
– Băm lại lần 5: h(k9) = (3+3*5)%11 = 7 ' Xung đột K8
– Băm lại lần 6: h(k9) = (3+3*6)%11 = 10 ' Lưu trữ tại địa chỉ 10
Kết quả ta có bảng băm như sau:

4.4 Ứng dụng hàm băm


Hiện nay, Hash được sử dụng nhiều và rộng rãi cho các mục đích khác
nhau. Các ứng dụng cơ bản của Hash như: mật mã (cryptography), nén
(compression), tạo tổng kiểm tra (checksum generation), lập chỉ mục dữ
liệu (data index), …
*Mật mã (Che dấu dữ liệu): Hash được sử dụng cho mật mã vì thông
qua hash dữ liệu gốc được thay thế bởi một dữ liệu khác thông qua hàm
Hash. Để đảm bảo an toàn trong che dấu dữ liệu khi sử dụng Hash thì việc
lựa chọn hàm Hash tốt là hàm Hash sao cho không thể đạo ngược. Ví dụ
trong các hệ thống hiện nay cần lưu trữ mật khẩu thì hệ thống sẽ không lưu
trữ trực tiếp giá trị mật khẩu của người dùng đã đăng ký để tranh trường
hợp khi Cơ sở dữ liệu bị truy cập bởi nhóm người dùng không được phép
thì dữ liệu về tài kh oản người dùng cũng như mật khẩu không bị lộ. Mỗi
khi người dùng đăng ký tài khoản và mật khậu hệ thống sử dụng thuật toán
hash (mã hoá) để tính toán giá trị mới của mật khẩu sau mã hoá và lưu vào
cơ sở dữ liệu. Sau đó, mỗi lần người dùng đăng nhập mật khẩu mà người
dùng đã đăng ký cho tài khoản thì hệ thống sẽ lấy giá trị mã hoá được băm
tại thời điểm đó với giá trị đã mã hoá trong cơ sở dữ liệu nếu hai giá trị
băm giống nhau thì mới cho phép đăng nhập thành công, ngược lại thì khi
nhập mật khẩu không chính xác thì hệ thống sinh ra giá trị mã hoá khác vì
vậy không thể đăng nhập thành công hệ thống.

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

4.5 Tổng kết chương và câu hỏi ôn tập


4.5.1 Tổng kết chương
Bảng băm là một cấu trúc dữ liệu đặc biệt, bảng băm được sử dụng cho
các bài toán có cấu trúc dữ liệu lớn và thường lưu trữ ở bộ nhớ ngoài.
Bảng băm cho phép truy xuất nhanh đến dữ liệu và không phụ thuộc đến
kích thước của cấu trúc lưu trữ. Các phép toán khi thực hiện trên bảng băm
được đánh giá thường có bậc O(1).
Thông thường bảng băm sẽ có giá trị khoá nhỏ hơn giá trị khoá gốc
mang đi lưu trữ. Vì vậy, khi tổ chức lưu trữ các khoá giá trị trên bảng băm
có thể dẫn đến việc hai khoá được lưu trữ vào chung một giá trị. Để giải
quyết việc lưu trữ vào chung giá trị thì chúng ta sử dụng kỹ thuật giải quyết
xung đột để tìm vị trí lưu trữ mới cho khoá xung đột.
4.5.2 Câu hỏi ôn tập
1. Nêu khái niệm hàm băm và bảng băm?
2. Nêu nguyên tắc thực hiện xây dựng các bảng băm cơ bản, gồm: bảng
băm với phương pháp kết nối trực tiếp, bảng băm với pháp pháp dò tuyến
tính, bảng băm với phương pháp dò bậc 2, bảng băm với phương pháp dò
hằng số.

125
Cấu trúc dữ liệu và giải thuật

4.6 Một số câu hỏi trắc nghiệm ôn tập


Câu 1. Trong các hàm băm sau, đâu là hàm băm sử dụng phương pháp
chia?
A. h(k) = k mod m = k % m
B. h(k) = k2 mod m = k2 % m
C. h(k) = Lấy ra m phần tử từ vị trị thứ n của khoá k
D. h(k) = Chia khóa k thành những đoạn gồm m phần tử
Câu 2. Trong các hàm băm sau, đâu là hàm băm sử dụng phương pháp
nhân?
A. h(k) = k mod m = k % m
B. h(k) = k2 mod m = k2 % m
C. h(k) = Lấy ra m phần tử từ vị trị thứ n của khoá k
D. h(k) = Chia khóa k thành những đoạn gồm m phần tử
Câu 3. Hàm băm sử dụng nào sử dụng công thức sau:
h(k) = k mod m = k % m
A. Hàm băm sử dụng phương pháp trích
B. Hàm băm sử dụng phương pháp nhân
C. Hàm băm sử dụng phương pháp chia
D. Hàm băm sử dụng phương pháp tách
Câu 4. Hàm băm sử dụng nào sử dụng công thức sau:
h(k) = k2 mod m = k2 % m
A. Hàm băm sử dụng phương pháp trích
B. Hàm băm sử dụng phương pháp nhân
C. Hàm băm sử dụng phương pháp chia
D. Hàm băm sử dụng phương pháp tách
Câu 5. Cho biết địa chỉ trong bảng băm để lưu phần tử 37 bằng phương
pháp kết nối trực tiếp bằng phương pháp chia với m = 10;

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

Chương 5. DANH SÁCH PHI TUYẾN DẠNG CÂY - TREE


Trong chương này tập trung giới thiệu một cấu trúc dữ liệu gần gũi và có
nhiều ứng dụng trong thực thế đó là cấu trúc dữ liệu dạng cây.
Nội dung chính được trình bày của chương này bao gồm:
- Định nghĩa và các khái niệm cơ bản liên quan đến cây,
- Tìm hiểu các phương pháp cài đặt cây: cài đặt bằng mảng hoặc bằng
danh sách liên kết,
- Tìm hiểu về trường hợp đặc biệt là cây nhị phân – cây nhị phân tìm
kiếm,
- Các thao tác cơ bản trên cây và các ứng dụng của cây trong bài toán
thực tế.
5.1 Các khái niệm – Định nghĩa
Cây là một cấu trúc dữ liệu đóng vai trò quan trọng khi phân tích và thiết
kế các thuật toán. Việc tiến hành lưu trữ và biểu diễn kiểu cây có thể thấy
trong nhiều lĩnh vực thực tế của cuộc sống. Ví dụ một quyển sách lưu trữ
mục lục các phần của sách được phân cấp các mức khác nhau được biểu
diễn dưới dạng cây mục lục. Hoặc trong một cơ quan thì tổ chức của cơ
quan cũng được biểu diễn thông qua cấu trúc cây, các phòng ban con nằm ở
dưới phòng ban quản lý trực tiếp, các phòng ban ngang hàng nằm cùng cấp
quản lý. Tương tự, trong việc lưu trữ dữ liệu của máy tính, cách tổ chức thư
mục – tệp của hệ điều hành cũng được áp dụng kiểu lưu trữ cây. Các tệp dữ
liệu được lưu trữ trong các thư mục, các thư mục cùng mức hoặc phân cấp,
các thư mục con nằm trong các thư mục cha, …

129
Cấu trúc dữ liệu và giải thuật

Hình 5.1 Sơ đồ tổ chức trường đại học

Hình 5.2 Sơ đồ tổ chức cây thư mục trong máy tính

130
Cấu trúc dữ liệu và giải thuật

Định nghĩa cây


Cây là tập hợp các nút (Node) và các cạnh, thỏa mãn một số yêu cầu nào
đó. Mỗi một nút của cây đều có một định danh xác định duy nhất và mang
thông tin nhất định. Các cạnh dùng để liên kết các nút lại với nhau. Giữa
các nút có một quan hệ phân cấp gọi là quan hệ “cha – con”. Một đường đi
trong cây là một danh sách các đỉnh phân biệt mà đỉnh trước có liên kết với
đỉnh sau. Trong cây có đúng một đường đi nối 2 nút bất kỳ của cây.
Một tính chất quan trọng xuất hiện trong cây đó là có đúng duy nhất một
đường đi nối 2 đỉnh bất kỳ trong cây. Nếu tồn tại 2 nút nào đó trong cây mà
có nhiều hơn một đường đi nối 2 nút đó thì cấu trúc cây được chuyển sang
cấu trúc kiểu đồ thị.
Nút gốc (Root): Trong cây thường có một nút đặc biệt gọi là nút gốc, là
nút nằm ở vị trí cao nhất, tiếp theo là các nút kế tiếp. Mỗi nút đều có thể coi
là nút gốc của cây con bao gồm chính nó và các nút bên dưới nó.
Xét cây dưới:

- 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:

-Các nút 5, 6, 3, 7, 10, 11, và 9 là các nút lá của cây.


Nút nhánh(Branch): Nút không phải nút lá và không phải nút gốc được
gọi là nút nhánh.
Xét cây dưới:

- Nút 2 là nút nhánh của cây

132
Cấu trúc dữ liệu và giải thuật

- Nút 4, 8 là nút nhánh của cây


Bậc của cây: Giá trị lớn nhất của các bậc trong tất cả các nút trong cây
được gọi là bậc của cây. Ví dụ: cây trên có bậc là 3.
Mức của nút: Độ dài đường đi từ gốc đến nút x nào đó là số nhánh cần
đi qua kể từ gốc để đi đến nút x. Độ dài đường đi đến nút x xác định số
mức tương ứng của nút đó trong cây.

- Mức 0 gồm đỉnh: 1


- Mức 1 gồm đỉnh: 2, 3, 4
- Mức 2 gồm đỉnh: 5, 6, 7, 8, 9
- Mức 3 gồm đỉnh: 10, 11
Độ cao (height) của cây hay được gọi là chiều sâu (Dept) của cây là Độ
dài đường đi lớn nhất từ gốc đến các tất cả các nút lá.
Ký hiệu: h(T).
Ví dụ: Cây trên có h(T) = 3.
Để hoàn tất định nghĩa kiểu dữ liệu trừu tượng trên cây, ta phải định
nghĩa các phép toán trừu tượng cơ bản trên cây, các phép toán này được
xem là phép toán nguyên thủy áp dụng trên cây. Đối với cây có các phép
toán nguyên thủy sau:

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

Kết quả lưu trữ dưới dạng mảng như sau:

Phần tử thứ i trong mảng 1 2 3 4 5 6 7 8 9 10 11

Giá trị lưu trữ trong mảng A 0 1 1 1 2 2 4 4 4 8 8

Ví dụ 5.2 Cho cây như hình dưới đây

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:

Quy ước mã hóa

Giá trị nút của cây A B C D E F G H I J K

Giá trị mã hóa sang số 1 2 3 4 5 6 7 8 9 10 11

Kết quả lưu trữ

Phần tử thứ i trong mảng 1 2 3 4 5 6 7 8 9 10 11

Giá trị lưu trữ trong mảng 0 1 1 1 2 2 4 4 4 8 8

Giá trị thực tương ứng 0 A A A B B D D D H H

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

Danh sách nút con của cây tương ứng:

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];

5.3 Cây nhị phân – cây nhị phân tìm kiếm


Cây nhị phân là một loại cây đặc biệt mà mỗi nút trong cây chỉ có nhiều
nhất là 2 nút con. Khi đó, 2 cây con của mỗi nút được gọi là cây con trái và
cây con phải.

139
Cấu trúc dữ liệu và giải thuật

Hình 5.1: Cây nhị phân


Cây nhị phân là cây có cấu trúc đơn giản và có nhiều ứng dụng trong tin
học. Một số dạng cây phị phân đặc biệt và được ứng dụng nhiều nhất là:
Cây nhị phân suy biến: là cây nhị phân mà các nút không phải là nút lá
chỉ có một nhánh con, thường có các trường hợp sau: cây lệch trái, cây lệch
phải, cây zích zắc.

Hình 5.2: Cây nhị phân suy biến


Cây lệch trái – Cây lệch phải – Cây Ziczac

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.

Hình 5.3: Cây nhị phân đầy đủ


Cây nhị phân tìm kiếm: là cây nhị phân mà khi xét giá trị của một nút
bất kỳ đều có giá trị khóa của các nút bên trái bao giờ cũng nhỏ hơn giá trị
khóa của nút đang xét và các giá trị khóa của các nút bên phải bao giờ cũng
lớn hơn giá trị khóa của nút đang xét.

Hình 5.4: Cây nhị phân tìm kiếm


5.4 Lưu trữ cây nhị phân
5.4.1 Lưu trữ kế tiếp dưới dạng mảng
Đối với cây nhị phân đầy đủ, mỗi nút đều có đúng tối đa hai nút con ta
có thể sử dụng mảng để biểu diễn cây theo nguyên tắc:
- Nút đầu tiên (nút thứ 1) của mảng lưu trữ nút gốc,
141
Cấu trúc dữ liệu và giải thuật

- 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

Ta có kết quả mảng tương ứng như sau:

Vị trí phần tử A[1] A[2] A[3] A[4] A[5] A[6] A[7]

Giá trị lưu trữ 15 13 18 11 14 16 20

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:

Vị trí phần tử A[1] A[2] A[3] A[4] A[5] A[6] A[7]

Giá trị lưu trữ A B E C D F G

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

Được lưu trữ như sau:

Vị trí 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

Giá trị lưu trữ 1 2 3 4

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

Giá trị lưu trữ A B C D

Đố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

Ta có kết quả mảng tương ứng như sau:

144
Cấu trúc dữ liệu và giải thuật

Vị trí phần tử A[1] A[2] A[3] A[4] A[5] A[6] A[7]

15 13 18 11 14 16 20

Giá trị lưu trữ 13 11 16 NULL NULL NULL NULL

18 14 20 NULL NULL NULL NULL

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[];

5.4.2 Lưu trữ móc nối


Do đặc điểm của cây nhị phân mỗi một nút có tối đa 2 nút con, do vậy sử
dụng danh sách liên kết để cài đặt cây nhị phân là một phương pháp hữu
hiệu. Mỗi nút của cây nhị phân được lưu trữ gồm 3 phần:
- Thành phần Infor chứa thông tin của nút cần lưu trữ
- Thành phần con trỏ Left chứa địa chỉ của nút con bên trái, tức là chứa
thông tin đủ để biết nút con trái của nút đó là nút nào, trong trường hợp
không có nút con trái trường này được gán một giá trị đặc biệt, thường để
là NULL
- Thành phần con trỏ Right chứa địa chỉ của nút con bên phải, tức là
chứa thông tin đủ để biết nút con phải của nút đó là nút nào, trong trường
hợp không có nút con trái trường này được gán một giá trị đặc biệt, thường
để là NULL

145
Cấu trúc dữ liệu và giải thuật

hoặc

Hình 5.4 Sơ đồ cấu trúc một nút


Ví dụ 5.10 Cho cây như hình sau

Kết quả danh sách liên kết như sau:

Khai báo cấu trúc của một nút


struct Node
{
Data Infor;
struct Node *Left;
struct Node *Right;
};

146
Cấu trúc dữ liệu và giải thuật

Định nghĩa cây:


Node *Tree;
5.5 Biểu diễn cây tổng quát bằng cây nhị phân
Trong một số trường hợp khi có cây tổng quát để đưa về cây nhị phân ta
áp dụng nguyên tắc sau:
- Giữ lại nút con trái nhất làm nút con trái.
- Các nút con còn lại chuyển thành nút con phải.
- Trong cây nhị phân mới, con trái thể hiện quan hệ cha con và con phải
thể hiện quan hệ anh em trong cây tổng quát ban đầu.
Ví dụ 5.11 Cho cây tổng quát, áp dụng chuyển về cây nhị phân tương
ứng

Kết quả ta có:

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

Kết quả duyệt trước trên cây: 15 → 13 → 11 → 14 → 18 → 16 → 20


b. Duyệt theo thứ tự giữa – inorder traversal
Trong phương pháp duyệt theo thứ tự giữa thì giá trị trong mỗi nút bất
kỳ được liệt kê sau giá trị lưu ở nút con trái và được liệt kê trước giá trị lưu
ở nút con phải, nguyên tắc như sau: trước tiên thăm tất cả các nút bên
nhánh trái, sau đó thăm nút gốc, 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 LNR.
Thuật toán giả mã:
void DuyetGiua( Node Root)
{
if ( Root != NULL)
{
DuyetGiua( Root -> Left );
<Thăm nút Root>;
DuyetGiua ( Root -> Right );
}
}
149
Cấu trúc dữ liệu và giải thuật

Ví dụ 5.13 Cho cây

Kết quả duyệt giữa trên cây: 11 → 13 → 14 → 15 → 16 → 18 → 20


c. Duyệt theo thứ tự sau – postorder traversal
Trong phương pháp duyệt theo thứ tự sau thì giá trị trong mỗi nút bất kỳ
được liệt kê sau giá trị lưu ở nút hai nút con của nút đó, nguyên tắc như
sau: trước tiên thăm tất cả các nút bên nhánh trái, sau đó thăm tất cả các
nút bên nhánh phải, cuối cùng thăm nút gốc. Phương pháp này còn có tên
là phương pháp duyệt LRN.
Thuật toán giả mã:
void DuyetSau( Node Root)
{
if ( Root != NULL)
{
DuyetSau( Root -> Left );
DuyetSau ( Root -> Right );
<Thăm nút Root>;
}
}
Ví dụ 5.14 Cho cây

150
Cấu trúc dữ liệu và giải thuật

Kết quả duyệt giữa trên cây: 11 → 14 → 13 → 16 → 20 → 18 → 15


5.6.2 Tìm kiếm trên cây nhị phân tìm kiếm
Việc tiến hành tìm kiếm trên cây nhị phân tìm kiếm được thực hiện
tương tự như phương pháp tìm kiếm nhị phân đã trình bày trong phần
phương pháp tìm kiếm.
Ý tưởng
Để tìm một nút có khóa là x, đầu tiên tiến hành so sánh phần tử x cần tìm
với khóa của nút gốc. Nếu phần tử x có giá trị nhỏ hơn khóa của nút gốc thì
tiến hành tìm kiếm ở cây con bên trái, nếu bằng nhau thì dừng lại, và nếu
giá trị x cần tìm lớn hơn thì tiến hành tìm kiếm ở cây con phải. Quá trình
tìm kiếm trên cây con lại được lặp lại tương tự.
Tại mỗi bước, ta loại bỏ được một phần của cây mà chắc chắn không
chứa nút cần tìm. Phạm vi tìm kiếm luôn được thu hẹp lại và quá trình tìm
kiếm kết thúc khi gặp nút khóa cần tìm hoặc không có nút nào thỏa mãn.
Thuật toán
- Xuất phát từ đỉnh gốc của cây,
- So sánh phần tử x với giá trị của nút được xét. Có ba khả năng:
+ Nếu giá trị Infor == x thì kết thúc tìm kiếm và tìm thấy,
+ Nếu giá trị Infor > x thì tiếp tục tìm kiếm trên nhánh trái của nút,
+ Nếu giá trị Infor < x thì tiếp tục tìm kiếm trên nhánh phải của nút.
Ví dụ 5.15 Tìm kiếm phần tử 55 trên cây nhị phân sau

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

Hình 5.7 Minh họa thêm phần tử vào cây NPTK


Cài đặt:
void insertNode(TREE &Root, Data X)
{
if (Root != NULL)
{
if (Root->Infor == X)
break;
else if (Root->Infor > X)
insertNode( Root -> Left , X);
else
insertNode( Root->Right , X);
}
else
{
Node *p;
p = new Node();
if (p!=NULL)
{

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

5.6.4 Xóa phần tử khỏi cây


Xóa phần tử là thao tác để loại bỏ một phần tử nào đó khỏi cây nhưng
vẫn đảm bảo các phần tử còn lại trên cây thoả mãn nguyên tắc cây nhị phân
tìm kiếm.
Để thực hiện xóa phần tử khỏi cây trước khi tiên tiến hành tìm kiếm
phần tử cần xóa và đảm bảo cây sau khi xóa vẫn phải là cây nhị phân tìm
kiếm.
Để hủy phần tử x thực hiện theo các bước sau:
# Bước 1: Tìm phần tử p có giá trị x cần xóa
# Bước 2: Có các trường hợp sau:
% Nếu p là nút lá của cây thì thực hiện hủy phần tử p trực tiếp vì
không làm ảnh hưởng đến các phần tử khác trong cây.

Hình 5.8 Minh họa xóa nút lá khỏi cây NPTK


Nút 15 là nút lá trong cây
% Nếu p có một nhánh con (chỉ có nhánh trái hoặc nhánh phải) khi
đó tạo liên kết trực tiếp từ phần tử cha của nút p cần xóa đến phần tử
con của nút p, sau đó hủy p đi.

159
Cấu trúc dữ liệu và giải thuật

Hình 5.9 Minh họa xóa nút có một nhánh con


Nút 13 có một nhánh con là 15
% Nếu p có hai nhánh con (có cả nhánh trái và nhánh phải): Tìm
phần tử thế mạng cho phần tử p để thay thế lên nút p theo nguyên tắc
sau:
" Phần tử thế mạng phải nhất của nhánh bên trái của p, có nghĩa là
phần tử lớn nhất trong số các phần tử bên nhánh trái (trường hợp
1),
" Phần tử thế mạng trái nhất của nhánh bên phải của p, có nghĩa là
phần tử nhỏ nhất trong số các phần tử bên nhánh phải (trường
hợp 2).
Sau đó chép thông tin của toàn bộ phần tử thế mạng vào phần tử p.

160
Cấu trúc dữ liệu và giải thuật

Hình 5.10 Minh họa xóa nút có 2 nhánh con


Nút 18 có hai nhánh con. Chọn phần tử thay thế là lớn nhất bên nhánh
trái có giá trị 15.
5.7 Cây biểu thức
5.7.1 Định nghĩa và các cách biểu diễn biểu thức
Cây nhị phân biểu thức hay được gọi là cây biểu thức, là cây nhị phân
dùng để biểu diễn các biểu thức toán học. Trong đó, các nút lá biểu thị các
hằng số hay các biến (được gọi là toán hạng) còn các nút không là nút lá
biểu thị cho các phép toán (được gọi là toán tử). Mỗi phép toán trong một
nút sẽ tác động lên hai biểu thức con nằm ở cây con bên trái và cây con bên
phải của nút đó.

Hình 5.11 Cây biểu thức (6/2 + 3) * (7 - 4)


161
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:

Dạng tiền tố: * + / 6 2 5 – 7 4


Duyệt theo thứ tự giữa (LNR-Left Node Right), cách gọi khác là biểu
thức dạng trung tố. Trong trường hợp này toán tử được viết giữa hai toán
hạng tương ứng, tuy nhiên nếu không có dấu ngoặc biểu thức thu được hơi
mập mờ, để tránh mập mờ nên thêm các cặp dấu ‘(‘ và ‘)’ vào.
Nguyên tắc: Toán hạng 1 - Phép toán – Toán hạng 2
Ví dụ 5.19
Cho cây biểu thức:

162
Cấu trúc dữ liệu và giải thuật

Dạng trung tố: 6 / 2 + 5 * 7 – 4


Hoặc: (( 6 / 2 ) + 5 ) * ( 7 – 4 )
Duyệt theo thứ tự sau (LRN-Left Right Node), cách gọi khác là biểu
thức dạng hậu tố. Trong trường hợp này toán tử được biết sau hai toán
hạng tương ứng, người ta gọi dạng này là Ký pháp BaLan đảo ngược (RPN
– Reserve Polish Notation).
Nguyên tắc: Toán hạng 1 – Toán hạng 2 - Phép toán
Ví dụ 5.20
Cho cây biểu thức:

Dạng hậu tố: 6 2 / 5 + 7 4 - *

163
Cấu trúc dữ liệu và giải thuật

5.7.2 Chuyển từ biểu thức sang ký pháp Balan đảo ngược


Thông thường biểu thức được cho dưới dạng trung tố, tuy nhiên các thao
tác thực hiện với cây nhị phân biểu thức trên máy tính đa số sử dụng dạng
hậu tố. Để chuyển từ biểu thức dưới dạng trung tố về dạng hậu tố sử dụng:
- một Stack để lưu các phép toán, các dấu ngoặc trong quá trình xử lý,
- một mảng Balan để lưu kết quả chuyển đổi tại từng bước.
Thuật toán chuyển biểu thức từ dạng trung tố sang dạng hậu tố:
- Đọc lần lượt từng phần tử của biểu thức dạng trung tố đã có từ trái sang
phải. Có các trường hợp sau:
+ Nếu đọc được toán hạng thì bổ sung kết quả đọc được vào mảng
Balan,
+ Nếu đọc được dấu mở ngoặc thì đưa dấu mở ngoặc vào Stack,
+ Nếu đọc được toán tử (phép toán) thì:
* Lấy lần lượt các toán tử ra từ Stack đã có cho đến khi gặp toán tử
có mức ưu tiên nhỏ hơn hoặc bằng toán tử đang xét thì dừng và bổ sung
toán tử đọc được từ Stack cho vào mảng Balan,
* Bổ sung thêm toán tử được đọc đó vào Stack,
+ Nếu đọc được dấu đóng ngoặc thì:
* Lần lượt lấy các phần tử của Stack ra và bổ sung sang mảng
Balan cho đến khi gặp dấu mở ngoặc thì dừng lại,
* Đồng thời xóa dấu mở ngoặc gặp được ra khỏi Stack.
Ví dụ 5.21 Xây dựng biểu thức hậu tố cho biểu thức: ((3 + 4)*(2 - 6))

Đọc Stack Mảng Balan

( (

( ( (

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, -, *

Kết luận: Biểu thức hậu tố là 3, 4, +, 2, 6, -, *


5.7.3 Xây dựng cây nhị phân biểu thức
Các biểu thức toán học đều có thể được thể hiện dưới dạng cấu trúc cây,
trong đó các nút lá là các toán hạng và các nút còn lại là các toán tử.
Cây biểu thức được tạo bởi các nút vì vậy trước tiên phải xây dựng cấu
trúc của một nút để biểu diễn cây biểu thức. Mỗi một nút gồm ba thành
phần: giá trị (value), nhánh con trái (LeftChild) và nhánh con phải
(RightChild).
struct BinaryTreeNode
{
String value;
Struct BinaryTreeNode *LeftChild;
Struct BinaryTreeNode *RightChild;
};
a. Tạo cây biểu thức từ dạng Tiền tố (prefix) và Hậu tố (postfix)
Các chuỗi tiền tố và hậu tố không chứa các dấu ngoặc đơn nên việc xây
dựng cây nhị phân biểu thức tương ứng rất dễ dàng, các bước thực hiện
tương tự như tính toán giá trị của biểu thức. Để thực hiện thuật toán sử

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.

Đọc Mô tả Cây biểu thức

Tạo nút chứa dấu


+
“+”

Tạo nút chứa dấu


- “-” và là con phải
của dấu “+”

166
Cấu trúc dữ liệu và giải thuật

Tạo nút chứa dấu


/ “/” và là con phải
của dấu “ - ”

Tạo nút chứa số


6 “6” và làm con
phải của dấu “/”

Tạo nút chứa số


5 “5” và làm con
trái của dấu “/”

167
Cấu trúc dữ liệu và giải thuật

Tạo nút chứa “*”


* và làm con trái
của “-”

Tạo nút chứa “4”


4 và làm con phải
của “*”

Tạo nút chứa “3”


3 và làm con trái
của “*”

168
Cấu trúc dữ liệu và giải thuật

Tạo nút chứa “2”


2 và làm con trái
của “+”

Vậy cây biểu thức tương ứng của biểu thức: 2, 3, 4, *, 5, 6, /, -, +

b. Tạo cây biểu thức từ dạng trung tố (Infix)


Thông thường khi ta có một biểu thức thì biểu thức thường được cho
dưới dạng trung tố vì vậy sẽ hợp lý hơn nếu ta có thể tạo được cây biểu
thức trực tiếp từ biểu thức trung tố. Tuy nhiên chi phí để tạo cây biểu thức
từ dạng trung tố sẽ cao hơn nhiều so với từ dạng hậu tố vì khi tạo cây biểu
thức từ dạng trung tố phải tiến hành xử lý các dấu ngoặc đơn. Chính vì vậy
phương pháp xây dựng cây biểu thức từ dạng trung tố thường là sự kết hợp
giữa hai thuật toán chuyển đổi sang dạng hậu tố từ dạng trung tố và tạo cây
biểu thức cùng lúc.
Để thực hiện thuật toán này cần sử dụng hai Stack là:
- Stack OperatorStrack để chứa các toán tử của biểu thức,
169
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

Đọc Mô tả OperatorStack NodeStack

( Đẩy “(“ vào OperatorStack ( {Rỗng}

a Đẩy “a” vào NodeStack ( a

170
Cấu trúc dữ liệu và giải thuật

+ Đẩy “+” và OperatorStack (, + a

b Đẩy “b” vào trong NodeStack (, + a, b

) Lấy “a”, “b” ra tạo thành hai nút con {rỗng} +


của nút “+”. Sau đó đẩy nút “+”
vào NodeStack

* Đẩy “*” vào OperatorStack * +

c Đẩy “c” vào NodeStack * +, c

- Lấy “+” và “c” tạo thành hai nút con - *


của “*”. Sau đó đẩy “*” vào
NodeStack và đẩy “-” vào
OperatorStack

d Đẩy “d” vào NodeStack - *, d

/ Đẩy “/” vào OperatorStack -, / *, d

e Đẩy “e” vào NodeStack -, / *, d, e

Kết thúc vòng lặp -, / *, d, e

Cho “d” và “e” thành nút con của - *, /


“/”. Sau đó đẩy “/” vào NodeStack

Cho “*” và “/” làm nút con của “-”. {rỗng} -


Sau đó đẩy “-” vào NodeStack

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

Ví dụ 5.24 Xây dựng cây biểu thức từ biểu thức (a + b) * c – d/e


Biểu thức dạng hậu tố tương ứng: a, b, +, c,*, d, e, /, -

Đọ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

Vậy cây biểu thức thu được là:

5.7.4 Tính giá trị của biểu thức


Khi tính toán giá trị một biểu thức số học thì tại một thời điểm máy tính
chỉ thực hiện được phép toán với hai toán hạng, nếu biểu thức phức tạp
gồm nhiều phép toán khác nhau thì biểu thức được chia nhỏ thành các biểu
thức trung gian và tính giá trị của các biểu thức trung gian sau đó mới lấy
rồi tính giá trị tìm tiếp. Ví dụ để tính biểu thức 1 + 2 + 4 thì máy tính phải

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

Value TinhGiaTri( Root )


{
if ( Root không là phép toán )
TinhGiaTri = <Giá trị trong Root>;
else
{
x := TinhGiaTri(Root -> Left );
y := TinhGiaTri(Root -> Right );
TinhGiaTri : = x R y
}
}
Một biểu thức có 3 dạng biểu diễn khác nhau nhưng việc tính toán giá trị
biểu thức dựa trên ký pháp Balan đảo ngược là khoa học hơn cả. Trong
những năm đầu 1950, nhà logic học người BaLan đã chứng minh rằng biểu
thức hậu tố không cần có dấu ngoặc vẫn có thể tính được một cách đúng
đắn bằng cách đọc lần lượt biểu thức từ trái qua phải và dùng một Stack để
lưu kết quả trung gian.
Các bước thực hiện tính giá trị biểu thức:
Bước 1: Biểu diễn biểu thức dưới dạng ký pháp Balan đảo ngược,
Bước 2: Khởi tạo một Stack rỗng,
Bước 3: Đọc lần lượt các phần tử của biểu thức theo ký pháp Balan đào
ngược từ trái sang phải, với mỗi phần tử tìm được, kiểm tra:
" Nếu phần tử là toán hạng (giá trị cụ thể) thì đẩy giá trị vào Stack
" Nếu phần tử là phép toán R thì lấy từ Stack ra hai giá trị y và x (giá
trị lấy từ Stack đầu tiên gán cho y, giá trị lấy từ Stack thứ hai gán cho
x). Thực hình tính toán phép tính “x R y” và đưa kết quả vào Stack.
Thực hiện lặp liên tiếp bước 3 cho đến khi duyệt hết các phần tử của ký
pháp Balan đảo ngược.
Bước 4: Giá trị cuối cùng trong Stack chính là giá trị cần tính của biểu
thức
Ví dụ 5.26 Tính giá trị của biểu thức sau

176
Cấu trúc dữ liệu và giải thuật

Ký pháp Balan đào ngược(Hậu tố): 6 2 / 5 + 7 4 - *

Đọc Xử lý Stack

6 Đọc được phần tử 6 => Đẩy 6 vào Stack 6

2 Đọc được phần tử 2 => Đẩy 2 vào Stack 6, 2

/ Đọc được phép toán R = ‘/ ‘=> Lấy 2 phần tử từ Stack $ y


= 2, x = 6 $ Tính giá trị biểu thức xRy = 6/2 = 3 => Đẩy 3 3
vào Stack

5 Đọc được phần tử 5 => Đẩy 5 vào Stack 3, 5

+ Đọc được phép toán R = ‘+’ => Lấy 2 phần tử từ Stack $ y


= 5, x = 3 => Tính giá trị biểu thức xRy = 3 + 5 = 8 => Đẩy 8
8 vào Stack

7 Đọc được phần tử 7 => Đẩy 7 vào Stack 8, 7

4 Đọc được phần tử 4 => Đẩy 4 vào Strack 8,7,4

- Đọc được phép toán R = ‘- => Lấy 2 phần tử từ Stack $ y =


8, 3
4 và x = 7 $ Tính xRy = 7 – 4 = 3 => Đẩy 3 vào Stack

* Đọc được phép toán R = ‘*’ => Lấy 2 phân tử từ Stack $ y 24

177
Cấu trúc dữ liệu và giải thuật

= 3, x = 8 $ Tính xRy = 8*3 = 24 => Đẩy 24 vào Stack

Vậy biểu thức có giá trị cuối cùng là 24


5.8 Bài toán áp dụng
5.8.1 Sắp xếp danh sách theo phương pháp vun đống – Heap Sort
Heap Sort là một giải thuật đảm bảo kể cả trong trường hợp xấu nhất thì
thời gian thực hiện thuật toán cũng chỉ là O(NlogN).
Ý tưởng của thuật toán này là thực hiện sắp xếp thông qua việc tạo ra các
Heap, trong đó Heap là một cây nhị phân hoàn chỉnh có tính chất khóa ở
nút cha bao giờ cũng lớn hơn khóa ở các nút con.
Việc thực hiện giải thuật Heap Sort được chia làm 2 giai đoạn. Đầu tiên
là việc tạo ra Heap từ dãy ban đầu. Theo định nghĩa của Heap thì nút cha
bao giờ cũng lớn hơn các nút con. Do vậy, nút gốc của Heap bao giờ cũng
là phần tử lớn nhất.
Giai đoạn thứ 2 là việc sắp dãy dựa trên Heap tạo được. Do nút gốc là
nút lớn nhất nên nó sẽ được về vị trí cuối cùng của dãy và phần tử cuối
cùng sẽ được thay vào gốc của Heap. Khi đó ta có một cây mới, không phải
Heap, với số nút được bớt đi một phần tử. Lại chuyển cây này về Heap và
lặp lại quá trình cho tớ khi Heap chỉ còn 1 nút. Đó chính là phần tử bé nhất
của dãy và được đặt lên đầu.
Quá trình sắp xếp bằng thuật toán Heap Sort được thực hiện qua hai giai
đoạn
- Tạo Heap từ dãy các phần tử cho trước,
- Thực hiện sắp xếp dựa vào nguyên tắc xử lý trên Heap.
a. Tạo Heap từ mảng các phần tử cho trước
Nguyên tắc tạo Heap:
- Phần tử đầu tiên trong mảng trở thành phần tử gốc của Heap ban đầu,
- Sau đó lần lượt lấy từng phần tử của mảng bổ sung lần lượt vào nhánh
trái và nhánh phải của Heap. Mỗi khi bổ sung kiểm tra thỏa mãn tính chất
của Heap (phần tử gốc phải lớn hơn các phần tử con của Heap).

178
Cấu trúc dữ liệu và giải thuật

Ví dụ 5.27 Cho dãy số sau, tạo Heap tương ứng

35 20 52 101 9 28 56 64

Các bước thực hiện:


Đầu tiên, tạo Heap chỉ có một phần tử là 35

Bước 1: Chèn thêm 20 vào Heap (Chèn sang bên trái của Heap)

Bước 2: Chèn thêm 52 vào 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

Cây thỏa mãn định nghĩa của Heap


Bước 4: chèn thêm 09 vào Heap, chèn vào bên phải của 52

180
Cấu trúc dữ liệu và giải thuật

Cây này hoàn toàn thỏa mãn định nghĩa Heap


Bước 5: Chèn thêm 28 vào Heap, chèn vào bên trái của 35

Cây này hoàn toàn thỏa mãn định nghĩa Heap


Bước 6: chèn thêm 56 vào Heap, chèn vào bên phải của 35

Cây này vi phạm Heap, do 56 lớn hơn 35, hoán đổi 56 và 35

181
Cấu trúc dữ liệu và giải thuật

Cây mới thỏa mãn định nghĩa Heap


Bước 7: Chèn thêm 64 vào Heap, bên trái của 20

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

b. Sắp xếp trên cây Heap đã tạo


Để thực hiện sắp xếp, ta lấy phần tử đầu và là phần tử lớn nhất của cây
và thay thế bằng phần tử cuối của dãy. Thao tác này có thể làm vi phạm
định nghĩa Heap vì phần tử mới đưa lên gốc có thể nhỏ hơn một trong hai
nút con. Do đó, ta cần phải chỉnh lại Heap ngay khi có một nút nào đó nhỏ
hơn một trong hai nút con của nó. Khi đó ta tiến hành thay thế nút này cho
nút con lớn hơn. Nếu cây vẫn vi phạm định nghĩa Heap thì lặp lại quá trình
cho tới khi nó lớn hơn cả 2 nút con hoặc trở thành nút lá

183
Cấu trúc dữ liệu và giải thuật

Ví dụ 5.28 Cho cây Heap đã tạo ở trên

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ị:

*Nút có hai mục giá trị:

*Nút có ba mục giá trị

186
Cấu trúc dữ liệu và giải thuật

5.9 Ví dụ tổng hợp


Bài 1. Cho tập hợp các số nguyên sau: 53, 58, 13, 23, 43, 38, 48, 18, 8,
28.
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.
struct Node
{
int value;
struct Node *Left;
struct Node *Right;
};
Node *Root;
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.

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

Bài 2. Cho biểu thức như hình sau

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

3 Được 3 => Đẩy 3 vào Stack 3

4 Được 4 => Đẩy 4 vào Stack 3, 4

+ Được + => R= “+”, y = 4, x = 3. Tính xRy = 7 => Đẩy 7


7
vào Stack

8 Được 8 => Đẩy 8 vào Stack 7, 8

2 Được 2 => Đẩy 2 vào Stack 7, 8, 2

- Được - => R = “-”, y = 2, x = 8. Tính xRy = 6 => Đẩy 6


7, 6
vào Stack

6 Được 6 => Đẩy 6 vào Stack 7, 6, 6

* Được * => R = “*”, y = 6, x = 6. Tính xRy = 36 => Đẩy 36


7, 36
vào Stack

* Được * => R = “*”, y = 36, x = 7. Tính xRy = 252. Đẩy


252
252 vào Stack

Vậy kết quả giá trị của biểu thức là 252.


5.10 Tổng kết chương và câu hỏi ôn tập
5.10.1 Tổng kết chương
Cây là một kiểu cấu trúc dữ liệu được sử dụng rộng rãi gồm một tập hợp
các nút được liên kết với nhau theo quan hệ giữa cha và con.
Có nhiều kiểu cây khác nhau tuỳ theo mục đích sử dụng để lựa chọn phù
hợp, như: Cây tổng quát, Cây nhị phân tìm kiếm, Cây cân bằng, Cây biểu
thức, Cây 2-3-4, Cây đỏ đen, …
Để tổ chức lưu trữ cây có thể sử dụng mảng hoặc danh sách liên kết. Tuy
nhiên việc tổ chức mảng sẽ tiêu tốn vùng nhớ nhiều hơn thống thường sử
dụng danh sách liên kết để lưu trữ.
5.10.2 Câu hỏi ôn tập
1. Nêu khái niệm cây và một số tính chất của cây?
191
Cấu trúc dữ liệu và giải thuật

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

B. Là số nhánh con phải của nút đó,


C. Là số nhánh con nhỏ nhất của nút con của nút đó,
D. Là số nhánh con trái của nút đó.
Câu 4. 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
đó áp dụng phương pháp duyệt RNL thì kết quả thu được thứ tự các phần
tử là như thế nào?
A. 31, 36, 41, 32, 19, 20, 17
B. 41, 36, 32, 31, 20, 19, 17
C. 31, 19, 36, 20, 41, 17, 32
D. 41, 32, 36, 20, 17, 19, 31
Câu 5. Nút lá trong cây là nút có đặc điểm gi?
A. Là nút có số bậc lớn nhất trong cây,
B. Là nút có số bậc nhỏ nhất trong cây,
C. Là nút có số bậc bằng 0,
D. Là nút có số bậc khác 0.
Câu 6. Có mấy phương pháp để tiến hành duyệt qua các phần tử của cây
NPTK?
A. 6 phương pháp. B. 7 phương pháp,
C. 5 phương pháp, D. 4 phương pháp.
Câu 7. Đâu là phát biểu đúng cho phương pháp duyệt LNR?
A. Trước tiên thăm các nút của nhánh trái theo nguyên tắc, sau đó đến
thăm nút gốc và cuối cùng duyệt các nút của nhánh phải theo nguyên tắc,
B. Trước tiên thăm các nút của nhánh trái theo nguyên tắc, sau đó đến
thăm các nút của nhánh phải theo nguyên tắc và cuối cùng mới thăm nút
gốc,
C. Cả ba phát biểu đều đúng,
D. Trước tiên thăm nút gốc, sau đó thăm các nút của nhánh trái theo
nguyên tắc và cuối cùng duyệt các nút của nhánh phải theo nguyên tắc.
193
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

A. Là số nhánh cần đi qua để đến được nút đó từ nút lá,


B. Cả hai phát biểu đều sai,
C. Là số nhánh cần đi qua để đến được nút đó từ nút gốc,
D. Cả hai phát biểu đều đúng.
Câu 17. Phương pháp duyệt NLR là phương pháp duyệt gì?
A. Left - Node – Right,
B. Cả ba phát biểu trên đều sai,
C. Left - Right - Node ,
D. Node - Left – Right.
Câu 18. Độ cao của cây là gì?
A. Là độ dài đường đi lớn nhất từ gốc đến các nút lá,
B. Là độ dài đường đi ngắn nhất từ gốc đến các nút lá,
C. Là độ dài đường đi lớn nhất từ gốc đến các nút nhánh,
D. Là độ dài đường đi ngắn nhất từ gốc đến các nút nhánh.
Câu 19. Đâu là phát biểu đúng cho phương pháp duyệt LRN?
A. Trước tiên thăm các nút của nhánh trái theo nguyên tắc, sau đó đến
thăm nút gốc và cuối cùng duyệt các nút của nhánh phải theo nguyên tắc,
B. Trước tiên thăm các nút của nhánh trái theo nguyên tắc, sau đó đến
thăm các nút của nhánh phải theo nguyên tắc và cuối cùng mới thăm nút
gốc,
C. Cả ba phát biểu đều đúng,
D. Trước tiên thăm nút gốc, sau đó thăm các nút của nhánh trái theo
nguyên tắc và cuối cùng duyệt các nút của nhánh phải theo nguyên tắc.
Câu 20. Cho cây NPTK, Cho biết kết quả duyệt cây theo thứ tự LNR là:

196
Cấu trúc dữ liệu và giải thuật

A. 6, 8, 11, 14, 16, 30, 31, 33, 36, 46


B. 8, 6, 14, 16, 11, 33, 31, 46, 36, 30
C. 46, 36, 33, 31, 30, 16, 14, 11, 8, 6
D. 30, 11, 6, 8, 16, 14, 36, 31, 33, 46
Câu 21. Phương pháp duyệt RLN là phương pháp duyệt gì?
A. Cả ba phát biểu trên đều đúng, B. Right - Node – Left,
C. Right - Left - Node , D. Node - Right – Left.
Câu 22. 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 NRL (Node Right Left):
A. 16, 17, 18, 19, 30, 31, 32, 35, 40, 43
B. 30, 18, 17, 16, 19, 35, 32, 31, 40, 43
C. 30, 35, 40, 43, 32, 31, 18, 19, 17, 16
D. 30, 18, 35, 17, 40, 16, 32, 31, 43, 19
Câu 23. Đâu là định nghĩa của cây nhị phân tìm kiếm?
A. Cả hai phát biểu đều sai,
B. 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 nhỏ 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 lớn hơn giá trị khóa của nút
đang xé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

A. 30, 11, 6, 8, 16, 14, 36, 31, 33, 46


B. 46, 36, 33, 31, 30, 16, 14, 11, 8, 6
C. 8, 6, 14, 16, 11, 33, 31, 46, 36, 30
D. 6, 8, 11, 14, 16, 30, 31, 33, 36, 46
Câu 30. 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 )
{
[2]…………
LRN ( Root -> Right);
< Xử lý Root >;
}
}
Đoạn mã cần điền vào phần trống tại dòng [2]
A. LRN ( Root -> Left ); B. LRN ( Root -> Right );
C. RLN ( Root -> Right ); D. RLN ( Root -> Left );
Câu 31. Cây biểu là cây như thế nào?
A. 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ị toán tử (phép toán) còn các nút không là lá biểu thị cho các hằng
hay các biến (các toán hạng),

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.

Hãy chuyển cây trên về cây nhị phân tương ứng.


Bài 4. Cho biểu thức số học dưới dạng cây với các nút là các toán hạng
và các nút còn lại là phép toán (+,-,*,/).
Hãy khai báo cấu trúc dữ liệu dạng cây nhị phân với các nút có khoá là
các kí tự. In cây biểu thức theo thứ tự trước (NLR). Và tính giá trị của biểu
thức với a=2, b=1, c=2, d=3, m=1, n=2, p=1,q=1.

203
Cấu trúc dữ liệu và giải thuật

Bài 5. Cho biểu thức: ( 2 + 9*6 ) / ( 8 – 4 )


a. Hãy vẽ lại cây biểu thức,
b. Duyệt cây biểu thức theo 3 cách,
c. Tính giá trị của biểu thức.

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

d. Số nút có khóa nhỏ hơn x (giả sử T là CNPTK),


e. Số nút có khóa lớn hơn x (giả sử T là CNPTK),
f. Số nút có khóa lớn hơn x và nhỏ hơn y (T là CNPTK),
g. Chiều cao của cây,
h. In ra tất cả các nút ở tầng (mức) thứ k của cây T,
i. In ra tất cả các nút theo thứ tự từ tầng 0 đến tầng thứ h-1 của cây T (h
là chiều cao của T),
j. Kiểm tra xem T có phải là cây cân bằng hoàn toàn không,
k. Độ lệch lớn nhất trên cây. (Độ lệch của một nút là độ lệch giữa chiều
cao của cây con trái và cây con phải của nó. Độ lệch lớn nhất trên cây là độ
lệch của nút có độ lệch lớn nhất).
5.13 Bài tập có hướng dẫn
Bài 1: Cài đặt cây nhị phân tìm kiếm
#include <iostream>
using namespace std;
struct Node
{
int key;
Node *Left, *Right;
};
typedef Node *Tree; //cay
int insertNode(Tree &T, int x)
{
if (T != NULL)
{
if (T->key == x) return -1;
if (T->key > x) return insertNode(T->Left, x);
else if (T->key < x) return insertNode(T->Right, x);
}
T = (Node *) malloc(sizeof(Node));
if (T == NULL) return 0;
T->key = x;
T->Left = T->Right = NULL;
return 1;
}

205
Cấu trúc dữ liệu và giải thuật

void CreateTree(Tree &T) // nhap cay


{
int x;
while (1)
{
printf("Nhap vao Node: ");
scanf("%d", &x);
if (x == 0) break; // x = 0 thi thoat
int check = insertNode(T, x);
if (check == -1) printf("\n Node da ton tai!");
else if (check == 0) printf("\n Khong du bo nho");

}
}
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

CreateTree_mang(T); //Nhap cay

printf("\n Duyet cay :");


LRN(T);
printf("\n");

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);

if (delKey(T, x)) printf("\n Xoa thanh cong");


else printf("\n Khong tim thay key %d can xoa", x);

printf("\n Duyet cay theo LNR: \n");


LNR(T);
printf("\n");

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;
};

typedef struct node Node;


typedef Node* Tree;

void initTree(Tree &T)


{
T = NULL;
}

void insertTree(Tree &T, int x)


{
if(T == NULL)
{
Node *p = new Node;
p->Data = x;
p->nLeft = NULL;
p->nRight = NULL;
T = p;
}
else
{
if(T->Data > x)
InsertTree(T->nLeft,x);
else if(T->Data < x)
InsertTree(T->nRight,x);
}
}

//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);

printf("\n Cac phan tu cua cay - Cac phuong phap duỵet:


\n");
printTreeFull(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);

printf("\nPhan tu lon nhat trong cay la:


%d",timMax(T));

prinft("\nPhan tu nho nhat trong cay la:


%d",timMin(T));

printf("\nTong cac phan tu cua cay la: %d",Sum(T));

printf("\nNhap phan tu can them vao cay: ");


scanf("%d",&x);
insertTree(T, x);
printf("\n Cay khi them phan tu %d vao cay! \n", x);
printTreeFull(T);}

213
Cấu trúc dữ liệu và giải thuật

Chương 6. ĐỒ THỊ - GRAPH


Đồ thị được sử dụng để mô hình hóa các vấn đề bao gồm một tập hữu
hạn các đối tượng có quan hệ với nhau theo một cách nào đó. Các vấn đề
như vậy xuất hiện rất nhiều trong các lĩnh vực khác nhau: khoa học máy
tính, công nghệ điện, hóa học, chính trị, kinh tế, … Chẳng hạn, mạng
truyền thông được mô hình hóa bởi đồ thì dưới dạng như sau: biểu diễn các
thành viên trong mạng như là các đỉnh của đồ thị, nếu thành viên a có
truyền tin tức cho thành viên b thì hình thành đường biểu diễn nối giữa
thành viên a và b. Các mạng giao thông, mạng máy tính, … cũng được mô
hình hóa một cách tương tự.
Khi cấu trúc vấn đề dưới dạng đồ thị, việc giải quyết vấn đề được quy về
giải quyết liên quan trên đồ thị, chẳng hạn tìm đường đi ngắn nhất, hay là
tìm thành phần liên thông hay tìm cây bao trùm ngắn nhất, …
6.1 Các khái niệm – Định nghĩa
6.1.1 Giới thiệu
Đồ thị G bao gồm một tập hợp V các đỉnh hay nút (vertices) và tập hợp
E các cung hay cạnh (edges), ký hiệu là G = (V,E). Các đỉnh của đồ thị còn
được gọi là các nút (Node) hay điểm (Point). Các cung nối giữa hai đỉnh
lại với nhau. Hai đỉnh có cung nối với nhau được gọi là hai đỉnh kề. Mỗi
cung nối giữa hai đỉnh v và w có thể coi như là một cặp điểm (v,w). Nếu
cặp này có thứ tự thì ta có cung có thứ tự, ngược lại cung không có thứ tự.
Nếu các cung trong đồ thị G có thứ tự thì G được gọi là đồ thị có hướng,
nếu các cung trong đồ thị G không có thứ tự thì đồ thị G được gọi là đồ thị
vô hướng.
Ví dụ 6.1

214
Cấu trúc dữ liệu và giải thuật

Hình 6.1 Đồ thị có hướng

Hình 6.2 Đồ thị vô hướng


Thông thường trong đồ thị, các đỉnh biểu diễn cho một đối tượng còn các
cung biểu diễn cho mối quan hệ giữa các đối tượng đó. Chẳng hạn trong
thực tế các đỉnh có thể biểu diễn cho các thành phố và các cung biểu diễn
cho đường giao thông nối giữa các thành phố đó.
6.1.2 Các định nghĩa cơ bản
Định nghĩa 1 Đơn đồ thị vô hướng G = (V,E) là đồ thị bao gồm V là tập
các đỉnh, và E là tập các cạnh của đồ thị, với cạnh là các cặp không thứ tự
của các đỉnh phân biệt. Có nghĩa là, Giữa hai đỉnh bất kỳ, phân biệt chỉ có
duy nhất một cặp cạnh nối liền.

215
Cấu trúc dữ liệu và giải thuật

Hình 6.3 Đơn đồ thị vô hướng


Định nghĩa 2 Đa đồ thị vô hướng G = (V,E) là đồ thị bao gồm V là tập
các đỉnh, và E là tập các cạnh của đồ thị, với cạnh là các cặp không thứ tự
của các đỉnh phân biệt. Hai cạnh e1 và e2 được gọi là cạnh lặp nếu chúng
cùng tương ứng với một cặp đỉnh. Có nghĩa là, giữa hai đỉnh bất kỳ, phân
biệt có thể có nhiều hơn một cặp cạnh.

Hình 6.4 Đa đồ thị vô hướng


Định nghĩa 3 Giả đồ thị G = (V,E) là đồ thị bao gồm V là tập các đỉnh,
và E là tập các cạnh gồm hai phần tử (không nhất thiết phải khác nhau.
Cạnh e được gọi là khuyên nếu đỉnh đầu và đỉnh cuối là trùng nhau. Có
nghĩa là, tồn tại ít nhất một khuyên trong đồ thị.

216
Cấu trúc dữ liệu và giải thuật

Hình 6.5 Giả đồ thị


Định nghĩa 4 Một đơn đồ thị có hướng G = (V,A) là một đồ thị bao
gồm tập tất các đỉnh V và tập các cung A là cặp có thứ tự giữa các đỉnh
trong đồ thị. Có nghĩa là, giữa hai đỉnh tồn tại cung nối theo hướng xác
định.

Hình 6.6 Đơn đồ thị có hướng


Định nghĩa 5 Một đa đồ thị có hướng G = (V,E) là một đồ thị bao gồm
V là tập các đỉnh của đồ thị và E là tập các cạnh có thứ tự gồm hai phần tử
khác nhau gọi là cung. Hai cung e1 và e2 được gọi là lặp nếu cùng một cặp
đỉnh.

217
Cấu trúc dữ liệu và giải thuật

Hình 6.7 Đa đồ thị có hướng


6.1.3 Các thuật ngữ cơ bản
Hai đỉnh u và v của đồ thị vô hướng G được gọi là kề nhau nếu (u,v) là
một cạnh của đồ thị G. Nếu e = (u,v) là một cạnh của đồ thị thì ta nói cạnh
e liên thuộc với hai đỉnh u và v. Còn đỉnh u và đỉnh v được gọi là đỉnh đầu
và đỉnh cuối của cạnh (u,v).
Bậc của một đỉnh trong đồ thị vô hướng là số cạnh liên thuộc với đỉnh
đó, riêng khuyết tại một đỉnh được tính hai lần cho bậc của đỉnh đó. Ký
hiệu bậc của đỉnh v là deg(v).
Định lý 1 Giả sử G = (V,E) là đồ thị vô hướng với m cạnh. Khi đó tổng
số bậc của tất cả các đỉnh bằng hai lần số cạnh, có nghĩa là Σ deg(v) = 2m
Hệ quả 1 Trong đồ thị vô hướng, số đỉnh bậc lẻ (có bậc số lẻ) là một số
chẵn
Ta gọi bán bậc ra của đỉnh v trong đồ thị có hướng là số cung của đồ thị
đi ra khỏi đỉnh v, ký hiệu deg+(v). Bán bậc vào của đỉnh v trong đồ thị có
hướng là số cung của đồ thị đi vào đỉnh v, ký hiệu deg-(v).
Định lý 2 Giả sử G = (V,E) là đồ thị có hướng, với m là số cung của đồ
thị.

2m = ∑ deg+ (v) + ∑ deg− (v)


v∈V v∈V

218
Cấu trúc dữ liệu và giải thuật

6.2 Lưu trữ đồ thị


Một đồ thị có thể được lưu trữ bằng một số cấu trúc dữ liệu khác nhau.
Việc lựa chọn cấu trúc nào là tùy thuộc vào các phép toán định thực hiện
trên các cung hay trên các đỉnh của đồ thị. Hai cấu trúc thường được sử
dụng là biểu diễn bằng ma trận kề và biểu diễn bằng danh sách các đỉnh kề.
6.2.1 Lưu trữ đồ thị bằng ma trận kề - Ma trận trọng số
Cho một đồ thị G với V đỉnh và U cạnh. Trong đó V là tập hợp n đỉnh
khác nhau của đồ thị và U là tập hợp m cạnh khác nhau của đồ thị.
Khi đó, để lưu trữ đồ thị G ta dùng mảng – ma trận hai chiều, chẳng hạn
A, kiểu nguyên. Nếu đồ thị có n đỉnh thì dùng mảng A có kích thước nxn.
Giả sử các đỉnh được đánh số là 1, 2, …, n. Giá trị của ma trận được xác
định như sau:
- A[i,j] = 1 nếu có đường nối giữa đỉnh i và đỉnh j
- A[i,j] = 0 nếu không tồn tại đường nối đỉnh i và đỉnh j.
- A[i,i] = 0 với mọi đỉnh i
Rõ ràng với đồ thị không định hướng thì ma trận biểu diễn đồ thị có các
phần tử đối xứng qua đường chéo chính, nghĩa là có phần tử bằng 1 tại
hàng i cột j thì cũng nhận giá trị bằng 1 tại hàng j cột i. Còn trong đồ thị có
hướng thì không đúng như vậy.
Với mỗi đồ thị G = (V,E) thì ma trận lưu trữ của đồ thị phụ thuộc vào
thứ tự ấn định của các nút, với cách đánh số thứ tự khác nhau đối với các
đỉnh của V thì ta sẽ có được ma trận kề là khác nhau cho cùng một đồ thị
G. Vì vậy, từ một ma trận kề nào đó của đồ thị G ta cũng có thể lập được
một ma trận kề khác lưu trữ đồ thị G bằng cách đổi chỗ một số hàng và cột
tương ứng của ma trận. Chính vì thế mà vấn đề ấn định cho các nút có thể
được tùy ý khác nhau.

Ví dụ 6.2 Tìm ma trận kề lưu trữ đồ thị sau:


a. Đồ thị vô hướng:

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

Tính chất của ma trận kề:


- Đối với đồ thị vô hướng G, thì ma trận kề luôn luôn là ma trận đối
xứng
- Nếu G là đồ thị vô hướng thì trong ma trận kề A có tính chất
Tổng các số trên hàng i = Tổng các số trên cột i = Bậc của đỉnh i =
deg(i)
-Nếu G là đồ thị có hướng và A là ma trận kề thì có tính chất
Tổng các số trên hàng i = Bán bậc ra của đỉnh i = deg+(i)
Tổng các số trên cột i = Bán bậc vào của đỉnh i = deg-(i)
Ưu điểm của ma trận kề:
- Đơn giản, trực quan, dễ dàng cài đặt trên máy tính
- Để kiểm tra hai đỉnh bất kỳ trong đồ thị có kề nhau hay không chỉ cần
kiểm tra giá trị của ma trận kề tương ứng khác 0
Nhược điểm của ma trận kề:
Lãng phí bộ nhớ vì trong mọi trường hợp không quan tâm số cạnh của
đồ thị thì ma trận kề luôn luôn có kích cỡ là nxn vì thế không hiệu quả
trong biểu diễn với các đồ thị có số đỉnh lớn được
Trong trường hợp trên các cạnh của đồ thị chứa các giá trị thể hiện trong
số của cạnh thì đồ thị trở thành đồ thị trọng số và ma trận lưu trữ đồ thị
được gọi là ma trận trọng số. Khi đó A[i, j] = a thể hiện trong số của cạnh
(i,j).
Nếu đồ thị có n đỉnh thì Ma trận trọng số là mảng A hai chiều có kích
thước nxn. Giả sử các đỉnh được đánh số là 1, 2, …, n. Giá trị của ma trận
được xác định như sau:
- A[i,j] = a thể hiện trọng số giữa đỉnh i và đỉnh j
- A[i,j] = 0 hoặc ∞ nếu không tồn tại đường nối đỉnh i và đỉnh j.

Ví dụ 6.3 Tìm ma trận trọng số lưu trữ đồ thị sau:

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ề

Danh sách đỉnh kề của đồ thị như sau:

223
Cấu trúc dữ liệu và giải thuật

Hình 6.8 Danh sách đỉnh kề lưu trữ đồ thị


6.2.3 Lưu trữ đồ thị bằng danh sách cạnh
Trong trường hợp đồ thị thưa (m<6n) người ta thường lưu trữ đồ thị dưới
dạng danh sách cạnh. Sử dụng danh sách chỉ cần lưu trữ tất cả các cạnh
hiện có của đồ thị. Một cạnh e của đồ thị được lưu tương ứng với hai thành
phần điểm đầu và điểm cuối.
Ví dụ 6.5 Cho đồ thị sau, lưu trữ đồ thị dưới dạng danh sách cạnh

Danh sách cạnh để lưu trữ đồ thị như sau:

224
Cấu trúc dữ liệu và giải thuật

Hình 6.9 Danh sách cạnh lưu trữ đồ thị


6.3 Các phép duyệt đồ thị và ứng dụng
Khi biết gốc bắt đầu của một đồ thị ta có thể áp dụng phép duyệt để tiến
hành thăm tất cả các nút của đồ thị theo một thứ tự nào đó. Hiện nay, có hai
phương pháp duyệt để thăm các đỉnh của đồ thị:
- Phép duyệt theo chiều sâu (DFS – Depth First Search),
- Phép duyệt theo chiều rộng (BFS – Breadth First Search).
6.3.1 Duyệt theo chiều sâu
Thao tác duyệt theo chiều sâu đối với một đồ thị không định hướng được
thực hiện theo nguyên tắc sau:
- Giả sử đỉnh xuất phát thực hiện duyệt là đỉnh v. Tiếp theo đó một đinh
w chưa được thăm, mà là lân cận của v, sẽ được chọn và một phép tìm
kiếm theo chiều sâu xuất phát từ w lại được thực hiện tiếp,
- Khi một đỉnh u đã được “với tới” (đã được thăm) mà tất cả các đỉnh lân
cận của nó đều đã được thăm rồi, thì ta sẽ quay ngược lên đỉnh cuối cùng
vừa được thăm (mà còn có đỉnh w lân cận với nó chưa được thăm), và một
phép tìm kiếm theo chiều sâu xuất phát từ đỉnh w lại được thực hiện. Phép
tìm kiếm sẽ kết thúc khi không còn đỉnh nào chưa được thăm mà vẫn có thể
với tới được từ nút đã được thăm.
Trong khi duyệt theo chiều sâu, ta sử dụng một mảng Mark có n phần tử
để đánh dấu các đỉnh của đồ thị là đã được duyệt hay chưa.

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

6.4 Một số bài toán trên đồ thị


6.4.1 Tìm đường đi ngắn nhất và ứng dụng
Cho đồ thị G = (V,E) với w[u,v] là trọng số của cạnh (cung) nối giữa
đỉnh u với v (ký hiệu: (u,v)) và w[u,v]= ∞ nếu không tồn tại cạnh (cung)
nối giữa u với v.
Ta có v0, v1, …, vp là đường đi trên đồ thị từ đỉnh v0 đến đỉnh vp. Khi đó,
độ dài đường đi từ đỉnh v0 đến đỉnh vp bằng tổng các trọng số của các cung
(cạnh) trên đường đi từ đỉnh v0 đền đỉnh vp đó và được biểu diễn bởi
p
d (v0 , v p ) = ∑ w(v0 , v p )
i =1

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

P2[i,j] = P1[i,j] nếu D2[i,j] = D1[i,j]


2 nếu D2[i,j] = D1[i,2] + D1[2,j]
- Bước k: Tổng quát
+ Tính giá trị cho ma trận Dk theo nguyên tắc sau:
Dk[i,j] = min{ Dk-1[i,j]; Dk-1[i,k] + Dk-1[k,j]}
+ Thay đổi giá trị cho ma trận Pk theo nguyên tắc sau:
Pk[i,j] = Pk-1[i,j] nếu Dk[i,j] = Dk-1[i,j]
k nếu Dk[i,j] = Dk-1[i,k] + Dk-1[k,j]
Ví dụ 6.8 Cho đồ thị sau, áp dụng giải thuật Floyd trên đồ thị để tìm đi
ngắn nhất giữa các cặp đỉnh

Bước 0: Tạo ma trận D0 và ma trận P0


⎡ ∞ 10 6 2 ⎤ ⎡1 1 1 1⎤
⎢10 ∞ 5 3 ⎥ ⎢2 2 2 2⎥⎥
D0 = ⎢ ⎥ và P0 = ⎢
⎢ 6 5 ∞ 1⎥ ⎢3 3 3 3⎥
⎢ ⎥ ⎢ ⎥
⎣ 2 3 1 ∞⎦ ⎣4 4 4 4⎦

Bước 1: Tạo ma trận D1 và ma trận P1


⎡ ∞ 10 6 2 ⎤ ⎡1 1 1 1⎤
⎢10 ∞ 5 3 ⎥ ⎢2 2 2 2⎥⎥
D1 = ⎢ ⎥ và P1 = ⎢
⎢ 6 5 ∞ 1⎥ ⎢3 3 3 3⎥
⎢ ⎥ ⎢ ⎥
⎣ 2 3 1 ∞⎦ ⎣4 4 4 4⎦

Bước 2: Tạo ma trận D2 và ma trận P2

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⎦

Bước 3: Tạo ma trận D3 và ma trận P3


⎡ ∞ 10 6 2 ⎤ ⎡1 1 1 1⎤
⎢10 ∞ 5 3 ⎥ ⎢2 2 2 2⎥⎥
D3 = ⎢ ⎥ và P3 = ⎢
⎢ 6 5 ∞ 1⎥ ⎢3 3 3 3⎥
⎢ ⎥ ⎢ ⎥
⎣ 2 3 1 ∞⎦ ⎣4 4 4 4⎦

Bước 4: Tạo ma trận D4 và ma trận P4


⎡∞ 5 3 2 ⎤ ⎡1 4 4 1⎤
⎢5 ∞ 4 3⎥ ⎢4 2 4 2⎥⎥
D4 = ⎢ ⎥ và P4 = ⎢
⎢3 4 ∞ 1⎥ ⎢4 4 3 3⎥
⎢ ⎥ ⎢ ⎥
⎣ 2 3 1 ∞⎦ ⎣4 4 4 4⎦

b. Thuật toán Dijkstra


Ý tưởng: Thuật toán được xây dựng dựa trên cơ sở gán cho các đỉnh của
đồ thị các nhãn tạm thời. Nhãn của mỗi đỉnh cho biết cận của độ dài đường
đi ngắn nhất từ đỉnh xuất phát đến đỉnh đó. Thuật toán Dijkstra cho phép
tìm đường đi ngắn nhất từ một đỉnh đến các đỉnh còn lại của đồ thị.
Các bước thực hiện thuật toán:
*Ký hiệu:
- L(v): để chỉ giá trị nhãn của đỉnh v, tức là cận trên chiều dài đường đi
ngắn nhất từ đỉnh xuất phát s0 đến đỉnh v
- d(s0,v): chiều dài đường đi ngắn nhất từ đỉnh s0 đến v
- m(s,v): trọng số của cạnh (s,v) nào đó
*Mô tả:
Input: đồ thị G, đỉnh xuất phát s0
Output: d(s0,v) với mọi v khác s0
*Khởi động
- L(v) = ∞ với mọi v khác s0, khởi nhãn tạm thời cho các đỉnh

230
Cấu trúc dữ liệu và giải thuật

- S = rỗng, S là tập các đỉnh đã cố định nhãn


Bước 0:
d(s0,s0) = L(s0) = 0
S = {s0} //s0 đã được gán nhãn chính thức
Bước 1:
- Tính lại nhãn tạm thời L(v) cho tất cả các đỉnh v chưa nằm trong tập S
theo nguyên tắc sau: nếu đỉnh v kề với đỉnh s0 thì
L(v) = Min{ L(v), L(s0) + m(s0,v) }
- Tìm một đỉnh s1 không thuộc tập S và kề với đỉnh s0 sao cho có chỉ
số L(s1) là nhỏ nhất, sao cho:
L(s1) = Min{ L(v): ∀v∉S}
- S = S ∪ {s1} //S chứa (s0, s1)
Bước 2:
- Tính lại nhãn tạm thời L(v) với v chưa có trong tập S theo nguyên tắc:
nếu v kề với s1 thì L(v) = Min{ L(v), L(s1) + m(s1,v) }
- Tím s2 không thuộc S và kề với s1 sao cho:
L(s2) = Min{ L(v): ∀v∉S}
- S = S ∪ {s2} //S chứa (s0, s1, s2)

Bước i:
- Tính lại nhãn tạm thời L(v) với các đỉnh chưa có trong tập S theo
nguyên tắc: nếu đỉnh v kề với si-1 thì:
L(v) = Min{ L(v), L(si-1) + m(si-1,v)}
- Tìm si không thuộc S và kề với sj (j=0, i-1) sao cho:
L(si) = Min{ L(v): ∀v∉S}
- S = S ∪ {si} //S chứa (s0, s1, s2, …, si)

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

0 (a, 0)* (a, ∞) (a, ∞) (a, ∞) (a, ∞) (a, ∞)

1 - (a, 4) (a, 2)* (a, ∞) (a, ∞) (a, ∞)

2 - (c, 3)* - (c, 10) (c, 12) (a, ∞)

3 - - - (b, 8)* (c, 12) (a, ∞)

4 - - - - (d, 10)* (d, 14)

5 - - - - - (e, 13)*

Vậy đường đi ngắn nhất từ a đến z là: a → c → b → d → e → z với


tổng trọng số là 13.
232
Cấu trúc dữ liệu và giải thuật

6.4.2 Tìm cây khung nhỏ nhất và ứng dụng


Cây là đồ thị vô hướng liên thông không có chu trình. Rừng là đồ thị
gồm nhiều thành phần liên thông tách rời nhau và mỗi thành phần liên
thông của nó lại là một cây.
Cho G = (V,E) là một đơn đồ thị. Một cây được gọi là cây khung của đồ
thị G nếu nó là một đồ thị con của đồ thị G và chứa tất cả các đỉnh của G.
Ví dụ 6.10 Cho đồ thị

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ị

Các bước thực hiện:


Bước 0: xuất phát từ đỉnh u0. Đánh dấu đỉnh u0 vào cây khung

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.

Vậy ta có kết quả cây khung là T1 ={(u0, u1)}

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.

Vậy kết quả cây khung T2 = {(u0,u1), (u1,u3)}

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.

Cây khung cuối cùng là:


T5 = {(u0, u1), (u1, u3), (u3, u4), (u3, u2), (u4, u5)}

Để 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

1 - 30,(u0,u1) 10,(u1,u3)* ∞,(u0,u4) ∞,(u0,u5)


2 10,(u3,u2) - 5,(u3,u4)* 20,(u3,u5)
3 10,(u3,u2)* - - 15,(u3,u5)
4 - - - 15,(u3,u5)*
5 - - - -
Kết quả cây khung của đồ thị là:
Tmin = {(u0, u1), (u1, u3), (u3, u4), (u3, u2), (u4, u5)}

b. Thuật toán Kruskal


Ý tưởng: Tại mỗi bước chọn cạnh có trọng số nhỏ nhất của đồ thị, lần
lượt ghép thêm vào cây khung các cạnh có trọng số tối thiểu trong số các
cạnh chưa xét sao cho không tạo thành chu trình với các cạnh đã chọn. Quá
trình lặp liên tiếp khi bổ sung n-1 cạnh vào cây khung.
Để thực hiện thuật toán ta cũng sử dụng một tập Tk để chứa các cạnh
được bổ sung vào cây khung ở mỗi bước của thuật toán.
Thuật toán:
- Bước 0: Sắp xếp các cạnh theo thứ tự tăng dần của trọng số
- Bước 1: Chọn cạnh đầu tiên có trọng số nhỏ nhất trong số các cạnh
chưa xét và bổ sung cạnh đó vào cây khung
- Bước k: Xây dựng tập Tk bằng cách chọn ra cạnh chưa xét có trọng số
nhỏ nhất theo thứ tự mà không tạo thành chu trình, sau khi chọn được cạnh
thì bổ sung cạnh vào tập Tk chứa cây khung. Quá trình thực hiện lặp liên

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

Các bước thực hiện


Bước 0: Sắp xếp các cạnh theo thứ tự tăng dần của trong số
Bước 1: Chọn cạnh có trọng số nhỏ nhất (e, f) với trọng số là 1 bổ sung
vào cây khung

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.

Kết quả cây khung nhỏ nhất là:


Tmin = {(a,d), (d,g), (b,d), (b,c), (c,f), (e,f), (e,h), (h,i)}

243
Cấu trúc dữ liệu và giải thuật

6.4.3 Tìm chu trình


a. Đồ thị Euler
Chu trình đơn trong đồ thị G đi qua mỗi cạnh của đồ thị đúng một lần
được gọi là chu trình Euler. Đường đi đơn trong đồ thị G đi qua mỗi cạnh
của đồ thị đúng một lần được gọi là đường đi Euler. Đồ thị được gọi là đồ
thị Euler nếu có chu trình Euler và được gọi là đồ thị nửa Euler nếu có
đường đi Euler.

Đồ thị G1, G2, G3


- Đồ thị G1 trong hình là đồ thị Euler vì nó có chu trình Euler a, e, c, d, e,
b, a.
- Đồ thị G3 không có chu trình Euler nhưng nó có đường đi Euler a, c, d,
e, b, d, a, b, vì thế G3 là đồ thị cửa Euler.
- Đồ thị G2 không có chu trình cũng như đường đi Euler.

Hình 6.10 Minh họa đồ thị Euler (H1, H2, H3)


- Đồ thị H2 trong hình 2 là đồ thị Euler vì nó có chu trình Euler a, b, c, d,
e, a.
- Đồ thị H3 không có chu trình Euler nhưng nó có đường đi Euler c, a, b,
c, d, b vì thế H3 là đồ thị nửa Euler.

244
Cấu trúc dữ liệu và giải thuật

- Đồ thị H1 không có chu trình cũng như đường đi Euler.


Định lý (Euler). Đồ thị vô hướng liên thông G là đồ thị Euler khi và chỉ
khi mọi đỉnh của G đều có bậc chẵn.
Bổ đề. Nếu bậc của mỗi đỉnh của đồ thị G không nhỏ hơn 2 thì G chứa
chu trình.
Hệ quả. Đồ thị vô hướng liên thông G là nửa Euler khi và chỉ khi nó có
không quá 2 đỉnh bậc lẻ.
Định lý. Đồ thị có hướng liên thông yếu có chu trình Euler thì mọi đỉnh
của đồ thị có bán bậc ra bằng bán bậc vào deg+(v) = deg-(v). Ngược lại,
nếu đồ thị có hướng G liên thông yếu mà mọi đỉnh của nó có bán bậc ra
bằng bán bậc vào thì đồ thị G có chu trình Euler.
Định lý. Đồ thị có hướng liên thông yếu G = (V, E) có đường đi Euler
nhưng không có chu trình Euler nếu tồn tại đúng hai đỉnh u, v sao cho
deg+(u) - deg-(u) = deg-(v) - deg+(u) = 1
còn các đỉnh khác đều có bán bậc ra bằng bán bậc vào.
Thuật toán xác định chu trình Euler:
Xuất phát từ một đỉnh u nào đó của G ta đi theo các cạnh của nó một
cách tuỳ ý chỉ cần tuân thủ 2 qui tắc sau:
Qui tắc 1: Mỗi khi đi qua một cạnh nào đó thì xoá cạnh đó đi, sau đó xoá
đi các đỉnh cô lập nếu có của đồ thị.
Qui tắc 2: Tại mỗi bước đi không bao giờ đi qua một cầu trừ khi không
còn cách nào khác để di chuyển.
b. Đồ thị Halminton
Đường đi qua tất cả các đỉnh của đồ thị mỗi đỉnh đúng một lần được gọi
là đường đi Hamilton. Chu trình bắt đầu từ một đỉnh v nào đó qua tất cả
các đỉnh còn lại mỗi đỉnh đúng một lần rồi quay lại đỉnh v được gọi là chu
trình Hamilton. Đồ thị G gọi là đồ thị Hamilton nếu nó chứa chu trình
Hamilton, và đồ thị G gọi là nửa Hamilton nếu nó chứa đường đi
Hamilton.

245
Cấu trúc dữ liệu và giải thuật

Hình 6.11 Minh họa đồ thị Hamilton (G1, G2, G3)


- G3 là đồ thị Hamilton,
- G2 là đồ thị nửa Hamilton,
- G1 không là đồ thị nửa Hamilton và không là đồ thị Hamilton.
Cho đến nay việc tìm một tiêu chuẩn nhận biết đồ thị Hamilton vẫn còn
là mở, mặc dù đây là một vấn đề trung tâm của lý thuyết đồ thị. Hơn thế
nữa, cũng chưa có thuật toán hiệu quả để kiểm tra một đồ thị có là
Hamilton hay không. Các kết quả thu được phần lớn là điều kiện đủ để
một đồ thị là đồ thị Hamilton. Phần lớn chúng điều có dạng "nếu G có số
cạnh đủ lớn thì G là Hamilton". Một kết quả như vậy được phát biểu
trong định lý sau đây.
Định lý (Dirak 1952). Đơn đồ thị vô hướng G với n>2 đỉnh, mỗi đỉnh có
bậc không nhỏ hơn n/2 là đồ thị Hamilton.
Định lý. Giả sử G là đồ thị có hướng liên thông với n đỉnh. Nếu
deg+(v)≥n/2, deg-(v)≥n/2 với mọi v thì G là đồ thị Hamlminton.
Thuật toán xác định chu trình Halminton
Cho đồ thị G = (V,E), để tìm chu trình Hamilton cho đồ thị, thực hiện
theo 4 nguyên tắc sau:
Quy tắc 1: Nếu tồn tại một đỉnh v của đồ thị G có deg(v)≤1 thì đồ thị G
không có chu trình Halminton
Quy tắc 2: Nếu đỉnh v có bậc là 2, deg(v) = 2, thì cả hai cạnh tới v đều
phải thuộc chu trình Halminton.
Quy tắc 3: Chu trình Hamilton không chứa bất kỳ chu trình con thực sự
nào.

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:

Mã MH Tên môn học Môn học tiên


quyết

M1 Tin đại cương

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

M5 Cơ sở lập trình M1, M3

M6 Kiến trúc máy tính M1

M7 Giải tích 2 M2

M8 Anh văn 2 M4

M9 Cấu trúc dữ liệu M5, M6

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

Đỉnh được chọn ra Phần đồ thị còn lại

250
Cấu trúc dữ liệu và giải thuật

6 Hết

Như vậy dãy đưa ra là 1, 2, 3, 4, 5, 6 và dưới dạng đồ thị có thể được


biểu diễn

1 2 3 4 5 6

Hình 6.12 Đồ thị minh họa sắp xếp Tôpô


6.5 Tổng kết chương và câu hỏi ôn tập
6.5.1 Tổng kết chương
Đồ thị là một cấu trúc dữ liệu kiểu rời rạc, bao gồm các đỉnh và các cạnh
nối giữa các đỉnh với nhau. Đồ thị hiện nay được ứng dụng rộng rãi trong
nhiều lĩnh vực khác nhau của khoa học máy tính.

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

A. In ma trận trọng số của đồ thị


B. In ma trận kề của đồ thị
C. In danh sách cạnh của đồ thị
D. In danh sách kề của đồ thị
Câu 2: Cho đồ thị vô hướng có 5 đỉnh với tổng bậc các đỉnh là 10. Vậy số
số cạnh của đồ thị là bao nhiêu
A. 3 B. 5
C. 4 D. 6
Câu 3: 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];
};
Đâu là đoạn mã để in ma trận trọng số lưu trữ đồ thị
A.
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");
}
}
B.
void XuLy(DoThi &G)
{
int dd,dc;
int i,j;
float ts;
printf("Nhap so dinh do thi:");
253
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:

Cho biết ma trận trọng số lưu trữ đồ thị trên là gì?


254
Cấu trúc dữ liệu và giải thuật

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

Câu 5: Cây là đồ thị vô hướng liên thông ….


A. Không có chu trình
B. Không có đỉnh cô lập
C. Không có cạnh cầu
D. Không có đỉnh treo
Câu 6: Giả sử T = <V,E> là đồ thị n đỉnh. Khẳng định nào không tương
đương với các khẳng định còn lại
A. T có đúng một chu trình n-1 cạnh
B. T liên thông và mỗi cạnh của nó đều là cầu
C. T liên thông và có đúng n-1 cạnh
D. T liên thông không có chu trình
Câu 7: Cho G =<V,E> là đồ thị vô hướng liên thông n đỉnh. T =<V, H>
được gọi là cây khung của đồ thị nếu:
A. T liên thông và có đúng n-1 cạnh
B. T liên thông và mỗi cạnh của nó đều là cầu;
C. T liên thông không có chu trình và H E
D. T liên thông và không có chu trình.
Câu 8: Ma trận nào dưới đây lưu trữ đúng của đồ thị trong số đã
cho trong hình vẽ
256
Cấu trúc dữ liệu và giải thuật

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

a. Lập ma trận kề,


b. Lập ma trận liên thuộc,
c. Lập ma trận trọng số.
Bài 2. Áp dụng thuật toán Dijkstra tìm đường đi ngắn nhất trong các đồ
thị được cho dưới đây
a. Từ đỉnh a đến đỉnh e,

b. Từ đỉnh a đến đỉnh i,

258
Cấu trúc dữ liệu và giải thuật

c. Từ đỉnh a đến đỉnh n (Mô tả chi tiết theo bảng),

d. Từ đỉnh a đến đỉnh s. theo thuật toán Dijkstra,

e. Từ đỉnh a đến đỉnh k (Mô tả chi tiết theo bảng).

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 ⎥⎦

2. Đồ thị được cho dưới hình vẽ sau:

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

6.8 Bài tập có hướng dẫn:


Bài 1: Chương trình lưu trữ đồ thị dưới dạng Ma trận trọng số
261
Cấu trúc dữ liệu và giải thuật

#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];
};

void nhapDoThi(DoThiCanh &G)


{
int d,c;
int i;
float ts;
printf("Nhap so canh cua do thi:");
263
Cấu trúc dữ liệu và giải thuật

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;
}
}

//In do thi duoi dang danch canh


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)
{
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);

264
Cấu trúc dữ liệu và giải thuật

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;
for(i=1; i<=G.m; i++)
{
GG.C[G.ds[i].dd][G.ds[i].dc] = G.ds[i].ts;
GG.C[G.ds[i].dc][G.ds[i].dd] = G.ds[i].ts;
}
}
void lietKeDinhKe_DSCanh(DoThiCanh G,int k)
{
int i;
printf("\n Cac dinh ke cua dinh %d la:",k);
for(i=1;i<=G.m;i++)
{
if(G.ds[i].dd == k)
printf("%7d",G.ds[i].dc);
if(G.ds[i].dc == k)
printf("%7d",G.ds[i].dd);
}
}

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;

for(i=1; i<=G.m; i++)


{
GG.C[G.ds[i].dd][G.ds[i].dc] = G.ds[i].ts;
GG.C[G.ds[i].dc][G.ds[i].dd] = G.ds[i].ts;
}
}
void lietKeDinhKe_DSCanh(DoThiCanh G,int k)
{
int i;
printf("\n Cac dinh ke cua dinh %d la:",k);
for(i=1;i<=G.m;i++)
{
if(G.ds[i].dd == k)
printf("%7d",G.ds[i].dc);
if(G.ds[i].dc == k)
printf("%7d",G.ds[i].dd);
}

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

You might also like