第八节--图的数据结构及其算法

第八节–图的数据结构及其算法

图除了被应用在数据结构中最短路径搜索,拓扑排序外,还能应用在系统分析中以时间为评审标准的性能评审技术等。采用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:把所有边的成本列出并从小到大排序

起始顶点终止顶点成本
BC3
BD5
AB6
CD7
BF8
DE9
AE10
DF11
AF12
EF16

步骤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,且有下列几点特性:

  1. 如果u是当前所找到最短路径的下一个节点,则u必属于V-S集合中最小成本的边
  2. 若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:加入最后一个顶点,即可得到下面的数据

步骤S012345选择
1512201401
25,11218201404
35,1,4261218201402
45,1,4,2261218201403
55,1,4,2,3261218201400

从顶点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算法定义:

  1. A[i][j]=ming{A[i][j],A[i][k]+A[k][j]},k>=1。k表示经过的顶点,A[i][j]为从顶点i到j的经由k顶点的最短路径
  2. A[i][j]=COST[i]j,A为顶点i到j间的直通距离
  3. A[i,j]代表i到j的最短距离,即A便是我们所要求出的最短路径成本矩阵
    原文作者:LQ6H
    原文地址: https://www.cnblogs.com/LQ6H/p/10346663.html
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞