[树层次遍历的应用] 116. 117. 填充每个节点的下一个右侧节点指针 I II (队列层次遍历、迭代)
116.填充每个节点的下一个右侧节点指针(完美二叉树)
题目链接:https://leetcode-cn.com/problems/populating-next-right-pointers-in-each-node/
分类:树(完美二叉树)、队列(层次遍历)、空间优化(线索二叉树的思想)
思路1:队列层次遍历 + 找出每一层的最后一个节点
首先,使用队列实现层次遍历(102. 二叉树的层序遍历),并且在处理每一层的第一个节点之前先获取当前队列长度==这一层的节点个数,所以可以执行for循环一次性处理这一层的所有节点(102. 二叉树的层序遍历-思路2)。
然后,在层次遍历过程中处理当前节点时,队首节点就是这一层的下一个节点,可以先将当前节点的左右孩子入队后,将next域填充为指向队首节点,就完成题目要求的填充。for循环的最后一轮处理最后一个节点时,让该节点的next=null。
class Solution {
public Node connect(Node root) {
if(root == null || (root.left == null && root.right == null)) return root;
Queue<Node> queue = new LinkedList<>();
queue.offer(root);
//层次遍历
while(!queue.isEmpty()){
int size = queue.size();
for(int i = 0; i < size; i++){
Node top = queue.poll();
if(top.left != null) queue.offer(top.left);
if(top.right != null) queue.offer(top.right);
if(i == size - 1) top.next = null;
else top.next = queue.peek();
}
}
return root;
}
}
- 空间复杂度:O(N),因为使用了队列来存放二叉树的每一个节点。
思路2:迭代 + 遍历上一层链表
每一层的工作内容是遍历上一层的链表,通过上一层的链表找到这一层节点的链接关系,从而填充这一层节点的next域。(用到的思想本质上是线索二叉树,只不过新增了一个next域,而不是在right域上做的修改)
首先,为上一层链表创建一个工作指针up,为这一层链表创建一个工作指针cur,设置一个头结点head用来层迭代时更新up。每填充完cur的next域,cur就取它的next(cur=cur.next);每当up的right填充给cur的next后,就更新up=up.next;每遍历完一层的链表,就更新head为下一层的头结点(节点的更新时机是解题的关键)。
当up指针指向上一层的头结点时,cur=up.left,这一层的头结点就是上一层头结点的左孩子,得到这一层的头结点就让head指向该头结点。
然后,开始遍历上一层的链表,观察下面例子中的2,3层,可以发现存在两种节点链接情况:
例如:
1
/ \
2 3
/ \ / \
4 5 6 7
- 一种是4,5之间,它们的父节点是同一个,所以当cur=4时,直接令cur.next=父节点.right即可;
- 另一种是5,6之间,它们的父节点不是同一个,所以当cur=5时,要令5.next=6的父节点.left。
关键就在于在处理节点5时,能够找到6的父节点,这一问题可以通过遍历上一层链表来实现:
在构建4567这一层的链表时,up就指向它们的上一层23的链表,在cur=4时up=2,处理完4->5后,因为up.right已经填充给了cur.next,所以更新up=2.next=3,此时的up就是6的父节点,同时cur更新为cur.next=5,之后就可以通过判断5不是3的左右孩子而取5.next=3.left。
直到上一层的链表遍历到末尾时,更新up=这一层的头结点head,进入下一层,重复这三个步骤,直到up.left==null。
- up.left == null 是完美二叉树特有的判断结束条件:如果左孩子为空,说明二叉树到此为止,后面都是空节点.
class Solution {
public Node connect(Node root) {
if(root == null || (root.left == null && root.right == null)) return root;
Node up = root, cur = null, head = null;//up是上一层链表的工作指针,cur是当前层链表的工作指针,head指向cur所在链表的头结点,用于更新up
while(up.left != null){//完美二叉树特有的判断结束条件:如果左孩子为空,说明二叉树到此为止,后面都是空节点
cur = up.left;//获取当前层的第一个节点==上一层头结点的左孩子
head = cur;//备份当前层的第一个节点
while(up != null){//遍历上一层的链表
cur.next = up.right;
cur = cur.next;
up = up.next;//处理完up的右孩子,就更新up为链表的下一个节点
if(up == null) break;
//当前cur节点不是当前up的孩子节点时,就是遇到第二种情况,将cur.next指向up的左孩子
if(cur != up.left && cur != up.right){
cur.next = up.left;
cur = cur.next;
}
}
up = head;//上一层链表处理完,更新up为当前层的头结点,为下一层的处理做准备
//head = null;//head要重新置为null才能进入更新head的分支中
}
return root;
}
}
- 空间复杂度:O(1)
117.填充每个节点的下一个右侧节点指针 II (一般二叉树)
题目链接:https://leetcode-cn.com/problems/populating-next-right-pointers-in-each-node-ii/
分类:树(普通二叉树)、空间优化(线索二叉树的思想)
分析(同116思路2)
这题和116题类似,但处理的树不再是完美二叉树,而是一般的二叉树。
所以迭代的终止条件、当前层链表头节点head的选取、树节点next域的填充都要做相应的修改。
1、迭代的终止条件
树不再是完美二叉树,所以不能用up.left == null作为终止条件。
迭代过程实际上就是不使用队列的层次遍历过程,寻找层次遍历的最后一个节点,就是寻找最底层最右节点。
当up到达最底层最右节点时,该节点必然是没有左右孩子且next == null的节点,所以可以用:
up.left==null && up.right==null && up.next==null
作为终止条件。
2、当前层的链表头结点head的选取
head指向当前层的第一个节点,要找到当前层的第一个节点,就要通过上一层的节点来实现。
116完美二叉树中,直接取上一层链表头结点的left即可,但在这里不再适用,因为上一层节点里可能存在左右孩子为null的情况,所以我们要找的是上一层链表里第一个拥有非null孩子节点的节点,找到该节点后,它如果有左孩子,它的左孩子就是当前层的头结点head,如果没有左孩子而有右孩子,它的右孩子就是当前层的头结点head。
//遍历上一层链表,直到找到第一个含有孩子节点的up/或up==null
while(up != null && up.left == null && up.right == null) up = up.next;
if(up == null) break;
//选取拥有孩子节点的up的第一个孩子作为cur
if(up.left != null) cur = up.left;
else cur = up.right;
head = cur;//更新头结点
3、节点next域的填充
比起116题,可能的链接情况有了更多种情况:
1
/ \
2 3
/ \ / \
4 →5 6 7
1
/ \
2 3
/ \ / \
4 5→6 7
1
/ \
2 3
/ \ / \
4 → 6 7
1
/ \
2 3
/ \ / \
4 5 → 7
这几种情况的处理总结起来就是:链表的某个节点在填充next域之前,要先找到下一个不为null的兄弟节点,而兄弟节点可以通过上一层的链表来寻找。
而且因为不再是完美二叉树,所以上一层链表的节点里可能存在孩子节点为空的情况,所以要找到这个兄弟节点,就要先在上一层链表里找到第一个存在非null孩子的节点,且这个非null孩子不能是cur本身。在找到这样的节点up后,如果up的左孩子不为null,就让cur.next=左孩子;如果up的左孩子为null而右孩子不为null,就让cur.next=右孩子,在拿右孩子填充next域之后,up要更新为up.next(指针更新时机不变,和116相同)。
while(up != null){
//寻找下一个不为null的兄弟节点
if(up.left != null && up.left != cur){//判断当前up的左孩子
cur.next = up.left;
cur = cur.next;
//左孩子链接后up可能还存在右孩子,所以先不后移
}
else if(up.right != null && up.right != cur){//判断当前up的右孩子
cur.next = up.right;
cur = cur.next;
up = up.next;//右孩子链接后up必定要后移一位
}
//当前up没有孩子节点,up就选择下一个节点
else up = up.next;
}
实现代码
class Solution {
public Node connect(Node root) {
if(root == null || (root.left == null && root.right == null)) return root;
Node up = root, cur = null, head = null;
//迭代
while(up.left != null || up.right != null || up.next != null){
//遍历上一层链表,直到找到第一个含有孩子节点的up/或up==null
while(up != null && up.left == null && up.right == null) up = up.next;
if(up == null) break;
//选取拥有孩子节点的up的第一个孩子作为cur
if(up.left != null) cur = up.left;
else cur = up.right;
head = cur;//更新头结点
while(up != null){
//寻找下一个不为null的兄弟节点
if(up.left != null && up.left != cur){//判断当前up的左孩子
cur.next = up.left;
cur = cur.next;
//左孩子链接后up可能还存在右孩子,所以先不后移
}
else if(up.right != null && up.right != cur){//判断当前up的右孩子
cur.next = up.right;
cur = cur.next;
up = up.next;//右孩子链接后up必定要后移一位
}
//当前up没有孩子节点,up就选择下一个节点
else up = up.next;
}
up = head;
}
return root;
}
}