赞
踩
二叉树的基本概念
二叉树定义:结点的度最多为2
二叉树的五种形态:
特殊的二叉树:
斜树:分为左斜树和右斜树,其实这算是一种线性结构了
满二叉树:如图1所示。
完全二叉树:如图2所示。满二叉树一定是完全二叉树,但完全二叉树不一定是满的。而图3中的三颗树,都不是完全二叉树。
图1
图2
图3
二叉树的性质
在二叉树的第i层上,最多有2i-1个结点;
深度为k的二叉树,最多有2k-1个结点;
对任何一棵二叉树T,如果其叶子结点数为n0,度为2的结点数为n2,则n0=n2+1;
具有n个结点的完全二叉树的深度为|log2n+1|(|x|表示不大于x的最大整数);
对一棵有n个结点的完全二叉树的结点按层序编号(从上到下从左到右,如图4所示),对任一结点i(1≤i≤n)有:
下面就来证明一下上述性质。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 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(),"先序构建"); }}
最后运行程序,构建出来的结果如下图所示。
我仔细验证了一下,是没有问题的。至于根据中、后序遍历结果来构建二叉树的逻辑,由你们来动脑实现吧!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。