一.定义
二叉查找树(Binary Search Tree),又被称为二叉搜索树。设x为二叉查找树中的一个结点,结点x的key值记为key[x]。如果y是x的左子树中的一个结点,则key[y]<=key[x];如果y是x的右子树的一个结点,则key[y]>=key[x]。
二.性质
在二叉查找树中:
- 若任意节点的左子树不为空,则左子树上的所有结点的值均小于它的根节点的值;
- 若任意节点的右子树不为空,则右子树上的所有结点的值均大于它的根节点的值;
- 任意节点的左、右子树也分别为二叉查找树。
- 没有键值相等的节点。
三.实现
1.节点定义
1.1节点定义
二叉查找树的节点包含的基本信息:
- key – 它是关键字,是用来对二叉查找树的节点进行排序的。
- left – 它指向当前节点的左孩子。
- right – 它指向当前节点的右孩子。
- parent – 它指向当前节点的父结点。
typedef int Type;
typedef struct BSTreeNode{
Type key; //键值
struct BSTreeNode *left; //左孩子
struct BSTreeNode *right; //右孩子
struct BSTreeNode *parent; //父节点
}Node, *BSTree;
1.2创建节点
给节点分配空间及成员赋值:
static Node * Create_bstree_node(Type key,Node *parent,Node *left,Node *right){
Node *p;
if ((p == (Node *)malloc(sizeof(Node)))==NULL) //分配内存
return NULL;
p->key = key;
p->left = left;
p->right = right;
p->parent = parent;
return p;
}
2. 遍历
这里只讲解先序遍历。
若二叉树非空,则执行以下操作:
- 访问根节点;
- 递归先序遍历左子树;
- 递归先序遍历右子树。
图例介绍:
前序遍历结果: 3 1 2 5 4 6
void preorder_bstree(BSTree tree) //tree作为根节点
{
if(tree != NULL)
{
printf("%d ", tree->key); //先访问根节点
preorder_bstree(tree->left); //递归遍历左子树
preorder_bstree(tree->right); //递归遍历右子树
}
}
3. 查找
给定一个节点的key,进行查找:
- key与当前树x的根结点比较,若大于根节点的key值,则去左子树中查找,否则去右子树中查找。
- 若找到,返回该节点,否则返回空
//递归实现:
Node* bstree_search(BSTree x, Type key)
{
if (x==NULL || x->key==key)
return x;
if (key < x->key)
return bstree_search(x->left, key);
else
return bstree_search(x->right, key);
}
//非递归实现:
Node* iterative_bstree_search(BSTree x, Type key)
{
while ((x!=NULL) && (x->key!=key))
{
if (key < x->key)
x = x->left;
else
x = x->right;
}
return x;
}
4.查找最大值和最小值
根据性质:右子树 > 父节点 > 左子树。
- 最大值:往右子树的右子树的方向一直寻找。若到最后某个节点没有右子树,则返回该节点(比左子树大)。
- 最小值:往左子树的左子树的方向一直寻找。若到最后某个节点没有左子树,则返回该节点(比右子树小)。
/*非递归实现:*/
Node* bstree_maximum(BSTree tree)
{
if (tree == NULL)
return NULL;
while(tree->right != NULL)
tree = tree->right;
return tree;
}
Node* bstree_minimum(BSTree tree)
{
if (tree == NULL)
return NULL;
while(tree->left != NULL)
tree = tree->left;
return tree;
}
5. 前驱和后继
节点的前驱:是该节点的左子树中最大的节点(对一棵二叉树进行中序遍历,遍历后的顺序,当前节点的前一个节点为该节点的前驱节点)。
- 若该节点没有左孩子,则该节点的前驱有以下两种可能:
(1)若该节点是“一个左孩子”,则查找“x的最低的父节点,并且该父节点要具有右子树”,找到的这个"最低的父结点"就是"x的后继结点"。
其中,要求“该父节点要具有右子树“,因为这是跳出while循环体的条件;
注意:这里的”最低父节点“是根据循环体不断变化的
(2)若该节点是”一个右孩子“,则”x的前驱节点“为”它的父节点“
节点的后继:是该节点的右子树中最小的节点(对一棵二叉树进行中序遍历,遍历后的顺序,当前节点的后一个节点为该节点的后继节点)。
- 若该节点没有左孩子,则该节点的前驱有以下两种可能:
(1)若该节点是"一个右孩子",则查找"x的最低的父结点,并且该父结点要具有左孩子",找到的这个"最低的父结点"就是"x的后继结点"。
(2)若该节点是"一个左孩子",则"x的后继结点"为 “它的父结点”。
//前驱
Node* bstree_predecessor(Node *x)
{
// 如果x存在左孩子,则"x的前驱结点"为 "以其左孩子为根的子树的最大结点"。
if (x->left != NULL)
return bstree_maximum(x->left);
// 如果x没有左孩子。则x有以下两种可能:
// (01) x是"一个右孩子",则"x的前驱结点"为 "它的父结点"。
// (01) x是"一个左孩子",则查找"x的最低的父结点,并且该父结点要具有右孩子",找到的这个"最低的父结点"就是"x的前驱结点"。
Node* y = x->parent;
while ((y!=NULL) && (x==y->left))
{
x = y;
y = y->parent;
}
return y;
}
//后继
Node* bstree_successor(Node *x)
{
// 如果x存在右孩子,则"x的后继结点"为 "以其右孩子为根的子树的最小结点"。
if (x->right != NULL)
return bstree_minimum(x->right);
// 如果x没有右孩子。则x有以下两种可能:
// (01) x是"一个左孩子",则"x的后继结点"为 "它的父结点"。
// (02) x是"一个右孩子",则查找"x的最低的父结点,并且该父结点要具有左孩子",找到的这个"最低的父结点"就是"x的后继结点"。
Node* y = x->parent;
while ((y!=NULL) && (x==y->right))
{
x = y;
y = y->parent;
}
return y;
}
图例讲解:
中序遍历结果: 1 2 3 4 5 6
例如:要寻找节点1的前驱。
- 判断节点1(x)是否有左孩子,没有!
- 判断循环体条件,节点3(y)满足条件(x是y的左孩子,且y不为空),进入循环体,y指向y的父节点(NULL);
- 判断循环条件,此时y=NULL,不满足,返回y。
此时,节点1的前驱为NULL。
例如:寻找节点4的前驱。
- 判断节点4(x)是否有左孩子,没有!
- 判断循环体条件,节点5(y)满足条件(x是y的左孩子且y不为空),进入循环体,y指向y的父节点,更新y为节点3,x为节点5;
- 判断循环条件,节点3(y)不满足条件(x是y的右孩子!),返回节点3(y)。
此时,节点4的前驱为节点3。
6.插入
其实插入的过程,就是查找的过程,找到要插入的位置后,比较要插入的key和此位置的key,若大于此位置的key,将此位置节点的右孩子指向插入节点,否则,其左孩子指向插入节点。
- bstree_insert(BSTree tree, Node *z)直接传入节点。内部函数。
- insert_bstree(BSTree tree, Type key)先传入key值,然后函数内部新建结点,最后将建好的结点传入bstree_insert(BSTree tree, Node *z)。只是个对外接口。
static Node* bstree_insert(BSTree tree, Node *z)
{
Node *y = NULL;
Node *x = tree;
// 查找z的插入位置
while (x != NULL)
{
y = x;
if (z->key < x->key)
x = x->left;
else
x = x->right;
}
z->parent = y;
if (y==NULL)
tree = z;
else if (z->key < y->key)
y->left = z;
else
y->right = z;
return tree;
}
Node* insert_bstree(BSTree tree, Type key)
{
Node *z; // 新建结点
// 如果新建结点失败,则返回。
if ((z=create_bstree_node(key, NULL, NULL, NULL)) == NULL)
return tree;
return bstree_insert(tree, z);
}
本文实现的二叉查找树是允许插入相同键值的节点的!
7.删除
删除有三种情况分析:
a.叶子结点:直接删除。
b.仅有左或右子树的结点:上移子树。
c.左右子树都有:用删除节点的直接前驱或者直接后继来替换当前节点,调整直接前驱或者直接后继的位置
static Node* bstree_delete(BSTree tree, Node *z)
{
Node *x=NULL;
Node *y=NULL;
if ((z->left == NULL) || (z->right == NULL) ) //情况a:后面判断条件不考虑,直接到最后的if条件。情况b:进入下一个if条件
y = z; //y指向要删除的结点
else //情况c
y = bstree_successor(z); //y指向要删除结点的后继,为填充做准备
/*找到需要调整指针的子树或结点*/
if (y->left != NULL) //b:情况x指向子树;c情况x指向后继节点子树
x = y->left;
else
x = y->right;
/*找到需要调整指针的子树或结点*/
/*调整指针*/
if (x != NULL) //b或c:子树不为空
x->parent = y->parent; //上移子树调整指针
if (y->parent == NULL) //上移子树调整指针
tree = x;
else if (y == y->parent->left) //上移子树调整指针
y->parent->left = x;
else //上移子树调整指针
y->parent->right = x;
if (y != z) //确认为情况c;
z->key = y->key; //后继节点y进行填充删除节点z;此时y没用了
/*调整指针*/
/*删除节点*/
if (y!=NULL) //删除y
free(y);
/*删除节点*/
return tree;
}
Node* delete_bstree(BSTree tree, Type key)
{
Node *z, *node;
if ((z = bstree_search(tree, key)) != NULL)
tree = bstree_delete(tree, z);
return tree;
}
bstree_delete(tree, z)是内部函数,它的作用是:删除二叉树(tree)中的节点(z),并返回删除节点后的根节点。
delete_bstree(tree, key)是对外接口,它的作用是:在树中查找键值为key的节点,找到的话就删除该节点;并返回删除节点后的根节点。
8.打印
void print_bstree(BSTree tree, Type key, int direction)
{
if(tree != NULL)
{
if(direction==0) // tree是根节点
printf("%2d is root\n", tree->key);
else // tree是分支节点
printf("%2d is %2d's %6s child\n", tree->key, key, direction==1?"right" : "left");
print_bstree(tree->left, tree->key, -1);
print_bstree(tree->right,tree->key, 1);
}
}
9.销毁二叉树
void destroy_bstree(BSTree tree)
{
if (tree==NULL)
return ;
if (tree->left != NULL)
destroy_bstree(tree->left);
if (tree->right != NULL)
destroy_bstree(tree->right);
free(tree);
}