Skip to content

Conversation

@codeflash-ai
Copy link

@codeflash-ai codeflash-ai bot commented Dec 26, 2025

📄 17,985% (179.85x) speedup for find_last_node in src/algorithms/graph.py

⏱️ Runtime : 91.0 milliseconds 503 microseconds (best of 250 runs)

📝 Explanation and details

The optimized code achieves a 180x speedup by eliminating redundant work through preprocessing.

Key optimization: The original code uses a nested loop structure where for each node, it checks all edges to verify the node isn't a source. This results in O(N × E) complexity. The optimized version precomputes a set of source node IDs in O(E) time, then checks each node against this set in O(1) time, reducing overall complexity to O(N + E).

What changed:

  • Built a sources set containing all source node IDs from edges upfront
  • Replaced the nested all(e["source"] != n["id"] for e in edges) check with a simple n["id"] not in sources set membership test

Why this is faster:

  1. Set lookup is O(1) vs O(E) linear scan through all edges for each node
  2. Single pass through edges instead of scanning edges repeatedly for every node
  3. Hash-based membership testing (set) vs repeated equality comparisons

Performance characteristics from tests:

  • Small graphs (2-10 nodes): 30-80% faster - modest gains due to setup overhead
  • Medium graphs (100-1000 nodes): 86-1465% faster - significant speedup as edge scanning cost dominates
  • Large linear chains (1000 nodes): 32,000%+ faster - the original's quadratic behavior becomes catastrophic
  • Dense graphs (fully connected): 8,600% faster - maximum benefit where edge count is highest
  • Empty/minimal cases: Slightly slower (13-30%) due to set creation overhead, but negligible in absolute terms (nanoseconds)

The optimization excels when the graph has many edges or nodes, which is typical in real-world graph processing scenarios. The preprocessing cost is amortized extremely well across all but the most trivial graphs.

Correctness verification report:

Test Status
⚙️ Existing Unit Tests 🔘 None Found
🌀 Generated Regression Tests 41 Passed
⏪ Replay Tests 🔘 None Found
🔎 Concolic Coverage Tests 🔘 None Found
📊 Tests Coverage 100.0%
🌀 Click to see Generated Regression Tests
from __future__ import annotations

# imports
import pytest
from src.algorithms.graph import find_last_node

# unit tests

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


def test_single_node_no_edges():
    # One node, no edges: should return the node itself
    nodes = [{"id": 1}]
    edges = []
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.29μs -> 958ns (34.9% faster)


def test_two_nodes_one_edge():
    # Two nodes, one edge from 1 to 2: last node is 2
    nodes = [{"id": 1}, {"id": 2}]
    edges = [{"source": 1, "target": 2}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.75μs -> 1.12μs (55.6% faster)


def test_three_nodes_linear_chain():
    # 1 -> 2 -> 3, last node is 3
    nodes = [{"id": 1}, {"id": 2}, {"id": 3}]
    edges = [{"source": 1, "target": 2}, {"source": 2, "target": 3}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 2.17μs -> 1.21μs (79.3% faster)


def test_three_nodes_branching():
    # 1 -> 2, 1 -> 3, both 2 and 3 are last nodes, should return first in nodes order
    nodes = [{"id": 1}, {"id": 2}, {"id": 3}]
    edges = [{"source": 1, "target": 2}, {"source": 1, "target": 3}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.83μs -> 1.17μs (57.2% faster)


def test_multiple_last_nodes():
    # 1 -> 2, 1 -> 3, 4 is isolated, so both 2, 3, 4 are last nodes, should return 2
    nodes = [{"id": 1}, {"id": 2}, {"id": 3}, {"id": 4}]
    edges = [{"source": 1, "target": 2}, {"source": 1, "target": 3}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.83μs -> 1.12μs (62.9% faster)


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


def test_empty_nodes_list():
    # No nodes at all, should return None
    nodes = []
    edges = []
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 792ns -> 916ns (13.5% slower)


def test_edges_with_missing_nodes():
    # Edges refer to nodes not in the nodes list; should ignore them
    nodes = [{"id": 1}]
    edges = [{"source": 2, "target": 3}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.46μs -> 1.08μs (34.6% faster)


def test_all_nodes_have_outgoing_edges():
    # Each node is a source in at least one edge, so no last node
    nodes = [{"id": 1}, {"id": 2}, {"id": 3}]
    edges = [
        {"source": 1, "target": 2},
        {"source": 2, "target": 3},
        {"source": 3, "target": 1},
    ]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 2.21μs -> 1.29μs (71.0% faster)


def test_duplicate_edges():
    # Duplicated edges should not affect result
    nodes = [{"id": 1}, {"id": 2}]
    edges = [{"source": 1, "target": 2}, {"source": 1, "target": 2}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.88μs -> 1.21μs (55.2% faster)


def test_node_with_self_loop():
    # Node with a self-loop is not a last node
    nodes = [{"id": 1}, {"id": 2}]
    edges = [{"source": 1, "target": 1}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.79μs -> 1.08μs (65.4% faster)


def test_node_with_incoming_but_no_outgoing():
    # Node with only incoming edges is a last node
    nodes = [{"id": 1}, {"id": 2}]
    edges = [{"source": 1, "target": 2}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.71μs -> 1.12μs (51.9% faster)


def test_nodes_with_non_integer_ids():
    # Node IDs are strings
    nodes = [{"id": "A"}, {"id": "B"}]
    edges = [{"source": "A", "target": "B"}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.92μs -> 1.17μs (64.3% faster)


def test_nodes_with_mixed_type_ids():
    # Node IDs of different types
    nodes = [{"id": 1}, {"id": "2"}]
    edges = [{"source": 1, "target": "2"}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.92μs -> 1.12μs (70.4% faster)


def test_nodes_with_extra_attributes():
    # Nodes have extra keys, should return full node dict
    nodes = [{"id": 1, "name": "A"}, {"id": 2, "name": "B"}]
    edges = [{"source": 1, "target": 2}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.75μs -> 1.12μs (55.6% faster)


def test_edges_with_extra_attributes():
    # Edges have extra keys, should not affect result
    nodes = [{"id": 1}, {"id": 2}]
    edges = [{"source": 1, "target": 2, "weight": 5}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.75μs -> 1.08μs (61.6% faster)


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


def test_large_linear_chain():
    # 1 -> 2 -> ... -> 1000, last node is 1000
    N = 1000
    nodes = [{"id": i} for i in range(1, N + 1)]
    edges = [{"source": i, "target": i + 1} for i in range(1, N)]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 18.4ms -> 56.6μs (32387% faster)


def test_large_star_topology():
    # 1 -> 2, 1 -> 3, ..., 1 -> 1000; last node is 2
    N = 1000
    nodes = [{"id": i} for i in range(1, N + 1)]
    edges = [{"source": 1, "target": i} for i in range(2, N + 1)]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 38.0μs -> 20.4μs (86.1% faster)


def test_large_all_sources():
    # All nodes are sources; no last node
    N = 1000
    nodes = [{"id": i} for i in range(1, N + 1)]
    edges = [{"source": i, "target": ((i % N) + 1)} for i in range(1, N + 1)]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 18.4ms -> 55.6μs (33090% faster)


def test_large_isolated_nodes():
    # All nodes, no edges; first node is last node
    N = 1000
    nodes = [{"id": i} for i in range(1, N + 1)]
    edges = []
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.33μs -> 1.04μs (27.9% faster)


def test_large_multiple_last_nodes():
    # 1 -> 2, 1 -> 3, 4..1000 are isolated; should return 2
    N = 1000
    nodes = [{"id": i} for i in range(1, N + 1)]
    edges = [{"source": 1, "target": 2}, {"source": 1, "target": 3}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.92μs -> 1.21μs (58.6% faster)


# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.
from __future__ import annotations

# imports
import pytest  # used for our unit tests
from src.algorithms.graph import find_last_node

# unit tests

# Basic Test Cases


def test_single_node_no_edges():
    # One node, no edges: should return the node itself
    nodes = [{"id": 1, "name": "A"}]
    edges = []
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.21μs -> 917ns (31.8% faster)


def test_two_nodes_one_edge():
    # Two nodes, one edge: should return the node that is not a source
    nodes = [{"id": 1}, {"id": 2}]
    edges = [{"source": 1, "target": 2}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.79μs -> 1.08μs (65.5% faster)


def test_three_nodes_linear_chain():
    # Three nodes in a chain: 1 -> 2 -> 3, last node is 3
    nodes = [{"id": 1}, {"id": 2}, {"id": 3}]
    edges = [{"source": 1, "target": 2}, {"source": 2, "target": 3}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 2.21μs -> 1.25μs (76.7% faster)


def test_three_nodes_branch():
    # 1 -> 2, 1 -> 3; both 2 and 3 are terminal, but function returns first found (2)
    nodes = [{"id": 1}, {"id": 2}, {"id": 3}]
    edges = [{"source": 1, "target": 2}, {"source": 1, "target": 3}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.79μs -> 1.12μs (59.3% faster)


def test_multiple_terminal_nodes():
    # 1 -> 2, 1 -> 3, 4 (isolated): function returns first terminal node (2)
    nodes = [{"id": 1}, {"id": 2}, {"id": 3}, {"id": 4}]
    edges = [{"source": 1, "target": 2}, {"source": 1, "target": 3}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.79μs -> 1.12μs (59.2% faster)


# Edge Test Cases


def test_empty_nodes_and_edges():
    # No nodes and no edges: should return None
    nodes = []
    edges = []
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 709ns -> 875ns (19.0% slower)


def test_edges_but_no_nodes():
    # Edges exist but no nodes: should return None
    nodes = []
    edges = [{"source": 1, "target": 2}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 667ns -> 958ns (30.4% slower)


def test_nodes_but_no_edges():
    # Multiple nodes, no edges: should return the first node
    nodes = [{"id": 10}, {"id": 20}]
    edges = []
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.21μs -> 958ns (26.1% faster)


def test_all_nodes_are_sources():
    # Every node is a source in at least one edge: should return None
    nodes = [{"id": 1}, {"id": 2}]
    edges = [{"source": 1, "target": 2}, {"source": 2, "target": 1}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.83μs -> 1.17μs (57.1% faster)


def test_isolated_node_among_others():
    # One node is not connected at all, should be returned as last node
    nodes = [{"id": 1}, {"id": 2}, {"id": 3}]
    edges = [{"source": 1, "target": 2}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.79μs -> 1.12μs (59.3% faster)


def test_node_with_multiple_incoming_edges():
    # Node 3 has two incoming edges, but is not a source, should be returned
    nodes = [{"id": 1}, {"id": 2}, {"id": 3}]
    edges = [{"source": 1, "target": 3}, {"source": 2, "target": 3}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 2.17μs -> 1.21μs (79.2% faster)


def test_node_with_self_loop():
    # Node with self-loop is still a source, so should not be returned
    nodes = [{"id": 1}, {"id": 2}]
    edges = [{"source": 1, "target": 1}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.71μs -> 1.08μs (57.8% faster)


def test_node_with_non_integer_ids():
    # Node IDs are strings
    nodes = [{"id": "a"}, {"id": "b"}]
    edges = [{"source": "a", "target": "b"}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.92μs -> 1.17μs (64.4% faster)


def test_node_with_extra_properties():
    # Node has extra properties, should be preserved in result
    nodes = [{"id": 1, "value": 42}, {"id": 2, "value": 99}]
    edges = [{"source": 1, "target": 2}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.75μs -> 1.08μs (61.6% faster)


def test_edges_with_extra_properties():
    # Edges have extra properties, should not affect result
    nodes = [{"id": 1}, {"id": 2}]
    edges = [{"source": 1, "target": 2, "weight": 5}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.75μs -> 1.08μs (61.6% faster)


def test_duplicate_node_ids():
    # Duplicate node IDs: first non-source node should be returned (should be robust)
    nodes = [{"id": 1}, {"id": 1}]
    edges = []
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.21μs -> 958ns (26.1% faster)


def test_large_linear_chain():
    # Large chain: 0 -> 1 -> ... -> 999, last node is 999
    N = 1000
    nodes = [{"id": i} for i in range(N)]
    edges = [{"source": i, "target": i + 1} for i in range(N - 1)]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 18.6ms -> 57.2μs (32482% faster)


def test_large_star_graph():
    # Star: 0 -> 1, 0 -> 2, ..., 0 -> 999; all except 0 are terminal, function returns first (1)
    N = 1000
    nodes = [{"id": i} for i in range(N)]
    edges = [{"source": 0, "target": i} for i in range(1, N)]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 38.2μs -> 20.2μs (89.3% faster)


def test_large_fully_connected_graph():
    # Every node is a source in at least one edge: should return None
    N = 100
    nodes = [{"id": i} for i in range(N)]
    edges = [{"source": i, "target": j} for i in range(N) for j in range(N) if i != j]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 17.1ms -> 196μs (8616% faster)


def test_large_graph_with_isolated_node():
    # Large graph with one isolated node at the end
    N = 999
    nodes = [{"id": i} for i in range(N)] + [{"id": "isolated"}]
    edges = [{"source": i, "target": i + 1} for i in range(N - 1)]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 18.2ms -> 56.6μs (32084% faster)


def test_large_graph_multiple_terminal_nodes():
    # Large graph with several terminal nodes, function returns the first one
    N = 100
    nodes = [{"id": i} for i in range(N)]
    # Half the nodes are sources, half are terminals
    edges = [{"source": i, "target": i + N // 2} for i in range(N // 2)]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 61.3μs -> 3.92μs (1465% faster)


# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.

To edit these changes git checkout codeflash/optimize-find_last_node-mjngkx4p and push.

Codeflash Static Badge

The optimized code achieves a **180x speedup** by eliminating redundant work through preprocessing. 

**Key optimization:** The original code uses a nested loop structure where for each node, it checks all edges to verify the node isn't a source. This results in O(N × E) complexity. The optimized version precomputes a set of source node IDs in O(E) time, then checks each node against this set in O(1) time, reducing overall complexity to O(N + E).

**What changed:**
- Built a `sources` set containing all source node IDs from edges upfront
- Replaced the nested `all(e["source"] != n["id"] for e in edges)` check with a simple `n["id"] not in sources` set membership test

**Why this is faster:**
1. **Set lookup is O(1)** vs O(E) linear scan through all edges for each node
2. **Single pass through edges** instead of scanning edges repeatedly for every node
3. **Hash-based membership testing** (set) vs repeated equality comparisons

**Performance characteristics from tests:**
- **Small graphs** (2-10 nodes): 30-80% faster - modest gains due to setup overhead
- **Medium graphs** (100-1000 nodes): 86-1465% faster - significant speedup as edge scanning cost dominates
- **Large linear chains** (1000 nodes): **32,000%+ faster** - the original's quadratic behavior becomes catastrophic
- **Dense graphs** (fully connected): **8,600% faster** - maximum benefit where edge count is highest
- **Empty/minimal cases**: Slightly slower (13-30%) due to set creation overhead, but negligible in absolute terms (nanoseconds)

The optimization excels when the graph has many edges or nodes, which is typical in real-world graph processing scenarios. The preprocessing cost is amortized extremely well across all but the most trivial graphs.
@codeflash-ai codeflash-ai bot requested a review from KRRT7 December 26, 2025 22:44
@codeflash-ai codeflash-ai bot added ⚡️ codeflash Optimization PR opened by Codeflash AI 🎯 Quality: High Optimization Quality according to Codeflash labels Dec 26, 2025
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