搜索二叉树_SearchBinaryTree

news2024/9/20 12:42:00

目录

搜索二叉树的原理

搜索二叉树的搜索时间复杂度

二叉搜索树实现_key 模型

节点

构造函数

查找

中序遍历

插入

循环

递归

删除

循环

1.删除叶子节点

2.删除有一个孩子的节点

3.左右孩子都不为空

递归

析构函数

拷贝构造

operator=

key_value 模型

节点

查找

插入


搜索二叉树的原理

搜索二叉树是通过根节点大于左子树节点,小于右子树节点来完成快速查找的。

搜索二叉树的搜索时间复杂度

搜索二叉树的时间复杂度是O(logN) ~ O(N)的。

如果是上面这样那么查找的时间复杂度就接近于 logN, 但是二叉搜索树不一定为满二叉树,或者接近满二叉树,所以如果二叉搜索树是下面这个样子,甚至退化为列表,那么查找的时间复杂度就是 N 了

 

二叉搜索树实现_key 模型

节点

二叉搜索树的节点可以保存它的左孩子,还有右孩子,以及就是它里面存储的值,并且二叉搜索树还需要可以存储任意类型的值,所以需要是模板类型。

    template<class K>
    struct BSTNode
    {
        BSTNode<K>* _left;
        BSTNode<K>* _right;
        K _key;
​
        BSTNode(K key)
            :_left(nullptr)
            , _right(nullptr)
            , _key(key)
        {}
    };
​
    template<class K>
    class BSTree
    {
     public:
        typedef BSTNode<K> Node;
     private:
        Node*  _root;
    };

上面就是二叉搜索树的成员变量。

构造函数

构造函数我们只需要将里面的 _root 成员变量初始化为空即可。

        BSTree()
            :_root(nullptr)
        {}

查找

对于搜索二叉树而言,它的根小于右子树节点,大于左子树节点,所以当我们要查找一个值是否存在的时候,只需要判断大小,如果大于根节点,那么就到右子树查找,小于根节点就去左子树查找,如若查找到空,那么就是没有查找到。

        bool find(Node* root, const K& key)
        {
            while (root)
            {
                if (root->_key < key)
                    root = root->_right;
                else if (root->_key > key)
                    root = root->_left;
                else
                    return true;
            }
            return false;
        }

上面就是我们的 find 函数,但是我们还是需要多一个根节点,但是我们在外面有无法访问根节点,那怎么办呢?

  • 解决方案1:写一个 getRoot() 函数

  • 封装

下面我们就使用封装来完成。

        bool find(const K& key)
        {
            return _find(_root, key);
        }
        
        bool _find(Node* root, const K& key)
        {
            while (root)
            {
                if (root->_key < key)
                    root = root->_right;
                else if (root->_key > key)
                    root = root->_left;
                else
                    return true;
            }
            return false;
        }

这里我们建议把子函数进行私有,子函数不对外公布。

所以我们用封装来解决类似的问题还是一个很好的办法,我们下面遇到的这种情况就是直接使用封装了。

中序遍历

中序遍历就是对一颗二叉树进行先访问左子树,在访问根节点,最后访问右子树,对于搜索二叉树而言,中序遍历就是排序,因为我们的左子树小于根节点,根节点小于右子树,所以对搜索二叉树进行中序遍历那么就是排序。

遍历也是需要传入根节点的,所以这里还是使用上面的套路(封装)。

        void InOrder()
        {
            _InOrder(_root);
            cout << endl;
        }
​
        void _InOrder(Node* root)
        {
            if (root == nullptr) return;
​
            _InOrder(root->_left);
            cout << root->_key << " ";
            _InOrder(root->_right);
        }

插入

插入,我们需要在插入后还要保证是我们的树是搜索二叉树,所以插入也是需要遵守根大于左子树,小于右子树的规则,所以这里就是需要查找,找到空为止,如果找到相同的值,那么就不能继续插入。

循环

我们这里先写一个循环版本的,下面还会有一个递归版本的,该循环版本就是我们需要查找到插入位置,我们循环遍历,当然在查找过程中我们还需要记录父节点,但是这里也是需要注意,如果根节点就是空,那么就是直接插入即可,在循环查找空的时候如果插入元素大于根那么就去右边插入,小于就去左边,直到找到空为止,此时也找到了空位置的父亲节点,然后new 一个节点连接起来,这里我们还是需要判断大小的,因为这里并不知道是连接到父亲节点的左子树还是右子树,所以还是需要判断的。

        bool insert(const K& key)
        {
            if (_root == nullptr)
            {
                _root = new Node(key);
            }
            else
            {
                // 查找插入位置
                Node* parent = nullptr;
                Node* cur = _root;
                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 = new Node(key);
                if (key < parent->_key)
                    parent->_left = cur;
                else
                    parent->_right = cur;
            }
​
            return true;
        }

递归

递归,我们这里使用的是封装的写法,因为这里需要递归所以我们需要找到插入的位置,这里插入是空就插入,这里我们是直接修改指针的,为什么可以直接修改指针呢,并且不考录父亲节点的连接呢?因为我们这里使用引用,这里我们如果直接就是空(根节点就是空,所以此时我们的 root 变量就是根节点的引用,我们修改root,也就是直接修改了根节点的指向),那么我们这里的引用其实在同一层是没有什么作用的,这里只有在下一层的时候才有作用(下一层就是该节点传下去的那一层),我们分析一下,如果这里我们插入的元素大于根节点,所以我们继续调用该函数,到下一层的时候我们的root节点就是上一层root节点的右子树的引用,所以我们修改root节点,也就是直接修改了上一层root节点的右子树指向,所以这里的引用在当前层没有发挥作用,如果插入的节点小于根节点,那么就是调用该函数去左子树进行插入,而这一层的root节点也就是上一层root节点的左子树的引用,所以修改该层的root节点也就是修改了上一层左子树的节点,直到查找到nullptr为止,然后进行插入,如果并未查找到 nullptr 节点那么就插入失败了。

        bool insertR(const K& key)
        {
            return _insertR(_root, key);
        }
        
        bool _insertR(Node*& root, const K& 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;
        }

删除

删除和插入一样,同样有循环和递归两个版本。

循环

循环删除我们同样是要先找要删除的节点,如果找到了才可恶意进行删除,如果查找为空,表示删除失败,要想删除一个点,我们同样需要记录删除节点的父亲节点。

对于删除而言,我们删除的位置不同,难易程度也是不同的。

1.删除叶子节点

如果我们这里删除的是节点 7, 那么我们这里就可以直接删除,然后将节点 8 的左子树置为空,所以删除叶子节点直接删除,然后将父亲节点的与该节点连接的孩子节点置为空即可。

2.删除有一个孩子的节点

 

 如果这里删除的是只有一个节点,那么我们就可以删除掉该节点,然后将该节点的孩子节点连接到父亲节点上。

我们将节点 5 的孩子节点连接到节点3 上,所以删除有一个孩子的节点,我们可以将它的孩子节点给父亲,然后删除掉该节点。

3.左右孩子都不为空

左右孩子都不为空是最难删除的,我们下面看一下怎么删除。

节点 10 左右孩子都不为空,我们如果想删掉该节点,那么我们就需要找一个可以代替节点10 的孩子节点,那么根据二叉搜索树的规则,我们应该保持删掉后还要让该树的所有节点都是根大于左子树,小于右子树所以我们可以找到左子树的最大节点,或者是右子树的最小节点,在查找右子树的最小,或者左子树的最大的时候,我们还是需要记录替换节点的父亲节点,因为我们最后查找到替换后,就是删除替换后的节点,所以这里需要记录替换你节点的父亲节点,那么10节点的左子树的最大节点就是9,右子树的最小节点就是15,也就是左子树的最右节点,左子树的最右节点一定是左子树的最大节点,右子树的最小节点就是右子树的最左节点,右子树的最左节点也就一定是右子树的最小节点,找到代替节点后,我们就可以交换,然后删掉交换后的节点,但是交换后的节点不一定没有左右孩子,对于左子树的最右节点一定没有右孩子,但是不一定没有左孩子,所以如果右左孩子的话,还需要将该节点删除后,还要将该节点的左孩子给查到到父亲,如果是右子树的话,那么查找到的最左孩子一定没有左节点,但是不一定没有右节点,所以如果有右节点还是需要将右节点也该查到到的该节点的父亲。

这里删除节点10不好演示,删除节点3,看一下。

如果这里要删除节点3,那么先查找要删除的节点,然后我们查找到了节点 3 由于节点3左右孩子都不为空,所以我们需要找一个代替的节点,我们这里选择左子树的最大节点,我们查找到了1,由于节点5的父亲节点就是节点3,所以我们直接替换,然后我们将替换节点的左子树给删除节点的父亲节点,然后我们在删除掉删除节点。

 

此时找到删除节点,然后查找左孩子的最大值,和它的父亲节点。

 

查找到了,这里左孩子的最大节点就是 1 ,它的父亲就是cur节点,然后这里交换。

 

交换后,然后我们将 leftMax 的孩子节点给 leftMaxParent 进行管理,最后删除掉 leftMax。

 

 

这里演示完毕后我们开始看代码。

在写代码之前,我们先总结一下,我们如果删除的节点是叶子节点或者是左孩子为空或者是右孩子为空的节点,那么我们可以分为一类,也就是叶子节点可以认为是只有一个孩子节点,这样我们更方便处理,如果我们只有一个孩子节点,那么我们就可以直接让删除节点的父亲节点指向另外一个孩子节点。

也就是下面这样

    if(cur->left == nullptr) // cur 表示要删除的节点
    {
        // 左为空,让父亲指向自己的右
        if(cur == parent->left)
            parent->left = cur->right;
        else
            parent->right = cur->right;
    }
    else
    {
        // 右为空,让父亲指向自己的左
         if(cur == parent->left)
            parent->left = cur->left;
        else
            parent->right = cur->left;
    }

但是这里还是有一个需要注意的地方,那就是如果我们的根节点就是删除的点呢?所以这时候我们的parent就是空,并且我们的根节点没有左子树或者右子树,那么就会让parent解引用,然后就是对空进行解引用最后报错,也就是我们上面的那一段代码。

那么这里怎么办?我们可以进去后继续判断一下,cur == _root ,如果等于那么就只需要将 _root 变成它的左子树或者右子树,然后删除掉该节点。

剩下的我们基本都清楚了,下面就看代码。

        bool erase(const K& key)
        {
            Node* parent = nullptr;
            Node* cur = _root;
            // 查找
            while (cur)
            {
                if (key < cur->_key)
                {
                    parent = cur;
                    cur = cur->_left;
                }
                else if (key > cur->_key)
                {
                    parent = cur;
                    cur = cur->_right;
                }
                else
                {
                    // 找到了
                    if (cur->_left == nullptr)
                    {
                        // cur 的左子树为空
                        if (cur == _root)
                        {
                            // 说明此时parent 为空,并且此时该树还没有左子树
                            _root = cur->_right;
                        }
                        else
                        {
                            if (cur == parent->_left)
                            {
                                parent->_left = cur->_right;
                            }
                            else if (cur == parent->_right)
                            {
                                parent->_right = cur->_right;
                            }
                        }
                    }
                    else if (cur->_right == nullptr)
                    {
                        // cur 的右子树为空
                        if (cur == _root)
                        {
                            // 说明此时parent 为空,并且此时该树还没有右子树
                            _root = cur->_left;
                        }
                        else
                        {
                            if (cur == parent->_left)
                            {
                                parent->_left = cur->_left;
                            }
                            else if (cur == parent->_right)
                            {
                                parent->_right = cur->_left;
                            }
                        }
                    }
                    else
                    {
                        // cur 的左右子树都不为空
                        Node* rightMinParent = cur;
                        Node* rightMin = cur->_right;
                        // 查找右子树的最左节点
                        while (rightMin->_left)
                        {
                            rightMinParent = rightMin;
                            rightMin = rightMin->_left;
                        }
                        swap(cur->_key, rightMin->_key);
                        if (rightMin == rightMinParent->_left)
                        {
                            rightMinParent->_left = rightMin->_right;
                        }
                        else
                        {
                            rightMinParent->_right = rightMin->_right;
                        }
                        cur = rightMin;
                    }
​
                    delete cur;
                    return true;
                }
            }
            // 没找到
            return false;
        }

递归

递归删除就是我们传入根节点,然后我们在传入删除节点,如果该节点就是删除的节点,那么就在判断该节点的孩子是只有一个还是左右孩子节点都不为空,如果是只有一个孩子节点或者为叶子节点的话,那么就直接删除,如果既有左孩子也有右孩子,那么就是查找左子树的最大,或者右子树的最小,找到后交换,然后递归去删除。

这里也是用引用,这样就可以不用记录父亲节点了。

        bool eraseR(const K& key)
        {
            return _eraseR(_root, key);
        }
​
        bool _eraseR(Node*& root, const K& key)
        {
            if (root == nullptr)
                return false;
​
            if (root->_key < key)
                _eraseR(root->_right, key);
            else if (root->_key > key)
                _eraseR(root->_left, key);
            else
            {
                Node* del = root;
                // 相等
                if (root->_left == nullptr) // 左边为空,将右边给父亲
                    root = root->_right;
                else if (root->_right == nullptr) // 右边为空,将左边给父亲
                    root = root->_left;
                else
                {
                    // 两边都不为空
                    Node* rightMin = root->_right;
                    while (rightMin->_right) rightMin = rightMin->_right; // 查找有边的最小值
                    swap(rightMin->_key, root->_key);
                    // 递归删除root->_right 
                    return _eraseR(root->_right, key);
                }
                delete del;
            }
        }

析构函数

析构函数我们可以后续遍历删除。我们写一个 destroy 函数去后续遍历删除。

        ~BSTree()
        {
            destroy(_root);
        }
​
        void destroy(Node*& root)
        {
            if (root == nullptr) return;
​
            destroy(root->_left);
            destroy(root->_right);
            delete root;
            root = nullptr;
        }

拷贝构造

拷贝构造我们也可以递归的去构造该函数。这里看代码可以直接明白。

        BSTree(const BSTree& BST)
        {
            _root = copy(BST._root);
        }
​
        Node* copy(Node* root)
        {
            if (root == nullptr)
                return nullptr;
​
            Node* copyNode = new Node(root->_key);
            copyNode->_left = copy(root->_left);
            copyNode->_right = copy(root->_right);
            return copyNode;
        }

operator=

赋值重载,我们可以使用现代写法也是很简单。

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

key 模型就到这里,下面我们看 key_value 模型

key_value 模型

其实 key 模型改 key_value 模型还是比较简单的,我们只需要加一个一个 value 就可以了,我们让 key 与 value 映射起来。

节点

key_value 模型与key 的差别就是多了一个 value 所以我们在写的时候模板还需要多加一个 value,我们使用 key 进行查找等,然后与 value 进行映射。

namespace key_value
{
    template<class K, class V>
    struct BSTNode
    {
        BSTNode<K, V>* _left;
        BSTNode<K, V>* _right;
        K _key;
        V _value;
​
        BSTNode(K key, V value)
            :_left(nullptr)
            , _right(nullptr)
            , _key(key)
            ,_value(value)
        {}
    };
    
    tempalte<class K, class V>
    class BSTree
    {
        public:
        typedef BSTNode<k, V> Node;
        private:
        Node* _root;
    };
}

查找

这里的查找基本还是没有变化的,我们知识返回值不同,如果是 key_value 模型的话,那么我们是需要返回查找的节点的指针的,所以我们的 find 函数只是返回值不同。

这里的 find 是用递归写的,上面也可以用递归只是这个 find 太简单了,就没有用递归是实现。

        Node* findR(const K& key)
        {
            return _findR(_root, key);
        }
​
        Node* _findR(Node* root, const K& key)
        {
            if (root == nullptr)
                return root;
​
            if (root->_key < key)
                return _findR(root->_right, key);
            else if (root->_key > key)
                return _findR(root->_left, key);
            else
                return root;
        }

插入

对于 key_value 模型来说,我们只有这么三个函数是不一样的(构造函数就不说了),我们 key_value 模型的插入也就是比 key 模型多了一个插入 value,所以这个并不难。

        bool insertR(const K& key, const V& value)
        {
            return _insertR(_root, key, value);
        }
​
        bool _insertR(Node*& root, const K& key, const V& value)
        {
            if (root == nullptr)
            {
                root = new Node(key, value);
                return true;
            }
​
            if (root->_key < key)
                return _insertR(root->_right, key, value);
            else if (root->_key > key)
                return _insertR(root->_left, key, value);
            else
                return false;
        }

上面就是我们今天要讲的,二叉搜索树的,key 模型 和 key_value 模型。

下次再见~

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

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

相关文章

JDBC-笔记

JDBC 1. JDBC介绍 JDBC&#xff08;Java Database Connectivity&#xff09;是一种用于连接和操作数据库的 Java API。 通过Java操作数据库的流程 第一步&#xff1a;编写Java代码 第二步&#xff1a;Java代码将SQL发送到MySQL服务端 第三步&#xff1a;MySQL服务端接收到SQ…

ems

【python爬虫】邮政包裹物流查询 目标网站 ems 邮政快递包裹查询: https://www.ems.com.cn/ 截图 接口预览 getPic请求滑动验证码的背景图片和滑块图片&#xff0c;返回的是base64编码的图片 getLogisticsTestFlag发送验证码的验证信息 xpos为滑动的距离&#xff0c;本站没…

CUDA编译器环境配置篇

cuda教程目录 第一章 指针篇 第二章 CUDA原理篇 第三章 CUDA编译器环境配置篇 第四章 kernel函数基础篇 第五章 kernel索引(index)篇 第六章 kenel矩阵计算实战篇 第七章 kenel实战强化篇 第八章 CUDA内存应用与性能优化篇 第九章 CUDA原子(atomic)实战篇 第十章 CUDA流(strea…

CHI中的System Debug, Trace, and Monitoring

Data Source indication □ Read request的completer&#xff0c;可以在CompData, DataSepResp, SnpRespData, and SnpRespDataPtl response中的datasource域段中指定data的来源&#xff1b;即使响应中带有错误&#xff0c;该datasource也是有效的&#xff1b; □ 该域段也可复…

Flutter 之Bloc入门指南实现倒计时功能

Flutter Timer By Bloc 前言Stream.periodic实现倒计时定义Bloc状态定义Bloc事件定义Bloc组件定义View层参考资料前言 使用Bloc开发Flutter的项目,其项目结构都很简单明确,需要创建状态,创建事件,创建bloc,创建对应的View。flutter_timer项目来分析下Bloc的使用方法。 通…

逻辑回归变量系数可为负数吗?应该如何解释?

之前很多学员来问逻辑回归变量系数是否都应该为正数&#xff0c;如果出现负的变量系数该怎么办&#xff1f;是否需要重新建模&#xff1f;这些学员都是在网上搜索时&#xff0c;被错误信息误导。网上信息可以随意转载&#xff0c;且无人审核对错。我见过最多情况时很多文章正确…

软工导论知识框架(三)结构化的设计

一.传统软件工程方法学采用结构化设计技术&#xff08;SD&#xff09; 从工程管理角度结构化设计分两步&#xff1a; 概要设计&#xff1a; 将软件需求转化为数据结构和软件系统结构。详细设计&#xff1a;过程设计&#xff0c;通过对结构细化&#xff0c;得到软件详细数据结构…

dubbo-helloworld示例

1、工程架构 2、创建模块 &#xff08;1&#xff09;创建父工程,引入公共依赖 pom.xml依赖 <dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></depende…

MultipartFile 获取文件名、文件前缀、文件后缀、文件类型

测试 debug 方法 RequestMapping(value "/test",method RequestMethod.POST)public void fileUpload(MultipartFile file){// 文件名String originalFilename file.getOriginalFilename();// 文件名前缀String fileName file.getOriginalFilename().substring(0,…

第5讲:VBA中OFFSET函数的利用

【分享成果&#xff0c;随喜正能量】幸福从来不是跟别人比来的&#xff0c;追求不同&#xff0c;各有活法&#xff0c;开心了就笑&#xff0c;累了就休息&#xff0c;日子安稳踏实就是最大的幸福。做人就怕尊严扫地&#xff0c;保留一点做人的尊严&#xff0c;是人生最大的本钱…

C语言每日一题

今天分享的是一道牛客网上面的题目&#xff0c;链接在下面 有序序列合并 这道题做法有很多&#xff0c;最简单的是合并一起&#xff0c;然后用排序就行了&#xff0c;今天将一个最高效的办法&#xff0c;思路是两个数组第一项进行比较&#xff0c;小的先输出&#xff0c;输出的…

Mac上命令

1. block端口&#xff1a; sudo cp /etc/pf.conf /etc/pf443.conf 编辑pf443.conf&#xff0c;vim /etc/pf443.conf&#xff0c;如 block on en0 proto udp from any to any port 9000 # block UDP port 9000 block on en0 proto tcp from any to any port 5004 # bloc…

InnoDB引擎底层逻辑讲解——后台线程

1.后台线程 后台线程的作用就是将innodb存储引擎缓冲池中的数据&#xff0c;在合适的时机刷新到磁盘文件当中。innodb存储引擎后台的线程主要分为四类&#xff1a;

Golang之路---02 基础语法——函数

函数 函数定义 func function_name( [parameter list] ) [return_types] {函数体 }参数解释&#xff1a; func&#xff1a;函数由 func 开始声明function_name&#xff1a;函数名称&#xff0c;函数名和参数列表一起构成了函数签名。[parameter list]&#xff1a;参数列表&a…

Istio 安全 mTLS认证 PeerAuthentication

这里定义了访问www.ck8s.com可以使用http也可以使用https访问&#xff0c;两种方式都可以访问。 那么是否可以强制使用mtls方式去访问&#xff1f; mTLS认证 PeerAuthentication PeerAuthentication的主要作用是别人在和网格里的pod进行通信的时候&#xff0c;是否要求mTLS mTL…

信息学奥赛一本通——1258:【例9.2】数字金字塔

文章目录 题目【题目描述】【输入】【输出】【输入样例】【输出样例】 AC代码 题目 【题目描述】 观察下面的数字金字塔。写一个程序查找从最高点到底部任意处结束的路径&#xff0c;使路径经过数字的和最大。每一步可以从当前点走到左下方的点也可以到达右下方的点。 在上面…

web前端开发工程师岗位的主要职责八篇

web前端开发工程师岗位的主要职责1 职责&#xff1a; 1、负责web前端系统和功能的开发、调试和维护&#xff0c;前端技术框架和js等互动效果开发; 2、负责公司现有项目和新项目的前端修改调试和开发工作; 3、根据工作安排高效、高质地完成代码编写&#xff0c;确保符合规范…

计算机视觉(六)图像分类

文章目录 常见的CNNAlexnet1乘1的卷积 VGG网络Googlenet&#xff08;Inception V1、V2、V3&#xff09;全局平均池化总结 Resnet、ResnextResNet残差网络ResNeXt网络 应用案例VGGResnet 常见的CNN Alexnet DNN深度学习革命的开始 沿着窗口进行归一化。 1乘1的卷积 VGG网络…

45.ubuntu Linux系统安装教程

目录 一、安装Vmware 二、Linux系统的安装 今天开始了新的学习&#xff0c;Linux,下面是今天学习的内容。 一、安装Vmware 这里是在 Vmware 虚拟机中安装 linux 系统&#xff0c;所以需要先安装 vmware 软件&#xff0c;然 后再安装 Linux 系统。 所需安装文件&#xff1a;…

Blueprint —— 蓝图通信

在使用蓝图时&#xff0c;如需在不同蓝图间传递或共享信息&#xff0c;此时就需要使用蓝图通信&#xff08;Blueprint Communication&#xff09;&#xff1b; 一&#xff0c;变量引用 创建变量&#xff0c;类型为指定对象的引用&#xff0c;默认值设置为指定对象的实例&#x…