Linked Lists
There are some notable disadvantages of dynamic arrays like list
in Python
- the length of the dynamic array might be longer than the actual number of elements it stores
- amortized bounds for operations may be unacceptable in real-time systems
- insertions and deletions at interior positions of an array are expensive
Linked lists provide an alternative to array-based ordered sequences.
While an array provides more centralized representation with one large chunk of memory capable of accommodating references to many elements, a linked list relies on a distributed representation using a lightweight object called a node for each element.
Each node maintains a reference to its element and one or more references to neighboring nodes in order to collectively represent the linear order of the sequence.
Trade-offs between Linked Lists and Arrays
Advantages of Array-Based Sequences
- Arrays provide
O(1)
time access to an element based on an integer index. Linked lists requiresO(k)
time to traverse the list from the beginning or possibly end of a doubly linked list. - Operations with equivalent asymptotic bounds typically run a constant factor more efficiently with an array-based structure versus a linked structure. For example appending a new element to a linked list requires instantiation of a node, appropriate linking of nodes, and an increment of an integer. While this operation completes in
O(1)
time the actual number of CPU operations will be more in the linked version. - Array based representations typically use proportionally less memory than linked structures. A dynamic array may be, in the worst case, allocated memory for 2n object references. With linked lists however the memory must be devoted not only to store a reference to each contained object, but also explicit references that link the nodes. So a singly linked list of length n already requires 2n references, and a doubly linked list 3n references.
Advantages of Link-based sequences
- Link based structures provide worst-case time bounds for their operations. This is in contrast to amortized bounds associated with the expansion and contraction of a dynamic array. In real-time systems designed for more immediate responses, a long delay caused a single (amortized) operation may have an adverse effect. Worst-case bound gives a guarantee on the sum of the time spent on the individual operations.
- Link based structures support
O(1)
time insertions and deletions at arbitrary positions. This is perhaps the biggest advantage of linked lists.
Singly Linked List
A collection of nodes that collectively form a linear sequence. Each node stores a reference to an object that is an element of the sequence, as well as a reference to the next node of the list.
Each node is represented as a unique object, with that instance storing a reference to its element and a reference to the next node (or None
). The linked list must keep a reference to the head of the list - without an explicit reference to the head, there would be no way to locate that node.
Unfortunately we cannot easily delete the last node of a singly linke list. The only way to access the node before the last is to start from the head of the list - which is inefficient. To make this efficient we will need a Doubly Linked List list.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
Efficiencies
Space Complexity: O(n)
Time Complexities:
Stack using a Singly Linked List
Using a singly linked list, we implement a stack. Since for a linked list we can insert and delete elements in constant time only at the head, we will use the head as the top
of the stack.
When implementing the node we also intentionally define __slots__
to streamline the memory usage as there may potentially be many node instances in a single list.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
Queue using a Singly Linked List
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
|
Circularly Linked Lists
Case where the next
reference of the tail of the list circularly points back to the head of the list.
Example of use
A Round-Robin scheduler iterates through a collection of elements in a circular fashion and "services" each element by performing a given action on it. These are usually used to fairly allocate a resource that must be shared by a collection of clients e.g. to allocate slices of CPU time to various applications running concurrently on a computer.
Round robin queues can be implemented using a general queue with the following operations:
1. current_process = queue.dequeue()
2. Service current_process
3. queue.enqueue(current_process)
With a circular linked list implementation of a queue, we just have to incrementing a reference that marks the head
and tail
of the queue without needing to do any operations.
Circular Queue implemented
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
|
Doubly Linked List
Each node keeps an explicit reference to the node before it and a reference to the node after it. This is known as a doubly linked list. This allows for a greater variety of O(1)
updates, inlcuding insertions and deletions at arbitrary positions within the list.
header
and trailer
sentinels - i.e. special "dummy" nodes - will be used as the head and tail of the list. By using just slightly extra space, our logic of operations greatly simplifies.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
|
Implementing a Dequeue using a Doubly Linked List
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
References
-
Skiena, Steven S.. (2008). The Algorithm Design Manual, 2nd ed. (2). : Springer Publishing Company. ↩
-
Goodrich, M. T., Tamassia, R., & Goldwasser, M. H. (2013). Data Structures and Algorithms in Python (1st ed.). Wiley Publishing. ↩
-
Cormen, T. H., Leiserson, C. E., & Rivest, R. L. (1990). Introduction to algorithms. Cambridge, Mass. : New York, MIT Press. ↩