DevTools Hub

Search tools

Search for a developer tool

Tree vs Graph

Part of the Algorithms Toolkit

A tree isn't a different data structure from a graph — it's a graph with three extra promises kept: no cycles, everything reachable, and exactly one fewer edge than nodes. Every algorithm difference between them — why tree traversal never needs a visited set and graph traversal always does, why "shortest path" is a trivial question in a tree and a genuinely hard one in a graph — falls directly out of which of those three promises gets broken.

The exact definition, not the fuzzy one

A tree with n nodes is a graph that is simultaneously:

  • Connected — every node is reachable from every other node.
  • Acyclic — no path leads back to a node you've already visited.
  • Exactly n − 1 edges — not a coincidence, a consequence: any two of these three properties force the third. A connected, acyclic graph always has exactly n − 1 edges; add one more edge to a tree and you're mathematically guaranteed to create exactly one cycle, because there was already exactly one path between every pair of nodes and the new edge just opened a second one.
Tree5 nodes, 4 edges
ABCDE

Exactly one path between any two nodes — no cycle possible.

Graph5 nodes, 5 edges
ABCDE

One extra edge (C–E) creates a second path between B and C — a cycle, and no longer a tree.

A general graph keeps none of these promises. It can have cycles, it can be split into disconnected pieces, its edge count can range anywhere from 0 up to roughly , and — the one most people forget — its edges can point in a direction. Every one of the specific differences below is one of these three constraints being lifted.

Traversal: why graphs need a visited set and trees never do

Because a tree has exactly one path between any two nodes, walking from the root can never revisit a node — there's structurally nowhere for a second visit to come from. Recursive tree traversal code reflects this directly:

function inorder(node) {
  if (!node) return;
  inorder(node.left);
  visit(node.value);
  inorder(node.right);
}   // no "visited" set anywhere — a tree can't loop back on itself

A graph offers no such guarantee — a cycle means a naive traversal can walk the same loop forever. Both breadth-first and depth-first search on a graph carry a visited set specifically to break that loop, and it's not optional:

function bfs(graph, start) {
  const visited = new Set([start]); // required — a cycle without this never terminates
  const queue = [start];
  while (queue.length) {
    const node = queue.shift();
    for (const neighbor of graph[node]) {
      if (!visited.has(neighbor)) {
        visited.add(neighbor);
        queue.push(neighbor);
      }
    }
  }
}

Graph Traversal Visualizer runs BFS and DFS on a real generated graph — and its own generator makes the tree/graph relationship concrete rather than theoretical: it builds a random spanning tree first (every node connected, zero cycles, exactly n − 1 edges by construction), then adds a handful of extra random edges specifically to introduce the cycles a plain tree can never have.

Shortest path: trivial question vs. real algorithm

In a tree, "what's the shortest path between B and C" isn't really a question — there's exactly one path, so you just walk it. There's nothing to optimize because there's nothing to choose between.

In a graph, multiple paths between the same two nodes are the normal case, and each can have a different total length or weight — which is precisely why Dijkstra's algorithm and Bellman-Ford exist at all. Shortest Path Visualizer runs both on the same weighted graph, including the case with negative edge weights where Dijkstra's greedy assumption breaks and gives a visibly wrong answer — a failure mode that has no equivalent in a tree, because a tree never gives you more than one candidate path to compare in the first place.

Directed graphs, and the DAG that looks like a tree but isn't

A tree is normally drawn as if undirected, but in practice it's used with an implicit direction — parent to child — formalized as a rooted tree: every edge points away from a single root, and critically, every node has exactly one parent (the root has none). Binary Search Tree Visualizer and Heap Visualizer are both rooted trees in exactly this sense.

A directed acyclic graph (DAG) keeps the "no cycles" promise but drops the "exactly one parent" one — and that single difference is enough to stop it from being a tree, even though it's acyclic. A git commit history is the clearest real example: it's a DAG, not a tree, precisely because a merge commit has two parents — something structurally impossible in a tree. Build-dependency graphs (two packages both depending on the same shared library) and spreadsheet formula dependencies (two cells both referencing the same source cell) are DAGs for the identical reason: a shared dependency is a second parent, and a second parent is a cycle-free graph that a tree definition explicitly forbids.

Representation: pointers vs. adjacency

A tree's representation follows straight from "every node has at most one parent" — each node just needs pointers to its children (left/right for a BST), or, when the tree's shape is predictable enough (a heap is always a complete tree), array index arithmetic instead of pointers at all — see Array vs Linked List for why that predictability is what makes the array version possible.

A graph has no such shape guarantee, so it needs a representation that can describe any connection pattern. The two standard choices trade space for lookup speed:

Adjacency listAdjacency matrix
SpaceO(V + E) — proportional to actual edgesO(V²) — fixed, regardless of edge count
"Are A and B connected?"O(degree of A) — scan A's neighbor listO(1) — direct index into the grid
Best forSparse graphs (most real-world graphs)Dense graphs, or when O(1) edge lookup matters most

Graph Traversal Visualizer uses an adjacency list internally — the standard choice, since most real graphs (social networks, road networks, dependency graphs) are sparse: a graph with a million nodes rarely has anywhere close to a million² edges.

Complexity: why graph traversal costs O(V + E), not just O(V)

A balanced tree's search cost is O(log n) — bounded purely by node count, because a tree's edge count is always exactly n − 1; there's no independent variable to add to the complexity. A graph traversal's cost is O(V + E) — two independent terms, because a graph's edge count isn't determined by its node count at all. It can be as low as V − 1 (a tree-shaped graph) or as high as roughly (nearly every node connected to every other), and BFS/DFS has to examine every edge at least once regardless — which is exactly why the complexity has to carry both terms instead of collapsing to one.

FAQ

Is a linked list a tree?

Technically yes — a linked list is the degenerate case of a tree where every node has at most one child, making it simultaneously a valid tree and a straight line. It's a useful mental check for the definition: connected, acyclic, n − 1 edges — a linked list of 5 nodes has exactly 4 edges and satisfies all three.

Can a tree have a cycle if you just don't traverse into it?

No — this isn't a traversal-time property, it's structural. A graph containing any cycle anywhere is not a tree, full stop, regardless of whether a particular traversal happens to avoid that part of it.

Is every acyclic graph a tree?

No, and this is the mistake worth remembering above all others in this post: acyclic only rules out cycles, not multiple parents or disconnection. A DAG is acyclic and is deliberately not a tree the moment any node has two incoming edges — see the git commit history example above. A forest (several separate trees, no edges between them) is also fully acyclic and still isn't a tree, because it fails the "connected" requirement.

Is anything I enter into these tools sent anywhere?

No — Graph Traversal Visualizer, Shortest Path Visualizer, Binary Search Tree Visualizer, and Heap Visualizer all run entirely in your browser.

Related tools