Introduction and Elementary Data Structures: Analysis of Algorithms
Introduction and Elementary Data Structures: Analysis of Algorithms
Chapter 1
Introduction and elementary data structures
What is an algorithm?
Insertion sort
Analyzing Algorithm
Order of growth
Asymptotic Notation
Elementary Data Structure
Heaps and Heapsort
Algorithm is an abstract computational procedure which takes some value or values as input and
produces a value or values as output. Another important criterion we will assume about algorithms is that they
terminate after a finite number of operations. There is another word for an algorithm which obeys all of the
above properties except termination, and that is computational procedure. A program is the expression of an
algorithm in a programming language.
An algorithm is thus a sequence of computational steps that transform the input into output. Algorithm is
a tool for solving a well specified computational problem. The algorithm describes a specific computational
procedure for achieving input-output relationship.
For example, we might need to sort a sequence of numbers into no decreasing order. How we formally
define the solving problem:
Input: A sequence of „n‟ numbers <a1, a2, ……………, an>.
Output: A permutation <a‟1, a‟2, ……………, a‟n> of the input sequence such that a‟1 ≤ a‟2 ≤ ……… ≤ a‟n
Eg, Input sequence <15, 26, 31, 7, 4, 26, 10>
Output sequence <4, 7, 10, 15, 26, 26, 31> such input sequence is called an instance of the sorting problem.
Instance of a problem consists of the i/p needed to compute a solution to the problem.
We have large number of good sorting algorithms. Which algorithm is best for a given application
depends on some factors:
The number of items to be sorted.
The extent to which the items are already somewhat sorted.
Possible restrictions on the item values.
The architecture of the computer.
The kind of storage devices to be used.(main memory, disks, etc).
An algorithm said to be correct if, for every input instance, it halts with the correct output. We say that a correct
algorithm solves the given computational problem. An incorrect algorithm might not halt at all or some input
instances, or it might halt with an incorrect answer.
Insertion Sort
Example:
Analyzing algorithms
We want to predict the resources that the algorithm requires. Usually, running time. In order to predict
resource requirements, we need a computational model.
The running time of the algorithm is the sum of running times for each statement executed. A statement that
takes ci steps to execute and execute n times will contribute cin to the total running time. To compute T(n), the
running time of INSERTION SORT on an input of n values, we sum the product of cost and times column.
∑ ( ) ( )
The running time depends on the values of tj . These vary according to the input.
We usually concentrate on finding the worst-case running time: the longest running time for any input of size n.
Reasons:
The worst-case running time gives a guaranteed upper bound on the running time for any input.
For some algorithms, the worst case occurs often. For example, when searching, the worst case often
occurs when the item being searched for is not present, and searches for absent items may be frequent.
Why not analyze the average case? Because it‟s often about as bad as the worst case.
Order of growth
For ease analysis, look only at the leading term of the formula for running time. Drop lower-order terms
and ignore the constant coefficient in the leading term.
Example: For insertion sort, we already abstracted away the actual statement costs to conclude that the worst-
case running time is an2 + bn + c.
Drop lower-order terms ⇒ an2
Ignore constant coefficient ⇒ n2
4 Prepared by: Mujeeb Rahman
But we cannot say that the worst-case running time T (n) equals n2. It grows like n2. But it doesn‟t equal n2.
We say that the running time is Θ(n2) to capture the notion that the order of growth is n2.
We usually consider one algorithm to be more efficient than another if its worst-case running time has a smaller
order of growth.
Due to constant factors and lower order terms, an algorithm whose running time has a higher order of
growth might take less time for small inputs than an algorithm whose running time has a lower order of growth.
But for large enough inputs, a Θ(n2) algorithm for example will run more quickly in the worst case than a Θ(n3)
algorithm.
Growth of Function
The order of growth of the running time of an algorithm gives a simple characterization of the
algorithm‟s efficiency and also allows us to compare the relative performance of alternative algorithms. For large
enough inputs, the multiplicative constants and lower order terms of an exact running time are dominated by the
effect of input size itself.
When we look at input sizes large enough to make only the order of growth of the running time relevant,
we are studying the asymptotic efficiency of algorithms. We are concerned with how the running time of an
algorithm increases with the size of the input in the limit, as the size of the input increases without bound.
Usually an algorithm that is asymptotically more efficient will be the best choice for all but very small
input.
Asymptotic Notation
The notation we use to describe the asymptotic running time of an algorithm are defined in terms of
functions whose domains are the set of natural numbers, N= {0,1,2,…….}. Such notations are convenient for
describing the worst-case running time function T(n), which usually is defined only on integer input sizes.
The best case step count is the minimum number of steps that can be executed for the given parameters.
The worst case step count is the maximum number of steps that can be executed for the given parameters. The
average step count is the average number of steps that can be executed for the given parameters.
If we have two algorithms with complexity c1n2+c2n and c3n respectively, then we know that the one with
complexity c3n will be faster than the one with complexity c1n2+c2n for sufficient large value of n. For small
value of „n‟, either algorithm could be faster(depending on c1, c2 and c3).
If c1 = 1, c2 = 2 and c3= 100 then
c1n2+c2n ≤ c3n for n ≤ 98
c1n2+c2n > c3n for n > 98
If c1 = 1, c2 = 2 and c3= 1000 then
c1n2+c2n ≤ c3n for n ≤ 998
No matter the values of c1, c2 and c3 , there will be an „n‟ beyond which the algorithm with complexity c3n will
be faster than the one with complexity c1n2+c2n. This value of „n‟ will be called the break-even point.
Θ – notation(Theta)
We found that the worst-case running time of insertion sort is T (n) = Θ(n2). Let us define what this
notation means. For a given function g(n), we denote by Θ(g(n)) the set of functions.
Θ(g(n)) = { f (n) : there exist positive constants c1, c2, and n0 such that 0 ≤ c1g(n) ≤ f (n) ≤ c2g(n) for all n ≥ n0}
O – notation(Big oh)
The Θ-notation asymptotically bounds a function from above and below. When we have only an
asymptotic upper bound, we use O-notation. For a given function g(n), we denote by O(g(n)) (pronounced
“big-oh of g of n” or sometimes just “oh of g of n”) the set of functions
O(g(n)) = { f (n) : there exist positive constants c and n0 such that 0 ≤ f (n) ≤ cg(n) for all n ≥ n0}
We use O-notation to give an upper bound on a function, to within a constant factor. Figure (b) shows
the intuition behind O-notation. For all values n to the right of n0, the value of the function f (n) is on or below
g(n). We write f (n) = O(g(n)) to indicate that a function f (n) is a member of the set O(g(n)). Note that
f (n) = Θ(g(n)) implies f (n) = O(g(n)), since Θ-notation is a stronger notion than O-notation. Written set-
theoretically, we have Θ(g(n)) ⊆ O(g(n)). Thus, our proof that any quadratic function an2 + bn + c, where a > 0,
is in Θ(n2) also shows that any quadratic function is in O(n2). What may be more surprising is that any linear
function an + b is in O(n2).
Using O-notation, we can often describe the running time of an algorithm merely by inspecting the
algorithm‟s overall structure. For example, the doubly nested loop structure of the insertion sort algorithm from
immediately yields an O(n2) upper bound on the worst-case running time: the cost of each iteration of the inner
loop is bounded from above by O(1) (constant), the indices I and j are both at most n, and the inner loop is
executed at most once for each of the n2 pairs of values for i and j.
Since O-notation describes an upper bound, when we use it to bound the worst-case running time of an
algorithm, we have a bound on the running time of the algorithm on every input. Thus, the O(n2) bound on
6 Prepared by: Mujeeb Rahman
worst-case running time of insertion sort also applies to its running time on every input. The Θ(n2) bound on the
worst-case running time of insertion sort, however, does not imply a Θ(n2) bound on the running time of
insertion sort on every input. For example, we saw that when the input is already sorted, insertion sort runs in
Θ(n) time.
Ω – notation(Omega)
Just as O-notation provides an asymptotic upper bound on a function, Ω-notation provides an
asymptotic lower bound. For a given function g(n), we denote by Ω(g(n)) (pronounced “big-omega of g of n”
or sometimes just “omega of g of n”) the set of functions
Ω(g(n)) = { f (n) : there exist positive constants c and n0 such that 0 ≤ cg(n) ≤ f (n) for all n ≥ n0}
The intuition behind Ω-notation is shown in Figure (c). For all values n to the right of n0, the value of f (n) is on
or above cg(n).
Theorem: For any two functions f (n) and g(n), we have f (n) = Θ(g(n)) if and only if f (n) = O(g(n)) and f (n) =
Ω(g(n)).
As an example of the application of this theorem, our proof that an2 + bn + c = _(n2) for any constants a, b, and
c, where a > 0, immediately implies that an2 + bn + c = Ω(n2) and an2 + bn + c = O(n2).
Since Ω-notation describes a lower bound, when we use it to bound the best-case running time of an
algorithm, by implication we also bound the running time of the algorithm on arbitrary inputs as well. For
example, the best-case running time of insertion sort is Ω(n), which implies that the running time of insertion
sort is Ω(n).
The running time of insertion sort therefore falls between Ω(n) and O(n2), since it falls anywhere
between a linear function of n and a quadratic function of n. Moreover, these bounds are asymptotically as tight
as possible: for instance, the running time of insertion sort is not Ω(n2), since there exists an input for which
insertion sort runs in Θ(n) time (e.g., when the input is already sorted). It is not contradictory, however, to say
that the worst-case running time of insertion sort is Ω(n2), since there exists an input that causes the algorithm to
take Ω(n2) time.
When we say that the running time of an algorithm is Ω(g(n)), we mean that no matter what particular
input of size n is chosen for each value of n, the running time on that input is at least a constant times g(n), for
sufficiently large n.
o-notation
The asymptotic upper bound provided by O-notation may or may not be asymptotically tight. The bound
2n = O(n2) is asymptotically tight, but the bound 2n = O(n2) is not. We use o-notation to denote an upper bound
2
that is not asymptotically tight. We formally define o(g(n)) (“little-oh of g of n”) as the set
o(g(n)) = { f (n) : for any positive constant c > 0, there exists a constant n0 > 0 such that 0 ≤ f (n) < cg(n) for all n ≥ n0}
ω(g(n)) = { f (n) : for any positive constant c > 0, there exists a constant n0 > 0 such that 0 ≤ cg(n) < f(n) for all n ≥ n0}
The operations of a stack imply that if the elements A,B,C,D,E are inserted into a stack, in that order,
then the first element to be removed/deleted must be E. Equivalently we say that the last element to be inserted
into the stack will be the first to be removed. For this reason stacks are sometimes referred to as Last In First Out
(LIFO) lists. The operations of a queue require that the first element which is inserted into the queue will be
the first one to be removed. Thus queues are known as First In First Out (FIFO) lists.
Trees
A tree is a finite set of one or more nodes such that (i) there is a specially designated node called the
root; (ii) the remaining nodes are partitioned into n ≥ 0 disjoint sets Tl, ... , Tn where each of these sets is a tree.
Tl, ... , Tn are called the subtrees of the root.
There are many terms which are often used when referring to trees. Consider the tree in Figure. This tree
has 13 nodes, each data item of a node being a single letter for convenience. The root contains A, (though we
will usually say node A), and we will normally draw trees with their root at the top.
The number of subtrees of a node is called its degree. The degree of A is 3, of C is 1, and of F is 0.
Nodes that have degree zero are called leaf or terminal nodes. The set {K, L, F, G, M, I, J} is the set of leaf
nodes of Figure. The other nodes are referred to as nonterminals. The roots of the subtrees of a node, X, are the
children of X. Xis the parent of its children. Thus the children of D are H, I, J; the parent of D is A.
Max Heap: A heap is known as Max Heap, if for every node „n‟(other than the root) relation between node „n‟
and its parent is the “smaller relation” (n ≤ parent (n)). In this maximum value is stored in the root.
Min Heap: A heap is known as Min Heap, if for every node „n‟(other than the root) the relation between node
„n‟ and its parent is the “greater relation” (n ≥ parent (n)). In this minimum value is stored in the root.
A heap can be stored as an array A.
Building a heap
We can use the procedure MAX-HEAPIFY in a bottom-up manner to convert an array A[1 . . n], where
n = length[A], into a max-heap.
We can compute a simple upper bound on the running time of BUILD-MAXHEAP as follows. Each call to
MAX-HEAPIFY costs O(lg n) time, and there are O(n) such calls. Thus, the running time is O(n lg n).
Eg:
Figure shows an example of the operation of heapsort after the max-heap is initially built. Each max-heap is
shown at the beginning of an iteration of the for loop of lines 2–5. The HEAPSORT procedure takes time
O(n lg n), since the call to BUILD-MAXHEAP takes time O(n) and each of the n − 1 calls to MAX-HEAPIFY
takes time O(lg n).
The MAX-HEAPIFY procedure, which runs in O(lg n) time, is the key to maintaining the max-heap
property.
The BUILD-MAX-HEAP procedure, which runs in linear time, produces a maxheap from an unordered
input array.
The HEAPSORT procedure, which runs in O(n lg n) time, sorts an array in place.
11 Prepared by: Mujeeb Rahman
Figure: The operation of HEAPSORT. (a) The max-heap data structure just after it has been built by BUILD-MAX-HEAP.
(b)–(j) The max-heap just after each call of MAX-HEAPIFY in line 5. The value of i at that time is shown. Only lightly
shaded nodes remain in the heap. (k) The resulting sorted array A.
***