The goal of this homework assignment is to allow you to explore building a
queue using a doubly linked list and then using this data structure to
implement two Unix utilities: tail and tac. The first two activities
involve implementing a queue which internally uses a doubly linked list
with a sentinel node. Once you have a working data structure library, you
will then use it to implement both the tail and tac utilities, which shows
the last few N
lines or reverses the contents of an input stream
(respectively). After these coding activities, there is a quiz where you will
have an opportunity to reflect on the code you have written for this homework
assignment.
For this assignment, you are to do your work in the homework04
folder of
your assignments GitHub repository and push your work by noon
Wednesday, September 20.
Before starting this homework assignment, you should first perform a git
pull
to retrieve any changes in your remote GitHub repository:
$ cd path/to/repository # Go to assignments repository
$ git switch master # Make sure we are in master branch
$ git pull --rebase # Get any remote changes not present locally
Next, create a new branch for this assignment:
$ git checkout -b homework04 # Create homework04 branch and check it out
To help you get started, the instructor has provided you with the following skeleton code:
# Go to assignments repository
$ cd path/to/assignments/repository
# -----------------------------------------------------
# MAKE SURE YOU ARE NOT INSIDE THE homework04 DIRECTORY
# -----------------------------------------------------
# MAKE SURE YOU ARE AT THE TOP-LEVEL DIRECTORY
# -----------------------------------------------------
# Download skeleton code tarball
$ curl -LO https://www3.nd.edu/~pbui/teaching/cse.20312.fa23/static/tar/homework04.tar.gz
# Extract skeleton code tarball
$ tar xzvf homework04.tar.gz
Once downloaded and extracted, you should see the following files in your
homework04
directory:
homework04
\_ Makefile # This is the Makefile for building all the project artifacts
\_ bin # This contains the project binary executables and test scripts
\_ test_node.sh # This is the shell script for testing the Node structure
\_ test_queue.sh # This is the shell script for testing the Queue utility
\_ test_tac.sh # This is the shell script for testing the tac program
\_ test_tail.sh # This is the shell script for testing the tail program
\_ include # This contains the project header files
\_ ds # This contains the project data structure header files
\_ node.h # This is the C header file for the Node structure
\_ queue.h # This is the C header file for the Queue structure
\_ lib # This contains the project library files
\_ src # This contains the project source code
\_ node.c # This is the C source code for the Node structure
\_ queue.c # This is the C source code for the Queue structure
\_ tac.c # This is the C source code for the tac program
\_ tail.c # This is the C source code for the tail program
\_ tests # This contains the project unit tests
\_ unit_node.c # This is the unit test for the Node structure
\_ unit_queue.c # This is the unit test for the Queue structure
Now that the files are extracted into the homework04
folder, you can
commit them to your git repository:
# Go into homework04 folder
$ cd homework04
# Add and commit initial skeleton files
$ git add Makefile # Mark changes for commit
$ git add bin/*.sh # Mark changes for commit
$ git add include/ds/*.h # Mark changes for commit
$ git add lib/.gitkeep # Mark changes for commit
$ git add src/*.c # Mark changes for commit
$ git add tests/*.c # Mark changes for commit
$ git commit -m "Homework 04: Initial Import" # Record changes
The details on what you need to implement are described in the following sections.
Makefile
¶The Makefile contains all the rules or recipes for building the project
artifacts (e.g. tail
, tac
, libds.a
, unit_node
, unit_queue
, etc.):
CC= gcc
CFLAGS= -Wall -std=gnu99 -g -Iinclude
AR= ar
ARFLAGS= rcs
LD= gcc
LDFLAGS= -Llib
all: bin/tail bin/tac
test:
@$(MAKE) -sk test-all
test-all: test-node test-queue test-tail test-tac
# TODO: Pattern rule for object files
# TODO: Rule for lib/libds.a
# TODO: Rule for bin/tail
# TODO: Rule for bin/tac
bin/unit_node: tests/unit_node.o lib/libds.a
$(LD) $(LDFLAGS) -o $@ $^
bin/unit_queue: tests/unit_queue.o lib/libds.a
$(LD) $(LDFLAGS) -o $@ $^
test-node: bin/unit_node
bin/test_node.sh
test-queue: bin/unit_queue
bin/test_queue.sh
test-tail: bin/tail
bin/test_tail.sh
test-tac: bin/tac
bin/test_tac.sh
clean:
rm -f bin/unit_* bin/tail bin/tac lib/*.a src/*.o tests/*.o
For this task, you will need to add rules for building the static library
lib/libds.a
and the programs bin/tail
and bin/tac
. Be sure to have a
recipe for any intermediate object files that libraries require as
shown in the DAG below:
You must use the CC
, CFLAGS
, LD
, LDFLAGS
, AR
, and ARFLAGS
variables when appropriate in your rules. You should also consider using
automatic variables such as $@
and $<
as well.
Once you have a working Makefile
, you should be able to run the following
commands:
# Build all TARGETS
$ make
gcc -Wall -std=gnu99 -g -Iinclude -c -o src/tail.o src/tail.c
gcc -Wall -std=gnu99 -g -Iinclude -c -o src/node.o src/node.c
gcc -Wall -std=gnu99 -g -Iinclude -c -o src/queue.o src/queue.c
ar rcs lib/libds.a src/node.o src/queue.o
gcc -Llib -o bin/tail src/tail.o lib/libds.a
gcc -Wall -std=gnu99 -g -Iinclude -c -o src/tac.o src/tac.c
gcc -Llib -o bin/tac src/tac.o lib/libds.a
# Run all tests
$ make test
Testing node...
...
# Remove generated artifacts
$ make clean
rm -f bin/unit_* bin/tail bin/tac lib/*.a src/*.o tests/*.o
Note: The tests will fail if you haven't implemented Node
or Queue
structures, or the tail
or tac
programs.
You must include the -Wall
flag in your CFLAGS
when you compile. This
also means that your code must compile without any warnings, otherwise
points will be deducted.
After completing the Makefile
and verifying that you can produce all the
specified artifacts, you may begin implementing the data structures and
applications for this assignment.
For the first activity, you are to implement a Node
structure to be
used in a doubly linked list. Each Node
structure will have the
following attributes:
next
: This is a pointer to the next Node
struct in the sequence.
prev
: This is a pointer to the previous Node
struct in the sequence.
include/ds/node.h
¶The include/ds/node.h
file is the header file for the Node
structure, which contains the following structs and function
prototypes:
/* node.h: Node Structure */
#pragma once
/* Structures */
typedef struct Node Node;
struct Node {
char *data; // String value
Node *next; // Pointer to next Node
Node *prev; // Pointer to previous Node
};
/* Functions */
Node * node_create(const char *data, Node *next, Node *prev);
void node_delete(Node *n);
Other programs will #include
this file in order to use the functions we
will be implementing in this library.
Note: For this task, you do not need to modify this file. Instead, you should review it and ensure you understand the provided code.
src/node.c
¶The src/node.c
file contains the C implementation for the Node
structure. For this task, you will need to implement the following
functions:
Node * node_create(const char *data, Node *next, Node *prev)
This function allocates a new
Node
struct and initializes its attributes:data
,next
,prev
as described above.
void node_delete(Node *n)
This function deallocates the given
Node
struct along with its internaldata
string.
As you implement the functions in src/node.c
, you should use
bin/test_node.sh
to test each function:
# Build unit-test
$ make bin/unit_node
gcc -Wall -std=gnu99 -g -Iinclude -c -o tests/unit_node.o tests/unit_node.c
gcc -Wall -std=gnu99 -g -Iinclude -c -o src/node.o src/node.c
gcc -Wall -std=gnu99 -g -Iinclude -c -o src/queue.o src/queue.c
ar rcs lib/libds.a src/node.o src/queue.o
gcc -Llib -o bin/unit_node tests/unit_node.o lib/libds.a
# Run test script manually
$ ./bin/test_node.sh
Testing node...
node_create ... Success
node_delete ... Success
Score 1.00 / 1.00
Status Success
Alternatively, you can both build the artifacts and run the test script by doing the following:
# Build and run test scripts
$ make test-node
...
If one of the functions fails, and you need to debug the unit tests, you
can run the bin/unit_node
command directly:
# Display usage message
$ ./bin/unit_node
Usage: ./bin/unit_node NUMBER
Where NUMBER is right of the following:
0 Test node_create
1 Test node_delete
# Run test for node_create
$ ./bin/unit_node 0
# Run test for node_create using GDB
$ gdb ./bin/unit_node
...
(gdb) run 0
# Run test for node_create using Valgrind
$ valgrind --leak-check=full ./bin/unit_node 0
Of course, you are free to create your own test programs to debug and test
your Node
structure.
You should practice iterative development. That is, rather than writing a bunch of code and then debugging it all at once, you should concentrate on one function at a time and then test that one thing at a time. The provided unit tests allow you to check on the correctness of the individual functions without implementing everything at once. Take advantage of this and build one thing at a time.
In high-level languages such as Python, we have built-in implementations of deques which support a variety of methods:
# Load library
>>> import collections
# Define a deque (unbounded)
>>> queue = collections.deque([4, 6, 6, 3, 7])
# Add a new value to the back of deque
>>> queue.append(9)
>>> queue
deque([4, 6, 6, 3, 7, 9])
# Remove value at the front of deque
>>> queue.popleft()
4
>>> queue
deque([6, 6, 3, 7, 9])
# Define a deque (bounded, no more than 5)
>>> queue = collections.deque([4, 6, 6, 3, 7], maxlen=5)
# Add a new value to the back of deque
>>> queue.append(9)
>>> queue
deque([6, 6, 3, 7, 9], maxlen=5)
As can be seen, deques in Python can behave like the queues discussed
in class with efficient push
operations to the back of the structure
via append
and efficient pop
operations to the front of the structure
via popleft
.
Moreover, deques in Python can also be bounded, that is they will maintain a maximum capacity once it is reached. If the user adds another value to a structure that is at its capacity, then the current value at the front of the structure is removed to make room for the new value to be added at the back.
For the second activity, you are to use the Node
structure from the
previous activity to construct a Queue
structure that implements a
doubly linked list with a sentinel node to mimic the functionality of
deques in Python.
The Queue
struct you are to implement has the following attributes:
sentinel
: This is a sentinel Node
struct embedded inside the
Queue
struct. It is used to simplify our internal Queue
methods by
ensuring that there will always be an allocated Node
in the doubly
linked list.
capacity
: This is the maximum capacity of the Queue
structure.
If this value is set to 0
, then the Queue
is considered to be
unbounded and may have any number of values. If this value is not 0
,
however, then you must ensure the Queue
never has more than this number
of values.
size
: This is the number of values in the Queue
structure. It
should not exceed the capacity
if it the capacity
is not 0
.
Note: The sentinel node is not considered part of the actual queue. It does not store any data
nor does it contribute to the size
of the Queue
.
Instead, it is there so that its next
pointer is the head of the
doubly linked list and its prev
pointer is the tail of the doubly
linked list.
With this Queue
struct in mind, you are to complete the queue library,
lib/ds.a.
, which contains functions such as queue_push
and queue_pop
that implement some of the functionality present in Python but missing in
C.
include/ds/queue.h
¶The include/ds/queue.h
file is the header file for the queue library,
which contains the following structs and function prototypes:
/* queue.h: Queue Structure */
#pragma once
#include "ds/node.h"
#include <stdbool.h>
#include <stdio.h>
/* Structures */
typedef struct {
Node sentinel; // Sentinel Node
size_t capacity; // Capacity or maximum size of Queue
size_t size; // Current size of Queue (how many Nodes)
} Queue;
/* Functions */
Queue * queue_create(size_t capacity);
void queue_delete(Queue *q);
void queue_push(Queue *q, const char *s);
char * queue_pop(Queue *q);
bool queue_empty(Queue *q);
void queue_dump(Queue *q, FILE *s);
void queue_reverse(Queue *q);
Note: For this task, you do not need to modify this file. Instead, you should review it and ensure you understand the provided code.
src/queue.c
¶The src/queue.c
file contains the C implementation for the queue
library.
For this task, you will need to implement the following functions:
Queue * queue_create(size_t capacity)
This function allocates a new
Queue
struct and initializes its attributes:sentinel
,capacity
,size
as described above.
void queue_delete(Queue *q)
This function deallocates the given
Queue
struct along with its internalNode
structs.
Hint: Use node_delete
to delete the internal Node
struct`s.
To loop through the internal Node
structures, you will want to
start with the first Node
the sentinel
points to and then stop once
you return to the sentinel
.
void queue_push(Queue *q, const char *s)
This function creates a new
Node
struct with the given strings
and adds it to the back of the internal doubly linked list (e.g.q.append(s)
in Python).
Hint: Use node_create
and take advantage of the sentinel
in the
Queue
struct. Likewise, be sure to not exceed the capacity
of
the Queue
if it is not 0
.
Note: The queue_capacity
test verifies whether or not you ensure
that queue_push
does not exceed the capacity
of the Queue
if it
is not 0
.
It is tested separately because you can use queue_pop
to implement
this functionality.
char * queue_pop(Queue *q)
This function removes the
Node
struct at the front of the internal doubly linked list and returns the associated stringdata
from theNode
struct (e.g.q.popleft()
in Python).
bool queue_empty(Queue *q)
This function returns
true
if theQueue
struct has no values, otherwisefalse
.
void queue_dump(Queue *q, FILE *s)
This function prints every value in the
Queue
struct to the given stream (one value per line).
void queue_reverse(Queue *q)
This function reverses the internal doubly linked list in the
Queue
struct.
Hint: Draw a picture and determine which next
and prev
pointers
needed to be updated and when.
As you implement the functions in src/queue.c
, you should use
bin/unit_queue.sh
to test each function:
# Build unit-test
$ make bin/unit_queue
gcc -Wall -std=gnu99 -g -Iinclude -c -o tests/unit_queue.o tests/unit_queue.c
gcc -Wall -std=gnu99 -g -Iinclude -c -o src/node.o src/node.c
gcc -Wall -std=gnu99 -g -Iinclude -c -o src/queue.o src/queue.c
ar rcs lib/libds.a src/node.o src/queue.o
gcc -Llib -o bin/unit_queue tests/unit_queue.o lib/libds.a
# Run test script manually
$ ./bin/test_queue.sh
Testing queue...
queue_create ... Success
queue_delete ... Success
queue_push ... Success
queue_pop ... Success
queue_capacity ... Success
queue_empty ... Success
queue_dump ... Success
queue_reverse ... Success
Score 6.00 / 6.00
Status Success
Alternatively, you can both build the artifacts and run the test script by doing the following:
# Build and run test scripts
$ make test
...
If one of the functions fails, and you need to debug the unit tests, you
can run the bin/unit_queue
command directly:
# Display usage message
$ ./bin/unit_queue
Usage: ./bin/unit_queue NUMBER
Where NUMBER is right of the following:
# Run test for queue_create
$ ./bin/unit_queue 0
# Run test for queue_create using GDB
$ gdb ./bin/unit_queue
...
(gdb) run 0
# Run test for queue_create using Valgrind
$ valgrind --leak-check=full ./bin/unit_queue 0
Of course, you are free to create your own test programs to debug and test your queue library.
Once you have a working queue library, you are to complete the src/tail.c
program, which is a simplified version of the tail utility:
# Show last 10 items by default
$ seq 100 | bin/tail
91
92
93
94
95
96
97
98
99
100
# Show last 5 items
$ seq 100 | bin/tail -n 5
96
97
98
99
100
As can be seen, the bin/tail
program reads [standard input] line by line
and only emits the last 10
lines by default.
If the user specifies another limit via the -n
flag, then only that number
of lines is shown.
src/tail.c
¶The src/tail.c
file contains the C implementation of the bin/tail
program described above. You will need to implement the following functions:
int main(int argc, char *argv[])
This function parses command line options and then reads one line at a time from
stdin
, and emits only the lastN
lines (whereN
is10
by default).
Hint: To parse command line options, use the slice
program from
Homework 02 as a reference.
In addition to meeting the functional requirements of the assignment (as described above), your program must not exhibit any memory leaks or invalid memory accesses as would be detected by Valgrind.
Be sure to free any memory that has been allocated on the heap and to initialize any allocate memory appropriately.
As you implement src/tail.c
, you can test it by running the test-tail
target:
# Build artifact and run test
$ make test-tail
Testing tail ...
Makefile (output) ... Success
Makefile (valgrind) ... Success
Makefile -n 5 (output) ... Success
Makefile -n 5 (valgrind) ... Success
Makefile -n 20 (output) ... Success
Makefile -n 20 (valgrind) ... Success
seq 100 (output) ... Success
seq 100 (valgrind) ... Success
seq 100 -n 5 (output) ... Success
seq 100 -n 5 (valgrind) ... Success
seq 100 -n 20 (output) ... Success
seq 100 -n 20 (valgrind) ... Success
Score 2.00 / 2.00
Status Success
Once you have a working queue library, you are to complete the src/tac.c
program, which is a simplified version of the tac utility:
# Show lines in reverse order
$ seq 5 10 | bin/tac
10
9
8
7
6
5
As can be seen, the bin/tac
program reads [standard input] line by line and
emits the lines in reversed order.
src/tac.c
¶The src/tac.c
file contains the C implementation of the bin/tac
program described above. You will need to implement the following functions:
int main(int argc, char *argv[])
This function reads one line at a time from
stdin
, and emits all the lines in reversed order.
As you implement src/tac.c
, you can test it by running the test-tac
target:
# Build artifact and run test
$ make test-tac
Testing tac ...
Makefile (output) ... Success
Makefile (valgrind) ... Success
seq 100 (output) ... Success
seq 100 (valgrind) ... Success
Score 1.00 / 1.00
Status Success
Once you have completed all the activities above, you are to complete the following reflection quiz:
As with Reading 01, you will need to store your answers in a
homework04/answers.json
file. You can use the form above to generate the
contents of this file, or you can write the JSON by hand.
To check your quiz directly, you can use the check.py
script:
$ ../.scripts/check.py
Checking homework04 quiz ...
Q1 0.50
Q2 0.50
Q3 0.60
Q4 0.40
Score 2.00 / 2.00
Status Success
For extra credit, you are to solve the following LeetCode problem in C.
To receive credit, you must pass on LeetCode and achieve an Accepted submission.
To get credit for this Leet Point, show your solution and the LeetCode acceptance page to a TA to verify (or attached a screenshot with both to your Pull Request). You have up until two days after this assignment is due to verify your Leet Point.
Remember that you can always forgo this Leet Point for two extra days to do the homework. That is, if you need an extension, you can simply skip the Leet Point and you will automatically have until Friday to complete the assignment for full credit.
Just leave a note on your Pull Request of your intentions.
To submit your assignment, please commit your work to the homework04
folder
of your homework04
branch in your assignments GitHub repository:
#-----------------------------------------------------------------------
# Make sure you have already completed Activity 0: Preparation
#-----------------------------------------------------------------------
...
$ git add Makefile # Mark changes for commit
$ git commit -m "Homework 04: Activity 0" # Record changes
...
$ git add src/node.c # Mark changes for commit
$ git commit -m "Homework 04: Activity 1" # Record changes
...
$ git add src/queue.c # Mark changes for commit
$ git commit -m "Homework 04: Activity 2" # Record changes
...
$ git add src/tail.c # Mark changes for commit
$ git commit -m "Homework 04: Activity 3" # Record changes
...
$ git add src/tac.c # Mark changes for commit
$ git commit -m "Homework 04: Activity 4" # Record changes
...
$ git add answers.json # Mark changes for commit
$ git commit -m "Homework 04: Activity 5" # Record changes
...
$ git push -u origin homework04 # Push branch to GitHub
Remember to create a Pull Request and assign the appropriate TA from the Reading 04 TA List.
DO NOT MERGE your own Pull Request. The TAs use open Pull Requests to keep track of which assignments to grade. Closing them yourself will cause a delay in grading and confuse the TAs.