Leetcode 47

1 题目

117. 填充每个节点的下一个右侧节点指针 II

给定一个二叉树:

复制代码
struct Node {
  int val;
  Node *left;
  Node *right;
  Node *next;
}

填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL

初始状态下,所有 next 指针都被设置为 NULL

示例 1:

复制代码
输入:root = [1,2,3,4,5,null,7]
输出:[1,#,2,3,#,4,5,7,#]
解释:给定二叉树如图 A 所示,你的函数应该填充它的每个 next 指针,以指向其下一个右侧节点,如图 B 所示。序列化输出按层序遍历顺序(由 next 指针连接),'#' 表示每层的末尾。

示例 2:

复制代码
输入:root = []
输出:[]

提示:

  • 树中的节点数在范围 [0, 6000]
  • -100 <= Node.val <= 100

进阶:

  • 你只能使用常量级额外空间。
  • 使用递归解题也符合要求,本题中递归程序的隐式栈空间不计入额外空间复杂度。

2 代码实现

cpp 复制代码
/**
 * Definition for a Node.
 * struct Node {
 *     int val;
 *     struct Node *left;
 *     struct Node *right;
 *     struct Node *next;
 * };
 */
typedef struct Node Node ;
Node* getNext(Node * node ){
    while (node ){
        if (node -> left ) return node -> left ;
        if (node -> right) return node -> right ;
        node = node -> next;
    }
    return NULL ;

}
void traverse(Node* node){
    if (!node)
    return;
    if(node -> left){
        node -> left -> next = node -> right ? node -> right : getNext(node -> next);
    }
    if(node -> right){
        node -> right -> next =  getNext(node -> next);
    }
    traverse(node -> right);
    traverse(node -> left );
}
struct Node* connect(struct Node* root) {
	if(!root) return NULL ;
    traverse (root);
    return root;
}

Leetcode 46-CSDN博客

lc116里面的一般请况解决版本,对于任意二叉树而不是完美二叉树。

cpp 复制代码
/**
 * Definition for a Node.
 * struct Node {
 *     int val;
 *     struct Node *left;
 *     struct Node *right;
 *     struct Node *next;
 * };
 */
typedef struct Node Node;

// 辅助函数:找到node的下一个可用的子节点(用于连接堂兄弟)
Node* findNext(Node* node) {
    if (node == NULL) return NULL;
    // 优先返回左孩子,左孩子不存在则返回右孩子
    if (node->left) return node->left;
    if (node->right) return node->right;
    // 若当前节点无孩子,递归找下一个节点的孩子
    return findNext(node->next);
}

void connectRecursive(Node* node) {
    if (node == NULL) return;
    
    // 处理左孩子的next
    if (node->left) {
        if (node->right) {
            // 左孩子next指向右孩子(同父母)
            node->left->next = node->right;
        } else {
            // 左孩子next指向堂兄弟(通过父节点的next查找)
            node->left->next = findNext(node->next);
        }
    }
    
    // 处理右孩子的next
    if (node->right) {
        // 右孩子next指向堂兄弟(通过父节点的next查找)
        node->right->next = findNext(node->next);
    }
    
    // 先递归处理右子树(确保查找堂兄弟时右子树已初始化)
    connectRecursive(node->right);
    // 再递归处理左子树
    connectRecursive(node->left);
}

struct Node* connect(struct Node* root) {
    if (root == NULL) return NULL;
    root->next = NULL; // 根节点next为NULL
    connectRecursive(root);
    return root;
}

错误百出的初版代码

cpp 复制代码
/**
 * Definition for a Node.
 * struct Node {
 *     int val;
 *     struct Node *left;
 *     struct Node *right;
 *     struct Node *next;
 * };
 */
typedef struct Node Node ;
Node getNext(Node * node ){
    while (node ){
        if (node -> left ) return node -> left ;
        if (node -> right) return node -> right ;
        node = node -> next;
    }
    return NULL ;

}
void traverse(Node node){
    if (!node)
    return;
    if(node -> left){
        node -> left -> next = node -> right ? node -> right : getNext(node -> next);
    }
    if(node -> right){
        node -> right -> next =  getNext(node -> next);
    }
    traverse(node -> left );
    traverse(node -> right );
}
struct Node* connect(struct Node* root) {
	if(!root) return NULL ;
    traverse (root);
    return root;
}

这段代码存在多处语法错误和逻辑问题,导致无法正常编译和运行,下面详细分析每一处问题:

1. getNext 函数返回值类型错误

  • 问题:函数声明为 Node getNext(...),表示返回一个 Node 结构体对象,但实际返回的是 Node*(结构体指针,如 return node->left),类型完全不匹配。
  • 后果:编译时会提示 "返回类型与函数声明不匹配" 的错误。
  • 修正:应将返回值类型改为 Node*,即 Node* getNext(Node* node)

2. traverse 函数参数类型错误

  • 问题:函数声明为 void traverse(Node node),参数是 Node 结构体对象,但函数内部却使用 node->leftnode->right 这样的指针操作(-> 是指针运算符)。
  • 后果:对非指针类型使用 -> 会直接导致编译错误,因为只有指针才能通过 -> 访问成员。
  • 修正:参数应改为指针类型,即 void traverse(Node* node)

3. traverse 函数内部空指针判断逻辑错误

  • 问题:if (!node) return; 这行代码本身逻辑没问题(判断指针是否为空),但结合参数类型错误来看:如果参数是 Node node(非指针),!node 会试图将结构体对象转换为布尔值,这在 C 语言中是不允许的(结构体不能直接判断真假)。
  • 后果:即使修正了参数类型,若参数为非指针,此处会编译错误;若参数改为指针(正确做法),这行逻辑才有效。

4. traverse 函数递归调用参数错误

  • 问题:traverse(node -> left);traverse(node -> right); 中,node->leftnode->rightNode* 类型(指针),但 traverse 函数声明的参数是 Node 类型(非指针)。
  • 后果:指针类型无法隐式转换为结构体对象类型,编译时会提示 "参数类型不匹配" 错误。
  • 修正:在 traverse 函数参数改为 Node* node 后,这两处调用才合法(指针传参)。

5. 递归顺序逻辑错误(功能层面)

  • 问题:即使修正了上述语法错误,当前代码先递归左子树 traverse(node->left),再递归右子树 traverse(node->right),会导致 getNext 函数可能无法正确找到目标节点。
  • 原因:右孩子的 next 指针依赖父节点 next 链指向的右侧节点(可能是叔叔节点或更远的节点),若先处理左子树,右侧节点的 next 链可能尚未初始化,getNext 无法获取有效节点。
  • 修正:应先递归右子树,再递归左子树(traverse(node->right); traverse(node->left);),确保右侧节点的 next 链先初始化。

总结所有问题及修正方向

问题位置 具体错误 修正方案
getNext 返回值类型 声明为 Node,实际返回 Node* 改为 Node* getNext(Node* node)
traverse 参数类型 声明为 Node,实际需要指针操作 改为 void traverse(Node* node)
traverse 内部 -> 操作 对非指针使用 -> 运算符 配合参数类型修正,改为指针后合法
traverse 递归参数 实参为 Node*,形参为 Node 配合参数类型修正,传指针合法
递归顺序 先左后右导致 next 链未初始化 改为先右后左:traverse(right); traverse(left)

修正后的完整代码

cpp 复制代码
/**
 * Definition for a Node.
 * struct Node {
 *     int val;
 *     struct Node *left;
 *     struct Node *right;
 *     struct Node *next;
 * };
 */
typedef struct Node Node;

// 修正返回值类型为 Node*
Node* getNext(Node* node) {
    while (node) {
        if (node->left) return node->left;
        if (node->right) return node->right;
        node = node->next;
    }
    return NULL;
}

// 修正参数类型为 Node*
void traverse(Node* node) {
    if (!node)  // 对指针判断空值,合法
        return;
    
    // 处理左孩子的 next 指针
    if (node->left) {
        node->left->next = node->right ? node->right : getNext(node->next);
    }
    // 处理右孩子的 next 指针
    if (node->right) {
        node->right->next = getNext(node->next);
    }
    
    // 修正递归顺序:先右后左
    traverse(node->right);
    traverse(node->left);
}

struct Node* connect(struct Node* root) {
    if (!root) return NULL;
    traverse(root);
    return root;
}

修正后,代码可正确处理任意二叉树的 next 指针连接,通过递归先处理右侧节点,确保 getNext 能利用已初始化的 next 链找到目标节点。

【next指针像一条小尾巴,起到的是连接作用,而不是left right物理上的左节点or右节点】

next 指针确实像给每个节点加了一条 "横向的小尾巴",它的核心作用是建立同一层节点之间的横向连接 ,和 left/right 这种 "纵向父子关系" 的物理结构完全不同。

具体来说,这两种指针的本质区别在于:

  • left/right 是 "血缘关系":固定指向当前节点的左 / 右孩子,是二叉树结构本身的一部分,一旦树的形态确定,它们的指向就固定了。
  • next 是 "邻居关系":动态指向当前节点在同一层的 "右侧邻居",是人为添加的横向关联,目的是快速访问同层节点(比如从一个节点直接跳到同层下一个,不用再从根节点重新遍历)。

理解这一点,就能更清晰地把握 next 指针的设计逻辑:比如在处理任意二叉树时,next 指针的指向不依赖于当前节点的左右孩子是否存在,而是要 "跨父节点" 找同层的下一个有效节点 ------ 就像邻居可能是隔壁(同一个父节点的右 sibling),也可能是对门(父节点的 next 指向的节点的孩子),甚至更远的住户。

这种 "横向尾巴" 的视角,也能帮你更直观地理解递归处理的顺序:为什么要先处理右子树?因为右侧节点的 "尾巴"(next)可能会成为左侧节点 "尾巴" 的指向目标,得先让右侧邻居 "就位",左侧才能准确找到自己的 "尾巴该勾向哪里"。

3 学习建议

【好难啊,才做了一题算法,这么不容易,漏洞逻辑错误百出,基础要补补,怎么补】

算法题做得不顺利、出现各种漏洞是非常正常的,尤其是刚开始练习的时候。基础薄弱并不可怕,关键是有系统的方法去补。结合你的情况(刚接触二叉树这类递归题,容易出语法和逻辑错误),可以按这个步骤补基础:

一、先补「C 语言基础语法」------ 避免低级错误

你之前的代码里出现了「指针与结构体混淆」「函数返回值类型不匹配」等问题,这些是语法层面的基础问题,必须先解决。

  1. 重点掌握 3 个核心概念

    • 指针与结构体 :明确 Node(结构体对象)和 Node*(结构体指针)的区别,记住 -> 只能用于指针,. 用于对象。
    • 函数参数与返回值 :函数声明的返回值类型(如 void/int/Node*)必须和实际 return 的内容一致。
    • 递归函数的终止条件 :递归一定要有 if (nullptr) 这类终止判断,否则会无限递归导致崩溃。
  2. 练习方式:找一些简单的「结构体 + 指针」小题目练手,比如:

    • 定义一个链表节点结构体,实现链表的创建、插入、打印。
    • 定义一个二叉树节点结构体,手动构建一棵简单的树(比如 3 个节点),并遍历打印节点值。先保证写出来的代码能编译通过,再考虑逻辑对错。

二、用「拆解法」补「数据结构基础」------ 从简单结构开始

二叉树、递归这些内容难,是因为你对「结构的操作逻辑」不熟悉。可以从「线性结构」到「非线性结构」逐步拆解学习:

  1. 先吃透「链表」:链表是二叉树的简化版(每个节点只有一个 "孩子"),很多二叉树的逻辑(如遍历、指针连接)都能在链表中找到影子。

    • 练熟「链表的遍历」(迭代 / 递归)、「链表的指针修改」(如反转链表、连接两个链表)。
    • 思考:链表中 "当前节点的下一个节点" 如何找?类比到二叉树中 "当前节点孩子的 next 指针" 该怎么找?(你之前的 getNext 函数其实和链表遍历逻辑类似)
  2. 再啃「二叉树」:从「二叉树的遍历」开始,这是所有二叉树题的基础:

    • 手动模拟「前中后序递归遍历」:拿一张纸,画一棵二叉树,一步一步写递归过程中每个节点的访问顺序,理解递归的 "栈" 是怎么工作的。
    • 重点理解「递归的子问题拆分」:比如 "连接二叉树的 next 指针",可以拆成 "当前节点的左孩子 next 指向谁""右孩子 next 指向谁""左子树和右子树分别处理",每个子问题和原问题结构相同,这就是递归的核心。

三、用「刻意练习」补「算法逻辑思维」------ 从模仿到独立

刚开始写算法题,不要怕 "抄" 和 "模仿",关键是理解别人的逻辑为什么对,自己的错在哪里。

  1. 对一道题,做 3 遍以上

    • 第一遍:看题解,看懂后照抄代码,运行通过,标记出自己之前没想到的逻辑(比如你之前递归顺序搞反了,就要记 "为什么先处理右子树")。
    • 第二遍:隔 1-2 天,不看题解,自己写。如果卡壳,只看提示(比如 "递归顺序应该先右后左"),然后继续写。
    • 第三遍:再隔几天,完全独立写,写完后对比自己的代码和最优解,分析哪里可以优化(比如是否有多余的判断)。
  2. 积累「常见逻辑模板」:比如二叉树的递归模板:

    cpp 复制代码
    void traverse(Node* node) {
        if (node == NULL) return; // 终止条件
        // 处理当前节点(前序)
        traverse(node->left);   // 处理左子树
        // 处理当前节点(中序)
        traverse(node->right);  // 处理右子树
        // 处理当前节点(后序)
    }

    很多题(包括你的 connect 函数)都是在这个模板上改的,只是 "处理当前节点" 的逻辑不同。你需要记住这个模板,然后思考 "当前节点需要做什么操作"。

四、工具辅助:用「画图」和「调试」理解错误

  1. 画图模拟过程:遇到递归或指针操作的问题,拿一张纸画出来:

    • 画一棵具体的二叉树(比如只有 3-5 个节点,包含空节点)。
    • 按代码的步骤,一步步标出来每个指针的指向(比如 node->left->next 应该指向谁,实际指向了谁)。你会发现很多逻辑错误(比如递归顺序错)在图上一目了然。
  2. 学会调试代码:用 IDE(如 VS Code、Clion)的调试功能,一步步执行代码:

    • 观察每个变量的值(比如 node 是不是 NULLnode->next 指向哪里)。
    • 看函数调用栈(递归到了哪一层,当前处理的是哪个节点)。调试多了,你会直观感受到代码的执行流程,更容易发现错误。

五、心态:接受 "不完美",从「小进步」开始

刚开始练算法,没人能一次写对。你现在能发现自己 "漏洞百出",其实已经是进步了 ------ 至少知道哪里错了。

  • 每天只做 1-2 题,但每道题必须搞懂所有错误原因(是语法错还是逻辑错)。
  • 记录一个「错题本」,把自己常犯的错误(比如指针类型搞混、递归顺序错)列出来,每次写代码前扫一眼,提醒自己注意。

按这个节奏坚持 1-2 个月,你会明显感觉到:语法错误越来越少,看递归题的逻辑会越来越清晰,甚至能预判出可能出错的地方。基础就是这样一点点补起来的,慢慢来,别急~

慢慢来,比较快。

相关推荐
HLJ洛神千羽2 小时前
C++程序设计实验(黑龙江大学)
开发语言·c++·软件工程
kyle~2 小时前
算法数学---差分数组(Difference Array)
java·开发语言·算法
橘颂TA3 小时前
机器人+工业领域=?
算法·机器人
滨HI03 小时前
C++ opencv拟合直线
开发语言·c++·opencv
BreezeJuvenile3 小时前
外设模块学习(17)——5V继电器模块(STM32)
stm32·单片机·嵌入式硬件·学习·5v继电器模块
一个数据大开发3 小时前
【零基础一站式指南】Conda 学习环境准备与 Jupyter/PyCharm 完全配置
学习·jupyter·conda
艾莉丝努力练剑3 小时前
【C++:红黑树】深入理解红黑树的平衡之道:从原理、变色、旋转到完整实现代码
大数据·开发语言·c++·人工智能·红黑树
No0d1es3 小时前
电子学会青少年软件编程(C/C++)1级等级考试真题试卷(2025年9月)
java·c语言·c++·青少年编程·电子学会·真题·一级
_OP_CHEN4 小时前
C++进阶:(七)红黑树深度解析与 C++ 实现
开发语言·数据结构·c++·stl·红黑树·红黑树的旋转·红黑树的平衡调整