二叉树的遍历
给定树节点:
public static class TreeNode {
public int value;
public TreeNode left;
public TreeNode right;
public TreeNode(int data) {
this.value = data;
}
public String toString() {
return this.val + "";
}
}
以及如下的树结构:
6
/ \
3 8
/ \ / \
1 5 null 10
/ \ / \ / \
null null null null null null
广度优先遍历
使用队列先进先出的规则实现
public static String breadthTravel(TreeNode root) {
if (root == null) {
return null;
}
LinkedList<String> res = new LinkedList<String>();
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.add(root);
while (!queue.isEmpty()) {
TreeNode cur = queue.poll();
res.add(cur.val + "");
if (cur.left != null) {
queue.add(cur.left);
}
if (cur.right != null) {
queue.add(cur.right);
}
}
return String.join(" ", res);
}
得到结果:6 3 8 1 5 10
先序遍历
二叉树的先中后序遍历实际上都是一种深度优先搜索,利用递归即可实现:
public static void order(TreeNode head) {
if (head != null) {
System.out.print(head.value + " ");
order(head.left);
order(head.right);
}
}
当打印在head.left
前,就是先序遍历;当打印在head.left
和head.right
中间,就是中序遍历;而当打印在head.right
之后,就是后序遍历。递归函数的实现很简单,但是很多人不清楚为什么这样就能达到效果,那是因为不了解递归的整个过程。
下面画个图来描述整个递归过程:
从上图的顺序可以发现:每个节点都会来到3次!!
不为空的节点的访问顺序依次是:6, 3, 1, 1, 1, 3, 5, 5, 5, 3, 6, 8, 8, 10, 10, 10, 8, 6
当提取每个节点出现的第一次时就是先序遍历顺序:即 6, 3, 1, 5, 8, 10
同理, 第二次就是中序遍历,即 1, 3, 5, 6, 8, 10
第三次就是后序遍历, 即 1, 5, 3, 10, 8, 6
对应在LeetCode上的题目链接:先序遍历
如果清楚明白递归的访问顺序之后,就可以实现一个先序遍历的非递归版本:
public static String preOrder(TreeNode head) {
if (head != null) {
List<String> res = new LinkedList<String>();
Stack<TreeNode> stack = new Stack<TreeNode>();
while (!stack.isEmpty() || head != null) {
if (head != null) {
res.add(head.value + ""); // 记录当前节点的值
stack.push(head); // 模拟递归当前节点进栈
head = head.left; // 模拟递归preOrder(head.left)
} else {
head = stack.pop(); // 模拟递归preOrder(head.left)到达空值返回
head = head.right; // 模拟递归preOrder(head.right)
}
}
return String.join(" ", res);
}
return null;
}
中序遍历
对应在LeetCode题目链接:中序遍历
按照先序遍历的非递归版,同样可以很容易的实现中序遍历的非递归版本:
public static String inOrder(TreeNode head) {
if (head != null) {
List<String> res = new LinkedList<String>();
Stack<TreeNode> stack = new Stack<TreeNode>();
while (!stack.isEmpty() || head != null) {
if (head != null) {
stack.push(head);
head = head.left;
} else {
head = stack.pop();
res.add(head.value + ""); // 与先序遍历不同的点是记录位置不一样
head = head.right;
}
}
return String.join(" ", res);
}
return null;
}
后序遍历
对应在LeetCode题目链接 :后序遍历
后序遍历的实现与先中序稍微不同,因为非递归无法能够向递归那样来到节点三次,所以只能转变思路,先实现中 -> 右 -> 左顺序,最后将结果逆序即可实现后序遍历!
public static String postOrder(TreeNode head) {
if (head != null) {
LinkedList<String> res = new LinkedList<String>();
Stack<TreeNode> stack = new Stack<TreeNode>();
// 模拟递归顺序中右左,通过链表来使结果逆序
while (!stack.isEmpty() || head != null) {
if (head != null) {
res.addFirst(head.value + ""); // 每次向链表头部添加元素
stack.push(head);
head = head.right; // 先向右
} else {
head = stack.pop();
head = head.left; // 再向左
}
}
return String.join(" ", res);
}
return null;
}
二叉树的存储
我们知道程序是运行在内存中的,一旦程序结束,那么在程序中所构建的结构都将不复存在。为了解决这个问题,就非常要必要将二叉树以某种特定的顺序保存起来,以便下次继续使用。
这就是二叉树的序列化
public static String serialize(TreeNode head){
if (head == null) {
return "#,";
}
String res = head.value + ","; // 保存序列化结果
// 始终存放有数值的节点
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.add(head);
while (!queue.isEmpty()) {
head = queue.poll();
if (head.left != null) {
res += head.left.value + ",";
queue.add(head.left);
} else {
res += "#,";
}
if (head.right != null) {
res += head.right.value + ",";
queue.add(head.right);
} else {
res += "#,";
}
}
return res;
}
以上就是一个根据广度优先顺序存储的序列化方法。其中有两点需要注意下:
- 节点为
null
,依然要记录,上述方法用#
代替。 - 在每个节点的数值后添加
,
,是为了分开各个节点的数值,不然全部合在一起无法区分。
有序列化,与之对应的当然就有反序列化,在平时做题的过程中,例如LeetCode就是通过将给定的数值列表反序列化为二叉树,而它的后台则是通过序列化来比较你提交的答案是否与之相同。
所以下面来实现二叉树的反序列化:
// 将给定的数值转化为树节点
private static TreeNode generateNodeByString(String val) {
if (val.equals("#")) {
return null;
}
return new TreeNode(Integer.valueOf(val));
}
public static TreeNode deserialize(String levelStr) {
String[] values = levelStr.split(",");
int index = 0;
TreeNode head = generateNodeByString(values[index++]);
Queue<TreeNode> queue = new LinkedList<TreeNode>();
if (head != null) {
queue.add(head);
}
TreeNode node = null;
while (!queue.isEmpty()) {
node = queue.poll();
node.left = generateNodeByString(values[index++]);
node.right = generateNodeByString(values[index++]);
if (node.left != null) {
node.left.parent = node;
queue.add(node.left);
}
if (node.right != null) {
node.right.parent = node;
queue.add(node.right);
}
}
return head;
}
上述的实现与LeetCode不同的是:最后一行的空节点依然要在序列化后的字符串表现出来。
打印二叉树
最后做题需要直观的打印我们所构造的二叉树,所以非常有必要实现。
// 用于获得树的层数
public static int getTreeDepth(TreeNode head) {
return head == null ? 0 : (1 + Math.max(getTreeDepth(head.left), getTreeDepth(head.right)));
}
private static void writeArray(TreeNode currNode, int rowIndex, int columnIndex, String[][] res, int treeDepth) {
// 保证输入的树不为空
if (currNode == null)
return;
// 先将当前节点保存到二维数组中
res[rowIndex][columnIndex] = String.valueOf(currNode.value);
// 计算当前位于树的第几层
int currLevel = ((rowIndex + 1) / 2);
// 若到了最后一层,则返回
if (currLevel == treeDepth)
return;
// 计算当前行到下一行,每个元素之间的间隔(下一行的列索引与当前元素的列索引之间的间隔)
int gap = treeDepth - currLevel - 1;
// 对左儿子进行判断,若有左儿子,则记录相应的"/"与左儿子的值
if (currNode.left != null) {
res[rowIndex + 1][columnIndex - gap] = "/";
writeArray(currNode.left, rowIndex + 2, columnIndex - gap * 2, res, treeDepth);
}
// 对右儿子进行判断,若有右儿子,则记录相应的"\"与右儿子的值
if (currNode.right != null) {
res[rowIndex + 1][columnIndex + gap] = "\\";
writeArray(currNode.right, rowIndex + 2, columnIndex + gap * 2, res, treeDepth);
}
}
public static void show(TreeNode root) {
if (root == null)
System.out.println("EMPTY!");
// 得到树的深度
int treeDepth = getTreeDepth(root);
// 最后一行的宽度为2的(n - 1)次方乘3,再加1
// 作为整个二维数组的宽度
int arrayHeight = treeDepth * 2 - 1;
int arrayWidth = (2 << (treeDepth - 2)) * 3 + 1;
// 用一个字符串数组来存储每个位置应显示的元素
String[][] res = new String[arrayHeight][arrayWidth];
// 对数组进行初始化,默认为一个空格
for (int i = 0; i < arrayHeight; i++) {
for (int j = 0; j < arrayWidth; j++) {
res[i][j] = " ";
}
}
// 从根节点开始,递归处理整个树
// res[0][(arrayWidth + 1)/ 2] = (char)(root.val + '0');
writeArray(root, 0, arrayWidth / 2, res, treeDepth);
// 此时,已经将所有需要显示的元素储存到了二维数组中,将其拼接并打印即可
for (String[] line : res) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < line.length; i++) {
sb.append(line[i]);
if (line[i].length() > 1 && i <= line.length - 1) {
i += line[i].length() > 4 ? 2 : line[i].length() - 1;
}
}
System.out.println(sb.toString());
}
}