题目描述
分析
这个题目leetcode难度定义为中等,也就是说官方认为这不是一道非常难的题目,需要一些技巧性。首先,我们应当具有的基础是,知道树的深度优先遍历(先序,中序,后序)、广度优先遍历(对树来说也可称为层次优先)。题目要求使用常数空间复杂度,因此个人猜测,题目其实是不希望通过层次遍历的方法去解决这个题目的。
这里暂且不讨论层次遍历所需要的队列结构是否属于允许范围内的开销,就递归而言,递归的开销一般是指栈结构。虽然层次遍历也涉及到了递归的概念。但严格上我认为并不算一个递归,这个问题且不深入讨论。我之所以猜测题目不希望用层次遍历解决问题的原因在于,对于层次遍历来说,我们只需要访问每一层的节点列表(层次遍历天然地维护了一颗树的某一层从左到右的节点列表),然后按顺序链接当前层的节点,然后处理下一层即可。
我们主要思考的问题在于,如果采用传统遍历(先序,中序,后序)方法,能不能够得到结果呢?即使不按照层次遍历的顺序,我们能否找到一个节点的链接对象呢?
思考过程
我是个菜鸟,我更乐意于分享我的思考过程,是如何通过已有的基础,去一步一步得到最终的算法的,任何得到答案的过程都不是灵感,而是有内在的积累,也许有时候连自己都不清楚为什么会这么想,但是它一定有个过程在里边。所以我也尽可能想去理清楚这个思路,这样才具有启发性,直接贴一段算法上去,会有些突然,为什么你要这么做,很多算法仿佛是天授旨意,一闭眼一睁眼就知道怎么做了一样,这样积累对于新手来说代价是有些巨大的。
那么回到题目上,最开始我能想到的就是自己画个图,瞎尝试一下,自己尝试手动构建链接,能做出什么样的猜想,取决于自己的经验和积累。
首先我发现一个问题是,对于同一个父亲下的两个结点,链接它总是很容易的。
如图中蓝色的虚线,显而易见的是,就是一个节点的左孩子指向右孩子就可以了。
注意思维是如何受影响的,从这里开始,结合了网上一些其他人的解法,就开始有了不一样的处理方式。因为很多人一定和我一样想到这里了,因此理所当然的想,所有的链接都是通过父节点来处理的。当然通过父亲处理孩子和通过孩子处理自己都是可以的,但是个人认为通过父亲处理孩子所涉及的 思维复杂度 要高一些,因此也更复杂一些。
思维复杂度是我自己起的名字,之前看到过,但是忘了专业名词是什么了,如果有看到的朋友又恰好知道我说的,还望能留言给我。它是一个什么概念呢,就是假设我脑子里在思考问题A,假如与A相关联的概念有B和C,C又依赖于D,在我们对BCD不是驾轻就熟(肌肉记忆,条件反射式推理)的情况下,对于我们思考A都是有负担的。而这个所依赖的概念越多,我们就认为思维复杂度越高
因为可以看到很多用父亲处理孩子的代码,不仅处理自己的孩子,还处理孩子的孩子。因此不论是边界条件还是初始化条件都会判断许久。但这并没有错,我刚开始所做的尝试也是这样的,这是最直观的,条件反射式的思考。
那么核心问题还是在于,不是同一个父亲的链接,该如何处理。
我想当然的继续尝试,这个时候我思考到了一个 二叉树的线索化 如果大家看过严蔚敏《数据结构》这本书的话,应该会多少对这个概念有点印象。线索化大概是做了这么一件事:二叉树的叶子结点的左右孩子是一个空指针,如果保持为空岂不是浪费了?于是这两个结点被利用来链接向了其它结点(某种遍历过程中的后序或前前序结点),这样遍历一个二叉树就不需要用一个递归栈来维护之前的结果了,因为,你可以很容易的通过这些额外的线索指针来进行回溯。
于是我想,这个节点能不能利用呢?
注意看红色的线,对于不能一步得到链接的结点,这个结点的指针能不能在第一次遍历的过程中指向自己的父亲呢?没错,在思考到这里的时候,我的想法是通过两遍遍历来解决整个问题,第一次只处理左右孩子的情况,第二次处理右孩子的情况。但是稍微想了一下,就发现,这没有必要,因为父节点很容易通过递归过程中加个参数来得到。这条红线可能只是多此一举。但是画这条线和不画,确实有不同的区别,因为我发现了一个问题。
看我橙色线框圈住的地方,恰好构成了一个链接,这样就能够找到我们无法处理右节点的链接问题了,只需要经过两次跳转就可以。经过尝试以后发现,对于所有的节点都成立。但是这样做有一个先决条件:处理右节点的时候,自己的父亲节点的链接必须已经链接正确。通过简单模拟递归过程,我发现如果通过先序遍历去做的话,父亲就一定会比子节点先处理,因此一切条件都OK了。
算法
① 先序遍历二叉树,遍历的过程中需要维护当前节点的父亲
② 对于①中的每个节点N和它的父亲P:
- a 若N为空,则直接返回,否则转到b
- b 若P为空,转到d,否则转到c(根结点的单独处理)
- c 若N是P的左孩子,则将N链接到P的右孩子 (N->next = P->Right), 若N是P的右孩子,且P的链接不为空,则将P链接到P的链接的左孩子 (N->next = P->next->left),若N是P的右孩子,且P的链接为空,则跳到d
- d* 递归处理该节点的左右孩子(d过程为递归框架,和①冲突,但写出来,为了表明b过程不执行也不会直接return)
由于每个节点链接域默认为NULL,因此算法只处理可以处理的情况
空间辅助度O(1) (不考虑递归开销)
时间复杂度O(N) N为树的节点数目
代码
class Solution {
public:
void connect(TreeLinkNode *root) {
function<void(TreeLinkNode*, TreeLinkNode*)> makeLink;
makeLink = [&makeLink](TreeLinkNode *root, TreeLinkNode *parent)->void{
if (root == NULL) { return; }
if (parent != NULL) {
if (root == parent->left) {
root->next = parent->right;
} else {
if (parent->next != NULL) {
root->next = parent->next->left;
}
}
}
makeLink(root->left, root);
makeLink(root->right, root);
};
makeLink(root, NULL);
}
};
总结
可以看出,这个题目涉及到的一些知识点
1、二叉树的遍历(先序,中序,后序,层次)
*2、二叉树的线索化
3、递归参数的各种写法
延伸
题目刻意强调,可以默认题目的二叉树是满二叉树,因此是不是又更简单的方法存在,我们知道满二叉树有一些独特的性质,比如通过层次遍历对每个结点标号1-N,则节点的父亲,孩子,兄弟,都可以通过一定的规则直接得到。因此会不会有一些骚操作可以进行?但是限于空间复杂度的要求,我没有额外尝试,但它确实可以帮我们打开思路,在思考问题的过程中,将我们已经掌握的知识点充分串起来。