[LeetCode 116 117] - 填充每一个节点的指向右边邻居的指针I & II (Populating Next Right Pointers in Each Node I & II)
问题
给出如下结构的二叉树:
struct TreeLinkNode {
TreeLinkNode *left;
TreeLinkNode *right;
TreeLinkNode *next;
}
填充每一个next指针使其指向自己的右边邻居节点。如果没有右边的邻居节点,next指针须设成NULL。
在开始时,所有的next指针被初始化成NULL。
注意:
- 你只能使用常数级别的额外空间
- 你可以假设该树为完全二叉树(即所有叶子节点都在同一层,而且每个父节点都有两个子节点)。
例如,给出如下完全二叉树:
1
/ \
2 3
/ \ / \
4 5 6 7
在调用你的函数后,树看起来将是这样:
1 -> NULL
/ \
2 -> 3 -> NULL
/ \ / \
4->5->6->7 -> NULL
初始思路
要得到每个节点的右边邻居,显然应该对二叉树进行层次遍历。使用队列+循环是进行二叉树层次遍历的标准方法。但是这里我们有一点特别的需求-需要知道每一层的结束,因为每一层最右边节点的next指针需要设为null。回想我们在 [LeetCode 126] - 单词梯II(Word Ladder II) 中最后提到的双vector交替循环法,在这里特别适用:遍历当前vector得到的子节点都放到下一个vector中,当遍历完毕时,下一个vector中恰好就是下一层的所有节点。我们只需将其中每个元素的next指针指向自己的下一个元素即可,当然,最后一个元素要跳过。然后清空当前vector,交换当前vector和下一个vector(通过对下标取反)并重复此步骤直到当前vector为空就完成了任务。最后完成的代码如下:
1 class Solution { 2 public: 3 void connect(TreeLinkNode *root) 4 { 5 if(!root) 6 { 7 return; 8 } 9 10 candidates_[0].clear(); 11 candidates_[1].clear(); 12 13 int flag = 0; 14 15 candidates_[flag].push_back(root); 16 17 while(!candidates_[flag].empty()) 18 { 19 for(auto iter = candidates_[flag].begin(); iter != candidates_[flag].end(); ++iter) 20 { 21 TreeLinkNode* node = *iter; 22 if(node->left) 23 { 24 candidates_[!flag].push_back(node->left); 25 } 26 if(node->right) 27 { 28 candidates_[!flag].push_back(node->right); 29 } 30 } 31 32 if(candidates_[!flag].empty()) 33 { 34 break; 35 } 36 37 for(auto iter = candidates_[!flag].begin(); iter != candidates_[!flag].end() - 1; ++iter) 38 { 39 (*iter)->next = *(iter + 1); 40 } 41 42 candidates_[flag].clear(); 43 flag = !flag; 44 } 45 } 46 47 private: 48 std::vector<TreeLinkNode*> candidates_[2]; 49 };
上面的代码能通过Judge Small和Judge Large的测试,但是仔细审题后可以发现,其实我们的方案并不符合题目的要求-你只能使用常数级别的额外空间。使用队列对二叉树进行层次遍历,队列需要的最大空间是和二叉树的大小有关的,对题目中的完全二叉树来说,它等于叶子节点的个数。即对层数为n(n>0)的二叉树,需要的空间为2^(n-1)级别。
改进方案
要符合题目的要求,看来是不能使用循环遍历队列的方式来层次遍历二叉树了。遍历二叉树,除了循环无非就是递归。对层次遍历来说,递归的方法并不是那么常用,那么,让我们来看看用递归怎么层次遍历二叉树。
想象一下我们要访问一个二叉树的第n层,我们会从顶点出发,一层层往下直到第n层。如果把一次往下走的这个动作看作一次函数调用,那么我们就可以得到一串递归调用。这个递归的结束条件是什么?从顶点往下一层后,还需要走n-1层能达到我们的目标,再下一层,需要n-2层。如此往复,当n=1时就走到了我们期望的层数。由此n=1就是递归结束的条件。用一个三层的完全二叉树来模拟递归访问第三层,情况如下:
Visit(node1, 3)
1
Visit(node1->left, 2) / \ Visit(node1->right, 2)
2 3
Visit(node2->left, 1) / \ Visit(node2->right, 1) Visit(node3->left, 1) / \ Visit(node3->right, 1)
4 5 6 7
由此,我们得到伪代码如下:
访问二叉树(节点,层数)
如果层数=1,访问当前节点
否则
访问二叉树(节点的左子树,层数-1)
访问二叉树(节点的右子树,层数-1)
现在我们得到了递归访问二叉树第n层的方法,但是离访问所有层还是有一段距离。要访问所有层,我们需要让n从1开始递增,循环调用访问二叉树第n层的递归函数。什么时候不需要再递增n了呢?不需要再递增层数意味着我们的n已经走到了二叉树的底层,根据题目中完全二叉树的条件,底层也就是叶子节点所在的层。而叶子节点的特性-左右子节点皆为null就为我们提供了检查的标准。由此我们就需要在上面两个对左右子树的递归调用中各返回一个指针,当返回的两个指针皆为null时,说明我们到达底层了。符合递归结束条件时,自然是返回当前节点,那递归过程中返回哪个指针?其实这里在左右指针中随便选一个就行了,因为我们只需要利用返回值是否为空指针这个信息,而对完全二叉树来说左右子节点的这个性质必然是一致的。
最后,不要忘了我们的目的是要为每个节点设置next指针,所以我们需要一个成员变量来保存左边邻居。每当访问到一个节点,我们将左边邻居的next指针指向它,然后将它的地址拷贝到保存左邻居的指针中。当然,不要忘了要做一些特殊判断来处理每层第一个节点的情况。
1 class Solution { 2 public: 3 void connect(TreeLinkNode *root) 4 { 5 if(!root) 6 { 7 return; 8 } 9 10 hasToTheEnd_ = false; 11 12 int level = 1; 13 14 while(!hasToTheEnd_) 15 { 16 nextLeft_ = nullptr; 17 VisitLevel(root, level); 18 ++level; 19 } 20 } 21 22 private: 23 TreeLinkNode* VisitLevel(TreeLinkNode* node, int level) 24 { 25 if(level == 1) 26 { 27 if(nextLeft_ != nullptr) 28 { 29 nextLeft_ ->next = node; 30 31 } 32 nextLeft_ = node; 33 return node; 34 } 35 36 TreeLinkNode* left = VisitLevel(node->left, level - 1); 37 TreeLinkNode* right = VisitLevel(node->right, level - 1); 38 39 if(left == nullptr && right == nullptr) 40 { 41 hasToTheEnd_ = true; 42 } 43 44 //完全二叉树,左右子树的非空性必然一致,随便返回一个即可 45 return left; 46 47 } 48 49 TreeLinkNode* nextLeft_; 50 bool hasToTheEnd_; 51 };
现在的代码就完全符合题目要求并且能通过大小数据集测试了。
后继问题
如果给出的树为任意二叉树,前面的解决方案还能工作吗?
后继思路
如果去掉了完全二叉树这个条件,那么我们用来判断是否到达二叉树底层的方法就不再生效了,如下面的二叉树:
1
/ \
2 3
/ \
4 5
可以看到编号为3的叶子节点并不是最底层。
我们怎么知道3号节点并不是最底层?因为和它在同一层的2号节点还存在子节点。那么我们可以更改一下判断最底层的条件:如果当前访问的层有任一节点存在子节点,说明当前层不是最底层。什么时候我们走到了当前访问的层?n = 1递归条件结束的时候。我们可以在这里判断当前节点是否存在子节点,如果有就设置继续循环标志。也就是说,我们把判断当前层是否为最底层的逻辑由“如果符合某条件,则终止循环”变为了“如果符合某条件,则继续循环”。因此相应的我们需要在完成针对一层的循环后将标志复位。做了上面的修改后,可以发现递归函数的返回值不再需要了,因为我们在最后一次递归调用返回前就完成了判断而不再通过返回信息供上层函数判断。得到的最终代码如下,现在处理任意二叉树都没有问题了:
1 class Solution{ 2 public: 3 void connect(TreeLinkNode *root) 4 { 5 if(!root) 6 { 7 return; 8 } 9 10 hasToTheEnd_ = false; 11 12 int level = 1; 13 14 while(!hasToTheEnd_) 15 { 16 hasToTheEnd_ = true; 17 nextLeft_ = nullptr; 18 VisitLevel(root, level); 19 ++level; 20 } 21 } 22 23 private: 24 void VisitLevel(TreeLinkNode* node, int level) 25 { 26 if(level == 1) 27 { 28 if(nextLeft_ != nullptr) 29 { 30 nextLeft_ ->next = node; 31 32 } 33 nextLeft_ = node; 34 35 if(node->left != nullptr || node->right != nullptr) 36 { 37 hasToTheEnd_ = false; 38 } 39 } 40 41 if(node->left) 42 { 43 VisitLevel(node->left, level - 1); 44 } 45 46 if(node->right) 47 { 48 VisitLevel(node->right, level - 1); 49 } 50 51 } 52 53 TreeLinkNode* nextLeft_; 54 bool hasToTheEnd_; 55 };