Introduction
This page describes the Generic Linked List library available for Applications.
The lib_lists provides two types of linked list implementations:
- Forward lists (
flist_*): Singly-linked lists with minimal memory overhead
- Doubly-linked lists (
list_*): Bidirectional lists with previous pointers
Both implementations share a common architecture with generic node structures that can be embedded in any data structure. This design is based on the app-ethereum implementation and is optimized for embedded systems like Ledger devices.
Node Structures
Forward List Node (Singly-Linked)
The forward list node contains only a next pointer:
Forward list node structure (singly-linked)
struct flist_node_t * next
Usage:** Embed flist_node_t as the first member of your structure:
typedef struct my_data_s {
int value;
char name[32];
} my_data_t;
Memory overhead:** 4-8 bytes per node (one pointer)
Doubly-Linked List Node
The doubly-linked node contains both next and previous pointers:
Doubly-linked list node structure.
struct list_node_t * prev
Usage:** Embed list_node_t as the first member of your structure:
typedef struct my_data_s {
int value;
char name[32];
} my_data_t;
Memory overhead:** 8-16 bytes per node (two pointers)
Choosing Between Forward Lists and Doubly-Linked Lists
| Feature | Forward List (flist) | Doubly-Linked (list) |
| Memory per node | 4-8 bytes | 8-16 bytes |
| Insert/remove at front | O(1) | O(1) |
| Insert/remove at back | O(n) | O(1) ⚡ |
| Insert after node | O(1) | O(1) |
| Insert before node | Not available | O(1) ⚡ |
| Backward traversal | No | Yes ⚡ |
| Reverse operation | O(n) | O(n) |
Best practice:** Use forward lists when memory is critical and you don't need backward traversal or frequent tail operations. Use doubly-linked lists when you need bidirectional access or O(1) tail operations.
Understanding Time Complexity
The documentation uses Big O notation to describe algorithm performance:
Basic Operations
All operations are available for both forward lists and doubly-linked lists with similar APIs:
- Forward list functions:
flist_*
- Doubly-linked list functions:
list_*
All mutating functions return bool to indicate success or failure.
Insertion Operations
Push Front** - Add a node at the beginning (O(1) for both types):
my_data_t *data = malloc(sizeof(my_data_t));
data->node.next = NULL;
data->value = 42;
}
my_data_t *data2 = malloc(sizeof(my_data_t));
data2->node._list.next = NULL;
data2->node.prev = NULL;
data2->value = 43;
}
bool list_push_front(list_node_t **list, list_node_t *node)
Add a node at the beginning of the doubly-linked list.
bool flist_push_front(flist_node_t **list, flist_node_t *node)
Add a node at the beginning of the forward list.
Push Back** - Add a node at the end:
- Forward list: O(n) - must traverse entire list
- Doubly-linked: O(1) - direct access via prev pointer ⚡
}
}
bool list_push_back(list_node_t **list, list_node_t *node)
Add a node at the end of the doubly-linked list.
bool flist_push_back(flist_node_t **list, flist_node_t *node)
Add a node at the end of the forward list.
Insert After** - Insert a node after a reference node (O(1) for both):
my_data_t *new_data = malloc(sizeof(my_data_t));
new_data->node._list.next = NULL;
new_data->node.prev = NULL;
}
bool list_insert_after(list_node_t **list, list_node_t *ref, list_node_t *node)
Insert a node after a reference node in the doubly-linked list.
Insert Before** - Insert a node before a reference node:
- Forward list: Not available (requires backward traversal)
- Doubly-linked: O(1) - direct access via prev pointer ⚡
my_data_t *new_data = malloc(sizeof(my_data_t));
new_data->node._list.next = NULL;
new_data->node.prev = NULL;
}
bool list_insert_before(list_node_t **list, list_node_t *ref, list_node_t *node)
Insert a node before a reference node in the doubly-linked list.
Removal Operations
All removal functions return bool to indicate success or failure (except remove_if). They accept an optional deletion callback to clean up node data.
Pop Front** - Remove the first node (O(1) for both):
my_data_t *data = (my_data_t *)node;
free(data);
}
}
}
bool flist_pop_front(flist_node_t **list, f_list_node_del del_func)
Remove and delete the first node from the forward list.
bool list_pop_front(list_node_t **list, f_list_node_del del_func)
Remove and delete the first node from the doubly-linked list.
void(* f_list_node_del)(flist_node_t *node)
Callback function to delete/free a node.
Pop Back** - Remove the last node:
- Forward list: O(n) - must find second-to-last node
- Doubly-linked: O(1) - direct access via prev pointer ⚡
}
}
bool list_pop_back(list_node_t **list, f_list_node_del del_func)
Remove and delete the last node from the doubly-linked list.
bool flist_pop_back(flist_node_t **list, f_list_node_del del_func)
Remove and delete the last node from the forward list.
Remove** - Remove a specific node (O(n) for forward, O(1) for doubly-linked):
if (
flist_remove(&my_flist, &node_to_remove, my_delete_func)) {
}
}
bool list_remove(list_node_t **list, list_node_t *node, f_list_node_del del_func)
Remove and delete a specific node from the doubly-linked list.
bool flist_remove(flist_node_t **list, flist_node_t *node, f_list_node_del del_func)
Remove and delete a specific node from the forward list.
Remove If** - Remove all nodes matching a predicate (O(n) for both):
const my_data_t *data = (const my_data_t *)node;
return data->value < 0;
}
printf("Removed %zu negative values\n", removed);
size_t flist_remove_if(flist_node_t **list, f_list_node_pred pred_func, f_list_node_del del_func)
Remove all nodes matching a predicate from the forward list.
Clear** - Remove all nodes (O(n) for both):
}
bool flist_clear(flist_node_t **list, f_list_node_del del_func)
Remove and delete all nodes from the forward list.
Utility Operations
Get Size** - Count the number of nodes (O(n) for both):
size_t flist_size(flist_node_t *const *list)
Get the number of nodes in the forward list.
size_t list_size(list_node_t *const *list)
Get the number of nodes in the doubly-linked list.
Check Empty** - Test if list is empty (O(1) for both):
printf("Forward list is empty\n");
}
printf("Doubly-linked list is empty\n");
}
bool list_empty(list_node_t *const *list)
Check if the doubly-linked list is empty.
bool flist_empty(flist_node_t *const *list)
Check if the forward list is empty.
Sort** - Sort the list using a comparison function (O(n²) for both):
my_data_t *data_a = (my_data_t *)a;
my_data_t *data_b = (my_data_t *)b;
return data_a->value <= data_b->value;
}
}
}
bool flist_sort(flist_node_t **list, f_list_node_cmp cmp_func)
Sort the forward list using a comparison function.
bool list_sort(list_node_t **list, f_list_node_cmp cmp_func)
Sort the doubly-linked list using a comparison function.
Unique** - Remove consecutive duplicate nodes (O(n) for both):
const my_data_t *data_a = (const my_data_t *)a;
const my_data_t *data_b = (const my_data_t *)b;
return data_a->value == data_b->value;
}
size_t removed =
flist_unique(&my_flist, are_equal, my_delete_func);
printf("Removed %zu duplicates\n", removed);
size_t flist_unique(flist_node_t **list, f_list_node_bin_pred pred_func, f_list_node_del del_func)
Remove consecutive duplicate nodes from the forward list.
Reverse** - Reverse the order of nodes (O(n) for both):
}
}
bool flist_reverse(flist_node_t **list)
Reverse the order of nodes in the forward list.
bool list_reverse(list_node_t **list)
Reverse the order of nodes in the doubly-linked list.
List Traversal
Forward List Traversal (Forward Only)
my_data_t *data = (my_data_t *)node;
printf("Value: %d\n", data->value);
}
List Traversal
my_data_t *data = (my_data_t *)node;
printf("Value: %d\n", data->value);
}
if (tail != NULL) {
}
my_data_t *data = (my_data_t *)node;
printf("Value: %d\n", data->value);
}
}
Safety and Error Handling
The library implements several safety checks:
- NULL pointer checks: All functions validate their parameters
- Node state validation:
- Forward list: Insertion functions verify that
node->next == NULL
- Doubly-linked: Insertion functions verify that both
node->_list.next and node->prev are NULL
- This prevents:
- Creating cycles in the list
- Accidentally linking nodes from different lists
- Losing track of existing node chains
- Return value checking: All mutating operations return
bool:
true: Operation succeeded
false: Invalid parameters or operation failed (e.g., node not found)
Counter operations: remove_if, unique, and size return size_t with count information
Common error scenarios:**
my_data_t *data = malloc(sizeof(my_data_t));
data->node.next = some_other_node;
}
my_data_t *data2 = malloc(sizeof(my_data_t));
data2->node.prev = some_node;
}
Best Practices
- Always initialize node pointers to NULL before insertion:
my_data_t *data = malloc(sizeof(my_data_t));
data->node.next = NULL;
my_data_t *data2 = malloc(sizeof(my_data_t));
data2->node._list.next = NULL;
data2->node.prev = NULL;
- Check return values to detect errors:
- Use the deletion callback to prevent memory leaks:
- Choose the right list type:
- Use
flist (forward list) for minimal memory usage
- Use
list (doubly-linked) for bidirectional access or frequent tail operations
- Prefer front operations when order doesn't matter:
flist_push_front is O(1)
list_push_front is O(1)
- Both are faster than push_back for forward lists
- Don't reuse nodes across multiple lists without proper unlinking
Complete Examples
Forward List Basic Usage
#include <stdlib.h>
#include <stdio.h>
typedef struct person_s {
char name[32];
int age;
} person_t;
person_t *person = (person_t *)node;
printf("Deleting: %s\n", person->name);
free(person);
}
int main(void) {
person_t *alice = malloc(sizeof(person_t));
alice->node.next = NULL;
strcpy(alice->name, "Alice");
alice->age = 30;
person_t *bob = malloc(sizeof(person_t));
bob->node.next = NULL;
strcpy(bob->name, "Bob");
bob->age = 25;
person_t *p = (person_t *)node;
printf("%s is %d years old\n", p->name, p->age);
}
return 0;
}
Generic linked list implementation (singly and doubly-linked)
Doubly-Linked List with Backward Traversal
#include <stdlib.h>
#include <stdio.h>
typedef struct person_s {
char name[32];
int age;
} person_t;
person_t *person = (person_t *)node;
printf("Deleting: %s\n", person->name);
free(person);
}
int main(void) {
person_t *alice = malloc(sizeof(person_t));
alice->node._list.next = NULL;
alice->node.prev = NULL;
strcpy(alice->name, "Alice");
alice->age = 30;
person_t *bob = malloc(sizeof(person_t));
bob->node._list.next = NULL;
bob->node.prev = NULL;
strcpy(bob->name, "Bob");
bob->age = 25;
printf("Forward:\n");
person_t *p = (person_t *)node;
printf(" %s is %d years old\n", p->name, p->age);
}
printf("Backward:\n");
}
person_t *p = (person_t *)node;
printf(" %s is %d years old\n", p->name, p->age);
}
return 0;
}
bool list_clear(list_node_t **list, f_list_node_del del_func)
Remove and delete all nodes from the doubly-linked list.
Sorting Example
const person_t *pa = (const person_t *)a;
const person_t *pb = (const person_t *)b;
return pa->age <= pb->age;
}
printf("Forward list sorted by age\n");
}
printf("Doubly-linked list sorted by age\n");
}
Advanced: Remove Duplicates and Reverse
const person_t *pa = (const person_t *)a;
const person_t *pb = (const person_t *)b;
return pa->age == pb->age;
}
size_t removed =
flist_unique(&people, are_equal, delete_person);
printf("Removed %zu duplicates\n", removed);
printf("List reversed (now descending order)\n");
}
const person_t *p = (const person_t *)node;
return p->age < 18;
}
printf("Removed %zu minors\n", removed_minors);
Performance Characteristics
Forward List Performance
| Operation | Time Complexity | Notes |
flist_push_front | O(1) | Constant time |
flist_pop_front | O(1) | Constant time |
flist_push_back | O(n) | Must traverse entire list |
flist_pop_back | O(n) | Must find second-to-last node |
flist_insert_after | O(1) | Constant time if reference known |
flist_remove | O(n) | Must find previous node |
flist_remove_if | O(n) | Traverses list once |
flist_clear | O(n) | Must visit all nodes |
flist_size | O(n) | Must count all nodes |
flist_empty | O(1) | Just checks if head is NULL |
flist_sort | O(n²) | Bubble sort implementation |
flist_unique | O(n) | Single pass after sorting |
flist_reverse | O(n) | Single pass, reverses pointers |
Memory overhead:** 4-8 bytes per node (one pointer)
Performance Characteristics
| Operation | Time Complexity | Notes |
list_push_front | O(1) ⚡ | Constant time |
list_pop_front | O(1) ⚡ | Constant time |
list_push_back | O(1) ⚡ | Direct access via prev! |
list_pop_back | O(1) ⚡ | Direct access via prev! |
list_insert_before | O(1) ⚡ | Direct access via prev! |
list_insert_after | O(1) ⚡ | Constant time |
list_remove | O(1) ⚡ | Direct access via prev! |
list_remove_if | O(n) | Traverses list once |
list_clear | O(n) | Must visit all nodes |
list_size | O(n) | Must count all nodes |
list_empty | O(1) ⚡ | Just checks if head is NULL |
list_sort | O(n²) | Bubble sort implementation |
list_unique | O(n) | Single pass after sorting |
list_reverse | O(n) | Single pass, swaps pointers |
Memory overhead:** 8-16 bytes per node (two pointers)
Performance Comparison
When to use forward lists (flist):**
- Memory is critical (saves 4-8 bytes per node)
- Only need forward traversal
- Rarely need tail operations
Example: Stack-like data structures, one-directional iteration
When to use doubly-linked lists (list):**
- Need frequent tail operations (O(1) vs O(n) ⚡)
- Need to insert/remove at known positions efficiently
- Need bidirectional traversal
- Memory overhead is acceptable
- Example: LRU caches, undo/redo systems, navigation histories
Limitations
Forward List Limitations
- No backward traversal: Cannot iterate backward through the list.
- No tail pointer:
push_back and pop_back are O(n). For frequent tail operations, use doubly-linked lists instead.
- No insert_before: Cannot insert before a node without traversing from head. Use doubly-linked lists for O(1) insert_before.
- Bubble sort: The sort implementation is O(n²). For large lists, consider using an external sorting algorithm.
Limitations
- Memory overhead: Uses twice the memory per node compared to forward lists (two pointers instead of one).
- Bubble sort: The sort implementation is O(n²). For large lists, consider using an external sorting algorithm.
- No tail caching: While tail operations are O(1), finding the tail initially requires traversal. For repeated tail access, cache the tail pointer in your application.
Common Limitations (Both Types)
- No built-in search: Searching requires manual traversal. Implement application-specific search functions.
- No thread safety: The library is not thread-safe. Implement external synchronization if needed.
- No automatic memory management: Applications must manage node allocation and provide deletion callbacks.
When to Use Alternatives
Consider alternatives if you need:
Instead of forward lists (flist):**
- Frequent tail operations: Use doubly-linked list (
list) for O(1) tail access ⚡
- Bidirectional traversal: Use doubly-linked list (
list)
Insert before operation: Use doubly-linked list (list) for O(1) insert_before ⚡
Instead of doubly-linked lists (list):**
- Minimal memory usage: Use forward list (
flist) to save 4-8 bytes per node
Simple one-directional access: Use forward list (flist) for simplicity
Instead of any linked list:**
- Fast random access: Use array-based structures (indexing is O(1) vs O(n))
- Efficient sorting of large datasets: Pre-sort data before insertion or use array + qsort
- Memory profiling: Use
lib_alloc for allocation tracking and debugging
Fixed-size collections: Use static arrays for better cache locality
Hybrid approaches:**
- Maintain a separate tail pointer with forward lists for O(1) tail access
- Use ring buffers for fixed-size FIFO/LIFO operations
- Combine with hash tables for O(1) lookup + ordered iteration