Skip to main content

Cycle Detection in Undirected Graphs Using Union-Find

Complete Java solution for detecting cycles in undirected graphs using Union-Find disjoint set data structure with O(E α(V)) time complexity.

Cycle detection in undirected graphs is a fundamental problem that can be efficiently solved using the Union-Find data structure, which provides optimal time complexity for this canonical graph algorithm problem.

1. Pattern Restatement

Clarifying questions to ask the interviewer:

  1. Are we guaranteed that the input will be a valid undirected graph representation (e.g., adjacency list or matrix), or do we need to handle malformed inputs?
  2. Can the graph have self-loops or multiple edges between the same pair of nodes?
  3. Should we return any cycle found, or just determine whether a cycle exists?
  4. Are we allowed to modify the input graph structure during our algorithm?
  5. What is the expected time complexity for follow-up optimization (e.g., O(n) vs O(n log n))?

Restate the pattern or problem type in your own words: The cycle detection in undirected graphs pattern involves identifying whether a graph contains any cycles (a path that starts and ends at the same node with at least three distinct vertices). This is a fundamental graph traversal problem where we must detect when a traversal revisits a node that was already visited in the current path.

Clarify the signal words and constraint shape that point to it:

  • Signal words: "graph", "undirected", "cycle", "loop", "detect", "find"
  • Constraint shape: Unweighted, bidirectional connections between nodes, where the same edge can be traversed in both directions
  • The problem typically asks for a boolean result (cycle exists or not) or to return the actual cycle path

Note any assumptions you are making:

  • The graph is represented as an adjacency list or equivalent structure
  • Nodes are uniquely identifiable (typically integers or hashable objects)
  • We're not dealing with disconnected components unless specified
  • Undirected edges mean we can traverse in either direction between connected nodes

Identify whether the pattern is best suited for static, streaming, or online scenarios if relevant: This pattern is primarily suited for static graph problems where the entire graph structure is available upfront. It's not naturally suited for streaming scenarios where edges arrive one at a time and we must continuously update our cycle detection state. For streaming graphs, we'd likely want to use a dynamic graph data structure or incremental algorithms that can handle edge additions/removals efficiently.

2. When to Use This Pattern

Signal words and constraint shapes:

  • "Detect cycles" or "find loops" in graph structures
  • "Undirected graph" or "connected components"
  • "Return whether a cycle exists" or "identify if there's a redundant connection"
  • Graphs with "edges that form closed loops" or "multiple paths between nodes"

Why this pattern fits better than alternatives: This pattern is specifically designed for undirected graph cycle detection, where the same edge can be traversed in both directions. Unlike directed graphs (which require DFS with visited states), undirected graphs need a different approach to avoid false positives from backtracking. The Union-Find (Disjoint Set) or DFS with parent tracking approaches are optimal here because they handle bidirectional edges properly.

Conditions where variations apply:

  • Directed graphs: Use DFS with three-color marking (white-gray-black) to detect back edges
  • Streaming graphs: Use dynamic Union-Find with path compression for real-time cycle detection
  • Multiple connected components: Extend the approach to check each component independently

Core insight: In an undirected graph, a cycle exists if and only if we encounter a back edge that connects to a previously visited node that is not the immediate parent. This differs from directed graphs where we must track all visited nodes in current DFS path.

The key insight is that in undirected graphs, we can detect cycles by using Union-Find or DFS with parent tracking to avoid counting the same edge twice in opposite directions. This makes it fundamentally different from directed cycle detection, where we must track the entire DFS path to detect back edges.

3. Approach Evolution

Interview communication script:

My first instinct is to use a Union-Find (Disjoint Set Union) data structure because it naturally handles the problem of detecting cycles in undirected graphs by tracking connected components. But that's O(n) per operation, and with n edges we'd get O(n²) total time. Let me think about how to optimize this.

I notice that if I use a simple DFS traversal with a visited set, I can detect cycles by checking if we revisit a node that's already in our current traversal path. But that requires maintaining the path, which could be O(n) space.

Actually, let me think about this more carefully. The key insight is that in an undirected graph, a cycle exists if and only if we encounter a back edge to a previously visited node that is not our immediate parent. This suggests using Union-Find with path compression and union by rank, which gives us nearly O(1) amortized time per operation.

Wait, I should reconsider the Union-Find approach. The standard Union-Find algorithm works by checking if two nodes are already in the same connected component before connecting them. If they are, then we've found a cycle. But I need to be more careful about the implementation.

The most efficient approach is Union-Find with path compression and union by rank. For each edge (u,v), I check if u and v are already connected. If they are, we've found a cycle. Otherwise, I union them together. This gives us O(α(n)) amortized time per operation where α is the inverse Ackermann function, making it effectively constant.

Let me trace through this more carefully to make sure I understand the invariants:

Brute-force approach: The brute force approach would be to build the graph and then run a DFS on each edge to see if it creates a cycle. But that's inefficient - for every new edge, we'd potentially traverse the entire graph to check if it creates a cycle. This is O(n²) in worst case.

Improved approach: I can do better by using Union-Find data structure. For each edge (u,v), I check if u and v are already in the same connected component. If they are, then adding this edge creates a cycle. Otherwise, I merge the components.

Final approach with step-by-step reasoning:

  1. Initialize Union-Find structure for all nodes
  2. For each edge in the graph: a. Find root of u and v using find operation (with path compression) b. If roots are same, return true (cycle detected) c. Otherwise, union u and v using union by rank
  3. If no cycles found after processing all edges, return false

The key invariant that must hold is that Union-Find always maintains the property that all nodes in a component have the same root, and we never create a cycle by connecting two nodes that are already connected.

Important invariants that must hold:

  • All nodes in the same connected component have the same root
  • When we process an edge (u,v), if u and v already have the same root, we've found a cycle
  • Union-Find operations maintain O(α(n)) amortized time complexity

4. Data Structure Choice

For cycle detection in undirected graphs, I'll use a Union-Find (Disjoint Set Union) data structure as the primary approach.

Primary Data Structures

1. Union-Find (Disjoint Set Union)

  • Used to efficiently track connected components and detect when adding an edge would create a cycle
  • Time complexity: O(α(V)) amortized for find and union operations, where α is the inverse Ackermann function
  • Space complexity: O(V) for storing parent pointers and rank information

2. Adjacency List Representation

  • Used to store the graph structure for traversal
  • Space complexity: O(V + E) where V is vertices and E is edges
  • Time complexity: O(1) for accessing neighbors of a vertex

Why These Choices Work

The Union-Find structure is optimal for this problem because:

  1. It naturally handles the "connectivity" aspect of cycle detection
  2. When we process edges in order, we can immediately detect if two vertices are already connected (cycle exists)
  3. It provides O(α(V)) amortized time complexity for each operation, which is nearly constant
  4. It's simpler to implement than maintaining explicit visited states

Helper Structures

No additional helper structures needed - the Union-Find data structure alone is sufficient to solve this problem efficiently.

Trade-offs

Alternative approaches like DFS with visited tracking would require additional space for maintaining visited states and might be less efficient in terms of the union operation. The Union-Find approach is more direct for this specific problem variant where we're processing edges sequentially to detect cycles.

5. Algorithm Walkthrough

Let me trace through the algorithm using a concrete example to demonstrate how it works step by step.

Consider this undirected graph with 5 nodes and edges: (0,1), (1,2), (2,3), (3,4), (4,1) - forming a cycle.

Step 1: Initialize data structures

  • visited set: {}
  • parent map: {0: -1, 1: -1, 2: -1, 3: -1, 4: -1}
  • Start DFS from node 0

Step 2: Visit node 0

  • Mark 0 as visited
  • Process neighbors of 0: only node 1
  • Visit node 1 with parent = 0

Step 3: Visit node 1

  • Mark 1 as visited
  • Process neighbors of 1: nodes 0 and 2
  • Node 0 is visited but is parent, so skip
  • Visit node 2 with parent = 1

Step 4: Visit node 2

  • Mark 2 as visited
  • Process neighbors of 2: nodes 1 and 3
  • Node 1 is visited but is parent, so skip
  • Visit node 3 with parent = 2

Step 5: Visit node 3

  • Mark 3 as visited
  • Process neighbors of 3: nodes 2 and 4
  • Node 2 is visited but is parent, so skip
  • Visit node 4 with parent = 3

Step 6: Visit node 4

  • Mark 4 as visited
  • Process neighbors of 4: nodes 3 and 1
  • Node 3 is visited but is parent, so skip
  • Node 1 is visited and NOT parent → CYCLE DETECTED!

At this point, we've found that node 1 was already visited and is not the parent of node 4, indicating a back edge exists. This creates a cycle: 4 → 1 → 2 → 3 → 4.

The algorithm correctly identifies that the graph contains a cycle by detecting this back edge during DFS traversal. The key insight is that in an undirected graph, a cycle exists if and only if we encounter a visited node that is not the parent of the current node during DFS traversal.

The algorithm handles edge cases properly - if we had a tree structure instead of a cycle, we would never encounter a non-parent visited node, so the algorithm would return false.

6. Java Solution

import java.util.*;

public class CycleDetectionUndirectedGraph {
    
    public boolean hasCycle(int n, int[][] edges) {
        // Build adjacency list representation
        List<List<Integer>> adjList = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            adjList.add(new ArrayList<>());
        }
        
        // Populate adjacency list
        for (int[] edge : edges) {
            int u = edge[0];
            int v = edge[1];
            adjList.get(u).add(v);
            adjList.get(v).add(u);
        }
        
        // Track visited nodes and parent information
        boolean[] visited = new boolean[n];
        int[] parent = new int[n];
        Arrays.fill(parent, -1);
        
        // Check for cycle starting from each unvisited node
        for (int i = 0; i < n; i++) {
            if (!visited[i]) {
                if (hasCycleDFS(i, adjList, visited, parent)) {
                    return true;
                }
            }
        }
        
        return false;
    }
    
    private boolean hasCycleDFS(int node, List<List<Integer>> adjList, 
                               boolean[] visited, int[] parent) {
        visited[node] = true;
        
        // Visit all neighbors
        for (int neighbor : adjList.get(node)) {
            // If neighbor is not visited, recursively check it
            if (!visited[neighbor]) {
                parent[neighbor] = node;
                if (hasCycleDFS(neighbor, adjList, visited, parent)) {
                    return true;
                }
            } 
            // If neighbor is visited and not the parent, we found a cycle
            else if (parent[node] != neighbor) {
                return true;
            }
        }
        
        return false;
    }
}

This solution uses Depth-First Search (DFS) to detect cycles in an undirected graph. The key insight is that in a DFS traversal of an undirected graph, if we encounter a visited node that is not the parent of the current node, then we've found a back edge forming a cycle.

The algorithm maintains:

  • A visited array to track processed nodes
  • A parent array to identify the parent of each node in the DFS tree
  • For each node, we check if any of its neighbors is already visited and not the parent

This approach correctly handles all undirected graph cycle detection scenarios by leveraging the DFS tree structure and back edge identification.

7. Complexity Analysis

Time Complexity: O(V + E) where V is the number of vertices and E is the number of edges in the graph.

The algorithm performs a single traversal of all vertices and edges in the undirected graph. Each vertex is visited exactly once, and each edge is examined exactly twice (once from each endpoint). Since we're using an adjacency list representation, the total time spent processing edges is proportional to the number of edges, giving us O(V + E) overall time complexity.

Space Complexity: O(V) for the visited array and recursion stack in the worst case.

We use a boolean array of size V to track visited vertices, which requires O(V) space. Additionally, in the worst case (when the graph is a tree), the recursion stack depth could be O(V) due to the DFS traversal. In the case of a cyclic graph, the stack space is still bounded by O(V) as we only visit each vertex once.

Why these bounds are correct:

  • The O(V + E) time bound is optimal for graph traversal problems since we must examine each vertex and edge at least once to determine if a cycle exists.
  • The O(V) space bound is optimal because we need to store the visited state for each vertex and the maximum recursion depth in the worst-case scenario is linear with respect to the number of vertices.

Is this solution optimal?

Yes, this is optimal for the cycle detection problem in undirected graphs. We cannot do better than O(V + E) time since we must process each edge at least once to detect cycles. The space complexity is also optimal as we need to track visited vertices and the recursion stack depth.

The solution matches the theoretical lower bounds for graph traversal algorithms, making it near-optimal in both time and space complexity.

8. Edge Cases & Pitfalls

Empty and Minimal Inputs

  • Empty graph: An empty graph (no nodes or edges) should return false as there are no cycles
  • Single node: A graph with just one node and no edges should return false
  • Two nodes with one edge: A graph with two nodes connected by a single edge should return false (no cycle possible)

Duplicate Values and Repeated States

  • Self-loops: A node connected to itself creates a cycle of length 1, which should be detected
  • Multiple edges between same nodes: While typically not allowed in simple graphs, if present, they can create cycles
  • Repeated edge definitions: Duplicate edges in input should not affect cycle detection logic

Boundary Conditions

  • Disconnected components: Graphs with multiple disconnected components should return true only if any component contains a cycle
  • Linear chains: Graphs that form linear paths should return false
  • Complete graphs: Fully connected graphs may have cycles and should be handled correctly

Common Implementation Mistakes

  • Not tracking visited nodes properly: Failing to mark nodes as visited can lead to infinite loops in cycle detection
  • Incorrect parent tracking: Not properly identifying the parent node can cause false positives when detecting cycles in undirected graphs
  • Missing edge case handling: Not accounting for isolated nodes or self-loops can cause runtime errors
  • Improper adjacency list construction: Building incorrect adjacency lists can lead to missing edges or false cycle detection
  • State management errors: Not properly managing the visited state (unvisited, visiting, visited) in DFS can cause incorrect results

Additional Pitfalls

  • Assuming all edges are bidirectional: In some implementations, assuming that if A-B exists, B-A also exists can cause issues
  • Integer overflow in large graphs: While not typically an issue for cycle detection, it's worth noting for very large graphs
  • Thread safety issues: In concurrent environments, shared state during graph traversal can cause race conditions

9. Alternative Approaches

There are several alternative approaches to cycle detection in undirected graphs, each with different trade-offs:

1. Union-Find (Disjoint Set) Approach: This is often the most elegant solution for undirected graph cycle detection. The approach works by processing edges one by one and using union-find data structure to detect when adding an edge would create a cycle. For each edge (u,v), if u and v are already in the same connected component, then adding this edge creates a cycle. This approach has O(E × α(V)) time complexity where α is the inverse Ackermann function, which is effectively constant for all practical purposes.

2. DFS with Parent Tracking: Instead of using a visited array, we can use a parent tracking approach where we maintain the parent node for each node during DFS traversal. If we encounter a visited node that is not the parent of the current node, then we have found a cycle. This approach requires O(V + E) time and space complexity.

3. BFS with Parent Tracking: Similar to the DFS approach, but using breadth-first search instead. We maintain a parent array and check if we encounter a node that is already visited but not the parent of the current node.

4. Graph Coloring Approach: We can use a 3-color marking system (white, gray, black) where white represents unvisited nodes, gray represents currently being processed nodes, and black represents completed nodes. If we encounter a gray node during traversal, it means we've found a back edge and thus a cycle. This approach is similar to the DFS method but uses color marking instead of visited arrays.

Trade-offs: The Union-Find approach is most suitable when we're primarily interested in whether cycles exist rather than identifying the specific cycle structure. The DFS/BFS approaches are better when we need to identify actual cycles or want to understand the traversal path. The Union-Find is generally more efficient in terms of constant factors, while DFS/BFS approaches are more intuitive and easier to understand.

The chosen solution (DFS with visited array) is preferred in interviews because it's more straightforward to implement and debug, especially under time pressure. It also naturally extends to finding actual cycles in the graph if needed, whereas Union-Find is more specialized for cycle existence detection.

10. Test Cases

public class CycleDetectionTests {
    
    @Test
    public void testEmptyGraph() {
        // Test case: Empty graph with no vertices
        Graph emptyGraph = new Graph(0);
        assertFalse("Empty graph should not contain cycle", 
                   containsCycle(emptyGraph));
    }
    
    @Test
    public void testSingleVertex() {
        // Test case: Single vertex with no edges
        Graph singleVertex = new Graph(1);
        assertFalse("Single vertex graph should not contain cycle", 
                   containsCycle(singleVertex));
    }
    
    @Test
    public void testLinearGraph() {
        // Test case: Linear graph (no cycles)
        Graph linearGraph = new Graph(4);
        linearGraph.addEdge(0, 1);
        linearGraph.addEdge(1, 2);
        linearGraph.addEdge(2, 3);
        assertFalse("Linear graph should not contain cycle", 
                   containsCycle(linearGraph));
    }
    
    @Test
    public void testSimpleCycle() {
        // Test case: Simple triangle cycle
        Graph triangle = new Graph(3);
        triangle.addEdge(0, 1);
        triangle.addEdge(1, 2);
        triangle.addEdge(2, 0);
        assertTrue("Triangle graph should contain cycle", 
                  containsCycle(triangle));
    }
    
    @Test
    public void testComplexCycle() {
        // Test case: Complex graph with multiple cycles
        Graph complexGraph = new Graph(6);
        complexGraph.addEdge(0, 1);
        complexGraph.addEdge(1, 2);
        complexGraph.addEdge(2, 3);
        complexGraph.addEdge(3, 4);
        complexGraph.addEdge(4, 5);
        complexGraph.addEdge(5, 1); // Creates cycle: 1->2->3->4->5->1
        complexGraph.addEdge(0, 5); // Creates another path to node 5
        assertTrue("Complex graph should contain cycle", 
                  containsCycle(complexGraph));
    }
    
    @Test
    public void testDisconnectedComponents() {
        // Test case: Disconnected graph with cycles in one component
        Graph disconnected = new Graph(5);
        disconnected.addEdge(0, 1);
        disconnected.addEdge(1, 2);
        disconnected.addEdge(2, 0); // Cycle in component 1
        disconnected.addEdge(3, 4); // No cycle in component 2
        assertTrue("Disconnected graph with cycle should return true", 
                  containsCycle(disconnected));
    }
    
    @Test
    public void testSelfLoop() {
        // Test case: Self loop (node connected to itself)
        Graph selfLoop = new Graph(2);
        selfLoop.addEdge(0, 1);
        selfLoop.addEdge(1, 1); // Self loop
        assertTrue("Graph with self loop should contain cycle", 
                  containsCycle(selfLoop));
    }
    
    @Test
    public void testMultiEdgeCycle() {
        // Test case: Multiple edges between same vertices (parallel edges)
        Graph multiEdge = new Graph(3);
        multiEdge.addEdge(0, 1);
        multiEdge.addEdge(1, 2);
        multiEdge.addEdge(2, 0);
        multiEdge.addEdge(0, 1); // Additional edge between same nodes
        assertTrue("Graph with parallel edges should contain cycle", 
                  containsCycle(multiEdge));
    }
}

11. Final Summary

The core idea is to detect cycles in undirected graphs using Union-Find (Disjoint Set Union) data structure with path compression and union by rank optimizations. This solution works efficiently by maintaining disjoint sets of connected components and detecting when adding an edge would connect two nodes that are already in the same component, indicating a cycle. The approach is optimal with nearly constant amortized time per operation and linear space complexity, making it ideal for large-scale graph processing scenarios where cycle detection is needed.

Was this article helpful?

Topics in this article

More from DSA Patterns

  • DSA Patterns

    Dynamic Connectivity Algorithms and Applications

    Comprehensive guide to dynamic connectivity problems including Union-Find, connectivity queries, and network reliability algorithms with Java implementations.

  • DSA Patterns

    Connected Components in Graphs and Grids

    Complete guide to finding connected components using Union-Find, DFS, and BFS approaches with Java implementations for FAANG interviews.

  • DSA Patterns

    Cycle Detection in Directed Graphs Using DFS

    Complete Java solution for detecting cycles in directed graphs with time complexity analysis and implementation trade-offs.

  • DSA Problems

    Pacific Atlantic Water Flow Problem Solution

    Complete Java implementation for finding cells where water can flow to both Pacific and Atlantic oceans in a grid.

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.