深入理解二叉搜索树:从原理到实现

目录

[一、 二叉搜索树的概念与特性](#一、 二叉搜索树的概念与特性)

[二、 二叉搜索树的性能分析](#二、 二叉搜索树的性能分析)

三、二叉搜索树的核心操作

[3.1 插入操作](#3.1 插入操作)

[3.2 查找操作](#3.2 查找操作)

[3.3 删除操作](#3.3 删除操作)

四、二叉搜索树的完整实现

[4.1 基础版本(Key模型)](#4.1 基础版本(Key模型))

[4.2 扩展版本(Key-Value模型)](#4.2 扩展版本(Key-Value模型))

五、二叉搜索树的应用场景

[5.1 key搜索场景](#5.1 key搜索场景)

[5.2 Key-Value模型应用场景](#5.2 Key-Value模型应用场景)

六、总结


本文将详细解析二叉搜索树的核心概念、操作实现及应用场景,帮助读者全面掌握这一重要的数据结构。

一、 二叉搜索树的概念与特性

二叉搜索树(Binary Search Tree,BST),又称二叉排序树,是一种特殊的二叉树数据结构,它具有以下性质:

  • 若左子树非空,则左子树上所有节点的值都小于等于根节点的值

  • 若右子树非空,则右子树上所有节点的值都大于等于根节点的值

  • 左右子树也分别为二叉搜索树

重要说明:关于相等值的处理,二叉搜索树可以设计为支持或不支持重复值,这取决于具体应用场景。在C++ STL中:

  • map/set:不支持重复键值

  • multimap/multiset:支持重复键值

二、 二叉搜索树的性能分析

最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其高度为:log₂N

最差情况下,二叉搜索树退化为单支树(或者类似单支树),其高度为:N

所以综合而言,二叉搜索树增删查改时间复杂度为:O(N)

那么这样的效率显然是无法满足我们需求的,我们后续课程需要继续讲解二叉搜索树的变形,平衡二叉树。二叉搜索树、AVL树和红黑树,才能适用于我们在内存中存储和搜索数据。

另外需要说明的是,二分查找也可以实现O(log₂N)级别的查找效率,但是二分查找有两大缺陷

  1. 需要存储在支持下标随机访问的结构中,并且有序。

  2. 插入和删除数据效率很低,因为存储在支持下标随机访问的结构中,插入和删除数据一般需要挪动数据。

情况 树形态 树高度 操作时间复杂度
最优 完全二叉树 O(log₂N) O(log₂N)
最差 单支树 O(N) O(N)

三、二叉搜索树的核心操作

3.1 插入操作

如下:

  1. 树为空,创建新节点作为根节点

  2. 树非空,从根节点开始比较:

  • 插入值 > 当前节点值:向右子树移动

  • 插入值 < 当前节点值:向左子树移动

  • 插入值 = 当前节点值:根据设计决定(支持重复值则继续移动,否则返回失败)

  1. 找到空位置后插入新节点

代码实现:

cpp 复制代码
bool 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;
}

3.2 查找操作

步骤:

  1. 从根节点开始比较

  2. 查找值 > 当前节点值:向右子树查找

  3. 查找值 < 当前节点值:向左子树查找

  4. 查找值 = 当前节点值:找到目标

  5. 遇到空节点:查找失败

代码实现

cpp 复制代码
bool 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.3 删除操作

首先查找元素是否在二叉搜索树中,如果不存在,则返回false。

如果查找元素存在则分以下四种情况分别处理:(假设要删除的结点为N)

情况1:删除叶子节点

直接删除,父节点对应指针置空

情况2:删除只有右子树的节点

用右子树替代被删节点

情况3:删除只有左子树的节点

用左子树替代被删节点

情况4:删除有两个子树的节点(最复杂)

使用替换法删除

  • 找到左子树的最大节点或右子树的最小节点

  • 用找到的节点值替换待删除节点的值

  • 删除被替换的节点(该节点必定是情况1、2或3)

代码实现:

cpp 复制代码
bool 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 {
            // 找到要删除的节点
            if (cur->_left == nullptr) {
                // 情况1、2:无左子树
                if (parent == nullptr) {
                    _root = cur->_right;
                } else {
                    if (parent->_left == cur)
                        parent->_left = cur->_right;
                    else
                        parent->_right = cur->_right;
                }
                delete cur;
            } else if (cur->_right == nullptr) {
                // 情况3:无右子树
                if (parent == nullptr) {
                    _root = cur->_left;
                } else {
                    if (parent->_left == cur)
                        parent->_left = cur->_left;
                    else
                        parent->_right = cur->_left;
                }
                delete cur;
            } else {
                // 情况4:有两个子树
                // 找右子树的最小节点
                Node* rightMinParent = cur;
                Node* rightMin = cur->_right;
                while (rightMin->_left) {
                    rightMinParent = rightMin;
                    rightMin = rightMin->_left;
                }
                
                // 替换值
                cur->_key = rightMin->_key;
                
                // 删除右子树的最小节点
                if (rightMinParent->_left == rightMin)
                    rightMinParent->_left = rightMin->_right;
                else
                    rightMinParent->_right = rightMin->_right;
                    
                delete rightMin;
            }
            return true;
        }
    }
    return false;
}

四、二叉搜索树的完整实现

4.1 基础版本(Key模型)

Key模型 是最基础的版本控制方式,主要用于管理简单的键值对数据。它的核心特点包括:

1. 数据结构

  • 采用最简单的键值对存储方式

  • 每个Key对应一个Value

  • Value可以是字符串、数字等基本数据类型

2. 版本控制机制

  • 每次修改都会生成新的版本记录

  • 通过时间戳或版本号标识不同版本

  • 支持基本的版本回退功能

3. 典型应用场景

  • 配置管理:如保存应用的参数配置

  • 用户偏好设置:存储用户的自定义设置

  • 简单的元数据管理:如产品属性、标签等

cpp 复制代码
template<class K>
struct BSTNode {
    K _key;
    BSTNode<K>* _left;
    BSTNode<K>* _right;

    BSTNode(const K& key)
        : _key(key), _left(nullptr), _right(nullptr) {}
};

template<class K>
class BSTree {
    typedef BSTNode<K> Node;
private:
    Node* _root = nullptr;
    
    // 递归中序遍历
    void _InOrder(Node* root) {
        if (root == nullptr) return;
        _InOrder(root->_left);
        std::cout << root->_key << " ";
        _InOrder(root->_right);
    }
    
public:
    // 插入、查找、删除等方法如前文所示
    void InOrder() {
        _InOrder(_root);
        std::cout << std::endl;
    }
};

4.2 扩展版本(Key-Value模型)

在标准版本的基础上,我们引入了Key-Value存储模型来增强数据结构的灵活性和查询效率。这个扩展版本特别适合需要快速查找和动态更新的应用场景。

主要特性:

  1. 灵活的键值对存储:每个数据项都由唯一的键(Key)和对应的值(Value)组成

    • 键通常采用字符串或数字类型

    • 值可以是任意数据结构(字符串、数字、数组、对象等)

    • 示例:{"user_id": 123, "name": "张三", "orders": [1001,1002]}

  2. 高效查询机制

    • 支持精确键查询(O(1)时间复杂度)

    • 可选的二级索引支持范围查询

cpp 复制代码
template<class K, class V>
struct BSTNode {
    K _key;
    V _value;
    BSTNode<K, V>* _left;
    BSTNode<K, V>* _right;

    BSTNode(const K& key, const V& value)
        : _key(key), _value(value), _left(nullptr), _right(nullptr) {}
};

template<class K, class V>
class BSTree {
    typedef BSTNode<K, V> Node;
private:
    Node* _root = nullptr;
    
public:
    // 插入方法
    bool Insert(const K& key, const V& value) {
        if (_root == nullptr) {
            _root = new Node(key, value);
            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, value);
        if (parent->_key < key) {
            parent->_right = cur;
        } else {
            parent->_left = cur;
        }
        return true;
    }
    
    // 查找方法,返回节点指针以便修改value
    Node* 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 cur;
            }
        }
        return nullptr;
    }
};

五、二叉搜索树的应用场景

5.1 key搜索场景

场景特点:只需要判断关键字是否存在,不关心关联值。

只有key作为关键码,结构中只需要存储key即可,关键码即为需要搜索到的值,搜索场景只需要判断key在不在。key的搜索场景实现的二叉树搜索树支持增删查,但是不支持修改,修改key破坏搜索树结构了。

场景1:小区无人值守车库,小区车库买了车位的业主车才能进小区,那么物业会把买了车位的业主的车牌号录入后台系统,车辆进入时扫描车牌在不在系统中,在则抬杆,不在则提示非本小区车辆,无法进入。

代码实现

cpp 复制代码
// 车牌检查系统
BSTree<std::string> carSystem;
carSystem.Insert("京A12345");
carSystem.Insert("沪B67890");

// 车辆入场检查
std::string licensePlate = "京A12345";
if (carSystem.Find(licensePlate)) {
    std::cout << "允许进入" << std::endl;
} else {
    std::cout << "未授权车辆" << std::endl;
}

5.2 Key-Value模型应用场景

场景特点:需要通过关键字查找对应的关联值。

每一个关键码key,都有与之对应的值value,value可以是任意类型对象。树的结构中(结点)除了需要存储key,还要存储对应的value,增/删/查还是以key为关键字走二叉搜索树的规则进行比较,可以快速查找到key对应的value。key/value的搜索场景实现的二叉搜索树支持修改,但是不支持修改key,修改key会破坏搜索树性质,可以修改value。

场景1:商场无人值守车库,入口进场时扫描车牌,记录车牌和入场时间,出口离场时,扫描车牌,查找入场时间,用当前时间-入场时间计算出停车时长,计算出停车费用,缴费后抬杆,车辆离场。

代码示例

cpp 复制代码
// 单词统计系统
BSTree<std::string, int> wordCount;
std::vector<std::string> words = {"apple", "banana", "apple", "orange", "banana", "apple"};

for (const auto& word : words) {
    auto node = wordCount.Find(word);
    if (node == nullptr) {
        wordCount.Insert(word, 1);
    } else {
        node->_value++;
    }
}

// 输出统计结果
// 中序遍历会按字典序输出

六、总结

二叉搜索树作为一种基础而重要的数据结构,具有以下特点:

优点

  • 动态性能好,支持高效插入删除

  • 中序遍历可得有序序列

  • 实现相对简单

缺点

  • 普通BST可能退化为链表

  • 需要平衡机制保证性能

适用场景

  • 需要动态维护有序数据集

  • 查找、插入、删除操作都较频繁

  • 数据量不是特别大,或可以使用平衡BST

掌握二叉搜索树不仅是学习更高级数据结构的基础,也是理解许多系统底层实现的关键。在实际应用中,我们通常使用其平衡变种(如红黑树)来保证稳定性能。

相关推荐
芝麻开门-新起点7 小时前
Flutter 框架设计与高效执行原理解析
1024程序员节
艾莉丝努力练剑7 小时前
【Linux权限 (二)】Linux权限机制深度解析:umask如何决定默认权限与粘滞位的妙用
大数据·linux·服务器·c++·ubuntu·centos·1024程序员节
知花实央l10 小时前
【数字逻辑】24小时数字钟实战!74HC161搭24/60进制计数器+Multisim仿真
算法·测试用例·1024程序员节
AORO202511 小时前
变身智能车载台,AORO P9000U三防平板赋能工业数字化升级
5g·智能手机·电脑·制造·信息与通信·1024程序员节
2501_9387742914 小时前
CCF B 类会议指南:DASFAA 2026 数据挖掘中的公平性与可解释性
1024程序员节
博风18 小时前
SQL进阶:not exists谓词
1024程序员节
!执行19 小时前
Web3 前端与合约交互
前端·web3·1024程序员节
星光一影20 小时前
供应链进销存源码uniapp全开源ERP多仓库管理系统pc+app手机端
mysql·elementui·uni-app·开源·php·phpstorm·1024程序员节
周杰伦_Jay20 小时前
【实战|旅游知识问答RAG系统全链路解析】从配置到落地(附真实日志数据)
大数据·人工智能·分布式·机器学习·架构·旅游·1024程序员节