搞定leetcode面试经典150题之图

系列博客目录



理论基础

图(Graph)是计算机科学中一种非常重要的数据结构,用于表示对象之间的关系。图由**节点(Vertex)边(Edge)**组成,广泛应用于社交网络、网页链接、路径查找、推荐系统等领域。

1. 图的基本定义

  • 图(Graph):由一组节点和一组边组成。每条边连接两个节点,表示节点之间的关系。

    图可以分为:

    • 无向图(Undirected Graph):边没有方向,边的两端节点是对称的(即边从节点 A 到节点 B 和从节点 B 到节点 A 是等价的)。
    • 有向图(Directed Graph 或 Digraph):边有方向,边是从一个节点指向另一个节点的(即边从节点 A 到节点 B 与从节点 B 到节点 A 是不同的)。

2. 图的基本术语

  • 节点(Vertex 或 Node):图中的基本元素,表示对象或实体。
  • 边(Edge 或 Arc):连接两个节点的关系,表示节点之间的连接或关联。
  • 邻接节点(Adjacent Vertex):如果存在一条边连接节点 A 和节点 B,则节点 A 和节点 B 是邻接的。
  • 度(Degree):一个节点的度是与该节点相连的边的数量。对于无向图,度数是边的数量;对于有向图,分为入度(指向该节点的边数)和出度(从该节点出发的边数)。
  • 路径(Path):从一个节点到另一个节点的边的序列。路径可以是简单的,也可以包含重复的节点和边。
  • 环(Cycle):一个从节点出发,并最终回到自身的路径。
  • 连通性
    • 连通图(Connected Graph):对于无向图,图中的任意两个节点都有一条路径相连。
    • 强连通图(Strongly Connected Graph):对于有向图,任意两个节点之间都有路径可以相互到达。

3. 图的表示方式

图的表示通常有以下几种方式:

1. 邻接矩阵(Adjacency Matrix)
  • 使用一个二维数组(矩阵)来表示图,其中行和列对应于节点,矩阵的元素表示边的存在性。
  • 时间复杂度
    • 查找边:O(1)
    • 添加边:O(1)
    • 遍历邻接节点:O(V),其中 V 是节点的数量。

例如,假设有一个图 G,其包含 4 个节点(0, 1, 2, 3):

    0 -- 1
    |    |
    3 -- 2

该图的邻接矩阵表示如下:

    0 1 2 3
0 [ 0 1 0 1 ]
1 [ 1 0 1 0 ]
2 [ 0 1 0 1 ]
3 [ 1 0 1 0 ]
2. 邻接列表(Adjacency List)
  • 使用一个数组或列表,其中每个元素是一个列表,存储与该节点直接相连的所有节点。
  • 时间复杂度
    • 查找边:O(V),最坏情况下需要遍历所有邻接节点。
    • 添加边:O(1)
    • 遍历邻接节点:O(E),其中 E 是边的数量。

例如,以上图的邻接列表表示如下:

0 -> [1, 3]
1 -> [0, 2]
2 -> [1, 3]
3 -> [0, 2]

4. 图的类型

  • 加权图(Weighted Graph):每条边有一个权重(或值),表示边的成本或距离等。

    • 有向加权图和无向加权图的边也可以具有方向和权重。
  • 有向无环图(DAG, Directed Acyclic Graph):一种特殊的有向图,没有环存在,广泛应用于拓扑排序和任务调度等场景。

  • 树(Tree):一种特殊的图,具有以下特征:

    • 只有一个连通分量(即连通图),并且没有环。
    • 每个节点都有一个父节点,除了根节点。

5. 常见的图的算法

  • 广度优先搜索(BFS, Breadth-First Search)

    • 从某个节点开始,访问所有相邻的节点,逐层访问节点,直到所有可达的节点都被访问。
    • 用途:查找最短路径、图的遍历、网络流等。
  • 深度优先搜索(DFS, Depth-First Search)

    • 从某个节点开始,沿着边尽可能深入访问,直到没有未访问的邻接节点,再回溯到上一个节点。
    • 用途:拓扑排序、图的遍历、环检测、连通分量等。
  • Dijkstra 算法:一种经典的单源最短路径算法,用于计算从起点到其他所有节点的最短路径(适用于加权图,边的权重为非负数)。

  • Bellman-Ford 算法:另一种单源最短路径算法,能处理图中有负权边的情况。

  • Floyd-Warshall 算法:一种用于计算所有节点对之间最短路径的算法(适用于加权图)。

  • 拓扑排序(Topological Sort):用于有向无环图(DAG)的排序,节点按依赖关系的顺序排列。

  • 最小生成树(MST, Minimum Spanning Tree)

    • Kruskal 算法:通过边的排序和并查集方法构建最小生成树。
    • Prim 算法:通过不断选择与当前树最小权重边的方式构建最小生成树。

6. 图的应用

  • 社交网络分析:社交网络中,用户之间的关系可以通过图表示,常见问题包括找出最短路径、群体检测等。
  • 网络流问题:如最大流最小割问题、最短路径问题。
  • 网页链接分析:搜索引擎通过有向图分析网页之间的链接关系来排名。
  • 任务调度与依赖:任务的执行顺序可以通过有向无环图(DAG)来表示,任务的依赖关系可通过图的拓扑排序来处理。
  • 推荐系统:利用图来表示用户与商品之间的关系,进行推荐。

图是非常重要且应用广泛的数据结构,理解图的基本理论和算法对于解决复杂的关系问题非常有帮助。

例题

200.岛屿数量

链接
思路:没检测到一块岛屿,我们要把构成这块岛屿的所有小块都标记成已探索过,防止重复计算。

class Solution {
    public int numIslands(char[][] grid) {
        int res = 0;
        for (int i = 0; i < grid.length; i++) {
            for (int j = 0; j < grid[0].length; j++) {
                if(grid[i][j] == '1'){
                    infect(grid, i, j);
                    res ++;
                }
            }
        }
        return res;
    }
    private void infect(char[][] grid,int y, int x){
        int m = grid.length;
        int n = grid[0].length;
        if(x<0|| x>=n) return;
        if(y<0 || y>=m) return;
        if(grid[y][x] == '1'){
            grid[y][x] = '.';
        }else {
            return;//注意再检测到不为陆地小块后要暂定
        }
        infect(grid, y+1, x);
        infect(grid, y-1, x);
        infect(grid, y, x+1);
        infect(grid, y, x-1);

    }
}

130.被围绕的区域

链接
在这里插入图片描述
使用深度优先遍历如下:

class Solution {
    int n, m;

    public void solve(char[][] board) {
        n = board.length;
        if (n == 0) {
            return;
        }
        m = board[0].length;
        for (int i = 0; i < n; i++) {
            dfs(board, i, 0);
            dfs(board, i, m - 1);
        }
        for (int i = 1; i < m - 1; i++) {
            dfs(board, 0, i);
            dfs(board, n - 1, i);
        }
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (board[i][j] == 'A') {
                    board[i][j] = 'O';
                } else if (board[i][j] == 'O') {
                    board[i][j] = 'X';
                }
            }
        }
    }

    public void dfs(char[][] board, int x, int y) {
        if (x < 0 || x >= n || y < 0 || y >= m || board[x][y] != 'O') {
            return;
        }
        board[x][y] = 'A';
        dfs(board, x + 1, y);
        dfs(board, x - 1, y);
        dfs(board, x, y + 1);
        dfs(board, x, y - 1);
    }
}


133.克隆图

链接
注意:这个图是连通的,我们cloneNode.neighbors.add(cloneGraph(neighbor));这条语句可以帮我们递归的一层一层的往下把所有结点以及其neighbors都弄好。最深层的节点,应该是开始的节点,因为连通。自己递归到自己为什么没错?因为我们存入到List中的是个地址,这个地址指向一个实体,地址虽然不变,但地址所指向的内容也就是实体是会变化的,比如这个道题,比如B作为A的邻居,我们在把B添加到A的neighbors中时,可能B自己的neighbors并没有完整。

class Solution {
    private HashMap <Node, Node> visited = new HashMap <> ();
    public Node cloneGraph(Node node) {
        if (node == null) {
            return node;
        }

        // 如果该节点已经被访问过了,则直接从哈希表中取出对应的克隆节点返回
        if (visited.containsKey(node)) {
            return visited.get(node);
        }

        // 克隆节点,注意到为了深拷贝我们不会克隆它的邻居的列表
        Node cloneNode = new Node(node.val, new ArrayList());
        // 哈希表存储
        visited.put(node, cloneNode);

        // 遍历该节点的邻居并更新克隆节点的邻居列表
        for (Node neighbor: node.neighbors) {
            cloneNode.neighbors.add(cloneGraph(neighbor));
        }
        return cloneNode;
    }
}

作者:力扣官方题解
链接:https://leetcode.cn/problems/clone-graph/solutions/370663/ke-long-tu-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

207.课程表

链接
思路:我们先学习可以学习的课(即不需要前置课程的课),学完后,解锁新的可以学习(即前置课程学完)的课程,再继续学习。
蜜糖:我们先建立一个数组,存储每门课程所需前置课程的数量。

class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        int[] courses = new int[numCourses];
        Arrays.fill(courses, 0);
        HashMap<Integer,List<Integer>> needUnlock = new HashMap<>();
        for(int[] pre : prerequisites){
            int target = pre[0];
            int need = pre[1];
            final List<Integer> orDefault = needUnlock.getOrDefault(need, new ArrayList<>());
            orDefault.add(target);
            needUnlock.put(need, orDefault);
            courses[target] ++;
        }
        Deque<Integer> queue = new ArrayDeque<>();
        for (int i = 0; i < courses.length; i++) {
            if(courses[i] == 0){
                queue.offerLast(i);
            }
        }
        while(!queue.isEmpty()){
            int course = queue.pollFirst();
            if(needUnlock.containsKey(course)){
                for(int num : needUnlock.get(course)){
                    if(courses[num] > 0) courses[num] --;
                    if(courses[num] == 0) queue.offerLast(num);
                }
            }
        }
        for (int i = 0; i < numCourses; i++) {
            if(courses[i] > 0) return false;
        }
        return true;
    }
}

210.课程表 II

链接

class Solution {
    public int[] findOrder(int numCourses, int[][] prerequisites) {
        List<Integer> res = new ArrayList<>();
        int[] courses = new int[numCourses];
        HashMap<Integer,List<Integer>> needUnlock = new HashMap<>();
        for(int[] pre : prerequisites){
            int target = pre[0];
            int need = pre[1];
            final List<Integer> orDefault = needUnlock.getOrDefault(need, new ArrayList<>());
            orDefault.add(target);
            needUnlock.put(need, orDefault);
            courses[target] ++;
        }
        Deque<Integer> queue = new ArrayDeque<>();
        for (int i = 0; i < numCourses; i++) {
            if(courses[i] == 0){
                queue.offerLast(i);
                res.add(i);
            }
        }
        while(!queue.isEmpty()){
            int course = queue.pollFirst();
            if(needUnlock.containsKey(course)){
                for(int num : needUnlock.get(course)){
                    if(courses[num] > 0) {
                        courses[num] --;
                    }
                    if(courses[num] == 0){
                        queue.offerLast(num);
                        res.add(num);
                    }
                }
            }
        }
        for (int i = 0; i < numCourses; i++) {
            
            if(courses[i] > 0) return new int[0];
        }
        int[] array = new int[res.size()];

        // 使用 for 循环将 List 中的元素赋值到数组中
        for (int i = 0; i < res.size(); i++) {
            array[i] = res.get(i);  // 将 List 中的 Integer 转换为 int 并赋值给数组
        }
        return  array;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值