Dictionary DatabaseThis is a fairly large program, and you will only haveto implement part of it. You will be provided with the source code for some basic functions, the structureyou can use to repres
Dictionary DatabaseThis is a fairly large program, and you will only haveto implement part of it. You will be provided with the source code for some basic functions, the structureyou can use to repres
Dictionary Database
This is a fairly large program, and you will only have
to implement part of it. You will be provided with the source code for some basic functions, the structure
you can use to represent the information for each book, and also the code for the top-level keyboard interface
to the program. Your task will be to build the lower level functions that build and manipulate the database.
You will represent the library database as either an array or a tree of structures, one structure per book,
each containing the relevant title, author name and year of publication for the book. The struct Book
structure you will be provided with contains this information, as follows:
#define MAX_TITLE_LENGTH 100
#define MAX_AUTHOR_LENGTH 100
/* Book structure
*/
struct Book
{
/* Book details */
char title[MAX_TITLE_LENGTH+1]; /* title string */
char author[MAX_AUTHOR_LENGTH+1]; /* author name string */
int year; /* year of publication */
/* pointers to left and right branches pointing down to next level in
the binary tree (for if you use a binary tree instead of an array) */
struct Book *prev, *next;
};
So the book title is represented by a string of maximum length MAX TITLE LENGTH (+1 to include the string
termination character), the author name as another string of maximum length MAX AUTHOR LENGTH, and the
year of publication as an integer. If either the title or author name is longer than the specified maximum
length, it should be truncated to the maxmimum length.
Representing a Database with an Array
In your program you will manipulate struct Book structures to build and modify the database. The simplest
way to represent several structures at once is as an array of structures. Let us set the maximum number of
books in our database to a defined constant MAX BOOKS:
/* maximum number of books that can be stored at once in the library
database (relevant only to storage using an array) */
#define MAX_BOOKS 200
Then we can define an array of MAX BOOKS structures, and use an integer to represent the number of books
currently represented in the program, which of course is initialized to zero:
/* array of books */
struct Book book_array[MAX_BOOKS];
2
/* number of books stored */
int no_books = 0;
This array representation may be used to represent our database. However there are some limitations with
using an array to represent a database:
• An array has a fixed size. The array book array of struct Book structures declared above can
obviously not hold details for more than MAX BOOKS books. This is only OK if you know beforehand
the maximum number of books you are likely ever to have, which is not likely.
• An array is inefficient in storing the information, again because of its fixed size. If you guess the
maximum number MAX BOOKS of books, and declare an array of that size, but in fact the program
actually only creates a much smaller database, most of the memory block used for the array is wasted.
• An array is inefficient in implementing important operations, especially if the elements of the array
are maintained in a specific order. Let us assume that our array is stored in alphabetical order of
title, and we wish to insert a new book “Fever Pitch” into the array shown in figure 1. As you can
see, insertion into an ordered array is a very inefficient operation, because every array element (in
this case struct Book structure) beyond the insertion point must shift up one place to make room.
This involves lots of copying data. The same applies to deleting a book, when all the book structures
beyond the deletion point would have to shift back one place.
The binary tree data structure gets around all these problems. It is a dynamic data structure, which expands
and contracts as the database size goes up and down.
However, the tree is also a much more complicated structure to create and work with. Implementing binary
trees involves the use of an advanced C feature called dynamic memory allocation. We shall first introduce
Figure 1: Illustration of the inefficiency of the array representation of an ordered database. Here the database
of books is ordered alphabetically by title name, and a new book “Fever Pitch” is to be inserted in the existing
array (a) of five books. To insert the new book, all the books with titles alphabetically “greater” than “Fever
Pitch” have to shift up one place in the array to make room for it.
3
the concept of dynamic memory allocation and provide a simple example, before discussing how we can use
it to efficiently build a binary tree.
If you do not want to use binary trees, you may use the much simpler array representation, but in this case
your mark will be limited to 90% for this assignment. Another possibility would be to convert your code
to the binary tree representation after you have successfully implemented the array representation. If you
do not want to know about binary trees right now, you may turn immediately to part 1 of the assignment
description on page 8.
Dynamic memory allocation
We must first introduce the mechanism provided in C to create temporary blocks of memory within a
program. The two most important functions that implement dynamic memory allocation in C are malloc()
and free(). malloc() is used to allocate a block of unused computer memory which can then subsequently
be used in the program. It is declared as:–
void *malloc(size_t size);
The size argument specifies how many bytes you want in your block of memory. The memory block is
returned as a generic pointer (void *) type, and can be cast (converted) to whatever C pointer type you
wish. When the program has finished with the memory block, the free() function is used to discard it so
that it may be used again:-
void free(void *ptr);
The void * memory block returned by malloc() should be passed to free() when it is not needed any
more. Here is a simple program using dynamic memory allocation.
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
int main ( int argc, char *argv[] )
{
char *string;
int length = 200;
string = (char *) malloc(length);
strcpy ( string, "This is a string that fits easily into 200 bytes" );
printf ( "String: %s\n", string );
free ( string );
return 0;
}
Firstly the character pointer string is declared. As you should know by now, a pointer can be used to point
to a single object, or it may specify the start of an array. In this case we dynamically create an array of
characters by calling malloc(), and set string to point to the start of it. string can then be used exactly
as if it had been declared as an array of characters, and we show an example, copying another string into it
and printing the copied string. The code for the alternative array version of the program would be
#include <string.h>
#include <stdio.h>
4
int main ( int argc, char *argv[] )
{
char string[200];
strcpy ( string, "This is a string that fits easily into 200 bytes" );
printf ( "String: %s\n", string );
return 0;
}
Comparing the two versions of the program, we note that
1. The version using dynamic memory allocation is more flexible, in that the size of the string allocated
need not be a constant (200 in this case) but can be any integer value, for instance (as here) the value
of an integer variable. The size of the allocated block of memory is determined when the program is
run. In the second version, the array size is fixed.
2. On the other hand, the array version is shorter and simpler. If malloc() is called, you must remember
to add a free() statement at the point where the program has finished using the memory block,
because otherwise the memory will be wasted. The memory taken up by arrays (allocated from the
“stack”) is returned automatically when the program block in which it was declared terminates, in this
simple case the end of the program. Predefined arrays are in this sense easier to use than dynamically
allocated arrays.
Another use of dynamic memory allocation is to create an instance of a structure. So for instance the
program segment
{
struct Book *new;
new = (struct Book *) malloc ( sizeof(struct Book) );
strcpy ( new->title, "Titus Groan" );
strcpy ( new->author, "Mervyn Peake" );
new->year = 1946;
free ( new );
}
creates a memory block the right size to hold a struct Book structure, sets new to point to it, and then fills
the fields of the structure with details of a book. The call to free() discards the memory block so that it
can be used again. Note the -> operator, which allows you to access fields of a structure via a pointer to it.
In fact new->author is equivalent to (*new).author, but a little simpler and more concise.
Without dynamic memory allocation one could write this program segment as
{
struct Book book, *new;
new = &book;
strcpy ( new->title, "Titus Groan" );
strcpy ( new->author, "Mervyn Peake" );
new->year = 1946;
}
5
Again this program segment is exactly equivalent. The book structure is here allocated from the “stack”,
and returned to the stack automatically at the end of the program segment.
NOTE: To use malloc() and free() in your program you must first #include the header file stdlib.h.
Binary Trees
So far the only benefit we have mentioned of dynamic memory allocation is the extra flexibility of being able
to create blocks of memory with size specified as the program runs rather than being fixed beforehand. The
other major benefit becomes clear if we extend the above program segment as follows:–
struct Book *book_tree = NULL;
{
struct Book *new;
/* create the first book */
new = (struct Book *) malloc ( sizeof(struct Book) );
strcpy ( new->title, "Something Happened" );
strcpy ( new->author, "Joseph Heller" );
new->year = 1973;
/* add first book to binary tree */
new->left = new->right = NULL;
book_tree = new;