Introduction to Graph Data Structure in Golang

Photo by JJ Ying on Unsplash

Introduction to Graph Data Structure in Golang

·

5 min read

Graphs are non-linear data structure, which contains nodes (vertices) connected by edges. Graphs have enough amount of applications which makes it a interesting topic to cover and also mostly asked in interviews.

Today we will talk about how to go about implementing graph data structure in golang. The graphs can either be undirected or directed, if the graph is directed we can only travel from one node to another not the other way. The undirected graphs allow you to move back and forth from every connected nodes (vertices).

Graphs as Adjacency List

Graphs can be represented in many forms, we will go with adjacency list approach which is a map having keys representing the nodes and values representing array of nodes that it is connected to or can traverse to which are called neighbor nodes.

Now, let's get into the interesting part by writing some go code. Here we will go about representing graph in adjacency list;

package main

type AdjacencyList[T comparable] map[T][]T
// here "T" is a generic of type "comparable interface"

func main() {
    al := AdjacencyList[string]{
        "a": {"b", "c", "d", "g"},
        "b": {"e"},
        "c": {"e", "g"},
        "d": {"f"},
        "e": {},
        "f": {},
        "g": {}
    }
}

The graph mentioned the above code can be visually shown as below,

Let's analyze the graph and try to understand it,

  • From node a, you can traverse to nodes c, d, b and g, so all these nodes are neighbors of node a.

  • From node b, you can only traverse to node e, so node e is the neighbor of node b. Same way we can say node f being neighbor of node d.

Now we know how to represent graph data structure in adjacency list, what's next. Next thought that will cross our mind immediately is how to traverse through graphs.

Traversing through Graphs

In graphs, when traverse you start from node which called source and from there we can move its neighbors. We will go through two most common ways we could traverse through graph which are;

  1. Depth first traversal

  2. Breadth first traversal

  1. Depth First Traversal

In depth first traversal, we move from node to its neighbor and then from the neighbor we move to its neighbor and so on until there are no more neighbors to traverse to which in turn completes the traversal.

We can implement depth first traversal, by making use of stack. Now let's say we have a source node from where we start the traversal, the process will be;

  • Add source node to stack.

  • Pop the stack which gives us the current node.

  • Loop through the neighbor nodes of the current node and push them on to the stack and we will also make sure that we don't revisit the nodes we already traversed through.

  • Repeat the steps from popping the stack.

package main

import (
    "fmt"

    "github.com/aseerkt/go-dsa/pkg/stack"
)

func (al *AdjacencyList[V]) DepthFirstPrint(src V) {
    st := stack.New()
    st.Push(src)

    visited := make(map[V]bool)

    for !st.IsEmpty() {
        current := st.Pop()

        fmt.Println(current)

        visited[current] = true

        neighbors := (*al)[current]

        for _, neighbor := range neighbors {
            if !visited[neighbor] {
                st.Push(neighbor)
                visited[neighbor] = true
            }
        }
    }
}
  1. Breadth First Traversal

In breadth first traversal, we cover all neighbors of the current node before moving onto the neighbors of neighbor of the current node.

We can implement breadth first traversal by making use queue instead of stack unlike depth first traversal. So the process will be;

  • Add source node to queue.

  • Dequeue the queue which gives us the current node.

  • Loop through the neighbors of current node and enqueue them and make sure we don't revisit the nodes which we already visited.

  • Repeat the step from dequeue the queue.

package main

import (
    "fmt"

    "github.com/aseerkt/go-dsa/pkg/queue"
)

func (al *AdjacencyList[V]) BreadthFirstPrint(src V) {
    qu := queue.New()
    st.Enqueue(src)

    visited := make(map[V]bool)

    for !qu.IsEmpty() {
        current := qu.Dequeue()

        fmt.Println(current)

        visited[current] = true

        neighbors := (*al)[current]

        for _, neighbor := range neighbors {
            if !visited[neighbor] {
                queue.Enqueue(neighbor)
                visited[neighbor] = true
            }
        }
    }
}

Now we know how to traverse through nodes of graph, we can get into more complex problems which are based on the graph.

Summary

  • Adjacency list is a way of representing graphs, where the keys are all nodes in the graphs and the values are the nodes (neighbors) to which we can traverse from the key node.

  • There are mainly two ways you can traverse through graphs, which are depth first and breadth first traversals.

  • In depth first traversal, we keep on moving towards neighbor of immediate neighbor until it reaches the end where there are no more neighbors.

  • In breadth first traversal. we traverse through all neighbors of the current node before moving on to the neighbors of the neighbor node.

All in one snippet;

package main

import (
    "fmt"

    "github.com/aseerkt/go-dsa/pkg/stack"
    "github.com/aseerkt/go-dsa/pkg/queue"
)

type AdjacencyList[V comparable] map[V][]V

func (al *AdjacencyList[V]) DepthFirstPrint(src V) {
    st := stack.New()
    st.Push(src)

    visited := make(map[V]bool)

    for !st.IsEmpty() {
        current := st.Pop()

        fmt.Println(current)

        visited[current] = true

        neighbors := (*al)[current]

        for _, neighbor := range neighbors {
            if !visited[neighbor] {
                st.Push(neighbor)
                visited[neighbor] = true
            }
        }
    }
}

func (al *AdjacencyList[V]) BreadthFirstPrint(src V) {
    qu := queue.New()
    st.Enqueue(src)

    visited := make(map[V]bool)

    for !qu.IsEmpty() {
        current := qu.Dequeue()

        fmt.Println(current)

        visited[current] = true

        neighbors := (*al)[current]

        for _, neighbor := range neighbors {
            if !visited[neighbor] {
                queue.Enqueue(neighbor)
                visited[neighbor] = true
            }
        }
    }
}

func main() {
   al := AdjacencyList[string]{
        "a": {"b", "c", "d", "g"},
        "b": {"e"},
        "c": {"e", "g"},
        "d": {"f"},
        "e": {},
        "f": {},
        "g": {}
    }

    al.DepthFirstPrint("a")
    // PRINT => a, b, e, c, g, d, f
    al.BreadthFirstPrint("a") 
    // PRINT => a, b, c, d, g, e, f
}

Good job making it this far. This will be enough for you to get started with graphs in Golang. For full source code, you can visit my GitHub repository go-dsa and leave a star.

I hope this would have given you an idea of how to get started with graphs in golang. The main important takeaway points should be the concepts about graphs which are language agnostics. Share your approach with me about how implemented graphs in you favorite programming language. Up until then let's see again with some new content. Stay tuned. Adios.