0% found this document useful (0 votes)
21 views51 pages

04-Chap 04-LISTS

Uploaded by

Dinesh Kavoor
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
21 views51 pages

04-Chap 04-LISTS

Uploaded by

Dinesh Kavoor
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 51

CHAPTER 4

LISTS

4.1 POINTERS

In the previous chapters we studied the representation of simple data structures using an
array and a sequential mapping. These representations stored successive elements of the
data object a fixed distance apart. Thus,

If the /th element in a queue was at location LoCi then the (i + 1 )th element was at
location (Loci + c) % MAX-QUEUE-SIZE for the circular representation.
If the top element of a stack was at location, Loct^p, then the element beneath it
was at location Loc^op - c.

These sequential representations were adequate for many operations including insertion
or deletion of elements from a stack or queue. However, when we use a sequential map­
ping for ordered lists, operations such as insertion and deletion of arbitrary elements
become expensive. For example, consider the following alphabetized list of three-letter
English words ending in at:

(bat, cat, sat, vat)

We would like to add the word mat to this list. If we store this list in an array, then we

135
136 Lists

must move sat and vat one position to the right before we insert mat. Similarly, if we
want to remove the word cat from the list, we must move sat and vat one position to the
left to maintain our sequential representation. In the general case, arbitrary insertion and
deletion from arrays can be very time-consuming.
We encountered an additional difficulty with sequential representations when we
used several ordered lists of varying sizes. By storing each list in a different array of
maximum size, we could waste storage. However, by maintaining the lists in a single
array, we might need to move data frequently. We observed this dilemma when we
represented multiple stacks, queues, polynomials, and sparse matrices. These data types
are examples of ordered lists. Polynomials are ordered by exponent, while matrices are
ordered by rows and columns. In this chapter, we present an alternate representation for
ordered lists that reduces the time needed for arbitrary insertion and deletion.
We can attain an elegant solution to the problem of data movement in sequential
representations by using linked representations. Unlike a sequential representation
where successive items of a list are located a fixed distance apart, in a linked representa­
tion these items may be placed anywhere in memory. In other words, in a sequential
representation the order of elements is the same as in the ordered list, while in a linked
representation these two sequences need not be the same. To access elements of the list
in the correct order with each element, we store the address, or location, of the next ele­
ment in that list. Thus, associated with each list element is a node which contains both a
data component and a pointer to the next item in the list. The pointers are often called
links.
C provides extensive support for pointers. In Chapter 2 we observed that an array
element, a[Z], is viewed as a pointer to the location containing the zth element of array a.
Actually, for any type Tin C there is a corresponding type pointer-to-T. The actual value
of a pointer type is an address of memory. The two most important operators used with
the pointer type are:

& the address operator


* the dereferencing (or indirection) operator

If we have the declaration:

int i, *pi ;

then i is an integer variable and pi is a pointer to an integer. If we say:

pi = &i;

then &i returns the address of i and assigns it as the value of pi. To assign a value to i we
can say:

1 10;
Pointers 137

or
*pi 10;

In both cases the integer 10 is stored as the value of i. In the second case, the * in front
of the pointer pi causes it to be dereferenced, by which we mean that instead of storing
10 into the pointer, 10 is stored into the location pointed at by the pointer pi.
There are other operations we can do on pointers. We may assign a pointer to a
variable of type pointer. Since a pointer is just a nonnegative integer number, C allows
us to perform arithmetic operations such as addition, subtraction, multiplication, and
division, on pointers. We also can determine if one pointer is greater than, less than, or
equal to another, and we can convert pointers explicitly to integers.
The size of a pointer can be different on different computers. In some cases the
size of a pointer on a computer can vary. For example, the size of a pointer to a char can
be longer than a pointer to a float. C has a special value that it treats as a null pointer.
The null pointer points to no object or function. Typically the null pointer is represented
by the integer 0. There is a macro called NULL which is defined to be this constant. The
macro is defined either in stddefh for ANSI C or in stdio.h for K&R C. The null pointer
can be used in relational expressions, where it is interpreted as false. Therefore, to test
for the null pointer in C we can say:

if (pi == NULL)
or more simply:
if dpi)

4.1.1 Pointers Can Be Dangerous

In this chapter we will see that by using pointers we can attain a high degree of flexibil­
ity and efficiency. But pointers can be dangerous as well. When programming in C, it is
a wise practice to set all pointers to NULL when they are not actually pointing to an
object. This makes it less likely that you will attempt to access an area of memory that is
either out of range of your program or that does not contain a pointer reference to a legi­
timate object. On some computers, it is possible to dereference the null pointer and the
result is NULL, permitting execution to continue. On other computers, the result is what­
ever the bits are in location zero, often producing a serious error.
Another wise programming tactic is to use explicit type casts when converting
between pointer types. For example:

pi = malloc(sizeof(int) ); /ssign
*
a to pi a pointer to int
/
*
float *) ) pi? /
pf = ((float *
casts an int pointer to a float pointer
/
*

Another area of concern is that in many systems, pointers have the same size as
type int. Since int is the default type specifier, some programmers omit the return type
when defining a function. The return type defaults to int which can later be interpreted
138 Lists

as a pointer. This has proven to be a dangerous practice on some computers and the pro­
grammer is urged to define explicit return types for functions.

4.1.2 Using Dynamically Allocated Storage

In your program you may wish to acquire space in which you will store infoiTnation.
When you write your program you may not know how much space you will need, nor do
you wish to allocate some very large area that may never be required. To solve this
problem C provides a mechanism, called a heap, for allocating storage at run-time.
Whenever you need a new area of memory, you may call a function, malloc, and request
the amount you need. If the memory is available, a pointer to the start of an area of
memory of the required size is returned. At a later time when you no longer need an area
of memory, you may free it by calling another function, free, and return the area of
memory to the system. Once an area of memory is freed, it is improper to use it. Pro­
gram 4.1 shows how we might allocate and deallocate storage to pointer variables.

*
int i, pi;
float f, *pf ;
pi = (int ★ ) malloc(sizeof(int));
pf - (float *) malloc(sizeof(float));
*pi 1024;
*pf = 3.14;

printf("an integer = %d, a float pi, *pf) ;
free(pi);
free(pf);

Program 4.1: Allocation and deallocation of pointers

The call to malloc includes a parameter that determines the size of storage
required to hold the int or the float. The result is a pointer to the first address of a
storage area of the proper size. The type of the result can vary. On some systems the
result of malloc is a char *, a pointer to a char. However, those who use ANSI C will
find that the result is void *. The notation {int *) and (float *) are type cast expressions.
They transform the resulting pointer into a pointer to the correct type. The pointer is
then assigned to the proper pointer variable. The free function deallocates an area of
memory previously allocated by malloc. In some versions of C, free expects an argu­
ment that is a char *, while ANSI C expects void *. However, the casting of the argu­
ment is generally omitted in the call to free.
In Program 4.1 if we insert the line:

pf = (float * ) malloc(sizeof(float));
Pointers 139

immediately after the printf statement, then the pointer to the storage used to hold the
value 3.14 has disappeared. Now there is no way to retrieve this storage. This is an
example of a dangling reference. Whenever all pointers to a dynamically allocated area
of storage are lost, the storage is lost to the program. As we examine programs that
make use of pointers and dynamic storage, we will make it a point to always return
storage after we no longer need it.

4.2 SINGLY LINKED LISTS

Linked lists are drawn as an ordered sequence of nodes with links represented as arrows
(Figure 4.1). The name of the pointer to the first node in the list is the name of the list.
Thus, the list of Figure 4.1 is called ptr. Notice that we do not explicitly put in the
values of the pointers, but simply draw arrows to indicate that they are there. We do this
to reinforce the facts that;

(1) the nodes do not reside in sequential locations


(2) the locations of the nodes may change on different runs

When we write a program that works with lists, we almost never look for a specific
address except when we test for the end of the list.

ptr

L bat cat > sat uat NULL

Figure 4.1 : Usual way to draw a linked list

Let us now see why it is easier to make arbitrary insertions and deletions using a
linked list rather than a sequential list. To insert the word mat between cat and sat, we
must:

(1) Get a node that is currently unused; let its address be paddr.
(2) Set the data field of this node to mat.
(3) Set paddr's link field to point to the address found in the link field of the node con­
taining cat.
(4) Set the link field of the node containing cat to point to paddr.
140 Lists

Figure 4.2 shows how the list changes after we insert mat. The dashed line out of
the node containing cat is the old link, while the solid line shows the new link. Notice
that when we insert mat we do not move any elements that are already in the list. Thus,
we have overcome the need to move data, but we have the additional storage needed for
the link field. As we will see, this is not too severe a penalty.

ptr
L
bat cat * sat uat NULL

c mat

Figure 4.2 : Insert mat after cat

Now suppose that we want to delete mat from the list. We only need to find the
element that immediately precedes mat, which is cat, and set its link field to point to
mat's link field (Figure 4.3). We have not moved any data, and although the link field of
mat still points to sat, mat is no longer in the list.

ptr
L 1
bat cat nat sat uat NULL

Figure 4.3 : Delete mat from list

From this brief discussion of linked lists, we see that we need the following capabilities
to make linked representations possible:

(1) A mechanism for defining a node’s structure, that is, the fields it contains. We use
self-referential structures, discussed in Section 2.2, to do this.
(2) A way to create new nodes when we need them. The malloc function handles this
operation.
(3) A way to remove nodes that we no longer need. The free function handles this
operation.
We will present several small examples to show how to create and use linked lists in C.

Example 4.1 [List of words ending in at}: To create a linked list of words, we first
define a node structure for the list. This structure specifies the type of each of the fields.
Singly Linked Lists 141

From our previous discussion we know that our structure must contain a character array
and a pointer to the next node. The necessary declarations are:

typedef struct list—node * list—pointer;


typedef struct list—node {
char data[4];
list—pointer link;
};
list—pointer ptr = NULL;

These declarations contain an example of a self-referential structure. Notice that we


have defined the pointer fist - pointer) to the struct before we defined the struct
fist - node). C allows us to create a pointer to a type that does not yet exist because oth­
erwise we would face a paradox: we cannot define a pointer to a nonexistent type, but to
define the new type we must include a pointer to the type.
After defining the node’s structure, we create a new empty list. This is accom­
plished by the statement:

list—pointer ptr = NULL;

This statement indicates that we have a new list called ptr. Remember that ptr
contains the address of the start of the list. Since the new list is initially empty, its start­
ing address is zero. Therefore, we use the reserved word NULL to signify this condition.
We also can use an IS-EMPTY macro to test for an empty list:

#define IS—EMPTY(ptr) {!(ptr))

To create new nodes for our list we use the malloc (memory allocation) function
provided in <alloc.h>. We would apply this function as follows to obtain a new node
for our list:

ptr (list—pointer)malloc(sizeof(list—node));

From the available memory, malloc obtains a storage block large enough to hold
struct list-node. We use sizeof to furnish malloc with the required block size. Since our
only access to this block is through its starting address, we type cast the address to type
pointer to list -node. (The type cast is unnecessary in ANSI C, but we include it here
for portability.) We then assign this pointer to the variable ptr.
We are now ready to assign values to the fields of the node. This introduces a new
operator, ->. If e is a pointer to a structure that contains the field name, then e->name
is a shorthand way of writing the expression f^e).name. The -> operator is referred to
as the structure member operator, and its use is preferred when one has a pointer to a
struct rather than the * and dot notation.
142 Lists

To place the word bat into our list we use the statements:

strcpy (ptr->data, "bat" ) ;


ptr->link NULL;

These statements create the list illustrated in Figure 4.4. Notice that the node has a null
link field because there is no next node in the list. □

^-address of -* |«- p'tr->data ■» | *■ ptr->linl<


first node
> b a t \0 NULL

ptr

Figure 4.4: Referencing the fields of a node

Example 4.2 [Two-node linked list]’. We want to create a linked list of integers. The
node structure is defined as:

list —pointer;
typedef struct list—node *
typedef struct list—node {
int data;
list—pointer link;
} ;
list—pointer ptr = NULL;

A linked list with two nodes is created by function create2 (Program 4.2). We set
the data field of the first node to 10 and that of the second to 20. The variable first is a
pointer to the first node; second is a pointer to the second node. Notice that the link field
of the first node is set to point to the second node, while the link field of the second node
is bilJLL. The variable first, which is the pointer to the start of the list, is returned by
create2. Figure 4.5 shows the resulting list structure. □

Example 4.3 [List insertion]’. Let ptr be a pointer to a linked list as in Example 4.2.
Assume that we want to insert a node with a data field of 50 after some arbitrary node.
Function insert (Program 4.3) accomplishes this task. In this function, we pass in two
pointer variables. The first variable, ptr, is the pointer to the first node in the list. If this
variable contains a null address (i.e., there are no nodes in the list), we want to change
ptr so that it points to the node with 50 in its data field. This means that we must pass in
the address of ptr. This is why we use the declaration list-pointer "^ptr. Since the
address of the second pointer, node, does not change, we do not need to pass in its
address as a parameter. A typical function call would be insert {&ptr, node); where ptr
points to the start of the list and node points to the new node.
Singly Linked Lists 143

list—pointer create2()
{
*
/ create a linked list with two nodes */
list—pointer first, second;
first (list—pointer)malloc(sizeof(list—node));
second (list—pointer)malloc(sizeof (list—node));
second->link = NULL;
second->data 20;
first->data = 10;
first->link = second;
return first;
}

Program 4.2: Create a two-node list

ptr

u 10 20 NULL

Figure 4.5 : A two-node list

The function insert uses an if • • • else statement to distinguish between empty and
nonempty lists. For an empty list, we set temp's link field to NULL and change the value
of ptr to the address of temp. For a nonempty list, we insert the temp node between node
and the node pointed to by its link field. Figure 4.6 shows the list from Figure 4.5 after
we insert temp between the first and second nodes.
Notice that we have added a new macro, IS-FULL, that allows us to determine if
we have used all available memory. This macro is used in conjunction with malloc,
which returns NULL if there is no more memory. It is defined as;

#define IS—FULL(ptr) {!(ptr)) □

Example 4.4 [List deletion}-. Deleting an arbitrary node from a list is slightly more com­
plicated than insertion because deletion depends on the location of the node. Assume
that we have three pointers; ptr points to the start of the list, node points to the node that
we wish to delete, and trail points to the node that precedes it. Figures 4.7 and 4.8 show
two examples. In Figure 4.7, the node to be deleted is the first node in the list. This
means that we must permanently change the starting address of ptr. In Figure 4.8, since
node is not the first node, we simply change the link field in trail to point to the link field
144 Lists

void insert(list—pointer *ptr, list—pointer node)


{
/
* insert a new node with data 50 into the list
ptr after node */
list—pointer temp;
temp = (list—pointer)malloc{sizeof(list—node));
if (IS_FULL(temp)){
fprintf{stderr, "The memory is full\n");
exit(1);
}
temp->data = 50;
if (
*
ptr) {
temp->link node—>link;
node->link temp;
}
else {
temp->link - NULL ;
*ptr = temp;
}
}

Program 4.3: Simple insert into front of list

ptr
h
10 > 20 MULL
r
*
node

r
► 50

tenp

Figure 4.6 : Two node list after the function call insert(&ptr, ptr);

in node.
An arbitrary node is deleted from a linked list by function delete (Program 4.4). In
addition to changing the link fields, or the value of delete also returns the space that
was allocated to the deleted node to the system memory. To accomplish this task, we use
free. □

Example 4.5 [Printing out a list}". Program 4.5 prints the data fields of the nodes in a
Singly Linked Lists 145

ptr node trail NULL ptr


i
11
10 50 20 NULL 50 20 NULL

(a) before deletion (b) after deletion

Figure 4.7 : List after the function call deleTe(&ptr, NULL, ptr);

ptr tra i1 node ptr


11 i i

10 50 20 NULL 10 20 NULL

(a) before deletion (b) after deletion

Figure 4.8 : List after the function call delete(&ptr, ptr, ptr->link);

void delete{list—pointer *ptr, list—pointer trail,


list—pointer node)
{
/
* delete node from the list, trail is the preceding node
ptr is the head of the list */
if (trail)
trail->link = node->link;
else
*ptr ptr )->link;
(*
free(node);
}

Program 4.4: Deletion from a list

list. To do this we first print out the contents of ptr's data field, then we replace ptr with
the address in its link field. We continue printing out the data field and moving to the
next node until we reach the end of the list. □
146 Lists

void print—list(list—pointer ptr)


{
printf{"The list contains: •’) ;
for (; ptr; ptr = ptr->link)
II Q.
printf( •€4d", ptr->data) ;
printf("\n") ;
}

Program 4.5: Printing a list

EXERCISES

1. Rewrite delete (Program 4.4) so that it uses only two pointers, ptr and trail.
2. Assume that we have a list of integers as in Example 4.2. Create a function that
searches for an integer, num. If num is in the list, the function should return a
pointer to the node that contains num. Otherwise it should return NULL.
3. Write a function that deletes a node containing a number, num, from a list. Use the
search function (Exercise 2) to determine if num is in the list.
4. Write a function, length, that counts the number of nodes in a list.
5. Let p be a pointer to the first node in a singly linked list. Write a procedure to
delete every other node beginning with node p (i.e., the first, third, fifth, etc. nodes
of the list are deleted). What is the time complexity of your algorithm?
6. Let X = (xi,X2, • . •,.x„) and y = (yi,y2’ • • ‘^ym) be two linked lists. Assume that
in each list, the nodes are in nondecreasing order of their data field values. Write
an algorithm to merge the two lists together to obtain a new linked list z in which
the nodes are also in this order. Following the merge, x and y do not exist as indi­
vidual lists. Each node initially in x or y is now in z. No additional nodes may be
used. What is the time complexity of your algorithm?
7. Let list I = (xy, X2, • • • , x„) and list 2 = (yi, y 2^ ' ’ ’ , ym)- Write a function to
merge the two lists together to obtain the linked list, list^ - (x ।, y 1, X2, y2^ ' ’'
+H • ,x„) if m < n; and ^3 = (xi,yi,X2,y2, ‘ ‘ +h ,
x^) if AH > n.
8. It is possible to traverse a linked list in both directions (i.e., left to right and res­
tricted right-to-left) by reversing the links during the left-to-right traversal. A pos­
sible configuration for the list, ptr, under this scheme is given in Figure 4.9. The
variable ptr points to the node currently being examined and left to the node on its
left. Note that all nodes to the left of ptr have their links reversed.
Singly Linked Lists 147

left ptr

1 I
NULL > NULL

Figure 4.9: Configuration for reversing links

(a) Write a function to move ptr to the right n nodes from a given position
p?r).
(b) Write a function to move ptr to the left n nodes from any given position
(left, ptr).

4.3 DYNAMICALLY LINKED STACKS AND QUEUES

Previously we represented stacks and queues sequentially. Such a representation proved


efficient if we had only one stack or one queue. However, when several stacks and
queues coexisted, there was no efficient way to represent them sequentially. Figure 4.10
shows a linked stack and a linked queue. Notice that the direction of links for both the
stack and the queue facilitate easy insertion and deletion of nodes. In the case of Figure
4.10(a), we can easily add or delete a node from the top of the stack. In the case of Fig­
ure 4.10(b), we can easily add a node to the rear of the queue and add or delete a node at
the front, although we normally will not add items to the front of a queue.

top

I element link
—K NULL

(a) Linked Stack

front rear

1 e lenent link i
MULL

(b) linked queue

Figure 4.10 : Linked stack and queue

If we wish to represent n stacks simultaneously, we begin with the declarations:


148 Lists

#define MAX—STACKS 10 /
maximum
* number of stacks
/
*
typedef struct {
int key;
*
/ other fields */
} element;
typedef struct stack *stack—pointer;
typedef struct stack {
element item;
stack—pointer link;
};
stack—pointer top[MAX—STACKS];

We assume that the initial condition for the stacks is:

top [z ] = NUm Q<i < MAX-STACKS

and the boundary conditions are:

fo/? [/ ] = NULL iff the zth stack is empty


and
IS - FULL (temp) iff the memory is full

Functions add (Program 4.6) and delete (Program 4.7) add and delete items
to/from a stack. The code for each is straightforward. In both functions, we pass in the
address of top so that top will point to the element that resides at the top of the stack.
Function add creates a new node, temp, and places item in the item field and top in the
link field. The variable top is then changed to point to temp. A typical function call
would be add(&top [stack-no },item}. Function delete returns the item and changes top
to point to the address contained in its link field. The removed node is then returned to
system memory. A typical function call would be item = delete (&top [stack-no ]);
To represent m queues simultaneously, we begin with the declarations:

#define MAX-QUEUES 10 / ★ maximum number of queues */


typedef struct queue *queue—pointer;
typedef struct queue {
element item;
queue—pointer link;
};
queue—pointer front[MAX—QUEUES] , rear[MAX—QUEUES];

We assume that the initial condition for the queues is:

front[i] = NULL, ()<i< MAX-QUEUES


Dynamically Linked Stacks And Queues 149

void add{stack—pointer •k top, element item)


{
*
/ add an element to the top of the stack */
stack-pointer temp =
(stack—pointer) malloc(sizeof (stack));
if (IS-FULL(temp) ) {
fprintf(stderr, "The memory is full\n");
exit(1);
}
temp->item = item;
temp->link = * top;
*top t emp ;
}

Program 4.6: Add to a linked stack

element delete{stack—pointer * top) {


*
/ delete an element from the stack */
stack—pointer temp = * top;
element item;
if (IS-EMPTY(temp) ) {
fprintf(stderr, "The stack is empty\n");
exit(1);
}
item temp->Item;
*top = temp->link;
free(temp);
return item;
}

Program 4.7: Delete from a linked stack

and the boundary conditions are:

front[i] = NULL iff the /th queue is empty


and
IS-FVLL{temp} iff the memory is full
150 Lists

Functions addq (Program 4.8) and deleteq (Program 4.9) implement the add and
delete operations for multiple queues. Function addq is more complex than add because
we must check for an empty queue. If the queue is empty, we change front to point to
the new node; otherwise we change rear's link field to point to the new node. In either
case, we then change rear to point to the new node. Function deleteq is similar to delete
since we are removing the node that is currently at the start of the list. Typical function
calls would be addq{&front, &rear, itemf and item = deleteq{&front};.
The solution presented above to the n-stack, m-queue problem is both computa­
tionally and conceptually simple. We no longer need to shift stacks or queues to make
space. Computation can proceed as long as there is memory available. Although we
need additional space for the link field, the use of linked lists makes sense because the
overhead incurred by the storage of the links is overridden by (1) the ability to represent
lists in a simple way, and (2) the reduced computing time required by linked representa­
tions.

EXERCISES

1. A palindrome is a word or phrase that is the same when spelled from the front or
the back. For example, "reviver" and "Able was 1 ere I saw Elba" are both palin­
dromes. We can determine if a word or phrase is a palindrome by using a stack.
Write a C function that returns TRUE if a word or phrase is a palindrome and
FALSE if it is not.
2. We can use a stack to determine if the parentheses in an expression are properly
nested. Write a C function that does this.
3. Consider the hypothetical data type X2. X 2 is a linear list with the restriction that
while additions to the list may be made at either end, deletions can be made at one
end only. Design a linked list representation for X 2. Write addition and deletion
functions for X2. Specify initial and boundary conditions for your representation.

4.4 POLYNOMIALS

4.4.1 Representing Polynomials As Singly Linked Lists

Let us tackle a reasonably complex problem using linked lists. This problem, the mani­
pulation of symbolic polynomials, has become a classic example of list processing. As
in Chapter 2, we wish to be able to represent any number of different polynomials as
long as memory is available. In general, we want to represent the polynomial:
^0
A(x) = a^_iX + * ' * +62

where the are nonzero coefficients and the e, are nonnegative integer exponents such
that em-\ > ^m-2 > ' '' > ^1 > ^0 - 0. We represent each term as a node containing
Polynomials 151


void addq(queue—pointer front, queue—pointer *rear,
element item)
{
*
/ add an element to the rear of the queue */
queue—pointer temp =
(queue—pointer) malloc(sizeof(queue) );
if (IS—FULL(temp)) {
fprintf(stderr, "The memory is full\n");
exit(1);
}
temp->item = item;
temp->link = NULL;
if (
front)
* rear )->link = temp;
(*

else *
front = temp;
front =
*rear = t emp;
}

Program 4.8: Add to the rear of a linked queue

*
element deleteq(queue—pointer front)
{
*
/ delete an element from the queue */
queue—pointer temp = *front;
element item;
if (IS—EMPTY(
* front)) {
fprintf(stderr, "The queue is emptyXn");
exit(1);
}
item temp->item;

front= temp->link;
free(temp);
return item;
}

Program 4.9: Delete from the front of a linked queue

coefficient and exponent fields, as well as a pointer to the next term. Assuming that the
coefficients are integers, the type declarations are:
152 Lists

typedef struct poly—node *


poly —pointer;
typedef struct poly—node {
int coef;
int expon;
poly—pointer link;
};
poly—pointer a,b,d;

We draw poly-nodes as:

coef expon link

Figure 4.11 shows how we would store the polynomials

a = 3x“* + 2x^ + 1
and
*^
b = 8x‘‘* -3x“’ + lOx

a
k
3 14 > 2 8 >■ 1 0 NULL
b (a)
1
8 14 > -3 10 > 10 6 NULL

(b)

Figure 4.11 : Polynomial representation

4.4.2 Adding Polynomials

To add two polynomials, we examine their terms starting at the nodes pointed to by a and
b. If the exponents of the two terms are equal, we add the two coefficients and create a
new term for the result. We also move the pointers to the next nodes in a and b. If the
exponent of the current term in a is less than the exponent of the current term in b, then
we create a duplicate term of b, attach this term to the result, called d, and advance the
pointer to the next term in b. We take a similar action on a if a->expon > b—>expon.
Figure 4.12 illustrates this process for the polynomials represented in Figure 4.11.
Each time we generate a new node, we set its coef and expon fields and append it
to the end of d. To avoid having to search for the last node in d each time we add a new
node, we keep a pointer, rear, which points to the current last node in d. The complete
Polynomials 153

3 14 2 8 1 0 NULL
t
a

8 14 -3 10 10 6 NULL
t
b
11 14 NULL

t
d
(a) a — > export == b — > export

3 14 2 8 > 1 0 NULL

T
a
8 14 -3 10 10 6 NULL

t
b

11 14 -3 10 NULL
t
d
(b) a —> export < b —> export

3 14 > 2 8 1 0 NULL

t
a
8 14 -3 10 10 6 NULL
t
b

11 14 -3 10 2 8 NULL

t
d
(c) a -> export > b -> export

Figure 4.12 : Generating the first three terms of d = a +b

addition algorithm is specified by padd (Program 4.10). To create a new node and
append it to the end of J, padd uses attach (Program 4.11). To make things work out
neatly, initially we give d a single node with no values, which we delete at the end of the
function. Although this is somewhat inelegant, it avoids more computation.
This is our first complete example of list processing, so you should study it care­
fully. The basic algorithm is straightforward, using a streaming process that moves
along the two polynomials, either copying terms or adding them to the result. Thus, the
154 Lists

poly—pointer padd(poly—pointer a, poly—pointer b)


{
/
* return a polynomial which is the sum of a and b */
poly—pointer front, rear, temp;
int sum;
rear (poly—pointer)malloc(sizeof(poly—node));
if (IS-FULL(rear)) {
fprintf (stderr, "The memory is fullin'’);
exit(1);
}
front rear;
while (a && b)
switch (COMPARE(a->expon,b->expon)) {
case -1: /a—>expon < b
a->expon —>expon
b->expon /
attach(b->coef,b->expon,&rear) ;
b b->link;
break;
case 0: *
/ a->expon = b->expon
sum = a->coef + b->coef;
if (sum) attach(sum,a->expon,&rear) ;
a = a—•>link;
>link; b = b—>link; break;
case 1: /'^ a->expon > b->expon */
attach(a->coef,a->expon,&rear) ;
a a->link;
}
copy rest of list a and then list b
for (; a; a a“>link) attach(a->coef,a->expon,&rear) ;
for (; b; b = b->link) attach(b->coef,b->expon,&rear);
rear->link NULL;
/
* delete extra initial node ■^ /
temp = front; front front->link; free(temp);
return front;
}

Program 4.10: Add two polynomials

while loop has three cases depending on whether the next pair of exponents are =, < , or
>. Notice that there are five places where we create a new term, justifying our use of
function attach.

Analysis of padd\ To determine the computing time of padd, we first determine which
operations contribute to the cost. For this algorithm, there are three cost measures:
Polynomials 155

void attach(float coefficient, int exponent, poly—pointer



ptr)
{
*
/ create a new node with coef = coefficient and expon =
exponent, attach it to the node pointed to by ptr. ptr is
updated to point to this new node */
poly—pointer temp;
temp (poly—pointer)malloc(sizeof(poly—node));
if (IS-FULL(temp) ) {
fprintf(stderr, "The memory is fullin’’);
exit(1);
}
temp->coef = coefficient;
temp->expon = exponent;
*
(
ptr)->link = temp;
*ptr temp;
}

Program 4.11: Attach a node to the end of a list

(1) coefficient additions


(2) exponent comparisons
(3) creation of new nodes for d

If we assume that each of these operations takes a single unit of time if done once,
then the number of times that we perform these operations determines the total time
taken by padd. This number clearly depends on how many terms are present in the poly­
nomials a and b. Assume that a and b have m and n terms, respectively:

—I
1-^ + ■ ■ ■ +

B{x') = ' -h ■ • ■ -I- b^x^''

where bi 0 and > ••• ^0 > 0, /„_i > • • • > /o 0. Then clearly the number
of coefficient additions varies as:

0 < number of coefficient additions < min{w,z2)

The lower bound is achieved when none of the exponents are equal, while the upper is
achieved when the exponents of one polynomial are a subset of the exponents of the
other.
156 Lists

As for the exponent comparisons, we make one comparison on each iteration of


the while loop. On each iteration, either a or b or both move to the next term. Since the
total number of terms is m + n, the number of iterations and hence the number of
exponent comparisons is bounded by m -i- n. You can easily construct a case when m 4- n
— 1 comparisons will be necessary, for example, m = n and

Jm-1 > ’m-2 > fm-2 >

The maximum number of terms in J is m -i- n, and so no more than m -i- n new terms are
created (this excludes the additional node that is attached to the front of d and later
removed).
In summary, the maximum number of executions of any statement in padd is
bounded above by m + n. Therefore, the computing time is O(m -i- n). This means that if
we implement and run the algorithm on a computer, the time it takes will be c j m -i- C2n +
where C|, C2, C3 are constants. Since any algorithm that adds two polynomials must
look at each nonzero term at least once, padd is optimal to within a constant factor. □

4.4.3 Erasing Polynomials

The use of linked lists is well suited to polynomial operations. We can easily imagine
writing a collection of functions for input, output, addition, subtraction, and multiplica­
tion of polynomials using linked lists as the means of representation. A hypothetical
user who wishes to read in polynomials £z(r), /?(%), and d{x) and then compute e(x) =
a (x) * b (x) + d (x) would write his or her main function as:

poly—pointer a, b, d. e

a = read—poly();
b = read—poly();
d = read—poly();
temp = pmult{a,b);
e = padd(temp,d);
print—poly(e);

If our user wishes to compute more polynomials, it would be useful to reclaim the nodes
that are being used to represent temp (r) since we created temp (x) only to hold a partial
result for d {x}. By returning the nodes of temp {x\ we may use them to hold other poly­
nomials. One by one, erase (Program 4.12) frees the nodes in temp.
Polynomials 157

void erase(poly—pointer ptr)


{
/
* erase the polynomial pointed to by ptr */
poly—pointer temp;
while *
(
ptr) {
temp = *ptr;
*ptr = (*ptr)->link;
free(temp);
}
}

Program 4.12: Erasing a polynomial

4.4.4 Representing Polynomials As Circularly Linked Lists

We can free all the nodes of a polynomial more efficiently if we modify our list structure
so that the link field of the last node points to the first node in the list (See Figure 4.13).
We call this a circular list. A singly linked list in which the last node has a null link is
called a chain.

c
ptr —► 3 14 > 2 8 > 1 0

Figure 4.13 : Circular representation of ptr = 3x + 2%^ + 1

As we indicated earlier, we free nodes that are no longer in use so that we may
reuse these nodes later. We can meet this objective, and obtain an efficient erase algo­
rithm for circular lists, by maintaining our own list (as a chain) of nodes that have been
"freed." When we need a new node, we examine this list. If the list is not empty, then
we may use one of its nodes. Only when the list is empty do we need to use malloc to
create a new node.
Let avail be a variable of type poly -pointer that points to the first node in our list
of freed nodes. Henceforth, we call this list the available space list or avail list. Ini­
tially, we set avail to NULL. Instead of using malloc and free, we now use get-node
(Program 4.13) and ret-node (Program 4.14).
158 Lists

poly—pointer get—node(void)
/
* provide a node for use */
{
poly—pointer node;
if (avail) {
node = avail;
avail = avail->link;
}
else {
node (poly—pointer) malloc(sizeof(poly—node));
if (IS-FULL(node)) {
fprintf(stderr, "The memory is full\n");
exit(1);
}
)
return node;
}

Program 4.13: get-node function

void ret—node(poly—pointer ptr)


{
*
/ return a node to the available list */
ptr->link = avail;
avail = ptr;
}

Program 4.14: ret-node function

We may erase a circular list in a fixed amount of time independent of the number
of nodes in the list using cerase (Program 4.15). Figure 4.14 shows the changes involved
in erasing a circular list.
A direct changeover to the structure of Figure 4.13 creates problems when we
implement the other polynomial operations since we must handle the zero polynomial as
a special case. To avoid this special case, we introduce a head node into each polyno­
mial, that is, each polynomial, zero or nonzero, contains one additional node. The expon
and coef fields of this node are irrelevant. Thus, the zero polynomial has the representa­
tion of Figure 4.15(a), while a(x) = + 2x^ + 1 has the representation of Figure
4.15(b).
Polynomials 159

void cerase(poly—pointer *ptr)

erase the circular list ptr */


poly—pointer temp;
if ptr)
*
( {
temp (*ptr)->link;
ptr)->link
*
( = avail;
avail = temp;
*ptr - NULL;
}

Program 4.15: Erasing a circular list

aua i 1

c *

r
ptr
------

t
tenp

NULL
r
auail

Figure 4,14 : Returning a circular list to the avail list

For the circular list with head node representation, we may remove the test for
{'^ptr} from cerase. The only changes that we need to make to padd are:

(1) Add two variables, starta = a and startb = b.


(2) Prior to the while loop, assign a ~ a->link and b = b—>link.
(3) Change the while loop to while {a ’= starta && b ’= startb}.
160 Lists

(a) Zero polynomial

3 14 2 8 1 0

(b)3x’‘‘+2!e« + l

Figure 4.15 : Polynomial representations

(4) Change the first for loop to for (; a != starta', a = a->Unk).


(5) Change the second for loop to for (; b ’= startb; b = b->linkf
(6) Delete the lines:

rear-> link = NULL;


/* delete extra initial node
(7) Change the lines:

temp = front;
front =front->link;
free{tempf.
to
rear->link - front;

Thus, the algorithm stays essentially the same, and we now handle zero polynomials in
the same way as nonzero polynomials.
We may further simplify the addition algorithm if we set the expon field of the
head node to -1. Now after we have examined all the nodes of a, starta = a and
starta->expon = -1. Since -1 < b->expon, we can copy the remaining terms of b by
further executions of the switch statement. The same is true if we examine all the nodes
of b before those of a. This means that we no longer need the additional code to copy
the remaining terms. The final algorithm, cpadd, takes the simple form given in Program
4.16.
Polynomials 161

poly—pointer cpadd(poly—pointer a, poly—pointer b)


{
/ polynomials a and b are singly linked circular lists
with a head node. Return a polynomial which is the sum
of a and b */
poly—pointer starta, d, lastd;
int sum, done FALSE;
starta - a; *
/ record start of a
a a->link; skip head node for a and b
/
*
b b->link;
d = get—node() ; *
/ get a head node for sum
d->expon = -1; lastd d;
do {
switch (COMPARE(a->expon, b->expon)) {
case -1: * * a->expon < b->expon *
/ /
attach (b->coef , b“>expon, Selastd) ;
b b->link;
break;
case 0: /
* a->expon - b->expon
if (starta == a) done TRUE;
else {
sum = a->coef + b->coef;
if (sum) attach(sum,a—>expon,&lastd);
a = a->link; b = b->link;
}
break;
case 1: /
* a->expon b->expon */
attach(a->coef, a->expon,&lastd);
a = a—>link;
}
} while (!done);
lastd->link = d;
return d;
}

Program 4.16: Adding circularly represented polynomials

4.4.5 Summary

Let us review what we have done so far. We have introduced the concepts of a singly
linked list, a chain, and a singly linked circular list. Each node on one of these lists
162 Lists

consists of exactly one link field and at least one other field.
In dealing with polynomials, we found it convenient to use circular lists. Another
concept we introduced was an available space list. This list consisted of all nodes that
had been used at least once and were not currently in use. By using the available space
list and get-node, ret-node, and cerase, it became possible to erase circular lists in con­
stant time, and also to reuse all nodes not currently in use. As we continue, we shall see
more problems that call for variations in node structure and list representation because of
the operations we wish to perform.

EXERCISES

1. Write a function, pread, that reads in n pairs of coefficients and exponents, (coefi,
I’
expofti), 0< i < n of a polynomial, x. Assume that exporij^i > expotii, < n-2.
and that Q, Q < i < n. Show that this operation can be performed in O(n)
time.
2. Let a and be pointers to two polynomials. Write a function to compute the pro­
duct polynomial d = a^b. Your function should leave o and b unaltered and
create J as a new list. Show that if n and m are the number of terms in a and b,
respectively, then this multiplication can be carried out in or time.
3. Let (3 be a pointer to a polynomial. Write a function, peval, to evaluate the polyno­
mial a at point x, where x is some floating point number.
4. Rewrite Exercise 1 using a circular representation for the polynomial.
5. Rewrite Exercise 2 using a circular representation for the polynomial.
6. Rewrite Exercise 3 using a circular representation for the polynomial.
7. § [Programming project] Design and build a linked allocation system to represent
and manipulate polynomials. You should use circularly linked lists with head
nodes. Each term of the polynomial will be represented as a node, using the fol­
lowing structure:

coef expon link

In order to erase polynomials efficiently, use the available space list and associated
functions discussed in this section.

Write and test the following functions:

(a) pread. Read in a polynomial and convert it to its circular representation.


Return a pointer to the head node of this polynomial.
Polynomials 163

(b) pwrite. Output the polynomial using a form that clearly displays it.
(c) padd. Compute d = a + b. Do not change either a or b.
(d) psub. Compute d = a-b. Do not change either a or b.
(e) pmult. Compute d = a'^b. Do not change either a or b.
(f) eval. Evaluate a polynomial at some point, tz, where a is a floating point con­
stant. Return the result as a floating point.
(g) perase. Return the polynomial represented as a circular list to the available
space list.

4.5 ADDITIONAL LIST OPERATIONS

4.5.1 Operations For Chains

It is often necessary, and desirable, to build a variety of functions for manipulating


singly linked lists. Some that we have seen already are get-node and ret-node, which
get and return nodes to the available space list. Inverting (or reversing) a chain (Program
4.17) is another useful operation. This routine is especially interesting because we can
do it "in place” if we use three pointers. We use the following declarations:


typedef struct list—node list—pointer;
typedef struct list—node {
char data;
list—pointer link;
) ;

Try out this function with at least three examples, an empty list and lists of one and
two nodes, so that you understand how it works. For a list of length > 1 nodes, the while
loop is executed length times and so the computing time is linear or O(length).
Another useful function is one that concatenates two chains, ptr\ and ptr2 (Pro­
gram 4.18). The complexity of this function is O(length of list ptri). Since this function
does not allocate additional storage for the new list, ptri also contains the concatenated
list. (The exercises explore a concatenation function that does not alter ptri.)

4.5.2 Operations For Circularly Linked Lists

Now let us take another look at circular lists like the one in Figure 4.16. Suppose we
want to insert a new node at the front of this list. We have to change the link field of the
node containing This means that we must move down the entire length of a until we
find the last node. It is more convenient if the name of the circular list points to the last
node rather than the first (Figure 4.16). Now we can write functions that insert a node at
the front (Figure 4.17) or at the rear of a circular list in a fixed amount of time. To insert
164 Lists

list—pointer invert(list—pointer lead)


{
*
/ invert the list pointed to by lead */
list-pointer middle,trail;
middle NULL ;
while (lead) {
trail = middle;
middle = lead;
lead - lead->link;
middle->link trail;
}
return middle;
}

Program 4.17: Inverting a singly linked list

list—pointer concatenate(list—pointer ptrl,


list—pointer ptr2)
{
/
* produce a new list that contains the list ptrl followed
by the list ptr2. The list pointed to by ptrl is changed
/
permanently *
list—pointer temp;
if (IS—EMPTY(ptrl)) return ptr2;
else {
if (!IS—EMPTY(ptr2)) {
for (temp = ptrl; temp->link; temp temp->link)

temp->link = ptr2;
)
return ptrl;
}
}

Program 4.18: Concatenating singly linked lists

node at the rear, we only need to add the additional statement = node to the else
clause of insert-front (Program 4.19).
Additional List Operations 165

>
a X
1 > *2 > X
3

Figure 4.16: Example circular list

>
X
1 > *2 X
3
4 a

Figure 4.17: Pointing to the last node of a circular list

As a last example of a simple function for circular lists, we write a function (Pro­
gram 4.20) that determines the length of such a list.

void insert—front(list—pointer *ptr, list—pointer node)


*
/ insert node at the front of the circular list ptr,
where ptr is the last node in the list */
{
if ptr ))
(IS—EMPTY(* {
/
* list is empty, change ptr to point to new entry */
*ptr = node;
node->link = node;
}
else {
*
/ list is not empty, add new entry at front */
node->link (*ptr)->link;
ptr)->link
*
( = node;
}
}

Program 4.19: Inserting at the front of a list


166 Lists

int length(list—pointer ptr)


{
*
/ find the length of the circular list ptr */
list—pointer temp;
int count = 0;
if (ptr) {
temp = ptr;
do {
count++;
temp = temp“>link;
} while {temp != ptr);
}
return count;
)

Program 4.20: Finding the length of a circular list

EXERCISES
1. Create a function that searches for an integer, num, in a circularly linked list. The
function should return a pointer to the node that contains num if num is in the list
and NULL otherwise.
2. Write a function that deletes a node containing a number, num, from a circularly
linked list. Your function should first search for num.
3. Write a function to concatenate two circular lists together. Assume that the
pointer to each such list points to the last node. Your function should return a
pointer to the last node of the concatenated circular list. Following the concatena­
tion, the input lists do not exist independently. What is the time complexity of
your function?
4. Write a function to reverse the direction of pointers in a circular list.

4.6 EQUIVALENCE RELATIONS

Let us put together some of the concepts on linked and sequential representations to
solve a problem that arises in the design and manufacture of very large-scale integrated
(VLSI) circuits. One of the steps in the manufacture of a VLSI circuit involves exposing
a silicon wafer using a series of masks. Each mask consists of several polygons.
Polygons that overlap electrically are equivalent and electrical equivalence specifies a
relationship among mask polygons. This relation has several properties that it shares
with other equivalence relations, such as the standard mathematical equals. Suppose that
we denote an arbitrary equivalence relation by the symbol = and that:
Equivalence Relations 167

(1) For any polygon x, x that is, x is electrically equivalent to itself. Thus, = is
reflexive.
(2) For any two polygons, x and y, if x =y then y =x. Thus, the relation = is sym­
metric.
(3) For any three polygons, x, and z, if x = y and y = z then x = z. For example, if x
and y are electrically equivalent and y and are also equivalent, then x and z are
also electrically equivalent. Thus, the relation = is transitive.

Definition: A relation, =, over a set, S, is said to be an equivalence relation over S iff\\. is


symmetric, reflexive, and transitive over S. □

Examples of equivalence relations are numerous. For example, the "equal to" (=)
relationship is an equivalence relation since

(1) X =X

(2) X = y implies y = x
(3) X = y and y = z implies that x = z

We can use an equivalence relation to partition a set S into equivalence classes


such that two members x and y of S are in the same equivalence class iffx y. For exam­
ple, if we have twelve polygons numbered 0 through 11 and the following pairs overlap:

0 = 4, 3= 1, 6= 10, 8 = 9. 7 = 4, 6 = 8, 3 = 5, 2= 11, 11 =0

then, as a result of the reflexivity, symmetry, and transitivity of the relation s, we can
partition the twelve polygons into the following equivalence classes:

{0, 2,4,7, 11); {1,3,5); {6, 8,9, 10)

These equivalence classes are important because they define a signal net that we can use
to verify the correctness of the masks.
The algorithm to determine equivalence works in two phases. In the first phase,
we read in and store the equivalence pairs <z, j >. In the second phase we begin at 0 and
find all pairs of the form <0, j>, where 0 and j are in the same equivalence class. By
transitivity, all pairs of the form <j, k> imply that k is in the same equivalence class as
0. We continue in this way until we have found, marked, and printed the entire
equivalence class containing 0. Then we continue on.
Our first design attempt appears in Program 4.21. Let m and n represent the
number of related pairs and the number of objects, respectively. We first must figure out
which data structure we should use to hold these pairs. To determine this, we examine
the operations that are required. The pair <Z, j > is essentially two random integers in the
range 0 to n-1. Easy random access would dictate an array, say pairs[n][m]. The /th
168 Lists

row would contain the elements, 7, that are paired directly to / in the input. However, this
could waste a lot of space since very few of the array elements would be used. It also
might require considerable time to insert a new pair, <i, k>, into row i since we would
have to scan the row for the next free location or use more storage.

void equivalence()
{
initialize;
while (there are more pairs) {
read the next pair i, j > ;
process this pair;
}
initialize the output;
do
output a new equivalence class;
while (not done);
}

Program 4.21: First pass at equivalence algorithm

These considerations lead us to a linked list representation for each row. Our node
structure requires only a data and a link field. However, since we still need random
access to the /th row, we use a one-dimensional array, seq [n J, to hold the head nodes of
the n lists. For the second phase of the algorithm, we need a mechanism that tells us
whether or not the object, /, has been printed. We use the array out [n ] and the constants
TRUE and FALSE for this purpose. Our next refinement appears in Program 4.22.
Let us simulate this algorithm, as we have developed it thus far, using the previous
data set. After the while loop is completed the lists resemble those appearing in Figure
4.18. For each relation / = 7, we use two nodes. The variable seq [/] points to the list of
nodes that contains every number that is directly equivalent to / by an input relation.
In phase two, we scan the seq array for the first /, 0 < / < n, such that outli] =
TRUE. Each element in the list seq [/] is printed. To process the remaining lists which,
by transitivity, belong in the same class as /, we create a stack of their nodes. We do this
by changing the link fields so that they point in the reverse direction. Program 4.23 con­
tains the complete equivalence algorithm.

#include <stdio.h>
#include <alloc.h>
#define MAX-SIZE 24
#define IS—FULL(ptr) (!(ptr))
#define FALSE 0
#define TRUE 1
Equivalence Relations 169

void equivalence()
{
initialize seq to NULL and out to TRUE;
while (there are more pairs) {
read the next pair, i, j > ;
put j on the seq[i] list;
put i on the seq[j] list;
}
for (i = 0; i < n; i++)
if (out [i]) {
out[i] FALSE;
output this equivalence class;
}
}

Program 4.22: A more detailed version of the equivalence algorithm

[0] ti) [21 C3] [41 [51 [6] [71 C81 19] [10] til]
seq

data 11 3 11 5 7 3 8 4 6 8 6 0
link MULL MULL NULL NULL NULL NULL

data 4 1 0 10 9 2

link NULL NULL NULL NULL NULL NULL

Figure 4.18 : Lists after pairs are input

node —pointer;
typedef struct node *
typedef struct node {
int data;
node—pointer link;
};
void main(void)
{
short int out[MAX—SIZE];
node—pointer seq[MAX—SIZE];
170 Lists

node—pointer x,y,top;
int i,j,n;

printf("Enter the size ( %d) ", MAX-SIZE) ;


II Q.
scanf( •«d", &n) ;
for (i = 0; i
n; i + +) {
*
/ initialize seq and out
out[i] TRUE;;
= TRUE seq[i] NULL ;
}

/★ Phase 1: Input the equivalence pairs: *


/
printf("Enter a pair of numbers (-1 -1 to quit):
scanf("%d%d", &i , &j ) ;
while (i 0) {
X (node—pointer)malloc(sizeof(node));
if (IS-FULL(x)) {
fprintf(stderr,"The memory is full\n");
exit(1);
}
x->data = j; x->link = seq[i]; seqEi]
X (node—pointer)malloc(sizeof(node));
if (IS-FULL(x)) {
fprintf(stderr, "The memory is full\n");
exit(1);
}
x->data = i;
1 ; x->link = seq[j]; seq[j]
printf("Enter a pair of numbers (-1 -1 to quit): ") ;
scanf("% d%d",&i,&j);
}

/■^ Phase 2: output the equivalence classes


for (i =0; i < n; i++)
if (out[i]) {
printf("\nNew class: %5d",i) ;
out[i] - FALSE; *
/ set class to false
X = seq[i]; top = NULL; / •k initialize stack ■^ /
for {;;} { find rest of class
while (x) { / •k process list
j x->data;
if (out[j]) {
printf("%5d",j); out[j]I = FALSE;
y = x->link; x-; link = top;
1 top : X y;

else X X— ■ link;
Equivalence Relations 171

}
if (1 top) break;
X = seq[top->data]; top top->link; /*
unstack /
}
}
}

Program 4.23: Program to find equivalence classes

Analysis of the equivalence program: The initialization of seq and out takes O(n) time.
Inputting the equivalence pairs in phase 1 takes a constant amount of time per pair.
Hence, the total time for this phase is O(m +n) where m is the number of pairs input. In
phase 2, we put each node onto the linked stack at most once. Since there are only 2m
nodes, and we execute the for loop n times, the time for this phase is 0{m + n\ Thus,
the overall computing time is O(m + m). Any algorithm that processes equivalence rela­
tions must look at all m equivalence pairs and at all n polygons at least once. Thus, there
is no algorithm with a computing time less than O(m+H). This means that the
equivalence algorithm is optimal to within a constant factor. Unfortunately, the space
required by the algorithm is also O(m 4- n). In Chapter 5, we look at an alternate solu­
tion to this problem that requires only Q{n} space. □

4.7 SPARSE MATRICES

In Chapter 2, we saw that we could save space and computing time by retaining only the
nonzero terms of sparse matrices. When the nonzero terms did not form a "nice" pattern,
such as a triangle or a band, we devised a sequential scheme in which we represented
each nonzero term by a node with three fields: row, column, and value. We organized
these nodes sequentially. However, we found that when we performed matrix operations
such as addition, subtraction, or multiplication, the number of nonzero terms varied.
Matrices representing partial computations, as in the case of polynomials, were created
and later destroyed to make space for further matrices. Thus, the sequential representa­
tion of sparse matrices suffered from the same inadequacies as the similar representation
of polynomials. In this section, we study a linked list representation for sparse matrices.
As we have seen previously, linked lists allow us to efficiently represent structures that
vary in size, a benefit that also applies to sparse matrices.
In our data representation, we represent each column of a sparse matrix as a circu­
larly linked list with a head node. We use a similar representation for each row of a
sparse matrix. Each node has a tag field, which we use to distinguish between head
nodes and entry nodes. Each head node has three additional fields: down, right, and ne.xt
(Figure 4.19(a)). We use the down field to link into a column list and the right field to
link into a row list. The next field links the head nodes together. The head node for row
i is also the head node for column i, and the total number of head nodes is max {number
of rows, number of columns).
172 Lists

Each entry node has five fields in addition to the tag field: row, col, down, right,
value (Figure 4.19(b)). We use the down field to link to the next nonzero term in the
same column and the right field to link to the next nonzero term in the same row. Thus,
if aij 0, there is a node with tag field = entry, value = a^j, row - i, and col = j (Figure
4.19(c)). We link this node into the circular linked lists for row i and column j. Hence,
it is simultaneously linked into two different lists.

down head right doun head rou col right entry I j


next value a.
ij
(a) head node (b) entry node (c) set up for a ■ ■
*J

Figure 4.19 : Node structure for sparse matrices

As we indicated earlier, each head node is in three lists: a list of rows, a list of
columns, and a list of head nodes. The list of head nodes also has a head node that has
the same structure as an entry node (Figure 4.19(b)). We use the row and col fields of
this node to store the matrix dimensions.
Suppose that we have the sample sparse matrix, a, shown in Figure 4.20. Figure
4.21 shows the linked representation of this matrix. Although we have not shown the
value of the tag fields, we can easily determine these values from the node structure. For
each nonzero term of a, we have one entry node that is in exactly one row list and one
column list. The head nodes are marked H()-H3. As the figure shows, we use the right
field of the head node list header to link into the list of head nodes. Notice also that we
may reference the entire matrix through the head node, a, of the list of head nodes.

0 0 11 0
12 0 0 0
0 -4 0 0
0 0 0 -15

Figure 4.20 : 4x4 sparse matrix a

If we wish to represent a num-rows x num-cols matrix with num-terms nonzero


terms, then we need max {num-rows, num-cols] + num-terms + 1 nodes. While each
node may require several words of memory, the total storage will be less than num-rows
• num-cols when num-terms is sufficiently small.
Having chosen our sparse matrix representation, we may now translate it into C
declarations. Since we have two different types of nodes in our representation, we use a
union to create the appropriate data structure. This means that our data structure is more
complex than any structure we have created previously. The necessary declarations are
Sparse Matrices 173

r I HO Hl H2 H3
> A 4 >
> >
4
HO 0 2
11

Hl > 1 0
12

H2 2 1
-4

H3 > 3 3
-15
t
NOTE: The tag field of a node is not shown; its value for each node should be clear from
the node structure.

Figure 4.21 : Linked representation of the sparse matrix a

as follows:

#define MAX-SIZE 50 size


*
/ of largest matrix
/
*
typedef enum {head,entry} tagfield;
typedef struct matrix—node *matrix—pointer;
typedef struct entry—node {
int row;
int col;
int value;
} ;
typedef struct matrix—node {
matrix—pointer down;
matrix—pointer right;
tagfield tag;
union {
matrix—pointer next;
entry—node entry;
} u;
} ;
matrix-pointer hdnode[MAX—SIZE];
174 Lists

The first operation we implement is that of reading in a sparse matrix and obtain­
ing its linked representation. We assume that the first input line consists of the number
of rows (num-rows), the number of columns (num~cols'), and the number of nonzero
terms (num-terms). This line is followed by num-terms lines of input, each of which is
of the form: row, col, value. We assume that these lines are ordered by rows and within
rows by columns. For example, Figure 4.22 shows the input for the 4 x 4 matrix of Fig­
ure 4.20.

[0] [1] [2]


[0] 4 4 4
[1] 0 2 11
[2] 1 0 12
[3] 2 1 -4
[4] 3 3 -15

Figure 4.22 : Sample input for sparse matrix

The function mread (Program 4.24) uses an auxiliary array, hdnode. which we
assume is at least as large as the largest-dimensioned matrix to be input. The variable
hdnode{i\ is a pointer to the head node for column i and row i. This allows us to access
efficiently columns at random, while we are setting up the input matrix. The function
mread first sets up all the head nodes and then sets up each row list while simultaneously
building the column lists. The next field of head node, i, is initially used to keep track of
the last node in column i. The last for loop of the function links the head nodes together
through this field.

matrix—pointer mread(void)
{
/
* read in a matrix and set up its linked representation.
An auxiliary global array hdnode is used */
int num—rows, cols,,
num—cols num—terms, num—heads, i;
int row, col, value, current—row;
matrix—pointer temp,last ,node;

printf("Enter the number of rows, columns


and number of nonzero terms:
scanf("%d%d%d",&num—rows, Snum-cols, &num-terms);
num—heads = (num—cols > num—rows) ? num—cols : num—rows;
*
/ set up head node for the list of head nodes */
node - new—node(); node->tag entry;
Sparse Matrices 175

node->u.entry.row = num-rows;
node->u.entry.col = num—cols;

if (!num-heads) node->right = node;


else { /★ initialize the head nodes
for (i = 0; 1 num—heads; i++) {
temp = new—node;
hdnode[i] temp; hdnode[i]->tag = head;
hdnode[i]->right = temp; hdnode[i]->u.next t emp;
}
current—row 0;
last = hdnode[0]; /
* last node in current */
terms;; i++) {
for (i = 0; i < num—terms
printf("Enter row, column and value:
scanf (" %d%d%d'', &row, &col, &value) ;
if {row > current—row) *
{/ close current row ^1
last->right = hdnode[current—row];
current—row row; last = hdnode[row];
}
temp = new—node() ;
temp->tag entry; temp—>u.entry,row = row;
temp->u.entry.col col ;
temp->u.entry.value = value;
last->right = temp; / ★ link into row list */
last t emp;
*
/ link into column list */
hdnode[col]->u.next->down t emp ;
hdnode[col]->u.next t emp ;
}
/lose
c
* last row * /
last->right = hdnode[current—row];
*
/ close all column lists ^1
for (i - 0; i < num—cols;
hdnode[i]—>u.next->down = hdnode[i];
*
/ link all head nodes together
for (i = 0; i < num—heads-1
heads—1;; i++)
hdnode[i]->u.next = hdnode[i+1];
hdnode[num—heads—1]->u.next = node;
node->right = hdnode[0];
}
return node;
}

Program 4.24: Read in a sparse matrix


176 Lists

matrix—pointer new—node(void)
{
matrix—pointer temp;
temp (matrix—pointer) malloc(sizeof(matrix —node));
if (IS—FULL(temp)) {
fprintf(stderr, "The memory is full\n");
exit(1);
}
return temp;
}

Program 4.25: Get a new matrix node

Analysis of mreadx Since malloc works in a constant amount of time, we can set up all
of the head nodes in O(max {num -rows,num-cols}) time. We can also set up each
nonzero term in a constant amount of time because we use the variable last to keep track
of the current row, while next keeps track of the current column. Thus, the for loop that
inputs and links the entry nodes requires only 0{num-terms) time. The remainder of
the function takes O(max [num-rows,num-colsj) time. Therefore, the total time is:

O(max [num -rows,num-cols] + num-terms)

- O(num-rows + num-cols + num-terms).

Notice that this is asymptotically better than the input time of O(num-rows • num-cols)
for a num-rows x num-cols matrix using a two-dimensional array. However it is
slightly worse than the sequential method used in Section 2.4. □

We would now like to print out the contents of a sparse matrix in a form that
resembles that found in Figure 4.22. The function mwrite (Program 4.26) implements
this operation.

Analysis of mwritez The function mwrite uses two for loops. The number of iterations
of the outer for loop is num-rows. For any row, i, the number of iterations of the inner
for loop is equal to the number of entries for row Z. Therefore, the computing time of the
mwrite function is O{num-rows + num-terms). □

Before closing this section we want to look at an algorithm that returns all nodes
of a sparse matrix to the system memory. We return the nodes one at a time using free,
although we could develop a faster algorithm using an available space list (see Section
4.4). The function merase (Program 4.27) implements the erase operation.

Analysis of merase'. First, merase returns the entry nodes and the row head nodes to the
Sparse Matrices 177

void mwrite(matrix—pointer node)


{
/
* print out the matrix in row major form */
int i;
matrix—pointer temp, head node->right;
/
* matrix dimensions /
*
printf(" \n num—rows Q. %d \n",
isd, num—cols
node->u.entry.row, node->u.entry.col);
printf(" The matrix by row, column, and value: \n\n");
for (i = 0; i < node—>u.entry.row; i++) {
/
* print out the entries in each row *
/
for (temp = head->right; temp != head;
temp temp->right)
printf( "%5d%5d%5d \n",temp->u.entry.row,
temp->u.entry.col, temp->u.entry.value);
head = head->u.next; / next row
}
}

Program 4.26: Write out a sparse matrix

system memory. It uses a nested loop structure that resembles the structure found in
mwrite. Thus, the computing time for the nested loops is Q{num~rows + num-terms}.
After these nodes are erased the remaining head nodes are erased. This requires
O{num-rows + num-cols) time. Hence, the computing time for merase is O(num-rows
+ num-cols + num—terms}. □

EXERCISES

1. Let a and Z? be two sparse matrices. Write a function, madd, to create the matrix d
~ a + b. Your function should leave matrices a and b unchanged, and set up ^7 as a
new matrix. Show that if a and b are num-rows y. num-cols matrices with
num-termSa and num-termsnonzero terms, then we can perform this addition in
O(num-rows + num—cols + num-terms^ + num-termsfj} time.
2. Let a and Z? be two sparse matrices. Write a function, mmult, to create the matrix d
= a
b.
* Show that if « is a num-rows^ x num-cols^ matrix with num-termSa
nonzero terms and Z? is a num-cols^ x num-cols^ matrix with num-terms/j
nonzero terms, then we can compute d in O(nz/m-c<?/57, • num-terms^ +
num-rowSa • num-termsfy} time. Can you think of a way to compute d in O(min
{num-colsiy • num-terms^. num-rowSa • num-termsiy}) time?
178 Lists

node)
void merase(matrix—pointer *
{
*
/ erase the matrix, return the nodes to the heap */
matrix—pointer x,y, head (*
node)->right ;
int i, num—heads ;
/
* free the entry and head nodes by row * /
for (i = 0; i {n
*ode)->u .entry.row; i++) {
y = head—>right;
while (y != head) {
X = y; y = y->right; free(x);
y; Y
}
X = head; head head->u.next; free(x);
}
1^ free remaining head nodes
/
*
y = head;
while (y != *node) {
X y; y = y->u.next; free(x);
}
free(
node);
* node
* = NULL;
}

Program 4.27: Erase a sparse matrix

3. (a) Rewrite merase so that it places the erased list into an available space list
rather than returning it to system memory.
(b) Rewrite mread so that it first attempts to obtain a new node from the avail­
able space list rather than the system memory.
4. Write a function, mtranspose, to compute the matrix b = a the transpose of the
sparse matrix a. What is the computing time of your function?
5. Design a function that copies a sparse matrix. What is the computing time of your
function?
6. § [Programming project] want to implement a complete linked list system to
perform arithmetic on sparse matrices using our linked list representation. Create
a user-friendly, menu-driven system that performs the following operations. (The
matrix names are used only for illustrative purposes. The functions are specified
as templates to which you must add the appropriate parameters.)
(a) mread. Read in a sparse matrix.
Sparse Matrices 179

(b) mwrite. Write out the contents of a sparse matrix.


(c) me rase. Erase a sparse matrix.
(d) madd. Create the sparse matrix d = a + b
(e) mmult. Create the sparse matrix J = a^b.
(f) mtranspose. Create the sparse matrix b = .

4.8 DOUBLY LINKED LISTS

Singly linked lists pose problems because we can move easily only in the direction of the
links. For example, suppose that we are pointing to a specific node, say ptr, and we want
to find the node that precedes ptr. We can only do this by starting at the beginning of the
list and searching until we find the node whose link field points to ptr. Since we must
know the preceding node for the deletion operation, we obviously cannot perform this
operation efficiently with singly linked lists. Whenever we have a problem that requires
us to move in either direction, it is useful to have doubly linked lists.
A node in a doubly linked list has at least three fields, a left link field (llink), a data
field (item), and a right link field (rlink). The necessary declarations are:

typedef struct node *


node —pointer;
typedef struct node {
node—pointer llink;
element item;
node—pointer rlink;
};

A doubly linked list may or may not be circular. A sample doubly linked circular
list with three nodes is given in Figure 4.23. Besides these three nodes, we have added a
head node. As was true in previous sections, a head node allows us to implement our
operations more easily. The item field of the head node usually contains no information.
Now suppose that ptr points to any node in a doubly linked list. Then:

ptr = ptr-> llink-> rlink = ptr-> rlink-> llink

This formula reflects the essential virtue of this structure, namely, that we can go back
and forth with equal ease. An empty list is not really empty since it always has a head
node whose structure is illustrated in Figure 4.24.
To use these lists we must be able to insert and delete nodes. Insertion into a dou­
bly linked list is fairly easy. Assume that we have two nodes, node and newnode, node
may be either a head node or an interior node in a list. The function dinsert {Program
4.28) performs the insertion operation in constant time. Figure 4.25 shows this insertion
process when node represents the head node of an empty list.
180 Lists

11 ink iten rl ink


Head Mode

Figure 4.23: Doubly linked circular list with head node

ptr > r
Figure 4.24: Empty doubly linked circular list with head node

void dinsert{node-pointer node, node—pointer newnode)


{
*
/ insert newnode to the right of node */
newnode->llink = node;
newnode-> r1ink node->rlink;
node->rlink->llink = newnode;
node—>rlink = newnode;
}

Program 4.28: Insertion into a doubly linked circular list

Deletion from a doubly linked list is equally easy. The function ddelete (Program
4.29) deletes the node deleted from the list pointed to by node. To accomplish this dele­
tion, we only need to change the link fields of the nodes that precede
{deleted->llink->rlink) and follow (deleted—>rlink—> Ilink] the node we want to
delete. Figure 4.26 shows the deletion in a doubly linked list with a single node.
Doubly Linked Lists 181

node node

newnode

Figure 4.25 : Insertion into an empty doubly linked circular list

void ddelete(node—pointer node, node—pointer deleted)


{
/
* delete from the doubly linked list */
if (node == deleted)
printf("Deletion of head node not permitted.\n") ;
else {
deleted->llink->rlink deleted->rlink;
deleted->rlink->llink deleted->llink;
free(deleted);
}
)

Program 4.29: Deletion from a doubly linked circular list

EXERCISES

1. Assume that we have a doubly linked list, as represented in Figure 4.23, and that
we want to add a new node between the second and third nodes in the list. Redraw
the figure so that it shows the insertion. Label the fields of the affected nodes so
that you show how each statement in the dinsert function is executed. For exam­
ple, label newnode->llmk, newnode->rlink, and node->rlink->Uink.
2. Repeat Exercise 1, but delete the second node from the list.
3. § [Programming project] Assume that we have information on the employees of a
computing firm as illustrated in Figure 4.27. For each employee, in addition to the
employee’s name, we have an occupational title, an identification number, and a
location. We would like to be able to access quickly the information for any of the
categories. For example, we might want to quickly retrieve the list of all
182 Lists

node node

^-1
◄-

*— deleted

Figure 4.26 : Deletion from a doubly linked circular list

employees who work in New York, or the list of all programmers. One way of
doing this is to the create a data structure known as a multilist. This data structure
contains an index table for each field, excluding the name. For instance, there is
an occupation index that divides the employees into each of the occupational
categories. For each category, we create a linked list. The index entry for the
category contains a field that holds the identifying information for the category
and a pointer to the first node in the list for that category. Figure 4.28 shows the
organization for our sample data using singly linked lists. Since we want to be
able to remove employees that leave the company easily, the singly linked struc­
ture appearing in Figure 4.28 is inadequate. Instead we actually want to represent
the multilist as doubly linked circular lists. Write a program that creates this struc­
ture and implements the following operations:
(a) insert a new employee record into the multilist
(b) remove an employee record from the multilist
(c) change the information for any field of the multilist, and relink the employee
record properly
(d) Query the multilist on any of the fields as described above.

4.9 REFERENCES AND SELECTED READINGS

For more information on pointers in C, consult R. Traister. Mastering C Pointers,


Academic Press, San Diego, Calif., 1990.
Additional Exercises 183

Node ID Number Name Occupation Location


A 30 Hawkins Programmer Minneapolis
B 25 Smith Analyst New York
C 60 Jones Programmer New York
D 55 Austin DataEntry Minneapolis
E 80 Messer Analyst Minneapolis

Figure 4.27: Sample set of employee data

Id Index Occupation Index

Max Ualue 30 60 90 Job Title Analyst Progranner DataEntry

Pointer Pointer

Ident if icat ion > NULL > NULL NULL


Occupat ion NULL NULL |-k NULL

Locat ion NULL -► NULL

Location M inneapo1 is NeuVork

Pointer
Location Index

Figure 4.28: Multilist structure represented as singly linked lists

4.10 ADDITIONAL EXERCISES

1. We can obtain a simpler and more efficient representation for sparse matrices if we
restrict our operations to addition, subtraction, and multiplication. In this
representation, nodes have down, right, row, col, and value fields. We represent
each nonzero term with a separate node and link these nodes together to form two
circular lists. We make the first list, the row list, by linking nodes by rows and
within rows by columns. This is done through the right field. We make the second
184 Lists

list, the column list, by linking nodes by columns and within columns by rows.
This is done through the down field. These two lists share a common head node. In
addition, we add a node that contains the dimensions of the matrix. Matrix a of
Figure 4.20 is shown in Figure 4.29.

Using the same assumptions as mread, write a function that reads in a sparse
matrix and sets up its internal representation. How much time does your function
take? How much additional space is needed?

a head node

0 2 1 0 2 1 3 3

11 12 -4 -15
4 4

solid lines = row links


broken lines = column links

Figure 4.29: Alternate sparse matrix representation

2. For the representation of Exercise 1, write functions that:


(a) erase a matrix
(b) add two matrices
(c) multiply two matrices
(d) print out a matrix.

For each of these operations, determine the computing time and compare these
times with those obtained using the representation found in Figure 4.21.
3. Compare the sparse matrix representations found in Figure 4.29 and Figure 4.21
with respect to the following operations:
(a) copy a matrix
Additional Exercises 185

(b) transpose a matrix


(c) output the entries in an arbitrary row
(d) output the entries in an arbitrary column.
4. § [Programming project] We want to implement a complete linked list system to
perform arithmetic on sparse matrices using the linked list representation found in
Figure 4.29. Create a user-friendly, menu-driven system that performs the follow­
ing operations:
(a) reads in a sparse matrix
(b) writes out the contents of a sparse matrix
(c) erases a sparse matrix
(d) adds two sparse matrices
(e) subtracts two sparse matrices
(f) multiplies two sparse matrices
(g) transposes a sparse matrix.
Test your system using suitable test data.

You might also like