zoukankan      html  css  js  c++  java
  • 图的增删、遍历与应用实现

    本文主要以一个带权有向图为例,讲解图相关的一些算法实现(Go语言),包括图的顶点和边的插入与删除操作,还有图的深度优先遍历和广度优先遍历,以及图的一些引申应用:最小生成树、从源点到其余各点的最短路径、拓扑排序。

    一、图的概念
    图是由顶点集合及顶点间的关系(边)集合组成的一种数据结构。
    关于图的更多基本术语的概念可以参考:https://www.jianshu.com/p/d9ca383e2bd8

    二、图的存储表示
    图的存储表示形式有两种:邻接矩阵、邻接表,下面分别是这两种存储表示形式的示例。
    一个无向图的邻接矩阵表示示例如下:

    一个无向图的邻接表表示示例如下:

    本文主要以一个带权有向图为例,使用邻接表进行存储表示,代码实现如下:

    /**
    * 带权有向图顶点数组节点结构
    **/
    type VertexArrayNode struct {
    	data byte          //顶点数据
    	link *EdgeListNode //出边链表
    }
    
    /**
    * 带权有向图出边链表节点结构
    **/
    type EdgeListNode struct {
    	vertexArrayIndex int           //顶点数组索引
    	weight           int           //权重值
    	next             *EdgeListNode //下一个指向节点
    }
    
    /**
    * 带权有向图结构
    **/
    type ListGraph struct {
    	slice []*VertexArrayNode //顶点数组
    	size  int                //顶点数量
    }
    
    /**
    * 创建空带权有向图
    **/
    func NewListGraph() *ListGraph {
    	return &ListGraph{
    		slice: make([]*VertexArrayNode, 10),
    		size:  0,
    	}
    }
    
    /**
    * 打印
    **/
    func (lg *ListGraph) Print() {
    	for i := 0; i < lg.size; i++ {
    		fmt.Print(string(lg.slice[i].data), ": (")
    		ptr := lg.slice[i].link
    		for ptr != nil {
    			fmt.Print("index:", ptr.vertexArrayIndex, ",weight:", ptr.weight, "  ")
    			ptr = ptr.next
    		}
    		fmt.Println(")")
    	}
    }
    

    三、图的顶点和边的插入与删除

    /**
    * 插入顶点
    **/
    func (lg *ListGraph) InsertVertex(data byte) error {
    	if lg.VertexIndex(data) != -1 {
    		return errors.New("节点已存在")
    	}
    	vertex := &VertexArrayNode{data: data}
    	lg.size++
    	if lg.size <= len(lg.slice) {
    		lg.slice[lg.size-1] = vertex
    	} else {
    		lg.slice = append(lg.slice, vertex)
    	}
    	return nil
    }
    
    /**
    * 删除顶点
    **/
    func (lg *ListGraph) RemoveVertex(data byte) error {
    	index := lg.VertexIndex(data)
    	if index == -1 {
    		return errors.New("节点不存在")
    	}
    	for i := index + 1; i < lg.size; i++ {
    		lg.slice[i-1] = lg.slice[i]
    	}
    	lg.size--
    	return nil
    }
    
    /**
    * 插入边
    **/
    func (lg *ListGraph) InsertEdge(from byte, to byte, weight int) error {
    	fromIndex := lg.VertexIndex(from)
    	toIndex := lg.VertexIndex(to)
    	if fromIndex == -1 || toIndex == -1 {
    		return errors.New("边节点不存在")
    	}
    	ptr := lg.slice[fromIndex].link
    	for ptr != nil {
    		if ptr.vertexArrayIndex == toIndex {
    			return errors.New("边已存在")
    		}
    		ptr = ptr.next
    	}
    	elNode := &EdgeListNode{
    		vertexArrayIndex: toIndex,
    		weight:           weight,
    		next:             lg.slice[fromIndex].link,
    	}
    	lg.slice[fromIndex].link = elNode
    	return nil
    }
    
    /**
    * 删除边
    **/
    func (lg *ListGraph) RemoveEdge(from byte, to byte) error {
    	fromIndex := lg.VertexIndex(from)
    	toIndex := lg.VertexIndex(to)
    	if fromIndex == -1 || toIndex == -1 {
    		return errors.New("边节点不存在")
    	}
    	ptr := lg.slice[fromIndex].link
    	parent := lg.slice[fromIndex].link
    	for ptr != nil {
    		if ptr.vertexArrayIndex == toIndex {
    			if parent == ptr {
    				lg.slice[fromIndex].link = ptr.next
    			} else {
    				parent.next = ptr.next
    			}
    		}
    		parent = ptr
    		ptr = ptr.next
    	}
    	return errors.New("边不存在")
    }
    
    /**
    * 返回某个节点的索引
    **/
    func (lg *ListGraph) VertexIndex(data byte) int {
    	for i := 0; i < lg.size; i++ {
    		if lg.slice[i].data == data {
    			return i
    		}
    	}
    	return -1
    }
    

    四、图的遍历

    1. 深度优先遍历
    算法描述:
    (1)在访问图中某一起始顶点V后,由V出发,访问它的任一邻接顶点W1;再从W1出发,访问与W1邻接但还没有访问过的顶点W2;然后再从W2出发,进行类似的访问操作,一直执行下去,直到到达所有邻接顶点都已被访问过的顶点U为止。
    (2)接着,往回退一步,退到前一次刚访问过的顶点,看看是否还有其它没有被访问过的邻接顶点。如果有,则访问此顶点,然后以此顶点为起始顶点进行(1)操作;如果没有,就再往回退一步进行搜索。
    (3)重复(1)和(2),直到连通图中所有顶点都被访问过为止。
    此过程为递归过程。
    代码实现:

    /**
    * 深度优先遍历
    **/
    func (lg *ListGraph) Dfs(start byte) {
    	index := lg.VertexIndex(start)
    	if index == -1 {
    		fmt.Println("节点不存在:", string(start))
    	}
    	visited := make([]int, lg.size) //访问标记
    	lg._dfs(index, visited)
    	fmt.Println()
    }
    
    /**
    * 深度优先遍历(递归,Dfs方法调用)
    **/
    func (lg *ListGraph) _dfs(startIndex int, visited []int) {
    	if visited[startIndex] == 1 { //避免重复访问
    		return
    	}
    	fmt.Print(string(lg.slice[startIndex].data), " ")
    	visited[startIndex] = 1
    	ptr := lg.slice[startIndex].link
    	for ptr != nil {
    		lg._dfs(ptr.vertexArrayIndex, visited)
    		ptr = ptr.next
    	}
    }
    

    2. 广度优先遍历
    算法描述:
    在访问了起始顶点V之后,由V出发,依次访问V的各个未被访问过的邻接顶点W1,W2,...,然后再顺序访问W1,W2,...的所有还未被访问过的邻接顶点。再从这些访问过的顶点出发,访问它们的所有还未被访问过的邻接顶点,...一直执行,直到图中所有顶点都被访问过为止。
    此过程是一种分层的搜索过程,不是递归过程,每向前走一步可能访问多个顶点,所以需要使用一个队列来存放依次访问的顶点。
    代码实现:

    /**
    * 广度优先遍历
    **/
    func (lg *ListGraph) Bfs(start byte) {
    	index := lg.VertexIndex(start)
    	if index == -1 {
    		fmt.Println("节点不存在:", string(start))
    	}
    	visited := make([]int, lg.size) //访问标记
    	lq := listQueue.NewListQueue()  //链表队列
    	lq.Push(index)
    	for !lq.IsEmpty() {
    		index, _ := lq.Pop()
    		if visited[index] == 1 { //已经访问过
    			continue
    		}
    		fmt.Print(string(lg.slice[index].data), " ")
    		visited[index] = 1
    		ptr := lg.slice[index].link
    		for ptr != nil {
    			lq.Push(ptr.vertexArrayIndex)
    			ptr = ptr.next
    		}
    	}
    	fmt.Println()
    }
    

    上面代码中使用的链表队列在我的另一篇博文有讲解:https://www.cnblogs.com/wujuntian/p/12263763.html,这里直接使用这个包。

    五、最小生成树
    在有n个顶点的带权值的网结构图中,选取n-1条边,使得将图中所有顶点连接起来的权值和最低。把构造连通网的最小代价生成树称为最小生成树。最小生成树的实现有两种经典算法。
    1. 普里姆算法
    基本思想:
    从顶点入手找边
    算法描述:
    (1)将所有顶点分组,出发点为第一组,其余所有节点为第二组。
    (2)在一端属于第一组,另一端属于第二组的边中选择一条权值最小的。
    (3)把这条边中原属于第二组的节点放入第一组中。
    (4)重复(2)和(3),直到第二组节点为空为止。
    代码实现:

    /**
    * 最小生成树(普里姆算法)
    **/
    func (lg *ListGraph) MinSpanTree_Prim(start byte) {
    	index := lg.VertexIndex(start)
    	if index == -1 {
    		fmt.Println("节点不存在:", string(start))
    	}
    	visited := make([]int, lg.size) //访问标记
    	visited[index] = 1
    	count := 0 //已找到的边数
    	var minIndex int
    	var minFrom, minTo byte
    	type fromTo struct {
    		from   byte
    		to     byte
    		weight int
    	}
    	edges := make([]fromTo, lg.size-1) //最小生成树的所有边
    	for count < lg.size-1 {            //需要找到lg.size-1条边
    		minWeight := 32767
    		for i := 0; i < lg.size; i++ { //从已访问过的节点找到通向未访问过的节点的最小权值路径
    			if visited[i] == 0 {
    				continue
    			}
    			ptr := lg.slice[i].link
    			for ptr != nil {
    				if visited[ptr.vertexArrayIndex] == 0 && ptr.weight < minWeight {
    					minWeight = ptr.weight
    					minIndex = ptr.vertexArrayIndex
    					minFrom = lg.slice[i].data
    					minTo = lg.slice[ptr.vertexArrayIndex].data
    				}
    				ptr = ptr.next
    			}
    		}
    		visited[minIndex] = 1
    		edges[count] = fromTo{
    			from:   minFrom,
    			to:     minTo,
    			weight: minWeight,
    		}
    		count++
    	}
    	sumWeight := 0
    	fmt.Println("最小生成树所有边:")
    	for i := 0; i < count; i++ {
    		fmt.Println("from:", string(edges[i].from), ", to:", string(edges[i].to), ", weight:", edges[i].weight)
    		sumWeight += edges[i].weight
    	}
    	fmt.Println("最小生成树路径权值和:", sumWeight)
    }
    

    2. 克鲁斯卡尔算法
    基本思想:
    从边入手找顶点
    算法描述:
    (1)先构造一个只有n个顶点的子图SG。
    (2)然后从权值最小的边开始,若它的加入不使SG中产生回路,则在SG上加上这条边。
    (3)反复执行第二步,直至加上n-1条边为止。

    六、从源点到其余各点的最短路径
    1. 问题描述
    给定一个带权有向图D与源点V,求从V到D中其它顶点的最短路径。
    2. 算法描述
    (1)将从V到其所有邻接顶点的边的权值作为从V到其所有邻接节点的路径值,不邻接的顶点的路径值暂时初始化为正无穷大。
    (2)从(1)中得到的所有路径值中选取一个最小值min,设其对应的顶点为W,则V到W的最短路径已确定。
    (3)从W出发,遍历其邻接的顶点W1,W2,...,若min+W到W1的边的权值<V到W1路径值,则更新V到W1的路径值为min+W到W1的边的权值。
    (4)重复(2)和(3),直至V到其它所有顶点的最短路径都已确定。
    3. 代码实现

    /**
    * 从源点到其余各点的最短路径
    **/
    func (lg *ListGraph) ShortestPath(start byte) {
    	index := lg.VertexIndex(start)
    	if index == -1 {
    		fmt.Println("节点不存在:", string(start))
    	}
    	weights := make([]int, lg.size) //源点到其余各点的路径长度
    	//初始化
    	for i := 0; i < lg.size; i++ {
    		weights[i] = 32767
    	}
    	weights[index] = 0
    	ptr := lg.slice[index].link
    	for ptr != nil {
    		weights[ptr.vertexArrayIndex] = ptr.weight
    		ptr = ptr.next
    	}
    	visited := make([]int, lg.size) //已确定最短路径的节点
    	visited[index] = 1
    	count := 1
    	var minIndex int
    	for count < lg.size { //需要确定lg.size个节点的最短路径
    		minWeight := 32767
    		for i := 0; i < lg.size; i++ { //找出源点通向节点的最短路径
    			if weights[i] < minWeight && visited[i] == 0 {
    				minWeight = weights[i]
    				minIndex = i
    			}
    		}
    		visited[minIndex] = 1 //确定节点最短路径
    		count++
    		ptr = lg.slice[minIndex].link
    		for ptr != nil { //以此最短路径节点为起点,更新其可以到达的各个节点的最短路径
    			if minWeight+ptr.weight < weights[ptr.vertexArrayIndex] && visited[ptr.vertexArrayIndex] == 0 {
    				weights[ptr.vertexArrayIndex] = minWeight + ptr.weight
    			}
    			ptr = ptr.next
    		}
    	}
    	for i := 0; i < lg.size; i++ {
    		fmt.Println(string(lg.slice[i].data), ": ", weights[i])
    	}
    }
    

    七、拓扑排序
    1. 问题描述
    需要完成多个任务,而任务与任务之间存在一定依赖关系,被依赖的任务需要先完成才能执行其它任务,求输出这些任务的一种符合要求的完成顺序。
    2. 算法描述
    (1)将这些任务作为顶点,任务之间的依赖关系作为有向边,被依赖者指向依赖者,构建一个有向图。
    (2)遍历有向图的所有顶点,计算所有顶点的入度,并将入度为0的顶点入栈(这里也可以使用队列)。
    (3)出栈一个入度为0的顶点,输出之。
    (4)遍历这个顶点的所有邻接顶点,将其邻接顶点的入度都减1,若入度已减为0,则将顶点入栈。
    (5)重复(3)和(4),直到出现以下情况之一:
    全部顶点都已输出,则拓扑排序完成。
    图中还有未输出的顶点,但已没有入度为0的节点,说明有向图中存在环,拓扑排序无法完成。
    3. 代码实现

    /**
    * 拓扑排序
    **/
    func (lg *ListGraph) Topological() {
    	pene := make([]int, lg.size)   //各个节点的入度
    	for i := 0; i < lg.size; i++ { //各个节点的入度初始化
    		ptr := lg.slice[i].link
    		for ptr != nil {
    			pene[ptr.vertexArrayIndex]++
    			ptr = ptr.next
    		}
    	}
    	as := arrayStack.NewArrayStack() //数组栈
    	for i := 0; i < lg.size; i++ {   //入度为0的节点索引入栈
    		if pene[i] == 0 {
    			as.Push(i)
    		}
    	}
    	var topoData []byte //拓扑排序序列
    	for !as.IsEmpty() {
    		index, _ := as.Pop()
    		topoData = append(topoData, lg.slice[index].data)
    		ptr := lg.slice[index].link
    		for ptr != nil { //更新所有到达节点的入度
    			index = ptr.vertexArrayIndex
    			pene[index]--
    			if pene[index] == 0 {
    				as.Push(index)
    			}
    			ptr = ptr.next
    		}
    	}
    	if len(topoData) < lg.size {
    		fmt.Println("有向图中存在回路,拓扑排序失败")
    	} else {
    		for i := 0; i < lg.size; i++ {
    			fmt.Print(string(topoData[i]), " ")
    		}
    	}
    }
    

    上面代码中使用的数组栈在我的另一篇博文有讲解:https://www.cnblogs.com/wujuntian/p/12263652.html,这里直接使用这个包。

  • 相关阅读:
    golang 数组的一些自问自答
    这辈子研究 我的闪存
    我写的诗 爱你等于爱自己
    个人创业相关资料 创业与投资文章
    公司商业模式案例讲座 公司商业模式
    1、开篇:公司的价值 企业管理线细化系列文章
    visual studio 2022 企业版 下载
    芝奇DDR4 5066Hz 8Gx2内存闲置 个人闲置物品买卖
    INF: How SQL Server Compares Strings with Trailing Spaces
    虚拟 DOM 到底是什么?
  • 原文地址:https://www.cnblogs.com/wujuntian/p/12294717.html
Copyright © 2011-2022 走看看