图的常见算法实现(汇总)

前言

本来是想用C语言好好写的,可是指针和结构体太烦人了,弄得我头凉。因此决定用python实现一下图的一些算法。

远程仓库地址:

https://github.com/XiaoZhong233/DataStructure_Python/tree/master/graph

图的存储结构实现

图的实现有邻接矩阵,邻接表,十字链表等。我后面的算法主要用邻接表

建议直接看

[邻接表实现2,基于字典实现](# 邻接表实现2 (基于字典实现,好用))

首先定义了一个异常类:

class GraphError(Exception):
    pass

邻接矩阵实现

基于邻接矩阵定义了一个实现图的类,其中矩阵元素可以是1或者其他权值,表示有边,或者用一个特殊值表示“无关联”。构造参数的unconn就是表示无关联的值,默认为0。

图的构造函数的主要参数是mat,表示初始的邻接矩阵。要求是一个二维数组,且为方阵。代码如下

# unconn 无关联参数
# 邻接矩阵实现
class Graph:
    def __init__(self, mat, unconn=0):
        vnum = len(mat)
        # 检查是否为方阵
        for x in mat:
            if len(x) != vnum:
                raise ValueError("参数错误:不为方阵")
        # 拷贝数据
        self._mat = [mat[i][:] for i in range(vnum)]
        self._unconn = unconn
        self._vnum = vnum
	
	# 返回顶点数目
    def vertex_num(self):
        return self._vnum
	
	# 检查该顶点是否合法,也就是下标是否找得到
    def _invalid(self, v):
        return v < 0 or v >= self._vnum
	
	# 加入新的顶点
    def add_vertex(self):
        raise GraphError("邻接矩阵不支持加入顶点")
	
	# 加入新的边
    def add_edge(self, vi, vj, val=1):
        if self._invalid(vi) or self._invalid(vj):
            raise GraphError("顶点不合法")
        self._mat[vi][vj] = val
	
	# 获得某条边
    def get_edge(self, vi, vj):
        if self._invalid(vi) or self._invalid(vj):
            raise GraphError("顶点不合法")
        return self._mat[vi][vj]
	
	# 获得某个顶点的出边
    def out_edges(self, vi):
        if self._invalid(vi):
            raise GraphError("顶点不合法")
        return self.out_edge(self._mat[vi], self._unconn)
	
	# 获得某个顶点的出边
    @staticmethod
    def _out_edges(row, unconn):
        edges = []
        for i in range(len(row)):
            if row[i] != unconn:
                edges.append((i, row[i]))
        return edges

    def __str__(self):
        return "[\n" + ",\n".join(map(str, self._mat)) + "\n]" \
               + "\nUnconnected: " + str(self._unconn)

这个简单的邻接矩阵实现的图类并未支持增加顶点,因为邻接矩阵增加顶点要增加多一行一列,挺麻烦的,就不想写了。

邻接表实现1(基于邻接矩阵,不好用)

邻接矩阵的缺点是占用空间很多,如果是稀疏图就很难受了,可能会有很大的空间损失,因此常用邻接表实现图的存储。在上面邻接矩阵的实现下,可考虑一种“压缩后”的邻接表实现。

# 邻接表实现(压缩邻接矩阵形式)
class GraphAL(Graph):
    def __init__(self, mat=[], unconn=0):
        vnum = len(mat)
        for x in mat:
            if len(x) != vnum:
                raise ValueError("参数错误:不为方阵")
        self._mat = [Graph._out_edges(mat[i], unconn) for i in range(vnum)]
        self._vnum = vnum
        self._unconn = unconn

    def add_vertex(self):
        self._mat.append([])
        self._vnum += 1
        return self._vnum - 1

    def add_edge(self, vi, vj, val=1):
        if self._vnum == 0:
            raise GraphError("无法为空图增加边")
        if self._invalid(vi) or self._invalid(vj):
            raise GraphError("顶点不合法")

        row = self._mat[vi]
        i = 0
        while i < len(row):
            if row[i][0] == vj:
                self._mat[vi][i] = (vj, val)
                return
            if row[i][0] > vj:  # 没有到与vj的边,退出循环加入边
                break
            i += 1
        self._mat[vi].insert(i, (vj, val))

    def get_edge(self, vi, vj):
        if self._invalid(vi) or self._invalid(vj):
            raise GraphError("顶点不合法")
        for i, val in self._mat[vi]:
            if i == vj:
                return val
        return self._unconn

    def out_edges(self, vi):
        if self._invalid(vi):
            raise GraphError("顶点不合法")
        return self._mat[vi]

邻接表实现2 (基于字典实现,后面的算法都以此作为存储结构)

新建一个GraphAL.py文件,在文件中添加:

# 邻接表实现无向网(图)(字典形式)
class GraphAL:
    def __init__(self, graph={}):
        self._graph = graph
        self._vnum = len(graph)

    def _invalid(self, vertex):
        return self._graph.__contains__(vertex)

    def add_vertex(self, vertex):
        if self._invalid(vertex):
            raise GraphError("添加顶点失败,已经有该顶点")
        self._graph[vertex] = {}
        self._vnum += 1

    def add_edge(self, vi, vj, val):
        if not self._invalid(vi) or not self._invalid(vj):
            raise GraphError("不存在" + vi + "或者" + vj + "这样的顶点")
        self._graph[vi].update({vj: val})
        self._graph[vj].update({vi: val})

    def get_edge(self, vi, vj):
        if not self._invalid(vi) or not self._invalid(vj):
            raise GraphError("不存在" + vi + "或者" + vj + "这样的顶点")
        return self._graph[vi][vj]

    def get_vertexNum(self):
        return self._graph.__len__()

    # 在无向网(图)中是边,有向网(图)是出边,取决于数据
    def out_edge(self, vertex):
        if not self._invalid(vertex):
            raise GraphError("不存在" + vertex + "这样的顶点")
        return self._graph[vertex]
    
    

你也可以不传入图的参数,会默认创建一个新图。通过add_vertexadd_edge即可完成图的构建。

数据格式如下所示:

graph = {
    "A": {"B": 5, "C": 1},
    "B": {"A": 5, "C": 2, "D": 1},
    "C": {"A": 1, "B": 2, "D": 4, "E": 8},
    "D": {"B": 1, "C": 4, "E": 3, "F": 6},
    "E": {"C": 8, "D": 3},
    "F": {"D": 6},
}

20190825160305
如上图所示,该类是一个无向网,如果需要改成有向网,只需要更改add_edge这个方法,修改为:

    def add_edge(self, vi, vj, val):
        if not self._invalid(vi) or not self._invalid(vj):
            raise GraphError("不存在" + vi + "或者" + vj + "这样的顶点")
        self._graph[vi].update({vj: val})

图的一些算法实现

图的遍历

BFS(广度优先搜索)

算法原理及步骤

按照广度优先原则遍历图,利用了队列,有点像树的层次遍历。广度优先遍历的结果不唯一。整个遍历过程大概是这样的:给定一个起始顶点,将该起始顶点入队

  1. 顶点出队,如果当前顶点未被标记访问,则访问该顶点,然后标记为已访问,如果当前顶点已访问则直接丢弃该顶点
  2. 当前访问顶点的邻接顶点入队
  3. 当队列不为空的时候,循环1,2步
算法流程
未被访问
已经被访问
不为空
起始点入队
出队
未被访问?
访问该结点并输出
检查队空
结束
Created with Raphaël 2.2.0 初始化队列,初始顶点入队 出队 是否访问过该顶点? 跳过该顶点的访问 是否队空 结束BFS 访问 yes no yes no
算法实现
    # 广度优先遍历
    def bfs(self, start):
        if not self._invalid(start):
            raise GraphError("不存在" + start + "这样的顶点")
        queue = [start]  # 队列实现BFS
        seen = set(start)  # 记录访问过的顶点
        parent = {start: None}  # Node代表根节点,数组形式保存树
        result = []
        while queue.__len__() > 0:  # 队非空时
            vertex = queue.pop(0)  # 队首顶点出队
            nodes = self._graph[vertex]  # 获得其邻接顶点
            for node in nodes:
                if node not in seen:
                    queue.append(node)  # 其邻接顶点如果没有被访问,则入队,并且保留父顶点
                    seen.add(node)
                    parent[node] = vertex
            result.append(vertex)
        return result, parent
测试

例如遍历下图

20190825160305

具体的存储结构为:

    data = {
        "A": {"B": 5, "C": 1},
        "B": {"A": 5, "C": 2, "D": 1},
        "C": {"A": 1, "B": 2, "D": 4, "E": 8},
        "D": {"B": 1, "C": 4, "E": 3, "F": 6},
        "E": {"C": 8, "D": 3},
        "F": {"D": 6},
    }
    def test_bfs(self):
        print("bfs测试:")
        bfs, bfsparent = TestGraph.g.bfs("A")
        print("BFS:" + graph.GraphAL.printPath(bfs))
        print("BFS生成路径:" + bfsparent.__str__())
        print("BFS生成路径打印:" + graph.GraphAL.printTreePath(bfsparent).__str__())
        pass

20190829183358

DFS(深度优先搜索)

算法原理及步骤

DFS和BFS很像,不过DFS是深度优先的原则,具体实现是栈。

DFS遍历的结果不唯一。整个遍历过程大概是这样的:给定一个起始顶点,将该起始顶点入栈

  1. 顶点出栈,如果当前顶点未被标记访问,则访问该顶点,然后标记为已访问,如果当前顶点已访问则直接丢弃该顶点
  2. 当前访问顶点的邻接顶点入栈
  3. 当栈不为空的时候,循环1,2步
算法流程
未被访问
已经被访问
不为空
起始点入栈
出栈
未被访问?
访问该结点并输出
检查栈空
结束
Created with Raphaël 2.2.0 初始化栈,初始顶点进栈 出栈 是否访问过该顶点? 跳过该顶点的访问 是否栈空 结束DFS 访问 yes no yes no
算法实现
    # 深度优先遍历
    def dfs(self, start):
        if not self._invalid(start):
            raise GraphError("不存在" + start + "这样的顶点")
        stack = [start]  # 栈实现DFS
        seen = set(start)  # 记录访问过的顶点
        parent = {start: None}  # Node代表根节点,数组形式保存树
        result = []
        while stack.__len__() > 0:  # 栈非空时
            vertex = stack.pop()  # 顶点出栈
            nodes = self._graph[vertex]  # 获取出栈顶点的邻接顶点
            for node in nodes:
                if node not in seen:
                    stack.append(node)
                    seen.add(node)
                    parent[node] = vertex
            result.append(vertex)
        return result, parent
测试

例如遍历下图

20190825160305

存储结构
    data = {
        "A": {"B": 5, "C": 1},
        "B": {"A": 5, "C": 2, "D": 1},
        "C": {"A": 1, "B": 2, "D": 4, "E": 8},
        "D": {"B": 1, "C": 4, "E": 3, "F": 6},
        "E": {"C": 8, "D": 3},
        "F": {"D": 6},
    }
测试结果
图的结构为:
('A', {'B': 5, 'C': 1})
('B', {'A': 5, 'C': 2, 'D': 1})
('C', {'A': 1, 'B': 2, 'D': 4, 'E': 8})
('D', {'B': 1, 'C': 4, 'E': 3, 'F': 6})
('E', {'C': 8, 'D': 3})
('F', {'D': 6})

dfs测试:
DFS:A->C->E->D->F->B
DFS生成路径:{'A': None, 'B': 'A', 'C': 'A', 'D': 'C', 'E': 'C', 'F': 'D'}
DFS生成路径打印:
A->B
A->C
A->C->D
A->C->E
A->C->D->F

最小生成树

最小生成树针对的是连通网而言的。假定一个网络G,他的边带有权值,自然可以通过BFS,DFS获得他的生成树,权值最小的那棵树,就称最小生成树

最小生成树有许多实际的应用,例如通信网,输电网及各种网的规划。

Prim算法

算法原理及算法流程
原理:

根据(MST性质:网络G必有一颗最小生成树),具体证明不再赘述,大概思想就是假设你现有一个图的集合G,从G中的一个顶点出发,不断的选择最短的一条连接边,扩充到已选边集N中,直至N包含了图G中的所有顶点。

构造过程举例

假设现在有这样一颗图

6
1
5
5
5
6
4
V1
V2
V3
V4
V5
V6

要对该图进行prim算法进行最小生成树。首先找一个开始顶点,假设从V1开始

第一次构造

V1的邻接节点全部入队。并且由于该队列是优先级队列,会按照权重排序

v1,v3,1
v1,v4,5
v1,v2,6

队首出队,构造边,将该边加入到N集

此时N集中就有了一条边了

1
v1
V3

V3除了N集中的结点的邻接节点入队,优先队列会按照权重进行排序

v3,v2,5
v3,v4,5
v3,v5,6
v3,v6,4
v1,v4,5
v1,v2,6
第二次构造

队首出队,构造边,将该边加入到N集

此时N集有两条边了

1
4
v1
V3
V6

V6的除N集中已有的邻接节点入队,

v6,v4,2
V6,V5,6
v3,v2,5
v3,v4,5
v3,v5,6
v1,v4,5
v1,v2,6
第三次构造

队首出队,构造边,将该边<6,4>加入到N集

此时的N集就有三条边了

1
4
2
v1
V3
V6
V4

V4的除N集中已有的邻接节点入队,发现v4的邻接顶点都在N集中了,所以没有顶点入队

V6,V5,6
v3,v2,5
v3,v4,5
v3,v5,6
v1,v4,5
v1,v2,6
第四次构造

队首<v1,v4>出队,构造边,将该边<v1,v4>加入到N集,注意此时由于<v1,v4>加入N集中会构成连通,所以跳过本次构造边

1
4
2
5
v1
V3
V6
V4

所以,当前N集中的边还是和原来一样,如下图:

1
4
2
v1
V3
V6
V4

V4的除N集中已有的邻接节点入队,发现v4的邻接顶点都在N集中了,所以没有顶点入队

V6,V5,6
v3,v2,5
v3,v4,5
v3,v5,6
v1,v2,6
第五次构造

队首<v3,v2>出队,构造边,将该边加入到N集中。

此时N集就有五条边了

1
4
2
5
v1
V3
V6
V4
V2

V2的除N集中已有的邻接节点入队

V6,V5,6
v3,v2,5
v3,v4,5
v3,v5,6
v1,v2,6
V2,V5,3
第六次构造

队首<v2,v5>出队,构造边,加入边到N集中。此时N集中有5条边。由于总共就6个顶点,当构成最小生成树的时候边只能是5条,你如果在加一条边就连通了,所以prim构造生成树结束

1
4
2
5
3
v1
V3
V6
V4
V2
V5
最终结果
1
4
2
5
3
v1
V3
V6
V4
V2
V5
算法实现
   # prim算法,最小生成树,前提该图必须是连通网
    def prim(self, start):
        if not self._invalid(start):
            raise GraphError("不存在" + start + "这样的顶点")
        result = GraphAL({})
        edgeCount = 0
        pqueue = []  # 优先级队列,候选列表
        # 初始化优先级队列
        for node in self._graph[start]:
            heapq.heappush(pqueue, (self._graph[start][node], start, node))
            pass
        while edgeCount < self.get_vertexNum() - 1 and not pqueue.__len__() == 0:
            # 出队
            pair = heapq.heappop(pqueue)
            distance = pair[0]
            start = pair[1]
            end = pair[2]
            # 判断是否有该顶点,如果没有就要加入
            if start not in result._graph:
                result.add_vertex(start)
            if end not in result._graph:
                result.add_vertex(end)
            # 如果当前点与下一节点未建立边,则尝试建立边
            # 方式是检查下一节点是否在result中,如果有则说明这个节点已经建立过边了,再建立边的话会可能会形成连通,因此直接舍弃该边的建立
            if end not in result._graph[start]:
                # 如果下一个节点如果未被其他节点连接则result._graph[end]返回false,开始构造边,
                # 如果下一个节点已经被连接了,则result._graph[end]返回true,舍弃该边的建立
                if not result._graph[end]:
                    result.add_edge(start, end, distance)
                    edgeCount += 1
                    pass
            start = end
            # 子节点入队
            for node in self._graph[start]:
                if node not in result._graph:
                    heapq.heappush(pqueue, (self._graph[start][node], start, node))
            pass
        return result
测试

20190830224239

与刚才流程构造的结果一致

1
4
2
5
3
v1
V3
V6
V4
V2
V5

克鲁斯卡尔算法

算法原理及流程
原理

在一个连通图中不断选取权值最小的边,然后连起来,就是这样。

假设给定图G,结果图T

基本步骤如下:

  1. 将G中的所有边按权值递增的顺序进行排序
  2. 选择权值最短的边且边的两端点属于不同连通分量(如果两端点属于同一个连通分量中,那么就说明该子图是连通图!所以不行),然后该边与T中已选择的边进行连接,每次边连接都会使得T的连通分量减1
  3. 当边数小于顶点数时,不断重复1,2

如果当做完上面这些步骤后,得出的结果T中不能包含G中的所有顶点,则说明原图G是不连通的(也就是不是任意一个节点到另一个节点都走的通)

这里有两个难点:

  1. 最短边的选取

    思路①:采用优先队列,在python中可以通过优先级队列实现,其他语言像C++,Java中也有类似实现的数据结构。

    思路②:不断的扫描候选边列表,然后进行排序。这种方法就比较麻烦了,写的代码比较多,不过也很灵活,具体排序方式你可以选择。

  2. 如何判断边的两个端点的连通分量

    思路①:不断的检查两个端点之间是否有路径,有路径就说明在同一个子图中,连通分量相同。不过这样也太麻烦了点还浪费计算时间

    思路②:前人提出的一种方法,为每个连通分量确定一个代表元,如果两个顶点的代表元相同,则表示他们连通成环。例如下图

    v1
    V2
    V3
    V4

    当初始化的时候v1,v2,v3,v4的代表元就是他们的序号也就对应0,1,2,3

    v1 v2 构成新边的时候,就要把v2的代表元改为v1 的代表元0

    这时候v1,v2,v3,v4的代表元就更新为0,0,2,3

    v1
    V2
    V3
    V4

    v1 v2是的连通分量相同并且他们的代表元也是相同的。

    类似,如果想要连接v2 v3,此时v2 v3 的代表元不同,因此连接了也不会构成环。 直接把v3的代表元修改为v2的代表元即可,即0

    v1
    V2
    V3
    V4

    此时v1 v2 v3是连通的,他们的代表元是0,0,0

    如果下一次操作中,想要把v3 连接到v1 ,检查他们的代表元,都是0所以连接起来一定会构成环

    v1
    V2
    V3
    V4

    因此,可以使用代表元判断欲加入的边是否会与已选择集合T中的边构成环路。

构成过程举例

假设还是之前的这颗图G,其结果集T中目前还为空

6
1
5
5
5
6
4
V1
V2
V3
V4
V5
V6
初始化

全部边入队,自动在优先队列中根据权值排好序

v4,v6,2
V5,V6,6
v2,v3,5
v3,v4,5
v3,v5,6
v3,v6,4
v2,v5,3
v1,v4,5
v1,v2,6
v1,v3,1

并且初始化代表元列表,初始值为他们的下标。

例如v1的代表元初始值为1,v2的代表元初始值为2…vn的代表元初始值为n

v1,1
v2,2
v3,3
v4,4
v5,5
v6,6
第一次构造
  1. 队首出队,所以<v1,v3>边出队
v6,v4,2
V6,V5,6
v3,v2,5
v3,v4,5
v3,v5,6
v3,v6,4
v2,v5,3
v1,v4,5
v1,v2,6
v1,v3,1
  1. 检查v1 v3的代表元,很明显不同,所以将<v1,v3>加入T集合中
1
v1
V3
  1. 合并代表元,修改等于代表元值为3的代表元的值,改为v1的代表元即1
v1,1
v2,2
v3,1
v4,4
v5,5
v6,6
第二次构造
  1. 队首出队,所以<v4,v6>边出队
v4,v6,2
V5,V6,6
v2,v3,5
v3,v4,5
v3,v5,6
v3,v6,4
v2,v5,3
v1,v4,5
v1,v2,6
  1. 检查v4 v6的代表元,很明显不同,所以将<v4,v6>加入T集合中
1
2
v1
V3
V6
V4
  1. 合并代表元,修改等于代表元值为6的代表元的值,改为v4的代表元的值即4
v1,1
v2,2
v3,1
v4,4
v5,5
v6,4
第三次构造
  1. 队首出队,所以<v2,v5>边出队
V5,V6,6
v3,v2,5
v3,v4,5
v3,v5,6
v3,v6,4
v2,v5,3
v1,v4,5
v1,v2,6
  1. 检查v2 v5的代表元,很明显不同,所以将<v2,v5>加入T集合中
1
2
3
v1
V3
V6
V4
V2
V5
  1. 合并代表元,修改等于代表元值为5的代表元的值,改为v2的代表元的值即2
v1,1
v2,2
v3,1
v4,4
v5,2
v6,4
第三次构造
  1. 队首<v3,v6>出队
v5,V6,6
v3,v2,5
v3,v4,5
v3,v5,6
v3,v6,4
v1,v4,5
v1,v2,6
  1. 检查v3 v6的代表元,不同,所以将<v3,v6>加入T集合中
1
2
3
4
v1
V3
V6
V4
V2
V5
  1. 合并代表元,修改等于代表元值为4的代表元的值,改为v3代表元的值即1
v1,1
v2,2
v3,1
v4,1
v5,2
v6,1
第四次构造
  1. 队首<v1,v4>出队
V5,V6,6
v3,v2,5
v3,v4,5
v3,v5,6
v1,v4,5
v1,v2,6
  1. 检查v1 v4的代表元,相同,所以不将<v1,v4>加入T集合中
1
2
3
4
v1
V3
V6
V4
V2
V5
  1. 此时代表元不进行任何操作
v1,1
v2,2
v3,1
v4,1
v5,2
v6,1
第五次构造
  1. 队首<v3,v4>出队
V5,V6,6
v3,v2,5
v3,v4,5
v3,v5,6
v1,v2,6
  1. 检查v3 v4的代表元,相同,所以不将<v3,v4>加入T集合中
1
2
3
4
v1
V3
V6
V4
V2
V5
  1. 此时代表元不进行任何操作
v1,1
v2,2
v3,1
v4,1
v5,2
v6,1
第六次构造
  1. <v2,v3>出队
V5,V6,6
v3,v2,5
v3,v5,6
v1,v2,6
  1. 检查v2 v3的代表元,不同,所以将<v2,v3>加入T集合中
1
2
3
4
5
v1
V3
V6
V4
V2
V5
  1. 合并代表元,修改等于代表元为2的代表元的值,改为v3代表元的值即1
v1,1
v2,1
v3,1
v4,1
v5,1
v6,1

可以发现,当前T集合中已经有5条边了,最小生成树已经生成完毕。同时观察到,代表元中的值也相同了,表示他们都在同一个子图中了。

算法实现
    # kruskal算法,最小生成树,前提该图必须是连通网
    def kruskal(self):
        # 初始化代表元和结果图
        result, reps, pqueue, edgesCount = GraphAL(graph={}), {}, [], 0
        for key in self._graph.keys():
            reps[key] = key
        # 边入队,按优先级排序,选出最短边
        for key in self._graph:
            for end in self._graph[key].keys():
                edges = self._graph[key][end]
                heapq.heappush(pqueue, (edges, key, end))  # 边入队
            pass
        # 当边数达到n-1条时,即成功得到最小生成树时停止
        while edgesCount < self.get_vertexNum() - 1 and not pqueue.__len__() == 0:
            # 出队
            pair = list(heapq.heappop(pqueue))
            # 判断是否有该顶点,如果没有就要加入
            if pair[1] not in result._graph:
                result.add_vertex(pair[1])
            if pair[2] not in result._graph:
                result.add_vertex(pair[2])
            # 检查两点是否属于不同连通分量
            if reps[pair[1]] != reps[pair[2]]:
                result.add_edge(pair[1], pair[2], pair[0])
                edgesCount += 1
                # 合并连通分量
                rep, orep = reps[pair[1]], reps[pair[2]]
                for key in reps.keys():
                    if reps[key] == orep:
                        reps[key] = rep
        return result
        pass
测试

20190831002634

与刚才结果自动推的完全一致。

1
2
3
4
5
v1
V3
V6
V4
V2
V5

最短路径

dijkstra算法

算法原理

在看迪杰斯特拉算法之前,可以先回顾下BFS算法的过程。BFS的实现是通过一个队列实现。还是这张图

20190825160305

选择假设BFS从A节点开始,A节点出队后,将A的邻接节点B,C入队

20190831190937

然后B出队,D入队,C出队,E入队。整个BFS的流程大概如此,在这之中,可以看到BFS队列中不同节点离A的距离,每个出队的结点对于他的邻接节点的距离都是1,并且在队列中他们也是紧紧挨着的。

假如可以把这些顶点进行排序,然后不断更新队中节点到A的距离值,那么应该可以一步步的获得当前节点到A节点的最短距离了。

该算法有两个难点:

  1. 如何排序

我使用的是python的优先队列,该队列是基于堆这一种数据结构实现的,你也可以自行选择排序算法进行排序

  1. 如何更新距离

在BFS中每个节点到A的距离是固定的,是不会发生更新操作的,这是由于BFS算法实现过程中有个访问标志会标志某个节点是否已被访问,该标志保证了每个节点只访问一次。但是在迪杰斯特拉算法中,这样是不行的,因为想要在每个节点出队后,都要将该结点的邻接节点到目标点(这个例子中是A点)的距离进行比较更新,选择权值和小的。

看下面这个网

20190831193520

当遍历到B的时候A到B有两条路,一条是A-B,另一条是A-C-B,前者的距离为5,比较长,后者的距离为3(1+2)。在迪杰斯特拉中就会选择路径A-C-B这条路径。

在实际的算法实现中,距离的比较是通过一个distance的列表实现的,该列表距离了每个顶点到目标点的最短距离。然后在下一次遍历中不断得更新这个距离就可以了。

构造过程举例

假设还是上面这个图。

20190831193520

要求图中顶点到A的最短距离

初始化

初始化距离列表,inf 表示无穷,A的目标点,所以距离为0

A,0
B,inf
C,inf
D,inf
E,inf
F,inf

初始化优先级队列,目标节点A入队

A,0

当队列不为空时,循环。

第一次构造
  1. A出队,并标记为已访问,遍历A的邻接节点B、C,同时将A到B的距离5(0+5)和A到C的距离1(0+1),与distance列表中的距离进行比较,由于distance中的距离都是无穷,所以distance中C的距离更新为1,B的距离更新为5
A,0
B,5
C,1
D,inf
E,inf
F,inf
  1. B,C节点由于距离被更新了。需要参与下一次比较,所以B、C入队
B,5
C,1

第二次构造

  1. C出队,并标记为已访问,遍历C的邻接节点A,B,D,E,将C-A的距离1,C-B的距离3(1+2)和C-D的距离5(1+4)和C-E的距离9(1+8),与distance列表中的B,C,E的距离进行比较,更新为其中的较小值
A,0
B,3
C,1
D,5
E,9
F,inf
  1. 由于B,D,E的距离被更新了。需要参与下一次的比较,所以B,C,E需要入队,带着他们的更新后的权值
B,5
B,3
D,5
E,9
第三次构造
  1. B出队,并标记为已访问,遍历B的子节点D,C,A,将B-D(3+1=4),B-C(3+2=5),B-A(3+5)的距离分别与distance中的D,C,A距离进行比较,取小的值,发现只有D的距离被更新为了4
A,0
B,3
C,1
D,4
E,9
F,inf
  1. 由于D被更新了,需要参与下一次的比较,所以D入队,带着D更新后的权值
D,4
B,5
D,5
E,9
第四次构造
  1. D出队,并标记为已访问,遍历D的子节点B,C,E,F。将D-B(4+1=5),D-C(4+4=8),D-E(4+3=7),D-F(4+6=10)的距离分别与distance中的B,C,E,F距离进行比较,取小的值
A,0
B,3
C,1
D,4
E,7
F,10
  1. 由于只有E,F的距离被更新为7,和10,所以E,F需要带着他们更新后的权值入队,参与下一次的比较。
B,5
D,5
E,7
E,9
F,10
第五次构造
  1. 队首B出队,由于B被标记已访问,所以直接扔掉,进入下一个循环
D,5
E,7
E,9
F,10
  1. 队首D出队,由于D已经被标记已访问,扔掉。进入下一个循环
E,7
E,9
F,10
  1. 队首E出队,标记为已访问。遍历其邻接节点C,D,将E-C(7+4=11),E-D(7+3=10)与distance中的C,D值进行比较,取小的值,发现C,D都不需要更新。

  2. 由于没有节点被更新,所以没有节点入队。此时的distance如下图

A,0
B,3
C,1
D,4
E,7
F,10
第六次构造
  1. 队首E出队,由于E被标记为已访问,扔掉,进入下一个循环
E,9
F,10
  1. 队首E出队,由于E被标记为已访问,扔掉,进入下一个循环
F,10
  1. 队首F出队,发现他没有子节点,所以distance不会被更新,队列将不会加入新的结点。此时的distance如下图
A,0
B,3
C,1
D,4
E,7
F,10
第七次构造

由于此时队列为空,所以循环结束,迪杰斯特拉算法求解完毕!此时的distance就是每个节点到目标点A的最短距离了。

A,0
B,3
C,1
D,4
E,7
F,10

迪杰斯特拉算法就是基于这种"宽度优先遍历"的思想,按路径的长度选择下一个最短节点然后逐步扩张(这一点也很像用MST性质实现的prim算法)。这个算法在探索中也会更新已经节点的最短路径,每一步都可以找到一个确定的最短路径,这就是典型的动态规划思想(在计算中保留一些信息,用来支持下一步的决策信息)

算法实现
    # 迪杰斯特拉法算最短路径
    def dijkstra(self, start):
        if not self._invalid(start):
            raise GraphError("不存在" + start + "这样的顶点")
        graph = self._graph
        pqueue = []  # 优先级队列
        heapq.heappush(pqueue, (0, start))  # 根顶点进队,最高优先级
        seen = set()  # 记录访问过的顶点
        parent = {start: None}  # 生成树
        distance = self.__init_distance(start)  # 初始化距离
        while pqueue.__len__() > 0:
            pair = heapq.heappop(pqueue)  # pop弹出的是元组,第一个参数是距离(优先级),第二个是顶点
            dist = pair[0]
            vertex = pair[1]
            seen.add(vertex)  # 记录访问过的顶点
            nodes = graph[vertex].keys()  # 获取其顶点的邻接顶点
            for node in nodes:
                if node not in seen:
                    if dist + graph[vertex][node] < distance[node]:  # 如果当前顶点到开始顶点的距离小于距离列表中的值,更新距离
                        heapq.heappush(pqueue, (dist + graph[vertex][node], node))
                        parent[node] = vertex
                        distance[node] = dist + graph[vertex][node]
            # 输出遍历结果
            # print(vertex)
        return distance, parent
        pass
测试

20190831205632

可以发现,如刚才推导的结果一模一样。

A,0
B,3
C,1
D,4
E,7
F,10

弗洛依德算法(待填坑)

算法原理
算法实现
测试

拓扑排序

算法原理

拓扑排序是有向图(网)中的内容,只在有向网(图)的范畴中讨论。

先看一个实际生活中可能遇到的问题:选课问题,例如上大一的时候你肯定要先学C语言,然后才能学数据结构。这个时候C语言和数据结构就构成了一个排列问题,谁在前谁在后。用图中的顶点表示一个活动,边表示活动之间的顺序关系。这样的图就称为AOV网(顶点活动网)

下图就是一个典型的AOV网实例。

20190831210501

任何无回路的AOV网N都可以求解出拓扑序列,方法很简单:

  • 从N中选出一个入度为0的顶点作为序列的下一个顶点
  • 从N网中删除所选顶点的出边
  • 重复执行上面两步,直到已经选出了图N的所有顶点

拓扑排序算法有两个难点:

  1. 如何寻找入度为0的顶点
  2. 真的需要拷贝整张图,然后进行删除

一个显然的办法是不断遍历图,寻找入度为0的顶点。但时间代价会很高。顶点间的制约关系决定了顶点的入度。入度是一个整数,用一个整数表就能记录所以顶点的入度了。因此,我的方法是用一张入度表记录了每个顶点的入度,初始时,表中的各顶点的入度对应为图中顶点的入度,在随后的计算中,一旦选中一个顶点,就将该顶点的出边入度减一。

在实际的算法实现中还用了一个0度栈来记录已经入度为0但还未处理的顶点。

算法比较简单。

可以慢慢调试

20190831212236

算法实现

    def topological_sort(self):
        indegree = {}  # 入度表
        zerov = []  # 利用0度栈记录已知的入度为0的但还未处理的顶点
        m = 0  # 输出顶点计数
        topo = []  # 拓扑排序结果
        # 生成入度表和0度栈
        for vetx in self._graph:
            indegree[vetx] = self.get_inEdge(vetx).__len__()
            if indegree[vetx] == 0:
                zerov.append(vetx)
            pass

        while zerov.__len__() != 0:
            Vi = zerov.pop()
            topo.append(Vi)
            m += 1
            for Vj in self.get_outEdge(Vi).keys():  # 对顶点Vi的每个邻接点入度减1,如果Vj的入度变为0,则将Vj入栈,表示Vj就是下一个需要处理的顶点
                indegree[Vj] -= 1
                if indegree[Vj] == 0:
                    zerov.append(Vj)

        if m < self.get_vertexNum():  # 该有向图有回路
            return False
        return topo

测试

1567257835949

关键路径

算法原理

算法实现

    # 关键路径
    def criticalPath(self, delay=0):
        topo = self.topological_sort()
        if not topo:
            raise GraphError("存在有向环!")
        ve = [0 for i in range(len(topo))]  # 事件最早开始时间
        vl = [0 for i in range(len(topo))]  # 事件最迟开始时间
        cp = []  # 关键路径
        result = {}  # 返回结果
        # --------------------------------计算事件的最早发生时间-----------------------------
        for i in range(topo.__len__()):
            start = topo[i]  # 取出拓扑节点
            for node in self.get_outEdge(start).keys():  # 获取拓扑节点的邻接点,计算ve
                w = self._graph[start][node]  # 当前节点与邻接节点的边
                j = topo.index(node)  # 邻接节点的下标
                if ve[j] < ve[i] + w:  # 更新邻接点的最早发生时间,选大的时间
                    ve[j] = ve[i] + w
                pass
        # --------------------------------计算事件的最晚发生时间-----------------------------
        for i in range(topo.__len__()):  # 给每个事件的最迟发生时间置初值,初值为最早发生时间中的最大值
            vl[i] = ve[topo.__len__() - 1] + delay
        for i in reversed(range(topo.__len__())):
            k = topo[i]  # 取出拓扑节点
            for node in self.get_inEdge(k).keys():  # 获取拓扑节点的逆邻接点,计算vl
                w = self._graph[node][k]  # 逆邻接点和当前节点的边
                j = topo.index(node)  # 逆邻接点的下标
                if vl[j] > vl[i] - w:  # 更新逆邻接点的最晚发生时间,选小的时间
                    vl[j] = vl[i] - w
                pass
        # --------------------------------判断每一活动是否为关键路径--------------------------
        for i in range(topo.__len__()):
            start = topo[i]
            for node in self.get_outEdge(start).keys():
                j = topo.index(node)  # 获得邻接顶点的下标
                w = self._graph[start][node]  # 当前节点与邻接节点的边
                e = ve[i]  # 计算活动<start,node>的最早开始时间
                l = vl[j] - w - delay  # 计算活动<start,node>的最晚开始时间
                if e == l:
                    cp.append((start, node))  # 如果相等就说明为关键路径
                pass

        for i in range(topo.__len__()):
            result[topo[i]] = (ve[i], vl[i])
            pass
        return result, cp

测试

20190831212504

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: 常见算法可以通过JS来实现,下面是一些常见算法汇总: 1. 排序算法:JS中常见的排序算法有冒泡排序、选择排序、插入排序、快速排序、归并排序等。这些算法可以根据数组中的元素进行排序,最终得到有序的数组。 2. 查找算法:在JS中,常见的查找算法有线性查找和二分查找。线性查找从数组的第一个元素开始逐个比较,直到找到目标元素或遍历完整个数组。二分查找则是在有序数组中进行的,通过不断缩小查找范围来找到目标元素。 3. 算法:JS中可以使用邻接矩阵或邻接表来表示的结构,并实现算法常见算法包括广度优先搜索(BFS)和深度优先搜索(DFS),用于遍历的所有节点,以及最短路径算法(如Dijkstra算法)和最小生成树算法(如Prim和Kruskal算法)等。 4. 字符串算法:JS中的字符串算法包括字符串匹配算法和字符串处理算法常见的字符串匹配算法有KMP算法和Boyer-Moore算法,用于在字符串中寻找指定的模式。字符串处理算法包括字符串的反转、拼接、替换等操作。 5. 动态规划算法:JS中可以通过递归或动态规划实现一些动态规划算法,如背包问题、最长公共子序列、最长递增子序列等。 6. 算法:JS中可以使用canvas或SVG等技术来实现算法,如几何变换、线段相交判定、Convex Hull等。 以上是一些常见算法在JS中的实现汇总,通过这些算法的掌握和实现,可以提高编程效率和解决实际问题的能力。 ### 回答2: 常见算法是js实现汇总可以包括以下几种算法: 1. 排序算法: - 冒泡排序:通过相邻元素的比较和交换来实现排序。 - 插入排序:将数组分为已排序和未排序两部分,每次从未排序中选择一个元素插入到已排序部分的正确位置。 - 选择排序:每次从未排序部分选择最小(或最大)的元素放到已排序部分的末尾。 - 快速排序:通过一次划分将数组分为两部分,并递归地对两部分进行排序。 2. 查找算法: - 顺序查找:逐个比较数组中的元素直到找到目标元素。 - 二分查找:将有序数组从中间划分,缩小查找范围,直到找到目标元素。 - 哈希查找:借助哈希表来实现高效的元素查找。 3. 算法: - 深度优先搜索(DFS):以深度优先的方式遍历的所有节点。 - 广度优先搜索(BFS):以广度优先的方式遍历的所有节点。 - 最短路径算法:例如Dijkstra算法、Floyd-Warshall算法等用于寻找两个节点之间的最短路径。 4. 动态规划算法: - 最长公共子序列(LCS):用于寻找序列中的最长公共子序列。 - 背包问题:在给定一组物品和一定容量的背包下,选择物品使得总价值最大。 5. 搜索算法: - 回溯法:采用试错的思想,在遇到不能继续前进的情况时回退并尝试其他可能。 - 分支限界法:通过剪枝策略减少搜索空间,同时利用优先队列实现高效搜索。 以上仅是常见算法的一部分,在JavaScript中可以通过函数和数据结构来实现这些算法,并应用于实际问题的解决。 ### 回答3: 常见算法在JavaScript中有不同的实现方式,以下是其中一些常见算法及其在JavaScript中的实现汇总: 1. 冒泡排序:通过多次比较和交换相邻元素的位置,将最大(或最小)值逐步“冒泡”到数组的一端。实现代码如下: ```javascript function bubbleSort(arr) { for (let i = 0; i < arr.length - 1; i++) { for (let j = 0; j < arr.length - 1 - i; j++) { if (arr[j] > arr[j + 1]) { [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]; } } } return arr; } ``` 2. 快速排序:通过选择一个基准元素,将数组分成两个子数组,在每次迭代中将小于基准的元素放在左边,大于基准的元素放在右边,然后对子数组递归地进行快速排序。实现代码如下: ```javascript function quickSort(arr) { if (arr.length <= 1) { return arr; } const pivotIndex = Math.floor(arr.length / 2); const pivot = arr.splice(pivotIndex, 1)[0]; const left = []; const right = []; for (let i = 0; i < arr.length; i++) { if (arr[i] < pivot) { left.push(arr[i]); } else { right.push(arr[i]); } } return [...quickSort(left), pivot, ...quickSort(right)]; } ``` 3. 二分查找:在有序数组中查找特定元素的算法,通过将数组分成两部分,判断目标元素位于左侧还是右侧,然后递归地对目标部分进行查找。实现代码如下: ```javascript function binarySearch(arr, target) { let low = 0; let high = arr.length - 1; while (low <= high) { const mid = Math.floor((low + high) / 2); if (arr[mid] === target) { return mid; } else if (arr[mid] < target) { low = mid + 1; } else { high = mid - 1; } } return -1; } ``` 以上只是常见算法的一小部分,JavaScript有许多其他的实现方式和算法,根据具体的需求和使用场景选择适合的算法能够提高代码的性能和效率。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值