ExerciseChallenge
Challenge
Make LinkedList actually win
20 minintermediate2–8 yrs
Edge cases. You have to reason, and two valid fixes differ.
What this teaches
- LinkedList's O(1) splice is only reachable when you already hold the node
- ListIterator.add/remove is how you hold it; add(i, e) is not
- The same algorithm can be O(n) or O(n^2) depending on how you reach a position
- Winning here still does not make LinkedList the right default
Starter
Starter.java
import java.util.*;
/**
* Challenge: LinkedList's O(1) insertion is real. Find the access pattern where
* you can actually use it.
*
* Below, both lists do the same job: insert a marker after every tenth element.
* The LinkedList version loses badly, because index-based insertion walks the
* chain every single time.
*
* Your job: rewrite the LinkedList version so it wins. The input size must not
* change.
*/
public class Starter {
static final int N = 40_000;
static long ms(Runnable r) {
long start = System.nanoTime();
r.run();
return (System.nanoTime() - start) / 1_000_000;
}
static List<Integer> filled(List<Integer> list) {
for (int i = 0; i < N; i++) list.add(i);
return list;
}
/** Index-based: every add(i, e) walks to i first. */
static void insertMarkersByIndex(List<Integer> list) {
for (int i = list.size() - 1; i >= 0; i -= 10) {
list.add(i, -1);
}
}
// TODO 1: write insertMarkersByIterator(List<Integer>) doing the SAME job
// using a ListIterator: walk the list once, and call it.add(-1) at every
// tenth position. No index arithmetic, no list.get(), no list.add(i, e).
public static void main(String[] args) {
List<Integer> al = filled(new ArrayList<>());
List<Integer> ll = filled(new LinkedList<>());
long alIndex = ms(() -> insertMarkersByIndex(al));
long llIndex = ms(() -> insertMarkersByIndex(ll));
System.out.println("by index ArrayList " + alIndex + "ms, LinkedList " + llIndex + "ms");
System.out.println("sizes match: " + (al.size() == ll.size()));
// TODO 2: time your iterator version on a fresh LinkedList and print it
// next to the numbers above. Is it faster than llIndex? Than alIndex?
// TODO 3: run your iterator version on a fresh ArrayList too. ArrayList
// also has a ListIterator. Does it help there? Explain why or why not
// in a comment — the answer is about what add() has to do, not about
// how you reached the position.
// TODO 4: one comment, one sentence: the access pattern where you would
// genuinely choose LinkedList in production code.
}
}Run it locally:
cd exercises/java/collections/arraylist-vs-linkedlist/02-challenge
javac Starter.java -d /tmp/out && java -cp /tmp/out StarterHints
Hint 1
add(i, e) calls an internal node(i) that walks from the nearer end. That walk is the cost, and it happens on every call.
Hint 2
ListIterator keeps a cursor. Advancing it once per element means you walk the list once in total, not once per insertion.
Hint 3
Write the ArrayList version of your winning approach too. Is LinkedList still ahead? By how much?
Done when
- A LinkedList operation that beats the equivalent ArrayList operation, timed
- The improvement comes from ListIterator, not from a smaller input
- A comment stating the one access pattern where LinkedList is the right choice
← Back to When would you use LinkedList instead of ArrayList?