二叉排序树的查找、插入和删除
1. 二叉排序树
二叉排序树(Binary Sort Tree)或者是一棵空树,或者是具有下列性质的二叉树:
(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
(3)左、右子树也分别为二叉排序树;
(4)没有结点值相同的结点。
二叉排序树又称二叉查找树(Binary Search Tree),亦称二叉搜索树。通常采用二叉链表作为二叉排序树的存储结构。中序遍历二叉排序树可以得到关键字有序的序列,即一个无序序列可以通过构造二叉排序树成为有序序列,构造树的过程即为对无序序列排序的过程。
2. 查找
当二叉排序树不为空时,首先将给定值和根结点的关键字比较,若相等则查找成功;若给定值小于根结点的关键字,在左子树上递归查找;若给定值大于根结点的关键字,在右子树上递归查找。
代码:
/* * 在以T为根结点的树中,查找与给定值key相同的结点。 * 如果存在返回指向该结点的指针,否则返回空指针。 */ BiTree SearchBST1(BiTree T, TElemType key) { if (!T) //空树,查找失败 return NULL; else if (key == T->data) //查找成功 return T; else if (key < T->data) //在左子树中继续查找 return SearchBST1(T->lchild, key); else //在右子树中继续查找 return SearchBST1(T->rchild, key); }
3. 插入
二叉排序树是一种动态树表。树的结构不是一次生成的,而是在查找过程中,当树中不存在关键字等于给定值的结点时再进行插入。新插入的结点一定是叶子结点,而且一定是查找不成功时查找路径上最后一个结点的左孩子或者右孩子。重写上述查找算法,以便查找不成功时,函数可以返回新结点的插入位置。
代码:
/* * 在以T为根结点的树中,查找与key相同的结点。 * 形参中,结点F为结点T的父节点,初始调用值为NULL。 * 如果存在此结点,返回TRUE,并使*p指向该结点; * 如果不存在此结点,返回FALSE,并使*p指向查找路径上的最后一个结点。 */ bool SearchBST2(BiTree T, BiTree F, TElemType key, BiTree *p) { //函数中,只能对*p进行修改,而不能试图修改p的值。 //因为p为形参,形参的改变并不会影响实参。 if (!T) { //T为空树,此时父结点F或者为NULL,或者为查找路径上的最后一个结点。 //查找失败 *p = F; return false; } else if (key == T->data) //查找成功 { *p = T; return true; } else if (key < T->data) { return SearchBST2(T->lchild, T, key, p); } else { return SearchBST2(T->rchild, T, key, p); } } /* * 在树*T中查找与key相同的结点。 * 如果存在返回FALSE,否则将key值插入到树中。 */ bool InsertBST(BiTree *T, TElemType key) { BiTree p, n; if (SearchBST2(*T, NULL, key, &p)) return false; else { n = (BiTree)malloc(sizeof(BiNode)); n->data = key; n->lchild = NULL; n->rchild = NULL; //注意不要漏掉p为NULL的情况, //此时表明*T为空树,将新结点直接作为根结点。 if (!p) *T = n; //key比查找路径上的最后一个结点小,则将n作为p的左子树。 else if (key < p->data) { p->lchild = n; } //key比查找路径上的最后一个结点大,则将n作为p的右子树。 else { p->rchild = n; } return true; } }
4. 删除
对于二叉排序树,删除一个结点相当于删除有序序列中的一个记录,只要在删除某结点之后调整树中某些结点,使之继续保持二叉排序树的特性即可。
假如要删除的结点为*p(p为指向结点的指针),*p的父节点为*f,不失一般性,假设*p为*f的左孩子结点:
1) 若*p为叶子结点,由于删除叶子结点不破坏树的结构,因此只需修改f->lchild为空指针即可。
2) 若*p只有左子树PL或者只有右子树PR,此时只要令PL或PR直接成为*f的左子树即可。
3) 若*p既有左子树又有右子树,此时有两种处理方法:一是先直接令PL为*f的左子树,再令PR为PL子树中最右孩子的右子树。PL子树中最右孩子即为PL子树中最大的结点。二是令*p的直接前驱(或直接后继)代替*p,然后从二叉排序树中删除它的直接前驱(或直接后继)。*p的直接前驱为PL子树中最右孩子,大小仅次于*p;*p的直接后继为PR子树中最左孩子,大小仅大于*p。
代码:(使用第一种处理方法)
/* * 在以*T为根结点的树中,删除与key相同的结点。 * 如果没有此结点返回FALSE。 */ bool DeleteBST(BiTree *T, TElemType key) { if (!*T) //空树。查无此结点。 return false; else if (key == (*T)->data) { Delete(T); return true; } else if (key < (*T)->data) { return DeleteBST(&((*T)->lchild), key); } else { return DeleteBST(&((*T)->rchild), key); } } /* * 删除*T指向的结点 */ bool Delete(BiTree *T) { BiTree L; //*T既没有左孩子,又没有右孩子,为叶子结点 if (!(*T)->lchild && !(*T)->rchild) *T = NULL; //*T只有右孩子 else if (!(*T)->lchild) *T = (*T)->rchild; //*T只有左孩子 else if (!(*T)->rchild) *T = (*T)->lchild; //*T既有左孩子,又有右孩子 else { L = (*T)->lchild;//L指向被删除结点的左子树 //寻找L的最右孩子 while (L->rchild) L = L->rchild; //把*T的右子树接到左子树最右孩子的右子树上。 L->rchild = (*T)->rchild; //*T的左子树直接作为*T父结点的子树 *T = (*T)->lchild; } return true; }
5. 时间复杂度
二叉排序树相对于其他数据结构的优势在于查找、插入、删除的时间复杂度较低。含有n个结点的二叉排序树的平均查找长度和树的形态有关。
最坏的情况:当先后插入的关键字有序时,二叉排序树退化为单枝树,树的深度为n,平均查找长度为(n+1)/2。
最好的情况:二叉排序树的形态和折半查找的判定树相同,平均查找长度和log2n成正比。
平均性能:随机情况下,平均查找长度和logn成正比。P(n)=O(logn)。
6. 测试代码(需额外添加上述代码)
/* * 二叉排序树的查找,插入和删除操作 */ #define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <stdlib.h> #include <stdbool.h> #define TOTAL 10 //二叉树的二叉链表存储表示 typedef int TElemType; typedef struct BiNode { TElemType data; struct BiNode *lchild; struct BiNode *rchild; } BiNode,*BiTree; //函数声明 BiTree SearchBST1(BiTree T, TElemType key); bool SearchBST2(BiTree T, BiTree f, TElemType key, BiTree *p); bool InsertBST(BiTree *T, TElemType key); bool DeleteBST(BiTree *T, TElemType key); bool Delete(BiTree *T); bool InOrderTraverse_Recursive(BiTree T, bool(*Visit)(TElemType e)); bool PrintElement(TElemType e); int main() { BiTree T = NULL;//注意一定要先将T初始化为空树 int i, num; //使用插入操作构建二叉排序树。(插入过程中已检验查找函数SearchBST2()) printf("输入TOTAL个数构建二叉排序树:\n"); for (i = 0; i < TOTAL; i++) { scanf("%d", &num); InsertBST(&T, num); } //检验插入效果。中序遍历该树,若结果为由小到大,表明插入成功。 printf("中序遍历该树:\n"); InOrderTraverse_Recursive(T, PrintElement); putchar('\n'); //删除操作 printf("输入待删除的数:"); scanf("%d", &num); DeleteBST(&T, num); //检验删除操作 printf("重新中序遍历该树:\n"); InOrderTraverse_Recursive(T, PrintElement); putchar('\n'); //检验查找操作SearchBST1() printf("输入待查找的数:"); scanf("%d", &num); if (SearchBST1(T, num)) printf("Yes\n"); else printf("No\n"); return 0; } /* * 中序递归遍历 */ bool InOrderTraverse_Recursive(BiTree T, bool(*Visit)(TElemType e)) { if (T) { if (InOrderTraverse_Recursive(T->lchild, Visit)) if (Visit(T->data)) if (InOrderTraverse_Recursive(T->rchild, Visit)) ; } return true; } /* * 遍历函数 */ bool PrintElement(TElemType e) { printf("%-3d ",e); return true; }
7. 测试结果
参考: