patterncppMinor
Undirected Unweighted Graph Implementation - C++
Viewed 0 times
implementationundirectedunweightedgraph
Problem
For a class we needed to implement an undirected unweighted graph that:
With the above being said, I opted for an adjacency matrix to represent the graph, as I already have to use a distance matrix so why not (though the change from matrix => list should be fairly trivial). I also implemented
To implement the
The main goal of this implementation, and this post for that matter, is to determine whether the logic in my methods are reasonable, specifically with BFS/DFS. I'd like to know if I'm missing anything or if there's some simplification I had not considered. Note the implementation does not really have error handling for invalid input, it is just to practice and experiment with!
Graph.h
Note the reason I'm using two dimensional
```
class Graph {
private:
int numVertices;
bool **adjacencyMatrix;
int **distanceMatrix;
bool distanceMatrixComputed;
void initAdjacencyMatrix();
void initDistanceMatrix();
bool computeDistanceMatrix();
std::unordered_map bfsWithDistance(int);
void dfsHelper(int, std::vector&, std::unordered_set&);
public:
Graph(int);
void add
- Maintains a distance matrix that can be printed
- Supports the computation of the graph's diameter (uses dist matrix)
- Prints the number of connected components and their included vertices
With the above being said, I opted for an adjacency matrix to represent the graph, as I already have to use a distance matrix so why not (though the change from matrix => list should be fairly trivial). I also implemented
- DFS from a given node
- BFS from a given node
- Shortest path. Returns the shortest number of edges between vertices
v1andv2if such a path exists and-1otherwise.
To implement the
shortest path I used a modified BFS that would increment a variable called distance that indicates how far the node at the front of the queue is away from the node we started at. Got the idea from a similar algorithm which solves the problem "Given a binary tree return a vector of vectors where each nested vector contains the values of each node at a particular level". Found here.The main goal of this implementation, and this post for that matter, is to determine whether the logic in my methods are reasonable, specifically with BFS/DFS. I'd like to know if I'm missing anything or if there's some simplification I had not considered. Note the implementation does not really have error handling for invalid input, it is just to practice and experiment with!
Graph.h
Note the reason I'm using two dimensional
bool/int arrays is because I didn't feel the overhead from std::vector was necessary, though in production it would be a good choice```
class Graph {
private:
int numVertices;
bool **adjacencyMatrix;
int **distanceMatrix;
bool distanceMatrixComputed;
void initAdjacencyMatrix();
void initDistanceMatrix();
bool computeDistanceMatrix();
std::unordered_map bfsWithDistance(int);
void dfsHelper(int, std::vector&, std::unordered_set&);
public:
Graph(int);
void add
Solution
General Comments
Separation of Concerns
This principle basically says your class should do either business logic or resource management not both. You break this principle as your
It would be better if you split these up into two different classes. Note: Them memory management can be done for you by simply using the
Rule of Three
Because you have not separated your concerns your resource management logic is flawed and your code broken (in terms of resource management).
You should look up the rule of three and implement it.
Move Semantics.
This is 2017. Move semantics have been around since 2011. You should definitely be thinking about how your objects are moved. Currently your classes can only be copied.
You should expand the rule of three to the rule of five on your code.
Distance calculator is half of dykstra
Your distance calculator is basically a simplification of dykstra. I did not read it thouroughly to make sure it works in all situations but I would check out "dykstra Algorithm" you will find many references on the web.
Visitor Pattern
Your
Code Review
Don't use
Using
If you allow shadowed variables you will eventually forget to add
The better solution is to not allow shadowed variables and make the compiler generate an error when you do have shadowed variables. Since you now never have shadowed variables there is never a reason to use
One Initialize per line
Just like normal variable declarations where you should only initialize one variable per line (we are human and we somebody probably has to read your code. You should arrange your initializer list so each member is on a separate line.
This not only increases general readability but quickly allows you to establish order (and verify that it is correct).
Never do manual memory management
Use
Here you forgot that
###Is this test not redundant.
while (!q.empty()) {
if (visited.find(q.front()) != visited.end()) {
q.pop();
continue;
}
If you have entered the loop. Then
Std::cout is not the only stream
Printing a graph should not change the state of the graph. So you should probably mark it const.
I would pass a stream as a parameter (it can default to std::cout). But the standard idiom for printing is using the
Separation of Concerns
This principle basically says your class should do either business logic or resource management not both. You break this principle as your
Graph object is responsible for maintaining a graph but also has to maintaining the memory (resources) associated with the graph (You try to dynamically manage adjacencyMatrix and distanceMatrix).It would be better if you split these up into two different classes. Note: Them memory management can be done for you by simply using the
std::vector<> to handle the resource management.Rule of Three
Because you have not separated your concerns your resource management logic is flawed and your code broken (in terms of resource management).
{
Graph one(15);
Graph two(one); // Compiler generated copy constructor is not
// doing what you actually need.
}
// Resulting in a double delete when these go out of scope.You should look up the rule of three and implement it.
Move Semantics.
This is 2017. Move semantics have been around since 2011. You should definitely be thinking about how your objects are moved. Currently your classes can only be copied.
You should expand the rule of three to the rule of five on your code.
Distance calculator is half of dykstra
Your distance calculator is basically a simplification of dykstra. I did not read it thouroughly to make sure it works in all situations but I would check out "dykstra Algorithm" you will find many references on the web.
Visitor Pattern
Your
DFS and BFS look like they probably work. Though I would look up the visitor pattern. It allows a more generic traversal of the graph and allows arbitrary actions to be taken at each node.Code Review
Don't use
this->new bool*[this->numVertices];Using
this-> hides errors and will result in more bugs. The only reason to actually use this-> is when you have shadowed variables and you want to distinguish between a local and a member.If you allow shadowed variables you will eventually forget to add
this-> and get the wrong one. The compiler will not be able to detect that you got the wrong one and thus you have introduced a bug.The better solution is to not allow shadowed variables and make the compiler generate an error when you do have shadowed variables. Since you now never have shadowed variables there is never a reason to use
this->.One Initialize per line
Just like normal variable declarations where you should only initialize one variable per line (we are human and we somebody probably has to read your code. You should arrange your initializer list so each member is on a separate line.
Graph::Graph(int inNumVertices): numVertices(MAX(inNumVertices, 0)), distanceMatrixComputed(false) {This not only increases general readability but quickly allows you to establish order (and verify that it is correct).
Graph::Graph(int inNumVertices)
: numVertices(MAX(inNumVertices, 0))
, distanceMatrixComputed(false)
{Never do manual memory management
/**
* Allocate memory for adjacency matrix
*/
void Graph::initAdjacencyMatrix() {
this->adjacencyMatrix = new bool*[this->numVertices];
for (int i = 0; i numVertices; ++i) {
this->adjacencyMatrix[i] = new bool[this->numVertices];
}
}Use
std::vector they have everything you need built in.Here you forgot that
new can fail. In which case you will end up leaking a lot of memory (if it is not the first new that fails).###Is this test not redundant.
while (!q.empty()) {
if (visited.find(q.front()) != visited.end()) {
q.pop();
continue;
}
If you have entered the loop. Then
q is not empty. If q is not empty then front() will not be end().Std::cout is not the only stream
void Graph::printComponents() {Printing a graph should not change the state of the graph. So you should probably mark it const.
I would pass a stream as a parameter (it can default to std::cout). But the standard idiom for printing is using the
operator<<. So you should also define one of those.void Graph::printComponents(std::ostream& str = std::cout) const;
friend std::ostream& operator<<(std::ostream& str, Graph const& data)
{
data.printComponents(str);
return str;
}Code Snippets
{
Graph one(15);
Graph two(one); // Compiler generated copy constructor is not
// doing what you actually need.
}
// Resulting in a double delete when these go out of scope.new bool*[this->numVertices];Graph::Graph(int inNumVertices): numVertices(MAX(inNumVertices, 0)), distanceMatrixComputed(false) {Graph::Graph(int inNumVertices)
: numVertices(MAX(inNumVertices, 0))
, distanceMatrixComputed(false)
{/**
* Allocate memory for adjacency matrix
*/
void Graph::initAdjacencyMatrix() {
this->adjacencyMatrix = new bool*[this->numVertices];
for (int i = 0; i < this->numVertices; ++i) {
this->adjacencyMatrix[i] = new bool[this->numVertices];
}
}Context
StackExchange Code Review Q#160051, answer score: 4
Revisions (0)
No revisions yet.