Java LinkedList exercises — the booking at position N, the repeated members and the queue split in half (with solutions and cheat sheet)
In an Exam 3 mock I wrote removeDuplicates comparing with ==. I tested it with names typed into main and it worked first time. The lecturer’s tests read the names from a file and my method didn’t remove a single one. It was the == trap with Strings that we saw in the theory, and you’ll see it with real output in fragment 1 of “What’s wrong here?”.
You’ve seen the theory and practised with three library programs. Now it’s your turn. Three exercises at three levels on the same booking queue at the faculty library: read the booking at a position and find a member’s position, remove repeated members without collections and split the queue in half in a single pass. All three go after the mistakes that cost the most marks in the linked-structures exam: moving one node too far, comparing text with == and forgetting one end.
How to use this article: try each exercise on your own; if you’ve been stuck for more than ten minutes, open the hint; before looking at the solution, do “What’s wrong here?”, and paste your code into Python Tutor (choosing Java) to see the arrows. At the end there’s an exam-style quiz and the topic’s cheat sheet.
All three exercises use the exam’s template: the same Node and ContainerBase as in the theory (with first protected) and your class with extends ContainerBase, which has its own size and insertBack (the same as in the theory). Remember the rule: no arrays and no collections to store the data; in the tests we only use them to set up the names.
Table of Contents
Java LinkedList exercises: basic level
Exercise 1: get the booking at a position and find a member
Write the SearchableQueue class with two methods:
String get(int pos): the member at that position, starting at 0 at the head. If the position doesn’t exist (negative or greater than or equal tosize), it returnsnull.int indexOf(String member): the position of the first occurrence of the member, or-1if they’re not there. It has to work even if the text is another object (new String("Marta")).
With the empty queue and then with Laura -> Pablo -> Marta -> Pablo, the output must be:
Empty: get(0) = null, indexOf("Laura") = -1
➡Laura->Pablo->Marta->Pablo->null
get(0) = Laura, get(2) = Marta, get(3) = Pablo
get(4) = null, get(-1) = null
indexOf("Pablo") = 1, indexOf("Nora") = -1
indexOf(new String("Marta")) = 2
In get, validate before moving and then make pos hops with a for. In indexOf, traverse with the usual for while keeping a counter, and compare with equals. If you come out of the loop without finding it, -1.
Java LinkedList exercises: intermediate level
Exercise 2: remove repeated members without collections
Some members book the same book two or three times. Write the NoDuplicatesQueue class with int removeDuplicates(), which keeps only the first occurrence of each member, modifying the queue itself (without creating another), keeps size correct and returns how many it deleted. No HashSet, no ArrayList and no arrays: the exam doesn’t allow them.
Expected output with these four queues:
➡Laura->Pablo->Laura->Marta->Pablo->Iván->Laura->null => ➡Laura->Pablo->Marta->Iván->null (deleted 3, size 4) ➡Ana->Nora->Iván->null => ➡Ana->Nora->Iván->null (deleted 0, size 3) ➡Marta->Marta->Marta->Marta->null => ➡Marta->null (deleted 3, size 1) ➡null => ➡null (deleted 0, size 0)
Without a set of “seen” values, each node has to look for its copies behind it. Two loops: the outer one picks a node (current) and the inner one traverses what’s behind it with the “stop at the previous one” technique from the practice. The key is when previous moves on: if you delete the next one, its new next hasn’t been checked yet. Try it in your head with four Martas in a row. And compare with equals.
Java LinkedList exercises: final challenge
Exercise 3: split the queue in half in a single pass
The library opens a second desk and wants to share the booking queue between the two: the first half stays at the usual desk and the second moves to the new one, in the same order. If the queue has an odd number of people, the middle one stays at the first.
Write SplittableQueue splitInHalf(), which cuts the queue itself and returns another SplittableQueue with the second half, with both sizes correct. The challenge’s condition: a single pass, without using size to find the middle, and without creating new nodes.
Expected output:
➡Laura->Pablo->Marta->Iván->Nora->null => ➡Laura->Pablo->Marta->null (3) + ➡Iván->Nora->null (2) ➡Laura->Pablo->Marta->Iván->null => ➡Laura->Pablo->null (2) + ➡Marta->Iván->null (2) ➡Ana->null => ➡Ana->null (1) + ➡null (0) ➡null => ➡null (0) + ➡null (0)
Two references moving at different speeds: slow makes one hop per lap and fast, two. When fast reaches the end, slow will be in the middle. Make fast start one node ahead (first.next) and think, with four nodes, where you want slow to stop: at the last node of the first half, because that’s the one that has to cut its arrow.
What’s wrong here?
Three fragments with a mistake of the kind that comes up in exams. Predict what each one prints before reading the explanation.
Fragment 1: the repeats that won’t go
removeDuplicates with a single difference: it compares with ==.
public int removeDuplicates() {
int deleted = 0;
for (Node current = first; current != null; current = current.next) {
Node previous = current; // we look for copies of current BEHIND it
while (previous.next != null) {
if (previous.next.value == current.value) {
previous.next = previous.next.next; // we skip the copy; previous does NOT move
deleted++;
} else {
previous = previous.next;
}
}
}
size -= deleted;
return deleted;
}
The test puts the same three names into two queues: in one with the literals as they are and in the other with new String(s), as if they came from a file or the keyboard:
public class BadTest {
public static void main(String[] args) {
NoDuplicatesQueue literals = new NoDuplicatesQueue();
NoDuplicatesQueue fromFile = new NoDuplicatesQueue();
for (String s : new String[] {"Laura", "Pablo", "Laura"}) {
literals.insertBack(s); // literals: Java reuses the same object
fromFile.insertBack(new String(s)); // as if they came from a file: different objects
}
System.out.println("Literals: deleted " + literals.removeDuplicates() + " -> " + literals);
System.out.println("From file: deleted " + fromFile.removeDuplicates() + " -> " + fromFile);
}
}
Literals: deleted 1 -> ➡Laura->Pablo->null From file: deleted 0 -> ➡Laura->Pablo->Laura->null
What’s wrong: == compares references. Equal literals you write in the code are the same object (Java reuses them), so in the first queue the trick “works”. In the second, each new String is a different object and == says Laura isn’t Laura: nothing is deleted. That’s exactly my mock exam: tests don’t always use literals. Fix: previous.next.value.equals(current.value).
Accepting a method with == because it passes your tests. Always test it with new String(...).
Fragment 2: the counter that falls short
public int count() {
int total = 0;
Node n = first;
while (n.next != null) { // WRONG: it stops BEFORE the last one
total++;
n = n.next;
}
return total;
}
It’s tested with Laura -> Pablo -> Marta and then with an empty queue:
➡Laura->Pablo->Marta->null has 2 nodes
Exception in thread "main" java.lang.NullPointerException: Cannot read field "next" because "n" is null
at CountQueue.count(CountQueue.java:12)
at CountTest.main(CountTest.java:9)
What’s wrong: two things, because of the same condition. while (n.next != null) stops at the last node without counting it: with three nodes it gives 2. And with the empty queue, n is already null before going in, so n.next throws the NullPointerException on line 12 of CountQueue.java, the while line. Notice that Java’s message tells you exactly what was null: "n". Fix: while (n != null), which counts them all and doesn’t go in with the empty queue.
Fragment 3: the empty queue that remembers someone
The practice’s serve for the queue with first and last, with one line missing:
public String serve() {
if (first == null) {
return null; // nobody to serve
}
String member = first.value;
first = first.next;
size--;
return member;
}
Laura and Pablo are enqueued and both are served:
Empty: ➡null | first null, last Pablo, size 0
What’s wrong: when the last person is served, first becomes null but last still points at Pablo. The queue claims to be empty (➡null, size 0) and at the same time to have a last one. So far it only lies; the serious part comes with the next enqueue: since first == null, it makes the new node first and last and everything adds up again by chance. But if another method of yours uses last before that (for example, a peekLast() in the exam’s tests), the test fails. Fix: after first = first.next;, if (first == null) last = null;.
Solutions explained
Solution to exercise 1
public String get(int pos) {
if (pos < 0 || pos >= size) { // we validate BEFORE moving
return null;
}
Node current = first;
for (int i = 0; i < pos; i++) { // pos hops from the head
current = current.next;
}
return current.value;
}
public int indexOf(String member) {
int pos = 0;
for (Node n = first; n != null; n = n.next) {
if (n.value.equals(member)) {
return pos; // the first one that matches
}
pos++;
}
return -1; // not there
}
get: the validation goes before the loop because, otherwise, with pos equal to size the last hop leaves current at null and current.value throws NullPointerException. With a negative pos the for wouldn’t do a single lap and you’d return the head, which is worse: a wrong result with no error. Then, pos hops: for get(2), from Laura to Pablo (i = 0) and from Pablo to Marta (i = 1); with i = 2 the for ends and we return Marta.
indexOf: the counter pos moves at the same time as n. The return inside the loop means we return the first occurrence: with Pablo twice, it gives 1, not 3. And because we compare with equals, new String("Marta") is found at position 2.
| Lap | n | pos | n.value.equals("Pablo")? |
|---|---|---|---|
| 1 | Laura | 0 | No |
| 2 | Pablo | 1 | Yes: returns 1 |
A method that searches and might not find anything returns an “impossible” value (-1 for positions, null for objects). That’s what indexOf does in String and in Java’s collections.
Solution to exercise 2
public int removeDuplicates() {
int deleted = 0;
for (Node current = first; current != null; current = current.next) {
Node previous = current; // we look for copies of current BEHIND it
while (previous.next != null) {
if (previous.next.value.equals(current.value)) {
previous.next = previous.next.next; // we skip the copy; previous does NOT move
deleted++;
} else {
previous = previous.next;
}
}
}
size -= deleted;
return deleted;
}
The outer for traverses the queue with current. For each member, the inner while starts with previous = current and always looks at previous.next: if it’s a copy, we skip it and previous doesn’t move; if not, it moves on. That way, when current reaches a node, all its copies behind it are already gone, and the for carries on with nodes that are the first occurrence of their value.
Trace with Marta -> Marta -> Marta -> Marta (current on the first Marta):
| Lap | previous | previous.next | Copy? | What happens |
|---|---|---|---|---|
| 1 | Marta 1 | Marta 2 | Yes | Marta 1 points at Marta 3, previous stays |
| 2 | Marta 1 | Marta 3 | Yes | Marta 1 points at Marta 4 |
| 3 | Marta 1 | Marta 4 | Yes | Marta 1 points at null |
| 4 | Marta 1 | null | — | The while ends; the for moves to null and finishes |
Three deleted and size 4 − 3 = 1, as in the output. With the first queue, Laura deletes her two copies, Pablo one and Marta and Iván none: 2 + 1 = 3 deleted and 7 − 3 = 4 nodes.
Why O(n²)? Each node traverses what’s behind it. With a HashSet of seen values it would be O(n), but the exam forbids collections. With library queues of a few dozen people, it doesn’t matter.
“look for copies behind me” is the same idea as the practice’s removeAll, except the value to delete changes on each lap of the for.
Solution to exercise 3
public SplittableQueue splitInHalf() {
SplittableQueue second = new SplittableQueue();
if (first == null || first.next == null) { // 0 or 1 nodes: there is no second half
return second;
}
Node slow = first;
Node fast = first.next;
while (fast != null && fast.next != null) {
slow = slow.next; // 1 hop
fast = fast.next.next; // 2 hops
}
second.first = slow.next; // the second half starts after slow
slow.next = null; // we cut the chain
second.size = size / 2; // the first keeps the extra one if odd
size = size - second.size;
return second;
}
Special cases first: with 0 or 1 nodes there’s no second half, and the loop couldn’t start (first.next with first set to null throws NullPointerException). We return an empty queue.
The loop: fast makes two hops for every one of slow. Since it starts one node ahead, when fast can no longer make two hops, slow is on the last node of the first half. Trace with five people:
| Lap | slow | fast | Go on? |
|---|---|---|---|
| Start | Laura | Pablo | fast.next is Marta: yes |
| 1 | Pablo | Iván | fast.next is Nora: yes |
| 2 | Marta | null | No |
slow stops at Marta: the second half starts at Iván. With four, fast ends on Iván (whose next is null) and slow on Pablo: 2 + 2. Look at the drawing: the cut is two lines. First the new queue saves slow.next and then slow.next = null cuts the chain (once again, save first and move afterwards).
The sizes: the second half has size / 2 (integer division: 5 / 2 = 2) and the first, the rest (5 − 2 = 3). There’s no need to count again.
Why does second.first compile? first is protected and we’re inside SplittableQueue, which inherits from ContainerBase: we can touch the first of another object of our own class.
This two-speed technique (the tortoise and the hare) also works for finding the middle element or detecting whether a list has a cycle, which you saw in the Python LinkedList exercises.
Exam-style quiz
Eleven questions like the ones in the FP2 linked-structures exam. Answer without looking at the article and then go over the explanations.
Complete how a linked list is stored in Java:
Each node stores a piece of data in value and a reference to the next node in ____. The list only stores the first node, first, which is ____ when it is empty. The last node in the chain has next set to null. For a subclass to be able to use first, ContainerBase declares it ____.
Words:
- 0
- private
- null
- last
- protected
- size
- next
Solution
Each node stores a piece of data in value and a reference to the next node in next. The list only stores the first node, first, which is null when it is empty. The last node in the chain has next set to null. For a subclass to be able to use first, ContainerBase declares it protected.
A node doesn’t know its position: only its data and who comes next. The list only knows the head (first). With private the subclass couldn’t touch first (first has private access); with protected, it can.
What does this fragment print? Node has public value and next and a constructor that receives the value.
Node a = new Node("A");
Node b = new Node("B");
Node c = new Node("C");
a.next = b;
b.next = c;
Node newNode = new Node("X");
newNode.next = a.next;
a.next = newNode;
String s = "";
for (Node n = a; n != null; n = n.next) {
s += n.value;
}
System.out.println(s);Solution
Answer: AXBC
newNode first points to whatever was after a (B) and then a points to newNode: X squeezes in between A and B without moving any node. The traversal gives A, X, B, C.
True or false?
- To compare the text of two
Strings inside the nodes you must useequals, not==. - Inserting at the front of a linked list forces you to move every node one place.
- If the list stores
firstandlast, inserting at the end doesn’t need to traverse it. - In the linked-structures exam you can store the data in an
ArrayListif it’s more convenient. n.next.valuethrowsNullPointerExceptionifn.nextisnull.
Solution
- T — To compare the text of two
Strings inside the nodes you must useequals, not==. - F — Inserting at the front of a linked list forces you to move every node one place.
- T — If the list stores
firstandlast, inserting at the end doesn’t need to traverse it. - F — In the linked-structures exam you can store the data in an
ArrayListif it’s more convenient. - T —
n.next.valuethrowsNullPointerExceptionifn.nextisnull.
== asks whether they’re the same object; two equal texts can be different objects (for example, if they come from a file). Inserting at the front only changes two references. With last you hook on at the back without traversing. And in the exam collections and arrays are forbidden for storing the data.
Put the statements in order to insert a node at the front of the list:
newNode.next = first;first = newNode;size++;Node newNode = new Node(data);
Solution
Correct order:
Node newNode = new Node(data);newNode.next = first;first = newNode;size++;
First the node is created, then it’s linked to the current head and only then is first moved. If you do first = newNode before newNode.next = first, the new node points to itself and the rest of the list is lost.
This method is meant to count the nodes. What does it return with the list A -> B -> C?
public int count() {
int total = 0;
Node n = first;
while (n.next != null) {
total++;
n = n.next;
}
return total;
}- 2
- 3
- 4
- It throws NullPointerException
Solution
Answer: 2
while (n.next != null) is for stopping AT the last node (for example, to hook something on), not for visiting them all. Here it counts A and B and stops with n on C. With the empty list, n.next would also throw NullPointerException.
When you delete the first node of a list with first, last and size, what do you have to do?
first = first.next;- Subtract 1 from
size - If the list ends up empty, set
last = null - Traverse the list to move the other nodes
- Always set
last = null
Solution
Answer: first = first.next; · Subtract 1 from size · If the list ends up empty, set last = null
It’s enough for the head to move to the second node. The case people forget most is the single node: if last isn’t set to null, the empty list still “remembers” the deleted node.
Match each operation with its cost in a linked list that only stores first:
| Insert at the front | |
| Insert at the end | |
| Get the element at position i | |
| Check whether it’s empty |
Definitions:
- O(n²): two nested loops
- O(log n): binary search
- O(n): you have to reach the last node
- O(1): two references change
- O(1): see whether first is null
- O(n): you have to make i hops
Solution
- Insert at the front → O(1): two references change
- Insert at the end → O(n): you have to reach the last node
- Get the element at position i → O(n): you have to make i hops
- Check whether it’s empty → O(1): see whether first is null
In a linked list what’s cheap is whatever happens at the head. Anything that depends on a position means traversing from first. Binary search doesn’t work: you can’t jump straight to the middle.
queue holds Laura -> Pablo -> Marta. What does it print? (ContainerBase‘s toString() draws ➡A->B->null; write the answer without the leading arrow)
Node previous = null;
Node current = first;
while (current != null) {
Node nextNode = current.next;
current.next = previous;
previous = current;
current = nextNode;
}
first = previous;
System.out.println(this);Solution
Answer: Marta->Pablo->Laura->null
It’s in-place reversal: on each lap the arrow of current is turned to point at previous. At the end previous is the old last node (Marta) and becomes the head.
In ContainerBase, first is private. What happens with a subclass that does first = newNode;?
- It doesn’t compile:
first has private access in ContainerBase - It compiles, but
firststays null - It works just like with protected
- It creates a new first attribute in the subclass
Solution
Answer: It doesn’t compile: first has private access in ContainerBase
That’s why the exam’s ContainerBase declares first as protected: it’s the attribute you’ll handle from your child class.
To delete a node in the middle of the list, which reference do you need?
- The previous node’s, to do
previous.next = current.next - Only the one of the node being deleted
- The one of
firstand nothing else - None: you set
current = null
Solution
Answer: The previous node’s, to do previous.next = current.next
Deleting means skipping a node: the one that has to change its arrow is the previous one. Setting a variable to null doesn’t remove anything from the chain.
What does this fragment print?
String a = "Pablo";
String b = new String("Pablo");
System.out.println((a == b) + " " + a.equals(b));Solution
Answer: false true
new String creates another object with the same text: == compares references (they’re different) and equals compares the content (it’s the same). In a remove written with == that Pablo would never be found.
Watch it step by step in Python Tutor
Go to Python Tutor, choose Java and paste this version of exercise 3 with the five-person queue (in Python Tutor everything goes in one class, so Node goes inside Tutor):
public class Tutor {
static class Node {
String value;
Node next;
Node(String value) {
this.value = value;
}
}
public static void main(String[] args) {
Node first = new Node("Laura");
first.next = new Node("Pablo");
first.next.next = new Node("Marta");
first.next.next.next = new Node("Iván");
first.next.next.next.next = new Node("Nora");
Node slow = first;
Node fast = first.next;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
Node second = slow.next; // Iván
slow.next = null; // the first half ends at Marta
System.out.println(first.value + " ... " + slow.value + " | " + second.value + " ... " + second.next.value);
}
}
Output:
Laura ... Marta | Iván ... Nora
What to watch step by step:
- After the five
nextlines, the chain goes from Laura to Nora and Nora’s arrow isnull. - Before the loop,
slowpoints at Laura andfastat Pablo, one node ahead. - On each lap,
slow‘s arrow moves one node andfast‘s, two. Count the hops. - When
fastbecomesnull,slowis on Marta, the third one. - After
slow.next = null, Python Tutor draws two separate chains: Laura -> Pablo -> Marta and, apart, Iván -> Nora, which stays alive becausesecondpoints at it.
Cheat sheet: Java LinkedList
The essentials in 30 seconds: a Node stores value and next; the list only stores first (protected in ContainerBase) and your class inherits with extends. To insert, link first and move afterwards. To delete, stop at the previous one. Traverse with for (Node n = first; n != null; n = n.next); to stop at the last one, while (n.next != null). Compare text with equals. Validate before moving on and always test the empty list and the one-node list.
| I want to… | Code | Cost |
|---|---|---|
| Insert at the front | newNode.next = first; first = newNode; | O(1) |
Insert at the back (only first) | while (current.next != null) current = current.next; and current.next = newNode; | O(n) |
Insert at the back (with last) | last.next = newNode; last = newNode; | O(1) |
| Traverse | for (Node n = first; n != null; n = n.next) | O(n) |
| Delete the head | first = first.next; (+ last = null if it empties) | O(1) |
| Delete another | previous.next = previous.next.next; | O(n) |
| Position N | Validate and for (int i = 0; i < pos; i++) current = current.next; | O(n) |
| Find a text | n.value.equals(member) | O(n) |
| Reverse in place | nextNode = current.next; current.next = previous; and move on | O(n), memory O(1) |
| Split in half | slow 1 hop, fast 2, cut at slow | One pass |
Exercise templates:
| Exercise | Pattern | Result with the sample queue |
|---|---|---|
get(2) | Validate + pos hops | Marta |
indexOf("Pablo") | Counter + equals + return inside | 1 |
removeDuplicates() | Two loops + previous stays when deleting | Laura -> Pablo -> Marta -> Iván |
splitInHalf() | Slow and fast + cut | Laura -> Pablo -> Marta / Iván -> Nora |
| Symptom | Likely cause |
|---|---|
NullPointerException: Cannot read field "next" because "n" is null | You moved one node too far or didn’t validate the empty list |
first has private access in ContainerBase | first is private; in the exam it’s protected |
| It doesn’t find a text that is there | You compare with == instead of equals |
| Repeats in a row stay | previous moves on even when you delete |
| The list is left with one node | You turned or changed an arrow before saving the next one |
| The empty queue has a “last” | last = null is missing when serving the last person |
| A counter falls one short | while (n.next != null) instead of while (n != null) |
Before handing in, ask yourself:
- Does it work with the empty list and with a single node?
- Is there any
.valueor.nexton something that could benull? - Have I linked before moving in every reference change?
- Do
size(andlast, if I have it) still tell the truth after my method? - Have I compared the text with
equals?
Did this article help you? Here is how you can support SergioLearns →




What is an operating system — theory with your laptop on a study afternoon, a write() call and a CPU that never rests (and the three programs that weren’t running at the same time)