0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

图的逻辑结构是怎样的?如何去实现它?

算法与数据结构 来源:labuladong 作者:labuladong 2021-05-08 16:34 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

经常有读者问我「图」这种数据结构,因为我们公众号什么数据结构和算法都写过了,唯独没有专门介绍「图」。

其实在学习数据结构和算法的框架思维中说过,虽然图可以玩出更多的算法,解决更复杂的问题,但本质上图可以认为是多叉树的延伸。

面试笔试很少出现图相关的问题,就算有,大多也是简单的遍历问题,基本上可以完全照搬多叉树的遍历。

至于最小生成树,Dijkstra,网络流这些算法问题,他们当然很牛逼,但是,就算法笔试来说,学习的成本高但收益低,没什么性价比,不如多刷几道动态规划,真的。

那么,本文依然秉持我们号的风格,只讲「图」最实用的,离我们最近的部分,让你心里对图有个直观的认识。

图的逻辑结构和具体实现

一幅图是由节点和边构成的,逻辑结构如下:

图的逻辑结构是怎样的?如何去实现它?

什么叫「逻辑结构」?就是说为了方便研究,我们把图抽象成这个样子。

根据这个逻辑结构,我们可以认为每个节点的实现如下:

/* 图节点的逻辑结构 */class Vertex {

int id;

Vertex[] neighbors;

}

看到这个实现,你有没有很熟悉?它和我们之前说的多叉树节点几乎完全一样:

/* 基本的 N 叉树节点 */class TreeNode {

int val;

TreeNode[] children;

}

所以说,图真的没啥高深的,就是高级点的多叉树而已。

不过呢,上面的这种实现是「逻辑上的」,实际上我们很少用这个Vertex类实现图,而是用常说的邻接表和邻接矩阵来实现。

比如还是刚才那幅图:

图的逻辑结构是怎样的?如何去实现它?

用邻接表和邻接矩阵的存储方式如下:

邻接表很直观,我把每个节点x的邻居都存到一个列表里,然后把x和这个列表关联起来,这样就可以通过一个节点x找到它的所有相邻节点。

邻接矩阵则是一个二维布尔数组,我们权且成为matrix,如果节点x和y是相连的,那么就把matrix[x][y]设为true。如果想找节点x的邻居,去扫一圈matrix[x][。。]就行了。

图的逻辑结构是怎样的?如何去实现它?

那么,为什么有这两种存储图的方式呢?肯定是因为他们各有优劣。

对于邻接表,好处是占用的空间少。

你看邻接矩阵里面空着那么多位置,肯定需要更多的存储空间。

但是,邻接表无法快速判断两个节点是否相邻。

比如说我想判断节点1是否和节点3相邻,我要去邻接表里1对应的邻居列表里查找3是否存在。但对于邻接矩阵就简单了,只要看看matrix[1][3]就知道了,效率高。

所以说,使用哪一种方式实现图,要看具体情况。

好了,对于「图」这种数据结构,能看懂上面这些就绰绰够用了。

那你可能会问,我们这个图的模型仅仅是「有向无权图」,不是还有什么加权图,无向图,等等……

其实,这些更复杂的模型都是基于这个最简单的图衍生出来的。

有向加权图怎么实现?很简单呀:

如果是邻接表,我们不仅仅存储某个节点x的所有邻居节点,还存储x到每个邻居的权重,不就实现加权有向图了吗?

如果是邻接矩阵,matrix[x][y]不再是布尔值,而是一个 int 值,0 表示没有连接,其他值表示权重,不就变成加权有向图了吗?

无向图怎么实现?也很简单,所谓的「无向」,是不是等同于「双向」?

图的逻辑结构是怎样的?如何去实现它?

如果连接无向图中的节点x和y,把matrix[x][y]和matrix[y][x]都变成true不就行了;邻接表也是类似的操作。

把上面的技巧合起来,就变成了无向加权图……

好了,关于图的基本介绍就到这里,现在不管来什么乱七八糟的图,你心里应该都有底了。

下面来看看所有数据结构都逃不过的问题:遍历。

图的遍历

图怎么遍历?还是那句话,参考多叉树,多叉树的遍历框架如下:

/* 多叉树遍历框架 */void traverse(TreeNode root) {

if (root == null) return;

for (TreeNode child : root.children)

traverse(child);

}

图和多叉树最大的区别是,图是可能包含环的,你从图的某一个节点开始遍历,有可能走了一圈又回到这个节点。

所以,如果图包含环,遍历框架就要一个visited数组进行辅助:

Graph graph;

boolean[] visited;

/* 图遍历框架 */void traverse(Graph graph, int s) {

if (visited[s]) return;

// 经过节点 s

visited[s] = true;

for (TreeNode neighbor : graph.neighbors(s))

traverse(neighbor);

// 离开节点 s

visited[s] = false;

}

好吧,看到这个框架,你是不是又想到了 回溯算法核心套路 中的回溯算法框架?

这个visited数组的操作很像回溯算法做「做选择」和「撤销选择」,区别在于位置,回溯算法的「做选择」和「撤销选择」在 for 循环里面,而对visited数组的操作在 for 循环外面。

在 for 循环里面和外面唯一的区别就是对根节点的处理。

比如下面两种多叉树的遍历:

void traverse(TreeNode root) {

if (root == null) return;

System.out.println(“enter: ” + root.val);

for (TreeNode child : root.children) {

traverse(child);

}

System.out.println(“leave: ” + root.val);

}

void traverse(TreeNode root) {

if (root == null) return;

for (TreeNode child : root.children) {

System.out.println(“enter: ” + child.val);

traverse(child);

System.out.println(“leave: ” + child.val);

}

}

前者会正确打印所有节点的进入和离开信息,而后者唯独会少打印整棵树根节点的进入和离开信息。

为什么回溯算法框架会用后者?因为回溯算法关注的不是节点,而是树枝,不信你看 回溯算法核心套路 里面的图,它可以忽略根节点。

显然,对于这里「图」的遍历,我们应该把visited的操作放到 for 循环外面,否则会漏掉起始点的遍历。

当然,当有向图含有环的时候才需要visited数组辅助,如果不含环,连visited数组都省了,基本就是多叉树的遍历。

题目实践

下面我们来看力扣第 797 题「所有可能路径」,函数签名如下:

List《List《Integer》》 allPathsSourceTarget(int[][] graph);

题目输入一幅有向无环图,这个图包含n个节点,标号为0, 1, 2,。。。, n - 1,请你计算所有从节点0到节点n - 1的路径。

输入的这个graph其实就是「邻接表」表示的一幅图,graph[i]存储这节点i的所有邻居节点。

比如输入graph = [[1,2],[3],[3],[]],就代表下面这幅图:

图的逻辑结构是怎样的?如何去实现它?

算法应该返回[[0,1,3],[0,2,3]],即0到3的所有路径。

解法很简单,以0为起点遍历图,同时记录遍历过的路径,当遍历到终点时将路径记录下来即可。

既然输入的图是无环的,我们就不需要visited数组辅助了,直接套用图的遍历框架:

// 记录所有路径

List《List《Integer》》 res = new LinkedList《》();

public List《List《Integer》》 allPathsSourceTarget(int[][] graph) {

LinkedList《Integer》 path = new LinkedList《》();

traverse(graph, 0, path);

return res;

}

/* 图的遍历框架 */void traverse(int[][] graph, int s, LinkedList《Integer》 path) {

// 添加节点 s 到路径

path.addLast(s);

int n = graph.length;

if (s == n - 1) {

// 到达终点

res.add(new LinkedList《》(path));

path.removeLast();

return;

}

// 递归每个相邻节点

for (int v : graph[s]) {

traverse(graph, v, path);

}

// 从路径移出节点 s

path.removeLast();

}

这道题就这样解决了。

最后总结一下,图的存储方式主要有邻接表和邻接矩阵,无论什么花里胡哨的图,都可以用这两种方式存储。

在笔试中,最常考的算法是图的遍历,和多叉树的遍历框架是非常类似的。

当然,图还会有很多其他的有趣算法,比如二分图判定呀,环检测呀(编译器循环引用检测就是类似的算法)等等,以后有机会再讲吧,本文就到这了。

责任编辑:lq6

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 算法
    +关注

    关注

    23

    文章

    4803

    浏览量

    98521
  • 节点
    +关注

    关注

    0

    文章

    230

    浏览量

    25662
  • 数据结构
    +关注

    关注

    3

    文章

    573

    浏览量

    41675

原文标题:为什么我没写过「图」相关的算法?

文章出处:【微信号:TheAlgorithm,微信公众号:算法与数据结构】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    Debian 69未检测到Intenso NVME,怎样才能让工作?

    嘿。我刚刚设法更新到 2.5.0 并启动了 Debian 版本 69。 但未检测到我的新 NVME SSD它在闪烁,但也许我必须做其他事情然后将其内置? 我怎样才能让工作? 谢谢
    发表于 03-25 06:38

    Altair OptiStruct:重构结构研发逻辑,引领工业仿真与优化新纪元

    在工业制造向高端化、轻量化、智能化全速转型的当下,产品结构研发早已告别“经验设计+反复试制”的传统模式,转而依托精准仿真与智能优化技术,实现性能、成本、效率的三重突破。作为Altair澳汰尔旗下历经
    发表于 03-20 10:25

    怎样开始启用独立看门狗呢?

    看门狗的原理是什么呢? 怎样开始启用独立看门狗呢?
    发表于 01-08 06:33

    深入Linux内核:进程调度的核心逻辑实现细节

    ,背后都离不开内核调度算法的精准操控。今天,我们就从优先级、调度算法、时间片分配到底层实现,全方位拆解Linux内核进程调度的核心逻辑。 一、进程调度的“身份标识”:优先级与分类 要理解调度逻辑,首先得搞懂:进程凭什么“插队”?
    的头像 发表于 12-24 07:05 4571次阅读
    深入Linux内核:进程调度的核心<b class='flag-5'>逻辑</b>与<b class='flag-5'>实现</b>细节

    顺络绕线贴片电感结构怎样的?

    组成 骨架 :采用高精度材料(如陶瓷、铁氧体或铝)制成,作为绕线的基础支撑结构。骨架形状多样,包括工字型、长方形等,其中工字型骨架底部设计焊接部分,便于表面贴装。 绕线 :使用漆包线绕制在骨架上,通过调整匝数实现不同电
    的头像 发表于 11-07 17:45 883次阅读
    顺络绕线贴片电感<b class='flag-5'>结构</b>是<b class='flag-5'>怎样</b>的?

    图解码说-六大UML类关系(依赖,继承,实现,关联,聚合,组合)

    UML 类是面向对象设计的 “施工”,而依赖、继承、实现、关联、聚合、组合这六大关系,就是图中定义类与类互动规则的核心 “语法”。掌握它们,就能快速看懂类的协作逻辑与系统
    的头像 发表于 11-05 09:03 897次阅读
    图解码说-六大UML类<b class='flag-5'>图</b>关系(依赖,继承,<b class='flag-5'>实现</b>,关联,聚合,组合)

    复杂的软件算法硬件IP核的实现

    的函数功能的简短的描述。 HASM 语言包含了两种结构,一种是功能域一种是结构域。 功能域负责一些基本的运算操作,例如算术运算、逻辑运算还有数据传送操作,通常由组合逻辑
    发表于 10-30 07:02

    AES加解密算法逻辑实现及其在蜂鸟E203SoC上的应用介绍

    这次分享我们会简要介绍AES加解密算法的逻辑实现,以及如何将AES算法做成硬件协处理器集成在蜂鸟E203 SoC上。 AES算法介绍 AES算法属于对称密码算法中的分组密码,其明文/密文分组长度为
    发表于 10-29 07:29

    如何通过地址生成器实现神经网络特征的padding?

    。所以我们选择更易实现,更加节省控制逻辑结构的直接存储方式。 对于这种直接存储方式,其填充零的判断逻辑为: 当目标地址为输出特征的第一行
    发表于 10-22 08:15

    可编程逻辑控制器PLC是什么?如何实现上网通信?

    具有编程灵活、抗干扰强、适应工业环境的特点,广泛应用于制造业、交通、能源等领域。 PLC上网通信的实现方式 PLC实现上网通信的核心是通过通信模块、网络协议和硬件连接,将工业控制设备接入局域网或互联网,
    的头像 发表于 09-22 17:27 1212次阅读

    光纤光谱仪是什么?一分钟读懂的原理与结构

    众多领域。那么,什么是光纤光谱仪?的工作原理和内部结构又是怎样的?本文将用通俗易懂的方式为你揭开光纤光谱仪的“神秘面纱”。 一、什么是光纤光谱仪? 光纤光谱仪是一种通过光纤采集被测光源,并对其进行光谱分解与分析
    的头像 发表于 07-07 14:27 1356次阅读

    HarmonyOS实战:3秒实现一个自定义轮播

    轮播作为应用程序中最普通使用的控件被广泛应用,相信对于来发者来说并不陌生。在 Android 中实现一个 轮播很多选择使用第三方的插件,毕竟在有限的开发排期中自己动手
    的头像 发表于 06-24 17:06 1591次阅读

    从底层逻辑到架构设计:聚徽解析MES看板的技术实现路径

    与数据接口的协同设计。本文将从底层逻辑出发,深入解析MES看板的技术架构与实现路径。 一、底层逻辑:数据驱动的生产管理 MES看板的核心价值在于将生产现场的离散数据转化为可执行信息,其底层逻辑
    的头像 发表于 06-16 15:23 819次阅读

    实用电子电路设计(全6本)——数字逻辑电路的ASIC设计

    由于资料内存过大,分开上传,有需要的朋友可以主页搜索下载哦~ 本文以实现高速高可靠性的数字系统设计为目标,以完全同步式电路为基础,从技术实现的角度介绍ASIC逻辑电路设计技术。
    发表于 05-15 15:22

    SMA 接头与 PCB 原理连接的底层逻辑

    SMA插头与PCB原理连接的底层逻辑涵盖连接结构、信号传输和电磁兼容性等多个方面。德索精密工业凭借深厚的技术积累和丰富的实践经验,为电子工程师在设计和制造过程中提供有力的技术支持,助力确保信号的稳定传输,提升电子设备的性能。
    的头像 发表于 04-23 08:53 1303次阅读
    SMA 接头与 PCB 原理<b class='flag-5'>图</b>连接的底层<b class='flag-5'>逻辑</b>