Add ru version (#1865)
* Add Russian docs site baseline * Add Russian localized codebase * Polish Russian code wording * Update ru code translation. * Update code translation and chapter covers. * Fix pythontutor extraction. * Add README and landing page. * placeholder of profiles * Use figures of English version * Remove chapter paperbook
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,227 @@
|
||||
# Массив
|
||||
|
||||
<u>Массив (array)</u> - это линейная структура данных, которая хранит элементы одного типа в непрерывной области памяти. Положение элемента в массиве называется его <u>индексом (index)</u>. На рисунке ниже показаны основные понятия, связанные с массивом, и способ его хранения.
|
||||
|
||||

|
||||
|
||||
## Основные операции с массивом
|
||||
|
||||
### Инициализация массива
|
||||
|
||||
В зависимости от задачи мы можем выбрать один из двух способов инициализации массива: без начальных значений или с заданными начальными значениями. Если начальные значения не указаны, большинство языков программирования инициализируют элементы массива значением $0$ :
|
||||
|
||||
=== "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%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2%0Aarr%20%3D%20%5B0%5D%20%2A%205%20%20%23%20%5B%200%2C%200%2C%200%2C%200%2C%200%20%5D%0Anums%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%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)$ времени.
|
||||
|
||||
```src
|
||||
[file]{array}-[class]{}-[func]{random_access}
|
||||
```
|
||||
|
||||
### Вставка элемента
|
||||
|
||||
Элементы массива в памяти расположены "вплотную" друг к другу, и между ними нет места для размещения новых данных. Как показано на рисунке ниже, если мы хотим вставить элемент в середину массива, то все элементы после этой позиции нужно сдвинуть на одну позицию вправо, а затем записать новое значение в освободившийся индекс.
|
||||
|
||||

|
||||
|
||||
Стоит отметить, что длина массива фиксирована, поэтому вставка нового элемента неизбежно приведет к "потере" элемента на конце массива. Решение этой проблемы мы оставим для обсуждения в разделе о "списках".
|
||||
|
||||
```src
|
||||
[file]{array}-[class]{}-[func]{insert}
|
||||
```
|
||||
|
||||
### Удаление элемента
|
||||
|
||||
Аналогично, как показано на рисунке ниже, если нужно удалить элемент по индексу $i$ , то все элементы после индекса $i$ необходимо сдвинуть на одну позицию влево.
|
||||
|
||||

|
||||
|
||||
Обрати внимание: после удаления исходный последний элемент становится "бессмысленным", поэтому специально изменять его не требуется.
|
||||
|
||||
```src
|
||||
[file]{array}-[class]{}-[func]{remove}
|
||||
```
|
||||
|
||||
В целом операции вставки и удаления в массиве имеют следующие недостатки.
|
||||
|
||||
- **Высокая временная сложность**: средняя временная сложность и вставки, и удаления равна $O(n)$ , где $n$ - длина массива.
|
||||
- **Потеря элементов**: поскольку длина массива неизменяема, после вставки элементы, выходящие за пределы длины массива, будут потеряны.
|
||||
- **Потери памяти**: можно заранее инициализировать более длинный массив и использовать только его переднюю часть; тогда "теряемые" при вставке элементы на конце не будут нести смысла, но такой подход приводит к лишнему расходу памяти.
|
||||
|
||||
### Обход массива
|
||||
|
||||
В большинстве языков программирования массив можно обходить как по индексу, так и напрямую перебирая каждый элемент:
|
||||
|
||||
```src
|
||||
[file]{array}-[class]{}-[func]{traverse}
|
||||
```
|
||||
|
||||
### Поиск элемента
|
||||
|
||||
Чтобы найти заданный элемент в массиве, нужно пройти по массиву и на каждой итерации проверять, совпадает ли значение; если совпадает, вернуть соответствующий индекс.
|
||||
|
||||
Поскольку массив - это линейная структура данных, такая операция поиска называется "линейным поиском".
|
||||
|
||||
```src
|
||||
[file]{array}-[class]{}-[func]{find}
|
||||
```
|
||||
|
||||
### Расширение массива
|
||||
|
||||
В сложной системной среде программа не может гарантировать, что память сразу после массива доступна, поэтому безопасно расширить емкость массива невозможно. Поэтому в большинстве языков программирования **длина массива неизменяема**.
|
||||
|
||||
Если мы хотим расширить массив, нужно заново создать больший массив и затем по одному скопировать в него элементы исходного массива. Это операция с временной сложностью $O(n)$ , и при больших массивах она очень затратна. Соответствующий код показан ниже:
|
||||
|
||||
```src
|
||||
[file]{array}-[class]{}-[func]{extend}
|
||||
```
|
||||
|
||||
## Преимущества и ограничения массива
|
||||
|
||||
Массив хранится в непрерывной области памяти, и все его элементы имеют один и тот же тип. Такой подход содержит много априорной информации, которую система может использовать для оптимизации эффективности операций со структурой данных.
|
||||
|
||||
- **Высокая пространственная эффективность**: массив выделяет для данных непрерывный блок памяти без дополнительного структурного накладного расхода.
|
||||
- **Поддержка произвольного доступа**: массив позволяет обращаться к любому элементу за $O(1)$ времени.
|
||||
- **Локальность кэша**: при обращении к элементу массива компьютер загружает не только сам элемент, но и соседние данные, что позволяет использовать кэш для ускорения последующих операций.
|
||||
|
||||
Хранение в непрерывной области памяти - палка о двух концах, и у него есть следующие ограничения.
|
||||
|
||||
- **Низкая эффективность вставки и удаления**: когда элементов в массиве много, вставка и удаление требуют сдвига большого количества элементов.
|
||||
- **Неизменяемая длина**: после инициализации длина массива фиксирована; расширение массива требует копирования всех данных в новый массив, что стоит дорого.
|
||||
- **Потери памяти**: если выделенный массив больше, чем реально необходимо, лишнее пространство пропадает впустую.
|
||||
|
||||
## Типичные применения массива
|
||||
|
||||
Массив - это базовая и очень распространенная структура данных. Он часто используется как в различных алгоритмах, так и при реализации более сложных структур данных.
|
||||
|
||||
- **Произвольный доступ**: если мы хотим случайным образом выбирать некоторые образцы, можно сохранить их в массиве и сгенерировать случайную последовательность индексов для выборки.
|
||||
- **Сортировка и поиск**: массив - самая распространенная структура данных для алгоритмов сортировки и поиска. Быстрая сортировка, сортировка слиянием, бинарный поиск и многие другие алгоритмы в основном работают именно с массивами.
|
||||
- **Таблица поиска**: когда нужно быстро находить элемент или его соответствие, массив можно использовать как lookup table. Например, если мы хотим реализовать отображение символов в коды ASCII, можно использовать значение ASCII как индекс, а соответствующий элемент хранить по этой позиции массива.
|
||||
- **Машинное обучение**: в нейронных сетях широко используются операции линейной алгебры над векторами, матрицами и тензорами, и все эти данные строятся в форме массивов. Массив - самая часто используемая структура данных в программировании нейросетей.
|
||||
- **Реализация структур данных**: массивы можно использовать для реализации стеков, очередей, хеш-таблиц, куч, графов и других структур данных. Например, матрица смежности графа по сути является двумерным массивом.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Массивы и списки
|
||||
|
||||

|
||||
|
||||
!!! abstract
|
||||
|
||||
Мир структур данных напоминает прочную кирпичную стену.
|
||||
|
||||
Кирпичи массива уложены ровно и плотно прилегают друг к другу. Кирпичи связного списка разбросаны в разных местах, а соединяющие их лозы свободно тянутся между щелями.
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,702 @@
|
||||
# Связный список
|
||||
|
||||
Память является общим ресурсом для всех программ, и в сложной среде выполнения свободные участки памяти могут быть разбросаны по всему адресу памяти. Мы знаем, что память для хранения массива должна быть непрерывной, а если массив очень велик, память может не суметь предоставить столь большой непрерывный блок. Именно здесь проявляется преимущество гибкости связного списка.
|
||||
|
||||
<u>Связный список (linked list)</u> - это линейная структура данных, в которой каждый элемент представляет собой объект-узел, а сами узлы соединены между собой через "ссылки". Ссылка хранит адрес памяти следующего узла, благодаря чему из текущего узла можно получить доступ к следующему.
|
||||
|
||||
Конструкция связного списка позволяет хранить отдельные узлы в разных местах памяти, и их адреса вовсе не обязаны быть непрерывными.
|
||||
|
||||

|
||||
|
||||
Если посмотреть на рисунок выше, можно заметить, что базовой единицей связного списка является объект <u>узел (node)</u>. Каждый узел содержит две части данных: "значение" узла и "ссылку" на следующий узел.
|
||||
|
||||
- Первый узел связного списка называется "головным узлом", а последний - "хвостовым узлом".
|
||||
- Хвостовой узел указывает на "пусто", что в Java, C++ и Python обозначается как `null` , `nullptr` и `None` соответственно.
|
||||
- В языках, поддерживающих указатели, таких как C, C++, Go и Rust, упомянутую выше "ссылку" следует заменить на "указатель".
|
||||
|
||||
Как показано в коде ниже, узел связного списка `ListNode` хранит не только значение, но и дополнительную ссылку (указатель). Поэтому **при одинаковом объеме данных связный список занимает больше памяти, чем массив**.
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title=""
|
||||
class ListNode:
|
||||
"""Класс узла связного списка"""
|
||||
def __init__(self, val: int):
|
||||
self.val: int = val # Значение узла
|
||||
self.next: ListNode | None = None # Ссылка на следующий узел
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title=""
|
||||
/* Структура узла связного списка */
|
||||
struct ListNode {
|
||||
int val; // Значение узла
|
||||
ListNode *next; // Указатель на следующий узел
|
||||
ListNode(int x) : val(x), next(nullptr) {} // Конструктор
|
||||
};
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title=""
|
||||
/* Класс узла связного списка */
|
||||
class ListNode {
|
||||
int val; // Значение узла
|
||||
ListNode next; // Ссылка на следующий узел
|
||||
ListNode(int x) { val = x; } // Конструктор
|
||||
}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title=""
|
||||
/* Класс узла связного списка */
|
||||
class ListNode(int x) { // Конструктор
|
||||
int val = x; // Значение узла
|
||||
ListNode? next; // Ссылка на следующий узел
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title=""
|
||||
/* Структура узла связного списка */
|
||||
type ListNode struct {
|
||||
Val int // Значение узла
|
||||
Next *ListNode // Указатель на следующий узел
|
||||
}
|
||||
|
||||
// NewListNode Конструктор, создает новый связный список
|
||||
func NewListNode(val int) *ListNode {
|
||||
return &ListNode{
|
||||
Val: val,
|
||||
Next: nil,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title=""
|
||||
/* Класс узла связного списка */
|
||||
class ListNode {
|
||||
var val: Int // Значение узла
|
||||
var next: ListNode? // Ссылка на следующий узел
|
||||
|
||||
init(x: Int) { // Конструктор
|
||||
val = x
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title=""
|
||||
/* Класс узла связного списка */
|
||||
class ListNode {
|
||||
constructor(val, next) {
|
||||
this.val = (val === undefined ? 0 : val); // Значение узла
|
||||
this.next = (next === undefined ? null : next); // Ссылка на следующий узел
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title=""
|
||||
/* Класс узла связного списка */
|
||||
class ListNode {
|
||||
val: number;
|
||||
next: ListNode | null;
|
||||
constructor(val?: number, next?: ListNode | null) {
|
||||
this.val = val === undefined ? 0 : val; // Значение узла
|
||||
this.next = next === undefined ? null : next; // Ссылка на следующий узел
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title=""
|
||||
/* Класс узла связного списка */
|
||||
class ListNode {
|
||||
int val; // Значение узла
|
||||
ListNode? next; // Ссылка на следующий узел
|
||||
ListNode(this.val, [this.next]); // Конструктор
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title=""
|
||||
use std::rc::Rc;
|
||||
use std::cell::RefCell;
|
||||
/* Класс узла связного списка */
|
||||
#[derive(Debug)]
|
||||
struct ListNode {
|
||||
val: i32, // Значение узла
|
||||
next: Option<Rc<RefCell<ListNode>>>, // Указатель на следующий узел
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title=""
|
||||
/* Структура узла связного списка */
|
||||
typedef struct ListNode {
|
||||
int val; // Значение узла
|
||||
struct ListNode *next; // Указатель на следующий узел
|
||||
} ListNode;
|
||||
|
||||
/* Конструктор */
|
||||
ListNode *newListNode(int val) {
|
||||
ListNode *node;
|
||||
node = (ListNode *) malloc(sizeof(ListNode));
|
||||
node->val = val;
|
||||
node->next = NULL;
|
||||
return node;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title=""
|
||||
/* Класс узла связного списка */
|
||||
// Конструктор
|
||||
class ListNode(x: Int) {
|
||||
val _val: Int = x // Значение узла
|
||||
val next: ListNode? = null // Ссылка на следующий узел
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title=""
|
||||
# Класс узла связного списка
|
||||
class ListNode
|
||||
attr_accessor :val # Значение узла
|
||||
attr_accessor :next # Ссылка на следующий узел
|
||||
|
||||
def initialize(val=0, next_node=nil)
|
||||
@val = val
|
||||
@next = next_node
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Основные операции со связным списком
|
||||
|
||||
### Инициализация связного списка
|
||||
|
||||
Построение связного списка состоит из двух шагов: сначала нужно инициализировать объекты всех узлов, затем установить связи-ссылки между ними. После завершения инициализации мы можем, начиная с головы списка, последовательно проходить все узлы по ссылке `next`.
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="linked_list.py"
|
||||
# Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4
|
||||
# Инициализация отдельных узлов
|
||||
n0 = ListNode(1)
|
||||
n1 = ListNode(3)
|
||||
n2 = ListNode(2)
|
||||
n3 = ListNode(5)
|
||||
n4 = ListNode(4)
|
||||
# Построение ссылок между узлами
|
||||
n0.next = n1
|
||||
n1.next = n2
|
||||
n2.next = n3
|
||||
n3.next = n4
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="linked_list.cpp"
|
||||
/* Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4 */
|
||||
// Инициализация отдельных узлов
|
||||
ListNode* n0 = new ListNode(1);
|
||||
ListNode* n1 = new ListNode(3);
|
||||
ListNode* n2 = new ListNode(2);
|
||||
ListNode* n3 = new ListNode(5);
|
||||
ListNode* n4 = new ListNode(4);
|
||||
// Построение ссылок между узлами
|
||||
n0->next = n1;
|
||||
n1->next = n2;
|
||||
n2->next = n3;
|
||||
n3->next = n4;
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="linked_list.java"
|
||||
/* Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4 */
|
||||
// Инициализация отдельных узлов
|
||||
ListNode n0 = new ListNode(1);
|
||||
ListNode n1 = new ListNode(3);
|
||||
ListNode n2 = new ListNode(2);
|
||||
ListNode n3 = new ListNode(5);
|
||||
ListNode n4 = new ListNode(4);
|
||||
// Построение ссылок между узлами
|
||||
n0.next = n1;
|
||||
n1.next = n2;
|
||||
n2.next = n3;
|
||||
n3.next = n4;
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="linked_list.cs"
|
||||
/* Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4 */
|
||||
// Инициализация отдельных узлов
|
||||
ListNode n0 = new(1);
|
||||
ListNode n1 = new(3);
|
||||
ListNode n2 = new(2);
|
||||
ListNode n3 = new(5);
|
||||
ListNode n4 = new(4);
|
||||
// Построение ссылок между узлами
|
||||
n0.next = n1;
|
||||
n1.next = n2;
|
||||
n2.next = n3;
|
||||
n3.next = n4;
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="linked_list.go"
|
||||
/* Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4 */
|
||||
// Инициализация отдельных узлов
|
||||
n0 := NewListNode(1)
|
||||
n1 := NewListNode(3)
|
||||
n2 := NewListNode(2)
|
||||
n3 := NewListNode(5)
|
||||
n4 := NewListNode(4)
|
||||
// Построение ссылок между узлами
|
||||
n0.Next = n1
|
||||
n1.Next = n2
|
||||
n2.Next = n3
|
||||
n3.Next = n4
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="linked_list.swift"
|
||||
/* Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4 */
|
||||
// Инициализация отдельных узлов
|
||||
let n0 = ListNode(x: 1)
|
||||
let n1 = ListNode(x: 3)
|
||||
let n2 = ListNode(x: 2)
|
||||
let n3 = ListNode(x: 5)
|
||||
let n4 = ListNode(x: 4)
|
||||
// Построение ссылок между узлами
|
||||
n0.next = n1
|
||||
n1.next = n2
|
||||
n2.next = n3
|
||||
n3.next = n4
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="linked_list.js"
|
||||
/* Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4 */
|
||||
// Инициализация отдельных узлов
|
||||
const n0 = new ListNode(1);
|
||||
const n1 = new ListNode(3);
|
||||
const n2 = new ListNode(2);
|
||||
const n3 = new ListNode(5);
|
||||
const n4 = new ListNode(4);
|
||||
// Построение ссылок между узлами
|
||||
n0.next = n1;
|
||||
n1.next = n2;
|
||||
n2.next = n3;
|
||||
n3.next = n4;
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="linked_list.ts"
|
||||
/* Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4 */
|
||||
// Инициализация отдельных узлов
|
||||
const n0 = new ListNode(1);
|
||||
const n1 = new ListNode(3);
|
||||
const n2 = new ListNode(2);
|
||||
const n3 = new ListNode(5);
|
||||
const n4 = new ListNode(4);
|
||||
// Построение ссылок между узлами
|
||||
n0.next = n1;
|
||||
n1.next = n2;
|
||||
n2.next = n3;
|
||||
n3.next = n4;
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="linked_list.dart"
|
||||
/* Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4 */\
|
||||
// Инициализация отдельных узлов
|
||||
ListNode n0 = ListNode(1);
|
||||
ListNode n1 = ListNode(3);
|
||||
ListNode n2 = ListNode(2);
|
||||
ListNode n3 = ListNode(5);
|
||||
ListNode n4 = ListNode(4);
|
||||
// Построение ссылок между узлами
|
||||
n0.next = n1;
|
||||
n1.next = n2;
|
||||
n2.next = n3;
|
||||
n3.next = n4;
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="linked_list.rs"
|
||||
/* Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4 */
|
||||
// Инициализация отдельных узлов
|
||||
let n0 = Rc::new(RefCell::new(ListNode { val: 1, next: None }));
|
||||
let n1 = Rc::new(RefCell::new(ListNode { val: 3, next: None }));
|
||||
let n2 = Rc::new(RefCell::new(ListNode { val: 2, next: None }));
|
||||
let n3 = Rc::new(RefCell::new(ListNode { val: 5, next: None }));
|
||||
let n4 = Rc::new(RefCell::new(ListNode { val: 4, next: None }));
|
||||
|
||||
// Построение ссылок между узлами
|
||||
n0.borrow_mut().next = Some(n1.clone());
|
||||
n1.borrow_mut().next = Some(n2.clone());
|
||||
n2.borrow_mut().next = Some(n3.clone());
|
||||
n3.borrow_mut().next = Some(n4.clone());
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="linked_list.c"
|
||||
/* Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4 */
|
||||
// Инициализация отдельных узлов
|
||||
ListNode* n0 = newListNode(1);
|
||||
ListNode* n1 = newListNode(3);
|
||||
ListNode* n2 = newListNode(2);
|
||||
ListNode* n3 = newListNode(5);
|
||||
ListNode* n4 = newListNode(4);
|
||||
// Построение ссылок между узлами
|
||||
n0->next = n1;
|
||||
n1->next = n2;
|
||||
n2->next = n3;
|
||||
n3->next = n4;
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="linked_list.kt"
|
||||
/* Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4 */
|
||||
// Инициализация отдельных узлов
|
||||
val n0 = ListNode(1)
|
||||
val n1 = ListNode(3)
|
||||
val n2 = ListNode(2)
|
||||
val n3 = ListNode(5)
|
||||
val n4 = ListNode(4)
|
||||
// Построение ссылок между узлами
|
||||
n0.next = n1;
|
||||
n1.next = n2;
|
||||
n2.next = n3;
|
||||
n3.next = n4;
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="linked_list.rb"
|
||||
# Инициализация связного списка 1 -> 3 -> 2 -> 5 -> 4
|
||||
# Инициализация отдельных узлов
|
||||
n0 = ListNode.new(1)
|
||||
n1 = ListNode.new(3)
|
||||
n2 = ListNode.new(2)
|
||||
n3 = ListNode.new(5)
|
||||
n4 = ListNode.new(4)
|
||||
# Построение ссылок между узлами
|
||||
n0.next = n1
|
||||
n1.next = n2
|
||||
n2.next = n3
|
||||
n3.next = n4
|
||||
```
|
||||
|
||||
??? pythontutor "Визуализация выполнения"
|
||||
|
||||
https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%D1%81%D0%B2%D1%8F%D0%B7%D0%BD%D1%8B%D0%B9%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%D1%83%D0%B7%D0%B5%D0%BB%D0%BA%D0%BB%D0%B0%D1%81%D1%81%22%22%22%0A%20%20%20%20def%20__init__%28self%2C%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%D0%97%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B5%20%D1%83%D0%B7%D0%BB%D0%B0%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%D0%A1%D1%81%D1%8B%D0%BB%D0%BA%D0%B0%20%D0%BD%D0%B0%20%D1%81%D0%BB%D0%B5%D0%B4%D1%83%D1%8E%D1%89%D0%B8%D0%B9%20%D1%83%D0%B7%D0%B5%D0%BB%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%81%D0%B2%D1%8F%D0%B7%D0%BD%D1%8B%D0%B9%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%201%20-%3E%203%20-%3E%202%20-%3E%205%20-%3E%204%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D0%BA%D0%B0%D0%B6%D0%B4%D1%8B%D0%B9%20%D1%83%D0%B7%D0%B5%D0%BB%0A%20%20%20%20n0%20%3D%20ListNode%281%29%0A%20%20%20%20n1%20%3D%20ListNode%283%29%0A%20%20%20%20n2%20%3D%20ListNode%282%29%0A%20%20%20%20n3%20%3D%20ListNode%285%29%0A%20%20%20%20n4%20%3D%20ListNode%284%29%0A%20%20%20%20%23%20%D0%9F%D0%BE%D1%81%D1%82%D1%80%D0%BE%D0%B8%D1%82%D1%8C%20%D1%81%D1%81%D1%8B%D0%BB%D0%BA%D0%B8%20%D0%BC%D0%B5%D0%B6%D0%B4%D1%83%20%D1%83%D0%B7%D0%BB%D0%B0%D0%BC%D0%B8%0A%20%20%20%20n0.next%20%3D%20n1%0A%20%20%20%20n1.next%20%3D%20n2%0A%20%20%20%20n2.next%20%3D%20n3%0A%20%20%20%20n3.next%20%3D%20n4&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
||||
|
||||
Массив в целом - это одна переменная: например, массив `nums` содержит элементы `nums[0]` , `nums[1]` и т.д. Связный список же состоит из множества независимых объектов-узлов. **Обычно в качестве обозначения всего связного списка используют головной узел**; например, в приведенном выше коде связный список можно обозначить как список `n0` .
|
||||
|
||||
### Вставка узла
|
||||
|
||||
Вставить узел в связный список очень легко. Как показано на рисунке ниже, предположим, что мы хотим вставить новый узел `P` между двумя соседними узлами `n0` и `n1` ; **для этого нужно изменить всего две ссылки (указателя)**, а временная сложность будет равна $O(1)$ .
|
||||
|
||||
Для сравнения: временная сложность вставки элемента в массив составляет $O(n)$ , и при большом объеме данных это неэффективно.
|
||||
|
||||

|
||||
|
||||
```src
|
||||
[file]{linked_list}-[class]{}-[func]{insert}
|
||||
```
|
||||
|
||||
### Удаление узла
|
||||
|
||||
Как показано на рисунке ниже, удалить узел из связного списка тоже очень удобно: **нужно изменить всего одну ссылку (указатель)**.
|
||||
|
||||
Обрати внимание: хотя после завершения операции удаления узел `P` все еще указывает на `n1` , при обходе связного списка до `P` уже нельзя добраться, а значит `P` больше не принадлежит данному списку.
|
||||
|
||||

|
||||
|
||||
```src
|
||||
[file]{linked_list}-[class]{}-[func]{remove}
|
||||
```
|
||||
|
||||
### Доступ к узлу
|
||||
|
||||
**Доступ к узлам в связном списке менее эффективен**. Как уже обсуждалось в предыдущем разделе, к любому элементу массива можно обратиться за $O(1)$ времени. Со связным списком это не так: программе нужно стартовать от головного узла и последовательно двигаться дальше, пока не будет найден целевой узел. То есть для доступа к $i$ -му узлу списка нужно выполнить $i - 1$ итераций, а временная сложность составляет $O(n)$ .
|
||||
|
||||
```src
|
||||
[file]{linked_list}-[class]{}-[func]{access}
|
||||
```
|
||||
|
||||
### Поиск узла
|
||||
|
||||
Выполни обход связного списка, найди в нем узел со значением `target` и верни индекс этого узла в списке. Этот процесс тоже относится к линейному поиску. Код выглядит следующим образом:
|
||||
|
||||
```src
|
||||
[file]{linked_list}-[class]{}-[func]{find}
|
||||
```
|
||||
|
||||
## Сравнение массива и связного списка
|
||||
|
||||
В таблице ниже обобщаются свойства массива и связного списка, а также сравнивается эффективность соответствующих операций. Поскольку они используют противоположные стратегии хранения, их свойства и эффективность операций тоже во многом противоположны.
|
||||
|
||||
<p align="center"> Таблица <id> Сравнение эффективности массива и связного списка </p>
|
||||
|
||||
| | Массив | Связный список |
|
||||
| ------------- | ---------------------------------------------- | ----------------------- |
|
||||
| Способ хранения | Непрерывная область памяти | Разрозненная область памяти |
|
||||
| Расширение емкости | Длина неизменяема | Гибкое расширение |
|
||||
| Эффективность памяти | Элементы занимают меньше памяти, но возможны потери пространства | Элементы занимают больше памяти |
|
||||
| Доступ к элементу | $O(1)$ | $O(n)$ |
|
||||
| Добавление элемента | $O(n)$ | $O(1)$ |
|
||||
| Удаление элемента | $O(n)$ | $O(1)$ |
|
||||
|
||||
## Основные типы связных списков
|
||||
|
||||
Как показано на рисунке ниже, существует три распространенных типа связных списков.
|
||||
|
||||
- **Односвязный список**: это обычный связный список, рассмотренный выше. Узел односвязного списка содержит значение и ссылку на следующий узел. Первый узел называется головным, последний - хвостовым, и хвост указывает на пусто `None` .
|
||||
- **Циклический список**: если заставить хвостовой узел односвязного списка указывать на головной (то есть соединить хвост с головой), получится циклический список. В циклическом списке любой узел можно рассматривать как головной.
|
||||
- **Двусвязный список**: по сравнению с односвязным списком двусвязный хранит ссылки в двух направлениях. Определение узла двусвязного списка включает как ссылку на следующий узел, так и ссылку на предыдущий узел. По сравнению с односвязным списком двусвязный более гибок и позволяет проходить список в обе стороны, но за это приходится платить дополнительной памятью.
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title=""
|
||||
class ListNode:
|
||||
"""Класс узла двусвязного списка"""
|
||||
def __init__(self, val: int):
|
||||
self.val: int = val # Значение узла
|
||||
self.next: ListNode | None = None # Ссылка на следующий узел
|
||||
self.prev: ListNode | None = None # Ссылка на предыдущий узел
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title=""
|
||||
/* Структура узла двусвязного списка */
|
||||
struct ListNode {
|
||||
int val; // Значение узла
|
||||
ListNode *next; // Указатель на следующий узел
|
||||
ListNode *prev; // Указатель на предыдущий узел
|
||||
ListNode(int x) : val(x), next(nullptr), prev(nullptr) {} // Конструктор
|
||||
};
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title=""
|
||||
/* Класс узла двусвязного списка */
|
||||
class ListNode {
|
||||
int val; // Значение узла
|
||||
ListNode next; // Ссылка на следующий узел
|
||||
ListNode prev; // Ссылка на предыдущий узел
|
||||
ListNode(int x) { val = x; } // Конструктор
|
||||
}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title=""
|
||||
/* Класс узла двусвязного списка */
|
||||
class ListNode(int x) { // Конструктор
|
||||
int val = x; // Значение узла
|
||||
ListNode next; // Ссылка на следующий узел
|
||||
ListNode prev; // Ссылка на предыдущий узел
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title=""
|
||||
/* Структура узла двусвязного списка */
|
||||
type DoublyListNode struct {
|
||||
Val int // Значение узла
|
||||
Next *DoublyListNode // Указатель на следующий узел
|
||||
Prev *DoublyListNode // Указатель на предыдущий узел
|
||||
}
|
||||
|
||||
// NewDoublyListNode Инициализация
|
||||
func NewDoublyListNode(val int) *DoublyListNode {
|
||||
return &DoublyListNode{
|
||||
Val: val,
|
||||
Next: nil,
|
||||
Prev: nil,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title=""
|
||||
/* Класс узла двусвязного списка */
|
||||
class ListNode {
|
||||
var val: Int // Значение узла
|
||||
var next: ListNode? // Ссылка на следующий узел
|
||||
var prev: ListNode? // Ссылка на предыдущий узел
|
||||
|
||||
init(x: Int) { // Конструктор
|
||||
val = x
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title=""
|
||||
/* Класс узла двусвязного списка */
|
||||
class ListNode {
|
||||
constructor(val, next, prev) {
|
||||
this.val = val === undefined ? 0 : val; // Значение узла
|
||||
this.next = next === undefined ? null : next; // Ссылка на следующий узел
|
||||
this.prev = prev === undefined ? null : prev; // Ссылка на предыдущий узел
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title=""
|
||||
/* Класс узла двусвязного списка */
|
||||
class ListNode {
|
||||
val: number;
|
||||
next: ListNode | null;
|
||||
prev: ListNode | null;
|
||||
constructor(val?: number, next?: ListNode | null, prev?: ListNode | null) {
|
||||
this.val = val === undefined ? 0 : val; // Значение узла
|
||||
this.next = next === undefined ? null : next; // Ссылка на следующий узел
|
||||
this.prev = prev === undefined ? null : prev; // Ссылка на предыдущий узел
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title=""
|
||||
/* Класс узла двусвязного списка */
|
||||
class ListNode {
|
||||
int val; // Значение узла
|
||||
ListNode? next; // Ссылка на следующий узел
|
||||
ListNode? prev; // Ссылка на предыдущий узел
|
||||
ListNode(this.val, [this.next, this.prev]); // Конструктор
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title=""
|
||||
use std::rc::Rc;
|
||||
use std::cell::RefCell;
|
||||
|
||||
/* Тип узла двусвязного списка */
|
||||
#[derive(Debug)]
|
||||
struct ListNode {
|
||||
val: i32, // Значение узла
|
||||
next: Option<Rc<RefCell<ListNode>>>, // Указатель на следующий узел
|
||||
prev: Option<Rc<RefCell<ListNode>>>, // Указатель на предыдущий узел
|
||||
}
|
||||
|
||||
/* Конструктор */
|
||||
impl ListNode {
|
||||
fn new(val: i32) -> Self {
|
||||
ListNode {
|
||||
val,
|
||||
next: None,
|
||||
prev: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title=""
|
||||
/* Структура узла двусвязного списка */
|
||||
typedef struct ListNode {
|
||||
int val; // Значение узла
|
||||
struct ListNode *next; // Указатель на следующий узел
|
||||
struct ListNode *prev; // Указатель на предыдущий узел
|
||||
} ListNode;
|
||||
|
||||
/* Конструктор */
|
||||
ListNode *newListNode(int val) {
|
||||
ListNode *node;
|
||||
node = (ListNode *) malloc(sizeof(ListNode));
|
||||
node->val = val;
|
||||
node->next = NULL;
|
||||
node->prev = NULL;
|
||||
return node;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title=""
|
||||
/* Класс узла двусвязного списка */
|
||||
// Конструктор
|
||||
class ListNode(x: Int) {
|
||||
val _val: Int = x // Значение узла
|
||||
val next: ListNode? = null // Ссылка на следующий узел
|
||||
val prev: ListNode? = null // Ссылка на предыдущий узел
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title=""
|
||||
# Класс узла двусвязного списка
|
||||
class ListNode
|
||||
attr_accessor :val # Значение узла
|
||||
attr_accessor :next # Ссылка на следующий узел
|
||||
attr_accessor :prev # Ссылка на предыдущий узел
|
||||
|
||||
def initialize(val=0, next_node=nil, prev_node=nil)
|
||||
@val = val
|
||||
@next = next_node
|
||||
@prev = prev_node
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Типичные применения связных списков
|
||||
|
||||
Односвязные списки обычно используются для реализации стеков, очередей, хеш-таблиц и графов.
|
||||
|
||||
- **Стеки и очереди**: если операции вставки и удаления выполняются на одном конце связного списка, он проявляет свойства LIFO, соответствующие стеку; если вставка происходит на одном конце, а удаление на другом, он проявляет свойства FIFO, соответствующие очереди.
|
||||
- **Хеш-таблицы**: метод цепочек - один из основных способов разрешения коллизий в хеш-таблицах. В этом подходе все конфликтующие элементы помещаются в связный список.
|
||||
- **Графы**: список смежности - это распространенный способ представления графа, при котором каждой вершине графа соответствует связный список, а каждый элемент этого списка представляет другую вершину, соединенную с данной.
|
||||
|
||||
Двусвязные списки обычно используются там, где нужен быстрый доступ как к предыдущему, так и к следующему элементу.
|
||||
|
||||
- **Продвинутые структуры данных**: например, в красно-черных деревьях и B-деревьях нам нужен доступ к родительскому узлу; этого можно добиться, сохранив в узле ссылку на родителя, по аналогии с двусвязным списком.
|
||||
- **История браузера**: когда пользователь в браузере нажимает кнопки "вперед" или "назад", браузеру нужно знать предыдущую и следующую веб-страницы, которые он посещал. Свойства двусвязного списка делают такую операцию простой.
|
||||
- **Алгоритм LRU**: в алгоритмах вытеснения из кэша (LRU) нужно быстро находить наименее недавно использованные данные, а также быстро добавлять и удалять узлы. Для этого двусвязный список подходит очень хорошо.
|
||||
|
||||
Циклические списки часто применяются в сценариях, требующих периодических операций, например при планировании ресурсов в операционной системе.
|
||||
|
||||
- **Алгоритм циклического распределения кванта времени**: в операционных системах round-robin scheduling - это распространенный алгоритм планирования CPU, который циклически обходит набор процессов. Каждому процессу выделяется квант времени, и когда он исчерпан, CPU переключается на следующий процесс. Такую циклическую операцию удобно реализовать с помощью кольцевого списка.
|
||||
- **Буферы данных**: в некоторых реализациях буферов данных также могут использоваться циклические списки. Например, в аудио- и видеоплеерах поток данных может делиться на несколько буферных блоков и помещаться в кольцевой список для обеспечения непрерывного воспроизведения.
|
||||
@@ -0,0 +1,961 @@
|
||||
# Список
|
||||
|
||||
<u>Список (list)</u> - это абстрактное понятие структуры данных, обозначающее упорядоченную коллекцию элементов, которая поддерживает доступ к элементам, их изменение, добавление, удаление и обход, не требуя от пользователя учитывать ограничения по емкости. Список может быть реализован как на основе связного списка, так и на основе массива.
|
||||
|
||||
- Связный список естественным образом можно рассматривать как список: он поддерживает операции добавления, удаления, поиска и изменения элементов и может гибко расширяться динамически.
|
||||
- Массив тоже поддерживает операции добавления, удаления, поиска и изменения элементов, но из-за неизменяемости длины его можно считать лишь списком с ограниченной длиной.
|
||||
|
||||
Когда список реализуется с помощью массива, **неизменяемость длины снижает его практическую полезность**. Причина в том, что мы обычно не можем заранее точно знать, сколько данных нужно хранить, а значит, трудно выбрать подходящую длину списка. Если длина слишком мала, она может не покрыть реальные потребности; если слишком велика, будет зря расходоваться память.
|
||||
|
||||
Чтобы решить эту проблему, можно использовать <u>динамический массив (dynamic array)</u> для реализации списка. Он сохраняет все преимущества массива и при этом может динамически расширяться во время выполнения программы.
|
||||
|
||||
На практике **списки из стандартных библиотек многих языков программирования реализованы именно на основе динамических массивов**, например `list` в Python, `ArrayList` в Java, `vector` в C++ и `List` в C#. В дальнейшем обсуждении мы будем считать понятия "список" и "динамический массив" эквивалентными.
|
||||
|
||||
## Основные операции со списком
|
||||
|
||||
### Инициализация списка
|
||||
|
||||
Обычно мы используем два способа инициализации: "без начальных значений" и "с начальными значениями":
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="list.py"
|
||||
# Инициализация списка
|
||||
# Без начальных значений
|
||||
nums1: list[int] = []
|
||||
# С начальными значениями
|
||||
nums: list[int] = [1, 3, 2, 5, 4]
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="list.cpp"
|
||||
/* Инициализация списка */
|
||||
// Обрати внимание: в C++ vector соответствует описываемому здесь nums
|
||||
// Без начальных значений
|
||||
vector<int> nums1;
|
||||
// С начальными значениями
|
||||
vector<int> nums = { 1, 3, 2, 5, 4 };
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="list.java"
|
||||
/* Инициализация списка */
|
||||
// Без начальных значений
|
||||
List<Integer> nums1 = new ArrayList<>();
|
||||
// С начальными значениями (обрати внимание: элементы массива должны использовать обертку Integer[] вместо int[])
|
||||
Integer[] numbers = new Integer[] { 1, 3, 2, 5, 4 };
|
||||
List<Integer> nums = new ArrayList<>(Arrays.asList(numbers));
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="list.cs"
|
||||
/* Инициализация списка */
|
||||
// Без начальных значений
|
||||
List<int> nums1 = [];
|
||||
// С начальными значениями
|
||||
int[] numbers = [1, 3, 2, 5, 4];
|
||||
List<int> nums = [.. numbers];
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="list_test.go"
|
||||
/* Инициализация списка */
|
||||
// Без начальных значений
|
||||
nums1 := []int{}
|
||||
// С начальными значениями
|
||||
nums := []int{1, 3, 2, 5, 4}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="list.swift"
|
||||
/* Инициализация списка */
|
||||
// Без начальных значений
|
||||
let nums1: [Int] = []
|
||||
// С начальными значениями
|
||||
var nums = [1, 3, 2, 5, 4]
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="list.js"
|
||||
/* Инициализация списка */
|
||||
// Без начальных значений
|
||||
const nums1 = [];
|
||||
// С начальными значениями
|
||||
const nums = [1, 3, 2, 5, 4];
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="list.ts"
|
||||
/* Инициализация списка */
|
||||
// Без начальных значений
|
||||
const nums1: number[] = [];
|
||||
// С начальными значениями
|
||||
const nums: number[] = [1, 3, 2, 5, 4];
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="list.dart"
|
||||
/* Инициализация списка */
|
||||
// Без начальных значений
|
||||
List<int> nums1 = [];
|
||||
// С начальными значениями
|
||||
List<int> nums = [1, 3, 2, 5, 4];
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="list.rs"
|
||||
/* Инициализация списка */
|
||||
// Без начальных значений
|
||||
let nums1: Vec<i32> = Vec::new();
|
||||
// С начальными значениями
|
||||
let nums: Vec<i32> = vec![1, 3, 2, 5, 4];
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="list.c"
|
||||
// В C нет встроенного динамического массива
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="list.kt"
|
||||
/* Инициализация списка */
|
||||
// Без начальных значений
|
||||
var nums1 = listOf<Int>()
|
||||
// С начальными значениями
|
||||
var numbers = arrayOf(1, 3, 2, 5, 4)
|
||||
var nums = numbers.toMutableList()
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="list.rb"
|
||||
# Инициализация списка
|
||||
# Без начальных значений
|
||||
nums1 = []
|
||||
# С начальными значениями
|
||||
nums = [1, 3, 2, 5, 4]
|
||||
```
|
||||
|
||||
??? pythontutor "Визуализация выполнения"
|
||||
|
||||
https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%0A%20%20%20%20%23%20%D0%91%D0%B5%D0%B7%20%D0%BD%D0%B0%D1%87%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D1%85%20%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D0%B9%0A%20%20%20%20nums1%20%3D%20%5B%5D%0A%20%20%20%20%23%20%D0%A1%20%D0%BD%D0%B0%D1%87%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%BC%D0%B8%20%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D1%8F%D0%BC%D0%B8%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D&cumulative=false&curInstr=4&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
||||
|
||||
### Доступ к элементам
|
||||
|
||||
Список по своей сути является массивом, поэтому доступ к элементам и их обновление можно выполнять за $O(1)$ времени, что очень эффективно.
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="list.py"
|
||||
# Доступ к элементу
|
||||
num: int = nums[1] # Доступ к элементу по индексу 1
|
||||
|
||||
# Обновление элемента
|
||||
nums[1] = 0 # Обновить элемент по индексу 1 значением 0
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="list.cpp"
|
||||
/* Доступ к элементу */
|
||||
int num = nums[1]; // Доступ к элементу по индексу 1
|
||||
|
||||
/* Обновление элемента */
|
||||
nums[1] = 0; // Обновить элемент по индексу 1 значением 0
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="list.java"
|
||||
/* Доступ к элементу */
|
||||
int num = nums.get(1); // Доступ к элементу по индексу 1
|
||||
|
||||
/* Обновление элемента */
|
||||
nums.set(1, 0); // Обновить элемент по индексу 1 значением 0
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="list.cs"
|
||||
/* Доступ к элементу */
|
||||
int num = nums[1]; // Доступ к элементу по индексу 1
|
||||
|
||||
/* Обновление элемента */
|
||||
nums[1] = 0; // Обновить элемент по индексу 1 значением 0
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="list_test.go"
|
||||
/* Доступ к элементу */
|
||||
num := nums[1] // Доступ к элементу по индексу 1
|
||||
|
||||
/* Обновление элемента */
|
||||
nums[1] = 0 // Обновить элемент по индексу 1 значением 0
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="list.swift"
|
||||
/* Доступ к элементу */
|
||||
let num = nums[1] // Доступ к элементу по индексу 1
|
||||
|
||||
/* Обновление элемента */
|
||||
nums[1] = 0 // Обновить элемент по индексу 1 значением 0
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="list.js"
|
||||
/* Доступ к элементу */
|
||||
const num = nums[1]; // Доступ к элементу по индексу 1
|
||||
|
||||
/* Обновление элемента */
|
||||
nums[1] = 0; // Обновить элемент по индексу 1 значением 0
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="list.ts"
|
||||
/* Доступ к элементу */
|
||||
const num: number = nums[1]; // Доступ к элементу по индексу 1
|
||||
|
||||
/* Обновление элемента */
|
||||
nums[1] = 0; // Обновить элемент по индексу 1 значением 0
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="list.dart"
|
||||
/* Доступ к элементу */
|
||||
int num = nums[1]; // Доступ к элементу по индексу 1
|
||||
|
||||
/* Обновление элемента */
|
||||
nums[1] = 0; // Обновить элемент по индексу 1 значением 0
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="list.rs"
|
||||
/* Доступ к элементу */
|
||||
let num: i32 = nums[1]; // Доступ к элементу по индексу 1
|
||||
/* Обновление элемента */
|
||||
nums[1] = 0; // Обновить элемент по индексу 1 значением 0
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="list.c"
|
||||
// В C нет встроенного динамического массива
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="list.kt"
|
||||
/* Доступ к элементу */
|
||||
val num = nums[1] // Доступ к элементу по индексу 1
|
||||
/* Обновление элемента */
|
||||
nums[1] = 0 // Обновить элемент по индексу 1 значением 0
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="list.rb"
|
||||
# Доступ к элементу
|
||||
num = nums[1] # Доступ к элементу по индексу 1
|
||||
# Обновление элемента
|
||||
nums[1] = 0 # Обновить элемент по индексу 1 значением 0
|
||||
```
|
||||
|
||||
??? pythontutor "Визуализация выполнения"
|
||||
|
||||
https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D%0A%0A%20%20%20%20%23%20%D0%9F%D0%BE%D0%BB%D1%83%D1%87%D0%B8%D1%82%D1%8C%20%D0%B4%D0%BE%D1%81%D1%82%D1%83%D0%BF%20%D0%BA%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D1%83%0A%20%20%20%20num%20%3D%20nums%5B1%5D%20%20%23%20%D0%BE%D0%B1%D1%80%D0%B0%D1%82%D0%B8%D1%82%D1%8C%D1%81%D1%8F%20%D0%BA%D0%B8%D0%BD%D0%B4%D0%B5%D0%BA%D1%81%201%20%D0%BF%D0%BE%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%0A%0A%20%20%20%20%23%20%D0%9E%D0%B1%D0%BD%D0%BE%D0%B2%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%0A%20%20%20%20nums%5B1%5D%20%3D%200%20%20%20%20%23%20%D0%9E%D0%B1%D0%BD%D0%BE%D0%B2%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%BF%D0%BE%20%D0%B8%D0%BD%D0%B4%D0%B5%D0%BA%D1%81%D1%83%201%20%D0%B4%D0%BE%200&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
||||
|
||||
### Вставка и удаление элементов
|
||||
|
||||
По сравнению с массивами список позволяет свободно добавлять и удалять элементы. Добавление элемента в конец списка имеет временную сложность $O(1)$ , но операции вставки и удаления по-прежнему имеют ту же эффективность, что и у массива, то есть $O(n)$ .
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="list.py"
|
||||
# Очистить список
|
||||
nums.clear()
|
||||
|
||||
# Добавить элементы в конец
|
||||
nums.append(1)
|
||||
nums.append(3)
|
||||
nums.append(2)
|
||||
nums.append(5)
|
||||
nums.append(4)
|
||||
|
||||
# Вставить элемент в середину
|
||||
nums.insert(3, 6) # Вставить число 6 по индексу 3
|
||||
|
||||
# Удалить элемент
|
||||
nums.pop(3) # Удалить элемент по индексу 3
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="list.cpp"
|
||||
/* Очистить список */
|
||||
nums.clear();
|
||||
|
||||
/* Добавить элементы в конец */
|
||||
nums.push_back(1);
|
||||
nums.push_back(3);
|
||||
nums.push_back(2);
|
||||
nums.push_back(5);
|
||||
nums.push_back(4);
|
||||
|
||||
/* Вставить элемент в середину */
|
||||
nums.insert(nums.begin() + 3, 6); // Вставить число 6 по индексу 3
|
||||
|
||||
/* Удалить элемент */
|
||||
nums.erase(nums.begin() + 3); // Удалить элемент по индексу 3
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="list.java"
|
||||
/* Очистить список */
|
||||
nums.clear();
|
||||
|
||||
/* Добавить элементы в конец */
|
||||
nums.add(1);
|
||||
nums.add(3);
|
||||
nums.add(2);
|
||||
nums.add(5);
|
||||
nums.add(4);
|
||||
|
||||
/* Вставить элемент в середину */
|
||||
nums.add(3, 6); // Вставить число 6 по индексу 3
|
||||
|
||||
/* Удалить элемент */
|
||||
nums.remove(3); // Удалить элемент по индексу 3
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="list.cs"
|
||||
/* Очистить список */
|
||||
nums.Clear();
|
||||
|
||||
/* Добавить элементы в конец */
|
||||
nums.Add(1);
|
||||
nums.Add(3);
|
||||
nums.Add(2);
|
||||
nums.Add(5);
|
||||
nums.Add(4);
|
||||
|
||||
/* Вставить элемент в середину */
|
||||
nums.Insert(3, 6); // Вставить число 6 по индексу 3
|
||||
|
||||
/* Удалить элемент */
|
||||
nums.RemoveAt(3); // Удалить элемент по индексу 3
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="list_test.go"
|
||||
/* Очистить список */
|
||||
nums = nil
|
||||
|
||||
/* Добавить элементы в конец */
|
||||
nums = append(nums, 1)
|
||||
nums = append(nums, 3)
|
||||
nums = append(nums, 2)
|
||||
nums = append(nums, 5)
|
||||
nums = append(nums, 4)
|
||||
|
||||
/* Вставить элемент в середину */
|
||||
nums = append(nums[:3], append([]int{6}, nums[3:]...)...) // Вставить число 6 по индексу 3
|
||||
|
||||
/* Удалить элемент */
|
||||
nums = append(nums[:3], nums[4:]...) // Удалить элемент по индексу 3
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="list.swift"
|
||||
/* Очистить список */
|
||||
nums.removeAll()
|
||||
|
||||
/* Добавить элементы в конец */
|
||||
nums.append(1)
|
||||
nums.append(3)
|
||||
nums.append(2)
|
||||
nums.append(5)
|
||||
nums.append(4)
|
||||
|
||||
/* Вставить элемент в середину */
|
||||
nums.insert(6, at: 3) // Вставить число 6 по индексу 3
|
||||
|
||||
/* Удалить элемент */
|
||||
nums.remove(at: 3) // Удалить элемент по индексу 3
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="list.js"
|
||||
/* Очистить список */
|
||||
nums.length = 0;
|
||||
|
||||
/* Добавить элементы в конец */
|
||||
nums.push(1);
|
||||
nums.push(3);
|
||||
nums.push(2);
|
||||
nums.push(5);
|
||||
nums.push(4);
|
||||
|
||||
/* Вставить элемент в середину */
|
||||
nums.splice(3, 0, 6); // Вставить число 6 по индексу 3
|
||||
|
||||
/* Удалить элемент */
|
||||
nums.splice(3, 1); // Удалить элемент по индексу 3
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="list.ts"
|
||||
/* Очистить список */
|
||||
nums.length = 0;
|
||||
|
||||
/* Добавить элементы в конец */
|
||||
nums.push(1);
|
||||
nums.push(3);
|
||||
nums.push(2);
|
||||
nums.push(5);
|
||||
nums.push(4);
|
||||
|
||||
/* Вставить элемент в середину */
|
||||
nums.splice(3, 0, 6); // Вставить число 6 по индексу 3
|
||||
|
||||
/* Удалить элемент */
|
||||
nums.splice(3, 1); // Удалить элемент по индексу 3
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="list.dart"
|
||||
/* Очистить список */
|
||||
nums.clear();
|
||||
|
||||
/* Добавить элементы в конец */
|
||||
nums.add(1);
|
||||
nums.add(3);
|
||||
nums.add(2);
|
||||
nums.add(5);
|
||||
nums.add(4);
|
||||
|
||||
/* Вставить элемент в середину */
|
||||
nums.insert(3, 6); // Вставить число 6 по индексу 3
|
||||
|
||||
/* Удалить элемент */
|
||||
nums.removeAt(3); // Удалить элемент по индексу 3
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="list.rs"
|
||||
/* Очистить список */
|
||||
nums.clear();
|
||||
|
||||
/* Добавить элементы в конец */
|
||||
nums.push(1);
|
||||
nums.push(3);
|
||||
nums.push(2);
|
||||
nums.push(5);
|
||||
nums.push(4);
|
||||
|
||||
/* Вставить элемент в середину */
|
||||
nums.insert(3, 6); // Вставить число 6 по индексу 3
|
||||
|
||||
/* Удалить элемент */
|
||||
nums.remove(3); // Удалить элемент по индексу 3
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="list.c"
|
||||
// В C нет встроенного динамического массива
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="list.kt"
|
||||
/* Очистить список */
|
||||
nums.clear();
|
||||
|
||||
/* Добавить элементы в конец */
|
||||
nums.add(1);
|
||||
nums.add(3);
|
||||
nums.add(2);
|
||||
nums.add(5);
|
||||
nums.add(4);
|
||||
|
||||
/* Вставить элемент в середину */
|
||||
nums.add(3, 6); // Вставить число 6 по индексу 3
|
||||
|
||||
/* Удалить элемент */
|
||||
nums.remove(3); // Удалить элемент по индексу 3
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="list.rb"
|
||||
# Очистить список
|
||||
nums.clear
|
||||
|
||||
# Добавить элементы в конец
|
||||
nums << 1
|
||||
nums << 3
|
||||
nums << 2
|
||||
nums << 5
|
||||
nums << 4
|
||||
|
||||
# Вставить элемент в середину
|
||||
nums.insert(3, 6) # Вставить число 6 по индексу 3
|
||||
|
||||
# Удалить элемент
|
||||
nums.delete_at(3) # Удалить элемент по индексу 3
|
||||
```
|
||||
|
||||
??? pythontutor "Визуализация выполнения"
|
||||
|
||||
https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%A1%20%D0%BD%D0%B0%D1%87%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%BC%D0%B8%20%D0%B7%D0%BD%D0%B0%D1%87%D0%B5%D0%BD%D0%B8%D1%8F%D0%BC%D0%B8%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9E%D1%87%D0%B8%D1%81%D1%82%D0%B8%D1%82%D1%8C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%0A%20%20%20%20nums.clear%28%29%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%94%D0%BE%D0%B1%D0%B0%D0%B2%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D0%BA%D0%BE%D0%BD%D0%B5%D1%86%0A%20%20%20%20nums.append%281%29%0A%20%20%20%20nums.append%283%29%0A%20%20%20%20nums.append%282%29%0A%20%20%20%20nums.append%285%29%0A%20%20%20%20nums.append%284%29%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%92%D1%81%D1%82%D0%B0%D0%B2%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%B2%20%D1%81%D0%B5%D1%80%D0%B5%D0%B4%D0%B8%D0%BD%D1%83%0A%20%20%20%20nums.insert%283%2C%206%29%20%20%23%20%D0%92%D1%81%D1%82%D0%B0%D0%B2%D0%B8%D1%82%D1%8C%20%D1%87%D0%B8%D1%81%D0%BB%D0%BE%206%20%D0%BF%D0%BE%20%D0%B8%D0%BD%D0%B4%D0%B5%D0%BA%D1%81%D1%83%203%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%A3%D0%B4%D0%B0%D0%BB%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%0A%20%20%20%20nums.pop%283%29%20%20%20%20%20%20%20%20%23%20%D0%A3%D0%B4%D0%B0%D0%BB%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%20%D0%BF%D0%BE%20%D0%B8%D0%BD%D0%B4%D0%B5%D0%BA%D1%81%D1%83%203&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
||||
|
||||
### Обход списка
|
||||
|
||||
Как и массив, список можно обходить как по индексам, так и напрямую по элементам.
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="list.py"
|
||||
# Обход списка по индексам
|
||||
count = 0
|
||||
for i in range(len(nums)):
|
||||
count += nums[i]
|
||||
|
||||
# Прямой обход элементов списка
|
||||
for num in nums:
|
||||
count += num
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="list.cpp"
|
||||
/* Обход списка по индексам */
|
||||
int count = 0;
|
||||
for (int i = 0; i < nums.size(); i++) {
|
||||
count += nums[i];
|
||||
}
|
||||
|
||||
/* Прямой обход элементов списка */
|
||||
count = 0;
|
||||
for (int num : nums) {
|
||||
count += num;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="list.java"
|
||||
/* Обход списка по индексам */
|
||||
int count = 0;
|
||||
for (int i = 0; i < nums.size(); i++) {
|
||||
count += nums.get(i);
|
||||
}
|
||||
|
||||
/* Прямой обход элементов списка */
|
||||
for (int num : nums) {
|
||||
count += num;
|
||||
}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="list.cs"
|
||||
/* Обход списка по индексам */
|
||||
int count = 0;
|
||||
for (int i = 0; i < nums.Count; i++) {
|
||||
count += nums[i];
|
||||
}
|
||||
|
||||
/* Прямой обход элементов списка */
|
||||
count = 0;
|
||||
foreach (int num in nums) {
|
||||
count += num;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="list_test.go"
|
||||
/* Обход списка по индексам */
|
||||
count := 0
|
||||
for i := 0; i < len(nums); i++ {
|
||||
count += nums[i]
|
||||
}
|
||||
|
||||
/* Прямой обход элементов списка */
|
||||
count = 0
|
||||
for _, num := range nums {
|
||||
count += num
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="list.swift"
|
||||
/* Обход списка по индексам */
|
||||
var count = 0
|
||||
for i in nums.indices {
|
||||
count += nums[i]
|
||||
}
|
||||
|
||||
/* Прямой обход элементов списка */
|
||||
count = 0
|
||||
for num in nums {
|
||||
count += num
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="list.js"
|
||||
/* Обход списка по индексам */
|
||||
let count = 0;
|
||||
for (let i = 0; i < nums.length; i++) {
|
||||
count += nums[i];
|
||||
}
|
||||
|
||||
/* Прямой обход элементов списка */
|
||||
count = 0;
|
||||
for (const num of nums) {
|
||||
count += num;
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="list.ts"
|
||||
/* Обход списка по индексам */
|
||||
let count = 0;
|
||||
for (let i = 0; i < nums.length; i++) {
|
||||
count += nums[i];
|
||||
}
|
||||
|
||||
/* Прямой обход элементов списка */
|
||||
count = 0;
|
||||
for (const num of nums) {
|
||||
count += num;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="list.dart"
|
||||
/* Обход списка по индексам */
|
||||
int count = 0;
|
||||
for (var i = 0; i < nums.length; i++) {
|
||||
count += nums[i];
|
||||
}
|
||||
|
||||
/* Прямой обход элементов списка */
|
||||
count = 0;
|
||||
for (var num in nums) {
|
||||
count += num;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="list.rs"
|
||||
// Обход списка по индексам
|
||||
let mut _count = 0;
|
||||
for i in 0..nums.len() {
|
||||
_count += nums[i];
|
||||
}
|
||||
|
||||
// Прямой обход элементов списка
|
||||
_count = 0;
|
||||
for num in &nums {
|
||||
_count += num;
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="list.c"
|
||||
// В C нет встроенного динамического массива
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="list.kt"
|
||||
/* Обход списка по индексам */
|
||||
var count = 0
|
||||
for (i in nums.indices) {
|
||||
count += nums[i]
|
||||
}
|
||||
|
||||
/* Прямой обход элементов списка */
|
||||
for (num in nums) {
|
||||
count += num
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="list.rb"
|
||||
# Обход списка по индексам
|
||||
count = 0
|
||||
for i in 0...nums.length
|
||||
count += nums[i]
|
||||
end
|
||||
|
||||
# Прямой обход элементов списка
|
||||
count = 0
|
||||
for num in nums
|
||||
count += num
|
||||
end
|
||||
```
|
||||
|
||||
??? pythontutor "Визуализация выполнения"
|
||||
|
||||
https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9E%D0%B1%D1%85%D0%BE%D0%B4%D0%B8%D1%82%D1%8C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%20%D0%BF%D0%BE%20%D0%B8%D0%BD%D0%B4%D0%B5%D0%BA%D1%81%D0%B0%D0%BC%0A%20%20%20%20count%20%3D%200%0A%20%20%20%20for%20i%20in%20range%28len%28nums%29%29%3A%0A%20%20%20%20%20%20%20%20count%20%2B%3D%20nums%5Bi%5D%0A%0A%20%20%20%20%23%20%D0%9D%D0%B5%D0%BF%D0%BE%D1%81%D1%80%D0%B5%D0%B4%D1%81%D1%82%D0%B2%D0%B5%D0%BD%D0%BD%D0%BE%20%D0%BE%D0%B1%D1%85%D0%BE%D0%B4%D0%B8%D1%82%D1%8C%20%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D1%8B%20%D1%81%D0%BF%D0%B8%D1%81%D0%BA%D0%B0%0A%20%20%20%20for%20num%20in%20nums%3A%0A%20%20%20%20%20%20%20%20count%20%2B%3D%20num&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
||||
|
||||
### Конкатенация списков
|
||||
|
||||
Если дан новый список `nums1` , мы можем присоединить его к хвосту исходного списка.
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="list.py"
|
||||
# Конкатенация двух списков
|
||||
nums1: list[int] = [6, 8, 7, 10, 9]
|
||||
nums += nums1 # Присоединить список nums1 к концу nums
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="list.cpp"
|
||||
/* Конкатенация двух списков */
|
||||
vector<int> nums1 = { 6, 8, 7, 10, 9 };
|
||||
// Присоединить список nums1 к концу nums
|
||||
nums.insert(nums.end(), nums1.begin(), nums1.end());
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="list.java"
|
||||
/* Конкатенация двух списков */
|
||||
List<Integer> nums1 = new ArrayList<>(Arrays.asList(new Integer[] { 6, 8, 7, 10, 9 }));
|
||||
nums.addAll(nums1); // Присоединить список nums1 к концу nums
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="list.cs"
|
||||
/* Конкатенация двух списков */
|
||||
List<int> nums1 = [6, 8, 7, 10, 9];
|
||||
nums.AddRange(nums1); // Присоединить список nums1 к концу nums
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="list_test.go"
|
||||
/* Конкатенация двух списков */
|
||||
nums1 := []int{6, 8, 7, 10, 9}
|
||||
nums = append(nums, nums1...) // Присоединить список nums1 к концу nums
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="list.swift"
|
||||
/* Конкатенация двух списков */
|
||||
let nums1 = [6, 8, 7, 10, 9]
|
||||
nums.append(contentsOf: nums1) // Присоединить список nums1 к концу nums
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="list.js"
|
||||
/* Конкатенация двух списков */
|
||||
const nums1 = [6, 8, 7, 10, 9];
|
||||
nums.push(...nums1); // Присоединить список nums1 к концу nums
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="list.ts"
|
||||
/* Конкатенация двух списков */
|
||||
const nums1: number[] = [6, 8, 7, 10, 9];
|
||||
nums.push(...nums1); // Присоединить список nums1 к концу nums
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="list.dart"
|
||||
/* Конкатенация двух списков */
|
||||
List<int> nums1 = [6, 8, 7, 10, 9];
|
||||
nums.addAll(nums1); // Присоединить список nums1 к концу nums
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="list.rs"
|
||||
/* Конкатенация двух списков */
|
||||
let nums1: Vec<i32> = vec![6, 8, 7, 10, 9];
|
||||
nums.extend(nums1);
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="list.c"
|
||||
// В C нет встроенного динамического массива
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="list.kt"
|
||||
/* Конкатенация двух списков */
|
||||
val nums1 = intArrayOf(6, 8, 7, 10, 9).toMutableList()
|
||||
nums.addAll(nums1) // Присоединить список nums1 к концу nums
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="list.rb"
|
||||
# Конкатенация двух списков
|
||||
nums1 = [6, 8, 7, 10, 9]
|
||||
nums += nums1
|
||||
```
|
||||
|
||||
??? pythontutor "Визуализация выполнения"
|
||||
|
||||
https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9E%D0%B1%D1%8A%D0%B5%D0%B4%D0%B8%D0%BD%D0%B8%D1%82%D1%8C%20%D0%B4%D0%B2%D0%B0%20%D1%81%D0%BF%D0%B8%D1%81%D0%BA%D0%B0%0A%20%20%20%20nums1%20%3D%20%5B6%2C%208%2C%207%2C%2010%2C%209%5D%0A%20%20%20%20nums%20%2B%3D%20nums1%20%20%23%20%D0%9F%D1%80%D0%B8%D1%81%D0%BE%D0%B5%D0%B4%D0%B8%D0%BD%D0%B8%D1%82%D1%8C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%20nums1%20%D0%BA%20nums&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
||||
|
||||
### Сортировка списка
|
||||
|
||||
После сортировки списка мы сможем применять алгоритмы "бинарный поиск" и "два указателя", которые очень часто встречаются в задачах по массивам.
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="list.py"
|
||||
# Отсортировать список
|
||||
nums.sort() # После сортировки элементы списка идут по возрастанию
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="list.cpp"
|
||||
/* Отсортировать список */
|
||||
sort(nums.begin(), nums.end()); // После сортировки элементы списка идут по возрастанию
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="list.java"
|
||||
/* Отсортировать список */
|
||||
Collections.sort(nums); // После сортировки элементы списка идут по возрастанию
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="list.cs"
|
||||
/* Отсортировать список */
|
||||
nums.Sort(); // После сортировки элементы списка идут по возрастанию
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="list_test.go"
|
||||
/* Отсортировать список */
|
||||
sort.Ints(nums) // После сортировки элементы списка идут по возрастанию
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="list.swift"
|
||||
/* Отсортировать список */
|
||||
nums.sort() // После сортировки элементы списка идут по возрастанию
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="list.js"
|
||||
/* Отсортировать список */
|
||||
nums.sort((a, b) => a - b); // После сортировки элементы списка идут по возрастанию
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="list.ts"
|
||||
/* Отсортировать список */
|
||||
nums.sort((a, b) => a - b); // После сортировки элементы списка идут по возрастанию
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="list.dart"
|
||||
/* Отсортировать список */
|
||||
nums.sort(); // После сортировки элементы списка идут по возрастанию
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="list.rs"
|
||||
/* Отсортировать список */
|
||||
nums.sort(); // После сортировки элементы списка идут по возрастанию
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="list.c"
|
||||
// В C нет встроенного динамического массива
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="list.kt"
|
||||
/* Отсортировать список */
|
||||
nums.sort() // После сортировки элементы списка идут по возрастанию
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="list.rb"
|
||||
# Отсортировать список
|
||||
nums = nums.sort { |a, b| a <=> b } # После сортировки элементы списка идут по возрастанию
|
||||
```
|
||||
|
||||
??? pythontutor "Визуализация выполнения"
|
||||
|
||||
https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%98%D0%BD%D0%B8%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D0%B7%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%0A%20%20%20%20nums%20%3D%20%5B1%2C%203%2C%202%2C%205%2C%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%D0%9E%D1%82%D1%81%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%0A%20%20%20%20nums.sort%28%29%20%20%23%20%D0%A1%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0%D0%BF%D0%BE%D1%81%D0%BB%D0%B5%2C%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D1%80%D0%B0%D1%81%D0%BF%D0%BE%D0%BB%D0%BE%D0%B6%D0%B5%D0%BD%D1%8B%20%D0%BF%D0%BE%20%D0%B2%D0%BE%D0%B7%D1%80%D0%B0%D1%81%D1%82%D0%B0%D0%BD%D0%B8%D1%8E&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
||||
|
||||
## Реализация списка
|
||||
|
||||
Во многих языках программирования списки встроены в стандартную библиотеку, например в Java, C++ и Python. Их реализация довольно сложна, а настройки параметров тщательно продуманы: начальная емкость, коэффициент расширения и так далее. Если тебе интересно, стоит заглянуть в исходный код.
|
||||
|
||||
Чтобы лучше понять принцип работы списка, попробуем реализовать его упрощенную версию, в которой есть три ключевых аспекта проектирования.
|
||||
|
||||
- **Начальная емкость**: выбрать разумную начальную емкость внутреннего массива. В этом примере мы берем 10.
|
||||
- **Учет количества элементов**: объявить переменную `size` , которая будет хранить текущее число элементов в списке и обновляться в реальном времени при вставке и удалении элементов. С помощью этой переменной можно находить конец списка и понимать, требуется ли расширение.
|
||||
- **Механизм расширения**: если при вставке элементов емкость списка исчерпана, нужно выполнить расширение. Для этого сначала создается больший массив с учетом коэффициента расширения, а затем все элементы текущего массива по порядку переносятся в новый. В этом примере мы считаем, что каждый раз массив расширяется в 2 раза.
|
||||
|
||||
```src
|
||||
[file]{my_list}-[class]{my_list}-[func]{}
|
||||
```
|
||||
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,71 @@
|
||||
# Оперативная память и кэш *
|
||||
|
||||
В первых двух разделах этой главы мы разобрали массивы и связные списки - две фундаментальные и важные структуры данных, которые соответственно представляют две физические структуры хранения: "непрерывное хранение" и "разрозненное хранение".
|
||||
|
||||
На практике **физическая структура во многом определяет, насколько эффективно программа использует память и кэш**, а это, в свою очередь, влияет на общую производительность алгоритмической программы.
|
||||
|
||||
## Устройства хранения данных в компьютере
|
||||
|
||||
В компьютере есть три типа устройств хранения данных: <u>жесткий диск (hard disk)</u> , <u>оперативная память (random-access memory, RAM)</u> и <u>кэш-память (cache memory)</u> . В таблице ниже показаны их различные роли и характеристики производительности в компьютерной системе.
|
||||
|
||||
<p align="center"> Таблица <id> Устройства хранения данных в компьютере </p>
|
||||
|
||||
| | Жесткий диск | Оперативная память | Кэш |
|
||||
| -------------- | --------------------------------------- | ----------------------------------------- | ------------------------------------------------------- |
|
||||
| Назначение | Долговременное хранение данных, включая ОС, программы, файлы и т.д. | Временное хранение выполняемых программ и обрабатываемых данных | Хранение часто используемых данных и инструкций, уменьшающее число обращений CPU к памяти |
|
||||
| Энергозависимость | Данные не теряются после отключения питания | Данные теряются после отключения питания | Данные теряются после отключения питания |
|
||||
| Емкость | Большая, уровень TB | Меньшая, уровень GB | Очень малая, уровень MB |
|
||||
| Скорость | Низкая, от сотен до тысяч MB/s | Высокая, десятки GB/s | Очень высокая, десятки и сотни GB/s |
|
||||
| Цена (юани) | Дешевый, от долей юаня до нескольких юаней за GB | Дорогая, десятки и сотни юаней за GB | Очень дорогой, входит в стоимость упаковки CPU |
|
||||
|
||||
Компьютерную систему хранения можно представить в виде пирамиды, показанной на рисунке ниже. Чем ближе устройство хранения к вершине пирамиды, тем оно быстрее, тем меньше его емкость и тем выше его стоимость. Такая многоуровневая конструкция возникла не случайно, а стала результатом тщательных инженерных компромиссов.
|
||||
|
||||
- **Жесткий диск трудно заменить оперативной памятью**. Во-первых, данные в оперативной памяти исчезают после отключения питания, поэтому она не подходит для долговременного хранения. Во-вторых, память стоит в десятки раз дороже жесткого диска, что мешает ее широкому применению в потребительском сегменте.
|
||||
- **Кэш не может одновременно быть и очень большим, и очень быстрым**. По мере роста емкости кэшей L1, L2 и L3 их физический размер увеличивается, расстояние до ядра CPU становится больше, время передачи данных растет, а задержка доступа к элементам увеличивается. При текущем уровне технологий многоуровневая структура кэша является лучшим балансом между емкостью, скоростью и стоимостью.
|
||||
|
||||

|
||||
|
||||
!!! tip
|
||||
|
||||
Иерархия памяти компьютера отражает тонкий баланс между скоростью, емкостью и стоимостью. На самом деле подобные компромиссы встречаются почти во всех отраслях инженерии: приходится искать оптимальный баланс между преимуществами и ограничениями.
|
||||
|
||||
В итоге **жесткий диск используется для долговременного хранения больших объемов данных, оперативная память - для временного хранения данных, с которыми программа работает прямо сейчас, а кэш - для хранения часто используемых данных и инструкций**, чтобы ускорять выполнение программ. Все три уровня работают совместно и обеспечивают эффективную работу компьютерной системы.
|
||||
|
||||
Как показано на рисунке ниже, во время выполнения программы данные читаются с жесткого диска в оперативную память, а затем используются CPU в вычислениях. Кэш можно рассматривать как часть CPU: **он интеллектуально подгружает данные из оперативной памяти**, обеспечивая CPU высокоскоростной доступ и тем самым значительно ускоряя выполнение программы и уменьшая зависимость от более медленной RAM.
|
||||
|
||||

|
||||
|
||||
## Эффективность использования памяти структурами данных
|
||||
|
||||
С точки зрения использования пространства памяти массивы и связные списки имеют свои преимущества и ограничения.
|
||||
|
||||
С одной стороны, **память ограничена, и один и тот же участок памяти не может совместно использоваться несколькими программами**, поэтому нам хочется, чтобы структуры данных использовали пространство как можно эффективнее. Элементы массива расположены плотно и не требуют дополнительного места для хранения ссылок (указателей) между узлами списка, поэтому массивы эффективнее по памяти. Однако массиву нужно сразу выделить достаточно большой непрерывный участок памяти, что может приводить к потерям пространства, а его расширение требует дополнительных затрат времени и памяти. Напротив, связные списки выполняют динамическое выделение и освобождение памяти "по узлам", что дает большую гибкость.
|
||||
|
||||
С другой стороны, во время выполнения программы **при многократном выделении и освобождении памяти фрагментация свободной памяти становится все более серьезной**, что снижает эффективность ее использования. Массивы из-за непрерывного хранения относительно менее подвержены фрагментации. Напротив, элементы связного списка распределены по памяти, и частые операции вставки и удаления легче приводят к фрагментации.
|
||||
|
||||
## Эффективность использования кэша структурами данных
|
||||
|
||||
Хотя по объему кэш намного меньше оперативной памяти, он значительно быстрее и играет критически важную роль в скорости выполнения программ. Поскольку объем кэша ограничен и в нем можно хранить только небольшую долю часто используемых данных, когда CPU пытается обратиться к данным, которых в кэше нет, происходит <u>промах кэша (cache miss)</u> , и CPU вынужден загружать нужные данные из более медленной памяти.
|
||||
|
||||
Очевидно, что **чем меньше "промахов кэша", тем выше эффективность чтения и записи данных CPU**, а значит, тем лучше производительность программы. Долю обращений, при которых CPU успешно получает данные из кэша, называют <u>коэффициентом попадания в кэш (cache hit rate)</u> ; этот показатель обычно используют для оценки эффективности кэша.
|
||||
|
||||
Чтобы добиться как можно большей эффективности, кэш использует следующие механизмы загрузки данных.
|
||||
|
||||
- **Строки кэша**: кэш хранит и загружает данные не по одному байту, а строками кэша. По сравнению с передачей по байтам это гораздо эффективнее.
|
||||
- **Механизм предвыборки**: процессор старается предсказать шаблон доступа к данным (например последовательный доступ, доступ с фиксированным шагом и т.д.) и на основе этого шаблона заранее загружает данные в кэш, повышая вероятность попадания.
|
||||
- **Пространственная локальность**: если к некоторым данным уже обратились, то велика вероятность, что в ближайшее время понадобятся и соседние данные. Поэтому, загружая некоторые данные, кэш часто подгружает и окружающие их данные.
|
||||
- **Временная локальность**: если к данным уже обратились, то высока вероятность, что к ним снова обратятся в ближайшем будущем. Кэш использует это свойство, сохраняя недавно использованные данные.
|
||||
|
||||
На практике **массивы и связные списки по-разному используют кэш**, и это проявляется в нескольких аспектах.
|
||||
|
||||
- **Занимаемое пространство**: элементы связного списка занимают больше места, чем элементы массива, поэтому в кэше помещается меньше полезных данных.
|
||||
- **Строки кэша**: данные списка разбросаны по памяти, а кэш загружает данные "строками", поэтому доля бесполезно загружаемых данных оказывается выше.
|
||||
- **Механизм предвыборки**: шаблон доступа к данным у массивов более "предсказуем", чем у списков, то есть системе легче угадать, какие данные понадобятся следующими.
|
||||
- **Пространственная локальность**: массив хранится в компактной области памяти, поэтому данные рядом с уже загруженными с большей вероятностью скоро будут использованы.
|
||||
|
||||
В целом **массивы имеют более высокий коэффициент попадания в кэш, поэтому по эффективности операций они обычно превосходят связные списки**. Именно поэтому при решении алгоритмических задач структуры данных на основе массивов часто оказываются предпочтительнее.
|
||||
|
||||
Важно понимать, что **высокая эффективность кэша не означает, что массивы во всех случаях лучше связных списков**. В реальных приложениях выбор структуры данных должен определяться конкретными требованиями. Например, и массивы, и списки могут использоваться для реализации "стека" (подробнее об этом будет рассказано в следующей главе), но подходят они для разных сценариев.
|
||||
|
||||
- При решении алгоритмических задач мы обычно предпочитаем стек на основе массива, потому что он дает более высокую эффективность операций и поддерживает произвольный доступ, а цена за это - необходимость заранее выделить некоторый объем памяти под массив.
|
||||
- Если объем данных очень велик, структура сильно динамична, а ожидаемый размер стека трудно оценить заранее, то более уместен стек на основе связного списка. Список позволяет распределить большой объем данных по разным участкам памяти и избегает накладных расходов, связанных с расширением массива.
|
||||
@@ -0,0 +1,86 @@
|
||||
# Резюме
|
||||
|
||||
### Ключевые выводы
|
||||
|
||||
- Массивы и связные списки - это две базовые структуры данных, представляющие два способа хранения данных в памяти компьютера: хранение в непрерывной области и хранение в разрозненных областях. Их свойства во многом взаимно дополняют друг друга.
|
||||
- Массив поддерживает произвольный доступ и занимает меньше памяти; однако вставка и удаление элементов в нем неэффективны, а длина после инициализации неизменяема.
|
||||
- Связный список позволяет эффективно вставлять и удалять узлы путем изменения ссылок (указателей), а также гибко менять длину; однако доступ к узлам неэффективен, а памяти он занимает больше. Распространенные типы списков включают односвязные, циклические и двусвязные списки.
|
||||
- Список - это упорядоченная коллекция элементов, поддерживающая добавление, удаление, поиск и изменение, и обычно реализуемая на основе динамического массива. Он сохраняет преимущества массива и при этом может гибко менять длину.
|
||||
- Появление списка значительно повысило практическую полезность массива, хотя это и может приводить к потерям части памяти.
|
||||
- Во время работы программы данные в основном хранятся в оперативной памяти. Массив обеспечивает более высокую эффективность использования пространства памяти, а связный список дает большую гибкость в использовании памяти.
|
||||
- Кэш, используя строки кэша, механизм предвыборки, а также пространственную и временную локальность, предоставляет CPU быстрый доступ к данным и заметно повышает эффективность выполнения программ.
|
||||
- Поскольку массивы обычно имеют более высокий коэффициент попадания в кэш, они в большинстве случаев работают эффективнее списков. При выборе структуры данных нужно исходить из конкретных требований и сценариев.
|
||||
|
||||
### Q & A
|
||||
|
||||
**Q**: Влияет ли хранение массива в стеке или в куче на временную и пространственную эффективность?
|
||||
|
||||
Массивы, расположенные и в стеке, и в куче, все равно хранятся в непрерывной области памяти, поэтому эффективность операций с данными у них в целом одинакова. Однако у стека и кучи есть собственные особенности, из-за которых возникают следующие различия.
|
||||
|
||||
1. Эффективность выделения и освобождения: стек представляет собой относительно небольшой участок памяти, а выделение в нем обычно выполняется автоматически компилятором; куча же обычно больше, может выделяться динамически из кода и легче фрагментируется. Поэтому выделение и освобождение памяти в куче обычно медленнее, чем в стеке.
|
||||
2. Ограничение размера: объем стека относительно невелик, а размер кучи обычно ограничивается доступной памятью. Поэтому куча лучше подходит для хранения больших массивов.
|
||||
3. Гибкость: размер массива в стеке должен быть известен во время компиляции, а размер массива в куче может определяться динамически во время выполнения.
|
||||
|
||||
**Q**: Почему для массива требуется, чтобы все элементы были одного типа, а для связного списка это не подчеркивается?
|
||||
|
||||
Связный список состоит из узлов, а узлы соединяются между собой через ссылки (указатели), поэтому каждый узел в принципе может хранить данные разного типа, например `int` , `double` , `string` , `object` и т.д.
|
||||
|
||||
Напротив, элементы массива должны быть одного типа, иначе нельзя будет вычислять адрес элемента через смещение. Например, если массив одновременно содержит `int` и `long` , один элемент занимает 4 байта, а другой - 8 байт ; в этом случае формула ниже уже не позволит вычислить смещение, потому что в массиве будут присутствовать элементы разной длины.
|
||||
|
||||
```shell
|
||||
# Адрес элемента в памяти = адрес массива в памяти (адрес первого элемента) + длина элемента * индекс элемента
|
||||
```
|
||||
|
||||
**Q**: После удаления узла `P` нужно ли присваивать `P.next = None` ?
|
||||
|
||||
Можно и не изменять `P.next` . С точки зрения данного списка, при обходе от головы к хвосту узел `P` уже больше не встретится. Это означает, что узел `P` уже удален из списка, и то, куда он указывает после этого, на сам список больше не влияет.
|
||||
|
||||
С точки зрения задач по структурам данных и алгоритмам, отсутствие такого разрыва обычно не критично, если логика программы остается корректной. Но с точки зрения стандартной библиотеки разорвать связь безопаснее и логичнее. Если этого не сделать и удаленный узел не будет нормально собран, он может мешать освобождению памяти последующих узлов.
|
||||
|
||||
**Q**: Временная сложность вставки и удаления в связном списке равна $O(1)$ . Но до вставки или удаления обычно еще нужно потратить $O(n)$ на поиск элемента. Почему тогда общая сложность не $O(n)$ ?
|
||||
|
||||
Если сначала искать элемент, а потом удалять его, то временная сложность действительно будет $O(n)$ . Однако преимущество связного списка с $O(1)$ вставкой и удалением проявляется в других сценариях. Например, двустороннюю очередь удобно реализовывать именно на связном списке: мы поддерживаем указатели на голову и хвост, и тогда каждая операция вставки или удаления остается $O(1)$ .
|
||||
|
||||
**Q**: На рисунке "Определение связного списка и способ хранения" светло-голубой блок с указателем узла - это отдельный адрес памяти? Или он делит память пополам со значением узла?
|
||||
|
||||
Этот рисунок дает только качественное представление; количественно все зависит от конкретных условий.
|
||||
|
||||
- Значения узлов разных типов занимают разный объем памяти, например `int` , `long` , `double` и объекты-экземпляры.
|
||||
- Размер памяти, занимаемой переменной-указателем, зависит от операционной системы и среды компиляции и обычно составляет 8 байт или 4 байта.
|
||||
|
||||
**Q**: Всегда ли добавление элемента в конец списка имеет сложность $O(1)$ ?
|
||||
|
||||
Если при добавлении элемента длина списка превышается, то сначала приходится расширять список, а уже затем добавлять новый элемент. Система выделяет новый участок памяти и переносит туда все элементы исходного списка, и в этот момент временная сложность становится $O(n)$ .
|
||||
|
||||
**Q**: В утверждении "появление списка сильно повысило практическую полезность массива, но может приводить к потере части памяти" под потерями памяти имеется в виду дополнительная память под такие переменные, как емкость, длина и коэффициент расширения?
|
||||
|
||||
Потери памяти здесь в основном имеют два значения: во-первых, список обычно имеет некоторую начальную емкость, которая может быть нам не нужна целиком; во-вторых, чтобы избежать слишком частых расширений, емкость при расширении обычно умножается на некоторый коэффициент, например $\times 1.5$ . Из-за этого появляется много пустых слотов, которые обычно нельзя полностью заполнить.
|
||||
|
||||
**Q**: В Python после инициализации `n = [1, 2, 3]` адреса этих трех элементов выглядят непрерывными, но после `m = [2, 1, 3]` можно заметить, что `id` элементов не идут подряд, а совпадают с одинаковыми числами из `n` . Если адреса элементов не непрерывны, остается ли `m` массивом?
|
||||
|
||||
Предположим, что элементами списка являются узлы `n = [n1, n2, n3, n4, n5]` . Обычно эти 5 объектов-узлов тоже будут храниться в разных местах памяти. Однако, имея индекс списка, мы по-прежнему можем за $O(1)$ получить адрес памяти соответствующего узла и обратиться к нему. Это связано с тем, что в массиве хранятся ссылки на узлы, а не сами узлы.
|
||||
|
||||
В отличие от многих других языков, в Python даже числа обернуты в объекты, и в списке хранятся не сами числа, а ссылки на них. Поэтому мы и наблюдаем, что одинаковые числа в двух массивах имеют один и тот же `id` , а адреса этих чисел не обязаны быть непрерывными.
|
||||
|
||||
**Q**: В C++ STL уже есть двусвязный список `std::list` , но в некоторых учебниках по алгоритмам им пользуются не так часто. Это связано с какими-то ограничениями?
|
||||
|
||||
С одной стороны, при разработке алгоритмов мы обычно предпочитаем структуры на основе массива, а к связным спискам прибегаем только при необходимости, по двум главным причинам.
|
||||
|
||||
- Накладные расходы по памяти: поскольку каждому элементу нужны два дополнительных указателя (на предыдущий и следующий элементы), `std::list` обычно занимает больше памяти, чем `std::vector` .
|
||||
- Низкая дружелюбность к кэшу: поскольку данные не лежат непрерывно, `std::list` хуже использует кэш. В большинстве случаев `std::vector` показывает лучшую производительность.
|
||||
|
||||
С другой стороны, случаи, когда связный список действительно необходим, в основном возникают в деревьях и графах. Для стеков и очередей чаще используют предоставляемые языком `stack` и `queue` , а не связный список напрямую.
|
||||
|
||||
**Q**: Операция `res = [[0]] * n` создает двумерный список. Каждый `[0]` в нем независим?
|
||||
|
||||
Нет, они не независимы. В таком двумерном списке все `[0]` на самом деле являются ссылками на один и тот же объект. Если изменить один из них, окажется, что меняются и все остальные соответствующие элементы.
|
||||
|
||||
Если нужно, чтобы каждый `[0]` был независимым, можно использовать `res = [[0] for _ in range(n)]` . В этом варианте создаются $n$ независимых объектов-списков `[0]` .
|
||||
|
||||
**Q**: Операция `res = [0] * n` создает список. Каждый целочисленный `0` в нем независим?
|
||||
|
||||
В этом списке все целые числа `0` являются ссылками на один и тот же объект. Это связано с тем, что Python использует механизм кэш-пула для маленьких целых чисел (обычно от -5 до 256), чтобы максимально переиспользовать объекты и повысить производительность.
|
||||
|
||||
Хотя все элементы указывают на один и тот же объект, мы все равно можем независимо изменять элементы списка, потому что целые числа в Python - это "неизменяемые объекты". Когда мы изменяем некоторый элемент, на самом деле происходит переключение ссылки на другой объект, а не изменение исходного объекта.
|
||||
|
||||
Однако если элементами списка являются "изменяемые объекты" (например списки, словари или экземпляры классов), то изменение одного элемента прямо меняет сам объект, и все элементы, ссылающиеся на него, увидят одно и то же изменение.
|
||||