You are on page 1of 37

Cấu trúc dữ liệu cây

Cấu trúc dữ liệu cây

Mục lục
1 Dẫn nhập 3

2 Một số khái niệm 3

3 Các loại cây thường gặp 4

4 Biểu diễn cây 4

5 Các phương pháp duyệt cây 4

6 Một số cây cơ bản 5


6.1 Cây nhị phân, cây nhị phân tìm kiếm . . . . . . . . . . . . . . . . . . . . . 5
6.2 B-Tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
6.3 B+-Tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
6.4 B*-Tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
6.5 Trie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
6.6 Cây nhị phân tìm kiếm tự cân bằng (Self - balancing BST) . . . . . . . . . 9
6.6.1 AVL Tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
6.6.2 Splay Tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
6.6.3 Red - Black Tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
6.6.4 AA Tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11

7 Các thao tác cơ bản trên cây 12

8 Cài đặt các loại cây cơ bản 12


8.1 Binary Search Tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
8.1.1 Class BST với Nested class Node . . . . . . . . . . . . . . . . . . . 12
8.1.2 Thêm key vào BST . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
8.1.3 Xóa key ra khỏi BST . . . . . . . . . . . . . . . . . . . . . . . . . . 14
8.1.4 Xóa hết node ra khỏi BST . . . . . . . . . . . . . . . . . . . . . . . 16
8.2 AVL Tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
8.2.1 Class AVL và Nested class Node . . . . . . . . . . . . . . . . . . . . 17
8.2.2 Tính chiều cao của cây . . . . . . . . . . . . . . . . . . . . . . . . . 18
8.2.3 Tính hệ số cân bằng của node . . . . . . . . . . . . . . . . . . . . . 19
8.2.4 Left rotation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
8.2.5 Right rotation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
8.2.6 Left right rotation và Right left rotation . . . . . . . . . . . . . . . 20
8.2.7 Cân bằng lại cây . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
8.2.8 Thêm node vào cây AVL và xóa node khỏi cây AVL . . . . . . . . . 22
8.3 Splay Tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
8.3.1 Class SplayTree và Nested class Node . . . . . . . . . . . . . . . . . 24
8.3.2 Zag rotation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25

Page 1/36
Cấu trúc dữ liệu cây

8.3.3 Zig rotation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26


8.3.4 Zig-zig, zig-zag, zag-zig, zag-zag . . . . . . . . . . . . . . . . . . . . 26
8.3.5 Thao tác splay . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
8.3.6 Thêm key vào Splay Tree . . . . . . . . . . . . . . . . . . . . . . . 28
8.3.7 Tìm kiếm key trên Splay Tree . . . . . . . . . . . . . . . . . . . . . 28
8.3.8 Xóa key ra khỏi Splay Tree . . . . . . . . . . . . . . . . . . . . . . 29
8.3.8.1 Thao tác split: . . . . . . . . . . . . . . . . . . . . . . . . 29
8.3.8.2 Thao tác join: . . . . . . . . . . . . . . . . . . . . . . . . . 30
8.4 B-Tree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
8.4.1 Class BTree và hai Nested class: Entry và Node . . . . . . . . . . . 32
8.4.2 Thao tác split . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
8.4.3 Thêm key vào B-Tree . . . . . . . . . . . . . . . . . . . . . . . . . . 34

9 Một số trang web để học tốt DSA 36

Page 2/36
Cấu trúc dữ liệu cây

1 Dẫn nhập
Cây là cấu trúc dữ liệu được sử dụng rộng rãi gồm tập hợp các nút (tiếng Anh: Node)
được liên kết với nhau theo quan hệ cha con. Cây trong cấu trúc dữ liệu là mô phỏng (hay
nói chính xác hơn là tham khảo) từ cây trong lý thuyết đồ thị (chỉ khác là cây trong cấu
trúc dữ liệu thì có gốc - root). Hầu như mọi khái niệm trong cây của lý thuyết đồ thị đều
được thể hiện trong cấu trúc dữ liệu. Tuy nhiên cây trong cấu trúc dữ liệu đã tìm được
ứng dụng phong phú và hiệu quả trong nhiều giải thuật. Khi phân tích các giải thuật trên
cấu trúc dữ liệu cây, người ta vẫn thường vẽ ra các cây tương ứng trong lý thuyết đồ thị.

2 Một số khái niệm


1. Nút - node: Chứa trường data mang dữ liệu bất kì và các liên kết để tạo thành cây.
Khi A liên kết với B, ta nói A là node cha của B, và B gọi là node con của A.

2. Nút gốc - root node: Mỗi cây đều có một node đặc biệt, gọi là node gốc (hay gốc cho
ngắn gọn). Gốc của cây là node duy nhất không có node cha, là nơi khởi đầu của
nhiều giải thuật trên cây. Tất cả các node khác đều được liên kết với node gốc bằng
các liên kết.

3. Nút lá - leaf node: Là các node không có node con nào. Các node này có tên khác là
node ngoài nhánh - external node. Các liên kết của node lá trỏ về NULL.

4. Nút trong nhánh - internal node: Các node có ít nhất một con thì gọi là node trong
nhánh.

5. Mức của nút - level of node: là tổng số liên kết trên đường đi duy nhất từ node gốc
đến node đó. Mức của gốc là 0, các node con cùng cha thì có cùng mức và bằng mức
của cha + 1.

6. Chiều cao của cây - height of tree: Tổng số node trên đường đi dài nhất từ node
gốc đến node lá bất kỳ.

7. Cây con - subtree: Với cây T bất kì, cây gồm 1 node thuộc T (ngoại trừ node gốc)
cùng với tất cả các node con của T tạo thành cây con của T.

Page 3/36
Cấu trúc dữ liệu cây

8. Nút anh em - siblings: Là các node có cùng mức với nhau

9. Bậc của node - degree of node: Là tổng số node con của node đó

10. Bậc của cây - degree of tree: Là bậc lớn nhất của tất cả các node có trên cây

Note: Từ node sẽ được dùng trong phần còn lại của bài.

3 Các loại cây thường gặp


1. Cây sắp thứ tự và cây không sắp thứ tự: Một cây không sắp thứ tự là cây mà các
node trong cây không có thứ tự nào, ngược lại cây có các node tuân thủ theo một
thứ tự xác định gọi là cây sắp thứ tự.

2. Cây tổng quát và cây nhị phân: Cây mà mỗi node có thể có nhiều hơn hai node con
gọi là cây tổng quát, ngược lại cây mà mỗi node có không quá hai node con gọi là
cây nhị phân.

4 Biểu diễn cây


Có rất nhiều cách để biểu diễn cây, trong đó cách phổ biến nhất là sử dụng con trỏ và
node. Ngoài ra, với cây nhị phân, ta có thể sử dụng mảng để biểu diễn cây nhị phân.
Trong bài viết này, chúng ta sẽ sử dụng các biểu diễn cây bằng node và các con trỏ

5 Các phương pháp duyệt cây


Duyệt cây là một trình tự làm việc với các node trong cây. Trình tự này giống như một
chuyến đi qua tất cả các node trên cây theo thứ tự cha-con, bắt đầu từ node gốc. Dựa vào
trình tự "viếng thăm" các node thì chúng ta có ba phương pháp duyệt chính sau đây:

1. Duyệt tiền thứ tự (NLR - duyệt node gốc trước, kế là duyệt bên trái, cuối cùng là
duyệt bên phải)

2. Duyệt trung thứ tự (LNR - duyệt bên trái trước, đến node gốc, cuối cùng là duyệt
bên phải)

Page 4/36
Cấu trúc dữ liệu cây

3. Duyệt hậu thứ tự (LRN - duyệt bên trái trước, kế là duyệt bên phải, cuối cùng là
node gốc)

Kỹ thuật đệ quy sẽ được sử dụng trong các cách duyệt cây này.
Ngoài ba cách duyệt này ra thì còn một cách duyệt nữa, đó là level-order (duyệt theo
mức của node). Khi này node gốc sẽ được duyệt trước, sau đó là các node ở mức 1, từ trái
sang phải, kế đến là các node ở mức 2,... Các node lá được duyệt cuối cùng.

6 Một số cây cơ bản


6.1 Cây nhị phân, cây nhị phân tìm kiếm
Cây nhị phân (Binary Tree) là cây mà mỗi node có không quá hai node con.

1. Cây nhị phân mà mỗi node (trừ node lá) đều có hai con gọi là cây nhị phân đầy đủ.

2. Cây nhị phân mà tại mỗi mức của node (trừ node lá và node cha của node lá), mỗi
node thuộc mức đó đều có hai node con gọi là cây nhị phân hoàn chỉnh. Tại mức của
node cha của node lá, các node lá không nhất thiết phải được lấp đầy, nhưng càng
nằm bên trái càng tốt.

Ví dụ tuyệt vời nhất của cây nhị phân hoàn chỉnh là Binary Heap.

3. Cây nhị phân mà tại mỗi mức của node (trừ node lá) đều có 2 con và các node lá ở
cùng một cấp gọi là cây nhị phân hoàn hảo.

Ví dụ tuyệt vời nhất của cây nhị phân hoàn hảo là phả hệ của một gia đình.

Câu hỏi: Với cây nhị phân hoàn hảo có chiều cao h thì sẽ có tất cả bao nhiêu node?

4. Cây nhị phân n node mà chiều cao của nó là O(log(n)) được gọi là cây nhị phân
cân bằng (Balanced Binary Tree). Ví dụ các cây nhị phân cân bằng: AVL, Red-Black
Tree, Scapegoat Tree,...

5. Cây nhị phân mà mỗi node (trừ node lá) chỉ có đúng một node con gọi là cây nhị
phân thoái hóa (degenerate binary tree). Cây này giống như một linked list.

Page 5/36
Cấu trúc dữ liệu cây

Cây nhị phân mà tại mỗi node t trong cây (trừ node lá), các node bên trái của t có giá
trị không vượt quá giá trị của t và các node bên phải của t có giá trị không nhỏ hơn giá
trị của t được gọi là cây nhị phân tìm kiếm (Binary Search Tree hay BST). BST có thể cải
thiện hiệu quả tìm kiếm, thêm node, xóa node do độ phức tạp về mặt thời gian của việc
tìm kiếm một node nào đó trên cây là O(log n).

6.2 B-Tree
Là dạng tổng quát hơn của BST do mỗi node trên B-Tree (trừ các node lá) đều có thể
có nhiều hơn hai node con. Hơn nữa, B-Tree là cây cân bằng, có các tính chất đặc biệt sau:

1. Tất cả node lá đều nằm ở cùng một level

2. Mỗi node của B-Tree có thể chứa nhiều giá trị (gọi là key), được giới hạn bởi bậc m
của nó. Tính chất này giúp B-Tree lưu trữ được nhiều dữ liệu hơn trên đĩa.
lmm lmm
3. Với cây B-Tree bậc m, mỗi node đều có tối thiểu − 1 key và node con, có
2 2
tối đa m - 1 key và m node con (gốc của cây có thể có tối thiểu 1 key).

4. Tại 1 node, các key được sắp xếp theo thứ tự tăng dần. Các node con giữa 2 key
key1 và key2 đều có key thỏa mãn key1 ≤ key ≤ key2

5. Các thao tác tìm kiếm, thêm node, xóa node trên B-Tree đều có độ phức tạp thời
gian là O(log n). Điều này giúp B-Tree là cấu trúc dữ liệu lý tưởng cho các vùng lưu
trữ vốn có tốc độ truy xuất dữ liệu chậm như CD-ROMs, phần cứng, bộ nhớ flash,...

6. Thông thường, B-Tree không cho phép có key trùng nhau.

Page 6/36
Cấu trúc dữ liệu cây

Hình 1: B-Tree bậc 3

6.3 B+-Tree
Là biến thể của B-Tree, điểm khác biệt của B+ Tree so với B-Tree là các node lá có
cấu trúc khác với các internal node: các internal node chỉ được dùng để indexing (chứa
data nhưng chỉ để xác định khoảng của các key ở các node con), trong khi các node lá
dùng để lưu data thật sự (lưu cả data của internal node). Các node con được kết nối với
nhau bằng các liên kết để tạo thành một danh sách liên kết đơn giúp việc thực hiện truy
vấn trên khoảng được tối ưu hơn. B+ Tree cho phép các key của node trùng nhau.

Hình 2: B+ Tree

6.4 B*-Tree
Cũng là một biến thể của B-Tree, với một tính chất đặc biệt là "two - to - three" split,
2
tức là mỗi node của cây sẽ chỉ chứa tối đa key so với số key tối đa mỗi node của B-Tree.
3
Nhược điểm lớn nhất của B*-Tree có lẽ là thao tác xóa rất phức tạp.

Page 7/36
Cấu trúc dữ liệu cây

Hình 3: B*-Tree bậc 3

6.5 Trie
Còn có tên gọi khác là cây tiền tố (prefix tree), là một cấu trúc dữ liệu dạng cây hữu
dụng được dùng để quản lí một tập hợp các xâu. Mặc dù dễ hiểu và dễ cài đặt, trie lại
có rất nhiều ứng dụng. Do vậy, trie thường xuyên xuất hiện trong các cuộc thi lập trình ở
Việt Nam nói riêng và quốc tế nói chung.
Trie có thể thực hiện ba loại truy vấn sau với độ phức tạp thời gian tuyến tính:

• Thêm một xâu vào trie

• Xóa một xâu ra khỏi trie

• Kiểm tra xem xâu có tồn tại trong trie hay không.

Page 8/36
Cấu trúc dữ liệu cây

Hình 4: Cây Trie

6.6 Cây nhị phân tìm kiếm tự cân bằng (Self - balancing BST)
Là một BST với thêm các thao tác và quy tắc để cân bằng cây giúp cải thiện hiệu quả
về mặt tìm kiếm node trên cây, có thể kể đến các loại cây sau:

6.6.1 AVL Tree

Là cây nhị phân tìm kiếm tự cân bằng dựa trên hằng số cân bằng (balance factor)
của mỗi node. Hằng số cân bằng của một node được tính bằng hiệu số giữa chiều cao của
subtree bên trái và chiều cao của subtree bên phải, và với cây AVL, các giá trị được chấp
nhận là -1, 0, 1. Nếu vi phạm, các thao tác rotate sẽ được thực hiện (sẽ nói rõ ở mục sau).
Nhược điểm lớn nhất của cây AVL là nếu tìm kiếm các node lá thường xuyên, chi phí
cân bằng lại cây sẽ rất lớn.

6.6.2 Splay Tree

Là cây nhị phân tìm kiếm tự cân bằng dựa trên việc splay (thực hiện các thao tác
rotate - hay ở đây gọi là các thao tác zig, zag) một node (sau khi thêm, tìm kiếm, xóa một
node khác ra khỏi cây) lên làm gốc của cây. Đối với những node được tìm kiếm thường

Page 9/36
Cấu trúc dữ liệu cây

xuyên, việc splay lên gốc tỏ ra hiệu quả vì khi này việc tìm kiếm node gốc có độ phức tạp
thời gian là O(1). Tuy nhiên, splay tree có hạn chế rất lớn là mất cân bằng chiều cao của
cây. Từ một splay tree có thể biến thành một linked list là điều hoàn toàn có thể xảy ra.

6.6.3 Red - Black Tree

Đúng như tên gọi của nó, Red - Black Tree là cây nhị phân tìm kiếm tự cân bằng dựa
trên màu của mỗi node (Red hoặc Black) cùng với các tính chất:

1. Gốc của cây luôn là màu đen.

2. Mọi node lá của Red - Black Tree (ở đây node lá của một node là các node NULL)
đều có màu đen.

3. Nếu một node có màu đỏ thì node con của nó có màu đen (nên là sẽ có trường hợp
một cây Red - Black Tree có tất cả node đều màu đen).

4. Tất cả các node lá màu đen đều có chung mức.

5. Tại mỗi node bất kì (tính cả gốc của cây), đường đi duy nhất từ node đó đến mọi
node cha của node lá bất kỳ (nếu tồn tại đường đi) đều có số lượng node màu đen
giống nhau (đây là điều kiện khó để cân bằng nhất).

Các cấu trúc dữ liệu trong STL (Standard Template Library) của C++ như map,
multiset, multimap (hoặc trong Java có java.util.TreeMap và java.util.TreeSet) đều
được cài đặt bằng Red - Black Tree.

Hình 5: Cây Red - Black Tree

Page 10/36
Cấu trúc dữ liệu cây

6.6.4 AA Tree

Là biến thể của Red - Black Tree. Điểm đặc biệt của AA Tree so với các cây nhị phân
tìm kiếm thông thường là node con bên phải có thể có cùng mức với node cha của nó (khi
này đường liên kết phải của node cha sẽ trở thành đường liên kết ngang).
Các định nghĩa trên cây AA:

• Mức (level) của một node: Khác với định nghĩa mức của một node truyền thống,
mức của node trên cây AA được định nghĩa là số liên kết trái từ node đó đến node
NULL (do đó mức của node lá trên cây AA bằng 1)

• Đường liên kết ngang (horizontal link): khi node con bên phải cùng mức với node
cha, đường liên kết phải sẽ thành đường liên kết ngang

• Xoay phải (right rotation - skew) và xoay trái (left rotation - split) là 2 thao tác
dùng để điều chỉnh cây AA

Các quy tắc sau luôn được tuân thủ trên cây AA:

1. Liên kết ngang luôn hướng về bên phải.

2. Không có 2 liên kết ngang liên tiếp nhau.

3. Mọi node có mức lớn hơn 1 đều có 2 node con.

4. Nếu một node không có liên kết ngang bên phải thì 2 node con của nó phải ở cùng
mức.

Tính chất:

1. Node con bên trái có mức nhỏ hơn node cha 1 đơn vị

2. Node con bên phải có mức nhỏ hơn hoặc bằng mức của node cha (không quá 1 đơn
vị)

3. Việc thêm node luôn được diễn ra tại mức 1

Page 11/36
Cấu trúc dữ liệu cây

Hình 6: AA Tree

7 Các thao tác cơ bản trên cây


Các thao tác cơ bản trên cây bao gồm:

1. Duyệt cây

2. Thêm một node vào cây

3. Xóa một node ra khỏi cây

4. Tìm kiếm một node trong cây

5. Xóa hết node trong cây (giải phóng bộ nhớ đã cấp phát cho cây)

8 Cài đặt các loại cây cơ bản


Note: các cài đặt dưới đây sử dụng ngôn ngữ C++, các ngôn ngữ khác thì dựa trên lý
thuyết đã học để cài đặt lại cho đúng.
Các cài đặt dưới đây có sử dụng khuôn mẫu hàm và generic data type cùng với lập trình
hướng đối tượng

8.1 Binary Search Tree


8.1.1 Class BST với Nested class Node

1 template < typename T >


2 class BST
3 {
4 public :
5 class Node ;

Page 12/36
Cấu trúc dữ liệu cây

6 protected :
7 Node * root ;
8 public :
9 BST () : root ( nullptr ) {}
10 ~ BST ()
11 {
12 this - > removeAllNode (& this - > root ) ;
13 }
14 private : // Support method
15 Node * addHelper ( Node * root , const T & key ) ;
16 Node * removeHelper ( Node * root , const T & key ) ;
17 bool searchHelper ( Node * root , const T & key ) ;
18 void printLNRHelper ( Node * root ) ;
19 void printLRNHelper ( Node * root ) ;
20 void printNLRHelper ( Node * root ) ;
21 void removeAllNode ( Node ** root ) ;
22 public : // Main method to call
23 void add ( const T & key ) ;
24 void remove ( const T & key ) ;
25 bool search ( const T & key ) ;
26 void printLNR () ;
27 void printLRN () ;
28 void printNLR () ;
29 class Node
30 {
31 private :
32 T key ;
33 Node * left , * right ;
34 friend class BST <T >;
35 public :
36 Node ( T key ) : key ( key ) , left ( nullptr ) ,
right ( nullptr ) {}
37 };
38 };

Các hàm có thuộc tính private là các hàm hỗ trợ (sử dụng đệ quy với Node* root
bất kì) và sẽ được gọi thông qua các main method có thuộc tính public.

8.1.2 Thêm key vào BST

Do BST tuân thủ quy luật: Với mỗi node t, các node bên trái của t đều có key không
vượt quá t→key và các node bên phải của t đều có key không nhỏ hơn t→key, do đó, ta
thực hiện đệ quy như sau:

Page 13/36
Cấu trúc dữ liệu cây

• Điều kiện dừng: nếu root đang là NULL, gán root = new Node(key) và return root

• Ngược lại, kiểm tra giá trị của key và giá trị root->key:

– Nếu key ≤ root->key, gọi đệ quy addHelper(root->left, key) và gán cho


root->left

– Ngược lại, gọi đệ quy addHelper(root->right, key) và gán cho root->right

Cuối cùng ta return root

Đoạn code tham khảo:

1 template < typename T >


2 typename BST <T >:: Node * BST <T >:: addHelper ( Node * root , const
T & key ) {
3 if ( root == nullptr ) return new Node ( key ) ;
4 if ( key <= root - > key ) root - > left =
addHelper ( root - > left , key ) ;
5 else root - > right = addHelper ( root - > right , key ) ;
6 return root ;
7 }

Khi này, hàm add(const T& key) được hiện thực như sau:

1 template < typename T >


2 void BST <T >:: add ( const T & key ) {
3 this - > root = addHelper ( this - > root , key ) ;
4 }

8.1.3 Xóa key ra khỏi BST

Giải thuật như sau:

• Nếu root đang NULL (khi này cây đang rỗng hoặc key không có trong BST), ta
return NULL

• Ngược lại: gọi đệ quy để xác định node chứa key cần xóa:

Page 14/36
Cấu trúc dữ liệu cây

– Nếu key < root->key, gọi đệ quy removeHelper(root->left, key) và gán


cho root->left
– Nếu key > root->key, gọi đệ quy removeHelper(root->right, key) và gán
cho root->right.
– Khi key == root->key, xét 3 trường hợp:
1. root->left == NULL, gán Node* temp = root->right, xóa root đi và re-
turn temp
2. root->right == NULL, gán Node* temp = root->left, xóa root đi và re-
turn temp
3. Cả root->left và root->right đều khác NULL, ta đi tìm giá trị thích
hợp trên cây để thay thế key hiện tại (gọi là temp_key. Có 2 cách thay thế:
thay bằng key lớn nhất của cây con bên trái hoặc thay bằng key bé nhất
của cây con bên phải. Ở đây mình chọn thay bằng key lớn nhất của cây con
bên trái (cách còn lại làm tương tự) và cách này chúng ta sẽ sử dụng cho
toàn bộ BST trở về sau.
Thay thế root->key bằng temp_key.
Thực hiện gọi đệ quy removeHelper(root->left, temp_key) và gán cho
root->left (nếu chọn key bé nhất bên phải thì thay root->left bằng
root->right).

Cuối cùng, ta return root.

Đoạn code tham khảo:

1 template < typename T >


2 typename BST <T >:: Node * BST <T >:: removeHelper ( Node * root ,
const T & key ) {
3 if ( root == nullptr )
4 return nullptr ;
5 if ( key < root - > key )
6 root - > left = removeHelper ( root - > left , key ) ;
7 else if ( key > root - > key )
8 root - > right = removeHelper ( root - > right , key ) ;
9 else {
10 if ( root - > left == nullptr ) {

Page 15/36
Cấu trúc dữ liệu cây

11 Node * temp = root - > right ;


12 delete root ;
13 return temp ;
14 }
15 else if ( root - > right == nullptr ) {
16 Node * temp = root - > left ;
17 delete root ;
18 return temp ;
19 }
20 else {
21 Node * replace = root - > left ;
22 while ( replace - > right != nullptr )
23 replace = replace - > right ;
24 root - > key = replace - > key ;
25 root - > left = removeHelper ( root - > left ,
replace - > key ) ;
26 }
27 }
28 return root ;
29 }

Khi này, hàm void remove(const T& key) được hiện thực như sau:

1 template < typename T >


2 void BST <T >:: remove ( const T & key ) {
3 this - > root = removeHelper ( this - > root , key ) ;
4 }

Bài tập về nhà:


Hiện thực các hàm helper còn lại (searchHelper, printLNRHelper, printLRNHelper,
printNLRHelper) và trong các main method, hãy gọi các hàm helper tương ứng với tham
số Node* root đầu vào của các hàm helper là this->root.

8.1.4 Xóa hết node ra khỏi BST

Để việc xóa được thuận tiện hơn, ta thực hiện xóa LRN (với mục đích xóa các node lá
trước, sau đó xóa node cha của chúng).
Đoạn code tham khảo:

Page 16/36
Cấu trúc dữ liệu cây

1 void BST <T >:: removeAllNode ( Node ** root ) {


2 if (* root == nullptr ) return ;
3 removeAllNode (&(* root ) -> left ) ;
4 removeAllNode (&(* root ) -> right ) ;
5 delete (* root ) ;
6 * root = nullptr ;
7 }

Độ phức tạp về thời gian (với h là chiều cao của BST):

1. Thêm key vào BST: O(h) do ta chỉ duyệt một trong hai nhánh để tìm ra vị trí cần
thêm node.

2. Xóa key ra khỏi BST: O(h). Tương tự như việc thêm key vào BST, ta cũng chỉ duyệt
một trong hai nhánh.

3. Search trên BST: O(h).

4. 3 phương pháp duyệt cây: O(n) với n là tổng số node trên BST.

5. Xóa toàn bộ node trên BST: O(n)

Note: Từ các cây sau chúng ta sẽ chỉ bàn đến hai thao tác chính: thêm node vào cây và
xóa node ra khỏi cây. Các hàm còn lại các bạn tự cài đặt nó, xem như bài tập

8.2 AVL Tree


8.2.1 Class AVL và Nested class Node

1 template < typename T >


2 class AVL
3 {
4 public :
5 class Node ;
6 protected :
7 Node * root ;
8 public :
9 AVL () : root ( nullptr ) {}
10 ~ AVL ()
11 {

Page 17/36
Cấu trúc dữ liệu cây

12 this - > removeAllNode (& this - > root ) ;


13 }
14 private : // Helper function for rotation and balance the
tree
15 int getHeight ( Node * root ) ;
16 int getBalance ( Node * root ) ;
17 Node * LRotate ( Node * x ) ;
18 Node * RRotate ( Node * x ) ;
19 Node * LRRotate ( Node * x ) ;
20 Node * RLRotate ( Node * x ) ;
21 Node * balance ( Node * x ) ;
22 private : // Support method
23 Node * addHelper ( Node * root , const T & key ) ;
24 Node * removeHelper ( Node * root , const T & key ) ;
25 void removeAllNode ( Node ** root )
26 {
27 if (* root == nullptr ) return ;
28 removeAllNode (&(* root ) -> left ) ;
29 removeAllNode (&(* root ) -> right ) ;
30 delete (* root ) ;
31 * root = nullptr ;
32 }
33 public : // Main method to call
34 void add ( const T & key ) ;
35 void remove ( const T & key ) ;
36 class Node
37 {
38 private :
39 T key ;
40 Node * left , * right ;
41 friend class AVL <T >;
42 public :
43 Node ( T key ) : key ( key ) , left ( nullptr ) ,
right ( nullptr ) {}
44 };
45 };

8.2.2 Tính chiều cao của cây

Chiều cao của cây với gốc là root sẽ được tính bằng cách gọi đệ quy cho root->left
và root->right, cứ đi qua mỗi node thì ta cộng thêm 1, cuối cùng return giá trị lớn nhất
trong 2 kết quả trả về:

Page 18/36
Cấu trúc dữ liệu cây

Đoạn code tham khảo:

1 template < typename T >


2 int AVL <T >:: getHeight ( Node * root ) {
3 if ( root == nullptr ) return 0;
4 return 1 + max ( getHeight ( root - > left ) ,
getHeight ( root - > right ) ) ;
5 }

8.2.3 Tính hệ số cân bằng của node

Hệ số cân bằng của một node bất kỳ bằng hiệu số giữa chiều cao của subtree bên trái
và chiều cao của subtree bên phải. Nếu node đó NULL thì return 0.
Đoạn code tham khảo:

1 template < typename T >


2 int AVL <T >:: getBalance ( Node * root ) {
3 return ( root == nullptr ? 0 : getHeight ( root - > left ) -
getHeight ( root - > right ) ;
4 }

8.2.4 Left rotation

Điều kiện thực hiện: Khi node cần rotate có node con bên phải khác NULL. Để thực
hiện left rotation tại node x, ta làm các bước:

1. Gọi y là node con bên phải của x

2. Gán con bên trái của y cho con bên phải của x

3. Gán x cho con bên trái của y

Cuối cùng ta return y.


Đoạn code tham khảo:

1 template < typename T >


2 typename AVL <T >:: Node * AVL <T >:: LRotate ( Node * x ) {
3 Node * y = x - > right ;

Page 19/36
Cấu trúc dữ liệu cây

4 x - > right = y - > left ;


5 y - > left = x ;
6 return y ;
7 }

8.2.5 Right rotation

Điều kiện thực hiện: Khi node cần rotate có node con bên trái khác NULL. Để thực
hiện right rotation tại node x, ta làm các bước:

1. Gọi y là node con bên trái của x

2. Gán con bên phải của y cho con bên trái của x

3. Gán x cho con bên phải của y

Cuối cùng ta return y.


Đoạn code tham khảo:

1 template < typename T >


2 typename AVL <T >:: Node * AVL <T >:: RRotate ( Node * x ) {
3 Node * y = x - > left ;
4 x - > left = y - > right ;
5 y - > right = x ;
6 return y ;
7 }

8.2.6 Left right rotation và Right left rotation

Hai hàm này là không bắt buộc, tuy nhiên chúng ta vẫn hiện thực nó để dễ hình dung
hơn.
Để thực hiện Left right rotation trên node x, ta thực hiện left rotation trên x->left và
gán lại vào x->left, và return node nhận được sau khi thực hiện right rotation tại node
x.
Để thực hiện Right left rotation trên node x, ta thực hiện right rotation trên x->right
và gán lại vào x->right, và return node nhận được sau khi thực hiện left rotation tại node
x.

Page 20/36
Cấu trúc dữ liệu cây

Đoạn code tham khảo:

1 template < typename T >


2 typename AVL <T >:: Node * AVL <T >:: LRRotate ( Node * x ) {
3 x - > left = LRotate (x - > left ) ;
4 return RRotate ( x ) ;
5 }
6

7 template < typename T >


8 typename AVL <T >:: Node * AVL <T >:: RLRotate ( Node * x ) {
9 x - > right = RRotate (x - > right ) ;
10 return LRotate ( x ) ;
11 }

8.2.7 Cân bằng lại cây

Bước này là cần thiết sau khi thêm hoặc xóa một key. Các bước để cân bằng lại cây
tại node x như sau:

1. Nếu x đang NULL, ta return NULL và không làm gì cả.

2. Ngược lại, thực hiện tính bal_factor = getBalance(x):

• Nếu bal_factor > 1 (cây bị lệch trái): Nếu getBalance(x->left) > 0, ta


thực hiện right rotate tại x, ngược lại ta thực hiện left right rotation tại x.

• Nếu bal_factor < -1 (cây bị lệch phải): Nếu getBalance(x->right) > 0, ta


thực hiện right left rotate tại x, ngược lại ta thực hiện left rotation tại x.

Cuối cùng ta return x.


Có thể xem hình sau để dễ hình dung hơn:

Page 21/36
Cấu trúc dữ liệu cây

Hình 7: Các trường hợp cân bằng cây AVL

Đoạn code tham khảo:

1 template < typename T >


2 typename AVL <T >:: Node * AVL <T > balance ( Node * x ) {
3 if ( x == nullptr ) return nullptr ;
4 int bal_fac = getBalance ( x ) ;
5 if ( bal_fac > 1) {
6 return ( getBalance (x - > left ) > 0? RRotate ( x ) :
LRRotate ( x ) ) ;
7 }
8 else if ( bal_fac < -1) {
9 return ( getBalance (x - > right ) > 0? RLRotate ( x ) :
LRotate ( x ) ) ;
10 }
11 }

8.2.8 Thêm node vào cây AVL và xóa node khỏi cây AVL

Tương tự như thêm node vào BST và xóa node ra khỏi BST, chúng ta chỉ cần thêm
bước cân bằng cây là xong.

Page 22/36
Cấu trúc dữ liệu cây

Đoạn code tham khảo:

1 template < typename T >


2 typename AVL <T >:: Node * AVL <T >:: addHelper ( Node * root , const
T & key ) {
3 if ( root == nullptr ) return new Node ( key ) ;
4 if ( key <= root - > key ) root - > left =
addHelper ( root - > left , key ) ;
5 else root - > right = addHelper ( root - > right , key ) ;
6 return balance ( root ) ;
7 }
8

9 template < typename T >


10 typename AVL <T >:: Node * AVL <T >:: removeHelper ( Node * root ,
const T & key ) {
11 if ( root == nullptr )
12 return nullptr ;
13 if ( key < root - > key )
14 root - > left = removeHelper ( root - > left , key ) ;
15 else if ( key > root - > key )
16 root - > right = removeHelper ( root - > right , key ) ;
17 else {
18 if ( root - > left == nullptr ) {
19 Node * temp = root - > right ;
20 delete root ;
21 return temp ;
22 }
23 else if ( root - > right == nullptr ) {
24 Node * temp = root - > left ;
25 delete root ;
26 return temp ;
27 }
28 else {
29 Node * replace = root - > left ;
30 while ( replace - > right != nullptr )
31 replace = replace - > right ;
32 root - > key = replace - > key ;
33 root - > left = removeHelper ( root - > left ,
replace - > key ) ;
34 }
35 }
36 return balance ( root ) ;
37 }

Page 23/36
Cấu trúc dữ liệu cây

Khi đó, các hàm void add(const T& key) và void remove(const T& key) có thể
hiện thực như sau:

1 template < typename T >


2 void AVL <T >:: add ( const T & key ) {
3 this - > root = addHelper ( this - > root , key ) ;
4 }
5

6 template < typename T >


7 void AVL <T >:: remove ( const T & key ) {
8 this - > root = removeHelper ( this - > root , key ) ;
9 }

8.3 Splay Tree


Note: Đây là cây duy nhất chúng ta sẽ thêm thao tác search key

8.3.1 Class SplayTree và Nested class Node

1 template < typename T >


2 class SplayTree
3 {
4 public :
5 class Node ;
6 protected :
7 Node * root ;
8 public :
9 SplayTree () : root ( nullptr ) {}
10 ~ SplayTree ()
11 {
12 this - > removeAllNode (& this - > root ) ;
13 }
14 private : // Helper function for rotation , splay operation
and delete operation
15 void LRotate ( Node * x ) ;
16 void RRotate ( Node * x ) ;
17 void splay ( Node * x ) ;
18 void split ( Node *& x , Node *& s , Node *& t ) ;
19 Node * join ( Node * s , Node * t ) ;
20 private : // Support method
21 Node * removeHelper ( Node * root , const T & key ) ;

Page 24/36
Cấu trúc dữ liệu cây

22 bool searchHelper ( Node * root , const T & key , Node *&


prev ) ;
23 void removeAllNode ( Node ** root )
24 {
25 if (* root == nullptr ) return ;
26 removeAllNode (&(* root ) -> left ) ;
27 removeAllNode (&(* root ) -> right ) ;
28 delete (* root ) ;
29 * root = nullptr ;
30 }
31 public : // Main method to call
32 void add ( const T & key ) ;
33 void remove ( const T & key ) ;
34 bool search ( const T & key ) ;
35 class Node
36 {
37 private :
38 T key ;
39 Node * left , * right , * parent ;
40 friend class SplayTree <T >;
41 public :
42 Node ( T key ) : key ( key ) , left ( nullptr ) ,
right ( nullptr ) , parent ( nullptr ) {}
43 };
44 };

Do có sự xuất hiện của Node* parent để trỏ tới node cha của node hiện tại cho nên
các thao tác left rotation (zag rotation), right rotation (zig rotation) và splay không cần
phải return node nữa, tuy nhiên chúng ta cần phải cập nhật mối liên hệ parent cho hợp
lý.

8.3.2 Zag rotation

Zag rotation (single left rotation) có đôi nét giống với left rotation của AVL, chúng ta
cần cập nhật lại các mối liên hệ parent cho phù hợp với mối quan hệ cha - con giữa các
node.
Đoạn code tham khảo:

1 template < typename T >


2 void SplayTree <T >:: LRotate ( Node * x ) {

Page 25/36
Cấu trúc dữ liệu cây

3 Node * y = x - > right ;


4 x - > right = y - > left ;
5 if (y - > left != nullptr ) y - > left - > parent = x ;
6 y - > parent = x - > parent ;
7 if (x - > parent == nullptr ) this - > root = y ;
8 else if ( x == x - > parent - > left ) x - > parent - > left = y ;
9 else x - > parent - > right = y ;
10 y - > left = x ;
11 x - > parent = y ;
12 }

8.3.3 Zig rotation

Zig rotation (single right rotation) có đôi nét giống với right rotation của AVL, chúng
ta cần đảm bảo mối liên hệ cha - con thông qua con trỏ parent cho phù hợp.
Đoạn code tham khảo:

1 template < typename T >


2 void SplayTree <T >:: RRotate ( Node * x ) {
3 Node * y = x - > left ;
4 x - > left = y - > right ;
5 if (y - > right != nullptr ) y - > right - > parent = x ;
6 y - > parent = x - > parent ;
7 if (x - > parent == nullptr ) this - > root = y ;
8 else if ( x == x - > parent - > right ) x - > parent - > right = y ;
9 else x - > parent - > left = y ;
10 y - > right = x ;
11 x - > parent = y ;
12 }

8.3.4 Zig-zig, zig-zag, zag-zig, zag-zag

Đây thực chất chỉ là sự kết hợp giữa hai thao tác zig và zag, chúng ta không hiện thực
nó để đơn giản hơn cho bạn đọc.

8.3.5 Thao tác splay

Splay là thao tác đưa một node lên làm gốc. Gọi x là node cần splay lên làm gốc của
cây, có các trường hợp sau:

Page 26/36
Cấu trúc dữ liệu cây

• Nếu x->parent->parent đang NULL, ta kiểm tra xem x đang là con trái hay con
phải của x->parent, nếu là con trái thì thực hiện RRotate, ngược lại thì thực hiện
LRotate.

• Ngược lại, có các trường hợp sau:

1. Nếu x, x->parent, x->parent->parent nằm trên cùng 1 đường thẳng (cùng


hướng trái hoặc cùng hướng phải): Nếu cùng hướng trái thì thực hiện 2 phép
RRotate lên x->parent, ngược lại thì thực hiện 2 phép LRotate lên x->parent.

2. Ngược lại, nếu x->parent nằm bên phải x->parent->parent, thực hiện liên
tiếp RRotate lên x->parent và LRotate lên x->parent. Ngược lại thì thực hiện
liên tiếp LRotate lên x->parent và RRotate lên x->parent.

Lặp lại các bước trên cho đến khi x lên làm gốc.
Đoạn code tham khảo:

1 template < typename T >


2 void SplayTree <T >:: splay ( Node * x ) {
3 if ( x == nullptr ) return ;
4 while (x - > parent != nullptr ) {
5 if (x - > parent - > parent == nullptr ) {
6 if ( x == x - > parent - > left )
rightRotate (x - > parent ) ;
7 else leftRotate (x - > parent ) ;
8 }
9 else if ( x == x - > parent - > left && x - > parent ==
x - > parent - > parent - > left ) {
10 rightRotate (x - > parent - > parent ) ;
11 rightRotate (x - > parent ) ;
12 }
13 else if ( x == x - > parent - > right && x - > parent ==
x - > parent - > parent - > right ) {
14 leftRotate (x - > parent - > parent ) ;
15 leftRotate (x - > parent ) ;
16 }
17 else if ( x == x - > parent - > left && x - > parent ==
x - > parent - > parent - > right ) {
18 rightRotate (x - > parent ) ;
19 leftRotate (x - > parent ) ;
20 }

Page 27/36
Cấu trúc dữ liệu cây

21 else {
22 leftRotate (x - > parent ) ;
23 rightRotate (x - > parent ) ;
24 }
25 }
26 }

8.3.6 Thêm key vào Splay Tree

Tương tự như việc thêm key vào BST. Tuy nhiên sau khi thêm node chứa key vào Splay
Tree, chúng ta thực hiện splay node này lên làm gốc:
Chúng ta không sử dụng hàm addHelper cùng với kỹ thuật đệ quy ở đây do chỉ cần
cập nhật lại con trỏ parent của node mới thêm vào.
Đoạn code tham khảo:

1 template < typename T >


2 void SplayTree <T >:: add ( const T & key ) {
3 Node * tmp = new Node ( key ) ;
4 Node * x = this - > root , * y = nullptr ;
5 while ( x != nullptr ) {
6 y = x;
7 if (x - > key > tmp - > key ) x = x - > left ;
8 else x = x - > right ;
9 }
10 tmp - > parent = y ;
11 if ( y == nullptr ) this - > root = tmp ;
12 else if ( tmp - > key < y - > key ) y - > left = tmp ;
13 else y - > right = tmp ;
14 splay ( tmp ) ;
15 }

8.3.7 Tìm kiếm key trên Splay Tree

Ta thực hiện tìm kiếm tương tự như việc tìm kiếm trên BST, có hai điểm cần lưu ý:

1. Nếu tìm thấy node chứa key: ta thực hiện splay node đó lên làm gốc.

2. Nếu không tìm thấy, ta splay node cuối cùng trên đường đi tìm node chứa key đó
lên làm gốc.

Page 28/36
Cấu trúc dữ liệu cây

Đoạn code tham khảo:

1 template < typename T >


2 typename SplayTree < T :: Node * SplayTree < T :: searchHelper ( Node *
root , T key , Node *& prev ) {
3 if ( root == nullptr ) return NULL ;
4 if ( root - > key == key ) return root ;
5 prev = root ;
6 if ( root - > key > key ) {
7 return searchHelper ( root - > left , key , prev ) ;
8 }
9 return searchHelper ( root - > right , key , prev ) ;
10 }

Con trỏ prev dùng để lưu lại node cha của node root (do hàm đã return vị trí của
node chứa key và không thể return con trỏ parent trước đó).
Hiện thực hàm search như sau:

1 template < typename T >


2 bool SplayTree <T >:: search ( const T & key ) {
3 Node * prev = NULL ;
4 Node * tmp = searchHelper ( root , key , prev ) ;
5 if ( tmp != nullptr ) {
6 splay ( tmp ) ;
7 return true ;
8 }
9 splay ( prev ) ;
10 return false ;
11 }

8.3.8 Xóa key ra khỏi Splay Tree

Thao tác xóa trên Splay Tree thì phức tạp hơn, chúng ta cần thêm hai thao tác sau:

8.3.8.1 Thao tác split:


Split là thao tác tách một cây ra thành hai cây con. Để thực hiện split cây với gốc là
x thành 2 cây con với gốc là s và t, ta làm như sau:

1. Splay node x lên làm gốc

Page 29/36
Cấu trúc dữ liệu cây

2. Gán cây con phải của x cho t, khi này hoặc là t = NULL, hoặc t->parent = NULL

3. Gán x cho s, trỏ liên kết phải của s về NULL (do t đang là cây con phải của x) và
cho x = NULL.

Đoạn code tham khảo:

1 template < typename T >


2 void SplayTree <T >:: split ( Node *& x , Node *& s , Node *& t ) {
3 splay ( x ) ;
4 if (x - > right != nullptr ) {
5 t = x - > right ;
6 t - > parent = nullptr ;
7 }
8 else t = nullptr ;
9 s = x;
10 s - > right = nullptr ;
11 x = nullptr ;
12 }

8.3.8.2 Thao tác join:


Dùng để nối hai cây con lại với nhau. Để nối hai cây con s và t, ta làm như sau:

1. Nếu một trong 2 cây con s và t đang rỗng, ta return cây con còn lại.

2. Ngược lại, tìm node x là node có key lớn nhất của s, thực hiện splay node x này lên
làm gốc, cho cây con phải của x là t. Nếu t không NULL thì gán t->parent = x.

Cuối cùng ta return x.


Đoạn code tham khảo:

1 template < typename T >


2 typename SplayTree <T >:: Node * SplayTree <T >:: join ( Node * s ,
Node * t ) {
3 if ( s == nullptr ) return t ;
4 Node * x = findMax ( s ) ;
5 splay ( x ) ;
6 x - > right = t ;
7 if ( t != nullptr ) t - > parent = x ;
8 return x ;

Page 30/36
Cấu trúc dữ liệu cây

9 }

Cuối cùng là thao tác xóa key ra khỏi Splay Tree. Chúng ta tìm vị trí của node chứa
key cần xóa (nếu không tìm thấy thì splay node cuối cùng trên đường đi tìm node chứa
key cần xóa lên làm gốc), xong rồi thực hiện thao tác các thao tác split và join. Kết thúc
xóa ta return root hiện tại của Splay Tree (không phải return tham số root).
Để dễ hình dung thì đây là đoạn code tham khảo:

1 template < typename T >


2 typename SplayTree <T >:: Node *
SplayTree <T >:: removeHelper ( Node * root , const T & key ) {
3 Node * x = nullptr ;
4 Node * s , * t ;
5 Node * temp = root , * prev = nullptr ;
6 while ( temp != nullptr ) {
7 if ( temp - > key == key ) {
8 x = temp ;
9 break ;
10 }
11 if ( temp - > key < key ) {
12 prev = temp ;
13 temp = temp - > right ;
14 }
15 else {
16 prev = temp ;
17 temp = temp - > left ;
18 }
19 }
20 if ( x == nullptr ) {
21 splay ( prev ) ;
22 return nullptr ;
23 }
24 split (x , s , t ) ;
25 if (s - > left != nullptr ) s - > left - > parent = nullptr ;
26 this - > root = join (s - > left , t ) ;
27 delete ( s ) ;
28 s = nullptr ;
29 return this - > root ;
30 }

Và trong hàm void remove(const T& key), ta chỉ cần gọi lại hàm removeHelper với

Page 31/36
Cấu trúc dữ liệu cây

tham số truyền vào là this->root và key, xong gán lại cho this->root.
Code tham khảo:

1 template < typename T >


2 void SplayTree <T >:: remove ( const T & key ) {
3 this - > root = removeHelper ( this - > root , key ) ;
4 }

8.4 B-Tree
Note: Phần xóa key ra khỏi B-Tree sẽ không được đề cập do cách xóa phức tạp

8.4.1 Class BTree và hai Nested class: Entry và Node

1 # include < vector >


2 # include < stack >
3 # include < utility >
4 template < typename T >
5 class BTree
6 {
7 public :
8 class Node ;
9 class Entry ;
10 protected :
11 Node * root ;
12 int order ;
13 public :
14 BTree ( int m = 3) : root ( nullptr ) , order ( m ) {}
15 ~ BTree ()
16 {
17 cleanUp (& this - > root ) ;
18 }
19 private : // Support method
20 void splitNode ( Node * prev , Node * temp ) ;
21 void cleanUp ( Node ** root )
22 {
23 if (* root != nullptr ) {
24 cleanUp (&(* root ) -> firstPtr ) ;
25 for ( int i = 0; i <
( int ) (* root ) -> entries . size () ; i ++) {
26 cleanUp (&(* root ) -> entries [ i ]. rightPtr ) ;

Page 32/36
Cấu trúc dữ liệu cây

27 }
28 delete (* root ) ;
29 (* root ) = nullptr ;
30 }
31 }
32 public :
33 void add ( const T & key ) ;
34 class Entry
35 {
36 private :
37 T key ;
38 Node * rightPtr ;
39 friend class BTree <T >;
40 public :
41 Entry ( T key ) : key ( key ) , rightPtr ( nullptr ) ;
42 };
43 class Node
44 {
45 private :
46 vector < Entry > entries ;
47 Node * firstPtr ;
48 friend class BTree <T >;
49 public :
50 Node () : firstPtr ( nullptr ) {}
51 };
52 }

Ở đây, vector được sử dụng thay thế cho mảng bình thường để tiện hơn trong việc
thêm phần tử vào mảng.
Class Entry có con trỏ Node* rightPtr để lưu trữ các Node có key lớn hơn key của
Entry đó.
Class Node bao gồm một mảng kiểu Entry có kích thước tối đa là bằng bậc của B-Tree
- 1 và một con trỏ Node* firstPtr để lưu trữ các Node có key nhỏ hơn key của Entry
đầu tiên trong mảng.

8.4.2 Thao tác split

Split là thao tác tách một node có số key bằng với bậc của B-Tree thành hai node con,
mỗi node con chứa 1 nửa số Entry của node ban đầu (không tính phần tử chính giữa của
mảng) và đẩy phần tử tại vị trí chính giữa mảng lên node cha của nó.

Page 33/36
Cấu trúc dữ liệu cây

Code tham khảo:

1 template < typename T >


2 void BTree <T >:: splitNode ( Node * prev , Node * temp ) {
3 Entry median = temp - > entries [ temp - > entries . size () / 2];
4 temp - > entries . erase ( temp - > entries . begin () +
( temp - > entries . size () / 2) , temp - > entries . begin () +
( temp - > entries . size () / 2 + 1) ) ;
5 Node * temp1 = new Node ;
6 temp1 - > entries = vector < Entry >( temp - > entries . begin () +
order / 2 , temp - > entries . end () ) ;
7 temp - > entries = vector < Entry >( temp - > entries . begin () ,
temp - > entries . begin () + order / 2) ;
8 if ( prev == nullptr ) {
9 this - > root = new Node ;
10 this - > root - > entries . push_back ( median ) ;
11 this - > root - > firstPtr = temp ;
12 temp1 - > firstPtr = this - > root - > entries [0]. rightPtr ;
13 this - > root - > entries [0]. rightPtr = temp1 ;
14 }
15 else {
16 int i = ( int ) prev - > entries . size () - 1;
17 while ( i >= 0 && prev - > entries [ i ]. key >= median . key )
18 {
19 i - -;
20 }
21 median . rightPtr = temp1 ;
22 prev - > entries . insert ( prev - > entries . begin () + ( i +
1) , median ) ;
23 }
24 }

Do không sử dụng thư viện algorithm để sort mảng nên chúng ta sử dụng vòng while
để xác định vị trí mà key của median nên thuộc về.

8.4.3 Thêm key vào B-Tree

Khi thêm key vào B-Tree, ta luôn thực hiện ở node lá của cây, sau đó thực hiện split
từ node lá lên nếu node lá có số key = bằng bậc của B-Tree, cho đến khi không thể split
được nữa thì ta dừng lại.

Page 34/36
Cấu trúc dữ liệu cây

Chúng ta sử dụng stack và thư viện utility (để dùng cấu trúc dữ liệu pair, có thể
tự implement nếu muốn) để không cần phải cài đặt bằng đệ quy.
Code tham khảo:

1 template < typename T >


2 void BTree <T >:: add ( const T & key ) {
3 Node * temp = this - > root , * prev = nullptr ;
4 stack < pair < Node * , Node * > > st ;
5 st . push ( make_pair ( prev , temp ) ) ;
6 while ( temp != nullptr ) {
7 if ( temp - > firstPtr == nullptr ) {
8 int i = ( int ) temp - > entries . size () - 1;
9 while ( i >= 0 && temp - > entries [ i ]. key >= key )
i - -;
10 temp - > entries . insert ( temp - > entries . begin () + ( i
+ 1) , Entry ( key ) ) ;
11 st . push ( make_pair ( prev , temp ) ) ;
12 break ;
13 }
14 if ( key < temp - > entries [0]. key ) {
15 prev = temp ;
16 temp = temp - > firstPtr ;
17 st . push ( make_pair ( prev , temp ) ) ;
18 }
19 else {
20 for ( int i = ( int ) temp - > entries . size () - 1; i
>= 0; i - -) {
21 if ( key >= temp - > entries [ i ]. key ) {
22 prev = temp ;
23 temp = temp - > entries [ i ]. rightPtr ;
24 st . push ( make_pair ( prev , temp ) ) ;
25 break ;
26 }
27 }
28 }
29 }
30 if ( prev == nullptr ) {
31 if ( temp == nullptr ) {
32 this - > root = new Node ;
33 this - > root - > entries . push_back ( Entry ( key ) ) ;
34 }
35 else {
36 if (( int ) this - > root - > entries . size () == order ) {

Page 35/36
Cấu trúc dữ liệu cây

37 splitNode ( prev , temp ) ;


38 }
39 }
40 }
41 else {
42 while (! st . empty () ) {
43 pair < Node * , Node * > p = st . top () ;
44 st . pop () ;
45 if (( int ) p . second - > entries . size () == order ) {
46 splitNode ( p . first , p . second ) ;
47 }
48 }
49 }
50 }

9 Một số trang web để học tốt DSA


1. Youtube:

• Data Structures and Algorithms playlist (thực hiện bởi kênh Jenny’s Lectures
CS IT): https://www.youtube.com/@JennyslecturesCSIT

• Data Structures playlist (thực hiện bởi kênh Neso Academy): https://www.
youtube.com/@nesoacademy

2. Web:

• GeeksforGeeks: https://www.geeksforgeeks.org

• Programiz: https://www.programiz.com/dsa

Page 36/36

You might also like