数据结构~二叉搜索树

ops/2024/9/25 0:49:02/

文章目录

    • 一、二叉树搜索的概念
    • 二、二叉树搜索的结构
      • 二叉树搜索的性能分析
      • 二叉树搜索的插入
      • 二叉树搜索的查找
      • 二叉树搜索的删除
    • 三、二叉搜索树key和key/value使用场景
    • 四、二叉树搜索的练习
      • 将二叉搜索树就地转化为已排序的双向循环链表
      • 从前序与中序遍历序列构造二叉树
      • 二叉树的前序遍历(非递归)
    • 五、完整代码
      • 二叉树搜索的实现代码
      • key/value二叉树搜索代码实现
    • 六、总结

一、二叉树搜索的概念

二叉树搜索又称二叉排序树(Binary Search Tree,BST),它或者是一棵空树,或者是具有以下性质的⼆叉树:

  • 若它的左子树不为空,则左子树上所有结点的值都小于等于根结点的值
  • 若它的右子树不为空,则右子树上所有结点的值都大于等于根结点的值
  • 它的左右子树也分别为二叉搜索树
  • 二叉搜索树中可以支持插入相等的值,也可以不支持插入相等的值,具体看使用场景定义,后续学习map/set/multimap/multiset系列容器底层就是二叉搜索树,其中map/set不支持插入相等值,multimap/multiset支持插入相等值
    在这里插入图片描述

二、二叉树搜索的结构

二叉树搜索的性能分析

最优情况下,二叉搜索树为完全二叉树(或者接近完全⼆叉树),其高度为:O(log2N)
最差情况下,二叉搜索树退化为单支树(或者类似单支),其高度为:O(N/2)
所以综合而言⼆叉搜索树增删查改时间复杂度为:O(N)
那么这样的效率显然是无法满足需求的,后续讲解⼆叉搜索树的变形,平衡二叉搜索树AVL树和红黑树,才能适用于我们在内存中存储和搜索数据。另外需要说明的是,二分查找也可以实现O(logN) 级别的查找效率,但是二分查找有两大缺陷:

  1. 需要存储在支持下标随机访问的结构中,并且有序。
  2. 插入和删除数据效率很低,因为存储在下标随机访问的结构中,插⼊和删除数据⼀般需要挪动数据。这里也就体现出了平衡⼆叉搜索树的价值。
    在这里插入图片描述

二叉树搜索的插入

插入的具体过程如下:

  1. 树为空,则直接新增结点,赋值给root指针
  2. 树不空,按二叉搜索树性质,插入值比当前结点大往右走,插入值比当前结点小往左走,找到空位置,插入新结点。
  3. 如果支持插入相等的值,插入值跟当前结点相等的值可以往右走,也可以往左走,找到空位置,插入新结点。(要注意的是要保持逻辑⼀致性,插入相等的值不要⼀会往右走,⼀会往左走)
int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};

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

二叉树搜索的查找

  1. 从根开始比较,查找x,x比根的值大则往右边走查找,x比根值小则往左边走查找。
  2. 最多查找高度次,走到到空,还没找到,这个值不存在。
  3. 如果不支持插入相等的值,找到x即可返回
  4. 如果支持插入相等的值,意味着有多个x存在,⼀般要求查找中序的第⼀个x。如下图,查找3,要找到1的右孩子的那个3返回
  5. 在这里插入图片描述

二叉树搜索的删除

首先查找元素是否在二叉搜索树中,如果不存在,则返回false。
如果查找元素存在则分以下四种情况分别处理:(假设要删除的结点为N)

  1. 要删除结点N左右孩子均为空
  2. 要删除的结点N左孩子位空,右孩子结点不为空
  3. 要删除的结点N右孩子位空,左孩子结点不为空
  4. 要删除的结点N左右孩子结点均不为空

对应以上四种情况的解决方案:

  1. 把N结点的父亲对应孩子指针指向空,直接删除N结点(情况1可以当成2或者3处理,效果是⼀样的)
  2. 把N结点的父亲对应孩子指针指向N的右孩子,直接删除N结点
  3. 把N结点的父亲对应孩子指针指向N的左孩子,直接删除N结
  4. 无法直接删除N结点,因为N的两个孩子无处安放,只能用替换法删除。找N左子树的值最大结点R(最右结点)或者N右子树的值最小结点R(最左结点)替代N因为这两个结点中任意⼀个,放到N的位置,都满足⼆叉搜索树的规则。替代N的意思就是N和R的两个结点的值交换,转而变成删除R结点,R结点符合情况2或情况3,可以直接删除。
    在这里插入图片描述
    在这里插入图片描述

在这里插入图片描述

三、二叉搜索树key和key/value使用场景

key搜索场景:
只有key作为关键码,结构中只需要存储key即可,关键码即为需要搜索到的值,搜索场景只需要判断key在不在。key的搜索场景实现的二叉树搜索树支持增删查,但是不支持修改,修改key破坏搜索树结构了。
场景1:小区无人值守车库,小区车库买了车位的业主车才能进小区,那么物业会把买了车位的业主的车牌号录入后台系统,车辆进入时扫描车牌在不在系统中,在则抬杆,不在则提示非本小区车辆,无法进入。
场景2:检查⼀篇英文章章单词拼写是否正确,将词库中所有单词放入⼆叉搜索树,读取文章中的单词,查找是否在⼆叉搜索树中,不在则波浪线标红提示。
在这里插入图片描述
key/value搜索场景:
每⼀个关键码key,都有与之对应的值value,value可以任意类型对象。树的结构中(结点)除了需要存储key还要存储对应的value,增/删/查还是以key为关键字走二叉搜索树的规则进行比较,可以快速查找到key对应的value。key/value的搜索场景实现的二叉树搜索树支持修改,但是不支持修改key,修改key破坏搜索树结构了,可以修改value。
场景1:简单中英互译字典,树的结构中(结点)存储key(英文)和vlaue(中文),搜索时输入英文,则同时查找到了英文对应的中文。
场景2:商场无人值守车库,入口进场时扫车牌,记录车牌和入场时间,出口离场时,扫描车牌,查找入场时间,用当前时间-入场时间计算出停车时长,计算出停车费用,缴费后抬杆,车辆离场。
场景3:统计⼀篇文章中单词出现的次数,读取⼀个单词,查找单词是否存在,不存在这个说明第⼀次出现,(单词,1),单词存在,则++单词对应的次数。
在这里插入图片描述

四、二叉树搜索的练习

将二叉搜索树就地转化为已排序的双向循环链表

将一个 二叉搜索树 就地转化为一个 已排序的双向循环链表 。
对于双向循环列表,你可以将左右孩子指针作为双向循环链表的前驱和后继指针,第一个节点的前驱是最后一个节点,最后一个节点的后继是第一个节点。
特别地,我们希望可以 就地 完成转换操作。当转化完成以后,树中节点的左指针需要指向前驱,树中节点的右指针需要指向后继。还需要返回链表中最小元素的指针。
在这里插入图片描述

思想:

  • 搜索二叉树走中序是有序的,本题目要求原地修改,也就是不能创建新的结点。
  • 思路1:中序遍历搜索⼆叉树,遍历顺序是有序的,将⼆叉树的结点指针放到⼀个vector中,再把前后结点的链接关系进⾏修改。这个思路最简单,但是需要消耗O(N)的空间复杂度。
  • 思路2:依旧中序遍历搜索二叉树,遍历顺序是有序的,遍历过程中修改左指针为前驱和右指针为后继指针。记录⼀个cur和prev,cur为当前中序遍历到的结点,prev为上⼀个中序遍历的结点,cur->left指向prev,cur->right⽆法指向中序下⼀个,因为不知道中序下⼀个是谁,但是prev->right指向cur;也就是说每个结点的左是在中遍历到当前结点时修改指向前驱的,但是当前结点的右,是在遍历到下⼀个结点时,修改指向后继的。

代码实现

class Solution {
public:void InOrderConvert(Node* cur, Node*& prev){if (cur == nullptr)return;// 中序遍历InOrderConvert(cur->left, prev);// 当前结点的左,指向前⼀个结点cur->left = prev;// 前⼀个结点的右,指向当前结点if (prev)prev->right = cur;prev = cur;InOrderConvert(cur->right, prev);}Node* treeToDoublyList(Node* root) {if (root == nullptr)return nullptr;Node* prev = nullptr;InOrderConvert(root, prev);// 从根开始往左⾛,找到第⼀个结点Node* head = root;while (head->left){head = head->left;}// head为第⼀个结点,prev是最后⼀个结点// 题⽬要求为循环链表,进⾏⼀些链接head->left = prev;prev->right = head;return head;}
};

从前序与中序遍历序列构造二叉树

给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。
在这里插入图片描述
思路:
先序遍历的顺序是根节点、左子树、右子树。这意味着先序遍历的第一个元素就是根节点的值。
中序遍历的顺序是左子树、根节点、右子树。通过在中序遍历中找到根节点的位置,可以确定左子树和右子树的元素范围。
可以根据先序遍历和中序遍历构建出二叉树。这个过程利用了先序遍历确定根节点和中序遍历划分左右子树的特点,通过递归的方式逐步构建出整个二叉

class Solution {
public:TreeNode* _buildTree(vector<int>& preorder, vector<int>& inorder, int&prei, int inbegin, int inend) {if (inbegin > inend)return nullptr;// 前序确定根TreeNode* root = new TreeNode(preorder[prei++]);// 根分割中序左右⼦区间int rooti = inbegin;while (rooti <= inend){if (inorder[rooti] == root->val)break;elserooti++;}// 递归左右⼦区间,递归构建左右⼦树// [inbegin, rooti-1] rooti [rooti+1, inend]root->left = _buildTree(preorder, inorder, prei, inbegin, rooti - 1);root->right = _buildTree(preorder, inorder, prei, rooti + 1, inend);return root;}TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {int i = 0;TreeNode* root = _buildTree(preorder, inorder, i, 0, inorder.size() - 1);return root;}
};

二叉树的前序遍历(非递归)

给你二叉树的根节点 root ,返回它节点值的 前序 遍历。
在这里插入图片描述
在这里插入图片描述
非递归迭代实现思想:
要迭代非递归实现⼆叉树前序遍历,首先还是要借助递归的类似的思想,只是需要把结点存在栈中,方便类似递归回退时取父路径结点。跟这里不同的是,这里把⼀棵⼆叉树分为两个部分:

  1. 先访问左路结点
  2. 再访问左路结点的右子树
    这里访问右子树要以循环从栈依次取出这些结点,循环子问题的思想访问左路结点的右子树。中序和后序跟前序的思路完全⼀致,只是前序先访问根还是后访问根的问题。后序稍微麻烦⼀些,因为后序遍历的顺序是左子树右子树根,当取到左路结点的右子树时,需要想办法标记判断右子树是否访问过了,如果访问过了,就直接访问根,如果访问过需要先访问右子树
class Solution {
public:vector<int> preorderTraversal(TreeNode* root) {stack<TreeNode*> s;vector<int> v;TreeNode* cur = root;while (cur || !s.empty()){// 访问⼀颗树的开始// 1、访问左路结点,左路结点⼊栈while (cur){v.push_back(cur->val);s.push(cur);cur = cur->left;}// 2、从栈中依次访问左路结点的右⼦树TreeNode* top = s.top();s.pop();// 循环⼦问题⽅式访问左路结点的右⼦树 --cur = top->right;}return v;}
};
class Solution {
public:vector<int> inorderTraversal(TreeNode* root) {stack<TreeNode*> st;TreeNode* cur = root;vector<int> v;while (cur || !st.empty()){// 访问⼀颗树的开始// 1、左路结点⼊栈while (cur){st.push(cur);cur = cur->left;}// 访问问左路结点 和 左路结点的右⼦树TreeNode* top = st.top();st.pop();v.push_back(top->val);// 循环⼦问题⽅式访问右⼦树cur = top->right;}return v;}
};
class Solution {
public:vector<int> postorderTraversal(TreeNode* root) {TreeNode* cur = root;stack<TreeNode*> s;vector<int> v;TreeNode* prev = nullptr;while (cur || !s.empty()){// 1、访问⼀颗树的开始while (cur){s.push(cur);cur = cur->left;}TreeNode* top = s.top();// top结点的右为空 或者 上⼀个访问结点等于他的右孩⼦// 那么说明(空)不⽤访问 或者 (不为空)右⼦树已经访问过了// 那么说明当前结点左右⼦树都访问过了,可以访问当前结点了if (top->right == nullptr || top->right == prev){s.pop();v.push_back(top->val);prev = top;}else{// 右⼦树不为空,且没有访问,循环⼦问题⽅式右⼦树cur = top->right;}}return v;}
};

五、完整代码

二叉树搜索的实现代码

// Binary Search Tree
template<class K>
class BSTree
{typedef BSTNode<K> Node;
public:bool Insert(const K& key){if (_root == nullptr){_root = new Node(key);return true;}Node* parent = nullptr;Node* cur = _root;while (cur){if (cur->_key < key){parent = cur;cur = cur->_right;}else if (cur->_key > key){parent = cur;cur = cur->_left;}else{return false;}}cur = new Node(key);if (parent->_key < key){parent->_right = cur;}else{parent->_left = cur;}return true;}bool Find(const K& key){Node* cur = _root;while (cur){if (cur->_key < key){cur = cur->_right;}else if (cur->_key > key){cur = cur->_left;}else{return true;}}return false;}bool Erase(const K& key){Node* parent = nullptr;Node* cur = _root;while (cur){if (cur->_key < key){parent = cur;cur = cur->_right;}else if (cur->_key > key){parent = cur;cur = cur->_left;}else{// 0-1个孩⼦的情况 // 删除情况1 2 3均可以直接删除,改变⽗亲对应孩⼦指针指向即可if (cur->_left == nullptr){if (parent == nullptr){_root = cur->_right;}else{if (parent->_left == cur)parent->_left = cur -> _right;elseparent->_right = cur -> _right;}delete cur;return true;}else if (cur->_right == nullptr){if (parent == nullptr){_root = cur->_left;}else{if (parent->_left == cur)parent->_left = cur -> _left;elseparent->_right = cur -> _left;}delete cur;return true;}else{// 2个孩⼦的情况 // 删除情况4,替换法删除 // 假设这⾥我们取右⼦树的最⼩结点作为替代结点去删除// 这⾥尤其要注意右⼦树的根就是最⼩情况的情况的处理,对应课件图中删除8的情况// ⼀定要把cur给rightMinP,否会报错。 Node* rightMinP = cur;Node* rightMin = cur->_right;while (rightMin->_left){rightMinP = rightMin;rightMin = rightMin->_left;}cur->_key = rightMin->_key;if (rightMinP->_left == rightMin)rightMinP->_left = rightMin -> _right;elserightMinP->_right = rightMin -> _right;delete rightMin;return true;}}}return false;}void InOrder(){_InOrder(_root);cout << endl;}
private:void _InOrder(Node* root){if (root == nullptr){return;}_InOrder(root->_left);cout << root->_key << " ";_InOrder(root->_right);}
private:Node* _root = nullptr;
};

key/value二叉树搜索代码实现

template<class K, class V>
struct BSTNode
{// pair<K, V> _kv;K _key;V _value;BSTNode<K, V>* _left;BSTNode<K, V>* _right;BSTNode(const K& key, const V& value):_key(key), _value(value), _left(nullptr), _right(nullptr){}
};
template<class K, class V>
class BSTree
{typedef BSTNode<K, V> Node;
public:BSTree() = default;BSTree(const BSTree<K, V>& t){_root = Copy(t._root);}BSTree<K, V>& operator=(BSTree<K, V> t){swap(_root, t._root);return *this;}~BSTree(){Destroy(_root);_root = nullptr;}bool Insert(const K& key, const V& value){if (_root == nullptr){_root = new Node(key, value);return true;}Node* parent = nullptr;Node* cur = _root;while (cur){if (cur->_key < key){parent = cur;cur = cur->_right;}else if (cur->_key > key){parent = cur;cur = cur->_left;}else{return false;}}cur = new Node(key, value);if (parent->_key < key){parent->_right = cur;}else{parent->_left = cur;}return true;}Node* Find(const K& key){Node* cur = _root;while (cur){if (cur->_key < key){cur = cur->_right;}else if (cur->_key > key){cur = cur->_left;}else{return cur;}}return nullptr;}bool Erase(const K& key){Node* parent = nullptr;Node* cur = _root;while (cur){if (cur->_key < key){parent = cur;cur = cur->_right;}else if (cur->_key > key){parent = cur;cur = cur->_left;}else{if (cur->_left == nullptr){if (parent == nullptr){_root = cur->_right;}else{if (parent->_left == cur)parent->_left = cur -> _right;elseparent->_right = cur -> _right;}delete cur;return true;}else if (cur->_right == nullptr){if (parent == nullptr){_root = cur->_left;}else{if (parent->_left == cur)parent->_left = cur -> _left;elseparent->_right = cur -> _left;}delete cur;return true;}else{Node* rightMinP = cur;Node* rightMin = cur->_right;while (rightMin->_left){rightMinP = rightMin;rightMin = rightMin->_left;}cur->_key = rightMin->_key;if (rightMinP->_left == rightMin)rightMinP->_left = rightMin -> _right;elserightMinP->_right = rightMin -> _right;delete rightMin;return true;}}}return false;}void InOrder(){_InOrder(_root);cout << endl;}
private:void _InOrder(Node* root){if (root == nullptr){return;}_InOrder(root->_left);cout << root->_key << ":" << root->_value << endl;_InOrder(root->_right);}void Destroy(Node* root){if (root == nullptr)return;Destroy(root->_left);Destroy(root->_right);delete root;}Node* Copy(Node* root){if (root == nullptr)return nullptr;Node* newRoot = new Node(root->_key, root->_value);newRoot->_left = Copy(root->_left);newRoot->_right = Copy(root->_right);return newRoot;}
private:Node* _root = nullptr;
};
void test1()
{BSTree<string, string> dict;//BSTree<string, string> copy = dict;dict.Insert("left", "左边");dict.Insert("right", "右边");dict.Insert("insert", "插⼊");dict.Insert("string", "字符串");string str;while (cin >> str){auto ret = dict.Find(str);if (ret){cout << "->" << ret->_value << endl;}else{cout << "无此单词,请重新输⼊" << endl;}}
}
void  test2()
{string arr[] = { "苹果", "西⽠", "苹果", "西⽠", "苹果", "苹果", "西⽠","苹果", "⾹蕉", "苹果", "⾹蕉" };BSTree<string, int> countTree;for (const auto& str : arr){// 先查找⽔果在不在搜索树中 // 1、不在,说明⽔果第⼀次出现,则插⼊<⽔果, 1> // 2、在,则查找到的结点中⽔果对应的次数++ //BSTreeNode<string, int>* ret = countTree.Find(str);auto ret = countTree.Find(str);if (ret == NULL){countTree.Insert(str, 1);}else{ret->_value++;}}countTree.InOrder();
}int main()
{test1();test2();return 0;
}

六、总结

本章节和之后的文章使用的语言从C变为C++
优点与缺点:

  • 优点:
    • 查找、插入、删除操作的时间复杂度在平均情况下为 ,效率较高,特别是对于经常进行动态插入和查找操作的数据集合非常适用。
    • 中序遍历可以方便地得到有序的数据序列,无需额外的排序操作。
  • 缺点:
    • 最坏情况下(例如树退化为链表),查找、插入、删除操作的时间复杂度会退化到 ,性能下降严重。
    • 二叉搜索树的平衡情况依赖于插入和删除操作的顺序,如果数据的插入顺序不合理,可能会导致树的不平衡,影响操作的效率。
  • 应用场景:
    • 二叉搜索树常用于实现动态集合的数据结构,例如在数据库索引、文件系统的目录结构、编译器的符号表等场景中广泛应用,能够快速地进行数据的查找、插入和删除操作。

二叉搜索树在 C++ 中是一种重要的数据结构,掌握其基本操作和遍历方式对于解决各种实际问题具有重要意义。


http://www.ppmy.cn/ops/115538.html

相关文章

MODELS 2024震撼续章:科技与可持续性的未来交响曲

MODELS 2024国际会议正如火如荼地进行着&#xff0c;每一天都充满了新的发现与启迪&#xff0c;每一场分享都是对技术前沿的一次深刻探索&#xff0c;更是对现实世界可持续性挑战的一次积极回应。现在让我们继续这场科技盛宴&#xff0c;看看小编为您精选几场的学术分享吧~ 会议…

10KV并网分布式光伏电力监控解决方案

一、分布式光伏并网要求 Q/GDW1480-2015 《分布式电源接入电网技术规定》&#xff1a;分布式电源并网电压等级可根据各并网点装机容量进行初步选择&#xff0c;推荐如下&#xff1a; 8kW 及以下可接入220V&#xff1b; 8kW~400kW可接入380V&#xff1b; 400kW~6MW可接入10k…

考研数据结构——C语言实现小顶堆

数组初始化&#xff1a; 首先&#xff0c;我们有一个整数数组arr&#xff0c;里面包含了一系列需要排序的数字。数组的长度n是通过对数组arr的总字节大小除以单个元素的字节大小得到的。 小顶堆调整函数&#xff1a; adjustHeapMin函数的作用是将数组中的元素从某个节点向下调整…

Allegro视频去除走线的小方块

走线出现小方块图如下&#xff1a; 其实这种情况并不影响PCB生产和布线的联通性&#xff0c;只是多少会影响美观和性能&#xff0c;在Allegro视频中去除的方法比较简单&#xff0c;是由模块复用以后&#xff0c;没有打散模块引起的。只要我们将模块的打散即可。具体操作如下:…

[Linux]自定义shell详解

自定义shell 前言1.命令行提示符&#xff0c;字符串的打印1.1命令行提示符2.命令行字符串 2.0对命令行字符串进行切割2.执行命令3.有趣的小问题完整代码 前言 写之前我们先看看一个完整的shell都包括了什么 $符号前面&#xff08;包括这个符号&#xff09;就是命令行提示符&a…

C#设计模式之访问者模式

总目录 前言 在软件构建过程中&#xff0c;由于需求的改变&#xff0c;某些类层次结构中常常需要增加新的行为&#xff0c;如果直接在基类中做这样的更改&#xff0c;将会给子类带来很繁重的变更负担&#xff0c;甚至破坏原有设计。如何在不更改类层次结构的前提下&#xff0c…

数据库1-1、1-n 、n-n关系实际场景

数据库1-1、1-n 、n-n关系实际场景 每种关系类型的 3 个不同场景案例&#xff1a; 1 对 1 关系&#xff08;One-to-One&#xff09; 用户与个人资料&#xff1a; 场景&#xff1a;每个用户有唯一的个人资料&#xff0c;每个个人资料只对应一个用户。例子&#xff1a;User 和…

深入剖析Docker容器安全:挑战与应对策略

随着容器技术的广泛应用&#xff0c;Docker已成为现代应用开发和部署的核心工具。它通过轻量级虚拟化技术实现应用的隔离与封装&#xff0c;提高了资源利用率。然而&#xff0c;随着Docker的流行&#xff0c;其安全问题也成为关注焦点。容器化技术虽然提供了良好的资源隔离&…