瑞_数据结构与算法_二叉搜索树

文章目录

    • 1 什么是二叉搜索树
      • 1.1 二叉搜索树的特征
      • 1.2 前驱后继
    • 2 二叉搜索树的Java实现
      • 2.1 定义二叉搜索树节点类BSTNode
        • 泛型key改进
      • 2.2 实现查找方法get(int key)
        • 递归实现
        • 非递归实现 ★
        • 非递归实现 泛型key版本
      • 2.3 实现查找最小方法min()
        • 递归实现
        • 非递归实现 ★
      • 2.4 实现查找最大方法max()
        • 递归实现
        • 非递归实现 ★
      • 2.5 实现新增方法put(int key, Object value)
        • 递归实现
        • 非递归实现 ★
      • 2.6 实现查找关键字的前任值predecessor(int key)
      • 2.7 查找关键字的后任值successor(int key)
      • 2.8 根据关键字删除remove(int key)
        • 非递归实现 ★
        • 递归实现
    • 3 二叉搜索树——范围查询
      • 3.1 找所有小于索引的值less(int key)
      • 3.2 找所有大于索引的值greater(int key)
      • 3.3 找范围值between(int key1, int key2)
      • 3.3 小结

🙊前言:本文章为瑞_系列专栏之《数据结构与算法》的二叉搜索树篇。由于博主是从B站黑马程序员的《数据结构与算法》学习到的相关知识,所以本系列专栏主要针对该课程进行笔记总结和拓展,文中的部分原理及图解也是来源于黑马提供的资料。本文仅供大家交流、学习及研究使用,禁止用于商业用途,违者必究!

在这里插入图片描述

1 什么是二叉搜索树

  二叉搜索树最早是由Bernoulli兄弟在18世纪中提出的,但是真正推广和应用该数据结构的是1960年代的D.L. Gries。他的著作《The Science of Programming》中详细介绍了二叉搜索树的实现和应用。

  在计算机科学的发展中,二叉搜索树成为了一种非常基础的数据结构,被广泛应用在各种领域,包括搜索、排序、数据库索引等。随着计算机算力的提升和对数据结构的深入研究,二叉搜索树也不断被优化和扩展,例如AVL树、红黑树等。

1.1 二叉搜索树的特征

  二叉搜索树(也称二叉排序树)是符合下面特征的二叉树:

  1. 树节点增加 key 属性,用来比较谁大谁小,key 不可以重复
  2. 对于任意一个树节点,它的 key 比左子树的 key 都大,同时也比右子树的 key 都小,例如下图所示:

在这里插入图片描述

  轻易看出要查找 7 (从根开始)自然就可应用二分查找算法,只需三次比较

  • 与 4 比,较之大,向右找
  • 与 6 比,较之大,继续向右找
  • 与 7 比,相等,即找到

  查找的时间复杂度与树高相关,插入、删除也是如此。

  • 如果这棵树长得还不赖(左右平衡)如上图,那么时间复杂度均是 O ( log ⁡ N ) O(\log{N}) O(logN)
  • 当然,这棵树如果长得丑(左右高度相差过大)如下图,那么这时是最糟的情况(链表),时间复杂度是 O ( N ) O(N) O(N)

在这里插入图片描述

注:

  • 二叉搜索树 - 英文 binary search tree,简称 BST
  • 二叉排序树 - 英文 binary ordered tree 或 binary sorted tree
    二叉树的相关知识可以参考博客:《瑞_数据结构与算法_二叉树》
    二分查找的相关知识可以参考博客:瑞_数据结构与算法_二分查找

1.2 前驱后继

  • 前驱值(前任值):找到一个离该节点最近且比该节点存储值小的值
  • 后继值(后任值):找到一个离该节点最近且比该节点存储值大的值



2 二叉搜索树的Java实现


  以简单实现为主,主要是学习其思想,和Java中的Map集合实现的思维类似,通过key查找value

内部节点类BSTNode中含有属性:

  • 索引
  • 存储值
  • 左孩子
  • 右孩子属性

BSTTree二叉搜索树类含有方法:

  • 查找关键字对应的值get(int key)
  • 查找最小关键字对应值min()
  • 查找最大关键字对应值max()
  • 存储关键字和对应值put(int key, Object value)
  • 查找关键字的前任值predecessor(int key)
  • 查找关键字的后任值successor(int key)
  • 根据关键字删除remove(int key)

2.1 定义二叉搜索树节点类BSTNode

  BSTNode即二叉搜索树节点类(内部类),含有索引、存储值、左孩子、右孩子属性,由于是简单实现,索引定义基本数据类型(若希望任意类型作为 key,则后续可以将其设计为 Comparable 接口)

/*** Binary Search Tree 二叉搜索树** @author LiaoYuXing-Ray* @version 1.0* @createDate 2024/1/24 19:27**/
public class BSTTree {/*** 根节点*/BSTNode root;static class BSTNode {// 索引,比较值int key;// 该节点的存储值Object value;// 左孩子BSTNode left;// 右孩子BSTNode right;public BSTNode(int key) {this.key = key;}public BSTNode(int key, Object value) {this.key = key;this.value = value;}public BSTNode(int key, Object value, BSTNode left, BSTNode right) {this.key = key;this.value = value;this.left = left;this.right = right;}}}
泛型key改进

  使用泛型上限语法,让泛型的Key继承Comparable接口,使其能够进行大小比较。改进后代码如下(含get方法):

/*** 二叉搜索树, 泛型 key 版本*/
public class BSTTree<K extends Comparable<K>, V> {static class BSTNode<K, V> {K key;V value;BSTNode<K, V> left;BSTNode<K, V> right;public BSTNode(K key) {this.key = key;}public BSTNode(K key, V value) {this.key = key;this.value = value;}public BSTNode(K key, V value, BSTNode<K, V> left, BSTNode<K, V> right) {this.key = key;this.value = value;this.left = left;this.right = right;}}BSTNode<K, V> root;public V get(K key) {if (key == null) {return null;}BSTNode<K, V> node = root;while (node != null) {int result = key.compareTo(node.key);if (result < 0) {node = node.left;} else if (result > 0) {node = node.right;} else {return node.value;}}return null;}
}

瑞:此处泛型没有定义为T而定义K,是因为K、V迎合map集合中的key,value,容易理解

2.2 实现查找方法get(int key)

  get(int key)方法通过关键字查找对应的值

递归实现

  get(int key)方法是通过key索引关键字查找对应的值,可以使用递归查找

    /*** <h3>查找关键字对应的值</h3>** @param key 关键字* @return 关键字对应的值*/public Object get(int key) {return doGet(root, key);}/*** 私有 - 封装BSTNode参数,外部只需要调用get(int key)方法即可,不用关心具体细节** @param node 根节点* @param key 索引关键字 * @return java.lang.Object 关键字对应的值* @author LiaoYuXing-Ray 2024/1/24 19:53**/private Object doGet(BSTNode node, int key) {if (node == null) {return null; // 没找到}if (key < node.key) {return doGet(node.left, key); // 向左找} else if (node.key < key) {return doGet(node.right, key); // 向右找} else {return node.value; // 找到了}}

瑞:该递归实现是尾递归

  尾递归是一种特殊形式的递归,它的特点是在函数的最后一步调用自身,并且不需要保留外层函数的调用记录。
  注意区别伪递归,伪递归通常指的是使用迭代实现的递归算法,它们在逻辑上模拟了递归的过程,但实际上并不通过函数调用自身来实现。

  尾递归的特点:

  • 在函数的最后一步调用自身,不保留外层函数的调用记录。
  • 可以仅使用常量级的栈空间,与迭代过程类似。
  • 有着与循环同样优秀的计算性能。

    伪递归的特点:
  • 使用迭代来模拟递归的逻辑。
  • 不涉及实际的函数自我调用。
  • 通常用于那些不支持尾递归优化的编程语言中,以减少内存消耗。

  方法测试代码如下:

/*** Binary Search Tree 二叉搜索树** @author LiaoYuXing-Ray* @version 1.0* @createDate 2024/1/24 19:27**/
public class BSTTree {/*** 根节点*/BSTNode root;static class BSTNode {// 索引,比较值int key;// 该节点的存储值Object value;// 左孩子BSTNode left;// 右孩子BSTNode right;public BSTNode(int key) {this.key = key;}public BSTNode(int key, Object value) {this.key = key;this.value = value;}public BSTNode(int key, Object value, BSTNode left, BSTNode right) {this.key = key;this.value = value;this.left = left;this.right = right;}}/*** <h3>查找关键字对应的值</h3>* 递归实现** @param key 关键字* @return 关键字对应的值*/public Object get(int key) {return doGet(root, key);}/*** 私有 - 封装BSTNode参数,外部只需要调用get(int key)方法即可,不用关心具体细节** @param node 根节点* @param key  索引关键字* @return java.lang.Object 关键字对应的值* @author LiaoYuXing-Ray 2024/1/24 19:53**/private Object doGet(BSTNode node, int key) {if (node == null) {return null; // 没找到}if (key < node.key) {return doGet(node.left, key); // 向左找} else if (node.key < key) {return doGet(node.right, key); // 向右找} else {return node.value; // 找到了}}public static void main(String[] args) {/*4/   \2     6/ \   / \1   3 5   7*/BSTNode n1 = new BSTNode(1, "Ray1");BSTNode n3 = new BSTNode(3, "Ray3");BSTNode n2 = new BSTNode(2, "Ray2", n1, n3);BSTNode n5 = new BSTNode(5, "Ray5");BSTNode n7 = new BSTNode(7, "Ray7");BSTNode n6 = new BSTNode(6, "Ray6", n5, n7);BSTNode root = new BSTNode(4, "Ray4", n2, n6);BSTTree tree = new BSTTree();tree.root = root;for (int i = 0; i <= 8; i++) {System.out.println(tree.get(i));}}}

  运行结果如下,找不到索引为0和8的元素存储值,而1-7都能找到,符合预期:

    nullRay1Ray2Ray3Ray4Ray5Ray6Ray7null
非递归实现 ★

  由于之前使用的是尾递归实现,而尾递归转换成迭代法是非常容易的,性能上也能有所提升,所以一般情况下,如果使用尾递归都会转化为迭代法。

    /*** <h3>查找关键字对应的值</h3>* 非递归实现** @param key 关键字* @return 关键字对应的值*/public Object get(int key) {BSTNode node = root;while (node != null) {if (key < node.key) {// 向左找node = node.left;} else if (node.key < key) {// 向右找node = node.right;} else {// 找到return node.value;}}// 没找到return null;}

瑞:测试用例可以使用同一个,同样能通过,且性能更佳。

非递归实现 泛型key版本

  如果希望让除 int 外更多的类型能够作为 key,一种方式是 key 必须实现 Comparable 接口。

  由于compareTo方法返回-1,表示key < node.key,0表示key == node.key,1表示key > node.key,所以改进后代码如下:

    public V get(K key) {BSTNode<K, V> node = root;while (node != null) {/*-1 key < p.key0 key == p.key1 key > p.key*/int result = key.compareTo(node.key);if (result < 0) {node = node.left;} else if (result > 0) {node = node.right;} else {return node.value;}}return null;}

  还有一种做法不要求 key 实现 Comparable 接口,而是在构造 Tree 时把比较规则作为 Comparator 传入,将来比较 key 大小时都调用此 Comparator 进行比较,这种做法可以参考 Java 中的 java.util.TreeMap

  方法测试代码如下:

/*** 二叉搜索树, 泛型 key 版本*/
public class BSTTree<K extends Comparable<K>, V> {static class BSTNode<K, V> {K key;V value;BSTNode<K, V> left;BSTNode<K, V> right;public BSTNode(K key) {this.key = key;}public BSTNode(K key, V value) {this.key = key;this.value = value;}public BSTNode(K key, V value, BSTNode<K, V> left, BSTNode<K, V> right) {this.key = key;this.value = value;this.left = left;this.right = right;}}BSTNode<K, V> root;public V get(K key) {if (key == null) {return null;}BSTNode<K, V> node = root;while (node != null) {/*-1 key < node.key0 key == node.key1 key > node.key*/int result = key.compareTo(node.key);if (result < 0) {node = node.left;} else if (result > 0) {node = node.right;} else {return node.value;}}return null;}public static void main(String[] args) {BSTNode<String,String> n1 = new BSTNode<>("a", "RayA");BSTNode<String,String> n3 = new BSTNode<>("c", "RayC");BSTNode<String,String> n2 = new BSTNode<>("b", "RayB", n1, n3);BSTNode<String,String> n5 = new BSTNode<>("e","RayE");BSTNode<String,String> n7 = new BSTNode<>("g","RayG");BSTNode<String,String> n6 = new BSTNode<>("f", "RayF", n5, n7);BSTNode<String,String> root = new BSTNode<>("d", "RayD", n2, n6);BSTTree<String,String> tree = new BSTTree<>();tree.root = root;System.out.println(tree.get("a"));System.out.println(tree.get("b"));System.out.println(tree.get("c"));System.out.println(tree.get("d"));System.out.println(tree.get("e"));System.out.println(tree.get("f"));System.out.println(tree.get("g"));System.out.println(tree.get("h"));}
}

  运行结果如下,符合预期:

	RayARayBRayCRayDRayERayFRayGnull


由于使用泛型代码阅读对新手会比较困难,本文主要是学习思想,所以后续方法采用int类型作为key

2.3 实现查找最小方法min()

  min()方法查找最小关键字key的对应存储值

递归实现

  根据二叉搜索树的特征,树中最左的节点即为最小索引(关键字),即向左走到头即可,所以只要左孩子为null,即找到最小关键字,使用递归实现的代码就非常简单,如下:

    /*** <h3>查找最小关键字对应值</h3>* 递归实现** @return 关键字对应的值*/public Object min() {return doMin(root);}public Object doMin(BSTNode node) {if (node == null) {return null;}// 左边已走到头if (node.left == null) {return node.value;}return doMin(node.left);}
非递归实现 ★

  由于之前使用的是尾递归实现,所以下面转化为迭代法,代码如下:

    /*** <h3>查找最小关键字对应值</h3>* 非递归实现** @return 关键字对应的值*/public Object min() {return min(root);}private Object min(BSTNode node) {if (node == null) {return null;}BSTNode p = node;// 左边未走到头while (p.left != null) {p = p.left;}return p.value;}

  方法测试代码如下:

    public static void main(String[] args) {/*4/   \2     6/ \   / \1   3 5   7*/BSTNode n1 = new BSTNode(1, "Ray1");BSTNode n3 = new BSTNode(3, "Ray3");BSTNode n2 = new BSTNode(2, "Ray2", n1, n3);BSTNode n5 = new BSTNode(5, "Ray5");BSTNode n7 = new BSTNode(7, "Ray7");BSTNode n6 = new BSTNode(6, "Ray6", n5, n7);BSTNode root = new BSTNode(4, "Ray4", n2, n6);BSTTree tree = new BSTTree();tree.root = root;System.out.println(tree.min()); }

  运行结果如下,查找到最小(最左节点)索引key为1的存储值Ray1,符合预期:

	Ray1

2.4 实现查找最大方法max()

  max()方法查找最大关键字key的对应存储值

递归实现

  与min()方法实现的思想类似,树中最右的节点即为最大索引(关键字),即向右走到头即可,所以只要右孩子为null,即找到最大关键字,使用递归实现的代码如下:

    /*** <h3>查找最大关键字对应值</h3>* 递归实现** @return 关键字对应的值*/public Object max() {return doMax(root);}public Object doMax(BSTNode node) {if (node == null) {return null;}// 右边已走到头if (node.right == null) {return node.value;}return doMax(node.right);}
非递归实现 ★

  由于之前使用的是尾递归实现,所以下面转化为迭代法,代码如下:

    /*** <h3>查找最大关键字对应值</h3>** @return 关键字对应的值*/public Object max() {return max(root);}private Object max(BSTNode node) {if (node == null) {return null;}BSTNode p = node;while (p.right != null) {p = p.right;}return p.value;}

  方法测试代码如下:

    public static void main(String[] args) {BSTNode n1 = new BSTNode(1, "Ray1");BSTNode n3 = new BSTNode(3, "Ray3");BSTNode n2 = new BSTNode(2, "Ray2", n1, n3);BSTNode n5 = new BSTNode(5, "Ray5");BSTNode n7 = new BSTNode(7, "Ray7");BSTNode n6 = new BSTNode(6, "Ray6", n5, n7);BSTNode root = new BSTNode(4, "Ray4", n2, n6);BSTTree tree = new BSTTree();tree.root = root;System.out.println(tree.max());}

  运行结果如下,查找到最大(最右节点)索引key为7的存储值Ray7,符合预期:

	Ray7

2.5 实现新增方法put(int key, Object value)

  新增put(int key, Object value)方法存储关键字和对应值,和Java中的Map的put方法类似,分为两种情况:

  1️⃣ key在整个树中已经存在,新增操作变为更新操作,将旧的值替换为新的值
  2️⃣ key在整个树中未存在,执行新增操作,将key value添加到树中

递归实现

  递归实现思路如下:

  • 若找到 key,走 else 更新找到节点的值
  • 若没找到 key,走第一个 if,创建并返回新节点
    • 返回的新节点,作为上次递归时 node 的左孩子或右孩子
    • 缺点是,会有很多不必要的赋值操作
    /*** <h3>存储关键字和对应值</h3>* 递归实现** @param key   关键字* @param value 值*/public void put(int key, Object value) {root = doPut(root, key, value);}private BSTNode doPut(BSTNode node, int key, Object value) {if (node == null) {return new BSTNode(key, value);}if (key < node.key) {node.left = doPut(node.left, key, value);} else if (node.key < key) {node.right = doPut(node.right, key, value);} else {node.value = value;}return node;}
非递归实现 ★

  查找逻辑和get方法的逻辑基本一样,区别是如果查找到值则更新node.value = value;,没查找到则新增。在新增时为了防止频繁的更新操作,添加parent父节点,判断新增节点是父节点的左孩子还是右孩子,然后再新增,代码如下:

    /*** <h3>存储关键字和对应值</h3>* 非递归实现** @param key   关键字* @param value 值*/public void put(int key, Object value) {BSTNode node = root;BSTNode parent = null; // 父节点while (node != null) {parent = node;if (key < node.key) {node = node.left;} else if (node.key < key) {node = node.right;} else {// 1. key 存在则更新node.value = value;return;}}// 2. key 不存在则新增,判断新节点是父节点的左孩子还是右孩子// parent 为空代表树为空的情况,那新增节点就是根节点if (parent == null) {root = new BSTNode(key, value);}// 新增节点为左孩子else if (key < parent.key) {parent.left = new BSTNode(key, value);}// 新增节点为右孩子else {parent.right = new BSTNode(key, value);}}

  测试代码:

    public static void main(String[] args) {/*4/   \2     6/ \   / \1   3 5   7*/BSTNode n1 = new BSTNode(1, "Ray1");BSTNode n3 = new BSTNode(3, "Ray3");BSTNode n2 = new BSTNode(2, "Ray2", n1, n3);BSTNode n5 = new BSTNode(5, "Ray5");BSTNode n7 = new BSTNode(7, "Ray7");BSTNode n6 = new BSTNode(6, "Ray6", n5, n7);BSTNode root = new BSTNode(4, "Ray4", n2, n6);BSTTree createTree = new BSTTree();createTree.root = root;BSTTree tree = new BSTTree();tree.put(4, new Object());tree.put(2, new Object());tree.put(6, new Object());tree.put(1, new Object());tree.put(3, new Object());tree.put(5, new Object());tree.put(7, new Object());System.out.println(createTree == tree);tree.put(1, "Ray486");System.out.println(tree.get(1).equals("Ray486"));}

  两树的key一致但输出false,说明不是同样的树,索引1的存储值被成功替换,均符合预期,测试结果如下:

	falsetrue

2.6 实现查找关键字的前任值predecessor(int key)

  回顾跳转前驱后继

  一个节点的前驱(前任)节点是指:比它小的节点中,最大的那个

  一个节点的后继(后任)节点是指:比它大的节点中,最小的那个

在这里插入图片描述

  例如上图中

  • 1 没有前驱,后继是 2
  • 2 前驱是 1,后继是 3
  • 3 前驱是 2,后继是 4

  想要找到二叉搜索树的某节点的前驱后继节点,简单(但不高效)的办法是中序遍历,即可获得排序结果,此时很容易找到前驱后继。因为使用中序遍历后二叉搜索树的值就是升序的,有了升序排序,前驱后继节点就很好找。但是,由于中序遍历的性能不高,所以不推荐使用。

  要效率更高,需要研究一下规律,找前驱分成以下 2 种情况:

  1️⃣节点 左子树,此时前驱节点就是左子树的最大值,图中属于这种情况的有:

  • 2 的前驱是1
  • 4 的前驱是 3
  • 6 的前驱是 5
  • 7 的前驱是 6

  2️⃣节点 没有 左子树,若离它最近的祖先自从左而来,此祖先即为前驱,如:

  • 3 的祖先 2 自左而来,前驱 2
  • 5 的祖先 4 自左而来,前驱 4
  • 8 的祖先 7 自左而来,前驱 7
  • 1 没有这样的祖先,前驱 null

瑞:祖先不止是父节点,也可以是父节点的父节点,父节点的的父节点的父节点…

  对于情况2,只需要添加一个指针,记录最近一个自左而来的祖先即可,实现代码如下:

    /*** <h3>查找关键字的前任值</h3>** @param key 关键字* @return 前任值*/public Object predecessor(int key) {BSTNode p = root;// 自左而来的祖先BSTNode ancestorFromLeft = null;while (p != null) {if (key < p.key) {p = p.left;} else if (p.key < key) {// 记录自左而来的祖先ancestorFromLeft = p;p = p.right;} else {break;}}// 没找到节点if (p == null) {return null;}// 找到节点 情况1:节点有左子树,此时前任就是左子树的最大值if (p.left != null) {return max(p.left);}// 找到节点 情况2:节点没有左子树,若离它最近的、自左而来的祖先就是前任return ancestorFromLeft != null ?ancestorFromLeft.value : null;}

2.7 查找关键字的后任值successor(int key)

  回顾跳转前驱后继

  一个节点的前驱(前任)节点是指:比它小的节点中,最大的那个

  一个节点的后继(后任)节点是指:比它大的节点中,最小的那个

在这里插入图片描述

  与前驱类似找后继也分成 2 种情况:

  1️⃣节点有右子树,此时后继节点即为右子树的最小值,如:

  • 2 的后继 3
  • 3 的后继 4
  • 5 的后继 6
  • 7 的后继 8

  2️⃣节点没有右子树,若离它最近的祖先自右而来,此祖先即为后继,如:

  • 1 的祖先 2 自右而来,后继 2
  • 4 的祖先 5 自右而来,后继 5
  • 6 的祖先 7 自右而来,后继 7
  • 8 没有这样的祖先,后继 null

  代码实现和前任类似,如下:

    /*** <h3>查找关键字的后任值</h3>** @param key 关键字* @return 后任值*/public Object successor(int key) {BSTNode p = root;// 自右而来的祖先BSTNode ancestorFromRight = null;while (p != null) {if (key < p.key) {// 记录自右而来的祖先ancestorFromRight = p;p = p.left;} else if (p.key < key) {p = p.right;} else {break;}}// 没找到节点if (p == null) {return null;}// 找到节点 情况1:节点有右子树,此时后任就是右子树的最小值if (p.right != null) {return min(p.right);}// 找到节点 情况2:节点没有右子树,若离它最近的、自右而来的祖先就是后任return ancestorFromRight != null ?ancestorFromRight.value : null;}

2.8 根据关键字删除remove(int key)

  删除remove(int key)方法需要考虑的情况较多。要删除某节点(称为 D),必须先找到被删除节点的父节点,这里称为 Parent,具体情况如下:

  1. 删除节点没有左孩子,将右孩子托孤给 Parent
  2. 删除节点没有右孩子,将左孩子托孤给 Parent
  3. 删除节点左右孩子都没有,已经被涵盖在情况1、情况2 当中,把 null 托孤给 Parent
  4. 删除节点左右孩子都有,可以将它的后继节点(称为 S)托孤给 Parent,设 S 的父亲为 SP,又分两种情况
    1. SP 就是被删除节点,此时 D 与 S 紧邻,只需将 S 托孤给 Parent
    2. SP 不是被删除节点,此时 D 与 S 不相邻,此时需要将 S 的后代托孤给 SP,再将 S 托孤给 Parent

  删除本身很简单,只要通过索引查找到该节点删除即可,但是,由于需要料理后事,所以想要做好删除操作,需要处理好“托孤”操作。

非递归实现 ★

  情况3走情况1或者情况2的逻辑都是可以的,所以不用管,主要是情况4的第二种子情况(删除和后继不相邻),需要处理好该情况下要删除节点的后任的托孤(利用后继节点不会有左孩子,如果有左孩子,那就不会是最小值,所以后继不可能有左孩子),再将后继节点托孤给被删除节点的父节点,代码如下:

/*** <h3>根据关键字删除</h3>* 非递归实现** @param key 关键字* @return 被删除关键字对应值*/
public Object remove(int key) {BSTNode p = root;// 记录待删除节点的父亲BSTNode parent = null;while (p != null) {if (key < p.key) {parent = p;p = p.left;} else if (p.key < key) {parent = p;p = p.right;} else {break;}}if (p == null) {return null;}// 删除操作if (p.left == null) {shift(parent, p, p.right); // 情况1} else if (p.right == null) {shift(parent, p, p.left); // 情况2} else {// 情况4// 4.1 被删除节点找后继BSTNode s = p.right;BSTNode sParent = p; // 后继父亲while (s.left != null) {sParent = s;s = s.left;}// 4.2 删除和后继不相邻, 处理后继的后事if (sParent != p) {                shift(sParent, s, s.right); // 不可能有左孩子(否则就不是后继节点)s.right = p.right;}// 4.3 后继取代被删除节点shift(parent, p, s);s.left = p.left;}return p.value;
}/*** 托孤方法** @param parent  被删除节点的父亲* @param deleted 被删除节点* @param child   被顶上去的节点*/
// 只考虑让 n1父亲的左或右孩子指向 n2, n1自己的左或右孩子并未在方法内改变
private void shift(BSTNode parent, BSTNode deleted, BSTNode child) {if (parent == null) {// 根节点(特殊情况)root = child;} else if (deleted == parent.left) {parent.left = child;} else {parent.right = child;}
}

  测试代码:

    public static void main(String[] args) {/*4/   \2     7/ \   /1   3 6/5*/BSTNode n1 = new BSTNode(1, 1);BSTNode n3 = new BSTNode(3, 3);BSTNode n2 = new BSTNode(2, 2, n1, n3);BSTNode n5 = new BSTNode(5, 5);BSTNode n6 = new BSTNode(6, 6, n5, null);BSTNode n7 = new BSTNode(7, 7, n6, null);BSTNode root1 = new BSTNode(4, 4, n2, n7);BSTTree tree1 = new BSTTree();tree1.root = root1;Object delete = tree1.remove(7);System.out.println(delete + "\t" + 7); // 7     7// 删除后/*4/   \2     6/ \   /1   3 5*/BSTNode x1 = new BSTNode(1, 1);BSTNode x3 = new BSTNode(3, 3);BSTNode x2 = new BSTNode(2, 2, x1, x3);BSTNode x5 = new BSTNode(5, 5);BSTNode x6 = new BSTNode(6, 6, x5, null);BSTNode root2 = new BSTNode(4, 4, x2, x6);BSTTree tree2 = new BSTTree();tree2.root = root2;System.out.println(isSameTree(tree1.root,tree2.root)); // true}// 判断是否为同一树(key对应的value都相等)static boolean isSameTree(BSTNode tree1, BSTNode tree2) {if (tree1 == null && tree2 == null) {return true;}if (tree1 == null || tree2 == null) {return false;}if (tree1.key != tree2.key) {return false;}return isSameTree(tree1.left, tree2.left) && isSameTree(tree1.right, tree2.right);}

  测试结果如下,删除的元素符合预期,且删除后的树与预期结果的结构一致

	7	7true
递归实现

  由于递归实现效率低,且较难理解,主要是为了结合非递归实现提供思路,可以结合注释思考,不过多讲解,代码如下:

    /*** <h3>根据关键字删除</h3>* 递归实现** @param key 关键字* @return 被删除关键字对应值*/public Object remove(int key) {ArrayList<Object> result = new ArrayList<>(); // 保存被删除节点的值root = doRemove(root, key, result);return result.isEmpty() ? null : result.get(0);}/*** 私有 递归删除具体实现细节** @param node   起点* @param key    删除索引* @param result 保存被删除节点的值* @return node 删剩下的孩子(找到) 或 null(没找到)* @author LiaoYuXing-Ray 2024/1/25 19:59**/private BSTNode doRemove(BSTNode node, int key, ArrayList<Object> result) {if (node == null) {return null;}if (key < node.key) {node.left = doRemove(node.left, key, result);return node;}if (node.key < key) {node.right = doRemove(node.right, key, result);return node;}result.add(node.value);if (node.left == null) { // 情况1 - 只有右孩子return node.right;}if (node.right == null) { // 情况2 - 只有左孩子return node.left;}BSTNode s = node.right; // 情况3 - 有两个孩子while (s.left != null) {s = s.left;}s.right = doRemove(node.right, s.key, new ArrayList<>());s.left = node.left;return s;}

  说明

  1. ArrayList<Object> result 用来保存被删除节点的值
  2. 第二、第三个 if 对应没找到的情况,继续递归查找和删除,注意后续的 doRemove返回值代表删剩下的,因此需要更新
  3. 最后一个 return 对应删除节点只有一个孩子的情况,返回那个不为空的孩子,待删节点自己因没有返回而被删除
  4. 第四个 if 对应删除节点有两个孩子的情况,此时需要找到后继节点,并在待删除节点的右子树中删掉后继节点,最后用后继节点替代掉待删除节点返回,别忘了改变后继节点的左右指针

3 二叉搜索树——范围查询

             4/   \2     6/ \   / \1   3 5   7

3.1 找所有小于索引的值less(int key)

  less(int key)方法:找 < key 的所有 value

  使用中序遍历

    // 找 < key 的所有 valuepublic List<Object> less(int key) {ArrayList<Object> result = new ArrayList<>();BSTNode p = root;LinkedList<BSTNode> stack = new LinkedList<>();while (p != null || !stack.isEmpty()) {if (p != null) {stack.push(p);p = p.left;} else {BSTNode pop = stack.pop();// 处理值if (pop.key < key) {result.add(pop.value);} else {break;}p = pop.right;}}return result;}

3.2 找所有大于索引的值greater(int key)

  greater(int key)方法:找 > key 的所有 value

  使用常规的中序遍历(从左向右的中序遍历)

public List<Object> greater(int key) {ArrayList<Object> result = new ArrayList<>();BSTNode p = root;LinkedList<BSTNode> stack = new LinkedList<>();while (p != null || !stack.isEmpty()) {if (p != null) {stack.push(p);p = p.left;} else {BSTNode pop = stack.pop();if (pop.key > key) {result.add(pop.value);}p = pop.right;}}return result;
}

  中序遍历效率不高,可以用 RNL 遍历(中序遍历的逆操作)

  注:

  • Pre-order, NLR
  • In-order, LNR
  • Post-order, LRN
  • Reverse pre-order, NRL(反向前序遍历,值右左)
  • Reverse in-order, RNL(反向中序遍历,右值左)
  • Reverse post-order, RLN(反向后续序遍历,右左值)

  RNL 遍历代码如下:

// 找 > key 的所有 value
public List<Object> greater(int key) {ArrayList<Object> result = new ArrayList<>();BSTNode p = root;LinkedList<BSTNode> stack = new LinkedList<>();while (p != null || !stack.isEmpty()) {if (p != null) {stack.push(p);p = p.right;} else {BSTNode pop = stack.pop();if (pop.key > key) {result.add(pop.value);} else {break;}p = pop.left;}}return result;
}

3.3 找范围值between(int key1, int key2)

  between(int key1, int key2)方法:找 >= key1 且 <= key2 的所有值

    // 找 >= key1 且 <= key2 的所有值public List<Object> between(int key1, int key2) {ArrayList<Object> result = new ArrayList<>();BSTNode p = root;LinkedList<BSTNode> stack = new LinkedList<>();while (p != null || !stack.isEmpty()) {if (p != null) {stack.push(p);p = p.left;} else {BSTNode pop = stack.pop();// 处理值if (pop.key >= key1 && pop.key <= key2) {result.add(pop.value);} else if (pop.key > key2) {break;}p = pop.right;}}return result;}

  测试:

    public static void main(String[] args) {/*4/   \2     6/ \   / \1   3 5   7*/BSTNode n1 = new BSTNode(1, 1);BSTNode n3 = new BSTNode(3, 3);BSTNode n2 = new BSTNode(2, 2, n1, n3);BSTNode n5 = new BSTNode(5, 5);BSTNode n7 = new BSTNode(7, 7);BSTNode n6 = new BSTNode(6, 6, n5, n7);BSTNode root = new BSTNode(4, 4, n2, n6);BSTTree tree = new BSTTree();tree.root = root;System.out.println(tree.less(6));System.out.println(tree.greater(6));System.out.println(tree.between(3,5));}

  输出如下,符合预期

	[1, 2, 3, 4, 5][7][3, 4, 5]

3.3 小结

优点:

  1. 如果每个节点的左子树和右子树的大小差距不超过一,可以保证搜索操作的时间复杂度是 O(log n),效率高。
  2. 插入、删除结点等操作也比较容易实现,效率也比较高。
  3. 对于有序数据的查询和处理,二叉查找树非常适用,可以使用中序遍历得到有序序列。

缺点:

  1. 如果输入的数据是有序或者近似有序的,就会出现极度不平衡的情况,可能导致搜索效率下降,时间复杂度退化成O(n)。
  2. 对于频繁地插入、删除操作,需要维护平衡二叉查找树,例如红黑树、AVL 树等,否则搜索效率也会下降。
  3. 对于存在大量重复数据的情况,需要做相应的处理,否则会导致树的深度增加,搜索效率下降。
  4. 对于结点过多的情况,由于树的空间开销较大,可能导致内存消耗过大,不适合对内存要求高的场景。



本文是博主的粗浅理解,可能存在一些错误或不完善之处,如有遗漏或错误欢迎各位补充,谢谢

  如果觉得这篇文章对您有所帮助的话,请动动小手点波关注💗,你的点赞👍收藏⭐️转发🔗评论📝都是对博主最好的支持~


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

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

相关文章

Django从入门到精通(三)

目录 七、ORM操作 7.1、表结构 常见字段 参数 示例 7.2、表关系 一对多 多对多 第一种方式 第二种方式 7.3、连接MYSQL 7.4、数据库连接池 7.5、多数据库 读写分离 分库&#xff08;多个app ->多数据库&#xff09; 分库&#xff08;单app&#xff09; 注意…

狗东云搭建幻兽帕鲁(奶妈级别)

使用狗东云搭建幻兽帕鲁 同配置狗东云比腾讯云便宜&#xff0c;2核2G服务器仅50元1年&#xff0c;4核8G服务器458元1年&#xff0c;点击链接直达. 进入页面会跳转到注册&#xff0c;先注册账户&#xff0c;注册好后页面跳转&#xff0c;没有跳转点这里&#xff0c;选择页面左侧…

[AIGC 大数据基础] 浅谈hdfs

HDFS介绍 什么是HDFS&#xff1f; HDFS&#xff08;Hadoop Distributed File System&#xff09;是Apache Hadoop生态系统的一部分&#xff0c;是一个分布式文件系统。它被设计用于存储和处理大规模数据集&#xff0c;并且能够容错、高可靠和高性能地处理文件。 HDFS是为了支…

2024转行程序员的请注意:均月薪在40-70k

前言 2023年&#xff0c;对大多数行业来说都是不太好过的一年。 对程序员来说也是如此&#xff0c;很多粉丝朋友都在说android工作特别难找&#xff0c;一个岗位都是几千份简历........大家心里都是特别的焦虑&#xff0c;本以为2024年就业情况会有好转&#xff0c;但实际上并…

PHP - Yii2 异步队列

1. 前言使用场景 在 PHP Yii2 中&#xff0c;队列是一种特殊的数据结构&#xff0c;用于处理和管理后台任务。队列允许我们将耗时的任务&#xff08;如发送电子邮件、push通知等&#xff09;放入队列中&#xff0c;然后在后台异步执行。这样可以避免在处理大量请求时阻塞主应用…

[GXYCTF2019]BabySQli1

单引号闭合&#xff0c;列数为三列&#xff0c;但是没有期待的1 2 3回显&#xff0c;而是显示wrong pass。 尝试报错注入时发现过滤了圆括号&#xff0c;网上搜索似乎也没找到能绕过使用圆括号的方法&#xff0c;那么按以往爆库爆表爆字段的方法似乎无法使用了 在响应报文找到一…

ORM-07-querydsl 入门介绍

拓展阅读 The jdbc pool for java.(java 手写 jdbc 数据库连接池实现) The simple mybatis.&#xff08;手写简易版 mybatis&#xff09; 1. 介绍 1.1 背景 Querydsl的诞生源于以类型安全的方式维护HQL查询的需求。逐步构建HQL查询需要进行字符串连接&#xff0c;导致代码难以…

32个Java面试必考点-06常用工具集

本课时主要介绍常用的工具&#xff0c;将会讲解三个知识点&#xff1a; & JVM 相关工具的作用和适用场景&#xff1b; & Git 常用命令和工作流&#xff1b; & Linux 系统中常用分析工具。 常用工具汇总 常用工具汇总如下图所示。 说明&#xff1a;这里列出的都…

k8s的图形化工具--rancher

什么是rancher&#xff1f; rancher是一个开源的企业级多集群的k8s管理平台 rancher和k8s的区别 都是为了容器的调度和编排系统&#xff0c;但是rancher不仅能够调度&#xff0c;还能管理k8s集群&#xff0c;自带监控&#xff08;普罗米修斯&#xff09; 实验部署 实验架构…

电容主要特点和作用,不同类型的电容区别

电容 两个相互靠近的金属板中间夹一层绝缘介质组成的器件&#xff0c;当两端存在电势差时&#xff0c;由于介质阻碍了电荷移动而累积在金属板上&#xff0c;衡量金属板上储存电荷的能力称之为电容&#xff0c;相应的器件称为电容器。 电容&#xff08;Capacitance&#xff09…

移动端 h5-table react版本支持虚拟列表

介绍 适用于 react ts 的 h5 移动端项目 table 组件 github 链接 &#xff1a;https://github.com/duKD/react-h5-table 有帮助的话 给个小星星 有两种表格组件 常规的&#xff1a; 支持 左侧固定 滑动 每行点击回调 支持 指定列排序 支持滚动加载更多 效果和之前写的vue…

【开源】基于JAVA的实验室耗材管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 耗材档案模块2.2 耗材入库模块2.3 耗材出库模块2.4 耗材申请模块2.5 耗材审核模块 三、系统展示四、核心代码4.1 查询耗材品类4.2 查询资产出库清单4.3 资产出库4.4 查询入库单4.5 资产入库 五、免责说明 一、摘要 1.1…

四、Kotlin 表达式

1. 常量 & 变量 1.1 可读写变量&#xff08;var&#xff09; var x initValue // x 称为可读写变量注意&#xff1a;当 var 声明的变量做成员属性时&#xff0c;默认提供 setter/getter 方法。 1.2 只读变量&#xff08;val&#xff09; val x initValue // x 称为只…

FPGA:我的零基础学习路线(2022秋招已上岸)持续更新中~

可内推简历&#xff0c;丝我即可 前言 初次接触FPGA是在2022年3月左右&#xff0c;正处在研二下学期&#xff0c;面临着暑假找工作&#xff0c;周围的同学大多选择了互联网&#xff0c;出于对互联网的裁员形势下&#xff0c;我选择了FPGA&#xff0c;对于硬件基础知识我几乎是…

Vue+OpenLayers7入门到实战:鹰眼控件简单介绍,并使用OpenLayers7在地图上添加鹰眼控件

返回《Vue+OpenLayers7》专栏目录:Vue+OpenLayers7 前言 本章介绍OpenLayers7添加鹰眼控件到地图上的功能。 在OpenLayers中,想要实现鹰眼控件,必须要新建一个数据源,且不能跟其他图层混用,相当于鹰眼是一个单独图层。 补充知识,鹰眼控件是什么? 鹰眼控件是一种在地…

大数据平台红蓝对抗 - 磨利刃,淬精兵!

背景 目前大促备战常见备战工作&#xff1a;专项压测&#xff08;全链路压测、内部压测&#xff09;、灾备演练、降级演练、限流、巡检&#xff08;监控、应用健康度&#xff09;、混沌演练&#xff08;红蓝对抗&#xff09;&#xff0c;如下图所示。随着平台业务越来越复杂&a…

OpenCV功能特性和依赖关系

有许多可选的依赖项和功能可以打开或关闭。 CMake 具有特殊选项&#xff0c;允许打印所有可用的配置参数&#xff1a; cmake -LH ../opencv 选项命名约定 有三种选项用于控制库的依赖项&#xff0c;它们具有不同的前缀&#xff1a; 以启用或禁用依赖项开头的选项WITH_ 从启…

输入框限制输入两位小数 输入金额限制 双向绑定输入框能继续输入但是变量的数据不变解决方案 input 保留两位小数

移动端项目 需求是 输入框只能输入1000以内的数字保留两位小数 开发中发现 用vue开发双向绑定 不管是用value还是v-model 在输入时用input监听输入框变化 校验是否匹配 当不匹配是修改绑定的变量 inputValue时 打印inputValue符合预期 但是input输入框中还是原来输入的值 没有…

RocketMQ源码阅读-八-定时消息和消息重试

RocketMQ源码阅读-八-定时消息和消息重试 定时消息概念逻辑流程图延迟级别Producer发送定时消息Broker存储定时消息Broker发送定时消息Broker 持久化定时发送进度 消息重试总结 定时消息 概念 官网给出的概念&#xff1a;https://rocketmq.apache.org/zh/docs/featureBehavior…

树莓派无显示屏连接

终端命令控制树莓派关机 1&#xff1a;用网线连接树莓派 按照正常的步骤 &#xff0c;搜索控制面板&#xff0c;网络和internet&#xff0c;网络和共享中心&#xff0c;更改适配器设置&#xff0c;右键WIFI&#xff0c;点击属性&#xff0c;点击共享&#xff0c;打勾允许即可&…