【C++】二叉搜索树

目录

[一 二叉搜索树的概念](#一 二叉搜索树的概念)

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

[三 二叉搜索树的插入](#三 二叉搜索树的插入)

[四 二叉搜索树的查找](#四 二叉搜索树的查找)

[五 二叉搜索树的删除(重点)](#五 二叉搜索树的删除(重点))

[六 二叉搜索树key何key/value使用场景](#六 二叉搜索树key何key/value使用场景)

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

[2 key/value搜索场景](#2 key/value搜索场景)

[3 key/value⼆叉搜索树代码实现](#3 key/value⼆叉搜索树代码实现)

[4 水果类示例](#4 水果类示例)


一 二叉搜索树的概念

二叉搜索树又称二叉排序树,它要么是一棵空树,要么是具有以下性质的二叉树:

  • 若其左子树不为空,则左子树上所有结点的值都小于等于根结点的值。
  • 若其右子树不为空,则右子树上所有结点的值都大于等于根结点的值。
  • 它的左、右子树也分别为二叉搜索树。

二叉搜索树是否支持插入相等的值,需根据具体使用场景定义。后续学习的 map、set、multimap、multiset 系列容器,其底层实现均基于二叉搜索树。其中,map 和 set 不支持插入相等值,而 multimap 和 multiset 支持插入相等值。

1 存储

2 高效搜索 查找次数:二叉树的高度

中序查找:有序的


二 二叉搜索树的性能分析

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

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

所以综合而言,二叉搜索树增删查改的时间复杂度为:O (N) 。这样的效率显然无法满足需求,后续课程需要继续讲解二叉搜索树的变形 ------ 平衡二叉搜索树,包括 AVL 树和红黑树,它们才能适用于在内存中存储和搜索数据

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

  1. 需要存储在支持下标随机访问的结构中,且数据必须有序。
  2. 插入和删除数据的效率很低,因为这类结构中插入或删除数据,通常需要挪动其他数据。

这一点也正体现出了平衡二叉搜索树的价值。

如果是最优的情况,那么搜索二叉树的时间复杂度为log(N),但是如果像上述右图一样,那么时间复杂度是O(N),但是时间复杂度一般考虑最差情况


三 二叉搜索树的插入

插入过程如下:

  1. 若树为空,直接新增结点,并将该结点赋值给 root 指针。

  2. 若树不为空,按照二叉搜索树的性质查找插入位置:插入值小于当前结点值则向左遍历,插入值大于当前结点值则向右遍历,直至找到空位置后插入新结点

  3. 若支持插入相等的值,需保持逻辑一致性:插入值与当前结点值相等时,需固定选择向左或向右遍历(避免有时向左、有时向右),找到空位置后插入新结点

先定义搜索二叉树的结构和初始化

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

	BSTreeNode(const K& key)
		:_left(nullptr)
		,_right(nullptr)
		,_key(key)
	{}
};
cpp 复制代码
// 不允许相等的值插入
template<class K>
class BSTree
{
	typedef BSTreeNode<K> Node;
public:
	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;
	}
步骤 逻辑描述 代码对应片段
1. 空树处理 若根节点为nullptr(树空),直接创建新节点作为根节点,插入成功 if (_root == nullptr) { _root = new Node(key); return true; }
2. 查找插入位置 cur指针遍历树,parent指针记录cur的父节点,通过键值比较确定遍历方向:- 若cur->_key < key:目标位置在右子树,parent更新为curcur移至右子节点;- 若cur->_key > key:目标位置在左子树,parent更新为curcur移至左子节点;- 若cur->_key == key:键值重复,返回false while (cur) { ... } 中的分支判断
3. 插入新节点 遍历结束后,curnullptrparent为新节点的父节点:- 若parent->_key < key:新节点作为parent的右子节点;- 若parent->_key > key:新节点作为parent的左子节点 cur = new Node(key); 后的分支判断
4. 返回结果 插入完成,返回true

四 二叉搜索树的查找

遍历步骤:

  1. 从根节点开始比较,查找目标值x:若x大于当前节点值,则向右遍历查找;若x小于当前节点值,则向左遍历查找。

  2. 最多查找树的高度次,若遍历至空节点仍未找到x,则说明该值不存在。

  3. 若不支持插入相等的值,一旦找到与x相等的节点,直接返回该节点即可。

  4. 若支持插入相等的值(即可能存在多个值为x的节点),通常要求查找中序遍历中的第一个x。例如:查找值为3时,需返回中序遍历中第一个出现的3(如示意图中1的右孩子对应的3)。

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;
     }

五 二叉搜索树的删除(重点)

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

如果查找元素存在则分以下四种情况分别处理:(假设要删除的结点为N)
1. 要删除结点N左右孩子均为空
2. 要删除的结点N左孩子位空,右孩子结点不为空
3. 要删除的结点N右孩子位空,左孩子结点不为空
4. 要删除的结点N左右孩子结点均不为空

对应以上四种情况的解决方案:

  1. 把N结点的父亲对应孩子指针指向空,直接删除N结点(情况1可以当成2或者3处理,效果是⼀样

的)

  1. 把N结点的父亲对应孩子指针指向N的右孩子,直接删除N结点

  2. 把N结点的父亲对应孩子指针指向N的左孩子,直接删除N结点

  3. 无法直接删除N结点,因为N的两个孩子无处安放,只能用替换法删除。找N左子树的值最大结点

R(最右结点)或者N右子树的值最小结点R(最左结点)替代N,因为这两个结点中任意⼀个,放到N的

位置,都满足二叉搜索树的规则。替代N的意思就是N和R的两个结点的值交换,转而变成删除R结

点,R结点符合情况2或情况3,可以直接删除。

情况一:这是最简单的删除。因为1既没有左孩子又没有右孩子,所以可以直接删除不用考虑孩子节点

情况二 三:这两种都是只有一边孩子的。把父亲节点指向有的那边孩子节点,然后删除节点

情况四:最复杂(面试可能会考)删除根节点或者删除的节点左右孩子都有。

1 左右孩子都有:需要用到替换法。找左子树的最右节点(左子树的最大值)或右子树的最左节点(右子树的最小值)来替换当前的节点

2删除根节点:如上图是一种特殊情况(1),当使用替换法删除的时候,发现根节点的右子树的左孩子根本不存在,如果不额外列出这种情况,就会报错。还有一种特殊情况(2):删除根节点时,它的某一子树不存在,就可以直接把存在的孩子节点设为根节点

特殊情况(1):

注意:我们删除这个值必须要先找到对应的节点

代码实现:

cpp 复制代码
// 从二叉搜索树中删除键值为key的节点,成功返回true,失败返回false
bool Erase(const K& key)
{
    Node* parent = nullptr;  // 记录当前节点的父节点
    Node* cur = _root;       // 从根节点开始查找目标节点

    // 查找目标节点key
    while (cur)
    {
        if (cur->_key < key)  // 当前节点值小于key,向右子树查找
        {
            parent = cur;
            cur = cur->_right;
        }
        else if (cur->_key > key)  // 当前节点值大于key,向左子树查找
        {
            parent = cur;
            cur = cur->_left;
        }
        else  // 找到目标节点cur,执行删除操作
        {
            // 情况1:目标节点左子树为空(包括左空右空和左空右非空)
            if (cur->_left == nullptr)
            {
                // 若目标节点是根节点,直接让根指向其右子树
                if (cur == _root)
                {
                    _root = cur->_right;
                }
                else  // 非根节点:更新父节点的指针,指向目标节点的右子树
                {
                    // 判断目标节点是父节点的左孩子还是右孩子
                    if (cur == parent->_left)
                    {
                        parent->_left = cur->_right;
                    }
                    else
                    {
                        parent->_right = cur->_right;
                    }
                }
                delete cur;  // 释放目标节点内存
                return true;  // 删除成功
            }
            // 情况2:目标节点右子树为空(左子树非空)
            else if (cur->_right == nullptr)
            {
                // 若目标节点是根节点,直接让根指向其左子树
                if (cur == _root)
                {
                    _root = cur->_left;
                }
                else  // 非根节点:更新父节点的指针,指向目标节点的左子树
                {
                    // 判断目标节点是父节点的左孩子还是右孩子
                    if (cur == parent->_left)
                    {
                        parent->_left = cur->_left;
                    }
                    else
                    {
                        parent->_right = cur->_left;
                    }
                }
                delete cur;  // 释放目标节点内存
                return true;  // 删除成功
            }
            else  // 情况3:目标节点左右子树均非空(使用替换法删除)
            {
                // 找目标节点右子树中值最小的节点(最左节点)作为替换节点
                Node* replaceParent = cur;  // 替换节点的父节点(初始为目标节点)
                Node* replace = cur->_right; // 从右子树开始查找
                while (replace->_left)  // 循环找到最左节点(值最小)
                {
                    replaceParent = replace;
                    replace = replace->_left;
                }

                // 将替换节点的值赋给目标节点(完成值替换)
                cur->_key = replace->_key;

                // 删除替换节点(替换节点必为左空或左右均空,符合情况1或2)
                // 判断替换节点是其父节点的左孩子还是右孩子
                if (replaceParent->_left == replace)
                {
                    replaceParent->_left = replace->_right;  // 替换节点的右子树补位
                }
                else
                {
                    replaceParent->_right = replace->_right; // 替换节点的右子树补位
                }

                delete replace;  // 释放替换节点内存
                return true;     // 删除成功
            }
        }
    }

    // 循环结束仍未找到目标节点,删除失败
    return false;
}

六 二叉搜索树key何key/value使用场景

1 key搜索场景

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

场景1:小区无人值守车库 小区车库中,仅购买了车位的业主车辆可进入小区。物业会将这些业主的车牌号录入后台系统,当车辆进入时,系统扫描车牌并查询其是否在系统内。若在,则抬杆放行;若不在,则提示"非本小区车辆",禁止进入。

场景2:英文文章单词拼写检查 将词库中所有单词存入二叉搜索树,读取文章中的每个单词后,在二叉搜索树中查找该单词。若未找到,则用波浪线标红该单词,提示拼写可能错误。

2 key/value搜索场景

每个关键码key都对应一个值value,value可以是任意类型的对象。树的结点结构中,除了存储key,还需存储对应的value。增、删、查操作仍以key为关键字,遵循二叉搜索树的规则进行比较,从而快速找到key对应的value。 基于key/value的搜索场景,实现的二叉搜索树支持修改操作,但仅能修改value,不能修改key。修改key会破坏二叉搜索树的性质。

场景1:简单中英互译字典 树的结点中存储key(英文单词)和value(对应的中文含义)。使用时输入英文单词,通过二叉搜索树查找该key,找到后即可获取其对应的中文含义,完成翻译

场景2:商场无人值守车库计费 车辆在入口进场时,系统扫描车牌并记录,此时树中存储的key为车牌,value为入场时间。车辆在出口离场时,系统再次扫描车牌并查找对应的value(入场时间)。用当前时间减去入场时间计算出停车时长,再根据时长计算停车费用,用户缴费后系统抬杆,车辆即可离场。

场景3:文章单词出现次数统计 读取文章中的每个单词,在二叉搜索树中查找该单词(key)。 - 若单词不存在,说明是首次出现,在树中新增结点,存储key(该单词)和value(次数1)。 - 若单词已存在,直接将该key对应的value(次数)加1,完成统计更新。

3 key/value⼆叉搜索树代码实现

key/value和上面写的搜索二叉树本质区别不大,需要在插入时增加value参数

cpp 复制代码
#include <iostream>
using namespace std;

// 命名空间:封装key-value型二叉搜索树相关结构
namespace key_value
{
    // 二叉搜索树节点结构体:存储key、value及左右孩子指针
    template<class K, class V>
    struct BSTreeNode
    {
        BSTreeNode<K, V>* _left;   // 左孩子指针
        BSTreeNode<K, V>* _right;  // 右孩子指针
        K _key;                    // 关键码:搜索的关键字(不可重复、不可修改)
        V _value;                  // 对应的值:可任意类型,支持修改

        // 节点构造函数:初始化左右孩子为nullptr,赋值key和value
        BSTreeNode(const K& key, const V& value)
            :_left(nullptr)
            , _right(nullptr)
            , _key(key)
            , _value(value)
        {}
    };

    // 二叉搜索树类:key不可重复,支持增、删、查,不支持修改key(仅能通过Find间接改value)
    template<class K, class V>
    class BSTree
    {
        typedef BSTreeNode<K, V> Node;  // 类型别名:简化节点类型书写
    public:
        // 插入操作:向树中插入(key, value)键值对,key不可重复
        // 参数:key-插入的关键码,value-对应的关联值
        // 返回值:插入成功返回true,key已存在返回false
        bool Insert(const K& key, const V& value)
        {
            // 情况1:树为空(根节点为nullptr),直接创建新节点作为根
            if (_root == nullptr)
            {
                _root = new Node(key, value);
                return true;
            }

            Node* parent = nullptr;  // 记录当前节点的父节点(用于后续挂接新节点)
            Node* cur = _root;       // 遍历指针:从根节点开始查找插入位置

            // 循环查找插入位置:根据BST性质(左小右大)遍历
            while (cur)
            {
                if (cur->_key < key)  // 当前key小于目标key,去右子树找
                {
                    parent = cur;
                    cur = cur->_right;
                }
                else if (cur->_key > key)  // 当前key大于目标key,去左子树找
                {
                    parent = cur;
                    cur = cur->_left;
                }
                else  // 找到相同key,不允许重复插入,返回false
                {
                    return false;
                }
            }

            // 循环结束:cur为nullptr,parent指向插入位置的父节点,创建新节点
            cur = new Node(key, value);
            // 根据父节点key与目标key的大小关系,挂接新节点到父节点的左/右孩子
            if (parent->_key < key)
            {
                parent->_right = cur;  // 父key小,新节点挂在右子树
            }
            else
            {
                parent->_left = cur;   // 父key大,新节点挂在左子树
            }

            return true;  // 插入成功
        }

        // 查找操作:根据key查找对应的节点
        // 参数:key-要查找的关键码
        // 返回值:找到返回对应节点指针,未找到返回nullptr
        Node* Find(const K& key)
        {
            Node* cur = _root;  // 遍历指针:从根节点开始查找
            while (cur)
            {
                if (cur->_key < key)  // 当前key小,去右子树找
                {
                    cur = cur->_right;
                }
                else if (cur->_key > key)  // 当前key大,去左子树找
                {
                    cur = cur->_left;
                }
                else  // 找到匹配key,返回当前节点(可通过节点指针修改value)
                {
                    return cur;
                }
            }

            return nullptr;  // 遍历完未找到,返回nullptr
        }

        // 删除操作:根据key删除对应的节点,维护BST树性质
        // 参数:key-要删除的关键码
        // 返回值:删除成功返回true,key不存在返回false
        bool Erase(const K& key)
        {
            Node* parent = nullptr;  // 记录待删除节点的父节点
            Node* cur = _root;       // 遍历指针:查找待删除节点

            // 第一步:查找待删除节点cur及其父节点parent
            while (cur)
            {
                if (cur->_key < key)  // 当前key小,去右子树找
                {
                    parent = cur;
                    cur = cur->_right;
                }
                else if (cur->_key > key)  // 当前key大,去左子树找
                {
                    parent = cur;
                    cur = cur->_left;
                }
                else  // 找到待删除节点cur,进入删除逻辑
                {
                    // 情况1:待删除节点无左孩子(左子树为空)
                    if (cur->_left == nullptr)
                    {
                        // 子情况1.1:待删除节点是根节点(无父节点)
                        if (cur == _root)
                        {
                            _root = cur->_right;  // 根节点更新为右孩子(可能为nullptr)
                        }
                        else  // 子情况1.2:待删除节点是父节点的左/右孩子
                        {
                            if (cur == parent->_left)  // cur是父节点的左孩子
                            {
                                parent->_left = cur->_right;  // 父节点左孩子指向cur的右孩子
                            }
                            else  // cur是父节点的右孩子
                            {
                                parent->_right = cur->_right;  // 父节点右孩子指向cur的右孩子
                            }
                        }
                        delete cur;  // 释放待删除节点内存
                        return true;
                    }
                    // 情况2:待删除节点无右孩子(右子树为空)
                    else if (cur->_right == nullptr)
                    {
                        // 子情况2.1:待删除节点是根节点
                        if (cur == _root)
                        {
                            _root = cur->_left;  // 根节点更新为左孩子(可能为nullptr)
                        }
                        else  // 子情况2.2:待删除节点是父节点的左/右孩子
                        {
                            if (cur == parent->_left)  // cur是父节点的左孩子
                            {
                                parent->_left = cur->_left;  // 父节点左孩子指向cur的左孩子
                            }
                            else  // cur是父节点的右孩子
                            {
                                parent->_right = cur->_left;  // 父节点右孩子指向cur的左孩子
                            }
                        }
                        delete cur;  // 释放待删除节点内存
                        return true;
                    }
                    // 情况3:待删除节点左右子树均不为空
                    else
                    {
                        // 解决方案:用cur右子树的最小节点(最左节点)替代cur
                        Node* replaceParent = cur;  // 替代节点的父节点(初始为cur)
                        Node* replace = cur->_right;  // 替代节点(从cur右子树开始找)

                        // 查找右子树的最左节点(即右子树的最小节点)
                        while (replace->_left)
                        {
                            replaceParent = replace;
                            replace = replace->_left;
                        }

                        // 用替代节点的key和value覆盖待删除节点cur(仅覆盖值,不改变指针关系)
                        cur->_key = replace->_key;
                        cur->_value = replace->_value;

                        // 处理替代节点的后续指针(替代节点无左孩子,只需处理右孩子)
                        if (replaceParent->_left == replace)  // 替代节点是其父节点的左孩子
                        {
                            replaceParent->_left = replace->_right;
                        }
                        else  // 替代节点是其父节点的右孩子(cur右子树无左节点时)
                        {
                            replaceParent->_right = replace->_right;
                        }

                        delete replace;  // 释放替代节点内存(原cur节点已被"间接删除")
                        return true;
                    }
                }
            }

            // 遍历完未找到key,删除失败
            return false;
        }

        // 中序遍历:输出树中所有(key:value),BST中序遍历结果为key升序排列
        void InOrder()
        {
            _InOrder(_root);  // 调用私有递归遍历函数,从根节点开始
            cout << endl;     // 遍历结束后换行
        }

    private:
        // 私有递归中序遍历函数:供公有InOrder调用,隐藏递归实现细节
        void _InOrder(Node* root)
        {
            if (root == nullptr)  // 递归终止条件:节点为空
                return;

            _InOrder(root->_left);          // 递归遍历左子树
            cout << root->_key << ":" << root->_value << endl;  // 访问当前节点(输出key:value)
            _InOrder(root->_right);         // 递归遍历右子树
        }

    private:
        Node* _root = nullptr;  // 树的根节点指针,初始化为空树
    };
}

4 水果类示例

cpp 复制代码
int main()
{
 string arr[] = { "苹果", "西⽠", "苹果", "西⽠", "苹果", "苹果", "西⽠", "苹
果", "⾹蕉", "苹果", "⾹蕉" };
 BSTree<string, int> countTree;
 for (const auto& str : arr)
 {
 // 先查找⽔果在不在搜索树中 
 // 1、不在,说明⽔果第⼀次出现,则插⼊<⽔果, 1> 
 // 2、在,则查找到的结点中⽔果对应的次数++ 
 //BSTreeNode<string, int>* ret = countTree.Find(str);
 auto ret = countTree.Find(str);
 if (ret == NULL)
 {
 countTree.Insert(str, 1);
 }
 else
 {
 ret->_value++;
 }
 }
 countTree.InOrder();
 return 0;
}

countTree是实例化对象

疑问点:为什么是ret->_value++,而不是str->_value++

countTree,find(str)的功能是:根据str,根据二叉搜索树中查找对应得节点,找到后返回该节点的指针(BSTreeNode<string,int>*)

因为ret指向的是存储键值对的树节点,而str只是单纯的的字符串,只有节点才能访问------value这个成员,字符串不能访问

相关推荐
无限进步_2 小时前
C语言atoi函数实现详解:从基础到优化
c语言·开发语言·c++·git·后端·github·visual studio
AA陈超2 小时前
ASC学习笔记0022:在不打算修改属性集时访问生成的属性集
c++·笔记·学习·ue5·虚幻引擎·unreal engine
初夏睡觉2 小时前
P1048 [NOIP 2005 普及组] 采药
数据结构·c++·算法
上去我就QWER2 小时前
C++中的堆和栈
开发语言·c++
小欣加油2 小时前
leetcode 1513 仅含1的子串数
c++·算法·leetcode·职场和发展
HalvmånEver2 小时前
Linux:基础开发工具(四)
linux·运维·服务器·开发语言·学习·makefile
专注VB编程开发20年2 小时前
.net按地址动态调用VC++DLL将非托管DLL中的函数地址转换为.NET可调用的委托
开发语言·c++·c#·.net
u***u6853 小时前
PHP最佳实践
开发语言·php
是店小二呀3 小时前
使用Rust构建一个完整的DeepSeekWeb聊天应用
开发语言·后端·rust