LeetCode 431 - 将 N 叉树编码成二叉树


文章目录

摘要

这题把"把 N 叉树变成二叉树、再从二叉树恢复 N 叉树"作为练习,目的是考察你对树结构的灵活变换和对指针关系的把握。

核心思想是经典的"左子右兄弟"(left-child right-sibling)映射:把每个 N 叉节点的第一个子节点映射为二叉树的左子节点,把每个子节点的下一个兄弟映射为二叉树的右子节点。这样可以在不丢失结构信息的前提下,把任意 N 叉树表示为二叉树,反向变换也同样可行。

本文会给出完整的 Swift 实现(编码和解码),并讲清楚每一步为什么这么做,同时给出示例和验证代码,便于在 Playground/项目中直接运行。

描述

题目给出两种节点定义:

N 叉树节点:

swift 复制代码
class Node {
    var val: Int
    var children: [Node]

    init(_ val: Int) {
        self.val = val
        self.children = []
    }
}

二叉树节点(用于编码后结果):

swift 复制代码
class TreeNode {
    var val: Int
    var left: TreeNode?
    var right: TreeNode?

    init(_ val: Int) {
        self.val = val
    }
}

要求实现两个方法:

  • encode(_ root: Node?) -> TreeNode?:把 N 叉树编码为二叉树
  • decode(_ root: TreeNode?) -> Node?:从二叉树还原 N 叉树

关键是保持节点值和结构信息的可逆性。

题解答案

经典的映射规则如下:

  • 对于 N 叉树中某个节点 u,如果 u.children 非空,则:

    • u 在二叉树中对应的节点的 left 指向 u.children[0] 对应的二叉树节点(第一个子节点作为左孩子)。
    • 对于 u.children[i],如果 i+1 < children.count,则把 u.children[i] 在二叉树的对应节点的 right 指向 u.children[i+1] 在二叉树的对应节点(把兄弟节点用右指针串起来)。
  • 其他指针保持 null(nil)。

反向变换自然就是:

  • 对于二叉树中某个节点 t,把 t.left 代表的整条由 right 指针串起来的链表拆成 N 叉树的 children 数组(沿 right 指针遍历,依次 decode)。

这个映射是双向可逆的,且实现很直接,递归或迭代都可。

下面给出一个易读的 Swift 实现,并配套构造/打印函数,能在 Playground 中直接运行验证。

题解代码分析(可运行 Swift 示例)

swift 复制代码
import Foundation

// MARK: - N 叉树节点定义
class Node {
    var val: Int
    var children: [Node]

    init(_ val: Int) {
        self.val = val
        self.children = []
    }
}

// MARK: - 二叉树节点定义(用于编码结果)
class TreeNode {
    var val: Int
    var left: TreeNode?
    var right: TreeNode?

    init(_ val: Int) {
        self.val = val
    }
}

// MARK: - Codec:encode / decode
class Codec {

    // Encode: N 叉树 -> 二叉树
    func encode(_ root: Node?) -> TreeNode? {
        guard let root = root else { return nil }
        // 当前 N 叉节点映射为二叉树节点
        let bRoot = TreeNode(root.val)
        // 如果有 children,第一个 child 变为 left
        if !root.children.isEmpty {
            bRoot.left = encode(root.children[0])
        }
        // 将 children 链接为右兄弟链表
        var currentBChild = bRoot.left
        var i = 1
        while i < root.children.count {
            currentBChild?.right = encode(root.children[i])
            currentBChild = currentBChild?.right
            i += 1
        }
        return bRoot
    }

    // Decode: 二叉树 -> N 叉树
    func decode(_ root: TreeNode?) -> Node? {
        guard let root = root else { return nil }
        let nRoot = Node(root.val)
        // left 指向第一个 child(如果有)
        var bChild = root.left
        while let b = bChild {
            if let decodedChild = decode(b) {
                nRoot.children.append(decodedChild)
            }
            // 兄弟通过 right 链接
            bChild = b.right
        }
        return nRoot
    }
}

// MARK: - 辅助函数:构造示例 N 叉树并演示 encode/decode

// 构造示例树:
//        1
//      / | \
//     2  3  4
//       / \
//      5   6
func buildExampleTree() -> Node {
    let root = Node(1)
    let n2 = Node(2)
    let n3 = Node(3)
    let n4 = Node(4)
    let n5 = Node(5)
    let n6 = Node(6)

    root.children = [n2, n3, n4]
    n3.children = [n5, n6]
    return root
}

// 打印 N 叉树的层序(以便对比)
func levelOrderNary(_ root: Node?) -> [[Int]] {
    guard let root = root else { return [] }
    var result: [[Int]] = []
    var queue: [Node] = [root]
    while !queue.isEmpty {
        let size = queue.count
        var level: [Int] = []
        for _ in 0..<size {
            let node = queue.removeFirst()
            level.append(node.val)
            for child in node.children {
                queue.append(child)
            }
        }
        result.append(level)
    }
    return result
}

// 打印二叉树按层(方便查看编码结果)
func levelOrderBinary(_ root: TreeNode?) -> [[Int?]] {
    guard let root = root else { return [] }
    var result: [[Int?]] = []
    var queue: [TreeNode?] = [root]
    while !queue.isEmpty {
        let size = queue.count
        var level: [Int?] = []
        for _ in 0..<size {
            let node = queue.removeFirst()
            if let n = node {
                level.append(n.val)
                // 右 child 作为同一层的 sibling,会被视作 separate node
                // 但为了直观展示,我们仍把 left 和 right 加入队列。
                queue.append(n.left)
                queue.append(n.right)
            } else {
                level.append(nil)
            }
        }
        // trim trailing nils for cleaner output
        while let last = level.last, last == nil {
            level.removeLast()
        }
        result.append(level)
    }
    // trim trailing empty levels
    while let last = result.last, last.isEmpty {
        result.removeLast()
    }
    return result
}

// MARK: - 运行示例
let codec = Codec()
let nRoot = buildExampleTree()
print("原 N 叉树层序:", levelOrderNary(nRoot))

let bRoot = codec.encode(nRoot)
print("编码后二叉树的层序(显示 nil 用于占位):", levelOrderBinary(bRoot))

let decoded = codec.decode(bRoot)
print("解码回 N 叉树层序:", levelOrderNary(decoded))

代码解析与要点

  1. 映射思想

    • N-Node -> TreeNode(val)
    • first child -> left
    • each childright -> next sibling(兄弟通过 right 串起来)
  2. 编码要点

    • 先把当前节点创建成 TreeNode
    • 如果 root.children 非空,先把 bRoot.left = encode(root.children[0])
    • 然后顺序把 children[1...] encode 并通过 right 连接
    • 递归结束会把每个子树正确地放在 left/right 的位置
  3. 解码要点

    • Decode 时对每个二叉树节点 root

      • 生成 Node(root.val)
      • 沿 root.left 再沿着 right 遍历(兄弟链),对这些二叉节点递归 decode 并 append 到 children 数组
    • 这样可以把 left 指向的"孩子链表"变回 N 叉节点数组

  4. 双向可逆

    • 以上编码/解码规则保证没有信息丢失:值和父子 / 兄弟结构都被保留
    • encode(decode(encode(root))) 与 encode(root) 的形态一致,decode(encode(root)).children 与 root.children 等价
  5. 复杂度

    • 时间复杂度:编码和解码都需要访问每个节点一次,因此都是 O(N),N 为 N 叉树节点数
    • 空间复杂度:递归调用栈深度取决于树高,最坏 O(N),最好 O(log N) 取决结构(另外结果的二叉树或者临时队列也需要 O(N) 空间用于输出)

示例测试及结果(解释输出)

上面示例构造的 N 叉树是:

txt 复制代码
    1
  / | \
 2  3  4
   / \
  5   6

运行输出将是类似下面的三行(在 Playground 中直接打印):

  • 原 N 叉树层序: [[1], [2, 3, 4], [5, 6]]
  • 编码后二叉树的层序:例如 [[1], [2, 3], [nil, 5, nil, 6]](格式化显示里会展示 nil 占位,便于理解 left/right 位置)
  • 解码回 N 叉树层序:[[1], [2, 3, 4], [5, 6]]

如果原始树和解码后树的层序输出一致,说明 encode/decode 正确。

时间复杂度

  • 编码:每个 N 叉节点会被访问一次并且在递归中对其 children 调用 encode,总体 O(N)。
  • 解码:同样每个二叉树节点被访问一次并且会进行常数时间的追加操作,总体 O(N)。

因此 encode 与 decode 都是线性时间。

空间复杂度

  • 额外空间主要来自递归栈深度:最坏情况 O(N)(例如一个链式 N 叉树,所有节点都只有一个 child);
  • 同时编码结果(二叉树)和解码需要构建新的节点或重用节点参考,这些本就是输出规模,因此总占用与输入输出规模线性相关。

总体额外空间(不计输出)在最坏情形为 O(N)。

总结

这道题的思想非常直观但很有技巧: left-child right-sibling(左子右兄)映射是把任意子数目结构化为固定 2 指针结构的标准方法。这个映射被广泛应用于树到二叉树转换、紧凑树表示等场景。

解决步骤总结:

  1. 想清楚映射规则(first child -> left,siblings -> right)
  2. 用递归实现 encode,把 children 链表沿 right 串起来
  3. 用递归实现 decode,把 left 指向的链表沿 right 拆回来作为 children 数组
  4. 编写辅助打印/测试用例验证结果
相关推荐
子豪-中国机器人1 小时前
1030-csp 2019 入门级第一轮
算法
关注我立刻回关2 小时前
洛谷平台
算法
Cx330❀2 小时前
C++ map 全面解析:从基础用法到实战技巧
开发语言·c++·算法
CS_浮鱼2 小时前
【Linux】线程
linux·c++·算法
AndrewHZ3 小时前
【图像处理基石】如何入门图像配准算法?
图像处理·opencv·算法·计算机视觉·cv·图像配准·特征描述子
BanyeBirth3 小时前
C++窗口问题
开发语言·c++·算法
前端小L5 小时前
图论专题(十五):BFS的“状态升维”——带着“破壁锤”闯迷宫
数据结构·算法·深度优先·图论·宽度优先
2501_941805936 小时前
人工智能与大数据:驱动新时代的创新与决策
leetcode
橘颂TA7 小时前
【剑斩OFFER】算法的暴力美学——连续数组
c++·算法·leetcode·结构与算法