数据结构与算法12:图、广度优先、深度优先

news2024/11/8 17:50:20

目录

【图】

【图的存储方法】

方法1:邻接矩阵

方法2:邻接链表 

【图的算法】 

广度优先搜索(BFS)

深度优先搜索(DFS)


【图】

在 数据结构与算法09:二叉树 这篇文章中讲述了“树”这种数据结构,如果把树中非父子关系的节点连接起来,就是一个“图”(Graph),如下所示,将树中的B和C节点连接起来就是一个图:

  • 图中的元素叫作顶点(vertex)
  • 图中的一个顶点可以与任意其他顶点建立连接关系,这种关系叫作边(edge)
  • 跟顶点相连接的边的条数叫作顶点的度(degree)

树和图的区别:

  • 树表达的是层级化的结构,图表达的是网络化的结构。
  • 树有一个根节点,下面的每一棵子树都有唯一的根节点;图的每一个节点都可以看作是平等的,并且节点与节点之间的连接也更为自由。
  • 在树中一个父节点只能与它的子节点相连,但不会与孙子节点相连;图的任意节点都是可以相连的。

图的一个主要应用就是表示社交网络,比如微信中的每个用户可以看作一个顶点,如果两个用户是好友,那就在两者之间建立一条边,整个微信的好友关系就可以用一张图来表示。每个用户有多少个好友,在图中表示就是某个顶点有多少个度。如下所示:

一共有6个用户,其中用户A的好友有B、C、D。用户A一共有3个好友。

另外还有微博的社交关系也可以用图来表示,但是更加复杂一点,因为微博允许单向关注,假设用户A关注了用户B,但用户B可以不关注用户A。用图表示这种单向的社交关系需要引入边的“方向”,如下所示:

用户B和D互相关注,用户C和F互相关注,其它的都是单向关注。

对于 边没有方向的图叫作“无向图”,边有方向的图叫作“有向图”。在有向图中,把度分为入度(In-degree)出度(Out-degree)

  • 顶点的入度:表示有多少条边指向这个顶点,在微博中可以表示有多少粉丝;
  • 顶点的出度:表示有多少条边是以这个顶点为起点指向其他顶点,在微博中表示关注了多少人。

另外还有一种图叫做带权图(weighted graph),每条边都有一个权重(weight),可以通过这个权重来表示好友间的亲密度。如下所示:

图在现实生活中的另一个重要应用就是地图交通网络,比如要规划一条双向车道,可以使用无向图;规划一条单向车道,可以使用有向图。其实无向图也可以认为是有向图的双向指向。

【图的存储方法】

方法1:邻接矩阵

邻接矩阵(Adjacency Matrix):底层依赖一个二维数组,存储起来比较浪费空间,但是使用起来比较节省时间。(空间换时间)

  • 对于无向图来说,如果顶点A与顶点B之间有边,就将 array[A][B] 和 array[B][A] 标记为 1;
  • 对于有向图来说,如果有一条箭头从顶点A指向顶点B,就将 array[A][B] 标记为 1;如果有一条箭头从顶点B指向顶点A,就将 array[B][A] 标记为 1;
  • 对于带权图,数组中就存储相应的权重。

对于无向图来说,array[A][B] = 1 和 array[B][A] = 1 其实只需要存储一个就可以了,所以使用邻接矩阵表示一个图会比较浪费存储空间。

还有一种图是 稀疏图(Sparse Matrix),顶点很多但每个顶点的边并不多,如果使用邻接矩阵的存储方法就更加浪费空间了。如下所示: 

比如微信有好几亿的用户,对应到图上就是好几亿的顶点,但是每个用户的好友并不会很多,一般也就三五百个而已。如果用邻接矩阵来存储,那么绝大部分的存储空间都被浪费了。

方法2:邻接链表 

邻接链表(Adjacency List):底层依赖一个链表,存储起来比较节省空间,但是使用起来比较耗时间。(时间换空间)

  • 这种存储方式有点类似散列表,每个顶点对应一条链表,链表中存储的是与这个顶点相连接的其他顶点;
  • 对于有向图的邻接链表存储方式,每个顶点对应的链表里面存储的是指向的顶点;
  • 对于无向图的邻接链表存储方式,每个顶点的链表中存储的是跟这个顶点有边相连的顶点。

【图的算法】 

 图的算法一般分为广度优先搜索(BFS)深度优先搜索(DFS),主要实现在图中从一个顶点出发到另一个顶点的路径。

广度优先搜索(BFS)

广度优先搜索(Breadth-First-Search,简称为 BFS),是一种“地毯式”层层推进的搜索策略,即先查找离起始顶点最近的,然后依次往外搜索。如下所示:

举个例子:当你去一个地方旅游,进入景区后漫无目的的游览,会有很多条可以游览的路线,如果从起点出发,由近到远的溜达一遍,不走回头路。这个过程就可以理解为广度优先。

使用Go代码实现广度优先搜索如下:

// go-algo-demo/graph/Graph.go
package main

import (
	"container/list"
	"fmt"
)

// 使用邻接链表存储无向图
type Graph struct {
	data  []*list.List
	value int
}

// 根据设定的容量初始化一个图
func newGraph(v int) *Graph {
	graph := &Graph{
		data:  make([]*list.List, v),
		value: v,
	}
	for i := range graph.data {
		graph.data[i] = list.New()
	}
	return graph
}

// 给图添加边,每条边都添加进去
func (self *Graph) addEdge(start int, end int) {
	//无向图的一条边需要添加两次
	self.data[start].PushBack(end)
	self.data[end].PushBack(start)
}

// 广度优先搜索:start起始点,end结束点
// 搜索一条从start到end的最短路径
func (self *Graph) BFS(start int, end int) {
	if start == end {
		return
	}

	//visited记录已经被访问的顶点
	visited := make([]bool, self.value)
	visited[start] = true

	//queue是一个队列,用来存储已经被访问、但相连的顶点还没有被访问的顶点
	var queue []int
	queue = append(queue, start)

	//path用来记录搜索路径,从顶点start开始,广度优先搜索到顶点end后,path数组中存储的就是搜索的路径
	path := make([]int, self.value)
	for index := range path {
		path[index] = -1
	}

	//标记是否已找到
	isFound := false
	for len(queue) > 0 && !isFound {
		top := queue[0]
		queue = queue[1:]
		linkedlist := self.data[top]
		for e := linkedlist.Front(); e != nil; e = e.Next() {
			k := e.Value.(int)
			if !visited[k] {
				path[k] = top
				if k == end {
					isFound = true
					break
				}
				queue = append(queue, k)
				visited[k] = true
			}
		}
	}
	if isFound {
		printPath(path, start, end)
	} else {
		fmt.Printf("从 %d 到 %d 的路径没有找到\n", start, end)
	}
}

// 递归打印路径
func printPath(path []int, s int, t int) {
	if t == s || path[t] == -1 {
		fmt.Printf("%d ", t)
	} else {
		printPath(path, s, path[t])
		fmt.Printf("%d ", t)
	}
}

func main() {
	graph := newGraph(8)
	//把图的所有边都添加进去
	graph.addEdge(0, 1)
	graph.addEdge(1, 2)
	graph.addEdge(0, 3)
	graph.addEdge(1, 4)
	graph.addEdge(3, 4)
	graph.addEdge(4, 5)
	graph.addEdge(2, 5)
	graph.addEdge(5, 7)
	graph.addEdge(4, 6)
	graph.addEdge(6, 7)

	//广度优先搜索从0到6的路径
	graph.BFS(0, 6) //0 1 4 6

	//广度优先搜索从3到7的路径
	graph.BFS(3, 7) //3 4 5 7
}

代码说明:

  • visited记录的是已经被访问的顶点,避免顶点被重复访问。如果顶点 q 被访问,那相应的 visited[q] 设置为 true。
  • queue是一个队列,用来存储已经被访问、但相连的顶点还没有被访问的顶点。因为广度优先搜索是逐层访问的,只有把第 k 层的顶点都访问完成之后,才能访问第 k+1 层的顶点。当访问到第 k 层顶点的时候,需要把第 k 层的顶点记录下来,稍后才能通过第 k 层的顶点来找第 k+1 层的顶点。
  • path用来记录搜索路径,从顶点start开始,广度优先搜索到顶点end后,path数组中存储的就是搜索的路径。不过,这个路径是反向存储的, path[x] 存储的是顶点x是从哪个前驱顶点遍历过来的。比如,通过顶点2的邻接表访问到顶点3,那 path[3] 就等于2。为了正向打印出路径,需要递归地打印,可以看下 printPath() 函数的实现方式。

复杂度分析:

  • 假设 V 表示顶点的个数,E 表示边的个数
  • 最坏情况下,结束点end距离起始点start很远,需要遍历完整个图才能找到。这个时候,每个顶点都要进出一遍队列,每个边也都会被访问一次,所以广度优先搜索的时间复杂度是 O(V+E)。如果一个图中的所有顶点都是连通的,E 肯定要大于等于 V-1。所以,广度优先搜索的时间复杂度是 O(E)
  • 广度优先搜索的空间消耗主要在变量 visited 数组、queue 队列、path 数组上,这三个存储空间的大小都不会超过顶点的个数,所以广度优先搜索的空间复杂度是 O(V)

深度优先搜索(DFS)

深度优先搜索(Depth-First-Search,简称为 DFS),从起始顶点出发随意选一条路走下去,如果发现路不通,又得原路返回到上一个岔路口重新找下一条路线,类似于“走迷宫”的场景。如下所示:

举个例子:当你去一个地方旅游,进入景区后不想瞎溜达,只想奔着一个目的地,需要从起点出发找一条路试试,如果这条路不能到达目的地,需要原路返回到上一个岔路口重新选择新的路线。这个过程就可以理解为深度优先。深度优先搜索用的是 回溯思想,适合用递归来实现。 

使用Go代码实现深度优先搜索如下:

// 广度优先搜索:start起始点,end结束点
func (self *Graph) DFS(start int, end int) {
	path := make([]int, self.value)
	for i := range path {
		path[i] = -1
	}
	visited := make([]bool, self.value)
	visited[start] = true

	isFound := false
	self.DFSRecurse(start, end, path, visited, isFound)
	printPath(path, start, end)
}

// 广度优先:递归搜索路径
func (self *Graph) DFSRecurse(start int, end int, path []int, visited []bool, isFound bool) {
	if isFound {
		return
	}
	visited[start] = true
	if start == end {
		isFound = true
		return
	}
	linkedlist := self.data[start]
	for e := linkedlist.Front(); e != nil; e = e.Next() {
		k := e.Value.(int)
		if !visited[k] {
			path[k] = start
			self.DFSRecurse(k, end, path, visited, false)
		}
	}
}

func main() {
	graph := newGraph(8)
	//把图的所有边都添加进去
	graph.addEdge(0, 1)
	graph.addEdge(1, 2)
	graph.addEdge(0, 3)
	graph.addEdge(1, 4)
	graph.addEdge(3, 4)
	graph.addEdge(4, 5)
	graph.addEdge(2, 5)
	graph.addEdge(5, 7)
	graph.addEdge(4, 6)
	graph.addEdge(6, 7)

	//深度优先搜索从0到6的路径
	graph.DFS(0, 6) //0 1 2 5 4 6

	//深度优先搜索从3到7的路径
	graph.DFS(3, 7) //3 0 1 2 5 4 6 7
}

复杂度分析:

  • 假设 V 表示顶点的个数,E 表示边的个数。

  • 每条边最多会被访问两次,一次是遍历,一次是回退。所以,深度优先搜索算法的时间复杂度是 O(E)

  • 深度优先搜索算法的消耗内存主要是 visited、path 数组和递归调用栈,visited、path 数组的大小跟顶点的个数 V 成正比,递归调用栈的最大深度不会超过顶点的个数,所以深度优先算法的空间复杂度是 O(V)

总结:广度优先搜索需要使用队列来实现,遍历得到的路径就是起始顶点到终止顶点的最短路径;深度优先搜索用的是回溯思想,适合用递归实现,可以使用栈来实现的。深度优先和广度优先的时间复杂度都是 O(边的个数),空间复杂度是 O(顶点的个数)。

源代码:https://gitee.com/rxbook/go-algo-demo/blob/master/graph/Graph.go

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/624371.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

RPC(远程过程调用)与消息队列介绍

文章目录 前言 一、过程调用分类 1.本地调用(Local Procedure Call,简称LPC) 2.远程调用(Remote Procedure Call,简称RPC) 二、API/SDK的区别是什么? 开发过程中,我们经常需要调用别人写的功能 三、WebClient 四、消息服务 目录 前言 一、过程调用分类 1.本…

excel转xmind

有如下excel,我们想把它转为xmind; 一、主流程 先说一下主要的流程: 需要把excel数据复制出来,放到文本编辑器(如notepad)中,比较乱哈,如下: 然后需要调整成如下格式…

JavaSE-04【方法】

JavaSE-04【方法】 第一章 方法 1.1 方法定义的格式详解 1、方法:就是若干语句的功能集合2、生活案例: 爆米花机: 原料:(玉米、糖) 产物:爆米花 3、方法中的两个重要名词 参数:即原料,就是进…

api接口汇总的平台

大麦网是一个在线购票平台,为音乐会、演唱会、话剧、体育比赛等各类娱乐活动提供门票销售服务。通过大麦网,用户可以轻松购买心仪的演出门票,并享受到良好的购票体验。 为了让更多用户了解到大麦网的商品详情,并能够方便地获取相…

网络协议 — IPv6 互联网协议第 6 版

目录 文章目录 目录IPv6IPv6 数据包格式固定报头扩展头部 IPv6 地址格式IPv6 网络的基本组成元素IPv6 的地址分类和寻址模式单播地址全球唯一地址(Global Unique Address)唯一本地地址(Unique Local Unicast Address)链路本地地址…

新手学习eclipse使用

目录 1 工具安装2 安装插件3 创建项目4 启动项目总结 对于新手程序员来说,选择一款趁手的工具还是有必要的。目前IDE比较好使用的是两块IDEA和eclipse,IDEA收费而且每年的费用不低,eclipse免费比较适合使用。 1 工具安装 下载地址&#xff1…

oracle-缩小表空间

刚准备收拾东西准备下班,突然接一个帮忙的事情,11g rac环境数据磁盘组使用率100%了,业务无法使用了,重新开电脑速战速决。 直接登录环境中,计划立刻释放一点点空间出来让业务恢复使用,业务恢复了再考虑增加…

100天精通Golang(基础入门篇)——第4天: Go语言中的变量与常量详解:声明、赋值和使用

🌷 博主 libin9iOak带您 Go to Golang Language.✨ 🦄 个人主页——libin9iOak的博客🎐 🐳 《面试题大全》 文章图文并茂🦕生动形象🦖简单易学!欢迎大家来踩踩~🌺 🌊 《I…

Shell脚本攻略:expect脚本免交互

目录 目录 一、理论 1.Here Document 2.expect实现免交互 二、实验 1.实验一 2.实验二 3.实验三 4.问题 一、理论 1.Here Document (1)概念 Here Document也被称为here-document/here-text/heredoc/hereis/here-string/here-script&#xff0c…

MYSQL的卸载、下载、安装、配置一步到位(超详细教程)

MYSQL的卸载、下载、安装、配置一步到位(超详细教程) 一、卸载 1.控制面板卸载 全部卸载 2.卸载数据文件 在安装的时候会有安装地址和数据库地址 找到自己安装数据库的地址,删除MySQL里面的内容 3.删除环境变量 此电脑–>属性–>高级系统设置–>环境变量…

爬虫代理IP池怎么来的,可能遇到哪些问题,怎么解决

目录 前言 一、代理IP对爬虫工作的重要性 二、代理IP池从哪里来 三、爬虫工作中可能会遇到哪些问题 四、怎么解决遇到的问题 总结 前言 爬虫工作离不开代理IP的支持,代理IP在爬虫工作中发挥重要的作用,但爬虫代理IP池从哪里来呢,爬虫工…

【MS1023 串化器和 MS1224 解串器--10MHz 到 80MHz、10:1 LVDS 并串转换器(串化器)/串并转换器(解串器)无标题】

MS1023 串化器和 MS1224 解串器是一对 10bit 并串 / 串并转 换芯片,用于在 LVDS 差分底板上传输和接收 10MHz 至 80MHz 的并行字速率的串行数据。起始 / 停止位加载后,转换为负载编 码输出,串行数据速率介于 120Mbps 至 960M…

网络安全真的没法入行吗?——网络安全自学笔记

前言 十多年前还是高中生的时候开始搞安全的,刚开始是看大佬们在群里发黑页觉得很牛逼。然后慢慢开始学,当时还在网上问过IP和ID有什么区别,,, 后来慢慢开始学注入,日到了第一个站,是家卖钢琴…

freeswitch透传带SDP的180

概述 freeswitch是一款简单好用的VOIP开源软交换平台。 freeswitch对于180/183的消息处理有默认的规则,但是在3GPP的标准中,消息流程会更加复杂,场景更多变。 这样就需要我们根据实际环境中的场景定制消息流程。 本文只讨论带SDP的183/18…

用代码创造童话,永葆快乐时光

前言 随着科技的不断发展,大家对于世界的认知也越来越广泛和深入。在这个数字化时代,编程语言已经成为了一种全新的语言形式,创造了一个全新的世界,也为人们带来了无限的可能性。当然,这些可能性不止局限于商业领域和技…

如何获得铁粉

「作者主页」:士别三日wyx 「作者简介」:CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 「推荐专栏」:对网络安全感兴趣的小伙伴可以关注专栏《网络安全入门到精通》 很多博主都有一些困惑: 我写了一些博客…

【多线程】模拟实现一个定时器

1. Java自带的定时器 相信大家都定过闹钟,在我上学有早八的时候,硬是要定三个闹钟才起得来,7:20,7:30,7:40,那么我们今天所要实现的定时器,就类似于闹钟,设定多长时间之后&#xff0…

全网IPv6流量监控分析案例

前言 随着某学院IPv6网络的建设和应用投产,在不影响现有应用、网络及用户端的情况下,实时掌握IPv6网络运行状况、用户体验快慢、网络应用性能好坏及网络资源利用等需求已迫在眉睫。 学校率先采用IPv6全流量分析系统 NetInside率先推出支持IPv6的全流量…

高速电路PCB布线还有不会的吗?

数字电路很多时候需要的电流是不连续的,所以对一些高速器件就会产生浪涌电流。如果电源走线很长,则由于浪涌电流的存在进而会导致高频噪声,而此高频噪声会引入到其他信号中去。而在高速电路中必然会存在寄生电感和寄生电阻以及寄生电容&#…

pwn(1)-栈溢出(上)

熟悉栈溢出的原理熟悉栈溢出的防御方法学会栈溢出的利用方法学会栈溢出的奇技淫巧 栈溢出原理和防御&#xff08;一&#xff09; 栈的高地址在下低地址在上&#xff0c;先进入的数据压入栈底。 例如 #include <stdio.h> int add(int a,int b) {return ab; } int main…