Data structures in C · about 55 min

Stacks and queues

Why this matters

Stacks and queues are less data structures than disciplines: rules about which element you may take out next. A stack hands back the most recently added element; a queue hands back the oldest. These two rules turn up everywhere. The call stack that holds your function frames is a stack. Undo is a stack. Parsers use stacks to match brackets and evaluate expressions. Keyboard input, network packets, print jobs and messages between threads wait in queues. Both are easy to build from what you already have, and the ring buffer you will write for the queue is one of the most widely used structures in systems programming: it sits inside every terminal driver, audio pipeline and network card.

Stacks: last in, first out

A stack supports push (add on top), pop (remove from the top), and often peek (look at the top without removing it). Pop on an empty stack is underflow, an error the code must report.

   push 1    push 2    push 3    pop -> 3   pop -> 2
                         [3]
               [2]       [2]       [2]
     [1]       [1]       [1]       [1]        [1]

A dynamic array is a perfect stack: the top is the end of the array, so push and pop are O(1) and nothing ever shifts.

typedef struct {
    int *data;
    size_t len, cap;
} Stack;

int stack_push(Stack *s, int value)
{
    if (s->len == s->cap) {
        size_t new_cap = s->cap ? s->cap * 2 : 8;
        int *tmp = realloc(s->data, new_cap * sizeof *tmp);
        if (tmp == NULL)
            return -1;
        s->data = tmp;
        s->cap = new_cap;
    }
    s->data[s->len++] = value;
    return 0;
}

int stack_pop(Stack *s, int *out)
{
    if (s->len == 0)
        return -1;          /* underflow */
    *out = s->data[--s->len];
    return 0;
}

Pushing 1, 2, 3 and then popping until empty prints pop 3, pop 2, pop 1. Notice s->data[--s->len]: decrement first, then index, because len is one past the top element. A linked list with push_front and a "pop front" also makes a fine stack, with one allocation per element.

A classic use: matching brackets

To check that f(a[1], {b}) is properly nested, scan left to right. Push every opener. At every closer, the most recent unclosed opener (the top of the stack) must be its partner: pop it. A mismatch, or a closer when the stack is empty, is an error; openers left at the end are unclosed.

  input:   f  (  a  [  1  ]  ,     {  b  }  )
  stack:      (     ([    (        ({    (
                       pop [          pop {  pop (   -> empty at end: ok

Compilers, JSON parsers and your text editor's bracket highlighting all use this idea.

Queues: first in, first out

A queue supports enqueue (add at the back) and dequeue (remove from the front). Using a plain array with the front at index 0 would make dequeue O(n), because everything shifts left. Two good implementations avoid that:

  1. A linked list with a tail pointer: enqueue appends at the tail, dequeue removes the head, both O(1). Unbounded, but one malloc per element.
  2. A ring buffer (circular buffer): a fixed array where the front index moves forward and wraps around. No allocation at all after setup, which is why low-level code loves it.

The ring buffer

Keep an array, the index of the oldest element (head), and how many elements are stored (count). The next free slot is head + count, wrapped around the end of the array:

#define QCAP 4

typedef struct {
    int items[QCAP];
    size_t head;    /* index of the oldest item */
    size_t count;   /* number of items stored */
} Queue;

int queue_push(Queue *q, int value)
{
    if (q->count == QCAP)
        return -1;                              /* full */
    q->items[(q->head + q->count) % QCAP] = value;
    q->count++;
    return 0;
}

int queue_pop(Queue *q, int *out)
{
    if (q->count == 0)
        return -1;                              /* empty */
    *out = q->items[q->head];
    q->head = (q->head + 1) % QCAP;
    q->count--;
    return 0;
}

Trace it with capacity 4: push 1 to 5, pop twice, push 6 and 7, then pop everything.

  push 1..4     [1][2][3][4]   head=0 count=4    push 5 -> full
  pop, pop      [.][.][3][4]   head=2 count=2    (returned 1, 2)
  push 6        [6][.][3][4]   head=2 count=3    slot (2+2)%4 = 0
  push 7        [6][7][3][4]   head=2 count=4    slot (2+3)%4 = 1
  pop x4        returns 3, 4, 6, 7; head goes 3, 0, 1, 2

The real program prints:

queue full, 5 rejected
dequeue 1
dequeue 2
dequeue 3 (head now 3)
dequeue 4 (head now 0)
dequeue 6 (head now 1)
dequeue 7 (head now 2)

The elements never move; only head chases around the circle. When the capacity is a power of two, % QCAP can be & (QCAP - 1) (from the bit tricks lesson), which is why real ring buffers are usually sized 256, 4096 and so on.

Note: a ring buffer can also overwrite the oldest item when full instead of rejecting the new one. Logging systems and the kernel's dmesg buffer do exactly that: you keep the most recent N messages.

Choosing an implementation

needstackqueue
unbounded, simpledynamic arraylinked list with tail, or a growable ring buffer
fixed size, no allocationarray and a countring buffer
also need access at both endsa deque (double-ended queue), usually a ring buffer

Common mistakes

Recap

Going further: extend the ring buffer to grow: when full, allocate a block twice the size and copy the elements in queue order (two memcpy calls, because the contents may wrap). Then read about how the Linux kernel's kfifo uses free-running unsigned indexes and power-of-two masks.

Exercises

Exercise 1 · quiz

Stack or queue?

Which of these naturally use a queue (first in, first out) rather than a stack? Select all that apply.

Answer
  • Jobs waiting for a printer

  • Keystrokes buffered between the keyboard driver and a program

  • Breadth-first search, visiting nodes level by level

Anything that must be served in arrival order is a queue: print jobs, input buffers, network packets, BFS frontiers. Anything where the most recent item must be handled first is a stack: undo (undo the latest change), nested brackets (the latest opener must close first), and the call stack itself (the most recently called function returns first), which is where the name of the stack memory region comes from.

Exercise 2 · write code

Check the brackets

Read lines from stdin (each at most 200 characters) and check that the brackets (), [] and {} in each line are balanced and properly nested. Other characters are ignored. For each line print exactly one of:

Use a stack (an array of char and a count is enough here, since a line has at most 200 brackets).

$ printf 'f(a[1], {b})
(]
x = (1 + (2
)
' | ./prog
ok
error at 2
unclosed 2
error at 1

File: main.c

Starting point

#include <stdio.h>
#include <string.h>

int main(void)
{
    char line[256];
    while (fgets(line, sizeof line, stdin) != NULL) {
        /* check line */
        printf("ok\n");
    }
    return 0;
}

Example

input

f(a[1], {b})
(]
x = (1 + (2
)

expected output

ok
error at 2
unclosed 2
error at 1

Example

input

{[()()]}

([)]
no brackets here
((((
}{

expected output

ok
ok
error at 3
ok
unclosed 4
error at 1
Hint 1

Push every opener. On a closer, the top of the stack must be the matching opener: pop it if so, otherwise report an error at this column.

Hint 2

A closer with an empty stack is also an error. After the line, anything left on the stack is unclosed.

Hint 3

A helper that maps ')' to '(' and so on keeps the comparison short.

Reference solution
#include <stdio.h>
#include <string.h>

static char opener_for(char close)
{
    switch (close) {
    case ')': return '(';
    case ']': return '[';
    case '}': return '{';
    default:  return 0;
    }
}

int main(void)
{
    char line[256];
    while (fgets(line, sizeof line, stdin) != NULL) {
        char stack[256];
        size_t top = 0;            /* number of items on the stack */
        int error_col = 0;
        for (size_t i = 0; line[i] != '\0' && line[i] != '\n'; i++) {
            char c = line[i];
            if (c == '(' || c == '[' || c == '{') {
                stack[top++] = c;
            } else if (opener_for(c)) {
                if (top == 0 || stack[top - 1] != opener_for(c)) {
                    error_col = (int)i + 1;
                    break;
                }
                top--;
            }
        }
        if (error_col)
            printf("error at %d\n", error_col);
        else if (top > 0)
            printf("unclosed %zu\n", top);
        else
            printf("ok\n");
    }
    return 0;
}

Exercise 3 · quiz

Full or empty?

A ring buffer with 8 slots is implemented with two indexes only, head (next item to read) and tail (next free slot), both advancing modulo 8, and no count. After pushing 8 items without popping, tail has wrapped around and equals head again. What is the problem?

Answer
  • head == tail is also the state of an empty buffer, so full and empty cannot be told apart

With only two indexes, both "nothing stored" and "every slot stored" look like head == tail. Common fixes: keep a separate count (as the lesson does), deliberately leave one slot unused so "full" means (tail + 1) % N == head, or let the indexes run freely and compute tail - head with unsigned arithmetic, masking only when indexing (this works when N is a power of two).

Exercise 4 · write code

A ring-buffer queue

Implement a fixed-capacity queue of int stored in a ring buffer:

#define QCAP 8
typedef struct {
    int items[QCAP];
    unsigned head;    /* index of the oldest item */
    unsigned count;   /* number of items stored   */
} Queue;

void q_init(Queue *q);
int  q_push(Queue *q, int value);  /* 0, or -1 if full  */
int  q_pop(Queue *q, int *out);    /* 0, or -1 if empty */

Do not shift elements: head moves forward on pop and wraps around to 0 after QCAP - 1. QCAP is a power of two, so you may use & (QCAP - 1) instead of % QCAP.

main reads commands from stdin: a number pushes it (printing full if rejected), and p pops one (printing the value or empty). Keep main as given.

$ echo "1 2 p 3 p p p" | ./prog
1
2
3
empty

File: main.c

Starting point

#include <stdio.h>
#include <stdlib.h>

#define QCAP 8

typedef struct {
    int items[QCAP];
    unsigned head;
    unsigned count;
} Queue;

void q_init(Queue *q)
{
    q->head = 0;
    q->count = 0;
}

int q_push(Queue *q, int value)
{
    if (q->count == QCAP)
        return -1;
    q->items[q->count++] = value;
    return 0;
}

int q_pop(Queue *q, int *out)
{
    if (q->count == 0)
        return -1;
    *out = q->items[--q->count];
    return 0;
}

int main(void)
{
    Queue q;
    q_init(&q);
    char word[32];
    while (scanf("%31s", word) == 1) {
        if (word[0] == 'p') {
            int x;
            if (q_pop(&q, &x) == 0)
                printf("%d\n", x);
            else
                printf("empty\n");
        } else if (q_push(&q, atoi(word)) != 0) {
            printf("full\n");
        }
    }
    return 0;
}

Example

input

1 2 p 3 p p p

expected output

1
2
3
empty

Example

input

1 2 3 4 5 6 7 8 9 p p 10 11 12 p p p p p p p p p p

expected output

full
1
2
full
3
4
5
6
7
8
10
11
empty
empty
Hint 1

The starter is a stack: it pops the newest item. A queue must pop items[head], the oldest.

Hint 2

The next free slot is (head + count) wrapped to the array size. After popping, advance head by one, wrapping.

Hint 3

q->items[(q->head + q->count) & (QCAP - 1)] = value; and in pop: q->head = (q->head + 1) & (QCAP - 1);

Reference solution
#include <stdio.h>
#include <stdlib.h>

#define QCAP 8

typedef struct {
    int items[QCAP];
    unsigned head;
    unsigned count;
} Queue;

void q_init(Queue *q)
{
    q->head = 0;
    q->count = 0;
}

int q_push(Queue *q, int value)
{
    if (q->count == QCAP)
        return -1;
    q->items[(q->head + q->count) & (QCAP - 1)] = value;
    q->count++;
    return 0;
}

int q_pop(Queue *q, int *out)
{
    if (q->count == 0)
        return -1;
    *out = q->items[q->head];
    q->head = (q->head + 1) & (QCAP - 1);
    q->count--;
    return 0;
}

int main(void)
{
    Queue q;
    q_init(&q);
    char word[32];
    while (scanf("%31s", word) == 1) {
        if (word[0] == 'p') {
            int x;
            if (q_pop(&q, &x) == 0)
                printf("%d\n", x);
            else
                printf("empty\n");
        } else if (q_push(&q, atoi(word)) != 0) {
            printf("full\n");
        }
    }
    return 0;
}