Linear Lists
A linear list is one of the most fundamental and the most frequently used logical structures in data structures; structures such as stacks, queues and strings can all be seen as special cases or extensions of it. This chapter revolves around the central comparison of sequential storage versus linked storage: it introduces the definition of a linear list, the implementation and the basic operations of the sequential list and of the linked list, and it trains you to choose the right storage structure for a given scenario. After reading this chapter you will be able to understand the definition and the basic characteristics of a linear list, master the storage structure of the sequential list together with its insertion and deletion operations and the complexity analysis, master the storage structure of the singly linked list together with its insertion and deletion operations and the complexity analysis, learn the characteristics and the applicable scenarios of doubly linked lists, circular linked lists and static linked lists, and understand how to choose the right storage structure for the requirement at hand.
When a set of elements with a linear relationship has to be stored, do you choose a sequential list whose addresses are contiguous, or a linked list whose nodes are joined by pointers? The two have their own advantages and disadvantages in random access, in insertion and deletion, and in the use of the space. This chapter revolves around this central question of choosing a storage structure, and it introduces the implementation details of the two storage schemes, the differences in complexity and the scenarios in which each of them applies.
Basic Concepts
Definition
A definition is a precise description of what a concept, a term, or an object means. Understanding definitions is the foundation of learning any field, and every concept has its own exact meaning.
A linear list (Linear List) is a finite sequence of n data elements of the same data type, written as .
- Characteristic: there is a unique first element and a unique last element , and every element except the first and the last has exactly one predecessor and exactly one successor
- The number of the elements n is called the length of the linear list; when n = 0 the list is called an empty list
Click "Step forward" to watch the algorithm state change.
Comparison of the Storage Structures
| Storage scheme | Contiguous addresses | Supports random access | Efficiency of insertion and deletion | Space utilization |
|---|---|---|---|---|
| Sequential list | Yes | Yes | Low | Low |
| Linked list | No | No | High | High |
- Sequential list: the elements of the linear list are stored one after another in a set of storage units with contiguous addresses, so logically adjacent elements are physically adjacent.
- Linked list: every node contains a data field and a pointer field and the nodes are joined by pointers, so logically adjacent elements are not necessarily physically adjacent.
Exercises
Exercise 1
The characteristic of a linear list is ( )
A. The number of the elements is fixed and never changes B. Every element except the first and the last has exactly one predecessor and exactly one successor C. The storage addresses have to be contiguous D. All the elements may have different types
Approach:
This question tests the logical characteristics of a linear list. When deciding it, distinguish the abstract logical structure of a linear list from a concrete storage structure such as a sequential list, and compare the four options one by one with respect to the number of the elements, the predecessors and the successors, the contiguity of the storage and the element types.
Steps:
- A linear list is a finite sequence of n (with ) data elements of the same type, and its core characteristic is that every element except the first and the last has exactly one direct predecessor and exactly one direct successor.
- Option A, “the number of the elements is fixed and never changes”, is wrong because the length of a linear list can change; option C, “the storage addresses have to be contiguous”, is a characteristic of the sequential list and not a necessary characteristic of a linear list.
- Option D, “all the elements may have different types”, is wrong because a linear list requires all of its elements to have the same type.
- Only option B matches the definition of a linear list exactly.
Answer:
B.
Exercise 2
Briefly describe the definition and the main characteristics of a linear list.
Approach:
This question tests the definition of a linear list. In the answer, break the definition down into three levels, the number and the type of the elements, the unique first and last element, and the predecessor and successor relations, and then point out that this one-to-one relation is exactly where the word “linear” comes from.
Steps:
- Regarding the number and the type of the elements: a linear list is a finite sequence of n (with ) data elements of the same type.
- Regarding the two ends: the list has a unique first element and a unique last element; when it is an empty list.
- Regarding the relations: every element except the first and the last has exactly one predecessor and exactly one successor, so the elements stand in a one-to-one linear relation.
Answer:
A linear list is an ordered sequence of n (with n >= 0) data elements of the same type; it has a unique first element and a unique last element, every element except the first and the last has exactly one predecessor and exactly one successor, and the elements stand in a one-to-one linear relation.
Exercise 3
What are the advantages and the disadvantages of the sequential list and of the linked list?
Approach:
This question tests the comparison of the advantages and the disadvantages of the sequential list and of the linked list. In the answer, evaluate the two storage structures along three lines, the capability of random access, the cost of insertion and deletion, and the way the space is used, and then summarize the scenario in which each of them applies.
Steps:
- The advantages of the sequential list: the storage space is contiguous, it supports random access, searching is fast () and the storage density is high; the disadvantages: an insertion or a deletion has to move a great many elements (), and the space has to be allocated in advance, so the space utilization is low.
- The advantages of the linked list: an insertion or a deletion only modifies the pointers ( once the position has been located), the space is allocated on demand and the utilization is high; the disadvantages: random access is not supported, searching is slow (), every node needs an extra pointer and the storage density is low.
- Comparative conclusion: choose the sequential list when a frequent random access by index is needed and the size of the structure is relatively stable; choose the linked list when the elements are frequently inserted and deleted at a known position.
Answer:
Sequential list: the advantages are the support of random access, the fast search () and the high storage density; the disadvantages are that an insertion or a deletion has to move a great many elements () and the low space utilization, since the space has to be allocated in advance. Linked list: the advantages are the fast insertion and deletion ( once the position has been located), the space allocated on demand and the high utilization; the disadvantages are that random access is not supported, that the search is slow (), that every node stores an extra pointer and that the storage density is low.
A sequential list is implemented with an array; it is the simplest and the most commonly used storage scheme. Its core operations are insertion, deletion and searching.
Insertion into a Sequential List
To insert the element x at position i, the elements from the i-th to the last one have to be shifted one place to the right, one after another.
#define MaxSize 100
typedef struct {
int data[MaxSize];
int length; // current length
} SqList;
// insert the element x at position i (i starts from 1)
bool ListInsert(SqList &L, int i, int x) {
if (i < 1 || i > L.length + 1) return false; // illegal position
if (L.length >= MaxSize) return false; // the list is full
for (int j = L.length; j >= i; j--)
L.data[j] = L.data[j - 1]; // shift the elements to the right
L.data[i - 1] = x;
L.length++;
return true;
}
Deletion from a Sequential List
To delete the element at position i, the elements from the (i+1)-th to the last one have to be shifted one place to the left, one after another.
// delete the element at position i and return it in e
bool ListDelete(SqList &L, int i, int &e) {
if (i < 1 || i > L.length) return false;
e = L.data[i - 1];
for (int j = i; j < L.length; j++)
L.data[j - 1] = L.data[j]; // shift the elements to the left
L.length--;
return true;
}
Complexity Derivation
Insertion: in the best case, inserting at the tail of the list, 0 elements are moved; in the worst case, inserting at the head of the list, n elements are moved; on average elements are moved. Therefore:
Deletion: similarly, elements are moved on average, so .
Access by position: the element is reached directly through its index, so .
Exercises
Exercise 1
In a sequential list of length n, inserting one element at position i (where ) requires moving how many elements ( )
A. n-i+1 B. n-i C. i D. i-1
Approach:
This question tests the number of the elements moved when an element is inserted into a sequential list by position. A sequential list relies on the contiguous storage to keep logically adjacent elements adjacent, and an insertion shifts all the elements behind the insertion point to the right, so the number of the moved elements is exactly the number of the elements from the insertion position to the tail of the list.
Steps:
- When an element is inserted at position , the -th element and every element behind it have to be moved one place to the right so that the relative order of the original elements is preserved.
- The elements that have to be moved are the -th to the -th, that is, elements in total.
- When , that is, when the element is inserted at the tail of the list, the number of the moved elements is 0, which agrees with the formula.
Answer:
A (n - i + 1).
Exercise 2
How is the traversal of a linear list implemented?
Approach:
This question tests the way a sequential list and a linked list are traversed. Both of them have to visit every element in the logical order; the difference lies in how the next element is located, since a sequential list increments the index while a linked list advances the pointer.
Steps:
- Sequential list: the elements are stored in a contiguous space, and the loop
for (i = 0; i < L.length; i++)visitsL.data[i]one after another by index. - Linked list: starting from the first node, the pointer p is made to point at the current node, the node is visited, and then p = p->next is executed until p is null.
- Both of them visit the elements in the same logical order and differ only in the way the elements are addressed, so the complexity of the traversal is for both of them.
Answer:
Sequential list: use a loop over the index, for (i = 0; i < L.length; i++) visits L.data[i] one after another. Linked list: starting from the first node, the pointer p visits the nodes one by one, p = p->next, until p is null.
A singly linked list joins its nodes with pointers: an insertion and a deletion do not have to move any element, but a search has to start from the head of the list and traverse it.
typedef struct LNode {
int data;
struct LNode *next;
} LNode, *LinkList;
Insertion into a Singly Linked List
To insert a new node s after the i-th node, first let s point to the successor and then let the predecessor point to s. The order must not be reversed.
// insert the node s at position i (the insertion is made before the node at position i)
bool ListInsert(LinkList &L, int i, int x) {
LNode *p = L; // head node
int j = 0;
while (p != NULL && j < i - 1) { // find the (i-1)-th node
p = p->next;
j++;
}
if (p == NULL) return false;
LNode *s = (LNode *)malloc(sizeof(LNode));
s->data = x;
s->next = p->next; // (1) the new node points to the successor
p->next = s; // (2) the predecessor points to the new node
return true;
}
Deletion from a Singly Linked List
To delete the i-th node, it is enough to let the predecessor node point to the successor of the deleted node.
bool ListDelete(LinkList &L, int i, int &e) {
LNode *p = L;
int j = 0;
while (p->next != NULL && j < i - 1) { // find the (i-1)-th node
p = p->next;
j++;
}
if (p->next == NULL) return false; // the i-th node does not exist
LNode *q = p->next;
e = q->data;
p->next = q->next; // skip the deleted node
free(q);
return true;
}
Complexity Derivation
- Insertion and deletion: locating the position takes , but modifying the pointers after the position has been located takes only . If the predecessor of the insertion position is already known, the insertion and the deletion are .
- Search by value: the elements have to be compared one by one starting from the head of the list, so the worst case is .
- Access by position: the list has to be traversed, so the worst case is .
Exercises
Exercise 1
The advantage of a singly linked list over a sequential list is ( )
A. It supports random access B. Its insertion and deletion operations are efficient C. Its storage density is higher D. Its search is more efficient
Approach:
This question tests the advantages and the disadvantages of a singly linked list relative to a sequential list. In the answer, hold on to the essential property of a linked list, that the nodes are scattered in the memory and joined by pointers, and judge whether each of the four options holds.
Steps:
- An insertion and a deletion in a linked list only modify the pointer fields of the relevant nodes and do not move the elements as a whole as a sequential list does, so the insertion and the deletion are efficient and option B is correct.
- Option A is wrong: the nodes of a linked list are not stored contiguously, so the address of an element cannot be computed directly from its index and random access is not supported.
- Option C is wrong: every node of a linked list stores an extra pointer field, so its storage density is in fact lower than that of a sequential list.
- Option D is wrong: searching for a given element has to traverse the list node by node from the head node along the pointers, so the time complexity is .
Answer:
B.
Exercise 2
What is the basic idea of the insertion and of the deletion in a singly linked list?
Approach:
This question tests the basic idea of the insertion and of the deletion in a singly linked list. Both of them take finding the predecessor node as the premise, and they differ in whether the new node is first joined to the successor and the predecessor is then updated, or the predecessor simply steps over the deleted node.
Steps:
- Insertion: create the new node s, point s->next at the successor of the insertion position first, and then point the next field of the predecessor node at s, that is, connect first and break later.
- Deletion: find the predecessor p of the deleted node q, make p->next point to the successor of q, skip q and then release the memory of q.
- Both of them modify only a constant number of pointer fields and never move the other element nodes, so once the predecessor has been located the time complexity of both of them is .
Answer:
Insertion: create the new node s, point s->next at the successor of the insertion position first, and then point the next field of the predecessor node at s, that is, connect first and break later. Deletion: find the predecessor p of the deleted node q, make p->next point to the successor of q, skip q and then release the memory of q.
Doubly Linked List
Every node has two pointer fields, one pointing to the predecessor and one to the successor, so a bidirectional traversal is supported. The price is one extra pointer per node and a larger space cost.
typedef struct DNode {
int data;
struct DNode *prior, *next;
} DNode, *DLinkList;
The insertion operation (inserting s after the node p):
s->prior = p;
s->next = p->next;
p->next->prior = s;
p->next = s;
Circular Linked List
- Circular singly linked list: the pointer of the last node points to the head node or to the first node, so that the whole list is connected end to end and every node can be reached starting from any node.
- Circular doubly linked list: the prior of the head node points to the tail node, and the next of the tail node points to the head node.
A circular linked list is often used in the scenarios that operate frequently on the two ends, the Josephus ring and the process scheduling of an operating system for instance.
Static Linked List
A static linked list simulates a linked list with an array, and the “pointer” of a node is replaced by an array index, that is, a cursor; it suits the high-level languages that do not support pointers and the scenarios where the memory is limited.
Exercises
Exercise 1
Briefly describe the difference between a singly linked list and a doubly linked list and the scenario in which each of them applies.
Approach:
This question tests the difference in the node structure between a singly linked list and a doubly linked list together with the trade-off between them. In the answer, start from the structural difference, namely the number of the pointer fields, and then give the typical application scenario of each of them.
Steps:
- Singly linked list: every node has only one pointer, which points to the successor; the structure is simple and it occupies little space, so it suits the scenarios in which only a traversal in one direction is needed.
- Doubly linked list: every node has two pointers, one to the predecessor and one to the successor, so a bidirectional traversal is supported, but every node occupies one more pointer of space.
- Choice of the scenario: choose the doubly linked list when the position has to be moved frequently in both directions, the cursor movement in a text editor and the LRU cache being the typical cases.
Answer:
Singly linked list: every node has only one pointer to the successor, the structure is simple and it occupies little space, so it suits the scenarios in which only a traversal in one direction is needed. Doubly linked list: every node has two pointers, one to the predecessor and one to the successor, so a bidirectional traversal is supported, but every node occupies one more pointer of space. It suits the scenarios in which the position has to be moved frequently in both directions, such as the cursor movement in a text editor and the LRU cache.
The two preceding sections have made clear the difference in the storage between the sequential list and the linked list; this section puts them back into real decisions: task queues, browsing history, shopping carts and the undo and the redo of an editor are all built on a linear list underneath, and it is only the question of whether a random access is frequent that decides between the sequential list and the linked list. The common pitfalls at the end remind you that in a singly linked list the order of connecting first and breaking later must not be reversed during an insertion, and that is the line of code which is the easiest to get wrong.
Typical Applications
- As the foundation of the other data structures: the linked storage of stacks, queues, strings and sparse matrices is in essence a linked list node with a different linking rule.
- Task queues, browsing history and shopping carts: in these scenarios the elements are frequently added and removed at the two ends and are seldom accessed by their serial number in the middle, so a linked list, or a queue and a stack built on a linked list, avoids the cost of moving the elements that a sequential list would incur.
- The undo and the redo of a text editor: every edit produces one record, and organizing the records with a stack naturally supports undoing the most recent operation and restoring the operation that has been undone.
| Application scenario | Recommended structure | Reason |
|---|---|---|
| A frequent random access is needed | Sequential list | The address is obtained from the index with one multiplication and one addition, so the access is |
| Frequent insertions and deletions are needed | Linked list | Once the predecessor is known the insertion and the deletion are , and the following elements do not have to be moved |
| The operations are confined to the two ends | Doubly ended linked list | Both ends support an insertion and a deletion in |
| The number of the elements changes frequently and a contiguous storage is required | Sequential list | A linked list has the extra overhead of the nodes and a poor cache locality |
Terminology Summary
| Term | Abbreviation | Explanation |
|---|---|---|
| Sequential list | - | The elements are stored in a contiguous space and the address is computed from the index |
| Linked list | - | A node consists of a data field and a pointer field, and the nodes are chained by pointers |
| First node | - | The first node of the list; the pointer field points to it |
| Last node | - | The last node of the list; its pointer field is null |
| Head pointer | - | The pointer to the first node, the entry point of the list |
| Head node | - | The node attached in front of the first node, which makes it easy to handle the empty list and the insertion and the deletion in a uniform way |
| Length | - | The number of the nodes in the list |
Common Pitfalls
Two other frequent mistakes come next. The first is mixing a list with a head node and a list without one: with a head node p points at the head node, and the first element that is inserted has to be inserted after the head node; without a head node the special case of the empty list has to be taken into account. The second is the condition for testing whether the list is empty: with a head node the empty list is head->next == NULL, and without a head node it is head == NULL.
