第三章:二叉树问题(七)

二叉树节点间的最大距离问题

【题目】

从二叉树的节点A出发,可以向上或者向下走,但沿途的节点只能经过一次,当到达节点B时,路径上的节点数叫作A到B的距离。

比如,图3-43所示的二叉树,节点4和节点2的距离为2,节点5和节点6的距离为5。给定一棵二叉树的头节点head,求整棵树上节点间的最大距离。


【要求】

如果二叉树的节点数为N,时间复杂度要求为O(N)。

【难度】

尉     ★★☆☆

【解答】

本题解法的整体过程为树形dp套路,请读者先阅读本书“找到二叉树中的最大搜索二叉子树”问题了解这个套路,本题是这个套路的再一次展示。首先本题对于树形dp套路前提是满足的:依次求出每一个节点为头节点的子树上的最大距离,那么最终答案一定在其中。

树形dp套路第一步:以某个节点X为头节点的子树中,分析答案来自哪些可能性,并且这种分析是以X的左子树、X的右子树和X整棵树的角度来考虑可能性的。

可能性一:以X为头节点的子树,最大距离可能是左子树上的最大距离。

可能性二:以X为头节点的子树,最大距离可能是右子树上的最大距离。

可能性三:以X为头节点的子树,最大距离可能是从X的左子树离X最远的节点,先到达X,然后走到X的右子树离X最远的节点。也就是左子树高度+右子树高度+1。

树形dp套路第二步:根据第一步的可能性分析,列出所有需要的信息。左子树和右子树都需要知道自己这棵子树上的最大距离,以及高度这两个信息。

树形dp套路第三步:根据第二步信息汇总。定义信息如ReturnType类所示。

 

	public class ReturnType {
		public int maxDistance;
		public int height;

		public ReturnType(int maxDistance, int height) {
			this.maxDistance = maxDistance;
			this.height = height;
		}
	}

树形dp套路第四步:设计递归函数。递归函数是处理以X为头节点的情况下的***括设计递归的base case、默认直接得到左树和右树的所有信息,以及把可能性做整合,并且也要返回第三步的信息结构这四个小步骤。本题的递归实现请看以下的process方法,主函数是以下的getMaxDistance方法。

 

	public ReturnType process(Node head) {
		if (head == null) {
			return new ReturnType(0, 0);
		}
		ReturnType leftData = process(head.left);
		ReturnType rightData = process(head.right);
		int height = Math.max(leftData.height, rightData.height) + 1;
		int maxDistance = Math.max(leftData.height + rightData.height + 1,
				Math.max(leftData.maxDistance, rightData.maxDistance));
		return new ReturnType(maxDistance, height);
	}


	public int getMaxDistance(Node head) {
		return process(head).maxDistance;
	}


派对的最大快乐值

【题目】

员工信息的定义如下:

	class Employee {
		public int happy; // 这名员工可以带来的快乐值
		List<Employee> subordinates; // 这名员工有哪些直接下级
	}

公司的每个员工都符合Employee类的描述。整个公司的人员结构可以看作是一棵标准的、没有环的多叉树。树的头节点是公司唯一的老板,除老板外,每个员工都有唯一的直接上级。叶节点是没有任何下属的基层员工(subordinates列表为空),除基层员工外,每个员工都有一个或多个直接下级。

这个公司现在要办party,你可以决定哪些员工来,哪些员工不来。但是要遵循如下规则。

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

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

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

给定一个头节点boss,请返回派对的最大快乐值。

【要求】

如果以boss为头节点的整棵树有N个节点,请做到时间复杂度为O(N)。

【难度】

尉 ★★☆☆

【解答】

假设以X为头节点的整棵树如图3-44所示。


X有a、b、c三个直接下级,a、b、c再往下一级的关系在图中已经省略。现在分析以X为头节点的整棵树,最大快乐值如何得到。情况只有两种,一种为X来的情况下,整棵树的最大快乐值,记为yes_X_max;另一种为X不来的情况下,整棵树的最大快乐值,记为no_X_max。下面分别进行分析。

yes_X_max。在X来的情况下,派对一定会累加X的快乐值,记为X_happy,同时在这种情况下,a、b、c都不能来。假设以a为头节点的整棵树,在a不来情况下的最大快乐值记为no_a_max;以b为头节点的整棵树,在b不来的情况下的最大快乐值记为no_b_max;以c为头节点的整棵树,在c不来情况下的最大快乐值记为no_c_max。那么yes_X_max的值如下:

yes_X_max = X_happy + no_a_max + no_b_max + no_c_max

no_X_max。在X不来的情况下,派对无法累加X的快乐值,同时在这种情况下,a、b、c谁来不来都可以。假设以a为头节点的整棵树,在a不来情况下的最大快乐值记为no_a_max,在a来情况下的最大快乐值记为yes_a_max;以b为头节点的整棵树,在b不来情况下的最大快乐值记为no_b_max,在b来情况下的最大快乐值记为yes_b_max;以c为头节点的整棵树,在c不来情况下的最大快乐值记为no_c_max,在c来情况下的最大快乐值记为yes_c_max。那么no_X_max的值如下:

 

no_X_max = Max { no_a_max, yes_a_max } + Max { no_b_max, yes_b_max} + Max{ no_c_max, yes_c_max}

 

也就是说,某一个下级节点来还是不来,要看这个下级节点来还是不来的两种情况下,哪一种获得的收益最多。

yes_X_max和no_X_max哪个大,哪个就是X为头节点的整棵树的最大快乐值。

上面的分析说明中,以X为头节点的整棵树,需要以直接下级为头节点的每一棵子树,都给出子树的头节点(a、b、c)来还是不来两种情况下的最大收益。整个过程明显是一个递归过程。递归过程的返回值结构如ReturnData类所示。

	// 每棵树处理完之后的返回值类型
	public class ReturnData {
		public int yesHeadMax; // 树的头节点来的情况下,整棵树的最大收益
		public int noHeadMax; // 树的头节点不来的情况下,整棵树的最大收益
		public ReturnData(int yesHeadMax, int noHeadMax) {
			this.yesHeadMax = yesHeadMax;
			this.noHeadMax = noHeadMax;
		}
	}

整个递归过程如process方法所示。

	// 该函数处理以X为头节点的树,并且返回X来和不来两种情况下的最大快乐值
	// 所以返回值的类型为ReturnData类型
	public static ReturnData process(Employee X) {
	    int yesX = X.happy;// X来的情况下,一定要累加上X自己的快乐值
	    int noX = 0;// X不来的情况下,不累加上X自己的快乐值
	    if (X.subordinates.isEmpty()) { // 如果X没有直接下属,说明是基层员工,直接返回即可
		return new ReturnData(yesX, noX);
	    } else { // 如果X有直接下属,就按照题目的分析来
		// 枚举X的每一个直接下级员工next
		for (Employee next : X.subordinates) {
		    // 递归调用process,得到以next为头节点的子树,
		    //在next来和不来两种情况下分别获得的最大收益
		    ReturnData subTreeInfo = process(next);
		    yesX += subTreeInfo.noHeadMax; // 见书中yes_X_max的分析
		    noX += Math.max(subTreeInfo.yesHeadMax, 
				subTreeInfo.noHeadMax);// 见书中no_X_max的分析
		}
		return new ReturnData(yesX, noX);
	    }
	}

理解了上面的分析之后,我们看以boss为头节点的整棵树的答案怎么得到。毫无疑问,以boss为头节点的整棵树分两种情况,boss来的情况下整棵树的最大快乐值和boss不来的情况下整棵树的最大快乐值,最大的那个就是我们想要的答案。整个解法的主方法请看如下的getMaxHappy方法。

 

	public int getMaxHappy(Employee boss) {
		ReturnData allTreeInfo = process(boss);
		return Math.max(allTreeInfo.noHeadMax, allTreeInfo.yesHeadMax);
	}


通过先序和中序数组生成后序数组

【题目】

已知一棵二叉树所有的节点值都不同,给定这棵树正确的先序和中序数组,不要重建整棵树,而是通过这两个数组直接生成正确的后序数组。

【难度】

士     ★☆☆☆

【解答】

举例说明生成后序数组的过程,假设pre=[1,2,4,5,3,6,7],in=[4,2,5,1,6,3,7]。

1.根据pre和in的长度,生成长度为7的后序数组pos,按以下规则从右到左填满pos。

2.根据[1,2,4,5,3,6,7]和[4,2,5,1,6,3,7],设置pos[6]=1,即先序数组最左边的值。根据1,把in划分成[4,2,5]和[6,3,7],pre中1的右边部分根据这两部分等长划分出[2,4,5]和[3,6,7]。[2,4,5]和[4,2,5]一组,[3,6,7]和[6,3,7]一组。

3.根据[3,6,7]和[6,3,7],设置pos[5]=3,再次划分出[6](来自[3,6,7])和[6](来自[6,3,7])一组,[7](来自[3,6,7])和[7](来自[6,3,7])一组。

4.根据[7]和[7],设置pos[4]=7。

5.根据[6]和[6],设置pos[3]=6。

6.根据[2,4,5]和[4,2,5],设置pos[2]=2,再次划分出[4](来自[2,4,5])和[4](来自[4,2,5])一组,[5](来自[[2,4,5])和[5](来自[4,2,5])一组。

7.根据[5]和[5],设置pos[1]=5。

8.根据[4]和[4],设置pos[0]=4。

如上过程简单总结为:根据当前的先序和中序数组,设置后序数组最右边的值,然后划分出左子树的先序、中序数组,以及右子树的先序、中序数组,先根据右子树的划分设置好后序数组,再根据左子树的划分,从右边到左边依次设置好后序数组的全部位置。

具体过程请参看如下代码中的getPosArray方法。

 

	public int[] getPosArray(int[] pre, int[] in) {
		if (pre == null || in == null) {
			return null;
		}
		int len = pre.length;
		int[] pos = new int[len];
		HashMap<Integer, Integer> map = new HashMap<Integer, Integer>();
		for (int i = 0; i < len; i++) {
			map.put(in[i], i);
		}
		setPos(pre, 0, len - 1, in, 0, len - 1, pos, len - 1, map);
		return pos;
	}

	// 从右往左依次填好后序数组s
	// si为后序数组s该填的位置
	// 返回值为s该填的下一个位置
	public int setPos(int[] p, int pi, int pj, int[] n, int ni, int nj,
			int[] s, int si, HashMap<Integer, Integer> map) {
		if (pi > pj) {
			return si;
		}
		s[si--] = p[pi];
		int i = map.get(p[pi]);
		si = setPos(p, pj - nj + i + 1, pj, n, i + 1, nj, s, si, map);
		return setPos(p, pi + 1, pi + i - ni, n, ni, i - 1, s, si, map);
	}


统计和生成所有不同的二叉树

【题目】

给定一个整数N,如果N<1,代表空树结构,否则代表中序遍历的结果为{1,2,3,…,N}。请返回可能的二叉树结构有多少。

例如,N=-1时,代表空树结构,返回1;N=2时,满足中序遍历为{1, 2}的二叉树结构只有如图3-45所示的两种,所以返回结果为2。


进阶:N的含义不变,假设可能的二叉树结构有M种,请返回M个二叉树的头节点,每一棵二叉树代表一种可能的结构。

【难度】

尉      ★★☆☆

【解答】

如果中序遍历有序且无重复值,则二叉树必为搜索二叉树。假设num(a)代表a个节点的搜索二叉树有多少种可能,再假设序列为{1,…,i,…,N},如果以1作为头节点,1不可能有左子树,故以1作为头节点有多少种可能的结构,完全取决于1的右子树有多少种可能结构,1的右子树有N-1个节点,所以有num(N-1)种可能。

如果以i作为头节点,i的左子树有i-1个节点,所以可能的结构有num(i-1)种,右子树有N-i个节点,所以有num(N-i)种可能。故以i为头节点的可能结构有num(i-1)´num(N-i)种。

如果以N作为头节点,N不可能有右子树,故以N作为头节点有多少种可能,完全取决于N的左子树有多少种可能,N的左子树有N-1个节点,所以有num(N-1)种。

把从1到N分别作为头节点时,所有可能的结构加起来就是答案,可以利用动态规划来加速计算的过程,从而做到O(N2)的时间复杂度。

具体请参看如下代码中的numTrees方法。

 

	public int numTrees(int n) {
		if (n < 2) {
			return 1;
		}
		int[] num = new int[n + 1];
		num[0] = 1;
		for (int i = 1; i < n + 1; i++) {
			for (int j = 1; j < i + 1; j++) {
				num[i] += num[j - 1] * num[i - j];
			}
		}
		return num[n];
	}


进阶问题与原问题的过程其实很类似。如果要生成中序遍历是{a…b}的所有结构,就从a开始一直到b,枚举每一个值作为头节点,把每次生成的二叉树结构的头节点都保存下来即可。假设其中一次是以i值为头节点的(aib),以i为头节点的所有结构按如下步骤生成。

1.用{a…i-1}递归生成左子树的所有结构,假设所有结构的头节点保存在listLeft链表中。

2.用{a…i+1}递归生成右子树的所有结构,假设所有结构的头节点保存在listRight链表中。

3.在以i为头节点的前提下,listLeft中的每一种结构都可以与listRight中的每一种结构构成单独的结构,且和其他任何结构都不同。为了保证所有的结构之间不互相交叉,所以对每一种结构都复制出新的树,并记录在总的链表res中。

具体过程请参看如下代码中的generateTrees方法。

	public List<Node> generateTrees(int n) {
		return generate(1, n);
	}

	public List<Node> generate(int start, int end) {
		List<Node> res = new LinkedList<Node>();
		if (start > end) {
			res.add(null);
		}
		Node head = null;
		for (int i = start; i < end + 1; i++) {
			head = new Node(i);
			List<Node> lSubs = generate(start, i - 1);
			List<Node> rSubs = generate(i + 1, end);
			for (Node l : lSubs) {
				for (Node r : rSubs) {
					head.left = l;
					head.right = r;
					res.add(cloneTree(head));
				}
			}
		}
		return res;
	}

	public Node cloneTree(Node head) {
		if (head == null) {
			return null;
		}
		Node res = new Node(head.value);
		res.left = cloneTree(head.left);
		res.right = cloneTree(head.right);
		return res;
	}

统计完全二叉树的节点数

【题目】

给定一棵完全二叉树的头节点head,返回这棵树的节点个数。

【要求】

如果完全二叉树的节点数为N,请实现时间复杂度低于O(N)的解法。

【难度】

尉 ★★☆☆

【解答】

遍历整棵树当然可以求出节点数,但这肯定不是最优解法,本书不再详述。

如果完全二叉树的层数为h,本书的解法可以做到时间复杂度为O(h2),具体过程如下:

1.如果head==null,说明是空树,直接返回0。

2.如果不是空树,就求树的高度,求法是找到树的最左节点看能到哪一层,层数记为h

3.这一步是求解的主要逻辑,也是一个递归过程,记为bs(node,l,h),node表示当前节点,l表示node所在的层数,h表示整棵树的层数是始终不变的。bs(node,l,h)的返回值表示以node为头节点的完全二叉树的节点数是多少。初始时,node为头节点head,l为1,因为head在第1层,一共有h层始终不变。那么这个递归的过程可以用两个例子来说明,如图3-46和图3-47所示。



找到node右子树的最左节点,如果像图3-47的例子一样,发现它能到达最后一层,即h==4层。此时说明node的整棵左子树都是满二叉树,并且层数为h-l层,一棵层数为h-l的满二叉树,其节点数为2h-1-1个。如果加上node节点自己,那么节点数为2^(h-1)-1+1==2^(h-1)个。此时如果再知道node右子树的节点数,那么以node为头节点的完全二叉树上到底有多少个节点就求出来了。那么node右子树的节点数到底是多少呢?就是bs(node.right,l+1,h)的结果,递归去求即可。最后整体返回2^(h-1)+bs(node.right,l+1,h)。

找到node右子树的最左节点,如果像图3-47的例子一样,发现它没有到达最后一层,说明node的整棵右子树都是满二叉树,并且层数为h-l-1层,一棵层数为h-l-1的满二叉树,其节点数为2h-l-1-1个。如果加上node节点自己,那么节点数为2^(h-l-1)-1+1==2^(h-l-1)个。此时如果再知道node左子树的节点数,那么以node为头节点的完全二叉树上到底有多少个节点就求出来了。node左子树的节点数到底是多少呢?就是bs(node.left,l+1,h)的结果,递归去求即可,最后整体返回2^(h-l-1)+bs(node.left,l+1,h)。

全部过程请参看如下代码中的nodeNum方法。

 

	public int nodeNum(Node head) {
		if (head == null) {
			return 0;
		}
		return bs(head, 1, mostLeftLevel(head, 1));
	}

	public int bs(Node node, int l, int h) {
		if (l == h) {
			return 1;
		}
		if (mostLeftLevel(node.right, l + 1) == h) {
			return (1 << (h - l)) + bs(node.right, l + 1, h);
		} else {
			return (1 << (h - l - 1)) + bs(node.left, l + 1, h);
		}
	}

	public int mostLeftLevel(Node node, int level) {
		while (node != null) {
			level++;
			node = node.left;
		}
		return level - 1;
	}

每一层只会选择一个节点node进行bs的递归过程,所以调用bs函数的次数为O(h)。每次调用bs函数时,都会查看node右子树的最左节点,所以会遍历O(h)个节点,整个过程的时间复杂度为O(h2)。

程序员代码面试指南 文章被收录于专栏

<p> 本专刊为左程云书《程序员代码面试指南》前三章,已获得官方授权,想看书中更多内容可去购买书籍查看。 同时点击可查看牛客算法笔面试真题精讲班,每周由左老师讲解经典高频校招题目:https://www.nowcoder.com/courses/cover/live/350 本专刊购买后即可解锁所有章节,故不可以退换哦~ </p> <p> <br /> </p>

全部评论
你居然还会写代码-=。
1 回复 分享
发布于 2019-12-30 15:54

相关推荐

Aki-Tomoya:窝趣,人家这是先富带动后富,共同富裕了属于是
投递英伟达等公司9个岗位
点赞 评论 收藏
分享
点赞 评论 收藏
分享
评论
1
1
分享

创作者周榜

更多
牛客网
牛客企业服务