【数据结构与算法】二叉搜索树

news2024/11/25 10:58:26

文章目录

    • 二叉搜索树的结构
    • 二叉搜索树的实现
      • 节点的定义
      • 二叉搜索树的框架
      • 构造函数
      • 拷贝构造函数
      • 赋值运算符重载
      • 析构函数
      • 搜索操作
      • 插入操作
      • 删除操作
    • 二叉搜索树的应用
    • 二叉搜索树的效率

二叉搜索树的结构

在浅学一下二叉树链式存储结构的遍历_链式存储二叉树按层次遍历_LeePlace的博客-CSDN博客一文中简单介绍了一下普通二叉树的三种遍历方式。

我们知道普通二叉树是没有什么实用性的,

但是如果在普通二叉树的基础上对其结构进行一些改进,

或许能发挥巨大的价值。

二叉搜索树(BinarySearchTree/二叉排序树)就是普通二叉树的一种变种。

我们规定一个普通二叉树具有如下性质:

  • 若它的左子树不为空,则它左子树上所有节点的值都小于根节点的值
  • 若它的右子树不为空,则它右子树上所有节点的值都大于根节点的值
  • 它的左右子树也具有上面两条性质

那么这颗二叉树就是一颗二叉搜索树。

下面就是一颗简单的二叉搜索树:

image-20230523170609771

此时可以观察到二叉搜索树有一个良好的性质,

当我们按中序去遍历这棵树时,

遍历到的数据是以此有序的,

所以二叉搜索树可以进行排序。

另一方面,当我们想要查询某个数据时,只需要走一条路径即可,

举个例子,假设我要寻找7

从根节点8开始,78小,那么7应该在左子树,

此时往左走,根节点变成373大,那么7应该在右子树,

现在往右走,根节点变成676大,那么7应该在右子树,

再往右走,根节点变成7,和我们要查找的一样,就找到了。

如果沿着某条路径没有找到,说明树中没有该目标值。

如果这棵树是一颗满二叉树,那么查询的效率就来到logN

当然,普通二叉树是很难做到这一点的,

具体情景会在最后进行分析,

所以想要实现高效的查询,还需要对普通的二叉搜索树进行改造,

也就是后续会讲到的AVL树和红黑树。


二叉搜索树的实现

说明一下,下面实现的搜索二叉树存放的都是相异的值,

也就是说不会插入重复的值,

想要处理相同值也很简单,后面会提一下。


节点的定义

普通二叉搜索树的每个节点都要存放数据,

并且要链接子树,

所以普通二叉搜索树的节点很简单:

template<class T>
struct BSTreeNode
{
    T _key;
    BSTreeNode<T>* _left;
    BSTreeNode<T>* _right;

    BSTreeNode(const T& x)
        : _key(x)
        , _left(nullptr)
        , _right(nullptr)
    {}
};

注意要定义节点的构造函数,

因为插入的时候会有Node* newnode = new Node(x)这种操作,

如果不写构造函数的话就new不出新节点。


二叉搜索树的框架

我们要实现一个二叉搜索树类型,

类的成员只要一个指向根节点的指针就可。

考虑一下四个默认构造函数能不能完成任务:

插入一个新节点时要先new一个新节点出来,

也就意味着如果进行拷贝构造一颗搜索二叉树的话要进行深拷贝,

所以拷贝构造函数需要我们自己写,

赋值运算符重载同理。

成员是指针,指针指向一堆动态开辟的节点,

所以析构函数也需要我们自己写,

析构掉一个个节点。

对于二叉搜索树,我们主要完成三个功能:搜索、插入、删除。

每一个功能我们既可以通过非递归的方式实现,

也可以通过递归的方式实现,

所以我们主要完成三个功能的递归和非递归版本。

所以代码的基本框架就有了:

(这里只是给出一个大致的框架,不用纠结于参数设计,在下面具体实现的时候会进行讨论)

template <class T>
class BSTree
{
	typedef BSTreeNode<T> Node;
    
public:
    // 构造函数
    BSTree();
    
    // 拷贝构造函数
    BSTree(const BSTree& t); 
    
    // 赋值运算符重载函数
    BSTree<T>& operator=(BSTree<T> t);
    
    //析构函数
    ~BSTree();
    
    //搜索
    bool Find(const T& key);  //非递归
    bool FindR(const T& key); //递归
    
    //插入
    bool Insert(const T& key);  //非递归
    bool InsertR(const T& key); //递归
    
    //删除
    bool Erase(const T& key);   //非递归
    bool EraseR(const T& key);	//递归

private:
    Node* _root;
};

构造函数

构造函数还是比较简单的,

只需要把_root初始化为nullptr就OK:

public:
    BSTree()
        : _root(nullptr)
    {}

拷贝构造函数

拷贝构造函数需要我们完成深拷贝,

需要用一棵树构造出一颗一模一样的树。

一个思路是遍历树的每个节点,

然后将遍历到的节点的值作为参数,

调用Insert函数插入到新树中,

这样就只能用前序遍历,

虽然可行,但是效率太低。

不妨想一下,还是用前序遍历,

遍历到一个节点就拷贝new一个新节点,

对新节点的左子树和右子树也执行上述这个过程:

public:
    BSTree(const BSTree<T>& t)
    {
        _root = Copy(t._root);
    }
private:
    Node* Copy(Node* root)
    {
        if (root == nullptr)
            return nullptr;
        Node* newNode = new Node(root->_key); //拷贝根节点
        newNode->_left = Copy(root->_left);   //拷贝左子树
        newNode->_right = Copy(root->_right); //拷贝右子树
        return newNode;                       //返回根节点
    }

因为是要递归构造,

所以这里定义了一个辅助函数。


赋值运算符重载

对于赋值运算符重载函数就不需要写得很复杂了,

我们已经有了拷贝构造函数,

所以使用一种很妙的写法就行:

public:
    BSTree<T>& operator=(BSTree<T> t)
    {
        swap(_root, t._root);
        return *this;
    }

这里参数没有设置成const BSTree<T>& t

而是用了一个用实参拷贝构造出来的临时对象,

把两棵树进行换根,

临时对象就变成了原来的树,

出了函数要对临时对象进行析构,

原来的那些节点也就释放掉了。


析构函数

析构函数要完成的就是释放掉每一个节点,

这个需要通过后序遍历来完成:

析构

先释放左子树,再释放右子树,最后释放根节点,

同样需要递归完成,

所以同样需要一个辅助函数:

public:
    ~BSTree()
    {
        Destroy(_root);
    }
private:
    void Destroy(Node*& root)
    {
        if (root == nullptr)
            return;
        Destroy(root->_left);  //先释放左子树
        Destroy(root->_right); //再释放右子树
        delete root;           //最后释放根节点
        root = nullptr;		   //释放后把根节点指针置空
    }

搜索操作

搜索操作还是很简单的,

通过传入的key值直接沿着路径寻找就好,

如果当前节点的值比key小,那就向右走,

如果当前节点的值比key大,那就向左走,

如果当前节点的值与key相等,那就找到了,

如果走到空了还没有找到那就是不存在:

搜索成功 搜索失败

非递归搜索代码如下:

public:
    bool Find(const T& key)
    {
        Node* cur = _root;
        while (cur)
        {
            if (cur->_key > key)
                cur = cur->_left;
            else if (cur->_key < key)
                cur = cur->right;
            else
                return true;
        }
        return false;
    }

当然我们还可以通过递归实现,

如果当前节点的值比key小,那就去它的右子树中查找,

如果当前节点的值比key大,那就去它的左子树中查找,

如果当前节点的值与key相等,那就找到了,

如果走到空了还没有找到那就是不存在。

需要注意,递归版本的参数是要有一个Node*的,

Node* _root是私有成员,

在外面调用函数传参的时候访问不到,

所以需要在内部定义一个搜索函数,

对外开放的接口只需要调用这个内部函数就OK:

public:
    bool findR(const T& key)
    {
        return _findR(_root, key);
    }
private:
    bool _FindR(Node*& root, const T& key)
    {
        if (root == nullptr)
            return false;
        if (root->_key > key)
            return _FindR(root->_left, key);
        else if (root->_key < key)
            return _FindR(root->_right, key);
        else
            return true;
    }

插入操作

插入一个新节点,

要在叶子节点下面的空节点进行插入,

还是以上图的例子,假设要插入9

还是跟查找一样的逻辑先找到待插入的位置,

然后创建新节点并进行链接:

插入

有几个细节需要注意一下:

  • 当树为空时,直接在根节点插入
  • 最后还要和上一个节点链接,这里没有用三叉链结构,所以还需要一个parent指针保存上一个节点的地址
  • 链接节点时要判断一下是链到父节点的左边还是右边

非递归插入代码如下:

public:
	bool Insert(const T& key)
    {
        if (_root == nullptr)  //树为空,直接在根节点插入
        {
            _root = new Node(key);
            return true;
        }

        Node* parent = nullptr;
        Node* cur = _root;
        while (cur)
        {
            if (cur->_key > key)       //cur的值更大,要在cur的左侧插入
            {
                parent = cur;
                cur = cur->_left;
            }
            else if (cur->_key < key)  //cur的值更小,要在cur的右侧插入
            {
                parent = cur;
                cur = cur->_right;
            }
            else  //要插入的节点已经存在,插入失败
                return false;  
        }

        cur = new Node(key);
        if (parent->_key > key)  //如果比父节点小就链到左边
            parent->_left = cur;
        else                     //如果比父节点大就链到右边
            parent->_right = cur;

        return true;
    }

当然还可以递归插入,

如果比当前节点大就去左树插入,

如果比当前节点小就去右树插入,

如果走到空就进行插入:

public:
    bool InsertR(const T& key) 
    {
        return _InsertR(_root, key);
    }

private:
    bool _InsertR(Node*& root, const T& key)
    {
        if (root == nullptr)
        {
            root = new Node(key);
            return true;
        }
        if (root->_key < key)
            return _InsertR(root->_right, key);
        else if (root->_key > key)
            return _InsertR(root->_left, key);
        else
            return false;
    }

注意这里辅助函数的参数类型是Node*& root

它是父节点的左右节点的别名,

以上面插入9为例,

当走到空的时候它是Node(10)->left的别名,

所以我们直接在这儿插入就省去了链接这一过程。

而如果把参数设置成Node* root的话一方面要面临链接逻辑的问题,

另一方面当树为空的时候就无法完成插入,

因为root_root的一个别名,

因此无法改变_root的指向!


删除操作

搜索二叉树的删除操作是比较麻烦的,

如果删除的节点是叶子节点的话直接delete掉很简单,

但如果删除的节点不是叶子结点就需要进行讨论了。

  1. 待删除的节点左不为空右为空:

    以下面删除14节点为例:

    image-20230524162358108

    14的父节点是10

    1410的右子树的根节点,

    说明14的左子树节点都比10大,

    所以直接将左子树链到10的右边,

    这是待删除节点是父节点的右孩子时的处理方式。

    如果待删除节点是父节点的左孩子,

    那就把待删除节点的左子树链到父节点的左边。

    此时 需要考虑一下父节点为空的情况,

    也就是待删除节点是根节点,

    因为它的右子树为空,

    所以直接另_root指向它的左子树就好。

    这里还是涉及到了和父节点进行链接的步骤,

    所以还是需要一个parent节点记录当前节点的父节点。

    下面的代码省去了找到待删除节点的步骤,

    cur是待删除节点,parent是待删除节点的父节点,

    直接做这种情况下的删除操作:

    if (cur == _root)
    	_root = cur->_left;
    else
    {
    	if (parent->_left == cur)
    		parent->_left = cur->_left;
     	else
     		parent->_left = cur->_right;
    }
    delete cur;
    
  2. 待删除的节点右不为空左为空:

    这种情况跟上面大差不差,

    就不做详细的分析了:

    还是直接看代码,

    cur是待删除节点,parent是待删除节点的父节点:

    if (cur == _root)
         _root = cur->_right;
    else
    {
         if (parent->_left == cur)
             parent->_left = cur->_right;
         else
             parent->_right = cur->_right;
    }
    delete cur;
    
  3. 待删除节点的左右都不为空:

    下面以删除8节点为例:

    image-20230526115322517

    此时就不能进行简单的删除然后处理子树,

    因为待删除节点有两个子树不好处理。

    所以这种情况下考虑一种新的方法——替换法。

    首先考虑当前中序的遍历顺序为1 3 4 6 7 8 10 13 14

    删除掉8之后8的位置应当被710取而代之,

    710又是什么呢?是左子树的最大节点或右子树的最小节点!

    所以第一步是先找到左子树的最大节点或右子树的最小节点,

    左子树的最大节点只需要从左子树的根节点出发,

    一路向右走,走到叶子节点就找到了:

    寻找右子树的最小节点就是一路向左走,

    这里就不做演示了。

    然后我们不真正删除待删除节点,

    只是把它的值替换成我们查找到的左右子树的最大或最小值,

    然后删掉左右子树的最大或最小值。

    但是左右子树的最大或最小节点不能直接删除,

    因为他们可能还有子树需要处理,

    不过方便的是它们只有单边存在子树:

    左子树的最大节点只可能存在左子树,右子树的最小节点只可能存在右子树

    那么对于被替换的节点的删除又可以采取此前的方法,

    将左子树或右子树托付给它们的父节点。

    下面以用右子树的最小节点替换为例进行演示:

    替换法删除节点

    cur是待删除的节点,

    minRight记录待删除的节点的右树的最小节点,

    parent记录右树的最小节点的父节点,便于链接最小节点的右子树,

    代码如下:

    Node* parent = cur;
    Node* minRight = cur->_right;
    while (minRight->_left)
    {
        parent = minRight;
        minRight = minRight->_left;
    }
    cur->_key = minRight->_key;
    if (minRight == parent->_left)
        parent->_left = minRight->_right;
    else
        parent->_right = minRight->_right;
    delete minRight;
    

综上三种情况,非递归方式的删除就处理完毕了,

对于要删除的节点是叶子结点,其实处理方式跟一二两种情况完全一样。

完整代码如下:

bool Erase(const T& key)
{
    //寻找待删除节点
    Node* cur = _root;
    Node* parent = nullptr;  //这里的parent是便于一二两种情况托付孩子
    while (cur)
    {
        if (cur->_key > key)
        {
            parent = cur;
            cur = cur->_left;
        }
        else if (cur->_key < key)
        {
            parent = cur;
            cur = cur->_right;
        }
        else  //到这里说明找到了要删除的节点
        {
            if (cur->_left == nullptr)  //左子树是空的情况
            {
                if (cur == _root)  //如果待删除的节点是根节点直接换根
                    _root = cur->_right;
                else  //如果待删除节点不是根节点则需要托付孩子
                {
                    if (parent->_left == cur)
                        parent->_left = cur->_right;
                    else
                        parent->_right = cur->_right;
                }
                delete cur;
            }
            else if (cur->_right == nullptr)  //右子树是空的情况
            {
                if (cur == _root)  //如果待删除的节点是根节点直接换根
                    _root = cur->_left;
                else  //如果待删除节点不是根节点则需要托付孩子
                {
                    if (parent->_left == cur)
                        parent->_left = cur->_left;
                    else
                        parent->_left = cur->_right;
                }
                delete cur;
            }
            else  //左右子树都不为空
            {
                Node* parent = cur;  //记录最小节点的父节点,便于托付最小节点的右孩子
                Node* minRight = cur->_right;  //记录待删除节点的右树的最小节点
                while (minRight->_left)
                {
                    parent = minRight;
                    minRight = minRight->_left;
                }
                cur->_key = minRight->_key;
                if (minRight == parent->_left)  //托付最小节点的右孩子给parent
                    parent->_left = minRight->_right;
                else
                    parent->_right = minRight->_right;
                delete minRight;
            }

            return true;
        }
    }
    return false;
}

以上是非递归方法,下面介绍一下递归方法。

同递归插入一样,我们还是以Node*& root作为参数,

const T& key作为待删除的参考值,

代码如下,后面做解释:

public:
    bool EraseR(const T& key)
    {
        return _EraseR(_root, key);
    }
    
private:
    bool _EraseR(Node*& root, const T& key)
    {
        if (root == nullptr)
            return false;
        if (root->_key > key)
            return _EraseR(root->_left, key);
        else if (root->_key < key)
            return _EraseR(root->_right, key);
        else
        {
            Node* del = root;
            if (root->_left == nullptr)
                root = root->_right;
            else if (root->_right == nullptr)
                root = root->_left;
            else
            {
                Node* minRight = root->_right;
                while (minRight->_left)
                    minRight = minRight->_left;
                swap(root->_key, minRight->_key);
                return _EraseR(root->_right, key);
            }
            delete del;
            return true;
        }
    }

如果root->_keykey大,那就去root的左树去删除;

如果root->_keykey小,那就去root的右树去删除;

如果root走到空了,说明树中不存在要删除的节点,直接return false

如果找到了待删除的节点,那就进行同上面一样的分类讨论:

假设待删除节点的父节点是parent

那这个待删除节点root就是parent->leftparent->right的别名,

所以我们改变root就是改变的parent->leftparent->right

对于左右子树有一个为空的情况,我们就可以直接进行链接,

对于左右子树都不为空的情况,

我们可以先交换待删除节点的值和右子树的最小节点的值(或左子树最大节点的值),

此时待删除节点就变到了原来右子树最小节点所在位置,

我们就可以通过递归去当前替换后的节点的左树(用左子树最大节点替换)或右树(用右子树最小节点替换)进行删除,

也就是_EraseR(root->_right, key)


二叉搜索树的应用

上面管于基本的二叉搜索树的实现已经讲解完了,

下面就是一些简单应用。

有时候我们只需要存储单个的值,

比如我们可以想验证一个单词的拼写正不正确,

我们可以把单词一个个的插入到搜索树中建立一个单词库,

然后直接在搜索树中查找要验证的单词。

此时上面搜索树的结构足以完成这个任务。

而有时我们又需要存储多个值,

比如我们要做一个英语词典,

给我一个英文单词我能给出它的汉语意思,

此时可能搜索树的一个节点就要存储多个数据,

比如其中一个数据string word是英文单词,

另一个数据vector<string>就是该单词对应的所有汉语翻译,

此时我们就需要对二叉树进行一些简单的改造:

此时对应的存储模型其实就是key_value模型,

一个key对应一个value

key就是我们查找、插入、删除的键值,

value就是键值对应的数据。

比如上面那个场景下我们就可以使用英文单词作为key

汉语翻译合集作为value

此时的二叉树节点就要存放两个类型的数据stringvector<string>

后续的查找插入删除操作也许进行些许改动,

改变一下比较key的方式,

都是些小打小闹的改动,大框架还是不变的。

下面是各个部分改造后的代码:

  1. 节点:

    template<class K, class V>
    struct BSTreeNode
    {
        K _key;
        V _value;
        BSTreeNode<K, V>* _left;
        BSTreeNode<K, V>* _right;
    
        BSTreeNode(const K& key, const V& val)
            : _key(key)
            , _value(val)
            , _left(nullptr)
            , _right(nullptr)
        {}
    };
    
  2. 框架:

    template<class K, class V>
    class BSTree
    {
        typedef BSTreeNode<K, V> Node;
    
    public:
    	//构造函数
    	BSTree();
    	
    	//拷贝构造函数
        BSTree(const BSTree<K, V>& t);
        
        //赋值运算符重载
        BSTree<K, V>& operator=(BSTree<K, V> t)//析构函数
        ~BSTree();
        
        //搜索
        bool Find(const K& key);  //非递归
        bool FindR(const K& key); //递归
        
        //插入
        bool Insert(const K& key);  //非递归
        bool InsertR(const K& key); //递归
        
        //删除
        bool Erase(const K& key);   //非递归
        bool EraseR(const K& key);	//递归
        
    private:
        Node* _root = nullptr;
    
  3. 构造函数

    public:
        BSTree()
            : _root(nullptr)
        {}
    
  4. 拷贝构造函数

    public:
        BSTree(const BSTree<K, V>& t)
        {
            _root = Copy(t._root);
        }
    private:
        Node* Copy(Node* root)
        {
            if (root == nullptr)
                return nullptr;
            Node* newNode = new Node(root->_key, root->_value);
            newNode->_left = Copy(root->_left);
            newNode->_right = Copy(root->_right);
            return newNode;
        }
    
  5. 赋值运算符重载

    public:
        BSTree<K, V>& operator=(BSTree<K, V> t)
        {
            swap(_root, t._root);
            return *this;
        }
    
  6. 析构函数

    public:
        ~BSTree()
        {
            Destroy(_root);
        }
    private:
        void Destroy(Node*& root)
        {
            if (root == nullptr)
                return;
            Destroy(root->_left);
            Destroy(root->_right);
            delete root;
            root = nullptr;
        }
    
  7. 搜素操作

    //非递归搜素
    puclic:
        Node* Find(const K& key)
        {
            Node* cur = _root;
            while (cur)
            {
                if (cur->_key > key)
                    cur = cur->_left;
                else if (cur->_key < key)
                    cur = cur->_right;
                else
                    return cur;
            }
            return nullptr;
        }
    
    
    //递归搜索
    puclic:
        Node* FindR(const K& key)
        {
            return _FindR(_root, key);
        }
    private:
        Node* _FindR(Node* root, const K& key)
        {
            if (root == nullptr)
                return nullptr;
            if (root->_key > key)
                return _FindR(root->_left, key);
            else if (root->_key < key)
                return _FindR(root->_right, key);
            else
                return root;
        }
    
  8. 插入操作

    //非递归插入
    public:
    	bool Insert(const K& key, const V& val)
        {
            if (_root == nullptr)
            {
                _root = new Node(key, val);
                return true;
            }
    
            Node* parent = nullptr;
            Node* cur = _root;
            while (cur)
            {
                if (cur->_key > key)
                {
                    parent = cur;
                    cur = cur->_left;
                }
                else if (cur->_key < key)
                {
                    parent = cur;
                    cur = cur->_right;
                }
                else
                    return false;
            }
    
            cur = new Node(key, val);
            if (parent->_key > key)
                parent->_left = cur;
            else
                parent->_right = cur;
    
            return true;
        }
    
    
    //递归插入
    public:
        bool InsertR(const K& key, const V& val) 
        {
            return _InsertR(_root, key, val);
        }
    private:
        bool _InsertR(Node*& root, const K& key, const K& val)
        {
            if (root == nullptr)
            {
                root = new Node(key, val);
                return true;
            }
            if (root->_key < key)
                return _InsertR(root->_right, key, val);
            else if (root->_key > key)
                return _InsertR(root->_left, key, val);
            else
                return false;
        }
    
  9. 删除操作

    //非递归删除
    public:
        bool Erase(const K& key)
        {
            Node* cur = _root;
            Node* parent = nullptr;
            while (cur)
            {
                if (cur->_key > key)
                {
                    parent = cur;
                    cur = cur->_left;
                }
                else if (cur->_key < key)
                {
                    parent = cur;
                    cur = cur->_right;
                }
                else
                {
                    if (cur->_left == nullptr)
                    {
                        if (cur == _root)
                            _root = cur->_right;
                        else
                        {
                            if (parent->_left == cur)
                                parent->_left = cur->_right;
                            else
                                parent->_right = cur->_right;
                        }
                        delete cur;
                    }
                    else if (cur->_right == nullptr)
                    {
                        if (cur == _root)
                            _root = cur->_left;
                        else
                        {
                            if (parent->_left == cur)
                                parent->_left = cur->_left;
                            else
                                parent->_left = cur->_right;
                        }
                        delete cur;
                    }
                    else
                    {
                        Node* parent = cur;
                        Node* minRight = cur->_right;
                        while (minRight->_left)
                        {
                            parent = minRight;
                            minRight = minRight->_left;
                        }
                        cur->_key = minRight->_key;
                        if (minRight == parent->_left)
                            parent->_left = minRight->_right;
                        else
                            parent->_right = minRight->_right;
                        delete minRight;
                    }
    
                    return true;
                }
            }
            return false;
        }
    
    
    //递归删除
    public:
        bool EraseR(const K& key)
        {
            return _EraseR(_root, key);
        }
    private:
    	bool _EraseR(Node*& root, const K& key)
        {
            if (root == nullptr)
                return false;
            if (root->_key > key)
                return _EraseR(root->_left, key);
            else if (root->_key < key)
                return _EraseR(root->_right, key);
            else
            {
                Node* del = root;
                if (root->_left == nullptr)
                    root = root->_right;
                else if (root->_right == nullptr)
                    root = root->_left;
                else
                {
                    Node* minRight = root->_right;
                    while (minRight->_left)
                        minRight = minRight->_left;
                    swap(root->_key, minRight->_key);
                    return _eraseR(root->_right, key);
                }
                delete del;
                return true;
            }
        }
    

二叉搜索树的效率

二叉搜索树的查找插入或删除的效率取决于找到目标节点的效率,

而二叉树要找到目标节点只需要遍历一条路径即可,

最坏的情况就是遍历最长的一条路径。

所以二叉搜索树的效率就取决于最长路径。

如果搜索树始终始终是满二叉树的形态,那么查找效率就来到了O(logN)

此时效率是最高的。

而如果搜索树来到了只有一条路径的形态,那查找效率就退化到了O(N)

甚至对于同一组数据按照不同顺序插入都可能有不同结果:

image-20230527181257424

所以保持搜索二叉树高性能的关键就在于如何保持二叉树的左右平衡,

这个问题就交给名声在外的AVL树红黑树解决。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/876460.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【SpringBoot】| 接口架构风格—RESTful

目录 一&#xff1a;接口架构风格—RESTful 1. 认识RESTful 2. RESTful 的注解 一&#xff1a;接口架构风格—RESTful 1. 认识RESTful &#xff08;1&#xff09;接口 ①接口&#xff1a; API&#xff08;Application Programming Interface&#xff0c;应用程序接口&…

NLP文本匹配任务Text Matching [有监督训练]:PointWise(单塔)、DSSM(双塔)、Sentence BERT(双塔)项目实践

NLP文本匹配任务Text Matching [有监督训练]&#xff1a;PointWise&#xff08;单塔&#xff09;、DSSM&#xff08;双塔&#xff09;、Sentence BERT&#xff08;双塔&#xff09;项目实践 0 背景介绍以及相关概念 本项目对3种常用的文本匹配的方法进行实现&#xff1a;Poin…

物联网和不断发展的ITSM

物联网将改变社会&#xff0c;整个技术行业关于对机器连接都通过嵌入式传感器、软件和收集和交换数据的电子设备每天都在更新中。Gartner 预测&#xff0c;全球将有4亿台互联设备投入使用。 无论企业采用物联网的速度如何&#xff0c;连接设备都将成为新常态&#xff0c;IT服务…

iVX引领自动编程新时代:从百万应用到普适AST转换的技术突破

一、引言 在人工智能和自动编程的交汇点上&#xff0c;iVX以其独特的自动编程训练模型&#xff0c;正引领着新一轮的技术革命。通过百万个通过iVX IDE生成的应用进行有监督训练&#xff0c;iVX成功地将任意网站或网页转成了iVX IDE中的AST&#xff0c;展示了其强大的技术实力。…

浅谈GIS和三维GIS的区别?

GIS&#xff08;地理信息系统&#xff09;和三维GIS&#xff08;3D地理信息系统&#xff09;是地理信息领域的两个重要概念&#xff0c;它们在地理数据的处理和分析方面具有不同的特点和应用。可能很多人分不清二者的区别&#xff0c;本文就带大家简单了解一下二者的区别。 定义…

【闲侃历史】 唐朝----安史之乱那些事(1)

说到安史之乱&#xff0c;可谓是唐朝最乱的一段时期&#xff0c;据说唐朝当时也就5000多万人&#xff0c;而经历了这一战&#xff0c;人口只剩1000多万人了。著名的杨国忠和杨贵妃也是在这个时候死的。这个系列我们就先来侃侃发起安史之乱的两个人----安禄山和史思明 一. 安禄…

免费插件集-illustrator插件-Ai插件-路径编辑-统一线宽

文章目录 1.介绍2.安装3.通过窗口>扩展>知了插件4.功能解释5.示例6.总结 1.介绍 本文介绍一款免费插件&#xff0c;加强illustrator使用人员工作效率&#xff0c;统一路径线宽。首先从下载网址下载这款插件 https://download.csdn.net/download/m0_67316550/87890501&am…

2023年05月 C/C++(一级)真题解析#中国电子学会#全国青少年软件编程等级考试

第1题&#xff1a;输出第二个整数 输入三个整数&#xff0c;把第二个输入的整数输出。 时间限制&#xff1a;1000 内存限制&#xff1a;65536 输入 只有一行&#xff0c;共三个整数&#xff0c;整数之间由一个空格分隔。整数是32位有符号整数。 输出 只有一行&#xff0c;一个整…

支持M1 Syncovery for mac 文件备份同步工具

Syncovery for Mac 是一款功能强大、易于使用的文件备份和同步软件&#xff0c;适用于需要备份和同步数据的个人用户和企业用户。Syncovery 提供了一个直观的用户界面&#xff0c;使用户可以轻松设置备份和同步任务。用户可以选择备份的文件类型、备份目录、备份频率等&#xf…

【python实现向日葵控制软件功能】手机远程控制电脑

大家好&#xff0c;我是csdn的博主&#xff1a;lqj_本人 这是我的个人博客主页&#xff1a; lqj_本人_python人工智能视觉&#xff08;opencv&#xff09;从入门到实战,前端,微信小程序-CSDN博客 最新的uniapp毕业设计专栏也放在下方了&#xff1a; https://blog.csdn.net/lbcy…

Redis数据结构——Redis简单动态字符串SDS

定义 众所周知&#xff0c;Redis是由C语言写的。 对于字符串类型的数据存储&#xff0c;Redis并没有直接使用C语言中的字符串。 而是自己构建了一个结构体&#xff0c;叫做“简单动态字符串”&#xff0c;简称SDS&#xff0c;比C语言中的字符串更加灵活。 SDS的结构体是这样的…

vue + less 实现动态主题换肤功能

文章目录 前言一、前提条件1. 初始化vue项目2. 安装插件 二、新建文件夹主题theme1.style.less文件2.model.js文件3.theme.js文件theme文件夹最终效果 三、修改vue.config.js文件四、页面上的具体使用1. index.vue 页面2. index.vue 页面注意点说明3. index.vue 效果 五、在js中…

学会这一招,轻松玩转小程序自动化

jmeter 可以做性能测试&#xff0c;这个很多人都知道&#xff0c;那你知道&#xff0c;jmeter 可以在启动运行时&#xff0c;指定线程数和运行时间&#xff0c;自定义性能场景吗&#xff1f; jmeter 性能测试&#xff0c;动态设定性能场景 平时&#xff0c;我们使用 jmeter 进…

《图解HTTP》——HTTP协议详解

一、HTTP协议概述 HTTP是一个属于应用层的面向对象协议&#xff0c;由于其简捷、快速的方式&#xff0c;适用于分布式超媒体信息系统。它于1990年提出&#xff0c;经过几年的使用与发展&#xff0c;得到不断地完善和扩展。目前在WWW中使用的是HTTP/1.0的第六版&#xff0c;HTTP…

TiDB Bot:用 Generative AI 构建企业专属的用户助手机器人

本文介绍了 PingCAP 是如何用 Generative AI 构建一个使用企业专属知识库的用户助手机器人。除了使用业界常用的基于知识库的回答方法外&#xff0c;还尝试使用模型在 few shot 方法下判断毒性。 最终&#xff0c;该机器人在用户使用后&#xff0c;点踩的比例低于 5%&#xff0…

安科瑞电力监控系统在某区块页岩气地面集输工程中的应用

摘要&#xff1a;Acrel-2000Z电力监控系统适用于35kV及以下电压等级的各类变电站&#xff0c;可以帮助用户掌握配电系统实时运行状态&#xff0c; 获取预警、告警等各类事件&#xff0c;实现区域的无人值守&#xff0c;提高监管水平。本文介绍了安科瑞电力监控系统Acrel-2000在…

elasticsearch-head 插件

1、elastic 插件说明 **Head** 是第三方提供的一款很优秀的插件&#xff0c;集监控、查询、配置一体的web功能系统&#xff0c;可以在系统中进行创建、删除索引 、文档。以及查询、配置索引等功能&#xff0c;深受广大开发者的喜爱 **Kopf** 是另一个第三方提供的一款很优秀…

【Java】2022 RoboCom 机器人开发者大赛-高职组(省赛)题解

7-15 您好呀 本届比赛的主题是“智能照护”&#xff0c;那么就请你首先为智能照护机器人写一个最简单的问候程序 —— 无论遇见谁&#xff0c;首先说一句“您好呀~”。 输入格式&#xff1a; 本题没有输入 输出格式&#xff1a; 在一行中输出问候语的汉语拼音 Nin Hao Ya ~…

《局外人》阅读笔记

《局外人》阅读笔记 2023年8月14日在杭州小屋读完。我看的是张雨彤编译的这本&#xff0c;这本书包含了两部分&#xff0c;第一部分是《局外人》是原版的小说故事&#xff0c;第二部分是《堕落》包括了六天内的自然自语&#xff0c;完全没看懂&#xff0c;写作风格突变&#xf…

Python爬虫IP代理池的建立和使用

写在前面 建立Python爬虫IP代理池可以提高爬虫的稳定性和效率&#xff0c;可以有效避免IP被封锁或限制访问等问题。 下面是建立Python爬虫IP代理池的详细步骤和代码实现&#xff1a; 1. 获取代理IP 我们可以从一些代理IP网站上获取免费或付费的代理IP&#xff0c;或者自己租…