面试题面试官:判断图是否有环?
Posted 前端技术栈
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试题面试官:判断图是否有环?相关的知识,希望对你有一定的参考价值。
- 面试官让我写一个判断图是否有环,我没写出来,心想又是“面试造火箭,入职拧螺丝”。我把面试官pass了。没想到开发中真的遇到了判断有向图是否有环。
- 图是一种常见的数据结构,分为有向图和无向图。图是由边和节点组成的。
- 在前端开发中,接触到图的场景不算多。常见的有流程、图形可视化等场景。
- 我们在配置题目流程时遇到了需要判断图是否有环的需求。
大厂面试题分享 面试题库
前后端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库 web前端面试题库 VS java后端面试题库大全
背景
- 简单介绍需求,通过可视化流程配置答题流程,题目与题目之间用线连接,箭头的方向代表下一个题目。回答完当前题目,根据不同的条件,跳到下一题;如果题目流程中有循环,会导致答题流程无法结束,所以需要校验题目的流程中不能有循环。
- 下面的是有循环,不符合条件
- 下面的是无循环,符合条件
技术方案
- 根据需求,我们把题目的流程配置抽象成有向图,题目是节点,题目之间的连线是边。
- 需求里的有无循环,最终可以转换成图是否有环的问题。从图的某个节点作为起点,根据边的方向出发跳到下一个节点,最终是否回到起点。如果回到起点,就是有循环、有环,否则是无循环、无环。
- 去除题目和各种条件等无关的结构,数据结构如下。
//边
export interface Edge
id: string;
source:
cell: string; //这条边的起点的id
[x: string]: any;
;
target:
cell: string; //这条边的终点的id
[x: string]: any;
;
data:
type: 'EDGE',
[x: string]: any;
[x: string]: any;
;
//节点
export interface Node
id: string;
data:
type: 'NODE';
name: string;
[x: string]: any;
;
[x: string]: any;
;
export type Data = Node | Edge;
复制代码
- 测试数据如下
const data: Data[] = [
id: '1',
data:
type: 'NODE',
name: '节点1'
,
id: '2',
data:
type: 'NODE',
name: '节点2'
,
id: '3',
data:
type: 'NODE',
name: '节点3'
,
id: '4',
source:
cell: '1'
,
target:
cell: '2'
,
data:
type: 'EDGE'
,
id: '5',
source:
cell: '1'
,
target:
cell: '3'
,
data:
type: 'EDGE'
];
复制代码
- 根据数据结构和测试数据
data:Data[]
,分为以下几个步骤:- 获得边的集合和节点的集合。
- 根据边的集合和节点的集合,获得每个节点的有向邻居节点的集合。即以每个节点的为起点,通过边连接的下一个节点的集合。例如测试数据
节点1
,通过边id4
和边id5
,可以连接节点2
和节点3
,所以节点1
的邻居节点是节点2
和节点3
,而节点2
和节点3
无有向邻居节点。 - 最后根据有向邻居节点的集合,判断是否有环。
具体实现
- 获得边的集合和节点的集合
const edges: Map<string, Edge> = new Map(), nodes: Map<string, Node> = new Map();
const idMapTargetNodes: Map<string, Node[]> = new Map();
const initGraph = () =>
for (const item of data)
const id = item;
if (item.data.type === 'EDGE')
edges.set(id, item as Edge);
else
nodes.set(id, item as Node);
;
复制代码
- 获取有向邻居节点的集合,这里的集合,可以优化成
id
。我为了方便处理,存储了节点
const idMapTargetNodes: Map<string, Node[]> = new Map();
const initTargetNodes = () =>
for (const [id, edge] of edges)
const source, target = edge;
const sourceId = source.cell, targetId = target.cell;
if (nodes.has(sourceId) && nodes.has(targetId)) //防止有空的边,即边的起点和终点不在节点的集合里
const targetNodes = idMapTargetNodes.get(sourceId);
if (Array.isArray(targetNodes))
targetNodes.push(nodes.get(targetId) as Node);
else
idMapTargetNodes.set(sourceId, [nodes.get(targetId) as Node]);
;
复制代码
- 最后判断是否有环,有两种方式:递归和循环。都是深度优先遍历。
execute
是遍历所有节点,hasCycle
是把图的某个节点做为起点,判断是否有环。如果以所有节点为起点,都没有环,说明这个图没有环。- 递归。
hasCycle
判断当前节点是否有环;checked
是做优化,防止某些节点多次检查,回溯阶段,把当前节点加入checked
;visited
记录当前执行的hasCycle
里是否访问过,如果访问过,就是有环。需要注意的是,每次执行hasCycle
时,visited
用的是一个变量,所以在回溯阶段需要把当前节点从visited
里删除。
const checked: Set<string> = new Set(); const hasCycle = (node: Node, visited: Set<Node>) => if (checked.has(node.id)) return false; if (visited.has(node)) return true; visited.add(node); const id = node; const targetNodes = idMapTargetNodes.get(id); if (Array.isArray(targetNodes)) for (const item of targetNodes) if (hasCycle(item, visited)) return true; checked.add(node.id); visited.delete(node); return false; ; const execute = () => const visited: Set<Node> = new Set(); for (const [id, node] of nodes) if (hasCycle(node, visited)) return true; checked.add(id); return false; ; 复制代码
- 循环。
checked
和递归时,作用一样,这里不做说明。visited
是用来判断当前的节点是否遍历过,如果遍历过,就是有环。用循环实现深度优先遍历时,需要用栈
来存储当前链路上的节点,即当前节点已经后代节点。并且从栈
里面获取最后一个节点,作为当前遍历的节点。如果当前节点有向邻居节点不为空,就把有向邻居节点的最后一个节点拿出来压栈;如果有向邻居节点为空,就把当前的节点出栈。在压栈时,如果当前节点在visited
里,就说明有环,如果没有就要把这个节点加入到visited
。在出栈时,把当前节点从visited
里删除掉,因为如果不删掉,当一个节点的多个邻居节点最终指向同一个节点时,会判断为有环。
const checked: Set<string> = new Set(); const hasCycle = (node: Node) => const id = node; if (checked.has(id)) return false; const stack = [id]; const visited: Set<string> = new Set(); visited.add(id); while (stack.length > 0) const lastId = stack[stack.length - 1]; const targetNodes = idMapTargetNodes.get(lastId) || []; if (targetNodes.length > 0) const id = targetNodes.pop() as Node; if (visited.has(id)) return true; stack.push(id); visited.add(id); else stack.pop(); visited.delete(lastId); return false; ; const execute = () => for (const [id, node] of nodes) if (hasCycle(node)) return true; checked.add(id); return false; ; 复制代码
- 递归。
总结
- 要掌握常见的数据结构与算法,本例中用到了图、深度优先遍历
- 大厂面试题分享 面试题库
-
前后端面试题库 (面试必备) 推荐:★★★★★
面试官问我有环链表中怎么找到入口,本以为很简单当场却想傻了
链表是否有环问题看似简单,但实际处理上有很多需要注意的,这个问题是非常高频笔试面试题,记忆不牢固容易遗忘,可以认真看看学习一波!有个小伙伴就在某手面试中遇到了。
判断链表是否有环
题目描述:
给定一个链表,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。
如果链表中存在环,则返回 true 。 否则,返回 false 。
你能用 O(1)(即,常量)内存解决此问题吗?
分析:
对于这个问题,如果没有内存空间的限制,首先想到的就是使用哈希的方法,用一个哈希存储节点,然后向下枚举链表节点:
如果发现其中有在哈希中,那么就说明有环返回true。
如果枚举到最后结束,那就说明没有环
但是这样并不满足O(1)空间复杂度的要求,我们应该怎么处理呢?
如果链表尾部有环,如果一个节点枚举到后面会在闭环中不断循环枚举,那么怎么样能高效判断有环并且能快速终止呢?
有环,其实就是第二次、第三次走过这条路才能说它有环,一个指针在不借助太多空间存储状态下无法有效判断是否有环(有可能链表很长、有可能已经在循环了),咱们可以借助 快慢指针(双指针) 啊。
其核心思想就是利用两个指针:快指针(fast)和慢指针(slow),它们两个同时从链表头遍历链表,只不过两者速度不同,如果存在环那么最终会在循环链表中相遇。
我们在具体实现的时候,可以快指针(fast)每次走两步,慢指针(slow)每次走一步。如果存在环的话快指针先进入环,慢指针后入环,在慢指针到达末尾前快指针会追上慢指针。
快慢指针如果有相遇那就说明有环,如果快指针先为null那就说明没环。
具体实现代码为:
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
ListNode fast=head;
ListNode slow=fast;
while (fast!=null&&fast.next!=null) {
slow=slow.next;
fast=fast.next.next;
if(fast==slow)
return true;
}
return false;
}
}
提高:找到环的入口位置
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。
说明:不允许修改给定的链表。
你是否可以使用 O(1) 空间解决此题?
这题相比上一题又难了一些,因为如果链表成环,需要找到入口。
分析:
如果不考虑内存使用,我肯定还会首先考虑哈希,将节点存着然后如果出现第二次则说明有环并直接返回,实现的代码也很简单,走投无路可以用这个方法:
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
int pos=-1;
Map<ListNode,Integer>map=new HashMap<ListNode, Integer>();
ListNode team=head;
while (team!=null)
{
if(map.containsKey(team)){
pos=map.get(team);
return team;
}
else
map.put(team,++pos);
team=team.next;
}
return null;
}
}
但是怎么使用O(1)的空间复杂度完成这个操作呢?上面一题的思路是使用快慢指针判断是否有环,但是怎么锁定环的入口呢?
这个题看起来是个算法题,实际上是个数学推理题。这题的关键也是快慢指针,不过需要挖掘更多的细节 。
回忆一下快慢指针能够挖掘的细节:
知道慢指针走了x步,快指针走了2x步,但是仅仅知道这两个条件还推导不出什么东西,我们能够进行的操作也只有用O(1)的方法进行一些操作。不过这里面快慢指针和前面有点不同的是我们前面用一个头结点开始计数。
我们还可以进行什么操作?
既然知道相遇的这个点在环内,那么我们可以用一个新的节点去枚举一圈看看环的长度是多少哇!
这里面,我们可以知道fast走的步数2x,slow走的步数x,以及环长y。
我们知道,慢指针是第一次入环,但快指针可能已经走了好几圈,但是多走的步数一定是环的整数倍(不然不可能在同一个位置相遇)。
那么可以得到 快指针步数=慢指针步数+n圈环长度。当然这里n我暂时不知道是多少。换算成公式,那就是 2x=x+ny 消去一个x得到:x=ny。
上面的图我也标注快指针多走的是整数圈数。难点就在这里,需要变通:
快指针多走的x是环长y的整数倍n,慢指针走的x也是环长y的整数倍n。
那么这样有什么用呢?
如果某个节点从起点出发,走到fast,slow交汇点走的是x步(n*y步)。此时,如果某个指针从fast,slow交汇点开始如果走环长的整数倍,那么它到时候还会在原位置。
也就是说从开始head节点team1走x步,从fast,slow交汇节点team2走x步,它们最终依然到达fast,slow交汇的节点,但是在枚举的途中,一旦team1节点遍历的到环内,那么就和team2节点重合了,所以它们一旦相等那就是第一个交汇的点了。
实现代码为:
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
boolean isloop=false;
ListNode fast=new ListNode(0);//头指针
ListNode slow=fast;
fast.next=head;
if(fast.next==null||fast.next.next==null)
return null;
while (fast!=null&&fast.next!=null) {
fast=fast.next.next;
slow=slow.next;
if(fast==slow)
{
isloop=true;
break;
}
}
if(!isloop)//如果没有环返回
return null;
ListNode team=new ListNode(-1);//头指针 下一个才是head
team.next=head;
while (team!=fast) {
team=team.next;
fast=fast.next;
}
return team;
}
}
结语
到这里,链表找环问题就解决了,代码分析可能写的不够好,有问题还请指出,再接再厉!加油!
关于作者:bigsai 主要致力于Java、数据结构与算法知识分享,有个同名原创公众号:
bigsai
,第一时间收获干货!
以上是关于面试题面试官:判断图是否有环?的主要内容,如果未能解决你的问题,请参考以下文章