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

目录

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

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

1.1 链表(顺序查找)

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

1.2 有序数组(二分查找)

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

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

1.3 二叉查找树

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

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

音视频开发之旅(27) 算法序列 - 二叉查找树_第1张图片

图片来源于《算法》

二、二叉查找树(BST)

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

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

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

template
struct BSTNode
{
    BSTNode(const T& key = T())
        : _left(nullptr)
        , _right(nullptr)
        , _key(key)
    {}
    
    BSTNode* _left;
    BSTNode* _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) 算法序列 - 二叉查找树)