在数据的查找这个问题上,如果查找的数据集是有序线性表,并且是顺序存储的,查找可以使用折半、插值、斐波那契等查找算法实现,可惜,因为有序,在插入和删除操作上,就需要耗费大量的时间。
那么有没有一种可以使得插入和删除的效率不错,又可以比较高效地实现查找的算法呢?也就是说有没有一种算法能够使用与动态查找。
动态查找:在查找时插入或者删除的查找表称为动态查找表。
所以就引入了二叉排序树,它是这样的一个二叉树:
- 左子树上的所有结点的值小于它的根结点的值;
- 右子树上的所有结点的值大于它的根结点的值;
- 它的左子树和右子树同样也是一颗二叉排序树。
根据上面的性质可以知道,对这棵二叉排序树中序遍历就得到了一个升序的序列。但是构造一个二叉排序树的目的,并不是为了排序,而是为了提高查找和插入删除的速度。
代码实现
树结构结点定义:
typedef struct BiTNode
{
int data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
查找实现:查找成功,返回查找到的结点的指针,查找不成功,返回查找路径上最后的一个结点。
//f是当前根结点T的父结点,当查找失败时返回这个父结点
bool SearchBST(BiTree T, int key, BiTree f, BiTree *p)
{
if (T == NULL) //递归查找不成功
{
*p = f;
return false;
}
if (T->data == key) //查找成功
{
*p = T;
return true;
}
else if (key < T->data)
{
return SearchBST(T->lchild, key, T, p); //左子树递归查找
}
else
{
return SearchBST(T->rchild, key, T, p); //右子树递归查找
}
}
这里为什么要返回查找路径上的最后一个结点:因为在插入操作的时候,也需要用到查找,而插入的点正好是查找路径上的最后一个结点也就是把要插入的结点作为这个最后结点的孩子。
//插入新结点的时候,要先查找,找到应该插入的位置
bool InsertBST(BiTree *T, int key)
{
BiTree p, s;
if (!SearchBST(*T, key, NULL, &p)) //查找不成功,可以插入
{
s = (BiTNode*)malloc(sizeof(BiTNode));
s->data = key;
s->lchild = s->rchild = NULL;
if (p == NULL)
{
*T = s;
}
else if(p->data > key)
{
p->lchild = s;
}
else
{
p->rchild = s;
}
return true;
}
else //查找成功,要插入的元素已经存在,插入失败
{
return false;
}
}
再看删除操作:
- 如果要删除的结点是叶子结点,那么好,直接就删除了。(因为参数是指针的指针,所有要删除一个结点,不用找到它的父结点)
- 如果要删除的结点仅有左孩子或者仅有右孩子,那么直接让左子树或者右子树继承。
- 如果有右孩子有左孩子,那么有其直接前驱替代这个结点,然后删除其直接前驱(之前前驱肯定只有一个孩子或者没有孩子,回到上面两个问题)
注:使用直接后继代替当前结点也是可以的。
//删除后的指针要从被调函数中带出来
bool DeleteBST(BiTree *T, int key)
{
if (*T == NULL)
{
return false;
}
if (key == (*T)->data)
{
Delete(T);
return true;
}
else if((*T)->data > key)
{
return DeleteBST(&(*T)->lchild, key);
}
else
{
return DeleteBST(&(*T)->rchild, key);
}
}
void Delete(BiTree *p)
{
BiTree s,q;
if ((*p)->rchild == NULL)
{
q = (*p);
(*p) = (*p)->lchild;
free(q);
}
else if((*p)->lchild == NULL)
{
q = (*p);
(*p) = (*p)->rchild;
free(q);
}
else //有左孩子和右孩子
{
q = (*p);
s = (*p)->lchild;
while (s->rchild)
{
q = s;
s = s->rchild;
}//while
(*p)->data = s->data;
if(q != (*p))
{
q->rchild = s->lchild;
}
else
{
q->lchild = s->lchild;
}
free(s);
}
}
总结:我们构建二叉排序树的目的就是能够在有序的情况下快速的查询,同时在插入或者删除的时候又不要有太高的复杂度。但是给定一个数组序列,构建的二叉排序树是不唯一的,他别数组有序的情况下,构建的二叉排序树是单边树,这样的查找的时间复杂度已经退化到了O(n)。所以我们要防止这种单边树的产生。尽量是树的左子树和右子树的高度相同。这就是二叉平衡树。