116. 填充每个节点的下一个右侧节点指针
文章目录
- [116. 填充每个节点的下一个右侧节点指针](#116. 填充每个节点的下一个右侧节点指针)
-
- 题目描述
- [示例 1:](#示例 1:)
- [示例 2:](#示例 2:)
- 提示:
- 进阶:
- 问题深度分析
- 算法对比
- 算法流程图
- 复杂度分析
- 关键优化技巧
-
- [技巧一:利用 next 指针实现 O(1) 空间](#技巧一:利用 next 指针实现 O(1) 空间)
- 技巧二:递归连接跨子树节点
- [技巧三:BFS 层序遍历](#技巧三:BFS 层序遍历)
- [技巧四:DFS 前序遍历](#技巧四:DFS 前序遍历)
- 边界情况处理
-
- [1. 空树](#1. 空树)
- [2. 单节点树](#2. 单节点树)
- [3. 每层最后一个节点](#3. 每层最后一个节点)
- [4. 叶子节点层](#4. 叶子节点层)
- 测试用例设计
- 常见错误与陷阱
-
- 错误一:忘记处理跨子树连接
- [错误二:在建立连接前访问 next](#错误二:在建立连接前访问 next)
- [错误三:BFS 中队列大小变化](#错误三:BFS 中队列大小变化)
- 实战技巧总结
-
- [1. 完美二叉树特性利用](#1. 完美二叉树特性利用)
- [2. next 指针的双重作用](#2. next 指针的双重作用)
- [3. 空间优化思路](#3. 空间优化思路)
- [4. 递归 vs 迭代](#4. 递归 vs 迭代)
- 进阶扩展
-
- [扩展一:非完美二叉树(LeetCode 117)](#扩展一:非完美二叉树(LeetCode 117))
- [扩展二:N 叉树](#扩展二:N 叉树)
- 扩展三:从任意节点开始
- 扩展四:双向链表
- 应用场景
-
- [1. 树形结构的横向遍历](#1. 树形结构的横向遍历)
- [2. 完美二叉树的特殊操作](#2. 完美二叉树的特殊操作)
- [3. 空间受限环境](#3. 空间受限环境)
- [4. 树形数据结构的序列化](#4. 树形数据结构的序列化)
- 相关题目
- 总结
- 完整题解代码
题目描述
给定一个 完美二叉树 ,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下:
C++
struct Node {
int val;
Node *left;
Node *right;
Node *next;
}
填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。
初始状态下,所有 next 指针都被设置为 NULL。
示例 1:

输入 :root = [1,2,3,4,5,6,7]
输出 :[1,#,2,3,#,4,5,6,7,#]
解释 :给定二叉树如图 A 所示,你的函数应该填充它的每个 next 指针,以指向其下一个右侧节点,如图 B 所示。序列化的输出按层序遍历排列,同一层节点由 next 指针连接,'#' 标志着每一层的结束。
示例 2:
输入 :root = []
输出 :[]
提示:
- 树中节点的数量在
[0, 2^12 - 1]范围内 -1000 <= node.val <= 1000
进阶:
- 你只能使用常量级额外空间。
- 使用递归解题也符合要求,本题中递归程序占用的栈空间不算做额外的空间复杂度。
问题深度分析
这是一道二叉树层序遍历的变种问题 ,核心在于利用已建立的 next 指针进行空间优化 。题目要求填充每个节点的 next 指针,使其指向同一层的下一个右侧节点。对于完美二叉树,我们可以利用其结构特性(每层节点数确定、左右子树对称)来实现高效的算法。
问题本质
给定一个完美二叉树,需要为每个节点建立指向同一层右侧相邻节点 的 next 指针。关键点:
- 同一层:节点必须在同一深度
- 右侧相邻:必须是紧邻的右侧节点
- 完美二叉树:所有叶子节点在同一层,每个节点都有两个子节点
核心思想
利用已建立的 next 指针:
- 层序遍历思路 :逐层处理,为每层节点建立
next连接 - 递归思路 :利用父节点的
next指针连接跨子树的节点 - 迭代优化 :利用已建立的
next指针,实现 O(1) 空间复杂度
关键难点
- 跨子树连接:左子树的右节点需要连接到右子树的左节点
- 空间优化:如何在 O(1) 空间内完成操作
- 边界处理 :每层最后一个节点的
next应为NULL
算法对比
| 方法 | 时间复杂度 | 空间复杂度 | 特点 |
|---|---|---|---|
| 方法一:层序遍历(BFS) | O(n) | O(w) | 直观易懂,需要队列存储 |
| 方法二:递归连接 | O(n) | O(h) | 利用递归栈,代码简洁 |
| 方法三:迭代 O(1) 空间 | O(n) | O(1) | 最优解,利用 next 指针 |
| 方法四:DFS 前序遍历 | O(n) | O(h) | 深度优先,先处理父节点 |
说明:
n:节点总数w:树的最大宽度(最后一层节点数)h:树的高度
算法流程图
主算法流程
是
否
是
否
是
否
开始
根节点是否为空?
返回空
选择算法方法
方法一: BFS层序遍历
方法二: 递归连接
方法三: 迭代O1空间
方法四: DFS前序遍历
创建队列
逐层处理节点
连接同层相邻节点
处理下一层
是否还有层?
完成
递归处理左右子树
连接同一父节点的子节点
利用父节点next连接跨子树节点
递归处理子节点
从根节点开始
逐层处理
利用已建立的next指针遍历
连接下一层节点
是否还有层?
前序遍历当前节点
连接当前节点的子节点
递归处理左子树
递归处理右子树
结束
方法三:迭代 O(1) 空间详细流程
是
否
是
否
是
否
开始: root节点
初始化: leftmost = root
leftmost是否为空?
结束
当前节点curr = leftmost
curr是否为空?
更新leftmost到下一层
连接curr.left.next = curr.right
curr.next是否存在?
连接curr.right.next = curr.next.left
curr.right.next = NULL
curr = curr.next
返回root
跨子树连接示意图
子节点层
父节点层
next
next
next
next
建立连接
建立连接
通过A.next建立
Node A
Node B
A.left
A.right
B.left
B.right
复杂度分析
时间复杂度
所有方法均为 O(n):
- 每个节点被访问一次
- 每个节点的
next指针被设置一次 - 总操作次数与节点数成正比
空间复杂度
-
方法一(BFS):O(w)
- 队列最多存储一层的节点
- 完美二叉树最后一层有
2^(h-1)个节点 w = 2^(h-1) = O(n/2) = O(n)
-
方法二(递归):O(h)
- 递归调用栈深度等于树的高度
- 完美二叉树高度
h = log₂(n+1) O(log n)
-
方法三(迭代 O(1)):O(1)
- 只使用常数额外变量
- 利用已建立的
next指针遍历
-
方法四(DFS):O(h)
- 递归调用栈深度等于树的高度
O(log n)
关键优化技巧
技巧一:利用 next 指针实现 O(1) 空间
核心思想 :已建立的 next 指针可以作为"横向链表",用于遍历当前层,从而建立下一层的连接。
go
// 方法三:迭代 O(1) 空间
func connect3(root *Node) *Node {
if root == nil {
return nil
}
leftmost := root
for leftmost.Left != nil { // 还有下一层
curr := leftmost
for curr != nil {
// 连接同一父节点的子节点
curr.Left.Next = curr.Right
// 连接跨子树的节点
if curr.Next != nil {
curr.Right.Next = curr.Next.Left
}
curr = curr.Next // 利用next指针横向移动
}
leftmost = leftmost.Left // 移动到下一层
}
return root
}
优势:
- 空间复杂度 O(1)
- 时间复杂度 O(n)
- 完美利用完美二叉树的结构特性
技巧二:递归连接跨子树节点
核心思想 :利用父节点的 next 指针,连接不同父节点的子节点。
go
// 方法二:递归连接
func connect2(root *Node) *Node {
if root == nil {
return nil
}
// 连接同一父节点的子节点
if root.Left != nil {
root.Left.Next = root.Right
}
// 利用父节点的next连接跨子树的节点
if root.Right != nil && root.Next != nil {
root.Right.Next = root.Next.Left
}
// 递归处理左右子树
connect2(root.Left)
connect2(root.Right)
return root
}
优势:
- 代码简洁清晰
- 逻辑直观易懂
- 空间复杂度 O(log n)
技巧三:BFS 层序遍历
核心思想 :使用队列逐层处理,为每层节点建立 next 连接。
go
// 方法一:BFS层序遍历
func connect1(root *Node) *Node {
if root == nil {
return nil
}
queue := []*Node{root}
for len(queue) > 0 {
size := len(queue)
for i := 0; i < size; i++ {
node := queue[0]
queue = queue[1:]
// 连接同层相邻节点
if i < size-1 {
node.Next = queue[0]
}
// 添加子节点到队列
if node.Left != nil {
queue = append(queue, node.Left)
}
if node.Right != nil {
queue = append(queue, node.Right)
}
}
}
return root
}
优势:
- 思路直观
- 适用于任意二叉树
- 易于理解和实现
技巧四:DFS 前序遍历
核心思想:先处理父节点建立连接,再递归处理子节点。
go
// 方法四:DFS前序遍历
func connect4(root *Node) *Node {
if root == nil {
return nil
}
// 连接当前节点的子节点
if root.Left != nil {
root.Left.Next = root.Right
if root.Next != nil {
root.Right.Next = root.Next.Left
}
}
// 递归处理左右子树
connect4(root.Left)
connect4(root.Right)
return root
}
优势:
- 深度优先遍历
- 先处理父节点,再处理子节点
- 逻辑清晰
边界情况处理
1. 空树
go
if root == nil {
return nil
}
2. 单节点树
- 根节点的
next保持为NULL - 无需特殊处理
3. 每层最后一个节点
- 最后一个节点的
next应为NULL - BFS 方法中通过索引判断:
if i < size-1
4. 叶子节点层
- 叶子节点没有子节点
- 迭代方法中通过
leftmost.Left != nil判断是否还有下一层
测试用例设计
基础测试
-
空树
- 输入:
[] - 输出:
[]
- 输入:
-
单节点
- 输入:
[1] - 输出:
[1,#]
- 输入:
-
两层树
- 输入:
[1,2,3] - 输出:
[1,#,2,3,#] - 验证:节点2的next指向节点3
- 输入:
标准测试
-
三层完美二叉树
- 输入:
[1,2,3,4,5,6,7] - 输出:
[1,#,2,3,#,4,5,6,7,#] - 验证:
- 节点2的next指向节点3
- 节点4的next指向节点5
- 节点5的next指向节点6
- 节点6的next指向节点7
- 输入:
-
四层完美二叉树
- 输入:
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15] - 验证:每层节点正确连接
- 输入:
边界测试
-
最大深度树
- 测试递归深度限制
- 验证空间复杂度
-
单侧树(非完美二叉树,用于测试)
- 注意:完美二叉树不会有这种情况
- 但可以测试算法的健壮性
常见错误与陷阱
错误一:忘记处理跨子树连接
错误代码:
go
// 只连接了同一父节点的子节点
curr.Left.Next = curr.Right
// 忘记了跨子树的连接!
正确做法:
go
curr.Left.Next = curr.Right
if curr.Next != nil {
curr.Right.Next = curr.Next.Left // 跨子树连接
}
错误二:在建立连接前访问 next
错误代码:
go
// 错误:在建立连接前就使用next指针
for curr != nil {
curr = curr.Next // 此时next可能还未建立
curr.Left.Next = curr.Right
}
正确做法:
go
// 先建立连接,再移动
for curr != nil {
curr.Left.Next = curr.Right
if curr.Next != nil {
curr.Right.Next = curr.Next.Left
}
curr = curr.Next // 建立连接后再移动
}
错误三:BFS 中队列大小变化
错误代码:
go
for len(queue) > 0 {
node := queue[0]
queue = queue[1:]
// 错误:队列大小在循环中变化
if len(queue) > 0 {
node.Next = queue[0]
}
// ...
}
正确做法:
go
for len(queue) > 0 {
size := len(queue) // 固定当前层大小
for i := 0; i < size; i++ {
node := queue[0]
queue = queue[1:]
if i < size-1 { // 使用索引判断
node.Next = queue[0]
}
// ...
}
}
实战技巧总结
1. 完美二叉树特性利用
- 结构对称:左右子树结构完全相同
- 层数确定:所有叶子节点在同一层
- 节点数确定 :第
i层有2^(i-1)个节点
2. next 指针的双重作用
- 结果:存储答案(指向右侧节点)
- 工具:用于遍历当前层(在迭代方法中)
3. 空间优化思路
- 利用已有结构 :使用
next指针代替队列 - 逐层处理:处理完一层再处理下一层
- 避免存储:不存储所有节点,只存储必要信息
4. 递归 vs 迭代
- 递归:代码简洁,但空间复杂度 O(log n)
- 迭代:空间复杂度 O(1),但代码稍复杂
- 选择:根据题目要求选择合适方法
进阶扩展
扩展一:非完美二叉树(LeetCode 117)
- 节点可能缺失
- 需要处理
nil节点 - 需要找到每层第一个非空节点
扩展二:N 叉树
- 每个节点有多个子节点
- 需要连接所有相邻的子节点
- 算法需要相应调整
扩展三:从任意节点开始
- 给定树中某个节点
- 填充该节点所在层的所有
next指针 - 需要先找到该层的起始节点
扩展四:双向链表
- 不仅建立向右的
next指针 - 还建立向左的
prev指针 - 形成双向链表结构
应用场景
1. 树形结构的横向遍历
- 场景:需要按层遍历树,但不想使用队列
- 优势 :利用
next指针可以直接遍历同一层
2. 完美二叉树的特殊操作
- 场景:对完美二叉树进行层序相关操作
- 优势:可以利用结构特性优化算法
3. 空间受限环境
- 场景:内存有限,需要 O(1) 空间算法
- 优势:迭代方法满足空间要求
4. 树形数据结构的序列化
- 场景:需要按层序列化树结构
- 优势 :
next指针提供了层序信息
相关题目
- 117. 填充每个节点的下一个右侧节点指针 II:非完美二叉树版本
- 102. 二叉树的层序遍历:层序遍历基础
- 103. 二叉树的锯齿形层序遍历:层序遍历变种
- 199. 二叉树的右视图:利用层序遍历
总结
本题是二叉树层序遍历的经典变种 ,核心在于利用已建立的 next 指针实现空间优化。通过四种不同的方法,我们展示了:
- BFS 方法:直观易懂,适用于理解问题
- 递归方法:代码简洁,逻辑清晰
- 迭代 O(1) 方法:最优解,空间效率最高
- DFS 方法:深度优先,先处理父节点
关键要点:
- 完美二叉树的结构特性可以用于优化
next指针既可以存储结果,也可以作为遍历工具- 跨子树连接是本题的核心难点
- 空间优化需要充分利用已有结构
掌握本题有助于理解树形结构的层序遍历 和空间优化技巧,为后续更复杂的树形问题打下基础。
完整题解代码
go
package main
import (
"fmt"
)
// =========================== Node 定义 ===========================
type Node struct {
Val int
Left *Node
Right *Node
Next *Node
}
// =========================== 方法一:BFS层序遍历 ===========================
// 时间复杂度:O(n),空间复杂度:O(w),w为树的最大宽度
func connect1(root *Node) *Node {
if root == nil {
return nil
}
queue := []*Node{root}
for len(queue) > 0 {
size := len(queue)
for i := 0; i < size; i++ {
node := queue[0]
queue = queue[1:]
// 连接同层相邻节点(不是最后一个节点)
if i < size-1 {
node.Next = queue[0]
}
// 添加子节点到队列
if node.Left != nil {
queue = append(queue, node.Left)
}
if node.Right != nil {
queue = append(queue, node.Right)
}
}
}
return root
}
// =========================== 方法二:递归连接 ===========================
// 时间复杂度:O(n),空间复杂度:O(h),h为树的高度
func connect2(root *Node) *Node {
if root == nil {
return nil
}
// 连接同一父节点的子节点
if root.Left != nil {
root.Left.Next = root.Right
}
// 利用父节点的next连接跨子树的节点
if root.Right != nil && root.Next != nil {
root.Right.Next = root.Next.Left
}
// 递归处理左右子树
connect2(root.Left)
connect2(root.Right)
return root
}
// =========================== 方法三:迭代 O(1) 空间(最优解) ===========================
// 时间复杂度:O(n),空间复杂度:O(1)
func connect3(root *Node) *Node {
if root == nil {
return nil
}
// leftmost指向每层最左边的节点
leftmost := root
// 逐层处理,直到叶子节点层
for leftmost.Left != nil {
// 当前层的当前节点
curr := leftmost
// 遍历当前层的所有节点
for curr != nil {
// 连接同一父节点的子节点
curr.Left.Next = curr.Right
// 连接跨子树的节点
if curr.Next != nil {
curr.Right.Next = curr.Next.Left
}
// 移动到当前层的下一个节点
curr = curr.Next
}
// 移动到下一层
leftmost = leftmost.Left
}
return root
}
// =========================== 方法四:DFS前序遍历 ===========================
// 时间复杂度:O(n),空间复杂度:O(h),h为树的高度
func connect4(root *Node) *Node {
if root == nil {
return nil
}
// 连接当前节点的子节点
if root.Left != nil {
root.Left.Next = root.Right
// 如果当前节点有next,连接跨子树的节点
if root.Next != nil {
root.Right.Next = root.Next.Left
}
}
// 递归处理左右子树
connect4(root.Left)
connect4(root.Right)
return root
}
// =========================== 工具函数:从数组构建完美二叉树 ===========================
func arrayToTreeLevelOrder(arr []interface{}) *Node {
if len(arr) == 0 || arr[0] == nil {
return nil
}
root := &Node{Val: arr[0].(int)}
queue := []*Node{root}
i := 1
for i < len(arr) && len(queue) > 0 {
node := queue[0]
queue = queue[1:]
// 左子节点
if i < len(arr) && arr[i] != nil {
left := &Node{Val: arr[i].(int)}
node.Left = left
queue = append(queue, left)
}
i++
// 右子节点
if i < len(arr) && arr[i] != nil {
right := &Node{Val: arr[i].(int)}
node.Right = right
queue = append(queue, right)
}
i++
}
return root
}
// =========================== 工具函数:验证next指针连接 ===========================
func validateNextPointers(root *Node) []string {
if root == nil {
return []string{}
}
var result []string
leftmost := root
// 逐层遍历
for leftmost != nil {
curr := leftmost
level := []int{}
// 通过next指针遍历当前层
for curr != nil {
level = append(level, curr.Val)
curr = curr.Next
}
// 格式化输出
levelStr := ""
for i, v := range level {
if i > 0 {
levelStr += " -> "
}
levelStr += fmt.Sprintf("%d", v)
}
result = append(result, levelStr)
// 移动到下一层
leftmost = leftmost.Left
}
return result
}
// =========================== 工具函数:按层序输出(带next信息) ===========================
func levelOrderWithNext(root *Node) []interface{} {
if root == nil {
return []interface{}{}
}
var result []interface{}
leftmost := root
for leftmost != nil {
curr := leftmost
for curr != nil {
result = append(result, curr.Val)
curr = curr.Next
}
result = append(result, "#") // 层分隔符
leftmost = leftmost.Left
}
return result
}
// =========================== 工具函数:比较结果 ===========================
func equal(a, b []interface{}) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
// =========================== 测试 ===========================
func main() {
fmt.Println("=== LeetCode 116: 填充每个节点的下一个右侧节点指针 ===\n")
testCases := []struct {
name string
root *Node
expected []interface{}
}{
{
name: "空树",
root: arrayToTreeLevelOrder([]interface{}{}),
expected: []interface{}{},
},
{
name: "单节点",
root: arrayToTreeLevelOrder([]interface{}{1}),
expected: []interface{}{1, "#"},
},
{
name: "两层树",
root: arrayToTreeLevelOrder([]interface{}{1, 2, 3}),
expected: []interface{}{1, "#", 2, 3, "#"},
},
{
name: "三层完美二叉树",
root: arrayToTreeLevelOrder([]interface{}{1, 2, 3, 4, 5, 6, 7}),
expected: []interface{}{1, "#", 2, 3, "#", 4, 5, 6, 7, "#"},
},
{
name: "四层完美二叉树",
root: arrayToTreeLevelOrder([]interface{}{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}),
expected: []interface{}{1, "#", 2, 3, "#", 4, 5, 6, 7, "#", 8, 9, 10, 11, 12, 13, 14, 15, "#"},
},
}
methods := []struct {
name string
fn func(*Node) *Node
}{
{"方法一:BFS层序遍历", connect1},
{"方法二:递归连接", connect2},
{"方法三:迭代O(1)空间", connect3},
{"方法四:DFS前序遍历", connect4},
}
allPassed := true
for _, method := range methods {
fmt.Printf("--- %s ---\n", method.name)
passed := 0
for i, tc := range testCases {
// 重新构建树(因为会修改原树)
testRoot := arrayToTreeLevelOrder(getTreeArray(tc.name))
result := method.fn(testRoot)
got := levelOrderWithNext(result)
if equal(got, tc.expected) {
fmt.Printf(" Test %d: ✓ PASSED\n", i+1)
passed++
} else {
fmt.Printf(" Test %d: ✗ FAILED\n", i+1)
fmt.Printf(" 输入: %v\n", getTreeArray(tc.name))
fmt.Printf(" 期望: %v\n", tc.expected)
fmt.Printf(" 得到: %v\n", got)
allPassed = false
}
}
fmt.Printf("通过率: %d/%d\n\n", passed, len(testCases))
}
if allPassed {
fmt.Println("🎉 所有测试通过!")
} else {
fmt.Println("❌ 部分测试失败")
}
}
// 辅助函数:根据测试用例名称获取树数组
func getTreeArray(name string) []interface{} {
switch name {
case "空树":
return []interface{}{}
case "单节点":
return []interface{}{1}
case "两层树":
return []interface{}{1, 2, 3}
case "三层完美二叉树":
return []interface{}{1, 2, 3, 4, 5, 6, 7}
case "四层完美二叉树":
return []interface{}{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
default:
return []interface{}{}
}
}