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

目录

  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. 学习实践了二分查找树的新增结点、查找结点、以及删除结点的逻辑和实现。

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,651评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,468评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,931评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,218评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,234评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,198评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,084评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,926评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,341评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,563评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,731评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,430评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,036评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,676评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,829评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,743评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,629评论 2 354

推荐阅读更多精彩内容