前言
最近突然又找回了在学校刷算法题和学习数据结构的乐趣,在学校未能打好这些基础,所以决定在学习技术框架的时候,也同时学习回顾以前的基本功,这些在面试中特别重要,框架可以学,程序无非就是算法+数据结构,技术跟着官方文档可以快速上手,但基本功是个积累的过程,很多公司看重的恰恰是基本功。好了,废话不多说,先开始今天的主角重建二叉树,也就是根据先序遍历和中序遍历还原成一棵完整的二叉树。
基本知识介绍
首先我们得了解一下一些有关二叉树的基本知识。二叉树就是如下图所示,也就是每个根结点最多只有两个子结点,这样自上而下形成的一棵树,就是二叉树,我经常搞混的一个点,老是以为左边孩子要大于右边,但实时满足这种性质的是有序二叉树,这里我们只说基本的二叉树。
下面是一棵二叉树。
先序遍历(前序遍历)
先序遍历的规则是根左右,所以上面的二叉树前序遍历结果是 6 > 3 > 1 > 11 > 8 > 2 > 5 > 7 > 10 > 13 > 12
解释: 因为当前二叉树的根是6,所以先找到的是6,根据根左右的法则,再找左孩子,找到3,注意这里不是直接就取3了,而是以3为根节点的左子树中再按照根左右的法则去遍历,在3这颗子树中,3为根结点,故6之后是3,根找到了,还有左右没找,这时候再遍历3的左子树,也就是根结点为1的子树,再根据法则,取到1,然后再继续,找到11,11没有子结点了,所以1这颗子树左边也就走完了,再走右找到8,这时候对于3这颗子树来说左边走完了,那么下一个结点是右,也就是找到2,这时候对于6这颗树来说,左边所有的已经走完,再走右边,右边按照法则继续去遍历得到的是5 > 7 > 10 > 13 > 12,所以最后的结果是6 > 3 > 1 > 11 > 8 > 2 > 5 > 7 > 10 > 13 > 12
中序遍历
中序遍历的规则是左根右,所以上面的二叉树中序遍历结果是 11 > 1 > 8 > 3 > 2 > 6 > 7 > 5 > > 13 > 10 > 12,中序遍历的和前序类似,只不过法则换成了左根右
后序遍历
后序遍历的规则是左右根,所以上面的二叉树后序遍历结果是 11 > 8 > 1 > 2 > 3 > 7 > 13 > 12 > 10 > 5 > 6,后序遍历的和前序类似,只不过法则换成了左右根
PS:记忆小技巧,观察根的位置,前序的根在前面,中序的在中间,后序的在最后,左永远在右前面。
题目描述
给出前序遍历和中序遍历,还原出一棵完整的二叉树。
input:
前序遍历 6 -> 3 > 1 > 11 > 8 > 2 > 5 > 7 > 10 > 13 > 12
中序遍历 11 > 1 > 8 > 3 > 2 > 6 > 7 > 5 > > 13 > 10 > 12
output:
6
/ \
3 5
/ \ / \
1 2 7 10
/ \ / \
11 8 13 12
重建二叉树相关思路
根据前序遍历的第一个是根结点的原则,去中序遍历中去找到根节点,记录当前位置,因为中序遍历法则是左根右。
1、6 -> 3 > 1 > 11 > 8 > 2 > 5 > 7 > 10 > 13 > 12,第一个是6,所以6是这颗主树的根节点,那么这个时候我们可以拿根结点去中序遍历中找,11 > 1 > 8 > 3 > 2 > 6 > 7 > 5 > 13 > 10 > 12,那么6的左边就是左子树的中序遍历,右边就是右子树的中序遍历。
2、这个时候我们得到左子树的中序遍历11 > 1 > 8 > 3 > 2,那么我们还需要前序遍历才能还原这颗二叉树,那么我们想一下,不管什么遍历,左子树一定会在右子树之前遍历完是不是?,这点可以从我们的法则得出,左在右前面,也就是说,6 -> 3 > 1 > 11 > 8 > 2 > 5 > 7 > 10 > 13 > 12这段前序遍历的右子树遍历结果都集结在末尾,那么这个时候我们要得到左子树的前序遍历是不是就是(根结点的索引位置 + 1,根结点的索引位置 + 左子树的长度),也就是(1,5),最终左子树的前序遍历是3 > 1 > 11 > 8 > 2,剩余的是右子树的前序遍历了。
3、那么我们理一下思路,这时候我们的当前的子问题是不是就是根据前序遍历3 > 1 > 11 > 8 > 2,中序遍历11 > 1 > 8 > 3 > 2,去还原二叉树,有木有发现问题和主问题一样^-^。
4、归纳上面的结果总的来说就是,划分出子树的前序和中序,然后去还原子树,子树的问题中再划分子树的前序和中序,这俄罗斯套娃似的,总之就是我们针对这种问题,也就是子问题和主问题一样的,可以使用递归去解决,关键就在于划分前序和中序出来。
代码实现
看主要核心代码即可,其他的只是为了输出成树状,为了好看 ^-^
package com.cj;
import java.util.Arrays;
import java.util.Scanner;
/**
* 二叉树的重建
*
* 根据前序遍历和中序遍历推测出二叉树
* 6
* / \
* 3 5
* / \ / \
* 1 2 7 10
* / \ / \
* 11 8 13 12
*
* 前序遍历 6 -> 3 > 1 > 11 > 8 > 2 > 5 > 7 > 10 > 13 > 12
* 中序遍历 11 > 1 > 8 > 3 > 2 > 6 > 7 > 5 > > 13 > 10 > 12
* 后序遍历 11 > 8 > 1 > 2 > 3 > 7 > 13 > 12 > 10 > 5 > 6
*
* 推测出二叉树
*/
public class Main {
//前序遍历存储
private static Integer []preOrderStringArray;
//中序遍历存储
private static Integer []middleOrderStringArray;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("请输入二叉树的前序遍历(以逗号隔开):");
//处理输入
preOrderStringArray = Arrays.stream(sc.nextLine().split(",")).map(Integer::valueOf).toArray(Integer[]::new);
System.out.println("请输入二叉树的中序遍历(以逗号隔开):");
//处理输入
middleOrderStringArray = Arrays.stream(sc.nextLine().split(",")).map(Integer::valueOf).toArray(Integer[]::new);
//调用方法获取二叉树
Node node = getTwoBranchTree(0,preOrderStringArray.length - 1,0,middleOrderStringArray.length - 1);
//打印二叉树
show(node);
}
/**
* 核心代码
* @param preI
* @param preJ
* @param middleI
* @param middleJ
* @return
*/
private static Node getTwoBranchTree(int preI,int preJ,int middleI,int middleJ){
//前序遍历的第一个节点是根节点
Node root = new Node(preOrderStringArray[preI]);
//如果当前preI和preJ相等,说明该子树只有一个结点,不需要再遍历左右孩子,直接return
if(preI == preJ){
return root;
}
//中序遍历根所在的索引位置
int index = 0;
//开始分界中序遍历中左右子树范围
for (int i = middleI ; i < middleJ ; i++){
if(root.value == middleOrderStringArray[i]){
index = i;
break;
}
}
/**
* 根据范围去递归,循环当前逻辑
左子树,在前序遍历当中,第一个就是根结点,所以当前根结点的索引是preI,注意index是中序遍历根结点的索引位置
另外根据前序遍历为(`根结点的索引位置 + 1`,`根结点的索引位置 + 左子树的长度`),中序遍历根结点左边的都是左子树的中序遍历
所以左子树的长度是(index - middleI)得出前序的索引范围(preI + 1,preI + (index - middleI))
左子树的中序遍历索引范围是第一个到根结点前一个位置(middleI,index - 1)
*/
root.left = getTwoBranchTree(preI + 1,preI + (index - middleI),middleI,index - 1);
/**
* 根据范围去递归,循环当前逻辑
右子树,在前序遍历当中,左子树右边就是右子树的前序遍历,所以索引范围是(preI + (index - middleI + 1),preJ)
中序遍历是根结点右边开始到结束,所以是(index + 1,middleJ)
*/
root.right = getTwoBranchTree(preI + (index - middleI + 1),preJ,index + 1,middleJ);
return root;
}
// 用于获得树的层数
private static int getTreeDepth(Node root) {
return root == null ? 0 : (1 + Math.max(getTreeDepth(root.left), getTreeDepth(root.right)));
}
// 将计算的格式写入数组中,便于输出
private static void writeArray(Node 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);
}
}
//遍历数组进行打印二叉树
private static void show(Node 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());
}
}
//二叉树的结点定义
static class Node{
//当前结点的值
private int value;
//左孩子
private Node left;
//右孩子
private Node right;
public Node(int value){
this.value = value;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
", left=" + left +
", right=" + right +
'}';
}
}
}
结语
好久没写文章了,学过的东西还是得总结记录,以后一周一定至少发一篇文章,另外两两遍历都可以推出二叉树,这里只是前序+中序,另外如果有任何错误,欢迎大家指正。
数据结构和算法之路慢慢,吾将上下而求索。