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

二叉查找树详解

目录

二叉查找树的定义

二叉查找树的基本操作

查找

插入

建立

删除

二叉树查找树的性质

二叉查找树的定义

二叉查找树是一种特殊的二叉树,又称为排序二叉树、二叉搜索树、二叉排序树。

二叉树的递归定义如下:

(1)要么二叉查找树是一棵空树。

(2)要么二叉查找树由根节点、左子树、右子树组成,其中左子树和右子树都是二叉查找树,且左子树上所以结点的数据域均小于或等于根节点的数据域,右子树上的所有结点的数据域均大于根节点的数据域。

二叉查找树的基本操作

查找

进行二叉树的查找操作时,由于无法确定二叉树的具体特性,因此只能对左右子树都进行递归遍历。但是二叉查找树的性质决定了可以只选择一棵子树进行遍历。因此查找将会是从根节点查找的一条路径,故最坏时间复杂度是O\left ( h \right ),其中h是二叉查找树的高度。于是就可以得到查找操作的基本思路:

(1)如果当前根节点root为空,说明查找失败,返回。

(2)如果需要查找的x等于当前根节点的数据域root->data,查找成功,访问。

(3)如果需要查找的x小于当前根节点的数据域root->data,说明应该往左子树查找,因此向root->lchild递归。

(4)如果需要查找的x大于当前结点的数据域root->data,则应该往右子树查找,因此向root->rchild递归。

void search(node* root,int x){if(root==NULL){printf("search failed\n");return;}if(x==root->data){printf("%d\n",root->data);}else if(x<root->data){search(root->lchild,x);}else{search(root->rchild,x);}
}

可以看到,和普通二叉树的查找函数不同,二叉查找树的查找在于对左右子树的选择递归。在普通二叉树中,无法确定需要查找的值x到底是在左子树还是右子树,但是在二叉查找树中就可以确定,因为二叉查找树中的数据域顺序总是左子树<根节点<右子树。

插入

对一棵二叉查找树来说,查找某个数据域的结点一定是沿着确定的路径进行的。因此,当对某个需要查找的值在二叉查找树中查找成功,说明结点已经存在。反之,如果这个需要查找的值在二叉查找树中查找失败,那么说明查找失败的地方一定是结点需要插入的地方。因此可以在上面查找操作的基础上,在root==NULL时新建需要插入的点。

void insert(node* &root,int x){if(root==NULL){root=newNode[x];return;}if(x==root->data){return;}else if(x<root->data){insert(root->lchild,x);}else{insert(root->rchild,x);}
}

建立

node* Create(int data[],int n){node* root=NULL;for(int i=0;i<n;i++){insert(root,data[i]);}return root;
}

需要注意的是,即使是一组相同的数字,如果插入它们的顺序不同,最后生成的二叉查找树也可能不同。

删除

把二叉查找树中比结点权值小的最大结点称为该结点的前驱,而把比结点权值大的最小结点称为该结点的后继。显然,结点的前驱是该结点左子树的最右结点(也就是从左子树根节点开始不断沿着rchild往下直到rchild为NULL时的结点),而结点的后继则是该结点右子树的最左节点(也就是从右子树根节点开始不断沿着lchild往下直到lchild为NULL时的结点)。下面两个函数用来寻找以root为根的树中最大或最小权值的结点,用以辅助寻找结点的前驱和后继:

//寻找以root为根结点的树中的最大权值结点 
node* findMax(node* root){while(root->rchild!=NULL){root=root->rchild;}return root;
}
//寻找以root为根结点的树中的最小权值结点
node* findMin(node* root){while(root->lchild!=NULL){root=root->lchild;}return root;
} 

删除操作的基本思路如下:

(1)如果当前结点root为空,说明不存在权值为给定权值x的结点,直接返回。

(2)如果当前结点root的权值恰为给定的权值x,说明找到了想要删除的结点,此时进入删除处理。如果当前结点root不存在左右孩子,说明是叶子节点,直接删除。如果当前结点root存在左孩子,那么在左子树中寻找结点前驱pre,然后让前驱的数据覆盖root,接着在左子树中删除结点pre。如果当前结点root存在右孩子,那么在右子树中寻找结点后继next,然后让next的数据覆盖root,接着在右子树中删除结点next。

(3)如果当前结点root的权值大于给定权值的x,则在左子树中递归删除权值为x的结点。

(4)如果当前结点root的权值小于给定权值的x,则在右子树中递归删除权值为x的结点。

//寻找以root为根结点的树中的最大权值结点 
node* findMax(node* root){while(root->rchild!=NULL){root=root->rchild;}return root;
}
//寻找以root为根结点的树中的最小权值结点
node* findMin(node* root){while(root->lchild!=NULL){root=root->lchild;}return root;
} 
void deleteNode(node* &root,int x){if(root==NULL){return;}if(root->data==x){if(root->lchild==NULL&&root->rchild==NULL){root==NULL;}else if(root->lchild!=NULL){node* pre=findMax(root->lchild);root->data=pre->data;deleteNode(root->lchild,pre->data);}else{node* next=findMin(root->rchild);root->data=next->data;deleteNode(root->rchild,next->data);}}else if(root->data>x){deleteNode(root->lchild,x);}else{deleteNode(root->rchild,x);}
}

但是也要注意,总是优先删除前驱或后继容易导致树的左右子树高度极度不平衡,使得二叉查找树退化成一条链。解决这一问题的办法有两种:一种是每次交替删除前驱或后继;另一种是记录子树高度,总是优先在高度较高的一棵子树里删除结点。

二叉树查找树的性质

二叉查找树一个实用的性质:对二叉查找树进行中序遍历,遍历的结果是有序的

另外,如果合理调整二叉查找树的形态,使得树上的每个结点都尽量有两个子节点,这样整个二叉查找树的高度就会很低,也即树的高度大概在logN的级别。

例题

给出N个正整数来作为一棵二叉排序树的结点插入顺序,问:这串序列是否是该二叉排序树的先序序列或是该二叉排序树的镜像树的先序序列。所谓镜像树是指交换二叉树的所有结点的左右子树而形成的树(也即左子树所有结点数据域大于或等于根节点,而根节点数据域小于右子树所有结点的数据域)。如果是,则输出YES,并输出对应的树的后序序列;否则,输出NO。

思路

通过给定的插入序列,构建出二叉排序树。对镜像树的先序遍历只需要在原树的先序遍历时交换左右子树的访问顺序即可。

//镜像树先序遍历,结果存放于vi
void preordermirror(node* root,vector<int>&vi){if(root==NULL){return;}vi.push_back(root->data);preordermirror(root->right,vi);preordermirror(root->left,vi);
} 

注意点

使用vector来存放初始序列、先序序列、镜像树先序序列,可以方便相互之间的比较。若使用数组,则比较操作需要使用循环才能实现。

#include<cstdio>
#include<vector>
using namespace std;
struct node{int data;node* left,*right;
}; 
vector<int> origin,pre,preM,post,postM;
void insert(node* &root,int data){if(root==NULL){root=new node;root->data=data;root->left=root->right=NULL;return;}if(data<root->data){insert(root->left,data);}else{insert(root->right,data);}
}
void preorder(node* root,vector<int> &vi){if(root==NULL){return;}vi.push_back(root->data);preorder(root->left,vi);preorder(root->right,vi);
}
void preordermirror(node* root,vector<int> &vi){if(root==NULL){return;}vi.push_back(root->data);preordermirror(root->right,vi);preordermirror(root->left,vi);
}
void postorder(node* root,vector<int> &vi){if(root==NULL){return;}postorder(root->left,vi);postorder(root->right,vi);vi.push_back(root->data);
}
void postordermirror(node* root,vector<int> &vi){if(root==NULL){return;}postordermirror(root->right,vi);postordermirror(root->left,vi);vi.push_back(root->data);
}
int main(){int n,data;node* root=NULL;scanf("%d",&n);for(int i=0;i<n;i++){scanf("%d",&data);origin.push_back(data);insert(root,data);}preorder(root,pre);preordermirror(root,preM);postorder(root,post);postordermirror(root,postM);if(origin==pre){printf("YES\n");for(int i=0;i<post.size();i++){printf("%d",post[i]);if(i<post.size()-1){printf(" ");}}}else if(origin==preM){printf("YES\n");for(int i=0;i<postM.size();i++){printf("%d",postM[i]);if(i<postM.size()-1){printf(" ");}}}else{printf("NO\n");return 0;}
}
http://www.lryc.cn/news/367093.html

相关文章:

  • 3072. 将元素分配到两个数组中 II
  • 城市之旅:使用 LLM 和 Elasticsearch 简化地理空间搜索(二)
  • 【知识点】 C++ 构造函数 参数类型为右值引用的模板函数
  • 华为云服务器-云容器引擎 CCE环境构建及项目部署
  • Linux shell编程学习笔记57:lshw命令 获取cpu设备信息
  • 连山露【诗词】
  • 【Qt】Frame和Widget的区别
  • Python爬虫实战:从入门到精通
  • 堆算法详解
  • 6.6SSH的运用
  • MySQL-备份(三)
  • 结构体(1)<C语言>
  • HW面试应急响应之场景题
  • 30-unittest生成测试报告(HTMLTestRunner插件)
  • 鸿蒙北向开发 IDE DevEco Studio 3.1 傻瓜式安装闭坑指南
  • Oracle数据库面试题-9
  • 跟着小白学linux的基础命令
  • 2024-06-08 Unity 编辑器开发之编辑器拓展9 —— EditorUtility
  • Mac下删除系统自带输入法ABC,正解!
  • redis学习路线
  • 数据库练习题
  • 【每日一函数】uname 函数介绍及代码演示
  • linux:命令别名,文件描述符及重定向
  • 前端开发之中svg图标的使用和实例
  • BeagleBone Black入门总结
  • 笔记:Mysql的安全策略
  • AI绘画中的图像格式技术
  • 前端如何封装自己的npm包并且发布到npm注册源
  • vue油色谱画 大卫三角形|大卫五边形|PD图
  • 【React】前端插件 uuidjs 的使用 --随机生成id