Stacks and Queues are widely used these days to solve a multitude of problems like evaluation of RPN expressions or checking if the parentheses in a string expression are balanced or not. Practically, we see use of stacks in a web browser’s back/forward functionality or undo/redo functionality seen today. Queues are used to implement caches and packet buffers in network routers to store packets in sequence in case of congestion.
As we see going further, these data structures are the basic building blocks used to implement advanced data structures like trees, graphs, tries etc.
What is a Stack?
A stack is a collection of memory spaces designed to store previous tasks in a Last In First Out (LIFO) order.
To better understand, we can assume a stack to be like a storage bin to store things, which is only open on one side. You can insert or remove items from only that side. Items inserted first, can only be removed at last.
A real-life example would be a pile of books placed in a vertical fashion. To get to a book in the middle, first we have to remove all books sitting on top of the book we want. This fashion of organizing things is called Last In First Out (LIFO) order.
Stack can be used to implement undo/redo functionality, webpage back/forward functionality, depth first search algorithm, evaluating Reverse Polish Notatiom (RPN) expressions (also called postfix expressions) etc.
A typical stack looks like this:
How does a stack work?
A stack must implement the following functionality:
Functionality
Description
push(data)
insert data on top of stack
pop()
fetch and remove thr element on top of stack
is_full()
return true if stack is at its capacity
is_empty()
return true if no element in stack
peek()
returns the value on top of stack, but does not remove it
Entire functionality of a stack is implemented using push(data) and pop() methods.
The following pictures show the transition:
From an empty stack to a full stack –> push(data)
From a full stack to an empty stack –> pop()
When inserting an element into the stack, the variable that stores the position of the top element would be pointing to the number below it. So it needs to be updated to the new element every time we insert a value. Similarly, it will need to updated when removing an element from the stack.
It’s a good practice to update the value of the top variable before inserting or removing an element, so that it always points to the correct value.
Stack Implementation
Modern languages like Python or Java already have a stack implementation inbuilt, but we can implement it ourselves to extend its functionaloty for our use.
Stacks can be implemented using linked lists and arrays. Performance wise, it is not yet concluded which implementation is better. Array implementation is better because it takes less space and we don’t need to store additional pointers for linked lists.
We need an array to store the elements, a variable to store its max capacity and a variable to store the top index.
We instantiate a stack instance by creating an array of size max_capacity and setting top index to -1.
For push(data), we increment the top index and insert data to array[top].
For pop(), we return the value from array[top] and decrement the top index.
For is_empty(), we check if the value to top index is -1?
For is_full(), we check it the top index is max_capacity - 1?
For peek() (or top()), we simply return the current value at array[top] without removing the element.
NOTES:
suppress_printing flag is to simply avoid printing the element value to console every time it is pushed or popped from the stack.
prettify() returns a string representation of the stack.
print_stack() prints the string representation of the stack, as returned by prettify().
Validating Stack Implementation
Output
What is a Queue?
A queue is also a linear data structure like stack. It stores data elements sequentially in a First In First Out (FIFO) order.
To better understand, a queue can be visualized as a pipe which is open from both ends. Elements enter from the “back” and exit from the “front”. Elements entered first, will leave first.
A real life example would be a line of people waiting to be served at a booth. If a new person comes, he joins the queue at the beack and when the person in front is served, they will exit the queue from the front.
Queues can be used to implement a cache, or a packet buffer in a network router which will store packets in order in case of network congestion. Generally, queues are used in scenarios where we want to prioritize something over another, or when a resource (like compute cycle time or memory) is shared among multiple tasks.
A typical Queue looks like this:
How does a Queue work?
A queue needs to implement the following functionality:
Functionality
Description
enqueue(data)
insert data at the end of queue
dequeue()
fetch and remove the element on front of queue
is_full()
return true if queue is at its capacity
is_empty()
return true if no element in queue
peek()
returns the value on front of queue, but does not remove it
Entire functionality of a queue is implemented using enqueue(data) and dequeue() methods.
The following pictures show the transition:
From an empty queue to a full queue –> enqueue(data)
From a full queue to an empty queue –> dequeue()
Types of Queues
There 2 common types of queues which cover a wide range of problems:
Linear Queues
Circular Queues
Priority Queues
Linear Queues:
The Queue discussed above is a Linear queue.
Circular Queues:
A Circular queue is circular in structure, meaning both ends are connected to form a circle. Initially, “front” and “back” point to the same node and eventually move apart as new elements are added.
Generally, circular queues are used to simulate objects and to implement event handling.
This is how it looks like:
Priority Queues:
A Priority queue keeps its elements sorted at a given time based on a specific order. Most prioritzed element stays at the front of the queue and the least stays at the back.
Priority Queues are used in an Operating System to determine which programs take priority over others.
Queue Implementation (Linear Queue)
Queues can be implemened using an array, linked list or a stack. Array implementation is the easiest and is discussed below.
We need an array to store the elements, a variable to store its max capacity and a variable to store its current size.
We instantiate a queue instance by declaring an array to store elements, setting the current_size = 0 and setting the max_capacity to a variable.
For enqueue(data), we simply append the data to the elements array and update the current_size = len(elements array).
For dequeue(), we return and delete the value from array[0] update and the current_size = len(elements array).
For is_empty(), we check if current_size = 0?
For is_full(), we check is current_size = max_capacity?
For peek() (or top()), we simply return the current value at array[0] without removing the element.
NOTES:
suppress_printing flag is to simply avoid printing the element value to console every time it is enqueued or dequeued from the queue.
prettify() returns a string representation of the queue.
print_queue() prints the string representation of the queue, as returned by prettify().
Validating Queue Implementation
Output
Challenges
Challenges 1 - 10
Challenge 1: Generate Binary Numbers from 1 to n using a Queue
Steps:
Initialize a queue with capacity n + 1 -> for every dequeue, we enqueue 2 times.
Insert 1 in queue.
temp = dequeue() -> temp = 1.
append temp in “result” array.
append 0 to temp => 10
append 1 to temp => 11
Enqueue 10 and 11 in queue.
Repeat 2. and 3. until n.
“result” array now contains binary numbers until n.
Output
Time Complexity: O(n)
Challenge 2: Implement Two Stacks using one Array
Using Stacks on opposite ends:
This implementation uses space in an efficient way.
max_capacity defines the number of total elements both stacks can accomodate at a given time.\
At initialization:
top index of stack 1 is set to the 0th index - 1 (or -1)
top index of stack 2 is set to the last index + 1 (or max_capacity).
Both stacks grown or shrink in opposity directions.
We keep track of overflow by checking the gap in top indices of both stacks.
Implementation:
set max_capacity from input, initialize an elements array of size max_capacity, set top1 = -1, set top2 = max_capacity
is_full() returns true if abs(top2 - top 1) = 1, meaning top1 and top2 are next to each other.
is_empty() returns true if top1 = -1 and top2 = max_capacity.
push1(data):
checks if top1 < top2 - 1 (to ensure there is a 1 unit gap between top1 and top2)
increments top1 by 1
inserts data at elements array[top1]
push2(data):
checks if top2 > top1 + 1 (to ensure there is a 1 unit gap between top1 and top2)
decrements top2 by 1
inserts data at elements array[top2]
pop1():
checks if top1 > -1 (to ensure stack1 is not empty. is_full() will not work as array may be full by stack2 elements)
set result = elements array[top1]
set elements array[top1] = None
decrement top1
return result
pop2():
checks if top2 < max_capacity (to ensure stack2 is not empty. is_full() will not work as array may be full by stack1 elements)
set result = elements array[top2]
set elements array[top2] = None
increment top1
return result
peek1() and peek2() return elements at array[top1] and array[top2] respectively.
NOTES:
suppress_printing flag is to simply avoid printing the element values to console every time they are pushed or popped from the 2 stacks.
prettify() returns a string representation of the 2 stacks.
print_stack() prints the string representation of the 2 stacks, as returned by prettify().
Validating Two Stacks
Output
Time Complexity: O(n)
Challenge 3: Reversing First k Elements of a Queue
Using a stack to reverse elements:
Since we are reversing k elements, initialize a stack with k capacity to hold k elements
This algorithm consists of 3 steps:
dequeue k elements from queue and push to stack
pop() elements from stack and enqueue in queue (in reverse order)
now dequeue elements from the front and enqueue again at the back
resulting array will contain first k elements in reverse order
Here is an illustration:
Output
Time Complexity: O(n)
Challenge 4: Implement a Queue using Stack
Solution 1: Using 2 Stacks (Optimizing only dequeue() operation)
Implementation uses 2 stacks, one for storing the queue elements and 2nd as a buffer to be used at the time of dequeue.
enqueue(data) simply pushes the data on top of stack 1
dequeue():
checks if stack 1 is empty and returns False if no elements
stack 1 is emptied into stack 2
This operation brings the first value in the queue to the top of stack 2
Now it is popped and saved for returning to the caller
Finally stack 2 is emptied back into stack 1 and the queue returns to normal.
result is returned to the caller
peek() follows the same logic as dequeue(), but instead of popping top of stack2, we simply peek() top of stack2 before emptying stack2 into stack1.
NOTES:
suppress_printing flag is to simply avoid printing the element values to console every time they are pushed or popped from the 2 stacks or enqueued/dequeued from the queue
prettify() returns a string representation of the queue.
print_queue() prints the string representation of the queue, as returned by prettify().
Validating Solution 1: “Queue using Stack” Implementation
Output
Time Complexity - enqueue(data) = O(1), dequeue() = O(n)
Solution 2: Using 2 Stacks (Optimizing enqueue() and dequeue() operations)
Implementation uses 2 stacks, one for storing the queue elements and 2nd as a buffer to be used at the time of dequeue
enqueue(data) simply pushes the data on top of stack 1
dequeue()
checks if stack 1 and stack 2 are empty and returns False if no elements
IF stack 2 is empty, stack 1 is emptied into stack 2
This operation reverses the order of stored elements and the top element in stack 2 represents the front of the queue
Elements in stack 2 can be popped for dequeuing until it goes empty.
and IF stack2 is NOT emptied yet, it still contains the front element in the queue.
Finally, result is popped off of stack 2 and returned to the caller
peek() follows the same logic as dequeue(), but instead of popping top of stack2, we simply peek() top of stack2 before emptying stack2 into stack1.
NOTES:
suppress_printing flag is to simply avoid printing the element values to console every time they are pushed or popped from the 2 stacks or enqueued/dequeued from the queue
prettify() returns a string representation of the queue.
print_queue() prints the string representation of the queue, as returned by prettify().
Validating Solution 2: “Queue using Stack” Implementation
Output
Time Complexity - enqueue(data) = O(1), dequeue() < O(n) –> amortized = O(n), because worst-case = O(n)
Challenge 5: Sort the Values in a Stack
This solution uses a second stack to hold resulting sorted stack. (See online video if confusing)
Until input stack is not empty, we pop the top value and compare it with the top value of the second stack
if value > top of stack 2, we insert the popped value in stack 2
else:
while popped value < top of stack 2, we keep pushing top of stack 2 to stack 1
Finally when stack 2 is empty we push the popped value and start over again
The output will be a sorted stack
NOTE - This can also be done by recursion
Output
Time Complexity = O(n^2) –> Inner and Outer loops traverse all n elements
Challenge 6: Evaluate PostFix Epressions using Stacks (RPN)
PostFix or RPN notations have the operator between two numbers written after the concerned numbers as an epxression (like 912+-4+2* = 20)
So stacks are perfect to evaluate such strings:
Traverse the expression character by character.
If character is numerical, push the character to a temp_stack.
If character is an operator, pop last 2 characters from stack and perform the arithmetic operatio and push the result back to the stack.
After the expression is traversed, the temp_stack will contain the result of given expression.
Output
Time Complexity = O(n)
Challenge 7: Next Greater Element using Stack
The most optimal solution is to use a stack for storing elements yet to be processed. The key is to order the stack in ascending order until the next element under comparison is bigger than the top element on the stack.
Steps:
For each element in input, check:
if stack is empty, push the element to stack as stack is empty and move to the next iteration
if stack does have a node, check:
if current element is smaller than top of the stack, push element on top of stack
else:
iterate the stack popping all elements one by one
appending current element to the result array for each pop
finally after all nodes in stack are popped, push the current element on top of the stack for next iteration
Now that all array elements are processed, we are left with a stack of nodes which don’t have the next greater element.
pop() the elements one by one and append -1 to the result array for each popped element.
Resulting array will now contain -1 for each element which does not have a NGE.
Return the result array
Output
Time Complexity = O(n) -> Significant Improvement over brute force - O(n^2)
Challenge 8: Solve a Celebrity Problem using a Stack
Definitions:
input_data[row][col] = 1 means row knows column
input_data[row][col] = 0 means row does not know col
A celebrity does not know anyone, but everyone knows a celebrity.
Assume there is at max a single celebrity at the party
Terminology:
Row index represents a person identifier eg. row 1 represents the guest no. 2 at the party,
columns represent if the guest knows another guest whose row index is same as column index
Meaning in above example:
Guest no. 1 knows guest no. 2 and guest no. 3, but not guest no. 4
Guest no. 4 only knows guest no. 2 and guest no. 3, but not guest no. 1
Guest no. 3 however does not know anybody.
–> This means guest no. 3 is a celebrity as everyone else knows guest no. 3,
but guest no. 3 does not know anyone
Solution:
Push all row indices (guest ids) in a stack
Pop guest ids from stack until stack is empty, 2 at a time
If party[x][y] 1, means guest x knows guest y
push next guest y to stack and discard x
else:
push guest x back to stack, ignoring y (compare x with y+1 in the next iteration)
Finally the stack only contains 1 guest it means everyone knows that guest
Now assume that guest is a celebrity and check if the guest knows anyone else at the party.
If the guest knows anyone:
means they are not a celebrity
return -1 (no celebrity)
else:
guest is a celebrity
return guest id
Output
Time Complexity = O(n)
Challenge 9: Check for Balanced Parentheses using Stack
We can check if the parentheses are balanced by pushing all opening parentheses on to a stack and then popping elements one by one, comparing
next character with the popped element in a loop.
Iterate the input expression character by character.
If current character is an opening parentheses - “(“ or “[“ or “{“.
Push character to stack.
Else:
If stack is empty already, means number of opening parentheses in the stack were less then number of closing parentheses in the string, means string is unbalanced.
Else:
Pop the last element from stack and compare with current character.
If the popped element is NOT the opening parentheses of the current character, it means the string is not balanced.
So, return False.
Output
Time Complexity = O(n)
Challenge 10: Create Stack where min() gives minimum in O(1)
Defintion:
Min Stack implementation returns the minimum element of a stack in O(1), instead of O(n) which will be the case if we searched for the minimum value every time.
Solution:
This is achieved by keeping track of the minimum value is a separate stack in parallel to the main stack.
push(data) ->
Push data to main_stack
If min stack is empty:
Push the data to min stack and return
if data <= top of min_stack
Push the data to min stack and return
pop() ->
If min_stack is not empty, check:
If top of min stack is same as top of main stack:
Pop the top of min_stack
Else:
Pop and return the top of main stack.
min() ->
If min_stack is not empty:
Return top of min_stack.
Validating Stack with O(1) min() Implementation
Output
Time Complexity for min() = O(1)
project showcase + professional profile | Read the fine print. README