How is the Big O of this function O(n^3)? - python

I am stuck on this Big O notation on how it is supposed to be O(n^3). Where did my thought process go wrong?
I know that a nested for loop is O(n^2) and that the while loop is probably a O(nlogn) function because the for loop is a O(n) function and the value for the while loop is being multiplied by two which makes it O(logn). That being said, the answer is stated to be O(n^3) and I'm confused how this came to be unless the recursive part of the function has something to do with it?
def do_stuff2(n, x=1.23):
if n <= 0:
return 0
val = 1
for i in range(n//2):
for j in range(n//4):
x += 2*x + j/2 + i*1.2
while val <= n:
for i in range(n):
x += val**2 + i//2
val *= 2
x += do_stuff2(n - 1, x/2)
return x
I believe that the x does not affect the big o notation because it is a constant because it is not used in deciding how many times any of the loops loop.
So again, I expected the output of the function to be O(n^2), but the actual output is O(n^3)

Your function has two nested for loops, that's O(n^2):
for i in range(n//2):
for j in range(n//4):
x += 2*x + j/2 + i*1.2
But on top of that, your do_stuff2() function takes an argument n and calls itself until n <= 0, meaning that's one more O(n).

Related

Is it possible to have a runtime of O(n) with just one while loop?

If there are no other loops inside of that while loop, is it possible to have O(n^2) runtime?
Oh, sure! Your runtime will be determined by how long it takes to meet your termination condition on the loop. If your program makes a small amount of progress on each iteration, it might well take worse than linear time. For example, consider the following bubble sort implementation:
to_sort = [1,5,2,3,7,6,4]
sorted = True
i = 0
while not sorted or i < len(to_sort):
if i == len(to_sort):
sorted = True
i = 0
if i < len(to_sort) - 1 and to_sort[i] > to_sort[i + 1]:
to_sort[i], to_sort[i + 1] = to_sort[i + 1], to_sort[i]
sorted = False
i += 1
print(to_sort)
You'll notice that I'm mistreating my i variable a little bit. That's because bubble sort is usually written as nested loops. But it's often possible to rewrite nested loops as one, more complicated, less readable loop, as I did here.
for i in range(n * n):
foo(i)
for i in range(2 ** n):
<look at bits of i>
Loops with runtime O(n^2) and O(2^n). You need to be a lot more specific about your question.

Confused about how nested for loops could be both O(n!) and O(n^2)

Let's say there is a simple nested for loop:
for i in range(0, n):
for j in range(0, n):
print(i*j)
This is very easily seen to be O(n^2) by pretty much everyone. Now if we modify the nested for loop:
for i in range(0, n):
for j in range(i, n):
print(i*j)
It's going to something along the lines of n x n-1 x n-2 ... x 1 right? This would be the same n!, which should be a horrendous upper bound. So what am I missing here? Why is the smaller version of the for loop, which is clearly skipping a couple of iterations of the loop resulting in a big o notation that is worse?
That calculation should be n + n-1 + n-2 ... + 1, which is O(n²).
for i in range(0, n):
for j in range(i, n):
print(i*j)
On the first iteration of the outer loop, the inner loop does n operations.
On the second one, n-1 operations.
On the third one, n-2 operations.
...and so forth, until the inner loop does only 1 iteration.
n + n-1 + n-2 + ... + 1 = O(n^2), where would the multiplication come from?
Note that in a pedantic sense, O(n^2) is also O(n!). That is, O(n!) includes functions that are O(n^2) (and then some).
I am also new. Due to my understanding, I think
for i in range(0,n) -> n times
for j in range(i,n) -> n-i times
Because there are just 2 loops which mean n * (n-i) for whole operations. It should be only O(n^2).

Time Complexity of recursive of function

Need help proving the time complexity of a recursive function.
Supposedly it's 2^n. I need to prove that this is the case.
def F(n):
if n == 0:
return 0
else:
result = 0
for i in range(n):
result += F(i)
return n*result+n`
Here's another version that does the same thing. The assignment said to use an array to store values in an attempt to reduce the time complexity so what I did was this
def F2(n,array):
if n < len(array):
answer = array[n]
elif n == 0:
answer = 0
array.append(answer)
else:
result = 0
for i in range(n):
result += F2(i,array)
answer = n*result+n
array.append(answer)
return answer
Again what I am looking for is the explanation of how to find the complexities of the two snippets of code, not interested in just knowing the answer.
All and any help greatly appreciated.
PS: for some reason, I can't get "def F2" to stay in the code block...sorry about that
Okay, the first function you wrote is an example of Exhaustive Search where you are exploring every possible branch that can be formed from a set of whole numbers up to n (which you have passed in the argument and you are using for loop for that). To explain you the time complexity I am going to consider the recursion stack as a Tree (to represent a recursive function call stack you can either use a stack or use an n-ary Tree)
Let's call you first function F1:
F1(3), now three branches will be formed for each number in the set S (set is the whole numbers up to n). I have taken n = 3, coz it will be easy for me to make the diagram for it. You can try will other larger numbers and observe the recursion call stack.
3
/| \
0 1 2 ----> the leftmost node is returns 0 coz (n==0) it's the base case
| /\
0 0 1
|
0 ----> returns 0
So here you have explored every possibility branches. If you try to write the recursive equation for the above problem then:
T(n) = 1; n is 0
= T(n-1) + T(n-2) + T(n-3) + ... + T(1); otherwise
Here,
T(n-1) = T(n-2) + T(n-3) + ... T(1).
So, T(n-1) + T(n-2) + T(n-3) + ... + T(1) = T(n-1) + T(n-1)
So, the Recursive equation becomes:
T(n) = 1; n is 0
= 2*T(n-1); otherwise
Now you can easily solve this recurrence relation (or use can use Masters theorem for the fast solution). You will get the time complexity as O(2^n).
Solving the recurrence relation:
T(n) = 2T(n-1)
= 2(2T(n-1-1) = 4T(n-2)
= 4(2T(n-3)
= 8T(n-3)
= 2^k T(n-k), for some integer `k` ----> equation 1
Now we are given the base case where n is 0, so let,
n-k = 0 , i.e. k = n;
Put k = n in equation 1,
T(n) = 2^n * T(n-n)
= 2^n * T(0)
= 2^n * 1; // as T(0) is 1
= 2^n
So, T.C = O(2^n)
So this is how you can get the time complexity for your first function. Next, if you observe the recursion Tree formed above (each node in the tree is a subproblem of the main problem), you will see that the nodes are repeating (i.e. the subproblems are repeating). So you have used a memory in your second function F2 to store the already computed value and whenever the sub-problems are occurring again (i.e. repeating subproblems) you are using the pre-computed value (this saves time for computing the sub-problems again and again). The approach is also known as Dynamic Programming.
Let's now see the second function, here you are returning answer. But, if you see your function you are building an array named as array in your program. The main time complexity goes there. Calculating its time complexity is simple because there is always just one level of recursion involved (or casually you can say no recursion involved) as every number i which is in range of number n is always going to be less than the number n, So the first if condition gets executed and control returns from there in F2. So each call can't go deeper than 2 level in the call stack.
So,
Time complexity of second function = time taken to build the array;
= 1 comparisions + 1 comparisions + 2 comparisions + ... + (n-1) comparisions
= 1 + 2 + 3 + ... + n-1
= O(n^2).
Let me give you a simple way to observe such recursions more deeply. You can print the recursion stack on the console and observe how the function calls are being made. Below I have written your code where I am printing the function calls.
Code:
def indent(n):
for i in xrange(n):
print ' '*i,
# second argument rec_cnt is just taken to print the indented function properly
def F(n, rec_cnt):
indent(rec_cnt)
print 'F(' + str(n) + ')'
if n == 0:
return 0
else:
result = 0
for i in range(n):
result += F(i, rec_cnt+1)
return n*result+n
# third argument is just taken to print the indented function properly
def F2(n, array, rec_cnt):
indent(rec_cnt)
print 'F2(' + str(n) + ')'
if n < len(array):
answer = array[n]
elif n == 0:
answer = 0
array.append(answer)
else:
result = 0
for i in range(n):
result += F2(i, array, rec_cnt+1)
answer = n*result+n
array.append(answer)
return answer
print F(4, 1)
lis = []
print F2(4, lis, 1)
Now observe the output:
F(4)
F(0)
F(1)
F(0)
F(2)
F(0)
F(1)
F(0)
F(3)
F(0)
F(1)
F(0)
F(2)
F(0)
F(1)
F(0)
96
F2(4)
F2(0)
F2(1)
F2(0)
F2(2)
F2(0)
F2(1)
F2(3)
F2(0)
F2(1)
F2(2)
96
In the first function call stack i.e. F1, you see that each call is explored up to 0, i.e. we are exploring each possible branch up to 0 (the base case), so, we call it Exhaustive Search.
In the second function call stack, you can see that the function calls are getting only two levels deep, i.e. they are using the pre-computed value to solve the repeated subproblems. Thus, it's time complexity is lesser than F1.

Problems measuring complexity in python's scripts

I have problems measuring complexity with python. Given the next two scripts:
1 def program1(L):
2 multiples = []
3 for x in L:
4 for y in L:
5 multiples.append(x*y)
6 return multiples
1 def program3(L1, L2):
2 intersection = []
3 for elt in L1:
4 if elt in L2:
5 intersection.append(elt)
6 return intersection
In the first one the best case (minimum steps to run the sript) is two considering an empty list L so are executed only the second and the sixth lines. The solution for the best case scenario: is 2.
In the worst case scenario L is a long list it goes through the loop for x in L n times.
The inner loop has three operations (assignment of a value to y, x*y, and list appending). So the inner loop executes 3*n times on each iteration of the outer loop. Thus the nested loop structure is executed n * (3*n + 1) = 3*n**2 + n times. Adding the second and the sixth line we get the solution 3n²+n+2.
But my question is: Where does comes from the number 1 in n(3n+1)?
According to me the solution is n(3n)+2 = 3n²+2 vs the right answer n(3n+1)+2 = 3n²+n+2.
Meanwhile in the second one the worst case scenario is n²+2n+2 but I don't understand why there is a quadratic term if there is only one loop.
According to you, there are three instructions in the innermost (y) loop of program1.
Assign to y.
Compute x*y.
Append to list.
By that same logic, there is one instruction in the outmost (x) loop:
Assign to x.
Perform inmost loop, see above.
That would make the outer loop:
n * (1 {assign to x} + n * 3 {assign, multiply, append})
Or:
n * (1 + 3n)
Adding the init/return instructions gives:
2 + n + 3n²
In program2, there is a similar situation with a "hidden loop":
2 instructions for init/return, plus ...
Then you run for elt in L1, which is going to be n iterations (n is size of L1). Your inner code is an if statement. In the worst case, the if body always runs. In the best case, it never runs.
The if condition is testing elt in L2, which is going to run an iterative function, type(L2).__contains__() on L2. The simple case will be an O(m) operation, where m is the length of L2. It is possible that L2 is not a list but some type where the in operation does not require a linear scan. For example, it might be a B-tree, or a dict, or a set, or who knows what? So you could assume that the best-case scenario is that elt in L2 is O(1) and the answer is no, while the worst-case is that elt in L2 is O(m) and the answer is yes.
Best case: 2 + n * (1 {assign to elt} + 1 {search L2})
Best case if L2 is a list: 2 + n * (1 {assign to elt} + m {search L2})
Worst case: 2 + n * (1 {assign to elt} + m {search L2} + 1 {append})
Which gives you 2 + 2n best case, 2 + n + nm best case if L2 is a list, and 2 + 2n + nm worst case.
You may be inclined to treat m as equal to n. That's your call, but if you're counting assignment statements, I'd argue against it.

How do I return the product of a while loop

I don't get the concept of loops yet. I got the following code:
x=0
while x < n:
x = x+1
print x
which prints 1,2,3,4,5.
That's fine, but how do I access the computation, that was done in the loop? e.g., how do I return the product of the loop( 5*4*3*2*1)?
Thanks.
Edit:
That was my final code:
def factorial(n):
result = 1
while n >= 1:
result = result *n
n=n-1
return result
You want to introduce one more variable (total) which contains accumulated value of a bunch of actions:
total = 1
x = 1
while x <= 5:
total *= x
x += 1
print x, total
print 'total:', total
Actually, more pythonic way:
total = 1
n = 5
for x in xrange(1, n + 1):
total *= x
print total
Note, that the initial value of total must be 1 and not 0 since in the latter case you will always receive 0 as a result (0*1*.. is always equals to 0).
By storing that product and returning that result:
def calculate_product(n):
product = 1
for x in range(n):
product *= x + 1
return product
Now we have a function that produces your calculation, and it returns the result:
print calculate_product(5)
A "one-liner"
>>> import operator
>>> reduce(operator.mul, xrange(1, n + 1))
120
>>>
Alternately you could use the yield keyword which will return the value from within the while loop. For instance:
def yeild_example():
current_answer = 1
for i in range(1,n+1):
current_answer *= i
yield current_answer
Which will lazily evaluate the answers for you. If you just want everything once this is probably the way to go, but if you know you want to store things then you should probably use return as in other answers, but this is nice for a lot of other applications.
This is called a generator function with the idea behind it being that it is a function that will "generate" answers when asked. In contrast to a standard function that will generate everything at once, this allows you to only perform calculations when you need to and will generally be more memory efficient, though performance is best evaluated on a case-by-case basis. As always.
**Edit: So this is not quite the question OP is asking, but I think it would be a good introduction into some of the really neat and flexible things about python.
use a for loop:
sum_ = 1
for i in range(1, 6):
sum_ *= i
print sum_
If you prefer to keep your while loop structure, you could do it like (there are 1000 +1 ways to do it ...):
x=1
result = 1
while x <= n:
x += 1
result *= x
Where result will store the factorial. You can then just return or print out result, or whatever you want to do with it.
to access the computation done in the loop, you must use counter(with useful and understandable name), where you will store the result of computation. After computation you just return or use the counter as the product of the loop.
sum_counter=0
x=0
while x < 10:
sum_counter +=x
x+=1
print sum_counter

Categories