【高阶数据结构】二叉树搜索树 {概念;实现:核心结构,增删查,默认成员函数;应用:K模型和KV模型;性能分析;相关练习}

二叉搜索树

一、二叉搜索树的概念

在这里插入图片描述

二叉搜索树又称二叉排序树,它可以是一棵空树,若果不为空则满足以下性质:

  1. 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  2. 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  3. 它的左右子树也分别为二叉搜索树
  4. 二叉搜索树中不允许出现相同的键值

提示:二叉搜索树的附加功能是排序和去重。对于一个二叉搜索树,按照左子树-根节点-右子树的顺序进行中序遍历,得到的序列就是有序的。


二、二叉树搜索树的实现

2.1 核心结构

template <class K>
struct BSTreeNode{ //搜索二叉树的节点typedef BSTreeNode<K> Node;Node *_left;Node *_right;K _key;BSTreeNode(const K &key = K()):_left(nullptr), //必须将指针初始化为nullptr,防止出现野指针问题。_right(nullptr),_key(key){}
}; template <class K>
class BSTree{ //搜索二叉树typedef BSTreeNode<K> Node;Node *_root = nullptr;public:BSTree() = default; //C++11的用法:使用了default关键字,表示使用编译器自动生成的默认构造函数void InOrder(){  //中序遍历多套一层是因为递归调用需要传递根节点指针,而根节点指针_root是private权限,在类外不能访问。_InOrder(_root);  cout << endl;}//......
};
template <class K>void BSTree<K>::_InOrder(Node *root){ //中序遍历if(root == nullptr) return;_InOrder(root->_left);cout << root->_key << " ";_InOrder(root->_right);}

2.2 查找

  1. 从根开始比较,如果key比根大则到右树去找;如果key比根小则到左树去找;
  2. 最多查找高度次。如果走到空节点还未找到,则说明这个键值不存在。
//迭代法查找:
template <class K>bool BSTree<K>::Find(const K &key){//如果是空树,直接返回false;if(_root == nullptr) return false;Node *cur = _root;while(cur != nullptr){if(key > cur->_key) //如果键值大,就到右树去找{cur = cur->_right;}else if(key < cur->_key) //如果键值小,就到左树去找{cur = cur->_left;}else{//找到返回truereturn true;}}//如果走到空节点还未找到,则说明这个键值不存在,返回falsereturn false;}//递归法查找:
template <class K>bool BSTree<K>::_rFind(Node *root, const K &key){if(root == nullptr) return false; //如果是空树或者走到空节点还未找到,返回falseif(key > root->_key) return _rFind(root->_right, key); //如果键值大,就到右树去找else if(key < root->_key) return _rFind(root->_left, key); //如果键值小,就到左树去找else return true;//找到返回true}

2.3 插入

插入的具体过程如下:

  1. 树为空,则直接新增节点,赋值给root指针
  2. 树不为空,按二叉搜索树性质查找插入位置,插入新节点:
  3. 如果找到相同的键值,则不进行插入。
  4. 直到找到合适的空位置,才能进行插入操作。
//迭代法插入:
template <class K>
bool BSTree<K>::Insert(const K &key){//树为空,直接进行插入:if(_root == nullptr){_root = new Node(key);return true;}//树不为空,按二叉搜索树性质查找插入位置,插入新节点:Node *cur = _root;Node *parent = _root;while(cur != nullptr){if(key > cur->_key) //如果键值大,就到右树去找{parent = cur; //移动cur指针前记录父节点指针parent,便于下一步插入操作的连接。cur = cur->_right;}else if(key < cur->_key) //如果键值小,就到左树去找{parent = cur;cur = cur->_left;}else{return false; //如果找到相同的键值,则插入失败。}}//直到找到合适的空位置,才能进行插入操作:if(key > parent->_key) //判断该位置是父节点的左节点还是右节点{parent->_right = new Node(key);}else{parent->_left = new Node(key);}return true;
}//递归法插入:
/*传引用的好处:root是父节点内_left,_right指针的引用(对于空树就是_root的引用),修改root就是修改父节点的_left,_right指针。所以我们不需要再记录父节点的指针,也不需要再判断该位置是父节点的左节点还是右节点。*/
template <class K>bool BSTree<K>::_rInsert(Node* &root, const K &key){//如果树为空或者找到了合适的空位置,进行插入操作:if(root == nullptr){root = new Node(key);return true;}if(key > root->_key)  //如果键值大,就到右树去插入return _rInsert(root->_right, key);else if(key < root->_key)  //如果键值小,就到左树去插入return _rInsert(root->_left, key);else  //如果找到相同的键值,则插入失败。return false;}

2.4 删除

首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情况:

  1. 要删除的结点是叶节点
  2. 要删除的结点只有左结点
  3. 要删除的结点只有右结点
  4. 要删除的结点有左、右结点

看起来待删除节点有4中情况,实际情况1可以与情况2或者3合并起来(使父节点指向nullptr),因此真正的删除过程如下:

  1. 使父结点指向被删除节点的左结点;然后删除该结点;–直接删除
  2. 使父结点指向被删除节点的右结点;然后删除该结点;–直接删除
  3. 找到被删除节点左树的最大值(最右)或右树的最小值(最左);将其key值与被删除节点的key值交换,保证搜索树的结构;最后删除该节点(最大或最小节点)–替换法删除
//迭代法删除:
template <class K>bool BSTree<K>::Erase(const K &key){Node *cur = _root;Node *parent = _root; while(cur != nullptr){if(key > cur->_key){parent = cur; //移动cur指针前记录父节点指针parent,便于下一步删除操作的连接。cur = cur->_right;}else if(key < cur->_key){parent = cur;cur = cur->_left;}else{//情况1,2,3 直接删除if(cur->_left == nullptr || cur->_right == nullptr){DelNode(parent, cur);}else{//情况4 替换法删除Node *lmaxp = cur; //如果cur->left就是左树的最大值,lmaxp应该指向curNode *lmax = cur->_left; //lmax找左树的最大值,即左树的最右节点。while(lmax->_right != nullptr) {lmaxp = lmax; //移动lmax指针前记录父节点指针lmaxp,便于下一步删除操作的连接。lmax = lmax->_right;} swap(cur->_key, lmax->_key); //将其key值与被删除节点的key值交换,保证搜索树的结构//最后删除该节点。//注意:lmax指向最右节点,但该节点可能有左树;即使是叶节点,删除后也要接nullptr;//因此要调用DelNode删除前进行连接。替换法删除实际是将问题转化为情况1或2或3DelNode(lmaxp, lmax); }return true; //完成删除马上返回}}//如果是空树,或者找不到要删除的元素,删除失败。return false; }//直接删除节点
template <class K>void BSTree<K>::DelNode(Node *parent, Node *cur){if(cur->_left == nullptr) //要删除的结点只有右结点(或者是叶节点);使其父结点指向其右结点(叶节点指向空);{if(cur == _root) //如果要删除的是根节点,需要特殊处理。{_root = cur->_right;}else if(parent->_left == cur) //判断要删除的结点是父节点的左节点还是右节点{parent->_left = cur->_right;}else{parent->_right = cur->_right;}}else if(cur->_right == nullptr) //要删除的结点只有左结点;使父结点指向其左结点;{if(cur == _root){_root = cur->_left;}else if(parent->_left == cur){parent->_left = cur->_left;}else{parent->_right = cur->_left;}}//最后释放删除节点delete cur;}//递归法删除:
/*传引用的好处:root是父节点内_left,_right指针的引用,修改root就是修改父节点的_left,_right指针。所以我们不需要再记录父节点的指针,也不需要再判断该位置是父节点的左节点还是右节点。*/
template <class K>bool BSTree<K>::_rErase(Node* &root, const K &key){//如果是空树,或者找不到要删除的元素,删除失败。if(root == nullptr) return false;if(key > root->_key) _rErase(root->_right, key);else if(key < root->_key) _rErase(root->_left, key);else{Node *del = root; //记录要删除的节点//情况1,2,3 直接删除if(root->_left == nullptr){root = root->_right;}else if(root->_right == nullptr){root = root->_left; }else{//情况4 替换法删除 Node *rmin = root->_right; //rmin找右树的最小值,即右树的最左节点。while(rmin->_left!=nullptr){rmin = rmin->_left;}swap(rmin->_key, root->_key); //将其key值与被删除节点的key值交换,保证搜索树的结构//最后删除该节点。//注意:rmin指向最左节点,但该节点可能有右树;即使是叶节点,删除后也要接nullptr;//因此要递归调用_rErase删除前进行连接。替换法删除实际是将问题转化为情况1或2或3//交换后,已经不能从根节点开始找key了(key的位置已经不符合搜索树结构);//应该从右子树的根节点开始找(key的位置在右子树中仍符合搜索树结构);return _rErase(root->_right, key);}//释放节点delete del;return true;}}

2.5 默认成员函数

template <class K>
class BSTree{typedef BSTreeNode<K> Node;Node *_root = nullptr;
public:BSTree() = default; //使用编译器自动生成的默认构造函数//多套一层是因为递归调用需要传递根节点指针,而根节点指针_root是private权限,在类外不能访问。析构同理。BSTree(const BSTree<K> &bst){_root = _Copy(bst._root);}BSTree<K>& operator=(BSTree<K> bst){ //复用拷贝构造swap(bst._root, _root);return *this;}~BSTree(){_Destroy(_root);}//......
};

拷贝构造

  template <class K>Node* BSTree<K>::_Copy(Node *root){if(root == nullptr) return nullptr;Node *copyroot = new Node(root->_key); //前序遍历拷贝,保证两棵树的结构顺序完全相同copyroot->_left = _Copy(root->_left);copyroot->_right = _Copy(root->_right);return copyroot;}

析构

  template <class K>void BSTree<K>::_Destroy(Node *root){if(root == nullptr) return;_Destroy(root->_left); //析构必须采用后序遍历_Destroy(root->_right);delete root;} 

三、二叉搜索树的应用

3.1 K模型

K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。

比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:(示例代码:wordchecker.cc)

  1. 以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
  2. 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
//wordchecker.cc
#include "BSTree_K.hpp"
#include <iostream>
#include <string>
using namespace std;int main(){string tmp[] = {"search", "word", "vector", "string", "dictionary", "list", "binary"};BSTree<string> lib;for(string &e : tmp){lib.Insert(e);}lib.InOrder();string input;while(cin >> input){bool ret = lib.Find(input);if(ret){cout << "拼写正确!" << endl;}else{cout << "拼写错误!" << endl;}}return 0;
}

3.2 KV模型

KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。

KV模型中key为关键码,二叉搜索树构建过程中的比较仍以key值为准。value只是一个对应值,附加值。

该种方式在现实生活中非常常见:

  • 比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;(示例代码:dictionary.cc)
  • 再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对。(示例代码:wordcounter.cc)

将二叉搜索树改造成KV模型:

//将二叉搜索树改造成KV模型
//只有少量改动,不变的内容省略不写。
#include <iostream>
using namespace std;template <class K, class V>
struct BSTreeNode{typedef BSTreeNode<K, V> Node;Node *_left;Node *_right;K _key;V _val; //加了一个valueBSTreeNode(const K &key = K(), const V &val = V()):_left(nullptr),_right(nullptr),_key(key),_val(val){}
}; template <class K, class V>
class BSTree{typedef BSTreeNode<K, V> Node;Node *_root = nullptr;public:bool Insert(const K &key, const V &val);Node* Find(const K &key);//......private:Node* _Copy(Node *root);//......
};template <class K, class V>
bool BSTree<K,V>::Insert(const K &key, const V &val){if(_root == nullptr){_root = new Node(key, val); //插入时要初始化valuereturn true;}//......if(key > parent->_key){parent->_right = new Node(key, val);}else{parent->_left = new Node(key, val);}return true;
}template <class K, class V>BSTreeNode<K,V>* BSTree<K,V>::Find(const K &key){if(_root == nullptr) return nullptr;Node *cur = _root;while(cur != nullptr){//......else{return cur; //找到返回节点指针,便于查看和修改value}}return nullptr;}template <class K, class V>BSTreeNode<K,V>* BSTree<K,V>::_Copy(Node *root){if(root == nullptr) return nullptr;Node *copyroot = new Node(root->_key, root->_val);  //拷贝也要复制value//......}template <class K, class V>void BSTree<K,V>::_InOrder(Node *root){if(root == nullptr) return;_InOrder(root->_left);cout << root->_key << "-->" << root->_val << endl;_InOrder(root->_right);}

KV模型的应用示例:

//dictionary.cc
#include "BSTree_KV.hpp"
#include <iostream>
#include <string>
using namespace std;int main(){BSTree<string, string> dct;dct.Insert("buffer", "缓冲器");dct.Insert("error", "错误");dct.Insert("derive", "继承自,来自");dct.Insert("soldier", "士兵");dct.Insert("column", "列");dct.Insert("row", "行");dct.InOrder();string input;while(cin >> input){BSTreeNode<string,string> *ret = dct.Find(input);if(ret != nullptr){cout << ret->_val << endl;}else{cout << "词库中无此单词" << endl;}}
}//wordcounter.cc
int main(){string tmp[] = {"orange", "orange","orange","strawberry", "pear","apple", "apple","apple","apple","peach","peach"};BSTree<string, int> counter;for(string &e : tmp){BSTreeNode<string, int> *ret = counter.Find(e);if(ret){++ret->_val;}else{counter.Insert(e, 1);}}counter.InOrder();return 0;
}

四、二叉搜索树的性能分析

插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。

对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找次数是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。时间复杂度:O(h),h是树的深度。

但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:

在这里插入图片描述

最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:log_2 N
最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:N

问题:如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插入关键码,二叉搜索树的性能都能达到最优?那么我们后续章节学习的AVL树和红黑树就可以上场了。


五、相关练习

这些题目更适合使用C++完成,难度也更大一些

  1. 二叉树创建字符串。OJ链接
  2. 二叉树的分层遍历1。OJ链接
  3. 二叉树的分层遍历2。OJ链接 (翻转“分层遍历1”中的二维数组)
  4. 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先 。OJ链接
  5. 二叉树搜索树转换成排序双向链表。OJ链接

根据前、中序或中、后序遍历构造二叉树

  1. 根据一棵树的前序遍历与中序遍历构造二叉树。 OJ链接
  2. 根据一棵树的中序遍历与后序遍历构造二叉树。OJ链接

非递归实现二叉树的前中后序遍历(重点)

  1. 二叉树的前序遍历,非递归迭代实现 。OJ链接
  2. 二叉树中序遍历 ,非递归迭代实现。OJ链接
  3. 二叉树的后序遍历 ,非递归迭代实现。OJ链接

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/110772.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

null和undefined区别

1.undefined&#xff0c;表示无值。 比如下面场景&#xff1a; a. 变量被声明了&#xff0c;但是没有被赋值&#xff1b; b. 调用函数的时候&#xff0c;应该给函数传参却没有给函数传这个参数打印出来就是 undefined&#xff1b; c. 访问一个对象中没有的属性&#xff1b;…

水论文路线图

1、定基准模型 首先找大量的论文&#xff0c;基本是顶刊顶会&#xff0c;大概率会改代码&#xff0c;且给的代码有README&#xff0c;代码有系统性&#xff0c;方便你以后改动&#xff0c;有了基准模型之后。尽力把这个基准模型的模型部分(models)给看懂&#xff0c;值域数据及…

适合本地运营的同城团购优质商家圈子小程序开发演示

很火的一款适合本地同城运营的同城团购商家圈子小程序。有很多城市都有在用这个小程序做同城资源&#xff0c;实现完美变现。 小程序功能就是将本地商家邀请入驻&#xff0c;以团购的形式出售商家产品或服务套餐。借助微信的社交属性配合同城推广员可以迅速推广起来。 对于商…

Linux内核学习(十二)—— 页高速缓存和页回写(基于Linux 2.6内核)

目录 一、缓存手段 二、Linux 页高速缓存 三、flusher 线程 Linux 内核实现了一个被叫做页高速缓存&#xff08;page cache&#xff09;的磁盘缓存&#xff0c;它主要用来减少对磁盘的 I/O 操作。它是通过把磁盘中的数据缓存到内存中&#xff0c;把对磁盘的访问变为对物理内…

解决Three.js辉光背景不透明

使用此pass canvas元素的background都能看到 不过相应的辉光颜色和背景颜色不相容的地方看起来颜色会怪一些 如图 不过如果是纯色就没什么问题了 //ts-nocheck /** Author: hongbin* Date: 2023-04-06 11:44:14* LastEditors: hongbin* LastEditTime: 2023-04-06 11:49:23* De…

RT_Thread内核机制学习(四)队列

队列 队列中每个消息块都有一个头部&#xff0c;指向下一个消息块。 消息块的内存是连在一起的&#xff0c;但是是用链表组织的。 struct rt_messagequeue {struct rt_ipc_object parent; /**< inherit from ipc_object */void *m…

go vet中的那些检测项

go vet 是 Go 语言自带的一个工具&#xff0c;用于分析 Go 代码中的常见错误和潜在问题。它可以检查代码中可能存在的各种问题&#xff0c;例如&#xff1a; 未使用的变量、函数或包 可疑的函数调用 错误的函数签名 程序中的竞态条件 错误的类型转换等 本文意图指令当前go vet所…

在 AWS 中导入 qcow2 镜像

文章目录 在 AWS 中导入 qcow2 镜像使用的格式和问题步骤概述前提条件转换镜像格式并上传至 S3创建角色并配置策略策略文件内容创建container.json配置文件导入镜像创建 AMI 并启动实例参考:在 AWS 中导入 qcow2 镜像 当我们在多云环境中部署应用时,有时候可能需要把基于 qem…

装备一台ubuntu

配置远程连接&#xff1a; ubuntu的root用户无法远程登入问题&#xff1a; openssh安装命令&#xff1a; sudo apt-get install openssh-server 安装完成通过以下命令查看SSH是否启动 ps -e | grep ssh 如果只有ssh-agent表示还没启动&#xff0c;需要&#xff1a; /etc/i…

2023高教社杯数学建模思路 - 复盘:校园消费行为分析

文章目录 0 赛题思路1 赛题背景2 分析目标3 数据说明4 数据预处理5 数据分析5.1 食堂就餐行为分析5.2 学生消费行为分析 建模资料 0 赛题思路 &#xff08;赛题出来以后第一时间在CSDN分享&#xff09; https://blog.csdn.net/dc_sinor?typeblog 1 赛题背景 校园一卡通是集…

Visual Studio软件安装包分享(附安装教程)

目录 一、软件简介 二、软件下载 一、软件简介 Visual Studio是微软公司开发的一款集成开发环境&#xff08;IDE&#xff09;&#xff0c;广泛应用于Windows平台上的应用程序和Web应用程序的开发。以下是Visual Studio软件的主要特点和功能&#xff1a; 集成开发环境&#x…

力扣:74. 搜索二维矩阵(Python3)

题目&#xff1a; 给你一个满足下述两条属性的 m x n 整数矩阵&#xff1a; 每行中的整数从左到右按非递减顺序排列。每行的第一个整数大于前一行的最后一个整数。 给你一个整数 target &#xff0c;如果 target 在矩阵中&#xff0c;返回 true &#xff1b;否则&#xff0c;返…

Spring与MyBatis集成 AOP整合PageHelper插件

目录 1.什么是集成&#xff1f; 2.Spring与MyBatis集成 3.Spring与MyBatis集成的基本配置 4.AOP整合PageHelper插件 1.什么是集成&#xff1f; 集成是指将不同的组件、框架或系统整合到一起&#xff0c;使它们可以协同工作、相互调用、共享资源等。通过集成&#xff0c;可以…

linux系统(centos、ubuntu、银河麒麟服务、uos、deepin)判断程序是否已安装,通用判断方法:适用所有应用和命令的判断

前言 项目中需要判断linux服务器中是否已经安装了某个服务 方法有很多种&#xff0c;但是很多都不通用&#xff0c; 脚本代码就不容易做成统一的 解决方案 用下面的脚本代码去进行判断 用jdk测试 脚本意思如下&#xff1a; 输入java -version命令&#xff0c;将返回的字…

ES6中promise的使用

ES6中promise的使用 本文目录 ES6中promise的使用基础介绍箭头函数function函数状态 原型方法Promise.prototype.then()Promise.prototype.catch() 静态方法Promise.all()Promise.race()Promise.any() 链式回调 基础介绍 官网&#xff1a;https://promisesaplus.com/ window.…

Tomcat和Servlet基础知识的讲解(JavaEE初阶系列16)

目录 前言&#xff1a; 1.Tomcat 1.1Tomcat是什么 1.2下载安装 2.Servlet 2.1什么是Servlet 2.2使用Servlet来编写一个“hello world” 1.2.1创建项目&#xff08;Maven&#xff09; 1.2.2引入依赖&#xff08;Servlet&#xff09; 1.2.3创建目录&#xff08;webapp&a…

高职教育应对ChatGPT应用的策略

一、完善顶层设计&#xff0c;提升技术水平 在推广ChatGPT平台的过程中&#xff0c;高职院校需要关注技术本身的问题。这就需要在国家和地方政府的引导下&#xff0c;引入更完善的技术顶层设计&#xff0c;提高人工智能在高职教育中的运用水平。具体来说&#xff0c;一方面需要…

linux 设置与命令基础(二)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 前言 一、系统基本操作 二、命令类型 三、命令语法 四、命令补齐 五、命令帮助 六、系统基本操作命令 总结 前言 这是本人学习Linux的第二天&#xff0c;今天主…

高频电流探头的钳口使用和如何调零消磁

高频电流探头采用霍尔效应传感器技术来测量交流和直流信号&#xff0c;标配通用的 BNC 接口&#xff0c;可直接用示波器或记录仪等观察测量波形及数值&#xff0c;具有强大的通用性能。 电流探头钳口使用&#xff1a; 为电流指示方向。测量时&#xff0c;被测导体电流方向与指…

Matlab图像处理-平移运算

几何运算 几何运算又称为几何变换&#xff0c;是将一幅图像中的坐标映射到另外一幅图像中的新坐标位置&#xff0c;它不改变图像的像素值&#xff0c;只是改变像素所在的几何位置&#xff0c;使原始图像按照需要产生位置、形状和大小的变化。 图像几何运算的一般定义为&#…