20 二叉查找树

二叉查找树结构

二叉查找树又称二叉搜索树,顾名思义是为了快速查找而生。实际上还支持快速插入和快速删除。
二叉查找树要求:
在任意的一个节点中,其左子树的每个值,都要小于该节点的值,而其右子树的每个节点的 值都要大于该节点值。
在这里插入图片描述

二叉查找树的查询、插入、删除操作

二叉查找树-查找

思想:如果要在一个大的树中查找一个值x,先取根节点的值与x对比,如果x=根节点的值,则返回根节点值。如果x<根节点的值,则在左子树中递归查找;如果x>根节点的值,则在右子树中递归查找。
在这里插入图片描述

实现代码:

public static Node find(int data){
        Node p = node33 ;
        while (p !=null){
            if (data == p.getData() ){
                System.out.println(p.getData());
                return  p;
            }else if (data < p.getData()){
                System.out.println(p.getData());
                p = p.getLeftNode();
            }else {
                System.out.println(p.getData());
                p = p.getRightNode();
            }
        }
        return null;
    }

完整代码:

package Learn;

import java.util.Queue;
import java.util.concurrent.LinkedBlockingDeque;

public class tree {
    static Node node19 = new Node(19,null,null);
    static Node node27 = new Node(27,null,null);
    static Node node16 = new Node(16,null,null);
    static Node node25 = new Node(25,node19,node27);
    static Node node51 = new Node(51,null,null);
    static Node node66 = new Node(66,null,null);
    static Node node13 = new Node(13,null,node16);
    static Node node18 = new Node(18,null,node25);
    static Node node34 = new Node(34,null,null);
    static Node node58 = new Node(58,node51,node66);
    static Node node17 = new Node(17,node13,node18);
    static Node node50 = new Node(50,node34,node58);
    static Node node33 = new Node(33,node17,node50);

    public static void main(String[] args) {
        Node node = find(27);
        System.out.println(node.getData());
    }
    public static Node find(int data){
        Node p = node33 ;
        while (p !=null){
            if (data == p.getData() ){
                System.out.println(p.getData());
                return  p;
            }else if (data < p.getData()){
                System.out.println(p.getData());
                p = p.getLeftNode();
            }else {
                System.out.println(p.getData());
                p = p.getRightNode();
            }
        }
        return null;
    }

class Node{
    private int data;
    private Node leftNode;
    private Node rightNode;

    public Node(int data, Node leftNode, Node rightNode) {
        this.data = data;
        this.leftNode = leftNode;
        this.rightNode = rightNode;
    }

    public int getData() {
        return data;
    }

    public void setData(int data) {
        this.data = data;
    }

    public Node getLeftNode() {
        return leftNode;
    }

    public void setLeftNode(Node leftNode) {
        this.leftNode = leftNode;
    }

    public Node getRightNode() {
        return rightNode;
    }

    public void setRightNode(Node rightNode) {
        this.rightNode = rightNode;
    }
}

二叉查找树-插入

二叉查找树插入数据跟查找类似。
思想:
首先确认二叉查找树不插入在树已存在的数据。假如要插入的数据x在树中不存在,且插入的数据比根节点大,如果根节点右子节点为空,则插入右子节点中,如果不为空,则递归遍历右子树,查找插入位置。同理如果要插入的数据x比根节点小,且根节点左子节点为空,则插入到左子节点中,否则递归遍历左子树,查找插入位置。
在这里插入图片描述

主要实现代码:

public static void insertNode(int data){
        Node p = node33;
        if (p == null){
            node33 = new Node(data,null,null);
        }
        while (p != null){
            if (data > p.getData()){
                if (p.rightNode==null){
                    p.rightNode = new Node(data,null,null);
                    return;
                }
                p = p.rightNode;
            }else {
                if (p.leftNode==null){
                    p.leftNode = new Node(data,null,null);
                    return;
                }
                p = p.leftNode;
            }
        }
    }

完整代码:

package Learn;

import java.util.Queue;
import java.util.concurrent.LinkedBlockingDeque;

public class tree {
    static Node node19 = new Node(19,null,null);
    static Node node27 = new Node(27,null,null);
    static Node node16 = new Node(16,null,null);
    static Node node25 = new Node(25,node19,node27);
    static Node node51 = new Node(51,null,null);
    static Node node66 = new Node(66,null,null);
    static Node node13 = new Node(13,null,node16);
    static Node node18 = new Node(18,null,node25);
    static Node node34 = new Node(34,null,null);
    static Node node58 = new Node(58,node51,node66);
    static Node node17 = new Node(17,node13,node18);
    static Node node50 = new Node(50,node34,node58);
    static Node node33 = new Node(33,node17,node50);

    public static void main(String[] args) {
        insertNode(55);
        System.out.println(node51.getRightNode().getData());
    }
    public static void insertNode(int data){
        Node p = node33;
        if (p == null){
            node33 = new Node(data,null,null);
        }
        while (p != null){
            if (data > p.getData()){
                if (p.rightNode==null){
                    p.rightNode = new Node(data,null,null);
                    return;
                }
                p = p.rightNode;
            }else {
                if (p.leftNode==null){
                    p.leftNode = new Node(data,null,null);
                    return;
                }
                p = p.leftNode;
            }
        }
    }

   }

class Node{
    public int data;
    public Node leftNode;
    public Node rightNode;

    public Node(int data, Node leftNode, Node rightNode) {
        this.data = data;
        this.leftNode = leftNode;
        this.rightNode = rightNode;
    }

    public int getData() {
        return data;
    }

    public void setData(int data) {
        this.data = data;
    }

    public Node getLeftNode() {
        return leftNode;
    }

    public void setLeftNode(Node leftNode) {
        this.leftNode = leftNode;
    }

    public Node getRightNode() {
        return rightNode;
    }

    public void setRightNode(Node rightNode) {
        this.rightNode = rightNode;
    }
}

二叉查找树-删除

删除操作比较麻烦,分三种情况
1、要删除的节点没有子节点,直接将该节点的父节点要指向该节点的指针置为null即可。
2、要删除的节点只有一个子节点(左或右),只需要更新父节点原本指向该节点的指针置为该节点的子节点即可。
3、要删除的节点有两个节点,需要找到该节点右子树上最小的节点,然后把最小节点替换到要删除的节点,再删除这个最小节点。
在这里插入图片描述

实现代码

public class tree {
    static Node node19 = new Node(19,null,null);
    static Node node27 = new Node(27,null,null);
    static Node node55 = new Node(55,null,null);
    static Node node15 = new Node(15,null,null);
    static Node node17 = new Node(17,null,null);
    static Node node25 = new Node(25,node19,node27);
    static Node node51 = new Node(51,null,node55);
    static Node node66 = new Node(66,null,null);
    static Node node13 = new Node(13,null,node15);
    static Node node18 = new Node(18,node17,node25);
    static Node node34 = new Node(34,null,null);
    static Node node58 = new Node(58,node51,node66);
    static Node node16 = new Node(16,node13,node18);
    static Node node50 = new Node(50,node34,node58);
    static Node node33 = new Node(33,node16, node50);
    public static void main(String[] args) {
        deletNode(55);
        System.out.println(node51.getRightNode().getData());
    }

    public static void deletNode(int data){
        Node p =null; //要删除的节点
        Node pp = null;//要删除的节点的父节点
        p = node33; //指向根节点,遍历根节点找到要删除的节点
        while (p != null && p.data != data){
            pp = p;
            if (data > p.data) {
                p = p.rightNode;
            }else p=p.leftNode;
        }// while循环后找到了要删除的p电
        if (p==null) return;; //如果找不到就返回

        //此时p是要删除的节点,pp是此节点的父节点,
        // 如果被删除的节点有两个子节点,就把要删除的节点数据替换该节点右子树最小的值,
        // 替换后要删除最小节点的原来节点,
        // 因为最小的值肯定是没有左子节点的(即可能存在一个右子节点,或者没有子节点)。
        // 所以要把这个问题降维成了(被删除节点最多只有一个子节点的问题)
        if (p.leftNode !=null && p.rightNode !=null){
            Node minp = p.rightNode;
            Node minpp= p;
            //当min的左子节点为null的时候,此时minp是最小的
            while (minp.leftNode !=null){
                minpp = minp;
                minp = minpp.leftNode;
            }
            p.data = minp.data;
            //即把p节点的值替换成其最小右节点值后,把p定义成最小节点,最小节点是最多只存在一个子节点的,
            // 然后可以按代码顺序被后面删除单子节电的代码删除
            p = minp;
            pp = minpp;
        }
        //p是要删除的节点,
        //child主要是来缓存数据,例如p节点没有子节点,则child为null,如有左子节点,child=左子节点之类
        Node child ;
        if (p.leftNode !=null) child=p.leftNode;
        else if (p.rightNode != null) child = p.rightNode;
        else child=null;

        //真正的要删除节点操作
        //如果pp父节点是空,则代表p是根节点,根节点被删除后为null,对应上面的child=null
        //如果p在pp的左节点或右节点,此时pp只有一个子节点,不管是左还是右,用child替换p即可
        if (pp ==null) node33 = child;
        else if(pp.rightNode == p) pp.rightNode=child;
        else  pp.leftNode=child;
                }
}

class Node{
    public int data;
    public Node leftNode;
    public Node rightNode;

    public Node(int data, Node leftNode, Node rightNode) {
        this.data = data;
        this.leftNode = leftNode;
        this.rightNode = rightNode;
    }

    public int getData() {
        return data;
    }

    public void setData(int data) {
        this.data = data;
    }

    public Node getLeftNode() {
        return leftNode;
    }

    public void setLeftNode(Node leftNode) {
        this.leftNode = leftNode;
    }

    public Node getRightNode() {
        return rightNode;
    }

    public void setRightNode(Node rightNode) {
        this.rightNode = rightNode;
    }
}

实际上还有一种更简单的方法:只是将要删除的节点标记为“删除”,但是不进行真正意义上的删除操作。这样原本要删除的节点还存储在内存中,但是删除操作就变的简单。

二叉查找树-快速查找最大节点和最小节点

先找到根节点,因为二叉查找树左边是比根节点小的左子树,根节点左边是比根节点大的右子树,所以可以在查找最大节点时,判定根节点有没有右子树,没有的话根节点就是最大的,然后遍历右子树,找到最后一个节点的右节点为null的节点。找最小节点也一样。

public class tree {
    static Node node19 = new Node(19,null,null);
    static Node node27 = new Node(27,null,null);
    static Node node55 = new Node(55,null,null);
    static Node node15 = new Node(15,null,null);
    static Node node17 = new Node(17,null,null);
    static Node node25 = new Node(25,node19,node27);
    static Node node51 = new Node(51,null,node55);
    static Node node66 = new Node(66,null,null);
    static Node node13 = new Node(13,null,node15);
    static Node node18 = new Node(18,node17,node25);
    static Node node34 = new Node(34,null,null);
    static Node node58 = new Node(58,node51,node66);
    static Node node16 = new Node(16,node13,node18);
    static Node node50 = new Node(50,node34,node58);
    static Node node33 = new Node(33,node16, node50);
    public static void main(String[] args) {
        Node max = findMax();
        System.out.println(max.getData());
        Node min = findMin();
        System.out.println(min.getData());
    }
    public static Node findMax(){
        Node p = node33;
        while (p !=null){
            if (p.rightNode == null){
                return p;
            }else {
                p = p.rightNode;
            }
        }
        return null;
    }
    public static Node findMin(){
        Node p = node33;
        while (p != null){
            if (p.leftNode == null){
                return p;
            }else {
                p = p.leftNode;
            }
        }
        return null;
    }
}

class Node{
    public int data;
    public Node leftNode;
    public Node rightNode;

    public Node(int data, Node leftNode, Node rightNode) {
        this.data = data;
        this.leftNode = leftNode;
        this.rightNode = rightNode;
    }

    public int getData() {
        return data;
    }

    public void setData(int data) {
        this.data = data;
    }

    public Node getLeftNode() {
        return leftNode;
    }

    public void setLeftNode(Node leftNode) {
        this.leftNode = leftNode;
    }

    public Node getRightNode() {
        return rightNode;
    }

    public void setRightNode(Node rightNode) {
        this.rightNode = rightNode;
    }
}

支持重复数据的二叉查找树

在实际中,二叉查找树存储的是一个包含很多自动的对象,利用对象的某个字段作为key值构建二叉查找树,剩余其他字段称为卫星字段。
那么二叉查找树如何存储两个key相同的对象呢,有两种方法:
1、通过链表或者数组等容易扩容的结构,把相同key值的数据存储在同一个节点。
2、每个节点存储一个数据,但是在插入数据的时候,如果插入的数据与一个节点的数据相同,那就把这个数据插入到这个节点的右子树,把这个数据当作大于这个节点数据来处理。
在这里插入图片描述

但是这样处理后,遇到查找某个数据操作时,当查找到与值相同的节点时,此时并不能停止查询,仍需要继续查找右子树,直至遇到叶子节点为止。这样才能把所有相同值的所有结点查找出来。
在这里插入图片描述

对于删除操作,按正常操作删除即可,依次删除。
在这里插入图片描述

二叉查找树的时间复杂度分析

在这里插入图片描述

在极端条件下,例如上面的第一种情况,二叉树已经退化成链表结构,这是最糟糕的情况,此时时间复杂度是O(n)。
从图中来看,操作不管是查询还是插入、删除,时间复杂度都跟树的高度有关,成正比关系,即O(height)。
那么完全二叉树的高度怎么求?
假设完全二叉树的结点总数为n,最大层数是L。
L=1 的时候共有节点2 1 - 1 个, L =2的时候共有节点 22 -1 个,L=3的时候共有节点23 -1 个,所以是满二叉树的时候L层满二叉树共有2L-1 个节点,前面L-1层共有2L-1 -1个 ,而L层的完全二叉树最后一层子节点是不满的,最少是1个,最多是2 L-1个,所以L层的完全二叉树共有节点(2L-1 -1) +1 <= n <= (2L -1),得出L的范围是[log2(n+1),log2n +1],所以L层数小于等于log2n +1,因为高度H=L-1,所以高度H小于等于log2n 。

散列表和二叉查询树

散列表的删除、查询、插入时间复杂度都可以做到O(1),而二叉查找树只能做到O(logN),两者对比散列表更有优势,但是为什么在别的场景还用二叉查找树呢
1、散列表的数据是无需的,在需要输出有序数据的时候,需要重新排序,而二叉查找树只需中序遍历即可在O(n)的时间复杂度内输出有序数据。
2、散列表扩容耗时多,遇到散列冲突时,性能不稳定。二叉查找树性能也不稳定,但是常用的平衡二叉查找树的性能是非常稳定的,时间复杂度是O(logN)
3、散列表正常情况查找的时间复杂度是常量级别O(1)的,但是因为散列冲突,导致这个常量不一定比logN小。所以查找速度不一定比O(logN)快。同时哈希函数的耗时,也不一定比平衡二叉查找树效率高。
4、散列表的构造比二叉查找树复杂,需要考虑很多,例如散列函数设计、散列冲突 的解决,扩容、缩容等。而平衡二叉树只考虑平衡性一个问题即可。
5、因为散列表的装载因子过大时,散列冲突发生的概率越高。所以希望装载因子平衡在一个相对比较小的状况,要维持这种情况,占用的内存就会很高,这部分内存是被浪费掉的。
二者对比有好有坏,需要在不同场景下根据不同需求使用。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值