Ответ
Добавление элемента в конец слайса с помощью встроенной функции append
имеет амортизированную постоянную сложность O(1).
Давайте разберемся, почему так.
Внутреннее устройство слайса
Слайс — это структура-заголовок, которая содержит три поля:
- Указатель на начало нижележащего массива (underlying array).
- Длина (
len
) — количество элементов в слайсе. - Емкость (
cap
) — количество элементов, которое может вместить нижележащий массив, начиная с указателя.
Два сценария при вызове append
Когда
len < cap
(в массиве есть свободное место):- Сложность: O(1).
- Происходит следующее: новый элемент просто добавляется в следующую свободную ячейку нижележащего массива, и
len
слайса увеличивается на 1. Это очень быстрая операция.
Когда
len == cap
(свободного места нет):- Сложность: O(n), где
n
— текущий размер слайса. - Происходит дорогостоящая операция переаллокации:
- Выделяется память под новый, больший массив.
- Все элементы из старого массива копируются в новый.
- Новый элемент добавляется в конец нового массива.
- Заголовок слайса обновляется: указатель теперь ведет на новый массив, а
len
иcap
обновляются.
- Сложность: O(n), где
Почему сложность амортизированная O(1)?
Хотя операция переаллокации имеет сложность O(n), она происходит нечасто. Go использует стратегию роста с запасом: при переаллокации capacity
нового массива становится значительно больше, чем len
.
- Стратегия роста: До Go 1.18 для слайсов размером до 1024 элементов
capacity
удваивалась (new_cap = 2 * old_cap
). Для больших слайсов она росла медленнее (примерно на 25%). В новых версиях Go алгоритм может быть более сложным, но общий принцип сохраняется.
Благодаря такому экспоненциальному росту, стоимость дорогой операции O(n) «размазывается» по множеству предшествующих ей дешевых операций O(1). В среднем, стоимость добавления одного элемента остается постоянной.
Практический совет
Если вы заранее знаете примерное количество элементов, которое будет храниться в слайсе, создавайте его с нужной capacity
, чтобы избежать переаллокаций и связанных с ними накладных расходов.
// Создаем слайс с длиной 0, но с емкостью 1000.
// Все последующие 1000 вызовов append будут иметь сложность O(1).
s := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
s = append(s, i) // Нет переаллокаций
}