Trees & Graphs: Traversal & Backtracking
In life and in graphs, you either go deep and get lost or go wide and stay grounded. Both paths reveal something new.
Imagine you're in a maze. How do you find the exit? You have two primary strategies:
These two traversal algorithms, DFS and BFS, are the absolute foundation of almost every graph problem you will ever encounter.
Problem: Find if Path Exists in Graph (LeetCode Link): You are given a graph and two nodes, source and destination. Return true if a path exists from source to destination, or false otherwise. 💡 HintDoes it matter if you go deep or wide? As long as you explore systematically, you'll find the destination if it's reachable.
DFS goes as deep as possible down one path before backtracking. It's the more intuitive, "natural" way we often solve mazes. The key to implementing it is a Stack (or recursion, which internally uses a call stack).
visited set and a stack. Push the start_node onto the stack.node from the stack.node is our destination, we've found a path! Return true.node has not been visited:
false.def dfs(graph, start_node, end_node):
visited = set()
stack = [start_node]
while stack:
node = stack.pop()
if node == end_node:
return True
if node not in visited:
visited.add(node)
for neighbor in graph.get(node, []):
if neighbor not in visited:
stack.append(neighbor)
return False
public boolean dfs(Map<Integer, List<Integer>> graph, int startNode, int endNode) {
Set<Integer> visited = new HashSet<>();
Deque<Integer> stack = new ArrayDeque<>();
stack.push(startNode);
while (!stack.isEmpty()) {
int node = stack.pop();
if (node == endNode) return true;
if (!visited.contains(node)) {
visited.add(node);
for (int neighbor : graph.getOrDefault(node, new ArrayList<>())) {
if (!visited.contains(neighbor)) {
stack.push(neighbor);
}
}
}
}
return false;
}
bool dfs(const map<int, vector<int>>& graph, int startNode, int endNode) {
unordered_set<int> visited;
stack<int> s;
s.push(startNode);
while (!s.empty()) {
int node = s.top();
s.pop();
if (node == endNode) return true;
if (visited.find(node) == visited.end()) {
visited.insert(node);
if (graph.count(node)) {
for (int neighbor : graph.at(node)) {
if (visited.find(neighbor) == visited.end()) {
s.push(neighbor);
}
}
}
}
}
return false;
}
BFS explores layer by layer. It finds all nodes at distance 1, then all nodes at distance 2, and so on. This is useful for finding the shortest path. The key to implementing it is a Queue.
The implementation is nearly identical to DFS. You just swap the stack for a queue!
visited set and a queue. Add the start_node to the queue.node.node is our destination, return true.node has not been visited:
false.from collections import deque
def bfs(graph, start_node, end_node):
visited = set()
queue = deque([start_node])
while queue:
node = queue.popleft()
if node == end_node:
return True
if node not in visited:
visited.add(node)
for neighbor in graph.get(node, []):
if neighbor not in visited:
queue.append(neighbor)
return False
public boolean bfs(Map<Integer, List<Integer>> graph, int startNode, int endNode) {
Set<Integer> visited = new HashSet<>();
Queue<Integer> queue = new ArrayDeque<>();
queue.add(startNode);
while (!queue.isEmpty()) {
int node = queue.poll();
if (node == endNode) return true;
if (!visited.contains(node)) {
visited.add(node);
for (int neighbor : graph.getOrDefault(node, new ArrayList<>())) {
if (!visited.contains(neighbor)) {
queue.add(neighbor);
}
}
}
}
return false;
}
bool bfs(const map<int, vector<int>>& graph, int startNode, int endNode) {
unordered_set<int> visited;
queue<int> q;
q.push(startNode);
while (!q.empty()) {
int node = q.front();
q.pop();
if (node == endNode) return true;
if (visited.find(node) == visited.end()) {
visited.insert(node);
if (graph.count(node)) {
for (int neighbor : graph.at(node)) {
if (visited.find(neighbor) == visited.end()) {
q.push(neighbor);
}
}
}
}
}
return false;
}
BFS is fantastic for unweighted graphs, but what if the edges have weights or costs (e.g., distance, time)? For that, you need more advanced algorithms. Dijkstra's algorithm is the classic solution, which is essentially a BFS where the queue is replaced by a priority queue that always explores the path with the lowest current cost. For even more complex scenarios, like finding a path on a map with a target destination, the A* algorithm improves upon Dijkstra's by using a heuristic to guide its search toward the goal.