二叉搜索树
引入原因
在查找的过程中,为了尽可能的节省时间成本,往往会使用二分查找的思想。而数组和链表作为线性结构,都各自存在自己的缺陷。
首先二分查找只能应用于有序的的一串数字中。排序便会消耗一部分的时间。
数组受限于长度是确定的,如果数据量大于数组本身的容量,那么便要重新新建一个数组并将原来的值都迁移到新的数组中。在数据量庞大时会耗费很多时间。
链表受限于在释放和查找时必须遍历整个链表找到结尾的节点。且指针过多不易操作。
因此需要引入一种树形的结构,使他在创建时便已经形成了一种顺序,并且符合二分的思想,由此便产生了搜索二叉树。
二叉树搜索树的基本概念
如图:
每个节点的左孩子应该小与右孩子,且要保证根节点的左子树均小于右子树的所有值。
二叉搜索树的相关操作以及与递归的关系
构建节点的结构体:
typedef struct TreeNode
{
int data;
struct TreeNode * left;
struct TreeNode * right;
}SearchTree;
创建新节点:
SearchTree * getnewnode(int data)
{
SearchTree *T = NULL;
T = (SearchTree *)malloc(sizeof(SearchTree));
T->data = data;
T->right = NULL;
T->left = NULL;
return T;
}
插入节点:
SearchTree* insertTree(SearchTree * T, int data)
{
// 如果为空就创建一个节点并令根指针指向它
if (T == NULL){
T=getnewnode(data);
printf("%d插入树中\n",data);
return T;
}
//若不为空则进入递归调用找到树的尾端并将他与新创建的节点连接起来
if (T->data > data){
T->left = insertTree(T->left, data);
}else{
if(T->data < data){
T->right = insertTree(T->right, data);
}else{
printf("%d已在树中,不能再次插入!!!\n",data);
}
}
return T;
}
递归的原理:
例如第一插入15,此时T为NULL,所以直接创建data为15的新节点并让T指向它
在此之后。若再次插入10,10此时小于15,则会再次调用函数,此时T指向15所在的节点,它的左节点为空,进入第一个if
语句返回新创建的节点的地址将左指针与新建立的节点所在的节点连接在一起。此时返回上一级,将T的值返回。
每次插入新值时都会从T所指向的根节点逐级向下寻找直到节点为空,此时将新建立节点的位置返回并使其与上一级的父节点连接,之后则逐级返回这一级的位置,最终T又回到了原来根节点的位置。
插入时函数栈的调用:
在未到叶节点时函数会不断递归调用,当到叶节点10的时候,此时10的左右指针均指向空,此时便会结束递归并返回8所在的位置使10的左指针与8所在的位置相连。接着会逐层返回本层节点的位置直到最后T又回到了原来根节点的位置
查找指定数据:
int Search(SearchTree * root,int a)
{
if(T==NULL){
return T;
}else{
if(T->data==data){
return T;
}else{
if(data<=T->data){
return Search(T->left, data);
}else{
return Search(T->right, data);
}
}
}
}
寻找最大值与最小值:
递归的方式:
// 寻找树的最小节点
SearchTree* FindMin(SearchTree * T)
{
if (T == NULL)
{
printf("该树为空");
}
if (T->left != NULL)
{
return FindMin(T->left);
}
return T;
}
// 寻找树的最大节点
SearchTree* findMax(SearchTree * T)
{
if (T == NULL)
{
printf("该树为空");
}
if (T->right != NULL)
{
return findMax(T->right);
}
return T;
}
循环方式:
SearchTree* findMax2(SearchTree * T)
{
SearchTree* p=T;
while(p->right!=NULL){
p=p->right;
}
return p;
}//最小值同理
计算二叉树的深度
int deepfind(SearchTree * T)
{
if(T==NULL){
return -1;
}else{
return max(deepfind(T->left),deepfind(T->right))+1;
}
}
层次遍历——广度优先
在按层遍历时,使用队列来储存每个节点的位置,这样的这样的结构更符合使用时的需求。
在一个节点被使用过后就先将他的左右孩子节点的位置储存到队列当中去这样在下一次循环的时候就可以通过访问队首的元素来得知孩子节点的位置
如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X0dfDAhn-1672984243461)(/home/wlx/图片/博客用图/bianki.png)]
void Leveolder(SearchTree * T,Queue *q)
{
SearchTree *t=T;
if(t==NULL){
printf("该树为空\n");
return;
}
InQueue(t,q);
while(EmptyQueue(q)==0){
t=DeQueue(q);
printf("%d\n",t->data);
if(t->left!=NULL){
InQueue(t->left,q);
}
if(t->right!=NULL){
InQueue(t->right,q);
}
}
}
前中后序遍历——深度优先
//前序
void Preorder(SearchTree * T)
{
if(T==NULL){
return;
}
printf("%d",T->data);
Preorder(T->left);
Preorder(T->right);
}
//中序
void Preorder(SearchTree * T)
{
if(T==NULL){
return;
}
Preorder(T->left);
printf("%d",T->data);
Preorder(T->right);
}
//后序
void Preorder(SearchTree * T)
{
if(T==NULL){
return;
}
Preorder(T->left);
Preorder(T->right);
printf("%d",T->data);
}
删除一个节点
在调用删除节点的函数前要先调用search函数找到要删除的数字所在的位置
当待删除节点为叶节点或只有一个子节点时,此时删除一个节点的操作较为简单,便不赘述
当待删除节点有两个子节点时情况较为复杂,此时我们需要先在左子树中找到最大的,或在右子树中找到最小的数,将该数填入到待删除节点的位置上,再将此节点删除(该节点一定为叶节点)以此起将该问题简化为删除一个叶节点的问题
如图:
SearchTree* Delste(SearchTree * T)
{
if(T==NULL){
return T;//当T为空时直接返回T的地址
}else{
if(T->left==NULL&&T->right==NULL){
free(T);
T=NULL;//当节点没有子节点的时候直接删除该节点,并置空
}else{
if(T->right==NULL){
//当有一个子节点时,删除该节点并将子节点的位置赋给该节点
SearchTree *temp=T;
T=T->left;
free(temp);
}else{
if(T->left==NULL){
SearchTree *temp=T;
T=T->right;
free(temp);
}else{
SearchTree *temp=findMax2(T->left);
T->data=temp->data;
T->left=Delste(T->left);//递归调用,使待删除节点父节点的指针指向空。此处若直接释放找到的替代节点,会使该节点的父节点出现指针随机乱指的情况
}
}
}
return T;//最后逐级返回每一层节点的位置并使其连接
}
}