Professional Documents
Culture Documents
Data structure is a storage that is used to store and organize data. It is a way of
arranging data on a computer so that it can be accessed and updated efficiently.
What is an Algorithm?
In computer programming terms, an algorithm is a set of well-defined instructions to solve a
particular problem. It takes a set of input and produces a desired output. For example,
An algorithm to add two numbers:
1. Take two number inputs
2. Add numbers using the + operator
3. Display the result
Qualities of Good Algorithms
Input and output should be defined precisely.
Each step in the algorithm should be clear and unambiguous.
Algorithms should be most effective among many different ways to solve a problem.
An algorithm shouldn't include computer code. Instead, the algorithm should be written in
such a way that it can be used in different programming languages.
Why Learn Data Structures and Algorithms? (programiz.com)
1.2 Data Structures: Definition and Types
Types of Data Structure
Basically, data structures are divided into two categories:
It works just like a queue of people in the ticket counter where first person
on the queue will get the ticket first.
https://www.programiz.com/dsa/data-structure-types
Unlike linear data structures, elements in non-linear data structures are not in any
sequence. Instead they are arranged in a hierarchical manner where one element will be
connected to one or more elements.
Non-linear data structures are further divided into graph and tree based data structures.
a. Graph Data Structure
In graph data structure, each node is called vertex and each vertex is connected to other
vertices through edges.
Similar to a graph, a tree is also a collection of vertices and edges. However, in tree data
structure, there can only be one edge between two vertices.
Topics:
Array
Linked List
Stack
Queue
Binary Tree
Binary Search Tree
Heap
Hashing
Graph
Matrix
Misc
Advanced Data Structure
All the items are present on the single The data items are present at
layer. different layers.
The time complexity increase with the Time complexity remains the
data size. same.
Assignment 1
Write a case study about implication of binary tree and binary search tree.
1. Introduction
2. Advantage of binary tree and binary search tree
3. Use of binary tree and binary search tree
4. Literature review about binary tree and binary search tree
5. Finding about binary tree and binary search tree (conclusion)
6. References
To make presentation slide about this case study and demonstrate in class
presentation.
Assignment 2
To read and mention any three article, journals and book about dynamic memory.
List ADT
The data is generally stored in key sequence in a list which has a head structure
consisting of count, pointers and address of compare function needed to compare the
data in the list.
The data node contains the pointer to a data structure and a self-referential
pointer which points to the next node in the list.
public:
Triangle(double a_in, double b_in, double c_in);
void scale(double s) {
this->a *= s;
this->b *= s;
this->c *= s;
}
};
Stack ADT
Stack is a linear data structure that follows a particular order in which the operations are
performed. The order may be LIFO(Last In First Out) or FILO(First In Last Out).
Mainly the following four basic operations are performed in the stack:
Push: Adds an item in the stack. If the stack is full, then it is said to be an Overflow
condition.
Pop: Removes an item from the stack. The items are popped in the reversed order in
which they are pushed. If the stack is empty, then it is said to be an Underflow
condition.
Peek or Top: Returns the top element of the stack.
isEmpty: Returns true if the stack is empty, else false.
How to understand a stack practically?
There are many real-life examples of a stack. Consider the simple example of plates
stacked over one another in a canteen. The plate which is at the top is the first one to be
removed, i.e. the plate which has been placed at the bottommost position remains in the
stack for the longest period of time. So, it can be simply seen to follow the LIFO/FILO
order.
Time Complexities of operations on stack:
push(), pop(), isEmpty() and peek() all take O(1) time. We do not run any loop in any of
these operations.
Applications of stack:
Balancing of symbols
Infix to Postfix /Prefix conversion
Redo-undo features at many places like editors, photoshop.
Forward and backward feature in web browsers
Used in many algorithms like Tower of Hanoi, tree traversals, stock span
problem, histogram problem.
Backtracking is one of the algorithm designing techniques. Some examples of
backtracking are the Knight-Tour problem, N-Queen problem, find your way through a
maze, and game-like chess or checkers in all these problems we dive into someway if
that way is not efficient we come back to the previous state and go into some another
path. To get back from a current state we need to store the previous state for that
purpose we need a stack.
In Graph Algorithms like Topological Sorting and Strongly Connected Components
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
String reversal is also another application of stack. Here one by one each character
gets inserted into the stack. So the first character of the string is on the bottom of the
stack and the last element of a string is on the top of the stack. After Performing the
pop operations on the stack we get a string in reverse order.
Implementation:
There are two ways to implement a stack:
Using array
Using linked list
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);
}
return 0;
}
Queue ADT
A Queue is a linear structure which follows a particular order in which the operations are
performed. The order is First In First Out (FIFO). A good example of a queue is any queue of
consumers for a resource where the consumer that came first is served first. The difference
between stacks and queues is in removing.
1.4 Dynamic Memory: malloc, calloc, realloc and free
C is a structured language, it has some fixed rules for programming. One of them includes
changing the size of an array. An array is a collection of items stored at contiguous
memory locations.
As it can be seen that the length (size) of the array above made is 9. But what if there is a
requirement to change this length (size). For Example,
If there is a situation where only 5 elements are needed to be entered in this array. In
this case, the remaining 4 indices are just wasting memory in this array. So there is a
requirement to lessen the length (size) of the array from 9 to 5.
Take another situation. In this, there is an array of 9 elements with all 9 indices filled.
But there is a need to enter 3 more elements in this array. In this case, 3 indices more
are required. So the length (size) of the array needs to be changed from 9 to 12.
Since the size of int is 4 bytes, this statement will allocate 400 bytes of memory. And, the
pointer ptr holds the address of the first byte in the allocated memory.
#include <cstdlib>
#include <iostream>
using namespace std;
int main() {
int *ptr;
ptr = (int *)calloc(5, sizeof(int));
if (!ptr) {
cout << "Memory Allocation Failed";
exit(1);
}
cout << "Initializing values..." << endl
<< endl;
for (int i = 0; i < 5; i++) {
ptr[i] = i * 2 + 1;
}
cout << "Initialized values" << endl;
for (int i = 0; i < 5; i++) {
/* ptr[i] and *(ptr+i) can be used interchangeably */
cout << *(ptr + i) << endl;
}
free(ptr);
return 0;
}
Example
#include <cstdlib>
#include <iostream>
using namespace std;
int main() {
int *ptr = (int *)calloc(0, 0);
if (ptr == NULL) {
cout << "Null pointer";
} else {
cout << "Address = " << ptr << endl;
}
free(ptr);
return 0;
}
C++ free()
The free() function in C++ deallocates a block of memory previously allocated using
calloc, malloc or realloc functions, making it available for further allocations.
The free() function in C++ deallocates a block of memory previously allocated using calloc,
malloc or realloc functions, making it available for further allocations.
The free() function does not change the value of the pointer, that is it still points to
the same memory location.
Example 1
#include <iostream>
#include <cstdlib>
using namespace std;
int main()
{
int *ptr;
ptr = (int*) malloc(5*sizeof(int));
cout << "Enter 5 integers" << endl;
https://www.programiz.com/cpp-programming/library-function/cstdlib/free
C++ realloc()
The realloc() function in C++ reallocates a block of memory that was previously
allocated but not yet freed.The realloc() function reallocates memory that was
previously allocated using malloc(), calloc() or realloc() function and yet not freed
using the free() function.
If the new size is zero, the value returned depends on the implementation of the
library. It may or may not return a null pointer.
Example 1: How realloc() function works?
#include <iostream>
#include <cstdlib>
using namespace std;
int main()
{
float *ptr, *new_ptr;
ptr = (float*) malloc(5*sizeof(float));
if(ptr==NULL)
{
cout << "Memory Allocation Failed";
exit(1);
}
/* reallocating memory */
new_ptr = (float*) realloc(ptr, 10*sizeof(float));
if(new_ptr==NULL)
{
cout << "Memory Re-allocation Failed";
exit(1);
}
return 0;
}
o Input: An algorithm has some input values. We can pass 0 or some input value to an
algorithm.
o Output: We will get 1 or more output at the end of an algorithm.
o Unambiguity: An algorithm should be unambiguous which means that the
instructions in an algorithm should be clear and simple.
o Finiteness: An algorithm should have finiteness. Here, finiteness means that the
algorithm should contain a limited number of instructions, i.e., the instructions
should be countable.
o Effectiveness: An algorithm should be effective as each instruction in an algorithm
affects the overall process.
o Language independent: An algorithm must be language-independent so that the
instructions in an algorithm can be implemented in any of the languages with the
same output.
The following are the factors that we need to consider for designing an algorithm:
o Modularity: If any problem is given and we can break that problem into small-small
modules or small-small steps, which is a basic definition of an algorithm, it means
that this feature has been perfectly designed for the algorithm.
o Correctness: The correctness of an algorithm is defined as when the given inputs
produce the desired output, which means that the algorithm has been designed
algorithm. The analysis of an algorithm has been done correctly.
o Maintainability: Here, maintainability means that the algorithm should be designed
in a very simple structured way so that when we redefine the algorithm, no major
change will be done in the algorithm.
o Functionality: It considers various logical steps to solve the real-world problem.
o Robustness: Robustness means that how an algorithm can clearly define our
problem.
o User-friendly: If the algorithm is not user-friendly, then the designer will not be able
to explain it to the programmer.
o Simplicity: If the algorithm is simple then it is easy to understand.
o Extensibility: If any other algorithm designer or programmer wants to use your
algorithm then it should be extensible.
o Priori Analysis: Here, priori analysis is the theoretical analysis of an algorithm which
is done before implementing the algorithm. Various factors can be considered before
implementing the algorithm like processor speed, which has no effect on the
implementation part.
o Posterior Analysis: Here, posterior analysis is a practical analysis of an algorithm. The
practical analysis is achieved by implementing the algorithm using any programming
language. This analysis basically evaluate that how much running time and space
taken by the algorithm.
Algorithm Complexity
The performance of the algorithm can be measured in two factors:
sum=0;
// Suppose we have to calculate the sum of n numbers.
for i=1 to n
sum=sum+i;
// when the loop ends then sum holds the sum of the n numbers
return sum;
print sum
o Space complexity: An algorithm's space complexity is the amount of space required
to solve a problem and produce an output. Similar to the time complexity, space
complexity is also expressed in big O notation.
Auxiliary space: The extra space required by the algorithm, excluding the input size, is
known as an auxiliary space. The space complexity considers both the spaces, i.e., auxiliary
space, and space used by the input.
So,
Types of Algorithms
The following are the types of algorithm:
o Search Algorithm
o Sort Algorithm
data structure is a way of organizing the data efficiently and that efficiency is
measured either in terms of time or space. So, the ideal data structure is a structure
that occupies the least possible time to perform all its operation and the memory
space. Our focus would be on finding the time complexity rather than space
complexity, and by finding the time complexity, we can decide which data structure is
the best for an algorithm.
The main question arises in our mind that on what basis should we compare the time
complexity of data structures?. The time complexity can be compared based on
operations performed on them.
How to find the Time Complexity or running time for performing the operations?
The measuring of the actual running time is not practical at all. The running time to
perform any operation depends on the size of the input. Let's understand this statement
through a simple example.
Suppose we have an array of five elements, and we want to add a new element at the
beginning of the array. To achieve this, we need to shift each element towards right, and
suppose each element takes one unit of time. There are five elements, so five units of time
would be taken. Suppose there are 1000 elements in an array, then it takes 1000 units of
time to shift. It concludes that time complexity depends upon the input size.
Therefore, if the input size is n, then f(n) is a function of n that denotes the time
complexity.
f(n) = 5n2 + 6n + 12
where n is the number of instructions executed, and it depends on the size of the input.
When n=1
From the above calculation, it is observed that most of the time is taken by 12. But, we
have to find the growth rate of f(n), we cannot say that the maximum amount of time is
taken by 12. Let's assume the different values of n to find the growth rate of f(n).
n 5n2 6n 12
1 21.74 26.09 52.17%
% %
10 87.41 10.49 2.09%
% %
100 98.79 1.19% 0.02%
%
100 99.88 0.12% 0.0002
0 % %
Usually, the time required by an algorithm comes under three types:
Worst case: It defines the input for which the algorithm takes a huge time.
Best case: It defines the input for which the algorithm takes the lowest time
Asymptotic Notations
The commonly used asymptotic notations used for calculating the running time complexity
of an algorithm is given below:
It is the formal way to express the upper boundary of an algorithm running time. It
measures the worst case of time complexity or the algorithm's longest amount of time to
complete its operation. It is represented as shown below:
For example:
If f(n) and g(n) are the two functions defined for positive integers,
then f(n) = O(g(n)) as f(n) is big oh of g(n) or f(n) is on the order of g(n)) if there exists
constants c and no such that:
This implies that f(n) does not grow faster than g(n), or g(n) is an upper bound on the
function f(n). In this case, we are calculating the growth rate of the function which
eventually calculates the worst time complexity of a function, i.e., how worst an algorithm
can perform.
f(n)<=c.g(n)
First, we will replace f(n) by 2n+3 and g(n) by n.
2*1+3<=5*1
5<=5
If n=2
2*2+3<=5*2
7<=10
We know that for any value of n, it will satisfy the above condition, i.e., 2n+3<=c.n. If the
value of c is equal to 5, then it will satisfy the condition 2n+3<=c.n. We can take any value
of n starting from 1, it will always satisfy. Therefore, we can say that for some constants c
and for some constants n0, it will always satisfy 2n+3<=c.n. As it is satisfying the above
condition, so f(n) is big oh of g(n) or we can say that f(n) grows linearly. Therefore, it
concludes that c.g(n) is the upper bound of the f(n). It can be represented graphically as:
The idea of using big o notation is to give an upper bound of a particular function, and
eventually it leads to give a worst-time complexity. It provides an assurance that a
particular function does not behave suddenly as a quadratic or a cubic fashion, it just
behaves in a linear manner in a worst-case.
If we required that an algorithm takes at least certain amount of time without using an
upper bound, we use big- Ω notation i.e. the Greek letter "omega". It is used to bound the
growth of running time for large input size.
If f(n) and g(n) are the two functions defined for positive integers,
then f(n) = Ω (g(n)) as f(n) is Omega of g(n) or f(n) is on the order of g(n)) if there exists
constants c and no such that:
Is f(n)= Ω (g(n))?
f(n)>=c.g(n)
To check the above condition, we first replace f(n) by 2n+3 and g(n) by n.
2n+3>=c*n
Suppose c=1
2n+3>=n (This equation will be true for any value of n starting from 1).
As we can see in the above figure that g(n) function is the lower bound of the f(n) function
when the value of c is equal to 1. Therefore, this notation gives the fastest running time.
But, we are not more interested in finding the fastest running time, we are interested in
calculating the worst-case scenarios because we want to check our algorithm for larger
input that what is the worst time that it will take so that we can take further decision in the
further process.
Let f(n) and g(n) be the functions of n where n is the steps required to execute the program
then:
f(n)= θg(n)
c1.g(n)<=f(n)<=c2.g(n)
where the function is bounded by two limits, i.e., upper and lower limit, and f(n) comes in
between. The condition f(n)= θg(n) will be true if and only if c1.g(n) is less than or equal to
f(n) and c2.g(n) is greater than or equal to f(n). The graphical representation of theta
notation is given below:
Let's consider the same example where
f(n)=2n+3
g(n)=n
As c1.g(n) should be less than f(n) so c1 has to be 1 whereas c2.g(n) should be greater than
f(n) so c2 is equal to 5. The c1.g(n) is the lower limit of the of the f(n) while c2.g(n) is the
upper limit of the f(n).
c1.g(n)<=f(n)<=c2.g(n)
c1.n <=2n+3<=c2.n
If n=2
1*2<=2*2+3<=2*2
2<=7<=4 // for n=2, it satisfies the condition c1.g(n)<=f(n)<=c2.g(n)
Therefore, we can say that for any value of n, it satisfies the condition
c1.g(n)<=f(n)<=c2.g(n). Hence, it is proved that f(n) is big theta of g(n). So, this is the
average-case scenario which provides the realistic time complexity.
2 Stacks
2.1 Definition, Stack as ADT
Enqueue Operation
Queues maintain two data pointers, front and rear. Therefore, its operations are
comparatively difficult to implement than that of stacks.
The following steps should be taken to enqueue (insert) data into a queue −
Step 1 − Check if the queue is full.
Step 2 − If the queue is full, produce overflow error and exit.
Step 3 − If the queue is not full, increment rear pointer to point the next empty space.
Step 4 − Add data element to the queue location, where the rear is pointing.
Step 5 − return success.
Sometimes, we also check to see if a queue is initialized or not, to handle any unforeseen
situations.
Algorithm for enqueue operation
procedure enqueue(data)
step1: start to procedure enqueue s[3]={8,1,5};
step 2: if queue is full?
return or exit
step3: else
if queue is not full
increase rear place
step4: assign value enqueue
step5: to take any new space/place
step6:- end
if queue is full
return overflow
end if
rear ← rear + 1
queue*rear+ ← data
return true
end procedure
Implementation of enqueue() in C programming language −
Example
int enqueue(int data)
if(isfull())
return 0;
rear = rear + 1;
queue[rear] = data;
return 1;
end procedure
Dequeue Operation
Accessing data from the queue is a process of two tasks − access the data where front is
pointing and remove the data after access. The following steps are taken to
perform dequeue operation −
Step 1 − Check if the queue is empty.
Step 2 − If the queue is empty, produce underflow error and exit.
Step 3 − If the queue is not empty, access the data where front is pointing.
Step 4 − Increment front pointer to point to the next available data element.
Step 5 − Return success.
return data;
}
peek() operation
This function helps to see the data at the front of the queue. The algorithm of peek()
function is as follows −
Algorithm
begin procedure peek
return queue[front]
end procedure
Implementation of peek() function in C programming language −
Example
int peek() {
return queue[front];
}
isfull()
As we are using single dimension array to implement queue, we just check for the
rear pointer to reach at MAXSIZE to determine that the queue is full. In case we
maintain the queue in a circular linked-list, the algorithm will differ. Algorithm of
isfull() function −
Algorithm
begin procedure isfull
if rear equals to MAXSIZE
return true
else
return false
endif
end procedure
Implementation of isfull() function in C programming language −
Example
bool isfull() {
if(rear == MAXSIZE - 1)
return true;
else
return false;
}
isempty()
Algorithm of isempty() function −
Algorithm
begin procedure isempty
end procedure
If the value of front is less than MIN or 0, it tells that the queue is not yet initialized,
hence empty.
Here's the C programming code −
Example
bool isempty() {
if(front < 0 || front > rear)
return true;
else
return false;
}
Circular Queue: Circular Queue is just a variation of the linear queue in which front and
rear-end are connected to each other to optimize the space wastage of the Linear queue
and make it efficient.
In a normal Queue, we can insert elements until queue becomes full. But once queue
becomes full, we can not insert the next element even if there is a space in front of
queue.
Operations on Circular Queue:
class Queue
{
// Initialize front and rear
int rear, front;
// Circular Queue
int size;
int *arr;
public:
Queue(int s)
{
front = rear = -1;
size = s;
arr = new int[s];
}
else
{
rear++;
arr[rear] = value;
}
}
return data;
}
q.displayQueue();
q.enQueue(9);
q.enQueue(20);
q.enQueue(5);
q.displayQueue();
q.enQueue(20);
return 0;
}
4 Recursion
Computer programming languages allow a module or function to call itself. This technique
is known as recursion.
a function calling itself.
int function(int value) {
if(value < 1)
return;
function(value - 1);
printf("%d ",value);
}
a function that calls another function which in turn calls it again.
int function1(int value1) {
if(value1 < 1)
return;
function2(value1 - 1);
printf("%d ",value1);
}
int function2(int value2) {
function1(value2);
}
A program is called recursive when an entity calls itself. A program is call iterative
when there is a loop
// C++ program to find factorial of given number
#include<bits/stdc++.h>
using namespace std;
// using iteration
for (i = 2; i <= n; i++)
res *= i;
return res;
}
int main()
{
int num = 5;
cout << "Factorial of " << num <<
" using Recursion is: " <<
factorialUsingRecursion(5) << endl;
return 0;
}
int fib(int n)
{
if (n <= 1)
return n;
return fib(n-1) + fib(n-2);
}
int main ()
{
int n = 9;
cout << fib(n);
getchar();
return 0;
}
TOH
Tower of Hanoi is a mathematical puzzle where we have three rods and n disks. The
objective of the puzzle is to move the entire stack to another rod, obeying the following
simple rules:
1. Only one disk can be moved at a time.
2. Each move consists of taking the upper disk from one of the stacks and placing it on top
of another stack i.e. a disk can only be moved if it is the uppermost disk on a stack.
3. No disk may be placed on top of a smaller disk.
int main()
{
int n = 4; // Number of disks
towerOfHanoi(n, 'A', 'C', 'B'); // A, B and C are names of rods
return 0;
}
5 Linked lists
5.1 Definition, Linked List as ADT
A linked list is a sequence of data structures, which is connect together via links. Linked List is
a sequence of links, which contains items. Each link contains a connection to another link.
Linked list is the second most-used data structure after array. Following are the important
terms to understand the concept of Linked List.
Link − Each link of a linked list can store a data called an element.
Next − Each link of a linked list contains a link to the next link called Next.
LinkedList − A Linked List contains the connection link to the first link called First.
Linked List is an Abstract Data Type (ADT) that holds a collection of node and the nodes can
be accessed in a sequential way. When the Nodes are connect with only the next pointer the
list is called Singly Link List.
★ A linked list is a series of connected nodes, where each node is a data structure.
★ A linked list can grow or shrink in size as the program runs. This is possible
because the nodes in a linked list are dynamically allocated.
★ If new data need to be added to a linked list, the program simply allocates another
node and inserts it into the series.
★ If a particular piece of data needs to be removed from the linked list, the program
deletes the node containing that data.
★ Linked lists are among the simplest and most common data structures. They can be
used to implement other common abstract data types, including lists, stacks, queues,
and so on.
struct node {
int value;
struct node *next;
};
void insert();
void display();
void delete();
int count();
printf("\nOptions\n");
printf("1 : Insert into Linked List \n");
printf("2 : Delete from Linked List \n");
printf("3 : Display Linked List\n");
printf("4 : Count Linked List\n");
printf("Others : Exit()\n");
printf("Enter your option:");
scanf("%d", &option);
switch (option) {
case 1:
insert();
break;
case 2:
delete();
break;
case 3:
display();
break;
case 4:
count();
break;
default:
break;
}
}
return 0;
}
void insert() {
printf("\nEnter Element for Insert Linked List : \n");
scanf("%d", &data);
temp_node->value = data;
if (first_node == 0) {
first_node = temp_node;
} else {
head_node->next = temp_node;
}
temp_node->next = 0;
head_node = temp_node;
fflush(stdin);
}
void delete() {
int countvalue, pos, i = 0;
countvalue = count();
temp_node = first_node;
printf("\nDisplay Linked List : \n");
void display() {
int count = 0;
temp_node = first_node;
printf("\nDisplay Linked List : \n");
while (temp_node != 0) {
printf("# %d # ", temp_node->value);
count++;
temp_node = temp_node -> next;
}
printf("\nNo Of Items In Linked List : %d\n", count);
}
int count() {
int count = 0;
temp_node = first_node;
while (temp_node != 0) {
count++;
temp_node = temp_node -> next;
}
printf("\nNo Of Items In Linked List : %d\n", count);
return count;
}
https://www.c-lang.thiyagaraaj.com/data-structures/linked-list/simple-singly-linked-
list-example-program-in-c
5.4 Linked List Implementation of Stack and Queue
Stack can be implemented using both, arrays and linked list. The limitation in case of array is
that we need to define the size at the beginning of the implementation. This makes our Stack
static. It can also result in "Stack overflow" if we try to add elements after the array is full. So, to
alleviate this problem, we use linked list to implement the Stack so that it can grow in real time.
First, we will create our Node class which will form our Linked List. We will be using this same
Node class to implement the Queue also in the later part of this article.
Now, we will create our Stack Class. We will define a pointer, top, and initialize it to null.
So, our LinkedListStack class will be –
Node top;
public LinkListStack()
this.top = null;
Now, our Stack and Node class is ready. So, we will proceed to Push operation on Stack. We
will add a new element at the top of Stack.
Algorithm
Create a new node with the value to be inserted.
If the Stack is empty, set the next of the new node to null.
If the Stack is not empty, set the next of the new node to top.
Finally, increment the top to point to the new node.
The time complexity for Push operation is O(1). The method for Push will look like this.
if (top == null)
newNode.next = null;
else
newNode.next = top;
top = newNode;
Console.WriteLine("{0} pushed to stack", value);
Algorithm
If the Stack is empty, terminate the method as it is Stack underflow.
If the Stack is not empty, increment top to point to the next node.
Hence the element pointed by top earlier is now removed.
The time complexity for Pop operation is O(1). The method for Pop will be like following.
The peek operation will always return the top element of Stack without removing it from Stack.
Algorithm
If the Stack is empty, terminate the method as it is Stack underflow.
If the Stack is not empty, return the element pointed by the top.
The time complexity for Peek operation is O(1). The Peek method will be like following.
if (top == null)
Console.WriteLine("Stack Underflow.");
return;
Uses of Stack
Stack can be used to implement back/forward button in the browser.
Undo feature in the text editors are also implemented using Stack.
It is also used to implement recursion.
Call and return mechanism for a method uses Stack.
It is also used to implement backtracking.
Similar to Stack, the Queue can also be implemented using both, arrays and linked list. But it
also has the same drawback of limited size. Hence, we will be using a Linked list to implement
the Queue.
The Node class will be the same as defined above in Stack implementation. We will define
LinkedListQueue class as below.
Node front;
Node rear;
public LinkListQueue()
Here, we have taken two pointers - rear and front - to refer to the rear and the front end of the
Queue respectively and will initialize it to null.
Enqueue of an Element
We will add a new element to our Queue from the rear end.
Algorithm
Create a new node with the value to be inserted.
If the Queue is empty, then set both front and rear to point to newNode.
If the Queue is not empty, then set next of rear to the new node and the rear to point to
the new node.
The time complexity for Enqueue operation is O(1). The Method for Enqueue will be like the
following.
if (this.rear == null)
else
// Add the new node at the end of queue and change rear
this.rear.next = newNode;
this.rear = newNode;
Dequeue of an Element
We will delete the existing element from the Queue from the front end.
Algorithm
If the Queue is empty, terminate the method.
If the Queue is not empty, increment front to point to next node.
Finally, check if the front is null, then set rear to null also. This signifies empty Queue.
The time complexity for Dequeue operation is O(1). The Method for Dequeue will be like
following.
if (this.front == null)
return;
Uses of Queue
CPU scheduling in Operating system uses Queue. The processes ready to execute and the
requests of CPU resources wait in a queue and the request is served on first come first
serve basis.
Data buffer - a physical memory storage which is used to temporarily store data while it is
being moved from one place to another is also implemented using Queue.
6 Trees
6.1 Concept and Definition: Concept of level, depth, number of nodes
A tree is non-linear and a hierarchical data structure consisting of a collection of
nodes such that each node of the tree stores a value and a list of references to
other nodes (the “children”). It consists of a central node, structural nodes, and
sub-nodes, which are connected via edges. We can also say that tree data
structure has roots, branches, and leaves connected with one another.
A tree consists of a root, and zero or more subtrees T1, T2, … , Tk such that there
is an edge from the root of the tree to the root of each subtree.
Now, let us write the traversal sequences for this binary search tree-
Preorder Traversal-
100 , 20 , 10 , 30 , 200 , 150 , 300
Inorder Traversal-
10 , 20 , 30 , 100 , 150 , 200 , 300
Postorder Traversal-
10 , 30 , 20 , 150 , 300 , 200 , 100
There are three possible cases to consider deleting a node from BST:
Case 1: Deleting a node with no children: remove the node from the tree.
Case 2: Deleting a node with two children: call the node to be deleted N . Do not delete N . Instead,
choose either its inorder successor node or its inorder predecessor node, R . Copy the value
of R to N , then recursively call delete on R until reaching one of the first two cases. If we choose
the inorder successor of a node, as the right subtree is not NULL (our present case is a node with 2
children), then its inorder successor is a node with the least value in its right subtree, which will
have at a maximum of 1 subtree, so deleting it would fall in one of the first 2 cases.
Case 3: Deleting a node with one child: remove the node and replace it with its child.
Broadly speaking, nodes with children are harder to delete. As with all binary trees, a node’s
inorder successor is its right subtree’s leftmost child, and a node’s inorder predecessor is the left
subtree’s rightmost child. In either case, this node will have zero or one child. Delete it according
to one of the two simpler cases above.
#include <stdio.h>
#include <stdlib.h>
struct node {
int data;
struct node* left;
struct node* right;
};
temp->data = data;
temp->left = temp->right = NULL;
return temp;
}
if (low == high)
return root;
int i;
for (i = low; i <= high; ++i)
if (pre[i] > root->data)
break;
return root;
}
// Driver code
int main()
{
int pre[] = { 10, 5, 1, 7, 40, 50 };
int size = sizeof(pre) / sizeof(pre[0]);
return 0;
}
What is Tree ?
Tree is collection of nodes. A tree is a hierarchical data structure. Tree is a non-linear data structure
which contains nodes and edges.
Terminologies :
According to the above example image of tree
Nodes : 1 2 3 4 5 6 7 8 9 10 11 13 14
Root : 1
Internal Nodes : 1 2 3 4 5 6 7
External nodes : 8 9 10 11 13 14
(Parent , Child) : (1, 2 and 3), (2, 4 and 5), (3, 6 and 7),(4, 8 and 9), (5, 10 and 11) , (6, 13) , (7,14)
Siblings : (2, 3) , (4, 5), (6, 7), (8, 9), (10, 11)
Why Tree?
Unlike Array and Linked List, which are linear data structures, tree is hierarchical (or non-linear)
data structure.
One reason to use trees might be because you want to store information that naturally forms a
hierarchy. For example, the file system on a computer: file system
/ <-- root
/ \
... home
/ \
ugrad course
/ / | \
... cs101 cs112 cs113
If we organize keys in form of a tree (with some ordering e.g., BST), we can search for a given
key in moderate time (quicker than Linked List and slower than arrays). Self-balancing search
trees like AVL and Red-Black trees guarantee an upper bound of O(Logn) for search.
We can insert/delete keys in moderate time (quicker than Arrays and slower than Unordered
Linked Lists). Self-balancing search trees like AVL and Red-Black trees guarantee an upper
bound of O(Logn) for insertion/deletion.
Like Linked Lists and unlike Arrays, Pointer implementation of trees don’t have an upper
limit on number of nodes as nodes are linked using pointers.
Other Applications :
1. Store hierarchical data, like folder structure, organization structure, XML/HTML data.
2. Binary Search Tree is a tree that allows fast search, insert, delete on a sorted data. It also
allows finding closest item
3. Heap is a tree data structure which is implemented using arrays and used to implement
priority queues.
4. B-Tree and B+ Tree : They are used to implement indexing in databases.
5. Syntax Tree: Scanning, parsing , generation of code and evaluation of arithmetic expressions
in Compiler design.
6. K-D Tree: A space partitioning tree used to organize points in K dimensional space.
7. Trie : Used to implement dictionaries with prefix lookup.
8. Suffix Tree : For quick pattern searching in a fixed text.
9. Spanning Trees and shortest path trees are used in routers and bridges respectively in
computer networks
10. As a workflow for compositing digital images for visual effects.
11. Decision trees.
12. Organization chart of a large organization.
13. In XML parser.
14. Machine learning algorithm.
15. For indexing in database.
16. IN server like DNS (Domain Name Server)
17. In Computer Graphics.
18. To evaluate an expression.
19. In chess game to store defense moves of player.
20. In java virtual machine.
7 Graphs
7.1 Representation and Applications of Graph
Graphs are powerful data structures that represent real-life relationships between entities. Graphs are used
everywhere, from social networks, Google maps, and the world wide web to blockchains and
neural networks. Due to their ability to provide abstractions to real-life, they are used in a
variety of practical problems.
A graph is a non-linear data structure, which consists of vertices(or nodes) connected by
edges(or arcs) where edges may be directed or
undirected.
In Computer science graphs are used to represent the flow of computation.
Google maps uses graphs for building transportation systems, where intersection of two(or
more) roads are considered to be a vertex and the road connecting two vertices is
considered to be an edge, thus their navigation system is based on the algorithm to
calculate the shortest path between two vertices.
In Facebook, users are considered to be the vertices and if they are friends then there is an
edge running between them. Facebook’s Friend suggestion algorithm uses graph theory.
Facebook is an example of undirected graph.
In World Wide Web, web pages are considered to be the vertices. There is an edge from a
page u to other page v if there is a link of page v on page u. This is an example of Directed
graph. It was the basic idea behind Google Page Ranking Algorithm.
In Operating System, we come across the Resource Allocation Graph where each process
and resources are considered to be vertices. Edges are drawn from resources to the
allocated process, or from requesting process to the requested resource. If this leads to any
formation of a cycle then a deadlock will occur.
In mapping system we use graph. It is useful to find out which is an excellent place from
the location as well as your nearby location. In GPS we also use graphs.
Facebook uses graphs. Using graphs suggests mutual friends. it shows a list of the f
following pages, friends, and contact list.
Microsoft Excel uses DAG means Directed Acyclic Graphs.
In the Dijkstra algorithm, we use a graph. we find the smallest path between two or many
nodes.
On social media sites, we use graphs to track the data of the users. liked showing preferred
post suggestions, recommendations, etc.
7.2 Graph Traversal Algorithms: Depth First Traversal and Breadth First Traversal
Depth first traversal
Depth First Traversal (or Search) for a graph is similar to Depth First Traversal of a
tree. The only catch here is, unlike trees, graphs may contain cycles (a node may be
visited twice). To avoid processing a node more than once, use a boolean visited array.
Example:
Input: n = 4, e = 6
0 -> 1,
0 -> 2,
1 -> 2,
2 -> 0,
2 -> 3,
3 -> 3
Output: DFS from vertex 1 : 1 2 0 3
Explanation:
DFS Diagram:
Algorithm:
Create a recursive function that takes the index of the node and a visited array.
1. Mark the current node as visited and print the node.
2. Traverse all the adjacent and unmarked nodes and call the recursive function with the
index of the adjacent node
// C++ program to print DFS traversal from
// a given vertex in a given graph
#include <bits/stdc++.h>
using namespace std;
class Graph {
public:
map<int, bool> visited;
map<int, list<int> > adj;
void Graph::DFS(int v)
{
visited[v] = true;
cout << v << " ";
list<int>::iterator i;
for (i = adj[v].begin(); i != adj[v].end(); ++i)
if (!visited[*i])
DFS(*i);
}
int main()
{
Graph g;
g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 2);
g.addEdge(2, 0);
g.addEdge(2, 3);
g.addEdge(3, 3);
cout << "Following is Depth First Traversal"
" (starting from vertex 2) \n";
g.DFS(2);
return 0;
}
class Graph
{
int V;
vector<list<int>> adj;
public:
Graph(int V);
Graph::Graph(int V)
{
this->V = V;
adj.resize(V);
}
void Graph::BFS(int s)
{
vector<bool> visited;
visited.resize(V,false);
list<int> queue;
visited[s] = true;
queue.push_back(s);
while(!queue.empty())
{
s = queue.front();
cout << s << " ";
queue.pop_front();
int main()
{
Graph g(4);
g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 2);
g.addEdge(2, 0);
g.addEdge(2, 3);
g.addEdge(3, 3);
return 0;
}
7.3 Minimum Spanning Trees: Kruskal’s Algorithms
https://www.youtube.com/watch?v=plqvOcnYWkg
8.2 Comparison Sorting: Bubble, Selection, and Insertion Sort and their complexity
a. Comparison Sorting: Bubble
Bubble Sort is the simplest sorting algorithm that works by repeatedly swapping the
adjacent elements if they are in the wrong order. This algorithm is not suitable for large
data sets as its average and worst-case time complexity is quite high.
1.1.3 How Bubble Sort Works?
Consider an array arr[] = {5, 1, 4, 2, 8}
First Pass:
Bubble sort starts with very first two elements, comparing them to check which one is
greater.
( 5 1 4 2 8 ) –> ( 1 5 4 2 8 ), Here, algorithm compares the first two elements,
and swaps since 5 > 1.
( 1 5 4 2 8 ) –> ( 1 4 5 2 8 ), Swap since 5 > 4
( 1 4 5 2 8 ) –> ( 1 4 2 5 8 ), Swap since 5 > 2
( 1 4 2 5 8 ) –> ( 1 4 2 5 8 ), Now, since these elements are already in order (8
> 5), algorithm does not swap them.
Second Pass:
Now, during second iteration it should look like this:
( 1 4 2 5 8 ) –> ( 1 4 2 5 8 )
( 1 4 2 5 8 ) –> ( 1 2 4 5 8 ), Swap since 4 > 2
( 1 2 4 5 8 ) –> ( 1 2 4 5 8 )
( 1 2 4 5 8 ) –> ( 1 2 4 5 8 )
Third Pass:
Now, the array is already sorted, but our algorithm does not know if it is completed.
The algorithm needs one whole pass without any swap to know it is sorted.
( 1 2 4 5 8 ) –> ( 1 2 4 5 8 )
( 1 2 4 5 8 ) –> ( 1 2 4 5 8 )
( 1 2 4 5 8 ) –> ( 1 2 4 5 8 )
( 1 2 4 5 8 ) –> ( 1 2 4 5 8 )
b. Comparison Sorting: Selection
The selection sort algorithm sorts an array by repeatedly finding the minimum element
(considering ascending order) from unsorted part and putting it at the beginning. The
algorithm maintains two subarrays in a given array.
The subarray which is already sorted.
Remaining subarray which is unsorted.
In every iteration of selection sort, the minimum element (considering ascending order)
from the unsorted subarray is picked and moved to the sorted subarray
Lets consider the following array as an example: arr[] = {64, 25, 12, 22, 11}
First pass:
For the first position in the sorted array, the whole array is traversed from index 0 to 4
sequentially. The first position where 64 is stored presently, after traversing whole array
it is clear that 11 is the lowest value.
64 25 12 22 11
Thus, replace 64 with 11. After one iteration 11, which happens to be the least value in
the array, tends to appear in the first position of the sorted list.
11 25 12 22 64
Second Pass:
For the second position, where 25 is present, again traverse the rest of the array in a
sequential manner.
11 25 12 22 64
After traversing, we found that 12 is the second lowest value in the array and it should
appear at the second place in the array, thus swap these values.
11 12 25 22 64
Third Pass:
Now, for third place, where 25 is present again traverse the rest of the array and find
the third least value present in the array.
11 12 25 22 64
While traversing, 22 came out to be the third least value and it should appear at the
third place in the array, thus swap 22 with element present at third position.
11 12 22 25 64
Fourth pass:
Similarly, for fourth position traverse the rest of the array and find the fourth least
element in the array
As 25 is the 4th lowest value hence, it will place at the fourth position.
11 12 22 25 64
Fifth Pass:
At last the largest value present in the array automatically get placed at the last position
in the array
The resulted array is the sorted array.
11 12 22 25 64
Complexity Analysis of Selection Sort:
Time Complexity: The time complexity of Selection Sort is O(N 2) as there are two nested
loops:
One loop to select an element of Array one by one = O(N)
Another loop to compare that element with every other Array element = O(N)
Therefore overall complexity = O(N)*O(N) = O(N*N) = O(N 2)
Auxiliary Space: O(1) as the only extra memory used is for temporary variable while
swapping two values in Array. The good thing about selection sort is it never makes more
than O(n) swaps and can be useful when memory write is a costly operation.
12 11 13 5 6
First Pass:
Initially, the first two elements of the array are compared in insertion sort.
12 11 13 5 6
Here, 12 is greater than 11 hence they are not in the ascending order and 12 is not at
its correct position. Thus, swap 11 and 12.
So, for now 11 is stored in a sorted sub-array.
11 12 13 5 6
Second Pass:
Now, move to the next two elements and compare them
11 12 13 5 6
Here, 13 is greater than 12, thus both elements seems to be in ascending order, hence,
no swapping will occur. 12 also stored in a sorted sub-array along with 11
Third Pass:
Now, two elements are present in the sorted sub-array which are 11 and 12
Moving forward to the next two elements which are 13 and 5
11 12 13 5 6
Both 5 and 13 are not present at their correct place so swap them
11 12 5 13 6
After swapping, elements 12 and 5 are not sorted, thus swap again
11 5 12 13 6
5 11 12 13 6
Clearly, they are not sorted, thus perform swap between both
5 11 12 6 13
8.3 Divide and Conquer Sorting: Merge, and Quick Sort and their Complexity
Divide and Conquer is an algorithmic paradigm in which the problem is solved using the
Divide, Conquer, and Combine strategy.
A typical Divide and Conquer algorithm solves a problem using following three steps:
1. Divide: This involves dividing the problem into smaller sub-problems.
2. Conquer: Solve sub-problems by calling recursively until solved.
3. Combine: Combine the sub-problems to get the final solution of the whole problem.
1.2 Example of Divide and Conquer algorithm
A classic example of Divide and Conquer is Merge Sort demonstrated below. In Merge Sort, we divide array
into two halves, sort the two halves recursively, and then merge the sorted halves.