CSC 308
CSC 308
CSC 308
ANALYSIS
Dr. S. A. Bakura
January 9, 2023
1 Mathematical preliminaries
Review the following Mathematical topics as related to Algorithm Complex-
ity. Chapter 2 of [clifford] gives the overview of the concepts
• Logarithms
• Recursion
• Induction Proofs
• Summations
• Recurrence Relations
2 An algorithm
Is a step-by-step sequence of instruction that must be followed by computer
to solve a particular problem. ... more explanation needed here!
3 Algorithm Analysis
The concept of algorithm analysis is one of the foundemental concepts in
computer science. It’s a theorical approach to formally estimate the running
cost of an algorithm. So, there is nothing to fear as the course is friendly
and has a well established approaches to deal with any diffuculty that may
arises.
1
One of the best methodology used to estimate running cost (Resource
Comsumption) of an algorthm is an Asymtotic algorithm analysis. In
this methodology an attempt is made to estimate the resource consumption
of an algorithm. It allows us to compare the relative costs of two or more
algorithms for solving the same problem. Asymptotic analysis also gives
algorithm designers a tool for estimating whether a proposed solution is
likely to meet the resource constraints for a problem before they implement
an actual program.
3.1 Motivation
The existance of some tools used to measure how fast a program runs such
as Profilers which measure running time in milliseconds and can help us
optimize our code by spotting bottlenecks. However, such tools are irrelevant
to algorithm complexity. In an algorithm complexity analysis, details such
as the implementation programming language, the hardware the algorithm
runs on, or the instruction set of the given CPU are usually ignored.
Moreover, comparing two algorithms by counting the number of milisec-
onds by each algorithm or by implementing the algorithm on actual hard-
ware does not help. It’s quite possible that a bad algorithm written in a
low-level programming language such as assembly runs much quicker than
a good algorithm written in a high-level programming language such as Java
or Python.
So, how can we compare algorithms and determine a better algorithm?
The answer is asymptotic analysis. Asymptotic analysis measures the
efficiency of an algorithm, or its implementation as a program, as the input
size becomes large. It is actually an estimating technique and does not tell
us anything about the relative merits of two programs where one is always
“slightly faster” than the other. However, asymptotic analysis has proved
useful to computer scientists who must determine if a particular algorithm
is worth considering for implementation.
The critical resource for a program is most often its running time. How-
ever, you cannot pay attention to running time alone. You must also be
concerned with other factors such as
• Space required to run the program (both main memory and disk
space).
• Speed of the computer’s CPU, bus, and peripheral hardware.
• Programming language and the quality of code generated by a partic-
ular compiler
2
• Coding efficiency of the programmer who converts the algorithm to a
program
Ideally we would measure the running time of the algorithm under standard
benchmark conditions. However, we have no way to calculate the running
time reliably other than to run an implementation of the algorithm on some
computer. The only alternative is to use some other measure as a surrogate
for running time.
Now, the first thing we’ll do is count how many basic operations this piece
of code executes. As we analyze this piece of code, we want to break it up
into simple instructions; things that can be executed by the CPU directly
- or close to that. We’ll assume our processor can execute the following
operations as one instruction each:
• Incrementing a value
3
• Basic arithmetic operations such as addition and multiplication
We’ll assume branching (the choice between if and else parts of code
after the if condition has been evaluated) occurs instantly and won’t count
these instructions. In the above code, the first line of code is:
var M = A[ 0 ];
This requires 2 instructions: One for looking up A[ 0 ] and one for assign-
ing the value to M (we’re assuming that n is always at least 1). These two
instructions are always required by the algorithm, regardless of the value of
n. The for loop initialization code also has to always run. This gives us two
more instructions; an assignment and a comparison:
i = 0;
i < n;
These will run before the first for loop iteration. After each for loop
iteration, we need two more instructions to run, an increment of i and a
comparison to check if we’ll stay in the loop:
++i;
i < n;
So, if we ignore the loop body, the number of instructions this algorithm
needs is 4 + 2n. That is, 4 instructions at the beginning of the for loop and
2 instructions at the end of each iteration of which we have n. We can now
define a mathematical function f( n ) that, given an n, gives us the number
of instructions the algorithm needs. For an empty for body, we have f( n )
= 4 + 2n.
Worst-case analysis in side the for body
Looking at the for body; we have two basic operation: an array lookup op-
eration and a comparison that happen always.
if ( A[ i ] >= M ){ ...
4
algorithm uses more operation to find the largest value at the last index po-
sition. In complexity analysis we refer to the first case as Best-case while
the second case, which is more interesting is called the worst-case ; that’s
nothing more than just considering the case when we’re the most unlucky.
So, in the worst case, we have 4 operations to run within the for body, so
finally we have f( n ) = 4 + 2n + 4n = 6n + 4. This function f, given a
problem size n, gives us the number of instructions that would be needed in
the worst-case.
• f( n ) = 8n + 34 gives f( n ) = n.
• f( n ) = 300 gives f( n ) = 1
• f( n ) = n2 + 6n + 42 gives f( n ) = n2
• f( n ) = nn + n gives f(n) = nn
5
Example 2: What is the running time for this code fragment?
4 Upper Bounds
Several terms are used to describe the running-time equation for an algo-
rithm. These terms — and their associated symbols — indicate precisely
what aspect of the algorithm’s behavior is being described. One is the up-
per bound for the growth of the algorithm’s running time. It indicates the
upper or highest growth rate that the algorithm can have called big-Oh
notation.
Because the phrase “has an upper bound to its growth rate of f(n)” is
long and often used when discussing algorithms, we adopt a special nota-
tion, called big-Oh notation. If the upper bound for an algorithm’s growth
rate (for, say, the worst case) is f(n), then we would write that this algo-
rithm is “in the set O(f(n)) in the worst case” (or just “in O(f(n)) in the
worst case”). For example, if n2 grows as fast as T(n) (the running time
of our algorithm) for the worst-case input, we would say the algorithm is
“in O( n2 ) in the worst case.” The following is a precise definition for an
upper bound. T(n) represents the true running time of the algorithm. f(n)
is some expression for the upper bound.
6
value for c actually is. In other words, the definition says that for all inputs
of the type in question (such as the worst case for all inputs of size n) that
are large enough (i.e., n > n0 ), the algorithm always executes in less than
cf(n) steps for some constant c.
Example 1
Consider the sequential search algorithm for finding a specified value in an
array of integers. If visiting and examining one value in the array requires cs
steps where cs is a positive number, and if the value we search for has equal
probability of appearing in any position in the array, then in the average
case T(n) = cs n/2. For all values of n > 1, cs n/2 ≤ cs n. Therefore, by the
definition, T(n) is in O(n) for n0 = 1 and c = cs .
Example 2 Assigning the value from the first position of an array to a
variable takes constant time regardless of the size of the array. Thus, T(n)
= c (for the best, worst, and average cases). We could say in this case that
T(n) is in O(c). However, it is traditional to say that an algorithm whose
running time has a constant upper bound is in O(1)
5 Lower Bound
Similar notation is used to describe the least amount of a resource that an
algorithm needs for some class of input i.e the lower bound of an algorithm.
The lower bound for an algorithm (or a problem, as explained later) is
denoted by the symbol Ω, pronounced “big-Omega” or just “Omega.” The
following definition for is symmetric with the definition of big-Oh.
For T(n) a non-negatively valued function, T(n) is in set (g(n))
if there exist two positive constants c and n0 such that T(n) ≥
cg(n) for all n > n0 .
6 Θ Notation
The definitions for big-Oh and give us ways to describe the upper bound
for an algorithm (if we can find an equation for the maximum cost of a
particular class of inputs of size n) and the lower bound for an algorithm (if
we can find an equation for the minimum cost for a particular class of inputs
of size n). When the upper and lower bounds are the same within a constant
factor, we indicate this by using Θ (big-Theta) notation. An algorithm is
said to be Θ (h(n)) if it is in O(h(n)) and it is in Ω(h(n))
Note that we drop the word “in” for Θ notation, because there is a
strict equality for two equations with the same Θ. In other words, if f(n) is
7
Θ(g(n)), then g(n) is Θ(f(n))
Because the sequential search algorithm is both in O(n) and in Ω (n) in
the average case, we say it is Θ (n) in the average case.
Given an algebraic equation describing the time requirement for an al-
gorithm, the upper and lower bounds always meet. That is because in some
sense we have a perfect analysis for the algorithm, embodied by the running-
time equation. For many algorithms (or their instantiations as programs),
it is easy to come up with the equation that defines their runtime behavior.
The first rule says that if some function g(n) is an upper bound for your cost
function, then any upper bound for g(n) is also an upper bound for your
cost function. A similar property holds true for notation: If g(n) is a lower
bound for your cost function, then any lower bound for g(n) is also a lower
bound for your cost function. Likewise for Θ notation.
The significance of rule (2) is that you can ignore any multiplicative
constants in your equations when using big-Oh notation. This rule also
holds true for Ω and Θ notations.
Rule (3) says that given two parts of a program run in sequence (whether
two statements or two sections of code), you need consider only the more
expensive part. This rule applies to Ω and Θ notations as well: For both,
you need consider only the more expensive part.
Rule (4) is used to analyze simple loops in programs. If some action is
repeated some number of times, and each repetition has the same cost, then
the total cost is the cost of the action multiplied by the number of times
that the action takes place. This rule applies to Ω and Θ notations as well.
8
Taking the first three rules collectively, you can ignore all constants and
all lower-order terms to determine the asymptotic growth rate for any cost
function. The advantages and dangers of ignoring constants were discussed
near the beginning of this section. Ignoring lower-order terms is reason-
able when performing an asymptotic analysis. The higher-order terms soon
swamp the lower-order terms in their contribution to the total cost as n
becomes larger. Thus, if T (n) = 3n4 + 5n2 , then T(n) is in O(n4 ). The n2
term contributes relatively little to the total cost for large n.
Asymptotic comparison operator Numeric comparison opera-
tor
Our algorithm is o( something ) A number is < something
Our algorithm is O( something ) A number is≤ something
Our algorithm is Θ( something ) A number is = something
Our algorithm is Ω( something ) A number is ≥ something
Our algorithm is ω( something ) A number is > something
If the limit goes to ∞, then f(n) is in Ω (g(n)) because f(n) grows faster.
If the limit goes to zero, then f(n) is in O(g(n)) because g(n) grows faster. If
the limit goes to some constant other than zero, then f(n)= Θ(g(n)) because
both grow at the same rate
The first line is Θ(1). The for loop is repeated n times. The third line
takes constant time so, by simplifying rule (4) of Section 6.1, the total cost
9
for executing the two lines making up the for loop is Θ(n). By rule (3), the
cost of the entire code fragment is also Θ(n).
This code fragment has three separate statements: the first assignment
statement and the two for loops. Again the assignment statement takes
constant time; call it c1 . The second for loop is just like the one in Algorithm
4 and takes c2 n = Θ(n) time.
The first for loop is a double loop and requires a special technique. We
work from the inside of the loop outward. The expression sum++ requires
constant time; call it c3 . Because the inner for loop is executed i times, by
simplifying rule (4) it has cost c3 i. The outer for loop is executed n times,
but each time the cost of the inner loop is different because it costs c3 i with
i changing each time. You should see that for the first execution of the outer
loop, i is 1. For the second execution of the outer loop, i is 2. Each time
through the outer loop, i becomes one greater, until the last time through
the loop when i = n. Thus, the total cost of the loop is c3 times the sum of
the integers 1 through n.
From our mathematical preliminaries, we know that
n
X n(n + 1)
i= (1)
2
i=1
10
Algorithm 5 Not all doubly nested for loops are Θ(n2 ). The following pair
of nested loops illustrates this fact.
1:
sum1 = 0;
for (k=1; k<=n; k*=2) // Do log n times
for (j=1; j<=n; j++) // Do n times
sum1++;
2:
sum2 = 0;
for (k=1; k<=n; k*=2) // Do log n times
for (j=1; j<=k; j++) // Do k times
sum2++;
7 Complexity Classes
Let’s start by reviewing the definition of A decision problem.
11
7.3 Search problems
Search problems—they ask not just whether a solution exists, but also what
the solution is?
7.4 Definition
A complexity class is simply a set of decision problems.
7.5 Problem
Given a graph G and an integer k, is there a spanning tree of size less than k?
For most real-world applications, search problems are much more impor-
tant than decision problems. So why do we restrict our attention to decision
problems when defining complexity classes? Here are a few reasons:
12
8.1 Definition
A decision problem P is in NP if there exists a polynomial-time algorithm
A(x, y) such that, for every input x to the problem P,
8.2 Proposition
P ⊆ NP.
8.3 Proof
. Given a decision problem P, view P as a function whose domain is the set
of strings and whose range is 0,1. If P can be computed in polynomial time,
then we can just take A(x, y)= P(x). In this case, the verifier just re-solves
the entire problem. The converse to the above proposition is a famous open
problem:
8.4 Problem
(P vs. NP). Is it true that P= NP? The vast majority of computer scientists
believe that P 6= NP, and so the P vs. NP problem is sometimes called the
P 6= NP problem. If it were true that P = NP, then lots of problems that
seem hard would actually be easy. The P vs NP relationship is illustrated
on Figure 1
13
Figure 1: P Vs NP
The Towers of Hanoi problem takes exponential time, that is, its running
time is Θ(2n ). This is radically different from an algorithm that takes Θ(n
log n) time or Θ(n2 ) time. It is even radically different from a problem
that takes Θ(n4 ) time. These are all examples of polynomial running time,
because the exponents for all terms of these equations are constants.
14
Figure 2: An illustration of the TRAVELING SALESMAN problem. Five
vertices are shown, with edges between each pair of cities. The problem is
to visit all of the cities exactly once, returning to the start city, with the
least total cost
8.7 NP-hard
A problem is NP-hard if any problem in NP can be reduce to X in polyno-
mial time. Thus, X is as hard as any problem in NP.
8.8 NP-Complete
These are the problem that we know efficient non-deterministic algorithms
to solve them, but we do not know if there are efficient deterministic algo-
rithms. At the same time, we have not been able to prove that any of these
problems do not have efficient deterministic algorithms. This class of prob-
lems is called NP-complete. What is truly strange and fascinating about
NP-complete problems is that if anybody ever finds the solution to any
one of them that runs in polynomial time on a regular computer, then by a
series of reductions, every other problem that is in NP can also be solved in
polynomial time on a regular computer!
Definition of NP-Complete A problem X is defined to be NP-complete
if
1. X is in NP, and
2. X is NP-hard.
15
SALESMAN. Another such problem is called K-CLIQUE. Figure 3 shows
the relationship between P, NP, NP-complete, and exponential time
problems.
16
How is NP-completeness of practical significance for typical programmers?
Well, if your boss demands that you provide a fast algorithm to solve a
problem, he will not be happy if you come back saying that the best you
could do was an exponential time algorithm. But, if you can prove that the
problem is NP-complete, while he still won’t be happy, at least she should
not be mad at you! By showing that her problem is NP-complete, you are
in effect saying that the most brilliant computer scientists for the last 50
years have been trying and failing to find a polynomial time algorithm for
her problem.
References
[1] Clifford A. Shaffer Data Structures and Algorithm Analysis in Java,
Third Edition
17