mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-10 04:56:36 +08:00
build
This commit is contained in:
@@ -4,7 +4,7 @@ comments: true
|
||||
|
||||
# 4.1 Массив
|
||||
|
||||
<u>Массив (array)</u> - это линейная структура данных, которая хранит элементы одного типа в непрерывной области памяти. Положение элемента в массиве называется его <u>индексом (index)</u>. На рисунке 4-1 показаны основные понятия, связанные с массивом, и способ его хранения.
|
||||
<u>Массив (array)</u> - это линейная структура данных, в которой элементы одного типа хранятся в непрерывной области памяти. Положение элемента в массиве называется его <u>индексом (index)</u>. На рисунке 4-1 показаны основные понятия, связанные с массивом, и способ его хранения.
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
@@ -14,7 +14,7 @@ comments: true
|
||||
|
||||
### 1. Инициализация массива
|
||||
|
||||
В зависимости от задачи мы можем выбрать один из двух способов инициализации массива: без начальных значений или с заданными начальными значениями. Если начальные значения не указаны, большинство языков программирования инициализируют элементы массива значением $0$ :
|
||||
Существует два способа инициализации массива: без начальных значений и с заданными начальными значениями. Если начальные значения не указаны, большинство языков программирования инициализируют элементы массива нулями:
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -138,13 +138,13 @@ comments: true
|
||||
|
||||
### 2. Доступ к элементам
|
||||
|
||||
Элементы массива хранятся в непрерывной области памяти, а это означает, что вычислить адрес любого элемента очень просто. Зная адрес массива в памяти (то есть адрес первого элемента) и индекс некоторого элемента, мы можем по формуле с рисунка ниже вычислить адрес этого элемента и напрямую обратиться к нему.
|
||||
Элементы массива хранятся в непрерывной области памяти, что упрощает вычисление их адресов. Зная адрес массива в памяти (то есть адрес первого элемента) и индекс некоторого элемента, мы можем по формуле с рисунка ниже вычислить адрес этого элемента и напрямую обратиться к нему.
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Рисунок 4-2 Вычисление адреса элемента массива </p>
|
||||
|
||||
Если посмотреть на рисунок 4-2, можно заметить, что индекс первого элемента массива равен $0$ , и это кажется не слишком интуитивным, ведь естественнее было бы начинать счет с $1$ . Однако с точки зрения формулы адресации **индекс по сути является смещением относительно адреса памяти**. Смещение первого элемента равно $0$ , поэтому индекс $0$ вполне логичен.
|
||||
Если посмотреть на рисунок 4-2, можно заметить, что индекс первого элемента массива равен $0$ , и это кажется не слишком интуитивным, ведь естественнее было бы начинать счет с $1$ . Однако с точки зрения формулы адресации **индекс по сути является смещением относительно адреса памяти**. Смещение первого элемента равно $0$ , поэтому индекс $0$ полностью логичен.
|
||||
|
||||
Доступ к элементам массива очень эффективен: любой элемент массива можно получить за $O(1)$ времени.
|
||||
|
||||
@@ -324,13 +324,13 @@ comments: true
|
||||
|
||||
### 3. Вставка элемента
|
||||
|
||||
Элементы массива в памяти расположены "вплотную" друг к другу, и между ними нет места для размещения новых данных. Как показано на рисунке 4-3, если мы хотим вставить элемент в середину массива, то все элементы после этой позиции нужно сдвинуть на одну позицию вправо, а затем записать новое значение в освободившийся индекс.
|
||||
Элементы массива в памяти расположены вплотную друг к другу, и между ними нет места для размещения новых данных. Как показано на рисунке 4-3, если мы хотим вставить элемент в середину массива, то все элементы после этой позиции нужно сдвинуть на одну позицию вправо, а затем записать новое значение в освободившийся индекс.
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Рисунок 4-3 Пример вставки элемента в массив </p>
|
||||
|
||||
Стоит отметить, что длина массива фиксирована, поэтому вставка нового элемента неизбежно приведет к "потере" элемента на конце массива. Решение этой проблемы мы оставим для обсуждения в разделе о "списках".
|
||||
Стоит отметить, что длина массива фиксирована, поэтому вставка нового элемента неизбежно приведет к потере элемента на конце массива. Решение этой проблемы мы оставим для обсуждения в разделе о "списках".
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -526,7 +526,7 @@ comments: true
|
||||
|
||||
<p align="center"> Рисунок 4-4 Пример удаления элемента из массива </p>
|
||||
|
||||
Обрати внимание: после удаления исходный последний элемент становится "бессмысленным", поэтому специально изменять его не требуется.
|
||||
Обрати внимание: после удаления исходный последний элемент становится бессмысленным, поэтому специально изменять его не требуется.
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -692,7 +692,7 @@ comments: true
|
||||
|
||||
- **Высокая временная сложность**: средняя временная сложность и вставки, и удаления равна $O(n)$ , где $n$ - длина массива.
|
||||
- **Потеря элементов**: поскольку длина массива неизменяема, после вставки элементы, выходящие за пределы длины массива, будут потеряны.
|
||||
- **Потери памяти**: можно заранее инициализировать более длинный массив и использовать только его переднюю часть; тогда "теряемые" при вставке элементы на конце не будут нести смысла, но такой подход приводит к лишнему расходу памяти.
|
||||
- **Потери памяти**: можно заранее инициализировать более длинный массив и использовать только его переднюю часть; тогда теряемые при вставке элементы на конце не будут нести смысла, но такой подход приводит к лишнему расходу памяти.
|
||||
|
||||
### 5. Обход массива
|
||||
|
||||
@@ -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 Преимущества и ограничения массива
|
||||
|
||||
Массив хранится в непрерывной области памяти, и все его элементы имеют один и тот же тип. Такой подход содержит много априорной информации, которую система может использовать для оптимизации эффективности операций со структурой данных.
|
||||
Массив хранится в непрерывной области памяти, и все его элементы имеют один и тот же тип. Такой подход содержит богатую априорную информацию, которую система может использовать для оптимизации эффективности операций с этой структурой данных.
|
||||
|
||||
- **Высокая пространственная эффективность**: массив выделяет для данных непрерывный блок памяти без дополнительного структурного накладного расхода.
|
||||
- **Поддержка произвольного доступа**: массив позволяет обращаться к любому элементу за $O(1)$ времени.
|
||||
- **Локальность кэша**: при обращении к элементу массива компьютер загружает не только сам элемент, но и соседние данные, что позволяет использовать кэш для ускорения последующих операций.
|
||||
|
||||
Хранение в непрерывной области памяти - палка о двух концах, и у него есть следующие ограничения.
|
||||
Непрерывное хранение данных - это палка о двух концах, и у него есть следующие ограничения.
|
||||
|
||||
- **Низкая эффективность вставки и удаления**: когда элементов в массиве много, вставка и удаление требуют сдвига большого количества элементов.
|
||||
- **Неизменяемая длина**: после инициализации длина массива фиксирована; расширение массива требует копирования всех данных в новый массив, что стоит дорого.
|
||||
@@ -1366,7 +1366,7 @@ comments: true
|
||||
Массив - это базовая и очень распространенная структура данных. Он часто используется как в различных алгоритмах, так и при реализации более сложных структур данных.
|
||||
|
||||
- **Произвольный доступ**: если мы хотим случайным образом выбирать некоторые образцы, можно сохранить их в массиве и сгенерировать случайную последовательность индексов для выборки.
|
||||
- **Сортировка и поиск**: массив - самая распространенная структура данных для алгоритмов сортировки и поиска. Быстрая сортировка, сортировка слиянием, бинарный поиск и многие другие алгоритмы в основном работают именно с массивами.
|
||||
- **Таблица поиска**: когда нужно быстро находить элемент или его соответствие, массив можно использовать как lookup table. Например, если мы хотим реализовать отображение символов в коды ASCII, можно использовать значение ASCII как индекс, а соответствующий элемент хранить по этой позиции массива.
|
||||
- **Сортировка и поиск**: массив - самая распространенная структура данных для алгоритмов сортировки и поиска. Быстрая сортировка, сортировка слиянием, двоичный поиск и многие другие алгоритмы в основном работают именно с массивами.
|
||||
- **Таблица поиска**: когда нужно быстро находить элемент или его соответствие, массив можно использовать как таблицу поиска. Например, если мы хотим реализовать отображение символов в коды ASCII, можно использовать значение ASCII как индекс, а соответствующий элемент хранить по этой позиции массива.
|
||||
- **Машинное обучение**: в нейронных сетях широко используются операции линейной алгебры над векторами, матрицами и тензорами, и все эти данные строятся в форме массивов. Массив - самая часто используемая структура данных в программировании нейросетей.
|
||||
- **Реализация структур данных**: массивы можно использовать для реализации стеков, очередей, хеш-таблиц, куч, графов и других структур данных. Например, матрица смежности графа по сути является двумерным массивом.
|
||||
|
||||
@@ -11,12 +11,12 @@ icon: material/view-list-outline
|
||||
|
||||
Мир структур данных напоминает прочную кирпичную стену.
|
||||
|
||||
Кирпичи массива уложены ровно и плотно прилегают друг к другу. Кирпичи связного списка разбросаны в разных местах, а соединяющие их лозы свободно тянутся между щелями.
|
||||
Кирпичи массива уложены ровно и плотно прилегают друг к другу. Узлы связного списка, напротив, разбросаны в разных местах, а соединяющие их связи свободно тянутся между промежутками.
|
||||
|
||||
## Содержание главы
|
||||
|
||||
- [4.1 Массив](array.md)
|
||||
- [4.2 Связный список](linked_list.md)
|
||||
- [4.3 Список](list.md)
|
||||
- [4.4 Память и кеш *](ram_and_cache.md)
|
||||
- [4.4 Оперативная память и кэш *](ram_and_cache.md)
|
||||
- [4.5 Резюме](summary.md)
|
||||
|
||||
@@ -4,21 +4,21 @@ comments: true
|
||||
|
||||
# 4.2 Связный список
|
||||
|
||||
Память является общим ресурсом для всех программ, и в сложной среде выполнения свободные участки памяти могут быть разбросаны по всему адресу памяти. Мы знаем, что память для хранения массива должна быть непрерывной, а если массив очень велик, память может не суметь предоставить столь большой непрерывный блок. Именно здесь проявляется преимущество гибкости связного списка.
|
||||
Память - общий ресурс для всех программ, и в сложной среде выполнения свободные участки памяти могут быть разбросаны по всему адресному пространству. Мы знаем, что память для хранения массива должна быть непрерывной, а если массив очень велик, в памяти может не оказаться столь большого непрерывного блока. Именно здесь и проявляется преимущество гибкости связного списка.
|
||||
|
||||
<u>Связный список (linked list)</u> - это линейная структура данных, в которой каждый элемент представляет собой объект-узел, а сами узлы соединены между собой через "ссылки". Ссылка хранит адрес памяти следующего узла, благодаря чему из текущего узла можно получить доступ к следующему.
|
||||
<u>Связный список (linked list)</u> - это линейная структура данных, в которой каждый элемент представляет собой объект-узел, а сами узлы соединены между собой с помощью ссылок. Ссылка хранит адрес памяти следующего узла, благодаря чему из текущего узла можно перейти к следующему.
|
||||
|
||||
Конструкция связного списка позволяет хранить отдельные узлы в разных местах памяти, и их адреса вовсе не обязаны быть непрерывными.
|
||||
Конструкция связного списка позволяет хранить отдельные узлы в разных местах памяти, и их адреса вовсе не обязаны быть последовательными.
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Рисунок 4-5 Определение связного списка и способ хранения </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. Инициализация связного списка
|
||||
|
||||
Построение связного списка состоит из двух шагов: сначала нужно инициализировать объекты всех узлов, затем установить связи-ссылки между ними. После завершения инициализации мы можем, начиная с головы списка, последовательно проходить все узлы по ссылке `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. Вставка узла
|
||||
|
||||
Вставить узел в связный список очень легко. Как показано на рисунке 4-6, предположим, что мы хотим вставить новый узел `P` между двумя соседними узлами `n0` и `n1` ; **для этого нужно изменить всего две ссылки (указателя)**, а временная сложность будет равна $O(1)$ .
|
||||
|
||||
Для сравнения: временная сложность вставки элемента в массив составляет $O(n)$ , и при большом объеме данных это неэффективно.
|
||||
Для сравнения: временная сложность вставки элемента в массив составляет $O(n)$ , и при большом объеме данных это менее эффективно.
|
||||
|
||||
{ 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. Удаление узла
|
||||
|
||||
Как показано на рисунке 4-7, удалить узел из связного списка тоже очень удобно: **нужно изменить всего одну ссылку (указатель)**.
|
||||
Как показано на рисунке 4-7, удалить узел из связного списка тоже очень просто: **нужно изменить всего одну ссылку (указатель)**.
|
||||
|
||||
Обрати внимание: хотя после завершения операции удаления узел `P` все еще указывает на `n1` , при обходе связного списка до `P` уже нельзя добраться, а значит `P` больше не принадлежит данному списку.
|
||||
Стоит отметить, что хотя после завершения операции удаления узел `P` все еще указывает на `n1` , при обходе связного списка до `P` уже нельзя добраться. Это означает, что `P` фактически больше не принадлежит данному списку.
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
@@ -789,7 +789,7 @@ comments: true
|
||||
|
||||
### 4. Доступ к узлу
|
||||
|
||||
**Доступ к узлам в связном списке менее эффективен**. Как уже обсуждалось в предыдущем разделе, к любому элементу массива можно обратиться за $O(1)$ времени. Со связным списком это не так: программе нужно стартовать от головного узла и последовательно двигаться дальше, пока не будет найден целевой узел. То есть для доступа к $i$ -му узлу списка нужно выполнить $i - 1$ итераций, а временная сложность составляет $O(n)$ .
|
||||
**Доступ к узлам в связном списке менее эффективен**. Как уже обсуждалось в предыдущем разделе, к любому элементу массива можно обратиться за $O(1)$ времени. Со связным списком это не так: программе нужно начать с головного узла и последовательно двигаться дальше, пока не будет найден целевой узел. То есть для доступа к $i$ -му узлу списка нужно выполнить $i - 1$ итераций, а временная сложность составляет $O(n)$ .
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -993,7 +993,7 @@ comments: true
|
||||
|
||||
### 5. Поиск узла
|
||||
|
||||
Выполни обход связного списка, найди в нем узел со значением `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 переключается на следующий процесс. Такую циклическую операцию удобно реализовать с помощью кольцевого списка.
|
||||
- **Буферы данных**: в некоторых реализациях буферов данных также могут использоваться циклические списки. Например, в аудио- и видеоплеерах поток данных может делиться на несколько буферных блоков и помещаться в кольцевой список для обеспечения непрерывного воспроизведения.
|
||||
|
||||
@@ -19,7 +19,7 @@ comments: true
|
||||
|
||||
### 1. Инициализация списка
|
||||
|
||||
Обычно мы используем два способа инициализации: "без начальных значений" и "с начальными значениями":
|
||||
Обычно используются два способа инициализации: без начальных значений и с начальными значениями:
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -157,7 +157,7 @@ comments: true
|
||||
|
||||
### 2. Доступ к элементам
|
||||
|
||||
Список по своей сути является массивом, поэтому доступ к элементам и их обновление можно выполнять за $O(1)$ времени, что очень эффективно.
|
||||
Поскольку в этом разделе список рассматривается как структура на основе динамического массива, доступ к элементам и их обновление можно выполнять за $O(1)$ времени, что очень эффективно.
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -288,7 +288,7 @@ comments: true
|
||||
|
||||
### 3. Вставка и удаление элементов
|
||||
|
||||
По сравнению с массивами список позволяет свободно добавлять и удалять элементы. Добавление элемента в конец списка имеет временную сложность $O(1)$ , но операции вставки и удаления по-прежнему имеют ту же эффективность, что и у массива, то есть $O(n)$ .
|
||||
В отличие от массива список позволяет свободно добавлять и удалять элементы. Добавление элемента в конец списка имеет временную сложность $O(1)$ , но операции вставки и удаления по-прежнему имеют ту же эффективность, что и у массива, то есть $O(n)$ .
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -743,7 +743,7 @@ comments: true
|
||||
|
||||
### 5. Конкатенация списков
|
||||
|
||||
Если дан новый список `nums1` , мы можем присоединить его к хвосту исходного списка.
|
||||
Создав новый список `nums1` , мы можем присоединить его к хвосту исходного списка.
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -854,7 +854,7 @@ comments: true
|
||||
|
||||
### 6. Сортировка списка
|
||||
|
||||
После сортировки списка мы сможем применять алгоритмы "бинарный поиск" и "два указателя", которые очень часто встречаются в задачах по массивам.
|
||||
После сортировки списка мы сможем применять алгоритмы "двоичный поиск" и "два указателя", которые очень часто встречаются в задачах по массивам.
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -952,12 +952,12 @@ comments: true
|
||||
|
||||
## 4.3.2 Реализация списка
|
||||
|
||||
Во многих языках программирования списки встроены в стандартную библиотеку, например в Java, C++ и Python. Их реализация довольно сложна, а настройки параметров тщательно продуманы: начальная емкость, коэффициент расширения и так далее. Если тебе интересно, стоит заглянуть в исходный код.
|
||||
Во многих языках программирования списки встроены в стандартную библиотеку, например в Java, C++, Python и других языках. Их реализация довольно сложна, а настройки параметров тщательно продуманы: начальная емкость, коэффициент расширения и так далее. Если это интересно, стоит заглянуть в исходный код.
|
||||
|
||||
Чтобы лучше понять принцип работы списка, попробуем реализовать его упрощенную версию, в которой есть три ключевых аспекта проектирования.
|
||||
|
||||
- **Начальная емкость**: выбрать разумную начальную емкость внутреннего массива. В этом примере мы берем 10.
|
||||
- **Учет количества элементов**: объявить переменную `size` , которая будет хранить текущее число элементов в списке и обновляться в реальном времени при вставке и удалении элементов. С помощью этой переменной можно находить конец списка и понимать, требуется ли расширение.
|
||||
- **Учет количества элементов**: объявить переменную `size` , которая будет хранить текущее число элементов в списке и обновляться в реальном времени при вставке и удалении элементов. С помощью этой переменной можно определять конец списка и понимать, требуется ли расширение.
|
||||
- **Механизм расширения**: если при вставке элементов емкость списка исчерпана, нужно выполнить расширение. Для этого сначала создается больший массив с учетом коэффициента расширения, а затем все элементы текущего массива по порядку переносятся в новый. В этом примере мы считаем, что каждый раз массив расширяется в 2 раза.
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -4,13 +4,13 @@ comments: true
|
||||
|
||||
# 4.4 Оперативная память и кэш *
|
||||
|
||||
В первых двух разделах этой главы мы разобрали массивы и связные списки - две фундаментальные и важные структуры данных, которые соответственно представляют две физические структуры хранения: "непрерывное хранение" и "разрозненное хранение".
|
||||
В первых двух разделах этой главы мы разобрали массивы и связные списки - две базовые и важные структуры данных, которые представляют соответственно непрерывное хранение и разрозненное хранение.
|
||||
|
||||
На практике **физическая структура во многом определяет, насколько эффективно программа использует память и кэш**, а это, в свою очередь, влияет на общую производительность алгоритмической программы.
|
||||
На практике **физическая структура во многом определяет, насколько эффективно программа использует память и кэш**, а это, в свою очередь, влияет на общую производительность алгоритма.
|
||||
|
||||
## 4.4.1 Устройства хранения данных в компьютере
|
||||
|
||||
В компьютере есть три типа устройств хранения данных: <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 Устройства хранения данных в компьютере </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 становится больше, время передачи данных растет, а задержка доступа к элементам увеличивается. При текущем уровне технологий многоуровневая структура кэша является лучшим балансом между емкостью, скоростью и стоимостью.
|
||||
|
||||
{ class="animation-figure" }
|
||||
@@ -37,11 +37,11 @@ comments: true
|
||||
|
||||
!!! tip
|
||||
|
||||
Иерархия памяти компьютера отражает тонкий баланс между скоростью, емкостью и стоимостью. На самом деле подобные компромиссы встречаются почти во всех отраслях инженерии: приходится искать оптимальный баланс между преимуществами и ограничениями.
|
||||
Иерархия памяти компьютера отражает тонкий баланс между скоростью, емкостью и стоимостью. Подобные компромиссы встречаются почти во всех областях инженерии: приходится искать оптимальный баланс между преимуществами и ограничениями.
|
||||
|
||||
В итоге **жесткий диск используется для долговременного хранения больших объемов данных, оперативная память - для временного хранения данных, с которыми программа работает прямо сейчас, а кэш - для хранения часто используемых данных и инструкций**, чтобы ускорять выполнение программ. Все три уровня работают совместно и обеспечивают эффективную работу компьютерной системы.
|
||||
|
||||
Как показано на рисунке 4-10, во время выполнения программы данные читаются с жесткого диска в оперативную память, а затем используются CPU в вычислениях. Кэш можно рассматривать как часть CPU: **он интеллектуально подгружает данные из оперативной памяти**, обеспечивая CPU высокоскоростной доступ и тем самым значительно ускоряя выполнение программы и уменьшая зависимость от более медленной RAM.
|
||||
Как показано на рисунке 4-10, во время выполнения программы данные читаются с жесткого диска в оперативную память, а затем используются CPU в вычислениях. Кэш можно рассматривать как часть CPU: **он подгружает данные из оперативной памяти**, обеспечивая CPU высокоскоростной доступ и тем самым значительно ускоряя выполнение программы и уменьшая зависимость от более медленной RAM.
|
||||
|
||||
{ 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
|
||||
На практике **массивы и связные списки по-разному используют кэш**, и это проявляется в нескольких аспектах.
|
||||
|
||||
- **Занимаемое пространство**: элементы связного списка занимают больше места, чем элементы массива, поэтому в кэше помещается меньше полезных данных.
|
||||
- **Строки кэша**: данные списка разбросаны по памяти, а кэш загружает данные "строками", поэтому доля бесполезно загружаемых данных оказывается выше.
|
||||
- **Механизм предвыборки**: шаблон доступа к данным у массивов более "предсказуем", чем у списков, то есть системе легче угадать, какие данные понадобятся следующими.
|
||||
- **Строки кэша**: данные списка разбросаны по памяти, а кэш загружает данные строками, поэтому доля бесполезно загружаемых данных оказывается выше.
|
||||
- **Механизм предвыборки**: шаблон доступа к данным у массивов более предсказуем, чем у списков, то есть системе легче угадать, какие данные понадобятся следующими.
|
||||
- **Пространственная локальность**: массив хранится в компактной области памяти, поэтому данные рядом с уже загруженными с большей вероятностью скоро будут использованы.
|
||||
|
||||
В целом **массивы имеют более высокий коэффициент попадания в кэш, поэтому по эффективности операций они обычно превосходят связные списки**. Именно поэтому при решении алгоритмических задач структуры данных на основе массивов часто оказываются предпочтительнее.
|
||||
|
||||
@@ -6,11 +6,11 @@ comments: true
|
||||
|
||||
### 1. Ключевые выводы
|
||||
|
||||
- Массивы и связные списки - это две базовые структуры данных, представляющие два способа хранения данных в памяти компьютера: хранение в непрерывной области и хранение в разрозненных областях. Их свойства во многом взаимно дополняют друг друга.
|
||||
- Массив поддерживает произвольный доступ и занимает меньше памяти; однако вставка и удаление элементов в нем неэффективны, а длина после инициализации неизменяема.
|
||||
- Связный список позволяет эффективно вставлять и удалять узлы путем изменения ссылок (указателей), а также гибко менять длину; однако доступ к узлам неэффективен, а памяти он занимает больше. Распространенные типы списков включают односвязные, циклические и двусвязные списки.
|
||||
- Массивы и связные списки - это две базовые структуры данных, представляющие два способа хранения данных в памяти компьютера: хранение в непрерывном пространстве и хранение в разрозненном пространстве. Их свойства во многом взаимно дополняют друг друга.
|
||||
- Массив поддерживает произвольный доступ и занимает меньше памяти; однако вставка и удаление элементов в нем неэффективны, а длина после инициализации фиксирована.
|
||||
- Связный список позволяет эффективно вставлять и удалять узлы путем изменения ссылок (указателей), а также гибко менять длину; однако доступ к узлам менее эффективен, а памяти он занимает больше. Распространенные типы списков включают односвязные, циклические и двусвязные списки.
|
||||
- Список - это упорядоченная коллекция элементов, поддерживающая добавление, удаление, поиск и изменение, и обычно реализуемая на основе динамического массива. Он сохраняет преимущества массива и при этом может гибко менять длину.
|
||||
- Появление списка значительно повысило практическую полезность массива, хотя это и может приводить к потерям части памяти.
|
||||
- Появление списка значительно повысило практическую ценность массива, хотя это и может приводить к потере части памяти.
|
||||
- Во время работы программы данные в основном хранятся в оперативной памяти. Массив обеспечивает более высокую эффективность использования пространства памяти, а связный список дает большую гибкость в использовании памяти.
|
||||
- Кэш, используя строки кэша, механизм предвыборки, а также пространственную и временную локальность, предоставляет CPU быстрый доступ к данным и заметно повышает эффективность выполнения программ.
|
||||
- Поскольку массивы обычно имеют более высокий коэффициент попадания в кэш, они в большинстве случаев работают эффективнее списков. При выборе структуры данных нужно исходить из конкретных требований и сценариев.
|
||||
|
||||
Reference in New Issue
Block a user