当前位置:   article > 正文

二叉树(二叉查找树)的遍历(前中后序),添加,查找,删除,节点个数等基础操作,java实现及代码演示。_java 二叉搜索树所有叶子节点

java 二叉搜索树所有叶子节点

目录

 前言

二叉树的数据节点结构

管理二叉树的类

二叉树的遍历

前序遍历

 中序遍历

 后序遍历

层序遍历

 计算二叉树中节点的总个数

二叉树中叶子节点个数

获取第K层的节点个数

二叉树的高度

添加节点

一次添加一个节点

一次添加多个节点

查找结点

删除节点


 前言

        二叉树是一种常用的树结构。

        对于树,我们称每一个节点的子节点个数为度;

        二叉树就是任意一个度小于等于2树,也就是说,二叉树中任意节点的子节点个数小于等于2;

        二叉查找树是指按照一定规则次序排列的二叉树,这种二叉树左子树的任意节点的值小于当前节点,右子树的任意节点的值大于当前节点,二叉查找树中不能有重复数据。

 

这是我通过随意数据画出的二叉查找树,之后的操作都将用这组数据作为测试;
7,7,4,5,3,2,8,1,1,9,22;

如图可以观察出二叉查找树的特点:任意节点的子节点个数小于等于2 且 左子树的任意节点的值小于当前节点,右子树的任意节点的值大于当前节点。

专有名词

 以上是树的一些专有名词。

二叉树的数据节点结构

 结构代码

  1. class Node{
  2. class Node {
  3. private int val;//储存节点的数据
  4. private Node left;//储存该节点的左子节点
  5. private Node right;//储存该节点的右子节点
  6. public Node(int val) {
  7. this.val = val;
  8. }//赋值构造器
  9. @Override
  10. public String toString() {
  11. return "Node{" +
  12. "val=" + val +
  13. '}';
  14. }
  15. //以下是获取该节点信息的方法
  16. public int getVal() {
  17. return val;
  18. }
  19. public void setVal(int val) {
  20. this.val = val;
  21. }
  22. public Node getLeft() {
  23. return left;
  24. }
  25. public void setLeft(Node left) {
  26. this.left = left;
  27. }
  28. public Node getRight() {
  29. return right;
  30. }
  31. public void setRight(Node right) {
  32. this.right = right;
  33. }
  34. }

管理二叉树的类

为了方便对二叉树的操作,我定义一个用来管理二叉树的类  class ChargeTree

  1. class ChargeTree {
  2. private Node root;//二叉树的根节点
  3. public ChargeTree() {
  4. }//因为创建时不需要给二叉树赋任何值所以用了各无参构造器
  5. //获取根节点的方法
  6. public Node getRoot() {
  7. return root;
  8. }
  9. public void setRoot(Node root) {
  10. this.root = root;
  11. }
  12. }

之后的增,查,遍历等操作方法都写在这个类中。

二叉树的遍历

所有类型的二叉树通用的遍历方法;

前序遍历

遍历顺序:打印当前节点 --> 遍历左子树节点  --> 遍历右子树节点;

注意动词!

 参照上图:

从根节点 7进入,7 作为当前节点先打印出来(打印当前节点);

7 的左子树不为null,所以向 7 的左子树遍历;(遍历左子树节点)

4 作为当前节点打印出来;(打印当前节点)

 4 的左子树不为null,所以向 4 的左子树遍历;(遍历左子树节点)

3 作为当前节点打印;(打印当前节点)

由此类推,直到打印完 1 ,发现没有左子树和右子树,开始返回到 节点2 ;(递归的返回)

对于 节点2来说,当前节点打印过了,左子树也遍历完了,该遍历右子树了;(遍历右子树节点)

但 2 的右子树为null,那就跳过这一步骤,再返回到 3 ;(递归的返回)

3 也是同样的情况,3 的右子树为null,继续返回到4;(递归的返回)

4 的右子树不为空,向 4 的右子树遍历;

 遍历到 5 成为当前节点,打印出来;(打印当前节点)

5 的左子树和右子树都为null,只能返回到 4 ;(递归的返回)

4 的三步已经完成,返回到 7;(递归的返回)

7 的右子树不为空,继续遍历;(遍历右子树节点)

……

预测的打印顺序:7,4,3,2,1,5,8,22,9

代码实现:采用递归,子问题为 打印当前节点,遍历左子树,再遍历右子树;

  1. //前序遍历
  2. public void preOrder2(Node cur){
  3. if(this.root == null){
  4. System.out.println("树为空");
  5. return;
  6. }
  7. System.out.println(cur);
  8. if(cur.getLeft() != null){
  9. preOrder2(cur.getLeft());
  10. }
  11. if(cur.getRight() != null){
  12. preOrder2(cur.getRight());
  13. }
  14. }

测试代码

  1. System.out.println("----------------前序遍历-------------------");
  2. tree.preOrder2(tree.getRoot());

测试数据:7,7,4,5,3,2,8,1,1,9,22;

测试结果:

 中序遍历

再二叉查找树中中序遍历是最常用的;因为二叉查找树有中序遍历出来的结果是按照由小到大的顺序排列好的;

遍历顺序:遍历左子树节点 -->  打印当前节点 --> 遍历右子树节点;

过程就不再讲解,和前序遍历一样。

测试数据:7,7,4,5,3,2,8,1,1,9,22;

预测打印出来的结果:1 2 3 4 5 7 8 9 22;

代码实现:同样是递归

  1. //中序遍历
  2. public void inOrder2(Node cur){
  3. if(this.root == null){
  4. System.out.println("树为空");
  5. return;
  6. }
  7. if(cur.getLeft() != null){
  8. inOrder2(cur.getLeft());
  9. }
  10. System.out.println(cur);
  11. if(cur.getRight() != null){
  12. inOrder2(cur.getRight());
  13. }
  14. }

测试代码:

  1. System.out.println("----------------中序遍历-------------------");
  2. tree.inOrder2(tree.getRoot());

测试结果:

 后序遍历

遍历顺序:遍历左子树节点 -->  遍历右子树节点 --> 打印当前节点;

测试数据:7,7,4,5,3,2,8,1,1,9,22;

预期打印结果:1 2 3 5 4 9 22 8 7;

代码实现:递归思想

  1. //后序遍历
  2. public void postOrder2(Node cur){
  3. if(this.root == null){
  4. System.out.println("树为空");
  5. return;
  6. }
  7. if(cur.getLeft() != null){
  8. postOrder2(cur.getLeft());
  9. }
  10. if(cur.getRight() != null){
  11. postOrder2(cur.getRight());
  12. }
  13. System.out.println(cur);
  14. }

测试代码:

  1. System.out.println("----------------后序遍历-------------------");
  2. tree.postOrder2(tree.getRoot());

测试结果:

层序遍历

层序遍历是按照一层一层,每层从左至右依次遍历的;

 层序遍历利用了队列先进先出的特性;

最开始将根节点 7 存入队列,之后进入循环,每删除一个节点就记录它的两个子节点进入队尾;

直到队列为空时,二叉树所有元素遍历完成;

测试数据:7,7,4,5,3,2,8,1,1,9,22;

预测打印结果:7 4 8 3 5 22 2 9 1;

代码实现:

  1. //层序遍历
  2. public void floorOrder() {
  3. if (this.root == null) {
  4. System.out.println("树为空");
  5. return;
  6. }
  7. Queue<Node> queue = new LinkedList<>();
  8. queue.offer(root);
  9. while (!queue.isEmpty()) {
  10. Node ret = queue.poll();
  11. System.out.println(ret);
  12. if (ret.getLeft() != null) {
  13. queue.offer(ret.getLeft());
  14. }
  15. if (ret.getRight() != null) {
  16. queue.offer(ret.getRight());
  17. }
  18. }
  19. }

测试代码:

  1. System.out.println("----------------层序遍历-------------------");
  2. tree.floorOrder();

测试结果:

 计算二叉树中节点的总个数

方法一:遍历 + 计数

在类中定义一个整型表示计数;

将刚才的遍历方法中的打印语句替换成一个计数语句即可;

  1. class ChargeTree{
  2. private int count;
  3. //方法一:中序遍历 + 计数
  4. public int size1B(Node cur){
  5. if(cur.getLeft() != null){
  6. inOrder2(cur.getLeft());
  7. }
  8. this.count++;
  9. if(cur.getRight() != null){
  10. inOrder2(cur.getRight());
  11. }
  12. return this.count;
  13. }
  14. }

依旧是用原来的数据测试(7,7,4,5,3,2,8,1,1,9,22;)

 就算去掉重复的一个 7 和一个 1也应该有9 个节点啊?

注意

问题1:这里的 count必须用static修饰,因为函数的实现过程用到了递归,递归每进行一次相当于实例化了一个对象然后使用里面的count字段;所以每次实例化count都会清0;

用 static 修饰的话,count就变成了类的属性,类的字段,每次使用count++的时候,count在类(模板)里面的值就会改变,实例化的时候也就不会清0;

修改过后

  1. class ChargeTree{
  2. private static int count;
  3. //方法一:中序遍历 + 计数
  4. public int size1B(Node cur){
  5. if(cur.getLeft() != null){
  6. inOrder2(cur.getLeft());
  7. }
  8. this.count++;
  9. if(cur.getRight() != null){
  10. inOrder2(cur.getRight());
  11. }
  12. return this.count;
  13. }
  14. }

再次测试

 此时的结果好像是正常了……

问题2:如果我对该树进行了增加或删除的操作导致结点的数量改变;当我想要重新计算结点的总数时,count因为用static修饰了,所以保持着之前的值,count++就不是在0的基础上加了;计算出的结果肯定有问题;

尝试调用两次计数节点

果然出问题了

原因是:我们虽然不希望count在每次递归的时候重置,但每次计算二叉树总结点个数的时候又不得不重置。

解决方法:我们可以设置一个中间方法,用来将count重置,充值后再调用计算二叉树总结点数的方法。

最终结果

  1. class ChargeTree{
  2. private static int count;
  3. //方法一:中序遍历 + 计数
  4. public int size1(Node cur){
  5. if(this.root == null){
  6. System.out.println("树为空");
  7. return 0;
  8. }
  9. this.count = 0;
  10. return size1B(cur);
  11. }
  12. public int size1B(Node cur){
  13. if(cur.getLeft() != null){
  14. size1B(cur.getLeft());
  15. }
  16. this.count++;
  17. if(cur.getRight() != null){
  18. size1B(cur.getRight());
  19. }
  20. return this.count;
  21. }
  22. }

方法二:递归  子问题  总节点个数 = 该节点的左子树节点个数 + 该节点的右子树节点个数 + 1(当前节点)

用这个方法更容易实现;

代码实现:

  1. public int size2(Node cur) {
  2. if (cur == null) {
  3. return 0;
  4. }
  5. //如果节点为null,返回0
  6. int count = size2(cur.getLeft()) + size2(cur.getRight()) + 1;
  7. return count;
  8. }

二叉树中叶子节点个数

根据上一个问题 “计算二叉树中节点的总个数” 可以总结出计数结点的规则。

  • 基本方法:遍历 + 计数;
  • 用 static 修饰类中记录节点数量的变量(count);
  • 用中间函数实现数为空的判断和 count 的重置。

叶子节点计数问题也是采用同样的方法,不一样的只是需要加一个判断是叶子结点的语句。

叶子结点的特点是左子树为空,右子树也为空;

  1. class Main{
  2. public static int count2;
  3. public int leavesNode(Node cur){
  4. if(this.root == null){
  5. System.out.println("树为空");
  6. return 0;
  7. }
  8. this.count2 = 0;
  9. return leavesNodeB(cur);
  10. }
  11. public int leavesNodeB(Node cur){
  12. if(cur.getLeft() != null){
  13. leavesNode(cur.getLeft());
  14. }
  15. if(cur.getLeft() == null && cur.getRight() == null){
  16. System.out.println(cur);
  17. this.count2++;
  18. }
  19. if(cur.getRight() != null){
  20. leavesNode(cur.getRight());
  21. }
  22. return this.count2;
  23. }
  24. }

获取第K层的节点个数

 获取第 K 层节点个数 并返回第 k 层节点数据

  1. //递归: 子问题 一层的节点个数 = 上一层所有的父节点的 左子节点个数 + 右子节点个数
  2. public int floorNodeCount(Node cur, int k) {
  3. if (cur == null) {
  4. return 0;
  5. }//当前节点为空,没有节点返回0
  6. if (k == 1) {
  7. System.out.println(cur);//打印第k层节点
  8. return 1;
  9. }//第k层的1个节点,返回1;
  10. return floorNodeCount(cur.getLeft(), k - 1) + floorNodeCount(cur.getRight(), k - 1);
  11. }

递归思想是 下一层的节点数 = 该层所有父节点的 左子树节点个数 + 右子树节点个数;

在编写递归函数的时候:

设想 floorNodeCount(Node cur, int k)函数的功能是求树的K层节点的个数;

这个问题拆解为求左子树的K层节点个数和右子树的K层节点个数;

直到当K == 1 时,当前所在层就是第k层。

实际运行更像是 将每条路径一直走到第K层(当 k==1 时),如果存在节点返回1;如果不存在(cur == null)则返回0;(深度优先搜索)

换言之当k == 1时就已经将一条路径走到了第k层,所以当k == 1时可以打印出第k层节点数据;

二叉树的高度

  1. //二叉树的高度
  2. //递归:子问题 求左子树高度与右子树高度作比较 取较高者 +1(父节点)
  3. public int treeHigh(Node cur) {
  4. if (cur == null) {
  5. return 0;
  6. }
  7. return Math.max(treeHigh(cur.getLeft()), treeHigh(cur.getRight())) + 1;
  8. //取左子树和右子树较大高度 +1
  9. }

添加节点

一次添加一个节点

非递归实现:

根据二叉查找树的特性,添加节点会从根节点开始一次比较遍历:

如果当前节点的值大于要添加的值,那么向左子树遍历;

如果当前节点的值小于要添加的值,那么向右子树遍历;

如果当前节点的值等于要添加的值,那么不添加;

直到遍历到叶子节点,将要添加的数据赋给新的节点,并连接到叶子节点的后面;

  1. //排序二叉树(二叉查找树)的节点添加
  2. //非递归实现
  3. public void add(int val) {
  4. Node newNode = new Node(val);
  5. if (this.root == null) {
  6. this.root = new Node(val);
  7. return;
  8. }
  9. Node m = root;
  10. while (true) {
  11. if (val < m.getVal()) {
  12. if (m.getLeft() != null) {
  13. m = m.getLeft();
  14. continue;
  15. } else {
  16. m.setLeft(newNode);//添加成功,退出循环
  17. return;
  18. }
  19. } else if (val > m.getVal()) {
  20. if (m.getRight() != null) {
  21. m = m.getRight();
  22. continue;
  23. } else {
  24. m.setRight(newNode);//添加成功,退出循环
  25. return;
  26. }
  27. } else {
  28. System.out.println("元素 " + val + " 已存在");
  29. //元素已存在
  30. return;
  31. }
  32. }
  33. }

递归实现:

把握好递归条件,条件和非递归的一样,判断当前结点的数据与要添加的数据的大小关系;

完成深搜递归。

  1. public void add(Node cur, int val) {
  2. if(this.root == null){
  3. root = new Node(val);
  4. return;
  5. }
  6. if (cur.getVal() == val) {
  7. System.out.println("元素 " + val + "已存在");
  8. return;
  9. }
  10. //满足添加条件
  11. if (cur.getLeft() == null && val < cur.getVal()) {
  12. cur.setLeft(new Node(val));
  13. return;
  14. }
  15. if (cur.getRight() == null && val > cur.getVal()) {
  16. cur.setRight(new Node(val));
  17. return;
  18. }
  19. if (val < cur.getVal()) {
  20. add(cur.getLeft(), val);
  21. } else if (val > cur.getVal()) {
  22. add(cur.getRight(), val);
  23. }
  24. }

一次添加多个节点

规则都一样,不一样的只是利用了可变参数,相当于一个数组;

  1. public void addAll(int... val) {
  2. //利用可变参数;远离相当于创建了一个数组
  3. int k = 0;//记录第一个元素应添加下标
  4. //如果根节点的数据是新赋值的,则val中第一个数据赋给根节点(特判),其他数据正常赋值
  5. //如果不是新的,那么val中所有数据正常赋值
  6. if (this.root == null) {
  7. this.root = new Node(val[0]);
  8. k = 1;//再给子节点赋值的时候跳过第一个根据节点利用过的数据
  9. }
  10. for (int i = k; i < val.length; i++) {
  11. Node m = root;
  12. while (true) {
  13. if (val[i] < m.getVal()) {
  14. if (m.getLeft() != null) {
  15. m = m.getLeft();
  16. continue;
  17. } else {
  18. m.setLeft(new Node(val[i]));
  19. break;
  20. }
  21. } else if (val[i] > m.getVal()) {
  22. if (val[i] > m.getVal()) {
  23. if (m.getRight() != null) {
  24. m = m.getRight();
  25. continue;
  26. } else {
  27. m.setRight(new Node(val[i]));
  28. break;
  29. }
  30. }
  31. } else {
  32. System.out.println("元素 " + val[i] + " 已存在");
  33. break;
  34. }
  35. }
  36. }
  37. }

查找结点

非递归:

步骤和添加节点类似

  1. //1.非递归
  2. public Node query(int target) {
  3. if (this.root == null) {
  4. return null;
  5. }
  6. Node m = this.root;
  7. while (true) {
  8. if (target == m.getVal()) {
  9. return m;
  10. } else if (target < m.getVal()) {
  11. if (m.getLeft() != null) {
  12. m = m.getLeft();
  13. continue;
  14. } else {
  15. return null;
  16. }
  17. } else {
  18. if (m.getRight() != null) {
  19. m = m.getRight();
  20. continue;
  21. } else {
  22. return null;
  23. }
  24. }
  25. }
  26. }

递归:

  1. //2.递归 分为左右树递归查询
  2. public Node query(Node cur, int target) {
  3. if (cur == null) {
  4. return null;
  5. }
  6. if (cur.getVal() == target) {
  7. //如果当前节点是目标值
  8. return cur;
  9. }
  10. //在左子树中查找
  11. if (target < cur.getVal()) {
  12. Node ret = query(cur.getLeft(), target);
  13. if (ret != null) {
  14. //找到了 返回
  15. return ret;
  16. }
  17. }
  18. //在右子树中查找
  19. if (target > cur.getVal()) {
  20. Node ret = query(cur.getRight(), target);
  21. if (ret != null) {
  22. //找到了 返回
  23. return ret;
  24. }
  25. }
  26. //左右树中都没有找到
  27. return null;
  28. }

删除节点

删除节点分为以下几种情况:

  1. 要删除的节点不存在;
  2. 要删除的节点是叶子节点(没有左子树和右子树),这种可以直接删除;
  3. 要删除的节点左子树和右子树有一个为空,这种的需要跳过该节点,返回该节点不为空的一棵子树;
  4. 要删除的节点左子树和右子树都不为空,
  • 这种的需要先将左子树继承到右子树中最小的节点上,然后直接跳过该节点,返回右子树(演示代码所采用的);
  • 或者将右子树继承到左子树最大的节点上,然后直接跳过该节点,返回左子树;

第4种情况,如果在这颗树中要删除的是4节点

 先继承,后删除,结果为:

在这棵树中恰好4的右子树只有一个节点,那么他就作为右子树最小节点,连接4的左子树。

代码演示:

  1. //删除节点
  2. public Node remove(Node cur, int val){
  3. if(cur == null){
  4. return null;
  5. }
  6. if(cur.getVal() == val){
  7. //找到了要删除的节点
  8. if(cur.getLeft() == null && cur.getRight() == null){
  9. //叶子节点
  10. return null;
  11. }
  12. if(cur.getLeft() != null && cur.getRight() == null){
  13. //左子树不为空,右子树为空
  14. return cur.getLeft();
  15. }
  16. if(cur.getRight() != null && cur.getLeft() == null){
  17. //左子树为空,右子树不为空
  18. return cur.getRight();
  19. }
  20. if(cur.getLeft() != null && cur.getRight() != null){
  21. //左子树不为空,右子树不为空
  22. Node help = cur.getRight();
  23. //右孩子继承
  24. while(help.getLeft() != null){
  25. help = help.getLeft();
  26. }//找到右子树中最小的(离cur的值最近的)节点
  27. help.setLeft(cur.getLeft());
  28. //将删除节点的左子树连接到help的左子树上
  29. //变成左子树为空,右子树不为空的情况
  30. return cur.getRight();
  31. }
  32. }//找到了要删除的节点
  33. if(cur.getVal() > val){
  34. cur.setLeft(remove(cur.getLeft(), val));
  35. }//遍历左子树查找
  36. if(cur.getVal() < val){
  37. cur.setRight(remove(cur.getRight(), val));
  38. }//遍历右子树查找
  39. return cur;
  40. //不需要删除则直接返回当前
  41. }

在编写递归函数的时候,函数的返回值类型是树节点Node,return可以表示结点的连接关系,return null就是上一个节点不连接任何东西了;return cur就是上一个节点连接到该节点;同理,return cur.getRight()和return cur.getLeft()就是上一个节点直接连接到当前节点的右子树或左子树。

这样就达到了删除的目的。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/275039?site
推荐阅读
相关标签
  

闽ICP备14008679号

        
cppcmd=keepalive&