java数据结构与算法刷题-----LeetCode207. 课程表

java数据结构与算法刷题目录(剑指Offer、LeetCode、ACM)-----主目录-----持续更新(进不去说明我没写完):https://blog.csdn.net/grd_java/article/details/123063846

在这里插入图片描述

这是图的拓扑排序相关的经典问题。如果没有学过图的拓扑排序确实不好做。我们只需要将其抽象为有向图,然后判断是否可以完成拓扑排序即可。

深度优先遍历1(逆拓扑排序)

解题思路:时间复杂度O( m + n m+n m+n)n是课程数,m是n的前驱个数,空间复杂度O( n + m n+m n+m)我们需要存储其抽象为图数据结构的邻接表

深度优先遍历实现拓扑排序比较绕,但是为什么将深度优先放在前面,因为深度优先遍历是面试最喜欢考的(逆向思维)。因为广度优先遍历的逻辑和我们人脑一样,而深度优先却将过程反了过来,很适合考察面试者的逻辑处理能力。

  1. 拓扑排序,就是图满足绝对先后关系。
  2. 也就是每次输出的结点,必须是入度为0的结点。
  3. 例如下图:
    在这里插入图片描述
  1. B和D的入度为0,所以先输出他俩,D和B。然后剩余A的入度为0
  2. 输出A后,C和F的入度为0
  3. 输出C和F后,E和G入度为0
  4. 最后E和G输出后,所有结点输出完成
  1. 如果发现还有结点没有输出,但是没有入度为0的结点时,此时表明这个图,不能完成拓扑排序
  2. 拓扑排序结果不唯一,例如BACDFEG也是结果之一。

但是以上过程是我们人脑的思路,深度优先遍历很难轻松完成以上过程的复现。所以我们得反过来思考。
在这里插入图片描述

  1. 构建邻接表,统计每个结点的出度边(当前顶点指向哪些顶点)
  2. 然后依次深度优先遍历
  1. 访问A,发现它指向C和E和F,访问F,发现F指向G,访问G,发现G不指向任何顶点,输出G(如果想要正着输出,需要放入栈中)
  2. 回到F,发现F还指向E,访问E,发现E没有指向任何顶点,输出E
  3. 回到F,F指向都已经输出,输出F
  4. 回到A,发现A还指向C,C没有指向,输出C
  5. 回到A,A的指向都被输出,输出A
  6. A已经深度遍历完成,接下来对下一个没有输出的B进行,B指向A和C都已经输出,则输出B
  7. 下一个没有输出的是D,对D进行深度优先遍历,发现D指向F,但是F已经输出,则输出D
  1. 故我们获得了GEFCABD的逆拓扑排序结构,如果想要非逆拓扑排序,可以使用法二的思路(及其的难,只有高精度导航系统等需要用,实战中的代码也会及其复杂,法二中我只给出关键逻辑),或者用栈,依次入栈后出栈,就完成了逆拓扑排序的正序输出

深度优先遍历2(我当时华为三面的算法题)

是大厂(华为三面,技术面)中的一道面试题,要求使用深度优先遍历,优先处理入度为0的顶点。这个算法的使用场景是人工智能导航系统的算法,如果先处理入度不为0的顶点,后面再处理入度为0的顶点时,会数据不一致。但是又不能使用广度优先(需要用大量的锁,导致无法及时处理信息)

解题思路:时间复杂度O( m + n m+n m+n)n是课程数,m是n的前驱个数,空间复杂度O( n + m n+m n+m)我们需要存储其抽象为图数据结构的邻接表
  1. 法一中,我们最后需要将栈中元素倒着输出才是拓扑排序结果
  2. 有没有办法还是先处理入度为0的顶点呢?
  3. 不可以使用队列,因为队列需要编程语言自己处理,而我们需要原子操作,也就是系统底层的栈帧来操作,可以省下很多锁操作。而且底层的栈非常快,编程语言的队列没有系统底层的栈帧快。
  4. 因此,用邻接表就无法完成此操作了。我们不再统计每个结点的出度(每个结点伸出去的边),而是再次逆转思想(逆中逆),选择统计每个结点的入度(有哪些结点直接指向当前顶点)

在这里插入图片描述

  1. 我们创建一个容器(数组,栈,链表都可以),这个容器每个元素都保存一个链表
  2. 这个链表表示单个顶点的所有前驱(有几条边指向它)。例如corses[1] = node{val = 3,next = node{val = 2,next = node{val = 0,next = null}}};表示对于课程1,有3,2,0号课程指向它,学习1,必须先完成3,2,0
  1. 结点A,有B指向它
  2. 结点B,无结点指向
  3. 结点C,有A和B指向它
  4. 结点D,无结点指向
  5. 结点E,有A和F指向它
  6. 结点F,有A和D指向它
  7. 结点G,有F指向它
  1. 构建完成后,我们依次深度优先遍历结点。每访问一个结点,将结点标识为true
  1. 先访问结点A,我们发现B指向它,访问B,然后发现B无结点指向(入度为0)
  2. 输出B,标识B已经输出,然后回到A,输出A,标识A已经输出
  3. 然后访问C,发现A和B指向它,但是A和B已经输出,所以输出C,标识C已经输出
  4. 访问D,无结点指向,直接输出
  5. 访问E,A和F指向,但是A已经输出,直接访问F。发现F有A和D指向,但是A和D已经标识为输出。直接输出F。然后回到E,输出E
  6. 访问F,发现已经输出,直接跳过
  7. 访问G,发现F指向,但是F已经输出,直接输出G
  1. 我们发现全程完整输出,这是一个可以拓扑排序的图。但是如果在深度优先遍历过程中,发现没有输出,但是已经访问过的结点,这就说明图中有环,深度优先遍历兜了一圈又回来了。出现这种情况直接返回false即可
代码

在这里插入图片描述

class Solution {
    static class Node {//课程结点,用来抽象为图
        Node next;//当前结点指向的下一个结点
        int val;//值
        public Node(){
            this.val = -1;
        }
        public Node(int val){
            this.val = val;
        }
        public Node(int val,Node next){
            this.val = val;
            this.next = next;
        }
    }
    //入口方法
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        Node[] corses = new Node[numCourses];//创建结点数组,充当栈,保存每个结点的前置结点(有几条边指向它)
        for (int[] pre : prerequisites) {//依次遍历图中顶点
            //想要完成课程pre[0],需要先完成课程pre[1]
            //因此corses中的前后继关系为pre[1]->pre[0],表示先完成pre[1]才有pre[0]
            //因此对于pre[0],我们保存其所有前驱结点(入度结点,有向图指向它的结点),这些前驱必须先遍历才能保证拓扑排序
            //例如[1,0]表示完成1需要0.则有corses[1] = node{val = 0,next = null}.表示对于1,有一个前驱0指向它
            //此时发现[1,2],表示完成1需要2,则有corses[1] = node{val = 2,next = node{val = 0,next = null}};//表示对于1,有2和0指向它
            //此时发现[1,3],表示完成1需要3,则有corses[1] = node{val = 3,next = node{val = 2,next = node{val = 0,next = null}}};//表示对于1,有3,2,0指向它
            corses[pre[0]] = new Node(pre[1],corses[pre[0]]);
        }
        boolean[] finished = new boolean[numCourses];//标识当前结点是否已经拓扑排序过了
        boolean[] visited = new boolean[numCourses];//标识结点是否已经访问
        for (int i = 0; i < numCourses; i++) {//遍历每个结点
            if (!dfs(corses, finished, visited, i)) {//对其深度优先遍历
                return false;//失败就返回false
            }
        }
        return true;//成功返回true
    }
    private boolean dfs(Node[] corses, boolean[] finished, boolean[] visited, int i) {
        if (finished[i]) return true;//已经完成过拓扑排序,直接返回true
        if (visited[i]) return false;//没有完成过拓扑排序,但是又重新访问了这个结点,说明有环,无法进行拓扑排序

        visited[i] = true;//访问该结点
        Node node = corses[i];
        while (node != null) {//深度优先
            if (!dfs(corses, finished, visited, node.val)) return false;//如果拓扑失败就返回false
            node = node.next;//深度优先
        }
        finished[i] = true;//当前结点被拓扑排序完成,标志为true
        return true;//返回true
    }
}

广度优先

解题思路:时间复杂度O( m + n m+n m+n)n是课程数,m是n的前驱个数,空间复杂度O( n + m n+m n+m)我们需要存储其抽象为图数据结构的邻接表
  1. 将其抽象为邻接表(一个数组,下标表示每个顶点,数组元素保存一个链表,链表表示每个顶点的边指向哪里)
  2. 我们初始化邻接表的过程中,要统计每个顶点的入度
  3. 然后开始依次遍历入度为0的结点u
  4. 如果u遍历完成后,它的邻居的入度变为0,则将邻居加入队列下次遍历
  5. 期间统计访问的顶点个数,如果最后正好将图遍历完,就说明是拓扑排序返回true,否则返回false
代码

在这里插入图片描述

class Solution {
    List[] edges;//邻接表
    int[] indeg;//顶点入度

    public boolean canFinish(int numCourses, int[][] prerequisites) {
        edges = new List[numCourses];
        for (int i = 0; i < numCourses; ++i) {
            edges[i] = new ArrayList<Integer>();//初始化邻接表
        }
        indeg = new int[numCourses];//保存入度
        for (int[] info : prerequisites) {
            edges[info[1]].add(info[0]);//想要完成info[0],必须先完成info[1],则info[1]->info[0]
            ++indeg[info[0]];//info[0]的入度+1
        }
        //用队列进行广度优先遍历
        int[] queueA = new int[numCourses];
        int queueHead = 0;//头指针
        int queueRear = 0;//尾指针

        for (int i = 0; i < numCourses; ++i) {//依次查看入度为0的结点
            if (indeg[i] == 0) {
                queueA[queueRear++] = i;//如果是入度为0,放入队列进行广度优先遍历
            }
        }
        int visited = 0;//如果可以完成拓扑排序操作,过程中访问的结点个数,正好是图中顶点的个数
        while (queueHead<queueRear) {//只要队列有东西就继续
            ++visited;//每访问一个,访问顶点个数+1
            int u = queueA[queueHead++];//拓扑遍历顶点
            for (Object x: edges[u]) {//获取它指向的顶点
                int v = (int) x;
                --indeg[v];//被u指向的顶点v,因为u被遍历,所以少一个入度
                if (indeg[v] == 0) {//如果顶点v在输出u后,变为0入度顶点
                    queueA[queueRear++] = v;//下一个可以遍历它
                }
            }
        }

        return visited == numCourses;//访问的结点个数,正好是图中顶点的个数,表示图可以完成拓扑排序
    }
}
  • 50
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

殷丿grd_志鹏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值