数据结构-二叉查找树

前面介绍了树和二叉树的基本概念以及存储。

今天再来学习一下一种特殊的二叉树,二叉查找树。二叉查找树最大的特点就是,支持动态数据集合的快速插入、删除、查找操作。

散列表也是支持这些操作的,并且散列表更加的高效,时间复杂度是O(1)。既然有了这么高效的散列表,为什么还需要二叉查找树呢?使用二叉树的地方是不是都可以替换成散列表呢?有没有哪些地方是散列表做不了,必须要用二叉树来做的呢?

1.二叉查找树

又称为二叉搜索树,顾名思义,二叉查找树是为了实现快速查找而生的。不过,还支 持快速插入、删除一个数据。

1.1二叉查找树的结构

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

1.2二叉查找树的查找操作

查找过程:从根节点开始查找,如果查找的值等于根节点就返回。如果小于根节点,那么就在左子树中递归查找。如果大于根节点,则在右子树中递归查找。

java代码实现二叉查找:

public TreeNode find(int value){
    TreeNode node = root;
    while (node != null){
        if (value < node.value){
            node = node.leftNode;
        } else if (value > node.value){
            node = node.rightNode;
        }else {
            return node; 
        }
    }
    return null;
}

1.3二叉查找树的插入操作

二叉查找树的插入过程有点类似查找操作。新插入的数据一般都是在叶子节点上,所以我们只需要从根节点开始,依次比较要插入的数据和节点的大小关系。

如果要插入的数据比节点的数据大,并且节点的右子树为空,就将新数据直接插到右子节点的位置;如果不为空,就再递归遍历右子树,查找插入位置。同理, 如果要插入的数据比节点数值小,并且节点的左子树为空,就将新数据插入到左子节点的位置;如果不为空,就再递归遍历左子树,查找插入位置。

java代码实现插入:

public void insert(int value){
    if (root == null){
        root = new TreeNode(value);
        return;
    }
    TreeNode curNode = root;
    while (curNode != null){
        if (value < curNode.value){
            //在左子树
            if (root.leftNode == null){
                root.leftNode = new TreeNode(value);
            }
            curNode = root.leftNode;
        }else {
            //在右子树
            if (root.rightNode == null){
                root.rightNode = new TreeNode(value);
            }
            curNode = root.rightNode;

        }
    }
}

1.4二叉查找树的删除

相对于二叉查找树的查找和插入,二叉查找树的删除就比较复杂了。针对要删除节点的子节点个数的不同,我们需要分三种情况来处理:

1,如果要删除的节点没有子节点,我们只需要直接将父节点中,指向要删除节点的指针置为null。比下图中的删除节点55。

2,如果要删除的节点只有一个子节点(只有左子节点或者右子节点),我们只需要更新父节点中,指向要删除节点的指针,让它指向要删除节点的子节点就可以了。比下图中的删除节点13。

3,如果要删除的节点有两个子节点,这就比较复杂了。我们需要找到这个节点的右子树中的最小节点,把它替换到要删除的节点上。然后再删除掉 这个最小节点,因为最小节点肯定没有左子节点(如果有左子结点,那就不是最小节点了),所以,我们可以应用上面两条规则来删除这个最小节点。比如下图中的删除节点18。

java实现删除:

/**
 * 删除
 * @param value
 */
public void delete(int value){
    //先找到要删除的节点
    TreeNode delNode = null;//要删除的节点
    TreeNode delNodeParent = root;//要删除节点的父节点
    TreeNode curNode = root;//当前遍历节点
    boolean isLeftNode = false;
    while (curNode != null){
        if (value < curNode.value){
            delNodeParent = curNode;
            curNode = curNode.leftNode;
            isLeftNode = true;
        }else if (value > curNode.value){
            delNodeParent = curNode;
            curNode = curNode.rightNode;
            isLeftNode = false;
        }else {
            delNode = curNode;
            break;
        }
    }
    if (delNode == null){
        return;
    }
    
    //要删除的节点有两个子节点
    if (delNode.leftNode != null && delNode.rightNode != null){
        TreeNode minLeftNode = delNode.rightNode;//被删除节点的右子树最小的左节点
        TreeNode minLeftNodeParent = delNode;//表示minLeftNode的父节点
        while (minLeftNode.leftNode != null){
            //找出被删除节点的右子树最小的左节点
            minLeftNodeParent = minLeftNode;
            minLeftNode = minLeftNode.leftNode;
        }
        if (delNode == root){
            //删除的是根节点
            root = minLeftNode == null?delNode.leftNode:minLeftNode;
        }else {
            //替换删除的节点
            if (isLeftNode){
                delNodeParent.leftNode = minLeftNode;
            }else {
                delNodeParent.rightNode = minLeftNode;
            }
            //被删除节点的右子树最小的左节点的父节点 连接最小左节点的右节点
            minLeftNodeParent.leftNode = minLeftNode.rightNode; 
        }
        //被删除节点的右子树最小的左节点 连接被删除节点的左右节点
        minLeftNode.leftNode = delNode.leftNode;
        minLeftNode.rightNode = delNode.rightNode;

    }else {
        //删除的节点有一个子节点或者没子节点
        if (isLeftNode){
            delNodeParent.leftNode = delNode.leftNode == null ? delNode.rightNode : delNode.leftNode;
        }else {
            delNodeParent.rightNode = delNode.leftNode == null ? delNode.rightNode : delNode.leftNode;
        }
    }

}

解答开篇

我们在散列表那节中讲过,散列表的插入、删除、查找操作的时间复杂度可以做到常量级的O(1),非常高效。而二叉查找树在比较平衡的情况下,插入、删除、

查找操作时间复杂度才是O(logn),相对散列表,好像并没有什么优势,那我们为什么还要用二叉查找树呢? 我认为有下面几个原因:

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

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

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

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

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值