Estruturas de dados Golang: gráficos

Este artigo apresenta brevemente 2 implementações de gráficos e sua travessia BFS. Referência: gráfico de estrutura de dados golang

livros de referência

Análise de Estruturas de Dados e Algoritmos: Descrição da Linguagem C

"Solução ideal para algoritmos e estruturas de dados"

foto

A estrutura de dados do gráfico é uma abstração da estrutura de rede. Existem muitos exemplos na vida real, como redes de rotas de voo e redes sociais. Para conceitos básicos como nós, arestas, pesos, direcionados e não direcionados, e conectividade forte e fraca de grafos, consulte o Capítulo 8 do primeiro livro.

matriz de adjacência

Para a aresta do nó u ao nó v (u, v), ela pode ser expressa como A[u][v] = 1e é 0 se não estiver diretamente conectada. A matriz de adjacência correspondente à figura acima é a seguinte:

A matriz do gráfico acima descreve completamente a conectividade do gráfico. Por ser um grafo não direcionado, a matriz de adjacência é simétrica em relação à diagonal principal : A[u, v] = 1ou seja A[v, u] = 1, correspondente à implementação do código, é um array bidimensional ou estrutura de mapa.

lista de adjacências

Para cada nó, os nós diretamente conectados a ele são armazenados na estrutura da tabela, e a lista de adjacências correspondente à figura acima é a seguinte:

As setas na figura acima podem representar um gráfico direcionado e fatias ou listas vinculadas podem ser usadas para armazenar nós de conexão na implementação.

Escolha a estrutura de armazenamento

Selecione a estrutura de armazenamento de acordo com a densidade do grafo , assumindo que o grafo possui N nós e E arestas, se:

E << N^2Gráfico esparso com poucas interseções

Usar a lista de adjacência para armazenar apenas nós conectados economiza espaço de armazenamento; usar a matriz de adjacência armazenará um grande número de 0valores e desperdiçará espaço.

E ≈ N^2gráfico denso com muitas interseções

Usar a matriz de adjacência será muito conveniente para consultar a conectividade e reduzir o desperdício de espaço de armazenamento. A lista de adjacências procurará problemas.

Realização do gráfico

Os gráficos possuem 2 operações básicas: AddNode()adicionar nós e AddEdge()conectar nós para formar arestas.

definição básica

type Node struct {
    
    
	value int
}

type Graph struct {
    
    
	nodes []*Node          // 节点集
	edges map[Node][]*Node // 邻接表表示的无向图
	lock  sync.RWMutex     // 保证线程安全
}

implementação da operação

// 增加节点
func (g *Graph) AddNode(n *Node) {
    
    
	g.lock.Lock()
	defer g.lock.Unlock()
	g.nodes = append(g.nodes, n)
}

// 增加边
func (g *Graph) AddEdge(u, v *Node) {
    
    
	g.lock.Lock()
	defer g.lock.Unlock()
	// 首次建立图
	if g.edges == nil {
    
    
		g.edges = make(map[Node][]*Node)
	}
	g.edges[*u] = append(g.edges[*u], v) // 建立 u->v 的边
	g.edges[*v] = append(g.edges[*v], u) // 由于是无向图,同时存在 v->u 的边
}

// 输出图
func (g *Graph) String() {
    
    
	g.lock.RLock()
	defer g.lock.RUnlock()
	str := ""
	for _, iNode := range g.nodes {
    
    
		str += iNode.String() + " -> "
		nexts := g.edges[*iNode]
		for _, next := range nexts {
    
    
			str += next.String() + " "
		}
		str += "\n"
	}
	fmt.Println(str)
}

// 输出节点
func (n *Node) String() string {
    
    
	return fmt.Sprintf("%v", n.value)
}

teste

package graph

import "testing"

func TestAdd(t *testing.T) {
    
    

	g := Graph{
    
    }
	n1, n2, n3, n4, n5 := Node{
    
    1}, Node{
    
    2}, Node{
    
    3}, Node{
    
    4}, Node{
    
    5}

	g.AddNode(&n1)
	g.AddNode(&n2)
	g.AddNode(&n3)
	g.AddNode(&n4)
	g.AddNode(&n5)

	g.AddEdge(&n1, &n2)
	g.AddEdge(&n1, &n5)
	g.AddEdge(&n2, &n3)
	g.AddEdge(&n2, &n4)
	g.AddEdge(&n2, &n5)
	g.AddEdge(&n3, &n4)
	g.AddEdge(&n4, &n5)

	g.String()
}

O teste foi bem-sucedido: use a lista de adjacências para representar o gráfico não direcionado acima

BFS: Primeira Pesquisa Ampla

BFS (Breadth First Search): Amplitude First Search, largura refere-se à travessia divergente dos nós circundantes a partir de um nó. A partir de um determinado nó, visite todos os seus nós adjacentes e, a partir desses nós, visite seus nós adjacentes não visitados... até que todos os nós sejam visitados.

É um pouco semelhante ao percurso em ordem de árvore, mas existem loops no gráfico e os nós visitados podem ser visitados novamente, portanto, é necessária uma fila auxiliar para armazenar os nós adjacentes a serem visitados.

processo transversal

  1. Selecione um nó para enfileirar
  2. Nó fora da fila
    1. Se a fila estiver vazia, significa que a travessia foi concluída e retorna diretamente
    2. Enfileirar todos os vizinhos não visitados de um nó
  3. Retorno de chamada de execução (pode ser uma comparação de igualdade para pesquisas)
  4. Repita o passo 2

Código

package graph

import "sync"

type NodeQueue struct {
    
    
	nodes []Node
	lock  sync.RWMutex
}

// 实现 BFS 遍历
func (g *Graph) BFS(f func(node *Node)) {
    
    
	g.lock.RLock()
	defer g.lock.RUnlock()

	// 初始化队列
	q := NewNodeQueue()
	// 取图的第一个节点入队列
	head := g.nodes[0]
	q.Enqueue(*head)
	// 标识节点是否已经被访问过
	visited := make(map[*Node]bool)
	visited[head] = true
	// 遍历所有节点直到队列为空
	for {
    
    
		if q.IsEmpty() {
    
    
			break
		}
		node := q.Dequeue()
		visited[node] = true
		nexts := g.edges[*node]
		// 将所有未访问过的邻接节点入队列
		for _, next := range nexts {
    
    
			// 如果节点已被访问过
			if visited[next] {
    
    
				continue
			}
			q.Enqueue(*next)
			visited[next] = true
		}
		// 对每个正在遍历的节点执行回调
		if f != nil {
    
    
			f(node)
		}
	}
}

// 生成节点队列
func NewNodeQueue() *NodeQueue {
    
    
	q := NodeQueue{
    
    }
	q.lock.Lock()
	defer q.lock.Unlock()
	q.nodes = []Node{
    
    }
	return &q
}

// 入队列
func (q *NodeQueue) Enqueue(n Node) {
    
    
	q.lock.Lock()
	defer q.lock.Unlock()
	q.nodes = append(q.nodes, n)
}

// 出队列
func (q *NodeQueue) Dequeue() *Node {
    
    
	q.lock.Lock()
	defer q.lock.Unlock()
	node := q.nodes[0]
	q.nodes = q.nodes[1:]
	return &node
}

// 判空
func (q *NodeQueue) IsEmpty() bool {
    
    
	q.lock.RLock()
	defer q.lock.RUnlock()
	return len(q.nodes) == 0
}

teste

func TestBFS(t *testing.T)  {
    
    
	g.BFS(func(node *Node) {
    
    
		fmt.Printf("[Current Traverse Node]: %v\n", node)
	})
}

Sucesso no teste:

  • Primeiro visite o nó 1, depois visite 2 e 5 adjacente a 1, neste momento 1, 2, 5 são marcados como visitados
  • Em seguida, atravesse os nós adjacentes do nó 2 que não foram visitados: 3, 4
  • Neste ponto, todos os nós foram visitados e a fila está vazia. fim da travessia

Análise de Complexidade

complexidade de tempo

Para um gráfico com N nós e E arestas, os nós e cada aresta são percorridos uma vez. A complexidade do tempo é O (N + E)

complexidade do espaço

Para gráficos divergentes, a fila auxiliar armazenará até N nós. A complexidade do espaço é O(N)

Resumir

Na verdade, existem dois tipos de travessia de gráfico: BFS e DFS, o primeiro usa filas auxiliares para armazenar nós temporariamente e o último usa chamadas recursivas de pilha. Ambos têm suas próprias vantagens e desvantagens. Por exemplo, o BFS pode controlar o comprimento da fila. Ao contrário do DFS, não é fácil controlar o tamanho da pilha. O DFS é adequado para travessia pré-ordenada de gráficos e árvores, e irá ser estudado no capítulo das árvores.

Acho que você gosta

Origin blog.csdn.net/qq_24694139/article/details/131663748
Recomendado
Clasificación