Skip to Content
Course content

213: Implementing Data Structures from Scratch: Trees and Graphs

Click on the "Edit" button in the top corner of the screen to edit your slide content.

How do I actually structure a tree node without creating a memory leak nightmare?

When you first start building trees, the instinct is to use raw pointers. But in a real C++ project, raw new and delete calls are a recipe for disaster, especially when you have a recursive structure where one missing delete in a destructor cascades into a massive leak.

I always recommend std::unique_ptr for tree nodes. Since a parent "owns" its children, unique_ptr perfectly models this relationship. When the root is destroyed, it triggers a chain reaction that cleans up the entire tree automatically. Here is how I'd set up a basic node for something like a company org chart:

#include <memory>
#include <string>
#include <vector>

struct Employee {
    std::string name;
    // Using a vector of unique_ptrs allows for an N-ary tree (multiple subordinates)
    std::vector<std::unique_ptr<Employee>> subordinates;

    Employee(std::string n) : name(n) {}
    
    void addSubordinate(std::unique_ptr<Employee> emp) {
        subordinates.push_back(std::move(emp));
    }
};

Notice the std::move. Since unique_ptr cannot be copied, you have to explicitly transfer ownership of the employee node into the subordinates list. It feels a bit clunky at first, but it forces you to be honest about who owns what memory.

What is the most efficient way to represent a graph in C++?

You'll see two main options: Adjacency Matrices and Adjacency Lists. If you're building a dense graph (where almost every node connects to every other node), a matrix (a 2D array) is fine. But in the real world—think of a map of city intersections or a social network—graphs are "sparse." Using a matrix for a city map would waste gigabytes of RAM on zeros.

I almost always go with an Adjacency List. I personally prefer using a std::unordered_map where the key is the node ID and the value is a std::vector of neighbors. This gives you O(1) lookup to find a node's neighbors while keeping the memory footprint lean.

#include <unordered_map>
#include <vector>
#include <string>

class CityMap {
    // Maps a city name to a list of cities it's connected to
    std::unordered_map<std::string, std::vector<std::string>> adjList;

public:
    void addRoad(const std::string& u, const std::string& v) {
        adjList[u].push_back(v);
        adjList[v].push_back(u); // Assuming roads are bidirectional
    }

    const std::vector<std::string>& getConnections(const std::string& city) {
        return adjList[city];
    }
};

How do I stop a graph traversal from running in an infinite loop?

Unlike trees, graphs can have cycles. If you write a simple recursive Depth-First Search (DFS) and your graph has a loop (A → B → C → A), your program will just keep spinning until the stack overflows. It's a classic mistake.

The fix is a "visited" set. You need to keep track of every node you've already touched. I use a std::unordered_set for this because it tells me in constant time if I've been to a city already. Here is a concise way to implement a DFS that actually terminates:

#include <iostream>
#include <unordered_set>
#include <string>

void exploreCity(const std::string& current, 
                 CityMap& map, 
                 std::unordered_set<std::string>& visited) {
    // Base case: if we've already been here, turn back
    if (visited.find(current) != visited.end()) return;

    std::cout < Reproductive "Visiting: " < current < "\n";
    visited.insert(current); // Mark as visited

    for (const auto& neighbor : map.getConnections(current)) {
        exploreCity(neighbor, map, visited);
    }
}

By passing the visited set by reference, every recursive call shares the same record of where the traversal has been. If you accidentally pass it by value, you'll find your code still looping because every function call gets its own fresh, empty set.




📋 Practical Task

Exercise: Build a Package Dependency Resolver

In software engineering, build systems (like Make or CMake) use a Directed Acyclic Graph (DAG) to determine the order in which libraries should be compiled. If Library A depends on Library B, B must be compiled first.

Your Task: Implement a DependencyGraph class that handles the following:

  • A method addDependency(string package, string dependsOn) to build the graph.
  • A method resolveOrder(string targetPackage) that performs a Depth-First Search to print the correct compilation order.

Requirements:
1. Use a std::unordered_map<std::string, std::vector<std::string>> for the adjacency list.
2. Ensure that a package is only printed after all of its dependencies have been printed (this is essentially a Topological Sort).
3. Use a std::unordered_set to track visited nodes to avoid redundant processing.

Test Case:
If you add dependencies: ("Engine", "Core"), ("Engine", "Renderer"), ("Renderer", "Core"), ("App", "Engine").
Resolving "App" should output a valid order, such as: Core, Renderer, Engine, App.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.