当前位置: 首页 > ds >正文

【C++篇】二叉树进阶(上篇):二叉搜索树

文章目录

    • 前言
    • 1. 二叉搜索树的概念
    • 2. 二叉搜索树的操作及模拟实现
      • 2.1 查找
      • 2.2 插入
      • 2.3 删除
    • 3. 二叉搜索树的应用
      • 3.1 K模型(Key)
      • 3.2 KV模型(Key_Value)
    • 4. 二叉搜索树查找的时间复杂度分析


前言

本文我们补充二叉树的知识——二叉搜索树。在之前学习初阶数据结构时,我们还留下了这部分知识没有讲解,具体原因是由于C语言的限制,会大大增大我们的学习成本,因此,在学会C++后学习会事半功倍。

二叉搜索树我将会分两个阶段来学习:

  • 二叉搜索树的概念和应用,以及模拟实现二叉搜索树(本文内容)
  • 二叉树OJ题(下篇内容)

1. 二叉搜索树的概念

二叉搜索树又称二叉排序树,它可以是一颗空树。
它是具有以下性质的二叉树:
如果树不为空时

  1. 非空左子树的所有节点的值小于其根节点的值。
  2. 非空左子树的所有节点的值小于其根节点的值。
  3. 左右子树都是二叉搜索树
  4. 节点的值都是唯一的,不存在相等值的两个节点

为什么又可以叫二叉排序树呢?
因为二叉搜索树的中序遍历的结果一定是一个升序序列

那么,二叉搜索树应该是链式结构还是顺序结构呢?
其实很容易想到,这棵树需要具备增删的功能,若是顺序结构,每次都得挪动数据,效率太差。
因此,二叉搜索树是链式结构


2. 二叉搜索树的操作及模拟实现

这里操作的实现可以有两种方式:递归和非递归(迭代)。这里我们两者一起实现。

2.1 查找

  1. 从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
  2. 最多查找高度次,走到到空,还没找到,这个值不存在。

非递归:

bool 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 true;}}return false;
}

递归:

bool _FindR(Node* root, const K& key)
{if (root == nullptr){return false;}if (root->_key > key)//比根小了,就在左子树上查找{_FindR(root->_left, key);}else if (root->_key < key)//比根大了,就在右子树上查找{_FindR(root->_right, key);}else{return true;}
}

2.2 插入

插入的具体过程分两种情况:
a. 树为空,则直接新增节点,赋值给root指针
b. 树不空,按二叉搜索树性质查找插入位置,插入新节点

如果有重复的节点,则插入失败,返回false。

非递归:

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

递归:

  1. 小于根节点,化子问题为在左子树插入
  2. 大于根节点,化子问题为在右子树插入

设计参数:

bool _InsertR(Node*& root, const K& key)

这里root参数巧用&引用,可以达到效果是:递归的每层root都是同一个,以达到每一层递归都是作用在一颗树上。
后面的删除也是如此。

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

2.3 删除

删除就比较复杂了,情况分为:

  1. 删除的节点没有孩子,也就是叶子节点,直接删即可。
  2. 删除的节点有一个孩子,将这个孩子交给父亲,也就是托孤。注意区分左右孩子。
  3. 删除的节点有两个孩子,替换法,将其与左子树最大的节点或右子树最小的节点进行替换,转换为一个孩子或两个孩子的节点进行删除。
    求法:
    左子树最大节点——左子树的最右节点;
    右子树最小节点——右子树的最左节点。

还有一个很容易疏忽的一个情况:LeftMax存在左孩子的情况
删除8:
在这里插入图片描述
如果替换后直接删除的话,LeftMax的左孩子就丢失了,因此这里需要托孤

if (parent->_left == LeftMax)
{parent->_left = LeftMax->_left;
}
else
{parent->_right = LeftMax->_left;
}

完整代码(非递归):

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->_left;}else{if (parent->_left == cur){parent->_left = cur->_left;}else{parent->_right = cur->_left;}}}//右为空else if (cur->_right == nullptr){if (cur == _root){_root = cur->_right;}else{if (parent->_right == cur){parent->_right = cur->_right;}else{parent->_left = cur->_right;}}}//左右都不为空else{Node* LeftMax = cur->_left;parent = cur;//找最右节点while (LeftMax->_right){parent = LeftMax;LeftMax = LeftMax->_right;}swap(cur->_key, LeftMax->_key);//处理LeftMax存在左孩子的情况if (parent->_left == LeftMax){parent->_left = LeftMax->_left;}else{parent->_right = LeftMax->_left;}cur = LeftMax;}delete cur;return true;}}return false;
}

递归:

  1. 小于根节点,化子问题为在左子树删除
  2. 大于根节点,化子问题为在右子树删除
  3. 等于根节点,进行删除操作(同上)
    好处是无需控制父节点了,也避免了许多复杂的情况。
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* LeftMax = root->_left;while (LeftMax->_right){LeftMax = LeftMax->_right;}swap(root->_key, LeftMax->_key);return _EraseR(root->_left, key);}delete del;return true;}
}Node* _root;
};

3. 二叉搜索树的应用

3.1 K模型(Key)

K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
应用场景案例:
给一个单词word,判断该单词是否拼写正确
具体方式如下:

  1. 以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
  2. 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。

3.2 KV模型(Key_Value)

每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。
这种模型在现实生活中更为常见:

  • 英汉词典就是英文与中文的对应关系
    通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;
  • 统计单词次数,统计成功后,给定单词就可快速找到其出现的次数
    单词与其出现次数就是<word, count>就构成一种键值对。

4. 二叉搜索树查找的时间复杂度分析

二叉搜索树的操作都用到了查找
那对于查找,它的时间复杂度是多少呢?

对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在树的深度
但二叉搜索树为完全二叉树时就是最好的情况了,查找的时间复杂度为O(logN)O(logN)O(logN)

但是,时间复杂度计算的最坏情况,并非平均。我们需要寻找极端情况。

极端情况:

在这里插入图片描述
这种趋于线性的二叉搜索树,查找的时间复杂是O(N)O(N)O(N)

那能不能将此类情况优化呢??

有的有的,后继我们将要学习的AVL树、红黑树,它们的最多查找次数就是树的高度次,敬请期待吧!

http://www.xdnf.cn/news/15305.html

相关文章:

  • Qt中QGraphicsView类应用解析:构建高效2D图形界面的核心技术
  • 数据结构-顺序表
  • 【C语言网络编程】HTTP 客户端请求(域名解析过程)
  • Oracle字符类型详解:VARCHAR、VARCHAR2与CHAR的区别
  • Qt数据库编程详解:SQLite实战指南
  • 解决Linux绑定失败地址已使用(端口被占用)的问题
  • 设计仿真 | MSC Apex Simufact实现铁路铰链轻量化与高精度增材制造
  • 在 Spring Boot 中优化长轮询(Long Polling)连接频繁建立销毁问题
  • MySQL:分析表锁的常见问题
  • JavaScript加强篇——第四章 日期对象与DOM节点(基础)
  • P9755 [CSP-S 2023] 种树
  • 【JavaScript高级】构造函数、原型链与数据处理
  • OS16.【Linux】冯依诺曼体系结构和操作系统的浅层理解
  • docker-compose安装常用中间件
  • 【unitrix】 4.21 类型级二进制数基本结构体(types.rs)
  • 1965–2022年中国大陆高分辨率分部门用水数据集,包含:灌溉用水、工业制造用水、生活用水和火电冷却
  • C语言的程序控制语句
  • VR协作海外云:跨国企业沉浸式办公解决方案
  • 决策树算法在医学影像诊断中的广泛应用
  • ch07 题解
  • 番外-linux系统运行.net framework 4.0的项目
  • [特殊字符]远程服务器配置pytorch环境
  • 设计模式笔记_结构型_代理模式
  • 基于vscode开发工具显示git提交信息的插件
  • 世界现存燃油汽车品牌起源国别梳理
  • 【实时Linux实战系列】硬实时与软实时设计模式
  • 【网络】Linux 内核优化实战 - net.netfilter.nf_conntrack_max
  • 基于开源AI智能名片链动2+1模式与S2B2C商城小程序的渠道选择策略研究
  • BPE(Byte Pair Encoding)分词算法
  • flutter鸿蒙版 环境配置