Elements are stored in one contiguous block of memory – to grow, we must allocate a bigger block and copy everything over
| 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
A linked list is a chain of nodes, where each node contains:
No need for contiguous memory (nodes can be scattered anywhere in memory)
To create a linked list:
NodeWrite a generic class in Java called Node that has two instance variables: data (any type), and a reference to another node.
Write the constructor method, and a set and get method for each instance variable.
public class Node<T> {
private T data;
private Node<T> next;
// constructor
public Node(T data) {
this.data = data;
next = null;
}
// setters
public void setNext(Node<T> next) {
this.next = next;
}
public void setData(T data) {
this.data = data;
}
// getters
public Node<T> getNext() {
return next;
}
public T getData() {
return data;
}
}LinkedList classLinkedList classhead – reference to the first Nodetail – reference to the last Nodelength – number of nodes in the listStrictly speaking, only head is required – tail and length save us from walking the whole list
LinkedList set up and constructorWhat methods do you need to implement?
Here’s our generic interface we used to implement our ArrayList:
public interface List<T> {
void append(T value);
void prepend(T value);
void insert(int index, T value);
int length();
T get(int index);
void set(int index, T value);
T pop(int index);
}Implement append, prepend and length in your LinkedList<T> class + a toString() method to test your code. Submit your List.java, Node.java and LinkedList.java files to gradescope.
Linked list bugs almost always hide in the edge cases. For every method, ask:
head == null)head == tail)head?)tail?)length?before: head → [A] → [B] → [C] → null
1. create node: [X]
2. X.next = head: [X] → [A] → [B] → [C] → null
3. head = X: head → [X] → [A] → [B] → [C] → null
What happens if we do step 3 before step 2?
If we only had head, we would need to walk to the end of the list:
Keep a tail instance variable.
toString() methodLet’s create a toString() method to test our list insertion.
Write a toString() method that traverses the entire list
toString() solutionOur UseList test from the ArrayList lecture – change one word:
public class UseList {
public static void main(String[] args) {
List<String> fruitList = new LinkedList<>(); // was: new ArrayList<>()
fruitList.append("apple");
fruitList.append("pear");
fruitList.append("banana");
fruitList.prepend("kiwi");
fruitList.prepend("tomato");
System.out.println(fruitList); // tomato kiwi apple pear banana
}
}This is the payoff of writing the List interface first: code that uses a List doesn’t care how it is implemented.
search() methodWrite a search() method that traverses the entire list trying to find a node that matches the argument value – if found, return true, otherwise return false. Add the boolean search(T data) signature to the List.java interface.
search() is similar to toString() (write a while loop)List must implement it – including ArrayList!Submit your List.java, Node.java and LinkedList.java with the search() method to gradescope.
search() solutionWhat we have implemented is an unsorted linked list – how many comparisons does it require to find a target element?
In the worst case, each node must be examined once, resulting in \(O(n)\) complexity – \(n\) is the number of elements in the list.
Removing a node takes a few more steps:
previous.setNext(cur.getNext())Removing the head – there is no previous node!
before: head → [A] → [B] → [C] → null
pop(0): head = head.getNext()
after: head → [B] → [C] → null
Removing the tail – previous becomes the new tail
before: [A] → [B] → [C] → null tail → [C]
pop(2): previous.setNext(null); tail = previous
after: [A] → [B] → null tail → [B]
pop(int index)Implement pop(int index), get(int index), and set(int index, T value) in your LinkedList.java and submit it alongside with List.java and Node.java to gradescope.
Go back to the edge cases checklist!
public T pop(int index) {
if (index < 0 || index >= length) throw new IndexOutOfBoundsException("index out of range");
int count = 0;
Node<T> previous = null;
Node<T> cur = head;
while (count < index) {
previous = cur;
cur = cur.getNext();
count++;
}
if (previous == null) head = cur.getNext(); // removing the head
else previous.setNext(cur.getNext());
if (cur == tail) tail = previous; // removing the tail
length--;
return cur.getData();
}LinkedList solutionpublic class LinkedList<T> implements List<T> {
private Node<T> head;
private Node<T> tail;
private int length;
public LinkedList() {
head = null;
tail = null;
length = 0;
}
public void append(T value) {
Node<T> nodeToAdd = new Node<>(value);
if (head == null) {
head = nodeToAdd;
tail = nodeToAdd;
} else {
tail.setNext(nodeToAdd);
tail = nodeToAdd;
}
length++;
}
public void prepend(T value) {
Node<T> nodeToAdd = new Node<>(value);
if (head == null) {
head = nodeToAdd;
tail = nodeToAdd;
} else {
nodeToAdd.setNext(head);
head = nodeToAdd;
}
length++;
}
public void insert(int index, T value) {
if (index < 0 || index > length) throw new IndexOutOfBoundsException("index out of range");
if (index == 0) {
prepend(value);
return;
}
if (index == length) {
append(value);
return;
}
Node<T> nodeToAdd = new Node<>(value);
int count = 0;
Node<T> previous = head;
while (count < index - 1) {
previous = previous.getNext();
count++;
}
nodeToAdd.setNext(previous.getNext());
previous.setNext(nodeToAdd);
length++;
}
public int length() {return length; }
public T get(int index) {
if (index < 0 || index >= length) throw new IndexOutOfBoundsException("index out of range");
int count = 0;
Node<T> cur = head;
while (count < index) {
cur = cur.getNext();
count++;
}
return cur.getData();
}
public void set(int index, T value) {
if (index < 0 || index >= length) throw new IndexOutOfBoundsException("index out of range");
int count = 0;
Node<T> cur = head;
while (count < index) {
cur = cur.getNext();
count++;
}
cur.setData(value);
}
public T pop(int index) {
if (index < 0 || index >= length) throw new IndexOutOfBoundsException("index out of range");
int count = 0;
Node<T> previous = null;
Node<T> cur = head;
while (count < index) {
previous = cur;
cur = cur.getNext();
count++;
}
if (previous == null) head = cur.getNext(); // removing the head
else previous.setNext(cur.getNext());
if (cur == tail) tail = previous; // removing the tail
length--;
return cur.getData();
}
public String toString() {
String out = "";
Node<T> cur = head;
while (cur != null) {
out += cur.getData() + " ";
cur = cur.getNext();
}
return out.trim();
}
public boolean search(T data) {
Node<T> currNode = head;
while (currNode != null) {
if (currNode.getData().equals(data)) {
return true;
}
currNode = currNode.getNext();
}
return false;
}
}| Front of List | Middle of List | End of List | |
|---|---|---|---|
| get | O(1) | O(n) | O(n)* |
| set | O(1) | O(n) | O(n)* |
| insert | O(1) | O(n) | O(1) |
| pop | O(1) | O(n) | O(n) |
* could be O(1) if we check for the last index and use tail – our implementation walks the list
Why is pop at the end \(O(n)\) even though we have tail? (Hint: what does tail need to become?)
| ArrayList | LinkedList | |
|---|---|---|
| get/set by index | O(1) | O(n) |
| insert/pop at front | O(n) | O(1) |
| append | O(1) amortized | O(1) (with tail) |
| pop at end | O(1) | O(n) |
| insert/pop in middle | O(n) | O(n) |
| memory | one block, may have unused slots | one node (data + reference) per item |
We just implemented a singly linked list
Doubly linked list – each node also points to the previous node, so pop at the end becomes O(1):
Circular linked list: