Stack using dynamic array
A dynamic array stack still stores elements in one contiguous block and uses a top index, but the block grows on the heap (for example with realloc) when it runs out of room. Push and pop at the top stay O(1) in the usual case; growing is O(n) when it happens, so push is O(1) amortized if you double the capacity each resize. This page keeps top == -1 for empty and grows before push when top + 1 == capacity.
- Representation — heap buffer,
top, andcapacity - Operations — push with optional grow, pop, peek, free
- Example in C —
malloc/realloc, file-scope API, nostruct - Trade-offs — vs fixed array and linked list
1) Representation
You still need a contiguous place for values, the top index, and how many slots are currently allocated (capacity). The storage is typically an int * (or similar) pointing at the first element of a heap-allocated array; logical “array” operations use buf[0] .. buf[top].
| Component | Role | Notes |
|---|---|---|
| Heap buffer | Holds elements from index 0 through top. |
Created with malloc; grown with realloc; released with free. |
top |
Index of the current top; -1 when empty. |
Same convention as a fixed array stack. |
capacity |
Number of allocated slots (not the same as “how many pushes succeeded”). | When top + 1 == capacity, allocate a larger block before the next push. |
2) Mapping operations to the array
Push checks whether another element would exceed capacity; if so, grow (often double the capacity), then increment top and assign. Pop and peek match the fixed-array case; shrinking the buffer on pop is optional and less common in simple stacks.
| Operation | Steps | Failure case |
|---|---|---|
push(x) |
If top + 1 == capacity, realloc to a larger size; then top++, buf[top] = x. |
Out of memory if realloc returns NULL. |
pop |
If not empty: read buf[top], then top--. |
Underflow if empty. |
peek |
If not empty: read buf[top] without changing top. |
Underflow if empty. |
destroy / free |
Call free on the buffer when the stack is no longer needed. |
— |
3) Example in C
Below, one stack lives at file scope: a heap buffer, top, and capacity. The API uses no struct and no pointer parameters in main; stack_last holds the result of a successful peek or pop. INITIAL_CAPACITY is 1 so the second push triggers a grow in the trace tables.
#include <stdio.h>
#include <stdlib.h>
#define INITIAL_CAPACITY 1
/* Heap-backed array: valid indices 0..top; top == -1 means empty. */
static int *stack;
static int top;
static int capacity;
/* After stack_peek or stack_pop returns 1, read the value from here. */
static int stack_last;
/* Double capacity (or start at 1); returns 0 if realloc fails. */
static int grow_stack(void) {
int new_cap = capacity == 0 ? 1 : capacity * 2;
int *p = realloc(stack, (size_t)new_cap * sizeof *p);
if (p == NULL)
return 0;
stack = p;
capacity = new_cap;
return 1;
}
void stack_init(void) {
top = -1;
stack = malloc((size_t)INITIAL_CAPACITY * sizeof *stack);
if (stack == NULL) {
capacity = 0;
return;
}
capacity = INITIAL_CAPACITY;
}
void stack_destroy(void) {
free(stack);
stack = NULL;
top = -1;
capacity = 0;
}
int stack_is_empty(void) {
return top < 0;
}
/* Returns 1 on success, 0 if out of memory. */
int stack_push(int value) {
if (top + 1 >= capacity) {
if (!grow_stack())
return 0;
}
top++;
stack[top] = value;
return 1;
}
/* Returns 1 on success, 0 if underflow. */
int stack_pop(void) {
if (stack_is_empty())
return 0;
stack_last = stack[top];
top--;
return 1;
}
/* Returns 1 on success, 0 if underflow. */
int stack_peek(void) {
if (stack_is_empty())
return 0;
stack_last = stack[top];
return 1;
}
int main(void) {
stack_init();
stack_push(10);
stack_push(20);
if (stack_peek())
printf("peek: %d\n", stack_last);
if (stack_pop())
printf("pop: %d\n", stack_last);
stack_destroy();
return 0;
}
The buffer type can be changed from int; production code often wraps these fields in a struct or uses a growable “vector” type so you can have multiple stacks.
Explanation of the program output
Here’s the explanation of the program’s output in table format. INITIAL_CAPACITY is 1, so the second push grows the buffer before storing 20.
| Step | Operation | Stack state (top to bottom) | capacity |
Return value | stack_last value |
Output |
|---|---|---|---|---|---|---|
| 1 | stack_init() |
[] (empty, top = -1) |
1 (heap block allocated) |
(void) | unchanged | (none) |
| 2 | stack_push(10) |
[10] (top = 0) |
1 |
1 (success) |
unchanged | (none) |
| 3 | stack_push(20) |
realloc to 2 slots, then [20, 10] (top = 1) |
2 (after grow) |
1 (success) |
unchanged | (none) |
| 4 | stack_peek() |
[20, 10] (unchanged, top = 1) |
2 |
1 (success) |
20 (top value) |
peek: 20 |
| 5 | stack_pop() |
[10] (top = 0; extra slot still allocated) |
2 |
1 (success) |
20 (popped value) |
pop: 20 |
| 6 | stack_destroy() |
Buffer freed; stack logically empty | 0 |
(void) | — | (none) |
| 7 | Program ends | — | — | — | — | — |
| Line | Output |
|---|---|
| 1 | peek: 20 |
| 2 | pop: 20 |
Key points
| Behavior | Explanation |
|---|---|
Peek shows 20 |
Stack is LIFO (Last-In-First-Out), so the most recent push (20) is on top. |
Pop shows 20 |
The same top element (20) is removed and returned. |
| No output for pushes | stack_push() returns a success code but doesn’t print anything. |
| Second push grows storage | With INITIAL_CAPACITY == 1, the first push fills the only slot; the next push calls realloc before writing 20. |
10 was not popped |
The program frees the buffer with stack_destroy() before popping the remaining 10. |
capacity may exceed size |
After one pop, top + 1 is less than capacity; the extra slot is spare capacity for future pushes (this example does not shrink). |
4) Trade-offs
- Pros — No fixed compile-time cap; contiguous memory and good cache locality; push/pop still simple at the top.
- Cons —
reallocmay copy the whole block when growing; occasional O(n) cost; you mustfreethe buffer; wasted capacity if you shrink rarely.
Compare with a fixed array stack when the max size is known, or a linked-list stack if you want growth without reallocating a contiguous block. Operation definitions are on Stack operations.
Key takeaway
Treat top like a fixed-array stack, but add a grow step when top + 1 would reach capacity. Doubling capacity keeps the amortized cost of push low; always pair malloc/realloc with a clear free path.
See also: Stack using array · Stack using linked list · Stack operations