Android程序员会遇到的算法(part 7 拓扑排序)

这一期是我打算做的安卓算法面试系列的最后一期了,一来是自从来了美国之后,每天的工作实在太忙了,除了周末之外很少时间能完完整整的总结一些东西。不过第二个原因,也是最重要的原因,就是在这之后我打算好好沉淀积累一下,等有更多的心得体会再分享出来。

这期我打算聊一聊拓扑排序这个算法。在Java里面具体的实现和一些细节。这里我尽量不用太多的专业术语,用比较通俗的讲法来解释一些概念。(其实是我的狗嘴也吐不出啥象牙。。。以前学的算法知识早就还给老师了)

d2fce9868ad44bb98cb89ae4d780c369_th.jpg

其实拓扑排序和广度优先搜索算法在代码上真的很像,说穿了其实就是图的遍历,只不过遍历的顺序和规则有些少许不同。

相信各位学习计算机科学专业的同学应该都对高等数学或者大学物理有深刻的阴影。。。我还记得我当时考完大学物理2已经觉得自己要挂了,没忍住给老师打了一个电话求情,虽然最后老师说我离挂科还远,但是69分的大学物理2也让我与那个学期的奖学金无缘了。

download (1).jpeg

可能有人问为什么计算机专业不直接学Java,C++或者web开发?一定要先上大学物理或者高等数学?说了这么多废话,我想说的重点是,每个学科都有一个自己的课程安排,学习一门专业课之前必须要有一些基础课程的支撑才行。我们不能不学高等数学和线性代数直接跳去学机器学习,我们也不能不学Java或者python直接上手web项目。这也引申出了这一期的内容,拓扑排序, 怎么样在已知某些节点的前序(prerequisites) 节点的情况下,把这些节点的顺序排列出来。就好比,我知道一定课程的前后顺序的情况下,把我这四年大学的课程时间安排排列出来,最后打印成课程表。

Screen Shot 2019-04-06 at 12.12.31 PM.png

比如上面这幅图,我们怎么可以将其课程的依赖关系,按照先后顺利排列起来,这就是拓扑排序可以解决的其中一种,也是最经典的问题。


1.怎么定义数据结构

首先对于图来说,我们要知道每个节点有多少子节点,也就是后继节点,在课程安排例子里面可以理解为,学了A课程之后可以学的课程B。那么A就是B的前驱节点,B就是A的后继节点。
在Java中我们可以使用HashMap来实现,根据题目的不同,有时候也可以使用别的数据结构比如二维数组。不过我个人比较喜欢HashMap。

那么节点的关系可以用一个HashMap来表达,课程使用String 来表示

//节点的后继节点
HashMap<String, HashSet<String>>  courses = new HashMap();

同时,在拓扑排序中,我们还需要记录某个节点的前驱节点的数量,因为只有当某个节点的前驱节点为0的时候,我们才能处理该节点。对应到课程学习中,就是只有当我们学习完毕了某个课程的所有前驱课程,我们才能学习该课程。比如图中的计算机网络课程,需要先学习组成原理和通信原理一样。

//记录每个点的前驱节点数量
HashMap<String,Integer> preCount = new HashMap<String,Integer>

2.拓扑排序

假设我们已经有了这两个数据结构并且数据已经填充好了。我们就可以开始进行拓扑排序了。算法很简单,把前驱节点数量为0的节点先放入队列,每次从队列弹出的时候把自己的后继节点的preCount数量减少1,假如此时后继节点的preCount数量减少到0了,就把节点加入到队列中。在这个例子里面,弹出一个节点的意义就是学习一门课程。

这个很好理解,比如我们学习完组成原理,距离学习计算机网络还差一门课。

Screen Shot 2019-04-06 at 1.10.59 PM.png

当我们把通信原理学习完毕之后,计算机网络的前驱节点数量从1减少为0,我们才可以学习计算机网络。

用代码来表示的话,如下

//课程调度队列
        Queue<String> queue = new LinkedList<>();
        //最后课程的顺序
        List<String> sequence = new ArrayList<>();
        while (!queue.isEmpty()) {
            //获取当前队列中的第一个课程,将其加入到最后的课程顺序列表中
            String currentCourse = queue.poll();
            sequence.add(currentCourse);
            
            //每当一个课程结束学习之后,找到它的后继课程
            for (String course : courses.get(currentCourse)) {
                //加入后继课程的前驱节点数量还是大于0 的,说明该课程还没被学习
                if (preCount.get(course) > 0) {
                    //减少该后继课程的前驱节点数量
                    preCount.put(course, preCount.get(course) - 1);
                    //如果前去梳理减到0,说明我们已经可以开始学习该课程了,
                    //加到队列里面
                    if (preCount.get(course) == 0) {
                        queue.add(course);
                    }
                }
            }

        }
       return sequence;

3.和广度优先的不同

其实看代码大家也可以知道,拓扑排序其实就是广度优先搜索的一种,只不过拓扑排序在插入子节点到队列的时候,有一些限制。就是在这里:

 if (preCount.get(course) == 0) {
                        queue.add(course);
                    }

一般的广度优先只要遍历了当前节点,就要把当前节点的所有自己点都一股脑的插入到队列中。在拓扑排序里面,因为每个节点的前驱节点数量可能会大于1,所以,不能简单的插入子节点(或者说后继节点),而是需要额外的数据结构,preCount这个HashMap来决定是否可以把后继节点插入。

4.有环?

图搜索的一个经典问题是,如果有环怎么办?同样的,在拓扑排序里面,也可能出现存在环的情况。比如


Screen Shot 2019-04-06 at 1.26.07 PM.png

在下图这种情况,学生就没办法学了。。。。


download.jpeg

但是在拓扑排序下面,判断是否有环的方法还不太一样,比如宽度优先搜索的情况下,我们可以用一个叫visited的HashSet来记录已经访问过的节点。但是拓扑排序不行。

比如下图这种情况


Screen Shot 2019-04-06 at 1.32.26 PM.png

当我们学习完A之后,其实我们是不能遍历完全所有节点的,因为B和C的前驱节点数量都为1,程序在跑完第一个循环

  while (!queue.isEmpty()) {
            //获取到A
            String currentCourse = queue.poll();
            sequence.add(currentCourse);

之后,就会直接结束了。
所以其实我们判断环的方法要换成->判断我们是否能学习完所有课程。

HashMap<String, HashSet<String>>  courses = new HashMap();
//假如最后我们能学习完所有课程
if(result.size() == course.keySet().size()){
     return true;
}else{
     return false;
}

5.应用的范围

拓扑排序的题目可以出现很多种,但是都是万变不离其宗,掌握好我们需要的数据结构,熟练的写出广度优先算法的模板代码, 其实就万事大吉了。以后比如还有类似的问题,像安装软件,比如要安装A,要先安装依赖C,等等之类的问题,相信大家都可以迎刃而解了。总结的来讲,一旦我们发现需要进行对依赖之间进行排序的,用拓扑排序都没毛病。

6.题目代码

Leetcode 里面的Course Schedule, 大家可以自己练习一下。
我没有讲的部分就是数据初始化的部分,不过很简单,大家自己摸索。
我的答案

public int[] findOrder(int numCourses, int[][] prerequisites) {
        // record dependecy counts
        HashMap<Integer, Integer> dependeciesCount = new HashMap<>();
        HashMap<Integer, HashSet<Integer>> dependeciesRelation = new HashMap<>();
        for (int i = 0; i < numCourses; i++) {
            dependeciesCount.put(i, 0);
            dependeciesRelation.put(i, new HashSet<>());
        }
        for (int i = 0; i < prerequisites.length; i++) {
            int pre = prerequisites[i][1];
            int suf = prerequisites[i][0];
            dependeciesCount.put(suf, dependeciesCount.get(suf) + 1);
            dependeciesRelation.get(pre).add(suf);
        }
        Queue<Integer> queue = new LinkedList<>();
        for (Map.Entry<Integer, Integer> entry : dependeciesCount.entrySet()) {
            if (entry.getValue() == 0) {
                queue.add(entry.getKey());
            }
        }

        int[] index = new int[numCourses];
        int currentIndex = 0;
        while (!queue.isEmpty()) {
            Integer currentCourse = queue.poll();
            index[currentIndex] = currentCourse;
            currentIndex++;

            for (Integer nei : dependeciesRelation.get(currentCourse)) {
                if (dependeciesCount.get(nei) > 0) {
                    dependeciesCount.put(nei, dependeciesCount.get(nei) - 1);
                    if (dependeciesCount.get(nei) == 0) {
                        queue.add(nei);
                    }
                }
            }

        }

        int[] empty = {};
        return currentIndex == numCourses ? index : empty;
    }

后记

最后一期算法教程写完了,其实感觉如果大家能把这7个大块给充分理解,面对大部分的公司的算法面试其实也没多大问题了。这也是我2017年-2018年初面试各个公司的算法题的一些心得体会。
虽然我的标题一直都是以面试 开头,但是我觉得最重要的还是学习,或者说是复习算法的这个过程。去理解去学习的这个过程才是精髓。当然,这些内容也是上学就应该学好的,现在重新复习,也算是还债(technical debt)。。。。
回头看这个系列的初衷,也是希望大家在面对面试的同时,能回顾一些以前上学时候的知识,做到温故而知新。只要读者看了我的文章,能发出一种“挖槽这个以前好像学过啊”的感叹,我也就满足了~

2019年对我来说是一个新的起点,我也要不停的督促自己好好工作,多反思多学习,以后争取能分享更多高质量的文章和知识。希望自己永远不要忘掉当初雄心壮志面试硅谷公司的那颗赤子之心。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,451评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,172评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,782评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,709评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,733评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,578评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,320评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,241评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,686评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,878评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,992评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,715评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,336评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,912评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,040评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,173评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,947评论 2 355

推荐阅读更多精彩内容