面试题6:重建二叉树

题目描述:

输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建出图2.6所示的二叉树并输出它的头结点。二叉树结点的定义如下:

  1. class Node{
  2. int e;
  3. Node left;
  4. Node right;
  5. Node(int x) { e = x; }
  6. }

这道题主要测试你对二叉树性质的了解,非常有代表性,不过在这之前,我们先把二叉树的基本性质捋一遍。

二叉树的基本性质

(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

了解这第四条性质就差不多了。如果想进一步学习二叉树的性质,不妨去找本《离散数学》?

对二叉树遍历的理解

以这棵树为例:

前序遍历:根结点 —> 左子树 —> 右子树(先遍历根节点,然后左右)

  1. //前序遍历以node为根
  2. public void preOrder(Node node) {
  3. //终止条件
  4. if(node == null) {
  5. return;
  6. }
  7. System.out.println(node.e);
  8. preOrder(node.left);
  9. preOrder(node.right);
  10. }

这棵树的前序遍历为:FCADBEHGM

中序遍历:左子树—> 根结点 —> 右子树(在中间遍历根节点)

  1. //中序以node为根的
  2. public void inOrder(Node node) {
  3. //终止条件
  4. if(node == null) {
  5. return;
  6. }
  7. inOrder(node.left);
  8. System.out.println(node.e);
  9. inOrder(node.right);
  10. }

这棵树的中序遍历为:ACBDFHEMG

后序遍历:左子树 —> 右子树 —> 根结点(最后遍历根节点)

  1. //中序以node为根
  2. public void postOrder(Node node) {
  3. //终止条件
  4. if(node == null) {
  5. return;
  6. }
  7. postOrder(node.left);
  8. postOrder(node.right);
  9. System.out.println(node.e);
  10. }

这棵树的后序遍历为:ABDCHMGEF

层序遍历:

  1. //层序遍历
  2. public void levelOrder() {
  3. Queue<Node> q = new LinkedList<>();
  4. q.add(root);
  5. while( !q.isEmpty() ) {
  6. Node cur = q.remove();
  7. System.out.println(cur.e);
  8. if(cur.left != null)
  9. q.add(cur.left);
  10. if(cur.right != null)
  11. q.add(cur.right); //后出
  12. }
  13. }

这棵树层序遍历为:FCEADHGBM

所谓的前序、中序、后序,就是对根节点而言的,左右的遍历顺序不变,前序就是根节点最先遍历,然后左右;中序就是把根节点放在中间遍历;后序则是把根节点放在最后遍历。

中序遍历能够帮助我们很好地确定根节点的位置,这个就有点可怕了,实际面试的时候,不单单会有给出前序遍历和中序遍历的结果让你重建二叉树,还有给出后序遍历和中序遍历结果或者 层序遍历和中序遍历的结果重建二叉树、

其他的遍历组合均不能还原出二叉树的形状,因为无法确认其左右孩子。例如,前序为AB,后序为AB,则无法确认出,B节点是A节点的左孩子还是右孩子,因此无法还原。

题解

思路:通过前序遍历获得根节点的位置,利用根节点将中序序列分为左子树和右子树,然后不断的递归划分即可。

代码中有解释。

  1. private Node buildTree(int[] pre, int preBegin, int preEnd, int[] mid, int midBegin, int midEnd) {
  2. // 前序遍历确第一个元素为根节点
  3. Node root = new Node(pre[preBegin]);
  4. // 用于标记中序遍历结果中根节点的位置
  5. int midRootLocation= 0;
  6. for (int i = midBegin; i <= midEnd; i++) {
  7. if (mid[i] == pre[preBegin]) {
  8. midRootLocation= i;
  9. break;
  10. }
  11. }
  12. if ( midRootLocation - midBegin >= 1 ) {
  13. // 递归得到左子树
  14. // 中序遍历:左子树—> 根结点 —> 右子树(在中间遍历根节点)
  15. // midRootLocation标记了中序遍历结果中根节点的位置,这个位置两端对应根节点左子树和右子树
  16. // midRootLocation- midBegin 表示该根节点左边的节点的数量
  17. // 前序遍历:根结点 —> 左子树 —> 右子树(先遍历根节点,然后左右)
  18. // preBegin 标记了前序遍历结果中根节点的位置
  19. // preBegin + 1 表示该根节点左子树起始位置
  20. // preBegin + (midRootLocation- midBegin) 表示给根节点左子树结束的位置
  21. Node left = buildTree(pre, preBegin + 1, preBegin + (midRootLocation- midBegin),
  22. mid, midBegin, midRootLocation - 1);
  23. root.left = left;
  24. }
  25. if ( midEnd - midRootLocation >= 1 ) {
  26. // 递归得到右子树
  27. // 原理和上面相同
  28. Node right = buildTree(pre, preEnd - (midEnd - midRootLocation) + 1, preEnd,
  29. mid, midRootLocation+ 1, midEnd);
  30. root.right = right;
  31. }
  32. return root;
  33. }

举一反三

(1)给出后序遍历和中序遍历的结果重建二叉树

思路:通过后序获取根节点的位置,然后在中序中划分左子树和右子树,然后递归划分即可。

形式与上面 给出 前序遍历和中序遍历的结果重建二叉树相同

  1. private Node buildTree(int[] mid, int midBegin, int midEnd, int[] end, int endBegin, int endEnd) {
  2. Node root = new Node(end[endEnd]);
  3. int midRootLocation = 0;
  4. for (int i = midEnd; i >= midBegin; i--) {
  5. if (mid[i] == end[endEnd]) {
  6. midRootLocation = i;
  7. break;
  8. }
  9. }
  10. //还原左子树
  11. if (midRootLocation - midBegin >= 1 ) {
  12. Node left = buildTree(mid, midBegin, midRootLocation - 1, end, endBegin, endBegin + (midRootLocation - midBegin) - 1);
  13. root.left = left;
  14. }
  15. //还原右子树
  16. if (midEnd - midRootLocation >= 1 ) {
  17. Node right = buildTree(mid, midRootLocation + 1, midEnd, end, endEnd - (midEnd - midRootLocation), endEnd - 1);
  18. root.right = right;
  19. }
  20. return root;
  21. }

(2)给出层序遍历和中序遍历结果重建二叉树

思路:

(1)根据层序遍历获取根节点的位置

(2)根据(1)将中序划分为左子树和右子树

(3)根据(2)划分出的左子树和右子树分别在层序遍历中获取其对应的层序顺序

(4)然后递归调用划分。

  1. private Node buildTree(int[] mid, int[] level, int midBegin, int midEnd) {
  2. // 层序遍历的第一个结果是 根节点
  3. Node root = new Node(level[0]);
  4. // 用于标记中序遍历的根节点
  5. int midLocation = -1;
  6. for (int i = midBegin; i <= midEnd; i++) {
  7. if (mid[i] == level[0]) {
  8. midLocation = i;
  9. break;
  10. }
  11. }
  12. if (level.length >= 2) {
  13. if (isLeft(mid, level[0], level[1])) {
  14. Node left = buildTree(mid, getLevelArray(mid, midBegin, midLocation - 1, level), midBegin, midLocation - 1);
  15. root.left = left;
  16. if (level.length >= 3 && !isLeft(mid, level[0], level[2])) {
  17. Node right = buildTree(mid, getLevelArray(mid, midLocation + 1, midEnd, level), midLocation + 1, midEnd);
  18. root.right = right;
  19. }
  20. } else {
  21. Node right = buildTree(mid, getLevelArray(mid, midLocation + 1, midEnd, level), midLocation + 1, midEnd);
  22. root.right = right;
  23. }
  24. }
  25. return root;
  26. }
  27. // 功能 : 判断元素是根节点的左子树节点还是右子树节点
  28. // 参数 : target为根节点 isLeft在中序遍历结果中判断children是根节点的左子树还是右子树
  29. // 返回值 : 如果为左子树节点则为true 否则为false
  30. private boolean isLeft(int[] array, int target, int children) {
  31. boolean findC = false;
  32. for (int temp : array) {
  33. if (temp == children) {
  34. findC = true;
  35. } else if (temp == target) {
  36. return findC;
  37. }
  38. }
  39. return false;
  40. }
  41. // 功能: 将中序序列中midBegin与midEnd的元素依次从level中提取出来,保持level中的元素顺序不变
  42. private int[] getLevelArray(int[] mid, int midBegin, int midEnd, int[] level) {
  43. int[] result = new int[midEnd - midBegin + 1];
  44. int curLocation = 0;
  45. for (int i = 0; i < level.length; i++) {
  46. if (contains(mid, level[i], midBegin, midEnd)) {
  47. result[curLocation++] = level[i];
  48. }
  49. }
  50. return result;
  51. }
  52. // 如果array的begin和end位置之间(包括begin和end)含有target,则返回true。
  53. private boolean contains(int[] array, int target, int begin, int end) {
  54. for (int i = begin; i <= end; i++) {
  55. if (array[i] == target) {
  56. return true;
  57. }
  58. }
  59. return false;
  60. }

剑指Offer对答如流系列 - 重建二叉树的更多相关文章

  1. 剑指Offer(四):重建二叉树

    说明: 1.本系列是根据<剑指Offer>这个系列做的一个小笔记. 2.直接动力是因为师兄师姐找工作很难,而且机械出生的我面试算法更难. 3.刚开始准备刷LeetCode.LintCode ...

  2. 【剑指offer】07重建二叉树,C++实现

    本博文是原创博文,转载请注明出处! # 本文为牛客网<剑指offer>刷题笔记 1.题目 # 输入某二叉树的前序遍历和中序遍历的结果,重建二叉树 2.思路(递归) # 前序遍历中,第一个数 ...

  3. 剑指offer 4.树 重建二叉树

    题目描述 输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树.假设输入的前序遍历和中序遍历的结果中都不含重复的数字.例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7, ...

  4. 剑指offer四之重建二叉树

    一.题目: 输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树.假设输入的前序遍历和中序遍历的结果中都不含重复的数字.例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7 ...

  5. 【剑指offer】04 重建二叉树

    题目地址:重建二叉树 题目描述                                    输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树.假设输入的前序遍历和中序遍历的结果中都不 ...

  6. 【剑指 Offer】07.重建二叉树

    题目描述 输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树.假设输入的前序遍历和中序遍历的结果中都不含重复的数字. 示例: 前序遍历 preorder = [3,9,20,15,7] 中序遍历 ...

  7. 【剑指Offer】07. 重建二叉树 解题报告(Java & Python & C++)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客:http://fuxuemingzhu.cn/ 个人微信公众号:负雪明烛 目录 题目描述 解题方法 基本方法:线性查找根节点的位置 方法优 ...

  8. 剑指Offer对答如流系列 - 实现Singleton模式

    目录 面试题2:实现Singleton模式 一.懒汉式写法 二.饿汉式写法 三.枚举 面试题2:实现Singleton模式 题目:设计一个类,我们只能生成该类的一个实例. 由于设计模式在面向对象程序设 ...

  9. 剑指offer题目系列三(链表相关题目)

    本篇延续上一篇剑指offer题目系列二,介绍<剑指offer>第二版中的四个题目:O(1)时间内删除链表结点.链表中倒数第k个结点.反转链表.合并两个排序的链表.同样,这些题目并非严格按照 ...

随机推荐

  1. Visual Studio Team Services and Team Foundation Server官方资料入口

    Team Foundation Server msdn 中文文档入口 Visual Studio Team Services or Team Foundation Server www.visuals ...

  2. 牛客国庆days赛 地铁

    传送门:https://ac.nowcoder.com/acm/problem/52805 我佛了,还能跑边图啊!!! 跑边图不能用vector啦啦啦啦啦 具体也不难,就直接上代码了 #include ...

  3. 003 ansible部署ceph集群

    介绍:在上一次的deploy部署ceph,虽然出了结果,最后的结果并没有满足最初的目的,现在尝试使用ansible部署一遍,看是否会有问题 一.环境准备 ceph1充当部署节点,ceph2,ceph3 ...

  4. C# ref参数

    ref关键字用于将方法内的变量改变后带出方法外.具体我们通过例子来说明: static void Main(string[] args) { int c = 0; Add(1, 2,ref c); C ...

  5. $[NOIp2017]$ 逛公园 $dp$/记搜

    \(Des\) 给定一个有向图,起点为\(1\),终点为\(n\),求和最短路相差不超过\(k\)的路径数量.有\(0\)边.如果有无数条,则输出\(-1\). \(n\leq 10^5,k\leq ...

  6. table 组件

    table 组件了解一下? https://juejin.im/post/5da925bdf265da5b5d205b3f?utm_source=gold_browser_extension

  7. vux中x-input在安卓手机输入框的删除按钮(@on-click-clear-icon)点击没反应

    首先看你自己的的版本好,如果在2.6.9以上,我是在git上找到的解决办法,记录一下,希望可以帮到有需要的小伙伴. 在项目中找 node_modules > vux > x-input & ...

  8. 浅谈Go类型转换之间的那些事

    试着答一答这些问题 s[i]和(for _,v range)的v的区别是什么 var s string = "AB" fmt.Println(reflect.TypeOf(s[0] ...

  9. nginx错误: [error] OpenEvent("Global\ngx_reload_10444") failed (2: The system cannot find the file specified)

    执行nginx -s reload命令: nginx: [error] OpenEvent("Global\ngx_reload_10444") failed (2: The sy ...

  10. bootstrap:导航下拉菜单

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name ...