Lists
The basic List interface supports the following features:
L.get(i): returns the ith element
L.set(i,val): sets ith element to val
L.length(): returns the current length
L.add(val): grows the list by adding one new "slot" at the end.
Internally, space for multiple slots is usually added when necessary.
Vector
Chapter 3 contains Bailey's Vector class, which represents a List of
Objects implemented via an array. The main feature is that the vector can
grow.
We'll start with the Vector interface on page 46.
Bailey's examples:
Vector used for Wordlist
Vector used for L-systems
There is an issue with Vectors of Objects: we don't really want Objects.
Look closely at the code on page 47; it contains a cast
to (String):
targetWord = (String)
list.get(index);
What this does is takes the Object list.get(index), and, because it is
really a string, allows its use as a string at compile-time.
In C# (and now in fact in Java) we will usually use generics,
eg Vector<T>.
In the word-frequency example on page 49 (actually starting on p 48)
there is
wordInfo = (Association) vocab.get(i);
and
vocabWord = (String) wordInfo.getKey();
Adding to the middle of a vector: you need to move from right to left.
See the picture on Bailey p 52.
Vector Growth Demo
Let us start with C#. We will use the command-line version. The
List<T> class is documented at http://msdn.microsoft.com/en-us/library/6sh2ey19%28v=vs.110%29.aspx.
The demo program is listgrowth.cs.
We first create the list.
List<String> s = new List<String>();
We can examine s.Count to get the current length of the list, and s.Capacity
to get the current length of the internal array in object s. Initially, both
are 0.
If we add an item: s.Add("apple"), then s.Count = 1 and s.Capacity = 4.
If we add three more items, both s.Count and s.Capacity are 4.
If we then execute s.Add("banana"), s.Count is now 5 and s.Capacity is now
8.
If we then execute s.Add("cherry"), s.Capacity becomes 16.
Demo 2: the same example, but using the Object Inspector of the BlueJ Java
system. The package is "arraylisttest"
§3.5: analysis of costs of expansion
- The number of moves to
insert at a random place in the middle of a list of length N is, on
average, N/2.
- The number of compares to
search a list of length N for a random element that is in fact present
is, on average, N/2.
- The number of compares to search a list of length N for an element not there is simply N.
These costs are all linear; that
is, proportional to N.
Now suppose we want to insert N items into a list initially of length 0,
perhaps searching the list each time in order to insert in alphabetical
order. Each item's required position is more-or-less random, and so takes on
average size()/2 moves. That is, to insert the 1st element takes 0/2 moves,
the 2nd takes 1/2, the 3rd takes 2/2, the 4th takes 3/2, the 5th takes 4/2,
etc. Adding all these up gives us a total "average" number of moves of
1/2 + 2/2 + 3/2 + 4/2 + 5/2 + ... + (N-1)/2 = 1/2*(1 + 2+
3 + ... + (N-1))
= 1/2*(N(N-1)/2) = N2/4-N/4
Now, for large N this is approximately
N2/4, that is, proportional to N2, or quadratic.
Using the big-O notation, later, this is equivalent to saying the number of
moves is O(N2).
Search and insert costs
These operations both take, on average, currsize/2 steps, or N/2 if we
follow the convention that N represents the current size. For search, we
have to check on average half the list; the steps we are counting are the
comparisons. For insert, the operations are the assignments
things[i+1]=things[i].
Both these can be described as O(N).
Generics
We would really like to be able to declare containers with a fixed type,
where the type is supplied as parameter:
List<String> s = new List<String>();
If we use Bailey's Vector class, we would have pretty much the same
performance, but getting strings out of the Vector would always
require a cast:
String s = (String) vect.get(3);
Stack
A stack is a data structure that supports push() and pop() operations. A
stack looks like a list except there is no direct way to access anything but
the topmost element; you cannot even do that except by also deleting that
element from the stack. The basic operations are
- s.push(A): adds data value A to the stack
- s.pop(): returns the most recently pushed value, and deletes it from
the stack
The Stack class, with specific methods push(), pop(), and isEmpty(), is sort
of the canonical example of an Abstract Data Type, that is, a class where
the focus is on representing a "thing" (as is usually the case). A stack can
be implemented as an array, but
we have no access to the top element except through pop(), and no access to
the middle elements at all. Alternatively, we could change the
implementation to that of, say, "linked list", and the class users would be
unaffected. Note that push() and pop() do not simply perform individual
field updates.
Finding something to do with the
stack is harder; why would you need that very specific last-in, first-out
(LIFO) access? There are lots of examples from system design and
programming-language design, but they tend not to be trivial. One
straightforward example is to confirm that a line consisting of ()[]{} has
all the braces in balance. The algorithm is as follows:
if you encounter an opening symbol, (, [, or {, push it.
if you encounter a closing symbol, ), ], or }, pop what
is on the stack and verify the two correspond.
when you get to the end of the input, verify that the
stack is empty.
Note that generally popping something off an empty stack is an error, so
that you should check with isEmpty().
Implementing a stack
Here's a stack of strings:
class Stack {
private List<string> L;
public void push(string s) {L.Add(s)}
public string pop() {string s = L[L.Count - 1]; L.remove[L.Count-1];
return s;}
public boolean is_empty() {return L.Count == 0;}
}
push(e) corresponds to L.add(e),
is_empty() corresponds to L.size() == 0.
pop() corresponds to {e=L.get(size()-1; L.delete(size()-1); return e;}
Deleting from a list
If all we do is add, then the growth strategy of doubling the internal
space when necessary makes perfect sense.
But what happens if we will regularly grow lists to large size, and then
delete most of the entries? A list grown to have internal
capacity 1024 will retain that forever, even if we shrink down to just a
few elements.
One approach is to re-allocate to a smaller elements[] whenever
L.Count < L.Capacity/2, or something like that.
Morin in §2.6 (p 49) introduces what he calls a RootishArrayStack, which
is an array-based list with an efficient delete
operation. Here are the key facts (big-O notation is officially introduced
in the next section):
- The space used for n elements is n + O(√n)
- For any m add/remove operations, the time spent growing and shrinking
is O(m)
The idea is to keep a list of arrays (an array of pointers to arrays).
These sub-arrays have size 1, 2, 3, 4, etc respectively. For 10 elements,
the RootishArrayStack would have four arrays, and thus a capacity of
1+2+3+4.
If the RootishArrayStack has N elements in n arrays, then N ≃ n²/2; this
follows because 1+2+...+n ≃ n²/2. When the RootishArrayStack needs to
expand, it will add an array of size n+1 to the pool; this is about √(2n).
Thus, growth is "slower" than for C# Lists or Java ArrayLists. However,
when a new allocation is made for growth, the old space is not discarded.
The real advantage of the RootishArrayStack is for deletions. If the list
shrinks so that the last sub-array is now empty, that sub-array and that
sub-array only is discarded. This is a relatively efficient operation.
Big-O notation and Bailey Chapter 5:
Analysis
We will need to be able to talk about runtime costs. To this end, the big-O
and (to a lesser extent) little-o notations are useful. If N is the size of
the data structure, and f(N) is a growth function (like f(N) = log(N) or
f(N) = N or f(N) = N2), then we say that a cost is O(f(N))
provided the number of steps is bounded by k×f(N), for a constant k, as N
grows large. We say that a cost is o(f(N)) if cost(N)/f(N) → 0 as N grows
large. (Alternatively, cost(N) is O(f(N)) if cost(N)/f(N) is bounded by
constant k as N grows large.)
See Figure 5.1 on page 83.
Here are a few examples for an array-based Vector as in Bailey of length N:
operation
|
cost
|
provisos and notes
|
Inserting at the end of a Vector |
O(1)
|
if no expansion is necessary |
Inserting in the middle of a Vector |
O(N) |
N/2 moves on average |
Searching a Vector |
O(N) |
N/2 comparisons on average if found |
Inserting and searching are both linear.
Adding an element to a SetVector takes O(n) comparisons, because we have to
make sure it isn't already there.
Now suppose we want to insert N items into a Vector initially of length 0,
perhaps searching the list each time in order to insert in alphabetical
order. Each item's required position is more-or-less random, and so takes on
average size()/2 moves. That is, to insert the 1st element takes 0/2 moves,
the 2nd takes 1/2, the 3rd takes 2/2, the 4th takes 3/2, the 5th takes 4/2,
etc. Adding all these up gives us a total "average" number of moves of
1/2 + 2/2 + 3/2 + 4/2 + 5/2 + ... + (N-1)/2 = 1/2*(1 + 2+
3 + ... + (N-1))
= 1/2*(N(N-1)/2) = N2/4-N/4
Now, for large N this is approximately
N2/4, that is, O(N2), or quadratic.
Building a list up by inserting each element at the front (or inserting each
element at random) is O(n2). (This is the last example on Bailey
page 87.)
Taking the union or intersection of two sets is O(n2) (Why? Is
there a faster way?)
Finding if a number is prime by checking every k < sqrt(n) is O(n1/2).
How hard is it to find the minimum of an array of length N? O(N)
How hard is it to find the median of an array of length N?
Somewhat surprisingly, this can also be done in O(N) time. See sorting.html#median.
See sorting.html#binsearch for an
analysis of binary search
A function is said to be polynomial
if it is O(nk) for some fixed k; quadratic growth is a special
case.
So far we've been looking mainly at running time. We can also consider
space needs. As an example, see the Table of Factors
example on Bailey page 88. Let us construct a table of all the k<=n and
a list of all the factors (prime or not) of k, and ask how much space
is needed. This turns out to be n log n. The argument here is a bit
mathematical; see Bailey. If the table length is n, then factor f can
appear no more than n/f times (once every fth line).
The running time to construct the table varies with how clever the
algorithm is, it can be
- O(n2) [check all i<k for divisibility]
- O(n3/2) [check all i<sqrt(k)]
- O(n log n) [Sieve of Eratosthenes]
Now suppose we want to search a large string for a specific character.
How long should this take? Bailey has an example on p 90. The answer
depends on whether we're concerned with the worst case or the average case
(we are almost never interested in the best case). If the average case,
then the answer typically depends on the probability distribution of the
data.
Linked Lists
The standard "other" way of implementing a list is to build it out of cells,
where each cell contains a pointer to the next item. See
- Bailey, chapter 9 section 4
- Morin, Chapter 3 (p 63) (we will look at singly-linked lists, or
Morin's SLList)
Linked lists are very efficient in terms of time to allocate and de-allocate
space. Insertion is O(1). Finding an element is O(n), however, even if the
list is sorted; there is no fast binary search.
Each linked-list block contains two pointers, one for data and one for the
link. That's a 2x space overhead. For array-based lists, that would
correspond to having each list have a Capacity that was double its Count.
That's not necessarily bad, but the point is that linked lists have limited
space efficiency. (They may be quite efficient in terms of
allocation time, though; each block allocated amounts to one list cell, and
if many linked lists are growing and shrinking then the allocator can in
effect just trade cells back and forth. With array-based lists, however, if
two lists have just deleted blocks of size 256 and a third list now needs a
block of size 512, the deleted blocks cannot be recycled into the new block
unless they just happen to be adjacent.
Here is some code from the demo file linkedlist.cs
class TLinkedList<T> {
private T data;
private TLinkedList<T> next;
public TLinkedList(T d, TLinkedList<T> n)
{data=d; next=n;}
public T first() {return data;}
public TLinkedList<T> rest() {return next;}
}
The interface is peculiar here; ignore that for now.
A program that uses this might be:
static void
Main(string[] args) {
TLinkedList<string> slist =
new TLinkedList<string>("apple", null);
slist = new
TLinkedList<string>("banana", slist);
slist = new
TLinkedList<string>("cherry", slist);
slist = new
TLinkedList<string>("daikon", slist);
slist = new
TLinkedList<string>("eggplant", slist);
slist = new
TLinkedList<string>("fig", slist);
TLinkedList<string> p = slist;
while (p!= null) {
Console.WriteLine(p.first());
p = p.rest();
}
}
This is not exactly what we want: too many internals are exposed.
A more contained implementation would be as follows:
class TLinkedList<T> {
class Cell<T> {
private T data;
private Cell<T> next;
public Cell(T d, Cell<T> n)
{data=d; next=n;}
public T first() {return data;}
public Cell<T> rest() {return
next;}
}
private Cell<T> head = null;
public void AddToFront(T element) {head = new
Cell<T>(element, head);}
public bool is_empty() {return head == null;}
public T First() {return head.first();}
public void DelFromFront() {head = head.rest();}
}
A slightly more complete Cell subclass is the following (changes in bold)
public class Cell<T> {
private T data_;
private Cell<T> next_;
public Cell(T s, Cell<T> n) {data_ = s; next_ = n;}
public T data() {return data_;}
public Cell<T> next() {return next_;}
public void setData(T s) {data_ = s;}
public void setNext(Cell<T> c) {next_ = c;}
}
Implementing a stack using linkedlist
push(e) corresponds to AddToFront(e), is_empty() corresponds to head ==
null.
pop() corresponds to ...
The code is in linkedstack.cs.
Implementing a set
In section 3.7 Bailey uses vectors/Mylists to implement
an abstract Set. Note the more limited set of operations; there is no get()
and no set().
add() now works very differently: add(E e) is basically if
(!contains(e) ) add(e), where the second add(e) is Vector.add(e).
On the face of it, to form the union of two sets A and B of size N, we need
N2 equality comparisons: each element of A has to be compared
with each element of B to determine if it is already there. This cost is
sometimes said to be O(N2)
if we don't care if it's N2, or N2/2, or 3N2.
Later we'll make this faster with hashing.
Brief summary: choose a relatively large M, maybe quite a bit larger than N.
Define h(obj) = hashcode(obj) % M. Now choose a big array ht (for hash
table) of size M, initially all nulls. For each a in A, do something with
ht[hash(a)] to mark the table. Then, for each b in B, if ht[hash(b)] is
still null, put it in; it's not a duplicate! If ht[hash(b)] is
there already, then we have to check "the long way", but in general we save
a great deal.
Is there an intersect option?
Using List<T> to implement a Matrix class in C#
Suppose we want to construct a two-dimensional object, Matrix. Values in the
Matrix will have type double.
The class should have the following operations:
- Matrix(int height, int width)
- getWidth()
- getHeight()
- get(int row, int col)
- set(int row, int col, double val)
How should we proceed?
Here's a simpler problem: how should we implement a Vector<T>
class, where vector objects have a fixed length, and are initialized to 0?
C# does take care of that latter, but List<T>'s do not automatically
have the right length. Also, ideally we'd like to "hide" the add()
operation, that can make a List<T> grow longer than we'd
like.
class
Vector { public Vector(int l) {...}
public int getlength() {...}
public double get(int i) {...}
public void set(int i, double val) {...}
}
Now let's return to the Matrix class. As in Vector, we will pre-allocate
space for all the elements. Here is some simple code to implement a matrix
class with TList objects; also in matrix.cs
(the companion "driver" is in matrixdemo.cs)
/**
* Class Matrix is implemented by a TList of rows.
*/
class Matrix {
// instance variables - replace the example below with your own
private TList<TList<double> > m;
// list of lists
private int height, width;
/**
* Constructor for objects of class TList
*/
public Matrix(int h, int w)
{
// initialize instance variables
height = h;
width = w;
m = new TList<TList<double>>(height);
// we must preallocate all the rows
for (int i = 0; i<height; i++) {
TList<double> theRow = new TList<double>(width);
theRow.Fill(0.0);
// we must preallocate all the slots (columns) in each row
m.Add(theRow);
}
}
public int getwidth() {return width;}
public int getheight() {return height;}
// get nth value, with range check
public double get(int r, int c) {
if (r<0 || r >= height) {
Console.WriteLine("Warning: Matrix.get() called with out-of-range row = " + r);
return 0.0;
}
if (c<0 || c >= width) {
Console.WriteLine("Warning: Matrix.get() called with out-of-range column = " + c);
return 0.0;
}
return m.get(r).get(c);
}
// set nth value, with range check
public void set(int r, int c, double val) {
if (r<0 || r >= height) {
Console.WriteLine("Warning: Matrix.set() called with out-of-range row = " + r);
return;
}
if (c<0 || c >= width) {
Console.WriteLine("Warning: Matrix.get() called with out-of-range column = " + c);
return;
}
m.get(r).set(c,val);
}
}
Things to note:
- We're using TList to build a 2-D structure.
- There's no analogue to TList.add(E e); we have to add entire rows or
columns or else the Matrix will no longer be neatly rectangular. Note
that we add new rows and columns "empty", that is, populated with nulls.
(In the code above, there is no way to add a new row or column.)
- Because the generic class uses TLists, not arrays, we don't have any
problem using the element type E directly throughout. When we created
the Vector and MyList classes, we had that annoying need to use Object
when creating arrays even when we wanted EltType.
- Matrix.print(int fieldwidth) is a handy way of generating output. Note
the parameter. Because of the parameter, making this into ToString() is
tricky. (Not shown above.)
- How do we know all the rows are the same length?
Linked List Efficiency
What good are linked lists? Inserting in the middle is fast, but finding
a point in the middle is slow. So almost everything is O(n).
But inserting at the head is always fast.
Also, linked lists use memory efficiently if you have a great many shorter
lists. While the next_ fields require space, there are no "empty" slots as
in an array-based stack. And no memory wasted due to list expansion.
These are singly linked lists; a doubly linked
list has a pointer prev_ as well as next_, that points to the
previous element in the chain.
Stacks and Linked Lists
While the array implementation of a stack is quite fast, the linked list
approach is equally straightforward. All we have to do is maintain a
pointer to the head:
class stack<T> {
private Cell<T> head_;
public bool is_empty() {return (head_ == null);}
public T pop() {T val = head_.data(); head_ =
head_.next(); return val;}
public void push(T val) {head_ = new
Cell<T>(val, head_);}
}
What would we need to do in C++ if we wanted to be sure we deleted a
popped cell?
Sorting Linked Lists
How would you sort a linked list? QuickSort is out.
Hashing
"When in doubt, use a hash table"
- Brian Fitzpatrick, Google engineering manager and former Loyola
undergrad
One way to search through a large number of values is to create a hash
function hash(T) that returns an integer in the range 0..hmax-1.
Then, given a data value d, we calculate h = hash(d)
and then put d into "bucket" h. A convenient way to do this is to have an
array htable of lists, and add d to the list htable[i]. This particular
technique is sometimes called "bucket hashing" or "chain hashing"; see
Bailey 15.4.2.
Linked lists are particularly convenient for representing the buckets, as we
will have a relatively large number of them, and most will be small.
What shall we use as a hash function? This comes up often, and a great
number of standard data structures rely on having something available.
Therefore, C# provides every object with a GetHashCode() method. It returns
a 32-bit value.
Demo: what are hashcodes of
- int values
- "d"
- "A"
- " "
- "2"
On my system, for a two-character string GetHashCode() returns
31*first_char + second_char, where the values first_char and second_char are
the ascii numeric values.
Example: bucket hashing of
"avocado",
"banana",
"canteloupe",
"durian",
"eggplant",
"feijoa",
where hash(s) = s.Length;
Many classes choose to "tune" the standard GetHashCode() by providing their
own version. Many data structures will simply assume that two objects with
different hashcodes are unequal, so it is important when providing an
overriding .Equals() method to also provide .GetHashCode(). In lab 3, I
provided Equals() and GetHashCode() for class LinkedList<T>; for lab 1
I did this for StrList.
If you were to create a class with its own .Equals(), but no .GetHashCode(),
search might fail with some containers. Given a container of your class, C#
might determine that there was no value in the container that had the same
GetHashCode() value as the search target, and give up, even if there was in
fact a value in the container that was .Equals() to the search target.
Mid-class exercise: call GetHashCode() on the following strings:
{
"avocado",
"banana",
"canteloupe",
"durian",
"eggplant",
"feijoa",
"guava",
"hackberry",
"iceberg",
"jicama",
"kale",
"lime",
"mango",
"nectarine",
"orange",
"persimmon",
"quince",
"rutabega",
"spinach",
"tangerine",
};
The above can be assigned to an array string[] A; this is done in hash.cs.
1. Do all of you get the same values for s.GetHashCode()? For "avocado" I
get -622659773; for "guava" I get 98705182.
2. Now use hash(s), in the file above, and put the strings into htable. For
what htablesize do you get buckets with "collisions": more than one string
assigned to it? For what htablesize is this particular table collision-free?
3. Can you think of an orderly way of searching for the answer for #2?
The hash table in hash.cs is not actually an object. What do we have to do
to make it one? Perhaps htablesize could be a parameter to the constructor.
Open Hashing
Another way to do hashing is so-called "open" hashing: a data object d
is simply put into htable[hash(d)]. If that position is
taken, the next position is used. For this to work, we need to be sure
that htablesize is quite a bit larger (eg at least double) the number of
elements added. Deletions require careful thought. See Bailey 15.4.1.
Traversing a Hash Table
If we want to print out a hash table, or construct an iterator to step
through each element in turn, we can simply run linearly through the
hashtable array. For bucket hashing, each hashtable[i] represents a linked
list to be traversed. For open hashing, we simply skip over the unused
elements.
This traversal is in no particular order!
A class based on this is in hashclass.cs;
note the print() method. This class uses the string type; there is also inthashclass.cs that uses int (yes, I
should have made this use a generic type).
Hash Sets and Hash Dictionaries
One way to implement sets of strings (or of a generic type T) is with
lists, as in StrSet.cs:
class StrSet {
private StrList sl;
public StrSet() {sl = new StrList(100); }
public bool isMember(string s) {
for (int i=0; i<sl.size(); i++) {
if (sl.get(i) == s) return true;
}
return false;
}
public void Add(string s) {
if (isMember(s)) return;
sl.Add(s);
}
}
But there is a problem here: the isMember() and Add() methods are O(N).
[why?]
Can we do better? Yes, with hashing.
To create a HashSet, we use a hash table as in hashclass.cs. The code for
this is in hashset.cs.
class hashset {
private hashtable ht;
public hashset(int size) {ht = new hashtable(size);}
public bool isMember(string s) {
return ht.isMember(s);
}
public void Add(string s) {
if (isMember(s)) return;
ht.Add(s);
}
public void print() {
ht.print();
}
}
To run this, it must be linked with hashclass.cs:
mcs hashset.cs hashclass.cs
Demo: hashvlist.cs, to compare lookup
times for a set of integers implemented as a list versus implemented with a
hashtable.
Dictionaries
To create a dictionary, we will use generic type parameters K for the key
and V for the values. We will rewrite our hashtable class so that the Cell
contains fields for the key (of type K) and value (of type V).
The interface will then be:
- V get (K key): returns the value corresponding to key, or else
default(V) (generally null)
- void add(K key, V val): adds the new pair. Precondition: K is not
already present
- void update(K key, V newval): like add. K may or may not be
present.
The dictionary example is at dictionary.cs.
It contains a simple driver program.
Demo: use dictionary.cs and count the word occurrences in a paragraph pasted
in from some other source (these notes, or else a paragraph from Bailey). If
s is a long string holding the paragraph, use s.Split() to divide it into
words. Extra features:
- Use s.Split(" .,;[]()\t") to split at other characters besides spaces
- Convert each word to lowercase
The file dictionary.cs contains a full-fledged implementation of a
dictionary type, complete with generics, support for "iterators", and the
d[] notation.
hashtable enumerator: demos/dictionary.cs
I want this to work:
foreach (KeyValuePair<string,int> kvp in d)
Console.WriteLine("{0}: {1}", kvp.Key, kvp.Value);
The hashtable is an array of linked lists; the linked-list cell type is
public class Cell {
private K key_;
private V val_;
private Cell next_;
public Cell(K k, V v, Cell n) {key_ = k; val_ = v; next_ = n;}
public K getKey() {return key_;}
public V getVal() {return val_;}
public Cell next() {return next_;}
public void setVal(V v) {val_ = v;}
public void setNext(Cell c) {next_ = c;}
}
To start, I must have class dictionary inherit from
System.Collections.Generic.whatever. This works:
class dictionary : System.Collections.Generic.IEnumerable> {
Then I must implement the IEnumerable method. The exact method signature is
as follows; note the return type.
IEnumerator> IEnumerable>.GetEnumerator() {
return foonumerator();
}
What is up with foonumerator()? That's here:
IEnumerator> foonumerator() {
for (int i = 0; i(p.getKey(), p.getVal());
p = p.next();
}
}
yield break;
}
Why didn't I just define this in IEnumerable, above? Because we also must
implement the non-generic form of IEnumerable, due to inheritance
constraints. I did that this way:
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() {
return foonumerator();
}
Otherwise I would have to type everything twice.
I figured this all out by reading the MSDN Dictionary.cs reference code, here.