数据结构-动态查找-二叉排序树

507 阅读8分钟

开场白

工作中不只是仅仅进行查找工作,还会在查找后进行插入或删除。

  • 普通顺序存储:插入操作就是将记录插入在表的末端;删除操作可以是删除后,后面的记录前移,也可以用最后一个元素与要删除的元素互换,表记录减1。对于顺序表来说,插入和删除这两个操作效率还可以接受。但是顺序表本身的查找效率是很低的。
  • 有序存储:查找效率相对比较高,但是在每次插入或删除操作后,都需要对有序表进行排序或整理,这个需要花费大量时间。

二叉排序树就是一种即可以使得插入和删除的效率不错,又可以有比较高的查找效率的方案。

二叉排序树

二叉排序树:又称二叉查找树。它或者是一个空树,或者具有下列性质的二叉树:

  • 若左子树不为空,则左子树所有的结点的值均小于当前左子树的双亲结点值。
  • 若右子树不为空,则右子树的所有结点的值均大于当前右子树的双亲结点值。
  • 左右子树也分别为二叉排序树。

构建二叉排序树

集合:{62,88,58,47,35,73,51,99,37,93}

  1. 第一个元素62作为根结点;
  2. 第二个元素88>62,结点88作为根结点66的右孩子;
  3. 第三个元素58<62,结点58作为根结点66的左孩子;
  4. 第四个元素47<62,找到结点62的左子树根结点58,47<58,结点47左右结点58的左孩子;
  5. 第五个结点35<62,35<58,35<47,结点35作为47的左孩子
  6. ......

通过这种方式,我们就可以创建一颗二叉树了,并且当我们进行中序遍历的时候,就可以得到一个有序的序列{35,37,47,51,58,62,73,88,93,99},这棵树就是我们所说的二叉排序树。

二叉排序树的查找操作

#define TRUE 1
#define FALSE 0

typedef int Status;

typedef struct BiTNode{
    int data;
    struct BiTNode *lChild, *rChild;
} BiTNode, *BiTree;

//查找
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);
    }
}
  1. 递归函数。BiTree T:链表树;int key:要查找的元素;BiTree f:指向T的双亲结点,递归时使用,如果T是根结点,f为NULL;BiTree *p:查找成功后返回的结点位置,不成功指向最后查找位置的双亲结点;用key=93距离,SearchBST(T,93,NULL,p)
  2. 根结点T存在,且T->data=62 < key=93,进入下一层递归,执行在右子树查找
  3. 根结点T->data=88 < key=93,进入下一层递归,执行在右子树查找
  4. 根结点T->data=99 > key=93,进入下一层递归,执行在左子树查找
  5. 根结点T->data=93 == key=93,返回成功,递归逐层返回。

二叉排序树插入操作

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 {//s结点作为右孩子
            p->rChild = s;
        }
        return TRUE;
    }
    
    return FALSE;
}

查找不成功,进行插入操作。

  1. 创建结点s
  2. 树不存在,s作为树的根
  3. 树存在,p指向最后查找到叶子结点。key小于p->data,s作为p的左孩子;key大于p->data,s作为p的右孩子。

二叉排序树删除操作

“请神容易送神难”,删除操作要比插入操作考虑的事情要多了,大概有3种情况

  1. 删除叶子结点
  2. 删除只有左子树或者右子树的结点
  3. 删除同时含有左子树和右子树的结点
1.删除叶子结点

这种情况是最简单的,直接删除就可以了

例如图片中的37、51、73、93这些红色的结点

2.删除只有左子树或者右子树的结点

这种情况也不难,直接将左子树或者右子树的根结点补充到要删除结点的位置上就可以了

例如图中的删除结点35,直接把右孩子结点37补充到35的位置就可以了;同理结点58和结点99

3.删除同时含有左子树和右子树的结点

这种情况是比较复杂的,如图删除结点47:

脑中第一个想法就是暴力法,保留左子树,遍历右子树中的结点调用插入函数。后来发现这么做效率不高,而且会导致整个二叉排序树结构发生很大的变化:比如增加数的高度。增加树的高度,会增加查找路径。

替换法:用要删除的结点的左子树或者右子树中的叶子结点代替,之前说过左子树的所有结点都小于根结点;右子树所有的结点都大于根结点。那么我们用左子树中的最大值或者右子树中的最小值就可以替换掉要删除的结点了。这样对原来的树的其他结点不会产生什么影响。

例子中可以使用左子树中的37结点和右子树中的48结点替换,代码中我们用左子树来处理:

//删除结点,并重接该结点的子树
Status Delete(BiTree *p) {
    BiTree temp, s;
    if((*p)->rChild == NULL) {//没有右孩子
        temp = *p;
        *p = (*p)->lChild;
        free(temp);
    } else if((*p)->lChild == NULL) {//没有左孩子
        temp = *p;
        *p = (*p)->rChild;
        free(temp);
    } else {//左右孩子都存在
        temp = *p;
        s = (*p)->lChild;//s指向左子树
        
        while (s->rChild) {//找到最后一个叶子结点
            temp = s;//记录前驱结点
            s = s->rChild;//s结点才是最后要删除释放的结点,因为s的相关信息赋值给了在外部看来要删除的结点上
        }
        
        //找到的叶子结点的data赋值给要删除结点的data
        (*p)->data = s->data;
        
        if (temp != *p) {//s的左子树整体顶替到s的位置
            temp->rChild = s->lChild;
        } else {//s没有右子树
            temp->lChild = s->lChild;
        }
        free(s);
    }
    return TRUE;
}

//查找结点,并将其在二叉排序中删除
Status DeleteBST(BiTree *T, int key) {
    if (!*T) {
        return FALSE;
    } else {
        if (key == (*T)->data) {//找到结点进行删除操作
            return Delete(T);
        } else if (key < (*T)->data) {
            return DeleteBST(&(*T)->lChild, key);
        } else {
            return DeleteBST(&(*T)->rChild, key);
        }
    }
}

代码解读:

  1. 执行DeleteBST递归函数,这个函数与查找SearchBST函数实现差不多;
  2. 如果T不存在,返回FALSE;
  3. key与(*T)->data)不相等,继续递归查找
  4. 相等走Delete函数;
  5. 如果没有右子树,左子树顶替到指定位置;
  6. 如果没有左子树,右子树顶替到指定位置;
  7. 左右子树都存在,遍历查找左子树中的右子树最后一个右孩子结点,进行替换

模拟删除结点47:

  1. 找到47结点后,*p指向47,s指向47左子树的根结点35,temp指向s的前驱结点47
  2. 遍历找到s树找它的右子树,同时修改temp和s,最终s指向37,temp指向35
  3. (*p)->data = s->data,断开temp->rChild与s的关系,将temp->rChild与s->lChild连接起来
  4. free(s)

总结

二叉排序树是以链接的方式存储,保持了链接存储结构再执行插入和删除操作时的优势,只要找到合适的插入和删除位置后,仅仅需要修改指针即可。时间性能比较好。对于查找,就是从根结点到要查找的结点的路径,比较次数就是树的层数。也就说二叉排序树的查找性能,取决于二叉排序树的形状。而树的形状取决于最初的数组。

举一个极端的例子:最初的数组是有序的{35,37,47,51,58,62,73,88,93,99},样子如图

通常称之为右斜树,它依然是一个二叉排序树,如果这种情况下去查询99结点,就要从头遍历到尾部。此时我们需要对这棵树进行平衡处理,下篇文章研究研究平衡二叉树