[树层次遍历的应用] 116. 117. 填充每个节点的下一个右侧节点指针 I II (队列层次遍历、迭代)

6 篇文章 0 订阅
5 篇文章 0 订阅

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;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值