The time complexity of a for loop with n as the input is O(n) from what I've understood till now but what about the code inside the loop?
while var in arr:
arr.remove(var)
arr is a list with n elements and var can be a string or a number.
How do I know if I should multiply or add time complexities? Is the time complexity of the above code O(n**2) or O(n)?
for i in range(n):
arr.remove(var)
arr.remove(var1)
What would the time complexity be now? What should I add or multiply?
I tried learning about time complexity but couldn't understand how to deal with code having more than one time complexity.
You need to know the time complexity of the content inside the loop.
for i in arr: # O(n)
print(sum(arr) - i) # O(n)
In this case, the .pop(0) is nested in the forloop, so you need to multiply the complexity to the forloop complexity: O(n) * O(n) > O(n*n) > O(n²).
for i in arr: # O(n)
print(sum(arr) - i) # O(n)
print(sum(arr) - i) # O(n)
In this case, it's
O(n) * (O(n) + O(n))
O(n) * O(n+n)
O(n) * O(2n)
O(n) * O(n)
O(n*n)
O(n²)
See When to add and when to multiply to find time complexity for more information about that.
For a while loop, it doesn't change anything: multiply content with the complexity of the while.
Related
This question is general, but also has a function in question:
def quick_sort(lst):
if len(lst) < 2: return lst
pivot_lst = lst[0]
left_side = [el for el in lst[1:] if el < pivot_lst]
right_side = [el for el in lst[1:] if el >= pivot_lst]
return quick_sort(left_side) + [pivot_lst] + quick_sort(right_side)
Time complexity: O(nlog(n)) expected, O(n^2) worst case
Space complexity: ???
So for the expected time complexity, which the best case of would be when left and right are split evenly the following series would apply for n size input:
n + n/2 + n/4 + n/8 +... +1
= n(1 + 1/2 + 1/4 + 1/8 + 1/16 + 1/32 + ... . )
= O(n)
It follows that in the worst case, which occurs when the pivot point selected is the largest or smallest value in the list, this would apply:
n + (n-1) + (n-2) +... + 1
= (n^2 + n) / 2
= O(n^2)
My question is, do the series' above represent expected and worst space complexities of O(n) and O(n^2), respectively?
I'm struggling with the idea of how stack frame memory comes into play here.
Would we just add it on?
So, if its O(log(n)), then space complexity is O(n) + O(log(n)) -> O(n)
Or would its relationship with the auxiliary data be something else?
Can I conclude that when both an auxiliary data structure and recursive stack are present, we only need to calculate the larger of the two?
Summary
In this implementation of Quicksort, yes—the expected auxiliary space complexity is O(n) and the worst-case auxiliary space complexity is O(n^2).
I'm struggling with the idea of how stack frame memory comes into play here. Would we just add it on?
So, if its O(log(n)), then space complexity is O(n) + O(log(n)) -> O(n)
[...]
Can I conclude that when both an auxiliary data structure and recursive stack are present, we only need to calculate the larger of the two?
No.
I think you're correctly noticing that the recursive stack depth is O(log(n)) in the expected case, but incorrectly thinking that that means its space complexity is also O(log(n)) in the expected case. That's not necessarily true.
An individual stack frame can represent more space than O(1).
How much space a frame represents might vary from frame to frame.
So, when finding an algorithm's total space complexity, you can't analyze its recursion depth separately from its data requirements, and then add the two up at the end. You need to analyze them together.
In general, you'll need to understand:
How deep the recursion goes—how many stack frames there will be.
For each of those stack frames, what its space complexity is. This includes function arguments, local variables, and so on.
Then, you can add up the space complexities of all the stack frames that will be simultaneously active.
Example: Expected case
Imagine this function call tree for n=8. I'm using the notation quick_sort(n) to mean "quicksort with a list of n elements."
quick_sort(8)
quick_sort(4)
quick_sort(2)
quick_sort(1)
quick_sort(1)
quick_sort(2)
quick_sort(1)
quick_sort(1)
quick_sort(4)
quick_sort(2)
quick_sort(1)
quick_sort(1)
quick_sort(2)
quick_sort(1)
quick_sort(1)
Since your implementation is single-threaded, only one branch will be active at a time. At its deepest, that will look like:
quick_sort(8)
quick_sort(4)
quick_sort(2)
quick_sort(1)
Or, in general:
quick_sort(n)
quick_sort(n/2)
quick_sort(n/4)
...
quick_sort(1)
Let's look at the space that each frame will consume.
<calling function>
lst: O(n)
quick_sort(n)
lst: O(1)
pivot_lst: O(1)
left_side: O(n/2)
right_side: O(n/2)
quick_sort(n/2)
lst: O(1)
pivot_lst: O(1)
left_side: O(n/4)
right_side: O(n/4)
quick_sort(n/4)
lst: O(1)
pivot_lst: O(1)
left_side: O(n/8)
right_side: O(n/8)
...
quick_sort(1)
lst: O(1)
Note that I'm considering the lst argument to always have a space complexity of O(1) to reflect Python lists being pass-by-reference. If we made it O(n), O(n/2), etc., we would be double-counting it, because it's really the same object as the calling function's left_side or right_side. This won't end up mattering for the final result of this particular algorithm, but you'll need to keep it in mind, in general.
I'm also being notationally sloppy. Writing O(n/2) makes it tempting to immediately simplify it to O(n). Don't do that yet: if you do, you'll end up overstating the total space complexity.
Simplifying a bit:
<calling function>
lst: O(n)
quick_sort(n)
everything: O(n/2)
quick_sort(n/2)
everything: O(n/4)
quick_sort(n/4)
everything: O(n/8)
...
quick_sort(1)
everything: O(1)
Adding them up:
O(n) + O(n/2) + O(n/4) + O(n/8) + ... + O(1)
= O(n)
Example: Worst case
Using the same methodology as above, but skipping some steps for brevity:
<calling function>
lst: O(n)
quick_sort(n)
everything: O(n-1)
quick_sort(n-1)
everything: O(n-2)
quick_sort(n-2)
everything: O(n-3)
...
quick_sort(1)
everything: O(1)
O(n) + O(n-1) + O(n-2) + O(n-3) + ... + O(1)
= O(n^2)
I'm farily new to time complexity. I'm looking for the time complexity of this code
def func(arg):
list= []
for i in range(len(arg):
list.append(arg.count(i)
return list
I know that the loop would make it O(n), but then count is also O(n) in python, would that make this function O(n) or O(n2)?
You have a loop within a loop:
for i in range(len(arg)): # outer loop => O(n)
arg.count(i) # inner loop hidden inside a function => O(n)
So that's O(n^2).
If you wanted two loops that sum to O(n), you'd need something like this:
for x in range(N): # O(N)
... # do stuff
for y in range(N): # O(N)
... # do other stuff
The overall complexity will be the sum of the loops' complexities, so
O(N) + O(N) = O(2 * N) ~= O(N)
O(n^2).
The outer loop executes n times the inner statement(which is O(n)) so we get quadratic complexity.
Let's say we have the following code.
def problem(n):
list = []
for i in range(n):
list.append(i)
length = len(list)
return list
The program has time complexity of O(n) if we don't calculate len(list). But if we do, will the time complexity be O(n * log(n)) or O(n^2)? .
No, the len() function has constant time in python and it is not dependent on the length of the element, your time complexity for the above code would remain O(N) governed by your for i in range(n) loop. Here is the time complexity for many CPython functions, like len()! (Get Length in table)
def myFunction(mylist):
n = len(mylist)
p = []
sum = 0
for x in mylist:
if n > 100:
sum = sum + x
else:
for y in mylist:
p.append(y)
My thought process was that if the else statement were to be executed, the operations within are O(n) because the number of times through depends on the length of the list. Similarly, I understood the first loop to be O(n) as well thus making the entire worst-case complexity O(n^2).
Apparently the correct answer is O(n). Any explanation would be greatly appreciated :)
Just to add a bit, we typically think of Big-O complexity being in the case where n gets large. Thus, as n gets large, we won't execute the second statement. Thus it would just be O(n)
I have a question about iterating through a list in python.
Let's say I have lists A = [1, 2, 3, 4] and B = []. What is the difference (if any) between using these two cycles? I'm intrested in the time complexity.
for i in range(len(A)):
B.append(A[i])
for i in A:
B.append(i)
The time-complexity is identical for both of those operations loops.
Think about it this way:
How many iterations will they have to do?
They'll both have to do len(A) number of loops. So therefore, they will take the same length of time.
Another way that this may be written is O(n). This is an example of Big-O-Notation and just means that the time-complexity is linear - i.e both of the operations will take the same amount of time longer if the list goes from being length 5 --> 10 as it would if the list went from being length 1000 --> 1005.
--
The other time-complexities can be seen clearly in the following grap which was stolen from this great explanation in another answer:
According to this question/answer, len(A) has a time-complexity of O(1), so it doesn't increase the complexity of the first loop that you mentioned. Both possibilities have to do n cycles, where n is the length of A.
All in all, both possibilities have a time-complexity of O(n).
Each loop is O(n), or linear time:
for i in range(len(A)):
B.append(A[i])
for i in A:
B.append(i)
Each append operation is O(1), and the indexing occurring at B.append(A[i]) is also O(1). Thus, the overall time complexity for this code block is:
T(N) = O(n) + O(n) = 2*O(n) => O(n)
since Big - O measures worst case.