数据结构系列(5)— 二叉查询树

525 阅读11分钟

学 无 止 境 , 与 君 共 勉 。


相关系列

什么是树

树是由边连接的节点构成的一种数据结构,每棵树有N个节点和N-1条边。

  • 根:树顶端的节点叫做树的根;
  • 路径:从一个节点到另一个节点所经过的节点顺序叫做路径;
  • 父节点:除根节点外指向自己的节点称为它的父节点(有且只有一个);
  • 子节点:当前节点指向的节点称为它的子节点(可以有多个);
  • 兄弟节点:拥有相同父节点的节点称为兄弟(siblings);
  • 叶节点:没有子节点的节点称为“叶子节点”,简称“叶节点”(leaf);
  • 子树:每个节点都可以称为是“子树”的根,它和它下面的节点构成了一个子树;
  • 层:从根节点到这个节点有多少代;

二叉树

二叉树是每个节点最多只能有两个子树的树结构。通常子树被称作“左子树”(left-subtree)和“右子树”(right-subtree),它通常被用于实现二叉查询树和二叉堆。这里我们主要讲二叉查询树;

二叉查询树

本文主要讲解二叉查询树的内容,二叉查询树有以下几个特性:

  1. 若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  2. 若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  3. 左、右子树也分别为二叉查询树;
  4. 没有键值相等的结点;

节点信息

二叉查询树节点除了包含数据外,还包含了对其左右子节点的引用

    private static class TreeNode<E{

        E data;

        TreeNode<E> leftChild;

        TreeNode<E> rightChild;

        public TreeNode(E data, TreeNode<E> leftChild, TreeNode<E> rightChild) {
            this.data = data;
            this.leftChild = leftChild;
            this.rightChild = rightChild;
        }
    }

查询

  • 在while循环中从根节点开始对比,如果值相同,则返回
  • 小于对比节点的,和对比节点的左子节点进行对比;
  • 大于对比节点的,和对比节点的右子节点进行对比;
  • 如果遇到子节点不存在,则查询不成功;
    public TreeNode<E> find(E data) {
        TreeNode<E> curNode = root;
        int compareResult;
        while (true) {
            compareResult = data.compareTo(curNode.data);
            if (compareResult == 0) {
                break;
            }
            if (compareResult < 0) {
                curNode = curNode.leftChild;
            } else {
                curNode = curNode.rightChild;
            }
            if (curNode == null) {
                return null;
            }
        }
        return curNode;
    }

插入

新插入的节点总是叶子节点(没有子节点),所以只需要找到要插入节点的父节点,然后将父节点的左子节点或者右子节点指向新插入的节点。如果当前树为空,则在在根节点插入。

    /**
     * while循环的方式插入数据,
     * 比递归方式更好
     */
    
    public void insert(E data) {
        TreeNode<E> newNode = new TreeNode<>(data, nullnull);
        if (root == null) {
            this.root = newNode;
            return;
        }
        TreeNode<E> parentNode = root;
        int compare;
        while (true) {
            compare = data.compareTo(parentNode.data);
            // 左节点
            if (compare < 0) {
                if (parentNode.leftChild == null) {
                    parentNode.leftChild = newNode;
                    return;
                } else {
                    parentNode = parentNode.leftChild;
                }
            } else if (compare > 0) {
                // 右节点
                if (parentNode.rightChild == null) {
                    parentNode.rightChild = newNode;
                    return;
                } else {
                    parentNode = parentNode.rightChild;
                }
            } else {
                System.out.println(String.format("已存在相同元素【%s】", data));
                return;
            }
        }
    }

删除

  • 首先获取到要删除的节点
    // 删除节点的父节点    
    TreeNode<E> parentNode;
    TreeNode<E> delNode = root;
    // 要删除的节点是否为左子节点
    boolean isLeftNode = true;
    int compareResult;
    // 找到要删除的节点
    while (true) {
        parentNode = delNode;
        compareResult = data.compareTo(delNode.data);
        if (compareResult == 0) {
            break;
        }
        if (compareResult < 0) {
            isLeftNode = true;
            delNode = delNode.leftChild;
        } else {
            isLeftNode = false;
            delNode = delNode.rightChild;
        }
        if (delNode == null) {
            System.out.println(String.format("删除失败,没有当前数据【%s】", data));
            return false;
        }
    }
  • 删除叶子节点(没有子节点)

如果删除的是根节点,直接将根节点设为NULL

否则直接将删除节点的父节点对应的左子节点或者右子节点设为NULL

    // 删除叶子节点
    if (delNode.leftChild == null && delNode.rightChild == null) {
        // 删除根目录
        if (delNode == root) {
            this.root = null;
            return true;
        }
        if (isLeftNode) {
            parentNode.leftChild = null;
        } else {
            parentNode.rightChild = null;
        }
    }
  • 要删除的节点存在一个子节点

如果删除的是根节点,直接将根节点指向删除节点的子节点;

否则将删除节点的父节点的左子节点或者右子节点指向删除节点的子节点

    // 只有1个子节点的,获取不为空的子节点
    TreeNode<E> childNode = delNode.leftChild == null ? delNode.rightChild:delNode.leftChild;
    if (delNode == root) {
        root = childNode;
    } else {
        if (isLeftNode) {
            parentNode.leftChild = childNode;
        } else {
            parentNode.rightChild = childNode;
        }
    }
  • 要删除的节点有两个子节点

一般我们用删除节点右子树的最小值得节点去代替删除节点的位置,称为后继节点

如果后继节点存在右子节点,这个后继父节点的左子节点指向这个后继右子节点。

后继节点的左子节点指向删除节点的左子节点。

    // 有两个子节点,先获取代替删除节点的后继节点
    TreeNode<E> successor = getSuccessor(delNode);
    if (delNode == root) {
        root = successor;
    } else {
        if (isLeftNode) {
            parentNode.leftChild = successor;
        } else {
            parentNode.rightChild = successor;
        }
    }    

    /**
     * @param delNode 要删除的元素
     * @return 后继节点
     */

    private TreeNode<E> getSuccessor(TreeNode<E> delNode) {
        // 后继节点
        TreeNode<E> successor = delNode;
        // 后继节点的父节点
        TreeNode<E> successorParent = delNode;
        TreeNode<E> curNode = delNode.rightChild;
        while (curNode != null) {
            successorParent = successor;
            successor = curNode;
            curNode = curNode.leftChild;
        }
        // 删除节点的右节点存在左子节点
        if (successor != delNode.rightChild) {
            // 上移后继节点的右子节点
            successorParent.leftChild = successor.rightChild;
            successor.rightChild = delNode.rightChild;
        }
        // 后继节点的左子节点指向删除节点的左子节点
        successor.leftChild = delNode.leftChild;
        return successor;
    }

性能优化

当插入的数据是有序时,构成的树将变成单支树,所有的节点在一条直线上,没有分支,这时候就相当于一个链表传送门:链表),这个时候它的时间复杂度变成了O(n)。针对这种情况,我们可以采用平衡树去处理,如ALV树红黑树

访问源码

本系列所有代码均上传至Github上,方便大家访问

>>>>>> 数据结构-二叉查询树 <<<<<<

日常求赞

创作不易,如果各位觉得有帮助,求点赞支持

求关注