Containers

A container is a class that holds a collection of objects. All containers have some common properties. Elements can be added and deleted, you can ask how many elements are in the container. Examples include arrays, vectors, lists, stacks and others.

Linked Lists

Lists are a container class. Each element in the container includes the location of the next. Elements can be added at either end or anywhere in between. Lists can be empty and have no length limit. You cannot directly access any element as you can in an array. To get to the third element, you have to go through the first two. Access is slower than arrays. Insertions and deletions are easier. Elements can either be single or double linked to each other.

Implementations

Lists can be implemented in a number of ways. The links between elements can be C++ pointers but can be other things. The definition is that one element must tell where the next is. For example. a set of files, where each file contains the name of the next one in the sequence, would be a list. Our first implementation uses arrays.

Array implementation

We will make an array of structures.

const int SIZE=5;
struct List_data {
	Data d;
	int next;
};
struct List_data List[SIZE];

Initially, all the next values in the array are set to -1.

When we want to add an element to the list, we look through the array for an element that has a next value of -1. To make the search a little faster, we can keep a counter to tell us where we stopped last.

int Pos = 0;
So the process for creating a list is this. We need an integer to hold the location of the beginning of the list.
int lptr=-1;

In our implementation, -1 serves the purpose of NULL. We need an instance of Data and we need to set it. Then we need to find the end of the list. We also need to find an empty element. We copy the Data to that slot. The next field of the element at the end of the list is changed to point to the new element. Now the element is added.

We need the data array initialized so we can use the find_empty() function. Let's set all the next fields in the array to -1.

void
init()
{
	for(int i = 0; i < SIZE; i++)
		List[i].next = -1;
} // init

The first version of the function uses the Pos variable to record where we last looked. It searched the array circularly looking for slots that aren't used.

int
find_empty()
{
	int start = Pos;
	// if we are at the end of the array, start at the top
	if(start == SIZE) start = 0;
	while((start != Pos) && (List[start].next != FREE)) {
		start  += ++start % SIZE;
	} // while
	if(List[start].next == -1)
		return start;
	else 
		return -1;
} // find_empty

There are several problems that need to be solved in this code. One is that the initial value is wrong. -1 is also used to mark the end of a list. Thus, the last element of list could be mistaken for a free element. We should have used something else to mark free elements. Perhaps something like

const int FREE = -2;

Also notice that we don't change the value of Pos in the code. Let's combine some of these fixes together into e new version.

int
find_empty()
{
	int start = Pos;
	while(List[start].next != FREE) {
		start  += ++start % SIZE;
		if(start == Pos) return -1;  // we wrapped around
	} // while
	// the only way out of the loop this way is if
	// we found one
	Pos = (start+1) % SIZE; // start again after the one we found
	return start;
} // find_empty

Another variation doesn't use the Pos position to find out if we have wrapped around. We could use a for loop to control how many times we go around.

int
find_empty()
{
	int start = Pos;
	for(int i = 0; i < SIZE; i++) {
		if(List[start].next != FREE) {
			Pos = start;
			return start;
		}
		start += ++start % SIZE;
	} // for
	return -1; // if we get here, we wrapped around
} // find_empty

Let's try one more variation

int
find_empty()
{
	for(int start = Pos+1; start != Pos; start += ++start % SIZE;) {
		if(List[start].next != FREE) {
			Pos = start;
			return start;
		}
	} // for
	return -1; // if we get here, we wrapped around
} // find_empty

int
add_element(int L, Data my_d)
{
	int lptr = l;
	int my_l = find_empty();
	if(my_l == -1) return -1;  // no memory
	List[my_l].d = my_d;
	List[my_l].next = -1;
	// find the end of the list
	while( (l != -1) && (l.next != -1)) {
		l = list[l].next;
	} // while
	if(l == -1) 
		l = my_l; // first entry
	else
		List[l].next = my_l;
	return 0;	
} // add_element

p> To find an element, we use this algorithm
int
find_element(int l, int one, int two)
{
	int lptr = l;
	if(l == -1) return -1;
	while( (lptr ! = -1) && (list[lptr].d != my_d)) {
	} // while
	if( lptr == -1) // didn't find it
		return -1;
	else
		return lptr;
} // find_element

This is enough for this implementation. You can see that the pointers are represented by the array references.

Linked structure representation

We use a similar data structure as above.

struct List_data {
	Data d;
	List_data *next;
};

Now our list variable is a C Pointer.

In this implementation, we don't need to find a free element, we will use new to make one.

// add an element to the end of the list
int
add_end(List_data* & l, Data my_d)
{
   List_data *lptr = l,*my_l;
   if( (my_l = new List_data) == NULL) return -1;

   my_l->next = NULL;  // since this will be the new last element
   my_l->d = my_d;

   // if there are no nodes on the list, then the new one is
   // both the first and last node, so l (remember it is a
   // reference parameter) should point at it.
   if(lptr == NULL)
      l = my_l;
   else {
      // move lptr until it points at the current last node
      while(lptr->next != NULL) lptr = lptr->next;
      lptr->next = my_l; // point it at our new node
   }
   return 0;
} // add_element

// search the list node by node until we find the one
// that contains my_d. Return NULL if we don't find it
List_data *
find_element(List_data *l, Data my_d)
{
   List_data * lptr = l;
   while(lptr != NULL) {
      if( is_equal(lptr->d,my_d) ){
         return(lptr); // found it
      }
      lptr = lptr->next;
   } // while
   return(NULL);
} // find_element

// add a new node to the front of the list.
int
add_front(List_data * & l, Data my_d)
{
   List_data *lp;
   if( (lp = new List_data) == NULL) return -1;
   lp->d = my_d;
   // since this will be the new first one,
   // it should point to the current first one
   lp->next = l;
   l = lp; // make the new one the first one.
   return 0;
} // add_front

// search the list for the target data and add a new node after
// that one.
int
insert(List_data* l, Data target,Data my_d)
{
   List_data *lp,*lptr;
   
   if((lp = new List_data) == NULL) return -1;
   lp->d = my_d;
   // use the find_element() function above to locate target
   // in the list, if it is there
   lptr = find_element(l, target);
   if(lptr == NULL)  // not there
      return(-1);
   else {
      // the new one should point to the one
      // after the one we found (lptr->next)
      lp->next = lptr->next;
      // the one we found should point to the new one
      lptr->next = lp;
      // so the new one is after the one we found
   }
   return 0;   
} // insert

// same as above, but takes uses the fact we are adding after the target.
// The one find_element() returns (lptr) can be seen as the front of a list 
// that starts with the one that comes after it.
// So we want to add to the front of that list.
// Which is what add_front() does.
int
insert2(List_data* &l, Data target,Data my_d)
{
   List_data *lptr;
   
   lptr = find_element(l, target);
   if(lptr == NULL)
      return(-1);  // didn't find it
   else
      add_front(lptr->next,my_d);
   return 0;   
} // insert

// walk the list, printing each data element
void
print_list(List_data *l)
{
   List_data * lptr = l;
   
   cout << "list: ";
   while(lptr != NULL) {
      cout << "(" <<  lptr->d.x << "," << lptr->d.y << ") ";
      lptr = lptr->next;
   } // while
   cout << endl;
} // print_list


Now for something trickier, deletions. This function will remove a given element from the list.

// remove the node that contains the data my_d
int
delete1(List_data *l, Data my_d)
{
   List_data *before,*current;
   // if the first one is the one we want, delete it quick
   if( is_equal(l->d,my_d) ){
      l = l->next;
      return 0;
   }

   // set up two pointers. One (current) we walk along the list
   // looking for the data my_d.
   // the other points to the one that comes before it.
   before = l;  // we know already that the first one isn't it
   current = l->next;  // start with the second one

   // if we find it, make the one that comes before it
   // point to  the one that comes after. This deletes the node
   // from the list
   while(current != NULL) {
      if( is_equal(current->d,my_d) ){ 
         before->next = current->next;
      }
      // move the two pointers to the next nodes
      before = current;
      current = current->next;
   } // while

   return -1;  // didn't find it
} // delete1
	

There are some things wrong, find them in class. Here is the complete deletion routine.

// remove the node that contains the data my_d
int
remove(List_data *&l, Data my_d)
{
   List_data *before,*current;
   if(l == NULL) return(-1);

   // if the first one is the one we want, delete it quick
   if( is_equal(l->d,my_d) ){
      l = l->next;
      return 0;
   }

   // set up two pointers. One (current) we walk along the list
   // looking for the data my_d.
   // the other points to the one that comes before it.
   before = l;  // we know already that the first one isn't it
   current = l->next;  // start with the second one

   // if we find it, make the one that comes before it
   // point to the one that comes after. This deletes the node
   // from the list. Then delete the memory it used.
   while(current != NULL) {
      if( is_equal(current->d,my_d) ){ 
         before->next = current->next;
         delete current;
         return(0);
      }
      // move the two pointers to the next nodes
      before = current;
      current = current->next;
   } // while

   return -1;  // didn't find it
} // remove

If we assume that l points not to the first element but to a header record, what would the code look like?