← Back to DSA Course | Chapter 5: Stacks | Lesson 2 of 5

Stack Implementation

Array Stack

A stack can be built on top of a plain array by keeping a top index that tracks the position of the most recently pushed element, incrementing it on push and decrementing it on pop.

Example: Array Stack

#include <iostream>
using namespace std;
int main() {
    int arr[5]; int top = -1;
    arr[++top] = 10; arr[++top] = 20;
    cout << "top index=" << top << ", value=" << arr[top] << endl;
    top--;
    cout << "after pop, top index=" << top << endl;
    return 0;
}
public class Main {
    public static void main(String[] args) {
        int[] arr = new int[5]; int top = -1;
        arr[++top] = 10; arr[++top] = 20;
        System.out.println("top index=" + top + ", value=" + arr[top]);
        top--;
        System.out.println("after pop, top index=" + top);
    }
}
arr = [0] * 5
top = -1
top += 1; arr[top] = 10
top += 1; arr[top] = 20
print("top index=", top, ", value=", arr[top])
top -= 1
print("after pop, top index=", top)
#include <stdio.h>
int main() {
    int arr[5]; int top = -1;
    arr[++top] = 10; arr[++top] = 20;
    printf("top index=%d, value=%d\n", top, arr[top]);
    top--;
    printf("after pop, top index=%d\n", top);
    return 0;
}

Stack Class

Wrapping that array and top index inside a class keeps the internal details private and exposes only clean push, pop, and peek methods, so code using the stack doesn't need to know how it's implemented underneath.

Example: Stack Class

#include <iostream>
using namespace std;
class Stack {
    int arr[5]; int top = -1;
public:
    void push(int x) { arr[++top] = x; }
    int pop() { return arr[top--]; }
    int peek() { return arr[top]; }
};
int main() {
    Stack s;
    s.push(5); s.push(15);
    cout << "peek: " << s.peek() << endl;
    return 0;
}
class Stack {
    int[] arr = new int[5]; int top = -1;
    void push(int x) { arr[++top] = x; }
    int pop() { return arr[top--]; }
    int peek() { return arr[top]; }
}
public class Main {
    public static void main(String[] args) {
        Stack s = new Stack();
        s.push(5); s.push(15);
        System.out.println("peek: " + s.peek());
    }
}
class Stack:
    def __init__(self):
        self.items = []

    def push(self, x):
        self.items.append(x)

    def pop(self):
        return self.items.pop()

    def peek(self):
        return self.items[-1]

s = Stack()
s.push(5); s.push(15)
print("peek:", s.peek())
#include <stdio.h>
typedef struct { int arr[5]; int top; } Stack;
void push(Stack *s, int x) { s->arr[++s->top] = x; }
int peek(Stack *s) { return s->arr[s->top]; }
int main() {
    Stack s = { .top = -1 };
    push(&s, 5); push(&s, 15);
    printf("peek: %d\n", peek(&s));
    return 0;
}

Overflow and Underflow

An array-backed stack has a fixed capacity, so pushing onto a full stack causes overflow, and popping from an empty one causes underflow. Both need explicit checks to avoid crashing or returning garbage data.

Example: Overflow and Underflow

#include <iostream>
using namespace std;
int main() {
    int arr[2]; int top = -1; int capacity = 2;
    arr[++top] = 1; arr[++top] = 2;
    if (top == capacity - 1) cout << "overflow: stack is full, cannot push" << endl;
    top = -1;
    if (top == -1) cout << "underflow: stack is empty, cannot pop" << endl;
    return 0;
}
public class Main {
    public static void main(String[] args) {
        int[] arr = new int[2]; int top = -1; int capacity = 2;
        arr[++top] = 1; arr[++top] = 2;
        if (top == capacity - 1) System.out.println("overflow: stack is full, cannot push");
        top = -1;
        if (top == -1) System.out.println("underflow: stack is empty, cannot pop");
    }
}
arr = [0, 0]
top = -1
capacity = 2
top += 1; arr[top] = 1
top += 1; arr[top] = 2
if top == capacity - 1:
    print("overflow: stack is full, cannot push")
top = -1
if top == -1:
    print("underflow: stack is empty, cannot pop")
#include <stdio.h>
int main() {
    int arr[2]; int top = -1; int capacity = 2;
    arr[++top] = 1; arr[++top] = 2;
    if (top == capacity - 1) printf("overflow: stack is full, cannot push\n");
    top = -1;
    if (top == -1) printf("underflow: stack is empty, cannot pop\n");
    return 0;
}

Linked List Stack

A stack can also be built on a linked list instead of an array, pushing and popping at the head, which naturally has no fixed size limit and never overflows, at the cost of a little extra memory per element for the node pointers.

Example: Linked List Stack

#include <iostream>
using namespace std;
struct Node { int data; Node* next; };
int main() {
    Node* top = nullptr;
    Node* n1 = new Node{1, top}; top = n1;
    Node* n2 = new Node{2, top}; top = n2;
    cout << "pushed onto head, top=" << top->data << endl;
    top = top->next;
    cout << "popped from head, new top=" << top->data << endl;
    return 0;
}
class Node { int data; Node next; Node(int d, Node n) { data = d; next = n; } }
public class Main {
    public static void main(String[] args) {
        Node top = null;
        top = new Node(1, top);
        top = new Node(2, top);
        System.out.println("pushed onto head, top=" + top.data);
        top = top.next;
        System.out.println("popped from head, new top=" + top.data);
    }
}
class Node:
    def __init__(self, data, next_node):
        self.data = data
        self.next = next_node

top = None
top = Node(1, top)
top = Node(2, top)
print("pushed onto head, top=", top.data)
top = top.next
print("popped from head, new top=", top.data)
#include <stdio.h>
#include <stdlib.h>
struct Node { int data; struct Node *next; };
int main() {
    struct Node *top = NULL;
    struct Node *n1 = malloc(sizeof(struct Node)); n1->data = 1; n1->next = top; top = n1;
    struct Node *n2 = malloc(sizeof(struct Node)); n2->data = 2; n2->next = top; top = n2;
    printf("pushed onto head, top=%d\n", top->data);
    top = top->next;
    printf("popped from head, new top=%d\n", top->data);
    return 0;
}

Implementation Practice

Whether backed by an array or a linked list, both implementations expose identical push/pop/peek behavior; the choice comes down to whether you need a fixed, predictable memory footprint or unbounded, dynamic growth.

Example: Implementation Practice

#include <iostream>
#include <stack>
using namespace std;
int main() {
    stack<int> arrayLike;
    arrayLike.push(1); arrayLike.push(2);
    cout << "same push/pop/peek interface regardless of array or linked-list backing: " << arrayLike.top() << endl;
    return 0;
}
import java.util.Stack;
public class Main {
    public static void main(String[] args) {
        Stack<Integer> arrayLike = new Stack<>();
        arrayLike.push(1); arrayLike.push(2);
        System.out.println("same push/pop/peek interface regardless of backing: " + arrayLike.peek());
    }
}
array_like = []
array_like.append(1)
array_like.append(2)
print("same push/pop/peek interface regardless of backing:", array_like[-1])
#include <stdio.h>
int main() {
    int s[10], top = -1;
    s[++top] = 1; s[++top] = 2;
    printf("same push/pop/peek interface regardless of backing: %d\n", s[top]);
    return 0;
}
🔒

Chapter Quiz — Complete all 5 topics to unlock

0/5 topics done

Complete these topics first:

Login to run this code

C/C++/Java/PHP execution requires a free account. Your code is saved — you'll land right back in the editor after logging in.