数据结构与算法----树表查询与二叉排序树

418 阅读6分钟

树表查询简介

常见的树表查询算法为,二叉排序树(BST)、平衡二叉树(AVL)、红黑树、B树、B+树、B*树等

参考思维导图,这里循序渐进,逐渐介绍到红黑树即可,其他的可参考前面二叉排序树基本思想加以理解,或者参考其他文章更详细学习

本节主要讲解二叉排序树

二叉排序树(BST)

二叉排序树,又称二叉查找树,还称二叉搜索树

注意:其他排序树都是基于其逻辑基础进行改进的

特征

1.从根节点开始向两侧生成

2.左节点的值永远小于其父节点,右节点值永远大于其父节点(反向设置意义不大)

3.一般情况下查询效率要高于链表

4.插入和删除需要查找到指定位置方可操作

节点基本数据结构

typedef struct TreeNode {
    int data; //节点数据
    struct TreeNode *l, *r; //左右孩子
}LSTreeNode;

查询

所有二叉排序树的查询过程都是一致的,包括后面的平衡二叉树和红黑树,都是通过二分法向下查询

通过排序二叉树的特征:左节点的值永远小于其父节点,右节点值永远大于其父节点

因此:只需要从根节点开始,当前节点的值与查找值一致,则找到;比当前节点小,就到左孩子找;比当前节点大,则去右节点找;一直找到叶子节点为止

案例图

下图为查找节点11的过程

代码实现

//在排序树、平衡树查找返回指定内容节点
LSTreeNode * search(LSTreeNode *node, int value) {
    while (node) {
        if (node->data == value) return node;
        if (node->data > value) {
            node = node->l;
        }else {
            node = node->r;
        }
    }
    return NULL; //到这里就是没找到了
}

插入

插入的过程与查找类似,也是比父节点小的,去左节点插入,比父节点大的去右节点插入,直到左右节点为空时,插入即可

插入步骤如下:

1.记录父节点,和当前节点内容,用于更新节点

2.从根节点开始,向下进行对比遍历

3.查看当前节点是否存在,如果不存在(空树,或者遍历到的子节点为空),则进入步骤7

4.保存当前节点为父节点,以便后续使用

5.对比当前节点是否与查找节点一致,如果一致则更新内容,结束整个步骤;

6.如果节点值比查找的值小,则向左节点开始向下查找,即左节点作为当前节点;如果节点值比查找的大,则向右节点开始向下查找,右节点作为当前节点,然后回到步骤3

7.通过预留的父节点(最后应插入的节点的父节点),比插入数据大,就放到左孩子出,否则放到右孩子处,插入结束

案例图

下图为插入节点12的过程

代码实现

//二叉排序树的插入(不允许重复,如果重复就是替换更新功能了这里就不讨论了),如果比根节点大
LSTreeNode * insertBinSortNode(LSTreeNode *root, int data) {
    LSTreeNode *node = (LSTreeNode *)malloc(sizeof(LSTreeNode));
    node->data = data;
    if (!root) return node;
    LSTreeNode *p = root, *q = NULL;
    while (p) {
        q = p; //记录待插记录的父节点
        if (p->data == data) {
            free(node);
            return root;
        }; //该节点已经存在,结束(这一步可以进行替换更新功能)
        //只要比根节点大,就一直往左侧找,否则往右找,不停缩小范围,一直到叶子节点为止
        if (p->data > data) {
            p = p->l;
        }else {
            p = p->r;
        }
    }
    //比插入数据大,就放到左侧,否则右侧
    if (q->data > data) {
        q->l = node;
    }else {
        q->r = node;
    }
    return root;
}

删除

同查找一样,删除需要找到要删除的点,然后删除,如果删除的是叶子节点直接删除,否则,查找逻辑上一个节点或者逻辑下一个节点(该节点为叶子节点或单孩子节点)进行补位,即:将补位节点内容替换到被删除节点,然后删除补位节点接口

注意:无论采用上一个节点还是下一个节点,替换后虽然树有所不同,但不影响最终查询

删除步骤

1.检查是否为空树,如果是结束

2.查找要删除节点的位置,查找过程保存父节点,找到后进行下一步,找不到结束

3.如果要删除的节点是叶子节点,则直接删除;如果只有一个孩子,则将孩子节点赋值到要删除的节点位置,然后结束

4.其他情况就是在中间,这种情况直接查找其逻辑上一个节点(即左子树中序遍历的最后节点),然后将其值覆盖到被删除节点处

5.逻辑上一个节点如果是叶子节点,直接删除该节点;否则只有左孩子,如果父节点是原删除节点,则直接将原删除节点的左节点指针指向替换删除节点的左孩子即可,否则将替换删除节点其父节点右孩子指针指向删除节点的左孩子

6.删除该替换节点结束

案例图

下面是逻辑上一个节点替换的删除图

下面是逻辑下一个节点替换的删除图

代码实现

//删除二叉排序树某个节点(方案思路,叶子节点直接删除即可,非叶子节点将改替换的节点数据替换成上一个节点,上一个节点一般都是叶子节点或者没有右节点的节点)
LSTreeNode *deleteBinSortNode(LSTreeNode *root, int data) {
    if (!root) return NULL;
    LSTreeNode *p = root, *s = NULL;
    //查看现有二叉树是否存在data节点
    while (p) {
        if (p->data == data) break;
        s = p; //保存搜索到节点的上一个节点
        if (p->data > data) {
            p = p->l;
        }else {
            p = p->r;
        }
    }
    if (!p) return root;//没找到对应的点,结束
    
    if (!p->l && !p->r) {
        //为叶子节点
        if (p == root) root = NULL; //只有根节点置空
        else if (s->l == p) s->l = NULL; //不是根节点左右节点为空
        else s->r = NULL;
    }else if (!p->l) {
        //只有右侧分支, 那么吧p的父节点指向p的指针指向p的右节点
        if (p == root) root = p->r;
        else if (s->r == p) s->r = p->r;
        else s->l = p->r;
    }else if (!p->r) {
        //同右侧指向左侧
        if (p == root) root = p->l;
        else if (s->r == p) s->r = p->l;
        else s->l = p->l;
    }else {
        //两侧都存在的
        LSTreeNode *q = p; //保存查找替换点的数值逻辑的前一个节点的父节点
        s = p; //保存找到的原始节点
        p = p->l;
        while (p->r) {
            q = p;
            p = p->r;
        }
        q->data = p->data;
        if (s == q) s->l = p->l;
        else q->r = p->l;
    }
    free(p);
    return root;
}

最后

删除插入过程中会发现,排序二叉树会出现两极分化的情况,例如下图所示

这样查询效率会大大降低,排序二叉树演化成了链表,不符合树表查询设计初衷,因此引出了平衡二叉树、红黑树