今天我们的主题是二叉搜索树也有叫二叉排序树
还是和以前一样,我们谈某个技术之前先谈谈出现的理由:
二叉搜索树那么到底是什么,做什么的,根据字面意思其实就是使用特定二叉树的特性去查找树里的某个元素。
我们先来谈谈查找的概念: 查找分静态查找和动态查找
静态查找:比如我们在字典里找个单词,所谓静态就去查找元素的时候,并不会去改集合,比如我查找单词,假如字典里没有想要的单词,你是不会把你这个单词插入到字典里面的。我们在数组里面的查找也是静态查找,因此数组分配内存以后就不可能动态分配内存了。
动态查找:看到这个动了吗,和上面刚好不同,假如我去集合当中去查找一个元素,如果没有我们就插入到集合里。由于这个操作,那么这个集合就必须可以动态申请内存才可以。
二叉树有什么特性呢 我直接给图如果搞不懂就需要先搞定二叉树在继续学
根据上面图我们知道二叉排序树有3个名称的。简称BST Binary Search Tree
性质: 假如二叉树不为空。
- 节点大于左孩子节点但是要小于其右孩子节点
如图就是棵二叉排序树
那么我们想想,假如我们在这棵树上查找元素33,
那么我们先得到的是根据节点30, 这个时候由于30的右节点全部大于30的,因此我们就可以往右子树继续查找这个时候到了41这个根节点对应的子树,又和41比较 小于41的,因此我们在左子树去找, 刚好是33 找到了。 发现我们查找递归调了3次。 大家发现没有,查找的次数和树的高度相同刚好是3 ,
因此我们的查询效率和树的高度有关。
其实我们可以用数组去组织数据,然后在生成排序二叉树,在查找,但是这样的查找是静态的。不方便。
我们要实现动态的查找,就是假如在二叉树中没有找到数据,我们就插入进去,并且插入之后还是排序二叉树。
因此我们就引出了我们的主题二叉树排序。
现在我们知道了什么是二叉排序树,以及查找效率,对了这个效率假如我们和顺序表比较我们是一一去对比的话时间复杂度是0(n) 但是是二叉树中是0(log2^n) 很明显效率提高很大 特别是数据量增多的情况更加明显。
现在我们来具体谈二叉树排序树的相关操作:
二叉排序数的查找操作
//Postion:是元素的地址 x是待查找的元素 BST:是二叉树的根节点地址
//我们用的递归 递归比较容易理解 代码少,但是容易出现栈溢出,溢出不适合数据量大的情况,
//并且由于每次递归的时候需要保存现场为了下次可以返回,因此比较浪费性能
//保存现场环境其实是用了栈数据结构来入栈和出栈操作,而相比循环来查找可能就没有这个入栈出栈操作
Positon Find(ElementType x, BinTree BST)
{
//需要提前判断是不是空树,否则执行任何代码都是浪费计算机资源
if(BST == null) return; //空树就退出当前查找操作
//进入了非空情况的操作
//现在我们是拿到了根节点的,因此我们可以开始比较数据了
if(BST->data == x) //运气好,测试就是当前节点 那么就返回当前节点地址
{
return BST;
}
else if(BST->data > x) //比较发现我们的数据小于当前节点里面的数据,因此我们需要去左子树找
{
return Find(x,BST->lChild); //把左子树里找到的数据地址返回出来
}
else
{
return Find(x,BST->rChild); // 把x传给右子树,并且在右子树中去比较
}
}
我们发现上面查找操作是用递归的,我们都知道递归只是代码容易些和比较好理解,但是性能比较差。
因此我们用迭代模式去查找。
**
//采用迭代去查找元素 也就是循环
Position IterFind(ElementType x, BinTree BST)
{
//因此比较操作是重复性的,只是数据不同,代码一样。我们直接来个循环
while(BST) //BST必须不为null,也就是你给我的子树节点是不为空的,比如叶子节点就没有子树。
{
//开始比较操作
if(BST->data == x) //找到了数据节点
{
return BST;
}
else if(BST->data > x)
{
//当前需要查找的元素x比当前节点数据小,说明在二叉树的当前节点的左子树那边 当中
BST = BST->lChild; //执行这个代码我们就转向了当前节点的左子树了
}else
{
//说明查找节点在当前节点的右子树上
BST = BST->rChild; //转向右子树
}
}
//如果代码还执行到这里只能一个情况就是没有在循环里return操作也就是没有找到,并且已经BST== null.说明已经查找完全部的节点。那么我们就返回一个空地址表示没有找到。
return null;
}
顺便分析下查找效率问题,我们已经知道被查找的节点在二叉树的高度中越深查找次数就越多,因此我们的元素的在树中的哪层是影响查找的效率的。
**
现在我们来实现查找二叉树中的最大和最小元素操作。
那么我必须先了解二叉排序树的特性。
我还是先上图:
大家看图观察,是不是最小的那个元素一定在左子树最左边那个元素,并且这个节点一定是某个左孩子节点或者是根节点。
最大的你那个节点是右节点最底层那个元素。
先实现递归查找最小元素算法
Position FindMin(ElementType x, BinTree BST)
{
//还是判空树
if(BST == null) return null;
//我们必须使用二叉树的性质
//因此我们使用递归找到左边最底层那个节点就是最小值
else if( !BST->lChild) //判断当前节点有没有左孩子节点 没有就找到了
{
//说明当前节点就是最底层节点 因为没有子树了
return BST;
}else
{
return FindMin(x, BST->lChild); //递归调用,把当前节点左子树传进去
}
}
//迭代查找最大值
Position FindMax(ElementType x, BinTree BST)
{
if(BST == null) return null; //空树
while(BST)
{
BST = BST->rChild; //得到当前节点的右孩子
}
// 代码执行到这来的时候说明当前节点没有右子树了,因此我们直接返回
return BST;
}
二叉排序树的插入算法实现
如图:当我们插入元素35到二叉排序树中的之后是图二所示;
因此从上图我们知道插入元素35的时候,由于我们只拿到这棵树的根节点,因此我们需要从30这个节点开始查找到一个位置,并且这个位置插入后还是满足二叉排序树的。
分析: 我们先和30比较 35大,因此我们就到右子树中找,我们就和41比较。35小,因此我们就跑左子树树去找位置。
这个时候我们找到33这个节点。 当然如果我们插入的元素里面有就不需要重复插入了。35大于33 ,因此我在判断33有没有右子树,有的话我继续去右子树找,没有的话我就把35挂在33的右指针域就搞定。
//现在我们来实现用递归算法实现插入一个元素到二叉排序树当中
//规则:我们假如插入的元素已经存在二叉排序树当中的时候,我们就把当前节点的地址返回出来
//BinTree是节点的指针
//注意我们算法为了简单,我们这个算法是针对上图来说的,也就是我们插入的元素一定是叶子的情况下写的算法。
BinTree Insert(ElementType x, BinTree BST)
{
//判断是不是到了叶子节点位置,到了的话说明找到了待插的位置
if(BST == null)
{
//生成节点数据
BST = (BST)malloc(sizeof(*BST)); //把当前新节点挂在当前节点的指针域中
//开始存数据
BST->data = x;
//清空指针域,表示叶子节点
BST->lChild = BST->rChild = null;
}else if(BST->data > x) //当前插入的节点小于当前访问的节点 那么我们就去左子树里找位置
{
BST->lChild = Insert(x,BST->lChild); //递归调用,
}else //当前插入的节点大于当前访问的节点 那么我们就去左子树里找位置
{
BST->rChild = Insert(x,BST->rChild); //递归调用,
}
return BST; //说明跑了null代码 找到了插入节点位置那么我们就直接返回地址
}
二叉排序树的删除操作代码实现
二叉树删除操作比较复杂需要分析删除节点在不同的位置写不同的代码:
先上图;
首先删除的是叶子节点的情况下。比较简单我们知道修改父节点指针域置空,然后在删除当前节点,释放其他咱有的内存。
分析删除过程:先和30比较 35大,那么就转到右子树上和41比较 35小 在转41的左子树 和33比较 35大 因此在转33的右子树,通过判断右子树,35 刚好等于,找到了,开始删除。
先上图看第二种情况的节点
假如我们删除的不是叶子节点,比如33 ,并且只有一个孩子节点。
那么先找到这个被删的节点的父地址。 然后直接把父地址指向他的孩子节点的孩子节点这里也就是35, 41节点中的左指针直接保存了35节点的地址,然后在删除33这个节点
删除后的图如上:
第三种情况就比较复杂:
根据上图我们发现删除节点41他是既有左子树又有右子树的,
根据二叉排序树的特性:就是保证插入或者删除节点后父节点永远是大于左子树根节点和小于右子树根节点。
那么如果我们根据上图把41这个节点删除,必须调整二叉排序树让其还是满足二叉排序树,那么我们有2个方案
方案1:我们从41这个节点中的右子树找到一个最小的节点放在当前位置,然后删除41节点的内存,搞定
如图删除后的效果;
方案2:就是在左子树找一个最大的节点放在当前被删节点的位置。想想对不对。
被删后的效果图:
当然这个图中也有要保证被删后左子树也要保持二叉排序树的特性。
现在我们在前面实现函数的基础上实现看起来复杂其实也简单的算法;
针对有2个孩子节点的删除算法代码贴上:
//BinTree是二叉树节点的指针也就是用来保存二叉树节点的地址用的,我们就要返回删除节点的地址
//x删除的元素x BST二叉树根节点地址 BST 都二叉搜索树的三个单词的第一个字母binary search tree
BinTree Delete(ElementType x, BinTree BST)
{
BinTree temp; // 保存临时的节点元素
//首先做判空操作
if(BST == null) return null; //没有找到元素
//开始真正查找操作
if(BST->data > x) //查找的节点在当前节点的左子树上,需要递归
{
BST->lChild = Delete(x,BST->lChild); //左子树递归删除
}else if(BST->data < x)
{
BST->rChild = Delete(x,BST->rChild); //右子树递归删除
}
esle //找到了要删除的节点元素了
{
//判断当前节点是不是有2个孩子节点的情况单独考虑
if(BST->lChild != null && BST->rChild != null) //有左右2个孩子的情况下
{
//这样的话我们假如在当前节点的右子树上找到最小的那个元素来替换删除节点的位置。其实就是
//找到这个节点把里面的数据写进入,然后在删除这个节点。
temp = FindMin(BST->data,BST->rChild);
//开始替换数据操作
BST->data = temp->data;
//开始删除当前数据元素对应的节点
BST->rChild = Delete(BST->data,BST->rChild);
}
else //没有2个孩子节点的情况
{
//保存当前节点
temp = BST; //为了后面可以删除当前节点
//假如只有左孩子的情况下
if(BST->lChild)
{
BST = BST->lChild;
}
else if(BST->rChild)
{
BST = BST->rChild;
}
//开始释放待删节点内存
free(temp);
}
}
return BST; //返回删除节点的地址
}