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.
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:
- Before changing a reference, save the rest you need.
- The two-reference technique (two pointers): one fast and one slow, or one ahead and one behind.
- A dummy head node eliminates the checks “is the list empty, does the head change”.
- Try every method in your head on an empty list and a single-element list.
Review: a summary of three weeks
| Operation | Singly (head) | Singly (head+tail) | Doubly (head+tail) | Array |
|---|---|---|---|---|
| Insert at front | O(1) | O(1) | O(1) | O(n) |
| Insert at end | O(n) | O(1) | O(1) | O(1)* |
| Delete from front | O(1) | O(1) | O(1) | O(n) |
| Delete from end | O(n) | O(n) | O(1) | O(1) |
| i-th element | O(n) | O(n) | O(n) | O(1) |
| Search | O(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 isprev. 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?
| Criterion | Array | Linked list |
|---|---|---|
| Access to the i-th element | O(1) | O(n) |
| Insert / delete at the front | O(n), shifting | O(1) |
| Insert after a known node | O(n) | O(1) |
| Size | Fixed (copy when full) | Dynamic |
| Memory | Only data, contiguous | Data + next (+ prev) |
| Binary search (if sorted) | Possible, O(log n) | Not possible |
| Cache friendly | Yes | No, 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
| Wrong | Correct |
|---|---|
| Not saving next in the reversal | First Node next = curr.next; |
return head at the end of reverse | return prev |
slow.data == fast.data in the cycle check | slow == fast (the same node) |
while (fast.next != null) | Check fast != null first, otherwise an NPE |
| Not appending the leftover list at the end of merge | t.next = (a != null) ? a : b; |
return dummy in merge | return dummy.next |
| Advancing after deleting while removing duplicates | Triple 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.
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
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.
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 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.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
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.
Write static boolean hasCycle(Node head) that returns true if the list contains a cycle. Use O(1) extra memory.
Show answer
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.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
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.
What does merge(a, b) return for the sorted lists a = 1 4 7 and b = 2 3 8 9?
Show answer
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.
Write static Node merge(Node a, Node b) that merges two ascending lists into one ascending list by relinking the existing nodes.
Show answer
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.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
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.
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
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.
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
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.
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) 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.
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
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.