AlgoTree: Comprehensive Tutorial

1. Introduction

AlgoTree is a Python library that provides flexible and powerful tools for working with tree-like data structures. It offers various implementations of trees, along with utilities for manipulation, traversal, and visualization. The key feature of AlgoTree is its node-centric API, which provides a consistent interface across different tree implementations.

2. Installation

To install AlgoTree, use pip:

pip install algotree

3. Core Concepts

Node-Centric API

AlgoTree implements a node-centric API, which means that operations are performed from the perspective of individual nodes rather than the tree as a whole. This approach provides a consistent interface across different tree implementations and allows for intuitive navigation and manipulation of tree structures.

Tree Node Concept

In AlgoTree, a tree node must implement the following properties and methods:

  • children: A list of child nodes.

  • parent: The parent node (None for root nodes).

  • node(name: str) -> NodeType: Returns a node in the current subtree by name.

  • root: The root node of the current subtree.

  • payload: The data associated with the node.

  • name: An identifier for the node.

  • contains(name) -> bool: Checks if a node with the given name exists in the subtree.

Optional methods:

  • subtree(name: Optional[str] = None) -> NodeType: Returns a view of another subtree.

4. FlatForest: A Flexible Tree Structure

FlatForest represents a tree or forest using a flat dictionary structure.

from AlgoTree.flat_forest import FlatForest
from AlgoTree.flat_forest_node import FlatForestNode

data = {
    "A": {"parent": None, "data": "Root"},
    "B": {"parent": "A", "data": "Child of A"},
    "C": {"parent": "A", "data": "Another child of A"},
    "D": {"parent": "B", "data": "Child of B"}
}

forest = FlatForest(data)

# Accessing nodes
root = forest.subtree("A")
print(root)  # FlatForestNode(name=A, parent=None, payload={'data': 'Root'}, root=A, children=['B', 'C'])

# Adding a new node
forest.node("C").add_child(name="E", data="Child of C")

# Traversing the tree
for node in forest.subtree("A").children:
    print(node.name, node.payload)

5. TreeNode: A Simple Recursive Tree Structure

TreeNode provides a traditional recursive tree structure.

from AlgoTree.treenode import TreeNode

root = TreeNode(name="root", payload={"value": 0})
a = TreeNode(name="A", parent=root, payload={"value": 1})
b = TreeNode(name="B", parent=root, payload={"value": 2})
c = TreeNode(name="C", parent=a, payload={"value": 3})

print(root.children)  # [TreeNode(name=A, ...), TreeNode(name=B, ...)]
print(c.parent.name)  # A

# Cloning a subtree
cloned_a = a.clone()
print(cloned_a)  # TreeNode(name=A, parent=None, root=A, payload={'value': 1}, len(children)=1)

6. Tree Traversal and Manipulation

AlgoTree provides utility functions for tree traversal and manipulation:

from AlgoTree import utils

# Get descendants
descendants = utils.descendants(root)
print([node.name for node in descendants])  # ['A', 'B', 'C']

# Find leaves
leaves = utils.leaves(root)
print([node.name for node in leaves])  # ['C', 'B']

# Get tree height
height = utils.height(root)
print(height)  # 2

# Breadth-first traversal
def print_node(node, level):
    print(f"Level {level}: {node.name}")
    return False

utils.breadth_first(root, print_node)
# Output:
# Level 0: root
# Level 1: A
# Level 1: B
# Level 2: C

7. Tree Visualization

Use the pretty_tree function to visualize trees:

from AlgoTree.pretty_tree import pretty_tree

print(pretty_tree(root, node_details=lambda n: n.payload['value']))

Output:

root ◄ 0
├───── A ◄ 1
│      └───── C ◄ 3
└───── B ◄ 2

8. Tree Conversion

AlgoTree allows conversion between different tree representations:

from AlgoTree.tree_converter import TreeConverter

# Convert FlatForest to TreeNode
flat_forest_root = forest.subtree("A")
tree_node_root = TreeConverter.convert(flat_forest_root, TreeNode)

# Convert TreeNode to dictionary
tree_dict = TreeConverter.to_dict(tree_node_root)
print(tree_dict)

9. Advanced Features

Node Hashing

AlgoTree provides various hash functions for comparing nodes and trees:

from AlgoTree.node_hash import NodeHash

# Assuming we have two nodes 'node1' and 'node2'
print(NodeHash.name_hash(node1) == NodeHash.name_hash(node2))  # Compare nodes by name
print(NodeHash.node_hash(node1) == NodeHash.node_hash(node2))  # Compare nodes by name and payload
print(NodeHash.tree_hash(node1) == NodeHash.tree_hash(node2))  # Compare entire subtrees

10. Working with Subtrees

AlgoTree allows you to work with subtrees, maintaining consistency with the original tree:

# Using FlatForest
subtree_B = forest.subtree("B")
print(pretty_tree(subtree_B))

# Add a child to the subtree
subtree_B.add_child(name="F", data="Child of B")

# The change is reflected in the original tree
print(pretty_tree(forest.subtree("A")))

# Using TreeNode
subtree_A = root.node("A")
print(pretty_tree(subtree_A))

# Modify the subtree
subtree_A.add_child(name="D", payload={"value": 4})

# The change is reflected in the original tree
print(pretty_tree(root))

11. Expression Trees and Evaluation

Let’s create and evaluate an expression tree using AlgoTree:

expr = TreeNode.from_dict({
    "value": "+",
    "type": "op",
    "children": [
        {
            "value": "max",
            "type": "op",
            "children": [
                {"type": "var", "value": "x"},
                {"type": "const", "value": 1},
            ],
        },
        {
            "type": "op",
            "value": "+",
            "children": [
                {"type": "var", "value": "y"},
                {"type": "const", "value": 3},
            ],
        },
    ],
})

print(pretty_tree(expr, node_name=lambda x: x.payload["value"]))

def evaluate(node, context):
    if node.payload["type"] == "const":
        return node.payload["value"]
    elif node.payload["type"] == "var":
        return context[node.payload["value"]]
    elif node.payload["type"] == "op":
        if node.payload["value"] == "+":
            return sum(evaluate(child, context) for child in node.children)
        elif node.payload["value"] == "max":
            return max(evaluate(child, context) for child in node.children)

context = {"x": 2, "y": 5}
result = evaluate(expr, context)
print(f"Result: {result}")  # Should print: Result: 9

12. Conclusion

This tutorial has covered the main features of AlgoTree, demonstrating how to work with different tree implementations, traverse and manipulate trees, visualize tree structures, and even create and evaluate expression trees. The node-centric API provides a consistent interface across different tree types, making it easier to work with complex tree structures in Python.

Remember that AlgoTree is flexible and can be extended to suit various tree-based applications. Whether you’re working on data structures, parsing, or any domain that requires hierarchical data representation, AlgoTree provides a solid foundation for your tree-related operations.