+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 120 of 365

๐Ÿ“˜ Graph Representation: Adjacency Lists

Master graph representation: adjacency lists in Python with practical examples, best practices, and real-world applications ๐Ÿš€

๐Ÿš€Intermediate
25 min read

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:

  1. Space Efficiency ๐Ÿ”’: Only stores existing edges
  2. Fast Neighbor Access ๐Ÿ’ป: O(1) to get all neighbors
  3. Dynamic Structure ๐Ÿ“–: Easy to add/remove vertices and edges
  4. 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

  1. ๐ŸŽฏ Use Sets for Unique Edges: adjacency_list[v] = set() prevents duplicates
  2. ๐Ÿ“ Validate Vertices: Always check if vertices exist before operations
  3. ๐Ÿ›ก๏ธ Handle Edge Cases: Empty graphs, self-loops, disconnected components
  4. ๐ŸŽจ Choose Right Representation: Lists for simple graphs, dicts for weighted
  5. โœจ 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:

  1. ๐Ÿ’ป Practice with the quest system exercise above
  2. ๐Ÿ—๏ธ Build a small project using graphs (maybe a friend recommendation system?)
  3. ๐Ÿ“š Move on to our next tutorial: Graph Algorithms - Depth-First Search
  4. ๐ŸŒŸ 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! ๐ŸŽ‰๐Ÿš€โœจ