搜索二叉树的概念
搜索二叉树满足下面两个要求:
(1)它是一棵二叉树
(2)该二叉树中,任意一棵树的根节点值大于它左子树中的所有结点的值,小于右子树中的所有结点的值
因此对于搜索二叉树的中序遍历来说,它是按由小到大依次递增的顺序排列的。
搜索二叉树的相关操作
在本文中将介绍搜索二叉树的以下操作:
(1)初始化搜索二叉树
(2)在搜索二叉树中插入指定元素(递归实现)
(3)在搜索二叉树中插入指定元素(非递归实现)
(4)在搜索二叉树中查找指定元素(递归实现)
(5)在搜索二叉树中查找指定元素(非递归实现)
(6)在搜索二叉树中删除指定元素(递归实现)
(7)在搜索二叉树中删除指定元素(非递归实现)
1. 搜索二叉树的表示
搜索二叉树本质上也是一棵二叉树,所以与二叉树的表示方法相同,可以根节点的指针来表示一棵搜索二叉树。因此,搜索二叉树的结点结构也与二叉树表示相同,可以利用孩子表示法来实现,至于各节点之间的大小对应关系,应在具体插入和删除时来保证。所以结点的结构表示如下:
//定义二叉搜索树的节点结构:孩子表示法
typedef char SearchTreeType;
typedef struct SearchTreeNode
{
SearchTreeType data;
struct SearchTreeNode* lchild;
struct SearchTreeNode* rchild;
}SearchTreeNode;
2. 搜索二叉树的初始化
搜索二叉树的结点结构定于与普通二叉树相同,因此在初始化时也相同。直接将根节点的指针置为空,来表示一棵空的搜索二叉树。
因为要对根节点的指针进行置空,即修改指针的值/指向,所以,这里在传参时应传递二级指针。
//初始化二叉搜索树
void SearchTreeInit(SearchTreeNode** proot)
{
if(proot == NULL)
{
//非法输入
return;
}
*proot = NULL;
return;
}
3. 在搜索二叉树中插入节点(递归实现)
要使在插入元素之后,依然为搜索二叉树,因此还必须满足搜索二叉树的第二个约束条件。所以对指定元素进行插入是,该结点所在的位置应当是固定的。
首先指定结点应作为某个节点的左孩子或右孩子。如果作为左孩子,则该结点的左子树必须为空,如果作为右孩子,则该节点的右子树必须为空。
其次作为右子树还是左子树,应根据指定元素与该结点的值进行大小判断,如果指定元素小于该结点的值,则作为该结点的左子树,如果大于,则作为该结点的右子树。
注意:这里规定搜索二叉树中的结点元素值是不重复的,如果新元素值已在树中存在,则不进行插入。
所以,需要不断的寻找满足上述两个要求的结点,思路如下:
(1)如果跟结点为空,则直接将根节点的指针指向新节点
(2)如果根节点不为空,则递归遍历寻找至少有一个子树为空的结点。
a)如果指定元素的值小于根节点的值,则递归遍历左子树,在左子树中寻找满足条件的值
b)如果指定元素的值大于根节点的值,则递归遍历右子树,在右子树中寻找满足条件的值
c)如果指定元素的值等于根节点的值,则直接返回,不进行插入。
代码如下:
//在二叉搜索树中插入元素
void SearchTreeInsert(SearchTreeNode** proot,SearchTreeType value)
{
if(proot == NULL)
{
//非法输入
return;
}
SearchTreeNode* new_node = CreateNode(value);
if(*proot == NULL)
{
//插入之前树为空,则将根节点的指针指向新创建的节点
*proot = new_node;
return;
}
//插入之前树不为空,则从根节点开始遍历查找应该插入的位置
//如果新节点的值小于根节点的值,则新节点应插入到左子树中
if(value < (*proot)->data)
{
//然后递归遍历左子树进行插入
SearchTreeInsert(&(*proot)->lchild,value);
}
//如果新节点的值大于根节点的值,则新节点应插入到右子树中
else if(value > (*proot)->data)
{
//然后递归遍历右子树进行插入
SearchTreeInsert(&(*proot)->rchild,value);
}
else
{
//如果相等,作如下规定:二叉搜索树中不允许有相等的节点
//此时,直接返回即可
return;
}
return;
}
当搜索二叉树为单支树时,需遍历整棵树中的结点,所以该函数的时间复杂度为:O(n)。
4. 在搜索二叉树中插入指定元素结点(非递归实现)
在非递归实现中的思路与上述递归实现思路相同,只是在代码实现上略有不同。
(1)如果根节点为空,则使跟结点的指针指向新节点,结束操作
(2)如果根节点不为空,则将根节点作为当前节点cur,一层一层往下循环遍历寻找满足条件的结点
a)如果指定元素比根节点值小,则将当前节点cur替换为原cur的左子树,在左子树中继续循环寻找
b)如果指定元素比根节点大,则价将当前节点cur替换为原cur的右子树,在右子树中继续循环寻找
(3)当遍历到某一个结点时,
a)如果该结点的左子树为空,且指定元素小于该结点的值,则将新节点作为该结点的左子树
b)如果该结点的右子树为空,且指定元素大于该结点的值,则将新结点作为该结点的右子树
c)如果指定元素等于该结点的值,则直接结束返回,不进行插入。
代码如下:
//在二叉搜索树中插入指定元素(非递归)
void SearchTreeInsertByLoop(SearchTreeNode** proot,SearchTreeType value)
{
if(proot == NULL)
{
//非法输入
return;
}
//创建新节点
SearchTreeNode* new_node = CreateNode(value);
//如果根节点为空,直接使根节点的指针指向新节点
if(*proot == NULL)
{
*proot = new_node;
return;
}
//从根节点开始循环遍历寻找新节点要插入的节点,该节点的左右子树必须至少有一个为空,
//且与新节点的大小关系满足搜索二叉树的定义
//在插入新节点时,要一层一层的往下遍历直到找到可以插入的节点
SearchTreeNode* cur = *proot;
while(1)
{
//如果某个节点的左子树为空,且value的值小于该节点的值
//将新节点作为该节点的左子树
if(cur->rchild == NULL && value > cur->data)
{
cur->rchild = new_node;
break;
}
//如果某个节点的左子树不为空,且value的值小于该节点的值
//则在该节点的左子树中继续循环遍历查找
if(value < cur->data)
{
cur = cur->lchild;
}
//如果某个节点的右子树不为空,且value的值大于该节点的值
//则在该节点的右子树中继续循环遍历查找
else if(value > cur->data)
{
cur = cur->rchild;
}
//如果遇到与某个节点的值相等,则直接结束,不进行插入
else
{
DestroyNode(new_node);//释放新创建的节点
break;
}
}
return;
}
5. 在二叉树中查找指定元素(递归)
在上述的插入操作中,其实已经包含该步操作。当遍历到一个结点,就将指定元素与该结点的值进行比较。思路如下:
(1)如果该树给空,则查找失败
(2)如果树不为空,则比较跟结点与指定元素的大小
a)如果指定元素比跟结点值小,此时需要在跟结点的左子树中寻找,因此需要递归遍历左子树
b)如果指定元素比跟结点大,此时需要在跟结点的右子树中寻找,因此需要递归遍历右子树
c)如果相等,则直接返回根节点的指针。
代码如下:
//在二叉搜索树中查找指定元素
SearchTreeNode* SearchTreeFind(SearchTreeNode* root,SearchTreeType to_find)
{
if(root == NULL)
{
//空树
return NULL;
}
//首先判断根节点处的值与所要查找元素的大小关系,如果相等,则直接返回根节点的指针
if(root->data == to_find)
{
return root;
}
else if(to_find < root->data)//如果要查找节点的值小于根节点的值
{
//则在左子树中递归遍历查找
return SearchTreeFind(root->lchild,to_find);
}
else//如果要查找节点的值大于根节点的值
{
//则在右子树中递归遍历查找
return SearchTreeFind(root->rchild,to_find);
}
}
6. 在二叉树中查找指定元素(非递归)
非递归实现时与上述递归实现思路相同,
(1)判断跟节点是否为空,为空,则查找失败
(2)不为空,则将根节点作为当前节点cur,
a)如果指定元素小于根节点的值,则将cur更新为cur的左子树,循环(2)
b)如果指定元素大于根节点的值,则将cur更新为cur的右子树,循环(2)
c)如果指定元素等于根节点的值,则返回cur的值
d)如果遍历到cur为空时还没找到,说明要查找的元素不存在,直接跳出循环返回即可。
代码如下:
//在二叉搜索树中查找指定元素(非递归)
SearchTreeNode* SearchTreeFindByLoop(SearchTreeNode* root,SearchTreeType to_find)
{
if(root == NULL)
{
//空树
return NULL;
}
//从根节点开始遍历查找,待找到某个节点
SearchTreeNode* cur = root;
while(1)
{
//如果遍历到某个节点时,该节点为空,则说明要查找的值不存在
if(cur == NULL)
{
break;
}
//如果要查找节点的值小于根节点的值,则在左子树中循环遍历查找
if(to_find < cur->data)
{
cur = cur->lchild;
}
//rug要查找节点的值大于根节点的值,则在该节点的右子树中继续循环遍历查找
else if(to_find > cur->data)
{
cur = cur->rchild;
}
else
{
return cur;
}
}
return NULL;
}
7. 在二叉搜索树中删除指定元素(非递归实现)
(1)在树中删除元素之前,首先要判断这棵树是否为空,为空则删除失败。
(2)树不为空,此时需要在树中查找指定元素所在的位置。如果没找到,也删除失败
(3)如果找到了,根据删除元素的不同状态可分为以下几种情况讨论。
a)如果要删除的结点没有左右子树
i)如果此时要删除的结点是根节点,此时直接将存放根节点指针的内存空间置空。
ii)如果此时要删除的结点不是根节点。
若要删除结点是其父节点的左子树,此时需要将父节点的左子树置为空
若要删除节点是其父节点的右子树,此时需要将父节点的右子树置为空
最后释放根节点的内存空间即可。上述三种情形如下图所示:
b)要删除的结点只有左子树
i)如果此时要删除的结点是根节点,则将存放根节点指针的内存空间用于存放原根节点的左子树的指针
ii)如果此时要删除的结点不是根节点。
若要删除结点是其父节点的左子树,则将要删除结点的左子树作为其父节点的左子树
若要删除节点是其父节点的右子树,则将要删除结点的左子树作为其父节点的右子树
最后在释放要删除的结点,上述三种情形如下图所示:
c)要删除的结点只有右子树
与b)的情况类似:
i)如果要删除的结点是根节点,则将根节点的右子树直接作为新的根节点
ii)如果要删除的结点不是根节点
如果要删除结点为其父节点的左子树,则将要删除结点的右子树作为其父节点的左子树
如果要删除结点为其父节点的右子树,则将要删除结点的右子树作为其父节点的右子树
最后再释放要删除的结点,上述三种情形如下图所示:
d)要删除的结点左右子树都有
首先要在要删除节点的右子树中找到最小值,此时,最小值必定没有左子树(可以根据该条件来寻找最小值)。将该最小值赋值给要删除结点的数据域,然后再删除最小值即可。而最小值有如下两种情形:
i)最小值是要删除结点的右孩子,则将最小值结点的右子树(要么有要么为空)作为要删除结点的右子树
ii)最小值不是要删除结点的右孩子但一定在要删除结点的右子树中,且最小值一定使其父节点的左孩子(因为最小值在寻找过程中一直在遍历左子树)此时将最小值结点的右子树作为最小值父节点的左子树。
最后释放最小值结点。
上述过程中如果删除的是根节点,在实际处理过程中只是改变了根节点的数据域,该没有真正删除根节点,所以此时不用讨论删除的是根节点的情况。上述两种情形如下图所示:
根据上述解题思路,编写代码如下:
//在二叉搜索树中删除指定元素(非递归)
void SearchTreeRemoveByLoop(SearchTreeNode** proot,SearchTreeType to_remove)
{
if(proot == NULL)
{
//非法输入
return;
}
if(*proot == NULL)
{
//空树,删除失败
return;
}
//1. 首先查找要删除元素和其父节点所在的位置
SearchTreeNode* to_remove_node = *proot;
SearchTreeNode* pre = NULL;
while(1)
{
if(to_remove_node == NULL)
{
//2. 如果找不到,则删除失败
//此时说明没有找到要删除的节点,说明删除失败,直接返回即可
return;
}
if(to_remove_node->data == to_remove)
{
break;//此时to_remove_node即为要删除的节点
}
else if(to_remove < to_remove_node->data)
{
//说明要删除的节点可能在to_remove_node的左子树中
pre = to_remove_node;
to_remove_node = to_remove_node->lchild;
}
else
{
//说明要删除的节点可能在to_remove_node的右子树中
pre = to_remove_node;
to_remove_node = to_remove_node->rchild;
}
}
//走到这里,说明to_remove_node就是要删除节点的指针
//3. 如果找到了该节点,可分为以下几种情形讨论
// a)该节点没有左右子树
if(to_remove_node->lchild == NULL && to_remove_node->rchild == NULL)
{
// i)如果该节点是根节点,则直接将根节点置空即可
if(to_remove_node == *proot)
{
*proot = NULL;
}
// ii)如果该节点不是根节点,那么该节点就是叶子节点
else
{
// 如果要删除的节点是其父节点的右子树,则将父节点的右子树置为空
if(to_remove_node == pre->rchild)
{
pre->rchild = NULL;
}
// 如果要删除节点是其父节点的左子树,则将父节点的左子树置为空
else if(to_remove_node == pre->lchild)
{
pre->lchild = NULL;
}
}
// 最后将该节点释放即可
DestroyNode(to_remove_node);
}
// b)该节点只有左子树
else if(to_remove_node->lchild != NULL && to_remove_node->rchild == NULL)
{
// i)如果要删除的节点是跟节点,则将跟节点的左子树作为新的根节点
if(to_remove_node == *proot)
{
*proot = to_remove_node->lchild;
}
// ii)如果要删除的元素不是父节点
else
{
// 如果要删除的元素是其父节点的左子树,则将要删除的节点的左子树作为其父节点的左子树
if(to_remove_node == pre->lchild)
{
pre->lchild = to_remove_node->lchild;
}
// 如果要删除节点是其父节点的右子树,则将要删除节点的左子树作为其父节点的右子树
else if(to_remove_node == pre->rchild)
{
pre->rchild = to_remove_node->lchild;
}
}
// 最后,释放该节点
DestroyNode(to_remove_node);
}
// c)该节点只有右子树
else if(to_remove_node->lchild == NULL && to_remove_node->rchild != NULL)
{
// i)如果要删除的节点是根节点,则将要删除节点的右子树作为新的根节点
if(to_remove_node == *proot)
{
*proot = to_remove_node->rchild;
}
// ii)如果要删除节点不是根节点
else
{
// 如果要删除节点是其父节点的左子树,则将要删除节点的右子树作为其父节点的左子树
if(to_remove_node == pre->lchild)
{
pre->lchild = to_remove_node->rchild;
}
// 如果要删除节点是其父节点的右子树,则将要删除节点的右子树作为其父节点的右子树
else if(to_remove_node == pre->rchild)
{
pre->rchild = to_remove_node->rchild;
}
}
// 最后释放要删除节点
DestroyNode(to_remove_node);
}
// d)该节点有左右节点
else
{
// 先在要删除节点的右子树中找到最小值,同时记录最小值的父节点
SearchTreeNode* min = to_remove_node->rchild;
SearchTreeNode* min_pre = to_remove_node;
while(min->lchild != NULL)
{
min_pre = min;
min = min->lchild;
}
//此时,min即为要删除元素右子树中的最小值
// 然后将最小值赋值给要删除节点的值,则现在要删除的就是最小值节点了
to_remove_node->data = min->data;
// i)如果最小值是要删除节点的右子树,则最小值的父节点一定是要删除的节点则该右子树一定没有左子树,
if(min == to_remove_node->rchild)
{
// 因此将最小值的右子树赋值给要删除节点的右子树
to_remove_node->rchild = min->rchild;
}
// ii)如果最小值不是要删除节点的右子树,则该最小值一定也没有左子树
else
{
// 则将最小值的右子树赋值给最小值父节点的左子树
min_pre->lchild = min->rchild;
}
// 最后释放要最小值节点
DestroyNode(min);
}
}
8. 在二叉搜索树中删除指定元素(递归实现)
递归实现的思路上述非递归相同,也要分成四种情形来讨论,流程如下:
(1)如果根节点为空即为空树,则删除失败
(2)如果根节点不为空。
如果要删除的值小于根节点的值,则在根节点的左子树中递归寻找要删除的结点
如果要删除的值大于根节点的值,则在根节点的右子树中递归寻找要删除的结点
(3)当遍历到某个根节点时,发现根节点为空了,说明要删除的元素在树中不存在,则删除失败。此时(1)就表示该含义。
当遍历到某个根节点时,发现根节点的值等于要删除元素,说明找到要删除的节点了。此时还是分四种情况来讨论:
a)如果要删除结点没有左右子树,此时直接将存放该结点指针的内存空间置空即可
b)要删除结点只有左子树,直接将要删除结点的左子树指针存放在原本存放要删除结点指针的内存空间即可。
c)要删除结点只有左子树,直接将要删除结点的右子树指针存放在原本存放要删除结点指针的内存空间即可
d)要删除结点左右子树都有。
首先,在要删除结点的右子树中寻找最小值(最小值必定只有右子树)。
然后,将最小值赋值给要删除结点的数据域。
最后,删除最小值结点,即将最小值的右子树指针存放在原本存放最小值结点指针的内存空间中。
注意:在上述的a)b)c)中,不必讨论要删除的结点是其父节点的左子树还是右子树。
如果要删除的是右子树,父节点是根据存放右子树指针的内存空间来找到右子树的,在删除右子树之后,直接将存放右子树指针的内存空间内容改变为新的右子树即可。
如果要删除的是左子树,父节点是根据存放左子树指针的内存空间来找到左子树的,在删除左子树之后,直接将存放左子树指针的内存空间内容改变为新的左子树即可。
也就是说,上述两中情况都是在对存放子树的内存空间来重新赋值。所以只需知道该内存空间的地址即可。而我们在函数进行传参时,传的就是内存空间的地址。因此直接对该地址解引用来改变其中的内容即可。
在d)中对最小值进行删除时,也是同样的道理。
根据上述思路,编写代码如下:
//在二叉搜索树中删除指定元素(递归)
void SearchTreeRemove(SearchTreeNode** proot,SearchTreeType to_remove)
{
if(proot == NULL)
{
//非法输入
return;
}
if(*proot == NULL)
{
//空树,删除失败
//该语句还表示,如果最后最后遍历到空的根节点,说明没有找到要删除的节点,此时也是删除失败
return;
}
//接下来在左右子树中遍历查找要删除元素所在的位置
SearchTreeNode* root = *proot;//proot是存放要删除结点指针的地址,root是要删除结点的指针
//如果要删除元素小于根节点,则在左子树中递归遍历查找
if(to_remove < root->data)
{
SearchTreeRemove(&root->lchild,to_remove);
}
else if(to_remove > root->data)//如果要删除元素大于根节点的值,则在右子树中递归遍历查找
{
SearchTreeRemove(&root->rchild,to_remove);
}
else//此时说明找到了要删除的节点了
{
//此时root便是要删除的节点,根据要删除节点的状态可分为如下四种情况讨论
//1. 如果要删除的节点没有左右子树
if(root->lchild == NULL && root->rchild == NULL)
{
//此时直接将该处的节点指针置为空,并释放内存即可
//该处的指针为root,要改变root的指向,即改变root的值
//此时就需要通过二级指针来改变指针的指向
//在该层递归函数中,如果直接改变行参root的值,并不会真正改变实际内存中的值,
//所以要是用二级指针来找到真正的内存空间,再将其内容置为空即可
//这里不用区分要删除的节点是其父节点的左子树还是右子树
//因为父节点就是通过指针的指针来找到它的该孩子节点所在的位置
//然后在对其进行判断其左右子树是否为空的,所以直接通过二级指针来改变该节点处的值即可
*proot = NULL;
DestroyNode(root);
}
//2. 如果要删除的节点只有左子树
else if(root->lchild != NULL && root->rchild == NULL)
//将要删除节点的左子树放在要删除节点的位置,再将要删除的节点释放即可
//*proot相当于要删除节点指针所存放的内存空间
//root相当于该空间中存放的值
//所以如果要删除的元素为父节点的左子树,只需将该空间中的值替换为要删除节点的左子树的指针
//然后释放要删除节点的值即可
//如果要删除元素为父节点的左子树,也就是说*proot中存放的就是父节点左子树的值
*proot = root->lchild;//*proot 作右值时表示的是值的大小,作左值时表示的是空间
DestroyNode(root);
}
//3. 如果要删除的节点只有右子树
else if(root->lchild == NULL && root->rchild != NULL)
{
*proot = root->rchild;
DestroyNode(root);
}
//4. 如果要删除的节点左右子树都有
else
{
//此时,需要现在要删除节点的右子树中找到到最小值,将最小值赋值给要删除节点的数据域
//然后再递归删除最小值即可
SearchTreeNode* min = root->rchild;
while(min->lchild != NULL)
{
min = min->lchild;
}
//当循环结束时,说明min即为要删除元素右子树中的最小值
root->data = min->data;
SearchTreeRemove(&root->rchild,min->data);
}
}
return;
}
我们以上述代码中的情形a)为例,来讨论为什么不用区分删除的是否为根节点。在a)中要删除结点的左右子树均为空。
(1)如果删除的是根节点,即该树中只有根节点。而在函数传参时传的是根节点的地址。如下图所示
此时,要将proot中本来用于存放根节点地址的值置为BULL,再释放根节点。
即:*proot = NULL ,free(root);
(2)如果要删除的是其父节点的左子树。如下图:
现在回到代码中:
上述代码中,要进行一次递归,递归时,传入的是root->lchild的地址,也就是proot = &root->lchild,此时要将存放root左子树的内存空间值置为NULL,只需根据proot找到该内存空间,在置空就可以。
即:*proot = NULL ,free(root->lchild)。
同理,如果要删除元素是其父节点的右子树,如下图所示:
即:*proot = null,free(root->rchild)
所以,上述三种情形,都是通过proot来进行操作的,所以不用区分删除的是根节点,还是左子树或右子树。
其余几种情形,可类似讨论。