对于树的基本认识,我们很容易通过我们平常所见到的树来理解:一棵树,有一个根,根往上又会分叉出大树枝,大树枝又会分叉出小树枝,以此往复,直到最后是叶子。而作为数据结构的树也是类似的,只不过我们通常将它倒着画。树的应用也相当广泛,例如在文件系统,数据库索引中的应用等。本文会对树的基本概念做介绍,但重点介绍二叉查找树。
现实中的树
树是一种无向图,其中任意两个节点间存在唯一一条路径。树常常用一种递归的方式来定义,它是一种数据结构,要么为空,要么具有一个值并且有零个或多个孩子,而每个孩子又是树。
例如下图一棵树,任意两个节点间只有一条路径可到达:
树 图一:树
但下图并非树:
非树 图二:非树
因为从节点root到节点b并非只有一条路径。
我们可能已经听过很多树的名词,例如,红黑树,霍夫曼树,B树,B+树,平衡二叉树等等,而本文将要介绍二叉查找树,很多其他树都是它的变种,不像链表的线性访问,二叉查找树的大部分操作时间复杂度都为O(logN)。
在学习二叉查找树之前,必须要先了解一些名词,我们借助下面的图来了解,如果已经清楚了可以跳过此节。
介绍树的基本名词:
树 图三:树
二叉树 图四:二叉树
二叉树是一种树的特殊形式,它的每个节点最多两个孩子节点,分别为左孩子和右孩子。而二叉查找树在此基础上,还有一个特点,就是每个节点比它左子树的节点值都要大,而比右子树的节点值都要小。
二叉查找树常见操作有插入,查找,删除以及遍历。而实际上二叉查找树既可以使用数组来实现,也可以使用链表,本文采用链表来实现。另外本文也排除了两个节点存在相同值的情况。
我们使用一个结构体来定义它:
typedef struct Tree_Node
{
ElementType value; //节点值
struct Tree_Node *left; //左节点
struct Tree_Node *right; //右节点
}TreeNode;
我们以数据 10,5,19,4,8,13,24为例,来看看二叉查找树的插入流程是怎样的。
二叉查找树插入 图五:二叉查找树插入1
二叉查找树插入 图六:二叉查找树插入2
二叉查找树插入 图七:二叉查找树插入3
最终完成了所有元素的插入。而观察插入后的二叉树可以发现,每个节点都要比左子树的值大,而比右子树的值小。另外,我们在插入的时候不断地与节点值比较,比它大,则将插入右子树,而比它小,则将插入左子树,因此这个过程非常容易用递归来实现。
/*将value插入到树中*/
TreeNode *insertTree(ElementType value,TreeNode *tree)
{
if(NULL == tree)
{
/*创建一个节点*/
tree = malloc(sizeof(TreeNode));
if(NULL == tree)
{
printf("malloc failed\n");
return NULL;
}
else
{
/*将节点信息存储在此叶子节点中*/
printf("insert %d to tree\n",value);
tree->value = value;
tree->left = NULL;
tree->right = NULL;
}
}
/*比当前节点小,则插入到左子树*/
else if(value < tree->value)
{
tree->left = insertTree(value,tree->left);
}
/*比当前节点大,则插入到右子树*/
else if(value > tree->value)
{
tree->right = insertTree(value,tree->right);
}
return tree;
}
实际上,我们在做插入操作的时候,已经在做查找动作了,因为为了将一个元素插入到正确的位置,我们需要从根节点开始,不断比较其值和要插入(查找)的值的大小,如果比该节点值小,则说明该值需要插入到左子树,否则插入到右子树,并且递归调用,最终找到插入的位置。
查找的思路有点像二分查找,我们知道根节点左子树的值都比根节点值小,而右子树的值都比根节点的值要大,以此递归调用,可以很容易找到:
/*查找值为value的节点*/
TreeNode *findTree(ElementType value,TreeNode *tree)
{
if(NULL == tree)
{
/*最后一层还没有找到*/
return NULL;
}
/*从左子树查找*/
if(value < tree->value)
{
return findTree(value,tree->left);
}
/*从右边子树查找*/
else if(value > tree->value)
{
return findTree(value,tree->right);
}
/*找到*/
else
return tree;
}
相对来说,删除要比插入和查找复杂很多。因为它需要考虑很多情况:
第一种情况很容易理解,我们来看第二种和第三种情况。
先看第三种情况,假如我们要从前面的二叉树中删除节点值为10的节点。首先我们可以找到节点值为10的节点的右子树的最小值,为13,因此,将13代替节点值为10的节点值,而幸运的是,节点值为13的节点没有右子树,因此释放根节点的内存,完成删除操作。删除前后如图所示:
二叉查找树删除
图八:二叉查找树删除1
再看第二种情况,假如此时要删除值为19的节点,从图中可知,它有一个右儿子,因此删除该节点后,需要将其父节点指向它的右儿子,删除后如下图:
二叉查找树删除2
图九:二叉查找树删除2
总体来说,删除操作并不是一次简单的查找就可以完成,甚至删除会使得整个二叉树变得非常不平衡,所以如果删除次数不多,完全可以采用懒惰删除,即当节点被删除时,仅做一个标记,表明它被删除了,而不是真的从树中移除,这样虽然表面上浪费了一点空间,但是如果后面又要插入该元素值,则为新元素分配内存的开销就免了。
根据上面的分析,代码如下:
TreeNode *deleteTree(ElementType value,TreeNode *tree)
{
TreeNode *tempNode = NULL;;
if(NULL == tree)
{
printf("not fount \n");
return NULL;
}
/*比当前节点值小,从左子树查找并删除*/
else if(value < tree->value)
{
tree->left = deleteTree(value,tree->left);
}
/*比当前节点值大,从右子树查找并删除*/
else if(value > tree->value)
{
tree->right = deleteTree(value,tree->right);
}
/*等于当前节点值,并且当前节点有左右子树*/
else if(NULL != tree->left && NULL != tree->right)
{
/*用右子树的最小值代替该节点,并且递归删除该最小值节点*/
tempNode = findMin(tree->right);
tree->value = tempNode->value;
tree->right = deleteTree(tree->value,tree->right);
}
/*要删除的节点只有一个子节点或没有子节点*/
else
{
tempNode = tree;
/*要删除节点有右孩子*/
if(NULL == tree->left)
tree=tree->right;
/*要删除节点有左孩子*/
else if(NULL == tree->right)
tree = tree->left;
free(tempNode);
tempNode = NULL;
}
return tree;
}
完整代码可阅读原文或访问 https://www.yanbinghu.com/2019/04/07/55964.html查看 编译运行结果:
$ gcc -o binarySearchTree binarySearchTree.c
$ ./binarySearchTree
insert 10 to tree
insert 5 to tree
insert 19 to tree
insert 4 to tree
insert 8 to tree
insert 13 to tree
insert 24 to tree
find 13
本文简单介绍了二叉查找树的插入,查找,删除操作,其中删除操作较为复杂,需要特别注意。