美文网首页
音视频开发之旅(27) 算法序列 - 二叉查找树

音视频开发之旅(27) 算法序列 - 二叉查找树

作者: yabin小站 | 来源:发表于2021-01-30 18:53 被阅读0次

    目录

    1. 常见的查找数据结构和算法介绍
    2. 二叉查找树
    3. 资料
    4. 收获

    一、常见的查找数据结构和算法介绍

    1.1 链表(顺序查找)

    针对少量的、无规则的数据,可以采用链表进行顺序查找
    从头到尾依次逐个查找,直到找到所要的数据或搜索完整个数据序列。时间复杂度是O(n)
    它的优点是插入比较快,但是查找比较慢。

    1.2 有序数组(二分查找)

    针对有序数组,可以采用二分查找法(折半查找法)
    基本原理:
    首先讲要查找的元素月数组的中间元素比较
    定义三个遍历,最小值,最大值,中间值,其中中间值=(最小值+最大值)/2
    如果key小于中间值,把最大值的下标移动到中间值的前一个
    如果key大于中间值,把最小值的下标移动到中间值的后一个。
    如果key和中间元素相等,匹配成功,结束查找。

    一般情况下二分查找比顺序查找快很多,是很多实际应用中经常被采用。
    有序数组的二分查找法的不足:插入操作非常慢,需要后插入位置后的都要向后移动一位。

    1.3 二叉查找树

    那么是否有同时保证查找、插入、删除操作效率都比较高的算法和数据结构呐?
    要满足插入的高效性,首先能想到的是链表,但是链表无法使用 查找时高效的二分查找法。而二叉查找树将二分查找的效率和链表的灵活性结合起来。也是这篇我们学习实践的重点。

    还有对二叉树进行优化的平衡二叉查找树以及散列表,我们将在后面几篇进行学习实践。

    图片来源于《算法》

    二、二叉查找树(BST)

    通过上面一小节,我们了解到二叉查找树,结合了链表的灵活性以及数组的二分查找的高效性。这一小节,我们来了解二叉查找树数据结构,以及创建(插入)、查找(遍历)、删除过程。

    先来看下二叉查找树的数据结构
    二叉查找树使用的数据结构由结点组成,每个结点只能有一个父结点(跟结点除外),每个结点包含两个链接(也可以为空)分别指向左子结点和右子结点。

    我们先来定义二叉查找树一个结点的结构体

    template<class T>
    struct BSTNode
    {
        BSTNode(const T& key = T())
            : _left(nullptr)
            , _right(nullptr)
            , _key(key)
        {}
        
        BSTNode<T>* _left;
        BSTNode<T>* _right;
        T _key;
    };
    

    2.1 插入(创建)

    二叉查找树采用二叉树的中序遍历的方式进行创建插入。
    如果树是空的,就返回一个包含该键值对的新结点,称为根结点。
    如果被查找的键小于根结点的键,在左子树中插入该键,否则在右子树中插入。

    具体实现如下

    bool insertNode(const T& key)
        {
            //如果树为空,直接创建一个新结点进行插入
            if (_root == nullptr)
            {
                _root = new Node(key);
                return true;
            }
            //查找要插入的位置,从根结点作为比较的起始点
            Node* cur = _root;
            Node* parent = nullptr;
           //通过下面这个while循环找到要插入的位置。也可以使用递归函数实现
            while (cur)
            {
                //通过parent记录要出入结点的位置
                parent = cur;
                //要插入的值比当前结点值小,在其左子结点树上继续查找
                if (key < cur->_key)
                {
                    cur = cur->_left;
                }
               //否则,在其右子结点树上继续查找
                else if (key >= cur->_key)
                {
                    cur = cur->_right;
                }
                
            }
    
            //插入元素
           //生成新的结点
            cur = new Node(key);
            //和父节点进行比较,决定插入在左结点还是右结点
            if (key < parent->_key)
            {
                parent->_left = cur;
            }
            else
            {
                parent->_right = cur;
            }
            return true;
        }
    

    2.2 查找(遍历)

    从根结点开始查找,

    具体实现如下

        Node* find(const T& key)
        {
            Node* cur = _root;
            //也可以改为递归的方式
           //运用了高效的二分查找
            while (cur)
            {
                if (cur->_key == key)
                {
                    return cur;
                }
                else if (key < cur->_key)
                {
                    cur = cur->_left;
                }
                else
                {
                    cur = cur->_right;
                }
            }
            return nullptr;
        }
    

    2.3 删除

    删除结点分为以下三种情况

    1. 删除叶子结点、
    2. 删除有一个子树的结点;
    3. 删除有两个子树的结点。

    这个比较复杂些。我们先把流程搞清楚,然后在通过代码实现

    删除叶子结点

    1. 找到要删除的叶子结点 targetNode
    2. 找到targetNode的父节点parentNode
    3. 确定targetNode是parentNode的左子结点还是右子结点
    4. 进行删除 左子结点: parentNode.left = nullptr; 右子节点 parentNode.right = nullptr;

    删除有一个子树的结点
    这种情况要考虑,该结点的子结点是左子结点还是右子结点。以及该结点是其父节点的左子结点还是右子结点。具体步骤如下:

    1. 找到要删除的叶子结点 targetNode (同删除叶子结点第一步

    2. 找到targetNode的父节点parentNode (同删除叶子结点第一步

    3. 确定targetNode的子结点是左子结点还是右子结点

    4. 确定targetNode的是其parentNode结点的左子结点还是右子结点

    5. 如果targetNode的子结点是左子结点
      如果targetNode是其父节点parentNode的左子结点
      parentNode.left = targetNode.left

      如果targetNode是其父结点parentNode的右子结点
      parentNode.right = targetNode.left

    6. 如果targetNode的子结点是右子结点
      如果targetNode是其父节点parentNode的左子结点
      parentNode.left = targetNode.right

      如果targetNode是其父结点parentNode的右子结点
      parentNode.right = targetNode.right

    删除有两个子树的结点
    删除拥有两个子树的结点后,左子树和右子树又该如何和原结点建立关系呐? 具体步骤如下

    1. 找到要删除的叶子结点 targetNode (同删除叶子结点第一步
    2. 找到targetNode的父节点parentNode (同删除叶子结点第一步
    3. 从tartgetNode的右子树中找到最小的结点(或者从targetNode的左子树中找到最大的结点)
    4. 用一个临时变量tmp存储该上一步找到结点的值
    5. 删除第3步找到的结点
    6. 把targetNode.key = tmp,即不改变链表的指向,只需要改变当前要删除结点的值即可。

    梳理清楚上面的步骤后,我们看下通过代码如何实现结点的删除

    bool delete(const T& key)
        {
            //如果树为空,删除失败
            if (_root == nullptr)
            {
                return false;
            }
            //查找key在树中的位置 
            //targetNode的位置以及parentNode的位置
            Node* cur = _root;
            Node* parent = nullptr;
            while (cur)
            {
                if (key == cur->_key)
                {
                    break;
                }
                else if (key < cur->_key)
                {
                    parent = cur;
                    cur = cur->_left;
                }
                else
                {
                    parent = cur;
                    cur = cur->_right;
                }
            }
            
            //遍历了整棵树,如果key不在树中,无法删除
            if (cur == nullptr)
            {
                return false;
            }
    
            //如果在树中找到了key,进行删除结点,要分三种情况:
            //1.该结点是叶子结点
            //2.该结点只有左子结点或者右子结点
            //3.该结点左右子树都存在
    
             //情况1
            if (cur->_left == nullptr &&  cur->_right == nullptr)
            {
                if(cur == parent->_left)
                {
                    parent->_left = nullptr;
                }else
                {
                    parent->_right = nullptr;
                }
    
            }
            //情况2 
        
            else if (cur->_left == nullptr)
            {
                //情况2.1:
                //只有根结点和根的右孩子,此时要删除的结点正好是树的根
                if (cur == _root)
                {
                    _root = cur->_right;
                }
                else
                {
                    //或该结点不是树的根
                    if (cur == parent->_left)
                    {
                        parent->_left = cur->_right;
                    }
                    else
                    {
                        parent->_right = cur->_right;
                    }
                }
            }
            //情况2.2:
            else if (cur->_right == nullptr)
            {
                if (cur == _root)
                {
                    _root = cur->_left;
                }
                else
                {
                    if (cur == parent->_right)
                    {
                        parent->_right = cur->_left;
                    }
                    else
                    {
                        parent->_left = cur->_left;
                    }
                }
     
            }
            else
            {
                //当前结点左右孩子都存在,直接删除不好删,可以在其子树中找一个替代结点,比如找其左子树中的最大结点,即左子树中最右侧的结点,或者找其右子树中最小的结点,即右子树中最小的结点。替换结点找到后,将替代结点中的值交给待删除结点,转换成删除替代结点。
                    if (cur->_left != nullptr || cur->_right != nullptr)
                    {
                        //找右子树中最小的结点替换待删除的结点
                        Node* repalce = cur->_right;
                        parent = cur;
                        while (repalce->_left)
                        {
                            parent = repalce;
                            repalce = repalce->_left;
                        }
                        cur->_key = repalce->_key;
                        if (repalce == parent->_left)
                        {
                            parent->_left = repalce->_right;
                        }
                        else
                        {
                            parent->_right = repalce->_right;
                        }
                        delete repalce;
                        repalce = nullptr;
                    }
                    return true;
            }
            return false;
        }
    

    五、资料

    《算法》
    [【老九学堂】二分查找法] : https://www.bilibili.com/video/BV1LJ411X76n?from=search&seid=16165089659396424420
    [【C语言描述】《数据结构和算法》(小甲鱼)-二叉排序树] : https://www.bilibili.com/video/BV1jW411K7yg?p=76
    [尚硅谷Java数据结构与java算法(Java数据结构与算法)] : https://www.bilibili.com/video/BV1E4411H73v?p=132
    [二叉搜索树的实现(C++)] : https://blog.csdn.net/tangya3158613488/article/details/89390232

    六、 收获

    1. 了解查找算法几种方式(无序链表顺序查找、有序数组二分查找、二分查找树、平衡二叉查找树、散列表)
    2. 学习实践了二分查找树的新增结点、查找结点、以及删除结点的逻辑和实现。

    感谢你的阅读
    下一篇我们继续学习实践平衡二叉查找树,欢迎关注公众号“音视频开发之旅”,一起学习成长。
    欢迎交流

    相关文章

      网友评论

          本文标题:音视频开发之旅(27) 算法序列 - 二叉查找树

          本文链接:https://www.haomeiwen.com/subject/kjsktltx.html