Cây nhị phân
Cập nhật lần cuối
Cây nhị phân (binary tree) là một cấu trúc phân cấp trong đó mỗi nút có tối đa hai con, gọi là con trái và con phải. Nút trên cùng là gốc, các nút không có con là lá, và số cạnh từ gốc đến lá sâu nhất là chiều cao của cây. Khác với cây tìm kiếm nhị phân, cây nhị phân thông thường không có quy tắc thứ tự nào, nó chỉ là một hình dạng. Nhấn Phát ở trên để xem cây được lấp đầy theo từng tầng, rồi được duyệt trung thứ tự (trái, nút, phải).
Cây nhị phân là nền tảng của nhiều cấu trúc khác: cây tìm kiếm nhị phân, heap, cây biểu thức và nhiều hơn nữa. Các phép duyệt đi qua mọi nút theo một thứ tự xác định: trung thứ tự (in-order), tiền thứ tự (pre-order) và hậu thứ tự (post-order) là ba cách duyệt theo chiều sâu, mỗi cách hữu ích cho một loại việc khác nhau.
Thuật ngữ
| Thuật ngữ | Ý nghĩa |
|---|---|
| Gốc | Nút trên cùng, không có nút cha |
| Lá | Nút không có con |
| Chiều cao | Đường đi dài nhất từ gốc đến lá (tính theo cạnh) |
| Độ sâu | Khoảng cách từ một nút đến gốc |
| Cây hoàn chỉnh | Mọi tầng đều đầy, có thể trừ tầng cuối, được lấp từ trái sang phải |
Ba cách duyệt theo chiều sâu
| Cách duyệt | Thứ tự | Ứng dụng phổ biến |
|---|---|---|
| Trung thứ tự | Trái, nút, phải | Xuất các giá trị đã sắp xếp của BST |
| Tiền thứ tự | Nút, trái, phải | Sao chép / tuần tự hóa một cây |
| Hậu thứ tự | Trái, phải, nút | Xóa / tính giá trị một cây |
Ví dụ minh họa
Duyệt trung thứ tự cây được tạo từ [4, 2, 6, 1, 3, 5] (lấp đầy theo từng tầng):
| Bước | Tại nút | Thao tác |
|---|---|---|
| 1 | 4 | Đệ quy vào cây con trái của 4 trước khi thăm nút này |
| 2 | 2 | Đệ quy vào cây con trái của 2 trước khi thăm nút này |
| 3 | 1 | Nút lá, không có con trái: thăm 1, kết quả [1] |
| 4 | 2 | Xong bên trái: thăm 2, kết quả [1, 2], rồi đệ quy sang phải |
| 5 | 3 | Nút lá: thăm 3, kết quả [1, 2, 3] |
| 6 | 4 | Xong cây con trái: thăm 4, kết quả [1, 2, 3, 4], rồi đệ quy sang phải |
| 7 | 5 | Con trái của 6, là một lá: thăm 5, kết quả [1, 2, 3, 4, 5] |
| 8 | 6 | Xong bên trái, không có con phải: thăm 6, kết quả [1, 2, 3, 4, 5, 6] |
Khi nào nên dùng cây nhị phân
| Nên dùng khi | Tránh dùng khi |
|---|---|
| Bạn cần mô hình hóa dữ liệu có tính phân cấp tự nhiên (hệ thống tệp, cây biểu thức, DOM) | Dữ liệu phẳng và một danh sách hoặc mảng là đủ: cây chỉ tốn thêm chi phí |
| Bạn muốn các thao tác có thứ tự và có thể giữ cây cân bằng (BST hoặc cây tự cân bằng) | Bạn cần tra cứu theo khóa trung bình O(1): bảng băm nhanh hơn mọi loại cây |
| Bạn cần xử lý dữ liệu có cấu trúc theo trung thứ tự, tiền thứ tự hoặc hậu thứ tự | Các nút được chèn theo thứ tự đã sắp xếp vào một BST không cân bằng: cây suy biến thành danh sách O(n) |
| Truy vấn theo khoảng hoặc duyệt có thứ tự là quan trọng, điều mà bảng băm không làm được | Bộ nhớ eo hẹp: mỗi nút mang hai con trỏ đến nút con cùng với dữ liệu |
Code Binary Tree
Một cách cài đặt Binary Tree gọn gàng, chạy được ngay bằng Python, JavaScript, Java, C++, C. Chọn một ngôn ngữ, sao chép code hoặc mở sẵn trong Coddy Playground.
Code Binary Tree bằng Python
1from collections import deque2
3
4class Node:5 def __init__(self, value):6 self.value = value7 self.left = None8 self.right = None9
10
11def insert(root, value):12 # Level-order insert: fill the first empty child slot found13 if root is None:14 return Node(value)15 queue = deque([root])16 while queue:17 node = queue.popleft()18 if node.left is None:19 node.left = Node(value)20 return root21 queue.append(node.left)22 if node.right is None:23 node.right = Node(value)24 return root25 queue.append(node.right)26
27
28def inorder(node):29 if node is None:30 return []31 return inorder(node.left) + [node.value] + inorder(node.right)32
33
34root = None35for value in [1, 2, 3, 4, 5, 6, 7]:36 root = insert(root, value)37
38print("Root: ", root.value)39print("Inorder:", inorder(root))Code Binary Tree bằng JavaScript
1class Node {2 constructor(value) {3 this.value = value;4 this.left = null;5 this.right = null;6 }7}8
9class BinaryTree {10 constructor() {11 this.root = null;12 }13
14 // Insert at the first free spot, top to bottom (level order)15 insert(value) {16 const node = new Node(value);17 if (!this.root) {18 this.root = node;19 return;20 }21 const queue = [this.root];22 while (queue.length > 0) {23 const current = queue.shift();24 if (!current.left) {25 current.left = node;26 return;27 }28 if (!current.right) {29 current.right = node;30 return;31 }32 queue.push(current.left, current.right);33 }34 }35
36 inorder(node = this.root, out = []) {37 if (!node) return out;38 this.inorder(node.left, out);39 out.push(node.value);40 this.inorder(node.right, out);41 return out;42 }43}44
45const tree = new BinaryTree();46for (const value of [1, 2, 3, 4, 5, 6, 7]) tree.insert(value);47console.log("Inorder traversal:", tree.inorder().join(" "));Code Binary Tree bằng Java
1import java.util.ArrayDeque;2import java.util.Queue;3
4public class Main {5 static class Node {6 int value;7 Node left, right;8 Node(int value) { this.value = value; }9 }10
11 static Node root;12
13 // Insert at the first free slot in level order (keeps the tree complete)14 static void insert(int value) {15 Node node = new Node(value);16 if (root == null) { root = node; return; }17 Queue<Node> queue = new ArrayDeque<>();18 queue.add(root);19 while (!queue.isEmpty()) {20 Node cur = queue.poll();21 if (cur.left == null) { cur.left = node; return; }22 if (cur.right == null) { cur.right = node; return; }23 queue.add(cur.left);24 queue.add(cur.right);25 }26 }27
28 // Inorder: left subtree, node, right subtree29 static void inorder(Node node, StringBuilder sb) {30 if (node == null) return;31 inorder(node.left, sb);32 sb.append(node.value).append(" ");33 inorder(node.right, sb);34 }35
36 public static void main(String[] args) {37 for (int v = 1; v <= 7; v++) insert(v);38 StringBuilder sb = new StringBuilder();39 inorder(root, sb);40 System.out.println("Root: " + root.value);41 System.out.println("Inorder: " + sb.toString().trim());42 }43}Code Binary Tree bằng C++
1#include <iostream>2
3struct Node {4 int value;5 Node* left = nullptr;6 Node* right = nullptr;7 explicit Node(int v) : value(v) {}8};9
10Node* insert(Node* root, int value) {11 if (root == nullptr) return new Node(value);12 if (value < root->value) root->left = insert(root->left, value);13 else root->right = insert(root->right, value);14 return root;15}16
17// In-order traversal: left subtree, node, right subtree18void inorder(const Node* node) {19 if (node == nullptr) return;20 inorder(node->left);21 std::cout << node->value << " ";22 inorder(node->right);23}24
25int countNodes(const Node* node) {26 if (node == nullptr) return 0;27 return 1 + countNodes(node->left) + countNodes(node->right);28}29
30int main() {31 Node* root = nullptr;32 for (int value : {8, 3, 10, 1, 6, 14, 4}) {33 root = insert(root, value);34 }35 std::cout << "In-order: ";36 inorder(root);37 std::cout << "\n";38 std::cout << "Node count: " << countNodes(root) << "\n";39 return 0;40}Code Binary Tree bằng C
1#include <stdio.h>2#include <stdlib.h>3
4typedef struct Node {5 int value;6 struct Node* left;7 struct Node* right;8} Node;9
10Node* newNode(int value) {11 Node* n = malloc(sizeof(Node));12 n->value = value;13 n->left = n->right = NULL;14 return n;15}16
17Node* insert(Node* root, int value) {18 if (root == NULL) return newNode(value);19 if (value < root->value) root->left = insert(root->left, value);20 else root->right = insert(root->right, value);21 return root;22}23
24// In-order traversal: left subtree, node, right subtree25void inorder(const Node* node) {26 if (node == NULL) return;27 inorder(node->left);28 printf("%d ", node->value);29 inorder(node->right);30}31
32int countNodes(const Node* node) {33 if (node == NULL) return 0;34 return 1 + countNodes(node->left) + countNodes(node->right);35}36
37int main(void) {38 int values[] = {8, 3, 10, 1, 6, 14, 4};39 Node* root = NULL;40 for (int i = 0; i < 7; i++) root = insert(root, values[i]);41 printf("In-order: ");42 inorder(root);43 printf("\n");44 printf("Node count: %d\n", countNodes(root));45 return 0;46}Câu hỏi thường gặp về cây nhị phân
Cây nhị phân và cây tìm kiếm nhị phân khác nhau như thế nào?
Cây nhị phân chỉ giới hạn mỗi nút có tối đa hai con, không có thứ tự nào. Cây tìm kiếm nhị phân thêm quy tắc: mọi giá trị trong cây con trái nhỏ hơn và mọi giá trị trong cây con phải lớn hơn, và chính điều này giúp tìm kiếm nhanh.
Duyệt trung thứ tự là gì?
Duyệt trung thứ tự thăm cây con trái, rồi đến nút, rồi đến cây con phải, một cách đệ quy. Với cây tìm kiếm nhị phân, cách duyệt này cho ra các giá trị theo thứ tự tăng dần, vì vậy đây là cách duyệt thường được dùng để minh họa nhất.
Cây nhị phân hoàn chỉnh là gì?
Cây nhị phân hoàn chỉnh có mọi tầng đều đầy, có thể trừ tầng cuối, và tầng cuối được lấp từ trái sang phải. Hình dạng này cho phép lưu cây gọn gàng trong một mảng (không cần con trỏ đến nút con), và đó là cách binary heap được cài đặt.
Khi nào nên dùng cây nhị phân thay cho mảng hoặc bảng băm?
O(1) của bảng băm nhanh hơn cây, còn với dữ liệu phẳng thì mảng đơn giản hơn và thân thiện với bộ nhớ đệm hơn.Chiều cao và độ sâu của cây nhị phân khác nhau như thế nào?
0, và nút duy nhất đó cũng có độ sâu 0.Vì sao cây nhị phân không cân bằng lại chạy chậm như vậy?
O(h) với h là chiều cao. Khi các khóa được chèn theo thứ tự đã sắp xếp, cây trở thành một đường thẳng, nên h tăng lên n và mọi thao tác suy biến thành O(n), không hơn gì danh sách liên kết. Các cây tự cân bằng như cây AVL hay cây đỏ đen giữ chiều cao ở mức O(log n) để tránh điều này.