algorithm_lecture_33
algorithm_lecture_33
3.2 Pseudocode
i. Pseudocode Approach:
3
5. To compare the performance of the algorithm with respect to
other techniques.
6. It is the best method of description without describing the
implementation detail.
7. The Algorithm gives a clear description of requirements and goal
of the problem to the designer.
8. A good design can produce a good solution.
9. To understand the flow of the problem.
10. To measure the behavior (or performance) of the methods in all
cases (best cases, worst cases, average cases)
11. With the help of an algorithm, we can also identify the resources
(memory, input-output) cycles required by the algorithm.
12. With the help of algorithm, we convert art into a science.
13. To understand the principle of designing.
14. We can measure and analyze the complexity (time and space) of
the problems concerning input size without implementing and
running it; it will reduce the cost of design.
Analysis of Algorithm
The analysis is a process of estimating the efficiency of an algorithm and that is, trying to
know how good or how bad an algorithm could be. There are two main parameters based
on which we can analyze the algorithm:
Space Complexity: The space complexity can be understood as the amount of space
required by an algorithm to run to completion.
Time Complexity: Time complexity is a function of input size n that refers to the
amount of time needed by an algorithm to run to completion.
Or in other words, you should describe what you want to include in your code in an English-
like language for it to be more readable and understandable before implementing it, which
is nothing but the concept of Algorithm.
In general, if there is a problem P1, then it may have many solutions, such
that each of these solutions is regarded as an algorithm. So, there may be
many algorithms such as A1, A2, A3, …, An.
So, the Design and Analysis of Algorithm talks about how to design
various algorithms and how to analyze them. After designing and
analyzing, choose the best algorithm that takes the least time and the least
memory and then implement it as a program in C or any preferable
language.
We will be looking more on time rather than space because time is instead
a more limiting parameter in terms of the hardware. It is not easy to take
a computer and change its speed. So, if we are running an algorithm on a
particular platform, we are more or less stuck with the performance that
platform can give us in terms of speed.
3.1.1 Worst-case time complexity: For 'n' input size, the worst-case
time complexity can be defined as the maximum amount of time
needed by an algorithm to complete its execution. Thus, it is
nothing but a function defined by the maximum number of steps
performed on an instance having an input size of n. Computer
Scientists are more interested in this.
3.1.2 Average case time complexity: For 'n' input size, the average-
case time complexity can be defined as the average amount of time
needed by an algorithm to complete its execution. Thus, it is
nothing but a function defined by the average number of steps
performed on an instance having an input size of n.
3.1.3 Best case time complexity: For 'n' input size, the best-case time
complexity can be defined as the minimum amount of time needed
by an algorithm to complete its execution. Thus, it is nothing but a
5
function defined by the minimum number of steps performed on
an instance having an input size of n.
The term algorithm complexity measures how many steps are required by
the algorithm to solve the given problem. It evaluates the order of count
of operations executed by an algorithm as a function of input data size.
The complexity can be found in any form such as constant, logarithmic, linear, n*log(n),
quadratic, cubic, exponential, etc. It is nothing but the order of constant, logarithmic, linear
and so on, the number of steps encountered for the completion of a particular algorithm.
To make it even more precise, we often call the complexity of an algorithm as "running
time".
For example, if there exist 500 elements, then it will take about 500 steps.
Basically, in linear complexity, the number of elements linearly depends
on the number of steps. For example, the number of steps for N elements
can be N/2 or 3*N.
It also imposes a run time of O(n*log(n)). It undergoes the
execution of the order N*log(N) on N number of elements to solve
the given problem.
For a given 1000 elements, the linear complexity will execute
10,000 steps for solving a given problem.
If N = 100, it will endure 10,000 steps. In other words, whenever the order
of operation tends to have a quadratic relation with the input data size, it
results in quadratic complexity.
For example, for N number of elements, the steps are found to be in the
order of 3*N2/2.
Since the constants do not hold a significant effect on the order of count
of operation, so it is better to ignore them.
Thus, to consider an algorithm to be linear and equally efficient, it must undergo N, N/2 or
3*N count of operation, respectively, on the same number of elements to solve a particular
problem
So, to find it out, we shall first understand the types of the algorithm we
have. There are two types of algorithms:
8
However, it is worth noting that any program that is written in iteration
could be written as recursion. Likewise, a recursive program can be
converted to iteration, making both of these algorithms equivalent to each
other.
But to analyze the iterative program, we have to count the number of times
the loop is going to execute, whereas in the recursive program, we use
recursive equations, i.e., we write a function of F(n) in terms of F(n/2).
Suppose the program is neither iterative nor recursive. In that case, it can
be concluded that there is no dependency of the running time on the input
data size, i.e., whatever is the input size, the running time is going to be
a constant value. Thus, for such programs, the complexity will be O(1).
Consider the following programs written in simple English and does not
correspond to any syntax.
Example1
In the first example, we have an integer i and a for loop running from i
equals 1 to n. Now the question arises, how many times does the name
get printed?
A()
{
int i;
for (i=1 to n)
printf("Abdullahi");
}
9
In this case, firstly, the outer loop will run n times, such that for each time,
the inner loop will also run n times. Thus, the time complexity will be
O(n2).
Example3:
A()
{
i = 1; S = 1;
while (S<=n)
{
i++;
SS = S + i;
printf("Abdullahi");
}
}
As we can see from the above example, we have two variables; i, S and
then we have while S<=n, which means S will start at 1, and the entire
loop will stop whenever S value reaches a point where S becomes greater
than n.
Initially;
i=1, S=1
After 1st iteration;
i=2, S=3
After 2nd iteration;
i=3, S=6
After 3rd iteration;
i=4, S=10 … and so on.
Thus, it is nothing but a series of the sum of first n natural numbers, i.e.,
by the time i reaches k, the value of S will be � (�+1) .
2
� (�+1)
To stop the loop, has to be greater than n, and when we solve
2
this equation,
10
we will get > n.
Hence, it can be concluded that we get a complexity of O(√n) in this
case.
Example1
A(n)
{
if (n>1)
return (A(n-1))
}
Solution;
Here we will see the simple Back Substitution method to solve the
above problem.
Now, according to Eqn. (1), i.e. T(n) = 1 + T(n-1), the algorithm will run
until n>1. Basically, n will start from a very large number, and it will
decrease gradually. So, when T(n) = 1, the algorithm eventually stops,
and such a terminating condition is called anchor condition, base
condition or stopping condition.
11
Thus, for k = n-1, the T(n) will become.
12
Following are some standard algorithms that are of the Divide and
Conquer algorithms variety.
Tower of Hanoi
Dijkstra Shortest Path
Fibonacci sequence
Matrix chain multiplication
Egg-dropping puzzle, etc
The branch and bound method is a solution approach that partitions the
feasible solution space into smaller subsets of solutions. , can assume
any integer value greater than or equal to zero is what gives this model its
designation as a total integer model.
Knapsack problems
Traveling Salesman Problem
Job Assignment Problem, etc
Given a problem:
\Backtrack(s)
if is not a solution return false if is a new solution add to list of
solutions backtrack(expand s)
There are the following scenarios in which you can use the
backtracking:
In this function, the n2 term dominates the function that is when n gets
sufficiently large.
16
3.2.1 Asymptotic analysis
ƒ (n) ~ n2.
Asymptotic notations are used to write fastest and slowest possible
running time for an algorithm. These are also referred to as 'best case' and
'worst case' scenarios respectively.
17
Hence, function g (n) is an upper bound for function f (n), as g (n) grows
faster than f (n)
Examples:
18
Example:
f (n) =8n2+2n-3≥8n2-3
=7n2+(n2-3)≥7n2 (g(n))
Thus, k1=7
Hence, the complexity of f (n) can be represented as Ω (g (n))
The function f (n) = θ (g (n)) [read as "f is the theta of g of n"] if and
only if there exists positive constant k1, k2 and k0 such that
k1 * g (n) ≤ f(n)≤ k2 g(n)for all n, n≥ n0
For Example:
Self-Assessment Exercise
19
Recursion is a method of solving problems that involves breaking a
problem down into smaller and smaller sub-problems until you get to a
small enough problem that it can be solved trivially. In computer science,
recursion involves a function calling itself. While it may not seem like
much on the surface, recursion allows us to write elegant solutions to
problems that may otherwise be very difficult to program.
There are two main instances of recursion. The first is when recursion is
used as a technique in which a function makes one or more calls to itself.
The second is when a data structure uses smaller instances of the exact
same type of data structure when it represents itself.
n! = n ⋅ (n−1) ⋅ (n−2) … 3 ⋅ 2 ⋅ 1
20
Note, if n = 0, then n! = 1. This is important to take into account,
because it will serve as our base case.
Take this example:
4! = 4 ⋅ 3 ⋅ 2 ⋅ 1 = 24.
So how can we state this in a recursive manner? This is where the
concept of base case comes in.
Base case is a key part of understanding recursion, especially when it
comes to having to solve interview problems dealing with recursion. Let’s
rewrite the above equation of 4! so it looks like this:
4! = 4 ⋅ (3 ⋅ 2 ⋅ 1) = 24
Notice that this is the same as:
4! = 4 ⋅ 3! = 24
Meaning we can rewrite the formal recursion definition in terms of
recursion like so:
n! = n ⋅ (n−1) !
Note, if n = 0, then n! = 1. This means the base case occurs once n=0, the
recursive cases are defined in the equation above. Whenever you are
trying to develop a recursive solution it is very important to think about
the base case, as your solution will need to return the base case once all
the recursive cases have been worked through. Let’s look at how we can
create the factorial function in Python:
def fact(n):
'''
Returns factorial of n (n!).
Note use of recursion
'''
# BASE CASE!
if n == 0:
return 1
# Recursion!
else:
return n * fact(n-1)
Recursive functions have many uses, but like any other kind of code, their
necessity should be considered. As discussed above, consider the
differences between recursions and loops, and use the one that best fits
your needs. If you decide to go with recursions, decide what you want the
function to do before you start to compose the actual code.
It’s important to look at any arguments or conditions that would start the
recursion in the first place. For example, the function could have an
argument that might be a string or array. The function itself may have to
recognize the datatype versus it being recognized before this point (such
as by a parent function). In simpler scenarios, starting conditions may
often be the exact same conditions that force the recursion to continue.
More importantly, you want to establish a condition where the recursive
action stops. These conditionals, known as base cases, produce an actual
value rather than another call to the function. However, in the case of tail-
22
end recursion, the return value still calls a function but gets the value of
that function right away.
a. Tail Recursion:
If a recursive function calling itself and that recursive call is the last
statement in the function then it’s known as Tail Recursion. After that call
the recursive function performs nothing. The function has to process or
perform any operation at the time of calling and it does nothing at
returning time.
23
Example:
// Code Showing Tail Recursion
#include <iostream>
using namespace std;
// Recursion function
void fun(int n)
{
if (n > 0) {
cout << n << " ";
// Driver Code
int main()
{
int x = 3;
fun(x);
return 0;
}
Output:
321
Lets us convert Tail Recursion into Loop and compare each other in
terms of Time & Space Complexity and decide which is more efficient.
void fun(int y)
{
while (y > 0) {
cout << y << " ";
y--; }}
24
// Driver code
int main()
{
int x = 3;
fun(x);
return 0;
}
Output
321
Time Complexity: O(n)
So it was seen that in case of loop the Space Complexity is O(1) so it was
better to write code in loop instead of tail recursion in terms of Space
Complexity which is more efficient than tail recursion.
b. Head Recursion:
If a recursive function calling itself and that recursive call is the first
statement in the function then it’s known as Head Recursion. There’s no
statement, no operation before the call. The function doesn’t have to
process or perform any operation at the time of calling and all operations
are done at returning time.
Example:
#include <bits/stdc++.h>
using namespace std;
// Recursive function
void fun(int n)
{
if (n > 0) {
Output:
123
// Recursive function
void fun(int n)
{
int i = 1;
while (i <= n) {
cout <<" "<< i;
i++;
}
}
// Driver code
int main()
{
int x = 3;
fun(x);
return 0;
}
Output:
1 23
c. Tree Recursion:
To understand Tree Recursion let’s first understand Linear
Recursion. If a recursive function calling itself for one time then
26
it’s known as Linear Recursion. Otherwise if a recursive
27
function calling itself for more than one time then it’s known as
Tree Recursion.
fun(n)
{
// some code
if(n>0)
{
fun(n-1); // Calling itself only once
}
// some code
}
// Recursive function
void fun(int n)
{
if (n > 0)
{
cout << " " << n;
// Calling once
fun(n - 1);
// Calling twice
fun(n - 1);
}
}
// Driver code
int main()
{
fun(3);
return 0;
}
28
Output:
3211211
Example:
Output:
9 1
In this recursion, there may be more than one functions and they are
calling one another in a circular manner.
29
From the above diagram fun(A) is calling for fun(B), fun(B) is calling
for fun(C) and fun(C) is calling for fun(A) and thus it makes a cycle.
Example:
// Driver code
int main()
{
funA(20);
return 0;
}
Output:
20 19 9 8 4 3 1
30
3.3 Recursion versus Iteration
Recursion
Iteration
Self-Assessment Exercises
31
3.4 Some Recursive Algorithms (Examples)
n0 = 1
n1 = 1
n2 = n1 + n0 + 1 = 3 > 21
n3 = n2 + n1 + 1 = 5 > 22
n4 = n3 + n2 + 1 = 9 > 23
32
n5 = n4 + n3 + 1 = 15 > 23
...
nk > 2k/2
This means that the Fibonacci recursion makes a number of calls that are
exponential in k. In other words, using binary recursion to compute
Fibonacci numbers is very inefficient. Compare this problem with binary
search, which is very efficient in searching items, why is this binary
recursion inefficient? The main problem with the approach above, is that
there are multiple overlapping recursive calls.
We can compute F(n) much more efficiently using linear recursion. One
way to accomplish this conversion is to define a recursive function that
computes a pair of consecutive Fibonacci numbers F(n) and F(n-1) using
the convention F(-1) = 0.
Algorithm LinearFib(n) {
Input: A nonnegative integer n
Output: Pair of Fibonacci numbers (Fn, Fn-1)
if (n <= 1) then
return (n, 0)
else
(i, j) <-- LinearFib(n-1)
return (i + j, i)
}
Since each recursive call to LinearFib decreases the argument n by 1,
the original call results in a series of n-1 additional calls. This
performance is significantly faster than the exponential time needed by
the binary recursion. Therefore, when using binary recursion, we should
first try to fully partition the problem in two or, we should be sure that
overlapping recursive calls are really necessary.
Let's use iteration to generate the Fibonacci numbers. What's the
complexity of this algorithm?
33
Recurrence Relations
For Example, the Worst Case Running Time T(n) of the MERGE SORT Procedures is
described by the recurrence.
2T + θ (n) if n>1
34
As when solving any other mathematical problem, we are not required to
explain where our solution came from as long as we can prove that it is
correct. So the most general method for solving recurrences can be called
"guess but verify". Naturally, unless you are very good friends with the
existential quantifier you may find it had to come up with good guesses.
But sometimes it is possible to make a good guess by iterating the
recurrence a few times and seeing what happens.
T (n) = T +n
We have to show that it is asymptotically bound by O (log n).
Solution:
T (n) ≤c log +1
T (n) = 2T + n n>1
Find an Asymptotic bound on T.
Solution:
We guess the solution is O (n (logn)).Thus for constant 'c'.
T (n) ≤c n logn
Put this in given Recurrence Equation.
Now,
35
T (n) ≤2c log +n
≤cnlogn-cnlog2+n
=cn logn-n (clog2-1)
≤cn logn for (c≥1)
Thus T (n) = O (n logn).
T (n) = 1 if n=1
= 2T (n-1) if n>1
Solution:
T (n) = 2T (n-1)
= 2[2T (n-2)] = 22T (n-2)
= 4[2T (n-3)] = 23T (n-3)
= 8[2T (n-4)] = 24T (n-4) (Eq.1)
T (n) = 2i T (n-i)
Put n-i=1 or i= n-1 in (Eq.1)
T (n) = 2n-1 T (1)
= 2n-1 .1 {T (1) =1 ..... given}
= 2n-1
Example2: Consider the Recurrence
T (n) = T (n-1) +1 and T (1) = θ (1).
Solution:
T (n) = T (n-1) +1
= (T (n-2) +1) +1 = (T (n-3) +1) +1+1
= T (n-4) +4 = T (n-5) +1+4
= T (n-5) +5= T (n-k) + k
Where k = n-1
T (n-k) = T (1) = θ (1)
T (n) = θ (1) + (n-1) = 1+n-1=n= θ (n).
37
Example 1
Consider T (n) = 2T + n2
We have to obtain the asymptotic bound using recursion tree method.
Solution: The Recursion tree for the above recurrence is
T (n) = 4T +n
Obtain the asymptotic bound using recursion tree method.
Solution: The recursion trees for the above recurrence
38
Example 3: Consider the following recurrence
The Master Method is used for solving the following types of recurrence
T (n) = a T + f (n) with a≥1 and b≥1 be constant & f(n) be a function
T (n) = a T + f (n)
40
In the function to the analysis of a recursive algorithm, the constants and
function take on the following significance:
Master Theorem:
T (n) = Θ
Example:
T (n) = a T
a = 8, b=2, f (n) = 1000 n2, logba = log28 = 3
T (n) = Θ
Therefore: T (n) = Θ (n3)
F (n) = Θ
Example:
Therefore: T (n) = Θ
= Θ (n log n)
also true that: a f for some constant c<1 for large value of n
,then :
T (n) = Θ((f (n))
42
T (n) = 2
Solution:
T (n) = a T
a= 2, b =2, f (n) = n2, logba = log22 =1
∀ n ≥1
So it follows: T (n) = Θ ((f (n))
T (n) = Θ(n2)
Self-Assessment Exercises
Once all the disks have been moved, the World will end !!! This
problem can be easily solved by Divide & Conquer algorithm
In the above 7 step all the disks from peg A will be transferred to C
given Condition:
Let T (n) be the total time taken to move n disks from peg A to peg C
1. Moving n-1 disks from the first peg to the second peg. This can
be done in T (n-1) steps.
2. Moving larger disks from the first peg to the third peg will
require first one step.
3. Recursively moving n-1 disks from the second peg to the third
peg will require again T (n-1) step.
44
So, total time taken T (n) = T (n-1)+1+ T(n-1)
We get,
[As in concept we have proved that there will be 7 steps now proved by
general equation]
#include<stdio.h>
void towers(int, char, char, char);
int main()
{
int num;
printf ("Enter the number of disks : ");
scanf ("%d", &num);
printf("The sequence of moves involved in the Tower of Hanoi are:\n");
}
void towers( int num, char from peg, char topeg, char auxpe
g)
{
if (num == 1)
{
46
printf ("\n Move disk 1 from peg %c to peg %c", from peg, to
peg);
return;
}
Towers (num - 1, from peg, auxpeg, topeg);
Printf ("\n Move disk %d from peg %c to peg %c", num, from pe
g, topeg);
Towers (num - 1, auxpeg, topeg, from peg);
}
It has been used to determine the extent of brain injuries and helps to
build/rebuild neural pathways in the brain as attempting to solve, Tower
of Hanoi uses parts of the brain that involve managing time, foresight of
whether the next move will lead us closer to the solution or not.
The Tower of Hanoi is a simple puzzle game that is used to amuse
children. It is also often used as programming challenge when discussing
recursion,
To answer how long it will take our friendly monks to destroy the world,
we write a recurrence (let's call it M(n)) for the number of moves
MoveTower takes for an n-disk tower.
The base case - when n is 1 - is easy: The monks just move the single disk
directly.
M(1) = 1
In the other cases, the monks follow our three-step procedure. First they
move the (n-1)-disk tower to the spare peg; this takes M(n-1) moves. Then
the monks move the nth disk, taking 1 move. And finally they move the
(n-1)-disk tower again, this time on top of the nth disk, taking M(n-1)
moves. This gives us our recurrence relation,
M(n) = 2 M(n-1) + 1.
47
recursion. Do you see what it should be? (It may be helpful if you go
ahead and compute the first few values, like M(2), M(3), and M(4).)
M(1) =1
M(2)=2M(1) + 1 =3
M(3)=2M(2) + 1 =7
M(4)=2M(3) + 1 =15
M(5)=2M(4) + 1 =31
So the monks will move 264+1 (about 18.45x1018) disks. If they are really
good and can move one disk a millisecond, then they'll have to work for
584.6 million years. It looks like we're safe.
Self-Assessment Exercise
1.0 INTRODUCTION
Sorting and searching are two of the most frequently needed algorithms
in program design. Common algorithms have evolved to take account of
this need.
bubble sort
merge sort
insertion sort
quicksort
radix sort
selection sort
49
Algorithm
Step 1 ➤Initialization
set 1 ← n, p ← 1
Step 2 ➤loop,
Repeat through step 4 while (p ≤ n-1)
set E ← 0 ➤Initializing exchange variable. Step
3 ➤comparison, loop.
Repeat for i ← 1, 1, ........ l-1.
if (A [i] > A [i + 1]) then
set A [i] ↔ A [i + 1] ➤Exchanging values.
Set E ← E + 1
Step 4 ➤Finish, or reduce the size. if
(E = 0) then
exit
else
set l ← l - 1.
3.1 How Bubble Sort Works
1. The bubble sort starts with the very first index and makes it a
bubble element. Then it compares the bubble element, which is
currently our first index element, with the next element. If the
bubble element is greater and the second element is smaller, then
both of them will swap.
After swapping, the second element will become the bubble
element. Now we will compare the second element with the third
as we did in the earlier step and swap them if required. The same
process is followed until the last element.
2. We will follow the same process for the rest of the iterations. After
each of the iteration, we will notice that the largest element present
in the unsorted array has reached the last index.
For each iteration, the bubble sort will compare up to the last unsorted
element.
Once all the elements get sorted in the ascending order, the algorithm
will get terminated.
Initially,
16 36 24 37 15
Pass 1:
o Compare a0 and a1
16 30 24 37 15
51
16 24 15
As a2 < a3 so the array will remain as it is.
o Compare a3 and a4
16 24 36 37 15
Here a3 > a4, so we will again swap both of them.
16 24 36 15 37
Pass 2:
o Compare a0 and a1
16 24 36 15 37
As a0 < a1 so the array will remain as it is.
o Compare a1 and a2
16 24 36 15 37
Here a1 < a2, so the array will remain as it is.
o Compare a2 and a3
16 24 36 15 37
In this case, a2 > a3, so both of them will get swapped.
16 24 15 36 37
Pass 3:
o Compare a0 and a1
16 24 15 36 37
As a0 < a1 so the array will remain as it is.
o Compare a1 and a2
16 24 15 36 37
Now a1 > a2, so both of them will get swapped.
16 15 24 36 37
Pass 4:
o Compare a0 and a1
16 15 24 36 37
Here a0 > a1, so we will swap both of them.
15 16 24 36 37
Best Case Complexity: The bubble sort algorithm has a best- case
time complexity of O(n) for the already sorted array.
Average Case Complexity: The average-case time complexity for
the bubble sort algorithm is O(n2), which happens when 2 or more
elements are in jumbled, i.e., neither in the ascending order nor in
the descending order.
Worst Case Complexity: The worst-case time complexity is also
O(n2), which occurs when we sort the descending order of an array
into the ascending order.
53
3.2.4 Disadvantages of Bubble Sort
1. It does not work well when we have large unsorted lists, and it
necessitates more resources that end up taking so much of time.
2. It is only meant for academic purposes, not for practical
implementations.
3. It involves the n2 order of steps to sort an algorithm.
Self-Assessment Exercise
The selection sort enhances the bubble sort by making only a single swap
for each pass through the rundown. In order to do this, a selection sort
searches for the biggest value as it makes a pass and, after finishing the
pass, places it in the best possible area. Similarly, as with a bubble sort,
after the first pass, the biggest item is in the right place. After the second
pass, the following biggest is set up. This procedure proceeds and requires
n-1 goes to sort n item since the last item must be set up after the (n-1) th
pass.
k ← length [A]
for j ←1 to n-1
smallest ← j
for I ← j + 1 to k
if A [i] < A [ smallest]
then smallest ← i
exchange (A [j], A [smallest])
1st Iteration:
Set minimum = 7
o Compare a0 and a1
55
As, a2 < a4, set minimum =3.
Since 3 is the smallest element, so we will swap a0 and a2.
2nd Iteration:
Set minimum = 4
o Compare a1 and a2
3rd Iteration:
Set minimum = 7
o Compare a2 and a3
4th Iteration:
Set minimum = 6
o Compare a3 and a4
do n-1 comparisons; in the second pass, it will do n-2; in the third pass, it
will do n-3 and so on. Thus, the total number of comparisons can be found
by;
57
Therefore, the selection sort algorithm encompasses a time complexity of
O(n2) and a space complexity of O(1) because it necessitates some extra
memory space for temp variable for swapping.
Self-Assessment Exercise
Insertion sort is one of the simplest sorting algorithms for the reason that
it sorts a single element at a particular instance. It is not the best sorting
algorithm in terms of performance, but it's slightly more efficient than
selection sort and bubble sort in practical scenarios. It is an intuitive
sorting technique.
Suppose we have a set of cards in our hand, such that we want to arrange
these cards in ascending order. To sort these cards, we have a number of
intuitive ways.
One such thing we can do is initially we can hold all of the cards in our
left hand, and we can start taking cards one after other from the left hand,
followed by building a sorted arrangement in the right hand.
Assuming the first card to be already sorted, we will select the next
unsorted card. If the unsorted card is found to be greater than the selected
card, we will simply place it on the right side, else to the left side. At any
stage during this whole process, the left hand will be unsorted, and the
right hand will be sorted.
In the same way, we will sort the rest of the unsorted cards by placing
them in the correct position. At each iteration, the insertion algorithm
places an unsorted element at its right place.
1. for j = 2 to A.length
2. key = A[j]
3. // Insert A[j] into the sorted sequence A[1.. j - 1]
4. i = j - 1
5. while i > 0 and A[i] > key
6. A[i + 1] = A[i]
7. ii = i -1
8. A[i + 1] = key
1. We will start by assuming the very first element of the array is already
59
sorted. Inside the key, we will store the second element.
Next, we will compare our first element with the key, such that if the key
is found to be smaller than the first element, we will interchange their
indexes or place the key at the first index. After doing this, we will notice
that the first two elements are sorted.
2. Now, we will move on to the third element and compare it with the
left-hand side elements. If it is the smallest element, then we will place
the third element at the first index.
Else if it is greater than the first element and smaller than the second
element, then we will interchange its position with the third element and
place it after the first element. After doing this, we will have our first three
elements in a sorted manner.
3. Similarly, we will sort the rest of the elements and place them in their
correct position.
Consider the following example of an unsorted array that we will sort with
the help of the Insertion Sort algorithm.
1st Iteration:
Set key = 22
Compare a1 with a0
2nd Iteration:
Set key = 63
Compare a2 with a1 and a0
Since a2 > a1 > a0, keep the array as it is.
61
3rd Iteration:
Set key = 14
Compare a3 with a2, a1 and a0
Since a3 is the smallest among all the elements on the left-hand side,
place a3 at the beginning of the array.
4th Iteration:
Set key = 55
Compare a4 with a3, a2, a1 and a0.
5th Iteration:
Set key = 36
Compare a5 with a4, a3, a2, a1 and a0.
Since a5 < a2, so we will place the elements in their correct positions.
Hence the array is arranged in ascending order, so no more swapping is
required.
63
The insertion sort algorithm is highly recommended, especially when a
few elements are left for sorting or in case the array encompasses few
elements.
1. It is simple to implement.
2. It is efficient on small datasets.
3. It is stable (does not change the relative order of elements with
equal keys)
4. It is in-place (only requires a constant amount O (1) of extra
memory space).
5. It is an online algorithm, which can sort a list when it is received.
Self-Assessment Exercise:
Suppose we were to search for the value 2. The search would start at
position 0 and check the value held there, in this case 3.
3 does not match 2, so we move on to the next position.
The value at position 1 is 5.
5 does not match 2, so we move on to the next position.
The value at position 2 is 2 - a match. The search ends.
A linear search in pseudocode might look like this:
find = 2
found = False
length = list.length
counter = 0
while found == False and counter < length
if list[counter] == find then found = True
print ('Found at position', counter)
else:
counter = counter + 1
endif
endwhile
if found == False then
print('Item not found')
endif
65
A linear search, although simple, can be quite inefficient. Suppose the
data set contained 100 items of data, and the item searched for happens to
be the last item in the set? All of the previous 99 items would have to be
searched through first.
However, linear searches have the advantage that they will work on any
data set, whether it is ordered or unordered.
Radix sort is one of the simplest sorting algorithms for the reason that it
sorts a single element at a particular instance. It is not the best sorting
algorithm in terms of performance, but it's slightly more efficient than
selection sort and bubble sort in practical scenarios. It is an intuitive
sorting technique.
Radix sort is a sorting technique that sorts the elements digit to digit based
on radix. It works on integer numbers. To sort the elements of the string
type, we can use their hash value. This sorting algorithm makes no
comparison.
The Code for Radix Sort is straightforward. The following procedure assumes that each
element in the n-element array A has d digits, where digit 1 is the lowest order digit and
digit d is the highest-order digit.
Here is the algorithm that sorts A [1.n] where each number is d digits
long.
Example: The first Column is the input. The remaining Column shows
the list after successive sorts on increasingly significant digit position.
The vertical arrows indicate the digits position sorted on to produce each
list from the previous one.
67
2.1 Complexity of the Radix sort algorithm
The worst case in radix sort occurs when all elements have the same
number of digits except one element which has significantly large number
of digits. If the number of digits in the largest element is equal to n, then
the runtime becomes O(n2). The worst case running time of Counting sort
is O(n+b). If b=O(n), then the worst case running time is
O(n). Here,the countingSort function is called for d times, where d
= ⌊ logb(mx)+1⌋ ⌊ logb(mx)+1⌋ .
Total worst case complexity of radix sort is O(logb(mx)(n+b)).
The best case occurs when all elements have the same number of digits.
The best case time complexity is O(d(n+b)). If b = O(n), then time
complexity is O(dn).
Space Complexity
The base of the radix sort doesn't depend upon the number of elements.
In some cases, the base may be larger than the number of elements.
Radix sort becomes slow when the element size is large but the radix is
small. We can't always use a large radix cause it requires large memory
in counting sort. It is good to use the radix sort when d is small.
Stable sort algorithms sort equal elements in the same order that they
appear in the input. For example, in the card sorting example to the right,
the cards are being sorted by their rank, and their suit is being ignored.
This allows the possibility of multiple different correctly sorted versions
of the original list. Stable sorting algorithms choose one of these,
according to the following rule: if two items compare as equal (like the
two 5 cards), then their relative order will be preserved, i.e. if one comes
before the other in the input, it will come before the other in the output.
An example of stable sort on playing cards. When the cards are sorted by
rank with a stable sort, the two 5s must remain in the same order in the
sorted output that they were originally in. When they are sorted with a
non-stable sort, the 5s may end up in the opposite order in the sorted
output.
Within each suit, the stable sort preserves the ordering by rank that was
already done. This idea can be extended to any number of keys and is
utilized by radix sort. The same effect can be achieved with an unstable
sort by using a lexicographic key comparison, which, e.g., compares first
by suit, and then compares by rank if the suits are the same.
Now, there is two possible solution for the two pairs where the key is
same i.e. (4,5) and (4,3) as shown below:
On the other hand, the algorithm which produces second output will know
as an unstable sorting algorithm because the order of objects with the
same key is not maintained in the sorted order. You can see that in the
second output, the (4,3) comes before (4,5) which was not the case in the
original input.
1.0 INTRODUCTION
Examples: The specific computer algorithms are based on the Divide &
Conquer approach:
1. Relational Formula
2. Stopping Condition
73
1.1.1 Applications of Divide and Conquer Approach:
77
endwhile
The time complexity of the binary search algorithm is O(log n). The
best-case time complexity would be O(1) when the central index would
directly match the desired value. The worst-case scenario could be the
values at either extremity of the list or values not in the list.
The space complexity of the binary search algorithm depends on the
implementation of the algorithm. There are two ways of implementing it:
Iterative method
Recursive method
Both methods are quite the same, with two differences in implementation.
First, there is no loop in the recursive method. Second, rather than passing
the new values to the next iteration of the loop, it passes them to the next
recursion. In the iterative method, the iterations can be controlled through
the looping conditions, while in the recursive method, the maximum and
minimum are used as the boundary condition.
In the iterative method, the space complexity would be O(1). While in the
recursive method, the space complexity would be O(log n).
79
MERGE SORT AND QUICK SORT ALGORITHMS
1.0 INTRODUCTION
Merge sort is yet another sorting algorithm that falls under the category
of Divide and Conquer technique. It is one of the best sorting techniques
that successfully build a recursive algorithm.
In this technique, we segment a problem into two halves and solve them
individually. After finding the solution of each half, we merge them back
to represent the solution of the main problem.
Suppose we have an array A, such that our main concern will be to sort
the subsection, which starts at index p and ends at index r, represented by
A[p..r].
Divide
Conquer
After splitting the arrays into two halves, the next step is to conquer. In
this step, we individually sort both of the subarrays A[p..q] and A[q+1,
r]. In case if we did not reach the base situation, then we again follow the
same procedure, i.e., we further segment these subarrays followed by
sorting them separately.
Combine
As when the base step is acquired by the conquer step, we successfully
get our sorted subarrays A[p..q] and A[q+1, r], after which we merge
them back to form a new sorted array [p..r].
The MergeSort function keeps on splitting an array into two halves until
a condition is met where we try to perform MergeSort on a subarray of
size 1, i.e., p == r.
And then, it combines the individually sorted subarrays into larger arrays
until the whole array is merged.
ALGORITHM-MERGE SORT
1. If p<r
2. Then q → ( p+ r)/2
3. MERGE-SORT (A, p, q)
4. MERGE-SORT ( A, q+1,r)
5. MERGE ( A, p, q, r)
As you can see in the image given below, the merge sort algorithm
recursively divides the array into halves until the base condition is met,
where we are left with only 1 element in the array. And then, the merge
function picks up the sorted sub-arrays and merge them back to sort the
entire array.
81
FUNCTIONS: MERGE (A, p, q, r)
1. n 1 = q-p+1
2. n 2= r-q
3. create arrays [1.....n 1 + 1] and R [ 1..... n 2 +1 ]
4. for i ← 1 to n 1
5. do [i] ← A [ p+ i-1]
6. for j ← 1 to n2
7. do R[j] ← A[ q + j]
8. L [n 1+ 1] ← ∞
9. R[n 2+ 1] ← ∞
10. I ← 1
11. J ← 1
12. For k ← p to r
13. Do if L [i] ≤ R[j]
14. then A[k] ← L[ i]
15. i ← i +1
16. else A[k] ← R[j]
17. j ← j+1
To any given problem, the merge step is one such solution that combines
the two individually sorted lists(arrays) to build one large sorted
list(array).
The merge sort algorithm upholds three pointers, i.e., one for both of the
two arrays and the other one to preserve the final sorted array's current
index.
A= (36,25,40,2,7,80,15)
Step1: The merge sort algorithm iteratively divides an array into equal
halves until we achieve an atomic value. In case if there are an odd
number of elements in an array, then one of the halves will have more
elements than the other half.
Step2: After dividing an array into two subarrays, we will notice that it
did not hamper the order of elements as they were in the original array.
After now, we will further divide these two arrays into other halves.
Step4: Next, we will merge them back in the same way as they were
broken down.
Step5: For each list, we will first compare the element and then combine
them to form a new sorted list.
Step6: In the next iteration, we will compare the lists of two data values
and merge them back into a list of found data values, all placed in a sorted
manner.
83
Hence the array is sorted.
Best Case Complexity: The merge sort algorithm has a best-case time
complexity of O(n*log n) for the already sorted array.
85
Divide: Rearrange the elements and split arrays into two sub-arrays and
an element in between search that each element in left sub array is less
than or equal to the average element and each element in the right sub-
array is larger than the middle element.
Algorithm:
Partition Algorithm:
Let 44 be the Pivot element and scanning done from right to left
Comparing 44 to the right-side elements, and if right-side elements
are smaller than 44, then swap it. As 22 is smaller than 44 so swap
them.
22 33 11 55 77 90 40 60 99 44 88
Now comparing 44 to the left side element and the element must
be greater than 44 then swap them. As 55 are greater than 44 so swap
them.
22 33 11 44 77 90 40 60 99 55 88
Recursively, repeating steps 1 and steps 2 until we get two lists one left
from pivot element 44 & one right from pivot element.
22 33 40 77 90 44 60 99 55 88
22 33 11 40 44 90 77 60 99 55 88
Now, the element on the right side and left side are greater than and
smaller than 44 respectively.
And these sublists are sorted under the same process as above done.
These two sorted sublists side by side.
Merging Sublists:
87
CIT 310 ALGORITHMS AND COMPLEXITY ANALYSIS
SORTED LISTS
Worst Case Analysis: The worst case occurs when the partition
process always picks greatest or smallest element as pivot. If we
consider above partition strategy where last element is always picked
as pivot, the worst case would occur when the array is already sorted in
increasing or decreasing order. Following is recurrence for worst case.
Worst Case Complexity of Quick Sort is T (n) =O (n2)
Best Case Analysis: In any sorting, best case is the only case in which
we don't make any comparison between elements that is only done when
we have only one element to sort.
88
OTHER ALGORITHM TECHNIQUES
1.0 INTRODUCTION
We introduce here a special search tree called the Binary Search Tree
and a derivative of it known as the Red Black Tree.
A binary search tree, also known as ordered binary tree is a binary tree
wherein the nodes are arranged in a order. The order is : a) All the values
in the left sub-tree has a value less than that of the root node. b) All the
values in the right node have a value greater than the value of the root
node.
On the other hand, a red-black tree is a Binary tree where a particular node has color as an
extra attribute, either red or black. By check the node colors on any simple path from the
root to a leaf, red-black trees secure that no such path is higher than twice as long as any
other so that the tree is generally balanced.
In Inorder Tree walk, we always print the keys in the binary search tree
in a sorted order.
In Preorder Tree walk, we visit the root node before the nodes in either
subtree.
PREORDER-TREE-WALK (x):
1. If x ≠ NIL.
2. then print key [x]
3. PREORDER-TREE-WALK (left [x]).
4. PREORDER-TREE-WALK (right [x]).
In Postorder Tree walk, we visit the root node after the nodes in its
subtree.
POSTORDER-TREE-WALK (x):
1. If x ≠ NIL.
2. then POSTORDER-TREE-WALK (left [x]).
3. POSTORDER-TREE-WALK (right [x]).
4. print key [x]
90
3.2 Querying a Binary Search Trees:
3.2.1. Searching:
TREE-SEARCH (x, k)
1. If x = NIL or k = key [x].
2. then return x.
3. If k < key [x].
4. then return TREE-SEARCH (left [x], k)
5. else return TREE-SEARCH (right [x], k)
Clearly, this algorithm runs in O (h) time where h is the height of the tree.
The iterative version of the above algorithm is very easy to implement
TREE-MAXIMUM (x)
1. While left [x] ≠ NIL
2. do x←right [x].
3. return x.
CIT 310 ALGORITHMS AND COMPLEXITY ANALYSIS
The code for TREE-SUCCESSOR is broken into two cases. If the right
subtree of node x is nonempty, then the successor of x is just the leftmost
node in the right subtree, which we find in line 2 by calling TREE-
MINIMUM (right [x]). On the other hand, if the right subtree of node x is
empty and x has a successor y, then y is the lowest ancestor of x whose
left child is also an ancestor of x. To find y, we quickly go up the tree
from x until we encounter a node that is the left child of its parent; lines
3-7 of TREE-SUCCESSOR handle this case.
To insert a new value into a binary search tree T, we use the procedure
TREE-INSERT. The procedure takes a node ´ for which key [z] = v, left
[z] NIL, and right [z] = NIL. It modifies T and some of the attributes of
z in such a way that it inserts into an appropriate position in the tree.
92
TREE-INSERT (T, z)
1. y ←NIL.
2. x←root [T]
3. while x ≠ NIL.
4. do y←x
5. if key [z]< key [x]
6. then x←left [x].
7. else x←right [x].
8. p [z]←y
9. if y = NIL.
10. then root [T]←z
11. else if key [z] < key [y]
12. then left [y]←z
For Example:
Working of TREE-INSERT
13 < 15
x←left [x]
x←NIL
p [z]←6
Now our node z will be either left or right child of its parent (y).
key [z] < key [y]
13 < 15
Left [y] ← z
Left [6] ← z
So, insert a node in the left of node index at 6.
TREE-DELETE (T, z)
If left [z] = NIL or right [z] = NIL.
Then y ← z
Else y ← TREE- SUCCESSOR (z)
If left [y] ≠ NIL.
Then x ← left [y]
Else x ← right [y]
If x ≠NIL
Then p[x] ← p [y]
If p[y] = NIL.
Then root [T] ← x
94
Else if y = left [p[y]]
Then left [p[y]] ← x
Else right [p[y]] ← y
If y ≠ z.
Then key [z] ← key [y]
If y has other fields, copy them, too.
Return y
Node z has two children; its left child is node l, its right child is its
successor y, and y's right child is node x. We replace z by y, updating y's
left child to become l, but leaving x as y's right child.
CIT 310 ALGORITHMS AND COMPLEXITY ANALYSIS
Node z has two children (left child l and right child r), and its successor y
≠ r lies within the subtree rooted at r. We replace y with its own right child
x, and we set y to be r's parent. Then, we set y to be q's child and the parent
of l.
96
A tree T is an almost red-black tree (ARB tree) if the root is red, but other
conditions above hold.
3.4.1. Rotation:
1. y ← right [x]
1. y ← right [x]
2. right [x] ← left [y]
3. p [left[y]] ← x
4. p[y] ← p[x]
5. If p[x] = nil [T]
then root [T] ← y
else if x = left [p[x]]
then left [p[x]] ← y
else right [p[x]] ← y
6. left [y] ← x.
7. p [x] ← y.
Example: Draw the complete binary tree of height 3 on the keys {1, 2,
3... 15}. Add the NIL leaves and color the nodes in three different ways
such that the black heights of the resulting trees are: 2, 3 and 4.
Solution:
98
Tree with black-height-3
3.4.2. Insertion:
Insert the new node the way it is done in Binary Search Trees.
Color the node red
If an inconsistency arises for the red-black tree, fix the tree
according to the type of discrepancy.
RB-INSERT (T, z)
y ← nil [T]
x ← root [T]
while x ≠ NIL [T]
do y ← x
if key [z] < key [x]
then x ← left [x]
else x ← right [x]
p [z] ← y
if y = nil [T]
then root [T] ← z
else if key [z] < key [y]
then left [y] ← z
else right [y] ← z
left [z] ← nil [T]
right [z] ← nil [T]
color [z] ← RED
RB-INSERT-FIXUP (T, z)
After the insert new node, Coloring this new node into black may violate
the black-height conditions and coloring this new node into red may
violate coloring conditions i.e. root is black and red node has no red
children. We know the black-height violations are hard. So we color the
node red. After this, if there is any color violation, then we have to correct
them by an RB-INSERT-FIXUP procedure.
RB-INSERT-FIXUP (T, z)
100
Example: Show the red-black trees that result after successively inserting
the keys 41,38,31,12,19,8 into an initially empty red-black tree. Solution:
Insert 41
CIT 310 ALGORITHMS AND COMPLEXITY ANALYSIS
Insert 19
3.4.3. Deletion:
102
If the element to be deleted is in a node with only right child, swap
this node with the one containing the smallest element in the right
subtree (This node has no left child).
If the element to be deleted is in a node with both a left child and
a right child, then swap in any of the above two ways. While
swapping, swap only the keys but not the colors.
The item to be deleted is now having only a left child or only a
right child. Replace this node with its sole child. This may violate
red constraints or black constraint. Violation of red constraints can
be easily fixed.
If the deleted node is black, the black constraint is violated. The
elimination of a black node y causes any path that contained y to
have one fewer black node.
Two cases arise:
The replacing node is red, in which case we merely color it black
to make up for the loss of one black node.
The replacing node is black.
RB-DELETE (T, z)
RB-DELETE-FIXUP (T, x)
Solution:
104
Delete 38
Delete 41
No Tree.
Self-Assessment Exercises
DYNAMIC PROGRAMMING
1.0 INTRODUCTION
In the above example, if we calculate the F(18) in the right subtree, then
it leads to the tremendous usage of resources and decreases the overall
performance.
The following are the steps that the dynamic programming follows:
The above five steps are the basic steps for dynamic programming. The
dynamic programming is applicable that are having properties such as:
Those problems that are having overlapping sub-problems and
optimal substructures. Here, optimal substructure means that the
solution of optimization problems can be obtained by simply
combining the optimal solution of all the sub-problems.
Top-down approach
Bottom-up approach
int fib(int n)
{
if(n<0)
error;
if(n==0)
return 0;
if(n==1)
return 1;
sum = fib(n-1) + fib(n-2);
}
In the above code, we have used the recursive approach to find out the
Fibonacci series. When the value of 'n' increases, the function calls will
also increase, and computations will also increase. In this case, the time
complexity increases exponentially, and it becomes O(2n).
Suppose we have an array that has 0 and 1 values at a[0] and a[1]
positions, respectively shown as below:
Since the bottom-up approach starts from the lower values, so the values
at a[0] and a[1] are added to find the value of a[2] shown as below:
The value of a[3] will be calculated by adding a[1] and a[2], and it
becomes 2 shown as below:
110
The value of a[4] will be calculated by adding a[2] and a[3], and it
becomes 3 shown as below:
The value of a[5] will be calculated by adding the values of a[4] and
a[3], and it becomes 5 shown as below:
The code for implementing the Fibonacci series using the bottom-up
approach is given below:
int fib(int n)
{
int A[];
A[0] = 0, A[1] = 1;
for( i=2; i<=n; i++)
{
A[i] = A[i-1] + A[i-2]
}
return A[n];
}
In the above code, base cases are 0 and 1 and then we have used for loop
to find other values of Fibonacci series.
When i=2 then the values 0 and 1 are added shown as below:
CIT 310 ALGORITHMS AND COMPLEXITY ANALYSIS
When i=3 then the values 1and 1 are added shown as below:
112
When i=4 then the values 2 and 1 are added shown as below:
When i=5, then the values 3 and 2 are added shown as below:
In the above case, we are starting from the bottom and reaching to the
top
CIT 310 ALGORITHMS AND COMPLEXITY ANALYSIS
114
'A' stands for Analyse the solution: Once we find the recursive
solution then we have to analyse the solution and look for the
overlapping problems.
'S' stands for Save the results for future use: Once we find the
overlapping problems, we store the solutions of these sub-
problems. To store the solutions, we use the n-dimensional array
for caching purpose.
The above three steps are used for the top-down approach if we use 'F',
'A' and 'S', which means that we are achieving the Top-down approach
and since it is not purely because we are using the recursive technique.
As we can observe in the above figure that fib(2) is calculated two times
while fib(1) is calculated three times. So, here overlapping problem
occurs. In this step, we have analysed the solution.
Fib(n)
{
int cache = new int[n+1];
if(n<2)
return n;
if(cache[n]!= 0)
return cache[n];
return cache[n] = fib(n-1) + fib(n-2);
}
In the above code, we have used a cache array of size n+1. If cache[n] is
not equal to zero then we return the result from the cache else we will
calculate the value of cache and then return the cache. The technique that
we have used here is top-down approach as it follows the recursive
approach. Here, we always look for the cache so cache will be populated
on the demand basis. Suppose we want to calculate the fib(4), first we
look into cache, and if the value is not in the cache then the value is
calculated and stored in the cache.
116
Visual representation of the above code is:
As we can observe in the above figure that we are populating the cache
from bottom to up so it is known as bottom-up approach. This approach
is much more efficient than the previous one as it is not using recursion
but both the approaches have the same time and space complexity, i.e.,
O(n).
In this case, we have used the FAST method to obtain the optimal
solution. The above is the optimal solution that we have got so far but this
is not the purely an optimal solution.
Efficient solution:
fib(n)
{
int first=0, second=1, sum=0;
if(n<2)
{
return 0;
}
for(int i =2; i<=n; i++)
{
sum = first + second;
first = second;
second = sum;
}
return sum;
}
118
The above solution is the efficient solution as we do not use the cache.
The Following are the top 10 problems that can easily be solved using
Dynamic programming:
COMPUTATIONAL COMPLEXITY
1.0 INTRODUCTION
For example, the mergesort algorithm can sort a list of N numbers in O(N
log N) time. Complexity theory asks what you can learn about the task of
sorting in general, not what you can learn about a specific algorithm. It
turns out that you can show that any sorting algorithm that sorts by using
comparisons must use at least N × log(N) time in the worst case.
120
large enough. In other words, the function g(N) is an upper bound for the
actual run-time function f(N). .
Two other notations similar to Big O notations are sometimes useful when
discussing algorithmic complexity.
Big Omega notation, written Ω(g(N)), means that the run-time function
is bounded below by the function g(N) . For example, as explained a
moment ago, N log(N) is a lower bound for algorithms that sort by using
comparisons, so those algorithms are Ω(N logN) .
Big Theta notation, written ϴ(g(N)) , means that the run-time function
is bounded both above and below by the function g(N) . For example, the
mergesort algorithm’s run time is bounded above by O(N log N), and the
run time of any algorithm that sorts by using comparisons is bounded
below by Ω(N log N), so mergesort has performance ϴ(N log N).
In summary,
Deterministic algorithms are by far the most studied and familiar kind of
algorithm as well as one of the most practical, since they can be run on
real machines efficiently.
122
An algorithm that solves a problem in nondeterministic polynomial time
can run in polynomial time or exponential time depending on the choices
it makes during. The nondeterministic algorithms are often used to find
an approximation to a solution, when the exact solution would be too
costly using a deterministic one.
i. If it uses external state other than the input, such as user input, a
global variable, a hardware timer value, a random value, or stored
disk data.
ii. If it operates in a way that is timing-sensitive, for example if it has
multiple processors writing to the same data at the same time. In
this case, the precise order in which each processor writes its data
will affect the result.
iii. If a hardware error causes its state to change in an unexpected way.
124
3.3 NP (Non-Deterministic Polynomial) Problem
2. If you convert the issue into one form to another form within the
polynomial time
126
– Finding a minimum spanning tree in a graph (even though there’s a
gap)
This algorithm, however, does not provide an efficient solution and is,
therefore, not feasible for computation with anything more than the
smallest input.
Examples
Towers of Hanoi: we can prove that any algorithm that solves this
problem must have a worst-case running time that is at least 2n − 1.
* List all permutations (all possible orderings) of n numbers.
3.5.3 IS P = NP?
If it turns out that P ≠ NP, which is widely believed, it would mean that
there are problems in NP that are harder to compute than to verify: they
could not be solved in polynomial time, but the answer could be verified
in polynomial time.
1.0 INTRODUCTION
128
Intuitively, the approximation ratio measures how bad the approximate
solution is distinguished with the optimal solution. A large (small)
approximation ratio measures the solution is much worse than (more or
less the same as) an optimal solution.
Observe that P (n) is always ≥ 1, if the ratio does not depend on n, we may
write P. Therefore, a 1-approximation algorithm gives an optimal
solution. Some problems have polynomial-time approximation algorithm
with small constant approximate ratios, while others have best-known
polynomial time approximation algorithms whose approximate ratios
grow with n.
3.1 Vertex Cover
The idea is to take an edge (u, v) one by one, put both vertices to C, and
remove all the edges incident to u or v. We carry on until all edges have
been removed. C is a VC. But how good is C?
VC = {b, c, d, e, f, g}
Triangle inequality
Let u, v, w be any three vertices, we have
130
T;
4. Return the Hamiltonian cycle H that visits the vertices in the
order L;
}
Before knowing about the minimum spanning tree, we should know about
the spanning tree.
The above graph can be represented as G(V, E), where 'V' is the number
of vertices, and 'E' is the number of edges. The spanning tree of the above
CIT 310 ALGORITHMS AND COMPLEXITY ANALYSIS
E` € E
E` = |V| - 1
132
CIT 310 ALGORITHMS AND COMPLEXITY ANALYSIS
The minimum spanning tree is a spanning tree whose sum of the edges
is minimum. Consider the below graph that contains the edge weight:
The following are the spanning trees that we can make from the above
graph.
i. The first spanning tree is a tree in which we have removed the
edge between the vertices 1 and 5 shown as below:
The sum of the edges of the above tree is (1 + 4 + 5 + 2): 12
ii. The second spanning tree is a tree in which we have removed
the edge between the vertices 1 and 2 shown as below:
The sum of the edges of the above tree is (3 + 2 + 5 + 4) : 14
iii. The third spanning tree is a tree in which we have removed the
edge between the vertices 2 and 3 shown as below:
The sum of the edges of the above tree is (1 + 3 + 2 + 5) : 11
iv. The fourth spanning tree is a tree in which we have removed
the edge between the vertices 3 and 4 shown as below: The
sum of the edges of the above tree is (1 + 3 + 2 + 4) : 10. The
edge cost 10 is minimum so it is a minimum spanning tree.
134
The number of spanning trees that can be made from the above complete
graph equals to nn-2 = 44-2 = 16.
136
APPROXIMATE ALGORITHMS II
1.0 INTRODUCTION
1. Kruskal's Algorithm
2. Prim's Algorithm
A←∅
for each vertex v ∈ V [G]
do MAKE - SET (v)
sort the edges of E into non decreasing order by weight w
for each edge (u, v) ∈ E, taken in non decreasing order by
weight
do if FIND-SET (μ) ≠ if FIND-SET (v)
then A ← A ∪ {(u, v)}
CIT 310 ALGORITHMS AND COMPLEXITY ANALYSIS
UNION (u, v)
return A
Analysis:
Example:
Find the Minimum Spanning Tree of the following graph using Kruskal's
algorithm.
Solution:
First we initialize the set A to the empty set and create |v| trees, one
containing each vertex with MAKE-SET procedure. Then sort the edges
in E into order by non-decreasing weight.
138
Now, check for each edge (u, v) whether the endpoints u and v belong to
the same tree. If they do then the edge (u, v) cannot be supplementary.
Otherwise, the two vertices belong to different trees, and the edge (u, v)
is added to A, and the vertices in two trees are merged in by union
procedure.
Step 3: then (a, b) and (i, g) edges are considered, and the forest
becomes
Step 4: Now, edge (h, i). Both h and i vertices are in the same set. Thus
it creates a cycle. So this edge is discarded.
Then edge (c, d), (b, c), (a, h), (d, e), (e, f) are considered, and the forest
becomes.
Step 5: In (e, f) edge both endpoints e and f exist in the same tree so
discarded this edge. Then (b, h) edge, it also creates a cycle.
Step 6: After that edge (d, f) and the final spanning tree is shown as in
dark lines.
140
Step 7: This step will be required Minimum Spanning Tree because it
contains all the 9 vertices and (9 - 1) = 8 edges
At every step, it considers all the edges and picks the minimum weight
edge. After picking the edge, it moves the other endpoint of edge to set
containing MST.
a. Pick vertex u which is not is MST set and has minimum key
value. Include 'u'to MST set.
b. Update the key value of all adjacent vertices of u. To
update, iterate through all adjacent vertices. For every
adjacent vertex v, if the weight of edge u.v less than the
previous key value of v, update key value as a weight of u.v.
MST-PRIM (G, w, r)
Example:
Generate minimum cost spanning tree for the following graph using
Prim's algorithm.
Solution:
142
whose key is set to 0. Suppose 0 vertex is the root, i.e., r. By EXTRACT
- MIN (Q) procure, now u = r and Adj [u] = {5, 1}.
Removing u from set Q and adds it to set V - Q of vertices in the tree.
Now, update the key and π fields of every vertex v adjacent to u but not
in a tree.
144
Adj [3] = {4, 6, 2}
4 is already in heap
Now in Q, key [2] = 12, key [6] = 18, key [1] = 28 and parent of 2 and 6
is 3.
π [2] = 3 π[6]=3
u = EXTRACT_MIN (2, 6)
u=2 [key [2] < key [6]]
12 < 18
Now the root is 2
Adj [2] = {3, 1}
3 is already in a heap
Taking 1, key [1] = 28
w (2,1) = 16
w (2,1) < key [1]
So update key value of key [1] as 16 and its parent as 2.
π[1]= 2
CIT 310 ALGORITHMS AND COMPLEXITY ANALYSIS
Now all the vertices have been spanned, Using above the table we get
Minimum Spanning Tree.
0→5→4→3→2→1→6
[Because Π [5] = 0, Π [4] = 5, Π [3] = 4, Π [2] = 3, Π [1] =2, Π [6] =1]
146
Total Cost = 10 + 25 + 22 + 12 + 16 + 14 = 99
Self-Assessment Exercises