Перейти к основному содержимому

Big O для памяти

Big O может описывать память так же, как описывает время: сколько дополнительной памяти нужно алгоритму по мере роста входных данных?

Мы считаем только дополнительную память — переменные и срезы, которые создаёт алгоритм, — а не сами входные данные. Эту дополнительную память называют вспомогательной памятью.

Память O(1): фиксированное число переменных, независимо от входных данных. Память O(n): на каждый входной элемент сохраняется что-то новое, например копия или map уже встреченных значений.


Память O(1)

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

Суммирование, поиск максимума и линейный поиск — всё это время O(n), но память O(1): они много читают и почти ничего не запоминают.

Сумма с одной переменной

Весь алгоритм запоминает ровно одно число — sum. Расход памяти не растёт вместе со срезом: память O(1).

Prompt
Напиши программу на Go, которая суммирует срез 3, 1, 4, 1, 5, 9, 2, 6 с помощью одной дополнительной переменной и выводит сумму, чтобы показать алгоритм со временем O(n), но памятью O(1).
Вывод: программа на Go и захваченный вывод
Sum with one variable

Память O(n)

Как только алгоритм строит что-то, где на каждый входной элемент приходится своя запись — новый срез, map, — его память становится O(n): вдвое больше входных данных — вдвое больше памяти.

Само по себе это не плохо. Это просто цена, которую стоит замечать, потому что её можно обменивать.

Построение нового среза

Выходной срез получает по одному элементу на каждый входной элемент. 5 на входе, 5 на выходе — память растёт вместе с входными данными: память O(n).

Prompt
Напиши программу на Go, которая строит новый срез, где каждый элемент среза 1, 2, 3, 4, 5 удвоен, используя make и append, а затем выводит входной срез и выходной срез.
Вывод: программа на Go и захваченный вывод
Build a new slice

Обмен памяти на время

Часто память можно потратить, чтобы сэкономить время. Классический пример — Two Sum: найти в срезе два числа, которые в сумме дают заданное значение.

Прямой способ проверяет каждую пару — время O(n²), память O(1). Быстрый способ проходит по срезу один раз и запоминает в map каждое встреченное число. Для каждого числа он спрашивает: «встречалось ли уже target - n?» — время O(n), память O(n).

Такой обмен обычно оправдан: память в машину можно добавить, а время — нет.

Two Sum с двумя циклами

Проверяется каждая пара, пока не найдётся совпадение, — до 15 проверок для 6 элементов, и это число растёт как квадрат размера входных данных. Дополнительная память не используется.

Prompt
Напиши программу на Go, которая находит в срезе 2, 11, 15, 1, 8, 7 два числа, дающих в сумме 9, проверяя каждую пару двумя вложенными циклами и подсчитывая проверки. Выведи каждое совпадение вместе с числом проверок на этот момент, а в конце выведи общее число проверок.
Вывод: программа на Go и захваченный вывод
Two Sum with two loops

Two Sum с map

Один проход, 6 проверок. Каждое число ищется и сохраняется в map seen — работа за константное время на элемент. Этот map и есть память O(n), которая покупает время O(n).

Prompt
Напиши программу на Go, которая за один проход находит в срезе 2, 11, 15, 1, 8, 7 два числа, дающих в сумме 9: храни map уже встреченных чисел и для каждого числа проверяй, встречалось ли уже целевое значение минус это число. Выведи каждое совпадение, а в конце выведи, что каждый элемент был проверен один раз.
Вывод: программа на Go и захваченный вывод
Two Sum with a map