Prerequisites
- Basic understanding of programming concepts ๐
- Python installation (3.8+) ๐
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write clean, Pythonic code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on Graph Representation using Adjacency Lists! ๐ In this guide, weโll explore how to represent graphs efficiently in Python using one of the most popular and flexible data structures.
Youโll discover how adjacency lists can transform your ability to work with networks, relationships, and connected data. Whether youโre building social networks ๐, route planning systems ๐บ๏ธ, or recommendation engines ๐ฏ, understanding adjacency lists is essential for efficient graph manipulation.
By the end of this tutorial, youโll feel confident implementing and using adjacency lists in your own projects! Letโs dive in! ๐โโ๏ธ
๐ Understanding Adjacency Lists
๐ค What is an Adjacency List?
An adjacency list is like a phone book for graphs! ๐ฑ Think of it as a collection where each person (node) has their own list of contacts (connected nodes) that they can directly call.
In Python terms, an adjacency list is a way to represent a graph where each vertex stores a list of its neighboring vertices. This means you can:
- โจ Efficiently store sparse graphs (graphs with few edges)
- ๐ Quickly find all neighbors of a vertex
- ๐ก๏ธ Use minimal memory for large graphs
๐ก Why Use Adjacency Lists?
Hereโs why developers love adjacency lists:
- Space Efficiency ๐: Only stores existing edges
- Fast Neighbor Access ๐ป: O(1) to get all neighbors
- Dynamic Structure ๐: Easy to add/remove vertices and edges
- Flexible Representation ๐ง: Works with weighted and directed graphs
Real-world example: Imagine building a social network ๐. With adjacency lists, you can efficiently store each userโs friends list without wasting space on non-existent friendships!
๐ง Basic Syntax and Usage
๐ Simple Example
Letโs start with a friendly example:
# ๐ Hello, Graph Adjacency Lists!
# ๐จ Creating a simple graph using a dictionary
graph = {
'A': ['B', 'C'], # ๐ค A is connected to B and C
'B': ['A', 'D', 'E'], # ๐ B is connected to A, D, and E
'C': ['A', 'F'], # ๐ฏ C is connected to A and F
'D': ['B'], # ๐ D is connected to B
'E': ['B', 'F'], # ๐ E is connected to B and F
'F': ['C', 'E'] # ๐ F is connected to C and E
}
# ๐ Accessing neighbors
print(f"Neighbors of A: {graph['A']}") # Output: ['B', 'C']
๐ก Explanation: Notice how we use a dictionary where keys are vertices and values are lists of neighbors. Simple and efficient!
๐ฏ Common Patterns
Here are patterns youโll use daily:
# ๐๏ธ Pattern 1: Graph class implementation
class Graph:
def __init__(self):
self.adjacency_list = {} # ๐ฆ Initialize empty graph
# โ Add a vertex
def add_vertex(self, vertex):
if vertex not in self.adjacency_list:
self.adjacency_list[vertex] = []
print(f"โจ Added vertex: {vertex}")
# ๐ Add an edge (undirected)
def add_edge(self, v1, v2):
if v1 in self.adjacency_list and v2 in self.adjacency_list:
self.adjacency_list[v1].append(v2)
self.adjacency_list[v2].append(v1)
print(f"๐ Connected {v1} โ๏ธ {v2}")
# ๐ฎ Using our graph
my_graph = Graph()
my_graph.add_vertex("Alice")
my_graph.add_vertex("Bob")
my_graph.add_edge("Alice", "Bob")
๐ก Practical Examples
๐ Example 1: Social Network Friend System
Letโs build something real:
# ๐ Social Network using Adjacency Lists
class SocialNetwork:
def __init__(self):
self.users = {} # ๐ฅ User -> Friends list
self.user_data = {} # ๐ Additional user info
# ๐ค Add a new user
def add_user(self, username, emoji="๐"):
if username not in self.users:
self.users[username] = []
self.user_data[username] = {"emoji": emoji, "friend_count": 0}
print(f"{emoji} {username} joined the network!")
# ๐ค Add friendship (mutual connection)
def add_friendship(self, user1, user2):
if user1 in self.users and user2 in self.users:
if user2 not in self.users[user1]:
self.users[user1].append(user2)
self.users[user2].append(user1)
self.user_data[user1]["friend_count"] += 1
self.user_data[user2]["friend_count"] += 1
print(f"๐ {user1} and {user2} are now friends!")
# ๐ Get friend recommendations
def get_friend_suggestions(self, user):
if user not in self.users:
return []
suggestions = set()
# Friends of friends who aren't already friends
for friend in self.users[user]:
for friend_of_friend in self.users[friend]:
if friend_of_friend != user and friend_of_friend not in self.users[user]:
suggestions.add(friend_of_friend)
return list(suggestions)
# ๐ Display network stats
def show_network(self):
print("\n๐ Social Network Status:")
for user, friends in self.users.items():
emoji = self.user_data[user]["emoji"]
print(f" {emoji} {user}: {len(friends)} friends โ {friends}")
# ๐ฎ Let's use it!
network = SocialNetwork()
network.add_user("Alice", "๐ฉ")
network.add_user("Bob", "๐จ")
network.add_user("Charlie", "๐ง")
network.add_user("Diana", "๐ฉโ๐ผ")
network.add_user("Eve", "๐ฉโ๐จ")
network.add_friendship("Alice", "Bob")
network.add_friendship("Bob", "Charlie")
network.add_friendship("Charlie", "Diana")
network.add_friendship("Bob", "Eve")
network.show_network()
print(f"\n๐ก Friend suggestions for Alice: {network.get_friend_suggestions('Alice')}")
๐ฏ Try it yourself: Add a method to find the shortest path between two users!
๐ฎ Example 2: City Route Planner
Letโs make it fun with a route planning system:
# ๐บ๏ธ City Route Planner with Weighted Edges
class CityMap:
def __init__(self):
self.cities = {} # City -> [(neighbor, distance, emoji)]
# ๐๏ธ Add a city
def add_city(self, name, emoji="๐๏ธ"):
if name not in self.cities:
self.cities[name] = []
print(f"{emoji} Added city: {name}")
# ๐ฃ๏ธ Add a road between cities
def add_road(self, city1, city2, distance, road_type="๐ฃ๏ธ"):
if city1 in self.cities and city2 in self.cities:
self.cities[city1].append((city2, distance, road_type))
self.cities[city2].append((city1, distance, road_type))
print(f"{road_type} Connected {city1} โ๏ธ {city2} ({distance} km)")
# ๐ Find all routes from a city
def find_routes_from(self, start_city):
if start_city not in self.cities:
return []
routes = []
for destination, distance, road_type in self.cities[start_city]:
routes.append({
"to": destination,
"distance": distance,
"road": road_type,
"travel_time": distance / 60 # Assuming 60 km/h average
})
return routes
# ๐ฏ Find cities within distance
def find_nearby_cities(self, city, max_distance):
if city not in self.cities:
return []
nearby = []
for dest, dist, road in self.cities[city]:
if dist <= max_distance:
nearby.append((dest, dist, road))
return sorted(nearby, key=lambda x: x[1]) # Sort by distance
# ๐ Display map
def show_map(self):
print("\n๐บ๏ธ City Map Network:")
for city, connections in self.cities.items():
print(f"\n๐ {city}:")
for dest, dist, road in connections:
print(f" {road} โ {dest} ({dist} km)")
# ๐ฎ Create our map!
city_map = CityMap()
city_map.add_city("Paris", "๐ผ")
city_map.add_city("London", "๐ฐ")
city_map.add_city("Berlin", "๐๏ธ")
city_map.add_city("Rome", "๐๏ธ")
city_map.add_city("Madrid", "๐ฐ")
city_map.add_road("Paris", "London", 344, "๐") # High-speed rail
city_map.add_road("Paris", "Berlin", 878, "โ๏ธ") # Flight
city_map.add_road("Berlin", "Rome", 1181, "๐ฃ๏ธ") # Highway
city_map.add_road("Rome", "Madrid", 1365, "โ๏ธ") # Flight
city_map.add_road("Madrid", "Paris", 1052, "๐") # High-speed rail
city_map.show_map()
print(f"\n๐ Routes from Paris: {city_map.find_routes_from('Paris')}")
print(f"\n๐ Cities within 1000km of Berlin: {city_map.find_nearby_cities('Berlin', 1000)}")
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Directed Graphs
When youโre ready to level up, try directed graphs:
# ๐ฏ Advanced: Directed Graph with Cycle Detection
class DirectedGraph:
def __init__(self):
self.adj_list = {}
def add_vertex(self, v):
if v not in self.adj_list:
self.adj_list[v] = []
def add_directed_edge(self, from_v, to_v):
if from_v in self.adj_list and to_v in self.adj_list:
self.adj_list[from_v].append(to_v)
print(f"โก๏ธ {from_v} โ {to_v}")
# ๐ Detect cycles using DFS
def has_cycle(self):
visited = set()
rec_stack = set()
def dfs(vertex):
visited.add(vertex)
rec_stack.add(vertex)
for neighbor in self.adj_list.get(vertex, []):
if neighbor not in visited:
if dfs(neighbor):
return True
elif neighbor in rec_stack:
return True
rec_stack.remove(vertex)
return False
for vertex in self.adj_list:
if vertex not in visited:
if dfs(vertex):
return True
return False
# ๐ช Using directed graphs
task_graph = DirectedGraph()
for task in ["A", "B", "C", "D"]:
task_graph.add_vertex(task)
task_graph.add_directed_edge("A", "B") # A must complete before B
task_graph.add_directed_edge("B", "C") # B must complete before C
task_graph.add_directed_edge("C", "D") # C must complete before D
# task_graph.add_directed_edge("D", "A") # This would create a cycle!
print(f"\n๐ Has cycle? {task_graph.has_cycle()}")
๐๏ธ Advanced Topic 2: Graph Traversal Algorithms
For the brave developers - BFS and DFS:
# ๐ Graph Traversal: BFS and DFS
from collections import deque
class GraphTraversal:
def __init__(self):
self.graph = {}
def add_edge(self, u, v):
if u not in self.graph:
self.graph[u] = []
if v not in self.graph:
self.graph[v] = []
self.graph[u].append(v)
self.graph[v].append(u)
# ๐ Breadth-First Search
def bfs(self, start):
visited = set()
queue = deque([start])
visited.add(start)
path = []
while queue:
vertex = queue.popleft()
path.append(vertex)
for neighbor in self.graph.get(vertex, []):
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
return path
# ๐๏ธ Depth-First Search
def dfs(self, start):
visited = set()
path = []
def dfs_helper(vertex):
visited.add(vertex)
path.append(vertex)
for neighbor in self.graph.get(vertex, []):
if neighbor not in visited:
dfs_helper(neighbor)
dfs_helper(start)
return path
# ๐ฎ Test traversals
g = GraphTraversal()
edges = [("A", "B"), ("A", "C"), ("B", "D"), ("C", "E"), ("D", "E"), ("D", "F")]
for u, v in edges:
g.add_edge(u, v)
print(f"๐ BFS from A: {g.bfs('A')}")
print(f"๐๏ธ DFS from A: {g.dfs('A')}")
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Missing Vertex Check
# โ Wrong way - no vertex validation!
graph = {'A': ['B'], 'B': ['A']}
graph['C'].append('D') # ๐ฅ KeyError: 'C' doesn't exist!
# โ
Correct way - always check first!
graph = {'A': ['B'], 'B': ['A']}
if 'C' in graph:
graph['C'].append('D')
else:
graph['C'] = ['D'] # Create vertex first
print("โ
Created vertex C before adding edge!")
๐คฏ Pitfall 2: Duplicate Edges in Undirected Graphs
# โ Dangerous - duplicate edges!
def add_edge_wrong(graph, u, v):
graph[u].append(v)
graph[v].append(u)
# Calling again creates duplicates!
# โ
Safe - check for existing edges!
def add_edge_safe(graph, u, v):
if v not in graph[u]:
graph[u].append(v)
if u not in graph[v]:
graph[v].append(u)
print(f"โ
Added unique edge {u} โ๏ธ {v}")
๐ ๏ธ Best Practices
- ๐ฏ Use Sets for Unique Edges:
adjacency_list[v] = set()
prevents duplicates - ๐ Validate Vertices: Always check if vertices exist before operations
- ๐ก๏ธ Handle Edge Cases: Empty graphs, self-loops, disconnected components
- ๐จ Choose Right Representation: Lists for simple graphs, dicts for weighted
- โจ Keep It Pythonic: Use defaultdict, list comprehensions, and generators
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Game Quest Dependency System
Create a quest system for an RPG game:
๐ Requirements:
- โ Quests with prerequisites (directed graph)
- ๐ท๏ธ Quest categories (main, side, daily)
- ๐ค Track completed quests per player
- ๐ Unlock new quests when prerequisites are met
- ๐จ Each quest needs an emoji and rewards!
๐ Bonus Points:
- Find all available quests for a player
- Detect circular dependencies
- Calculate quest completion percentage
๐ก Solution
๐ Click to see solution
# ๐ฏ Our quest dependency system!
class QuestSystem:
def __init__(self):
self.quests = {} # quest_id -> quest_data
self.prerequisites = {} # quest_id -> [prerequisite_ids]
self.player_progress = {} # player -> set of completed quests
# ๐ Add a quest
def add_quest(self, quest_id, name, category, emoji, rewards):
self.quests[quest_id] = {
"name": name,
"category": category,
"emoji": emoji,
"rewards": rewards
}
self.prerequisites[quest_id] = []
print(f"{emoji} Added quest: {name}")
# ๐ Add prerequisite
def add_prerequisite(self, quest_id, prereq_id):
if quest_id in self.prerequisites:
self.prerequisites[quest_id].append(prereq_id)
print(f"๐ {quest_id} requires {prereq_id}")
# ๐ค Initialize player
def add_player(self, player_name):
self.player_progress[player_name] = set()
print(f"๐ฎ {player_name} joined the game!")
# โ
Complete quest
def complete_quest(self, player, quest_id):
if player in self.player_progress and quest_id in self.quests:
self.player_progress[player].add(quest_id)
quest = self.quests[quest_id]
print(f"๐ {player} completed {quest['emoji']} {quest['name']}!")
print(f"๐ Rewards: {quest['rewards']}")
# ๐ Get available quests
def get_available_quests(self, player):
if player not in self.player_progress:
return []
completed = self.player_progress[player]
available = []
for quest_id, prereqs in self.prerequisites.items():
if quest_id not in completed:
if all(prereq in completed for prereq in prereqs):
available.append(quest_id)
return available
# ๐ Get completion percentage
def get_completion_rate(self, player):
if player not in self.player_progress:
return 0
completed = len(self.player_progress[player])
total = len(self.quests)
return (completed / total * 100) if total > 0 else 0
# ๐ Check for circular dependencies
def has_circular_dependency(self):
visited = set()
rec_stack = set()
def dfs(quest):
visited.add(quest)
rec_stack.add(quest)
for prereq in self.prerequisites.get(quest, []):
if prereq not in visited:
if dfs(prereq):
return True
elif prereq in rec_stack:
return True
rec_stack.remove(quest)
return False
for quest in self.prerequisites:
if quest not in visited:
if dfs(quest):
return True
return False
# ๐ฎ Test it out!
game = QuestSystem()
# Add quests
game.add_quest("Q1", "The Beginning", "main", "๐", {"xp": 100, "gold": 50})
game.add_quest("Q2", "Find the Sword", "main", "โ๏ธ", {"xp": 200, "item": "Iron Sword"})
game.add_quest("Q3", "Defeat the Dragon", "main", "๐", {"xp": 1000, "gold": 500})
game.add_quest("Q4", "Collect Herbs", "side", "๐ฟ", {"xp": 50, "item": "Healing Potion"})
game.add_quest("Q5", "Master the Blade", "main", "๐ก๏ธ", {"xp": 500, "skill": "Sword Mastery"})
# Add prerequisites
game.add_prerequisite("Q2", "Q1") # Need to complete Q1 before Q2
game.add_prerequisite("Q3", "Q2") # Need sword before fighting dragon
game.add_prerequisite("Q3", "Q5") # Need sword mastery too
game.add_prerequisite("Q5", "Q2") # Need sword to master it
# Play the game!
game.add_player("Hero")
print(f"\n๐ Available quests: {game.get_available_quests('Hero')}")
game.complete_quest("Hero", "Q1")
print(f"\n๐ Available quests: {game.get_available_quests('Hero')}")
game.complete_quest("Hero", "Q4")
game.complete_quest("Hero", "Q2")
print(f"\n๐ Available quests: {game.get_available_quests('Hero')}")
print(f"\n๐ Completion: {game.get_completion_rate('Hero'):.1f}%")
print(f"๐ Has circular dependency? {game.has_circular_dependency()}")
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Create adjacency lists with confidence ๐ช
- โ Implement graph algorithms like BFS and DFS ๐ก๏ธ
- โ Build real-world applications using graphs ๐ฏ
- โ Avoid common graph pitfalls like a pro ๐
- โ Design efficient graph-based systems with Python! ๐
Remember: Graphs are everywhere - social networks, maps, dependencies, and more. Adjacency lists are your swiss army knife for handling them efficiently! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered adjacency lists for graph representation!
Hereโs what to do next:
- ๐ป Practice with the quest system exercise above
- ๐๏ธ Build a small project using graphs (maybe a friend recommendation system?)
- ๐ Move on to our next tutorial: Graph Algorithms - Depth-First Search
- ๐ Explore weighted graphs and shortest path algorithms!
Remember: Every graph expert started with simple adjacency lists. Keep coding, keep learning, and most importantly, have fun with graphs! ๐
Happy coding! ๐๐โจ