二叉树的基本概念
二叉树定义:结点的度最多为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)有:
- 结点i的左孩子为2i,右孩子为2i+1。
- 如果i>n/2,则结点i无左孩子(结点i为叶子结点)。
- 如果i>(n-1)/2,则结点i无右孩子。
下面就来证明一下上述性质。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(),"先序构建");
}
}
最后运行程序,构建出来的结果如下图所示。
我仔细验证了一下,是没有问题的。至于根据中、后序遍历结果来构建二叉树的逻辑,由你们来动脑实现吧!