在数据结构的世界中,高效的数据存储与查找是核心诉求。二叉搜索树、AVL 树、红黑树和哈希表作为四种经典的数据结构,分别从不同角度解决了数据操作的效率问题。本文将结合具体代码,深入解析这四种数据结构的核心原理、实现细节及适用场景,帮助读者构建完整的知识体系。
一、二叉搜索树(BST):基础搜索树的设计与实现
1.1 核心概念
二叉搜索树(又称二叉排序树)是一种特殊的二叉树,满足以下性质:
- 左子树所有节点值 ≤ 根节点值
- 右子树所有节点值 ≥ 根节点值
- 左右子树均为二叉搜索树
- 支持相等值插入(如 multimap 场景)或禁止(如 set 场景),需保持逻辑一致性
1.2 关键操作实现
(1)插入操作
插入逻辑遵循 "左小右大" 原则,找到空位置插入新节点:
template<class K>
bool BSTree<K>::Insert(const K& key) {
if (_root == nullptr) {
_root = new Node(key);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur) {
if (cur->_key < key) {
parent = cur;
cur = cur->_right;
} else if (cur->_key > key) {
parent = cur;
cur = cur->_left;
} else {
return false; // 不支持重复插入
}
}
cur = new Node(key);
if (parent->_key < key) {
parent->_right = cur;
} else {
parent->_left = cur;
}
return true;
}
(2)查找操作
从根节点开始,根据值的大小向左或向右遍历,最多遍历树的高度次:
template<class K>
bool BSTree<K>::Find(const K& key) {
Node* cur = _root;
while (cur) {
if (cur->_key < key) {
cur = cur->_right;
} else if (cur->_key > key) {
cur = cur->_left;
} else {
return true;
}
}
return false;
}
(3)删除操作
删除操作是二叉搜索树的难点,需处理四种情况:
template<class K>
bool BSTree<K>::Erase(const K& key) {
Node* parent = nullptr;
Node* cur = _root;
while (cur) {
if (cur->_key < key) {
parent = cur;
cur = cur->_right;
} else if (cur->_key > key) {
parent = cur;
cur = cur->_left;
} else {
// 情况1-3:0个或1个孩子
if (cur->_left == nullptr) {
if (parent == nullptr) _root = cur->_right;
else parent->_left == cur ? parent->_left = cur->_right : parent->_right = cur->_right;
delete cur;
return true;
} else if (cur->_right == nullptr) {
if (parent == nullptr) _root = cur->_left;
else parent->_left == cur ? parent->_left = cur->_left : parent->_right = cur->_left;
delete cur;
return true;
} else {
// 情况4:两个孩子,找右子树最小节点替换
Node* rightMinP = cur;
Node* rightMin = cur->_right;
while (rightMin->_left) {
rightMinP = rightMin;
rightMin = rightMin->_left;
}
cur->_key = rightMin->_key;
if (rightMinP->_left == rightMin) rightMinP->_left = rightMin->_right;
else rightMinP->_right = rightMin->_right;
delete rightMin;
return true;
}
}
}
return false;
}
1.3 性能分析
- 最优情况(完全二叉树):高度为 log2N,增删查改时间复杂度 O(logN)
- 最差情况(退化为单支树):高度为 N,时间复杂度 O(N)
- 缺陷:无法保证平衡,极端情况下效率低下
二、AVL 树:自平衡二叉搜索树的实现
2.1 核心概念
AVL 树是最早的自平衡二叉搜索树,通过控制左右子树高度差(平衡因子)来维持平衡:
- 平衡因子 = 右子树高度 - 左子树高度,取值只能是 -1、0、1
- 左右子树均为 AVL 树
- 高度严格控制在 logN,确保所有操作时间复杂度 O(logN)
2.2 关键技术:旋转操作
当插入或删除导致平衡因子为 ±2 时,需通过旋转恢复平衡,共四种旋转方式:
(1)右单旋(左子树过高)
void AVLTree<K, V>::RotateR(Node* parent) {
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR) subLR->_parent = parent;
Node* parentParent = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
if (parentParent == nullptr) {
_root = subL;
subL->_parent = nullptr;
} else {
if (parent == parentParent->_left) parentParent->_left = subL;
else parentParent->_right = subL;
subL->_parent = parentParent;
}
parent->_bf = subL->_bf = 0;
}
(2)左单旋(右子树过高)
与右单旋对称,核心是将右子树的左孩子作为中间节点,调整父子关系并重置平衡因子。
(3)左右双旋(左子树的右子树过高)
void AVLTree<K, V>::RotateLR(Node* parent) {
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(parent->_left);
RotateR(parent);
if (bf == -1) {
subL->_bf = 0;
subLR->_bf = 0;
parent->_bf = 1;
} else if (bf == 1) {
subL->_bf = -1;
subLR->_bf = 0;
parent->_bf = 0;
} else {
subL->_bf = subLR->_bf = parent->_bf = 0;
}
}
(4)右左双旋(右子树的左子树过高)
与左右双旋对称,先对右子树进行右单旋,再对整体进行左单旋。
2.3 插入操作与平衡维护
bool AVLTree<K, V>::Insert(const pair<K, V>& kv) {
// 1. 按BST规则插入
if (_root == nullptr) {
_root = new Node(kv);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur) {
if (cur->_kv.first < kv.first) {
parent = cur;
cur = cur->_right;
} else if (cur->_kv.first > kv.first) {
parent = cur;
cur = cur->_left;
} else {
return false;
}
}
cur = new Node(kv);
cur->_parent = parent;
parent->_kv.first < kv.first ? parent->_right = cur : parent->_left = cur;
// 2. 更新平衡因子并维护平衡
while (parent) {
if (cur == parent->_left) parent->_bf--;
else parent->_bf++;
if (parent->_bf == 0) {
break; // 子树高度不变,无需继续更新
} else if (parent->_bf == 1 || parent->_bf == -1) {
cur = parent;
parent = parent->_parent;
} else {
// 平衡被破坏,进行旋转
if (parent->_bf == 2) {
if (cur->_bf == 1) RotateL(parent);
else RotateRL(parent);
} else {
if (cur->_bf == -1) RotateR(parent);
else RotateLR(parent);
}
break;
}
}
return true;
}
2.4 性能特点
- 严格平衡,所有操作时间复杂度稳定 O(logN)
- 缺点:旋转操作频繁,插入删除效率低于红黑树
三、红黑树:近似平衡的高效实现
3.1 核心规则
红黑树通过颜色约束维持近似平衡,每个节点要么红色要么黑色,满足:
- 根节点为黑色
- 红色节点的子节点必须为黑色(无连续红节点)
- 从任意节点到所有空节点的路径,黑色节点数量相同
- 空节点(NIL)为黑色
这些规则确保最长路径不超过最短路径的 2 倍,实现近似平衡。
3.2 插入操作与平衡调整
插入新节点默认为红色(避免破坏规则 4),若父节点为红色则触发调整,分三种情况:
bool RBTree<K, V>::Insert(const pair<K, V>& kv) {
// 1. 按BST规则插入
if (_root == nullptr) {
_root = new Node(kv);
_root->_col = BLACK;
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur) {
if (cur->_kv.first < kv.first) {
parent = cur;
cur = cur->_right;
} else if (cur->_kv.first > kv.first) {
parent = cur;
cur = cur->_left;
} else {
return false;
}
}
cur = new Node(kv);
cur->_col = RED;
cur->_parent = parent;
parent->_kv.first < kv.first ? parent->_right = cur : parent->_left = cur;
// 2. 调整平衡
while (parent && parent->_col == RED) {
Node* grandfather = parent->_parent;
if (parent == grandfather->_left) {
Node* uncle = grandfather->_right;
if (uncle && uncle->_col == RED) {
// 情况1:叔叔为红,变色
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
} else {
// 情况2-3:叔叔为黑或不存在,旋转+变色
if (cur == parent->_right) {
RotateL(parent);
swap(parent, cur);
}
RotateR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
break;
}
} else {
// 对称情况处理
Node* uncle = grandfather->_left;
if (uncle && uncle->_col == RED) {
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
} else {
if (cur == parent->_left) {
RotateR(parent);
swap(parent, cur);
}
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
break;
}
}
}
_root->_col = BLACK;
return true;
}
3.3 性能特点
- 时间复杂度:增删查改 O(logN)
- 旋转次数少:插入最多 2 次旋转,删除最多 3 次旋转
- 适用场景:STL 中的 map、set 底层实现,平衡要求适中的场景
四、哈希表:O (1) 效率的终极方案
4.1 核心概念
哈希表通过哈希函数将关键字映射到存储位置,实现近似 O (1) 的操作效率:
- 哈希函数:将 key 转换为存储下标(如除留余数法 h(key)=key%M)
- 哈希冲突:不同 key 映射到同一位置的现象
- 负载因子:loadFactor=N/M,控制冲突概率
4.2 冲突解决方案
(1)链地址法(哈希桶)
将冲突的元素链接成链表,挂在对应哈希桶下:
namespace hash_bucket {
template<class K, class V>
struct HashNode {
pair<K, V> _kv;
HashNode* _next;
HashNode(const pair<K, V>& kv) : _kv(kv), _next(nullptr) {}
};
template<class K, class V, class Hash = HashFunc<K>>
class HashTable {
private:
vector<HashNode<K, V>*> _tables;
size_t _n = 0; // 元素个数
public:
bool Insert(const pair<K, V>& kv) {
// 扩容(负载因子=1时)
if (_n == _tables.size()) {
size_t newSize = __stl_next_prime(_tables.size() + 1);
vector<HashNode<K, V>*> newTables(newSize, nullptr);
for (size_t i = 0; i < _tables.size(); i++) {
HashNode<K, V>* cur = _tables[i];
while (cur) {
HashNode<K, V>* next = cur->_next;
// 重新映射到新桶
size_t hashi = Hash()(cur->_kv.first) % newSize;
cur->_next = newTables[hashi];
newTables[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newTables);
}
// 插入新节点(头插)
size_t hashi = Hash()(kv.first) % _tables.size();
HashNode<K, V>* newNode = new HashNode<K, V>(kv);
newNode->_next = _tables[hashi];
_tables[hashi] = newNode;
_n++;
return true;
}
HashNode<K, V>* Find(const K& key) {
size_t hashi = Hash()(key) % _tables.size();
HashNode<K, V>* cur = _tables[hashi];
while (cur) {
if (cur->_kv.first == key) return cur;
cur = cur->_next;
}
return nullptr;
}
};
}
(2)开放定址法
当发生冲突时,按规则探测下一个空闲位置(线性探测、二次探测等),适用于空间紧张的场景。
4.3 关键优化
-
哈希函数优化:string 等非整形 key 需转换为整形,采用 BKDR 哈希算法
template<>
struct HashFunc<string> {
size_t operator()(const string& key) {
size_t hash = 0;
for (char c : key) {
hash = hash * 131 + c; // 131为质数,减少冲突
}
return hash;
}
}; -
扩容策略:使用质数表确保哈希表大小为质数,减少冲突
4.4 性能特点
- 平均时间复杂度:增删查改 O(1)
- 缺点:无序存储,哈希函数设计不当会导致冲突加剧
- 适用场景:unordered_map、unordered_set 底层实现,追求极致查找效率的场景
五、四种数据结构对比与选型建议
| 数据结构 | 时间复杂度 | 有序性 | 平衡特性 | 适用场景 |
|---|---|---|---|---|
| 二叉搜索树 | 平均 O (logN),最差 O (N) | 是 | 无 | 数据分布均匀,无需严格平衡 |
| AVL 树 | 稳定 O (logN) | 是 | 严格平衡 | 对平衡要求高,查询频繁的场景 |
| 红黑树 | 稳定 O (logN) | 是 | 近似平衡 | 插入删除频繁,STL 容器底层 |
| 哈希表 | 平均 O (1),最差 O (N) | 否 | 无 | 无序存储,追求极致查找效率 |