Files
hello-algo/ru/docs/chapter_array_and_linkedlist/array.md
T
2026-01-20 15:08:42 +08:00

16 KiB
Raw Blame History

Массивы

Массив представляет собой линейную структуру данных, в которой элементы одного типа хранятся в непрерывной области памяти. Положение элемента в массиве называется его индексом. На рисунке ниже изображены основные понятия и способ хранения массивов.

Определение и способ хранения массива

Основные операции с массивом

Инициализация массива

Существует два способа инициализации массива: без начальных значений и с заданными начальными значениями. Если начальные значения не указаны, большинство языков программирования инициализируют элементы массива нулями.

=== "Python"

```python title="array.py"
# Инициализация массива
arr: list[int] = [0] * 5  # [ 0, 0, 0, 0, 0 ]
nums: list[int] = [1, 3, 2, 5, 4]
```

=== "C++"

```cpp title="array.cpp"
/* Инициализация массива */
// Хранение в стеке
int arr[5];
int nums[5] = { 1, 3, 2, 5, 4 };
// Хранение в куче (требуется ручное освобождение памяти)
int* arr1 = new int[5];
int* nums1 = new int[5] { 1, 3, 2, 5, 4 };
```

=== "Java"

```java title="array.java"
/* Инициализация массива */
int[] arr = new int[5]; // { 0, 0, 0, 0, 0 }
int[] nums = { 1, 3, 2, 5, 4 };
```

=== "C#"

```csharp title="array.cs"
/* Инициализация массива */
int[] arr = new int[5]; // [ 0, 0, 0, 0, 0 ]
int[] nums = [1, 3, 2, 5, 4];
```

=== "Go"

```go title="array.go"
/* Инициализация массива */
var arr [5]int
// В Go при указании длины ([5]int) создается массив, без указания длины ([]int) - срез
// Поскольку массивы Go определяются на этапе компиляции, для указания длины можно использовать только константы
// Для удобства реализации метода extend() далее срез (Slice) рассматривается как массив (Array)
nums := []int{1, 3, 2, 5, 4}
```

=== "Swift"

```swift title="array.swift"
/* Инициализация массива */
let arr = Array(repeating: 0, count: 5) // [0, 0, 0, 0, 0]
let nums = [1, 3, 2, 5, 4]
```

=== "JS"

```javascript title="array.js"
/* Инициализация массива */
var arr = new Array(5).fill(0);
var nums = [1, 3, 2, 5, 4];
```

=== "TS"

```typescript title="array.ts"
/* Инициализация массива */
let arr: number[] = new Array(5).fill(0);
let nums: number[] = [1, 3, 2, 5, 4];
```

=== "Dart"

```dart title="array.dart"
/* Инициализация массива */
List<int> arr = List.filled(5, 0); // [0, 0, 0, 0, 0]
List<int> nums = [1, 3, 2, 5, 4];
```

=== "Rust"

```rust title="array.rs"
/* Инициализация массива */
let arr: [i32; 5] = [0; 5]; // [0, 0, 0, 0, 0]
let slice: &[i32] = &[0; 5];
// В Rust при указании длины ([i32; 5]) создается массив, без указания длины (&[i32]) - срез
// Поскольку массивы Rust определяются на этапе компиляции, для указания длины можно использовать только константы
// Vector - это тип, обычно используемый в Rust в качестве динамического массива
// Для удобства реализации метода extend() далее vector рассматривается как массив (array)
let nums: Vec<i32> = vec![1, 3, 2, 5, 4];
```

=== "C"

```c title="array.c"
/* Инициализация массива */
int arr[5] = { 0 }; // { 0, 0, 0, 0, 0 }
int nums[5] = { 1, 3, 2, 5, 4 };
```

=== "Kotlin"

```kotlin title="array.kt"
/* Инициализация массива */
var arr = IntArray(5) // { 0, 0, 0, 0, 0 }
var nums = intArrayOf(1, 3, 2, 5, 4)
```

=== "Ruby"

```ruby title="array.rb"
# Инициализация массива
arr = Array.new(5, 0)
nums = [1, 3, 2, 5, 4]
```

??? pythontutor "Визуализация выполнения"

https://pythontutor.com/render.html#code=%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0Aarr%20%3D%20%5B0%5D%20*%205%20%20%23%20%5B%200,%200,%200,%200,%200%20%5D%0Anums%20%3D%20%5B1,%203,%202,%205,%204%5D&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false

Доступ к элементам

Элементы массива хранятся в непрерывной области памяти, что упрощает вычисление их адресов. Зная адрес массива (адрес первого элемента) и индекс элемента, можно вычислить адрес этого элемента по формуле, показанной на рисунке ниже, и получить к нему прямой доступ.

Вычисление адреса элемента массива

Как видно из рисунка, индекс первого элемента массива равен 0, что может показаться неочевидным, так как отсчет с 1 кажется более естественным. Однако с точки зрения формулы вычисления адреса индекс является смещением адреса в памяти. Смещение адреса первого элемента равно 0, поэтому и его индекс равен 0.

Доступ к элементам массива осуществляется очень эффективно, так как позволяет за время O(1) произвольно обращаться к любому элементу.

[file]{array}-[class]{}-[func]{random_access}

Вставка элемента

Элементы массива в памяти расположены вплотную, между ними нет места для хранения других данных. Для вставки элемента в середину массива необходимо сдвинуть все последующие элементы на одну позицию вправо, а затем присвоить значение элементу по заданному индексу, как показано на рисунке ниже.

Пример вставки элемента в массив

Следует отметить, что длина массива фиксирована, поэтому вставка элемента неизбежно приведет к потере элемента в конце массива. Решение этой проблемы будет рассмотрено в разделе «Списки».

[file]{array}-[class]{}-[func]{insert}

Удаление элемента

Аналогично для удаления элемента по индексу i необходимо сдвинуть все последующие элементы на одну позицию влево, как показано на рисунке ниже.

Пример удаления элемента из массива

Обратите внимание, что после удаления элемента последний элемент становится бессмысленным, поэтому его можно не изменять.

[file]{array}-[class]{}-[func]{remove}

В целом операции вставки и удаления в массиве имеют следующие недостатки.

  • Высокая временная сложность: средняя временная сложность операций вставки и удаления в массиве составляет O(n), где n -- длина массива.
  • Потеря элементов: так как длина массива фиксирована, при вставке элемента элементы, выходящие за пределы длины массива, теряются.
  • Расточительность памяти: можно инициализировать длинный массив и использовать только его часть, но это приведет к потере памяти, так как неиспользуемые элементы в конце массива будут бессмысленными.

Обход массива

В большинстве языков программирования массив можно обходить как по индексам, так и непосредственно по элементам.

[file]{array}-[class]{}-[func]{traverse}

Поиск элемента

Для поиска заданного элемента в массиве необходимо обойти массив и на каждой итерации проверить, совпадает ли значение элемента с искомым. Если совпадает, вывести соответствующий индекс.

Поскольку массив является линейной структурой данных, этот процесс называется линейным поиском.

[file]{array}-[class]{}-[func]{find}

Расширение массива

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

Если необходимо увеличить массив, нужно создать новый, больший массив и последовательно скопировать в него элементы исходного массива. Эта операция имеет сложность O(n) и при больших массивах занимает много времени. Пример кода представлен ниже.

[file]{array}-[class]{}-[func]{extend}

Преимущества и ограничения массивов

Массивы хранятся в непрерывном пространстве памяти, а его элементы имеют одинаковый тип. Этот подход содержит богатую априорную информацию, которую система может использовать для оптимизации эффективности операций с данной структурой данных.

  • Высокая эффективность использования пространства: массивы выделяют непрерывные блоки памяти для данных без дополнительных структурных затрат.
  • Поддержка произвольного доступа: массивы позволяют получить доступ к любому элементу за время O(1).
  • Локальность кеширования: при доступе к элементам массива компьютер загружает не только его, но и кеширует окружающие данные, что позволяет ускорить выполнение последующих операций за счет использования высокоскоростного кеша.

Непрерывное хранение в пространстве -- это палка о двух концах, имеющая следующие ограничения.

  • Низкая эффективность вставки и удаления: при большом количестве элементов в массиве операции вставки и удаления требуют перемещения множества элементов.
  • Неизменная длина: после инициализации длина массива фиксируется, а увеличение массива требует копирования всех данных в новый массив, что влечет за собой значительные затраты.
  • Расточительность пространства: если размер выделенного массива превышает фактические потребности, избыточное пространство оказывается потраченным впустую.

Типичные сценарии применения массивов

Массивы -- это базовая и распространенная структура данных, часто используемая в различных алгоритмах и для реализации сложных структур данных.

  • Произвольный доступ: если требуется случайный выбор элементов, можно использовать массив для хранения и генерации случайной последовательности, осуществляя случайную выборку по индексу.
  • Сортировка и поиск: массивы являются наиболее часто используемой структурой данных для алгоритмов сортировки и поиска. Быстрая сортировка, сортировка слиянием, двоичный поиск и другие алгоритмы в основном работают с массивами.
  • Таблица поиска: когда необходимо быстро найти элемент или его соответствие, можно использовать массив в качестве таблицы поиска. Например, для реализации отображения символов в ASCII-коды можно использовать значение ASCII-кода символа в качестве индекса, а соответствующий элемент хранить в соответствующем месте массива.
  • Машинное обучение: в нейронных сетях широко используются операции линейной алгебры между векторами, матрицами и тензорами, которые реализуются в виде массивов. Массивы являются наиболее часто используемой структурой данных в программировании нейронных сетей.
  • Реализация структур данных: массивы могут использоваться для реализации стека, очереди, хеш-таблицы, кучи, графа и других структур данных. Например, представление графа в виде матрицы смежности фактически является двумерным массивом.