← Skill tree CS Skill Tree 0 CSCD211

Why add and remove use different bounds checks

Textbook: Liang

Try This First

You have a linked list containing [10, 20, 30] (size 3). Which of these calls is valid?

Think about it, then reveal the answer.

Reveal

list.add(3, 40) is valid. Inserting at index 3 means appending after the last element, giving [10, 20, 30, 40]. The valid range for add is 0 through size inclusive.

list.remove(3) is not valid. There is no element at index 3 (indices run 0, 1, 2). The call throws IndexOutOfBoundsException. The valid range for remove is 0 through size - 1 inclusive.

What You Need To Walk In With

How It Works

The key insight is what each operation needs at position i:

Here are the two guard clauses for a singly linked list with a head reference and a size field:

// In add(int i, T x)
if (i < 0 || i > size) {          // i == size is VALID for add
    throw new IndexOutOfBoundsException("Index: " + i + ", Size: " + size);
}

// In remove(int i)
if (i < 0 || i >= size) {         // i == size is INVALID for remove
    throw new IndexOutOfBoundsException("Index: " + i + ", Size: " + size);
}

Notice the one character that differs: > in add, >= in remove.

A minimal sketch for add on [a, b, c] at index 3 (size = 3):

Before:  head -> [a] -> [b] -> [c] -> null

add(3, "d"):  traverse to node at index 2 (the last node),
              attach new node after it.

After:   head -> [a] -> [b] -> [c] -> [d] -> null
         size becomes 4.

For remove(3) on the same list, the guard fires immediately because 3 >= 3 (size is 3), and no traversal happens.

Worked Example: Predict, Then Check

A list holds [x, y, z] (size 3). Trace these two calls in order:

list.add(3, "w");   // call A
list.remove(4);     // call B

After call A, what is the list state and size? After call B, what happens?

Reveal

Call A: i = 3, size = 3. The guard checks 3 < 0 || 3 > 3, which is false (3 is not greater than 3). The call is valid. "w" is appended: list is [x, y, z, w], size becomes 4.

Call B: i = 4, size = 4. The guard checks 4 < 0 || 4 >= 4, which is true (4 is equal to 4). IndexOutOfBoundsException is thrown. The list remains [x, y, z, w].

A Common Mistake

When implementing add, developers sometimes copy the guard from remove without changing it:

// Wrong: copied from remove
if (i < 0 || i >= size) {
    throw new IndexOutOfBoundsException(...);
}

On a list of size 5, this rejects i = 5 even though appending to the end is perfectly valid. The method throws where it should insert. The fix is simple: change >= to > so that i == size passes the guard. (Source: Liang 10e Ch 24)

Go Deeper (optional)

Both add and remove at an arbitrary index cost O(n) time because the guard check is O(1) but the traversal to position i is O(n). If you later study a doubly linked list with both a head and a tail reference, the bounds logic stays identical: the index range asymmetry between add and remove is not a property of the node structure, it is a property of what each operation means.

Check Yourself

Close the notes and answer each one from memory, then reveal it. Pulling an idea back from memory is one of the strongest ways to make it stick.

Check your understanding

For a list of size 7, which index range is valid for add(int i, T x)?

Tier 1 · Liang 10e Ch 24

For the same list of size 7, which index range is valid for remove(int i)?

Tier 1 · Liang 10e Ch 24

list = [p, q, r] (size 3). What is the result of list.add(3, “s”)?

Tier 2 · Liang 10e Ch 24

list = [p, q, r] (size 3). What is the result of list.remove(3)?

Tier 2 · Liang 10e Ch 24

A developer writes the add guard as: if (i < 0 || i >= size). What bug does this introduce?

Tier 3 · Liang 10e Ch 24