08 图的数据结构和算法

图的遍历

  • 深度优先遍历
    有些类似前序遍历,从图的某一顶点开始遍历,被访问过的顶点就做上已访问的记号,接着遍历此顶点所有相邻且未访问过的顶点中的任意一个顶点,并做上已访问的记号,再以该点为新的起点继续进行深度优先的搜索。
    这种遍历方法结合了递归和堆栈两种数据结构的技巧,由于此方法会造成无限循环,因此必须加入一个变量,判断该点是否已经遍历完毕。
  class Node:
      def __init__(self):
          self.val = 0
          self.next = None
  
  head = [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)
          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], [7, 8], [8, 7]
  ]
  
  for i in range(1, 9):
      run[i] = 0
      head[i] = Node()
      head[i].val = i  # 给各个链表头设置初值
      head[i].next = None
      ptr = head[i]
      for j in range(20):  # 20条边线
          if data[j][0] == i:  # 如果起点和链表头相等,就把顶点加入链表
              newnode = 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()
图的邻接表内容:
  顶点 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] 
  • 广度优先遍历
    利用队列和递归技巧。从图的某一顶点开始遍历,被访问过的顶点做上已访问的记号,接着遍历此顶点的所有相邻且未访问过的顶点中的任意一个顶点,并做上已访问的记号,再以该点为起点继续进行广度优先遍历。
  MAXSIZE = 10  # 定义队列最大容量
  
  front = -1  # 指向队列的前端
  rear = -1  # 执行队列的后端
  
  
  class Node:
      def __init__(self, x):
          self.val = 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.val, 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
      print(" [%d] " % current, end='')
      while front != rear:  # 判断当前队列是否为空
          current = dequeue()
          tempnode = Head[current].first  # 先记录当前顶点的位置
          while tempnode != None:
              if run[tempnode.val] == 0:
                  enqueue(tempnode.val)
                  run[tempnode.val] = 1
                  print(" [%d] " % tempnode.val, 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, 8], [8, 4], [5, 8], [8, 5],
      [6, 8], [8, 6], [7, 8], [8, 7]
  ]
  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()
图的邻接表内容:
  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]  [3]  [4]  [5]  [6]  [7]  [8] 

最小生成树(Minimum Cost Spanning Tree,MST)

一个图的生成树(spanning tree)就是以最少的边来连通图中所有的顶点,且不造成回路(cycle)的树形结构。
weighted graph / network

  • 贪婪法则(Greedy Rule)
    1. Prim 算法
      P氏法。对一个加权图形 G=(V, E),设 V = {1,2,...,n},假设 U = {1},也就是说,U 和 V 是两个顶点的集合。
      然后从 U-V 的差集所产生的集合中找出一个顶点 x,该顶点 x 能与 U 集合中的某点形成最小成本的边,且不会造成回路。然后将顶点 x 加入 U 集合中,反复执行同样的步骤,一直到 U 集合等于 V 集合( U=V )为止。
    2. Kruskal 算法
      K氏法。将各边按权值大小从小到大排列,接着从权值最低的边开始建立最小成本的生成树,如果加入的边会造成回路则舍弃不用,直到加入了 n-1 个边为止。

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:
              minval = ptr.val
              retptr = ptr
          ptr = ptr.next
      retptr.find = 1
      return 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] -= 1
              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] -> 终止顶点[3] -> 路径长度[3]
  起始顶点[2] -> 终止顶点[4] -> 路径长度[5]
  起始顶点[1] -> 终止顶点[2] -> 路径长度[6]
  起始顶点[2] -> 终止顶点[6] -> 路径长度[8]
  起始顶点[4] -> 终止顶点[5] -> 路径长度[9]

图的最短路径法

MST 计算连通网络中的每一个顶点所需的最小花费,但是连通树中任意两顶点的路径不一定是一条花费最少的路径。

  • Dijkstra算法与A*算法

Dijkstra算法 —— 实际权重

  SIZE = 7
  NUMBER = 6
  INFINITE = 99999
  
  Graph_Matrix = [[0] * SIZE for row in range(SIZE)]  # 图的数组
  distance = [0] * SIZE  # 路径长度数组
  
  
  def build_graph_matrix(path_cost):
      for i in range(1, SIZE):
          for j in range(1, SIZE):
              if i == j:  # 自己到自己的距离
                  Graph_Matrix[i][j] = 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 shortest_path(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
  
      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],
  ]
  build_graph_matrix(path_cost)
  shortest_path(1, NUMBER)
  print('------------------------------')
  print('顶点 1 到各顶点最短距离的最终结果:')
  print('------------------------------')
  for i in range(1, SIZE):
      print('顶点 1 到顶点 %d 的最短距离为 %d ' % (i, distance[i]))
  ------------------------------
  顶点 1 到各顶点最短距离的最终结果:
  ------------------------------
  顶点 1 到顶点 1 的最短距离为 0 
  顶点 1 到顶点 2 的最短距离为 29 
  顶点 1 到顶点 3 的最短距离为 59 
  顶点 1 到顶点 4 的最短距离为 64 
  顶点 1 到顶点 5 的最短距离为 87 
  顶点 1 到顶点 6 的最短距离为 139 

效率不高,因为在寻找起点到各个顶点距离的过程中,无论哪一个点,都要实际去计算起点与各个顶点之间的距离,以便获得最后的一个判断:到底哪一个顶点距离与起点最近。也就是说该算法在带有权重值的有向图间的最短路径的寻找方式,只是简单的使用了广度优先进行查找,完全忽略了许多有用的信息,这种查找算法会消耗很多系统资源,包括CPU的时间和内存空间。

A*算法 —— 实际权重+推测权重
结合了在路径查找过程中从起点到各个顶点的“实际权重”及各个顶点预估到达终点的“推测权重”(heuristic cost)两个因素,可以有效减少不必要的查找操作,提高效率。

  • 距离评估函数
    1. 曼哈顿距离(Manhattan distance):D = | x1 - x2 | + | y1 - y2 |
    2. 切比雪夫距离(Chebysev distance):D = max( | x1 - x2 |, | y1 - y2 | )
    3. 欧式几何平面直线距离(Euclidean distance):D = ( ( x1 - x2)^2 + (y1 - y2 )^2 )^1/2
  • A* 算法主要步骤
    1. 首先确定各个顶点到终点的“推测权重”。“推测权重”可采取各个顶点和终点之间的直线距离(四舍五入后的值),直线距离的计算函数,可用上述其一。
    2. 分别计算从起点可抵达的各个顶点的权重,其计算方法是由起点到该顶点的“实际权重”,加上该顶点抵达终点的“推测权重”。计算完毕后,选出权重最小的点,并标示为查找完毕的点。
    3. 接着计算从查找完毕的顶点出发到各个顶点的权重,并在从其中选出一个权重最小的顶点,并再将其标示为查找完毕的顶点。以此类推。。。,反复同样的计算过程,一直到抵达最后的终点。

A* 算法适用于可以事先获得或预估各个顶点到终点距离的情况,万一无法获得各个顶点到达目的地终点的距离信息时,就无法使用A*算法。效率不是总优于 Dijkstra 算法,当“推测权重”的距离和实际两个顶点间的距离相差很大时,A* 算法的查找效率可能比 Dijkstra 算法更差,甚至还会误导方向,造成无法得到最短路径的最终答案。
但是,如果推测权重所设置的距离和实际两个顶点间的真实距离误差不大时,A* 算法的查找效率远大于 Dijkstra算法。
A* 算法常被用于游戏中角色追逐与快速突破关卡的设计。

Floyd算法
Dijkstra 算法只能求出某一点到其他顶点的距离,如果要求出图中任意两点甚至所有点间最短的距离,就必须使用 Floyd 算法。

  SIZE = 7
  NUMBER = 6
  INFINITE = 99999
  
  Graph_Matrix = [[0] * SIZE for row in range(SIZE)]  # 图的数组
  distance = [[0] * SIZE for row in range(SIZE)]  # 路径长度数组
  
  
  def build_graph_matrix(path_cost):
      for i in range(1, SIZE):
          for j in range(1, SIZE):
              if i == j:
                  Graph_Matrix[i][j] = 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 shortest_path(vertex_total):
      # 初始化图的长度数组
      for i in range(1, vertex_total + 1):
          for j in range(i, vertex_total + 1):
              distance[i][j] = Graph_Matrix[i][j]
              distance[j][i] = Graph_Matrix[i][j]
  
      # 使用 Floyd 算法找出所有顶点两两之间的最短距离
      for k in range(1, vertex_total + 1):
          for i in range(1, vertex_total + 1):
              for j in range(1, vertex_total + 1):
                  if distance[i][k] + distance[k][j] < distance[i][j]:
                      distance[i][j] = distance[i][k] + distance[k][j]
  
  
  path_cost = [
      [1, 2, 20], [2, 3, 30], [2, 4, 25],
      [3, 5, 28], [4, 5, 32], [4, 6, 95],
      [5, 6, 67]
  ]
  
  build_graph_matrix(path_cost)
  print('=================================================')
  print('所有顶点两两之间的最短距离:')
  print('=================================================')
  shortest_path(NUMBER)
  print('        顶点1   顶点2  顶点3  顶点4   顶点5  顶点6')
  for i in range(1, NUMBER + 1):
      print('顶点 %d' % i, end='')
      for j in range(1, NUMBER + 1):
          print('%6d ' % distance[i][j], end='')
      print()
  =================================================
  所有顶点两两之间的最短距离:
  =================================================
          顶点1   顶点2  顶点3  顶点4   顶点5  顶点6
  顶点 1     0     20     50     45     77    140 
  顶点 2    20      0     30     25     57    120 
  顶点 3    50     30      0     55     28     95 
  顶点 4    45     25     55      0     32     95 
  顶点 5    77     57     28     32      0     67 
  顶点 6   140    120     95     95     67      0 

拓扑排序的步骤:

  1. 寻找图中任何一个没有先行者的起点
  2. 输出此顶点,并将此顶点的所有边删除
  3. 重复上述步骤,处理所有顶点
posted @ 2019-10-29 18:01  catyuang  阅读(605)  评论(0编辑  收藏  举报