Reed CSCI 221 Fall 2020
Homework 05: linked lists
Due October 8th at 4pm.
This assignment has two parts. Each ask you to mimic our
llist
library code from lecture. You’ll invent a linked
list implementation of an ordered dictionary data structure and
also a linked list implementation of a queue data
structure.
This folder contains the linked list library as the source files
llist.hh
and llist.cc
, along with the sample
client test_llist.cc
. There is also a Makefile
and the starting code for the second part of the assignment. Once you’ve
completed the assignment you will have edited or created 6 files that
you’ll need to submit, namely
order.cc order.hh test_order.cc (Part 1)
queue.cc queue.hh test_queue.cc (Part 2)
Please work to mimic the coding style guidelines and formatting I’ve
suggested so far. Include comments that describe the code, when
appropriate. Include some comments at the top of your source code,
including your name and other brief info describing the program. There
should also be a brief comment that precedes each struct, function, and
procedure definition you write. Mimic the work I’ve done in the
llist
files.
You should work to complete each of these exercises so that they
compile and run correctly. Should you face problems in solving an
exercise, you can still submit broken code. In your top comment in that
file PLEASE let us know in what way the code is
broken (why it doesn’t compile, or what tests failed, etc.). In
many cases, we are willing to give partial credit if you identify
problems in the code.
Part 1: write ordered
For this exercise, you’re going to invent a different linked list
data structure that holds a sequence of key-value pairs, sorted
by the key. This is called an ordered dictionary data
structure. We’ll make the keys of type std::string
and
we’ll have each key’s associated value be of type int
. The
resulting code will mimic many aspects of Python’s dict
type, except entries won’t be removed, and missing entries will have a
default associated value of 0.
The best way to develop this code is to make a copy of the
llist
source files with the commands:
cp llist.cc ordered.cc
cp llist.hh ordered.hh
cp test_llist.cc test_ordered.cc
In these you’ll create a new namespace called ordered
and invent a new struct type called dict
, rather than
node
and llist
. The test client program for
ordered
should have a main
that uses of a
value of type ordered::dict*
. We’ll use entry
as the type name for its linked list nodes, rather than the type name
node
. The code in ordered.cc
will use pointers
of type entry*
rather than node*
.
In ordered.hh
define a struct entry
that
contains three components:
• key
: a std::string
value stored for a
dictionary entry,
• value
: an int
stored for each entry,
associated with its key string, and
• next
: a struct entry*
pointer to the next
entry in the dictionary.
Also in ordered.hh
define a dict
struct
that contains two components:
• first
: an entry*
that points to the first
entry in the dictionary, or is nullptr
, and
• defaultValue
: an int
that represents the
associated values of keys that don’t yet have an entry.
You are to define these functions in ordered.cc
:
• dict* build(int v)
: returns a pointer to an empty
dictionary. Missing entries have a default associated value of
v
. • int get(dict* D, std::string k)
: returns
the value associated with the entry with key k
, or the
default value if that key has no entry yet in D
.
• void set(dict* D, std::string k, int v)
: updates
D
so that value v
is associated with key
k
. This could either add an entry into the linked list if
k
doesn’t yet have one, or update an existing entry if
k
has one. • std::string toString(dict* D)
:
give back a Python representation of the dictionary’s existing entries,
and in increasing alphabetical order by their keys.
For all of these functions, the linked list that you maintain should
be in alphabetical order of the entry’s keys. This means that
your set
operation will have to find the place where it
needs to place an entry, when it discovers that the entry k
doesn’t yet exist. This also means that your code for get
can exit the traversal loop early. It won’t need to traverse
all the nodes to discover that a key has no entry.
You’ll want to modify the testing code’s main
so that it
tests the set
and get
operations. It should
start by building an empty order::dict*
, accept a series of
those commands to modify that ordered dictionary, and it should output
the order::toString
of the dictionary that results from
each change. Include help
and quit
commands,
too.
Note that you can get rid of the main
code that
processes argc
and argv
, instead having it
declared as type int main(void)
. You’ll want to keep the
parseCommand
code to figure out the user’s inputs for the
commands get <key>
and
set <key> <value>
.
Here is a transcription of the test_ordered
code
working:
$ ./test_ordered
Your dictionary is {}.
Enter a dictionary command: get bob
That key has value 0.
Your dictionary is {}.
Enter a dictionary command: set bob 42
Your dictionary is {'bob': 42}.
Enter a dictionary command: get bob
That key has value 42.
Your dictionary is {'bob': 42}.
Enter a dictionary command: set alice 101
Your dictionary is {'alice': 101, 'bob': 42}.
Enter a dictionary command: set carlos -6
Your dictionary is {'alice': 101, 'bob': 42, 'carlos': -6}.
Enter a dictionary command: set bob 1234
Your dictionary is {'alice': 101, 'bob': 1234, 'carlos': -6}.
Enter a dictionary command: get carlos
That key has value -6.
Your dictionary is {'alice': 101, 'bob': 1234, 'carlos': -6}.
Enter a dictionary command: quit
Bye!
$
Note that main
creates a dictionary whose default value
is 0.
You do not need to write the code for a destroy
function. This function, like llist::destroy
, would release
all its nodes’ storage back to the heap. You are welcome to write this
as a BONUS exercise.
RECOMMENDATION: I recommend getting this code
working first without it putting the keys in sorted order. Once you’ve
got it working in that way, change set
so that it inserts
the keys in the right order. There are several cases you’ll need to
consider in that code to get it to work correctly. It’s tricky.
Part 2: write queue
A queue is a container data structure that holds a set of
items, and keeps track of the order that they were added to the queue.
The item at the front, called the head of the queue, was the
item that was first added. The item at the end is the item that was last
added. The items in between are ordered from the earliest added after
the head, to the latest added, working from front to back. The operation
for adding an item to the back of the queue is normally called
enqueue.
An item can be removed from the queue using the operation called
dequeue. With queues, however, you can’t just remove any of the
items it stores. Rather, instead, the next item removed with a dequeue
is the one at the front of the queue, the head item. It gets removed
from the front, and so the next item in line becomes the new head,
unless of course that dequeued item was the only item in the queue.
We’ll see how queues are used in search and graph algorithms later,
but for now we simply want to practice writing more linked list code.
This exercise asks you to complete the code in queue.cc
for
a linked list data structure queue::queue
defined in
queue.hh
and tested in test_queue.cc
. Once all
your code is working, this is the kind of interaction I’d expect:
$ ./test_queue
Your queue contains []
Enter a command: head
Your queue is empty and has no head.
Your queue contains []
Enter a command: enqueue 1
Your queue contains [(1)]
Enter a command: enqueue 2
Your queue contains [(1) 2]
Enter a command: enqueue 3
Your queue contains [(1) 2 3]
Enter a command: head
1 is at the head of your queue.
Enter a command: dequeue
1 was removed from the head of your queue.
Your queue contains [(2) 3]
Bye!
$
The code currently compiles and runs, but several of the queue
functions aren’t properly written. Finish the code in
queue.cc
. These two are already written:
• build
: constructs, initializes, and returns an empty
queue.
• isEmpty
: returns whether or not the queue is empty.
The others, listed just below, need to be written by you.
To implement a queue as a linked list, we use two node pointers
first
and last
, so each is of type
node*
. An empty queue has both of these attributes set to
nullptr
. A queue with only one item has first
and last
pointing to the same node
struct. A
queue with more items has first
pointing to the head item’s
node, that node points to the next item in line, and so on, and the
queue has last
pointing to the node whose next
attribute is a nullptr
.
Here are the other operations you need to complete:
• enqueue
: taked an integer and places it, in a
new
linked list node, at the end of the queue. When the
queue isn’t empty, you can use last
to append to the
end.
• dequeue
: this should remove and return the value
stored at the front node of the queue. NOTE: you should
also deallocate its linked list node removed by calling
delete
on its pointer. ALSO NOTE: you
should make sure that both first
and last
are
made null should this removed head be the only item that was in the
queue.
• head
: this should return the value stored at the front
node of the queue.
• toString
: this should create and return a string that
represents the contents of the queue. Mimic the sample output above.
• destroy
: this should dequeue each item in the queue.
Because you delete
within dequeue
, this gives
back each node’s storage to the heap. It should then delete
the pointer of the queue
itself.