首页 > 编程语言 >二叉搜索树(Java语言实现)

二叉搜索树(Java语言实现)

时间:2023-06-11 17:32:46浏览次数:32  
标签:right Java cur 二叉 搜索 else 节点 left

前言

每个人都有自己的追求和梦想,想要成为更好的自己。而在软件开发领域,掌握数据结构是成为优秀程序员的必备技能之一。二叉搜索树是其中重要的一种,它的优缺点和实现原理不仅是程序员必须了解的内容,同时也是程序员个人成长的一部分。在Java语言中实现一个二叉搜索树需要的不仅仅是技术上的熟练掌握,更需要的是内心的坚定和自我超越的精神。掌握数据结构,科学编程,这是我们一路走来的坚定信念,也是我们通向成功的必经之路。

二叉搜索树(Java语言实现)_二叉搜索树


正文

一、概念

二叉搜索树(Binary Search Tree,BST)是一种基于二叉树而产生的一种数据结构。在二叉搜索树中,每个节点的值只能比它的左子节点大或者比它的右子节点小,这一特性使得数据的查找、插入和删除等操作变得高效而容易。今天我们将学习如何使用Java语言实现二叉搜索树,以及该数据结构的一些重要概念和操作。

二、相关操作的代码实现

二叉搜索树主要要实现的代码就是构建、查找、删除,当然,构建是第一位,下面将介绍二叉搜索树是如何构建出来的。

2.1、构建

构建二叉搜索树首先要搞清楚他有几个属性。首先,左右孩子节点肯定要有吧?然后就是要有个数字的属性吧?还要有个头,也就是根节点,有了这三个属性,再写个构造方法,我们就可以先写一部分了,代码实现如下:

public class BinarySearchTree {
    static class TreeNode {
        private int val;
        private TreeNode left;
        private TreeNode right;
        public TreeNode( int val) {
            this.val = val;
        }
    }
}

然后就是构造二叉搜索树的正式代码了,在写这个之前,我们要搞清楚二叉搜索树到底是怎么构建的。首先来看一个构建好的二叉搜索树,如下图就是一课标准的二叉搜索树:

二叉搜索树(Java语言实现)_二叉搜索树_02

如上图,大家先找找这颗树的特点,和普通的二叉树有什么不同呢?是不是这棵树所有的左右子树都满足左边子树的数据全部比根节点小,右边的子树所有的数据全都比根节点大呢?是这样的没错,所以我们思路可以这样:

首先,如果root == null,那么一定是第一次插入,我们直接让头等于这个值就行了。如果是后面的插入,我们定义需要插入的数据为val,然后我们要在这棵树里面找到val合适的位置,此时我们可以定义一个代走的指针为cur,如下图:

二叉搜索树(Java语言实现)_数据_03

就比如我要插入的数据是 4 ,那cur该怎么走呢?首先是不是得和根节点 8 进行比较呢?比较的比 8 小,就走左边,再跟 5 比较,还是比 5 小,继续走左边,再跟 3 比较,这回比较大了,就走右边,但是一走右边就为null了呀,那还怎么插?所以,光有一个变量cur是肯定不够的,还要定义一个在cur之上的,也就是cur的父亲,当cur == null了,直接跳出去,跳出去之后,直接和这个cur的父亲节点比较,如果大就放右边,如果小就左边,插入成功返回true,否则返回false,思路理顺,代码实现如下:

public boolean insert(int key) {
    TreeNode node = new TreeNode(key);
    if (root == null) {
        root = node;
        return true;
    }
    TreeNode cur = root;
    TreeNode curParent = null;
    while (cur != null) {
        // 如果比它小就往左边走
        if (key < cur.val) {
            curParent = cur;
            cur = cur.left;
        }
        // 如果比它大就往右边走
        else if (key > cur.val) {
            curParent = cur;
            cur = cur.right;
        }
        // 如果相等就提醒用户已存在,然后返回 false
        else {
            System.out.println("该值得节点已存在!!!");
            return false;
        }
    }
    // 出来判断到底是大还是小
    if (key < curParent.val) {
        curParent.left = node;
    }
    else {
        curParent.right = node;
    }
    return true;
}

如上,构造二叉搜索树的代码就实现了,可以先试着插入几个数据试试,运行结果如下:

二叉搜索树(Java语言实现)_数据_04

这是我随便写的数据,按照这个数据,画出的图如下:

二叉搜索树(Java语言实现)_数据结构_05

2.2、查找

构造完成之后,我们还要学习关于二叉搜索树的查找方法,只要建好了一颗二叉搜索树,查找起来那速度真的是没得说,一颗普通的二叉树查找效率的时间复杂度是 O(N*long2n) ,搜索二叉树查找起来的效率那就直接减少了N倍,时间复杂度直接变成了 O(long2n) 。

那这个查找方法到底该怎么写呢?首先我们可以定义一个代走的指针->Phead, 让这个指针走,小走左,大往右,找到返回节点,找不到返回空,思路理顺,代码实现如下:

public TreeNode search(int val) {
    TreeNode phead = root;
    while (phead != null) {
        if (val < phead.val) {
            phead = phead.left;
        }
        else if (val > phead.val) {
            phead = phead.right;
        }
        else {
            break;
        }
    }
    return phead;
}

这样子二叉搜索树查找的代码就写好了。因为如果打印返回值的话,打印的是一个地址,所以大家可以重写toString方法或者在break前面也写个打印,主要是打印找到该节点的地址,然后两个节点的地址相比较,如下:

二叉搜索树(Java语言实现)_二叉搜索树_06

2.3、删除

然后就是二叉搜索树比较绕的删除部分了,首先看图:

二叉搜索树(Java语言实现)_二叉搜索树_07

删除一般是分三种情况,一种是删除的该节点左边是空,一种是删除的该节点右边是空,一种是两边都不是空,每种又分三种情况,所以说比较绕,但是你先别急,我带你慢慢来,先来看删除节点左边是空的情况:

先假设,假设我要删除的节点左边是空,然后分三种:

  • 如果要删除的节点是头。
  • 如果不是头,但是是在父亲节点的左边。
  • 如果不是头,但是是在父亲节点的右边。

主要就是这些情况,我们先写找节点的代码,如下:

public void removeNode(int key) {
    // 先找到节点
    TreeNode cur = root;
    TreeNode parent = null;
    while (cur != null) {
        if (key < cur.val) {
            parent = cur;
            cur = cur.left;
        }
        else if (key > cur.val) {
            parent = cur;
            cur = cur.right;
        }
        else {
            remove(cur, parent);
            break;
        }
    }
    System.out.println("未找到该节点!!!");
}

上面三种情况,我们一个一个慢慢分析,先假设是头,那我们直接让头等于头的右边不就行了吗?那如果不是头呢?比如这样的:

二叉搜索树(Java语言实现)_数据_08

假如我要删除的节点是 3 ,那该怎么办?哎,我们不是都找到了cur的父亲节点吗?直接让父亲节点指向cur.right不就好了?这个也分析完毕,我们再来看第三种情况:如果不是头,但是在父亲节点的右边,比如我要删除的是 6 ,如下:

二叉搜索树(Java语言实现)_数据结构_09

还是老样子,看图说话,我们直接让parent.right = cur.right不就好了?所以第一种大情况的代码如下:

private void remove(TreeNode cur, TreeNode parent) {
    // 左边是空
    if (cur.left == null) {
        // 判断是不是头
        if (cur == root) {
            root = root.right;
        }
        // 如果不是头
        else {
            // 判断是左还是右
            if (cur == parent.left) {
                parent.left = cur.right;
            }
            else {
                parent.right = cur.right;
            }
        }
    }
    // 右边是空
    else if (cur.right == null) {

    }
    // 左右都不是空
    else {

    }
}

紧接着我们来分析待删除的节点如果右边是空的情况,老样子,又分为三种情况:

  • 如果要删除的节点是头。
  • 如果不是头,但是是在父亲节点的左边。
  • 如果不是头,但是是在父亲节点的右边。

这三种和上面大差不差,先来看第一种:待删除的节点是头节点,如下:

二叉搜索树(Java语言实现)_二叉搜索树_10

就比如说我要删除 8 ,那我们直接让root = root.left不就好了?

再来分析第二种情况:不是头,但是待删除节点在父亲节点的左边,比如像删除 3 这种情况,我们直接让 5 这个节点指向 3 的左就行了,然后分析第三种情况,要一个新的图,如下:

二叉搜索树(Java语言实现)_数据_11

第三种情况的条件是:待删除的节点右边是空,但是这个节点在父亲节点的右边,比如说 20 这个节点就很是满足这种情况,还是看图说话,我们直接让 8 这个父亲节点跳过 20 ,指向 15 就行了,所以当待删除节点右边为空的情况代码实现如下:

else if (cur.right == null) {
    // 判断是不是头
    if (cur == root) {
        root = root.left;
    }
    // 如果不是头
    else {
        // 判断是左还是右
        if (cur == parent.left) {
            parent.left = cur.left;
        }
        else {
            parent.right = cur.left;
        }
    }
}

最后接着看我们最麻烦的一个:待删除节点左右都不是空的情况:

二叉搜索树(Java语言实现)_二叉搜索树_12

比如说我要删除 5 这个节点,我们用的是替罪羊的方法来实现这个要求的,可以找待删除节点左树的最大值,也可以找待删除节点右树的最小值,我们就拿找右树最小值举例,是不是可以定义一个代cur走的节点pcur呀,然后我们让pcur走,一直走到pcur.left是空,然后这个节点和cur交换,完成之后删掉pcur这个节点,但是因为pcur的左边是空的,不就又变成我们第一种大情况(当待删除节点左树为空时)的情况了吗?这个思想使用的是数学里面的归元的思想。思路理顺之后,代码实现如下:

// 左右树都不是空
else {
    // 先找到右边的最小值
    TreeNode pcur = cur.right;
    TreeNode pparent = cur;
    while (pcur.left != null) {
        pparent = pcur;
        pcur = pcur.left;
    }
    cur.val = pcur.val;
    // 然后删除这个替罪的节点
    // 又到了待删除的节点左边是空的情况了
    if (pcur == pparent.left) {
        pparent.left = pcur.right;
    }
    else {
        pparent.right = pcur.right;
    }
}

然后所有关于删除的代码整合一下,就是这样的:

public void removeNode(int key) {
    // 先找到节点
    TreeNode cur = root;
    TreeNode parent = null;
    while (cur != null) {
        if (key < cur.val) {
            parent = cur;
            cur = cur.left;
        }
        else if (key > cur.val) {
            parent = cur;
            cur = cur.right;
        }
        else {
            remove(cur, parent);
            break;
        }
    }
    System.out.println("未找到该节点!!!");
}

private void remove(TreeNode cur, TreeNode parent) {
    // 左边是空
    if (cur.left == null) {
        // 判断是不是头
        if (cur == root) {
            root = root.right;
        }
        // 如果不是头
        else {
            // 判断是左还是右
            if (cur == parent.left) {
                parent.left = cur.right;
            }
            else {
                parent.right = cur.right;
            }
        }
    }
    // 右边是空
    else if (cur.right == null) {
        // 判断是不是头
        if (cur == root) {
            root = root.left;
        }
        // 如果不是头
        else {
            // 判断是左还是右
            if (cur == parent.left) {
                parent.left = cur.left;
            }
            else {
                parent.right = cur.left;
            }
        }
    }
    // 左右都不是空
    else {
        // 先找到右边的最小值
        TreeNode pcur = cur.right;
        TreeNode pparent = cur;
        while (pcur.left != null) {
            pparent = pcur;
            pcur = pcur.left;
        }
        cur.val = pcur.val;
        // 然后删除这个替罪的节点
        // 又到了待删除的节点左边是空的情况了
        if (pcur == pparent.left) {
            pparent.left = pcur.right;
        }
        else {
            pparent.right = pcur.right;
        }
    }
}

这样一个普通二叉搜索树就建成了,大家可以试着插入、删除、查找数据了。


总结

在本文中,我们学习了二叉搜索树的基本概念、特征和操作,以及如何在Java语言中实现它。通过学习二叉搜索树,我们可以更加深刻地理解数据结构和算法,并且了解如何使用二叉搜索树处理各种问题。

与其他数据结构相比,二叉搜索树具有许多优点,例如进行插入、查找、删除等基本操作时速度非常快,而且它的内存使用效率也相对较高。因此,理解和掌握二叉搜索树是非常有必要的。

当然,二叉搜索树也存在缺陷,例如不同的插入和删除顺序会影响树的平衡,使得二叉搜索树的性能降低等。为了解决这些问题,我们需要使用更高级的数据结构,例如红黑树和AVL树等。

在学习过程中,我们需要时刻注意代码实现的复杂度和鲁棒性,尤其是处理边缘情况的能力。当然,能够熟练掌握二叉搜索树的基本操作远远不够,在实际应用中,我们还需要结合具体问题,在实际情况中加以运用。

希望本文能够对读者了解和掌握二叉搜索树提供一点参考和帮助,欢迎读者在实践中不断探索和发现!


结语

作为程序员,学习数据结构和算法是必不可少的。数据结构不仅在理解算法中扮演着重要的角色,而且在程序设计和软件开发中也扮演着至关重要的角色。因此,了解和掌握二叉搜索树对于我们的成长和进步是非常重要的。

学习编程的过程充满了挫折与艰辛,甚至可能会让我们失去信心和方向。但是,只要怀有激情和毅力,有着不断探索和学习的心态,一切困难都将迎刃而解。在二叉搜索树的学习中,我们不仅掌握了基础知识和技能,更重要的是在不断学习和探索中,我们转变了解决问题的思维方式,更深刻地认识了自己的不足,也拥有了更广阔的视野和更多的机遇。

因此,让我们一起坚持不懈地学习、实践和探索,在编程的旅途中越走越远,成为真正的技术巨匠!

二叉搜索树(Java语言实现)_数据结构_13

标签:right,Java,cur,二叉,搜索,else,节点,left
From: https://blog.51cto.com/bitzmbdp/6458374

相关文章

  • Java 网络编程 —— 基于 UDP 的数据报和套接字
    UDP简介UDP(UserDatagramProtocol,用户数据报协议)是传输层的另一种协议,比TCP具有更快的传输速度,但是不可靠。UDP发送的数据单元被称为UDP数据报,当网络传输UDP数据报时,无法保证数据报一定到达目的地,也无法保证各个数据报按发送的顺序到达目的地,例如:当发送方先发送包含字符......
  • java多线程基础的学习
    java多线程学习(主要围绕着线程的实现、状态、同步、通信以及高级主题如线程池)1.线程、进程、多线程进程:正在进行中的程序,一个程序的执行过程,需要资源:内存、cpu。线程:属于进程,指的是一个可以独立运行的代码片段(执行单元、执行路径)。一个进程中有多个可以独立运行的执行单元,这......
  • Java 基础面试笔记(二)
    1.arraylist和linkedlist区别:概念上:ArrayList是实现了基于动态数组的数据结构,LinkedList是基于链表结构。性能上: ArrayList 的查询效率比较高,增删动作的效率比较差,适用于查询比较频繁,增删动作较少的元素管理的集合。LinkedList 的查询效率低,但是增删效率很高。适用于增删动作的......
  • JAVA Set 交集,差集,并集
    /***Createdbyyuhuion2017/7/110011.*/importjava.util.HashSet;importjava.util.Set;publicclassTestSet{publicstaticvoidmain(String[]args){Set<String>result=newHashSet<String>();Set<String>......
  • Java8新特性Stream之list转map及问题解决
    List集合转Map,用到的是Stream中Collectors的toMap方法:Collectors.toMap具体用法实例如下://声明一个List集合Listlist=newArrayList();list.add(newPerson("1001","小A"));list.add(newPerson("1002","小B"));list.add(......
  • Java中匿名内部类
      packagecom.itheima.d8_innerclass_anonymous;/***目标:学习匿名内部类的形式和特点*/publicclassTest{publicstaticvoidmain(String[]args){Animala=newAnimal(){@Overridepublicvoidrun(){......
  • 一份55页Java性能调优PPT分享
    提起“肖桦”这个人,相信很多小伙伴对他比较陌生。除去现任唯品会资深技术专家头衔外,他更为技术圈所熟知的是他的著名开源项目:SpringSide。SpringSide是以springFramework为核心的,Pragmatic风格的JavaEE应用参考示例,是JavaEE世界中主流技术选型,最佳实践的总结与演示。到目前为......
  • 对数据进行模糊匹配搜索(动态规划、最长公共子串、最长公共子序列)
    在搜索时常常在输入一半或者输入错误时,搜索引擎就给出智能提示。已知的搜索推荐主要包括以下几个方面:包含:“清华”和“清华大学”相似:“聊天软件”和“通讯软件”相关:“明星”和“刘亦菲”纠错:“好奇害死毛”和“好奇害死猫”其中包含模糊匹配可以使用动态规划算......
  • java——微服务——spring cloud——Nacos——Nacos与Eureka区别
        ......
  • 【Java技术专题】「Guava开发指南」手把手教你如何进行使用Guava工具箱进行开发系统实
    异常传播有时候,您可能需要重新抛出捕获到的异常。这种情况通常发生在捕获到Error或RuntimeException时,因为您可能没有预料到这些异常,但在声明捕获Throwable和Exception时,它们也被包含在内了。为了解决这个问题,Guava提供了多种方法来判断异常类型并重新抛出异常。例如:try{......