Data Structure
Data Structure
Part I : The dynamic array
(1) Introduction to data structures
(2) Creating our first data structure with functional support
(3) The dynamic array
(4) Design
(5) Operations
(6) Pseudocode: core functions
(7) Pseudocode: utility functions
(8) The DArray definition (DARRAY.H)
(9) The DArray operations (DARRAY.C)
(10) Code to test the DArray (DATEST.C)
(11) Packaging the DArray
(12) An application of the DArray
(13) Memory map
(14) Application pseudocode
(15) Selected application code
(16) Choosing the best DArray size, growth
(17) Selected exercises
Part II : The dynamic sorted array
(18) A dynamic sorted array
(19) Sorting algorithms
(20) Algorithm performance
(21) Quicksort
(22) A partition algorithm
(23) Quicksort pseudocode
(24) Binary search
(25) Packaging the dynamic sorted array
(26) DSArray pseudocode
(27) The DSArray definition (DSARRAY.H)
(28) The DSArray operations (DSARRAY.C)
(29) Code to test the DSARRY(DSTEST.C)
(30) A fast record search application
(31) Memory map
(32) Application pseudocode
(33) Selected exercises
Part III : The linked list
(34) The linked list
(35) What is a linked list?
(36) Inserting/deleting elements
(37) Searching for elements
(38) Design
(39) Linked list traversal
(40) Formal definition
(41) Pseudocode for the operations
(42) The linked list definition (LL.H)
(43) The linked list operations (LL.C)
(44) Code to test the linked list (LLTEST.C_
(45) A linked list of heterogeneous data
(46) Design
(47) Data definition
(48) Memory map
(49) Pseudocode
(50) Selected application code
(51) Doubly linked lists
(52) Sorted linked lists
(53) Selected exercises
Part IV : The hash table
(54) What is a hash table?
(55) How does the hash table provide fast search/add?
(56) Rehashing
(57) Choosing the number of buckets
(58) Formal definition
(59) Pseudocode for the operations
(60) The HashTable definition (HT.H)
(61) The HashTable operations (HT.C)
(62) Code to test the hash table (HTTEST.C)
(63) Output of HTTEST.C
(64) Testing the rehash() function
(65) Code to test rehashing (HTTEST2.C)
(66) Output of HTEST2.C
(67) Car dealership search
(68) Design
(69) AUTO.H
(70) AUTOTEST.C
(71) Output of AUTOTEST.C
(72) Designing hash functions
(73) Which fields to hash on?
(74) Multiple hashing
(75) Analysis of the multiple hashing example
(76) A better solution...
(77) Multiple hashing on ForestryRegister
(78) How to implement multiple hashing
(79) Selected exercises
Part V : The binary tree
(80) The binary tree
(81) Searching for elements in a binary tree
(82) Search performance
(83) Adding elements to a binary tree
(84) How is the tree organized in memory
(85) Binary tree definition
(86) Ordering elements in the tree
(87) Deleting elements in the tree
(88) A deletion algorithm
(89) The binary tree definition (BT.H)
(90) Pseudocode for the operations
(91) The binary tree definition (BT.C)
(92) Code to test the binary tree (BTTEST.C)
(93) Output of BTTEST.C
(94) Expression trees
(95) Expression tree evaluator (EXP.H)
(96) Expression tree evaluator (EXP.C)
(97) Crossreferencing with trees
(98) Companyemployee original database
(99) Employee history generated tree
(100) Performing queries
(101) COMPANY.DAT input file
(102) Employee crossreferencer (COMPANY.H)
(103) Employee crossreferencer (COMPANY.C)
(104) Output of COMPANY.C
(105) Expanding a BTNode's definition
(106) Supporting complex BTNodes
(107) Inorder, Postorder, Preorder traversals
(108) Balancing binary trees
(109) Balancing algorithm pseudocode
(110) BTrees
(111) Selected exercises
Introduction to data structures
After having studied the basics of the C language it is now possible to study more advanced
programming techniques. If we are to write fast and efficient programs we will have need of
optimal programming building blocks. These we can call data structures.
A data structure is a packaged unit of functions that works on specific data to produce some
useful result.
To fully appreciate the worth of a data structure, it is necessary to study in detail the design and
implementation of some standard ones from first principles. Four core data structures will be
discussed in detail:
Dynamic Array
Linked List
Hash Table
Binary Tree
Even after having designed these data structures from scratch, the other aspect of the art
of designing good programs is to decide, when given a problem, how to apply data structures to
solve the problem at hand. This will be addressed in the latter portion of this data structure unit.
Advantages of data structures
I. Promotes reuse of important functions on data.
II. Implementation is hidden from the user to allow
concentration on high level solutions of the problem at
hand.
III. Prepackaged data structures by third party vendors can be
guaranteed to be terribly efficient, probably the best
solution for low level data manipulation. The more
optimized libraries that we use in our software, the
greater the potential exists for faster, more powerful
results with the minimum of coding.
IV. A standard set of functions provides a separation from
lowlevel implementation to the high level application,
giving room for enhancing/modifying the data structure
functions to other methodologies if the need be without
touching the application code.
I Creating our first data structure with functional support
The standard C arrays int i[5]; float f[10]; ... can be considered data structures but with minimal
builtin support. We have to write the functions from scratch to do basic but welldefined
operations: add an element, remove an element, search for an element, display an element, edit
an element, ...
If we had a prepackaged array type which included the above
operations for free, we could concentrate on how to use
this black box to solve higher applications, like a record
manager, an attendance monitor, a graphics application, etc.
Note that we don't have such a black box shipped with standard C so we have to write our own
code from scratch which unfortunately, ultimately involves writing the same kind of logic again
and again for each and every application that requires comprehensive array capabilities.
To avoid this tedious scenario, why not create an array package, or data structure, general
enough to be used in many programs. We can extend its capabilities from the standard C array
type to include the following aspects:
. stores arbitrary types of data
. resizes itself automatically
as data is added/deleted over time
. range checks index accesses to preserve integrity
We can call our a DARRAY, or dynamic array, a vector list implementation whose internal size
varies over time.
The Dynamic array
We have described the general qualities of a DArray, but what does it look like internally?
Physically, we can picture a vector, or contiguous list of pointers to data of some type:
[0]| | > Data
[1]| | > Data
[2]| | > Data
[3]| | > Data
. | | .
. | | .
. | | .
[nElements1]| | > Data
. | |
. | |
. | |
[size1]| |
|
| | | possible future growth
| of DArray by <growthSize>
| | | if necessary
|
| | |
| |
| |
| |
.
.
.
Design
I..Indirect or direct storage?
We can base our design on a list of pointers to data instead of a list of raw data in this case
because we really don't know what type of data users of the DArray will be using, so we could
never guess the size of each cell beforehand. Also as we have seen, it is much more memory
efficient to allocate pointers to data initially, and at runtime point them to reallife data that is
needed and created via malloc.
Along this line, since the application of interest knows best about the data it is working on, we
will leave it up to the application designer to manage the creation/deletion of the data. The
DArray's sole concern is to manage the pointers to the data.
II..Internal variables?
Note that we have two important variables that describe the contents of the DArray,
<nElements> and <size>.
nElements describes the actual number of pieces of data stored
in the list.
size describes the actual size of the vector of pointers
which changes over time.
<size> is always >= <nElements>
growthSize describes the extension cell size of the array
when the <size> is exhausted
III..Dynamic paradigm?
Adding elements to the DArray is not too difficult if we foresee ourselves patching items to the
end of the list. But removing elements presents difficulties. What do we do when a hole is
created by deleting a pointer to a piece of data in the middle of the list? One paradigm, or
method is to shrink, or shift the data downwards to absorb the hole:
| | > T | | > T
| | > X | | > X
| x | > Y ==> | | > Q
| | > Q | | > Z
| | > Z | |
|...| > |...|
Before deleting "Y" After deleting "Y"
The advantage of this method is that we keep the pointer list tight, a onetoone snapshot of the
data in the list. The disadvantage is that each memory shift operation is time expensive and may
slow down the performance appreciably in large lists.
An alternate paradigm may reduce the overall number of memory shifts, that is to keep holes
vacant, or in more technical terms, keep holes as holes and only do memory shifts when
necessary, at a certain userdefined tolerance, say when the number of holes exceeds a certain
absolute tolerance or a certain percentage of the size of the DArray.
Ex: size = 4, Tolerance = 50 % of size
| | > A | x | | | | | | | > B
| | > B | | > B | | > B | | > B | | > D
| | > C | | > C | | > C | x | |...|
|...| |...| | | > D | | > D | |
Delete "A" Add "D" Delete "C" Result
/\ /\
|| hole ||
|| adjustment ||
|| triggered ||
Using this paradigm, we definitely reduce the number of overall memory shifts, but another
contingency crops up. What if the user of the data structure refers to a particular cell, expecting
a particular piece of data, assuming a tight representation of the data, only to find that at some
magical time, namely at hole adjustment time, the data has been shifted beyond his knowledge?
Which dynamic model to choose?
There are many other paradigms, too numerous to discuss at this time. For the sake of
simplicity, and considering that the DArray is our first data structure, let's opt for the first
solution, the tight model.
Operations
Let's propose a series of specific functions on our DArray that application designers would like
to have.
(1) init(darray,initialSize)
(2) add(element,darray)
(3) remove(index,darray,shouldDelete)
(4) flush(darray,shouldDelete)
(5) nElementsIn(darray)
(6) get(index,darray)
(7) searchFor(element,darray,comparisonFunction)
(8) wrapup(darray)
Armed with a DArray supporting these functions we could solve many useful problems. This is
great, but where do we start?
We have already begun by identifying the memory model and the
operations on the data of interest. Now we must be sure to pseudocode the operations and walk
through the first round logic with several test cases before launching into the code.
Why? Because if we don't, we are sure to make mistakes, if not a mess and regret jumping into
the code too quickly, especially faulty code. Don't be alarmed! Even experienced programmers
suffer from this nasty habit!
Pseudocode: core functions
********************************************************
* Initialize <array> to <initialSize>
init(array,initialSize,growthDelta)
nElements = 0
size = initialSize;
growthSize = growthDelta
array = new array of size [initialSize]
********************************************************
* add a piece of <data> to the <array>
add(data,array)
if (nElements >= size)
grow(array)
array[nElements] = data
nElements = nElements + 1
********************************************************
* remove a <index>'th piece of data from the <array>
* if <shouldDelete> is set, the data is deallocated
remove(index,array,shouldDelete)
if (!inRange(index,array)
return ERROR
else
if (shouldDelete)
deallocate(array[index])
removeHole(index,array)
nElements = nElements 1
return NO_ERROR
********************************************************
* Remove all pieces of data from the array
* if <shouldDelete> is set, the data is deallocated
flush(array,shouldDelete)
if (shouldDelete)
for (i = 0 to nElements1)
remove(i,array,shouldDelete)
nElements = 0
********************************************************
* Return the number of elements in <array>
nElementsIn(array)
return nElements
********************************************************
* Access the <index>'th piece of data in <array>
and return a reference (address) to the data
get(index,array)
if (index >= 0 AND index < nElements)
return array[index]
else
return errorReference
********************************************************
* Linear search to check if <searchElement> matches
* any element in <array> recognized by <comparisonFunction>
* Returns the index of the matched item
searchFor(searchElement,array,comparisonFunction)
for (i = 0 to nElements1)
if (comparisonFunction(array[i],searchElement)
return (i)
return(NOT_FOUND)
********************************************************
* Deallocate all memory associated with array
* if <shouldDelete> is set, the items in the array
* are automatically deleted
wrapup(array,shouldDelete)
flush(array,shouldDelete)
delete array
Pseudocode : utility functions
********************************************************
* Check to see if <index> is a valid index into <array>
* Returns <TRUE> or <FALSE>
inRange(index,array)
if (index < 0 OR index >= nElements)
return FALSE
else
return TRUE
********************************************************
* Expand an <array> by <growthSize>
* and recopy the original contents to new area of memory
grow(array)
newSize = size + growthSize
newArray = a new area of memory of <newSize>
i = 0
while (i < size)
newArray[i] = array[i]
deallocate <array>
array = newArray
size = newSize
********************************************************
* Remove the hole at array[index] by shifting elements
* in <array> from <index> to <nElements> down one cell
removeHole(index,array)
for (i = index + 1 to nElements1)
array[i1] = array[i];
The DArray definition (DARRAY.H)
#ifndef DARRAYH
#define DARRAYH
/* Constants */
enum {FALSE,TRUE};
enum {ERROR,NO_ERROR};
#define NOT_FOUND 1
/* Data description */
typedef struct
{
void **array;
int nElements;
int size;
int growthSize;
} DArray;
typedef int (*CompareFunction)(void *data1,void *data2);
/* Function prototypes */
int init(DArray *d,int initialSize,int growthDelta);
void wrapup(DArray *d,int shouldDelete);
int add(DArray *d,void *data);
void reMove(DArray *d,int index,int shouldDelete);
void flush(DArray *d,int shouldDelete);
int search(DArray *d,void *data,CompareFunction cmp);
void *get(DArray *d,int index);
int nElements(DArray *d);
#endif
Notes:
We can generalize the type of data pointed to by declaring it as the generic void*. The type of
data that <array> can point to is now arbitrary. It could be int*, float*, char* or any user defined
structure type like Student*, Record*, etc. void* is ideal for the purposes of creating a DArray
data structure that can handle any type of data.
We defer creating the actual elements of the internal array until runtime when the user of the
DArray has specified what the initial size of the list should be.
The DArray operations (DARRAY.C)
#include "darray.h"
#include <stdlib.h>
/* Utility functions */
static void display(DArray *d)
{
printf("DA:nElements,size,growthSize=%d,%d,%d\n",
d>nElements,d>size,d>growthSize);
}
static int inRange(DArray *d,int index)
{
if ((index < 0) || (index >= d>nElements))
{
printf("Index not in range %d\n",index);
display(d);
return(FALSE);
}
else
return(TRUE);
}
static int grow(DArray *d)
{
int newSize;
if (d>growthSize <= 0)
return(ERROR);
newSize = d>size + d>growthSize;
d>array = (void**)realloc(d>array,sizeof(void*)*newSize);
if (!d>array)
return(ERROR);
d>size = newSize;
}
static void removeHole(DArray *d,int index)
{
int i;
for (i = index+1; i < d>nElements1; i++)
d>array[i1] = d>array[i];
}
/* Core functions */
int init(DArray *d,int initialSize,int growthDelta)
{
if (initialSize <= 0)
initialSize = initialSize;
d>nElements = 0;
d>size = initialSize;
d>growthSize = growthDelta;
d>array = (void**)malloc(sizeof(void*)*initialSize);
return(d>array ? NO_ERROR : ERROR);
}
void wrapup(DArray *d,int shouldDelete)
{
flush(d,shouldDelete);
free(d>array);
}
int nElements(DArray *d)
{
return(d>nElements);
}
int add(DArray *d,void *data)
{
if (d>nElements >= d>size)
if (ERROR == grow(d))
return(ERROR);
d>array[d>nElements] = data;
d>nElements++;
return(NO_ERROR);
}
void reMove(DArray *d,int index,int shouldDelete)
{
if (!inRange(d,index))
return(ERROR);
if (shouldDelete)
free(d>array[index]);
removeHole(d,index);
d>nElements;
return(NO_ERROR);
}
void flush(DArray *d,int shouldDelete)
{
int i;
if (shouldDelete)
for (i = 0; i < nElements; i++)
remove(d,i,shouldDelete);
d>nElements = 0;
}
int search(DArray *d,void *data,CompareFunction cmp)
{
int i;
for (i = 0; i < d>nElements; i++)
if ((*cmp)(d>array[i],data))
return(i);
return(NOT_FOUND);
}
void *get(DArray *d,int index)
{
if (!inRange(d,index))
return(0);
return(d>array[index]);
}
Code to test the DArray (DATEST.C)
#include <stdio.h>
#include <string.h>
#include "darray.h"
/* */
/* APPLICATION TEST CODE on integer data */
/* to verify the basic operations of the */
/* DARRAY, dynamic array data structure */
/* */
static void displayData(DArray *d)
{
int i;
printf("[");
for (i = 0; i < nElements(d); i++)
{
int *intData = (int*)get(d,i);
printf("%d ",*intData);
}
printf("]\n");
}
static int compareData(void *d1,void *d2)
{
int *id1 = (int*)d1,*id2 = (int*)d2;
return( (*id1 == *id2) ? TRUE : FALSE);
}
void main()
{
int i=1,j=2,k=3,m=4,n=5;
int index,*idata;
DArray da,*d = &da;
init(d,2,2);
add(d,(void*)&i);
add(d,(void*)&j);
add(d,(void*)&k);
add(d,(void*)&m);
displayData(d);
reMove(d,2,FALSE);
add(d,(void*)&n);
displayData(d);
index = search(d,(void*)&j,compareData);
if (index != NOT_FOUND)
printf("<j> found at index <%d>\n",index);
flush(d,FALSE);
displayData(d);
idata = (int*)get(d,89);
wrapup(d,FALSE);
}
Output Memory map
1 2 3 4 d >0| . | > | i | Initial size is 2 cells
1 2 3 5 1| . | > | j |
<j> found at index <1> 2| . | > | k | Cells 2,3 are added
[] 3| . | > | n | upon third add(..) call
Index not in range 89
DA:nElements,size,growthSize=0,4,2
Packaging the DArray
Note the separation between the application code and the generic data structure code. Two
independent teams are working here. The data structure designers have created the DArray
independently, without prior knowledge, of what application designers will use it for.
|
|
|
|
| Data structure support | | | Application |
<=======>
Communication
occurs via the
operations or
public interface
DARRAY.H |
DARRAY.C | APP.C
|
This separation is important. The application should not demand to know the internal details of
the data structure, nor should the data structure demand to know the nature of every application
that will use it.
If in any way a data structure depends on specifics of the application then we question the quality
of its design. For example, if we had hardcoded the DArray to work only with string data, then
we seriously restrict its application in the real world.
Viceverse, if the application designer modifies the DArray search function to perform correctly
for the specific data it works on, then we have the sense that the application is overstepping its
bounds.
An application of the DArray
The test application was worthy of demonstrating the usage of the DArray functions, but a small
application is worthy of demonstrating its real world application. Particularly, we should
investigate the functioning of the DArray when records are allocated on the fly, stored in the
DArray and deleted when processing is completed.
Consider a personal telephone directory application that reads name/telephone pairs from a file
and allows the user to search for a particular person's phone number. Here is a sample run:
1 Read phone list
2 Search for a phone number
3 Quit
=> 1
Enter phone file: => phone.dat
21 records read...
=> 2
Enter person's name: => karen t
Karen Telford, 5678901
=> 2
Enter person's name: => joe crow
Sorry, no directory for joe crow
...
phone.dat
Larry_Radon 3455671
Susan_Verlin 4317788
Marla_Hawkens 2454566
...
Memory map
Before getting to the code, let's first consider how the data could be stored in memory, assuming
that pointers to the data are managed by a DArray, d:
d:
array > | . | > | . | > "Larry Radon"
| | | . | > "3455671"
| |
| . | > | . | > "Susan Verlin"
| | | . | > "4317788"
| |
| . | > | . | > "Marla Hawkens"
| | | . | > "2454566"
| |
.
.
.
Application pseudocode
TelephoneStructure: TelRec
pointer to name
pointer to telephone
********************************************************
* MAIN: display menu to user and decode the options:
* 1 Read phone list
* 2 Search for a record
* 3 Quit
initialSize = 50
growthSize = 10
main()
DArray telList
TelRec searchRecord
init(telList,initialSize,growthSize)
repeat
userChoice = displayMenu()
if (userChoice is READ)
input filename
flush(telList,TRUE) * Note we must delete any existing records
* before allocating new ones
readRecords(filename,telList)
else if (userChoice is SEARCH)
input personsName
searchRecord.name = personsName
index = search(telList,recMatch,searchRecord)
if (index is FOUND)
display personsName,telephoneNumber
until (userChoice is QUIT)
deallocateRecords(telList)
wrapup(telList,TRUE)
********************************************************
* Checks if <rec1.name> matches <rec2.name>
* up to the minimum number of characters
* between the two names
* Returns TRUE upon a match otherwise FALSE
recMatch(rec1,rec2)
if (matchesMinimally(rec1.name,rec2.name))
return TRUE
else
return FALSE
********************************************************
* Read telephone records from <fileName>
* and store in heap records
* and add to <telList>
readRecords(fileName,telList)
open fileName
if (successful)
while (not end of file)
read name, telephone
rec = new TelRec * create a heap record to be
* deleted by the caller
rec.name = name
rec.telephone = telephone
add(telList,rec)
close fileName
********************************************************
* Cycle through all records in <telList> and deallocate
* memory previously allocated for each record
deallocateRecords(telList)
for (i = 0 to nElements()1)
record = get(telList,i)
deallocateRecord(record)
deallocateRecord(record)
deallocate name
deallocate telephone
Selected application code
...
typedef struct
{
char *name;
char *telephone;
} TelRec;
...
#define MAX_CHARS 80
int readRecords(char *fileName,DArray *d)
{
FILE *fp;
char name[MAX_CHARS],telephone[MAX_CHARS];
if (!(fp = fopen(fileName,"r")))
return(ERROR);
/* read the first record from the file */
fscanf(fp,"%s %s",name,telephone);
/* loop for the whole file */
while (!feof(fp))
{
/* allocate a telephone record */
tRec = (TelRec*)malloc(sizeof(TelRec));
/* allocate space for <name,telephone> fields */
tRec>name = (char*)malloc(strlen(name)+1);
tRec>telephone = (char*)malloc(strlen(telephone)+1);
/* copy the <name,telephone> fields into the telephone record */
strcpy(tRec>name,name);
strcpy(tRec>telephone,telephone);
/* add the completed telephone record to the DArray */
add(d,(void*)tRec);
/* read the next record from the file */
fscanf(fp,"%s %s",name,telephone);
}
fclose(fp);
return(NO_ERROR);
}
void deallocateRecord(TelRec *record)
{
free(record>name);
free(record>telephone);
}
void deallocateRecords(DArray *d)
{
int i;
TelRec *record;
for (i = 0; i < d>nElements(); i++)
{
record = (TelRec*)get(d,i);
deallocateRecord(record);
}
}
Choosing the best DArray size, growth
The DArray is user configurable upon creation via the two parameters initialSize and growthSize.
These two parameters are important and can affect overall application execution time.
If we are doing many adds to the DArray and our growthSize is too small, many unnecessary
reallocations of memory will occur thus slowing down execution.
Bad case scenarios:
Case 1 Case 2
| | initial
| | | | ||
| | initial | | ||
| | | | \/
| | | |
| | realloc | |
| | realloc | |
| | realloc | |
... ... | |
| | Large initialSize,
| | growthSize hogs
Small growthSize,initialSize ... memory
Runtime slow down | |
| |
On the other hand, if we make initialSize too large and we only end up using a small portion of
it, our application ends up hogging memory which can be used for other programs (and our
own).
The best choice of initialSize and growthSize depends on the expected amount of data used in
the DArray and how often we expect the DArray to grow in size.
It is almost impossible to give hard and fast rules for choosing these parameters; it depends
totally on the application and how important is the tradeoff of memory versus execution time.
Tips
Try to snapshot the size of the data structure at key times in the program, to get a feel for the the
expected case.
Try experimenting with different initialSize, growthSize settings and observe if there is any
noticeable execution performance.
Unit 1 : Exercises
(1) ABC Tuneup corporation has need of a system which records information on their customers
for a daily period only. The services they offer in their drivein, driveout garage are:
Engine oil change $25
Transmission oil change $50
Radiator fluid add $5
At the end of the day, they want a report of all the people who required servicing: Customer
name, vehicle license, year, make, and a breakdown of the services requested. Some days are
slack, others are busy, so as there is really no way of knowing how many customers may come, a
DArray is ideal for recording the information in memory.
To get a better idea of which services are popular, you are also requested to submit at day's end a
report of the totals and percentages of each service requested by customers and the revenue
gained for each service. Example:
Engine oil Transmission oil Radiator fluid
Customers: 16 2 4
Percent: 80% 10% 16%
Revenue: $400 $100 $20
Total customers: 20
Total sales : $520
(2)(a) You are asked to add a function to the DArray class that allows inserting an item into the
array at a given index. Pictorally:
0| | > A 0| | > A
1| | > B 1| | > B
2| | > C 2| | > C
3| | > D 3| | > Z
|..| 4| | > D
|..|
before after inserting "Z" at index 3
Pseudocode your solution first then code, test and debug it.
(b) Text writers often have need of editing their table of contents regularly; adding, removing
and editing chapter titles and subsections are the common operations. Using your enhanced
version of the DArray, you are asked to develop an application that allows writers to do this. A
sample run may appear something like:
1..Display contents
2..Insert a chapter
3..Insert a chapter item
4..Remove a chapter
5..Remove a chapter item
...
=> 1
1.. Computer architecture
1.1 Memory
1.2 CPU
2.. Programming languages
2.1 C
2.2 Ada
...
=> 3
Insert item in what chapter,item? => 1,1.2
Insert what text into chapter 3? => I/O Devices
=> 1
1.. Computer architecture
1.1 Memory
1.2 I/O Devices
1.3 CPU
2.. Programming languages
2.1 C
2.2 Ada
A Dynamic sorted array
Often the data we wish to store should be ordered in a particular way, say sorted by name, or
ordered by increasing age or salary. This way, the data is nicer to display, it is easier to produce
reports on the data, and perhaps the greatest advantage, is that searches on the key or sorted data
can be performed practically instantaneously.
How would we enhance the DArray to support an ordering feature?
Certainly we could add a function sort() to the list of operations that would transform the
existing list of records from random or scattered order to sorted order. This function could be
called at the discretion of the application designer whenever necessary.
We could even go so far as to support another add function, addSorted() which would insert a
new element. This way the list is sorted on the fly as data is added to the list:
DArray nameList, sorted by name
| | > Zoey | | > Alfred | | > Alfred | | > Alfred
| | | | > Zoey | | > Bertha | | > Bertha
| | | | | | > Zoey | | > Joey
| | | | | | | | > Zoey
|..| |..| |..| | |
addSorted "Alfred" addSorted "Bertha" addSorted "Joey"
Sorting algorithms
Perhaps the easiest sorting algorithm to code and understand is bubble sort, which can be
depicted in a series of pass diagrams:
|5 | |5 | |5 | |5 | |2 | |2 | |2 | |2 |
|22| |8 | |8 | |2 | |3 | |3 | |2 | |2 |
|8 | |22| |2 | |3 | |5 | |2 | |3 | |3 |
|67| |2 | |3 | |8 | |2 | |5 | |5 | |5 |
|2 | |3 | |22| |2 | |8 | |8 | |8 | |8 |
|3 | |45| |2 | |22| |22| |22| |22| |22|
|45| |2 | |45| |45| |45| |45| |45| |45|
|2 | |67| |67| |67| |67| |67| |67| |67|
Pass: 1 2 3 4 5 6 7
. N = number of elements in the list
. On pass 1, the largest element "67" is bubbled up to the
last position in the list
. On pass 2, the second largest element, "45" is bubbled up to
the second last position in the list.
. N1 passes later, the list is sorted
. adjacent elements are "bubbled" to the end of the list
depending on whether (array[j] > array[j+1])
Pseudocode
* Sort the elements in <array> in ascending order
* by bubbling up the largest elements to the end of the list
bubbleSort(array)
for (pass = 1 to nElements)
for (i = 0 to nElements1)
if (array[i] is greater than array[i+1])
swap(array[i],array[i+1])
Algorithm performance
Bubble sort presents a simplistic approach, but is unfortunately slow in execution, especially
with large list sizes.
Specifically, we can say that this algorithm is of order n squared, or O(n2) for short, which means
to say that as n, the size of the list increases, so does the algorithm execution time by a factor of
the square of n. We can readily write down and graph this expression by taking note of the inner
for loop of order n, which multiplies the outer for loop, also of order n.
| . |
| . |
t | . t | . .
i | . i | .
m | . m | .
e | . e |.
n n
Bubble sort Quick sort
O (n2) O (nlog(n))
Quicksort
A better solution is an O(nlog(n)) algorithm called quicksort, which certainly merits
construction.
The algorithm involves two steps:
. Partitioning the list into two sublists
. Sorting each sublist recursively
The partitioning is the trickiest part. We wish to divide the list into two parts, of which all the
elements in the lowest part of the list are less than or equal to those in the higher part of the list.
Consider an unsorted list, L:
L |34|2 |98|44|19|11| 5| 4 |10|42 |
We wish to arrange L into two distinct parts. To the left of a certain pivot value in the list, all the
elements are less than or equal to the pivot value. Let's choose the first element in the list, 34 as
the pivot value. Then the following would be an acceptable partitioning of L, conforming to the
partitioning rule:
|2 |5 |4 |10 | 19 | 11 | 34 | 98 | 44 | 42 |
"less than side" /\ "greater than side"
||
pivot
If we had an algorithm that could arrange a list like this, then we could reapply it to the "less than
side" and the "greater than side" until the list was sorted.
A partitioning algorithm
Partitioning can be done quickly by swapping elements alternately from the "less than" and
"greater than" sides of the list. Consider the following method by example on the original list:
34 2 98 44 19 11 5 4 10 42
Starting from the greater than side, compare each element with the pivot, 34 until an element less
than 34 is found, then swap 34 with that element. In the above case it is 10. The list becomes:
10 2 98 44 19 11 5 4 34 42
Now, starting from the less than side, beginning from the position of the last swapped element,
10 in this case, compare each element with the pivot, 34 until an element greater than 34 is
found, then swap 34 with that element. In the above case it is 98. The list becomes:
10 2 34 44 19 11 5 4 98 42
Back to the greater than side we compare from the last swapped element, 98 to swap 34 with 4:
10 2 4 44 19 11 5 34 98 42
Back to the less than side, we swap 34 with 44:
10 2 4 34 19 11 5 44 98 42
Back to the greater than side, we swap 34 with 5 and we are done because no more elements
need to be swapped, all elements to the left of 34 are less than or equal to 34; all elements on the
right are greater than 34.
10 2 4 5 19 11 34 44 98 42
"less than side" /\ "greater than side"
||
pivot
Quicksort pseudocode
**********************************************************
* Sorts a <list> of elements from index <low> to <high>
* recursively
quicksort(list,low,high)
if (low < high)
pivot = partition(list,low,high)
quicksort(list,low,pivot1)
quicksort(list,pivot+1,high)
**********************************************************
* Arranges a <list> of elements from index <low> to <high>
* into two contiguous sets:
*
* Set 1 appears on the lower portion of list
* Set 2 on the upper portion of the list
* Set 1 has the property that all elements in it are less
* than or equal to the pivot value
* Set 2 has the property that all elements in it are greater
* than or equal to the pivot value
* The pivot value is chosen as list[low]
*
* Returns the index of the pivot value
partition(list,low,high)
...
Binary search
One of the advantages of having the data arranged in sorted order in the array is that we can
employ binary search, a quick "subdivide and conquer" search routine, several times faster than
the "brute force" linear search approach employed in the DArray.
Consider the following sorted namelist and a search for "Martha":
0 Alfred
1 Brenda
2 Carol
3 Dana
4 Edward
5 Frank
6 Hergold
7 Ian
8 Jennifer *
9 Karen < <
10 Laura | | *
11 Martha | | <| *
12 Nancy | < <|
13 Oliver | *
14 Paula |
15 Quintus |
16 Rasputin |
17 Sandy |
18 Theodore <
Pass 1 Pass 2 Pass 3 Pass 4
success!
On pass 1, we subdivide the list in two and compare the name in the middle, the pivot (*),
"Jennifer" for a match with "Martha". If there is no success, we decide which of the two sublists
to check for "Martha" in. Obviously, her name could only be in the upper sublist because
"Martha" appears higher alphabetically than the pivot "Jennifer". Continuing like this, we
squeeze out the result in no less than log2(18) = 4 comparisons.
Performance : O (log2(n))
Packaging the dynamic sorted array
We could go ahead and implement the functions sort() and addSorted() and binarySearch() and
add them to the list of operations in DARRAY.C.
However at this time, it is worthy to mention an alternate packaging, one to preserve the original
contents of DARRAY.C. We could in fact create a new name of whole new data structure that
supports the new philosophy of an ordered list, a DSArray, a dynamic sorted array, and add the
new functionality to another file, DSARRAY.C with its own header DSARRAY.H.
Advantages
. The application designer could choose between a DArray or DSArray type depending on
his/her needs, and without us, the data structure designers having to add to or take away from the
already simple definition/usage of the DArray.
. Such code is somewhat easier to read. When we see a DSArray type in the declaration section
of a function, we know the code deals particularly with a sorted list.
It turns out that this method is preferred in industry, especially in the case of the design of C++
class libraries.
Let's package our DSArray using this philosophy.
DSArray pseudocode
************************************************************
* Insert <data> into <array> at <index>
* All following array elements are bumped up one cell
* Increments the <nElements> count in <array>
insert(array,data,index)
if (nElements+1 > size)
grow(array)
for (i = nElements1 to index step 1)
swap(array[i],array[i+1])
array[index] = data
nElements = nElements + 1
***********************************************************
* Searches for <data> in <array> by successively calling
* <compareFunction>
*
* <closestIndex> is set to the index of the "closest" match
* regardless of whether a match was found or not
*
* Returns <TRUE> or <FALSE> depending on whether a match
* was found
*
* Assumes <array> is sorted by a key value corresponding
* to <compareFunc>
binarySearch(array,data,compareFunction,closestIndex)
if (nElements == 0)
closestIndex = 0
return FALSE
upper = nElements1
lower = 0
do
mid = (upper + lower)/2
result = compareFunction(array[mid],data)
if (result is EQUAL)
closestIndex = mid
return TRUE
else if (result is GREATER THAN)
upper = mid 1
else if (result is LESS THAN)
lower = mid + 1
while (upper >= lower)
if (compareFunction(array[mid],data) == LESS THAN)
closest = mid+1
else
closest = mid
return FALSE
***********************************************************
* Add <data> to <array> in the correct sorted order
* Assumes <array> is currently in sorted order
addSorted(array,data,compareFunction)
binarySearch(d,data,compareFunction,closestIndex)
insert(array,data,closestIndex)
The DSArray definition (DSARRAY.H)
#ifndef DSARRAYH
#define DSARRAYH
#include "darray.h"
enum {GREATER_THAN,LESS_THAN,EQUAL};
typedef DArray DSArray;
extern void sort(DSArray *d,CompareFunction cmp);
extern void addSorted(DSArray *d,void *data,CompareFunction cmp);
extern int binarySearch(
DSArray *d,
void *data,
CompareFunction cmp,
int *closestIndex);
#endif
The DSArray operations (DSARRAY.C)
#include "dsarray.h"
extern int grow(DArray *d);
/* utility functions */
static void swap(void **i,void **j)
{
void* temp = *i;
*i = *j;
*j = temp;
}
static int insert(DSArray *d,void *data,int index)
{
int i;
if (d>nElements+1 > d>size)
{
if (ERROR == grow(d))
return(ERROR);
}
for (i = d>nElements1; i >= index; i)
{
swap(&d>array[i],&d>array[i+1]);
}
d>array[index] = data;
d>nElements++;
return(NO_ERROR);
}
/* Core functions */
void sort(DSArray *d,CompareFunction cmp)
{
int pass,j;
for (pass = 1; pass < d>nElements; pass++)
{
for (j = 0; j < d>nElements1; j++)
{
if ((*cmp)(d>array[j],d>array[j+1]) == GREATER_THAN)
swap(&d>array[j],&d>array[j+1]);
}
}
}
int binarySearch(
DSArray *d,
void *data,
CompareFunction cmp,
int *closestIndex)
{
int upper,lower,mid,result;
if (!d>nElements)
{
*closestIndex = 0;
return(FALSE);
}
upper = d>nElements1;
lower = 0;
do
{
mid = (upper+lower)/2;
result = (*cmp)(d>array[mid],data);
if (result == EQUAL)
{
*closestIndex = mid;
return(TRUE);
}
else if (result == GREATER_THAN)
upper = mid1;
else
lower = mid+1;
}
while (upper >= lower);
if ((*cmp)(d>array[mid],data) == LESS_THAN)
*closestIndex = mid+1;
else
*closestIndex = mid;
return(FALSE);
}
void addSorted(DSArray *d,void *data,CompareFunction cmp)
{
int closestIndex;
binarySearch(d,data,cmp,&closestIndex);
insert(d,data,closestIndex);
}
Code to test the DSArray (DSTEST.C)
#include <stdio.h>
#include <string.h>
#include "dsarray.h"
/* */
/* APPLICATION TEST CODE on integer data */
/* to verify the basic operations of the */
/* DSARRAY, dynamic array data structure */
/* */
void displayData(DArray *d)
{
int i;
printf("[");
for (i = 0; i < nElements(d); i++)
{
int *intData = (int*)get(d,i);
printf("%d ",*intData);
}
printf("]\n");
}
static int compareFunction(void *d1,void *d2)
{
int *id1 = (int*)d1,*id2 = (int*)d2;
if (*id1 > *id2)
return(GREATER_THAN);
else if (*id1 < *id2)
return(LESS_THAN);
else
return(EQUAL);
}
void main()
{
#define NELEMENTS 8
DSArray da,*d = &da;
int i,j=11,data[NELEMENTS] = {5,22,8,67,2,3,45,2};
init(d,2,2);
for (i = NELEMENTS; i >= 0; i)
{
add(d,(void*)&data[i]);
}
displayData(d);
sort(d,compareFunction);
displayData(d);
addSorted(d,(void*)&j);
displayData(d);
wrapup(d,FALSE);
}
Output
[ 5 22 8 67 2 3 45 2 ]
[ 2 2 3 5 8 22 45 67 ]
[ 2 2 3 5 8 11 22 45 67 ]
A fast record search application
Let's take our telephone search application and expand it to suit the needs of a wider application.
Suppose that a recruiting agency or training institute wishes to perform fast searches on client
telephone number and/or on the company that clients are currently working for. Study the
following sample run:
1 Read client file
2 Search for clients by name
3 Search for clients by company
4 Quit
=> 1
Enter client file name: client.dat
40 records read..
=> 2
Enter client name: Halling, K
2 matches found...
Halling, Kim, Computer world, (613) 4324521 ext 2133
Halling, Jake E., Learning Enterprises, (613) 2221141
=> 3
Enter company name: Cybex systems
3 matches found...
Arnold, M., Cybex systems, (613) 5676544 ext 8410
Irina, H., Cybex systems, (613) 5676544 ext 7655
Desere, N. M., Cybex systems, (613) 5676544 ext 8410
...
Obervations:
To perform binary searches on the two fields, name and company, we will need two ordered
DSArrays, one ordered by name and one ordered by company.
We only need one copy of the data. Each sorted DArray points to the data in the correct order.
For efficiency, we can sort the records after they are read, rather than insert the records in sorted
order at read time.
Memory map
Let dSNameList be the list ordered by name
dSCompanyList be the list ordered by company
(*)'s denote the client records as ordered in the input file
(1) Telnet, Joe (2) Orr, Bob (3) Arnold, M (4) Irina, H
Radarscape technology Neton inc. Cybex systems Cybex systems
4565678 5435333 ext 001 5676544 ext 8410 5676544 ext 7655
(5) Desere, N.M (6) Zeren, J. (7) Kaliko, M (8) Geiger, O.
Cybex systems Knick nack suppliers Abraham Inc. The real way
5676544 ext 8410 6666666 4325675 ext 404 4316611
dsNameList dsCompanyList
| . | > (3) | . | > (7)
| . | > (15) | . | > (9)
| . | > (13) | . | > (14)
| . | > (14) | . | > (11)
| . | > (5) | . | > (3)
| . | > (11) | . | > (4)
| . | > (8) | . | > (5)
| . | > (12) | . | > (15)
| . | > (9) | . | > (6)
| . | > (4) | . | > (12)
| . | > (10) | . | > (10)
| . | > (16) | . | > (16)
| . | > (7) | . | > (2)
| . | > (2) | . | > (13)
| . | > (1) | . | > (1)
| . | > (6) | . | > (8)
(9) Halling, Kim (10) Jimlo, B (11) Ena, B (12) Halling, Jake E.
Computer World Magazine Plus Corrington Warehouses Learning Enterprises
4324521 ext 2133 4444112 5437890 ext 87 2221141
(13) Blade, J. (14) Cathleen, S (15) Ballerina, T (16) Johnson, T
Neton, Inc. Computer World Granville exports Magazine Plus
5435333 ext 001 4324521 ext 2101 3211332 8019876
Application pseudocode
ClientStructure: ClientRec
pointer to name
pointer to telephone
pointer to company
********************************************************
* MAIN: display menu to user and decode the options:
* 1 Read client info
* 2 Search for a client by name
* 3 Search for a client by company
* 4 Quit
initialSize = 50
growthSize = 10
main()
DArray dsNameList,dsCompanyList;
ClientRec searchRecord
init(dsNameList,initialSize,growthSize)
init(dsCompanyList,initialSize,growthSize)
repeat
userChoice = displayMenu()
if (userChoice is READ)
input filename
flush(dsNameList,TRUE)
flush(dsCompanyList,FALSE) * Note we cannot delete the actual data
a second time, so we code FALSE
for the <shouldDelete> parameter
on the second flush().
readRecords(filename,dsNameList,dsCompanyList)
sort(dsNameList,nameMatch)
sort(dsCompanyList,companyMatch)
else if (userChoice is SEARCH BY NAME)
input personsName
searchRecord.name = personsName
index = search(dsNameList,nameMatch,searchRecord)
if (index is FOUND)
displayClientDataByName(dsNameList,personsName,index)
else if (userChoice is SEARCH BY COMPANY)
input companyName
searchRecord.company = companyName
index = search(dsCompanyList,companyMatch,searchRecord)
if (index is FOUND)
displayClientDataByCompany(dsCompanyList,companyName,index)
until (userChoice is QUIT)
deallocateRecords(dsNameList)
wrapup(dsNameList,TRUE)
wrapup(dsCompanyList,FALSE)
********************************************************
* Checks if <rec1.name> matches <rec2.name>
* up to the minimum number of characters
* between the two names
* Returns EQUAL upon a match otherwise {GREATER THAN,LESS THAN}
nameMatch(rec1,rec2)
if (rec1.name = rec2.name)
return EQUAL
else if (rec1.name > rec2.name)
return GREATER THAN
else
return LESS THAN
********************************************************
* Checks if <rec1.company> matches <rec2.company>
* up to the minimum number of characters
* between the two names
* Returns TRUE upon a match otherwise FALSE
companyMatch(rec1,rec2)
if (rec1.company = rec2.company)
return EQUAL
else if (rec1.company > rec2.company)
return GREATER THAN
else
return LESS THAN
********************************************************
* Read client records from <fileName>
* and store in heap records
* and add to <dsNameList> and <dsCompanyList>
readRecords(fileName,dsNameList,dsCompanyList)
open fileName
if (successful)
while (not end of file)
read name, company name, telephone
rec = new ClientRec * create a heap record to be
* deleted by the caller
rec.name = name
rec.company = company
rec.telephone = telephone
add(dsNameList,rec)
add(dsCompanyList,rec)
close fileName
********************************************************
* Display full record information for clients with
* matching names of <name> in sorted name
* list <dsNameList> starting from <index>
displayClientDataByName(dsNameList,name,index)
i = index
while ( (i < nElements(dsNameList))
AND matchesMinimally(get(dsNameList,i)>name,name))
displayClientData(dsNameList[index])
index = index + 1
display iindex,"matching records found"
Unit II exercises
(1) The DSArray data structure supports two methods of sorting records. When would it be
desirable to use the addSorted() function over the sort() function? List some examples.
(2) You are asked to create a system that reads <first name>, <last name> author records in
random order from a file and displays the names sorted by first name within last. Example:
Holbrook J. Foster
Holbrook Johnny 1..Jenny
Holbrook Sam 2..K.
Holbrook Abe => Hillington
Lincolne C. 1..Tad
Lincoln A. Holbrook
Foster K. 1..Abe
Foster Jenny 2..J.
Hillington, Tad 3..Johnny
4..Sam
Lincoln
1..A.
Lincolne
1..C.
(3) (a) Users of the name,company search utility have asked for support to facility update/query
on clients' previous work experience. The additional information they wish cached is:
Date, Previous employer, Address, Employer telephone number
In the reporting sequence of the searchreporting scenario, this involves a more detailed view of
the clients. For example, consider the sample run:
...
Enter client name: => Farley, M.
1 Record matched...
Farley M, Anarac data systems, 5677889 ext 809
Work experience:
1996 Bat cable operations, 119 Holgate Rd. 7656545
1994 Jumper cable corp, 4565 Essen way, 7899888 ext 33
1992 Able cable, #2 111 Mean St, 5555555 ext 55
You are asked to sketch the design for the enhanced system. Start with a memory map of your
"view" of the data, then sketch the pseudocode.
(b) If you are adventurous, develop a prototype in C for your design in part (a).
(2) You are requested to upgrade the DSArray to support quicksort. Write pseudocode for the
quicksort algorithm. Walk through your pseudocode with several unsorted lists. Try ones with
only one or two elements, and those with several elements the same value. Write the C code and
test it with numerous examples to verify its integrity.
The linked list
If we take a close hard look at the DSArray, we will observe three things.
. Excellent search performance. O(logn)
. Poor add/delete performance. O(n)
. Mediocre allocation of memory.
When the list is sorted, the DSArray has great search performance, we will not argue with that,
but look at the slow down when inserting or deleting elements. Expensive memory shifts are
necessary, not to mention that the allocation of memory is not optimal. More often than not, the
tailend portion of the DSArray pointer structure is not in use.
If ordering and search performance is the key issue in our application, we will stick with the
DSArray and be content. But if we analyze our application carefully and discover that most of
the operations are of the type {insertion/deletion}, then we may not be overly pleased with the
DArray.
The question is, does there exist another data structure that improves upon the latter failings of
the DSArray?
Yes! The linked list.
What is a linked list?
A linked list is an unordered set of data, not required to be contiguous in memory, yet
grouped via links between adjacent elements starting from a head pointer.
Here is an example of what we may visualize as a singly linked list of five elements:
| Element 1 | <=== head end
/\
|| ||
||
\/ | Element 5 |
| Element 2 | ====> | Element 3 | /\
||
\\ ||
\\ ||
| Element 4 |
The actual data of elements 1..5 may be scattered arbitrarily through memory, but each element
knows where exactly one of his brother resides, so we don't lose the whole.
For example, if we wanted to get to element 3's data, we would start at head, follow the pointer
to element 1, then follow the pointer from 1 to 2 to get to element 2, then follow the pointer from
2 to 3 to get to element 3.
This is what we would call a traversal. By following successive pointers, we can effectively
visit, or traverse the entire data set in the linked list.
Note the beauty of this scenario: We don't need to allocate an array and make assumptions about
the initial size of the list, hence an optimal memory model.
Inserting/deleting elements
Bottom line : great performance!, O(1)
Deleting elements
If we wish to delete an element, we can patch a pointer from the previous element to the next,
and deallocate the piece of data. Effectively, we would have removed the data from the chain.
Suppose that we wish to delete element 3 from the following list:
head => | 1 | => | 2 | => | 3 | => | 4 | => end
\ /\
\__________________||
previous next
Adding elements
Adding an element poses no difficulty either. We just allocate the new piece of data in memory
somewhere, anywhere in fact, and patch a pointer at the insertion point from the previous to the
next element in the chain.
Suppose that we wish to insert the element 3.1 between elements 3 and 4 in the above example:
head => | 1 | => | 2 | => | 3 | | 4 | > end
|| /\
|| ||
|| ||
===>| 3.1 |===
Searching for elements
Bottom line : Thumbs down!
There is no secret that the link list's searching performance is linear, that is, O(n), no different
than the original DArray's.
If we have a thousand elements in the linked list then the worst case scenario is that we have to
visit all the nodes, to find out that the element was not in the list.
The average case scenario for a match would be that we have to visit on average, half of the
elements before we find the one we are looking for.
If searching is important, we had better stick to the DSArray!
Design
In theory, the linked list seems to be a simple entity. In fact it is, but we will see that its
implementation is semitricky.
First, let's identify this data structure's components. We have seen data and links in the pictorial
description. We can even define a linked list as a series of data joined by links. Actually the
two are inseparable and we will call them together a node. Having said this, we can refine our
definition of a linked list as a series of nodes.
head => | DATA | => | DATA | => ...
link link
\_________________/ \_________________/
NODE NODE
Node
pointer to data
pointer to next node
Our initial linked list, devoid of data, turns out to be merely a pointer to a node, set to a sentinel
value, for convenience zero.
head => 0
When the first piece of data is added to the list, head no longer points to 0, the empty set, but
rather a physical node sitting in memory at some address.
head => | FIRST ELEMENT |
| . | => 0
When the next piece of data is added, we must tell the linked list where to insert the data.
Suppose we told it to insert it after head by passing in the head pointer to add() as the place of
insertion. We would get the result:
head => | SECOND ELEMENT | | FIRST ELEMENT |
| . | => | . | => 0
If we told add() to insert a third element after the first element, we would get:
head => | SECOND ELEMENT | | FIRST ELEMENT | | THIRD ELEMENT |
| . | => | . | => | . | => 0
Linked list traversal
Linked list's do not provide random access to data like the DArray does. Recall that the DArray
allows any element to be accessed at any time via the get() function. We merely pass an index to
the function and it returns a pointer to the data in that slot.
When we wish to access an element in the linked list, we must visit each node starting from head
until we get to the one we want.
Equivalently to the DArray, the linked list must provide a get() interface. In the linked list's case
it will not be a direct get, but a get of a pointer to the next element in the list. In order to start
getting next elements in the list, we must have already accessed a previous element. This leads
to the need of a function which returns the very first element in the list.
Traversal functions:
begin() : return a pointer to the first element
in the list
next() : return a pointer to the next element
in the list
* In order to facilitate a next(), the linked list must maintain a record of the current element
being accessed.
Formal definition
Putting all the points of discussion together, we can formulate a data definition for the linked list
with a comprehensive set of operations.
LinkedList
head: pointer to a LinkedList Node
LinkedListNode
data: pointer to data
next: pointer to the next LinkedList Node in the chain
Operations
init()
begin()
current()
next()
add()
remove()
wrapup()
Pseudocode for the operations
****************************************************************
* Initialize a linked list for useful work to be done
* This function must be called first before <llist> can be used
* Initializes head,current to sentinel values
init(llist)
head = NULL
current = NULL
****************************************************************
* Deallocate all structural information associated with <llist>
* if <shouldDelete> is set, all node data is deallocated as well
wrapup(llist,shouldDelete)
node = head
while (node is not NULL)
if shouldDelete
deallocate node>data
nextNode = node>next
deallocate node
node = nextNode
****************************************************************
* Prepare a linked list <llist> to be traversed
* This function must be called before next() can be called
begin(llist)
current = 0
****************************************************************
* Return the pointer to the last accessed or "current"
* element in <llist>
current(llist)
return(current)
****************************************************************
* Returns a pointer to the next element in <llist>
* Updates internal pointer <current>
next(llist)
if (current is NULL) * CASE 1, get 1st element in list
current = head
else
current = current>next * CASE 2, get subsequent
return(current) elements in list
****************************************************************
* Adds <data> element to <llist> after the node <insertAfterNode>
* If <insertAfterNode> is NULL, assumes caller wishes to add
* the node at the head of the list, otherwise adds <data>
* after <insertAfterNode>
add(llist,data,insertAfterNode)
newNode = new LinkedListNode
newNode>data = data
if (insertAfterNode is NULL)
newNode>next = head * CASE 1: add at head
head = newNode
else
newNode>next = insertAfterNode>next * CASE 2: add after head
insertAfterNode>next = newNode
current = newNode
****************************************************************
* Trys to find <data> in <llist> by comparing <data> with
* elements in <llist> using <compareFunction>
* If a match is found <previousNode> is set to the node previous
* to the matched node
* Returns the node if a match found otherwise NULL
find(llist,data,compareFunction,previousNode)
LLBegin(llist)
previousNode = NULL
while (currentNode = LLNext(llist))
if (compareFunction(currentNode>data,data) is EQUAL)
current = currentNode
return(current)
previousNode = currentNode
return(NULL)
****************************************************************
* Trys to remove <data> from <llist> by comparing <data> with
* elements in <llist> using <compareFunction>
* If a match is found the node containing <data> is removed
* If <shouldDelete> is set, the <data> is deallocated
* Returns TRUE if remove is successful, otherwise FALSE
remove(llist,data,compareFunction,shouldDelete)
nodeToDelete = find(llist,data,compareFunction,previousNode)
if (nodeToDelete is NULL)
return(FALSE)
if (previousNode is NULL)
head = nodeToDelete>next
else
previousNode>next = nodeToDelete>next
if (shouldDelete)
deallocate nodeToDelete>data
deallocate nodeToDelete
current = previousNode
The linked list definition (LL.H)
#ifndef LLH
#define LLH
enum {FALSE,TRUE};
enum {ERROR,NO_ERROR};
enum {GREATER_THAN,LESS_THAN,EQUAL};
typedef int (*CompareFunction)(void *data1,void *data2);
typedef struct LLNode
{
void *data;
struct LLNode *next;
} LLNode;
typedef struct
{
LLNode *head;
LLNode *current;
} LinkedList;
extern void LLInit(LinkedList *llist);
extern void LLWrapup(LinkedList *llist,int shouldDelete);
extern void LLBegin(LinkedList *llist);
extern void LLCurrent(LinkedList *llist);
extern LLNode* LLNext(LinkedList *llist);
extern void LLAdd(LinkedList *llist,void *data,LLNode *insertAfterNode);
extern LLNode* LLSearch(
LinkedList *llist,
void *data,
CompareFunction cmp);
extern int LLRemove(LinkedList *llist,
void *data,
CompareFunction cmp,
int shouldDelete);
#endif
The linked list operations (LL.C)
#include "ll.h"
#include <stdlib.h>
void LLInit(LinkedList *llist)
{
llist>head = 0;
llist>current = llist>head;
}
void LLWrapup(LinkedList *llist,int shouldDelete)
{
LLNode *node,*nextNode;
node = llist>head;
while (node)
{
if (shouldDelete)
free(node>data);
/* important to get next node before freeing <node>! */
nextNode = node>next;
free(node);
node = nextNode;
}
}
void LLBegin(LinkedList *llist)
{
llist>current = 0;
}
LLNode* LLCurrent(LinkedList *llist)
{
return(llist>current);
}
LLNode* LLNext(LinkedList *llist)
{
/* CASE 1: get first element in list */
if (!llist>current)
{
llist>current = llist>head;
return(llist>current);
}
/* CASE 2: get subsequent elements in list */
llist>current = llist>current>next;
return(llist>current);
}
void LLAdd(LinkedList *llist,void *data,LLNode *insertAfterNode)
{
LLNode *newNode;
/* Create the new node */
newNode = (LLNode*)malloc(sizeof(LLNode));
newNode>data = data;
if (!insertAfterNode)
{
/* CASE 1: insert element at start of list */
newNode>next = llist>head;
llist>head = newNode;
}
else
{
/* CASE 2: adding after the first element */
newNode>next = insertAfterNode>next;
insertAfterNode>next = newNode;
}
/* Make the added node the current one */
llist>current = newNode;
}
static LLNode* LLFind(
LinkedList *llist,
void *data,
CompareFunction cmp,
LLNode **previousNode)
{
LLNode *currentNode;
LLBegin(llist);
*previousNode = 0;
while (currentNode = LLNext(llist))
{
if ((*cmp)(currentNode>data,data) == EQUAL)
{
llist>current = currentNode;
return(currentNode);
}
*previousNode = currentNode;
}
return(0);
}
LLNode* LLSearch(
LinkedList *llist,
void *data,
CompareFunction cmp)
{
LLNode *previousNode;
return(LLFind(llist,data,cmp,&previousNode));
}
int LLRemove(LinkedList *llist,
void *data,
CompareFunction cmp,
int shouldDelete)
{
LLNode *nodeToDelete,*previousNode;
nodeToDelete = LLFind(llist,data,cmp,&previousNode);
if (!nodeToDelete)
return(FALSE);
if (!previousNode)
{
/* CASE 1: delete element at start of list, update the head pointer */
llist>head = nodeToDelete>next;
}
else
{
/* CASE 2: delete an element after the first element,
patch a previous pointer */
previousNode>next = nodeToDelete>next;
}
if (shouldDelete)
free(nodeToDelete>data);
free(nodeToDelete);
/* make the previous node current */
llist>current = previousNode;
return(TRUE);
}
Code to test the Linked list (LLTEST.C)
#include "ll.h" Output
#include <stdio.h>
#include <stdlib.h> <9>
<4> <9>
void displayData(LinkedList *llist) <3> <4> <9>
{ <2> <3> <4> <9>
LLNode *node; <99> <2> <3> <4> <9>
int *element; <88> <99> <2> <3> <4> <9>
Element < 3 > found
LLBegin(llist); Element < 100 > added after < 3 >
while (node = LLNext(llist)) <88> <99> <2> <3> <100> <4> <9>
{ Element 4 not found
element = (int*)node>data; Element < 88 > removed
printf("<%d> ",*element); <99> <2> <3> <100> <4> <9>
} Element < 3 > removed
printf("\n"); <99> <2> <100> <4> <9>
}
static int compareFunction(void *d1,void *d2)
{
int *id1 = (int*)d1,*id2 = (int*)d2;
if (*id1 > *id2)
return(GREATER_THAN);
else if (*id1 < *id2)
return(LESS_THAN);
else
return(EQUAL);
}
#define NELEMENTS 6
void main()
{
int i,data[NELEMENTS] = {9,4,3,2,99,88};
int three = 3,minusFour = 4,eightyEight = 88,oneHundred= 100;
LinkedList list,*llist=&list;
LLNode *node;
LLInit(llist);
for (i = 0; i < NELEMENTS; i++)
{
LLAdd(llist,(void*)&data[i],0);
displayData(llist);
}
node = LLSearch(llist,(void*)&three,compareFunction);
if (node)
printf("Element < %d > found\n",*(int*)node>data);
LLAdd(llist,(void*)&oneHundred,node);
printf("Element < %d > added after < % d >\n",oneHundred,three);
displayData(llist);
node = LLSearch(llist,(void*)&minusFour,compareFunction);
if (!node)
{
printf("Element 4 not found\n");
}
if (LLRemove(llist,(void*)&eightyEight,compareFunction,FALSE))
printf("Element < %d > removed\n",eightyEight);
displayData(llist);
if (LLRemove(llist,(void*)&three,compareFunction,FALSE))
printf("Element < %d > removed\n",three);
displayData(llist);
LLWrapup(llist,FALSE);
}
A linked list of heterogeneous data
So far we have assumed a single type of data residing in nodes of our data structures. If we
choose students to store in our DArray we would not populate it also with telephone records.
But note, with our data structure design centered around the void* data type, we can populate our
data structures with any type of data. At first this may seem silly, but we will learn that powerful
storage paradigms can be derived from this technique.
Let's study a basic example.
A holiday ticket agency sells various types of tickets to various types of clients, specifically:
(1) Live concerts
(2) Boat cruises
(3) Sightseeing tours
They wish to record all the tickets sold from various locations throughout the province. Any
particular shop will sell tickets during the day, record all the info judiciously, and at the stroke of
five, when the business day ends, immediately get a hardcopy of all the sales, while a clerk fires
up all the relevant data to home office. The ticket details are as follows:
Concert info: Boat cruise info Tour info
Performing artist: Destination: Details:
Date: Date: Date:
Seat # Company: Start time:
Cost: Cost: End time:
Rain date: Cost:
Design
Searching and sorting is not a requirement in this application. Why opt for the overhead of a
DSArray when we can add incoming ticket information to the end of a linked list?
This is a nice idea but how do we handle the varying types in one list?
Of course if we were to mix data types in the same data structure, we would quickly realize that
the application would have to decode the data as it is snatched from the data structure.
Remember the data structure has no knowledge of the details of the data, it only returns a void*
pointer to the caller.
The application could facilitate this by having a type field in every record which defines how to
decode it:
Concert info Boat cruise Tour
type = CONCERT type = BOAT_CRUISE type = TOUR
artist destination details
date date date
seat ... ...
...
We can make a generic type called Ticket, consisting of some core fields and a union of all the
Concert, Cruise, and Tour structures.
Data definition
Type : [ CONCERT, CRUISE, TOUR ]
Ticket
type < Field to decode the specific ticket
clientName < Data common to
cost < all tickets
union of Concert, Cruise and Tour < Specific ticket data
Concert
artist
date
seatNumber
rainDate
Cruise
destination
date
company
Tour
details
date
startTime
endTime
Memory map
llist
| . > current
| . |
|
| head
\ /
.
| . >| CONCERT |
| . | | A.K Lansley |
| | $26.00 |
| | The blues brothers |
| | 10/01/96 |
| | 82A |
| | 10/10/96 |
|
|
\ /
.
| . >| CRUISE |
| . | | Alison R. |
| | $60.00 |
| | Dow's lake |
| | 11/20/96 |
| | Valley cruise inc. |
|
|
\ /
.
| . >| TOUR |
| . | | Ross E. |
| | $15.00 |
| | Old city |
| | 11/20/96 |
| | 9:00 AM |
| | 3:00 PM |
|
|
|
|
\ /
.
.
.
Pseudocode
****************************************************************
* Input ticket records from the user and store them in a linked
* list until QUIT is selected
* At this time, a report of all tickets sold is written to
* an output file
*
* MENU:
*
* 1 = CONCERT ticket
* 2 = BOAT CRUISE ticket
* 3 = TOUR ticket
* 4 = QUIT
main()
init(llist)
lastNode < current(llist)
do
choice < doMenu()
if (choice = CONCERT)
ticketRecord < readConcertRecord()
else if (choice = CRUISE)
ticketRecord < readCruiseRecord()
else if (choice = TOUR)
ticketRecord = readTourRecord()
add(llist,lastNode,ticketRecord)
lastNode < current(llist)
while (choice is not QUIT)
writeTicketRecords(llist)
deallocateRecords(llist)
wrapup(llist,TRUE)
****************************************************************
* Displays a menu of four choices to the user and asks the
* user for his/her choice
* Returns the choice without validation
doMenu()
display "1 CONCERT ticket"
display "2 BOAT CRUISE ticket"
display "3 TOUR ticket"
display "4 QUIT"
input choice
return (choice)
****************************************************************
* Input the required information for purchasing a concert
* ticket from the user
* Creates the new ticket record to store the information in
* and returns the pointer to this record to the caller
*
* NOTE: it is the responsibility of the caller to deallocate
* the record and all its fields
* <artist>, <date>, <seatNumber>, <rainDate>
readConcertRecord()
ticketRecord < new Ticket
readGeneralTicketInfo(ticketRecord)
input ticketRecord.artist
input ticketRecord.date
input ticketRecord.seatNumber
input ticketRecord.rainDate
return (ticketRecord)
****************************************************************
* Input the general information common to all tickets
* from the user
* Fields include <purchaser's name> and <ticket cost>
readGeneralTicketInfo(ticketRecord)
input ticketRecord.name
input ticketRecord.cost
****************************************************************
* Write all ticket records in <llist> to a file
writeTicketRecords(llist)
open file
begin(llist)
while (node < next(llist))
ticketRecord < node.data
writeTicketRecord(file,ticketRecord)
close file
****************************************************************
* Write complete details of <ticketRecord> to <file>
* First the record header information is written
* Then the ticket record <type> is decoded and control transferred
* to the appropriate lowlevel file output function
* {concert, cruise, tour}
writeTicketRecord(file,ticketRecord)
write to file, ticketRecord.name
write to file, ticketRecord.cost
if (ticketRecord.type = CONCERT)
writeConcertRecord(file,ticketRecord)
else if (ticketRecord.type = CRUISE)
writeCruiseRecord(file,ticketRecord)
else if (ticketRecord.type = TOUR)
writeTourRecord(file,ticketRecord)
****************************************************************
* Write all details of concert record to <file> assuming
* that <ticketRecord> is encoded in concert record form
writeConcertRecord(file,ticketRecord)
write to file, ticketRecord.artist
write to file, ticketRecord.date
write to file, ticketRecord.seatNumber
write to file, ticketRecord.rainDate
Selected application code
TICKET.H
#ifndef TICKETH
#define TICKETH
enum {CONCERT=1,CRUISE,TOUR};
typedef struct
{
char *artist;
char *date;
char *seat;
char *rainDate;
} Concert;
typedef struct
{
char *dest;
char *date;
char *company;
} Cruise;
typedef struct
{
char *details;
char *date;
char *startTime;
char *endTime;
} Tour;
typedef union
{
Concert concert;
Cruise cruise;
Tour tour;
} TicketData;
typedef struct
{
int type;
char *name;
float cost;
TicketData ticketData;
} Ticket;
#endif
TICKET.C
#include "ll.h"
#include "ticket.h"
#include <stdio.h>
#include <stdlib.h>
#define MAX_CHARS 80
#define FILENAME "C:\\TICKET.DAT"
static int doMenu()
{
int reply;
printf("1 Concert\n");
printf("2 Boat cruise\n");
printf("3 Tour\n");
printf("4 Quit\n");
printf("Enter 14: ");
scanf("%d",&reply);
return(reply);
}
static void readGeneralTicketInfo(Ticket *ticket)
{
char name[MAX_CHARS];
printf("Enter client's name:");
scanf("%s",name);
ticket>name = (char*)malloc(sizeof(name)+1);
strcpy(ticket>name,name);
printf("Enter ticket price $:");
scanf("%f",&ticket>cost);
}
static Ticket* readConcertRecord()
{
Ticket *ticket;
TicketData *ticketData;
char buf[MAX_CHARS];
ticket = (Ticket*)malloc(sizeof(Ticket));
readGeneralTicketInfo(ticket);
ticket>type = CONCERT;
ticketData = &ticket>ticketData;
printf("Enter artist:");
scanf("%s",buf);
ticketData>concert.artist = (char*)malloc(sizeof(buf)+1);
strcpy(ticketData>concert.artist,buf);
printf("Enter concert date:");
scanf("%s",buf);
ticketData>concert.date = (char*)malloc(sizeof(buf)+1);
strcpy(ticketData>concert.date,buf);
...
return(ticket);
}
static Ticket* readCruiseRecord()
{
Ticket *ticket;
TicketData *ticketData;
char buf[MAX_CHARS];
ticket = (Ticket*)malloc(sizeof(Ticket));
readGeneralTicketInfo(ticket);
ticket>type = CRUISE;
ticketData = &ticket>ticketData;
printf("Enter destination:");
scanf("%s",buf);
ticketData>cruise.dest = (char*)malloc(sizeof(buf)+1);
strcpy(ticketData>cruise.dest,buf);
printf("Enter date:");
scanf("%s",buf);
ticketData>cruise.date = (char*)malloc(sizeof(buf)+1);
strcpy(ticketData>cruise.date,buf);
...
return(ticket);
}
static Ticket* readTourRecord()
{
...
}
static int writeConcertRecord(FILE *fp,Ticket *ticket)
{
TicketData *ticketData;
Concert *concert;
ticketData = &ticket>ticketData;
concert = &ticketData>concert;
fprintf(fp,"%s\n",concert>artist);
fprintf(fp,"%s\n",concert>date);
fprintf(fp,"\n");
}
static int writeCruiseRecord(FILE *fp,Ticket *ticket)
{
TicketData *ticketData;
Cruise *cruise;
ticketData = &ticket>ticketData;
cruise = &ticketData>concert;
fprintf(fp,"%s\n",cruise>dest);
fprintf(fp,"%s\n",cruise>date);
fprintf(fp,"\n");
}
static int writeTourRecord(FILE *fp,Ticket *ticket)
{
...
}
static void writeTicketRecord(Ticket *ticket)
{
/* write common data to all tickets */
fprintf(fp,"%d: %s\n",clientNumber++,ticket>name);
fprintf(fp,"$%6.2f\n",ticket>cost);
/* write details of each ticket sold */
if (ticket>type == CONCERT)
writeConcertRecord(fp,ticket);
else if (ticket>type == CRUISE)
writeCruiseRecord(fp,ticket);
else if (ticket>type == TOUR)
writeTourRecord(fp,ticket);
}
static int writeTicketInfoToFile(LinkedList *llist)
{
int clientNumber;
LLNode *node;
Ticket *ticket;
FILE *fp;
if (!(fp = fopen(FILENAME,"w")))
return(FALSE);
LLBegin(llist);
clientNumber = 1;
while (node = LLNext(llist))
{
ticket = (Ticket*)node>data;
writeTicketRecord(fp,ticket);
}
fclose(fp);
return(TRUE);
}
static void deallocateConcertRecord(Ticket *ticket)
{
Concert *concert;
TicketData *ticketData;
ticketData = &ticket>ticketData;
concert = (Concert*)ticketData;
free(concert>artist);
free(concert>date);
...
}
static void deallocateCruiseRecord(Ticket *ticket)
{
Cruise *cruise;
TicketData *ticketData;
ticketData = &ticket>ticketData;
cruise = (Cruise*)ticketData;
free(cruise>dest);
free(cruise>date);
...
}
static void deallocateTourRecord(Ticket *ticket)
{
...
}
static void deallocateTickets(LinkedList *llist)
{
LLNode *node;
Ticket *ticket;
LLBegin(llist);
while (node = LLNext(llist))
{
ticket = (Ticket*)node>data;
/* deallocate data common to all tickets */
free(ticket>name);
/* deallocate particular ticket details */
if (ticket == CONCERT)
deallocateConcertRecord(ticket);
else if (ticket == CONCERT)
deallocateCruiseRecord(ticket);
else if (ticket == TOUR)
deallocateTourRecord(ticket);
}
}
void main()
{
LinkedList list,*llist=&list;
LLNode *node,*lastNode;
int choice;
Ticket *ticket;
LLInit(llist);
lastNode = LLCurrent(llist);
do
{
choice = doMenu();
ticket = 0;
if (choice == CONCERT)
ticket = readConcertRecord();
else if (choice == CRUISE)
ticket = readCruiseRecord();
else if (choice == TOUR)
ticket = readTourRecord();
if (ticket)
{
LLAdd(llist,(void*)ticket,lastNode);
lastNode = LLCurrent(llist);
}
} while ((choice >= CONCERT) && (choice <= TOUR));
writeTicketInfoToFile(llist);
deallocateTickets(llist);
LLWrapup(llist,TRUE);
}
Doubly linked lists
Sometimes we may wish to traverse the elements from the end of the list backwards, or visit the
element previous to the one current.
If we add backward links to our singly linked list model and a previous() function, we can create
a doubly linked list.
This data structure is specifically engineered for bidirectional sequential access: browsing help
pages, documents, records etc.
Definition
LList
head:
tail:
current:
LLNode
pointer to data
pointer to previous node
pointer to next node
|DATA 1| ====> |DATA 2| ====> ... ====> |DATA N|
head > | | <==== | | <==== <==== | |
/\
||
tail
Operations
init()
begin()
current()
next()
previous()
add()
remove()
search()
wrapup()
Sorted linked lists
The other enhancement we can make to a linked list is to insert the elements in sorted order. The
result is always a sorted list.
This could be effected by the mere addition of a function:
addSorted(llist,data,comparisonFunction)
to the linked list library.
Performance
Each addSorted() call is O(n).
Note that the linked list addSorted() performance is no worse than the DSArray's. They both are
O(n), yet for different reasons.
Recall that the DSArray addSorted() bottleneck occurs when the insert memory shift takes place.
The linked list bottleneck occurs when searching linearly for the insert position. For the linked
list, we can't use binary search because we don't have random access to the elements!
A one time DSArray quicksort is definitely faster, but if we know that the linked list is fairly
static, or equivalently if adds are infrequent, addSorted() is acceptable.
Note that this is not a good way of going about sorting elements! DSArray quicksort is faster,
but if we know that the linked list is fairly static or equivalently, if adds are infrequent,
addSorted() has a cause.
Unit III exercises
(1) (a) Implement a sorted linked list as outlined in the notes.
(b) Use the sorted linked list from part (a) to enhance the ticket program. You are required to
generate two other reports: one which displays all the tickets sold during the day, sorted from
highest price to lowest price, a second which reports the tickets upon date of use. Send the
output to two separate files, report1.dat and report2.dat.
(2) (a) Design a doubly linked list from memory map to pseudocode to C code. Test and debug
it.
(b) Use your doubly linked list to create a record browser that allows both forward and backward
motion through the records via F and N keyboard commands. The details of the records and how
you populate your linked list with those records is up to you. Be creative!
(3) Implement a function called forEach(llist,codeToExecute), a useful and generic linked list
traversal function.
It scans all the elements in <llist> and executes the function codeToExecute() for each element,
passing a pointer to the current element.
For example, suppose we wanted to display all tickets whose cost was greater than $40, we could
write code like:
void displayExpensiveTickets(void *pointerToTicket)
{
Ticket *ticket;
ticket = (Ticket*)pointerToTicket;
if (ticket>cost > 40)
displayTicketRecord(ticket)
}
void main()
{
LinkedList linkedList,*llist=&linkedList;
...
LLForEach(llist,displayExpensiveTicket)
}
(4) A ring list, or circular list, is useful for polling data sequentially. There is neither a head nor
tail, just an entry point into a looped chain of data:
entry > | A | ===> | B |
/\ ||
|| ||
|| \/
| D | <=== | C |
Entry can point to any node in the loop.
When we traverse the data, we actually traverse the data forever: A, B, C, D, A, B, C, D, A, B, ...
(a) Create a circular linked list data structure CLinkedList, based on the singly linked list design.
The operation interface remains the same as the singly linked list's.
(b) Use this new data structure to create the following application:
Electronic bulletins at airports announce recent arrivals and nearfuture departures. Consider a
single line bulletin board that updates its flight information every t seconds for flights within a w
minute time window. Suppose t = 5 and w = 30. Then we would expect arrival/departure detail
lines to update every five seconds, showing those arrivals within the last 30 minutes and those
departures due within the next 30 minutes. Here is a snapshot at 11:20 AM :
| Arrivals | Departures | Current Time : 11:20 |
| | | |
| TWA 678 Frankfurt 11:13 | JA 99 Paris 11:40 | |
Suppose flight information is fixed and recorded in the file flight.dat. Your program can read
this file, along with a suggested user defined t and w, and simulate the above bulletin display.
Check the system help to find out how to query the current date and time in code.
Sample flight data (flight.dat):
TWA_66 Moscow 11:01 A
KLM_504 Athens 11:07 D
TWA_678 Frankfurt 11:13 A
MNB_01 New_York 11:15 A
KLM_113 London 11:17 D
DBA_333 Helsinki 11:24 D
JA_99 Paris 11:40 D
JA_102 Boston 14:01 A
...
(c) Enhance your application to simulate bulletin boards of s lines of flight details. Suppose s =
3, then up to three arrival/departure lines can be displayed at any one time:
| Arrivals | Departures | Current Time : 11:15 |
| | | |
| MNB_01 New York 11:15 | KLM 113 London 11:17 | |
| TWA 678 Frankfurt 11:13 | DBA 333 Helsinki 11:24 | |
| TWA 66 Moscow 11:01 | JA 99 Paris 11:40 | |