几天没更新了, 最近比较多事情做,题目有做就是没时间写文案。现在回来更新一篇理论+算法
最近做题目的时候, 看到了有题目需要用到「图」的算法, 由于我一直在沉迷于做题目, 比较少讲数据结构相关的知识, 因为感觉那些结构体的比较容易懂, 而且实现起来也不难, 只需要动手敲好代码, 你就能懂「单链表、双链表、队列、栈、数、图等数据结构」
今天呢,我只来讲解「图」的结构,毕竟在数据结构中「树和图」比较常用, 当然了这也是建立与链表的基础上才能比较容易的去理解「图和树」 所以还不懂这些数据结构的小伙伴建议先补补知识,再回来看我这一篇关于图的文章。
我们先来看一下,我这一章节主要讲解的目录
图的分类
图有很多种类型,但他们的本质都差不多
- 无向图
- 有向图
- 无向完全图
- 有向完全图
- 连通图、强连通图
- 网
这是整个图的大部分知识了, 其实也象征着,今天这个文章将会很长很长,那么废话不多说,进入主题好了
无向图
无向图 的定义很简单,看名字大致上都知道这个图不存在方向。也就是顶点和顶点之间只存在一条「边」
若顶点 「A」 和 「B」 之间的边没有方向,则叫做「无向边」,如果任意两个顶点之间的边都是无向的,则叫该图为「无向图」
对于下图而言,我们用G代表该图,V代表顶点,E代表边,所以下图的表达式则是:
「G = (V,{E})」,顶点集合「V={A,B,C,D}」,边集合「E={(A,B),(A,C),(A,D),(B,C),(C,D)}」
有向图
有向图的意思是说,存在一条「弧」(也可以叫「有向边」),在任意两个顶点中,称为有向图
若顶点 「A」 和 「B」 之间的边有方向,则叫做「有向边或弧」,如果任意两个顶点之间的边都是有向的,则叫该图为「有向图」
对于下图而言,我们用G代表该图,V代表顶点,E代表边,若顶点A和D是有向边,则「A是弧尾」,「D是弧头」,「<A,B>表示弧」所以下图的表达式则是:
「G = (V,{E})」,顶点集合「V={A,B,C,D}」,边集合「E={<A,D>,<B,A>,<B,C>,<C,A>,<D,C>}」
无向完全图
在无向图的定义中,增加了一条规矩,若「任意两个顶点存都存在边,则称为无向完全图」
有向完全图
在有向图的定义中,也增加了一条规矩,若「任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图」
连通图
连通图的定义, 在无向图中,如果顶点A到顶点B之间有路径,就说A和B是连通的,如果说任意两顶点都是连通图,则称为连通图
强连通图
在有向图里,如果「每一对」顶点,都有「A->B 或 B->A」的路径,就称为是强连通图
图的存储
这里讲解2种比较常见的(剩下很多种,都是基于邻接表的改良,有兴趣的小伙伴可自行搜索)
- 邻接矩阵
- 邻接表
邻接矩阵
邻接矩阵主要的是把图 描述成了一个数组的形式,使用两个数组将图的顶点和边的集合进行了一个结构体的整合,也可以称为一个结构体。当然也可以分成两个数组来描述一个图 需要注意的是, 矩阵的对角线上的数据都为0
如果是有向图的话矩阵法又应该如何表达呢?
需要注意的是,在有向图里因为是有方向性的,因此要注意他的「入度」跟「出度」 「纵向」代表着某个顶点的「入度」,「横向」代表着某个顶点的「出度」
邻接表
在说邻接表之前,我们先来复习一下什么是链表?
链表分为了一个个单独的存储块,使用指针将他们一个个串起来,也就是连表有头和尾之分,链表的形式有很多种,用法也各有不同,大部分数据结构都是从链表变化而来。「图、树、队列、栈等」都是从它而来,在这里我也简单带过一下而已,不详细讨论它的好坏。
最常见的链表格式就长这个样子,链表头通常不存放数据,只指向第一个元素,返回的时候就「返回这个链表头」就能找到整个链表的数据了。每个元素的指针项都指向下一个元素。以此类推
然后我们再来看一下邻接表的表示方法
我们使用 「^」 去代表这个节点是最终的节点了,我们把A的连通顶点的边都使用变脸去串起来。最后的节点则使用终结符 「^」 代表\
那如果是有向图呢?应该怎么表示?
我们只记录有向图的「出度」,与无向图的记录方式一致
图的算法
图的算法相信大家怎么样也听说过一些他们的名声,而我是主要介绍以下几种算法的实现
- DFS(Depth First Search)深度优先算法
- BFS(Breadth First Search)广度优先算法
- Dijkstra 最短路径算法 「(下一节详细讲, 这一节讲不完)」
- Kruskal算法(生成树的算法-暂时不讲)
- Prim 算法(生成树的算法-暂时不讲)
DFS(Depth First Search)深度优先算法
用一句话来概括深度优先算法「一条道走到黑」,没听错,深度优先算法的核心就是从一个顶点出发,往该顶点能通往的路一致往前走。而实现DFS的数据结构是「栈」,先进后出的思路,让我们来看一下图解
- 从顶点A 入栈,每一次从栈顶元素弹出一个元素
- 把弹出元素的「邻接节点」入栈
- 循环这个操作。
所以图中输出是「A->D->E->B->C」 从一条路走到底后,再选另外一条路, 让我们来看一下代码
先使用邻接矩阵表达式 记录一个新的图「A,B,C,D,E,F」
ans := make(map[string][]string)
ans["A"] = []string{"B", "C"}
ans["B"] = []string{"A", "C", "D"}
ans["C"] = []string{"A", "B", "D", "E"}
ans["D"] = []string{"B", "C", "D", "F"}
ans["E"] = []string{"C", "D"}
ans["F"] = []string{"D"}
dfs方法代码
func dfs(graph map[string][]string, start string) {
stack := []string{}
isTouch := make(map[string]int)
stack = append(stack, start) //把开始顶点入栈
isTouch[start] = 1 //标记当前节点已经走过了
for len(stack) > 0 {
n := len(stack)
node := stack[n-1] //弹出栈顶元素
if len(stack) >= 1 {
stack = stack[:n-1] // 去掉栈顶元素
}
nodes := graph[node] //获取元素的邻接节点
for _, node := range nodes {
if _, ok := isTouch[node]; !ok {
stack = append(stack, node) //把邻接元素入栈
isTouch[node] = 1//标记当前节点已经走过了
}
}
fmt.Println(node)
}
}
然后我们只需要调用一下就可以了
dfs(ans, "E")
BFS(Breadth First Search)广度优先算法
接着我们来讲一下 广度优先算法,广度优先算法是从多条路一起往前出发。就不会与深度一样一次只走一条道,因此广度的实现方法是走「队列」,我们同样来看一下实现方法。
- 从顶点A入队,每一次从队列中出队一个元素
- 把出队元素的「邻接节点」入队
- 循环这个操作。
接着我们看一下这两个非常相似的代码
func bfs(graph map[string][]string, start string) {
queue := []string{}
isTouch := make(map[string]int)
queue = append(queue, start)
isTouch[start] = 1 //標記
for len(queue) > 0 {
node := queue[0] //彈出第一個
if len(queue) >= 1 {
queue = queue[1:]
}
nodes := graph[node]
for _, node := range nodes {
if _, ok := isTouch[node]; !ok {
queue = append(queue, node)
isTouch[node] = 1
}
}
fmt.Println(node)
}
}
两段代码十分相似,唯一的差别就是数据结构的用法不同,不详细标注了
这样子就先说了图的一些基础知识了, 然后我们下一讲开始讲一下最短路径算法,因为最短路径涉及的知识点比较多,我需要点时间准备一下,这一节大家就先了解一下基本的「BFS和DFS」 因为最短路径也是基于「BFS」的
今天的所有代码都会放在Github这里->leetcode算法
创作不易,还希望爱心点一下!今天就讲到这里啦, 我们下一节见。