Stack and Queue Data Structures
Stack and Queue Data Structures
Introduction
Stack:
In the pushdown stacks only two operations are allowed: push the item into the stack and pop the item
out of the stack. A stack is a limited access data structure - elements can be added and removed from
the stack only at the top. push adds an item to the top of the stack, pop removes the item from the top.
A helpful analogy is to think of a stack of books; you can remove only the top book, also you can add
a new book on the top.
Queue:
An excellent example of a queue is a line of students in the food court of the UC. New additions to a
line made to the back of the queue, while removal (or serving) happens in the front. In the queue only
two operations are allowed enqueue and dequeue. Enqueue means to insert an item into the back of
the queue, dequeue means removing the front item. The picture demonstrates the FIFO access. The
difference between stacks and queues is in removing. In a stack we remove the item the most recently
added; in a queue, we remove the item the least recently added.
int main() {
int i;
struct stack mine;
// Set up the stack and push a couple items, then pop one. initialize(&mine);
push(&mine, 4);
push(&mine, 5);
printf("Popping %d\n", pop(&mine));
system("PAUSE");
return 0;
}
// returned.
// Add value to the top of the stack and adjust the value of the top.
stackPtr->items[stackPtr->top+1] = value;
(stackPtr->top)++; return
1;
}
// Post-condition: The value on the top of the stack is popped and returned.
int retval;
// Retrieve the item from the top of the stack, adjust the top and return
// the item.
retval = stackPtr->items[stackPtr->top];
(stackPtr->top)--;
return retval;
return stackPtr->items[stackPtr->top];
Queue C Code:
// Example of how to implement a queue with an array. #include
<stdio.h>
#define EMPTY -1
#define INIT_SIZE 10
int i;
enqueue(MyQueuePtr, 4);
// Enqueue one more item, then try several dequeues and one front. enqueue(MyQueuePtr,
2);
printf("Dequeue %d\n", dequeue(MyQueuePtr));
// Try enqueuing and dequeuing again to make sure that our previous
// Try lots of enqueues to test the dynamic capability of the queue. for
(i=30; i<60; i++)
enqueue(MyQueuePtr, i);
return 0;
// empty queue.
// Pre-condition: qPtr points to a valid struct queue and val is the value to
// Note: Right now, I don't know how to detect that the realloc failed, so 0
// does not get returned.
int i;
// Case where the queue is full, we must find more space before we enqueue. else {
// Allocate more space!
// Copy all of the items that are stored "before" front and copy them
// Enqueue the new item, now that there is space. We are guaranteed that
// More bookkeeping: The size of the queue as doubled and the number of
return 1;
// Empty case.
if (qPtr->numElements == 0)
return EMPTY;
// Store the value that should be returned.
retval = qPtr->elements[qPtr->front];
// Adjust the index to the front of the queue accordingly. qPtr-
>front = (qPtr->front + 1)% qPtr->queueSize;
Stack ADT:
A stack is an Abstract Data Type (ADT), commonly used in most programming languages. It is
named stack as it behaves like a real-world stack, for example – a deck of cards or a pile of plates, etc.
A real-world stack allows operations at one end only. For example, we can place or remove a card or
plate from the top of the stack only. Likewise, Stack ADT allows all data operations at one end only.
At any given time, we can only access the top element of a stack.
This feature makes it LIFO data structure. LIFO stands for Last-in-first-out. Here, the element which
is placed (inserted or added) last, is accessed first. In stack terminology, insertion operation is called
PUSH operation and removal operation is called POP operation.
Stack Representation
The following diagram depicts a stack and its operations −
A stack can be implemented by means of Array, Structure, Pointer, and Linked List. Stack can either
be a fixed size one or it may have a sense of dynamic resizing. Here, we are going to implement stack
using arrays, which makes it a fixed size stack implementation.
Basic Operations
Stack operations may involve initializing the stack, using it and then de-initializing it. Apart from
these basic stuffs, a stack is used for the following two primary operations −
push() − Pushing (storing) an element on the stack.
pop() − Removing (accessing) an element from the stack.
When data is PUSHed onto stack.
To use a stack efficiently, we need to check the status of stack as well. For the same purpose, the
following functionality is added to stacks −
peek() − get the top data element of the stack, without removing it.
isFull() − check if stack is full.
isEmpty() − check if stack is empty.
At all times, we maintain a pointer to the last PUSHed data on the stack. As this pointer always
represents the top of the stack, hence named top. The top pointer provides top value of the stack
without removing it.
First, we should learn about procedures to support stack functions –
peek()
Algorithm of peek() function −
begin procedure peek
return stack[top]
end procedure
int peek() {
return stack[top];
}
isfull()
Algorithm of isfull() function −
end procedure
bool isfull() {
if(top == MAXSIZE)
return true;
else
return false;
}
isempty()
Algorithm of isempty() function −
end procedure
Implementation of isempty() function in C programming language is slightly different. We initialize
top at -1, as the index in array starts from 0. So we check if the top is below zero or
-1 to determine if the stack is empty. Here's the code −
Example
bool isempty() {
if(top == -1)
return true;
else
return false;
}
Push Operation
The process of putting a new data element onto stack is known as a Push Operation. Push
operation involves a series of steps −
Step 1 − Checks if the stack is full.
Step 2 − If the stack is full, produces an error and exit.
Step 3 − If the stack is not full, increments top to point next empty space.
Step 4 − Adds data element to the stack location, where top is pointing.
Step 5 − Returns success.
If the linked list is used to implement the stack, then in step 3, we need to allocate space
dynamically.
if stack is full
return null
endif
top ← top + 1
stack[top] ← data
end procedure
Pop Operation
Accessing the content while removing it from the stack, is known as a Pop Operation. In an array
implementation of pop() operation, the data element is not actually removed, instead top is
decremented to a lower position in the stack to point to the next value. But in linked-list
implementation, pop() actually removes data element and deallocates memory space.
A Pop operation may involve the following steps −
Step 1 − Checks if the stack is empty.
Step 2 − If the stack is empty, produces an error and exit.
Step 3 − If the stack is not empty, accesses the data element at which top is pointing.
Step 4 − Decreases the value of top by 1.
Step 5 − Returns success.
if stack is empty
return null
endif
data ← stack[top]
top ← top - 1
return data
end procedure
Implementation of this algorithm in C, is as follows −
Example
if(!isempty()) {
data = stack[top];
top = top - 1;
return data;
} else {
printf("Could not retrieve data, Stack is empty.\n");
}
}
Queue ADT:
Queue is an abstract data structure, somewhat like Stacks. Unlike stacks, a queue is open at both its
ends. One end is always used to insert data (enqueue) and the other is used to remove data (dequeue).
Queue follows First-In-First-Out methodology, i.e., the data item stored first will be accessed first.
A real-world example of queue can be a single-lane one-way road, where the vehicle enters first, exits
first. More real-world examples can be seen as queues at the ticket windows and bus-stops.
Queue Representation
As we now understand that in queue, we access both ends for different reasons. The following
diagram given below tries to explain queue representation as data structure −
As in stacks, a queue can also be implemented using Arrays, Linked-lists, Pointers and Structures. For
the sake of simplicity, we shall implement queues using one-dimensional array.
Basic Operations
Queue operations may involve initializing or defining the queue, utilizing it, and then completely
erasing it from the memory. Here we shall try to understand the basic operations associated with
queues −
enqueue() − add (store) an item to the queue.
dequeue() − remove (access) an item from the queue.
Few more functions are required to make the above-mentioned queue operation efficient. These
are −
peek() − Gets the element at the front of the queue without removing it.
isfull() − Checks if the queue is full.
isempty() − Checks if the queue is empty.
In queue, we always dequeue (or access) data, pointed by front pointer and while enqueing (or
storing) data in the queue we take help of rear pointer.
Let's first learn about supportive functions of a queue −
peek()
This function helps to see the data at the front of the queue. The algorithm of peek() function is as
follows −
Algorithm
begin procedure peek
return queue[front]
end procedure
Implementation of peek() function in C programming language −
Example
int peek() {
return queue[front];
}
isfull()
As we are using single dimension array to implement queue, we just check for the rear pointer to
reach at MAXSIZE to determine that the queue is full. In case we maintain the queue in a circular
linked-list, the algorithm will differ. Algorithm of isfull() function −
Algorithm
end procedure
Implementation of isfull() function in C programming language −
Example
bool isfull() {
if(rear == MAXSIZE - 1)
return true;
else
return false;
}
isempty()
Algorithm of isempty() function −
Algorithm
end procedure
If the value of front is less than MIN or 0, it tells that the queue is not yet initialized, hence empty.
Example
bool isempty() {
if(front < 0 || front > rear)
return true;
else
return false;
}
Enqueue Operation
Queues maintain two data pointers, front and rear. Therefore, its operations are
comparatively difficult to implement than that of stacks.
The following steps should be taken to enqueue (insert) data into a queue −
Step 1 − Check if the queue is full.
Step 2 − If the queue is full, produce overflow error and exit.
Step 3 − If the queue is not full, increment rear pointer to point the next empty space.
Step 4 − Add data element to the queue location, where the rear is pointing.
Step 5 − return success.
Sometimes, we also check to see if a queue is initialized or not, to handle any unforeseen
situations.
Algorithm for enqueue operation
procedure enqueue(data)
if queue is full
return overflow
endif
rear ← rear + 1
queue[rear] ← data
return true
end procedure
Implementation of enqueue() in C programming language −
Example
int enqueue(int data)
if(isfull())
return 0;
rear = rear + 1;
queue[rear] = data;
return 1;
end procedure
Dequeue Operation
Accessing data from the queue is a process of two tasks − access the data where front is pointing and
remove the data after access. The following steps are taken to perform dequeue operation −
Step 1 − Check if the queue is empty.
Step 2 − If the queue is empty, produce underflow error and exit.
Step 3 − If the queue is not empty, access the data where front is pointing.
Step 4 − Increment front pointer to point to the next available data element.
Step 5 − Return success.
procedure dequeue
if queue is empty
return underflow
end if
data = queue[front]
front ← front + 1
return true
end procedure
return data;
}
Expression Parsing
The way to write arithmetic expression is known as a notation. An arithmetic expression can be
written in three different but equivalent notations, i.e., without changing the essence or output of an
expression. These notations are −
Infix Notation
As multiplication operation has precedence over addition, b * c will be evaluated first. A table of
operator precedence is provided later.
Associativity
Associativity describes the rule where operators with the same precedence appear in an expression.
For example, in expression a + b − c, both + and – have the same precedence, then which part of the
expression will be evaluated first, is determined by associativity of those operators. Here, both + and
− are left associative, so the expression will be evaluated as (a + b) − c.
Precedence and associativity determine the order of evaluation of an expression. Following is an
operator precedence and associativity table (highest to lowest) −
The above table shows the default behavior of operators. At any point of time in expression
evaluation, the order can be altered by using parenthesis. For example −
In a + b*c, the expression part b*c will be evaluated first, with multiplication as precedence over
addition. We here use parenthesis for a + b to be evaluated first, like (a + b)*c.
A Stack is a linear data structure which allows adding and removing of elements in a particular order.
New elements are added at the top of Stack. If we want to remove an element from the Stack, we can
only remove the top element from Stack. Since it allows insertion and deletion from only one end and
the element to be inserted last will be the element to be deleted first, hence it is called Last in First
Out data structure (LIFO).
What is Queue?
A Queue is also a linear data structure where insertions and deletions are performed from two
different ends. A new element is added from the rear of Queue and deletion of existing element occurs
from the front. Since we can access elements from both ends and the element inserted first will be the
one to be deleted first, hence Queue is called First in First Out data structure (FIFO).
Uses of Queue
CPU scheduling in Operating system uses Queue. The processes ready to execute and
the requests of CPU resources wait in a queue and the request is served on first come
first serve basis.
Data buffer - a physical memory storage which is used to temporarily store data while it
is being moved from one place to another is also implemented using Queue.
Implementation of Circular Queue
The operations are performed based on FIFO (First In First Out) principle. It is also called ‘Ring
Buffer’.
In a normal Queue, we can insert elements until queue becomes full. But once queue
becomes full, we cannot insert the next element even if there is a space in front of queue.
enQueue(value) This function is used to insert an element into the circular queue.
In a circular queue, the new element is always inserted at Rear position.
Applications:
Application of Stack:
Expression Evaluation:
While reading the expression from left to right, push the element in the stack if it is an operand. Pop
the two operands from the stack, if the element is an operator and then evaluate it. Push back the
result of the evaluation. Repeat it till the end of the expression.
Expression Conversion:
One of the applications of Stack is in the conversion of arithmetic expressions in high-level
programming languages into machine readable form. An expression can be represented in infix,
postfix and prefix and stack proves to be useful while converting one form to another.
Syntax Parsing:
Conversion from one form of the expression to another form needs a stack. Many compilers use a
stack for parsing the syntax of expressions, program blocks etc. before translating into low level code.
Parenthesis Checking:
One of the most important applications of stacks is to check if the parentheses are balanced in each
expression. The compiler generates an error if the parentheses are not matched.
String Reversal:
Reversing string is an operation of Stack by using it we can reverse any string.
Function Call:
The function call stack (often referred to just as the call stack or the stack) is responsible for
maintaining the local variables and parameters during function execution.
Expression Evaluation
Evaluate an expression represented by a String. The expression can contain parentheses, you can
assume parentheses are well-matched. For simplicity, you can assume only binary operations allowed
are +, -, *, and /. Arithmetic Expressions can be written in one of three forms:
Infix Notation: Operators are written between the operands they operate on, e.g., 3 +
4.
Prefix Notation: Operators are written before the operands, e.g., + 3 4.
Postfix Notation: Operators are written after operands.
Infix Expressions are harder for Computers to evaluate because of the additional work
needed to decide precedence. Infix notation is how expressions are written and recognized
by humans and, generally, input to programs. Given that they are harder to evaluate, they
are generally converted to one of the two remaining forms.
This algorithm takes as input an Infix Expression and produces a queue that has this
expression converted to postfix notation. The same algorithm can be modified so that it
outputs the result of the evaluation of expression instead of a queue. The trick is using two
stacks instead of one, one for operands, and one for operators.
Consider the following infix expression a convert into reverse polish notation using stack.
i) Postfix Expression:
Recursion
What is Recursion?
The process in which a function calls itself directly or indirectly is called recursion and the
corresponding function is called a recursive function. Using a recursive algorithm, certain problems
can be solved quite easily. Examples of such problems are Towers of Hanoi (TOH),
Inorder/Preorder/Postorder Tree Traversals, DFS of Graph, etc. A recursive function solves a
particular problem by calling a copy of itself and solving smaller subproblems of the original
problems. Many more recursive calls can be generated as and when required. It is essential to know
that we should provide a certain case in order to terminate this recursion process. So, we can say that
every time the function calls itself with a simpler version of the original problem.
Need of Recursion
Recursion is an amazing technique with the help of which we can reduce the length of our code and
make it easier to read and write. It has certain advantages over the iteration technique which will be
discussed later. A task that can be defined with its similar subtask, recursion is one of the best
solutions for it. For example, The Factorial of a number.
Properties of Recursion:
Algorithm: Steps
The algorithmic steps for implementing recursion in a function are as follows:
Step1 - Define a base case: Identify the simplest case for which the solution is known or
trivial. This is the stopping condition for the recursion, as it prevents the function from
infinitely calling itself.
Step2 - Define a recursive case: Define the problem in terms of smaller subproblems. Break
the problem down into smaller versions of itself and call the function recursively to solve
each subproblem.
Step3 - Ensure the recursion terminates: Make sure that the recursive function eventually
reaches the base case and does not enter an infinite loop.
Step4 - Combine the solutions: Combine the solutions of the subproblems to solve the
original problem.
A Mathematical Interpretation
Let us consider a problem that a programmer must determine the sum of first n natural numbers, there are
several ways of doing that, but the simplest approach is simply to add the numbers starting from 1 to n.
So, the function simply looks like this,
approach (1) – Simply adding one by
one. f(n) = 1 + 2 + 3 +… + n
int fact(int n)
{
if (n < = 1) // base case
return 1;
else
return n*fact(n-1);
}
In the above example, the base case for n < = 1 is defined and the larger value of a number can be
solved by converting to a smaller one till the base case is reached.
How is a particular problem solved using recursion?
The idea is to represent a problem in terms of one or more smaller problems and add one or more
base conditions that stop the recursion. For example, we compute factorial n if we know the factorial
of (n-1). The base case for factorial would be n = 0. We return 1 when n= 0.
int fact(int n)
{
// wrong base case (it may cause
// stack overflow). if
(n == 100)
return 1;
else
return n*fact(n-1);
}
If fact(10) is called, it will call fact(9), fact(8), fact(7), and so on but the number will never reach 100.
So, the base case is not reached. If the memory is exhausted by these functions on the stack, it will
cause a stack overflow error.
When printFun(3) is called from main(), memory is allocated to printFun(3) and a local variable
test is initialized to 3 and statement 1 to 4 are pushed on the stack as shown in below diagram.
It first prints ‘3’. In statement 2, printFun(2) is called and memory is allocated to printFun(2) and a
local variable test is initialized to 2 and statement 1 to 4 are pushed into the stack.
Similarly, printFun(2) calls printFun(1) and printFun(1) calls printFun(0). printFun(0) goes to
if statement and it return to printFun(1). The remaining statements of printFun(1)
are executed and it returns to printFun(2) and so on. In the output, values from 3 to 1 are printed and
then 1 to 3 are printed. The memory stack has been shown in below diagram.
Recursion VS Iteration
Every recursive call needs extra space in the Every iteration does not require any extra
3) stack memory. space.
Note that both recursive and iterative programs have the same problem-solving powers, i.e., every
recursive program can be written iteratively and vice versa is also true. The recursive program has
greater space requirements than the iterative program as all functions will remain in the stack until the
base case is reached. It also has greater time requirements because of function calls and returns
overhead.
Moreover, due to the smaller length of code, the codes are difficult to understand, and hence
extra care must be practiced while writing the code. The computer may run out of memory if the
recursive calls are not properly checked.
What are the advantages of recursive programming over iterative programming?
Recursion provides a clean and simple way to write code. Some problems are inherently recursive
like tree traversals, Tower of Hanoi, etc. For such problems, it is preferred to write recursive
code. We can write such codes also iteratively with the help of a stack data structure. For example,
refer Inorder Tree Traversal without Recursion, Iterative Tower of Hanoi.
Summary of Recursion:
There are two types of cases in recursion i.e., recursive case and a base case.
The base case is used to terminate the recursive function when the case turns out to be
true.
Each recursive call makes a new copy of that method in the stack memory.
Infinite recursion may lead to running out of stack memory.
Examples of Recursive algorithms: Merge Sort, Quick Sort, Tower of Hanoi,
Fibonacci Series, Factorial Problem, etc.
Priority Queue
A priority queue is a type of queue that arranges elements based on their priority values. Elements
with higher priority values are typically retrieved before elements with lower priority values.
In a priority queue, each element has a priority value associated with it. When you add an element to
the queue, it is inserted in a position based on its priority value. For example, if you add an element
with a high priority value to a priority queue, it may be inserted near the front of the queue, while
an element with a low priority value may be inserted near the back.
There are several ways to implement a priority queue, including using an array, linked list, heap, or
binary search tree. Each method has its own advantages and disadvantages, and the best choice will
depend on the specific needs of your application.
Priority queues are often used in real-time systems, where the order in which elements are processed
can have significant consequences. They are also used in algorithms to improve their efficiencies,
such as Dijkstra’s algorithm for finding the shortest path in a graph and the A* search algorithm for
pathfinding.
Binary Heap is generally preferred for priority queue implementation because heaps provide
better performance compared to arrays or LinkedList. Considering the properties of a heap, The
entry with the largest key is on the top and can be removed immediately. It will, however, take
time O(log n) to restore the heap property for the remaining keys. However if another entry is to be
inserted immediately, then some of this time may be combined with the O(log n) time needed to insert
the new entry. Thus, the representation of a priority queue as a heap proves advantageous for large n,
since it is represented efficiently in contiguous storage and is guaranteed to require only logarithmic
time for both insertions and deletions. Operations on Binary Heap are as follows:
insert(p): Inserts a new element with priority p.
extractMax(): Extracts an element with maximum priority.
remove(i): Removes an element pointed by an iterator i.
getMax(): Returns an element with maximum priority.
changePriority(i, p): Changes the priority of an element pointed by i to p.
A Self-Balancing Binary Search Tree like AVL Tree, Red-Black Tree, etc. can also be used to
implement a priority queue. Operations like peek(), insert() and delete() can be performed using
BST.