C
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
161: Graph Traversal: BFS in C
I remember the first time I tried to write a search algorithm for a social network map. I wanted to find the shortest path between two people—the classic "six degrees of separation" problem. My first instinct was to use recursion. I thought, "I'll just visit a friend, then their friend, then their friend's friend."
The problem? I ended up diving deep into one single branch of the network, potentially traveling halfway across the globe in my data structure, only to realize the person I was looking for was actually a direct friend of the starting person. I had accidentally written a Depth-First Search (DFS), which is great for exploring a maze, but terrible for finding the shortest path. To find the closest connection, I needed to explore in concentric circles.
The "Layer" Problem
Let's look at a small graph. Imagine we have four people: 0 (Alice), 1 (Bob), 2 (Charlie), and 3 (Diana). Alice is friends with Bob and Charlie. Bob is friends with Diana. Charlie is also friends with Diana. If I start at Alice and want to find Diana, I want to check everyone at distance 1 first, then everyone at distance 2.
# Simple adjacency matrix for our network
int graph[4][4] = {
{0, 1, 1, 0}, // Alice: friends with Bob, Charlie
{1, 0, 0, 1}, // Bob: friends with Alice, Diana
{1, 0, 0, 1}, // Charlie: friends with Alice, Diana
{0, 1, 1, 0} // Diana: friends with Bob, Charlie
};
If I use recursion, I might go Alice $\rightarrow$ Bob $\rightarrow$ Diana. That works, but in a massive graph, I might go Alice $\rightarrow$ Bob $\rightarrow$ ... $\rightarrow$ someone in Tokyo $\rightarrow$ ... $\rightarrow$ Diana, without ever realizing Charlie was right there next to Alice. I need a way to say, "Hold on, finish checking all my immediate friends before you move on to their friends."
Managing the Waiting Room
Since C doesn't give us a built-in Queue, I have to build one. Think of a queue as a waiting room. When I visit a person, I don't immediately jump to their friends; instead, I put all their friends in the waiting room. I only process the next person once everyone who arrived before them has been handled.
I'll start with a simple array-based queue. It's not the most memory-efficient for huge graphs, but for our exploration, it's perfect.
int queue[100];
int front = 0, rear = 0;
void enqueue(int v) {
queue[rear++] = v;
}
int dequeue() {
return queue[front++];
}
int isEmpty() {
return front == rear;
}
Now, here is where I hit my first snag during my early attempts: cycles. If Alice is friends with Bob, and Bob is friends with Alice, my code would just bounce between them forever. I need a visited array to keep track of who has already been "processed."
Putting the Pieces Together
Now I can implement the Breadth-First Search. The logic is: mark the start as visited, put it in the queue, and then loop until the queue is empty. Inside that loop, I pull someone out, look at all their neighbors, and if a neighbor hasn't been visited, I mark them and put them in the queue.
void bfs(int startNode) {
int visited[4] = {0, 0, 0, 0}; // Initialize all to not visited
visited[startNode] = 1;
enqueue(startNode);
printf("Traversal order: ");
while (!isEmpty()) {
int current = dequeue();
printf("%d ", current);
for (int i = 0; i < 4; i++) {
// If there's an edge AND we haven't been there yet
if (graph[current][i] == 1 && !visited[i]) {
visited[i] = 1;
enqueue(i);
}
}
}
printf("\n");
}
If I run this starting at node 0 (Alice), the output is 0 1 2 3. Notice how it hits Bob (1) and Charlie (2) before it ever reaches Diana (3). It effectively maps the graph in "waves."
Refining the Logic
You might be wondering: "Why mark it as visited before putting it in the queue, rather than after taking it out?" I tried it both ways. If you mark it after dequeue, you might end up adding the same node to the queue multiple times if it's a neighbor to several people currently being processed. That's a waste of memory and can lead to redundant work. Always mark the node the moment you "discover" it.
📋 Practical Task
Exercise: Calculating Degrees of Separation
Modify the BFS implementation provided in the lesson to not only traverse the graph but to calculate the distance (the number of hops) from the starting node to every other node in the graph.
Requirements:
- Create an integer array
int distance[4]. - Initialize the starting node's distance to 0 and all others to -1.
- When you discover a new neighbor and enqueue it, set its distance as
distance[current] + 1. - Print the final distance of each node from the start node (e.g., "Node 3 is 2 hops away from Node 0").
There are no comments for now.