Когда CPython устанавливает оператор `in`, это O (n)?

Я читал о временной сложности операций с множествами в CPython и узнал, что оператор in для множеств имеет средняя временная сложность O (1) и наихудшая временная сложность O (n). Я также узнал, что наихудший случай не произойдет в CPython , если коэффициент загрузки хэш-таблицы набора слишком высок.

Это заставило меня задуматься, когда такой случай может произойти в реализации CPython? Есть ли простой демонстрационный код, который показывает набор с четко наблюдаемой временной сложностью O (n) оператора in?


person ruohola    schedule 07.12.2019    source источник


Ответы (3)


Коэффициент загрузки — отвлекающий маневр. В CPython наборы (и словари) автоматически изменяют размер, чтобы коэффициент загрузки не превышал 2/3. Вы ничего не можете сделать в коде Python, чтобы остановить это.

O(N) поведение может иметь место, когда очень много элементов имеют одинаковый хеш-код. Затем они сопоставляются с одним и тем же хэшем и устанавливают вырожденные поисковые запросы в медленную форму линейного поиска.

Самый простой способ придумать такие плохие элементы — это создать класс с ужасной хэш-функцией. Как, например, и непроверенный:

class C:
    def __init__(self, val):
        self.val = val
    def __eq__(a, b):
        return a.val == b.val
    def __hash__(self):
        return 3

Затем hash(C(i)) == 3 независимо от значения i.

Чтобы сделать то же самое со встроенными типами, требуется глубокое знание деталей их реализации в CPython. Например, вот способ создать произвольно большое количество различных целых чисел с одним и тем же хэш-кодом:

>>> import sys
>>> M = sys.hash_info.modulus
>>> set(hash(1 + i*M) for i in range(10000))
{1}

что показывает, что все десять тысяч созданных различных целых чисел имеют хеш-код 1.

person Tim Peters    schedule 07.12.2019

Вы можете просмотреть исходный код set здесь, который может помочь: https://github.com/python/cpython/blob/723f71abf7ab0a7be394f9f7b2daa9ecdf6fb1eb/Objects/setobject.c#L429-L441

Трудно придумать конкретный пример, но, к счастью, теория довольно проста :) Набор хранит ключи, используя hash значения, пока это hash достаточно уникально, вы получите производительность O(1), как и ожидалось.

Если по какой-то странной причине все ваши элементы имеют разные данные, но один и тот же хэш, он сталкивается, и ему придется проверять их все по отдельности.

Чтобы проиллюстрировать, вы можете увидеть набор в виде словаря следующим образом:

import collection


your_set = collection.defaultdict(list)


def add(value):
    your_set[hash(value)].append(value)


def contains(value):
    # This is where your O(n) can occur, all values the same hash()
    values = your_set.get(hash(value), [])
    for v in values:
        if v == value:
            return True
    return False
person Wolph    schedule 07.12.2019

Это иногда называют «амортизацией» набора или словаря. Это появляется время от времени как вопрос интервью. Как говорит @TimPeters, изменение размера происходит автоматически на 2/3 емкости, поэтому вы нажмете O (n), только если вы форсируете хэш самостоятельно.

In computer science, amortized analysis is a method for analyzing a given algorithm's complexity, or how much of a resource, especially time or memory, it takes to execute. The motivation for amortized analysis is that looking at the worst-case run time per operation, rather than per algorithm, can be too pessimistic.

`/* GROWTH_RATE. Growth rate upon hitting maximum load.
 * Currently set to used*3.
 * This means that dicts double in size when growing without deletions,
 * but have more head room when the number of deletions is on a par with the
 * number of insertions.  See also bpo-17563 and bpo-33205.
 *
 * GROWTH_RATE was set to used*4 up to version 3.2.
 * GROWTH_RATE was set to used*2 in version 3.3.0
 * GROWTH_RATE was set to used*2 + capacity/2 in 3.4.0-3.6.0.
 */
#define GROWTH_RATE(d) ((d)->ma_used*3)`

Больше к эффективности. Почему 2/3? В статье Википедии есть хороший график https://upload.wikimedia.org/wikipedia/commons/1/1c/Hash_table_average_insertion_time.png к статье . (линейная кривая зондирования соответствует для наших целей от O(1) до O(n), цепочка — более сложный подход к хешированию) См. https://en.wikipedia.org/wiki/Hash_table для полного

Скажем, у вас есть набор или словарь, который стабилен и занимает 2/3 - 1 от его базовой емкости. Вы действительно хотите вялую производительность навсегда? Вы можете принудительно увеличить его размер.

«если ключи всегда известны заранее, вы можете хранить их в наборе и создавать свои словари из набора, используя dict.fromkeys ()». плюс некоторые другие полезные, хотя и датированные наблюдения. Повышение производительности очень большого словаря в Python

Для хорошего чтения о dictresize(): (dict был в Python до установки) https://github.com/python/cpython/blob/master/Objects/dictobject.c#L415

person ShpielMeister    schedule 25.01.2020
comment
Это не связано с амортизацией; временная сложность одной операции in в наборе Python в среднем составляет O(1). Вам не нужно усреднять весь алгоритм, чтобы он был O (1) за операцию. - person kaya3; 25.01.2020
comment
OP и временная сложность в наихудшем случае O (n). По мере заполнения хеш-таблицы каждый «входящий» запрос при поиске набора или словаря стремится к O (n). - person ShpielMeister; 25.01.2020
comment
Да, но это не связано с амортизацией; это время, необходимое для одной операции in в худшем случае. Это не усреднение по большому количеству in операций. - person kaya3; 25.01.2020
comment
Конечно, размер O (n) может быть изменен. By instrumenting a pure Python model for dictionaries (such as this one), it is possible to count the weighted-average number of probes for an alternative insertion order. For example, inserting dict.fromkeys([11100, 22200, 44400, 33300]) averages 1.75 probes per lookup. That beats the 2.25 average probes per lookup for dict.fromkeys([33300, 22200, 11100, 44400]). .Раймонд Хеттингер, ссылка на улучшение производительности, находится в моем ответе выше. - person ShpielMeister; 25.01.2020
comment
Операция in не изменяет состояние набора; он не может инициировать изменение размера базового массива набора. Ваша цитата Рэймонда Хеттингера ничего не говорит об амортизации, и он не говорит об амортизированной средней; просто средний. - person kaya3; 25.01.2020
comment
Конечно, нет. дело в том, что если набор близок к 2/3 емкости базовой хеш-таблицы, функция «in» будет относительно медленной, и, возможно, стоит изменить размер таблицы вверх, принудительно изменив размер. Для огромного набора, который будет интенсивно использоваться, это может стоить усилий, поскольку может сэкономить значительное время. Вариант использования является определяющим фактором. Стоимость изменения размера также должна быть принята во внимание. - person ShpielMeister; 25.01.2020