Content
Introduction to Linked List Traversing a Linked List Linked List Big Three Insertion and Removal at the End List Template <- Go BackUniversity of Michigan at Ann Arbor
Last Edit Date: 04/03/2023
Disclaimer and Term of Use:
We do not guarantee the accuracy and completeness of the summary content. Some of the course material may not be included, and some of the content in the summary may not be correct. You should use this file properly and legally. We are not responsible for any results from using this file
This personal note is adapted from Professor Amir Kamil, Andrew DeOrio, James Juett, Sofia Saleem, and Saquib Razak. Please contact us to delete this file if you think your rights have been violated.
This work is licensed under a Creative Commons Attribution 4.0 International License.
Sequential Containers
A sequential container is a container that holds elements and allows them to be accessed in sequential order. The simplest sequential container is a build-in array, which stores its elements contiguously in memory.
Traverse by pointer | Traverse by index |
---|---|
const int SIZE = 5; int array[SIZE] = { 1, 2, 3, 4, 5 }; for (int *ptr = array; ptr != array + SIZE; ++ptr) { cout << *ptr < endl; } |
const int SIZE = 5; int array[SIZE] = { 1, 2, 3, 4, 5 }; for (int i = 0; i < SIZE; ++i) { cout << array[i] < endl; } |
A vector has the same property, which stores elements contiguously in memory. It grows as needed by swapping out an old array for a new one.
Sequence abstractions based on contiguous memory have the drawback of inefficient insertion at the beginning or middle of the sequence. For instance, to insert an item at a particular position, we need to first move the elements at that position and later out of the way. Shown as the following picture.
Thus, insertion at the beginning or middle is a linear-time operation.
On the other hand, if we give up the abstraction of contiguous memory, we can place a new item anywhere in memory.
Since the elements are no longer stored contiguously, we cannot move to the next element by just incrementing an address.
Instead, we have to explicitly keep track of where the next element is through a pointer, which just involves changing the value of a pointer.
Node
For each element in our sequence, we need to keep track of both the datum itself as well as a pointer to the next piece of the sequence.
This is heterogeneous data, so we use a node class-type object to keep track of the two values:
1 struct Node {
2 int datum;
3 Node *next;
4 };
Here the Node
is a plain-old data, using the struct
keyword and can access its members directly.
int
is they type of the element in a node, here the element is called datum
.
The remaining Node
member, which is *next
, is a pointer to the next Node
in out sequence.
Take the following sequence (1, 2, 3, 4)
as an example:
Note: We use a null pointer as a sentinel denoting the end of the sequence, storing that in the next
pointer of the last node. This is called a linked list, which consists of a list of items linked together by pointers.
We can also define a class called IntList
to internally maintains a sequence of nodes.
1 class IntList {
2 ...
3 private:
4 struct Node {
5 int datum;
6 Node *next;
7 };
8 Node *first;
9 };
Since the Node
objects are stored indirectly from the IntList
object, they must be in dynamic memory, and IntList
must ensure that thier memory is properly managed.
The following is the interface of the IntList
class:
1 class IntList {
2 public:
3 // EFFECTS: Constructs an empty list.
4 IntList();
5
6 // EFFECTS: Returns true if the list is empty.
7 bool empty() const;
8
9 // REQUIRES: the list is not empty
10 // EFFECTS: Returns (by reference) the first element.
11 int & front();
12
13 // EFFECTS: Inserts datum at the front of the list.
14 void push_front(int datum);
15
16 // REQUIRES: the list is not empty
17 // EFFECTS: Removes the first element.
18 void pop_front();
19
20 // MODIFIES: os
21 // EFFECTS: Prints the items in the list, each followed by a space.
22 void print(std::ostream &os) const;
23
24 private:
25 struct Node {
26 int datum;
27 Node *next;
28 };
29 Node *first;
30 };
If we want to have an empty linked list, we just let first
store a null pointer.
The following are some invariants for the IntList
object:
first
is either null or a pointer to a valid Node
the next
member of all but the last node points to another valid Node
the next
member of the last is null
Implementation
The constructor must ensure that the list is initialized to satisfy the representation invariants. Since the default constructor makes the list empty, it must initialize first
to be null:
1 IntList::IntList()
2 : first(nullptr) {}
The empty()
function just needs to check whether first
is null:
1 bool IntList::empty() const {
2 return first == nullptr; // return first; also works
3 }
For front()
, we will first assert the REQUIRES clause that the list not be empty. If that is satisfied, the representation invariants tell us that first
is pointing to a valid node. Its datum
member holds the first element.
1 int & IntList::front() {
2 assert(!empty());
3 return first->datum;
4 }
For push_front()
, we need to consider two cases:
The list is empty, in which case first
is null
The list is not empty, in which case first
points to a valid node
1 void IntList::push_front(int datum) {
2 Node *p = new Node;
3 p->datum = datum;
4 p->next = first;
5 first = p;
6 }
We can also use a initialization list to achieve this:
1 void IntList::push_front(int datum) {
2 first = new Node{ datum, first };
3 }
In pop_front()
, we again assert the REQUIRES clause. We should then consider two possible cases:
The list has one item, in which case it will end up as empty
The list has more than one item, and it will not end up empty
The following two implementations are both correct:
1 void IntList::pop_front() {
2 assert(!empty());
3 Node *victim = first; // temporary keeps track of old first
4 first = first->next;
5 delete victim;
6 }
1 void IntList::pop_front() {
2 assert(!empty());
3 Node *new_first = first->next; // temporary keeps track of new first
4 delete first;
5 first = new_first;
6 }
The following is the test code:
1 int main() {
2 IntList list; // ( )
3 list.push_front(1); // ( 1 )
4 list.push_front(2); // ( 2 1 )
5 list.push_front(3); // ( 3 2 1 )
6
7 cout << list.front(); // 3
8
9 list.front() = 4; // ( 4 2 1 )
10
11 list.print(cout); // 4 2 1
12
13 list.pop_front(); // ( 2 1 )
14 list.pop_front(); // ( 1 )
15 list.pop_front(); // ( )
16
17 cout << list.empty(); // true (or 1)
18 }
Try it out:
You may notice we have a print()
function inside the class. It prints out the elements linked from the first to the end.
Iterating over a linked list’s elements from outside the class requires an iterator abstraction.
The Node
struct is private, so external code cannot use Nodes to iterate through the list. Code within IntList
, on the other hand, does have access to Node
, so it can traverse the list by starting at the first
member and following each node’s next
pointer until reaching the null sentinel.
The following print()
member function uses this strategy:
1 void print(std::ostream &os) const {
2 for (Node *node_ptr = first; node_ptr; node_ptr = node_ptr->next) {
3 os << node_ptr->datum << " ";
4 }
5 }
The loop initializes node_ptr
as a copy of first.
If the list is empty, first is null, so node_ptr will be initialized to null as well.
The truth value of a null pointer is false, so the condition of the loop will evaluate to false and the loop will exit. (Alternatively, node_ptr
can be compared to nullptr
instead: node_ptr != nullptr
.)