Untitled
Untitled
This problem, the m anipulation of polynomials, has become a classic example of the use of list proc essing. As in Chapter 2, we wish to be able to represent any number of different polynomials as long as their combined size does not exceed our block of memory. In general, we want to represent the polynomial A(x)=amxem + + a1xe1, where the ai are nonzero coefficients and the exponents ei are nonnegative integers such that em>em-1>...>e2>e10. We will define a Polynomial class to implement polynomai ls. Since a polynomial is to be represented by a list, we have Polynomail IS-IMP LEMENTED-BY list. Definiton: We say that a data object of type A IS-IMPLEMENTED-IN-TERMS-OF a data objects of Type B if the Type B object is central to the implementation of the Type A object. This relationship is usually expressed by declaring the Type B ob ject as a data member of the Type A object. <S> Thus, we will make the linked list object poly a data member of Polynomial. We w ill employ the linked list template classes developed in Program 4.8 to implemen t the linked list. Each listnode will represent a term in the polynomial. To do this, the list template Type is instantiated to struct Term; where, Term consist s of two data members coef and exp. We use struct rather than class to define Te rm to emphasize that the data members of Term are public. The data members of Te rm are made public so that any function that has access to a Term object also ha s access to its data members. This does not violate data eccapsulation for Polyn omial because the linked list and its contents, including all Term objects are a ll private and cannot be accessed. Assuming thar the coefficients are integers, the required class declarations are developed in Program 4.20. Even though polynodes contain two fields data and link, and data contains two fi elds coef and exp, we will draw polynodes as follows for simplicity Coef Exp Link
For instance, the polynomials a = 3x14 + 2x8 +1 and b = 8x14-3x10+10x6 would be stored as in Figure 4.18. 4.6.2 Adding Polynomials To add two polynomials a and b, we use the list iterators Aiter and Biter to exa mine their terms starting at the nodes pointed to by a.first and b.first. Two va riables p and q are used to move along the terms of a and b. If the exponents of two terms are equal, then the coefficients are added. A new term is created for the result if the sum of the
Coefficient is not zero. If the exponent of the current term in a is less than t he exponent of the current term in b, then a copy of the term in b is created an d attached to the Polynomial object c. The variable q is advanced to the next te rm. Similar action is taken on a if p exp > q exp. Figure 4.19 illustrates the ad dition process on the polynomials a and b of the example of Figure 4.18 Each time a new node is generated, its coef and exp data members are set and it is appended to the end of c by function List::Attach (Program 4.11) provided by class List. The complete addition algorithm is specified by the operator+() (Pro
gram 4.12).
This is our first complete example of the use of list processing, so it should b e carefully studied. The basic algorithm is straightforward, using a merging pro cess that streams along the two polynomials, either copying terms or adding them to the result. Thus, the main while loop of lines 7-22 has three cases dependin g upon whether the exponents of the terms are = =,<, or >.
Analysis of operator+(): The following operations contribute to the computing ti me: Coefficient additions Exponent comparisons Additions/deletions to available space. Creation of new nodes for c Let us assume that each of these four operations, if done once, takes a single u nit of time. The total time taken by algorithm operator+() is then determined by the number of times these operations are performed. This number clearly depends on how many terms are present on the polynomials a and b. Assumes that a and b h ave m and n terms, respectively: a(x) = amxem + + a1xe1, b(x )= bnxfn + + b1xf1 where ai,bi 0 and em> > e1 0, fn > > f1 0 Then clearly the number of coefficient additions can vary as 0 coefficient additions min{m,n} The lower bound is achieved when none of the exponents are equal; the upper bond is achieved when the exponents of one polynomial are a subset of the exponents of the other polynomial. As for exponent comparisons, one comparison is made on each iteration of the fir st while loop. On each iteration either p or q or both of the move to the next t erm on their respective polynomials. Since the total number of terms is m + n. y ou can easily construct a case when m + n -1 comparisons will be necessary-e.g., m=n and en > fn > en-1 > fn-1 > > e2 > f2 > e1 > f1 The maximum number of terms in c is m +n, so no more than m +n new nodes are cre ated. In summary, the maximum number of executions of any of the statements in o perator+() is bounded above by m +n. Therefore, the computing time is O(m +n). I f the algorithm is implemented and run on computer, the time taken will be c1m + c2n + c3, where c1, c2, and c3 are constants. Since any algorithm that adds two polynomials must look at each nonzero term at least once, algorithm operator+() is optimal to within a constant factor. <S> 4.6.3 Erasing Polynomials The use of linked lists is well suited to polynomial operations. We can easily i
magine writing a collection of functions for input, output, assignment, addition , subtraction, and multiplication of polynomials using linked lists as the means of representation. Consider a function that reads in polynomials a(x), b(x), an d c(x) and then computes and prints d(x) = a(x)*b(x) + c(x): void func() { Polynomial a,b,c,d,t ; cin >> a ; // read and create polynomials cin >> b ; cin >> c ; t = a * b ; d = t + c count << d ;} What happens to these polynomials after function func has been exited? The answe r is that the memory occupied by the class object a, b, c, t, and d is returned once these objects go out of scope. Unfortunately, this mean that only the Polyn omial class objects and the List <Term> objects they physically contain are dele ted. ListNode <Term> objects are not physically contained in List <Term> objects (and therefore not physically contained in Polynomial objects). So, these objec ts do not get automatically freed up by the system. Worse still, it is not possi ble to access these objects because all first data members were deleted when the function was exited. The memory they occupy is lost to the program as it is not returned to the system. Figure 4.20 illustrates the process. If function func i s used many times in the program, it is possible that these lost nodes will even tually occupy all the memory causing the program to crash. To prevent this, we d efine a destructor to delete all objects that are conceptually part of a class b ut not physically part of it. The destructor is implemented in Program 4.22: Now, when a Polynomial object goes out of scope, the system first checks to see of a destructor exists for that class. If so, the destructor is executed. We hav e not defined a destructor on Polynomial, so a destructor is not executed. Next, if the class object contains other class objects as data members, they are dele ted. Polynomial contains a single data member poly of type list. Since a destruc tor is defined on List, it is executed on poly. Finally, the data members of pol y (poly . First and poly . Last) are deleted. 4.6.4 Circular List Representation of Polynomials It is possible to free all the nodes in a list more efficiently by employing a c ircular list instead of a chain. Figure 4.21 shows the circular list representat ion of a polynomial. We delete nodes that are longer in use is so that these nodes may be reused late r. This objective, together with an efficient erase algorithm for circular lists , may be met by maintaining our own list (as a chain) of nodes that have been del eted. When a new node is needed, we may examine this list. If this list is not em pty, then one of the nodes on it may be made available for use. Only when the li st is empty do we need to use command new to create a new node.
Figure 4.20 template <class type> List <Type>:: List() // Free all nodes in the chain {ListNode<Type>* next for (; first ; first = next) next = first link; delete first ;}
Let av be a static class member of CircList<Type> of type ListNode<Type>* that p oints to our chain of nodes that have been delted (We implement the chain directly rather than reusing our linked list class for efficiency reasons.) This list wi ll be henceforth be called available-space list or av list. Initially, av=0. Ins tead of using the commands new and delete, we shall now use the functions CircLi st::GetNode (Program 4.23) and CircList:: RetNode (program 4.24) template <class type> ListNode<Type>* CircList::GetNode() // Provide a node for use. {ListNode<Type>*x; if(!av)x = new listNode <Type>; else {x=av ; av =avlink ; } return x ; } Program 4.23: Getting a node As illustrated by function CircList <Type>:: CircList (Program 4.25), a circular list may now be erased in a fixed amount of time independent of the number of n odes on the list. Figure 4.22 is a schematic showing the link changes involves i n erasing a circular list. A direct changeover to the structure of Figure 4.21, however, causes some proble ms during addition and other operations, as the zero polynomial has to be handle d as a special case. To avoid such special cases you may introduce a head node i nto each polynomial (i.e. each polynomial, zero or nonzero, will contain one add itional node). The exp and coef data members of this node will not be relevant. Thus, the zero polynomial will have representation of Figure 4.23(a) and a = 3x1 4 + 2x8 + 1 will have the representation of Figure 4.23(b). For this circular list with head node representation, the test for first = 0 may be removed from CircList(). The only changes to be made for algorithm operator+ () (Pro template <class Type> void CircList <Type>::RetNode(ListNode <Type> * x) // Free the node pointed to by x {xlink = av; av = x ;} Program 4.24: Returning a node template <class KeyType> void CircList <Type>:: CircList () // Erase the circular list pointed by first. { if (first) { ListNode * second = first link; // second node firstlink = av; // first node linked to av av = second; // second node of list becomes front of av list first = 0;} } Program 4.25: Erasing a circular list gram 4.21) to work are in the implementation of the iterator functions First (), Next(), Not null (), and NextNotNull() and in the implementation of Attach (the
link field of the last node must point to the headnode). A further simplification in the addition algorithm is possible of the exp fiel o f head node is set to -1. Now when all nodes of a have been examined, p=a and ex p (p) = -1. Since -1 exp (q), the remaining term of b can be copied by further e xecution of the case statement. The same is true if all nodes of b are examined before those of a. This implies that there is no need for additional code to cop y the remaining terms as in operator+(). The final algorithm takes the simple fo rm given in program 4.26.
Figure 4.22
Figure 4.23 4.6.5 Summary Let us review what we have done so far. We have introduced the notions of a sing ly linked list (chain), and a singly linked circular list. Each node on one of t hese lists consists of exactly one link data member and some number of other dat a members. On all of our examples, all nodes on any given list had the same data members. The concept of a singly linked list does not require this property and in subsequent sections we shall see the lists that violate it. In dealing with polynomials, we found it convenient to use circular lists. This required us to introduce the notion of an available space list. Such a list cons ist of all nodes that have been used at least once and are currently not in use. By using the available-space list and the functions GetNode, RetNode, and CircL ist, it became possible to erase circular lists in constant time and to reuse al l nodes currently not in use. As Polynomial operator+ (const polynomial & a, const polynomial & b) { Term *p,*q, temp; CircListIterator<Term>Aiter(a.poly) ; CircListIterator<Term> Bitter (b.poly); P olynomial c;// assumes the constructer creates a head node with exp = -1 p = Ait er.First(); q = Biter.First(); while (1) switch (compare (pexp,qexp)) {case=: if (pexp = = -1) return c; else { int sum = p coef +q coef ; if (sum) { int sum = p coef +q coef ; if (sum) { temp.Init(sum,qexp); c.poly.Attach(temp) ; } p = Aiter.Next (); q= Biter.Next (); } break; case<: temp.Init(qcoef,qexp); c.poly.Attach(temp); q= Biter.Next(); break;
case>: temp.Init(pcoef,pexp); c.poly.Attach(temp); p= Aiter.Next(); }// end of switch and while Program 4.26 Adding circularly represented polynomials we continue, we shall see more problems that call for variations in node structu re and lists representation because of the operations we wish to perform. EXERCISE When a class object CO is passed by value, it is copied into the functio ns local store. The algorithm for copying the object is specified by the copy con structer. If the class definition of CO does not define the copy constructor, th e default copy constructor is used. The default copy constructor only copies all the object physically contained in CO. Define a copy constructor for a singly l inked list that copies all the nodes of the list into the new list. Write an algorithm to read in n pairs of coefficients and exponents, (ci , ei), 1 i n, of a univariate polynomial and to convert the polynomial into the circularly linked list representation described in this section. Assume that e i > ei+1, 1 i < n, and that ci 0, 1 i n. your algorithm should leave first pointi ng to the head node. Show that this operation can be performed in time O(n). Let a and b be two polynomials represented as circular lists with headno des. Write an algorithm to compute the product polynomial c=a*b. Your algorithm should leave a and b unaltered. Show that if n and m are the number of terms in a and b, respectively, then this multiplication can be carried out in time O(nm2 ) or O(mn2). If a and b are dense, show that the multiplication takes O(mn). Write an algorithm, Polynomial:: Eval (x), to evaluate the polynomial at the point x, where x is some real number. Assume that the polynomial is represe nted as a circularly linked list with a headnode. [Programming Project] Design a linked allocation system to represent and manipulate univariate polynomials with integers coefficients (use circular link ed lists with head nodes). Each term of the polynomial will be represented as a node. Thus, a node on this system will be have three data members as below: Exponent Coefficient Link
To erase polynomials efficiently, we need to use an available-space list and ass ociated functions as described in Section 4.6. The external (i.e. for input or o utput) representation of a univariate polynomial will be assumed to be a sequenc e of integers of the form: n, c1,e1,c2,e2,c3,e3,,cn,en, where ei represents an e xponent and ci a coefficient; n gives the number of terms in the polynomial. The exponents Constructor]: Initialize the polynomial *this to the are in decreasing order i.e ., e1>e2>>en. Write and test the following functions: istream& operator >> (istream& is, Polynomial & x): Read in an input pol ynomial and convert it to its circular list representing using a head node. ostream& operator<< (ostream& os, Polynomial& x): Convert x from its lin ked list representation of its expternal representation and output it.
Polynomial:: Polynomial (const polynomial& a) [Copy polynomial a. const Polynomial& Polynomial:: operator = (const Polynomial& a) [Assignm ent Operator]: Assign polynomial a to * this. Polynomial:: Polynomial () [Destructor]: Return all nodes of the polynom ial *this to the available-space list. Polynomial operator+ (const Polynomial& a, const Polynomial& b) [additio n]: Create and return the polynomial a+b. a and b are to be left unaltered. Polynomial operator- (const Polynomial& a, const Polynomial& b) [subtrac tion]: Create and return the polynomial a-b. a and b are to be left unaltered. Polynomial operator* (const Polynomial& a, const Polynomial& b) [multipl ication]: Create and return the polynomial a*b. a and b are to be left unaltered . float polynomial:: Evaluate (float x): Evaluate the polynomial *this at x and return the result. 4.7 EQUIVALENCE CLASSES Let us put together some of these ideas on linked and sequential representations to solve a problem that arises in the design and manufacture of very large-scal e integrated (VLSI) circuits. One of the steps in the manufacture of a VLSI circ uit involves exposing a silicon wafer using a series of masks. Each mask consist s of several polygons. Polygons that overlap are electrically equivalent. Electr ical equivalence specifies a relationship among the mask polygons. This relation has several properties that it shares with other relations, such as the convent ional mathematics equivalence. Suppose we donate an arbitrary relation by the sy mbol , and suppose that For any polygon x, x x (e.g. x is electrically equivalent to itself). Th us, is reflexive. For any two polygons x and y, if x y, then y x. Thus, the relation is symmetric. For any three polygons x, y and z if x y and y z then xz (e.g. if x and y are electrically equivalent and y and z are also, then so also are x and z). The relation is transitive. Definition: A relation over a set S, is said to be an equivalence relation over S if it is symmetric, reflexive, and transitive over S. Equivalence relations are numerous. For example, the equal to (=) relationship is an equivalence relation, since (1) x = x, (2) x = y implies y=x, and y=z implies x=z. One effect of the equivalence relation is to partition the set S onto equi valence classes such that two members x and y of S are in the same equivalence c lass if x y. For example, if we have 12 polygons numbered 0 through 11 and the f ollowing overlaps pairs are defined: 04, 3 1, 6 10, 8 9, 7 4, 6 8, 3 5, 2 11, and 11 0 then, as a result of the reflexive, symmetry and transitivity of the relation , w e get the following partition of the 12 polygons into three equivalence classes: {0,2,4,7,11}; {1,3,5}; {6,8,9,10}
These equivalence classes are important, as each such class defines a signal net . The signal nets can be used to verify the correctness of the masks. The algorithm to determine equivalence classes works in essentially two phases. In the first phase the equivalence pairs (i,j) are read in and stored. In phase two we begin at 0 and find all pairs of the form (j,k) imply k is in the same cl ass as 0. We continue in this way until the entire equivalence class containing 0 has been found, marked, and printed. Then we find an object not yet output. Th is is in a new equivalence class. The objects in this equivalence class are foun d as before and output. void equivalence () { initialize; while more pairs { read the next pair (i,j); process this pair; } initialize for output; for (each object not yet output) output the equivalence class that contains this object; } // end of equivalence Program 4.27: First pass at equivalence algorithm The first design for this algorithm might be as in Program 4.27. Let m and n rep resent the number of related pairs and the numbers of objects, respectively. Now we need to determine which data structure should be used to hold these pairs. E asy random access would be suggest a Boolean array, say pairs [n][n]. The elemen t pairs [i][j] = TRUE if and only if i and j are paired directly in the input. However, this could potentially be wasteful of space since very few of the array elements may be used. Any algorithm that uses this data structure would require <s> (n2) time, just to initialize the array. These considerations lead us to consider a linked list to represent each row. Ea ch node on the list requires only a data and a link field. However, we still nee d random access to the ith row, so a one-dimensional array, seq[n], can be used as the head nodes of the n lists. Looking at the second phase of the algorithm we need a mechanism that tells us w hether or not object i is yet to be printed. A Boolean array, out [n], can be us ed for this. The next refinement of the algorithm (Program 4.28) follows. void equivalence() { read n;// read in number of objects initialize seq to 0 and out to FALSE; while more pairs // input 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] == FALSE) { out [i] = TRUE; output the equivalence class that contain object i ; } } // end of equivalence
Program 4.28: A more detailed version of equivalent algorithm Let us simulate the algorithm, as we have it so far, on the previous data set. A fter the while loop is completed the lists will look as they do in Figure 4.24. For each relation i=j, two nodes are used. seq [i] points to a list of nodes tha t contains every number directly equivalent to i by an input relation. In phase two we can scan the seq array and start with the first, i, 0 i n such t hat out[i]==FALSE. Each element in the list seq [i] is printed. To process the r emaining lists which, by transitivity, belong in the same class as i, a stack of their nodes is created. This is accomplished by changing link data members so t hey point in the reverse direction. The complete function is given in Program 4. 29.
Figure 4.24 enum Boolean {FALSE, TRUE}; class ListNode { friend void equivalence () ; private: int data ; ListNode*link ; ListNode (int) ; }; typedef ListNode*ListNodePtr; // so we can create an array of pointers using new ListNode :: ListNode (int d) { data = d; link = 0; } void equivalence() // Input the equivalence pairs and output the equivalence classes { ifstream inFile (equiv.in,ios::in) ; // equiv.in is the input file if(!inFile) { cerr << Cannot open input file << end1 ; return; } int i,j,n; inFile >> n ; // read number of objects //initialize seq and out ListNodePtr * seq = new ListNodePtr [n]; Boolean * out = new Boolean [n] ; for (i=0; i < n;i++) { seq [i] = 0; out [i] = FALSE; } // PHASE 1: input equivalence pairs infile>>i>>j; while (inFile.good()) { // check end of file ListNode *x = new ListNode (j); x link = seq[i]; seq[i] = x; // add j to seq[i] ListNode *y = new ListNode (i); y link = seq[j]; seq[j] = y; // add i to seq[j] inFile >> i >> j ; } // Phase 2: output equivalence classes for (i =0; i<n ; i++) if (out[i] ==FALSE) {// needs to be output
count <<endl<< A new class << i; out[i]= TRUE; ListNode*x = seq [i]; ListNode* top= 0 ; // init stack while (1) {// find rest of class while (x) {// process the list j = x data; if (out [j]== FALSE) { count << , << j ; out [j] == TRUE ; ListNode*y =x link; x link =top ; top = x; x = y; } else x = x link; } //end of while(x) if (!top) break; else { x= seq [top data] ; top = top link; // unstuck } } // end of while (1) } // end of if (out[i] == FALSE) for (i=0; i < n; i++) while (seq[i]) { ListNode * delnode = seq[i]; seq[i] = delnode link; delete delnode; } delete delnode; } delete [ ] seq; delete [ ] out; } // end of equivalence Program 4.29: Algorithm to find equivalence classes Analysis of equivalence: This initialization of seq and out takes O(n) time. The processing of each input pair in phase 1 takes a constant amount of time. Hence , the total time of this phrase is O (n + m), where m is the number of input pai rs. In phase 2 each node is put onto the linked stack at most once. Since there are only 2m nodes and the for loop is executed n times, the time for this phase is O (m + n). Hence, the overall computing time is O (m + n). Any algorithm that processes equivalence relations must look at all m equivalence pairs and at all n polygons at least once. Thus, no algorithm can have a computing time less tha n O (m + n). This means that the function equivalence is optimal to within a con stant factor. Unfortunately, the space required by the algorithm is also O (m +n ). In Chapter 5 we shall see an O (n) space solution to this problem. 4.8 SPARSE MATRICES 4.8.1 Sparse Matrix Representation In chapter 2, we saw that when matrices were sparse (i.e., many of the entries w ere zero), then much space and computing time could be saved of only the nonzero terms were retained explicitly. In the case where these nonzero terms did not f orm any nice pattern such as a triangle or a band, we devised a sequential scheme in which each non-zero term was represented by a structure with three data membe rs: row, column, and value. These terms were organized sequentially. However, as matrix operations such as addition, subtraction, and multiplication are perform ed, the number of nonzero terms in matrices will vary. Matrices representing par
tial computations (as in the case of polynomials) will be created and will have to be destroyed later to make space for additional matrices. Thus, sequential sc hemes for representing sparse matrices suffer from the same inadequacies as simi lar schemes for polynomials. In this section we study a very general linked-list scheme for sparse matrix representation. As we have already seen, linked scheme s facilitate efficient representation of varying-size structures, and here, too, our scheme overcomes the aforementioned shortcomings of the sequential represen tation studied in Chapter 2. In the data representation we use, each column of a sparse matrix is represented by a circularly linked list with a head node. In addition, each row is also a c ircularly linked list with a head node. Each node is represented by the class MatrixNode. This class has a field called head, which is used to distinguish between head nodes and nodes representing non zero matrix elements. Each head node has three additional fields: down, right, a nd next. The total number of head nodes is max {number of rows, number of column s}. The head node for row i is also the head node for column i. The down field o f a head node is used to link into a column list; the right field is used to lin k into a row list. The next field links the head nodes together. All other nodes have six fields: head, row, col, down, right, and value (Figure 4.25). The down field is used to link to the nonzero term in the same column and the right field links to the next nonzero term in the same row. Thus, if aij 0, then there is a node with head = FALSE, value =aij, row = i and col = j. This n ode is linked onto the circular linked list for row i and column j. Hence, the n ode is simultaneously in two different lists.
Figure 4.25 As noted earlier, each head node is in three lists: a row list, a column list, a nd a list of head nodes. The list of head nodes itself has a head node that is i dentical to the nodes used to represent nonzero elements. The row and col fields of the node are used to store the matrix dimensions. The entire matrix is repre sented by class Matrix with the data member headnode, which points to this node. The linked structure obtained for the 6 x 7 matrix, A, of Figure 4.26 is shown i n Figure 4.27. Although Figure 4.27 does not show the value of the head fields, these values are readily determined form the node structure shown. For each nonz ero term of A, we have one six-field node that is exactly one column list and on e row list. The head nodes are marked H0 to H6 and are drawn twice to simplify t he figure. As seen in the figure, the right field of headnode is used to link in to the list of head nodes. Notice that the whole matrix may be referenced throug h the head node, headnode, of the list of head nodes.
Figure 4.26 If we wish to represent an n * m sparse matrix with r nonzero terms, the number of nodes needed is max {n,m} + r + 1. Although each node may require several wor ds of memory, the total storage needed will be less than nm for sufficiently sma ll r. The required node structure may be defined in C++ using an anonymous union as in Program 4.30. Since all nodes contain fields head, down, and right, these are d eclared outside the anonymous union. Head nodes also contain next while all othe r nodes contain row, col, and value. These fields are all incorporated into stru ct Triple. Space is allocated by the union declaration for the larger of fields next and triple (which is an object of type Triple). Because of this, fields row , col, and values are accessed indirectly through triple <s>
4.8.2 Sparse Matrix Input The first operation we shall consider is that of reading in a sparse matrix and obtaining its linked representation. We assume that the first input line consist of s.row (the number of rows), s.col (the number of columns), and s.value (the number of nonzero terms). The line is followed by s.value lines of input; each o f these is a triple of the nonzero terms of matrix. We also assume that these tr iples are ordered by rows and within rows by columns. For example, the input for the 6 x 7 sparse matrix of Figure 4.26, which has sev en nonzero terms, would take the following form: 6,7,7;0,2,11;0,5,13;1,0,12;1,6, 14;2,1,-4;2,5,-8;5,1,-9. We shall not concern ourselves here with the actual for mat of this input on the input media (tape, disk, terminal, etc) but shall assum e that we have a mechanism to get the next triple (see Exercise for a possible i nput format). The function operator>> (Program 4.31) also makes use of an auxili ary array head, of size max (s.row, s.col). head [i] is a pointer to the head no de for column i and hence also for row i, permitting us efficient random access to columns while the input matrix is set up. Function operator>> proceeds first by setting up all the head nodes and then by setting up each row list, simultane ously building the column lists. The next data member of head node I is used ini tially to keep track of the last node in column i. Eventually, in line 30, the h ead nodes are linked together through this data member.
Figure 4.27 enum Boolean {FALSE, TRUE}; struct Triple {int value, row, col;}; class Matrix ; // forward declaration class MatrixNode { friend class Matrix; friend istream& operator>> (istream&, Matrix&); // for reading in a matrix private: MatrixNode * down, *right; Boolean head ; uninon { // anonymous union MatrixNode * next ; Triple triple ; }; MatrixNode (Boolean, Triple*) ; // construct }; MatrixNode::MatrixNode (Boolean b, Triple*t) // constructor { head=b if (b) {right = next = down = this;} //row/column head node else triple = *t ; // head node for list of headnodes OR element node } typedef MatrixNode * MatrixNodePtr ; // to allow subsequent creation of array of pointers class Matrix { friend istream& operator>> (istream&, Matrix&); public: Matrix() ; // destructor private: MatrixNode *headnode ; };
Program 4.30: Class definition for sparse matrices Analysis of operator>>: Since new works in a constant amount of time, all the he ad nodes may be set up in 0(max{n,m}) time, where n is the number of rows and m the number of columns on the matrix being input. Each nonzero term can be setup in a constant amount of time because of the use of the variable last and a rando m access scheme for the bottommost node in each column list. Hence, the for loop of lines 16-27 can be 1 istream& operator>> (istream& is, Matrix& matrix). 2 // Read in a matrix and set up its linked representation. 3 // An auxiliary array head is used. 4 { 5 Triple s; int p ; 6 is>> s.row>>s.col>>s.value; // matrix dimensions 7 if (s.row > s.col) p = s.row; else p = s.col; 8 // set up headnode for list of head nodes. 9 matrix.headnode = new MatrixNode (False, &s); 10 if (p=0) { matrix.headnoderight = matrix.headnode; return is ;} 11 // at least one row or column 12 MatrixNodePtr* head = new MatrixNodePtr [p] ; // initialize head nodes 13 for (int i = 0 ; i < p; i++) 14 head [i] = new MatrixNode (TRUE, 0) ; 15 int CurrentRow = 0 ; MatrixNode * last = head [0] ; // last node in current r ow 16 for (i=0 ; i < s.value ; i++) // input triples 17 { 18 Tripe t ; 19 is >> t.row >> t.col >> t.value ; 20 if (t.row > CurrentRow) { // close current row 21 lastright = head [CurrentRow]; 22 CurrentRow = t.row ; 23 last = head [CurrentRow] ; 24 } // end of if 25 last = lastright = new MatrixNode (FALSE,&t) ; // link new node into row list 26 head [t.col]next = head [t.col]nextdown = last ; // link into column lists 27 } // end of for 28 lastright = head [CurrentRow] ; // close last row 29 for (i=0 ; i < s.col; i++) head [i]nextdown =head [i] ; // close all column lis ts 30 // link the head nodes together 31 for (i=0; i < p-1; i++) head[i]next = head [i + 1] ; 32 head[p-1]next = matrix.headnode ; 33 matrix.headnoderight = head [0]; 34 delete [ ] head; 35 return is ; 36 } Program 4.31: Reading in a sparse matrix carried out in O(r) time. The rest of the algorithm rakes O(max{n,m}) time. The total time is therefore O(max{n,m}+ r) = O (n+m+r )). Note that this time is asy mptotically better than the input time of O(nm) for an n * m matrix using a twodimensional array but slightly worse than the sequential sparse method of Sectio n 2.3. 4.8.3 Erasing a Sparse Matrix All the nodes of a sparse matrix may be returned once at a time using delete. A faster way to return the nodes is to set up an available space list as was done
for polynomials. Assume that av points to the front of this list and that this l ist is linked through the data member right. Function Matrix::Matrix() (Program 4.32) solves our problem in an efficient way. Matrix::Matrix() // Returm all nodes to the av list. This list is a chain linked via the right // field.av is a global variable of type MatrixNode * and points of its first no de. { if (!headnode) return; // no nodes to dispose MatrixNode * x= headnode right, * y ; headnode right =av ; av = headnode ; // return headnode while (x != headnode) {// erase by rows y = xright ; x right=av; av=y x = x next; // next row } headnode =0 ; } Program 4.32: Erasing a sparse matrix Analysis of Matrix( ): Since each node is in only one row list, it is sufficient to return all the row lists of the matrix a. Each row list is circularly linked through the data member right. Thus, nodes need not be returned one by one as a circular list can be erased in a constant amount of time. The computing time fo r the algorithm is readily seen to be 0(n+m). Note that even if the available sp ace list had been linked through the data member down, erasing still could have been carried out in 0(n+m) time. The subject of manipulating sparse matrix structures is studied further in the e xercises. The representation studied here is rather general. For most applicatio ns this generality is not needed. A simpler representation resulting in simpler algorithm is discussed in the exercise. EXERCISE In Exercise 1 to 5, the sparse matrix representation described in this section i s assumed. Let a and b be two sparse matrices. Write an algorithm, operator+ (a,b), to create the matrix c=a+b. your algorithm should leave the matrices a and b un changed and set up c as a new matrix in accordance with this data representatio n. Show that if a and b are n*m matrices with rA and rB nonzero terms, them this addition can be carried out in 0(n+m+rA+rB) time. Let a and b be two sparse matrices. Write an algorithm operator* (a,b), to set up the structure for c= a*b. Show that if a is an n*m matrix with rA nonz ero terms and if b is an m*n matrix with rB nonzero terms, then c can be compute d in time 0(prA + nrB ). Can you think of a way to compute c in 0(min{prA ,nrB}) ? Write an algorithm operator<< ( ) to write out the terms of a sparse mat rix a as triples (i,j,aij). The terms are to be output by rows and within rows b y columns. Show that this operation can be performed in time 0(n+rA) if there ar e rA nonzero terms in a and a is an n*m matrix. Write an algorithm Transpose (a), to compute the matrix b = aT , the tra nspose of the sparse matrix a. what is the computing time of your algorithm?
Design an algorithm to implement the copy constructor for sparse matrice s. What is the computing time of your algorithm? A simpler and more efficient representation for sparse matrices can be o btained when no one is limited to the operations of addition, subtraction, and m ultiplication. Now, nodes have the data member down, right, row, col, and value. Each nonzero term is represented by a node. These nodes are linked together to form two circular lists. The first list, the row list, is made up by linking nod es by rows and within columns. The linking is done via the right data member. Th e second list, the column list, is made up by linking nodes via the down data me mber. In this list, nodes are linked by columns and within columns by rows. Thes e two lists share a common head node. In addition, a node is added to contain th e dimensions of the matrix. The matrix A of Figure 4.26 has the representation s hown in Figure 4.28. Provide a C++ class definition for this representation. For the representation of Exercise 6, write algorithms to (a) read a matrix (operator>>) (b) erase a matrix (c) add two matrices (operator+)
Figure 4.28: Representation of matrix A of Figure 4.26 using the scheme of Exerc ise 6 (d) multiply two matrices (operator*) (e) print out a matrix (operator<<) For each of these algorithms obtaining computing times. How do these times compa re with the corresponding times for the representation of this section? Compare the sparse representations of Exercise 6 and this section with r espect to some other operations. For example, how much time is needed to output the entries in an arbitrary row or column? [Programming Project] In this project, we shall implement a complete a c omplete linked-list system to perform arithmetic on sparse matrices using the re presentation of Section 4.8. First design a convenient node structure assuming v alue is an integer. Since we shall need to erase circular lists, we shall utilize the available- spa ce list concept described in the section. So, we may begin by writing and testin g the function associated with this list. Next, write and test the following fun ctions for matrix operations: (a) istream& operator>> (istream& os, Matrix& m): Read in the matrix and set it up according to the representation of this section . The first input line gives the matrix dimensions. The next several lines conta in one triple, (row, column, value), each. The last triple ends the input file. These triples are in increasing orders by rows. Within rows, the triples are in increasing order of columns. The data is to be read in one line at a time and co nverted to internal representation.
(b) ostream& operator<<(ostream& os, const Matrix& m): (c) Matrix::Matrix(const Matrix& a) [Copy Constructor]: Initializing the sparse matrix* this to the sparse matrix a. (d) const Matrix& Matrix::operator=(const Matrix& a) [Assignment Operator]: Assi gn sparse matrix a to * this. (e) Matrix::Matrix() [Destructor]: Return all nodes of the sparse matrix * this to the available-space list. (f) Matrix operator+ (const Matrix& a, const Matrix& b): Create and return the s parse matrix a+b. a and b are to be left unaltered. (g) Matrix operator- (const Matrix& a, const Matrix& b): Create and return matri x a-b. a and b are to be left unaltered. (h) Matrix operator* (const Matrix& a, const Matrix& b): Create and return matri x a*b. a and b are to be left unaltered. (i) Matrix Matrix:: Transpose (const Matrix& a): Create the sparse matrix a1. a is to be left unaltered. [Programming Project] Do Exercise 9 using the representation of Exercise 6. 4.9 DOUBLY LINKED LISTS So far we have been working chiefly with singly linked linear lists. For some pr oblems these would be too restrictive. One difficulty with these lists is that i f we are pointing to a specify node, say p, then we can easily move only in the direction of the links. The only way to find the node that precedes p is to star t at the beginning of the list. The same problem arises when one wishes to delet e an arbitrary node from a singly linked list. As can be seen from Example 4.4, easy deletion of an arbitrary node requires knowing the preceding node. If we ha ve a problem in which moving in either direction is often necessary, then it is useful to have doubly linked lists. Each node now has two link data members, one linking in the forward direction and one in the backward direction.
Figure 4.29: Doubly linked circular list with head node A node in a doubly linked list has at least three fields- e.g., data, llink (lef t link) rlink (right link). A doubly linked list may or may not be circular. A s ample doubly linked circular list with three nodes is shown in Figure 4.29. Besi des these three nodes, a special node called a head node has been added. As was true in the earlier sections, head nodes are convenient for the algorithms. The data field of the head node usually contains no information. Now suppose that p points to any node in a doubly linked list. In this case p== pllinkrlink == prlinkll ink. Thos formula reflects the essential virtues of this structure- namely, that one can go back and forth with equal ease. An empty list is not really empty, s ince it always has its head node and it appears as in Figure 4.30. Program 4.33 contains the class definition of a doubly linked list of integers. To work with these lists we must be able to insert and delete nodes. Function DblList::Delete (Program 4.34) deletes node x now points to a node that
Figure 4.30: Empty doubly linked circular list with head node class DblList ; class DblListNode { friend class DblList ; private: int data ; DblListNode *llink, *rlink ; }; class DblList { public: // List manipulation operations . . private: DbllistNode *first ; // points to head node }; Program 4.33: Class definition of a doubly linked list is no longer part of the list. Figure 4.31 shows how the method works on a doubl y linked list with only a single node. Even though the rlink and llink data memb ers of node x still point to the head node, this node has effectively been remov ed, as there is no way to access x through first. Insertion is only slightly mor e complex (see Program 4.35). EXERCISES Devise a representation of a list in which insertions and deletion can b e made at either end. Such a structure is called a deque. Write a function for i nserting at either end. void DblList::Delete(DblListNode *x) { If (x==first) cerr << Deletion of head node not permitted << endl; else { xllinkrlink = xrlink ; xrlinkllink = xllink ; delete x ; } } Program 4.34: Deletion from a doubly linked circular list
Figure 4.31: Deletion from a doubly linked circular list void DblList::Inset(DblListNode *p, DblListNode * x) // insert node p to the right of node x { pllink = x ; prlink = x rlink ; xrlinkllink = p ; xrlink = p ; } Program 4.35: Insertion into a doubly linked circular list
Consider the operation XOR (exclusive OR, also written as ) defines as fo llows (for i, j binary) i j = {(0 if i and j are identical@1 otherwise) This definition differs from the usual OR of logic, which is defined as i OR j = {(0 if i=j=0@1 otherwise) The notation gives up a space- saving device for storing the right and left link s of a doubly linked list. The nodes will now have only two data members: info a nd link. If l is to the left node x and r to its right, then link(x) = l r (as i n the case of storage management algorithms, we assume that available memory is an arbitrary memory [i], 1 i n, and that the link data member just gives us the position of the next node in this array). For the leftmost node l =0, and for t he rightmost node r =0. Let (l, r) be a doubly linked list to represent; l point s to the leftmost node and r to the rightmost node in the list. Write an algorithm to traverse the doubly linked list (l, r) from left t o right, listing the contents of the info data member of each node. Write an algorithm to traverse the list right to left, listing the conte nts of the info data member of each node. [Programming Project]: Implement doubly linked circular lists with head nodes using three template classes (similar to our implementation of singly link ed lists). In addition to the iterator member functions developed for chains, de fine an iterator member function that returns the previous list element. 4.10 GENERALIZED LISTS 4.10.1 Representation of Generalized lists In Chapter 2 a linear list was defined to be a finite sequence of n 0 elements, 0 , .n-1, which we write as A = (0,, n-1). The elements of a linear list are restricted to atoms; thus, the only structural property a linear list has is that of posit ion (i.e. i precedes i+1, 0 i n-1). Relaxing this restriction on the elements of a list and permitting them to have a structure of their own leads to the notion o f a generalized list. Now, the elements i, 0 i n-1, may be either atoms or lists. Definition: A generalized list, A, is a finite sequence of n 0 elements, 0,.n-1 whe re i is either an atom or a list. The elements i, 0 i n-1, that are not atoms are said to be sublists of A. The list A itself is written as A = (0, .n-1). A is the name of the list (0, .n-1), an d n is the length of the list. By convention, all list names are represented by a capital letters. Lowercase letters are used to represent atoms. If n 1, then 0 is the head of a A, and (0, .n-1) is the tail of A. This definition is our first example of a recursive definition, so study it care fully. The definition is recursive because within our description of a list, we use the notion of a list. This may appear to be circular, but it is not. It is a compact way of describing a potentially large and varied structure. We will see more such definitions later. Some examples of generalized lists are D = ( ): the null, or empty, list; its length is zero. A = (a, (b,c)): a list of length two; its first element is the atom a, a nd its second element is the linear list (b,c).
B = (A,A,()): a list of length three whose first two elements are the li st A, and the third element is the null list. C = (a,C): a recursive list of length two; C corresponds to the infinite list C = (a, (a, (a,). D is the empty list. For list A, we have head (A) = a and tail (A) = ((b,c)); tail (A) also has a head and tail, which are (b,c) and ( ), respectively. Looking at this list B, we see that head (B) = A and tail (B) = (A, ( )). Continuing, we h ave head (tail (B)) = A and tail (tail (B)) = (( )), both of which are lists. Two important consequences of our definition for a list are (1) lists may be sha red by other lists as in example (3), where list A makes up two of the sublists of B; and (2) lists may be recursive as in example (4). The implications of thes e two consequences for the data structures needed to represent lists will become evident. First, let us restrict ourselves to the situation in which the lists being repre sented are neither shared nor recursive. To see where this notion of a list may be useful, consider the problem of representing polynomials in several variables . For example. P (x,y,z) = x10 y3 z2 + 2x8 y3 z2 + 3x8 y2 z2 + x4 y4 z + 6x3 y4 z + 2yz You can easily think of a sequential representation for P, say using a structure with four fields to represent a single array element: coef, expx, expy and expz . But this would mean that polynomials in a different number of variables would need a different number of fields, adding another conceptual inelegance to other difficulties we have already seen with the sequential representation of polynom ials. If we used linear lists, we might conceive a node of the form. coef expz expx Link expy
These nodes would have to vary in size depending on the number of variables, cau sing difficulties in storage management. The idea of using a general list struct ure with nodes of fixed size arises naturally if we consider rewriting P (x,y,z) , we see that there are two forms in the variable z, Cz2 and Dz, where C and D a re polynomials themselves, but in the variable x and y. Looking more closely at C (x,y), we see that it is of the form Ey3 + Fy2, where E and F are polynomials in x. Continuing in this way, we see that every polynomial consists of a variabl e plus coefficient-exponent pairs. Each coefficient is itself a polynomial (in o ne less variable) if we regard a single numerical coefficient as a polynomial in zero variables. We see that every polynomial, regardless of the number of variables in it, can b e represented using nodes of the type PolyNode, defined as enum Triple { var, ptr, no }; Class PolyNode { PolyNode *link ; int exp ; Triple trio ; union { char vble ; polyNode *dlink ; int coef ; }; }; In this representation, there are three types of nodes, depending on the value o f trio.if trio == var, then the node is the head node for a list; in this case,
the field vble is used to indicate the name of the variable on which that list i s based and the exp field is set to 0. Note that the type of data member vble ca n be changed to int if all varialbles are kept in a table and vble just gives th e corresponding table index. If trio==ptr, then the coefficient is itself a list and is pointed by the field dlink. If trio == no, then the coefficient is an in teger and is stored in the field coef. In both these cases, exp represents the e xponent of the variable in which that list is based. The polynomial P =3x2y now takes the representation given in Figure 4.32. here P points to a head node which indicates that the upper list is based on y. Hence the next term in the list refer to them y1 = y. The coefficient of this term is the polynomial represented by the lower list. This polynomial is 3x2. The polyno mial P (x,y,z) defined before has the list representation shown in Figure 4.33. For simplicity, the trio data member is omitted from Figure 4.33. The value of t his data member for each node is self-evident.
Figure 4.32: Representation of 3x2y Every generalized list can be represented using the node structure tag = FALSE/TRUE data/dlink link
This data structure may be defined in C++ as: enum Boolean { FALSE, TRUE }; class GenList ; // forward declaration class GenListNode { friend class GenList ; private: GenListNode *link ; Boolean tag ; union { char data ; GenListNode *dlink ; }; };
Figure 4.33: Representation of P (x,y,z) class GenList { public: // List manipulation operations private: GenListNode *first ; }; where the type of the data field changes from one application to the next. The d ata/dlink data member holds an atom if head (A) is an atom and holds a pointer t o the list representation of head (A) if head (A) is a list. (The exercises exam ine hoe a multivariate polynomial could be stored using this structure.) Using t his node structure, the example lists (1) to (4) have the representation shown i n Figure 4.34.
Figure 4.34: Representation of lists (1) to (4) (page 221; an f in the tag data member represents the value false, whereas a t represents the value true) 4.10.2 Recursive Algorithms for Lists Now that we have seen a particular example where generalized lists are useful, l et us return to their definition. When a data object is defined recursively, it is often easy to describe algorithms that work on these objects recursively. A r ecursive algorithm consists of two components- the recursive function itself (th e workhorse) and a second function that invokes the recursive function at the to p level (the driver). Therefore, member function while the workhorse is declared as a private member function. 4.10.2.1 Copying a List To see how recursion is useful, let us write a function (Program 4.36) that prod uces an exact copy of a nonrecursive list l in which no sublists are shared. We will assume the nodes of l are of type GenlistNode as defined earlier. // Driver void GenList::Copy (const GenList& l) { first = Copy (l.first) ; } // Workhorse GenListNode* GenList::Copy(GenListNode*p) // Copy the nonrecursive list with no shared sublists pointed at by p { GenListNod *q = 0 ; if (p) { q = new GenListNode ; qtag = ptag ; if (!ptag) qdata = pdata ; else qdlink = Copy (pdlink) ; qlink = Copy (plink) ; } return q ; } Program 4.36: Copying a list Program 4.36 reflects exactly the definition of a list. We see immediately that Copy works correctly for an empty list. A simple proof using induction will veri fy the correctness of the entire function. Now let us consider the computing tim e of this algorithm. The empty list takes a constant amount of time. For the lis t A = ((a,b),((c,d),e)), which has the representation of Figure 4.35, p takes on the values given in Figure 4.36. The sequence of values should be read down the columns; b, r, s, t, u, v, w, and x are the addresses of the eight nodes of thi s list. From this example one should be able to see that nodes with tag = FALSE will be visited twice, whereas nodes with tag = TRUE will be visited three times . Thus, if a list has a total of m nodes, no more than 3m executions of any stat ement will occur. Hence the algorithm is O (m), or linear, which is the best we can hope to achieve. Another factor of interest is the maximum depth of recursio n or, equivalently, how many locations are needed for the recursion stack. Again , by carefully following the algorithm on the previous example, we see that the maximum depth is a combination of the lengths and depths of all sublists. Howeve
r, a simple upper bound is m, the total number of nodes. Although this bound wil l be extremely large in many cases, it is achievable, for instance, if A = ((((( a))))).
Figure 4.36 Values of parameters in execution of GenList::Copy (A) 4.10.2.2 List Equality Another useful function determines whether two lists are identical. To be identi cal, the list must have the same structure and the same data in corresponding da ta members. Again, using the recursive definition of a list, we can write a shor t recursive function (Program 4.37) to accomplish this task. // Driver assumed to be a friend of Genlist int operator == (const GenList& l, const GenList& m) // l and m are non-recursive lists. // The function returns 1 if the two lists are identical and 0, otherwise. { return equal (l, first, m, first) ; } // Workhorse assumed to be a friend of GenListNode int equal (GenListNode *s, GenListNode *t) { int x ; if ((!s) && (!t)) return 1 ; if (s && t && (stag == ttag)) { if (!stag) if (sdata == tdata) x = 1 ; else x = 0 ; else x = equal (sdlink, tdlink) ; if (x) return equal (slink, tlink) ; } return 0 ; } Program 4.37: Determining if two lists are identical Function operator == returns the value 1 or 0. Its computing time is clearly no more than linear when no sublists are shared, since it looks at each node of l a nd m no more than three times. For unequal lists the function terminates as soon as it discovers that the lists are not identical. Another handy operation on nonrecursive lists is the function that computes the depth of a list. The depth of the empty list is defined to be zero and, in gener al. Depth (s) = {(0 epth (x) } if s is the list (x,,x),n 1 ) if s is an atom@1+max {depth (x),,d
Function depth (Program 4.38) is a very close transformation of the definition, which is itself recursive. By now you have seen several programs of this type, a nd you should be feeling more comfortable both reading and writing recursive alg orithms. // Driver int GenList::depth () // compte the depth of a non-recursive list { return depth (first) ; } // Workhorse int GenList::depth(GenListNode *s) { if (!s) return 0 ; GenListNode *p = s ; int m =0 ; while (p) { if (ptag) { int n = depth (pdlink) ; if (m<n) m = n ; } p = plink ; } return m+1 ; } Program 4.38: Computing the depth of a list 4.10.3 Reference Counts, Shared and Recursive Lists In this section we shall consider some of the problems that arise when lists are allowed to be shared by other lists and when recursive lists are permitted. Sha ring of sublists can, in some situations, result in great savings in storage use d, as identical sublists occupy the same space. To facilitate specifying shared sublists, we extend the definition of a list to allow for naming of sublists. A sublist appearing within a list definition of a list may be named through the us e of a list name preceding it. For example, in the list A = (a, (b, c)), the sub list (b,c) could be assigned the name Z by writing A = (a,Z(b,c)). In fact, to b e consistent, we should then write A (a,Z(b,c)) which would define the list a as above. Lists that are shared by other lists, such as list A of Figure 4.34, create prob lems when you wish to add or delete a node at the front. If the first node of A is deleted, it is necessary to change the pointers from the list B to point to t he second node. If a new node is added, pointers from B have to be changed to po int to the new first node. However, we normally do not know all the points from which a particular list is being referenced. (Even if you did have this informat ion, addition and deletion of nodes could require a large amount of time.) This problem is easily solved through the use of head nodes. If you expect to perform any additions and deletions at the front of lists, then the use of a head node with each list or named sublist will eliminate the need to retain a list of all pointers to any specific list. If each list is to have a head node, then lists ( 1) to (4) are represented as in Figure 4.37. The values in the data/dlink fields of the head nodes is the reference count of the corresponding list and are expl ained below. Even in situations in which you do not wish to add or delete nodes from lists dynamically, as in the case of multivariate polynomials, head nodes p rove useful in determining when the nodes of a particular structure may be retur ned to the storage pool. For example, let t and u be program variables represent ing the two polynomials (3x4 +5x3 +7x)y3 and (3x4 +5x3 +7x)y6 + (6x)y of Figure 4.38. If Polynomial () is to erase a polynomial, then the invocation delete t sho
uld not return the nodes corresponding to the coefficient 3x4 +5x3 +7x, since th is sublist is also part of u. Thus, whenever lists are being shared by other lists, we need a mechanism to hel p determine whether or not the list nodes may be physically returned to the avai lable-space list. This mechanism is generally provided through the use of a refe rence count maintained in the head node of each list. Since the data field of th e head nodes is free, the reference count is maintained in this field. (Alternat ively, a third variant may be introduced, with tag having three possible values: 0, 1 and 2) This reference count of a list is the number of pointers (either pr ogram variables or pointers from other lists) to that list. If the lists (1) to (4) of Figure 4.37 are accessible via the program variables x, y, z and w, then the reference counts for the lists are
Figure 4.37: Structure with head nodes for lists (1) to (4) ref (x) = 1 accessible only via x ref (y) = 3 pointed to by y and two pointers from z ref (z) = 1 accessible only via z ref (w) = 2 accessible via w and one pointer from itself Now a call to t.GenList () (list erase) should result only in a decrementing by 1 of the reference counter of t.first. Only if the reference count becomes zero are the nodes of t to be physically returned to the available-space list. The sa me is to be done with the sublists of t.
Figure 4.38: Assume that class GenList is unchanged and class GenListNode is defined as class GenListNode { friend class GenList ; private: GenListNode *link : int tag ; // 0 for data, 1 for dlink, 2 for ref union { char data GenListNode *dlink ; int ref ; }; }; A recursive algorithm to erase a list is given in Program 4.39. this proceeds by examining the top-level nodes of a list whose reference count has become zero. Any such sublists encountered are erased, and fanally, the top-level nodes are l inked into the available-space list. A call to y.GenList () now has only the effect of decreasing the reference count of y to 2. Such a call followed by a call to z GenList () results in
the reference count of z becomes zero the next node is processed and y.firstref reduces to 1 y.firstref becomes zero and the five nodes of list A (a,(b,c)) are return ed to the available-space list the top-level nodes of z are linked into the available-space list. The use of head nodes with reference counts solve the problem of determining whe n nodes are to be physically freed in the case of shared sublists. However, for recursive lists, the reference count never becomes zero. w.Genlist () results in w.firstref becoming one. The reference count does not become zero, even though li st is no longer accessible either through program variables or through other str uctures. The same is true in the case of indirect recursion (Figure 4.39). After calls to r.GenList () and s.GenList (), r.firstref =1 and s.firstref = 2 but the structure consisting of r and s is no longer being used, so it should have been returned to the available- space list. Unfortunately, there is no simple way to supplement the list structure of Figure 4.39 so as to be able to determine when recursive lists may be physically erase d. It is no longer possible to return all free nodes to the available-space list when they become free. When recursive lists are being used, it is possible to r un out of available space, even though not all nodes are in use. // Driver GenList:: GenList () // Each head node has a reference count. We assume first 0. { Delete (first) ; first = 0 ; } // Workhorse void GenList::Delete (GenListNode * x) { xref -- ; // decrement reference count of head node. if (!xref) { GenListNode *y = x ; // y traverses top-level of x. while (ylink) {y = ylink ; if (ytag == 1) Delete (ydlink) ;} ylink = av ; // attach top-level nodes to av list av = x ; } } Program 4.39: Erasing a list recursively
Figure 4.39: Indirect recursion of list A and B pointed to by program variables r and s EXERCISE Express a multivariate polynomial by using the following node structure that can be used to represent any generalized list: Tag = FALSE/TRUE data/dlink link
Why is not possible to use the program of Program 4.36 to copy lists wit h shared sublists? Implement generalized lists with reference counts using templates. Your implementation should support the copy constructor and destructor for this class . Write a nonrecursive version of algorithm GenList::GenList () (Program 4 .39). Write a nonrecursive version of algorithm operator == (Program 4.37). Write a nonrecursive version of algorithm GenList::depth (Program4.38) Write a function that inverts an arbitrary nonrecursive list l with no s hared sublists, and all of its sublists. For example, if l = (a, (b, c)), then i nverse (l) = ((c,b),a). Devise a function that produces the list representation of an arbitrary list, given its linear form as a string of atoms, commas, blanks and parentheses . For example, for the input l = (a, (b, c)), your function should produce the s tructure of Figure 4.40.
Figure 4.40 Structure for Exercise 8 One way to represent generalized lists is through the use of two data me mber nodes and a symbol table that contains all atoms and list names, together w ith pointers to these lists. Let the two fields of each node be named alink and blink. Then blink either points to the next node on the same level, if there is one, or it is 0. The alink points either to a node at a lower level or, in the c ase of an atom or list name, to the appropriate entry in the symbol table. For e xample, the list B (A,(D,E),( ),B) would have the representation given in figure assumes that all pointers are integer addresses.
Figure 4.41: Representation for Exercise 9 (The list name D and E were already in the table at the time the list B was inpu t. A was not in the table and is assumed to be an atom.) The symbol table retains a type bit for each entry. This bit is 1 if the entry i s a list name and 0 for an atom. The NIL atom may be in the table, or alink can be set to 0 to represent the NIL atom. Write an algorithm operator>> to read in a list in parenthesis notation and to set up its linked representation as above, with x set to point to the first node in the list. Note that no head nodes are in use. You may need to use variant records or simulate pointers by integers (or both). The following subalgorithms may be used by operator>>: int get(a) searches the symbol table for the name a. -1 is returned if a is not found in the table; otherwise, the position of a in the table is returne d. put(a, t, p) enters a into the table. p is the position at which a was e
ntered. If a is already in the table, then the type and address data members of the old entry are changed. t = 0 to enter an atom or t < > 0 to enter a list wit h first node t. (Note: This permits definition of lists using indirect recursion .) NextToken gets the next token in the input list. (A token may be a list name, atom, ( , ) or , . A # is returned if there are no more tokens.) new GenListNode gets a node for use. You may assume that the input list is syntactically correct. If a sublist is lab eled, as in the list C(D,E(F,G)), the structure should be set up as in the case C(D, (F,G)), and E should be entered into the symbol table as a list with the ap propriate starting address. [Wilczynski]. Following the conventions of LISP, assume nodes with two d ata members: HEAD and TAIL. If A = ((a(bc))), then HEAD (A) = (a(bc)), TAIL (A) = NIL, (HEAD (A)) = a, and TAIL (HEAD(A)) = ((bc)). CONS (A,B) gets a new node T , stores A in its HEAD, B in its TAIL, and return T. B must always be a list. If L = a and M = (bc), then CONS (L,M) = (abc) and CONS (M,M) = ((bc)bc). Three other useful functions are: ATOM (X) which is true if X is an atom else false, NULL (X) which is true if X is NIL, else false, EQUAL (X,Y) which is true if X a nd Y are the same atoms or equivalent lists, else false. Give a sequence of HEAD and TAIL operations for extracting a from the li sts: ((cat)), ((a)), ((mart)), and (((cb))a). Write recursive functions for the standard LISP functions COPY, REVERSE and APPEND. Implement this LISP subsystem, Store atoms in an array and write functio ns MakeGenList and GenListPrint for input and output of lists. 4.11 VIRTUAL FUNCTIONS AND DYNAMIC BINDING IN C++ We have already seen that public inheritance is used to express the IS-A relatio nship between ADTs. In this section, we will study another consequence of public inheritance called dynamic types. We will see how C++ virtual functions support dynamic types. These concepts are illustrated with an example in which a Rectan gle class is publicly derived from a Polygon class (Program 4.40). We will discu ss the details of this example later. For now, however, the reader should be sat isfied that the Rectangle IS-A Polygon relationship does hold. Dynamic Types and Public Inheritance: An important consequence of public inherit ance is that a pointer to a derived class type is implicitly converted to a poin ter to its base class; and a reference to a derived class type is implicitly con verted to a reference to its base class. For example, a function whose formal pa rameter is a reference to Polygon, type can be assigned the address of a Rectang le object. In the following example, the dynamic type of s become Rectangle sinc e it is assigned the address of a rectangle: Rectangle r; // instance of derived class Polygon *s = &r ; // assign rectangle to polygon This is a logical interpretation of the IS-A relationship since Rectangle IS-A P olygon, it is reasonable to expect it to have all the attributes of Polygon and thus, to use it instead of Polygon. class Polygon {
public: int GetId(); // non-virtual member function virtual Boolean Concave(); // virtual member function virtual int Perimeter() = 0; // pure virtual member function protected: int id; }; class Rectangle : public Polygon // Rectangle publicly inherits from Polygon { public: Boolean concave(); // redefined in Rectangle int Perimeter(); // defined in Rectangle // GetId() and id are inherited from Polygon // They, respectively, become public and protected members of Rectangle private: // additional data members required to specialize Rectangle int x1, y1, h, w; }; //GetId() must never be redefined in a derived class int Polygon::GetId() {return id;} // Default implementation of Concave() in polygon. A polygon is // concave if it is possible to construct a line joining two points in the polyg on // that does not entirely lie within the polygon Boolean Polygon::Concave() {return TRUE;} //Rectangle must define Perimeter() because it is a pure virtual function. int Rectangle::Perimeter() { return 2*(h + w );} // The default implementation of Concave() does not apply to rectangles. // So, it has to be redefined. Boolean Rectangle::Concave() { return FALSE;} Program 4.40: Public inheritance However, this raises the following question: suppose there is an ordinary class member function whose implementation for rectangles differs from its implementat ion for rectangles differ from its implementation for polygons. What would happe n if we were call to this operation on subject s in the above example? The answe r is that the implementation for polygons would be used because s is defined as a Polygon pointer. But, this is clearly incorrect since s actually points to a r ectangle object. This situation is remedied in C++ which provides virtual functi ons. A virtual function is dynamically boundthat is, the operation corresponding to the dynamic type of the object that invoked it, is used. If a virtual functio n is invoked by s in the above example, it would use the rectangle implementatio n because the dynamic type of s is Rectangle. Next, we discuss the three types o f member functions (non-virtual, virtual and pure virtual) and the conditions in which each should be used: Virtual member functions. A member function of a base class is virtual i f its prototype in its class definition is preceded by the keyword virtual. If a virtual function, whose implementation is redefined in a derived class, is invo ked by a base class object, then the implementation corresponding to the current dynamic type of that base class object is used. For example, the code of Progra m 4.41 uses the implementation defined in class Rectangle. If the implementation of a virtual function is not redefined in the derived class, the implementation defined in Polygon is used. virtual functions are therefore used to provide an interface and a default implementation for a function. In Program 4.40, Concave
is declared as a virtual function. The default implementation returns TRUE which covers the case for concave polygons (i.e., it is possible to find two points o n a polygon so that the line joining them does not lie entirely within the polyg on). However, the rectangle is an exception to this it is convexso we reimplement Concave inside class Rectangle, so that it returns FALSE. Rectangle r; . . // assume r is initialized at this point Polygon *s = &r; sConcave () ; // returns FALSE Program 4.14: Virtual member function operation Non-virtual member functions. These are not preceded by the virtual keyw ord in the base class. GetId() is a non-virtual member function in the example. Its purpose is to return id, an identification number associated with each polyg on object. While non-virtual functions can be legally redefined in a derived cla ss, it is considered to be a bad programming practice to do so. To illustrate th is point, let us assume that we have reimplemented function GetId() in Rectangle . Now there is an inconsistency in the actions of the two code fragments of Prog ram 4.42, which one would expect to give the same outcome if Rectangle IS-A Poly gon. In the first fragment, the implementation in the base class is used, while in the second, the implementation in the derived class is used! So, we will use non-virtual functions in base classes only of they will not be r edefined in a derived class. (If a function is to be redefined in a derived clas s, declare it as virtual). Thus, a non-virtual member function is used to provi de both its interface and its implementation to a derived class. Rectangle r; . . // assumes r is initialized at this point Polygon *s = &r; sGetId (); // uses the function implemented in Polygon Rectangle *t = &r; tGetId (); // uses the function implemented in Rectangle Program 4.42: Reimplementing a non-virtual function. Pure virtual functions. A virtual member function is said to be pure if it is not implemented by its class. It is assigned the value 0 to indicate that it is pure. If a base class contains one or more pure virtual functions, it is c alled an abstract class. Two consequences of using a pure virtual function are: You are not allowed to create instances of the abstract base class. For example, the following is illegal: Polygon p; // illegal Note that it is legal to declare pointers to abstract classes: Polygon *q; // legal Pure virtual functions must be redefined in the derived class. As with regular virtual functions, the code fragment of Program 4.43 uses the im plementation of the derived class. When should you use pure virtual function? Consider a scenario where it is obvio
us that a base class must support a particular function. However, the implementa tion of the function requires additional information that only a derived class c an provide. In this case, the function is declared as a pure virtual function. T he responsibility for implementing the function is passed on to the derived clas s. Thus, a pure virtual function provides only its interface to a derived class. Consider the function Perimeter() used to com Rectangle r; . . // assume r is initialized at this point Polygon *s = &r; sPerimeter(); // returns perimeter of r Program 4.43: Pure, virtual member function operation pute the perimeter of a polygon in Program 4.40. Any polygon must have a perimet er, so it is reasonable to declare Perimeter() as a member function of Polygon. However, it is impossible to implement Perimeter() for polygon without more info rmation polygon. Hence, it is declared as a pure virtual function. The implement ation of Perimeter() is left to class that are derived from Polygon such as Rect angle. In the next section, we discuss an application of dynamic types that allows us t o create a heterogeneous list. 4.12 HETROGENEOUS LISTS A heterogeneous list is one that contains nodes of different types. In Section 4 .8 and 4.11, we have studied list structures that contain nodes of two or more d ifferent types. For example, in Section 4.8 on sparse matrices, we used two type s of nodes: head nodes and element nodes (Figure 4.25). However, even though the se nodes are conceptually different, their definitions were coalesced into a sin gle class definition through the use of the union facility (Program 4.30). This is necessary because any pointer in the list structure may point to exactly one type of node. If each of the two-node-types has its own distinct class definitio n, then it is not possible to define a pointer that can point to both node types . This is achieved by merging the to node types into one class definition. A disadvantage of merging node type using union is that each node is allocated s pace for the largest node type. Program 4.44 defines a composite of classes that represent a chain of nodes such that each node contains either a character, an integer, or a floating point number. Let S(x) denote the space occupied by an object of type x. Then, S (CombinedNode) = S(Data) + S(CombinedNode *) = S(int) + max( S(char). S(int). S(float) ) + S( Co mbinedNode *) Assuming that a char requires 1 byte, an int or a pointer requires 2 bytes, and a float requires 4 bytes, we have S (CombinedNode) = 8 bytes. If a CombinedNode object is being used to represent a character, only 5 of its 8 bytes are being u tilized. If the linked list contains an equal number of each type of object (i.e ., one-third of all nodes contain characters struct Data { int id ; // id = 0, 1 or 2 if the node contains a char, an int, or float, respec tively union { int i; char c;
float f; }; }; class CombinedNode // use union to merge different node types into one class definition { friend class List; friend class ListIterator; private: Data data; CombinedNode *link; }; class List { friend class ListIterator; public: // List manipulation operations follow private: CombinedNode *first; }; // class ListIterator is defined as in Section 4.3, except that the return types // of First and Next are Data* Program 4.44: Using union to implement a list of mixed nodes one-third integers and one-third floats), then the unused or excess space occupi ed by the list is approximately 20.83% of the total space. Program 4.45 implements heterogeneous lists by using public inheritance. This is done as follows: An abstract base class Node is defined. This class contains data members common to all node types to be used in the node structure. In our example, ther e is only one such // struct Data is assumed to be defined as in Program 4.44 class Node { friend class List; friend class ListIterator; protected: Node *link; virtual Data GetData() = 0 ; }; template <class Type> class DerivedNode; public Node { friend class list; friend class ListIterator; public: DerivedNode (Type item) : data (item) { link = 0 ;}; private: Type data; Data GetData() ; }; Data DerivedNode <char>::GetData() { Data t; t.id = 0; t.c = data; return t;
} //The implementations of GetData() for DerivedNode<int> and DerivedNode <float> // are similar to that of DerivedNode<char>::GetData() Program 4.45: Using public inheritance to implement a list of mixed nodes (conti nued on net page) field: link of type Node*. (Note that it is not necessary to have an id field to identify the type of data contained in the node. This does not result in furthe r space savings, however, because the C++ compiler implicitly maintains a pointe r in class Node which determines the dynamic type of the object.) A template class DerivedNode <Type> which is publicly derived from Node is defined. This class contains the additional field required to contain an obje ct of type Type that will be stored in the node. Note that DerivedNode<Type> ISA Node justifying our use of public inheritance. We define a single pure virtual function GetData on Node that retrieves and returns the data in the node. Since the type of the data depends on the type of the node, it is not class List { friend class ListIterator; public: // List manipulation operations follow . . private: Node *first; }; class ListIterator { public: ListIterator(const List& l): list(l), current (l.first) { }; Data *First(); // minor change in homogeneous list implementation Data *Next(); // minor change in homogeneous list implementation Boolean NotNull(); // implemented as for homogeneous lists Boolean NextNotNull(); // implemented as for homogeneous lists private: const List& list; Node *current; Data temp; }; Data* ListIterator ::First() { if (list.first) { temp = list.firstGetData (); // use GetData to retrieve element return &temp; } return (); } Program 4.45: Using public inheritance to implement a list of mixed nodes possible to implement this operation in class Node, justifying our decision to m ake it a pure vital function. Class DerivedNode <Type> contains a different impl ementation of GetData for each type that the template Type is expected to be ins tantiated to (in case, char, int and float). In case, function GetData places th e node element in the appropriate field of a Data object.
Classes List and ListIterator are unchanged with the exception of the im plementation of operations First and Next. These now use GetData to access the d ata. Notice that if GetData were to directly return the node element, rather tha n placing the element in a Data object, then it is not possible to define First and Next, because their return types could vary from one invocation to the next. The key point here is that because of dynamic typing through public inheritance, a pointer to Node* (i.e., link) may be used to point to nodes of type DerivedNo de <char>, DerivedNode<int> and DerivedNode<float>. This eliminates the problem that necessitated the artificial union of the different node types. The techniqu e presented here can also be applied to other linked structures, such as trees. EXERCISE Write C++ code to insert an integer at the beginning of the homogeneous list of Program 4.44. Repeat for the heterogeneous list of Program 4.45. Using iterators, write C++ code for printing all elements in the heterog eneous list of Program 4.45. Write an algorithm to obtain the sum of all the floating point numbers i n the heterogeneous list of Program 4.45. Reimplement the algorithm for reading in a matrix and setting up the lin ked representation using the class definition of the previous exercise. Reimpliment the algorithm for reading in a matrix and setting up the lin ked representation using the class definition of the previous exercise. Write an algorithm to compute the memory requirements of the homogeneous list of Program 4.44. Repeat for the heterogeneous list of Program 4.45. use th e sizeof () function to determine the memory requirement of a node on your machi ne. Implement heterogeneous linked stacks and queues so that these data stru ctures can contain objects of type char, int and float. 4.13 REFERENCES AND SELECTED READINGS A more detailed discussion on list iterators may be found in C++ Stratergies and Tactics, Addison-Wesley, by Robert B. Murray. More list copying algorithms may be found in Copying list structures using bounded workspace, by G. Lindstrom, CACM, 17:4, 1974, pp. 198-202; A nonrecursive list moving algorithm, by E. Reingold. CAC M, 16:5, 1973, pp. 305-307; and Bounded workspace garbage collection in an addres s-order preserving list processing environment, by D. Fisher, Information Process ing Lettes, 3:1, 1974, pp. 29-32.