CSCI 151 - Lab 5 Link 'em up!

Due before 10pm, Sunday, March 17

In this lab you will implement a doubly linked list and perform some timing experiments on it.

The purpose of this lab is to:

If you'd like, you may work with one partner on this lab. If you choose to do so, you must both contribute equally to the work of this lab, and are both responsible for understanding its workings. You can then hand in one submission between the two of you, with both your names clearly marked somewhere obvious to the graders (e.g., README and headers of all files).

Part 1 - Doubly Linked Lists

Your first task is to implement a doubly linked list called MyLinkedList<T> and a corresponding ListIterator for your class.

I have some starting point code for you here: lab5.zip Create a Lab 5 folder and unzip this file into it before you create your Lab5 project in Eclipse.

MyLinkedList

In this lab you create a class called MyLinkedList<T>. Your class is just a subset of LinkedList, and therefore should match its behavior on this subset.

Your class should extend AbstractList<T> and later in this lab you will create a ListIterator (either as a nested class or as an anonymous class).

When you have finished your implementation, be sure to test it thoroughly before continuing. In doubly linked lists, the removal of items can be especially tricky as you need to be sure to properly update all of the pointers of the next and previous elements, as well as handle the special cases for removal from the front or tail. Keep a piece of paper with you and draw pictures to help with your coding. Nobody writes this code without referring to pictures.

You should not allow "null" values to be inserted into the list; if the user of your class attempts to do so, you should throw a NullPointerException.

Constructors

You should only need to have a single public 0-argument constructor that creates an empty list and initializes all the necessary variables.

Empty linked list

Private Methods

Node getNth(int index)
a method that returns the Node at a specified index, not the content.

I also had a removeNode(Node n) method that allowed me to not have to duplicate code in my iterator. But you aren't required to implement one unless you want to.

Public Methods

boolean add(T data)
void add(int index, T data)
add an element into this list (either at end or by index)
throw a NullPointerException if the user tries to add a null pointer
throw IndexOutOfBoundsException as needed (same rules as MyArrayList)
Note: the boolean add method will always return true; it is a boolean function due to the method definition in AbstractList
T get(int i)
get contents at position i
throw IndexOutOfBoundsException as needed
T set(int i,T data)
set the value at index i to data, return old value
throw NullPointerException if data is null
throw IndexOutOfBoundsException as needed
T remove(int i)
remove (and return) the element from position i in this list
throw IndexOutOfBoundsException as needed
void clear()
remove all elements from the list
boolean isEmpty()
determine if the list is empty
int size()
return the number of elements being stored

Programming hints

Testing your List

You should be able to re-use the tests you wrote in Lab 2 for MyArrayList. Copy the MyArrayListTest.java file into your lab5 folder and rename it to be MyLinkedListTest.java. Or just have eclipse create the JUnit source file MyLinkedListTest.java and copy and paste from your MyArrayListTest.java file. Then you can open up the file, rename the public class to be "MyLinkedListTest", and then update the methods to use LinkedLists instead of ArrayLists. Just change MyArrayList and ArrayList to MyLinkedList and LinkedList throughout so you get lines like

MyLinkedList<String> x = new MyLinkedList<String>();

Part 2 - ListIterator

Once your MyLinkedList class is working, you will need to create a ListIterator which is returned by the factory listIterator() method. You can do this one of two ways:

  1. Through the use of an anonymous class:
    public ListIterator<T> listIterator(){
        return new ListIterator<T>(){
            // TODO - your code here
        };
    }
  2. Or through the use of a helper class MyLinkedListIterator nested in the same file as MyLinkedList in the same way the Node class is nested within MyLinkedList.
    class MyLinkedListIterator implements ListIterator<T> {
    
        // class variables here
    
        public boolean hasNext() {
                // your code here
        }
    
        // more methods, etc.
    }
        

The text talks about nested classes on p. 96. If you want more, here is Oracle's discussion of nested classes. You can read about anonymous classes here. Most people new to Java, and even some experienced ones, find nested classes easier to handle than anonymous classes, but you are free to use either technique.

The ListIterator is able to traverse the list by moving a space at a time in either direction. It might be helpful to consider that the iterator has size+1 positions in the list: just before the head, between the 0 and 1 index, ..., just after the tail. After one call to next(), the iterator is logically in the state shown below (click to enlarge).

Linked list iterator after one call to next()

In either case, you will need to implement all of the methods of a listIterator -- hasNext()/next(), hasPrevious()/previous(), nextIndex()/previousIndex(), remove(), set(x), and add(x). See the JavaDoc for details, but I do have some notes below.

You will need to implement all of the following methods. See ListIterator for details.

Public Methods to add to MyLinkedListIterator

boolean hasNext()
Return true if there are more elements when going in the forward direction.
T next()
Return the next element in the list when going forward.
Throw NoSuchElementException if there is no such element
boolean hasPrevious()
Return true if there are more elements when going in the reverse direction.
T previous()
Return the next element in the list when going backwards.
Throw NoSuchElementException if there is no such element
int nextIndex()
Return the index of the element that would be returned by a call to next()
Return the list size if at the end of the list
int previousIndex()
Return the index of the element that would be returned by a call to previous()
Return -1 if at the start of the list
void set(T x)
Change the value in the node returned by the most recent next/previous with the new value.
Throw an IllegalStateException if neither next nor previous were called
Throw an IllegalStateException if add or remove have been called since the most recent next/previous
void remove()
Remove the last element returned by the most recent call to either next/previous
Throw an IllegalStateException if neither next nor previous were called
Throw an IllegalStateException if add has been called since the most recent next/previous
void add(T x)
Insert the given item into the list immediately before whatever would have been returned by a call to next()
The new item is inserted before the current cursor, so it would be returned by a call to previous() immediately following.
The value of nextIndex or previousIndex both are increased by one

Programming Hints

I found that it was useful to have a number of state fields to simplify the writing of the various methods above. Since the cursor of the ListIterator exists logically between 2 nodes, I found it was useful to just keep references to the next Node and the previous Node. I also kept an int value of the index of the next node.

If you construct your MyLinkedList to use sentinel nodes as discussed in the book and lecture, and you properly throw exceptions for going out of range, you shouldn't have to worry about checking for null values at the ends of the list since the sentinel nodes are there.

Since set() and remove() both change based on what direction we were last traversing, I kept a boolean flag to tell me if I was last going backwards.

It is possible, and sometimes useful, to have several iterators working on the same list. If they both try to change the structure of the list you could get into an unpredictable state. Therefore you should check that the list hasn't been modified by someone else before you let an iterator make a modification. An easy way to do this is to have both the list and the iterator keep track of the number of modifications that have been made to the list. When the iterator is created it grabs the list's modification count. Each change the iterator makes to the list increases both its and the lists modification count. However, if you have two iterators modifying the list, one of them will find that its modification count will be different from the list's, so you will know that the list structure has been changed and that iterator is no longer viable. See modCount and the text for additional details and suggestions.

Public Methods to add to MyLinkedList.java

Once you are sure your iterator is working, you should override the following methods in MyLinkedList. Each of these should just create a new MyLinkedListIterator and return it.

ListIterator<T> listIterator()
Iterator<T> iterator()
have these factory methods return your ListIterator class for the current list.

Note: You inherit a working ListIterator from AbstractList, but the one you create will be more efficient. I suggest that while you are building and initially testing your ListIterator, you create a differently named factory method to use. I tend to use names like QQQiterator() and QQQlistIterator() until I'm sure it is working correctly. If you jump right into overriding iterator()/listIterator() then things like toString() may stop working for you.

Be sure to have it called just iterator() and listIterator() in your submitted MyLinkedList and JUnit code. Many students last semester just left it as 'QQQiterator' which broke things, and made the graders think you had skipped that step.

Testing your Iterator

You'll want to test your ListIterator and be sure that it works properly. One good way to test this would be to create a JUnit test in MyLinkedListTest.java that will perform the Sieve of Eratosthenes. In case you've not encountered this before, the "sieve" is used to determine prime numbers. The basic idea is that you initially list all of the integers in the range as potential prime numbers, then go through the possible factors one-by-one (starting from 2) and cross out any value on your list that is a multiple of it (but not the number itself).

So assuming you have the numbers from 2-10, you'd first go through and cross out the multiples of 2 other than itself (4,6,8,10) and then multiples of 3 other than itself (9, since the 6 is already gone), etc. You should stop when you reach the square root of the larger range end. What you're left with should be all the primes in the given range.

For example, the Sieve of [11,20] would return 11, 13, 17, and 19, and the Sieve of [1,20] would return 2, 3, 5, 7, 11, 13, 17, and 19.

Note that the sieve is not a required part of this lab; it is just a suggestion for an interesting test case.

Part 3 - MRU List

Now that you are sure that you have a working MyLinkedList and a working iterator, let's create a variation of the list. You will be creating a Most Recently Used (MRU) List which is derived from your existing list. The idea behind a MRU list is that when an item is looked up in a list, it is often looked up again in the near future. To try and improve lookup times, whenever an item is "found" in the list, it is moved to the front of the list so that subsequent searches for it might be faster.

Create a class MRUList<T> that extends your MyLinkedList<T>. You will need to override a few inherited methods.

public boolean add(T x)
public void add(int index, T x)
Any item that has been added is considered to have been recently found, so just add these at index 0.
You can access a parent's version of a method by using the super keyword. For example super.add(0,x) calls the parent class' method.
public boolean contains(Object o)
This method is the one you will be using to see if an item is in your list. When this method finds the object in the list, it should remove it and add it to the front. You can do this through the use of your ListIterator's remove() method if it is working. You could also do this by finding the index of the item if it is in the list and then using your remove(index) and then adding back to the front.

Testing MRUList

Also create JUnit test cases for your MRUList in a file called MRUListTest.java.

If you are having trouble getting your MRU class working correctly, try extending java.util.LinkedList<T> instead of MyLinkedList and see if it is a problem in your logic or in your MyLinkedList class.

Part 4 - RuntimeExploration

For this next part, I want you to use a provided class called CollectionTimer that will let you compare the running time of using your MyLinkedList and MRUList to do a spell checking task.

What the CollectionTimer does is read in a list of known "good" words which it stores in a collection such as your MyLinkedList. It then reads a fixed number of words from a second file and checks to see if they are contained in the "good" list or not. The program will keep track of the number of words that matched or not, but that isn't displayed unless you enable debugging information in the CollectionTimer. Instead, it keeps track of the number of milliseconds that have elapsed during the performance of this task. It only starts timing once it is doing the word list lookup, so setup time is not included.

The program takes 6 arguments as described below. The first 2 are required. The other 4 are optional and are used to change the amount of work performed for each iteration.

  1. The name of the dictionary file
  2. The name of the document to be checked
  3. The number of words to initially read from the document (Default: 5000)
  4. The number of words to increase by for each run (Default: 5000)
  5. The number of times to increase (Default: 5)
  6. The number of times to re-run the test before averaging (Default: 5)

Here is the output on my laptop using the default number of words, increments, and steps.

% java CollectionTimer medium-wordlist.txt pride-and-prejudice.txt
Wordlist: medium-wordlist.txt  Document: pride-and-prejudice.txt
words: 5000  increment: 5000  steps: 5  reps: 5
Class: MyLinkedList
=======================================
  1:    5000 words in    2473 milliseconds
  2:   10000 words in    4848 milliseconds
  3:   15000 words in    7489 milliseconds
  4:   20000 words in    9731 milliseconds
  5:   25000 words in   12182 milliseconds

Wordlist: medium-wordlist.txt  Document: pride-and-prejudice.txt
words: 5000  increment: 5000  steps: 5  reps: 5
Class: MRUList
=======================================
  1:    5000 words in     908 milliseconds
  2:   10000 words in    1525 milliseconds
  3:   15000 words in    2092 milliseconds
  4:   20000 words in    2684 milliseconds
  5:   25000 words in    3310 milliseconds

I've included a number of wordlists for you to try comparing against. I also included a copy of "Pride and Prejudice" from Project Gutenberg which has 121557 words which should contain more than enough text for you to test against for your loops. The wordlist files have the following word counts:

You should not need to make any modifications to CollectionTimer. There is a debug flag that you can enable to let you see some of the inner workings. This might be useful if you think that your MyLinkedList or MRUList might be losing items or not working correctly.

README Question 1

Copy and paste the output from one run of CollectionTimer using small-wordlist.txt and pride-and-prejudice.txt with default for the rest. I.e., the command I used just above.

If you find that it is running too quickly/slowly, you may need to modify things from the default parameters. If you do so, be sure to document what settings you are using in your README. Try and have the number of lookups double, and then double again, to get a good range of observations.

README Question 2

The running time for this task should be linear in terms of the number of items in our wordlist. (To determine a "miss" you would need to look at every word in the list.) Let's call this n. It is also linear in terms of the number of words to be read in. Let's call this m. Taken together, you might express this as O(m n).

Looking at the table you've generated, as the size of m doubles, you would expect the worst case running time to also double. Does this hold for your observations? Be sure to consider MyLinkedList and MRUList separately. Why do you think this is?

README Question 3

Now re-run your experiment from Question 1, but this time using the medium-wordlist.txt. Include this table in your README.

This wordlist is 9 times the size of small-wordlist.txt. How does this change the performance of each type of list? Why do you think this is?

Note: if this is running too slowly for you, you can run it for values "500 500 5 5", but include the chart for both small and medium.

README Question 4

Now re-run your experiment but use pride-and-prejudice.txt as both the wordlist and the document to be checked. Include this table in your README. What do you observe about the running times now? Why do you think this is?

Part 5 - handin

Look through your programs and make sure you've included your name at the top of all of them.

README file

Include in your submission a file named README. The contents of the README file should include the following:

  1. Your name and your partner's name
  2. Any known problems or assumptions made in your classes or program
  3. The answers to the questions from Part 4

Look through your programs and make sure you've included your name at the top of all of them.

We are expecting you to hand in at least the following files:

Honor code

If you adhered to the honor code in this assignment, add the following statement to your README file:

I have adhered to the Honor Code in this assignment.

handin

You now just need to electronically handin all your files. Assignment is 5.

Don't forget to run lshand and verify that things were submitted.


Last Modified: October 1, 2013 - Benjamin A. Kuperman - JUnit by Alexa Sharp VI Powered