Before writing any code:
Discuss:
What about sort?
Let’s implement a list interface.
Let’s start with the basic methods:
append(T value) – adds an item to the end of the listlength() – returns how many items are in the listget(int index) – returns the item at indexset(int index, T value) – changes the value of the item at indexHere’s our initial generic interface:
List interface, we will start by implementing our List with an arrayArrayList and we will use the implements keyword:Note: Java already has java.util.List and java.util.ArrayList – don’t import them, or they will clash with ours!
We need to keep track of three things:
data – the array that stores the itemscapacity – how many items the array can holdlength – how many items the list actually holdsWe will create a generic ArrayList that implements List
public class ArrayList<T> implements List<T> {
private T[] data;
private int capacity = 10;
private int length = 0;
@SuppressWarnings("unchecked")
public ArrayList() {
data = (T[]) new Object[capacity];
}
@Override
public int length() {
return length;
}
@Override
public void append(T value) {
// TODO: implement
}
@Override
public T get(int index) {
// TODO: implement
return null;
}
@Override
public void set(int index, T value) {
// TODO: implement
}
@Override
public String toString() {
// TODO: implement
return "";
}
}Submit your List.java and ArrayList.java files to gradescope.
toString()What happens if we run System.out.println(fruitList); without a toString() method?
ArrayList@1b6d3586
Write a toString() that returns the items separated by spaces – only the first length items!
What happens when length is the same as capacity?
Submit your List.java and ArrayList.java files to gradescope making sure your array implementation can do multiple append calls (your implementation deals with array expansion) (don’t forget the toString() implementation as well)
The expandArray process is \(O(n)\) (gotta copy the array!) BUT it doesn’t run every time, so how do we handle the effect on append?
What we often care about is not the runtime of myList.append(4); but instead:
double array vs. increase array size by 10 plots
One way to think about it:
What if we grew the array by 10 each time instead of doubling? (see the plot!)
Let’s add prepend, insert, append to our interface.
When implementing ArrayList insert and prepend, we need to shift values in our array to the right
insert(1, X) on a list with 4 items:
index: 0 1 2 3 4
before: [ A | B | C | D | ]
after shiftRight(1): [ A | B | B | C | D ] copy backwards
data[1] = X: [ A | X | B | C | D ]
Why do we have to copy starting from the end? What happens if we start from index 1?
shiftRightpublic void insert(int index, T value) {
if (index < 0 || index > length) throw new IndexOutOfBoundsException("index out of range");
shiftRight(index);
data[index] = value;
}Note: index == length is allowed – that is the same as append
Submit your List.java and ArrayList.java files to gradescope making sure your array implementation contains methods prepend and insert.
Add T pop(int index) to our List interface – removes the item at index and returns it
We need to implement shiftLeft in ArrayList
index: 0 1 2 3 4
before (pop(1)): [ A | X | B | C | D ] save X to return
after shiftLeft(1): [ A | B | C | D | D ] copy forwards
clear last slot: [ A | B | C | D | ] length--
public class UseList {
public static void main(String[] args) {
List<String> fruitList = new ArrayList<>();
System.out.println(fruitList.length()); // 0
System.out.println(fruitList);
fruitList.append("apple");
fruitList.append("pear");
fruitList.append("banana");
fruitList.prepend("kiwi");
fruitList.prepend("tomato");
fruitList.insert(2, "pineapple");
System.out.println(fruitList); // tomato kiwi pineapple apple pear banana
System.out.println(fruitList.pop(1)); // kiwi
System.out.println(fruitList); // tomato pineapple apple pear banana
}
}Also test the edge cases: an empty list, a full array (append 10+ items, then insert), the first index, and the last index.
Submit your List.java and ArrayList.java files to gradescope making sure your array implementation contains the pop method.
ArrayList implementationpublic class ArrayList<T> implements List<T> {
private T[] data;
private int capacity = 10;
private int length = 0;
@SuppressWarnings("unchecked")
public ArrayList() {
data = (T[]) new Object[capacity];
}
@Override
public int length() {
return length;
}
@Override
public void append(T value) {
if (length == capacity) {
expandArray();
}
data[length] = value;
length++;
}
@Override
public void insert(int index, T value) {
if (index < 0 || index > length) throw new IndexOutOfBoundsException("index out of range");
shiftRight(index);
data[index] = value;
}
@Override
public void prepend(T value) {
shiftRight(0); // need to shift all the other values
data[0] = value;
}
@Override
public T pop(int index) {
if (index < 0 || index >= length) throw new IndexOutOfBoundsException("index out of range");
T out = data[index];
shiftLeft(index);
return out;
}
private void shiftRight(int startIndex) {
if (length == capacity) expandArray(); // make room BEFORE growing
length++;
for (int i = length-1; i > startIndex; i--) data[i] = data[i-1];
}
private void shiftLeft(int startIndex) {
for (int i = startIndex; i < length-1; i++) data[i] = data[i+1];
data[length-1] = null; // clear the old last slot
length--;
}
@SuppressWarnings("unchecked")
private void expandArray() {
capacity *= 2;
T[] newData = (T[]) new Object[capacity];
for (int i = 0; i < length; i++) newData[i] = data[i];
data = newData;
}
@Override
public T get(int index) {
if (index < 0 || index >= length) throw new IndexOutOfBoundsException("index out of range");
return data[index];
}
@Override
public void set(int index, T value) {
if (index < 0 || index >= length) throw new IndexOutOfBoundsException("index out of range");
data[index] = value;
}
@Override
public String toString() {
String out = "";
for (int i = 0; i < length; i++) out += data[i] + " ";
return out.trim();
}
}| Front of List | Middle of List | End of List | |
|---|---|---|---|
| get | O(1) | O(1) | O(1) |
| set | O(1) | O(1) | O(1) |
| insert | O(n) | O(n) | O(1)* |
| pop | O(n) | O(n) | O(1) |
* amortized – an occasional insert triggers an \(O(n)\) expansion
append = insert at the end → amortized \(O(1)\)prepend = insert at the front → \(O(n)\) (every item shifts)