03 · OOP for DSA & Design Rounds
#707 Design Linked List
the problem
In plain words
Write your own MyLinkedList with 0-indexed operations. get(index) returns the value, or -1 if the index is invalid. addAtHead(val) and addAtTail(val) insert at the ends. addAtIndex(index, val) inserts before position index: index equal to the length appends, and a larger index does nothing. deleteAtIndex(index) removes the node if the index is valid. Don't use the built-in LinkedList.
in addAtHead(1), addAtTail(3), addAtIndex(1, 2), get(1), deleteAtIndex(1), get(1)
out null, null, null, 2, null, 3
The list goes [1] → [1, 3] → [1, 2, 3]; get(1) is 2. After deleting index 1 the list is [1, 3], so get(1) is 3.
- 0 ≤ index, val ≤ 1000
- At most 2000 calls in total
- Do not use the built-in LinkedList
your turn
Try it before you peek
Read the examples, say the brute force out loud, then write code: here or on LeetCode ↗. Stuck for 20 minutes? Open one hint at a time.
Solve it in the browser
Design: implement every operation. Compiled by the real javac (Java 17) and judged right here — nothing is sent anywhere.
Solutions are hidden so you can try first. Even 20 minutes of real effort makes the answer stick far better than reading it.
Marking the problem solved, or opening every hint, also shows it.
the aha
Key insight
patternIn-place linked-list rewiringClass design: state + operations
where this runsjava.util.LinkedList is a doubly linked list of private static class Node objects whose get(index) walks from the nearer end, like the optimal approach.
Every operation is "walk to the node before position index, then rewire one or two arrows". A dummy node before the head means position 0 also has a node before it, so no special cases remain.
solutions
From brute force to optimal
Let ArrayList do the indexing.
- get: bounds check, then
items.get(index). - addAtIndex / addAtHead / addAtTail:
items.add(index, val)after checking 0 ≤ index ≤ size. - deleteAtIndex:
items.remove(index); theintoverload removes by index.
Inserting or removing shifts every later element by one.
The backing array plus a boxed Integer per value.
// On LeetCode, name this class MyLinkedList.class MyLinkedListArray { private final List<Integer> items = new ArrayList<>(); public int get(int index) { return index >= 0 && index < items.size() ? items.get(index) : -1; } public void addAtHead(int val) { items.add(0, val); // shifts every element right: O(n) } public void addAtTail(int val) { items.add(val); // amortized O(1) } public void addAtIndex(int index, int val) { if (index >= 0 && index <= items.size()) items.add(index, val); } public void deleteAtIndex(int index) { if (index >= 0 && index < items.size()) items.remove(index); // remove(int index) }}A dummy node before the head and a size field; walk prev to the node before index.
- Fields:
dummyandsize. - before(index): start at dummy and step index times.
- addAtIndex: check 0 ≤ index ≤ size;
node.next = prev.next; prev.next = node; size++. - deleteAtIndex: check 0 ≤ index < size;
prev.next = prev.next.next; size--.
Each call walks from the front to the position.
One Node per value.
// On LeetCode, name this class MyLinkedList.class MyLinkedListSingly { private static class Node { int val; Node next; Node(int val) { this.val = val; } } private final Node dummy = new Node(0); // sentinel before the real head private int size = 0; private Node before(int index) { // the node just BEFORE position index Node prev = dummy; for (int i = 0; i < index; i++) prev = prev.next; return prev; } public int get(int index) { if (index < 0 || index >= size) return -1; return before(index).next.val; } public void addAtHead(int val) { addAtIndex(0, val); } public void addAtTail(int val) { addAtIndex(size, val); } public void addAtIndex(int index, int val) { if (index < 0 || index > size) return; // index == size means append Node prev = before(index); Node node = new Node(val); node.next = prev.next; // link forward first... prev.next = node; // ...then hook it in size++; } public void deleteAtIndex(int index) { if (index < 0 || index >= size) return; Node prev = before(index); prev.next = prev.next.next; // the skipped node becomes garbage size--; }}Dry run
head and tail sentinels; walk from whichever end is closer.
- Sentinels
head ⇄ tailand a size field. - nodeAt(index): walk forward from head if index < size / 2, else backward from tail.
- insertBefore(at, val): link the new node between
at.prevandat. - delete:
node.prev.next = node.next; node.next.prev = node.prev.
At most half the list is walked, and the sentinels make both ends instant.
Two references per node instead of one.
class MyLinkedList { private static class Node { int val; Node prev; Node next; Node(int val) { this.val = val; } } private final Node head = new Node(0); // sentinel before the first node private final Node tail = new Node(0); // sentinel after the last node private int size = 0; public MyLinkedList() { head.next = tail; tail.prev = head; } private Node nodeAt(int index) { // 0 <= index <= size; index == size gives tail if (index < size / 2) { // closer to the front: walk forward Node cur = head.next; for (int i = 0; i < index; i++) cur = cur.next; return cur; } Node cur = tail; // closer to the back: walk backward for (int i = size; i > index; i--) cur = cur.prev; return cur; } public int get(int index) { if (index < 0 || index >= size) return -1; return nodeAt(index).val; } public void addAtHead(int val) { insertBefore(head.next, val); } public void addAtTail(int val) { insertBefore(tail, val); // O(1): no walk at all } public void addAtIndex(int index, int val) { if (index < 0 || index > size) return; insertBefore(nodeAt(index), val); } public void deleteAtIndex(int index) { if (index < 0 || index >= size) return; Node node = nodeAt(index); node.prev.next = node.next; // both neighbours skip the node node.next.prev = node.prev; size--; } private void insertBefore(Node at, int val) { Node node = new Node(val); node.prev = at.prev; node.next = at; at.prev.next = node; at.prev = node; size++; }}in the room
How to say it in the interview
talk track
- Write the Node class and the fields (
dummy,size) first, and say why the dummy node exists. - Write one helper that returns the node before index; every method reuses it.
- Insert in the safe order: set the new node's next before changing prev.next.
- Test aloud: index 0, index == size (append), index > size (ignored), deleting the last node.
traps
- Setting
prev.next = nodebeforenode.next = prev.next: the rest of the list is lost. - Forgetting
size++orsize--, so later bounds checks are wrong. - Using
index <= sizein get or delete (it must be< size), orindex < sizein addAtIndex (it must allow== size).
what they ask next
Follow-ups
Why keep a size field instead of counting nodes?
Counting is O(n) per call. A size field makes every bounds check O(1), and the doubly linked version needs it to pick the nearer end.
How does java.util.LinkedList compare?
It is the doubly linked version with first and last fields instead of sentinels, and its internal node(index) walks from the nearer end, just like the optimal approach.
before you submit
Edge cases to test
- get or deleteAtIndex on an empty list.
- addAtIndex with index == size (append) and with index > size (ignored).
- Deleting the head and deleting the tail.
- Deleting the only node, then adding again.
practise next
Similar problems
- #206Reverse Linked ListEasyin Linked Lists
- #19Remove Nth Node From End of ListMediumin Linked Lists
- #146LRU CacheMediumin Linked Lists
- #622Design Circular QueueMediumin Queues