This commit is contained in:
krahets
2026-04-03 18:46:15 +08:00
parent 377736b1bd
commit 9d21ca86b0
352 changed files with 46563 additions and 11262 deletions

View File

@@ -4,7 +4,7 @@ comments: true
# 4.1   Массив
<u>Массив (array)</u> - это линейная структура данных, которая хранит элементы одного типа в непрерывной области памяти. Положение элемента в массиве называется его <u>индексом (index)</u>. На рисунке 4-1 показаны основные понятия, связанные с массивом, и способ его хранения.
<u>Массив (array)</u> - это линейная структура данных, в которой элементы одного типа хранятся в непрерывной области памяти. Положение элемента в массиве называется его <u>индексом (index)</u>. На рисунке 4-1 показаны основные понятия, связанные с массивом, и способ его хранения.
![Определение массива и способ хранения](array.assets/array_definition.png){ class="animation-figure" }
@@ -14,7 +14,7 @@ comments: true
### 1. &nbsp; Инициализация массива
В зависимости от задачи мы можем выбрать один из двух способов инициализации массива: без начальных значений или с заданными начальными значениями. Если начальные значения не указаны, большинство языков программирования инициализируют элементы массива значением $0$ :
Существует два способа инициализации массива: без начальных значений и с заданными начальными значениями. Если начальные значения не указаны, большинство языков программирования инициализируют элементы массива нулями:
=== "Python"
@@ -138,13 +138,13 @@ comments: true
### 2. &nbsp; Доступ к элементам
Элементы массива хранятся в непрерывной области памяти, а это означает, что вычислить адрес любого элемента очень просто. Зная адрес массива в памяти (то есть адрес первого элемента) и индекс некоторого элемента, мы можем по формуле с рисунка ниже вычислить адрес этого элемента и напрямую обратиться к нему.
Элементы массива хранятся в непрерывной области памяти, что упрощает вычисление их адресов. Зная адрес массива в памяти (то есть адрес первого элемента) и индекс некоторого элемента, мы можем по формуле с рисунка ниже вычислить адрес этого элемента и напрямую обратиться к нему.
![Вычисление адреса элемента массива](array.assets/array_memory_location_calculation.png){ class="animation-figure" }
<p align="center"> Рисунок 4-2 &nbsp; Вычисление адреса элемента массива </p>
Если посмотреть на рисунок 4-2, можно заметить, что индекс первого элемента массива равен $0$ , и это кажется не слишком интуитивным, ведь естественнее было бы начинать счет с $1$ . Однако с точки зрения формулы адресации **индекс по сути является смещением относительно адреса памяти**. Смещение первого элемента равно $0$ , поэтому индекс $0$ вполне логичен.
Если посмотреть на рисунок 4-2, можно заметить, что индекс первого элемента массива равен $0$ , и это кажется не слишком интуитивным, ведь естественнее было бы начинать счет с $1$ . Однако с точки зрения формулы адресации **индекс по сути является смещением относительно адреса памяти**. Смещение первого элемента равно $0$ , поэтому индекс $0$ полностью логичен.
Доступ к элементам массива очень эффективен: любой элемент массива можно получить за $O(1)$ времени.
@@ -324,13 +324,13 @@ comments: true
### 3. &nbsp; Вставка элемента
Элементы массива в памяти расположены "вплотную" друг к другу, и между ними нет места для размещения новых данных. Как показано на рисунке 4-3, если мы хотим вставить элемент в середину массива, то все элементы после этой позиции нужно сдвинуть на одну позицию вправо, а затем записать новое значение в освободившийся индекс.
Элементы массива в памяти расположены вплотную друг к другу, и между ними нет места для размещения новых данных. Как показано на рисунке 4-3, если мы хотим вставить элемент в середину массива, то все элементы после этой позиции нужно сдвинуть на одну позицию вправо, а затем записать новое значение в освободившийся индекс.
![Пример вставки элемента в массив](array.assets/array_insert_element.png){ class="animation-figure" }
<p align="center"> Рисунок 4-3 &nbsp; Пример вставки элемента в массив </p>
Стоит отметить, что длина массива фиксирована, поэтому вставка нового элемента неизбежно приведет к "потере" элемента на конце массива. Решение этой проблемы мы оставим для обсуждения в разделе о "списках".
Стоит отметить, что длина массива фиксирована, поэтому вставка нового элемента неизбежно приведет к потере элемента на конце массива. Решение этой проблемы мы оставим для обсуждения в разделе о "списках".
=== "Python"
@@ -526,7 +526,7 @@ comments: true
<p align="center"> Рисунок 4-4 &nbsp; Пример удаления элемента из массива </p>
Обрати внимание: после удаления исходный последний элемент становится "бессмысленным", поэтому специально изменять его не требуется.
Обрати внимание: после удаления исходный последний элемент становится бессмысленным, поэтому специально изменять его не требуется.
=== "Python"
@@ -692,7 +692,7 @@ comments: true
- **Высокая временная сложность**: средняя временная сложность и вставки, и удаления равна $O(n)$ , где $n$ - длина массива.
- **Потеря элементов**: поскольку длина массива неизменяема, после вставки элементы, выходящие за пределы длины массива, будут потеряны.
- **Потери памяти**: можно заранее инициализировать более длинный массив и использовать только его переднюю часть; тогда "теряемые" при вставке элементы на конце не будут нести смысла, но такой подход приводит к лишнему расходу памяти.
- **Потери памяти**: можно заранее инициализировать более длинный массив и использовать только его переднюю часть; тогда теряемые при вставке элементы на конце не будут нести смысла, но такой подход приводит к лишнему расходу памяти.
### 5. &nbsp; Обход массива
@@ -939,7 +939,7 @@ comments: true
Чтобы найти заданный элемент в массиве, нужно пройти по массиву и на каждой итерации проверять, совпадает ли значение; если совпадает, вернуть соответствующий индекс.
Поскольку массив - это линейная структура данных, такая операция поиска называется "линейным поиском".
Поскольку массив - это линейная структура данных, такая операция поиска называется линейным поиском.
=== "Python"
@@ -1326,8 +1326,8 @@ comments: true
```ruby title="array.rb"
### Увеличить длину массива ###
# Обратите внимание: Array в Ruby является динамическим массивом и может быть расширен напрямую
# Для удобства обучения эта функция рассматривает Array как массив неизменяемой длины
# Обратите внимание: Array в Ruby является динамическим массивом и может расширяться напрямую
# Для удобства обучения в этой функции Array рассматривается как массив неизменяемой длины
def extend(nums, enlarge)
# Инициализировать массив увеличенной длины
res = Array.new(nums.length + enlarge, 0)
@@ -1349,13 +1349,13 @@ comments: true
## 4.1.2 &nbsp; Преимущества и ограничения массива
Массив хранится в непрерывной области памяти, и все его элементы имеют один и тот же тип. Такой подход содержит много априорной информации, которую система может использовать для оптимизации эффективности операций со структурой данных.
Массив хранится в непрерывной области памяти, и все его элементы имеют один и тот же тип. Такой подход содержит богатую априорную информацию, которую система может использовать для оптимизации эффективности операций с этой структурой данных.
- **Высокая пространственная эффективность**: массив выделяет для данных непрерывный блок памяти без дополнительного структурного накладного расхода.
- **Поддержка произвольного доступа**: массив позволяет обращаться к любому элементу за $O(1)$ времени.
- **Локальность кэша**: при обращении к элементу массива компьютер загружает не только сам элемент, но и соседние данные, что позволяет использовать кэш для ускорения последующих операций.
Хранение в непрерывной области памяти - палка о двух концах, и у него есть следующие ограничения.
Непрерывное хранение данных - это палка о двух концах, и у него есть следующие ограничения.
- **Низкая эффективность вставки и удаления**: когда элементов в массиве много, вставка и удаление требуют сдвига большого количества элементов.
- **Неизменяемая длина**: после инициализации длина массива фиксирована; расширение массива требует копирования всех данных в новый массив, что стоит дорого.
@@ -1366,7 +1366,7 @@ comments: true
Массив - это базовая и очень распространенная структура данных. Он часто используется как в различных алгоритмах, так и при реализации более сложных структур данных.
- **Произвольный доступ**: если мы хотим случайным образом выбирать некоторые образцы, можно сохранить их в массиве и сгенерировать случайную последовательность индексов для выборки.
- **Сортировка и поиск**: массив - самая распространенная структура данных для алгоритмов сортировки и поиска. Быстрая сортировка, сортировка слиянием, бинарный поиск и многие другие алгоритмы в основном работают именно с массивами.
- **Таблица поиска**: когда нужно быстро находить элемент или его соответствие, массив можно использовать как lookup table. Например, если мы хотим реализовать отображение символов в коды ASCII, можно использовать значение ASCII как индекс, а соответствующий элемент хранить по этой позиции массива.
- **Сортировка и поиск**: массив - самая распространенная структура данных для алгоритмов сортировки и поиска. Быстрая сортировка, сортировка слиянием, двоичный поиск и многие другие алгоритмы в основном работают именно с массивами.
- **Таблица поиска**: когда нужно быстро находить элемент или его соответствие, массив можно использовать как таблицу поиска. Например, если мы хотим реализовать отображение символов в коды ASCII, можно использовать значение ASCII как индекс, а соответствующий элемент хранить по этой позиции массива.
- **Машинное обучение**: в нейронных сетях широко используются операции линейной алгебры над векторами, матрицами и тензорами, и все эти данные строятся в форме массивов. Массив - самая часто используемая структура данных в программировании нейросетей.
- **Реализация структур данных**: массивы можно использовать для реализации стеков, очередей, хеш-таблиц, куч, графов и других структур данных. Например, матрица смежности графа по сути является двумерным массивом.

View File

@@ -11,12 +11,12 @@ icon: material/view-list-outline
Мир структур данных напоминает прочную кирпичную стену.
Кирпичи массива уложены ровно и плотно прилегают друг к другу. Кирпичи связного списка разбросаны в разных местах, а соединяющие их лозы свободно тянутся между щелями.
Кирпичи массива уложены ровно и плотно прилегают друг к другу. Узлы связного списка, напротив, разбросаны в разных местах, а соединяющие их связи свободно тянутся между промежутками.
## Содержание главы
- [4.1 &nbsp; Массив](array.md)
- [4.2 &nbsp; Связный список](linked_list.md)
- [4.3 &nbsp; Список](list.md)
- [4.4 &nbsp; Память и кеш *](ram_and_cache.md)
- [4.4 &nbsp; Оперативная память и кэш *](ram_and_cache.md)
- [4.5 &nbsp; Резюме](summary.md)

View File

@@ -4,21 +4,21 @@ comments: true
# 4.2 &nbsp; Связный список
Память является общим ресурсом для всех программ, и в сложной среде выполнения свободные участки памяти могут быть разбросаны по всему адресу памяти. Мы знаем, что память для хранения массива должна быть непрерывной, а если массив очень велик, память может не суметь предоставить столь большой непрерывный блок. Именно здесь проявляется преимущество гибкости связного списка.
Память - общий ресурс для всех программ, и в сложной среде выполнения свободные участки памяти могут быть разбросаны по всему адресному пространству. Мы знаем, что память для хранения массива должна быть непрерывной, а если массив очень велик, в памяти может не оказаться столь большого непрерывного блока. Именно здесь и проявляется преимущество гибкости связного списка.
<u>Связный список (linked list)</u> - это линейная структура данных, в которой каждый элемент представляет собой объект-узел, а сами узлы соединены между собой через "ссылки". Ссылка хранит адрес памяти следующего узла, благодаря чему из текущего узла можно получить доступ к следующему.
<u>Связный список (linked list)</u> - это линейная структура данных, в которой каждый элемент представляет собой объект-узел, а сами узлы соединены между собой с помощью ссылок. Ссылка хранит адрес памяти следующего узла, благодаря чему из текущего узла можно перейти к следующему.
Конструкция связного списка позволяет хранить отдельные узлы в разных местах памяти, и их адреса вовсе не обязаны быть непрерывными.
Конструкция связного списка позволяет хранить отдельные узлы в разных местах памяти, и их адреса вовсе не обязаны быть последовательными.
![Определение связного списка и способ хранения](linked_list.assets/linkedlist_definition.png){ class="animation-figure" }
<p align="center"> Рисунок 4-5 &nbsp; Определение связного списка и способ хранения </p>
Если посмотреть на рисунок 4-5, можно заметить, что базовой единицей связного списка является объект <u>узел (node)</u>. Каждый узел содержит две части данных: "значение" узла и "ссылку" на следующий узел.
Как видно на рисунке 4-5, базовой единицей связного списка является объект <u>узел (node)</u>. Каждый узел содержит две части данных: значение узла и ссылку на следующий узел.
- Первый узел связного списка называется "головным узлом", а последний - "хвостовым узлом".
- Хвостовой узел указывает на "пусто", что в Java, C++ и Python обозначается как `null` , `nullptr` и `None` соответственно.
- В языках, поддерживающих указатели, таких как C, C++, Go и Rust, упомянутую выше "ссылку" следует заменить на "указатель".
- Первый узел связного списка называется головным узлом, а последний - хвостовым узлом.
- Хвостовой узел указывает на пустое значение, что в Java, C++ и Python обозначается как `null` , `nullptr` и `None` соответственно.
- В языках, поддерживающих указатели, таких как C, C++, Go и Rust, упомянутую выше ссылку следует заменить на указатель.
Как показано в коде ниже, узел связного списка `ListNode` хранит не только значение, но и дополнительную ссылку (указатель). Поэтому **при одинаковом объеме данных связный список занимает больше памяти, чем массив**.
@@ -73,7 +73,7 @@ comments: true
Next *ListNode // Указатель на следующий узел
}
// NewListNode Конструктор, создает новый связный список
// NewListNode Конструктор, создает новый узел
func NewListNode(val int) *ListNode {
return &ListNode{
Val: val,
@@ -195,7 +195,7 @@ comments: true
### 1. &nbsp; Инициализация связного списка
Построение связного списка состоит из двух шагов: сначала нужно инициализировать объекты всех узлов, затем установить связи-ссылки между ними. После завершения инициализации мы можем, начиная с головы списка, последовательно проходить все узлы по ссылке `next`.
Построение связного списка состоит из двух шагов: сначала нужно инициализировать объекты всех узлов, затем установить ссылочные связи между ними. После завершения инициализации мы можем, начиная с головы списка, последовательно проходить все узлы по ссылке `next`.
=== "Python"
@@ -423,13 +423,13 @@ comments: true
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` .
Массив в целом - это одна переменная: например, массив `nums` содержит элементы `nums[0]` , `nums[1]` и т.д. Связный список же состоит из множества независимых объектов-узлов. **Обычно в качестве обозначения всего связного списка используют головной узел**; например, в приведенном выше коде связный список можно обозначить как `n0` .
### 2. &nbsp; Вставка узла
Вставить узел в связный список очень легко. Как показано на рисунке 4-6, предположим, что мы хотим вставить новый узел `P` между двумя соседними узлами `n0` и `n1` ; **для этого нужно изменить всего две ссылки (указателя)**, а временная сложность будет равна $O(1)$ .
Для сравнения: временная сложность вставки элемента в массив составляет $O(n)$ , и при большом объеме данных это неэффективно.
Для сравнения: временная сложность вставки элемента в массив составляет $O(n)$ , и при большом объеме данных это менее эффективно.
![Пример вставки узла в связный список](linked_list.assets/linkedlist_insert_node.png){ class="animation-figure" }
@@ -571,7 +571,7 @@ comments: true
```ruby title="linked_list.rb"
### Вставка узла _p после узла n0 в связном списке ###
# В Ruby `p` встроенная функция, а `P` — константа, поэтому вместо этого можно использовать `_p`
# В Ruby `p` является встроенной функцией, а `P` — константой, поэтому вместо них можно использовать `_p`
def insert(n0, _p)
n1 = n0.next
_p.next = n1
@@ -586,9 +586,9 @@ comments: true
### 3. &nbsp; Удаление узла
Как показано на рисунке 4-7, удалить узел из связного списка тоже очень удобно: **нужно изменить всего одну ссылку (указатель)**.
Как показано на рисунке 4-7, удалить узел из связного списка тоже очень просто: **нужно изменить всего одну ссылку (указатель)**.
Обрати внимание: хотя после завершения операции удаления узел `P` все еще указывает на `n1` , при обходе связного списка до `P` уже нельзя добраться, а значит `P` больше не принадлежит данному списку.
Стоит отметить, что хотя после завершения операции удаления узел `P` все еще указывает на `n1` , при обходе связного списка до `P` уже нельзя добраться. Это означает, что `P` фактически больше не принадлежит данному списку.
![Удаление узла из связного списка](linked_list.assets/linkedlist_remove_node.png){ class="animation-figure" }
@@ -789,7 +789,7 @@ comments: true
### 4. &nbsp; Доступ к узлу
**Доступ к узлам в связном списке менее эффективен**. Как уже обсуждалось в предыдущем разделе, к любому элементу массива можно обратиться за $O(1)$ времени. Со связным списком это не так: программе нужно стартовать от головного узла и последовательно двигаться дальше, пока не будет найден целевой узел. То есть для доступа к $i$ -му узлу списка нужно выполнить $i - 1$ итераций, а временная сложность составляет $O(n)$ .
**Доступ к узлам в связном списке менее эффективен**. Как уже обсуждалось в предыдущем разделе, к любому элементу массива можно обратиться за $O(1)$ времени. Со связным списком это не так: программе нужно начать с головного узла и последовательно двигаться дальше, пока не будет найден целевой узел. То есть для доступа к $i$ -му узлу списка нужно выполнить $i - 1$ итераций, а временная сложность составляет $O(n)$ .
=== "Python"
@@ -993,7 +993,7 @@ comments: true
### 5. &nbsp; Поиск узла
Выполни обход связного списка, найди в нем узел со значением `target` и верни индекс этого узла в списке. Этот процесс тоже относится к линейному поиску. Код выглядит следующим образом:
Поиск узла заключается в обходе связного списка, нахождении узла со значением `target` и возврате его индекса в списке. Этот процесс тоже относится к линейному поиску. Код выглядит следующим образом:
=== "Python"
@@ -1240,9 +1240,9 @@ comments: true
Как показано на рисунке 4-8, существует три распространенных типа связных списков.
- **Односвязный список**: это обычный связный список, рассмотренный выше. Узел односвязного списка содержит значение и ссылку на следующий узел. Первый узел называется головным, последний - хвостовым, и хвост указывает на пусто `None` .
- **Циклический список**: если заставить хвостовой узел односвязного списка указывать на головной (то есть соединить хвост с головой), получится циклический список. В циклическом списке любой узел можно рассматривать как головной.
- **Двусвязный список**: по сравнению с односвязным списком двусвязный хранит ссылки в двух направлениях. Определение узла двусвязного списка включает как ссылку на следующий узел, так и ссылку на предыдущий узел. По сравнению с односвязным списком двусвязный более гибок и позволяет проходить список в обе стороны, но за это приходится платить дополнительной памятью.
- **Односвязный список**: это обычный связный список, рассмотренный выше. Узел односвязного списка содержит значение и ссылку на следующий узел. Первый узел называется головным, последний - хвостовым, и хвост указывает на `None` .
- **Циклический список**: если заставить хвостовой узел односвязного списка указывать на головной, то есть соединить хвост с головой, получится циклический список. В циклическом списке любой узел можно рассматривать как головной.
- **Двусвязный список**: по сравнению с односвязным списком двусвязный хранит ссылки в двух направлениях. Определение узла двусвязного списка включает как ссылку на следующий узел, так и ссылку на предыдущий узел. По сравнению с односвязным списком двусвязный более гибок и позволяет обходить список в обе стороны, но за это приходится платить дополнительной памятью.
=== "Python"
@@ -1457,10 +1457,10 @@ comments: true
Двусвязные списки обычно используются там, где нужен быстрый доступ как к предыдущему, так и к следующему элементу.
- **Продвинутые структуры данных**: например, в красно-черных деревьях и B-деревьях нам нужен доступ к родительскому узлу; этого можно добиться, сохранив в узле ссылку на родителя, по аналогии с двусвязным списком.
- **История браузера**: когда пользователь в браузере нажимает кнопки "вперед" или "назад", браузеру нужно знать предыдущую и следующую веб-страницы, которые он посещал. Свойства двусвязного списка делают такую операцию простой.
- **История браузера**: когда пользователь в браузере нажимает кнопки "вперед" или "назад", браузеру нужно знать предыдущую и следующую посещенные страницы. Свойства двусвязного списка делают такую операцию простой.
- **Алгоритм LRU**: в алгоритмах вытеснения из кэша (LRU) нужно быстро находить наименее недавно использованные данные, а также быстро добавлять и удалять узлы. Для этого двусвязный список подходит очень хорошо.
Циклические списки часто применяются в сценариях, требующих периодических операций, например при планировании ресурсов в операционной системе.
Циклические списки часто применяются в сценариях, требующих циклических операций, например при планировании ресурсов в операционной системе.
- **Алгоритм циклического распределения кванта времени**: в операционных системах round-robin scheduling - это распространенный алгоритм планирования CPU, который циклически обходит набор процессов. Каждому процессу выделяется квант времени, и когда он исчерпан, CPU переключается на следующий процесс. Такую циклическую операцию удобно реализовать с помощью кольцевого списка.
- **Буферы данных**: в некоторых реализациях буферов данных также могут использоваться циклические списки. Например, в аудио- и видеоплеерах поток данных может делиться на несколько буферных блоков и помещаться в кольцевой список для обеспечения непрерывного воспроизведения.

View File

@@ -19,7 +19,7 @@ comments: true
### 1. &nbsp; Инициализация списка
Обычно мы используем два способа инициализации: "без начальных значений" и "с начальными значениями":
Обычно используются два способа инициализации: без начальных значений и с начальными значениями:
=== "Python"
@@ -157,7 +157,7 @@ comments: true
### 2. &nbsp; Доступ к элементам
Список по своей сути является массивом, поэтому доступ к элементам и их обновление можно выполнять за $O(1)$ времени, что очень эффективно.
Поскольку в этом разделе список рассматривается как структура на основе динамического массива, доступ к элементам и их обновление можно выполнять за $O(1)$ времени, что очень эффективно.
=== "Python"
@@ -288,7 +288,7 @@ comments: true
### 3. &nbsp; Вставка и удаление элементов
По сравнению с массивами список позволяет свободно добавлять и удалять элементы. Добавление элемента в конец списка имеет временную сложность $O(1)$ , но операции вставки и удаления по-прежнему имеют ту же эффективность, что и у массива, то есть $O(n)$ .
В отличие от массива список позволяет свободно добавлять и удалять элементы. Добавление элемента в конец списка имеет временную сложность $O(1)$ , но операции вставки и удаления по-прежнему имеют ту же эффективность, что и у массива, то есть $O(n)$ .
=== "Python"
@@ -743,7 +743,7 @@ comments: true
### 5. &nbsp; Конкатенация списков
Если дан новый список `nums1` , мы можем присоединить его к хвосту исходного списка.
Создав новый список `nums1` , мы можем присоединить его к хвосту исходного списка.
=== "Python"
@@ -854,7 +854,7 @@ comments: true
### 6. &nbsp; Сортировка списка
После сортировки списка мы сможем применять алгоритмы "бинарный поиск" и "два указателя", которые очень часто встречаются в задачах по массивам.
После сортировки списка мы сможем применять алгоритмы "двоичный поиск" и "два указателя", которые очень часто встречаются в задачах по массивам.
=== "Python"
@@ -952,12 +952,12 @@ comments: true
## 4.3.2 &nbsp; Реализация списка
Во многих языках программирования списки встроены в стандартную библиотеку, например в Java, C++ и Python. Их реализация довольно сложна, а настройки параметров тщательно продуманы: начальная емкость, коэффициент расширения и так далее. Если тебе интересно, стоит заглянуть в исходный код.
Во многих языках программирования списки встроены в стандартную библиотеку, например в Java, C++, Python и других языках. Их реализация довольно сложна, а настройки параметров тщательно продуманы: начальная емкость, коэффициент расширения и так далее. Если это интересно, стоит заглянуть в исходный код.
Чтобы лучше понять принцип работы списка, попробуем реализовать его упрощенную версию, в которой есть три ключевых аспекта проектирования.
- **Начальная емкость**: выбрать разумную начальную емкость внутреннего массива. В этом примере мы берем 10.
- **Учет количества элементов**: объявить переменную `size` , которая будет хранить текущее число элементов в списке и обновляться в реальном времени при вставке и удалении элементов. С помощью этой переменной можно находить конец списка и понимать, требуется ли расширение.
- **Учет количества элементов**: объявить переменную `size` , которая будет хранить текущее число элементов в списке и обновляться в реальном времени при вставке и удалении элементов. С помощью этой переменной можно определять конец списка и понимать, требуется ли расширение.
- **Механизм расширения**: если при вставке элементов емкость списка исчерпана, нужно выполнить расширение. Для этого сначала создается больший массив с учетом коэффициента расширения, а затем все элементы текущего массива по порядку переносятся в новый. В этом примере мы считаем, что каждый раз массив расширяется в 2 раза.
=== "Python"

View File

@@ -4,13 +4,13 @@ comments: true
# 4.4 &nbsp; Оперативная память и кэш *
В первых двух разделах этой главы мы разобрали массивы и связные списки - две фундаментальные и важные структуры данных, которые соответственно представляют две физические структуры хранения: "непрерывное хранение" и "разрозненное хранение".
В первых двух разделах этой главы мы разобрали массивы и связные списки - две базовые и важные структуры данных, которые представляют соответственно непрерывное хранение и разрозненное хранение.
На практике **физическая структура во многом определяет, насколько эффективно программа использует память и кэш**, а это, в свою очередь, влияет на общую производительность алгоритмической программы.
На практике **физическая структура во многом определяет, насколько эффективно программа использует память и кэш**, а это, в свою очередь, влияет на общую производительность алгоритма.
## 4.4.1 &nbsp; Устройства хранения данных в компьютере
В компьютере есть три типа устройств хранения данных: <u>жесткий диск (hard disk)</u> , <u>оперативная память (random-access memory, RAM)</u> и <u>кэш-память (cache memory)</u> . В таблице 4-2 показаны их различные роли и характеристики производительности в компьютерной системе.
В компьютере есть три типа устройств хранения данных: <u>жесткий диск (hard disk)</u> , <u>оперативная память (random-access memory, RAM)</u> и <u>кэш-память (cache memory)</u> . В таблице 4-2 показаны их различные роли и характеристики в компьютерной системе.
<p align="center"> Таблица 4-2 &nbsp; Устройства хранения данных в компьютере </p>
@@ -22,13 +22,13 @@ comments: true
| Энергозависимость | Данные не теряются после отключения питания | Данные теряются после отключения питания | Данные теряются после отключения питания |
| Емкость | Большая, уровень TB | Меньшая, уровень GB | Очень малая, уровень MB |
| Скорость | Низкая, от сотен до тысяч MB/s | Высокая, десятки GB/s | Очень высокая, десятки и сотни GB/s |
| Цена (юани) | Дешевый, от долей юаня до нескольких юаней за GB | Дорогая, десятки и сотни юаней за GB | Очень дорогой, входит в стоимость упаковки CPU |
| Цена | Низкая, единицы валюты за GB | Высокая, десятки и сотни валютных единиц за GB | Очень высокая, входит в стоимость CPU |
</div>
Компьютерную систему хранения можно представить в виде пирамиды, показанной на рисунке 4-9. Чем ближе устройство хранения к вершине пирамиды, тем оно быстрее, тем меньше его емкость и тем выше его стоимость. Такая многоуровневая конструкция возникла не случайно, а стала результатом тщательных инженерных компромиссов.
- **Жесткий диск трудно заменить оперативной памятью**. Во-первых, данные в оперативной памяти исчезают после отключения питания, поэтому она не подходит для долговременного хранения. Во-вторых, память стоит в десятки раз дороже жесткого диска, что мешает ее широкому применению в потребительском сегменте.
- **Жесткий диск трудно заменить оперативной памятью**. Во-первых, данные в оперативной памяти исчезают после отключения питания, поэтому она не подходит для долговременного хранения. Во-вторых, память стоит в разы дороже жесткого диска, что мешает ее широкому применению.
- **Кэш не может одновременно быть и очень большим, и очень быстрым**. По мере роста емкости кэшей L1, L2 и L3 их физический размер увеличивается, расстояние до ядра CPU становится больше, время передачи данных растет, а задержка доступа к элементам увеличивается. При текущем уровне технологий многоуровневая структура кэша является лучшим балансом между емкостью, скоростью и стоимостью.
![Система хранения данных компьютера](ram_and_cache.assets/storage_pyramid.png){ class="animation-figure" }
@@ -37,11 +37,11 @@ comments: true
!!! tip
Иерархия памяти компьютера отражает тонкий баланс между скоростью, емкостью и стоимостью. На самом деле подобные компромиссы встречаются почти во всех отраслях инженерии: приходится искать оптимальный баланс между преимуществами и ограничениями.
Иерархия памяти компьютера отражает тонкий баланс между скоростью, емкостью и стоимостью. Подобные компромиссы встречаются почти во всех областях инженерии: приходится искать оптимальный баланс между преимуществами и ограничениями.
В итоге **жесткий диск используется для долговременного хранения больших объемов данных, оперативная память - для временного хранения данных, с которыми программа работает прямо сейчас, а кэш - для хранения часто используемых данных и инструкций**, чтобы ускорять выполнение программ. Все три уровня работают совместно и обеспечивают эффективную работу компьютерной системы.
Как показано на рисунке 4-10, во время выполнения программы данные читаются с жесткого диска в оперативную память, а затем используются CPU в вычислениях. Кэш можно рассматривать как часть CPU: **он интеллектуально подгружает данные из оперативной памяти**, обеспечивая CPU высокоскоростной доступ и тем самым значительно ускоряя выполнение программы и уменьшая зависимость от более медленной RAM.
Как показано на рисунке 4-10, во время выполнения программы данные читаются с жесткого диска в оперативную память, а затем используются CPU в вычислениях. Кэш можно рассматривать как часть CPU: **он подгружает данные из оперативной памяти**, обеспечивая CPU высокоскоростной доступ и тем самым значительно ускоряя выполнение программы и уменьшая зависимость от более медленной RAM.
![Поток данных между жестким диском, RAM и кэшем](ram_and_cache.assets/computer_storage_devices.png){ class="animation-figure" }
@@ -51,7 +51,7 @@ comments: true
С точки зрения использования пространства памяти массивы и связные списки имеют свои преимущества и ограничения.
С одной стороны, **память ограничена, и один и тот же участок памяти не может совместно использоваться несколькими программами**, поэтому нам хочется, чтобы структуры данных использовали пространство как можно эффективнее. Элементы массива расположены плотно и не требуют дополнительного места для хранения ссылок (указателей) между узлами списка, поэтому массивы эффективнее по памяти. Однако массиву нужно сразу выделить достаточно большой непрерывный участок памяти, что может приводить к потерям пространства, а его расширение требует дополнительных затрат времени и памяти. Напротив, связные списки выполняют динамическое выделение и освобождение памяти "по узлам", что дает большую гибкость.
С одной стороны, **память ограничена, и один и тот же участок памяти не может совместно использоваться несколькими программами**, поэтому нам хочется, чтобы структуры данных использовали пространство как можно эффективнее. Элементы массива расположены плотно и не требуют дополнительного места для хранения ссылок (указателей) между узлами списка, поэтому массивы эффективнее по памяти. Однако массиву нужно сразу выделить достаточно большой непрерывный участок памяти, что может приводить к потерям пространства, а его расширение требует дополнительных затрат времени и памяти. Напротив, связные списки выделяют и освобождают память на уровне узлов, что дает большую гибкость.
С другой стороны, во время выполнения программы **при многократном выделении и освобождении памяти фрагментация свободной памяти становится все более серьезной**, что снижает эффективность ее использования. Массивы из-за непрерывного хранения относительно менее подвержены фрагментации. Напротив, элементы связного списка распределены по памяти, и частые операции вставки и удаления легче приводят к фрагментации.
@@ -59,7 +59,7 @@ comments: true
Хотя по объему кэш намного меньше оперативной памяти, он значительно быстрее и играет критически важную роль в скорости выполнения программ. Поскольку объем кэша ограничен и в нем можно хранить только небольшую долю часто используемых данных, когда CPU пытается обратиться к данным, которых в кэше нет, происходит <u>промах кэша (cache miss)</u> , и CPU вынужден загружать нужные данные из более медленной памяти.
Очевидно, что **чем меньше "промахов кэша", тем выше эффективность чтения и записи данных CPU**, а значит, тем лучше производительность программы. Долю обращений, при которых CPU успешно получает данные из кэша, называют <u>коэффициентом попадания в кэш (cache hit rate)</u> ; этот показатель обычно используют для оценки эффективности кэша.
Очевидно, что **чем меньше промахов кэша, тем выше эффективность чтения и записи данных CPU**, а значит, тем лучше производительность программы. Долю обращений, при которых CPU успешно получает данные из кэша, называют <u>коэффициентом попадания в кэш (cache hit rate)</u> ; этот показатель обычно используют для оценки эффективности кэша.
Чтобы добиться как можно большей эффективности, кэш использует следующие механизмы загрузки данных.
@@ -71,8 +71,8 @@ comments: true
На практике **массивы и связные списки по-разному используют кэш**, и это проявляется в нескольких аспектах.
- **Занимаемое пространство**: элементы связного списка занимают больше места, чем элементы массива, поэтому в кэше помещается меньше полезных данных.
- **Строки кэша**: данные списка разбросаны по памяти, а кэш загружает данные "строками", поэтому доля бесполезно загружаемых данных оказывается выше.
- **Механизм предвыборки**: шаблон доступа к данным у массивов более "предсказуем", чем у списков, то есть системе легче угадать, какие данные понадобятся следующими.
- **Строки кэша**: данные списка разбросаны по памяти, а кэш загружает данные строками, поэтому доля бесполезно загружаемых данных оказывается выше.
- **Механизм предвыборки**: шаблон доступа к данным у массивов более предсказуем, чем у списков, то есть системе легче угадать, какие данные понадобятся следующими.
- **Пространственная локальность**: массив хранится в компактной области памяти, поэтому данные рядом с уже загруженными с большей вероятностью скоро будут использованы.
В целом **массивы имеют более высокий коэффициент попадания в кэш, поэтому по эффективности операций они обычно превосходят связные списки**. Именно поэтому при решении алгоритмических задач структуры данных на основе массивов часто оказываются предпочтительнее.

View File

@@ -6,11 +6,11 @@ comments: true
### 1. &nbsp; Ключевые выводы
- Массивы и связные списки - это две базовые структуры данных, представляющие два способа хранения данных в памяти компьютера: хранение в непрерывной области и хранение в разрозненных областях. Их свойства во многом взаимно дополняют друг друга.
- Массив поддерживает произвольный доступ и занимает меньше памяти; однако вставка и удаление элементов в нем неэффективны, а длина после инициализации неизменяема.
- Связный список позволяет эффективно вставлять и удалять узлы путем изменения ссылок (указателей), а также гибко менять длину; однако доступ к узлам неэффективен, а памяти он занимает больше. Распространенные типы списков включают односвязные, циклические и двусвязные списки.
- Массивы и связные списки - это две базовые структуры данных, представляющие два способа хранения данных в памяти компьютера: хранение в непрерывном пространстве и хранение в разрозненном пространстве. Их свойства во многом взаимно дополняют друг друга.
- Массив поддерживает произвольный доступ и занимает меньше памяти; однако вставка и удаление элементов в нем неэффективны, а длина после инициализации фиксирована.
- Связный список позволяет эффективно вставлять и удалять узлы путем изменения ссылок (указателей), а также гибко менять длину; однако доступ к узлам менее эффективен, а памяти он занимает больше. Распространенные типы списков включают односвязные, циклические и двусвязные списки.
- Список - это упорядоченная коллекция элементов, поддерживающая добавление, удаление, поиск и изменение, и обычно реализуемая на основе динамического массива. Он сохраняет преимущества массива и при этом может гибко менять длину.
- Появление списка значительно повысило практическую полезность массива, хотя это и может приводить к потерям части памяти.
- Появление списка значительно повысило практическую ценность массива, хотя это и может приводить к потере части памяти.
- Во время работы программы данные в основном хранятся в оперативной памяти. Массив обеспечивает более высокую эффективность использования пространства памяти, а связный список дает большую гибкость в использовании памяти.
- Кэш, используя строки кэша, механизм предвыборки, а также пространственную и временную локальность, предоставляет CPU быстрый доступ к данным и заметно повышает эффективность выполнения программ.
- Поскольку массивы обычно имеют более высокий коэффициент попадания в кэш, они в большинстве случаев работают эффективнее списков. При выборе структуры данных нужно исходить из конкретных требований и сценариев.