Stacks and Queues

Introduction
As Android developers, our ability to manipulate data efficiently often determines the performance and scalability of our applications. While modern development frameworks provide numerous abstractions, a profound understanding of fundamental data structures remains essential for crafting optimal solutions to complex problems.
In this technical exploration, I'll dissect two critical data structures that form the backbone of countless algorithms: Stacks and Queues. Beyond merely defining their operations, we'll examine their internal mechanics, implementation variations, and practical applications with production-ready Java code examples.
1. Stack: The LIFO Powerhouse
1.1 Conceptual Framework
A stack operates on the Last-In-First-Out (LIFO) principle, or equivalently, First-In-Last-Out (FILO). This behavior mirrors common real-world scenarios, such as a stack of plates at a wedding buffet. You can only access the topmost plate; accessing plates beneath requires removing those above first.
1.2 Core Operations
Every stack implementation must support these fundamental operations:
push(E item): Adds an element to the top of the stack
pop(): Removes and returns the element at the top
peek(): Returns but doesn't remove the top element
empty(): Returns true if the stack contains no elements
1.3 Implementation
Java provides a Stack
class in the java.util
package, though the Deque
interface offers a more versatile alternative for implementing stack behavior:
import java.util.Stack;
import java.util.ArrayDeque;
import java.util.Deque;
public class StackOperations {
public static void main(String[] args) {
// Using the Stack class (legacy approach)
Stack<Integer> legacyStack = new Stack<>();
// Using Deque as a stack (preferred modern approach)
Deque<Integer> stack = new ArrayDeque<>();
// Pushing elements
stack.push(10);
stack.push(20);
stack.push(30);
System.out.println("Stack: " + stack); // Output: Stack: [30, 20, 10]
// Peek operation
System.out.println("Top element: " + stack.peek()); // Output: Top element: 30
// Pop operation
System.out.println("Removed element: " + stack.pop()); // Output: Removed element: 30
// Check if empty
System.out.println("Is stack empty? " + stack.isEmpty()); // Output: Is stack empty? false
// Current state
System.out.println("Current stack: " + stack); // Output: Current stack: [20, 10]
}
}
1.4 Implementation Considerations
While java.util.Stack
extends Vector
and is thread-safe, it suffers from performance penalties due to synchronization overhead. For most applications, using ArrayDeque
as a stack provides better performance characteristics:
// Preferred implementation with superior performance
Deque<Integer> stack = new ArrayDeque<>();
// Legacy implementation (avoid unless thread safety is required)
Stack<Integer> stack = new Stack<>();
1.5 Exception Handling
Robust stack implementations should account for potential exceptions:
public class CustomStack<E> {
private Deque<E> stack = new ArrayDeque<>();
public void push(E item) {
stack.push(item);
}
public E pop() throws StackException {
if (stack.isEmpty()) {
throw new StackException("Cannot pop from an empty stack");
}
return stack.pop();
}
public E peek() throws StackException {
if (stack.isEmpty()) {
throw new StackException("Cannot peek an empty stack");
}
return stack.peek();
}
public boolean isEmpty() {
return stack.isEmpty();
}
public int size() {
return stack.size();
}
}
class StackException extends Exception {
public StackException(String message) {
super(message);
}
}
2. Queue: The FIFO Foundation
2.1 Conceptual Framework
A queue implements the First-In-First-Out (FIFO) principle, or equivalently, Last-In-Last-Out (LILO). This behavior mimics real-world queuing systems, like customers waiting at a McDonald's counter, where the first person to arrive is the first to be served.
2.2 Core Operations
Queues support these essential operations:
add(E e)/offer(E e): Inserts an element at the tail of the queue
remove()/poll(): Removes and returns the element at the head
peek(): Returns but doesn't remove the element at the head
isEmpty(): Returns true if the queue contains no elements
2.3 Java Implementation
Java provides the Queue
interface with multiple implementations. The LinkedList
implementation is commonly used because it optimizes the specific operations required by a queue:
import java.util.LinkedList;
import java.util.Queue;
public class QueueOperations {
public static void main(String[] args) {
// Initialize queue using LinkedList
Queue<String> queue = new LinkedList<>();
// Adding elements
queue.add("Task 1");
queue.offer("Task 2"); // Preferred when capacity is restricted
queue.add("Task 3");
System.out.println("Queue: " + queue); // Output: Queue: [Task 1, Task 2, Task 3]
// Peek operation
System.out.println("Head element: " + queue.peek()); // Output: Head element: Task 1
// Remove operation
System.out.println("Removed element: " + queue.remove()); // Output: Removed element: Task 1
// Poll operation (safer alternative to remove)
System.out.println("Polled element: " + queue.poll()); // Output: Polled element: Task 2
// Current state
System.out.println("Current queue: " + queue); // Output: Current queue: [Task 3]
// Is empty check
System.out.println("Is queue empty? " + queue.isEmpty()); // Output: Is queue empty? false
}
}
2.4 Why LinkedList for Queue Implementation?
LinkedList
is the preferred implementation for Queue because:
It provides O(1) time complexity for adding elements at the end and removing from the front
The internal doubly-linked structure enables efficient insertion and deletion at both ends
Unlike array-based implementations, it doesn't require resizing or shifting elements
It natively supports all operations defined in the Queue interface
2.5 Understanding Method Differences
Queue operations come in two forms:
Throwing methods:
add(E e)
: ThrowsIllegalStateException
if capacity is restrictedremove()
: ThrowsNoSuchElementException
if queue is emptyelement()
: ThrowsNoSuchElementException
if queue is empty
Non-throwing methods:
offer(E e)
: Returns false if addition is not possiblepoll()
: Returns null if queue is emptypeek()
: Returns null if queue is empty
3. Deque: The Versatile Double-Ended Queue
3.1 Conceptual Framework
A Deque (Double-Ended Queue) represents a linear collection that supports element insertion and removal at both ends, combining functionality of both stacks and queues.
3.2 Core Operations
Deques support an extended set of operations:
Front operations:
addFirst(E e)/offerFirst(E e)
: Inserts at the frontremoveFirst()/pollFirst()
: Removes from the frontgetFirst()/peekFirst()
: Retrieves from the front
Rear operations:
addLast(E e)/offerLast(E e)
: Inserts at the rearremoveLast()/pollLast()
: Removes from the reargetLast()/peekLast()
: Retrieves from the rear
3.3 ArrayDeque Implementation
ArrayDeque
provides a resizable array implementation of the Deque interface with no capacity restrictions:
import java.util.ArrayDeque;
import java.util.Deque;
public class DequeOperations {
public static void main(String[] args) {
// Initialize ArrayDeque
Deque<Integer> deque = new ArrayDeque<>();
// Front operations
deque.addFirst(10);
deque.offerFirst(20);
// Rear operations
deque.addLast(30);
deque.offerLast(40);
System.out.println("Deque: " + deque); // Output: Deque: [20, 10, 30, 40]
// Access operations
System.out.println("First element: " + deque.getFirst()); // Output: First element: 20
System.out.println("Last element: " + deque.getLast()); // Output: Last element: 40
// Removal operations
System.out.println("Remove first: " + deque.removeFirst()); // Output: Remove first: 20
System.out.println("Poll last: " + deque.pollLast()); // Output: Poll last: 40
// Current state
System.out.println("Current deque: " + deque); // Output: Current deque: [10, 30]
}
}
3.4 ArrayDeque vs. LinkedList
ArrayDeque
offers several advantages over LinkedList
for most deque applications:
Memory efficiency: Consumes less memory per element
Locality of reference: Array-based structure improves cache performance
Faster operations: Generally outperforms
LinkedList
for most operationsNo allocation overhead: Doesn't create separate node objects for each element
4. Circular Queue: The Efficient Cyclic Buffer
4.1 Conceptual Framework
A Circular Queue is a linear data structure where operations are performed based on FIFO principle, and the last position is connected to the first position to form a circle. It efficiently utilizes memory by reusing spaces.
4.2 Core Operations
enqueue(E e): Adds an element to the rear
dequeue(): Removes an element from the front
peek(): Returns the front element without removing it
isFull(): Checks if the circular queue is full
isEmpty(): Checks if the circular queue is empty
4.3 Java Implementation
Implementing a circular queue requires careful management of front and rear pointers:
public class CircularQueue<E> {
private E[] elements;
private int front;
private int rear;
private int size;
private int capacity;
@SuppressWarnings("unchecked")
public CircularQueue(int capacity) {
this.capacity = capacity;
elements = (E[]) new Object[capacity];
front = 0;
rear = -1;
size = 0;
}
public boolean enqueue(E item) {
if (isFull()) {
return false;
}
rear = (rear + 1) % capacity;
elements[rear] = item;
size++;
return true;
}
public E dequeue() {
if (isEmpty()) {
return null;
}
E item = elements[front];
elements[front] = null; // Help with garbage collection
front = (front + 1) % capacity;
size--;
return item;
}
public E peek() {
if (isEmpty()) {
return null;
}
return elements[front];
}
public boolean isEmpty() {
return size == 0;
}
public boolean isFull() {
return size == capacity;
}
public int size() {
return size;
}
}
4.4 Using the Modulo Formula
The modulo operation (index % capacity)
is crucial for circular queue implementation because:
It enables wrap-around behavior when indices exceed array bounds
It transforms a linear array into a conceptual circular structure
It eliminates the need to physically shift elements when dequeuing
Example usage:
public class CircularQueueDemo {
public static void main(String[] args) {
CircularQueue<Integer> circularQueue = new CircularQueue<>(5);
// Enqueue operations
circularQueue.enqueue(10);
circularQueue.enqueue(20);
circularQueue.enqueue(30);
circularQueue.enqueue(40);
System.out.println("Front element: " + circularQueue.peek()); // Output: Front element: 10
// Dequeue operations
System.out.println("Dequeued: " + circularQueue.dequeue()); // Output: Dequeued: 10
System.out.println("Dequeued: " + circularQueue.dequeue()); // Output: Dequeued: 20
// Adding more elements (reusing the freed space)
circularQueue.enqueue(50);
circularQueue.enqueue(60);
System.out.println("Front element after wrap-around: " + circularQueue.peek());
// Output: Front element after wrap-around: 30
}
}
Top LeetCode Problems I've Solved
#1863: Sum of All Subset XOR Totals
#20: Valid Parentheses
GitHub Repository
For the complete implementation of these data structures along with additional optimizations and test cases, visit my GitHub repository:
https://github.com/QuantumPineapple68/DSA
Conclusion
Understanding stacks and queues at this fundamental level provides essential tools for solving complex algorithmic challenges. These data structures not only form the foundation for more advanced concepts but also offer elegant solutions to numerous real-world problems.
By mastering their implementations, variations, and application strategies, I've been able to solve numerous challenging problems across Android system architecture, background processing, and algorithmic optimizations in production applications.
What truly separates proficient developers from exceptional ones is the ability to select the right data structure for specific problem domains and implement it with attention to performance characteristics and edge cases.
Subscribe to my newsletter
Read articles from Nachiket directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
