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

【异世界历险之数据结构世界(二叉搜索树)】

二叉搜索树

介绍

二叉搜索树(Binary Search Tree, BST)是一种特殊的二叉树,满足以下性质:
若它的左子树不为空,则 左子树所有节点的值 < 根节点值。
若它的右子树不为空,则 右子树所有节点的值 > 根节点值。
左右子树本身也都是二叉搜索树。

在这里插入图片描述

性质

(1):中序遍历
BST中序遍历有序(左 →中 →根),递增顺序。
(2):查找
查找操作:从根开始比较,若小于当前节点,往左走;若大于当前节点,往右走。
时间复杂度:
最好情况:O(log n) (树接近平衡)
最坏情况:O(n) (树退化成链表,比如插入的序列是递增的)。
(3):插入与删除
插入:沿着查找路径找到 nullptr,新节点挂上去。
删除:分三种情况:
节点没有子树 → 直接删除。
节点只有一个子树 → 子树顶上来。
节点有两个子树 → 用左子树最大值(前驱)或右子树最小值(后继)替代,再删除该节点。
(4):局限性
BST 性能和树的高度相关,如果树退化成链表,效率就变差。
所以实际应用常用 平衡二叉搜索树(AVL、红黑树等),保证树高始终是 O(log n)。

例子:插入序列:5, 3, 7, 2, 4, 6, 8
构成的 BST 是:
5
/
3 7
/ \ /
2 4 6 8
中序遍历:2, 3, 4, 5, 6, 7, 8

非递归实现

前置代码

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

解析:
模板支持
使用 template 让节点可以存储任意类型的数据,例如 int、double、string 等。
成员变量
_left:指向左子树
_right:指向右子树
_key:当前节点存放的关键字
构造函数
形式:BSTreeNode(const K& key = K())
特点:
const K& 避免大对象拷贝,提高效率
默认参数 K() 调用类型的默认构造(int()=0,string()=空串)
初始化列表中:左右孩子指针置空,_key 初始化为传入值

初始化

BSTree():_root(nullptr)
{}

中序遍历

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

一句话
中序遍历的核心思想:先左子树 → 根节点 → 再右子树。
三点展开
递归基准条件
当 root == nullptr 时直接返回,说明子树为空,递归结束。
遍历顺序
_InOrder(root->_left);:先递归访问左子树。
cout << root->_key << " ";:再访问根节点。
_InOrder(root->_right);:最后递归访问右子树。
应用特点
在 二叉搜索树 上,中序遍历会按照从小到大的顺序输出所有节点的值。
常用于检验 BST 是否正确(输出是否有序)。

图解:
在这里插入图片描述

插入

bool Insert(const K& key)
{if (_root == nullptr){_root = new Node(key);}Node* cur = _root;Node* parent = cur;while (cur){if (key > cur->_key){parent = cur;cur = cur->_right;}else if (key < cur->_key){parent = cur;cur = cur->_left;}else{return false;}}Node* newnode = new Node(key);if (parent->_key > key){parent->_left = newnode;}else{parent->_right = newnode;}return true;
}

一句话总结
Insert 函数实现了 二叉搜索树的插入操作:从根开始比较,找到合适的空位置后挂上新节点。
三点展开
特殊情况处理
如果树为空(_root == nullptr),直接创建新节点作为根节点即可。
查找插入位置
从根节点出发,循环比较:
如果 key < cur->_key,往左子树走;
如果 key > cur->_key,往右子树走;
如果相等,则说明值已存在,插入失败。
挂接新节点
根据最终比较结果,将新节点挂到 parent 的左指针或右指针上,完成插入。

PS: 1. parent 节点,是cur的上一个节点,负责连接新插入的节点。
2. 新节点是插入left 还是 right 需要判断当前值的大小。

删除(不推荐)

bool erase(const K& data){if (_root == nullptr){return false;}Node* parent = nullptr;Node* cur = _root;while (cur){if (cur->_key > data){parent = cur;cur = cur->_left;}else if (cur->_key < data){parent = cur;cur = cur->_right;}else{if (cur->_left == nullptr){if (cur == _root){_root = _root->_right;}else{if (parent->_right == cur){parent->_right = cur->_right;}else{parent->_left = cur->_right;}}}else if (cur->_right == nullptr){if (cur == _root){_root = _root->_left;}else{if (parent->_right == cur){parent->_right = cur->_left;}else{parent->_left = cur->_left;}}}else{Node* leftMax = cur->_left;Node* parent = cur;while (leftMax->_right){parent = leftMax;leftMax = leftMax->_right;}std::swap(leftMax->_key, cur->_key);//无法使用erase删除if (parent->_left == leftMax){parent->_left = leftMax->_left;}else{parent->_right = leftMax->_right;}cur = leftMax;}delete cur;return true;}}return false;}

1. 查找目标节点 + 记录父节点
用 cur 找要删除的节点,同时用 parent 记录它的父节点。
根据 data 和当前节点的大小关系,向左/右子树遍历。
如果没找到,直接返回 false。
2. 三种删除情况
只有右子树 / 只有左子树
直接用子树顶替当前节点。
要分两种情况:
删除的是 _root(特殊处理)。
删除的是非根节点(挂到父节点的左/右)。
左右子树都存在
找左子树最大节点 leftMax(或者找右子树最小也行)。
和当前节点 cur 交换值。
再把 leftMax 删除(它最多只有一个左孩子)。
3. 内存释放 + 返回结果
delete cur; 释放目标节点(注意最后 cur 会指向真正被删掉的节点)。
删除成功返回 true,否则返回 false。

查找

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;
}

一句话概括:
Find 用于在二叉搜索树中查找指定值,依据节点大小关系逐层遍历,直到找到或为空。
三点展开:
空树直接返回 false:若根节点 _root 为空,说明树中没有任何元素,查找必然失败。
利用 BST 性质逐层搜索:若当前节点值大于 key,往左子树走;若小于 key,往右子树走;不断缩小查找范围。
成功或失败的判定:一旦找到等于 key 的节点立即返回 true;若遍历至空指针,说明不存在,返回 false。

销毁

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

PS:
为什么必须要后序遍历销毁?
因为必须先把子树删掉,再删当前节点,否则子树没法访问就会内存泄漏。

递归实现

递归一般使用两个函数,一个在public ,一个在private。
因为递归要使用_root这个私有成员,公有函数不能有这个参数,因为类外面无法使用_root,所以函数没法正常运行。

例子:

public:
bool FindR(const K& key)
{return _FindR(_root, key);
}
private:
bool _FindR(Node* root,const K& key)
{if (_root == nullptr)return false;if (key < root->_key){_Find(root->_left,key);}else if (key > root->_key){_Find(root->_right, key);}else{return true;}
}

插入

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

一句话概括:
递归插入通过不断比较 key 与当前节点的值,直到找到空位置再创建新节点。
三点展开:
递归基准:当 root == nullptr 时,说明找到了合适的位置,直接生成新节点并返回 true。
递归过程:若 key 大于当前节点值,就递归插入右子树;若小于,则递归插入左子树。
去重逻辑:若等于当前节点值,说明该元素已存在,直接返回 false,避免重复插入。

PS:
为什么不用 Node root?*
如果参数是普通指针 Node* root,在递归里 root = new Node(key) 只是修改了局部变量的拷贝,递归结束后父节点的 ->_left 或 ->_right 并不会被改变。
这样插入操作就失效了。
引用的作用
Node*& root 表示传进来的是一个“指针的引用”。
当你在递归里写 root = new Node(key),其实就是直接改了父节点的 ->_left 或 ->_right,保证树结构被真正更新。

图解:
在这里插入图片描述

查找

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

一点总结
递归版 Find 的核心思想是:不断缩小查找区间,直到找到目标值或走到空指针。
三点展开
递归基准条件
if (root == nullptr) return false;
说明走到叶子还没找到,递归结束,返回失败。
递归分支逻辑
if (key < root->_key):目标在左子树,递归到 root->_left。
else if (key > root->_key):目标在右子树,递归到 root->_right。
else:相等说明找到了,返回 true。
时间复杂度
在一棵高度为 h 的二叉搜索树里,最多递归 h 层。
平衡树时 h ≈ logN,最坏情况(退化成链表)是 O(N)。

删除

bool _EraseR(Node*& root, const K& key)
{if (root == nullptr)return false;if (key > root->_key){_EraseR(root->_right, key);}else if (key < root->_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* leftMax = root->_left;while (leftMax->_right){leftMax = leftMax->_right;}std::swap(leftMax->_key, root->_key);return _EraseR(root->_left, key);}delete del;return true;}
}

查找阶段:
按 BST 性质比较 key 与 root->_key,决定向左或向右递归。
Node*& root 的必要性:若节点被替换(例如 root = root->_right),需要修改父节点的 left/right 指针;传引用可以做到这一点。
无/单子树的删除:
如果某一边为空,直接用另一边顶替当前节点(保持子树接通)。
先保存 del = root,然后把 root 指向替代子树,最后 delete del 释放原节点,避免悬空指针。
双子树的删除(常用策略):
找 前驱(左子树最大)或 后继(右子树最小)——这两个节点至少有一个空子树(前驱没有右子树,后继没有左子树),因此删除它们简单。
用 swap 把要删除的 key 移到可被轻易删除的位置(前驱/后继处),然后递归删除该值在子树中的那份拷贝。
为什么 root->_left 而不是 leftMax:
leftMax 只是一个指针变量,若把 leftMax 传入(即 _EraseR(leftMax, key)),函数内部修改 leftMax(例如 leftMax = leftMax->_left)只会影响局部变量,不会修改父节点的子指针(父节点仍指向已删除内存)→ 悬空指针或崩溃。
传 root->_left(一个左值)能把 父节点的子指针 传入(绑定到 Node*&),递归删除时能正确修改父节点链接。

图解:
在这里插入图片描述


总结:二叉搜索树递归非递归对比

功能递归实现(Recursive)非递归实现(Iterative)
查找 (Find)简洁直观简洁直观,避免函数栈开销
插入 (Insert)短小优雅稍微繁琐,需要一个 parent 指针
删除 (Erase)逻辑清晰,但需要处理的子情况较多逻辑复杂,但避免爆栈
遍历 (Traversal)几行代码搞定,最直观需要用栈或队列来模拟递归过程,更适合处理大数据量的树,避免栈溢出
http://www.dtcms.com/a/403935.html

相关文章:

  • 宁夏建设银行网站好的兼职做调查网站
  • SQLMap数据库枚举靶机(打靶记录)
  • 镇江建设工程质量监督局网站虹口 教育 网站建设
  • stm32移植elog
  • 揭阳市网站建设徐州市建设局网站
  • 讯飞起点阅读器京东式开售,后kindle时代机会在哪里?
  • 2018/07 JLPT听力原文 问题四
  • 旅游网站开发说明书网站建设费用应按几年摊销
  • Redis数据持久化
  • wampserver搭建网站鹤山区网站建设
  • 河南省建设厅网站考试成绩查询东莞人才网求职
  • 【数据结构前置知识】泛型
  • Flink SourceOperator和WaterMark
  • 容器化 Djiango 应用程序
  • 营销网站建设企划案例网站建设业务越做越累
  • Java EE、Java SE 和 Spring Boot
  • 两学一做专题网站wordpress 用户密码的加密算法
  • 手写数据结构-- avl树
  • MySQL-事务日志
  • SpringBoot旅游管理系统
  • 永州市城乡建设规划局网站湖南大型网站建设公司
  • 买东西网站有哪些汽车设计公司排名前十强
  • IT 疑难杂症诊疗室:破解常见故障的实战指南​
  • 集团网站建设详细策划广告设计与制作模板
  • OSError: [WinError 182] 操作系统无法运行 %1。 解决办法
  • 部门网站建设的工作领导小组局域网建设简单的影视网站
  • 嵌入式学习(45)-基于STM32F407Hal库的Modbus Slave从机程序
  • 【字符串算法集合】KMP EXKMP Manacher Trie 树 AC 自动机
  • 网站是哪家公司开发的中山网站建设文化价位
  • 织梦网站如何备份教程企业网站建设公司网络