关于二叉树的实现,常见的大概有三种实现方法:
- 顺序存储:采用数组来记录二叉树的所有节点
- 二叉链表存储: 每个节点保留一个left,right域,指向左右孩子
- 三叉链表存储: 每个节点保留一个left, right, parent域,指向左右孩子和父亲
根据满二叉树的特性,第i层的节点数量为2^(i-1),对于一个深度为n的二叉树,节点数最多为2^n - 1。因此,只需要定义一个长度为2^n-1的数组即可定义出深度为n的二叉树。
注意:对于非完全二叉树,数组中必然会存在空元素的情况,这样情况下空间浪费比较严重。因此仅建议满二叉树使用顺序存储来实现,以便实现紧凑存储和高效访问。
顺序存储的实现
- 使用一个数组来存储所有节点
- 按数组下标进行存储,根节点存储在下标0处,其左孩子存储于下标2*0+1,右孩子存储于下标2*0+2 …依次类型。
- 下标为i的节点,左右孩子存储于下标2*i+1与2*i+2
简易代码:
public class OrderBinaryTree<T> {
// 二叉树所有节点的顺序存储的数组
public Object[] datas;
// 二叉树的规模最大值为2的deep次方-1
public int arraySize;
// 树的深度
public int deep;
// 默认深度
public static int DEFAULT_DEEP = 8;
public OrderBinaryTree() {
this.deep = DEFAULT_DEEP;
this.arraySize = (int) Math.pow(2, deep) - 1;
this.datas = new Object[arraySize];
}
public OrderBinaryTree(int deep) {
this.deep = deep;
this.arraySize = (int) Math.pow(2, deep) - 1;
this.datas = new Object[arraySize];
}
public OrderBinaryTree(int deep, T root) {
this(deep);
this.datas[0] = root;
}
/**
* 为指定的节点添加子节点
*
* @param index
* 该节点的索引
* @param data
* 被添加为子节点的索引
* @param left
* 是否为左孩子
* @throws Exception
**/
public void add(int index, T data, Boolean left) throws Exception {
int needSize = left? 2 * index + 1: 2 * index + 2;
if (needSize >= datas.length) {
throw new Exception("数组越界");
}
if (left) {
datas[2 * index + 1] = data;
} else {
datas[2 * index + 2] = data;
}
}
/**
* 为指定的节点添加子节点
*
* @param index
* 该节点的索引
* @return 子节点的索引
**/
public int getLeft(int index) throws Exception {
if (index >= 0 && datas.length > 2 * index + 1) {
return 2 * index + 1;
}
return -1;
}
/**
* 为指定的节点添加子节点
*
* @param index
* 该节点的索引
* @return 子节点的索引
**/
public int getRight(int index) throws Exception {
if (index >= 0 && datas.length > 2 * index + 2) {
return 2 * index + 2;
}
return -1;
}
}
上述代码实现了一个非常简易的二叉树,当然二叉树还有很多操作上述没有列出来。现在使用上述定义的数据结构来实现一个二叉树实例:
OrderBinaryTree<String> orderBinaryTree = new OrderBinaryTree<String>(3, "a");
orderBinaryTree.add(0, "b", true);
orderBinaryTree.add(0, "c", false);
orderBinaryTree.add(1, "d", true);
orderBinaryTree.add(1, "e", false);
orderBinaryTree.add(2, "f", true);
orderBinaryTree.add(2, "g", false);
上述定义来一个如下面所示的满二叉树:
二叉树遍历
遍历二叉树指的是按某种规律依次访问二叉树的每个节点,对于二叉树的遍历就是将一个非线性结构的二叉树中节点排列在一个线性序列上的过程。
遍历方法:
- 深度优先遍历
- 广度优先遍历
其中广度优先遍历非常简单,就按层遍历,访问第一层,第二层,..对于我们的顺序实现的二叉树来说,底层数组就是其广度优先遍历的结果。现在我们重点介绍深度优先遍历。
深度优先遍历
深度优先遍历分3种(假设L,D,R分别表示左,根,右子树):
- 先序遍历: DLR
- 中序遍历: LDR
- 后序遍历: LRD
一·先序遍历
对于先序遍历,或许你经常会看到如下的递归代码:
public void travPreRecursion() {
return this.preRecursion(0);
}
private List preRecursion(int index) {
List list = new ArrayList();
list.add(datas[index]);
if (2 * index + 1 < arraySize) {
list.addAll(preRecursion(2 * index + 1));// 左儿子
}
if (2 * index + 2 < arraySize) {
list.addAll(preRecursion(2 * index + 2));// 右儿子
}
return list;
}
笔者认为递归算法不仅效率低而且不利于阅读者理解,因此笔者将其修改为循环版
先沿着最左侧通路自顶而下访问沿路节点,再底而上依次遍历这些节点的右节点
public List travPreCirculate() {
int index = 0;
//定义一个辅助栈来完成最左侧通路自顶而下的节点的右节点
Stack<Integer> stack = new Stack<Integer>();
List list = new ArrayList();
try {
while (true) {
//从index开始最左侧通路自顶而下的访问节点,直到底
//并把每个访问到的节点存储在stack
setRightStack(index, stack, list);
//如果一条左子数链路中没有任何一个右子数,则遍历结束
if (stack.empty()) {
break;
}
// 从来临时栈中依次pop出节点,并继续取其右节点
index = stack.pop();
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return list;
}
public void setRightStack(int index, Stack<Integer> stack, List list) {
try {
// 从index开始访问其左子数,并将其所有的右子树全部push到临时栈中
while (index >= 0) {
list.add(datas[index]);
int right = getRight(index);
if (right > 0) {
stack.push(right);
}
index = this.getLeft(index);
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
二·中序遍历
对于中序遍历,与先序遍历类似,你经常会看到如下的递归代码:
public void travPreRecursion() {
return this.preRecursion(0);
}
private List preRecursion(int index) {
List list = new ArrayList();
if (2 * index + 1 < arraySize) {
list.addAll(preRecursion(2 * index + 1));// 左儿子
}
list.add(datas[index]);
if (2 * index + 2 < arraySize) {
list.addAll(preRecursion(2 * index + 2));// 右儿子
}
return list;
}
当然,笔者还是要将其修改为循环模式
沿着最左侧链路,自底而上依次访问每个节点的右子树
public void travInCirculate() {
int index = 0;
Stack<Integer> stack = new Stack<Integer>();
List list = new ArrayList();
while (true) {
//将index节点的左子树全部push到stack中
setLeftStack(index, stack);
if (stack.isEmpty()) {
break;
}
//pop出左子树
index = stack.pop();
list.add(datas[index]);
try {
//取该节点的右孩子,并继续push其所有左子树到栈中
index = this.getRight(index);
} catch (Exception e) { // TODO Auto-generated catch
block e.printStackTrace();
}
}
}
public void setLeftStack(int index, Stack<Integer> stack) {
try {
// 将root开始的所有右子树全部push到临时栈中
while (index >= 0) {
stack.push(index);
index = getLeft(index);
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
三.后序遍历
后序遍历的递归代码:
// 后序遍历(递归)
public void travPostRecursion() {
postRecursion(0);
}
public void postRecursion(int index) {
if (2 * index + 1 < arraySize) {
postRecursion(2 * index + 1);// 左儿子
}
if (2 * index + 2 < arraySize) {
postRecursion(2 * index + 2);// 右儿子
}
System.out.println(datas[index]);
}
与上述一致,改造为循环
先序,中序遍历中,第一位元素一定是某个节点的左孩子。
当后序遍历不一定,后序遍历的首位原型应该是左子树中度最高的一个元素,可能是左孩子也有可能是右孩子
// 后序遍历(循环)
public void travPostCirculate() {
int index = 0;
Stack<Integer> stack = new Stack<Integer>();
List list = new ArrayList();
while (true) {
//从index开始将其左孩子push到栈中
//如果到了最后一个左孩子,该节点存在右孩子,则继续push
setStackForPost(index, stack);
if (stack.empty()) {
break;
}
index = stack.pop();
list.add(datas[index]);
// 获取其右兄弟
int parent;
try {
parent = this.parent(index);
int right = this.getRight(parent);
index = index == right ? -1 : right;
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public void setStackForPost(int index, Stack stack) {
try {
// 将root开始的所有右子树全部push到临时栈中
while (index >= 0) {
stack.push(index);
int pre = index;
index = getLeft(index);
if (index < 0) {
// 当到达最左子树时,如果该最左子树存在右子树,则遍历从该右子树开始
index = this.getRight(pre);
}
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
补充
对于中序编译,也会看到如下写法,思路是一样
// 中序遍历(循环)
public void travInCirculate() {
int index = 0;
Stack<Integer> stack = new Stack<Integer>();
try {
while (true) {
if(index>=0) {
stack.push(index);
index = this.getLeft(index);
}else if(!stack.empty()) {
index = stack.pop();
System.out.println(datas[index]);
index = this.getRight(index);
}else {
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
笔者认为中序遍历是最重要的,一项最基本的操作将是定位中序遍历中某个节点的直接后继节点,在平衡二叉树的等场景中会使用