Lecture 06 Arrays and Lists
A Last Look at Linked Lists
Doubly Linked Lists
Behold. The state of the art as we arrived at in previous lecture. Through various improvements, we made all of the following operations fast:
-
,addFirst
addLast
-
,getFirst
getLast
-
,removeFirst
removeLast
Arbitrary Retrieval
Suppose we added
get(int i)
, which returns the ith item from the list.
Why would get be slow for long lists compared to getLast()? For what inputs?
- Have to scan to desired position. Slow for any i not near the sentinel node.
- How do we fix this?
- For now: We’ll take a different tack: Using an array instead (no links!).
Naive Array Lists
Random Access in Arrays
Retrieval from any position of an array is very fast.
- Independent of array size.
- Ultra fast random access results from the fact that memory boxes are the same size (in bits), we won’t talk about this in detail in this course about Algorithm and Data Structure.
Our Goal: AList.java
AList.java
Want to figure out how to build an array version of a list:
- In lecture we’ll only do back operations. Front operations are left for your practice.
Naive AList Code
/** Array based list. */
public class Alist {
private int[] items;
private int size;
/** Create an empty list. */
public AList() {
items = new int[100];
size = 0;
}
/** Insert X into the back of the list. */
public void addLast(int x) {
items[size] = x;
size += 1;
}
/** Returns the item from the back of the lists. */
public int getLast() {
return items[size - 1];
}
/** Gets the ith item in the list (0 is the front). */
public int get(int i) {
return items[i];
}
/** Returns the size of the list. */
public int size() {
return size;
}
/** Deletes item from back of the lisk and
* returns deleted item. */
public int removeLast() {
int x = getLast();
//items[size - 1] = 0;
/* Setting deleted item to zero is not necessary to
* preserve invariants, and thus not necessary for
* correctness. */
size -= 1;
return x;
}
}
AList Invariants:
- The position of the next item to be inserted is always size.
- size is always the number of items in the AList.
- The last item in the list is always in position size - 1.
Invariants: things that must be true.
Resizing Arrays
Array Resizing
When the array gets too full (
size == items.length
), e.g. addLast(11), just make a new array:
int[] a = new int[size+1];
System.arraycopy(...)
a[size] = 11;
items = a; size +=1;
// We call this process "resizing"
Resizing Array Code
public void addLast(int x) {
if (size == items.length) {
int[] a = new int[size + 1];
System.arraycopy(items, 0, a, 0, size);
items = a;
}
items[size] = x;
size += 1;
}
// Works.
private void resize(int capacity) {
int[] a = new int[capacity];
System.arraycopy(items, 0, a, 0, size);
items = a;
}
public void addLast(int x) {
if (size == items.length) {
resize(size + 1);
}
items[size] = x;
size += 1;
}
// Much Better !
Resizing Slowness
Inserting 100,000 items requires roughly 5,000,000,000 new containers.
- Computers operate at the speed of GHz (due billions of things per second).
- No huge surprise that 100,000 items took seconds.
How do we fix the Resizing Performance Bug?
(Probably) Surprising Fact
Geometric resizing is much faster: Just how much better will have to wait.
public void addLast(int x) {
if (size == items.length) {
resize(size + RFACTOR);
}
items[size] = x;
size += 1;
}
// Unsuably Bad!
public void addLast(int x) {
if (size == items.length) {
resize(size * RFACTOR);
}
items[size] = x;
size += 1;
}
// Great performance!
/* This is how the Python list is implemented.
Memory Efficiency
An AList should not only be efficient in time, but also efficient in space.
- Define the “usage ratio” R = size / items.length;
- Typical solution: Half array size when R < 0.25.
- More details later.
Later we will consider tradeoffs between time and space efficiency for a variety of algorithms and data structures.
Generic ALists
Similar to generic SLists.
public class AList<Glorp> {
private Glorp[] items;
private int size;
public AList() {
items = (Glorp []) new Object[8];
size = 0;
}
private void resize(int cap) {
Glorp[] a = (Glorp []) new Object[cap];
System.arraycopy(items, 0, a, 0, size);
items = a;
}
public Glorp get(int i) {
return items[i];
}
...
When creating an array of references to Glorps:
-
(Glorp []) new Object[cap];
- Causes a compiler warning, which you should ignore.
Why not just
new Glorp[cap]
;
- Will cause a “generic array creation” error.
Nulling Out Deleted Items
Unlike integer based ALists, we actually want to null out deleted items.
- Java only destroys unwanted objects when the last reference has been lost.
- Keeping references to unneeded objects is sometimes called loitering.
- Save memory. Don’t loiter.
public Glorp deleteBack() {
Glorp returnItem = getBack();
items[size - 1] = null;
size -= 1;
return returnItem;
}
Loitering Example
Changing
size
to
size - 1
yields a correct AList.
- But memory is wasted storing a reference to the unwanted object.
By nulling out
items[size]
, Java is free to destroy the unneeded object from memory, which could be potentially megabytes in size.
Obscurantism in Java
We talk of “layers of abstraction” often in computer science.
- Related concept: obscurantism. The user of a class does not and should not know how it works.
- The Java language allows you to enforce this with ideas like
!private
- The Java language allows you to enforce this with ideas like
- A good programmer obscures details from themselves, even within a class.
- Example:
andaddFirst
should be written totally independently. You should not be thinking about the details of one method while writing the other. Simply trust that the other works.resize
- Breaking programming tasks down into small pieces (especially functions) helps with this greatly!
- Through judicious use of testing, we can build confidence in these small pieces, as we’ll see later.
- Example: