一篇文章带你了解【图】的结构及相关【算法】

699 阅读7分钟

几天没更新了, 最近比较多事情做,题目有做就是没时间写文案。现在回来更新一篇理论+算法

最近做题目的时候, 看到了有题目需要用到的算法, 由于我一直在沉迷于做题目, 比较少讲数据结构相关的知识, 因为感觉那些结构体的比较容易懂, 而且实现起来也不难, 只需要动手敲好代码, 你就能懂单链表、双链表、队列、栈、数、图等数据结构

今天呢,我只来讲解的结构,毕竟在数据结构中树和图比较常用, 当然了这也是建立与链表的基础上才能比较容易的去理解图和树 所以还不懂这些数据结构的小伙伴建议先补补知识,再回来看我这一篇关于图的文章。

我们先来看一下,我这一章节主要讲解的目录

图的分类

图有很多种类型,但他们的本质都差不多

  • 无向图
  • 有向图
  • 无向完全图
  • 有向完全图
  • 连通图、强连通图

这是整个图的大部分知识了, 其实也象征着,今天这个文章将会很长很长,那么废话不多说,进入主题好了

无向图

无向图 的定义很简单,看名字大致上都知道这个图不存在方向。也就是顶点和顶点之间只存在一条

若顶点 AB 之间的边没有方向,则叫做无向边,如果任意两个顶点之间的边都是无向的,则叫该图为无向图

对于下图而言,我们用G代表该图,V代表顶点,E代表边,所以下图的表达式则是:
G = (V,{E}),顶点集合V={A,B,C,D},边集合E={(A,B),(A,C),(A,D),(B,C),(C,D)}

有向图

有向图的意思是说,存在一条(也可以叫有向边),在任意两个顶点中,称为有向图

若顶点 AB 之间的边有方向,则叫做有向边或弧,如果任意两个顶点之间的边都是有向的,则叫该图为有向图

对于下图而言,我们用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的数据结构是,先进后出的思路,让我们来看一下图解

    1. 从顶点A 入栈,每一次从栈顶元素弹出一个元素
    1. 把弹出元素的邻接节点入栈
    1. 循环这个操作。

所以图中输出是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)广度优先算法

接着我们来讲一下 广度优先算法,广度优先算法是从多条路一起往前出发。就不会与深度一样一次只走一条道,因此广度的实现方法是走队列,我们同样来看一下实现方法。

    1. 从顶点A入队,每一次从队列中出队一个元素
    1. 把出队元素的邻接节点入队
    1. 循环这个操作。

接着我们看一下这两个非常相似的代码

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算法

创作不易,还希望爱心点一下!今天就讲到这里啦, 我们下一节见。