Skip to main content

Visualizing Recursion Trees: A Step-by-Step Guide to Recursive Function Execution

By Han Qi

Recursion Execution Path

How difficult would it be to create a recursion visualization? Turns out, way harder than expected.

How to Reproduce

The following gist can be executed to experiment with recursion visualization:

recursion_viz.ipynb

This script generates two output files in the same directory for visualization:

  1. graph_data.json
  2. visualization.html

This article documents multiple failed attempts before achieving a clear visualization, ultimately providing insights into human-AI interaction.

Motivation

I wanted to understand how recursive functions work, but VSCode’s debugger proved unsuitable because:

  1. It forces users to click through stack frames one at a time, making it too linear.
  2. It provides too much granularity—I only wanted to see the overall call graph.

Upon searching for a “recursion tree visualizer,” I found no satisfactory results on the first page of Google.

Requirements

  1. Allow users to paste any custom function for visualization.
  2. Provide an easy way to define global variables.
  3. Include widgets (e.g., sliders) for stepping through execution.

Existing Options

The Plan

  1. Get a basic visualization working with free ChatGPT.
  2. Experiment with “vibe coding.”
  3. Assess patience levels before manually fixing AI-generated output.

The Journey

Attempted approaches:

  1. Basic function visualization.
  2. Tree structures with indentation.
  3. Use of Graphviz.
  4. D3.js (had CORS issues).
  5. Ipywidgets with Plotly.
  6. Matplotlib (ax.plot) – initially not refreshing.
  7. Creating a simplified working example.
  8. Switching to plt.show for debugging.
  9. Fixing ax.plot refresh issues.
  10. Refining visualization with decorators.

The Function: Permutations with Recursion

def permute(nums):
def backtrack(first=0):
if first == len(nums) - 1:
result.append(nums[:])
return
for i in range(first, len(nums)):
nums[first], nums[i] = nums[i], nums[first] # Swap
backtrack(first + 1)
nums[first], nums[i] = nums[i], nums[first] # Swap back

result = []
backtrack()
return result

print(permute([1, 2, 3]))

Output:

[[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 2, 1], [3, 1, 2]]

The base case is reached at first == len(nums) - 1. Visualization clarified that an extra unnecessary call was originally present.

Using Graphviz for Better Visualization

Graphviz was used to generate a graphical representation of recursion.

from graphviz import Digraph

def permute(nums):
graph = Digraph(format="png")
node_id = 0

def backtrack(first=0, depth=0, parent=None):
nonlocal node_id
node_label = f"{first}: {nums}"
current_id = str(node_id)
graph.node(current_id, label=node_label)
node_id += 1

if parent is not None:
graph.edge(parent, current_id)

if first == len(nums) - 1:
return

for i in range(first, len(nums)):
nums[first], nums[i] = nums[i], nums[first]
backtrack(first + 1, depth + 1, current_id)
nums[first], nums[i] = nums[i], nums[first]

backtrack()
return graph

Advantages:

  • Shows all calls at once.
  • Highlights hierarchy clearly.
  • Provides interactive visualization with colors.

Adding Interactivity with D3.js

Generated interactive visualizations using HTML and JSON. However, CORS issues required running a local server:

python3 -m http.server 8000 --bind 0.0.0.0

Refactoring with Decorators

Class-based decorators tracked execution while preserving function signatures.

class Visualizer:
def __init__(self, func):
self.func = func
self.graph = Digraph(format="png")
self.node_id = 0
self.parent = None
self.depth = -1
self.nodes_data = []

def __call__(self, *args, **kwargs):
node_label = f"{args=}"
current_id = str(self.node_id)
self.node_id += 1
self.graph.node(current_id, label=node_label)

if self.parent is not None:
self.graph.edge(self.parent, current_id)

self.nodes_data.append((current_id, self.depth + 1))
previous_parent = self.parent
self.parent = current_id
self.depth += 1

self.func(*args, **kwargs)

self.parent = previous_parent
self.depth -= 1

return self.nodes_data, self.graph

Lessons Learned

  1. Prompting Efficiency – Refining GPT usage improved results.
  2. Debugging Methods – Order of operations mattered.
  3. Trade-offs in Automation – Some tasks still required manual debugging.
  4. Balancing Constraints & AI Use – AI was useful for partial solutions but had nuance limitations.

Conclusion

While AI-assisted coding can help accelerate development, critical thinking and debugging remain essential. Developers must balance human intuition with AI-generated code for efficiency and accuracy.

Leave a Reply

Close Menu

Wow look at this!

This is an optional, highly
customizable off canvas area.

About Salient

The Castle
Unit 345
2500 Castle Dr
Manhattan, NY

T: +216 (0)40 3629 4753
E: hello@themenectar.com