An implementation of a tiny circular doubly-linked list in C.
Current version: 1.0.1
This implementation builds as a dynamic and a static library by default. In order to trigger the build, simply call the Makefile at the root of the project using make
.
This project is tested using the Google Test
framework, the tests are located in the tests/
directory and have their own Makefile which produce an executable. To launch the tests simply call the produced launch_tests
executable.
An instance of a list is of type list_t
.
A list holds its current size which is represented as the sum of its nodes that are of type node_t
. It also contains a pointer to the head of the list, and a pointer to the tail of the list.
In order to create a new instance of a list on the heap, you must call list_create
.
list_t* list = list_create();
If you feel you do not need a pointer to be returned and you'd like the list to be created statically, you can call list_create_static
:
list_t list = list_create_static();
Do not forget to clear the list when you are finished using a static instance, typically using
list_clear
in order for all the nodes to be properly deleted.
Elements you can push into an instance of a list_t
are of type void*
.
There are two ways to insert an element into an instance of a list :
// Pushing elements to head.
list_push_front(list, "foo");
list_push_front(list, "bar");
// Pushing elements to tail.
list_push_back(list, "baz");
list_push_back(list, "qux");
Both functions create an instance of a node_t
which wraps the given element to insert in the list. Both functions return a pointer to the created node.
The complexity of a node insertion is constant
O(1)
.
You can pop an element located at the head or at the tail of the list. Popping an element will remove the inserted node
and return a pointer to the value that was associated with that node.
// Will return the constant string `qux`.
const char* qux = list_pop_back(list);
// Will return the constant string `bar`.
const char* bar = list_pop_front(list).
The complexity of a node pop is constant
O(1)
.
It is possible to iterate over each nodes associated with a list by having a function called back on each node :
int iterator(size_t index, node_t* node, void* data) {
printf("Node (%zu) element : %s\n", index,
(const char*) node->element);
return (0);
}
list_iterate_over_nodes(list, iterator, NULL);
This method will allow you to iterate from the first element (the head of the list) to the last element (the tail of the list). If a negative value is returned from the iterator, the iteration will stop.
Another way of iterating over the list is by using the iterator APIs. The main difference with using a callback is that you are in full control of the iteration, you can stop it, but also go forward or backward.
To do so, first create a list_iterator_t
and use it to traverse the list.
list_iterator_t it = list_make_iterator(list, NULL);
for (size_t i = 0; i < list_get_size(list)
&& list_iterator_has_next(&it); ++i) {
node_t* node = list_iterator_next(&it);
// Do something with `node`.
}
ℹ️ The second parameter of
list_make_iterator
is optional, it specifies a node pointer you'd like the iterator to start at. If NULL is specified, the iterator will start at the head of the list.
The iterator created by list_make_iterator
will indefinitely iterate over the list (unless it is empty), since the list is circular. This is why we used a counter in the above example to stop the iteration at the end of the list.
Note that you are encouraged to write your own iterator functions that will implement the appropriate logic of how to iterate over an instance of a list given your application requirements.
The complexity of an iteration over each node in the list is linear
O(n)
.
It is possible to search through the list for a particular node. You can use the list_find_node
function to do so.
node_t* node = list_push_front(list, "foo");
if (list_find_node(list, node)) {
// `node` was found
}
To customize the way to find an element in the list you can provide a predicate function to the list_find_node_if
function :
int predicate(size_t index, node_t* node, void* data) {
return (!strcmp(node->element, data));
}
if (list_find_node_if(list, predicate, "foo")) {
// `foo` was found
}
The complexity of both basic and customized lookups is linear
O(n)
.
To remove a given node from the list, you can pass a pointer to the node you'd like to be removed to the list_remove_node
function :
list_t* list = list_create();
node_t* node = list_push_front(list, "foo");
// Remove the node from the list.
list_remove_node(list, node);
It is possible to remove a node using a more functional way, by using a predicate passed to list_remove_node_if
.
int predicate(size_t index, node_t* node, void* data) {
return (!strcmp(node->element, "bar")
|| !strcmp(node->element, "foo"));
}
// Remove from the list each node holding
// the constant strings 'foo' or 'bar'.
list_remove_node_if(list, predicate, NULL);
It is also possible to pass a third parameter to list_remove_node_if
, for it to be passed to your predicate function.
int predicate(size_t index, node_t* node, void* data) {
return (!strcmp(node->element, data));
}
// Remove from the list each node holding
// the constant string 'foo'.
list_remove_node_if(list, predicate, "foo");
The complexity of a node removal is linear
O(n)
.
Retrieving the size of the list is a constant time operation O(1)
, since the size is maintained across each insertions and removals. You can retrieve it as follow :
size_t size = list_get_size(list);
Similarly, it is also possible to call list_is_empty
if you'd like to know whether the given list is empty :
int empty = list_is_empty(list)
To remove all the nodes contained in the list, you can call list_clear
.
list_clear(list);
Similarly to creating a new instance of a list dynamically, to delete a dynamic instance of a list, you must call list_destroy
. This will cause every node left in the list to be deleted, and the list itself to be destroyed.
list_destroy(list);