A queue in an array has eight cells, yet it turns an item away with only two waiting. Watch where the other six went.
A queue that runs out of room
Eight cells; the head moves on and the cells behind it are lost
bool enqueue(struct queue *q, int v){ if (q->tail == CAP) return false; q->items[q->tail] = v; q->tail++; return true;} int dequeue(struct queue *q){ int v = q->items[q->head]; q->head++; return v;}int main(void){ struct queue q = { .head = 0, .tail = 0 }; for (int v = 1; v <= 3; v++) enqueue(&q, v); printf("served"); for (int v = 4; v <= 9; v++) { int x = dequeue(&q); printf(" %d", x); if (!enqueue(&q, v)) printf("\n%d refused with %d of %d cells in use\n", v, q.tail - q.head, CAP); } return 0;}main has started. q, a struct queue, is in main's frame: 8 ints, head and tail.
- head
- ?
- tail
- ?
- waiting
- ?
- behind head
- ?
Output
Describe this picture
Across the top is q.items, main’s array of 8 ints, as a row of cells numbered 0 to 7 with each cell’s value inside. A tag named tail sits above the cell where the next item will go, and a tag named head sits below the cell of the oldest item. The cells before head are greyed: they still hold numbers, but the queue can no longer use them. The cell the last line wrote is outlined. Under the picture are the lines of queue.c from enqueue to the end, with the line about to run marked, one sentence on the current step, four readouts (head, tail, the items waiting, and the cells behind head) and the output. The clip plays once, about 22 seconds. q starts all zeros, head and tail 0. The first enqueue runs line by line: 1 goes into cell 0 and tail moves to 1. 2 and 3 join a beat each, so tail is 3. printf prints served. The first dequeue runs line by line: it reads 1 from cell 0, then head moves to 1 and cell 0 greys with its 1 still in it; 4 joins in cell 3. Four more turns follow, one leaving and one joining, a beat each: head moves right one cell at a time, the greyed cells pile up behind it, and when 8 goes into cell 7 tail reaches 8, past the last cell, and its tag fades. In the last turn one more item leaves, then the ninth enqueue runs line by line: tail is 8, so 9 is refused, and cells 0 to 5 turn to the warning colour. The output ends 9 refused with 2 of 8 cells in use, and the clip rests with head at 6 and 7 and 8 waiting. The transport plays and pauses, steps one line back or forward, and scrubs; a paused step is kept in the page address as queue.step.
First in, first out
A printer takes jobs in the order they were sent. The first job sent is printed first, the next one after it, and a new job waits behind all the others. A structure that hands things back in the order they arrived is a queue. First in, first out.
A queue has three operations:
- enqueue: add an item at the back, the tail.
- dequeue: take the item at the front, the head, and return it.
- peek: read the item at the head without taking it.
A stack does all its work at one end. A queue works at both: items join at one end and leave at the other. It is not a stack read backwards: in a stack the item that leaves is the newest; in a queue it is the oldest.
The code and its cost
An array with two indices
#define CAP 8
struct queue {
int items[CAP];
int head, tail;
};
head is the index of the oldest item. tail is the index of the next free cell, one past the newest item. The items waiting are the cells from head up to tail − 1, so there are tail − head of them, and the queue is empty when head equals tail. sizeof (struct queue) is 40 bytes: 32 for the array, 4 each for head and tail. In the clip, q sits at 0x7fc0, so items[i] is at 0x7fc0 + 4i, head at 0x7fe0 and tail at 0x7fe4.
bool enqueue(struct queue *q, int v)
{
if (q->tail == CAP)
return false;
q->items[q->tail] = v;
q->tail++;
return true;
}
int dequeue(struct queue *q)
{
int v = q->items[q->head];
q->head++;
return v;
}
enqueue writes into the cell at tail, then moves tail on. dequeue reads the cell at head, then moves head on. Like pop in Stacks, dequeue erases nothing: after the first dequeue in the clip, items[0] still holds 1. It is simply behind the head, and no longer in the queue. dequeue also trusts its caller: on an empty queue (head equal to tail) it would read a cell that holds no item, and with head at CAP it would read items[8], past the end of the array, which is undefined behaviour. The caller must check first.
Each operation is a fixed few steps: a comparison and two writes for enqueue, a read and a write for dequeue, one read for peek. How many items wait makes no difference.
Why it runs out of room
head and tail only ever move right. When tail reaches 8, enqueue refuses every new item, however many cells lie unused behind head. In the clip that happens with two items waiting: cells 0 to 5 hold old numbers that no one will read again, and the queue cannot reach them. They come back only if the queue empties completely and both indices are put back to 0, and a queue that is never empty never gets them back.
The program prints the refusal itself:
served 1 2 3 4 5 6
9 refused with 2 of 8 cells in use
Shifting instead
One fix keeps the oldest item always in cell 0: dequeue takes items[0] and then moves every other item one cell to the left.
int dequeue(struct queue *q)
{
int v = q->items[0];
for (int i = 1; i < q->tail; i++) {
q->items[i - 1] = q->items[i];
moves++;
}
q->tail--;
return v;
}
Now no cell is ever lost, and the same arrivals are never refused. But each dequeue moves every item left behind it. In queue-shift.c, the same program with this dequeue and a counter, three items wait at every dequeue, so each one moves two items, and the six dequeues move 12 in all:
served 1 2 3 4 5 6
12 moves
With n items waiting, a dequeue costs n − 1 moves. The two-index queue never moves an item; the shifting queue never loses a cell. Circular queues get both, by letting head and tail wrap round to cell 0.
A queue of nodes
On the nodes of Singly linked lists, a queue has no capacity to run out of. The head of the list is the head of the queue, and the queue keeps a second pointer to the last node:
struct lqueue {
struct node *head, *tail;
};
void enqueue(struct lqueue *q, int v)
{
struct node *n = malloc(sizeof *n);
if (n == NULL)
exit(1);
n->key = v;
n->next = NULL;
if (q->tail == NULL)
q->head = n;
else
q->tail->next = n;
q->tail = n;
}
A new node joins after the last one. The tail pointer is what makes that fast: without it, every enqueue would have to walk the whole list from the head to find the last node, a step for every item waiting. With it, enqueue is a fixed few steps. dequeue unlinks the head node, as removing the first node in Removing from a linked list does, and frees it. When it takes the last node, it must clear tail as well, or tail would keep the address of a freed node.
The same queue on nodes
Join after the tail node, leave from the head node
#include <stdio.h>#include <stdlib.h> struct node { int key; struct node *next;}; struct lqueue { struct node *head, *tail;}; void enqueue(struct lqueue *q, int v){ struct node *n = malloc(sizeof *n); if (n == NULL) exit(1); n->key = v; n->next = NULL; if (q->tail == NULL) q->head = n; else q->tail->next = n; q->tail = n;} int dequeue(struct lqueue *q){ struct node *n = q->head; int v = n->key; q->head = n->next; if (q->head == NULL) q->tail = NULL; free(n); return v;} int main(void){ struct lqueue q = { NULL, NULL }; for (int v = 1; v <= 3; v++) enqueue(&q, v); printf("served"); while (q.head != NULL) { int x = dequeue(&q); printf(" %d", x); } printf("\n"); return 0;}main has started. q, a struct lqueue, holds two pointers: head and tail.
Output
Describe this picture
queue-linked.c, all of it, with the line about to run marked, beside a picture of memory. At the top is the stack: main’s frame with q.head and q.tail (the struct lqueue at 0x7fe0), v and x, and beside it the frame of the function main has called. Below is the heap: one row of 16-byte nodes in address order, 1 at 0x1000, 2 at 0x1020 and 3 at 0x1040, each with its key and next field; arrows run from each next field to the node it holds the address of, and from q.head and q.tail down to their nodes; a NULL pointer is a short bar. A pointer of a called function is a small tag on its node. The replay plays once, about 24 seconds. The first enqueue runs line by line: the node appears at 0x1000, and because tail is NULL, q.head and then q.tail both point at it. The second runs line by line too: the 2 node appears, the 1 node’s next arrow reaches it, and q.tail moves onto it. 3 joins in a beat. Then the dequeue loop: the first dequeue runs line by line, n tagged on the 1 node, q.head moving to 2 and the 1 node hatched; the output shows served 1. The second takes a beat. The last runs line by line: q.head becomes NULL, so q.tail is cleared too, and the 3 node is hatched. The output reads served 1 2 3. The transport plays and pauses, steps one line back or forward, and scrubs; a paused step is kept in the page address as nodes.step.
The linked queue never refuses an item while malloc can find memory. It pays with a malloc per enqueue, a free per dequeue, and a next pointer in every node: 32 bytes of heap for each int on this platform.
Running it yourself
gcc -std=c11 -Wall -Wextra -pedantic queue.c -o queue
./queue
It prints the two lines above. queue-shift.c prints 12 moves after the served line, and queue-linked.c prints served 1 2 3.
Worked example
By hand. On an empty array queue of 8 cells: enqueue 5, enqueue 8, dequeue, enqueue 2, dequeue, enqueue 6.
| Operation | Returns | items[0..3] | head | tail |
|---|---|---|---|---|
| enqueue 5 | 5, 0, 0, 0 | 0 | 1 | |
| enqueue 8 | 5, 8, 0, 0 | 0 | 2 | |
| dequeue | 5 | 5, 8, 0, 0 | 1 | 2 |
| enqueue 2 | 5, 8, 2, 0 | 1 | 3 | |
| dequeue | 8 | 5, 8, 2, 0 | 2 | 3 |
| enqueue 6 | 5, 8, 2, 6 | 2 | 4 |
The dequeues return 5 and then 8, in the order they arrived. Two items wait, 2 and 6, in cells 2 and 3, and cells 0 and 1 are behind head.
Empty and refused. Start empty, enqueue 8 items and dequeue all 8. head and tail are both 8, so the queue is empty, and yet the next enqueue is refused: tail is at the end. Every cell is free and none can be used.
Shifting a long queue. With 1,000 items waiting, one dequeue of the shifting version moves 999 of them. Serving them all moves 999 + 998 + … + 1 + 0 = 499,500 items, where the two-index queue moves none.
The fix to come. If tail went back to cell 0 after cell 7, the empty-and-refused queue above would take its next item in cell 0. That is the ring buffer of Circular queues.
Where it’s used
Printers and servers. A print queue holds documents in the order they were sent; a web server keeps requests waiting in a queue until a worker is free.
Messages between parts of a program. One part puts messages in a queue and another takes them out in the same order, so neither has to wait for the other.
Searching a graph. Breadth-first search (19.2) visits a maze or a network one layer at a time by keeping the places still to visit in a queue, and walking a tree level by level (12.4) works the same way.
Reference card
| Code or idea | Meaning |
|---|---|
| enqueue, dequeue, peek | join at the tail, leave from the head: first in, first out |
head, tail | the oldest item’s index, and the next free cell |
tail - head | the number of items waiting; 0 when empty |
q->items[q->tail] = v; q->tail++; | enqueue: write, then move on |
v = q->items[q->head]; q->head++; | dequeue: read, then move on |
tail == CAP | refused, even with free cells behind head |
| shift on dequeue | no lost cells, n − 1 moves per dequeue |
| nodes with head and tail pointers | no capacity; the tail pointer makes enqueue a fixed few steps |
Platform: C11, gcc on x86-64 Linux: int 4 bytes, pointers 8, little-endian. Addresses in the pictures are short stand-ins, by one rule for the whole course; sizes are the real ones.