【数据结构】巅峰对决:AVL树 vs 红黑树 vs 哈希表 —— 从底层原理到 C++ 手撕代码
·
摘要
在计算机科学中,查找(Search)是最高频的操作之一。为了极致的查找效率,前人发明了 AVL 树、红黑树和哈希表。但你是否真正理解它们的区别?为什么 Linux 内核选择红黑树?为什么 Redis 使用哈希表?本文将从底层原理出发,配合 C++ 核心代码解析,带你彻底搞懂这三大神器的优劣与应用场景。
目录
- 绝对平衡的王者:AVL 树
- 原理与旋转详解
- C++ 核心代码实现
- 中庸之道的智慧:红黑树 (RB-Tree)
- 五大性质与平衡策略
- 为什么它比 AVL 更流行?
- C++ 核心代码实现
- 速度的极限:哈希表 (Hash Table)
- 哈希函数与冲突解决
- C++ 核心代码实现
- 终极对比:谁才是最强查找结构?
- 总结与面试题
一、 绝对平衡的王者:AVL 树
1.1 原理分析
AVL 树是最早被发明的自平衡二叉查找树。它的特点是**“严格”**:
- 平衡因子 (Balance Factor):任意节点的左右子树高度差绝对值不超过 1 (
|BF| <= 1)。 - 优点:查找效率极高,严格维持在 O ( log N ) O(\log N) O(logN)。
- 缺点:为了维持这种严格的平衡,插入和删除操作时需要进行大量的旋转(左旋、右旋、双旋),写入性能较差。
1.2 核心代码:旋转 (C++)
AVL 的核心在于检测到不平衡时进行的四种旋转:LL、RR、LR、RL。
template<class K, class V>
struct AVLTreeNode {
pair<K, V> _kv;
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;
int _bf; // 平衡因子
AVLTreeNode(const pair<K, V>& kv) : _kv(kv), _left(nullptr), _right(nullptr), _parent(nullptr), _bf(0) {}
};
// 右单旋(针对左边太高:LL型)
void RotateR(Node* parent) {
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR) subLR->_parent = parent;
subL->_right = parent;
Node* ppNode = parent->_parent;
parent->_parent = subL;
if (parent == _root) {
_root = subL;
subL->_parent = nullptr;
} else {
if (ppNode->_left == parent) ppNode->_left = subL;
else ppNode->_right = subL;
subL->_parent = ppNode;
}
// 更新平衡因子
parent->_bf = subL->_bf = 0;
}
// 左右双旋(针对 LR 型)
void RotateLR(Node* parent) {
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf; // 记录旋转前的平衡因子
RotateL(parent->_left); // 先对左子树左旋
RotateR(parent); // 再对自己右旋
// 重新调整平衡因子
if (bf == 1) {
parent->_bf = 0;
subL->_bf = -1;
subLR->_bf = 0;
} else if (bf == -1) {
parent->_bf = 1;
subL->_bf = 0;
subLR->_bf = 0;
} else if (bf == 0) {
parent->_bf = 0;
subL->_bf = 0;
subLR->_bf = 0;
}
}
avl树的旋转
二、 中庸之道的智慧:红黑树 (RB-Tree)
2.1 原理分析
红黑树是一种弱平衡二叉树。它不追求高度差为 1,而是保证最长路径不超过最短路径的 2 倍。
它通过 5 条规则(性质)来约束:
- 每个节点是红色或黑色。
- 根节点是黑色。
- 每个叶子节点(NIL)是黑色。
- 如果一个节点是红色,则它的两个子节点必须是黑色(不能有连续红节点)。
- 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
2.2 为什么红黑树更流行?
虽然 AVL 树查找更快,但红黑树在插入和删除时的旋转次数远少于 AVL 树。
- AVL:删除一个节点可能需要 O ( log N ) O(\log N) O(logN) 次旋转。
- RB-Tree:插入最多旋转 2 次,删除最多旋转 3 次。
- 结论:红黑树是综合性能最好的二叉树,因此成为 C++ STL
map/set的底层实现。
2.3 核心代码:插入调整 (C++)
红黑树最难的是插入后的颜色调整。
enum Color { RED, BLACK };
template<class K, class V>
struct RBTreeNode {
pair<K, V> _kv;
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
Color _col;
RBTreeNode(const pair<K, V>& kv) : _kv(kv), _left(nullptr), _right(nullptr), _parent(nullptr), _col(RED) {}
};
// 插入后的调整逻辑
void InsertFixUp(Node* node) {
Node* parent = nullptr;
Node* gParent = nullptr;
// 如果父节点存在且为红色,违反了规则4(不能连续红),需要调整
while ((parent = node->_parent) && parent->_col == RED) {
gParent = parent->_parent;
// 父节点是祖父的左孩子
if (parent == gParent->_left) {
Node* uncle = gParent->_right;
// Case 1: 叔叔存在且为红 -> 变色
if (uncle && uncle->_col == RED) {
parent->_col = BLACK;
uncle->_col = BLACK;
gParent->_col = RED;
node = gParent; // 继续向上调整
continue;
}
// Case 2: 叔叔不存在或为黑 -> 旋转 + 变色
if (node == parent->_right) {
// 如果是 LR 型,先左旋变成 LL 型
RotateL(parent);
std::swap(parent, node);
}
// LL 型:右旋 + 变色
parent->_col = BLACK;
gParent->_col = RED;
RotateR(gParent);
}
else {
// 对称处理 Parent 是右孩子的情况...
// (代码逻辑类似,方向相反)
}
}
// 根节点永远保持黑色
_root->_col = BLACK;
}
三、 速度的极限:哈希表 (Hash Table)
3.1 原理分析
哈希表不使用树结构,而是通过哈希函数将 Key 映射到数组的索引。
- 时间复杂度:理想情况下插入、查找、删除均为 O ( 1 ) O(1) O(1)。
- 冲突解决:
- 开放定址法:线性探测。
- 链地址法(拉链法):数组每个位置挂一个链表(C++
unordered_map采用此法)。
3.2 扩容与负载因子
当数据量增多,哈希冲突概率增大,查找效率退化。需要扩容(Rehash)。通常当 负载因子 (Load Factor) > 0.7~1.0 时扩容。
3.3 核心代码:拉链法实现 (C++)
template<class K, class V>
struct HashNode {
pair<K, V> _kv;
HashNode<K, V>* _next;
HashNode(const pair<K, V>& kv) : _kv(kv), _next(nullptr) {}
};
template<class K, class V>
class HashTable {
vector<HashNode<K, V>*> _tables; // 指针数组
size_t _size = 0; // 有效元素个数
public:
bool Insert(const pair<K, V>& kv) {
// 1. 检查扩容
if (_tables.size() == 0 || _size * 10 / _tables.size() >= 10) {
size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
vector<HashNode<K, V>*> newTables(newSize, nullptr);
// ... (需要将旧表节点重新计算哈希值挂到新表,代码略)
_tables.swap(newTables);
}
// 2. 计算哈希位置
size_t index = HashFunc(kv.first) % _tables.size();
// 3. 头插法插入链表
HashNode<K, V>* cur = _tables[index];
while (cur) {
if (cur->_kv.first == kv.first) return false; // 重复元素
cur = cur->_next;
}
HashNode<K, V>* newNode = new HashNode<K, V>(kv);
newNode->_next = _tables[index];
_tables[index] = newNode;
_size++;
return true;
}
};
哈希表拉链式解决冲突
四、 终极对比:谁才是最强查找结构?
这是面试中最重要的部分,建议背诵以下表格:
| 特性 | AVL 树 | 红黑树 (RB-Tree) | 哈希表 (Hash Table) |
|---|---|---|---|
| 底层结构 | 严格平衡二叉树 | 弱平衡二叉树 | 数组 + 链表/红黑树 |
| 平均查找 | O ( log N ) O(\log N) O(logN) (最快) | O ( log N ) O(\log N) O(logN) | O ( 1 ) O(1) O(1) (极速) |
| 最坏查找 | O ( log N ) O(\log N) O(logN) | O ( log N ) O(\log N) O(logN) | O ( N ) O(N) O(N) (冲突严重时) |
| 插入/删除 | 慢 (旋转频繁) | 中 (旋转少) | 快 (扩容时慢) |
| 内存占用 | 中 (需存平衡因子) | 中 (需存颜色) | 高 (需预留空间减少冲突) |
| 是否有序 | 有序 | 有序 | 无序 |
| 典型应用 | Windows 进程地址空间管理 | STL map/set, Linux epoll, CFS调度 | STL unordered_map, Redis, Memcached |
场景选择建议:
- 如果你需要数据有序(例如需要按 Key 范围查找
lower_bound):- 如果在构建后很少修改,主要是查询 -> 选 AVL 树。
- 如果频繁插入、删除和查询 -> 选 红黑树(这是大多数场景的选择)。
- 如果你不需要数据有序,且追求极致的单点查询速度:
- 选 哈希表。
五、 总结
- AVL 树是完美的理想主义者,为了极致的平衡付出了高昂的维护代价。
- 哈希表是极速的实用主义者,用空间换时间,但牺牲了顺序性。
- 红黑树是中庸的政治家,在平衡与变动之间找到了最佳的折中点,因此统治了各大语言的标准库。
理解了这三个数据结构,你不仅能轻松应对面试,更能在实际开发中做出正确的技术选型。
如果你觉得这篇文章对你有帮助,请点赞、收藏支持!如有疑问,欢迎在评论区讨论。有需要视频对应资源的可以私信我哦。
更多推荐


所有评论(0)