Визуализация
Сценарий: Рост массива
Легенда цветов
Как работает динамический массив
Идея структуры: Массив фиксированной длины в оперативной памяти занимает непрерывный блок. Мы не можем просто "дописать" элемент в конец, если там уже лежат другие данные. Поэтому динамический массив выделяет память с запасом (это называется емкость). Когда запас исчерпывается, он выделяет новый, более крупный блок, копирует туда все элементы и удаляет старый блок.
Зачем нужно удвоение. Самая популярная стратегия роста - удвоение емкости. Это гарантирует, что операция копирования происходит все реже по мере роста массива. На больших массивах копирование остается дорогой, но крайне редкой операцией, поэтому в среднем вставка остается очень быстрой.
Амортизированная O(1). Хотя при перевыделении мы тратим O(n) времени на перенос всех элементов, на каждую такую дорогую вставку приходится множество дешевых. В среднем (в пересчете на одну операцию) добавление в конец занимает константное время O(1). Это и называется амортизированной сложностью.
Обратная сделка. Если сдвиги при вставке и перевыделение памяти мешают, берут связный список: он растет по одному узлу и не двигает соседей, но теряет доступ по индексу - к пятому элементу придется идти через первые четыре.
Синонимы названия: В разных языках динамический массив может называться
по-разному: вектор (std::vector в C++), ArrayList (в Java/C#),
растяжимый массив. В Python стандартный список (list) и в JavaScript
стандартный массив (Array) под капотом тоже реализованы как динамические массивы.
Сложность
Сложность - это оценка того, как быстро растет потребление ресурса при росте числа элементов n. Ресурса у структуры два, поэтому и оценки две: по времени и по памяти. Обе записывают через O-нотацию - у нас есть отдельная анимация о том, как читать такие записи.
Динамический массив объединяет быстрый доступ по индексу с возможностью изменения длины.
По времени
| Операция | Сложность | Комментарий |
|---|---|---|
| Доступ по индексу (get/set) | O(1) | Мгновенное вычисление адреса ячейки в памяти |
| Вставка в конец (push) | O(1)* | Амортизированно O(1). Худший случай O(n), если потребовалось перевыделение |
| Вставка в середину/начало | O(n) | Нужно сдвинуть все последующие элементы вправо |
| Удаление из конца (pop) | O(1) | Уменьшаем размер на 1 (обычно без сужения емкости) |
По памяти
| Характеристика | Оценка | Комментарий |
|---|---|---|
| Затраты по памяти | O(n) | Память выделяется непрерывным блоком. Из-за запаса емкости потребление памяти обычно в 1.5-2 раза больше фактического числа элементов |
Свойства
| Свойство | Значение | Комментарий |
|---|---|---|
| Организация памяти | Непрерывная | Элементы расположены в памяти строго друг за другом, обеспечивая отличную локальность кэша CPU. |
| Доступ по индексу | O(1) | Мгновенное обращение к любому элементу по индексу благодаря вычислению базового адреса. |
| Изменение емкости | Амортизированное O(1) | При заполнении массив выделяет новый блок большего размера и копирует существующие элементы. |
Примеры реализации: Динамический массив (Dynamic Array)
Код на C
#include <stdio.h>
#include <stdlib.h>
// Структура, описывающая динамический массив
typedef struct {
int* data; // Указатель на начало блока памяти в куче
int size; // Текущее количество полезных элементов
int capacity; // Текущая вместимость (сколько ячеек выделено)
} DynamicArray;
// Инициализация пустого массива с начальной емкостью
void init(DynamicArray* arr, int initial_capacity) {
arr->capacity = initial_capacity;
arr->size = 0;
// Выделяем непрерывный блок памяти под capacity элементов
arr->data = (int*) malloc(arr->capacity * sizeof(int));
}
// Добавление элемента в конец (Push) - Амортизированная O(1)
void push(DynamicArray* arr, int value) {
// 1. Проверяем, есть ли свободное место
if (arr->size == arr->capacity) {
// Места нет. Удваиваем емкость (стратегия роста x2)
arr->capacity *= 2;
// realloc делает три вещи:
// 1) Пытается расширить текущий блок памяти
// 2) Если не выходит - выделяет новый блок большего размера
// 3) Копирует старые данные в новый блок и удаляет старый!
arr->data = (int*) realloc(arr->data, arr->capacity * sizeof(int));
}
// 2. Добавляем элемент и сдвигаем границу полезных данных
arr->data[arr->size] = value;
arr->size++;
}
// Очистка памяти после использования (обязательно в C!)
void destroy(DynamicArray* arr) {
free(arr->data);
arr->data = NULL;
arr->size = 0;
arr->capacity = 0;
}
Код на Python
# В Python стандартный тип `list` под капотом реализован именно
# как динамический массив указателей (в CPython).
# Он сам управляет емкостью и перевыделением (рост примерно в 1.125 раз).
# Создаем пустой динамический массив
arr = []
# Добавление в конец (Append / Push) - Амортизированная O(1)
# Большую часть времени это работает мгновенно.
# Иногда Python останавливается, выделяет новый кусок памяти и копирует все элементы.
arr.append(42)
arr.append(15)
# Вставка в середину (Insert) - Строгая O(n)
# Python придется сдвинуть на один шаг вправо все элементы, начиная с нулевого,
# чтобы освободить ячейку под число 99. Чем ближе к началу вставка, тем больше сдвиг.
arr.insert(0, 99)
# Чтение и запись по индексу - Строгая O(1)
# Адрес нужной ячейки вычисляется мгновенно математически:
# Адрес_ячейки = Базовый_адрес + Индекс * Размер_элемента
val = arr[1]
arr[1] = 100
# Удаление из конца (Pop) - O(1)
# Обычно Python не уменьшает емкость массива при удалении
# сразу, чтобы избежать лишних аллокаций.
last = arr.pop()
Код на JavaScript
// В JavaScript стандартный объект Array работает как динамический массив
// (хотя движки вроде V8 могут оптимизировать его по-разному в зависимости от типов).
let arr = [];
// Добавление элементов в конец (Push) - Амортизированная O(1)
// Движок JS (например, V8 в Chrome/Node) сам следит за длиной (length)
// и реальной емкостью (capacity) внутреннего C++ массива.
arr.push(42);
arr.push(15);
// Вставка элемента в начало (Unshift) - O(n)
// Метод unshift сдвигает все существующие элементы на один индекс вправо.
// Для большого массива это очень медленная операция!
arr.unshift(99);
// Удаление элемента из конца (Pop) - O(1)
// Просто отдает последний элемент и уменьшает length на 1.
let last = arr.pop();
// Доступ по индексу работает за O(1)
// Движок знает точное смещение в памяти, так как элементы (обычно) лежат непрерывно.
const firstElement = arr[0];
Хотите разобрать тему глубже? В нашем бесплатном курсе есть урок про вектор, malloc и realloc - с подробной теорией, задачами и проверочными вопросами.