一文带你读懂二叉树

Posted 波波Tea

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文带你读懂二叉树相关的知识,希望对你有一定的参考价值。



二叉树的基本概念




二叉树定义:结点的度最多为2


二叉树的五种形态:

  • 空二叉树
  • 只有一个根结点
  • 根结点只有左子树
  • 根结点只有右子树
  • 根结点既有左子树又有右子树


特殊的二叉树:

  • 斜树:分为左斜树和右斜树,其实这算是一种线性结构了

  • 满二叉树:如图1所示。

  • 完全二叉树:如图2所示。满二叉树一定是完全二叉树,但完全二叉树不一定是满的。而图3中的三颗树,都不是完全二叉树。






图1



一文带你读懂二叉树

图2



一文带你读懂二叉树

图3





二叉树的性质





  1. 在二叉树的第i层上,最多有2i-1个结点;

  2. 深度为k的二叉树,最多有2k-1个结点;

  3. 对任何一棵二叉树T,如果其叶子结点数为n0,度为2的结点数为n2,则n0=n2+1

  4. 具有n个结点的完全二叉树的深度为|log2n+1|(|x|表示不大于x的最大整数);

  5. 对一棵有n个结点的完全二叉树的结点按层序编号(从上到下从左到右,如图4所示),对任一结点i(1≤i≤n)有:

  • 结点i的左孩子为2i,右孩子为2i+1。
  • 如果 i > n/ 2 ,则结点i无左孩子(结点i为叶子结点)。
  • 如果i>(n-1)/2,则结点i无右孩子。

一文带你读懂二叉树

图4


下面就来证明一下上述性质。1、2、4这个挺好理解的,无需证明。


关于第三点,为什么n0=n2+1呢?首先,n=n0+n1+n2这个是毫无疑问的。

然后将我们的注意力转移到分支线上,而不是结点上。

对于一颗有n个结点的二叉树来说,除了根结点头顶上没有分支线连接它,其余结点的头顶上有且仅有一条分支线与它对应,因此分支线总数=n0+n1+n2-1

度为2的结点脚下有2条分支线,度为1的有1条,度为0的有0条,因此分支线总数=2*n2+n1。

根据上述标记为红色字体的两条公式,即可得出结论。


关于第五点,其实只用证明结点i的左孩子为2i即可,后面的结论都是根据这个结论而来的。下面的证明与图4有关。注意这是一颗完全二叉树。

首先我们证明这棵树的每一层的最左边结点i是否满足左孩子为2i(左孩子必然在该结点的下一层的最左边)。比如证明结点4的左孩子是否为8。假设结点4在k层(当然结点4在3层,k只是针对一般情况),那么结点4的值为2k-1,结点4的左孩子的值为2k,因此满足。

接下来证明不在每一层的最左边的结点i是否满足左孩子为2i。比如证明结点5的左孩子是否为10。设最左边的结点为i,结点5相对于本层最左边那个结点来说,偏移了n个单位(即结点5相对于结点4偏移了1个单位,n只是针对一般情况),结点5的值为i+n;其左孩子必然偏移2n个单位,结点5左孩子的值位2i+2n,因此满足。




二叉树的存储结构




对于一般的树来说,是不太适合用顺序存储结构来存储树的逻辑关系的,但是二叉树作为一种特殊的树,是可以这样做的。如下图所示。

一文带你读懂二叉树


这张图表明用数组去存储一颗普通的二叉树,如果某个结点不存在,则用空值表示。对于普通二叉树来说,顺序存储显然是浪费空间的,因此顺序存储结构一般只适用于完全二叉树。


第二种就是用二叉链表了。因为作为一颗二叉树,很容易想到每个结点应该存储结点的值和左右孩子的指针,如果想快速找到某个结点的双亲,则还可以加一个双亲指针。二叉链表如下图所示。


一文带你读懂二叉树




二叉树的遍历




二叉树的遍历: 按照一定的次序,将树中的所有结点遍历一次。
遍历方式:
  • 前序遍历
  • 中序遍历
  • 后序遍历
  • 层序遍历:从上到下、从左到右。
其中,前中后是相对于任一子树的根节点来说的。

下面用Java代码实现前中后三种遍历方式。

首先定义结点类。


public class Node{
    private Node left;
    private char data;
    private Node right;

    public Node(Node left, char data ,Node right) {
        this.left = left;
        this.right = right;
        this.data = data;
    }

    // 省略getter/setter方法
}


然后定义遍历类,该类有三个方法,分别对应前中后遍历,因为树的定义是采用递归思想的,因此树的遍历也采用了递。另外该类还有一个构建树的方法,构建的树如下图所示。最后就是main方法了,相信大家都能看懂。

一文带你读懂二叉树



public class BinaryTreeTraverse {
    /**
     * 先序
     */

    public void firstTraverse(Node node){
        if(node == null){
            return;
        }
        System.out.print(node.getData());
        firstTraverse(node.getLeft());
        firstTraverse(node.getRight());
    }
    /**
     * 中序
     */

    public void middleTraverse(Node node){
        if(node == null){
            return;
        }
        middleTraverse(node.getLeft());
        System.out.print(node.getData());
        middleTraverse(node.getRight());
    }
    /**
     * 后序
     */

    public void laterTraverse(Node node){
        if(node == null){
            return;
        }
        laterTraverse(node.getLeft());
        laterTraverse(node.getRight());
        System.out.print(node.getData());
    }

    /**
     * 构建二叉树,并返回根结点
     */

    public Node buildTree(){
        Node node_k = new Node(null, 'K', null);
        Node node_h = new Node(null, 'h', node_k);
        Node node_d = new Node(node_h, 'd', null);
        Node node_e = new Node(null, 'e', null);
        Node node_b = new Node(node_d, 'b', node_e);
        Node node_i = new Node(null, 'i', null);
        Node node_j = new Node(null, 'j', null);
        Node node_f = new Node(node_i, 'f', null);
        Node node_g = new Node(null, 'g', node_j);
        Node node_c = new Node(node_f, 'c', node_g);
        Node node_a = new Node(node_b, 'a', node_c);
        return node_a;
    }

    public static void main(String[] args) {
        BinaryTreeTraverse traverse = new BinaryTreeTraverse();
        Node root = traverse.buildTree();
        //先序遍历
        traverse.firstTraverse(root);
        System.out.println();

        //中序遍历
        traverse.middleTraverse(root);
        System.out.println();
        //后序遍历
        traverse.laterTraverse(root);

    }

}






二叉树的建立




上面讲到了二叉树的遍历,但是如果我们的内存中都没有一颗二叉树,又哪来的遍历呢?因此下面我来讲讲二叉树的建立。

上面的例子中其实已经包含了二叉树的建立,但那是通过硬编码建立的,不符合一般情况。

我们可以通过二叉树的遍历结果反向构建二叉树。比如说先序遍历吧,可以根据其先序遍历结果来构建一颗二叉树,另外,我们还需要加一些虚结点,以保证能顺利地在内存中构建出一条二叉链条出来。


本次要构建的二叉树原型如下图所示。

一文带你读懂二叉树

然后我们加一些虚节点,如下图所示。

一文带你读懂二叉树


本来的先序遍历结果应该是:ABDHKECFIGJ,我们将虚结点记为#(别的符号也可以),其先序遍历结果就变为了:ABDH#K###E##CFI###G#J##,这个很容易弄错,一定要仔细,否则输入(即将遍历结果视为输入,将构建出来的二叉树视为输出)都是错误的,那么构建出来的二叉树肯定也是错的。


二叉树的构建也由递归来实现,下面用Java代码实现通过先序遍历结果来构建一颗二叉树。

首先是结点Node类。


public class Node{
    private Node left;
    private char data;
    private Node right;

    public Node(Node left, char data ,Node right) {
        this.left = left;
        this.right = right;
        this.data = data;
    }
    // 省略getter/setter方法

    @Override
    public String toString() {
        return "Node{" +
                "left=" + (left==null?"null":left.getData()) +
                ", data=" + data +
                ", right=" + (right==null?"null":right.getData()) +
                '}';
    }
}


然后是具体的实现逻辑。


import java.util.ArrayList;
import java.util.List;

public class BinaryTreeBuilder {
    private List<Node> nodes;
    /**
     * 索引跟踪,每构建一个结点就加1,包括虚结点
     */

    private int index;

    /**
     * 构建某个结点,仅适用于先序构建
     * @param curIndex 该结点的索引,该索引不能只能存于方法客栈
     */

    public void buildNode(int curIndex){
        index++;
        //如果要构建的结点是个虚结点,那就不用构建左子树和右子树了,直接return;
        if(null == nodes.get(curIndex)){
            return;
        }
        // 递归构建左子树
        buildLeft(curIndex);
        // 递归构建右子树
        buildRight(curIndex);
    }

    /**
     * 构建左子树
     * @param curIndex 要构建左孩子的结点的索引
     */

    public void buildLeft(int curIndex){
        //设置左孩子
        nodes.get(curIndex).setLeft(nodes.get(index));
        //构建左孩子结点,因此传入的索引是左孩子的索引
        buildNode(index);
    }

    /**
     * 构建右子树
     * @param curIndex 要构建右孩子的结点的索引
     */

    public void buildRight(int curIndex){
        //设置右孩子
        nodes.get(curIndex).setRight(nodes.get(index));
        //构建右孩子结点,因此传入的索引是右孩子的索引
        buildNode(index);
    }

    public void initNodes(char[] arr){
        nodes = new ArrayList<>();
        for (char c : arr) {
            if(c=='#'){
                nodes.add(null);
            }else{
                nodes.add(new Node(null,c,null));
            }
        }
    }

    /**
     * 构建完成,打印构建结果
     */

    public void buildOK(){
        for (Node node : nodes) {
            if(null == node){
                continue;
            }
            System.out.println(node.getData()
                    +" -(左孩子->"+(node.getLeft()==null?"null":(node.getLeft().getData()+" "))+")"
                    +" -(右孩子->"+(node.getRight()==null?"null":(node.getRight().getData())+" ")+")");
        }
    }

    public void buildTree(char[] arr,String charArrType){
        initNodes(arr);
        if("先序构建".equals(charArrType)){
            buildNode(0);
        }else if("中序构建".equals(charArrType)){
            // 请参考buildNode先序构建方法,实现中序构建

        }else if("后序构建".equals(charArrType)){
            // 请参考buildNode先序构建方法,实现后序构建

        }else{
            return;
        }
        buildOK();
    }

    public static void main(String[] args) {
        BinaryTreeBuilder builder = new BinaryTreeBuilder();
        builder.buildTree("ABDH#K###E##CFI###G#J##".toCharArray(),"先序构建");
    }
}



最后运行程序,构建出来的结果如下图所示。


我仔细验证了一下,是没有问题的。至于根据中、后序遍历结果来构建二叉树的逻辑,由你们来动脑实现吧!





以上是关于一文带你读懂二叉树的主要内容,如果未能解决你的问题,请参考以下文章

一文带你读懂Python中的进程

一文带你读懂Dockerfile

一文带你读懂JDK源码:Lambda表达式

各行各业都在关注的“密评”到底是啥?一文带你读懂!

一文带你读懂 C/C++ 语言输入输出流与缓存区

Nginx系列教程| 一文带你读懂 Nginx 的负载均衡