树专题 —— 二叉搜索树和中序遍历

大家好,我是 方圆。我准备把树写成一个专题,包括二叉搜索树、前序、中序、后序遍历以及红黑树,我也想试试能不能将红黑树写好。

本篇是关于二叉搜索树,也是所有后续学习的基础,其中会涉及前序、中序、后序遍历,后续再介绍相关遍历则是以题目为主。如果大家想要找刷题路线的话,可以参考 Github: LeetCode

二叉搜索树

二叉搜索树(Binary Search Tree)是基础数据结构,在它是完全二叉树的情况下执行查找和插入的时间复杂度为 O(logn),然而如果这棵树是一条 n 个节点做成的线性链表,那么这些操作的时间复杂度为 O(n),它具备如下性质:

  • 若任意节点的左子树不为空,则左子树上所有节点值 均小于 它的根节点值

  • 若任意节点的右子树不为空,则右子树上所有节点值 均大于 它的根节点值

  • 左子树为节点值均小于根节点的二叉搜索树;右子树为节点值均大于根节点的二叉搜索树

二叉搜索树.png

通过中序遍历我们能获取到二叉搜索树的有序序列,二叉树中序遍历模板如下:

    private void midOrder(TreeNode node) {if (node == null) {return;}midOrder(node.left);// do something...midOrder(node.right);}

中序遍历对节点的操作顺序是 “左根右”,恰好能以二叉搜索树节点值递增的顺序访问,二叉搜索树相关的题目大多与中序遍历有关,我们先看几道简单的题目:

注意:树相关的题目一般我们都会选择递归方法求解,但是大家千万不要把自己的脑袋当成计算机去模拟递归的过程,我们只需关注节点的递归顺序和在“当前”节点处所做的逻辑即可

  • 1305. 两棵二叉搜索树中的所有元素

本题我们可以很轻松地将其解出来,分别用两个队列将两棵二叉搜索树中的节点值保存,之后根据大小关系将其合并到结果列表中即可,题解如下:

    public List<Integer> getAllElements(TreeNode root1, TreeNode root2) {LinkedList<Integer> queue1 = new LinkedList<>();LinkedList<Integer> queue2 = new LinkedList<>();midOrder(root1, queue1);midOrder(root2, queue2);List<Integer> res = new ArrayList<>();while (!queue1.isEmpty() || !queue2.isEmpty()) {if (queue1.isEmpty()) {res.add(queue2.poll());continue;}if (queue2.isEmpty()) {res.add(queue1.poll());continue;}if (queue1.peek() <= queue2.peek()) {res.add(queue1.poll());} else {res.add(queue2.poll());}}return res;}private void midOrder(TreeNode node, Queue<Integer> queue) {if (node == null) {return;}midOrder(node.left, queue);queue.offer(node.val);midOrder(node.right, queue);}
  • LCR 174. 寻找二叉搜索树中的目标节点

本题是查找二叉搜索树中的第 K 大节点,我们可以通过中序遍历将所有节点顺序保存下来再返回它的第 K 大节点,题解如下:

class Solution {List<Integer> nodes;public int findTargetNode(TreeNode root, int cnt) {nodes = new ArrayList<>();midOrder(root);return nodes.get(nodes.size() - cnt);}private void midOrder(TreeNode node) {if (node == null) {return;}midOrder(node.left);nodes.add(node.val);midOrder(node.right);}
}
  • 230. 二叉搜索树中第K小的元素

我们再来看一道,本题也可以按照上一道题的思路来求解,不过在这里我们介绍一种更优的的方法:要求的是第 K 小节点,那么每经过一次节点将 K 减一,减到 0 时便是我们想要的节点,那么接下来我们便可以不再进行递归搜索了,避免了后续的无效递归,题解如下:

class Solution {int k;int res;public int kthSmallest(TreeNode root, int k) {this.k = k;midOrder(root);return res;}private void midOrder(TreeNode node) {if (node == null || k == 0) {return;}midOrder(node.left);k--;if (k == 0) {res = node.val;return;}midOrder(node.right);}
}

实现二叉搜索树

现在我们已经对二叉搜索树的性质有了基本的了解,接下来我们看看该如何实现一颗二叉搜索树。

定义 Node 节点类和 root 根节点的全局变量
public class BinarySearchTree {static class Node {int key;int val;Node left;Node right;public Node(int key, int val) {this.key = key;this.val = val;}}// 根节点Node root;}
查询节点值

查询方法的实现还是比较简单的,根据键值的大小关系来判断是去左子树、右子树还是返回当前节点值即可,代码如下:

    /*** 根据 key 获取对应的节点值*/public Integer getValue(int key) {return getValue(root, key);}private Integer getValue(Node node, int key) {if (node == null) {return null;}if (key > node.key) {return getValue(node.right, key);}if (key < node.key) {return getValue(node.left, key);}return node.val;}
插入节点

插入节点的方法和查询方法实现的逻辑基本一致,不过插入节点会 变换父节点对子节点的引用关系,第一个被插入的键就是根节点,第二个被插入的键则会根据大小关系成为根节点的左节点还是右节点,以此类推,代码实现如下:

    /*** 将节点插入二叉搜索树中合适的位置*/public void putNode(int key, int val) {root = putNode(root, key, val);}private Node putNode(Node node, int key, int val) {if (node == null) {return new Node(key, val);}if (key > node.val) {node.right = putNode(node.right, key, val);} else if (key < node.val) {node.left = putNode(node.left, key, val);} else {node.val = val;}return node;}
获取最大/最小节点

这两个方法比较简单,最大节点为右子树最大节点,最小节点为左子树最小节点:

    /*** 获取最大节点*/public Node getMaxNode() {if (root == null) {return null;}return getMaxNode(root);}private Node getMaxNode(Node node) {if (node.right == null) {return node;}return getMaxNode(node.right);}/*** 获取最小节点*/public Node getMinNode() {if (root == null) {return null;}return getMinNode(root);}private Node getMinNode(Node node) {if (node.left == null) {return node;}return getMinNode(node.left);}
向下取整查找

这个方法比较有意思,向下取整是查找小于等于 key 值的最大节点。如果给定的 key 小于根节点的值,那么小于等于 key 的最大节点一定在左子树中;如果给定的 key 大于根节点的值,那么小于等于 key 的最大节点 可能 在右子树中,当右子树中不存在小于等于 key 值的节点的话,最大节点就是根节点,否则为右子树中某节点,代码逻辑如下:

    /*** 向下取整查找*/public Node floor(int key) {return floor(root, key);}private Node floor(Node node, int key) {if (node == null) {return null;}if (key == node.val) {return node;}if (key < node.val) {return floor(node.left, key);}Node right = floor(node.right, key);return right != null ? right : node;}
向上取整查找

向上取整与向下取整的逻辑相反,我们把代码列在下面,具体的执行步骤大家思考一下:

    /*** 向上取整查找*/public Node ceiling(int key) {return ceiling(root, key);}private Node ceiling(Node node, int key) {if (node == null) {return null;}if (key == node.val) {return node;}if (key > node.val) {return ceiling(node.right, key);}Node left = ceiling(node.left, key);return left != null ? left : node;}
删除节点

删除节点在二叉搜索树中实现起来相对不容易,在实现删除任意节点之前,我们先写一下简单的删除最大/小值节点的方法。

删除最小节点

删除最小节点需要找到节点左子树为空树的节点,这个节点便是我们需要删除的最小节点,这个节点被删除后,我们需要将它的右子树拼接到它原来的位置,实现如下:

    /*** 删除最小节点*/public void deleteMin() {root = deleteMin(root);}private Node deleteMin(Node node) {if (node == null) {return null;}if (node.left == null) {return node.right;}node.left = deleteMin(root.left);return node;}
删除最大节点

该实现和删除最小节点的逻辑完全类似,如下:

    /*** 删除最大节点*/public void deleteMax() {root = deleteMax(root);}private Node deleteMax(Node node) {if (node == null) {return null;}if (node.right == null) {return node.left;}node.right = deleteMax(node.right);return node;}
删除指定节点

删除指定 key 值的节点我们需要先找到该节点,之后分情况讨论:

  • 如果该节点左子树为空,那么需要将该节点的右子树拼接到该删除节点的位置;

  • 如果该节点右子树为空,那么需要将该节点的左子树拼接到该删除节点的位置;

  • 前两种情况和我们删除最值节点类似,第三种情况是该节点左右子树均不为空,那么我们可以找到该节点右子树的最小节点,并将该节点的左子树拼接到该最小节点的左子树上(同样地, 我们也可以找到该节点左子树的最大节点,然后将该节点右子树拼接到该最大节点的右子树上),实现如下:

    /*** 删除指定节点*/public void delete(int key) {root = delete(key, root);}private Node delete(int key, Node node) {if (node == null) {return null;}if (key > node.val) {node.right = delete(key, node.right);return node;}if (key < node.val) {node.left = delete(key, node.left);return node;}if (node.left == null) {return node.right;}if (node.right == null) {return node.left;}Node min = getMinNode(node.right);min.left = node.left;return node;}
范围查找

二叉搜索树的范围查找实现很简单,只需根据大小条件关系中序遍历即可,实现如下:

    /*** 范围查找** @param left  区间下界* @param right 区间上界*/public List<Integer> keys(int left, int right) {ArrayList<Integer> res = new ArrayList<>();keys(root, left, right, res);return res;}private void keys(Node node, int left, int right, ArrayList<Integer> res) {if (node == null) {return;}if (node.val > left) {keys(node.left, left, right, res);}if (node.val >= left && node.val <= right) {res.add(node.val);}if (node.val < right) {keys(node.right, left, right, res);}}

总的来说,二叉搜索树的实现并不困难,大家最好将这些方法都实现一遍,以便更好的学习和理解。

相关习题

现在我们对二叉搜索树和中序遍历已经比较熟悉了,接下来再做一些题目来检查检查。在前文中我们已经说过,一般二叉搜索树的题目大概率会与中序遍历相关,此外,进行中序遍历时,有的题目需要我们 记录节点的“前驱节点” 来帮助解题,这一点需要注意。

  • 235. 二叉搜索树的最近公共祖先

在二叉搜索树上找最近的公共祖先,我们分情况讨论:如果两节点值都比当前节点小的话,那么去左子树找;如果两节点值都比当前节点大的话,那么去右子树找;如果两节点中任意一节点等于当前节点或者两节点分别大于或小于当前节点的话,那么当前节点就是最近的公共祖先(大家可以画图看一下),题解如下:

    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {if (p.val < root.val && q.val < root.val) {return lowestCommonAncestor(root.left, p, q);}if (p.val > root.val && q.val > root.val) {return lowestCommonAncestor(root.right, p, q);}return root;}
  • 450. 删除二叉搜索树中的节点

本题和我们上述二叉搜索树删除节点的方法实现逻辑一致,只不过在这里我们获取右子树的最小节点时是通过迭代实现的,题解如下:

    public TreeNode deleteNode(TreeNode root, int key) {if (root == null) {return null;}if (key > root.val) {root.right = deleteNode(root.right, key);return root;}if (key < root.val) {root.left = deleteNode(root.left, key);return root;}if (root.right == null) {return root.left;}if (root.left == null) {return root.right;}TreeNode rightNode = root.right;while (rightNode.left != null) {rightNode = rightNode.left;}rightNode.left = root.left;return root.right;}
  • 669. 修剪二叉搜索树

本题也是依赖二叉搜索树的性质来求解:

  • 如果当前节点值比 low 小的话,那么需要将它修剪掉,并去它的右子树找满足区间条件的节点;

  • 如果当前节点值比 high 大的话,那么也需要将它修剪掉,并去它的左子树找满足区间条件的节点;

  • 如果当前节点值在区间范围内,则需要对它的左子树和右子树进行修剪,并将当前节点返回即可,题解如下:

    public TreeNode trimBST(TreeNode root, int low, int high) {if (root == null) {return null;}if (root.val < low) {return trimBST(root.right, low, high);}if (root.val > high) {return trimBST(root.left, low, high);}root.left = trimBST(root.left, low, high);root.right = trimBST(root.right, low, high);return root;}
  • 98. 验证二叉搜索树

二叉搜索树需要满足根节点大于左子树任意节点和根节点小于右子树任意节点的性质,我们根据这个条件进行验证即可,需要注意的是:我们需要记录前驱节点来帮助比较节点的大小关系,题解如下:

    long pre = Long.MIN_VALUE;public boolean isValidBST(TreeNode root) {if (root == null) {return true;}boolean left = isValidBST(root.left);if (pre >= root.val) {return false;}pre = root.val;boolean right = isValidBST(root.right);return left && right;}
  • LCR 155. 将二叉搜索树转化为排序的双向链表

根据中序遍历的顺序拼接链表即可,可以为前驱节点创建哨兵节点,减少判空逻辑,题解如下:

    Node pre = null;public Node treeToDoublyList(Node root) {if (root == null) {return null;}Node head = new Node();pre = head;midOrder(root);head.right.left = pre;pre.right = head.right;return head.right;}private void midOrder(Node node) {if (node == null) {return;}midOrder(node.left);pre.right = node;node.left = pre;pre = node;midOrder(node.right);}
  • 99. 恢复二叉搜索树

本题解题思路并不复杂,题目确定有两个节点发生了交换,那么我们通过两个指针对它们进行标记即可,题解如下:

    TreeNode one = null, two = null;TreeNode pre;public void recoverTree(TreeNode root) {midOrder(root);int temp = one.val;one.val = two.val;two.val = temp;}private void midOrder(TreeNode node) {if (node == null) {return;}midOrder(node.left);if (pre != null && pre.val > node.val) {if (one == null) {one = pre;}two = node;}pre = node;midOrder(node.right);}
  • 面试题 04.06. 后继者

查找指定节点的后继节点,如果当前驱节点为指定节点时,那么当前节点即为所求的后继节点,题解如下:

    TreeNode pre = null;public TreeNode inorderSuccessor(TreeNode root, TreeNode p) {if (root == null) {return null;}TreeNode left = inorderSuccessor(root.left, p);if (pre != null && pre.val == p.val) {pre = root;return root;}pre = root;TreeNode right = inorderSuccessor(root.right, p);return left == null ? right : left;}

巨人的肩膀

  • 维基百科 - 二元搜寻树

  • 《Hello算法》:第 7.4 章 二叉搜索树

  • 《算法 第四版》:第 3.2 章 二叉查找树

  • 《算法导论 第三版》:第 12 章 二叉搜索树

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

css排版—— 一篇优雅的文章(中英文) vs 聊天框的特别排版

文章 <div class"contentBox"><p>这是一篇范文——仅供测试使用</p><p>With the coming of national day, I have a one week holiday. I reallyexpect to it, because it want to have a short trip during these days. Iwill travel to Ji…

PTL货位指引标签为仓储管理打开新思路

PTL货位指引标签是一种新型的仓储管理技术&#xff0c;它通过LED灯光指引和数字显示&#xff0c;为仓库管理带来了全新的管理思路和效率提升&#xff0c;成为现代物流仓库管理中的重要工具。 首先&#xff0c;PTL货位指引标签为仓储管理业务带来了管理新思路。传统的仓库管理中…

数据结构:AVL树的旋转(平衡搜索二叉树)

1、AVL树简介 AVL树是最先发明的自平衡二叉查找树。在AVL树中任何节点的两个子树的高度最大差别为1&#xff0c;所以它也被称为高度平衡树。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。AVL树得名于它的发明者G. M. Adelson-Velsky和E. M. Landis&#xff0c;他们…

flutter项目引入本地静态图片资源并展示

想要在flutter中引入静态资源&#xff0c;需要配置pubspec.yaml&#xff0c;将本地的静态资源添加到assets下面&#xff1a; 然后在flutter引入这些静态资源&#xff1a; Image.asset("images/squick.png") 就可以在app中看到这个图片了&#xff1a; 也可以使用网…

falsk框架中安装flask-mysqldb报错解决方案

错误示例 我的是py37版本&#xff0c;无法直接安装flask-mysqldb pip install flask-mysqldb报错如下 解决方案 先去第三方库 https://www.lfd.uci.edu/~gohlke/pythonlibs/#mysqlclient 下载mysqlclient 这个是我的版本 mysqlclient-1.4.6-cp37-cp37m-win_amd64.whl 下…

C++ [继承]

本文已收录至《C语言和高级数据结构》专栏&#xff01; 作者&#xff1a;ARMCSKGT 继承 前言正文继承的概念及定义继承的概念继承的定义重定义 基类和派生类对象赋值转换派生类中的默认成员函数隐式调用显示调用 继承中的友元与静态成员友元静态成员 菱形继承概念 虚继承原理继…

react组件通信

目录 前言&#xff1a; 父子组件通信 子父组件通信 兄弟组件通信 总结 前言&#xff1a; React是一种流行的JavaScript库&#xff0c;用于构建现代化的、高性能的Web应用程序。在React中&#xff0c;组件是代码的构建块。组件通信是React中一个非常重要的概念&#xff0c;…

Kafka反序列化RCE漏洞(CVE-2023-34040)

漏洞描述 Spring Kafka 是 Spring Framework 生态系统中的一个模块&#xff0c;用于简化在 Spring 应用程序中集成 Apache Kafka 的过程&#xff0c;记录 (record) 指 Kafka 消息中的一条记录。 受影响版本中默认未对记录配置 ErrorHandlingDeserializer&#xff0c;当用户将…

Bean的四种实例化方式以及BeanFactory和FactoryBean的区别

2023.11.8 Spring为Bean提供了多种实例化方式&#xff0c;通常包括4种方式。 第一种&#xff1a;通过构造方法实例化第二种&#xff1a;通过简单工厂模式实例化第三种&#xff1a;通过factory-bean实例化第四种&#xff1a;通过FactoryBean接口实例化 通过构造方法实例化 创…

android display 笔记(三)WMS

用来记录学习wms&#xff0c;后续会一点一点更新。。。。。。 代码&#xff1a;android14 WMS是在SystemServer进程中启动的 在SystemServer中的main方法中&#xff0c;调用run方法。 private void run() { // Initialize native services.初始化服务&#xff0c;加载andro…

FreeRTOS_空闲任务

目录 1. 空闲任务详解 1.1 空闲任务简介 1.2 空闲任务的创建 1.3 空闲任务函数 2. 空闲任务钩子函数详解 2.1 钩子函数 2.2 空闲任务钩子函数 3. 空闲任务钩子函数实验 3.1 main.c 空闲任务是 FreeRTOS 必不可少的一个任务&#xff0c;其他 RTOS 类系统也有空闲任务&a…

广东开放大学:电大搜题助力学子迎考利器

近年来&#xff0c;广东开放大学一直致力于为广大学子提供优质的教育资源和学习服务。作为一所专注于远程教育的学府&#xff0c;广东开放大学不仅拥有雄厚的师资力量和丰富的教育经验&#xff0c;还致力于创新教学手段&#xff0c;为学生提供更便捷、高效的学习体验。在这个信…

2023年11月在线IDE流行度最新排名

点击查看最新在线IDE流行度最新排名&#xff08;每月更新&#xff09; 2023年11月在线IDE流行度最新排名 TOP 在线IDE排名是通过分析在线ide名称在谷歌上被搜索的频率而创建的 在线IDE被搜索的次数越多&#xff0c;人们就会认为它越受欢迎。原始数据来自谷歌Trends 如果您相…

08.Diffusion Model数学原理分析(上)

文章目录 Diffusion Model回顾Diffusion Model算法TrainingInference 图像生成模型的本质目标MLE vs KLVAE计算 P θ ( x ) P_\theta(x) Pθ​(x)Lower bound of log ⁡ P ( x ) \log P(x) logP(x) DDPM计算 P θ ( x ) P_\theta(x) Pθ​(x)Lower bound of log ⁡ P ( x ) \…

数据结构与算法-(11)---有序表(OrderedList)

&#x1f308;个人主页: Aileen_0v0 &#x1f525;系列专栏:PYTHON学习系列专栏 &#x1f4ab;"没有罗马,那就自己创造罗马~" 目录 知识回顾及总结 有序表的引入 ​编辑 实现有序表 1.有序表-类的构造方法 2.有序表-search方法的实现 3.有序表-add方法的实现…

【技术类-01】doc转PDF程序卡死的解决方案,

摘要&#xff1a; 1、出现 raise AttributeError("%s.%s" % (self._username_, attr))&#xff09; 2、表现&#xff1a;doc转PDF卡死&#xff08;白条不动或出现以上英文&#xff09; 3、解决&#xff1a;在docx保存代码行后面加上time.sleep(3) 4、原因&#x…

SpringBoot系列之集成Redission入门与实践教程

Redisson是一款基于java开发的开源项目&#xff0c;提供了很多企业级实践&#xff0c;比如分布式锁、消息队列、异步执行等功能。本文基于Springboot2版本集成redisson-spring-boot-starter实现redisson的基本应用 软件环境&#xff1a; JDK 1.8 SpringBoot 2.2.1 Maven 3.2…

Java进阶篇--线程池之FutureTask

目录 FutureTask简介 FutureTask的基本使用 FutureTask的应用场景 FutureTask简介 FutureTask是Java中的一个类&#xff0c;用于表示可获取结果的异步任务。它实现了java.util.concurrent.Future接口&#xff0c;提供了启动和取消异步任务、查询任务是否已完成以及获取最终…

腾讯云3年云服务器价格及购买教程

腾讯云作为国内领先的云计算服务提供商&#xff0c;提供了多种优惠的云服务器套餐&#xff0c;以满足不同用户的需求&#xff0c;本文将详细介绍腾讯云3年云服务器价格及购买教程&#xff0c;新老用户均可购买&#xff01; 1、活动页面&#xff1a;传送门>>> 2、进入…

P3379 【模板】最近公共祖先(LCA)

洛谷里面8页题解千篇一律&#xff0c;就没有用线段树求解的&#xff0c;这下不得不由本蒟蒻来生啃又臭又硬&#xff0c;代码又多的线段树了。 样例的欧拉序列&#xff1a;4 2 4 1 3 1 5 1 4 记录每个节点最早在欧拉序列中的时间&#xff0c;任意两个节点的LCA就是他们两个节点…