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
148: Implementing a Doubly Linked List
A few years ago, I was reviewing code for a junior developer who was building a basic text editor. He had implemented the "Undo" functionality using a singly linked list. It worked great for moving backward through the history, but when he tried to implement "Redo," he hit a wall. To move forward again, he had to start from the very beginning of the history list and traverse forward until he reached the point where the user had stopped undoing. As the document grew, the Redo button started lagging. He didn't need a fancy array or a complex tree; he just needed his nodes to know who their parents were.
That's where the doubly linked list comes in. You've already mastered the singly linked list, where each node is a one-way street. In a doubly linked list, we add a second pointer to the node structure. This allows us to traverse the list in both directions, making operations like deleting a node or moving backward significantly more efficient.
Giving Your Nodes a Memory of Where They've Been
To turn a singly linked list into a doubly linked one, we modify the struct. Instead of just having a next pointer, we add a prev pointer. It looks something like this:
struct Node {
int data;
struct Node* next;
struct Node* prev;
};
I like to think of the prev pointer as a breadcrumb trail. In a singly linked list, if you are at node C, you have no idea that node B exists unless you start over from the head. With a doubly linked list, node C explicitly points back to node B. This sounds simple, but it changes the mental model of how you manage your memory. Every time you link a node forward, you now have a reciprocal obligation to link it backward.
The Delicate Dance of Four Pointers
The real challenge—and where most bugs creep in—is the insertion and deletion logic. When you insert a node into the middle of a singly linked list, you only have to update two pointers. In a doubly linked list, you're often juggling four.
Imagine you're inserting node N between node A and node B. You can't just point A to N and N to B. You also have to ensure N points back to A, and B points back to N. If you miss even one of these, you'll end up with a "broken" list that works fine when traversing forward, but crashes your program the moment you try to go backward. I've spent way too many hours debugging segfaults because I forgot to update a single prev pointer during a deletion.
When deleting a node, the process is actually cleaner than in a singly linked list. In a singly list, you need a reference to the previous node to bridge the gap. In a doubly linked list, the node you're deleting already knows who its predecessor is via node->prev. You can simply tell the previous node to point to the next node, and vice versa, then free the memory. It's an elegant trade-off: you use slightly more memory per node to gain a lot of flexibility in how you move through your data.
📋 Practical Task
Implementing a Bidirectional Music Playlist Manager
Your task is to create a simple music playlist manager using a doubly linked list. Each node should represent a "Song" containing a string for the song title.
Write a program that implements the following:
- A
struct Songthat contains achar title[50], anextpointer, and aprevpointer. - A function
void addSong(struct Song** head, char* title)that appends a new song to the end of the list. - A function
void printPlaylistForward(struct Song* head)that prints all songs from start to finish. - A function
void printPlaylistBackward(struct Song* tail)that prints all songs from finish to start.
In your main function, add three songs ("Song A", "Song B", "Song C"), print the list forward, and then use the prev pointers to print the list backward starting from the last node. Ensure you properly free all allocated memory before the program exits.
There are no comments for now.