SEN2211 · Week 2 · Introduction to Linked Lists

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”.

linked listnodeheadtailnext referencetraversaladdFirstaddLastremoveFirstnull referenceNullPointerException

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:

  1. List = the head reference. If you lose the head, you lose the list.
  2. Always traverse with p = p.next. The stopping condition is p != null (visit every node) or p.next != null (stop at the last node).
  3. When inserting, first link the new node, then redirect the old one.
  4. To delete, find the previous node. You can’t go backward in a singly linked list.
  5. 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 do p.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 of p.next (30) is lost. Then n.next = p.next; actually becomes n.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

OperationHead onlyhead + tailArray
addFirstO(1)O(1)O(n) (shifting)
addLastO(n)O(1)O(1) (if there is room)
removeFirstO(1)O(1)O(n)
removeLastO(n)O(n)O(1)
get(i)O(n)O(n)O(1)
containsO(n)O(n)O(n)

Common mistakes

WrongCorrect
head = head.next while traversingUse a helper Node p = head;
p.next = n first when inserting in the middleFirst n.next = p.next, then p.next = n
No empty-list check in addLastIf head == null then head = n
Stopping at the target node itself when deletingStop 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 safeNullPointerException with an odd number of nodes

Test yourself

Exam-style questions. Solve them on paper first, then reveal the answer.

Question 1easy

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
1 7 3 5

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.

Question 2medium

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
9 4 8

[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].

Question 3medium

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
1 3 4

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.

Question 4medium

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
30 10 15

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.

Question 5hard

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
5 1 9 2 3 7 / 1 9 3 7

[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].

Question 6easy

Write a method int countNodes() for the SinglyLinkedList class that returns the number of nodes without using the size field.

Show answer
Traverse with p from head to null, counting.

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).

Question 7medium

Write void addLast(int x) for a singly linked list that has only a head reference (no tail). Handle the empty list.

Show answer
Walk to the last node (p.next == null) and link the new node.

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.

Question 8hard

Write boolean remove(int x) that removes the first node whose data equals x and returns whether something was removed.

Show answer
Special-case head; otherwise stop at the node before the target and do p.next = p.next.next.

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.

Question 9easy

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
O(1), O(n), O(n); with tail: addLast O(1), removeLast still O(n)

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).

Question 10medium

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
1 8 8 8 ... (infinite loop: n points to itself)

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.

Question 11hard

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
10 30 50, then NullPointerException

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.

Question 12easy

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
9 3

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.

Review card

Node: int data; Node next; list = just the head reference (+ tail, size if you like) Traversal: for (Node p = head; p != null; p = p.next) Stop at the last node: while (p.next != null) p = p.next; addFirst: n.next = head; head = n; O(1) addLast (no tail): walk to the end, p.next = n; O(n) empty-list check REQUIRED Insert in the middle (after p): FIRST n.next = p.next; THEN p.next = n; Delete (the one after p): p.next = p.next.next; to delete you need the PREVIOUS node removeFirst: head = head.next; O(1) removeLast: walk to the node before the last, p.next = null; O(n) even with a tail Deleting the head is a special case; empty list is a special case; single-element list is a special case NPE traps: null.next, head.data on an empty list, p.next.next Array vs list: a[i] is O(1) but the i-th element of a list is O(n)