Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
0% found this document useful (0 votes)
66 views

Data Structure Unit 2 Notes

Uploaded by

Sanjay Kewat
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
66 views

Data Structure Unit 2 Notes

Uploaded by

Sanjay Kewat
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 79

Data structure unit 2 notes

What is Abstract Data Type?


An a high-level Abstract Data Type (ADT) is a programming concept that defines
view of a data structure, without specifying the implementation details. In
other words, it is a blueprint for creating a data structure that defines the
behavior and interface of the structure, without specifying how it is
implemented.
An ADT in the data structure can be thought of as a set of operations that can
be performed on a set of values. This set of operations actually defines the
behavior of the data structure, and they are used to manipulate the data in a
way that suits the needs of the program.
ADTs are often used to abstract away the complexity of a data structure and to
provide a simple and intuitive interface for accessing and manipulating the
data. This makes it easier for programmers to reason about the data structure,
and to use it correctly in their programs.

Examples of abstract data type in data structures are List, Stack, Queue, etc.
Abstract Data Type Model

List ADT
Lists are linear data structures that hold data in a non-continuous structure. The list is made up of
data storage containers known as "nodes." These nodes are linked to one another, which means that
each node contains the address of another block. All of the nodes are thus connected to one another
via these links. You can discover more about lists in this article: Linked List Data Structure.

Some of the most essential operations defined in List ADT are listed below.

• front(): returns the value of the node present at the front of the list.

• back(): returns the value of the node present at the back of the list.

• push_front(int val): creates a pointer with value = val and keeps this pointer to the front of
the linked list.

• push_back(int val): creates a pointer with value = val and keeps this pointer to the back of
the linked list.

• pop_front(): removes the front node from the list.

• pop_back(): removes the last node from the list.

• empty(): returns true if the list is empty, otherwise returns false.

• size(): returns the number of nodes that are present in the list

Stack ADT
A stack is a linear data structure that only allows data to be accessed
from the top. It simply has two operations: push (to insert data to the
top of the stack) and pop (to remove data from the stack). (used to
remove data from the stack top).
Some of the most essential operations defined in Stack ADT are listed below.

• top(): returns the value of the node present at the top of the stack.

• push(int val): creates a node with value = val and puts it at the stack top.

• pop(): removes the node from the top of the stack.

• empty(): returns true if the stack is empty, otherwise returns false.

• size(): returns the number of nodes that are present in the stack.

Queue ADT

A queue is a linear data structure that allows data to be accessed from both ends. There are two
main operations in the queue: push (this operation inserts data to the back of the queue) and pop
(this operation is used to remove data from the front of the queue).

Some of the most essential operations defined in Queue ADT are listed below.

• front(): returns the value of the node present at the front of the queue.
• back(): returns the value of the node present at the back of the queue.

• push(int val): creates a node with value = val and puts it at the front of the queue.

• pop(): removes the node from the rear of the queue.

• empty(): returns true if the queue is empty, otherwise returns false.

• size(): returns the number of nodes that are present in the queue

Advantages of ADT in Data Structures

The advantages of ADT in Data Structures are:

• Provides abstraction, which simplifies the complexity of the data structure and allows users
to focus on the functionality.

• Enhances program modularity by allowing the data structure implementation to be separate


from the rest of the program.

• Enables code reusability as the same data structure can be used in multiple programs with
the same interface.

• Promotes the concept of data hiding by encapsulating data and operations into a single unit,
which enhances security and control over the data.

• Supports polymorphism, which allows the same interface to be used with different
underlying data structures, providing flexibility and adaptability to changing requirements.

Disadvantages of ADT in Data Structures

There are some potential disadvantages of ADT in Data Structures:

• Overhead: Using ADTs may result in additional overhead due to the need for abstraction and
encapsulation.

• Limited control: ADTs can limit the level of control that a programmer has over the data
structure, which can be a disadvantage in certain scenarios.

• Performance impact: Depending on the specific implementation, the performance of an ADT


may be lower than that of a custom data structure designed for a specific application.

Implementation of Stack Data Structure:


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

• Using Linked List


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.

Implementation of Stack Data Structure using Array:

/* C++ program to implement basic stack

operations */

#include<iostream>

using namespace std;

#define MAX 1000

class Stack {

int top;

public:

int a[MAX]; // Maximum size of Stack

Stack() { top = -1; }

bool push(int x);

int pop();

int peek();

bool isEmpty();

};

bool Stack::push(int x)

if (top >= (MAX - 1)) {


cout << "Stack Overflow";

return false;

else {

a[++top] = x;

cout << x << " pushed into stack\n";

return true;

int Stack::pop()

if (top < 0) {

cout << "Stack Underflow";

return 0;

else {

int x = a[top--];

return x;

int Stack::peek()

if (top < 0) {

cout << "Stack is Empty";

return 0;

else {

int x = a[top];

return x;

}
}

bool Stack::isEmpty()

return (top < 0);

// Driver program to test above functions

int main()

class Stack s;

s.push(10);

s.push(20);

s.push(30);

cout << s.pop() << " Popped from stack\n";

//print top element of stack after popping

cout << "Top element is : " << s.peek() << endl;

//print all elements in stack :

cout <<"Elements present in stack : ";

while(!s.isEmpty())

// print top element in stack

cout << s.peek() <<" ";

// remove top element in stack

s.pop();

return 0;

}
//Code is Modified By Vinay Pandey

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

Advantages of Array Implementation:

• Easy to implement.

• Memory is saved as pointers are not involved.

Disadvantages of Array Implementation:

• 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.

Implementation of Stack Data Structure using Linked List:

// C++ program for linked list implementation of stack

#include <bits/stdc++.h>

using namespace std;

// A structure to represent a stack

class StackNode {

public:

int data;

StackNode* next;

};
StackNode* newNode(int data)

StackNode* stackNode = new StackNode();

stackNode->data = data;

stackNode->next = NULL;

return stackNode;

int isEmpty(StackNode* root)

return !root;

void push(StackNode** root, int data)

StackNode* stackNode = newNode(data);

stackNode->next = *root;

*root = stackNode;

cout << data << " pushed to stack\n";

int pop(StackNode** root)

if (isEmpty(*root))

return INT_MIN;

StackNode* temp = *root;

*root = (*root)->next;

int popped = temp->data;

free(temp);

return popped;
}

int peek(StackNode* root)

if (isEmpty(root))

return INT_MIN;

return root->data;

// Driver code

int main()

StackNode* root = NULL;

push(&root, 10);

push(&root, 20);

push(&root, 30);

cout << pop(&root) << " popped from stack\n";

cout << "Top element is " << peek(root) << endl;

cout <<"Elements present in stack : ";

//print all elements in stack :

while(!isEmpty(root))

// print top element in stack

cout << peek(root) <<" ";

// remove top element in stack

pop(&root);

}
return 0;

// This is code is contributed by rathbhupendra

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

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

Advantages of Stack Data Structure:

• 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.
Disadvantages of Stack Data Structure:

• 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.

Applications of Stack Data Structure:

• Infix to Postfix /Prefix conversion

• Redo-undo features at many places like editors, photoshop.

• Forward and backward features in web browsers

• In Memory management, any modern computer uses a stack as the primary management
for a running purpose. Each program that is running in a computer system has its own
memory allocations.

• Stack also helps in implementing function call in computers. The last called function is always
completed first.

Explanation
• 1434 0

How to convert an Infix expression to a Postfix expression?

To convert infix expression to postfix expression, use the stack data structure. Scan the infix
expression from left to right. Whenever we get an operand, add it to the postfix expression and if we
get an operator or parenthesis add it to the stack by maintaining their precedence.

Below are the steps to implement the above idea:

1. Scan the infix expression from left to right.

2. If the scanned character is an operand, put it in the postfix expression.

3. Otherwise, do the following

• If the precedence and associativity of the scanned operator are greater than the
precedence and associativity of the operator in the stack [or the stack is empty or
the stack contains a ‘(‘ ], then push it in the stack. [‘^‘ operator is right associative
and other operators like ‘+‘,’–‘,’*‘ and ‘/‘ are left-associative].

o Check especially for a condition when the operator at the top of the stack
and the scanned operator both are ‘^‘. In this condition, the precedence of
the scanned operator is higher due to its right associativity. So it will be
pushed into the operator stack.

o In all the other cases when the top of the operator stack is the same as the
scanned operator, then pop the operator from the stack because of left
associativity due to which the scanned operator has less precedence.

• Else, Pop all the operators from the stack which are greater than or equal to in
precedence than that of the scanned operator.

o After doing that Push the scanned operator to the stack. (If you encounter
parenthesis while popping then stop there and push the scanned operator in
the stack.)

4. If the scanned character is a ‘(‘, push it to the stack.

5. If the scanned character is a ‘)’, pop the stack and output it until a ‘(‘ is encountered, and
discard both the parenthesis.

6. Repeat steps 2-5 until the infix expression is scanned.

7. Once the scanning is over, Pop the stack and add the operators in the postfix expression until
it is not empty.

8. Finally, print the postfix expression.

Illustration:

Follow the below illustration for a better understanding

Consider the infix expression exp = “a+b*c+d”


and the infix expression is scanned using the iterator i, which is initialized as i = 0.

1st Step: Here i = 0 and exp[i] = ‘a’ i.e., an operand. So add this in the postfix expression. Therefore,
postfix = “a”.
Add ‘a’ in the postfix

2nd Step: Here i = 1 and exp[i] = ‘+’ i.e., an operator. Push this into the stack. postfix = “a” and stack =
{+}.

Push ‘+’ in the stack

3rd Step: Now i = 2 and exp[i] = ‘b’ i.e., an operand. So add this in the postfix expression. postfix =
“ab” and stack = {+}.

Add ‘b’ in the postfix

4th Step: Now i = 3 and exp[i] = ‘*’ i.e., an operator. Push this into the stack. postfix = “ab” and stack
= {+, *}.
Push ‘*’ in the stack

5th Step: Now i = 4 and exp[i] = ‘c’ i.e., an operand. Add this in the postfix expression. postfix = “abc”
and stack = {+, *}.

Add ‘c’ in the postfix

6th Step: Now i = 5 and exp[i] = ‘+’ i.e., an operator. The topmost element of the stack has higher
precedence. So pop until the stack becomes empty or the top element has less precedence. ‘*’ is
popped and added in postfix. So postfix = “abc*” and stack = {+}.
Pop ‘*’ and add in postfix

Now top element is ‘+‘ that also doesn’t have less precedence. Pop it. postfix = “abc*+”.

Pop ‘+’ and add it in postfix

Now stack is empty. So push ‘+’ in the stack. stack = {+}.


Push ‘+’ in the stack

7th Step: Now i = 6 and exp[i] = ‘d’ i.e., an operand. Add this in the postfix expression. postfix =
“abc*+d”.

Add ‘d’ in the postfix

Final Step: Now no element is left. So empty the stack and add it in the postfix expression. postfix =
“abc*+d+”.
Below is the implementation of the above algorithm:

#include <bits/stdc++.h>

using namespace std;

// Function to return precedence of operators

int prec(char c) {

if (c == '^')

return 3;

else if (c == '/' || c == '*')

return 2;

else if (c == '+' || c == '-')

return 1;

else

return -1;

// Function to return associativity of operators


char associativity(char c) {

if (c == '^')

return 'R';

return 'L'; // Default to left-associative

// The main function to convert infix expression

// to postfix expression

void infixToPostfix(string s) {

stack<char> st;

string result;

for (int i = 0; i < s.length(); i++) {

char c = s[i];

// If the scanned character is

// an operand, add it to the output string.

if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'))

result += c;

// If the scanned character is an

// ‘(‘, push it to the stack.

else if (c == '(')

st.push('(');

// If the scanned character is an ‘)’,

// pop and add to the output string from the stack

// until an ‘(‘ is encountered.

else if (c == ')') {

while (st.top() != '(') {

result += st.top();
st.pop();

st.pop(); // Pop '('

// If an operator is scanned

else {

while (!st.empty() && prec(s[i]) < prec(st.top()) ||

!st.empty() && prec(s[i]) == prec(st.top()) &&

associativity(s[i]) == 'L') {

result += st.top();

st.pop();

st.push(c);

// Pop all the remaining elements from the stack

while (!st.empty()) {

result += st.top();

st.pop();

cout << result << endl;

// Driver code

int main() {

string exp = "a+b*(c^d-e)^(f+g*h)-i";

// Function call
infixToPostfix(exp);

return 0;

Evaluation of Postfix Expression using Stack:

To evaluate a postfix expression we can use a stack.

Iterate the expression from left to right and keep on storing the operands into a stack. Once an
operator is received, pop the two topmost elements and evaluate them and push the result in the
stack again.

Illustration:

Follow the below illustration for a better understanding:

Consider the expression: exp = “2 3 1 * + 9 -“

• Scan 2, it’s a number, So push it into stack. Stack contains ‘2’.

Push 2 into stack

• Scan 3, again a number, push it to stack, stack now contains ‘2 3’ (from bottom to top)
Push 3 into stack

• Scan 1, again a number, push it to stack, stack now contains ‘2 3 1’


Push 1 into stack

• Scan *, it’s an operator. Pop two operands from stack, apply the * operator on operands. We
get 3*1 which results in 3. We push the result 3 to stack. The stack now becomes ‘2 3’.
Evaluate * operator and push result in stack

• Scan +, it’s an operator. Pop two operands from stack, apply the + operator on operands. We
get 3 + 2 which results in 5. We push the result 5 to stack. The stack now becomes ‘5’.
Evaluate + operator and push result in stack

• Scan 9, it’s a number. So we push it to the stack. The stack now becomes ‘5 9’.
Push 9 into stack

• Scan -, it’s an operator, pop two operands from stack, apply the – operator on operands, we
get 5 – 9 which results in -4. We push the result -4 to the stack. The stack now becomes ‘-4’.
Evaluate ‘-‘ operator and push result in stack

• There are no more elements to scan, we return the top element from the stack (which is the
only element left in a stack).

So the result becomes -4.

Follow the steps mentioned below to evaluate postfix expression using stack:

• Create a stack to store operands (or values).

• Scan the given expression from left to right and do the following for every scanned element.

o If the element is a number, push it into the stack.

o If the element is an operator, pop operands for the operator from the stack. Evaluate
the operator and push the result back to the stack.

• When the expression is ended, the number in the stack is the final answer.

Below is the implementation of the above approach:


What is the Recursive stack size ?

Last Updated : 06 Feb, 2023

Recursion: The function calling itself is called recursion.

Stack: A stack is a data structure in which elements are inserted and deleted only at one end called
the top of the stack. It follows the LIFO (Last In First Out) mechanism.

Recursive Stack Size:

When a recursive function is invoked, its parameters are stored in a data structure called activation
records. Every time the recursive function is invoked, a new activation record is generated and stored
in the memory. These activation records are stored in the special stack called the recursive stack.
These activation records are deleted when the function execution is completed.

So, when a recursive function is invoked, it generates the activation records for different values of the
recursive function hence, extending the recursion stack size. When the recursive function execution
is completed one by one its activation records get deleted hence, contracting the recursive stack size.
The recursive stack size depends on the number of activation records created and deleted.

#include <bits/stdc++.h>

using namespace std;

int fact(int n)

if (n == 1)

return 1;

return fact(n - 1);

int main()

int p;

p = fact(2);

return 0;
Time complexity: O(n)

Auxiliary space: O(n) // because we are using stack.

Illustration:

For the above program. Firstly, the activation record for main stack is generated and stored in the
stack.

Initial state

In the above program, there is a recursive function fact that has n as the local parameter. In the
above example program, n=2 is passed in the recursive function call.

First Step: First, the function is invoked for n =2 and its activation record are created in the recursive
stack.
1st step

2nd Step: Then according to the recursive function, it is invoked for n=1 and its activation record is
created in the recursive stack.
2nd step

3rd Step: After the execution of the function for value n=1 as it is a base condition, its execution gets
completed and its activation record gets deleted.
3rd step

4th step: Similarly, the function for value n=2(its previous function) gets executed and its activation
record gets deleted. It comes out from the recursive function to the main function.
4th step

So, in the above example for recursive function fact and value, n=2 recursive stack size excluding the
main function is 2. Hence, for value n recursive stack size is n.

Queue Data Structure

Last Updated : 30 Jul, 2024

A Queue Data Structure is a fundamental concept in computer science used for storing and
managing data in a specific order. It follows the principle of “First in, First out” (FIFO), where the first
element added to the queue is the first one to be removed. Queues are commonly used in various
algorithms and applications for their simplicity and efficiency in managing data flow.
What is Queue in Data Structures?

A queue is a linear data structure that follows the First-In-First-Out (FIFO) principle. It operates like a
line where elements are added at one end (rear) and removed from the other end (front).

Basic Operations of Queue Data Structure

• Enqueue (Insert): Adds an element to the rear of the queue.

• Dequeue (Delete): Removes and returns the element from the front of the queue.

• Peek: Returns the element at the front of the queue without removing it.

• Empty: Checks if the queue is empty.

• Full: Checks if the queue is full.

Applications of Queue

• Task scheduling in operating systems

• Data transfer in network communication

• Simulation of real-world systems (e.g., waiting lines)

• Priority queues for event processing queues for event processing

Implementation of Queues

Queues can be implemented using Two techniques:

• Implementations of Queue Data Structure using Arrays

• Implementations of Queue Data Structure using Linked List

Array implementation of queue (Simple)

Last Updated : 25 Jul, 2024


Please note that a simple array implementation discussed here is not used in practice as it is not
efficient. In practice, we either use Linked List Implementation of Queue or circular array
implementation of queue. The idea of this post is to give you a background as to why we need a
circular array implementation.

To implement a queue using a simple array,

• create an array arr of size n and

• take two variables front and rear which are initialized as 0 and -1 respectively

• rear is the index up to which the elements are stored in the array including the rear index
itself. We mainly add an item by incrementing it.

• front is the index of the first element of the array. We mainly remove the element at this
index in Dequeue operation.

• Enqueue: Addition of an element to the queue. Adding an element will be performed after
checking whether the queue is full or not. If rear == capacity-1, then it is said to be an
Overflow condition as the array is full. If array is not full. then store the element at arr[rear]
and increment rear by 1 .

• Dequeue: Removal of an element from the queue. An element can only be deleted when
there is at least an element to delete. If rear == -1, then it is said to be an Underflow
condition as the array is empty. If the element at arr[front] can be deleted then move all the
remaining elements to the left by one position in order to maintain the FIFO (the first element
must be deleted).

• Front: Get the front element from the queue i.e. arr[front] if the queue is not empty.

• Display: Print all elements of the queue. If the queue is non-empty, traverse and print all the
elements from the index front to rear.
Below is the implementation of a queue using an array:

#include <bits/stdc++.h>

using namespace std;

struct Queue {

int front, rear, capacity;

int* queue;

// Constructor to initialize the queue

Queue(int c)

front = 0;
rear = -1;

capacity = c;

queue = new int[c];

// Destructor to free the allocated memory

~Queue() { delete[] queue; }

// Function to insert an element at the rear of the queue

void queueEnqueue(int data)

// Check if the queue is full

if (rear == capacity - 1) {

printf("\nQueue is full\n");

return;

// Insert element at the rear

queue[++rear] = data;

// Function to delete an element from the front

// of the queue

void queueDequeue()

// If the queue is empty

if (front > rear) {

printf("\nQueue is empty\n");

return;

}
// Shift all elements from index 1 till rear to

// the left by one

for (int i = 0; i < rear; i++) {

queue[i] = queue[i + 1];

// Decrement rear

rear--;

// Function to print queue elements

void queueDisplay()

if (front > rear) {

printf("\nQueue is Empty\n");

return;

// Traverse front to rear and print elements

for (int i = front; i <= rear; i++) {

printf(" %d <-- ", queue[i]);

printf("\n");

// Function to print the front of the queue

void queueFront()

if (rear == -1) {

printf("\nQueue is Empty\n");

return;
}

printf("\nFront Element is: %d\n", queue[front]);

};

// Driver code

int main(void)

// Create a queue of capacity 4

Queue q(4);

// Print queue elements

q.queueDisplay();

// Insert elements in the queue

q.queueEnqueue(20);

q.queueEnqueue(30);

q.queueEnqueue(40);

q.queueEnqueue(50);

// Print queue elements

q.queueDisplay();

// Insert element in the queue

q.queueEnqueue(60);

// Print queue elements

q.queueDisplay();

// Dequeue elements

q.queueDequeue();
q.queueDequeue();

printf("\nAfter two node deletions\n");

// Print queue elements

q.queueDisplay();

printf("\nAfter one insertion\n");

q.queueEnqueue(60);

// Print queue elements

q.queueDisplay();

// Print front of the queue

q.queueFront();

return 0;

Output

Queue is Empty

20 <-- 30 <-- 40 <-- 50 <--

Queue is full

20 <-- 30 <-- 40 <-- 50 <--

After two node deletions

40 <-- 50 <--

After one insertion

40 <-- 50 <-- 60 <--

Front Element...
Queue – Linked List Implementation

Last Updated : 31 Jul, 2024

In this article, the Linked List implementation of the queue data structure is discussed and
implemented. Print ‘-1’ if the queue is empty.

Approach: To solve the problem follow the below idea:

we maintain two pointers, front, and rear. The front points to the first item of the queue and rear
points to the last item.

• enQueue(): This operation adds a new node after the rear and moves the rear to the next
node.

• deQueue(): This operation removes the front node and moves the front to the next node.

Follow the below steps to solve the problem:

• Create a class QNode with data members integer data and QNode* next

o A parameterized constructor that takes an integer x value as a parameter and sets


data equal to x and next as NULL

• Create a class Queue with data members QNode front and rear

• Enqueue Operation with parameter x:

o Initialize QNode* temp with data = x

o If the rear is set to NULL then set the front and rear to temp and return(Base Case)

o Else set rear next to temp and then move rear to temp

• Dequeue Operation:

o If the front is set to NULL return(Base Case)

o Initialize QNode temp with front and set front to its next

o If the front is equal to NULL then set the rear to NULL

o Delete temp from the memory

// C++ program to implement the queue data structure using

// linked list

#include <bits/stdc++.h>

using namespace std;


// Node class representing a single node in the linked list

class Node {

public:

int data;

Node* next;

Node(int new_data)

this->data = new_data;

this->next = nullptr;

};

// Class to implement queue operations using a linked list

class Queue {

// Pointer to the front and the rear of the linked list

Node *front, *rear;

public:

// Constructor to initialize the front and rear

Queue() { front = rear = nullptr; }

// Function to check if hte queu is empty

bool isEmpty()

// If the front and rear are null, then the queue is

// empty, otherwise it's not

if (front == nullptr) {

return true;

return false;
}

// Function to add an element to the queue

void enqueue(int new_data) {

// Create a new linked list node

Node* new_node = new Node(new_data);

// If queue is empty, the new node is both the front

// and rear

if (this->isEmpty()) {

front = rear = new_node;

return;

// Add the new node at the end of the queue and

// change rear

rear->next = new_node;

rear = new_node;

// Function to remove an element from the queue

void dequeue() {

// If queue is empty, return

if (this->isEmpty()) {

cout << "Queue Underflow\n";

return;

// Store previous front and move front one node


// ahead

Node* temp = front;

front = front->next;

// If front becomes nullptr, then change rear also

// to nullptr

if (front == nullptr)

rear = nullptr;

// Deallocate memory of the old front node

delete temp;

// Function to get the front element of the queue

int getFront() {

// Checking if the queue is empty

if (this->isEmpty()) {

cout << "Queue is empty\n";

return INT_MIN;

return front->data;

// Function to get the rear element of the queue

int getRear() {

// Checking if the queue is empty

if (this->isEmpty()) {

cout << "Queue is empty\n";

return INT_MIN;
}

return rear->data;

};

// Driver code to test the queue implementation

int main()

Queue q;

// Enqueue elements into the queue

q.enqueue(10);

q.enqueue(20);

// Display the front and rear elements of the queue

cout << "Queue Front: " << q.getFront() << endl;

cout << "Queue Rear: " << q.getRear() << endl;

// Dequeue elements from the queue

q.dequeue();

q.dequeue();

// Enqueue more elements into the queue

q.enqueue(30);

q.enqueue(40);

q.enqueue(50);

// Dequeue an element from the queue

q.dequeue();
// Display the front and rear elements of the queue

cout << "Queue Front: " << q.getFront() << endl;

cout << "Queue Rear: " << q.getRear() << endl << endl;

return 0;

Output

Queue Front: 10

Queue Rear: 20

Queue Front: 40

Queue Rear: 50

Circular Queue Data Structure

A circular queue is the extended version of a regular queue where the last element is connected to
the first element. Thus forming a circle-like structure.

Circular queue representation

The circular queue solves the major limitation of the normal queue. In a normal queue, after a bit of
insertion and deletion, there will be non-usable empty space.
Limitation of the regular Queue

Here, indexes 0 and 1 can only be used after resetting the queue (deletion of all elements). This
reduces the actual size of the queue.

How Circular Queue Works

Circular Queue works by the process of circular increment i.e. when we try to increment the pointer
and we reach the end of the queue, we start from the beginning of the queue.

Here, the circular increment is performed by modulo division with the queue size. That is,

if REAR + 1 == 5 (overflow!), REAR = (REAR + 1)%5 = 0 (start of queue)

Circular Queue Operations

The circular queue work as follows:

• two pointers FRONT and REAR

• FRONT track the first element of the queue

• REAR track the last elements of the queue

• initially, set value of FRONT and REAR to -1

1. Enqueue Operation

• check if the queue is full

• for the first element, set value of FRONT to 0

• circularly increase the REAR index by 1 (i.e. if the rear reaches the end, next it would be at
the start of the queue)

• add the new element in the position pointed to by REAR

2. Dequeue Operation

• check if the queue is empty

• return the value pointed by FRONT

• circularly increase the FRONT index by 1


• for the last element, reset the values of FRONT and REAR to -1

However, the check for full queue has a new additional case:

• Case 1: FRONT = 0 && REAR == SIZE - 1

• Case 2: FRONT = REAR + 1

The second case happens when REAR starts from 0 due to circular increment and when its value is
just 1 less than FRONT, the queue is full.
Enque and Deque Operations
Circular Queue Implementations in Python, Java, C, and C++

The most common queue implementation is using arrays, but it can also be implemented using lists.

// Circular Queue implementation in C++

#include <iostream>

#define SIZE 5 /* Size of Circular Queue */

using namespace std;

class Queue {

private:

int items[SIZE], front, rear;

public:

Queue() {

front = -1;

rear = -1;

// Check if the queue is full

bool isFull() {

if (front == 0 && rear == SIZE - 1) {

return true;

if (front == (rear + 1) % SIZE) {

return true;

return false;

// Check if the queue is empty


bool isEmpty() {

if (front == -1)

return true;

else

return false;

// Adding an element

void enQueue(int element) {

if (isFull()) {

cout << "Queue is full" << endl;

} else {

if (front == -1) front = 0;

rear = (rear + 1) % SIZE;

items[rear] = element;

cout << endl

<< "Inserted " << element << endl;

// Removing an element

int deQueue() {

int element;

if (isEmpty()) {

cout << "Queue is empty" << endl;

return (-1);

} else {

element = items[front];

if (front == rear) {

front = -1;

rear = -1;

// Q has only one element,


// so we reset the queue after deleting it.

else {

front = (front + 1) % SIZE;

return (element);

void display() {

// Function to display status of Circular Queue

int i;

if (isEmpty()) {

cout << endl

<< "Empty Queue" << endl;

} else {

cout << "Front -> " << front;

cout << endl

<< "Items -> ";

for (i = front; i != rear; i = (i + 1) % SIZE)

cout << items[i] << "\t";

cout << items[i];

cout << endl

<< "Rear -> " << rear << endl;

};

int main() {

Queue q;

// Fails because front = -1


q.deQueue();

q.enQueue(1);

q.enQueue(2);

q.enQueue(3);

q.enQueue(4);

q.enQueue(5);

// Fails to enqueue because front == 0 && rear == SIZE - 1

q.enQueue(6);

q.display();

int elem = q.deQueue();

if (elem != -1)

cout << endl

<< "Deleted Element is " << elem << endl;

q.display();

q.enQueue(7);

q.display();

// Fails to enqueue because front == rear + 1

q.enQueue(8);

return 0;

}
Circular Queue Data Structure

A circular queue is the extended version of a regular queue where the last element is connected to
the first element. Thus forming a circle-like structure.

Circular queue representation

The circular queue solves the major limitation of the normal queue. In a normal queue, after a bit of
insertion and deletion, there will be non-usable empty space.

Limitation of the regular Queue

Here, indexes 0 and 1 can only be used after resetting the queue (deletion of all elements). This
reduces the actual size of the queue.

How Circular Queue Works

Circular Queue works by the process of circular increment i.e. when we try to increment the pointer
and we reach the end of the queue, we start from the beginning of the queue.

Here, the circular increment is performed by modulo division with the queue size. That is,

if REAR + 1 == 5 (overflow!), REAR = (REAR + 1)%5 = 0 (start of queue)


Circular Queue Operations

The circular queue work as follows:

• two pointers FRONT and REAR

• FRONT track the first element of the queue

• REAR track the last elements of the queue

• initially, set value of FRONT and REAR to -1

1. Enqueue Operation

• check if the queue is full

• for the first element, set value of FRONT to 0

• circularly increase the REAR index by 1 (i.e. if the rear reaches the end, next it would be at
the start of the queue)

• add the new element in the position pointed to by REAR

2. Dequeue Operation

• check if the queue is empty

• return the value pointed by FRONT

• circularly increase the FRONT index by 1

• for the last element, reset the values of FRONT and REAR to -1

However, the check for full queue has a new additional case:

• Case 1: FRONT = 0 && REAR == SIZE - 1

• Case 2: FRONT = REAR + 1

The second case happens when REAR starts from 0 due to circular increment and when its value is
just 1 less than FRONT, the queue is full.
Enque and Deque Operations
Circular Queue Implementations in Python, Java, C, and C++

The most common queue implementation is using arrays, but it can also be implemented using lists.

Python

Java

C++

// Circular Queue implementation in C++

#include <iostream>

#define SIZE 5 /* Size of Circular Queue */

using namespace std;

class Queue {

private:

int items[SIZE], front, rear;

public:

Queue() {

front = -1;

rear = -1;

// Check if the queue is full

bool isFull() {

if (front == 0 && rear == SIZE - 1) {

return true;

if (front == (rear + 1) % SIZE) {

return true;

}
return false;

// Check if the queue is empty

bool isEmpty() {

if (front == -1)

return true;

else

return false;

// Adding an element

void enQueue(int element) {

if (isFull()) {

cout << "Queue is full" << endl;

} else {

if (front == -1) front = 0;

rear = (rear + 1) % SIZE;

items[rear] = element;

cout << endl

<< "Inserted " << element << endl;

// Removing an element

int deQueue() {

int element;

if (isEmpty()) {

cout << "Queue is empty" << endl;

return (-1);

} else {

element = items[front];

if (front == rear) {
front = -1;

rear = -1;

// Q has only one element,

// so we reset the queue after deleting it.

else {

front = (front + 1) % SIZE;

return (element);

void display() {

// Function to display status of Circular Queue

int i;

if (isEmpty()) {

cout << endl

<< "Empty Queue" << endl;

} else {

cout << "Front -> " << front;

cout << endl

<< "Items -> ";

for (i = front; i != rear; i = (i + 1) % SIZE)

cout << items[i] << "\t";

cout << items[i];

cout << endl

<< "Rear -> " << rear << endl;

};
int main() {

Queue q;

// Fails because front = -1

q.deQueue();

q.enQueue(1);

q.enQueue(2);

q.enQueue(3);

q.enQueue(4);

q.enQueue(5);

// Fails to enqueue because front == 0 && rear == SIZE - 1

q.enQueue(6);

q.display();

int elem = q.deQueue();

if (elem != -1)

cout << endl

<< "Deleted Element is " << elem << endl;

q.display();

q.enQueue(7);

q.display();

// Fails to enqueue because front == rear + 1

q.enQueue(8);
return 0;

Circular Queue Complexity Analysis

The complexity of the enqueue and dequeue operations of a circular queue is O(1) for (array
implementations).

Applications of Circular Queue

• CPU scheduling

• Memory management

• Traffic Management

Priority Queue

A priority queue is a special type of queue in which each element is associated with a priority value.
And, elements are served on the basis of their priority. That is, higher priority elements are served
first.

However, if elements with the same priority occur, they are served according to their order in the
queue.

Assigning Priority Value

Generally, the value of the element itself is considered for assigning the priority. For example,

The element with the highest value is considered the highest priority element. However, in other
cases, we can assume the element with the lowest value as the highest priority element.

We can also set priorities according to our needs.


Removing Highest
Priority Element

Difference between Priority Queue and Normal Queue

In a queue, the first-in-first-out rule is implemented whereas, in a priority queue, the values are
removed on the basis of priority. The element with the highest priority is removed first.

Implementation of Priority Queue

Priority queue can be implemented using an array, a linked list, a heap data structure, or a binary
search tree. Among these data structures, heap data structure provides an efficient implementation
of priority queues.

Hence, we will be using the heap data structure to implement the priority queue in this tutorial. A
max-heap is implemented in the following operations. If you want to learn more about it, please
visit max-heap and min-heap.

A comparative analysis of different implementations of priority queue is given below.

Operations peek insert delete

Linked List O(1) O(n) O(1)


Binary Heap O(1) O(log n) O(log n)

Binary Search Tree O(1) O(log n) O(log n)

Priority Queue Operations

Basic operations of a priority queue are inserting, removing, and peeking elements.

Before studying the priority queue, please refer to the heap data structure for a better understanding
of binary heap as it is used to implement the priority queue in this article.

1. Inserting an Element into the Priority Queue

Inserting an element into a priority queue (max-heap) is done by the following steps.

• Insert the new element at the end of the tree.

Insert an element at the end of the queue

• Heapify the tree. Heapify after insertion

Algorithm for insertion of an element into priority queue (max-heap)

If there is no node,

create a newNode.
else (a node is already present)

insert the newNode at the end (last node from left to right.)

heapify the array

For Min Heap, the above algorithm is modified so that parentNode is always smaller than newNode.

2. Deleting an Element from the Priority Queue

Deleting an element from a priority queue (max-heap) is done as follows:

• Select the element to be deleted. Select the


element to be deleted

• Swap it with the last element. Swap with the


last leaf node element
• Remove the last element. Remove the last
element leaf

• Heapify the tree. Heapify the priority


queue

Algorithm for deletion of an element in the priority queue (max-heap)

If nodeToBeDeleted is the leafNode

remove the node

Else swap nodeToBeDeleted with the lastLeafNode

remove noteToBeDeleted

heapify the array

For Min Heap, the above algorithm is modified so that the both childNodes are smaller
than currentNode.

3. Peeking from the Priority Queue (Find max/min)


Peek operation returns the maximum element from Max Heap or minimum element from Min Heap
without deleting the node.

For both Max heap and Min Heap

return rootNode

4. Extract-Max/Min from the Priority Queue

Extract-Max returns the node with maximum value after removing it from a Max Heap whereas
Extract-Min returns the node with minimum value after removing it from Min Heap.

Priority Queue Implementations in Python, Java, C, and C++

// Priority Queue implementation in C++

#include <iostream>

#include <vector>

using namespace std;

// Function to swap position of two elements

void swap(int *a, int *b) {

int temp = *b;

*b = *a;

*a = temp;

// Function to heapify the tree

void heapify(vector<int> &hT, int i) {

int size = hT.size();

// Find the largest among root, left child and right child

int largest = i;

int l = 2 * i + 1;

int r = 2 * i + 2;

if (l < size && hT[l] > hT[largest])


largest = l;

if (r < size && hT[r] > hT[largest])

largest = r;

// Swap and continue heapifying if root is not largest

if (largest != i) {

swap(&hT[i], &hT[largest]);

heapify(hT, largest);

// Function to insert an element into the tree

void insert(vector<int> &hT, int newNum) {

int size = hT.size();

if (size == 0) {

hT.push_back(newNum);

} else {

hT.push_back(newNum);

for (int i = size / 2 - 1; i >= 0; i--) {

heapify(hT, i);

// Function to delete an element from the tree

void deleteNode(vector<int> &hT, int num) {

int size = hT.size();

int i;

for (i = 0; i < size; i++) {

if (num == hT[i])

break;
}

swap(&hT[i], &hT[size - 1]);

hT.pop_back();

for (int i = size / 2 - 1; i >= 0; i--) {

heapify(hT, i);

// Print the tree

void printArray(vector<int> &hT) {

for (int i = 0; i < hT.size(); ++i)

cout << hT[i] << " ";

cout << "\n";

// Driver code

int main() {

vector<int> heapTree;

insert(heapTree, 3);

insert(heapTree, 4);

insert(heapTree, 9);

insert(heapTree, 5);

insert(heapTree, 2);

cout << "Max-Heap array: ";

printArray(heapTree);

deleteNode(heapTree, 4);
cout << "After deleting an element: ";

printArray(heapTree);

Priority Queue Applications

Some of the applications of a priority queue are:

• Dijkstra's algorithm

• for implementing stack

• for load balancing and interrupt handling in an operating system

• for data compression in Huffman code

Deque Data Structure

Deque or Double Ended Queue is a type of queue in which insertion and removal of elements can
either be performed from the front or the rear. Thus, it does not follow FIFO rule (First In First Out).

Representation of Deque

Types of Deque

• Input Restricted Deque


In this deque, input is restricted at a single end but allows deletion at both the ends.

• Output Restricted Deque


In this deque, output is restricted at a single end but allows insertion at both the ends.

Operations on a Deque

Below is the circular array implementation of deque. In a circular array, if the array is full, we start
from the beginning.

But in a linear array implementation, if the array is full, no more elements can be inserted. In each of
the operations below, if the array is full, "overflow message" is thrown.
Before performing the following operations, these steps are followed.

1. Take an array (deque) of size n.

2. Set two pointers front = -1 and rear = 0.

Initialize an array and pointers for


deque

1. Insert at the Front

This operation adds an element at the front.

1. Check if the deque is full.


Check the position of front

2. If the deque is full (i.e. (front == 0 && rear == n - 1) || (front == rear + 1)), insertion operation
cannot be performed (overflow condition).

3. If the deque is empty, reinitialize front = 0. And, add the new key into array[front].

4. If front = 0, reinitialize front = n-1 (last index).

Shift front to the end

5. Else, decrease front by 1.


6. Add the new key 5 into array[front].

Insert the element at Front

2. Insert at the Rear

This operation adds an element to the rear.

1. Check if the deque is full.


Check if deque is full

2. If the deque is full, insertion operation cannot be performed (overflow condition).

3. If the deque is empty, reinitialize rear = 0. And, add the new key into array[rear].

4. If rear = n - 1, reinitialize real = 0 (first index).

5. Else, increase rear by 1.


Increase the rear
6. Add the new key 5 into array[rear].

Insert the element at rear

3. Delete from the Front

The operation deletes an element from the front.

1. Check if the deque is empty.


Check if deque is empty

2. If the deque is empty (i.e. front = -1), deletion cannot be performed (underflow condition).

3. If the deque has only one element (i.e. front = rear), set front = -1 and rear = -1.

4. Else if front is at the last index (i.e. front = n - 1), set front = 0.

5. Else, front = front + 1.


Increase the front

4. Delete from the Rear

This operation deletes an element from the rear.


1. Check if the deque is empty.
Check if deque is empty

2. If the deque is empty (i.e. front = -1), deletion cannot be performed (underflow condition).

3. If the deque has only one element (i.e. front = rear), set front = -1 and rear = -1, else follow
the steps below.

4. If rear is at the first index (i.e. rear = 0), reinitialize rear = n - 1.

5. Else, rear = rear - 1. Decrease


the rear

5. Check Empty

This operation checks if the deque is empty. If front = -1, the deque is empty.

6. Check Full

This operation checks if the deque is full. If front = 0 and rear = n - 1 OR front = rear + 1, the deque is
full.

Deque Implementation in Python, Java, C, and C++

// Deque implementation in C++

#include <iostream>

using namespace std;

#define MAX 10
class Deque {

int arr[MAX];

int front;

int rear;

int size;

public:

Deque(int size) {

front = -1;

rear = 0;

this->size = size;

void insertfront(int key);

void insertrear(int key);

void deletefront();

void deleterear();

bool isFull();

bool isEmpty();

int getFront();

int getRear();

};

bool Deque::isFull() {

return ((front == 0 && rear == size - 1) ||

front == rear + 1);

bool Deque::isEmpty() {

return (front == -1);

}
void Deque::insertfront(int key) {

if (isFull()) {

cout << "Overflow\n"

<< endl;

return;

if (front == -1) {

front = 0;

rear = 0;

else if (front == 0)

front = size - 1;

else

front = front - 1;

arr[front] = key;

void Deque ::insertrear(int key) {

if (isFull()) {

cout << " Overflow\n " << endl;

return;

if (front == -1) {

front = 0;

rear = 0;
}

else if (rear == size - 1)

rear = 0;

else

rear = rear + 1;

arr[rear] = key;

void Deque ::deletefront() {

if (isEmpty()) {

cout << "Queue Underflow\n"

<< endl;

return;

if (front == rear) {

front = -1;

rear = -1;

} else if (front == size - 1)

front = 0;

else

front = front + 1;

void Deque::deleterear() {

if (isEmpty()) {

cout << " Underflow\n"


<< endl;

return;

if (front == rear) {

front = -1;

rear = -1;

} else if (rear == 0)

rear = size - 1;

else

rear = rear - 1;

int Deque::getFront() {

if (isEmpty()) {

cout << " Underflow\n"

<< endl;

return -1;

return arr[front];

int Deque::getRear() {

if (isEmpty() || rear < 0) {

cout << " Underflow\n"

<< endl;

return -1;

return arr[rear];

}
int main() {

Deque dq(4);

cout << "insert element at rear end \n";

dq.insertrear(5);

dq.insertrear(11);

cout << "rear element: "

<< dq.getRear() << endl;

dq.deleterear();

cout << "after deletion of the rear element, the new rear element: " << dq.getRear() << endl;

cout << "insert element at front end \n";

dq.insertfront(8);

cout << "front element: " << dq.getFront() << endl;

dq.deletefront();

cout << "after deletion of front element new front element: " << dq.getFront() << endl;

Applications of Deque Data Structure

1. In undo operations on software.

2. To store history in browsers.

3. For implementing both stacks and queues.

You might also like