DS_UNIT _3
DS_UNIT _3
UNIT – 3
STACKS
Definition:
Stack Data Structure is a linear data structure that follows LIFO (Last In First Out) Principle , so the last
element inserted is the first to be popped out. In this article, we will cover all the basics of Stack,
Operations on Stack, its implementation, advantages, disadvantages which will help you solve all the
problems based on Stack.
Stack is a linear data structure based on LIFO(Last In First Out) principle in which the insertion of a new
element and removal of an existing element takes place at the same end represented as the top of the
stack.
To implement the stack, it is required to maintain the pointer to the top of the stack , which is the last
element to be inserted because we can access the elements only on the top of the stack.
This strategy states that the element that is inserted last will come out first. You can take a pile of plates
kept on top of each other as a real-life example. The plate which we put last is on the top and since we
remove the plate that is at the top, we can say that the plate that was put last comes out first.
Stack follows LIFO (Last In First Out) Principle so the element which is pushed last is popped first.
Dynamic Size Stack : A dynamic size stack can grow or shrink dynamically. When the stack is full, it
automatically increases its size to accommodate the new element, and when the stack is empty, it
decreases its size. This type of stack is implemented using a linked list, as it allows for easy resizing of
the stack.
In order to make manipulations in a stack, there are certain operations provided to us.
Adds an item to the stack. If the stack is full, then it is said to be an Overflow condition.
• Before pushing the element to the stack, we check if the stack is full .
• If the stack is full (top == capacity-1) , then Stack Overflows and we cannot insert the element to
the stack.
• Otherwise, we increment the value of top by 1 (top = top + 1) and the new value is inserted at top
position .
• The elements can be pushed into the stack till we reach the capacity of the stack.
Removes an item from the stack. The items are popped in the reversed order in which they are pushed.
If the stack is empty, then it is said to be an Underflow condition.
• Before popping the element from the stack, we check if the stack is empty .
• If the stack is empty (top == -1), then Stack Underflows and we cannot remove any element from
the stack.
• Otherwise, we store the value at top, decrement the value of top by 1 (top = top – 1) and return
the stored top value.
• Before returning the top element from the stack, we check if the stack is empty.
• If the stack is empty (top == -1), we simply print “Stack is empty”.
• Otherwise, we return the element stored at index = top .
The basic operations that can be performed on a stack include push, pop, and peek. There are two ways
to implement a stack –
• Using Array
In an array-based implementation, the push operation is implemented by incrementing the index of the
top element and storing the new element at that index. The pop operation is implemented by returning
the value stored at the top index and then decrementing the index of the top element.
In a linked list-based implementation, the push operation is implemented by creating a new node with
the new element and setting the next pointer of the current top node to the new node. The pop operation
is implemented by setting the next pointer of the current top node to the next node and returning the
value of the current top node.
}
// Function to add an item to stack. It increases top by 1
void push(struct Stack* stack, int item)
{
if (isFull(stack))
return;
stack->array[++stack->top] = item;
printf("%d pushed to stack\n", item);
}
// Function to remove an item from stack. It decreases top by 1
int pop(struct Stack* stack)
{
if (isEmpty(stack))
return INT_MIN;
return stack->array[stack->top--];
}
// Function to return the top from stack without removing it
int peek(struct Stack* stack)
{
if (isEmpty(stack))
return INT_MIN;
return stack->array[stack->top];
}
// Driver program to test above functions
int main()
{
struct Stack* stack = createStack(100);
push(stack, 10);
push(stack, 20);
push(stack, 30);
printf("%d popped from stack\n", pop(stack));
return 0;
}
Output
10 pushed into stack
20 pushed into stack
30 pushed into stack
30 Popped from stack
Top element is : 20
Elements present in stack : 20 10
• It is not dynamic i.e., it doesn’t grow and shrink depending on needs at runtime. [But in case of
dynamic sized arrays like vector in C++, list in Python, ArrayList in Java, stacks can grow and
shrink with array implementation as well].
• The total size of the stack must be defined beforehand.
return popped;
}
int main()
{
struct StackNode* root = NULL;
push(&root, 10);
push(&root, 20);
push(&root, 30);
Output
10 pushed to stack
20 pushed to stack
30 pushed to stack
30 popped from stack
Top element is 20
Elements present in stack : 20 10
• Simplicity: Stacks are a simple and easy-to-understand data structure, making them suitable
for a wide range of applications.
• Efficiency: Push and pop operations on a stack can be performed in constant time (O(1)) ,
providing efficient access to data.
• Last-in, First-out (LIFO): Stacks follow the LIFO principle, ensuring that the last element added
to the stack is the first one removed. This behavior is useful in many scenarios, such as function
calls and expression evaluation.
• Limited memory usage: Stacks only need to store the elements that have been pushed onto
them, making them memory-efficient compared to other data structures.
• Limited access: Elements in a stack can only be accessed from the top, making it difficult to
retrieve or modify elements in the middle of the stack.
• Potential for overflow: If more elements are pushed onto a stack than it can hold, an overflow
error will occur, resulting in a loss of data.
• Not suitable for random access: Stack s do not allow for random access to elements, making
them unsuitable for applications where elements need to be accessed in a specific order.
• Limited capacity: Stacks have a fixed capacity, which can be a limitation if the number of
elements that need to be stored is unknown or highly variable.
#include <stdio.h>
#include <stdlib.h>
#define MAX 4
int stack_arr[MAX];
int top = -1;
int isFull ()
{
if (top == MAX - 1)
return 1; //indicates the end of the function
else
return 0;
}
int isEmpty ()
{
if (top == -1)
return 1;
else
return 0;
}
int pop ()
{
int value;
if(isEmpty ())
{
printf("\nStack underflow\n");
exit (1) ;
}
value = stack_arr[top];
top = top - 1;
return value;
}
int peek()
{
if(isEmpty ())
{
printf("Stack underflow\n");
exit (1);
}
return stack_arr[top];
}
void print ()
{
int i;
if (top == -1)
{
printf("Stack underflow\n");
return;
}
printf("\nThe elements in the stack are..");
for(i=top; i>=0; i -- )
printf("%d ", stack_arr[i]);
printf("\n");
}
int main ()
{
int choice, data;
while (1)
{
printf("\n");
printf("1. Push\n");
printf("2. Pop\n");
printf("3. Print the top element\n");
printf("4. Print all the elements of the stack\n");
printf("5. Quit\n");
printf ("Please enter your choice: ") ;
scanf ("%d", &choice);
switch (choice)
{
case 1:
printf ("Enter the element to be pushed: ");
scanf ("%d", &data) ;
push (data);
break;
case 2:
data = pop () ;
printf ("\nDeleted element is %d\n", data);
break;
case 3:
printf ("\nThe topmost element of the stack is %d\n", peek() );
break;
case 4:
print ();
break;
case 5:
exit (1);
default:
printf("\nWrong choice\n");
}
}
return 0;
}
#include <stdio.h>
#include <stdlib.h>
struct node
{
int data;
struct node* link;
} *top = NULL;
int isEmpty()
{
if (top == NULL)
return 1;
else
return 0;
}
newNode->link = top;
top = newNode;
}
int pop()
{
struct node* temp;
int val;
if(isEmpty())
{
printf("Stack Underflow.");
exit(1);
}
temp = top;
val = temp->data;
top = top->link;
free (temp) ;
temp = NULL;
return val;
}
int peek()
{
if(isEmpty())
{
printf("Stack Underflow.");
exit(1);
}
return top->data;
}
void print()
{
struct node* temp;
temp = top;
if(isEmpty())
{
printf("Stack Underflow.");
exit(1);
}
printf("The stack elements are: ") ;
while(temp)
{
printf("%d ", temp->data);
temp = temp->link;
}
printf("\n");
}
int main()
{
int choice, data;
while(1)
{
printf("\n");
printf("1. Push\n");
printf("2. Pop\n");
printf("3. Print the top element\n");
printf("4. Print all the elements of the stack\n");
printf("5. Quit\n");
printf ("Please enter your choice: ") ;
scanf ("%d", &choice) ;
switch(choice)
{
case 1:
printf("Enter the element to be pushed: ") ;
scanf("%d", &data);
push(data) ;
break;
case 2:
data = pop() ;
printf("Deleted element is %d\n", data);
break;
case 3:
printf("The topmost element of the stack is %d\n", peek() );
break;
case 4:
print();
break;
case 5:
exit(1) ;
default:
printf("Wrong choice\n");
}
}
return 0;
}
QUEUES
1. A queue can be defined as an ordered list which enables insert operations to be performed at one
end called REAR and delete operations to be performed at another end called FRONT.
2. Queue is referred to as First in First Out list.
3. For example, people waiting in line for a rail ticket form a queue.
Applications of Queue
Because queue performs actions on first in first out basis which is quite fair for the ordering of actions.
There are various applications of queues discussed as below.
1. Queues are widely used as waiting lists for a single shared resource like printer, disk, CPU.
2. Queues are used in asynchronous transfer of data (where data is not being transferred at the
same rate between two processes) for eg. pipes, file IO, sockets.
3. Queues are used as buffers in most of the applications like MP3 media player, CD player, etc.
4. Queues are used to maintain the play list in media players to add and remove the songs from
the play-list.
5. Queues are used in operating systems for handling interruptions.
Complexity
Types of Queues
What is a Queue?
Queue is the data structure that is like the queue in the real world. A queue is a data structure in which
whatever comes first will go out first, and it follows the FIFO (First-In-First-Out) policy. The queue can
also be defined as the list or collection in which the insertion is done from one end known as the rear
end or the tail of the queue, whereas the deletion is done from another end known as the front end or
the head of the queue.
The real-world example of a queue is the ticket queue outside a cinema hall, where the person who
enters first in the queue gets the ticket first, and the last person enters the queue gets the ticket at last.
A similar approach is followed in the queue in data structure.
Types of Queues
There are four different types of queues that are listed as follows -
In Linear Queue, an insertion takes place from one end while the deletion occurs from another end. The
end at which the insertion takes place is known as the rear end, and the end at which the deletion takes
place is known as the front end. It strictly follows the FIFO rule.
The major drawback of using a linear Queue is that insertion is done only from the rear end. If the first
three elements are deleted from the Queue, we cannot insert more elements even though the space is
available in a Linear Queue. In this case, the linear Queue shows the overflow condition as the rear is
pointing to the last element of the Queue.
Circular Queue
In Circular Queue, all the nodes are represented as circular. It is similar to the linear Queue except that
the last element of the queue is connected to the first element. It is also known as Ring Buffer, as all
the ends are connected to another end. The representation of circular queue is shown in the image
below -
The drawback that occurs in a linear queue is overcome by using the circular queue. If the empty space
is available in a circular queue, the new element can be added in an empty space by simply
incrementing the value of rear. The main advantage of using the circular queue is better memory
utilization.
Priority Queue
It is a special type of queue in which the elements are arranged based on the priority. It is a special type
of queue data structure in which every element has a priority associated with it. Suppose some
elements occur with the same priority, they will be arranged according to the FIFO principle. The
representation of priority queue is shown in the below image -
Insertion in priority queue takes place based on the arrival, while deletion in the priority queue occurs
based on the priority. Priority queues are mainly used to implement the CPU scheduling algorithms.
There are two types of priority queue that are discussed as follows -
o Ascending priority queue - In ascending priority queue, elements can be inserted in arbitrary
order, but only smallest can be deleted first. Suppose an array with elements 7, 5, and 3 in the
same order, so, insertion can be done with the same sequence, but the order of deleting the
elements is 3, 5, 7.
o Descending priority queue - In descending priority queue, elements can be inserted in arbitrary
order, but only the largest element can be deleted first. Suppose an array with elements 7, 3,
and 5 in the same order, so, insertion can be done with the same sequence, but the order of
deleting the elements is 7, 5, 3.
In Deque or Double Ended Queue, insertion and deletion can be done from both ends of the queue
either from the front or rear. It means that we can insert and delete elements from both front and rear
ends of the queue. Deque can be used as a palindrome checker, which means that if we read the string
from both ends, then the string would be the same.
Deque can be used both as stack and queue as it allows the insertion and deletion operations on both
ends. Deque can be considered as stack because stack follows the LIFO (Last In First Out) principle in
which insertion and deletion both can be performed only from one end. And in Deque, it is possible to
perform both insertion and deletion from one end, and Deque does not follow the FIFO principle.
The representation of the deque is shown in the below image -
o Output restricted deque - As the name implies, in output restricted queue, deletion operation
can be performed at only one end, while insertion can be performed from both ends.
The fundamental operations that can be performed in a queue are listed as follows -
• Enqueue: The Enqueue operation is used to insert the element at the rear end of the queue. It
returns void.
• Dequeue: It performs the deletion from the front-end of the queue. It also returns the element
which has been removed from the front-end. It returns an integer value.
• Peek: This is the third operation that returns the element, which is pointed by the front pointer
in the queue but does not delete it.
• Queue overflow (isfull): It shows the overflow condition when the queue is completely full.
• Queue underflow (isempty): It shows the underflow condition when the Queue is empty, i.e.,
no elements are in the Queue.
We can easily represent queues by using linear arrays. There are two variables, i.e. front and rear, that
are implemented in the case of every queue. Front and rear variables point to the position from where
insertions and deletions are performed in a queue. Initially, the value of front and queue is -1 which
represents an empty queue. Array representation of a queue containing 5 elements along with the
respective values of front and rear, is shown in the following figure.
The above figure shows the queue of characters forming the English word "HELLO". Since, No deletion
is performed in the queue till now, therefore the value of front remains -1. However, the value of rear
increases by one every time an insertion is performed in the queue. After inserting an element into the
queue shown in the above figure, the queue will look like following. The value of rear will become 5 while
the value of front remains same.
After deleting an element, the value of front will increase from -1 to 0. however, the queue will look
something like following.
Check if the queue is already full by comparing rear to max - 1. if so, then return an overflow error.
If the item is to be inserted as the first element in the list, in that case set the value of front and rear to
0 and insert the element at the rear end.
Otherwise keep increasing the value of rear and insert each element one by one having rear as the
index.
Algorithm
o Step 1: IF REAR = MAX - 1
Write OVERFLOW
Go to step
[END OF IF]
o Step 2: IF FRONT = -1 and REAR = -1
SET FRONT = REAR = 0
ELSE
SET REAR = REAR + 1
[END OF IF]
o Step 3: Set QUEUE[REAR] = NUM
o Step 4: EXIT
If, the value of front is -1 or value of front is greater than rear , write an underflow message and exit.
Otherwise, keep increasing the value of front and return the item stored at the front end of the queue at
each time.
Algorithm
o Step 1: IF FRONT = -1 or FRONT > REAR
Write UNDERFLOW
ELSE
SET VAL = QUEUE[FRONT]
SET FRONT = FRONT + 1
[END OF IF]
o Step 2: EXIT
void dequeue()
{
if (front == -1 && rear == -1)
{
printf("Queue is empty!\n");
}
else if(front == rear)
{
front = rear = -1;
}
else
{
printf("Deleted %d\n", queue[front]);
front++;
}
}
#include <stdio.h>
#define MAX 3
int queue[MAX];
int front = -1;
int rear = -1;
void dequeue()
{
if (front == -1 && rear == -1)
{
printf("Queue is empty!\n");
}
else if(front == rear)
{
front = rear = -1;
}
else
{
printf("Deleted %d\n", queue[front]);
front++;
}
}
void display()
{
if (front == -1)
{
printf("Queue is empty!\n");
}
else
{
printf("Queue elements are: ");
for (int i = front; i <= rear; i++)
{
printf("%d ", queue[i]);
}
printf("\n");
}
}
void peek()
{
if (front == -1)
{
printf("Queue is empty!\n");
}
else
{
printf("The peek is %d\n", queue[front]);
}
}
int main()
{
int choice, value;
while (1)
{
printf("\nMenu:\n");
printf("1. Enqueue\n");
printf("2. Dequeue\n");
printf("3. Display\n");
printf("4. Peek\n");
printf("5. Exit\n");
printf("Enter your choice: ");
scanf("%d", &choice);
switch (choice)
{
case 1:
printf("Enter the value to be inserted: ");
scanf("%d", &value);
enqueue(value);
break;
case 2:
dequeue();
break;
case 3:
display();
break;
case 4:
peek();
break;
case 5:
return 0;
default:
printf("Invalid choice! Please try again.\n");
}
}
return 0;
}
Although, the technique of creating a queue is easy, but there are some drawbacks of using this
technique to implement a queue.
o Memory wastage : The space of the array, which is used to store queue elements, can never be
reused to store the elements of that queue because the elements can only be inserted at front
end and the value of front might be so high so that, all the space before that, can never be filled.
The above figure shows how the memory space is wasted in the array representation of queue. In the
above figure, a queue of size 10 having 3 elements, is shown. The value of the front variable is 5,
therefore, we can not reinsert the values in the place of already deleted element before the position of
front. That much space of the array is wasted and can not be used in the future (for this queue).
o Deciding the array size
On of the most common problem with array implementation is the size of the array which requires to
be declared in advance. Due to the fact that, the queue can be extended at runtime depending upon
the problem, the extension in the array size is a time taking process and almost impossible to be
performed at runtime since a lot of reallocations take place. Due to this reason, we can declare the
array large enough so that we can store queue elements as enough as possible but the main problem
with this declaration is that, most of the array slots (nearly half) can never be reused. It will again lead
to memory wastage.
The storage requirement of linked representation of a queue with n elements is o(n) while the time
requirement for operations is o(1).
In a linked queue, each node of the queue consists of two parts i.e. data part and the link part. Each
element of the queue points to its immediate next element in the memory.
In the linked queue, there are two pointers maintained in the memory i.e. front pointer and rear pointer.
The front pointer contains the address of the starting element of the queue while the rear pointer
contains the address of the last element of the queue.
Insertion and deletions are performed at rear and front end respectively. If front and rear both are NULL,
it indicates that the queue is empty.
There are two basic operations which can be implemented on the linked queues. The operations are
Insertion and Deletion.
Insert operation
The insert operation append the queue by adding an element to the end of the queue. The new element
will be the last element of the queue.
Firstly, allocate the memory for the new node ptr by using the following statement.
1. Ptr = (struct node *) malloc (sizeof(struct node));
There can be the two scenario of inserting this new node ptr into the linked queue.
In the first scenario, we insert element into an empty queue. In this case, the condition front =
NULL becomes true. Now, the new element will be added as the only element of the queue and the
next pointer of front and rear pointer both, will point to NULL.
ptr -> data = item;
if(front == NULL)
{
front = ptr;
rear = ptr;
front -> next = NULL;
rear -> next = NULL;
}
In the second case, the queue contains more than one element. The condition front = NULL becomes
false. In this scenario, we need to update the end pointer rear so that the next pointer of rear will point
to the new node ptr. Since, this is a linked queue, hence we also need to make the rear pointer point to
the newly added node ptr. We also need to make the next pointer of rear point to NULL.
rear -> next = ptr;
rear = ptr;
rear->next = NULL;
In this way, the element is inserted into the queue. The algorithm and the C implementation is given as
follows.
Algorithm
o Step 1: Allocate the space for the new node PTR
o Step 2: SET PTR -> DATA = VAL
o Step 3: IF FRONT = NULL
SET FRONT = REAR = PTR
SET FRONT -> NEXT = REAR -> NEXT = NULL
ELSE
SET REAR -> NEXT = PTR
SET REAR = PTR
SET REAR -> NEXT = NULL
[END OF IF]
o Step 4: END
Deletion
Deletion operation removes the element that is first inserted among all the queue elements. Firstly, we
need to check either the list is empty or not. The condition front == NULL becomes true if the list is
empty, in this case , we simply write underflow on the console and make exit.
Otherwise, we will delete the element that is pointed by the pointer front. For this purpose, copy the
node pointed by the front pointer into the pointer ptr. Now, shift the front pointer, point to its next node
and free the node pointed by the node ptr. This is done by using the following statements.
ptr = front;
front = front -> next;
free(ptr);
The algorithm and C function is given as follows.
Algorithm
o Step 1: IF FRONT = NULL
Write " Underflow "
Go to Step 5
[END OF IF]
o Step 2: SET PTR = FRONT
o Step 3: SET FRONT = FRONT -> NEXT
o Step 4: FREE PTR
o Step 5: END
void dequeue()
{
if (front == NULL)
{
printf("Queue is empty!\n");
return;
}
struct Node* temp = front;
front = front->next;
if (front == NULL)
{
rear = NULL;
}
printf("Deleted %d\n", temp->data);
free(temp);
}
#include <stdio.h>
#include <stdlib.h>
struct Node
{
int data;
struct Node* next;
};
void dequeue()
{
if (front == NULL)
{
printf("Queue is empty!\n");
return;
}
struct Node* temp = front;
front = front->next;
if (front == NULL)
{
rear = NULL;
}
printf("Deleted %d\n", temp->data);
free(temp);
}
void display()
{
if (front == NULL)
{
printf("Queue is empty!\n");
return;
}
struct Node* temp = front;
printf("Queue elements are: ");
while (temp != NULL)
{
printf("%d ", temp->data);
temp = temp->next;
}
printf("\n");
}
void peek()
{
if (front == NULL)
{
printf("Queue is empty!\n");
return;
}
else
{
printf("Peek is %d\n", front->data);
}
}
int main()
{
enqueue(10);
enqueue(20);
enqueue(30);
peek();
display();
dequeue();
peek();
display();
return 0;
}