Why this answer passes Leetcode Q41 - python

Question here: https://leetcode.com/problems/first-missing-positive/description/
Your algorithm should run in O(n) time and uses constant extra space.
I have a very naive solution that passes, since the question is marked as hard and most people's solution in discussion is much more complicated.
def firstMissingPositive(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
if nums == []:
return 1
for i in range(1, max(nums)+2):
if i not in nums:
return i
find max uses O(n), since loop would stop once found the missing positive it would be O(n). range in py3 returns an iterable, every loop of the for statement produces the next number on the fly. So the time complexity should be O(n)
space complexity is O(1) since only i is created
I suppose the OJ only checks the correctness but not the space/ time complexity. However I cant see how this solution is wrong. Could anyone point it out?

Explicit loop for i in range(1, max(nums)+2): with nested implicit loop if i not in nums: is not O(n) ;)

You have two loops inside each other. You have a iterating from 1 to max(nums)+2 and inside that if i not in nums: which iterates over nums. so your complexity will be something like O(n^2).

Related

Time complexity of python two-sum solution

I have a question about the a widely accepted solution to the classic leetcode two-sum question, which reads as follows:
Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target.
You may assume that each input would have exactly one solution, and you may not use the same element twice.
The solution I'm referring to iterates over nums and stores a mapping of values in nums to their indices. With each iteration over nums, we check if there is a complementary key to the current element in nums which would give us target, and if so we lookup and return its value:
def twoSum(self, nums: List[int], target: int) -> List[int]:
lookup = {}
for i in range(0, len(nums)):
if target - nums[i] in lookup:
return [i, lookup[target - nums[i]]]
lookup[nums[i]] = i
Everyone seems to think this is an O(n) solution. However, I understand it to be O(n^2). For each iteration i, we're also iterating over potentially n-1 keys in the lookup table when we do nums[i] in lookup. Am I missing something?
As lookup is a dictionary, it doesn't linearly search through its keys when looking up a value. It uses a mathematical function (called a hash) to calculate the value's location using the input key. This is an O(1) operation, meaning that checking if nums[i] in lookup is also an O(1) operation.
If lookup were a list, then you'd be right in saying the algorithm was O(n²)
You can google "Python how do dictionaries work" or something similar to find out more. I found this as the first result :)

Does one for loop mean a time complexity of n in this case?

So, I've run into this problem in the daily coding problem challenge, and I've devised two solutions. However, I am unsure if one is better than the other in terms of time complexity (Big O).
# Given a list of numbers and a number k,
# return whether any two numbers from the list add up to k.
#
# For example, given [10, 15, 3, 7] and k of 17, return true since 10 + 7 is 17.
#
# Bonus: Can you do this in one pass?
# The above part seemed to denote this can be done in O(n).
def can_get_value(lst=[11, 15, 3, 7], k=17):
for x in lst:
for y in lst:
if x+y == k:
return True
return False
def optimized_can_get_value(lst=[10, 15, 3, 7], k=17):
temp = lst
for x in lst:
if k-x in temp:
return True
else:
return False
def main():
print(can_get_value())
print(optimized_can_get_value())
if __name__ == "__main__":
main()
I think the second is better than the first since it has one for loop, but I'm not sure if it is O(n), since I'm still running through two lists. Another solution I had in mind that was apparently a O(n) solution was using the python equivalent of "Java HashSets". Would appreciate confirmation, and explanation of why/why not it is O(n).
The first solution can_get_value() is textbook O(n^2). You know this.
The second solution is as well. This is because elm in list has O(n) complexity, and you're executing it n times. O(n) * O(n) = O(n^2).
The O(n) solution here is to convert from a list into a set (or, well, any type of hash table - dict would work too). The following code runs through the list exactly twice, which is O(n):
def can_get_value(lst, k):
st = set(lst) # make a hashtable (set) where each key is the same as its value
for x in st: # this executes n times --> O(n)
if k-x in st: # unlike for lists, `in` is O(1) for hashtables
return True
return False
This is thus O(n) * O(1) = O(n) in most cases.
In order to analyze the asymptotic runtime of your code, you need to know the runtime of each of the functions which you call as well. We generally think of arithmetic expressions like addition as being constant time (O(1)), so your first function has two for loops over n elements and the loop body only takes constant time, coming out to O(n * n * 1) = O(n^2).
The second function has only one for loop, but checking membership for a list is an O(n) function in the length of the list, so you still have O(n * n) = O(n^2). The latter option may still be faster (Python probably has optimized code for checking list membership), but it won't be asymptotically faster (the runtime still increases quadratically in n).
EDIT - as #Mark_Meyer pointed out, your second function is actually O(1) because there's a bug in it; sorry, I skimmed it and didn't notice. This answer assumes a corrected version of the second function like
def optimized_can_get_value(lst, k=17):
for x in lst:
if k - x in lst:
return True
return False
(Note - don't have a default value for you function which is mutable. See this SO question for the troubles that can bring. I also removed the temporary list because there's no need for that; it was just pointing to the same list object anyway.)
EDIT 2: for fun, here are a couple of O(n) solutions to this (both use that checking containment for a set is O(1)).
A one-liner which still stops as soon as a solution is found:
def get_value_one_liner(lst, k):
return any(k - x in set(lst) for x in lst)
EDIT 3: I think this is actually O(n^2) because we call set(lst) for each x. Using Python 3.8's assignment expressions could, I think, give us a one-liner that is still efficient. Does anybody have a good Python <3.8 one-liner?
And a version which tries not to do extra work by building up a set as it goes (not sure if this is actually faster in practice than creating the whole set at the start; it probably depends on the actual input data):
def get_value_early_stop(lst, k):
values = set()
for x in lst:
if x in values:
return True
values.add(k - x)
return False

What is the space complexity of the following algorithm?

The following algorithm finds the largest element of a list using recursion.
def largest(s):
if len(s) == 0:
return 'List can\'t be empty'
elif len(s) == 1:
return s[0]
elif s[0] <= s[1]:
return largest(s[1:])
else:
s.remove(s[1])
return largest(s)
The time complexity is O(n) because we are making total of n calls to the function largest and each call does O(1) operations.
I am having trouble figuring out the space complexity. I think it's O(n) but I am not sure.
First of all, the time complexity is not O(n) because the list.remove operation is not O(1), but O(n).
So your time complexity would be O(n^2) - Imagine applying largest over this array [5 4 3 2 1]
You can see here a list of python operation complexity.
The space complexity is O(n^2) because when you are doing return largest(s[1:]) you are copying the list, not getting a reference, so you're keeping all the intermediate cuts of the list. Doing s.remove(s[0]) and then return largest(s) will give you O(n) space complexity because you're working with references.
Slicing a standard list does create a (shallow!) copy of the slice. You're correct about this making it O(n). (In additional memory allocated; not counting the list itself, which is of course already in memory.)
As Reut points out in the comments, this is an implementation detail of the Python interpreter, but I couldn't say for sure whether any interpreters handle slices differently. Any implementation that does create a slice without copying would have to use copy-on-write instead.

What is the time complexity in my single number code

The question is "Given an array of integers, every element appears twice except for one. Find that single one.
Note:
Your algorithm should have a linear runtime complexity. Could you implement it without using extra memory?"
My code is below:
def singleNumber(nums):
for i in range(len(nums)):
if nums.count(nums[i]) == 1:
return nums[i]
Why my code is not O(N)? did it calculate by for loop, which takes n rounds?
Thanks.
We can do this using an xor command:
a = [0,0,1,1,2,2,3,3,4,5,5,6,6,7,7,8,8,9,9]
ans = 0
for i in a:
ans = i^ans
ans
4
This works, as the xor is effectively doing (1^1)^(2^2)^4^(5^5), and we cancel all the doubles out.
You code runs in O(N^2) because the count method runs in O(N) and it is executed inside of a for loop. Which in total gives you O(N^2).
If you want to make it run in O(N).
You can do the following:
Loop through the array and set its values as keys to a dict and the number of times it appears in the list as it key's value.
Then iterate through the key-value pairs of the dict again and look for the key whose value is 1. This is will give O(2*N) which is O(N) time complexity.

Python Non-recursive Permutation

Does anyone understand the following iterative algorithm for producing all permutations of a list of numbers?
I do not understand the logic within the while len(stack) loop. Can someone please explain how it works?
# Non-Recursion
#param nums: A list of Integers.
#return: A list of permutations.
def permute(self, nums):
if nums is None:
return []
nums = sorted(nums)
permutation = []
stack = [-1]
permutations = []
while len(stack):
index = stack.pop()
index += 1
while index < len(nums):
if nums[index] not in permutation:
break
index += 1
else:
if len(permutation):
permutation.pop()
continue
stack.append(index)
stack.append(-1)
permutation.append(nums[index])
if len(permutation) == len(nums):
permutations.append(list(permutation))
return permutations
I'm just trying to understand the code above.
As mentioned in the comments section to your question, debugging may provide a helpful way to understand what the code does. However, let me provide a high-level perspective of what your code does.
First of all, although there are no recursive calls to the function permute, the code your provided is effectively recursive, as all it does is keeping its own stack, instead of using the one provided by the memory manager of your OS. Specifically, the variable stack is keeping the recursive state, so to speak, that is passed from one recursive call to another. You could, and perhaps should, consider each iteration of the outer while loop in the permute function as a recursive call. If you do so, you will see that the outer while loop helps 'recursively' traverse each permutation of nums in a depth-first manner.
Noticing this, it's fairly easy to figure out what each 'recursive call' does. Basically, the variable permutation keeps the current permutation of nums which is being formed as while loop progresses. Variable permutations store all the permutations of nums that are found. As you may observe, permutations are updated only when len(permutation) is equal to len(nums) which can be considered as the base case of the recurrence relation that is being implemented using a custom stack. Finally, the inner while loop picks which element of nums to add to the current permutation(i.e. stored in variable permutation) being formed.
So that is about it, really. You can figure out what is exactly being done on the lines relevant to the maintenance of stack using a debugger, as suggested. As a final note, let me repeat that I, personally, would not consider this implementation to be non-recursive. It just so happens that, instead of using the abstraction provided by the OS, this recursive solution keeps its own stack. To provide a better understanding of how a proper non-recursive solution would be, you may observe the difference in recursive and iterative solutions to the problem of finding nth Fibonacci number provided below. As you can see, the non-recursive solution keeps no stack, and instead of dividing the problem into smaller instances of it(recursion) it builds up the solution from smaller solutions. (dynamic programming)
def recursive_fib(n):
if n == 0:
return 0
elif n == 1:
return 1
return recursive_fib(n-1) + recursive_fib(n-2)
def iterative_fib(n):
f_0 = 0
f_1 = 1
for i in range(3, n):
f_2 = f_1 + f_0
f_0 = f_1
f_1 = f_2
return f_1
The answer from #ilim is correct and should be the accepted answer but I just wanted to add another point that wouldn't fit as a comment. Whilst I imagine you are studying this algorithm as an exercise it should be pointed out that a better way to proceed, depending on the size of the list, may be to user itertools's permutations() function:
print [x for x in itertools.permutations([1, 2, 3])]
Testing on my machine with a list of 11 items (39m permutations) took 1.7secs with itertools.permutations(x) but took 76secs using the custom solution above. Note however that with 12 items (479m permutations) the itertools solution blows up with a memory error. If you need to generate permutations of such size efficiently you may be better dropping to native code.

Categories

Resources