【数据结构】二叉搜索树(二叉排序树)

简介: 本文介绍了二叉搜索树(Binary Search Tree, BST)的定义、实现及其性能分析。二叉搜索树是一种特殊的二叉树,其特点是左子树所有节点值小于根节点值,右子树所有节点值大于根节点值,且每个子树也满足此特性。文中详细讲解了BST的节点结构、插入、查找、删除等操作的实现,并通过C++代码展示了这些功能。此外,还讨论了BST的性能:在理想情况下,时间复杂度接近O(logN),但在最坏情况下可能退化为O(N)。为了提高效率,后续将学习自平衡二叉搜索树如AVL树和红黑树。掌握BST有助于理解STL中的set和map容器。感谢阅读,欢迎点赞支持。

前言

       之前,我们学习了树这一数据结构,并尝试实现了堆以及链式二叉树的相关功能。


今天,我们将在简单二叉树的基础上,进一步学习一种更为复杂的结构——二叉搜索树。


       之前我们利用c语言实现了顺序表、链表、二叉树等数据结构。但是在实现一些复杂数据结构时,c语言显得相对繁琐,并且容易出现代码冗余的问题。由于我们现在已经具备了一定的c++代码基础,因此在之后的数据结构学习中,我们将用c++实现。


正文开始


一、什么是二叉搜索树

       二叉搜索树(Binary Search Tree),也叫二叉排序树或者二叉查找树, 是一种特殊的二叉树结构。它或者是一棵空树,或者具有以下特点:


1. 如果根节点的左子树不为空,则左子树上的所有节点都小于等于根节点的值。


2. 如果根节点的右子树不为空,则右子树上的所有节点都大于等于根节点的值。


3. 它的每一棵子树也都符合前两点特性(每棵子树也都是二叉搜索树)。


简单地说,二叉搜索树是一棵中序遍历结果有序的二叉树。



一般情况下,二叉搜索树不允许有相同的值出现。当然,你在设计二叉搜索树的时候也可以允许相同值出现,只要满足二叉搜索树的特性即可。注意:二叉搜索树不一定是一棵完全二叉树。


二、二叉搜索树的实现

       接下来,我们尝试实现二叉搜索树的结构及其常用方法。


节点

       首先是它的节点的结构。二叉搜索树的节点包含一个数据域和指向两孩子的指针:

//节点
template<class K>
struct BSTNode
{
    K _key;//数据域
    BSTNode<K>* _left;//指向左孩子的指针
    BSTNode<K>* _right;//指向右孩子的指针
 
    //节点构造
    BSTNode(const K& key)
        :_key(key)
        , _left(nullptr)
        , _right(nullptr)
    {}
};

可以看到,节点的定义当中,模板参数被命名为"K"。这里的"K"通常表示键(key)的类型,特别应用于key_value(键值对,可以理解为一个二元组,每一个key都有对应的value)的场景。一般情况下,key的值是不允许修改的,但是其对应的value可以修改。本次实现二叉搜索树的过程当中,我们仅实现带有key的结构,至于key_value结构,会在之后的AVL树和红黑树中实现。


属性和接口的声明

       接下来是二叉搜索树类的成员变量和成员函数的声明:

//二叉搜索树
template<class K>
class BST
{
    typedef BSTNode<K> Node;//重命名,简化代码
public:
    //强制生成无参构造
    BST() = default;
 
    //拷贝构造
    BST(const BST<K>& t);
 
    //析构
    ~BST();
 
    //插入
    bool Insert(const K& key);
 
    //删除
    bool Erase(const K& key);
 
    //查找
    Node* Find(const K& key);
 
    //中序遍历
    void Inorder();
private:
    Node* _root = nullptr;//根节点的指针
};


插入

       现在我们开始实现二叉搜索树节点的插入。为了保持搜索树的特性,有如下插入步骤:


1. 如果树为空,则直接将新节点赋值给根节点的指针。


2. 如果树不为空,则需要根据新节点的值进行搜索,如果某节点的值大于新节点,则向右走;若小于新节点,则向左走。走到空位置之后,按照值插入节点即可。


3. 如果要插入重复的值,则遇到相同值的节点之后可以向左走,也可以向右走,直到找到空位置插入。(我们的实现默认不支持插入重复值)



代码实现:

//插入
bool Insert(const K& key)
{
    if (_root == nullptr)//树为空,直接插入
    {
        _root = new Node(key);
        return true;
    }
    else//树不为空,进行搜索
    {
        Node* cur = _root;//从根节点开始搜索
        Node* parent = nullptr;//记录cur的父亲节点
        while (cur)//走到空为止
        {
            if (key < cur->_key)//值小向左走
            {
                parent = cur;
                cur = cur->_left;
            }
            else if (key > cur->_key)//值大向右走
            {
                parent = cur;
                cur = cur->_right;
            }
            else//这里不允许重复的值插入
            {
                return false;
            }
        }
 
        //此时cur走到空,进行插入
        cur = new Node(key);
        if (key < parent->_key)//值小,往左边插
        {
            parent->_left = cur;
        }
        else//值大,往右边插
        {
            parent->_right = cur;
        }
        return true;
    }
}


这里我们定义了一个parent指针,用于记录cur的父亲节点。当cur走到空时,parent刚好指向插入位置的父亲节点,然后与parent值进行大小比较,插入新节点。


查找

       二叉搜索树的特性使其具备了较高的查找效率。查找步骤如下:


1. 从根节点开始,与要查找的值进行比较,如果要找的值比根小,则向左走,否则向右走。循环往复。

2. 如果走到空,则说明不存在,返回空指针。

3. 如果不支持插入重复值,找到后返回节点地址。

4. 如果支持插入重复值,则找到后一般返回中序遍历结果的第一个节点。



代码实现:

//查找
Node* Find(const K& key)
{
    Node* cur = _root;//从根节点开始搜索
    while (cur)//走到空为止
    {
        if (key < cur->_key)//值小向左走
        {
            cur = cur->_left;
        }
        else if (key > cur->_key)//值大向右走
        {
            cur = cur->_right;
        }
        else//相等说明找到了,返回节点地址
        {
            return cur;
        }
    }
 
    //走到空说明没找到,返回空指针
    return nullptr;
}


删除

       删除节点是所有接口中实现难度最大的一个。我们默认需要按值来删除元素,所以要先进行查找,找到之后再删除该节点。删除节点之后,为了保持二叉搜索树的原有特性,就需要调整其他节点。根据被删除的节点,有四种情况需要分析讨论:


1. 被删除节点的左右孩子均为空



2. 被删除的节点只有左孩子



3. 被删除的节点只有右孩子



4.  被删除的节点既有左孩子,又有右孩子



这里说明一下寻找右子树最小值的方法:从右孩子开始,一直访问其左孩子节点,直到访问到空为止。这里访问到的最后一个节点即是右子树的最小值。(左子树最大值也同理)


接下来,我们尝试实现删除节点的操作:


//删除
bool Erase(const K& key)
{
    //首先按值查找要被删除的节点
    Node* cur = _root;
    Node* parent = nullptr;//记录父亲节点
    while (cur)
    {
        if (key < cur->_key)//值小向左走
        {
            parent = cur;
            cur = cur->_left;
        }
        else if (key > cur->_key)//值大向右走
        {
            parent = cur;
            cur = cur->_right;
        }
        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 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;
            }
            //有两个孩子的情况
            else
            {
                //首先寻找右子树的最小值
                Node* rightMin = cur->_right;//从右孩子开始搜索
                Node* rightMinParent = cur;//记录最小值的父亲节点
 
                while (rightMin->_left)//left走到空,rightMin即为右子树的最小值
                {
                    rightMinParent = rightMin;
                    rightMin = rightMin->_left;//一直向左走,找最小值
                }
 
                //将最小值赋值给被删除节点
                cur->_key = rightMin->_key;
 
                //将替代节点的右孩子托付给父亲
                //先判断自己是父亲节点的哪一个孩子
                if (rightMin == rightMinParent->_left)
                    rightMinParent->_left = rightMin->_right;
                else
                    rightMinParent->_right = rightMin->_right;
                delete rightMin;//删除该节点
                return true;
            }
        }
    }
    //没找到,返回false
    return false;
}


这里有两点需要注意:


1. 第一种情况可以和第二/第三种情况合并,将空指针托付给父亲节点不会影响整体操作。


2. 第四种情况当中,如果我们找的是右子树的最小值,那么它或是叶子节点(第一种情况),或只有右孩子(第三种情况),不会有左孩子;如果我们找的是左子树的最大值,那么它或是叶子节点(第一种情况),或只有左孩子(第二种情况),不会有右孩子。所以代码中可以非常确定托付哪一个孩子。


拷贝构造

       接下来我们实现拷贝构造。该接口比较简单,直接递归拷贝即可。代码如下:

//拷贝构造
BST(const BST<K>& t)
{
    _root = _Copy(t._root);
}
 
Node* _Copy(Node* root)
{
    if (root == nullptr)
    {
        return nullptr;
    }
    Node* newRoot = new Node(root->_key);//创建新的根节点
    newRoot->_left = _Copy(root->_left);//拷贝左子树
    newRoot->_right = _Copy(root->_right);//拷贝右子树
    return newRoot;//返回根节点
}

析构

       与拷贝构造相同,析构时递归释放每一个节点。代码如下:

//析构
~BST()
{
    _Destroy(_root);
}
void _Destroy(Node* root)
{
    if (root == nullptr)
    {
        return;
    }
    _Destroy(root->_left);//销毁左子树
    _Destroy(root->_right);//销毁右子树
    delete root;//删除根节点
}

中序遍历

       由于二叉搜索树的中序遍历结果是有序的,所以我们来实现一个中序遍历接口。中序遍历的代码与传统的二叉树完全相同。


实现如下:

//中序遍历
void Inorder()
{
    _Inorder(_root);
    cout << endl;
}
void _Inorder(Node* root)
{
    if (root == nullptr)
    {
        return;
    }
    _Inorder(root->_left);//遍历左子树
    cout << root->_key << ' ';//访问根节点
    _Inorder(root->_right);//遍历右子树
}


最后,我们来使用一下我们实现的二叉搜索树:

int main()
{
    BST<int> t;
    int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
    for (auto& e : a)//循环插入
    {
        t.Insert(e);
        t.Inorder();
    }
    cout << endl;
    for (auto& e : a)//循环删除
    {
        t.Erase(e);
        t.Inorder();
    }
    return 0;
}

运行结果:



程序全部代码

二叉搜索树的全部实现代码如下:

#include <iostream>
using namespace std;
 
//节点
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 BST
{
    typedef BSTNode<K> Node;//重命名,简化代码
public:
    //强制生成无参构造
    BST() = default;
 
    //拷贝构造
    BST(const BST<K>& t)
    {
        _root = _Copy(t._root);
    }
 
    //析构
    ~BST()
    {
        _Destroy(_root);
    }
 
    //插入
    bool Insert(const K& key)
    {
        if (_root == nullptr)//树为空,直接插入
        {
            _root = new Node(key);
            return true;
        }
        else//树不为空,进行搜索
        {
            Node* cur = _root;//从根节点开始搜索
            Node* parent = nullptr;//记录cur的父亲节点
            while (cur)//走到空为止
            {
                if (key < cur->_key)//值小向左走
                {
                    parent = cur;
                    cur = cur->_left;
                }
                else if (key > cur->_key)//值大向右走
                {
                    parent = cur;
                    cur = cur->_right;
                }
                else//这里不允许重复的值插入
                {
                    return false;
                }
            }
 
            //此时cur走到空,进行插入
            cur = new Node(key);
            if (key < parent->_key)//值小,往左边插
            {
                parent->_left = cur;
            }
            else//值大,往右边插
            {
                parent->_right = cur;
            }
            return true;
        }
    }
 
    //删除
    bool Erase(const K& key)
    {
        //首先按值查找要被删除的节点
        Node* cur = _root;
        Node* parent = nullptr;//记录父亲节点
        while (cur)
        {
            if (key < cur->_key)//值小向左走
            {
                parent = cur;
                cur = cur->_left;
            }
            else if (key > cur->_key)//值大向右走
            {
                parent = cur;
                cur = cur->_right;
            }
            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 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;
                }
                //有两个孩子的情况
                else
                {
                    //首先寻找右子树的最小值
                    Node* rightMin = cur->_right;//从右孩子开始搜索
                    Node* rightMinParent = cur;//记录最小值的父亲节点
 
                    while (rightMin->_left)//left走到空,rightMin即为右子树的最小值
                    {
                        rightMinParent = rightMin;
                        rightMin = rightMin->_left;//一直向左走,找最小值
                    }
 
                    //将最小值赋值给被删除节点
                    cur->_key = rightMin->_key;
 
                    //将替代节点的右孩子托付给父亲
                    //先判断自己是父亲节点的哪一个孩子
                    if (rightMin == rightMinParent->_left)
                        rightMinParent->_left = rightMin->_right;
                    else
                        rightMinParent->_right = rightMin->_right;
                    delete rightMin;//删除该节点
                    return true;
                }
            }
        }
        //没找到,返回false
        return false;
    }
 
    //查找
    Node* Find(const K& key)
    {
        Node* cur = _root;//从根节点开始搜索
        while (cur)//走到空为止
        {
            if (key < cur->_key)//值小向左走
            {
                cur = cur->_left;
            }
            else if (key > cur->_key)//值大向右走
            {
                cur = cur->_right;
            }
            else//相等说明找到了,返回节点地址
            {
                return cur;
            }
        }
 
        //走到空说明没找到,返回空指针
        return nullptr;
    }
 
    //中序遍历
    void Inorder()
    {
        _Inorder(_root);
        cout << endl;
    }
private:
    Node* _Copy(Node* root)
    {
        if (root == nullptr)
        {
            return nullptr;
        }
        Node* newRoot = new Node(root->_key);//创建新的根节点
        newRoot->_left = _Copy(root->_left);//拷贝左子树
        newRoot->_right = _Copy(root->_right);//拷贝右子树
        return newRoot;//返回根节点
    }
 
    void _Destroy(Node* root)
    {
        if (root == nullptr)
        {
            return;
        }
        _Destroy(root->_left);//销毁左子树
        _Destroy(root->_right);//销毁右子树
        delete root;//删除根节点
    }
 
    void _Inorder(Node* root)
    {
        if (root == nullptr)
        {
            return;
        }
        _Inorder(root->_left);//遍历左子树
        cout << root->_key << ' ';//访问根节点
        _Inorder(root->_right);//遍历右子树
    }
 
    Node* _root = nullptr;//根节点的指针
};


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

       接下来,我们简单分析一下二叉搜索树的性能。


设二叉树的节点数为N:


在较好情况下,二叉搜索树接近于完全二叉树的状态,它的高度为log₂N;


在极端情况下,二叉搜索树是单支树的状态,高度为N。



综合而言, 二叉搜索树增、删、改的时间复杂度为O(N)。


       在较好情况下,其增删改的效率可接近于O(logN),但是它的效率受高度影响较大。所以传统的二叉搜索树显然不满足我们高效查找的需求。之后我们会学习自平衡的二叉搜索树(如AVL树、红黑树),它们会尽量降低二叉搜索树的高度,提高效率。


总结

       今天我们学习了一种特殊的二叉树结构--二叉搜索树,它的特性使其具备了较好的查找效率。掌握二叉搜索树的知识,将为我们后续学习STL中的set和map容器打下坚实的基础。如果你觉得博主讲的还不错,就请留下一个小小的赞在走哦,感谢大家的支持❤❤❤

相关文章
|
1月前
|
算法 Java
算法系列之数据结构-二叉搜索树
二叉查找树(Binary Search Tree,简称BST)是一种常用的数据结构,它能够高效地进行查找、插入和删除操作。二叉查找树的特点是,对于树中的每个节点,其左子树中的所有节点都小于该节点,而右子树中的所有节点都大于该节点。
85 22
|
3月前
|
算法 C++
【C++数据结构——查找】二叉排序树(头歌实践教学平台习题)【合集】
【数据结构——查找】二叉排序树(头歌实践教学平台习题)【合集】 目录 任务描述 相关知识 测试说明 我的通关代码: 测试结果: 任务描述 本关任务:实现二叉排序树的基本算法。 相关知识 为了完成本关任务,你需要掌握:二叉树的创建、查找和删除算法。具体如下: (1)由关键字序列(4,9,0,1,8,6,3,5,2,7)创建一棵二叉排序树bt并以括号表示法输出。 (2)判断bt是否为一棵二叉排序树。 (3)采用递归方法查找关键字为6的结点,并输出其查找路径。 (4)分别删除bt中关键
102 11
【C++数据结构——查找】二叉排序树(头歌实践教学平台习题)【合集】
|
7月前
|
存储 Java Serverless
【数据结构】哈希表&二叉搜索树详解
本文详细介绍了二叉搜索树和哈希表这两种数据结构。二叉搜索树是一种特殊二叉树,具有左子树节点值小于根节点、右子树节点值大于根节点的特点,并且不允许键值重复。文章给出了插入、删除和搜索等方法的具体实现。哈希表则通过哈希函数将键名映射为数组下标,实现快速查找,其插入、删除和查找操作时间复杂度理想情况下为O(1)。文中还讨论了哈希函数的设计原则、哈希冲突的解决方法及哈希表的实现细节。
164 8
【数据结构】哈希表&二叉搜索树详解
|
6月前
|
存储 算法 数据管理
数据结构与算法学习二零:二叉排序树(BST)、平衡二叉树(AVL)
这篇文章通过需求分析、代码实现和测试验证,详细介绍了二叉排序树的创建、遍历和删除操作,以及二叉平衡树(AVL)的自平衡特性和单旋转操作,旨在提高树结构在数据管理中的效率和性能。
133 0
数据结构与算法学习二零:二叉排序树(BST)、平衡二叉树(AVL)
|
6月前
【高阶数据结构】深度探索二叉树进阶:二叉搜索树概念及其高效实现(三)
【高阶数据结构】深度探索二叉树进阶:二叉搜索树概念及其高效实现
|
6月前
|
存储
【高阶数据结构】深度探索二叉树进阶:二叉搜索树概念及其高效实现(二)
【高阶数据结构】深度探索二叉树进阶:二叉搜索树概念及其高效实现
|
6月前
【高阶数据结构】深度探索二叉树进阶:二叉搜索树概念及其高效实现(一)
【高阶数据结构】深度探索二叉树进阶:二叉搜索树概念及其高效实现
|
7月前
|
存储 C语言
数据结构基础详解(C语言): 树与二叉树的应用_哈夫曼树与哈夫曼曼编码_并查集_二叉排序树_平衡二叉树
本文详细介绍了树与二叉树的应用,涵盖哈夫曼树与哈夫曼编码、并查集以及二叉排序树等内容。首先讲解了哈夫曼树的构造方法及其在数据压缩中的应用;接着介绍了并查集的基本概念、存储结构及优化方法;随后探讨了二叉排序树的定义、查找、插入和删除操作;最后阐述了平衡二叉树的概念及其在保证树平衡状态下的插入和删除操作。通过本文,读者可以全面了解树与二叉树在实际问题中的应用技巧和优化策略。
176 2
|
6月前
【数据结构】二叉搜索树的功能实现详解
【数据结构】二叉搜索树的功能实现详解
56 0
|
10月前
数据结构学习记录——判断是否为同一颗二叉搜索树(题意理解、求解思路、程序搭建框架、具体函数的实现)
数据结构学习记录——判断是否为同一颗二叉搜索树(题意理解、求解思路、程序搭建框架、具体函数的实现)
111 2
下一篇
oss创建bucket