集合系列(二十二) -一文到你搞懂二叉树实现

一、介绍

在前面的文章中,我们对树这种数据结构做了一些基本介绍,今天我们继续来聊聊一种非常常用的动态查找树: 二叉查找树

二叉查找树,英文全称:Binary Search Tree,简称:BST,它是计算机科学中最早投入实际使用的一种树形结构,特性如下:

  • 若左子树不为空,则左子树上所有结点的值均小于它的根结点的值;
  • 若右子树不为空,则右子树上所有结点的值均大于或等于它的根结点的值;
  • 它的左、右子树也分别为二叉查找树;

特性定义比较粗放,所以在树形形态结构上,有着多样,例如下图:

上图 a、b、c 三个图,都满足以上特性,也被称为二叉查找树,虽然通过中序遍历可以得到一个有效的数组:[1、2、3、4、5、6、7、8],但是就查找效率来说,有着一定的差别,例如查询目标为8的内容,从根目录开始查询,结构如下:

  • a图,需要5次;
  • b图,需要3次;
  • c图,需要8次;

由此可见,不同的形状,所需查找的次数是不一样的,关于这一点,后面我们在介绍平衡二叉查找树、红黑树这种数据结构的时候,会进行详细介绍。

虽然二叉查找树,在不同的形状下,查找效率不一样,但是它是学习其他树形结构的基础,了解了二叉查找树的算法,相信再了解其他二叉树结构会轻松很多。

二、算法思路

2.1、 新增

新增元素表示向二叉树中添加元素,比较简单。如果二叉树为空,默认第一个元素就是根节点,如果二叉树不为空,就以上面提到的特点为判断条件,进行左、右节点的添加。

2.3、 查找

查找元素表示从根节点开始查找元素,如果根节点为空,就直接返回空值,如果不为空,通过以左子树小于父节点,右子树大于父节点的特性为依据进行判断,然后以递归方式进行查找元素,直到找到目标的元素为止。

2.2、 删除

删除元素表示从二叉树中移除要删除的元素,逻辑稍微复杂一些。同样,先要判断根节点是否为空,如果为空,直接返回,如果不为空,分情况考虑。

  • 被删除的节点,右子树为空

这种场景,只需要将被删除元素的左子树的父节点移动到被删除元素的父节点,然后将被删除元素移除即可。

  • 被删除的节点,左子树为空

这种场景,与上面类似,只需要将被删除元素的右子树的父节点移动到被删除元素的父节点,然后将被删除元素移除即可。

  • 被删除的节点,左、右子树不为空

这种场景,稍微复杂一点,先定位到要删除的目标元素,根据左子节点内容一定小于当前节点内容特点,找到目标元素的左子树,通过递归遍历找到目标元素的左子树的右子树,找到最末端的元素之后,进行与目标元素进行替换,最后移除最末端元素。

2.4、 遍历

二叉树的遍历方式,分两类:

  • 层次遍历,从根节点开始;
  • 深度遍历,又分为前序、中序、后序遍历三种方式;
2.4.1、层次遍历

层次遍历,算法思路比较简单,从根节点开始,分层从左到右进行遍历元素。

2.4.2、深度遍历

深度遍历,在遍历起始位置上又分三种,分别是前序遍历、中序遍历、后序遍历,每种遍历方式输出的结果不一样。

  • 前序遍历:从树根开始 -> 左子树 -> 右子树

  • 中序遍历:从最末端左子树开始 -> 树根 -> 右子树

  • 后序遍历:从最末端左子树 -> 右子树 -> 最后到树根

尽管二叉树在遍历方式上有多种,但是只要我们掌握了其中的思路原理,再去实现起来,就会轻松很多。

三、代码实践

首先创建一个实体数据结构BSTNode,内容如下:

/**
 * BST全称:Binary Search Tree
 * 二叉查找树,节点数据结构
 */
public class BSTNode<E extends Comparable<E>> {

    /**节点数据*/
    E key=null;
    /**直接父节点*/
    BSTNode<E> parent = null;
    /**当前节点的左子节点*/
    BSTNode<E> lChild = null;
    /**当前节点的右子节点*/
    BSTNode<E> rChild = null;

    /**构造方法*/
    BSTNode(E key){
        this.key = key;
    }
}

然后,创建一个二叉查找树操作类BinarySearchTree,内容如下:

/**
 * 二叉查找树 Binary Search Tree(BST)
 * 算法实现
 */
public class BinarySearchTree<E extends Comparable<E>> {

    /**定义根节点*/
    private BSTNode<E> root = null;

    /**
     * 获取二叉树根节点
     * @return
     */
    public BSTNode<E> getBoot(){
        return this.root;
    }

    /**
     * BST 查询关键字
     * 快速搜索
     * @param key
     * @return
     */
    public boolean search(E key){
        System.out.println("搜索关键字key=" + key + " ");
        if(key == null || root == null){
            return false;
        }else{
            System.out.print("搜索路径[");
            //从根节点开始查询
            if(searchBST(root,key) != null){
                //搜索到目标元素
                return true;
            }
            System.out.print("\n");
            return false;
        }

    }

    private BSTNode<E> searchBST(BSTNode<E> node, E key){
        if(node == null){
            System.out.print("],搜索失败");
            return null;
        }
        //当前搜索节点
        System.out.print(node.key + " ->");
        //判断当前节点是否为需要找到元素,通过compareTo方法进行比较,如果相等,说明已经找到
        if(node.key.compareTo(key) == 0){
            System.out.print("],搜索成功\n");
            return node;
        }else if(node.key.compareTo(key) > 0){
            //左子树查询
            //当前节点比需要查询的节点大,那么向树的左边查询
            //递归查询
            return searchBST(node.lChild,key);
        }else if(node.key.compareTo(key) < 0){
            //右子树查询
            //需要查询的节点大于当前节点大,那么向树的右边查询
            //递归查询
            return searchBST(node.rChild,key);
        }
        System.out.print("],未找到对于的关键字,搜索失败");
        return null;
    }

    public boolean insert(E key){
        System.out.println("插入关键字key=" + key + " ");
        if(key == null){
            return false;
        }
        if(root == null){
            //插入根节点
            System.out.print("插入到树根节\n");
            root = new BSTNode<E>(key);
            return true;
        }else{
            //插入的时候,需要先查找需要插入的节点,然后在其节点插入
            //从根路径向下递归遍历比较元素内容,直到找到元素插入内容位置
            return insertBST(root,key);
        }
    }

    private boolean insertBST(BSTNode<E> node, E key){
        //在原树中找到相同的内容,无需插入
        if(node.key.compareTo(key)==0){
            return false;
        }else if(node.key.compareTo(key) > 0){
            //如果要插入的节点内容小于当前节点,那么搜索当前节点左子树
            //判断左子树,是否为空,如果为空,直接插入
            if(node.lChild == null){
                //创建一个新节点,插入节点
                BSTNode<E> newNode = new BSTNode<E>(key);
                node.lChild = newNode;
                newNode.parent = node;
                return true;
            }else{
                //否则,继续向左递归判断
                return insertBST(node.lChild,key);
            }
        }else if(node.key.compareTo(key) < 0){
            //如果要插入的节点内容大于当前节点,那么搜索当前节点右子树
            //判断右子树,是否为空,如果为空,直接插入
            if(node.rChild == null){
                //创建一个新节点,插入节点
                BSTNode<E> newNode = new BSTNode<E>(key);
                node.rChild = newNode;
                newNode.parent = node;
                return true;
            }else{
                //否则,继续向右递归判断
                return insertBST(node.rChild,key);
            }
        }
        return false;
    }

    public boolean delete(E key){
        System.out.println("删除关键字key=" + key + " ");
        if(key == null || root == null){
            System.out.print("删除失败");
            return false;
        }else{

            return deleteBST(root,key);
        }
    }

    private boolean deleteBST(BSTNode<E> node,E key){
        //定位到树中需要删除的节点
        System.out.print("开始搜索目标元素[");
        BSTNode<E> nodeDel = searchBST(node,key);
        if(nodeDel == null){
            return false;
        }else{
            //树的右子节点为空,只需要重新连接它的左树
            if(nodeDel.rChild == null){
                BSTNode<E> parent = nodeDel.parent;
                //判断当前需要删除的元素是父节点左子节点还是右子节点
                if(parent.lChild != null && parent.lChild.key.compareTo(nodeDel.key) == 0){
                    //如果为左子节点,那么当前节点的左子节点挂到父亲的左子节点上
                    parent.lChild = nodeDel.lChild;
                }else{
                    //如果为右子节点,那么当前节点的左子节点挂到父亲的右子节点上
                    parent.rChild = nodeDel.lChild;
                }
                if(nodeDel.lChild != null){
                    //同时将当前元素的左子节点的父亲,设置为当前节点的父亲
                    nodeDel.lChild.parent = parent;
                }
            }else if(nodeDel.lChild == null){
                //树的左子节点为空,只需要重新连接它的右树
                BSTNode<E> parent = nodeDel.parent;
                if(parent.lChild != null && parent.lChild.key.compareTo(nodeDel.key) == 0){
                    //如果为左子节点,那么当前节点的左子节点挂到父亲的左子节点上
                    parent.lChild = nodeDel.rChild;
                }else{
                    //如果为右子节点,那么当前节点的左子节点挂到父亲的右子节点上
                    parent.rChild = nodeDel.rChild;
                }
                if(nodeDel.rChild != null){
                    //同时将当前元素的右子节点的父亲,设置为当前节点的父亲
                    nodeDel.rChild.parent = parent;
                }
            }else{
                //当前节点的左、右树都不为空
                //根据二叉树特性,左子节点内容一定小于当前节点内容,右子节点一定大于当前节点内容
                //根据这个特性,对当前节点的左子节点的右子节点进行递归遍历,找到最末端的节点的内容,用来替换即将要删除的节点
                BSTNode<E> q = nodeDel;//临时变量值
                BSTNode<E> s = nodeDel.lChild;//先找到要删除节点的左子节点

                //循环遍历左子节点下右子节点元素,直到找到最末端
                while(s != null && s.rChild != null){
                    q = s;
                    s = s.rChild;
                }
                s = (s != null) ? s:q;
                //采用内容替换和位置调整法
                //将当前要删除节点左树最大的内容,替换掉要删除节点的内容
                nodeDel.key = s.key;
                //节点调整
                if(q != nodeDel){
                    //如果当前节点的左节点,有右节点那么进行位置调整
                    //将s的左节点加入到q的右节点上
                    q.rChild = s.lChild;
                }else{
                    //要删除的节点的左子节点,没有右子节点,因为内容已经替换,直接将当前元素的左子节点的左节点移动到当前节点上
                    q.lChild = s.lChild;
                }
                if(s.lChild != null){
                    //如果s的左节点不为空,将其父亲挂在q上
                    s.lChild.parent = q;
                }
            }
            return true;
        }
    }

    /**
     * 前序遍历
     * @param node
     */
    public void frontTreeIterator(BSTNode<E> node){
        if(node != null){
            System.out.println("key:" + node.key);
            frontTreeIterator(node.lChild);//遍历当前节点左子树
            frontTreeIterator(node.rChild);//遍历当前节点右子树
        }
    }

    /**
     * 中序遍历
     * @param node
     */
    public void middleTreeIterator(BSTNode<E> node){
        if(node != null){
            middleTreeIterator(node.lChild);//遍历当前节点左子树
            System.out.println("key:" + node.key);
            middleTreeIterator(node.rChild);//遍历当前节点右子树
        }
    }


    /**
     * 后序遍历
     * @param node
     */
    public void backTreeIterator(BSTNode<E> node){
        if(node != null){
            backTreeIterator(node.lChild);//遍历当前节点左子树
            backTreeIterator(node.rChild);//遍历当前节点右子树
            System.out.println("key:" + node.key);
        }
    }
}

最后,我们来测试一下,代码内容如下:

public class BSTClient {

    /**
     * 测试
     * @param args
     */
    public static void main(String[] args) {
        //创建一个Integer型的数据结构
        BinarySearchTree<Integer> tree = new BinarySearchTree<Integer>();

        //插入节点
        System.out.println("========插入元素========");
        tree.insert(new Integer(5));
        tree.insert(new Integer(2));
        tree.insert(new Integer(7));
        tree.insert(new Integer(1));
        tree.insert(new Integer(6));
        tree.insert(new Integer(4));
        tree.insert(new Integer(8));
        tree.insert(new Integer(3));
        tree.insert(new Integer(9));
        tree.insert(new Integer(10));
        System.out.println("========中序遍历元素========");

        //中序遍历
        tree.middleTreeIterator(tree.getBoot());
        System.out.println("========查找key为9的元素========");

        //查询节点
        boolean searchResult = tree.search(9);
        System.out.println("查找结果:" + searchResult);
        System.out.println("========删除key为10的元素========");

        //删除节点
        boolean deleteResult = tree.delete(10);
        System.out.println("删除结果:" + deleteResult);
        System.out.println("========再次中序遍历元素========");

        //中序遍历
        tree.middleTreeIterator(tree.getBoot());
    }
}

输出结果:

========插入元素========
插入关键字key=5 
插入到树根节
插入关键字key=2 
插入关键字key=7 
插入关键字key=1 
插入关键字key=6 
插入关键字key=4 
插入关键字key=8 
插入关键字key=3 
插入关键字key=9 
插入关键字key=10 
========中序遍历元素========
key:1
key:2
key:3
key:4
key:5
key:6
key:7
key:8
key:9
key:10
========查找key为9的元素========
搜索关键字key=9 
搜索路径[5 ->7 ->8 ->9 ->],搜索成功
查找结果:true
========删除key为10的元素========
删除关键字key=10 
开始搜索目标元素[5 ->7 ->8 ->9 ->10 ->],搜索成功
删除结果:true
========再次中序遍历元素========
key:1
key:2
key:3
key:4
key:5
key:6
key:7
key:8
key:9

四、总结

二叉查找树,作为树类型中一种非常重要的数据结构,有着非常广泛的应用,但是二叉查找树具有很高的灵活性,不同的插入顺序,可能造成树的形态差异比较大,如开文介绍的图c,在某些情况下会变成一个长链表,此时的查询效率会大大降低,如何解决这个问题呢,平衡二叉树就要派上用场了,会在后面的文章进行介绍!

五、参考

iteye - Heart.X.Raid - 二叉查找树

六、写到最后

最近无意间获得一份阿里大佬写的技术笔记,内容涵盖 Spring、Spring Boot/Cloud、Dubbo、JVM、集合、多线程、JPA、MyBatis、MySQL 等技术知识。需要的小伙伴可以点击如下链接获取,资源地址:技术资料笔记。

不会有人刷到这里还想白嫖吧?点赞对我真的非常重要!在线求赞。加个关注我会非常感激!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/583113.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

js cookie和它的写入,读取,删除

什么是cookie Cookie 是直接存储在浏览器中的一小串数据&#xff0c;它们是 HTTP 协议的一部分 Cookie 通常是由 Web 服务器使用响应 Set-Cookie HTTP-header 设置的。然后浏览器使用 Cookie HTTP-header 将它们自动添加到&#xff08;几乎&#xff09;每个对相同域的请求中。…

升级价值主张 用友帮企业找到乘风破浪的“密码”

近期&#xff0c;用友发布了其战略级产品用友BIP的全新价值主张&#xff0c;将其从原来的“企业数智化 用友BIP”升级为“用友BIP 成就数智企业”。用友这次价值主张升级看似变动不大&#xff0c;实则大有深意。 顺势而为的主动升级 从当前数智化发展的形势来看&#xff0c;各…

牛客NC320 装箱问题【中等 动态规划,背包问题 C++/Java/Go/PHP】

题目 题目链接&#xff1a; https://www.nowcoder.com/practice/d195a735f05b46cf8f210c4ad250681c 几乎完全相同的题目&#xff1a; https://www.lintcode.com/problem/92/description 思路 动态规划都是递归递推而来。php答案是动态规划版本&#xff0c;递归版本有 测试用…

ios CI/CD 持续集成 组件化专题五-(自动发布私有库-组件化搭建)

一&#xff1a;手动发布私有库总结 手动发布pod私有库&#xff0c;需要进行如下几步操作&#xff1a; 1、修改完代码之后&#xff0c;需要提交代码push到git仓库。 2、给代码打tag。 3、修改podspec文件的version值&#xff0c;使其和设置的tag一直。 4、命令行执行pod repo…

【蓝桥杯省赛真题41】python搬运物品方案 中小学青少年组蓝桥杯比赛 算法思维python编程省赛真题解析

目录 python搬运物品方案 一、题目要求 1、编程实现 2、输入输出 二、算法分析 三、程序编写 四、程序说明 五、运行结果 六、考点分析 七、 推荐资料 1、蓝桥杯比赛 2、考级资料 3、其它资料 python搬运物品方案 第十三届蓝桥杯青少年组python省赛比赛 一、题目…

【CGALDotNet】二维矢量多边形可视域计算(C#调用CGAL)

参考 CGALDotNet快速开始&#xff1a;https://blog.csdn.net/liqian_ken/article/details/138274933 CGAL二维可视域计算介绍&#xff1a;https://doc.cgal.org/latest/Visibility_2/index.html#visibility_2_introduction CGAL相关接口&#xff1a;https://doc.cgal.org/late…

明日周刊-第8期

现在露营的人越来越多了&#xff0c;都是带着帐篷或者遮阳篷聚在一起喝喝茶聊聊天&#xff0c;这是一种很好的放松方式。最近我养了一只金毛&#xff0c;目前两个月大&#xff0c;非常可爱。 文章目录 一周热点资源分享言论歌曲推荐 一周热点 一、人工智能领域 本周&#xff…

2024.4.29

模板类实现顺序栈 #include <iostream>using namespace std; template <typename T> class Seqlite{T data[30];int len0; public:void head_inst(T date);void head_dele();void show(); }; template <typename T> //头插函数 void S…

如何快速申请SSL证书实现HTTPS访问?

申请SSL证书最简单的方法通常涉及以下几个步骤&#xff0c;尽量简化了操作流程和所需专业知识&#xff1a; 步骤一&#xff1a;选择适合的SSL证书类型 根据您的网站需求&#xff0c;选择最基础的域名验证型&#xff08;DV SSL&#xff09;证书&#xff0c;它通常只需验证域名所…

OpenAI 新推出 AI 问答搜索引擎——SearchGPT 震撼登场

您的浏览器不支持 video 标签。 OpenAI-SearchGPT 近日&#xff0c;OpenAI 曝光了自己的一款令人瞩目的 AI 问答搜索引擎——SearchGPT。这款搜索引擎带来了全新的搜索体验&#xff0c;给整个行业带来了巨大的压力。 SearchGPT 支持多种强大的功能。首先&#xff0c;它能够通过…

新一代状态空间模型网络替代Transformer 综述

本文首先初步介绍了状态空间模型&#xff08;SSM&#xff09;的工作原理。然后&#xff0c;从多个方面回顾SSM的相关工作&#xff0c;包括SSM的起源和变化、自然语言处理、计算机视觉、图、多模态处理、多模态和多媒体、点云/事件流数据、时间序列数据等领域的相关工作。 此外…

网络安全设计的技术有哪些?

目录 1. 防火墙 2. 入侵检测系统&#xff08;IDS&#xff09;和入侵防御系统&#xff08;IPS&#xff09; 3. 身份和访问管理&#xff08;IAM&#xff09; 4. 数据加密 5. 网络分割和虚拟化 6. 安全信息和事件管理&#xff08;SIEM&#xff09; 7. 端点保护 8. 配置管理…

链表带环问题的方法证明

目录 一、带环问题的解决 1、固定思路 2、思路后的数学证明 3、不相遇的情况分析 二、环入口问题 ​编辑 1、固定思路 2、数学证明 三、求环的长度 一、带环问题的解决 1、固定思路 链表带环问题比较传统的思路是使用快慢指针&#xff0c;当快和慢指针相遇的时候那么…

工具篇--Window--常用工具命令汇总(持续更新)

文章目录 前言一、常用工具&#xff1a;1.1 window host 修改&#xff1a;1.2 window 端口占用&#xff1a;1.3 打开/关闭防火墙&#xff1a;1.4 JavaScript 对象或值转换为 JSON 字符串: 二、命令行&#xff1a;2.1 获取本机ip&#xff1a; 三、网页在线工具&#xff1a;3.1 本…

吴恩达深度学习笔记:深度学习的 实践层面 (Practical aspects of Deep Learning)1.9-1.10

目录 第一门课&#xff1a;第二门课 改善深层神经网络&#xff1a;超参数调试、正 则 化 以 及 优 化 (Improving Deep Neural Networks:Hyperparameter tuning, Regularization and Optimization)第一周&#xff1a;深度学习的 实践层面 (Practical aspects of Deep Learning)…

LabVIEW自动剪板机控制系统

LabVIEW自动剪板机控制系统 随着工业自动化的快速发展&#xff0c;钣金加工行业面临着生产效率和加工精度的双重挑战。传统的手动或脚踏式剪板机已无法满足现代生产的高效率和高精度要求&#xff0c;因此&#xff0c;自动剪板机控制系统的研究与开发成为了行业发展的必然趋势。…

如何快速开发个性化回收小程序

回收小程序的开发无疑是提升回收业务效率的重要途径。它不仅可以清晰地列出各类回收物品&#xff0c;还能在微信、抖音、支付宝等多个平台同时上线&#xff0c;让回收服务触手可及。那么&#xff0c;如何以最快、最简单、最经济的方式上线这样一个小程序呢&#xff1f; 在这里&…

Linux实训-用户和组的管理

实训1&#xff1a;用户的管理 创建一个新用户user1&#xff0c;设置其主目录为/home/user1。查看/etc/passwd文件的最后一行&#xff0c;看看是如何记录的。查看文件/etc/shadow文件的最后一行&#xff0c;看看如何记录的。给用户user1设置密码。再次查看文件/etc/shadow文件的…

分享5个图源二维码及使用方法

数据是GIS的血液&#xff01; 我们在《4个在ArcGIS中可加载的图源分享》一文中&#xff0c;为大家分享了4个可以直接在ArcMap中打开查看的图源。 现在&#xff0c;我们再分享5个可以在水经微图&#xff08;以下简称“微图”&#xff09;桌面版&#xff08;PC端&#xff09;、…

Kafka Exactly Once 语义实现原理:幂等性与事务消息

01 前言 在现代分布式系统中&#xff0c;确保数据处理的准确性和一致性是至关重要的。Apache Kafka&#xff0c;作为一个广泛使用的流处理平台&#xff0c;提供了强大的消息队列和流处理功能。随着业务需求的增长&#xff0c;Kafka 的事务消息功能应运而生&#xff0c;它允许应…