Skip to content

Conversation

@codeflash-ai
Copy link

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

📄 21,472% (214.72x) speedup for find_last_node in src/algorithms/graph.py

⏱️ Runtime : 77.8 milliseconds 360 microseconds (best of 250 runs)

📝 Explanation and details

The optimization transforms an O(n × m) nested loop into an O(n + m) linear scan by eliminating redundant edge traversals.

What changed:

  • Pre-computed source lookup: Instead of checking all(e["source"] != n["id"] for e in edges) for every node (which scans all edges repeatedly), the code now builds a set source_ids containing all source IDs upfront
  • Set membership test: The condition changes from "is this node's ID different from ALL edge sources?" to "is this node's ID NOT IN the set of source IDs?"

Why it's faster:
In the original code, for each node, Python iterates through every edge to verify none has that node as a source. With N nodes and M edges, this results in N × M comparisons.

The optimized version:

  1. Creates the source_ids set in one pass: O(M) time, O(M) space
  2. For each node, performs a O(1) hash lookup instead of O(M) iteration

This is a classic space-time tradeoff: we use O(M) additional memory for massive time savings.

Performance impact:

  • Small graphs (2-4 nodes/edges): 30-100% faster due to reduced Python interpreter overhead
  • Large linear chains (1000 nodes): 32,000%+ faster - the quadratic nested loop becomes linear
  • Large cycles (1000 nodes, all connected): 32,000%+ faster - eliminates catastrophic N×M behavior
  • Star graphs: 85% faster even though the original code exits early for most nodes
  • Empty edges: Slightly slower (10-15%) due to set creation overhead, but negligible in absolute terms (nanoseconds)

The optimization particularly shines when there are many edges to check against each node, transforming what would be prohibitively slow for production graphs into microsecond-level performance.

Correctness verification report:

Test Status
⚙️ Existing Unit Tests 🔘 None Found
🌀 Generated Regression Tests 38 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  # used for our unit tests
from src.algorithms.graph import find_last_node

# unit tests

# 1. Basic Test Cases


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


def test_two_nodes_one_edge():
    # Two nodes, one edge from 1->2; should return node 2 (no outgoing edges)
    nodes = [{"id": 1, "label": "A"}, {"id": 2, "label": "B"}]
    edges = [{"source": 1, "target": 2}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.79μs -> 1.12μs (59.2% faster)


def test_three_nodes_linear_chain():
    # 1->2->3; should return node 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.17μs (85.6% faster)


def test_multiple_last_nodes():
    # 1->2, 1->3; nodes 2 and 3 have no outgoing edges, should return 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.17μs (53.7% 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.75μs -> 1.08μs (61.6% faster)


# 2. Edge Test Cases


def test_empty_nodes_and_edges():
    # Both lists empty; should return None
    nodes = []
    edges = []
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 792ns -> 875ns (9.49% slower)


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


def test_all_nodes_have_outgoing_edges():
    # All nodes have outgoing edges; 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.2% faster)


def test_edges_with_nonexistent_nodes():
    # Edges reference nodes not in the nodes list; should ignore such edges
    nodes = [{"id": 1}, {"id": 2}]
    edges = [{"source": 3, "target": 1}, {"source": 4, "target": 2}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.54μs -> 1.12μs (37.1% faster)


def test_duplicate_node_ids():
    # Duplicate node IDs; should still work and return the first node with no outgoing edges
    nodes = [{"id": 1}, {"id": 1}, {"id": 2}]
    edges = [{"source": 1, "target": 2}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.96μs -> 1.17μs (67.9% faster)


def test_edge_with_missing_source_key():
    # Edge dict missing 'source' key; should raise KeyError
    nodes = [{"id": 1}, {"id": 2}]
    edges = [{"src": 1, "target": 2}]
    with pytest.raises(KeyError):
        find_last_node(nodes, edges)  # 1.83μs -> 792ns (131% faster)


def test_node_with_extra_keys():
    # Nodes have extra keys; function should not be affected
    nodes = [{"id": 1, "foo": "bar"}, {"id": 2, "baz": 123}]
    edges = [{"source": 1, "target": 2}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.83μs -> 1.12μs (62.9% faster)


def test_node_ids_are_strings():
    # Node IDs are strings instead of ints
    nodes = [{"id": "a"}, {"id": "b"}]
    edges = [{"source": "a", "target": "b"}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.83μs -> 1.12μs (63.0% faster)


def test_edge_with_extra_keys():
    # Edges have extra keys; function should not be affected
    nodes = [{"id": 1}, {"id": 2}]
    edges = [{"source": 1, "target": 2, "weight": 5}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.71μs -> 1.08μs (57.8% faster)


def test_node_id_is_none():
    # Node id is None; edge references None
    nodes = [{"id": None}, {"id": 2}]
    edges = [{"source": None, "target": 2}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.92μs -> 1.12μs (70.4% faster)


def test_node_id_type_mismatch():
    # Node id is int, edge source is string
    nodes = [{"id": 1}, {"id": 2}]
    edges = [{"source": "1", "target": 2}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.54μs -> 1.08μs (42.3% faster)


# 3. Large Scale Test Cases


def test_large_linear_chain():
    # Large chain of nodes: 0->1->2->...->999; last node should be 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.1ms -> 56.2μs (32102% faster)


def test_large_star_graph():
    # Star graph: node 0 points to all other nodes; all others are last nodes, should return first found (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  # 37.9μs -> 20.5μs (85.1% faster)


def test_large_no_edges():
    # 1000 nodes, no edges; should return first node
    N = 1000
    nodes = [{"id": i} for i in range(N)]
    edges = []
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.25μs -> 1.00μs (25.0% faster)


def test_large_all_nodes_have_outgoing_edges():
    # 1000 nodes, each node has an outgoing edge (cycle); should return None
    N = 1000
    nodes = [{"id": i} for i in range(N)]
    edges = [{"source": i, "target": (i + 1) % N} for i in range(N)]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 18.3ms -> 55.9μs (32699% 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
from src.algorithms.graph import find_last_node

# unit tests

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


def test_single_node_no_edges():
    # Only one node, no edges: should return the node itself
    nodes = [{"id": 1, "label": "A"}]
    edges = []
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.21μs -> 958ns (26.1% faster)


def test_two_nodes_one_edge():
    # Two nodes, one edge from A to B: last node is B
    nodes = [{"id": "A"}, {"id": "B"}]
    edges = [{"source": "A", "target": "B"}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.96μs -> 1.08μs (80.8% faster)


def test_three_nodes_linear_chain():
    # Linear chain: A -> B -> C; last node is C
    nodes = [{"id": "A"}, {"id": "B"}, {"id": "C"}]
    edges = [{"source": "A", "target": "B"}, {"source": "B", "target": "C"}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 2.33μs -> 1.17μs (99.9% faster)


def test_multiple_last_nodes():
    # Two disconnected chains: A->B, C->D; last nodes are B and D, should return B (first found)
    nodes = [{"id": "A"}, {"id": "B"}, {"id": "C"}, {"id": "D"}]
    edges = [{"source": "A", "target": "B"}, {"source": "C", "target": "D"}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.92μs -> 1.08μs (76.9% faster)


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


def test_no_nodes():
    # No nodes at all: should return None
    nodes = []
    edges = []
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 709ns -> 833ns (14.9% slower)


def test_nodes_but_no_last_node():
    # All nodes have outgoing edges (cycle): should return None
    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.17μs -> 1.21μs (79.4% faster)


def test_isolated_node_among_chain():
    # Chain: 1->2->3, plus isolated node 99; should return 3 (first last node found)
    nodes = [{"id": 1}, {"id": 2}, {"id": 3}, {"id": 99}]
    edges = [{"source": 1, "target": 2}, {"source": 2, "target": 3}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 2.08μs -> 1.21μs (72.5% faster)


def test_node_with_multiple_incoming_edges():
    # Graph: 1->3, 2->3; last node is 3
    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.08μs -> 1.21μs (72.5% faster)


def test_node_with_self_loop():
    # Node with self-loop: 1->1; no last node
    nodes = [{"id": 1}]
    edges = [{"source": 1, "target": 1}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.38μs -> 1.08μs (27.0% faster)


def test_duplicate_edges():
    # Duplicate edges: 1->2, 1->2; last node is 2
    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.79μs -> 1.21μs (48.3% faster)


def test_node_with_nonexistent_target():
    # Edge points to a node not in nodes; should not affect result
    nodes = [{"id": "A"}, {"id": "B"}]
    edges = [
        {"source": "A", "target": "B"},
        {"source": "B", "target": "C"},
    ]  # "C" not in nodes
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.96μs -> 1.21μs (62.1% faster)


def test_node_with_nonexistent_source():
    # Edge from node not in nodes; should not affect result
    nodes = [{"id": "A"}, {"id": "B"}]
    edges = [{"source": "Z", "target": "A"}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.46μs -> 1.08μs (34.6% faster)


def test_nodes_with_extra_properties():
    # Node dicts have extra fields; should still match by "id"
    nodes = [{"id": 1, "name": "foo"}, {"id": 2, "name": "bar"}]
    edges = [{"source": 1, "target": 2}]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 1.75μs -> 1.12μs (55.6% faster)


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


def test_large_linear_chain():
    # Large chain: 0->1->2->...->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.2ms -> 56.2μs (32265% faster)


def test_large_star_graph():
    # One central node (0) points to all others; all others are last nodes
    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.0μs -> 20.3μs (87.3% faster)


def test_large_disconnected_graph():
    # 500 isolated nodes, 500 in a chain; last node is first found among isolated or chain
    N = 500
    isolated_nodes = [{"id": f"iso_{i}"} for i in range(N)]
    chain_nodes = [{"id": f"chain_{i}"} for i in range(N)]
    nodes = isolated_nodes + chain_nodes
    edges = [{"source": f"chain_{i}", "target": f"chain_{i+1}"} for i in range(N - 1)]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 21.1μs -> 24.8μs (14.8% slower)


def test_large_cycle():
    # Large cycle: all nodes have outgoing edges; should return None
    N = 1000
    nodes = [{"id": i} for i in range(N)]
    edges = [{"source": i, "target": (i + 1) % N} for i in range(N)]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 18.4ms -> 55.8μs (32818% faster)


def test_large_graph_with_multiple_last_nodes():
    # Two large chains: 0->1->...->499, 500->501->...->999; last nodes are 499 and 999
    N = 500
    nodes = [{"id": i} for i in range(2 * N)]
    edges = [{"source": i, "target": i + 1} for i in range(N - 1)] + [
        {"source": i, "target": i + 1} for i in range(N, 2 * N - 1)
    ]
    codeflash_output = find_last_node(nodes, edges)
    result = codeflash_output  # 4.63ms -> 38.2μs (11995% 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-mjnhr8my and push.

Codeflash Static Badge

The optimization transforms an **O(n × m) nested loop** into an **O(n + m) linear scan** by eliminating redundant edge traversals.

**What changed:**
- **Pre-computed source lookup**: Instead of checking `all(e["source"] != n["id"] for e in edges)` for every node (which scans all edges repeatedly), the code now builds a set `source_ids` containing all source IDs upfront
- **Set membership test**: The condition changes from "is this node's ID different from ALL edge sources?" to "is this node's ID NOT IN the set of source IDs?"

**Why it's faster:**
In the original code, for each node, Python iterates through **every** edge to verify none has that node as a source. With N nodes and M edges, this results in N × M comparisons. 

The optimized version:
1. Creates the `source_ids` set in one pass: **O(M) time, O(M) space**
2. For each node, performs a **O(1) hash lookup** instead of O(M) iteration

This is a classic **space-time tradeoff**: we use O(M) additional memory for massive time savings.

**Performance impact:**
- **Small graphs** (2-4 nodes/edges): 30-100% faster due to reduced Python interpreter overhead
- **Large linear chains** (1000 nodes): **32,000%+ faster** - the quadratic nested loop becomes linear
- **Large cycles** (1000 nodes, all connected): **32,000%+ faster** - eliminates catastrophic N×M behavior
- **Star graphs**: 85% faster even though the original code exits early for most nodes
- **Empty edges**: Slightly slower (10-15%) due to set creation overhead, but negligible in absolute terms (nanoseconds)

The optimization particularly shines when there are many edges to check against each node, transforming what would be prohibitively slow for production graphs into microsecond-level performance.
@codeflash-ai codeflash-ai bot requested a review from KRRT7 December 26, 2025 23:17
@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