Repository navigation
Add a page detailing the time complexity of operations on built-in types #154363
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
80088bf
84b95f9
1982202
64fb553
0392d7e
9e1a82a
f19c249
d0ca44c
7e297d6
22d5586
5f21510
29e2017
4d223f1
a893080
c4a75fd
eac04b8
27e9cd6
116f21e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,308 @@ | ||
| .. _time-complexity: | ||
|
|
||
| =============================================== | ||
| Time complexity of operations on built-in types | ||
| =============================================== | ||
|
|
||
| This page documents the time-complexity of various operations on built-in types | ||
|
StanFromIreland marked this conversation as resolved.
Resolved
|
||
| in CPython. Other Python implementations may have different performance | ||
| characteristics. Additionally, the listed costs assume exact built-in types, as | ||
| instances of subclasses may have different costs. | ||
|
|
||
| We use |big O notation|_ to describe how the running time of an operation grows | ||
|
picnixz marked this conversation as resolved.
Resolved
|
||
| with the size of its input. Unless stated otherwise, *n* denotes the number of | ||
|
StanFromIreland marked this conversation as resolved.
Resolved
|
||
| elements currently in the container, and *k* is either the value of a parameter | ||
| or the number of elements in the parameter. | ||
|
StanFromIreland marked this conversation as resolved.
Resolved
|
||
|
|
||
| For a pragmatic approach to assessing time complexity, see Ned Batchelder's | ||
|
StanFromIreland marked this conversation as resolved.
Resolved
|
||
| `Big-O: How Code Slows as Data Grows <https://nedbatchelder.com/text/bigo>`__ | ||
| talk and blog post. | ||
|
|
||
| .. |big O notation| replace:: Big *O* notation | ||
| .. _big O notation: https://en.wikipedia.org/wiki/Big_O_notation | ||
|
|
||
|
|
||
| :class:`!list` | ||
| ============== | ||
|
|
||
| Lists are mutable sequences; for more detail on the implementation see | ||
| :ref:`how-are-lists-implemented`. The largest costs come from growing beyond the | ||
| current allocation size (because everything must move), or from inserting or | ||
| deleting somewhere near the beginning (because everything after that must move). | ||
| If you need to add or remove at both ends, consider using a | ||
| :class:`collections.deque` instead. | ||
|
|
||
| .. list-table:: | ||
|
StanFromIreland marked this conversation as resolved.
Resolved
|
||
| :header-rows: 1 | ||
|
|
||
| * - Operation | ||
| - Complexity | ||
| * - Copy (``s.copy()``) | ||
|
StanFromIreland marked this conversation as resolved.
Resolved
|
||
| - *O*\ (*n*) | ||
| * - Append (``s.append(x)``) [1]_ | ||
| - *O*\ (1) | ||
| * - Pop (``s.pop(k)``) [1]_ [2]_ | ||
| - *O*\ (*n* - *k*) | ||
| * - Insert (``s.insert(k, x)``) [1]_ [2]_ | ||
| - *O*\ (*n* - *k*) | ||
|
StanFromIreland marked this conversation as resolved.
Resolved
|
||
| * - Get item (``s[k]``) | ||
| - *O*\ (1) | ||
| * - Set item (``s[k] = x``) | ||
| - *O*\ (1) | ||
| * - Delete item (``del s[k]``) [2]_ | ||
| - *O*\ (*n* - *k*) | ||
| * - Iteration | ||
| - *O*\ (*n*) | ||
| * - Get slice (``s[i:j]``) | ||
| - *O*\ (*k*) | ||
|
StanFromIreland marked this conversation as resolved.
Resolved
|
||
| * - Set slice (``s[i:j] = t``) | ||
| - *O*\ (*k* + *n*) | ||
|
StanFromIreland marked this conversation as resolved.
Resolved
|
||
| * - Delete slice (``del s[i:j]``) | ||
| - *O*\ (*n*) | ||
|
StanFromIreland marked this conversation as resolved.
Resolved
|
||
| * - Extend (``s.extend(t)``) [1]_ | ||
| - *O*\ (*k*) | ||
|
StanFromIreland marked this conversation as resolved.
Resolved
|
||
| * - Sort (``s.sort()``) [3]_ | ||
| - *O*\ (*n* log *n*) | ||
| * - Multiply (``s * k``) | ||
| - *O*\ (*nk*) | ||
| * - ``x in s`` | ||
| - *O*\ (*n*) | ||
| * - ``min(s)``, ``max(s)`` | ||
| - *O*\ (*n*) | ||
| * - Get length (``len(s)``) [4]_ | ||
| - *O*\ (1) | ||
|
|
||
|
StanFromIreland marked this conversation as resolved.
Resolved
|
||
|
|
||
| :class:`!tuple` | ||
| =============== | ||
|
|
||
| A :class:`tuple` is an :term:`immutable` sequence. Because a tuple can never | ||
| change, there are no insertion or deletion costs, and making a copy is constant | ||
| time as the same object is returned. | ||
|
StanFromIreland marked this conversation as resolved.
Resolved
|
||
|
|
||
| .. list-table:: | ||
| :header-rows: 1 | ||
|
|
||
| * - Operation | ||
| - Complexity | ||
| * - Copy (``tuple(s)``) | ||
|
StanFromIreland marked this conversation as resolved.
Resolved
|
||
| - *O*\ (1) | ||
| * - Get item (``s[k]``) | ||
| - *O*\ (1) | ||
| * - Get slice (``s[i:j]``) | ||
| - *O*\ (*k*) | ||
| * - Concatenate (``s + t``) | ||
| - *O*\ (*n* + *k*) | ||
| * - Multiply (``s * k``) | ||
| - *O*\ (*nk*) | ||
| * - Iteration | ||
| - *O*\ (*n*) | ||
| * - ``x in s`` | ||
| - *O*\ (*n*) | ||
| * - ``min(s)``, ``max(s)`` | ||
| - *O*\ (*n*) | ||
| * - Get length (``len(s)``) [4]_ | ||
| - *O*\ (1) | ||
|
|
||
|
|
||
| :class:`!dict`, :class:`!frozendict` | ||
| ==================================== | ||
|
|
||
| The times listed for dict objects are average-case times, as they assume the | ||
| hash function for the objects is sufficiently robust to make collisions | ||
| uncommon; they also assume the keys used in parameters are selected uniformly | ||
| at random from the set of all keys. In the worst case, when every key hashes | ||
|
StanFromIreland marked this conversation as resolved.
Resolved
|
||
| to the same value, each of the *O*\ (1) operations below instead takes | ||
| *O*\ (*n*) time. For more detail on the implementation, see | ||
| :ref:`how-are-dictionaries-implemented`. | ||
|
|
||
| A :class:`frozendict` is immutable, so it does not support setting or deleting | ||
| items; the other operations below apply to it at the same costs. | ||
|
|
||
| .. list-table:: | ||
|
StanFromIreland marked this conversation as resolved.
Resolved
|
||
| :header-rows: 1 | ||
|
|
||
| * - Operation | ||
| - Complexity | ||
| * - ``key in d`` | ||
|
StanFromIreland marked this conversation as resolved.
Resolved
|
||
| - *O*\ (1) | ||
| * - Copy (``d.copy()``) [5]_ [6]_ | ||
| - *O*\ (*n*) | ||
| * - Get item (``d[key]``) | ||
| - *O*\ (1) | ||
|
StanFromIreland marked this conversation as resolved.
Resolved
|
||
| * - Set item (``d[key] = value``) [1]_ | ||
| - *O*\ (1) | ||
| * - Delete item (``del d[key]``) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can also mention
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Leaving to a follow up.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not about adding more entries. It's about adding it under the same row
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes, I left |
||
| - *O*\ (1) | ||
| * - Iteration [6]_ | ||
| - *O*\ (*n*) | ||
| * - Get length (``len(d)``) [4]_ | ||
| - *O*\ (1) | ||
|
|
||
|
picnixz marked this conversation as resolved.
Resolved
|
||
|
|
||
| :class:`!set`, :class:`!frozenset` | ||
|
StanFromIreland marked this conversation as resolved.
Resolved
|
||
| ================================== | ||
|
|
||
| See :class:`dict` as the :class:`set` and :class:`frozenset` implementations are | ||
| intentionally very similar, and the same hash collision caveat applies. | ||
|
StanFromIreland marked this conversation as resolved.
Resolved
|
||
| In the worst case, *O*\ (1) operations instead take *O*\ (*n*) time, | ||
| and operations that look up every element of an operand degrade accordingly. | ||
|
StanFromIreland marked this conversation as resolved.
Resolved
|
||
|
|
||
| A :class:`frozenset` is :term:`immutable`, so it does not support adding, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would be interested in the complexity of
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed this would be quite useful I think, but I'm deferring to a follow up PR so that we can hopefully land this. |
||
| discarding, or the in-place update operations; the others below apply to it at | ||
|
StanFromIreland marked this conversation as resolved.
Resolved
|
||
| the same costs. | ||
|
|
||
| .. list-table:: | ||
| :header-rows: 1 | ||
|
|
||
| * - Operation | ||
| - Complexity | ||
| * - ``x in s`` | ||
| - *O*\ (1) | ||
| * - Add (``s.add(x)``) [1]_ | ||
| - *O*\ (1) | ||
| * - Discard (``s.discard(x)``) | ||
|
StanFromIreland marked this conversation as resolved.
Resolved
|
||
| - *O*\ (1) | ||
| * - Union (``s | t``) [6]_ | ||
| - *O*\ (len(*s*) + len(*t*)) | ||
| * - Intersection (``s & t``, ``s.intersection(t)``) [6]_ [7]_ | ||
| - *O*\ (min(len(*s*), len(*t*))) | ||
| * - Difference (``s - t``, ``s.difference(t)``) [6]_ [8]_ | ||
| - *O*\ (len(*s*)) | ||
| * - Difference update (``s.difference_update(t)``) [1]_ [6]_ [7]_ | ||
| - *O*\ (min(len(*s*), len(*t*))) | ||
| * - Symmetric difference (``s ^ t``) [6]_ | ||
| - *O*\ (len(*s*) + len(*t*)) | ||
| * - Symmetric difference update (``s.symmetric_difference_update(t)``) [1]_ [6]_ | ||
| - *O*\ (len(*t*)) | ||
| * - Get length (``len(s)``) [4]_ | ||
| - *O*\ (1) | ||
|
|
||
|
StanFromIreland marked this conversation as resolved.
Resolved
|
||
|
|
||
| :class:`!str`, :class:`!bytes`, :class:`!bytearray` | ||
| =================================================== | ||
|
|
||
| :class:`str` and :class:`bytes` objects are immutable sequences of characters and | ||
| bytes respectively; As with tuples, copying one returns the original object. | ||
|
StanFromIreland marked this conversation as resolved.
Resolved
|
||
| A :class:`bytearray` is mutable, and additionally supports the mutating operations | ||
| of :class:`list` (except :meth:`!sort`), at the same costs. However, deleting at | ||
| the front only advances the start of the buffer instead of moving the remaining | ||
|
StanFromIreland marked this conversation as resolved.
Resolved
|
||
| bytes, and is amortized *O*\ (1). | ||
|
|
||
| .. list-table:: | ||
| :header-rows: 1 | ||
|
|
||
| * - Operation | ||
|
StanFromIreland marked this conversation as resolved.
Resolved
|
||
| - Complexity | ||
| * - Get item (``s[k]``) | ||
| - *O*\ (1) | ||
| * - Get slice (``s[i:j]``) | ||
| - *O*\ (*k*) | ||
| * - Concatenate (``s + t``) | ||
|
StanFromIreland marked this conversation as resolved.
Resolved
|
||
| - *O*\ (*n* + *k*) | ||
| * - Multiply (``s * k``) | ||
| - *O*\ (*nk*) | ||
| * - Substring search (``x in s``, ``s.find(x)``, ``s.index(x)``) [9]_ | ||
| - *O*\ (*n*) | ||
| * - Encode or decode | ||
| - *O*\ (*n*) | ||
|
StanFromIreland marked this conversation as resolved.
Resolved
|
||
| * - Iteration | ||
| - *O*\ (*n*) | ||
| * - Get length (``len(s)``) [4]_ | ||
| - *O*\ (1) | ||
|
|
||
|
StanFromIreland marked this conversation as resolved.
Resolved
|
||
|
|
||
| :class:`!memoryview` | ||
| ==================== | ||
|
|
||
| :class:`memoryview` objects allow Python code to access the internal data | ||
| of an object that supports the :ref:`buffer protocol <bufferobjects>` without | ||
| copying. In particular, slicing a memory view returns a new view onto the same | ||
| buffer. | ||
|
|
||
| .. list-table:: | ||
| :header-rows: 1 | ||
|
|
||
| * - Operation | ||
| - Complexity | ||
| * - Create (``memoryview(obj)``) | ||
| - *O*\ (1) | ||
| * - Get item (``v[k]``) | ||
| - *O*\ (1) | ||
| * - Get slice (``v[i:j]``) | ||
| - *O*\ (1) | ||
| * - Convert to bytes (``v.tobytes()``, ``bytes(v)``) | ||
|
StanFromIreland marked this conversation as resolved.
Resolved
|
||
| - *O*\ (*n*) | ||
| * - Get length (``len(v)``) [4]_ | ||
| - *O*\ (1) | ||
|
|
||
|
|
||
| :class:`!range` | ||
| =============== | ||
|
|
||
| A :class:`range` object computes its items on demand from its *start*, *stop* and | ||
| *step* values, so most operations do not depend on the length of the range. | ||
|
|
||
| .. list-table:: | ||
| :header-rows: 1 | ||
|
|
||
| * - Operation | ||
| - Complexity | ||
| * - Get item (``s[k]``) | ||
|
StanFromIreland marked this conversation as resolved.
Resolved
|
||
| - *O*\ (1) | ||
| * - Get slice (``s[i:j]``) | ||
| - *O*\ (1) | ||
| * - ``x in s`` [10]_ | ||
| - *O*\ (1) | ||
| * - Index and count (``s.index(x)``, ``s.count(x)``) [10]_ | ||
| - *O*\ (1) | ||
| * - Iteration | ||
| - *O*\ (*n*) | ||
| * - ``min(s)``, ``max(s)`` | ||
| - *O*\ (*n*) | ||
| * - Get length (``len(s)``) [4]_ | ||
| - *O*\ (1) | ||
|
|
||
|
|
||
| Notes | ||
| ===== | ||
|
|
||
| .. [1] Amortized. An individual operation may occasionally be *O*\ (*n*) | ||
| when the underlying storage is resized, but this cost is spread over | ||
| many operations, depending on the history of the container. | ||
|
|
||
| .. [2] Popping or deleting the element at index *k* of a list of size *n* | ||
| shifts all elements after *k* one slot to the left, moving *n* - *k* - 1 | ||
| elements; inserting at index *k* shifts the elements from *k* onwards one | ||
| slot to the right, moving *n* - *k* elements. The worst case is index 0, | ||
| where the whole rest of the list has to be moved; the average case, an | ||
| index in the middle of the list, takes *O*\ (*n*/2) = *O*\ (*n*) | ||
| operations; and operating at the end of the list moves nothing and is | ||
| *O*\ (1). | ||
|
|
||
| .. [3] This is the worst case scenario. Sorting is adaptive and input that is | ||
| already sorted or reverse-sorted takes only *O*\ (*n*) comparisons. | ||
| See :source:`Objects/listsort.txt` for more information. | ||
|
|
||
| .. [4] The number of elements is stored in the object, so ``len()`` does | ||
| not need to count them. | ||
|
|
||
| .. [5] Copying a :class:`frozendict` is *O*\ (1) as it returns the original object. | ||
|
|
||
| .. [6] These operations scan the container's internal hash table, which is | ||
| not shrunk when elements are removed. After removing most elements, they | ||
| still take time proportional to the container's former size, until a | ||
| later insertion triggers a resize. | ||
|
|
||
| .. [7] *O*\ (len(*t*)) if *t* is not a set. | ||
|
|
||
| .. [8] *O*\ (len(*s*) + len(*t*)) if *t* is not a set. | ||
|
|
||
| .. [9] A naive substring search would need *O*\ (*nk*) comparisons in the | ||
| worst case, where *k* is the length of the substring searched for, but | ||
| CPython uses search algorithms with a linear worst case for forward | ||
| searches. See :source:`Objects/stringlib/stringlib_find_two_way_notes.txt` | ||
| for details. | ||
|
|
||
| .. [10] Assuming :class:`int` or :class:`bool` arguments. For other types, | ||
| the range is searched like any other sequence in *O*\ (*n*) time. | ||
Uh oh!
There was an error while loading. Please reload this page.