Queues
A queue is a linear data structure that follows the FIFO principle — First In, First Out. Picture a line of people waiting at a checkout counter: the first person to join the line is the first person to be served. Elements are added at the rear (enqueue) and removed from the front (dequeue). Queues show up everywhere: task scheduling, breadth-first search, print spoolers, and message buffers.
Core Operations
enqueue— add an element to the rear of the queuedequeue— remove and return the element at the frontpeek/front— look at the front element without removing itisEmpty— check whether the queue has any elementsisFull— for a fixed-size implementation, check whether there is room left
The Naive Array Implementation Wastes Space
A first attempt at an array-based queue keeps a front index and a rear index. enqueue writes at rear and increments it; dequeue reads at front and increments it. The problem: front only ever moves forward, so once rear reaches the end of the array, the queue reports itself as full even though slots at the beginning have been vacated by earlier dequeues. The fix is a circular buffer.
Circular Buffer Indexing
A circular (ring) buffer treats the array as if its end wraps around to its beginning. Instead of always incrementing an index, we increment it modulo the array size: index = (index + 1) % capacity. This lets the queue reuse slots that were freed by earlier dequeues, so a fixed-size array can hold a continuous stream of enqueue/dequeue operations without ever needing to shift elements.
#include <stdio.h>
#define CAPACITY 5
typedef struct {
int items[CAPACITY];
int front;
int rear;
int count; /* number of elements currently stored */
} CircularQueue;
void initQueue(CircularQueue *q) {
q->front = 0;
q->rear = -1;
q->count = 0;
}
int isEmpty(const CircularQueue *q) {
return q->count == 0;
}
int isFull(const CircularQueue *q) {
return q->count == CAPACITY;
}
int enqueue(CircularQueue *q, int value) {
if (isFull(q)) {
return 0;
}
q->rear = (q->rear + 1) % CAPACITY; /* wrap around */
q->items[q->rear] = value;
q->count++;
return 1;
}
int dequeue(CircularQueue *q, int *outValue) {
if (isEmpty(q)) {
return 0;
}
*outValue = q->items[q->front];
q->front = (q->front + 1) % CAPACITY; /* wrap around */
q->count--;
return 1;
}
int main(void) {
CircularQueue q;
int value;
initQueue(&q);
enqueue(&q, 1);
enqueue(&q, 2);
enqueue(&q, 3);
dequeue(&q, &value);
printf("Dequeued: %d\n", value); /* 1 */
/* Because the buffer wraps around, we can keep enqueueing
even though "rear" would otherwise run off the array. */
enqueue(&q, 4);
enqueue(&q, 5);
enqueue(&q, 6);
while (dequeue(&q, &value)) {
printf("Dequeued: %d\n", value);
}
return 0;
}Implementing a Queue with a Linked List
A linked-list queue keeps two pointers: front (the head, where we dequeue) and rear (the tail, where we enqueue). Both operations run in O(1) because we never need to walk the list.
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int data;
struct Node *next;
} Node;
typedef struct {
Node *front;
Node *rear;
} LinkedQueue;
void initLinkedQueue(LinkedQueue *q) {
q->front = NULL;
q->rear = NULL;
}
int lqIsEmpty(const LinkedQueue *q) {
return q->front == NULL;
}
int lqEnqueue(LinkedQueue *q, int value) {
Node *node = malloc(sizeof(Node));
if (node == NULL) {
return 0;
}
node->data = value;
node->next = NULL;
if (q->rear == NULL) {
q->front = node;
q->rear = node;
} else {
q->rear->next = node;
q->rear = node;
}
return 1;
}
int lqDequeue(LinkedQueue *q, int *outValue) {
if (lqIsEmpty(q)) {
return 0;
}
Node *old = q->front;
*outValue = old->data;
q->front = old->next;
if (q->front == NULL) {
q->rear = NULL; /* queue is now empty */
}
free(old);
return 1;
}
int main(void) {
LinkedQueue q;
int value;
initLinkedQueue(&q);
lqEnqueue(&q, 100);
lqEnqueue(&q, 200);
lqEnqueue(&q, 300);
while (lqDequeue(&q, &value)) {
printf("Dequeued: %d\n", value);
}
return 0;
}Array (Circular) vs Linked List
Aspect | Circular array queue | Linked-list queue |
|---|---|---|
Maximum size | Fixed at creation | Limited only by available memory |
Memory overhead | None per element | Extra pointer per node |
Cache locality | Good (contiguous memory) | Poorer (nodes scattered on the heap) |
Resizing | Requires reallocation + re-copy | Grows naturally, one node at a time |