Professional Documents
Culture Documents
Linked Lists II
Chapter LX Topics
40.1 Introduction
Linked lists are not neatly divided into two separate topics. There is only one
reason for creating two chapters on this important topic: volume. The large
volume of information associated with linked lists is easier to digest with two
chapters. Two chapters also allows a separation of quizzes, lab assignments and
especially two chapter tests for each part. The feedback of a test in the middle of
linked list materials is important for both teachers and students.
The previous chapter introduced the idea of linking memory locations that had
been allocated dynamically. This type of linking during program execution
promoted not only better space efficiency, but execution efficiency as well. You
learned how there are three fundamental linking approaches with a linked list.
Linking can be done in a LIFO sequence, a FIFO sequence or it can be ordered to
some desired linking sequence.
In this chapter we start by looking at some of the concepts learned in the previous
chapter and add practicality by a developing a library of functions to assist with
linking as a stack or as a queue. We will also show how to declare classes that
process linked data structure.
The chapter will finish by showing that there exist other linking approaches as
well and you will learn about linked lists that provide links in two directions,
linked lists that are circular and complex linked data structures that are linked lists
of linked lists.
So far, you have not seen an ounce of new information. In fact, dereferencing is
a good thing because you can access data fields in the nodes of a linked lists.
How else can a pointer traverse along a linked list without dereferencing? Totally
true, which gets us to the specific point that the problem occurs when the NULL
pointer is dereferenced.
This definition may be satisfactory for many people, but for others it does not
clarify the problem. So let us look at some program examples and see why this is
an important issue. Program PROG4001.CPP starts with a simple program that
creates a linked list and traverses the linked list. For simplicity sake, the linked
list is ordered as a stack. All dereferencing in this program is correct and the
NULL pointer is not accessed in any improper fashion.
Pay particular attention to the manner in which NULL is used. You will see
NULL used to provide some initial pointer value. NULL is also used in the loop
condition of the while loop with the statement while(P != NULL) This
statement causes no problems. At no time is any attempt made to dereference our
hard working NULL pointer.
Please be aware that NULL is not a pointer to some special memory location that
sits around waiting to announce that the end of the road has been reached. NULL
is a constant, which equals zero. You could easily write all your linked list
programs and use the zero value to indicate the pointer value of the Next pointer
in the last node of the linked list.
// PROG4001.CPP
// This program creates and displays a linked list properly.
// The nodes are inserted in a LIFO sequence.
struct ListNode
{
int Data;
ListNode * Next;
};
void main()
{
clrscr();
ListNode * Temp = NULL;
ListNode * P = NULL;
int K, N;
cout << "Enter number of nodes in the list ===>> ";
cin >> N;
cout << endl;
PROG4001.CPP OUTPUT
The point is that we now have the need to get a pointer to the bottom node of the
existing list before it is possible to link anything there. In other words, we need
to traverse the list, and not until NULL is encountered, but rather until we reach
the last node. Now the whole point of using NULL is to identify the end of the
list. So what mechanism is available to identify the last node in the list?
The next program example shows the trick. This time traversing is done with the
statement while(Temp->Next != NULL). Our concern is not to compare
the traversing pointer, Temp, to NULL, but rather use Temp->Next, like the
program segment shows below.
Will that work? Check out program PROG4002.CPP and see if it works without
any problems. You may suspect that nothing should happen, but please have
patience I am trying hard to set the stage for this dereferencing problem.
// PROG4002.CPP
// This program creates a linked list in a LIFO sequence.
// A new node is linked at the bottom of the stack.
// The purpose of this program is to create a situation
// where dereferencing NULL might happen.
#include <iostream.h>
#include <stdlib.h>
#include <conio.h>
struct ListNode
{
int Data;
ListNode * Next;
};
PROG4002.CPP OUTPUT
Now here comes the basic question. What happens if the number of nodes
requested for the original list is zero? In the example above the original list
started with 8 nodes before one more node was added. Check to see what
happens to the execution sequence if the original list has zero nodes. The Temp
pointer is initialized to NULL. The whole loop business of creating a linked list
is ignored. A single node is created with full intentions to be linked at the bottom
of the “existing” list. The following loop will execute in an attempt to identify
the last node in the list.
What happens when this horrible sin is committed? This is difficult to predict.
Different compilers will behave differently and even the same compilers may not
react the same way depending on when NULL is dereferenced. Absolutely
nothing may happen or the computer may freeze up. The point is that some
unpredictable result will follow, and we are not in the business of writing
programs that have unpredictable results.
// PROG4003.CPP
// This program serves the same purpose as the previous program.
// The big difference is that this program checks to see if Temp
// might be NULL. This check prevents "dereferencing" the
// NULL pointer.
#include <iostream.h>
#include <stdlib.h>
#include <conio.h>
struct ListNode
{
int Data;
ListNode * Next;
};
void main()
{
clrscr();
ListNode * Temp = NULL;
ListNode * P = NULL;
ListNode * Head = NULL;
int K, N;
cout << "Enter number of nodes in the list ===>> ";
cin >> N;
cout << endl;
PROG4003.CPP OUTPUT
The output execution is identical to the previous program without any special
dereference protection. This is logical because both executions use an initial list
of eight nodes. The second program will work much better if the initial list is
zero. Now the program still does not work perfectly since it is not meant to add a
new node to a non-existing list. However, the main point is that the program will
not crash and result in peculiar computer behavior.
The second solution relies on the unique property of C++ which short circuits
compound conditions based on the value of the first condition. This topic was
handled in the first course in a special Boolean Algebra chapter. Look at the next
program example. Do you recognize the short circuit approach?
// PROG4004.CPP
// This program creates a linked in a FIFO sequence and then
// traverses and display all the node values except for the
// last one. This demonstrates how dereferencing can be
// avoided by using a compound Boolean condition that will
#include <iostream.h>
#include <stdlib.h>
#include <conio.h>
struct ListNode
{
int Data;
ListNode * Next;
};
void main()
{
clrscr();
ListNode * Front;
ListNode * Temp = NULL;
ListNode * P = NULL;
int K, N;
cout << "Enter number of nodes in the list ===>> ";
cin >> N;
cout << endl;
Several chapters back you were introduced to the stack abstract data structure.
You learned about the stack in an abstract sense, used the provided apstack library
of functions and also created a set of stack library functions for yourself. The
// PROG4005.CPP
// This program implements a stack data structure library
// of functions implemented with a dynamic linked list.
// The parameter style of using both an asterisk and an
// ampersand is the style expected for the AP examination.
#include <iostream.h>
#include <conio.h>
#include "BOOL.H"
struct StackNode
{
int Data;
StackNode * Next;
};
void main()
{
clrscr();
StackNode * TopPtr;
ClearStack(TopPtr);
FillStack(TopPtr);
ShowStack(TopPtr);
getch();
}
/////////////////////////////////////////////////////////////////
// function implementations that check the stack library
/////////////////////////////////////////////////////////////////
// stack function implementations
PROG4005.CPP OUTPUT
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1
Now that you are fresh from a thou shalt not dereference the NULL pointer
section, do you recognize that function Pop has some potential problems. Calling
Pop any time that the Top pointer equals NULL will result in dereferencing fun.
In the next program example you will see an improved Pop function and this
program also uses typedef to make the syntax of pointer parameter passing more
readable and user-friendly.
// PROG4006.CPP
// This program implements a stack data structure library
// of functions with a dynamic linked list like the last program.
// This version uses a typedef to simplify parameter declarations.
// Function Pop is improved by preventing potential dereferencing.
#include <iostream.h>
#include <conio.h>
#include "BOOL.H"
void main()
{
clrscr();
StackPtr TopPtr;
ClearStack(TopPtr);
FillStack(TopPtr);
ShowStack(TopPtr);
getch();
}
/////////////////////////////////////////////////////////////////
// function implementations that check the stack library
/////////////////////////////////////////////////////////////////
// stack function implementations
PROG4006.CPP OUTPUT
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1
struct StackNode
{
int Data;
StackNode * Next;
};
When it comes to creating a queue library of functions - using linked lists - you
will now realize that working with queues does not have the special complication
that was experienced with earlier queue functions. In our previous queue chapter
we implemented queues with a record that used an array to store the queue
elements, along with an integer value for the front and rear index of the queue.
This approach required special wrap-around techniques because the array is a
linear list. There exists no such problem with the approach shown in program
PROG4007.CPP. Note that the DeQueue function - like the Pop function - uses
the conditional statement to make sure that NULL is not dereferenced.
#include <iostream.h>
#include <conio.h>
#include "BOOL.H"
struct QueueNode
{
int Data;
QueueNode * Next;
};
void main()
{
clrscr();
QueuePtr FrontPtr;
QueuePtr BackPtr;
ClearQueue(FrontPtr,BackPtr);
FillQueue(FrontPtr,BackPtr);
ShowQueue(FrontPtr);
/////////////////////////////////////////////////////////////////
// queue function implementations
/////////////////////////////////////////////////////////////////
// function implementations that check the queue library
PROG4007.CPP OUTPUT
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
The stack and queue libraries shown here are by no means complete. I have not
presented as many functions as the apstack class and the apqueue class provide
for you. This is intentional because my interest is in showing how to use linked
lists for various situations. It is not the intention of this chapter to do an
exhaustive treatment on every data structure. Hopefully, you have the tools to
create more functions if you have the need for any special situation. C++ never
tried to be everything to everybody, but provide you with the tools to create any
data structure and functions needed for any purpose.
This linked approach allows the creation of very large data structures even though
your computer may not have any large chunks of memory floating around. This
is all very lovely, but with very little imagination this dynamic memory allocation
business can come down to a crashing halt in a very big hurry. The problem is
that once memory is allocated it is now reserved for whatever purpose it was
allocated. Now it may be used or not but no future new operation can take that
memory away. It is reserved, plain and simple.
Picture the following realistic situation. Some program retrieves data from an
external file and stores the data in a linked list for processing requirements. This
process continues and every time a different file is retrieved and stored in a
different linked list. At some point the processing with the first linked list is no
longer needed, but the allocated memory is still reserved. Sooner or later the
continuous process of allocating memory for a new linked list exhausts all the
available memory in the computer. At this point the computer will freeze up,
crash, courtesy reboot, or perform some other undesirable ritual. It is vitally
important to write programs that prevent such problems.
// PROG4008.CPP
// This program demonstrates how to deallocate memory dynamically.
#include <iostream.h>
#include <conio.h>
struct ListNode
{
int Data;
ListNode * Next;
};
void main()
{
clrscr();
ListPtr HeadPtr;
CreateList(HeadPtr);
ShowList(HeadPtr);
DeleteList(HeadPtr);
getch();
}
void ShowList(ListPtr P)
{
cout << endl << endl;
while (P != NULL)
{
cout << P->Data << " ";
P = P->Next;
}
}
PROG4008.CPP OUTPUT
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
MEMORY IS DEALLOCATED
Returning memory as was just shown is by no means the only method or the most
desirable method of returning memory. There are a variety of approaches to this
problem. In this section the main concern is to create awareness for a very
important aspect of dynamic memory allocation, which is controlling the
available memory for future use.
If you write the small programs required for each chapter’s lab assignment, it is
doubtful that you will find any problems. Problems, that is, by failing to return
allocated memory. Anytime that a program performs intensive data processing,
and especially when the data is retrieved from large external files, problems will
happen quickly. Follow this advise closely. It is a very common cause of
program crashes when students start to work on their large projects.
Now that you have a good linked list introduction behind you, the time has come
to add OOP to the story and see how it plays out. It is very well possible that
many students will take a so what attitude and conclude that object oriented
programming and linked lists are no big deal. Encapsulate the functions in the
class declaration, change the syntax by adding some funky scope resolution
operators and you are mostly there. If you feel that way, terrific. For many
students a longer, two-step approach is simpler.
Program PROG4009.CPP shows a stack library with the same set of functions as
the previous library. In this program example all the stack functions are
encapsulated in the MyStack class. This program example does not show any
new concepts. It combines the object oriented programming style, which you
learned earlier with the recent linked lists concepts in one program. Object
oriented programming is not meant to be something that is used now and then. It
is a style of programming that impacts program development tremendously at
every level. Keep in mind that there is a major difference between learning
computer science concepts and using computer science concepts. The object
// PROG4009.CPP
// This program encapsulates the stack library functions
// in a MyStack class.
#include <iostream.h>
#include <conio.h>
#include "BOOL.H"
class MyStack
{
public:
MyStack();
~MyStack();
void ClearStack();
bool EmptyStack();
void Push(int StackItem);
void Pop(int &StackItem);
private:
struct StackNode
{
int Data;
StackNode * Next;
};
StackNode * Top;
};
void main()
{
clrscr();
MyStack S;
FillStack(S);
ShowStack(S);
getch();
}
/////////////////////////////////////////////////////////////////
// implementations of the Mystack class member functions
MyStack::MyStack()
{
Top = NULL;
}
MyStack::~MyStack()
{
StackNode * Temp;
while (Top != NULL)
{
Temp = Top;
Top = Top->Next;
delete Temp;
}
}
void MyStack::ClearStack()
{
Top = NULL;
}
bool MyStack::EmptyStack()
{
PROG4009.CPP OUTPUT
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1
If you were strictly focused on the program code inside each stack function, then
you would see no difference. The logic and the program statements of Push, Pop
and the other stack functions do not change a class declaration. What does
change is that everything connected with the stack library is neatly contained
within the MyStack class declaration. With that approach we gain readability
and we also increase reliability. How is reliability improved?
Reliability is improved two ways and both are connected to some major aspects
of object oriented programming. The stack data structure will not work properly
unless a new stack starts out empty, ready for business. This means that a
function like makeEmpty or Clearstack or what you select to call it, makes sure
that a new stack is ready for business and the stack is totally empty.
This is all very nice, but it assumes that the person using the stack library is
clever enough to clear any newly created stack. With object oriented
At the tail end of the stack usage, when the object leaves its scope, the destructor
is called automatically. In previous object oriented programming chapters, it has
been difficult to demonstrate practical examples for using a destructor. Well, we
have arrived because the evils of forgetting to deallocate memory can now be
avoided automatically.
You just make sure that the destructor returns any allocated memory correctly and
the rest is done during program execution. In the previous program example a
special function was called to delete any unused memory. With the class
declaration of MyStack such a function is totally unnecessary. The MyStack
destructor does that job ever so nicely.
If you look closely at program PROG4010.CPP you may see some other
advantages as well that may not have been so apparent with the stack library.
// PROG4010.CPP
// This program encapsulates the queue library functions
// in a MyQueue class.
#include <iostream.h>
#include <conio.h>
#include "BOOL.H"
void main()
{
clrscr();
MyQueue Q;
FillQueue(Q);
ShowQueue(Q);
getch();
}
/////////////////////////////////////////////////////////////////
// implementations of the free functions
/////////////////////////////////////////////////////////////////
// implementations of the MyQueue class member functions
// note the special jobs that are performed by the constructor
// and the destructor to make the class more reliable.
MyQueue::MyQueue()
{
Front = NULL;
Rear = NULL;
First = true;
}
MyQueue::~MyQueue()
{
QueueNode * Temp;
while (Front != NULL)
{
Temp = Front;
Front = Front->Next;
delete Temp;
}
}
void MyQueue::ClearQueue()
{
Front = NULL;
Rear = NULL;
}
PROG4010.CPP OUTPUT
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
You remember that the queue has some extra baggage that makes queue
operations more complicated to implement than stack operations. First there is
the business that a Front pointer needs to anchor the first node of the linked list
while two other pointers build the list at the rear end. With a class it is easy to
store a Boolean First variable that is initialized to true by the constructor when a
new queue object is defined. The EnQueue function can change that variable to
false as soon as the first node is created. This is a clean operation and there are
no problems with how to deal with that first node.
Another advantage is the parameter business. The queue library presented earlier
in this chapter needed to pass both the Front pointer and the Rear pointer along
with a parameter for the queue data structure. This type of parameter passing is
quite messy and can be avoided so easily with a class. Both Front and Rear are
private data that is stored in the class along with First. Calling any of the
functions is now much simpler and the only parameters in sight are the ones used
to add a new element to the queue or to remove an element from a queue.
Every linked list that has been demonstrated so far, whether linked in a stack,
queue, or ordered sequence, had one pointer per node. Furthermore, every
pointer always linked in one direction. In other words, it is possible to traverse a
linked list from the front to the rear, or the top to the bottom, but not from the
rear back to the front. There are times when it is not practical to have a data
structure that has such inflexibility. You need the advantages that pointers allow
but at the same time you want the ability to move along a linked list in both
directions.
The solution? Well . . . it is a doubly linked list that comes to the rescue. There
is no requirement that limits a linked node to one pointer per node. In all the
previous linked list examples the single pointer in each node has been called
Next. If we place two pointer fields in a node, then we create the ability to link
the pointers in two opposite directions.
In the next program example we will not be using functions to demonstrate the
doubly linking process. Program PROG4011.CPP creates a linked list that can
be traversed in both directions.
// PROG4011.CPP
// This program demonstrates how to create a doubly linked list.
#include <iostream.h>
#include <conio.h>
struct DoubleNode
{
int Data;
DoubleNode * Fwrd;
DoubleNode * Back;
};
void main()
{
clrscr();
DoubleNode * FrontPtr; // points to front of list
DoubleNode * BackPtr; // points to back of the list
DoubleNode * Temp1; // pointer used to built the list
DoubleNode * Temp2; // pointer used to built the list
int K;
PROG4011.CPP OUTPUT
Did you perhaps not grasp every detail of the last program example? Well not to
worry because we will now look at each component of the program and digest the
logic in small chunks. Starting with the DoubleNode declaration you will notice
the same basic style of previous linked list declarations. The only difference is
that Next has been replaced with two pointers, Fwrd and Back.
struct DoubleNode
{
int Data;
DoubleNode * Fwrd;
DoubleNode * Back;
};
The first program segment of the program involves declaring pointers and setting
up the first node of the doubly linked list.
If you look at these steps closely you will realize that the logic of setting up the
first node of the linked list is the same as you learned with earlier singly linked
lists. There are more steps because both directions of the list need to have the end
of the list indicated by NULL. The diagram below illustrates the result of the
first ten program statements. Fr is the FrontPtr, Bk is the BackPtr, T1 is the
Temp1 pointer and T2 is the Temp2 pointer.
[Fr] [Bk]
[T1] A
[T2]
In the next program segment 8 nodes will be linked for a total of 9 nodes that will
store multiples of 100. Pay close attention to the linking details, which are quite a
bit more complex with a doubly linked list.
(11) Temp2 is assigned to Temp1. This sets up the pattern of the trailing
pointer that allows two nodes to be linked.
(12) Temp1 points to a newly created node
(13) The next integer value is stored in the Data field of the new node
(14) Temp1->Fwrd is assigned to NULL
(15) The BackPtr is shifted to the newly constructed back-end node
Did you understand why only one pointer is assigned to NULL inside the loop,
even though there are two sets of pointers? The list is created in a queue
sequence, which means that new nodes are linked at the back of the existing list.
The very first node requires that the Back pointer is assigned NULL, but after
that first node, it is only the Fwrd pointer that is assigned NULL with the
creation of each node. The diagram, on the next page, shows the result after the
loop body has executed one time.
If you followed the advice of these chapters, and the advice of your teacher, then
you know the importance of drawing pictures when you work with linked lists.
This was important when there was only one set of pointers. Now that pointers
go in two directions, the use of pictures and clear diagrams becomes absolutely
vital in the design of accurate programs.
The next time through the loop, Temp1 creates another node. Linking is made
possible because Temp2 is left behind at the previously created node. The
FrontPtr anchors one end of the list for traversing left to right and the BackPtr
anchors the other end of the list for traversing right to left.
[T2] [T1]
[Bk]
A B
[Fr]
Please keep in mind that the left-to-right and right-to-left linking is figuratively
speaking. The illustration is strictly an abstract, symbolic representation of the
pointer objects. It does not matter if the pointers point to the left side of the
object or the right side or the top or bottom of the drawing.
I am going to try to represent a more realistic view of the previous linked list.
The next page shows a diagram that represents memory cells. Each cell is
identified by a base 16 memory address like @7b00. The @ indicates address in
this illustration. Each memory cell is six bytes. Two bytes for each pointer and
two bytes for the integer data field.
A pointer may store a memory address, but this memory address in turn needs to
be stored somewhere. The space requirement for these two pointers is technically
only two bytes. The illustration, on the next page, may give the impression that
six bytes are needed which is not true.
@7b78
@7b90
Now to interpret this illustration properly and see what is going on here:
FrontPtr is located at @7b0cb and stores the address of the first node, @7b78.
At @7b78b the Fwrd pointer field stores @7b90 for the next node, 100 for
the Data field and @0000 for the Back pointer field, which is NULL.
At @7b90 the Fwrd pointer stores @0000, for the next node, which is NULL,
Now let us return to the final part of the doubly linked list program. Assume that
the loop has finished iterating. Nine nodes have been created, and linked in both
directions. There is now a starting point at both ends, and there is NULL at both
ends of the list to stop traversing in either direction. The remaining code
traverses the list in both directions to test the doubly linked list.
Computer science never ceases to amaze you. There are always other structures
and goodies to be added to your cerebral enjoyment. In this section we are going
to make an extremely minor change to the program, which created a linked list in
a queue sequence and alter its structure considerably.
Both the stack and the queue have been “dead-end” data structures. With the
implementations shown in this chapter you start at one end and you continue to
traverse the linked list until you bump into NULL. Once NULL is reached, the
ride is over and any additional rides require a fresh start from the head of the list.
What if we did away with NULL? Let us try not to point at NULL, but what if
the last pointer in the linked list pointed at the first object? This will create a
circular linked list.
Let us start by looking at a typical queue that was implemented with a linked list.
Node-A is the front node and Node-E is the last node, which points at NULL.
New nodes will be linked to Node-E and de-queueing will happen at the front,
starting with Node-A.
[F] A B C D E
Now we can alter the picture, ever so slightly, by changing one pointer. We can
take the Next pointer, in Node-E, and redirect it to the front. Our drawing now
looks as follows . . . and we have a lovely circular linked list.
It is true that we can change a linear linked list into a circular linked list with a
single program statement, such as P->Next = Front; assuming that P is
pointing at the last node in the linked list. However, such an approach defeats the
purpose of using a circular linked list or ring. A ring should be circular at all
times from the very beginning. Other linked structures have a linking sequence
that is established at the beginning and maintained throughout the existing of the
structure. The same should apply to a ring.
Is there a practical consideration for a circular linked list? Well, the days in our
week behave like a circular queue. We always go from Sunday to Saturday and
then start over again. How about a company that handles maintenance on a
rotating schedule? When maintenance is finished on the last component, it is
time to start back at the beginning. There is a feeling that you start over from the
beginning each time, but the reality behaves more like a circular queue than a
linear structure.
Now let us check out the implementation of a ring. First, start by recalling a
program that creates a linked list sequenced as a queue. Now comes a question to
mind, what about NULL or perhaps what about identifying the end of the list?
The list may be circular but any traversal needs a starting and ending point and
cannot aimlessly traverse without stopping.
When the ring is first started, both Start and End point to the same, single node.
Furthermore, you will observe the odd situation that the Next pointer of the first
node points to itself. Even with a single node the list is indeed circular.
The remainder of the class declaration follows the style of the MyStack and
MyQueue class with a library of functions. The ring will be processed with
functions like ClearRing, EmptyRing, EnRing and DeRing. A constructor
makes sure that the data members are properly initialized and the destructor
returns any allocated memory for future use.
// PROG4012.CPP
// This program demonstrate how to implement a circular
// linked list class. The class is called MyRing.
// The sequence of new nodes are linked in a queue sequence.
#include <iostream.h>
#include <conio.h>
#include "BOOL.H"
// class declaration of a circular linked list
class MyRing
{
public:
MyRing();
// constructor instantiates object with zero elements
~MyRing();
// destructor
void ClearRing();
// makes an existing ring empty
bool EmptyRing() const;
// return true is ring is empty and false otherwise
void EnRing(int RingItem);
// adds RingItem to the ring at the end node
void DeRing(int &RingItem);
// removes a ring element from the start node
private:
struct RingNode
{
int Data;
RingNode * Next;
};
RingNode * Start;
RingNode * End;
bool First;
};
void main()
{
clrscr();
MyRing R;
FillRing(R);
ShowRing(R);
}
/////////////////////////////////////////////////////////////////
// implementations of the free functions
/////////////////////////////////////////////////////////////////
// implementations of the MyRing class member functions
MyRing::MyRing()
{
Start = NULL;
End = NULL;
First = true;
}
MyRing::~MyRing()
{
void MyRing::ClearRing()
{
Start = NULL;
End = NULL;
}
PROG4012.CPP OUTPUT
1 2 3 4 5 6 7 8
1 2 3 4 5 6 7 8
In the real (computational) world, matrices with a huge quantity of variables are
frequently necessary. Many equations interact in the design of a car engine, the
lift-off of a space shuttle and the design of a new manufacturing plant.
Imagine that it is necessary to store values in a matrix of 1000 rows with 1000
columns. Furthermore, imagine you do not need more than 1%, or 10,000, values
that must be stored. In a conventional two-dimensional array implementation of a
matrix, there would be a terrible waste of memory space. You would reserve
1,000,000 spaces for the array and yet only 10,000 spaces are necessary for the
significant values. Such a situation calls for a different data structure than the
conventional N X N array.
Another problem with this sparse matrix business is that you do not really know
how many values will need to be stored and the anticipation of a large memory
requirement can waste so much useful memory space. What we need is dynamic
memory allocations. We need to use pointers.
This comes as no big surprise in a chapter on pointers, but how does one
implement a two-dimensional, sparse matrix, with pointers? The situation to
consider is a linked list of linked lists. You already know that an array of arrays
creates a two-dimensional data structure. Perhaps the same concept applies to
linked lists.
On the next page we will start by drawing some symbolic pictures to get a feel for
the type of data structure that helps in this situation. Keep in mind that this is not
the only solution for a sparse matrix, but the purpose in this section is to
demonstrate the flexibility of linked lists.
[Front]
RowNr NextRow NextCol ColNr Data NextCol ColNr Data NextCol
24 15 34.6785 54 117.0345
Our linked list of linked lists will be implemented with a Sparse class. The first
step of this case study is to identify the structures needed to store the data and the
various pointers that will be used. All this information is placed in the private
segment of the Sparse class.
Each individual node of the linked lists that stores actual data is handled by
ColNode, which has three fields for the ColNumber, the Data to be stored, and a
NextCol pointer. Each header node of the matrix needs to store the
RowNumber, a pointer, NextRow, to the header node of the next linked list and
a pointer, NextCol, to each column node. This job is handled by RowNode.
In this private segment there are also two pieces of information. The pointer that
has access to the header node of the first linked list row, called Head, is stored
First is set to true and creates the first two nodes in the data structure. Two
initial nodes may seem odd, but remember that this is a two-dimensional
structure. We need one header node, and then a second node that actually stores
data. One node by itself is not enough. The illustration below shows one
example of getting a sparse matrix started.
[Front]
RowNr NextRow NextCol ColNr Data NextCol
24 54 117.0345
class Sparse
{
private:
struct ColNode
{
int Data; // stores sparse matrix data
int ColNumber; // stores matrix column number
ColNode * NextCol; // pointer to next column node
};
struct RowNode
{
int RowNumber; // stores matrix row number
ColNode *NextCol; // pointer to next column node
RowNode *NextRow; // pointer to header node of next row
};
RowNode *Head;
bool First;
};
The second step of our case study looks at the necessary member functions of the
Sparse class. Now do not get confused. This stage is unconcerned how any of
these functions perform their job. Implementation comes later. The only focus
right now is on the actions that need to be performed by the Sparse class. Each
one of the actions will be listed with a brief description of its purpose.
Sparse()
The constructor initializes the Head pointer to NULL and sets the Boolean
variable First to true.
~Sparse()
EnterSparse()
This is the function responsible for gathering data from the program user, in this
case entered by the keyboard, and calling the appropriate functions to handle the
new information that is entered.
FirstNode()
If First is true then EnterSparse will call this function, which will create the
very first header node and well as the first column node that stores the first piece
of data entered from the keyboard.
InsertNode()
This is the workhorse function in the group. It is the responsibility of this
function to insure that new nodes are properly inserted in the existing matrix
structure. This job includes not only creating new column nodes for existing
rows, but also to create new header nodes for new rows.
DisplaySparse()
This function traverses the entire matrix structure and displays all the row values,
column values and data values that are stored. This function is considerably more
complex than the standard traversal that you have seen so far.
class Sparse
{
public:
Sparse(); // constructor
~Sparse(); // destructor
void EnterSparse(); // enter sparse values with keyboard
// creates the first header node of the sparse matrix
void FirstNode(int Row, int Col, int Value);
// inserts a new node in a linked list of linked lists
void InsertNode(int Row, int Col, int Value);
// displays all the values stored in the sparse matrix
void DisplaySparse();
private:
struct ColNode
Since this is a case study you might find it useful to look at a sample output of the
program. The stages shown so far are not close to generating any type of an
execution, but knowing what the goal of the program is will help to understand
the logic of some of the functions that follow. The main function is very simple.
Object S is defined and then EnterSparse and DisplaySparse are called.
void main()
{
Sparse S;
S.EnterSparse();
S.DisplaySparse();
}
The simplicity of the main function hides the real work performed by this
program. Several other functions will do the real work of this program, but these
functions are not called from the main function body.
The sample output execution shows that the matrix is linked in an ordered
sequence in two dimensions. The diagram, shown earlier, illustrated this
symbolically by ordering the row header nodes from top to bottom. The column
nodes were ordered from left to right. The sample output has intentionally
entered sequential numbers, which demonstrates that the output display does not
follow the order that the nodes were created, but the order in which the nodes are
linked in the sparse matrix data structure.
PROG4013.CPP OUTPUT
[ 1, 1] - 5555
[ 2, 3] - 2222
[ 5, 3] - 6666
[ 5, 5] - 1111
[ 5, 10] - 4444
[121,563] - 3333
[122,100] - 7777
void Sparse::EnterSparse()
{
cout << endl << endl;
cout << "CALLING ENTER SPARSE FUNCTION" << endl;
cout << endl;
int Row, Col;
int SparseValue;
bool Done;
cout << "Enter [0 <space> 0 <space> 0] to exit function"
<< endl;
do
{
cout << endl;
cout << "Enter Row <space> Col <space> SparseValue ===>> ";
The logic of the EnterSparse function is not at a high cerebral level. The
function consists of a loop that repeats while . . . (!Done); Inside the loop three
values are entered: row index, column index and the sparse matrix value. The
private data member First, checks to see if function FirstNode needs to be
called, and if it does, First is set to false, otherwise function InsertNode is
called. At this level of the sparse matrix program there is no appearance of any
dynamic linked list coding.
Most classes of any consequence have multiple constructors. Our class will have
to live with one modest default constructor. The constructor may be modest but it
does provide some basic and very important processing that will be performed
automatically every time a new Sparse object is instantiated.
Sparse::Sparse()
{
cout << endl << endl;
cout << "CALLING SPARSE CONSTRUCTOR" << endl;
Head = NULL;
First = true;
getch();
}
The time has come to start building some type of a linked list. Remember that
there are two types of nodes involved. First there are RowNodes, which act as a
header node for each individual linked list. Additionally the RowNodes store the
RowNumber for the entire linked list and provide a link to the next row, and
header node in the matrix. Second, there are ColNodes, which store the actual
sparse matrix values along with the ColNumber of the stored matrix value.
A diagram is drawn below that shows the minimum size of a linked list of linked
lists, when only one value has been entered. Note that there are two nodes, which
is one RowNode and one ColNode. It will be the job of the FirstNode function
to achieve this minimal, initial, start in creating the sparse matrix structure.
[Head]
RowNr NextRow NextCol ColNr Data NextCol
24 15 34.6785
[Temp]
With the construction of these two nodes the “easy” part of this program is now
finished. Now the time has comes to start thinking two-dimensionally. The real
essence of this whole program revolves around writing the InsertNode correctly.
This is not a piece a cake because there are many cases to consider in this process.
You learned about cases before, but that was in a one-dimensional environment.
Now the stakes are higher and along the way you will think back fondly of the
nice, simple linked lists of the previous chapter. All in all, the process is not that
bad. It is really a matter of carefully checking different situations and paying
close attention to details.
Function InsertNode is supplied Row and Col values via parameters to record
the location of the new value to store that is passed to SparseValue. Locally
there are six pointers declared. C1, C2, and C3 will be used with the column
node. R1, R2 and R3 will be used with row node linking.
To help explain this rather long function, text boxes are inserted with
explanations of the code that follows. Work through the function and follow the
logic in a slow step by step manner. It will be particularly helpful if you simulate
an actual linked list and draw each node as it is inserted. Make sure to use a good
distribution of data that will insert nodes in all types to situations. In other words,
check the function for all the different possible cases.
/////////////////////////////////////////////////////////////////
// Space is allocated for a new column node.
// Both SparseValue and ColNumber values are stored in this node.
// The NextCol pointer is assigned NULL.
// This node will be called the Insertion Node for
// identification.
////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////
// The first consideration is to determine if the new node needs
// to be placed ahead of the existing first header node. This
// situation occurs if the Row value is less than the RowNumber
// value of the first header node. If this is true, a new header
// node needs to be created, which will be linked to the
// insertion node and the previous first header node.
/////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////
// If the insertion node does not create a new linked list before
// the first header node, a search needs to be performed to first
// identify the proper row location of the insertion node.
// Temporary pointer R2 starts at the Head pointer and traverses
// ”downward” as the Row values is greater than the RowNumber of
// each header node along the traversal. Note that dereferencing
// NULL is avoided by the compound decisions, which will
// ”short circuit” in the event that R2 == NULL.
////////////////////////////////////////////////////////////////
else
{
R2 = Head;
while (R2 != NULL && Row > R2->RowNumber)
// traverse to find proper row location
{
R3 = R2;
R2 = R2->NextRow;
}
/////////////////////////////////////////////////////////////////
// When the loop exits, it is possible that Row is equal to the
// current RowNumber. In that case it is not necessary to create
// a new header node. A check is made to see if this is true by
// comparing Row and the current RowNumber.
/////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////
{
C2 = R2->NextCol;
if (Col < C2->ColNumber)
{
R2->NextCol = C1;
C1->NextCol = C2;
}
/////////////////////////////////////////////////////////////////
// If the insertion node does not link at the head of the current
// linked list, a search must be performed to find the proper
// column position. The C2 pointer traverses with a while loop
// as long as Col is greater than ColNumber. Note that once
// again short-circuiting prevents dereference problems.
// Another pointer, C3 trails C2 one node behind. When C2
stops // there are now two pointers at the location for the
insertion // node. C3 links to the insertion node C1 and the
insertion
// node links to C2.
////////////////////////////////////////////////////////////////
else
{
while (C2 != NULL && Col > C2->ColNumber)
// find proper column insertion position
{
C3 = C2;
C2 = C2->NextCol;
}
C3->NextCol = C1;
C1->NextCol = C2;
}
}
////////////////////////////////////////////////////////////////
// It is easy to think that the job is done. However, there
// exists one more very important case. This is the situation
// of placing the insertion node neither before the first header
// node nor in a linked list with an existing header node. In
else
{
R1 = new RowNode;
R1->RowNumber = Row;
R3->NextRow = R1;
R1->NextRow = R2;
R1->NextCol = C1;
}
}
}
InsertNode Warning
void Sparse::DisplaySparse()
{
cout << endl << endl;
cout << "CALLING DISPLAY SPARSE FUNCTION" << endl;
cout << endl;
int Row, Col;
int Value;
RowNode * R;
ColNode * C;
R = Head;
while (R != NULL)
{
C = R->NextCol;
while (C != NULL)
{
Row = R->RowNumber;
Col = C->ColNumber;
Value = C->Data;
cout << "[" << setw(3) << Row << "," << setw(3) << Col
<< "] - " << setw(5) << Value << endl;
C = C->NextCol;
}
R = R->NextRow;
}
}
In the process of showing one small chunk after another, you may have lost
overlook over the entire program. The final step will show the complete program
and execution in one continuous presentation. Are you ready to write this
program on your own? Could you write additional functions for this program?
// PROG4013.CPP
// This program demonstrates one implementation of a "sparse"
// matrix with a linked lists of linked lists.
#include <iostream.h>
#include <conio.h>
#include <iomanip.h>
#include "BOOL.H"
private:
struct ColNode
{
int Data; // stores sparse matrix data
int ColNumber; // stores matrix column number
ColNode * NextCol; // pointer to next column node
};
struct RowNode
{
int RowNumber; // stores matrix row number
ColNode * NextCol; // pointer to next column node
RowNode * NextRow; // pointer to next header node
};
RowNode * Head;
bool First;
void main()
{
clrscr();
Sparse S;
S.EnterSparse();
S.DisplaySparse();
}
/////////////////////////////////////////////////////////////////
// implementations of the Sparse class member functions
Sparse::Sparse()
{
cout << endl << endl;
cout << "CALLING SPARSE CONSTRUCTOR" << endl;
Head = NULL;
First = true;
getch();
}
Sparse::~Sparse()
{
cout << endl << endl;
cout << "CALLING SPARSE DESTRUCTOR" << endl;
getch();
}
void Sparse::EnterSparse()
{
cout << endl << endl;
cout << "CALLING ENTER SPARSE FUNCTION" << endl;
cout << endl;
int Row, Col;
int SparseValue;
bool Done;
cout << "Enter [0 <space> 0 <space> 0] to exit function"
<< endl;
do
{
cout << endl;
cout << "Enter Row <space> Col <space> SparseValue ===>> ";
cin >> Row >> Col >> SparseValue;
Done = Row == 0;
if (!Done)
{
if (First)
{
FirstNode(Row,Col,SparseValue);
First = false;
C3->NextCol = C1;
C1->NextCol = C2;
}
}
else
{
R1 = new RowNode;
R1->RowNumber = Row;
R3->NextRow = R1;
R1->NextRow = R2;
R1->NextCol = C1;
}
}
}
void Sparse::DisplaySparse()
{
cout << endl << endl;
cout << "CALLING DISPLAY SPARSE FUNCTION" << endl;
cout << endl;
int Row, Col;
int Value;
RowNode * R;
ColNode * C;
R = Head;
while (R != NULL)
{
C = R->NextCol;
while (C != NULL)
{
Row = R->RowNumber;
Col = C->ColNumber;
Value = C->Data;
cout << "[" << setw(3) << Row << "," << setw(3) << Col
<< "] - " << setw(5) << Value << endl;
C = C->NextCol;
}
R = R->NextRow;
}
}
[ 1, 1] - 5555
[ 2, 3] - 2222
[ 5, 3] - 6666
[ 5, 5] - 1111
[ 5, 10] - 4444
[121,563] - 3333
[122,100] - 7777
This chapter finishes with a set of exercises in the same manner as the previous
chapter. Once again you will be shown the diagram of some initial linked list. This
is followed by a set of program statements and you select the appearance of the
linked list after the execution of the program statements are completed.
DblNode * P1;
DblNode * P2;
DblNode * P3;
char Temp;
[P1] [P2]
B F K Q W
How is the data structure pictured after the program segment below is executed?
[P1] [P2]
(A) B F K Q W
[P1] [P2]
(B) B F K Q W
[P1] [P2]
(C) B F K Q W
[P2] [P1]
(D) B F K Q W
[P1] [P2]
B F K Q W
How is the data structure pictured after the program segment below is executed?
[P1] [P2]
(A) B F K Q W
[P1] [P2]
(B) B F K Q W
[P1] [P2]
(C) B F K Q W
[P2] [P1]
(D) B F K Q W
[P1] [P2]
B F K Q W
How is the data structure pictured after the program segment below is executed?
[P1] [P2]
(A) B F K Q W
[P1] [P2]
(B) B Q K F W
[P1] [P2]
(C) B Q K F W
[P1] [P2]
(D) W Q K F B
[P1] [P2]
B F K Q W
How is the data structure pictured after the program segment below is executed?
[P1] [P2]
(A) B F K Q W
[P1] [P2]
(B) B Q K F W
[P1] [P2]
(C) B Q K F W
[P1] [P2]
(D) W Q K F B
[P1] [P2]
B F K Q W
How is the data structure pictured after the program segment below is executed?
(A) B F K Q W
(B) W F K Q B
(C) B Q K F W
(D) W Q K F B
[P1] [P2]
B F K Q W
How is the data structure pictured after the program segment below is executed?
Temp = P1->Info;
P1->Info = P2->Info;
P2->Info = Temp;
P1 = P1->Frwd;
P2 = P2->Back;
}
(A) B F K Q W
(B) W F K Q B
(C) B Q K F W
(D) W Q K F B
[P1] [P2]
B F K Q W
How is the data structure pictured after the program segment below is executed?
(A) B F K Q W
(B) W F K Q B
(C) B Q K F W
(D) W Q K F B
[P1] [P2]
B F K Q W
How is the data structure pictured after the program segment below is executed?
P3 = new DoubleNode;
P3->Info = 'M';
while(P3->Info > P1->Info)
P1 = P1->Frwd;
P1->Back->Frwd = P3;
P3->Back = P1->Back;
P3->Frwd = P1->Frwd;
P1->Frwd->Back = P3;
delete P1;
(A) B F K M W
(B) B F M Q W
(C) B F K Q M
(D) B M K Q W
[P1] [P2]
B F K Q W
How is the data structure pictured after the program segment below is executed?
P1 = P2->Back->Back->Frwd
P1->Back->Frwd = P1->Frwd;
P1->Frwd->Back = P1->Back;
delete P1;
(A) B K Q W
(B) B F Q W
(C) B F K W
(D) B F K Q
[P1] [P2]
B F K Q W
How is the data structure pictured after the program segment below is executed?
P1->Frwd->Back = Null;
P1->Frwd = Null;
P2->Back->Frwd = P1;
P1->Back = P2->Back;
delete P2;
(A) F K Q W
(B) F K Q B
(C) B F K Q
(D) F K B W
Exercise Answers
1–A 6–D
2–A 7–C
3–B 8–A
4–B 9–C
5–C 10 – B