动态查找的大多借助于树类型的结构,这里只介绍最简单的一种——二叉排序树。
二叉排序树是这样一种树:它的要么是空的;如果它的左子树不为空,那么左子树上所有节点的值均小于根节点的值;如果右子树不为空,那么右子树上的值均大于根节点上的值,并且它的左右子树还是二叉排序树。二叉排序树有一个重要的性质:当你中序遍历该树时,得到的遍历结果是有序的。
但这一切跟动态查找查找有什么关系呢?使用线性数据结构,也能较好地完成查找工作,比如之前提过的折半查找法。但是如果我想完成的是:如果找不到这个元素,就把它插入或者如果找到这个元素,就把它删除,那么就比较麻烦了。原因出在两个方面,如果使用类似于数组的结构,那么插入、删除都会导致大量元素的移动;如果使用类似于链表的结构,由于链表不支持随机访问特性,只能从头开始一个挨着一个查找,不能发挥折半查找的效率优势。此时,如果我们使用二叉排序树就能很好解决这两个问题:首先,它可以较方便的插入和删除元素,其次沿着跟向任意路径的遍历恰好就是折半查找!每次遇到根节点,如果待查找的值小于根节点的值,则访问根节点的左子树;否则访问根节点的右子树。
下面我们看看程序的实现:
数据结构和函数的声明如下:
#include <stdio.h>
#include <malloc.h>
typedef struct BinarySortTree
{
int key;
//父节点
BinarySortTree* parent;
//左孩子
BinarySortTree* lchild;
//右孩子
BinarySortTree* rchild;
}BST,*pBST;
//初始化指向一个节点的指针
BST* initNode(int k);
//给parent节点增加一个名为child的孩子节点,lr=0为左孩子,否则为右孩子
bool addBranch(pBST parent,pBST child,int lr);
//中序遍历树
void InOrderTraverse(pBST t);
//查找key是否在t中,f为待查子树的父节点,找到的位置由p带出
bool searchBST(pBST t,int key,pBST f,pBST* p);
//向t中插入key
bool insertBST(BST* t,int key);
//执行删除的实际函数
void Delete(pBST t);
//从t中删除key
bool deleteBST(pBST t,int key);
//删除整个树
void destroyBST(pBST t);
函数的定义如下:函数的定义如下:
#include "BinarySortTree.h"
pBST initNode(int k)
{
pBST p = (BST*)malloc(sizeof(BST));
p->key = k;
p->lchild = p->parent = p->rchild = NULL;
return p;
}
bool addBranch(pBST parent,pBST child,int lr)
{
//如果节点已经满了,则不能插入
if(parent->lchild != NULL &&parent->rchild != NULL)
return false;
if(0 == lr)
parent->lchild = child;
else
parent->rchild = child;
child->parent = parent;
return true;
}
//中序遍历一棵树,遍历结果是有序的
void InOrderTraverse(pBST t)
{
if(NULL == t)
return;
else
{
InOrderTraverse(t->lchild);
printf("%d ",t->key);
InOrderTraverse(t->rchild);
}
}
//二叉排序树的查找
//输入参数为待查找的树根、key,当前节点的父节点(第一次调用时为NULL),查找结果通过p返回出来
bool searchBST(pBST t,int key,pBST father,pBST* p)
{
//如果没有找到,p返回查找路径上指向的最后一个节点,返回false
if(NULL == t)
{
*p = father;
return false;
}
//若找到,则p返回指向该节点的指针,返回true
if(t->key == key)
{
*p = t;
return true;
}
if(t->key > key)
return searchBST(t->lchild,key,t,p);
else
return searchBST(t->rchild,key,t,p);
}
//二叉排序树的插入,插入后依然是二叉排序树
//如果找到该元素,返回false,否则插入并返回true
bool insertBST(pBST t,int key)
{
//查找结果的返回值从result带出去
pBST result;
//如果找到了,返回假
if(searchBST(t,key,NULL,&result))
return false;
else
{
//新分配一个节点
pBST pnew = initNode(key);
//如果插入位置是头结点
if(NULL == result)
pnew = t;
//如果key小于节点的key,插到节点的左孩子
else if(result->key > key)
addBranch(result,pnew,0);
else
//插到节点的右孩子
addBranch(result,pnew,1);
return true;
}
}
//从二叉排序树中删除元素,删除以后还是二叉排序树
bool deleteBST(pBST t,int key)
{
if(NULL == t)
return false;
//如果找到对应的节点,进行具体的删除操作
if(t->key == key)
Delete(t);
else if(t->key > key)
return deleteBST(t->lchild,key);
else
return deleteBST(t->rchild,key);
return true;
}
//具体删除操作的函数
void Delete(pBST p)
{
//如果被删除的是叶子节点
if(NULL == p->rchild && NULL == p->lchild)
{
if(p->key < p->parent->key)
p->parent->lchild = NULL;
else
p->parent->rchild = NULL;
free(p);
p = NULL;
return ;
}
//如果被删除节点的右子树为空,只需要重接左子树
if(NULL == p->rchild && NULL != p->lchild)
{
//先判断p是p->parent的左孩子还是右孩子
//如果是左孩子
if(p->parent->key >= p->key)
p->parent->lchild = p->lchild;
else
p->parent->rchild = p->lchild;
p->lchild->parent = p->parent;
free(p);
p = NULL;
return;
}
//如果p的左子树为空,则只需要重接右子树
if(NULL == p->lchild && NULL != p->rchild)
{
p->rchild->parent = p->parent;
//先判断p是p->parent的左孩子还是右孩子
//如果是左孩子
if(p->parent->lchild->key == p->key)
p->parent->lchild = p->rchild;
else
p->parent->rchild = p->rchild;
free(p);
p = NULL;
return ;
}
//如果左右子树都不为空:
//寻找p的左子树的右链中的最大值
//如果左子树的右链为空,那么直接将左子树替换到最大值上
//如果不为空将这个值作为p的值
//将最大值删去,就是将最大值的左孩子与最大值的父亲相连
pBST q = p;
pBST s = p->lchild;
//找到p左子树的最大值
while(s->rchild)
{
q = s;
s = s->rchild;
}
//循环完成以后
//如果左子树的右链为空,则不执行循环,q指向p,s为p的左孩子
//否则s记录的最大值的位置,q记录的是s的父节点
//左子树的右链为空
if(q == p )
{
p->key = s->key;
//等价于p->lchild = NULL;
p->lchild = s->lchild;
free(s);
}
//走到了右链的最后一个元素
else
{
p->key = s->key;
//如果右链有左孩子
if(s->lchild != NULL)
{
//最大值的左孩子的父亲指向最大值的父亲
s->lchild->parent = q;
//最大值的父节点的右孩子指向最大值的左孩子
q->rchild = s->lchild;
}
//如果右链没有左孩子
else
q->rchild = NULL;
free(s);
}
return ;
}
//删除整个树
//通过后序遍历完成
void destroyBST(pBST t)
{
if(NULL == t)
return ;
destroyBST(t->lchild);
destroyBST(t->rchild);
free(t);
t = NULL;
}
这些函数中,searchBST中的father函数看似多余,但是由于它的存在,在执行插入元素操作时,result能够获取被插入元素的“应该的”父节点。然后方便操作。删除一个节点就相对比较麻烦了,你得考虑3种情况:
1.被删除的节点是叶子节点,那么只需要把它删除并把它的父节点的lchild或者rchild设为NULL就行了。
2.如果被删除的节点有一个孩子节点,那么只需要把这个孩子节点接到被删除节点的父节点下面就行了。
3.如果被删除节点的左右孩子都存在,就比较麻烦了,因为你得保证删除之后重新拼起来的树还是二叉排序树。具体的说,你得在这个节点的左子树中找一个最大的,将它值放到这个节点,再把那个最大值的节点删除。具体的找法,如果左子树的右链为空,那么就把左子树这个节点的值放上去;否则沿着左子树的右链寻找到头。这个元素肯定是最大的。最后删除这个最大值。这时还分两种情况:这个最大值有左孩子与最大值没有左孩子。此时的处理对应于前两种情况中的一种。
在主函数中,需要通过类似于如下的代码建立一个树:
pBST root = initNode(45);
pBST node1 = initNode(12);
addBranch(root,node1,0);
pBST node2 = initNode(53);
addBranch(root,node2,1);
pBST node3 = initNode(3);
addBranch(node1,node3,0);
pBST node4 = initNode(37);
addBranch(node1,node4,1);
pBST node5 = initNode(100);
addBranch(node2,node5,1);
pBST node6 = initNode(24);
addBranch(node4,node6,0);
pBST node7 = initNode(61);
addBranch(node5,node7,0);
pBST node8 = initNode(90);
addBranch(node7,node8,1);
pBST node9 = initNode(78);
addBranch(node8,node9,0);
然后才能进行各种操作。
最后简单的提一下这种查找的效率:咋看之下,这种查找类似于折半查找,但实际上却依赖于二叉排序树的形状:极端情况下,假如所有的节点节点依次为前一个节点的左子树,那么这个二叉排序树退化为一个链表,那么它的查找效率就会大大降低。由此可见,我们希望数据在整个树上的分布是左右平衡的,这样查找效率才会更高。于是,人们提出了一种长相更加苛刻树,平衡二叉树,(AVL树)这种树的左子树与右子树高度的绝对值之差不超过1。如果使用这种树来做二叉排序树,那么可以提高二叉排序树的效率。