Csc 401 Lesson 2
Csc 401 Lesson 2
Resources for an algorithm are usually expressed as a function of input. Often this
function is messy and complicated to work. For effective study of Function growth, the
function should be reduced to only important part.
In this function, the n2 term dominates the function, that is when n gets sufficiently large.
In function reduction, we are interested in dominate terms, because they determine the
function growth rate. Thus; we ignore all constants and coefficient and look at the highest
order term concerning n.
Asymptotic analysis
It is a technique of representing limiting behavior which can be used to analyze the
performance of an algorithm for some large data set.
In algorithms analysis (considering the performance of algorithms when applied to very
large input datasets), The simplest example is a function
ƒ(n) = n2+3n,
the term 3n becomes insignificant compared to n2 when n is very large. The function "ƒ
(n) is said to be asymptotically equivalent to n2 as n → ∞", and here is
written symbolically as
ƒ (n) ~ n2.
Asymptotic notations are used to write fastest and slowest possible
running time for an algorithm also known as 'best case'and 'worst case' scenarios
respectively.
"In asymptotic notations, we derive the complexity concerning the size
of the input in terms of n . These notations enable us to estimate the complexity of
algorithms without expanding its running cost. This notations compare functions,
ignoring constant factors and small input sizes.
Examples:
1. 3n+2=O(n) as 3n+2≤4n for all n≥2
2. 3n+3=O(n) as 3n+3≤4n for all n≥3
Hence, the complexity of f(n) can be represented as O (g (n))
• Brute Force
• Divide and Conquer Approach
• Greedy Strategy
• Dynamic Programming
• Branch and Bound
• Backtracking Algorithm
Brute Force
This is a simple technique with naïve approach. It relies on huge processing power and testing of
all possibilities to improve efficiency. A scenario where a brute force search can be used;
suppose you forgot the combination of a 4-digit padlock and still want to use it, the padlock can
be opened by trying all possible 4-digit combinations from0 to 9 to unlock it. That combination
could be anything between 0000 to 9999, hence there are 10,000 combinations. So we can say
that in the worst case, for you to find the actual combination, you have up to 10, 000
possibilities.
The time complexity of brute force is O(mn), which can also be written as O(n*m). This means
that if we need to search a string of n characters in a string of m characters, the no of turns should
be n*m times.
Divide and Conquer Approach
This algorithmic technique is preferred for complex problems. It uses top-down approach
following the underlisted steps as the name implies:
Step 1: Divide the problem into several subproblems.
Step 2: Conquer or solve each sub-problem.
Step 3:Combine each sub-problem to get the required result.
Divide and Conquer solve each subproblem recursively, so each subproblem will be the smaller
original problem. Example is shown in Figure2.
Examples of some standard algorithms that are of the Divide and Conquer algorithms variety.
a. Binary Search: a searching algorithm. ...
b. Quicksort: sorting algorithm. ...
c. Merge Sort : sorting algorithm. ...
d. Closest Pair of Points: The problem is to find the closest pair of points in a set of
points in x-y plane.
Dynamic Programming
Dynamic Programming (DP) is an algorithmic technique for solving optimization problems by
breaking them into simpler sub-problems and storing each sub-solution for reuse. For instance
when using this technique to figure out all possible results from a set of numbers, the solution
obtained from first calculation is saved and put into the equation later instead of being
recalculated, so it is used for complicated equations and processes, thus it is both a mathematical
optimization method and a computer programming method. The sub-problems are optimized to
find the overall solution which usually has to do with finding the maximum and minimum range
of algorithmic query. DP can be used in calculation of Fibonacci Series in which each number is
the sum of the two preceding numbers. Suppose the first two numbers of the series are 0,1.
To solve the problem of finding the nth number of the series, the overall problem i.e., Fib(n), we
can be tackled by breaking it down into two smaller sub-problems i.e.; Fib(n-1) and Fib(n-2).
Hence, we can use Dynamic Programming to solve above mentioned problem, which is
elaborated in more detail in the following Figure 3:
Firstly, a rooted decision tree where the root node represents the entire search space is built. Each
child node is a part of the solution set and is a partial solution. Based on the optimal solution, we
set an upper and lower bound for a given problem before constructing the rooted decision tree
and we need to make a decision about which node to include in the solution set at each level. It is
very important to find upper and lower bound and to find upper bound any local optimization
method can be used. It can also be found by picking any point in the search space and convex
relaxation. Whereas, duality can be used for finding lower bound.
Randomized Algorithm
Randomized Algorithm strategy uses random numbers to determine the next line of action at any
point in its logic. In a standard algorithm, it is usually used to reduce either the running time, or
time complexity, or the memory used, or space complexity. The algorithm works by creating a
random number, r, from a set of numbers and making decisions based on its value. This algorithm
could assist in making a decision in a situation of doubt by flipping a coin or drawing a card from
a deck.
Input Output
Algorithm
Random Number
Figure 5: Randomized Algorithm Flowchart
The output of a randomized algorithm on a given input is a random variable. Thus,
there may be a positive probability that the outcome is incorrect. As long as the
probability of error is small for every possible input to the algorithm, this is not a
problem
When utilizing a randomized method, keep the following two considerations in mind:
It takes source of random numbers and makes random choices during execution along with the
input. Behavior of an algorithm varies even on fixed inputs.
Two main types of randomized algorithms:
a. Las Vegas algorithms
b. Monte-Carlo algorithms.
Backtracking Algorithms
This technique steps backward to try another option if current solution fails. It is a method for
resolving issues recursively by attempting to construct a solution incrementally, one piece at a
time, discarding any solutions that do not satisfy the problem’s constraints at any point in time. It
ca be said to use brute force approach which resolves problems with multiple solutions. It finds a
solution by building a solution step by step, increasing levels over time, using recursive calling.
A search tree known as the statespace tree is used to find these solutions. Each branch in a state-
space tree represents a variable, and each level represents a solution.
A backtracking algorithm uses the depth-first search method. When the algorithm begins to
explore the solutions, the abounding function is applied so that the algorithm can determine
whether the proposed solution satisfies the constraints. If it does, it will keep looking. If it does
not, the branch is removed, and the algorithm returns to the previous level.
In any backtracking algorithm, the algorithm seeks a path to a feasible solution that includes
some intermediate checkpoints. If the checkpoints do not lead to a viable solution, the problem
can return to the checkpoints and take another path to find a solution.
The algorithm works as follows:
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)
For example, if we want to find all the possible ways of arranging 2 boys and 1 girl on 3 benches
with a constraint that Girl should not be on the middle bench. So there will be 3! = 6 (3x2x1)
possibilities to solve this problem. All possible ways should be tried recursively to get the
required solution as shown:
Figure 6: Solution of backtracking
Instances of Recursion
There are two main instances of recursion.
• Recursion as a technique in which a function makes one or more calls to itself.
• A data structure using smaller instances of the exact same type of data structure when it
represents itself.
Importance of Recursion
• It provides an alternative for performing repetitions of the task in which a loop is not
ideal.
• It serves as a great tool for building out particular data structures.
We can follow this flow chart from the top, reaching the base case, and
then working our way back up.
Recursion is a powerful tool, but it can be a tricky concept to implement.
Laws of Recursion
All recursive algorithms must obey three important laws:
i. A recursive algorithm must have a base case, which denotes the point when it
should stop.
ii. A recursive algorithm must change its state and move toward the base case which
enables it to store and accumulate values that end up becoming the answer.
iii. A recursive algorithm must call itself, recursively with smaller and smaller values.
Types of Recursion
Recursion are mainly of two types:
i. Direct Recursion: when a function calls itself from within itself
ii. Indirect Recursion: When more than one function call one another mutually.
Direct Recursion
These can be further categorized into four types:
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.
Example:
// Code Showing Tail Recursion
#include <iostream>
using namespace std;
// Recursion function
void fun(int n)
{
if (n > 0) {
cout << n << " ";
// Last statement in the function
fun(n - 1);
}
}
// Driver Code
int main()
{
int x = 3;
fun(x);
return 0;
}
Output:
321
Time Complexity For Tail Recursion : O(n)
Space Complexity For Tail Recursion : O(n)
Lets us convert Tail Recursion into Loop and compare each other in
terms of Time & Space Complexity and decide which is more efficient.
// Converting Tail Recursion into Loop
#include <iostream>
using namespace std;
void fun(int y)
{
while (y > 0) {
cout << y << " ";
y--; }}
// Driver code
int main()
{
int x = 3;
fun(x);
return 0;
}
Output
321
Time Complexity: O(n)
Space Complexity: O(1)
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:
// C++ program showing Head Recursion
#include <bits/stdc++.h>
using namespace std;
// Recursive function
void fun(int n)
{
if (n > 0) {
// First statement in the function
fun(n - 1);
cout << " "<< n;
}
}
// Driver code
CIT 310 ALGORITHMS AND COMPLEXITY ANALYSIS
34
int main()
{
int x = 3;
fun(x);
return 0;
}
Output:
123
Time Complexity For Head Recursion: O(n)
Space Complexity For Head Recursion: O(n)
c. Tree Recursion:
To understand Tree Recursion let’s first understand Linear Recursion. If a recursive
function calling itself for one time then it’s known as Linear Recursion. Otherwise if a
recursive function calling itself for more than one time then it’s known as
Tree Recursion.
Indirect Recursion:
In this recursion, there may be more than one functions and they are calling one another
in a circular manner. From the diagram below 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:
// C++ program to show Indirect Recursion
#include <iostream>
using namespace std;
void funB(int n);
void funA(int n)
{
if (n > 0) {
cout <<" "<< n;
// Fun(A) is calling fun(B)
funB(n - 1);
}
}
void funB(int n)
{
if (n > 1) {
cout <<" "<< n;
// Fun(B) is calling fun(A)
funA(n / 2);
}
}
// Driver code
int main()
{
funA(20);
return 0;
}
Output:
20 19 9 8 4 3 1
Recursion versus Iteration
The Recursion and Iteration both repeatedly execute the set of instructions. Recursion is
when a statement in a function calls itself repeatedly. The iteration is when a loop
repeatedly executes until the controlling condition becomes false. The primary difference
between recursion and iteration is that recursion is a process, always applied to a function
and iteration is applied to the set of instructions which we want to get repeatedly
executed.
Features Recursion
• Recursion uses selection structure.
• Infinite recursion occurs if the recursion step does not reduce the problem in a
manner that converges on some condition (base case) and Infinite recursion can
crash the system.
• Recursion terminates when a base case is recognized.
• Recursion is usually slower than iteration due to the overhead of maintaining the
stack.
• Recursion uses more memory than iteration.
• Recursion makes the code smaller.
Features of Iteration
• Iteration uses repetition structure.
• An infinite loop occurs with iteration if the loop condition test never becomes
false and Infinite looping uses CPU cycles repeatedly.
• An iteration terminates when the loop condition fails.
• An iteration does not use the stack so it's faster than recursion.
• Iteration consumes less memory.
• Iteration makes the code longer.
Algorithm Fib(n) {
if (n < 2) return 1
else return Fib(n-1) + Fib(n-2)
}
The above recursion is called binary recursion since it makes two recursive calls instead
of one. How many number of calls are needed to compute the kth Fibonacci number? Let
nk denote the number of calls performed in the execution.
n0 = 1
n1 = 1
n2 = n1 + n0 + 1 = 3 > 21
n3 = n2 + n1 + 1 = 5 > 22
n4 = n3 + n2 + 1 = 9 > 23
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.
Exercises
i. Try and find the Sum of the elements of an array recursively
ii. Find the maximum number of elements in an array A of n elements using
recursion, then iteration. What are their time complexities.