二叉树简介
百度百科
在计算机科学中,二叉树是每个结点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)。二叉树常被用于实现二叉查找树和二叉堆。
一棵深度为k,且有2^k-1个节点的二叉树,称为满二叉树。这种树的特点是每一层上的节点数都是最大节点数。而在一棵二叉树中,除最后一层外,若其余层都是满的,并且最后一层或者是满的,或者是在右边缺少连续若干节点,则此二叉树为完全二叉树。具有n个节点的完全二叉树的深度为floor(log2n)+1。深度为k的完全二叉树,至多有2k-1个叶子节点,至多有2k-1个节点。
相关术语
树的结点(node):包含一个数据元素及若干指向子树的分支;
孩子结点(child node):结点的子树的根称为该结点的孩子;
双亲结点:B 结点是A 结点的孩子,则A结点是B 结点的双亲;
兄弟结点:同一双亲的孩子结点; 堂兄结点:同一层上结点;
祖先结点: 从根到该结点的所经分支上的所有结点
子孙结点:以某结点为根的子树中任一结点都称为该结点的子孙
结点层:根结点的层定义为1;根的孩子为第二层结点,依此类推;
树的深度:树中最大的结点层
结点的度:结点子树的个数
树的度: 树中最大的结点度。
叶子结点:也叫终端结点,是度为 0 的结点;
分枝结点:度不为0的结点;
有序树:子树有序的树,如:家族树;
无序树:不考虑子树的顺序;
二叉树定义
二叉树结点数据结构很简单,包括一个左(孩子)结点,一个右(孩子)结点和一个数据成员。所以简单定义下二叉树的数据结构,如下:
public class BinaryTree<T>{
private T data;// 数据域
public BinaryTree left;// 左子树
public BinaryTree right;// 右子树
private boolean visited;// 是否已被访问
public BinaryTree(T data){
this.data = data;
left = null;
right = null;
visited = false;
}
}
二叉树构建
简单构建
BinaryTree root = new Binary(0);
BinaryTree left = new Binary(1);
BinaryTree right = new Binary(2);
root.left = left;
root.right = right;
代码非常简单,先创建三个深度为1的二叉树,然后把root作为父(双亲)节点,左右子树分别指向left和right。
完全二叉树构建
/**
* 利用完全二叉树特性构建完全二叉树
* 左结点为父节点的两倍,右结点为父节点的两倍多一
* @param array
* @return
*/
public static BinaryTree createCompleteBinaryTree(int [] array) {
List<BinaryTree> list = new ArrayList<BinaryTree>();
//创建所有结点
for (int i = 0; i < array.length; i++) {
list.add(new BinaryTree(array[i]));
}
//连接所有左子树
for (int i = 0; (2 * i + 1) < list.size(); i++) {
list.get(i).left = list.get(2 * i + 1);
}
//连接所有右子树
for (int i = 0; (2 * i + 2) < list.size(); i++) {
list.get(i).right = list.get(2 * i + 2);
}
// 返回根结点
return list.get(0);
}
原理:如果将完全二叉树从根结点开始,层次遍历依次编号0,1,2…,设父结点的编号为i,则左结点的编号为2i+1,右结点的编号为2i+2。
二叉树高度
二叉树的层次即是二叉树的高度。这里的解题思想就是,对于一棵树的高度,就是左子树和右子树的最大高度加上1,而左子树和右子树的高度计算又可以递归调用同样的方法。
public static int deep(BinaryTree t) {
if (t == null) {
return 0;
}
int leftDeep = deep(t.left);
int rightDeep = deep(t.right);
return (leftDeep > rightDeep ? leftDeep : rightDeep) + 1;
}
结束条件为空树,返回高度0;然后分别计算左子树,右子树高度,最后返回较大者的高度加1。
二叉树遍历
二叉树的遍历分为DFS(Depth-First-Search)和BFS(Breadth-First-Search),其中DFS又按根结点的访问顺序分为先序遍历,中序遍历和后序遍历,如先序遍历就是先访问根结点,然后访问孩子结点,孩子结点的访问不分先后。方便起见,我们统一先访问左结点,再访问右结点。
递归遍历
二叉树的遍历完全契合递归的思想,所以用递归来遍历二叉树非常简单而且清晰。
先序遍历
public static void firstorderTraversal(BinaryTree t) {
if (t == null) {
return;
}
// 先序遍历,先输出父结点
System.out.println(t.visit().toString());
// 递归遍历左子树
firstorderTraversal(t.left);
// 递归遍历右子树
firstorderTraversal(t.right);
}
中序遍历
public static void firstorderTraversal(BinaryTree t) {
if (t == null) {
return;
}
firstorderTraversal(t.left);
System.out.println(t.visit().toString());
firstorderTraversal(t.right);
}
后序遍历
public static void firstorderTraversal(BinaryTree t) {
if (t == null) {
return;
}
firstorderTraversal(t.left);
firstorderTraversal(t.right);
System.out.println(t.visit().toString());
}
非递归遍历
非递归遍历使用的是栈数据结构来遍历。因为是深度优先遍历,所以最后肯定需要回溯。又因为我们这个是单向的二叉树,并不保存父结点的引用,而且是从最深处往回回溯,所以很自然的想到了利用栈这种数据结构来保存沿途的父结点。
下面我们看下流程图:
先序遍历
public void firstorderTraversal3() {
// 定义栈用于保存沿途结点
Stack<BinaryTree<T>> stack = new Stack<BinaryTree<T>>();
// 定义当前结点p
BinaryTree<T> p = this;
// 当结点p或栈二者任一不为空时循环
while (p != null || !stack.isEmpty()) {
// 循环,直至取到最左结点
while (p != null) {
System.out.println(p.visit());
// 沿途结点入栈
stack.push(p);
p = p.left;
}
// 到达最左后,出栈,同时获得右结点,找到存在右结点的栈顶结点
while (!stack.isEmpty() && p == null) {
p = stack.pop();
p = p.right;
}
}
}
这里我们的访问是打印visit()方法返回的字符串,所以为BinaryTree类添加一个visit()方法
public T visit(){
T.toString();// 这里不多说了,按需重写toString()即可
visited = true;// 修改访问标识符
}
中序遍历
中序遍历和先序遍历的不同点在于,先序遍历在寻找最左结点过程中,父结点和左结点都会被依次访问,所以可以在第一个while循环里面输出,而中序遍历先访问左结点然后访问是父结点,父结点在第二个while循环中弹出,所以我们可以再第二个while循环里面输出。
/**
* 中序遍历非递归实现 算法:
* 1、先找到最左结点,直到结点为空,沿途结点入栈
* 2、栈不为空,出栈,输出当前结点,判断是否有右结点,如果存在,重复第一步
*
* @param tree
*/
public void inorderTraversal3() {
// 定义栈用于保存沿途结点
Stack<BinaryTree<T>> stack = new Stack<BinaryTree<T>>();
// 定义当前结点p
BinaryTree<T> p = this;
// 当结点p或栈二者任一不为空时循环
while (p != null || !stack.isEmpty()) {
// 循环,直至取到最左结点
while (p != null) {
// 沿途结点入栈
stack.push(p);
p = p.left;
}
// 到达最左后,出栈,同时获得右结点,找到存在右结点的栈顶结点
while (!stack.isEmpty() && p == null) {
p = stack.pop();
System.out.println(p.visit());
p = p.right;
}
}
}
非递归后序遍历
非递归后序遍历应该是二叉树遍历里面最复杂的一个,因为父结点最后才打印,而前面两个算法在访问右结点的时候,父结点的信息已经丢失了,这里我们需要重新思考一下。
public void postorderTraversal2() {
Stack<BinaryTree<T>> stack = new Stack<BinaryTree<T>>();
BinaryTree<T> t = this;
// 循环直到t为空
while (t != null) {
// 引入了访问标记,当左子树不为空并且左子树未被访问过才入栈
if (t.left != null && !t.left.visited) {
stack.push(t);
t = t.left;
// 如果左子树无效,则右子树有效也入栈
} else if (t.right != null && !t.right.visited) {
stack.push(t);
t = t.right;
// 如果左右子树都无效(也可以理解为左右子树都已被访问过了),则输出当前结点,并出栈
} else {
System.out.println(t.visit().toString());
t = stack.isEmpty() ? null : stack.pop();
}
}
}
非递归后序遍历的流程图如下:
广度优先遍历
广度优先遍历和深度优先遍历相同的地方在于都需要存储沿途的父结点,不同点是深度优先是存储纵向关系的结点,而广度优先存储的是横向关系的结点,而对于我们类似链表的数据结构,要存储横向关系一个集合并不能达到要求,所以我们要定义两个集合。一个集合用于保存父结点们,另一个集合用于保存当前需要保存的孩子结点。
相较于深度优先遍历,广度优先遍历具有FIFO性质,所以选用Queue这样的数据结构来保存。
/**
* 二叉树层次遍历
*/
public void breathFirst() {
// 队列1用于保存当前要遍历的父结点
Queue<BinaryTree<T>> q1 = new LinkedList<BinaryTree<T>>();
// 队列2用于保存已经访问过的孩子结点
Queue<BinaryTree<T>> q2 = new LinkedList<BinaryTree<T>>();
// 临时交换变量
Queue<BinaryTree<T>> tempQ = null;
// 根结点入队
q1.add(this);
// 当前结点
BinaryTree<T> p = null;
// 当前结点为空时结束循环
while (!q1.isEmpty()) {
// 出队
p = q1.poll();
System.out.println(p.visit());
// 左子树进队
if (p.left != null) {
q2.add(p.left);
}
// 右子树进队
if (p.right != null) {
q2.add(p.right);
}
/*
* 如果父结点队列遍历完,则交换两个队列
* 也就是当前要访问的队列就是刚才进队的孩子结点
* 而父结点队列废物利用作为空队列利用
*/
if (q1.isEmpty()) {
tempQ = q1;
q1 = q2;
q2 = q1;
}
}
}
文章可能有疏漏之处,欢迎指出,互相学习。