0% found this document useful (0 votes)
3 views214 pages

Data Structure and Algorithm Notes

An algorithm is a set of well-defined instructions for solving a problem, taking inputs and producing outputs. Good algorithms should have clear inputs and outputs, be effective, and be language-agnostic. The document also discusses data structures, their types, and asymptotic analysis for evaluating algorithm efficiency.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
3 views214 pages

Data Structure and Algorithm Notes

An algorithm is a set of well-defined instructions for solving a problem, taking inputs and producing outputs. Good algorithms should have clear inputs and outputs, be effective, and be language-agnostic. The document also discusses data structures, their types, and asymptotic analysis for evaluating algorithm efficiency.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 214

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(s) and
produces the 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 a Good Algorithm


 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.
Algorithm Examples
Algorithm to add two numbers
Algorithm to find the largest among three numbers
Algorithm to find all the roots of the quadratic equation
Algorithm to find the factorial
Algorithm to check prime number
Algorithm of Fibonacci series

Algorithm 1: Add two numbers entered by the user

Step 1: Start

Step 2: Declare variables num1, num2 and sum.

Step 3: Read values num1 and num2.

Step 4: Add num1 and num2 and assign the result to sum.

sum←num1+num2

Step 5: Display sum

Step 6: Stop
Algorithm 2: Find the largest number among three
numbers

Step 1: Start

Step 2: Declare variables a,b and c.

Step 3: Read variables a,b and c.

Step 4: If a > b

If a > c

Display a is the largest number.

Else

Display c is the largest number.

Else

If b > c

Display b is the largest number.

Else

Display c is the greatest number.

Step 5: Stop

Algorithm 3: Find Roots of a Quadratic Equation ax + 2

bx + c = 0
Step 1: Start

Step 2: Declare variables a, b, c, D, x1, x2, rp and ip;


Step 3: Calculate discriminant

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

Algorithm 4: Find the factorial of a number

Step 1: Start

Step 2: Declare variables n, factorial and i.

Step 3: Initialize variables

factorial ← 1

i ← 1

Step 4: Read value of n

Step 5: Repeat the steps until i = n

5.1: factorial ← factorial*i

5.2: i ← i+1

Step 6: Display factorial

Step 7: Stop
Algorithm 5: Check whether a number is prime or not

Step 1: Start

Step 2: Declare variables n, i, flag.

Step 3: Initialize variables

flag ← 1

i ← 2

Step 4: Read n from the user.

Step 5: Repeat the steps until i=(n/2)

5.1 If remainder of n÷i equals 0

flag ← 0

Go to step 6

5.2 i ← i+1

Step 6: If flag = 0

Display n is not prime

else

Display n is prime

Step 7: Stop
Algorithm 6: Find the Fibonacci series till the term
less than 1000

Step 1: Start

Step 2: Declare variables first_term,second_term and temp.

Step 3: Initialize variables first_term ← 0 second_term ← 1

Step 4: Display first_term and second_term

Step 5: Repeat the steps until second_term ≤ 1000

5.1: temp ← second_term

5.2: second_term ← second_term + first_term

5.3: first_term ← temp

5.4: Display second_term

Step 6: Stop

Data Structure and Types


What are Data Structures?
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.

Depending on your requirement and project, it is important to choose the right


data structure for your project. For example, if you want to store data
sequentially in the memory, then you can go for the Array data structure.
Array data Structure Representation

Note: Data structure and data types are slightly different. Data structure is the
collection of data types arranged in a specific order.

Types of Data Structure


Basically, data structures are divided into two categories:

 Linear data structure

 Non-linear data structure

Let's learn about each type in detail.

Linear data structures


In linear data structures, the elements are arranged in sequence one after the
other. Since elements are arranged in particular order, they are easy to
implement.

However, when the complexity of the program increases, the linear data
structures might not be the best choice because of operational complexities.

Popular linear data structures are:


1. Array Data Structure
In an array, elements in memory are arranged in continuous memory. All the
elements of an array are of the same type. And, the type of elements that can
be stored in the form of arrays is determined by the programming language.

To learn more, visit Java Array.

An array with each element represented by an index

2. Stack Data Structure


In stack data structure, elements are stored in the LIFO principle. That is, the
last element stored in a stack will be removed first.

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

3. Queue Data Structure


Unlike stack, the queue data structure works in the FIFO principle where first
element stored in the queue will be removed first.

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.

In a queue, addition and removal are performed from separate ends.


4. Linked List Data Structure
In linked list data structure, data elements are connected through a series of
nodes. And, each node contains the data items and address to the next node.

To learn more, visit Linked List Data Structure.

A linked list

Non linear data structures


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.

1. Graph Data Structure


In graph data structure, each node is called vertex and each vertex is
connected to other vertices through edges.

To learn more, visit Graph Data Structure.


Graph data structure example

Popular Graph Based Data Structures:


 Spanning Tree and Minimum Spanning Tree
 Strongly Connected Components
 Adjacency Matrix
 Adjacency List
2. Trees Data Structure
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.

To learn more, visit Tree Data Structure.

Tree data structure example


Popular Tree based Data Structure
 Binary Tree
 Binary Search Tree
 AVL Tree
 B-Tree
 B+ Tree
 Red-Black Tree

Linear Vs Non-linear Data Structures


Now that we know about linear and non-linear data structures, let's see the
major differences between them.
Linear Data Structures Non Linear Data Structures

The data items are arranged in


The data items are arranged in non-sequential
sequential order, one after the
order (hierarchical manner).
other.

All the items are present on the


The data items are present at different layers.
single layer.

It can be traversed on a single


run. That is, if we start from the It requires multiple runs. That is, if we start from
first element, we can traverse the first element it might not be possible to
all the elements sequentially in traverse all the elements in a single pass.
a single pass.

The memory utilization is not Different structures utilize memory in different


efficient. efficient ways depending on the need.

The time complexity increase


Time complexity remains the same.
with the data size.

Example: Arrays, Stack, Queue Example: Tree, Graph, Map

Why Data Structure?


Knowledge about data structures help you understand the working of each
data structure. And, based on that you can select the right data structures for
your project.

This helps you write memory and time efficient code.


Asymptotic Analysis: Big-O
Notation and More
The efficiency of an algorithm depends on the amount of time, storage and
other resources required to execute the algorithm. The efficiency is measured
with the help of asymptotic notations.

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.

Do you want to learn Time Complexity the right way? Enroll in


our Interactive Complexity Calculation Course for FREE.

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.

There are mainly three asymptotic notations:

 Big-O notation

 Omega notation

 Theta notation

Big-O Notation (O-notation)


Big-O notation represents the upper bound of the running time of an
algorithm. Thus, it gives the worst-case complexity of an algorithm.
Big-O gives the upper bound of a function

O(g(n)) = { f(n): there exist positive constants c and n 0


such that 0 ≤ f(n) ≤ cg(n) for all n ≥ n 0 }

The above expression can be described as a function f(n) belongs to the


set O(g(n)) if there exists a positive constant c such that it lies
between 0 and cg(n) , for sufficiently large n .
For any value of n , the running time of an algorithm does not cross the time
provided by O(g(n)) .

Since it gives the worst-case running time of an algorithm, it is widely used to


analyze an algorithm as we are always interested in the worst-case scenario.
Omega Notation (Ω-notation)
Omega notation represents the lower bound of the running time of an
algorithm. Thus, it provides the best case complexity of an algorithm.

Omega gives the lower bound of a function

Ω(g(n)) = { f(n): there exist positive constants c and n 0


such that 0 ≤ cg(n) ≤ f(n) for all n ≥ n 0 }

The above expression can be described as a function f(n) belongs to the


set Ω(g(n)) if there exists a positive constant c such that it lies above cg(n) , for

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.

Theta bounds the function within constants factors

For a function g(n) , Θ(g(n)) is given by the relation:

Θ(g(n)) = { f(n): there exist positive constants c 1, c2 and n0


such that 0 ≤ c 1g(n) ≤ f(n) ≤ c 2g(n) for all n ≥ n 0 }

The above expression can be described as a function f(n) belongs to the


set Θ(g(n)) if there exist positive constants c1 and c2 such that it can be
sandwiched between c1g(n) and c2g(n) , for sufficiently large n.
If a function f(n) lies anywhere in between c1g(n) and c2g(n) for all n ≥ n0 ,

then f(n) is said to be asymptotically tight bound.


Master Theorem
The master method is a formula for solving recurrence relations of the form:

T(n) = aT(n/b) + f(n),

where,

n = size of input

a = number of subproblems in the recursion

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

Here, a ≥ 1 and b > 1 are constants, and f(n) is an asymptotically positive


function.

An asymptotically positive function means that for a sufficiently large value


of n , we have f(n) > 0 .

The master theorem is used in calculating the time complexity of recurrence


relations (divide and conquer algorithms) in a simple and quick way.
Master Theorem
If a ≥ 1 and b > 1 are constants and f(n) is an asymptotically positive function,
then the time complexity of a recursive relation is given by

T(n) = aT(n/b) + f(n)

where, T(n) has the following asymptotic bounds:

1. If f(n) = O(n log b a-ϵ), then T(n) = Θ(n log b a).

2. If f(n) = Θ(n log b a), then T(n) = Θ(n log b a * log n).

3. If f(n) = Ω(n log b a+ϵ), then T(n) = Θ(f(n)).

ϵ > 0 is a constant.

Each of the above conditions can be interpreted as:

1. If the cost of solving the sub-problems at each level increases by a certain


factor, the value of f(n) will become polynomially smaller than nlog b a . Thus, the
time complexity is oppressed by the cost of the last level ie. nlog b a

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

3. If the cost of solving the subproblems at each level decreases by a certain


factor, the value of f(n) will become polynomially larger than nlog b a . Thus, the
time complexity is oppressed by the cost of f(n) .
Solved Example of Master Theorem
T(n) = 3T(n/2) + n2
Here,
a = 3
n/b = n/2
f(n) = n2

logb a = log 2 3 ≈ 1.58 < 2

ie. f(n) > n log b a+ϵ , where, ϵ is a constant.

Case 3 implies here.

Thus, T(n) = f(n) = Θ(n 2)

Master Theorem Limitations


The master theorem cannot be used if:

 T(n) is not monotone. eg. T(n) = sin n

 f(n) is not a polynomial. eg. f(n) = 2n

 a is not a constant. eg. a = 2n

 a < 1

Divide and Conquer Algorithm


A divide and conquer algorithm is a strategy of solving a large problem by
1. breaking the problem into smaller sub-problems

2. solving the sub-problems, and


3. combining them to get the desired output.

To use the divide and conquer algorithm, recursion is used.

How Divide and Conquer Algorithms Work?


Here are the steps involved:

1. Divide: Divide the given problem into sub-problems using recursion.


2. Conquer: Solve the smaller sub-problems recursively. If the subproblem is
small enough, then solve it directly.
3. Combine: Combine the solutions of the sub-problems that are part of the
recursive process to solve the actual problem.
Let us understand this concept with the help of an example.

Here, we will sort an array using the divide and conquer approach (ie. merge
sort).

1. Let the given array be: Array for merge sort

2. Divide the array into two halves.

Divide the array into two


subparts
Again, divide each subpart recursively into two halves until you get individual
elements.
Divide the array into smaller subparts
3. Now, combine the individual elements in a sorted manner.
Here, conquer and combine steps go side by side.

4. Combine the subparts

Time Complexity
The complexity of the divide and conquer algorithm is calculated using
the master theorem.

T(n) = aT(n/b) + f(n),

where,

n = size of input

a = number of subproblems in the recursion

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

Let us take an example to find the time complexity of a recursive problem.

For a merge sort, the equation can be written as:

T(n) = aT(n/b) + f(n)

= 2T(n/2) + O(n)

Where,

a = 2 (each time, a problem is divided into 2 subproblems)

n/b = n/2 (size of each sub problem is half of the input)

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

Now, T(n) = 2T(n log n) + O(n)

≈ 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,

Divide and Conquer approach:

fib(n)

If n < 2, return 1

Else , return f(n - 1) + f(n -2)

Dynamic approach:

mem = []

fib(n)

If n in mem: return mem[n]

else,

If n < 2, f = 1

else , f = f(n - 1) + f(n -2)


mem[n] = f

return f

In a dynamic approach, mem stores the result of each subproblem.

Advantages of Divide and Conquer Algorithm


 The complexity for the multiplication of two matrices using the naive method
is O(n3) , whereas using the divide and conquer approach (i.e. Strassen's
matrix multiplication) is O(n2.8074) . This approach also simplifies other problems,
such as the Tower of Hanoi.
 This approach is suitable for multiprocessing systems.

 It makes efficient use of memory caches.

Divide and Conquer Applications


 Binary Search
 Merge Sort
 Quick Sort
 Strassen's Matrix multiplication

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

Following are the important terms to understand the concept of Array.

 Element − Each item stored in an array is called an element.


 Index − Each location of an element in an array has a numerical index, which is
used to identify the element.

Syntax
Creating an array in C and C++ programming languages −

data_type array_name [array_size]={elements separated by commas }


or,
data_type array_name [array_size];

Creating an array in Java programming language −

data_type[] array_name = {elements separated by commas }


or,
data_type array_name = new data_type[array_size];

Need for Arrays


Arrays are used as solutions to many problems from the small sorting problems
to more complex problems like travelling salesperson problem. There are many
data structures other than arrays that provide efficient time and space
complexity for these problems, so what makes using arrays better? The answer
lies in the random access lookup time.

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

Therefore, in an array with 6 elements, to access the 1st element, array is


pointed towards the 0th index. Similarly, to access the 6 th element, array is
pointed towards the 5 th index.

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.

This indexing will be similar for the multidimensional arrays as well. If it is a 2 -


dimensional array, it will have sub-buckets in each bucket. Then it will be
indexed as array_name[m][n], where m and n are the sizes of each level in the
array.
As per the above illustration, following are the important points to be
considered.

 Index starts with 0.


 Array length is 9 which means it can store 9 elements.
 Each element can be accessed via its index. For example, we can fetch an
element at index 6 as 23.

Basic Operations in Arrays


The basic operations in the Arrays are insertion, deletion, searching, display,
traverse, and update. These operations are usually performed to either modify
the data in the array or to report the status of the array.

Following are the basic operations supported by an array.

 Traverse − print all the array elements one by one.


 Insertion − Adds an element at the given index.
 Deletion − Deletes an element at the given index.
 Search − Searches an element using the given index or by the value.
 Update − Updates an element at the given index.
 Display − Displays the contents of the array.

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

Array - Insertion Operation


In the insertion operation, we are adding one or more elements to the array.
Based on the requirement, a new element can be added at the beginning, end,
or any given index of array. This is done using input statements of the
programming languages.

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 −

C C++ Java Python

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;

//prints garbage values


cout << "Inserting elements.." <<endl;
cout << "Array After Insertion:" << endl; // prints array values
for(i = 0; i < 5; i++) {
LA[i] = i + 2;
cout << "LA[" << i <<"] = " << LA[i] << endl;
}
return 0;
}

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

For other variations of array insertion operation, click here.

Array - Deletion Operation


In this array operation, we delete an element from the particular index of an
array. This deletion operation takes place as we assign the value in the
consequent index to the current index.

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 −

C C++ Java Python

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

Array - Search Operation


Searching an element in the array using a key; The key element sequentially
compares every value in the array to check if the key is present in the array or
not.

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 −

C C++ Java Python

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

Array - Traversal Operation


This operation traverses through all the elements of an array. We use loop
statements to carry this out.

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 −

C C++ Java Python

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

Array - Update Operation


Update operation refers to updating an existing element from the array at a
given index.

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 −

C C++ Java Python

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

Array - Display Operation


This operation displays all the elements in the entire array using a print
statement.

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 list Data Structure


You have to start somewhere, so we give the address of the first node a
special name called HEAD . Also, the last node in the linked list can be identified
because its next portion points to NULL .

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.

Representation of Linked List


Let's see how each node of the linked list is represented. Each node consists:

 A data item

 An address of another node

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));

/* Assign data values */


one->data = 1;
two->data = 2;
three->data=3;

/* Connect nodes */
one->next = two;
two->next = three;
three->next = NULL;

/* Save address of first node in head */


head = one;

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.

Linked list Representation


The power of a linked list comes from the ability to break the chain and rejoin
it. E.g. if you wanted to put an element 4 between 1 and 2, the steps would
be:

 Create a new struct node and allocate memory to it.

 Add its data value as 4

 Point its next pointer to the struct node containing 2 as the data value

 Change the next pointer of "1" to the node we just created.

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.

Linked List Utility


Lists are one of the most popular and efficient data structures, with
implementation in every programming language like C, C++, Python, Java,
and C#.

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++

# Linked list implementation in Python

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()

# Assign item values


linked_list.head = Node(1)
second = Node(2)
third = Node(3)

# Connect nodes
linked_list.head.next = second
second.next = third

# Print the linked list item


while linked_list.head != None:
print(linked_list.head.item, end=" ")
linked_list.head = linked_list.head.next

Linked List Complexity


Time Complexity

Worst case Average Case

Search O(n) O(n)

Insert O(1) O(1)

Deletion O(1) O(1)

Space Complexity: O(n)

Linked List Applications


 Dynamic memory allocation

 Implemented in stack and queue

 In undo functionality of softwares


 Hash tables, Graphs
Linked List Operations:
Traverse, Insert and Delete
There are various linked list operations that allow us to perform different
actions on linked lists. For example, the insertion operation adds a new
element to the linked list.

Here's a list of basic linked list operations that we will cover in this article.

 Traversal - access each element of the linked list


 Insertion - adds a new element to the linked list
 Deletion - removes the existing elements
 Search - find a node in the linked list
 Sort - sort the nodes of the linked list
Before you learn about linked list operations in detail, make sure to know
about Linked List first.
Things to Remember about Linked List
 head points to the first node of the linked list
 next pointer of the last node is NULL , so if the next current node is NULL , we

have reached the end of the linked list.


In all of the examples, we will assume that the linked list has three nodes 1 --

->2 --->3 with node structure as below:

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.

struct node *temp = head;


printf("\n\nList elements are - \n");
while(temp != NULL) {
printf("%d --->",temp->data);
temp = temp->next;
}

The output of this program will be:

List elements are -


1 --->2 --->3 --->

Insert Elements to a Linked List


You can add elements to either the beginning, middle or end of the linked list.

1. Insert at the beginning


 Allocate memory for new node

 Store data
 Change next of new node to point to head

 Change head to point to recently created node

struct node *newNode;


newNode = malloc(sizeof(struct node));
newNode->data = 4;
newNode->next = head;
head = newNode;

2. Insert at the End


 Allocate memory for new node

 Store data

 Traverse to last node

 Change next of last node to recently created node

struct node *newNode;


newNode = malloc(sizeof(struct node));
newNode->data = 4;
newNode->next = NULL;

struct node *temp = head;


while(temp->next != NULL){
temp = temp->next;
}

temp->next = newNode;

3. Insert at the Middle


 Allocate memory and store data for new node

 Traverse to node just before the required position of new node

 Change next pointers to include new node in between


struct node *newNode;
newNode = malloc(sizeof(struct node));
newNode->data = 4;

struct node *temp = head;

for(int i=2; i < position; i++) {


if(temp->next != NULL) {
temp = temp->next;
}
}
newNode->next = temp->next;
temp->next = newNode;

Delete from a Linked List


You can delete either from the beginning, end or from a particular position.

1. Delete from beginning


 Point head to the second node

head = head->next;

2. Delete from end


 Traverse to second last element

 Change its next pointer to null

struct node* temp = head;


while(temp->next->next!=NULL){
temp = temp->next;
}
temp->next = NULL;

3. Delete from middle


 Traverse to element before the element to be deleted

 Change next pointers to exclude the node from the chain

for(int i=2; i< position; i++) {


if(temp->next!=NULL) {
temp = temp->next;
}
}

temp->next = temp->next->next;

Search an Element on a Linked List


You can search an element on a linked list using a loop using the following
steps. We are finding item on a linked list.
 Make head as the current node.
 Run a loop until the current node is NULL because the last element points
to NULL .

 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;

while (current != NULL) {


if (current->data == key) return true;
current = current->next;
}
return false;
}

Sort Elements of a Linked List


We will use a simple sorting algorithm, Bubble Sort, to sort the elements of a
linked list in ascending order below.
1. Make the head as the current node and create another node index for later use.
2. If head is null, return.
3. Else, run a loop till the last node (i.e. NULL ).

4. In each iteration, follow the following step 5-6.

5. Store the next node of current in index .

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.

// Sort the linked list


void sortLinkedList(struct Node** head_ref) {
struct Node *current = *head_ref, *index = NULL;
int temp;

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;
}
}
}

Types of Linked List - Singly


linked, doubly linked and
circular
Before you learn about the type of the linked list, make sure you know about
the LinkedList Data Structure.
There are three common types of Linked List.

1. Singly Linked List


2. Doubly Linked List
3. Circular Linked List

Singly Linked List


It is the most common. Each node has data and a pointer to the next node.
Singly linked list

Node is represented as:

struct node {
int data;
struct node *next;
}

A three-member 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));

/* Assign data values */


one->data = 1;
two->data = 2;
three->data = 3;

/* Connect nodes */
one->next = two;
two->next = three;
three->next = NULL;

/* Save address of first node in head */


head = one;
Doubly Linked List
We add a pointer to the previous node in a doubly-linked list. Thus, we can go
in either direction: forward or backward.

Doubly linked list

A node is represented as

struct node {
int data;
struct node *next;
struct node *prev;
}

A three-member doubly 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));

/* Assign data values */


one->data = 1;
two->data = 2;
three->data = 3;

/* Connect nodes */
one->next = two;
one->prev = NULL;

two->next = three;
two->prev = one;

three->next = NULL;
three->prev = two;

/* Save address of first node in head */


head = one;

If you want to learn more about it, please visit doubly linked list and operations
on it.

Circular Linked List


A circular linked list is a variation of a linked list in which the last element is
linked to the first element. This forms a circular loop.

Circular linked list

A circular linked list can be either singly linked or doubly linked.

 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));

/* Assign data values */


one->data = 1;
two->data = 2;
three->data = 3;

/* Connect nodes */
one->next = two;
two->next = three;
three->next = one;

/* Save address of first node in head */


head = 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

Here, you can:

 Put a new plate on top

 Remove the top 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.

LIFO Principle of Stack


In programming terms, putting an item on top of the stack is called push and
removing an item is called pop.

Stack Push and Pop Operations

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.

Basic Operations of Stack


There are some basic operations that allow us to perform different actions on
a stack.

 Push: Add an element to the top of a stack


 Pop: Remove an element from the top of a stack
 IsEmpty: Check if the stack is empty
 IsFull: Check if the stack is full
 Peek: Get the value of the top element without removing it

Working of Stack Data Structure


The operations work as follows:

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 .

4. On popping an element, we return the element pointed to by TOP and reduce


its value.
5. Before pushing, we check if the stack is already full

6. Before popping, we check if the stack is already empty


Working of Stack Data Structure
// Stack implementation in C++

#include <stdlib.h>
#include <iostream>

using namespace std;

#define MAX 10
int size = 0;

// Creating a stack
struct stack {
int items[MAX];
int top;
};
typedef struct stack st;

void createEmptyStack(st *s) {


s->top = -1;
}

// Check if the stack is full


int isfull(st *s) {
if (s->top == MAX - 1)
return 1;
else
return 0;
}

// Check if the stack is empty


int isempty(st *s) {
if (s->top == -1)
return 1;
else
return 0;
}

// Add elements into stack


void push(st *s, int newitem) {
if (isfull(s)) {
cout << "STACK FULL";
} else {
s->top++;
s->items[s->top] = newitem;
}
size++;
}

// Remove element from stack


void pop(st *s) {
if (isempty(s)) {
cout << "\n STACK EMPTY \n";
} else {
cout << "Item popped= " << s->items[s->top];
s->top--;
}
size--;
cout << endl;
}

// Print elements of stack


void printStack(st *s) {
printf("Stack: ");
for (int i = 0; i < size; i++) {
cout << s->items[i] << " ";
}
cout << endl;
}

// 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);

cout << "\nAfter popping out\n";


printStack(s);
}

Stack Time Complexity


For the array-based implementation of a stack, the push and pop operations
take constant time, i.e. O(1) .

Applications of Stack Data Structure


Although stack is a simple data structure to implement, it is very powerful. The
most common uses of a stack are:

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

Step2 – Define a recursive case: Define the problem in terms of smaller


subproblems. Break the problem down into smaller versions of itself, and call
the function recursively to solve each subproblem.
Step3 – Ensure the recursion terminates: Make sure that the recursive
function eventually reaches the base case, and does not enter an infinite
loop.

Step4 – Combine the solutions: Combine the solutions of the subproblems


to solve the original problem.
Example 1 : Sum of Natural Numbers
Let us consider a problem to find the sum of natural numbers, there are
several ways of doing that but the simplest approach is simply to add the
numbers starting from 0 to n.

Comparison of Recursive and Iterative Approaches


Approach Complexity Memory Usage

Iterative Approach O(n) O(1)

Recursive Approach O(n) O(n)

Infix to Postfix Expression


Write a program to convert an Infix expression to Postfix form.
Infix expression: The expression of the form “a operator b” (a + b) i.e.,
when an operator is in-between every pair of operands.
Postfix expression: The expression of the form “a b operator” (ab+) i.e.,
When every pair of operands is followed by an operator.
Examples:
Input: s = “A*(B+C)/D”
Output: ABC+*D/
Input: s = “a+b*(c^d-e)^(f+g*h)-i”
Output: abcd^e-fgh*+^*+i-

Try it on GfG Practice

Why postfix representation of the expression?


The compiler scans the expression either from left to right or from right to
left.
Consider the expression: a + b * c + d
 The compiler first scans the expression to evaluate the expression b * c,
then again scans the expression to add a to it.
 The result is then added to d after another scan.
The repeated scanning makes it very inefficient. Infix expressions are easily
readable and solvable by humans whereas the computer cannot differentiate
the operators and parenthesis easily so, it is better to convert the expression
to postfix (or prefix) form before evaluation.
The corresponding expression in postfix form is abc*+d+. The postfix
expressions can be evaluated easily using a stack.
Conversion of an Infix expression to Postfix expression
To convert infix expression to postfix expression, use the stack data
structure. Scan the infix expression from left to right. Whenever we get an
operand, add it to the postfix expression and if we get an operator or
parenthesis add it to the stack by maintaining their precedence.
Below are the steps to implement the above idea:
1. Scan the infix expression from left to right.
2. If the scanned character is an operand, put it in the postfix expression.
3. Otherwise, do the following
 If the precedence of the current scanned operator is higher than the
precedence of the operator on top of the stack, or if the stack is empty,
or if the stack contains a ‘(‘, then push the current operator onto the
stack.
 Else, pop all operators from the stack that have precedence higher
than or equal to that of the current operator. After that push the current
operator onto the stack.
4. If the scanned character is a ‘(‘, push it to the stack.
5. If the scanned character is a ‘)’, pop the stack and output it until a
‘(‘is encountered, and discard both the parenthesis.
6. Repeat steps 2-5 until the infix expression is scanned.
7. Once the scanning is over, Pop the stack and add the operators in the
postfix expression until it is not empty.
8. Finally, print the postfix expression.
Prefix to Postfix Conversion
Given a Prefix expression, convert it into a Postfix expression.
Conversion of Prefix expression directly to Postfix without going through the
process of converting them first to Infix and then to Postfix is much better in
terms of computation and better understanding the expression (Computers
evaluate using Postfix expression).
let’s discuss about Prefix and Postfix notation:
Prefix: An expression is called the prefix expression if the operator appears
in the expression before the operands. Simply of the form (operator
operand1 operand2).
Example : *+AB-CD (Infix : (A+B) * (C-D) )
Postfix: An expression is called the postfix expression if the operator
appears in the expression after the operands. Simply of the form (operand1
operand2 operator).
Example : AB+CD-* (Infix : (A+B * (C-D) )
Note : Follow the link for prefix to postfix online convertor.
Examples:
Input : Prefix : *+AB-CD
Output : Postfix : AB+CD-*
Explanation : Prefix to Infix : (A+B) * (C-D)
Infix to Postfix : AB+CD-*

Input : Prefix : *-A/BC-/AKL


Output : Postfix : ABC/-AK/L-*
Explanation : Prefix to Infix : (A-(B/C))*((A/K)-L)
Infix to Postfix : ABC/-AK/L-*

Try it on GfG Practice


Algorithm for Prefix to Postfix:
 Read the Prefix expression in reverse order (from right to left)
 If the symbol is an operand, then push it onto the Stack
 If the symbol is an operator, then pop two operands from the Stack
Create a string by concatenating the two operands and the operator after
them.
string = operand1 + operand2 + operator
And push the resultant string back to Stack
 Repeat the above steps until end of Prefix expression.
Prefix to Infix Conversion
Infix : An expression is called the Infix expression if the operator appears in
between the operands in the expression. Simply of the form (operand1
operator operand2).
Example : (A+B) * (C-D)
Prefix : An expression is called the prefix expression if the operator appears
in the expression before the operands. Simply of the form (operator
operand1 operand2).
Example : *+AB-CD (Infix : (A+B) * (C-D) )
Given a Prefix expression, convert it into a Infix expression.
Computers usually does the computation in either prefix or postfix (usually
postfix). But for humans, its easier to understand an Infix expression rather
than a prefix. Hence conversion is need for human understanding.
Examples:
Input : Prefix : *+AB-CD
Output : Infix : ((A+B)*(C-D))

Input : Prefix : *-A/BC-/AKL


Output : Infix : ((A-(B/C))*((A/K)-L))

Try it on GfG Practice

Algorithm for Prefix to Infix:


 Read the Prefix expression in reverse order (from right to left)
 If the symbol is an operand, then push it onto the Stack
 If the symbol is an operator, then pop two operands from the Stack
Create a string by concatenating the two operands and the operator
between them.
string = (operand1 + operator + operand2)
And push the resultant string back to Stack
 Repeat the above steps until the end of Prefix expression.
 At the end stack will have only 1 string i.e resultant string

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.

FIFO Representation of Queue

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.

Basic Operations of Queue


A queue is an object (an abstract data structure - ADT) that allows the
following operations:

 Enqueue: Add an element to the end of the queue


 Dequeue: Remove an element from the front of the queue
 IsEmpty: Check if the queue is empty
 IsFull: Check if the queue is full
 Peek: Get the value of the front of the queue without removing it

Working of Queue
Queue operations work as follows:

 two pointers FRONT and REAR

 FRONT track the first element of the queue


 REAR track the last element of the queue
 initially, set value of FRONT and REAR to -1
Enqueue Operation
 check if the queue is full
 for the first element, set the value of FRONT to 0
 increase the REAR index by 1
 add the new element in the position pointed to by REAR

Dequeue Operation
 check if the queue is empty

 return the value pointed by FRONT

 increase the FRONT index by 1


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

Queue Implementations in Python, Java, C,


and C++
We usually use arrays to implement queues in Java and C/++. In the case of
Python, we use lists.

Python

Java

C++

// Queue implementation in C++

#include <iostream>
#define SIZE 5

using namespace std;

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;
}

void enQueue(int element) {


if (isFull()) {
cout << "Queue is full";
} else {
if (front == -1) front = 0;
rear++;
items[rear] = element;
cout << endl
<< "Inserted " << element << endl;
}
}

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;

//deQueue is not possible on empty queue


q.deQueue();

//enQueue 5 elements
q.enQueue(1);
q.enQueue(2);
q.enQueue(3);
q.enQueue(4);
q.enQueue(5);

// 6th element can't be added to because the queue is full


q.enQueue(6);

q.display();

//deQueue removes element entered first i.e. 1


q.deQueue();

//Now we have just 4 elements


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

 When data is transferred asynchronously between two processes.The queue


is used for synchronization. For example: IO Buffers, pipes, file IO, etc

 Handling of interrupts in real-time systems.

 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

 Double Ended 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.

Simple Queue Representation

To learn more, visit Queue Data Structure.

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.

To learn more, visit Circular Queue Data Structure.


Priority Queue
A priority queue is a special type of queue in which each element is
associated with a priority and is served according to its priority. If elements
with the same priority occur, they are served according to their order in the
queue.

Priority Queue
Representation

Insertion occurs based on the arrival of the values and removal occurs based
on priority.

To learn more, visit Priority Queue Data Structure.

Deque (Double Ended Queue)


In a double ended queue, insertion and removal of elements can be
performed from either from the front or rear. Thus, it does not follow the FIFO
(First In First Out) rule.

Deque Representation

Circular Queue Data Structure


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

Circular queue representation

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

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

How Circular Queue Works


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

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

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

Circular Queue Operations


The circular queue work as follows:
 two pointers FRONT and REAR

 FRONT track the first element of the queue


 REAR track the last elements of the queue
 initially, set value of FRONT and REAR to -1
1. Enqueue Operation
 check if the queue is full

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


 circularly increase the REAR index by 1 (i.e. if the rear reaches the end, next it
would be at the start of the queue)
 add the new element in the position pointed to by REAR

2. Dequeue Operation
 check if the queue is empty

 return the value pointed by FRONT

 circularly increase the FRONT index by 1


 for the last element, reset the values of FRONT and REAR to -1
However, the check for full queue has a new additional case:

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

 Case 2: FRONT = REAR + 1

The second case happens when REAR starts from 0 due to circular increment
and when its value is just 1 less than FRONT , the queue is full.
Operations

Circular Queue Complexity Analysis


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

Applications of Circular Queue


 CPU scheduling

 Memory management

 Traffic Management

Priority Queue
A priority queue is a special type of queue in which each element is
associated with a priority value. And, elements are served on the basis of
their priority. That is, higher priority elements are served first.
However, if elements with the same priority occur, they are served according
to their order in the queue.

Assigning Priority Value


Generally, the value of the element itself is considered for assigning the
priority. For example,
The element with the highest value is considered the highest priority element.
However, in other cases, we can assume the element with the lowest value as
the highest priority element.

We can also set priorities according to our needs.

Removing Highest Priority


Element

Difference between Priority Queue and Normal Queue


In a queue, the first-in-first-out rule is implemented whereas, in a priority
queue, the values are removed on the basis of priority. The element with
the highest priority is removed first.
Implementation of Priority Queue
Priority queue can be implemented using an array, a linked list, a heap data
structure, or a binary search tree. Among these data structures, heap data
structure provides an efficient implementation of priority queues.

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

Operations peek insert delete

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

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

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

Priority Queue Operations


Basic operations of a priority queue are inserting, removing, and peeking
elements.
Before studying the priority queue, please refer to the heap data structure for
a better understanding of binary heap as it is used to implement the priority
queue in this article.

1. Inserting an Element into the Priority Queue


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

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

Insert an element at the end of the queue

 Heapify the tree. Heapify after insertion


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

If there is no node,

create a newNode.

else (a node is already present)

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

heapify the array

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

2. Deleting an Element from the Priority Queue


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

 Select the element to be deleted. Select the


element to be deleted
 Swap it with the last element. Swap with
the last leaf node element

 Remove the last element. Remove the


last element leaf
 Heapify the tree. Heapify the priority
queue

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

If nodeToBeDeleted is the leafNode

remove the node

Else swap nodeToBeDeleted with the lastLeafNode

remove noteToBeDeleted

heapify the array

For Min Heap, the above algorithm is modified so that the both childNodes are
smaller than currentNode .
3. Peeking from the Priority Queue (Find max/min)
Peek operation returns the maximum element from Max Heap or minimum
element from Min Heap without deleting the node.

For both Max heap and Min Heap

return rootNode

4. Extract-Max/Min from the Priority Queue


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

Priority Queue Implementations in Python,


Java, C, and C++
Python

Java

C++

# Priority Queue implementation in Python

# Function to heapify the tree


def heapify(arr, n, i):
# Find the largest among root, left child, and right child
largest = i
l = 2 * i + 1
r = 2 * i + 2

if l < n and arr[i] < arr[l]:


largest = l

if r < n and arr[largest] < arr[r]:


largest = r

# Swap and continue heapifying if root is not the largest


if largest != i:
arr[i], arr[largest] = arr[largest], arr[i]
heapify(arr, n, largest)

# Function to insert an element into the tree


def insert(array, newNum):
size = len(array)
if size == 0:
array.append(newNum)
else:
array.append(newNum)
for i in range((size // 2) - 1, -1, -1):
heapify(array, size, i)

# Function to delete an element from the tree


def deleteNode(array, num):
size = len(array)
i = 0
for i in range(0, size):
if num == array[i]:
break

# Swap the element to delete with the last element


array[i], array[size - 1] = array[size - 1], array[i]

# Remove the last element (the one we want to delete)


array.pop()
# Rebuild the heap
for i in range((len(array) // 2) - 1, -1, -1):
heapify(array, len(array), i)

arr = []

insert(arr, 3)
insert(arr, 4)
insert(arr, 9)
insert(arr, 5)
insert(arr, 2)

print("Max-Heap array: " + str(arr))

deleteNode(arr, 4)
print("After deleting an element: " + str(arr))

Priority Queue Applications


Some of the applications of a priority queue are:

 Dijkstra's algorithm

 for implementing stack

 for load balancing and interrupt handling in an operating system

 for data compression in Huffman code


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

Representation of Deque

Types of Deque
 Input Restricted Deque
In this deque, input is restricted at a single end but allows deletion at both the
ends.
 Output Restricted Deque
In this deque, output is restricted at a single end but allows insertion at both
the ends.

Operations on a Deque
Below is the circular array implementation of deque. In a circular array, if the
array is full, we start from the beginning.
But in a linear array implementation, if the array is full, no more elements can
be inserted. In each of the operations below, if the array is full, "overflow
message" is thrown.

Before performing the following operations, these steps are followed.

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


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

Initialize an array and pointers for


deque

1. Insert at the Front


This operation adds an element at the front.

1. Check if the deque is full.


Check the position of front

2. If the deque is full (i.e. (front == 0 && rear == n - 1) || (front == rear + 1) ),

insertion operation cannot be performed (overflow condition).


3. If the deque is empty, reinitialize front = 0 . And, add the new key
into array[front].
4. If front = 0 , reinitialize front = n-1 (last index).

Shift front to the end


5. Else, decrease front by 1.
6. Add the new key 5 into array[front] .

Insert the element at Front


2. Insert at the Rear
This operation adds an element to the rear.

1. Check if the deque is full.


Check if deque is full

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


condition).
3. If the deque is empty, reinitialize rear = 0 . And, add the new key
into array[rear] .
4. If rear = n - 1 , reinitialize real = 0 (first index).

5. Else, increase rear by 1.


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

Insert the element at rear


3. Delete from the Front
The operation deletes an element from the front .

1. Check if the deque is empty.


Check if deque is empty

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


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

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

5. Else, front = front + 1 .

Increase the front


4. Delete from the Rear
This operation deletes an element from the rear .

1. Check if the deque is empty.


Check if deque is empty

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


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

-1 , else follow the steps below.


4. If rear is at the first index (i.e. rear = 0 ), reinitialize rear = n - 1 .
5. Else, rear = rear - 1 . Decrease
the rear
5. Check Empty
This operation checks if the deque is empty. If front = -1 , the deque is empty.
6. Check Full
This operation checks if the deque is full. If front = 0 and rear = n -

1 OR front = rear + 1 , the deque is full.

Deque Implementation in Python, Java, C, and


C++
Python

Java

C++

# Deque implementaion in python

class Deque:
def __init__(self):
self.items = []

def isEmpty(self):
return self.items == []

def addRear(self, item):


self.items.append(item)

def addFront(self, item):


self.items.insert(0, item)

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.

2. To store history in browsers.

3. For implementing both stacks and queues.

Hash Table
The Hash table data structure stores elements in key-value pairs where

 Key- unique integer that is used for indexing the values


 Value - data that are associated with keys.

Key and Value in Hash table

Hashing (Hash Function)


In a hash table, a new index is processed using the keys. And, the element
corresponding to that key is stored in the index. This process is
called hashing.
Let k be a key and h(x) be a hash function.
Here, h(k) will give us a new index to store the element linked with k .
Hash table Representation

To learn more, visit Hashing.

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.

 Collision resolution by chaining


 Open Addressing: Linear/Quadratic Probing and Double Hashing

1. Collision resolution by chaining


In chaining, if a hash function produces the same index for multiple elements,
these elements are stored in the same index by using a doubly-linked list.

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 .

Collision Resolution using chaining

Pseudocode for operations

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 .

Different techniques used in open addressing are:

i. Linear Probing
In linear probing, collision is resolved by checking the next slot.

h(k, i) = (h′(k) + i) mod m

where

 i = {0, 1, ….}

 h'(k) is a new hash function


If a collision occurs at h(k, 0) , then h(k, 1) is checked. In this way, the value
of i is incremented linearly.
The problem with linear probing is that a cluster of adjacent slots is filled.
When inserting a new element, the entire cluster must be traversed. This adds
to the time required to perform operations on the hash table.
ii. Quadratic Probing
It works similar to linear probing but the spacing between the slots is
increased (greater than one) by using the following relation.

h(k, i) = (h′(k) + c1i + c2i2) mod m

where,

 c1 and c2 are positive auxiliary constants,


 i = {0, 1, ….}

iii. Double hashing


If a collision occurs after applying a hash function h(k) , then another hash
function is calculated for finding the next slot.
h(k, i) = (h1(k) + ih2(k)) mod m

Good Hash Functions


A good hash function may not prevent the collisions completely however it can
reduce the number of collisions.

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 ,

we will always get the lower order p-bits.

if m = 22, k = 17, then h(k) = 17 mod 22 = 10001 mod 100 = 01


if m = 23, k = 17, then h(k) = 17 mod 22 = 10001 mod 100 = 001
if m = 24, k = 17, then h(k) = 17 mod 22 = 10001 mod 100 = 0001
if m = 2p, then h(k) = p lower bits of m

2. Multiplication Method
h(k) = ⌊m(kA mod 1)⌋

where,

 kA mod 1 gives the fractional part kA ,

 ⌊ ⌋ gives the floor value


 A is any constant. The value of A lies between 0 and 1. But, an optimal choice
will be ≈ (√5-1)/2 suggested by Knuth.
3. Universal Hashing
In Universal hashing, the hash function is chosen at random independent of
keys.

Python, Java and C/C++ Examples


Python

Java

C
C++

# Python program to demonstrate working of HashTable

# 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

for i in range(2, n // 2):


if n % i == 0:
return 0

return 1

def getPrime(n):
if n % 2 == 0:
n = n + 1

while not checkPrime(n):


n += 2

return n

def hashFunction(key):
capacity = getPrime(10)
return key % capacity

def insertData(key, data):


index = hashFunction(key)
# Check if the key already exists in the list to update it, otherwise append
found = False
for i, kv in enumerate(hashTable[index]):
if kv[0] == key:
hashTable[index][i] = (key, data) # Update existing key-value pair
found = True
break
if not found:
hashTable[index].append((key, data)) # Add new key-value pair if not
found

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

# Test the hash table


insertData(123, "apple")
insertData(432, "mango")
insertData(213, "banana")
insertData(654, "guava")
insertData(213, "orange") # This should update the value for key 213

print(hashTable)

removeData(123)

print(hashTable)

Applications of Hash Table


Hash tables are implemented where

 constant time lookup and insertion is required

 cryptographic applications

 indexing data is required


Week 8
Tree Data Structure
A tree is a nonlinear hierarchical data structure that consists of nodes
connected by edges.

A Tree

Why Tree Data Structure?


Other data structures such as arrays, linked list, stack, and queue are linear
data structures that store data sequentially. In order to perform any operation
in a linear data structure, the time complexity increases with the increase in
the data size. But, it is not acceptable in today's computational world.
Different tree data structures allow quicker and easier access to the data as it
is a non-linear data structure.

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.

Nodes and edges of a tree

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.

Height and depth of each node in a tree

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

You can create a forest by cutting the root of 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.

To learn more, please visit tree traversal.


Tree Applications
 Binary Search Trees(BSTs) are used to quickly check whether an element is
present in a set or not.

 Heap is a kind of tree that is used for heap sort.

 A modified version of a tree called Tries is used in modern routers to store


routing information.

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

Tree Traversal - inorder,


preorder and postorder
Traversing a tree means visiting every node in the tree. You might, for
instance, want to add all the values in the tree or find the largest one. For all
these operations, you will need to visit each node of the tree.

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.

Starting from top, Left to right

1 -> 12 -> 5 -> 6 -> 9

Starting from bottom, Left to right

5 -> 6 -> 12 -> 9 -> 1

Although this process is somewhat easy, it doesn't respect the hierarchy of


the tree, only the depth of the nodes.

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

 A node carrying data

 Two subtrees

Left and Right Subtree

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.

Depending on the order in which we do this, there can be three types of


traversal.
Inorder traversal
1. First, visit all the nodes in the left subtree

2. Then the root node

3. Visit all the nodes in the right subtree

inorder(root->left)
display(root->data)
inorder(root->right)

Preorder traversal
1. Visit root node

2. Visit all the nodes in the left subtree

3. Visit all the nodes in the right subtree

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

3. Visit the root node

postorder(root->left)
postorder(root->right)
display(root->data)

Let's visualize in-order traversal. We start from the root node.

Left and Right Subtree

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.

Let's put all this in a stack so that we remember.


Stack

Now we traverse to the subtree pointed on the TOP of the stack.

Again, we follow the same rule of inorder

Left subtree -> root -> right subtree

After traversing the left subtree, we are left with


Final Stack

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

5 -> 12 -> 6 -> 1 -> 9

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

 address of left child

 address of right child

Binary Tree

Types of Binary Tree


1. Full Binary Tree
A full Binary tree is a special type of binary tree in which every parent
node/internal node has either two or no children.
Full Binary Tree

To learn more, please visit full binary tree.


2. Perfect Binary Tree
A perfect binary tree is a type of binary tree in which every internal node has
exactly two child nodes and all the leaf nodes are at the same level.

Perfect Binary Tree

To learn more, please visit perfect binary tree.


3. Complete Binary Tree
A complete binary tree is just like a full binary tree, but with two major
differences
1. Every level must be completely filled

2. All the leaf elements must lean towards the left.

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.

Complete Binary Tree


To learn more, please visit complete binary tree.
4. Degenerate or Pathological Tree
A degenerate or pathological tree is the tree having a single child either left or
right.

Degenerate Binary Tree


5. Skewed Binary Tree
A skewed binary tree is a pathological/degenerate tree in which the tree is
either dominated by the left nodes or the right nodes. Thus, there are two
types of skewed binary tree: left-skewed binary tree and right-skewed
binary tree.

Skewed Binary Tree

6. Balanced Binary Tree


It is a type of binary tree in which the difference between the height of the left
and the right subtree for each node is either 0 or 1.

Balanced Binary Tree

To learn more, please visit balanced binary tree.


Binary Tree Representation
A node of a binary tree is represented by a structure containing a data part
and two pointers to other structures of the same type.

struct node
{
int data;
struct node *left;
struct node *right;
};

Binary Tree Representation

Binary Tree Applications


 For easy and quick access to data

 In router algorithms

 To implement heap data structure


 Syntax tree
Full Binary Tree
A full Binary tree is a special type of binary tree in which every parent
node/internal node has either two or no children.

It is also known as a proper binary tree.

Full Binary Tree

Full Binary Tree Theorems


Let, i = the number of internal nodes
n = be the total number of nodes
l = number of leaves
λ = number of levels

1. The number of leaves is i + 1.

2. The total number of nodes is 2i + 1 .

3. The number of internal nodes is (n – 1) / 2 .

4. The number of leaves is (n + 1) / 2 .


5. The total number of nodes is 2l – 1 .

6. The number of internal nodes is l – 1.

2λ - 1
7. The number of leaves is at most .

Perfect Binary Tree


A perfect binary tree is a type of binary tree in which every internal node has
exactly two child nodes and all the leaf nodes are at the same level.

Perfect Binary Tree

All the internal nodes have a degree of 2.

Recursively, a perfect binary tree can be defined as:

1. If a single node has no children, it is a perfect binary tree of height h = 0,

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)

Complete Binary Tree


A complete binary tree is a binary tree in which all the levels are completely
filled except possibly the lowest one, which is filled from the left.

A complete binary tree is just like a full binary tree, but with two major
differences

1. All the leaf elements must lean towards the left.

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

Full Binary Tree vs Complete Binary Tree


Comparison between full binary tree and complete binary

tree Comparison between full binary tree and complete binary

tree Comparison between full binary tree and complete binary


tree Comparison between full binary tree and complete binary
tree

How a Complete Binary Tree is Created?


1. Select the first element of the list to be the root node. (no. of elements on

level-I: 1) Select the first element


as root

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

4. Keep repeating until you reach the last element.

5. 5 as a left child and 6 as a right child

Balanced Binary Tree


A balanced binary tree, also referred to as a height-balanced binary tree, is
defined as a binary tree in which the height of the left and right subtree of any
node differ by not more than 1.

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

2. the left subtree is balanced

3. the right subtree is balanced

Balanced Binary Tree with depth at each level

Balanced Binary Tree Applications


 AVL tree
 Balanced Binary Search Tree
Binary Search Tree(BST)
Binary search tree is a data structure that quickly allows us to maintain a
sorted list of numbers.

 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)

Let us try to visualize this with a diagram.

4 is not found so, traverse through the left subtree of 8


4 is not found so, traverse through the right subtree of 3

4 is not found so, traverse through the left subtree of 6


4 is found

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.

4<8 so, transverse through the left child of 8


4>3 so, transverse through the right child of 8

4<6 so, transverse through the left child of 6


Insert 4 as a left child of 6

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

Delete the node


Case II
In the second case, the node to be deleted lies has a single child node. In
such a case follow the steps below:

1. Replace that node with its child node.

2. Remove the child node from its original position.

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:

1. Get the inorder successor of that node.


2. Replace the node with the inorder successor.

3. Remove the inorder successor from its original position.

3 is to be deleted
Copy the value of the inorder successor (4) to the node

Delete the inorder successor

Python, Java and C/C++ Examples


Python

Java

C++

# Binary Search Tree operations in Python

# 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 a new node if the tree is empty


if node is None:
return Node(key)

# Traverse to the right place and insert the node


if key < node.key:
node.left = insert(node.left, key)
else:
node.right = insert(node.right, key)

return node

# Find the inorder successor


def minValueNode(node):
current = node

# Find the leftmost leaf


while(current.left is not None):
current = current.left

return current
# Deleting a node
def deleteNode(root, key):

# Return if the tree is empty


if root is None:
return root

# Find the node to be deleted


if key < root.key:
root.left = deleteNode(root.left, key)
elif(key > root.key):
root.right = deleteNode(root.right, key)
else:
# If the node is with only one child or no child
if root.left is None:
temp = root.right
root = None
return temp

elif root.right is None:


temp = root.left
root = None
return temp

# If the node has two children,


# place the inorder successor in position of the node to be deleted
temp = minValueNode(root.right)

root.key = temp.key

# Delete the inorder successor


root.right = deleteNode(root.right, 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("Inorder traversal: ", end=' ')


inorder(root)

print("\nDelete 10")
root = deleteNode(root, 10)
print("Inorder traversal: ", end=' ')
inorder(root)

Binary Search Tree Complexities


Time Complexity
Operation Best Case Complexity Average Case Complexity Worst Case Complexity

Search O(log n) O(log n) O(n)

Insertion O(log n) O(log n) O(n)

Deletion O(log n) O(log n) O(n)

Here, n is the number of nodes in the tree.


Space Complexity
The space complexity for all the operations is O(n) .
Binary Search Tree Applications
1. In multilevel indexing in the database

2. For dynamic sorting

3. For managing virtual memory areas in Unix kernel

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.

Balance Factor = (Height of Left Subtree - Height of Right Subtree) or (Height


of Right Subtree - Height of Left Subtree)

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.

An example of a balanced avl tree is:


Avl tree

Operations on an AVL tree


Various operations that can be performed on an AVL tree are:

Rotating the subtrees in an AVL Tree


In rotation operation, the positions of the nodes of a subtree are interchanged.

There are two types of rotations:

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

2. If y has a left subtree, assign x as the parent of the left subtree of y .

Assign x as the parent of the left subtree of y


3. If the parent of x is NULL , make y as the root of the tree.
4. Else if x is the left child of p , make y as the left child of p .

5. Else assign y as the right child of p . Change the


parent of x to that of y
6. Make y as the parent of x . Assign y as the parent of
x.

Right Rotate
In right-rotation, the arrangement of the nodes on the left is transformed into
the arrangements on the right node.

1. Let the initial tree be: Initial tree


2. If x has a right subtree, assign y as the parent of the right subtree of x .

Assign y as the parent of the right subtree of x


3. If the parent of y is NULL , make x as the root of the tree.
4. Else if y is the right child of its parent p , make x as the right child of p .

5. Else assign x as the left child of p . Assign the parent


of y as the parent of x.

6. Make x as the parent of y . Assign x as the parent of y


Left-Right and Right-Left Rotate
In left-right rotation, the arrangements are first shifted to the left and then to
the right.

1. Do left rotation on x-y.

Left rotate x-y

2. Do right rotation on y-z.

Right rotate z-y

In right-left rotation, the arrangements are first shifted to the right and then to
the left.
1. Do right rotation on x-y.

Right rotate x-y

2. Do left rotation on z-y.

Left rotate z-y

Algorithm to insert a newNode


A newNode is always inserted as a leaf node with balance factor equal to 0.
1. Let the initial tree be:
Initial tree for insertion

Let the node to be inserted be: New node


2. Go to the appropriate leaf node to insert a newNode using the following
recursive steps. Compare newKey with rootKey of the current tree.
a. If newKey < rootKey , call insertion algorithm on the left subtree of the current
node until the leaf node is reached.
b. Else if newKey > rootKey , call insertion algorithm on the right subtree of current
node until the leaf node is reached.
c. Else, return leafNode .

Finding the
location to insert newNode
3. Compare leafKey obtained from the above steps with newKey :

a. If newKey < leafKey , make newNode as the leftChild of leafNode .


b. Else, make newNode as rightChild of leafNode .

Inserting the new


node
4. Update balanceFactor of the nodes.

Updating the balance factor


after insertion
5. If the nodes are unbalanced, then rebalance the node.

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.

Balancing the tree with rotation


Balancing the tree with 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

Algorithm to Delete a node


A node is always deleted as a leaf node. After deleting a node, the balance
factors of the nodes get changed. In order to rebalance the balance factor,
suitable rotations are performed.
1. Locate nodeToBeDeleted (recursion is used to find nodeToBeDeleted in the code

used below). Locating the


node to be deleted
2. There are three cases for deleting a node:

a. If nodeToBeDeleted is the leaf node (ie. does not have any child), then
remove nodeToBeDeleted .

b. If nodeToBeDeleted has one child, then substitute the contents


of nodeToBeDeleted with that of the child. Remove the child.
c. If nodeToBeDeleted has two children, find the inorder
successor w of nodeToBeDeleted (ie. node with a minimum value of key in the
right subtree). Finding the
successor
a. Substitute the contents of nodeToBeDeleted with that of w .

Substitute the node to be


deleted
b. Remove the leaf node w .
Remove w
3. Update balanceFactor of the nodes.

Update bf
4. Rebalance the tree if the balance factor of any of the nodes is not equal to -1,
0 or 1.

a. If balanceFactor of currentNode > 1,


a. If balanceFactor of leftChild >= 0, do right rotation.

Right-rotate for balancing the tree


b. Else do left-right rotation.

b. If balanceFactor of currentNode < -1,


a. If balanceFactor of rightChild <= 0, do left rotation.
b. Else do right-left rotation.

5. The final tree is: Avl tree final


Python, Java and C/C++ Examples
Python

Java

C++

# AVL tree implementation in Python

import sys

# Create a tree node


class TreeNode(object):
def __init__(self, key):
self.key = key
self.left = None
self.right = None
self.height = 1

class AVLTree(object):

# Function to insert a node


def insert_node(self, root, key):

# Find the correct location and insert the node


if not root:
return TreeNode(key)
elif key < root.key:
root.left = self.insert_node(root.left, key)
else:
root.right = self.insert_node(root.right, key)

root.height = 1 + max(self.getHeight(root.left),
self.getHeight(root.right))

# Update the balance factor and balance the tree


balanceFactor = self.getBalance(root)
if balanceFactor > 1:
if key < root.left.key:
return self.rightRotate(root)
else:
root.left = self.leftRotate(root.left)
return self.rightRotate(root)

if balanceFactor < -1:


if key > root.right.key:
return self.leftRotate(root)
else:
root.right = self.rightRotate(root.right)
return self.leftRotate(root)

return root

# Function to delete a node


def delete_node(self, root, key):

# Find the node to be deleted and remove it


if not root:
return root
elif key < root.key:
root.left = self.delete_node(root.left, key)
elif key > root.key:
root.right = self.delete_node(root.right, key)
else:
if root.left is None:
temp = root.right
root = None
return temp
elif root.right is None:
temp = root.left
root = None
return temp
temp = self.getMinValueNode(root.right)
root.key = temp.key
root.right = self.delete_node(root.right,
temp.key)
if root is None:
return root

# Update the balance factor of nodes


root.height = 1 + max(self.getHeight(root.left),
self.getHeight(root.right))

balanceFactor = self.getBalance(root)

# Balance the tree


if balanceFactor > 1:
if self.getBalance(root.left) >= 0:
return self.rightRotate(root)
else:
root.left = self.leftRotate(root.left)
return self.rightRotate(root)
if balanceFactor < -1:
if self.getBalance(root.right) <= 0:
return self.leftRotate(root)
else:
root.right = self.rightRotate(root.right)
return self.leftRotate(root)
return root

# Function to perform left rotation


def leftRotate(self, z):
y = z.right
T2 = y.left
y.left = z
z.right = T2
z.height = 1 + max(self.getHeight(z.left),
self.getHeight(z.right))
y.height = 1 + max(self.getHeight(y.left),
self.getHeight(y.right))
return y

# Function to perform right rotation


def rightRotate(self, z):
y = z.left
T3 = y.right
y.right = z
z.left = T3
z.height = 1 + max(self.getHeight(z.left),
self.getHeight(z.right))
y.height = 1 + max(self.getHeight(y.left),
self.getHeight(y.right))
return y
# Get the height of the node
def getHeight(self, root):
if not root:
return 0
return root.height

# Get balance factore of the node


def getBalance(self, root):
if not root:
return 0
return self.getHeight(root.left) - self.getHeight(root.right)

def getMinValueNode(self, root):


if root is None or root.left is None:
return root
return self.getMinValueNode(root.left)

def preOrder(self, root):


if not root:
return
print("{0} ".format(root.key), end="")
self.preOrder(root.left)
self.preOrder(root.right)

# Print the tree


def printHelper(self, currPtr, indent, last):
if currPtr != None:
sys.stdout.write(indent)
if last:
sys.stdout.write("R----")
indent += " "
else:
sys.stdout.write("L----")
indent += "| "
print(currPtr.key)
self.printHelper(currPtr.left, indent, False)
self.printHelper(currPtr.right, indent, True)

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)

Complexities of Different Operations on an


AVL Tree
Insertion Deletion Search

O(log n) O(log n) O(log n)

AVL Tree Applications


 For indexing large records in databases

 For searching in large databases

Heap Data Structure


Heap data structure is a complete binary tree that satisfies the heap
property, where any given node is
 always greater than its child node/s and the key of the root node is the largest
among all other nodes. This property is also called max heap property.
 always smaller than the child node/s and the key of the root node is the
smallest among all other nodes. This property is also called min heap
property.
Max-heap
Min-heap

This type of data structure is also called a binary heap.

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

2. Create a complete binary tree from the array

Complete
binary tree
3. Start from the first index of non-leaf node whose index is given by n/2 - 1 .

Start from the


first on leaf node
4. Set current element i as largest .

5. The index of left child is given by 2i + 1 and the right child is given by 2i + 2 .

If leftChild is greater than currentElement (i.e. element at ith index),


set leftChildIndex as largest.
If rightChild is greater than element in largest , set rightChildIndex as largest .
6. Swap largest with currentElement

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

if leftChild > array[largest]


set leftChildIndex as largest
if rightChild > array[largest]
set rightChildIndex as largest

swap array[i] and array[largest]


To create a Max-Heap:

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.

Insert Element into Heap


Algorithm for insertion in Max Heap

If there is no node,
create a newNode.
else (a node is already present)
insert the newNode at the end (last node from left to right.)

heapify the array


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

Insert
at the end

2. Heapify the tree.


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

Delete Element from Heap


Algorithm for deletion in Max Heap

If nodeToBeDeleted is the leafNode


remove the node
Else swap nodeToBeDeleted with the lastLeafNode
remove noteToBeDeleted
heapify the array

1. Select the element to be deleted.

Select
the element to be deleted
2. Swap it with the last element.

Swap
with the last element
3. Remove the last element.

Remove the last element

4. Heapify the tree.


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

Peek (Find max/min)


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

For both Max heap and Min Heap

return rootNode
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.

Let's try to understand this through an example. On facebook, everything is a


node. That includes User, Photo, Album, Event, Group, Page, Comment,
Story, Video, Link, Note...anything that has data is a node.

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.

More precisely, a graph is a data structure (V, E) that consists of

 A collection of vertices V

 A collection of edges E, represented as ordered pairs of vertices (u,v)

Vertices and edges


In the graph,

V = {0, 1, 2, 3}

E = {(0,1), (0,2), (0,3), (1,2)}

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.

If the value of any element a[i][j] is 1, it represents that there is an edge


connecting vertex i and vertex j.
The adjacency matrix for the graph we created above is

Graph adjacency matrix

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.

Edge lookup(checking if an edge exists between vertex A and vertex B) is


extremely fast in adjacency matrix representation but we have to reserve
space for every possible link between all vertices(V x V), so it requires more
space.

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:

Adjacency list representation

An adjacency list is efficient in terms of storage because we only need to store


the values for the edges. For a graph with millions of vertices, this can mean a
lot of saved space.

Graph Operations
The most common graph operations are:

 Check if the element is present in the graph

 Graph Traversal

 Add elements(vertex, edges) to graph


 Finding the path from one vertex to another

Spanning Tree and Minimum


Spanning Tree
Before we learn about spanning trees, we need to understand two graphs:
undirected graphs and connected graphs.

An undirected graph is a graph in which the edges do not point in any


direction (ie. the edges are bidirectional).

Undirected Graph

A connected graph is a graph in which there is always a path from a vertex


to any other vertex.

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 edges may or may not have weights assigned to them.

The total number of spanning trees with n vertices that can be created from a
complete graph is equal to n(n-2) .

If we have n = 4, the maximum number of possible spanning trees is equal


to 44-2 = 16 . Thus, 16 spanning trees can be formed from a complete graph with
4 vertices.

Example of a Spanning Tree


Let's understand the spanning tree with examples below:

Let the original graph be:

Normal graph
Some of the possible spanning trees that can be created from the above
graph are:

A spanning tree A spanning tree

A spanning tree A spanning tree

A spanning tree A spanning tree

Minimum Spanning Tree


A minimum spanning tree is a spanning tree in which the sum of the weight of
the edges is as minimum as possible.
Example of a Spanning Tree
Let's understand the above definition with the help of the example below.

The initial graph is:

Weighted graph

The possible spanning trees from the above graph are:


Minimum spanning tree - 1 Minimum

spanning tree - 2 Minimum spanning tree - 3

Minimum spanning tree - 4

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

Spanning Tree Applications


 Computer Network Routing Protocol

 Cluster Analysis

 Civil Network Planning

Minimum Spanning tree Applications


 To find paths in the map
 To design networks like telecommunication networks, water supply networks,
and electrical grids.

Depth First Search (DFS)


Depth first Search or Depth first traversal is a recursive algorithm for
searching all the vertices of a graph or tree data structure. Traversal means
visiting all the nodes of a graph.

Depth First Search Algorithm


A standard DFS 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.

The DFS algorithm works as follows:

1. Start by putting any one of the graph's vertices on top of a stack.

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.

Depth First Search Example


Let's see how the Depth First Search algorithm works with an example. We
use an undirected graph with 5 vertices.

Undirected graph with 5 vertices

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 Pseudocode (recursive implementation)


The pseudocode for DFS is shown below. In the init() function, notice that we
run the DFS function on every node. This is because the graph might have
two different disconnected parts so to make sure that we cover every vertex,
we can also run the DFS algorithm on every node.

DFS(G, u)
u.visited = true

for each v ∈ G.Adj[u]

if v.visited == false

DFS(G,v)

init() {

For each u ∈ G

u.visited = false

For each u ∈ G

DFS(G, u)

DFS Implementation in Python, Java and


C/C++
The code for the Depth First Search Algorithm with an example is shown
below. The code has been simplified so that we can focus on the algorithm
rather than other details.

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)

for next in graph[start] - visited:


dfs(graph, next, visited)
return visited

graph = {'0': set(['1', '2']),


'1': set(['0', '3', '4']),
'2': set(['0']),
'3': set(['1']),
'4': set(['2', '3'])}

dfs(graph, '0')

Complexity of Depth First Search


The time complexity of the DFS algorithm is represented in the form of O(V +

E) , where V is the number of nodes and E is the number of edges.


The space complexity of the algorithm is O(V) .

Application of DFS Algorithm


1. For finding the path

2. To test if the graph is bipartite

3. For finding the strongly connected components of a graph

4. For detecting cycles in a graph

Breadth first search


Traversal means visiting all the nodes of a graph. Breadth First Traversal or
Breadth First Search is a recursive algorithm for searching all the vertices of a
graph or tree data structure.

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.

The algorithm works as follows:

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.

4. Keep repeating steps 2 and 3 until the queue is empty.

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.

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.

Visit 2 which was added to queue earlier to add its neighbours

4 remains in 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

mark v as visited and put v into Q

while Q is non-empty

remove the head u of Q

mark and enqueue all (unvisited) neighbours of u

You might also like