IA1 - Question Bank
IA1 - Question Bank
Time Complexity
The time T(P)taken by a program P is the sum of the compile time and the run (or
execution)time. The compile time does not depend on the instance characteristics.
Also, we may assume that a compiled program will be run several times without
recompilation. Consequently, we concern ourselves with just the run time of a
program.
This run time is denoted by tp(instance characteristics).
If we knew the characteristics of the compiler to be used, we could proceed to
determine the number of additions, subtractions, multiplications, divisions, compares,
loads, stores, and soon, that would be made by the code for P.
So, we could obtain an expression for tp(n) of the form
For simplicity, we consider n itself as an indicator of this algorithm's input size. The
basic operation of the algorithm is multiplication, whose number of executions we
denote M(n). Since the function F(n) is computed according to the formula:
F(n) = F(n- 1) · n for n > 0,
the number of multiplications M (n) needed to compute it must satisfy the equality
( ) ( )
( )
the number of multiplications M (n) needed to compute it must satisfy the equality
Indeed, 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. Recurrence relations play an important role not only in
analysis of algorithms but also in some areas of applied mathematics.
We measure an input's size by matrix order n. In the algorithm's innermost loop are
two arithmetical operations-multiplication and addition-that, in principle, can compete
for designation as the algorithm's basic operation. We consider multiplication as the
algorithm's basic operation.
Let us set up a sum for the total number of multiplications M(n) executed by the
algorithm.
and the total number of multiplications M(n) is expressed by the following triple sum:
( ) ∑∑∑
Now we can compute this sum by using formula (S1) and rule ( R1) (see above).
Starting with the innermost sum ∑ , which is equal ton (why?), we get
( ) ∑∑∑ ∑∑ ∑
If we now want to estimate the running time of the algorithm on a particular machine,
we can do it by the product
( ) ( )
where Cm is the time of one multiplication on the machine in question. We would get a
more accurate estimate if we took into account the time spent on the additions, too:
( ) ( ) ( ) ( + )
where ca is the time of one addition. Note that the estimates differ only by their
multiplicative constants, not by their order of growth.
Clearly, 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.
The worst-case efficiency of an algorithm is its efficiency for the worst-case input of
size n, which is an input (or inputs) of size n for which the algorithm runs the longest
among all possible inputs of that size.
The best-case efficiency of an algorithm is its efficiency for the best-case input of size
n, which is an input (or inputs) of size n for which the algorithm runs the fastest
among all possible inputs of that size. For example, for sequential search, best-case
inputs are lists of size n with their first elements equal to a search key; accordingly,
Cbest(n) = 1.
In the case of a successful search, the probability of the first match occurring in the i
th position of the list is p / n for every i, and the number of comparisons made by the
algorithm in such a situation is obviously i. In the case of an unsuccessful search,
the number of comparisons is n with the probability of such a search being (1- p).
Therefore,
6. Design algorithm for sorting using selection sort technique. Also analyze the
time efficiency.
Answer:
We start selection sort by scanning the entire given list to find its smallest element
and exchange it with the first element, putting the smallest element in its final
position in the sorted list.
Then we scan the list, starting with the second element, to find the smallest among
the last n - 1 elements and exchange it with the second element, putting the second
smallest element in its final position.
Generally, on the ith pass through the list, which we number from 0 to n - 2, the
algorithm searches for the smallest item among the last n - i elements and swaps it
with Ai:
Thus, selection sort is a ( ) algorithm on all inputs. Note, however, that the number
of key swaps‘ is only (1)) Or, more precisely, n - 1 (one for each repetition of the i
loop).
O-notation
DEFINITION 1 A function t(n) is said to be in O(g(n)), denoted t(n) Є O(g(n)), if t(n) is
bounded above by some constant multiple of g(n) for all large 11, i.e., if there exist
some positive constant c and some nonnegative integer n0 such that
( ) ( )
The definition is illustrated in Figure below where, for the sake of visual clarity, n is
extended to be a real number.
Ω-notation
DEFINITION 2 A function t(n) is said to be in Ω(g(n)), denoted t(n) Ω (g(n)), if t(n) is
bounded below by some positive constant multiple of g(n) for all large n, i.e., if there
exist some positive constant c and some nonnegative integer n0 such that
( ) ( )
The definition is illustrated in Figure below.
Θ-notation
DEFINITION 3 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 constant c1 and c2 and some nonnegative integer n0
such that
( ) ( ) ( )
Using a natural language has an obvious appeal; however, the inherent ambiguity of
any natural language makes a succinct and clear description of algorithms
surprisingly difficult.
A pseudocode is a mixture of a natural language and programming language like
constructs. A pseudocode is usually more precise than a natural language, and its
usage often yields more succinct algorithm descriptions.
In the earlier days of computing, the dominant vehicle for specifying algorithms was a
flowchart, a method of expressing an algorithm by a collection of connected
geometric shapes containing descriptions of the algorithm's steps. This
representation technique has proved to be inconvenient for all but very simple
algorithms.
Analyzing an Algorithm
We usually want our algorithms to possess several qualities. After correctness, by far
the most important is efficiency. In fact, there are two kinds of algorithm efficiency:
time efficiency and space efficiency. Time efficiency indicates how fast the
algorithm runs; space efficiency indicates how much extra memory the algorithm
needs.
Coding an Algorithm
Most algorithms are destined to be ultimately implemented as computer programs.
Programming an algorithm presents both a peril and an opportunity. The peril lies in
the possibility of making the transition from an algorithm to a program either
incorrectly or very inefficiently. Some influential computer scientists strongly
believe that unless the correctness of a computer program is proven with full
mathematical rigor, the program cannot be considered correct.
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. The
following is a pseudocode of a standard algorithm for solving the problem.
The obvious 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?. We should consider the comparison to be the algorithm's
basic operation.
Let us denote C(n) the number of times this comparison is executed (size n). The
algorithm makes one comparison on each execution of the loop, which is repeated for
each value of the loop's variable i within the bounds 1 and n - 1 (inclusively).
Therefore, we get the following sum for C(n):
This is an easy sum to compute because it is nothing else but 1 repeated n - 1
times. Thus,
[ ] [ ]
Answer:
The principal insight of the algorithm lies in the discovery that we can find the
product C of two 2 × 2 matrices A and B with just seven multiplications as opposed to
the eight required by the brute-force algorithm. This is accomplished by using the
following formulas:
Where,
11. Explain the concept of Divide and Conquer. Write the recursive algorithm
to perform mergesort on the list of elements.
Answer:
Divide-and-conquer algorithms work according to the following general plan:
1. A problem's instance is divided into several smaller instances of the same
problem, ideally of about the same size.
2. The smaller instances are solved (typically recursively, though sometimes a
different algorithm is employed when instances become small enough).
3. If necessary, the solutions obtained for the smaller instances are combined to
get a solution to the original instance.
12. Explain the concept of Decrease and Conquer. With algorithm and analysis
explain insertion sort.
Answer:
The decrease-and-conquer technique is based on exploiting the “relationship
between a solution to a given instance of a problem and a solution to a smaller”
instance of the same problem.
Once such a relationship is established, it can be exploited either top down
(recursively) or bottom up (without a recursion).
There are three major variations of decrease-and-conquer:
decrease by a constant
decrease by a constant factor
variable size decrease
Insertion Sort:
Following the technique's idea, we assume that the smaller problem of sorting
the array A[O .. n- 2] has already been solved to give us a sorted array of size n - 1:
A[O] ≤ ... ≤ A [n - 2]. How can we take advantage of this solution to the smaller
problem to get a solution to the original problem by taking into account the element
A[n -1]?
Obviously, all we need is to find an appropriate position for A[n - 1] among the sorted
elements and insert it there.
Figure 2.5: Iteration of insertion sort: A[i] is inserted in its proper position among the
preceding elements previously sorted.
Since v = A[i], it happens if and only if A[j] > A[i] for j = i- 1, ... , 0. Thus, for the
worst-case input, we get A[O] > A[1] (for i= 1), A[1] > A[2] (for i= 2), ... , A[n- 2] > A[n -
1] (for i= n -1). In other words, the worst-case input is an array of strictly decreasing
values. The number of key comparisons for such an input is
( )
( ) ∑∑ ∑ ( )
Thus, in the worst case, insertion sort makes exactly the same number of comparisons
as selection sort.
13. Obtain the topological sort for the graph given below using source removal
method. Explain.
Answer:
“Solve the problem by yourself”
The source removal method is based on a direct implementation of the
decrease ( by one ) -and-conquer technique: repeatedly, identify in a remaining
digraph a source, which is a vertex with no incoming edges, and delete it along
with all the edges outgoing from it.
If there are several sources, break the tie arbitrarily. If there is none,
stop because the problem cannot be solved). The order in which the vertices are
deleted yields a solution to the topological sorting problem.
Note that the solution obtained by the source-removal algorithm is different
from the one obtained by the DFS-based algorithm. Both of them are correct, of
course; the topological sorting problem may have several alternative solutions.
14. Explain Quick sort algorithm in details and arrange the following elements
using quick sort. {5, 3, 1, 9, 8, 2, 4, 7}.
Answer:
Quicksort is the other important sorting algorithm that is based on the divide-
and conquer approach. Unlike mergesort, which divides its input elements according
to their position in the array, quicksort divides them according to their value. A
partition is an arrangement of the array’s elements so that all the elements to the left
of some element A[s] are less than or equal to A[s], and all the elements to the right of
A[s] are greater than or equal to it:
Obviously, after a partition is achieved, A[s] will be in its final position in the sorted
array, and we can continue sorting the two subarrays to the left and to the right of
A[s] independently (e.g., by the same method).
Unlike the Lomuto algorithm, we will now scan the subarray from both ends,
comparing the subarray’s elements to the pivot. The left-to-right scan, denoted
below by index pointer i, starts with the second element. Since we want elements
smaller than the pivot to be in the left part of the subarray, this scan skips over
elements that are smaller than the pivot and stops upon encountering the first
element greater than or equal to the pivot. The right-to-left scan, denoted below by
index pointer j, starts with the last element of the subarray. Since we want elements
larger than the pivot to be in the right part of the subarray, this scan skips over
elements that are larger than the pivot and stops on encountering the first element
smaller than or equal to the pivot.
After both scans stop, three situations may arise, depending on whether or not
the scanning indices have crossed. If scanning indices i and j have not crossed, i.e., i <
j, we simply exchange A[i] and A[j] and resume the scans by incrementing i and
decrementing j, respectively:
If the scanning indices have crossed over, i.e., i > j, we will have partitioned the
subarray after exchanging the pivot with A[j]:
Finally, if the scanning indices stop while pointing to the same element, i.e., i = j,
the value they are pointing to must be equal to p. Thus, we have the subarray
partitioned, with the split position s = i = j:
We can combine the last case with the case of crossed-over indices (i > j ) by
exchanging the pivot with A[j ] whenever i ≥ j .
Here is pseudocode implementing this partitioning procedure.
_______________________________________________________________________________
ALGORITHM Partition(A[l..r])
//Partitions a subarray by Hoare’s algorithm, using the first element
// as a pivot
//Input: Subarray of array A[0..n − 1], defined by its left and right
// indices l and r (l<r)
//Output: Partition of A[l..r], with the split position returned as
// this function’s value
p←A[l]
i ←l; j ←r + 1
repeat
repeat i ←i + 1 until A[i]≥ p
repeat j ←j − 1 until A[j ]≤ p
swap(A[i], A[j ])
until i ≥ j
swap(A[i], A[j ]) //undo last swap when i ≥ j
swap(A[l], A[j ])
return j
An example of sorting an array by quicksort is given in Figure 2.13.
In the worst case, all the splits will be skewed to the extreme: one of the two
subarrays will be empty, and the size of the other will be just 1 less than the size of
the subarray being partitioned. This unfortunate situation will happen, in particular,
for increasing arrays, i.e., for inputs for which the problem is already solved! Indeed, if
A[0..n − 1] is a strictly increasing array and we use A[0] as the pivot, the left-to-
right scan will stop on A[1] while the right-to-left scan will go all the way to reach
A[0], indicating the split at position 0:
So, after making n + 1 comparisons to get to this partition and exchanging the
pivot A[0] with itself, the algorithm will be left with the strictly increasing array A[1..n
− 1] to sort. This sorting of strictly increasing arrays of diminishing sizes will continue
until the last one A[n − 2..n − 1] has been processed. The total number of key
comparisons made will be equal to
15. With algorithm and analysis explain finding of binary tree height.
Answer:
A binary tree T is defined as a finite set of nodes that is either empty or consists of a
root and two disjoint binary trees TL and TR called, respectively, the left and right
subtree of the root. We usually think of a binary tree as a special case of an ordered
tree (Figure 2.14).
Since the definition itself divides a binary tree into two smaller structures of the
same type, the left subtree and the right subtree, many problems about binary trees
can be solved by applying the divide-and-conquer technique. As an example, let us
consider a recursive algorithm for computing the height of a binary tree.
The height is defined as the length of the longest path from the root to a leaf.
Hence, it can be computed as the maximum of the heights of the root’s left and
right subtrees plus 1. (We have to add 1 to account for the extra level of the root.) Also
note that it is convenient to define the height of the empty tree as −1.
Thus, we have the following recursive algorithm.
ALGORITHM Height(T)
//Computes recursively the height of a binary tree
//Input: A binary tree T
//Output: The height of T
if T = ∅
return −1
else
return max{ Height(Tleft), Height(Tright)} + 1
We measure the problem’s instance size by the number of nodes n(T) in a given binary
tree T. Obviously, the number of comparisons made to compute the maximum of
two numbers and the number of additions A(n(T)) made by the algorithm are the same.
We have the following recurrence relation for A(n(T )):
A(n(T )) = A(n(Tleft)) + A(n(Tright)) + 1 for n(T ) > 0,
A(0) = 0.
Figure 5: (a) Instance of the knapsack problem. (b) Its solution by exhaustive search.
(The information about the optimal selection is in bold.)
The exhaustive-search approach to this problem leads to generating all the
subsets of the set of n items given, computing the total weight of each subset to
identify feasible subsets (i.e., the ones with the total weight not exceeding the
knapsack's capacity), and finding a subset of the largest value among them. As an
example, the solution to the instance of Figure 5 is given in Figure 5b. Since the
number of subsets of an n-element set is 2n, the exhaustive search leads to a Ω (2n)
algorithm no matter how efficiently individual subsets are generated.