二叉搜索树(BST)

来源:互联网 发布:数据访问等级 编辑:程序博客网 时间:2024/05/20 22:03

什么是二叉搜索树

        一颗二叉搜索树是以一颗二叉树来组织的,这样的一棵树可以用一个链表数据结构来表示,其中每一个结点就是一个对象。除了key和卫星数据之外,每个结点还包括属性left,right和p。它们分别指向结点的左孩子、右孩子和双亲。如果某个孩子结点和父节点不存在,则相应属性的值为NIL。其中卫星数据是指:在实际中,待排序的数很少是单独的数值,每个记录包含一个关键字(key),就是排序问题中需要重排的值。记录的其他部分有卫星数据组成,通常与关键字是一同存取的。

        二叉排序树( Binary Sort Tree),又称二叉搜索树。它或者是一颗空树,或者是具有下列性质的二叉树:

  • 若它的左子树不为空,则左子树上所有结点的值均小于它的根结点的值。
  • 若它的右子树不为空,则右子树上所有结点的值均大于它的根结点的值。
  • 它的左、右子树也都为二叉排序树。
        由前面介绍的顺序查找可知,插入和删除的效率并不是十分理想,构造一颗二叉排序树的目的,其实不是为了排序,而是为了提高查找和插入删除关键字的速度。二叉搜索树性质允许我们通过一个简单的递归算法来按序输出二叉搜索树中的所有关键字,这种算法称为中序遍历算法(输出的子树根的关键字位于其左子树的关键字值和右子树关键字值之间)。但不管怎么说,在一个有序数据集上的查找,速度总是要快于无序的数据集的,而二叉排序树这种非线性的结构,也有利于插入和删除的实现。因此,对于一颗二叉搜索树,使用简单的递归算法即可输出二叉搜索树T中的所有元素:
        
        对于上面的算法我们可知,如果x为一棵有n个结点子树的根,那么遍历一棵有n个结点的二叉搜索树需要耗费O(n)的时间。(NIL表示指向一个对象的指针为空)

二叉搜索树的查找

        首先,我们给出一个简单的二叉树的结构:

typedf struct BiTNode{    int data;//结点数据    struct BiTNode *lchild, *rchild;//左右孩子指针}BiTNode,*BiTree;
        在一棵二叉搜索树中查找一个具有给定关键字的结点。输入一个指向树根的指针和一个关键字k,如果这个结点存在,函数返回一个指向关键字k的结点的指针,否则返回NIL。

        对此,我们可以给出类似的实现:

//递归查找二叉排序树T中是否存在key//指针f指向T的双亲,其初始值为NULL//若查找成功,则指针p指向该数据元素结点,并返回TRUE//否则指针p指向路径上访问的最后一个结点并返回FALSEint SearchBST(BiTree T,int key,BiTree f,BiTree *p){    if(!T)    {        *p = f;        return false;    }    else if (key == T->data)    {        *p = T;        retrun true;            }    else if(key < T->data)        return SearchBST(T->lchild,key,T,p);    else        return SearchBST(T->rchild,key,T,p);}
        这个过程从树根开始查找,并沿着这棵树的可行路径向下进行,对遇到的每一个结点进行与关键字key的比较,根据比较结果的大小关系向下继续查找或结束查找。其运行时间为O(h),h即为树的高度。例如SearchBST(T,93,NULL,p),其中参数T为一个二叉链表,key为要查找的关键字,二叉树f指向T的双亲,当T指向根结点时,f的初值为NULL,它在递归时有用。最后的参数是为了查找成功后可以得到查找到的结点位置。同时,可以采用while循环来展开递归,用一种迭代方式重写这个过程。

二叉搜索树的插入和删除

        插入和删除操作会引起二叉搜索树表示的动态集合的变化,一定要修改数据结构来反映这个变化。

二叉排序树插入操作

int InsertBST(BiTree *T,int key)//当二叉排序树不存在关键字等于key的数据元素时插入key{    BiTree p,s;    if(!SearchBST(*T,key,NULL,&p))//p指向查找路径的中最后一个访问的结点。    {        s = (BiTree)malloc(sizeof(BiTNode));        s->data = key;        s->lchild = s->rchild = NULL;        if(!p)            *T = s;//插入s为新的根结点                                 //这里表示结点为空时,将插入结点作为根结点。          else if (key < p->data)            p->lchild = s;        else            p->rchild = s;        return true;    }    else        return false;}

二叉排序树删除操作

        从一棵二叉搜索树T中删除一个结点z的整个策略分为三种基本情况:

  • 叶子结点。即z没有孩子结点,那么只是简单的将其删除,并修改它的父结点,用NIL作为孩子结点来替代z。
  • 仅有左子树或右子树的结点。即z只有一个孩子,那么将z删除,并修改z的父结点,用z的孩子结点来替代z的位置。(将z的孩子提升到树中z的位置上)
  • 左右子树都有结点。即z有两个孩子,那么找z的后继y(一定在z的右子树中)或者前驱(在z的左子树中),根据中序遍历的性质可知,目标节点的直接前继和直接后继为当前结点左子树最右的结点和右子树下最左的结点。并让y占据树中z的位置。z原来的右子树成为y的新的右子树,并且z的左子树成为y的新的左子树。(这里还与y是否为z的右孩子有关)
        可以分为下面是四种情况:
  • 如果z没有左孩子,那么用其右孩子来替代z。这个右孩子可以是NIL,亦可以不是(即z为叶子结点的情况)如,如下图a。
  • 如果z仅有一个孩子且只有一个左孩子,那么就用左孩子来替代z,如下图b。
  • z既有一个左孩子又有一个右孩子,我们要查找z的后继y,y位于z都右子树中并且没有左孩子。
    • 如果y是z的右孩子,那么用y替代z,并仅留下y的右孩子,如下图c。
    • 否则,y位于z的右子树中但并不是z的右孩子,这种情况下,先用y的右孩子替换y,再用y替换z,如下图d。
        


/* 若二叉排序树T中存在关键字等于key的数据元素时,则删除该数据元素结点, *//* 并返回TRUE;否则返回FALSE。 */int DeleteBST(BiTree *T,int key){ if(!*T) /* 不存在关键字等于key的数据元素 */ return FALSE;else{if (key==(*T)->data) /* 找到关键字等于key的数据元素 */ return Delete(T);else if (key<(*T)->data)return DeleteBST(&(*T)->lchild,key);elsereturn DeleteBST(&(*T)->rchild,key); }}/* 从二叉排序树中删除结点p,并重接它的左或右子树。 */int Delete(BiTree *p){BiTree q,s;if((*p)->rchild==NULL) /* 右子树空则只需重接它的左子树(待删结点是叶子也走此分支) */{q=*p; *p=(*p)->lchild; free(q);}else if((*p)->lchild==NULL) /* 只需重接它的右子树 */{//*p=(*p)->lchild操作不仅能够改变当前结点指针p的指向,同时在此函数修改形参的值,由于它的指针类型导致外层函数传入的实参也被修改,//它的实参值实际为p的双亲结点指针域的值(存放的是子节点的地址)。即修改p也会导致删除结点父结点的指针域中存储的值的变化(即改变了指向)。q=*p; *p=(*p)->rchild; free(q);}else /* 左右子树均不空 */{q=*p; s=(*p)->lchild;while(s->rchild) /* 转左,然后向右到尽头(找待删结点的前驱) */{q=s;s=s->rchild;}(*p)->data=s->data; /*  s指向被删结点的直接前驱(将被删结点前驱的值取代被删结点的值) */if(q!=*p)q->rchild=s->lchild; /*  重接q的右子树 */ elseq->lchild=s->lchild; /*  重接q的左子树 */free(s);}return TRUE;}
else//后继结点实现法{    q=*p,s=(*p)->rchild;//取右子树    while(s->lchild)//转左,求直接后继    {        q = s; s = s->lchild;    }    (*p)->data = s->data;    if(q != *p) //z的直接后继不为z的右孩子        q->lchild = s->rchild;    else        q->rchild = s->rchild;    free(s);    }
        这里,给出以下图例来描述这个过程,如下图所示,删除结点47的过程:
        这里采用求前驱的方法,根据中序遍历的性质我们可知,某结点的前驱即为该结点左子树最右的结点。因此,由上图可知,该子树的前驱即为37。
        根据循环,直到s指向左子树最右结点为止。
        复制结点数据到目的结点。
        续接结点q的右子树,如果s为待删除结点的左子树(即s没有右孩子),那么即需要重接q的左孩子(将s的左孩子作为q的左孩子,注意,这里的每一个叶子结点都省略了其子结点指向NULL的事实)。
        释放结点。

小结

        二叉排序树的查找, 其比较次数等于给定值的结点在二叉排序树中的层数。其时间性能取决于二叉树的高度。然而问题在于,二叉排序树的形状是不确定的。它的平均时间复杂度为O(logn),而最坏的的时间复杂度为O(n)。这里就引申出另一问题,即如何让二叉排序树平衡的问题。