二叉树算是树结构中最常见也相对简单的一种结构了,平常在查找、排序算法中也常常作为优化工具被使用。这篇文章就记录一下如何多种方式遍历二叉树。
先贴出基本的代码结构:
public class MyTree {
public static void main(String[] args) {
int[] list = { 6, 3, 2, 8, 5, 9, 0, 1, 7, 4 };
TreeNode head = new TreeNode(list[0]);
makeTree(head, list, 1);
// 打印树
System.out.println("树形打印:");
writeTree(head, list.length);
// 前序
System.out.print("前序打印:");
preOrder(head);
System.out.println();
// 中序
System.out.print("中序打印:");
midOrder(head);
System.out.println();
// 后序
System.out.print("后序打印:");
postOrder(head);
System.out.println();
// 前中后序
int[] pre = { 6, 3, 2, 0, 1, 5, 4, 8, 7, 9 };
int[] mid = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int[] post = { 1, 0, 2, 4, 5, 3, 7, 9, 8, 6 };
// 前中序还原二叉树
System.out.println("前中序还原二叉树:");
writeTree(preAndMid(pre, mid), list.length);
// 中后序还原二叉树
System.out.println("中后序还原二叉树:");
writeTree(midAndPost(mid, post), list.length);
}
public static class TreeNode {
private int value;
private TreeNode left;
private TreeNode right;
//省略get、set方法与构造结构
}
/*
* 将顺序表转化为二叉查找树 head 根节点 li 线性表 key 准备插入二叉树的元素位置
*/
public static void makeTree(TreeNode head, int[] li, int key) {
// 递归结束条件
if (key >= li.length) {
return;
}
// 复制头结点向下遍历
TreeNode node = head;
// 准备插入的子结点
TreeNode newNode = new TreeNode(li[key]);
while (node != null) {
if (node.value > newNode.value) {
// 左子树
// 若为空则存放,不为空则继续向下
if (node.left == null) {
node.setLeft(newNode);
break;
}
node = node.left;
} else {
// 右子树
// 若为空则存放,不为空则继续向下
if (node.right == null) {
node.setRight(newNode);
break;
}
node = node.right;
}
}
// 递归下一个元素
makeTree(head, li, key + 1);
}
// 自定义幂
public static int myPower(int count, int p) {
int i = 1;
while (p-- > 0) {
i = i * count;
}
return i;
}
// 自定义对数
public static int log2(int n) {[添加链接描述](https://blog.csdn.net/TiramisuDP/article/details/117334889?spm=1001.2014.3001.5501)
return (int) (Math.log(n) / Math.log(2));
}
}
二叉树的遍历是为了能让仅保存头结点的数据结构展示成更加明朗易懂的文字,在这里我仅贴出实现代码,详情可以去看【算法】Java线性表转化为二叉树与二叉树打印(优化思路过程)。
public static void writeTree4(TreeNode node, int sum) {
// 用队列存储树的节点
Queue<TreeNode> treeQueue = new LinkedList<>();
treeQueue.add(node);
// 得到树的深度
int deep = treeDeep(node);
// 记录剩余元素
int residue = sum - 1;
// 该层元素个数
int round = 1;
// 该层已打印元素个数
int count = 0;
// 定义一个空节点
TreeNode empty = new TreeNode(-1);
while (!treeQueue.isEmpty()) {
for (int i = 0; i < (myPower(2, deep - log2(round * 2)) - 1); i++) {
System.out.print(" ");
}
// 提取队列首个元素
TreeNode n = treeQueue.poll();
if (n.getValue() != -1) {
System.out.print(n.getValue());
} else {
System.out.print("#");
}
for (int i = 0; i < (myPower(2, deep - log2(round * 2))); i++) {
System.out.print(" ");
}
// 左子节点不为空则加入队列
if (n.getLeft() != null) {
treeQueue.add(n.getLeft());
residue--;
} else if (residue > 0) {
treeQueue.add(empty);
}
// 右子节点不为空则加入队列
if (n.getRight() != null) {
treeQueue.add(n.getRight());
residue--;
} else if (residue > 0) {
treeQueue.add(empty);
}
// 该层打印结束后进入下一层打印
if (++count == round) {
System.out.println();
round = round * 2;
count = 0;
}
}
System.out.println();
}
{ 6, 3, 2, 8, 5, 9, 0, 1, 7, 4 }
按照自己、左子节点、右子节点的顺序遍历二叉树。
// 前序遍历二叉树
public static void preOrder(TreeNode node) {
if (node == null) {
return;
}
System.out.print(node.getValue());
preOrder(node.getLeft());
preOrder(node.getRight());
}
按照左子节点、自己、右子节点的顺序遍历二叉树
// 中序遍历二叉树
public static void midOrder(TreeNode node) {
if (node == null) {
return;
}
midOrder(node.getLeft());
System.out.print(node.getValue());
midOrder(node.getRight());
}
按照左子节点、右子节点、自己的顺序遍历二叉树
// 后序遍历二叉树
public static void postOrder(TreeNode node) {
if (node == null) {
return;
}
postOrder(node.getLeft());
postOrder(node.getRight());
System.out.print(node.getValue());
}
根据前中后序遍历得到的序列可以倒过来还原一棵完整的二叉树,而且只需前序或后序其中一个序列以及中序序列就可以还原,因为中序序列可以反映出结点对另一个结点的相对位置,进而可以确定结点在树中的位置。
/*
* 确定code是否在parent的左子树
*/
public static boolean isLeft(int[] mid, int parent, int code) {
int pPlace = 0;
int cPlace = 0;
for (int i = 0; i < mid.length; i++) {
if (mid[i] == parent) {
pPlace = i;
}
if (mid[i] == code) {
cPlace = i;
}
}
return pPlace > cPlace;
}
前序遍历会先将根节点和靠左结点先打印,所以前中序还原就是从左到右放入树。
// 前中序还原二叉树
public static TreeNode preAndMid(int[] pre, int[] mid) {
// 前序首个元素必为树的根节点
TreeNode head = new TreeNode(pre[0]);
// 建立树结点的栈并放入根节点
Stack<TreeNode> treeStack = new Stack<>();
treeStack.add(head);
// 按照前序序列放入树结点
for (int i = 1; i < pre.length; i++) {
TreeNode node = new TreeNode(pre[i]);
// 放入树节点
while (true) {
if (isLeft(mid, treeStack.peek().getValue(), node.getValue())) {
// 当前栈结点的左子树
if (treeStack.peek().getLeft() == null) {
// 为空则放入
treeStack.peek().setLeft(node);
break;
} else {
// 非空则放入栈顶树的左节点
treeStack.add(treeStack.peek().getLeft());
}
} else {
// 当前栈结点的右子树
if (treeStack.peek().getRight() == null) {
// 为空则放入
treeStack.peek().setRight(node);
break;
} else {
// 非空则放入栈顶树的右节点
treeStack.add(treeStack.peek().getRight());
}
}
}
// 清空栈
treeStack.clear();
treeStack.add(head);
}
return head;
}
后序遍历正好与前序遍历相反,所以可以把后序序列从后到前遍历,中后序还原就是从右到左放入树。
// 中后序还原二叉树
public static TreeNode midAndPost(int[] mid, int[] post) {
// 后序最后一个元素必为树的根节点
TreeNode head = new TreeNode(post[post.length - 1]);
// 建立树结点的栈并放入根节点
Stack<TreeNode> treeStack = new Stack<>();
treeStack.add(head);
// 按照前序序列放入树结点
for (int i = post.length - 2; i >= 0; i--) {
TreeNode node = new TreeNode(post[i]);
// 放入树节点
while (true) {
if (isLeft(mid, treeStack.peek().getValue(), node.getValue())) {
// 当前栈结点的左子树
if (treeStack.peek().getLeft() == null) {
// 为空则放入
treeStack.peek().setLeft(node);
break;
} else {
// 非空则放入栈顶树的左节点
treeStack.add(treeStack.peek().getLeft());
}
} else {
// 当前栈结点的右子树
if (treeStack.peek().getRight() == null) {
// 为空则放入
treeStack.peek().setRight(node);
break;
} else {
// 非空则放入栈顶树的右节点
treeStack.add(treeStack.peek().getRight());
}
}
}
// 清空栈
treeStack.clear();
treeStack.add(head);
}
return head;
}
对于二叉树的最大深度查找是比较简单的,用一个递归即可得到:
// 树的深度
public static int treeDeep(TreeNode node) {
if (node == null) {
return 0;
}
return 1 + Math.max(treeDeep(node.left), treeDeep(node.right));
}
而面试中常遇到的是查找二叉树的最小深度,这里可以用两个方法实现:
方法一:递归查找,原理和查找最大深度类似,不过在递归判断时需要加入只存在单子树的条件。
//递归查找最小深度
public static int treeMinDeep(TreeNode root) {
if(root == null){
return 0;
}
if(root.left == null){
return run(root.right)+1;
}
if(root.right == null){
return run(root.left)+1;
}
return Math.min(run(root.left)+1,run(root.right)+1);
}
方法二:层序查找,由于递归查找时几乎会遍历到每一个子结点,多次递归会导致效率低下。根据二叉树的特性,我们可以使用层序遍历,当找到一个左右子树都为空的结点时,当前层数即是最小深度。
public static int treeMinDeep(TreeNode root) {
if(root == null){
return 0;
}
if(root.left == null && root.right == null){
return 1;
}
int deep = 0;
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
while(!queue.isEmpty()){
int length = queue.size();
deep++;
for(int i = 0; i < length; i++){
TreeNode t = queue.poll();
if(t.left == null && t.right == null){
//左右都为空则为最小深度
return deep;
}
if(t.left != null){
queue.add(t.left);
}
if(t.right != null){
queue.add(t.right);
}
}
}
return 0;
}
对于还原二叉树的代码我个人其实是略有不满意的,虽然实现时没有遇到较大困难,但最后的代码还是比较繁琐的,另外在实现时并未考虑不合法输入的情况,加入健壮性分析后的代码肯定更为冗长,若日后有机会优化的话定会及时再做修改。
相关文章:
【算法】Java实现常用查找算法一(顺序查找、对分查找、插值查找、斐波那契查找)
【算法】Java实现常用查找算法二(树表查找、分块查找、哈希查找)
【算法】Java实现常用排序算法一(冒泡排序、选择排序、插入排序、堆排序、快速排序)
【算法】Java实现常用排序算法二(希尔排序、归并排序、计数排序、桶排序、基数排序)
【算法】Java线性表转化为二叉树与二叉树打印(优化思路过程)