Implementing a Stack with an Array
An array is one of the simplest ways to implement a stack. Since a stack follows the
Last In, First Out (LIFO) principle, the end of
an array can be used as the top of the stack. Elements are added with
push() and removed with
pop().
This approach is especially convenient in JavaScript because arrays already provide the operations needed to build a basic stack. A custom class can then expose only the operations that a stack should support.
How the Array Represents a Stack
Consider an array containing three values:
const stack = [10, 20, 30];
Top
↓
[10, 20, 30]
The last array element represents the top of the stack. Therefore:
stack.push(40)adds 40 to the top.stack.pop()removes 40 from the top.stack[stack.length - 1]reads the top value.stack.lengthgives the current stack size.
Basic Array-Based Stack
The simplest implementation can use an array directly:
const stack = [];
stack.push(10);
stack.push(20);
stack.push(30);
console.log(stack);
// [10, 20, 30]
console.log(stack.pop());
// 30
console.log(stack);
// [10, 20]
Although this works, exposing the array directly allows other code to perform operations that do not follow stack behavior. A class provides a cleaner abstraction.
Creating a Stack Class
A custom stack class can keep the underlying array private from the code using the
data structure conceptually and provide methods such as
push, pop,
peek, isEmpty,
and size.
class Stack {
constructor() {
this.items = [];
}
push(value) {
this.items.push(value);
}
pop() {
if (this.isEmpty()) {
return undefined;
}
return this.items.pop();
}
peek() {
if (this.isEmpty()) {
return undefined;
}
return this.items[this.items.length - 1];
}
isEmpty() {
return this.items.length === 0;
}
size() {
return this.items.length;
}
}
Adding Elements with Push
The push operation adds a new value to the end of the array. Because the end represents the top of the stack, the new value immediately becomes the top element.
const stack = [];
stack.push("A");
stack.push("B");
stack.push("C");
console.log(stack);
// ["A", "B", "C"]
After these operations, C is the top element.
If another value is pushed, it will be placed above C.
Removing Elements with Pop
The pop operation removes the last element of the array and returns it. This matches the LIFO behavior of a stack.
const stack = ["A", "B", "C"];
const removed = stack.pop();
console.log(removed);
// C
console.log(stack);
// ["A", "B"]
The important point is that pop() removes only
the current top element. The remaining elements stay in their original order.
Reading the Top with Peek
JavaScript arrays do not provide a method specifically named
peek(), so we can access the last element using
its index.
const stack = [10, 20, 30];
const top = stack[stack.length - 1];
console.log(top);
// 30
console.log(stack);
// [10, 20, 30]
The array is unchanged because reading the last element does not remove it.
A custom stack class can wrap this expression inside a
peek() method.
Checking Whether the Stack Is Empty
The stack is empty when its array contains zero elements. The
length property makes this check straightforward.
const stack = [];
console.log(stack.length === 0);
// true
stack.push(100);
console.log(stack.length === 0);
// false
Encapsulating this logic inside an isEmpty()
method makes algorithms easier to read and keeps implementation details out of
calling code.
Getting the Stack Size
Since the array stores every stack element, its
length property represents the current stack size.
const stack = [15, 25, 35, 45];
console.log(stack.length);
// 4
No additional counter is necessary when using the array's length directly.
Complete Array-Based Implementation
The following implementation brings the core operations together into a reusable stack data structure.
class Stack {
constructor() {
this.items = [];
}
push(value) {
this.items.push(value);
}
pop() {
if (this.items.length === 0) {
return undefined;
}
return this.items.pop();
}
peek() {
if (this.items.length === 0) {
return undefined;
}
return this.items[this.items.length - 1];
}
isEmpty() {
return this.items.length === 0;
}
size() {
return this.items.length;
}
clear() {
this.items.length = 0;
}
}
const stack = new Stack();
stack.push(10);
stack.push(20);
stack.push(30);
console.log("Top:", stack.peek());
console.log("Size:", stack.size());
console.log("Removed:", stack.pop());
console.log("Top:", stack.peek());
console.log("Size:", stack.size());
Understanding the Operation Flow
Suppose an empty stack receives the following operations:
push(10),
push(20),
push(30),
and then pop().
| Step | Operation | Array | Top |
|---|---|---|---|
| 1 | push(10) | [10] | 10 |
| 2 | push(20) | [10, 20] | 20 |
| 3 | push(30) | [10, 20, 30] | 30 |
| 4 | pop() | [10, 20] | 20 |
Why the End of the Array Should Be the Top
A common implementation mistake is to use the beginning of the array as the top and
call unshift() and
shift(). While this can model the correct LIFO
behavior, removing or adding at the beginning can require existing elements to be
moved.
// Less suitable for a large stack
const stack = [];
stack.unshift(10);
stack.unshift(20);
stack.unshift(30);
const value = stack.shift();
Using the end of the array avoids this unnecessary shifting in the normal case:
// Preferred approach
const stack = [];
stack.push(10);
stack.push(20);
stack.push(30);
const value = stack.pop();
Handling an Empty Array
Calling pop() on an empty JavaScript array returns
undefined. A custom stack can preserve this behavior
or explicitly check the stack before removing an element.
class Stack {
constructor() {
this.items = [];
}
pop() {
if (this.items.length === 0) {
return undefined;
}
return this.items.pop();
}
peek() {
if (this.items.length === 0) {
return undefined;
}
return this.items[this.items.length - 1];
}
}
const stack = new Stack();
console.log(stack.pop()); // undefined
console.log(stack.peek()); // undefined
Array Stack and Memory
A stack implemented with a dynamic JavaScript array can grow as elements are added. JavaScript manages the underlying array storage automatically. This makes the approach convenient, but the runtime may occasionally allocate a larger internal storage area as the array grows.
For most application-level stack usage, this implementation detail does not need to be managed manually. When learning data structures, the important design decision is to keep the stack's top at the array end so the primary operations remain efficient.
Array Stack vs Linked List Stack
| Feature | Array Stack | Linked List Stack |
|---|---|---|
| Storage | Array elements | Individual nodes |
| Push | O(1) amortized | O(1) |
| Pop | O(1) | O(1) |
| Memory overhead | Generally lower per element | Extra reference per node |
| Implementation | Simple | More manual pointer management |
When an Array-Based Stack Is Useful
An array-based stack is a practical choice when the application needs a straightforward LIFO structure and does not require specialized linked-node behavior. Common examples include:
- Undo operations in editors
- Temporary processing of tasks in reverse order
- Depth-first search implementations
- Backtracking algorithms
- Expression and parentheses processing
- Maintaining application state history
- Interview and competitive programming problems
Common Implementation Mistakes
-
Using
shift()andunshift()for a large stack when the array end can be used instead. - Forgetting to handle pop and peek when the stack is empty.
- Exposing the underlying array and allowing arbitrary insertion or deletion that breaks stack behavior.
- Confusing the top element with the first element of the array.
- Assuming that every array operation has the same complexity as push and pop.
Complexity of an Array-Based Stack
When the end of the array represents the stack top, the main stack operations are efficient.
| Operation | Time Complexity |
|---|---|
| push | O(1) amortized |
| pop | O(1) |
| peek | O(1) |
| isEmpty | O(1) |
| size | O(1) |
The amortized O(1) complexity for push accounts for occasional internal resizing performed by a dynamic array implementation. Individual resizing operations can take longer, but over a sequence of insertions the average cost remains constant under the usual dynamic-array growth strategy.
Key Takeaways
- An array can represent a stack by using its end as the top.
push()adds an element to the top.pop()removes the most recently added element.peek()reads the top without removing it.- The array's
lengthprovides the stack size. - Using the array end avoids the shifting associated with front-based operations.
- Push is typically O(1) amortized, while pop and peek are O(1).
- Array-based stacks are simple and practical for many LIFO-based problems.