Algorithm Final Notes
Algorithm Final Notes
Contents:
1. Introduction
2. Characteristics of Algorithm
3. Algorithm Complexity
4. Algorithm Analysis
5. Asymptotic Analysis
6. Asymptotic Notations
Introduction
As we know that all programming languages share basic code constructs like loops
(do, for, while), flow-control (if-else), etc.
These common constructs can be used to write an algorithm.
Example:
Let's try to learn algorithm-writing by using an example.
Problem − Design an algorithm to add two numbers and display the result.
Solution:
Page 1 of 55
step 1 − START
step 6 − print c
step 7 – STOP
There are so many ways of writing algorithm for the above problem. It can also be
written as –
step 3 − c ← a + b
step 4 − display c
step 5 – STOP
Characteristics of Algorithm
Page 2 of 55
Not all procedures can be called an algorithm. An algorithm should have the following
characteristics:
✓ Unambiguous
✓ Input
✓ Output
✓ Correct
✓ Complete
✓ Definiteness
✓ Sequence
✓ Language independence
Algorithm Complexity
Suppose X is an algorithm and n is the size of input data, the time and space used by
the algorithm X are the two main factors, which decide the efficiency of X.
Time Factor – Time is measured by counting the number of key operations such as
comparisons in the sorting algorithm.
Space Factor − Space is measured by counting the maximum memory space required
by the algorithm.
The complexity of an algorithm f(n) gives the running time and/or the storage space
required by the algorithm in terms of n as the size of input data.
Space Complexity
The space required by an algorithm is equal to the sum of the following two
components:
A fixed part that is a space required to store certain data and variables, that are
independent of the size of the problem.
Page 3 of 55
A variable part is a space required by variables, whose size depends on the size of the
problem.
Step 1 - START
Step 2 - C ← A + B + 10
Step 3 - Stop
return total;
}
The above algorithm requires 3 units of space for the parameters and 1 for the local
variable, and this never changes, so this is O(1).
Example: 2 Find the space complexity of the following algorithm
total=total+ a[i];
}
return total;
Page 4 of 55
}
One has to be able to choose the best algorithm for the problem at hand using some
scientific method.
To classify some data structures and algorithms as good, we need precise ways of
analyzing them in terms of resource requirement.
✓ Memory Usage
Running time is usually treated as the most important since computational time is the
most precious resource in most problem domains.
Page 5 of 55
There are two approaches to measure the efficiency of algorithms:
✓ Input Properties
✓ Operating Environment
Asymptotic Analysis
This means the first operation running time will increase linearly with the increase
in n and the running time of the second operation will increase exponentially
when n increases.
Similarly, the running time of both operations will be nearly the same if n is
significantly small.
Page 6 of 55
Usually, the time required by an algorithm falls under three types −
✓ Security
✓ Clarity
✓ Performance
Page 7 of 55
Answer should be independent of CPU speed, programming language, coding tricks,
etc.
Asymptotic Notations
Given two algorithms for a task, how do we find out which one is better?
One naive way of doing this is – implement both the algorithms and run the two
programs on your computer for different inputs and see which one takes less time.
There are many problems with this approach for analysis of algorithms.
✓ It might be possible that for some inputs, first algorithm performs better than
the second. And for some inputs second performs better.
✓ It might also be possible that for some inputs, first algorithm performs better on
one machine and the second works better on other machine for some other
inputs.
Asymptotic Analysis is the big idea that handles above issues in analysing algorithms.
Asymptotic analysis refers to computing the running time of any operation in
mathematical units of computation.
1. For loop
• The product of the number of iterations times the time of the inside
statements.
3. Consecutive Statements
• The testing time plus the larger running time of the cases.
Page 8 of 55
Analysis of iterative programs with simple examples
A loop or recursion that runs a constant number of times is also considered as O(1).
For example the following loop is O(1).
// Here c is a constant
for (int i = 1; i <= c; i++) {
}
2. O(n): Time Complexity of a loop is considered as O(n) if the loop variables is
incremented / decremented by a constant amount.
3. O(𝒏𝒄 ): Time complexity of nested loops is equal to the number of times the
innermost statement is executed.
For example the following sample loops have O(𝒏𝟐 ) time complexity
}
Page 9 of 55
4. O(Logn): Time Complexity of a loop is considered as O(Logn) if the loop variables
is divided / multiplied by a constant amount.
}
for (int i = n; i > 0; i /= c) {
✓ Big Oh notations(O)
✓ Big Omega notations(Ω)
1. Big oh Notations
The notation Ο(n) is the formal way to express the upper bound of an algorithm's
running time.
It measures the worst case time complexity or the longest amount of time an
algorithm can possibly take to complete.
Definition:
f (n)= O (g (n)) if there exist c, k ∊ ℛ+ such that for all n≥ k, f (n) ≤ c.g (n).
Example 1: Given f(n)=5n+3 and g(n)=n. Prove that running time f(n)=O(g(n)).
Proof: By the Big oh definition, f(n) is O(n) if and only if f(n)<=c.g(n) for all n>=k.
f(n)<=c.g(n), 5n+3<=c.n
Example 2: Given f(n)=𝒏𝟑 +20n+1 and g(n)= 𝒏𝟑 . Prove that running time
f(n)=O(g(n)).
Proof: By the Big oh definition, f(n) is O(n) if and only if f(n)<=c.g(n) for all n>=k.
f(n)<=c.g(n)
Page 10 of 55
𝒏𝟑 +20n+1 <=c. 𝒏𝟑
Example 3: Given f(n)=𝒏𝟑 +5n+3 and g(n)= 𝒏𝟐 . Prove that running time f(n) is not
O(g(n)).
Proof: By the Big oh definition, f(n) is O(n) if and only if f(n)<=c.g(n) for all n>=k.
f(n)<=c.g(n)
𝒏𝟑 +5n+3 <=c. 𝒏𝟐
For (c,k)=(9,1), the above statement is valid only once.
Therefore, the Big-Oh condition can not hold true for the above statement. (the left
side of the latter inequality is growing infinitely, so that there is no such constant
factor c).
The big-Oh notation gives an upper bound on the growth rate of a function.
The statement “f(n) is O(g(n))” means that the growth rate of f(n) is no more than the
growth rate of g(n).
Example:
1. For f(n)=4n & g(n)=n 2, prove f(n)=O(g(n))
Page 11 of 55
When n=4, f=16 and g=16, so this is the crossing over point
It measures the best case time complexity or the best amount of time an algorithm can
possibly take to complete.
Ω Notation can be useful when we have lower bound on time complexity of an
algorithm.
Definition:
Ω(g(n)) = {f(n): there exist positive constants c and k such
In simple terms, f(n)= ( g (n)) means that the growth rate of f(n) is greater than or
equal to g(n).
3. Big Theta Notation ()
The theta notation bounds a functions from above and below, so it defines exact
asymptotic behaviour.
A simple way to get Theta notation of an expression is to drop low order terms and
ignore leading constants.
Page 12 of 55
Big-Theta is a tight bound; both upper and lower bound. Tight bound is more precise,
but also more difficult to compute.
Definition:
Θ(g(n)) = {f(n): there exist positive constants c1, c2 and k such
that 0 <= c1*g(n) <= f(n) <= c2*g(n) for all n >= k}
The above definition means, if f(n) is theta of g(n), then the value f(n) is always between
c1*g(n) and c2*g(n) for large values of n (n >= k).
The definition of theta also requires that f(n) must be non -negative for values of n
greater than k.
If the algorithm for the input n takes 2n2 + 2n + 1 operations to finish, we say that its
time complexity is O(n2 ), but is also O(n3 ) , O(n10 ) , O(n30 ) and O(n100 ) .
Warning!
Algorithm that is Ө(f(n)) is also O(f(n)), but not vice versa!
Example:
From the definition of Big-theta notation above, we need to find constants c1,c2 and k
that satisfies the statement below.
c1*g(n) <= f(n) <= c2*g(n) for all n >= k
So, the above statement will always holds true for n>=1.
Less common notation:
Page 13 of 55
✓ little omega.
Little o provides strict upper bound (equality condition is removed from Big O) and
little omega provides strict lower bound (equality condition removed from big omega).
Little o notation (o)
Definition:
Let f(n) and g(n) be functions that map positive integers to positive real numbers.
We say that f(n) is ο(g(n)) (or f(n) Ε ο(g(n))) if for any real constant c > 0, there exists
an integer constant n0 ≥ 1 such that 0 ≤ f(n) < c*g(n).
In mathematical relation,f(n)=o(g(n))means
𝑓(𝑛)
𝐿𝑖𝑚 =0
n→∞ 𝑔(𝑛)
In order for that to be true, for any c, we have to be able to find an n0 that makes f(n)
< c * g(n) asymptotically true.
Then lets check the limits,
(7𝑛+8)
lim f(n)/g(n) =𝐿𝑖𝑚
n→∞ 𝑛2
(7)
= 𝐿𝑖𝑚
n→∞ 𝑛
= 0 (true)
Hence 7n + 8 ∈ o(n 2)
Little ω notation
Definition:
Let f(n) and g(n) be functions that map positive integers to positive real numbers.
We say that f(n) is ω(g(n)) (or f(n) ∈ ω(g(n))) if for any real constant c > 0, there exists
an integer constant n0 ≥ 1 such that f(n) > c * g(n) ≥ 0 for every integer n ≥ n0.
f(n) has a higher growth rate than g(n) so main difference between Big Omega (Ω) and
little omega (ω) lies in their definitions.
In the case of Big Omega f(n)=Ω(g(n)) and the bound is 0<=cg(n)<=f(n), but in case
of little omega, it is true for 0<=c*g(n)<f(n).
We use ω notation to denote a lower bound that is not asymptotically tight.
and, f(n) ∈ ω(g(n)) if and only if g(n) ∈ ο((f(n)).
Page 14 of 55
𝑓(𝑛)
𝐿𝑖𝑚 =∞
n→∞ 𝑔(𝑛)
The little omega(ο) running time can be proven by applying limit formula given
below.
𝑓(𝑛)
if 𝐿𝑖𝑚 =∞, then functions f(n) is ω(g(n))
n→∞ 𝑔(𝑛)
Here, we have functions f(n)=4n+6 and g(n)=1
4𝑛+6
𝐿𝑖𝑚 =∞
n→∞ 1
and, also for any c we can get n0 for this inequality 0 <= c*g(n) < f(n), 0 <= c*1 <
4n+6
Hence proved.
Do Not Be Confused!
Page 15 of 55
CHAPTER: TWO
RECURENCE
Contents
1. Introduction to recurrence
2. Substitution method
2. Iteration method
Introduction to recurrence
When an algorithm contains a recursive call to itself, its running time can often be
described by a recurrence.
Many algorithms are recursive in nature. When we analyse them, we get a recurrence
relation for time complexity. We get running time on an input of size n as a function of
n. For example, in Merge Sort, to sort a given array, we divide it in two halves and
recursively repeat the process for the two halves. Finally, we merge the results.
✓ Iteration Method
✓ Recurrence tree method, and
✓ Master Method
1. Substitution Method
The substitution method for solving recurrences is famously described using two steps:
Page 16 of 55
N.B: It is powerful method, but applied only in cases when it is easy to guess the form
of the solution. It can be used to establish either an upper or lower bounds on a
recurrence.
General Rules for substitution method
T(n) = 2T(n/2) + n
= 2(c(n/2)Log(n/2)) + n
= cnlog(n/2)+n
= cnlog 𝑛 - cnlog 2 + n, since log(2)=1
= cnlog 𝑛 - cn + n
= cnlog 𝑛 which is true for (c>=1 and n>=2), because for n=1,
Exercise
1. Apply the substitution method to show that the solution
Of T(n)=T(n−1)+n is O(𝑛2 ).
2. Consider the recurrence defined by T(n)=T( 𝑛⁄2 )+1. Show that the solution to this
recurrence is O(log 2 𝑛).
2. Iteration method
✓ No need to guess the solution.
Page 17 of 55
Example 1: T(n)= T(n-1)+n for n>1, T(0)= 0, T(1)= 1.
T(n)= T(n-1)+n
= [T(n-2)+n-1]+n= T(n-2)+(n-1)+n
= [T(n-3)+n-2]+(n-1)+n= T(n-3)+(n-2)+(n-1)+n
after i Substitutions----
= T(n-i)+(n-i+1)+(n-i+2)+------+n
at n th iteration
= T(0)+1+2+3+-------+n= n(n+1)/2
1
= (𝑛2 + 𝑛). So, the time complexity becomes T(n)=O(𝒏𝟐 )
2
Example 2: T(n)= 2T(n/2)+4n and T(1)= 4. Solve the recurrence using the iteration
method.
To draw the recurrence tree, we start from the given recurrence and keep drawing till
we find a pattern among levels.
The pattern is typically an arithmetic or geometric series.
𝑛
Example 1 Consider T (n) = 2T( )+ n 2
2
Page 18 of 55
Example 2: Consider the following recurrence
𝑛
T (n) = 4T( ) +n Obtain the asymptotic bound using recursion tree method.
2
Page 19 of 55
Example 3: Consider the following recurrence
When we add the values across the levels of the recursion trees, we get a value of n for every
level. The longest path from the root to leaf is
Page 20 of 55
Exercise 1. Given a recurrence T(n) =𝑇(𝑛⁄4 )+𝑇( 𝑛⁄2)+ 𝑛2 . Solve using recurrence
tree method.
2. Given a recurrence T(n) = 2𝑇 (𝑛⁄2 )+4n and T(1)=4 . Solve using recurrence tree
method.
4. Master Method:
✓ It is another method for solving recurrences of the form T(n)= aT(n/b)+f(n),
where a>=1 and b>1 are constants and f(n) is asymptotically positive
function.
✓ Requires memorization of three cases.
✓ Can solve many recurrences quite easily.
✓ Each sub problem has size of n/b, a and b are positive constants.
1. If f(n)= O(n logb a-€) for some constant €>0, then T(n)= (n logb a).
3. If f(n)= (n logb a+€ ) for some constant €>0, then T(n)= (f(n)).
Example 1: Given T(n)= 9T(n/3) +n. Solve the recurrence using the master method.
Page 21 of 55
Is f(n)= nlog ba?
Example 2: Given T(n)= 2T(n/2)+n. Solve the recurrence using the master method.
Soln: We have a=2, b=2, f(n)= n, thus we need to check that
Is f(n)= nlog ba ?
Example 3: Given T(n)= 4T(n/2)+𝑛3 . Solve the recurrence using the master method.
Is f(n)= nlog ba ?
Is 𝑛3 = n logba
= n log24
1. Given T(n)= 9T(n/3)+n. Solve the recurrence using the master method.
Here we have a=9, b=3 and d=1.
Page 22 of 55
T(n) = (nlogba)
= (nlog39)
= (𝑛2 )
2. Given T(n)= 8T(n/2)+1000𝑛2 . Solve the recurrence using the master method.
Here we have a=8, b=2 and d=2. Then we need to compare a and 𝑏𝑑 .
T(n) = (nlogba)
= (nlog28)
= (𝑛3 )
3. Given T(n)= 2T(n/2)+10n. Solve the recurrence using the master method.
= (nlog n)
Page 23 of 55
CHAPTER: THREE
SEARCHING AND SORTING ALGORITHMS
1. Linear search
Linear search is a very simple search algorithm. In this type of search, a sequential
search is made over all items one by one. Every item is checked and if a match is found
then that particular item is returned, otherwise the search continues till the end of the
data collection.
Algorithm
Step 1: Set i to 1
Step 2: if i > n then go to step 7
Step 5: Go to Step 2
Step 6: Print Element x Found at index i and go to step 8
Step 8: Exit
Pseudocode
end function
2. Binary search
Binary search is a fast search algorithm with run-time complexity of Ο(log n). This
search algorithm works on the principle of divide and conquer. For this algorithm to
work properly, the data collection should be in the sorted form.
Page 24 of 55
Binary search looks for a particular item by comparing the middle most item of the
collection. If a match occurs, then the index of item is returned. If the middle item is
greater than the item, then the item is searched in the sub-array to the right of the middle
item. Otherwise, the item is searched for in the sub-array to the left of the middle item.
This process continues on the sub-array as well until the size of the subarray reduces to
zero.
Pseudo code
Procedure binary_search
A ← sorted array
n ← size of array
x ← value ot be searched
Set lowerBound = 0
Set upperBound = n-1
if A[midPoint] > x
set upperBound = midPoint - 1
if A[midPoint] = x
EXIT: x found at location midPoint
end while
end procedure
Page 25 of 55
ADVANCED SEARCHING ALGORITHMS
3. Interpolation search
In binary search, if the desired data is not found then the rest of the list is divided in two
parts, lower and higher. The search is carried out in either of them.
Even when the data is sorted, binary search does not take advantage to probe the
position of the desired data.
If a match occurs, then the index of the item is returned. To split the list into two parts,
we use the following method:
pos = low + [ (x-list[low])*(high-low) / (list[high]-list[low]) ]
N.B: The idea of the formula is to return higher value of pos when element to be
searched is closer to arr[hi]. And smaller value when closer to arr[lo]
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 sub-
array to the left of the middle item. This process continues on the sub -array as well until
the size of subarray reduces to zero.
Page 26 of 55
Step 2 − If it is a match, return the index of the item, and exit.
Pseudocode:
A → Array list
N → Size of A
X → Target Value
Procedure Interpolation_Search()
Set Lo → 0
Set Mid → -1
Set Hi → N-1
While X does not match
end if
if A[Mid] < X
Set Lo to Mid+1
else if A[Mid] > X
Set Hi to Mid-1
end if
end if
Page 27 of 55
End While
End Procedure
4. Jump search
Jump search is a searching algorithm for sorted arrays. The basic idea is to check
fewer elements by jumping ahead by fixed steps or skipping some elements in place of
searching all elements.
Algorithm for jump search
Step 3: If element at current position < target element, then do Linear Search on element
from position current position -B to current position else go to step 2. If current position
is last position, go to step 4.
Step 4: Exit. Element not found.
int prev = 0;
step += sqrt(n);
if (prev >= n)
return -1; }
Page 28 of 55
prev++;
return -1; }
// If element is found
if (arr[prev] == x)
return prev;
return -1;
For example, suppose we have an array arr[] of size n and block (to be jumped) size m.
Then we search at the indexes arr[0], arr[m], arr[2m]…..arr[km] and so on.
Once we find the interval (arr[km] < x < arr[(k+1)m]), we perform a linear search
operation from the index km to find the element x.
Therefore, the total number of comparisons in the worst case will be ((N/B) + B-1). The
value of the function ((N/B) + B-1) will be minimum when B = √N.
Therefore, the best block size is B = √N.
✓ The time complexity of Jump Search is between Linear Search ( ( O(n) ) and
Binary Search ( O (Log n) ).
✓ Binary Search is better than Jump Search, but Jump search has an advantage that
we traverse back only.
Page 29 of 55
Example (solved in lecture class):
Find the element 22 from the below array using jump search algorithm:
4 6 8 10 13 14 20 22 25 30
5. Exponential search
The idea is to start with sub-list of size 1. Compare the last element of the list with the
target element, then try size 2, then 4 and so on until last element of the list is not greater.
Once we find a location loc (after repeated doubling of list size), we know that the
element must be present between loc/2 and loc.
if(arr[0]==k)
return 0;
int i=1;
while(i<=n&&arr[min(i,(n-1))]<k){
i=i*2;}
return binary_search(arr,i/2,min(i,(n-1)),k); }
//Perform binary search
while(l<=r){
int mid=(l+r)/2;
Page 30 of 55
if(arr[mid]==k)
return mid;
else if(arr[mid]>k)
r=mid-1;
else
l=mid+1; }
return -1; }
Time complexity of exponential search
Exponential Binary Search is useful for unbounded searches where size of array is
infinite.
It works better than Binary Search for bounded arrays when the element to be searched
is closer to the beginning of the array.
Find the element 80 from the below array using exponential search algorithm:
8 10 13 20 36 37 40 45 60 80
Page 31 of 55
SORTING ALGORITHMS
Sorting is one of the most important operations performed by computers. It is a process
of reordering a list of items in either increasing or decreasing order.
Decreasing Order
A sequence of values is said to be in decreasing order, if the successive element is less
than the current one. For example, 9, 8, 6, 4, 3, 1 are in decreasing order, as every next
element is less than the previous element.
Non-Increasing Order
A sequence of values is said to be in non-increasing order, if the successive element is
less than or equal to its previous element in the sequence. This order occurs when the
Page 32 of 55
greater than or equal to its previous element in the sequence. This order occurs when
the sequence contains duplicate values. For example, 1, 3, 3, 6, 8, 9 are in non -
decreasing order, as every next element is greater than or equal to (in case of 3) but not
less than the previous one.
The following are simple sorting algorithms used to sort small-sized lists.
✓ Insertion Sort
✓ Selection Sort
✓ Bubble Sort
1. Insertion sort
This is an in-place comparison-based sorting algorithm. Here, a sub-list is maintained
which is always sorted. For example, the lower part of an array is maintained to be
sorted.
An element which is to be 'inserted’ in this sorted sub-list, has to find its appropriate
place and then it has to be inserted there. Hence the name, insertion sort.
The array is searched sequentially and unsorted items are moved and inserted in to the
sorted sub-list (in the same array). This algorithm is not suitable for large data sets as
its average and worst case complexity are of Ο(𝒏𝟐 ), where n is the number of items.
In each step, pick the first element from the unsorted part and insert in the correct
position in the sorted part.
Initially, the sorted part is empty and finally, the unsorted part is empty.
int key, j;
j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
Page 33 of 55
j = j - 1; }
arr[j + 1] = key; } }
Example: Sort the elements [12,11,13,5,6] using insertion sort
i = 3. 5 will move to the beginning and all other elements from 11 to 13 will move one
position ahead of their current position.
5, 11, 12, 13, 6
i = 4. 6 will move to position after 5, and elements from 11 to 13 will move one
position ahead of their current position.
5, 6, 11, 12, 13
✓ In the best case, we already have a sorted array but we need to go through the
array at least once to be sure! Therefore, in the best case, Insertion Sort
takes O(n) time complexity.
✓ Since it is in-place sorting, space complexity is O(1).
2. Selection sort
The smallest element is selected from the unsorted array and swapped with the leftmost
element, and that element becomes a part of the sorted array. This process continues
moving unsorted array boundary by one element to the right.
This algorithm is not suitable for large data sets as its average and worst case
complexities are of O(n²), where n is the number of items.
Page 34 of 55
Selection sort works as follows:
3. Repeat the steps above for remainder of the list (starting at the second position)
Working principle(Implementation) of selection sort
for(int j=i+1;j<n;j++) {
if(arr[j]<arr[min_indx]){
min_indx=j;
}}
swap(arr,i,min_indx); } }
{
int temp;
temp = arr[firstIndex];
arr[firstIndex] = arr[min_indx];
arr[min_indx] = temp;
}
Example: Sort arr[] = [64, 25, 12, 22, 11] using selection sort
11 25 12 22 64
Step 2: Find the minimum element in arr[1...4] and place it at beginning of arr[1...4]
11 12 25 22 64
Step 3: Find the minimum element in arr[2...4] and place it at beginning of arr[2...4]
11 12 22 25 64
Page 35 of 55
Step 4: Find the minimum element in arr[3...4] and place it at beginning of arr[3...4]
11 12 22 25 64
Time and Space Complexity of selection sort
✓ To sort an array with Selection Sort, you must iterate through the array once for
every value you have in the array.
✓ If we have n values in our array, Selection Sort has a time complexity of O(n²) in
the worst case.
✓ In the best case, we already have a sorted array but we need to go through the
array O(n²) times to be sure!
✓ Therefore, Selection Sort’s best and worst case time complexity are the same.
It works by repeatedly stepping through the list to be sorted, comparing two items at a
time and swapping them if they are in the wrong order.
The pass through the list is repeated until no swaps are needed, which means the list is
sorted.
int i, j;
bool swapped;
Page 36 of 55
swapped = false;
swap(arr,j, (j+1));
swapped = true; } }
if (swapped == false)
break; } }
Now, since these elements are already in order (8 > 5), algorithm does not swap
them.
Second Pass (i=1): ( 1 4 2 5 8 ) –> ( 1 4 2 5 8 )
( 1 2 4 5 8 ) –> ( 1 2 4 5 8 )
( 1 2 4 5 8 ) –> ( 1 2 4 5 8 )
Now, the array is already sorted, but our algorithm does not know if it is completed.
The algorithm needs another pass without any swap to know it is sorted.
Third Pass (i=2) : ( 1 2 4 5 8 ) –> ( 1 2 4 5 8 )
(1 2 4 5 8) –> (1 2 4 5 8)
(1 2 4 5 8) –> (1 2 4 5 8)
(1 2 4 5 8) –> (1 2 4 5 8)
Page 37 of 55
Time and space complexity of bubble sort
✓ In Bubble Sort, n-1 comparisons will be done in the 1st pass, n-2 in 2nd pass, n-
3 in 3rd pass and so on. So the total number of comparisons will be:
(n-1) + (n-2) + (n-3) + ..... + 3 + 2 + 1
Sum = n(n-1+1)/2
T(n)= O(𝒏𝟐 ).
✓ Worst and Average Case Time Complexity: O(𝑛2 ). Worst case occurs when
array is reverse sorted.
✓ Best Case Time Complexity: O(n). Best case occurs when array is already sorted.
✓ Space Complexity is: O(1), since it is in-place sorting
int i, j;
bool swapped;
swapped = true; } }
// IF no two elements were swapped by inner loop, then break
if (swapped == false)
break; } }
Here, algorithm compares the first two elements, and swaps since 5 > 1.
Page 38 of 55
( 1 5 4 2 8 ) –> ( 1 4 5 2 8 ), Swap since 5 > 4.
Now, since these elements are already in order (8 > 5), algorithm does not swap
them.
Second Pass (i=1): ( 1 4 2 5 8 ) –> ( 1 4 2 5 8 )
( 1 2 4 5 8 ) –> ( 1 2 4 5 8 )
Now, the array is already sorted, but our algorithm does not know if it is completed.
The algorithm needs another pass without any swap to know it is sorted.
Third Pass (i=2) : ( 1 2 4 5 8 ) –> ( 1 2 4 5 8 )
(1 2 4 5 8) –> (1 2 4 5 8)
(1 2 4 5 8) –> (1 2 4 5 8)
(1 2 4 5 8) –> (1 2 4 5 8)
Finally, the algorithm terminates.
✓ In Bubble Sort, n-1 comparisons will be done in the 1st pass, n-2 in 2nd pass, n-
3 in 3rd pass and so on. So the total number of comparisons will be:
(n-1) + (n-2) + (n-3) + ..... + 3 + 2 + 1
Sum = n(n-1+1)/2
T(n)= O(𝒏𝟐 ).
✓ Worst and Average Case Time Complexity: O(𝑛2 ). Worst case occurs when
array is reverse sorted.
✓ Best Case Time Complexity: O(n). Best case occurs when array is already sorted.
✓ Space Complexity is: O(1), since it is in-place sorting
Page 39 of 55
ADVANCED SORTING ALGORITHMS
3. [Link]
4. [Link] (Very important site)
Quick Sort is also based on the concept of Divide and Conquer, just like merge sort. But in
quick sort all the heavy lifting (major work) is done while dividing the array into subarrays,
while in case of merge sort, all the real work happens during merging the subarrays. In case of
quick sort, the combine step does absolutely nothing.
It is also called partition-exchange sort. This algorithm divides the list into three main parts:
Pivot element can be any element from the array, it can be the first element, the last element
or any random element. In this tutorial, we will take the rightmost element or the last element
as pivot.
The general algorithm for Quicksort is given below.
quicksort (A, low, high)
begin
begin
Page 40 of 55
End
end
For example: In the array {52, 37, 63, 14, 17, 8, 6, 25}, we take 25 as pivot. So after the first
pass, the list will be changed like this.
{6 8 17 14 25 63 37 52}
Hence after the first pass, pivot will be set at its position, with all the elements smaller to it
on its left and all the elements larger than to its right. Now 6 8 17 14 and 63 37 52 are
considered as two separate sub arrays, and same recursive logic will be applied on them, and
we will keep doing this until the complete array is sorted.
1. After selecting an element as pivot, which is the last index of the array in our case, we
divide the array for the first time.
2. In quick sort, we call this partitioning. It is not simple breaking down of array into 2
subarrays, but in case of partitioning, the array elements are positioned so that all the
elements smaller than the pivot will be on the left side of the pivot and all the
elements greater than the pivot will be on the right side of it.
3. And the pivot element will be at its final sorted position.
4. The elements to the left and right, may not be sorted.
5. Then we pick subarrays, elements on the left of pivot and elements on the right
of pivot, and we perform partitioning on them by choosing a pivot in the subarrays.
Whereas if partitioning leads to almost equal subarrays, then the running time is the best,
with time complexity as O (n*log n).
Page 41 of 55
2. Merge Sort Algorithm
Merge Sort follows the rule of Divide and Conquer to sort a given set of numbers/elements,
recursively, hence consuming less time. Merge sort, runs in O (n*log n) time in all the cases.
Before jumping on to, how merge sort works and its implementation, first let’s understand what
is the rule of Divide and Conquer?
When Britishers came to India, they saw a country with different religions living in harmony,
hardworking but naive citizens, unity in diversity, and found it difficult to establish their
empire. So, they adopted the policy of Divide and Rule. Where the population of India was
collectively a one big problem for them, they divided the problem into smaller problems, by
instigating rivalries between local kings, making them stand against each other, and this
worked very well for them.
Well, that was history, and a socio-political policy (Divide and Rule), but the idea here is, if
we can somehow divide a problem into smaller sub-problems, it becomes easier to eventually
solve the whole problem.
In Merge Sort, the given unsorted array with n elements, is divided into n subarrays, each
having one element, because a single element is always sorted in itself. Then, it repeatedly
merges these subarrays, to produce new sorted subarrays, and in the end, one complete sorted
array is produced.
Page 42 of 55
How Merge Sort Works?
As we have already discussed that merge sort utilizes divide-and-conquer rule to break the
problem into sub-problems, the problem in this case being, sorting a given array.
In merge sort, we break the given array midway, for example if the original array had 6
elements, then merge sort will break it down into two subarrays with 3 elements each. But
breaking the original array into 2 smaller subarrays is not helping us in sorting the array. So,
we will break these subarrays into even smaller subarrays, until we have multiple subarrays
with single element in them. Now, the idea here is that an array with a single element is already
sorted, so once we break the original array into subarrays which has only a single element, we
have successfully broken down our problem into base problems. And then we have to merge
all these sorted subarrays, step by step to form one single sorted array.
General Algorithm
The general pseudo-code for the merge sort technique is given below.
Declare an array Arr of length N
If N=1, Arr is already sorted
If N>1,
Left = 0, right = N-1
Find middle = (left + right)/2
Call merge_sort(Arr,left,middle) =>sort first half recursively
Call merge_sort(Arr,middle+1,right) => sort second half recursively
Call merge(Arr, left, middle, right) to merge sorted arrays in above steps.
Exit
Page 43 of 55
In merge sort we follow the following steps:
1. We take a variable p and store the starting index of our array in this. And we take
another variable r and store the last index of array in it.
2. Then we find the middle of the array using the formula (p + r)/2 and mark the middle
index as q, and break the array into two subarrays, from p to q and from q +
1 to r index.
3. Then we divide these 2 subarrays again, just like we divided our main array and this
continues.
4. Once we have divided the main array into subarrays with single elements, then we
start merging the subarrays.
Page 44 of 55
Complexity Analysis of Merge Sort
Merge Sort is quite fast, and has a time complexity of O (n*log n). It is also a stable sort, which
means the "equal" elements are ordered in the same order in the sorted list. In this section we
will understand why the running time for merge sort is O (n*log n).
What is a Heap?
Heap is a special tree-based data structure, that satisfies the following special heap properties:
1. Shape Property: Heap data structure is always a Complete Binary Tree, which
means all levels of the tree are fully filled.
2. Heap Property: All nodes are either greater than or equal to or less than or equal to
each of its children. If the parent nodes are greater than their child nodes, heap is
called a Max-Heap, and if the parent nodes are smaller than their child nodes, heap is
called Min-Heap.
Page 45 of 55
How Heap Sort Works?
Heap sort algorithm is divided into two basic parts:
Shell sort is often termed as an improvement over insertion sort. In insertion sort, we take
increments by 1 to compare elements and put them in their proper position.
In shell sort, the list is sorted by breaking it down into a number of smaller sub lists. It’s not
necessary that the lists need to be with contiguous elements. Instead, shell sort technique uses
increment i, which is also called “gap” and uses it to create a list of elements that are “i”
elements apart.
Page 46 of 55
Time Complexity: Time complexity of shell sort is O(n2). The gap is reducing by half in every
iteration.
shell_sort (A, N)
begin
set flag = 0
end
for i = 0 to i< (N-gap_size) repeat
begin
set flag = 0
end
end
Page 47 of 55
CHAPTER 4
Selecting a proper design technique for algorithms is a complex but important task.
Following are some of the main algorithm design techniques:
1. Brute-force or exhaustive search
2. Divide and Conquer
3. Greedy Algorithms
4. Dynamic Programming
5. Branch and Bound Algorithm
6. Randomized Algorithm
7. Backtracking
8. Amortized analysis
A given problem can be solved in various different approaches and some approaches
deliver much more efficient results than others. Algorithm analysis is a technique used
to measure the effectiveness and performance of the algorithms. It helps to determine
the quality of an algorithm based on several parameters such as user -friendliness,
maintainability, security, space usage and usage of other resources.
Selecting a proper designing technique for a parallel algorithm is the most difficult and
important task. Most of the parallel programming problems may have more than one
solution. In this chapter, we will discuss the following designing techniques for par allel
algorithms −
Greedy Method
Dynamic Programming
Backtracking
Branch & Bound
Linear Programming
Page 48 of 55
In the divide and conquer approach, the problem is divided into several small sub-
problems. Then the sub-problems are solved recursively and combined to get the
solution of the original problem.
The divide and conquer approach involves the following steps at each level −
Greedy Method
In greedy algorithm of optimizing solution, the best solution is chosen at any moment.
A greedy algorithm is very easy to apply to complex problems. It decides which step
will provide the most accurate solution in the next step.
This algorithm is a called greedy because when the optimal solution to the smaller
instance is provided, the algorithm does not consider the total program as a whole. Once
a solution is considered, the greedy algorithm never considers the same solution aga in.
A greedy algorithm works recursively creating a group of objects from the smallest
possible component parts. Recursion is a procedure to solve a problem in which the
solution to a specific problem is dependent on the solution of the smaller instance of
that problem.
Dynamic Programming
Dynamic programming is an optimization technique, which divides the problem into
smaller sub-problems and after solving each sub-problem, dynamic programming
combines all the solutions to get ultimate solution. Unlike divide and conquer method,
dynamic programming reuses the solution to the sub-problems many times. Recursive
algorithm for Fibonacci Series is an example of dynamic programming.
Backtracking Algorithm
Page 49 of 55
A branch and bound algorithm is an optimization technique to get an optimal solution
to the problem. It looks for the best solution for a given problem in the entire space of
the solution. The bounds in the function to be optimized are merged with the valu e of
the latest best solution. It allows the algorithm to find parts of the solution space
completely.
The purpose of a branch and bound search is to maintain the lowest -cost path to a target.
Once a solution is found, it can keep improving the solution. Branch and bound search
is implemented in depth-bounded search and depth–first search.
Linear Programming
Linear programming describes a wide class of optimization job where both the
optimization criterion and the constraints are linear functions. It is a technique to get
the best outcome like maximum profit, shortest path, or lowest cost.
In this programming, we have a set of variables and we have to assign absolute values
to them to satisfy a set of linear equations and to maximize or minimize a given linear
objective function.
Dynamic Programming
Page 50 of 55
Backtracking Algorithms
For example, consider the SudoKo solving Problem, we try filling digits one by one.
Whenever we find that current digit cannot lead to a solution, we remove it (backtrack)
and try next digit. This is better than naive approach (generating all possible
combinations of digits and then trying every combination one by one) as it drops a set
of permutations whenever it backtracks.
Branch and Bound Algorithm
Branch and bound is an algorithm design paradigm which is generally used for solving
combinatorial optimization problems. These problems are typically exponential in
terms of time complexity and may require exploring all possible permutations in worst
case. The Branch and Bound Algorithm technique solves these problems relatively
quickly.
Let us consider the 0/1 Knapsack problem to understand Branch and Bound.
There are many algorithms by which the knapsack problem can be solved:
================================
Brute-force algorithm
It is a type of algorithm that tries a large number of patterns to solve a problem. A
common example of a brute force algorithm is a security threat that attempts to guess a
password using known common passwords.
=====================
Dynamic programming
A dynamic-programming algorithm solves each sub-problem just once and then saves
its answer in a table, thereby avoiding the work of re-computing the answer every time
it solves each sub-problem.
Page 51 of 55
optimal solution to the problem, as opposed to the optimal solution, since there may be
several solutions that achieve the optimal value.
four steps:
1. Characterize the structure of an optimal solution.
Amortize Analysis
This analysis is used when the occasional operation is very slow, but most of the
operations which are executing very frequently are faster. Data structures we need
amortized analysis for Hash Tables, Disjoint Sets etc. In the Hash-table, the most of
the time the searching time complexity is O (1), but sometimes it executes O(n)
operations. When we want to search or insert an element in a hash table for most of
the cases it is constant time taking the task, but when a collision occurs, it needs
O(n) times operations for collision resolution.
Page 52 of 55
Chapter Five
Algorithm for graph problems
An algorithm is a mathematical process to solve a problem using a well-defined or optimal
number of steps. It is simply the basic technique used to get a specific job done. A graph is
an abstract notation used to represent the connection between all pairs of objects. Graphs
are widely-used mathematical structures visualized by two basic components: nodes and
edges. Graph algorithms are used to solve the problems of representing graphs as networks
like airline flights, how the Internet is connected, or social network connectivity on
Facebook. They are also popular in NLP and machine learning to form networks. Some of
the top graph algorithms include:
Page 53 of 55
Chapter Six
Problem Complexity
Problem complexity refers to the resources (primarily time and space) required by an
algorithm to solve a problem as the input size grows. It's a way to characterize the inherent
difficulty of a problem, independent of specific implementations or hardware. We express
complexity using Big O notation (and related notations).
Analyzing Algorithm Complexity:
1. Identify the Basic Operations: Determine the operations that contribute most to the
running time (e.g., comparisons, assignments, arithmetic operations).
2. Count the Operations: Count how many times these basic operations are executed as a
function of the input size n.
3. Determine the Dominant Term: Identify the term that grows the fastest as n increases.
This is the dominant term.
4. Express in Big O Notation: Drop the constant factors and lower-order terms. Express
the complexity using Big O notation.
Page 54 of 55
• NP: The class of problems whose solutions can be verified in polynomial time. It's not
known whether all problems in NP can also be solved in polynomial time (the P vs. NP
problem).
• NP-Complete: The hardest problems in NP. If you can solve any NP-complete problem
in polynomial time, you can solve all problems in NP in polynomial time.
• NP-hard: Problems that are at least as hard as the hardest problems in NP, but they may
not be in NP themselves.
Page 55 of 55