LeetCode 1361. 验证二叉树
字数 3208 2025-12-24 22:29:58
LeetCode 1361. 验证二叉树
题目描述
二叉树上有 n 个节点,编号从 0 到 n-1。我们用一个长度为 n 的整数数组 leftChild 和一个长度为 n 的整数数组 rightChild 来表示这棵树,其中:
leftChild[i]表示节点i的左子节点编号。如果i没有左子节点,则leftChild[i] = -1。rightChild[i]表示节点i的右子节点编号。如果i没有右子节点,则rightChild[i] = -1。
你需要验证输入的这两个数组是否构成一棵有效的二叉树。一棵有效的二叉树必须满足:
- 所有节点有且只有一个父节点,除了根节点(根节点没有父节点)。
- 树中不存在环。
- 所有节点必须是连通的(即从根节点出发,可以到达所有节点)。
解题思路
这道题的关键是识别出二叉树的根节点,并从它开始验证结构的合法性。我们可以将其分解为以下几个步骤:
-
寻找根节点:根据定义,根节点是所有节点中,唯一一个没有父节点的节点。我们可以通过遍历
leftChild和rightChild数组,记录每个节点是否有父节点。最后,那个没有被任何节点指向(即没有父节点)的节点,就是候选的根节点。注意,如果找到了多个这样的节点,说明结构分散成了多棵树,不符合“所有节点连通”的条件,因此是无效的。 -
深度优先搜索(DFS)验证:一旦我们确定了候选的根节点,就可以从它开始,进行深度优先搜索(或广度优先搜索),来验证剩下的两个条件:无环和全连通。
- 无环检查:在DFS过程中,我们访问一个节点时,就将其标记为“正在访问”或“已访问”。如果我们在探索过程中,遇到了一个“正在访问”的节点,说明存在环。为了避免重复访问导致无限循环,当我们探索完一个节点的所有后代后,将其标记为“已访问”,这样下次再遇到它就知道是合法的重复引用(虽然二叉树结构不允许一个节点有多个父节点,但DFS的防重逻辑能帮助检查环)。
- 全连通检查:在DFS过程中,我们记录访问到了多少个节点。最终,如果访问到的节点数等于总节点数
n,说明从根节点出发可以到达所有节点,即全连通;否则,说明存在无法从根节点到达的节点,结构无效。
-
数据结构:我们需要一个数据结构来记录每个节点的状态(未访问、正在访问、已访问),以及一个变量来记录每个节点是否有父节点。
解题步骤详解
我们按照上面的思路,一步一步来实现:
步骤 1:初始化与寻找根节点
- 给定
n个节点,编号从 0 到 n-1。 - 我们创建一个布尔数组
hasParent,长度为n,初始值全为false,表示每个节点初始时都没有父节点。 - 遍历
leftChild和rightChild数组:- 对于
leftChild[i],如果它的值不为-1,则将hasParent[leftChild[i]]标记为true,表示这个左子节点有了父节点i。 - 对于
rightChild[i],做同样的处理。
- 对于
- 遍历结束后,检查
hasParent数组。理论上,一棵有效的二叉树应该有且仅有一个节点的hasParent值为false,这个节点就是根节点。如果hasParent数组中false的个数不为 1,说明要么没有根(每个节点都有父节点,形成了环),要么有多个根(森林),都直接返回false。
步骤 2:从根节点开始深度优先搜索
- 假设我们找到了一个候选根节点
root。 - 我们创建一个整数数组
visited,长度为n,用来标记每个节点的状态。我们可以用0表示“未访问”,1表示“正在访问”(在本次DFS的递归栈中),2表示“已访问”(已从递归栈中弹出,其所有后代都已处理完毕)。 - 我们从
root开始,进行DFS。DFS函数dfs(node)的逻辑如下:- 首先检查当前节点
node的状态:- 如果
visited[node] == 1,说明我们在同一条DFS路径上又遇到了这个节点,这表示存在环,立即返回false。 - 如果
visited[node] == 2,说明这个节点及其后代已经被完整探索过,为了避免重复工作,直接返回true。
- 如果
- 将当前节点状态标记为
1(正在访问)。 - 递归地探索左子节点和右子节点:
- 如果
leftChild[node] != -1,则递归调用dfs(leftChild[node])。如果递归返回false,说明在左子树中发现了问题(环或不连通),本层也直接返回false。 - 对右子节点
rightChild[node]做同样的处理。
- 如果
- 探索完所有子节点后,将当前节点状态标记为
2(已访问)。 - 如果整个过程没有提前返回
false,说明从当前节点node开始的子树是有效的,返回true。
- 首先检查当前节点
步骤 3:检查连通性
- 在调用完从根节点开始的DFS后,我们需要检查是否所有节点都被访问过。
- 遍历
visited数组,检查是否所有节点的状态都是2(已访问)。如果不是,说明有节点无法从根节点到达,结构不连通,返回false。 - 如果所有节点都是
2,则说明这是一棵有效的二叉树,返回true。
图解与边界条件
-
示例1(有效):
- n = 4
- leftChild = [1, -1, 3, -1]
- rightChild = [2, -1, -1, -1]
- 结构:节点0是根,左子为1,右子为2;节点1的左子为3。
- 步骤1:
hasParent数组为 [false, true, true, true],只有节点0无父节点,是根。 - 步骤2:从节点0开始DFS,访问顺序0->1->3->2。所有节点状态变为2,无环。
- 步骤3:所有节点已访问。返回
true。
-
示例2(有环,无效):
- n = 4
- leftChild = [1, -1, 3, -1]
- rightChild = [2, 3, -1, -1]
- 结构:节点0(根)的右子2的左子是3,但节点3又同时是节点0的左子1的右子?实际上,这会导致节点3有两个父节点(1和2),这首先违反了“每个节点只有一个父节点”的规则。在我们的算法中,
hasParent[3]会被设为true两次,但这不是关键。关键是在DFS中,当从节点0探索左子树(节点1)和右子树(节点2)时,节点3会被访问两次。如果第一次访问(从节点1)将其标记为2,第二次访问(从节点2)发现它是2,会直接返回true,不会报错。但我们需要在步骤1中就通过hasParent检查来防止这种情况。更复杂的环,比如节点0的左子指向节点1,节点1的右子指向节点0,我们的DFS在探索节点0时,会进入节点1,又从节点1回到节点0,此时节点0的状态是1(正在访问),从而检测到环并返回false。
-
边界条件:
- n = 1, leftChild = [-1], rightChild = [-1]。单个节点,是有效的二叉树。
hasParent[0]=false,是根。DFS访问它,然后检查所有节点已访问。返回true。 - n = 0, 但题目中 n >= 1,所以不用特殊处理。
- n = 1, leftChild = [-1], rightChild = [-1]。单个节点,是有效的二叉树。
总结
这道题将二叉树结构的验证转化为了图的遍历问题。通过统计入度(父节点数)找到唯一的根节点,然后通过带有状态标记的深度优先搜索来检查从该根节点出发,图中是否存在环以及是否所有节点都能被访问到。这是一种非常经典的、结合了拓扑排序思想和图遍历思想的解法。