当前位置:   article > 正文

二叉树的建立_一文带你读懂二叉树

二叉树的建立

二叉树的基本概念

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

二叉树的五种形态:

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

特殊的二叉树:

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

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

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

530c5722557c7343e8b6808dd7549636.png

图1

2110e33a8d7cae200a70b7d2c577af53.png

图2

72a11b74f6bbbdda01abb3f5e3c7bb31.png

图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无右孩子。

ceb9895bacb91e8a785071c65ce09115.png

图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,因此满足。

二叉树的存储结构

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

f9a629870e862d5c0878cdf561d15296.png

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

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

f179d376768d33bf4dbdc4253dd21991.png

二叉树的遍历

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

下面用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方法了,相信大家都能看懂。

485b827bf5cf76a12e4ab977af00dd13.png

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);    }}

二叉树的建立

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

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

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

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

43c2a5d23cb9a7f36901c8f6a1a97862.png

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

fc2080fb45b67df1872bd5e3c9fc4eae.png

本来的先序遍历结果应该是: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 nodes;/**     * 索引跟踪,每构建一个结点就加1,包括虚结点     */private int index;/**     * 构建某个结点,仅适用于先序构建     * @param curIndex 该结点的索引,该索引不能只能存于方法客栈     */public void buildNode(int curIndex){        index++;//如果要构建的结点是个虚结点,那就不用构建左子树和右子树了,直接returnif(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(),"先序构建");    }}
最后运行程序,构建出来的结果如下图所示。

d4cb4102eb888a0df869d2fdcb9e1506.png

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

609e74775fbc13bf411a4f21838eab5b.png
声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号