【算法--链表】117.填充每个节点的下一个右侧节点指针Ⅱ--通俗讲解

78 阅读7分钟

通俗算法讲解推荐阅读:
【算法--链表】83.删除排序链表中的重复元素--通俗讲解
【算法--链表】删除排序链表中的重复元素 II--通俗讲解
【算法--链表】86.分割链表--通俗讲解
【算法】92.翻转链表Ⅱ--通俗讲解
【算法--链表】109.有序链表转换二叉搜索树--通俗讲解
【算法--链表】114.二叉树展开为链表--通俗讲解
【算法--链表】116.填充每个节点的下一个右侧节点指针--通俗讲解


通俗易懂讲解“填充每个节点的下一个右侧节点指针Ⅱ”算法题目

一、题目是啥?一句话说清

给定一个二叉树,填充每个节点的next指针,使其指向同一层的下一个右侧节点;如果找不到下一个节点,则next指针为NULL。初始时所有next指针都是NULL。

示例:

  • 输入:二叉树(可能不完整,即不是满二叉树)
  • 输出:二叉树 with next pointers filled

二、解题核心

使用层次遍历,但不需要使用队列,而是利用已建立的next指针来遍历下一层。我们使用一个虚拟节点(dummy)来帮助构建下一层的链表,然后用当前层的next指针来访问所有节点,同时连接下一层的节点。

这就像在每一层中,我们有一个链表,我们遍历这个链表来处理每个节点,同时将它们的子节点连接到下一层的链表中,从而节省空间。

三、关键在哪里?(3个核心点)

想理解并解决这道题,必须抓住以下三个关键点:

1. 利用当前层的next指针遍历

  • 是什么:从根节点开始,当前层已经通过next指针连接起来,所以我们可以像遍历链表一样遍历当前层。
  • 为什么重要:这样可以避免使用队列,节省空间。我们不需要存储所有节点,只需要几个指针。

2. 构建下一层的链表

  • 是什么:在处理当前层的每个节点时,我们将它的左子节点和右子节点依次添加到下一层的链表中。使用一个tail指针来维护下一层链表的尾部。
  • 为什么重要:这样我们可以按顺序连接下一层的节点,为下一层的遍历做准备,并保持节点的顺序。

3. 虚拟节点(Dummy Node)的运用

  • 是什么:为下一层创建一个虚拟头节点,这样我们可以轻松地访问下一层的头节点。
  • 为什么重要:虚拟节点简化了链表操作,避免了处理空链表的情况。当下一层没有节点时,虚拟节点的next为NULL,我们可以停止循环。

四、看图理解流程(通俗理解版本)

假设我们有这样一个二叉树:

        1
       / \
      2   3
     / \   \
    4   5   7

我们需要填充next指针。

  1. 初始化:从根节点开始,当前current指向节点1。
  2. 第一层处理
    • 创建虚拟节点dummytail指向dummy
    • 遍历当前层(只有节点1):
      • 节点1有左子节点2:将tail.next指向节点2,tail移动到节点2。
      • 节点1有右子节点3:将tail.next指向节点3,tail移动到节点3。
    • 当前层遍历完后,current设置为dummy.next(即节点2)。
  3. 第二层处理
    • 当前current指向节点2(第二层的头)。
    • 创建新的虚拟节点dummytail指向dummy
    • 遍历当前层(节点2和节点3通过next连接):
      • 节点2有左子节点4:tail.next指向节点4,tail移动到节点4。
      • 节点2有右子节点5:tail.next指向节点5,tail移动到节点5。
      • 节点3有右子节点7:tail.next指向节点7,tail移动到节点7。
    • 当前层遍历完后,current设置为dummy.next(即节点4)。
  4. 第三层处理
    • 当前current指向节点4(第三层的头)。
    • 创建虚拟节点dummytail指向dummy
    • 遍历当前层(节点4、5、7通过next连接):
      • 节点4没有子节点,跳过。
      • 节点5没有子节点,跳过。
      • 节点7没有子节点,跳过。
    • 下一层为空,循环结束。

最终next指针:

  • 节点1的next为NULL。
  • 节点2的next指向节点3。
  • 节点3的next为NULL。
  • 节点4的next指向节点5。
  • 节点5的next指向节点7。
  • 节点7的next为NULL。

五、C++ 代码实现(附详细注释)

#include <iostream>
using namespace std;

// 二叉树节点定义
class Node {
public:
    int val;
    Node* left;
    Node* right;
    Node* next;

    Node() : val(0), left(NULL), right(NULL), next(NULL) {}
    Node(int _val) : val(_val), left(NULL), right(NULL), next(NULL) {}
    Node(int _val, Node* _left, Node* _right, Node* _next)
        : val(_val), left(_left), right(_right), next(_next) {}
};

class Solution {
public:
    Node* connect(Node* root) {
        if (root == nullptr) return root;
        Node* current = root;  // 当前层的头节点
        while (current != nullptr) {
            Node dummy(0);  // 虚拟头节点用于下一层
            Node* tail = &dummy;  // tail用于构建下一层的链表
            // 遍历当前层
            while (current != nullptr) {
                if (current->left != nullptr) {
                    tail->next = current->left;
                    tail = tail->next;
                }
                if (current->right != nullptr) {
                    tail->next = current->right;
                    tail = tail->next;
                }
                current = current->next;  // 移动到当前层的下一个节点
            }
            current = dummy.next;  // 移动到下一层的头节点
        }
        return root;
    }
};

// 辅助函数:打印层次链表(用于测试)
void printLevels(Node* root) {
    Node* levelStart = root;
    while (levelStart != nullptr) {
        Node* current = levelStart;
        while (current != nullptr) {
            cout << current->val << " ";
            current = current->next;
        }
        cout << "#" << endl;
        // 找到下一层的起始点:由于next指针已填充,但下一层的起始点需要通过第一个节点的左子节点或右子节点?
        // 实际上,我们无法直接知道下一层的起始点,但算法中是通过虚拟节点获取的。这里为了演示,我们假设从当前层第一个节点的左子节点开始(如果有),否则右子节点?
        // 但这不是通用的,因为下一层可能从任意节点开始。所以通常测试时,我们只打印已知的层次。
        levelStart = levelStart->left; // 这可能不准确,但对于简单二叉树可行
    }
}

// 测试代码
int main() {
    // 构建示例二叉树
    Node* root = new Node(1);
    root->left = new Node(2);
    root->right = new Node(3);
    root->left->left = new Node(4);
    root->left->right = new Node(5);
    root->right->right = new Node(7);

    Solution solution;
    solution.connect(root);

    // 打印层次
    cout << "Level 1: ";
    Node* current = root;
    while (current) {
        cout << current->val << " ";
        current = current->next;
    }
    cout << "#" << endl;

    cout << "Level 2: ";
    current = root->left;
    while (current) {
        cout << current->val << " ";
        current = current->next;
    }
    cout << "#" << endl;

    cout << "Level 3: ";
    current = root->left->left;
    while (current) {
        cout << current->val << " ";
        current = current->next;
    }
    cout << "#" << endl;

    return 0;
}

六、时间空间复杂度

  • 时间复杂度:O(n),其中n是节点数。每个节点被访问一次。
  • 空间复杂度:O(1),只使用了常数额外的空间(几个指针),不包括递归栈空间(因为这里是迭代解法)。

七、注意事项

  • 虚拟节点的使用:每次外层循环都创建一个新的虚拟节点(在栈上),这是安全的,因为它是局部变量。
  • 移动指针:在内层循环中,我们移动current指针遍历当前层,直到NULL,确保不遗漏节点。
  • 连接顺序:在添加子节点时,先左后右,保持从左到右的顺序。
  • 空树处理:如果根节点为空,直接返回。
  • 二叉树可能不完整:这个算法适用于任何二叉树,因为我们不依赖完整的结构,而是按顺序连接子节点。

通俗算法讲解推荐阅读:
【算法--链表】83.删除排序链表中的重复元素--通俗讲解
【算法--链表】删除排序链表中的重复元素 II--通俗讲解
【算法--链表】86.分割链表--通俗讲解
【算法】92.翻转链表Ⅱ--通俗讲解
【算法--链表】109.有序链表转换二叉搜索树--通俗讲解
【算法--链表】114.二叉树展开为链表--通俗讲解
【算法--链表】116.填充每个节点的下一个右侧节点指针--通俗讲解