edubun
Русский

Массивы

Прочитать миллионное значение массива можно за один шаг, а вставить новое значение в начало того же массива значит сдвинуть миллион значений.

Что такое структура данных

Программа, которая хранит список имён, очередь заданий на печать или таблицу цен, должна где-то держать эти данные и отвечать на вопросы о них: какой третий элемент, есть ли это имя в списке, какое задание следующее? Структура данных представляет собой способ хранить данные вместе с методами, которые отвечают на эти вопросы. Курс 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 сдвигов плюс одну запись, поэтому начало является худшим местом, а конец лучшим.

Загрузка

Перетащи, чтобы повернуть. Колёсико или щипок для масштаба.

Массив из пяти значений в блоке из пяти ячеек. Двигай ползунок, чтобы выбрать 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
КонтекстМассивы старше большинства языков программирования. Ранние машины хранили таблицу как пронумерованные слова в памяти, и программисты вычисляли адрес элемента i вручную, ровно как это делает формула выше. Fortran, выпущенный IBM в 1957 г., сделал массивы частью языка, чтобы можно было писать A(I) и оставлять адресную арифметику компилятору. Fortran считал с 1. C и JavaScript считают с 0, и это подходит к формуле: a[0] лежит прямо по начальному адресу.
ИнтерфейсСписок операций, которые можно запросить, например get at(i) или insert last(x).
Структура данныхКак эти операции выполняются в памяти.
МассивЗначения в одном блоке одинаковых ячеек; a[i] лежит по адресу начало + i × размер ячейки.
Динамический массивМассив, который растёт, копируясь в больший блок, обычно вдвое больше.
Амортизированная стоимостьСредняя стоимость одной операции в длинной серии, даже если отдельные операции медленные.
  1. Сколько шагов по счёту этого урока занимает чтение a[700000] из массива в 1 000 000 значений?
    • 1
    • 20
    • 700,000
    • 1,000,000
  2. В массиве 7, 3, 9, 4, 6, и место ещё есть. Сколько шагов занимает вставка значения в начало (сдвиги плюс запись)?
  3. Тот же массив из 5 значений: сколько шагов занимает удаление первого значения (1 чтение плюс сдвиги)?
  4. Вставка на позицию 2 в массив из 5 значений (позиции от 0 до 4), место есть: сколько шагов?
  5. В удваивающемся массиве 5 значений, и его ёмкость 5. Ты добавляешь ещё одно значение. Сколько шагов занимает это добавление?
  6. Какая ёмкость после этого добавления?
  7. Если начать с ёмкости 1 и удваивать, сколько всего копирований сделают 17 добавлений?
  8. Почему добавление в удваивающийся массив называют амортизированным Θ(1)?
    • Каждое отдельное добавление занимает ровно один шаг
    • Копирования редки, поэтому сумма за n добавлений остаётся меньше константы, умноженной на n
    • Компьютер копирует в фоне бесплатно
    • JavaScript никогда не копирует массивы
  9. В терминах MIT 6.006 «insert last(x) и delete last()» является
    • структурой данных
    • интерфейсом
    • адресом в памяти
    • способом сортировки
  10. У массива длина 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