Original Copy As On 27.11.2024
Original Copy As On 27.11.2024
ON
III Semester
Prepared by
V.SARANYA,M.TECH
RAGAVI.,M.E
INDEX
2.GRAPH ALGORITHM
Definition: An algorithm is a finite set of instructions that accomplishes a particular task. Another
definition is a sequence of unambiguous instructions for solving a problem i.e, for obtaining a
required output for any legitimate (genuine) input in a finite amount of time.
In addition all algorithms must satisfy the following criteria (characteristics).
An algorithm is a finite set of instructions that accomplishes a particular task. Another definition is a
sequence of unambiguous instructions for solving a problem i.e, for obtaining a required output for any
legitimate (genuine) input in a finite amount of time.
Algorithmic is the backend concept of the program or it is just like the recipe of the program.
1.1 Understanding of Algorithm
An algorithm is a sequence of unambiguous instructions for solving a problem. i.e., for obtaining a
required output for any legitimate input in a finite amount of time.There are various methods to solve the
same problem. The important points to be remembered are:
1. The non-ambiguity requirement for each step of an algorithm cannot be compromised.
2. The range of input for which an algorithm works has to be specified carefully.
3. The same algorithm can be represented in different ways.
4. Several algorithms for solving the same problem may exist.
5. Algorithms for the same problem can be based on very different ideas and
can solve the problem with dramatically different speeds.
The example here is to find the gcd of two integers with three different ways: The gcd of
two nonnegative, not-both –zero integers m & n, denoted as gcd (m, n) is defined as the
largest integer that divides both m & n evenly, i.e., with a remainder of zero.
Euclid of Alexandria outlined an algorithm, for solving this problem in one of the
volumes of his Elements.
Gcd (m, n) = gcd (n,
to 0;
since gcd (m, o) = m. {the last value of m is also the gcd of the initial m & n.}
nd
This algorithm comes to a stop, when the 2 no becomes 0. The second number of the pair gets
smaller with each iteration and it cannot become negative. Indeed, the new value of n on the next
iteration is m mod n, which is always smaller than n. hence, the value of the second number in
the pair eventually becomes 0, and the algorithm stops.
The second method for the same problem is: obtained from the definition itself. i.e., gcd of m & n
is the largest integer that divides both numbers evenly. Obviously, that number cannot be greater
than the second number (or) smaller of these two numbers,which we will denote by t = min {m,
n}. So start checking whether t divides both m and n: if it does t is the answer ; if it doesn’t t is
decreased by 1 and try again. (Do this repeatedly till you reach 12 and then stop for the example
given below)
Example: 60 = 2.2.3.5
24 = 2.2.2.3
This procedure is more complex and ambiguity arises since the prime factorization is not
defined. So to make it as an efficient algorithm, incorporate the algorithm to find the prime
factors.
Performance Analysis:
Performance analysis or analysis of algorithms refers to the task of determining the efficiency of an algorithm i.,e
how much computing time and storage an algorithm requires to run (or execute). This analysis of algorithm helps
in judging the value of one algorithm over another.
To judge an algorithm, particularly two things are taken into consideration
1. Space complexity
2. Time complexity.
1.2.1 Space Complexity: The space complexity of an algorithm (program) is the amount of memory it needs to
run to completion.
The space needed by an algorithm has the following components.
1. Instruction Space.
2. Data Space.
3. Environment Stack Space.
1. Instruction Space: Instruction space is the space needed to store the compiled version of the program
instructions. The amount of instruction space that is needed depends on factors such as
i). The compiler used to compile the program into machine code.
ii). The compiler options in effect at the time of compilation.
iii). The target computer, i.,e computer on which the algorithm run. Note that, one compiler may produce less
code as compared to another compiler, when the same program is compiled by these two.
2.Data Space: Data space is the space needed to store all constant and variable values.
Thus, the space requirement of any program p may therefore be written as Space complexity
This equation shows that the total space needed by a program is divided into two parts.
Fixed space requirements(C) is independent of instance characteristics of the inputs and outputs. - Instruction
space - Space for simple variables, fixed-size structure variables, constants.
A variable space requirements (SP(1)) dependent on instance characteristics..
This part includes dynamically allocated space and the recursion stack space.
In the above algorithm, there are no instance characteristics and the space needed by X, Y, Z is independent of
instance characteristics, therefore we can write, S(XYZ) =3+0=3 One space each for X, Y and Z Space
complexity is O(1).
Examples: 2
Algorithm ADD ( float [], int n)
{
sum = 0.0;
for i=1 to n do
sum=sum+X[i];
return sum;
}
Here, atleast n words since X must be large enough to hold the n elements to be summed. Here the problem
instances is characterized by n, the number of elements to be summed. So, we can write,
S(ADD) =3+n
3-one each for n,
I and sum Where n- is for array X[],
Space complexity is O(n).
The time (T(P)) taken by a program P is the sum of the compile time and execution time. The compile
time does not depend on the instance characteristics, so we concentrate on the runtime of a program.
This runtime is denoted by tp (instance characteristics).
The following equation determines the number of addition, subtraction, multiplication, division
compares, loads stores and so on, that would be made by the code for p.
where n denotes instance characteristics, and Ca, Cs, Cm, Cd and so on….. As denote the time needed
for an addition, subtraction, multiplication, division and so on, and ADD, SUB, MUL, DIV and so on,
are functions whose values are the number of additions, subtractions, multiplications, divisions and so
on. But this method is an impossible task to find out time complexity.
Method 1: introduce a global variable “count”, which is initialized to zero. So each time a statement in the signal
program is executed, count is incremented by the step count of that statement.
Method 2:
1. Algorithm Sum(a, n) 0 - 0
2. { 0 - 0
3. s:=0; 1 1 1
5. s:=s+a[i]; 1 N N
6. return s; 1 1 1
7. } 0 - 0
The second method to determine the step count of an algorithm is to build a table in which we list
the total number of steps contributed by each statement.
The S/e (steps per execution) of a statement is the amount by which the count changes as a
result of the execution of that statement. The frequency determines the total number of times each
statement is executed.
Complexity of Algorithms:
1. Best Case: Inputs are provided in such a way that the minimum time is required to
process them.
2. Average Case: The amount of time the algorithm takes on an average set of inputs.
3. Worst Case: The amount of time the algorithm takes on the worst possible set of inputs.
Example: Linear Search
3 4 5 6 7 9 10 12 15
A 1 2 3 4 5 6 7 8 9
Best Case: If we want to search an element 3, whether it is present in the array or not. First, A(1) is
compared with 3, match occurs. So the number of comparisons is only one. It is observed that search
takes minimum number of comparisons, so it comes under best case.
Time complexity is O(1).
verage Case: If we want to search an element 7, whether it is present in the array or not.
First, A(1) is compared with 7 i,.e, (3=7), no match occurs. Next, compare A(2) and 7, no match
occurs. Compare A(3) and A(4) with 7, no match occurs. Up to now 4 comparisons takes place. Now
compare A(5) and 7 (i.,e, 7=7), so match occurs. The number of comparisons is 5. It is observed that
search takes average number of comparisons. So it comes under average case.
Note: If there are n elements, then we require n/2 comparisons.
.
. . Time complexity is O n = O(n) (we neglect constant)
2
Worst Case: If we want to search an element 15, whether it is present in the array or not.
First, A(1) is compared with 15 (i.,e, 3=15), no match occurs. Continue this process until either
element is found or the list is exhausted. The element is found at 9 th comparison. So number of
comparisons are 9.
Time complexity is O(n).
Note: If the element is not found in array, then we have to search entire array, so it comes under
worst case.
O(2^n) or exponential time: the algorithm's running time increases exponentially with the
size of the input.
Space complexity, on the other hand, is a measure of how much memory an algorithm uses
as a function of the size of the input. Like time complexity, it is typically expressed using
big O notation. For example, an algorithm with a space complexity of O(n) uses more
memory as the input size (n) increases. Space complexities are generally categorized as:
O(1) or constant space: the algorithm uses the same amount of memory regardless of the
size of the input.
O(n) or linear space: the algorithm's memory usage increases linearly with the size of the
input.
O(n^2) or quadratic space: the algorithm's memory usage increases quadratically with the
size of the input.
O(2^n) or exponential space: the algorithm's memory usage increases exponentially with the
Asymptotic Notation is used to describe the running time of an algorithm - how much time an algorithm takes with a
given input, n. There are three different notations: big O, big Theta (Θ), and big Omega (Ω).
Big O notation (O(f(n))) provides an upper bound on the growth of a function. It describes
the worst-case scenario for the time or space complexity of an algorithm. For example, an
algorithm with a time complexity of O(n^2) means that the running time of the algorithm is
at most n^2, where n is the size of the input.
Big Ω notation (Ω(f(n))) provides a lower bound on the growth of a function. It describes
the best-case scenario for the time or space complexity of an algorithm. For example, an
algorithm with a space complexity of Ω(n) means that the memory usage of the algorithm
is at least n, where n is the size of the input.
Big Θ notation (Θ(f(n))) provides a tight bound on the growth of a function. It describes the
average-case scenario for the time or space complexity of an algorithm. For example, an
algorithm with a time complexity of Θ(n log n) means that the running time of the
algorithm is both O(n log n) and Ω(n log n), where n is the size of the input.
It's important to note that the asymptotic notation only describes the behavior of the
function for large values of n, and does not provide information about the exact behavior of
the function for small values of n. Also, for some cases, the best, worst and average cases
can be the same, in that case the notation will be simplified to O(f(n)) = Ω(f(n)) = Θ(f(n))
Additionally, these notations can be used to compare the efficiency of different algorithms,
where a lower order of the function is considered more efficient. For example, an algorithm
with a time complexity of O(n) is more efficient than an algorithm with a time complexity
of O(n^2).
It's also worth mentioning that asymptotic notation is not only limited to time and space
complexity but can be used to express the behavior of any function, not just algorithms.
There are three asymptotic notations that are used to represent the time complexity of an
algorithm. They are:
Input: Here our input is an integer array of size "n" and we have one integer "k" that
we need to search for in that array.
Output: If the element "k" is found in the array, then we have return 1, otherwise we have
}
return 0; // return 0, if you didn't find "k"
}
f the input array is [1, 2, 3, 4, 5] and you want to find if "1" is present in the array or not,
then the if-condition of the code will be executed 1 time and it will find that the element 1
is there in the array. So, the if-condition will take 1 second here.
If the input array is [1, 2, 3, 4, 5] and you want to find if "3" is present in the array or not,
then the if-condition of the code will be executed 3 times and it will find that the element 3
is there in the array. So, the if-condition will take 3 seconds here.
If the input array is [1, 2, 3, 4, 5] and you want to find if "6" is present in the array or not,
then the if-condition of the code will be executed 5 times and it will find that the element 6
is not there in the array and the algorithm will return 0 in this case. So, the if-condition will
take 5 seconds here.
As we can see that for the same input array, we have different time for different values of
"k". So, this can be divided into three cases:
Best case: This is the lower bound on running time of an algorithm. We must know the
case that causes the minimum number of operations to be executed. In the above example,
our array was [1, 2, 3, 4, 5] and we are finding if "1" is present in the array or not. So here,
after only one comparison, we will get that ddelement is present in the array. So, this is the
best case of our algorithm.
Average case: We calculate the running time for all possible inputs, sum all the calculated
values and divide the sum by the total number of inputs. We must know (or predict)
distribution of cases.
Worst case: This is the upper bound on running time of an algorithm. We must know the
case that causes the maximum number of operations to be executed. In our example, the
worst case can be if the given array is [1, 2, 3, 4, 5] and we try to find if element "6" is
present in the array or not. Here, the if-condition of our loop will be executed 5 times and
then the algorithm will give "0" as output.
So, we learned about the best, average, and worst case of an algorithm. Now, let's get back to the
asymptotic notation where we saw that we use three asymptotic notation to represent the complexity
of an algorithm i.e. Θ Notation (theta), Ω Notation, Big O Notation
NOTE: In the asymptotic analysis, we generally deal with large input size.
Θ Notation (theta)
The Θ Notation is used to find the average bound of an algorithm i.e. it defines an upper
bound and a lower bound, and your algorithm will lie in between these levels. So, if a
function is g(n), then the theta representation is shown as Θ(g(n)) and the relation is shown
as:
Θ(g(n)) = { f(n): there exist positive constants c1, c2 and n0
Ω Notation
The Ω notation denotes the lower bound of an algorithm i.e. the time taken by the
algorithm can't be lower than this. In other words, this is the fastest time in which the
algorithm will return a result.
Its the time taken by the algorithm when provided with its best-case input. So, if a function
is g(n), then the omega representation is shown as Ω(g(n)) and the relation is shown as:
Ω(g(n)) = { f(n): there exist positive constants
c and n0 such that 0 ≤ cg(n) ≤ f(n) for all
n ≥ n0 }
The above expression can be read as omega of g(n) is defined as set of all the functions
f(n) for which there exist some constants c and n0 such that c*g(n) is less than or equal to
f(n), for all n greater than or equal to n0.
if f(n) = 2n² + 3n
+ 1 and g(n) = n²
then for c = 2 and n0 = 1, we can say that f(n) = Ω(n²)
Big O Notation
The Big O notation defines the upper bound of any algorithm i.e. you algorithm can't take
more time than this time. In other words, we can say that the big O notation denotes the
maximum time taken by an algorithm or the worst-case time complexity of an algorithm.
So, big O notation is the most used notation for the time complexity of an algorithm. So, if
a function is g(n), then the big O representation of g(n) is shown as O(g(n)) and the relation
is shown as:
O(g(n)) = { f(n): there exist positive constants
c and n0 such that 0 ≤ f(n) ≤ cg(n) for all
n ≥ n0 }
The above expression can be read as Big O of g(n) is defined as a set of functions f(n) for
which there exist some constants c and n0 such that f(n) is greater than or equal to 0 and
f(n) is smaller than or equal to c*g(n) for all n greater than or equal to n0.
if f(n) = 2n² + 3n
+ 1 and g(n) = n²
then for c = 6 and n0 = 1, we can say that f(n) = O(n²)
Big O notation example of Algorithms
Big O notation is the most used notation to express the time complexity of an algorithm. In
this section of the blog, we will find the big O notation of various algorithms.
Example 1: Finding the sum of the first n numbers.
In this example, we have to find the sum of first n numbers. For example, if n = 4, then our
output should be 1 + 2 + 3 + 4 = 10. If n = 5, then the ouput should be 1 + 2 + 3 + 4 + 5 =
15. Let's try various solutions to this code and try to compare all those codes.
O(1) solution
// function taking input "n"
int findSum(int n)
{
return n * (n+1) / 2; // this will take some constant time c1
}
In the above code, there is only one statement and we know that a statement takes constant
time for its execution. The basic idea is that if the statement is taking constant time, then it
will take the same amount of time for all the input size and we denote this as O(1) .
O(n) solution
In this solution, we will run a loop from 1 to n and we will add these values to a variable
named "sum".
// function taking input "n"
int findSum(int n)
{
int sum = 0; //----------------> it takes some constant time "c1"
for(int i = 1; i <= n; ++i) // --> here the comparision and increment will take place n times(c2*n)
and the creation of i takes place with some constant time
sum = sum + i; //----------> this statement will be executed n times i.e. c3*n
* here in our example we have 3 constant time taking statements i.e. "sum = 0", "i = 0", and "return
sum", so we can add all the constatnts and replacce with some new constant "c"
* apart from this, we have two statements running n-times i.e. "i < n(in real n+1)" and "sum = sum
+ i" i.e. c2*n + c3*n = c0*n
*/
The big O notation of the above code is O(c0*n) + O(c), where c and c0 are constants. So,
the overall time complexity can be written as O(n) .
O(n²) solution
In this solution, we will increment the value of sum variable "i" times i.e. for i = 1, the sum
variable will be incremented once i.e. sum = 1. For i = 2, the sum variable will be
incremented twice. So, let's see the solution.
// function taking input "n"
int findSum(int n)
{
int sum = 0; //--------------------> constant time
for(int i = 1; i <= n; ++i)
for(int j = 1; j <= i; ++j)
sum++; //------------------> it will run [n * (n + 1) / 2]
return sum; //---------------------> constant time
}
/*
* the statement that is being executed most of the time is "sum++" i.e. n * (n + 1) / 2
* So, total complexity will be: c1*n² + c2*n + c3 [c1 is for the constant terms of n², c2 is for the
constant terms of n, and c3 is for rest of the constant time]
*/
The big O notation of the above algorithm is O(c1*n²) +O( c2*n) + O(c3). Since we take
the higher order of growth in big O. So, our expression will be reduced to O(n²) .
So, until now, we saw 3 solutions for the same problem. Now, which algorithm will you
prefer to use when you are finding the sum of first "n" numbers? If your answer is O(1)
solution, then we have one bonus section for you at the end of this blog. We would prefer
the O(1) solution because the time taken by the algorithm will be constant irrespective of
the input size.
1.4 Recurrence Relation
A recurrence relation is a mathematical equation that describes the relation between the
input size and the running time of a recursive algorithm. It expresses the running time of a
problem in terms of the running time of smaller instances of the same problem.
A recurrence relation typically has the form T(n) = aT(n/b) + f(n) where:
T(n) is the running time of the algorithm on an input of size n
a is the number of recursive calls made by the algorithm
b is the size of the input passed to each recursive call
f(n) is the time required to perform any non-recursive operations
The recurrence relation can be used to determine the time complexity of the algorithm
using techniques such as the Master Theorem or Substitution Method.
For example, let's consider the problem of computing the nth Fibonacci number. A simple
recursive algorithm for solving this problem is as follows:
Fibonacci(n)
if n <= 1
return n else
return Fibonacci(n-1) + Fibonacci(n-2)
The recurrence relation for this algorithm is T(n) = T(n-1) + T(n-2) + O(1), which describes the
running time of the algorithm in terms of the running time of the two smaller instances of the
problem with input sizes n-1 and n-2. Using the Master Theorem, it can be shown that the time
complexity of this algorithm is O(2^n) which is very inefficient for large input sizes.
The Lower and Upper Bound Theory provides a way to find the lowest complexity algorithm to solve a
problem. Before understanding the theory, first, let’s have a brief look at what Lower and Upper bounds
are.
Lower Bound –
Let L(n) be the running time of an algorithm A(say), then g(n) is the Lower Bound of A if there
exist two constants C and N such that L(n) >= C*g(n) for n > N. Lower bound of an algorithm is
shown by the asymptotic notation called Big Omega (or just Omega).
Upper Bound –
Let U(n) be the running time of an algorithm A(say), then g(n) is the Upper Bound of A if there
exist two constants C and N such that U(n) <= C*g(n) for n > N. Upper bound of an algorithm is
shown by the asymptotic notation called Big Oh(O) (or just Oh).
1. Lower Bound Theory:
According to the lower bound theory, for a lower bound L(n) of an algorithm, it is not possible to have
any other algorithm (for a common problem) whose time complexity is less than L(n) for random input.
Also, every algorithm must take at least L(n) time in the worst case. Note that L(n) here is the minimum
of all the possible algorithms, of maximum complexity.
The Lower Bound is very important for any algorithm. Once we calculated it, then we can compare it
with the actual complexity of the algorithm and if their order is the same then we can declare our
algorithm as optimal. So in this section, we will be discussing techniques for finding the lower bound of
an algorithm.
Note that our main motive is to get an optimal algorithm, which is the one having its Upper Bound the
Same as its Lower Bound (U(n)=L(n)). Merge Sort is a common example of an optimal algorithm.
Trivial Lower Bound –
It is the easiest method to find the lower bound. The Lower bounds which can be easily observed based
on the number of input taken and the number of output produced are called Trivial Lower Bound
A hash function is a mathematical function that takes an input (or "message") and produces a fixed-size
string of characters, often in the form of a hash value or hash code. This output is typically a sequence of
numbers and letters, and the same input always produces the same output. Hash functions are widely used
in computer science, cryptography, and data processing.
1. Data Integrity: Ensures data has not been altered during transmission or storage by comparing hash values.
2. Cryptography: Used in digital signatures, password hashing, and secure communication protocols.
3. Hash Tables: Enables fast data retrieval by mapping keys to unique indices.
4. Blockchain: Verifies transactions and maintains chain integrity by linking blocks with hash values.
5. Checksums: Verifies file integrity during downloads or transfers.
1.7 Searching
Searching is the process of fetching a specific element in a collection of elements. The
collection can be an array or a linked list. If you find the element in the list, the process is
considered successful, and it returns the location of that element.
Two prominent search strategies are extensively used to find a specific item on a list.
However, the algorithm chosen is determined by the list's organization.
1. Linear Search
2. Binary Search
3. Interpolation search
Linear Search ( Array Arr, Value a ) // Arr is the name of the array, and a is the searched
element. Step 1: Set i to 0 // i is the index of an array which starts from 0
Step 2: ifi > n then go to step 7 // n is the number of elements in
array Step 3: if Arr[i] = a then go to step 6
Step 4: Set i to i + 1
Step 5: Goto step 2
Step 6: Print element a found at index i and go to
step 8 Step 7: Print element not found
Step 8: Exit
Start
linear_search ( Array , value)
The match is not found, you now move on to the next element and try to implement a
comparison. Step 2: Now, search element 39 is compared to the second element of an
array, 9.
Step 3: Now, search element 39 is compared with the third element, which is 21.
Again, both the elements are not matching, you move onto the next following
element. Step 4; Next, search element 39 is compared with the fourth element,
which is 15.
Step 5: Next, search element 39 is compared with the fifth element 39.
#include<stdio.h.>
#include<stdlib.h>
#include<conio.h>
int main()
{
int array[50],i,target,num;
Binary search is the search technique that works efficiently on sorted lists. Hence, to search
an element into some list using the binary search technique, we must ensure that the list is
sorted.
Binary search follows the divide and conquer approach in which the list is divided into two
halves, and the item is compared with the middle element of the list. If the match is found
then, the location of the middle element is returned. Otherwise, we search into either of
the halves depending upon the result produced through the match
NOTE: Binary search can be implemented on sorted array elements. If the list elements are
not arranged in a sorted manner, we have first to sort them.
Algorithm
1. Binary_Search(a, lower_bound, upper_bound, val) // 'a' is the given array, 'lower_bound' is
t he index of the first array element, 'upper_bound' is the index of the last array element,
'val' is the value to search
2. Step 1: set beg = lower_bound, end = upper_bound, pos = - 1
3. Step 2: repeat steps 3 and 4 while beg <=end
4. Step 3: set mid = (beg + end)/2
5. Step 4: if a[mid] = val
6. set pos = mid
7. print pos
8. go to step 6
9. else if a[mid] > val
10. set end = mid - 1
11. else
12. set beg = mid + 1
13. [end of if]
14. [end of loop]
15. Step 5: if pos = -1
16. print "value is not present in the array"
17. [end of if]
18. Step 6: exit
Procedure binary_search
A ← sorted
array n ←
size of array
x ← value to be
searched Set
lowerBound = 1
Set upperBound
= n while x not
found
if upperBound <
lowerBound EXIT: x does
not exists.
set midPoint = lowerBound + ( upperBound -
lowerBound ) / 2 if A[midPoint] < x
set lowerBound =
midPoint + 1 if
A[midPoint] > x
set upperBound =
midPoint - 1 if A[midPoint]
=x
EXIT: x found at location
midPoint end while
end procedure
beg = 0
end = 8
mid = (0 + 8)/2 = 4. So, 4 is the mid of the array.
Now, the element to search is found. So algorithm will return the index of the element
matched. Binary Search complexity
Now, let's see the time complexity of Binary search in the best case, average case, and
worst case. We will also see the space complexity of Binary search.
1. Time Complexity
o Best Case Complexity - In Binary search, best case occurs when the element to search is
found in first comparison, i.e., when the first middle element itself is the element to be
searched. The best-case time complexity of Binary search is O(1).
o Average Case Complexity - The average case time complexity of Binary search is O(logn).
o Worst Case Complexity - In Binary search, the worst case occurs, when we have to keep
reducing the search space till it has only one element. The worst-case time complexity of
Binary search is O(logn).
2. Space Complexity
Space Complexity O(1)
If a match occurs, then the index of the item is returned. To split the list into two parts, we
use the following method −
mid = Lo + ((Hi - Lo) / (A[Hi] - A[Lo])) * (X - A[Lo])
where
− A
= list
Lo = Lowest index of
the list Hi = Highest
index of the list
A[n] = Value stored at index n in the list
If the middle item is greater than the item, then the probe position is again calculated in the
sub- array to the right of the middle item. Otherwise, the item is searched in the subarray to
the left of the middle item. This process continues on the sub-array as well until the size of
subarray reduces to zero.
Runtime complexity of interpolation search algorithm is Ο(log (log n)) as compared to
Ο(log n) of BST in favorable situations.
Algorithm
As it is an improvisation of the existing BST algorithm, we are mentioning the steps to
search the 'target' data value index, using position probing −
Step 1 − Start searching data from middle of the list.
Step 2 − If it is a match, return the index of the item,
and exit. Step 3 − If it is not a match, probe position.
Step 4 − Divide the list using probing formula and find the new
midle. Step 5 − If data is greater than middle, search in higher
sub-list.
Step 6 − If data is smaller than middle, search in lower
sub-list. Step 7 − Repeat until match.
Pseudoco
de A →
Array list
N → Size
of A
X → Target Value
Procedure
Interpolation_Search() Set
Lo → 0
Set Mid
→ -1 Set
Hi → N-1
if A[Mid] = X
EXIT: Success, Target found at
Mid else
if A[Mid] < X
Set Lo to
Mid+1 else if
A[Mid] > X
Set Hi to
Mid-1
end
if end
if
End
While End
Procedure
Implementation of interpolation in C
#include<stdio.h>
#define MAX 10
// array of items on which linear search will be
conducted. int list[MAX] = { 10, 14, 19, 26, 27, 31,
33, 35, 42, 44 };
int find(int
data) { int lo
= 0;
int hi = MAX
- 1; int mid =
-1;
int comparisons
= 1; int index = -
1; while(lo <=
hi) {
printf("\nComparison %d \n" , comparisons ) ;
printf("lo : %d, list[%d] = %d\n", lo, lo, list[lo]);
printf("hi : %d, list[%d] = %d\n", hi, hi, list[hi]);
comparisons++;
// probe the mid point
mid = lo + (((double)(hi - lo) / (list[hi] - list[lo])) * (data
- list[lo])); printf("mid = %d\n",mid);
// data found
if(list[mid] ==
data) {
index = mid;
break;
} else {
if(list[mid] < data) {
// if data is larger, data is in upper
half lo = mid + 1;
} else {
// if data is smaller, data is in lower
half hi = mid - 1;
}
}
}
Total comparisons
made: 1 Element found
at location: 7
Time Complexity
Bestcase-O(1)
The best-case occurs when the target is found exactly as the first expected position
computed using the formula. As we only perform one comparison, the time
complexity is O(1).
Worst-case-O(n)
The worst case occurs when the given data set is exponentially distributed.
Averagecase-O(log(log(n)))
If the data set is sorted and uniformly distributed, then it takes O(log(log(n))) time
as on an average (log(log(n))) comparisons are made.
Space Complexity
O(1) as no extra space is required.
Naïve pattern searching is the simplest method among other pattern searching algorithms.
It checks for all character of the main string to the pattern. This algorithm is helpful for
smaller texts. It does not need any pre-processing phases. We can find substring by
checking once for the string. It also does not occupy extra space to perform the operation.
The time complexity of Naïve Pattern Search method is O(m*n). The m is the size of
pattern and n is the size of the main string.
Algorithm
naive_algorithm(pattern, text)
Input − The text and the pattern
Output − locations, where the pattern is present in the
St a rt
text p a _len := pattern Size
str_len := string size
for i := 0 to (str_len -
pat_len), do for j := 0 to
pat_len, do
if text[i+j] ≠ pattern[j],
then break
if j == patLen, then
display the position i, as there pattern
found End
Implementation in C
#include
<stdio.h>
#include
<string.h> int
main (){
char txt[] =
"tutorialsPointisthebestplatformforprogrammers"; char
pat[] = "a";
int M = strlen
(pat); int N =
strlen (txt);
for (int i = 0; i <= N - M;
i++){ int j;
for (j = 0; j < M;
j++) if (txt[i +
j] != pat[j])
break;
if (j == M)
printf("Patternmatchesatindex%d", i);
}
return 0;
}
Output
Pattern matches at
6 Pattern matches
at 25 Pattern
matches at 39
#include<stdio.h
>
#include<string.h
> int main (){
char txt[80],
pat[80]; int q;
printf("Enterthecontainerstri
ng "); scanf ("%s", &txt);
printf("Enterthepatterntobesearche
d "); scanf ("%s", &pat);
int d = 256;
printf("Enteraprimenumber ");
scanf ("%d", &q);
int M = strlen
(pat); int N =
strlen (txt); int
i, j;
int p
= 0;
int t
= 0;
int h
= 1;
for (i = 0; i < M - 1; i++)
h = (h * d) % q;
for (i = 0; i < M; i++){
p = (d * p + pat[i]) % q;
t = (d * t + txt[i]) % q;
}
for (i = 0; i <= N -
M; i++){ if (p == t)
{
for (j = 0; j < M;
j++){ if (txt[i +
j] != pat[j])
break;
}
if (j == M)
printf("Patternfoundatindex%d
", i);
}
if (i < N - M){
t = (d * (t - txt[i] * h) + txt[i +
M]) % q; if (t < 0)
t = (t + q);
}
}
return 0;
}
Output
Enter the container string tutorials point
is the best programming website Enter
the pattern to be searched
p
Enter a prime
number 3
Pattern found at index 8
Pattern found at index
21
n this problem, we are given two strings a text and a pattern. Our task is to create a
program for KMP algorithm for pattern search, it will find all the occurrences of pattern in
text string.
Here, we have to find all the occurrences of patterns in the text.
Let’s take an example to understand the problem,
Input
text = “xyztrwqxyzfg” pattern =
“xyz” Output
Found at index
0 Found at
index 7
Here, we will discuss the solution to the problem using KMP (Knuth Morris Pratt) pattern
searching algorithm, it will use a preprocessing string of the pattern which will be used for
matching in the text. And help’s in processing or finding pattern matches in the case where
matching characters are followed by the character of the string that does not match the
pattern.
We will preprocess the pattern wand to create an array that contains the proper prefix and
suffix from the pattern that will help in finding the mismatch patterns.
Program for KMP Algorithm for Pattern Searching
// C Program for KMP Algorithm for Pattern
Searching Example
#include<iostream>
#include<string.h
> using
namespace std;
void prefixSuffixArray(char* pat, int M, int*
pps) { int length = 0;
pps[0] =
0; int i =
1; while
(i < M) {
if (pat[i] ==
pat[length]) {
length++;
pps[i] =
length; i++;
} else {
if (length != 0)
length = pps[length
- 1]; else {
pps[i] =
0; i++;
}
}
}
}
void KMPAlgorithm(char* text, char*
pattern) { int M = strlen(pattern);
int N =
strlen(text); int
pps[M];
prefixSuffixArray(pattern, M,
pps); int i = 0;
int j = 0;
while (i <
N) {
if (pattern[j] ==
text[i]) { j++;
i++;
}
if (j == M)
{
printf("Foundpatternatindex%d",
i - j); j = pps[j - 1];
}
else if (i < N && pattern[j] !=
text[i]) { if (j != 0)
j = pps[j -
1]; else
i = i + 1;
}
}
}
int main() {
char text[] = "xyztrwqxyzfg";
1.9 Sorting :
Sorting is a fundamental operation in data structures and algorithms. It involves arranging elements of a
list or array in a specific order, usually ascending or descending. Sorting is essential for optimizing search
operations, organizing data, and improving the efficiency of algorithms.
The same approach is applied in insertion sort. The idea behind the insertion sort is that
first take one element, iterate it through the sorted array. Although it is simple to use, it is
not appropriate for large data sets as the time complexity of insertion sort in the average
case and worst case is O(n2), where n is the number of items. Insertion sort is less efficient
than the other sorting algorithms like heap sort, quick sort, merge sort, etc.
Algorithm
The simple steps of achieving the insertion sort are listed as follows -
Step 1 - If the element is the first element, assume that it is already sorted. Return 1.
Step2 - Pick the next element, and store it separately in a
key. Step3 - Now, compare the key with all elements in the
sorted array.
Step 4 - If the element in the sorted array is smaller than the current element, then move to the
next element. Else, shift greater elements in the array towards the right.
Step 5 - Insert the value.
Step 6 - Repeat until the array is
sorted. Working of Insertion sort
Algorithm
Now, let's see the working of the insertion sort Algorithm.
To understand the working of the insertion sort algorithm, let's take an unsorted array. It
will be easier to understand the insertion sort via an example.
Let the elements of array are -
Here, 31 is greater than 12. That means both elements are already in ascending order. So,
for now, 12 is stored in a sorted sub-array.
Here, 25 is smaller than 31. So, 31 is not at correct position. Now, swap 31 with 25.
Along with swapping, insertion sort will also check it with all elements in the sorted array.
For now, the sorted array has only one element, i.e. 12. So, 25 is greater than 12. Hence,
the sorted array remains sorted after swapping.
Now, two elements in the sorted array are 12 and 25. Move forward to the next elements
that are 31 and 8.
Both 31 and 8 are not sorted. So, swap them.
Now, the sorted array has three items that are 8, 12 and 25. Move to the next items that are
31 and 32.
Hence, they are already sorted. Now, the sorted array includes 8, 12, 25 and 31.
Heap sort processes the elements by creating the min-heap or max-heap using the elements
of the given array. Min-heap or max-heap represents the ordering of array in which the root
element represents the minimum or maximum element of the array.
o Repeatedly delete the root element of the heap formed in 1st phase.
A heap is a complete binary tree, and the binary tree is a tree in which the node can have
the utmost two children. A complete binary tree is a binary tree in which all the levels
except the last level, i.e., leaf node, should be completely filled, and all the nodes should be
left-justified.
Heapsort is a popular and efficient sorting algorithm. The concept of heap sort is to
eliminate the elements one by one from the heap part of the list, and then insert them into
the sorted part of the list.
Algorithm
1. HeapSort(arr)
2. BuildMaxHeap(arr)
3. for i = length(arr) to 2
5. heap_size[arr] = heap_size[arr] ? 1
6. MaxHeapify(arr,1)
7. End
BuildMaxHeap(arr)
1. BuildMaxHeap(arr)
2. heap_size(arr) = length(arr)
3. for i = length(arr)/2 to 1
4. MaxHeapify(arr,i)
5. End
MaxHeapify(arr,i)
1. MaxHeapify(arr,i)
2. L = left(i)
3. R = right(i)
5. largest = L
6. else
7. largest = i
9. largest = R
10. if largest != i
12. MaxHeapify(arr,largest)
13. End
In heap sort, basically, there are two phases involved in the sorting of elements. By using
the heap sort algorithm, they are as follows -
o The first step includes the creation of a heap by adjusting the elements of the array.
o After the creation of heap, now remove the root element of the heap repeatedly by shifting
it to the end of the array, and then store the heap structure with the remaining elements.
First, we have to construct a heap from the given array and convert it into max heap.
After converting the given heap into max heap, the array elements are -
Next, we have to delete the root element (89) from the max heap. To delete this node, we
have to swap it with the last node, i.e. (11). After deleting the root element, we again have
to heapify it to convert it into max heap.
After swapping the array element 89 with 11, and converting the heap into max-heap, the
elements of array are -
In the next step, again, we have to delete the root element (81) from the max heap. To delete
this node, we have to swap it with the last node, i.e. (54). After deleting the root element, we
again have to heapify it to convert it into max heap.
After swapping the array element 81 with 54 and converting the heap into max-heap, the
elements of array are -
In the next step, we have to delete the root element (76) from the max heap again. To delete
this node, we have to swap it with the last node, i.e. (9). After deleting the root element, we
again have to heapify it to convert it into max heap.
After swapping the array element 76 with 9 and converting the heap into max-heap, the
elements of array are -
In the next step, again we have to delete the root element (54) from the max heap. To delete
this node, we have to swap it with the last node, i.e. (14). After deleting the root element, we
again have to heapify it to convert it into max heap.
After swapping the array element 54 with 14 and converting the heap into max-heap, the
elements of array are -
In the next step, again we have to delete the root element (22) from the max heap. To delete
this node, we have to swap it with the last node, i.e. (11). After deleting the root element, we
again have to heapify it to convert it into max heap.
After swapping the array element 22 with 11 and converting the heap into max-heap, the
elements of array are -
In the next step, again we have to delete the root element (14) from the max heap. To delete
this node, we have to swap it with the last node, i.e. (9). After deleting the root element, we
again have to heapify it to convert it into max heap.
After swapping the array element 14 with 9 and converting the heap into max-heap, the
elements of array are -
In the next step, again we have to delete the root element (11) from the max heap. To delete
this node, we have to swap it with the last node, i.e. (9). After deleting the root element, we
again have to heapify it to convert it into max heap.
After swapping the array element 11 with 9, the elements of array are -
Now, heap has only one element left. After deleting it, heap will be empty.
o Best Case Complexity - It occurs when there is no sorting required, i.e. the array is already
sorted. The best-case time complexity of heap sort is O(n logn).
o Average Case Complexity - It occurs when the array elements are in jumbled order that is
not properly ascending and not properly descending. The average case time complexity of
heap sort is O(n log n).
o Worst Case Complexity - It occurs when the array elements are required to be sorted in
reverse order. That means suppose you have to sort the array elements in ascending order, but
its elements are in descending order. The worst-case time complexity of heap sort is O(n log
n).
The time complexity of heap sort is O(n logn) in all three cases (best case, average case, and
worst case). The height of a complete binary tree having n elements is logn.
2. Space Complexity
Stable N0
3. index of root node in array a[], and 'n' is the size of heap. */
5. {
16. if (largest != i) {
22. }
23. }
24. /*Function to implement the heap sort*/
26. {
35. a[i] =
temp; 36.
37. heapify(a, i, 0);
38. }
39. }
40. /* function to print the array elements */
44. {
47. }
48.
49. }
50. int main()
51. {
52. int a[] = {48, 10, 23, 43, 28, 26, 1};
59. return 0;
60. }
Output
UNIT 2
2.GRAPH ALGORITHMS
Graph algorithms are methods for solving problems related to graph data structures, which
consist of nodes (vertices) connected by edges. Graphs are widely used in various fields,
including computer networks, social networks, transportation, and computational biology.
Definition
Directed
Undirected
Directed graph
Example
Directed Graph
Undirected graph
graph. Example
Representation of Graphs
Graph data structure is represented using the following
representations.
1. Adjacency Matrix
2. Adjacency List
Adjacency Matrix
Adjacency list
Adjacency List
Graph traversals
Applications of graphs
1. Social network graphs : To tweet or not to tweet. Graphs that
represent who knows whom, who communicates with whom,
who influences whom, or other relationships in social
structures. An example is the twitter graph of who follows
whom.
2. Graphs in epidemiology: Vertices represent individuals and
directed edges to view the transfer of an infectious disease
from one individual to another. Analyzing such graphs has
become an important component in understanding and
controlling the spread of diseases.
3. Protein-protein interactions graphs: Vertices represent
proteins and edges represent interactions between them that
carry out some biological function in the cell. These graphs
can be used to, for example, study molecular pathway—
chains of molecular interactions in a cellular process.
4. Network packet traffic graphs: Vertices are IP (Internet
protocol) addresses and edges are the packets that flow
between them. Such graphs are used for analyzing network
security, studying the spread of worms, and tracking
criminal or non- criminal activity.
5. Neural networks: Vertices represent neurons and edges are
the synapses between them. Neural networks are used to
understand how our brain works and how connections
change when we learn. The human brain has about 1011
neurons and close to 1015 synapses.
2.1 Graph traversal
Graph traversal is a technique used for searching a vertex in a graph. The graph traversal is also used to
decide the order of vertices is visited in the search process. A graph traversal finds the edges to be used
in the search process without creating loops. That means using graph traversal we visit all the vertices
of the graph without getting into looping path.
There are two graph traversal techniques and they are as follows...
BFS traversal of a graph produces a spanning tree as final result. Spanning Tree is a graph
without loops. We use Queue data structure with maximum size of total number of vertices in the
graph to implement BFS traversal.
BFS Algorithm
For the BFS implementation, we will categorize each vertex of the graph into two categories:
1. Visited
2. Not Visited
The reason for this division is to prevent the traversal of the same node again thus
avoiding cycles in the graph. Breadth First Traversal in Data Structure uses a queue to keep
track of the vertices to visit. By processing vertices in the order they were added to the queue,
BFS ensures that all the vertices at the current level are visited before moving on to the next
level. A boolean visited array is used to mark the visited vertices. This leads to a breadth-first
exploration of the graph or tree, hence the name "breadth-first traversal."
Here's a step-by-step explanation of how the BFS algorithm works:
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.
Example
Step Traversal Description
4
Next, the unvisited adjacent node
from S is B. We mark it as visited and
enqueue it.
5
Next, the unvisited adjacent node
from S is C. We mark it as visited and
enqueue it.
6
Now, S is left with no unvisited
adjacent nodes. So, we dequeue and
find A.
7
From A we have D as
unvisited adjacent node. We
mark it as visited and enqueue
it.
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
BFS Algorithm Complexity
The time complexity of the BFS 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).
DFS Algorithm
For the DFS implementation, we will categorize each vertex of the graph into two
categories:
1. Visited
2. Not Visited
The reason for this division is to prevent the traversal of the same node again thus, avoiding cycles in
the graph. Depth First Traversal in Data Structure uses a stack to keep track of the vertices to visit. A
boolean visited array is used to mark the visited vertices.
Here's a step-by-step explanation of how the DFS algorithm works::
Keep repeating steps 3 and 4 until the stack 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.
Depth First Search (DFS) algorithm traverses a graph in a depthward motion and uses a
stack to remember to get the next vertex to start a search, when a dead end occurs in any
iteration.
5
We choose B, mark it as visited and
put onto the stack. Here B does not
have any unvisited adjacent node. So,
we pop B from the stack.
6
We check the stack top for return to
the previous node and check if it has
any unvisited nodes. Here, we
find D to be on the top of the stack.
7
Only unvisited adjacent node is
from D is C now. So we visit C, mark
it as visited and put it onto the stack.
Pseudocode of DFS
DFS(G, u)
for each v ∈
u.visited = true
G.Adj[u] if
v.visited ==
false
DFS(G,v)
init() {
For each u ∈
G u.visited
= false
∈G
For each u
DFS(G, u)
}
Full form BFS is Breadth First Search. DFS is Depth First Search.
BFS uses a Queue to find the DFS uses a Stack to find the
Data Structure
shortest path. shortest path.
It works on the concept It works on the concept
Principle
of FIFO (First In First Out). of LIFO (Last In First Out).
Visiting of
Here, siblings are visited before Here, children are visited before
Siblings/
the children. their siblings.
Children
BFS is optimal for finding the DFS is not optimal for finding
Optimality
shortest path. the shortest path.
A minimum spanning tree of a graph is unique, if the weight of all the edges are distinct.
Otherwise, there may be multiple minimum spanning trees. (Specific algorithms typically output
one of the possible minimum spanning trees).
Minimum spanning tree is also the tree with minimum product of weights of edges. (It can be
easily proved by replacing the weights of all edges with their logarithms)
In a minimum spanning tree of a graph, the maximum weight of an edge is the minimum possible
from all possible spanning trees of that graph. (This follows from the validity of Kruskal's
algorithm).
The maximum spanning tree (spanning tree with the sum of weights of edges being maximum) of a
graph can be obtained similarly to that of the minimum spanning tree, by changing the signs of the
weights of all the edges to their opposite and then applying any of the minimum spanning tree
algorithm.
Choose a vertex
T = ∅;
weight edge.
U = { 1 };
T = T ∪ {(u, v)}
U = U ∪ {v}
Kruskal's algorithm is a minimum spanning tree algorithm that takes a graph as input
and finds the subset of the edges of that graph which
form a tree that includes every vertex
has the minimum sum of weights among all the trees that can be formed from the graph
How Kruskal's algorithm works
It falls under a class of algorithms called greedy algorithms that find the local optimum
in the hopes of finding a global optimum.
We start from the edges with the lowest weight and keep adding edges until we reach
our goal. The steps for implementing Kruskal's algorithm are as follows:
1. Sort all the edges from low weight to high
2. Take the edge with the lowest weight and add it to the spanning tree. If
adding the edge created a cycle, then reject this edge.
Choose the edge with the least weight, if there are more than 1, choose anyone
Choose the next shortest edge and add it
Choose the next shortest edge that doesn't create a cycle and add it
Choose the next shortest edge that doesn't create a cycle and add it
A=∅
KRUSKAL(G):
For each edge (u, v) ∈ G.E ordered by increasing order by weight(u, v):
MAKE-SET(v)
A = A ∪ {(u,
if FIND-SET(u) ≠ FIND-SET(v):
v)}
UNION(u, v)
return A
Time Complexity: O(E * logE) or O(E * logV)
Sorting of edges takes O(E * logE) time.
After sorting, we iterate through all edges and apply the find-union algorithm. The find and union
operations can take at most O(logV) time.
So overall complexity is O(E * logE + E * logV) time.
The value of E can be at most O(V2), so O(logV) and O(logE) are the same. Therefore, the overall
time complexity is O(E * logE) or O(E*logV)
Auxiliary Space: O(V + E), where V is the number of vertices and E is the number of edges in the
graph.
The shortest path problem is about finding a path between vertices in a graph such
that the total sum of the edges weights is minimum.
previous[] Bellman
Complexity
Best Case O(E
Complexity )
Average Case O(VE
Complexity )
Worst Case O(VE
Complexity )
2.4.2 Dijkstra Algorithm
Dijkstra's algorithm allows us to find the shortest path between any two vertices of a graph.
It differs from the minimum spanning tree because the shortest distance between
two vertices might not include all the vertices of the graph.
Djikstra used this property in the opposite direction i.e we overestimate the distance of each
vertex from the starting vertex. Then we visit each node and its neighbors to find the shortest
subpath to those neighbors.
The algorithm uses a greedy approach in the sense that we find the next best solution hoping
that the end result is the best solution for the whole problem.
Choose a starting vertex and assign infinity path values to all other devices
After each iteration, we pick the unvisited vertex with the least path length. So we choose 5 before 7
Notice how the rightmost vertex has its path length updated twice
Time Complexity: Time complexity of the above algorithm is O(max_flow * E). We run a loop while there
is an augmenting path. In worst case, we may add 1 unit flow in every iteration. Therefore the time
complexity becomes O(max_flow * E).
Ford-Fulkerson Algorithm
Initially, the flow of value is 0. Find some augmenting Path p and increase flow f on each
edge of p by residual Capacity cf (p). When no augmenting path exists, flow f is a maximum
flow.
FORD-FULKERSON METHOD (G, s, t)
1. Initialize flow f to 0
2. while there exists an augmenting path p
3. do argument flow f along p
4. Return f
2. do f [u, v] ← 0
3. f [u, v] ← 0
4. while there exists a path p from s to t in the residual network Gf.
5. do cf (p)←min?{ Cf (u,v):(u,v)is on p}
6. for each edge (u, v) in p
7. do f [u, v] ← f [u, v] + cf (p)
8. f [u, v] ←-f[u,v]
Example: Each Directed Edge is labeled with capacity. Use the Ford-Fulkerson algorithm to
find the maximum flow.
Solution: The left side of each part shows the residual network Gf with a shaded augmenting
path p,and the right side of each part shows the net flow f.
2.5 MAXIMUM FLOW
It is defined as the maximum amount of flow that the network would allow to flow from source to sink.
Multiple algorithms exist in solving the maximum flow problem. Two major algorithms to solve these kind
of problems are Ford-Fulkerson algorithm and Dinic's Algorithm.
In optimization theory, maximum flow problems involve finding a feasible flow through a flow network that
obtains the maximum possible flow rate.
The maximum flow problem can be seen as a special case of more complex network flow problems, such as
the circulation problem. The maximum value of an s-t flow (i.e., flow from source s to sink t) is equal to the
minimum capacity of an s-t cut (i.e., cut severing s from t) in the network, as stated in the max-flow min-cut
theorem.
1. For each edge (u, v) ∈ E, we associate a nonnegative weight capacity c (u, v) ≥ 0.If (u, v) ∉
Definition: A Flow Network is a directed graph G = (V, E) such that
The quantity f (u, v), which can be positive or negative, is known as the net flow from
vertex u to vertex v. In the maximum-flow problem, we are given a flow network G with
source s and sink t, and
a flow of maximum value from s to t.
A bipartite graph is an undirected graph G=(V,E) in which V can be partitioned into two sets V1 and V2 such
that (u,v) E implies either u V1 and v V12 or vice versa. • That is, all edges go between the two sets V1
and V2 and not within V1 and V2.
The bipartite matching is a set of edges in a graph is chosen in such a way, that no two
edges in that set will share an endpoint. The maximum matching is matching the
maximum number of edges.
When the maximum match is found, we cannot add another edge. If one edge is added to the
maximum matched graph, it is no longer a matching. For a bipartite graph, there can be more
than one maximum matching is possible.
Algorithm