mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-10 21:16:43 +08:00
First version.
This commit is contained in:
227
ru/docs/chapter_array_and_linkedlist/array.md
Normal file
227
ru/docs/chapter_array_and_linkedlist/array.md
Normal file
@@ -0,0 +1,227 @@
|
||||
# Массивы
|
||||
|
||||
<u>Массив</u> представляет собой линейную структуру данных, в которой элементы одного типа хранятся в непрерывной области памяти. Положение элемента в массиве называется его <u>индексом</u>. На рисунке ниже изображены основные понятия и способ хранения массивов.
|
||||
|
||||

|
||||
|
||||
## Основные операции с массивом
|
||||
|
||||
### Инициализация массива
|
||||
|
||||
Существует два способа инициализации массива: без начальных значений и с заданными начальными значениями. Если начальные значения не указаны, большинство языков программирования инициализируют элементы массива нулями.
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="array.py"
|
||||
# Инициализация массива
|
||||
arr: list[int] = [0] * 5 # [ 0, 0, 0, 0, 0 ]
|
||||
nums: list[int] = [1, 3, 2, 5, 4]
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="array.cpp"
|
||||
/* Инициализация массива */
|
||||
// Хранение в стеке
|
||||
int arr[5];
|
||||
int nums[5] = { 1, 3, 2, 5, 4 };
|
||||
// Хранение в куче (требуется ручное освобождение памяти)
|
||||
int* arr1 = new int[5];
|
||||
int* nums1 = new int[5] { 1, 3, 2, 5, 4 };
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="array.java"
|
||||
/* Инициализация массива */
|
||||
int[] arr = new int[5]; // { 0, 0, 0, 0, 0 }
|
||||
int[] nums = { 1, 3, 2, 5, 4 };
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="array.cs"
|
||||
/* Инициализация массива */
|
||||
int[] arr = new int[5]; // [ 0, 0, 0, 0, 0 ]
|
||||
int[] nums = [1, 3, 2, 5, 4];
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="array.go"
|
||||
/* Инициализация массива */
|
||||
var arr [5]int
|
||||
// В Go при указании длины ([5]int) создается массив, без указания длины ([]int) - срез
|
||||
// Поскольку массивы Go определяются на этапе компиляции, для указания длины можно использовать только константы
|
||||
// Для удобства реализации метода extend() далее срез (Slice) рассматривается как массив (Array)
|
||||
nums := []int{1, 3, 2, 5, 4}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="array.swift"
|
||||
/* Инициализация массива */
|
||||
let arr = Array(repeating: 0, count: 5) // [0, 0, 0, 0, 0]
|
||||
let nums = [1, 3, 2, 5, 4]
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="array.js"
|
||||
/* Инициализация массива */
|
||||
var arr = new Array(5).fill(0);
|
||||
var nums = [1, 3, 2, 5, 4];
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="array.ts"
|
||||
/* Инициализация массива */
|
||||
let arr: number[] = new Array(5).fill(0);
|
||||
let nums: number[] = [1, 3, 2, 5, 4];
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="array.dart"
|
||||
/* Инициализация массива */
|
||||
List<int> arr = List.filled(5, 0); // [0, 0, 0, 0, 0]
|
||||
List<int> nums = [1, 3, 2, 5, 4];
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="array.rs"
|
||||
/* Инициализация массива */
|
||||
let arr: [i32; 5] = [0; 5]; // [0, 0, 0, 0, 0]
|
||||
let slice: &[i32] = &[0; 5];
|
||||
// В Rust при указании длины ([i32; 5]) создается массив, без указания длины (&[i32]) - срез
|
||||
// Поскольку массивы Rust определяются на этапе компиляции, для указания длины можно использовать только константы
|
||||
// Vector - это тип, обычно используемый в Rust в качестве динамического массива
|
||||
// Для удобства реализации метода extend() далее vector рассматривается как массив (array)
|
||||
let nums: Vec<i32> = vec![1, 3, 2, 5, 4];
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="array.c"
|
||||
/* Инициализация массива */
|
||||
int arr[5] = { 0 }; // { 0, 0, 0, 0, 0 }
|
||||
int nums[5] = { 1, 3, 2, 5, 4 };
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="array.kt"
|
||||
/* Инициализация массива */
|
||||
var arr = IntArray(5) // { 0, 0, 0, 0, 0 }
|
||||
var nums = intArrayOf(1, 3, 2, 5, 4)
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="array.rb"
|
||||
# Инициализация массива
|
||||
arr = Array.new(5, 0)
|
||||
nums = [1, 3, 2, 5, 4]
|
||||
```
|
||||
|
||||
??? pythontutor "Визуализация выполнения"
|
||||
|
||||
https://pythontutor.com/render.html#code=%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0Aarr%20%3D%20%5B0%5D%20*%205%20%20%23%20%5B%200,%200,%200,%200,%200%20%5D%0Anums%20%3D%20%5B1,%203,%202,%205,%204%5D&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
||||
|
||||
### Доступ к элементам
|
||||
|
||||
Элементы массива хранятся в непрерывной области памяти, что упрощает вычисление их адресов. Зная адрес массива (адрес первого элемента) и индекс элемента, можно вычислить адрес этого элемента по формуле, показанной на рисунке ниже, и получить к нему прямой доступ.
|
||||
|
||||

|
||||
|
||||
Как видно из рисунка, индекс первого элемента массива равен 0, что может показаться неочевидным, так как отсчет с 1 кажется более естественным. Однако с точки зрения формулы вычисления адреса **индекс является смещением адреса в памяти**. Смещение адреса первого элемента равно 0, поэтому и его индекс равен 0.
|
||||
|
||||
Доступ к элементам массива осуществляется очень эффективно, так как позволяет за время $O(1)$ произвольно обращаться к любому элементу.
|
||||
|
||||
```src
|
||||
[file]{array}-[class]{}-[func]{random_access}
|
||||
```
|
||||
|
||||
### Вставка элемента
|
||||
|
||||
Элементы массива в памяти расположены вплотную, между ними нет места для хранения других данных. Для вставки элемента в середину массива необходимо сдвинуть все последующие элементы на одну позицию вправо, а затем присвоить значение элементу по заданному индексу, как показано на рисунке ниже.
|
||||
|
||||

|
||||
|
||||
Следует отметить, что длина массива фиксирована, поэтому вставка элемента неизбежно приведет к потере элемента в конце массива. Решение этой проблемы будет рассмотрено в разделе «Списки».
|
||||
|
||||
```src
|
||||
[file]{array}-[class]{}-[func]{insert}
|
||||
```
|
||||
|
||||
### Удаление элемента
|
||||
|
||||
Аналогично для удаления элемента по индексу $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)$.
|
||||
- **Локальность кеширования**: при доступе к элементам массива компьютер загружает не только его, но и кеширует окружающие данные, что позволяет ускорить выполнение последующих операций за счет использования высокоскоростного кеша.
|
||||
|
||||
Непрерывное хранение в пространстве -- это палка о двух концах, имеющая следующие ограничения.
|
||||
|
||||
- **Низкая эффективность вставки и удаления**: при большом количестве элементов в массиве операции вставки и удаления требуют перемещения множества элементов.
|
||||
- **Неизменная длина**: после инициализации длина массива фиксируется, а увеличение массива требует копирования всех данных в новый массив, что влечет за собой значительные затраты.
|
||||
- **Расточительность пространства**: если размер выделенного массива превышает фактические потребности, избыточное пространство оказывается потраченным впустую.
|
||||
|
||||
## Типичные сценарии применения массивов
|
||||
|
||||
Массивы -- это базовая и распространенная структура данных, часто используемая в различных алгоритмах и для реализации сложных структур данных.
|
||||
|
||||
- **Произвольный доступ**: если требуется случайный выбор элементов, можно использовать массив для хранения и генерации случайной последовательности, осуществляя случайную выборку по индексу.
|
||||
- **Сортировка и поиск**: массивы являются наиболее часто используемой структурой данных для алгоритмов сортировки и поиска. Быстрая сортировка, сортировка слиянием, двоичный поиск и другие алгоритмы в основном работают с массивами.
|
||||
- **Таблица поиска**: когда необходимо быстро найти элемент или его соответствие, можно использовать массив в качестве таблицы поиска. Например, для реализации отображения символов в ASCII-коды можно использовать значение ASCII-кода символа в качестве индекса, а соответствующий элемент хранить в соответствующем месте массива.
|
||||
- **Машинное обучение**: в нейронных сетях широко используются операции линейной алгебры между векторами, матрицами и тензорами, которые реализуются в виде массивов. Массивы являются наиболее часто используемой структурой данных в программировании нейронных сетей.
|
||||
- **Реализация структур данных**: массивы могут использоваться для реализации стека, очереди, хеш-таблицы, кучи, графа и других структур данных. Например, представление графа в виде матрицы смежности фактически является двумерным массивом.
|
||||
9
ru/docs/chapter_array_and_linkedlist/index.md
Normal file
9
ru/docs/chapter_array_and_linkedlist/index.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Массивы и списки
|
||||
|
||||

|
||||
|
||||
!!! abstract
|
||||
|
||||
Мир структур данных подобен прочной кирпичной стене.
|
||||
|
||||
Кирпичи массива аккуратно выстроены в ряд, плотно прилегая друг к другу. Кирпичи связного списка разбросаны по разным местам, а соединяющие их лианы свободно проходят сквозь щели между кирпичами.
|
||||
473
ru/docs/chapter_array_and_linkedlist/linked_list.md
Normal file
473
ru/docs/chapter_array_and_linkedlist/linked_list.md
Normal file
@@ -0,0 +1,473 @@
|
||||
# Связные списки
|
||||
|
||||
Память -- это общий ресурс всех программ, и в сложной системной среде выполнения участки свободной памяти могут быть разбросаны по всему пространству памяти. Нам уже известно, что память для хранения массива должна быть непрерывной, и когда массив очень велик, в памяти может не оказаться столь большого непрерывного участка. В этом случае проявляется преимущество гибкости связного списка.
|
||||
|
||||
*Связный список* -- это линейная структура данных, в которой каждый элемент является объектом-узлом. При этом узлы соединяются друг с другом с помощью ссылок. В ссылке хранится адрес памяти следующего узла, по которому можно перейти от текущего узла к следующему.
|
||||
|
||||
Структура связного списка позволяет узлам храниться в различных местах памяти, а их адреса памяти не обязаны быть последовательными.
|
||||
|
||||

|
||||
|
||||
На рис. 4.5 изображена структура связного списка. Составным элементом является объект узла. Каждый узел содержит две части данных: значение узла и ссылку на следующий узел.
|
||||
|
||||
- Первый узел связного списка называется головным узлом, а последний узел -- хвостовым узлом.
|
||||
- Хвостовой узел указывает на пустое значение, которое в 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
|
||||
```
|
||||
|
||||
<!-- 🔴 Русская версия не содержит раздел "可视化运行" (визуализация выполнения) -->
|
||||
|
||||
Массив в целом является переменной, например массив nums содержит элементы nums[0] и nums[1] и т.д., в то время как связный список состоит из множества независимых объектов-узлов. **Обычно головной узел рассматривается как представитель связного списка**, например, в приведенном выше коде связный список можно обозначить как связный список n0.
|
||||
|
||||
### Вставка узла
|
||||
|
||||
<!-- 🔴 Русская версия не содержит описание вставки узла с изображением -->
|
||||
<!-- Китайский оригинал: 在链表中插入节点非常容易。如下图所示,假设我们想在相邻的两个节点 `n0` 和 `n1` 之间插入一个新节点 `P`... -->
|
||||
|
||||
```src
|
||||
[file]{linked_list}-[class]{}-[func]{insert}
|
||||
```
|
||||
|
||||
### Удаление узла
|
||||
|
||||
<!-- 🔴 Русская версия не содержит описание удаления узла с изображением -->
|
||||
<!-- Китайский оригинал: 如下图所示,在链表中删除节点也非常方便,**只需改变一个节点的引用(指针)即可**... -->
|
||||
|
||||
```src
|
||||
[file]{linked_list}-[class]{}-[func]{remove}
|
||||
```
|
||||
|
||||
### Доступ к узлу
|
||||
|
||||
<!-- 🔴 Русская версия не содержит полное описание доступа к узлу -->
|
||||
<!-- Китайский оригинал: **在链表中访问节点的效率较低**。如上一节所述,我们可以在 $O(1)$ 时间下访问数组中的任意元素... -->
|
||||
|
||||
```src
|
||||
[file]{linked_list}-[class]{}-[func]{access}
|
||||
```
|
||||
|
||||
### Поиск узла
|
||||
|
||||
<!-- 🔴 Русская версия не содержит описание поиска узла -->
|
||||
<!-- Китайский оригинал: 遍历链表,查找其中值为 `target` 的节点,输出该节点在链表中的索引... -->
|
||||
|
||||
```src
|
||||
[file]{linked_list}-[class]{}-[func]{find}
|
||||
```
|
||||
|
||||
## Массивы vs. Связные списки
|
||||
|
||||
<!-- 🔴 Русская версия не содержит сравнительную таблицу и описание -->
|
||||
<!-- Китайский оригинал: 下表总结了数组和链表的各项特点并对比了操作效率... -->
|
||||
|
||||
## Распространенные типы связных списков
|
||||
|
||||
<!-- 🔴 Русская версия не содержит описание типов связных списков -->
|
||||
<!-- Китайский оригинал: 如下图所示,常见的链表类型包括三种... -->
|
||||
|
||||
<!-- 🔴 Русская версия не содержит код для двусвязного списка на разных языках -->
|
||||
|
||||
<!-- 🔴 Русская версия не содержит изображение с типами связных списков -->
|
||||
|
||||
## Типичные применения связных списков
|
||||
|
||||
<!-- 🔴 Русская версия не содержит раздел о применении связных списков -->
|
||||
<!-- Китайский оригинал: 单向链表通常用于实现栈、队列、哈希表和图等数据结构... -->
|
||||
724
ru/docs/chapter_array_and_linkedlist/list.md
Normal file
724
ru/docs/chapter_array_and_linkedlist/list.md
Normal file
@@ -0,0 +1,724 @@
|
||||
# Список
|
||||
|
||||
<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%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20%23%20%E6%97%A0%E5%88%9D%E5%A7%8B%E5%80%BC%0A%20%20%20%20nums1%20%3D%20%5B%5D%0A%20%20%20%20%23%20%E6%9C%89%E5%88%9D%E5%A7%8B%E5%80%BC%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%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%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E5%85%83%E7%B4%A0%0A%20%20%20%20num%20%3D%20nums%5B1%5D%20%20%23%20%E8%AE%BF%E9%97%AE%E7%B4%A2%E5%BC%95%201%20%E5%A4%84%E7%9A%84%E5%85%83%E7%B4%A0%0A%0A%20%20%20%20%23%20%E6%9B%B4%E6%96%B0%E5%85%83%E7%B4%A0%0A%20%20%20%20nums%5B1%5D%20%3D%200%20%20%20%20%23%20%E5%B0%86%E7%B4%A2%E5%BC%95%201%20%E5%A4%84%E7%9A%84%E5%85%83%E7%B4%A0%E6%9B%B4%E6%96%B0%E4%B8%BA%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%E6%9C%89%E5%88%9D%E5%A7%8B%E5%80%BC%0A%20%20%20%20nums%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B8%85%E7%A9%BA%E5%88%97%E8%A1%A8%0A%20%20%20%20nums.clear%28%29%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%9C%A8%E5%B0%BE%E9%83%A8%E6%B7%BB%E5%8A%A0%E5%85%83%E7%B4%A0%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%E5%9C%A8%E4%B8%AD%E9%97%B4%E6%8F%92%E5%85%A5%E5%85%83%E7%B4%A0%0A%20%20%20%20nums.insert%283,%206%29%20%20%23%20%E5%9C%A8%E7%B4%A2%E5%BC%95%203%20%E5%A4%84%E6%8F%92%E5%85%A5%E6%95%B0%E5%AD%97%206%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E5%85%83%E7%B4%A0%0A%20%20%20%20nums.pop%283%29%20%20%20%20%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E7%B4%A2%E5%BC%95%203%20%E5%A4%84%E7%9A%84%E5%85%83%E7%B4%A0&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 =
|
||||
66
ru/docs/chapter_array_and_linkedlist/ram_and_cache.md
Normal file
66
ru/docs/chapter_array_and_linkedlist/ram_and_cache.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Память и кеш *
|
||||
|
||||
В первых двух разделах этой главы мы рассмотрели массивы и связные списки — две базовые и важные структуры данных, которые представляют собой "непрерывное хранение" и "распределенное хранение" соответственно.
|
||||
|
||||
На самом деле, **физическая структура в значительной степени определяет эффективность использования программой памяти и кеша**, что в свою очередь влияет на общую производительность алгоритма.
|
||||
|
||||
## Устройства хранения данных компьютера
|
||||
|
||||
Компьютер включает три типа устройств хранения данных: <u>жесткий диск (hard disk)</u>, <u>оперативная память (random-access memory, RAM)</u>, <u>кеш-память (cache memory)</u>. В таблице ниже показаны их различные роли в компьютерной системе и характеристики производительности.
|
||||
|
||||
<p align="center"> Таблица <id> Устройства хранения данных компьютера </p>
|
||||
|
||||
| | Жесткий диск | Оперативная память | Кеш |
|
||||
| ------------------------ | --------------------------------------------------------------- | --------------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
||||
| Назначение | Долговременное хранение данных, включая ОС, программы, файлы | Временное хранение выполняемых программ и обрабатываемых данных | Хранение часто используемых данных и инструкций, уменьшение обращений к памяти |
|
||||
| Энергозависимость | Данные не теряются при отключении питания | Данные теряются при отключении питания | Данные теряются при отключении питания |
|
||||
| Емкость | Большая, уровень ТБ | Меньшая, уровень ГБ | Очень малая, уровень МБ |
|
||||
| Скорость | Медленная, сотни-тысячи МБ/с | Быстрая, десятки ГБ/с | Очень быстрая, десятки-сотни ГБ/с |
|
||||
| Стоимость | Дешевая, несколько копеек-юаней / ГБ | Дорогая, десятки-сотни юаней / ГБ | Очень дорогая, входит в стоимость процессора |
|
||||
|
||||
Мы можем представить систему хранения данных компьютера в виде пирамидальной структуры, показанной на рисунке ниже. Чем ближе к вершине пирамиды находится устройство хранения, тем выше его скорость, меньше емкость и выше стоимость. Такая многоуровневая конструкция не случайна, а является результатом тщательного продумывания компьютерными учеными и инженерами.
|
||||
|
||||
- **Жесткий диск сложно заменить оперативной памятью**. Во-первых, данные в оперативной памяти теряются при отключении питания, поэтому она не подходит для долговременного хранения данных; во-вторых, стоимость оперативной памяти в десятки раз выше стоимости жесткого диска, что затрудняет ее распространение на потребительском рынке.
|
||||
- **Сложно совместить большую емкость и высокую скорость кеша**. По мере постепенного увеличения емкости кешей L1, L2, L3 их физический размер увеличивается, физическое расстояние до ядра процессора возрастает, что приводит к увеличению времени передачи данных и задержки доступа к элементам. При современных технологиях многоуровневая структура кеша является оптимальным балансом между емкостью, скоростью и стоимостью.
|
||||
|
||||

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

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