Linked Lists
Objectives
- Understand the concept of a linked list
- Implement the basic operations
- Analyze the complexity of operations
- Distinguish singly and doubly linked lists
Principle
A linked list is a data structure where each element (node) contains:
- A value (data)
- A pointer to the next element
[1] -> [2] -> [3] -> [4] -> NULL
Comparison with Arrays
| Criterion | Array | Linked list |
|---|---|---|
| Access by index | O(1) | O(n) |
| Insertion at the front | O(n) | O(1) |
| Insertion at the back | O(1)* | O(n)** |
| Deletion at the front | O(n) | O(1) |
| Memory | Contiguous | Fragmented |
* Amortized dynamic array ** O(1) with a tail pointer
When to use which?
- Array: frequent access by index, known fixed size
- List: frequent insertions/deletions, variable size
Singly Linked List
Structure
typedef struct Node {
int data; // Data
struct Node* next; // Pointer to the next node
} Node;
typedef Node* List; // Convenient alias
Creating a Node
Node* create_node(int value) {
Node* node = (Node*)malloc(sizeof(Node));
if (node == NULL) {
exit(EXIT_FAILURE);
}
node->data = value;
node->next = NULL;
return node;
}
Fundamental Operations
Prepend (O(1)):
List prepend(List list, int value) {
Node* node = create_node(value);
node->next = list; // New node points to the old head
return node; // New node becomes the head
}
Append (O(n)):
List append(List list, int value) {
Node* node = create_node(value);
if (list == NULL) {
return node;
}
Node* current = list;
while (current->next != NULL) {
current = current->next;
}
current->next = node;
return list;
}
Traversal:
void print_list(List list) {
Node* current = list;
while (current != NULL) {
printf("%d -> ", current->data);
current = current->next;
}
printf("NULL\n");
}
Search (O(n)):
Node* find(List list, int value) {
Node* current = list;
while (current != NULL) {
if (current->data == value) {
return current;
}
current = current->next;
}
return NULL; // Not found
}
Memory deallocation:
void free_list(List list) {
Node* current = list;
while (current != NULL) {
Node* next = current->next;
free(current);
current = next;
}
}
Doubly Linked List
Structure
Each node has two pointers: previous and next.
typedef struct DNode {
int data;
struct DNode* prev; // Pointer to the previous node
struct DNode* next; // Pointer to the next node
} DNode;
typedef struct {
DNode* head; // First element
DNode* tail; // Last element
int size; // Number of elements
} DoublyLinkedList;
NULL <- [1] <-> [2] <-> [3] <-> [4] -> NULL
^ ^
head tail
Advantages
- Traversal in both directions
- Deletion in O(1) if we have a pointer to the node
- Append in O(1) with the
tailpointer
Operations
Push front:
void push_front(DoublyLinkedList* list, int value) {
DNode* node = create_dnode(value);
if (list->head == NULL) {
list->head = list->tail = node;
} else {
node->next = list->head;
list->head->prev = node;
list->head = node;
}
list->size++;
}
Push back:
void push_back(DoublyLinkedList* list, int value) {
DNode* node = create_dnode(value);
if (list->tail == NULL) {
list->head = list->tail = node;
} else {
node->prev = list->tail;
list->tail->next = node;
list->tail = node;
}
list->size++;
}
Operation Complexity
| Operation | Singly | Doubly |
|---|---|---|
| Access to i-th element | O(n) | O(n) |
| Insert at front | O(1) | O(1) |
| Insert at back | O(n) | O(1)* |
| Delete at front | O(1) | O(1) |
| Delete at back | O(n) | O(1)* |
| Search | O(n) | O(n) |
* With tail pointer
Common Errors
Pitfalls to avoid
- Uninitialized list: always initialize to
NULL - Access on NULL: check before
node->next - Lost reference: save the pointer before modifying it
- Memory leak: free all nodes
Applications
- Stack and queue implementations
- History list (web navigation)
- Memory management (free block list)
- Sparse representation (polynomials, matrices)