切换主题
字数
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]
,可以按以下规则构建一棵二叉树:
- 数组索引与树节点的关系:
- 数组中的第一个元素
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
的右子节点。
为什么代码中有 left
和 right
?
尽管输入是一个数组,但它表示的是一个二叉树的结构。在层序遍历算法中,我们需要明确每个节点的左右子节点,因此在程序中我们会构建一个 二叉树结构,每个节点都有 left
和 right
指针来表示左右子树。
那为什么给leetcode提交时,不需要转换
你提到在 LeetCode 提交时不需要将输入的数组转换为二叉树,原因在于 LeetCode 在题目后台已经自动处理了这个转换。
LeetCode 的处理机制
在 LeetCode 上,题目通常会直接给你一个输入的数组或链表,但在你提交代码时,这些输入实际上已经被 LeetCode 的系统预处理,并转换成了相应的数据结构(如二叉树、链表等)。所以你不需要手动进行转换操作。
举例来说,题目给出如下输入:
输入:root = [3,9,20,null,null,15,7]
LeetCode 实际上已经将这个数组 [3,9,20,null,null,15,7]
在后台转换为一个二叉树,因此你在代码中会直接接收到一个 TreeNode
类型的二叉树对象,而不是数组。
LeetCode 的处理流程:
- 用户看到的输入:数组
[3,9,20,null,null,15,7]
表示的是树的层序遍历。 - 后台预处理:LeetCode 的系统自动将该数组转换为一个
TreeNode
类型的二叉树对象。 - 你接收的输入:在你提交的代码中,输入
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
}
}
};
参数解释:
root
是TreeNode
类型的根节点,值为3
。root.left
是根节点的左子节点,值为9
,并且9
没有左右子节点,因此root.left.left
和root.left.right
都为null
。root.right
是根节点的右子节点,值为20
,它有两个子节点,分别为15
和7
。
因此,LeetCode 传递给你的并不是数组,而是一个具有这种嵌套结构的二叉树对象。
你在代码中接收到的 root
参数:
当你提交代码时,实际上你会接收到上面的 root
对象。你可以直接使用 root.left
、root.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
声明的,但它的内容(即数组元素)仍然是可以改变的。我们可以使用push
、shift
等方法来操作队列的内容,而不会改变它的引用。 - 这也意味着,即使你用
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
是一个数组,用来保存二叉树中的节点。让我们来解释为什么加数组:
queue
是一个数组:queue
是一个队列(数组),用于存储等待处理的二叉树节点。- 广度优先搜索使用队列(FIFO)来逐层遍历二叉树。每次取出一个节点,处理它后,将它的左右子节点加入队列,接着处理下一层的节点。
初始值为
[root]
:- 初始时,我们的队列中只有根节点
root
,所以使用[root]
表示队列的初始状态。根节点root
是一个TreeNode
类型的对象,放在一个数组中表示当前队列中只有一个节点需要处理。 - 之后,在遍历过程中,队列会不断向其中添加新的节点(即左子节点和右子节点),从而实现层序遍历。
- 初始时,我们的队列中只有根节点
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
只代表当前正在处理的那一层的节点值。
详细步骤:
初始状态:
queue
初始化时只包含根节点。- 每一层开始时,我们创建一个空的
currentLevel
数组,用来存储这一层节点的值。
处理当前层的节点:
- 我们根据
queue.length
(即当前队列中的节点数量),知道这一层有多少个节点需要处理。 - 使用
for
循环,逐个取出queue
中的节点,并将它们的值加入currentLevel
中。
- 我们根据
将下一层的节点加入队列:
- 对于每一个节点,处理完后,检查它是否有左、右子节点。如果有,就将它们加入
queue
,为下一轮的遍历做准备。
- 对于每一个节点,处理完后,检查它是否有左、右子节点。如果有,就将它们加入
存储结果:
- 一旦当前层的所有节点都被处理完,我们将
currentLevel
(存储着当前层所有节点的值)加入result
。 - 然后我们继续处理下一层。
- 一旦当前层的所有节点都被处理完,我们将
总结:
currentLevel
是当前层的节点值:它仅存储当前正在遍历的这一层节点的值,等这一层的节点处理完之后,它会被加入到result
中。result
是整个遍历的结果:result
是最终结果,它是一个二维数组,每个子数组对应树的一层,存储的是那一层所有节点的值。
示例:
假设二叉树是这样的:
3
/ \
9 20
/ \
15 7
在遍历过程中:
- 第一层:
currentLevel = [3]
,然后将currentLevel
放入result
,result = [[3]]
。 - 第二层:
currentLevel = [9, 20]
,然后将currentLevel
放入result
,result = [[3], [9, 20]]
。 - 第三层:
currentLevel = [15, 7]
,然后将currentLevel
放入result
,result = [[3], [9, 20], [15, 7]]
。
最终返回的是 result
,即二叉树的层序遍历结果:[[3], [9, 20], [15, 7]]
。
那currentLevel 和 currentNode 有什么区别
在层序遍历的代码中,currentLevel
和 currentNode
是两个不同的概念,它们有着不同的角色和作用:
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
层序遍历步骤:
第一层:
- 队列最初包含根节点
[3]
。 currentNode = 3
,将3
的值加入currentLevel
,得到currentLevel = [3]
。- 将
9
和20
节点加入队列。 currentLevel
加入result
,得到result = [[3]]
。
- 队列最初包含根节点
第二层:
- 队列现在是
[9, 20]
。 - 处理
currentNode = 9
,将9
的值加入currentLevel
,得到currentLevel = [9]
。 9
没有子节点,不添加任何节点到队列。- 处理
currentNode = 20
,将20
的值加入currentLevel
,得到currentLevel = [9, 20]
。 - 将
15
和7
节点加入队列。 currentLevel
加入result
,得到result = [[3], [9, 20]]
。
- 队列现在是
第三层:
- 队列现在是
[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 过程中扮演的角色:
- 取出节点:
shift()
取出队列的第一个节点,并且在之后处理这个节点。 - 更新队列:取出节点后,队列长度减1,队列中的其他节点向前移动。
- 添加子节点:然后根据当前节点的左右子节点,将它们添加到队列末尾。
示例解释:
假设 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)**的队列操作。
贡献者
sunchengzhi