二叉树的递归套路

Posted Mozartto

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了二叉树的递归套路相关的知识,希望对你有一定的参考价值。

二叉树的递归套路

woc...现在在图书馆二楼,脚冻僵了...法克...

好,下面我来讲一下如何打印一个二叉树(不是你们想像的那种写个递归遍历代码两三行)

首先来看一下打印出来的二叉树是什么样子,横着的,每个孩子结点左右有箭头,箭头朝下代表右结点,箭头朝上代表左结点。H代表头结点

打印顺序是右头左。

public static void printTree(Node head) {
   System.out.println("Binary Tree:")
   printInOrder(head, 0, "H", 17);
   System.out.println();
}


//String to参数的意思是当前结点的那个箭头
//height的意思是横着打印的时候结点前面预留的空格长度,空格长度与结点在树中的高度呈正相关
//len的意思是,不管长度是多少,我都搞出一个len长度的字符串打印出来,即统一认为值是17个空间
//我自己占多少位我预留好,前面空多少个空格出来跟我的高度相关
public static void printInOrder(Node head, int height, String to, int len) {
   if(head == null) {
       return;
  }
   printInorder(head.right, height + 1, "v", len);
   
   String val = to + head.value + to;
   int lenM = val.length();         //中间部分数字占的长度
   int lenL = (len - lenM) / 2;     //左侧部分占的长度
   int lenR = len - lenM - lenL;    //右侧部分占的长度
   val = getSpace(lenL) + val + getSpace(lenR);      //我这个值要占的总长度,左+值+右
   System.out.println(getSpace(height * len) + val); //根据高度先在前面输出空格,再在后面补上我这个值(左+值+右)
   //好累。。。不想写注释   //2021.1.21注释:啊这,你也太懒了吧,我现在写注释
   //getSpace函数自己实现
}

题目:二叉树结构如下定义:

Class Node {
   V value;
   Node left;
   Node right;
   Node parent;
}

给你二叉树的某个结点,返回该结点的的后继结点(红黑树就是在普通二叉树上多加了一个指向父结点的指针)

后继结点:中序遍历结果中后面的那个结点,最后一个结点的后继结点是null

普遍的做法:先根据parent指针找到整棵树的头结点,然后根据头结点对整棵树进行中序遍历来寻找结点X的后继结点

不管是先序中序还是后序,本质上都是递归序(上一讲讲过),都是每一个结点到达三次,时间复杂度都是O(N)

假如我们有一种方法,能够直接找到一个结点的后继结点,如果此结点到其后继结点的距离为k,如果能把时间复杂度控制在O(k)那岂不是妙哉!

x的后继结点是其右树上的最左结点

找某个结点的中序遍历中的后继:

一直往上找,如果此结点是上一个结点的右孩子,就继续往上找,直到变成上一个结点的左孩子,上一个结点此时就是我们要找的后继

二叉树的递归套路

好困啊...不想更这个代码了。找中序遍历中的前驱结点也是这个思路,自己写。

下面来讲一个有意思的题:在国外流行了一年多,从微软开始流行,到Facebook等FLAG大厂也喜欢问。

请把一段纸条竖着放在桌子上,然后从纸条的下面向上方对折一次,压出折痕后展开。此时折痕是凹下去的,即折痕突起的方向指向纸条的背面。如果从纸条的下边向上方连续对折两次,压出折痕后展开,此时有三条折痕,从上到下依次是下折痕、下折痕和上折痕。

给定一个输入参数N,代表纸条从下边向上方连续对折N次,请从上到下打印所有折痕的方向。

有一个规律,你每次对折之后,在你前一次对折的折痕上方是凹折痕,下方是凸折痕

所以你想从上到下打印所有折痕,就是下图中那棵树的中序遍历

例如:N = 1时,打印down。N = 2时,打印down down up

二叉树的递归套路

就是一个二叉树的中序遍历,代码很简单,如下:

二叉树的递归套路

二叉树的递归套路

可以解决面试中绝大多数的二叉树问他尤其是树型dp问题,绝大多数大到什么程度,95%以上

本质是利用二叉树的便利性。本质就是在树上做动态规划。

给定一个二叉树的头结点head,返回这颗二叉树是不是平衡二叉树

public static class Info{
   public Boolean isBalanced;
   public int height;
   
   public Info(bollean b, int h) {
       isBalanced = b;
       height = h;
  }
}

//以X为头结点的树
public static Info process2(Node X) {
   if(X == null) {
       return new Info(true, 0); //空树直接返回(空树是平衡二叉树)
  }
   Info leftInfo = process2(X.left);
   Info rightInfo = process2(X.right);
   //左树高度是多少,左树是否平衡;右树高度是多少,右树是否平衡;两个信息都拽在手里
   //接下来只需要再生成出我的两个信息再返回整个函数就结束了
   int height = Math.max(leftInfo.height, rightInfo.height) + 1;
   
   boolean isBalanced = true;
   
   if(!leftInfo.isBalanced || !rightInfo.isBalanced || Math.abs(leftInfo.height - right.height) > 1) {
       isBalanced = false; //左树或右树不平衡,或左右两子树高度差超过1,将当前树改为不平衡
  }
}

二叉树的递归套路

  1. 假设以X结点为头,假设可以向X左树和X右树要任何信息

  2. 在上一步的假设情况下,讨论以X为头结点的树,得到答案的可能性(很重要)

  3. 列出所有可能性后,确定到底需要向左树和右树要什么样的信息

  4. 把左树信息和右树信息求全集,就是任何一颗子树都需要返回的信息S

  5. 递归函数都返回S,每一棵子树都这么要求

  6. 写代码,在代码中考虑如何把左树的信息和右树的信息整合出整棵树的信息


例题:给定一棵二叉树的头结点head,任何两个结点之间都存在距离,返回整棵二叉树的最大距离

两个节点之间的距离就是两个结点之间的结点数

  1. 得到答案的可能性:

    • X左树上离自己最远的点(左树的高度)经过X到X右树上离自己最远的点,即左高+X自己+右高

    • 左树上的最大距离与右树上的最大距离两个取其中之一的max

    1. 最大距离与头结点X无关

    2. 最大距离与头结点X有关

  2. 需要左右树分别提供其高度以及其最大距离

public static int maxDistance2(Node head) {
   return process(head).maxDistance; //主函数只要你整棵树的最大距离
}
public static class Info {
   public int maxDistance;
   public int height;
   
   public info(int dis, int h) {
       maxDistance = dis;
       height = h;  //需要提供的1就这两个信息
  }
}

//返回的是左右子树提供的信息
public static Info process(Node X) {
   if(X == null) {
       return new Info(0, 0);
  }
   Info leftInfo = process(X.left);
   
   Info rightInfo = process(X.right);
   
   //返回完了左右子树的信息,开始加工我自己的信息
   int height = Math.max(leftInfo.height, rightInfo.height) + 1;
   int maxDistance = Math.max(
       Math.max(leftInfo.maxDistance, rightInfo.maxDistance),
       leftInfo.height + rightInfo.height + 1);
   return new Info(maxDistance, height);
   //加工完我自己的信息,就可以把左右子树的信息丢掉了。下一次之间给我的父结点返回我自己的信息。这就是动态规划的本质。跟跑个递归是一样的
}


例题:给定一棵二叉树的头结点head,返回这棵二叉树中最大的二叉搜索子树的头结点

搜索二叉树(BST):整棵树上没有重复值,左子树结点永远比当前结点值小,右子树结点永远比当前结点值大

大分类:

  1. 与X无关

    • 就是你最终找的这棵搜索二叉树不以X为头

    • 就是在左右子树上单独找满足条件的最大二叉搜索树

  2. 与X有关

    • 左树和右树都是搜索二叉树

    • 左max < x,右min > x,这些条件都必须全部满足,有一点破坏,就只存在情况1

  3. 需要左右子树提供的信息

    1. 左子树:最大搜索子树的size、boolean isAllBst、左子树上的最大值max

    2. 右子树:最大搜索子树的size、boolean isAllBst、有子树上的最小值min

  4. 这种属于左树上的要求和右树上的要求不一样的,但是我想一劳永逸,就是不用区分左右子树,所以你需要给我返回四个信息:

    1. 最大搜索子树的size

    2. 整体是否为搜索二叉树

    3. 整棵子树的最大值

    4. 整棵子树的最小值

所以求全集:

public static class Info {
   public boolean isAllBst;
   public int maxSubBstSize;
   public int min;
   public int max;
   
   public Info(boolean isAllBst, int size, int min, int max) {
       this.isAllBst = isAllBst;
       this.maxSubBstSize = size;
       this.min = min;
       this.max = max;
  }
}

public static Info process(Node head) {
   if(X == null) {
       return null;
  }
   Info leftInfo = process(X.left);
   Info rightInfo = process(X.right);
   
   //接下来加工完我自己的四个信息就可以返回了
   
   int min = X.value;
   int max = X.value;
   
   if(leftInfo != null) {
       min =  Math.min(min, leftInfo.min);
       max = Math.max(max, leftInfo.max);
  }
   
    if(rightInfo != null) {
       min =  Math.min(min, rightInfo.min);
       max = Math.max(max, rightInfo.max);
  }
   //上面两个if是在左右子树中分别找出最大值和最小值,令其成为当前树的最大/最小值
   //有关min和max的加工就加工完了
 
   //先说满足条件1,与x无关的情况(不满足条件2则必满足条件1
   int maxSubBstSize = 0;//先默认整棵树的最大二叉搜索树的大小是0
   if(leftInfo != null) {
       maxSubBstSize = leftInfo.maxSubSize;//先把左树的最大搜索子树值赋给当前树
  }
   if(rightInfo != null) {
       maxSubBstSize = Math.max(maxSubBstSize, right.maxSubBstSize);
       //比较右树的最大值和刚才取得的最大值
  }
   
   boolean isAllBst = false;  //满足这个条件证明其满足条件2,否则满足条件1
   //什么样的情况会成立条件2
   if(
   
   //左树整体是搜索二叉树
  (leftInfo == null ? true : leftInfo.isAllBst)  //这个地方容易误解,注意这个括号里只有判断条件,没有赋值语句
       &&
   //右树整体是搜索二叉树
  (rightInfo == null ? true : rightInfo.isAllBst)
  &&
   //左树最大值小于X的值
  (leftInfo == null ? true : leftInfo.max < X.value)
       &&
   //右树最大值大于X的值
  (rightInfo == null ? true : rightInfo.max > X.value)
       //以上四个条件头成立的情况下
       
  ) {
       maxSubBstSize =
          (leftInfo == null ? 0 : leftInfo.maxSubBstSize)
           +
          ( rightInfo == null ? 0 : rightInfo.maxSubBstSize)
           +
           1;
           isAllBst = true;
  }
   return new Info(isAllBst, maxSubBstSize, min, max)ñ
}

例题:派对的最大快乐值问题

//员工的信息定义如下(这是一棵多叉树)
class Employee {
   public int happy; //这名员工可以带来的快乐值
   List<Employee> nexts; //这名员工有哪些直接下级  
}
//规定好不存在一个上级是另一个下级的下级的情况
//每一个人都只有唯一的一个直接上级
//所以整棵树可以写成一个多叉树结构

现在,公司要办party,你可以决定哪些员工来,哪些员工不来,规则:

  1. 如果某个员工来了,那么这个员工的所有直接下级都不能来

  2. 派对整体的快乐值是所有到场员工快乐值的累加

  3. 你的目标是让派对的整体快乐值尽量大

给定一棵多叉树的头结点boss,请返回派对的最大快乐值

假设以X为头结点,可以跟我的子树要信息,得到答案的可能性是什么?——X来和X不来。即不管你底下的树是什么样子,你发请柬的答案都可以分为两类,发了和没发。所有的答案都只能分为这两类

  1. X来

    • 需要获得的信息:x的happy值,以x的子结点为根的树的所有快乐值的总和

  2. X不来

  • 0 +

    max(x子树a来的最大快乐值,a不来情况下的最大快乐值)

    +

    max(x子树b来的最大快乐值,b不来情况下的最大快乐值)

    +

    max(x子树c来的最大快乐值,c不来情况下的最大快乐值)

所以所有的子树需要返回两个信息

  1. 这棵子树头结点来的情况下它能带来的最大快乐值

  2. 这棵子树头结点不来的情况下它能带来的最大快乐值

public static info {
   public int yes; //来时的最大快乐值
   public int no;  //不来时的最大快乐值
   
   public Info(int yes, int no) {
       this.yes = yes;
       this.no = no;  
  }
   //需要返回的信息:这个结点来和不来时能带来的快乐值
}
public static Info process2(Employee x) {
   if(x.nexts.isEmpty()) { //最基层的员工
       return new Info(x.happy, 0);
  }
   int yes = x.happy;
   int no = 0;
   for(Employee next : x.nexts) {
       Info nextInfo = process2(next); //对每一个下级员工,我都调用递归函数要来它的信息
       yes += nextInfo.no; //如果我的头结点来了,对每棵子树,我只能获得它不来状况下的最大快乐值
       no += Math.max(nextInfo.yes, nextInfo.no);
       //如果我不来,就把每棵子树来和不来都做max,累加到我的no上
       //no的时候我的子树可来,可不来
  }
   return new Info(yes, no);
}
 //复杂度是O(n),递归套路一个结点最多经过3次

高度结构化的coding,让你即便是个新手,也能把这个套路写出来。

本篇结束。

以上是关于二叉树的递归套路的主要内容,如果未能解决你的问题,请参考以下文章

一个套路,写出来二叉树的迭代遍历

数据结构与算法之深入解析二叉树的基本算法和递归套路深度实践

144. 二叉树的前序遍历

二叉树的非递归遍历怎么写?

二叉树的非递归遍历

二叉树的遍历(递归+迭代)