Data Structure and Algorithm Notes
Data Structure and Algorithm Notes
Step 1: Start
Step 4: Add num1 and num2 and assign the result to sum.
sum←num1+num2
Step 6: Stop
Algorithm 2: Find the largest number among three
numbers
Step 1: Start
Step 4: If a > b
If a > c
Else
Else
If b > c
Else
Step 5: Stop
bx + c = 0
Step 1: Start
D ← b2-4ac
Step 4: If D ≥ 0
r1 ← (-b+√D)/2a
r2 ← (-b-√D)/2a
Display r1 and r2 as roots.
Else
Calculate real part and imaginary part
rp ← -b/2a
ip ← √(-D)/2a
Display rp+j(ip) and rp-j(ip) as roots
Step 5: Stop
Step 1: Start
factorial ← 1
i ← 1
5.2: i ← i+1
Step 7: Stop
Algorithm 5: Check whether a number is prime or not
Step 1: Start
flag ← 1
i ← 2
flag ← 0
Go to step 6
5.2 i ← i+1
Step 6: If flag = 0
else
Display n is prime
Step 7: Stop
Algorithm 6: Find the Fibonacci series till the term
less than 1000
Step 1: Start
Step 6: Stop
Note: Data structure and data types are slightly different. Data structure is the
collection of data types arranged in a specific order.
However, when the complexity of the program increases, the linear data
structures might not be the best choice because of operational complexities.
It works just like a pile of plates where the last plate kept on the pile will be
removed first. To learn more, visit Stack Data Structure.
In a stack, operations can be perform only from one end (top
here).
It works just like a queue of people in the ticket counter where first person on
the queue will get the ticket first. To learn more, visit Queue Data Structure.
A linked list
Non-linear data structures are further divided into graph and tree based data
structures.
An algorithm may not have the same performance for different types of inputs.
With the increase in the input size, the performance will change.
The study of change in performance of the algorithm with the change in the
order of the input size is defined as asymptotic analysis.
Asymptotic Notations
Asymptotic notations are the mathematical notations used to describe the
running time of an algorithm when the input tends towards a particular value
or a limiting value.
For example: In bubble sort, when the input array is already sorted, the time
taken by the algorithm is linear i.e. the best case.
But, when the input array is in reverse condition, the algorithm takes the
maximum time (quadratic) to sort the elements i.e. the worst case.
When the input array is neither sorted nor in reverse order, then it takes
average time. These durations are denoted using asymptotic notations.
Big-O notation
Omega notation
Theta notation
sufficiently large n .
For any value of n , the minimum time required by the algorithm is given by
Omega Ω(g(n)) .
Theta Notation (Θ-notation)
Theta notation encloses the function from above and below. Since it
represents the upper and the lower bound of the running time of an algorithm,
it is used for analyzing the average-case complexity of an algorithm.
where,
n = size of input
2. If f(n) = Θ(n log b a), then T(n) = Θ(n log b a * log n).
ϵ > 0 is a constant.
2. If the cost of solving the sub-problem at each level is nearly equal, then the
value of f(n) will be nlog b a . Thus, the time complexity will be f(n) times the total
number of levels ie. nlog b a * log n
a < 1
Here, we will sort an array using the divide and conquer approach (ie. merge
sort).
Time Complexity
The complexity of the divide and conquer algorithm is calculated using
the master theorem.
where,
n = size of input
n/b = size of each subproblem. All subproblems are assumed to have the same size.
f(n) = cost of the work done outside the recursive call, which includes the cost
of dividing the problem and cost of merging the solutions
= 2T(n/2) + O(n)
Where,
f(n) = time taken to divide the problem and merging the subproblems
T(n/2) = O(n log n) (To understand this, please refer to the master theorem.)
≈ O(n log n)
Divide and Conquer Vs Dynamic approach
The divide and conquer approach divides a problem into smaller
subproblems; these subproblems are further solved recursively. The result of
each subproblem is not stored for future reference, whereas, in a dynamic
approach, the result of each subproblem is stored for future reference.
Use the divide and conquer approach when the same subproblem is not
solved multiple times. Use the dynamic approach when the result of a
subproblem is to be used multiple times in the future.
Let us understand this with an example. Suppose we are trying to find the
Fibonacci series. Then,
fib(n)
If n < 2, return 1
Dynamic approach:
mem = []
fib(n)
else,
If n < 2, f = 1
return f
Karatsuba Algorithm
Week 3
Array Data Structure
What is an Array?
An array is a type of linear data structure that is defined as a collection of
elements with same or different data types. They exist in both single dimension
and multiple dimensions. These data structures come into picture when there is
a necessity to store multiple elements of similar nature together at one place.
The difference between an array index and a memory address is that the array
index acts like a key value to label the elements in the array. However, a
memory address is the starting address of free memory available.
Syntax
Creating an array in C and C++ programming languages −
Arrays provide O(1) random access lookup time. That means, accessing the
1 st index of the array and the 1000 th index of the array will both take the same
time. This is due to the fact that array comes with a pointer and an offset value.
The pointer points to the right location of the memory and the offset value
shows how far to look in the said memory.
array_name[index]
| |
Pointer Offset
Array Representation
Arrays are represented as a collection of buckets where each bucket stores one
element. These buckets are indexed from '0' to 'n-1', where n is the size of that
particular array. For example, an array with size 10 will have buckets indexed
from 0 to 9.
In C, when an array is initialized with size, then it assigns defaults values to its
elements in following order.
Data Type Default Value
bool false
char 0
int 0
float 0.0
double 0.0f
void
wchar_t 0
Algorithm
Following is an algorithm to insert elements into a Linear Array until we reach
the end of the array −
1. Start
2. Create an Array of a desired datatype and size.
3. Initialize a variable 'i' as 0.
4. Enter the element at ith index of the array.
5. Increment i by 1.
6. Repeat Steps 4 & 5 until the end of the array.
7. Stop
Example
Here, we see a practical implementation of insertion operation, where we add
data at the end of the array −
Open Compiler
#include <iostream>
using namespace std;
int main(){
int LA[3] = {}, i;
cout << "Array Before Insertion:" << endl;
for(i = 0; i < 3; i++)
cout << "LA[" << i <<"] = " << LA[i] << endl;
Output
Array Before Insertion:
LA[0] = 0
LA[1] = 0
LA[2] = 0
Inserting elements..
Array After Insertion:
LA[0] = 2
LA[1] = 3
LA[2] = 4
LA[3] = 5
LA[4] = 6
Algorithm
Consider LA is a linear array with N elements and K is a positive integer such
that K<=N. Following is the algorithm to delete an element available at the
K th position of LA.
1. Start
2. Set J = K
3. Repeat steps 4 and 5 while J < N
4. Set LA[J] = LA[J + 1]
5. Set J = J+1
6. Set N = N-1
7. Stop
Example
Following are the implementations of this operation in various programming
languages −
Open Compiler
#include <iostream>
using namespace std;
int main(){
int LA[] = {1,3,5};
int i, n = 3;
cout << "The original array elements are :"<<endl;
for(i = 0; i<n; i++) {
cout << "LA[" << i << "] = " << LA[i] << endl;
}
for(i = 1; i<n; i++) {
LA[i] = LA[i+1];
n = n - 1;
}
cout << "The array elements after deletion :"<<endl;
for(i = 0; i<n; i++) {
cout << "LA[" << i << "] = " << LA[i] <<endl;
}
}
Output
The original array elements are :
LA[0] = 1
LA[1] = 3
LA[2] = 5
The array elements after deletion :
LA[0] = 1
LA[1] = 5
Algorithm
Consider LA is a linear array with N elements and K is a positive integer such
that K<=N. Following is the algorithm to find an element with a value of ITEM
using sequential search.
1. Start
2. Set J = 0
3. Repeat steps 4 and 5 while J < N
4. IF LA[J] is equal ITEM THEN GOTO STEP 6
5. Set J = J +1
6. PRINT J, ITEM
7. Stop
Example
Following are the implementations of this operation in various programming
languages −
Open Compiler
#include <iostream>
using namespace std;
int main(){
int LA[] = {1,3,5,7,8};
int item = 5, n = 5;
int i = 0;
cout << "The original array elements are : " <<endl;
for(i = 0; i<n; i++) {
cout << "LA[" << i << "] = " << LA[i] << endl;
}
for(i = 0; i<n; i++) {
if( LA[i] == item ) {
cout << "Found element " << item << " at position " << i+1 <<endl;
}
}
return 0;
}
Output
The original array elements are :
LA[0] = 1
LA[1] = 3
LA[2] = 5
LA[3] = 7
LA[4] = 8
Found element 5 at position 3
Algorithm
Following is the algorithm to traverse through all the elements present in a
Linear Array −
1 Start
2. Initialize an Array of certain size and datatype.
3. Initialize another variable i with 0.
4. Print the ith value in the array and increment i.
5. Repeat Step 4 until the end of the array is reached.
6. End
Example
Following are the implementations of this operation in various programming
languages −
Open Compiler
#include <iostream>
using namespace std;
int main(){
int LA[] = {1,3,5,7,8};
int item = 10, k = 3, n = 5;
int i = 0, j = n;
cout << "The original array elements are:\n";
for(i = 0; i<n; i++)
cout << "LA[" << i << "] = " << LA[i] << endl;
return 0;
}
Output
The original array elements are :
LA[0] = 1
LA[1] = 3
LA[2] = 5
LA[3] = 7
LA[4] = 8
Algorithm
Consider LA is a linear array with N elements and K is a positive integer such
that K<=N. Following is the algorithm to update an element available at the Kth
position of LA.
1. Start
2. Set LA[K-1] = ITEM
3. Stop
Example
Following are the implementations of this operation in various programming
languages −
Open Compiler
#include <iostream>
using namespace std;
int main(){
int LA[] = {1,3,5,7,8};
int item = 10, k = 3, n = 5;
int i = 0, j = n;
cout << "The original array elements are :\n";
for(i = 0; i<n; i++)
cout << "LA[" << i << "] = " << LA[i] << endl;
LA[2] = item;
cout << "The array elements after updation are :\n";
for(i = 0; i<n; i++)
cout << "LA[" << i << "] = " << LA[i] << endl;
return 0;
}
Output
The original array elements are :
LA[0] = 1
LA[1] = 3
LA[2] = 5
LA[3] = 7
LA[4] = 8
The array elements after updation :
LA[0] = 1
LA[1] = 3
LA[2] = 10
LA[3] = 7
LA[4] = 8
Algorithm
Consider LA is a linear array with N elements. Following is the algorithm to
display an array elements.
1. Start
2. Print all the elements in the Array
3. Stop
Example
Following are the implementations of this operation in various programming
languages –
Open Compiler
#include <iostream>
using namespace std;
int main(){
int LA[] = {1,3,5,7,8};
int n = 5;
int i;
cout << "The original array elements are :\n";
for(i = 0; i<n; i++)
cout << "LA[" << i << "] = " << LA[i] << endl;
return 0;
}
Output
The original array elements are :
LA[0] = 1
LA[1] = 3
LA[2] = 5
LA[3] = 7
LA[4] = 8
Types of Arrays on the basis of Dimensions
1. One-dimensional Array(1-D Array): You can imagine a 1d array as a
row, where elements are stored one after another.
2. Multi-dimensional Array: A multi-dimensional array is an array with more
than one dimension. We can use multidimensional array to store complex
data in the form of tables, etc. We can have 2-D arrays, 3-D arrays, 4-D
arrays and so on.
Two-Dimensional Array(2-D Array or Matrix): 2-D Multidimensional
arrays can be considered as an array of arrays or as a matrix consisting
of rows and columns.
To read more about Matrix Refer, Matrix Data Structure
Three-Dimensional Array(3-D Array): A 3-D Multidimensional
array contains three dimensions, so it can be considered an array of two-
dimensional arrays.
To read more about Multidimensional Array Refer, Multidimensional Arrays
in C – 2D and 3D Arrays
Operations on Array
1. Array Traversal
Array traversal refers to the process of accessing and processing each
element of an array sequentially. This is one of the most fundamental
operations in programming, as arrays are widely used data structures for
storing multiple elements in a single variable.
How Array Traversal Works?
When an array is created, it occupies a contiguous block of memory where
elements are stored in an indexed manner. Each element can be accessed
using its index, which starts from 0 in most programming languages.
For example, consider an array containing five integers:
arr = [10, 20, 30, 40, 50]
Here:
The first element (10) is at index 0.
The second element (20) is at index 1.
The last element (50) is at index 4.
Array traversal means accessing each element from start to end (or
sometimes in reverse order), usually by using a loop.
Types of Array Traversal
Array traversal can be done in multiple ways based on the requirement:
1. Sequential (Linear) Traversal
This is the most common way of traversing an array.
It involves iterating through the array one element at a time from the
first index to the last.
Used for printing elements, searching, or performing calculations (such
as sum or average).
2. Reverse Traversal
Instead of starting from index 0, the traversal begins from the last
element and moves towards the first.
This is useful in cases where we need to process elements from the
end.
To read more about Array Traversal Refer, Traversal in Array
2. Insertion in Array
Insertion in an array refers to the process of adding a new element at a
specific position while maintaining the order of the existing elements. Since
arrays have a fixed size in static implementations, inserting an element often
requires shifting existing elements to make space.
How Insertion Works in an Array?
Arrays are stored in contiguous memory locations, meaning elements are
arranged in a sequential block. When inserting a new element, the following
happens:
1. Identify the Position: Determine where the new element should be
inserted.
2. Shift Elements: Move the existing elements one position forward to
create space for the new element.
3. Insert the New Element: Place the new value in the correct position.
4. Update the Size (if applicable): If the array is dynamic, its size is
increased.
For example, if we have the array:
arr = [10, 20, 30, 40, 50]
and we want to insert 25 at index 2, the new array will be:
arr = [10, 20, 25, 30, 40, 50]
Here, elements 30, 40, and 50 have shifted right to make space.
Types of Insertion
1. Insertion at the Beginning (Index 0)
Every element must shift one position right.
This is the least efficient case for large arrays as it affects all elements.
2. Insertion at a Specific Index
Elements after the index shift right.
If the index is in the middle, half of the array moves.
3. Insertion at the End
The simplest case since no shifting is required.
Used in dynamic arrays where size increases automatically (e.g., Python
lists, Java ArrayList).
To read more about Insertion in Array Refer, Inserting Elements in an Array
– Array Operations
3. Deletion in Array
Deletion in an array refers to the process of removing an element from a
specific position while maintaining the order of the remaining elements.
Unlike linked lists, where deletion is efficient, removing an element from an
array requires shifting elements to fill the gap.
How Deletion Works in an Array?
Since arrays have contiguous memory allocation, deleting an element does
not reduce the allocated memory size. Instead, it involves:
1. Identify the Position: Find the index of the element to be deleted.
2. Shift Elements: Move the elements after the deleted element one
position to the left.
3. Update the Size (if applicable): If using a dynamic array, the size might
be reduced.
For example, consider the array:
arr = [10, 20, 30, 40, 50]
If we delete the element 30 (index 2), the new array will be:
arr = [10, 20, 40, 50]
Here, elements 40 and 50 shifted left to fill the gap.
Types of Deletion
1. Deletion at the Beginning (Index 0)
Every element shifts left by one position.
This is the most expensive case as it affects all elements.
2. Deletion at a Specific Index
Only elements after the index shift left.
If the index is in the middle, half of the array moves.
3. Deletion at the End
The simplest case since no shifting is required.
The size of the array is reduced (in dynamic arrays).
To read more about Deletion in Array Refer, Deleting Elements in an Array –
Array Operations
4. Searching in Array
Searching in an array refers to the process of finding a specific element in a
given list of elements. The goal is to determine whether the element exists in
the array and, if so, find its index (position).
Searching is a fundamental operation in programming, as it is used in data
retrieval, filtering, and processing.
Types of Searching in an Array
There are two main types of searching techniques in an array:
1. Linear Search (Sequential Search)
This is the simplest search algorithm.
It traverses the array one element at a time and compares each element
with the target value.
If a match is found, it returns the index of the element.
If the element is not found, the search continues until the end of the array.
Example:
Consider an array:
arr = [10, 20, 30, 40, 50]
If we search for 30, the algorithm will:
1. Compare 10 with 30 → No match.
2. Compare 20 with 30 → No match.
3. Compare 30 with 30 → Match found at index 2.
2. Binary Search (Efficient Search for Sorted Arrays)
Works only on sorted arrays (in increasing or decreasing order).
Uses a divide and conquer approach.
It repeatedly divides the search space in half until the target element is
found.
How Binary Search Works?
1. Find the middle element of the array.
2. If the target is equal to the middle element, return its index.
3. If the target is less than the middle element, search the left half.
4. If the target is greater than the middle element, search the right half.
5. Repeat until the element is found or the search space is empty.
Example:
Consider a sorted array:
arr = [10, 20, 30, 40, 50]
If we search for 30:
1. Middle element = 30 → Match found!
2. The search ends in just one step, making it much faster than linear
search.
Week 4 & 5
Linked list Data Structure
A linked list is a linear data structure that includes a series of connected
nodes. Here, each node stores the data and the address of the next node.
For example,
Linked lists can be of multiple types: singly, doubly, and circular linked list.
In this article, we will focus on the singly linked list. To learn about other
types, visit Types of Linked List.
Note: You might have played the game Treasure Hunt, where each clue
includes the information about the next clue. That is how the linked list
operates.
A data item
We wrap both the data item and the next node reference in a struct as:
struct node
{
int data;
struct node *next;
};
Understanding the structure of a linked list node is the key to having a grasp
on it.
Each struct node has a data item and a pointer to another struct node. Let us
create a simple Linked List with three items to understand how this works.
/* Initialize nodes */
struct node *head;
struct node *one = NULL;
struct node *two = NULL;
struct node *three = NULL;
/* Allocate memory */
one = malloc(sizeof(struct node));
two = malloc(sizeof(struct node));
three = malloc(sizeof(struct node));
/* Connect nodes */
one->next = two;
two->next = three;
three->next = NULL;
If you didn't understand any of the lines above, all you need is a refresher
on pointers and structs.
In just a few steps, we have created a simple linked list with three nodes.
Point its next pointer to the struct node containing 2 as the data value
Doing something similar in an array would have required shifting the positions
of all the subsequent elements.
In python and Java, the linked list can be implemented using classes as
shown in the codes below.
Apart from that, linked lists are a great way to learn how pointers work. By
practicing how to manipulate linked lists, you can prepare yourself to learn
more advanced data structures like graphs and trees.
Linked List Implementations in Python, Java,
C, and C++ Examples
Python
Java
C++
class Node:
# Creating a node
def __init__(self, item):
self.item = item
self.next = None
class LinkedList:
def __init__(self):
self.head = None
if __name__ == '__main__':
linked_list = LinkedList()
# Connect nodes
linked_list.head.next = second
second.next = third
Here's a list of basic linked list operations that we will cover in this article.
struct node {
int data;
struct node *next;
};
Traverse a Linked List
Displaying the contents of a linked list is very simple. We keep moving the
temp node to the next one and display its contents.
When temp is NULL , we know that we have reached the end of the linked list so
we get out of the while loop.
Store data
Change next of new node to point to head
Store data
temp->next = newNode;
head = head->next;
temp->next = temp->next->next;
In each iteration, check if the key of the node is equal to item . If it the key
matches the item, return true otherwise return false .
// Search a node
bool searchNode(struct Node** head_ref, int key) {
struct Node* current = *head_ref;
6. Check if the data of the current node is greater than the next node. If it is
greater, swap current and index .
Check the article on bubble sort for better understanding of its working.
if (head_ref == NULL) {
return;
} else {
while (current != NULL) {
// index points to the node next to current
index = current->next;
while (index != NULL) {
if (current->data > index->data) {
temp = current->data;
current->data = index->data;
index->data = temp;
}
index = index->next;
}
current = current->next;
}
}
}
struct node {
int data;
struct node *next;
}
/* Initialize nodes */
struct node *head;
struct node *one = NULL;
struct node *two = NULL;
struct node *three = NULL;
/* Allocate memory */
one = malloc(sizeof(struct node));
two = malloc(sizeof(struct node));
three = malloc(sizeof(struct node));
/* Connect nodes */
one->next = two;
two->next = three;
three->next = NULL;
A node is represented as
struct node {
int data;
struct node *next;
struct node *prev;
}
/* Initialize nodes */
struct node *head;
struct node *one = NULL;
struct node *two = NULL;
struct node *three = NULL;
/* Allocate memory */
one = malloc(sizeof(struct node));
two = malloc(sizeof(struct node));
three = malloc(sizeof(struct node));
/* Connect nodes */
one->next = two;
one->prev = NULL;
two->next = three;
two->prev = one;
three->next = NULL;
three->prev = two;
If you want to learn more about it, please visit doubly linked list and operations
on it.
for singly linked list, next pointer of last item points to the first item
In the doubly linked list, prev pointer of the first item points to the last item as
well.
A three-member circular singly linked list can be created as:
/* Initialize nodes */
struct node *head;
struct node *one = NULL;
struct node *two = NULL;
struct node *three = NULL;
/* Allocate memory */
one = malloc(sizeof(struct node));
two = malloc(sizeof(struct node));
three = malloc(sizeof(struct node));
/* Connect nodes */
one->next = two;
two->next = three;
three->next = one;
week 6
Stack Data Structure
A stack is a linear data structure that follows the principle of Last In First Out
(LIFO). This means the last element inserted inside the stack is removed first.
You can think of the stack data structure as the pile of plates on top of
another.
Stack representation
similar to a pile of plate
And, if you want the plate at the bottom, you must first remove all the plates
on top. This is exactly how the stack data structure works.
In the above image, although item 3 was kept last, it was removed first. This is
exactly how the LIFO (Last In First Out) Principle works.
We can implement a stack in any programming language like C, C++, Java,
Python or C#, but the specification is pretty much the same.
1. A pointer called TOP is used to keep track of the top element in the stack.
2. When initializing the stack, we set its value to -1 so that we can check if the
stack is empty by comparing TOP == -1 .
3. On pushing an element, we increase the value of TOP and place the new
element in the position pointed to by TOP .
#include <stdlib.h>
#include <iostream>
#define MAX 10
int size = 0;
// Creating a stack
struct stack {
int items[MAX];
int top;
};
typedef struct stack st;
// Driver code
int main() {
int ch;
st *s = (st *)malloc(sizeof(st));
createEmptyStack(s);
push(s, 1);
push(s, 2);
push(s, 3);
push(s, 4);
printStack(s);
pop(s);
To reverse a word - Put all the letters in a stack and pop them out. Because
of the LIFO order of stack, you will get the letters in reverse order.
In compilers - Compilers use the stack to calculate the value of expressions
like 2 + 4 / 5 * (7 - 9) by converting the expression to prefix or postfix form.
In browsers - The back button in a browser saves all the URLs you have
visited previously in a stack. Each time you visit a new page, it is added on top
of the stack. When you press the back button, the current URL is removed
from the stack, and the previous URL is accessed.
Introduction to Recursion
The process in which a function calls itself directly or indirectly is called
recursion and the corresponding function is called a recursive function.
A recursive algorithm takes one step toward solution and then recursively
call itself to further move. The algorithm stops once we reach the solution.
Since called function may further call itself, this process might continue
forever. So it is essential to provide a base case to terminate this
recursion process.
Need of Recursion
Recursion helps in logic building. Recursive thinking helps in solving
complex problems by breaking them into smaller subproblems.
Recursive solutions work as a a basis for Dynamic Programming and
Divide and Conquer algorithms.
Certain problems can be solved quite easily using recursion like Towers
of Hanoi (TOH), Inorder/Preorder/Postorder Tree Traversals, DFS of
Graph, etc.
Steps to Implement Recursion
Step1 – Define a base case: Identify the simplest (or base) 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.
Week 7
Queue Data Structure
A queue is a useful data structure in programming. It is similar to the ticket
queue outside a cinema hall, where the first person entering the queue is the
first person who gets the ticket.
Queue follows the First In First Out (FIFO) rule - the item that goes in first is
the item that comes out first.
In the above image, since 1 was kept in the queue before 2, it is the first to be
removed from the queue as well. It follows the FIFO rule.
In programming terms, putting items in the queue is called enqueue, and
removing items from the queue is called dequeue.
We can implement the queue in any programming language like C, C++,
Java, Python or C#, but the specification is pretty much the same.
Working of Queue
Queue operations work as follows:
Dequeue Operation
check if the queue is empty
Python
Java
C++
#include <iostream>
#define SIZE 5
class Queue {
private:
int items[SIZE], front, rear;
public:
Queue() {
front = -1;
rear = -1;
}
bool isFull() {
if (rear == SIZE - 1) {
return true;
}
return false;
}
bool isEmpty() {
if (front == -1)
return true;
else
return false;
}
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++;
}
cout << endl
<< "Deleted -> " << element << endl;
return (element);
}
}
void display() {
/* Function to display elements of Queue */
int i;
if (isEmpty()) {
cout << endl
<< "Empty Queue" << endl;
} else {
cout << endl
<< "Front index-> " << front;
cout << endl
<< "Items -> ";
for (i = front; i <= rear; i++)
cout << items[i] << " ";
cout << endl
<< "Rear index-> " << rear << endl;
}
}
};
int main() {
Queue q;
//enQueue 5 elements
q.enQueue(1);
q.enQueue(2);
q.enQueue(3);
q.enQueue(4);
q.enQueue(5);
q.display();
return 0;
}
Limitations of Queue
As you can see in the image below, after a bit of enqueuing and dequeuing,
the size of the queue has been reduced.
Limitation of a queue
And we can only add indexes 0 and 1 only when the queue is reset (when all
the elements have been dequeued).
After REAR reaches the last index, if we can store extra elements in the empty
spaces (0 and 1), we can make use of the empty spaces. This is implemented
by a modified queue called the circular queue.
Complexity Analysis
The complexity of enqueue and dequeue operations in a queue using an array
is O(1) . If you use pop(N) in python code, then the complexity might
be O(n) depending on the position of the item to be popped.
Applications of Queue
CPU scheduling, Disk Scheduling
Call Center phone systems use Queues to hold people calling them in order.
Types of Queues
A queue is a useful data structure in programming. It is similar to the ticket
queue outside a cinema hall, where the first person entering the queue is the
first person who gets the ticket.
There are four different types of queues:
Simple Queue
Circular Queue
Priority Queue
Simple Queue
In a simple queue, insertion takes place at the rear and removal occurs at the
front. It strictly follows the FIFO (First in First out) rule.
Circular Queue
In a circular queue, the last element points to the first element making a
circular link.
Circular Queue
Representation
The main advantage of a circular queue over a simple queue is better memory
utilization. If the last position is full and the first position is empty, we can
insert an element in the first position. This action is not possible in a simple
queue.
Priority Queue
Representation
Insertion occurs based on the arrival of the values and removal occurs based
on priority.
Deque 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.
Here, the circular increment is performed by modulo division with the queue
size. That is,
2. Dequeue Operation
check if the queue is empty
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.
Operations
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.
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.
If there is no node,
create a newNode.
insert the newNode at the end (last node from left to right.)
For Min Heap, the above algorithm is modified so that parentNode is always
smaller than newNode .
remove noteToBeDeleted
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.
return rootNode
Java
C++
arr = []
insert(arr, 3)
insert(arr, 4)
insert(arr, 9)
insert(arr, 5)
insert(arr, 2)
deleteNode(arr, 4)
print("After deleting an element: " + str(arr))
Dijkstra's algorithm
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.
-1 .
4. Else if front is at the last index (i.e. front = n - 1 ), set front = 0 .
Java
C++
class Deque:
def __init__(self):
self.items = []
def isEmpty(self):
return self.items == []
def removeFront(self):
return self.items.pop(0)
def removeRear(self):
return self.items.pop()
def size(self):
return len(self.items)
d = Deque()
print(d.isEmpty())
d.addRear(8)
d.addRear(5)
d.addFront(7)
d.addFront(10)
print(d.size())
print(d.isEmpty())
d.addRear(11)
print(d.removeRear())
print(d.removeFront())
d.addFront(55)
d.addRear(45)
print(d.items)
Time Complexity
The time complexity of all the above operations is constant i.e. O(1) .
Applications of Deque Data Structure
1. In undo operations on software.
Hash Table
The Hash table data structure stores elements in key-value pairs where
Hash Collision
When the hash function generates the same index for multiple keys, there will
be a conflict (what value to be stored in that index). This is called a hash
collision.
We can resolve the hash collision using one of the following techniques.
If j is the slot for multiple elements, it contains a pointer to the head of the list
of elements. If no element is present, j contains NIL .
chainedHashSearch(T, k)
return T[h(k)]
chainedHashInsert(T, x)
T[h(x.key)] = x //insert at the head
chainedHashDelete(T, x)
T[h(x.key)] = NIL
2. Open Addressing
Unlike chaining, open addressing doesn't store multiple elements into the
same slot. Here, each slot is either filled with a single key or left NIL .
i. Linear Probing
In linear probing, collision is resolved by checking the next slot.
where
i = {0, 1, ….}
where,
Here, we will look into different methods to find a good hash function
1. Division Method
If k is a key and m is the size of the hash table, the hash function h() is
calculated as:
h(k) = k mod m
For example, If the size of a hash table is 10 and k = 112 then h(k) =
112 mod 10 = 2 . The value of m must not be the powers of 2 . This is because
the powers of 2 in binary format are 10, 100, 1000, … . When we find k mod m ,
2. Multiplication Method
h(k) = ⌊m(kA mod 1)⌋
where,
Java
C
C++
# Initialize the hash table with 10 empty lists (each index is a list to handle
collisions)
hashTable = [[] for _ in range(10)]
def checkPrime(n):
if n == 1 or n == 0:
return 0
return 1
def getPrime(n):
if n % 2 == 0:
n = n + 1
return n
def hashFunction(key):
capacity = getPrime(10)
return key % capacity
def removeData(key):
index = hashFunction(key)
# Remove the key-value pair from the list if it exists
for i, kv in enumerate(hashTable[index]):
if kv[0] == key:
del hashTable[index][i]
break
print(hashTable)
removeData(123)
print(hashTable)
cryptographic applications
A Tree
Tree Terminologies
Node
A node is an entity that contains a key or value and pointers to its child nodes.
The last nodes of each path are called leaf nodes or external nodes that do
not contain a link/pointer to child nodes.
The node having at least a child node is called an internal node.
Edge
It is the link between any two nodes.
Root
It is the topmost node of a tree.
Height of a Node
The height of a node is the number of edges from the node to the deepest leaf
(ie. the longest path from the node to a leaf node).
Depth of a Node
The depth of a node is the number of edges from the root to the node.
Height of a Tree
The height of a Tree is the height of the root node or the depth of the deepest
node.
Degree of a Node
The degree of a node is the total number of branches of that node.
Forest
A collection of disjoint trees is called a forest.
Creating forest from a
tree
Types of Tree
1. Binary Tree
2. Binary Search Tree
3. AVL Tree
4. B-Tree
Tree Traversal
In order to perform any operation on a tree, you need to reach to the specific
node. The tree traversal algorithm helps in visiting a required node in the tree.
Most popular databases use B-Trees and T-Trees, which are variants of the
tree structure we learned above to store their data
Compilers use a syntax tree to validate the syntax of every program you write.
Linear data structures like arrays, stacks, queues, and linked list have only
one way to read the data. But a hierarchical data structure like a tree can be
traversed in different ways.
Tree traversal
Let's think about how we can read the elements of the tree in the image
shown above.
Instead, we use traversal methods that take into account the basic structure of
a tree i.e.
struct node {
int data;
struct node* left;
struct node* right;
}
The struct node pointed to by left and right might have other left and right
children so we should think of them as sub-trees instead of sub-nodes.
According to this structure, every tree is a combination of
Two subtrees
Remember that our goal is to visit each node, so we need to visit all the nodes
in the subtree, visit the root node and visit all the nodes in the right subtree as
well.
inorder(root->left)
display(root->data)
inorder(root->right)
Preorder traversal
1. Visit root node
display(root->data)
preorder(root->left)
preorder(root->right)
Postorder traversal
1. Visit all the nodes in the left subtree
2. Visit all the nodes in the right subtree
postorder(root->left)
postorder(root->right)
display(root->data)
We traverse the left subtree first. We also need to remember to visit the root
node and the right subtree when this tree is done.
Since the node "5" doesn't have any subtrees, we print it directly. After that we
print its parent "12" and then the right child "6".
Putting everything on a stack was helpful because now that the left-subtree of
the root node has been traversed, we can print it and go to the right subtree.
After going through all the elements, we get the inorder traversal as
We don't have to create the stack ourselves because recursion maintains the
correct order for us.
Binary Tree
A binary tree is a tree data structure in which each parent node can have at
most two children. Each node of a binary tree consists of three items:
data item
Binary Tree
3. The last leaf element might not have a right sibling i.e. a complete binary tree
doesn't have to be a full binary tree.
struct node
{
int data;
struct node *left;
struct node *right;
};
In router algorithms
2λ - 1
7. The number of leaves is at most .
2. If a node has h > 0, it is a perfect binary tree if both of its subtrees are of
height h - 1 and are non-overlapping.
Perfect
Binary Tree (Recursive Representation)
A complete binary tree is just like a full binary tree, but with two major
differences
2. The last leaf element might not have a right sibling i.e. a complete binary tree
doesn't have to be a full binary tree.
Complete Binary Tree
2. Put the second element as a left child of the root node and the third element
as the right child. (no. of elements on level-II: 2)
12 as a left child and 9
as a right child
3. Put the next two elements as children of the left node of the second level.
Again, put the next two elements as children of the right node of the second
level (no. of elements on level-III: 4) elements).
To learn more about the height of a tree/node, visit Tree Data Structure.
Following are the conditions for a height-balanced binary tree:
1. difference between the left and the right subtree for any node is not more than
one
It is called a binary tree because each tree node has a maximum of two
children.
It is called a search tree because it can be used to search for the presence of
a number in O(log(n)) time.
The properties that separate a binary search tree from a regular binary tree is
1. All nodes of left subtree are less than the root node
2. All nodes of right subtree are more than the root node
3. Both subtrees of each node are also BSTs i.e. they have the above two
properties
A tree having a right subtree with one value smaller than the root is shown to demonstrate that it
is not a valid binary search tree
The binary tree on the right isn't a binary search tree because the right
subtree of the node "3" contains a value smaller than it.
There are two basic operations that you can perform on a binary search tree:
Search Operation
The algorithm depends on the property of BST that if each left subtree has
values below root and each right subtree has values above the root.
If the value is below the root, we can say for sure that the value is not in the
right subtree; we need to only search in the left subtree and if the value is
above the root, we can say for sure that the value is not in the left subtree; we
need to only search in the right subtree.
Algorithm:
If root == NULL
return NULL;
If number == root->data
return root->data;
If number < root->data
return search(root->left)
If number > root->data
return search(root->right)
If the value is found, we return the value so that it gets propagated in each
recursion step as shown in the image below.
If you might have noticed, we have called return search(struct node*) four
times. When we return either the new node or NULL, the value gets returned
again and again until search(root) returns the final result.
If the value is found in any of the subtrees, it is propagated up so that in the end it is returned,
otherwise null is returned
If the value is not found, we eventually reach the left or right child of a leaf
node which is NULL and it gets propagated and returned.
Insert Operation
Inserting a value in the correct position is similar to searching because we try
to maintain the rule that the left subtree is lesser than root and the right
subtree is larger than root.
We keep going to either right subtree or left subtree depending on the value
and when we reach a point left or right subtree is null, we put the new node
there.
Algorithm:
If node == NULL
return createNode(data)
if (data < node->data)
node->left = insert(node->left, data);
else if (data > node->data)
node->right = insert(node->right, data);
return node;
The algorithm isn't as simple as it looks. Let's try to visualize how we add a
number to an existing BST.
We have attached the node but we still have to exit from the function without
doing any damage to the rest of the tree. This is where the return node; at the
end comes in handy. In the case of NULL , the newly created node is returned
and attached to the parent node, otherwise the same node is returned without
any change as we go up until we return to the root.
This makes sure that as we move back up the tree, the other node
connections aren't changed.
Image showing the importance of returning the root element at the end so that the elements
don't lose their position during the upward recursion step.
Deletion Operation
There are three cases for deleting a node from a binary search tree.
Case I
In the first case, the node to be deleted is the leaf node. In such a case,
simply delete the node from the tree.
4 is to be deleted
6 is to be deleted
copy the value of its child to the node and delete the child
Final tree
Case III
In the third case, the node to be deleted has two children. In such a case
follow the steps below:
3 is to be deleted
Copy the value of the inorder successor (4) to the node
Java
C++
# Create a node
class Node:
def __init__(self, key):
self.key = key
self.left = None
self.right = None
# Inorder traversal
def inorder(root):
if root is not None:
# Traverse left
inorder(root.left)
# Traverse root
print(str(root.key) + "->", end=' ')
# Traverse right
inorder(root.right)
# Insert a node
def insert(node, key):
return node
return current
# Deleting a node
def deleteNode(root, key):
root.key = temp.key
return root
root = None
root = insert(root, 8)
root = insert(root, 3)
root = insert(root, 1)
root = insert(root, 6)
root = insert(root, 7)
root = insert(root, 10)
root = insert(root, 14)
root = insert(root, 4)
print("\nDelete 10")
root = deleteNode(root, 10)
print("Inorder traversal: ", end=' ')
inorder(root)
AVL Tree
AVL tree is a self-balancing binary search tree in which each node maintains
extra information called a balance factor whose value is either -1, 0 or +1.
AVL tree got its name after its inventor Georgy Adelson-Velsky and Landis.
Balance Factor
Balance factor of a node in an AVL tree is the difference between the height of
the left subtree and that of the right subtree of that node.
The self balancing property of an avl tree is maintained by the balance factor.
The value of balance factor should always be -1, 0 or +1.
Left Rotate
In left-rotation, the arrangement of the nodes on the right is transformed into
the arrangements on the left node.
Algorithm
1. Let the initial tree be: Left rotate
Right Rotate
In right-rotation, the arrangement of the nodes on the left is transformed into
the arrangements on the right node.
In right-left rotation, the arrangements are first shifted to the right and then to
the left.
1. Do right rotation on x-y.
Finding the
location to insert newNode
3. Compare leafKey obtained from the above steps with newKey :
a. If balanceFactor > 1, it means the height of the left subtree is greater than that
of the right subtree. So, do a right rotation or left-right rotation
a. If newNodeKey < leftChildKey do right rotation.
b. Else, do left-right rotation.
b. If balanceFactor < -1, it means the height of the right subtree is greater than
that of the left subtree. So, do right rotation or right-left rotation
a. If newNodeKey > rightChildKey do left rotation.
b. Else, do right-left rotation
6. The final tree is: Final
balanced tree
a. If nodeToBeDeleted is the leaf node (ie. does not have any child), then
remove nodeToBeDeleted .
Update bf
4. Rebalance the tree if the balance factor of any of the nodes is not equal to -1,
0 or 1.
Java
C++
import sys
class AVLTree(object):
root.height = 1 + max(self.getHeight(root.left),
self.getHeight(root.right))
return root
balanceFactor = self.getBalance(root)
myTree = AVLTree()
root = None
nums = [33, 13, 52, 9, 21, 61, 8, 11]
for num in nums:
root = myTree.insert_node(root, num)
myTree.printHelper(root, "", True)
key = 13
root = myTree.delete_node(root, key)
print("After Deletion: ")
myTree.printHelper(root, "", True)
Heap Operations
Some of the important operations performed on a heap are described below
along with their algorithms.
Heapify
Heapify is the process of creating a heap data structure from a binary tree. It
is used to create a Min-Heap or a Max-Heap.
1. Let the input array be
Initial Array
Complete
binary tree
3. Start from the first index of non-leaf node whose index is given by n/2 - 1 .
5. The index of left child is given by 2i + 1 and the right child is given by 2i + 2 .
Swap if
necessary
7. Repeat steps 3-7 until the subtrees are also heapified.
Algorithm
Heapify(array, size, i)
set i as largest
leftChild = 2i + 1
rightChild = 2i + 2
MaxHeap(array, size)
loop from the first index of non-leaf node down to zero
call heapify
For Min-Heap, both leftChild and rightChild must be larger than the parent for
all nodes.
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.)
Insert
at the end
Select
the element to be deleted
2. Swap it with the last element.
Swap
with the last element
3. Remove the last element.
return rootNode
Extract-Max/Min
Extract-Max returns the node with maximum value after removing it from a
Max Heap whereas Extract-Min returns the node with minimum after removing
it from Min Heap.
Week 11
Graph Data Stucture
A graph data structure is a collection of nodes that have data and are
connected to other nodes.
Every relationship is an edge from one node to another. Whether you post a
photo, join a group, like a page, etc., a new edge is created for that
relationship.
Example of graph data structure
All of facebook is then a collection of these nodes and edges. This is because
facebook uses a graph data structure to store its data.
A collection of vertices V
V = {0, 1, 2, 3}
G = {V, E}
Graph Terminology
Adjacency: A vertex is said to be adjacent to another vertex if there is an
edge connecting them. Vertices 2 and 3 are not adjacent because there is no
edge between them.
Path: A sequence of edges that allows you to go from vertex A to vertex B is
called a path. 0-1, 1-2 and 0-2 are paths from vertex 0 to vertex 2.
Directed Graph: A graph in which an edge (u,v) doesn't necessarily mean
that there is an edge (v, u) as well. The edges in such a graph are
represented by arrows to show the direction of the edge.
Graph Representation
Graphs are commonly represented in two ways:
1. Adjacency Matrix
An adjacency matrix is a 2D array of V x V vertices. Each row and column
represent a vertex.
Since it is an undirected graph, for edge (0,2), we also need to mark edge
(2,0); making the adjacency matrix symmetric about the diagonal.
2. Adjacency List
An adjacency list represents a graph as an array of linked lists.
The index of the array represents a vertex and each element in its linked list
represents the other vertices that form an edge with the vertex.
The adjacency list for the graph we made in the first example is as follows:
Graph Operations
The most common graph operations are:
Graph Traversal
Undirected Graph
Connected Graph
Spanning tree
A spanning tree is a sub-graph of an undirected connected graph, which
includes all the vertices of the graph with a minimum possible number of
edges. If a vertex is missed, then it is not a spanning tree.
The total number of spanning trees with n vertices that can be created from a
complete graph is equal to n(n-2) .
Normal graph
Some of the possible spanning trees that can be created from the above
graph are:
Weighted graph
The minimum spanning tree from the above spanning trees is:
Minimum spanning tree
The minimum spanning tree from a graph is found using the following
algorithms:
1. Prim's Algorithm
2. Kruskal's Algorithm
Cluster Analysis
1. Visited
2. Not Visited
The purpose of the algorithm is to mark each vertex as visited while avoiding
cycles.
2. Take the top item of the stack and add it to the visited list.
3. Create a list of that vertex's adjacent nodes. Add the ones which aren't in the
visited list to the top of the stack.
4. Keep repeating steps 2 and 3 until the stack is empty.
We start from vertex 0, the DFS algorithm starts by putting it in the Visited list
and putting all its adjacent vertices in the stack.
Visit the element and put it in the visited list
Next, we visit the element at the top of stack i.e. 1 and go to its adjacent
nodes. Since 0 has already been visited, we visit 2 instead.
Visit the element at the top of stack
Vertex 2 has an unvisited adjacent vertex in 4, so we add that to the top of the
stack and visit it.
Vertex 2 has an unvisited adjacent vertex in 4, so we add that to the top of the stack and visit it.
Vertex 2 has an unvisited adjacent vertex in 4, so we add that to the top of the stack and visit it.
After we visit the last element 3, it doesn't have any unvisited adjacent nodes,
so we have completed the Depth First Traversal of the graph.
After we visit the last element 3, it doesn't have any unvisited adjacent nodes, so we have
completed the Depth First Traversal of the graph.
DFS(G, u)
u.visited = true
if v.visited == false
DFS(G,v)
init() {
For each u ∈ G
u.visited = false
For each u ∈ G
DFS(G, u)
Python
Java
C++
# DFS algorithm in Python
# DFS algorithm
def dfs(graph, start, visited=None):
if visited is None:
visited = set()
visited.add(start)
print(start)
dfs(graph, '0')
BFS algorithm
A standard BFS implementation puts each vertex of the graph into one of two
categories:
1. Visited
2. Not Visited
The purpose of the algorithm is to mark each vertex as visited while avoiding
cycles.
1. Start by putting any one of the graph's vertices at the back of a queue.
2. Take the front item of the queue and add it to the visited list.
3. Create a list of that vertex's adjacent nodes. Add the ones which aren't in the
visited list to the back of the queue.
The graph might have two different disconnected parts so to make sure that
we cover every vertex, we can also run the BFS algorithm on every node
BFS example
Let's see how the Breadth First Search algorithm works with an example. We
use an undirected graph with 5 vertices.
We start from vertex 0, the BFS algorithm starts by putting it in the Visited list
and putting all its adjacent vertices in the queue.
Visit start vertex and add its adjacent vertices to queue
Next, we visit the element at the front of queue i.e. 1 and go to its adjacent
nodes. Since 0 has already been visited, we visit 2 instead.
Visit
the first neighbour of start node 0, which is 1
Vertex 2 has an unvisited adjacent vertex in 4, so we add that to the back of
the queue and visit 3, which is at the front of the queue.
Only 4 remains in the queue since the only adjacent node of 3 i.e. 0 is already
visited. We visit it.
Visit last remaining item in
the queue to check if it has unvisited neighbors
Since the queue is empty, we have completed the Breadth First Traversal of
the graph.
BFS pseudocode
create a queue Q
while Q is non-empty