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.
Module Contents
Learning Resources
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
Type | Description |
---|---|
Binary Tree | Each 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 Tree | Self-balancing BST where heights of left and right subtrees differ by at most 1 |
Red-Black Tree | Self-balancing BST with additional properties to ensure balance |
B-Tree | Self-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.
Explanation:
Click play or step to start the visualization.
Traversal Path:
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.
Queue/Stack:
Explanation:
Click play or step to start the visualization.
Visited Nodes:
Path:
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.
2. Validate Binary Search Tree
Determine if a given binary tree is a valid binary search tree (BST).
3. Number of Islands
Count the number of islands in a 2D grid (an island is formed by connecting adjacent lands horizontally or vertically).
4. Course Schedule
Determine if it's possible to finish all courses given the prerequisites (detect cycle in directed graph).