文章目录
引入
之前两篇关于查找的博文讲的都是静态查找表,一般都是线性表,查找和修改很方便,但是插入和删除就不方便了,插入或删除数据后要保证查找表仍然有序,对于静态查找表是不好做到的,因为要涉及大量元素的移动(数组实现线性表)或者一些指针的改动和大量比较操作。
插入和删除操作频繁的查找表,应该用动态查找表(需要在查找时插入或者删除的查找表)实现。静态查找表一般使用便于随机访问的线性表数据结构实现,而动态查找表则使用便于插入和删除的非线性数据结构实现,比如用二叉树,注意一般不会用m元树(m>2)。
本文的主角,二叉查找树,也叫做二叉排序树,就是第一种要介绍的动态查找表。
定义(递归)
从第三点性质可以看到,二叉查找树也用递归实现定义。
虽然名字叫做二叉排序树,但是主要目的并不是为了排序哦,所以大名是二叉查找树,小名才是二叉排序树,毕竟确实也实现了排序,就把排序当做一个副产品好啦
用一个例子来比较线性表和二叉树的插入操作的难易
如果用线性表,第三个元素58小于62,如果用数组实现线性表,则需要把62和88往后移动;如果用链表,这里即头插。但是后面每一个元素都需要和前面已有的所有元素一一对比,比较操作太多了,不过链表的插入倒也不算特别麻烦,但是肯定是不可能用数组的····
用二叉树,第一个元素62作为根节点,第二个元素88大于62,于是作为右孩子,第三个元素58小于62,于是作为左孩子······,新元素的插入非常方便,比较操作又好实现又好理解,并且比较操作的数量很少——等于待查找结点在二叉查找树的层数。所以比较次数最少为1次(待查找结点就是根节点),次数最多为树的深度(待查找结点是最深的叶子结点)。
对二叉查找树进行中序遍历,就可以得到从小到大的排列顺序的线性序列。
代码
二叉树的结点,二叉树
这是表示二叉树的结点的结构体,BiTree是指向二叉树结点的指针,所以二叉树实际上是用二叉链表实现的哦,要知道本质
typedef struct BiTNode
{
ElemType data;
struct BiTNode * lchild, * rchild;
}BiTNode, *BiTree;
查找操作(递归)
/*在二叉查找树T中查找关键字key*/
/*f指向T的父亲,初次调用时其值是NULL,因为初次调用时T指向根节点,根节点没有父亲*/
/*T指向当前查找子树的根节点*/
/*如果找到了key,则p指向该元素所在结点的地址;否则,p指向查找路径上访问的最后一个结点*/
bool SearchBST(BiTree T, BiTree f, int key, BiTree * p)
{
if (!T)//当前查找的子树为空,查找失败,返回
{
*p = f;//查找路径的最后一个结点是当前查找子树T的父亲
return false;
}
if (key == T->data)//如果T指向的数据刚好就是关键字,查找成功
{
*p = T;
return true;
}
if (key < T->data)//关键字小于T指向的数据,则递归查找T的左子树
return SearchBST(T->lchild, T, key, p);
if (key > T->data)
return SearchBST(T->rchild, T, key, p);
}
插入操作(基于查找找到的位置)
基于查找,先找找是否已经存在,不存在才插入。
p是查找路径的最后一个结点,它决定了新结点的插入位置
/*不存在关键字Key,则插入并返回true;否则返回false*/
bool InsertBST(BiTree *T, int key)
{
BiTree * p = NULL;//p是查找路径的最后一个结点,它决定了新结点的插入位置
if (SearchBST(*T, NULL, key, p))//已经存在
return false;
//建立新结点
BiTree s;
s = (BiTree)malloc(sizeof(BiTNode));
s->data = key;
//把结点插入到正确位置
if (*p)//p是空指针,则这棵树是空树,插入为根结点
*T = s;
else if (key < p->data)//插为p指向结点的左孩子
p->lchild = s;
else if (key > p->data)
p->rchild = s;
return true;
}
删除操作(难咯)
难在删除之后留下的二叉树,仍然是一棵二叉查找树。
删除叶子结点(最简单)
因为删除叶子并不会影响其他部分的结构
待删除结点只有左子树,或者只有右子树(较简单)
直接把左子树或右子树搬到待删除结点的位置,独子承父业
待删除结点既有左子树又有右子树(难):用待删除结点的直接前驱或直接后继替代它,可报二叉树其他部分的结构不变
选一个子树代替待删除结点,把另一棵子树的所有结点依次重新插入到二叉树中(不好)
如果待删除结点有两个孩子,那么我们最直观想到的办法就是:随便选一个子树来代替待删除结点,然后把另一棵子树的所有结点依次重新插入到二叉树中。
这么做,确实可以实现目的,但绝对不是什么好办法,因为另一棵子树的结点也许很多,更重要的是,这样做有可能会加深树的高度!!!这是我们绝对不想看到的后果,后面我们还要专门研究平衡二叉树,目的就是要让树的高度尽量小,让树尽量平衡,而不要出现左重右轻或者右重左轻的情况(最极端的是左斜树和右斜树)。
用直接前驱或直接后继替代(好)
观察发现,用左子树的37,或者右子树的48直接替代待删除结点,可以保证二叉树其他部分结构不变,还是一棵二叉排序树,并且深度只可能降低或者不变,绝不对增高。
其实,对二叉排序树进行中序遍历得到的线性序列中,左子树的37正是待删除结点的直接前驱,右子树的48正是待删除结点的直接后继。
需要注意的是,直接前驱也许不是叶子结点,而是有左子树的结点(一定没有右子树),所以拿他们替换待删除结点时,相当于要删除他们自己,所以要把左子树移动到直接前驱结点处。同样的,直接后继也可能有右子树,要拿右子树替换直接后继结点。
怎么找一个结点的直接前驱:从左子树根节点出发,一路向右直到尽头
那么怎么找一个结点的直接前驱呢?
肯定不需要把整棵树中序遍历一次。
找直接前驱:
由于直接前驱比自己小,在左子树中,但是它一定是左子树中最大的元素,所以从左子树的根节点出发,一路沿着右边往下找,直到尽头,就是直接前驱。
找直接后继:
同理,直接后继是右子树中最小的元素,所以从右子树的根结点出发,一路沿着左边走,直到尽头。
这也是下面的代码的关键。
删除操作的代码
综合三种情况
/*如果有key,则删除并返回true;否则返回false*/
bool DeleteBST(BiTree T, int key)
{
BiTree * p = NULL;
if (!SearchBST(T, NULL, key, p))//没找到
return false;
//如果找到了,则p一定不是空指针,且指向待删除结点
DeleteNode(p);
}
bool DeleteNode(BiTree * p)
{
/*删除结点p*/
//待删除结点是叶子,或者待删除结点只有一个子树
BiTree s;
if (!p->lchild)
{
s = *p;//存住待删除结点的地址
*p = (*p)->rchild;
free(s);
return true;
}
if (!p->rchild)
{
s = *p;
*p = (*p)->lchild;
free(s);
return true;
}
//两个子树均非空,则从左子树找待删除结点的直接前驱(或者从右子树找直接后继)
BiTree q;
if ((*p)->lchild && (*p)->rchild)
{
s = (*p)->lchild;
q = *p;
while (s->rchild)
{
q = s;
s = s->rchild;
}
//此时s指向待删除结点的直接前驱,q是s的父亲
(*p)->data = s->data;
if (q==*p)//待删除结点的直接前驱的父亲就是待删除结点,则重接q的左子树
q->lchild = s->lchild;
else
q->rchild = s->lchild;//重接q的右子树
free(s);
return true;
}
}
其中第一个函数也可以这么写:
bool DeleteBST(BiTree T, int key)
{
if (!T)
return false;
if (key == T->data)//查找成功
return DeleteNode(T);
if (key < T->data)
return DeleteBST(T->lchild, key);
if (key > T->data)
return DeleteBST(T->rchild, key);
}
这样就和查找的代码几乎一样,就不调用查找函数,自己查找了。
二叉查找树的查找性能不稳定,引出平衡二叉树的设计需求
前面说了,二叉查找树中比较次数最少是1,最多是树的深度。听起来感觉好像还行,但是如果二叉树是一个极端的右斜树或者左斜树,这个最坏就坏到了极致,直接差到和顺序查找一样了!所以我们希望树不要走极端,尽量平衡一些,以减小树的深度,从而优化二叉查找树的查找效率。
二叉查找树的查找性能取决于二叉树的形状,可二叉树的形状不是确定的!所以查找性能也不稳定
再举一个例子(图来自B站)
再比如(图来自B站)
可以看到,同样一组数据,用不同的顺序建树,得到的树的形状不同,查找效率也不同。所以我们当然要想办法,尽量去建出一个AVL更小的树,这样的树其实就是比较平衡的树,即下一篇博文的主角,平衡二叉树。