前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >二叉树详细教程 --- 请食用

二叉树详细教程 --- 请食用

作者头像
贪挽懒月
发布2020-11-11 11:03:39
5230
发布2020-11-11 11:03:39
举报
文章被收录于专栏:JavaEEJavaEE

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

一、树的介绍

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

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

  • 数组:因为有索引,所以可以快速地访问到某个元素。但是如果要进行插入或者删除的话,被插入/删除位置之后的元素都得移动,如果插入后超过了数组容量,还得进行数组扩容。可见,数组查询快,增删慢。
  • 链表:没有索引,要查询某个元素,得从第一个元素开始,一个一个往后遍历。但是要进行插入或者删除,无需移动元素,只要找到插入/删除位置的前一个元素即可。所以链表查询慢,增删快。

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


欢迎大家关注我的公众号 javawebkf,目前正在慢慢地将简书文章搬到公众号,以后简书和公众号文章将同步更新,且简书上的付费文章在公众号上将免费。


2. 二叉树:

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

二、二叉树的遍历

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

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

1. 新建一个TreeNode类:

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

代码语言:javascript
复制
public class TreeNode {
    
    private Object element;
    private TreeNode left;
    private TreeNode right;
    
    public TreeNode() {}
    public TreeNode(Object element) {
        this.element = element;
    }
}

2. 构建一棵二叉树:

二叉树

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

代码语言:javascript
复制
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. 代码实现三种遍历方式:

代码语言:javascript
复制
/**
 * 前序遍历
 * 
 * @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即可;
  • 判断当前节点的左子节点是否为要删除的节点,如果是,就删除当前节点左子节点;
  • 判断当前节点的右子节点是否为要删除的节点,如果是,就删除当前节点右子节点;
  • 如果上述操作没有找到要删除的节点,就向当前节点左子树递归;
  • 如果向左子树递归也没找到要删除的节点,就向当前节点右子树递归;

代码实现:

代码语言:javascript
复制
/**
* 删除节点
* @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

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

代码实现:

代码语言:javascript
复制
/**
* 前序遍历顺序存储的二叉树
* @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表示前驱/后继。改造后的节点类如下:

代码语言:javascript
复制
public class TreeNode {
    private Object element;
    private TreeNode left;
    private TreeNode right;
    private int leftType; // 0左子树, 1前驱节点
    private int rightType; // 0右子树,1后继节点
}

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

代码语言:javascript
复制
private static TreeNode preNode; // 前一个节点

线索化二叉树的代码:

代码语言:javascript
复制
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。

代码语言:javascript
复制
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());
}

运行结果

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

六、遍历线索化二叉树

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

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

代码语言:javascript
复制
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后,运行结果是和中序遍历的结果一致的,说明没问题。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、树的介绍
  • 二、二叉树的遍历
  • 三、二叉树的删除
  • 四、顺序存储二叉树
  • 五、线索化二叉树
  • 六、遍历线索化二叉树
相关产品与服务
云数据库 MySQL
腾讯云数据库 MySQL(TencentDB for MySQL)为用户提供安全可靠,性能卓越、易于维护的企业级云数据库服务。其具备6大企业级特性,包括企业级定制内核、企业级高可用、企业级高可靠、企业级安全、企业级扩展以及企业级智能运维。通过使用腾讯云数据库 MySQL,可实现分钟级别的数据库部署、弹性扩展以及全自动化的运维管理,不仅经济实惠,而且稳定可靠,易于运维。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档