Solution: Access and Search
The sentinels change where the traversal starts and stops.
For indexed access, index 0 is no longer at head. head is the front sentinel now, so index 0 is head.next. For search, the traversal does not stop at null. It stops when it reaches the back sentinel, tail.
Solution
First, here is node(index). It traverses from whichever end is closer, with no bounds check of its own:
private Node<T> node(int index) {
if (index < size / 2) {
Node<T> current = head.next;
for (int i = 0; i < index; i++) {
current = current.next;
}
return current;
} else {
Node<T> current = tail.prev;
for (int i = size - 1; i > index; i--) {
current = current.prev;
}
return current;
}
}
One thing changed from the chapter version. The starting points moved: a forward traversal starts at head.next and a backward traversal starts at tail.prev, because head and tail are sentinels now and the first and last real nodes are just inside them.
The bounds check itself is unchanged from the chapter: a separate validateIndex helper, called once by every method that takes an index, before it touches node:
private void validateIndex(int index) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException();
}
}
For a front-half index, the traversal starts at the first real node and steps forward. To reach index 2, current begins at head.next:
┌─────┐ ┌───┐ ┌───┐ ┌───┐ ┌─────┐
head ───▶ │ ░░░ │ ◀──▶ │ 3 │ ◀──▶ │ 5 │ ◀──▶ │ 7 │ ◀──▶ │ ░░░ │ ◀─── tail
└─────┘ └───┘ └───┘ └───┘ └─────┘
▲
│
current
front back
sentinel sentinel
Step forward:
┌─────┐ ┌───┐ ┌───┐ ┌───┐ ┌─────┐
head ───▶ │ ░░░ │ ◀──▶ │ 3 │ ◀──▶ │ 5 │ ◀──▶ │ 7 │ ◀──▶ │ ░░░ │ ◀─── tail
└─────┘ └───┘ └───┘ └───┘ └─────┘
▲
│
current
front back
sentinel sentinel
And stop at the target:
┌─────┐ ┌───┐ ┌───┐ ┌───┐ ┌─────┐
head ───▶ │ ░░░ │ ◀──▶ │ 3 │ ◀──▶ │ 5 │ ◀──▶ │ 7 │ ◀──▶ │ ░░░ │ ◀─── tail
└─────┘ └───┘ └───┘ └───┘ └─────┘
▲
│
current (target)
front back
sentinel sentinel
For a back-half index, the closer end is the back, so the traversal starts at the last real node and steps backward. To reach index 2, current begins at tail.prev:
┌─────┐ ┌───┐ ┌───┐ ┌───┐ ┌─────┐
head ───▶ │ ░░░ │ ◀──▶ │ 3 │ ◀──▶ │ 5 │ ◀──▶ │ 7 │ ◀──▶ │ ░░░ │ ◀─── tail
└─────┘ └───┘ └───┘ └───┘ └─────┘
▲
│
current
front back
sentinel sentinel
Step backward to the target:
┌─────┐ ┌───┐ ┌───┐ ┌───┐ ┌─────┐
head ───▶ │ ░░░ │ ◀──▶ │ 3 │ ◀──▶ │ 5 │ ◀──▶ │ 7 │ ◀──▶ │ ░░░ │ ◀─── tail
└─────┘ └───┘ └───┘ └───┘ └─────┘
▲
│
current (target)
front back
sentinel sentinel
With the bounds check factored out, get and set each call validateIndex first:
public T get(int index) {
validateIndex(index);
return node(index).value;
}
public void set(int index, T value) {
validateIndex(index);
node(index).value = value;
}
For search, the traversal starts at the first real node and continues until it reaches the back sentinel:
public int indexOf(T value) {
Node<T> current = head.next;
int index = 0;
while (current != tail) {
if (Objects.equals(current.value, value)) {
return index;
}
current = current.next;
index++;
}
return -1;
}
public boolean contains(T value) {
return indexOf(value) != -1;
}
Looking for 7, current begins at the first real node:
┌─────┐ ┌───┐ ┌───┐ ┌───┐ ┌─────┐
head ───▶ │ ░░░ │ ◀──▶ │ 3 │ ◀──▶ │ 5 │ ◀──▶ │ 7 │ ◀──▶ │ ░░░ │ ◀─── tail
└─────┘ └───┘ └───┘ └───┘ └─────┘
▲
│
current
front back
sentinel sentinel
Advance:
┌─────┐ ┌───┐ ┌───┐ ┌───┐ ┌─────┐
head ───▶ │ ░░░ │ ◀──▶ │ 3 │ ◀──▶ │ 5 │ ◀──▶ │ 7 │ ◀──▶ │ ░░░ │ ◀─── tail
└─────┘ └───┘ └───┘ └───┘ └─────┘
▲
│
current
front back
sentinel sentinel
Stop on the match:
┌─────┐ ┌───┐ ┌───┐ ┌───┐ ┌─────┐
head ───▶ │ ░░░ │ ◀──▶ │ 3 │ ◀──▶ │ 5 │ ◀──▶ │ 7 │ ◀──▶ │ ░░░ │ ◀─── tail
└─────┘ └───┘ └───┘ └───┘ └─────┘
▲
│
current (match)
front back
sentinel sentinel
The file needs:
import java.util.Objects;
The sentinels stay inside the list. get(0) returns the first real value, not the front sentinel’s null.