The first time I heard about linked lists, I didn’t really understand why anyone would use them instead of Python’s built-in lists but I got to understand the true value of this data structure when I worked on an application that required to frequently add and remove elements from a collection that constantly changed in size.
In this article, I’ll guide you through implementing linked lists in Python from scratch. We’ll cover basic concepts but will also see full implementations with code examples.
What is a Linked List?
Linked lists are linear data structures where elements are stored in nodes, and each node points to the next node in the sequence. These lists are different from the built-in Python ones because they are not stored in contiguous memory locations.
We can think of linked lists as treasure hunts in which each clue points to the location of the next clue without the possibility to skip ahead.
Singly Linked Lists
There are two types of linked lists. The first one, the singly linked list, has nodes that point only to the next node in the sequence. Unlike Python’s built-in lists (contiguous memory), linked list elements can be scattered throughout memory, with each node storing data plus one reference (~8 bytes overhead per node). A singly linked list is simpler to implement but only allows forward traversal. We can represent it this way:
Figure 1. Singly linked list structure with forward-only navigation
Doubly Linked Lists
The second type, the doubly linked list, has nodes that point to both the next and previous nodes. This allows for traversal in both directions but requires double the reference memory (~16 bytes overhead per node) and more code complexity. This list type looks like this:
Figure 2. Doubly linked list structure with bidirectional navigation
Step-by-Step Implementation of a Singly Linked List
Let’s start with the implementation of a singly linked list in Python. For this, we’ll need:
- A Node class to represent each element
- A LinkedList class to manage the collection of nodes
- Insertion, deletion, and search methods
The Node Class
This class is super straightforward. It will allow us to store a piece of data and a reference to the next node each time we create a node.
The LinkedList Class
Now let’s build the LinkedList class with a couple of basic operations:
Now that we have this class to give us a basic linked list structure, we can add methods to insert, delete, and search for elements.
Insertion Methods
We can basically insert elements in three different locations within a list. Let’s add a method for each one of these:
- At the beginning (prepend)
- At the end (append)
- After a specific node
Figure 3. Animation demonstrating node insertion in a linked list
Deletion Methods
Obviously, we will need to have the ability to delete nodes from the lists we create. Let’s create a method to achieve this.
Figure 4. Animation demonstrating node deletion from a linked list
Search Method
It’s equally important that we are able to search for values within our lists. Let’s create a method for that too:
Testing Our Implementation
Now that we’ve put together all these methods, we can finally try them out. I encourage you to follow along and play around with the methods we’ve created to get the feeling of how singly linked lists work.
Implementation of a Doubly Linked List
Now that we’ve covered singly linked lists, let’s implement a doubly linked list. The main difference here is that each node will have both next and prev pointers. This doubles the memory overhead (~16 bytes vs ~8 bytes per node) but enables bidirectional traversal and more efficient operations at both ends.
For this one, let’s just jump right into the code:
Testing Our Implementation
Now that we’ve implemented the core methods for our doubly linked list, we can put them to the test. This will help us confirm that our methods work as expected.
Feel free to follow along, experiment with different inputs, and get a sense of how a doubly linked list behaves compared to its singly linked counterpart.
This covers most common operations and shows how the doubly linked list correctly updates both next and prev pointers. It also demonstrates how easy it is to traverse the list in both directions, which is one of the main advantages of this structure.
Common Use Cases for Linked Lists
Linked lists aren’t the best solution for every single scenario out there but are ideal for the following:
- Implementing stacks and queues: Linked lists are perfect for these data structures because they allow for efficient insertion and deletion at the ends.
- Managing dynamic datasets: When you don’t know how many elements you’ll need in advance, linked lists can grow and shrink efficiently without reallocation.
- Memory allocation systems: Many memory managers use linked lists to track free memory blocks.
- Implementing hash tables: Linked lists are often used to handle collisions in hash tables.
- Building music playlists: A doubly linked list makes an excellent structure for a playlist where you need to go both forward and backward through songs.
Performance and Time Complexity
In the end, choosing the right structure comes down to performance. I had no idea of complexities back in the day but soon enouhg, I began basing most of my decisions on them.
This comparison can help you make an informed choice next time you need to figure out whether a linked list might work better for the application you’re working on.
If you’re not familiar with Big O notation:
- O(1) means constant time (the operation takes the same amount of time regardless of the size of the data).
- O(n) means linear time (the operation time increases in direct proportion to the size of the data).
Table 1. Time Complexity of Common Operations
Operation | Singly Linked List | Doubly Linked List | Python List (Array) |
Access | O(n) | O(n) | O(1) |
Insert at beginning | O(1) | O(1) | O(n) |
Insert at end | O(n) or O(1)* | O(1) | O(1) |
Insert in middle | O(n) | O(n) | O(n) |
Delete at beginning | O(1) | O(1) | O(n) |
Delete at end | O(n) or O(1)* | O(1) | O(1) |
Delete in middle | O(n) | O(n) | O(n) |
Search | O(n) | O(n) | O(n) |
* O(1) if we keep a tail pointer
We typically aim for O(1) operations whenever possible because they scale better and are faster under most conditions.
As we can see from the table, linked lists are particularly efficient for operations at the beginning (and end for doubly linked lists), while traditional Python lists work better at random access.
Real Debugging Experience
My first time working with linked lists was a bit sloppy. I was working on a project that required a custom data structure to manage a dynamic set of user sessions. My initial implementation seemed solid to me, but the list would occasionally lose nodes, so something was obviously not right.
After hours of debugging, I discovered I had an “off-by-one” error in my deletion method. When I removed a node, I wasn’t properly updating the references and that was causing some nodes to become orphaned.
Here’s the buggy code I initially wrote:
The fix was quite straightforward:
After this, I never had this particular issue with linked lists again. I learned the hard way to always:
- Handle edge cases in data structures (empty lists, operations on the first or last element)
- Carefully trace through the logic when working with references and pointers
Comparison to Lists in Python
Sometimes it isn’t easy to figure out whether linked lists would be a better choice over Python’s built-in lists. If you find yourself trying to figure out the correct structure, this can help you out:
Use Python lists when:
- You need random access to elements (by index)
- Your data size is relatively stable
- Memory overhead is a concern (lists have less overhead per element)
- You perform many searches
Use linked lists when:
- You frequently insert or delete elements at the beginning
- The size of your data structure changes significantly and frequently
- You don’t need random access
- You’re implementing another data structure (like a stack or queue)
In practice, I’ve found that Python’s built-in lists are sufficient for most scenarios due to their optimization. However, linked lists have great use-cases and understanding them has allowed me to create a mental model for thinking about memory, references, and data organization.
Wrapping Up
Implementing linked lists in Python is a fantastic exercise you can do to reinforce fundamental programming concepts like references, pointers, and dynamic memory management.
As a practice challenge, I encourage you to try these exercises:
- Implement a method to detect cycles in a linked list
- Create a method to reverse a linked list in-place
- Implement a method to find the middle element of a linked list in one pass
Understanding linked lists will make you a better programmer, not just because you’ll occasionally need to implement them, but because they will deepen your understanding of how data structures work at a fundamental level. The skills you develop working with linked lists transfer to many other areas of programming.
If you want to deepen your knowledge of data structures and algorithms, Udacity’s Data Structures & Algorithms Nanodegree offers comprehensive training on these fundamental concepts. For a broader foundation in Python programming, check out the Programming for Data Science with Python Nanodegree. And if you’re interested in applying data structures to solve complex problems, the Full Stack Web Developer Nanodegree teaches you how to build robust applications using these principles.