数据结构之树-第二篇
1、此时,将元素30从队首拿出来,进行访问,之后将30的左孩子29、右孩子42入队,那么此时队首元素就是13了。
此时,将将元素13从队首拿出来,进行访问,之后将13的左孩子、右孩子入队,由于13是叶子节点没有左右孩子,所以没有元素入队了。
此时,看队首元素是22,将元素22从队首拿出来,进行访问,之后将22的左孩子、右孩子入队,由于22是叶子节点没有左右孩子,所以没有元素入队了。
此时,看队首元素是29,将元素29从队首拿出来,进行访问,之后将29的左孩子、右孩子入队,由于29是叶子节点没有左右孩子,所以没有元素入队了。
此时,看队首元素是42,将元素42从队首拿出来,进行访问,之后将42的左孩子、右孩子入队,由于42是叶子节点没有左右孩子,所以没有元素入队了。
最后,查看队列中队首是谁,现在整个队列都为空了,没有队首元素,说明没有任何元素排队了,那么,广度优先遍历也就是我们的层序遍历就结束了。
2、删除二分搜索树的最小值,对于下面这个二分搜索树,最小值就是一个叶子节点13,在这种情况下,非常的简单,直接将这个叶子节点删除即可,对于整棵二分搜索树,不需要改变任何结构。
同理,此时这个二分搜索树的最小值也是一个叶子节点15,在这种情况下,非常的简单,直接将这个叶子节点删除掉即可,对于整棵二分搜索树,不需要改变任何结构。
但是,复杂的是,在这种情况下,此时在这种二分搜索树中最小值是22,但是此时22这个节点不是叶子节点,22这个节点向左走再也走不动了,但是22这个节点有右子树,在这种情况下,只需要将22这个节点删除,将它的整个右子树都变成是41的左子树,就完成了这个删除操作。
同理,删除二分搜索树的最大值,如果是叶子节点直接删除掉即可,对于整棵二分搜索树,不需要改变任何结构,同理,但是如果不是叶子节点,它向右走再也走不动了,但是58这个节点有左子树,在这种情况下,只需要删除58这个节点,然后将它的整个左子树都变成是41的右子树,就完成了这个删除操作。
3、如果删除的节点只有左孩子的节点,在逻辑上和删除最大节点的逻辑是一致的,注意,只有左孩子的节点不一定是最大值所在的节点,删除满足这种特性的节点,删除方式就是将该节点删除之后,将这个节点的左孩子所在的这棵二叉树,也就是左子树取代被删除的节点的位置,连到原来这个节点的父亲节点的右孩子节点上。比如58这个节点,将58节点删除之后,将58的左子树挂到58这个位置。
如果删除的只有右孩子的节点,比如58这个节点,只有右孩子,在逻辑上和删除最小值的逻辑是一致的,注意,只有右孩子的节点不一定是最小值所在的节点,删除满足这种特性的节点,删除方式就是将该节点删除之后,将这个节点的右孩子所在的这棵二叉树,连接到被删除节点的这个位置上。
二分搜索树中删除节点真正难的地方是删除左右还有孩子的节点,比如删除58这个节点如何删除。删除左右都有孩子的节点d,将58这个节点暂时起名为d,这个d节点既有左孩子又有右孩子,在这种情况下,如何删除呢,此时需要将d节点的左右子树融合起来,那么如何进行融合呢,对于这个d节点,既有左子树,又有右子树,根据二分搜索树的定义,d节点的左子树的所有节点元素都小于d节点元素,d节点的右子树的所有节点元素都大于d节点元素,此时,将d节点删除掉了,需要找到一个节点替代58这个节点的位置,如何进行找呢,此时,找到的58这个节点的后继,也就是,在58这个节点的左右子树中离58最近饿,比58还要大的这个节点,其实就是59,根据58的右子树中所有对应的那个最小值的节点,这也很好理解,58的右子树中所有的元素最小值的那个节点,因为根据二分搜索树,58右子树中所有元素都比58大,其中最小的那个元素就是比58大,离58最近的那个元素,这里面的最近,不是位置近,是大小的意思。
此时,让s的左子树等于d的左子树,s -> node.left = d -> node.left;最后,删除d节点,s是新的子树的根节点。 4、数据结构之树,代码实现,如下所示。
1 package com.tree; 2 3 4 import java.util.*; 5 6 /** 7 * 1、二分搜索树,二分搜索树存储的内容支持泛型,但是不是支持所有的类型,应该对类型有一个限制, 8 * 这个限制是这个类型必须拥有可比较性,对泛型E进行限制,这个限制就是继承Comparable<E>接口, 9 * 这个类型E必须具有可比较性。 10 * <p> 11 * <p> 12 * 2、对于前序、中序、后序遍历来说,不管是递归写法还是非递归写法,对于我们这棵二分搜索树来说, 13 * 我们都是在遍历的过程中一扎到底,这样的一种遍历方式,其实还有另外一个名字,叫做,深度优先遍历。 14 * 对于这棵树来说,都会先来到这棵树最深的地方,直到不能再深了,才开始返回回去, 15 * 这种遍历叫做深度优先遍历。和深度优先遍历相对的是另外一种遍历方式,叫做广度优先遍历, 16 * 广度优先遍历遍历出来的结果,这个顺序其实是整个二分搜索树的层序遍历的顺序。 17 */ 18 public class BinarySearchTree<E extends Comparable<E>> { 19 20 // 二分搜索树的节点类,私有内部类。 21 private class Node { 22 private E e;// 存储元素e; 23 private Node left;// 指向左子树,指向左孩子。 24 private Node right;// 指向右子树,指向右孩子。 25 26 /** 27 * 含参构造函数 28 * 29 * @param e 30 */ 31 public Node(E e) { 32 this.e = e;// 用户存储的元素e。 33 left = null;// 左孩子初始化为空。 34 right = null;// 右孩子初始化为空。 35 } 36 } 37 38 private Node root;// 根节点 39 private int size;// 二分搜索树存储了多少个元素 40 41 /** 42 * 无参构造函数,和默认构造函数做的事情一样的。 43 */ 44 public BinarySearchTree() { 45 // 初始化的时候,二分搜索树一个元素都没有存储 46 root = null; 47 size = 0;// 大小初始化为0 48 } 49 50 /** 51 * 返回二分搜索树的含有多少个元素 52 * 53 * @return 54 */ 55 public int size() { 56 // 二分搜索树的大小 57 return size; 58 } 59 60 /** 61 * 判断二分搜索树是否为空。 62 * 63 * @return 64 */ 65 public boolean isEmpty() { 66 return size == 0; 67 } 68 69 // /** 70 // * 向二分搜索树中添加新的元素e. 71 // * <p> 72 // * 在公开的二分搜索树的调用的add方法。在将递归调用中, 是将新的元素e作为node子节点插入进去的。 73 // * 这个时候,形成了一个逻辑上的不统一,递归函数中,e元素和node.e元素进行了两轮比较,第一轮比较, 74 // * 在比较完他们的大小的同时,还要看一下node的左右两边是不是为空,如果为空直接插入, 75 // * 对于第二轮比较,我们知道我们不能直接作为Node的孩子插入这个元素e,只要再次递归的调用add的函数。 76 // * 77 // * @param e 78 // */ 79 // public void add(E e) { 80 // // 判断,如果根节点是空的时候,直接指向新创建的节点即可 81 // if (root == null) { 82 // // 对根节点进行特殊的处理,如果根为空的时候,就直接创建一个新的节点。 83 // root = new Node(e); 84 // // 维护size 85 // size++; 86 // } else { 87 // // 否则,根节点不是空的时候。从根节点开始添加一个元素。 88 // // 注意,由于需要递归的调用,对于每一个节点的左子树也是一个更小的二分搜索树的根节点, 89 // // 对于每一个节点的右子树也是一个更小的二分搜索树的根节点。 90 // // 所以在递归的过程中,创建一个新的递归的函数。 91 // 92 // // 如果根不为空,尝试从根节点开始插入元素e。 93 // add(root, e); 94 // } 95 // 96 // } 97 // 98 // /** 99 // * 私有的递归添加方法 100 // * <p> 101 // * 整体上是,向以node为根的二分搜索树中插入元素E,递归算法。 102 // * <p> 103 // * 之所以设置Node,就是因为在插入的过程中,我们要不断地转换到新的更小的二分搜索树中, 104 // * 去找到这个新的元素e真正应该插入的位置,那么相应的,我们在递归调用的过程中, 105 // * 相当于二分搜索树的根在逐渐变化的,所以,我们需要靠这个参数来体现这个变化。 106 // * 107 // * @param node Node结构本身都是对用户屏蔽的,用户不需要了解二分搜索树中节点的结构。 108 // * @param e 109 // */ 110 // private void add(Node node, E e) { 111 // // 精髓,将新的元素e插入给node的左孩子还是node的右孩子。 112 // // 对当前的node来说,如果想插入到左边,但是左孩子不为空的话,再递归的将新的元素e插入到add(node.left, e);node的左子树。 113 // // 如果想插入到右边,但是右孩子不为空的话,再递归的将新的元素e插入到add(node.right, e);node的右子树。 114 // 115 // 116 // // 第一部分,递归终止的条件。 117 // // 先检查一下元素e,是不是等于node的元素e。 118 // if (e.equals(node.e)) { 119 // // 如果相等,说明要插入的元素已经存在于二分搜索树中了。 120 // return; 121 // // 主要的插入代码,就是下面的判断,然后将元素e进行插入到左子树还是右子树的操作。 122 // } else if (e.compareTo(node.e) < 0 && node.left == null) { 123 // // 如果待插入元素e小于node的元素e,则把元素e插入到node的左子树上面。 124 // // 由于元素E满足Comparable<E>类型的,所以应该是使用compareTo方法比较,不是基础数据类型,不能使用大于号小于号比较。 125 // 126 // // 如果此时node的左子树又等于空,直接创建一个Node,将元素存储到Node里面即可。 127 // // 此时元素e就变成了node的左孩子了。 128 // node.left = new Node(e); 129 // // 维护size的大小。 130 // size++; 131 // // return表示此次插入操作也完成了。 132 // return; 133 // } else if (e.compareTo(node.e) > 0 && node.right == null) { 134 // // 如果待插入元素e大于node节点的元素e,则把元素e插入到node的右子树上面。 135 // // 此时,如果node的右孩子又等于空,直接创建一个Node,将元素存储到Node即可。 136 // // 此时,元素就变成了node的右孩子了。 137 // node.right = new Node(e); 138 // // 维护size的大小。 139 // size++; 140 // // return表示此次插入操作也完成了。 141 // return; 142 // } 143 // 144 // 145 // // 递归的第二部分。递归调用的逻辑。 146 // if (e.compareTo(node.e) < 0) { 147 // // 如果待插入元素e小于node的元素e,递归调用add方法,参数一是向左子树添加左孩子。 148 // // 向左子树添加元素e。 149 // add(node.left, e); 150 // } else if (e.compareTo(node.e) > 0) { 151 // // 如果待插入元素e大于node的元素e,递归调用add方法,参数一是向右子树添加右孩子。 152 // // 向右子树添加元素e。 153 // add(node.right, e); 154 // } 155 // } 156 157 158 /** 159 * 向二分搜索树中添加新的元素e。 160 * 161 * @param e 162 */ 163 public void add(E e) { 164 // 此时,不需要对root为空进行特殊判断。 165 // 向root中插入元素e。如果root为空的话,直接返回一个新的节点,将元素存储到该新的节点里面。 166 root = add(root, e); 167 } 168 169 /** 170 * 深入理解递归终止条件。 171 * <p> 172 * 返回插入新节点后二叉搜索树的根。 173 * <p> 174 * 向以node为根的二分搜索树中插入元素e,递归算法。 175 * <p> 176 * <p> 177 * 对于新的add递归函数,将向以node为根的二分搜索树中插入元素e,并且返回在做完这个操作以后这个二分搜索树的根节点, 178 * 如果我们传入的这个node为空的话,插入新的元素以后,就产生新的根节点,就是创建了一个Node e, 179 * 否则的话,我们来比较我们待插入的元素e和当前的node e的大小关系,如果小于零的话,向左子树添加一个左孩子, 180 * 如果大于零的话,向右子树添加一个右孩子。如果等于零的话,什么都不操作。 181 * 不管向左子树添加元素还是向右子树添加元素,插入完成之后,都将当前node的左孩子和右孩子进行重新赋值, 182 * 在重新赋值以后,这个node依赖是这个以node为根的二分搜索树。相应的根节点将他返回回去。 183 * <p> 184 * 链表的递归插入和二分搜索树的添加具有高度相似性的,在二分搜索树中需要判断一下,是要插入到左子树还是右子树。 185 * 186 * @param node 187 * @param e 188 * @return 189 */ 190 private Node add(Node node, E e) { 191 // 空本身也是一种二分搜索树,空本身也是一颗二叉树,换句话说,如果走递归函数,走到node为空的时候 192 // 一定要创建一个Node节点。上面的add方法实现,其实还没有递归到底部。 193 194 // 精髓,如果待插入元素e小于node的元素e,此时不管node.left是不是为空,就再递归一层, 195 // 如果递归的这一层,node等于空的话,也就是,现在要新插入一个元素,插入到哪里,插入到空这颗 196 // 二叉树上,那么,很显然,这个位置本身就应该是这个节点。 197 198 199 // 如果按照上面的思想,如果此时node等于空的话,此时,肯定要插入一个节点。 200 if (node == null) { 201 // 维护size的大小。 202 size++; 203 // 如果此时,直接创建一个Node的话,没有和二叉树挂接起来。 204 // 如何让此节点挂接到二叉树上呢,直接将创建的节点return返回回去即可,返回给调用的上层。 205 return new Node(e); 206 } 207 208 // 递归的第二部分。递归调用的逻辑。 209 if (e.compareTo(node.e) < 0) { 210 // 如果待插入元素e小于node的元素e,递归调用add方法,参数一是向左子树添加左孩子。 211 // 向左子树添加元素e。 212 // 向左子树添加元素e的时候,为了让整颗二叉树发生改变,在node的左子树中插入元素e, 213 // 插入的结果,有可能是变化的,所以就要让node的左子树连接住这个变化。 214 215 // 注意,如果此时,node.left是空的话,这次add操作相应的就会返回一个新的Node节点, 216 // 对于这个新的节点,我们的node.left被赋值这个新的节点,相当于我们就改变了整棵二叉树。 217 node.left = add(node.left, e); 218 } else if (e.compareTo(node.e) > 0) { 219 // 如果待插入元素e大于node的元素e,递归调用add方法,参数一是向右子树添加右孩子。 220 // 向右子树添加元素e。 221 node.right = add(node.right, e); 222 } 223 224 // 注意,如果如果待插入元素e等于node的元素e,不进行任何操作。 225 226 // 最后将以node为根的二叉树返回回去。 227 // 不管在第二部分产生了什么变化,如果我们的node不为空的话,我们进去了第二部分, 228 // 向以node为根的二分搜索树中插入元素e之后,最终,插入了这个节点以后, 229 // 二分搜索树的根呢,还是这个node。 230 return node; 231 } 232 233 234 /** 235 * 查看二分搜索树中是否包含元素e 236 * 237 * @param e 238 * @return 239 */ 240 public boolean contains(E e) { 241 // 递归实现,递归的过程中,就要从这个二分搜索树的根节点开始,逐渐的转移在新的二分搜索树的子树中, 242 // 缩小问题规模,缩小查询的树的规模,直到发现找到这个元素e或者找不到这个元素e。 243 244 // 整体是看以我们这整棵二分搜索树为根的二分搜索树中是否包含元素e。 245 return contains(root, e); 246 } 247 248 /** 249 * 看以node为根的二分搜索树中是否包含元素e,递归算法 250 * <p> 251 * 二分搜索树查询元素,对于查询元素来说,我们只要不停的看节点node里面保存的元素就好了。 252 * 不牵扯到二分搜索树,添加一个元素之后,又如何把它挂接到整个二分搜索树中。 253 * 254 * @param node 节点Node 255 * @param e 带查询的元素e 256 * @return 257 */ 258 private boolean contains(Node node, E e) { 259 // 递归的第一部分 260 if (node == null) { 261 // 如果node节点为空,是不包含元素e的,此时直接返回false 262 return false; 263 } 264 265 // 递归的第二部分,开始逻辑判断 266 if (e.compareTo(node.e) == 0) { 267 // 如果待查询元素e和node的元素e相等 268 return true; 269 } else if (e.compareTo(node.e) < 0) { 270 // 如果待查询元素e小于node的元素e,如果此二分搜索树中还包含元素e的话,那么只可能在node的左子树 271 return contains(node.left, e); 272 } else { 273 // 如果待查询元素e大于node的元素e,如果此二分搜索树中还包含元素e的话,那么只可能在node的右子树 274 return contains(node.right, e); 275 } 276 } 277 278 279 /** 280 * 二分搜索树的前序遍历。二分搜索树的遍历操作,就是把所有节点都访问一遍。 281 * 对于二分搜索树的遍历操作来说,两棵子树都要顾及,区别于添加或者查询是否包含, 282 * 只走左子树或者右子树。 283 * <p> 284 * <p> 285 * 二分搜索树的前序遍历。为什么这种遍历称为前序遍历呢,是因为先访问这个节点, 286 * 再访问左右子树,也就是说,访问这个节点放在了访问左右子树的前面,所以叫做前序遍历。 287 * 基于此,也就有了二叉树的中序遍历和后序遍历。 288 */ 289 public void preOrder() { 290 // 需要指定递归的调用,指定是那一棵二叉树进行的前序遍历。 291 292 // 用户的初始的调用,只需要对root根节点调用递归的preOrder就行了。 293 preOrder(root); 294 } 295 296 /** 297 * 前序遍历以node为根的二分搜索树,递归算法。 298 * <p> 299 * 前序遍历思路。 300 * 首先先遍历这个节点,再遍历这个节点的左子树,之后遍历这个节点的右子树。 301 * <p> 302 * 前序遍历是最自然的遍历方式,同时也是最常用的遍历方式。如果没有特殊情况下,在大多数情况下都是使用前序遍历。 303 * 304 * @param node 305 */ 306 private void preOrder(Node node) { 307 // 递归的第一部分 308 if (node == null) { 309 // 如果node为空的话,直接返回 310 return; 311 } 312 313 // 递归的第二部分,开始遍历 314 System.out.println(node.e); 315 // 递归的调用根节点root的左子树。 316 preOrder(node.left); // 此处是循环遍历左孩子,直到左孩子为空,直接返回。 317 // 递归的调用根节点root的右子树。 318 preOrder(node.right); // 执行完上面循环遍历左孩子,执行下面的循环遍历右孩子,直到右孩子是空,直接返回。 319 // // 此时,完成了二分搜索树的前序遍历。 320 321 322 // 升级版递归过程 323 // if (node != null) { 324 // // 递归的第二部分,开始遍历 325 // System.out.println(node.e); 326 // // 递归的调用根节点root的左子树。 327 // preOrder(node.left); 328 // // 递归的调用根节点root的右子树。 329 // preOrder(node.right); 330 // // 此时,完成了二分搜索树的前序遍历。 331 // } 332 } 333 334 335 /** 336 * 二分搜索树的的前序遍历的非递归方式实现。 337 */ 338 public void preOrderNonRecursive() { 339 // 栈的作用是记录下面要到底要访问那些节点,记录下一次依次要访问那些节点。 340 // 泛型是Node类型的,存储的二叉树节点类型的对象。 341 Stack<Node> stack = new Stack<Node>(); 342 // 初始的时候将root根节点push进去 343 stack.push(root); 344 345 // 进行循环操作,只要stack.isEmpty()为false,说明了栈里面有元素。 346 // !stack.isEmpty()说明了记录了下面要访问那个节点。 347 while (!stack.isEmpty()) { 348 // 只要栈不为空,说明记录了下面要访问那个节点。 349 // 就要进行访问这个节点。 350 351 // 当前访问的节点。 352 Node current = stack.pop();//将当前的栈顶元素拿出来。 353 // 此时current就是当前要访问的节点。当前要访问的节点直接打印即可。 354 System.out.println(current.e); 355 356 357 // 访问了当前节点之后,就要依次访问当前节点的左子树,右子树。 358 // 由于栈是后入先出的,所以先将当前节点的右子树push进去。 359 // 此时应该判断当前节点的右子树是否为空,如果为空,就不要压入栈了。 360 if (current.right != null) { 361 stack.push(current.right);// 右子树压入栈 362 } 363 // 此时应该判断当前节点的左子树是否为空,如果为空,就不要压入栈了。 364 if (current.left != null) { 365 stack.push(current.left);//左子树压入栈 366 } 367 } 368 369 } 370 371 372 /** 373 * 二分搜索树的中序遍历。 374 * <p> 375 * 二分搜索树的的中序遍历,先访问这个节点的左子树,再访问这个节点,再访问这个节点的右子树。 376 * 中序遍历的中就体现在访问该节点放在遍历这个节点的左子树和右子树的中间。 377 */ 378 public void inOrder() { 379 inOrder(root); 380 } 381 382 /** 383 * 中序遍历以node为根的二分搜索树,递归算法。 384 * <p> 385 * <p> 386 * 二分搜索树的中序遍历结果是顺序的。有时候,因为这个功能,二分搜索树也叫做排序树。 387 * 388 * @param node 389 */ 390 private void inOrder(Node node) { 391 // 递归的第一部分,递归终止的条件 392 if (node == null) { 393 // 如果node为空的话,直接返回 394 return; 395 } 396 397 // 递归的第二部分,开始遍历 398 // 递归的调用根节点root的左子树。 399 inOrder(node.left); // 此处是循环遍历左孩子,直到左孩子为空,直接返回。 400 // 此程序就是简单的打印node节点的元素。就是访问这个节点元素的过程。 401 System.out.println(node.e); 402 // 递归的调用根节点root的右子树。 403 inOrder(node.right); // 执行完上面循环遍历左孩子,执行下面的循环遍历右孩子,直到右孩子是空,直接返回。 404 // 此时,完成了二分搜索树的中序遍历。 405 } 406 407 408 /** 409 * 后续遍历,二分搜索树的后序遍历。 410 */ 411 public void postOrder() { 412 postOrder(root); 413 } 414 415 /** 416 * 二分搜索树的的后序遍历。 417 * <p> 418 * 二分搜索树的的后序遍历,先访问这个节点的左子树,再访问这个节点的右子树,最后再访问这个节点。 419 * <p> 420 * 后续遍历必须先处理完左子树,再处理完右子树,最后处理这个节点。 421 * <p> 422 * 后续遍历的一个应用,为二分搜索树释放内存。先释放左右孩子的内存,最后释放这个节点的内存。 423 * 424 * @param node 425 */ 426 private void postOrder(Node node) { 427 // 递归的第一部分,递归终止的条件 428 if (node == null) { 429 // 如果node为空的话,直接返回 430 return; 431 } 432 433 // 递归的第二部分,开始遍历 434 // 递归的调用根节点root的左子树。 435 postOrder(node.left); // 此处是循环遍历左孩子,直到左孩子为空,直接返回。 436 // 递归的调用根节点root的右子树。 437 postOrder(node.right); // 执行完上面循环遍历左孩子,执行下面的循环遍历右孩子,直到右孩子是空,直接返回。 438 // 此程序就是简单的打印node节点的元素。就是访问这个节点元素的过程。 439 System.out.println(node.e); 440 // 此时,完成了二分搜索树的后序遍历。 441 } 442 443 444 /** 445 * 二分搜索树的层序遍历 446 * <p> 447 * <p> 448 * 相对于深度优先遍历,广度优先遍历最大的有点,可以更快的找到你想要查询的那个元素, 449 * 这样的区别,主要用于搜索策略上,而不是用于遍历这种操作上, 450 * 因为遍历需要将所有的元素都访问一遍,在这种情况下,深度优先遍历, 451 * 广度优先遍历是没有区别的。但是如果想要在一棵树中找到某个问题的解的话, 452 * 对于深度优先遍历来说,将从根节点一下子访问到树的最深的地方, 453 * 但是问题的解很可能在树的上面,所以深度优先遍历先估计左子树, 454 * 需要很长的时间才能访问到最上面,在这种情况下,广度优先遍历就很有意义了, 455 * 这种问题的模型,最常用于算法设计中的最短路径的。 456 */ 457 public void levelOrder() { 458 // 由于Queue是接口,这里使用链表的方式实现该Queue。 459 Queue<Node> queue = new LinkedList<Node>(); 460 // 将根节点添加到队列中 461 queue.add(root); 462 // 循环遍历,当队列不为空的时候,queue.isEmpty()结果为false表示队列不为空, 463 // 当!queue.isEmpty()表示队列不为空的时候,继续循环遍历。 464 while (!queue.isEmpty()) { 465 // 声明一个当前的节点current,也就是我们队列中的元素出队之后的那个元素, 466 // 就是我们当前要访问的这个元素。 467 Node current = queue.remove(); 468 // 对于当前要访问的这个元素,可以进行打印输出 469 System.out.println(current.e); 470 471 // 之后,根据当前访问的元素来看当前的这个节点左右孩子,如果有左孩子或者右孩子进行入队。 472 if (current.left != null) { 473 queue.add(current.left); 474 } 475 // 如果当前节点有右孩子,就将当前节点的右孩子进行入队操作。 476 if (current.right != null) { 477 queue.add(current.right); 478 } 479 } 480 481 } 482 483 484 /** 485 * 寻找二分搜索树的最小元素。 486 * <p> 487 * <p> 488 * 此时,使用递归的算法比写非递归的算法,要麻烦一点,因为这里相当于我们不停的只想左走, 489 * 根本不考虑每一个节点的它的右子树,或者是右孩子,那么此时呢,我们跟操作一个链表是没有区别的, 490 * 相当于我们在操作对于每一个节点的next就是node.left这样的一个链表,找它的尾节点而已。 491 * 492 * @return 493 */ 494 public E minimum() { 495 // 首先,判断二分搜索树中元素个数为零,说明二分搜索树中没有元素,就抛出异常。 496 if (size == 0) { 497 // 首先,判断二分搜索树中元素个数为零,就抛出异常。 498 throw new IllegalArgumentException("BinarySearchTree is Empty."); 499 } 500 501 // 调用递归的方式进行查询最小值 502 Node minimum = minimum(root); 503 // 此时将返回的最小节点的元素值e返回即可。 504 return minimum.e; 505 } 506 507 /** 508 * 返回以node为根的二分搜索树的最小值所在的节点。 509 * 510 * @param node 511 */ 512 private Node minimum(Node node) { 513 // 递归算法,第一个部分,递归终止的条件 514 // 如果向左走,走不动了,这个就是最左节点了,即最小值 515 if (node.left == null) { 516 return node; 517 } else { 518 // 递归算法,第二部分,即如果节点最左节点不为空 519 // 将这个节点的左孩子作为node传递进去,依次查看最左节点的值。 520 return minimum(node.left); 521 } 522 } 523 524 525 /** 526 * 寻找二分搜索树的最大元素。 527 * 528 * @return 529 */ 530 public E maximum() { 531 // 首先,判断二分搜索树中元素个数为零,说明二分搜索树中没有元素,就抛出异常。 532 if (size == 0) { 533 // 首先,判断二分搜索树中元素个数为零,就抛出异常。 534 throw new IllegalArgumentException("BinarySearchTree is Empty."); 535 } 536 537 // 调用递归的方式进行查询最大值 538 Node maximum = maximum(root); 539 // 此时将返回的最大节点的元素值e返回即可。 540 return maximum.e; 541 } 542 543 /** 544 * 返回以node为根的二分搜索树的最大值所在的节点。 545 * 546 * @param node 547 */ 548 private Node maximum(Node node) { 549 // 递归算法,第一个部分,递归终止的条件 550 // 如果向右走,走不动了,这个就是最右节点了,即最大值 551 if (node.right == null) { 552 return node; 553 } else { 554 // 递归算法,第二部分,即如果节点最右节点不为空 555 // 将这个节点的右孩子作为node传递进去,依次查看最右节点的值。 556 return maximum(node.right); 557 } 558 } 559 560 561 /** 562 * 从二分搜索树中删除最小值所在节点,返回最小值。 563 * 564 * @return 565 */ 566 public E removeMin() { 567 // 将删除的元素删除掉 568 E ret = minimum(); 569 570 // 设计删除最小值的逻辑。最小值所在的节点从二分搜索树中删除掉。 571 // 从根节点开始,尝试从root节点开始,删除最小值所在的节点。 572 573 // 将删除最小值以后的节点返回给root根节点。 574 // 换句话说,我们从root为根的二分搜索树删除掉了最小值, 575 // 之后又返回了删除掉最小值之后的那棵二分搜索树对应的根节点。 576 // 这个根节点就是新的root。 577 root = removeMin(root); 578 579 // 最后,将待删除元素删除掉就行了。 580 return ret; 581 } 582 583 /** 584 * 删除掉以node为根的二分搜索树中的最小节点。 585 * 返回删除节点后新的二分搜索树的根。 586 * <p> 587 * <p> 588 * 这个逻辑就是在这种情况下,删除node节点的左子树对应的最小值,删除掉之后,对于我们当前 589 * 这个以node为根的二分搜索树,在做完这些操作之后,它的根节点依然是node,将它返回回去。 590 * 591 * @param node 592 */ 593 private Node removeMin(Node node) { 594 // 递归调用,递归到底,终止条件。 595 // 就是节点左子树为空的时候,就是向左走,直到不能再走的时候,就终止条件 596 // 就是当前这个节点不能再向左走了,即当前这个节点就是最小值。 597 // 所在的节点,就是这个待删除的节点。 598 if (node.left == null) { 599 // 我们要删除当前这个节点,但是当前这个节点可能有右子树的,则右子树的部分不可以丢失掉。 600 // reightNode保存当前节点的右子树。 601 Node reightNode = node.right; 602 // 保存当前节点的右子树之后,就将当前节点的右子树置空就行了。 603 // 将当前节点的右子树和这个二分搜索树脱离关系。 604 node.right = null; 605 // 返回保存的右子树,为什么返回这个呢,因为这个函数做的事情就是删除掉以node为根的二分搜索树中的最小节点, 606 // 返回删除节点后新的二分搜索树的根,此时,最小节点就是node节点本身,将node删除掉之后, 607 // 新的二分搜索树的根就是node.right,就是node的右孩子,它就是node的右子树对应的那个根节点。 608 // 如果node的右孩子为空,没有关系,就说明node的右子树为空,一棵空树,它的根节点还是空。 609 610 // 维护size的大小。因为删除掉一个元素,所以要维护size的大小。 611 size--; 612 return reightNode; 613 // 这就是删除最小节点这个算法来说,递归到底的情况。 614 } 615 616 617 // 如果此时,没有递归到底,就说明还有左孩子。 618 // 就是进行递归操作的第二部分就行了。去删除掉这个当前节点node的左子树所对应的最小值。 619 // Node removeMin = removeMin(node.left); 620 // 删除掉以node为根的二分搜索树中的最小节点。 621 // 返回删除节点后新的二分搜索树的根。 622 // 这样才可以真正改变二分搜索树的结构。 623 // node.left = removeMin; 624 node.left = removeMin(node.left); 625 626 // 返回当前节点 627 return node; 628 } 629 630 631 /** 632 * 从二分搜索树中删除最大值所在节点,返回最大值。 633 * <p> 634 * 删除最大值,类比删除最小值的思路即可。 635 * 636 * @return 637 */ 638 public E removeMax() { 639 // 将删除的元素删除掉 640 E ret = maximum(); 641 642 root = removeMax(root); 643 644 // 最后,将待删除元素删除掉就行了。 645 return ret; 646 } 647 648 649 /** 650 * 删除掉以node为根的二分搜索树中的最大节点。 651 * 返回删除节点后新的二分搜索树的根。 652 * 653 * @param node 654 */ 655 private Node removeMax(Node node) { 656 // 当走到该节点最右孩子的时候,即最右孩子是空的时候,就走到了最底。 657 if (node.right == null) { 658 // 保存当前节点的左子树。因为对于当前节点,右子树已经为空了。 659 // 删除掉这个节点之后,这个节点的左子树的根节点就是新的根节点, 660 Node leftNode = node.left; 661 // 保存当前节点的左子树之后,就将当前节点的左子树置空就行了。 662 // 将当前节点的左子树和这个二分搜索树脱离关系。 663 node.left = null; 664 // 删除节点,维护size的大小 665 size--; 666 // 删除掉这个节点之后,那么,这个节点的左子树的根节点,就是新的根节点。 667 return leftNode; 668 } 669 670 // 如果递归没有到到底的话,我们做的事情就是去删除当前这个节点右子树中的最大值, 671 // 将最终的结果返回给当前的这个节点的右孩子。然后返回node节点。 672 node.right = removeMax(node.right); 673 674 // 返回当前节点 675 return node; 676 } 677 678 679 /** 680 * 从二分搜索树中删除元素为e的节点。 681 * 682 * @param e 683 */ 684 public void remove(E e) { 685 // 参数一是根节点,参数二是待删除元素 686 687 // 将删除掉元素e之后d得到的新的二分搜索树的根节点返回回来 688 root = remove(root, e); 689 } 690 691 /** 692 * 删除掉以node为根的二分搜索树中值为e的节点,递归算法。 693 * 返回删除节点后新的二分搜索树的根。 694 * <p> 695 * <p> 696 * 删除左右都有孩子的节点d,找到p = max(d -> node.left)。p是d的前驱。 697 * 此删除方法是找到待删除节点的后继,也可以找到当前节点的前驱。 698 * 699 * @param node 700 * @param e 701 */ 702 private Node remove(Node node, E e) { 703 // 如果node节点为空,直接返回空即可。 704 // 另外一层含义,在二分搜索树中找这个元素为e的节点,根本没有找到,最后找到node为空的位置了, 705 // 直接返回空就行了。 706 if (node == null) { 707 return null; 708 } 709 710 // 递归函数,开始近逻辑 711 // 如果待删除元素e和当前节点的元素e进行比较,如果待删除元素e小于该节点的元素e 712 if (e.compareTo(node.e) < 0) { 713 // 此时,去该节点的左子树,去找到待删除元素节点 714 // 递归调用,去node的左子树,去删除这个元素e。 715 // 最后将删除的结果赋给该节点左子树。 716 node.left = remove(node.left, e); 717 return node; 718 } else if (e.compareTo(node.e) > 0) { 719 // 如果待删除元素e大于该节点的元素e 720 // 去当前节点的右子树去寻找待删除元素节点 721 // 将删除后的结果返回给当前节点的右孩子 722 node.right = remove(node.right, e); 723 return node; 724 } else { 725 // 当前节点元素e等于待删除节点元素e,即e == node.e, 726 // 相等的时候,此时就是要删除这个节点的。 727 728 // 如果当前节点node的左子树为空的时候,待删除节点左子树为空的情况 729 if (node.left == null) { 730 // 保存该节点的右子树 731 Node rightNode = node.right; 732 // 将node和这棵树断开关系 733 node.right = null; 734 // 维护size的大小 735 size--; 736 // 返回原来那个node的右孩子。也就是右子树的根节点,此时就将node删除掉了 737 return rightNode; 738 } 739 740 // 如果当前节点的右子树为空,待删除节点右子树为空的情况。 741 if (node.right == null) { 742 // 保存该节点的左子树 743 Node leftNode = node.left; 744 // 将node节点和这棵树断开关系 745 node.left = null; 746 // 维护size的大小 747 size--; 748 //返回原来那个节点node的左孩子,也就是左子树的根节点,此时就将node删除掉了。 749 return leftNode; 750 } 751 752 // 待删除节点左右子树均为不为空的情况。 753 // 核心思路,找到比待删除节点大的最小节点,即待删除节点右子树的最小节点 754 // 用这个节点顶替待删除节点的位置。 755 756 // 找到当前节点node的右子树中的最小节点,找到比待删除节点大的最小节点。 757 // 此时的successor就是node的后继。 758 Node successor = minimum(node.right); 759 // 此时将当前节点node的右子树中的最小节点删除掉,并将二分搜索树的根节点返回。 760 // 将新的二分搜索树的根节点赋值给后继节点的右子树。 761 successor.right = removeMin(node.left); 762 763 // 因为removeMin操作,删除了一个节点,但是此时当前节点的右子树的最小值还未被删除 764 // 被successor后继者指向了。所以这里做一些size加加操作, 765 size++; 766 767 // 将当前节点的左子树赋值给后继节点的左子树上。 768 successor.left = node.left; 769 // 将node节点没有用了,将node节点的左孩子和右孩子置空。让node节点和二分搜索树脱离关系 770 node.left = node.right = null; 771 772 // 由于此时,将当前节点node删除掉了,所以这里做一些size减减操作。 773 size--; 774 775 // 返回后继节点 776 return successor; 777 } 778 779 } 780 781 782 @Override 783 public String toString() { 784 StringBuilder stringBuilder = new StringBuilder(); 785 // 使用一种形式展示整个二分搜索树,可以先展现根节点,再展现左子树,再展现右子树。 786 // 上述这种过程就是一个前序遍历的过程。 787 // 参数一,当前遍历的二分搜索树的根节点,初始调用的时候就是root。 788 // 参数二,当前遍历的这棵二分搜索树的它的深度是多少,根节点的深度是0。 789 // 参数三,将字符串传入进去,为了方便生成字符串。 790 generateBSTString(root, 0, stringBuilder); 791 792 return stringBuilder.toString(); 793 } 794 795 /** 796 * 生成以node为根节点,深度为depth的描述二叉树的字符串。 797 * 798 * @param node 节点 799 * @param depth 深度 800 * @param stringBuilder 字符串 801 */ 802 private void generateBSTString(Node node, int depth, StringBuilder stringBuilder) { 803 // 递归的第一部分 804 if (node == null) { 805 // 显示的,将在字符串中追加一个空字符串null。 806 // 为了表现出当前的空节点对应的二分搜索树的层次,封装了一个方法。 807 stringBuilder.append(generateDepthString(depth) + "null\n"); 808 return; 809 } 810 811 812 // 递归的第二部分 813 // 当当前节点不为空的时候,就可以直接访问当前的node节点了。 814 // 将当前节点信息放入到字符串了 815 stringBuilder.append(generateDepthString(depth) + node.e + "\n"); 816 817 // 递归进行调用 818 generateBSTString(node.left, depth + 1, stringBuilder); 819 generateBSTString(node.right, depth + 1, stringBuilder); 820 } 821 822 /** 823 * 为了表现出二分搜索树的深度 824 * 825 * @param depth 826 * @return 827 */ 828 private String generateDepthString(int depth) { 829 StringBuilder stringBuilder = new StringBuilder(); 830 for (int i = 0; i < depth; i++) { 831 stringBuilder.append("--"); 832 } 833 return stringBuilder.toString(); 834 } 835 836 837 public static void main(String[] args) { 838 BinarySearchTree<Integer> binarySearchTree = new BinarySearchTree<Integer>(); 839 int[] nums = new int[]{1, 5, 3, 6, 8, 4, 2}; 840 // for (int i = 0; i < nums.length; i++) { 841 // // 二分搜索树的新增 842 // binarySearchTree.add(nums[i]); 843 // } 844 for (int num : nums) { 845 binarySearchTree.add(num); 846 } 847 848 849 // 二分搜索树的前序遍历 850 binarySearchTree.preOrder(); 851 System.out.println(); 852 853 854 // 二分搜索树的中序遍历 855 binarySearchTree.inOrder(); 856 System.out.println(); 857 858 // 二分搜索树的后序遍历 859 binarySearchTree.postOrder(); 860 System.out.println(); 861 862 // 二分搜索树的非递归的前序遍历。 863 binarySearchTree.preOrderNonRecursive(); 864 System.out.println(); 865 866 // 二分搜索树的非递归的层序遍历。 867 binarySearchTree.levelOrder(); 868 System.out.println(); 869 870 // 二分搜索树的递归的删除任意元素 871 System.out.println("二分搜索树的递归的删除任意元素."); 872 binarySearchTree.remove(5); 873 System.out.println(binarySearchTree); 874 875 876 // System.out.println(binarySearchTree.toString()); 877 // 5 878 // --3 879 // ----2 880 // ------null 881 // ------null 882 // ----4 883 // ------null 884 // ------null 885 // --6 886 // ----null 887 // ----8 888 // ------null 889 // ------null 890 891 892 // 二分搜索树的递归的删除最小值 893 // BinarySearchTree<Integer> binarySearchTree = new BinarySearchTree<Integer>(); 894 // Random random = new Random(); 895 // int n = 1000; 896 // for (int i = 0; i < n; i++) { 897 // // 随机添加1000个0-10000之间的数字。有可能重复,总数可能小于1000 898 // binarySearchTree.add(random.nextInt(10000)); 899 // } 900 // 901 // ArrayList<Integer> nums = new ArrayList<Integer>(); 902 // while (!binarySearchTree.isEmpty()) { 903 // nums.add(binarySearchTree.removeMin()); 904 // } 905 // System.out.println(nums); 906 // 907 // // 判断是否是从小到大排序的 908 // for (int i = 1; i < nums.size(); i++) { 909 // if (nums.get(i - 1) > nums.get(i)) { 910 // throw new IllegalArgumentException("Error."); 911 // } 912 // } 913 // System.out.println("removeMin test completed."); 914 915 916 // 二分搜索树的递归的删除最大值 917 // BinarySearchTree<Integer> binarySearchTree2 = new BinarySearchTree<Integer>(); 918 // Random random2 = new Random(); 919 // int n2 = 1000; 920 // for (int i = 0; i < n2; i++) { 921 // // 随机添加1000个0-10000之间的数字。有可能重复,总数可能小于1000 922 // binarySearchTree2.add(random2.nextInt(10000)); 923 // } 924 // 925 // ArrayList<Integer> nums2 = new ArrayList<Integer>(); 926 // while (!binarySearchTree2.isEmpty()) { 927 // nums2.add(binarySearchTree2.removeMax()); 928 // } 929 // System.out.println(nums2); 930 // 931 // // 判断是否是从小到大排序的 932 // for (int i = 1; i < nums2.size(); i++) { 933 // if (nums2.get(i - 1) < nums2.get(i)) { 934 // throw new IllegalArgumentException("Error."); 935 // } 936 // } 937 // System.out.println("removeMax test completed."); 938 939 } 940 941 }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
2017-03-13 Spring框架+Struts2框架第一次整合
2017-03-13 Spring之对象依赖关系(依赖注入Dependency Injection)