剑指Offer对答如流系列 - 重建二叉树
面试题6:重建二叉树
题目描述:
输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建出图2.6所示的二叉树并输出它的头结点。二叉树结点的定义如下:
class Node{
int e;
Node left;
Node right;
Node(int x) { e = x; }
}
这道题主要测试你对二叉树性质的了解,非常有代表性,不过在这之前,我们先把二叉树的基本性质捋一遍。
二叉树的基本性质
(1)关于树
树是一种数据结构,它是由n(n>=1)个有限结点组成一个具有层次关系的集合。
树的基本术语有:
- 每个结点有零个或多个子结点
- 没有父节点的结点称为
根节点
- 每一个非根结点有且只有一个
父节点
- 除了根结点外,每个子结点可以分为多个不相交的子树。
- 若一个结点有子树,那么该结点称为子树根的
“双亲”
,子树的根称为该结点的“孩子”
。有相同双亲的结点互为“兄弟”
。 - 一个结点的所有子树上的任何结点都是该结点的后裔。从根结点到某个结点的路径上的所有结点都是该结点的祖先。
- 结点的度:结点拥有的子树的数目
- 叶子结点:度为0的结点
- 分支结点:度不为0的结点
- 树的度:树中结点的最大的度
- 层次:根结点的层次为1,其余结点的层次等于该结点的双亲结点的层次加1
- 树的高度:树中结点的最大层次
- 森林:0个或多个不相交的树组成。对森林加上一个根,森林即成为树;删去根,树即成为森林。
“树”是计算机科学中非常重要的一部分内容,它的变形和应用非常之多。比如说,在做通讯录的时候,Android 工程师往往喜欢用字典树
来存取数据,对于Java后端,有的时候我们处理的数据的时候也需要进行区间的查询,比如说去年你的博客在什么时间段关注你的人增长最快啊,一天中自己的博文阅读量最高的时间段啊,可以采用线段树
来实现。在JDK1.8的HashMap的结构中,当元素超过8个的时候,会转为红黑树
等等。
不过对于Java开发而言,二叉树是基础也是接触较多的一种结构。
(2)关于二叉树
二叉树是每个结点最多有两个子树的树结构。它有五种基本形态:
1)空树;
2)只有根的树,即单结点;
3)有根且有一个左子树;
4)有根且有一个右子树;
5)有根且有一个左子树,有一个右子树。
二叉树的性质有:
- 二叉树第i层上的结点数目最多为2i-1(i>=1)
- 深度为k的二叉树至多有2k-1个结点(k>=1)
- 包含n个结点的二叉树的高度至少为(log2n)+1
- 在任意一棵二叉树中,若叶子结点的个数为n0,度为2的结点数为n2,则n0=n2+1
第四条的证明: 因为二叉树中所有结点的度数均不大于2,设n0表示度为0的结点个数,n1表示度为1的结点个数,n2表示度为2的结点个数。
三类结点加起来为总结点个数,于是便可得到:n=n0+n1+n2 (公式1)
由度之间的关系可得第二个等式:n=n0*0+n1*1+n2*2+1 即n=n1+2n2+1 (公式2)
将(公式1)(公式2)组合在一起可得到n0=n2+1
了解这第四条性质就差不多了。如果想进一步学习二叉树的性质,不妨去找本《离散数学》?
对二叉树遍历的理解
以这棵树为例:
前序遍历:根结点 —> 左子树 —> 右子树(先遍历根节点,然后左右)
//前序遍历以node为根
public void preOrder(Node node) {
//终止条件
if(node == null) {
return;
}
System.out.println(node.e);
preOrder(node.left);
preOrder(node.right);
}
这棵树的前序遍历为:FCADBEHGM
中序遍历:左子树—> 根结点 —> 右子树(在中间遍历根节点)
//中序以node为根的
public void inOrder(Node node) {
//终止条件
if(node == null) {
return;
}
inOrder(node.left);
System.out.println(node.e);
inOrder(node.right);
}
这棵树的中序遍历为:ACBDFHEMG
后序遍历:左子树 —> 右子树 —> 根结点(最后遍历根节点)
//中序以node为根
public void postOrder(Node node) {
//终止条件
if(node == null) {
return;
}
postOrder(node.left);
postOrder(node.right);
System.out.println(node.e);
}
这棵树的后序遍历为:ABDCHMGEF
层序遍历:
//层序遍历
public void levelOrder() {
Queue<Node> q = new LinkedList<>();
q.add(root);
while( !q.isEmpty() ) {
Node cur = q.remove();
System.out.println(cur.e);
if(cur.left != null)
q.add(cur.left);
if(cur.right != null)
q.add(cur.right); //后出
}
}
这棵树层序遍历为:FCEADHGBM
所谓的前序、中序、后序,就是对根节点而言的,左右的遍历顺序不变,前序就是根节点最先遍历,然后左右;中序就是把根节点放在中间遍历;后序则是把根节点放在最后遍历。
中序遍历能够帮助我们很好地确定根节点的位置,这个就有点可怕了,实际面试的时候,不单单会有给出前序遍历和中序遍历的结果
让你重建二叉树,还有给出后序遍历和中序遍历结果
或者 层序遍历和中序遍历的结果
重建二叉树、
其他的遍历组合均不能还原出二叉树的形状,因为无法确认其左右孩子。例如,前序为AB,后序为AB,则无法确认出,B节点是A节点的左孩子还是右孩子,因此无法还原。
题解
思路:通过前序遍历获得根节点的位置,利用根节点将中序序列分为左子树和右子树,然后不断的递归划分即可。
代码中有解释。
private Node buildTree(int[] pre, int preBegin, int preEnd, int[] mid, int midBegin, int midEnd) {
// 前序遍历确第一个元素为根节点
Node root = new Node(pre[preBegin]);
// 用于标记中序遍历结果中根节点的位置
int midRootLocation= 0;
for (int i = midBegin; i <= midEnd; i++) {
if (mid[i] == pre[preBegin]) {
midRootLocation= i;
break;
}
}
if ( midRootLocation - midBegin >= 1 ) {
// 递归得到左子树
// 中序遍历:左子树—> 根结点 —> 右子树(在中间遍历根节点)
// midRootLocation标记了中序遍历结果中根节点的位置,这个位置两端对应根节点左子树和右子树
// midRootLocation- midBegin 表示该根节点左边的节点的数量
// 前序遍历:根结点 —> 左子树 —> 右子树(先遍历根节点,然后左右)
// preBegin 标记了前序遍历结果中根节点的位置
// preBegin + 1 表示该根节点左子树起始位置
// preBegin + (midRootLocation- midBegin) 表示给根节点左子树结束的位置
Node left = buildTree(pre, preBegin + 1, preBegin + (midRootLocation- midBegin),
mid, midBegin, midRootLocation - 1);
root.left = left;
}
if ( midEnd - midRootLocation >= 1 ) {
// 递归得到右子树
// 原理和上面相同
Node right = buildTree(pre, preEnd - (midEnd - midRootLocation) + 1, preEnd,
mid, midRootLocation+ 1, midEnd);
root.right = right;
}
return root;
}
举一反三
(1)给出后序遍历和中序遍历的结果重建二叉树
思路:通过后序获取根节点的位置,然后在中序中划分左子树和右子树,然后递归划分即可。
形式与上面 给出 前序遍历和中序遍历的结果重建二叉树相同
private Node buildTree(int[] mid, int midBegin, int midEnd, int[] end, int endBegin, int endEnd) {
Node root = new Node(end[endEnd]);
int midRootLocation = 0;
for (int i = midEnd; i >= midBegin; i--) {
if (mid[i] == end[endEnd]) {
midRootLocation = i;
break;
}
}
//还原左子树
if (midRootLocation - midBegin >= 1 ) {
Node left = buildTree(mid, midBegin, midRootLocation - 1, end, endBegin, endBegin + (midRootLocation - midBegin) - 1);
root.left = left;
}
//还原右子树
if (midEnd - midRootLocation >= 1 ) {
Node right = buildTree(mid, midRootLocation + 1, midEnd, end, endEnd - (midEnd - midRootLocation), endEnd - 1);
root.right = right;
}
return root;
}
(2)给出层序遍历和中序遍历结果重建二叉树
思路:
(1)根据层序遍历获取根节点的位置
(2)根据(1)将中序划分为左子树和右子树
(3)根据(2)划分出的左子树和右子树分别在层序遍历中获取其对应的层序顺序
(4)然后递归调用划分。
private Node buildTree(int[] mid, int[] level, int midBegin, int midEnd) {
// 层序遍历的第一个结果是 根节点
Node root = new Node(level[0]);
// 用于标记中序遍历的根节点
int midLocation = -1;
for (int i = midBegin; i <= midEnd; i++) {
if (mid[i] == level[0]) {
midLocation = i;
break;
}
}
if (level.length >= 2) {
if (isLeft(mid, level[0], level[1])) {
Node left = buildTree(mid, getLevelArray(mid, midBegin, midLocation - 1, level), midBegin, midLocation - 1);
root.left = left;
if (level.length >= 3 && !isLeft(mid, level[0], level[2])) {
Node right = buildTree(mid, getLevelArray(mid, midLocation + 1, midEnd, level), midLocation + 1, midEnd);
root.right = right;
}
} else {
Node right = buildTree(mid, getLevelArray(mid, midLocation + 1, midEnd, level), midLocation + 1, midEnd);
root.right = right;
}
}
return root;
}
// 功能 : 判断元素是根节点的左子树节点还是右子树节点
// 参数 : target为根节点 isLeft在中序遍历结果中判断children是根节点的左子树还是右子树
// 返回值 : 如果为左子树节点则为true 否则为false
private boolean isLeft(int[] array, int target, int children) {
boolean findC = false;
for (int temp : array) {
if (temp == children) {
findC = true;
} else if (temp == target) {
return findC;
}
}
return false;
}
// 功能: 将中序序列中midBegin与midEnd的元素依次从level中提取出来,保持level中的元素顺序不变
private int[] getLevelArray(int[] mid, int midBegin, int midEnd, int[] level) {
int[] result = new int[midEnd - midBegin + 1];
int curLocation = 0;
for (int i = 0; i < level.length; i++) {
if (contains(mid, level[i], midBegin, midEnd)) {
result[curLocation++] = level[i];
}
}
return result;
}
// 如果array的begin和end位置之间(包括begin和end)含有target,则返回true。
private boolean contains(int[] array, int target, int begin, int end) {
for (int i = begin; i <= end; i++) {
if (array[i] == target) {
return true;
}
}
return false;
}
剑指Offer对答如流系列 - 重建二叉树的更多相关文章
- 剑指Offer(四):重建二叉树
说明: 1.本系列是根据<剑指Offer>这个系列做的一个小笔记. 2.直接动力是因为师兄师姐找工作很难,而且机械出生的我面试算法更难. 3.刚开始准备刷LeetCode.LintCode ...
- 【剑指offer】07重建二叉树,C++实现
本博文是原创博文,转载请注明出处! # 本文为牛客网<剑指offer>刷题笔记 1.题目 # 输入某二叉树的前序遍历和中序遍历的结果,重建二叉树 2.思路(递归) # 前序遍历中,第一个数 ...
- 剑指offer 4.树 重建二叉树
题目描述 输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树.假设输入的前序遍历和中序遍历的结果中都不含重复的数字.例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7, ...
- 剑指offer四之重建二叉树
一.题目: 输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树.假设输入的前序遍历和中序遍历的结果中都不含重复的数字.例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7 ...
- 【剑指offer】04 重建二叉树
题目地址:重建二叉树 题目描述 输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树.假设输入的前序遍历和中序遍历的结果中都不 ...
- 【剑指 Offer】07.重建二叉树
题目描述 输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树.假设输入的前序遍历和中序遍历的结果中都不含重复的数字. 示例: 前序遍历 preorder = [3,9,20,15,7] 中序遍历 ...
- 【剑指Offer】07. 重建二叉树 解题报告(Java & Python & C++)
作者: 负雪明烛 id: fuxuemingzhu 个人博客:http://fuxuemingzhu.cn/ 个人微信公众号:负雪明烛 目录 题目描述 解题方法 基本方法:线性查找根节点的位置 方法优 ...
- 剑指Offer对答如流系列 - 实现Singleton模式
目录 面试题2:实现Singleton模式 一.懒汉式写法 二.饿汉式写法 三.枚举 面试题2:实现Singleton模式 题目:设计一个类,我们只能生成该类的一个实例. 由于设计模式在面向对象程序设 ...
- 剑指offer题目系列三(链表相关题目)
本篇延续上一篇剑指offer题目系列二,介绍<剑指offer>第二版中的四个题目:O(1)时间内删除链表结点.链表中倒数第k个结点.反转链表.合并两个排序的链表.同样,这些题目并非严格按照 ...
随机推荐
- geoip ip2region2 with spark
上一篇文章中 我使用 maxmind的免费库开发了一个waterdrop的 插件,测试数据发现,国内的有些市级还是不准确,而且香港并不是显示中国,这就不友好了. 找了一下,发下 ip2region 这 ...
- Codeforces Round #524 (Div. 2) codeforces 1080A~1080F
目录 codeforces1080A codeforces 1080B codeforces 1080C codeforces 1080D codeforces 1080E codeforces 10 ...
- hadoop fs、hadoop dfs与hdfs dfs命令的区别
Hadoop fs:使用面最广,可以操作任何文件系统. hadoop dfs与hdfs dfs:只能操作HDFS文件系统相关(包括与Local FS间的操作),前者已经Deprecated,一般使用后 ...
- 学习python资料
资料链接:https://www.cnblogs.com/wupeiqi/articles/5433893.html
- Spring Cloud的核心成员、以及架构实现详细介绍
什么是微服务 微服务的概念源于Martin Fowler所写的一篇文章“Microservices”. 微服务架构是一种架构模式,它提倡将单一应用程序划分成一组小的服务,服务之间互相协调.互相配合,为 ...
- 【小技巧】在PS中测量图层间的边距
今天学到了一个小技巧,前端切页面时会很方便,就是测量间距margin的. 在ps中,选中某个图层,然后按住ctrl键,再移动鼠标,就可以出现这个图层距其他元素的边距,这个太方便了.在此记录一下,免的以 ...
- $CH5105\ Cookies$ 线性$DP+$贪心
CH 是很有趣的一道题 : ) Sol 第一反应就是f[i][j]表示前i个小朋友分j块饼干的最小怨气值 但是一个孩子所产生的怨气值并不固定,它与其他孩子获得饼干的情况有关 这里可以用到一个贪心,就是 ...
- DecoratorPattern(装饰器模式)-----Java/.Net
装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构.这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装
- A记录都不懂,怎么做开发Leader?
开发 Leader 和一线开发的区别在于:普通一线开发很多时候都只接触业务编码,不需要关注除开发之外的其他事情.但是作为一个开发 Leader,不仅仅需要懂开发层面的东西,还需要懂得运维层面的东西. ...
- docker-网桥
使用桥接网络 在网络方面,桥接网络是链路层设备,它在网络段之间转发流量. 网桥可以是硬件设备或在主机内核中运行的软件设备. Docker而言,桥接网络使用软件桥接器,该软件桥接器允许连接到同一桥接网络 ...