DEV Community

Noor
Noor

Posted on AI-assisted

Why doubling makes a dynamic array O(1) (and the realloc bug almost everyone writes)

If you have used std::vector or Python lists, you have used a dynamic array. In C you have to build it yourself, and two details decide whether it is fast and correct: how much you grow, and how you call realloc.

The idea

A dynamic array stores three things: a pointer to the data, the number of elements (size) and the space available (capacity).

typedef struct {
    int    *data;
    size_t  size;
    size_t  capacity;
} Vec;
Enter fullscreen mode Exit fullscreen mode

When size == capacity and you want to push another element, you need more room.

Growing by +1 is slow

If every push on a full array grows capacity by one, each push may copy the whole array. Pushing n elements costs about 1 + 2 + ... + n, which is O(n²). Ten thousand pushes means roughly fifty million element copies.

Doubling is amortized O(1)

If you double the capacity when it is full, copies happen rarely. Starting at capacity 1, you copy at sizes 1, 2, 4, 8, ... up to n. Total copies are 1 + 2 + 4 + ... + n/2, which is less than n.

So n pushes cost O(n) in total, and each push costs O(1) on average. Most pushes are cheap and an occasional push is expensive. That is what "amortized" means.

The realloc bug

This is the version many people write first:

v->data = realloc(v->data, new_cap * sizeof *v->data);   // BUG
Enter fullscreen mode Exit fullscreen mode

If realloc fails it returns NULL and does not free the old block. You have just overwritten your only pointer to it. The old memory leaks, and v->data is now NULL, so the next access crashes.

The fix is to use a temporary:

int *tmp = realloc(v->data, new_cap * sizeof *tmp);
if (tmp == NULL) {
    return -1;            // v->data is still valid, nothing is lost
}
v->data = tmp;
v->capacity = new_cap;
Enter fullscreen mode Exit fullscreen mode

Also check for overflow before you multiply: if capacity is already very large, capacity * 2 can wrap around.

A complete push

int vec_push(Vec *v, int value) {
    if (v->size == v->capacity) {
        size_t new_cap = v->capacity ? v->capacity * 2 : 4;
        int *tmp = realloc(v->data, new_cap * sizeof *tmp);
        if (tmp == NULL) return -1;
        v->data = tmp;
        v->capacity = new_cap;
    }
    v->data[v->size++] = value;
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Test it

Push 100,000 integers, then check every value and size. Run it under a leak checker and make sure that a failed allocation leaves the vector usable.

Code

A commented, tested version of this (with a linked list and a stack) is on GitHub under the MIT license: https://github.com/abbomokhnoor/c-data-structures-lite

Disclosure: I also sell a larger paid version of this kit (trees, heaps, hash tables, graphs, sorting, 114 tests and a study guide): https://payhip.com/b/djq5l

Top comments (0)