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

Android程序员面试会遇到的算法系列:java

Android程序员面试会遇到的算法(part 1 关于二叉树的那点事) 附Offer状况python

Android程序员面试会遇到的算法(part 2 广度优先搜索)程序员

Android程序员面试会遇到的算法(part 3 深度优先搜索-回溯backtracking)web

Android程序员面试会遇到的算法(part 4 消息队列的应用)面试

Android程序员会遇到的算法(part 5 字典树)算法

Android程序员会遇到的算法(part 6 优先级队列PriorityQueue)数组

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

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

这期我打算聊一聊拓扑排序这个算法。在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年对我来讲是一个新的起点,我也要不停的督促本身好好工做,多反思多学习,之后争取能分享更多高质量的文章和知识。但愿本身永远不要忘掉当初雄心壮志面试硅谷公司的那颗赤子之心。

相关文章
相关标签/搜索