Time Complexity
Introduction
Time complexity of a program shows the order of growth of the running time as the input gets larger and larger.
It tells nothing about the absolute/actual running time of the algorithm. So, it is not always a good idea to compare two algorithms simply based on their time complexities. For example, MergeSort
runs in whereas InsertionSort
runs in . But in practice, for smaller size arrays, InsertionSort
often runs faster than MergeSort
!!
Since constants are omitted while dealing with time complexity of algorithms, two programs may have the same complexity but very different running times. For example, consider program A with and program B with . Obviously program B will run faster than program A, but this is not evident if you only look at their orders of growth.
Useful Mathematical Facts
It is useful to know the following properties of exponents and logarithms:
→ definition of logarithm
→ product property
→ quotient property
→ power property
→ change of base property
→ number raised to log
(when ) → can take on both sides to verify its true
The above arithmetic progression can appear in various different forms. The more general form is:
Given an arithmetic series where is the last term of the series and is the number of terms.
More generally, the solution to the geometric progression
The above series (called the harmonic series) is divergent but the sum of terms is upper bounded by . This is because:
It is easy to view the bound in terms of the graphs.
The following result is also very useful (e.g. for deriving the time complexity of heapify
)
Common Recurrence Relations
In general,
A common example of an algorithm whose running time has the above recurrence relation is MergeSort
In general,
where
Proof
Let . Then, .
It is clear from the pattern that that the running time doubles when n is increased by 1.
Hence, the solution of this recurrence is given by
Note that the solution depends on the exact value of and cannot be determined simply by knowing that because when it comes to algorithmic time complexity, the base of the exponent matters!
An example of an algorithm that follows the above recurrence relation is BinarySearch
(here, c = 2).
Q. Solve the following recurrence relation:
Some Fun Time Complexity Analysis Questions
What is the order of growth of a program which has running time
Answer: There is no bound! Because, as approaches 7, grows without any bound. Mathematically,
What is the order of growth of the following code?
Answer:
It is easy to observe that the total running time is directly proportional to the number of times “hello” is printed (i.e., the number of times the inner loop runs). This can be analysed as follows
Note that even though the outer loop runs n times, since the value of n2
falls below 1 in less than n iterations, in the last few iterations of the outerloop, the inner loop does not run at all!
This is also a good example to not just assume because there is a nested for loop or assume since at each stage, n2 is reduced by a fraction. Don’t jump to conclusions!
is the running time of a divide-and-conquer algorithm that divides the input of size into equal parts and recurses on all of them. It uses work in dividing/recombining on all of them (and there’s no other cost, i.e., no other work done). The base case for the recursion is when the input is less than size 20, which costs . What is the order of growth of ?
Ans: (Read the question carefully! IT IS NOT )
What is the running time of the following code, as a function of :
Ans:
It is obvious that the nested for loops run in time. The slightly more interesting part is the recursive call. If is even, recursiveloopy(n+1)
is called to make it odd. If is odd, recursiveloopy(n-2)
is called (and remains odd). So, recursiveloopy(n+1)
is called at most 1 time. All other times, we can safely assume that recursiveloopy(n-2)
is called. Now, the recurrence relation would be (Since we can ignore the one time that recursiveloopy(n+1)
is called, if ever). Solving the recurrence relation,
It is clear that the sum of all even squares and sum of all odd squares should contribute nearly equally to the sum of squares of all integers from 1 through n. That is, we are estimating that the sum of squares of odd integers from 1 to n would be roughly , which would still be order
What is the running time of the following code, as a function of :
Ans: This is a classic example of 2 nested for loops that produce order when the inner loop variable grows by a constant factor. The detailed analysis is as follows:
During the iteration of the outer for loop, the inner loop runs for about iterations. So,
What is the running time of the following code, as a function of :
Ans: . This appears to be at first glance since there is only 1 for loop that runs exactly n times. However, string concatenation takes time, where is the length of the string. During the iteration of the loop, the string is characters long. So, adding all of them up gives us
What is the asymptotic running time of the following code, as a function of when you execute loopyloop(n,n)
?
Ans: . By the time b
equals 1, there have been recursive calls made to loopyloop
and so, the value of a
would have reduced to . So, the for
loop runs for times. This is still .
What is the asymptotic running time of the following code, as a function of when you execute doubleTwist(n,n)
?
Ans: Let the running time of doubleTwist
be and that of twist
be . Then, and . We can draw out the recursive tree or we can solve the recurrence relation rigorously. Either way the answer is .
Find the running time of the following program as a function of n (Assume that isDivisible(x,y)
takes to run):
Ans:
Let us consider the recurrence relation of the function.
This is because the for loop runs for times (and each call of isDivisible
is ). So, excluding the recursive call, the time complexity of should be . But it also makes recursive calls in every loop - so we add those too.
Note that this is not a trivial recurrence relation to solve.
Big-O Notation Questions
Is ?
Ans: No! (The base of the exponent matters!)
Is ?
Ans: Yes!! Because they differ only by a constant ()
What is the best (tightest) asymptotic upper bound for the following?
Ans: . Because, (obviously we assume that the base of log is 2 since we are computer scientists :) )
Ans: . Only is a constant. All other terms are being multiplied (not added!) and hence, cannot be ignored as being insignificant. In particular, the answer cannot be or since you are forgetting to multiply a factor of which is essentially , not a constant!
Amortized Analysis
It is a common technique for analyzing “average” cost per operation. We use this when most operations are cheap but once in a while, we need to pay a lot. This is similar to how you pay rent: you don’t pay rent every second or every day (although you could technically). So, you can think of it like you’re living for free on 29 days of the month and on 1 day you need to pay a large amount. But of course this does not give the true picture - so you find the cost you’re paying per day.
Similarly, we use amortized analysis for data structures.
An operation is said to have amortized cost if, for every integer , the cost of operations is
When we say operations, we mean continuous operations, starting from the first operation. You cannot pick any random operations from the middle and say that the amortized cost is so high.
In other words, for every prefix sequence, the total cost of that sequence of operations cannot exceed times the length of that prefix sequence.
Amortized ≠ Average! Amortized is a much stronger constraint than average since it needs to hold for every value of . In case of average, the total cost of all operations should not exceed times the total number of operations.
Example (hash table): Inserting elements into a hash table takes time . Therefore, the insert operation has amortized cost .
Accounting Method
Imagine a bank account . Each operation performed adds money to the bank acconut. Every step of the algorithm spends money:
Immediate money: to perform that particular operation
Deferred operation: from the bank account
Total cost of execution = total money (Average time per operation = total money divided by number of operations)
For each operation, you are given time. For most operations you need less than that. So, you’re saving the time when performing cheap operations and using that saved time to perform expensive operations (that take ) once in a while.
Binary Counter Amortized Analysis
Binary counter ADT is a data structure that counts in base two, i.e. 0s and 1s. Binary Counter ADT supports two operations:
increment() increases the counter by 1.
read() reads the current value of the binary counter
To make it clearer, suppose that we have a k-bit binary counter. Each bit is stored in an array A
of size , where A[k]
denotes the k-th bit (0-th bit denotes the most significant bit). For example, suppose A = [0, 1, 1, 0]
, which corresponds to the number in binary. Calling increment()
will yield A = [0, 1, 1, 1]
, i.e. . Calling increment()
again will yield A = [1, 0, 0, 0]
, the number in binary.
Suppose that the k-bit binary counter starts at 0, i.e. all the values in A is 0. A loose bound on the time complexity if increment()
is called times is (since each operation flips at most bits). What is the amortized time complexity of increment() operation if we call increment()
n times?
Let us look at a few operations to gain some insight into the amortized cost (the cost is essentially just the number of bits we need to flip):
(increment
index)
Binary Counter
Cost of operation
Total cost of operations
Total cost/
1
[0,0,0,0,1]
1
1
1
2
[0,0,0,1,0]
2
3
1.5
3
[0,0,0,1,1]
1
4
1.3
4
[0,0,1,0,0]
3
7
1.75
5
[0,0,1,0,1]
1
8
1.6
6
[0,0,1,1,0]
2
10
1.67
7
[0,0,1,1,1]
1
11
8
[0,1,0,0,1]
4
15
So, for each operation, if we assign a cost of , the “bank balance” will never be negative and we can pay for expensive operations using previously saved money.
We can solve this problem using the Accounting Method – having a “bank” of “time saved up” which pays for expensive operations. We know to use amortized analysis on this question because increment takes a variable number of flips (read: some operations are expensive and others are cheap), and we are looking for a tighter bound than where is the number of times increment is called and is the total number of digits.
We can also observe that in order for a digit to be flipped to 0, it must have been flipped to 1 first. By the accounting method, we want to “save” up for the more expensive operations, which is when many 1s have to be flipped to 0s. We propose that each flip of a bit takes 2 units of time - 1 unit to flip from 0 to 1, and 1 unit to be saved in the bank for when the bit must be flipped from 1 to 0 in the future. This has the invariant that the amount in the bank never dips below 0 (as when you flip the bit from 0 to 1, you pay in advance for the future flip back to 0). Hence the amortised bound is 2 flips per increment. Increment is in amortized time per increment and increments is bounded by .
Observe that the 0th bit flips times when increment is called times. The 1st bit flips times when increment is called times. The 2nd bit flips times when increment is called times. And so on. Also observe that in a sequence of operations, the total number of flips is . So, the total cost of operations is (Notice that this is true for any value of by the nature of how binary counting works)
Stack 2 Queue
It is possible to implement a queue using two stacks. But is it really efficient? (a) Design an algorithm to push and pop an element from the queue. (b) Determine the worst case and amortized runtime for each operation
(a) Let’s call the two stacks as and . When we push an element, we push it to stack . When we want to pop an element, note that the first element pushed in should be popped first. We should pop the elements from one by one and push it back at the same time to S2. Note that the ordering will be reversed in , hence when we pop from , it will be the first element pushed in . Therefore, when pop operation is called, we pop the element from . If is empty, then we transfer the elements in to .
(b) The worst case for one operation is , where n is the number of inserted elements. However, the amortized cost is much smaller than that. We will prove that the amortized cost for each operation is using Accounting Method. Whenever we push a new element, we will deposit $2 to the bank. When we transfer an element from to or popping an element from , we will pay $1. Note that when pop is called and is empty, we have at least $2k deposited in the bank, where k is the number of inserted elements in . This is enough money to pay for the transferring cost that takes time. Note that the remaining money $2k − $k = $k can be used to pay for when the element is popped from .
Clarification
There is a very important distinction between worst-case analysis, big-O notation, average-case analysis, expected time analysis, big-theta notation, etc.
Worst case analysis deals with the worst possible kind of input to a program. We carefully handpick the worst possible input by analysing how the algorithm works. When we say that insertion sort takes in the worst case, we mean that the algorithm running time grows quadratically as the array size increases and the worst possible input (e.g. reversed array) is fed to the program. It is not necessary to use while talking about worst-case analysis.
simply represents a family of functions that grow slower than for some positive constant , i.e., . We often abuse notation and for convenience, simply write that to mean that the running time is upper bounded by . BUT this does not give us any information regarding what type of running time we are dealing with or what scenario we are analysing.
Average-case analysis deals with how the program performs when a random input is fed to the algorithm. It is important to note that here, while the input is random, the program may still be deterministic.
An indeterministic program is one which makes random choices while the algorithm is running. When talking about indeterministic programs, the running time is a random variable (it depends on the random choice made by the algorithm). In fact, even for the same input, the running time may vary. For example, in case of QuickSort
the running time depends heavily on the choice of a pivot. So, it is more practical to talk about the expected running time of a randomised algorithm.
In short, big-O and big-theta notations only give us bounds of a function. They do not tell us anything about the scenario for which the function is being considered.
In CS2040S, we normally consider the worst-case scenario unless otherwise specified.
Last updated