You are on page 1of 65

GIỚI THIỆU MÔN HỌC

Tóm tắt nội dung:


Bài 1: Danh sách liên kết
Bài 2: Một số phương pháp sắp xếp
Bài 3: Hàm băm
Bài 4: Cây, cây nhị phân, cây nhị phân tìm kiếm, cây cân bằng
Bài 5: Cây đỏ đen
Bài 6: B-cây, cây 2-3-4
Bài 7: Các đống nhị thức
Bài 8: Các đống Fibonaci
Bài 9: Các tập rời nhau
Bài 10: Các thuật toán so khớp chuỗi

Tài liệu tham khảo:


1) Data Structures, Algorithms, and Object-Oriented Programming. NXB McGraw
Hill; Tác giả Gregory Heilleman -1996
2) Advanced Data Structures. NXB McGraw Hill - 1990; Tác giả Thomas H. C.,
Charles E.L., and Ronald L.R.
3) Giáo trình thuật toán. NXB Thống kế 2002. Nhóm Ngọc Anh Thư dịch
4) Algorithms and Data Structures in C++; Tác giả Alan Parker

Bài 1: Danh sách liên kết


I) Danh sách liên kết đơn

1. Tổ chức danh sách đơn

Danh sách liên kết bao gồm các phần tử. Mỗi phần tử của danh sách đơn là một cấu trúc
chứa 2 thông tin :
- Thành phần dữ liệu: lưu trữ các thông tin về bản thân phần tử .
- Thành phần mối liên kết: lưu trữ địa chỉ của phần tử kế tiếp trong danh sách, hoặc lưu trữ
giá trị NULL nếu là phần tử cuối danh sách.

Ta có định nghĩa tổng quát

typedef struct tagNode


{
Data Info; // Data là kiểu đã định nghĩa trước
Struct tagNode* pNext;

1
// con trỏ chỉ đến cấu trúc node
}NODE;

Ví dụ : Ðịnh nghĩa danh sách đơn lưu trữ hồ sơ sinh viên:

typedef struct SinhVien //Data


{ char Ten[30];
int MaSV;
}SV;

typedef struct SinhvienNode


{ SV Info;
struct SinhvienNode* pNext;
}SVNode;

Các phần tử trong danh sách sẽ được cấp phát động. Biết phần tử đầu tiên ta sẽ truy
xuất được các phần tử tiếp theo. Thường sử dụng con trỏ Head để lưu trữ địa chỉ đầu tiên
của danh sách.

Ta có khai báo:
NODE *pHead;

Để quản lý địa chỉ cuối cùng trong danh sách ta dùng con trỏ TAIL. Khai báo như sau:

NODE *pTail;

VD:

II. Các thao tác cơ bản trên danh sách đơn

Giả sử có các định nghĩa:


typedef struct tagNode
{
Data Info;
struct tagNode* pNext;
}NODE;
typedef struct tagList
{
NODE* pHead;
NODE* pTail;
2
}LIST;

NODE *new_ele // giữ địa chỉ của một phần tử mới được tạo
Data x; // lưu thông tin về một phần tử sẽ được tạo
LIST lst; // lưu trữ địa chỉ đầu, địa chỉ cuối của danh sách liên kết

1.Chèn một phần tử vào danh sách:

Có 3 loại thao tác chèn new_ele vào xâu:


Cách 1: Chèn vào đầu danh sách

Thuật toán :
Bắt đầu:
Nếu Danh sách rỗng Thì
B11 : pHead = new_ele;
B12 : pTail = pHead;
Ngược lại
B21 : new_ele ->pNext = pHead;
B22 : pHead = new_ele ;
Cài đặt:

Cách 2: Chèn vào cuối danh sách

Thuật toán :
Bắt đầu :
Nếu Danh sách rỗng thì
B11 : pHead = new_elelment;
B12 : pTail = pHead;
Ngược lại
B21 : pTail ->pNext = new_ele;
B22 : pTail = new_ele ;

Cách 3 : Chèn vào danh sách sau một phần tử q

3
Thuật toán :
Bắt đầu :
Nếu ( q != NULL) thì
B1 : new_ele -> pNext = q->pNext;
B2 : q->pNext = new_ele ;
Cài đặt :

2. Tìm một phần tử trong danh sách đơn

Thuật toán :
Bước 1:
p = pHead; //Cho p trỏ đến phần tử đầu danh sách
Bước 2:
Trong khi (p != NULL) và (p->Info != k ) thực hiện:
p:=p->pNext;// Cho p trỏ tới phần tử kế
Bước 3:
Nếu p != NULL thì p trỏ tới phần tử cần tìm
Ngược lại: không có phần tử cần tìm.
Cài đặt :

3. Hủy một phần tử khỏi danh sách

Hủy phần tử đầu xâu:

Thuật toán :
Bắt đầu:
Nếu (pHead != NULL) thì
B1: p = pHead; // p là phần tử cần hủy
B2:
B21 : pHead = pHead->pNext; // tách p ra khỏi xâu
B22 : free(p); // Hủy biến động do p trỏ đến
B3: Nếu pHead=NULL thì pTail = NULL; //Xâu rỗng

4
Hủy một phần tử đứng sau phần tử q

Thuật toán :
Bắt đầu:
Nếu (q!= NULL) thì
B1: p = q->Next; // p là phần tử cần hủy
B2: Nếu (p != NULL) thì// q không phải là cuối xâu
B21 : q->Next = p->Next; // tách p ra khỏi xâu
B22 : free(p); // Hủy biến động do p trỏ đến

Hủy 1 phần tử có khoá 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 (p!= NULL) thì // tìm thấy k
Hủy p ra khỏi xâu tương tự hủy phần tử sau q;
Ngược lại
Báo không có k;

4. Thăm các nút trên danh sách

- Ðếm các phần tử của danh sách,


- Tìm tất cả các phần tử thoả điều kiện,
- Huỷ toàn bộ danh sách (và giải phóng bộ nhớ)

Thuật toán xử lý các nút trên danh sách:


Bước 1:
p = pHead; //Cho p trỏ đến phần tử đầu danh sách
Bước 2:
Trong khi (Danh sách chưa hết) thực hiện
B21 : Xử lý phần tử p;
B22 : p:=p->pNext; // Cho p trỏ tới phần tử kế

Thuật toán hủy toàn bộ danh sách:


Bước 1:
Trong khi (Danh sách chưa hết) thực hiện
5
B11:
p = pHead;
pHead:=pHead->pNext; // Cho p trỏ tới phần tử kế
B12:
Hủy p;
Bước 2:
Tail = NULL; //Bảo đảm tính nhất quán khi xâu rỗng

6
II. Danh sách liên kết kép

Là danh sách mà mỗi phần tử trong danh sách có kết nối với 1 phần tử đứng trước và 1
phần tử đứng sau nó.

Khai báo:
typedef struct tagDNode
{
Data Info;
struct tagDNode* pPre; // trỏ đến phần tử đứng trước
struct tagDNode* pNext; // trỏ đến phần tử đứng sau
}DNODE;

typedef struct tagDList


{
DNODE* pHead; // trỏ đến phần tử đầu danh sách
DNODE* pTail; // trỏ đến phần tử cuối danh sách
}DLIST;

1. Chèn một phần tử vào danh sách:


Có 4 loại thao tác chèn new_ele vào danh sách:

Cách 1: Chèn vào đầu danh sách

Cài đặt :
Cách 2: Chèn vào cuối danh sách

Cài đặt :
Cách 3 : Chèn vào danh sách sau một phần tử q

7
Cài đặt :

Cách 4 : Chèn vào danh sách trước một phần tử q

Cài đặt :
2. Hủy một phần tử khỏi danh sách
- Hủy phần tử đầu xâu
- Hủy phần tử cuối xâu
- Hủy một phần tử đứng sau phần tử q
- Hủy một phần tử đứng trước phần tử q
- Hủy 1 phần tử có khoá k
3. Xử lý các nút trên danh sách:
- Tìm nút có khóa k
- Hiển thị giá trị khóa của các nút trong danh sách
- Hủy tòan bộ danh sách

8
III. Ngăn xếp (stack)
Stack chứa các đối tượng làm việc theo cơ chế LIFO (Last In First Out) nghĩa là việc
thêm một đối tượng vào stack hoặc lấy một đối tượng ra khỏi stack được thực hiện theo cơ
chế "Vào sau ra trước".
Thao tác thêm 1 đối tượng vào stack thường được gọi là "Push".
Thao tác lấy 1 đối tượng ra khỏi stack gọi là "Pop".
Trong tin học, CTDL stack có nhiều ứng dụng: khử đệ qui, lưu vết các quá trình tìm
kiếm theo chiều sâu và quay lui, ứng dụng trong các bài toán tính toán biểu thức, .

Một hình ảnh một stack

Các thao tác


Push(o): Thêm đối tượng o vào đầu stack
Pop(): Lấy đối tượng ở đỉnh stack ra khỏi stack và trả về giá trị của nó. Nếu stack
rỗng thì lỗi sẽ xảy ra.
isEmpty(): Kiểm tra xem stack có rỗng không.
Top(): Trả về giá trị của phần tử nằm ở đầu stack mà không hủy nó khỏi stack.
Nếu stack rỗng thì lỗi sẽ xảy ra.

Biểu diễn Stack dùng mảng


Ta có thể tạo một stack bằng cách khai báo một mảng 1 chiều với kích thước tối đa là
N (ví dụ, N có thể bằng 1000).

VD:

Tạo stack S và quản lý đỉnh stack bằng biến t – chỉ số của phần từ trên cùng trong
stack:
Data S [N];
int t;

Biểu diễn Stack dùng danh sách liên kết đơn


9
VD:
LIST S;
Các thao tác:
Tạo Stack S rỗng (S.pHead=l.pTail= NULL sẽ tạo ra một Stack S rỗng)
Kiểm tra stack rỗng: int IsEmpty(LIST &S)
Thêm một phần tử p vào stack S:void Push(LIST &S, Data x)
Trích huỷ phần tử ở đỉnh stack S: Data Pop(LIST &S)
Xem thông tin của phần tử ở đỉnh stack S: Data Top(LIST &S)

Ứng dụng của Stack:


Biến đổi biểu thức:
Dạng trung tố Dạng hậu tố
a+b ab+
a*b ab*
a*(b+c)-d/e abc+*de-/
Tính giá trị của biểu thức ở dạng hậu tố.

IV. Hàng đợi ( Queue)


Hàng đợi chứa các đối tượng làm việc theo cơ chế FIFO (First In First Out) nghĩa là
việc thêm một đối tượng vào hàng đợi hoặc lấy một đối tượng ra khỏi hàng đợi được thực
hiện theo cơ chế "Vào trước ra trước".

Hàng đội

Các thao tác:


EnQueue(o): Thêm đối tượng o vào cuối hàng đợi
DeQueue(): Lấy đối tượng ở đầu queue ra khỏi hàng đợi và trả về giá trị của nó.
Nếu hàng đợi rỗng thì lỗi sẽ xảy ra.
IsEmpty(): Kiểm tra xem hàng đợi có rỗng không.
Front(): Trả về giá trị của phần tử nằm ở đầu hàng đợi mà không hủy nó. Nếu hàng
đợi rỗng thì lỗi sẽ xảy ra.

Biểu diễn dùng mảng:


Ta có thể tạo một hàng đợi bằng cách sử dụng một mảng 1 chiều với kích thước tối đa
là N (ví dụ, N có thể bằng 1000) theo kiểu xoay vòng (coi phần tử an-1 kề với phần tử a0).
Ta ký hiệu nó là NULLDATA như ở những phần trước.

10
Trạng thái hàng đợi lúc bình thường:

Q – biến hàng đợi, f quản lý đầu hàng đợi, r quản lý phần tử cuối hàng đợi.
Trạng thái hàng đợi lúc xoay vòng (mảng rỗng ở giữa):

Câu hỏi đặt ra: khi giá trị f=r cho ta điều gì ? Ta thấy rằng, lúc này hàng đợi chỉ có thể
ở một trong hai trạng thái là rỗng hoặc đầy.

Hàng đợi có thể được khai báo cụ thể như sau:


Data Q[N] ;
int f, r;

Dùng danh sách liên kết


Ta có thể tạo một hàng đợi bằng cách sử dụng một danh sách liên kết đơn.
LIST Q;

Các thao tác:


Tạo hàng đợi rỗng: Lệnh Q.pHead = Q.pTail = NULL sẽ tạo ra một hàng đợi rỗng.
-Kiểm tra hàng đợi rỗng :
int IsEmpty(LIST Q)
- Thêm một phần tử p vào cuối hàng đợi :
void EnQueue(LIST Q, Data x)
- Trích/Hủy phần tử ở đầu hàng đợi:
Data DeQueue(LIST Q)
- Xem thông tin của phần tử ở đầu hàng đợi :
Data Front(LIST Q)
Ứng dụng của hàng đợi
- Bài toán quản lý tồn kho
- Bài toán xử lý các lệnh trong máy tính điện tử.

Bài tập:

Bài 2: Một số phương pháp sắp xếp

I. Thuật toán sắp xếp nhanh - Quick Sort


Ý tưởng:
11
Có dãy số: a1, a2, ..., an
Giải thuật QuickSort làm việc như sau:
Chọn x là một phần tử làm biên: thường chọn là phần tử ở giữa dãy số.
Phân hoạc dãy thành 3 dãy con
1. ak <= x , với k = 1..i
2. ak = x , với k = i..j
3. ak > =x , với k = j..N

Ak<=x Ak=x Ak>=x

Nếu số phần tử trong dãy con 1, 3 lớn hơn 1 thì ta tiếp tục phân hoạch dãy 1, 3 theo
phương pháp trên. Ngược lại thì: dừng.

Giải thuật phân hoạch dãy am, am+1, ., an thành 2 dãy con:
Bước 1 : Chọn tùy ý một phần tử a[k] trong dãy là giá trị biên, m<= k <=n:
x = a[k]; i = m; j = n;
Bước 2 : Phát hiện và hiệu chỉnh cặp phần tử a[i], a[j] nằm sai vị trí:
Bước 2a : Trong khi (a[i]<x) i++;
Bước 2b : Trong khi (a[j]>x) j--;
Bước 2c : Nếu i<= j
// a[i]>= x; a[j]<=x mà a[j] đứng sau a[i]
Hoán vị (a[i],a[j]);
i++;
j--;
Bước 3 :
Nếu i <= j: Lặp lại Bước 2.//chưa xét hết mảng
Ngược lại: Dừng

Có thể phát biểu giải thuật sắp xếp QuickSort một cách đệ qui như sau :

Bước 1 : Phân hoạch dãy am … an thành các dãy con :


- Dãy con 1 : am.. aj <= x
- Dãy con 2 : aj+1.. ai-1 = x
- Dãy con 1 : ai.. an >= x
Bước 2 :
Nếu ( m < j ) // dãy con 1 có nhiều hơn 1 phần tử
Phân hoạch dãy am.. aj
Nếu ( i < n ) // dãy con 3 có nhiều hơn 1 phần tử
Phân hoạch dãy ai.. ar
Ví dụ:
Cho dãy số a:
12 2 8 5 1 6 4 15

12
Phân hoạch đoạn l =1, r = 8: x = A[4] =5

Phân hoạch đoạn l =1, r =

3: x = A[2] = 2 Phân hoạch


đoạn l = 5, r = 8: x = A[6] = 6

Phân hoạch đoạn l = 7, r =

8: x = A[7] = 6
Dừng.
Cài đặt

13
Ðánh giá giải thuật
Hiệu qủa thực hiện của giải thuật QuickSort phụ thuộc vào việc chọn giá trị mốc.
Trường hợp tốt nhất xảy ra nếu mỗi lần phân hoạch đều chọn được phần tử median
(phần tử lớn hơn (hay bằng) nửa số phần tử, và nhỏ hơn (hay bằng) nửa số phần tử còn lại)
làm mốc, khi đó dãy được phân chia thành 2 phần bằng nhau và cần log2(n) bước phân
hoạch thì sắp xếp xong.
Nhưng nếu mỗi bước phân hoạch phần tử được chọn có giá trị cực đại (hay cực tiểu) là
mốc, dãy sẽ bị phân chia thành 2 phần không đều: một phần chỉ có 1 phần tử, phần còn lại
gồm (n-1) phần tử, do vậy cần thực hiện n bước phân hoạch mới sắp xếp xong. Ta có bảng
tổng kết
Trường hợp Ðộ phức tạp
Tốt nhất n*log(n)
Xấu nhất n2

II. Radix sort

Ý tưởng:
Khác với các thuật toán trước, Radix sort là một thuật toán tiếp cận theo một hướng
hoàn toàn khác. Nếu như trong các thuật toán khác, cơ sở để sắp xếp luôn là việc so sánh giá
trị của 2 phần tử thì Radix sort lại dựa trên nguyên tắc phân loại thư của bưu điện.
Ta biết rằng, để đưa một khối lượng thư lớn đến tay người nhận ở nhiều địa phương
khác nhau, bưu điện thường tổ chức một hệ thống phân loại thư phân cấp:
Trước tiên, các thư đến cùng một tỉnh, thành phố sẽ được sắp chung vào một lô để gửi
đến tỉnh thành tương ứng.
Bưu điện các tỉnh thành này lại thực hiện công việc tương tự. Các thư đến cùng một
quận, huyện sẽ được xếp vào chung một lô và gửi đến quận, huyện tương ứng. Cứ như vậy,
các bức thư sẽ được trao đến tay người nhận một cách có hệ thông mà công việc sằp xếp thư
không quá nặng nhọc.

Mô phỏng lại qui trình trên, để sắp xếp dãy a1, a2, ..., an, giải thuật Radix Sort thực
hiện như sau:
Trước tiên, ta có thể giả sử mỗi phần tử ai trong dãy: a1, a2, ..., an là một số nguyên có
tối đa m chữ số.
Ta phân loại các phần tử lần lượt theo các chữ số hàng đơn vị, hàng chục, hàng trăm, .
tương tự việc phân loại thư theo tỉnh thành, quận huyện, phường xã, ..

Các bước thực hiện thuật toán như sau:


Bước 1 : // k cho biết chữ số dùng để phân loại hiện hành
k = 0; // k = 0: hàng đơn vị; k = 1:hàng chục;
Bước 2 : //Tạo các lô chứa các loại phần tử khác nhau
14
Khởi tạo 10 lô B0, B1, ., B9 rỗng;
Bước 3 :
For i = 1 .. n do
Ðặt ai vào lô Bt với t = chữ số thứ k của ai;
Bước 4 :
Nối B0, B1, ., B9 lại (theo đúng trình tự) thành a.
Bước 5 :
k = k+1;
Nếu k < m thì trở lại bước 2.
Ngược lại: Dừng

Ví dụ
Cho dãy số a:
701 1725 999 9170 3252 4518 7009 1424 428 1239 8425 7013
Phân lô theo hàng đơn vị:
12 0701
11 1725
10 0999
9 9170
8 3252
7 4518
6 7009
5 1424
4 0428
3 1239 0999
2 8425 1725 4518 7009
1 7013 9170 0701 3252 7013 1424 8425 0428 1239
CS A 0 1 2 3 4 5 6 7 8 9
Các lô B dùng để phân loại

Phân lô theo hàng chục:


12 0999
11 7009
10 1239
9 4518
8 0428
7 1725
6 8425
5 1424
4 7013 0428
3 3252 1725

15
2 0701 7009 4518 8425
1 9170 0701 7013 1424 1239 3252 9170 0999
CS A 0 1 2 3 4 5 6 7 8 9
Phân lô theo hàng trăm:
12 0999
11 9170
10 3252
9 1239
8 0428
7 1725
6 8425
5 1424
4 4518
3 7013 0428
2 7009 7013 3252 8425 1725
1 0701 7009 9170 1239 1424 4518 0701 0999
CS A 0 1 2 3 4 5 6 7 8 9
Phân lô theo hàng ngàn:
12 0999
11 1725
10 0701
9 4518
8 0428
7 8425
6 1424
5 3252
4 1239
3 9170 0999 1725
2 7013 0701 1424 7013
1 7009 0428 1239 3252 4518 7009 8425 9170
CS A 0 1 2 3 4 5 6 7 8 9
Lấy các phần tử từ các lô B0, B1, ., B9 nối lại thành a:
12 9170
11 8425
10 7013
9 7009
8 4518
7 3252
6 1725
5 1424
4 1239
16
3 0999
2 0701
1 0428
CS A 0 1 2 3 4 5 6 7 8 9

Ðánh giá giải thuật


Với một dãy n số, mỗi số có tối đa m chữ số, thuật toán thực hiện m lần các thao tác
phân lô và ghép lô. Trong thao tác phân lô, mỗi phần tử chỉ được xét đúng một lần, khi ghép
cũng vậy. Như vậy, chi phí cho việc thực hiện thuật toán hiển nhiên là O(2mn) = O(n).
NHẬN XÉT
Thuật toán không có trường hợp xấu nhất và tốt nhất. Mọi dãy số đều được sắp với chi phí
như nhau nếu chúng có cùng số phần tử và các khóa có cùng chiều dài.
Thuật toán cài đặt thuận tiện với các mảng có khóa sắp xếp là chuỗi (ký tự hay số) hơn là
khóa số như trong ví dụ do tránh được chi phí lấy các chữ số của từng số.
Tuy nhiên, số lượng lô nhiều (10 khi dùng số thập phân, 26 khi dùng chuỗi ký tự tiếng
anh, ...) nhưng tổng kích thước của tất cả các lô chỉ bằng dãy ban đầu nên ta không thể dùng
mảng để biểu diễn B (B0->B9). Như vậy, phải dùng cấu trúc dữ liệu động để biểu diễn B =>
Radix sort rất thích hợp cho sắp xếp trên danh sách liên kết.
Khi sắp các dãy không nhiều phần tử, thuật toán Radix sort sẽ mất ưu thế so với các
thuật toán khác.
III. Sắp xếp cây - Heap sort
1.Ý tưởng:
Nhận xét: Khi tìm phần tử nhỏ nhất ở bước i, phương pháp sắp xếp chọn trực tiếp
không tận dụng được các thông tin đã có được do các phép so sánh ở bước i-1.
Vì lý do trên người ta tìm cách xây dựng một thuật toán sắp xếp có thể khắc phục
nhược điểm này.
Mấu chôt để giải quyết vấn đề vừa nêu là phải tìm ra được một cấu trúc dữ liệu cho
phép tích lũy các thông tin về sự so sánh giá trị các phần tử trong qua trình sắp xếp.
Giả sử dữ liệu cần sắp xếp là dãy số : 5 2 6 4 8 1 được bố trí theo quan hệ so sánh và
tạo thành sơ đồ dạng cây như sau :

Trong đó một phần tử ở mức i chính là phần tử lớn trong cặp phần tử ở mức i+1, do
đó phần tử ở mức 0 (nút gốc của cây) luôn là phần tử lớn nhất của dãy.
Nếu loại bỏ phần tử gốc ra khỏi cây (nghĩa là đưa phần tử lớn nhất về đúng vị trí), thì
việc cập nhật cây chỉ xảy ra trên những nhánh liên quan đến phần tử mới loại bỏ, còn các

17
nhánh khác được bảo toàn, nghĩa là bước kế tiếp có thể sử dụng lại các kết quả so sánh ở
bước hiện tại.
Trong ví dụ trên ta có :

Loại bỏ 8 ra khỏi cây và thế vào các chỗ trống giá trị -∞ để tiện việc cập nhật lại cây :

Tiến hành nhiều lần việc loại bỏ phần tử gốc của cây cho đến khi tất cả các phần tử của
cây đều là -∞, khi đó xếp các phần tử theo thứ tự loại bỏ trên cây sẽ có dãy đã sắp xếp. Trên
đây là ý tưởng của giải thuật sắp xếp cây.

2. Cấu trúc dữ liệu Heap


Tuy nhiên, để cài đặt thuật toán này một cách hiệu quả, cần phải tổ chức một cấu trúc
lưu trữ dữ liệu có khả năng thể hiện được quan hệ của các phần tử trong cây với n ô nhớ
thay vì 2n-1 như trong ví dụ.
Khái niệm heap và phương pháp sắp xếp Heapsort do J.Williams đề xuất đã giải quyết
được các khó khăn trên.
Ðịnh nghĩa Heap:
Giả sử xét trường hợp sắp xếp tăng dần, khi đó Heap được định nghĩa là một dãy các
phần tử ap, a2 ,... , aq thoả các quan hệ sau với mọi i thuộc [p, q]:
1/. ai >= a2i
ai >= a2i+1
2/.
{(ai , a2i), (ai ,a2i+1) là các cặp phần tử liên đới }
Heap có các tính chất sau :
Tính chất 1 : Nếu ap , a2 ,... , aq là một heap thì khi cắt bỏ một số phần tử ở hai đầu
của heap, dãy con còn lại vẫn là một heap.
Tính chất 2 : Nếu ap , a2 ,... , aq là một heap thì phần tử a1 (đầu heap) luôn là phần tử
lớn nhất trong heap.

18
Tính chất 3 : Mọi dãy ap , a2 ,... , aq, dãy con aj, aj+1,…, ar tạo thành một heap với
j=(q div 2 +1).

Giải thuật Heapsort :


Giải thuật Heapsort trải qua 2 giai đoạn :
Giai đoạn 1 :Hiệu chỉnh dãy số ban đầu thành heap;
Giai đoạn 2: Sắp xếp dãy số dựa trên heap:
Bước 1: Ðưa phần tử lớn nhất về vị trí đúng ở cuối dãy:
r = n; Hoánvị (a1 , ar );
Bước 2: Loại bỏ phần tử lớn nhất ra khỏi heap: r = r-1;
Hiệu chỉnh phần còn lại của dãy từ a1 , a2 ... ar thành một heap.
Bước 3: Nếu r>1 (heap còn phần tử ): Lặp lại Bước 2
Ngược lại : Dừng

Dựa trên tính chất 3, ta có thể thực hiện giai đoạn 1 bằng cách bắt đầu từ heap mặc
nhiên an/2+1 , an/2+2 ... an, sau đó thêm dần các phần tử an/2, an/2-1, ., a1 ta sẽ nhân được
heap theo mong muốn.
Ví dụ
Cho dãy số a:
12 2 8 5 1 6 4 15
Giai đoạn 1: hiệu chỉnh dãy ban đầu thành heap

19
Giai đoạn 2: Sắp xếp dãy số dựa trên heap :

20
thực hiện tương tự cho r=5,4,3,2 ta được:

Cài đặt

Ðánh giá giải thuật

Trong giai đoạn sắp xếp ta cần thực hiện n-1 bước mỗi bước cần nhiều nhất là log2(n-1),
log2(n-2), … 1 phép đổi chỗi.
Như vậy độ phức tạp thuật toán Heap sort O(nlog2n)

Bài 3: BẢNG BĂM (HASH TABLE)

Phép băm được đề xuất và hiện thực trên máy tính từ những năm 50 của thế kỷ 20. Nó
dựa trên ý tưởng: biến đổi giá trị khóa thành một số (xử lý băm) và sử dụng số này để đánh
chỉ cho bảng dữ liệu.
21
Các phép toán trên các cấu trúc dữ liệu như danh sách, cây nhị phân,… phần lớn được
thực hiện bằng cách so sánh các phần tử của cấu trúc, do vậy thời gian truy xuất không
nhanh và phụ thuộc vào kích thước của cấu trúc.
Trong bài này chúng ta sẽ khảo sát một cấu trúc dữ liệu mới được gọi là bảng băm
(hash table). Các phép toán trên bảng băm sẽ giúp hạn chế số lần so sánh, và vì vậy sẽ cố
gắng giảm thiểu được thời gian truy xuất. Độ phức tạp của các phép toán trên bảng băm
thường có bậc là 0(1) và không phụ thuộc vào kích thước của bảng băm.
Các khái niệm chính trên cấu trúc bảng băm:
· Phép băm hay hàm băm (hash function)
· Tập khoá của các phần tử trên bảng băm
· Tập địa chỉ trên bảng băm
· Phép toán thêm phần tử vào bảng băm
· Phép toán xoá một phần tử trên bảng băm
· Phép toán tìm kiếm trên bảng băm
Thông thường bảng băm được sử dụng khi cần xử lý các bài toán có dữ liệu lớn và
được lưu trữ ở bộ nhớ ngoài.

22
1. PHÉP BĂM (Hash Function)
Định nghĩa:
Trong hầu hết các ứng dụng, khoá được dùng như một phương thức để truy xuất dữ
liệu. Hàm băm được dùng để ánh xạ giá trị khóa khoá vào một dãy các địa chỉ của bảng băm
(hình 1).

Hình 1
Khóa có thể là dạng số hay số dạng chuỗi. Giả sử có 2 khóa phân biệt k i và kj nếu
h(ki)=h(kj) thì hàm băm bị đụng độ.
Một hàm băm tốt phải thỏa mãn các điều kiện sau:
Tính toán nhanh.
Các khoá được phân bố đều trong bảng.
Ít xảy ra đụng độ.
Xử lý được các loại khóa có kiểu dữ liệu khác nhau
Hàm Băm sử dụng Phương pháp chia
Dùng số dư: h(k) = k mod m
k là khoá, m là kích thước của bảng.
Như vậy h(k) sẽ nhận: 0,1,2,…,m-1.
Việc chọn m sẽ ảnh hưởng đến h(k).
Nếu chọn m=2p thì giá trị của h(k) sẽ là p bit cuối cùng của k trong biểu diễn nhị phân.
Nếu chọn m=10p thì giá trị của h(k) sẽ là p chữ số cuối cùng trong biểu diễn thập phân
của k.
Trong 2 ví dụ trên giá trị h(k) không phụ thuộc đầy đủ vào khóa k mà chỉ phụ thuộc
vào p bít (p chữ số) cuối cùng trong khóa k. Tốt nhất ta nên chọn m sao cho h(k) phụ thuộc
đầy đủ và khóa k. Thông thường chọn m là số nguyên tố.
VD: Bảng băm có 4000 mục, chọn m = 4093
Hàm Băm sử dụng Phương pháp nhân
h(k) = m*(k*A mod 1) 
k là khóa, m là kích thước bảng, A là hằng số: 0 < A < 1
Chọn m và A
Theo Knuth thì chọn A bằng giá trị sau:
A=( 5 -1)/2=0.6180339887…
m thường chọn m = 2p
VD: k=123456; m=10000
H(k)= 10000 (123456* 0.6180339887 mod 1) 
H(k)= 10000 (76300.0041089472 mod 1) 
H(k)= 10000 (0.0041089472) 
H(k)=41
Phép băm phổ quát (unisersal hashing)
Việc chọn hàm băm không tốt có thể dẫn đến xác suất đụng độ cao.
23
Giải pháp:
- Lựa chọn hàm băm h ngẫu nhiên.
- Khởi tạo một tập các hàm băm H phổ quát và từ đó h được chọn ngẫu nhiên.
Cho H là một tập hợp hữu hạn các hàm băm: ánh xạ các khóa k từ tập khóa U vào
miền giá trị {0,1,2,…, m-1}. Tập H là phổ quát nếu với mọi ∀ f ∈ H và 2 khoá phân biệt
k1,k2 ta có xác suất: Pr{f(k1) = f(k2)} <= 1/m

2. BẢNG BĂM (Hash Table - Direct-address table)


Phần này sẽ trình bày các vấn đề chính:
- Mô tả cấu trúc bảng băm tổng quát (thông qua hàm băm, tập khóa, tập địa chỉ)
- Các phép toán trên bảng băm như thêm phần tử (insert), loại bỏ (remove), tìm kiếm
(search), …

a. Mô tả dữ liệu

Tập khóa K Hàm băm Tập địa chỉ M

Giả sử
· K: tập các khoá (set of keys)
· M: tập các dịa chỉ (set of addresses).
· h(k): hàm băm dùng để ánh xạ một khoá k từ tập các khoá K thành một địa chỉ tương ứng
trong tập M.
b. Các phép toán trên bảng bă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.
· 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ó.
· Xử lý các khóa trong bảng băm (Traverse): xử lý toàn bộ khóa trong bảng băm theo thứ tự
địa chỉ từ nhỏ đến lớn.
Các Bảng băm thông dụng:

24
Với mỗi loại bảng băm cần thiết phải xác định tập khóa K, xác định tập địa chỉ M và
xây dựng hàm băm h cho phù hợp.
*) Bảng băm với phương pháp kết nối trực tiếp: mỗi địa chỉ của bảng băm tương ứng
một danh sách liên kết. Các phần tử bị xung đột được kết nối với nhau trên một danh sách
liên kết.
*) Bảng băm với phương pháp kết nối hợp nhất: bảng băm này được cài đặt bằng danh
sách kề, mỗi phần tử có hai trường: trường key chứa khóa của phần tử và trường next chỉ
phần tử kế bị xung đột. Các phần tử bị xung đột được kết nối nhau qua trường kết nối next.
*) Bảng băm với phương pháp dò tuần tự: Khi thêm phần tử vào bảng băm nếu bị đụng
độ thì sẽ dò địa chỉ kế tiếp… cho đến khi gặp địa chỉ trống đầu tiên thì thêm phần tử vào địa
chỉ này.
*) Bảng băm với phương pháp dò bậc hai: ví dụ khi thêm phần tử vào bảng băm này,
nếu băm lần đầu bị xung đột thì sẽ dò đến địa chi mới, ở lần dò thứ i sẽ xét phần tử cách i 2
cho đến khi gặp địa chỉ trống đầu tiên thì thêm phần tử vào địa chỉ này.
*) Bảng băm với phương pháp băm kép: bảng băm này dùng hai hàm băm khác nhau,
băm lần đầu với hàm băm thứ nhất nếu bị xung đột thì xét địa chỉ khác bằng hàm băm thứ
hai.
Ưu điểm của các Bảng băm:
Bảng băm là một cấu trúc dung hòa giữa thời gian truy xuất và dung lượng bộ nhớ:
- Nếu không có sự giới hạn về bộ nhớ thì chúng ta có thể xây dựng bảng băm với mỗi
khóa ứng với một địa chỉ với mong muốn thời gian truy xuất tức thời.
- Nếu dung lượng bộ nhớ có giới hạn thì tổ chức một số khóa có cùng địa chỉ, khi đó
tốc độ truy xuất sẽ giảm.
Bảng băm dược ứng dụng nhiều trong thực tế, rất thích hợp khi tổ chức dữ liệu có kích
thước lớn và được lưu trữ ở bộ nhớ ngoài.

3. Các phương pháp tránh xảy ra đụng độ


2.4.1. Bảng băm với phương pháp kết nối trực tiếp (Direct chaining Method)
Bảng băm được cài đặt bằng các danh sách liên kết, các phần tử trên bảng băm được
“băm” thành M danh sách liên kết (từ danh sách 0 đến danh sách M–1). Các phần tử bị xung
đột tại địa chỉ i được kết nối trực tiếp với nhau qua danh sách liên kết i. Chẳng hạn, với
M=10, các phần tử có hàng đơn vị là 9 sẽ được băm vào danh sách liên kết i = 9.
Khi thêm một phần tử có khóa k vào bảng băm, hàm băm f(k) sẽ xác định địa chỉ i
trong khoảng từ 0 đến M-1 ứng với danh sách liên kết i mà phần tử này sẽ được thêm vào.
Khi tìm một phần tử có khóa k vào bảng băm, hàm băm f(k) cũng sẽ xác định địa chỉ i
trong khoảng từ 0 đến M-1 ứng với danh sách liên kết i có thể chứa phần tử này. Như vậy,
việc tìm kiếm phần tử trên bảng băm sẽ được qui về bài toán tìm kiếm một phần tử trên danh
sách liên kết.
Để minh họa ta xét bảng băm có cấu trúc như sau:
- Tập khóa K: tập số tự nhiên
- Tập địa chỉ M: gồm 10 địa chỉ (M={0, 1, …, 9}
- Hàm băm h(key) = key % 10.

25
30, 50,60,11,21,31,…

Hình 1.6. bảng băm với


phương pháp kết nối trực tiếp
Hình trên minh họa bảng băm
vừa mô tả. Theo hình vẽ, bảng băm đã
"băm" phần tử trong tập khoá K theo
10 danh sách liên kết khác nhau, mỗi danh sách liên kết gọi là một bucket:
· Bucket 0 gồm những phần tử có khóa tận cùng bằng 0.
· Bucket i(i=0 | … | 9) gồm những phần tử có khóa tận cùng bằng i.
· Khi khởi động bảng băm, con trỏ đầu của các bucket là NULL.
Theo cấu trúc này, với tác vụ insert, hàm băm h(k) sẽ được dùng để tính địa chỉ của
khoá k, tức là xác định bucket chứa phần tử và đặt phần tử cần chèn vào bucket này.
Với tác vụ search, hàm băm sẽ được dùng để tính địa chỉ và tìm phần tử trên bucket
tương ứng
+ i=h(k) => thuoc danh sach thu I (bucket[i]
+ tim kiem khoa K tren danh sach bucket[i]
Cài đặt bảng băm dùng phương pháp kết nối trực tiếp :
a. Khai báo cấu trúc bảng băm:
#define M 100
struct nodes
{ int key;
struct nodes *next };
typedef struct nodes *NODEPTR; //khai bao kieu con tro chi nut
/*khai bao mang bucket chua M con tro dau cua Mbucket */
NODEPTR bucket[M];
BT: xay dung bang bam theo PP ket noi truc tiep
b.Các phép toán:
26
- Tính giá trị hàm băm: Giả sử chúng ta chọn hàm băm dạng %: h(key)=key % M.
- Phép toán initbuckets: khởi tạo các bucket băng Null.
- Phép toán emmptybucket(b): kiểm tra bucket b có bị rỗng không?
- Phép toán emmpty: Kiểm tra bảng băm có rỗng không?
- Phép toán insert: Thêm phần tử có khóa k vào bảng băm.
+ i=h(k)
+ ktra bucket [i]: neu rong =>cc o nho cho bucket, gan khoa k
them phan tu co khoa k vao ds theo thu tu tang dan.
- Phép toán remove: Xóa phần tử có khóa k trong bảng băm.
- Phép toán clear: Xóa tất cả các phần tử trong bảng băm.
- Phép toán traversebucket: Xử lý tất cả các phần tử trong bucket b.
- Phép toán traverse: Xử lý tất cả các phần tử trong bảng băm.
- Phép toán search: Tìm kiếm một phần tử trong bảng băm, nếu không tìm thấy hàm này
trả về hàm NULL, nếu tìm thấy hàm này trả về địa chỉ của phần tử có khóa k.
B1: Tìm danh sách liên kết có thể chứa khóa k
b = h(k); p = bucket[b];
B2: Tìm khóa k trong danh sách liên kết p.
Nhận xét bảng băm dùng phương pháp kết nối trực tiếp:
Bảng băm dùng phương pháp kết nối trực tiếp sẽ "băm” n phần tử vào danh sách liên
kết (M bucket).
Để tốc độ thực hiện các phép toán trên bảng hiệu quả thì cần chọn hàm băm sao cho
băm đều n phần tử của bảng băm cho M bucket, lúc này trung bình mỗi bucket sẽ có n/M
phần tử. Chẳng hạn, phép toán search sẽ thực hiện việc tìm kiếm tuần tự trên bucket nên thời
gian tìm kiếm lúc này có bậc 0(n/M) – nghĩa là, nhanh gấp M lần so với việc tìm kiếm trên
một danh sách liên kết có n phần tử.
Nếu chọn M càng lớn thì tốc độ thực hiện các phép toán trên bảng băm càng nhanh,
tuy nhiên lại càng dùng nhiều bộ nhớ. Do vậy, cần điều chỉnh M để dung hòa giữa tốc độ
truy xuất và dung lượng bộ nhớ.
· Nếu chọn M=n thì năng xuất tương đương với truy xuất trên mảng (có bậc O(1)), tuy nhiên
tốn nhiều bộ nhớ.
2.4.2. Bảng băm với phương pháp kết nối hợp nhất
Mô tả:
- Cấu trúc dữ liệu: Tương tự như trong trường hợp cài đặt bằng phương pháp kết nối trực
tiếp, bảng băm trong trường hợp này được cài đặt bằng danh sách liên kết dùng mảng, có M
phần tử. Các phần tử bị xung đột tại một địa chỉ được kết nối nhau qua một danh sách liên
kết. Mỗi phần tử của bảng băm gồm hai trường:
· Trường key: chứa khóa của mỗi phần tử
· Trường next: con trỏ chỉ đến phần tử kế tiếp nếu có xung đột.
- Khởi động: Khi khởi động, tất cả trường key của các phần tử trong bảng băm được gán bởi
giá trị NullKey, còn tất cả các trường next được gán –1.
- Thêm mới một phần tử: Khi thêm mới một phần tử có khóa key vào bảng băm, hàm băm
hkey) sẽ xác định địa chỉ i trong khoảng từ 0 đến M-1.

27
· Nếu chưa bị xung đột thì thêm phần tử mới vào địa chỉ này.
· Nếu bị xung đột thì phần tử mới được cấp phát là phần tử trống phía cuối mảng. Cập
nhật liên kết next sao cho các phần tử bị xung đột hình thành một danh sách liên kết.
- Tìm kiếm: Khi tìm kiếm một phần tử có khóa key trong bảng băm, hàm băm h(key) sẽ giúp
giới hạn phạm vi tìm kiếm bằng cách xác định địa chỉ i trong khoảng từ 0 đến M-1, và việc
tìm kiếm phần tử khóa có khoá key trong danh sách liên kết sẽ xuất phát từ địa chỉ i.

Để minh họa cho bảng băm với phương pháp kết nối hợp nhất, xét ví dụ sau:
Giả sử, khảo sát bảng băm có cấu trúc như sau:
- Tập khóa K: tập số tự nhiên
- Tập địa chỉ M: gồm 10 địa chỉ (M={0, 1, …, 9}
- Hàm băm f(key) = key % 10.
VD:
Key : 11 12 21 1 13
Hash: 1 2 1 1 3
Add Key Next Add Key Next
0 NullKey -1 0 NullKey -1
1 NullKey -1 1 11 9
… NullKey -1 2 12 -1
M-1 NullKey -1 3 13 -1
… NullKey -1
8 1 -1
9 21 8

Khai báo cấu trúc bảng băm:


#define NULLKEY –1
#define M 100
typedef struct node
{
int key; //khoa cua nut tren bang bam
int next; //con tro chi nut ke tiep khi co xung dot
} NODE;
NODE hashtable[M]; //Khai bao bang bam
Cài đặt bảng băm dùng phương pháp kết nối hợp nhất:
2.4.3. Bảng băm với phương pháp dò tuần tự
Mô tả:
- Cấu trúc dữ liệu: Bảng băm trong trường hợp này được cài đặt bằng danh sách kề có M
phần tử, mỗi phần tử của bảng băm là một mẫu tin có một trường key để chứa khoá của
phần tử. Khi khởi động bảng băm thì tất cả trường key được gán NullKey;
- Khi thêm phần tử có khoá key vào bảng băm, hàm băm h(key) sẽ xác định địa chỉ i trong
khoảng từ 0 đến M-1:
· Nếu chưa bị xung đột thì thêm phần tử mới vào địa chỉ này.
28
· Nếu bị xung đột thì hàm băm lại lần 1, hàm h1 sẽ xét địa chỉ kế tiếp, nếu lại bị xung
đột thì hàm băm thì hàm băm lại lần 2, hàm h2 sẽ xét địa chỉ kế tiếp nữa, …, và quá trình cứ
thế cho đến khi nào tìm được địa chỉ trống và thêm phần tử mới vào địa chỉ này.
- Khi tìm một phần tử có khoá key trong bảng băm, hàm băm h(key) sẽ xác định địa
chỉ i trong khoảng từ 0 đến M-1, tìm phần tử khoá key trong bảng băm xuất phát từ địa chỉ
i.
Hàm băm lại lần i được biểu diễn bằng công thức sau:
f(key)=(f(key)+i) %M với f(key) là hàm băm chính của bảng băm.
Lưu ý địa chỉ dò tìm kế tiếp là địa chỉ 0 nếu đã dò đến cuối bảng.
Giả sử, khảo sát bảng băm có cấu trúc như sau:
- Tập khóa K: tập số tự nhiên
- Tập địa chỉ M: gồm 10 địa chỉ (M={0, 1, …, 9}
- Hàm băm h(key) = key % 10.

29
Hình thể hiện thêm các nut 32, 53, 22, 92, 17, 34, 24, 37, 56 vào bảng băm.
0 NULL 0 NULL 0 NULL 0 NULL 0 56
1 NULL 1 NULL 1 NULL 1 NULL 1 NULL
2 32 2 32 2 32 2 32 2 32
3 53 3 53 3 53 3 53 3 53
4 NULL 4 22 4 22 4 22 4 22
5 NULL 5 92 5 92 5 92 5 92
6 NULL 6 NULL 6 34 6 34 6 34
7 NULL 7 NULL 7 17 7 17 7 17
8 NULL 8 NULL 8 NULL 8 24 8 24
9 NULL 9 NULL 9 NULL 9 37 9 37

Khai báo cấu trúc bảng băm:


#define NULLKEY –1
#define M 100
struct node
{
int key; //khoa cua nut tren bang bam
};
struct node hashtable[M]; //Khai bao bang bam co M nut
Cài đặt bảng băm dùng phương pháp dò tuyến tính:

2.4.4. Bảng băm với phương pháp dò bậc hai


Mô tả:
- Bảng băm trong trường hợp này được cài đặt bằng danh sách kề có M phần tử, mỗi phần tử
của bảng băm là một mẫu tin có một trường key để chứa khóa các phần tử.
- Khi khởi động bảng băm thì tất cả trường key bị gán NULLKEY.

Khi thêm phần tử có khóa key vào bảng băm, hàm băm h(key) sẽ xác định địa chỉ i trong
khoảng từ 0 đến M-1.
· Nếu chưa bị xung đột thì thêm phần tử mới vào địa chỉ i này.
· Nếu bị xung đột thì hàm băm lại lần 1 h1 sẽ xét địa chỉ cách i là 1 2, nếu lại bị xung
đột thì hàm băm lại lần 2 h2 sẽ xét địa chỉ cách i 22 ,… , quá trình cứ thế cho đến khi nào
tìm được trống và thêm phần tử vào địa chỉ này.

30
- Khi tìm kiếm một phần tử có khóa key trong bảng băm thì xét phần tử tại địa chỉ i=f(key),
nếu chưa tìm thấy thì xét phần tử cách i 12, 22, …, quá trình cứ thế cho đến khi tìm được
khóa (trường hợp tìm thấy) hoặc rơi vào địa chỉ trống (trường hợp không tìm thấy).
- Hàm băm lại lần thứ i được biểu diễn bằng công thức sau:
fi(key)=( f(key) + i2 ) % M
với f(key) là hàm băm chính của bảng băm.
Nếu đã dò đến cuối bảng thì trở về dò lại từ đầu bảng.
Bảng băm minh họa có cấu trúc như sau:
- Tập khóa K: tập số tự nhiên
- Tập địa chỉ M: gồm 10 địa chỉ (M={0, 1, …, 9}
- Hàm băm f(key) = key % 10.

Khai báo cấu trúc bảng băm:


#define NULLKEY –1
#define M 101
/*
M la so nut co tren bang bam,du de chua cac nut nhap vao bang bam,chon M la so nguyen to
*/
//Khai bao nut cua bang bam
struct node
{
int key; //Khoa cua nut tren bang bam
};
//Khai bao bang bam co M nut
struct node hashtable[M];
int N;
Cài đặt bảng băm dùng phương pháp dò bậc hai:
Hàm băm: Giả sử chúng ta chọn hàm băm dạng%: f(key)=key %10.
int hashfunc(int key)
{
return(key% 10);
}
Phép toán initialize
void initialize()
{
int i;
for(i=0; i<M;i++) hashtable[i].key = NULLKEY;
N=0; //so nut hien co khoi dong bang 0
}
Phép toán empty:
int empty()
{

31
return(N ==0 ?TRUE :FALSE);
}
Phép toán full:
int full()
{
return(N = = M-1 ?TRUE :FALSE);
}
Phép toán search:
Tìm phần tử có khóa k trên bảng băm,nếu không tìm thấy hàm này trả về trị M, nếu tìm thấy
hàm này trả về địa chỉ tìm thấy.
int search(int k)
{
int i, d;
i = hashfuns(k);
d = 1;
while(hashtable[i].key!=k&&hashtable[i].key !=NULLKEY)
{
//Bam lai (theo phuong phap bac hai)
i = (i+d) % M;
d = d+2;
}
hashtable[i].key =k; N = N+1;
return(i);
}
2.4.5. Bảng băm với phương pháp băm kép
Mô tả:
Phương pháp băm kép dùng hai hàm băm bất kì, ví dụ chọn hai hàm băm như sau:
h1(key)= key %M.
h2(key) =(M-2)-key %(M-2).

Bảng băm trong trường hợp này được cài đặt bằng danh sách kề có M phần tử, mỗi
phần tử của bảng băm là một mẫu tin có một trường key để lưu khoá các phần tử.
- Khi khởi động bảng băm, tất cả trường key được gán NULLKEY.
- Khi thêm phần tử có khoá key vào bảng băm, thì i=h1(key) và j=h2(key) sẽ xác định địa
chỉ i và j trong khoảng từ 0 đến M-1:
· Nếu chưa bị xung đột thì thêm phần tử mới tại địa chỉ i này.
· Nếu bị xung đột thì hàm băm lại lần 1 h1 sẽ xét địa chỉ mới i+j, nếu lại bị xung đột thì
hàm băm lại lần 2 h2 sẽ xét địa chỉ i+2j, …, quá trình cứ thế cho đến khi nào tìm được địa
chỉ trống và thêm phần tử vào địa chi này.
- Khi tìm kiếm một phần tử có khoá key trong bảng băm, hàm băm i=h1(key) và j=h2(key)
sẽ xác định địa chỉ i và j trong khoảng từ 0 đến M-1. Xét phần tử tại địa chỉ i, nếu chưa tìm

32
thấy thì xét tiếp phần tử i+j, i+2j, …, quá trình cứ thế cho đến khi nào tìm được khoá
(trường hợp tìm thấy) hoặc bị rơi vào địa chỉ trống (trường hợp không tìm thấy).
Bảng băm dùng hai hàm băm khác nhau, hàm băm lại của phương pháp băm kép được
tính theo hai giá trị: i (kết quả hàm băm thứ nhất) và j (kết qủa hàm băm thứ hai) theo một
công thức bất kì. Nếu đã dò đến cuối bảng thì trở về dò lại từ đầu bảng.
Bảng băm minh họa có cấu trúc như sau:
- Tập khóa K: tập số tự nhiên
- Tập địa chỉ M: gồm 11 địa chỉ (M={0, 1, …, 10}
- Chọn hàm băm f1(key)=key % 11 và f2(key)=9-key %9.
Khai báo
#define NULLKEY –1
#define M 101 /*M la so nut co tren bang bam,du de chua cac nut nhap vao
bang bam,chon M la so nguyen to */
struct node
{
int key;//khoa cua nut tren bang bam
};
struct node hashtable[M]; //khai bao bang bam co M nut

Bài 4:CÂY, CÂY NHỊ PHÂN, CÂY NHỊ PHÂN TÌM KIẾM
1. Cấu trúc cây
1.1. Định nghĩa 1:
Cây là một tập hợp T các phần tử (nút trên cây) trong đó có 1 nút đặc biệt T0 được gọi
là gốc, các nút còn khác được chia thành những tập rời nhau T1, T2 , ... , Tn theo quan hệ
phân cấp trong đó Ti cũng là một cây.
Nút ở cấp i sẽ quản lý một số nút ở cấp i+1. Quan hệ này người ta còn gọi là quan hệ
cha-con.
1.2. Một số khái niệm cơ bản
- Bậc của một nút: là số cây con của nút đó .
- Bậc của một cây: là bậc lớn nhất của các nút trong cây. Cây có bậc n thì gọi là cây n-phân.
- Nút gốc: nút không có nút cha.
- Nút lá: nút có bậc bằng 0 .
- Nút nhánh: nút có bậc khác 0 và không phải là gốc .
- Mức của một nút:
Mức (T0 ) = 1.
Gọi T1, T2, T3, ... , Tn là các cây con của T0
Mức (T1) = Mức (T2) = ... = Mức (Tn) = Mức (T0) + 1.
- Độ dài đường đi từ gốc đến nút x: là số nhánh cần đi qua kể từ gốc đến x.
- Chiều cao h của cây: mức lớn nhất của các nút lá.

1.3. Một số ví dụ về đối tượng các cấu trúc dạng cây


- Sơ đồ tổ chức của một doanh nghiệp

33
- Sơ đồ tổ chức cây thư mục

34
2. CÂY NHỊ PHÂN
2.1 Định nghĩa
Cây nhị phân là cây mà mỗi nút có tối đa 2 cây con

Cây nhị phân có thể ứng dụng trong nhiều bài toán thông dụng. Ví dụ dưới đây cho ta
hình ảnh của một biểu thức toán học:

2.2. Một số tính chất của cây nhị phân:


- Số nút ở mức I ≤ 2I-1.
- Số nút ở mức lá ≤ 2h-1, với h là chiều cao của cây.
- Chiều cao của cây h ≥ log2N (N - số nút trên trong cây).

35
2.3. Biểu diễn cây nhị phân T
Cây nhị phân là một cấu trúc bao gồm các phần tử (nút) được kết nối với nhau theo
quan hệ “cha-con” với mỗi cha có tối đa 2 con. Để biểu diễn cây nhị phân ta chọn phương
pháp cấp phát liên kết. Ứng với một nút, ta dùng một biến động lưu trữ các thông tin:
+ Thông tin lưu trữ tại nút.
+ Địa chỉ nút gốc của cây con trái trong bộ nhớ.
+ Địa chỉ nút gốc của cây con phải trong bộ nhớ.
Khai báo như sau:
typedef struct tagTNODE
{
Data Key;//Data là kiểu dữ liệu ứng với thông tin lưu tại nút
struct tagNODE *pLeft, *pRight;
}TNODE;
typedef TNODE *TREE;
2.4. Các thao tác trên cây nhị phân
Thăm các nút trên cây theo thứ tự trước (Node-Left-Right)
void NLR(TREE Root)
{
if (Root != NULL)
{
<Xử lý Root>; //Xử lý tương ứng theo nhu cầu
NLR(Root->pLeft);
NLR(Root->pRight);
}
}
Thăm các nút trên cây theo thứ tự giữa (Left- Node-Right)
void LNR(TREE Root)
{
if (Root != NULL)
{
LNR(Root->Left);
<Xử lý Root>; //Xử lý tương ứng theo nhu cầu LNR(Root->Right);
}
36
}
Thăm các nút trên cây theo thứ tự sau (Left-Right-Node)
void LRN(TREE Root)
{
if (Root != NULL)
{
LRN(Root->Left);
LRN(Root->Right);
<Xử lý Root>; //Xử lý tương ứng theo nhu cầu
}
}
Ứng dụng phương pháp này trong việc tính tổng kích thước của thư mục.
Ứng dụng tính toán giá trị của biểu thức.

(3 + 1)×3/(9 – 5 + 2) – (3×(7 – 4) + 6) = –13

37
2.5. Biểu diễn cây tổng quát bằng cây nhị phân
Nhược điểm của các cấu trúc cây tổng quát là bậc của các nút trên cây có thể rất khác
nhau ⇒ việc biểu diễn gặp nhiều khó khăn và lãng phí. Hơn nữa, việc xây dựng các thao tác
trên cây tổng quát phức tạp hơn trên cây nhị phân nhiều.
Vì vậy, nếu không quá cần thiết phải sử dụng cây tổng quát, người ta sẽ biến đổi cây
tổng quát thành cây nhị phân.
Ta có thể biến đổi một cây bất kỳ thành một cây nhị phân theo qui tắc sau:
- Giữ nút con trái nhất làm con trái.
- Các nút con còn lại biển đổi thành nút con phải.

VD: Giả sử có cây tổng quát như hình sau:

Cây nhị phân tương ứng sẽ như sau:

2.6. Một cách biểu diễn cây nhị phân khác


Đôi khi, trên cây nhị phân, người ta quan tâm đến cả quan hệ chiều cha con. Khi đó,
cấu trúc cây nhị phân có thể định nghĩa lại như sau:
typedef struct tagTNode
{
DataType Key;
struct tagTNode* pParent;
struct tagTNode* pLeft;
struct tagTNode* pRight;
}TNODE;
typedef TNODE *TREE;

3. CÂY NHỊ PHÂN TÌM KIẾM


3.1. Định nghĩa:

38
Cây nhị phân tìm kiếm (CNPTK) là cây nhị phân trong đó tại mỗi nút, khóa của nút
đang xét lớn hơn khóa của tất cả các nút thuộc cây con trái và nhỏ hơn khóa của tất cả các nút
thuộc cây con phải.
Dưới đây là một ví dụ về cây nhị phân tìm kiếm:

Nhờ ràng buộc về khóa trên CNPTK, việc tìm kiếm trở nên có định hướng. Hơn nữa,
do cấu trúc cây việc tìm kiếm trở nên nhanh đáng kể. Chi phí tìm kiếm trung bình chỉ
khoảng log2N.
Trong thực tế, khi xét đến cây nhị phân chủ yếu người ta xét CNPTK.
3.2. Các thao tác trên cây
3.2.1. Thăm các nút trên cây
3.2.2. Tìm một phần tử x trong cây
TToán:

Dễ dàng thấy rằng số lần so sánh tối đa phải thực hiện để tìm phần tử X là bằng h, với
h là chiều cao của cây.

Ví dụ: Tìm phần tử 55

3.3.3. Thêm một phần tử x vào cây

39
Việc thêm một phần tử X vào cây phải bảo đảm điều kiện ràng buộc của CNPTK. Ta
có thể thêm vào nhiều vị trí khác nhau trên cây, nhưng nếu thêm vào một nút lá thì sẽ dễ nhất
do ta có thể thực hiện quá trình tương tự thao tác tìm kiếm. Khi chấm dứt quá trình tìm kiếm
ta sẽ tìm được vị trí cần thêm.
Hàm insert trả về giá trị –1, 0, 1 khi không đủ bộ nhớ, gặp nút cũ hay thành công:
int insertNode(TREE &T, Data X)
{
if(T)
{
if(T->Key == X)return 0; //đã có
if(T->Key > X)
return insertNode(T->pLeft, X);
else
return insertNode(T->pRight, X);
}
T = new TNode;
if(T == NULL) return -1; //thiếu bộ nhớ
T->Key = X;
T->pLeft =T->pRight = NULL;
return 1; //thêm vào thành công
}

2.4. Hủy một phần tử có khóa x


Việc hủy một phần tử X ra khỏi cây phải bảo đảm điều kiện ràng buộc của CNPTK.
Có 3 trường hợp khi hủy nút X có thể xảy ra:
X - nút lá.
X - chỉ có 1 cây con (trái hoặc phải).
X có đủ cả 2 cây con

Trường hợp thứ nhất: chỉ đơn giản hủy X vì nó không móc nối đến phần tử nào khác.

40
Trường hợp thứ hai: trước khi hủy X ta móc nối cha của X với con duy nhất của nó.

Trường hợp cuối cùng: ta không thể hủy trực tiếp do X có đủ 2 con ⇒ Ta sẽ hủy gián
tiếp. Thay vì hủy X, ta sẽ tìm một phần tử thế mạng Y. Phần tử này có tối đa một con. Thông
tin lưu tại Y sẽ được chuyển lên lưu tại X. Sau đó, nút bị hủy thật sự sẽ là Y giống như 2
trường hợp đầu.
Vấn đề là phải chọn Y sao cho khi lưu Y vào vị trí của X, cây vẫn là CNPTK.
Có 2 phần tử thỏa mãn yêu cầu:
Phần tử nhỏ nhất (trái nhất) trên cây con phải.
Phần tử lớn nhất (phải nhất) trên cây con trái.

Việc chọn lựa phần tử nào là phần tử thế mạng hoàn toàn phụ thuộc vào ý thích của
người lập trình. Ở đây, cháng tôi sẽ chọn phần tử (phải nhất trên cây con trái làm phân tử thế
mạng.

41
VD:
Cần hủy phần tử 18.

2.5. ĐÁNH GIÁ


Tất cả các thao tác Tìm kiếm, Thêm mới, Xóa trên CNPTK đều có độ phức tạp trung
bình O(h), với h là chiều cao của cây
Trong trong trường hợp tốt nhất, CNPTK có n nút sẽ có độ cao h = log2(n). Chi phí tìm
kiếm khi đó sẽ tương đương tìm kiếm nhị phân trên mảng có thứ tự.
Tuy nhiên, trong trường hợp xấu nhất, cây có thể bị suy biến thành 1 DSLK. Lúc đó
các thao tác trên sẽ có độ phức tạp O(n). Vì vậy cần có cải tiến cấu trúc của CNPTK để đạt
được chi phí cho các thao tác là log2(n).

CÂY CÂN BẰNG


1.CÂY NHỊ PHÂN CÂN BẰNG HOÀN TOÀN
1.1. Định nghĩa
Cây cân bằng hoàn toàn là cây nhị phân tìm kiếm mà tại mỗi nút của nó, số nút của cây
con trái chênh lệch không quá một so với số nút của cây con phải.
1.2. Đánh giá
Một cây rất khó đạt được trạng thái cân bằng hoàn toàn và cũng rất dễ mất cân bằng vì
khi thêm hay hủy các nút trên cây có thể làm cây mất cân bằng, chi phí cân bằng lại cây cao
vì phải thao tác trên toàn bộ cây.

Đối với cây cân bằng hoàn toàn, trong trường hợp xấu nhất ta chỉ phải tìm qua log2N
phần tử (N là số nút trên cây).
Sau đây là ví dụ một cây cân bằng hoàn toàn (CCBHT):

42
CCBHT có N nút có chiều cao h ≈ log2N. Đây chính là lý do cho phép bảo đảm khả
năng tìm kiếm nhanh trên CTDL này. Do CCBHT là một cấu trúc kém ổn định nên trong
thực tế không thể sử dụng. Nhưng ưu điểm của nó lại rất quan trọng. Vì vậy, cần đưa ra một
CTDL khác có đặc tính giống CCBHT nhưng ổn định hơn.

2. CÂY NHỊ PHÂN CÂN BẰNG (AVL Tree)


2.1. Định nghĩa:
Cây nhị phân tìm kiếm cân bằng là cây mà tại mỗi nút của nó độ cao của cây con trái
và của cây con phải chênh lệch không quá một.

Dưới đây là ví dụ cây nhị phân cân bằng :

Dễ dàng thấy CCBHT là cây cân bằng. Điều ngược lại có thể không đúng không đúng.

2.2. Lịch sử cây cân bằng (AVL Tree)


AVL là tên viết tắt của các tác giả người Nga đã đưa ra định nghĩa của cây cân bằng
Adelson-Velskii và Landis (1962). Vì lý do này, người ta gọi cây nhị phân cân băng là cây
AVL. Từ cây AVL, người ta đã phát triển thêm nhiều loại CTDL hữu dụng khác như cây đỏ-
đen (Red-Black Tree), B-Tree, …
2.3. Chiều cao của cây AVL

Một vấn đề quan trọng, như đã đề cập đến ở phần trước, là ta phải khẳng định cây AVL
có N nút phải có chiều cao khoảng log2(n).

Để đánh giá chính xác về chiều cao của cây AVL, ta xét bài toán: cây AVL có chiều
cao h sẽ phải có tối thiểu bao nhiêu nút ?

Gọi N(h) là số nút tối thiểu của cây AVL có chiều cao h.

43
Ta có N(0) = 0, N(1) = 1 và N(2) = 2.

Cây AVL có chiều cao h sẽ có 1 cây con AVL chiều cao h-1 và 1 cây con AVL chiều
cao h-2. Như vậy:

N(h) = 1 + N(h-1) + N(h-2) (1)

Ta lại có: N(h-1) > N(h-2)


Nên từ (1) suy ra:
N(h) > 2N(h-2)
N(h) > 22N(h-4)

N(h) > 2iN(h-2i)
i =h/2
N(h)>2h/2
h < 2log2(N(h))
Như vậy, cây AVL có chiều cao O(log2(n)).

Ví dụ: cây AVL tối thiểu có chiều cao h=4

2.4. Cấu trúc dữ liệu cho cây AVL

Chỉ số cân bằng của một nút: Chỉ số cân bằng của một nút là hiệu của chiều cao cây con
phải và cây con trái của nó.

Đối với một cây cân bằng, chỉ số cân bằng (CSCB) của mỗi nút chỉ có thể nhận một
trong ba giá trị sau đây:

CSCB(p) = 0 <=> Độ cao cây trái (p) = Độ cao cây phải (p)
CSCB(p) = 1 <=> Độ cao cây trái (p) < Độ cao cây phải (p)
CSCB(p) =-1 <=> Độ cao cây trái (p) > Độ cao cây phải (p)

44
Xét nút P, ta dùng các ký hiệu sau:

P->balFactor = CSCB(P);
Độ cao cây trái P ký hiệu là hleft
Độ cao cây phải P ký hiệu là hright

Để khảo sát cây cân bằng, ta cần lưu thêm thông tin về chỉ số cân bằng tại mỗi nút. Lúc đó,
cây cân bằng có thể được khai báo như sau:

typedef struct tagAVLNode


{
char balFactor; //Chỉ số cân bằng
Data key;
struct tagAVLNode* pLeft;
struct tagAVLNode* pRight;
}
AVLNode;

typedef AVLNode *AVLTree;

Để tiện cho việc trình bày, ta định nghĩa một số hăng số sau:
#define LH -1 //Cây con trái cao hơn
#define EH -0 //Hai cây con bằng nhau
#define RH 1 //Cây con phải cao hơn

2.5. Đánh giá cây AVL


Cây cân bằng là CTDL ổn định hơn CCBHT vì khi thêm, hủy làm cây thay đổi chiều
cao các trường hợp mất cân bằng mới có khả năng xảy ra.

Cây AVL với chiều cao được khống chế sẽ cho phép thực thi các thao tác tìm, thêm,
hủy với chi phí O (log2(n)) và bảo đảm không suy biến thành O(n).

3. CÁC THAO TÁC CƠ BẢN TRÊN CÂY AVL

Ta nhận thấy trường hợp thêm hay hủy một phần tử trên cây có thể làm cây tăng hay
giảm chiều cao, khi đó phải cân bằng lại cây.
Việc cân bằng lại một cây sẽ phải thực hiện sao cho chỉ ảnh hưởng tối thiểu đến cây
nhằm giảm thiểu chi phí cân bằng. Như đã nói ở trên, cây cân bằng cho phép việc cân bằng
lại chỉ xảy ra trong giới hạn cục bộ nên chúng ta có thể thực hiện được mục tiêu vừa nêu.

45
Như vậy, ngoài các thao tác bình thường như trên CNPTK, các thao tác đặc trưng của
cây AVL gồm:
Thêm một phần tử vào cây AVL.
Hủy một phần tử trên cây AVL.
Cân bằng lại một cây vừa bị mất cân bằng.

3.1. CÁC TRƯỜNG HỢP MẤT CÂN BẰNG


Ta sẽ không khảo sát tính cân bằng của 1 cây nhị phân bất kỳ mà chỉ quan tâm đến các
khả năng mất cân bằng xảy ra khi thêm hoặc hủy một nút trên cây AVL.
Như vậy, khi mất cân bằng, độ lệch chiều cao giữa 2 cây con sẽ là 2. Ta có 6 khả năng
sau:
Trường hợp 1: cây T lệch về bên trái (có 3 khả năng)

46
Trường hợp 2: cây T lệch về bên phải
Ta có các khả năng sau:

Ta có thể thấy rằng các trường hợp lệch về bên phải hoàn toàn đối xứng với các trường
hợp lệch về bên trái. Vì vậy ta chỉ cần khảo sát trường hợp lệch về bên trái.
Trong 3 trường hợp lệch về bên trái, trường hợp T1 lệch phải là phức tạp nhất. Các
trường hợp còn lại giải quyết rất đơn giản.
Sau đây, ta sẽ khảo sát và giải quyết từng trường hợp nêu trên
T/h 1.1: cây T1 lệch về bên trái. Ta thực hiện phép quay đơn Left-Left

T/h 1.2: cây T1 không lệch. Ta thực hiện phép quay đơn Left-Left

47
Ttoán quay đơn Left-Left:
B1: T là gốc; T1 = T->pLeft;
T->pLeft = T1->pRight;
T1->pRight = T;
B2:// đặt lại chỉ số cân bằng
Nếu T1->balFactor = LH thì:
T->balFactor = EH;
T1->balFactor = EH;
break;
Nếu T1->balFactor = EH thì:
T->balFactor = LH;
T1->balFactor = RH;
break;
B3:// T trỏ đến gốc mới
T = T1;

T/h 1.3: cây T1 lệch về bên phải. Ta thực hiện phép quay kép Left-Right

Do T1 lệch về bên phải ta không thể áp dụng phép quay đơn đã áp dụng trong 2 trường
hợp trên vì khi đó cây T sẽ từ trạng thái mất cân bằng do lệch trái thành mất cân bằng do
lệch phải.
Hình vẽ dưới đây minh họa phép quay kép áp dụng cho trường hợp này:

Ttoán quay kép Left - Right


B1: gốc T;
T1 = T->pLeft; T2 = T1->pRight; T->pLeft = T2->pRight;

48
T2->pRight = T;T1->pRight = T2->pLeft;T2->pLeft = T1;
B2: //đặt lại chỉ số cân bằng
Nếu T2->balFactor = LH thì:
T->balFactor = RH; T1->balFactor = EH; break;
Nếu T2->balFactor = EH thì:
T->balFactor = EH; T1->balFactor = EH; break;
Nếu T2->balFactor = RH thì:
T->balFactor = EH; T1->balFactor = LH; break;
B3:
T2->balFactor = EH;
T = T2;

Lưu ý rằng, trước khi cân bằng cây T có chiều cao h+2 trong cả 3 trường hợp 1.1, 1.2
và 1.3.
Sau khi cân bằng, trong 2 trường hợp 1.1 và 1.3 cây có chiều cao h+1; còn ở trường
hợp 1.2 cây vẫn có chiều cao h+2. Và trường hợp này cũng là trường hợp duy nhất sau khi
cân bằng nút T cũ có chỉ số cân bằng khác 0.

Thao tác cân bằng lại trong tất cả các trường hợp đều có độ phức tạp O(1).
Với những xem xét trên, xét tương tự cho trường hợp cây T lệch về bên phải, ta có thể
xây dựng 2 hàm quay đơn và 2 hàm quay kép sau:

3.2.THÊM MỘT PHẦN TỬ TRÊN CÂY AVL:


Việc thêm một phần tử vào cây AVL diễn ra tương tự như trên CNPTK. Tuy nhiên, sau
khi thêm xong, nếu chiều cao của cây thay đổi, từ vị trí thêm vào, ta phải tìm ngược lên gốc
để kiểm tra các nút bị mất cân bằng không. Nếu có, ta phải cân bằng lại ở nút này.

TToán: Giả sử cần thêm vào một nút mang thông tin X.
1. Tìm kiếm vị trí thích hợp để thêm nút X (đưa ra thông báo nếu đã có nút X rồi)
2. Thêm nút X vào cây
3. Cân bằng lại cây.

3.3. HỦY MỘT PHẦN TỬ TRÊN CÂY AVL:


Cũng giống như thao tác thêm một nút, việc hủy một phần tử X ra khỏi cây AVL thực
hiện giống như trên CNPTK. Chỉ sau khi hủy, nếu tính cân bằng của cây bị vi phạm ta sẽ
thực hiện việc cân bằng lại.
Tuy nhiên việc cân bằng lại trong thao tác hủy sẽ phức tạp hơn.

BÀI 6: CÂY ĐỎ ĐEN


1. GIỚI THIỆU
Cây tìm kiếm nhị phân là một cấu trúc lưu trữ dữ liệu tốt với tốc độ tìm kiếm nhanh.

49
Tuy nhiên trong một số trường hợp cây tìm kiếm nhị phân có một số hạn chế. Nó hoạt
động tốt nếu dữ liệu được chèn vào cây theo thứ tự ngẫu nhiên. Tuy nhiên, nếu dữ liệu được
chèn vào theo thứ tự đã đuợc sắp xếp sẽ không hiệu quả. Khi các trị số cần chèn đã đuợc sắp
xếp thì cây nhị phân trở nên không cân bằng. Khi cây không cân bằng, nó mất đi khả năng
tìm kiếm nhanh (hoặc chèn hoặc xóa) một phần tử đã cho.
Chúng ta khảo sát một cách giải quyết vấn đề của cây không cân bằng: đó là cây đỏ
đen, là cây tìm kiếm nhị phân có thêm một vài đặc điểm .
Có nhiều cách tiếp cận khác để bảo đảm cho cây cân bằng: chẳng hạn cây 2-3-4. Tuy
vậy, trong phần lớn trường hợp, cây đỏ đen là cây cân bằng hiệu quả nhất, ít ra thì khi dữ
liệu được lưu trữ trong bộ nhớ chứ không phải trong những tập tin.
Trước khi khảo sát cây đỏ đen, hãy xem lại cây không cân bằng được tạo ra như thế
nào.

Hình 1. Các node được chèn theo thứ tự tăng dần


Những node này tự sắp xếp thành một đường không phân nhánh. Bởi vì mỗi node lớn
hơn node đã được chèn vào trước đó, mỗi node là con phải của nút trước đó. Khi ấy, cây bị
mất cân bằng hoàn toàn.
Độ phức tạp:
Khi cây một nhánh, sẽ trở thành một danh sách liên kết, dữ liệu sẽ là một chiều thay vì
hai chiều. Trong trường hợp này, thời gian truy xuất giảm về O(N), thay vì O(log2N) đối với
cây cân bằng.
Để bảo đảm thời gian truy xuất nhanh của cây, chúng ta cần phải bảo đảm cây luôn
luôn cân bằng (ít ra cũng là cây gần cân bằng). Điều này có nghĩa là mỗi node trên cây phải
có xấp xỉ số node con bên phải bằng số node con bên trái.
2. ĐỊNH NGHĨA CÂY ĐỎ ĐEN
Cây đỏ đen là một cây nhị phân tìm kiếm (BST) tuân thủ các quy tắc sau: (hình 2)
(1) Mọi node phải là đỏ hoặc đen.
(2) Node gốc và các node lá (NIL) phải luôn luôn đen.
(3) Nếu một node là đỏ, những node con của nó phải đen.
(4) Mọi đường dẫn từ gốc đến một lá phải có cùng số lượng node đen.
Khi chèn (hay xóa) một node mới, cần phải tuân thủ các quy tắc trên -gọi là quy tắc đỏ đen.
Nếu được tuân thủ, cây sẽ được cân bằng.

50
Hình 2. Một ví dụ về cây đỏ đen
Số lượng node đen trên một đường dẫn từ gốc đến lá được gọi là chiều cao đen (black
height). Ta có thể phát biểu quy tắc (4) theo một cách khác là mọi đường dẫn từ gốc đến lá
phải có cùng chiều cao đen.
Khai báo cấu trúc:
typedef int Data; /* Kiểu dữ liệu khoá */
typedef enum { BLACK, RED } nodeColor;
typedef struct NodeTag {
nodeColor color; /* Màu node (BLACK, RED) */
Data info; /* Khoá sử dụng tìm kiếm */
struct NodeTag *left; /* Con trái */
struct NodeTag *right; /* Con phải */
struct NodeTag *parent; /* Cha */
} NodeType;
typedef NodeType *iterator;
Bổ đề:
Một cây đỏ đen n-node có chiều cao h <= 2 log2(n+1)
3. PHÉP QUAY
Thực ra quay không có nghĩa là các node bị quay mà để chỉ sự thay đổi quan hệ giữa
chúng. Một node được chọn làm "đỉnh" của phép quay. Nếu chúng ta đang thực hiện một
phép quay qua phải, node "đỉnh" này sẽ di chuyển xuống dưới và về bên phải, vào vị trí của
node con bên phải của nó. Node con bên trái sẽ đi lên để chiếm lấy vị trí của nó.

Hình 3. Quay trái và quay phải

Phải đảm bảo trong phép quay phải, node ở đỉnh phải có node con trái. Nếu không
chẳng có gì để quay vào điểm đỉnh. Tương tự, nếu làm phép quay trái, node ở đỉnh phải có
node con phải.
4. THÊM NODE MỚI
51
Chúng ta sẽ xem xét việc mô tả qui trình chèn. Gọi X, P, và G để chỉ định nhãn những
node liên quan. X là node vi phạm quy tắc (X có thể là một node mới được chèn, hoặc node
con khi node cha và node con xung đột đỏ-đỏ, nghĩa là có cùng màu đỏ).
− X là một node cho trước.
− P là node cha của X.
− G là node ông bà của X (node cha của P).
Trong quá trình thêm vào node mới có thể vi phạm các quy tắc của cây đỏ đen, chúng
ta sẽ thực hiện các thao tác sau đây:
− Các phép lật màu trên đường đi xuống.
− Các phép quay khi node đã được chèn.
− Các phép quay trên đường đi xuống.

4.1 Các phép lật màu trên đường đi xuống


Phép thêm vào trong cây đỏ đen bắt đầu như trên cây tìm kiếm nhị phân thông thường:
đi theo một đường dẫn từ node gốc đến vị trí cần chèn, đi qua phải hay trái tùy vào giá trị
của khóa node và khóa tìm kiếm.
Tuy nhiên, trong cây đỏ đen, đến được điểm chèn là phức tạp bởi các phép lật màu và
quay.
Để bảo đảm không vi phạm các quy tắc màu, cần phải tiến hành các phép lật màu khi
cần theo quy tắc như sau:
Nếu phép thêm vào làm xuất hiện tình trạng một node đen có hai node con đỏ, chúng ta
đổi các node con thành đen và node cha thành đỏ (trừ khi node cha là node gốc, nó vẫn vẫn
giữ màu là đen).
Một phép lật màu ảnh hưởng đến các quy tắc đỏ-đen ra sao? chúng ta gọi node ở đỉnh
tam giác, node có màu đen trước phép lật là P (P thay cho node cha). Chúng ta gọi hai node
con trái và phải của P là X1 và X2. Xem hình 4a.
Hình 4. Lật màu

Hình 4a. trước khi lật màu, Hình 4b sau khi lật màu.
Chúng ta nhận thấy sau khi lật màu chiếu cao đen của cây không đổi. Như vậy phép lật
màu không vi phạm quy tắc (4).

52
Mặc dù quy tắc (4) không bị vi phạm qua phép lật, nhưng quy tắc 3 (một node con và
node cha không thể đồng màu đỏ) lại có khả năng bị vi phạm. Nếu node cha của P là đen,
không có vấn đề vi phạm khi P được đổi từ đen sang đỏ, nhưng nếu node cha của P là đỏ, thì
sau khi đổi màu, ta sẽ có hai node đỏ trên một hàng.
Điều này cần phải được chuẩn bị truớc khi đi xuống theo cây để chèn node mới. Chúng
ta có thể giải quyết trường hợp này bằng một phép quay.
Đối với node gốc thì phép lật màu node gốc và hai node con của nó vẫn làm cho node
gốc cũng như hai node con có màu đen. Điều này tránh sự vi phạm quy tắc 2 và quy tắc 3
(xung đột đỏ-đỏ). Trong trường hợp này, chiều cao đen trên mỗi đường đi từ node gốc tăng
lên 1, do đó quy tắc 4 cũng không bị vi phạm.
4.2. Các phép quay khi chèn node
Thao tác chèn node mới có thể làm cho quy tắc đỏ-đen bị vi phạm. Do vậy sau khi
chèn, cần phải kiểm tra xem có phạm quy tắc không và thực hiện những thao tác hợp lý.
Như đã xét ở trên, node mới được chèn mà ta gọi là node X, luôn luôn đỏ. Node X có
thể nằm ở những vị trí khác nhau đối với P và G, như trong hình 5.

Hình 5. Các biến dạng của node được chèn

X là một node cháu ngoại nếu nó nằm cùng bên node cha P và P cùng bên node cha G.
Điều này có nghĩa là, X là node cháu ngoại nếu hoặc nó là node con trái của P và P là node
con trái của G, hoặc nó là node con phải của P và node P là node con phải của G. Ngược lại,
X là một node cháu nội.
Nếu X là node cháu ngoại, nó có thể hoặc bên trái hoặc bên phải của P, tùy vào việc
node P ở bên trái hay bên phải node G. Có hai khả năng tương tự nếu X là một node cháu
nội. Bốn trường hợp này được trình bày trong hình 5.

53
Thao tác phục hồi quy tắc đỏ-đen được xác định bởi các màu và cấu hình của node X
và những bà con của nó. Có 3 khả năng xảy ra được xem xét như sau:(hình 6)

Hình 6. Ba khả năng sau khi chèn nút


i) Khả năng 1: P đen
ii) Khả năng 2: P đỏ và X là cháu ngoại của G
iii) Khả năng 3: P đỏ và X là cháu nội của G
Chúng ta sẽ xét các khả năng trên một cách cụ thể như sau:
i) Khả năng 1: P đen
P đen là trường hợp đơn giản. Node thêm vào luôn đỏ. Nếu node cha đen, không có
xung khắc đỏ-đỏ (quy tắc 3), và không có việc cộng thêm vào số node đen (quy tắc 4). Do
vậy, không bị vi phạm quy tắc về màu. Thao tác chèn đã hoàn tất.
ii) Khả năng 2: P đỏ và X là cháu ngoại của G
Nếu node P đỏ và X là node cháu ngoại, ta cần một phép quay đơn giản và một vài
thay đổi về màu. Bắt đầu với giá trị 50 tại node gốc, và chèn các node 25, 75 và 12. Ta cần
phải làm một phép lật màu trước khi chèn node 12.
Bây giờ, chèn node mới X là 6. (hình 7a. )xuất hiện lỗi: cha và con đều đỏ, vì vậy cần
phải có các thao tác như sau: (hình 7)
Trong trường hợp này, ta có thể áp dụng ba bước để phục hồi tính đỏ-đen và làm cho
cân bằng cây. Sau đây là các bước ấy:
-Đổi màu node G - node ông bà của node X (trong thí dụ này là node 25).
-Đổi màu node P - node cha của node X (node 12)
-Quay với node G (25) ở vị trí đỉnh, theo huớng làm nâng node X lên (6). Đây là
một phép quay phải.
Khi ta hoàn tất ba buớc trên sẽ bảo toàn cây đỏ đen. Xem hình 7b.
Trong thí dụ này, node X là node cháu ngoại của một node con trái. Có một trường hợp
đối xứng khi node X là node cháu ngoài nhưng của một node con phải. Thử làm điều này
bằng cách tạo nên cây 50, 25, 75, 87, 93 (với phép lật màu khi cần). Chỉnh sửa cây bằng
54
cách đổi màu node 75 và 87, và quay trái với node 75 là node đỉnh. Một lần nữa cây lại được
cân bằng.

Hình 7. Node P đỏ và X là node cháu ngoại


iii) Khả năng 3: P đỏ và X là cháu nội của G
Nếu node P đỏ và X là node cháu nội, chúng ta cần thực hiện hai phép quay và một vài
phép đổi màu. Cây đỏ đen được tạo thành từ các node 50, 25, 75, 12 và 18. (cần phải lật màu
trước khi chèn node 12). Xem hình 8a.
Lưu ý là node 18 là node cháu nội. Node này và node cha đều đỏ (cha và con đều đỏ).

55
hình 8.c
Hình 8. Khả năng 3: P đỏ và X là node cháu nội
Chỉnh lại sự sắp xếp này cũng khá rắc rối hơn. Nếu ta cố quay phải node ông bà G (25)
ở đỉnh, như ta đã làm trong khả năng 2, node cháu trong X (18) đi ngang hơn là đi lên, như
thế cây sẽ không còn cân bằng như trước. (Thử làm điều này, rồi quay trở lại, với node 12 ở
đỉnh, để phục hồi cây nhu cũ). Phải cần một giải pháp khác.
Thủ thuật cần dùng khi X là node cháu nội là tiến hành hai phép quay hơn là một phép.
Phép quay đầu biến X từ một node cháu nội thành node cháu ngoại, như trong hình 8b. Bây
giờ, trường hợp là tương tự như khả năng 1, và ta có thể áp dụng cùng một phép quay, với
node ông bà ở đỉnh, như đã làm trước đây. Kết quả như trong hình 8c.
Chúng ta cũng cần tô màu lại các nút. Ta làm điều này trước khi làm bất cứ phép quay
nào (thứ tự không quan trọng, nhưng nếu ta đợi đến khi sau khi quay mới tô màu lại node thì
khó mà biết phải gọi chúng như thế nào). Các bước là:
- Đổi màu node ông bà của node X ( node 25).
- Đổi màu node X ( node X đây là node 18).
- Quay trái với node P - node cha của X - ở đỉnh ( node cha đây là 12).
56
- Quay lần nữa với node ông bà của X (25) ở đỉnh, về hướng nâng X lên (quay phải).
5. LOẠI BỎ NODE
Trong cây BST chúng ta thấy rằng phép loại bỏ phức tạp hơn so với phép thêm vào.
Trong cây đỏ đen phép loại bỏ càng phức tạp hơn rất nhiều so với phép thêm vào vì yêu cầu
đảm bảo quy tắc đỏ đen. Chúng ta có thể tham khảo trong phần cài đặt.
• Nếu xóa một nút đỏ thì chiều cao đen của cây không đổi
• Nếu xóa một nút đen thì chúng ta phải cân bằng lại cây.

6. TÍNH HIỆU QUẢ CỦA CÂY ĐỎ ĐEN


Giống như cây tìm kiếm nhị phân thông thường, cây đỏ đen có thể cho phép việc tìm
kiếm, chèn và xóa trong thời gian O(log2N). Thời gian tìm kiếm là gần như bằng nhau đối
với hai loại cây, vì những đặc điểm của cây đỏ đen không sử dụng trong quá trình tìm kiếm.
Điều bất lợi là việc lưu trữ cần cho mỗi node tăng chút ít để điều tiết màu đỏ-đen (một biến
boolean).
Đặc thù hơn, theo Sedgewick, trong thực tế tìm kiếm trên cây đỏ đen mất khoảng
log2N phép so sánh, và có thể chứng minh rằng nó không cần hơn 2*log2N phép so sánh.
Thời gian chèn và xóa tăng dần bởi một hằng số vì việc phải thực thi phép lật màu và
quay trên đường đi xuống và tại những điểm chèn. Trung bình một phép chèn cần khoảng
chừng một phép quay. Do đó, chèn hày còn chiếm O(log2N) thời gian, nhưng lại chậm hơn
phép chèn trong cây nhị phân thường.
Bởi vì trong hầu hết các ứng dụng, có nhiều thao tác tìm kiếm hơn là chèn và xóa, có
lẽ không có nhiều bất lợi về thời gian khi dùng cây đỏ đen thay vì cây nhị phân thuờng. Dĩ
nhiên, điều thuận lợi là trong cây đỏ đen, dữ liệu đã sắp xếp không làm giảm hiệu suất O(N).
Một trở ngại trong cây đỏ đen là việc cài đặt các phép toán phức tạp hơn so với cây BST.
Chúng ta có thể tham khảo các phép toán thêm vào và loại bỏ trong phần cài đặt.

BÀI 7: CÂY 2-3-4


1. Giới thiệu về cây 2-3-4
Chúng ta sẽ xem xét các đặc tính của cây 2-3-4 và mối quan hệ khá gần gũi giữa cây 2-3-4 và cây
đỏ-đen.
Hình 1 trình bày một cây 2-3-4 đơn giản. Mỗi node có thể lưu trữ 1, 2 hoặc 3 mục dữ
liệu.

Hình 1 cây 2-3-4

57
Các số 2, 3 và 4 trong cụm từ cây 2-3-4 có ý nghĩa là khả năng có bao nhiêu liên kết
đến các node con có thể có được trong một node cho trước. Đối với các node không phải là
lá, có thể có 3 cách sắp xếp sau:
Một node với một mục dữ liệu thì luôn luôn có 2 con.
Một node với hai mục dữ liệu thì luôn luôn có 3 con.
Một node với ba mục dữ liệu thì luôn luôn có 4 con.

Như vậy, một node không phải là lá phải luôn luôn có số node con nhiều hơn 1 so với
số mục dữ liệu của nó. Nói cách khác, đối với mọi node với số con là k và số mục dữ liệu là

d, thì : k = d + 1
Hình 2. các trường hợp của cây 2-3-4
Với mọi node lá thì không có node con nhưng có thể chứa 1, 2 hoặc 3 mục dữ liệu,
không có node rỗng.
Một cây 2-3-4 có thể có đến 4 cây con nên được gọi là cây nhiều nhánh bậc 4.
Trong cây 2-3-4 mỗi node có ít nhất là 2 liên kết, trừ node lá (node không có liên kết
nào).
Hình 2 trình bày các trường hợp của cây 2-3-4. Một node với 2 liên kết gọi là một 2-
node, một node với 3 liên kết gọi là một 3-node, và một node với 4 liên kết gọi là một 4-
node, nhưng ở đây không có node là 1-node.

2. Tổ chức cây 2-3-4


Các mục dữ liệu trong mỗi node được sắp xếp theo thứ tự tăng dần từ trái sang phải
(sắp xếp từ thấp đến cao).

58
Trong cây tìm kiếm nhị phân, tất cả node của cây con bên trái có khoá nhỏ hơn khóa
của node đang xét và tất cả node của cây con bên phải có khoá lớn hơn hoặc bằng khóa của
node đang xét. Trong cây 2-3-4 thì nguyên tắc cũng giống như trên, nhưng có thêm một số
điểm sau:
Tất cả các node con của cây con có gốc tại node con thứ 0 thì có các giá trị khoá
nhỏ hơn khoá 0.
Tất cả các node con của cây con có gốc tại node con thứ 1 thì có các giá trị khoá
lớn hơn khoá 0 và nhỏ hơn khoá 1.
Tất cả các node con của cây con có gốc tại node con thứ 2 thì có các giá trị khoá
lớn hơn khoá 1 và nhỏ hơn khoá 2.
Tất cả các node con của cây con có gốc tại node con thứ 3 thì có các giá trị khoá
lớn hơn khoá 2.

Trong cây 2-3-4, các nút lá đều nằm trên cùng một mức. Các node ở mức trên thường không
đầy đủ, nghĩa là chúng có thể chứa chỉ 1 hoặc 2 mục dữ liệu thay vì 3 mục.

Lưu ý rằng cây 2-3-4 là cây cân bằng. Nó vẫn giữ được sự cân bằng khi thêm vào các
phần tử có thứ tự (tăng dần hoặc giảm dần).

3. Tìm kiếm

Thao tác tìm kiếm trong cây 2-3-4 tương tự như thủ tục tìm kiếm trong cây nhị phân.
việc tìm kiếm bắt đầu từ node gốc và chọn liên kết dẫn đến cây con với phạm vi giá trị phù
hợp.
Ví dụ, để tìm kiếm mục dữ liệu với khoá là 64 trên cây ở hình 1, bạn bắt đầu từ gốc.
Tại node gốc không tìm thấy mục khoá này. Bởi vì 64 lớn 50, chúng ta đi đến node con 1,
(60/70/80)(lưu ý node con 1 nằm bên phải, bởi vì việc đánh số của các node con và các liên
kết bắt đầu tại 0 từ bên trái). Tại vị trí này vẫn không tìm thấy mục dữ liệu, vì thế phải đi
đến node con tiếp theo. Tại đây bởi vì 64 lớn hơn 60 nhưng nhỏ hơn 70 nên đi tiếp đến node
con 1. Tại thời điểm chúng ta tìm được mục dữ liệu đã cho với liên kết là 62/64/66.

4. Thêm vào
Các mục dữ liệu mới luôn luôn được chèn vào tại các node lá . Nếu mục dữ liệu được
thêm vào node mà có node con, thì số lượng của các node con cần thiết phải được biến đổi
59
để duy trì cấu trúc cho cây, đây là lý do tại sao phải có số node con nhiều hơn 1 so với các
mục dữ liệu trong một nút.
Việc thêm vào cây 2-3-4 trong bất cứ trường hợp nào thì quá trình cũng bắt đầu bằng
cách tìm kiếm node lá phù hợp.
Nếu không có node đầy nào (node có đủ 3 mục dữ liệu) được bắt gặp trong quá trình
tìm kiếm, việc chèn vào khá là dễ dàng. Khi node lá phù hợp được tìm thấy, mục dữ liệu mới
đơn giản là thêm vào nó. Hình 3 trình bày một mục dữ liệu với khoá 18 được thêm vào cây
2-3-4.
Việc chèn vào có thể dẫn đến phải thay đổi vị trí của một hoặc hai mục dữ liệu trong
node vì thế các khoá sẽ nằm với trật tự đúng sau khi mục dữ liệu mới được thêm vào. Trong
ví dụ này số 23 phải được đẩy sang phải để nhường chỗ cho 18.

Hình 3 Chèn vào không làm tách cây


(i) trước khi chèn vào
(ii) sau khi chèn vào
Tách nút
Việc thêm vào sẽ trở nên phức tạp hơn nếu gặp phải một node đầy (node có số mục dữ
liệu đầy đủ) trên nhánh dẫn đến điểm thêm vào. Khi điều này xảy ra, node này cần thiết phải
được tách ra. Quá trình tách nhằm giữ cho cây cân bằng. Loại cây 2-3-4 mà chúng ta đề cập
ở đây thường được gọi là cây 2-3-4 top-down bởi vì các node được tách ra theo hướng đi
xuống điểm chèn.

60
Giả sử ta đặt tên các mục dữ liệu trên node bị phân chia là A, B và C. Sau đây là tiến
trình tách (chúng ta giả sử rằng node bị tách không phải là node gốc; chúng ta sẽ kiểm tra
việc tách node gốc sau này):
Một node mới (rỗng) được tạo. Nó là anh em với node sẽ được tách và được đưa
vào bên phải của nó.
Mục dữ liệu C được đưa vào node mới.
Mục dữ liệu B được đưa vào node cha của node được tách.
Mục dữ liệu A không thay đổi.
Hai node con bên phải nhất bị hủy kết nối từ node được tách và kết nối đến node
mới.
( Một cách khác để mô tả sự tách node là một 4-node được tách thành hai 2-nút)

Một ví dụ về việc tách node trình bày trên hình 4.

Hình 4: Tách một nút


(i ) Trước khi chèn vào
(ii) Sau khi chèn vào

Tách node gốc


Khi gặp phải node gốc đầy tại thời điểm bắt đầu tìm kiếm điểm chèn, kết quả của việc
tách thực hiện như sau:
Node mới được tạo ra để trở thành gốc mới và là cha của node được tách.
Node mới thứ hai được tạo ra để trở thành anh em với node được tách.
Mục dữ liệu C được dịch đưa sang node anh em mới.

61
Mục dữ liệu B được dịch đưa sang node gốc mới.
Mục dữ liệu A vẫn không đổi.
Hai node con bên phải nhất của node được phân chia bị hủy kết nối khỏi nó và
kết nối đến node mới bên phải.

Hình 4.5 Tách node gốc


i) Trước khi thêm vào
ii) Sau khi thêm vào

Hình 5 chỉ ra việc tách node gốc. Tiến trình này tạo ra một node gốc mới ở mức cao
hơn mức của node gốc cũ. Kết quả là chiều cao tổng thể của cây được tăng lên 1.
Đi theo node được tách này, việc tìm kiếm điểm chèn tiếp tục đi xuống phía dưới của
cây. Trong hình 5 mục dữ liệu với khoá 41 được thêm vào lá phù hợp.

Tách theo hướng đi xuống


Chú ý rằng, bởi vì tất cả các node đầy được tách trên đường đi xuống nên việc tách
node không gây ảnh hưởng gì khi phải đi ngược lên trên của cây. Node cha của bất cứ node
nào bị tách phải đảm bảo rằng không phải là node đầy, để đảm bảo node cha này có thể chấp
nhận mục dữ liệu B mà không cần thiết nó phải tách ra. Tất nhiên nếu node cha này đã có
hai con thì khi node con bị tách, nó sẽ trở thành node đầy. Tuy nhiên điều này chỉ có nghĩa là
nó có thể sẽ bị tách ra khi lần tìm kiếm kế tiếp gặp nó.
Hình 6 trình bày một loạt các thao tác chèn vào một cây rỗng. Có 4 node được tách, 2
node gốc và 2 node lá.
Thêm vào 70, 30, 50
30, 50,
70
62
Thêm 40

Thêm vào 20, 80

Thêm vào 25, 90

Thêm vào 75
Thêm vào 10

Hình 6 Minh họa thêm một node vào cây 2-3-4

5. Biến đổi cây 2-3-4 sang cây Đỏ-Đen


Một cây 2-3-4 có thể được biến đổi sang cây đỏ-đen bằng cách áp dụng các luật sau:

Biến đổi bất kỳ 2-node ở cây 2-3-4 sang node đen ở cây đỏ-đen.
Biến đổi bất kỳ 3-node sang node con C (với hai con của chính nó) và node cha
P (với các node con C và node con khác). Không có vấn đề gì ở đây khi một mục
trở thành node con và mục khác thành node cha. C được tô màu đỏ và P được tô
màu đen.

63
Biến đổi bất kỳ 4-node sang node cha P và cả hai node con C1, C2 màu đỏ.

Hình 4.7 trình bày các chuyển đổi này. Các node con trong các cây con được tô màu đỏ; tất
cả các node khác được tô màu đen.

Hình 7 Chuyển đổi từ cây 2-3-4 sang cây đỏ-đen

Hình 4.8 trình bày cây 2-3-4 và cây đỏ-đen tương ứng với nó bằng cách áp dụng các
chuyển đổi này. Các đường chấm xung quanh các cây con được tạo ra từ 3-node và 4-nút.
Các luật của cây đỏ-đen tự động thoả mãn với sự chuyển đổi này. Kiểm tra rằng: Hai node

64
đỏ không bao giờ được kết nối, và số lượng các node đen là như nhau ở mọi đường dẫn từ
gốc đến lá (hoặc node con null).

Hình 4.8 Cây 2-3-4 và cây đỏ-đen tương ứng

65

You might also like