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] = 1
e é 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] = 1
ou 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^2
Grá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 0
valores e desperdiçará espaço.
E ≈ N^2
grá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
- Selecione um nó para enfileirar
- Nó fora da fila
- Se a fila estiver vazia, significa que a travessia foi concluída e retorna diretamente
- Enfileirar todos os vizinhos não visitados de um nó
- Retorno de chamada de execução (pode ser uma comparação de igualdade para pesquisas)
- 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.