欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

二叉查找树

程序员文章站 2022-03-04 21:10:28
...

二叉查找树要求,在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值。
二叉查找树

public class TreeNode {
    private int data;
    private TreeNode leftNode;
    private TreeNode rightNode;
}

public class BinarySearchTree {

    private TreeNode rootNode;

    public TreeNode find(int data) {
        TreeNode currentNode = rootNode;
        while (currentNode != null) {
            if (currentNode.getData() == data) {
                return currentNode;
            } else if (currentNode.getData() > data) {
                currentNode = currentNode.getLeftNode();
            } else {
                currentNode = currentNode.getRightNode();
            }
        }
        return null;
    }

    public void insert(int data) {
        if (rootNode == null) {
            rootNode = new TreeNode(data);
            return;
        }
        TreeNode currentNode = rootNode;
        while (true) {
            if (currentNode.getData() > data) {
                if (currentNode.getLeftNode() == null) {
                    currentNode.setLeftNode(new TreeNode(data));
                    return;
                }
                currentNode = currentNode.getLeftNode();
            } else {
                if (currentNode.getRightNode() == null) {
                    currentNode.setRightNode(new TreeNode(data));
                    return;
                }
                currentNode = currentNode.getRightNode();
            }
        }
    }


    /**
     * 删除的情况比较复杂
     * 第一种情况是,如果要删除的节点没有子节点,我们只需要直接将父节点中,指向要删除节点的
     * 指针置为 null。
     * <p>
     * 第二种情况是,如果要删除的节点只有一个子节点(只有左子节点或者右子节点),我们只需要
     * 更新父节点中,指向要删除节点的指针,让它指向要删除节点的子节点就可以了。
     * <p>
     * 第三种情况是,如果要删除的节点有两个子节点,这就比较复杂了。我们需要找到这个节点的右
     * 子树中的最小节点,把它替换到要删除的节点上(比要删除的节点大一点点的数)。
     * 然后再删除掉这个最小节点(父节点的指针改变),因为最小节点肯定没有左子节点(如果有左子结点,那就不是最小节点了),
     * 所以,我们可以应用上面两条规则来删除这个最小节点
     * <p>
     * 注:左子树最大节点是可能有子节点的,左子树总是小于根节点的,右子树总是大于根节点的
     *
     * @param data
     */

    public void delete(int data) {
        TreeNode treeNode = rootNode;
        TreeNode parentNode = null;
        while (treeNode != null && treeNode.getData() != data) {
            parentNode = treeNode;
            if (treeNode.getData() > data) {
                treeNode = treeNode.getLeftNode();
            } else {
                treeNode = treeNode.getRightNode();
            }
        }
        if (treeNode == null) {
            return;
        }
        if (treeNode.getLeftNode() != null && treeNode.getRightNode() != null) {
            //有两个节点的时候
            TreeNode minNode = treeNode.getRightNode();
            TreeNode minParentNode  =treeNode;
            while (minNode.getLeftNode()!=null){
                minParentNode = minNode;
                minNode = minNode.getLeftNode();
            }
            treeNode.setData(minNode.getData());
            //接下来删除这个最小节点,走下面的流程
            treeNode = minNode;
            parentNode = minParentNode;
        }

        //证明有一个节点或者0个节点的时候
        TreeNode child;//删除节点的子节点
        if (treeNode.getLeftNode() != null) {
            child = treeNode.getLeftNode();
        } else {
            child = treeNode.getRightNode();
        }

        if (parentNode == null) {
            rootNode = child;//清空二叉树
            return;
        }
        if (parentNode.getLeftNode() == treeNode) {
            parentNode.setLeftNode(child);
        }
        if (parentNode.getRightNode() == treeNode) {
            parentNode.setRightNode(child);
        }

    }
    public void printNode() {
        inPrint(rootNode);
    }

    private void inPrint(TreeNode treeNode){
        if (treeNode.getLeftNode()!=null){
            inPrint(treeNode.getLeftNode());
        }
        System.out.println(treeNode.getData());
        if (treeNode.getRightNode()!=null){
            inPrint(treeNode.getRightNode());
        }
    }

}

通过上面的分析可知,我们的查找删除新增,其时间复杂度都和树的高度成正比,为O(height),遍历的时间复杂度是O(n),树的高度等于最大层数-1

包含 n个节点的完全二叉树中,第一层包含 1 个节点,第二层包含 2 个节点,第三层包含 4 个节点。
依次类推,下面一层节点个数是上一层的 2 倍,第 K 层包含的节点个数就是 2^(K-1)。

不过,对于完全二叉树来说,最后一层的节点个数有点儿不遵守上面的规律了。它包含的节点个
数在 1 个到 2^(L-1) 个之间(我们假设最大层数是 L)。如果我们把每一层的节点个数加起来
就是总的节点个数 n。也就是说,如果节点的个数是 n,那么 n 满足这样一个关系:

n >= 1+2+4+8+…+2^(L-2)+1 最后一层只有1个
n <= 1+2+4+8+…+2^(L-2) + 2^(L-1) 最后一层满了

借助等比数列的求和公式,我们可以计算出,L 的范围是 [log 2^(n+1), log2^(n) +1]。
完全二叉树的层数小于等于 log2^(n) +1,
也就是说,完全二叉树的高度小于等于 log2^n。

显然,极度不平衡的二叉查找树,它的查找性能肯定不能满足我们的需求。我们需要构建一种不
管怎么删除、插入数据,在任何时候,都能保持任意节点左右子树都比较平衡的二叉查找树,一种特殊的二叉查找树,平衡二叉查找树。平衡二叉查找树的高度接近 log2^n,所以插入、删除、查找操作的时间复杂度也比较稳定,是 O(logn)。

相对于hash表来讲,其删除,增加,查找的时间复杂度都是O(1),为什么还要用二叉查找树呢;

第一,散列表中的数据是无序存储的,如果要输出有序的数据,需要先进行排序。而对于二叉查找树来说,我们只需要中序遍历,就可以在 O(n) 的时间复杂度内,输出有序的数据序列。

第二,散列表扩容耗时很多,而且当遇到散列冲突时,性能不稳定,尽管二叉查找树的性能不稳定,但是在工程中,我们最常用的平衡二叉查找树的性能非常稳定,时间复杂度稳定在O(logn)。

第三,笼统地来说,尽管散列表的查找等操作的时间复杂度是常量级的,但因为哈希冲突的存在,这个常量不一定比 logn 小,所以实际的查找速度可能不一定比 O(logn) 快。加上哈希函数的耗时,也不一定就比平衡二叉查找树的效率高。

第四,散列表的构造比二叉查找树要复杂,需要考虑的东西很多。比如散列函数的设计、冲突解决办法、扩容、缩容等。平衡二叉查找树只需要考虑平衡性这一个问题,而且这个问题的解决方案比较成熟、固定。

最后,为了避免过多的散列冲突,散列表装载因子不能太大,特别是基于开放寻址法解决冲突的散列表,不然会浪费一定的存储空间。

综合这几点,平衡二叉查找树在某些方面还是优于散列表的,所以,这两者的存在并不冲突。我们在实际的开发过程中,需要结合具体的需求来选择使用哪一个。

相关标签: 数据结构和算法