Skip to content
字数
6076 字
阅读时间
25 分钟

二叉树的层序遍历

ts
function levelOrder(root: TreeNode | null): number[][] {
  const result: number[][] = [];
  if (root === null) return result;

  const queue: TreeNode[] = [root];  // 队列初始化,存入根节点

  while (queue.length > 0) {
    const levelSize = queue.length;  // 获取当前层的节点数量
    const currentLevel: number[] = [];  // 存储当前层节点的值

    for (let i = 0; i < levelSize; i++) {
      const currentNode = queue.shift() as TreeNode;  // 取出当前层的节点
      currentLevel.push(currentNode.val);  // 保存当前节点的值

      // 将当前节点的左右子节点(如果存在)加入队列
      if (currentNode.left !== null) {
        queue.push(currentNode.left);
      }
      if (currentNode.right !== null) {
        queue.push(currentNode.right);
      }
    }

    result.push(currentLevel);  // 当前层处理完,加入结果数组
  }

  return result;  // 返回最终的层序遍历结果
}

解题时的疑问:

'为什么创建队列用const'

输入的不是一个数组吗,为什么又left和right

'实际上的参数是一个什么样子的对象'

输入的不是一个数组吗,为什么又left和right

你的问题中提到的输入,例如 root = [3, 9, 20, null, null, 15, 7],在题目中是以数组形式表示一个二叉树,但实际上它并不是一个普通数组,而是二叉树的层序遍历表示方法。

这意味着这个数组其实是描述了一棵二叉树的结构,而我们在代码中要将其转换为二叉树的形式,并对树进行遍历。

如何理解数组表示的二叉树?

给定的数组 [3, 9, 20, null, null, 15, 7],可以按以下规则构建一棵二叉树:

  1. 数组索引与树节点的关系
    • 数组中的第一个元素 3 是二叉树的根节点。
    • 索引 i 处的节点的左子节点在索引 2*i + 1,右子节点在索引 2*i + 2

示例解析:

plaintext
输入数组: [3, 9, 20, null, null, 15, 7]

表示的二叉树:
       3
     /   \
    9    20
        /   \
      15     7

转换过程:

  • 数组的第一个元素 3 是根节点。
  • 第二个元素 9 是根节点 3 的左子节点,第三个元素 20 是根节点 3 的右子节点。
  • 第四个和第五个元素为 null,表示节点 9 没有左右子节点。
  • 第六个元素 15 是节点 20 的左子节点,第七个元素 7 是节点 20 的右子节点。

为什么代码中有 leftright

尽管输入是一个数组,但它表示的是一个二叉树的结构。在层序遍历算法中,我们需要明确每个节点的左右子节点,因此在程序中我们会构建一个 二叉树结构,每个节点都有 leftright 指针来表示左右子树。

那为什么给leetcode提交时,不需要转换

你提到在 LeetCode 提交时不需要将输入的数组转换为二叉树,原因在于 LeetCode 在题目后台已经自动处理了这个转换。

LeetCode 的处理机制

在 LeetCode 上,题目通常会直接给你一个输入的数组或链表,但在你提交代码时,这些输入实际上已经被 LeetCode 的系统预处理,并转换成了相应的数据结构(如二叉树、链表等)。所以你不需要手动进行转换操作。

举例来说,题目给出如下输入:

输入:root = [3,9,20,null,null,15,7]

LeetCode 实际上已经将这个数组 [3,9,20,null,null,15,7] 在后台转换为一个二叉树,因此你在代码中会直接接收到一个 TreeNode 类型的二叉树对象,而不是数组。

LeetCode 的处理流程:

  1. 用户看到的输入:数组 [3,9,20,null,null,15,7] 表示的是树的层序遍历。
  2. 后台预处理:LeetCode 的系统自动将该数组转换为一个 TreeNode 类型的二叉树对象。
  3. 你接收的输入:在你提交的代码中,输入 root 实际上已经是一个 TreeNode 类型的对象,因此你可以直接对 root 进行层序遍历等操作。

因此,当你在 LeetCode 上提交解决二叉树问题时,输入参数通常已经是二叉树节点类型(如 TreeNode),你只需要处理如何遍历或操作这个树。

实际上的参数是一个什么样子的对象

在 LeetCode 的二叉树题目中,虽然题目中的输入是以数组的形式给出的,例如 [3,9,20,null,null,15,7],但在你实际提交代码时,参数是一个表示二叉树节点的对象。这个对象通常是 TreeNode 类型,LeetCode 会预先构建这个二叉树,并将其作为参数传递给你的函数。

实际传递的对象

假设你有一个输入数组 [3,9,20,null,null,15,7],LeetCode 会将这个数组转换为一个 TreeNode 类型的二叉树对象,并以如下形式传递给你的代码:

ts
class TreeNode {
  val: number;
  left: TreeNode | null;
  right: TreeNode | null;
  constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
    this.val = (val === undefined ? 0 : val);
    this.left = (left === undefined ? null : left);
    this.right = (right === undefined ? null : right);
  }
}

根据输入 [3, 9, 20, null, null, 15, 7],实际传递给你的参数是一个如下结构的 TreeNode 对象:

       3
     /   \
    9    20
        /   \
      15     7

对象表示的二叉树:

LeetCode 会将数组 [3,9,20,null,null,15,7] 转换成一个具有如下结构的 TreeNode 对象:

ts
root = {
  val: 3,
  left: {
    val: 9,
    left: null,
    right: null
  },
  right: {
    val: 20,
    left: {
      val: 15,
      left: null,
      right: null
    },
    right: {
      val: 7,
      left: null,
      right: null
    }
  }
};

参数解释:

  • rootTreeNode 类型的根节点,值为 3
  • root.left 是根节点的左子节点,值为 9,并且 9 没有左右子节点,因此 root.left.leftroot.left.right 都为 null
  • root.right 是根节点的右子节点,值为 20,它有两个子节点,分别为 157

因此,LeetCode 传递给你的并不是数组,而是一个具有这种嵌套结构的二叉树对象

你在代码中接收到的 root 参数:

当你提交代码时,实际上你会接收到上面的 root 对象。你可以直接使用 root.leftroot.right 来访问左右子节点。

为什么创建队列用const

在 JavaScript 和 TypeScript 中,const 用于声明一个常量引用,即变量的引用地址不会改变,但这个变量本身可以是可变的,特别是对于对象数组这样的复杂数据类型。

在 BFS 算法中,我们使用 const 声明队列是合适的,因为我们并不是要改变队列本身的引用,而是对它的内容进行操作(例如添加或删除元素)。因此,const 关键字可以保证这个队列的引用不被意外更改,而队列中的元素可以动态变化。

详细解释为什么使用 const

1. const 确保引用不变

  • 使用 const 声明的变量不能重新赋值。对于队列来说,我们需要它一直指向同一个数组。
  • 但是,const 并不意味着队列的内容不可变。你可以对这个队列进行增删操作,向它添加或删除元素,而不改变其引用。

例如:

ts
const queue: number[] = [];
queue.push(1);  // 可以添加元素
queue.shift();  // 可以移除元素

在上面的代码中,queue 是用 const 声明的数组,尽管它的内容发生了变化(元素的增删),但 queue 这个引用本身始终没有改变,仍然指向同一个数组。

2. const 避免意外的引用更改

  • 使用 let 允许你重新分配队列,比如指向一个全新的数组,这在某些情况下可能引发错误或逻辑问题。使用 const 可以防止这样的情况发生,确保队列始终保持为同一个实例。

3. 对队列进行增删操作是合法的

  • 队列是一个数组,虽然队列本身是 const 声明的,但它的内容(即数组元素)仍然是可以改变的。我们可以使用 pushshift 等方法来操作队列的内容,而不会改变它的引用。
  • 这也意味着,即使你用 const 声明队列,你仍然可以动态地操作队列。

const queue: TreeNode[] = [root]; 为什么在treenode类后面还要加一个数组

在 TypeScript 中,TreeNode[] 表示的是一个**TreeNode 类型的数组**。让我们来逐步解释这一点。

1. TreeNode 类型

首先,TreeNode 是一个表示二叉树节点的类。每个 TreeNode 对象都有三个主要属性:

  • val:节点的值。
  • left:左子节点,类型是 TreeNode | null(可以是另一个 TreeNode 对象或 null)。
  • right:右子节点,类型同样是 TreeNode | null
ts
class TreeNode {
  val: number;
  left: TreeNode | null;
  right: TreeNode | null;
  constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
    this.val = val === undefined ? 0 : val;
    this.left = left === undefined ? null : left;
    this.right = right === undefined ? null : right;
  }
}

2. TreeNode[] 表示什么

TreeNode[] 表示**TreeNode 类型的数组**,即数组中的每个元素都必须是 TreeNode 类型的对象。这是 TypeScript 类型系统的一部分,用来确保数组中的元素类型一致。

在广度优先搜索(BFS)算法中,我们使用队列来遍历每一层的节点,而队列通常是一个数组。因为这个数组需要存储二叉树的节点,所以我们声明它的类型为 TreeNode[],表示它是一个包含 TreeNode 对象的数组

3. 为什么在 TreeNode 后面加一个数组?

你看到的代码:

ts
const queue: TreeNode[] = [root];

这里的 queue 是一个数组,用来保存二叉树中的节点。让我们来解释为什么加数组:

  1. queue 是一个数组

    • queue 是一个队列(数组),用于存储等待处理的二叉树节点。
    • 广度优先搜索使用队列(FIFO)来逐层遍历二叉树。每次取出一个节点,处理它后,将它的左右子节点加入队列,接着处理下一层的节点。
  2. 初始值为 [root]

    • 初始时,我们的队列中只有根节点 root,所以使用 [root] 表示队列的初始状态。根节点 root 是一个 TreeNode 类型的对象,放在一个数组中表示当前队列中只有一个节点需要处理。
    • 之后,在遍历过程中,队列会不断向其中添加新的节点(即左子节点和右子节点),从而实现层序遍历。
  3. TreeNode[] 是类型注解

    • TreeNode[] 是 TypeScript 中的类型注解,表示 queue 是一个由 TreeNode 类型组成的数组。它确保队列中的每个元素都是一个 TreeNode 类型的对象。

总结:

  • queue: TreeNode[]:表示队列是一个 TreeNode 类型的数组,用于存储树的节点。
  • [root]:初始化队列,队列的第一个元素是根节点 root
  • 在广度优先遍历中,队列用来逐层存储节点,便于按层访问和处理每个节点。

currentLevel是当前遍历到的那层节点的值,还是已经遍历完的二叉树

currentLevel当前正在遍历的那一层节点的值,并不是已经遍历完的整个二叉树。它用于存储当前层所有节点的值,并在完成这一层的遍历后,将其加入到最终结果 result 中。

具体解释:

在广度优先搜索(BFS)中,我们逐层遍历二叉树,每次处理一层的所有节点,然后将这些节点的值记录下来。在每一层遍历时,currentLevel 用来临时存储该层的节点值。

BFS 代码中的 currentLevel

ts
function levelOrder(root: TreeNode | null): number[][] {
  const result: number[][] = [];
  if (root === null) return result;

  const queue: TreeNode[] = [root];  // 队列初始化,只包含根节点

  while (queue.length > 0) {
    const levelSize = queue.length;  // 当前层节点的数量
    const currentLevel: number[] = [];  // 存储当前层节点的值

    for (let i = 0; i < levelSize; i++) {
      const currentNode = queue.shift() as TreeNode;  // 取出队列中的节点
      currentLevel.push(currentNode.val);  // 将该节点的值加入当前层

      // 将左右子节点加入队列,供下一层使用
      if (currentNode.left !== null) {
        queue.push(currentNode.left);
      }
      if (currentNode.right !== null) {
        queue.push(currentNode.right);
      }
    }

    result.push(currentLevel);  // 将当前层结果加入最终的结果中
  }

  return result;  // 返回层序遍历的结果
}

currentLevel 是什么?

  • 定义currentLevel 是一个临时数组,用来存储当前层节点的值。在处理完这一层的所有节点后,会将 currentLevel 放入 result,并开始处理下一层的节点。
  • 何时存储:每当处理完当前层的所有节点时,currentLevel 被存入 result
  • 作用currentLevel 只代表当前正在处理的那一层的节点值。

详细步骤:

  1. 初始状态

    • queue 初始化时只包含根节点。
    • 每一层开始时,我们创建一个空的 currentLevel 数组,用来存储这一层节点的值。
  2. 处理当前层的节点

    • 我们根据 queue.length(即当前队列中的节点数量),知道这一层有多少个节点需要处理。
    • 使用 for 循环,逐个取出 queue 中的节点,并将它们的值加入 currentLevel 中。
  3. 将下一层的节点加入队列

    • 对于每一个节点,处理完后,检查它是否有左、右子节点。如果有,就将它们加入 queue,为下一轮的遍历做准备。
  4. 存储结果

    • 一旦当前层的所有节点都被处理完,我们将 currentLevel(存储着当前层所有节点的值)加入 result
    • 然后我们继续处理下一层。

总结:

  • currentLevel 是当前层的节点值:它仅存储当前正在遍历的这一层节点的值,等这一层的节点处理完之后,它会被加入到 result 中。
  • result 是整个遍历的结果result 是最终结果,它是一个二维数组,每个子数组对应树的一层,存储的是那一层所有节点的值。

示例:

假设二叉树是这样的:

       3
     /   \
    9    20
        /   \
      15     7

在遍历过程中:

  • 第一层currentLevel = [3],然后将 currentLevel 放入 resultresult = [[3]]
  • 第二层currentLevel = [9, 20],然后将 currentLevel 放入 resultresult = [[3], [9, 20]]
  • 第三层currentLevel = [15, 7],然后将 currentLevel 放入 resultresult = [[3], [9, 20], [15, 7]]

最终返回的是 result,即二叉树的层序遍历结果:[[3], [9, 20], [15, 7]]

那currentLevel 和 currentNode 有什么区别

在层序遍历的代码中,currentLevelcurrentNode 是两个不同的概念,它们有着不同的角色和作用:

1. currentLevel 的作用

  • 定义currentLevel 是一个数组,用来存储当前层所有节点的值
  • 作用:它用于临时保存每一层的节点值,在遍历完当前层的所有节点后,会将 currentLevel 数组存储到最终的结果 result 中。每次进入新的一层遍历时,currentLevel 会被重新初始化为空数组。

2. currentNode 的作用

  • 定义currentNode 是一个 TreeNode 类型的对象,表示当前正在处理的具体某个节点
  • 作用currentNode 是指向队列中当前正在处理的树的某个节点。我们需要通过它来访问该节点的值、左子节点和右子节点,并决定是否将其子节点加入到队列中,供下一轮遍历时使用。

区别总结:

  • currentLevel 是一个数组,用于收集当前层的所有节点值。
  • currentNode 是一个单独的 TreeNode 对象,表示我们正在处理的当前层中的某一个节点。

层序遍历中的具体作用:

  • currentLevel

    • 在处理一层所有节点时,currentLevel 用来暂时存储这一层的节点值。
    • 每处理完一层的所有节点之后,会将 currentLevel 存入 result 数组,然后清空 currentLevel,为下一层做准备。
  • currentNode

    • 每次从队列中取出一个节点时,currentNode 就是当前处理的那个节点。
    • 处理 currentNode 时,我们将它的值加入到 currentLevel 数组中,并且将它的左右子节点(如果存在)加入到队列中,以便在后续的层中继续处理这些子节点。

示例代码解释:

ts
function levelOrder(root: TreeNode | null): number[][] {
  const result: number[][] = [];  // 用于存储最终的层序遍历结果
  if (root === null) return result;

  const queue: TreeNode[] = [root];  // 初始化队列,最开始包含根节点

  while (queue.length > 0) {
    const levelSize = queue.length;  // 当前层节点的数量
    const currentLevel: number[] = [];  // 用于存储当前层的所有节点值

    for (let i = 0; i < levelSize; i++) {
      const currentNode = queue.shift() as TreeNode;  // 取出当前层的一个节点
      currentLevel.push(currentNode.val);  // 将该节点的值加入到当前层数组中

      // 如果有左子节点,将其加入到队列中
      if (currentNode.left !== null) {
        queue.push(currentNode.left);
      }
      // 如果有右子节点,将其加入到队列中
      if (currentNode.right !== null) {
        queue.push(currentNode.right);
      }
    }

    result.push(currentLevel);  // 将当前层的节点值加入到结果中
  }

  return result;  // 返回最终的层序遍历结果
}

具体过程分析:

  • currentLevel

    • 在每一轮的 while 循环开始时,currentLevel 初始化为空数组,用于存储当前这一层所有节点的值。
    • for 循环结束时,currentLevel 已经包含了这一层所有节点的值,之后将其添加到 result 数组中。
  • currentNode

    • for 循环内部,每次调用 queue.shift() 会从队列中取出当前层的一个节点,并将其赋值给 currentNode
    • 然后我们对 currentNode 进行处理:将它的 val 加入 currentLevel 数组中,并且将它的左右子节点(如果存在)加入到队列中,以便在下一层进行处理。

执行过程示例:

假设输入的二叉树为:

       3
     /   \
    9    20
        /   \
      15     7

层序遍历步骤:

  1. 第一层

    • 队列最初包含根节点 [3]
    • currentNode = 3,将 3 的值加入 currentLevel,得到 currentLevel = [3]
    • 920 节点加入队列。
    • currentLevel 加入 result,得到 result = [[3]]
  2. 第二层

    • 队列现在是 [9, 20]
    • 处理 currentNode = 9,将 9 的值加入 currentLevel,得到 currentLevel = [9]
    • 9 没有子节点,不添加任何节点到队列。
    • 处理 currentNode = 20,将 20 的值加入 currentLevel,得到 currentLevel = [9, 20]
    • 157 节点加入队列。
    • currentLevel 加入 result,得到 result = [[3], [9, 20]]
  3. 第三层

    • 队列现在是 [15, 7]
    • 处理 currentNode = 15,将 15 的值加入 currentLevel,得到 currentLevel = [15]
    • 15 没有子节点。
    • 处理 currentNode = 7,将 7 的值加入 currentLevel,得到 currentLevel = [15, 7]
    • 7 没有子节点。
    • currentLevel 加入 result,得到 result = [[3], [9, 20], [15, 7]]

总结:

  • currentNode:表示当前正在处理的树节点,它是一个具体的节点对象。
  • currentLevel:表示当前层的所有节点值,它是一个数组,存储当前层的所有节点值,并在该层处理完后存入结果。

queue.shift()的语法

queue.shift() 是 JavaScript 和 TypeScript 中的一个数组方法,shift() 用于从数组中移除并返回第一个元素。这是一个数组操作方法,常用于实现先进先出(FIFO)的数据结构,比如队列。

shift() 方法的工作原理:

  • 作用:移除数组的第一个元素,并返回该元素。原数组长度会减1,其余元素向前移动。
  • 语法array.shift()
    • array 是你要操作的数组(在你的例子中是 queue)。
    • 返回值是被移除的数组元素。如果数组为空,shift() 会返回 undefined

queue.shift() 的含义:

在广度优先搜索(BFS)或其他类似算法中,queue 通常表示一个队列,shift() 用于从队列中移除并返回最前面的元素,即最早进入队列的元素(FIFO)。

例子:

ts
const queue: number[] = [1, 2, 3, 4];

const firstElement = queue.shift();  // 移除并返回第一个元素
console.log(firstElement);  // 输出: 1
console.log(queue);  // 输出: [2, 3, 4],数组剩下的元素

在 BFS 中的应用:

在二叉树的层序遍历中,queue.shift() 用来从队列中取出当前层的节点进行处理,然后将该节点的左右子节点(如果有)加入队列。

示例:

ts
const queue: TreeNode[] = [root];  // 队列初始化,包含根节点

while (queue.length > 0) {
  const currentNode = queue.shift();  // 取出队列的第一个节点
  if (currentNode !== undefined) {
    console.log(currentNode.val);  // 输出当前节点的值

    // 如果有左子节点,将其加入队列
    if (currentNode.left !== null) {
      queue.push(currentNode.left);
    }
    // 如果有右子节点,将其加入队列
    if (currentNode.right !== null) {
      queue.push(currentNode.right);
    }
  }
}

queue.shift() 在这个 BFS 过程中扮演的角色:

  1. 取出节点shift() 取出队列的第一个节点,并且在之后处理这个节点。
  2. 更新队列:取出节点后,队列长度减1,队列中的其他节点向前移动。
  3. 添加子节点:然后根据当前节点的左右子节点,将它们添加到队列末尾。

示例解释:

假设 queue = [root],初始时队列中只有一个元素(根节点)。在每一轮 BFS 中,queue.shift() 将会取出队列中第一个元素,并将其左右子节点依次加入队列,直到所有节点都被处理完毕。

阶段1:

  • 初始队列:[root]
  • queue.shift() 取出 root 节点,处理该节点。
  • root 的左右子节点加入队列。
  • 队列更新为:[root.left, root.right]

阶段2:

  • 当前队列:[root.left, root.right]
  • queue.shift() 取出 root.left 节点,处理该节点。
  • root.left 的左右子节点加入队列(如果有)。
  • 队列更新为:[root.right, root.left.left, root.left.right]

依次类推,直到队列为空为止。

总结:

  • queue.shift() 从队列中移除并返回队列的第一个元素,是实现广度优先搜索(BFS)等队列操作的关键。
  • shift() 操作后的队列长度会减少,剩下的元素会前移。
  • 它非常适合用来实现**先进先出(FIFO)**的队列操作。

贡献者

The avatar of contributor named as sunchengzhi sunchengzhi

文件历史

撰写