Professional Documents
Culture Documents
Introduction
We all know that the running time of an algorithm increases (or remains constant in case of
constant running time) as the input size ([Math Processing Error]) increases. Sometimes even
if the size of the input is same, the running time varies among different instances of the input.
In that case, we perform best, average and worst-case analysis. The best case gives the
minimum time, the worst case running time gives the maximum time and average case
running time gives the time required on average to execute the algorithm. I will explain all
these concepts with the help of two examples - (i) Linear Search and (ii) Insertion sort.
Usually, the time required by an algorithm falls under three types
−
Best Case − Minimum time required for program execution.
It is usually harder to analyze the average behavior of an algorithm than to analyze its
behavior in the worst case. This is because it may not be apparent what constitutes an
“average” input for a particular problem. A useful analysis of the average behavior of an
algorithm, therefore, requires a prior knowledge of the distribution of the input instances
which is an unrealistic requirement. Therefore often we assume that all inputs of a given size
are equally likely and do the probabilistic analysis for the average case.
The above expression can be described as a function f(n) belongs to the set Θ(g(n)) if there
exist positive constants c1 and c2 such that it can be sandwiched between c1g(n) and c2g(n),
for sufficiently large n.
If a function f(n) lies anywhere in between c1g(n) and c2g(n) for all
n ≥ n0, then f(n) is said to be asymptotically tight bound.
Calculating the running time of Algorithms
Introduction
Here we learn how to estimate the running time of an algorithm looking at the source code
without running the code on the computer. The estimated running time helps us to find the
efficiency of the algorithm. Knowing the efficiency of the algorithm helps in the decision
making process. Even though there is no magic formula for analyzing the efficiency of an
algorithm as it is largely a matter of judgment, intuition, and experience, there are some
techniques that are often useful which we are going to discuss here.
The approach we follow is also called a theoretical approach. In this approach, we calculate
the cost (running time) of each individual programming construct and we combine all the
costs into a bigger cost to get the overall complexity of the algorithm.
Basic operations
Knowing the cost of basic operations helps to calculate the overall running time of an
algorithm. The table below shows the list of basic operations along with their running time.
The list not by any means provides the comprehensive list of all the operations. But I am
trying to include most of the operations that we come across frequently in programming.
Consecutive statements
Let two independent consecutive statements are P1 and P2. Let t1 be the cost of running P1
and t2 be the cost of running P2. The total cost of the program is the addition of cost of
individual statement i.e. t1+t2. In asymptotic notation the total time is Θ(max(t1,t2))(we
ignore the non significant term).
Example: Consider the following code.
1
2
3
4
5
int main()
{
// 1. some code with running time n
// 2. some code with running time n^2
return 0;
}
Assume that statement 2 is independent of statement 1 and statement 1 executes first
followed by statement 2. The total running time is
Θ(max(n,n2))=Θ(n2)
for loops
It is relatively easier to compute the running time of for loop than any other loops. All we
need to compute the running time is how many times the statement inside the loop body is
executed. Consider a simple for loop in C.
for (i = 0; i < 10; i++)
{
In this analysis, we made one important assumption. We assumed that the body of the loop
doesn’t depend on i. Sometimes the runtime of the body does depend on i. In that case, our
calculation becomes a little bit difficult. Consider an example shown below.
1
2
3
4
5
for (i = 0; i < n; i++)
{
if (i % 2 == 0)
{
// some operations of runtime n^2
}
}
In the for loop above, the control goes inside the if condition only when i is an even number.
That means the body of if condition gets executed n/2 times. The total cost is therefore
n/2∗n2=n3/2=Θ(n3).
while loops
while loops are usually harder to analyze than for loops because there is no obvious a priori
way to know how many times we shall have to go round the loop. One way of analyzing
while loops is to find a variable that goes increasing or decreasing until the terminating
condition is met. Consider an example given below
while (i > 0) {
// some computation of cost n
i=i/2
}
How many times the loop repeats? In every iteration, the value of i gets halved. If the initial
value of i is 16, after 4 iterations it becomes 1 and the loop terminates. The implies that the
loop repeats log2i times.
In each iteration, it does the n work. Therefore, the total cost is Θ(nlog2i).
Recursive calls
To calculate the cost of a recursive call, we first transform the recursive function to a
recurrence relation and then solve the recurrence relation to get the complexity. There are
many techniques to solve the recurrence relation. These techniques will be discussed in
details in the next article.
int fact(int n)
{
if (n <= 2)
{
return n;
}
b + T(n−1) otherwise
T(n)=b+T(n−1)
=b+b+T(n−2)
=b+b+b+T(n−3)
=3b+T(n−3)
=kb+T(n−k)
=nb+T(0)
=nb+a
=Θ(n)
Example
Let us put together all the techniques discussed above and compute the running time of some
example programs.
Example 1
int sum(int a, int b) {
int c = a + b;
return c
}
The sum function has two statements. The first statement (line 2) runs in constant time i.e.
Theta(1) and second statement (line 3) also runs in constant time Θ(1). These two statements
are consecutive statements, so the total running time is Θ(1)+Θ(1)=Θ(1)
Example 2
int array_sum(int a, int n)
Let us also confirm that the rules hold for finding parent of any node
Parent of 9 (position 2)
= (2-1)/2
=½
= 0.5
~ 0 index
=1
Parent of 12 (position 1)
= (1-1)/2
= 0 index
=1
Understanding this mapping of array indexes to tree positions is critical to understanding how
the Heap Data Structure works and how it is used to implement Heap Sort.
Root = array[0]
if(Root != Largest)
Swap(Root, Largest)
How to heapify root element when its subtrees are already max heaps
int largest = i;
int left = 2 * i + 1;
int right = 2 * i + 2;
largest = left;
largest = right;
if (largest != i)
swap(&arr[i], &arr[largest]);
heapify(arr, n, largest);
This function works for both the base case and for a tree of any size. We can thus move the
root element to the correct position to maintain the max-heap status for any tree size as long
as the sub-trees are max-heaps.
Build max-heap
To build a max-heap from any tree, we can thus start heapifying each sub-tree from the
bottom up and end up with a max-heap after the function is applied to all the elements
including the root element.
In the case of a complete tree, the first index of a non-leaf node is given by n/2 - 1. All other
nodes after that are leaf-nodes and thus don't need to be heapified.
So, we can build a maximum heap as
// Build heap (rearrange array)
heapify(arr, n, i);
swap(&arr[0], &arr[i]);
heapify(arr, i, 0);
1. Starting from the first index, compare the first and the second elements.
2. If the first element is greater than the second element, they are swapped.
3. Now, compare the second and the third elements. Swap them if they are not in order.
4. The above process goes on until the last element.
Compare two adjacent elements and swap them if the first element is greater than the next
element
2. Remaining Iteration
Continue the swapping and put the largest element among the unsorted list at the end
In each iteration, the comparison takes place up to the last unsorted element.
Swapping occurs only if the first element is greater than the next element
The array is sorted when all the unsorted elements are placed at their correct positions.
Complexity in Detail
Bubble Sort compares the adjacent elements.
Cycle Number of Comparisons
1st (n-1)
1. The first element in the array is assumed to be sorted. Take the second element and
store it separately in key.
Compare the key with the first element. If the first element is greater than key, then
key is placed in front of the first element.
2. Compare minimum with the second element. If the second element is smaller than
minimum, assign the second element as minimum.
3. After each iteration, minimum is placed in the front of the unsorted list.
4. For each iteration, indexing starts from the first unsorted element. Step 1 to 3 are
repeated until all the elements are placed at their correct positions.
Counting sort
Complexity
Worst case time O(n)
Best case time O(n)
Average case time O(n)
Space O(n)
Strengths:
Linear time. Counting sort runs in O(n)O(n) time, making it asymptotically faster than
comparison-based sorting algorithms like quicksort or merge sort.
Weaknesses:
Restricted inputs. Counting sort only works when the range of potential items in the input is
known ahead of time.
Space cost. If the range of potential values is big, then counting sort requires a lot of space
(perhaps more than O(n)O(n)).
Counting sort works by iterating through the input, counting the number of times each item
occurs, and using those counts to compute an item's index in the final, sorted array.
Counting How Many Times Each Item Occurs
Say we have this array:
Unsorted input: [4, 8, 4, 2, 9, 9, 6, 2, 9].
And say we know all the numbers in our array will be whole numbers ↴ between 0 and 10
(inclusive).
The idea is to count how many 0's we see, how many 1's we see, and so on. Since there are 11
possible values, we'll use an array with 11 counters, all initialized to 0.
The first two elements in the input [4, 8, 4, 2, ...] are 4 and 8. To count them, we increment
the value at indices 4 and 8 in our counts list, which becomes [0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0].
And so on. When we reach the end, we'll have the total counts for each number:
Once we count all the values in [4, 8, 4, 2, 9, 9, 6, 2, 9], the counts list is [0, 0, 2, 0, 2, 0, 1, 0,
1, 3, 0].
one 8,
Create an array of size 10. Each slot of this array is used as a bucket for storing elements.
2. Insert elements into the buckets from the array. The elements are inserted according to the
range of the bucket.
In our example code, we have buckets each of ranges from 0 to 1, 1 to 2, 2 to 3,...... (n-1) to
n. Suppose, an input element is .23 is taken. It is multiplied by size = 10 (ie. .23*10=2.3).
Then, it is converted into an integer (ie. 2.3≈2). Finally, .23 is inserted into bucket-2.
3. The elements of each bucket are sorted using any of the stable sorting algorithms. Here, we
have used quicksort (inbuilt function).
bucketSort()
create N buckets each of which can hold a range of values
for all the buckets
initialize each bucket with 0 values
for all the buckets
put elements into buckets matching the range
for all the buckets
sort elements in each bucket
gather elements from each bucket
end bucketSort