1. 前言

因无向、无加权图的任意顶点之间的最短路径由顶点之间的边数决定,可以直接使用原始定义的广度优先搜索算法查找。

但是,无论是有向、还是无向,只要是加权图,最短路径长度的定义是:起点到终点之间所有路径中权重总和最小的那条路径。

如下图所示,A 到 C 的最短路径并不是 A 直接到 C(权重是 9),而是 A 到 B 再到 C(权重是 7)。所以,需要在广度优先搜索算法的基础上进行算法升级后才能查找到。

加权图的常用最短路径查找算法有:

  • 贝尔曼-福特(Bellman-Ford)算法
  • Dijkstra(迪杰斯特拉) 算法
  • A* 算法
  • D* 算法

2. 贝尔曼-福特(Bellman-Ford)算法

贝尔曼-福特算法取自于创始人理查德.贝尔曼莱斯特.福特,本文简称 BF 算法

BF 算法属于迭代、穷举算法,算法效率较低,如果图结构中顶点数量为 n,边数为 m ,则该算法的时间复杂度为 m*n ,还是挺大的。

理论上讲,图结构中边上的权重一般用来描述现实世界中的速度、时间、花费、里程……基本上都是非负数。即使是负数,BF 算法也能工作得较好。

2.1 BF 算法思想

问题:如下图,搜索 A 到其它任意顶点之间的最短路径。

首先给每一个顶点一个权重值(用来存储从起始顶点此顶点的最短路径上所有边上权重之和),刚开始除了出发点的权重 0 ,因为还不能确定到其它任意顶点的具体路径长度,其它顶点的权重值均初始为无穷大(只要是一个适当值都可以)。

下面的图结构是无向加权图,对于有向加权图同样适用 BF 算法。

BF 算法流程:

  • 更新顶点的权重: 计算任一条边上一端顶点(始点)到另一个端顶点(终点)的权重新权重=顶点(始点)权重+边的权重,然后使用新权重值更新终点的原来权重值

  • 更新原则: 只有当顶点原来的权重大于新权重时,才更新。

    如:先选择 A -> B 之间的路径,因为 A~B 是无向边,需要计算 2 次。如果是有向图,则只计算一个方向。

    先计算 A -> B 的新权重=A的权重+(AB)边上的权重,新权重=0+3=3。因 3 小于 B 顶点现在的权重(无穷大),B 的权重被更新为 3

    再计算 B -> A 的新权重=B的权重+(AB) 边上的权重。新权重=3+3=66 大于 A 现有的权重 0,则 A 顶点不更新。

    此时,意味着 A -> B 的最短路径长度为 3

    AB 的前序顶点。当然,这绝对不是最后的结论。

  • 对图中每一条边两端的顶点都执行上述同样的操作,对于执行顺序没有特定要求。

    如下继续计算 (A,C) 边的两端顶点的权重。

    A -> C 的新权重=0+5=5,更新 C 顶点权重为 5

    C -> A 的新权重=5+5=10 不更新。结论:AC 的前序顶点。

计算 (B,C) 权重:

B -> C 的新权重=3+1=4,小于 C 现有权重 5C 的权重更新为 4则 B 是 C的前序顶点

C -> B 的新权重= 4+1 =5 ,大于 B 现有权重,不更新。

经过上述操作后 (A,B,C)3 个顶点的前序关系:

AB 的前序、BC 的前序,当前 A -> B 的最短路径是 3A -> B -> C 的最短路径是 4,但不一定是最后结论。

Tips: 当顶点的权重更新后,也意味着前序顶点也发生了变化。如上述权重更新过程中 C 刚开始前序是 A,后来又更改成了 B

  • 当所有边两端顶点都更新完毕后,需要再重新开始,对图结构中所有边上的顶点权重再进行一次更新,一至到不再引起权重变化时 BF 算法才算结束。

  • BF 算法的本质还是广度优先搜索算法,附加了更新顶点权重的逻辑。

2.2 类结构设计:

本文的图结构存储使用链接表。

顶点类: 此类用来描述顶点本身信息,除了有顶点的常规属性,如编号、名称、链接表……外,还需要添加 2 个属性:

  • 顶点的权重:初始化时为无穷大。

    顶点权重用来保存起始点到此顶点的最短路径长度(边上权重之和)。

  • 前序顶点:BF 算法中,如果顶点的权重发生了更新,也意味着前序顶点也发生了变化。

初始化方法:

"""
节(顶)点类
"""
import sys
class Vertex:
def __init__(self, v_name, v_id=0):
# 顶点的编号
self.v_id = v_id
# 顶点的名称
self.v_name = v_name
# 是否被访问过:False 没有 True:有
self.visited = False
# 与此顶点相连接的其它顶点
self.connected_to = {}
# 前序顶点
self.preorder_vertex = None
# 权重(初始为无穷大)
self.weight = sys.maxsize

添加相邻顶点方法:

    '''
添加邻接顶点
nbr_ver:相邻顶点
weight:无向无权重图,权重默认设置为 1
'''
def add_neighbor(self, nbr_ver, weight=1):
# 字典中以相邻顶点为键,权重为值
self.connected_to[nbr_ver] = weight '''
判断给定的顶点是不是当前顶点的相邻顶点
'''
def is_neighbor(self, nbr_v):
return nbr_v in self.connected_to

顶点对象以字符串格式输出:

    '''
显示与当前顶点相邻的其它顶点
'''
def __str__(self):
return '权重{0}:名称{1}顶点相邻的顶点有:{2}'.format(self.weight, self.v_name,
str([(key.v_name, val) for key, val in self.connected_to.items()]))

顶点权重更新方法:

    '''
得到和某一个相邻顶点的权重
'''
def get_weight(self, nbr_v):
return self.connected_to[nbr_v] '''
计算顶点权重(路径长度)
'''
def cal_bf_weight(self, nbr_v):
# 顶点权重加上顶点间边的权重
new_weight = self.weight + self.get_weight(nbr_v)
if new_weight < nbr_v.weight:
# 计算出来权重小于顶点原来权重,则更新
nbr_v.weight = new_weight
# 设置前序顶点
nbr_v.preorder_vertex = self
return 1
return 0

上述方法为 BF 算法的关键,参数 nbr_v 是指与当前顶点相邻的顶点。先是计算和当前顶点的新权重,根据更新原则进行更新。如果有更新则需要把当前顶点指定为前序顶点。

Tips: 在图结构中,最短路径算法中的前序顶点指到达此顶点最近的顶点。

图类: 此类用来对图中的顶点进行维护,如添加新顶点,维护顶点之间的关系、提供搜索算法……

初始化方法:

class Graph:
def __init__(self):
# 一维列表,保存节点
self.vert_list = {}
# 顶点个数
self.v_nums = 0
# 使用队列模拟队列或栈,用于路径搜索算法
self.queue_stack = []
# 保存已经更新过的边
self.is_update = []

添加新顶点: 新顶点的编号由内部提供,统一管理,保证编号的一致性。

    def add_vertex(self, vert):
if vert.v_name in self.vert_list:
# 已经存在
return
# 顶点的编号内部生成
vert.v_id = self.v_nums
# 所有顶点保存在图所维护的字典中,以顶点名为键,顶点对象为值
self.vert_list[vert.v_name] = vert
# 数量增一
self.v_nums += 1

查询顶点:

    '''
根据顶点名找到顶点对象
'''
def find_vertex(self, v_name):
if v_name in self.vert_list:
return self.vert_list.get(v_name)
'''
查询所有顶点
'''
def find_vertexes(self):
return [str(ver) for ver in self.vert_list.values()]

添加顶点与顶点之间的关系: 此方法是一个封装方法,本质是调用顶点自身的添加相邻顶点方法。这里用到了递归算法,在 BF 算法中,一轮更新后可能还需要后续多轮更新才能让每一个顶点上的权重不再变化。这也是 BF 算法的缺陷。

    '''
添加节点与节点之间的关系(边),
如果是无权重图,统一设定为 1
''' def add_edge(self, from_v, to_v, weight=1):
# 如果节点不存在
if from_v not in self.vert_list:
self.add_vertex(from_v)
if to_v not in self.vert_list:
self.add_vertex(to_v)
from_v.add_neighbor(to_v, weight)

贝尔曼-福特算法: 图结构中 BF 算法的实现。

    '''
贝尔曼-福特算法
'''
def bf_nearest_path(self, from_v):
# 记录边更新次数
update_count = 0
# 设备起始点的权重为 0
from_v.weight = 0
# 起始点压入队列
self.queue_stack.append(from_v)
# 检查队列是否为空
while len(self.queue_stack) != 0:
# 从队列获取顶点
tmp_v = self.queue_stack.pop(0)
# 标记为已经处理
tmp_v.visited = True
# 得到与其相邻顶点
nbr_vs = tmp_v.connected_to.keys()
# 更新与其相邻顶点的权重
for v_ in nbr_vs:
# 把相邻顶点压入队列
self.push_queue(v_)
# 更新权重,并记录更新次数
update_count += tmp_v.cal_bf_weight(v_)
# 无向边,要双向更新
update_count +=v_.cal_bf_weight(tmp_v)
# 更新完毕后,如果更新次数为 0 ,则不用再更新。
if update_count != 0:
self.is_update = []
self.bf_nearest_path(from_v) '''
把某一顶点的相邻顶点压入队列
这里还是使用广度优先算法思路保存下一个需要搜索的顶点
'''
def push_queue(self, vertex):
# 获取 vertex 顶点的相邻顶点
for v_ in vertex.connected_to.keys():
# 检查此顶点是否压入过
if v_.visited:
continue
self.queue_stack.append(v_)

测试 BF 算法:

if __name__ == '__main__':
# 初始化图
graph = Graph()
# 添加节点
for v_name in ['A', 'B', 'C', 'D', 'E', 'F']:
v = Vertex(v_name)
graph.add_vertex(v) # 添加顶点之间关系
v_to_v = [('A', 'B', 3), ('A', 'C', 5), ('B', 'C', 1), ('B', 'D', 2), ('B', 'E', 4), ('C', 'E', 6), ('D', 'E', 7),
('D', 'F', 8),
('E', 'F', 3)]
# 无向图每 2 个顶点之间互为关系
for v in v_to_v:
f_v = graph.find_vertex(v[0])
t_v = graph.find_vertex(v[1])
graph.add_edge(f_v, t_v, v[2])
graph.add_edge(t_v, f_v, v[2]) # 输出所有顶点
print('-----------顶点及顶点之间的关系-------------')
for v in graph.find_vertexes():
print(v) # 查找起始点到任一顶点之间的最短路径长度
f_v = graph.find_vertex('A')
graph.bf_nearest_path(f_v) print('-----------BF 算法后顶点及顶点之间的关系-------------')
for v in graph.find_vertexes():
print(v) # 查询从起始点到任意顶点间的最短路径长度
print('----------f_v~t_v 最短路径长度------------')
for name in ['B', 'C', 'D', 'E', 'F']:
t_v = graph.find_vertex(name)
path = [t_v] while True:
v = t_v.preorder_vertex
path.insert(0, v)
if v.v_id == f_v.v_id:
break
t_v = v
print([(v.v_name, v.weight) for v in path])

测试结果:

-----------顶点及顶点之间的关系-------------
权重9223372036854775807:名称A顶点相邻的顶点有:[('B', 3), ('C', 5)]
权重9223372036854775807:名称B顶点相邻的顶点有:[('A', 3), ('C', 1), ('D', 2), ('E', 4)]
权重9223372036854775807:名称C顶点相邻的顶点有:[('A', 5), ('B', 1), ('E', 6)]
权重9223372036854775807:名称D顶点相邻的顶点有:[('B', 2), ('E', 7), ('F', 8)]
权重9223372036854775807:名称E顶点相邻的顶点有:[('B', 4), ('C', 6), ('D', 7), ('F', 3)]
权重9223372036854775807:名称F顶点相邻的顶点有:[('D', 8), ('E', 3)]
-----------BF 算法后顶点及顶点之间的关系-------------
权重0:名称A顶点相邻的顶点有:[('B', 3), ('C', 5)]
权重3:名称B顶点相邻的顶点有:[('A', 3), ('C', 1), ('D', 2), ('E', 4)]
权重4:名称C顶点相邻的顶点有:[('A', 5), ('B', 1), ('E', 6)]
权重5:名称D顶点相邻的顶点有:[('B', 2), ('E', 7), ('F', 8)]
权重7:名称E顶点相邻的顶点有:[('B', 4), ('C', 6), ('D', 7), ('F', 3)]
权重10:名称F顶点相邻的顶点有:[('D', 8), ('E', 3)]
----------起始点到任意顶点之间的最短路径长度------------
# A->B 最短路径长度 3
[('A', 0), ('B', 3)]
# A->B->C 最短路径长度为 4
[('A', 0), ('B', 3), ('C', 4)]
# A->B->D 最短路径长度为 5
[('A', 0), ('B', 3), ('D', 5)]
# A->B->E 最短路径长度为 7
[('A', 0), ('B', 3), ('E', 7)]
# A->B->E->F 最短路径长度为 10
[('A', 0), ('B', 3), ('E', 7), ('F', 10)]

3. Dijkstra(迪杰斯特拉)算法

迪杰斯特拉算法(Diikstra) 是由荷兰计算机科学家狄克斯特拉于1959 年提出的,因此又叫狄克斯特拉算法。为了便于表述,本文简称 DJ 算法。

DJ 算法和前面所聊的 BF 算法,可谓同工异曲,算法的核心思想是相同的:

  • 搜索到某一个顶点后,更新与其相邻顶点的权重

    权重计算法则以及权重更新原则两者相同。

  • 且顶点权重的数据含义和 BF 算法的一样。表示从起始点到此点的最短路径长度(也就是经过的所有边的权重之和)。

    初始时,因还不能具体最短路径,起始点的权重为 0 ,其它顶点权重可设置为无穷大。

DJ 算法相比较 BF 算法有 2 个不同的地方:

  • 在无向加权图中,BF 算法需要对相邻 2 个顶点进行双向权重计算。

  • DJ 算法搜索时,每次选择的下一个顶点是所有权重值最小的顶点。其思想是保证每一次选择的顶点和当前顶点权重都是最短的。

    如下图结构中,查找 A 到任一顶点的最短路径:

  1. 定位到起始点 AA 顶点也称为当前顶点

设置 A 的权重为 0,A 的相邻顶点有 BC,需要计算 AB 以及 AC 之间的权重。这里是先选择 B 还是 C 并不重要。

先选择 B 顶点,计算 A -> B 的路径长度权重。新权重计算公式=A顶点权重+(A,B)边的权重=0+3=3

更新原则和 BF 算法一样,当计算出来的权重值小于相邻顶点的权重值时,更新。于是 B 的权重更新为 .此时 AB 的前序顶点。

再选择 C 顶点,计算 A -> C 路径长度权重=0+9=9,因 9 小于 C 现在的无穷大权重,C 的权重被更新为 9.

到这里, 可以说 DJ 算法和 BF 算法没有什么不一样。

但是,DJ 算法不需要对边上 2 个顶点进行双向权重计算,这是 DJ 算法与 BF 算法的第一个差异性。

此时,更新后的图结构如下所示:

很显然, BC 将成为下一个搜索的候选点,这里 DJ 算法和 BF 算法就有了很大的差异性。

BF 算法对于选择 B 还是 C 的顺序没有要求。

DJ 算法则不同,会选择 BC 中权重值更小的那个, B 的权重 3 小于 C 的权重 9 ,当然选择 B 为下一个搜索顶点。

这是 BF 算法和 DJ 算法的第二个差异性!

选择 B 后也就意味着 A->B 之间的最短路径就确定了。为什么?

因你无法再找出一条比之更短的路径。

这里也是 DJ 算法的关键思想,在选择一个权重最小的候选顶点后,就能确定此顶点和它的前序顶点之间的最短路径长度。

到现在为止, B 的前序顶点是 A;C 的前序顶点也是 A 。

因为 B 已经被选为下一个搜索顶点,于是 B 顶点和它的前序顶点 A 之间的最短路径已经出来了。

A->B 最短路径长度为 3。

而 C 顶点还没有成为搜索顶点,其和 A 顶点之间的最短路径还是一个未知数。

  1. **B成为当前顶点 **

找到与 B 相邻的 CDE 3个顶点,然后分别计算路径长度权重。

B->C 的新权重=3+4=7 小于 C 现有的权重 9 ,C 的权重被更新为 7 ,C 的前序顶点也变成了 B

同理,B->D 路径中 D 的权重被更新为 5;B->E 路径中 E 的权重被更新为 6 。

再在 CDE 3 个顶点中选择权重值最小的 D 为下一个搜索顶点.到这里!可以发现 DJ 算法和原始定义的广度搜索算法以及 BF 之间就有了本质上的区别:

  • 广度优先搜索算法会选择和 B 同时进入队列的 C 顶点成为下一个搜索顶点。因为 B 和 C 都是离 A 较近的顶点。
  • DJ 算法是在候选顶点中,哪一个顶点的权重最少,就选择谁,不采用就近原则.而是以顶点的权重值小作为优先选择条件.

选择 D 后 ,各顶点间的关系:

B 的前序是 A,(A,B)间最短路径已经确定。

D 的前序是 B ,(B,D)间的最短路径可以确定,又因为 B 的前序顶点是 A ,所以 A->B->D 的最短路径可以确定。

其它项点间的最短路径暂时还不明朗。

  1. D 顶点为当前顶点

计算与 D 相邻的 E、F 顶点的权重。

D->E 的新权重=5+7=12 大于 E 现有权重 6 ,不更新。E 的前序顶点还是 B

D->F 的新权重=5+8=13 ,F 的权重由无穷大更新为 13

再在省下的所有可选顶点中选择权重值小的 E 顶点为下一个搜索点,当选择 E 后:

E 的前序为 B , B 的前序是 A,所以 A 到 E 的最短路径长度就是 A->B->C ,路径长度为 6。

  1. E 为当前顶点,计算和其相邻顶点间的权重。

唯一可改变的是 F 顶点的权重,F 顶点的前序顶点变为 E

  1. 再选择 C 为当前顶点

C 和相邻顶点不会再产生任何权重的变化,其前序顶点还是 B

所以 AC 之间的最短路径应该是 A->B->C 路径长度为 7。

最后选择 F 顶点,也不会再产生任何权重变化,F 的前序是 EE的前序是BB的前序是A,所 AF 的最短路径应该是 A->B->E->F 权重值为 9。

最后,以图示方式,比较 BF 算法和 DJ 算法中各顶点出队列的顺序:

BF 采用就近原则出队列,然后不停计算相邻顶点的权重,至到权重不再变化为止,显然用的是蛮力。

DJ 采用权重值小的优先出队列,显然用的是巧力。

分析完 DJ 算法的流程,现在编写代码:

和上面的 BF 算法相比较,顶点类一样,在图类中添加 DJ 算法:

DJ 算法的本质还是广度优先搜索算法,有所区别的地方是使用优先队列,每次从队列中选择顶点时,选择顶点权重最小的。

所以在图类中,需要修改或重载一个候选顶点入队列的方法:

    '''
把当前顶点的相邻顶点压入队列
'''
def push_queue_dj(self, vertex):
# 获取 vertex 顶点的相邻顶点
for v_ in vertex.connected_to.keys():
# 检查此顶点是否压入过
if v_.visited:
continue
# vertex.visited = True
self.queue_stack.append(v_)
# 对队列按顶点权重排序,保证权重值小的排在前面。是 DJ 算法的关键
self.queue_stack.sort(key=lambda v: v.weight) '''
检查边是否已经更新过
DJ 算法中已经更新过的边不需要再更新
'''
def is_updated(self, *edge):
s = ord(edge[0].v_name) + ord(edge[1].v_name)
for e in self.is_update:
s1 = ord(e[0].v_name) + ord(e[1].v_name)
if s == s1:
return True
return False

实现 DJ 算法:

 '''
Dijkstra(迪杰斯特拉)算法
'''
def dj_nearest_path(self, from_v):
# 设备起始点的权重为 0
from_v.weight = 0
# 起始点压入队列
self.queue_stack.append(from_v)
# 检查队列是否为空
while len(self.queue_stack) != 0:
# 从队列获取顶点
tmp_v = self.queue_stack.pop(0)
# 标记为已经处理
tmp_v.visited = True
# 得到与其相邻所有顶点
nbr_vs = tmp_v.connected_to.keys()
# 更新与其相邻顶点的权重
for v_ in nbr_vs:
# 边是否已经处理
if self.is_updated(tmp_v, v_):
continue
# 更新权重
tmp_v.cal_bf_weight(v_)
# 记录已经更新过
self.is_update.append((tmp_v, v_))
# 把相邻顶点压入队列
self.push_queue_dj(v_)

测试代码:

if __name__ == '__main__':
# 初始化图
graph = Graph()
# 添加节点
for v_name in ['A', 'B', 'C', 'D', 'E', 'F']:
v = Vertex(v_name)
graph.add_vertex(v) # 添加顶点之间关系
v_to_v = [('A', 'B', 3), ('A', 'C', 9), ('B', 'C', 4), ('B', 'D', 2), ('B', 'E', 3), ('C', 'E', 6), ('D', 'E', 7),
('D', 'F', 8),
('E', 'F', 3)]
# 无向图每 2 个顶点之间互为关系
for v in v_to_v:
f_v = graph.find_vertex(v[0])
t_v = graph.find_vertex(v[1])
graph.add_edge(f_v, t_v, v[2])
graph.add_edge(t_v, f_v, v[2]) # 输出所有顶点
print('-----------顶点及顶点之间的关系-------------')
for v in graph.find_vertexes():
print(v) # 查找起始点到任一顶点之间的最短路径长度
f_v = graph.find_vertex('A')
# DJ 算法
graph.dj_nearest_path(f_v) print('----------- DJ 算法后顶点及顶点之间的关系-------------')
for v in graph.find_vertexes():
print(v) #
print('----------f_v~t_v 最短路径长度------------')
for name in ['B', 'C', 'D', 'E', 'F']:
t_v = graph.find_vertex(name)
path = [t_v] while True:
# 找到前序顶点
v = t_v.preorder_vertex
path.insert(0, v)
if v.v_id == f_v.v_id:
break
t_v = v
print([(v.v_name, v.weight) for v in path])

输出结果:

-----------顶点及顶点之间的关系-------------
权重9223372036854775807:名称A顶点相邻的顶点有:[('B', 3), ('C', 9)]
权重9223372036854775807:名称B顶点相邻的顶点有:[('A', 3), ('C', 4), ('D', 2), ('E', 3)]
权重9223372036854775807:名称C顶点相邻的顶点有:[('A', 9), ('B', 4), ('E', 6)]
权重9223372036854775807:名称D顶点相邻的顶点有:[('B', 2), ('E', 7), ('F', 8)]
权重9223372036854775807:名称E顶点相邻的顶点有:[('B', 3), ('C', 6), ('D', 7), ('F', 3)]
权重9223372036854775807:名称F顶点相邻的顶点有:[('D', 8), ('E', 3)]
----------- DJ 算法后顶点及顶点之间的关系-------------
权重0:名称A顶点相邻的顶点有:[('B', 3), ('C', 9)]
权重3:名称B顶点相邻的顶点有:[('A', 3), ('C', 4), ('D', 2), ('E', 3)]
权重7:名称C顶点相邻的顶点有:[('A', 9), ('B', 4), ('E', 6)]
权重5:名称D顶点相邻的顶点有:[('B', 2), ('E', 7), ('F', 8)]
权重6:名称E顶点相邻的顶点有:[('B', 3), ('C', 6), ('D', 7), ('F', 3)]
权重9:名称F顶点相邻的顶点有:[('D', 8), ('E', 3)]
----------f_v~t_v 最短路径长度------------
[('A', 0), ('B', 3)]
[('A', 0), ('B', 3), ('C', 7)]
[('A', 0), ('B', 3), ('D', 5)]
[('A', 0), ('B', 3), ('E', 6)]
[('A', 0), ('B', 3), ('E', 6), ('F', 9)]

DJ 算法不适合用于边上权重为负数的图中,否则可能找不到路径。

3. 总结

在加权图中查找最短路径长度算法除了 BF、DJ 算法,还有 A* 算法 D* 算法。有兴趣的可以自行了解。

Python 图_系列之纵横对比 Bellman-Ford 和 Dijkstra 最短路径算法的更多相关文章

  1. Python 图_系列之基于邻接炬阵实现广度、深度优先路径搜索算法

    图是一种抽象数据结构,本质和树结构是一样的. 图与树相比较,图具有封闭性,可以把树结构看成是图结构的前生.在树结构中,如果把兄弟节点之间或子节点之间横向连接,便构建成一个图. 树适合描述从上向下的一对 ...

  2. Python 图_系列之基于<链接表>实现无向图最短路径搜索

    图的常用存储方式有 2 种: 邻接炬阵 链接表 邻接炬阵的优点和缺点都很明显.优点是简单.易理解,对于大部分图结构而言,都是稀疏的,使用炬阵存储空间浪费就较大. 链接表的存储相比较邻接炬阵,使用起来更 ...

  3. 【模板】Bellman—Fort 单源最短路径算法

    2333 适用于边集储存 #include<bits/stdc++.h> using namespace std; const int inf=0x3fffffff; ],t[],d[], ...

  4. python人工智能爬虫系列:怎么查看python版本_电脑计算机编程入门教程自学

    首发于:python人工智能爬虫系列:怎么查看python版本_电脑计算机编程入门教程自学 http://jianma123.com/viewthread.aardio?threadid=431 本文 ...

  5. PySide——Python图形化界面入门教程(六)

    PySide——Python图形化界面入门教程(六) ——QListView和QStandardItemModel 翻译自:http://pythoncentral.io/pyside-pyqt-tu ...

  6. python学习_数据处理编程实例(二)

    在上一节python学习_数据处理编程实例(二)的基础上数据发生了变化,文件中除了学生的成绩外,新增了学生姓名和出生年月的信息,因此将要成变成:分别根据姓名输出每个学生的无重复的前三个最好成绩和出生年 ...

  7. Python股票分析系列——基础股票数据操作(二).p4

    该系列视频已经搬运至bilibili: 点击查看 欢迎来到Python for Finance教程系列的第4部分.在本教程中,我们将基于Adj Close列创建烛台/ OHLC图,这将允许我介绍重新采 ...

  8. 深入浅出爬虫之道: Python、Golang与GraphQuery的对比

    深入浅出爬虫之道: Python.Golang与GraphQuery的对比 本文将分别使用 Python ,Golang 以及 GraphQuery 来解析某网站的 素材详情页面 ,这个页面的特色是具 ...

  9. Python基础笔记系列一:基本工具与表达式

    本系列教程供个人学习笔记使用,如果您要浏览可能需要其它编程语言基础(如C语言),why?因为我写得烂啊,只有我自己看得懂!! 工具基础(Windows系统下)传送门:Python基础笔记系列四:工具的 ...

随机推荐

  1. .NET 7 预览版2 的亮点之 NativeAOT 正式合并入 .NET 主线

    .NET 中备受追捧和期待已久的功能NativeAOT终于出现在本周的.NET 7 预览版2中,该项目的工作仍在继续,该版本将 NativeAOT 从实验性的 dotnet/runtimelab re ...

  2. Docker容器和虚拟机区别

    Docker .虚拟机之间区别 虚拟机技术的缺点: 1.资源占用太多 2.冗余步骤多 3.启动很慢 容器化技术 1.服务器资源利用率高 2.比较轻量化 3.打包镜像测试,一键运行 比较Docker和虚 ...

  3. 在Windows 10 上安装使用binwalk

    Windows 10 推出的WSL 功能可以协助我们直接使用binwalk 分析Windows 内的文件 文章更新时间:2020年3月11日 一.WSL 准备 1.打开控制面板→程序和功能→启动或关闭 ...

  4. 单循环链表(基于c语言)

    #include <stdio.h> #include <stdlib.h> #include <assert.h> typedef int LDataType; ...

  5. AQS详解之独占锁模式

    AQS介绍 AbstractQueuedSynchronizer简称AQS,即队列同步器.它是JUC包下面的核心组件,它的主要使用方式是继承,子类通过继承AQS,并实现它的抽象方法来管理同步状态,它分 ...

  6. nginx反向代理出现了代理的端口号

    nginx反向代理显示了代理的端口号 nginx反向代理配置文件 upstream mall { server 1.1.1.1:10261 weight=1; } server { listen 80 ...

  7. 三极管与MOS管主要参数差别及驱动电路基极(栅极)串联电阻选取原则

    三极管与MOS管都常在电路中被当做开关使用,比较起来: 1. 三极管集电极电流IC (一般为mA级别),远小于MOS管ID(一般为A级别),因此MOS管多用在大电流电路中,如电机驱动 2. 三极管耗散 ...

  8. Oracle 11G DBMS包和类型参考

    参阅:https://docs.oracle.com/cd/E11882_01/appdev.112/e40758/d_lob.htm#ARPLS66712

  9. JDBC中的Statement 和PreparedStatement的区别?

    PreparedStatement是预编译的SQL语句,效率高于Statement. PreparedStatement支持操作符,相对于Statement更加灵活. PreparedStatemen ...

  10. JDBC操作数据库的步骤 ?

    注册数据库驱动. 建立数据库连接. 创建一个Statement. 执行SQL语句. 处理结果集. 关闭数据库连接.