Skip to content

⚡️ Speed up method Graph.topologicalSort by 62%#885

Closed
codeflash-ai[bot] wants to merge 1 commit into
generated-tests-markdownfrom
codeflash/optimize-Graph.topologicalSort-mhp9l036
Closed

⚡️ Speed up method Graph.topologicalSort by 62%#885
codeflash-ai[bot] wants to merge 1 commit into
generated-tests-markdownfrom
codeflash/optimize-Graph.topologicalSort-mhp9l036

Conversation

@codeflash-ai
Copy link
Copy Markdown
Contributor

@codeflash-ai codeflash-ai Bot commented Nov 7, 2025

📄 62% (0.62x) speedup for Graph.topologicalSort in code_to_optimize/topological_sort.py

⏱️ Runtime : 9.29 milliseconds 5.72 milliseconds (best of 16 runs)

📝 Explanation and details

The key optimization replaces the inefficient stack.insert(0, v) operation with stack.append(v) followed by a single stack.reverse() at the end. This changes the time complexity from O(n²) to O(n) for building the result stack.

What changed:

  • In topologicalSortUtil: Changed stack.insert(0, v) to stack.append(v)
  • In topologicalSort: Added stack.reverse() after the DFS traversal completes
  • Minor style improvement: if visited[i] == False:if not visited[i]:

Why this is faster:
stack.insert(0, v) has O(n) complexity because it must shift all existing elements one position right. When called repeatedly during DFS traversal (6,110 times in the profiler), this creates O(n²) overhead. stack.append(v) is O(1), and the final stack.reverse() is O(n) for the entire list, resulting in O(n) total complexity for stack operations.

Performance impact:
The line profiler shows the stack operation time dropped from 2.196ms to 1.649ms (25% improvement). The overall speedup is 62%, from 9.29ms to 5.72ms. Test results show the optimization particularly benefits larger graphs - achieving 59-75% speedups on 1000-node test cases while having minimal impact on small graphs (often within measurement noise).

Behavioral preservation:
The final topological order is identical since reversing the append-built stack produces the same result as the original insert-at-beginning approach, just much more efficiently.

Correctness verification report:

Test Status
⚙️ Existing Unit Tests 8 Passed
🌀 Generated Regression Tests 78 Passed
⏪ Replay Tests 🔘 None Found
🔎 Concolic Coverage Tests 2 Passed
📊 Tests Coverage 100.0%
⚙️ Existing Unit Tests and Runtime
Test File::Test Function Original ⏱️ Optimized ⏱️ Speedup
test_topological_sort.py::test_topological_sort 6.50μs 6.25μs 4.00%✅
test_topological_sort.py::test_topological_sort_2 12.1μs 11.1μs 9.40%✅
test_topological_sort.py::test_topological_sort_3 7.82ms 4.75ms 64.5%✅
🌀 Generated Regression Tests and Runtime
import uuid
from collections import defaultdict

# imports
import pytest  # used for our unit tests
from code_to_optimize.topological_sort import Graph

# unit tests

# Helper function to check if a given order is a valid topological order for a graph
def is_valid_topological_order(graph, order):
    position = {node: idx for idx, node in enumerate(order)}
    for u in graph.graph:
        for v in graph.graph[u]:
            if position[u] >= position[v]:
                return False
    return True

# ----------------------
# Basic Test Cases
# ----------------------

def test_single_node():
    # Graph with a single node and no edges
    g = Graph(1)
    topo, sort_id = g.topologicalSort() # 7.08μs -> 7.58μs (6.59% slower)

def test_two_nodes_one_edge():
    # Graph: 0 -> 1
    g = Graph(2)
    g.graph[0].append(1)
    topo, _ = g.topologicalSort() # 5.46μs -> 5.58μs (2.24% slower)

def test_three_nodes_chain():
    # Graph: 0 -> 1 -> 2
    g = Graph(3)
    g.graph[0].append(1)
    g.graph[1].append(2)
    topo, _ = g.topologicalSort() # 5.46μs -> 5.46μs (0.000% faster)

def test_three_nodes_branch():
    # Graph: 0 -> 1, 0 -> 2
    g = Graph(3)
    g.graph[0].append(1)
    g.graph[0].append(2)
    topo, _ = g.topologicalSort() # 5.33μs -> 5.33μs (0.019% slower)

def test_disconnected_graph():
    # Graph: 0->1, 2 (no edges)
    g = Graph(3)
    g.graph[0].append(1)
    topo, _ = g.topologicalSort() # 5.29μs -> 5.33μs (0.769% slower)

def test_multiple_edges():
    # Graph: 0->1, 0->2, 1->2
    g = Graph(3)
    g.graph[0].append(1)
    g.graph[0].append(2)
    g.graph[1].append(2)
    topo, _ = g.topologicalSort() # 5.25μs -> 5.25μs (0.000% faster)

# ----------------------
# Edge Test Cases
# ----------------------

def test_empty_graph():
    # Graph with zero nodes
    g = Graph(0)
    topo, _ = g.topologicalSort() # 4.46μs -> 4.46μs (0.022% slower)

def test_no_edges():
    # Graph with multiple nodes, no edges
    g = Graph(4)
    topo, _ = g.topologicalSort() # 5.46μs -> 5.54μs (1.50% slower)

def test_cycle_detection_not_supported():
    # Graph with a cycle: 0->1->2->0
    g = Graph(3)
    g.graph[0].append(1)
    g.graph[1].append(2)
    g.graph[2].append(0)
    # The provided implementation does NOT detect cycles, so it will output something
    # We check that the output is a permutation of [0,1,2], but not necessarily valid
    topo, _ = g.topologicalSort() # 5.12μs -> 5.25μs (2.38% slower)

def test_multiple_disconnected_components():
    # Graph: 0->1, 2->3
    g = Graph(4)
    g.graph[0].append(1)
    g.graph[2].append(3)
    topo, _ = g.topologicalSort() # 5.38μs -> 5.21μs (3.21% faster)

def test_duplicate_edges():
    # Graph: 0->1 (twice)
    g = Graph(2)
    g.graph[0].append(1)
    g.graph[0].append(1)
    topo, _ = g.topologicalSort() # 5.08μs -> 5.17μs (1.63% slower)

def test_self_loop():
    # Graph: 0->0
    g = Graph(1)
    g.graph[0].append(0)
    topo, _ = g.topologicalSort() # 4.83μs -> 4.92μs (1.71% slower)

def test_non_integer_vertices():
    # Graph with string vertices (should not work with current implementation)
    # This test is expected to fail if someone mutates the code to support non-integer vertices
    g = Graph(2)
    g.graph["a"].append("b")
    # topologicalSort will ignore "a" and "b" since V=2 and only indices 0,1 are visited
    topo, _ = g.topologicalSort() # 5.17μs -> 5.17μs (0.019% faster)

# ----------------------
# Large Scale Test Cases
# ----------------------

def test_large_linear_chain():
    # Large chain: 0->1->2->...->999
    N = 1000
    g = Graph(N)
    for i in range(N-1):
        g.graph[i].append(i+1)
    topo, _ = g.topologicalSort()

def test_large_branching():
    # Graph: 0 points to all others
    N = 1000
    g = Graph(N)
    for i in range(1, N):
        g.graph[0].append(i)
    topo, _ = g.topologicalSort() # 233μs -> 140μs (65.6% faster)

def test_large_disconnected():
    # 500 chains of length 2: (0->1), (2->3), ..., (998->999)
    N = 1000
    g = Graph(N)
    for i in range(0, N, 2):
        if i+1 < N:
            g.graph[i].append(i+1)
    topo, _ = g.topologicalSort() # 196μs -> 112μs (74.4% faster)
    # Each even before its odd
    for i in range(0, N, 2):
        if i+1 < N:
            pass

def test_large_no_edges():
    # 1000 nodes, no edges
    N = 1000
    g = Graph(N)
    topo, _ = g.topologicalSort() # 199μs -> 119μs (67.2% faster)

def test_large_duplicate_edges():
    # 0->1, 0->1, ..., 0->1 (100 times)
    g = Graph(2)
    for _ in range(100):
        g.graph[0].append(1)
    topo, _ = g.topologicalSort() # 7.17μs -> 6.71μs (6.81% faster)
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.
import uuid
from collections import defaultdict

# imports
import pytest  # used for our unit tests
from code_to_optimize.topological_sort import Graph

# unit tests

# ----------- Basic Test Cases -----------

def test_single_node():
    # Graph with one node, no edges
    g = Graph(1)
    order, sort_id = g.topologicalSort() # 5.08μs -> 5.17μs (1.61% slower)

def test_two_nodes_one_edge():
    # Graph: 0 -> 1
    g = Graph(2)
    g.graph[0].append(1)
    order, _ = g.topologicalSort() # 5.25μs -> 5.12μs (2.44% faster)

def test_three_nodes_linear():
    # Graph: 0 -> 1 -> 2
    g = Graph(3)
    g.graph[0].append(1)
    g.graph[1].append(2)
    order, _ = g.topologicalSort() # 5.25μs -> 5.29μs (0.775% slower)

def test_three_nodes_branch():
    # Graph: 0 -> 1, 0 -> 2
    g = Graph(3)
    g.graph[0].append(1)
    g.graph[0].append(2)
    order, _ = g.topologicalSort() # 5.33μs -> 5.25μs (1.58% faster)

def test_disconnected_graph():
    # Graph: 0, 1 (no edges)
    g = Graph(2)
    order, _ = g.topologicalSort() # 5.04μs -> 5.08μs (0.846% slower)

def test_multiple_components():
    # Graph: 0->1, 2->3
    g = Graph(4)
    g.graph[0].append(1)
    g.graph[2].append(3)
    order, _ = g.topologicalSort() # 5.33μs -> 5.29μs (0.794% faster)
    # 0 before 1, 2 before 3
    idx_0 = order.index(0)
    idx_1 = order.index(1)
    idx_2 = order.index(2)
    idx_3 = order.index(3)

# ----------- Edge Test Cases -----------

def test_empty_graph():
    # Graph with zero nodes
    g = Graph(0)
    order, _ = g.topologicalSort() # 4.21μs -> 4.42μs (4.73% slower)

def test_no_edges():
    # Graph with nodes but no edges
    g = Graph(5)
    order, _ = g.topologicalSort() # 5.58μs -> 5.58μs (0.000% faster)

def test_self_loop():
    # Graph: 0 -> 0 (self-loop)
    g = Graph(1)
    g.graph[0].append(0)
    # This implementation does not detect cycles; it will recurse infinitely.
    # To prevent infinite recursion, we set a recursion limit and expect RecursionError.
    import sys
    old_limit = sys.getrecursionlimit()
    sys.setrecursionlimit(100)
    try:
        with pytest.raises(RecursionError):
            g.topologicalSort()
    finally:
        sys.setrecursionlimit(old_limit)

def test_cycle_detection():
    # Graph: 0->1->2->0 (cycle)
    g = Graph(3)
    g.graph[0].append(1)
    g.graph[1].append(2)
    g.graph[2].append(0)
    import sys
    old_limit = sys.getrecursionlimit()
    sys.setrecursionlimit(100)
    try:
        with pytest.raises(RecursionError):
            g.topologicalSort()
    finally:
        sys.setrecursionlimit(old_limit)

def test_duplicate_edges():
    # Graph: 0->1 twice
    g = Graph(2)
    g.graph[0].append(1)
    g.graph[0].append(1)
    order, _ = g.topologicalSort() # 6.42μs -> 6.54μs (1.91% slower)

def test_non_sequential_node_numbers():
    # Graph with nodes not in sequence (simulate by skipping some)
    g = Graph(4)
    g.graph[0].append(2)
    g.graph[2].append(3)
    # 1 is isolated
    order, _ = g.topologicalSort() # 5.92μs -> 5.83μs (1.44% faster)

def test_large_branching():
    # Graph: 0 -> 1, 0 -> 2, 0 -> 3, 0 -> 4
    g = Graph(5)
    for i in range(1, 5):
        g.graph[0].append(i)
    order, _ = g.topologicalSort() # 5.96μs -> 5.83μs (2.13% faster)

# ----------- Large Scale Test Cases -----------

def test_large_linear_graph():
    # Graph: 0 -> 1 -> 2 -> ... -> 999
    N = 1000
    g = Graph(N)
    for i in range(N - 1):
        g.graph[i].append(i + 1)
    order, _ = g.topologicalSort()

def test_large_disconnected_graph():
    # 1000 nodes, no edges
    N = 1000
    g = Graph(N)
    order, _ = g.topologicalSort() # 213μs -> 134μs (59.3% faster)

def test_large_star_graph():
    # 0 -> 1, 0 -> 2, ..., 0 -> 999
    N = 1000
    g = Graph(N)
    for i in range(1, N):
        g.graph[0].append(i)
    order, _ = g.topologicalSort() # 218μs -> 132μs (65.7% faster)

def test_large_multiple_components():
    # Two chains: 0->1->...->499, 500->501->...->999
    N = 1000
    g = Graph(N)
    for i in range(0, 499):
        g.graph[i].append(i + 1)
    for i in range(500, 999):
        g.graph[i].append(i + 1)
    order, _ = g.topologicalSort() # 222μs -> 137μs (61.3% faster)
    # Check that each chain is in order
    chain1 = order[:500]
    chain2 = order[500:]

def test_large_random_dag():
    # Random DAG with 1000 nodes and edges from i to i+1, i+2, ..., min(i+5, N-1)
    N = 1000
    g = Graph(N)
    for i in range(N):
        for j in range(i + 1, min(i + 6, N)):
            g.graph[i].append(j)
    order, _ = g.topologicalSort()
    pos = {node: idx for idx, node in enumerate(order)}
    for i in range(N):
        for j in g.graph[i]:
            pass

# ----------- Determinism and ID Test -----------

def test_sort_id_is_unique():
    # Each call should produce a unique sort_id
    g = Graph(3)
    g.graph[0].append(1)
    g.graph[1].append(2)
    ids = set()
    for _ in range(5):
        _, sort_id = g.topologicalSort() # 25.0μs -> 25.9μs (3.37% slower)
        ids.add(sort_id)
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.
from code_to_optimize.topological_sort import Graph

def test_Graph_topologicalSort():
    Graph.topologicalSort(Graph(1))
🔎 Concolic Coverage Tests and Runtime
Test File::Test Function Original ⏱️ Optimized ⏱️ Speedup
codeflash_concolic_9qb_ya1j/tmpxhmk42k9/test_concolic_coverage.py::test_Graph_topologicalSort 5.08μs 5.25μs -3.18%⚠️

To edit these changes git checkout codeflash/optimize-Graph.topologicalSort-mhp9l036 and push.

Codeflash Static Badge

The key optimization replaces the inefficient `stack.insert(0, v)` operation with `stack.append(v)` followed by a single `stack.reverse()` at the end. This changes the time complexity from O(n²) to O(n) for building the result stack.

**What changed:**
- In `topologicalSortUtil`: Changed `stack.insert(0, v)` to `stack.append(v)` 
- In `topologicalSort`: Added `stack.reverse()` after the DFS traversal completes
- Minor style improvement: `if visited[i] == False:` → `if not visited[i]:`

**Why this is faster:**
`stack.insert(0, v)` has O(n) complexity because it must shift all existing elements one position right. When called repeatedly during DFS traversal (6,110 times in the profiler), this creates O(n²) overhead. `stack.append(v)` is O(1), and the final `stack.reverse()` is O(n) for the entire list, resulting in O(n) total complexity for stack operations.

**Performance impact:**
The line profiler shows the stack operation time dropped from 2.196ms to 1.649ms (25% improvement). The overall speedup is 62%, from 9.29ms to 5.72ms. Test results show the optimization particularly benefits larger graphs - achieving 59-75% speedups on 1000-node test cases while having minimal impact on small graphs (often within measurement noise).

**Behavioral preservation:**
The final topological order is identical since reversing the append-built stack produces the same result as the original insert-at-beginning approach, just much more efficiently.
@codeflash-ai codeflash-ai Bot requested a review from aseembits93 November 7, 2025 19:44
@codeflash-ai codeflash-ai Bot added ⚡️ codeflash Optimization PR opened by Codeflash AI 🎯 Quality: High Optimization Quality according to Codeflash labels Nov 7, 2025
@aseembits93 aseembits93 closed this Nov 7, 2025
@codeflash-ai codeflash-ai Bot deleted the codeflash/optimize-Graph.topologicalSort-mhp9l036 branch November 7, 2025 21:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚡️ codeflash Optimization PR opened by Codeflash AI 🎯 Quality: High Optimization Quality according to Codeflash

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant