Loading learning content...
While isEmpty() tells you whether the stack has any elements, sometimes you need to know how many. How deep is this recursion? How many undo operations have accumulated? Are we approaching capacity?
The size operation (also called count, length, or depth in various contexts) returns the exact number of elements currently in the stack. This quantitative information enables capacity management, progress tracking, and algorithms that need to know the stack's magnitude, not just its emptiness.
By the end of this page, you will understand the size operation's semantics and contract, how it compares to isEmpty, its implementation across array and linked list representations, the importance of O(1) size tracking, capacity-related considerations, and practical use cases where size provides value beyond simple emptiness checks.
The size operation returns the number of elements currently stored in the stack. It's a non-negative integer that changes predictably with push and pop:
push(), size increases by 1pop(), size decreases by 1peek() and isEmpty() don't change sizeThe invariant:
size() === (number of push operations) - (number of pop operations)
This relationship holds from the moment the stack is created, providing a simple mental model for tracking size.
1234567891011121314151617
Operation Stack State size() Change─────────────────────────────────────────────────────────────(initial) [] 0 -push(A) [A] 1 +1push(B) [A, B] 2 +1push(C) [A, B, C] 3 +1peek() [A, B, C] 3 (no change)pop() [A, B] 2 -1isEmpty() [A, B] 2 (no change)push(D) [A, B, D] 3 +1pop() [A, B] 2 -1pop() [A] 1 -1pop() [] 0 -1pop() (error) - (blocked by underflow) Note: size() can never become negative. Attempting to pop when size is 0 results in an error, not -1.By definition, a stack cannot have a negative number of elements. The minimum size is 0 (empty stack). Any implementation that allows negative size has a bug. This is a fundamental invariant to maintain.
The relationship with isEmpty:
isEmpty() === true ⟺ size() === 0
isEmpty() === false ⟺ size() > 0
isEmpty() === !Boolean(size()) // In JavaScript
These are logically equivalent, but each has its place. isEmpty is preferred when you only care about emptiness; size is preferred when you need the count.
Like isEmpty, size is a pure observation operation with no preconditions and no side effects. It simply reports the current element count.
| Aspect | Description |
|---|---|
| Input | None |
| Output | Non-negative integer (0, 1, 2, ...) |
| Side Effect | None — purely observational |
| Time Guarantee | O(1) with proper implementation |
| Space Guarantee | O(1) — no allocations |
| Failure Mode | None — always succeeds |
Like isEmpty, size() can never fail. Both are pure queries that observe state without risk. Unlike peek() and pop() which fail on empty stacks, size() and isEmpty() are always safe to call. Use them liberally for defensive checks.
In an array-based stack, size is derived directly from topIndex. Since topIndex represents the index of the top element (with -1 meaning empty), the size is simply topIndex + 1.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
class ArrayStack<T> { private items: T[]; private topIndex: number; private capacity: number; constructor(capacity: number = 1000) { this.items = new Array<T>(capacity); this.topIndex = -1; // -1 indicates empty stack this.capacity = capacity; } /** * Size: Return the number of elements in the stack * * Time Complexity: O(1) * Space Complexity: O(1) * * @returns Non-negative integer representing element count */ size(): number { return this.topIndex + 1; } /** * Alternative: if we track size separately * (redundant with topIndex, but common in other implementations) */ // private _size: number = 0; // // size(): number { // return this._size; // } /** * isEmpty derived from size (showing the relationship) */ isEmpty(): boolean { return this.size() === 0; // Equivalent to: return this.topIndex < 0; } /** * Capacity: maximum number of elements (for bounded stacks) */ getCapacity(): number { return this.capacity; } /** * Check if stack is full */ isFull(): boolean { return this.size() >= this.capacity; // Equivalent to: return this.topIndex >= this.capacity - 1; } /** * Remaining capacity */ remainingCapacity(): number { return this.capacity - this.size(); }}Why topIndex + 1?
Array indices are 0-based, so:
The formula correctly converts the last occupied index to a count.
123456789101112131415
State: items = [10, 20, 30, _, _, ...] topIndex = 2 size() = topIndex + 1 = 2 + 1 = 3 Verification: Elements at indices 0, 1, 2 → three elements ✓ ───────────────────────────────────────── State: items = [_, _, _, _, _, ...] topIndex = -1 size() = topIndex + 1 = -1 + 1 = 0 Verification: No elements → zero ✓In array-based stacks, topIndex serves as the single source of truth for size. We derive size from topIndex rather than maintaining a separate count. This prevents synchronization bugs—there's only one value to update on push/pop.
In a linked list-based stack, size can be computed in two ways:
count variable, updating it on push (+1) and pop (-1).The second approach is strongly preferred for performance. Let's examine both.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
class StackNode<T> { value: T; next: StackNode<T> | null; constructor(value: T) { this.value = value; this.next = null; }} class LinkedListStack<T> { private head: StackNode<T> | null; private _count: number; // Tracked on each push/pop constructor() { this.head = null; this._count = 0; } /** * Size: Return the number of elements in the stack * * Time Complexity: O(1) — using tracked count * Space Complexity: O(1) */ size(): number { return this._count; } /** * BAD Alternative: Compute size by traversing (O(n)) * Don't use this! Included only to show why tracking is better. */ sizeByTraversal(): number { let count = 0; let current = this.head; while (current !== null) { count++; current = current.next; } return count; // This is O(n) every time size() is called — inefficient! } /** * Push: Add element and increment count */ push(element: T): void { const newNode = new StackNode(element); newNode.next = this.head; this.head = newNode; this._count++; // O(1) update keeps size() O(1) } /** * Pop: Remove element and decrement count */ pop(): T { if (this.head === null) { throw new Error("Stack underflow"); } const value = this.head.value; this.head = this.head.next; this._count--; // O(1) update keeps size() O(1) return value; } /** * isEmpty using tracked count */ isEmpty(): boolean { return this._count === 0; // Equivalent to: return this.head === null; }}| Approach | Time Complexity | Space Overhead | Pros | Cons |
|---|---|---|---|---|
| Traversal | O(n) | None | No extra state to maintain | Slow for large stacks; repeated calls are wasteful |
| Tracked count | O(1) | One integer | Fast, constant-time | Must update on every push/pop (easy to forget) |
Never implement size() as O(n) traversal in production code. Users expect size() to be O(1)—like isEmpty(), it's a simple query. An O(n) size() can cause unexpected performance degradation in algorithms that call it frequently, such as in loop conditions.
For bounded stacks (fixed-size array implementation), size and capacity form a pair:
The relationship size ≤ capacity must always hold. When size === capacity, the stack is full, and push operations will fail.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
class BoundedStack<T> { private items: T[]; private topIndex: number; private capacity: number; constructor(capacity: number) { this.items = new Array<T>(capacity); this.topIndex = -1; this.capacity = capacity; } size(): number { return this.topIndex + 1; } getCapacity(): number { return this.capacity; } /** * Check if stack is full * This is the 'capacity guard' — the complement to isEmpty */ isFull(): boolean { return this.size() >= this.capacity; } /** * How many more elements can be pushed? */ remainingCapacity(): number { return this.capacity - this.size(); } /** * What percentage of capacity is used? */ usagePercentage(): number { return (this.size() / this.capacity) * 100; } /** * Push with capacity check */ push(element: T): boolean { if (this.isFull()) { return false; // Or throw an error } this.topIndex++; this.items[this.topIndex] = element; return true; } /** * Attempt to push multiple elements * Returns number successfully pushed */ pushMany(elements: T[]): number { let pushed = 0; for (const element of elements) { if (this.isFull()) break; this.push(element); pushed++; } return pushed; }} // Usage example:const stack = new BoundedStack<number>(100);console.log(stack.size()); // 0console.log(stack.getCapacity()); // 100console.log(stack.remainingCapacity()); // 100console.log(stack.isFull()); // false for (let i = 0; i < 100; i++) { stack.push(i);} console.log(stack.size()); // 100console.log(stack.remainingCapacity()); // 0console.log(stack.isFull()); // trueconsole.log(stack.push(999)); // false (failed)Linked list stacks and dynamic array stacks have no fixed capacity—they grow until memory is exhausted. In these cases, getCapacity() might return Infinity or throw 'not applicable.' The concept of 'full' doesn't apply, only 'out of memory.'
While isEmpty covers most 'is it empty?' checks, size() is needed for more sophisticated scenarios involving counts, limits, and thresholds.
stack.size() + n <= capacityif (callStack.size() >= MAX_DEPTH) throw new Error('Too deep')1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
// Use Case 1: Depth-limited recursion simulationfunction limitedDFS(root: Node, maxDepth: number): void { const stack: Array<{node: Node, depth: number}> = []; stack.push({node: root, depth: 0}); while (stack.length > 0) { const {node, depth} = stack.pop()!; // SIZE-BASED LIMIT: Don't go deeper than maxDepth if (depth >= maxDepth) continue; process(node); for (const child of node.children) { stack.push({node: child, depth: depth + 1}); } }} // Use Case 2: Progress reporting for undo systemclass UndoManager { private undoStack: Action[] = []; private maxUndoSize: number = 100; recordAction(action: Action): void { this.undoStack.push(action); // SIZE-BASED LIMIT: Prevent unlimited memory usage if (this.undoStack.length > this.maxUndoSize) { this.undoStack.shift(); // Remove oldest } } // SIZE for user feedback getUndoCount(): number { return this.undoStack.length; } getStatusMessage(): string { const count = this.getUndoCount(); if (count === 0) return "Nothing to undo"; return `${count} action${count > 1 ? 's' : ''} can be undone`; }} // Use Case 3: Batch pop with size awarenessfunction popUpTo<T>(stack: Stack<T>, maxCount: number): T[] { const result: T[] = []; // Use SIZE to limit iterations const toPop = Math.min(maxCount, stack.size()); for (let i = 0; i < toPop; i++) { result.push(stack.pop()); } return result;} // Use Case 4: Monitoring and alertingfunction monitoredPush<T>(stack: Stack<T>, element: T, warnThreshold: number): void { stack.push(element); // SIZE-BASED monitoring if (stack.size() > warnThreshold) { console.warn(`Stack size ${stack.size()} exceeds threshold ${warnThreshold}`); }}During development, logging stack sizes at key points helps catch issues like 'stack growing unexpectedly' or 'stack not emptying when it should.' Size-based assertions like assert(stack.size() === expectedSize) catch bugs early.
Different languages and libraries use different names for the size operation. Being aware of these variations helps you work across ecosystems.
| Language/Library | Method Name | Return Type | Notes |
|---|---|---|---|
| Java Stack | size() | int | From Collection interface |
| C++ std::stack | size() | size_type | Returns size_t (unsigned) |
| Python list | len(stack) | int | Built-in function, not method |
| JavaScript Array | arr.length | number | Property, not method |
| C# Stack<T> | Count | int | Property, capitalized |
| Go slice | len(slice) | int | Built-in function |
| Rust Vec | .len() | usize | Unsigned size type |
| Swift Array | .count | Int | Property |
Key variations to note:
Method vs Property vs Function: Java uses size(), JavaScript uses .length, Python uses len(). Know your language's convention.
Naming: size, length, count, len — all mean the same thing in this context.
Return type: Some languages use unsigned integers (C++ size_t, Rust usize), others use signed integers (Java int). Unsigned prevents negative values but can cause overflow bugs in certain calculations.
Property vs Method: Properties (like JavaScript's .length) don't use parentheses. Calling arr.length() in JavaScript is an error.
Best practice: When implementing your own stack, use size() as a method for consistency with most standard libraries. It's more universally understood than alternatives.
If you're implementing a Stack class, match the conventions of your primary language. In Java, use size(). In JavaScript, you might use a length property or a length() method. The key is consistency within your codebase.
Size completes the stack interface, providing quantitative state information that isEmpty alone cannot provide. With push, pop, peek, isEmpty, and size, you have the complete toolkit for stack operations.
isEmpty() is size() === 0. Use isEmpty for boolean checks; use size when you need the count.size ≤ capacity. isFull() is size === capacity. Capacity management uses size extensively.Module Complete:
With this page, we've comprehensively covered all five standard stack operations:
These operations form the complete interface for interacting with stacks. In the next module, we'll see how to implement stacks using arrays, applying everything we've learned about these operations.
You now have comprehensive mastery of the complete stack interface. You understand what each operation does, how it's implemented in both array and linked list representations, its complexity characteristics, edge cases, and practical patterns. You're ready to implement full stack data structures and use them effectively in algorithms.