Download 6. Lists

Document related concepts

Array data structure wikipedia , lookup

Lattice model (finance) wikipedia , lookup

Quadtree wikipedia , lookup

Red–black tree wikipedia , lookup

Interval tree wikipedia , lookup

B-tree wikipedia , lookup

Binary search tree wikipedia , lookup

Linked list wikipedia , lookup

Transcript
6. Lists
6.1. The List Data Structure
• List structure is one of the most important advanced data structure.
• A list is a dynamic structure which is defined starting from the notion of vector.
• All the elements of a list are of the same type, thus a list is a homogenous data
structure.
• All the elements of a list are stored in the main memory of the computing system.
• Unlike the static data structure array which presumes a constant predetermined number of
elements, in the case of list data structure this number can be variable, even null.
• Lists are a particularly flexible structure because they can grow and shrink on demand,
and elements can be accessed, inserted, or deleted at any position within a list.
• Lists can also be concatenated together or split into sublists.
• Lists arise routinely in applications such as information retrieval, programming language
translation, and simulation.
6.2 The Abstract Data Type List
• Mathematically, a list is a sequence of zero or more elements of a given type (which we
generally call the type element or fundamental type).
• Formally we often represent such a list by a comma separated sequence of elements [6.2.a]:
------------------------------------------------------------------a1, a2,...,an
[6.2.a]
-------------------------------------------------------------------
• where n≥0, and each ai is of type element.
• The number n of elements is said to be the length of the list.
• Assuming n>1, we say that a1 is the first element and an is the last element.
• If n=0, we have an empty list, one which has no elements.
• An important property of a list is that its elements can be linearly ordered according to their
position on the list.
• We say that the element ai is at position i of the list.
• We say ai precedes ai+1 for i=1,2,...,n-1.
• We also say that ai succeeds (follows) ai-1 for i=2,3,4,...,n.
• It is also convenient to postulate the existence of a position following the last element on a list.
• The function END(L):TypePosition will return the position following position n in
an n-element list L.
• Note that position END(L) has a distance from the beginning of the list that varies as
the list grows or shrinks, while all other positions have a fixed distance from the
beginning of the list.
• To define an abstract data type, in our case ADT List, as we know is necessary:
• (1) To define the associate mathematical model. In our case it was presented above.
• (2) To define a set of operators which operate with objects of type List.
• Unfortunately, as in the case of many other ADT's, no one set of operations is suitable for all
applications.
• Generally, the nature of the operators set depends on the implementation manner and
on the using manner.
• Here, we shall give two representative sets of operations.
6.2.1 ADT List 1. Restraint Set of List Operators
• To define the restraint set of list operators we presume:
• L is a list of objects of type TypeElement.
• x: TypeElement is an object of that type,
• p is a variable of TypePosition [AH85]. .
• Note that "position" is another data type whose implementation will vary for different
list implementations.
• Even though we informally think of positions as integers, in practice, they may have
another representation (index, cursor, pointer, etc.)
• In terms of the formalism used in this course, ADT List – restraint variant appears in [6.2.1.a].
• The implementation support model is considered to be the array structure.
------------------------------------------------------------------ADT List 1
(Restraint operators set)
Mathematical model: a sequence of zero or more elements (nodes),
all belonging to the same TypeElement, named base type.
Notations:
L: TypeList;
p: TypePosition;
x: TypeElement.
[6.2.1.a]
Operators:
1.p= End(L:TypeList); - operator which returns the position
following last position in the list L, that means the next
position after its end. If the list is empty End(L)=0.
2.Insert(L:TypeList,x:TypeElement,p:TypePosition); - inserts
x at position p in list L, moving elements at p and
following positions to the next higher position. That is,
if L is a1,a2,...,an then L becomes a1,a2,...,ap-1,x,ap,...an.
If p is End(L), then L becomes a1,a2,...an,x. If list L has
no position p, (p>End(L)) the result is undefined.
3.p= Locate(x:TypeElement,L:TypeList) - returns the position
of x on list L. If x appears more than once, then the
position of the first occurrence is returned. If x does not
appear at all, then End(L) is returned.
4.x= Retrieve(p:TypePosition,L:TypeList) – operator which
returns the element at position p on list L. The result is
undefined if p = End(L) or if L has no position p. Note
that the elements must be of a type that can be returned by
a function if Retrieve is used. In practice, however, we
can always modify Retrieve to return a pointer to an object
of TypeElement.
5.Delete(p:TypePosition,L:TypeList); - deletes the element at
position p of list L. If L is a1,a2,...,an, then L becomes
a1,a2,...ap-1,ap+1,...,an. The result is undefined if L has no
position p or if p = End(L).
6.p= Next(p:TypePosition,L:TypeList);- operator which returns
the position following position p on list L. If p is the
last position on L, then Next(p,L)= End(L). Next is
undefined if p is End(L). The operator is also undefined if
L has no position p.
7.p= Previous(p:TypePosition,L:TypeList); - operator
which returns the position preceding position p on list L.
If p is the first position on L, then Previous is not
defined. The operator is also undefined if L has no
position p.
8.p= Initialize(L:TypeList);- operator which causes
L to become an empty list and returns position End(L).
9.p= First(L:TypeList); - returns the first position
on list L. If L is empty, the position returned is End(L).
10.ListTraversal(L:TypeList,ProcessElement(...):PROCEDURE); scans the elements of list L in order of occurrence and
applies to each the procedure ProcessElement.
------------------------------------------------------------
• Example 6.2.1. To illustrate the utility of this operators set, we consider a typical example of
application.
• Let us write using these operators, a procedure Purge that takes a list as argument and
eliminates duplicates from the list.
• The elements of the list are of type TypeElement, and the list of such elements has
TypeList.
• We presume there is a function same(x,y), where x and y are of TypeElement, that
returns true if x and y are "the same" and false if not. The notion of sameness is
purposely left vague.
• The procedure Purge is based on a simple algorithm: for each element of the list, the
equivalent following elements are deleted.
• Presuming that TypeElement is a RECORD defined as below, the eliminating
algorithm appears in [6.2.1.b].
------------------------------------------------------------------{Example - eliminating duplicates from a list}
TYPE TypeElement=RECORD
IdNumber: integer;
Name: string[20];
Address: string[40]
END;
[6.2.1.b]
PROCEDURE Purge(VAR L: TypeList);
{eliminates duplicates of the elements in the list}
VAR p,q: TypePosition; {p is the current position}
{q is used in search}
BEGIN
p:= First(L);
WHILE p<>End(L) DO
BEGIN
q:= Next(p,L);
WHILE q<>End(L) DO
IF same(Retrieve(p,L),Retrieve(q,L)) THEN
Delete(q,L)
ELSE
q:= Next(q,L);
p:= Next(p,L)
END;
END; {Purge}
------------------------------------------------------------------/* Example - eliminating duplicates from a list */
typedef struct{
int id_number;
char* name;
char* address;
}type_element;
/*[6.2.1.b]*/
void purge(type_list *L)
/* eliminates duplicates of the elements in the list */
{
type_position p,q; /* p is the current position */
/* q is used in search */
p= first(*L);
while (p!=end(*L)){
q= next(p,*L);
while (q!=end(*L))
if same(retrieve(p,*L),retrieve(q,*l))
delete(q,*l);
else
q= next(q,*l);
p= next(p,*l);
}
} /*purge*/
/*---------------------------------------------------------------*/
• In [6.2.1.b] we have to make an important observation about the implementation of the second
WHILE loop:
• If the element in position q of the list is deleted, the successors q+1,q+2, etc, down
with a position in list.
• If q is the last element of the list, then its value becomes End(L).
• Further, if the sentence q:=Next(q,L) is executed, as required by the algorithm’s
logic, q obtains a non-determined value.
• For this reason, the next element is accessed only after a new evaluation of the condition,
respectively, if the IF condition is true, the delete operator is executed, otherwise, the
advance in the list.
-------------------------------------------------------------------
6.2.2 ADT List 2. Extended List Operators Set
• In the same context, a second extended set of list operators is presented in [6.2.2.a].
• The support model of the representation is based on links (pointers) [SH90].
------------------------------------------------------------------ADT List 2
(Extended Operators Set)
[6.2.2.a]
Mathematical model: a finite sequence of nodes (elements). All the
nodes belong to the same type, named fundamental type. Each
node consists in two parts: an information part and a second
part containing the link to the next node. A special variable
indicates the first node of the list.
Notations:
- TypeNode – fundamental type;
- TypeList - type indicator to fundamental type;
- TypeInfo – information part of TypeNode;
- TypeNodeIndicator - type indicator to fundamental type
(identical with TypeList);
- List: TypeList - variable indicating the beginning of the
list;
- current,p,pnode: TypeNode - indicate nodes in the list;
- element: TypeNode;
- info: TypeInfo; - information part of a node;
- b – Boolean value;
- nil – empty indicator.
Operators:
1. MakeListEmpty(List: TypeList); - variable List becomes nil.
2. b:= ListIsEmpty(List:TypeList); - operator which returns
TRUE if List is empty, respectively FALSE otherwise.
3. ToFirst(List:TypeList,current:TypeNodeIndicator); - operator
which makes current to indicate first node of the List.
4. b:= First(List:TypeList,current:TypeNodeIndicator); operator which returns TRUE if current indicates the
first node of the list List.
5. b:= Last(List:TypeList, current: TypeNodeIndicator); operator which returns TRUE if current indicates the last
node of the list List.
6. InsertFirst(List: TypeList, pnode: TypeNodeIndicator); inserts the node indicated by pnode at the beginning of
the list List.
7. InsertAfter(List:TypeList, current,pnode:
TypeNodeIndicator); - inserts the node indicated by pnode
after the node indicated by current. It assumes that
current indicates a node from list List.
8. InsertInFrontOf(List:TypeList, current,pnode:
TypeNodeIndicator); - insertion in front of the current
node.
9. DeleteFirst(List: TypeList); - delete the first node of
list List.
10. DeleteNext(List: TypeList, current: TypeNodeIndicator); delete the node succeeding the node indicated by current
in list List.
11. DeleteCurrent(List: TypeList, current: TypeNodeIndicator);
delete the node indicated by current in list List.
12. Next(List: TypeList, current: TypeNodeIndicator); current indicates the next nod of list List. If current
indicates the last node, it becomes nil.
13. Predecessor(List: TypeList, current: TypeNodeIndicator); current indicates the preceding node in list List.
14. StoreInfo(List:TypeList, current: TypeNodeIndicator, info:
TipInfo); - assigns to node indicated by current the
information info.
15. StoreNext(List: TypeList, current,p: TypeNodeIndicator); assigns to field next of the node indicated by current
the value of p.
16. element:= RetrieveInfo(List:TypeList,current:
TypeNodeIndicator); - returns the value of field info of
the node indicated by current.
17. p:=RetrieveNext(List:TypeList,current: TypeNodeIndicator);
- returns the value of field next of the node indicated
by current.
18. TraverseList(List:TypeList,ProcessingNode(...):PROCEDURE);
- scans the nodes of list List in order and executes for
each the procedure ProcessingNode.
-------------------------------------------------------------------
6.3 Implementation of Lists
• Usually, for the fundamental data structures there are programming language constructions
designated to represent them, which are reflected at the hardware level in specific hardware
implemented characteristics.
• For the advanced data structures, which are characterized by a higher level of abstraction,
this is not true.
• Usually, the advanced data structures are represented using as starting point the fundamental
data structures, which is applicable in the case of the lists, too.
• That is the reason for, in this chapter:
• Will be presented some of the fundamental data structures, which can be used in lists
implementation.
• The procedure and functions implementing the list specific operators will be described
in terms of these structures.
6.3.1 Array Implementation of Lists
• In an array implementation of a list:
• A list is assimilated with an array.
• The nodes of the list are stored in contiguous cells of the array.
• With this representation:
• A list is easily traversed.
• The new elements can be appended readily to the tail of the list.
• Inserting an element into the middle of the list, however, requires shifting all following
elements one place over in the array to make room for the new element.
• Similarly, deleting any element except the last also requires shifting elements to close up
the gap.
• Insertion and deleting requires an execution effort O(n).
• In array implementation, TypeList is defined as a structure (record) with two fields.
• (1) First field is an array named nodes, with elements of TypeNode.
• The length of the array is chosen by the programmer, such to be sufficient to hold
the maximum size of the list in the considered application.
• (2) The second field is an indicator (last) which indicates the position in the array of
the last node of the list.
• The i-th element of the list is in the i-th cell of the array, for 1 ≤i ≤last (fig.6.3.1.a).
• Positions in the list are represented by integers, the i-th position by the integer i.
• The function End(L) returns last+1.
nodes
First node
Second node
last
•
Last node
maxLength
Fig.6.3.1. Array implementation of a list.
• An implementation variant of this model appears in [6.3.1.a].
------------------------------------------------------------------{Array implementation of lists – Pascal variant}
CONST maxLength=...;
TYPE TypeList=RECORD
[6.3.1.a]
nodes: ARRAY[1..maxLength] OF TypeNode;
last: TypePosition
END;
TypePosition=TypePosition;
------------------------------------------------------------------/* Array implementation of lists – C variant */
#define max_length = 100
typedef struct{
type_node nodes[max_length];
type_index last;
} type_list;
/*[6.3.1.a]*/
typedef type_index type_position;
/*---------------------------------------------------------------*/
• In [6.3.1.b] are presented the implementations of the End, Insert, Delete and Locate
operators belonging to the restraint set of list operators (ADT List 1) [&6.2.1].
• Some implementation details:
• Insert(L:TypeList,x:TypeElement,p:TypePosition)
moves
the
elements at locations p,p+1,...,last into locations p+1,p+2,...,last+1 and
then inserts the new element at location p.
• If there is no room in the array for an additional element, the variable error is set to
TRUE.
• Delete(p:TypePosition,L:TypeList) removes the element at position p by
moving the elements at positions p+1,p+2,...,last into positions p,p+1,...,
last-1.
• Locate(x:TypeElement,L:TypeList):TypePosition sequentially scans
the array to look for a given element. If the element is not found, Locate returns
last+1.
• The supplementary Boolean parameter error, can be used be the programmer to make
decisions when the operations are not successfully fulfilled.
------------------------------------------------------------------{Array implementation of the restraint list operators set (ADT List
1): End, Insert, Delete, Locate – Pascal variant}
FUNCTION End(VAR L: TypeList): TypePosition;
BEGIN
End:= L.last+1
{performance O(1)}
END; {End}
PROCEDURE Insert(VAR L: TypeList; x: TypeNode; p: TypePosition;
VAR error: boolean);
{places x in position p of list L; performance O(n)}
VAR q: TypePosition;
BEGIN
[6.3.1.b]
error:= false;
IF L.last>=maxLength THEN
BEGIN
error:= true; Write('list is full')
END
ELSE
IF (p>L.last+1)OR(p<1) THEN
BEGIN
error:= true; Write('position doesn’t exist')
END
ELSE
BEGIN
FOR q:= L.last DOWNTO p DO
L.nodes[q+1]:= L.nodes[q];
L.last:= L.last+1;
L.nodes[p]:= x
END
END; {Insert}
PROCEDURE Delete(p: TypePosition; VAR L: TypeList,
error: boolean);
{extracts x from position p of list L; performance O(n)}
VAR q: TypePosition;
BEGIN
error:= false;
IF (p>L.last)OR(p<1) THEN
BEGIN
error:= true; Write('position doesn’t exists')
END
ELSE
BEGIN
L.last:= L.last-1;
FOR q:=p TO L.last DO
L.nodes[q]:= L.nodes[q+1]
END
END; {Delete}
FUNCTION Locate(x: TypeNode; L: TypeList): TypePosition;
{returns position of x in list L}
VAR q: TypePosition; found: boolean;
BEGIN
q:= 1; found:= false;
{performance O(n)}
REPEAT
IF L.nodes[q]=x THEN
BEGIN
Locate:= q; found:= true
END;
q:= q+1
UNTIL found OR (q=L.last+1);
IF NOT found THEN Locate:= L.last+1
END; {Locate}
------------------------------------------------------------------/* Array implementation of the restraint list operators set (ADT
List 1): End, Insert, Delete, Locate – C variant */
#include <stdlib.h>
#define max_length 100
#define n 30
typedef int type_index;
typedef struct{
int current_number;
char* name;
char* address;
} type_node;
typedef struct{
type_node nodes[max_length];
type_index ultim;
} TypeList;
typedef type_index type_position;
typedef unsigned boolean;
#define true (1)
#define false (0)
boolean reccmp(type_node, type_node);
end(TypeList* list){
type_position end_result;
end_result= l->last+1;
/*performance O(1)*/
return fin_result;
}
/*end*/
void insert(TypeList* list, type_node x,
type_position p, boolean* error)
/* places x in position p of list; performance O(n)*/
{
type_position q;
/*[6.3.1.b]*/
*error= false;
if (list->last>=max_length){
*er= true;
printf("list is full");
}
else
if ((p>list->last+1)||(p<1)){
*error= true;
printf("position doesn’t exist");
}
else{
for (q=list->last; q>=p; q--)
list->nodes[q]= list->nodes[q-1];
list->last= list->last+1;
list->nodes[p-1]= x;
}
}
/*insert*/
void delete(type_position p, TypeList* list, boolean error)
/* extracts x from position p of list; performance O(n)*/
{
type_position q;
er= false;
if ((p>list->last)||(p<1)){
error= true;
printf("position doesn’t exist");
}
else{
list->last= list->last-1;
for(q=p; q<=list->last; q++)
list->nodes[q-1]= list->nodes[q];
}
}
/*delete*/
type_position locate(type_node x, type_list list)
/*returns position of x in list*/
type_position q;
boolean found;
type_position locate_result;
q= 1; found= false;
/*performance O(n)*/
do {
if (reccmp(list.nodes[q-1], x)==0){
locate_result= q;
found= true;
}
q= q+1;
} while (!(found||(q==list.last+1)));
if (!found) locate_result= list.last+1;
return locate_result;
}
/*locate*/
boolean reccmp(type_node x, type_node y)
{
if ((x.current_number==y.current_number) &&
!(strncmp(x.name,y.name,n)) &&
!(strncmp(x.address,y.address,n)))
return true;
else
return false;
}
/*---------------------------------------------------------------*/
• In this context, the implementation of the other operators doesn’t rise problems:
• Operator First returns always value 1.
• Operator Next returns the argument value incremented by 1.
• Operator Previous returns the argument value decremented by 1, after the limits
verifications has been made.
• Operator Initialize makes L.last equal to 0.
• At first sight, it may seem tedious writing procedures to govern all accesses to the underlying data
structures.
• However, this has a remarkable importance, being connected with the concept of “object”.
• If we discipline ourselves to writing programs in terms of the operations for manipulating
abstract data types rather than making use of particular implementation details:
• (1) We increase elegance, visibility and the reliability of the program.
• (2) We can modify programs more readily by re-implementing the operations rather than
searching all programs for places where we have made accesses to the underlying data
structures.
• (3) This flexibility can be particularly important in large software efforts, and the reader
should not judge the concept by the necessarily tiny examples found in this course.
6.3.2 Pointer Implementation of Lists
• Linear lists can be implemented using pointers.
• Since a linear list is a dynamic structure, it can be defined in recursive terms as in [6.3.2.a]:
------------------------------------------------------------------{Pointer implementation of lists as a recursive data structure –
Pascal variant}
TYPE TypePointerNode = ^TypeNode;
[6.3.2.a]
TypeNode = RECORD
key: integer;
next: TypePointerNode;
info: TypeInfo
END;
TypeList = TypePointerNode;
------------------------------------------------------------------/* Pointer implementation of lists as a recursive data structure –
C variant */
typedef int type_info;
typedef struct type_node* type_pointer_node;
/*[6.3.2.a]*/
typedef struct {
int key;
type_pointer_node next;
type_info info;
} type_node;
typedef type_pointer_node type_list;
/*---------------------------------------------------------------*/
• As it can be observed, a node of a list data structure contains three fields:
• A key for node identification.
• A pointer to the next node.
• A field info containing the utile information.
• In figure 6.3.2.a appears the representation of a linear list with the pointer variable head which
indicates the first node.
• The linear list in figure has the particularity that the key value of each node is identical
with the node position in the list.
• It can be observed in [6.3.2.a] that a linear list can be defined as a recursive structure having a
component whose type is identical with the type of the whole structure.
• The main characteristic of such structure is the presence of a single link.
head
•
1
2
3
•
•
•
n
nil
Fig.6.3.2.a. Example of linear list
• Further, some implementing techniques of the linear lists as recursive data structures will be
presented.
• (1) There is possible that the pointer head which indicates the first node of the list, to indicate a
component of TypeNode named fictive node. This node has no assigned values for fields key and
info, but its next field indicates the real first node of the list (fig.6.3.2.b).
• The usage of this node is known as fictive node technique, and it simplifies in many
situations the linked lists processing.
fictive node
head
•
•
1
2
•
•
Fig.6.3.2.b. List implementation using fictive node technique.
• (2) There is also possible to use a final fictive node as a sentinel whose next field is nil or
points to the node itself [Se88].
• This implementing manner is known as sentinel node technique (fig.6.3.2.c).
sentinel node
head
•
•
•
•
•
Fig.6.3.2.c. List implementation using sentinel node technique.
• (3) Another implementation possibility is to use two fictive nodes, one at beginning and the other at
the end of the list. This is two fictive nodes technique (fig.6.3.2.d).
fictive node 2
fictive node 1
head
•
•
•
•
•
Fig.6.3.2.d. List implementing using two fictive nodes technique.
• Each of the presented techniques has specific advantages which will be revealed in this chapter.
6.3.2.1 Nodes Insertion and Linked Lists Construction
• Presuming a list data structure, we have different possibilities to insert a new node in this list.
• Initially, the insertion is executed at the beginning of the list.
• We consider that head is a pointer which indicates the first node of the list.
• Variable auxiliary is also a pointer to a node of the list [6.3.2.1.a].
------------------------------------------------------------------{Node insertion at the beginning of the list – Pascal variant}
[6.3.2.1.a]
[1] new(auxiliary);
[2] auxiliary^.next:= head;
{performance O(1)}
[3] head:= auxiliary;
[4] head^.info:= ...;
------------------------------------------------------------------/* Node insertion at the beginning of the list – C variant */
/*[6.3.2.1.a]*/
[1] auxiliary= (type_node*)malloc(sizeof(type_node));
[2] auxiliary->next= head;
/*performance O(1)*/
[3] head= auxiliary;
[4] head->info= ...;
/*--------------------------------------------------------*/
• In figure 6.3.2.1.a is presented the graphic representation of an insertion at the beginning of a list.
head
•
•
•
head
•
•
[2]
[1]
•
auxiliary
[3]
•
[4]
Fig.6.3.2.1.a. Node insertion at the beginning of a linked list.
• The operation of inserting an element at the head of a list immediately suggests how such a list
can be generated:
• We start with the empty list, and a heading element is added repeatedly.
• The process of list generation is expressed in [6.3.2.1.b] were the number of elements to
be linked is n.
------------------------------------------------------------------{Creating a liked list}
[6.3.2.1.b]
head:= nil; {empty list}
WHILE n>0 DO
BEGIN
{performance O(n)}
new(auxiliary);
auxiliary^.next:= head;
head:= auxiliary;
auxiliary^.key:= n;
n:= n-1
END;
------------------------------------------------------------------/* creating a liked list */
/*[6.3.2.1.b]*/
head= NULL; /*empty list*/
while (n>0){
auxiliary = (type_node*)malloc(sizeof(type_node));
auxiliary->next= head;
head= auxiliary;
/*performance O(n)*/
auxiliary->key= n;
n=n-1;
}
/*---------------------------------------------------------------*/
• This is the simplest way of forming a list.
• However, the resulting order of elements is the inverse of the order of their insertion.
• In some applications this is undesirable, and consequently, new elements must be appended at the
end instead of the head of the list.
• Although the end can easily be determined by a scan of the list, this naive approach involves an
effort that may as well be saved by using a second pointer, say last, always designating the last
element.
• Using the pointer last, the insertion of a node at the end of the list pointed by head is
presented in [6.3.2.1.c]:
------------------------------------------------------------------{Node insertion at the end of a linked list}
[6.3.2.1.c]
[1] new(auxiliary);
[2] auxiliary^.next:= nil;
{performance O(1)}
[3] last^.next:= auxiliary;
[4] last:= auxiliary;
[5] last^.info:= ...;
------------------------------------------------------------------/*Node insertion at the end of a linked list */
/*[6.3.2.1.c]*/
[1] auxiliary= (type_node*)malloc(sizeof(type_node));
[2] auxiliary->next= NULL;
/*performance O(1)*/
[3] last->next= auxiliary;
[4] last= auxiliary;
[5] last->info= ...;
/*---------------------------------------------------------------*/
• Graphic representation of this insertion appears in figure 6.3.2.1.b.
•
last
[4]
•
[3]
•
auxiliary
[1]
Nil
[2]
[5]
Fig.6.3.2.1.b. Node insertion at the end of a linked list.
• It is simple to observe that the above sequence can’t insert a node in an empty list.
• The reason is that in this case last^.next doesn’t exist.
• There are some possibilities to solve this problem:
• (1) First node must be inserted using another technique, for example at the beginning of
the list.
• The rest of the nodes can be added to the end of the list in conformity with the
presented manner [6.3.2.1.c].
• (2) A second possibility is to use list which is implemented using the fictive node
technique.
• In this case, there is always a node in the list and as result last.next exists
even for an empty list.
• (3) A third possibility is to use list which is implemented using the sentinel node
technique.
• In this case, the node to be inserted is introduced in the sentinel node, and a new
sentinel is created.
• Further we will discuss the insertion of a node somewhere in a list.
• Let be current a pointer indicating a node of the list.
• Let be auxiliary a pointer variable of type TypePointerNode.
• In these conditions the insertion of a new node after the node indicated by current is
represented in figure 6.3.2.1.c in which the node to be inserted has the key 25.
10
20
30
•
•
•
•
current
•
[1]
25
•
auxiliary
[4]
[2]
[3]
10
20
30
•
•
•
•
current
Fig.6.3.2.1.c. Insertion of a new node after the node indicated by current
• The code for this insertion is presented in [6.3.2.1.d].
-----------------------------------------------------------------{Insertion of a new node after the current node}
[6.3.2.1.d]
[1] new(auxiliary);
[2] auxiliary^.next:= current^.next;
{performance O(1)}
[3] current^.next:= auxiliary;
[4] auxiliary^.info:= ...;
------------------------------------------------------------------/* insertion of a new node after the current node */
/*[6.3.2.1.d]*/
[1] auxiliary= (type_node*)malloc(sizeof(type_node));
[2] auxiliary->next= current->next;
/*performance O(1)*/
[3] current->next= auxiliary;
[4] auxiliary->info= ...;
/*---------------------------------------------------------------*/
• If insertion before the designated element current^ is desired, the unidirectional link chain seems
to cause a problem, because it does not provide any kind of path to an element's predecessors.
• As we have already mentioned, the rescanning of the list until the predecessor of current
node is reached, is not acceptable.
• However, this problem can be easily solved using a simple trick:
• (1) A new node is created.
• (2) The new node is assigned in its integrality with the content of the node indicated by
current.
• (3) The new created node is inserted after the node indicated by current. At this moment
there are two instances of the same node in the list.
• (4) The values of key and info fields for the new node are introduced in the
corresponding fields of the old node indicated by current.
• The code for this insertion appears in [6.3.2.1.e] and its graphic representation in figure 6.3.2.1.d.
The new inserted node has the key 15.
------------------------------------------------------------------{Insertion of a new node in front of the current node}
[6.3.2.1.e]
[1] new(auxiliary);
[2] auxiliary^:= current^;
{performance O(1)}
[3] current^.next:= auxiliary;
[4] current^.info:= ...;
------------------------------------------------------------------/*---------------------------------------------------------------*/
/* insertion of a new node in front of the current node */
/*[6.3.2.1.e]*/
[1] auxiliary = (type_node*)malloc(sizeof(type_node));
[2] *auxiliary= *current;
/*performance O(1)*/
[3] current->next= auxiliary;
[4] current->info= ...;
/*--------------------------------------------------------------*/
[1]
•
auxiliary
20
•
[2]
10
•
[3]
20
30
•
•
•
•
•
[4]
•
current
30
15
10
(a)
•
(b)
current
Fig.6.3.2.1.d. Insertion of a node in front of the current node
6.3.2.2 Nodes Deletion in Linked Lists
• We consider the following situation:
• Having a pointer current which indicates a node of a linear linked list, the delete of its
successor is required.
• The deletion is presented in [6.3.2.2.a] where auxiliary is a variable pointer to a node
of the list.
------------------------------------------------------------------{Deleting the successor of the node indicated by current (variant
1)}
[1] auxiliary:= current^.next;
{performance O(1)}
[2] current^.next:= auxiliary^.next;
[6.3.2.2.a]
[3] Dispose(auxiliary);
------------------------------------------------------------------/* deleting the successor of the node indicated by current (variant
1)*/
[1] auxiliary= current->next;
/*performance O(1)*/
[2] current->next= auxiliary->next;
/*[6.3.2.2.a]*/
[3] free(auxiliary);
/*---------------------------------------------------------------*/
• The effect of this code execution can be seen in figure 6.3.2.2.a.
[2]
10
•
20
•
•
current
•
•
•
•
20
10
30
current
[1]
30
•
•
(auxiliary) [3]
Fig.6.3.2.2.a. The deletion of the successor of the node indicated by current
• The above code can be replaced by [6.3.2.2.b] where the auxiliary pointer is no more
necessary.
------------------------------------------------------------------{The deletion of the successor of the node indicated by current
(variant 2)}
{performance O(1)}
current^.next:= current^.next^.next;
[6.3.2.2.b]
------------------------------------------------------------------/* deleting the successor of the node indicated by current (variant
2)*/
/*performance O(1)*/
current->next= current->next->next;
/*[6.3.2.2.b]*/
/*---------------------------------------------------------------*/
• The usage of the auxiliary pointer has the advantage that the programmer has
access to the deleted node for freeing the node allocated memory area. In [6.3.2.2.b]
this memory area is lost.
• Now returning to the deletion problem, we intend to delete from the list of the element
designated by current itself (instead of its successor).
• This is more difficult, because we encounter the same problem as with insertion: tracing
backward to the denoted element's predecessor is impossible.
• The solution is based on the same idea:
• (1) To move the successor’s content in the node indicated by current.
• (2) To delete the successor of the node indicated by current.
• This can be achieved using only one instruction [6.3.2.2.c]:
------------------------------------------------------------------{ Deletion of the node indicated by current (variant 1)}
{performance O(1)}
current^:= current^.next^;
[6.3.2.2.c]
------------------------------------------------------------------/* Deletion of the node indicated by current (variant 1)*/
/*performance O(1)*/
*current= *current->next;
/*[6.3.2.2.c]*/
/*---------------------------------------------------------------*/
• As in preceding situation, this solution has the disadvantage of loosing the memory area
allocated to successor.
• In [6.3.2.2.d] is presented a solution which avoid this loose.
------------------------------------------------------------------{ Deletion of the node indicated by current (variant 2)}
auxiliary:= current^.next;
{performance O(1)}
current^:= auxiliary^;
Dispose(auxiliary);
[6.3.2.2.d]
------------------------------------------------------------------/* Deletion of the node indicated by current (variant 2)*/
auxiliary= current->next;
/*performance O(1)*/
*current= *auxiliary;
free(auxiliary);
/*[6.3.2.2.d]*/
/*---------------------------------------------------------------*/
• We have to remark that the both deleting techniques can be applied whenever the node indicated
by current has a successor, i.e., is not the last element on the list (current^.urm ≠
nil).
• However, it must be assured that there exist no other variables pointing to the now
deleted element.
• To avoid these situations, the list can be implemented using other techniques, for example the
sentinel node technique.
6.3.2.3 Linked List Traversal
• List traversal is the fundamental list operation.
• By list traversal we understand the execution of a certain operation, over all the nodes of a list,
starting with the first, in an ordered manner.
• We presume that pointer head indicates the first node of a list, and current is a pointer
in the same list.
• We denote by Process(current)the generic operation that will be executed on the
node pointed by current. We don’t mention the content of this process.
• Under these circumstances, in [6.3.2.3.a] is presented the code for a linked list traversal.
• In [6.3.2.3.b] is presented a linked list traversal in reverse order.
------------------------------------------------------------------{Traversal of linked list}
current:= head;
WHILE current<>nil DO
{performance O(n)}
BEGIN
[6.3.2.3.a]
Process(current^);
current:= current^.next
END;
------------------------------------------------------------------/* traversal of linked list */
current= head;
/*performance O(n)*/
while (current!=NULL)
{
process(*current);
/*[6.3.2.3.a]*/
current= current->next;
}
/*---------------------------------------------------------------*/
{Traversal of linked list in reverse order (recursive variant)}
PROCEDURE InverseTraversal(current: TypeList);
BEGIN
IF current<>nil THEN
[6.3.2.3.b]
BEGIN
InverseTraversal(current^.next);
{performance O(n)}
Process(current^)
END
END;{InverseTraversal}
------------------------------------------------------------------/* Traversal of linked list in reverse order (recursive variant)*/
void inverse_traversal (type_list current)/*performance O(n)*/
{
if (current!=NULL)
/*[6.3.2.3.b]*/
{
inverse_traversal (current->next);
process(*current);
}
}
/*inverse_traversal*/
/*---------------------------------------------------------------*/
• A very frequent operation performed is list searching for an element with a given key x.
• Unlike for arrays, the search must here be purely sequential.
• The search terminates either if an element is found or if the end of the list is reached.
• This is reflected by a logical conjunction consisting of two terms.
• List searching for an element is in fact a specialized list traversal [6.3.2.3.c].
• Again we presume that pointer head indicates the first node of a list, and current is a
pointer in the same list.
------------------------------------------------------------------{List searching for a node with key x (variant 1)}
[6.3.2.3.c]
current:= head;
WHILE (current<>nil) AND (current^.key<>x) DO
current:= current^.next;
IF current<>nil THEN {the node x is pointed by current}
------------------------------------------------------------------/* List searching for a node with key x (variant 1)*/
/*[6.3.2.3.c]*/
current= head;
while ((current!=NULL)&&(current->key!=x))
current= current->next;
if (current!=NULL)
/* the node x is pointed by current */
/*---------------------------------------------------------------*/
• At the end of the above code execution:
• (1) current≠nil implies that current indicates the first node with key x.
• (2) current=nil implies that in the list doesn’t exist a node with key x. current^,
and hence that the expression current^.key is undefined. The order of the two terms
is therefore essential.
• With respect to code [6.3.2.3.c] we have to underline that is possible under certain compilers to
be incorrect.
• If at the end of the code execution we are in situation (2), current^ does not exist, and
hence that the expression current^.key is undefined.
• As result, function of the manner in which the compiler process Boolean expressions, it’s
possible that an error to be raised, although the expression is perfectly determined due to
its first factor.
• The variant [6.3.2.3.d] solves this problem using an auxiliary Boolean variable denoted by
found.
------------------------------------------------------------------{ List searching for a node with key x (variant 2)}
found:= false;
current:= head;
WHILE (current<>nil) AND NOT found DO
IF current^.key=x THEN
found:= true
ELSE
[6.3.2.3.d]
current:= current^.next;
IF found=true THEN {the node x is pointed by current}
------------------------------------------------------------------/* List searching for a node with key x (variant 2)*/
found= false;
current= head;
while ((current!=NULL)&&~found)
if (current->key==x)
found= true;
else
/*[6.3.2.3.d]*/
current= current->next;
if (found==true) ...; /* the node x is pointed by current */
/*---------------------------------------------------------------*/
• If at the end of the code execution found=true then current indicates the wanted node.
Otherwise there is not such a node in the list and current=nil.
• Having this background, it is not difficult to conceive the functions and procedures which
implement the ADT List operators for the restraint as well as for the extended variant.
6.3.3 Cursor–Based Implementation of Lists
• Some languages, such as Fortran and Algol, do not have pointers.
• If we are working with such a language, we can simulate pointers with cursors.
• Cursors are integers that indicate positions in arrays.
• We will discuss the cursor-based implementation of linked lists.
• For all lists of elements whose type is element type, we create one array of records.
• First we define the following data structures [6.3.3.a].
• In this implementation:
• (1) For all lists of elements whose type is TypeNode, we create one array of records
Zone with element of TypeElement.
• (2)
Each record of TypeElement consists of two fields: a field
listNode:TypeNode and a field next:TypeCursor defined as a sub-domain of
integers, which is used as a cursor.
• (3) TypeList is identical with TypeCursor, any list being indicated in fact by a
cursor.
------------------------------------------------------------------{Cursor–based implementation of lists – data structures}
TYPE TypeCursor = 0..MaxLength;
TypeNode=...
TypeElement = RECORD
listNode: TypeNode;
next: TypeCursor
END;
[6.3.3.a]
TypeList = TypeCursor;
VAR Zone: ARRAY[1..MaxLength] OF TypeElement;
L,M,available: TypeList;
------------------------------------------------------------------/* Cursor–based implementation of lists – data structures */
enum { max_length = 10};
typedef unsigned char type_cursor;
typedef unsigned boolean;
#define true (1)
#define false (0)
typedef int type_node;
typedef struct type_element {
type_node list_node;
type_cursor next;
} type_element;
/*[6.3.3.a]*/
typedef type_cursor type_list;
type_element zone[max_length];
type_list L,M,available;
/*--------------------------------------------------------*/
• For example, if L:TypeList is a cursor which indicates the beginning of list L, then:
• Zone[L].nodeList represents the first node of list L.
• Zone[L].next is the index (cursor) which indicates the second node of the list L, and
so one.
• The value zero for a cursor denotes the null link.
• In figure 6.3.3.a are represented two list L=A,B,C and M=D,E which share the array Zone
with length 10.
• Some remarks:
• The location of the array which are not used by the lists, are concatenated in another list
named available.
• The list available is used:
• (1) To supply a new location for an insertion.
• (2) To deposit a location resulted from a deletion, for a future utilization.
Zone
L
M
available
5•
1•
9•
1
2
3
4
5
6
7
8
9
10
D
C
A
E
B
listNode
7
4
0
6
8
0
0
3
10
2
next
Fig.6.3.3.a. An example of cursor-based implementation of linked list
• In fact this is a dynamic management of the memory allocated to lists in array Zone,
accomplished by the programmer.
• Thus, to insert an element x:TypeNode into a list L:
• (1) We take the first cell in the available list.
• (2) This cell is placed in the correct position of list L.
• (3) Element x is placed in the listNode field of this cell.
• To delete an element x from list L:
• (1) We remove the cell containing x from L.
• (2) This cell is inserted to the beginning of the available list.
• These two actions can be viewed as special cases of the next situation:
• Let be two lists pointed by the cursors source and destination.
• Let be y the first node of the list indicated by source.
• The node y is deleted from the list source and inserted in the first position of list
destination.
• This can be accomplished as follows:
• (1) The value of cursor destination is saved in auxiliary location temp.
• (2) The value of cursor destination is assigned with the value of cursor
source. Thus, destination will point y.
• (3) source is assigned with the value of the link of y.
• (4) The link of y is assigned with the old value of destination (temp).
• The graphic representation of these actions appears in figure 6.3.3.b where the links are
presented as continue lines (before) and as dashed lines, after the movement.
(3)
source
•
y
•
•
(2)
(4)
destination
•
•
•
(1)
temp
•
Fig.6.3.3.b. Moving a node from list source at beginning of the list destination
• In [6.3.3.b] appears the implementation of function Move in Pascal respectively C.
------------------------------------------------------------------{Cursor–based implementation of lists – The operator Move for
dynamic management of memory}
FUNCTION Move(VAR source,destination:TypeCursor):boolean;
{Moves a node from source at beginning of the list destination}
{Returns true if the move is successful}
VAR temp: TypeCursor;
BEGIN
IF source=0 THEN
{performance O(1)}
BEGIN
message('the location doesn’t exist');
Move:= false
END
ELSE
[6.3.3.b]
BEGIN
temp:= destination;
destination:= source;
source:= zone[destination].next;
zone[destination].next:= temp;
Move:= true
END
END;{Move}
------------------------------------------------------------------/* Cursor–based implementation of lists – The operator Move for
dynamic management of memory */
boolean move(type_cursor* source,type_cursor* destination)
/*moves a node from source at beginning of the list destination*/
/*returns true if the move is successful*/
{
type_cursor temp;
boolean move_result;
if (*source==0)
/*performance O(1)*/
{
printf("the location doesn’t exist");
move_result= false;
}
else
/*[6.3.3.b]*/
{
temp= *destination;
*destination=*source;
*source= zone[*destination-1].next;
zone[*destination-1].next= temp;
move_result= true;
}
return move_result;
}
/*move*/
/*---------------------------------------------------------------*/
• In [6.3.3.c] is presented an example of how operator Move can be used for cursor–based
implementation of operators Insert and Delete a node in a list. In the same time, is
presented the implementation of the operator Init that links the cells of the array Zone into
available space list.
• The procedures omit checks for errors.
• It supposes the function Move is available.
• The variable head indicates the beginning of the list.
------------------------------------------------------------------{ Cursor–based implementation of lists - Operators Insert, Delete
and Init}
PROCEDURE Insert(x: TypeNode; p: TypeCursor;
VAR head: TypeCursor);
BEGIN
{performance O(1)}
IF p=0 THEN
BEGIN {insertion on the first position}
IF Move(available,head) THEN
zone[head].nodeList:= x
[6.3.3.c]
END
ELSE
{insertion in a position different by first}
IF Move(available,zone[p].next) THEN
{location for x is indicated by zone[p].next}
zone[zone[p].next].nodeList:= x
END; {Insert}
PROCEDURE Delete(p:TypeCursor; VAR head:TypeCursor);
BEGIN
IF p=0 THEN
{performance O(1)}
Move(head,available)
ELSE
Move(zone[p].next,available)
END; {Delete}
PROCEDURE Init;
{initialize zone by linking its cells into available list}
VAR i: TypeCursor;
BEGIN
{performance O(n)}
FOR i:=maxLength-1 DOWNTO 1 DO
zone[i].next:= i+1;
available:= 1; zone[maxLength].next:= 0
END;{Init}
-----------------------------------------------------------/* Cursor–based implementation of lists - Operators Insert, Delete
and Init */
void insert(type_node x, type_cursor p,
type_cursor* head)
{
/*performance O(1)*/
if (p==0){ /* insertion on the first position */
if (move(&available,head))
zone[*head-1].node_list= x; /*[6.3.3.c]*/
}
else
/* insertion in a position different by first */
if (move(&available,&zone[p-1].next))
/*the location for x is indicated by zone[p].next*/
zone[zone[p-1].next-1].node_list= x;
} /*insert*/
void delete(type_cursor p,type_cursor* head)
{
if (p==0)
/*performance O(1)*/
move(head,&available);
else
move(&zone[p-1].next,&available);
} /*delete*/
void init()
/* initialize zone by linking its cells into available list */
{
type_cursor i;
/*performance O(n)*/
for(i=max_length-1; i>=1; i--)
zone[i-1].next= i+1;
available= 1; zone[max_length-1].next= 0;
} /*init*/
/*---------------------------------------------------------------*/
6.3.4 Reference-Based Implementation of Lists
• The object oriented languages in general, doesn’t implement type pointer.
• In this case the implementation of the recursive data structures in general, and of linked lists in
special can be achieved in an elegant manner using references.
• Thus, to implement a linked list in JAVA, we can start with class Node which specifies the
format of the objects associated to the list nodes [6.3.4.a].
------------------------------------------------------------------class Node {
private Object element; //the element stored in the current node
private Node next; //reference to the next node of list
// constructors
[6.3.4.a]
Nod() {
//create a node with a null element and a null reference
this(null,null);
}
public Node(Object e, Node n) {
//create a node with element e and reference n
element = e;
next = n;
}
//update methods
void setElement(Object elemNew) {
element = elemNew;
}
void setNext(Node nextNew) {
next = nextNew
}
//access methods
Object getElement() {
return element;
}
Node getNext() {
return next;
}
}
-------------------------------------------------------------------
• Now, having class Node we can define a class LinkedList which:
• Contains a reference to the beginning of the list.
• Optionally can contain other information concerning the list, for example a reference to
the last node of the list and/or the number of nodes.
• In [6.3.4.b] appears a fragment from such a class which defines the data structure List and
some methods implementing specific list operators.
------------------------------------------------------------------public class LinkedList implements List {
private Node head;
//reference to the beginning of the list
private int dimension; //number of nodes
public LinkedList () { //list initialization
head = null;
dimension = 0;
[6.3.4.b]
}
public int dimension(){ //returns current dimension
return dimension;
}
public boolean emptyList() { //return true if list is empty
if (head == null)
return true;
return false;
}
public void InserHead(Object elem){ //insertion at beginning of
the list
Node n = new Node();
n.setElement(elem);
n.setNext(head);
head = n;
dimension++;
}
public Object DeleteFirst() { //Delete the first node
Object obj;
if (head.emptyList())
//the exception is processed in a specific manner
obj = head.getElement();
head = head.getNext();
dimension--;
return obj;
}
//other methods ...
}
-------------------------------------------------------------------
6.3.5 Comparing List Implementation Methods
• It’s difficult to establish which of the presented list implementations methods is better because
the answer depends on:
• (1) The programming language.
• (2) The set of list operators.
• (3) The frequency of operators’ invocation.
• (4) Memory, access time, performance or other constraints.
• In any case we can make the next observations:
• (1) Array and cursor based implementation of linked lists requires to specify the
maximum dimension of the list at the compilation moment.
• If the programmer can’t appreciate the upper limit of list dimension, other
implementation methods are recommended for example, pointers or references.
• (2) The same list operators can have different performances in different
implementations. For example:
• Insertion and deletion are constant (O(1)) as performance in linked
implementations, but are (O(n)) in array implementation.
• The operator Predecessor is (O(1)) in array implementation and (O(n)) in
linked implementations based on pointers, references, or cursors.
• (3) The array and cursor based implementations can be inefficient from the memory
utilization point of view, because the memory space is statically allocated for the maximum
dimension, and usually partially used for the current dimension of the list.
• (4) The linked implementation uses in each moment exactly the necessary memory
space, but requires a pointer field in each node.
• In dependence of the circumstances, the developer can chose the most suitable implementation
manner.
6.4 Linked List Applications
6.4.1 Concordance Problem
• Problem specification:
• It is given a text consisting in a succession of words.
• The text is scanned and the words are identified and collected.
• For each word the algorithm verifies if is at its first appearance:
• If yes, the word is recorded and its associated counter is set to 1.
• If the word is not at its first appearance, the associated counter is incremented.
• Finally we can list all the words and the frequency of their occurrences.
• This problem is important because it reflects in a simplified manner, one of the compiler
activities, namely construction and handling of the identifiers list.
• The program Concordance solves this problem as follows [6.4.1.a]:
• Builds a linked list containing the distinct words of a source text.
• Initially the list is empty.
• The search in the list process, combined with insertion respectively the incrementing of
the associated counter is achieved by procedure Search.
• In order to be able to concentrate our attention on the essential part of list handling, we
presume that the text is in fact a sequence of positive integers which represent the words.
• The words (integers) are read from the input device. The end of the text is signalized by a
number with value zero.
• The list search is based on sequence [6.3.2.3.d] with the difference that the Boolean
variable found is replaced with his negate.
• The pointer variable head indicates the beginning of the list.
• We have to mention that insertions are “at beginning” of the list, and procedure Print is
an example of a list traversal process.
------------------------------------------------------------------{Concordance problem – Pascal variant }
PROGRAM Concordance;
TYPE TypeNodeReference = ^TypeNode;
TypeNode = RECORD
key: integer;
counter: integer;
next: TypeNodeReference
END;
[6.4.1.a]
VAR word: integer; head: TypeNodeReference; {*}
PROCEDURE Search(x:integer; VAR head: TypeNodeReference);
VAR q: TypeNodeReference; notfound: boolean;
BEGIN
q:= head;
notfound:= true;
WHILE (q<>NIL) AND notfound DO
IF q^.key=x THEN notfound:= false
ELSE q:= q^.next;
IF notfound THEN {not found, then insertion in front}
BEGIN
q:= head;
new(head);
WITH head^ DO
BEGIN
key:= x;
counter:= 1;
next:= q
END
END
ELSE {found, then increment the counter}
q^.counter:= q^.counter+1
END;{Search}
PROCEDURE Print(q: TypeNodeReference);
VAR r: TypeNodeReference;
BEGIN
r:= q;
WHILE r<>NIL DO
BEGIN
Writeln(r^.key,r^.counter);
r:= r^.next
END
END;{Print}
BEGIN
head:= NIL; {**}
Read(word);
WHILE word<>0 DO
BEGIN
Search(word,head);
Read(word)
END;
Print(head)
END.
-------------------------------------------------------------------/*Concordance problem – C variant*/
#include <stdio.h>
#include <stdlib.h>
typedef unsigned boolean;
#define true (1)
#define false (0)
typedef struct type_node* type_node_reference;
typedef struct {
int key;
int counter;
type_node_reference next;
}type_node;
int word;
type_node_reference head;
/**/
void search(int x, type_node_reference* head)
{
type_node_reference q;
boolean notfound;
q= *head;
notfound= true;
while ((q!=NULL) && notfound)
if (((type_node*)q)->key==x)
/*[6.4.1.a]*/
notfound= false;
else
q= ((type_node*)q)->next;
if (notfound){
/* not found, then insertion in front */
q= *head;
*head= (type_node*)malloc(sizeof(type_node));
((type_node*)(*head))->key= x;
((type_node*)(*head))->counter= 1;
((type_node*)(*head))->next= q;
}
else /* found, then increment the counter */
((type_node*)q)->counter= ((type_node*)q)->counter+1;
} /*search*/
void print(type_node_reference q)
{
type_node_reference r;
r= q;
while (r!=NULL)
{
printf("%i%i\n",((type_node*)r)->key,
((type_node*)r)->counter);
r= ((type_node*)r)->next;
}
} /*print*/
int main(int argc, const char* argv[])
{
head= NULL;
/**/
scanf("%i", &word);
while (word!=0)
{
search(word,&head);
scanf("%i", &word);
}
print(head);
return 0;
}
/*---------------------------------------------------------------*/
• Further, we will introduce an optimization of the procedure Search by using the sentinel
method.
• For this purpose, the list of words is extended with a supplementary node called sentinel (fig
6.4.1.a).
•
sentinel
Initial List
fictive node
head
sentinel
•
•
head
•
•
•
•
Fig.6.4.1.a. List implementation using the sentinel method.
• The search technique is similar with that used for linear array search (see &1.4.2.1).
• In order to use the optimized procedure Search1, in program Concordance [6.4.1.a] we have
to operate two changes:
• In declaration area indicated by [*], is added the variable sentinel:
TypeNodeReference;
• The word list initialization, indicated in the program [**], the instruction head:= nil
is replaced by the sequence new(head), sentinel:= head. Thus, the empty list
contains a node, the fictive one.
• The procedure Search1 is presented in [6.4.1.b].
• Comparing with the variant [6.4.1.a] the WHILE condition is simpler, and the search
performance, better.
• The test condition in procedure Print, must be also adapted to the new situation.
------------------------------------------------------------------{Search in words list using the sentinel method}
PROCEDURE Search1(x:integer, VAR head:TypeNodeReference);
VAR q: TypeNodeReference;
BEGIN
q:= head;
sentinel^.key:= x;
WHILE q^.key<>x DO q:= q^.next;
IF q=sentinel THEN {word not found}
BEGIN
q:= head;
new(head);
[6.4.1.b]
WITH head^ DO
BEGIN
key:= x;
counter:= 1;
next:= q
END
END
ELSE {word found}
q^.counter:= q^.counter+1
END; {Search}
------------------------------------------------------------------/* Search in words list using the sentinel method "*/
void search1(int x, type_node_reference* head)
{
type_node_reference q;
q= *head;
sentinel->key= x;
while (((type_node*)q)->key!=x)
/*[6.4.1.b]*/
q= ((type_node*)q)->next;
if (q==sentinel){
/*word not found*/
q= *head;
*head= (type_node*)malloc(sizeof(type_node))
((type_node*)(*head))->key= x;
((type_node*)(*head))->counter= 1;
((type_node*)(*head))->next= q;
}
else /*word found*/
((type_node*)q)->counter= ((type_node*)q)->counter+1;
} /*search1*/
/*---------------------------------------------------------------*/
6.4.2 Ordered Lists
• This paragraph will discuss about how to create a list, in which the keys of the nods are ordered
in increasing manner.
• That means, when the list is created it is already sorted.
• In the context of concordance problem, this is not a complication, because before to be inserted,
a node is searched in the list.
• (1) If the list is sorted, then the search will be finished on the first key which is grater
than the searched key. Then, the new node is inserted in front of this node.
• (2) If the list is not sorted, the search presumes to traverse the entire list. The new nod is
then inserted at the beginning of the list.
• As we can remark, situation (1) not only allow obtaining an ordered list, but the searching
process becomes more efficient.
• The insertion of a node in an ordered list presumes insertion of the new node in front of the
node indicated by the current pointer.
• A modality to solve this problem was presented in [6.3.2.1.e].
• Next, we will discuss another technique based on using two pointers q1 and q2, which indicate
all the time two consecutive nodes of the list as in figure 6.4.2.a.
head
•
•
•
•
•
q2
•
q1
•
•
Fig.6.4.2.a. List traversal using two pointers
• We presume also that the searching process is based on the sentinel technique. The searched
key is introduced initially in the sentinel.
• The two pointers advance in the same time along the list, until the key of the node indicated by q1
is greater the key to be inserted x.
• This will happen with certitude, the later in the moment when q1 becomes equal with the
sentinel.
• In this moment, if the key of the node indicated by q1 is greater than x, or
q1 = sentinel, then the new nod is inserted between nodes q2 and q1.
• If not, the searched key was found and the associated counter q1^.counter is
incremented.
• In implementation of this process, we have to take care that its correct operation presumes
initially existence in the list of at least two nodes. That means we need at least one more node,
besides sentinel.
• From this reason we will use in the list implementation the two fictive nodes technique
(fig.6.4.2.b).
•
head
•
sentinel
fictive node
•
empty list
sentinel
•
head
•
•
•
•
•
fictive node
Fig.6.4.2.b. List implementation based on two fictive nodes technique
• In order to implement these improvements, the Concordance program ([6.4.1.a]), must be
modified as follows:
• (1) In the variable declaration part (the place indicate by [*], is added the declaration
sentinel: TypeNodeReference;
• (2) The instruction head:=nil (indicated by [**]) is replaced with the sequence
[6.4.2.a] which creates the initially empty list.
------------------------------------------------------------------new(head); new(sentinel);
[6.4.2.a]
head^.next:= sentinel;
-------------------------------------------------------------------------
• (3) The procedure Search is replaced by Search2 [6.4.2.b].
------------------------------------------------------------------{Linked list search using the two pointers technique}
PROCEDURE Search2(x: integer; head: TypeNodeReference);
VAR q1,q2,q3: TypeNodeReference;
BEGIN
q2:= head;
q1:= q2^.next;
sentinel^.key:= x;
WHILE q1^.key<x DO
BEGIN
q2:= q1;
[6.4.2.b]
q1:= q2^.next
END;
IF (q1^.key=x)AND(q1<>sentinel) THEN
q1^.counter:= q1^.counter+1
ELSE
BEGIN {a new node indicated by q3 is created and inserted
between q2 and q1}
new(q3);
WITH q3^ DO
BEGIN
key:= x;
counter:= 1;
next:= q1
END;
q2^.next:= q3
END
END; {Search2}
------------------------------------------------------------------/* Linked list search using the two pointers technique */
void search2(int x, type_node_reference head)
{
type_node_reference q1,q2,q3;
q2= head;
q1= ((type_node*)q2)->next;
sentinel->key= x;
while (q1->key<x)
{
q2= q1;
/*[6.4.2.b]*/
q1= ((type_node*)q2)->next;
}
if ((((type_node*)q1)->key==x)&&(q1!=sentinel))
((type_node*)q1)->counter= ((type_node*)q1)->counter+1;
else
/* a new node indicated by q3 is created and inserted
between q2 and q1* /
q3= (type_node*)malloc(sizeof(type_node));
((type_node*)q3)->key= x;
((type_node*)q3)->counter= 1;
((type_node*)q3)->next= q1;
((type_node*)q2)->next= q3;
}
} /*search2*/
/*---------------------------------------------------------------*/
• The above modifications lead to a sorted list.
• We have to notice that the benefit resulted from the sorting is limited.
• It is evident only in the search of a node which is not in the list.
• This operation requires scanning in average of a half of the ordered list in
comparison with scanning of the entire unsorted list.
• If the searched nod is in the list, the search process requires in average the scanning of a
half of the list, in the both situation (sorted or unsorted list).
• This conclusion is valid if the key values have a normal or Gaussian distribution.
• Hence, ordered list insertion pays off only if a concordance is to be generated with many distinct
words compared to their frequency of occurrence.
6.4.3 List Search with Reordering
• Some applications require the arrangement of nodes in the list.
• The arrangement of data in a linked list is recommended when the number of elements is
relatively small (say less than 100), varies, and, moreover, when no information is given about
their frequencies of access.
• A typical example is the symbol table in compilers of programming languages.
• Each declaration causes the addition of a new symbol, and upon exit from its scope of
validity, it is deleted from the list.
• The use of simple linked lists is appropriate for applications with relatively short programs.
• Even in this case, a considerable improvement in access method can be achieved by a
very simple technique which is mentioned here again primarily because it constitutes a
pretty example for demonstrating the flexibilities of the linked list structure.
• A characteristic property of programs is that occurrences of the same identifier are very often
clustered, that is, one occurrence is often followed by one or more re-occurrences of the same
word.
• This is the well known principle of localization.
• This information is an invitation to re-organize the list after each access by moving the word that
was found to the top of the list, thereby minimizing the length of the search path the next time it
is sought.
• In other words, the list is re-organized after each successful search.
• In [6.4.3.a] appears procedure Search3 which implements this method using the two pointers
technique.
• Pointer q1 indicates the found node and pointer q2 the precedent.
• The list is implemented using the sentinel technique.
• The data structures are those defined in [6.4.1.a].
------------------------------------------------------------------{List search with reordering}
PROCEDURE Search3(x:integer; VAR head:TypeNodeReference);
VAR q1,q2,q3: TypeNodeReference;
BEGIN
q1:= head;
sentinel^.key:= x;
IF q1=sentinel THEN {insertion of the first node}
BEGIN
new(head);
WITH head^ DO
BEGIN
key:= x;
counter:= 1;
next:= sentinel
END
END
ELSE
IF q1^.key=x THEN
q1^.counter:= q1^.counter+1
ELSE
BEGIN {search}
[6.4.3.a]
REPEAT
q2:= q1;
q1:= q2^.next
UNTIL q1^.key=x;
IF q1=sentinel THEN {insertion}
BEGIN
q2:= head;
new(head);
WITH head^ DO
BEGIN
key:= x;
counter:= 1;
next:= q2
END
END
ELSE {found, then reordering}
BEGIN
q1^.counter:= q1^.counter+1;
q2^.next:= q1^.next;
q1^.next:= head;
head:= q1
END
END
END;{Search3}
-----------------------------------------------------------/* List search with reordering */
void search3(int x, type_node_reference* head)
{
type_node_reference q1,q2,q3;
q1= *head;
((type_node*)sentinel)->key= x;
if (q1==sentinel){
/* insertion of the first node */
*head = (type_node*)malloc(sizeof(type_node));
((type_node*)(*head))->key= x;
((type_node*)(*head))->counter= 1;
((type_node*)(*head))->next= sentinel;
}
else
if (((type_node*)q1)->key==x)
((type_node*)q1)->counter= ((type_node*)q1)->counter+1;
else
{
/*search*/
/*[6.4.3.a]*/
do {
q2= q1;
q1= ((type_node*)q2)->next;
} while (!(((type_node*)q1)->key==x));
if (q1==sentinel)
/*insertion*/
{
q2= *head;
*head = (type_node*)malloc(sizeof(type_node));
((type_node*)(*head))->key= x;
((type_node*)(*head))->counter= 1;
((type_node*)(*head))->next= q2;
}
else{ /*found, then reordering*/
((type_node*)q1)->counter=
((type_node*)q1)->counter+1;
((type_node*)q2)->next= ((type_node*)q1)->next;
((type_node*)q1)->next= *head;
*head= q1;
}
}
}/*search3*/
/*---------------------------------------------------------------*/
• We have to notice that the above implementation of list search with re-ordering requires
existence of at least two nodes in the list. These can be a real one and the sentinel.
• As result, in the list implementation was used the sentinel node technique. In these
circumstances:
• (1) The list initialization is achieved replacing the instruction head:=nil;, marked as
[**] in [6.4.1.a], with the sequence new(head); sentinel:= head;.
• (2) The insertion of the first node in the list is treated in an explicit manner in [6.4.3.a].
• The improvement in this search method strongly depends on the degree of clustering in the input
data.
• For a given factor of clustering, the improvement will be more pronounced for large lists.
• To provide an idea of how much gain can be expected, an empirical measurement was
made by applying the above Concordance program to a short and a relatively long
text and then comparing the methods of linear list ordering (Search2) and of list
reorganization (Search3).
• The measured data has shown an improvement factor for list reorganization ranged
between 1.37 and 4.7 [Wi76].
6.4.4 Partial Ordering (Topological Sorting)
• An appropriate example of the use of a flexible, dynamic data structure is the process of
topological sorting [Wi86].
• This is a sorting process of items over which a partial ordering is defined, i.e., where an
ordering is given over some pairs of items but not between all of them.
• The following are examples of partial orderings:
• (1) In a dictionary or glossary, words are defined in terms of other words.
• If a word w is defined in terms of a word v, we denote this by v  w.
• Topological sorting of the words in a dictionary means arranging them in an
order such that there will be no forward references.
• (2) A task (e.g., an engineering project) is broken up into subtasks.
• Completion of certain subtasks must usually precede the execution of other
subtasks.
• If a subtask v must precede a subtask w, we write v  w.
• Topological sorting means their arrangement in an order such that upon initiation
of each subtask all its prerequisite subtasks have been completed.
• (3) In a university curriculum, certain courses must be taken before others since they rely
on the material presented in their prerequisites.
• If a course v is a prerequisite for course w, we write v  w.
• Topological sorting means arranging the courses in such an order that no course
lists a later course as prerequisite.
• (4) In a program, some procedures may contain calls of other procedures.
• If a procedure v is called by a procedure w, we write v  w.
• Topological sorting implies the arrangement of procedure declarations in such a
way that there are no forward references.
• In general, a partial ordering of a set S is a relation between the elements of S.
• This relation is denoted by the symbol
 verbalized by precedes, and satisfies the
following three properties (axioms) for any distinct elements x,y,z of S:
• (1) If x  y and y  z then x  z (transitivity);
• (2) If x  y, then y  x (asymmetry);
• (3) x  x (irreflexivity).
• For evident reasons, we will assume that the sets S to be topologically sorted by an algorithm
are finite.
• Hence, a partial ordering can be illustrated by drawing a diagram or graph in which the vertices
denote the elements of S and the directed edges represent ordering relationships.
• An example is shown in Fig. 6.4.4.a.
1
2
1
6
4
8
3
5
9
7
Fig.6.4.4.a. Partially ordered set
• The problem of topological sorting is to embed the partial order in a linear order.
• Graphically, this implies the arrangement of the vertices of the graph in a row, such that all
arrows point to the right, as shown in fig. 6.4.4.b.
7
9
1
2
4
6
3
5
8
10
Fig.6.4.4.b. Linear order of the partially ordered set
• Properties (1) and (2) of partial orderings ensure that the graph contains no loops.
• This is exactly the necessary and sufficient condition under which such an embedding
in a linear order, respectively the topological sort, is possible.
• Topological sort algorithm is not complicated:
• We start by selecting any item that is not preceded by another item. There must be at
least one; otherwise a loop would exist.
• The selected object is removed from the set S and placed in the resulting sorted list.
• The remaining set is still partially ordered, and so the same algorithm can be applied
again until the set is empty.
• In order to describe this algorithm more rigorously, we must settle on a data structure and
representation of S and its ordering.
• It’s obvious that the choice of this representation is determined by the operations to be
performed, particularly the operation of selecting elements with zero predecessors.
• For this purpose, every item should therefore be represented by three characteristics:
• (1) Its identification key;
• (2) Its set of successors;
• (3) A count of its predecessors.
• Since the number n of elements in S is not given a priori, the set is conveniently organized as a
linked list, named the list of principals.
• Consequently, an additional entry in the description of each item contains the link to the
next item in the list.
• We will assume that the keys are integers, but not necessarily the consecutive integers,
from 1 to n.
• Analogously, the set of each item's successors is conveniently represented as a linked list.
• Each element of the successor list is described by:
• (1) The identification which is a reference in the principals’ list;
• (2) A link to the next item on this list.
• We will call the descriptors of the main list, in which each item of S occurs exactly once,
principals.
• The descriptors of elements on the successor chains will be named successors.
• Under these conditions we can define the following declarations of data types [6.4.4.a]:
------------------------------------------------------------------{Topological sort – data structures}
TYPE TypePointerPrincipals = ^TypeNodePrincipal;
TypePointerSuccessors = ^TypeNodeSuccessor;
TypeKey = integer;
TypeNodePrincipal = RECORD {node of principals list}
key: TypeKey;
count: integer;
next: TypePointerPrincipals;
[6.4.4.a]
succ: TypePointerSuccessors
END;
TypeNodeSuccessor = RECORD
{node of successors list}
id: TypePointerPrincipals;
next: TypePointerSuccessors
END;
------------------------------------------------------------------/* Topological sort – data structures */
#include <stdlib.h>
typedef struct type_node_principal* type_pointer_principals;
typedef struct type_node_successor* type_pointer_sucessors;
typedef int type_key;
typedef struct type_node_principal {
type_key key;
int count;
type_pointer_principals next;
type_pointer_succesors succ;
} type_node_principal;
/*[6.4.4.a]*/
typedef struct type_node_successor {
type_pointer_principals id;
type_pointer_successors next;
} type_node_successor;
/*---------------------------------------------------------------*/
• Assume that the set S and its ordering relations are initially represented as a sequence of pairs of
keys in the input file.
• The input data for the example in fig. 6.4.4.a are shown below, in which the symbols

are added for the sake of clarity, symbolizing partial order.
------------------------------------------------------------------1  2 2  4 4  6 2  10 4  8 6  3 1  3
[6.4.4.b]
3  5 5  8 7  5 7  9 9  4 9  10
-------------------------------------------------------------------
• The first part of the topological sort program must read the input and transform the data into a
list structure.
• This is performed by successively reading a pair of keys x and y (x  y).
• Let us denote the pointers to their representations on the linked list of principals by p and
q.
• These records must be located by a list search and, if not yet present, be inserted at the
end of principals list.
• This task is preformed by a function procedure called Find.
• Subsequently, a new entry is added in the list of successors of x, along with an
identification of y. The count of predecessors of y is incremented by 1.
• This algorithm is called Input Phase and is presented in [6.4.4.c].
• The function Find(w) yields the pointer to the principals list element with key w.
• We presume that the sequence of the pairs of keys supplied as input is closed by a key
with value zero.
------------------------------------------------------------------head,tail,p,q: TypePointerPrincipals;
t: TypePointerSuccessors;
x,y: TypeKey {keys}
z: integer; {counter}
{Toplogical Sort – Input Phase}
Read(x);
new(head); tail:= head; z:= 0;
WHILE x<>0 DO
BEGIN
[6.4.4.c]
Read(y); p:= Find(x); q:= Find(y);
new(t); t^.id:= q; t^.next:= p^.succ;
p^.succ:= t; {insertion in front of the successors list of x}
q^.count:= q^.count+1;
Read(x)
END;
------------------------------------------------------------------/*---------------------------------------------------------------*/
type_pointer_principals head, tail, p, q;
tipe_pointer_successors t;
type_key x,y; /*keys*/
int z; /*counter*/
/* Toplogical Sort – Input Phase */
scanf("%i", &x);
head = (type_node_principal*)malloc(sizeof(type_node_principal));
tail= head; z= 0;
while (x!=0)
{
/*[6.4.4.c]*/
scanf("%i", &y);
p= find(x);
q= find(y);
t = (type_node_successor*)malloc(sizeof(type_node_successor));
t->id= q;
t->next= p->succ;
p->succ= t; /*insertion in front of the successors list of x*/
q->count= q->count+1;
scanf("%i", &x);
}
/*---------------------------------------------------------------*/
•
Figure 6.4.4.c illustrates the data structure generated during processing the given input data.
Fig. 6.4.4.c. Data structure for topological sort
• After the data structure has been constructed in this input phase, the actual process of
topological sorting can be taken up as described above.
• As was mentioned, the topological sort consists of repeatedly selecting an element with
a zero count of predecessors, it seems sensible to first gather all such elements in a
linked list of principals with no predecessors.
• Since we note that the original chain of leaders will afterwards no longer be needed, the
same field called next may be used again to link the zero predecessor leaders.
• This operation of replacing one chain by another chain occurs frequently in list processing.
• It is expressed in detail in [6.4.4.d], where for reasons of convenience it constructs the new list in
reverse order by insertion in front of the list.
------------------------------------------------------------------{Topological Sort - Finding the principals with zero predecessors}
p:= head; head:= nil;
WHILE p<>tail DO
BEGIN
[6.4.4.d]
q:= p; p:= q^.next;
IF q^.count=0 THEN
BEGIN {insert q^ in the front of the new list}
q^.next:= head; head:= q
END
END;
------------------------------------------------------------------/*---------------------------------------------------------------*/
/*Topological Sort- Finding the principals with zero predecessors*/
p= head; head= NULL;
while (p!=tail)
{
/*[6.4.4.d]*/
q= p; p= q->next;
if (q->counter==0)
{
/*insert q^ in the front of the new list*/
q->next= head; head= q;
}
}
/*---------------------------------------------------------------*/
•
Referring to [6.4.4.d], we see that the list of principals is replaced by the list of principals with
zero predecessors, using the same link field next.
•
After all this preparatory establishing of a convenient representation of the partially ordered set
S, we can finally proceed to the actual task of topological sorting, i.e., of generating the output
sequence.
• The two steps refinement of the corresponding sorting algorithm is presented in [6.4.4.e,f] and is
named Output phase.
• The both steps are examples of list traversal.
• In each step, the auxiliary variable p designates the principal element whose count has to
be decremented and tested.
------------------------------------------------------------------{Topological Sort - Output phase}
q:= head;
[6.4.4.e]
WHILE q<>nil DO
BEGIN {print the current element and then delete it}
WriteLn(q^.key); z:= z-1;
t:= q^.succ; q:= q^.next;
*decrement the predecessors’ count of all its successors
from list t; if any count becomes zero, insert this
node in list of principals with zero predecessors;
END;
------------------------------------------------------------------{decrement the predecessors’ count...}
WHILE t<>nil DO
BEGIN
[6.4.4.f]
p:= t^.id; p^.count:= p^.count-1;
IF p^.count=0 THEN
BEGIN {insert p^ at the beginning of the list of principals
with zero predecessors}
p^.next:= q; q:= p
END;
t:= t^.next
END;
------------------------------------------------------------------/*---------------------------------------------------------------*/
/* Topological Sort - Output phase */
q= head;
/*[6.4.4.e]*/
while (q!=NULL)
{
/* print the current element and then delete it */
printf("%i\n", q->key); z= z-1;
t= q->succ; q= q->next;
*decrement the predecessors’ count of all its successors
from list t; if any count becomes zero, insert this
node in list of principals with zero predecessors;
}
/*---------------------------------------------------------------*/
/* decrement the predecessors’ count...*/
while (t!=NULL)
{
/*[6.4.4.f]*/
p= t->id; p->contor= p->contor-1;
if (p->contor==0)
/* insert p^ at the beginning of the list of principals with
zero predecessors */
p->next= q; q= p;
}
t= t->next;
}
/*---------------------------------------------------------------*/
• The counter z was introduced to count the principal nodes generated in the Input phase
([6.4.4.c]).
• This count is decremented each time a principal element is output in the Output
phase.
• It should therefore return to zero at the end of the program.
• Its failure to return to zero is an indication that there are elements left in the structure
when none is without predecessor.
• In this case the set S is evidently not partially ordered.
• The Output phase programmed above is an example of a process that maintains a list that
pulsates, i.e., in which elements are inserted and removed in an unpredictable order.
• It is therefore an example of a process which utilizes the full flexibility offered by the explicitly
linked list.
• The full code of the program TopSort is illustrated in [6.4.4.g].
------------------------------------------------------------------PROGRAM Topsort;
TYPE TypePointerPrincipals = ^TypeNodePrincipal;
TypePointerSuccessors = ^TypeNodeSuccessor;
TypeKey = integer;
TypeNodePrincipal = RECORD
key: TypeKey;
count: integer;
next: TypePointerPrincipals;
succ: TypePointerSuccesssors
END;
[6.4.4.g]
TypeNodeSuccessor = RECORD
id: TypePointerPrincipals;
next: TypePointerSuccessors
END;
VAR head,tail,p,q: TypePointerPrincipals;
t: TypePointerSuccessors;
z: integer; {nodes counter}
x,y: TypeKey; {keys}
FUNCTION Find(w: TypeKey): TypePointerPrincipals;
{supplies the reference of the principal node with the key w; if
such a node doesn’t exist it is inserted at the tail of the
list and counter z is incremented}
VAR h: TypePointerPrincipals;
BEGIN
h:= head; tail^.key:= w; {sentinel}
WHILE h^.key<>w DO h:= h^.next;
IF h=tail THEN
BEGIN {there is no element with key w in the list}
new(tail); z:= z+1;
h^.count:= 0; h^.count:= nil; h^.next:=tail
END;
Find:= h
END;{Find}
BEGIN {initialize list of principals with a dummy node acting as a
sentinel}
new(head); tail:= head; z:= 0;
{Input phase}
Read(x);
WHILE x<>0 DO
BEGIN
Read(y); WriteLn(x,y);
p:= Find(x); q:= Find(y);
new(t); t^.id:= q; t^.next:= p^.succ;
p^.succ:= t; q^.count:= q^.count+1; Read(x)
END;
{Search of principals with zero predecessors (count=0)}
p:= head; head:= nil;
WHILE p<>tail DO
BEGIN
q:= p; p:= p^.next;
IF q^.count=0 THEN
BEGIN
q^.next:= head; head:= q
END
END;
{Output phase}
q:= head;
WHILE q<>nil DO
BEGIN
Write(q^.key); z:= z-1;
t:= q^.succ; q:= q^.next;
WHILE t<>nil DO
BEGIN
p:= t^.id; p^.count:= p^.count-1;
IF p^.count=0 THEN
BEGIN { insert p^ in the list of principals with zero
predecessors }
p^.next:= q; q:= p
END;
t:= t^.next
END
END;
IF z<>0 THEN WriteLn('The set is not partially ordered!');
END.
{TopSort}
------------------------------------------------------------------/*Program TopSort*/
#include <stdio.h>
#include <stdlib.h>
typedef struct type_node_principal* type_pointer_principals;
typedef struct type_node_successor* type_pointer_successors;
typedef int type_key;
typedef struct type_node_principal {
type_key key;
int count;
type_pointer_principals next;
type_pointer_successors succ;
} type_node_principal;
/*[6.4.4.g]*/
typedef struct type_node_successor {
type_pointer_principals id;
type_pointer_successors next;
} type_node_sucessor;
type_pointer_principals head,tail,p,q;
type_pointer_successors t;
int z; /*nodes counter*/
type_key x,y; /*keys*/
type_pointer_principals find(type_key w)
/*supplies the reference of the principal node with the key w;
if such a node doesn’t exist it is inserted at the tail of the
list*/
{
type_pointer_principals h;
type_pointer_principals find_result;
h= head; tail->key= w;
/*sentinel*/
while (h->key!=w) h= h->urm;
if (h==tail)
{
/* there is no element with key w in the list */
tail=(type_node_principal*) malloc (sizeof
(type_node_principal));
z= z+1;
h->count= 0;
h->succ= NULL;
h->next=tail;
}
find_result= h;
return find_result;
}
/*find*/
int main(int argc, const char* argv[])
{ /* initialize list of principals with a dummy node acting as a
sentinel */
head = (type_node_principal*)malloc(sizeof(type_node_principal));
tail= head;
z= 0;
/*input phase*/
scanf("%i", &x);
while (x!=0)
{
scanf("%i", &y);
printf("%i %i\n", x,y);
p= find(x);
q= find(y);
t = (type_node_successor*)malloc(sizeof
(type_node_successor));
t->id= q;
t->next= p->succ;
p->succ= t;
q->count= q->count+1;
scanf("%i", &x);
}
/* search of principals with zero predecessors (count=0)*/
p= head;
head= NULL;
while (p!=tail)
{
q= p;
p= p->next;
if (q->count==0)
{
q->next= head;
head= q;
}
}
/*output phase*/
q= head;
while (q!=NULL){
printf("%i", q->key);
z= z-1;
t= q->succ;
q= q->next;
while (t!=NULL){
p= t->id;
p->count= p->count-1;
if (p->count==0){ /* insert p^ in the list of principals
with zero predecessors */
p->next= q;
q= p;
}
t= t->next;
}
}
if (z!=0)
printf("the set is not partially ordered!\n");
return 0;
}
/*---------------------------------------------------------------*/
6.5 Derived Data Structures from List Structures
• In this section we will present some of the data structures derived from list data structures, which
are considered special lists.
• In this category are included circular lists, double linked lists, stacks and queues.
• We will present also the mapping or memory association data structure as well as some
implementations of this abstract data type.
• In general we will keep the rule that for each described abstract data type some implementation
possibilities will be presented.
6.5.1 Circular Lists
• Circular lists are in fact linked lists whose links are closed.
• In these circumstances the notions of begin and end of the list are lost, the list being referred by
a pointer which moves along the list (fig.6.5.1.a).
• Circular lists raise some problems with insertion of the first node in the list and the deletion
of the last node.
• A simple modality to solve these problems is to use a fictive node in a similar manner with usual
linked lists – the fictive node technique &6.3.2
• This modality is illustrated in figure 6.5.1.b.
•
CircularList
1
2
•
5
•
3
•
4
•
Fig.6.5.1.a. Circular List
•
fictive
fictive
•
•
•
•
•
•
List
(a)
•
1
List
5
•
(b)
Fig.6.5.1.b. Circular list implemented using the fictive node technique.
Empty list (a), normal list (b)
6.5.2. Double Linked Lists
• Some applications require lists traversal in both senses.
• In other words being given a list element we need to find in an efficient manner the
predecessor and the successor of the element.
• The simplest manner to solve this requirement is to store in each list node two references:
upward and backward.
• This approach defines the structure double linked list (fig.6.5.2.a).
• • •
• •
•
•
•
•
•
•
•
•
•
•
Fig.6.5.2.a. Double linked list
• The price we pay for these features:
• (1) The presence of an additional pointer in each cell.
• (2) Somewhat lengthier procedures for some of the basic list operations.
• If we use pointers to implement double-linked list we may declare cells consisting of an element
and two pointers [6.5.2.a].
-------------------------------------------------------------------
{Double-linked lists – data structures}
TYPE TypePointerNode = ^TypeNode;
TypeNode = RECORD
element: TypeElement;
[6.5.2.a]
previous,next: TypePointerNode
END;
TypePosition: TypePointerNode;
TypeDoubleLinkedList: TypePointerNode;
------------------------------------------------------------------/*Double-linked lists*/
typedef struct type_node* type_pointer_node;
typedef struct type_node {
/*[6.5.2.a]*/
type_element element;
type_pointer_node previous,next;
} type_node;
typedef type_pointer_node type_position;
typedef type_pointer_node type_double-linked_list;
/*---------------------------------------------------------------*/
• In [6.5.2.b] is presented a procedure for deleting the element in position p, in a double-linked
list presuming that the deleted element is not the first nor the last in the list.
• (1) We first locate the preceding cell using the previous field. We make the next
field of this cell point to the cell following the one in position p.
• (2) Then we make the previous field of this following cell point to the cell preceding
the one in position p.
• (3) The cell pointed to by p becomes useless and should be reused automatically by the
dynamic memory allocation system functions if needed.
------------------------------------------------------------------{ Double-linked lists – deletion of the node pointed to by p}
PROCEDURE Delete(VAR p: TypePosition);
BEGIN
[6.5.2.b]
IF p^.previous<>NIL THEN {p^ is not the first node}
p^.previous^.next:= p^.next;
IF p^.next<>NIL THEN {p^ is not the last node}
p^.next^.previous:= p^.previous;
END;{Delete}
------------------------------------------------------------------/* Double-linked lists - deletion of the node pointed to by p */
void delete(type_position* p)
{
/*[6.5.2.b]*/
if ((*p)->previous!=NULL) /* p is not the first node*/
(*p)->previous->next= (*p)->next;
if ((*p)->next!=NULL) /*p^ is not the last node*/
(*p)->next->previous= (*p)->previous;
}
/*delete*/
/*---------------------------------------------------------------*/
• In the practice of programming, for double linked lists implementation, different techniques
derived from implementation of linear lists can be used.
• These techniques simplify the implementation of the operators which handle such lists,
especially in limit situations (empty list, or list with one node).
• (1) A first possibility is double-linked list with two fictive nodes (fig.6.5.2.b).
• The two fictive nodes (Fict1 and Fict2) allow that the insertion of the first node in
the list and the deletion of the last, to be achieved in a similar manner with any other
node of the list.
Fict1
•
•
•
Fict2
•
•
•
•
•
•
•
•
•
•
•
•
•
•
Fig.6.5.2.b. Double–linked list. The two fictive nodes variant
• (2) Another implementation variant is based on a data structure containing:
• (1) Two indicators for the extremities of the list.
• (2) A nodes counter which stores the number of nodes in the list, which is very useful
especially in managing the limit situations (when the list contains none or one node)
(fig.6.5.2.c).
•
• •
•
• •
Begin
Nodes
Counter
•
•
•
•
•
•
•
•
•
•
•
End
9
Fig.6.5.2.c. Double–linked list. The variant with indicators to extremities
• Double-linked lists can be implemented as circular lists.
• In figures 6.5.2.d respectively 6.5.2.e appears such a list in two equivalent graphical
representations.
•
•
1
•
•
2
•
•
3
•
•
4
•
Fig.6.5.2.d. Circular double–linked list
1
•
4
2
3
Fig.6.5.2.e. Circular double–linked list
• It’s also possible to use in implementation of the circular double-linked lists the fictive node
technique that means a node which “closes the circle”.
• Thus the field previous of this fictive node indicates the last node of the list, and the
field next of the fictive node indicates the first node (fig.6.5.2.f).
• When the list is empty, the both links indicate the fictive node itself.
• • •
•
•
•
•
•
• •
•
•
•
•
•
•
• •
•
Fictive
Fig.6.5.2.f. Double–linked list. The variant with fictive node
6.5.3. Stacks
• A stack is a special kind of list in which all insertions and deletions take place at one end,
called the top of the stack.
• Other names for a stack are "pushdown list," and "LIFO" or "last-in- first-out" list.
• The intuitive model of a stack is a pile of poker chips on a table, books on a floor, or dishes on a
shelf.
• In an obvious manner, it is only convenient to remove the top object on the pile or add a
new one above the top.
6.5.3.1. ADT Stack
• In the consequent manner of presenting abstract data types in this manual, the definition of the
ADT Stack presumes to specify:
• (1) The associate mathematical model.
• (2) The used notations.
• (3) The operators defined for this type.
• All these elements are presented in [6.5.3.1.a].
------------------------------------------------------------------ADT Stack
Mathematical Model: a finite sequence of nodes. All nodes belong
to the same TypeElement, named base type. A stack is a special
kind of list in which all insertions and deletions take place
at one end, called the top.
Notations:
s: TypeStack;
x: TypeElement.
[6.5.3.1.a]
Operators:
1.InitStack(s:TypeStack){:TypePosition}; - make stack s empty.
2.TopStack(s:TypeStack):TypeElement; - retrieves the top of
the stack s.
3.Pop(s:TypeStack); - delete the top of the stack s.
4.Push(x:TypeElement,s:TypeStack); - insert element x in the
top of the stack s. The old top becames the next element.
5.EmptyStack(s:TypeStack):boolean; - returns true if stack s
is empty and false otherwise.
------------------------------------------------------------------• In [6.5.3.1.b] apears an implementation example of ADT Stack using as support ADT List
restraint variant.
------------------------------------------------------------------{Implementation of ADT Stack using ADT List (restraint variant)}
TYPE TypeStack = TypeList;
VAR s: TypeStack;
p: TypePosition;
x: TypeNode;
b: boolean;
[6.5.3.1.b]
{InitStack(s:TypeStack);}
p:= Initialize(s);
{TopStack(s);}
x:= Retrieve(First(s),s);
{Pop(s);}
Delete(First(s),s);
{Push(x,s);}
Insert(s,x,First(s));
{EmptyStack(s);}
b:= End(s)=0;
-----------------------------------------------------------typedef type_list type_stack;
type_stack s;
type_position p;
type_node x;
boolean b;
/*[6.5.3.1.b]*/
/*init_stak(s:type_stack);*/
p= initialize(s);
/*top_stack(s);*/
x= retrieve(primul(s),s);
/*pop(s);*/
delete(&first(s),s);
/*push(x,s);*/
insert(s,x,first(s));
/*empty_stack(s);*/
b= end(s)==0;
/*--------------------------------------------------------*/
• In the same time, this is an example of hierarchical implementation of an ADT which
illustrates:
• (1) The flexibility and simplicity of this approach.
• (2) Its invariance related to the levels of hierarchy implementation.
• In other words, if the ADT List implementation is modified, the ADT Stack implementation is
not affected, if the prototypes of the defined operators remain unchanged.
• The frequent and efficient usage of the stack data structures in the programming domain, has
determined its evolution from the state of advanced data structure to that of fundamental
data structure.
• This tendency was materialized by hardware implementation of the stack in all modern
computing architectures and by including the specific stack data structure operators as wired
instructions.
6.5.3.2. An Array Implementation of ADT Stack
• Since a stack with its operations is a special case of a list with its operations, every
implementation of lists we have described works for stacks.
• In particular, the linked-list representation of a stack is easy, as PUSH and POP operate only on
the header cell and the first cell on the list.
• In fact, headers can be pointers or cursors rather than complete cells, since there is no
notion of "position" for stacks, and thus no need to represent position 1 in a way
analogous to other positions.
• However, the array-based implementation of lists we gave in &6.3.1 is not a particularly good
one for stacks.
• The reason: every PUSH or POP requires moving the entire list up or down, thus taking
time proportional to the number of elements on the stack.
• A better arrangement for using an array takes account of the fact that insertions and deletions in
a stack occur only at the top.
• We can anchor the bottom of the stack at the bottom (high-indexed end) of the array,
and let the stack grow towards the top (low-indexed end) of the array.
• An indicator called top indicates the current position of the first stack element (fig.
6.5.3.2.a).
• The abstract data structure designed for this implementation is presented in [6.5.3.2.a].
------------------------------------------------------------------{Array implementation of stacks}
TYPE TypeStack = RECORD
top: integer;
[6.5.3.2.a]
elements: ARRAY[1..maxLength] OF TypeElement
END;
------------------------------------------------------------------/* Array implementation of stacks */
enum {max_length = 50};
typedef struct type_stack {
int top;
type_element elements[max_length];
} type_stack;
/*[6.5.3.2.a]*/
/*---------------------------------------------------------------*/
1
2
3
top
(free area)
•••
•
(last element)
•••
(2nd element)
maxLength
(1st element)
elements
Fig.6.5.3.2.a. Array implementation of ADT Stack
• A stack
instance consist in
...,elements[maxLength].
sequence
elements[top],
elements[top+1],
• The stack is empty if top = maxLength +1.
• The specific operators for this implementation are presented in [6.5.3.2.b]
• {Stack
structure
operators}
–
array
based
implementation
PROCEDURE InitStack(VAR s: TypeStack);
BEGIN
s.top:= maxLength+1
END;{InitStack}
FUNCTION EmptyStack(s: TypeStack): boolean;
BEGIN
IF s.top>maxLength THEN
EmptyStack=: true
ELSE EmptyStack:= false
END;{EmptyStack}
FUNCTION TopStack(VAR s: TypeStack): TypeElement;
BEGIN
IF EmptyStack(s) THEN
BEGIN
er:= true; mesage('stack is empty')
END
ELSE
[6.5.3.2.b]
TopStack:= s.elements[s.top]
END;{TopStack}
PROCEDURE Pop(VAR s: TypeStack, VAR x: TypeElement);
BEGIN
IF EmptyStack(s) THEN
–
specific
BEGIN
er:= true; mesage('stack is empty')
END
ELSE
BEGIN
x:= s.elements[s.top]; s.top:= s.top+1
END
END;{Pop}
PROCEDURE Push(x: TypeElement; VAR s: TypeStack);
BEGIN
IF s.top=1 THEN
BEGIN
er:= true; mesage('stack is full')
END
ELSE
BEGIN
s.top:= s.top-1; s.elements[s.top]:= x
END
END;{Push}
------------------------------------------------------------------/* Stack structure – array based implementation – specific
operators */
#include <stdio.h>
typedef unsigned boolean;
#define true 1
#define false (0)
void init_stack(type_stack* s)
{
s->top= max_length+1;
}
/*init_stack*/
boolean empty_stack(type_stack s)
{
boolean empty_stack_result;
if (s.top>max_length)
empty_stack_result= true;
else empty_stack_result= false;
return empty_stack_result;
}/*empty_stack*/
type_element top_stack(type_stack* s)
{
boolean er;
type_element topstack_result;
if (empty_stack(*s)){
er= true;
printf("stack is empty");
}
else
/*[6.5.3.2.b]*/
topstack_result= s->elements[s->top-1];
return topstack_result;
}/*top_stack*/
void pop(type_stack* s, type_element* x)
{
boolean er;
if (empty_stack (*s)){
er= true;
printf("stack is empty");
}
else{
*x= s->elements[s->top-1];
s->top= s->top+1;
}
}
/*pop*/
void push(type_element x, type_stack* s)
{
boolean er;
if (s->top==1){
er= true;
printf("stack is full");
}
else{
s->top= s->top-1;
s->elements[s->top-1]= x;
}
}
/*push*/
/*----------------------------------------------------------------*/
6.5.3.3. Stack Using Examples
• Example 6.5.3.3.a. Evaluation of a postfix format expression.
• Problem. Given an arithmetic expression in postfix format, build a function which
evaluates the considered expression.
• In this situation, the following technique can be used:
• (1) The expression is parsed from left to right.
• (2) The arithmetic operations are executed as they are encountered, replacing each time
the group [operand, operand, operator] with the value obtained as result of current
evaluation.
• We presume that the correct postfix format of the arithmetic expression to be evaluated is
stored in the list PostFix, whose nodes contains an operand or an operator.
• The function PostfixEvaluation evaluates the given expression using the stack structure
Eval in which stores the numerical values of operands waiting for the execution of the required
arithmetic operation [6.5.3.3.a].
------------------------------------------------------------------{Evaluation of a postfix format expression}
FUNCTION PostfixEvaluation (PostFix: TypeList): REAL;
{Evaluate a postfix arithmetic expression represented as a list of
nodes. It is presumed that the postfix format of the expression is
correct}
VAR Eval: TypeStack;
TopNumber,SecondNumber,Result: REAL;
t: NodePostFix;
BEGIN
InitStack(Eval);
WHILE *there are nodes in PostFix DO
[6.5.3.3.a]
BEGIN
*read the current node of PostFix in t;
IF t is number THEN
{t is an operand}
Push(t,Eval)
ELSE
BEGIN {t is an operator}
TopNumber:= TopStack(Eval);
Pop(Eval);
SecondNumber:= TopStack(Eval);
Pop(Eval);
Result:= SecondNumber <t> TopNumber; {operator t}
Push(Result, Eval)
END
END
PostfixEvaluation:= TopStack(Eval)
END;{PostfixEvaluation}
------------------------------------------------------------------float postfix_evaluation (type_list postfix)
/* Evaluate a postfix arithmetic expression represented as a list
of nodes. It is presumed that the postfix format of the expression
is correct */
{
type_stack eval;
float top_number,second_number,result;
node_postfix t;
/*[6.5.3.3.a]*/
float postfix_evaluation_result;
init_stack(&eval);
while *there are nodes in post_fix
{
*read the current node of postfix in t;
if t is number /*t is an operand*/
push(t,&eval);
else
{
/*t is an operator*/
top_number= top_stack(&eval);
pop(&eval, 0);
second_number= top_stack(&eval);
pop(&eval, 0);
result= second_number <t> top_number; /*operator t*/
push(result, &eval);
}
}
postfix_evaluation_result= top_stack(&eval);
return postfix_evaluation_result;
} /*postfix_evaluation*/
/*---------------------------------------------------------------*/
• Example 6.5.3.3.b. Avoiding recursion.
• As it was discussed in &5.2.4, the recursion can be removed practically in any situation.
• Usually, the removing of recursion increases the algorithm’s performance but the algorithm
becomes more complicate and more difficult to be understood.
• The aim of this section is to present an example of a general case of removing recursion that
means the manner in which a recursive algorithm can be transformed in an iterative one.
• In this purpose, a stack data structures can be used.
• As example we will present a recursive solution and the correspondent iterative solution of a
simplified version of the classical “knapsack problem”.
• Problem specification:
• Being given a value TW (total weight) and a set of weights represented as positive
integers, w1, w2, ..., wn.
• Find if the weights can be selected in such manner that their sum to be exactly TW.
• For example if TW = 10 and the weights are 7, 5, 4, 4, 1 then the 2nd, 3rd and the last weight can
be selected because 5+4+1=10.
• This justifies the problem’s name – knapsack– because a traveler can carry as principle a
sack with a limited weight TW.
• The complete variant of this problem which takes into consideration the weights of the
objects as well as their values has been discussed and solved in a recursive manner in
&5.4.6.
• The recursive algorithm which solves the simplified version of the knapsack problem appears
in [6.5.3.3.b].
------------------------------------------------------------------{ Knapsack Problem - recursive solution}
FUNCTION Knapsack(TW:integer; Candidate:integer):boolean;
BEGIN
IF TW=0 THEN
Knapsack:=true
[6.5.3.3.b]
ELSE
IF (TW<0) OR (Candidate>n) THEN
Knapsack:=false
ELSE {solution which includes the current candidate}
[*]
IF Knapsack(TW-W[Candidate],Candidate+1) THEN
BEGIN
[**]
Write(W[Candidate]);
Knapsack:=true
END
ELSE {solution which excludes the current candidate}
Knapsack:= Knapsack(TW,Candidate+1);
Writeln
END; {Knapsack}
------------------------------------------------------------------/* Knapsack Problem – recursive solution */
boolean knapsack(int tw, int candidate)
{
boolean knapsack_result;
if (tw==0)
knapsack_result=true;
/*[6.5.3.3.b]*/
else
if ((tw<0) || (candidate>n))
knapsack_result=false;
else /* solution which includes the current candidate */
/*[*]*/
if (knapsack(tw-w[candidate-1],candidate+1))
{
printf("%i", w[candidate-1]);
knapsack_result=true;
}
else /*solution which excludes the current
candidate */
/*[**]*/
knapsack_result=knapsack(tw,candidate+1);
printf("\n");
return knapsack_result;
} /*knapsack*/
/*---------------------------------------------------------------*/
• The recursive function Knapsack uses an array structure W: ARRAY[1..n] OF
integer which stores the weights of the objects.
• A call of function Knapsack(s,i) establishes if there is a set of elements belonging to
the domain [W[i],W[n]] whose sum is exactly s and if the answer is affirmative,
prints them.
• The working manner of the algorithm is the following:
• First, the algorithm verifies if the solution is immediate.
• This is possible if:
• (1) s = 0 – in this case the solution of the problem is the empty set of objects.
• (2) s > 0 and i > n – in this case, all the objects have been verified without
finding a sum equal to s.
• If none of these situations is valuable, then the function Knapsack(s-W[i],i+1) is
called to verify if there is a solution which includes W[i].
• If such a solution exists, then W[i]is printed.
• If the solution doesn’t exists, the function Knapsack(s,i+1)is called to verify if there
is a solution which doesn’t include W[i].
• The most general method to transform a recursive algorithm into an iterative one is based on a
stack data structure defined and processed by the programmer.
• A node of the stack contains the following elements (the calls’ context):
• (1) The current values of the calling parameters of the function (procedure)
implementing the algorithm.
• (2) The current values of the all local variable of the function (procedure)
implementing the algorithm.
• (3) An indication referring the return address that means the place in which returns
the execution control when the current instance of the algorithm’s call is finished.
• In the case of Knapsack function, we can remark the followings:
• (1) First, we can observe that any time the function implementing the algorithm is called, that
presumes the insertion of a new node in the user stack and as result, the number of candidates
increases by 1.
• In these circumstances, the variable Candidate can be defined as a global variable
whose value is incremented by 1 for each push in the stack respectively decremented by
1 for each pop.
• (2) A second remark can be made regarding the call instance’s return address stored in the
stack.
• In fact the return address for a call instance of Knapsack:
• Is situated into another function, procedure, or program which has called the
function Knapsack initially.
• Is the next address of one of the two recursive calls from the inside of the
function Knapsack marked as [*] – (including the candidate) respectively [**]
– (excluding the candidate) in [6.5.3.3.b].
• All these situations can be modeled using a variable of type State which can have one
of the next values:
• external – indicates a call from outside of function Knapsack.
• included – indicates a recursive call from the place indicated by [*] in
[6.5.3.3.b], which presumes the inclusion of W[Candidate] in the solution.
• excluded - indicates a recursive call from the place indicated by [**] in
[6.5.3.3.b], which presumes the exclusion of
solution.
W[Candidate]
from the
• If the value of variable of type State is stored in the stack as an indication for the return
address, then TW can be treated as global variable.
• When the value of variable of type State changes from external in included,
W[Candidate] is subtracted from TW.
• When the value of variable of type State changes from included in excluded,
W[Candidate] is added to TW.
• To represent the effect of execution of function Knapsak, in the sense that a solution was found
or not, a Boolean global variable Victory is used.
• Once a solution is found, variable Victory is set to true and it determines the stack to be
emptied and all the weights which are in state included, to be printed.
• As result, the stack can be declared as consisting of nodes of type State [6.5.3.3.c]:
------------------------------------------------------------------{Knapsack Problem – iterative solution – stack definition}
[6.5.3.3.c]
TYPE State =(external,included,excluded);
TypeStack ={a suitable declaration of a stack having nodes of
type State}
------------------------------------------------------------------/* Knapsack Problem – iterative solution – stack definition */
typedef enum {external,included,excluded} state;
/*[6.5.3.3.c]*/
type_stack = /* a suitable declaration of a stack having nodes of
type State */
typedef state type_element;
/*---------------------------------------------------------------*/
• Taking into account all the above considerations, in [6.5.3.3.d] appears the iterative version of
procedure Knapsack which:
• Operate over an array of weights W.
• It is conceived in terms of operators defined for the abstract data structure stack.
• Although this procedure can be more efficient than the recursive variant of Knapsack:
• It is strictly specific for the algorithm in discussion.
• Contains in an obvious manner more lines of code.
• It is more complicated.
• It is difficult to be understood.
• For these reasons, the elimination of the recursion is recommended when the factor “execution
speed” is critical.
------------------------------------------------------------------procedure KnapsackIterative (TW: integer):boolean;
VAR Candidate: integer;
Victory: boolean;
s: TypeStack;
BEGIN
Candidate:= 1;
Victory:= false;
InititStack(s);
Push(extern,s); {init the stack with W[1] – starting
process}
REPEAT
IF Victory THEN
BEGIN {empty the stack and print the weights included
in the solution}
IF TopStack(s)=included THEN
Writeln(W[Candidate]);
Candidate:= Candidate-1;
Pop(s)
END
ELSE
[6.5.3.3.d]
IF TW=0 THEN
BEGIN {solution found}
Victory:= true; Candidate:= Candidate-1;
Pop(s)
END
ELSE
IF(((TW<0 AND (TopStack(s)=external)) OR
(Candidate>n)) THEN
BEGIN {no solution is possible with this
selection}
Candidate:= Candidate-1; Pop(s)
END
ELSE {there is not yet a solution; the current
Candidate’s state in s is analyzed}
IF TopStack(s)=external THEN
BEGIN {first attempt to include a candidate}
TW:= TW-W[Candidate];
Candidate:= Candidate+1;
Pop(s); Push(included,s);
Push(external,s)
END
ELSE
IF TopStack(s)=included THEN
BEGIN {attempt to exclude the candidate}
TW:= TW + W[Candidate];
Candidate:= Candidate+1;
Pop(s); Push(excluded,s);
Push(external,s)
END
ELSE
BEGIN {TopStack(s)=excluded;
abandon the current selection}
Pop(s); Candidate:= Candidate-1
END
UNTIL EmptyStack(s)
END;{KnapsackIterative}
------------------------------------------------------------------/*---------------------------------------------------------------*/
void knapsack_iterative (int tw)
{
int candidate;
boolean victory;
type_stack s;
/*[6.5.3.3.d]*/
candidate= 1;
victory= false;
init_stack(&s);
push(external,&s); /*init stack with W[1]*/
do {
if (victory)
{
/*empty stack and print weights included in
solution*/
if (top_stack(&s)==included)
printf("%i\n", w[candidate-1]);
candidate= candidate-1;
pop(&s, 0);
}
else
if (tw==0)
{
/*solution found*/
victory= true;
candidate= candidate-1;
pop(&s, 0);
}
else
if ((tw<0 &&(tops_tack(&s)==external))) ||
(candidate>n))
{ /* no solution is possible with this
selection */
candidate= candidate-1; pop(&s, 0);
}
else /* there is not yet a solution; the current
candidate’s state in s is analyzed */
if (tops_tack(&s)==external)
{
/*first attempt to include a
candidate*/
tw= tw-w[candidate-1];
candidate= candidate+1;
}
else
if (top_stack(&s)==included)
{
/*attempt to exclude the
candidate*/
tw= tw + w[candidate-1];
candidate= candidate+1;
pop(&s, 0); push(excluded,&s);
push(external,&s);
}
else
{
/*top_stack(s)=excluded;
abandon current selection*/
pop(&s,0); candidate= candidate-1;
}
} while (!(empty_stack(s)));
}
/*knapsack_iterative*/
/*---------------------------------------------------------------*/
6.5.4. Queues
• Queues are another special kind of list, where items are inserted at one end (the rear) and
deleted at the other end (the front).
• Another name for a queue is a "FIFO" or "first-in-first-out" list.
• The operations for a queue are analogous to those for a stack, the substantial differences being
that insertions go at the end of the list, rather than the beginning.
• The traditional terminology for stacks and queues is different.
6.5.4.1. The Abstract Data Type Queue
• In accordance with the previous treatment of an ADT, in [6.5.4.1.a] are defined two variants of
ADT Queue.
• In [6.5.4.1.b] is presented an implementation example of ADT Queue based on ADT List.
------------------------------------------------------------------ADT Queue
Mathematical Model: a finite sequence of nodes. All nodes belong
to the same TypeElement, named base type. A queue is a special
kind of list in which all insertions take place at one end
(the rear) and deletions at the other end (the front).
Notatations:
Q: TypeQueue;
x: TypeElement;
b: boolean;
[6.5.4.1.a]
Operators (set 1):
1.InitQueue(Q: TypeQueue){: TypePosition}; - makes Q empty
queue.
2.FrontQueue(Q: TypeQueue): TypeElement; element of Q.
returns the first
3.AddQueue(x: TypeElement, Q: TypeQueue); - inserts element x
at the rear of queue Q.
4.ExtractQueue(Q: TypeQueue); - delete the first element of Q.
5.EmptyQueue(Q: TypeQueue): boolean; - returns true if and only
if Q is empty.
Operators (set 2):
1.InitQueue(Q: TypeQueue); - makes Q empty queue.
2.EmptyQueue(Q: TypeQueue): boolean; - returns true if and only
if Q is empty.
3.FullQueue(Q: TypeQueue): boolean; - returns true if and only
if Q is full(operator dependent of implementation).
4.EnQueue(Q: TypeQueue, x: TypeElement); - inserts
element x at the rear of Q.
5.DeQueue(Q: TypeQueue, x: TypeElement); - delete the first
element of Q.
------------------------------------------------------------------{Implementation example of ADT Queue based on ADT List (operators
set 1)}
TYPE TypeQueue = TypeList;
VAR Q: TypeQueue;
p: TypePosition;
x: TypeElement;
b: boolean;
[6.5.4.1.b]
{InitQueue(Q);}
p:= Initialize(Q);
{FrontQueue(Q);}
x:= Retrieve(First(Q),Q);
{AddQueue(x,Q);}
Insert(Q,x,End(Q));
{ExtractQueue(Q);}
Delete(First(Q),Q);
{EmptyQueue(Q),Q);}
b:= End(Q)=0;
------------------------------------------------------------------/*---------------------------------------------------------------*/
/* Implementation example of ADT Queue based on ADT List (operators
set 1)*/
typedef type_list type_queue;
type_queue q;
type_position p;
type_element x;
boolean b;
/*init_queue(q);*/
/*front_queue(q);*/
/*add_queue(x,q);*/
/*[6.5.4.1.b]*/
p= initialize(q);
x= retrieve(first(q),q);
insert(q,x,end(q));
/*extract_queue(q);*/
delete(first(q),q);
/*empty_queue(q),q);*/
b= end(q)==0;
/*---------------------------------------------------------------*/
6.5.4.2. Pointer Implementation of Queues
• As for stacks, any list implementation is legal for queues.
• However, we can take advantage of the fact that insertions are only done at the rear to make
AddQueue efficient.
• Instead of running down the list from beginning to end each time we wish to add an
element, we can keep a pointer (or cursor) to the last element.
• As for all kinds of lists, we also keep a pointer to the front of the list. For queues that
pointer is useful for executing FrontQueue and ExtractQueue commands.
• In implementation, we can use a fictive cell as a header and have the front pointer point to it (the
fictive node technique).
• This convention allows us to handle an empty queue conveniently.
• The support data structures used for this purpose are presented in [6.5.4.2.a]:
------------------------------------------------------------------{Pointer implementation of queues - data structures definition}
TYPE TypeNodeReference=^TypeNode;
TypeNode = RECORD
element: TypeElement;
[6.5.4.2.a]
next: TypeNodeReference
END;
-----------------------------------------------------------------/* Pointer implementation of queues - data structures definition */
#include <stdlib.h>
typedef struct type_node* type_node_reference;
typedef struct type_node {
type_element element;
/*[6.5.4.2.a]*/
type_node_reference next;
} type_node;
/*---------------------------------------------------------------*/
• In consequence, we can define a queue data structure as a linked list consisting of two
pointers to the front and the rear of the list.
• The first node on a queue is a fictive one in which the element field is ignored.
• This convention, as mentioned above, allows a simple representation for an empty queue.
• Now TypeQueue can be defined as in [6.5.4.2.b].
------------------------------------------------------------------{Pointer implementation of queues – TypeQueue definition}
TypeQueue = RECORD
front,rear: TypeNodeReference
[6.5.4.2.b]
END;
------------------------------------------------------------------/* Pointer implementation of queues – type_queue definition */
typedef struct type_queue {
type_node_reference front,rear;
/*[6.5.4.2.b]*/
} type_queue;
/*---------------------------------------------------------------*/
• In [6.5.4.2.c] are presented the program sequences which implement the operators defined over
the ADT Queue (set 1).
------------------------------------------------------------------{Pointer implementation of queues – implementation of specific
operators (set 1)}
PROCEDURE InitQueue(VAR Q: TypeQueue);
BEGIN
NEW(Q.front); {create fictive node}
Q.front^.urm:= nil;
Q.rear:= Q.front; {fictive node is the first and the last of
the queue}
END;{InitQueue}
FUNCTION EmptyQueue(Q: TypeQueue): boolean;
BEGIN
IF Q.front:=Q.rear THEN
EmptyQueue:=true
ELSE
EmptyQueue:=false
END; {EmptyQueue}
FUNCTION FrontQueue(Q: TypeQueue): TypeElement;
BEGIN
[6.5.4.2.c]
IF EmptyQueue(Q) THEN
BEGIN
er:= true; WRITE('queue is empty')
END
ELSE
FrontQueue:= Q.front^.next^.element
END;{FrontQueue}
PROCEDURE AddQueue(x: TypeElement; VAR Q: TypeQueue);
BEGIN
NEW(Q.rear^.next); {a new node is added at the queue rear}
Q.rear:= Q.rear^.next;
Q.rear^.element:= x;
Q.rear^.next:= nil
END;{AddQueue}
PROCEDURE ExtractQueue(VAR Q: TypeQueue);
BEGIN
IF EmptyQueue (Q) THEN
BEGIN
er:= true; WRITE('queue is empty')
END
ELSE
Q.front:= Q.front^.next
END;{ExtractQueue}
------------------------------------------------------------------/* Pointer implementation of queues – implementation of specific
operators (set 1)- C*/
void init_queue(type_queue* q)
{
q->front = (type_node*)malloc(sizeof(type_node)); /*create
the fictive node */
q->front->next= NULL;
q->rear= q->front; /* fictive node is first and last node of
the queue */
}/*init_queue*/
boolean empty_queue(type_queue q)
{
boolean empty_queue_result;
if (q.front=q.rear)
empty_queue_result=true;
else
empty_queue_result=false;
return empty_queue_result;
}/*empty_queue*/
void front_queue(type_queue q)
{
/*[6.5.4.2.c]*/
void front_queue_result;
if (empty_queue(q))
{
er= true;
printf("queue is empty");
}
else
front_queue_result = q.front->next->element;
return front_queue_result;
}/*front_queue*/
void add_queue(type_element x, type_queue* q)
{
q->rear->next = (type_node*)malloc(sizeof(type_node));
/*a new node is added to the rear of the queue q*/
q->rear= q->rear->next;
q->rear->element= x;
q->rear->next= NULL;
}/*add_queue*/
void extract_queue(type_queue* q)
{
if (empty_queue(*q))
{
er= true;
printf("queue is empty");
}
else
q->front= q->front->next;
}/*extract_queue*/
/*----------------------------------------------------------------*/
6.5.4.3. Queues Implementation Using Circular Arrays
• The array representation of lists discussed in &6.3.1 can be used for queues, but it is not very
efficient.
• True, with a pointer to the last element, we can execute AddQueue in a fixed number of
steps.
• But ExtractQueue, which removes the first element, requires that the entire queue be
moved up one position in the array. Thus ExtractQueue takes O(n) time if the queue
has length n.
• To avoid this expense, we must take a different viewpoint. Think of an array as a circle, where
the first position (front) follows the last (rear), as suggested in Fig. 6.5.4.3.
1
MaxLength
2
.
.
.
.
.
.
...
Q.rear
•
•
Q.front
queue
Fig.6.5.4.3. Queue implementation using a circular array
• The queue is found somewhere around the circle in consecutive positions, with the rear
of the queue somewhere clockwise from the front.
• To add an element, we move the Q.rear pointer one position clockwise and store the
element in that position.
• To extract an element, we simply move Q.front one position clockwise.
• Thus, the queue migrates in a clockwise direction as we add and extract
elements.
• In these circumstances, if the circular array model is used, the procedures AddQueue and
ExtractQueue can be written to take some constant number of steps, that meaning O(1).
• In the following implementation, the indicator Q.front indicates the first element of the
queue, and Q.rear indicates the last element of the queue.
• There is one subtlety that comes up in the representation of Fig.6.5.4.3.
• The problem is that there is no way to distinguish an empty queue from one that
occupies the entire circle, because they have identical representations.
• Presuming that the queue represented in figure 6.5.4.3 is full (contains MaxLength elements),
indicator Q.rear will indicate the position adjacent to Q.front in parsing the queue in
clockwise sense (trigonometric negative).
• To represent the empty queue we presume a queue containing only one element.
• In this case Q.front and Q.rear indicate the same position (the unique element).
• If the only element is extracted, Q.front advances with one position forward (in
clockwise sense) indicating an empty queue.
• It can be observed that Q.front indicates one position in front of Q.rear if the array
is parsed in clockwise sense (trigonometric negative), which is exactly the same relative
position as when the queue had MaxLength elements.
• To solve this problem, we introduce the restriction that even though the array has MaxLength
places, we cannot let the queue grow longer than MaxLength-1.
• Thus, the full queue test is true if Q.rear becomes identical with C.fron after two
successive advancing steps.
• The empty queue test is true if Q.rear becomes identical with C.front after one
advancing step.
• In an evident manner the advancing steps are achieved in clockwise sense of parsing the
circular array.
• In [6.5.4.3.a] is presented the implementation of the operators defined for a queue data
structure based on circular array model.
------------------------------------------------------------------{Circular
array
implementation
of
queues
–
data
structures
definition}
TYPE TypeQueue = RECORD
[6.5.4.3.a]
elements: ARRAY[1..MaxLength] OF TypeElement;
front,rear: TypePosition
END;
------------------------------------------------------------------/* Circular array implementation of queues – data structures
definition */
typedef struct type_queue {
/*[6.5.4.3.a]*/
type_element elements[max_length];
type_index front,rear;
} type_queue;
/*---------------------------------------------------------------*/
• The operators’ implementation based on circular array appears in [6.5.4.3.b].
• Function Advance(i)returns the next position relative to the current position i in the circular
array.
------------------------------------------------------------------{Circular array implementation of queues - specific operators
implementation}
FUNCTION Advance(VAR i: TypePosition): TypePosition;
BEGIN
Advance:= (i mod MaxLength)+1
END;{Advance}
PROCEDURE InitQueue(VAR Q: TypeQueue);
BEGIN
Q.front:= 1;
Q.rear:= MaxLength
END;{InitQueue}
[6.5.4.3.b]
FUNCTION EmptyQueue(Q: TypeQueue): boolean;
BEGIN
IF Advance(Q.rear)=Q.front THEN
EmptyQueue:= true
ELSE
EmptyQueue:= false
END;{EmptyQueue}
FUNCTION FrontQueue(VAR Q: TypeQueue): TypeElement;
BEGIN
IF EmptyQueue(Q) THEN
BEGIN
er:= true; MESAGE('queue is empty')
END
ELSE
FrontQueue:= Q.elements[Q.front]
END;{FrontQueue}
PROCEDURE AddQueue(x: TypeElement; VAR Q: TypeQueue);
BEGIN
IF Advance(Advance(Q.rear))=C.front THEN
BEGIN
er:= true; MESAGE('queue is full')
END
ELSE
BEGIN
Q.rear:= Advance(Q.rear);
Q.elements[Q.rear]:= x
END
END;{AddStack}
PROCEDURE ExtractQueue(VAR Q: TypeQueue);
BEGIN
IF EmptyQueue(Q) THEN
BEGIN
er:= true;
MESAGE ('queue is empty')
END
ELSE
Q.front:= Advance(Q.front)
END;{ExtractQueue}
------------------------------------------------------------------/* Circular array implementation of queues - specific operators
implementation */
type_index advance(type_index* i)
{
type_index advance_result;
advance_result= (*i % max_length)+1;
return advance_result;
}/*advance*/
void init_queue(type_queue * q)
{
q->front= 1;
q->rear= max_length;
}/*init_queue*/
/*[6.5.4.3.b]*/
boolean empty_queue(type_queue q)
{
boolean empty_queue_result;
if (advance(&q.rear)==q.front)
empty_queue_result= true;
else
empty_queue_result= false;
return empty_queue_result;
}/*empty_queue*/
type_element front_queue(type_queue* q)
{
type_element front_queue_result;
if (empty_queue(*q))
{
er= true; printf("queue is empty");
}
else
front_queue_result= q->elements[q->front-1];
return front_queue_result;
}/*front_queue*/
void add_queue(type_element x, type_queue* q)
{
if (advance(&advance(&q->rear))==q->front)
{
er= true; printf("queue is full");
}
else
{
q->rear= advance(&q->rear);
q->elements[q->rear-1]= x;
}
}/*add_queue*/
void extract_queue(type_queue* q)
{
if (empty_queue(*q))
{
er= true;
printf ("queue is empty");
}
else
q->front= advance(&q->front);
}/*extract_queue*/
/*----------------------------------------------------------------*/
• The problems related to sensing the empty or the full queue can be solved simpler using a
counter of the elements in the queue.
• Thus the value 0 of the counter signifies empty queue and value MaxLength full
queue.
• The data structures related to this approach are presented in [6.5.4.3.c].
------------------------------------------------------------------{Circular array implementation of queues – data structures definition
(variant 2)}
CONST MaxLength = ...;
TYPE TypeElement = ...;
TypeIndex = 1..MaxLength;
TypeCounter = 0..MaxLength;
TypeArray = ARRAY[TypeIndex] OF TypeElement;
TypeQueue = RECORD
front,rear: TypeIndex;
counter: TypeCounter;
elements: TypeArray
END;
VAR C: TypeQueue;
x: TypeElement;
-----------------------------------------------------------/* Circular array implementation of queues – data structures
definition (variant 2*/
enum {max_length = 100};
typedef int type_element;
typedef unsigned char type_index;
/*[6.5.4.3.c]*/
typedef unsigned char type_counter;
typedef type_element type_array[max_length];
typedef struct type_queue {
type_index front,rear;
type_counter counter;
type_array elements;
} type_queue;
type_queue c;
type_element x;
/*----------------------------------------------------------------*/
• The procedures or functions which implement the specific operators in these circumstances can
be easily developed based on previous presented implementation examples.
6.5.4.4. Application. Infix-postfix Conversion
• The present application converts an infix format expression in the post fix representation of the
same expression using an iterative algorithm.
• The both formats of the expression are represented using as support linked lists.
• Two auxiliary data structures will be used:
• (1) The stack OperatorsStack which memorizes the operators for which the both
operands haven’t been yet determined.
• (2) The queue PostfixQueue which memorizes the postfix format of the expression
under construction.
• To each operator appearing in the infix format, a precedence value is associated:
• The operators ‘*’ and ‘/’ has the highest precedence.
• The operators ‘+’ and ‘–’ has the next precedence value.
• In order to simplify the algorithm design, in an artificial manner the ‘(‘ symbol
receives the lowest priority.
• The code of the conversion algorithm is presented in [6.5.4.4.a].
• As can be noticed, the elements are transferred from OperatorsStack in
PostfixQueue, which finally will be used as input for the evaluation procedure.
------------------------------------------------------------------{Application: Infix-postfix expression format conversion}
PROCEDURE InfixPostfixConversion(Infix: TypeList,
VAR PostfixQueue: TypeQueue);
VAR OperatorsStack: TypeStack;
PROCEDURE Transfer(VAR S: TypeStack, VAR Q: TypeQueue);
{Transfers the top of the stack S at the rear of the queue Q.
It is presumed that the elements of S and Q belongs to the same
type}
BEGIN
AddQueue(TopStack(S),Q);
Pop(S)
[6.5.4.4.a]
END;{Transfer}
BEGIN
InitStack(OperatorsStack);
InitQueue(PostfixQueue);
WHILE *not End(Infix) DO
BEGIN
t= First(Infix);
[1]
IF *t is a number THEN
AddQueue(t, PostfixQueue)
ELSE
[2]
IF EmptyStack(OperatorsStack) THEN
Push(t, OperatorsStack)
ELSE
[3]
IF *t is left paranthesis THEN
Push(t, OperatorsStack)
ELSE
[4]
IF *t is right parenthesis THEN
BEGIN
WHILE *TopStack(OperatorsStack) is
different from left parenthesis DO
Transfer (OperatorsStack,PostfixQueue);
{discharge the queue until the proximal left
parenthesis}
Pop(OperatorsStack){discharge the left
parenthesis from stack}
END
ELSE
[5]
BEGIN
WHILE *t’s precedence ≤ precedence
of TopStack(OperatorsStack) DO
Transfer(OperatorsStack, PostfixQueue)
Push(t, OperatorsStack)
END
END;{WHILE}
[6] WHILE EmptyStack(OperatorsStack) DO
Transfer(OperatorsStack,PostfixQueue) {transfer the remaining
operators from the stack}
END;{InfixPostFixConversion}
------------------------------------------------------------------/* Application: Infix-postfix expression format conversion */
void infix_postfix_conversion(type_list infix,
type_queue* postfix_queue);
static void transfer(type_stack* s, type_queue* q){
/*Transfers the top of S at the rear of Q. It is presumed that
elements of S and Q belong to the same type*/
add_queue(top_stack(s),q);
pop(s, 0);
/*[6.5.4.4.a]*/
} /*transfer*/
void infix_postfix_conversion(type_list infix, type_queue*
postfix_queue) {
type_stack operators_stack;
init_stack(&operators_stack);
init_queue(postfix_queue);
while not end(infix)
{
*t= first(infix);
/*[1]*/if (t is number)
Add_queue(t, postfix_queue);
else
/*[2]*/
if (empty_stack(operators_stack))
push(t, &operators_stack);
else
/*[3]*/
if (t is left parenthesis)
push(t, &operators_stack);
else
/*[4]*/
if (t is right parenthesis){
while (top_stack(operators_stack) is different
from left parenthesis)
transfer (&operators_stack,postfix_queue);
pop(&operators_stack, operators_stack);/* discharge the queue until the proximal left*/
else{
/*[5]*/
while (t’s precedence ≤ precedence of
top_stack(operators_stack))
transfer(&operators_stack, postfix_queue);
push(t, &operators_stack);}
}
/*while*/
/*[6]*/while (! Empty_stack(operators_stack))
transfer(&operators_stack,postfix_queue); /*transfer the
remaining operators from the stack*/
} /*infix_postfix_conversion*/
/*---------------------------------------------------------------*/
• The InfixPostfixConversion, illustrates a small part of the activity accomplished by the
compiler or the interpreter when a mathematical expressions are translated in executable
code.
• In fact, the syntactic and semantic analysis phases are eluded, as well as the effective
nature of the operators (constants, variables, etc.).
• The recursive variant of the same application has been presented as an example of recursive
algorithm.
6.5.5. Priority Queues
• In many applications, records with keys must be processed in order, but not necessarily in full
sorted order and not necessarily all at once.
• Often a set of records must be collected, then the largest processed, then perhaps more records
collected, then the next largest processed, and so forth.
• An appropriate data structure in such an environment is one which supports the operations of
inserting a new element and deleting the largest element.
• This can be contrasted with queues (delete the oldest) and stacks (delete the newest).
• Such a data structure is called a priority queue.
• In fact, the priority queue might be thought of as a generalization of the stack and the queue
and other simple data structures.
• These data structures can be implemented with priority queues, using appropriate
priority assignments.
• Applications of priority queues include:
• Simulation systems - where the keys might correspond to “event times” which must be
processed in order.
• Job scheduling in computer systems - where the keys might correspond to “priorities”
which indicate which users should be processed first.
• Numerical computations - where the keys might be computational errors, so the largest
can be worked on first.
• Special traversal of some data structures.
• Many other applications.
6.5.5.1. ADT Priority Queue
• Considering the priority queue as an abstract data structure whose elements are affected by
priorities, the definition of this ADT Priority queue appears in [6.5.5.1.a].
------------------------------------------------------------------ADT Priority queue
Mathematical model: o a finite sequence of nodes. All nodes belong
to the same TypeElement, named base type. Each node has an
associated specific priority. In fact, a priority queue is a
special list in which insertions are normal but each deletion
extracts the highest priority element from the queue.
Notations:
q: TypePriorityQueue;
x: TypeElement;
[6.5.5.1.a]
Operators:
1.InitPriorityQueue(q: TypePriorityQueue); - construct the
empty priority queue q.
2.InsertPriorityQueue(x: TypeElement, q: TypePriorityQueue); inserts a new element x in priority queue q.
3.ExtractPriorityQueue(q: TypePriorityQueue): TypeElement;
- extracts the element with the highest priority from
priority queue q.
4.Replace(q: TypePriorityQueue, x: TypeElement): TypeElement; replaces the highest priority element of q with element x,
less the situation in which the new element is the highest
priority element. Returns the current highest priority
element of the queue.
5.ExchangePriority(q: TypePriorityQueue, x: TypeElement;
p: TypePriority); - exchange the priority of
element x from q and assign it with the priority p.
6.DeletePriorityQueue(q: TypePriorityQueue, x: TypeElement); deletes the element x from q.
7.EmptyPriorityQueue(q: TypePriorityQueue): boolean; Operator which returns true if and only if q is empty.
-------------------------------------------------------------------
• Operators Replace consists in an insertion followed by the deletion of the highest priority
element.
• It’s a different operation from the sequence deletion-insertion because presumes the
increasing of the queue dimension with one element for a moment.
• This operator is defined separately because in certain implementation it can be conceived
in a very efficient manner.
• In an analog manner, the operator ExchangePriority can be implemented as a deletion of
element x followed by the insertion of the same element with a changeded priority
• The construction of the queue can be implemented as a series of InsertPriorityQueue
operations.
• In general, the priority queues can be implemented in different modes, some based on simple
data structures, others on advanced data structures, each of them presuming different
performances for the specific operators [Se88].
• Different implementations of priority queues involve different performance characteristics for
the various operations to be performed, leading to cost tradeoffs.
• In fact, the performance differences are really the only differences allowed by the
abstract data structure concept.
• First, we’ll illustrate this point by examining a few elementary data structures for
implementing priority queues as arrays or linked lists.
• Next, we’ll examine a more advanced data structure, the heap data structure, showing how the
various operations of the ADT Priority queue can be implemented efficiently using this data
structure.
6.5.5.2. Array Implementation of the Priority Queues
• Implementation of a priority queue can be achieved storing the queue’s elements in a non
ordered array.
• In [6.5.5.2.a] is presented the data structure corresponding to this approach and in [6.5.5.2.b] the
implementation of the operators InsertPriorityQueue and
ExtractPriorityQueue.
------------------------------------------------------------------{Array implementation of priority queues – data structures}
TYPE TypeElement = RECORD
priority: TypePriority;
info: TypeInfo
END;
[6.5.5.2.a]
TipTypePriorityQueue = RECORD
elements: ARRAY[1..MaxLength] OF TypeElement;
elementsNumber: 0..MaxLength
END;
------------------------------------------------------------------/* Array implementation of priority queues – data structures – C */
typedef struct type_element {
type_priority priority;
type_info info;
} type_element;
/*[6.5.5.2.a]*/
typedef struct type_priority_queue {
type_element elements[max_length];
unsigned char elements_number;
} type_priority_queue;
-------------------------------------------------------------------
{Array
implementation
of
priority
queues
InsertPriorityQueue and ExtractPriorityQueue}
-
operators
PROCEDURE InsertPriorityQueue(x: TypeElement,
q: TypePriority_Queue);
BEGIN {performance O(1)}
q.elementsNumber:= q.elementsNumber+1;
q.elements[elementsNumber]:= x
END;{InsertPriorityQueue}
FUNCTION ExtractPriorityQueue(q: TypePriority_Queue): TypeElement;
VAR j,max: 1..DimMax;
BEGIN {performance O(n)}
[6.5.5.2.b]
max:= 1;
FOR j:=1 TO q.elementsNumber DO
IF q.elements[j].priority >
q.elements[max].priority THEN max:= j;
ExtractPriorityQueue:= q.elements[max];
q.elements[max]:= q.elements[elementsNumber];
q.elementsNumber:= q.elementsNumber-1
END;{ExtractPriorityQueue}
------------------------------------------------------------------/*
Array
implementation
of
priority
queues
operators
InsertPriorityQueue and ExtractPriorityQueue –C */
void insert_priority_queue(type_element x, type_priority_queue q)
{
/*performance O(1)*/
q.elements_number= q.elements_number+1;
q.elements[q.elements_number-1]= x;
}/*insert_priority_queue*/
type_element extract_priority_queue(type_priority_queue q)
/*performance O(n)*/
{
unsigned char j,max;/*O(n)*/
/*[6.5.5.2.b]*/
type_element extract_result;
max= 1;
for( j=1; j <= q.elements_number; j ++)
if (q.elements[j-1].prioritate >
q.elements[max-1].prioritate) max= j;
extrage_result= q.elements[max-1];
q.elemente[max-1]= q.elements[q.elements_number-1];
q.elements_number= q.elements_number-1;
return extract_result;
}/*extract_priority_queue*/
/*---------------------------------------------------------------*/
• For insertion, elementsNumber is incremented and the element is inserted on the last
position of array q.elements, operation which is constant related to time (O(1)).
• For extraction, the array is searched in order to find the highest priority element which is
returned. This element is then replaced by the element situated on the last position of the
array and elementsNumber is decremented. The operator requires an O(n) effort.
• Implementation of the Replacee is similar, presuming an insertion followed by an
extraction.
• In this context, the implementation of the other operators raises any kind of problems.
• In priority queues implementation can be used also ordered arrays.
• The queue’s elements are stored in the array in increasing order of priorities.
• In these conditions, operator ExtractPriorityQueue, returns the last element
q.elements[elementsNumber] and decrements elementsNumber, operator
which takes a constant interval of time.
• Operator InsertPriorityQueue presumes the moving to right with an array
position of all elements having higher priorities than the inserted element, this requiring
an O(n) effort.
• Operators defined for priority queues can be used in implementation of some array sorting
algorithms. For example:
• (1) The operator InsertPriorityQueue is applied in a repetitive manner in order
to introduce in the priority queue the elements to be sorted.
• (2)
The elements are extracted one after other,
ExtractPriorityQueue, until the queue is empty.
using
the
operator
• (3) The sorted sequence of elements in decreasing order of their priorities is obtained.
• If an unordered array is used for priority queue implementation, the selection sorting
algorithm is implemented.
• If a sorted array is used for priority queue implementation, the insertion sorting algorithm is
implemented.
6.5.5.3. Linked Lists Implementation of Priority Queues
• For priority queues implementation can be used linked lists in ordered or unordered variant.
• This approach doesn’t affect in principle the specific operators’ implementation.
• It makes possible a more efficient implementation of the insertion and deletion phases
in the priority queues due to the flexibility of the linked lists processing.
• Below is presented an implementation example of the operator ExtractPriorityQueue,
using priority queues represented as unordered linked lists.
• The afferent data structures are figured in [6.5.5.3.a].
-------------------------------------------------------------------
{Unordered linked lists implementation of priority queues – data
structures}
TYPE TypeNodeReference = ^TypeNode;
TypeNode = RECORD
element: TypeElement;
next: TypeNodeReference
END;
[6.5.5.3.a]
TypePriorityQueue = TypeNodeReference;
------------------------------------------------------------------/* Unordered linked lists implementation of priority queues – data
structures –C */
typedef int type_element;
typedef struct type_node* type_node_reference;
typedef struct type_node {
type_element element;
type_node_reference next;
} type_node;
/*[6.5.5.3.a]*/
typedef type_node_reference type_priority_queue;
/*---------------------------------------------------------------*/
• Some implementation details:
• In the extraction operator’s implementation is used a variant of the two pointers
technique utilizing a single pointer (current) in a look ahead manner.
• The first node of the list is a fictive one, it doesn’t contain any element having assigned
only the field next (“fictive node” technique) [6.5.5.3.b].
------------------------------------------------------------------{Unordered linked lists implementation of priority queues operator ExtractPriorityQueue}
FUNCTION ExtractPriorityQueue(VAR q: TypePriorityQueue):
TypeNodeReference;
VAR current: TypeNodeReference; {indicates the node preceding the
inspected node}
big: TypeElement; {the value of the biggest element}
previous: TypeNodeReference; {indicates the node preceding
the current biggest node}
BEGIN
[6.5.5.3.b]
IF EmptyPriorityQueue(q) THEN Error(`queue is empty´)
ELSE
BEGIN
big:= q^.next^.element; {q indicates the fictive node}
previous:= q; current:= q^.next;
WHILE current^.next <> nil DO
BEGIN {compares the value of variable big with the value
of the element succeeding the current element (look-ahead)}
IF current^.next^.element > big TEHN
BEGIN {a bigger element was found}
previous:= current; {previous stores the pointer
indicating the node preceding the element
with the biggest key}
big:= current^next^.element
END;{IF}
current:= current^.next
END;{WHILE}
ExtractPriorityQueue:= previous^.next ; {returns the
pointer indicating the biggest element}
current:= previous^.next;
previous^.next:= previous^.next^.next; {deletion}
DISPOSE(current)
END;{ELSE}
END;{ExtractPriorityQueue}
------------------------------------------------------------------/* Unordered linked lists implementation of priority queues operator ExtractPriorityQueue – C */
#include <stdlib.h>
type_node_reference extract_priority_queue(type_priority_queue* q)
{
type_node_reference current; /* indicates the node preceding
the inspected node */
type_element big;
/* the value of the biggest element */
type_node_reference previous; /* indicates the node preceding
the current biggest node */
type_node_reference extract_result;
if (empty_priority_queue(*q))
printf("queue is empty");
/*[6.5.5.3.b]*/
else{
big= (*q)->next->element; /*q indicates the fictive node */
previous= *q; current= (*q)->next;
while (current->next != NULL){/* compares the value of
variable big with the value of the element succeeding the
current element (look-ahead)*/
if (current->next->element > big){
previous= current; /* previous stores the pointer
indicating the node preceding the element
with the biggest key */
big= current->next->element;
}/*if*/
current= current->next;
}/*while*/
extract_result= previous->next; /* returns the
pointer indicating the biggest element */
current= previous->next;
previous->next= previous->next->next;
}/*else*/
return extract_result;
}/*extract_priority_queue*/
/*---------------------------------------------------------------*/
• The implementation of the others operators concerning priority queues using unordered lists,
doesn’t raise any problems.
• These simple implementations of the ADT Priority queue in many situations are more useful
that those based on sophisticated models.
• Thus, the implementations based on unordered lists are suitable in the situations which
presume more insertions and few extractions in the priority queue.
• Instead, the implementations based on ordered lists are suitable when the priorities of
the queues’ elements are close as value with the highest priority element.
6.5.5.4. Heap Based Implementation of Priority Queues
• A heap is a partial ordered binary tree represented as an array, whose elements satisfy the
heap conditions (&3.2.5).
• As consequence, the largest (highest priority) key is always situated on the first position
of the array which materializes the heap.
• Algorithms implementing operators which are defined for heap data structures require in the
worst case an effort of order O(log2 N ).
• Heaps can be used in priority queues implementation.
• There are algorithms which handle heaps top-down, others bottom-up.
• With respect to elements of a queue (heap) we presume:
• (1) Each element has an associated priority.
• (2) Elements are stored in an array heap with a specified maximum size (MaxDim).
• (3) The current dimension of the array is stored in variable elementsNumber which is
included in the priority queue definition [6.5.5.4.a].
------------------------------------------------------------------{Heap based implementation of the priority queues – data
structures}
TYPE TypeElement = RECORD
priority: TypePriority;
info: TypeInfo
END;
[6.5.5.4.a]
TypePosition = 1..MaxDim;
TypePriorityQueue = RECORD
heap: ARRAY[TypePosition] OF TypeElement;
elementsNumber: 0..MaxDim
END;
-------------------------------------------------------------------
/* Heap based implementation
structures – C */
of
typedef struct type_element {
type_priority priority;
type_info info;
} type_element;
the
priority
queues
–
data
/*[6.5.5.4.a]*/
typedef unsigned char type_index;
typedef struct type_priority-queue {
type_element heap[max_dim];
unsigned char elements_number;
} type_priority-queue;
/*---------------------------------------------------------------*/
• In order to construct a heap the operator InsertHeap is used.
• In chapter 3 we have studied the possibility to extend a heap to left by placing the new
element in the top of the heap.
• The new element is let to descend to the heap bottom until it finds its suitable
place, interchanging it each time with its highest priority child (Shift operator,
&3.2.5). More correct, this operator should be named DownHeap operator.
• Another possibility to insert an element in a heap is to extend the heap to right, by
placing the new element in its last position.
• This operation may violate the heap’s rules if the new element has a higher priority as
its parent.
• In this situation, the element advances up in the heap, each time being
interchanged with its parent, until it reaches its proper position in heap.
• The advancing process is finished when the new element priority is lower than
the priority of its current parent, or he had reached the first position in the heap.
• Procedure UpHeap presented in [6.5.5.4.b] implements this operator. That means the element
placed in the last position in the heap is advanced bottom-up until its proper place.
• This method is the inverse of that implemented by the procedure Shift used in heap
sort. (&3.2.5).
• The heap is extended to left with position 0, which do not belong to the heap, but it is
used as sentinel in the ascending process.
• Beside operator UpHeap in [6.5.5.4.b] appears the procedure which implements the insertion in
a heap (InsertHeap).
------------------------------------------------------------------{Heap based implementation of the priority queues – operators
UpHeap and InsertHeap}
PROCEDURE UpHeap(q: TypePriorityQueue, k: TypePosition);
VAR v: TypeElement;
BEGIN
v:= q.heap[k]; {new elementul is in position k}
q.heap[0].priority:=(the_highest_priority)+1;
{assign position 0 with the sentinel value}
WHILE q.heap[k DIV 2].priority <=
v.priority DO {look ahead}
BEGIN
q.heap[k]:= q.heap[k DIV 2];
k:= k DIV 2
END;
q.heap[k]:= v
[6.5.5.4.b]
END;{UpHeap}
PROCEDURE InsertHeap(x: TypeElement, q: TypePriorityQueue);
BEGIN
q.elementsNumber:= q.elementsNumber+1;
q.heap[q.elementsNumber]:= x;
UpHeap(q,q.elementsNumber)
END;{InsertHeap}
------------------------------------------------------------------/* Heap based implementation of the priority queues – operators
UpHeap and InsertHeap - C*/
void up_heap(type_priority-queue q, type_position k){
type_element v;
v= q.heap[k]; /* new element is in position k*/
q.heap[0].priority=(/*the_highest_priority*/)+1;
/* assign position 0 with the sentinel value */
while (q.heap[k/2].priority <= v.priority)
/*look ahead*/
{
q.heap[k]= q.heap[k/2];
k= k/2;
}
q.heap[k]= v;
/*[6.5.5.4.b]*/
}/*up_heap*/
void insert_heap(type_element x, type_priority-queue q)
{
type_element v;
q.elements_number= q.elements_number+1;
q.heap[q.elements_number]= x;
upheap(q,q.elements_number);
} /*insert_heap*/
/*---------------------------------------------------------------*/
• If in operator UpHeap the calling parameter (k DIV 2) is replaced by (k-1) we
obtain in fact sorting by insertion (performance O(n2)).
• In this case, the place in which the new element is inserted is found verifying and
shifting in a sequential manner the elements with a position to right.
• In the UpHeap procedure the shift is not linear (sequential) but from level to level in the
heap.
• As in insertion sorting algorithm, the interchange is not total, variable v being implied
all the time in this operation.
• Position 0 of the heap q.heap[0] which is used as sentinel is initially assigned with a
value which is higher than the highest elements’ priority.
• Operator Replace presumes the replacement of the highest priority element, that means the
element placed in the first position of the heap, with another element which is shifted top-down
in the heap until it reaches its position in accordance with de heap definition.
• Operator ExtractPriorityQueue presumes:
• (1) Extraction of the highest priority element which is placed in the first position in heap
(q.heap[1]).
• (2) Placement of the last element of the heap (q.heap[q.elementsNumber]) in the
first position.
• (3) Decrement the number of elements (q.elementsNumber).
• (4) Top-down shifting the first element in the heap toward its proper position.
• The top-down shifting in the heap starting with position k is implemented by the operator
DownHeap [6.5.5.4.c].
------------------------------------------------------------------{Heap implementation of priority queues – DownHeap operator}
PROCEDURE DownHeap(q: TypePriorityQueue, k: TypePosition);
VAR j: TypePosition;
v: TypeElement;
ret: boolean;
BEGIN
v:= q.heap[k];
[6.5.5.4.c]
ret:= false;
WHILE(k < q.elementsNumber DIV 2) AND (NOT ret) DO
BEGIN
j:= k+k;
IF j < q.elementsNumber THEN
IF q.heap[j].priority <
q.heap[j+1].priority THEN j:= j+1;
IF v.priority >= q.heap[j].priority THEN
ret:= true
ELSE
BEGIN
q.heap[k]:= q.heap[j]; k:= j
END
END;{WHILE}
q.heap[k]:= v
END;{DownHeap}
------------------------------------------------------------------/* Heap implementation of priority queues – down_heap operator – C
variant */
void down_heap(type_priority-queue q; type_position k)
{
tipindice1 j;
type_element v;
boolean ret;
v= q.heap[k];
/*[6.5.5.4.c]*/
ret= false;
while((k < q.elementsNumber/2) && (! ret))
{
j= k+k;
if (j < q.elementsNumber)
if (q.heap[j].priority <
q.heap[j+1].priority) j= j+1;
if (reccmp(v.priority, q.heap[j].priority)
>= 0)
ret= true;
else
{
q.heap[k]= q.heap[j]; k= j;
}
}/*while*/
q.heap[k]= v;
}/*down_heap*/
/*---------------------------------------------------------------*/
• The top-down shift in the heap is achieved by interchanging the element situated in the
current position k with the child having the highest priority, and then descending to the
next level.
• The process continues until the element placed in the current k position in the heap has a
higher priority than any of his children or the bottom of the heap has been reached.
• As in the previous situation, the interchange is not total because v is implied all the time.
• The WHILE loop has two exits:
• One is correlated with the reaching the bottom of the heap
(k>q.elementsNumber DIV 2).
• The second is correlated with finding the proper position in heap of the shifted
element (Boolean variable ret).
• With these explanations, implementation of operators ExtractPriorityQueue and
Replace is immediate [6.5.5.4.d].
------------------------------------------------------------------{Heap
implementation
of
priority
queues
–
operators
ExtractPriorityQueue and Replace}
FUNCTION ExtractPriorityQueue(q: TypePriorityQueue): TypeElement;
BEGIN
ExtractPriorityQueue:= q.heap[1];
q.heap[1]:= q.heap[q.elementsNumber];
q.ElementsNumber:= q.ElementsNumber-1;
DownHeap(q,1)
END;{ExtractPriorityQueue}
[6.5.5.4.d]
FUNCTION Replace(q: TypePriorityQueue
x: TypeElement): TypeElement;
BEGIN
q.heap[0]:= x;
DownHeap(q,0);
Replace:= q.heap[0]
END;{Replace}
------------------------------------------------------------------/*
Heap
implementation
of
priority
queues
–
operators
extract_priority_queue and replace – C */
type_element extract_priority_queue(type_priority-queue q)
{
type_element extract_result;
extract_result= q.heap[1];
q.heap[1]= q.heap[q.elementsNumber];
q.elementsNumber= q.elementsNumber-1;
down_heap(q,1);
return extract_result;
}/*extract_priority_queue*/
/*[6.5.5.4.d]*/
type_element replace(type_priority_queue q, type_element x)
{
type_element replace_result;
q.heap[0]= x;
down_heap(q,0);
replace_result= q.heap[0];
return replace_result;
}/*replace*/
/*---------------------------------------------------------------*/
• In the case of operator Replace(q,x) the position q.heap[0] of the heap is also used. It
has as sons the position 0 (the position itself) and position 1.
• Thus, if x has a higher priority than any other heap member the heap remains unmodified
otherwise x is shifted in the heap.
• In any situation, q.heap[0] is returned as the element with the highest priority.
• Operators DeletePriorityQueue and ChangePriority can be implemented as simple
combination of the above presented techniques.
• For example, if the priority of the element situated on position k of the heap is high then
UpHeap(q,k)can be used, otherwise, if its priority is low, the operator
DownHeap(q,k) can solve the situation.
6.5.5.5.
Abstract Data Type Heap
• We reiterate the observation that again, the heap concept was used as implementation support
this time for implementing in an efficient manner the priorities queues.
• As result, we propose to define the heap as an abstract data type – ADT Heap.
• ADT Heap consists in:
• (1) A mathematical model represented as a partially ordered binary tree.
• (2) A couple of specific operators for constructing (extending) the heap as UpHeap and
DownHeap, the extracting operator ExtractHeap, the operator HeapDimension
and the operator InitHeap which create the empty heap.
• In synthesis, ADT Heap appears in [6.5.5.5.a] .
------------------------------------------------------------------ADT Heap
Mathematical model: a partially ordered binary tree implemented as
a specific designed linear array structure. The heap elements
belong to the same type, named base type.
Notations:
a: TypeHeap;
x: TypeElement;
n: integer;
p: TypePosition
[6.5.5.5.a]
Operators:
1. h= InitHeap: TypeHeap; - operator which initiates h as an
empty heap and make the heap dimension equal to 0.
2. UpHeap(h: TypeHeap, x: TypeElement, p: TypePosition); - extends
to right heap h. The operator introduces element x on
position p (the last heap position) and shifts x bottomup in the heap until the position specific to its
priority is reached or till in the top of the heap
(position 1) if its priority is highest than any other
heap element. Increments heap dimension.
3. DownHeap (h: TypeHeap, x: TypeElement); - extends to left heap
h. The operator introduces element x in the top of the
heap, (on its first position) and shift it top-down
until the position specific to its priority is reached
or till the
bottom of the heap (last position) if its
priority is lowest than any other heap element.
4. x= ExtractHeap(h: TypeHeap): TypeElement; - operator which
extracts and returns in x the heap element having the
highest priority and then decrements its dimension. Next,
the heap h is restructured so that the highest priority
element from the remaining elements to reach the pole
position.
5. n= DimensionHeap(h: TypeHeap); - operator which supplies the
current dimension of heap h.
-------------------------------------------------------------------
• As it was already specified, the operators acting on the proposed ADT Heap, are all framed in
O(log2 n ) performance class, where n is heap dimension.
• The exceptions are operators InitHeap and HeapDimension which are O(1).
• An example of a heap definition, conceived as a partially ordered binary tree, represented as
an array structure, was presented in chapter 3 (§ 3.2.5).
• Implementation modalities of some of its specific operators has been presented in the above
mentioned section (§3.2.5) as well as in the current chapter in [6.5.5.4.b-d].
• Depending on applications, other operators can be defined for ADT Heap, for example
operators referring to checking or modifying elements’ priorities. Such an example will be
presented in the next volume of this course it the part dedicated to graph presentation.
6.6. Multi-list Data Structure
• A multi-list is a data structure whose nodes contain more than one link fields.
• In other words, a node of such a structure can belong to more linked lists in the same
time.
• In the specific specialty literature, the consecrate terms for this concept are:
• Braid
• Multi-list
• Multiply linked list [De89].
• Figure 6.6.a presents an example of a graphical representation of a multi-list data structure an in
[6.6.a] a definition example for such a structures is proposed.
BeginNameList
NextRetribution Name Experience Retribution NextName NextExperience
400 €
•
•
25 1000 €
•
•
•
Nil
ANE
10
•
•
BIL
•
JOHN
20 1400 €
•
TOM
5
BeginExperienceList
•
•
•
Nil
Nil
BeginRetributionList
700 €
Fig.6.6.a. Multi-list data structure example
------------------------------------------------------------------{Multi-list – data structures definition}
TYPE TypeName = STRING[20];
TypeInfo = RECORD
Name: TypeName;
Experience: integer;
Retribution: real
END;
[6.6.a]
TypeNodeReference = ^TypeNode;
TypeNode = RECORD
Info: TypeInfo;
NextRetribution,NextName,NextExperience: TypeNodeReference
END;
VAR beginNameList,beganExperienceList,beginRetributionList:
TypeNodeReference;
------------------------------------------------------------------/* Multi-list – data structures definition – C */
typedef char* type_name;
typedef struct type_info {
type_name name;
int experience;
float retribution;
/*[6.6.a]*/
} type_info;
typedef struct type_node* type_node_reference;
typedef struct type_node {
type_info info;
type_node_reference next_retribution,next_name,next_experience;
}type_node;
type_node_reference begin_name_list, begin_experience_list,
begin_retribution_list;
/*----------------------------------------------------------------*/
• The advantage of using such structures is obvious.
• The presence of multiple links in the same node, respectively the simultaneous
appurtenance of the same node to more lists, assures to this structure a high flexibility.
• This advantage corroborated with the versatile manipulation specific to linked structures, is used
in data bases implementation, mainly in relational data bases.
• The usage area of multi-list structures is much wider.
• For example a multi-list structure can be successfully used to store sparse matrices.
• It’s known the fact that sparse matrices are big dimension matrices usually
containing a reduced number of elements, the rest, meaning the big majority of
them, being zero.
• That the reason for the usual implementation of matrices using two dimension
arrays presumes a huge waste of memory space.
• To avoid this waste, a multi-list data structure can be used as in [6.6.b].
• In this structure, each matrices’ element different by zero is assigned to a
structure’s node.
• A node stores:
• The current element indices.
• The links to the proximal valid element in the same row, respectively the
proximal valid element situated on the same column.
------------------------------------------------------------------{Multi-list structure application – sparse matrices storage}
CONST
TYPE
rowMax = ...;
colMax = ...;
TypeNodeReference = ^TypeNode;
TypeNode = RECORD
row: 1..rowMax;
[6.6.b]
column: 1..colMax;
info: ...;
nextRight,nextDown: TypeNodeReference;
END;
VAR matrices: TypeNodeReference;
------------------------------------------------------------------/* Multi-list structure application – sparse matrices storage – C
*/
enum { row_max = 1000,
col_max = 1000};
typedef struct type_node* type_node_reference;
typedef struct type_node {
unsigned row;
unsigned column;
type_ingfo info;
type_node_reference next_right, next_down;
} type_node;
/*[6.6.b]*/
type_node_reference matrices;
/*---------------------------------------------------------------*/
6.7. Generalized Lists
• The only data structure defined in the LISP programming language is the list.
• How is already known, LISP programming language is designated to manipulate
symbols, being used mainly in the domain of the Artificial Intelligence.
• The fundamental structural unit defined in LISP is the atom which is conceived as a string
containing characters and/or digits.
• In this context, in LISP a list is a set of parenthesis containing any number of atoms and lists.
For example:
• B =(b) represents a list containing a single element.
• L =(a,b,(c,d,e),f) represents a more complex list structure containing a sub-list.
• Atoms and lists are represented in LISP by means of a special data structure named
generalized list.
• A generalized list’s node contains three fields:
• (1) Atom –Boolean field.
• (2) A field depending on the Atom field value:
• If Atom is TRUE then this field is named info and stores information meaning an
atom (an atom value).
• If Atom is FALSE, the field is named list and stores a reference to a list.
• (3) Next – the linking field.
• Based on this representation, in figure 6.7.a appear the graphical representations of atom ‘a’
and lists B and L defined above.
a
B
•
T
a
•
Nil
F
L
•
Nil
T
•
F
T
a
•
•
b
Nil
Nil
T
b
T
c
•
•
F
•
T
•
d
T
•
f
T
Nil
e
Nil
Fig.6.7.a. Examples of generalized lists
• To a better understanding of this concept, in [6.7.a] are presented the PASCAL respectively C
specification of the generalized list.
• Due to the implementation restriction of records with variants in C programming
language which presumes the placement of the variants at the end of the record, the field
Next is placed at the beginning of the node structure declaration.
------------------------------------------------------------------{Generalized list – data structures definition - (Pascal variant)}
TYPE TypeGeneralizedList = ^TypeNode;
TypeNode = RECORD
Next: TypeGeneralizedList;
CASE Atom: boolean OF
TRUE:(info: CHAR);
FALSE: (list: TypeGeneralizedList)
END;
[6.7.a]
VAR a,B,L: TypeGeneralizedList;
-------------------------------------------------------------------
/*Generalized list – data structures definition - (C variant)*/
typedef unsigned boolean;
#define true (1)
#define false (0)
typedef struct type_node* type_generalized_list;
typedef struct type_node{
/*[6.7.a]*/
type_generalized_list urm;
boolean atom;
union{
char info;
type_generalized_list list;
}u;
}type_node;
type_generalized_list a,b,l;
/*---------------------------------------------------------------*/
6.7.1. Abstract Data Type Generalized List
• The definition of ADT Generalized List presumes in conformity with the course practice, to
specify the mathematical model, the afferent notations and the set of specific operators.
• All these are presented in[6.7.1.a].
------------------------------------------------------------------ADT Generalized List
Mathematical model: a finite sequence of nodes. All nodes belong
to the same type named base type. Each node contains three
fields:(1) a Boolean field – Atom;(2) a field depending on
Atom field value: if Atom is TRUE, then this field is named
info and stores an atom value, if Atom is FALSE, then the field
is named list and stores a reference to a list;(3) a linking
field – Next.
Notations:
list: TypeGeneralizedList;
[6.7.1.a]
Operators:
1. car (list:TypeGeneralizedList):TypeGeneralizedList; operator which returns the first element of the list (which
can be atom or list).
2. cdr (list:TypeGeneralizedList):TypeGeneralizedList; -operator
which returns the remaining of the list, that is what
remains after the first element is eliminated.
3. cons (M,N:TypeGeneralizedList):TypeGeneralizedList; -operator
which builds a list in which M is the first element and N
the rest of the list.
------------------------------------------------------------------L
•
•
F
T
a
•
T
•
b
T
•
c
•
F
T
•
T
•
d
f
T
Nil
e
Nil
Fig.6.7.1.a. Example of generalized list
• For example for the generalized list L =(a,b,(c,d,e),f)in figure 6.7.1.a, the operators
car(L)= a and cdr(L)=(b,(c,d,e),f) are visualized in figure 6.7.1.b.
cdr(L)
•
car(L)
•
T
a
Nil
•
F
T
b
T
c
Nil
•
F
•
•
T
•
d
•
T
f
T
Nil
e
Nil
Fig.6.7.1.b. The operators car and cdr applied to generalized list L from fig.6.7.1.a
• In the same manner, for the generalized lists M =(g,h) and N =(i,j,k), the execution of
cons(M,N)=((g,h),i,j,k)) operator is graphically represented in fig. 6.7.c.
N
•
F
•
Nil
T
i
•
h
Nil
M
•
F
•
Nil
T
g
•
T
T
•
j
T
k
Nil
cons(M,N)
F
• Nil
F
•
•
T
T
•
i
g
•
T
j
•
T
h
Nil
T
k
Nil
Fig.6.7.1.c. Example of cons operator usage
6.8. Mapping Abstract Data Type
• A mapping or associative store is a function from elements of one type, called the domain
type to elements of another (possibly the same) type, called the range type (co-domain).
• We express the fact that the mapping M associates the element r of range type
TypeRange with the element d of domain type TypeDomain by M(d)=r.
• Certain mappings such as Square(i)=i2 can be implemented easily as a function by giving
an arithmetic expression or other simple means for calculating M(d) from d.
• However, for many mappings there is no apparent way to describe M(d) other than to store for
each d the value of M(d).
• For example, to implement a payroll function that associates with each employee a
weekly salary seems to require that we store the current salary for each employee.
• In the remainder of this section we will describe some methods of implementing such
mappings.
• For the beginning we will start by defining the abstract data type mapping.
6.8.1. ADT Mapping
• For a mapping M, being given an element d belonging to TypeDomain, the following
operators can be defined:
• (1) Operator InitMapping initializes a mapping to the null mapping that means the
mapping whose domain is empty.
• (2) Operator Assign changes the value of an existing M(d)or introduces a new
element by specifying the corresponding values of the domain respectively co-domain.
• (3) Boolean operator Compute which:
• (a) Verifies if M(d) is defined or not for a given d.
• (b) If yes, returns the value TRUE and supplies value M(d).
• (c) If not, returns the value FALSE.
• The above operators are presented in a synthetically manner in [6.8.1.a] which describes ADT
Mapping.
------------------------------------------------------------------ADT Mapping
Mathematical model: a function (algebraic) defined from elements of
one type called the domain type to elements of another
(possibly the same) type, called the range type (co-domain).
Notatations:
M: TypeMapping;
d: TypeDomain;
r: TypeRange;
[6.8.1.a]
Operators:
1.InitMapping(M: TypeMapping); - initializes M as null
mapping.
2.Assign(M: TypeMapping, d: TypeDomain, r: TypeRange); operator which assigns M(d) with the value r. If M(d) has
already a value it is changed in r, if it hasn’t a
previous value, it receives now the value r.
3.Compute(M: TypeMapping, d: TypeDomain, r: TypeRange):
boolean; - operator which returns TRUE and assigns r with
M(d) value if this is defined, otherwise it returns
FALSE.
-------------------------------------------------------------------
6.8.2. Array Implementation of ADT Mapping
• In many situations the domain type of a mapping function is an elementary type which can be
used as index in an array structure.
• In Pascal such an elementary type can be any scalar type (sub-domain, char or
enumeration)
• In this section, we will use the mapping type defined in [6.8.2.a].
------------------------------------------------------------------{Array implementation of ADT Mapping – definition of the data
structures}
TYPE TypeDomain = firstValue ..lastValue;
[6.8.2.a]
TYPE TypeMapping = ARRAY[TypeDomain] OF TypeRange;
------------------------------------------------------------------/* Array implementation of ADT Mapping – definition of the data
structures – C */
enum {first_value = 0,
last_value = 100};
typedef unsigned char type_domain;
/*[6.8.2.a]*/
typedef type_value type_mapping[last_value-first_value+1];
/*---------------------------------------------------------------*/
• Presuming that ‘undefined’ is a constant of TypeRange, the operators afferent to ADT
Mapping are presented in [6.8.2.b].
------------------------------------------------------------------{Array implementation of ADT Mapping – operators InitMapping,
Assign and Compute}
PROCEDURE InitMapping(VAR M: TypeMapping);
VAR i: TypeDomain;
BEGIN
FOR i:= firstValue TO lastValue DO
M[i]:= undefined
END;{InitMapping}
PROCEDURE Assign(VAR M: TypeMapping; d: TypeDomain, r: TypeRange);
BEGIN
M[d]:= r
END;{Assign}
FUNCTION Compute(VAR M:TypeMapping; d: TypeDomain;
VAR r: TypeRange): boolean;
BEGIN
IF M[d]:= undefined THEN
Compute:= FALSE
ELSE
BEGIN
r:= M[d]
Compute:= TRUE
END
END;{Compute}
------------------------------------------------------------------/* Array implementation of ADT Mapping – operators InitMapping,
Assign and Compute – C */
void init_mapping(type_mapping m){
type_domain i;
for( i= first_value; i <= last_value; i ++)
m[i–first_value]= undefined;
}/*init_mapping*/
void assign(type_mapping m, type_domain d, type_range r){
m[d–first_value]= r;
}/*assign*/
/*[6.8.2.b]*/
boolean compute(type_mapping m, type_domain d,
type_range* r){
boolean compute_result;
if (m[d–first_value]= undefined)
compute_result= false;
else
{
*r= m[d–first_value];
compute_result= true;
}
return compute_result;
}/*compute*/
/*---------------------------------------------------------------*/
6.8.3. Linked List Implementations of ADT Mapping
• There are many possible implementations of mappings with finite domains.
• For example, hash tables are an excellent choice in many situations. The subject will be
discussed in the next chapter (§7).
• In the present section we will describe another possibility.
• Any mapping with a finite domain can be represented by the list of pairs (d1,r1),
(d2,r2),...,(dk,rk),where
• d1,d2,...,dk are all the current members of the domain.
• ri is the value that the mapping associates with di, for i=1,2,...,k,
representing the range.
• We can then use any implementation of lists we choose for this list of pairs.
• To be more precise, the abstract data type mapping can be implemented as a linked list,
implemented in any of the known manners, where TypeElement is defined as in [6.8.3.a].
------------------------------------------------------------------{ADT Mapping – linked list based implementation – data structures}
TYPE TypeElement = RECORD
domain: TypeDomain;
[6.8.3.a]
range: TypeRange
END;
------------------------------------------------------------------/* ADT Mapping – linked list based implementation – data structures
– C */
typedef struct type_element {
type_domain domain;
/*[6.8.3.a]*/
type_range range;
} type_element;
/*---------------------------------------------------------------*/
• The operators defined for the mapping data structure implemented using ADT List as support
are figured in [6.8.3.b].
------------------------------------------------------------------{ADT Mapping - implementation based on ADT List (restraint variant)
- operators InitMapping, Assign and Compute}
PROCEDURE InitMapping(VAR M: TypeMapping);
BEGIN
Initialize (M)
END;{InitMapping}
[6.8.3.b]
PROCEDURE Assign(VAR M: TypeMapping; d: TypeDomain;
r: TypeRange);
VAR x: TypeElement;
p: TypePosition;
BEGIN
x.domain:= d;
x.range:= r;
p:= First(M)
WHILE p<>End(M) DO
IF Retreive(p,M).domain=d THEN
Delete(p,M) {delete M(d)}
ELSE
p:= Next(p,M):
Insert(M,x,First(M)) {insert (d,v) in list}
END;{Assign}
FUNCTION Compute(VAR M: TypeMapping; d: TypeDomain;
VAR r: TypeRange): boolean;
VAR p: TypePosition;
found: boolean;
Retrieve
BEGIN
p:= First(M);
found:= false;
WHILE (p<>End(M)) AND NOT found DO
BEGIN
IF Retrieve(p,M).domain=d THEN
BEGIN
r:= Retrieve(p,M).range;
found:= true
END;{IF}
p:= Next(p,M)
END;{WHILE}
Compute:= found
END;{Compute}
------------------------------------------------------------------/*ADT Mapping - implementation based on ADT List (restraint
variant) - operators init_mapping, assign and compute – C variant
*/
void init_mapping(type_mapping m){
initialize(m) ;
}/*init_mapping*/
/*[6.8.3.b]*/
void assign(type_mapping m, type_domain d, type_range r){
tip_element x;
tip_position p
x.domain= d;
x.range= v;
p= first(m);
while (p!=end(m))
if (retrieve(p,m).argument==d)
delete(p,m); /*delete M(d)*/
else
p= next(p,m);
insert(m,x,first(m)); /*insert (d,r) in list*/
}/*assign*/
boolean compute(type_mapping m, type_domain d,
type_range* r){
tip_position p;
boolean found;
boolean compute_result;
p= first(m);
found= false;
while ((p!=end(m)) && ! found)
{
if (retrieve(p,m).argument==d)
{
*r= retrieve(p,m).range;
found= true;
}/*if*/
p= next(p,m);
}/*while*/
compute_result= found;
return compute_result;
}/*compute*/
/*---------------------------------------------------------------*/