Массивы
Прочитать миллионное значение массива можно за один шаг, а вставить новое значение в начало того же массива значит сдвинуть миллион значений.
Что такое структура данных
Программа, которая хранит список имён, очередь заданий на печать или таблицу цен, должна где-то держать эти данные и отвечать на вопросы о них: какой третий элемент, есть ли это имя в списке, какое задание следующее? Структура данных представляет собой способ хранить данные вместе с методами, которые отвечают на эти вопросы. Курс MIT 6.006 разделяет здесь две идеи. Интерфейс говорит, какие операции можно запросить, например «получить элемент на позиции i» или «добавить элемент в конец». Структура данных говорит, как эти операции на самом деле выполняются в памяти. За одним интерфейсом может стоять несколько структур данных, и по скорости они могут отличаться огромно.
Этот курс, как и курс об алгоритмах, считает шаги, чтобы сравнивать две структуры без секундомера. В этом уроке шагом считается чтение или запись одной ячейки памяти. Лаборатория ниже считает их за тебя.
Один блок ячеек, подряд
Массив хранит значения в одном сплошном блоке памяти, каждое значение в ячейке одинакового размера, с номерами от 0. Именно такое устройство делает чтение из массивов быстрым. Если блок начинается по адресу s и каждая ячейка занимает c байт, то a[i] лежит по адресу s + i × c. Компьютер вычисляет это одним умножением и одним сложением и сразу переходит туда, будь i равно 3 или 3 000 000. В конспектах MIT операции «get at(i)» и «set at(i, x)» для массива указаны как Θ(1): стоимость не растёт с длиной.
Из-за того же устройства изменения в середине медленные. Пусть в массиве 7, 3, 9, 4, 6, и нужно поставить 1 в начало. Перед 7 пустого места нет, поэтому сначала каждое значение должно сдвинуться на одну ячейку вправо, начиная с последнего, чтобы ничего не затереть. Это 5 сдвигов и 1 запись: 6 шагов. Удаление первого значения является зеркальным случаем: прочитать его, затем сдвинуть остальные 4 значения на одну ячейку влево. Вставка на позицию i в массив из n значений стоит n − i сдвигов плюс одну запись, поэтому начало является худшим местом, а конец лучшим.
Перетащи, чтобы повернуть. Колёсико или щипок для масштаба.
Когда блок полон
У блока памяти фиксированный размер, так что же происходит, когда добавляешь значение, а все ячейки заняты? Массиву приходится запросить новый, больший блок и скопировать туда каждое значение, прежде чем записать новое. В лаборатории добавление в полный блок из 5 стоит 5 копирований и 1 запись. Если бы массив каждый раз рос всего на одну ячейку, каждое добавление копировало бы всё, и n добавлений стоили бы около n²/2 копирований.
Решение, описанное в конспектах MIT, состоит в том, чтобы расти сразу сильно, например удваиваться. Если начать с ёмкости 1, копирования происходят, когда в массиве 1, 2, 4, 8 и 16 значений, так что 17 добавлений стоят в сумме 1 + 2 + 4 + 8 + 16 = 31 копирование, меньше 2 на одно добавление. Отдельное добавление всё ещё может быть медленным, но в любой длинной серии добавлений в среднем выходит постоянная стоимость на добавление. Это называется амортизированным временем Θ(1). Массив JavaScript снаружи ведёт себя именно так: MDN описывает массивы как изменяемые по размеру, а push и pop работают с концом, то есть с дешёвой стороной.
| Операция над n значениями | Шагов в лаборатории | Растёт как |
|---|---|---|
| Прочитать или записать a[i] | 1 | Θ(1) |
| Вставка в начало | n сдвигов + 1 запись | Θ(n) |
| Удаление первого | 1 чтение + (n − 1) сдвигов | Θ(n) |
| Добавление в конец (место есть) | 1 | Θ(1) |
| Добавление в конец (блок полон) | n копирований + 1 запись | Θ(n) один раз, Θ(1) амортизированно |
const a = [7, 3, 9, 4, 6];
console.log(a[3]); // 4, один шаг
a.push(8); // дёшево: конец
a.unshift(1); // каждое значение сдвигается на ячейку вправо
console.log(a.join(" ")); // 1 7 3 9 4 6 8| Интерфейс | Список операций, которые можно запросить, например get at(i) или insert last(x). |
| Структура данных | Как эти операции выполняются в памяти. |
| Массив | Значения в одном блоке одинаковых ячеек; a[i] лежит по адресу начало + i × размер ячейки. |
| Динамический массив | Массив, который растёт, копируясь в больший блок, обычно вдвое больше. |
| Амортизированная стоимость | Средняя стоимость одной операции в длинной серии, даже если отдельные операции медленные. |
- Сколько шагов по счёту этого урока занимает чтение a[700000] из массива в 1 000 000 значений?
- 1
- 20
- 700,000
- 1,000,000
- В массиве 7, 3, 9, 4, 6, и место ещё есть. Сколько шагов занимает вставка значения в начало (сдвиги плюс запись)?
- Тот же массив из 5 значений: сколько шагов занимает удаление первого значения (1 чтение плюс сдвиги)?
- Вставка на позицию 2 в массив из 5 значений (позиции от 0 до 4), место есть: сколько шагов?
- В удваивающемся массиве 5 значений, и его ёмкость 5. Ты добавляешь ещё одно значение. Сколько шагов занимает это добавление?
- Какая ёмкость после этого добавления?
- Если начать с ёмкости 1 и удваивать, сколько всего копирований сделают 17 добавлений?
- Почему добавление в удваивающийся массив называют амортизированным Θ(1)?
- Каждое отдельное добавление занимает ровно один шаг
- Копирования редки, поэтому сумма за n добавлений остаётся меньше константы, умноженной на n
- Компьютер копирует в фоне бесплатно
- JavaScript никогда не копирует массивы
- В терминах MIT 6.006 «insert last(x) и delete last()» является
- структурой данных
- интерфейсом
- адресом в памяти
- способом сортировки
- У массива длина n и счёт с 0. Какой индекс у его последнего значения?
- n
- n − 1
- n + 1
- 1
- Интерфейс перечисляет операции; структура данных говорит, как они выполняются.
- Массив хранит значения в одном блоке, поэтому a[i] занимает один шаг.
- Вставка или удаление рядом с началом сдвигает около n значений.
- Удваивающийся массив делает добавление амортизированным Θ(1): 17 добавлений от ёмкости 1 копируют 31 значение.
Источники курса (11)
- MIT OpenCourseWare 6.006 Introduction to Algorithms, Spring 2020 (Demaine, Ku, Solomon)
- lecture notes 2 Data Structures (interface vs data structure, Sequence and Set interfaces, stack = insert_last/delete_last, queue = insert_last/delete_first, array, linked list, dynamic array, amortized Θ(1) append)
- 4 Hashing (chaining, pigeonhole collisions, chain length n/m)
- 6 Binary Trees Part 1 (height, traversal order, O(h) operations) and 7 Binary Trees Part 2: AVL (Adelson-Velsky and Landis, 1962) at ocw.mit.edu/courses/6-006-introduction-to-algorithms-spring-2020/pages/lecture-notes
- MDN Web Docs, Array.prototype.shift (shifts all values left by 1)
- Array.prototype.unshift, Map (insertion order, Map vs Object, spec requires sublinear average access) and Set (developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects)
- ECMAScript 2025 Language Specification (tc39.es/ecma262)
- 24.1 Map Objects and 24.2 Set Objects ("must be implemented using either hash tables or other mechanisms that, on average, provide access times that are sublinear on the number of elements")
- Array.prototype.shift steps (Repeat, while k < len: move element k to k - 1)
- D. E. Knuth, The Art of Computer Programming vol. 3 (2nd ed. 1998) section 6.4 History (H. P. Luhn, IBM internal memorandum January 1953, hashing with chaining; digit pairs added mod 10, 31415926 -> 4548) via gwern.net copy, and IEEE Spectrum, "Hans Peter Luhn and the Birth of the Hashing Algorithm" (2018)
- "A brief history of the stack", SIGCIS (sigcis.org/files/A brief history.pdf: Bauer and Samelson applied for the patent in 1957, US 3,047,228 granted July 31, 1962; IPL list processing by Newell, Simon and Shaw at RAND 1956) and Wikipedia, Information Processing Language (Newell, Shaw, Simon, RAND and Carnegie Institute of Technology, about 1956).
Проверено: 2026-10-18