常用数据结构之图的深度优先搜索_广度优先搜索_生成树_普里姆算法_克鲁斯卡尔算法

2021年11月13日 1点热度 0条评论 来源: 杨天超

1.图的基本概念

前面我们聊到数据之间的关系有三种形式,一对一(线性表)、一对多(树),今天我们来聊聊第三种多对多数据的表示形式——图。下面第一幅图,顶点之间没有箭头,称为无向图;第二幅有箭头的称为有向图。如果从一个顶点出发,经过一定的路径后又回到自身,称为回环。图三中每条路径上面的数字称为,带权的图称为。如果两个顶点之间通过一定数量的路径连接,我们称为连通;如果图中任意两节点之间都是连通的,称图为连通图。对于非连通来说,各自连通的最大子图称为连通分量。如果有向图中任意两个节点A、B,从A到B至少有一条路径,从B到A也至少有一条路径,称这样的图为强连通图。非强连通的有向图,各自强连通的最大子图称为强连通分量。先介绍这些基本的概念,后续用到的时候再对相关的概念进行解释。

2.图的存储方式

1.邻接矩阵存储

邻接的意思就是两个顶点之间有边(无向图中节点的连线称为边)或者弧(有向图中节点的连线称为弧,无箭头一端称为弧尾,有箭头一端称为弧头)存在;
下图邻接矩阵中行表示某个顶点,行中的某一列数字表示该顶点与对应列数字的顶点是否邻接,邻接则为1,否则为0;
我们使用邻接矩阵存储顶点之间的关系,另外可以使用一个数组存储各节点对应的值。

2.邻接表存储

上面的邻接矩阵实际上是使用,为了表示顶点间的关系,我们还可以使用数组加链表的形式;使用数组存储每一个顶点中的值,使用链表存储与其相邻接的顶点。
邻接表即适合存储有向图,也适合存储无向图。

3.十字链表存储

十字链表更适合存储有向图,在每一个链表第一个位置成为首元结点,首元结点由三块构成,第一块存储该顶点的值,第二块指针指向以首元结点为弧头的节点,第三个节点指向以首元结点为弧尾的节点。链表中普通节点如图二所示:
tailvex:表示以首元结点为弧尾的顶点位置
headvex:表示以首元结点为弧头的顶点位置
hlink:下一个以首元结点为弧头的顶点的节点
tlink:下一个以首元结点为弧尾的顶点的节点
info:存储权值等信息


4.邻接多重表存储

邻接多重表更适合存储无向图,其相当于是邻接表和十字链表的结合。其首元结点的结构如图一所示,跟邻接表的首元结点结构一致;普通节点的结构如图二所示,跟十字链表一致;
其中:
mark:存储一些额外信息,比如节点是否被操作
ivex,jvex:存储边两端节点在数组中的位置
ilink:下一个于ivex有直接关联的节点
jlink:下一个于jvex有直接关系的节点
info:权值等信息


5.总结

上面我们聊到了四种存储树的方式,邻接矩阵是一种顺序表,使用二维数组表示节点之间两两的邻接关系;邻接表使用数组加链表的形式,数组存储顶点的值,链表存储其邻接顶点的下标位置。适合存储有向图和无向图;十字链表中节点的结构和邻接表不同,每一个节点不仅指向该链表中下一节点,还会指向其它链表中的相应节点,形成了交叉的多个十字。其适合存储有向图;多重邻接表,首元结点采用了邻接表的结构,普通节点采用十字链表的结构,适合存储无向图;

3.深度优先搜索和广度优先搜索

联系前面的树的遍历,深度优先搜索类似于前序遍历,从根节点开始,后被访问的顶点其邻接点先被访问;这是一种回溯的思想,假设节点是有子节点的,然后去找,找到则继续找子节点,如果没有的话则退回来,找上个节点的子节点。广度优先搜索类似了层次遍历,先被访问的顶点,其邻接点先被访问;
下面代码采用python实现,思路如下:广度优先搜索使用队列将每一层中的节点全部压入,再按次序弹出并且将每个节点的子节点压入,直到所有元素都被访问到。深度优先搜索使用迭代,迭代遍历每个子节点的子节点,直到所有的元素都被遍历。

#-*- coding:utf-8 -*-
from queue import Queue
class node:
    def __init__(self,val):
        self.val = val
        self.next = None
class Traversion:
    def __init__(self,nodes):
        self.peeks = []
        self.visted = [0]*len(nodes)
        self.nodes = nodes
    #广度优先搜索
    def bfs(self):
        #使用队列将需要遍历的节点按顺序压入
        queue = Queue()
        queue.put(0)
        self.visted[0] = 1
        self.peeks.append(self.nodes[0][0])
        while not queue.empty():
            index = queue.get()
            for i in self.nodes[index][1]:
                if self.visted[i] == 0:
                    self.visted[i] = 1
                    self.peeks.append(self.nodes[i][0])
                    queue.put(i)
        return self.peeks
    #对每一层中每一个节点进行递归的访问
    def dfs(self,index):
        self.visted[index] = 1
        self.peeks.append(self.nodes[index][0])
        for i in self.nodes[index][1]:
            if self.visted[i] == 0:
                self.dfs(i)
        return self.peeks
if __name__ == '__main__':
    nodes = [("a", [1, 2, 4]),
                ("b", [2]),
                ("c", [4, 3]),
                ("d", [4]),
                ("e", []),]
    traversion = Traversion(nodes)
    print(traversion.dfs(0))

4.深度优先生成树和广度优先生成树

我们现在来考虑如何将图转换为树,这样的话就可以按照树的一些操作方式来对图进行操作。我们考虑两种生成的方式:深度优先和广度优先。对于无向图来说,深度优先生成树就是按照深度优先遍历的方式,将图中经过的顶点进行记录得到的树就是深度优先生成树。广度优先生成树就是记录广度优先遍历经过的顶点来形成一棵树。对于图来说,其节点的发生和延伸是没有规律可循的,杂乱的。我们将其表达为树的形式可以在一定程度上使得数据的发展有规律可循。
例如下面我们对图1中的无向图采用深度优先生成的树为图2的样式,采用广度优先生成的树为图3的样式。



当然,对于非连通图来说。我们采用深度优先和广度优先生成的是多棵树组成的森林,然后可以用我们前面提到的孩子兄弟表示法将其转换为一棵树。

5.最小生成树

通过前面的学习我们知道,可以将图转换为一棵树,可以采用深度优先和广度优先的方式。但是对于现实来说,最优意义的往往是我们接下来要讨论的最小生成树。简单来说,最小生成树就是树中各路径加权求和值最小。
我们将图中的顶点看做是一个个城市,现在要在这些城市之间建立信号网,那么各边上的权就相当于在这两个城市间通信的成本,如果有n个城市,我们需要建立n-1条线。如何使得这n-1条线的总成本最小,对应的就是我们的最小生成树了。
这可是白花花的银子呀!我们接下来讨论一下两种具体的算法,以备不时之需。

1.普里姆算法

普里姆算法其实就是求顶点两两之间的最短距离。具体的流程如下,先将数据分为A、B两类。A代表已经确定的顶点集合,B代表未确定的顶点集合。初始状态A为空,B中包含所有的顶点。先随机选择一个顶点加入到A中,并从B中删除该顶点。然后选择B中剩下顶点到A中的顶点最近的,加入到A中,从B中删除该顶点。不断的迭代上面的过程,直到B为空。
该算法的时间复杂度为O(N^2)

2.克鲁斯卡尔算法(Kruskal算法)

上面讲述的普里姆算法更适合稠密的连通网,而接下来的克鲁斯卡尔算法则更适合稀疏的连通网,我们定义稠密和稀疏为:e<nlog(n),其中e表示边的数量,n表示顶点的数量。算法的时间复杂度为elog(e)
具体实现思路:首先将所有的边按照权值从小到大进行排序,然后对边进行遍历,如果这条边不会使树形成回路,则保留,否则删除。直到边的数量达到n-1(我们上面说过,连接n个顶点需要n-1条边)。总结就是选择权值最小并且不构成回路的n-1条边来生成树。
那么如何判断回路呢?我们初始给每个顶点赋予不同的状态值,凡是加入到树中的顶点状态值修改为一致的。这样在选择下一条边的时候,只需判断边两端的顶点状态是否一致,一致的话说明已经在树中(会形成回路),就丢弃,否则的话保留。

5.总结

本篇这种我们先介绍了图的基本概念,有向图、无向图、连通图、回环等。然后介绍了存储图的4种方式邻接矩阵是一种顺序表和邻接表对于有向图和无向图都适用;十字链表适和于存储有向图,多重邻接表适合存储无向图。我们还介绍了深度优先算法和广度优先算法,类似于前序遍历和层次遍历。深度优先算法沿子节点一直往下遍历,广度优先算法沿兄弟节点层遍历。最后介绍了如何使用深度优先和广度优先将无向图转换为一棵树。

6.Play With Fire

有一个人活到了84岁,突然想起了年轻时候做的一件羞耻的事情,后悔不已,并且因此死掉了。别人看到的时候只是说,这个人没有任何的疾病,是寿终正寝。没有人愿意去深究这件事情,人已经死了,对死者最大的善良就是让他的秘密和他一起死掉。可是万一那些死掉的人并不是真正的死了,它们只是升到了高维,我们如此保守他们的秘密是会令他们欣慰的吗?可是我自己的秘密呢?一个人的时候,我有时候也会做一些羞耻的事情。这些都被那些死了的人当做电影一样的欣赏吗?反正他们也不会告诉别人,我也就不当回事了吧。我并不会愚蠢到把自己的羞耻告诉别人,但我觉得对自己最大的善良就是,不论我在大众面前伪装的多么高尚,自己心里要明白自己只是千万卑劣的人中的一个而已。

【漫威/DC/踩点/1080p】前方高能!使人起鸡皮疙瘩的踩点与衔接的视觉盛宴!

    原文作者:杨天超
    原文地址: https://blog.csdn.net/gaobing1993/article/details/108923320
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系管理员进行删除。