打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
细说二叉树

为了后续学习堆排序以及MySQL索引等知识,接下来会重温一下树这种数据结构,包括二叉树、赫夫曼树、二叉排序树(BST)、平衡二叉树(AVL)、B树和B+树。本文只涉及二叉树,其他树后续再细说。

一、树的介绍

「1. 为什么要有树这种结构?」

有了数组和链表,为什么还要有树?先来看看数组和链表的优缺点。

  • 数组:因为有索引,所以可以快速地访问到某个元素。但是如果要进行插入或者删除的话,被插入/删除位置之后的元素都得移动,如果插入后超过了数组容量,还得进行数组扩容。可见,数组查询快,增删慢。

  • 链表:没有索引,要查询某个元素,得从第一个元素开始,一个一个往后遍历。但是要进行插入或者删除,无需移动元素,只要找到插入/删除位置的前一个元素即可。所以链表查询慢,增删快。

说到这里,那肯定知道树存在的意义了,没错,它吸收了链表和数组的优点,查询快,增删也快。

「2. 二叉树:」

每个节点最多有两个叶子节点的树,叫做二叉树。假如一棵树有n层,所以的叶子节点都在第n层,并且节点总数为(2^n) - 1,那么就把这棵树称为「满二叉树」。如果最后一层的叶子节点左边是连续的,倒数第二层右边的叶子节点是连续的,那就称为「完全二叉树」

二、二叉树的遍历

前序遍历、后序遍历和中序遍历,这里的前中后的是父节点的遍历时机。

  • 前序遍历:根左右。先输出当前节点;如果左子节点不为空,则递归进行前序遍历;如果右子节点不为空,则继续递归前序遍历。

  • 中序遍历:左根右。如果左子节点不为空,则递归中序遍历;输出当前节点;如果右子节点不为空,则递归中序遍历。

  • 后序遍历:左右根。如果左子节点不为空,递归后序遍历;如果右子节点不为空,递归后序遍历;输出当前节点。

「1. 新建一个TreeNode类:」

这个是节点类,省略了set/get方法。

public class TreeNode {
 
 private Object element;
 private TreeNode left;
 private TreeNode right;
 
    public TreeNode() {}
    public TreeNode(Object element) {
     this.element = element;
    }
}

「2. 构建一棵二叉树:」

二叉树

假如要构建这样一棵树,那么代码实现就是:

TreeNode root = new TreeNode(6);
TreeNode node1 = new TreeNode(5);
TreeNode node2 = new TreeNode(4);
root.setLeft(node1);
root.setRight(node2);
TreeNode node3 = new TreeNode(3);
node1.setLeft(node3);
TreeNode node4 = new TreeNode(2);
node1.setRight(node4);
TreeNode node5 = new TreeNode(1);
node2.setLeft(node5);

按照遍历规则,前中后序的遍历结果应该是:

  • 前序遍历:6, 5, 3, 2, 4, 1
  • 中序遍历:3, 5, 2, 6, 1, 4
  • 后序遍历:3, 2, 5, 1, 4, 6

「3. 代码实现三种遍历方式:」

/**
 * 前序遍历
 * 
 * @param root
 */
public static void preOrder(TreeNode root) {
 // 先输出当前节点
 System.out.println(root.getElement());
 // 判断左子节点是否为空,不为空就递归
 if (root.getLeft() != null) {
  preOrder(root.getLeft());
 }
 // 判断右子节点是否为空,不为空就递归
 if (root.getRight() != null) {
  preOrder(root.getRight());
 }
}

/**
 * 中序遍历
 * 
 * @param root
 */
public static void infixOrder(TreeNode root) {
 // 判断左子节点是否为空,不为空就递归
 if (root.getLeft() != null) {
  infixOrder(root.getLeft());
 }
 // 输出当前节点
 System.out.println(root.getElement());
 // 判断右子节点是否为空,不为空就递归
 if (root.getRight() != null) {
  infixOrder(root.getRight());
 }
}

/**
 * 后序遍历
 * @param root
 */
public static void postOrder(TreeNode root) {
 // 判断左子节点是否为空,不为空就递归
 if (root.getLeft() != null) {
  postOrder(root.getLeft());
 }
 // 判断右子节点是否为空,不为空就递归
 if (root.getRight() != null) {
  postOrder(root.getRight());
 }
 // 输出当前节点
 System.out.println(root.getElement());
}

二叉树的查找就不说了,都会遍历了还不会查找吗?

三、二叉树的删除

这里说的删除先不考虑子节点上浮的情况,即如果删除的非叶子节点,那就直接删除整棵子树。删除的思路如下:

  • 如果二叉树只有一个节点,直接将该节点设置为null即可;
  • 判断当前节点的左子节点是否为要删除的节点,如果是,就删除当前节点左子节点;
  • 判断当前节点的右子节点是否为要删除的节点,如果是,就删除当前节点右子节点;
  • 如果上述操作没有找到要删除的节点,就向当前节点左子树递归;
  • 如果向左子树递归也没找到要删除的节点,就向当前节点右子树递归;

「代码实现:」

/**
* 删除节点
* @param node
*/
public static void delNode(TreeNode root, Object ele) {
 // 如果二叉树为空,直接return
 if (root == null) {
  return;
 }
 // 如果只有一个节点,或者root就是要删除的节点,直接置空
 if ((root.getLeft() == null && root.getRight() == null) ||
   root.getElement() == ele) {
  root.setElement(null);
  root.setLeft(null);
  root.setRight(null);
  return;
 }
 // 判断左子节点是否为要删除的节点
 if (root.getLeft() != null && root.getLeft().getElement()== ele) {
  root.setLeft(null);
  return;
 }
 // 判断右子节点是否为要删除的节点
 if (root.getRight() != null && root.getRight().getElement()== ele) {
  root.setRight(null);
  return;
 }
 // 向左子树递归
 if (root.getLeft() != null) {
  delNode(root.getLeft(), ele);
 }
 // 向右子树递归
 if (root.getRight() != null) {
  delNode(root.getRight(), ele);
 }
}

四、顺序存储二叉树

所谓顺序存储二叉树,就是将二叉树的元素用数组存起来,并且在数组中遍历这些元素时依旧能体现出前/中/后序遍历。为了达到这个目的,所以顺序存储二叉树有一些要求:

  • 通常只考虑完全二叉树;

我们给二叉树的元素从上到下从左往右从0开始依次标上号,这些号得满足:

  • n号元素的左节点标号为2n + 1
  • n号元素的右节点标号为2n + 2
  • n号元素的父节点标号为(n-1) / 2

怎么将二叉树用数组存起来就不说了,进行层序遍历就好了,从上到下从左往右将元素依次存进数组。主要来看一看,用数组保存起来的二叉树。如何遍历,才能达到二叉树前/中/后序遍历的效果。

「代码实现:」

/**
* 前序遍历顺序存储的二叉树
* @param arr
*/
public static void preOder(int[] arr) {
 if (arr == null || arr.length == 0) {
  return;
 }
 preOder(arr, 0);
}
 
/**
* 前序遍历顺序存储的二叉树
* @param index
*/
private static void preOder(int[] arr, int index) {
 // 输出当前元素
 System.out.println(arr[index]);
 // 向左递归
 if ((index * 2 + 1) < arr.length) {
  preOder(arr, (index * 2 + 1));
 }
 // 向右递归
 if ((index * 2 + 2) < arr.length) {
  preOder(arr, (index * 2 + 2));
 }
}

这就是实现前序遍历的代码,中/后序遍历就换一下输出当前元素的位置就可以了。

五、线索化二叉树

二叉树

还是拿这棵二叉树来说,321节点的leftright指针都没用到,4节点的right指针没有用到,也就是整棵二叉树有7个指针是没有用到的。其实我们可以充分利用这些指针,让这些指针指向前/中/后序遍历的前/后一个节点,这就叫「线索化二叉树」。根据指针指向的不同节点,又可以分为前/中/后序线索化二叉树。

注意,要线索化二叉树,得满足一个条件,假如总共有n个节点,那么未使用的指针数应为n + 1。一个节点的前一个节点称为前驱节点,后一个节点称为后继节点。

「1. 中序线索化二叉树:」

上面这棵二叉树,中序遍历的结果是:3, 5, 2, 6, 1, 4,我们让有空闲指针的节点,left指针指向它的前驱,right指针指向它的后继。首先从3开始,3没有前驱,后继是5,所以3right指针指向5;然后是2,让它left指向5right指向61left指向6right指向4。中序线索化的二叉树如下图(绿色是左指针,黄色是右指针):

中序线索化二叉树
  • 线索化二叉树后,left指针可能指向的是左子树,也可能指向前驱节点;
  • right指针可能指向右子树,也可能指向后继节点;

「2. 代码实现:」

首先,对于TreeNode节点类,得增加两个属性,用来表示左右节点的类型,约定用0表示子树,用1表示前驱/后继。改造后的节点类如下:

public class TreeNode {
 private Object element;
 private TreeNode left;
 private TreeNode right;
 private int leftType; // 0左子树, 1前驱节点
 private int rightType; // 0右子树,1后继节点
}

上面所有操作二叉树的方法,我都封装在TreeUtil中,要线索化二叉树,还需要在TreeUtil中定义一个变量,用来保存前一个节点,如下:

private static TreeNode preNode; // 前一个节点

线索化二叉树的代码:

public static void inSeqLineTree(TreeNode curNode) {
  if (curNode == null){
            return;
        }
        // 处理左子树
        inSeqLineTree(curNode.getLeft());

        // 处理当前节点
        if (curNode.getLeft() == null){
            curNode.setLeft(preNode);
            curNode.setLeftType(1);
        }
        if (preNode != null && preNode.getRight() == null){
            preNode.setRight(curNode);
            preNode.setRightType(1);
        }
        preNode = curNode;

        // 处理右子树
        inSeqLineTree(curNode.getRight());
}

那么怎么验证有没有线索化成功呢?如果成功的话,节点3right应该是节点5,并且节点3的型是1;节点2left5,并且节点2的类型是1。

public static void main(String[] args) {
        TreeNode root = new TreeNode(6);
        TreeNode node1 = new TreeNode(5);
        TreeNode node2 = new TreeNode(4);
        root.setLeft(node1);
        root.setRight(node2);
        TreeNode node3 = new TreeNode(3);
        node1.setLeft(node3);
        TreeNode node4 = new TreeNode(2);
        node1.setRight(node4);
        TreeNode node5 = new TreeNode(1);
        node2.setLeft(node5);

        TreeUtil.inSeqLineTree(root); // 6, 5, 3, 2, 4, 1

        System.out.printf("节点3的right:%s, 类型:%s %n", node3.getRight().getElement(), node3.getRightType());
        System.out.printf("节点2的left:%s, 类型:%s %n", node4.getLeft().getElement(), node4.getLeftType());
}
运行结果

结果和预期一致,说明线索化成功。

六、遍历线索化二叉树

上面线索化了二叉树,有什么用呢?其实就是为了可以更简单地进行遍历。线索化之后,各个节点的指向有变化,所以原来的遍历方式就用不了了,现在可以用非递归的方式进行遍历,可以提高效率。

遍历线索化二叉树的代码:

public static void seqOrder(TreeNode root) {
  TreeNode curNode = root;
  while(curNode != null) {
   // 找到leftType为1的节点
   while(curNode.getLeftType() == 0) {
    curNode = curNode.getLeft();
   }
   // 找到就输出
   System.out.println(curNode.getElement());
   // 如果当前节点的右指针指向的是后继节点,就直接输出
   while(curNode.getRightType() == 1) {
    curNode = curNode.getRight();
    System.out.println(curNode.getElement());
   }
   // 遇到了不等于1的,替换遍历的节点
   curNode = curNode.getRight();
  }
}

传入root后,运行结果是和中序遍历的结果一致的,说明没问题。


扫描二维码

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
612,BFS和DFS解奇偶树
求二叉树的结点个数
基于c语言实现的二叉查找树
二叉树的查找方式
常见数据结构与算法整理总结(上)
不怕面试被问了!二叉树算法大盘点 | 原力计划
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服