Trees & Graphs

Trees and graphs are powerful data structures used to represent hierarchical and network relationships. This module covers their implementations, traversal algorithms, and common applications in problem-solving.

Introduction to Trees & Graphs

Trees and graphs are non-linear data structures that represent relationships between elements. They are essential for modeling real-world problems and are used extensively in computer science.

Trees

A tree is a hierarchical data structure consisting of nodes connected by edges. Each tree has a root node, and every node (except the root) has exactly one parent node.

  • Root: The topmost node of a tree
  • Parent: A node that has one or more child nodes
  • Child: A node that has a parent node
  • Leaf: A node that has no children
  • Subtree: A tree consisting of a node and all its descendants
  • Depth: The length of the path from the root to a node
  • Height: The length of the longest path from a node to a leaf

Graphs

A graph is a collection of nodes (vertices) connected by edges. Unlike trees, graphs can have cycles and multiple paths between nodes.

  • Vertex: A node in the graph
  • Edge: A connection between two vertices
  • Directed Graph: Edges have a direction
  • Undirected Graph: Edges have no direction
  • Weighted Graph: Edges have weights or costs
  • Path: A sequence of vertices connected by edges
  • Cycle: A path that starts and ends at the same vertex

Types of Trees

TypeDescription
Binary TreeEach node has at most two children
Binary Search Tree (BST)A binary tree where left child is less than parent and right child is greater
AVL TreeSelf-balancing BST where heights of left and right subtrees differ by at most 1
Red-Black TreeSelf-balancing BST with additional properties to ensure balance
B-TreeSelf-balancing tree with multiple keys per node, used in databases and file systems

Tree Implementation

Binary Tree in Java

// Binary Tree implementation in Java
class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    
    public TreeNode(int val) {
        this.val = val;
        this.left = null;
        this.right = null;
    }
}

class BinaryTree {
    TreeNode root;
    
    public BinaryTree() {
        root = null;
    }
    
    // Insert a new node
    public void insert(int val) {
        root = insertRec(root, val);
    }
    
    private TreeNode insertRec(TreeNode root, int val) {
        // If the tree is empty, create a new node
        if (root == null) {
            root = new TreeNode(val);
            return root;
        }
        
        // Otherwise, recur down the tree
        if (val < root.val) {
            root.left = insertRec(root.left, val);
        } else if (val > root.val) {
            root.right = insertRec(root.right, val);
        }
        
        // Return the unchanged node pointer
        return root;
    }
    
    // Inorder traversal
    public void inorder() {
        inorderRec(root);
    }
    
    private void inorderRec(TreeNode root) {
        if (root != null) {
            inorderRec(root.left);
            System.out.print(root.val + " ");
            inorderRec(root.right);
        }
    }
    
    // Search for a node
    public boolean search(int val) {
        return searchRec(root, val);
    }
    
    private boolean searchRec(TreeNode root, int val) {
        // Base case: root is null or value is present at root
        if (root == null) {
            return false;
        }
        if (root.val == val) {
            return true;
        }
        
        // Value is greater than root's value
        if (root.val < val) {
            return searchRec(root.right, val);
        }
        
        // Value is less than root's value
        return searchRec(root.left, val);
    }
}

Binary Tree in Python

# Binary Tree implementation in Python
class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

class BinaryTree:
    def __init__(self):
        self.root = None
    
    # Insert a new node
    def insert(self, val):
        self.root = self._insert_recursive(self.root, val)
    
    def _insert_recursive(self, root, val):
        # If the tree is empty, create a new node
        if root is None:
            return TreeNode(val)
        
        # Otherwise, recur down the tree
        if val < root.val:
            root.left = self._insert_recursive(root.left, val)
        elif val > root.val:
            root.right = self._insert_recursive(root.right, val)
        
        # Return the unchanged node pointer
        return root
    
    # Inorder traversal
    def inorder(self):
        result = []
        self._inorder_recursive(self.root, result)
        return result
    
    def _inorder_recursive(self, root, result):
        if root:
            self._inorder_recursive(root.left, result)
            result.append(root.val)
            self._inorder_recursive(root.right, result)
    
    # Search for a node
    def search(self, val):
        return self._search_recursive(self.root, val)
    
    def _search_recursive(self, root, val):
        # Base case: root is null or value is present at root
        if root is None:
            return False
        if root.val == val:
            return True
        
        # Value is greater than root's value
        if root.val < val:
            return self._search_recursive(root.right, val)
        
        # Value is less than root's value
        return self._search_recursive(root.left, val)

Graph Implementation

Graph in Java (Adjacency List)

// Graph implementation in Java using adjacency list
import java.util.*;

class Graph {
    private int V; // Number of vertices
    private LinkedList<Integer>[] adj; // Adjacency list
    
    // Constructor
    @SuppressWarnings("unchecked")
    public Graph(int v) {
        V = v;
        adj = new LinkedList[v];
        for (int i = 0; i < v; ++i) {
            adj[i] = new LinkedList<>();
        }
    }
    
    // Add an edge to the graph
    public void addEdge(int v, int w) {
        adj[v].add(w); // Add w to v's list
    }
    
    // BFS traversal from a given source
    public void BFS(int s) {
        // Mark all vertices as not visited
        boolean[] visited = new boolean[V];
        
        // Create a queue for BFS
        LinkedList<Integer> queue = new LinkedList<>();
        
        // Mark the current node as visited and enqueue it
        visited[s] = true;
        queue.add(s);
        
        while (!queue.isEmpty()) {
            // Dequeue a vertex from queue and print it
            s = queue.poll();
            System.out.print(s + " ");
            
            // Get all adjacent vertices of the dequeued vertex
            // If an adjacent has not been visited, mark it visited and enqueue it
            for (int n : adj[s]) {
                if (!visited[n]) {
                    visited[n] = true;
                    queue.add(n);
                }
            }
        }
    }
    
    // DFS traversal from a given source
    public void DFS(int s) {
        // Mark all vertices as not visited
        boolean[] visited = new boolean[V];
        
        // Call the recursive helper function to print DFS traversal
        DFSUtil(s, visited);
    }
    
    // A recursive function used by DFS
    private void DFSUtil(int v, boolean[] visited) {
        // Mark the current node as visited and print it
        visited[v] = true;
        System.out.print(v + " ");
        
        // Recur for all the vertices adjacent to this vertex
        for (int n : adj[v]) {
            if (!visited[n]) {
                DFSUtil(n, visited);
            }
        }
    }
}

Graph in Python (Adjacency List)

# Graph implementation in Python using adjacency list
from collections import defaultdict, deque

class Graph:
    def __init__(self):
        self.graph = defaultdict(list)
    
    # Add an edge to the graph
    def add_edge(self, u, v):
        self.graph[u].append(v)
    
    # BFS traversal from a given source
    def bfs(self, s):
        # Mark all vertices as not visited
        visited = set()
        
        # Create a queue for BFS
        queue = deque([s])
        visited.add(s)
        
        result = []
        
        while queue:
            # Dequeue a vertex from queue
            vertex = queue.popleft()
            result.append(vertex)
            
            # Get all adjacent vertices of the dequeued vertex
            # If an adjacent has not been visited, mark it visited and enqueue it
            for neighbor in self.graph[vertex]:
                if neighbor not in visited:
                    visited.add(neighbor)
                    queue.append(neighbor)
        
        return result
    
    # DFS traversal from a given source
    def dfs(self, s):
        # Mark all vertices as not visited
        visited = set()
        
        # Call the recursive helper function to print DFS traversal
        result = []
        self._dfs_util(s, visited, result)
        return result
    
    # A recursive function used by DFS
    def _dfs_util(self, v, visited, result):
        # Mark the current node as visited and add to result
        visited.add(v)
        result.append(v)
        
        # Recur for all the vertices adjacent to this vertex
        for neighbor in self.graph[v]:
            if neighbor not in visited:
                self._dfs_util(neighbor, visited, result)

Traversal Algorithms

Tree Traversal

Tree traversal is the process of visiting each node in a tree exactly once. There are three common ways to traverse a binary tree:

  • Inorder (Left, Root, Right): Visit left subtree, then root, then right subtree
  • Preorder (Root, Left, Right): Visit root, then left subtree, then right subtree
  • Postorder (Left, Right, Root): Visit left subtree, then right subtree, then root
// Tree traversal in Java
public void inorderTraversal(TreeNode root) {
    if (root != null) {
        inorderTraversal(root.left);
        System.out.print(root.val + " ");
        inorderTraversal(root.right);
    }
}

public void preorderTraversal(TreeNode root) {
    if (root != null) {
        System.out.print(root.val + " ");
        preorderTraversal(root.left);
        preorderTraversal(root.right);
    }
}

public void postorderTraversal(TreeNode root) {
    if (root != null) {
        postorderTraversal(root.left);
        postorderTraversal(root.right);
        System.out.print(root.val + " ");
    }
}

Graph Traversal

Graph traversal is the process of visiting each vertex in a graph exactly once. There are two common ways to traverse a graph:

  • Breadth-First Search (BFS): Visit all neighbors at the current depth before moving to nodes at the next depth level
  • Depth-First Search (DFS): Explore as far as possible along each branch before backtracking
# Graph traversal in Python
def bfs(graph, start):
    visited = set()
    queue = deque([start])
    visited.add(start)
    
    while queue:
        vertex = queue.popleft()
        print(vertex, end=" ")
        
        for neighbor in graph[vertex]:
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(neighbor)

def dfs(graph, start, visited=None):
    if visited is None:
        visited = set()
    
    visited.add(start)
    print(start, end=" ")
    
    for neighbor in graph[start]:
        if neighbor not in visited:
            dfs(graph, neighbor, visited)

Tree Visualization

Visualizing tree traversal algorithms helps in understanding how they work. Use the interactive visualization below to see different tree traversal algorithms in action.

Tree Traversal

Inorder traversal visits the left subtree, then the root, and finally the right subtree. For a binary search tree, this visits nodes in ascending order.

Speed:50%
Step: 0 / 0

Explanation:

Click play or step to start the visualization.

Traversal Path:

None

The visualization above demonstrates different tree traversal algorithms. You can select between inorder, preorder, postorder, and level order traversals to see how each algorithm visits the nodes in a binary tree. Use the controls to step through the traversal process and observe the order in which nodes are visited.

Graph Visualization

Visualizing graph algorithms helps in understanding how they work. Use the interactive visualization below to see different graph traversal and shortest path algorithms in action.

Graph Algorithms

Breadth-First Search (BFS) explores all the vertices of a graph at the present depth before moving on to vertices at the next depth level.

Speed:50%
Step: 0 / 0

Queue/Stack:

Empty

Explanation:

Click play or step to start the visualization.

Visited Nodes:

None

Path:

None

The visualization above demonstrates different graph algorithms including Breadth-First Search (BFS), Depth-First Search (DFS), and Dijkstra's shortest path algorithm. You can observe how these algorithms explore the graph and find paths between nodes. Use the controls to step through the algorithm execution and see the visited nodes, paths, and queue/stack states.

Practice Problems

Apply your knowledge of trees and graphs by solving these carefully selected problems:

1. Maximum Depth of Binary Tree

Find the maximum depth (height) of a binary tree.

EasyTreeDFS

2. Validate Binary Search Tree

Determine if a given binary tree is a valid binary search tree (BST).

MediumTreeDFS

3. Number of Islands

Count the number of islands in a 2D grid (an island is formed by connecting adjacent lands horizontally or vertically).

MediumGraphBFS/DFS

4. Course Schedule

Determine if it's possible to finish all courses given the prerequisites (detect cycle in directed graph).

MediumGraphTopological Sort