左神算法书籍《程序员代码面试指南》——3_08找到二叉树中符合搜索二又树条件的最大拓扑结构【***】

【题目】
给定一棵二叉树的头节点head,已知所有节点的值都不一样,返回其中最大的且符合搜索二叉树条件的最大拓扑结构的大小。

【题解】
方法一:二叉树的节点数为N,时间复杂度为O(N2)的方法。
首先来看这样一个问题,以节点h为头的树中,在拓扑结构中也必须以h为头的情况下,怎么找到符合搜索二叉树条件的最大结构?这个问题有一种比较容易理解的解法,我们先考查h的孩子节点,根据孩子节点的值从h开始按照二叉搜索的方式移动,如果最后能移动到同一个孩子节点上,说明这个孩子节点可以作为这个拓扑的一部分,并继续考查这个孩子节点的孩子节点,一直延伸下去。

我们以题目的例子来说明一下,假设在以12这个节点为头的子树中,要求拓扑结构也必须以12为头,如何找到最多的节点,并且整个拓扑结构是符合二叉树条件的?初始时考
查的节点为12节点的左右孩子,考查队列={10,13}。
考查节点10。最开始时10和12进行比较,发现10应该往12的左边找,于是节点10被找到,节点10可以加入整个拓扑结构,同时节点10的孩子节点4和14加入考查队列,考查队列为{13,4,14}。
考查节点13。13和12进行比较,应该向右,于是节点13被找到,它可以加入整个拓扑结构,同时它的两个孩子节点20和16加入考查队列,4,14,20,16}。考查节点4。4和12比较,应该向左,4和10比较,继续向左,节点4被找到,可以加入整个拓扑结构。同时它的孩子节点2和5加入考查队列,为{14,20,16,2,5}。
考查节点14。14和12比较,应该向右,接下来的查找过程会一直在12的右子树上,依然会找下去,但是节点14不可能被找到。所以它不能加入整个拓扑结构,它的孩子节点也都不能,此时考查队列为(20,16,2,5}。
考查节点20。20和12比较,应该向右,20和13比较,应该向右,节点20同样再也不会被发现了,所以它不能加入整个拓扑结构,此时考查队列为{16,2,5}。
按照如上方法,最后这三个节点(16,2,5)都可以加入拓扑结构,所以我们找到了必须以12为头,且整个拓扑结构是符合二叉树条件的最大结构,这个结构的节点数为7。也就是说,我们根据一个节点的值,根据这个值的大小,从h开始,每次向左或者向右移动,如果最后能移动到原来的节点上,说明该节点可以作为以h为头的拓扑的一部分。
解决了以节点h为头的树中,在拓扑结构也必须以h为头的情况下,怎么找到符合搜索二叉树条件的最大结构?接下来只要遍历所有的二叉树节点,并在以每个节点为头的子树中都求一遍其中的最大拓扑结构,其中最大的那个就是我们想找的结构,它的大小就是我们的返回值。

 1 ///////////////solve01////////////
 2 bool isBSTNode(Node* h, Node* n)//h为根节点,n为判断节点
 3 {
 4 if (h == nullptr)
 5 return false;
 6 if (h == n)
 7 return true;
 8 return isBSTNode(h->val > n->val ? h->l : h->r, n);//进行SBT树判断
 9 }
10 int maxTopo(Node* h, Node* n)
11 {
12 if (h != nullptr && n != nullptr && isBSTNode(h, n))
13 return maxTopo(h, n->l) + maxTopo(h, n->r) + 1;//树的大小
14 return 0;
15 }
16 int bstTopoSize1(Node* root)
17 {
18 if (root == nullptr)
19 return 0;
20 int maxN = maxTopo(root, root);//计算拓扑图节点数的大小
21 maxN = max(maxN, bstTopoSize1(root->l));
22 maxN = max(maxN, bstTopoSize1(root->r));
23 }

对于方法一的时间复杂度分析,我们把所有的子树(N个)都找了一次最大拓扑,每找一次所考查的节点数都可能是O(N)个节点,所以方法一的时间复杂度为O(N2)。
方法二:二叉树的节点数为N、时间复杂度最好为O(N)、最差为O(NlogN)的方法。
先来说明一个对方法二来讲非常重要的概念一—拓扑贡献记录。还是举例说明,请注意题目中以节点10为头的子树,这棵子树本身就是一棵搜索二叉树,那么整棵子树都可以作为以节点10为头的符合搜索二叉树条件的拓扑结构。如果对这个拓扑结构建立贡献记录,是如图3-20所示的样子。
在图3-20中,每个节点的旁边都有被括号括起来的两个值,我们把它称为节点对当前头节点的拓扑贡献记录。第一个值代表节点的左子树可以为当前头节点的拓扑贡献几个节点,第二个值代表节点的右子树可以为当前头节点的拓扑贡献几个节点。比如4(1,1),括号中的第一个1代表节点4的左子树可以为节点10为头的拓扑结构贡献1个节点,第二个1代表节点4的右子树可以为节点10为头的拓扑结构贡献1个节点。同样,我们也可以建立以节点13为头的记录,如图3-21所示。

整个方法二的核心就是如果分别得到了h左右两个孩子为头的拓扑贡献记录,可以快速得到以h为头的拓扑贡献记录。比如图3-20中每一个节点的记录都是节点对以节点10为头的拓扑结构的贡献记录,图3-21中每一个节点的记录都是节点对以节点13为头的拓扑结构的贡献记录,同时节点10和节点13分别是节点12的左孩子和右孩子。那么我们可以快速得到以节点12为头的拓扑贡献记录。在图3-20和图3-21中的所有节点的记录还没有变成节点12为头的拓扑贡献记录之前,是图3-22所示的样子。
如图3-22所示,在没有变更之前,节点12左子树上所有节点的记录和原来一样,都是对节点10负责的;节点12右子树上所有节点的记录也和原来一样,都是对节点13负责的。接下来我们详细展示一下,所有节点的记录如何变更为都对节点12负责,也就是所有节点的记录都变成以节点12为头的拓扑贡献记录。
先来看节点12的左子树,只需依次考查左子树右边界上的节点即可。先考查节点10,因为节点10的值比节点12的值小,所以节点10的左子树原来能给节点10贡献多少个节点,当前就一定都能贡献给节点12,所以节点10记录的第一个值不用改变,同时节点10左子树上所有节点的记录都不用改变。接下来考查节点14,此时节点14的值比节点10要大,说明以节点14为头的整棵子树都不能成为以节点12为头的拓扑结构的左边部分,那么删掉节点14的记录,让它不作为节点12为头的拓扑结构即可,同时只要删掉节点14一条记录,就可以断开节点11和节点15的记录,让节点14的整棵子树都不成为节点12的拓扑结构。后续的右边界节点也无须考查了。进行到节点14这一步,一共删掉的节点数可以直接通过节点14的记录得到,记录为14(1,1),说明节点14的左子树1个,节点14的右子树1个,再加上节点14本身,一共有3个节点。接下来的过程是从右边界的当前节点重回节点12的过程,先回到节点10,此时节点10记录的第二个值应该被修改,因为节点10的右子树上被删掉了3个节点,所以记录由10(3,3)修改为10(3,0),根据这个修改后的记录,节点12记录的第一个值也可以确定了,节点12的左子树可以贡献4个节点,其中3个来自节点10的左子树,还有1个是节点10本身,此时记录变为图3-23所示的样子。

 1 /////////////////solve02/////////////
 2 struct Record
 3 {
 4 int l, r;
 5 Record(int a = 0, int b = 0) :l(a), r(b) {}
 6 };
 7 int modifyMap(Node* n, int v, hash_map<Node*, Record>m, bool s)
 8 {
 9 if (n == nullptr || m.find(n) == m.end())
10 return 0;
11 Record r = m[n];
12 if ((s&&n->val > v) || ((!s) && n->val < v))
13 {
14 m.erase(n);
15 return r.l + r.r + 1;
16 }
17 else
18 {
19 int minus = modifyMap(s ? n->r : n->l, v, m, s);
20 if (s)
21 r.r = r.r - minus;
22 else
23 r.l = r.l - minus;
24 m[n] = r;
25 return minus;
26 }
27 }
28 int posOrder(Node* root, hash_map<Node*, Record>map)
29 {
30 if (root == nullptr)
31 return 0;
32 int ls = posOrder(root->l, map);
33 int rs = posOrder(root->r, map);
34 modifyMap(root->l, root->val, map, true);
35 modifyMap(root->r, root->val, map, false);
36 int lbst = map.find(root->l) == map.end() ? 0 : map[root->l].l + map[root->l].r + 1;
37 int rbst = map.find(root->r) == map.end() ? 0 : map[root->r].l + map[root->r].r + 1;
38 map[root] = Record{ lbst,rbst };
39 return max(lbst + rbst + 1, max(ls, rs));
40 }
41 int bstTopoSize2(Node* root)
42 {
43 hash_map<Node*, Record>map;
44 return posOrder(root, map);
45 }

原文地址:https://www.cnblogs.com/zzw1024/p/11485493.html

时间: 2024-11-05 06:10:03

左神算法书籍《程序员代码面试指南》——3_08找到二叉树中符合搜索二又树条件的最大拓扑结构【***】的相关文章

程序员代码面试指南 IT名企算法与数据结构题目最优解 ,左程云著pdf高清版免费下载

下载地址:网盘下载 备用地址:网盘下载 内容简介  · · · · · ·这是一本程序员面试宝典!书中对IT名企代码面试各类题目的最优解进行了总结,并提供了相关代码实现.针对当前程序员面试缺乏权威题目汇总这一痛点,本书选取将近200道真实出现过的经典代码面试题,帮助广大程序员的面试准备做到万无一失.“刷”完本书后,你就是“题王”!__eol__本书采用题目+解答的方式组织内容,并把面试题类型相近或者解法相近的题目尽量放在一起,读者在学习本书时很容易看出面试题解法之间的联系,使知识的学习避免碎片化

左神算法书籍《程序员代码面试指南》——1_06用栈来求解汉诺塔问题

[问题] 汉诺塔问题比较经典,这里修改一下游戏规则:现在限制不能从最左侧的塔直接移动到最右侧,也不能从最右侧直接移动到最左侧,而是必须经过中间.求当塔有N层的时候,打印最优移动过程和最优移动总步数.例如,当塔数为两层时,最上层的塔记为1,最下层的塔记为2,则打印:Move 1 from left to mid Move 1 from mid to right Move 2 from left to midMove 1 from right to mid Move 1 from mid to le

左神算法书籍《程序员代码面试指南》——2_07将单向链表按某值划分成左边小、中间相等、右边大的形式

Problem:[题目] 给定一个单向链表的头节点head,节点的值类型是整型,再给定一个整数pivot. 实现一个调整链表的函数,将链表调整为左部分都是值小于 pivot的节点, 中间部分都是值等于pivot的节点,右部分都是值大于 pivot的节点. 除这个要求外,对调整后的节点顺序没有更多的要求. 例如:链表9->0->4->5->1,pivot = 3. 调整后链表可以是1->0->4->9->5, 可以是0->1->9->5-&g

左神算法书籍《程序员代码面试指南》——2_12将搜索二叉树转换成双向链表

对二叉树的节点来说,有本身的值域,有指向左孩子和右孩子的两个指针:对双向链表的节点来说,有本身的值域,有指向上一个节点和下一个节点的指针.在结构上,两种结构有相似性,现在有一棵搜索二叉树,请将其转换为一个有序的双向链表. 1 #include <iostream> 2 #include <queue> 3 using namespace std; 4 struct treeNode 5 { 6 int v; 7 treeNode *l, *r; 8 treeNode(int a =

左神算法书籍《程序员代码面试指南》——3_02打印二叉树的边界节点【★★】

[题目]给定一棵二叉树的头节点head,按照如下两种标准分别实现二叉树边界节点的逆时针打印.标准一:1.头节点为边界节点.2.叶节点为边界节点.3.如果节点在其所在的层中是最左或最右的,那么也是边界革点.标准二:1.头节点为边界节点.2.叶节点为边界节点.3.树左边界延伸下去的路径为边界节点.4.树右边界延伸下去的路径为边界节点.例如,如图3 - 2所示的树. 按标准一的打印结果为:1,2,4,7,11,13,14,15,16,12,10,6,3按标准二的打印结果为:1,2,4,7,13,14,

左神算法书籍《程序员代码面试指南》——1_01设计一个有getMin功能的栈

[题目] 实现一个特殊的栈,在实现栈的基本功能的基础上,再实现返回栈中最小元素的操作. [要求] 1.pop.push.getMin操作的时间复杂度都是O(1).2.设计的栈类型可以使用现成的栈结构. [题解] 实现一个特殊的栈,在实现栈的基本功能的基础上,再实现返回栈中最小元素的操作.[要求]1.pop.push.getMin操作的时间复杂度都是O(1).2.设计的栈类型可以使用现成的栈结构. 解题思路: 使用一个辅助栈,里面存的目前栈中的最小值 1 #pragma once 2 #inclu

左神算法书籍《程序员代码面试指南》——1_10最大值减去最小值小于或等于num的子数组数量

[题目]给定数组arr和整数num,共返回有多少个子数组满足如下情况:max(arr[i.j]) - min(arr[i.j]) <= num max(arfi.j])表示子数组ar[ij]中的最大值,min(arli.j])表示子数组arr[i.j]中的最小值.[要求]如果数组长度为N,请实现时间复杂度为O(N)的解法.[题解]使用两个单调栈,一个栈维持从大到小的排序,头部永远是最大值一个维持从小到大的排序,头部永远都是最小值然后使用窗口进行数据移动当右移后,最大最小差超过num时,计算这段数

左神算法书籍《程序员代码面试指南》——2_06判断一个链表是否为回文结构

[题目]给定一个链表的头节点head,请判断该链表是否为回文结构.例如:1->2->1,返回true.1->2->2->1,返回true.15->6->15,返回true.1->2->3,返回false.进阶:如果链表长度为N,时间复杂度达到O(N),额外空间复杂度达到O(1).[题解]方法一:遍历一遍链表,将数据压入栈中然后再遍历一遍链表与栈的弹出数据对比方法二:使用快慢指针,将链表的前部分压入栈,然后栈数据弹出与链表的后半部分对比方法三:使用快慢指

[程序员代码面试指南]递归和动态规划-机器人达到指定位置方法数(一维DP待做)

题目描述 一行N个位置1到N,机器人初始位置M,机器人可以往左/右走(只能在位置范围内),规定机器人必须走K步,最终到位置P.输入这四个参数,输出机器人可以走的方法数. 解题思路 DP 方法一:时间复杂度O(NK),空间复杂度O(NK) 方法二:时间复杂度O(NK),空间复杂度O(N) 方法一代码 //ans=walk(N,M,K,P); public static int walk(int N,int cur,int rest,int P) { int[][] dp=new int[rest+