搜索题目:验证二叉树

文章目录

题目

标题和出处

标题:验证二叉树

出处:1361. 验证二叉树

难度

6 级

题目描述

要求

有 n \texttt{n} n 个二叉树结点,从 0 \texttt{0} 0 到 n − 1 \texttt{n} - \texttt{1} n−1 编号,其中结点 i \texttt{i} i 的两个子结点分别是 leftChild[i] \texttt{leftChild[i]} leftChild[i] 和 rightChild[i] \texttt{rightChild[i]} rightChild[i]。当且仅当所有 结点形成恰好一个 有效的二叉树时,返回 true \texttt{true} true。

如果结点 i \texttt{i} i 没有左子结点,那么 leftChild[i] \texttt{leftChild[i]} leftChild[i] 等于 -1 \texttt{-1} -1。右子结点也符合该规则。

注意:结点没有值,这道题中仅使用结点编号。

示例

示例 1:

输入: n = 4, leftChild = [1,-1,3,-1], rightChild = [2,-1,-1,-1] \texttt{n = 4, leftChild = [1,-1,3,-1], rightChild = [2,-1,-1,-1]} n = 4, leftChild = [1,-1,3,-1], rightChild = [2,-1,-1,-1]

输出: true \texttt{true} true

示例 2:

输入: n = 4, leftChild = [1,-1,3,-1], rightChild = [2,3,-1,-1] \texttt{n = 4, leftChild = [1,-1,3,-1], rightChild = [2,3,-1,-1]} n = 4, leftChild = [1,-1,3,-1], rightChild = [2,3,-1,-1]

输出: false \texttt{false} false

示例 3:

输入: n = 2, leftChild = [1,0], rightChild = [-1,-1] \texttt{n = 2, leftChild = [1,0], rightChild = [-1,-1]} n = 2, leftChild = [1,0], rightChild = [-1,-1]

输出: false \texttt{false} false

数据范围

  • n = leftChild.length = rightChild.length \texttt{n} = \texttt{leftChild.length} = \texttt{rightChild.length} n=leftChild.length=rightChild.length
  • 1 ≤ n ≤ 10 4 \texttt{1} \le \texttt{n} \le \texttt{10}^\texttt{4} 1≤n≤104
  • -1 ≤ leftChild[i], rightChild[i] ≤ n − 1 \texttt{-1} \le \texttt{leftChild[i], rightChild[i]} \le \texttt{n} - \texttt{1} -1≤leftChild[i], rightChild[i]≤n−1

前言

给定的 n n n 个结点以及左右子结点的关系可以看成有向图。如果有向图的所有结点形成恰好一个有效的二叉树,则应满足以下条件。

  1. 边数等于 n − 1 n - 1 n−1。

  2. 只有根结点的入度是 0 0 0,其余 n − 1 n - 1 n−1 个结点的入度都是 1 1 1。

  3. 全部结点和边组成连通无环图。

条件 1 和条件 2 可以通过遍历图中的全部边判断。具体而言,对于 0 ≤ i < n 0 \le i < n 0≤i<n,如果 leftChild [ i ] ≥ 0 \textit{leftChild}[i] \ge 0 leftChild[i]≥0 则存在一条从结点 i i i 指向结点 leftChild [ i ] \textit{leftChild}[i] leftChild[i] 的有向边,如果 rightChild [ i ] ≥ 0 \textit{rightChild}[i] \ge 0 rightChild[i]≥0 则存在一条从结点 i i i 指向结点 rightChild [ i ] \textit{rightChild}[i] rightChild[i] 的有向边。

遍历全部边之后,如果不满足条件 1 或条件 2,则有向图的所有结点一定不能形成恰好一个有效的二叉树,返回 false \text{false} false。当以下情况之一出现时,不满足条件 1 或条件 2。

  • 边数不等于 n − 1 n - 1 n−1。

  • 不存在入度是 0 0 0 的结点,或者有超过 1 1 1 个入度是 0 0 0 的结点。

  • 存在入度大于 1 1 1 的结点。

当条件 1 和条件 2 都满足时,有向图可能由一个二叉树和一个或多个环组成,此时有向图的所有结点不能形成恰好一个有效的二叉树,因此需要判断条件 3 是否满足。由于边数等于结点数少 1 1 1,因此连通图一定无环,只要判断有向图是否连通即可。

可以使用广度优先搜索、深度优先搜索或并查集判断有向图是否连通。

解法一

思路和算法

可以使用广度优先搜索判断有向图是否连通。入度是 0 0 0 的结点是根结点,从根结点开始遍历,对于每个结点,如果存在子结点则继续访问子结点。遍历结束时,根据访问的结点数是否等于 n n n 判断有向图是否连通。

根结点所在的连通分量中一定不存在环,否则会存在入度大于 1 1 1 的结点,因此广度优先搜索的过程中不需要记录每个结点是否被访问过。

代码

java 复制代码
class Solution {
    public boolean validateBinaryTreeNodes(int n, int[] leftChild, int[] rightChild) {
        int edges = 0;
        int[] indegrees = new int[n];
        for (int i = 0; i < n; i++) {
            int left = leftChild[i], right = rightChild[i];
            if (left >= 0) {
                edges++;
                indegrees[left]++;
            }
            if (right >= 0) {
                edges++;
                indegrees[right]++;
            }
        }
        if (edges != n - 1) {
            return false;
        }
        int root = -1;
        for (int i = 0; i < n; i++) {
            if (indegrees[i] == 0) {
                if (root < 0) {
                    root = i;
                } else {
                    return false;
                }
            } else if (indegrees[i] > 1) {
                return false;
            }
        }
        if (root < 0) {
            return false;
        }
        int visitCount = 0;
        Queue<Integer> queue = new ArrayDeque<Integer>();
        queue.offer(root);
        while (!queue.isEmpty()) {
            int node = queue.poll();
            visitCount++;
            int left = leftChild[node], right = rightChild[node];
            if (left >= 0) {
                queue.offer(left);
            }
            if (right >= 0) {
                queue.offer(right);
            }
        }
        return visitCount == n;
    }
}

复杂度分析

  • 时间复杂度: O ( n ) O(n) O(n),其中 n n n 是结点数。计算边数和每个结点的入度需要 O ( n ) O(n) O(n) 的时间,判断每个结点的入度是否符合要求需要 O ( n ) O(n) O(n) 的时间,广度优先搜索需要 O ( n ) O(n) O(n) 的时间遍历每个结点最多一次。

  • 空间复杂度: O ( n ) O(n) O(n),其中 n n n 是结点数。入度数组和队列需要 O ( n ) O(n) O(n) 的空间。

解法二

思路和算法

也可以使用深度优先搜索判断有向图是否连通。入度是 0 0 0 的结点是根结点,从根结点开始遍历,对于每个结点,如果存在子结点则继续访问子结点。遍历结束时,根据访问的结点数是否等于 n n n 判断有向图是否连通。

根结点所在的连通分量中一定不存在环,否则会存在入度大于 1 1 1 的结点,因此深度优先搜索的过程中不需要记录每个结点是否被访问过。

代码

java 复制代码
class Solution {
    int n;
    int[] leftChild;
    int[] rightChild;
    int visitCount = 0;

    public boolean validateBinaryTreeNodes(int n, int[] leftChild, int[] rightChild) {
        this.n = n;
        this.leftChild = leftChild;
        this.rightChild = rightChild;
        int edges = 0;
        int[] indegrees = new int[n];
        for (int i = 0; i < n; i++) {
            int left = leftChild[i], right = rightChild[i];
            if (left >= 0) {
                edges++;
                indegrees[left]++;
            }
            if (right >= 0) {
                edges++;
                indegrees[right]++;
            }
        }
        if (edges != n - 1) {
            return false;
        }
        int root = -1;
        for (int i = 0; i < n; i++) {
            if (indegrees[i] == 0) {
                if (root < 0) {
                    root = i;
                } else {
                    return false;
                }
            } else if (indegrees[i] > 1) {
                return false;
            }
        }
        if (root < 0) {
            return false;
        }
        dfs(root);
        return visitCount == n;
    }

    public void dfs(int node) {
        visitCount++;
        int left = leftChild[node], right = rightChild[node];
        if (left >= 0) {
            dfs(left);
        }
        if (right >= 0) {
            dfs(right);
        }
    }
}

复杂度分析

  • 时间复杂度: O ( n ) O(n) O(n),其中 n n n 是结点数。计算边数和每个结点的入度需要 O ( n ) O(n) O(n) 的时间,判断每个结点的入度是否符合要求需要 O ( n ) O(n) O(n) 的时间,深度优先搜索需要 O ( n ) O(n) O(n) 的时间遍历每个结点最多一次。

  • 空间复杂度: O ( n ) O(n) O(n),其中 n n n 是结点数。入度数组和递归调用栈需要 O ( n ) O(n) O(n) 的空间。

解法三

预备知识

该解法涉及到并查集。

并查集是一种树型的数据结构,用于处理不相交集合的合并与查询问题。

思路和算法

当每个结点的入度都不超过 1 1 1 时,根结点所在的连通分量中一定不存在环,且该连通分量一定是有效的二叉树,简单说明如下:对于除了根结点以外的每个结点 v v v,一定存在恰好一条指向结点 v v v 的有向边,将这条有向边的起点记为结点 u u u,则结点 u u u 是结点 v v v 的父结点,每一对相邻结点之间的父结点和子结点关系形成二叉树。

因此,使用并查集时不需要考虑每条边的方向,只要将每条边连接的两个结点合并即可。

并查集初始化时, n n n 个结点分别属于 n n n 个不同的集合,每个集合只包含一个结点,集合个数等于结点个数。

初始化之后,遍历每条边,将同一条边连接的两个结点所在的集合做合并,每次合并之后将集合个数减 1 1 1。

遍历结束之后,并查集的集合个数即为连通分量数,根据并查集的集合个数是否等于 1 1 1 判断有向图是否连通。

代码

java 复制代码
class Solution {
    int n;
    int[] leftChild;
    int[] rightChild;
    int visitCount = 0;

    public boolean validateBinaryTreeNodes(int n, int[] leftChild, int[] rightChild) {
        this.n = n;
        this.leftChild = leftChild;
        this.rightChild = rightChild;
        int edges = 0;
        int[] indegrees = new int[n];
        for (int i = 0; i < n; i++) {
            int left = leftChild[i], right = rightChild[i];
            if (left >= 0) {
                edges++;
                indegrees[left]++;
            }
            if (right >= 0) {
                edges++;
                indegrees[right]++;
            }
        }
        if (edges != n - 1) {
            return false;
        }
        int root = -1;
        for (int i = 0; i < n; i++) {
            if (indegrees[i] == 0) {
                if (root < 0) {
                    root = i;
                } else {
                    return false;
                }
            } else if (indegrees[i] > 1) {
                return false;
            }
        }
        if (root < 0) {
            return false;
        }
        UnionFind uf = new UnionFind(n);
        for (int i = 0; i < n; i++) {
            int left = leftChild[i], right = rightChild[i];
            if (left >= 0) {
                uf.union(i, left);
            }
            if (right >= 0) {
                uf.union(i, right);
            }
        }
        return uf.getCount() == 1;
    }
}

class UnionFind {
    private int[] parent;
    private int[] rank;
    private int count;

    public UnionFind(int n) {
        parent = new int[n];
        for (int i = 0; i < n; i++) {
            parent[i] = i;
        }
        rank = new int[n];
        count = n;
    }

    public void union(int x, int y) {
        int rootx = find(x);
        int rooty = find(y);
        if (rootx != rooty) {
            if (rank[rootx] > rank[rooty]) {
                parent[rooty] = rootx;
            } else if (rank[rootx] < rank[rooty]) {
                parent[rootx] = rooty;
            } else {
                parent[rooty] = rootx;
                rank[rootx]++;
            }
            count--;
        }
    }

    public int find(int x) {
        if (parent[x] != x) {
            parent[x] = find(parent[x]);
        }
        return parent[x];
    }

    public int getCount() {
        return count;
    }
}

复杂度分析

  • 时间复杂度: O ( n × α ( n ) ) O(n \times \alpha(n)) O(n×α(n)),其中 n n n 是结点数, α \alpha α 是反阿克曼函数。并查集的初始化需要 O ( n ) O(n) O(n) 的时间,然后遍历 n − 1 n - 1 n−1 条边执行 n − 1 n - 1 n−1 次合并操作,这里的并查集使用了路径压缩和按秩合并,单次操作的时间复杂度是 O ( α ( n ) ) O(\alpha(n)) O(α(n)),因此并查集初始化之后的操作的时间复杂度是 O ( n × α ( n ) ) O(n \times \alpha(n)) O(n×α(n)),总时间复杂度是 O ( n × α ( n ) ) O(n \times \alpha(n)) O(n×α(n))。

  • 空间复杂度: O ( n ) O(n) O(n),其中 n n n 是结点数。并查集需要 O ( n ) O(n) O(n) 的空间。

相关推荐
hnjzsyjyj5 天前
洛谷 P1333:瑞瑞的木棍 ← 欧拉回路 + 并查集
并查集·欧拉回路
伟大的车尔尼8 天前
搜索题目:单词接龙
广度优先搜索
伟大的车尔尼9 天前
搜索题目:最小基因变化
广度优先搜索
伟大的车尔尼11 天前
搜索题目:可能的二分法
并查集·深度优先搜索·广度优先搜索
伟大的车尔尼15 天前
搜索题目:边界着色
深度优先搜索·广度优先搜索
不知名的忻20 天前
并查集(QuickUnion)
java·数据结构·算法·并查集
伟大的车尔尼21 天前
搜索题目:二进制矩阵中的最短路径
广度优先搜索
伟大的车尔尼23 天前
搜索题目:被围绕的区域
并查集·深度优先搜索·广度优先搜索
Tisfy24 天前
LeetCode 1722.执行交换操作后的最小汉明距离:连通图
算法·leetcode·dfs·题解·深度优先搜索·连通图