第八节--图的数据结构及其算法
第八节–图的数据结构及其算法
图除了被应用在数据结构中最短路径搜索,拓扑排序外,还能应用在系统分析中以时间为评审标准的性能评审技术等。采用Dijkstra这种图形算法就能快速寻找出两个节点之间的最短路径
一.图的遍历
图的遍历,可以定义如下:
一个图G=(V,E),存在某一顶点v属于V,我们希望从v开始,通过此节点相邻的节点而去访问图G中的其他节点,这就被称为"图的遍历"。也就是从某一个顶点V1开始,遍历可以经过V1到达的顶点,接着遍历下一个顶点直到全部的顶点遍历完毕为止
图遍历的方法有两种:即"深度优先遍历"和"广度优先遍历",也称为"深度优先搜索"和"广度优先搜索"
1.深度优先遍历法
深度优先遍历的方式有点类似于前序遍历,从图的某一顶点开始遍历,被访问过的顶点就做上已访问的记号,接着遍历此顶点所有相邻且未访问过的顶点中的任意一个顶点,并做上已访问的记号,再以该点为新的起点继续进行深度优先搜索
这种图的遍历方法结合了递归和堆栈两种数据结构的技巧,由于此方法会造成无限循环,因此必须加入一个变量,判断该点是否已经遍历完毕
步骤01:以顶点1为起点,将相邻的顶点2和顶点5压人堆栈
步骤02:弹出顶点2,将与顶点2相邻且未访问过的顶点3和顶点4压人堆栈
步骤03:弹出顶点3,将与顶点3相邻且未访问过的顶点4和顶点5压人堆栈
步骤04:弹出顶点4,将顶点4相邻且未访问过的顶点5压人堆栈
步骤05:弹出顶点5,将与顶点5相邻且未访问过的顶点压人堆栈,大家可以发现与5相邻的顶点全部被访问过了,所以无须压人堆栈
步骤06:将堆栈内的值弹出并判断是否已经遍历过了,直到堆栈内无节点可遍历为止
故深度优先的遍历顺序为:顶点1,顶点2,顶点3,顶点4,顶点5
深度优先函数的算法如下:
def dfs(current): #深度优先函数
run[current]=1
print('[%d] ' %current, end='')
ptr=head[current].next
while ptr!=None:
if run[ptr.val]==0: #如果顶点尚未遍历,
dfs(ptr.val) #就进行dfs的递归调用
ptr=ptr.next
1.2.程序说明
1.2.1图的数组如下,进行深度优先遍历法
#声明图的边线数组
data=[[1,2],[2,1],[1,3],[3,1], \
[2,4],[4,2],[2,5],[5,2], \
[3,6],[6,3],[3,7],[7,3], \
[4,8],[8,4],[5,8],[8,5], \
[6,8],[8,6],[8,7],[7,8]]
1.2.2源程序
class list_node:
def __init__(self):
self.val=0
self.next=None
head=[list_node()]*9 #声明一个节点类型的链表数组
run=[0]*9
def dfs(current): #深度优先函数
run[current]=1
print('[%d] ' %current, end='')
ptr=head[current].next
while ptr!=None:
if run[ptr.val]==0: #如果顶点尚未遍历,
dfs(ptr.val) #就进行dfs的递归调用
ptr=ptr.next
#声明图的边线数组
data=[[1,2],[2,1],[1,3],[3,1], \
[2,4],[4,2],[2,5],[5,2], \
[3,6],[6,3],[3,7],[7,3], \
[4,8],[8,4],[5,8],[8,5], \
[6,8],[8,6],[8,7],[7,8]]
for i in range(1,9): #共有八个顶点
run[i]=0 #把所有顶点设置成尚未遍历过
head[i]=list_node()
head[i].val=i #设置各个链表头的初值
head[i].next=None
ptr=head[i] #设置指针指向链表头
for j in range(20): #二十条边线
if data[j][0]==i: #如果起点和链表头相等,则把顶点加入链表
newnode=list_node()
newnode.val=data[j][1]
newnode.next=None
while True:
ptr.next=newnode #加入新节点
ptr=ptr.next
if ptr.next==None:
break
print('图的邻接表内容:') #打印图的邻接表内容
for i in range(1,9):
ptr=head[i]
print('顶点 %d=> ' %i,end='')
ptr =ptr.next
while ptr!=None:
print('[%d] ' %ptr.val,end='')
ptr=ptr.next
print()
print('深度优先遍历的顶点:') #打印深度优先遍历的顶点
dfs(1)
print()
2.3运行结果
图的邻接表内容:
顶点 1=> [2] [3]
顶点 2=> [1] [4] [5]
顶点 3=> [1] [6] [7]
顶点 4=> [2] [8]
顶点 5=> [2] [8]
顶点 6=> [3] [8]
顶点 7=> [3] [8]
顶点 8=> [4] [5] [6] [7]
深度优先遍历的顶点:
[1] [2] [4] [8] [5] [6] [3] [7]
2.广度优先遍历法
广度优先(Breadth-FIrst Search,BFS)遍历法则是使用队列和递归技巧来遍历,也是从图的某一顶点开始遍历,被访问过的顶点就做上已访问的记号。接着遍历此顶点的所有相邻且未访问过的顶点中的任意一个顶点,并做上已访问的记号,再以该点为新的起点继续进行广度优先的遍历
步骤01:以顶点1为起点,将相邻且未访问过的顶点2和顶点5加入队列
步骤02:取出顶点2,将与顶点2相邻且未访问过的顶点3和顶点4加入队列
步骤03:取出顶点5,将与顶点5相邻且未访问过的顶点3和顶点4加入队列
步骤04:取出顶点3,将顶点3相邻且未访问过的顶点4加入队列
步骤05:取出顶点4,将与顶点4相邻且未访问过的顶点加入队列中,大家可以发现与顶点4相邻的顶点全部被访问过了,所以无须再加入队列中
步骤06:将队列内的值弹出并判断是否已经遍历过了,直到队列内无节点可遍历为止
故深度优先的遍历顺序为:顶点1,顶点2,顶点5,顶点3,顶点4
广度优先函数的python函数:
#广度优先查找法
def bfs(current):
global front
global rear
global Head
global run
enqueue(current) #将第一个顶点存入队列
run[current]=1 #将遍历过的顶点设置为1
print('[%d]' %current, end='') #打印出该遍历过的顶点
while front!=rear: #判断当前的队伍是否为空
current=dequeue() #将顶点从队列中取出
tempnode=Head[current].first #先记录当前顶点的位置
while tempnode!=None:
if run[tempnode.x]==0:
enqueue(tempnode.x)
run[tempnode.x]=1 #记录已遍历过
print('[%d]' %tempnode.x,end='')
tempnode=tempnode.next
2.1程序说明
2.1.1存放图的数组如下:
Data =[[1,2],[2,1],[1,3],[3,1],[2,4], \
[4,2],[2,5],[5,2],[3,6],[6,3], \
[3,7],[7,3],[4,5],[5,4],[6,7],[7,6],[5,8],[8,5],[6,8],[8,6]]
2.1.2源程序
MAXSIZE=10 #定义队列的最大容量
front=-1 #指向队列的前端
rear=-1 #指向队列的末尾
class Node:
def __init__(self,x):
self.x=x #顶点数据
self.next=None #指向下一个顶点的指针
class GraphLink:
def __init__(self):
self.first=None
self.last=None
def my_print(self):
current=self.first
while current!=None:
print('[%d]' %current.x,end='')
current=current.next
print()
def insert(self,x):
newNode=Node(x)
if self.first==None:
self.first=newNode
self.last=newNode
else:
self.last.next=newNode
self.last=newNode
#队列数据的存入
def enqueue(value):
global MAXSIZE
global rear
global queue
if rear>=MAXSIZE:
return
rear+=1
queue[rear]=value
#队列数据的取出
def dequeue():
global front
global queue
if front==rear:
return -1
front+=1
return queue[front]
#广度优先查找法
def bfs(current):
global front
global rear
global Head
global run
enqueue(current) #将第一个顶点存入队列
run[current]=1 #将遍历过的顶点设置为1
print('[%d]' %current, end='') #打印出该遍历过的顶点
while front!=rear: #判断当前的队伍是否为空
current=dequeue() #将顶点从队列中取出
tempnode=Head[current].first #先记录当前顶点的位置
while tempnode!=None:
if run[tempnode.x]==0:
enqueue(tempnode.x)
run[tempnode.x]=1 #记录已遍历过
print('[%d]' %tempnode.x,end='')
tempnode=tempnode.next
#声明图的边线数组
Data=[[0]*2 for row in range(20)]
Data =[[1,2],[2,1],[1,3],[3,1],[2,4], \
[4,2],[2,5],[5,2],[3,6],[6,3], \
[3,7],[7,3],[4,5],[5,4],[6,7],[7,6],[5,8],[8,5],[6,8],[8,6]]
run=[0]*9 #用来记录各顶点是否遍历过
queue=[0]*MAXSIZE
Head=[GraphLink]*9
print('图的邻接表内容:') #打印图的邻接表内容
for i in range(1,9): #共有8个顶点
run[i]=0 #把所有顶点设置成尚未遍历过
print('顶点%d=>' %i,end='')
Head[i]=GraphLink()
for j in range(20):
if Data[j][0]==i: #如果起点和链表头相等,则把顶点加入链表
DataNum = Data[j][1]
Head[i].insert(DataNum)
Head[i].my_print() #打印图的邻接标内容
print('广度优先遍历的顶点:') #打印广度优先遍历的顶点
bfs(1)
print()
2.1.3运行结果
图的邻接表内容:
顶点1=>[2][3]
顶点2=>[1][4][5]
顶点3=>[1][6][7]
顶点4=>[2][5]
顶点5=>[2][4][8]
顶点6=>[3][7][8]
顶点7=>[3][6]
顶点8=>[5][6]
广度优先遍历的顶点:
[1][2][3][4][5][6][7][8]
二.最小生成树
生成树又称"花费树",“出本树"或"值树”,一个图的生成树(spanning tree)就是以最少的边来连通图中所有的顶点,且不造成回路(cycle)的树形结构。假设在树的边加上一个权重(weight)值,这种图就成为"加权图(weighted graph)"。如果这个权重值代表两个顶点间的距离(distance)或成本(cost),这类图就被称为网络(network)
介绍以所谓"贪婪法则"(Greedy Rule)为基础来求得一个无向连通图的最小生成树的常见问题,分别是Prim算法和Kruskal算法
1.Prim算法
Prim算法又称P氏法,对一个加权图形G=(V,E),设V={1,2,…,n},假设U={1},也就是说,U和V是两个顶点的集合
然后从U-V差集所产生的集合找出一个顶点x,该顶点x能与U集合中的某点形成最小成本边,且不会造成回路。然后将顶点x加入U集合中,反复执行同样的步骤,一直到U集合等于V集合(U=V)为止
接下来,我们实际使用P氏法求出下图的最小生成树
从此图中可得V={1,2,3,4,5,6},U=1
步骤1:从V-U={2,3,4,5,6}中找一个顶点与U顶点能形成最小成本的边
步骤2:V-U={2,3,4,6},U={1,5}。从V-U中找到一个顶点与U顶点能形成最小成本的边
步骤3:U={1,5,6},V-U={2,3,4}。同理找到顶点4
步骤4:U={1,5,6,4},V-U={2,3},同理找到顶点3
步骤5:U={1,5,6,4,3},V-U={2},同理找到顶点2
2.Kruskal算法
Kruskal算法是将各边按权值大小从小到大排列,接着从权值最低的边开始建立最小成本生成树,如果加入的边会造成回路则舍弃不用,直到加入了n-1个边为止
步骤01:把所有边的成本列出并从小到大排序
起始顶点 | 终止顶点 | 成本 |
---|---|---|
B | C | 3 |
B | D | 5 |
A | B | 6 |
C | D | 7 |
B | F | 8 |
D | E | 9 |
A | E | 10 |
D | F | 11 |
A | F | 12 |
E | F | 16 |
步骤02:选择成本最低的一条边作为最小成本树的起点
步骤03:按所建立的表格,按序加入边
步骤04:C-D加入会形成回路,所以直接跳过
步骤05:完成图
用python语言编写的Kruskal算法:
VERTS=6 #图的顶点数
class edge: #声明边的类
def __init__(self):
self.start=0
self.to=0
self.find=0
self.val=0
self.next=None
v=[0]*(VERTS+1)
def findmincost(head): #搜索成本最小的边
minval=100
ptr=head
while ptr!=None:
if ptr.val<minval and ptr.find==0: #假如ptr.val的值小于minval
minval=ptr.val #就把ptr.val设为最小值
retptr=ptr #并且把ptr纪录下来
ptr=ptr.next
retptr.find=1 #将retptr设为已找到的边
return retptr #返回retptr
def mintree(head): #最小成本生成树函数
global VERTS
result=0
ptr=head
for i in range(VERTS):
v[i]=0
while ptr!=None:
mceptr=findmincost(head)
v[mceptr.start]=v[mceptr.start]+1
v[mceptr.to]=v[mceptr.to]+1
if v[mceptr.start]>1 and v[mceptr.to]>1:
v[mceptr.start]=v[mceptr.start]-1
v[mceptr.to]=v[mceptr.to]-1
result=1
else:
result=0
if result==0:
print('起始顶点 [%d] -> 终止顶点 [%d] -> 路径长度 [%d]' \
%(mceptr.start,mceptr.to,mceptr.val))
ptr=ptr.next
2.1程序说明
2.1.1功能说明
使用一个二维数组存储树并对K使法的成本表进行排序,试设计一个python程序来求取最小成本生成树
2.1.2源程序
VERTS=6 #图的顶点数
class edge: #声明边的类
def __init__(self):
self.start=0
self.to=0
self.find=0
self.val=0
self.next=None
v=[0]*(VERTS+1)
def findmincost(head): #搜索成本最小的边
minval=100
ptr=head
while ptr!=None:
if ptr.val<minval and ptr.find==0: #假如ptr.val的值小于minval
minval=ptr.val #就把ptr.val设为最小值
retptr=ptr #并且把ptr纪录下来
ptr=ptr.next
retptr.find=1 #将retptr设为已找到的边
return retptr #返回retptr
def mintree(head): #最小成本生成树函数
global VERTS
result=0
ptr=head
for i in range(VERTS):
v[i]=0
while ptr!=None:
mceptr=findmincost(head)
v[mceptr.start]=v[mceptr.start]+1
v[mceptr.to]=v[mceptr.to]+1
if v[mceptr.start]>1 and v[mceptr.to]>1:
v[mceptr.start]=v[mceptr.start]-1
v[mceptr.to]=v[mceptr.to]-1
result=1
else:
result=0
if result==0:
print('起始顶点 [%d] -> 终止顶点 [%d] -> 路径长度 [%d]' \
%(mceptr.start,mceptr.to,mceptr.val))
ptr=ptr.next
#成本表数组
data=[[1,2,6],[1,6,12],[1,5,10],[2,3,3], \
[2,4,5],[2,6,8],[3,4,7],[4,6,11], \
[4,5,9],[5,6,16]]
head=None
#建立图的链表
for i in range(10):
for j in range(1,VERTS+1):
if data[i][0]==j:
newnode=edge()
newnode.start=data[i][0]
newnode.to=data[i][1]
newnode.val=data[i][2]
newnode.find=0
newnode.next=None
if head==None:
head=newnode
head.next=None
ptr=head
else:
ptr.next=newnode
ptr=ptr.next
print('-------------------------------------------------')
print('建立最小成本生成树:')
print('-------------------------------------------------')
mintree(head) #建立最小成本生成树
2.1.3运行结果
-------------------------------------------------
建立最小成本生成树:
-------------------------------------------------
起始顶点 [2] -> 终止顶点 [3] -> 路径长度 [3]
起始顶点 [2] -> 终止顶点 [4] -> 路径长度 [5]
起始顶点 [1] -> 终止顶点 [2] -> 路径长度 [6]
起始顶点 [2] -> 终止顶点 [6] -> 路径长度 [8]
起始顶点 [4] -> 终止顶点 [5] -> 路径长度 [9]
三.图的最短路径法
在一个有向图G=(V,E)中,每一条边都有一个比例常数W(Weight)与之对应,如果想求G图中某一个顶点V到其他顶点的最少W总和之值,这类问题就称为最短路径问题(The Shortest Path Problem)
最小成本生成树(MST,或称最小花费生成树)就是计算连通网络中每一个顶点所需的最少花费,但是连通树中任意两顶点的路径不一定是一条花费最少的路径,这也是本节将研究最短路径问题的主要理由。一般讨论的方向有两种:一种是Dijkstra算法与A算法,另一种是Floyd算法
1.Dijkstra算法与A算法
1.1Dijkstra算法
一个顶点到多个顶点通常使用Dijkstra算法求得,Dijkstra的算法如下:假设S={Vi | V````V},且``Vi在已发现的最短路径中,其中V``S是起点
假设w∉S,定义Dist(w)是从V到w的最短路径,这条路径除了w外必属于S,且有下列几点特性:
- 如果u是当前所找到最短路径的下一个节点,则u必属于V-S集合中最小成本的边
- 若u被选中,将u加入S集合中,则会产生当前的从V到u的最短路径,对于w∉S,DIST(w)被改变成DIST(w)<—Min{DIST(w),DIST(u)+COST(u,w)}
步骤如下:
步骤01:首先进行定义
G=(V,E)
D[K]=A[F,K] 其中K从1到N
S={F}
V={1,2,.....N}
- D为一个N维数组,用来存放某一顶点到其他顶点的最短距离
- F表示起始顶点
- A[F,I]为顶点F到I的距离
- V是网络中所有顶点的集合
- E是网络中所有边的组合
- S也是顶点的集合,其初始值是S={F}
步骤02:从V-S集合中找到一个顶点x,使D(x)的值为最小值,并把x放入S集合中
步骤03:按公式D[I]=min(D[I],D[x]+A[x,I])(其中(x,I)∈E)来调整D数组的值,其中I是指x的相邻各顶点
步骤04:重复执行步骤2,一直到V-S是空集合为止
现在来直接看一个例子。在下图中,找出顶点5到各顶点间的最短路径
做法相当简单,首先从顶点5开始,找出顶点5到各顶点间最小的距离,到达不了的以∞表示。步骤如下:
步骤01:D[0]=∞,D[1]=12,D[2]=∞,D[3]=20,D[4]=14。在其中找出值最小的顶点并加入S集合中D[1]
步骤02:D[0]=∞,D[1]=12,D[2]=18,D[3]=20,D[4]=14。D[4]最小,加入S集合中
步骤03:D[0]=26,D[1]=12,D[2]=18,D[3]=20,D[4]=14。D[2]最小,加入S集合中
步骤04:D[0]=26,D[1]=12,D[2]=18,D[3]=20,D[4]=14。D[3]最小,加入S集合中
步骤05:加入最后一个顶点,即可得到下面的数据
步骤 | S | 0 | 1 | 2 | 3 | 4 | 5 | 选择 |
---|---|---|---|---|---|---|---|---|
1 | 5 | ∞ | 12 | ∞ | 20 | 14 | 0 | 1 |
2 | 5,1 | ∞ | 12 | 18 | 20 | 14 | 0 | 4 |
3 | 5,1,4 | 26 | 12 | 18 | 20 | 14 | 0 | 2 |
4 | 5,1,4,2 | 26 | 12 | 18 | 20 | 14 | 0 | 3 |
5 | 5,1,4,2,3 | 26 | 12 | 18 | 20 | 14 | 0 | 0 |
从顶点5到其他各顶点的最短距离为:
- 顶点5—>顶点0:26
- 顶点5—>顶点1:12
- 顶点5—>顶点2:18
- 顶点5—>顶点3:20
- 顶点5—>顶点4:14
程序说明
1.功能要求
使用Dijkstra算法来求取下面图结构中顶点1对全部图的顶点间的最短路径
Path_Cost = [ [1, 2, 29], [2, 3, 30],[2, 4, 35], \
[3, 5, 28],[3, 6, 87],[4, 5, 42], \
[4, 6, 75],[5, 6, 97]]
2.源程序
SIZE=7
NUMBER=6
INFINITE=99999 # 无穷大
Graph_Matrix=[[0]*SIZE for row in range(SIZE)] # 图的数组
distance=[0]*SIZE # 路径长度数组
def BuildGraph_Matrix(Path_Cost):
for i in range(1,SIZE):
for j in range(1,SIZE):
if i == j :
Graph_Matrix[i][j] = 0 # 对角线设为0
else:
Graph_Matrix[i][j] = INFINITE
# 存入图的边
i=0
while i<SIZE:
Start_Point = Path_Cost[i][0]
End_Point = Path_Cost[i][1]
Graph_Matrix[Start_Point][End_Point]=Path_Cost[i][2]
i+=1
# 单点对全部顶点的最短距离
def shortestPath(vertex1, vertex_total):
shortest_vertex = 1 #记录最短距离的顶点
goal=[0]*SIZE #用来记录该顶点是否被选取
for i in range(1,vertex_total+1):
goal[i] = 0
distance[i] = Graph_Matrix[vertex1][i]
goal[vertex1] = 1
distance[vertex1] = 0
print()
for i in range(1,vertex_total):
shortest_distance = INFINITE
for j in range(1,vertex_total+1):
if goal[j]==0 and shortest_distance>distance[j]:
shortest_distance=distance[j]
shortest_vertex=j
goal[shortest_vertex] = 1
# 计算开始顶点到各顶点的最短距离
for j in range(vertex_total+1):
if goal[j] == 0 and \
distance[shortest_vertex]+Graph_Matrix[shortest_vertex][j] \
<distance[j]:
distance[j]=distance[shortest_vertex] \
+Graph_Matrix[shortest_vertex][j]
# 主程序
global Path_Cost
Path_Cost = [ [1, 2, 29], [2, 3, 30],[2, 4, 35], \
[3, 5, 28],[3, 6, 87],[4, 5, 42], \
[4, 6, 75],[5, 6, 97]]
BuildGraph_Matrix(Path_Cost)
shortestPath(1,NUMBER) # 搜索最短路径
print('-----------------------------------')
print('顶点1到各顶点最短距离的最终结果')
print('-----------------------------------')
for j in range(1,SIZE):
print('顶点 1到顶点%2d的最短距离=%3d' %(j,distance[j]))
print('-----------------------------------')
print()
3.运行结果
-----------------------------------
顶点1到各顶点最短距离的最终结果
-----------------------------------
顶点 1到顶点 1的最短距离= 0
顶点 1到顶点 2的最短距离= 29
顶点 1到顶点 3的最短距离= 59
顶点 1到顶点 4的最短距离= 64
顶点 1到顶点 5的最短距离= 87
顶点 1到顶点 6的最短距离=139
2.A算法
3.Floyd算法
Dijkstra的方法只能求出某一点到其他顶点的最短距离,如果要求出图中任意两点甚至所有顶点间最短的距离,就必须使用Floyd算法。
Floyd算法定义:
- A[i][j]=ming{A[i][j],A[i][k]+A[k][j]},k>=1。k表示经过的顶点,A[i][j]为从顶点i到j的经由k顶点的最短路径
- A[i][j]=COST[i]j,A为顶点i到j间的直通距离
- A[i,j]代表i到j的最短距离,即A便是我们所要求出的最短路径成本矩阵