/* 8.6 二叉排序树:在创建树的时候就构建一个有序的树 特点: 1.若它的左子树不空,则左子树上所有结点的值均小于它的根结构的值; 2.若它的右子树不空,则右子树上所有结点的值均大于它的根节点的值; 3.它的左、右子树也分别为二叉排序树 构建一颗二叉排序树的目的,其实并不是为了排序,而是为了提高查找和插入删除关键字的速度。不管怎么说,在一个有序数据集上的查找, 速度总是要快于无序的数据集的,而二叉排序树这种非线性的结构,也有利于插入和删除的实现。 */ /* 8.6.1 二叉排序树查找操作 首先我们提供一个二叉树结构 */ typedef struct BiTNode { //结点数据 int data; //左右孩子指针 struct BiTNode *lchild, *rchild; } BiTNode, *BiTree; //然后我们来看看二叉排序树的查找是如何实现的 /* 递归查找二叉排序树T中是否存在key 指针f指向T的双亲,其初始调用值为NULL 若查找成功,则指针p指向该数据元素结点,并返回TRUE 否则指针p指向查找路径上访问的最后一个结点并返回FALSE */ Status SearchBST(BiTree T, int key, BiTree f, BiTree *p) { //查找不成功 if(!T) { *p = f; return FALSE; } //查找成功 else if(key == T->data) { *p = T; return TRUE; } else if (key < T->data) return SearchBST(T->lchild, key, T, p); //在左子树继续查找 else return SearchBST(T->rchild, key, T, p); //在右子树继续查找 } /*8.6.2 二叉排序树插入操作 有了二叉排序树的查找函数,那么所谓的二叉排序树的插入,其实也就是将关键字放到树中合适位置而已。 当二叉排序树T中不存在关键字等于key的数据元素时,插入key并返回TRUE,否则返回FALSE */ Status InsertBST(BiTree *T, int key) { BiTree p,s; //查找不成功 if(!SearchBST(*T, key, NULL, &p)) { s = (BiTree)malloc(sizeof(BiTNode)); s->data = key; s->lchild = s->rchild = NULL; if(!p) //插入s为新的根节点 *T = s; else if(key < p->data) //插入s为左孩子 p->lchild = s; else //插入孩子为右孩子 p->rchild = s; return TRUE; } else //树中已有关键字相同的结点,不再插入 return FALSE; } /* 8.6.3 二叉树删除操作 俗话说“请神容易送神难”,我们已经介绍了二叉排序树的查找与插入算法,但是对于二叉排序树的删除,就不那么容易, 我们不能因为删除了结点,而让这棵树变得不满足二叉树排序的特性,所以删除需要考虑多种情况。 */ /* 根据我们对删除结点三种情况的分析: 1.叶子结点; 2.仅有左或右子树的结点; 3.左右子树都有结点,我们来看代码,下面这个算法是递归方式对二叉排序树T查找key,查找到时删除。 */ /* 若二叉排序树T中存在关键字等于key的数据元素时,则删除该数据元素结点,并返回TRUE;否则返回FALSE */ Status DeleteBST(BiTree *T, int key) { //不存在关键字等于key的数据元素 if(!*T) return FALSE; else { //找到关键字等于key的数据元素 if (key == (*T)->data) return Delete(T); else if (key < (*T)->data) return DeleteBST(&(*T)->lchild, key); else return DeleteBST(&(*T)->rchild, key); } } /* 上边这段代码和前面的二叉排序树查找几乎完全相同,唯一的区别就在于第8行,此时执行的时Delete方法, 对当前结点进行三处操作。我们来看Delete的代码。 */ //从二叉排序树中删除结点p,并重接它的左或右子树。 Status Delete(BiTree *p) { BiTree q,s; //右子树空则只需重接它的左子树 if ((*p)->rchild == NULL) { q = *p; *p = (*p)->lchild; free(q); } //左子树为空则只需重接它的右子树 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; } //s指向被删除结点的直接前驱 (*p)->data = s->data; if (q != *p) //重接q的右子树 q->rchild = s->lchild; else //重接q的左子树 q->lchild = s->lchild; free(s); } return TRUE; } /* 8.6.4 二叉排序树总结 总之,二叉排序树是以链接的方式存储,保持了链接存储结构在执行插入或删除操作时不用移动元素的优点, 只要找到合适的插入和删除位置后,仅需修改链接指针即可。插入删除的时间性能比较好。而对于二叉排序树 的查找,走的就是从根节点到要查找的结点的路径,其比较次数等于给定值的结点在二叉排序树的层数。极端 情况,最少为1次,即根节点就是要找的结点,最多也不会超过树的深度。也就是说,二叉排序树的查找性能 取决于二叉树的形状。可问题就在于,二叉排序树的形状是不确定的,有可能时极端的左倾或者右倾。 也就是说,我们希望二叉排序树是平衡的,即其深度与完全二叉树相同,均为,那么查找的时间复杂度 也就是O(logn),近似于折半查找,事实上左图也不够平衡(p580 图8-6-18),明显的左重右轻。 不平衡的最坏情况就是像p580 图8-6-18 右图的斜树,查找时间复杂度为O(n),这等同于顺序查找。 因此,如果我们希望对一个集合按二叉排序树查找,做好是把它们构建成一颗平衡的二叉排序树。这样我们就 引申出另一个问题,如何让二叉排序树平衡的问题。 */