Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Doc/faq/design.rst
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,8 @@ tuples, but not lists, can be used as keys. Note, however, that a tuple is
only hashable if all of its elements are hashable.


.. _how-are-lists-implemented:

How are lists implemented in CPython?
-------------------------------------

Expand All @@ -445,6 +447,10 @@ cleverness is applied to improve the performance of appending items repeatedly;
when the array must be grown, some extra space is allocated so the next few
times don't require an actual resize.

See :ref:`time-complexity` for the costs of the various list operations.


.. _how-are-dictionaries-implemented:

How are dictionaries implemented in CPython?
--------------------------------------------
Expand All @@ -462,6 +468,8 @@ internal array where the value will be stored. Assuming that you're storing
keys that all have different hash values, this means that dictionaries take
constant time -- *O*\ (1), in Big-O notation -- to retrieve a key.

See :ref:`time-complexity` for the costs of the various dictionary operations.


Why must dictionary keys be immutable?
--------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion Doc/faq/programming.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1136,7 +1136,7 @@ What is the most efficient way to concatenate many strings together?
:class:`str` and :class:`bytes` objects are immutable, therefore concatenating
many strings together is inefficient as each concatenation creates a new
object. In the general case, the total runtime cost is quadratic in the
total string length.
total string length. See :ref:`time-complexity` for more information.

To accumulate many :class:`str` objects, the recommended idiom is to place
them into a list and call :meth:`str.join` at the end::
Expand Down
2 changes: 1 addition & 1 deletion Doc/glossary.rst
Original file line number Diff line number Diff line change
Expand Up @@ -942,7 +942,7 @@ Glossary
list
A built-in Python :term:`sequence`. Despite its name it is more akin
to an array in other languages than to a linked list since access to
elements is *O*\ (1).
elements is *O*\ (1). See :ref:`time-complexity`.

list comprehension
A compact way to process all or part of the elements in a sequence and
Expand Down
1 change: 1 addition & 0 deletions Doc/library/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ the `Python Package Index <https://pypi.org>`_.
stdtypes.rst
exceptions.rst
threadsafety.rst
time-complexity.rst

text.rst
binary.rst
Expand Down
6 changes: 6 additions & 0 deletions Doc/library/stdtypes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -999,6 +999,9 @@ The ``in`` and ``not in`` operations have the same priorities as the
comparison operations. The ``+`` (concatenation) and ``*`` (repetition)
operations have the same priority as the corresponding numeric operations. [3]_

See :ref:`time-complexity` for the costs of the various sequence
operations.

.. index::
triple: operations on; sequence; types
pair: built-in function; len
Expand Down Expand Up @@ -5112,6 +5115,7 @@ computing mathematical operations such as intersection, union, difference, and
symmetric difference.
(For other containers see the built-in :class:`dict`, :class:`list`,
and :class:`tuple` classes, and the :mod:`collections` module.)
See :ref:`time-complexity` for the costs of the various set operations.

Like other collections, sets support ``x in set``, ``len(set)``, and ``for x in
set``. Being an unordered collection, sets do not record element position or
Expand Down Expand Up @@ -5336,6 +5340,8 @@ There are currently two standard mapping types, the :dfn:`dictionary` and
(For other containers see the built-in
:class:`list`, :class:`set`, and :class:`tuple` classes, and the
:mod:`collections` module.)
See :ref:`time-complexity` for the costs of the various dictionary
operations.

A dictionary's keys are *almost* arbitrary values. Values that are not
:term:`hashable`, that is, values containing lists, dictionaries or other
Expand Down
308 changes: 308 additions & 0 deletions Doc/library/time-complexity.rst
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
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
with the size of its input. Unless stated otherwise, *n* denotes the number of
elements currently in the container, and *k* is either the value of a parameter
or the number of elements in the parameter.

For a pragmatic approach to assessing time complexity, see Ned Batchelder's
`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::
:header-rows: 1

* - Operation
- Complexity
* - Copy (``s.copy()``)
- *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*)
Comment thread
StanFromIreland marked this conversation as 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*)
* - Set slice (``s[i:j] = t``)
- *O*\ (*k* + *n*)
* - Delete slice (``del s[i:j]``)
- *O*\ (*n*)
* - Extend (``s.extend(t)``) [1]_
- *O*\ (*k*)
* - 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)


: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.

.. list-table::
:header-rows: 1

* - Operation
- Complexity
* - Copy (``tuple(s)``)
- *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
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::
:header-rows: 1

* - Operation
- Complexity
* - ``key in d``
- *O*\ (1)
* - Copy (``d.copy()``) [5]_ [6]_
- *O*\ (*n*)
* - Get item (``d[key]``)
- *O*\ (1)
* - Set item (``d[key] = value``) [1]_
- *O*\ (1)
* - Delete item (``del d[key]``)
- *O*\ (1)
* - Iteration [6]_
- *O*\ (*n*)
* - Get length (``len(d)``) [4]_
- *O*\ (1)


:class:`!set`, :class:`!frozenset`
==================================

See :class:`dict` as the :class:`set` and :class:`frozenset` implementations are
intentionally very similar, and the same hash collision caveat applies.
In the worst case, *O*\ (1) operations instead take *O*\ (*n*) time,
and operations that look up every element of an operand degrade accordingly.

A :class:`frozenset` is :term:`immutable`, so it does not support adding,
discarding, or the in-place update operations; the others below apply to it at
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)``)
- *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)


: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.
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
bytes, and is amortized *O*\ (1).

.. list-table::
:header-rows: 1

* - Operation
- Complexity
* - Get item (``s[k]``)
- *O*\ (1)
* - Get slice (``s[i:j]``)
- *O*\ (*k*)
* - Concatenate (``s + t``)
- *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*)
* - Iteration
- *O*\ (*n*)
* - Get length (``len(s)``) [4]_
- *O*\ (1)


: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)``)
- *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]``)
- *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.
Loading
Loading