Kruse - Data Structures and Program Design in C 1991
Kruse - Data Structures and Program Design in C 1991
ISBN
p. c,n.
Ir.eludes bibl iographical rererences. Inc ludes index..
0.. 13- 725649-3
I. C (Computer program language)
Ill. Title.
I. Leung, Bruce P. II. Tondo, Clovis L.
Contents
QA76.73.C 15K78 1991
005.13'3-dc20 90-7 161
The typesening for 1his book was done by the authors using PreTi:;X, a preprocessor and macro package
for the TE)( typcsening sys1cm and 1he PosTSCRIPT page-descrip1ion language.
PreTE)( is a trademark of Roben L. Kruse; TE)( is a trademark of the American Mathematical Society;
POSTSCRIPT is a registered 1rademark of Adobe Systems. Inc.
The au1hors and publisher of this book have used their best effons in preparing this book. These effons
include the research. developmen1, and 1es1ing of the theory and progrtms in the book lO determine their
effectiveness. The au1hors and publisher make no warranty of any kind, expressed or implied. wi1h
regard 10 1hese programs or 1he documen1ation contained in this book. The au1hors and publisher shall
not be liable in any event for inciden1al or consequent ial damages in connect ion wi1h. or arising out of, Preface _________ xi Review Questions 27
the furnishing, perfom1ance. or use of these programs.
Synopsis xii
Course Structure xiii
References for Further Study 28
Acknowledgments xiv T he C Language 28
Programming Principles 28
T he Game of Life 29
CHAPTER 1
• e 1991 by Prentice-Hall, Inc. Programming Principles_ __ _ 1
= A Paramount Communications Company
1.1 Introduction 2
CHAPTER 2
- Englewood Cliffs, New Jersey 07632 Introduction
All righcs reserved . No part of this book may be reproduced, in any form or by any means, withou1
1.2 The Game of Life 3
1.2.1 Rules for the Game of Life 4
to Software Engineering _ _ _ 30
permission in writing from the publisher. 1.2.2 Examples 4
2.1 Program Maintenance 31
1.2.3 The Solution 6
Printed in the Uniced States of America 1.2.4 Life: T he Main Program 6 2.2 Lists in C 35
10 9 1.3 Programming Style 9
2.3 Algorithm Development:
1.3.1 Names 9
A Second Version of Life 37
1.3.2 Documentation and Format 11 2.3 .1 The Main Program 37
ISBN 0 - 13-725649-3 1.3.3 Refinement and Modularity 12
2.3.2 Refinement:
1.4 Coding, Testing, and Further Refinement 17 Development of the Subprograms 39
1 .4.1 Stubs 17 2.3.3 Coding the Functions 40
Prentice-Hall lnccmational ( UK) L imiced. London
Prentice-Hall of Australia P1y. Limited. Sydney
1.4 .2 Cou nting Neighbors 18
1 .4.3 Input and Output 19 2.4 Verification of Algorithms 44
Prentice-Hall Canada Inc.. Toro1110
1.4 .4 Drivers 21 2.4.1 Proving the Program 44
Prentice-Hall Hispanoamericana, S.A., Mexico
Prenlice-Hall of India Private Limi1ed, New Delhi 1.4.5 Program Tracing 22 2.4.2 Invariants and Assertions 46
Pren1ice-Hall of Japan, Inc., Tokyo 1.4.6 Princip les of Program Testing 23 2.4.3 Initialization 47
Simon & Schus1er Asia Pie. Ltd .. Singapore
Edi1ora Pren1ice-Hall do Brasil. Ltda .. Rio de Janeiro Pointers and Pitfalls 26 2.5 Program A nalysis and Comparison 48
v
CONTENTS CONTENTS vii
CHAPTER 6
6 Conclusions and Preview 51 4.3 Further Operations on linked lists 114 7.7 Mergesort for linked Lists 240
2.6.1 The Game of Life 51 4.3.1 Algorithms for Simply Linked Lists 114 Tables 7. 7.1 The Functions 240
2.6.2 Program Design
2.6.3 The C Language
53
55
4.3.2 Comparison of Implementations
4.3.3 Programming Hints 121
120
and Information Retrieva~I_ _ 178 7.7.2 Analysis of Mergesort 242
An apprentice carpenter may want only a hammer and a saw, but a master craftsman
employs many precision tools. Computer programming likewise requires sophisticated
tools to cope with the com plexity of real applications, and only practice with these tools
will build skill in their use. This book treats structured problem solving, the process of
data abstraction and structuring, and the comparative study of algori thms as fundamental
tools of program design. Several case studies of s ubstantial size are worked out in detail,
to show how all the tools are used together to build complete programs.
Many of the algorithms and data s tructures st udied here possess an intrinsic elegance,
a sim plicity that cloaks the range and power of their applicability. Before long the student
discovers that vast improvements can be made over the naive methods us ually used in
introductory courses. And yet this elegance of method is tempered with uncertainty.
The s tudent soon finds that it can be far from obvious which of several approaches will
prove best in particular applications. Hence comes an early opportunity to introduce
trnly difficult problems of both intrinsic interest and practical importance and to exhibit
the applicability of mathematical methods to algorithm verification and analysis.
The goal of programming is the construction of programs that are clear, complete,
and functional. Many students, however, find difficulty in translating abstract ideas into
practice. This book, therefore, takes special care in the fom1ulation of ideas into algo-
rithms and in the refinement of algorithms into concrete programs that can be applied to
practical problems. The process of data specification and abstraction, similarly, comes
before the selection of data s tructures and their implementations.
We believe in progressing from the concrete to the abstract, in the careful develop-
ment of motivating examples, followed by the presentation of ideas in a more general
form. At an early stage of their careers most students need reinforcement from seeing the
xi
PR EFACE PR E FA CE xiii
immediate application of the ideas that they study, and they require the practice of writ- examples and apologizing for its alleged expense. Others give little regard to its pitfalls.
ing and running programs to illustrate each important concept that they learn. This book We have therefore essayed to provide as balanced a treatment as possible. Whenever
therefore contains many sample programs, both short functions and complete programs recursion is !he natural approach ii is used without hesitation. Its first use in this text
of substantial length. The exercises and programming projects, moreover, constitute an Recursion is the second half of Chapter 7. where mergesort and quicksort are introduced. Chapter
indispensable part of this book. Many of these are immediate applications of the topic 8 (which may be read any time after stacks are defined) continues to study recursion in
under study, often requesting that programs be written and run, so that algorithms may depth. It includes examples illustrating a broad range of applications, an exposition of
be tested and compared. Some are larger projects, and a few are su itable for use by a the implementation of recursion, and gu idelines for deciding whether recursion is or is
group of several students working together. not an appropriate method t.o em ploy.
Binary trees arc surely among the most elegant a nd useful of data structures. Their
ynopsis Binary Trees study, which occupies Chap1er 9, ties together concepts from lists, searching, and sorting.
Programming By working through the first large projec1 (CONWAY'S game of Life), Chapter I expounds At the same time, binary trees provide a natural examp le of recursively defined data
Principles principles of top-down refinement, program design, review, and testing, principles that structures, anc therewith_afford an excellenl opportuni1y for 1he student to become more
the student will see demonstrated and is expec1ed to follow throughout the seque l. At comfonablc v.ith recursion applied both to data structures and algorithms.
the same time, this project provides an opportunity for the student to review the syntax Trees and Graphs Chapter IO completes the study of data structures by collecting several important
of C, the programming language used throughout the book. uses of rnultiway trees as data structures, including tries and B-trees, after which the
Introduction to Chapter 2 introduces a few of the basic concerns of software e ngi neeri ng, including chapter introduces graphs as more general structures useful for problem solving. The
Software problem specification and ana lysis, prototyping, algorithm design, refinement, verifica- presentations in each major section of Chapter IO are independent from each other.
Engineering tion, and analysis. These topics are illustrated by the development of a second program Case Study: The The case study in Chapter 11 examines the Polish notation in considerable detail,
Polish Notation exploring !he interplay of recursion, trees, and stacks as vehicles for problem solving
for the Life game, one based on an algorithm that is sufficiently subtle as to s how the
need for precise specifi ca1ions and verifica1ion, and one that shows why care must be and a lgorithm development. Some of the questions addressed can serve as an informal
taken in the choice of data structures. introduction to compiler design. Again, the algori1hms are fully developed within a func-
Lists The study of data structures begins in Chapter 3 with stacks, queues, and lists in tioning C program. This program accepts as input an expression in ordinary (infix) form,
contiguous implementation, always emphasizing the separation between the use of data lranslates the expression into postfix fom1, and evaluaies the expression for specified
Linked Lists structures and their implementation. Chapter 4 continues by studying the same data values of the variable(s).
structures in linked implementations and culminaies with a discussion of abstract data Removal of recursion is a topic that, we hope, most programmers may soon no
types. Compatible packages of C functions are developed for each impl ementation and longer need to study. But at present much important work must be done in contexts
are used in both large and small sample programs. The major goal of these chapters (like FORTRAN or CoooL) disallowing recursion. Methods for manual recursion removal
is to bring the student to appreciate data abstraction and to apply methods of top-down Removal of are therefore requ ired, and are collected for reference as Appendix B. Some instruc-
Recursion tors will wish t.o include the study of threaded binary trees wilh Chapter 9; this section
design to data as well as to algorithms.
Searching Chapters 5, 6, and 7 present algorithms for searching, table access (including hash- is therefore \Hitten so that it can be read independently of the remainder of Appen-
Tables and ing), and sorting. These chapters illustrate the interplay between a lgorithms and the dix B.
Information associated abstract data types, data structures, and implementations. The text introduces An lmroduction to C Appendix C, finally, is a brief introduction to the C programming language. This
Retrieval the ''big Oh" notation for elementary algorithm ana lysis and highlights the crucial choices is not a thorough treaiment of 1he language, but it is imended to serve as a review of C
Sorting to be made regarding best use of space, time, and programming effo1t. syntax and as a reference for the student.
These choices require that we find ana lyt ica l methods 10 assess algorithms, and
producing such analyses is a battle for which combinatorial mathematics must provide Course Structure
the arsenal. At an elementary level we can expect s1udents neither to be well armed nor
to possess the mathematical maturity needed to hone their ski lls to perfection. Our goal, The prerequisite for this book is a first cou rse in programming, wilh experience using 1he
therefore, is only to help students recognize the impo1tance of such skills and be glad for prerequisite elementary features or C. It is possible to introduce C concurrently if the students have
Mathematical later chances to study mathematics. Appendix A presents some necessary mathematics. had experience with a similar high level language such as Pascal. Chapter 2 includes a
Methods Most of the topics in the appendix will be familiar to the well-prepared swdent, but brief discussion of structures and Chapter 4 a study of pointers, in case students have
are included to help with common deficiencies. The final two sections of Appendix A, not previously met these topics. A good knowledge of high school mathematics will
on Fibonacci and Catalan numbers, are more advanced, are no! needed for any vital suffice for almost all the algorithm analyses, but further (perhaps concu rrent) preparation
purpose in the text, but are included to encourage combinatorial interest in the more in discrete mathematics will prove valuable.
mathematically inclined. content This book. includes all the topics of the ACM Course CS 2 (Program Design and
Recursion is a powerful tool, but one that is often misunderstood and sometimes Implementation), with additional emphasis on data abstraction, data structures, algorithm
used improperly. Some tex1books treat it as an afterthought, applying it on ly 10 trivial analysis, and large case studies, so that it is also suitable for a version of Course CS 7
iv PR E FA CE P R EF A CE xv
(Data Structures and Algorithm Analysis) that emphasizes program design along with • Projects. Programming projects also appear in most major sections. These include
implementations and applications of data structures. The book contains significantly more simple variations of programs appearing in the text, completion of projects begun
material than can usually be studied in a single term, and hence it allows flexibility in in the te.>..t, and major new projects investigating questions posed in the text.
designing courses with different content and objectives.
• Review Questions. Each chapter (except Chapter 11) concludes with simple re-
CS 2 The core topics specified for ACM Course CS 2 occupy Chapters 1-8 and the first
view questions to help the student collect and summarize the principal concepts of
third of Chapter 9. A one-term course based closely on CS 2 will normally include
the chapter.
almost all the content of these chapters, except for the more detailed algorithm analy-
ses, verifications, and some of the sample programs. The later chapters include all the • hlstr11ctor's Supplements. Instructors teaching from this book may obtain copies
advanced optional topics suggested for possible inclusion in CS 2. of the following materials:
An elementary course on data structures and algorithms should consider Chapters
• The Instructor's Resource Manual contains teaching notes on each chapter,
data structures I and 2 briefly (to look at the questions of data specification and time-space tradeoffs),
together wi th complete, detailed solutions to every exercise and programming
emphasize Chapters 3-10, and select other topics if time permits.
project in the text. The manual also includes software disks with complete,
CS7 A more advanced course in Data Structures and Algorithm Analysis (ACM course
running versio_ns for every programming project in the text.
CS 7) should begin with a brief review of the topics early in the book, placing special
emphasis on data abstraction, algorithm analysis, and criteria for choosing data structures • The Transparency Masters (several hundred in total) contain enlarged copies
and algorithms under various conditions. The remaining chapters will then provide a solid of almost all diagrams, program segments, and other important extracts from
core of material on data structures, algorithm design, and applications. the text.
two-term course A two-term course can cover the entire book, thereby attaining a satisfying integra-
tion of many of the topics from both of ACM courses CS 2 and CS 7. Students need time Acknowledgments
and practice to understand general methods. By combining the study of data abstraction,
data structures, and algorithms with their implementation in projects of realistic size, an We are grateful to the many people-colleagues. students, friends, and family- who
integrated course can build a solid foundation on which later, more theoretical courses have contributed in numerous ways to the development of the Pascal version on which
can be built. the current book is based. Some of these people arc named in the preface to the Pascal
Even if it is not covered in its entirety, this book will provide enough depth to enable Version. In addition, many more people have helped us produce the current C version.
interested students to continue using it as a reference in later work. It is important in BRIAN KERNIGHAN reviewed the entire manuscript with the greatest of care and pro·
vided us with many valuable comments and suggestions.
any case to assign major programming projects and to allow adequate time for their
completion. Further detailed reviews giving us much helpful information came from PHIL ADAMS
(Nova University), ALAN J. F1ursK1 (Arizona State University), DAN HIRSCHBERG (Univer·
sity of California at Irvine), SAM Hs u (Florida Atlantic University). GARY KNOTI (Uni·
:urther Features
versity of Maryland), 0ARRJ;N MrcLETIE (IBM), ANuRi::w NATHANSON (AGS information
Systems), TERRY SHOR!:: (South Shore Enterprises), Eo S1Mc6 (Nova University), MARTIN
• Chapter Previews. Each chapter begins with an outline and a brief statement of
goals and content to help the reader establish perspective. K. SOLOMON (Florida Atlantic University), CARLOS To:-mo (South Shore Enterprises), and
RED V1scuso (IBM).
• Application Programs. The text includes several large, complete programs that GERRY DIAZ, ROLLIE Gu1LO, Eo SHOCKLEY, and EDEN YouNT provided many helpful
illustrate principles of good software design and application of the methods devel- suggestions.
oped in the text. Code reading is an important skill for a programmer, but one that STEVEN MATHESON helped to develop the PrefJ:X typesetting system, incl uding the
is often neglected in textbooks. software for typesetting C program listings.
• Programming Precepts. Many principles of good programming are summarized We are grateful for the support of fami ly and friends: ANNE ALDOUS, DAVE EGAN,
with short, pithy statements that are well worth remembering. CHRIS KIKG, ESTHER KRUSE, HOWARD and SHIRLEY LEUNG, JULIA MISTRELLO, DEEANNE
• Marginal Notes. Keywords and other important concepts are high lighted in the SAFFORD, VICKI SHORE. and CAREN WEBSTER.
left margin, thereby allowing the reader to locate the principal ideas of a section Finally, we owe much to our friends at Prentice Hall: our editor MARCIA HORTON
without delay. (Editor in ChiP.f for r.omp11tP.r St.iP.nt.~ anci Engineering), .ToHN WAIT, who suggested
• Pointers and Pitfalls. Each chapter of the book (except Chapter 11) contains a this project, supplements editor ALICE DwoRK1N. marketing manager JEN'IIFER YouNG,
section giving helpful hints concerning problems of program design. KATHLEEN ScHIAPARE1.1.1 and the production staff whose names appear on the copyright
page, and all the others who have contributed to our work in many ways.
• Exercises. Exercises appear not only at the ends of chapters but with almost every
major section. These exercises he lp with the immediate reinforcement of the ideas
of the section and develop further related ideas.
Data Structures
and
Program Design
inC
CHAPTER 1
Programming
Principles
This chapter summarizes important principles of f!,ood programming, espe-
cially as applied to larf!,e projects, and illustrates methods for discovering
effective algorithms. In the process we raise questions in program desif!,n
that we shall address in later chapters, and review many of the special
features of the laniuage C by using them to write programs.
1
Programming Principles C HAPTER 1 SEC TION 1.2 The Game of Life 3
.1 INTRODUCTION and algorithms that might work that it can be hard 10 decide which is best, which may
lead to programming difficulties, or which may be hopelessly inefficient. The greatest
The greatest difficulties of writing large computer programs are not in deciding what the data strucJures room for variability in algorithm design is generally in the way in which the data of the
goals of the program should be, nor even in finding methods that can be used to reach program are stored:
these goals. The presi<lent of a business might say, "Let's get a computer to keep track
of all our inventory information, accounting records, and personnel files, and let it tell us • How they are arranged in relation to each other.
when inventories need to be reordered and budget lines are overspent, and let it handle • \Vhich d~ta arc kept in memory.
the payroll." With enough time and effort, a staff of systems analysts and programmers
might be able to determine how various staff members are now doing these tasks and • Which are calculated when needed.
write programs to do the work in the same way. • Which are k.epl in files, and how are the files arranged.
This approach, however, is almost certain to be a disastrous failure. Wh ile inter-
viewing employees, the systems analysts will find some tasks that can be put on the A second goal of chis book, therefore, is 10 present several elegant, yet fundamentally
problems of computer easily and will proceed to do so. Then, as they move other work to the si mple ideas for the organization of data, and several powerful algorithms for important
large programs computer, they will find that it depends on the first tasks. The output from these, un- tasks within data processing, such as sorting and searching.
fortunately, will not be quite in the proper form. Hence they need more programming When there are several different ways to organize data and devise algorithms it
to convert the data from the form given for one task to the form needed for another. becomes impo11ant to develop criteria to recommend a choice. Hence we devote attention
The programming project begins 10 resemble a patchwork quilt. Some of the pieces are analysis to analyzing the behavior of algorithms under various conditions.
stronger, some weaker. Some of the pieces are carefully sewn onto the adjacent ones, The difficulty of debugg ing a program increases much faster than its size. That is,
some are barely tacked together. If the programmers are lucky, their creation may hold if one program is twice the size of another, then it will likely not take twice as long to
together well enough to do most of the routine work most of the time. But if any change tes1i11g a11d debug, but perhaps four times as long. Many very large programs (such as operating
verification systems) are put into use still containing bugs that the programmers have despaired
must be made, it will have unpredictable consequences throughout the system. Later, a
new request will come along, or an unexpected problem, perhaps even an emergency, of finding, because the difficulties seem insurmountable. Sometimes projects that have
and the programmers' efforts will prove as effective as using a patchwork qu ilt as a consumed years of effort must be discarded because it is impossible to discover why
safety net for people jumping from a tall building. they will not work. If we do not wish such a fate for our own projects, then we must
purpose of book The main purpose of this book is to describe programming methods and tools use methods that will
that will prove effective for projects of realistic size, programs much larger than those program correctness • Reduce the number of bugs, making it easier to spot those that remain.
ordinarily used to illustrate features of elementary programming. Since a piecemeal
approach to large problems is doomed to fail, we must first of all adopt a consistent, • Enable LL~ to verify in advance that our algorithms are correct.
unified, and logical approach, and we must also be careful to observe important principles • Provide us with ways to test our programs so that we can be reasonably confident
of program design, principles that are sometimes ignored in writing small programs, but that they will not misbehave.
whose neglect wi ll prove di sastrous for large projects.
The first major hurdle in attacking a large problem is deciding exactly what the Development of such methods is another of our goals, but one that cam1ot yet be fully
problem problem is. It is necessary to translate vague goals, contradictory requests, and perhaps within our grasp.
specification unstated desires into a precisely formulated project that can be programmed. And the Infonnal su rveys show that, once a large and impot1ant program is fully debugged
methods or division s of work that people have previously used are not necessarily the and in use, then less than half of the programming effort that will be invested altogether
best for use in a machine. Hence our approach must be to determine overall goals, but maintenance in the project will have been completed. Maiflten a11ce of programs, that is, modifications
precise ones, and then slowly divide the work into smaller problems unti l they become needed to meet new requests and new operating environments, takes, on average, more
of manageable size. than half of the programming investment. For this reason, it is essential that a large
program design The maxim that many programmers observe, "First make your program work, then project be written to make it as easy to understand and modify as possible.
make it pretty," may be effective for small programs, but not for large ones. Each part
of a large program must be well organized. clearly written. and thoroughly understood.
or else its structure will have been forgotten. and it can no longer be tied to the other 1.2 THE GAME OF LIFE
parts of the project at some much later time, perhaps by another programmer. Hence
we do not separate style from other parts of program design, but from the beginning we If we may take the liberty to abuse an old proverb:
must be careful 10 form good habits.
Even with very large projects, difficu lties usua lly arise not from the inability to One conG-rete problem is worth a thousand unapplied abstractions. s,
find a solution but, rather, from the fact that there can be so many different methods
SECTION 1 .2 The Game of Lile 5
Programming Principles CHAPTER 1
case study Throughout this chapter we shall concentrate on one case study that, while not large by
realistic standards, illustrates both the methods of program design and the pitfalls that we 0 0 0 0 0 0
should learn to avoid. Sometimes the example motivates general principles; sometimes
0 1 2 2 1 0
the general discussion comes first; always it is with the view of discovering general
methods that will prove their value in a range of practical applications. In later chapters 0 1 • 1 •1 1 0
we shall employ similar methods for much larger projects. moribund example
The example we shall use is the game called Life, which was introduced by the 0 1 2 2 1 0
British mathematician J. H. CONWAY in 1970.
0 0 0 0 0 0
generation.
5. If a cell is dead, then in the next generation it will become alive if it has exactly has the neighbor counts as shown. Each of the living cells has a neighbor count of three,
three neighboring cells, no more or fewer, that are already alive. All other dead and hence remains alive, but the dead cells all have neighbor counts of two or less, and
cells remain dead in the next generation. hence none of them becomes alive.
6. All births and deaths take place at exactly the same time, so that dying cells can The two communities
help to give birth to another, but cannot prevent the death of others by reducing
overcrowding, nor can cells being born either preserve or kill cells living in the
previous generation. 0 0 0 0 0 0 I 1 1 0
1 2 3 2 1 0 2 •1 2 0
1.2.2 Examples •1 •2 •1 •2
1 1 and 0 3 3 0
alternation
As a first example, consider the community
1 2 3 2 1 0 2 •1 2 0
0 0 0 0 0 0 1 1 I 0
variety not obvious what changes will happen as generations progress. Some very small initial I* Simulation of Conway's game of Life on a bounded grid
I* Version 1 • I
*'
configurations will grow into large communities; others will slowly die out; many will
reach a state where they do not change, or where they go through a repeating pattern #include II general.h II
every few generations. #include 11 lifedef.h"
#include 11 calls.h 11
I* common include files and definitions
I* Life's defines and typedefs *'*'
I* Life's function declarations
popularity Not long after its invention, M ARTIN GARDNER discussed the Life game in his column
in Scientific American, and, from that time on, it has fascinated many people, so that for void main (void)
*'
several years there was even a quarterly newsletter devoted to related topics. It makes {
an ideal display for microcomputers. int row, col;
Our first goal, of course, is to write a program that will show how an initial com-
*'*'
GridJype map; I* current generation
munity will change from generation to generation. GridJype newmap; I* next generation
#define MAXROW 50
#define MAXCOL 80
I* maximum number of rows
I* maximum number of columns
typedef enum status-tag { DEAD, ALIVE } Status.type;
typedef Status.type Grid_type [MAXROW] [MAXCOL] ;
*'*' (g )
I
I
••• •• •• •
(h)ma (i)- • •••
••
functions and calls.h contains the function prototypes for the Life program:
I
I
' Effl=a
void CopyMap ( Grid.type, GridJype); (j) (k) (I)
• ••••
BooleanJype Enquire (void);
void lnitialize (Grid.type); ••••• •• ••
int NeighborCount(int, int, Grid.type); I
I
• ••• ••
void WriteMap (Grid.type) ; I
'
We create a new calls.h file with the function prototypes for each program we write. In Figure 1.1. Life configurations
this program we still must write the functions Initialize and WriteMap that will do the input
and output, the function Enquire that will determine whether or not to go on to the next
generation, the function CopyMap that will copy the updated grid, newmap, into map,
and the function NeighborCount that will count the number of cells neighboring the one
1.3 PROGRAMMING STYLE
in row ,col that are occupied in the array map. The program is entirely straightforward. Before we tum to writing the functions for the Life game, let us pause to consider several
First, we read in the initial situation to establish the first configuration of occupied cells. principles that we should be careful to employ in programming.
Then we commence a loop that makes one pass for each generation. Within this loop
we first have a nested pair of loops on row and col that will mn over all entries in the 1.3.1 Names
array map. The body of these nested loops consists of the one special statement In the story of creation (Genesis 2: 19), God brought all the animals to ADAM to see
what names he would give them. Accord ing to an old Jewish tradition, it was only when
switch { . . . } ,
ADAM had named an animal that it sprang to life. This story brings an important moral
to computer programming: Even if data and algorithms exist before, it is only when they
which is a multiway selection statement. In the present application the function Neigh·
are given meaningful names that their places in the program can be properly recognized
borCount will return one of the values 0, 1, ... , 8, and for each of these cases we can
and appreciated, that they first acqu ire a life of their own.
take a separate action, or, as in our pro1,rram, some of the cases may lead to the same
purpose of For a program to work properly it is of the utmost importance to know exactly what
action. You should check that the action prescribed in each case corresponds correctly to careful naming each variable represents, and to know exactly what each function does. Documentation
the rules 2, 3, 4, and 5 of Section 1.2. l. Finally, after us ing the nested loops and switch
CHAPTER 1 SECTION 1 .3 Programming Style 11
O Programming Principles
explaining the variables and functions should therefore always be included. The names 6. Avoid choosing names th at are close to each other in spelling or otherwise easy to
of variables and functions should be chosen wit,h care so as to identify thei r meanings confuse.
clearly and succinctly. Finding good names is not always an easy task, but is important 7. Be careful in the use of the letter "I" (small ell}, ''O" (capi tal oh) and " O" (zero).
enough t.o be singled out as our fi rst programming precept Within words or numbers these usua ll y can be recognized from the context, and
cause nu p1 olJlt:111, lJu t '·I" am.I ''O " should 11ever l>e used alu11e as names. Consider
the examples
C requi res declarations for local variables and arguments. Constants used in a program 1.3.2 Documentation and Format
can be given names through enumerators or symbolic constants.
The careful choice of names can go a long way in clarifying a program and in Most students initially regard documentation as a chore that must be endured, after a
helping to avoid misprints and common errors. Some guidelines are program is finished, to ensure that the marker and instructor can read it, so that no credi t
will be lost for obscu rity. The author of a small program indeed can keep all the details
guidelines I. Give special care to the choice of names for functions, constants, symbolic constants, in his head, and so needs documentation only to explain the program to someone else.
and all global variables and types used in different parts of the program. These the purpose of With large programs (and with small ones after some months have elapsed), it becomes
names should be meaningful and should suggest clearly the purpose of the function, doc11me111arion impossible to remember how every detai l relates to every other, and therefore to write
variable, and the like. large programs, it is essential that appropriate documentation be prepared along with each
2. Keep the names simple for variables used only briefly and locally. A single letter small part of the program. A good habit is to prepare documentation as the program
is often a good choice for the variable controlling a for loop, but would be a poor is being written, and an even better one, as we shall see later, is to prepare part of the
choice for a function or for a variable used three or four times in widely separated documentation before starting to wri te the program.
parts of the program. Not all documentation is appropriate. Almost as common as programs with little
documentation or onl y cryptic comments are programs with verbose documentation that
3. Use common prefixes or suffixes to associate names of the same general category.
adds little to understanding the program. Hence our second programming precept:
The files used in a program, for example, might be called
5. Avoid comments that parrot what the code does, such as any large organization the top management cannot worry about every detail of every
activity; the top managers must concentrate on general goa ls and problems and delegate
count++; I* Increase counter by 1. *I specific responsibilities to their subordinates. Again, middle-level managers cannot do
everything: They must subdivide the work and send it to other people. So it is with
or that are meaningless jargu11, sud1 as subdivision computer programming. Even when a project is sma ll enough that one person can take
it from start to finish, it is most imponant to divide the work, starting with an overall
I* horse string length into correctitude *' understanding of the problem, dividing it into subproblems, and attacking each of these
in tum without worrying about the others.
(This example was taken directly from a systems program.) Let us restate this principle with a classic proverb:
6. Explain any statement that employs a trick or whose meaning is unclear. Better
still, avoid such statement.s. Programming Precept
7. The code itself should explain how the program works. The documentation should Don't lose sight of the forest for its trees.
explain why it works and what it does.
8. Whenever a program is modified, be sure that the documentation is correspondingly top-down refinement This principle, called top-down refinement, is the real key to writing large programs
modified. that work. The principle implies the postponement of detailed consideration, but not the
postponement of precision and rigor. It does not mean that the main program becomes
format Spaces, blank lines, and indentation in a program are an important fonn of documentation.
some vague entity whose task can hardly be described. On the contrary, the main program
TI1ey make the program easy to read, allow you to te ll at a glance which parts of
will send almost all the work out to various functions, and as we write the main program
the program relate to each other, where the major breaks occur, and precisely which
specifications (which we should do first), we decide exac1ly how the work will be divided among them.
statements are contained in each loop or each alternative of a conditional statement.
Then, as we later work on a particular function, we shall know before starting exactly
There are many systems (some automated) for indentation and spacing, all with the goal
what it is expected to do.
of making it easier to detennine the structure of the program.
It is often not easy to decide exactly how to divide the work into functions, and
A C beautifier, usually named cb, is a system utility that reads a C program moving
sometimes a decision once made must later be modified. Even so, two guidelines can
the text between lines and adjusting the indentation so as to improve the appearance of
help in deciding how to divide the work:
the program and make its structure more obvious. The utility cb was originally available
on UN1x. If a C beautifier is available on your system, you might experiment with it to
see if it helps the appearance of your programs. Programming Precept
<:0nsistency Because of t.he importance of good format for programs, you should settle on some
Each function should do only one task, but do it well.
reasonable rules for spacing and indentation and use your rules consistently in all the
programs you write. Consistency is essential if tJ1e system is to be useful in reading
programs. Many professional programming groups decide on a unifom1 system and That is, we should be able to describe the purpose of a function succinctly. If you find
insist that all the programs they write confonn. Some classes or student programming yourself writing a long paragraph to specify the task of a function, then either you are
teams do likewise. In this way, it becomes much easier for one programmer to read and givi ng too much detail (that is, you are writing the function before it is time to do so)
understand the work of another. or you should rethink the division of work. The function itself will undoubtedly contain
many details, but they should not appear until the next stage of refinement.
Programming Precept
Programming Precept
The reading time for programs is much more than the writing time.
Make reading easy to do. Each function should hide something.
A middle-level manager in a large company does not pass on everything he receives from
his departments to his superior; he summarizes, collates, and weeds out the infonnation,
1.3.3 Refinement and Modularity handles many requests himself, and sends on only what is needed at the upper levels.
problem solving Computers do not solve problems; people do. Usually the most important part of the Similarly, he does not transmit everything he learns from higher management to his
process is dividing the problem into smaller problems that can be understood in more subordinates. He transmits to each person only what he needs to do his job. The
detail. If these are still too difficult, then they are subdivided again, and so on. Jn functions we write should do likewise.
14 Programming Principles CHAPTER 1 SECTION 1.3 Programming Style 15
One of the most important parts of the refinement process is deciding exactly what needs to produce more than one result, then some of the parameters should be passed by
the task of each function is, specifying precisely what its input will be and what result it reference and the function should be of type void.
will produce. Errors in these specifications are among the most frequent program bugs before and after The second way in which the specifications of a function should be made precise is
conditions in describing the action of the funct ion. The documentation should include a statement
and are among the hardest to find . First, the data used in the function must be precisely
s pecified . These data are of five kinds: of exactly what conditions the funct ion expects to find when it is started. These are
called the preconditions for the fu nction. The documentation should also indicate what
changes the function will make, and therefore what cond itions will hold after the function
parame1ers • Input parameters are used by the function but are not changed by the function. In finishes. These are called the postconditions for the function.
C, input parameters are usually passed by value; that is, a copy of the parameter is While all these principles of top-down design may seem almost self-evident, the
passed to the function. (Exception: Arrays are always passed by reference; that is, only way to learn them thoroughly is by practice. Hence throughout this book we shall
the address of the array is passed to the function.) be careful to apply them to the large programs that we write, and in a moment it wi ll be
• Output parameters contain the resulls of the calculations from the function. In C, appropriate to return to our first example proj ect.
output parameters must be passed by reference.
• Inout parameters are used for both input and output; the initial value of the pa- Exercises E l. Rewrite the follow ing function so that it accomplishes the same resul t in a less
rameter is used and then modified by the function. In C, inout parameters must be 1.3 tricky way.
passed by reference.
void DoesSomething ( int * first, int *Second )
variables • Local variables are declared in the function and exist only while the function is {
being executed. T hey arc not initialized before the function begins and are discarded *first = * Second - *first;
when the function ends. *Second = * Second - * first;
• Global variables are used in the function but not defined in the function. It can * first = * Second + *first;
be quite dangerous to use global variables in a function , since after the function }
is written its author may forget exactly what global variables were used and how.
E2. Determine what each of the fo llowing funct ions does. Rewrite each function with
If the main program is later changed, then the function may mysteriously begin to
mys1ery f11nc1ions meaningful variable names, with better format, and without unnecessary variables
misbehave. If a function alters the value of a global variable it is said to cause a side
and statements.
side effects effect. Side effect5 are even more dangerous than using global variables as input
to the function because side effects may alter the performance of other functions, a. #define MAXINT 100
thereby misdirecting the programmer's debugging efforts to a part of the program int Calculate (int apple, int orange)
that is already correct. { int peach, lemon;
peach = O; lemon = O; if (apple < orange) {
peach = orange; } else if (orange<= apple) {
peach = apple; } else { peach = MAXINT; lemon = MAXINT;
Programming PreceJt " ., · } if (lemon ! = MAXINT) { return ( peach); } }
~~ ·ffi ·-~; m $- ·" : ,_ § ,,. *' · ~ X:!,
,, ® Keep you~ conmectioros simple, Avoid <9lobal variables whenever l)ossible. , b. double Figure(double vector1 [ J, int n)
{ int loop1; double loop2; double loop3; int loop4;
loop 1 = O; loop2 = vector1 [ loop 1J; loop3 = 0.0;
loop4 = loop1; for ( loop4 = O; loop4 < n; loop4 = loop4 + 1)
4
i» i " ': " ' i;, .
1
;·. ·;~fi @. ·<~;: ,~, ~-: ~J ·m
'" ~ rograminlhg
~ ::-- "{, 'Pri cef't " ~~' ~' "'·1' { loop1 = loop1 + 1; loop2 = vector1 [ loop1 - 1];
loop3 = loop2 + loop3; } loop2 = loop1;
1& ·
,,,. t '' .,, , ~,. Never cause side effe<::ts. loop2 = loop3/loop2; return ( loop2); }
If you must use•global variables as input;- document them thoroughly.'
' .~ §: ~;'.i:· '. -~ ~ ·~· & ~ ·~ -~ ,;~- ' ';; . ,:;.: c. void Question (int * a17, int *Stuff)
{ int another, yetanother, stillonemore;
In the case of a function that returns a non-void value, the definition of side effect i s
another= yetanother; stillonemore = * a17;
further expanded to include changes made to parameters as well as global variables.
yetanother = * Stuff; another = stillonemore; * a17 = yetanother;
A function typically returns one result. When a function does not return a result the
stillonemore = yetanother;
function should be of type void. When the function returns a result then the function
* Stuff = another; another = yetanother; yetanother = * Stuff; }
should have a type qualifier that indicates the type of result being returned. If a function
16 Programming Principles CHAPTER 1 SECTION 1 .4 Coding, Testing, and Further Re1inement 17
d. int Mystery ( int apple, int orange, int peach) y = (2*Y + xi ( y*y) ) /3
{ if (apple> orange) if (apple> peach) if
(peach> orange) return (peach); else if (apple < orange) until fabs ( Y*Y*Y - x) <= 0.00001.
return(apple); else return(orange); else return(apple); else c. Which of these tasks i s easier?
if (peach > apple) ii (peach > orange) return (orange); else
ES. The mean of a sequence of real numbers is cheir sum divided by the count of
return(peach); else return(apple); }
statistics numbers in the sequence. The (population) variance of the sequence is the mean
of the squares of all numbers in the sequence, minus the square of the mean of the
E3. The following statement is designed to check the relative sizes of three integers,
numbers in the sequence. The standard deviation i s the square root of the variance.
nested it statements which you may assume to be different from each other:
Write a well -structured C function to ca lculate the standard deviation of a sequence
if (x < z) if (x < y) if (y < z) c = 1; else c = 2; else of n numbers, where n is a constant and the numbers are in an array indexed from
if (y < z) c = 3; else c = 4; else if (x < y) O to n - I , where n i s a parameter to the function. Write, then use, subsidiary
if (x<z) c=S; elsec=6; else if (y<z) C=7; else functions 10 calculate the mean and variance.
if (z < x) if (z < y) c = 8; else c = 9; else c = 1O; plotting E6. Design a program that will plot a given set of points on a graph. T he input to the
program wi ll be a text file, each line of which contains two numbers that are the
a. Rewrite this statement in a form that is easier to read. x and y coordinates of a point to be plotted. The program will use a routine to
b. Since there are only six possible orderings for the three integers, only six of plot one such pair of coordinates. The details of the routine involve the specific
the ten cases can actually occur. Find those that can never occur, and eliminate method of plotting and cannot be written since they depend on the requirements
the redundant checks. of the plotting equipment, which we do not know. Before plotting the points the
c. Write a simpler, shorter statement that accomplishes the same result. program needs to know the maximum and minimum values of x and y that appear
in its input file. The program should therefore use another routine Bounds that
E4. The following C function calculates the cube root of a real number (by the N EWTON
wi ll read the whole file and determine these four maxima and minima. Afterward,
cube roots approximation), using the f'.lct that, if y is one approximation to the cube root of
another routine is used to draw and label the axes; then the file can be reset and the
x, then
indiv idual points plotted.
2y + x/y2
z = - - -- a. Write the main program , not including the rout ines.
3
b. Write the function Bounds.
is a closer approximation. c. Write the header lines for the remaining functions together with appropriate
documentation showing their purposes and their requirements.
double Fen (double stuff)
{ double april, tim, tiny, shadow, tom, tam, square;
BooleanJype flag;
tim = stuff; tam = stuff; tiny = 0.00001;
if (stuff!= O) do {shadow= tim + tim;
1.4 CODING , TESTING, AND FURTHER REFINEMENT
square = tim * tim; The three processes in the title above go hand-in-hand and must be done together. Yet it is
tom= (shadow + stuff/square); important to keep them separate in our thinking, since each requires its own approach and
april = tom/3; method. Coding, of course, is the process of writing an algorithm in the correct syntax
if (april * april * april - tam> -tiny) (grammar) of a computer language like C, and testing is the process of running the
if (april * april * april - tam < tiny) flag = TRUE; program on sample data chosen to find errors if they are present. For further refinement,
else flag = FALSE; else flag = FALSE; we tum to the functions not yet written and repeat these steps.
if (flag == FALSE) tim = april; else tim = tam; }
while (flag == FALSE) ;
1.4.1 Stubs
if (stuff== 0) return (stuff); else return(april); }
After coding the main program, most programmers will wish to complete the writing and
a. Rewrite this function with meaningful variable names, without the extra vari- coding of the functions as soon as possible, to see i f the whole project will work. For a
ables that contribute nothing to the understanding, with a better layout, and early debugging project as small as the Life game, this approach may work, but for larger projects, writing
without the redundant and useless statements. and testing and coding all the functions w ill be such a large job that, by the time it is complete,
b. Write a function for calculating the cube root of x directly from the mathemat- many of the details of the main program and functions that were written early wi ll have
ical formula, by starting with the assignment y = x and then repeating been forgotten. l n fact, different people may be writ ing different functions, and some
18 Programming Principles CHAPTER 1 SEC T I ON 1 . 4 Coding, Testing, and Further Refinement 19
of those who started the project may have left it before all functions are written. It is variables for the lower and upper limits of the loops, and make sure that they remain
much easier to understand and debug a progra111 when it is fresh in your mind. Hence, within range. Since the loops will incorrectly consider that the cell in position row, col
for larger projects, it is much more efficient to debug and test each function as soon as is a neighbor of itself, we must make a correction after completing the loops.
it is written than it is to wait until the project has been completely coded.
Even for smaller projects, there are good reasons for de huggi ng functions one at a
time. We might, for example, be unsure of some point of C syntax that will appear in
I* NeighborCount: count neighbors of row.col. * I
several places through the program. If we can compile each function separately, then
int NeighborCount (int row, int col, GridJype map)
we shall quickly learn to avoid errors in syntax in later functions. As a second example, {
suppose that we have decided that the major steps of the program should be done in a
cc1tain order. If we test the main program as soon as it is written, then we may find
that sometimes the major steps are done in the wrong order, and we can quickly correct
int i, j;
int rlow, rh igh;
I* loop indices for row, column
I* limits for row loop *'
*I
the problem, doing so more easily than if we waited until the major steps were perhaps
int claw, chigh;
int count;
I* limits for column loop
*'
obscured by the many details contained in each of them.
To compile the program correctly, there must be something in the place of each
I* Count neighbors.
*'
if ( row<= 0)
stubs function that is used, and hence we must put in short, dummy functions, called stubs.
The simplest stubs are those that do nothing at all:
else
rlow = O;
I* Determine the boundaries.
*'
I* Initialize: initialize grid map. * I rlow = row - 1 ;
void Initialize ( Grid.type map) if (row>= MAXROW - 1)
{ rhigh = MAXROW- 1;
} else
I* WriteMap: write grid map.
void WriteMap (GridJype map)
*' rhigh =row + 1;
if (col <= 0)
{ clow = O;
} else
claw = col - 1 ;
f* NeighborCount: count neighbors of row.col. * I if ( col >= MAXCOL -1)
int NeighborCount(int row, int col, GridJype map) chigh = MAXCOL - 1 ;
{
else
return 1; chigh = col + 1;
}
count = O;
Even with these stubs we can at least compile the program and make sure that the for (i = rlow; i <= rhigh; i+ +)
f* Nested loops count neighbors.
*'
declarations of types and variables are syntactically correct. Normally, however, each for (j = claw; j <= chigh; j ++)
stub should print a message stating that the function was invoked. When we execute the if ( map [i] [j) == ALIVE)
program, we find thal some variables are used without initialization, and hence, to avoid count++;
these errors, we can add code to function Initia lize. Hence the stub can slowly grow and if (map[row] [col] == ALIVE)
be refined into the final form of the function. For a small project like the Life game, we count - - ;
can simply write each funcLion in tum, substitute it for its stub, and observe the effect return count;
on program execution. }
The output must be carefully organized and formatted, with considerable thought to what The function CopyMap copies newmap into map.
should or should not be printed, and with provision of various alternatives to suit differing
circumstances.
I* CopyMap: copy newmap into map. • I
Initialization
void CopyMap(Grid. type map, Grid.type newmap)
The task that function Initialize must accomplish is to set the map to its initial configu- {
ration. To initialize the map, we could consider each possible coordinate pair separately int row, col;
input method and request the user to indicate whether the cell is to be occupied or not. This method
for (row= O; row< MAXROW; row++)
would require the user to type in MAXROW • MAXCOL = 50 * 80 = 4000 entries, which
for (col= O; col< MAXCOL; col++ )
is prohibitive. Hence, instead, we input only those coordinate pairs corresponding to
map [row) [col) = newmap [row] [col];
initially occupied cells.
}
'* Initialize: initialize grid map.
void lnitialize(Grid_type map)
*' response from user Finally comes the function Enquire that determines whether the user wishes to go on to
{ calculate the next generation. The task of Enquire i s to ask the user to respond yes or
int row, col; '* coordinates of a cell
printf ( "This program is a simulation of the game of Life. \n"); *'
no; to make the program more tolerant of mistakes in input, this request is placed in a
loop, and Enquire wai ts for a valid response.
for (row= O; row< MAXROW; row ++ )
for (col= O; col < MAXCOL; col ++ )
I* Enquire: TRUE if the user wants to continue execution. * I
map [row] [col] = DEAD;
Boolean.type Enquire ( void)
printf ( "On each line give a pair of coordinates for a living cell. \n 11 ) ; {
printf("Terminate the list with the special pair -1 -1 \n");
int c;
scant ( "%d %d ", &:row, &:col);
while (row!= -1 II col!= -1) { do {
if (row>= 0 &&: row< MAXROW &&: col >= 0 &:& col < MAXCOL) print! ( "Continue (y,n)?\n");
while ( (c = getchar ( ) ) == '\n')
else
map [row] [col) = ALIVE;
; I* Ignore the new line character.
} while (c !='y'&:&c !=' Y'&&c !='n'&&:c ! = ' N');
*'
printf ( "Values are not within range. \n");
scanf("%d %d", &row, &col); if (c == 'y' II c == 'Y')
} retu rn TRUE;
} else
return FALSE;
output For the output function WriteMap we adopt the simple method of writing out the entire }
array at each generation, with occupied cells denoted by * and empty cells by dashes.
Sometimes two functions can be used to check each other. The easiest way, for example,
Programming Precept
to check functions Initial ize and WriteMap is to use a driver whose declarations are those
of the main program, and whose action part is The quality of test data is more important than its quantity.
Initialize (map) ; Many sample runs that do the same calculations in the same cases provide no more
WriteMap(map); effective a tesl than one run.
Both functions can be tested by running this driver and making sure that the configuration
printed is the same as that given as input. Programming Precept
Program testing can be used to show the presence of bugs,
1.4.5 Program Tracing but never their absence.
After the functions have been tested, it is time to check out the complete program. One It is poss ible that other cases remain that have never been tested even after many sample
of the most. effective ways to uncover hidden defects is called a structured walkthro11gh. runs. For any program of substantial complexity, it is impossible to perform exhaustive
In this the programmer shows the completed program to another programmer or a small tests, yet the careful choice of test data can provide substantial confidence in the program.
group disc11ssio11 group of programmers and explains exactly what happens, beginning with an explanation Everyone, for example, has great confidence that the typical computer can add two
of the main program followed by the functions, one by one. Stmctured walkthroughs floating-point numbers correctly, but this confidence is certainly not based on testing
are helpful for three reasons. First, programmers who are not familiar with the actual the computer by having it add all possible floating-point numbers and checking the
code can often spot bugs or conceptual errors that the original programmer overlooked. results. If a double-precision floating-point number takes 64 bits, then there are 2 128
Second. the questions that other people ask can help you to clarify your o wn thinking distinct pairs of numbers that could be added. This number is astronom ically large: All
and discover your own mistakes. Third, the structured walkthrough often suggests tests computers manufactured to date have performed altogether but a tiny fraction of this
that prove useful in later stages of software production. number of additions. Our confidence that computers add correctly is based on tests
It is unusual for a large program to run correctly the first time it is executed as of each component separately, that is, by checking that each of the 64 digits is added
a whole, and if it does not, it may not be easy to determine exactly where the errors correctly, and that carrying from one place to another is done correctly.
are. On many systems sophisticated trace tools are available to keep track of function 1es1i11g methods There are at least three general philosophies that are used in the choice of test data.
calls, changes of variables, and so on. A simple and effective debugging tool, however,
!4 Programming Principles CHAPTER 1 S ECTION 1 . 4 Coding, Testing, and Further Relinement 25
c. A function that returns the least common multiple of its two parameters, which
must be positive integers. (The least common multiple is the smallest integer
••
• •• •• ••• •• •• •• •••
that is a multiple of both parameters. Examples: The least common multiple
of 4 and 6 is 12, of 3 and 9 is 9, and of 5 and 7 is 35.) •• •• •• ••
d. A function that sorts three integers, g iven as its parameters, into ascending
R Pentomino
••
•• ••
•• ••
•• ••
•• ••
•• ••
•• •• ••
order.
e. A function that sorts an array a of integers indexed from O to a variable n - 1 ••
•• ••
•• ••
• ••
•• ••
•• •• •• ••••
••
•• •• •• ••• •• •• ••••
into ascending order, where a and n arc both parameters.
E2. Find suitable glass-box test data for each of the following: • •••
•• •• •• •• •• ••
a. The statement
•• •• •• ••
•• ••
•• ••• ••• ••
•• ••
•• •• •
• •• •• • •• ••••
if (a < b) if (c > d) x = 1; else if Cc== d) x = 2 ;
••
•• ••• ••
•• •• ••
•• ••••
else x = 3; else if (a == b) x = 4; else i' (c == d) x = 5; Cheshire Cat
else x = 6; Virus
Programming Principles
Two books that contain many helpful hints on prog ramming style and correctness, as
well as examples of good and bad practices, are
BRIAN W. K F.RNJGHAN and P. J. P 1.A1:0ER. The Elements of Programming Style, second
edition, M cGraw- Hill, New York, 1978, 168 pages.
DEN:sre VAN TASSEL, Program Style, Design, Ehiciency, Debugging . and Testing, second
edition, Prentice Hall, Englewood Cliffs, N.J., 1978. 323 pages.
EDSGER W. DIJKSTRA pioneered the movement known as structured programming, which
insists on taking a carefully organized top-down approach to the design and writing of
programs, whe n in March 1968 he caused some consternation by publishing a lette r
e ntitled "Go 'Io Statement Considered Harmful" in the Communications of the ACM
(vol. 11, pages 147- 148). DIJKSTRA has since published several papers and books that
are most instructive in programming method. One book of s pecial interest is
E. W. DuKSTRA, A Discipline of Programming, Prentice Hall, Englewood Cli ffs. NJ.•
1976, 217 pages.
S ECTION 2. 1 Program Maintenance 31
CHA P T E R 2
Software engineering is the discip line within computer science concerned with tech-
niques needed for the production and maintenance of large software systems. Our goal
in introducing some of these techniques is to demonstrate their importance in problems
of practical size. A lthough much of the discussion in this c hapter is motivated by the
Introduction I .ifo game and applied specifically to its program, the discussion is always intende.d to
illustrate more general methods that can be applied to a much broader range of problems
of practical importance.
to Software I
2.1 PROGRAM MAINTENANCE
Small programs wriuen as exercises or demonstrations are usually run a few times and
the n discarded. but the disposition of large practical programs is quite different. A
program of pract ical value w ill be run many times, usuall y by many different people,
Enginee
and its writing and debugging mark only the beginning of its use. They also mark only
the beginning of the work required to make and keep the program useful. It is necessary
to review and analyze the program to ensure that it meets the requirements specified for
it, adapt it to changing e nvironments, and modify it to make it better meet the needs of
its users.
Let us illustrate these activities by reconsidering the program for the Life game
written and tested in Chapter 1.
This chapter continues to expound the principles of good program design, 1. Review of the Life Program
with special emphasis on techniques required for the production of large
software systems. These techniques include problem specification, algo- If you have run the Life program on a s mall computer or on a busy time-sharing system ,
rithm development, ver(fication, and analysis, as well as program testing problems then you will likely have found two major problems. First, the method for input of
the initial configuration is poor. It is unnatural for a person to calculate and type in
and maintenance. These general principles are introduced in the context
the numeri cal coordinates of each living cell. The form of input should instead reflect
of developing a second program for the Life game, one based on more
the same visual imagery as the way the map is printed. Second, you may have found
sophisticated methods than those of the last chapter. the program's speed somewhat disappointing. There can be a noticeable pause between
printing one generation a nd starti ng to print the next.
Our goal is to improve the program so that it will run really efficiently on a micro-
computer. T he problem of improving the fonn of input is addressed as an exercise; the
2.1 Program Maintenance 31 2.4.3 Initialization 47 text discusses the problem of improving the speed.
2.2 Lists in C 35 2.5 Program Analysis and 2. Analysis of the Life Program
Comparison 48
2.3 Algorithm Development: A Second We must first find out where the program is spending most of its computation time. If
Version of Life 37 2.6 Conclusions and Preview 51 we examine the program, we can first note that the trouble cannot be in the function
2.3.1 The Main Program 37 2.6.1 The Game of Life 51 Initialize, since this is done only once, before the main loop is started. Within the loop
2.3.2 Refinement: Development of the 2.6.2 Program Design 53
operation counts that counts generatio ns, we have a pair of nested loops that, together, will iterate
Subprograms 39 2.6.3 The C Language 55
2.3.3 Coding the Functions 40
Pointers and Pitfalls 57 MAXROW x MAXCOL = 50 x 80 = 4000
2.4 Verification of Algorithms 44 Review Questions 57 times. Hence program lines within these loops will contribute substantially to the time
2.4.1 Proving the Program 44 used.
2.4.2 Invariants and Assertions 46 References for Further Study 58 The first thing done within the loops is to invoke the function NeighborCount. The
nested loops function itself includes a pair of nested loops (note that we are now nested to a total
depth of 5), whic h usually do their inner statement 9 times. The func tion also does 7
statements outside the loops, for a to tal (usually) of 16.
10
32 Introduction to Software Engineering C HAP TER 2 SECTION 2 . 1 Program Maintenance 33
Within the nested loops of the main program there are, along with the call to the occupied. If we can prevent or substantially reduce such useless calculation, we shall
function, only the comparison to find which c3:se to do and the appropriate assignment obtain a much betler program.
statement, that. is, there arc only 2 statements additional to the 16 in the function. Out- As a fi rst approach, let us consider trying to limit the calculations to cells in a
side of the nested loops there is the function call CopyMap ( map, newmap), which, in limited area around those that are occupied. If th is occupied area (which we would have
copying 4000 entries, is about equivalent to 1 more stareme.nt within the loops. There to defi ne precisely) is roughly rec1angula r, then we c~ n implement this scheme easil y by
is also a call to the function WriteMap, some variation of which is needed in any case replacing the limits in the loops by ot her variables that wou ld bound the occupied area.
so that. the user can see what the program is doing. Our primary concern is with the But th is scheme wou ld be very inefficient if the occupied area were shaped like a large
computation, however, so let us not worry about the time that WriteMap may need. \Ve ri ng, or, indeed, if there were only two small occupied areas in opposite comers of a
thus see that for each generation, the computation involves about very large rectangle. To try to carry out this plan for occupied areas not at all rectangular
in shape would probably require us to do so many comparisons, as well as the loops, as
4000 x 19 = 76,000 to obviate any savi ng of time.
statements, of which about 4000 x J6 = 64,0CX) are done in the function. 4. A Fresh Start and a New Method
On a small microcomputer or a tiny share of a busy time-sharing system, each
statement can easily require 100 to 500 microseconds for execution, so the time to Let us back up for a moment. If we can now decide to keep an array to remember
calculate a generation may easily range as high as 40 seconds, a delay that most users the number of occupied neighbors of each cell, then the only counts in the array that
will find unacceptable. wi ll change from generation to generation will be those that correspond to immediate
Since by far the greatest amount of time is used in the function calculating the neighbors of cells th at die or are born.
number of occupied neighbors of a cell, we should concentrate our attention on doing We can substantially improve the ru nning time of our program if we convert the
this job more efficiently. Before starting to develop some ideas, however, let us pause arrays and f11nc1io11s function Ne ighborCount into an array and add appropriate statements to update the array
momentarily to pontificate: wh ile we are doing the changes from one generation to the nex t embodied in the s witch
statement, or, if we prefer (what is perhaps conceptually easier), while we are copying
newmap into map we can note where the births and deaths have occurred and at that
time update the array.
To emphasize that we are now using an array instead of the function NeighborCount,
we shall change the name and write numbernbrs for the array.
The method we have now developed still involves scanning at least once through
the fu ll array map at every generation, which like ly means much useless work. By
It takes much practice and experience to decide what 1s important and what may be being slightly more careful, we can avoid the need ever to look at unoccupied areas.
neglected in analyzing algorithms for efficiency, but it is a skill that you should carefully algorithm As a cell is born or dies it changes the value of nu mbernbrs for each of its immediate
develop to enable you 10 choose alternative methods or tu concentrate your programming developme111 neighbors. While making these changes we can note when we find a cell whose count
efforts where they will do the most good. becomes such that it will be born or die in the next generation. Thus we should set up
two lists that will contain the cells that, so to speak, are moribund or are expecting in
3. Problem-solving Alternatives the coming generation. In th is way, once we have finished making the changes of the
current generation and printing the map, we will have waiting for us complete lists of all
Once we know where a program is doing most of its work, we can begin to consider the births and death s to occur in the comi ng generation. It should now be clear th at we
allemative methods in the hope of improving its efficiency. ln the case of the Life game, reall y need two lists for births and two for deaths, one each for the changes being made
Jet us ask ourselves how we can reduce the amount of work needed to keep track of now (which are depleted as we proceed) and one list each (which are being added to)
the number of occupied neighbors of each Life cell. ls it necessary for us to calculate contai ning the changes for the next generation. When the changes on the current lists
use or array the number of neighbors of every cell at every generation? Clearly not, if we use some are complete, we pri nt the map, copy the coming lists to the current ones, and go on to
way (such as an array) t.o remember the number of neighbors, and if this number does the next generation.
not change from one generat;on to the next.. If you have spent some time experimenting
with the Life program , then you will ce1tainly have noticed that in many interesting
configurations, the number of occupied cells at any time is far below the total number of 5. Algorithm Outline
positions available. Out of 4000 positions, typically fewer than 100 are occupied. Our
program is spending much of its time laboriously calculating the obvious facts that cells Let us now summarize our decisions by writing down an informal outline of the program
isolated from the living cells indeed have no occupied neighbors and will not become we shall develop.
14 Introduction to Software Engineering CHAPTER 2 SEC T I ON 2 . 2 Lists in C 35
Initial configuration:
live d ie
Exercises El. Sometimes the user might wish to run the Life game on a grid smaller than 50 x 80.
2 3 4 5
2.1 Detem1ine how it is possible to make maxrow and maxcol into variables that the
user can set when the program is run. Try to make as few changes in the program
2 • (2, 2)
(2, 4)
(3, 3) as possible.
3 • • • (4, 2) E2. One idea for changing the program to save some of the if statements in the func-
4 • (4, 4 )
tion NeighborCount is to add two extra rows and columns to the arrays map and
newmap. by changing their dimensions to
5
2.2 LISTS IN C
l~___ ca_PY
__ ~J As we develop the revised Life program that follows the scheme we have devised in
Figure 2.1. Life using lists the last section, we must make some decisions about what variables we shall need and
the ways in which we shall implement the lists we have decided to use. (Lists will be
initialization Get the initial configuration of living cells and use it to calculate an array holding the
neighbor counts of all cells. Construct lists of the cell; that will become alive and covered in detail in Chapter 3.)
that will become dead in the first generation;
1. The Logical Structure of Lists
ma.in loop Repeat the following seeps as long as desired:
For each cell on the list of cells to become al ive: A list really has two distinct parts associated with it. First is a variable that gives the
Make the cell alive; number of items in the list. Second is an array that contains the items on the list. In
Update the neighbor c-ounts for each neighbor of the cell; most languages the programmer must carry the counter variable and the array separately
If a neighbor count reaches the appropriate value, then add the cell to the (and doing so is a frequent source of trouble for beginners). Sometimes tricks are used,
list of cells to be made alive or dead in the next generation; such as using entry O of the array as the counter.
For each cell on the list of cells to become dead:
Make the cell dead; Update the neighbor counts for each neighbor of the 2. Definition and Examples
cell; If a neighbor count reaches the appropriate value, then add the In our case, we may define a type called Lisltype with declarations such as the following:
cell to the list of cells to be made alive or dead in the next generntion;
The four lists that we wish to have are now variables of type LisUype, declared as usual: The arrow (->) operator is used to access a structure member through a pointer to
the structure. Thus, list->count dereferences the pointer list and accesses the structure
member called count.
LisUype die, live, ne).(tdie, neictlive;
Exercises El. Write a function that wi ll delete the last coordinate from a list (as defined in the
3. Hierarchical Structures: Data Abstraction 2.2 text) or will invoke a function Error if the list was empty.
The entries in our I ists will be coordinate pairs [x, y] , and there is no reason why we E2. Write functions that will copy one list to another list (as the structure type defi ned
should not think of these pairs as a single structure, by defining in the text). Use the foll owing methods: (a) copy the entire structures; (b) use a
loop to copy only the entries. Wh ich vers ion is easier to write? Which vers ion will
usually run faster, and why?
typedef struct coordJag {
int row; f* x *'
int col; I* y *I
} Coord.type;
2.3 ALGORITHM DEVELOPMENT: A SECOND VERSION OF LIFE
Tf we then define Entry _type in terms of Coord_type, After deciding on the basic method and the overall outline of the data structures needed
for solving a problem, it is time to commence the process of algorithm development,
typedef Coord.type Entry .type; beginning with an overall outline and slowly introducing refinements until all the details
are specified and a program is formulated in a computer language.
we shall have put these coordinates into our lists, as we wish to do.
Note that we now have an example of a structure (coord.tag) contained as entries 2.3.1 The Main Program
hierarchical in arrays that are members of another structure (lisUag). Such structures are called
structures hierarchical. By putting structures within structures, structures in arrays, and arrays In the case of the Life game. we can now combine our use of structures as the data
in structures, we can build up complicated data structures that precisely describe the structures for lists with the outline of the method given in part 5 of Section 2.1, thereby
relationships in the data processed by a program. translating the outline into a main program written in C. With few exceptions, the dec-
larations of constants, types, and variables follow the discussion in Sections 2.1 and 2.2
When we work with structures, however, we should never think of them as having
along with the corresponding dec larations for the first vers ion of the Life game. The
top-down. design of such a complicated form. Instead, we should use top-down design for data structures
data structures include file liledef. h contains:
as well as for algorithms. When we process the large, outer structures, we need not be
concerned about the exact nature of each component within the structure. When we write #define MAXROW 50 I* maximum number of rows allowed *I
algorithms to manipulate the innermost components, we should treat them only in terms #define MAXCOL 80 I* maximum number of columns allowed * I
of their simple form and not be concerned as to whether they may later be embedded in #define MAX LIST 100 I* maximum number of elements in a list *I
larger structures or arrays. We can thus use structures to accomplish inf ormation hiding,
whereby we can design the upper levels both of algorithms and data structures without
worrying about the detai ls that will be specified later on lower levels of refinement.
typedef enu m status.tag { DEAD, ALIVE } Status.type; I* status of cell
*'
As one further example of processing hierarchical structures, we can write a short
function that will add an entry coordinate to the end of a list called list.
typedef Status.type G rid.type [MAXROW] [MAXCOL]; I* grid definition
typedef int GridcounUype [ MAXROW] [MAXCOL] ; *'
typedef struct coord .tag {
int row;
void Add (List.type *list, Coord.type coordinate) int col;
adding to a list { } Coard.type;
if (list- >count >= MAXUST)
Error ( "list overflow" ) ; typedef struct list.tag {
else int count;
list- >entry [list->count+ + J = coordinate; Coord.type entry [MAXLIST] ;
} } List.type;
18 Introduction to Software Engineering CHAPTER 2 SEC T I ON 2 . 3 Algorithm Development: A Second Version of Lile 39
The include file calls.h contains the function prototypes: array numbernbrs. As part of the same functions, when the neighbor count reaches an
appropriate value, a cell is added to the list nextlive or nextdie to indicate that it will
void Copy ( LisUype *, LisUype *) ; be born or die in the coming generation. Finally, we must copy the lists for the coming
int Enquire (void);
generation into the current ones.
void lnitialize(LisUype *, LisUype *, LisUype *, LisUype *,
GridJype, GridcounUype);
void WriteMap(Grid_type) ; 2.3.2 Refinement: Development of the Subprograms
void ReadMap(LisUype *, GridJype); After the solution to a problem has been outlined, it is time to turn to the various parts
void Vivify ( LisUype *, Grid_type, GridcounUype); of the outline, to include more details and thereby specify the solu tion exactly. While
void Kill (LisUype *, GridJype, GridcounUype); making these refinements, however, the programmer often discovers that the task of
void AddNeighbors(LisUype *, LisUype *, LisUype *, each subprogram was not specified as carefully as necessary, that the interface between
GridcounUype, GridJype); specifications and different subprograms must be reworked and spelled out in more detail, so that the
void SubtractNeighbors (LisUype *, LisUype *, LisUype *, problem solving different subprograms accomplish all necessary tasks, and so that they do so without
GridcounUype, GridJ ype); duplication or contradictory requirements. In a real sense, therefore, the process of
void Add(LisUype *, CoordJype) ; refinemen t requires going tack to the problem-solving phase to find the best way to split
The main program then is: the required tasks among the various subprograms. Ideally, this process of refinement
and specification should be completed before any coding is done.
I* Simulation of Conway's game of Life on a bounded gnd * I Let us illustrate this activity by work ing through the requirements for the various
I* Version 2. * I subprograms for the Life game.
#include "general.h"
#include II lifedef.h II I* Life's defines and typedefs 1. The Task for AddNeighbors
#include 11 calls.h II I* Life's function declarations Much of the work of our program will be done in the functions AddNeighbors and
Life2, main program void main ( void) SubtractNeighbors. We shall develop the first of these, leaving the second as an exercise.
{ The function AddNeighbors will go through the list live, and for each entry will find its
GridJype map;
GridcounUype numbernbrs;
I* current generation
I* number of neighbors *'*' immediate neighbors (as done in the original function NeighborCount), will increase the
count in numbernbrs for each of these, and must put some of these on the lists nextlive
LisUype live, nextlive; and nextdie. To determine which, let us denote by n the updated count for one of the
List.type die, nextdie; neighbors and consider cases.
Initialize ( &live, &die, &nextlive, &nextdie, map, numbernbrs);
cases for I. It is impossible that n = 0, since we have just increased n by I.
WriteMap (map); AddNeighbors
do { 2. If n = I or n = 2, then the cell is already dead and it should remain dead in the
Vivify( &live, map, numbernbrs); next generation. We need do nothing.
Kill ( &die, map, numbernbrs) ; 3. If n = 3, then a previously live cell still lives; a previously dead cell must be added
WriteMap (map) ; to the list nextlive.
AddNeighbors( &live, &nextlive, &nextdie, numbernbrs, map); 4. If n = 4, then a previously live cell dies; add it to nextdie. If the cell is dead, it
SubtractNeighbors ( &die, &nextlive, &nextdie, numbernbrs, map); remains so.
Copy ( &live, &nextlive); 5. If n > 4, then the cell is already dead (or is already on list nextdie) and stays there.
Copy( &die, &nextdie);
} while ( Enquire ( ) ) ; 2. Problems
}
One subtle problem arises wi th this function. When the neighbor count for a dead
Most of the action of the program is postponed to various functions. After initializing cell reaches 3, we add it to the list nextlive, but it may well be that later in function
desc:riprion all the lists and arrays, the program begins its main loop. At each generation we first spurious emries AddNeighbors, its neighbor count will again be increased (beyond 3) so that it should not
go through the cells waiting in lists live and die in order to update the array map, be vivified in the next generation after all. Similarly when the neighbor count for a live
which, as in the first version of L ife, keeps track of which cells are alive. This work cell reaches 4, we add it to nextdie, but the function SubtractNeighbors may well reduce
is done in the functions Vivify (which means make alive) and Kill. After writing the its neighbor count below 4, so that it should be removed from nextdie. Thus the final
revised configuration, we update the count of neighbors for each cell that has been determination of lists nextlive and nextdie cannot be made until the array numbernbrs
born or has died, using the functions AddNeighbors and SubtractNeighbors and the has been fully updated, but yet, as we proceed, we must tentatively add entries to the lists.
10 Introduction to Software Engineering CHAPTER 2 SEC TION 2 . 3 Algorithm Development: A Second Version of Life 41
postpone dif]iculty II turns out that, if we postpone solution of this problem, it becomes much easier.
map live d ie
Jn the functions AddNe ighbors and SubtractNeighbors, .let us add eelIs to nextlive and 2 3 4 5
nextdie without worrying whether they will later be removed. Then when we copy
0 1 0 2, 2 2,3
duplicate eruries
nextlive and nextdie to lists live and die, we can check that the neighbor counts arc
cunecl (iu live, for example, only dead cells with a neighbor cou nt of exactly J should
appear) and delete the erroneous entries with no difficulty.
After doing this, however, an even more subtle error remains. It is possible th at
Initial configuration :
Mai n loop:
2
3
4
3 •3
1 •2 •3 •2
2 3 2
3
2, 4
4.3
3, 2
3,3
3,4 --
the same cell may appear in list nextlive (or nextdie) more than once. A dead cell,
for example, may initially have a count of 2, which, when increased, adds the cell to
nextlive. lts count may then be increased fu1ther, and in SubtractNeighbors decreased
one or more times, perhaps ending at 3, so that SubtractNe ighbors again adds it to
generation status after Vivify and Kill: status at end of loop:
nextlive. Then, when neighbor counts are updated in the next generation, this birth will
incorrectly contribute 2 rather than I to the neighbor counts. We could solve this problem live die map live die
by search ing the lists for duplicates before copying them, but to do so would be slow,
and we can again solve the problem more easily by postponing it. When, in the next 2, 2 3 1. 3 2.3
generation, we wish to vivify a cell, we shall fi rst check whether it is already alive. If
so, then we know that its entry is a duplicate of one earher on list live. While we are
postponing work, we might as well also postpone checking the neighbor counts, so that
2, 4
4, 3
- •
•4
3
•s
•s
•
•
. 4
3
-- 4, 2
4, 4
3, 3
3, 2
3, 4 -
the copying function will now do nothing but copy lists, and all the checking is done
in Vivify and Ki ll. Figure 2.2 shows the trace of Life2 for one small configuration and spurious spurious
exhibits the appearance and deletion of spurious and duplicate entries in the various lists. 2 1, 3 2, 3 2 • 2 1, 2 , 2, 2 ;
r
;:., ,, ~.. ~~ 1
·~.~ -4 $i;:.. • $
*,
c:t ;_;. -:,
Programming Precept
._;· ·:.:·
·· ~~o,miliOJe~ P9Stponin~ proqle!JlS simJ)lifie~ their solu!ion.
.;.:. . ~- . ·.
"
4, 2
4, 4
3, 3
3, 2
3, 4 - 2
• 1
3
•, • 2
3 • 1
• 1
3 2 - 1, 4
3,1
3.5
3, 2
3, 4
2, 4
4,3
2.2~
4, 2
2, 4
-
2, 3 4, 4
duplicates
!.3.3 Coding the Functions 3 2. 3 2, 2 2 •1 2 1, 2 3. 2
Now that we have spelled out completely and precisely the requirements for each func-
tion, it is time to code them into our programming language. In a large software project
it is necessary to do the coding at the right time, not too soon and not too late. Most pro-
grammers err by starting to code too soon. If coding is begun before the requirements are
3, 4
3, 2
2.4
4,4
4, 2 - 1 • 2
3 •3
• 2
•2
3
1 - 1, 4
2. 4
3, 5
2, 2
3, 1
3, 4
2,3
4, 3
1, 3
--
specifi.cations made precise, then unwarranted assumptions about the specifications will inevitably be
complete made while coding, and these assumptions may render different subprograms incompat-
ible with each other or make the programming task much more difficult than it need be.
··.·.;, <, ~ »: ..
4 2, 2
2.4
1, 3
- •
2
. 4
3
•
2
- 1, 2
1, 3
1, 4
2, 3 -
Brogramming Precept • •
•
t(•p-down coding It is possible, on the other hand, to delay coding too long. Just as we design from the top
down, we should code from the top down. Once the specifications at the top levels are 5 1,3 2,3 -- •
complete and precise, we should code the subprograms at these levels and test them by • •
including appropriat.e stubs. lf we then find that our design is flawed, we can modify it stable
configuration • •
without paying an exorbitant price in low-level functions that have been rendered useless.
Now that the specifications for several of the Life subprograms are complete, let us
•
embody our decisions into functions coded in C. Figure 2.2. A trace of program Life2
SECT ION 2.3 Algorithm Development: A Second Version of Life 43
42 Introduction to Software Engineering CHAP TE R 2
I* AddNeighbors: increase the neighbor count of vivified cells *I
1. Function Copy void AddNeighbors (LisUype * live, LisUype *nextlive, LisUype * nextdie,
Since we decided to postpone all the checking for spurious entries in the lists, the final Gridcount.type numbernbrs, GridJype map)
version of the copying function is trivial. It. need only copy one list to another and leave {
the first empty: int i, j; I* loop indices *I
int ilow, ihigh; I* row loop limits *I
I* Copy: copies the contents of one list to another. * I int jlow, jhigh; I* column loop limits *I
void Copy CList.type *IO, LisUype *from) int k, row, col; I* used to traverse list *'
copy one list { CoordJype nbr; I* structure form of a neighbor *'
int i;
for (k = O; k < live->count; k++ ) { I* Loop through vivified cells. *I
for ( i = O; i < from->count; i ++ ) row = live->entry [k] .row;
to- >entry [i] = from- >entry [i] ; col = live->entry [k] .col;
to->count = from->count; if (row<= 0) I* Set the loop limits. *I
}
from->count = O; I* Clear the copied list.
*' ilow = O;
else ilow = row - 1;
if (row>= MAXROW- 1)
2. Function Vivify ihigh = MAXROW - 1;
else ihigh = row + 1;
The function Vivify goes through the list live and vivifies each cell, provided that it was
if (col <= O)
previously dead and had a neighbor count of exactly 3. Otherwise, the cell is one of
jlow = O;
the spurious entries, and Vivify deletes it from the list. Hence at the conclusion of the
else jlow = col -1 ;
function, list live contains exactly those cells that were vivified and whose neighbor
if ( col >= MAXCOL- 1)
counts must therefore be updated by AddNeighbors.
jhigh = MAXCOL -1 ;
I* Vivify: make cells alive and check for duplicates *I else jhigh = col + 1;
void Vivify (List.type * live, GridJype map, GridcounUype numbernbrs) for (i = ilow; i <= ihigh; i+ +)
make cells alive { for (j = jlow; j <= jhigh; i ++ )
int k, row, col; if Ci ! = row 11 j ! = col) { I* Skip the cell itself. *I
k = O; find a neighbor nbr.row = i; f* Set up a coordinate structure. *I
while (k < live->count) { nbr.col = j;
row = live - >entry [k] .row; numbernbrs [i] [j] ++ ;
col = live->entry [ k] .col; switch ( numbernbrs [i] [j] ) {
if (map [row] [col] == DEAD && numbernbrs [row] [col] == 3) { put cells onto lists case 0:
map [row] [col] = ALIVE; I* Make the cell alive. *I printf (" Impossible case in AddNeighbors. \n") ;
k++; break;
} else { I* Delete entry k from the list. *' case 3:
live- >count--; if ( map [i] [j] == DEAD)
live->entry [k] = live- >entry [live- >count] ; Add(nextlive, nbr);
} break;
} case 4:
} if (map [i] [j] == ALIVE)
Add ( nextdie, nbr) ;
I. Function AddNeighbors break;
} /* switch statement *f
At the conclusion of function Vivify, the duplicate and ~purious entries have all been }
removed from list live, and what remains are the cells that were actually vivified. Hence
the function AddNeighbors can update all the related neighbor counts without problem.
This function has the following form.
}
} I* k loop
*'
SECTION 2.4 Verification of Algorithms 45
14 Introduction to Software Engineering CHAPTER 2
same status (alive or dead) from the end of function Kill until the next generation, and the
l. Remaining Functions functions AddNeighbors and SubtractNeighbors check that only dead cells are added to
The functions Kill and SubtractNeighbors are similar in form to Vivify and AddNeighbors; nextlive and only living cells to nextdie.
they will be left as exercises. The function WriteMap can be used without change from How can we be sure that there are not more subtle questions of this sort, some of
the first. version. but. a much more efficient version is possible that makes use of the lists which might not be so easy to answer? The only way we can really be confident is to
live and die to update the screen rather than rewriting it. at. each generation. The function prove that our program does the right action in each case.
Add appear s in Section 2.2. We shall postpone writing function Initialize to the end of 2. The Main Loop
the next section.
The difficulty with our program is that what happens in one generation might affect the
next generation in some unexpected way. Therefore we focus our attention on the large
Programming Pl. Write the missing functions for the second version of Life: (a) Kill and (b) Sub- loop in the main program. At the beginning of the loop it is the contents of lists live
Projects tractNeighbors. and die that determine everything that happens later. Let us therefore summarize what
2.3 P2. Write driver programs for the functions (a) Kill anc (b) SubtractNeighbors, and we know abou1 these lists from our previous study.
devise appropriate test data to check the performance of these functions.
loop invaria111 At the begi1111ing of the main loop, list live contains only dead cells, and list die
collfains only living cells, bur the lists may contain duplicate entries, or spurious
entries whose neighbor counts are wrong . The lists nextlive and nextdie are empty.
2.4 VERIFICATION OF ALGORITHMS
At the very start of the program, it is one task of function Initialize to ensure that the
Another important aspect of the design of large programs is algorithm verification, that
lists live and die are set up properly, so that the preceding statements are correct at the
is, a proof that the algorithm accomplishes its task. This kind of proof is usually
start of the firs1 generation. What we must prove, then, is that if the statements are true
form ulated by looking at the speci fications for the subprograms and then arguing that
at the start of any one generation, then after the eight function calls within the loop, they
these specifications combine properly to accomplish the task of the whole algorithm.
will again be true for the nex t generation.
purpose While constructing such a proof we may find that the specifications must be changed
to enable us to infer the correctness of the algorithm, and, in doing so, the proof itself 3. Proof by Mathematical Induction
helps us fomrnlate the specifications for each subprogram with greater precision. Hence At this point, you should note that what we are really doing is using the method of
algorithm verification and algorithm design can go hand-in-hand, and sometimes the mathematical induction to establish that the program is correct. In this method of
verification can even lead the way. In any case, algorithm verification shou.l d precede initial case proof, we begin by establishing the result for an initial case. Next we prove the result
coding. for a later case, say case n, by using the result for earlier cases (those between the initial
case and case n - I ). See Appendix A. I for further discussion and examples of proof
~.4.1 Proving the Program by mathematical induction.
For our program, verification of the initial case amounts to a verification that Initialize
Let us again illustrate these concepts by turning to the Life program, first to be sure
induction step works properly. For the second part of the proof, let us examine the actions in the main
that its algorithm is correct and second to assist us in designing the remaining function,
loop, assuming that the statements are correct at its beginning. Function Vivify uses only
Initialize.
list live and carefully checks each entry before it vivifies a cell, removing erroneous and
Indeed, the fact that there were subtle errors in our initial attempts to organize the duplicate entries from list live as it goes. Hence at the conclusion of Vivify, list live
work done in functions Vivify, Ki ll, AddNeighbors, SubtractNeighbors, and Copy should
contains only those cells that were properly vivified, and no duplicates. Function Kill
caution alert us to the possible presence of further errors, or at least to the necessity of exercising similarly cleans up list die. Since the two lists originally had no cells in common, and
considerably more care to be sure that our algorithms are correct.
none has been added to either list, no cells have been improperly both vivified and killed.
Next, funct ion WriteMap is called, but does not change the lists. Function AddNeighbors
I. Possible Problems
works only from list live and puts only dead cells on list nextlive, and only living ones
By postponing the checking of neighbor counts, we were able to avoid difficulties both on list nextdie. Similarly, function SubtractNeighbors keeps the dead and living cells
with the problems of duplicate and of erroneous entries. But, for example, how can properly separated. Together these two functions add all the cells whose status should
we be sure that it is not still possible that the same cell might erroneously be included change in the next generation 10 the lists, but may add duplicate or spurious entries.
in both lists nextlive and nextdie? Tf so, then it. might first be vivified and then killed Finally, the copying function sets up lists live and die and empties lists nextlive and
immediately in the following generation (clearly an illegal happening). The answer to nextdie, as required to show that all conditions in our statements are again true at the
this particular question is no, since the main program calls both functions Vivify and end of proof beginning of the next generation. The logic of our program is therefore correct.
Kill before either function AddNeighbors or SubtractNeighbors. Thus the cell keeps the
46 Introduction to Software Engineering CHAPTER 2 S EC T I O N 2. 4 Verification of Algorithms 47
2.4.2 Invariants and Assertions a proof that a program is correct, but it is a very useful exercise. Attempting to find
simplifica1io11 invariants and assenions sometimes leads to simplifications in design of the algorithm,
Statements such as the one we established in the preceding proof are called loop invari- which make its correctness more obvious. Our goa l should always be to make our
ants. In general, a loop invariant is a stateme_nt that is true at the beginning of every algorithms so straightforward and clear that their logic is obviously correct, and the use
iteration of the loop. The. statements we made aho11t th~ s1a111s of various lists at dif- of loop invariants can help in this process.
ferent points of the loop are called assertions, and assertions that hold at the beginning
and end of each function (or, more generally, before and after any statement) are called
preconditions and postconditions. Programming Precept
As an example, let us write down preconditions and postconditions for some of our Know your problem.
functions: Give precise preconditions and postconditions for each function.
Algorithm verification is a subject under active research, in which many imponant ques-
Vivify:
tions remain to be answered. Correctness proofs have not yet been supplied for a large
precondition: List live contains only dead cells and contains all cells ready to be number of imponant algorithms that are in constant use. Sometimes exceptional cases ap-
vivified. pear that cause an algorithm to misbehave; correctness proofs would provide a consistent
postcondition: Array map has been updated with vivified cells, and list live contains means to delineate these exceptions and provide for their processing.
only those cells that were vivified, with no duplicates or spurious
entries.
2.4.3 Initialization
As we turn, finally, to the function Initialize, let us use the postconditions for the function
10 help in its composition. The first postcondition states that the array map is to be
Add Neighbors:
design of Initialize initialized with the starting configuration of living and dead cells. This task is similar to
precondition: List live contains all vivified cells whose neighbor counts have not the initialization of the first version of the program, and we leave the resulting function
yet been updated. ReadMap as an exercise. We shall, however, need the list of initially living cells for
postcondition: Array numbernbrs has increased counts for all cells neighboring calculating the neighbor counts, so we shall also require ReadMap to put this list into list
cells in list live . If the increased neighbor count makes the cell a live. As one of its postconditions ReadMap will be required to make sure that there are
candidate to be vivified lresp. killedJ then the cell has been added no duplicates in this list. (This can be achieved easily if the reading is done properly.)
to list nextlive [resp. nextdie]. The second task is to initialize the neighbor counts in array numbernbrs. But we
have required ReadMap to set up list live so that it contains exactly the information
needed for function AddNeighbors to set the neighbor counts properly for all neighbors
of living cells, provided that before calling AddNeighbors, we first set all entries in
Initi alize: numbernbrs to 0. As well as initializing numbernbrs, function AddNeighbors will
prec-ondffion: None. locate all dead cells that will become alive in the following generation and add them
to list nextlive. Hence by setti ng nextlive 10 be empty before calling AddNeighbors
postcondition: Array map contains the initial configuration of liv ing and dead cells. and copying nextlive to live afterward, we accomplish another of the postconditions of
Array numbernbrs contains counts of living neighbors correspond-
Initialize.
ing to the configuration in array map. List live contains only dead
The final postcondition is that list die contain all living cells that should die in the
cells and includes all candidates that may be vivified in the next
next generation. Some, but perhaps not all, of these may be found by AddNeighbors (in
generation. List die contains only living cells and contains all can-
the main loop, the remainder would be found by SubtractNeighbors, which we have no
didates that may die in the next generation. way tu use in Initialize). We can accomplish the postcondition more easily, however, by
simply putting all the living cells into list die. Recall that function Kill allows spurious
entries on its input list.
The purpose of loop invariants and assertions is to capture the essence of the dynamic
In this way the postconditions lead to the following function:
process. It is not always easy to find loop invariants and assertions that will lead 10
CHAPTER 2 SECT I O N 2 . 5 Program Analysis and Comparison 49
48 Introduction to Software Engineeri ng
Programming Precept
Keep your algorithms as simple as you can.
2.5 PROGRAM ANALYSIS AND COMPARISON When in doubt, choose the simple way.
In designing algorithms we need methods to separate bad algorithms from good ones,
space requiremems The second point of view is that of storage requirements. Our first program used very
to help us decide, when we have several possible ways in which to proceed, which
little memory (apart from that for the instructions) except for the two arrays map and
way will prove the most effecti ve for our problem. For thi s reason the analysis of algo-
newmap. These arrays have entries that, in assuming only the two values alive and
rithms and the comparison of alternative methods constitute an important part of software
dead, can be packed so that each entry takes only a single bit. In a typical computer
engineering.
50 Introduction to Software Engineering CHAPTER 2 SECTION 2 . 6 Conclusions and Preview 51
with word size of 32 or 16 bits, the two arrays need then occupy no more than 250 or E3. Note that there is some inefficiency in the program Life2 in having functions
500 words, respectively. On the other hand, program Life2 requires, along with the space AddNeighbors and SubtractNeigh bors called once from the main program, since
for its instructions, space for one such array, plus 4000 words for the array numbernbrs these functions must loop through the lists live and die just as Vivify and Kill already
and 401 words for each of its four lists, giving a total of more than 5700 words. do. It would be faster if these functions were written to update the neighbors of
onl y one cell and were called from Vivify and Kill whenever a cell was vivi fied or
3. Time and Space Trade-offs killed.
We have just seen the first of many examples illustrating the substantial trade-offs that a. Will the program work correctly if these changes are made?
can occur between time and space in computer algorithms. Which to choose depends b. If not, what further changes will make it work?
on available equipment. If the storage space is available and otherwise unused, it is c. With your revised program, fi nd the proper loop invariants and verify that your
obviously preferable to use the algori thm requiring more space and Jess time. If not, algorithm is correct.
then time may have to be sacrificed. Finally, for an important problem, by far the best
approach may be to sit back and rethink the whole problem: you will have learned much Programn1ing Pl. If you use a video terminal wi th direct cursor address ing, write a version of the
from your first efforts and may very well be able to find another approach that will save Projects function WriteMap that takes advantage of the lists live and die 10 update the map
both time and space. 2.5 rather than completely rewriting it at each generation.
P2. Run the complete program Life2 and compare timings with those of Life 1.
When we sLart.ed in Section 1.4, we did nothing of the sort, but plunged right in with an 2.6.2 Program Design
approach leaving much to be desired. Almost every programmer learns this experience
the hard way and can sympathize with the following: 1. Criteria for Programs
A major goal of this book is to evaluate algorithms and data structures that purport to
' solve a problem. Amongst the many criteria by which we can judge a program, the
~·· t rogramming Precep.t
followi ng are some of the most important:
& "' Act in haste and repent at leisure.
:?'.-. ,;,:
:~...
Program in haste and debug forever.
;.,~. ~, ·h ' ·-.:
I. Does it solve the problem that is requested, according to the given specifications?
2. Does it work correctly under all conditions?
The same thought can be expressed somewhat more posirively: 3. Does it include clear and sufficient information for its user, in the form of instructions
and documentation?
, ·::;:. ~~::-;- --:-.:x: ~ cc')~' ~r· -~~ ·:-:-· ~f ~ ~ ? ~<... ~f
4. Is it logically and clearly written, with short modules and subprograms as appropriate
,,. v , . '" Programmln,g Precept ,. ~:
10 do logical tasks?
;, Starting' afresfl fs Jsually easier tnari patching an old program.
:r ~ ·.x ~-:.; <;;. :_f ::: *" _:, ·::t ..s:.. - ,.-: .. ,:x · 5. Does it make efficient use of time and of space?
Some of these criteria will be closely studied for the programs we write. Others will
A good rule of thumb is rhar, if more than Len percent of a program must be modified,
not be mentioned explicitly, but not because of any lack of importance. These criteria,
then it is time to rewrite the program completely. With repealed palches to a large
rather, can be met automatically if sufficient thought and effort are invested in every
program, the number of bugs Lends to remain constant. That is, the patches become so
stage of program design. We hope that the examples we study will reveal such care.
complicaled that each new patch tends to introduce as many new errors as it corrects.
2. Software Engineering
3. Prototyping
Software engineering is the study and practice of methods helpful for the construction
An excellent way to avoid having to rewrite a large project from scratch is to plan from and maintenance of large software systems. Although small by realistic standards, the
the beginning to write two versions. Before a program is running it is often impossible to program we have studied in this chapter illustrates many aspects of software engineering.
know what parts of the design will cause difficu lty or what features need to be changed to Software engineering begins with the realization that it is a very long process to
meet the needs of the users. Engineers have known for many years that it is not possible obtain good software. It begins before any programs are coded and continues maintenance
to build a large projecr. directly from the drawing board. For large projects engineers for years after the programs are put into use. This continuing process is known as the
always build prototypes, that is, scaled-down models that can be studied, tested, and life cycle of software. This life cycle can be divided into phases as follows:
sometimes even used for limiLed purposes. Models of bridges are built and tested in
wind tunnels; pilot plants are constructed before attempting to use new technology on phases of life cycle I. Analyze the problem precisely and completely. Be sure to specify all necessary
the assembly line. user interface with care.
software prototypes Prolotyping is especially helpful for computer software, since it eases communica- 2. Build a prototype and experiment with it until all specifications can be finalized.
tion between users and designers early in the project, thereby reducing misunderstandings 3. Design the algorithm, using the tools of data structures and of other algorithms
and helping to settle the design to everyone's satisfaction. In building a software proto- whose function is already known.
type the designer can use programs that are already written for input-output, for sorting,
or for other common requirements. The building blocks can be assembled with as little 4. Verify that the algorithm is correct, or make it so simple that its correctness is
new programming as possible to make a working model that can do some of the intended self-evident.
tasks. Even though the prototype may not function efficiently or do everything that the 5. Analyze the algorithm to determine its requirements and make sure that it meets the
final system will, it provides an excellent laboratory for the user and designer to exper- specifications.
iment with alternative ideas for the final design. The following precept is due to FRED 6. Code the algorithm into the appropriate programming language.
BROOKS. 7. Test and evaluate the program on carefully chosen test data.
~· ,, 8. Refine and repeat the foregoing steps as needed for additional subprograms until
t + Programming Pi ece.pt ,.;.. ,,. the software is complete and fully functional.
; Al~ays plan t9 ~uild a P(Ot~type an~ t~row it away. 9. Optimize the code to improve performance, but only if necessary .
·~
.· ..·''.: You'll
'
dO
'::;·
so whether
.
you;:,.-.; plan ...to or.not.
- ' JO. Maintain the program so th at it will meet the changing needs of its users.
S E C TION 2.6 Conclusions and Preview 55
54 Introduction to Software Engineering CHAPTER 2
2.6.3 The C Language
Most of these topics have been discussed and illustrated in various sections of this and
the preceding chapter, but a few further remarks on the first phase, problem analysis and In this chapter and the previous one, we have had a whirlwind tour of many features
specification, are in order. of C. No attempt has been made to present an orderly or complete description of C
features. A concise summary of C appears in Appendix C, to which you should refer
3. Problem Analysis with questions of C syntax. For further examples and discussion, consult a C textbook.
Analysis of the problem is often the most difficult phase of the software life cycle. This
is not because practical problems are conceptually more difficult than are computing Programming Pl. A magic square is a square array of integers such that the sum of every row, the
science exercises-the reverse is often the case-but because users and programmers
Proj ects sum of every column, and sum of each of the two diagonals are all equal. Two
tend to speak different languages. Here are some questions on which the analyst and magic squares are shown in Figure 2.3. 1
user must reach an understanding:
2.6
specifications 1. What form will the input and output data take? How much data will there be?
2. Are there any special requirements for the processing'? What special occurrences
will require separate treatment? Il6 J g ~J 17 24 t 8 15
23 5 7 14 16
3. Will these requirements change? How? How fast will the demands on the system
grow?
~ ~(Q) ~ ij ~ 4 6 13 20 22
4. What parts of the system arc the most important? Which must run most efficiently? 9> 6 71 ~g 10 12 19 21 3
5. How should erroneous data be treated? What other error processing is needed?
6. What kinds of people will use the software? What kind of training will they have? ~ ij~ Il~ ~ 11 18 25 2 9
What kind of user interface will be best? sum = 34 sum = 65
7. How port.able must the soft.ware be, to move to new kinds of equipment? With what Figure 2.3. Two magic squares
other software and hardware systems must the project be compatible?
a. Write a program that reads a square array of integers and determines whether
8. What extensions or other maintenance arc anticipated? What is the history of
or not it is a magic square.
previous changes to software and hardware?
b. Write a program that generates a magic square by the foll owing method. This
method works only when the size of the square is an odd number. Start
4. Requirements Specification by placing I in the middle of the top row. Write down successive integers
The problem analysis and experimentation for a large project finally lead to a fonnal 2, 3, ... a long a diagonal going upward and to the right. When you reach the
statement of the requirements for the project.. This statement becomes the primary way in top row (as you do immediately since I is in the top row), continue to the
which the user and the software engineer attempt to understand each other and establishes bottom row as though the bottom row were immediately above the top row.
the standard by which the final project will be j udged. Among the contents of this When you reach the rightmost column, continue to the leftmost column as
specification will be the following: though it were immediately to the right of the rightmost one. When you reach
a position that is a lready occupied, instead drop straight down one position
I. Functional requirements for the system: what it will do and what commands will from the previous number to insert the new one. The 5 x 5 magic sq uare
be available to the user. constructed by this method is shown in Figure 2.3.
2. Assumptions and limitations on the system: what hardware will be used for the P2. One-dimensional Life takes place on a straight line instead of a rectangular grid.
system, what form the input must take, the maximum size of input, the largest Each cell has four neighboring positions: those at distance one or two ftom it on
number of users, and so on. each side. The rules are similar to those of two-dimensional Life except ( I) a
3. Maintenance requirements: anticipated extensions or growth of the system, changes dead cell with either two or three living neighbors will become alive in the next
in hardware, changes in user interface. generation, and (2) a liv ing cell dies if it has zero, one, or three living neighbors.
4. Documentation requirements: what kind of explanatory material is required for (Hence a dead cell with zero, one, or fou r living neighbors stays dead; a living cell
what kinds of users. with two or four living neighbors stays alive.) The progress of sample communities
is shown in Figure 2.4. Design, write, and test a program for one-dimensional Life.
The requirements s pecifications state what the software will do, not how it will be done.
These specifications should be understandable both to the user and to the programmer. If I
The magic square on the left appears as shown here in the etching Mela11colia by ALBRECHT D ORER.
carefully prepared, t.h ey will fonn the basis for the subsequent phases of design, coding, Nole 1he inclusion of 1he date of the e1ching, 1514.
testing, and maintenance.
56 Introduction to Software Engineering CHAPTER 2 C HAPTER 2 Review Questions 57
e. Usi ng the rules on leap years, s how that the sequence of calendars repeats
exactly every 400 years.
f. What is the probabi lity (over a 400 year period) that the 13th of a month is
I• I I· I I I• I a Friday? Why is the 13th of the month more likely to be a Friday than any
other day of the week? Wri te a program to calculate how many Friday the
13ths occu r in th is century.
II
Dies out Oscillates
I l• l•I l•l•I I I I
REVIEW QUESTIONS
I· I I· I I· I I· I 2./ 1. What is program mai nte nance?
2.2 2. W hat are hierarchical structures, and how s hould they be processed?
Glides to the right Repeats in six generations 2 .3 3. When shou ld allocation of tasks among functions be made?
Figure 2.4. One-dimensional Life con6guralions 4. How long shou ld coding be delayed?
2.4 5. What is mathematical induction?
P3. a. Write a program that will print the calendar of the c urrent year.
6. What is a loop invariant?
b. Modify the program so that it will read a year number and print the calendar
for that year. A year is a leap year (that is, February has 29 instead of 28 7. What are preconditions and postcond itions of a subprogram?
days) if it is a m ultiple of 4, except that century years (multiple of I00) are
2.5 8. What is a time-space tradeoff?
leap years on ly when the ye ar is divisible by 400. Hence the year 1900 is not
a leap year, but the year 2000 is a leap year. 2.6 9. What is a prototype?
c. Mod ify the program so that it will accept any date (day, month, year) and print 10. Name at least six phases of the software life cycle and state what each is.
the day of the week for that date.
11. Define software e ngi neering.
d. Modify tlle program so that it will read two dates and prim the number of days
from one to the other. 12. What are requirements specifications for a program?
58 Introduction to Software Engineering CHAPTER 2
C HA P T E R 3
REFERENCES FOR FURTHER STUDY
software engineering A thorough discussion of many aspects of structured programming is:
EDWARD YouROON, Techniques of Program Structure and Design, Prentice Hall, Engle-
wood Cliffs, N. J., 1975, 364 pages.
A perceptive discussion (in a book that is also enjoyable reading) of the many problems
that arise in the construction of large software systems is:
FR!ilJcRICKP. BROOKS, JR. , The Mythical Man- Month: EssC1ys 011 Software Engineering,
Addison-Wesley, Reading, Mass., 1975, 195 pages.
A good textbook on software engineering is:
Lists
IAN SOMMERVILLE, Software Engineering, Addison-Wesley, Wokingham, England, 1985,
334 pages. This chapter introduces the study of data structures by studying various
Program testing has been developed to the point where its methods can fill a large book: kinds of lists, including stacks and queues, together with their implemen-
WILLIAM E. PERRY, A Structured Approach to Systems Testing, Prentice Hall, Englewood tations in computer storage and C functions. The separation between the
Cliffs, N. J., 1983, 451 pages. use of da1a structures and their implementation is emphasized. Several
algorithm Two books concerned with proving programs and using assertions and invariants to examples are developed, including a simulation problem.
verification develop algorithms are
DAv10 GR1~s, The Science of Programming, Springer-Verlag, New York, 1981, 366 pages.
SllAD AtAc.1c and M1rnAi;1. A. ARRJR, The Design of Well-Structured and Correct Pro-
grams, Springer-Verlag, New York, 1978, 292 pages.
Keeping programs so simple in design that they can be proved to be correct is not easy, 3.1 Static and Dynamic Structures 60 3.4.3 The Main Program 80
but is very important. C. A. R. HOARE (who invented the quicksort algorithm that we 3.4.4 Steps of the Simulatibn 81
shall study in Chapter 7) writes: "There are two ways of constructing a software design: 3.2 Stacks 60 3.4.5 Random Numbers 84
3.2.1 Definition and Operations 60
One way is to make it so simple that there are obviously no deficiencies, and the other 3.4.6 Sample Results 85
3.2.2 Examples 61
way is to make it so complicated that there are no obvious deficiencies. The first method 3.2.3 Array Implementation of
is far more difficult." This quotation is from the 1980 Turing Award Lecture, "The Stacks 64 3.5 Other Lists and their
emperor's old clothes," Communications of the ACM 24 (1981), 75- 83. Implementation 89
Two books concerned with methods of problem solving are 3.3 Queues 68
3.3.1 Definitions 68
problem solving GEORGE P<\1-vA, How to Solve It. second edition, Doubleday, Garden City, N.Y., 1957, Pointers and Pitfalls 96
253 pages. 3.3.2 Implementations of Queues 69
3.3.3 Circular Queues in C 72
WAYNF. A. W1cKF.J.GRF.x, How ro Solve Problems, W. H. Freeman, San Francisco, 1974,
262 pages. Review Questions 96
3.4 Application of Queues: Simulation 77
The programming project on one-dimensional life is taken from 3.4.1 Introduction 77
3.4.2 Simulation of an Airport 78 References for Further Study 97
JoNAl'IJANK. M1LL!iR, "One-dimensional Life," Byte 3 (December, 1978), 68- 74.
59
SEC TION 3.2 Stacks 61
60 Lists CHAPTER 3
This little exercise will probably cause difficu lty for some students. Most will realize
t.hat they need to use an array, but some will attempt to set up the array t.o have n entries
and will be confused by the error message resulting from attempti ng to use a variable
rather tJ1an a constant to declare the size of the array. O:her students will say, "I could
solve the problem if I knew that there were 25 numbers, but I don't see how to handle
fewer." Or "Tell me before I write the program how large n is, and then I can do it."
The difficulties of these students come not from st.upidity, but from thinking logi-
cally. In a beginning course, there is sometimes not enough distinction drawn between
list.\· and arrays two quite different concepts. First is the concept of a list of n numbers, a list whose
size is variable, that is, a list for which numbers can be inserted or deleted, so that, if
n = 3, then the list contains only 3 numbers, and if n 19, then it contains 19 num- =
bers. Second is the programming feature called an array or a vector, which contains a
constant number of positions, that is, whose size is fixed when the program is compiled.
implementation The two concepts are, of course, related in that a list of variable si:te can be implemented
in a computer as occupying part of an array of fixed size, with some of the entries in the Figure 3.1. Stacks
array remain ing unused.
In this chapter we shall see that sometimes there are several different ways to in a busy cafeteria. Throughout the lunch hour, customers take trays off the top of
implement a list within computer memory, and therefore the careful programmer needs the stack, and employees place returned trays back on top of the stack. The tray most
choice of to make a conscious decision about which of these to choose. The need for careful recently put on the stack is the first one taken off. The bottom tray is the first one put
data structures decision making about how to store dat.a, in fact, extends back even further in the on, and the last one to be used.
process of program design. We shall soon see that there is more than one kind of list Sometimes thi s picture is described with plates or trays on a spring-loaded device so
(stacks and queues are the first. two kinds we study), and therefore the programmer must that the top of the stack stays near the same height. This imagery is poor and should be
first decide what kind of list (or what other conceptual s'.ructure) is needed for the data avoided. If we were to implement a computer stack in this way, it would mean moving
choice (if and then must decide how the conceptual structure will be implemented in computer every item in the stack whenever one item was inserted or deleted. This would be costly.
implementations memory. By keeping these decisions separate, we shall be able both to simplify the It is far better to think of the stack as resting on a firm counter or floor, so that only the
programming process and avoid some of the pitfalls that attend premature decisions. top item is moved when it is added or deleted. The spring-loaded imagery, however, has
Already in Sect.ion 2.2 we have discussed the implementation of lists inside C contributed a pai r of colorful words that are fim1ly embedded in computer jargon, and
arrays. We set up a structure type, so that we could keep track both of the array and of which we shall use. When we add an item to a stack, we say that we push it onto the
the variable that counts entries in the list, tied together as one logical structure. In th is push and pop stack, and when we remove an item, we say that we p op it from the stack. From the
chapter we shall continue with similar methods. same analogy, the term push-dow11 list is used synonymously with stack, but we shall
not employ this term. See Figure 3.2.
?1
Push box A onto stack:
~ c
data
8
data
..----....._/
Pop a box from stack : I
/ Q
?1 I A c:::1 ~ A
data 8
data
8
data
8
data
/
!empty)
- /
I
Q
,;::, --- A
data
8
data
A
data
8
data
A
8
data
A
8
data
A
data
8
data
A
data
8
data
A
8
data
A
8
data
A
Re' data data data data data
Push box R onto stack: I
consider the machine's task of assigning temporary storage areas for use by subprograms,
~
then these areas would be allocated in a list with this same property, that is, in a stack
Push box M onto stack; (see Figure 3.3). Hence yet one more name sometimes used for stacks is LIFO lists,
based on the acronym for this property.
----... ----.._ 2. Reversing a Line
Pop a b-Ol< Irom stack :
s3 I
/ M CJ As a simple example of using stacks, let us suppose that we wish to make a function
that will read a line of input and will then write it out backward. We can accomplish
this task by pushing each character onto a stack as il is read. When the line is finished,
~
we then pop characters off the stack, and they will come off in the reverse order. Hence
Push box Q onto stack: our function takes the following form :
f* ReverseRead: read one line of input and write it backward.
void Reverse Read ( void)
*'
{
ltem _type item;
Push box S onto stack:
ltemJype *item_ptr = &item;
StackJype stack;
Figure 3.2. Pushing a nd popping a stack Stack_type * Stack_ptr = &stack;
complete. It must also remember all the local variables, CPU registers, and the like, so
that information will not be lost while the subprogram is working. We can think of all
Initialize (stack_ptr); f* Initialize the stack to be empty.
while ( 1 Full(stack_ptr) && (item= getchar()) ! = ' \n') *'
subprogram data
this information as one large structure, a temporary storage area for each subprogram.
Suppose now that we have three subprograms called A, B, and C , and suppose
Push ( item, stack ptr);
while ( 1 Empty(stack_ptr)) {
f* Push each item onto the stack.
*'
srorage that A invokes B and B invokes C. Then B will not have finished its work until C
has finished and returned. Similarly A is the first to start work, but it is the last to be
Pop(item_ptr, stack_ptr);
putchar (item) ;
I* Pop an item from the stack.
*'
}
finished, not until sometime after B has finished and returned. Thus the sequence by putchar(' \n');
which subprogram activity proceeds is summed up as the property last in, first out. If we }
64 Lists CHAPTER 3 SECTION 3. 2 Stacks 65
In this function we have used not only Push and Pop but also a function Initialize that I• Push: push an item onto the stack. • I
initializes the stack to be empty and Boolean-valued functions Empty that checks whether void Push ( Item.type item, Stack.type • stack..ptr)
the stack is empty or not and Full that checks if it is completely full. T he declarations {
for a stack appear in Section 3.2.3. if (stack..ptr->top >= MAXSTACK)
Error ( "Stack is full");
3. Information Hiding else
Notice that we have been able to write our function before we consider how the stack stack..ptr->entry [stack..ptr->top++ J = item;
}
will actually be implemented in storage and before we write the details of the varibus
use offunctions functions. In this way we have an example of information hiding : lf someone else I • Pop: pop an item from the stack. •t
had already written the functions for handling stacks, then we could use them without void Pop(ltem.type • item..ptr, Stack.type • stack..ptr)
{
needing to know the details of how stacks are kept in memory or of how the stack
operations are actually done. if (stack..ptr->top <= O)
Error( "Stack is empty");
else
3.2.3 Array Implementation of Stacks • item..ptr = stack..ptr->entry [ - -stack..ptr->top];
}
1. Declarations Error is a function lhat receives a pointer to a character string, prints the string, and then
invokes lhe standard function exit to terminate the execution of the program.
To implement a stack in a computer we shall set up an array that will hold the items
in the stack and a counter to indicate how many items there are. In C we can make t• Error: print error message and terminate the program. • I
void Error ( char • s)
declarations such as the following, where MAXSTACK is a symbolic constant giving the
{
maximum size allowed for stacks and Item.type is the type describing the data that
fprintf ( stderr, "%s \n", s);
will be put into the stack. lype Item.type depends on the application and can range
exit(1);
from a single character or number to a large structure with many elements. Our stack.h
}
contains:
ln normal circumstances Error is not executed because, as in ReverseRead, we check
#define MAXSTACK 10 whether the siack is full before we push an item and we check if the stack is empty
typedef char Item.type; before we pop an item.
3. Other Operations
typedef struct stack.tag {
int top; I• Empty: returns non-zero if the stack is empty. • I
Item.type entry [ MAXSTACK] ; Boolean.type Empty ( Stack.type •stack..ptr)
} Stack.type; {
Boolean.type Empty ( Stack.type * ) ; return stack..ptr->top <= O;
Boolean.type Full (Stack.type *); }
void Initialize (Stack.type * ); I • Full: returns non-zero if the stack is full. • I
void Push ( Item.type, Stack.type *); Boolean.type Full (Stack.type • stack..ptr)
void Pop ( Item.type *, Stack.type *); {
return stack..ptr->top >= MAXSTACK;
2. Pushing and Popping }
Pushing and popping the stack are then implemented as follows. We must be careful The next function initializes a siack t0 be empty before it is first used in a program:
of the extreme cases: We might attempt to pop an item from an empty stack or to t• Initialize: initialize the stack to be empty. • t
push an item onto a full stack. We shall regard such attempts as errors fatal to the void Initialize ( Stack.type • stack..ptr)
execution of the program, and therefore we must also write Boolean-valued functions so {
that by checking emptiness or fullness the program can guard against these errors ever stack..ptr->top = O;
occurring. }
SE C T I ON 3.2 Stacks 67
66 Lists CHAPTER 3
b. Return the third element from the top of the stack, provided that the stack con-
4. Advantages of the Operational Approach
tains at least three integers. I f not, return INT_MAX. Leave the stack unchanged.
If you think that we are belaboring the obvious by introducing all these functions, then c. Return the bottom element of stack (or INT_MAX i f stack is empty), and leave
in one sense you are right, since their substance is so simple that they could easily the stack unchanged. [Hint: Use a second stack.J
be written out wherever they are needed in a program, rather than keeping them as d. Delete all occurrences of x from the stack, leaving the other elements of the
separate functions. For large programs there is one important advantage, however, to stack in the same order.
flexibility of using a structure to implement stacks and separate functions to process them . It may E3. Sometimes a program requires two stacks containing the same type of items. If the
implernentarion well happen that, after we start work on a large project, we realize that another way of two stacks are stored in separate arrays, then one stack might overflow while there
implementing our stacks in storage would prove better than the method we had used. If was considerable unused space in the other. A neat way to avoid this problem is to
the instructions have been written out every time a stack is pushed or popped, then every put all the space in one array and let one stack grow from one end of the array and
occurrence will need to be changed. 1f we have used structures and functions, then only the other stack start at the other end and grow in the opposite direction, toward the
the definition of the structure type and the declarations of the functions must be altered. first stack. In this way, i f one stack turns out to be large and the other small, then
clarity of program A second advantage, of course, is that the very appearance of the words Push and Pop they w ill still both fit, and there will be no overflow until all the space is actually
will immediately alert a person reading the program to what is being done, whereas the used. Declare a new structure type DoubleStack that includes the array and the
wp-down design instructions themselves might be more obscure. A third advantage we shall find is that two indices topA and topB, and wri te fu nctions PushA, PushB, PopA, and PopB
separating the use of data structures from their implementation will help us improve the to handle the two stacks within one DoubleStack.
top-down design of both our data structures and our programs. E4. Write a program that uses a stack to read an integer and print all its prime divisors
in descending order. For example, with the integer 2 100 the output should be
Exercises El. Draw a sequence of stack frames showing the progress of each of the following
3.2 segments of code. (stack_ptr is a pointer to a stack of characters and x, y, z are 7 5 5 3 2 2.
character variables.)
[Hin t: The smallest div isor greater than l of any integer is guaranteed to be a
prime.)
a. Initialize (stack_ptr); c. Initialize (stack_ptr);
Push ('a', stack_ptr) ; Push ('a', stack_ptr) ; ES. A stack may be regarded as a railway switching network like the one in Figure 3.4.
Push C' b', stack_ptr); Push ( 'b' , stack. ptr ) ;
Push ( 'c', stack_ptr) ; Push ( 'c', stack_ptr ) ;
Pop C&x, stack_ptr);
Pop ( &y, stack_ptr) ;
Pop C&z, stack_ptr) ;
wh ile ( ! Empty(stack_ptr))
Pop ( &x, stack_ptr) ; - -
d. Initialize ( stack_ptr) ;
b. Initialize (stack_ptr); Push ('a' , stack_ptr) ;
Push ('a' , stack_ptr); Push ( 'b', stack_ptr);
Push C' b', stack._ptr) ; Push ( ' c' , stack_ptr) ;
Initialize (stack_ptr); Pop ( &x, stack_ptr) ;
Push ( 'c', stack_ptr); Pop ( &y, stack_ptr) ;
Pop ( &x, stack _ptr) ; Push (x, stack_ptr);
Push(' a', stack_ptr); Push(y, stack_ptr);
Pop ( &y, stack_ptr) ; Pop C&z, stack_ptr) ; Figure 3.4. Switching network for stack permutations
Push ( 'b', stack_ptr);
Pop ( &z, stack_ptr);
Cars numbered I, 2, ... , n are on the I ine at the left , and it is desired to
rearra nge (pennute) the cars as they leave on the rig ht ha nd track. A car that is on
E2. Let stack_ptr be a pointer to a stack stack of integers and item be an integer variable.
Use the functions Push, Pop, Initialize, Empty, and Full to write functions doing the spur (stack) can be left there or sent on i ts way down the right track, but it can
each of the following tasks. [You may declare additional variables in your functions never be sent back to the incoming track. For example, i f n = 3, and we have the
if needed.] cars I, 2, 3 on the left track , then 3 fi rst goes to the spur. We could then send 2 to
the spur, then on its way to the right, then send 3 on the way, then I , obtaining the
a. Return the top element of the stack and leave the top element unchanged. If new order l , 3, 2.
the stack is empty, return INLMAX.
68 Lists CHAPTER 3 S EC T ION 3 .3 Queues 69
a . For n = 3, find all possible permutations that can be obtained. 3.3.2 Implementations of Queues
b. Same, for n = 4.
c. [Challenging] For general n , find how many permutations can be obtained by 1. The Physical Model
using this stack.
As we d id for stacks, we can create a queue i11 computer storage eas il y by setting up an
ord inary array to hold the items. Now, however, we must keep track of both tiie front
and the rear of the queue. One method wou ld be to keep the front of the queue al ways
3.3 QUEUES in the first location of the array . Then an item could be added to the queue simp ly by
increas ing the counter showing the rear, in exact ly the same way as we added an item
to a stack. To delete an item from the queue, however, wou ld be very expensive indeed,
3.3.1 Definitions s ince after the fi rst item was removed, all the remaining items wou ld need to be moved
one position up the queue to fill in the vacancy. With a long queue, this process would
In ordinary English a queue is defined as a waiting line, like a line of people waiting to be prohibitively slow. Although this method of storage closely models a queue of people
purchase tickets, where the first person in line is the first person served. For computer waiting to be served, it is a poor choice for use in computers.
applications we s imilarly define a queue to be a list in which all additions to the li st are
made at one end, and all deletions from the list are made at the other end. Queues are 2. linear Implementation
a lso called first-in, first-out lists, or FIFO for short. See Figure 3.5.
For efficient processing of queues, we shall therefore need two indices so that we can
keep track of both the front and the rear of the queue without moving a ny items. To
add an item to the queue, we simply increase the rear by one and put the item in that
position. To remove an item, we take it from the position at the front and then increase
the front by one. This method, however, st ill has a major defect. Both the front and
defect rear indices are increased but never decreased. Even if there are never more than two
items in the queue, an unbounded amount of storage will be needed for the queue if the
sequence of operations is
The problem, of course, is that, as the queue moves down the array, the storage space at
the beginning of the array is discarded and never used again. Perhaps the queue can be
li kened to a snake crawling th rough storage. Sometimes the snake is longer, sometimes
shorter, but if it always keeps crawling in a stra ight line, then it will soon reach the end
of the storage space.
Note, however, that for applications where the queue is regularly empt ied (such as
advanwge when a series of requests is allowed to bu ild up to a certain point, and then a task is
Figure 3.5. A queue
initiated that clears all the requests before returning), at a time when the queue is empty,
the fron t and rear can both be reset to the beginning of the array, and the s imp le scheme
Applications o f que ues are, if anything , even more common than are applications of using two indices and s traight- line storage becomes a very efficient storage method.
applications of s tac ks , since in performing tasks by compute r, as in all parts of life, it is so ofte n
necessary to wait one 's tum before having access to some thing. Within a computer 3. Circular Arrays
system there may be queues o f tasks waiting for the line prin ter, for access to disk
storage, o r e ven, in a time-sharing system, for use of the CPU. \Vithin a s ingle prog ram, I n concept we can overcome the inefficient use of space simply by thinking of the array
there may be m ultiple requests to be kept in a que ue, or one task may create other tasks, as a circle rather than a stra ight line. See Figu re 3.6. In this way as items are added and
wh ich must be done in turn by kee ping the m in a queue. removed from the queue, the head will continually c hase the tail around the array, so
front and rear The item in a queue ready to be served, that is, the first item that will be remo ved that the snake can keep crawling indefini tely but stay in a confined circuit. At different
from the q ueue, we call the front o f the queue (or, sometimes. the head of the que ue). times the queue wi ll occupy different parts of the array, but we never need worry about
S imilarly, the last item in the queue , that is, the one most recently added , we call the runn ing out of space unless the array is fully occupied, in which case we tru ly have
rear (or the tail) of the queue. overfl ow.
70 Lists CHAPTER 3
S ECTION 3. 3 Queues 71
O ma• - 1
if (i>=MAX - 1)
i = O;
else
i++;
Circular
q ueue
or even more easily (but perhaps less efficiently at run time) by using the% operator:
i = (i + 1) % MAX;
6. Boundary Conditions
Before writi ng fonnal algorithms to add to and delete from a queue, let us consider the
boundary conditions, that is, the indicators that a queue is empty or fu ll. If there is
exactly one entry in the queue, then the front index will equal the rear index. When th is
Unwind ing one entry is removed, then the front will be increased by l, so that an empty queue is
indicated when the rear is one posi tion before the front. Now suppose that the queue
is nearly fu11. Then the rear wi11 have moved we11 away from the front, a11 the way
around the circle, and when the array is fu11 the rear will be exactly one position behind
the front. Thus we have another difficulty: The front and rear indices are in exactly the
empty orfu/1? same relative positions for an empty queue and for a fu ll queue! There is no way, by
looking at the indices alone, to te11 a fu11 queue from an empty one. This situation is
occupied
illustrated in Figure 3.7.
Linear
implementatio,, front rear
Queue
containing
one item
.: z
II(I (I (l
z z 2
rear
Jf i front
L( rrI I I I I 0
z z z z z
0 1 2 occupied J
max - 2
ma>< - 1 i Remove the item.
.CJ tic
..: z 2 2 2 2 2 2 ? 2 2 2 2
Figure 3.6. Queue in a circular array
Empty
queue II(I II I 0 fro nt
II (I I I II
4. Implementation of Circular Arrays
Our next problem is to implement a circular array as an ordinary linear (that is, straight-
I ( rr rrr ~lz I ~ I I I i i rI I [ D
line) array. To do so, we think of the positions around the ci rcle as numbered from O to
Queue
< '7' , '7 '7 z z z
with one
MAX- 1, where MAX is the total number of entries in the circular array, and to implement empty
position
the circular array, we use the same-numbered entries of a linear array. T hen moving the rear front
modulal' arirhmeric indices is just the same as doing modular arithmetic: When we increase an index past
MAX - 1 we start over again at 0. This is like doing arithmetic on a circular clock face;
i Insert an item.
the hours are numbered from I to 12, and if we add four hours to ten o'clock, we obtain
two o'clock.
Perhaps a good human analogy of this linear representation is that of a priest serving
Full
queue
<
I I r z
i(i r r ,1: r t
~~ r I I ( r rr i ( D
:ront
-::,, -::,,
communion to people kneeling at the front of a church. The communicants do not move Figure 3.7. E mpty and fu ll queue.~
until the priest comes by and serves them. When the priest reaches the end of the row,
he returns to the beginning and starts again, since by this time a new row of people have
7. Possible Solutions
come forward.
I. empty position There are at least three essentia11y different ways to resolve this problem. One is to insist
5. Circular Arrays in C
on leaving one empty position in the array so that the queue is considered fu11 when the
Tn C we can increase an index i by I in a circular array by writing 2. jlag rear index has moved within two positions of the front. A second method is to introduce
72 Lists CHAPTER 3 SEC TION 3.3 Queues 73
a new variable. This can be a Boolean variable that will be used when the rear comes 1. Implementation with a Counter
just before the front to indicate whether the queue is full or not (a Boolean variable to
First, we take the method in which a variable count is used to keep track of the number
check emptiness would be just as good) or an integer variable that counts the number
of items in the queue. The queue.h file contains the structure declaration for a queue
3. special values of items in the queue. The third method is to set one or both of the indices to some
and the function prototypes associated with queues:
value(s) that would otherwise never occur in order to indicate an cn1pty (or full) queue.
Tf, for example, the array entries are indexed from O to MAX - 1, then an empty queue
could be indicated by setting the rear index to - I.
#define MAXQUEUE 3
I* DeleteQueue: delete and return item in front of queue. * I We shall use the conditions rear == - 1 and front == 0 to indicate an empty queue.
deletion void DeleteQueue ( ltem_type * item, QueueJype *queue_ptr) The initialization function is
{ I* Initialize: initialize queue. * I
if (queue_ptr->count <= O) initialize void Initialize ( Oueue_type *queue_ptr)
Error ("Queue is empty") ; {
else { queue_ptr->front = O;
queue_ptr->count - - ; queue_ptr->rear = - 1;
* item = queue _ptr->entry [queue _ptr->from]; }
queue _ptr->front = (queue_ptr->front + 1) % MAXQUEUE;
} To write the functi on for adding to the queue, we must first (for error checking) find a
} condition indicating whether the queue is full or not. Fullness is typically indicated by
rear == front - 1 together with rear > - 1, but it is also possible that front == O and
We shall also find use for three functions concerning the size of the queue, all of which
rear >= MAXQUEUE - 1. Building these conditi ons into the function, we obtain
are easy to write with this implementation.
I* Size: return current number of items in the queue. * I I* AddQueue: add item to the queue. * I
size int Size ( QueueJype *queue_ptr) insertion void AddQueue ( ltemJype item, QueueJype *queue_ptr)
{ {
return queue_ptr->count; if ( (queue_ptr- >rear == queue_ptr->front - 1 &&
} queue _ptr- >rear > - 1) II
empty?
I* Empty: returns non-zero if the queue is empty.
Boolean_type Empty(QueueJype *queue_ptr)
*' (queue_ptr->rear == MAXQUEUE - 1 &&
queue _ptr->front == 0))
{ Error( "Queue is full");
return queue_ptr->count <= O; else {
} queue_ptr->rear = (queue_ptr->rear + 1) % MAXQUEUE;
{ }
return queue _ptr->count >= MAXQUEUE;
Since emptiness is indicated by rear == - 1, the deletion function closely resembles the
}
previou s version.
2. Implementation with Special Index Values
Next let us tum to the method that relies on special values for the indices. Most of the deletion
I* DeleteQueue: delete and return item in front of queue. *'
void OeleteOueue(ltemJype *item _ptr, Queue_type * queue_ptr)
necessary changes in declarations are clear. The modified queue.h then becomes {
#define MAXQUEUE 3 if (queue_ptr->rear <= - 1)
typedef char ltem _type; Error ("Queue is empty") ;
else {
typedef struct queueJag {
*item _ptr = queue_ptr->entry [queue_ptr->front] ;
1ype queue int front;
if (queue_ptr->front ! = queue_ptr->rear)
int rear;
queue_ptr->front = (queue _ptr->front + 1) % MAXQUEUE;
ltem _type entry [MAXQUEUE];
} OueueJype;
void AddQueue ( ltemJype, QueueJype *) ;
else {
queue_ptr->front = O;
I* The queue is now empty.
*'
queue_ptr->rear = - 1;
void OeleteQueue ( ltemJype *, Queue_type *) ; }
void Initialize ( Queue_type *) ; }
int Size ( QueueJype *); }
Boolean_type Empty ( Queue_type *) ;
BooleanJype Full ( QueueJype *); The three functions Size, Empty, and Full will be left as exercises.
76 Lists C H AP T E R 3 SEC T I ON 3. 4 Application of Queues: Simulation 77
Exercises El. Suppose that queue_ptr is a pointer to a queue that holds characters and that x, y, z ES. Rewrite the first set of C functions for queue processing from the text, using a
3.3 are character variables. Show the contents of the queue at each step of the following Boolean variable full instead of a counter of items in the queue.
code segments. E9. Write C functions to implement queues in a circular array with one unused entry in
the array. That is, we consider that the array is full when the rear is two positions
a. In itialize ( queue_ptr); b. Initial ze ( queue_ptr) ; before the front; when the rear is one position before, it will always indicate an
AddQueue ('a', queue_ptr); AddQueue ('a', queue_ptr) ; empty queue.
AddQueue('b', queue_ptr); x = 'b';
DeleteQueue ( &x, queue_ptr) ; AddQueue (' x', queue_ptr) ;
AddQueue (x, queue_ptr); DeleteOueue( &y, queue_ptr); Programming Pl. Write a function that will read one line of input from the terminal. The input is
DeleteQueue( &y, queue_ptr); AddQueue (x, queue_ptr) ; Proj ect 3.3 supposed to consist of two parts separated by a colon ': '. As its result your function
DeleteOueue ( &z, queue_ptr) ; DeleteOueue ( &z, queue_ptr) ; should produce a single character as follows:
AddQueue (y, queue_ptr) ;
N No colon on the line.
E2. Suppose that you are a financier and purchase 100 shares of stock in Company X L The left part (before the colon) is longer than the right.
in each of January, April, and September and sell 100 shares in each of June and R The right part (after the colon) is longer than the left.
accounting November. The prices per share in these months were D The left and right parts have the same length but are different.
s The left and right parts are exactly the same.
While one object in a system is involved in some action, other objects and actions
will often need to be kept waiting. Hence queues are important data structures for use
f * simulation of an airport*'
f* This is an outline only; not ready to compile. * I
in computer simulations. We shall study one of the most common and useful kinds of first outline void main ( void)
computer simulations, one that concentrates on queues as its basic data structure. These {
simulations imitate the behavior of systems (often, in fact, called qu~u eing systems) in Queue Jype landing, takeoff;
which there are queues of objects wai ting to be served by various processes. OueueJype *Pl = &landing;
OueueJype *Pl = &takeoff;
Plane_type plane;
int curtime; I* current time unit *I
3.4.2 Simulation of an Airport
As a specific example, let us consider a small but busy airport with only one runway
int endtime;
int i;
I* total number of time units to run
I* loop control variable *'
*I
(see Figure 3.8). In each unit of time one plane can land or one plane can take off, but Initialize (pl) ; I* Initialize landing queue. *I
not both. Planes arrive ready to land or to take off at random times, so at any given
unit of time, the runway may be idle or a plane may be landing or taking off, and there
Initialize (pt); I* Initialize takeoff queue.
for (curtime = 1; curtime <= endtime; curtime++) { *'
may be several planes waiting either to land or take off. We therefore need two queues,
rules which we shall call landing and takeoff, to hold these planes. It is bener to keep a plane
waiting on the ground than in the air, so a small airport allows a plane to take off only if
new plane
ready to land
for (i = 1; i <= RandomNumber(); i ++ ) { I* landing queue
NewPlane ( &plane) ; *'
if ( Full (pl))
there are no planes waiting to land. Hence, after receiving requests from new planes to
land or take off, our simulation will first service the head of the queue of planes waiting
to land, and only if the landing queue is empty will it allow a plane to take off. We shall
else
Refuse (plane); I* Refuse plane if full.
*'
wish to run the simulation through many units of time, and therefore we embed the main
action of the program in a loop that runs for curtime (denoting current time) from 1 to
}
AddQueue (plane, pl); I* Add to landing queue.
*'
a variable endtime. With this notation we can write an outline of the main program. new plane
ready to take off
for (i = 1; i <= RandomNumber(); i++) { I* takeoff queue
NewPlane ( &plane); *'
if ( Full (pt))
-- ---
. . . ___ _ __ ~~
1 ,..--
--
I?
----r--------"*
~- ---
Landing queue -
'
'I
~ }
else
Refuse (plane) ;
AddQueue(plane, pt);
. ' ..' .
--c".,...,..,--
- -
}
Conclude ( ) ;
error checking if ( *expectarrive < 0.0 II *expectdepart < 0.0) { 4. Processing an Arriving Plane
printf ( "These numbers must be nonnegative.\n");
ok = FALSE;
} else if ( *expectarrive + *expectdepart > 1.0) { I * Land: process a plane p that is actually landing. *'
void Land ( Plane_type p, int curtim e, int * nland, int *landwait)
print! ("The airport will become saturated. "
"Read new numbers?\n"); {
ok = ! Enquire ( ) ; I* If user says yes, repeat loop. int wait;
} else wait = curtime - p.tm;
ok = TRUE; printf("%3d: Plane %3d landed; in queue %d units.\n",
} while (ok == FALSE) ; curtime, p.id, wait);
} ( * nland) ++;
*landwait += wait;
2. Accepting a New Plane }
I* NewPlane: make a new record for a plane, update nplanes. * ' 5. Processing a Departing Plane
void NewPlane (PlaneJype *P, int *nplanes, int curtime,
ActionJype kind)
{
( *nplanes) ++ ;
f* Fly: process a plane p that is actually taking off. *'
void Fly(PlaneJype p, int curtime, int *ntakeoff, int * lakeoffwait)
p - >id = *nplanes; {
p- >tm = curtime; int wait;
switch (kind) { wait = curtime - p.tm;
case ARRIVE: printf("%3d : Plane %3d took off; in queue %d units.\n",
print! (" Plane %3d ready to land. \n", *nplanes); curtim e, p.id, wait);
break; ( *ntakeoff) ++;
case DEPART: *lakeoffwait += wait;
print!(" Plane %3d ready to take off.\n", *nplanes); }
break;
}
} 6. Marking an Idle Time Unit
{ the number of seconds elapsed modulus 10000. This number provides a different starting
printf ( "Simulation has concluded after %d units.\n", endtime); point for srand each time it is run.
printf("Total number of planes processed: %3d\n ", nplanes); We can then use the standard system function rand for producing each pseudorandom
printf (" Number of planes landed: %3d\n", nland) ; uniform distrib11tion number from its predecessor. The function rand produces as its result an integer number
printf (" Number of planes taken off: %3d\n", ntakeoff) ; between O and INT_MAX. (Consult the file limits.h 10 determine the value of INT.MAX on
printf (" Number of planes refused use: %3d\n", nrefuse); your system.) For our simulation we wish to obtain an integer giving the number of
printf (" Number left ready to land : %3d\n " , Size(pl)); planes arriving ready to land (or take off) in a given time unit. We can assume that
printf (" Number left ready to take off: %3d\n" , Size (pt)); the time when one plane enters the system is independent of that of any other plane.
if (endtime > O) Poisson distrib11tio11 The number of planes arriving in one unit of time then follows what is called a Poisson
printf(" Percentage of time runway idle: %6.2f\n 11 , distribution in statistics. To ca lculate the numbers, we need to know the expected value,
((double) idletime/endtime) * 100.0); that is, the average number of planes arriving in one unit of time. If, for example, on
if (nland > O) average one plane arrives in each of four time units, then the expected value is 0.25.
print! ( 11 Average wait time to land : %6.2f\ n", Sometimes several planes may arrive in the same time unit, but often no planes arrive,
(double) landwait/nland) ; so that taking the average over many units gives 0.25.
if ( ntakeoff > 0) The following function determines the number of planes by generating pseudoran-
printf ( 11 Average wait time to take off: %6.2f\n", dom integers according to a Poisson distribution:
(double) takeoffwait/ ntakeoff) ;
} I* RandomNumber: generate a pseudorandom integer according to the Poisson distri-
bution. * I
Poisson generator int RandomNumber (double expectedvalue)
3.4.5 Random Numbers {
A key step in our simulation is to decide, at each time unit, how many new planes become
int n = O;
double em;
I* counter of iterations
I* e-v. where v is the expected value *I
*'
ready to land or take off. Although there are many ways in which these decisions can double x; I* pseudorandom number *I
be made, one of the most interesting and useful is to make a random decision. When
the program is run repeatedly with random decisions, the results wi ll differ from run to em = exp( - expectedvalue);
run, and with sufficient experimentation, the simulation may display a range of behavior x = rand () I (double) l~L MAX;
not unlike that of the actual system being studied. while (x > em) {
system random Many computer systems include random number generators, and if one is available n++ ;
number generator x * = rand( )/(double) INT_MAX;
on your system, it can be used in place of the one developed here.
}
The idea is to start with one number and apply a series of arithmetic operations
return n;
that will produce another number with no obvious con!lcction to the first. Hence the
}
numbers we produce are not truly random at all, as eact one depends in a definite way
seed for on its predecessor, and we should more properly speak of pseudorandom numbers. If
pseudorandom we begin the simulation with the same value each time the program is run , then the
numbers whole sequence of pseudorandom numbers will be exa:tly the same, so we nonnally
begin by setting the starting point for the pseudorandom integers to some random value,
3.4.6 Sample Results
for example, the time of day: We conclude this section with the outpu t from a sample run of the airport simulation.
You should note that there are periods when the runway is idle and others when the
I* Randomize: set starting point for pseudorandom integers.
void Randomize(void)
*' queues are completely full, so that some planes must be turned away.
Programming Pl. Experiment with several sample runs of the airport simu lation, adjusti ng the values
Projects for the expected numbers of planes ready to land and take off. Find approximate
values for these expt:eted numbers that are as large as possible subject to the con-
3.4
dition that it is very unlikely that a plane must be refused service. What happens
to these values if the maximum size of the queues is increased or decreased?
P2. Modify the simulation to give the airport two runways, one always used for landings
and one always used for takeoffs. Compare the tolal number of planes that can be
served with the number for the one-runway airport. Does it more than double?
P3. Modify the simulation to give the airport two runways, one usually used for landings
and one usuall y used for takeoffs. Tf one of the queues is empty, then both rnnways
can be used for the other queue. Also, if the landing queue is full and another plane
arrives to land, then takeoffs will be stopped and both runways used to clear the
backlog of landing planes.
P4. Modify the simulation to have three runways, one always reserved for each of Figure 3.9. A random walk
landing and takeoff and the third used fur landings unless the landing queue is
empty, in which case it can be used for takeoffs. upper left comer of the grid. Mod ify the simulation to see how much faster he
PS. Modify the original (one-runway) simulation so that when each plane arrives to land, can now get home.
it will (as pa11 of its structure) have a (randomly generated) fuel level, measured c. Modify the original simulation so that, if the drunk happens to arrive back at
in units of time remaining. If the plane does not have enough fuel to wai t in the the pub, then he goes in and the walk ends. Find out (dependi ng on the size
queue, it is allowed to land immediately. Hence the planes in the landing queue and shape of the grid) what percen tage of the time the drunk makes it home
may be kept waiting additional units, and so may ru n out of fue l them selves. Check successfull y.
this out as part of the landing function, and find abou t how busy the airport can get d . Modify the origi nal simulation so as to give the drunk some memory to help
before planes stai1 to crash from running out of fuel. him. as fo llows. Each time he arrives at a corner, if he has been there before
ranitom numbers P6. Write a stub to take the place of the random number function . The stub can be used on the current walk, he remembers what streets he has already taken and tries
both to debug the program and to allow the user to control exactly the number of a new one. If he has already tried all the streets from the corner, he decides
planes arriving for each queue at each time unit. at random which to take now. How much more qu ickl y does he get home?
The main program and the remai ning functions are the same as in the previous
P7. Write a dri ver program for functi on RandomNumber; use it to check th at the func-
version.
tion produces random integers whose average over the number of iterations per-
fom1ed is the specified expected value.
scissors-paper-rock PS. In a certain children's game each of two players simultaneously puts out a hand held 3.5 OTHER LISTS AND THEIR IMPLEMENTATION
in a fashion to denote one of scissors, paper, or rock. The rules are that scissors
beats paper (since scissors cut paper), paper beats rock (since paper covers rock),
and rock beats scissors (since rock breaks scissors). Write a program to simulate 1. General lists
playing this game with a person who types in S, P, or R at each turn. Stacks are the easiest kind of list to use because all additions and delet ions are made at
P9. After leaving a pub a drunk tries to walk home. The streets between the pub and one end of the list. In queues changes are made at both ends, but only at the ends, so
random walk the home form a rectangular grid. Each time the drunk reaches a corner he decides queues are still relatively easy to use. For many applicat ions, however. it is necessary
at random what direction to walk next. He never, however, wanders outside the to access all the elements of the list and to be able to make insertions or deletions at
grid. any point in the list. It might be necessary, for example, to insert a new name into the
a. Write a program to simulate this random walk. The number of rows and middle of a list that is kept in alphabetica l order.
col umns in the grid should be variable. Your program should calculate, over
many random walks on the same grid, how long it takes the drunk to get home 2. Two Implementations
on average. In vestigate how this number depends on the shape and size of the The usua l way of implementing a list, the one we wou ld probably first th ink of, is to keep
grid.
I . coumer the en tries of the list in an array and to use an index that counts the number of entries in
b. To improve his chances, the drunk moves closer to the pub-to a room on the
90 Lists C H AP TER 3 SECTI O N 3.5 Other Li sts and their Implementation 91
the list and allows us to locate its end. Variations of this implementation are often useful, status operation Boolean_type Empty(LisUype *list);
however. Instead of using a counter of elements, for example, we can sometimes mark BooleanJype Full (LisUype *list);
2. special value entries of the array that do not contain list elements with some special symbol denoting int Size ( LisUype *list) ;
emptiness. In a list of words, we might mark all unused positions by setting them to
blanks. In a list of numbers, we might use some number guaranteed never to appear in For most other operations we must specify the place in the list to use. At any instant we
the list. are only looking at one entry of the list, and we shall refer to this entry as the window
window into a list into the list. Hence, for a stack the window is always at the same end of the list; for a
If it is frequently necessary to make insertions and deletions in the middle of the
queue, it is at the front for deletions and at the rear for additions. For an arbitrary list we
relative advantages list, then this second implementation will prove advantageous. If we wish to delete
can move the window through the list one entry at a time, or position it to any desired
an element from the middle of a list where the elements are kept next to each other,
entry. The following are some of the operations we wish to be able to do with windows
then we must move many of the remaining elements to fill in the vacant position, while
and lists:
with the second implementation, we need only mark the deleted position as empty by
putting the special symbol there. See Figure 3.10. If, later, we wish to insert a new
element into a list in the first implementation, we must again move many elements. With
I* Initialize: initialize the list to be empty.
void Initialize ( LisUype *list);
*'
the second implementation, we need only move elements until we encounter a position
I* lsFirst: non-zero if w is the first element of the list. *I
marked empty. With the first implementation, on the other hand, other operations are
Boolean_type lsFirst(LisUype *list, int w);
easier. We can tell immediately the number of elements in the list, but with the second
implementation, we may need to step through the entire array counting entries not marked I* lsLast: non-zero if w is the last element of the list.
Boolean_type lslast(LisUype *list, int w);
*'
empty. With the first implementation, we can move to the next entry of the list simply
by increasing an index by one; with the second, we must search for an entry not marked window positioning I* Start: position the window at the first entry of list. * I
empty. void Start (LisUype *list, int *W);
I* Next: position window on the next entry, if there is one. * I
void Next (LisUype *list, int *W);
hen
------
------
hen
pig
nul
do~
null
dog
list changes
void Finish ( LisUype *list, int *W);
pig
------ hen
null
hen
null
void Delete (LisUype *list, int *w);
I* lnsertAfter: insert item after the window. *'
void lnsertAfter(LisUype *list, int *W, ltemJype item) ;
pig pig
I* lnsertBefore: insert item before current window. * I
void lnsertBefore (LisUype *list, int *W, ltem_type item);
count = 5
First implementation
count = 4
Second i1nplementation
I* Replace: replace the item at the window w.
void Replace(Lisuype *list, int w, ltem_type item);
*'
w. *'
Figure 3.10. Deletion from a list
I* Retrieve: retrieve an item at the window
void Retrieve(LisUype list, int w, ltemJype *item);
3. Operations on Lists 4. List Traversal
Because variations in implementation are possible, it is wise in des igning programs to One more action is commonl y done with lists-traversal of the list-which means to
separate decisions concerning the use of data strucwres from decisions concerning their start at the beginning of the list and do some action for each item in the list in turn,
implementation. To help in delineating these decisions, let us enumerate some of the traverse and visit finishing with the last item in the list. What act ion is done for each item depends on the
operations we would like to do with lists. application, for generality we say that we visit each item in the list. Hence we have the
First, there are three functions whose purposes are clear from their names: final function for list processing:
92 Lists CHAPTER 3 SECTION 3.5 Other Lists and their Implementation 93
I* Traverse: traverse list and invoke Visit for each item. *I I* Delete: delete entry at window and update window. * I
void Traverse(LisUype *list, void ( * Visit) (ltem_type x)); delerion. version I void Delete (LisUype *lisLptr, int *W)
{
Jimction as an (Yes, this declaration is standard C; pointers to functions are allowed as formal parameters int i;
argument for other functions, although this feature is not often u&ed in elementary programming.)
When function Traverse finishes, the window has not been changed. To be sure of for (i = *W + 1; i < list_ptr- >count; i++)
proper functioning, it is necessary to assume that function Visit makes no insertions or lisLptr->entry [ i - 1) = lisLptr->entry [i);
deletions in the list list. lisLptr->count - - ;
if ( Empty ( lisLptr))
5. C Programs Start ( lisLptr, w) ;
Let us now finally tum to the C details of the two implementations of lists introduced else if ( *W > Size ( lisLptr) )
earlier. Both methods will require the declaration of a constant MAXLIST giving the Finish ( lisLptr, w);
maximum number of items allowed in a list and typedefs used. The first method then }
requires the following structure and typedefs:
I* Delete: delete entry at window and update window. * I
#define MAXLIST 10 delerio11, versio11 2 void Delete(LisUype *lisLptr, int *W)
{
typedef char ltemJype;
BooleanJype flag;
type list typedef struct lisUag {
flag= lslast(lisLptr, w);
int count;
lisLptr->entry [ *W) = (ltemJype) NULL;
ltem_type entry [MAXLISlJ ;
if ( flag)
} LisUype;
Start (lisLptr, w);
The second method replaces the last declaration with else
Next (lisLptr, w);
typedef struct lisUag { }
ltemJype entry [ MAXLIST) ;
} LisUype; A lthough there remai n a good many functions to write in order to implement these list
structures fully in C, most of the details are simple and can safely be left as exercises. It
To show the relative advantages of the two implementations, let us write the functions is, of course, pem1issible to use functions already written as part of later functions. To
Size and Delete for each implementation. illustrate this. let us conclude this section with a version of function Traverse that will
f* Size: return the size of a list. * I work with either implementation.
size, version] int Size(LisUype * list)
{ I* Traverse: traverse the list and invoke Visit for each item. * I
void Traverse(LisUype * lisLptr, void ( *Visit) (ltemJype))
return list->count;
{
}
int w;
I* Size: return the size of a list. * I ltemJype item;
size, version 2 int Size(LisUype *lisL ptr)
if ( ! Empty(lisLptr)) { I* only if not empty *I
{
Start ( lisLptr, &w);
inti;
while ( ! lslast (lisLptr, &w)) {
int count= O;
Retrieve (lisLptr, &w, &item);
for (i = O; i < MAXLIST; i++ ) ( *Visit) (item) ;
if ( lisLptr- >entry [i] ! = ' \O' )
count++ ;
return count;
}
Next(lisLptr, &w); I* next position in the list
*'
Retrieve (list.ptr, &w, &item); I* Get last item. *I
} ( tVisit) (item);
As you can see, the fi rst version is somewhat simpler. On the other hand, for the Delete }
function, the second version is simpler. }
94 Lists CHAPTER 3 SECTION 3 . 5 Other Lists and their Implementation 95
As an example, let us write a simple version of the function Visit. We shall assume that track. Devise and draw a railway swi tching network that will represent a deque.
the it.e ms are characters , and we simply pri nt the charact~r. The network should have only one entrance and one exit.
~
4.3.2 Comparison of Review Questions 146
Implementations 120
4.3.3 Programming Hints 121 References for Further Study 146
u Marsha
~
Jackie
295-0603
Feb. 18
!:r~
~T
!ffl
A pointer is simply a variable giving the location of some item, and for contiguous lists,
we have in fact been using pointers informally throughout the last chapter. The variable
top is a pointer givi ng the location of the item on the top of a stack, and the variables
front and rear give the locations of the front and rear of a queue. To avoid possible
ill
confusion, however, we shall generally reserve the word pointer for use with linked lists
Carol '",. Tom
~
•;;.
Rene I
and continue 10 use the word index (and, later in this chapter, the word cursor) to refer
'- 628-5100 ' 286-2 139 342-5 153 ' !I
Feb. 23
"
if: ' Feb. 28 ffil i
Figure 4.2. A linked list
Mar. 15 111ff, ll * to a location within an array.
C sets stringent rules for the use of pointers. Each pointer is bo11nd to the type of The creation and destruction of dynamic variables is done with standard functions in C.
variable Lo which it points, and the same pointer should not be used t.o point (at different If p has been declared as a pointer to type NodeJype, then the statement
times) to variables of different types. Variables of two d ifferent pointer types cannot be
mixed with each other; C will allow assignments between two pointer variablt:~ of th..: p = ( NodeJype *) malloc(sizeofC NodeJype ));
same type, but it will produce a warning when you assign pointers of different types. If
we have declarations creates a new dynamic variable of type NodeJype and assigns its location to the pointer
malloc p. The library function malloc allocates a block of memory and returns a pointer to
char *a, *b;
that block of memory, or it returns NULL if there is not enough remaining memory to
NodeJype *X, * Yi
satisfy the request. The argument to malloc indicates, in bytes, the size of the block of
then the assignments x = y; and a = b; are legal, buL the assignment x = a; is illegal. sizeof memory that is requested. For o ur application, we determine this size by using the unary
C HAPTER 4 SECT I ON 4.1 Dynamic Memory Allocation and Pointers 105
104 Linked Lists
compile-time operator sizeof, which computes the size in bytes of any object or type.
Hence,
sizeof ( Node_type)
PG,__,,__,,1' Music O
calculates the number of bytes occupied by a variable of Node.type. The placement of
type cast (Node.type *) before the call to malloc is a type cast: The value returned by malloc
is a generic poimer; the type cast forces it to become Node-type.
q Gf------<•~1' Calculus O
If there is insufficient memory to create the new dynamic variable, mal loc will fail
and will return NULL. Your program should always check for a NULL pointer returned by
malloc. ,
~ PG 0
free When a dynamic variable is no longer needed, the function call
free(p);
p
P • o; • p = •q;
·I, Calculus
returns the space used by the dynamic variable to which p points to the system. After
the function free (p) is called, the pointer variable pis undefined, and so cannot be used
q Calculus qG ·I Calculus
D
until it is assigned a new value. 1l1ese actions are illustrated in Figure 4.3. Figure 4.4. Assignment of poi nter variables
Either a call p = malloc (. . . ) or an assignment such asp = q or p = NU LL is required
before p can be used. After a call free(p), the value of pis undefined, so it is wise to poimer operations checked for equali ty, can appear in calls to functions, and the programmer is allowed to do
set p = NULL immediately, to be sure that p is not used with an undefined value. some limited ari thmetic with pointers. Appendix C gives more information on pointers.
assig11me111 In regard to assignment statements, it is important to remember the difference be·
6. Following the Pointers
tween p = q and *P = *q, both of which are legal (provided that p and q are bound to
The star * that appears in the declaration of a C pointer can also be used in a statement the same type), but which have quite different effects. The first statement makes p point
that uses the pointer. For example, the declaration to the same object to which q points, but does not change the value of either that object,
or of the other object that p was pointing to. The latter object will be lost unless there is
char *P; some other pointer variable that still refers to it. The second statement, *P = *q, on the
contrary , copies the val ue of the object *q into the object *P, so that we now have two
is read "p is a pointer to char" and when we use *P in an expression we read "what p
dereferencing points to." Again, the words link and reference are often used in this connection. The objects wi th the same va lue, with p and q pointing to the two separate copies. Finally, the
two' assignment statements p = *q and *P = q have mix_ed types and are illegal (except
action of taking *P is sometimes called "dereferencing the pointer p."
in the case that both p and q point to pointers of their same type!). Figure 4.4 illustrates
7. Restrictions on Pointer Variables these assignments.
The only use of variables of type Item.type * is to find the location of variables of
type Item.type. Thus pointer variables can participate in assignment statements, can be Exercises These exercises are based on the following declarations, where the type Node.type is
4.1 declared as follows.
~----~
·~r- ???
O p = malloc {size of (Node._1ype)I;
struct nodeJag *next;
} NodeJype;
NodeJype *P, *q, *r;
NodeJype x, y, z;
~ - - -- -• ~ p-mombor = 1378; Et. For each of the following s1a1emems, ei ther describe its effect, or state why it is
illegal.
v
Aln
-:: · ~ ·- ??
00 "~'"'
Figure 4.3. Creating a nd disposing of dynamic variables
a. p = (Node.type * ) malloc(sizeof( NodeJype));
b. *q = ( NodeJype *) malloc (sizeof ( Node_type));
c. x = (Node.type * ) malloc(sizeof(Node_type));
d . p = r;
CHAP T ER 4 S E C TION 4 .2 Linked Stacks and Queues 107
106 Linked Lists
Perhaps an analogy with reading a magazine article will help. If we are in the
e. q = y; j. free(*p) ;
middle of reading an article, then upon reaching the bottom of a page we often find the
= NULL;
f. r k. free(r);
instruction "Continued on page . .. ," and by following such instructions we can continue
g. z = *p; I. *q = NULL;
reading until we reach the end of the article. But how do we find the beginning? We look
h. p = *X; m. *P = *x;
i. free(y); II . z = tlULL; in the table of contents. which we expect to find in a fixed location near the beginning
of the magazine.
swap E2. Write a C function to interchange pointers p and q, so that after the function is For linked lists also we must refer to some fixed location to find the beginning;
performed, p will point ro the node to which q formerly pointed, and vice versa. that is, we shall use a static variable to locate the first node of the list. One method
E3. Write a C function to interchange the values in the dynamic variables to which p static head of doing this is to make the first node in the list a static variable, even though all the
and q point, so that after the function is perfonned * P will have the value formerly remaining nodes are dynamically all ocated. In this way the first node will have a unique
in *q and vice versa. name to which we can refer. Although we shall sometimes use this method, it has the
E4. Write a C function that makes p point to the same node to which q points, and disadvantage that the first node of the list is treated differently from all the others, a fact
disposes of the item to which p formerly pointed. that can sometimes complicate the algorithms.
ES. Write a C function that creates a new variable with p pointing to it, and with contents
the same as those of the node to which q points. 3. Headers
For linked stacks and queues and sometimes for other kinds of linked lists, we shall
employ another method that avoids these problems. The header for a linked list is a
pointer variable that locates the beginning of the list. The header will usually be a static
4.2 LINKED STACKS AND QUEUES variable, and by using its value we can arrive at the first (dynamic) node of the list. The
With these tools of pointers we can now begin to consider the implementation of linked header is also sometimes called the base or the a11chor of the list. These terms are quite
lists into C. Since stacks and queues are among the easiest lists ro process, we begin descriptive of providing a variable that ties down the beginning of the list, but si nce they
with them. are not so widely used, we shall generally employ the term header.
initialization When execution of the program starts we shall wish to initialize the linked list to
4.2.1 Declarations be empty; with a header pointer, this is now easy. The header is a static pointer; so it
exists when the program begins, and to set its value to indicate that its list is empty, we
need only the assignment
1. Nodes and Type Declarations
header= NULL;
Recall from Figure 4.2 that each entry of a linked list will be a structure containing
not only the items of infonnation but also a pointer to the next structure in the list.
Translating this requirement into C declarations yields
4. The End of the List
typedef struct node.tag {
ltem_type info; Finding the end of a linked list is a much easier task than is finding its beginning. Since
struct node.tag * next; each node contains a pointer to the next, the pointer field of the last node of the list has
} NodeJype; nowhere to point, so we give it the special value NULL. In this way we know that we are
at the end of the list if and only if the node we are using has a NULL pointer to the next.
use before Not.e that we have a problem of circularity in this declaration. struct nodeJag * appears Here we have one small advantage of linked lists over a contiguous implementation:
declaration in the declaration of the structure node. This is called a self-refere11tial structure. It There is no need to keep an explicit counter of the number of nodes in the list.
means that the pointer next may point to a structure of the same type as the structure
NodeJype.
4.2.2 Linked Stacks
2. Beginning of the List
Let us now tum to writing functions for processing linked stacks. As with the items of
In our linked list we shall use the pointer next to move from any one node in the contiguous stacks, we shall push and pop nodes from one end of a linked stack, called its
linked list to the next one, and thereby we can work our way through the list, once we top. We now face a small problem of inconsistency with the implementation of stacks in
have started. We must now, however, address a small problem that never arises with the last chapter: The entries of contiguous stacks were declared to have type ltemJype,
contiguous lists or other static variables and arrays: How do we find the beginning of items and nodes whereas the entries of linked stacks will have type Node_type, which we have already
the list?
108 Linked Lists CHAPTER 4 SECTION 4.2 Linked Stacks and Queues 109
declared to consist of an ltemJype together with a pointer. For some applications of Node processing Next let us turn to the processing of nodes in a linked stack. The first question to settle
linked stacks we wish to process nodes, but for other ipplications we wish to be able is to determine whether the beginning or the end of the linked list will be the top of the
t.o process items directly, so that we can substitute a linked implementation of stacks stack. At first glance it may appear that (as for contiguous lists) it might be easier to add
for a contiguous implementation without having to make any changes in the rest of the a node at the end of the list, but this method makes popping the stack difficult: There
program. is no quick wuy to find the node immediately before a given one in u linked list, since
For this reason we introduce two new functions, PushNode and PopNode, which the pointers stored in the list give only one-way directions. Thus, after we remove the
will process nodes in a linked stack. We can then use these functions to write linked last element, to find the new element at the end of the list it might be necessary to trace
versions of the functions Push and Pop, which, as in the last chapter, will process items all the way from the head of the list. To pop our linked stack it is better to make all
directly. If we have an it.em item that. we wish to push onto a linked stack, we must first additions and deletions at the beginning of the list. Each linked list has a header variable
make a new node and put item into the node, and the:1 push the node onto the stack. that points to its first node; for a linked stack we shall call this header variable top, and
Hence we obtain: it will always point to the top of the stack. Since each node of a linked list points to
the next one, the onl y information needed to keep track of a linked stack is the location
item processing
I* Push: make a new node with item and push it onto stack.
void Push ( ltemJype item, StackJype *Stack_ptr)
*' of its top. For consistency with other data structures, we shall put this information in a
simple structure, and hence we obtain the following declaration of a linked stack:
{
Push Node (MakeNode (item), stack_ptr); typedef struct stack.tag {
} NodeJype *lop;
} StackJype;
The function Push uses a function MakeNode that allocates enough space for a new
node and initializes it. This sequence of instructions i s used often, so it i s kept as a Then we can declare the stack and a pointer to it:
function for general use.
StackJype stack;
I* MakeNode: make a new node and insert item. *I StackJype *stack_ptr = &stack;
NodeJype *MakeNode(ltem.type item)
{ and a pointer node_ptr to a structure of the type Node_type
NodeJype *P;
NodeJype *node_ptr;
if ( (p = (NodeJype *) malloc(sizeof(Node_type))) == NULL)
Error ("Exhausted memory.") ;
else { Let us start with an empty stack, which now means
p->info = item;
p->next = NULL; stack_ptr->top = NULL;
}
return p; and add the first node. We shall assume that this node has already been made somewhere
} in dynamic memory, and we shall locate this node by using a pointer node_ptr. Pushing
node. ptr onto the stack consists of the instructions
The connection between the two functions for popping a node or an it.em from the stack
is just as close. stack_ptr->top = node_ptr;
node _ptr->next = NULL;
I* Pop: pop a node from the stack and return its item.
void Pop (ltemJype *item, StackJype *Stack_ptr)
*'
As we continue, let us suppose that we already have the stack and that we wish
{ to push a node node_ptr onto it. The required adjustments of pointers are shown in
NodeJype *node.ptr; Figure 4.5. First, we must set the pointer coming from the new node node_ptr to the old
PopNode( &node_ptr, stack_ptr); top of the stack, and then we must change the top to become the new node. The order
*item = node_ptr->info; of these two assignments is important: If we attempted to do them in the reverse order,
free(node_ptr); the change of top from its previous va lue would mean that we would lose track of the
} old part of the list. We thus obtain the following function.
110 Linked Lists CHAPTER 4 SECTION 4 .2 Linked Stacks and Queues 111
Node_type *node_ptr
stack_ptr ' t op
Old Old Old
top node se<:ond node bottom node It indicates that node_ptr is a pointer to a variable of type Node_type. In PopNode the
first parameter is
Figure 4.5. Pushing a node onto a linked stack NodeJype **node_ptr
push node
f* PushNode: push node onto the linked stack. *'
void PushNode ( NodeJype * node_ptr, StackJype *Slack _ptr)
It indicates that node_ptr is a pointer to a pointer to a variable of type Node_type. Re-
member that in a call by reference we pass 1he address of the variable. When we need
to make a call by reference and the variable involved is a pointer, we pass the address
{
of the pointer. The parameter to the function is then a pointer to a pointer to a variable.
if (node_ptr == NULL)
Note that the principal instructions for popping the linked stack are exactl y the
Error ("Attempted to push a nonexistent node 11 ) ;
reverse of those for pushing a node onto the stack. In popping the stack, it is necessary
else {
to check the stack for emptiness, but in pushing it, there is no need to check for overflow,
node_ptr->next = stack_ptr->top;
since the function itself does not call for any additional memory. The extra memory for
stack_ptr->top = node_ptr;
the new node is already assigned to the node to which node_ptr points.
}
}
In all our functions it is important to include error checking and to consider extreme
4.2.3 Linked Queues
cases. Hence we consider it an error to attempt to push a nonexistent node onto the In contiguous storage, queues were significantly harder to manipulate than were stacks,
stack. One extreme case for the function is that of an empty stack, which means top == because it was necessary to treat straight-line storage as though it were arranged in a
NULL. Note that in this case the function works just as well to add the first node to an circle, and the extreme cases of full queues and empty queues caused difficulties. It is
empty stack as to add another node to a nonempty stack. for queues that linked storage really comes into its own. Linked queues are just as easy
It is equally simple to pop a node from a linked stack. This process is illustrated in to handle as are linked stacks. We need only keep two pointers, front and rear, that
Figure 4 .6 and is implemented in the following function. will point, respectively, to the beginning and the end of the queue. The operations of
insertion and deletion are both illustrated in Figure 4.7.
rype queue To show the logical connection between the head and tai l pointers of a linked queue,
we shall introduce a structure type:
insert node
f* AddNode: add a node to the rear of the queue.
void AddNode(Node_type *node_ptr, QueueJype * queue_ptr)
*' E3. For cont iguous stacks and queues we wrote Boolean-valued functions Empty and
Full along with the various functions. Write analogous functions in C for (a) linked
stacks and (b) linked queues.
{
if (node _ptr == NULL) E4. By allocating and freeing new nodes, write functions (a) AddQueue and (b) Delete·
Queue that will process items directly for linked queues and that can be substituted
Error ( 11 Attempted to push a nonexistent node");
else if (queue_ptr->front == NULL) { directly for their contiguous counterparts.
queue_ptr->front = node_ptr; A circularly linked list, illustrated in Figure 4.8, is a linked list in which the node at
queue_ptr->rear = node_ptr; circularly linked lis1 the tail of the list, instead of having a NULL pointer, points back to the node at the head
} else { of the list. We then need only one pointer tail to access both ends of the list, since we
queue_ptr->rear->next = node _ptr; know that tail->next points back to the head of the list.
queue_ptr->rear = node_ptr;
}
}
tail
Note that this function includes error checking to prevent the insertion of a nonexistent .__..
node into the queue. The cases when the queue is empty or not must be treated separately,
since the addition of a node t.o an empty queue requires setting both the front and the
rear to the new node, while addition to a nonempty queue requires changing only the ( J
rear.
To remove a node from the front of a queue, we use the following function: Figure 4.8. A ci rcul arly linked list with tail pointer
114 Linked Lists CH AP TER 4 SECT I ON 4 .3 Further Operations on Linked Lists 115
ES. If we implement a queue as a circularly linked list then we need only one pointer
rear (or tail) to locate both the front and the rear. Write C functions to process a <' <'
queue stored in this way:
a. Initialize.
(a)
[ fl]Stacks
·I are
rrJ ·I lists.
Sh
b. AddNode.
ru 1~,}~" , ., . '1
c. DeleteNode.
J
r
<' <'
<'
deque E6. Recall (Section 3.5, Exercise 3) that a deque is a list in which additions or deletions
can be made at either the head or the tai1, but not elsewhere in the list.. With a
deq ue stored as a circularly linked list, three of the four algori thms to add a node to
(b)
I Stacks
·I are
f.[J lists.
either end of the deque and to delete a node from either end become easy to write,
but the fourth does not.
a. Which of the four operations is difficult? Why?
4i,tt,
(c) Stacks are
b. Write a C function to initialize a circularly linked deque 10 be empty. structures. simple
As a first example let us write a function to perform list. traversal, whic.h means to move
for (p =head; p; p = p->next)
( >1<Visit) (p);
through the list, visiting each node in tum. What we mean by visiting each node depends }
entirely on the application; it might mean printing out some information, or doing some
task that depends on the data in the node. Thus we leave the task unspecified and, as in Notice the close analogy between traversal of a linked list and traversal of a contiguous
Section 3.5, use a call to a function named Visit. To traverse the list, we shall need a list. If head and tail are indices marking the start and finish of a contiguous list inside
local pointer p that will start at the first node on the list and move from node to node. an array list of items, then traversal of the contiguous list is performed by the following
We need a loop to accomplish this movement, and since we wish to allow the possibility function:
116 linked lists CHAPTER 4 S ECTION 4 .3 Further Op erations on linked lists 117
I* Traverse: traverse the list visiting one node at a time. *I T he action of this function is illustrated in Figure 4.11.
comi11uous traversal void Tra verse ( int head, int tail, Node_type list [],
void ( *Visit) ( NodeJype *))
{
int p;
for ( p = head; p <= tail; p++)
( *Visit) ( &list [p));
}
Q ~ next
Comparing these versions of traversal, we see that the initialization is the same, the p -next
general structure is the same, and the instruction p = p->next replaces p + + as the way
p
to advance one node through the list. Figure 4.10 illustrates this process.
p ~ next q
p • p ~ next
node p that is not the first one, however, then to adjust the links, we must locate the link
comi ng into p. In a simply linked list with only one poi nter, this task is difficult, but
with a second pointer r o ne step behi nd p, it is easy. After includ ing appropriate e rror
checki ng, we obtain the following funct ion:
rwo-poimer dele1ion
I* Delete: delete node p from the list; r is in front of p.
void De lete ( Node.type *r, Node.type * P)
*'
{
if (r == NULL II p == NULL)
Error ( 11 At least one of nodes r and p is nonexistent 11 ) ;
q
else if (r->next ! = p)
Error( 11 Nodes rand p not in sequence") ;
else
r->next = p->next;
p
}
Figure 4.12. Insertion into a linked list with two pointers
This function as written keeps no pointer to the node bei ng deleted, bu t does not d ispose
of it either. Instead, the function assumes that the deleted node is to be used by the
f* lnsertBetween: insert q between the nodes r and p.
void lnsertBetween ( Node.type *q, Node_type * * head,
*' calling program and the ca ll ing program will keep some pointer o ther than p with which
insertion hetween to find the deleted node.
pointers Node.type **r, Node.type *P)
{ 6. Other Implementations
if (p == * head ) {
q- >next ~ p; Many variations are possible in the implementation of linked lists, some of which prov ide
*r = * head = q; advantages in certain situations. In addi tion to the one-pointer and two-poi nter simply
} else {
I* q is at the head of the list.
*' circularly linked lis1
linked lists, we might, for example, consider circularly linked lists in which the last node,
rather than containing a NULL link, has a link to the first node of the list (see Figure 4.8
q- >next = p;
( *r) ->next = q; and Exercise E6 of Section 4.2).
*r = q;
I* q is after rand before p.
*' doubly linked list Another form of data structure is a doubly linked list (like the one shown in
Figure 4. 13) in which each node contains two links, one to the next node in the list and
}
} one to the preceding node. It is thus possible to move either direction through the list
wh ile keeping only one pointer. With a doubly linked list, traversals in either di rection,
insert.ions, and deletions from arbitrary positions in the list can be programmed without
4. The Two-Pointer Implementation d ifficulty. The cost of a doubly linked list, of course, is the extra space requi red in each
In writing the function j ust finished we have introduced a subtle change in the structure node for a second li nk.
of o ur linked list, a change that reall y amounts to a ne w imple mentation of the list, Yet one more variation in implementation is to include a dummy node at the be-
new implememation o ne that requires keepi ng pointers to two adjacent nodes of the list at all times. In this dummy node ginning (or, less commonly, at the end) of a linked list. This d ummy node contains no
impleme ntation, some operations (like insertion) become easier, but others may become information and is used only to provide a link to the first true node of the list. It is never
harder. In list tra versa!, for e xample, we must be more careful to get the process s t.art.ed
correctly. When p poi nts to the head of the list, the n r is necessarily undefined. Thus the
first step of traversal m ust be considered separately. T he de tails are left as an exercise.
Similarl y, insertion of a new node before the curre nt head of the list is a special case, -
but an easy one already considered in s tudying linked stacks.
/ /
- -
5. Deletion - - -_,.._
Deletion of a node from a linked list is another o peration in which the implementation .L
makes a significant difference. From our study of linked s tacks and queues, we know
Figure 4.13. Doubly linked list with header
that it is never difficult to delete the first node on a linked list. If we wish to delete a
120 Linked Lists C HAPTER 4 S E CT I O N 4 .3 Further Operations on Linked Lists 121
deleted and, if desired, can even he a static variable (that is, one declared as the program need 10 be made in the middle of a list, and when random access is important. Linked
is written). Use of a dummy node al. the head of the list simplifies the form of some storage proves superior when the structures are large and flexibility is needed in inserting,
functions. Since the list is never empty, the special case of an empty list does not need deleting, and rearranging the nodes.
to be considered, and neither does the special case of inserting or deleting at the head of
the list. 4.3.3 Programming Hints
Some of the exercises at the end of this section request functions for manipulat-
ing doubly linked lists. We shall study linked lists with dummy header nodes in the To close this section we include several suggestions for programming with linked lists,
application in the next section. as well as some pitfalls to avoid.
poimers and pitfalls I. Draw "before" and "after" diagrams of the appropriate part of the linked list, showing
4.3.2 Comparison of Implementations the relevant pointers and the way in which they should be changed.
2. To determine in what order values should be placed in the pointer fields to implement
Now that we have seen several algorithms for manipulating linked lists and several the various changes, it is usuall y better first to assign the values to previously
variations in their implementation, let us pause to assess some relative advantages of undefined pointers. then to those with value NULL, and finally to the remaining
linked and of contiguous implementation of lists. pointers. After one pointer variable has been copied lo another, the first is free to
advantages The foremost advantage of dynamic storage for linked lists is flexibility. Overflow be reassigned to its new location.
is no problem until the computer memory is actually exhausted. Especially when the
undefined links 3. Be sure that no links are left undefi ned at the conclusion of your algorithm, either
overflow individual structures are quite large, it may be difficult to determine the amount of
as links in new nodes that have never been assigned, or links in old nodes that have
contiguous static storage that might be needed for the required arrays, while keeping
become dangling, that is, that point lo nodes that no longer are used. Such links
enough free for other needs. With dynamic allocation, there is no need to attempt to
should either be reassigned to nodes still in use or set to the value NULL.
make such decisions in advance.
changes Changes, especially insertions and deletions, can be made in the middle of a linked exrreme cases 4. Always verify that your algorithm works correctly for an empty list and for a list
list more easily than in the middle of a contiguous list. Even queues are easier to handle with only one node.
in linked storage. If the structures are large, then it is much quicker to change the values mulriple 5. Try 10 avoid constructions such as p->next->next, even though they are syntac-
of a few pointers than to copy the structures themselves from one location to another. dereferencing tically correct. A single variable should involve only a single pointer reference.
disadvantages The first drawback of linked lists is that the links themselves take space, space that Constructions with repeated references usually indicate that the algorithm can be
might otherwise be needed for additional data. In most systems, a pointer requires the improved by rethinking what pointer variables should be declared in the algorithm,
same amount of storage (one word) as does an integer. Thus a list of integers will require introducing new ones if necessary, so that no variable includes more than one pointer
double the space in linked storage that it would require in contiguous storage. On the reference (->).
other hand, in many practical applications the nodes in the list are quite large, with data alias variable 6. It is possi ble that two (or more) different pointer variables can point 10 the same
fields taking hundreds of words altogether. If each node contains I 00 words of data, node. Since this node can thereby be accessed under two different names, it is called
space me then using linked storage will increase the memory requirement by only one percent, an an alias variable. The node can be changed using one name and later used with the
insignificant amount. In fact, if extra space is allocated to arrays holding contiguous lists other name, perhaps wi thout the realization that it has been changed. One pointer
to allow for additional insertions, then linked storage will probably require less space can be changed to another node, and the second left dangling. Alias variables are
altogether. If each item takes I 00 words, then contiguous storage will save space only therefore dangerous and should be avoided as much as possible. Be sure you clearly
if all the arrays can be filled to more than 99 percent of capacity. understand whenever you must have two pointers that refer lo the same node, and
random access T he major drawback of Jinked lists is that they are not suited to random access. remember that changing one reference requires changing the other.
With contiguotis storage, the program can refer to any position within a list as quickly
as to any other posit.ion. With a linked list, it may be necessary to traverse a long path
to reach the desired node.
Finally, access to a node in linked storage may take slightly more computer time, Exercises El. Write a C function that counts the number of nodes in a linked list.
since it is necessary, first, to obtain the poimer and then go lo the address. This con- 4.3 E2. Draw a before-and-after diagram describing the main action of the Delete function
sideration, however, is usually of no importance. Similarly, you may find at lirsl that presented in the text.
programming writing functions t.o manipulate linked lists lakes a bit more programming effo1t, but,
with practice, this discrepancy will decrease. E3. Write a function that will concatenate two linked lists. The function should have
two parameters, pointers 10 the beginning of the lists, and the function should link
In summary, therefore, we can conclude that contiguous storage is generally prefer-
able when the structures are individually very small, when few insertions or deletions the end of the first list to the beginning of the second.
122 Linked Lists CHAPTER 4 SECTION 4 . 4 Application: Polynomial Arithmetic 123
E4. Write a function that will split a list in two. The function will use two pointers as E l4. Write functions for manipulating a doubly linked list as follows:
parameters; p will point to the beginning of the list, and q to the node at which it a. Add a node after p.
shou ld be spl it, so that all nodes before q are in the first list and all nodes after q
b. Add a node before p.
are in the second list. You may decide whether the node q itself will go into the
first list or the second. State a reason for your decision. c. Delete node p.
d . Traverse the list.
ES. Write a function that will insert a node before the node p of a linked list by the
following method . First, insert the new node after p, then copy the infom1ation EIS. A doubly linked list can be made circular by selling the values of links in the first
fields that p points to the new node, and then put the new information fields into and last nodes appropriately. Discuss the advantages and disadvantages of a c ircular
what p points to. Do you need to make a special case when p is the first node of doubly linked list in doing the various list operations.
the list?
El6. Make a chart that will compare the difficulties of doing various operations on dif-
E6. Write a function to delete the node p from a linked list when you are g iven only ferent implementations of lists. The rows should correspond to all the operations
the pointer p without a second pointer in lock s tep. on lists specified in Section 3.5.3 and the columns to (a) contiguous lists, (b) sim-
a. Use the dev ice of copying infonnation fields from one node to another in ply linked lists, and (c ) doubly linked lists. Fill in the entries to show the relative
designing your algorithm. difficulty of each operation for each implementation. Take into account both pro-
b. Will your function work when p is the first or the last node in the list? If gramming difficulty and expected running time.
not, either describe the changes needed or state why it cannot be done w ithout
providing additional information to your function.
c. Suppose that you are also given a pointer head to the first node of the list.
Write a deletion algorithm that does not copy information fields from node to
node.
4.4 APPLICATION: POLYNOMIAL ARITHMETIC
E7. Modify the function to traverse a linked list so that it will keep two pointers p and
r in lock step, with r always moving one node behir:d p (that is, r is one node closer
to the head of the list). Explain how your function gets started and how it handles 4.4.1 Purpose of the Project
the special cases of an empty list and a list with only one node. As an app lication of linked lists, this section outl ines a program for manipulating poly-
E8. Write a function that will reverse a linked list while traversing it only once. At the nomials. Our program will imitate the behavior of a simple calculator that does add ition,
conc lusion, each node should point to the node th at was previously its predecessor; calculator f or subtraction, multiplication, division, and perhaps some other operat ions, but one that
the head should point to the node that was formerly at the end, and the node that polynomials perfonns these operations for polynomials.
was formerly first should have a NULL link. There are many kinds of calculators avai lable, and we could model our program
after any of them. To provide a further illustration of the use of stacks, however,
E9. Write an algorithm that will sp lit a linked list into two linked lists, so that successive
let us choose to model what is often called a reverse Polish calculator. In such a
nodes go to differeru lists. (The first, third, and all odd-numbered nodes go to the
reverse Polish calculator, the operands (numbers usually, polynomials for us) are entered before the
first list, and the second, fourth, and a ll even-numbered nodes go t.o the second.)
calculations operation is specified. The operands are pushed onto a stack. When an operation
The following exercises concern circularly linked lists. Sec Exerc ise 6 of Section 4.2. is performed, it pops its operands from the stack and pushes its result back onto the
For each of these exercises, assume that the circularly linked list is specified by a pointer stack. If? denotes pushing an operand onto the stack, + , - , * , I represent arith-
rear to its last node, and e nsure that on concl usion your algorithm leaves the appropriate metic operations, and = means printing the top of the stack (but not popping it off),
pointer(s) pointing to the rear of the appropriate list(s). then? ? + = means reading two operands, then calculating and printing their sum.
ElO. Write a function to traverse a circularly linked list, visiting each node. First, do The instruction ? ? + ? ? + * = requests four operands. If these are a, b, c, d, then
the case where only a single pointer moves through the list, and then describe the the result printed is (a + b) * (c + d). S imilarly,? ? ? - = * ? + = pushes a, b, c
changes necessary to traverse the list with two pointers moving in lock step, one onto the stack, replaces b, c by b - c and prints its value, calculates a* (b - c), pushes
immediately behind the other. d on the stack., anti fiually calcu lates anti µriuts (a• ( b - c)) + d. The atlvantage of
a reverse Polish calculator is that any expression, no matter how complicated, can be
Ell. Write a function to delete a node from a circu larl y linked list. spec i tied without the use of parentheses.
El2. Write a function that will concatenate two circularly linked lists, producing a circu- This Polish notation is useful for compilers as well as for calculators. For the
larly linked list. present, however, a few minutes' practice with a reverse Polish calculator wi ll make you
E13. Write a function that will split a circularly linked li,1 into two circularly linked lists. quite comfortable with its use.
124 Linked Lists CHAPTER 4 SECTION 4.4 Application: Polynomial Arithmetic 125
break; stack.h:
output case ' ;': I* Write polynomial on the top of the stack. *I
TopStack( &item, sp); type stack typedef struct stack.tag {
Write Polynomial ( item) ; Node.type *lop;
break; } Stack.type;
addition case ' +' : f* Add top two polynomials and push answer. *f
Pop( &p, sp); 3. Reading Commands: The Main Program
Pop( &q, sp);
_Add(p,q,sp); f* p + g *I Now that we have decided th at the commands are to be denoted as single characters,
break; we could easily program function GetCommand to read one command at a time from
126 Linked Lists CHAPTER 4 SE CTION 4 . 4 Application: Polynomial Arithmetic 127
*'*'
structures. We shall use a function ReadCommand to read the string of commands; this f* Read a valid command line.
function will also be responsible for error checking. n = O; I* number of characters read
Prompt ();
while ( (c = getchar( )) r = '\n')
#define MAXCOMMAND 10 if (strchr( 11 ,\t 11 , c) != NULL)
main program
I* Implement calculator for polynomials. *f
void main ( void) error checking
I* empty * I ; f* Skip white-space characters.
else if (strchr ( 11 ?+ - */= 11 , c) == NULL) *'
{
inti, n; else
break; I* non-command character
*'
Stack.type stack; command [ n++ J = c;
if (c == '!' )
char command [MAXCOMMAN[B ;
i11s1r11ctio11s Help();
Initialize C&stack) ;
do {
Geteol(c);
} while Cc!= ' \n') ;
I* Skip characters until new line.
*'
n = ReadCommand (command);
for Ci = O; i < n; i++)
DoCommand(command [i), &stack); }
command [n) = ' \O ';
return n;
I* Add end of string marker.
*'
} while (Enquire ( ) ) ;
}
The next function is a utility that skips all characters until the end of a line.
4. Input Functions I* Geteol: get characters until the end of the line. * I
void Geteol(int c)
Before we turn to our principal task (deciding how to represent polynomials and writing {
functions to manipulate them), let us complete the preliminaries by giving derails for the while Cc!= '\n')
input functions. The prompting function need only write one line, but note that this line c = getchar();
gives the user the opportunity r.o request further instruclions if desired. }
I* Prompt: prompt the user for command line. *I The following functi on provides help to the user when requested.
prompting user void Prompt(void)
{
int c; I* Help: provide instructions for the user. * I
instructions void Help ( void)
printf( 11 Enter a string of commands or ! for help.\n 11 ) ; {
while CCc = getchar()) == '\n') printf ("Enter a string of commands in reverse Polish form .\n 11 ) ;
}
f* empty * ! ;
ungetc Cc, stdin) ;
I* Discard leftover newlines, if any.
*' print! ( "The valid instructions are:\n 11 ) ;
printf("? Read a polynomial onto stack\n 11 ) ;
printf ( 11 + Add top two polynomials on stack \n 11 ) ;
printf(" - Subtract top two polynomials on stack\n 11 ) ;
Function ReadCommand must check that the symbols typed in represent legitimate printf( 11 • Multiply top two polynomials on stack\n");
operations and must provide instructions if desired, along with doing its main task. If print! (" I Divide top two polynomials on stack\n 11 ) ;
there is an error or the user requests instructions, then the command string must be printf ( 11 = Print polynomial on top of the stack\n 11 ) ;
re-entered from the start. }
128 Linked Lists CHAPTER 4 SECTION 4 . 4 Application: Polynomial Arithmetic 129
5. Stubs and Testing pairs will constitute a structure, so a polynomial will be represented as a list of structures.
We must then build into our functions ru les for perfonning arithmetic on two such lists.
At this point we have written quite enough of our program that we should pause to implementation of a Should we use contiguous or Iinked Iists? If, in advance, we know a bound on
compile it, debug it, and test it to make sure that what has been done so far is correct. polynomial the degree of the polynomials that can occur and if the polynomials that occur have
If we were 10 wail umil all remaining functions had been writlen, the program would nonzero coefficients in almost all the ir possible terms, then we should probably do better
become so long and complicated that debugging would be much more difficult. with contiguous lists. But if we do not know a bound on the degree, or if polynomials
For the task of compiling the program, we must, of course, supply stubs for all the with only a few nonzero tenns are li kely to appear, then we shall find linked storage
missing functions. At present, however, we have not even completed the type decla- preferable. To illustrate the use of linked lists, we adopt the latter implementation, as
rations: the most important one. that of a Polynomial, remains unspecified. We could illustrated in Figure 4.14.
temporary type make this type declaration almost arbitrarily and still be able to compile the program,
declarario11 but for testing, it is much better to make the temporary type declaration
.f ~ ·f a ~
typedef double Polynomial.type ; 1.0 5.0
and test the program by running it as an ordinary reverse Polish calculator. The following
G I
4
G 0
I
In addition to stubs of the functions for doing arithmetic and the function Erase (which
stack package need do nothing), we need a package of functions to manipulate the stack. If you wish, 3x 5 - 2x 3 + x 2 + 4
you may use the package for linked stacks developed in chis chapter, but with the items in Figure 4.14. Polynomials as linked lists
the stack only real numbers, it is probably easier to use the contiguous stack algorithms
developed in Section 3.2. The functions Initialize and TopStack do not. appear there, but
assumptions We shall consider that each node represents one tenn of a polynomial, and we
are trivial to write. With these tools we can fully debug and test the part of the program
shall keep only nonzero terms in the list. The polynomial that is identically O will be
now wri tten, after which we can turn to the remainder of the project.
represented by an empty list. Hence each node will be a structure containing a nonzero
coefficient, an exponent, and, as usual for linked lists, a pointer to the next term of the
polynomial. To refer to polynomials, we shall always use header variables for the lists;
4.4.3 Data Structures and Their Implementation hence, it is sensible to make pointers of type Polyno miaUype and use it for the headers.
Moreover, the remaining tenns after any given one are again a (smaller) polynomial,
If we carefully consider a polynomial such as so the fie ld next again naturally has type PolynomiaUype. Refer to the include files in
Section 4.4.2.
We have not yet indicated the order of storing the tenns of the polynomial. If we
3x5 - 2x3 + x' + 4 all ow them to be stored in any order, then it might be difficult to recognize that
we see that the impo1tant information about the polynomial is contained in the coefficients x 5 + x2 - 3 and - 3 + x 5 + x 2 and x 2 - 3 + x5
essence of a and ex ponents of x; the variable x itself is really just a place holder (a dummy variable).
polynomial Hence, for purposes of calculation, we may think of a polynomial as a sum of t.enns,
restriction all represent the same polynomial. Hence we adopt the usual convention that the terms
each of which consists of a coefficient and an exponent. In a computer we can similarly
of every polynomial are stored in the order of decreasing exponent within the linked list,
implement a polynomial as a list of pairs of coefficients and exponents. Each of these
130 Linked Lists CHAPTER 4 SECTIO N 4.4 Application: Polynomial Arithmetic 131
and we further assume thal. no two terms have the same exponent and that no term has
reading
I* ReadPolynomial: read a polynomial and return its pointer.
PolynomiaUype *ReadPolynomial(void)
*'
a zero coefficient.
{
double coef;
4.4.4 Reading and Writing Polynomials int exp, lastexp;
PolynomiaUype *result, *!ail;
With polynomials implemented as linked lists, writing out a polynomial becomes simply
a traversal of the list, as follows. instmctions printf (" Enter coefficients and exponents for the polynomial. \n"
writing
I* WritePolynomial: write a polynomial to standard output.
void WritePolynomial(PolynomiaUype *P)
*' "Exponents must be in descending order.\n"
"Enter coefficient or exponent O to terminate. \n");
initialize lastexp =INT.MAX;
{
if ( !p)
print! ("zero polynomial\n");
I* Polynomial is an empty list.
*' read one term
tail= result= Make Term (0.0, 0); I* dummy head
while(1) { *'
print! ("coefficient? ") ;
else while (p) {
scant ( " %11 ", &coef) ;
print! ( "%5.21fx~% 1d", p->coef, p->exp);
if ( coef == 0.0)
p = p->next;
break;
if (p)
printf ("exponent? " );
print! ( "+" ) ;
scant( "%d" , &exp);
else
print!( "\n"); error checking if (exp>= lastexp II exp< 0) {
} printf("Bad exponent. Polynomial is terminated"
} "without its last term.\n");
break;
As we read in a new polynomial, we shall be constructing a new linked list, adding }
a node to the list for each term (coefficient-exponent pair) that we read. The process make one term tail= lnsertTerm(coef, exp, tail);
of creating a new node, attaching it to the tail of a !isl, pulling in the coefficient and if (exp== O)
exponent, and updating the tail pointer to point to the new term will reappear in almost break;
every function for manipulating polynomials. We therefore writ.e it as a separate utility. lastexp = exp;
}
We can now use this function t.o insert new terms as we read coefficient-exponent pairs. 4.4.5 Addition of Polynomials
lnsertTerm, however, expects to insert the new tern, after the term to which tail currently
points, so how do we make the first term of the list? One way is to write a separate The requirement that the tenns of a polynomial appear with descending exponents in the
dummy first node section of code. It is easier, however, to use a dummy header, as follows. Before starting list simplifies their addi tion. To add two polynomials we need only scan through them
to read, we make a new node at the head of lhe list, and we put dummy information once each. If we find tem,s with 1he same exponent in the two polynomials, then we
into it. We then insert all the actual terms, each one inserted after the preceding one. add the coefficients; otherwise, we copy the tern, of larger exponent into the sum and
\Vhen the function concludes the head pointer is advanced one position to the first actual go on. When we reach the end of one of 1he polynomials, then any remaining part of
term, and the dummy node is freed. This process is illustrated in Figure 4. 15 and is the other is copied to the sum. We must also be careful not to include tenns with zero
implemented in the following function: coefficient in the sum.
132 Linked Lists C HAPT ER 4 SECT I ON 4 . 4 Application: Polynomial Arithmetic 133
I* Add: add two polynomials and push the answer onto stack. *f 2
void Add(PolynomiaUype *P, Polynomial.type *q, Stack.type *SP)
{
double sum;
Preliminary
G ·[,;
head
tail
1 0
Polynomial.type *ptmp = p;
initialize
PolynomiaUype *qtmp = q;
PolynomiaUype *result, *tail;
tail = result= MakeTerm (0.0, 0);
while(p&&:q)
Term 1
G
head ·F 001n"'ll
1:0 ·F 1.0
tai l J
5 (0 x•
q = q->next; tail J
} else if (p->exp > q->exp) { I* Copy p to result.
.r ,:g. r
1Jnequal exponents *I
tail = lnsertTerm (p->coef, p->exp, tail); / 2
1:0 · I 1:0 ·[ 10
1.0 - 3.0
p = p->next; Term 3
G oo~n>>'
5 2
7.0
0
x5 - 3x 2 + 7
} else { I* Copy q to result.
tail= lnsertTerm (q->coef, q->exp, tail); *' head
tailJ
q = q->next;
}
remaining terms f* One or both of the summands has been exhausted. * f
I* At most one of the following loops will be executed.
for (; p; p = p->next)
*' Final
~
head
Lr 1.0
5 1:0 ·F
- 3.0
2 1:0 ·F 7.0
0 I~··-,,,.,
tail= lnsertTerm (p->coef, p->exp, tail);
for (; q; q = q->next) Figure 4.15. Construction of a linked lisl with dummy node
tail = lnsertTerm (q->coef, q->exp, tail);
conclude tail->next = NULL;
Push(result->next, sp);
I* Terminate polynomial.
*' 2. Group Project
Production of a coherenl package of algorithms for manipulating polynomials makes
free ( resu It) ;
Erase(ptmp);
Erase Cqtmp) ;
I* Free dummy head.
I* Erase summand. **'' an interesting group project. Different members of the group can write functions for
different operations. Some of these are indicated as projects at the end of thi s section,
but you may wish to include additional features as well. Any additional features should
}
be planned carefully to be sure that they can be implemented in a reasonable time,
without disrupting other parts of the program.
After deciding on the division of work among its members, the most important
4.4.6 Completing the Project
specifications decisions of the group relate to the exact ways in which the functions should communicate
with each other, and especially with the calling program. If you wish to make any
1_ The Missing Functions changes in the organization of the program, be certain that the precise details are spelled
At this point the remaining functions for the calculator project are sufficiently similar to out clearly and completely for all members of the group.
those already wriuen that they can be left as exercises. The function Erase need only Next, you will find that it is too much to hope that all members of the group will
traverse the list (as done in WrltePolynomlal) disposing of the nodes as it goes. Functions complete their work at 1he same time, or th ai all pans of che projec1 can be combined
for the remaining arithmetical operations have the same general form as our function for cooperation and debugged together. You will therefore need to use program stubs and drivers (see
addition. Some of these are easy: Subtraction is almost identical with addition. For Sections I .4.1 and 1.4.4) to debug and tesl the various parts of the project. One member
multiplication we can first write (a simple) function that multiplies a polynomial by a of the group might take special responsibility for these. In any case, you w ill find it very
monomial, where monomial means a polynomial with only one term. Then we combine effective for different members to read , help debug, and test each other's subprograms.
use of this function with the addition function to do a general multiplication. Division coordination Finally, there are the responsibilities of making sure that all members of the group
is a little more complicated. complete their work on time, of keeping track of the progress of various aspects of the
134 Linked Lists CHAPTER 4 SECTIO N 4 .5 Linked Lists in Arrays 135
project, of making sure that no subprograms are integrated into the project before they are 4.5 LINKED LISTS IN ARRAYS
thoroughly debugged and tested, and of combining all the work into the finished product.
Several of the o lder but widely-used computer languages, such as FORTRAN, COBOL, and
old languages BASIC, do not provide faci lities for dynamic sto rage allocation or pointers. Even when
Exercises El. Decide whether the stack for the calculator should be contiguous or linked. Justify
implemented in these languages. however. there are many problems where the methods
4.4 yvur t.lct:biv11. Incorporate the necessary functions to implement the stack in the
of linked lists are preferable to those of contiguous lists, where, for example, the ease
way you decide.
of c hang ing a pointer rather than copyi ng a large structure proves advantageous. This
E2. If polynomials are stored as circularly linked lists instead of si mply li nked lists, then
section shows how to implement linked lists using only simple integer variables and
a polynomial can be erased more quickly. What changes are needed to the addition
arrays.
function (and other subprograms) to implement circularly linked lists?
The idea is to begin with a large array (or several arrays to hold different parts of
E3. Consider generalizing the project of this section to polynomials in several variables. a structure) and regard the array as ou r allocation of unused space. We then set up our
own functions to keep track of which parts of the array are unused and to link entries of
Programming Pl. Write function Erase. the array toge1her in the desired order.
Projects P2. Write function Subtract. The one feature of linked lists that we must invariably lose in this implementation
4.4 P3. Write a function that will multiply a polynomial by a monomial (that is, by a dynamic memory method is the dynamic allocation of storage, since we must decide in advance how
polynomial consisting of a single term). much space to allocate to each array. All the remaining advantages of linked lists, such
as flexibility in rearranging large structures or ease in making insertions or deletions
P4. Use the function of the preceding problem, together with the function that adds
anywhere in the list. will st ill apply, and linked lists still prove a valuable method.
polynomials, to write the function Multiply.
The implementation of linked lists within arrays even proves valuable in languages
PS. Write function Divide.
like C that do provide pointer types and dynamic memory allocation. The applications
P6. The function ReadCommand, as writ.ten, will accept any sequence of commands, advantages where arrays may prove preferable are those whe re the number of items in a list is
but some sequences are illegal. If the stack begins empty, for example, then the known in advance, where the links are frequently rearranged , but relatively few add i-
sequence + ? ? is illegal, because it is impossible to add two polynomials before tions or deletions are made, or applications where the same data are sometimes best
reading them in. Modify function ReadCommand as follows so that it will accept treated as a linked list and other times as a contiguous list. An example of such an
only legal sequences of commands. The function should set up a counter and application is illustrated in Figure 4.16, wh ich shows a small pan of a student record
initialize it to the number of polynomials on the stack. Whenever the command? system. Identification numbers a re assigned to students first-come, first-served, so nei-
appears in the stream the counter is increased by one (since the read command ? ther the names nor the marks in any panicular course are in any special order. Given an
will push an additional polynomial onto the stack), and whenever one of+ , - , *, I
appears, it is decreased by one (since these commands will pop two polynomials '
and push one onto the stack). If the counter ever becomes zero or negative then the
sequence is iI legal. name next name math nextmath cs nextCS
P7. Many reverse Poli.s h calculators use not only a stack but also provide memory lo- 0 Clark, F. ,,.
cations where operands can be stored. Extend the project to provide for memory Smith. A. ,,.
locations for polynomials, and provide additional commands to store the top of the
stack into a memory location and to push the polynomial in a memory locati on onto
2 - ~
3 Garcia. T.
the stack.
PS. Write a function that will discard the top polynomial on the stack, and include th is 4 Ha l, W. ~
PIO. Write a function that will add all the polynomials on the stack together, nod include 8 Arthur, t . ~
info next
identification number, a student's records may be found immediately by using the iden-
multiple linkages
tification number as an index to look in the arrays. Sometimes, however, it is desired to
print out the student records alphabetically by name, and this can be done by following
0 Clark, F.
Sm ith, A.
-
_,,
the links stored in the array nextname. Similarly, student records can be ordered by
marks in any course by following the links in the appropriate array.
2 - -
In the implementation of linked lists in arrays, pointers become indices relative to 3 Garcia, T.
-
cursors the start of arrays, and the links of a list are stored in an array, each entry of which
gives the index where, within the array, the next entry of the list is stored. To distinguish
4
5
Hall, W.
Evans, 8 .
--
these indices from the pointers of a linked list in dynamic storage, we shall refer to links
within arrays as cursors and reserve the word pointer for links in dynamic storage.
For the sake of writing programs we shall use two arrays for each linked list, info [ ]
6
7
-
-
--
to hold the information in the nodes and next [ ] to give the cursor to the next node.
These arrays will be indexed from O 10 MAX - 1, where MAX is a symbolic constant.
8
9
Arth ur, E.
-
-
Since we begin the indices with 0, we can use the cursor value - 1 to indicate the end
of the list, just as the pointer value NULL is used in dynamic storage. This choice is also
illustrated in Figure 4.16.
10
11
- --
You should take a moment to trace through Figure 4.16, checking that the cursor
12
-
values as shown correspond to the arrows shown from each entry to its successor. 13
-
To obtain the flavor of implementing linked lists in arrays, let us rewrite several of
the algorithms of this chapter with th is implementation.
14
-
Our first task is to set up a list of available space and write functions to obtain a
new node and to return a node to available space. We shall set up a stack to keep track
malloc and tree of available space, but now this stack wi ll be linked by means of cursors in the array firstna,ne avail last node
next. To keep track of the stack "of available space, we need an integer variable avail that Figure 4.17. The available-space list
wi ll give the index of its top. If this stack is empty (which will be represented by avail=
- I) then we will need to obtain a new node, that is, a position withi n the array that has I* Initialize: initialize head and indices avail and lastnode. • I
not yet been used for any node. Thus we shall keep another integer variable lastnode initialization void Initialize ( int •head, int •avail, int •lastnode)
that will count the tota l number of positions within our array that have been used to hold {
list entries. When lastnode exceeds MAX-1 (the bound we have assumed for array size) •head = - 1; I* The list is empty.
*'
then we will have overflow and the program can proceed no further. When the main
program starts, both variables avail and lastnode should be initialized 10 - I, avail to
}
•avail = - 1;
•lastnode = - 1;
I* Stack of available nodes is empty.
I* None of the positions has been used. **''
indicate that the stack of space previously used but now available is empty, and lastnode
to indicate that no space from the array has yet been assigned. This available-space list
The following function NewNode allocates a new node from available space:
is illustrated in Figure 4.17. This figure, by the way, also illustrates how two linked lists
can coex ist in the same array. I* NewNode: select the index of first available position. * I
The decisions we have made translate into the follow ing declarations: allocate int NewNode (int •avail, int • lastnode, int next [ J )
{
int avail, lastnode; int n = -1;
int next [MAX] ; if ( •avail ! = - 1) {
We put MAX in an include file and call the file list.h: n = *avail;
*avail = next [ * avail] ;
#define MAX 5 } else if ( •lastnode < MAX) {
(*lastnode)++;
We take this small value for testing the program. In a typical application, MAX would be n = •lastnode;
as large as avai lable memory permits. With these declarations we can now rewrite the } else Error( "No available position left ");
functions for keeping track of unused space. At the start of the program, the lists should return n;
be initialized by invoking the follow ing: }
138 Linked Lists C HAP TE R 4 SECTIO N 4 .5 Linked Lists in Arrays 139
The purpose of the next function, called FreeNode, is to return a node that in no longer
needed to the list of avai lable space. This function now takes the form:
(a) (c) {d) (e)
E7. Write a function that will reverse a linked list within an array wh ile traversing it only implemenlation The way in which an underlying structure is implemented can have substantial effects
once. At the conclusion, each node s hould point to the node that was previously its on program development, and on the capabilities and usefulness of the result. Sometimes
predecessor; the header s hould point to the node that was formerly at the end, and these effects can be subtle. The underlying mathematical concept of a real number, for
the node that was formerly first s hould have a - 1 cursor. example. is usu2lly (but not always) implemented by computer as a floating-point number
with a ceitain degree of piecis iuu, aml tile iu1Jc1cu1 li111i1a1iu11~ i11 Lhb implt:mematiun
often produce difficulties with round-off error. Drawing a clear separation between the
logical structure of our data and its implementation in computer memory will help us
4.6 ABSTRACT DATA TYPES AND THEIR IMPLEMENTATIONS in designing programs. Our first step is to recogni7.e the logical connections among the
data and embody these connections in a log ical data s tructure. Later we can consider
our data structures and decide what is the best way to imp lement them for efficiency of
programming ar.d execution. By separating these decisions they both become easier, and
4.6.1 Introduction we avoid pitfalls that attend premature commitment.
Suppose that in deciphering a long and poorly documented program you found the To help us clarify this distinction and achieve greater generality let us now reconsider
following sets of instructions: some of the data structures we have s tudied from as general a perspective as we can.
For our general point of view we shall use mathematical tools to provide the rules Dl:'rlNITIOI\' A stack of elements of type 1' is a finite sequence of elements of T together with
uz,iidf::;; types for building up structured types. Among these tools are sets, sequences, and functions.
the operations
For the study of lists the one that we need is the finite sequence, and for its definition
we use mathematical induction. A definition by induction (like a proof by induction) I. Initialize the stack to be empty.
has two parts: First is an initial case, and second is the defin ition of the general case in
2. Determine if the stack is empty or not.
terms of preceding cases.
3. Determine if the stack is full or not.
4. If the stack is not full, then insert a new node at one end of the stack.' called .-~~
DEFINITION A sequence of kngth O is empty. A sequence of length n 2:: I of elements from '·
its top.
a set Tis an ordered pair (Sn-1> t) where Sn-I is a sequence of length n - I of
elements from T, and t is an element of T. 5. If the