12
votes

Hello all :) Today I am refining my skills on graph theory and data structures. I decided to do a small project in C++ because it's been a while since I've worked in C++.

I want to make an adjacency list for a directed graph. In other words, something which looks like:

0-->1-->3
1-->2
2-->4
3-->
4-->

This would be a directed graph with V0 (vertex 0) having an edge to V1 and V3, V1 having an edge to V2, and V2 having an edge to V4, like this:

V0----->V1---->V2---->V4
 |
 |
 v
 V3 

I know that in order to do this, I will need to create an adjacency list in C++. An adjacency list is basically an array of linked lists. Okay, let's see some pseudo C++ code:

#include <stdio>
#include <iostream>
using namespace std;

struct graph{
//The graph is essentially an array of the adjList struct.  
node* List[];

};

struct adjList{
//A simple linked list which can contain an int at each node in the list.

};

struct node {
int vertex;
node* next;
};

int main() {
//insert cool graph theory sorting algorithm here
}

As you can tell, this pseudocode is currently far from the mark. And that is what i wanted some help with -- pointers and structs in C++ have never been my strong suit. First of all, this takes care of the vertices that a vertex points to -- but what about the vertex itself? How can I keep track of that vertex? When I loop over the array, it will do me no good to only know what vertices are being pointed to, rather than also knowing what points to them. The first element in each list should probably be that vertex, and then the elements after that are the vertices it points to. But then, how can I access this first element of the list in my main program? (sorry if this is convoluted or confusing, I would happy to rephrase).

I would like to be able to loop over this adjacency list to do some cool things with graphs. For example, to implement some graph theory algorithms (sorts, shortest paths, etc) using the adjacency list representation.

(Also, I had a question about the adjacency list. What is different than just using a list of arrays? Why can't I just have a list with an array at each element in the list?)

5

5 Answers

16
votes

You may use a vector in node, as a adjacency list.

class node {
  int value;
  vector<node*> neighbors;
 };

If the graph is known at compile time, you can use array, but it's "a little bit" harder. If you know just size of graph (at compile time) you can do something like that.

template<unsigned int N>
class graph {
  array<node, N> nodes;
 }

To add a neighbor, you doing something like that (do not forget numbering from zero):

nodes[i].neighbors.push_back(nodes+j); //or &nodes[j]

Of course, you can do no-pointer adjacency list and work "above" a table. Than you have vector<int> in node and you pushing number of neighbour. With both representation of the graph, you can realize all algorithms which use adjacency list.

And finally, I might add. Some use a list instead of a vector, because the removal is in O(1) time. Mistake. For most algorithms, the order in the adjacency list is not important. So you can erase any element from vector in O(1) time. Just swap it with last element, pop_back is O(1) complexity. Something like that:

if(i != number_of_last_element_in_list) //neighbors.size() - 1
 swap(neighbors[i], neighbor.back());
neighbors.pop_back();

Specific example (you have vector in node, C++11 (!)):

//creation of nodes, as previously
constexpr unsigned int N = 3;
array<node,N> nodes; //or array<node, 3> nodes;
//creating edge (adding neighbors), in the constructor, or somewhere
nodes[0].neighbors = {&nodes[1]};
nodes[1].neighbors = {&nodes[0], &nodes[1]};
//adding runtime, i,j from user, eg. i = 2, j = 0
nodes[i].neighbors.push_back(&nodes[j]); //nodes[2].neighbors = {&nodes[0]};

I believe it's clear. From 0 you can go to 1, from 1 to 0 and to itself, and (as in eg.) from 2 to 0. It's directed graph. If you want undirected, you should add to both nodes neighbour’s addresses. You can use numbers instead of pointers. vector<unsigned int> in class node and pushing back numbers, no addresses.


As we know, you do not need to use pointers. Here is an example of it, too.

When the number of vertexes may change, you can use vector of nodes (vector<node>) instead array, and just resizing. The rest remains unchanged. For example:

vector<node> nodes(n); //you have n nodes
nodes.emplace_back(); //you added new node, or .resize(n+1)
//here is place to runtime graph generate
//as previously, i,j from user, but now you have 'vector<unsigned int>' in node
nodes[i].neighbors.push_back(j);

But you can't erase a node, this breaches numbering! If you want to erase something, you should use list (list<node*>) of pointers. Otherwise you must keep non-existent vertexes. Here, the order matters!


Regarding the line nodes.emplace_back(); //adding node, It is safe with graph without pointers. If you want use pointers, you predominately shouldn't change size of graph. You can accidentally change address of some nodes, while adding vertex, when vector will be copied to new place (out of space).

One way to deal with it is using reserve, although you have to know maximal size of graph! But in fact I encourage you not to use vector to keep vertexes, when you are using pointers. If you don't know implementation, more safe could be self memory management (smart pointers eg. shared_ptr or just new).

node* const graph = new node[size]; //<-- It is your graph.
//Here no address change accidentally.

Using vector as adjacency list is always fine! There's no chance to change node's address.

3
votes

This may not be very general approach but thats how I handle adjacency list in most of the cases. C++ has STL library which supports a data structure for linked list named as list.

Say you have N nodes in the graph, create a linked list for every node.

list graph[N];

Now graph[i] represent the neighbours of node i. For every edge i to j, do

graph[i].push_back(j);

The best comfort is no handling of pointers so as segmentation fault errors.

For more reference http://www.cplusplus.com/reference/list/list/

1
votes

I suggest you adding in the node structure, the Adjacency List And define the graph structure as List of Nodes instead of List of Adjacency Lists :)

struct node {
    int vertex;
    node* next;
    adjList m_neighbors;
};
struct graph{
    //List of nodes
};
0
votes

I would recommend the more general and simple approach of using vector and pairs: #include #include

typedef std::pair<int, int> ii; /* the first int is for the data, and the second is for the weight of the Edge - Mostly usable for Dijkstra */
typedef std::vector<ii> vii;
typedef std::vector <vii> WeightedAdjList; /* Usable for Dijkstra -for example */
typedef std::vector<vi> AdjList; /*use this one for DFS/BFS */

Or alias style (>=C++11):

using ii = std::pair<int,int>;
using vii = std::vector<ii>;
using vi = std::vector<int>;
using WeightedAdjList = std::vector<vii>;
using AdjList = std::vector<vi>;

From here: using vector and pairs (from tejas's answer)

For additional information you can refer to a very good summary of topcoder: Power up c++ with STL

0
votes

My approach would be to use a hash map to store the list of nodes in the graph

class Graph {
private:
  unordered_map<uint64_t, Node> nodeList;
  ...
}

The map takes the node ID as key, and the node itself as value. This way you could search for a node in the graph in constant time.

The node contains the adjacency list, in this case as a c++11 vector. It could also be a linked list, although for this use case I would not see a difference in efficiency. Maybe the list would be better if you would like to keep it sorted somehow.

class Node{
    uint64_t id;     // Node ID
    vector<uint64_t> adjList;  
...
}

With this approach you have to go through the adjacency list and then search the map on the ID to get the node.

As an alternative, you could have a vector of pointers to the neighbor nodes itself. That would give you a direct access to the neighbor nodes, but then you could not use a map to keep all your nodes in the graph, and you would loose the possibility to search entries easily in your graph.

As you can see, there is a lot of trade-off decisions you have to make when implementing a graph, all depends on your use cases.