SEN2211 · Week 4 · Linked List Applications and Review

Linked list applications and review

Reversing a list, finding the middle, cycle detection, merging two sorted lists, and comparing lists with arrays. Most of the midterm's “write a method” questions come from these four algorithms.

reverse a linked listslow and fast pointersmiddle nodecycle detectionFloyd's algorithmmerge two sorted listsdummy nodetwo-pointer techniquearray vs linked list

This week there is no new structure. You use the node skills you built in the first three weeks on the four algorithms most often asked on the exam: reversing a list, finding the middle, detecting a cycle and merging two sorted lists. All four run in O(n) time without using an extra array. At the end there is a summary comparing lists with arrays; for pre-midterm review, this page is enough.

Golden rules:

  1. Before changing a reference, save the rest you need.
  2. The two-reference technique (two pointers): one fast and one slow, or one ahead and one behind.
  3. A dummy head node eliminates the checks “is the list empty, does the head change”.
  4. Try every method in your head on an empty list and a single-element list.

Review: a summary of three weeks

OperationSingly (head)Singly (head+tail)Doubly (head+tail)Array
Insert at frontO(1)O(1)O(1)O(n)
Insert at endO(n)O(1)O(1)O(1)*
Delete from frontO(1)O(1)O(1)O(n)
Delete from endO(n)O(n)O(1)O(1)
i-th elementO(n)O(n)O(n)O(1)
SearchO(n)O(n)O(n)O(n), O(log n) if sorted

* If there is room in the array. If it fills up, copying to a new array is O(n).

Reversing a list (reverse)

The idea: turn the arrow of every node backward. But the moment you change curr.next you can’t reach the rest of the list, so save it first.

static Node reverse(Node head) {
    Node prev = null, curr = head;
    while (curr != null) {
        Node next = curr.next;   // 1) save the rest
        curr.next = prev;        // 2) flip the arrow
        prev = curr;             // 3) prev one step forward
        curr = next;             // 4) curr one step forward
    }
    return prev;
}

The state after each round on 1 2 3 4 (program output):

step 1: prev-list = 1 | curr = 2
step 2: prev-list = 2 1 | curr = 3
step 3: prev-list = 3 2 1 | curr = 4
step 4: prev-list = 4 3 2 1 | curr = null
4 3 2 1

PITFALL · writing return head. When the loop ends, the old head (1) is now the last node of the list and its next is null. The new head is prev. On the caller’s side, don’t forget to assign: head = reverse(head);.

The middle node: slow and fast references

Counting the length first and then walking n/2 steps is also a correct answer (two passes). To do it in a single pass, use two references: fast takes two steps while slow takes one. When fast reaches the end, slow is halfway.

static Node middle(Node head) {
    Node slow = head, fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }
    return slow;
}
1..7 -> 4
1..6 -> 4
10 20 30 40 50 -> 30

With an even length there are two middles (3 and 4 for 1..6). With this loop condition it returns the second middle. If the question wants the first middle, change the condition to fast.next != null && fast.next.next != null.

Cycle detection (Floyd)

Faulty code may have accidentally linked the next of the last node to an earlier node. If you try to print this list, you get into an infinite loop. To tell whether there is a cycle with O(1) memory, the same slow/fast idea is used: of two people running on a track, if the track is circular, the faster one is bound to lap the slower one.

static boolean hasCycle(Node head) {
    Node slow = head, fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow == fast) return true;
    }
    return false;
}

Trace: 1 → 2 → 3 → 4 → 5 and 5 points back to 3.

step   slow   fast
 0      1      1
 1      2      3
 2      3      5
 3      4      4    (5 → 3 → 4)  they met → true

The program gave the same result: met after 3 steps, meeting node 4. In a 6-node list whose end is linked to the start, they meet at 1 after 6 steps.

TIP · Why is it bound to catch it? Once both are in the cycle, at each step the fast one advances one node more, so the distance between them shrinks by 1. When the distance drops to 0 they meet; they can’t jump over each other.

Merging two sorted lists (merge)

This operation is the heart of merge sort (week 14). Compare the fronts of the two lists, add the smaller to the result, and advance in that list.

static Node merge(Node a, Node b) {
    Node dummy = new Node(0), t = dummy;
    while (a != null && b != null) {
        if (a.data <= b.data) { t.next = a; a = a.next; }
        else                  { t.next = b; b = b.next; }
        t = t.next;
    }
    t.next = (a != null) ? a : b;
    return dummy.next;
}
1 2 3 4 7 8 9
2 5 10 10 12 15
3 6

The third line is merge(null, 3 6): if one is empty the other is returned as it is. Without the dummy, you would have to pick the first element separately and make it the head; the dummy removes this special case. The result is dummy.next, not the dummy itself.

Using <= takes from a first when values are equal. This preserves the property we call stability in sorting algorithms.

Other two-reference patterns

The k-th element from the end. Move lead k steps ahead, then walk both together. When lead is null, trail is at the k-th node from the end. For 10 20 30 40 50 60 and k = 2 it is 50.

Removing duplicates in a sorted list. Skip an equal neighbour and don’t advance; advance if different. 1 1 2 3 3 3 4 → 1 2 3 4.

Swapping in pairs. Skip two at a time with p = p.next.next and swap data. 1 2 3 4 5 → 2 1 4 3 5.

List or array?

CriterionArrayLinked list
Access to the i-th elementO(1)O(n)
Insert / delete at the frontO(n), shiftingO(1)
Insert after a known nodeO(n)O(1)
SizeFixed (copy when full)Dynamic
MemoryOnly data, contiguousData + next (+ prev)
Binary search (if sorted)Possible, O(log n)Not possible
Cache friendlyYesNo, nodes are scattered

Rule: if there is a lot of random access, an array; if there is a lot of insertion/deletion at the front or middle, a list.

Common mistakes

WrongCorrect
Not saving next in the reversalFirst Node next = curr.next;
return head at the end of reversereturn prev
slow.data == fast.data in the cycle checkslow == fast (the same node)
while (fast.next != null)Check fast != null first, otherwise an NPE
Not appending the leftover list at the end of merget.next = (a != null) ? a : b;
return dummy in mergereturn dummy.next
Advancing after deleting while removing duplicatesTriple duplicates remain; advance only if different
“A list is always faster than an array”In access the array wins, in insertion the list

Test yourself

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

Question 1easy

What does the list print after head = reverse(head) if the list was 1 2 3 4? After the second iteration of the loop, what list does prev point to, and which node is curr?

Show answer
4 3 2 1; after step 2: prev = 2 1, curr = 3

In each round the next of curr is turned to prev and both references move one step forward. Step 1: prev = 1, curr = 2. Step 2: prev = 2 → 1, curr = 3. At step 4 curr becomes null, and prev = 4 3 2 1 is the new head.

Question 2medium

Write a static method Node reverse(Node head) that reverses a singly linked list in place (no new nodes) and returns the new head.

Show answer
Three pointers prev, curr, next; flip curr.next each step; return prev.

Three references: prev (the reversed part), curr (the node being processed), next (the rest, saved so as not to lose it).

static Node reverse(Node head) {
    Node prev = null, curr = head;
    while (curr != null) {
        Node next = curr.next;   // save the rest
        curr.next = prev;        // flip the arrow
        prev = curr;             // one step forward
        curr = next;
    }
    return prev;                 // the new head
}
Time O(n), extra memory O(1). On an empty list the loop doesn't run and null is returned. If you do curr.next = prev without saving next, you lose the rest of the list.

Question 3medium

Using slow/fast pointers with the loop condition fast != null && fast.next != null, which node does middle() return for the list 1 2 3 4 5 6? For 10 20 30 40 50?

Node slow = head, fast = head;
while (fast != null && fast.next != null) {
    slow = slow.next;
    fast = fast.next.next;
}
return slow;
Show answer
4 and 30

fast takes two steps and slow takes one; when fast reaches the end, slow is in the middle. With 6 elements slow goes 1 → 2 → 3 → 4, fast goes 1 → 3 → 5 → null. If there are two middles (3 and 4), it gives the second: 4. With 5 elements it is the exact middle: 30.

Question 4hard

Write static boolean hasCycle(Node head) that returns true if the list contains a cycle. Use O(1) extra memory.

Show answer
Floyd: slow moves 1, fast moves 2; if they ever meet there is a cycle, if fast reaches null there is not.

If there is no cycle, the fast reference eventually reaches null. If there is a cycle, both go around inside the cycle and the fast one is bound to catch the slow one (at each step the distance between them shrinks by 1).

static boolean hasCycle(Node head) {
    Node slow = head, fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow == fast) return true;   // the same NODE (==), not the same data
    }
    return false;
}
Don't write slow.data == fast.data: different nodes can hold the same value. A reference comparison is needed.

Question 5hard

The list is 1 → 2 → 3 → 4 → 5 and node 5's next points back to node 3. Running Floyd's algorithm (both start at head), after how many steps do slow and fast meet, and at which node?

Show answer
3 steps, at node 4

Step 1: slow 2, fast 3. Step 2: slow 3, fast 5. Step 3: slow 4, fast 5 → 3 → 4. Both are at 4. The program's output was also met after 3 steps, node 4.

Question 6easy

What does merge(a, b) return for the sorted lists a = 1 4 7 and b = 2 3 8 9?

Show answer
1 2 3 4 7 8 9

At each step the smaller of the fronts of the two lists is taken: 1(a), 2(b), 3(b), 4(a), 7(a). a is finished, and the remainder of b (8 9) is appended as it is.

Question 7hard

Write static Node merge(Node a, Node b) that merges two ascending lists into one ascending list by relinking the existing nodes.

Show answer
Use a dummy head and a tail pointer; repeatedly attach the smaller front node; attach the leftover list at the end.

A dummy head node eliminates the special case of inserting at the front.

static Node merge(Node a, Node b) {
    Node dummy = new Node(0), t = dummy;
    while (a != null && b != null) {
        if (a.data <= b.data) { t.next = a; a = a.next; }
        else                  { t.next = b; b = b.next; }
        t = t.next;
    }
    t.next = (a != null) ? a : b;   // the leftover list
    return dummy.next;
}
5 10 15 with 2 10 12 → 2 5 10 10 12 15. If one of the lists is null, the other is returned as it is. Time O(n + m), no new nodes.

Question 8medium

The sorted list is 1 1 2 3 3 3 4. What is printed after the method runs?

Node p = head;
while (p != null && p.next != null) {
    if (p.data == p.next.data) p.next = p.next.next;
    else p = p.next;
}
print(head);
Show answer
1 2 3 4

If two neighbouring nodes are equal, it skips the next one and does not advance p (there may be three 3s). If they differ, it advances. In a sorted list the duplicates are side by side, so this is enough. Result: 1 2 3 4.

Question 9medium

lead is moved k nodes ahead, then lead and trail move together until lead is null. For the list 10 20 30 40 50 60 and k = 2, what is trail.data?

Node lead = head, trail = head;
for (int i = 0; i < k; i++) lead = lead.next;
while (lead != null) { lead = lead.next; trail = trail.next; }
return trail.data;
Show answer
50

The gap between them is always k = 2 nodes. When lead reaches null (one past the end), trail is at the 2nd node from the end: 50. If k = 6 it would return 10. It finds the k-th element from the end in a single pass.

Question 10medium

What is printed for the list 1 2 3 4 5?

for (Node p = head; p != null && p.next != null; p = p.next.next) {
    int t = p.data;
    p.data = p.next.data;
    p.next.data = t;
}
print(head);
Show answer
2 1 4 3 5

In each round the data of p and p.next are swapped and p skips two nodes. (1,2) → 2 1, (3,4) → 4 3, and since 5 is left alone, the condition p.next != null fails and it is not touched.

Question 11easy

Choose array or linked list for each: (a) frequent insertions at the front, (b) frequent access to the i-th element, (c) unknown, constantly changing size with insertions in the middle of a traversal.

Show answer
(a) linked list, (b) array, (c) linked list

(a) Inserting at the front is O(1) in a list; in an array all elements shift: O(n). (b) In an array a[i] is O(1); in a list you have to walk i steps: O(n). (c) A list grows as much as needed and inserts after the node you hold in O(1); an array must be copied when full, and inserting in the middle needs shifting.

Question 12hard

What is printed?

Node x = build(3, 6, 9);
x = reverse(x);
Node y = build(1, 5);
Node z = merge(y, reverse(x));
System.out.println(str(z) + " middle=" + middle(z).data);
Show answer
1 3 5 6 9 middle=5

When x is reversed it is 9 6 3. Reversed again it is 3 6 9 (sorted). Merged with 1 5 it becomes 1 3 5 6 9. In a list of five elements the middle is 5.

Verification: All algorithms were run with the JDK in verify/SEN2211/W04.java (java W04.java). The reversal trace comes from the program's step-by-step output; the meeting node in the cycle and the step count come from the program's counter.

Review card

REVERSE: prev=null, curr=head; loop: next=curr.next; curr.next=prev; prev=curr; curr=next; return prev MIDDLE: slow 1 step, fast 2 steps; while (fast != null && fast.next != null); for even length the SECOND middle CYCLE (Floyd): same slow/fast; if slow == fast there is a cycle; if fast reaches null there is none In cycle detection compare NODES with ==, not data MERGE: dummy node + t; attach the smaller; at the end t.next = leftover list; O(n+m) k-th from the end: move lead k ahead, then walk both together Remove duplicates in a sorted list: if equal p.next = p.next.next (p does not advance) All are O(n) time, O(1) extra memory Array: a[i] O(1), insertion at front/middle O(n), fixed size, tight in memory List: get(i) O(n), insertion at front O(1), dynamic size, next cost per node Special cases: empty list, single element, does the head change?