Princeton University
COS 217: Introduction to Programming Systems

Assignment 2: A Symbol Table ADT


The purpose of this assignment is to help you learn/review (1) aspects of advanced C programming, and (2) how to create abstract data types (ADTs) in C. It will also give you the opportunity to gain more experience with the GNU/UNIX programming tools, especially Emacs, gdb, and gcc.


A symbol table is an unordered collection of bindings. A binding consists of a key and a value. A key is a string that uniquely identifies its binding; a value is data that is somehow pertinent to its key. A symbol table allows its client to insert (put) new bindings, to retrieve (get) the values of bindings with specified keys, and to remove bindings with specified keys. Symbol tables are used often in programming systems; compilers and assemblers use them extensively.

There are several reasonable ways to implement a symbol table. A simple implementation might store the bindings in a linked list. Linked lists are described in Section 17.5 of C Programming: A Modern Approach (King) and Section 2.7 of The Practice of Programming (Kernighan and Pike). A more efficient implementation might use a hash table. Hash tables are described in Section 2.9 of The Practice of Programming, Chapter 8 of C Interfaces and Implementations (Hanson), and Chapter 14 of Algorithms in C, Parts 1-4 (Sedgewick).

Your Task

Your task in this assignment is to create an ADT named SymTable. Each instance of the SymTable ADT should be a symbol table. You should design your SymTable ADT so it is "generic." That is, you should design your SymTable so its values are void pointers, and thus can point to data of any type.

You should create two implementations of your SymTable ADT: one that uses a linked list and another that uses a hash table.

Subsequent assignments will ask you to create programs that are clients of your SymTable ADT.

The SymTable Interface

The SymTable interface should be stored in a file named symtable.h. It should contain these function declarations:

SymTable_T SymTable_new(void);
void       SymTable_free(SymTable_T oSymTable);
int        SymTable_put(SymTable_T oSymTable, const char *pcKey, const void *pvValue);
int        SymTable_contains(SymTable_T oSymTable, const char *pcKey);
void      *SymTable_get(SymTable_T oSymTable, const char *pcKey);
int        SymTable_remove(SymTable_T oSymTable, const char *pcKey);
void       SymTable_map(SymTable_T oSymTable, 
                        void (*pfApply)(const char *pcKey, void *pvValue, void *pvExtra),
                        const void *pvExtra);
The SymTable_new function should return a new SymTable_T that contains no bindings.

The SymTable_free function should free all memory occupied by oSymTable.

If no binding with key pcKey exists in oSymTable, then the SymTable_put function should add a new binding to oSymTable consisting of key pcKey and value pvValue, and should return 1 (TRUE).  Otherwise the function should not change oSymTable, and should return 0 (FALSE). It should be a checked runtime error for oSymTable or pcKey to be NULL.

The SymTable_contains function should return 1 (TRUE) if oSymTable contains a binding whose key is pcKey, and 0 (FALSE) otherwise. It should be a checked runtime error for oSymTable or pcKey to be NULL.

The SymTable_get function should return the value of the binding within oSymTable whose key is pcKey, or NULL if no such binding exists. It should be a checked runtime error for oSymTable or pcKey to be NULL.

If a binding with key pcKey exists within oSymTable, then the SymTable_remove function should remove that binding from oSymTable and should return 1 (TRUE).  Otherwise the function should not change oSymTable, and should return 0 (FALSE). It should be a checked runtime error for oSymTable or pcKey to be NULL.

The SymTable_map function should apply function *pfApply to each binding in oSymTable, passing pvExtra as an extra parameter. It should be a checked runtime error for oSymTable or pfApply to be NULL.

A SymTable should "own" its keys. That is, the SymTable_put function should not simply store the value of pcKey within the binding that it creates. Rather, the SymTable_put function should make a copy of string pcKey, and store the address of that copy within the new binding. You will find the standard C functions strlen and malloc useful for making the copy. Conversely, a SymTable should not own its values. Indeed it cannot own its values; since is cannot determine the sizes of its values, it cannot create copies of them.

The SymTable Linked List Implementation

Your SymTable linked list implementation should:

The SymTable Hash Table Implementation

Your SymTable hash table implementation should:
#define HASH_MULTIPLIER 65599
static unsigned int SymTable_hash(const char *pcKey)

/* Return a hash code for pcKey.  Adapted from the Fall 2003 
   COS 217 lecture notes. */

   size_t ui;
   unsigned int uiHash = 0U;
   for (ui = 0; pcKey[ui] != '\0'; ui++)
      uiHash = uiHash * HASH_MULTIPLIER + pcKey[ui];
   return uiHash;

See page 578 of the Sedgewick textbook for an alternative. 


You should develop on arizona, using Emacs to create source code and gdb to debug.

A minimal test program is available in the file /u/cs217/Assignment2/testsymtable.c. You may use it as a starting point for creating your own test programs.

Create a "readme" text file that contains:

Submit your work electronically on arizona via the command:

/u/cs217/bin/submit 2 symtable.h symtablelist.c symtablehash.c readme


As always, we will grade your work on correctness and design, and will consider understandability to be an important aspect of good design. To encourage good coding practices, we will compile using "gcc -Wall -ansi -pedantic" and take off points based on warning messages during compilation.

A Challenge (Optional)

Enhance your SymTable hash table implementation so the hash table can expand. That is, for efficiency your hash table implementation should dynamically increase the number of buckets (and, necessarily, reposition all bindings) whenever the number of bindings becomes too large.

More precisely, page 126 of the Hanson textbook provides a sequence of integers that are appropriate bucket counts: 509, 1021, 2053, 4093, 8191, 16381, 32771, and 65521. When the SymTable_put function detects that the new binding count exceeds 509, it should increase the bucket count to 1021. When the function detects that the new binding count exceeds 1021, it should increase the bucket count to 2053. Etc. When SymTable_put detects that the new binding count exceeds 65521, it should not increase the bucket count. Thus 65521 is the maximum number of buckets that the SymTable should contain.

In your readme file, note the CPU times consumed by testsymtable.c using your enhanced hash table implementation with binding counts 100, 1000, 10000, and 100000.

You will receive a better grade if you submit a working non-expanding hash table implementation instead of a non-working expanding hash table implementation. If your attempts to develop the expansion code fail, then leave the expansion code in your symtablehash.c file as comments, and describe your attempts in your readme file.