Welcome to the first lesson of our new module on Stacks and Queues! In the previous module, we concluded our deep dive into linked lists by mastering complex pointer manipulations. Now, we shift our focus from a specific data structure implementation (the linked list) to a more abstract concept: Abstract Data Types (ADTs).
This lesson introduces two of the most fundamental ADTs in computer science: the Stack and the Queue. We will explore their defining principles—LIFO (Last-In, First-Out) for stacks and FIFO (First-In, First-Out) for queues. To truly understand how they work, we will implement them from the ground up using both arrays and linked lists. This foundational knowledge is essential before we move on to using Java's built-in collections to solve interview problems.
1. The Stack: Last-In, First-Out (LIFO)
A stack is a linear data structure that follows the Last-In, First-Out (LIFO) principle. This means the last element added to the stack will be the first one to be removed. A simple and effective analogy is a stack of plates: you place a new plate on top and you also take a plate from the top. The plate at the bottom of the stack was the first one placed, but it will be the last one you can access.
The core operations of a stack are:
push(element): Adds an element to the top of the stack.pop(): Removes and returns the element from the top of the stack.top()(orpeek()): Returns the top element without removing it.isEmpty(): Checks if the stack is empty.size(): Returns the number of elements in the stack.
The following video from the take U forward channel provides an excellent conceptual overview of both stacks and queues. For now, let's focus on the stack portion.
L1. Introduction to Stack and Queue | Implementation using Data Structures
This video introduces the core concepts and operations for stacks.
Watch the segment on stacks, from the introduction to its operations. Pay close attention to the visual demonstration of push, pop, and top to see the LIFO principle in action.
Now, let's explore how to build a stack.
1.1. Implementing a Stack with an Array
An array is a natural choice for implementing a stack. The main idea is to use an array to store the elements and an integer variable, often called top, to keep track of the index of the topmost element.
- Initialization: We declare an array of a fixed size and initialize
topto -1, signifying an empty stack. push(x): We first check for "stack overflow" (i.e., iftopis already at the last index). If there's space, we incrementtopand then place the new element atstackArray[top].pop(): We first check for "stack underflow" (i.e., if the stack is empty,top == -1). If not empty, we return the element atstackArray[top]and then decrementtop.top(): We check for underflow. If not empty, we simply returnstackArray[top].isEmpty(): We returntrueiftop == -1, andfalseotherwise.
Let's see this implemented.
Implement Stack using Array - Tutorial
This takeUforward article provides a clear algorithm and a complete Java implementation for an array-based stack.
First, read the Algorithm section to solidify your understanding of the logic. Then, carefully examine the Java code for the ArrayStack class. Note how the topIndex variable manages the state of the stack. Finally, review the Complexity Analysis.
The main drawback of this approach is its static nature. The size of the stack must be defined beforehand, which can lead to wasted memory or, worse, a stack overflow if the size is underestimated.
1.2. Implementing a Stack with a Linked List
To overcome the fixed-size limitation of arrays, we can use a linked list. This implementation is dynamic, meaning it only uses as much memory as it needs. This should feel familiar from our previous module. The idea is to treat the head of the linked list as the top of the stack.
- Initialization: We create a
head(ortop) node pointer and initialize it tonull. push(x): This is equivalent to inserting a new node at the beginning of the linked list. We create a new node, point itsnextreference to the currenthead, and then updateheadto be this new node.pop(): This is equivalent to deleting the head of the list. We retrieve the data from thehead, updateheadtohead.next, and return the data. We must handle the case where the list is empty.top(): We simply return the data stored in theheadnode, after checking it's notnull.
The following video explains this dynamic implementation.
L1. Introduction to Stack and Queue | Implementation using Data Structures
This video segment demonstrates how to implement a stack using a linked list, highlighting its dynamic nature.
Watch the portion explaining the linked list implementation. The visualization clearly shows how adding and removing nodes from the head of the list perfectly models the LIFO behavior.
This linked-list approach provides O(1) time complexity for all core operations, just like the array implementation, but with the added benefit of dynamic sizing.
2. The Queue: First-In, First-Out (FIFO)
A queue is another linear data structure that, in contrast to a stack, follows the First-In, First-Out (FIFO) principle. The first element added to the queue is the first one to be removed. Think of a line at a ticket counter: the first person to get in line is the first person to be served.
The core operations are similar to a stack's but with different behavior:
enqueue(element)(orpush): Adds an element to the back (rear) of the queue.dequeue()(orpop): Removes and returns the element from the front of the queue.peek()(orfront): Returns the front element without removing it.isEmpty(): Checks if the queue is empty.size(): Returns the number of elements.
Let's return to the introductory video to see the FIFO principle explained.
L1. Introduction to Stack and Queue | Implementation using Data Structures
This segment explains the core concept and operations of a queue.
Now, watch the section covering the queue data structure. Contrast its FIFO behavior with the LIFO behavior of the stack you just learned about.
2.1. Implementing a Queue with a Circular Array
If we implement a queue with a standard array and two pointers, front and rear, we run into a problem. When we dequeue an element, we increment front. Over time, front moves forward, leaving unused, "wasted" space at the beginning of the array. To fix this, we use a circular array.
A circular array allows the pointers to "wrap around" to the beginning when they reach the end of the array. This is elegantly achieved using the modulo operator (%).
- Initialization: We declare an array of fixed size and pointers
startandend, and acurrSizevariable, all initialized to indicate an empty queue (e.g., pointers at -1, size at 0). enqueue(x): We check if the queue is full (currSize == maxSize). If not, we calculate the newendindex usingend = (end + 1) % maxSize, place the element atarr[end], and incrementcurrSize.dequeue(): We check if the queue is empty. If not, we retrieve the element fromarr[start], calculate the newstartindex usingstart = (start + 1) % maxSize, and decrementcurrSize.
The implementation details, especially handling the edge cases of a completely empty or full queue, are important.
Implement Queue Using Array - Tutorial
This article details how to build an efficient queue using a circular array.
Read the Algorithm section to understand the role of the start, end, and size variables and the use of modular arithmetic. Then, study the Java code to see a complete implementation. Finally, check the Complexity Analysis, which confirms that all operations are O(1).
2.2. Implementing a Queue with a Linked List
A linked list provides a very natural and dynamic way to implement a queue. To achieve O(1) time for both enqueue and dequeue operations, we need to maintain pointers to both the front (head) and rear (tail) of the list.
- Initialization: We create
frontandrearnode pointers, both initialized tonull. enqueue(x): We create a new node. We link the currentrearnode'snextpointer to this new node, and then updaterearto point to the new node. If the queue was empty, we must also setfrontto this new node.dequeue(): We retrieve the data from thefrontnode, and then updatefrontto befront.next. If this was the last element, we must also updatereartonull.
The following image helps visualize the difference between using a contiguous block of memory (arrays) and non-contiguous memory blocks (linked lists).
Let's watch the final segment of the video for the linked list implementation of a queue.
L1. Introduction to Stack and Queue | Implementation using Data Structures
This part of the video covers the linked list implementation of a queue, which is a very common interview topic.
Watch the section on implementing a queue using a linked list. Notice how maintaining both start (front) and end (rear) pointers is crucial for O(1) performance.
Conclusion
In this lesson, we established a solid foundation by building stacks and queues from scratch. This theoretical understanding is what separates a good programmer from a great one and is frequently tested in interviews.
Key Takeaways:
- Stack (LIFO): The last element in is the first one out. Implemented with an array and a
toppointer, or more dynamically with a linked list by adding/removing at thehead. - Queue (FIFO): The first element in is the first one out. Implemented efficiently with a circular array (using
front,rear, and modular arithmetic), or dynamically with a linked list by usingfrontandrearpointers to manage the ends. - Complexity: For all four implementations (Stack/Queue with Array/Linked List), the core operations (
push/pop,enqueue/dequeue) achieve O(1) time complexity. The primary trade-off is between the static, fixed-size nature of arrays and the dynamic, flexible nature of linked lists.
In our next lesson, we will put this knowledge to practical use. We'll transition from implementing these structures ourselves to using Java's powerful, pre-built Stack, Queue, and Deque classes to solve the classic "Valid Parentheses" problem.