diff --git a/Doc/faq/design.rst b/Doc/faq/design.rst index c914089e9806ec6..02a3a3a22070dae 100644 --- a/Doc/faq/design.rst +++ b/Doc/faq/design.rst @@ -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? ------------------------------------- @@ -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? -------------------------------------------- @@ -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? -------------------------------------- diff --git a/Doc/faq/programming.rst b/Doc/faq/programming.rst index c2f8f72ee1f2c4b..4e1157e6ebe7296 100644 --- a/Doc/faq/programming.rst +++ b/Doc/faq/programming.rst @@ -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:: diff --git a/Doc/glossary.rst b/Doc/glossary.rst index bb00a4f02f0efd5..02f89b419d9fc2d 100644 --- a/Doc/glossary.rst +++ b/Doc/glossary.rst @@ -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 diff --git a/Doc/library/index.rst b/Doc/library/index.rst index 8fc77be520d4268..f28c03e2fae092f 100644 --- a/Doc/library/index.rst +++ b/Doc/library/index.rst @@ -44,6 +44,7 @@ the `Python Package Index `_. stdtypes.rst exceptions.rst threadsafety.rst + time-complexity.rst text.rst binary.rst diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index 886648e820f071d..b2f0d934980353a 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -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 @@ -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 @@ -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 diff --git a/Doc/library/time-complexity.rst b/Doc/library/time-complexity.rst new file mode 100644 index 000000000000000..8a73c52d6e54980 --- /dev/null +++ b/Doc/library/time-complexity.rst @@ -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 `__ +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*) + * - 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 ` 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. diff --git a/Doc/tutorial/datastructures.rst b/Doc/tutorial/datastructures.rst index 276e31a3056f0ee..77dd8a6e66d5cc4 100644 --- a/Doc/tutorial/datastructures.rst +++ b/Doc/tutorial/datastructures.rst @@ -167,7 +167,8 @@ It is also possible to use a list as a queue, where the first element added is the first element retrieved ("first-in, first-out"); however, lists are not efficient for this purpose. While appends and pops from the end of list are fast, doing inserts or pops from the beginning of a list is slow (because all -of the other elements have to be shifted by one). +of the other elements have to be shifted by one). See +:ref:`time-complexity` for more information. To implement a queue, use :class:`collections.deque` which was designed to have fast appends and pops from both ends. For example::