ArrayLists & Node Lists

Array List ADT, Dynamic Resizing, Position-Based Lists, and Iterators

CS205 Data Structures

Arrow keys to navigate

The List ADT

An abstract, ordered collection of elements

A List stores elements in a linear sequence. Each element has an index from 0 to n-1.

Core Operations

OperationDescription
get(i)Return element at index i
set(i, e)Replace at index i
add(i, e)Insert at i, shift right
remove(i)Remove at i, shift left
size()Number of elements

Analogy: Numbered Lineup

People in a numbered line. Insert someone at position 3 → everyone behind scoots back one spot.

Key Idea

The List ADT is abstract — it defines what operations exist, not how they work. Both ArrayList and LinkedList implement the same interface!

Array-Based List (ArrayList)

A dynamic array that grows as needed

An ArrayList stores elements in a contiguous backing array:

  • size — elements actually stored
  • capacity — total slots in backing array

Key Idea

size ≤ capacity always. When size == capacity, the array must be resized before adding more.

Index Bounds

Valid indices: 0 to size-1. Accessing beyond size throws IndexOutOfBoundsException even if the backing array has unused slots!

Interactive ArrayList

Get and Set — O(1) Random Access

The superpower of array-based lists

Direct Index Access

Go directly to arr[i] — no searching needed!

Code

// O(1) — constant time
public E get(int i) {
if (i < 0 || i >= size)
throw new IndexOutOfBoundsException();
return data[i];
}

Analogy: Hotel Rooms

Go directly to Room 302 — no checking rooms 1, 2, 3... Address arithmetic computes the memory location instantly.

Key Idea

O(1) random access is the main reason to use ArrayList. Read-heavy workloads love arrays.

Add at Index — Shifting Right

Making room costs O(n) in the worst case

Code

public void add(int i, E element) {
if (size == data.length)
resize(2 * data.length);
// shift right from back
for (k = size-1; k >= i; k--)
data[k+1] = data[k];
data[i] = element;
size++;
}

O(n) Worst Case

add(0, e) shifts ALL n elements. Only add(size, e) avoids shifting.

Animated Shift

Remove at Index — Shifting Left

Filling the gap costs O(n) in the worst case

Code

public E remove(int i) {
E removed = data[i];
for (k = i; k < size-1; k++)
data[k] = data[k+1];
data[size-1] = null; // help GC
size--;
return removed;
}

Memory Leak Prevention

Setting the old last slot to null prevents the array from referencing an object the user thinks was removed.

Animated Shift

Dynamic Resizing — The Doubling Strategy

Why doubling gives amortized O(1) appends

Push items and watch it grow!

Why doubling, not +1?

If grew by +1 each time: total copies = 1+2+3+...+n = O(n²).
Doubling: total copies = 1+2+4+...+n = O(n).
Amortized O(1) per append!

Growth history (capacity starts at 2): Op# Size Cap Resize? Copies --- ---- ---- ------- ------ 1 1 2 — 0 2 2 2 — 0 3 3 4 yes! 2 4 4 4 — 0 5 5 8 yes! 4 6-8 ... 8 — 0 9 9 16 yes! 8

Tradeoff

Doubling wastes up to 50% memory. Growth factor 1.5 wastes less but still O(1) amortized.

Shrinking Strategy

When and how to reclaim unused memory

Bad: Shrink at 1/2 full → Thrashing!

Thrashing

If grow at 2x and shrink at 1/2, alternating add/remove near the boundary triggers resize every single operation. O(n) per op!

Good: Shrink at 1/4 full

// GROW: when size == capacity
newCap = 2 * oldCap;
// SHRINK: when size == capacity/4
newCap = capacity / 2;

Hysteresis

Leaving a buffer zone between grow (full) and shrink (1/4 full) guarantees many operations between resizes, preserving amortized O(1).

After shrink: array is HALF full → need many removes before next shrink → or many adds before next grow → either way, amortized cost stays O(1)

The Position ADT

A stable handle immune to shifting

The Problem with Indices

Indices Are Fragile

Every insert/delete can invalidate stored indices. If you saved "index 2 = C", that reference breaks after any modification before it.

The Solution: Positions

A Position holds onto an element regardless of where it sits. Insertions and deletions don't break other positions.

Analogy: Name Tags vs Seat Numbers

Index = "person in seat #3" — changes when people move.
Position = "Alice's name tag" — always refers to Alice.

Node List (Positional Linked List)

A doubly-linked list where each node IS a position

Structure

Doubly-linked list with sentinel header & trailer nodes that simplify edge cases.

Positional Operations

OperationTime
addBefore(p, e)O(1)
addAfter(p, e)O(1)
remove(p)O(1)
before(p) / after(p)O(1)
first() / last()O(1)

Sentinels

Header and trailer are dummy nodes — never hold real data. Eliminates null-checking for first/last operations.

Interactive Node List

Node List Operations — O(1) Each!

Pointer rewiring instead of element shifting

addAfter(p2, "X")

Code

void addAfter(Position p, E e) {
Node node = (Node) p;
Node nn = new Node(e, node, node.next);
node.next.prev = nn;
node.next = nn;
size++;
}

remove(p2)

Code

E remove(Position p) {
Node n = (Node) p;
n.prev.next = n.next;
n.next.prev = n.prev;
size--;
return n.element;
}

Key Idea

Only 4 pointer assignments per insert, 2 per remove — O(1). No elements shifted. This is the fundamental advantage over ArrayList for positional operations.

Iterators

Traverse a collection without knowing its implementation

Iterator Interface

Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
process(s);
}
// Or with for-each (sugar):
for (String s : list) { process(s); }

Watch the Cursor

Analogy: TV Remote

Press "next channel" without knowing if signals come via cable, satellite, or streaming. The remote = iterator — uniform traversal regardless of source.

ArrayList vs Node List Iterator

ArrayList iterator: cursor = index (int) next() → data[cursor++] Node List iterator: cursor = node reference next() → { val=cursor.elem; cursor=cursor.next; return val; } SAME interface, DIFFERENT internals!

Abstraction

Iterators decouple traversal logic from data structure details. Code using iterators works with ANY Iterable.

ArrayList vs Node List — Comparison

Choose the right implementation for your workload

OperationArrayListNode List (Doubly Linked)
get(i) / index accessO(1)O(n)
set(i, e)O(1)O(n)
add(0, e) / insert frontO(n)O(1)
add(n, e) / appendO(1)*O(1)
Insert/delete at positionO(n)O(1) if you have position
Memory per element1 ref3 refs (elem+prev+next)
Cache performanceExcellentPoor (scattered)

Key Caveat

Node List insert/delete is O(1) only if you already have the position. Finding a position by value or index still costs O(n).

Analogy

ArrayList = numbered bookshelf (instant find, slow insert).
Node List = chain of paperclips (easy to add/remove, slow to find #47).

Java Collections Framework

How ArrayList and LinkedList fit into the bigger picture

Key Idea

Both ArrayList and LinkedList implement List<E> — code written against the interface works with either. Choose the implementation based on your workload.

java.util.ArrayList

  • Backed by Object[] array
  • Default capacity: 10, growth: ~1.5x
  • Implements RandomAccess marker

java.util.LinkedList

  • Doubly-linked with header/trailer sentinels
  • Also implements Deque
  • No RandomAccess — indexing is O(n)

Common Methods (via List interface)

List<String> list = new ArrayList<>();
list.add("hello"); // append
list.add(0, "world"); // insert at 0
list.get(1); // "hello"
list.set(0, "hi"); // replace
list.remove(0); // by index
list.size(); // element count
for (String s : list) { } // iterate

Warning

Java's LinkedList does NOT expose Position objects. Our lecture's positional list ADT is more powerful — positions enable O(1) insert/delete at known locations.

Choosing the Right List

Interactive decision helper — answer the questions

What is your primary access pattern?

Use ArrayList when...

  • Frequent random access by index
  • Mostly appending to the end
  • Memory efficiency matters
  • Good cache locality needed

Use LinkedList / Node List when...

  • Frequent insert/delete at both ends
  • You hold position references
  • Elements are frequently reordered

Rule of Thumb

When in doubt, use ArrayList. Cache performance dominates in practice. Even middle insertions are often faster with ArrayList on modern hardware.

Challenge: Count the Shifts

How many element moves happen in this sequence?

// Start: ArrayList capacity=6, size=4
// data = [A, B, C, D, _, _]
list.add(2, "X"); // insert X at index 2
list.add(0, "Y"); // insert Y at index 0
list.remove(3); // remove at index 3

Count the total number of individual element moves (shifts) across all three operations.

Moves: 0

Common Pitfalls

Mistakes that bite data structures students

1. ConcurrentModification

// WRONG
for (String s : list) {
if (s.equals("bad"))
list.remove(s); // CRASH!
}
// RIGHT
Iterator<String> it =
list.iterator();
while (it.hasNext()) {
if (it.next().equals("bad"))
it.remove(); // safe!
}

2. Off-by-One Index

// size=5, valid: [0..4]
list.get(5); // CRASH!
list.get(-1); // CRASH!
// add() valid: [0..size]

3. Index Shift After Remove

// [A, B, C, D, E]
list.remove(1); // B gone
// Now [A, C, D, E]
list.remove(3); // removes E!
// NOT D — indices shifted
// FIX: remove backwards
list.remove(3); // D gone
list.remove(1); // B gone

4. O(n²) LinkedList Loop

// O(n²)! get(i) walks each time
for (int i=0; i<list.size(); i++)
process(list.get(i));
// O(n) with for-each
for (String s : list)
process(s);

5. size() vs capacity

ArrayList list =
new ArrayList(100);
// capacity=100, size=0!
list.get(0); // CRASH!

#1 Bug: Remove While Iterating

Always use the iterator's remove(), or build a separate "to-remove" list first.

Pro Tip

When removing multiple elements by index, work backwards (high→low) so shifts don't affect remaining indices.

Challenge: Fix the Bug

This addAfter method for a doubly-linked node list has a bug — find and fix it

// Insert element e after position p
public Position<E> addAfter(Position<E> p, E e) {
Node<E> node = validate(p);
Node<E> succ = node.getNext();
Node<E> newest = new Node<>(e, node, succ);
node.setNext(newest);
node.setPrev(newest); // ← bug?
size++;
return newest;
}

Summary & Cheat Sheet

Everything on one slide

OperationArrayListNode List
Index accessO(1)O(n)
Insert at endsO(1)*/O(n)O(1)
Insert at positionO(n)O(1)
Memory/elementLowHigh
Cache localityGreatPoor

Core Takeaways

  • ArrayList: fast random access, slow middle insert/delete, great cache locality
  • Node List: O(1) insert/delete at known positions, O(n) to find a position
  • Positions are stable references; indices are fragile
  • Iterators abstract traversal from structure
  • Doubling + shrink-at-quarter = amortized O(1)
  • When in doubt, use ArrayList

Final Analogy

ArrayList = a numbered bookshelf — finding book #47 is instant, but inserting means sliding everything over. Node List = a chain of paperclips — easy to clip/unclip anywhere, but finding the 47th clip means counting from the start.

Challenge: Amortized Analysis

Starting with capacity 2, how many times does the array resize during 10 addLast operations?

An ArrayList starts with capacity = 2 and size = 0. We perform 10 consecutive addLast() calls. The array doubles when full.

Think About It

After each resize, what is the new capacity? When is the next resize triggered?

size: 0, cap: 2, resizes: 0

Quiz: ArrayList & Node List

Test your understanding — 3 questions

Q1: What is the amortized cost of addLast() on an ArrayList that doubles when full?

Q2: In a Node List with sentinels, what are the first() and last() positions?

Q3: Why does ArrayList have better cache performance than LinkedList?

Quiz: Trace the Operations

What does the ArrayList contain after these operations?

ArrayList list = new ArrayList(); // cap=4
list.add(0, "A");
list.add(1, "B");
list.add(0, "C");
list.add(2, "D");
list.remove(1);
list.add(1, "E");
list.set(0, "F");

Quiz: Iterator Behavior

What happens when we run this code?

List<String> list = new ArrayList<>();
list.add("A"); list.add("B");
list.add("C"); list.add("D");
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
if (s.equals("B") || s.equals("C"))
it.remove();
}
System.out.println(list);