DAA_Notes_Module 1_Complete
DAA_Notes_Module 1_Complete
Introduction and Examples: What is an Algorithm? Algorithm Specification, Examples from real life: Air
Travel, Xerox Shop, Document Similarity and types of algorithms.
Motivation for Performance Analysis using Examples: Bubble Sort, Selection Sort, Insertion Sort, String
Pattern Matching. Contrast performance analysis versus actual runs.
Performance Analysis Framework: Space complexity, Time complexity. Asymptotic Notations: Big-Oh
notation (O), Omega notation (Ω), Theta not - recursive and recursive Algorithms with Examples.
“Instructions” implies that there is something or someone capable of understanding and following the
instructions given.
“Computer,” - before the electronic computer was invented, the word “computer” meant a human
being involved in performing numeric calculations. Nowadays, “computers” are those ubiquitous
electronic devices that have become indispensable in almost everything we do.
Although the majority of algorithms are intended for eventual computer implementation, the notion
of algorithm does not depend on such an assumption.
To make a computer do anything, you have to write a computer program. To write a computer
program, you have to tell the computer, step by step, exactly what you want it to do. The computer
then "executes" the program, following each step mechanically, to accomplish the end goal. The
algorithm is the basic technique used to get the job done.
The consecutive integer checking algorithm, in the form presented, does not work correctly when
one of its input numbers is zero. This example illustrates why it is so important to specify the set of
an algorithm’s inputs explicitly and carefully.
What is the largest number p whose multiples can still remain on the list to make further iterations of the
algorithm necessary? - If p is a number whose multiples are being eliminated on the current pass, then the
first multiple we should consider is p . p because all its smaller multiples 2p, . . . , (p − 1)p have been
eliminated on earlier passes through the list. This observation helps to avoid eliminating the same number
more than once.
Also, p . p should not be greater than n, and therefore p cannot exceed √n rounded down (denoted ⌊√n⌋ ).
b. Flowchart
Flowchart is a graphic representation of an algorithm. It specifies an algorithm by a collection of connected
geometric shapes containing descriptions of the algorithm’s steps. But, flowcharts work well only if the
algorithm is small and simple.
Example: An algorithm to perform addition of two numbers.
1.3.1 Goals
Goal 1: Compute every pair of cities, which are actually connected by this network served by this airline.
How do we compute all pairs of cities A, B such that A and B are connected by a sequence of flights?
Goal 2: Is it really possible go from say Varanasi to Trivandrum or is it not possible?
Goal 3: How do we go from A to B?
1.3.2 Methodology
1.3.4 Efficiency
Next, how do we design such an algorithm, given the way we have represented these cities in the graph?
Does it depend on the representation? There could be multiple representations, some of which gives us more
or less efficient algorithm.
These are all the questions that we need to answer before we can decide on whether we have got the best
solution or not.
In terms of efficiency, we have to look at what are the things that determine the complexity of a problem.
1) It is fairly obvious that if we have more cities, the problem is more complicated. So, the number of cities,
say N, is parameter which determines how complicated the algorithm is going to be, or rather how long
it is going to take to run.
2) The other parameter which determines how complex the network is, is the number of direct flights, say
1.3.5 Variations
The problem discussed so far is a very simple problem; can I travel from A to B? But very often, you want
to go from A to B within some reasonable time frame.
Flights have arrival and departure times. It is not usually acceptable to break journey overnight. At the
same time, you also do not want to spend more than a certain amount of time (say four hours) in between
flights. Though there may be many connections, only some connections are feasible.
The problem becomes little more constraint. So, we do not just want to look at the connected paths from A
to B. But look at connected paths A to B, which meet some additional constraints in terms of timing and
other things.
How do we compute feasible paths with constraints? Can we solve this problem with the same approach
that we solve the simple problem or we need to take a radically different approach or do we need more
information in order to decide or solve the problem.
1.3.6.2 Maintenance
Next, is to decompose the problem and solve this problem by reducing it to a simpler problem. First,
choose a job optimally to schedule and the machine on which it will run, according to some strategy. That
is, pick each job as the first job, determine how much time it takes to complete and then select the most
efficient one. Then, recursively solve the problem for the remaining N-1 jobs.
Another option is the greedy approach. Look at all the jobs which are yet to be done, find some criteria by
which we choose the next job to do without looking at all the possibilities of doing the other jobs and
complete it. The choice of the next job is done once and for all; never go back and try another job sequence.
We have to justify that the chosen criteria is actually optimal. But, it is not sure if this strategy always gets
the best possible return.
There are different criteria to choose the next job. It could be the least number of pages that is the shortest
time to print, or the job closest to the deadline that is the one which we are most likely to miss finishing in
time and having to give a discount, etc.
1.4.3 Variations
There could be different variations to this basic problem.
- If we assume that the shop has many photo copiers, it is reasonable to assume that some are new and
some are old. The new ones may work faster than the ones are that are old. Therefore, the time that will
take to finish a job depends on which machine the job is put on.
- If competition among the xerox shops is considered, the strategies / rewards chosen for all types of
machines may not be uniform over all the machines.
- When we use a machine; we use some other resources as well – like ink, paper, electricity, etc. and
this cost may vary from one machine to another.
If a job is split machines, the cost for the shop might be only in terms of time; the cost for the shop
maybe more or less. So, the actual revenue that the shop realizes maybe more or less depending on
which machine it chooses.
- A machine cannot run indefinitely without having to be stopped for some time for maybe maintenance,
for loading paper, for setting up between jobs, etc. We cannot realistically assume that every machine
is continuously available.
Thus, there is a basic problem with some constraints which you want to solve, but that problem can be made
more realistic by adding several new features. By the time you add a new feature, you have to see whether
the solution that you have for the simpler problems still works or the new feature demands radically new
approach and if so how you should get that.
1.5.1.1 Plagiarism detection: Suppose somebody has put an article in a newspaper or on a website and you
believe that this author has not really written the article themselves. Probably, they have copied these articles
from somewhere else. If you are a teacher in a course, you might be worried that two students have submitted
the same assignments or one student has copied an assignment some details from some source. If you can
measure how similar two documents are, you can try to quantify this notion that somebody has copied from
somebody else.
1.5.1.2 Checking changes between versions of code: It might also be used when some people are writing
code typically writing programs for some application. Over a period of time, documents evolve as the
programs evolve. They might add some features. You might want to look at two different pieces of code
and try to figure out what changes have taken place - how similar they are or how different they are.
1.5.1.3 Answering web search queries more effectively: If you query a search engine and it reports results,
typically it tries to group together results which is similar. Suppose. there are 10 different copies or similar
copies of a document saying more or less the same thing and these show up as your first 10 search results.
Say, there exists another document that is highly relevant and quite different from these; this document will
be lost because it will not be in the first page of searches. Hence, it is useful to be able to group together the
results of a search query by similarity, so that the user is actually presented by an effective choice between
different answers to the search query and not just the large number of variations of the same answers.
Fig. 1.7 Tree of recursive calls for computing the 5th Fibonacci number by the recursive definition
The problem of computing F(n) is expressed in terms of its smaller and overlapping subproblems of
computing F(n − 1) and F(n − 2). So, we can simply assign elements of a one-dimensional array with the n
+ 1 consecutive values of F(n) by starting, in view of initial conditions (1.2), with 0 and 1 and using equation
(1.1) as the rule for producing all the other elements. Obviously, the last element of this array will contain
F(n).
The problems that can be solved using the Dynamic Programming algorithm are Knapsack
Problem, Weighted Job Scheduling, Floyd Warshall Algorithm, etc.
One possible approach is to count the number of times each of the algorithm’s operations is executed.
First, identify the most important operation of the algorithm, called the basic operation. The operation
contributing the most to the total running time is the basic operation.
Then, compute the number of times the basic operation is executed.
It is not difficult to identify the basic operation of an algorithm: it is usually the most time-consuming
operation in the algorithm’s innermost loop.
Therefore,
Note that we were able to find the complexity without actually knowing the value of cop: it was neatly
cancelled out in the ratio. Also note that ½, the multiplicative constant in the formula for the count C(n),
was also cancelled out.
The efficiency analysis framework ignores multiplicative constants and concentrates on the count’s order
of growth to within a constant multiple for large-size inputs.
- The running time of this algorithm can be quite different for the same list size n.
- In the worst case, when there are no matching elements or the first matching element happens to be the
last one on the list, the algorithm makes the largest number of key comparisons among all possible inputs
of size n: Cworst(n) = n.
This general formula yields some quite reasonable answers. For example,
- If p = 1 (i.e., the search must be successful), the average number of key comparisons made by sequential
search is (n + 1) /2; i.e., the algorithm will inspect, on average, about half of the list's elements.
- If p = 0 (i.e., the search must be unsuccessful), the average number of key comparisons will be n because
Investigation of the average-case efficiency is considerably more difficult than investigation of the worst-
case and best-case efficiencies. Hence, for simplicity, the average-case efficiency of algorithms under
discussion are mostly quoted.
There are many important algorithms for which the average-case efficiency is much better than the worst-
case efficiency.
Ω(g(n)), stands for the set of all functions with a higher or same order of growth as g(n) (to within a constant
multiple, as n goes to infinity). For example,
1.10.3 Θ-notation
DEFINITION A function t(n) is said to be in Θ(g(n)), denoted t(n) ∈ Θ(g(n)), if t(n) is bounded both
above and below by some positive constant multiples of g(n) for all large n, i.e., if there exist some
positive constants c1 and c2 and some nonnegative integer n0 such that
c2g(n) ≤ t(n) ≤ c1g(n) for all n ≥ n0.
The definition is illustrated in Fig. 1.10.
For example, we can check whether an array has equal elements by the following two-part algorithm:
i) sort the array by applying some known sorting algorithm;
ii) scan the sorted array to check its consecutive elements for equality.
If a sorting algorithm used in the first part makes no more than comparisons (and hence is in
O(n2)) while the second part makes no more than n − 1 comparisons (and hence is in O(n)), the efficiency
of the entire algorithm will be in O(max{n2, n}) = O(n2).
Stirling’s formula:
Since the limit is equal to a positive constant, the functions have the same order of growth or, symbolically,
Since the limit is equal to zero, log2n has a smaller order of growth than √n.
Thus, though 2n grows very fast, n! grows still faster. We can write symbolically that n! ∈ Ω(2n).
1.11.1 EXAMPLE 1
Consider the problem of finding the value of the largest element in a list of n numbers.
For simplicity, we assume that the list is implemented as an array.
Pseudocode
ALGORITHM MaxElement(A[0..n − 1])
//Determines the value of the largest element in a given array
//Input: An array A[0..n − 1] of real numbers
//Output: The value of the largest element in A
maxval ←A[0]
for i ←1 to n − 1 do
if A[i]>maxval
maxval←A[i]
return maxval
• The measure of an input’s size here is the number of elements in the array, i.e., n.
• The operations that are going to be executed most often are in the algorithm’s for loop.
• There are two operations in the loop’s body:
- the comparison A[i]> maxval and
- the assignment maxval←A[i].
Which of these two operations should we consider basic?
Since the comparison is executed on each repetition of the loop and the assignment is not, we should
consider the comparison to be the algorithm’s basic operation.
The number of comparisons will be the same for all arrays of size n; therefore, in terms of this metric, there
is no need to distinguish among the worst, average, and best cases here.
Let C(n) the number of times this comparison is executed.
1.11.2 General Plan for Analyzing the Time Efficiency of Nonrecursive Algorithms
1. Decide on a parameter (or parameters) indicating an input’s size.
2. Identify the algorithm’s basic operation. (As a rule, it is located in the innermost loop.)
3. Check whether the number of times the basic operation is executed depends only on the size of an input.
If it also depends on some additional property, the worst-case, average-case, and, if necessary, best-case
efficiencies have to be investigated separately.
4. Set up a sum expressing the number of times the algorithm’s basic operation is executed.
5. Using standard formulas and rules of sum manipulation, either find a closed-form formula for the count
or, at the very least, establish its order of growth.
1.11.4 EXAMPLE 2
Consider the element uniqueness problem: check whether all the elements in a given array of n elements
are distinct.
Pseudocode:
ALGORITHM UniqueElements(A[0..n − 1])
//Determines whether all the elements in a given array are distinct
//Input: An array A[0..n − 1]
//Output: Returns “true” if all the elements in A are distinct
// and “false” otherwise
• The natural measure of the input’s size here is again n, the number of elements in the array.
• Since the innermost loop contains a single operation (the comparison of two elements), we should
consider it as the algorithm’s basic operation.
• The number of element comparisons depends not only on n but also on whether there are equal elements
in the array and, if there are, which array positions they occupy.
• We will limit our investigation to the worst case only.
By definition, the worst-case input is an array for which the number of element comparisons Cworst(n) is the
largest among all arrays of size n.
An inspection of the innermost loop reveals that there are two kinds of worst-case inputs
- Inputs for which the algorithm does not exit the loop prematurely: arrays with no equal elements and
- arrays in which the last two elements are the only pair of equal elements.
For such inputs, one comparison is made for each repetition of the innermost loop, i.e., for each value of the
loop variable j between its limits i + 1 and n − 1; this is repeated for each value of the outer loop, i.e., for
each value of the loop variable i between its limits 0 and n − 2.
Accordingly, we get
1.11.5 EXAMPLE 3
Given two n × n matrices A and B, find the time efficiency of the definition-based algorithm for computing
their product C = AB.
By definition, C is an n × n matrix whose elements are computed as the scalar (dot) products of the rows of
matrix A and the columns of matrix B:
where C[i, j ]= A[i, 0]B[0, j]+ . . . + A[i, k]B[k, j]+ . . . + A[i, n − 1]B[n − 1, j] for every pair of indices 0 ≤ i,
- The total number of multiplications M(n) is expressed by the following triple sum:
1.11.6 EXAMPLE 4
The following algorithm finds the number of binary digits in the binary representation of a positive decimal
integer.
ALGORITHM Binary(n)
//Input: A positive decimal integer n
//Output: The number of binary digits in n’s binary representation
count ←1
while n > 1 do
count ←count + 1
n← ⌊ n/2⌋
return count
- The most frequently executed operation here is not inside the while loop but rather the comparison n >
1 that determines whether the loop’s body will be executed.
- Since the number of times the comparison will be executed is larger than the number of repetitions of
the loop’s body by exactly 1, the choice is not that important.
- The loop variable takes on only a few values between its lower and upper limits; therefore, we have to
use an alternative way of computing the number of times the loop is executed.
- Since the value of n is about halved on each repetition of the loop, the answer should be about log2 n.
- The exact formula for the number of times the comparison n>1 will be executed is actually⌊log2 n⌋+1;
which is the number of bits in the binary representation of n.
M(n − 1) multiplications are spent to compute F(n − 1), and one more multiplication is needed to multiply
the result by n.
The last equation defines the sequence M(n) that we need to find.
This equation defines M(n) not explicitly, i.e., as a function of n, but implicitly as a function of its value at
another point, namely n − 1. Such equations are called recurrence relations or, for brevity, recurrences.
Our goal now is to solve the recurrence relation M(n) = M(n − 1) + 1,
i.e., to find an explicit formula for M(n) in terms of n only.
To determine a solution uniquely, we need an initial condition that tells us the value with which the
sequence starts. We can obtain this value by inspecting the condition that makes the algorithm stop its
recursive calls:
if n = 0 return 1.
This tells us two things.
i) Since the calls stop when n = 0, the smallest value of n for which this algorithm is executed and hence
M(n) defined is 0.
ii) By inspecting the pseudocode’s exiting line, we can see that when n = 0, the algorithm performs no
multiplications. Therefore, the initial condition we are after is
Thus, we succeeded in setting up the recurrence relation and initial condition for the algorithm’s number of
multiplications M(n):
M(n) = M(n − 1) + 1 for n > 0,
M(0) = 0.
We are dealing here with two recursively defined functions.
The first is the factorial function F(n) itself; it is defined by the recurrence
1.12.2 General Plan for Analyzing the Time Efficiency of Recursive Algorithms
1. Decide on a parameter (or parameters) indicating an input’s size.
2. Identify the algorithm’s basic operation.
3. Check whether the number of times the basic operation is executed can vary on different inputs of the
same size; if it can, the worst-case, average-case, and best-case efficiencies must be investigated
separately.
4. Set up a recurrence relation, with an appropriate initial condition, for the number of times the basic
operation is executed.
5. Solve the recurrence or, at least, ascertain the order of growth of its solution.
1.12.3 EXAMPLE 2
The Tower of Hanoi puzzle. In this puzzle, we (or mythical monks) have n disks of different sizes that can
slide onto any of three pegs.
Initially, all the disks are on the first peg in order of size, the largest on the bottom and the smallest on top.
The goal is to move all the disks to the third peg, using the second one as an auxiliary, if necessary. We can
move only one disk at a time, and it is forbidden to place a larger disk on top of a smaller one.
Fig. 1.12 Tree of recursive calls made by the recursive algorithm for the Tower of Hanoi puzzle.
The number agrees, as it should, with the move count obtained earlier.
1.12.4 EXAMPLE 3
Recursive version of the algorithm that finds the number of binary digits in the binary representation of a
positive decimal integer.
ALGORITHM BinRec(n)
//Input: A positive decimal integer n
//Output: The number of binary digits in n’s binary representation
if n = 1 return 1
else return BinRec(⌊ n/2⌋) + 1
Let us set up a recurrence and an initial condition for the number of additions A(n) made by the algorithm.
The number of additions made in computing BinRec(⌊ n/2⌋) is A(⌊ n/2⌋), plus one more addition is made by
the algorithm to increase the returned value by 1. This leads to the recurrence
A(n) = A(⌊ n/2⌋) + 1 for n > 1
Since the recursive calls end when n is equal to 1 and there are no additions made then, the initial condition
is:
A(1) = 0.
To solving such a recurrence, we assume n is a power of 2 i.e. n = 2k.
A(2k) = A(2k−1) + 1 for k > 0,
1.14.3 Example
Sort the list 89, 45, 68, 90, 29, 34, 17 using selection sort algorithm.
1.14.4 Analysis
The analysis of selection sort is straightforward.
The input size is given by the number of elements n.
The basic operation is the key comparison A[j ]<A[min].
The number of times it is executed depends only on the array size and is given by the sum:
1.15.2 Pseudocode
ALGORITHM BubbleSort(A[0..n − 1])
1.15.3 Example
Sort the list 89, 45, 68, 90, 29, 34, 17 using bubble sort algorithm.
First two passes of bubble sort on the list 89, 45, 68, 90, 29, 34, 17.
- A new line is shown after a swap of two elements is done.
- The elements to the right of the vertical bar are in their final positions and are not considered in
subsequent iterations of the algorithm.
1.15.4 Analysis
The number of key comparisons for the bubble-sort version given above is the same for all arrays of size n.
It is obtained by a sum that is almost identical to the sum for selection sort:
The first version of the algorithm obtained can often be improved by exploiting the following observation:
if a pass through the list makes no exchanges, the list has been sorted and we can stop the algorithm.
- The new version runs faster on some inputs.
- It is still in Θ(n2) in the worst and average cases.
1.16.1 Working
Sequential search is a brute-force algorithm for the general searching problem.
The algorithm simply compares successive elements of a given list with a given search key until either a
match is encountered (successful search) or the list is exhausted without finding a match (unsuccessful
search).
A simple extra trick is often employed in implementing sequential search: if we append the search key to
the end of the list, the search for the key will have to be successful, and therefore we can eliminate the end
of list check altogether.
1.16.2 Pseudocode
ALGORITHM SequentialSearch2(A[0..n], K)
//Implements sequential search with a search key as a sentinel
//Input: An array A of n elements and a search key K
//Output: The index of the first element in A[0..n − 1] whose value is equal to K or
// −1 if no such element is found
A[n]←K
i ←0
1.16.3 Improvement
An improvement in sequential search if a given list is known to be sorted:
searching in such a list can be stopped as soon as an element greater than or equal to the search key is
encountered.
1.16.4 Analysis
Sequential search provides an excellent illustration of the brute-force approach, with its characteristic
strength (simplicity) and weakness (inferior efficiency).
The efficiency results obtained for the standard version of sequential search change for the enhanced version
varies only very slightly, so that the algorithm remains linear in both the worst and average cases.
1.17.2 Pseudocode
ALGORITHM BruteForceStringMatch(T [0..n − 1], P[0..m − 1])
//Implements brute-force string matching
//Input: An array T [0..n − 1] of n characters representing a text and
// an array P[0..m − 1] of m characters representing a pattern
//Output: The index of the first character in the text that starts a matching substring or
−1 if the search is unsuccessful
for i ←0 to n − m do
j ←0
while j <m and P[j ]= T [i + j ] do
1.17.3 Example
Example of brute-force string matching: The pattern’s characters that are compared with their text
counterparts are in bold type.
For this example, the algorithm shifts the pattern almost always after a single character comparison.
- The worst case is much worse: the algorithm may have to make all m comparisons before shifting the
pattern, and this can happen for each of the n − m + 1 tries.
- Thus, in the algorithm makes m(n − m + 1) character comparisons, which puts it in the O(nm) class.
For a typical word search in a natural language text, however, we should expect that most shifts would
happen after very few comparisons. Therefore, the average-case efficiency should be considerably better
than the worst-case efficiency.