This programming guide is only available in Vietnamese. Switch to Vietnamese to read the full article.
Chào mừng bạn đến với bài học tổng kết cuối cùng của chuỗi tự học C++ - một bài học Capstone Project tích hợp toàn bộ kiến thức OOP, quản lý con trỏ, cấu trúc dữ liệu nâng cao, và lập trình ứng dụng tương tác trên web. Trong bài viết này, chúng ta sẽ:
- Xây dựng Cây tìm kiếm nhị phân (BST) hoàn chỉnh với các phép toán Insert, Delete, Search và Traversal
- Tìm hiểu sâu về Cây AVL tự cân bằng (Self-Balancing Trees) với các phép quay (Rotations)
- Khám phá kiến trúc Visualizer Canvas - cách vẽ cây lên màn hình và tương tác real-time
- Áp dụng Design Patterns (Observer, Strategy, MVC) vào dự án thực tế
- Tìm hiểu các cấu trúc dữ liệu nâng cao khác và ứng dụng trong công nghệ hiện đại
1. Định nghĩa & Tính chất Cây tìm kiếm nhị phân (BST)
Cây tìm kiếm nhị phân (Binary Search Tree - BST) là một cấu trúc dữ liệu phân cấp dạng cây nhị phân có các ràng buộc:
- Cấu trúc nhị phân: Mỗi nút tối đa có 2 nút con (con trái và con phải)
-
Ràng buộc giá trị: Với bất kỳ nút nào:
- Tất cả giá trị ở cây con trái < giá trị của nút cha
- Tất cả giá trị ở cây con phải > giá trị của nút cha
- Tính chất đệ quy: Cây con trái và cây con phải của mỗi nút cũng phải là BST hợp lệ
Nhờ quy luật phân cấp này, việc tìm kiếm trên cây cân bằng chỉ tốn độ phức tạp trung bình là O(log n) - nhanh hơn linear search O(n) rất nhiều. Tuy nhiên, nếu cây bị mất cân bằng (ví dụ chỉ có nhánh trái liên tiếp), độ phức tạp có thể suy giảm thành O(n).
2. Thiết kế BST bằng C++ OOP
Để xây dựng BST trong C++, chúng ta cần:
- Lớp
Nodeđại diện cho từng nút với giá trị và con trỏ đến nút con - Lớp
BSTchứa gốc cây (root) và các phương thức công khai (public methods) - Các phương thức riêng (private methods) để thực hiện thuật toán đệ quy
struct BSTNode {
int value;
BSTNode* left;
BSTNode* right;
int height; // Dung cho AVL trees sau nay
BSTNode(int val)
: value(val), left(nullptr), right(nullptr), height(1) {}
};
#include <iostream>
#include <queue>
#include <vector>
class BST {
private:
BSTNode* root;
// Private recursive methods
BSTNode* insertNode(BSTNode* node, int value);
BSTNode* searchNode(BSTNode* node, int value);
BSTNode* deleteNode(BSTNode* node, int value);
BSTNode* findMin(BSTNode* node);
void inOrderTraversal(BSTNode* node, std::vector<int>& result);
void deleteTree(BSTNode* node);
public:
BST() : root(nullptr) {}
~BST() { deleteTree(root); }
// Public interface
void insert(int value);
bool search(int value);
void deleteValue(int value);
std::vector<int> getInOrder();
void print();
};
3. Thuật toán BST: Insert, Delete, Search - Deep Dive
Đây là phần lõi của BST. Chúng ta sẽ tìm hiểu chi tiết từng phép toán và các trường hợp đặc biệt.
3.1 Phép Insert (Chèn)
Khi chèn một giá trị mới vào BST, chúng ta luôn chèn ở lá cây. Quá trình đơn giản:
- Nếu cây rỗng, tạo nút mới làm gốc
- Nếu giá trị mới < giá trị nút hiện tại, đi sang trái
- Nếu giá trị mới > giá trị nút hiện tại, đi sang phải
- Nếu giá trị mới == giá trị nút hiện tại, tùy chọn (chấp nhận hay từ chối trùng lặp)
- Tiếp tục đệ quy cho đến khi tìm được vị trí lá
BSTNode* BST::insertNode(BSTNode* node, int value) {
// Base case: Found the position for new node
if (node == nullptr) {
return new BSTNode(value);
}
// Recursive case: Navigate left or right
if (value < node->value) {
node->left = insertNode(node->left, value);
}
else if (value > node->value) {
node->right = insertNode(node->right, value);
}
// If value == node->value, duplicate detected
// Option: Ignore duplicates or increment a counter
return node;
}
void BST::insert(int value) {
root = insertNode(root, value);
}
Độ phức tạp Insert: O(log n) trung bình (cây cân bằng), O(n) trường hợp xấu (cây suy biến)
3.2 Phép Search (Tìm kiếm)
Tìm kiếm giá trị trong BST lợi dụng tính chất sắp xếp: giảm nửa không gian tìm kiếm mỗi lần so sánh.
BSTNode* BST::searchNode(BSTNode* node, int value) {
// Base case: Node not found or tree is empty
if (node == nullptr) {
return nullptr;
}
// Check current node
if (value == node->value) {
return node; // Found!
}
// Navigate based on BST property
if (value < node->value) {
return searchNode(node->left, value);
} else {
return searchNode(node->right, value);
}
}
bool BST::search(int value) {
return searchNode(root, value) != nullptr;
}
Độ phức tạp Search: O(log n) trung bình, O(n) trường hợp xấu
3.3 Phép Delete (Xóa) - Trường hợp khó nhất
Delete là phép toán phức tạp nhất vì cần xử lý 3 trường hợp:
- Nút lá (No children): Xóa trực tiếp, gán con trỏ cha thành nullptr
- Nút có 1 con: Thay thế nút bằng con duy nhất của nó
- Nút có 2 con: Tìm nút thay thế (Inorder Successor hoặc Inorder Predecessor)
Inorder Successor: Nút nhỏ nhất trong cây con phải (đi phải 1 lần, sau đó đi trái liên tiếp)
Inorder Predecessor: Nút lớn nhất trong cây con trái (đi trái 1 lần, sau đó đi phải liên tiếp)
BSTNode* BST::findMin(BSTNode* node) {
while (node->left != nullptr) {
node = node->left;
}
return node;
}
BSTNode* BST::deleteNode(BSTNode* node, int value) {
if (node == nullptr) return nullptr;
// Navigate to the node to delete
if (value < node->value) {
node->left = deleteNode(node->left, value);
}
else if (value > node->value) {
node->right = deleteNode(node->right, value);
}
else {
// Found the node to delete
// Case 1: No children (leaf node)
if (node->left == nullptr && node->right == nullptr) {
delete node;
return nullptr;
}
// Case 2: One child
if (node->left == nullptr) {
BSTNode* temp = node->right;
delete node;
return temp;
}
if (node->right == nullptr) {
BSTNode* temp = node->left;
delete node;
return temp;
}
// Case 3: Two children
// Use Inorder Successor (smallest in right subtree)
BSTNode* successor = findMin(node->right);
node->value = successor->value; // Copy value
node->right = deleteNode(node->right, successor->value);
}
return node;
}
Độ phức tạp Delete: O(log n) trung bình, O(n) trường hợp xấu
Tóm tắt độ phức tạp BST:
| Phép toán | Trường hợp tốt | Trường hợp xấu | Cây cân bằng (Avg) |
|---|---|---|---|
| Insert | O(log n) | O(n) | O(log n) |
| Search | O(log n) | O(n) | O(log n) |
| Delete | O(log n) | O(n) | O(log n) |
| In-Order Traversal | O(n) | O(n) | O(n) |
4. Cây AVL & Cấu trúc tự cân bằng (Self-Balancing Trees)
BST thường có khuyết điểm: nếu bạn chèn dữ liệu đã được sắp xếp (ví dụ: 1, 2, 3, 4, 5), cây sẽ suy biến thành một linked list tuyến tính, mất đi ưu thế độ phức tạp logarithm. Cây AVL (Adelson-Velsky-Landis) giải quyết bài toán này bằng cách tự động cân bằng cây sau mỗi phép insert/delete.
4.1 Khái niệm Balance Factor & Rotations
Trong cây AVL, Balance Factor (BF) của một nút = chiều cao cây con trái - chiều cao cây con phải.
- Nếu BF = -1, 0, hay 1 → Cây cân bằng
- Nếu BF < -1 hoặc BF > 1 → Cây mất cân bằng, cần quay (rotation)
Có 4 loại quay:
1. LEFT ROTATION (Quay trái - khi cây con phải nặng)
Trước: Sau:
x y
\ / \
y --> x z
\
z
2. RIGHT ROTATION (Quay phải - khi cây con trái nặng)
Trước: Sau:
x y
/ / \
y --> z x
/
z
3. LEFT-RIGHT ROTATION (Quay trái-phải)
- Trước tiên quay trái nút con trái
- Sau đó quay phải nút cha
- Dùng khi: BF > 1 và con trái có BF < 0
4. RIGHT-LEFT ROTATION (Quay phải-trái)
- Trước tiên quay phải nút con phải
- Sau đó quay trái nút cha
- Dùng khi: BF < -1 và con phải có BF > 0
int getHeight(AVLNode* node) {
return node == nullptr ? 0 : node->height;
}
int getBalance(AVLNode* node) {
return node == nullptr ? 0 : getHeight(node->left) - getHeight(node->right);
}
// Left Rotation
AVLNode* rotateLeft(AVLNode* x) {
AVLNode* y = x->right;
AVLNode* temp = y->left;
y->left = x;
x->right = temp;
x->height = 1 + std::max(getHeight(x->left), getHeight(x->right));
y->height = 1 + std::max(getHeight(y->left), getHeight(y->right));
return y;
}
// Right Rotation
AVLNode* rotateRight(AVLNode* x) {
AVLNode* y = x->left;
AVLNode* temp = y->right;
y->right = x;
x->left = temp;
x->height = 1 + std::max(getHeight(x->left), getHeight(x->right));
y->height = 1 + std::max(getHeight(y->left), getHeight(y->right));
return y;
}
// Insert with Balancing
AVLNode* insertAVL(AVLNode* node, int value) {
// Standard BST insertion
if (node == nullptr) {
return new AVLNode(value);
}
if (value < node->value) {
node->left = insertAVL(node->left, value);
} else if (value > node->value) {
node->right = insertAVL(node->right, value);
} else {
return node; // Duplicate
}
// Update height
node->height = 1 + std::max(getHeight(node->left), getHeight(node->right));
// Get balance factor and rebalance if needed
int balance = getBalance(node);
// Left Heavy
if (balance > 1) {
if (getBalance(node->left) < 0) {
node->left = rotateLeft(node->left); // Left-Right case
}
return rotateRight(node); // Left-Left case
}
// Right Heavy
if (balance < -1) {
if (getBalance(node->right) > 0) {
node->right = rotateRight(node->right); // Right-Left case
}
return rotateLeft(node); // Right-Right case
}
return node;
}
Lợi thế của AVL Trees:
- Luôn duy trì độ cao logarithmic O(log n)
- Tất cả phép toán (insert, search, delete) đều O(log n) đảm bảo
- Hữu ích trong hệ thống real-time cần response time dự đoán được
Nhược điểm:
- Cần thêm logic rotation sau mỗi insert/delete phức tạp hơn BST
- Chi phí rebalance có thể cao với dữ liệu thay đổi liên tục
- Red-Black trees có thể tốt hơn khi insert/delete thường xuyên
5. Kiến trúc Canvas Visualizer
Để trực quan hóa cây trên màn hình, chúng ta cần:
- Tính toán vị trí (x, y) của mỗi nút trên canvas
- Vẽ nút (hình tròn) và cạnh (đường nối)
- Xử lý sự kiện người dùng (click, drag, input)
- Cập nhật trực quan khi tree thay đổi
5.1 Thuật toán định vị nút: In-order Positioning
Cách hiệu quả nhất để định vị nút là sử dụng In-order Traversal. Chúng ta duyệt cây theo thứ tự Trái-Cha-Phải, gán tọa độ x theo số hiệu, y theo độ sâu:
class TreeVisualizer {
constructor(canvasId) {
this.canvas = document.getElementById(canvasId);
this.ctx = this.canvas.getContext('2d');
this.nodeRadius = 25;
this.nodePositions = new Map();
this.counter = 0;
}
// In-order traversal to assign x-coordinates
assignPositions(node, depth, minX, maxX) {
if (!node) return;
const midX = (minX + maxX) / 2;
const y = 60 + depth * 80; // Depth determines y-coordinate
// Recursively position left subtree
this.assignPositions(node.left, depth + 1, minX, midX);
// Position current node
this.nodePositions.set(node.id, {
x: midX,
y: y,
value: node.value
});
// Recursively position right subtree
this.assignPositions(node.right, depth + 1, midX, maxX);
}
// Draw tree on canvas
draw(root) {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.nodePositions.clear();
// Calculate positions
this.assignPositions(root, 0, 50, this.canvas.width - 50);
// Draw edges first (so nodes appear on top)
this.drawEdges(root);
// Then draw nodes
this.drawNodes(root);
}
drawEdges(node) {
if (!node) return;
const parentPos = this.nodePositions.get(node.id);
if (!parentPos) return;
if (node.left) {
const childPos = this.nodePositions.get(node.left.id);
if (childPos) {
this.ctx.strokeStyle = 'rgba(255,255,255,0.2)';
this.ctx.lineWidth = 2;
this.ctx.beginPath();
this.ctx.moveTo(parentPos.x, parentPos.y);
this.ctx.lineTo(childPos.x, childPos.y);
this.ctx.stroke();
}
this.drawEdges(node.left);
}
if (node.right) {
const childPos = this.nodePositions.get(node.right.id);
if (childPos) {
this.ctx.strokeStyle = 'rgba(255,255,255,0.2)';
this.ctx.lineWidth = 2;
this.ctx.beginPath();
this.ctx.moveTo(parentPos.x, parentPos.y);
this.ctx.lineTo(childPos.x, childPos.y);
this.ctx.stroke();
}
this.drawEdges(node.right);
}
}
drawNodes(node) {
if (!node) return;
const pos = this.nodePositions.get(node.id);
if (!pos) return;
// Draw node circle
this.ctx.fillStyle = 'rgba(59, 130, 246, 0.8)';
this.ctx.beginPath();
this.ctx.arc(pos.x, pos.y, this.nodeRadius, 0, 2 * Math.PI);
this.ctx.fill();
// Draw value text
this.ctx.fillStyle = '#fff';
this.ctx.font = 'bold 14px Arial';
this.ctx.textAlign = 'center';
this.ctx.textBaseline = 'middle';
this.ctx.fillText(pos.value, pos.x, pos.y);
// Recursively draw children
if (node.left) this.drawNodes(node.left);
if (node.right) this.drawNodes(node.right);
}
}
5.2 Xử lý tương tác người dùng
Tương tác bao gồm nhập giá trị, thêm node, xóa node, tìm kiếm và highlight:
// Setup event listeners
setupEventListeners() {
const inputField = document.getElementById('valueInput');
const addBtn = document.getElementById('addBtn');
const deleteBtn = document.getElementById('deleteBtn');
const searchBtn = document.getElementById('searchBtn');
addBtn.addEventListener('click', () => {
const value = parseInt(inputField.value);
if (!isNaN(value) && value >= 1 && value <= 999) {
this.tree.insert(value);
this.draw(this.tree.root);
inputField.value = '';
inputField.focus();
}
});
deleteBtn.addEventListener('click', () => {
const value = parseInt(inputField.value);
if (!isNaN(value)) {
this.tree.deleteValue(value);
this.draw(this.tree.root);
inputField.value = '';
}
});
searchBtn.addEventListener('click', () => {
const value = parseInt(inputField.value);
if (!isNaN(value)) {
const found = this.tree.search(value);
this.highlightNode(value, found);
}
});
// Enter key support
inputField.addEventListener('keypress', (e) => {
if (e.key === 'Enter') addBtn.click();
});
}
// Highlight found node
highlightNode(value, found) {
if (found) {
// Find and highlight the node in our visualization
for (let [nodeId, pos] of this.nodePositions) {
if (pos.value === value) {
// Draw highlight effect
this.ctx.strokeStyle = '#10b981';
this.ctx.lineWidth = 3;
this.ctx.beginPath();
this.ctx.arc(pos.x, pos.y, this.nodeRadius + 5, 0, 2 * Math.PI);
this.ctx.stroke();
break;
}
}
}
}
6. Design Patterns trong Visualizer
Một dự án chuyên nghiệp cần sử dụng các design patterns để tổ chức code sạch và dễ bảo trì:
6.1 Observer Pattern: Cập nhật UI khi Tree thay đổi
Khi tree thay đổi (insert/delete), chúng ta cần thông báo cho tất cả observers (UI components) cập nhật:
class ObservableBST {
constructor() {
this.root = null;
this.observers = [];
}
// Register observer
subscribe(observer) {
this.observers.push(observer);
}
// Notify all observers
notifyObservers() {
this.observers.forEach(observer => observer.update(this.root));
}
insert(value) {
this.root = this._insertNode(this.root, value);
this.notifyObservers(); // Notify UI to redraw
}
deleteValue(value) {
this.root = this._deleteNode(this.root, value);
this.notifyObservers();
}
// ... rest of tree methods
}
// Usage
const tree = new ObservableBST();
const visualizer = new TreeVisualizer('canvas');
tree.subscribe({
update: (root) => {
visualizer.draw(root); // Auto-update when tree changes
}
});
6.2 Strategy Pattern: Khác nhau loại Tree (BST vs AVL vs Red-Black)
Sử dụng Strategy pattern cho phép switch giữa các loại cây mà không thay đổi code của visualizer:
// Abstract strategy
class TreeStrategy {
insert(value) { throw new Error('Not implemented'); }
delete(value) { throw new Error('Not implemented'); }
search(value) { throw new Error('Not implemented'); }
getRoot() { throw new Error('Not implemented'); }
}
// Concrete strategies
class BSTStrategy extends TreeStrategy {
constructor() {
super();
this.tree = new BST();
}
insert(value) { this.tree.insert(value); }
delete(value) { this.tree.deleteValue(value); }
search(value) { return this.tree.search(value); }
getRoot() { return this.tree.root; }
}
class AVLStrategy extends TreeStrategy {
constructor() {
super();
this.tree = new AVLTree();
}
insert(value) { this.tree.insert(value); }
delete(value) { this.tree.deleteValue(value); }
search(value) { return this.tree.search(value); }
getRoot() { return this.tree.root; }
}
// Usage - can switch strategies easily
class Application {
setTreeStrategy(strategy) {
this.treeStrategy = strategy;
}
addNode(value) {
this.treeStrategy.insert(value);
this.redraw();
}
}
6.3 MVC Pattern: Model-View-Controller Separation
Model: BST/AVL tree logic | View: Canvas visualization | Controller: Event handling & coordination
- Model: Lớp Tree chứa dữ liệu và logic, không biết gì về cách hiển thị
- View: Lớp Visualizer chỉ vẽ, không chứa logic cây
- Controller: Lớp Application điều phối giữa Model và View
7. Các cấu trúc dữ liệu nâng cao khác & Ứng dụng
7.1 Red-Black Trees
Như AVL nhưng ít rebalance hơn. Mỗi nút được tô màu đỏ hoặc đen với các ràng buộc:
- Nút gốc luôn đen
- Nút lá (NIL) là đen
- Nút đỏ phải có con đen
- Mọi đường từ nút đến lá có số lượng nút đen bằng nhau
Ưu điểm: Ít rotation hơn AVL, tốt cho insert/delete thường xuyên. Được dùng trong std::map, std::set của C++.
7.2 B-Trees
Được dùng trong database và file systems. Mỗi nút có thể chứa nhiều giá trị và con, tối ưu cho truy cập disk.
7.3 Trie Trees
Tối ưu cho autocomplete, spell checker, IP routing. Mỗi đường từ gốc đến lá tạo thành một từ.
7.4 Ứng dụng thực tế:
- Cơ sở dữ liệu (Database): B-trees, B+ trees cho indexing
- Hệ điều hành (OS): AVL/Red-Black trees cho memory management
- Trình duyệt web: BST cho DOM tree, event handling
- Game development: BSP (Binary Space Partition) trees cho collision detection
- Machine Learning: Decision trees cho classification
- Compiler design: Trie trees cho symbol table
8. Bộ mô phỏng tương tác BST Visualizer
Dưới đây là ứng dụng mô phỏng trực quan cây tìm kiếm nhị phân được dựng bằng HTML5 Canvas và JavaScript. Bạn hãy nhập các con số (từ 1 đến 999) và nhấn "Thêm Node" để nhìn thấy liên kết phân nhánh tự động dịch chuyển và vẽ đồ họa trực quan:
9. Tóm tắt Bài học & Khuyến nghị Tiếp tục Học
Qua bài tập tổng kết này, chúng ta đã áp dụng:
- Lập trình hướng đối tượng OOP (Classes, Encapsulation)
- Quản lý con trỏ và bộ nhớ Heap (new/delete)
- Thuật toán đệ quy phức tạp (Insert, Delete, Search, Traversals)
- Cấu trúc dữ liệu nâng cao (Self-balancing trees)
- Lập trình ứng dụng web (HTML5 Canvas, JavaScript events)
- Design patterns (Observer, Strategy, MVC)
- Phân tích độ phức tạp thuật toán (Big O notation)
Hướng phát triển tiếp theo:
- Nâng cấp: Thêm Red-Black tree, Splay tree implementations
- Performance: Viết benchmark so sánh BST vs AVL vs Red-Black trên dataset khác nhau
- Ứng dụng: Xây dựng database index system sử dụng B-trees
- Web: Tạo interactive visualizer cho Trie, Segment tree, Fenwick tree
- Competitive Programming: Sử dụng BST variants để giải các bài problem khó
Câu hỏi ôn tập kiến thức
Khi thực hiện delete nút có 2 con trong BST, ta sử dụng Inorder Successor. Tại sao lại chọn nút nhỏ nhất trong cây con phải, chứ không phải nút lớn nhất trong cây con trái?
Tải mã nguồn mẫu bài học
Bạn có thể tải file mã nguồn HTML mô phỏng Visualizer độc lập của bài học này để tự chạy trên máy tính hoặc chỉnh sửa tùy ý.
Tải bst_visualizer.html
Comments
Bình luận