4

I have the following function:

def foo(length, num):
  return num in range(length)

What's the time complexity of this function? Noting that range() creates a Range object on Python 3, will the time complexity of this function be O(1) or O(N)?

Would like to know whether there's a difference in time complexity between the various Python versions as well (2 vs 3).

Yangshun Tay
  • 33,183
  • 26
  • 102
  • 123
  • 4
    If the item is an `int`, then it will take *O(1)* in Python-3.x, and *O(n)* in Python-2.x. For `float`s, it is *O(n)* both. – Willem Van Onsem Sep 13 '19 at 19:47
  • 1
    Great! Would you like to answer the question and I can mark it as correct? – Yangshun Tay Sep 13 '19 at 19:48
  • I would guess `O(min(length, num)))`. `range(n)` will create `N` iterations generator, `x in range(n)` will iterate over whole generator if `x>n` or up `n` elements. `x in set(range(n))` would do a hash lookup which would be faster (faster = safe instructions) – geckos Sep 13 '19 at 19:49
  • 1
    Related - [Why is “1000000000000000 in range(1000000000000001)” so fast in Python 3?](https://stackoverflow.com/questions/30081275/why-is-1000000000000000-in-range1000000000000001-so-fast-in-python-3) – Anoop R Desai Sep 13 '19 at 19:50
  • Clear dup, see above. – SergeyA Sep 13 '19 at 19:50
  • 1
    @geckos there are no generators involved here. – juanpa.arrivillaga Sep 13 '19 at 19:51
  • 1
    I totally wrong, sorry, at least I learned something :D – geckos Sep 13 '19 at 19:51
  • 2
    @geckos, when you're creating a set from range(...), it is already O(n) complexity because all the elements of range(...) will be stored in memory. – Leo Leontev Sep 13 '19 at 19:52
  • The dupe question was not easy to find at all, at least it didn't show up in a Google search result of "Python in operator range() time complexity". Also, it's not as simple as saying the answer is straightforward O(1). As Willem mentioned it's O(N) in Python 2. I think there's still value in this question? – Yangshun Tay Sep 13 '19 at 19:54
  • 1
    @YangshunTay: Note: Your comment that "`range()` creates a Range object (not a list!)" is wrong on Python 2. Python 2 `range` doesn't produce a special object of any kind, it just returns a `list` of all the numbers in the desired range. `xrange` returns a special object similar to Python 3's `range` object, but it doesn't have the `O(1)` containment test (nor did Python 3's `range` before Python 3.2; `O(1)` containment tests were new in 3.2), and it's more limited in other ways as well (didn't support slicing or negative indices, didn't define equality properly, couldn't handle huge bounds). – ShadowRanger Sep 13 '19 at 19:58
  • Have modified the question. Thanks for pointing it out. – Yangshun Tay Sep 13 '19 at 20:01

1 Answers1

6

In a range(..) is an object, it does not construct a list. If you perform member checks with an int as item, then it can do that quite fast. The algorithm is a bit complicated, since there are both positive and negative steps. You can look it up on [GitHub]. A simple algorithm with a positive step count (c > 0) for x in range(a, b, c) is something like:

x ≥ a &wedge; x < b &wedge; mod(x-a, c) = 0.

For a negative step count (c < 0) is is quite similar:

x ≤ a &wedge; x > b &wedge; mod(x-a, c) = 0.

If we consider the comparisons to be done in O(1) and calculating the modulo as well, it is an O(1) algorithm. In reality for huge numbers, it scales in the number of digits of the numbers, so it is an O(log n) algorithm.

This however only works for ints. Indeed, in case you use floats, complex, other numerical or non-numerical types, it does not perform the above calculations. It will then fall back on iterating over the range(..) object. Which of course can take considerable time. If you have a range of a million elements, it will thus iterate over all these elements and eventually reach the end of the range, and return False, or find a match, and return True. In the future, perhaps some extra functionality can be implemented for some numerical types, but one can not do this in general, since you can define your own numerical type with an equality operation that works differently.

In , range(..) is a function that returns a list. Indeed:

>>> range(15)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
>>> type(range(15))
<type 'list'>

In order to check if an element is a member of a list, it will iterate over the list, and check equality for all items until it finds an element that is equal, or the list is exhausted. If we consider checking if two items are equal to be constant time, then this takes linear time O(n). In reality for huge numbers, checking if two numbers are equal, scales linear with the number of "digits", so O(log m) with m the value of that number.

has an xrange(..) object as well, but this does not check for membership with the above demonstrated trick.

Willem Van Onsem
  • 321,217
  • 26
  • 295
  • 405