Singly linked list
The Node class, inserting at the front/back/middle, deletion and traversal. The source of the midterm questions “what is the final state of the list?” and “write this method”.
An array is a nice structure, but it has two troubles: its size is fixed in advance, and to insert an element in the middle you have to shift the others. A linked list solves both troubles. Each element sits in its own little box (node) and carries the address of the next box. The exam has two types of question: writing the final state of the list after a series of operations, and writing a method such as addLast, remove or countNodes from scratch.
Golden rules:
- List = the head reference. If you lose the head, you lose the list.
- Always traverse with
p = p.next. The stopping condition isp != null(visit every node) orp.next != null(stop at the last node). - When inserting, first link the new node, then redirect the old one.
- To delete, find the previous node. You can’t go backward in a singly linked list.
- The empty list, the single-element list and the head are always special cases.
The Node class
A node carries two things: data and a reference to the next node (next). The next of the last node is null.
class Node {
int data;
Node next;
Node(int data) { this.data = data; } // next is automatically null
}
class SinglyLinkedList {
Node head; // first node; null in an empty list
int size;
}
In memory the list 10 → 20 → 30 looks like this:
head
|
v
[10 | *]--->[20 | *]--->[30 | null]
Each box can sit in a different place in memory. This is the difference from an array: a[2] is computed directly, but to reach the third element of a list you have to start from the head and follow next twice. That is why access by index is O(n).
PITFALL ·
Node p = head;does not create a new node. It just creates a second arrow pointing to the existing node. If you dop.data = 99, the head’s data also becomes 99, because it is the same box.
Traversal
Printing the list, summing it, searching for an element: all the same pattern.
void print() {
Node p = head;
while (p != null) {
System.out.print(p.data + " ");
p = p.next;
}
System.out.println();
}
boolean contains(int x) {
for (Node p = head; p != null; p = p.next)
if (p.data == x) return true;
return false;
}
Don’t advance head itself (head = head.next). After printing, the list would look empty. Always use a helper p.
Insertion
Inserting at the front (addFirst)
void addFirst(int x) {
Node n = new Node(x);
n.next = head; // the new node points to the old first node
head = n; // head becomes the new node
size++;
}
Two lines, no loop: O(1). It works on an empty list too (if head is null, n.next = null).
Inserting at the end (addLast)
If you have only head, you have to walk to the last node.
void addLast(int x) {
Node n = new Node(x);
if (head == null) { head = n; size++; return; }
Node p = head;
while (p.next != null) p = p.next; // p is now the last node
p.next = n;
size++;
}
Time O(n). If you add a tail reference to the class it becomes O(1) with tail.next = n; tail = n;, but then you also have to update the tail on every deletion.
Inserting in the middle (insertAt)
To insert at position index, go to the node at position index - 1.
void insertAt(int index, int x) { // 0 = front
if (index == 0) { addFirst(x); return; }
Node p = head;
for (int i = 0; i < index - 1; i++) p = p.next;
Node n = new Node(x);
n.next = p.next; // 1) the new node points to p's old next
p.next = n; // 2) p points to the new node
size++;
}
Before: p=[20] ---> [30]
1) [25] ---> [30] (n.next = p.next)
2) [20] ---> [25] ---> [30] (p.next = n)
PITFALL · The order of the lines. If you write
p.next = n;first, the old value ofp.next(30) is lost. Thenn.next = p.next;actually becomesn.next = n. The node points to itself, and the list enters an infinite loop.
Deletion
Deleting from the front
int removeFirst() {
if (head == null) throw new RuntimeException("empty list");
int x = head.data;
head = head.next;
size--;
return x;
}
Nobody points to the old first node any more, and Java’s garbage collector cleans it up. O(1).
Deleting from the end
To delete the last node you must set the next of the node before the last to null.
int removeLast() {
if (head == null) throw new RuntimeException("empty list");
if (head.next == null) { // single element
int x = head.data; head = null; size--; return x;
}
Node p = head;
while (p.next.next != null) p = p.next; // p = the node before the last
int x = p.next.data;
p.next = null;
size--;
return x;
}
Even with a tail reference this is O(n). The tail gives you the last node but not the one before it.
Deleting by value
boolean remove(int x) {
if (head == null) return false;
if (head.data == x) { head = head.next; size--; return true; }
Node p = head;
while (p.next != null && p.next.data != x) p = p.next;
if (p.next == null) return false;
p.next = p.next.next; // skip the target
size--;
return true;
}
The order of the loop condition matters: p.next != null is checked first, so p.next.data is not called on null (&& short-circuits).
Worked example: an operation trace
Start with an empty list and write the list at each step:
list.addFirst(20);
list.addFirst(10);
list.addLast(30);
list.addLast(40);
list.print();
list.insertAt(2, 25);
list.print();
System.out.println("removeFirst=" + list.removeFirst());
System.out.println("removeLast=" + list.removeLast());
list.print();
System.out.println(list.remove(25) + " " + list.remove(99));
list.print();
10 20 30 40
10 20 25 30 40
removeFirst=10
removeLast=40
20 25 30
true false
20 30
addFirst(20) [20]
addFirst(10) [10, 20]
addLast(30) [10, 20, 30]
addLast(40) [10, 20, 30, 40]
insertAt(2,25) [10, 20, 25, 30, 40] (after the 20 at index 1)
removeFirst [20, 25, 30, 40] returns 10
removeLast [20, 25, 30] returns 40
remove(25) [20, 30] true
remove(99) [20, 30] false, unchanged
On the exam, draw this table yourself. Writing the whole list at each row prevents mistakes.
Complexity summary
| Operation | Head only | head + tail | Array |
|---|---|---|---|
| addFirst | O(1) | O(1) | O(n) (shifting) |
| addLast | O(n) | O(1) | O(1) (if there is room) |
| removeFirst | O(1) | O(1) | O(n) |
| removeLast | O(n) | O(n) | O(1) |
| get(i) | O(n) | O(n) | O(1) |
| contains | O(n) | O(n) | O(n) |
Common mistakes
| Wrong | Correct |
|---|---|
head = head.next while traversing | Use a helper Node p = head; |
p.next = n first when inserting in the middle | First n.next = p.next, then p.next = n |
| No empty-list check in addLast | If head == null then head = n |
| Stopping at the target node itself when deleting | Stop at the one before it, p.next = p.next.next |
while (p.next.data != x && p.next != null) | Null check first: p.next != null && ... |
| With a tail, removeLast is O(1) | Still O(n): you have to walk to find the one before the last |
p = p.next.next is always safe | NullPointerException with an odd number of nodes |
Test yourself
Exam-style questions. Solve them on paper first, then reveal the answer.
Starting from an empty list, what does print() show after these calls?
list.addFirst(3);
list.addFirst(7);
list.addLast(5);
list.addFirst(1);
list.print();Show answer
addFirst(3) → [3]. addFirst(7) → [7, 3]. addLast(5) → [7, 3, 5]. addFirst(1) → [1, 7, 3, 5]. addFirst always makes the new node the head.
Starting from an empty list, what is printed?
list.addLast(4);
list.addLast(8);
list.addFirst(2);
list.addLast(6);
list.removeFirst();
list.addFirst(9);
list.removeLast();
list.print();Show answer
[4] → [4, 8] → [2, 4, 8] → [2, 4, 8, 6] → removeFirst removes 2: [4, 8, 6] → addFirst(9): [9, 4, 8, 6] → removeLast removes 6: [9, 4, 8].
What is printed?
Node a = new Node(1);
a.next = new Node(2);
a.next.next = new Node(3);
a.next.next.next = new Node(4);
a.next = a.next.next;
for (Node p = a; p != null; p = p.next)
System.out.print(p.data + " ");Show answer
The chain is 1 → 2 → 3 → 4. The line a.next = a.next.next links the next of 1 to 3. Nobody points to 2 any more, so it has dropped out of the list (the garbage collector cleans it up). What remains: 1 3 4.
What is printed?
Node h = new Node(5);
h.next = new Node(10);
h.next.next = new Node(15);
Node t = h.next;
t.next.data = t.data + h.data;
h.data = t.next.data * 2;
for (Node p = h; p != null; p = p.next)
System.out.print(p.data + " ");Show answer
t is the second node (10). t.next is the third node: data = 10 + 5 = 15 (it was already 15). h.data = t.next.data * 2 = 30. The list: 30 10 15. The reference variable t does not create a new node, it points to an existing one.
Using insertAt(index, x) (index 0 = front), what is printed? Start: list 1 2 3.
list.insertAt(1, 9);
list.insertAt(4, 7);
list.insertAt(0, 5);
list.print();
list.remove(2);
list.remove(5);
list.print();Show answer
[1,2,3] → insertAt(1,9): [1,9,2,3] → insertAt(4,7) at the end: [1,9,2,3,7] → insertAt(0,5): [5,1,9,2,3,7]. Then remove(2) removes the one whose value is 2: [5,1,9,3,7]; remove(5) removes the head: [1,9,3,7].
Write a method int countNodes() for the SinglyLinkedList class that returns the number of nodes without using the size field.
Show answer
Start a reference at head and advance it until it is null, counting at every step. On an empty list the loop never runs and 0 is returned.
int countNodes() {
int c = 0;
Node p = head;
while (p != null) {
c++;
p = p.next;
}
return c;
}For the list 4 7 2 9 5 it returns 5. Time O(n).Write void addLast(int x) for a singly linked list that has only a head reference (no tail). Handle the empty list.
Show answer
There are two cases. In an empty list the new node becomes the head. Otherwise, go to the last node (the node whose p.next is null) and link the new node to its next.
void addLast(int x) {
Node n = new Node(x);
if (head == null) {
head = n;
return;
}
Node p = head;
while (p.next != null)
p = p.next;
p.next = n;
}Without the empty-list check, the p.next line throws a NullPointerException. Time O(n), because you have to walk to the end.Write boolean remove(int x) that removes the first node whose data equals x and returns whether something was removed.
Show answer
To delete a node in a singly linked list you need the node before it. The head is a special case, because there is nothing before it.
boolean remove(int x) {
if (head == null) return false;
if (head.data == x) {
head = head.next;
return true;
}
Node p = head;
while (p.next != null && p.next.data != x)
p = p.next;
if (p.next == null) return false; // not found
p.next = p.next.next; // skip it
return true;
}On the list 20 25 30, remove(25) → true, the list becomes 20 30. remove(99) → false, the list is unchanged.For a singly linked list with only a head reference, what are the time complexities of addFirst, addLast and removeLast? What changes if a tail reference is added?
Show answer
addFirst only changes head: O(1). addLast and removeLast walk to the end: O(n). When tail is added, addLast becomes O(1). removeLast stays O(n), because to find the node before the last you still have to walk from the start (you can't go backward in a singly linked list).
p points to the first node of the list 1 2 3. A student inserts node n (data 8) after p with the two lines below. What happens when the list is printed?
p.next = n;
n.next = p.next;Show answer
The order is wrong. After the first line p.next is now n. The second line makes n.next = n, so the node points to itself. 2 and 3 are lost, and the print loop prints 8 forever. The right order: first n.next = p.next; then p.next = n;. Rule: first link the new node into the chain, then redirect the old one to it.
The list contains 10 20 30 40 50. What happens?
for (Node p = head; p != null; p = p.next.next)
System.out.print(p.data + " ");Show answer
p: 10 → 30 → 50 are printed. At 50, p.next is null, and p.next.next tries to access the next of null and a NullPointerException is thrown. If the list had an even number of elements (6 elements), p would become null at the last step and it would end without a problem. A safe condition: check with p != null && p.next != null.
The list is 4 7 2 9 5. What is printed?
int mx = head.data, cnt = 0;
for (Node r = head; r != null; r = r.next) {
if (r.data > mx) mx = r.data;
if (r.data % 2 == 1) cnt++;
}
System.out.println(mx + " " + cnt);Show answer
The largest element is 9. The odd ones are 7, 9, 5: three of them. The output is 9 3.
Verification: All list operations and traces were run on the JDK with the same Node/SinglyLinkedList code in verify/SEN2211/W02.java (java W02.java); every output on the page is taken verbatim from the program's output.