|
|
What is a Linked List?Linked lists are among the most useful data structures in Computer Science, and are therefore very heavily used in lots of different applications. For this assignment, we will concentrate on the doubly-linked list, which is perhaps the most useful of all linked lists. Here's what a doubly-linked list looks like:
As you can see, the linked list consists of a number of list items linked together in both the forward direction and the reverse direction. The links are typically pointers, and that is what we will use to implement this kind of list. Why are Linked Lists so Useful?Linked lists are useful because they are very flexible. You can use a linked list as a collection mechanism; that is, the linked list can collect together items which have something in common with each other. For example, perhaps you wish to collect together all the people who work in your office. Or all the people who work for a particular boss. Sometimes the order of items in a list is important, and sometimes it isn't. It all depends on what your needs might be. Perhaps you don't care about the order at all, in which case you could treat the items in a list as a set of items (in the Mathematical sense), or a multi-set of items (where the value of an item can be duplicated within the list). Alternatively, you might wish to maintain the items in the list in a particular order -- perhaps alphabetically, or by employee number, or by salary. Linked List OperationsThere are obviously a number of useful operations that a linked list should support. For example:
Depending on how you are using the list, you may need only the ability to add or remove an item at the end of a list, or at the front of a list, or to add or remove an item anywhere in the list. Similarly, you may need only to navigate the list from the start to the end, or you may need to move both in the forward direction or in the reverse direction. You may wish to start at the beginning of the list, at its end, or anywhere in between. Doubly-Linked ListsThe doubly-linked list is very flexible in its ability to support the above features. Doubly-linked lists allow you to:
The doubly-linked list supports these features by using two pointers in each list item:
In addition, it is very useful to add one more pointer in each list item:
The next pointer, as its name suggests, points to the next list item in the list. The previous pointer similarly points to the previous list item in the list. If a list item is the first one in the list, then its previous pointer is NULL. If a list item is the last one in the list, then its next pointer is NULL. In this way, we can navigate through the list and have a way of ensuring that we don't "fall off" at either end of the list. The list pointer serves two purposes:
Class DesignTo implement such a doubly-linked list, we will be implementing three classes:
What must each class provide to support the necessary features? Well, let's cut to the chase, and give you some code to start with:
The
|
//
// list.cpp
//
#include <iostream>
#include <cassert> // For assert() -- use encouraged!
using namespace std;
#include "list.h"
/////////// List ////////////
void List::Dump(const string prefix) const
{
// Print List information
cout << prefix << "List at: " << this << endl
<< prefix << " Count: " << GetCount() << endl
<< prefix << " First: " << GetFirst() << endl
<< prefix << " Last: " << GetLast() << endl;
// Print all the ListItems
for (ListItem *item = GetFirst();
item != 0;
item = item->GetNext()
)
{
item->Dump(prefix + " ");
}
}
//////////// ListItem /////////////
void ListItem::Dump(const string prefix) const
{
cout << prefix << " ListItem: " << this << endl
<< prefix << " Next: " << GetNext() << endl
<< prefix << " Prev: " << GetPrevious() << endl;
}
/////////////// ListIterator ////////////////
|
Of course, there's lots else for you to fill in in this file!
I've discovered that most of my students seem to struggle to create a thorough test for their implementation. So, to provide a (hopefully) good example of how to test, and to provide you with some guidance on what constitutes correct operation, here is a test program that tests just the basic List/ListItem/ListIterator class operations:
#include <iostream>
using namespace std;
#include "list.h"
/*
* testList.cpp
*/
/*
* Tests a ListIterator, by iterating forward through the list,
* then backward, then forward again.
*/
void testIterator(ListIterator &iter)
{
cout << "The list contains " << iter.GetList()->GetCount()
<< " items" << endl;
cout << "The iterator is at the start of the list: "
<< boolalpha << iter.AtFirst() << endl;
ListItem *item = 0;
cout << "Iterating forward..." << endl;
for ( item = iter.GetCurrent(); item != 0; item = iter.GetNext() )
{
double *d = (double *) item->GetData();
cout << *d << endl;
}
cout << "The iterator is at the end of the list: "
<< boolalpha << iter.AtLast() << endl;
cout << "Iterating backward..." << endl;
for ( item = iter.GetCurrent(); item != 0; item = iter.GetPrevious() )
{
double *d = (double *) item->GetData();
cout << *d << endl;
}
cout << "The iterator is at the start of the list: "
<< boolalpha << iter.AtFirst() << endl;
cout << "Iterating forward again..." << endl;
for ( item = iter.GetCurrent(); item != 0; item = iter.GetNext() )
{
double *d = (double *) item->GetData();
cout << *d << endl;
}
}
/*
* Main entry point to do a fairly complete test of the basic
* List/ListItem/ListIterator classes.
*/
int main()
{
List list;
ListIterator iter1(list);
cout << "---Testing iteration through an empty list---" << endl;
testIterator(iter1);
cout << "---Initial list---" << endl;
list.Dump();
double d1 = 1.0, d2 = 2.0, d3 = 3.0, d4 = 4.0;
ListItem item1(&d1);
cout << "---Appending list item---" << endl;
list.Append(item1);
list.Dump();
ListItem item2(&d2);
cout << "---Adding list item to front of list---" << endl;
item2.Add(list, 0);
list.Dump();
ListItem item3(&d3);
cout << "---Inserting list item in the middle of list---" << endl;
item3.Add(list, &item2);
list.Dump();
ListItem item4(&d4);
cout << "---Adding list item to the end of list---" << endl;
item4.Add(list, &item1);
list.Dump();
cout << "---Testing iterator initialized against full list---" << endl;
ListIterator iter2(list);
testIterator(iter2);
cout << "---Testing iterator initialized against empty list---" << endl;
testIterator(iter1);
cout << "---Testing removal of item from end of list---" << endl;
item4.Remove();
list.Dump();
cout << "---Testing removal of item from middle of list---" << endl;
item3.Remove();
list.Dump();
cout << "---Testing removal of item from front of list---" << endl;
item2.Remove();
list.Dump();
cout << "---Testing removal of item alone in list---" << endl;
item1.Remove();
list.Dump();
cout << "---Adding items back to list---" << endl;
list.Append(item1);
list.Append(item2);
list.Append(item3);
list.Append(item4);
list.Dump();
cout << "---Testing purge---" << endl;
list.Purge();
list.Dump();
cout << "---Ending main function---" << endl;
cout << "***You should be testing the List destructor***" << endl;
return 0;
}
|
To give you an idea how you might use these classes in a real situation, here's an example.
Naturally, when I thought of lists, the first thing that came into my mind was a wine list! So here's a very simple Wine class, some of which you will be filling in. Then there is a test program to give our list implementation a reasonable degree of testing in a wine list situation.
#ifndef _WINE
#define _WINE
//
// wine.h
//
#include <string>
using namespace std;
class Wine
{
public:
Wine(const string &name, int year);
Wine(const Wine &wine);
~Wine();
const string &Name() const
{
return m_name;
}
int Year() const
{
return m_year;
}
// Assignment operator
Wine &operator=(const Wine &wine);
void Print() const;
private:
// Data members
string m_name;
int m_year;
};
#endif
|
//
// wine.cpp
//
#include <iostream>
using namespace std;
#include "wine.h"
Wine::Wine(const string &name, int year)
: m_name(name), m_year(year)
{}
void Wine::Print() const
{
cout << "Wine: " << m_name << ", " << m_year << endl;
}
// You implement the remaining member functions of the Wine class
|
//
// test1.cpp
//
#include <iostream>
using namespace std;
#include "list.h"
#include "wine.h"
static void display(List &list)
{
cout << "Contents of list:" << endl;
ListIterator iter(list);
for (ListItem *item = iter.GetCurrent();
item != 0;
item = iter.GetNext()
)
{
Wine *wine = (Wine *)(item->GetData());
wine->Print();
}
cout << "-----Dump------" << endl;
list.Dump();
cout << "---End Dump----" << endl;
}
static void ListWines()
{
Wine beaujolais("Beaujolais", 1970);
Wine amontillado("Amontillado", 1964);
Wine riesling("Riesling", 1980);
Wine gewurztraminer("Gewurztraminer", 1993);
Wine shiraz("Shiraz", 2003);
Wine cabernet("Cabernet Sauvignon", 2001);
// Each item in the following list is an instance
// of ListItem, each pointing to its corresponding
// Wine instance.
ListItem items[] = { &beaujolais, &amontillado,
&riesling, &gewurztraminer,
&shiraz, &cabernet };
ListItem *item; // Item pointer used in several places
Wine *wine; // Wine pointer used in many places
List list; // The list of wines
// The following List iterator is positioned so that
// the list it associates with is empty at the time
// of the association.
ListIterator iter(list);
// Start testing
cout << "Empty list..." << endl;
display(list);
cout << "Testing Append" << endl;
cout << " Appending: ";
wine = (Wine *)(items[0].GetData());
wine->Print();
list.Append(items[0]);
display(list);
cout << "Testing a second Append" << endl;
cout << " Appending: ";
wine = (Wine *)(items[4].GetData());
wine->Print();
list.Append(items[4]);
display(list);
cout << "Testing Add to front" << endl;
cout << " Adding: ";
wine = (Wine *)(items[1].GetData());
wine->Print();
items[1].Add(list, 0);
display(list);
cout << "Testing Add to end" << endl;
cout << " Adding: ";
wine = (Wine *)(items[2].GetData());
wine->Print();
items[2].Add(list, list.GetLast());
display(list);
cout << "Testing Add to middle" << endl;
cout << " Adding: ";
wine = (Wine *)(items[3].GetData());
wine->Print();
items[3].Add(list, &items[1]);
display(list);
cout << "------------Iterator testing------------" << endl;
cout << "Testing iterator originally set on empty list..." << endl;
for (item = iter.GetCurrent();
item != 0;
item = iter.GetNext()
)
{
Wine *wine = (Wine *)(item->GetData());
wine->Print();
}
cout << "Now reversing direction on iterator..." << endl;
for (item = iter.GetCurrent();
item != 0;
item = iter.GetPrevious()
)
{
Wine *wine = (Wine *)item->GetData();
wine->Print();
}
cout << "Reversing direction again..." << endl;
for (item = iter.GetCurrent();
item != 0;
item = iter.GetNext()
)
{
Wine *wine = (Wine *)item->GetData();
wine->Print();
}
cout << "--------End Iterator testing------------" << endl;
cout << "Testing removal from middle" << endl;
cout << " Removing: ";
wine = (Wine *)(items[3].GetData());
wine->Print();
items[3].Remove();
display(list);
cout << "Testing removal from end" << endl;
cout << " Removing: ";
wine = (Wine *)(items[2].GetData());
wine->Print();
items[2].Remove();
display(list);
cout << "Testing removal from front" << endl;
cout << " Removing: ";
wine = (Wine *)(items[1].GetData());
wine->Print();
items[1].Remove();
display(list);
cout << "Testing removal from end" << endl;
cout << " Removing: ";
wine = (Wine *)(items[4].GetData());
wine->Print();
items[4].Remove();
display(list);
cout << "Testing removal of last item" << endl;
cout << " Removing: ";
wine = (Wine *)(items[0].GetData());
wine->Print();
items[0].Remove();
display(list);
// Finally, we test the actions of the List destructor
// First, go through the items to check that they are not in
// a list...
cout << "Checking that removed items do not appear to be in a list" << endl;
int len = sizeof(items)/sizeof(items[0]);
int elem;
int errors = 0;
for (elem = 0; elem < len; elem++)
{
if (items[elem].GetList() != 0)
{
errors++;
cerr << "Element " << elem << " still in a list" << endl;
}
if (items[elem].GetNext() != 0)
{
errors++;
cerr << "Element " << elem << " still has a next ptr" << endl;
}
if (items[elem].GetPrevious() != 0)
{
errors++;
cerr << "Element " << elem << " still has a previous ptr" << endl;
}
}
// Now, create the list and populate it from the items
cout << "Checking addition and removal to a dynamic list" << endl;
List *dynList = new List();
for (elem = len-1; elem >= 0; elem--)
{
dynList->Append(items[elem]);
}
// Display the list for validation
display(*dynList);
// Now destroy the list and examine the state of each
// of the items that were in the list
cout << "Destroying dynamic list" << endl;
delete dynList;
cout << "Checking that removed items do not appear to be in a list" << endl;
for (elem = 0; elem < len; elem++)
{
if (items[elem].GetList() != 0)
{
errors++;
cerr << "Element " << elem << " still in a list" << endl;
}
if (items[elem].GetNext() != 0)
{
errors++;
cerr << "Element " << elem << " still has a next ptr" << endl;
}
if (items[elem].GetPrevious() != 0)
{
errors++;
cerr << "Element " << elem << " still has a previous ptr" << endl;
}
}
cout << errors << " errors detected" << endl;
}
int main()
{
ListWines();
return 0;
}
|
Now, let's look at an alternative approach to using such Lists, ListItems, etc.
You may notice that it's a little clumsy to separate a ListItem from the data it represents. Here's another, slightly different, approach that makes things a little easier:
#ifndef _PERSON
#define _PERSON
// person.h
#include <string>
using namespace std;
#include "list.h"
class Person
{
public:
Person(const string &name);
Person(const Person &person);
~Person();
const string &Name() const
{
return m_name;
}
void Add(List &list, Person *prevPerson);
void Remove();
void Print() const;
// Assignment operator
Person &operator=(const Person &person);
// Conversion function to ListItem &
operator ListItem &()
{
return m_item;
}
private:
ListItem m_item;
string m_name;
};
#endif
|
// person.cpp
#include <iostream>
using namespace std;
#include "person.h"
Person::Person(const string &name)
: m_item(this), m_name(name)
{}
void Person::Print() const
{
cout << "Person: " << m_name << endl;
}
// You implement the remaining member functions of the Person class
|
// test2.cpp
#include <iostream>
using namespace std;
#include "list.h"
#include "person.h"
static void display(List &list)
{
cout << "Contents of list:" << endl;
ListIterator iter(list);
for (ListItem *item = iter.GetCurrent();
item != 0;
item = iter.GetNext()
)
{
Person *person = (Person *)(item->GetData());
person->Print();
}
cout << "-----Dump------" << endl;
list.Dump();
cout << "---End Dump----" << endl;
}
static void ListPersons()
{
Person fred("Fred");
Person mary("Mary");
Person joe("Joe");
Person clive("Clive");
Person nigel("Nigel");
Person nostradamus("Nostradamus");
Person einstein("Einstein");
Person *items[] = { &fred, &mary, &joe, &clive,
&nigel, &nostradamus, &einstein };
ListItem *item;
List list; // The list of people
// The following List iterator is positioned so that
// the list it associates with is empty at the time
// of the association.
ListIterator iter(list);
// Start testing
cout << "Empty list..." << endl;
display(list);
cout << "Testing Append" << endl;
cout << " Appending: ";
fred.Print();
list.Append(fred);
display(list);
cout << "Testing a second Append" << endl;
cout << " Appending: ";
nigel.Print();
list.Append(nigel);
display(list);
cout << "Testing Add to front" << endl;
cout << " Adding: ";
mary.Print();
mary.Add(list, 0);
display(list);
cout << "Testing Add to end" << endl;
cout << " Adding: ";
joe.Print();
joe.Add(list, (Person *)(list.GetLast()));
display(list);
cout << "Testing Add to middle" << endl;
cout << " Adding: ";
clive.Print();
clive.Add(list, &mary);
display(list);
cout << "------------Iterator testing------------" << endl;
cout << "Testing iterator originally set on empty list..." << endl;
for (item = iter.GetCurrent();
item != 0;
item = iter.GetNext())
{
Person *person = (Person *)(item->GetData());
person->Print();
}
cout << "Now reversing direction on iterator..." << endl;
for (item = iter.GetCurrent();
item != 0;
item = iter.GetPrevious()
)
{
Person *person = (Person *)(item->GetData());
person->Print();
}
cout << "Reversing direction again..." << endl;
for (item = iter.GetCurrent();
item != 0;
item = iter.GetNext()
)
{
Person *person = (Person *)(item->GetData());
person->Print();
}
cout << "--------End Iterator testing------------" << endl;
cout << "Testing removal from middle" << endl;
cout << " Removing: ";
clive.Print();
clive.Remove();
display(list);
cout << "Testing removal from end" << endl;
cout << " Removing: ";
joe.Print();
joe.Remove();
display(list);
cout << "Testing removal from front" << endl;
cout << " Removing: ";
mary.Print();
mary.Remove();
display(list);
cout << "Testing removal from end" << endl;
cout << " Removing: ";
nigel.Print();
nigel.Remove();
display(list);
cout << "Testing removal of last item" << endl;
cout << " Removing: ";
fred.Print();
fred.Remove();
display(list);
// Finally, we test the actions of the List destructor
// First, go through the items to check that they are not in
// a list...
cout << "Checking that removed items do not appear to be in a list" << endl;
int len = sizeof(items)/sizeof(items[0]);
int elem;
int errors = 0;
for (elem = 0; elem < len; elem++)
{
ListItem &item = *(items[elem]);
if (item.GetList() != 0)
{
errors++;
cerr << "Element " << elem << " still in a list" << endl;
}
if (item.GetNext() != 0)
{
errors++;
cerr << "Element " << elem << " still has a next ptr" << endl;
}
if (item.GetPrevious() != 0)
{
errors++;
cerr << "Element " << elem << " still has a previous ptr" << endl;
}
}
// Now, create the list and populate it from the items
cout << "Checking addition and removal to a dynamic list" << endl;
List *dynList = new List();
for (elem = len-1; elem >= 0; elem--)
{
dynList->Append(*(items[elem]));
}
// Display the list for validation
display(*dynList);
// Now destroy the list and examine the state of each
// of the items that were in the list
cout << "Destroying dynamic list" << endl;
delete dynList;
cout << "Checking that removed items do not appear to be in a list" << endl;
for (elem = 0; elem < len; elem++)
{
ListItem &item = *(items[elem]);
if (item.GetList() != 0)
{
errors++;
cerr << "Element " << elem << " still in a list" << endl;
}
if (item.GetNext() != 0)
{
errors++;
cerr << "Element " << elem << " still has a next ptr" << endl;
}
if (item.GetPrevious() != 0)
{
errors++;
cerr << "Element " << elem << " still has a previous ptr" << endl;
}
}
cout << errors << " errors detected" << endl;
}
int main()
{
ListPersons();
return 0;
}
|
You will need to do a reasonable amount of testing to
ensure that your implementation of these classes actually
works correctly. The above test1.cpp
and test2.cpp files do
some reasonable amount of testing, but don't feel restricted by them.
If you do add your own tests, please create your own test main program --
it's much easier for me to separate out your efforts from mine!
I strongly suggest that you freely instrument your code so that you can turn on tracing and debugging information when convenient and/or necessary. Adding a line like:
cout << "Adding item at " << this << " after item at " << prevItem
<< " and before item at " << nextItem << endl;
at the appropriate place in your code can really help you figure out what's going on. However, I don't need to see all these lines of code, so just use them for your debugging, and then clean them out so I can read your code.
Note that the calls to the Dump member functions may give you as much information as you need, but then again, if you need to do your own tracing, you need to do it.
I expect you to provide me with evidence (hard copy output would be best) that you have tested the program sufficiently. Since you will be using and enhancing these classes in later assignments, it's to your advantage to make sure that they work, now!
| This page was last changed on 09 Apr 2006 |