从二叉搜索树到哈希表:四种常用数据结构的原理与实现

在数据结构的世界中,高效的数据存储与查找是核心诉求。二叉搜索树、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 核心规则

红黑树通过颜色约束维持近似平衡,每个节点要么红色要么黑色,满足:

  1. 根节点为黑色
  2. 红色节点的子节点必须为黑色(无连续红节点)
  3. 从任意节点到所有空节点的路径,黑色节点数量相同
  4. 空节点(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) 无序存储,追求极致查找效率
相关推荐
没有bug.的程序员2 小时前
Java 并发容器深度剖析:ConcurrentHashMap 源码解析与性能优化
java·开发语言·性能优化·并发·源码解析·并发容器
kk哥88993 小时前
分享一些学习JavaSE的经验和技巧
java·开发语言
栈与堆3 小时前
LeetCode 21 - 合并两个有序链表
java·数据结构·python·算法·leetcode·链表·rust
viqjeee3 小时前
ALSA驱动开发流程
数据结构·驱动开发·b树
lagrahhn3 小时前
Java的RoundingMode舍入模式
java·开发语言·金融
鸽鸽程序猿3 小时前
【JavaEE】【SpringCloud】注册中心_nacos
java·spring cloud·java-ee
云上凯歌3 小时前
01 GB28181协议基础理解
java·开发语言
Coder_Boy_4 小时前
基于SpringAI的在线考试系统-考试系统DDD(领域驱动设计)实现步骤详解
java·数据库·人工智能·spring boot
毕设源码-钟学长4 小时前
【开题答辩全过程】以 基于Java的运动器材销售网站为例,包含答辩的问题和答案
java·开发语言