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
149: Implementing a Circular Linked List
I once worked with a junior developer who was building a simple turn-based combat system for a tabletop RPG. He used a standard singly linked list to keep track of the players' turn order. Every time the turn passed to the last player, he had to write a clunky if (current == NULL) { current = head; } block to reset the loop. It worked, but he was fighting the data structure. I told him he was manually doing what the memory layout should be doing for him. That's where a circular linked list comes in—it turns that "reset" logic into a natural property of the list itself.
The Advantage of the Tail Pointer
When you first think of a circular linked list, your instinct is probably to keep a pointer to the head. But here is a pro tip: keep a pointer to the tail instead. Why? Because in a circular list, the tail's next pointer is the head. If you only store the head, adding a node to the end of the list requires you to traverse the entire circle just to find the last element. If you store the tail, you have $O(1)$ access to both the end of the list (the tail) and the start of the list (tail->next). It's a small shift in perspective that saves you a lot of CPU cycles.
typedef struct Node { int id; struct Node* next; } Node; Node* createNode(int id) { Node* newNode = malloc(sizeof(Node)); newNode->id = id; newNode->next = NULL; return newNode; } void insertEnd(Node** tail, int id) { Node* newNode = createNode(id); if (*tail == NULL) { *tail = newNode; newNode->next = newNode; // Points to itself to start the circle } else { Node* head = (*tail)->next; newNode->next = head; (*tail)->next = newNode; *tail = newNode; // Move tail to the new last element } }Breaking the Infinite Loop
Traversal is where most people trip up. In a linear list, you just loop until you hit
NULL. In a circular list,NULLdoesn't exist. If you use a standardwhile(current != NULL)loop, your program will spin forever until it crashes or you kill the process. I've seen this happen in production code more often than I'd like to admit.To traverse a circular list, you have to remember where you started. You save a pointer to the head, move forward, and stop the moment you see that head pointer again. It's a
do-whileloop's perfect use case because you want to process the first node before checking if you've completed the circle.void printList(Node* tail) { if (tail == NULL) return; Node* head = tail->next; Node* current = head; do { printf("Player %d\n", current->id); current = current->next; } while (current != head); }Managing Node Removal
Deleting a node in a circular list is mostly the same as a linear list, but with one critical edge case: what happens when you delete the last remaining node? If you don't explicitly set your tail pointer to
NULL, you'll end up with a dangling pointer that thinks it's still part of a circle. Also, if you delete the head, you must remember to update the tail'snextpointer to point to the new head, otherwise, you've broken the circle and turned your list back into a linear one—or worse, created a memory leak.
📋 Practical Task
Build a Multiplayer Turn Manager
Create a program that simulates a turn-based game using a circular linked list. Your program should:
- Allow the user to add players (by ID) to the game.
- Implement a function
void advanceTurn(Node** current)that moves the "active player" pointer to the next person in the circle. - Implement a function
void removePlayer(Node** tail, int id)that removes a player from the game by their ID, ensuring the circle remains intact and thetailis updated if the last node is removed. - Run a loop where the user can either "Advance Turn", "Add Player", "Remove Player", or "Show Current Player" until they choose to exit.
Make sure you handle the case where the list becomes empty after a removal to prevent segmentation faults.
There are no comments for now.