0 added
51 removed
Original
2026-01-01
Modified
2026-02-26
1
<p>Вы уже знакомы с односвязным списком. Эта структура данных позволяет быстро вставлять и удалять элементы. Звучит удобно, но такой подход работает не во всех случаях. В этом уроке вы познакомитесь с двусвязным списком, который лучше подходит для некоторых типичных задач в программировании.</p>
1
<p>Вы уже знакомы с односвязным списком. Эта структура данных позволяет быстро вставлять и удалять элементы. Звучит удобно, но такой подход работает не во всех случаях. В этом уроке вы познакомитесь с двусвязным списком, который лучше подходит для некоторых типичных задач в программировании.</p>
2
<p>Есть несколько задач, для которых односвязный список подходит не очень хорошо. В частности, он несимметричен - если вставка и удаление в начале списка выполняются за константное время O(1), то в конце - за линейное O(n). Если в списке будет тысяча элементов, вставка в начало может оказаться в тысячу раз быстрее, чем вставка в конец.</p>
2
<p>Есть несколько задач, для которых односвязный список подходит не очень хорошо. В частности, он несимметричен - если вставка и удаление в начале списка выполняются за константное время O(1), то в конце - за линейное O(n). Если в списке будет тысяча элементов, вставка в начало может оказаться в тысячу раз быстрее, чем вставка в конец.</p>
3
<p>Другая задача, которую трудно решить с помощью односвязного списка - вставка перед заданным узлом. Предположим, у нас есть текст на русском языке, где каждый элемент - либо отдельное слово, либо знак препинания. В тексте могут быть ошибки: например, могут отсутствовать запятые и точки. Мы предполагаем, что если слово начинается с большой буквы, перед ним должна быть точка. Если в тексте есть союзы "но" и "а", перед ними должна стоять запятая.</p>
3
<p>Другая задача, которую трудно решить с помощью односвязного списка - вставка перед заданным узлом. Предположим, у нас есть текст на русском языке, где каждый элемент - либо отдельное слово, либо знак препинания. В тексте могут быть ошибки: например, могут отсутствовать запятые и точки. Мы предполагаем, что если слово начинается с большой буквы, перед ним должна быть точка. Если в тексте есть союзы "но" и "а", перед ними должна стоять запятая.</p>
4
<p>Расссмотрим такой пример:</p>
4
<p>Расссмотрим такой пример:</p>
5
<p>В этом тексте не хватает запятой перед словом "но" и точки перед словом "Какое". Попробуем решить эту задачу с помощью односвязного списка. Нам нужно:</p>
5
<p>В этом тексте не хватает запятой перед словом "но" и точки перед словом "Какое". Попробуем решить эту задачу с помощью односвязного списка. Нам нужно:</p>
6
<ul><li>Поместить слова в односвязный список</li>
6
<ul><li>Поместить слова в односвязный список</li>
7
<li>Найти слово "но"</li>
7
<li>Найти слово "но"</li>
8
<li>Попробовать вставить перед ним запятую</li>
8
<li>Попробовать вставить перед ним запятую</li>
9
</ul><p>Здесь мы сталкиваемся с тем, что в узле нет ссылки на предыдущий элемент, только на следующий:</p>
9
</ul><p>Здесь мы сталкиваемся с тем, что в узле нет ссылки на предыдущий элемент, только на следующий:</p>
10
<p>Односвязный список устроен так, что для вставки запятой между словами "наука" и "но" нужно модифицировать именно узел со словом "наука". Эти детали делают наш возможный алгоритм сложным и медленным.</p>
10
<p>Односвязный список устроен так, что для вставки запятой между словами "наука" и "но" нужно модифицировать именно узел со словом "наука". Эти детали делают наш возможный алгоритм сложным и медленным.</p>
11
<p>Для подобных задач лучше использовать списки, в котором хранятся обе ссылки.</p>
11
<p>Для подобных задач лучше использовать списки, в котором хранятся обе ссылки.</p>
12
<h2>Двусвязный список</h2>
12
<h2>Двусвязный список</h2>
13
<p>В каждом узле двусвязного списка хранится две ссылки - на следующий и на предыдущий узел. Кроме того, в нем хранятся ссылки и на голову списка (первый элемент), и на его хвост (последний элемент):</p>
13
<p>В каждом узле двусвязного списка хранится две ссылки - на следующий и на предыдущий узел. Кроме того, в нем хранятся ссылки и на голову списка (первый элемент), и на его хвост (последний элемент):</p>
14
<p>Как и в случае с односвязным списком, нам приходится особым образом хранить ссылку на следующий узел для последнего узла. Там мы помещаем значение null - пустую ссылку, которая ни на что не указывает. На рисунке последний узел в списке - это D.</p>
14
<p>Как и в случае с односвязным списком, нам приходится особым образом хранить ссылку на следующий узел для последнего узла. Там мы помещаем значение null - пустую ссылку, которая ни на что не указывает. На рисунке последний узел в списке - это D.</p>
15
<p>У первого узла не может быть предыдущего узла, поэтому и здесь мы записываем null вместо ссылки. На рисунке первый узел в списке - это A.</p>
15
<p>У первого узла не может быть предыдущего узла, поэтому и здесь мы записываем null вместо ссылки. На рисунке первый узел в списке - это A.</p>
16
<p>За счет изменения структуры, мы получаем две новые возможности:</p>
16
<p>За счет изменения структуры, мы получаем две новые возможности:</p>
17
<ul><li>Вставка и удаление в конце списка становятся настолько же быстрыми, как и в начале. Теперь они выполняются за константное время O(1)</li>
17
<ul><li>Вставка и удаление в конце списка становятся настолько же быстрыми, как и в начале. Теперь они выполняются за константное время O(1)</li>
18
<li>Вставка узла перед заданным узлом становится такой же простой операцией, как и вставка после</li>
18
<li>Вставка узла перед заданным узлом становится такой же простой операцией, как и вставка после</li>
19
</ul><p>Конечно, есть и минусы. Во-первых, сама структура и код становятся сложнее. Во-вторых, структура теперь занимает больше памяти, поскольку в каждом узле хранится две ссылки, а не одна.</p>
19
</ul><p>Конечно, есть и минусы. Во-первых, сама структура и код становятся сложнее. Во-вторых, структура теперь занимает больше памяти, поскольку в каждом узле хранится две ссылки, а не одна.</p>
20
<h2>Вставка узла в начало списка</h2>
20
<h2>Вставка узла в начало списка</h2>
21
<p>Посмотрим, как выглядит вставка узла в начало списка:</p>
21
<p>Посмотрим, как выглядит вставка узла в начало списка:</p>
22
-
<p><strong>Javascript</strong></p>
23
-
<p><strong>Python</strong></p>
24
-
<p><strong>PHP</strong></p>
25
-
<p><strong>Java</strong></p>
26
<p>Разберем этот фрагмент кода подробнее.</p>
22
<p>Разберем этот фрагмент кода подробнее.</p>
27
<p>Двусвязный список, как и односвязный, требует определения двух классов:</p>
23
<p>Двусвязный список, как и односвязный, требует определения двух классов:</p>
28
<p>Первый класс - DoublyLinkedListNode. Он описывает узел двусвязного списка и состоит из таких компонентов:</p>
24
<p>Первый класс - DoublyLinkedListNode. Он описывает узел двусвязного списка и состоит из таких компонентов:</p>
29
<ul><li>Значения - value</li>
25
<ul><li>Значения - value</li>
30
<li>Ссылки на предыдущий узел - previous</li>
26
<li>Ссылки на предыдущий узел - previous</li>
31
<li>Ссылки на следующий узел - next</li>
27
<li>Ссылки на следующий узел - next</li>
32
</ul><p>Второй класс - DoublyLinkedList. Он представляет список целиком, вместе с его операциями-алгоритмами. Там находятся:</p>
28
</ul><p>Второй класс - DoublyLinkedList. Он представляет список целиком, вместе с его операциями-алгоритмами. Там находятся:</p>
33
<ul><li>Ссылка на первый узел - head</li>
29
<ul><li>Ссылка на первый узел - head</li>
34
<li>Ссылка на последний узел - tail</li>
30
<li>Ссылка на последний узел - tail</li>
35
<li>Различные методы, например:<ul><li>insertBegin(value) - вставка в начало</li>
31
<li>Различные методы, например:<ul><li>insertBegin(value) - вставка в начало</li>
36
<li>insertEnd(value) - вставка в конец</li>
32
<li>insertEnd(value) - вставка в конец</li>
37
<li>removeBegin() - удаление из начала</li>
33
<li>removeBegin() - удаление из начала</li>
38
</ul></li>
34
</ul></li>
39
</ul><p>Новый список пуст, поэтому поля head и tail содержат значение null:</p>
35
</ul><p>Новый список пуст, поэтому поля head и tail содержат значение null:</p>
40
<p>После вставки первого узла head и tail содержат его адрес. При этом поля previous и next у этого узла никуда не указывают, потому что он одновременно является первым и последним в списке - другими словами, у него нет ни предыдущего, ни следующего узла:</p>
36
<p>После вставки первого узла head и tail содержат его адрес. При этом поля previous и next у этого узла никуда не указывают, потому что он одновременно является первым и последним в списке - другими словами, у него нет ни предыдущего, ни следующего узла:</p>
41
<p>Теперь посмотрим на фрагмент кода:</p>
37
<p>Теперь посмотрим на фрагмент кода:</p>
42
-
<p><strong>Javascript</strong></p>
43
-
<p><strong>Python</strong></p>
44
-
<p><strong>PHP</strong></p>
45
-
<p><strong>Java</strong></p>
46
<p>Условие this.head == null выполняется для пустого списка. Нам достаточно создать узел с пустыми ссылками на предыдущий и следующий узлы, а затем присвоить его адрес полям this.head и this.tail.</p>
38
<p>Условие this.head == null выполняется для пустого списка. Нам достаточно создать узел с пустыми ссылками на предыдущий и следующий узлы, а затем присвоить его адрес полям this.head и this.tail.</p>
47
<p>При вставке каждого следующего узла в начало, head всегда будет указывать на новый узел. Значение tail при этом не изменится, потому что хвост списка остается прежним. Поле next новой головы списка будет указывать на прежнюю голову, а в поле previous старой головы вместо null должен появиться адрес новой головы:</p>
39
<p>При вставке каждого следующего узла в начало, head всегда будет указывать на новый узел. Значение tail при этом не изменится, потому что хвост списка остается прежним. Поле next новой головы списка будет указывать на прежнюю голову, а в поле previous старой головы вместо null должен появиться адрес новой головы:</p>
48
<p>Это довольно сложная логика, которая требует аккуратной реализации и проверки граничных условий. Поэтому код методов двусвязного списка сложнее, чем код методов односвязного. Посмотрите на этот пример:</p>
40
<p>Это довольно сложная логика, которая требует аккуратной реализации и проверки граничных условий. Поэтому код методов двусвязного списка сложнее, чем код методов односвязного. Посмотрите на этот пример:</p>
49
-
<p><strong>Javascript</strong></p>
50
-
<p><strong>Python</strong></p>
51
-
<p><strong>PHP</strong></p>
52
-
<p><strong>Java</strong></p>
53
<p>Создавая узел, мы сразу записываем в поле next текущее значение this.head- текущую голову. Поле previous текущей головы должно ссылать на новый узел, за это отвечает такая строка:</p>
41
<p>Создавая узел, мы сразу записываем в поле next текущее значение this.head- текущую голову. Поле previous текущей головы должно ссылать на новый узел, за это отвечает такая строка:</p>
54
-
<p><strong>Javascript</strong></p>
55
-
<p><strong>Python</strong></p>
56
-
<p><strong>PHP</strong></p>
57
-
<p><strong>Java</strong></p>
58
<p>Наконец, новый узел становится новой головой списка:</p>
42
<p>Наконец, новый узел становится новой головой списка:</p>
59
-
<p><strong>Javascript</strong></p>
60
-
<p><strong>Python</strong></p>
61
-
<p><strong>PHP</strong></p>
62
-
<p><strong>Java</strong></p>
63
<h2>Вставка узла в конец списка</h2>
43
<h2>Вставка узла в конец списка</h2>
64
<p>Перейдем к вставке узла в конец списка:</p>
44
<p>Перейдем к вставке узла в конец списка:</p>
65
-
<p><strong>Javascript</strong></p>
66
-
<p><strong>Python</strong></p>
67
-
<p><strong>PHP</strong></p>
68
-
<p><strong>Java</strong></p>
69
<p>Такая вставка симметрична вставке в начало. Разница только в том, что здесь мы должны везде менять местами head и tail, а также previous и next.</p>
45
<p>Такая вставка симметрична вставке в начало. Разница только в том, что здесь мы должны везде менять местами head и tail, а также previous и next.</p>
70
<h2>Удаление узла</h2>
46
<h2>Удаление узла</h2>
71
<p>Перейдем к операциям удаления:</p>
47
<p>Перейдем к операциям удаления:</p>
72
-
<p><strong>Javascript</strong></p>
73
-
<p><strong>Python</strong></p>
74
-
<p><strong>PHP</strong></p>
75
-
<p><strong>Java</strong></p>
76
<p>Метод удаления возвращает значение из удаленного узла. Если список пуст и удалять нечего, метод возвращает undefined. В остальных случаях мы сохраняем значение в переменную result:</p>
48
<p>Метод удаления возвращает значение из удаленного узла. Если список пуст и удалять нечего, метод возвращает undefined. В остальных случаях мы сохраняем значение в переменную result:</p>
77
-
<p><strong>Javascript</strong></p>
78
-
<p><strong>Python</strong></p>
79
-
<p><strong>PHP</strong></p>
80
-
<p><strong>Java</strong></p>
81
<p>Если this.head == this.tail, значит, в списке находится один последний узел - он является одновременно и головой, и хвостом. Чтобы его удалить, достаточно обнулить head и tail:</p>
49
<p>Если this.head == this.tail, значит, в списке находится один последний узел - он является одновременно и головой, и хвостом. Чтобы его удалить, достаточно обнулить head и tail:</p>
82
-
<p><strong>Javascript</strong></p>
83
-
<p><strong>Python</strong></p>
84
-
<p><strong>PHP</strong></p>
85
-
<p><strong>Java</strong></p>
86
<p>А теперь посмотрим обратный пример - избавимся от первого узла в списке. Сначала записываем в head ссылку на второй узел, а потом обнуляем у нее поле previous:</p>
50
<p>А теперь посмотрим обратный пример - избавимся от первого узла в списке. Сначала записываем в head ссылку на второй узел, а потом обнуляем у нее поле previous:</p>
87
-
<p><strong>Javascript</strong></p>
88
-
<p><strong>Python</strong></p>
89
-
<p><strong>PHP</strong></p>
90
-
<p><strong>Java</strong></p>
91
<h2>Перебор значений в прямом порядке</h2>
51
<h2>Перебор значений в прямом порядке</h2>
92
<p>Раньше для работы с массивами, связными и двусвязными списками, программисты писали разный код. Например, если надо было просуммировать элементы массива и элементы связного списка, приходилось писать две похожие функции. Каждая из них складывала элементы коллекций, но доступ к этим элементам у массива и списка был разным.</p>
52
<p>Раньше для работы с массивами, связными и двусвязными списками, программисты писали разный код. Например, если надо было просуммировать элементы массива и элементы связного списка, приходилось писать две похожие функции. Каждая из них складывала элементы коллекций, но доступ к этим элементам у массива и списка был разным.</p>
93
<p>Дублирование кода - одна из самых неприятных вещей в программировании. При внесении правок можно забыть поменять код в одной из копий и это приведет к ошибкам, которые трудно обнаружить. Но есть решение этой проблемы: в языках постоянно появляются новые инструменты, которые помогают избавиться от старых ошибок и реже дублировать код.</p>
53
<p>Дублирование кода - одна из самых неприятных вещей в программировании. При внесении правок можно забыть поменять код в одной из копий и это приведет к ошибкам, которые трудно обнаружить. Но есть решение этой проблемы: в языках постоянно появляются новые инструменты, которые помогают избавиться от старых ошибок и реже дублировать код.</p>
94
<p>Чтобы просуммировать элементы из разных структур данных, сейчас достаточно написать всего одну функцию. Это стало возможным благодаря итераторам. Обычно итерацией в программировании называют отдельный шаг цикла. Но у слова есть и другое значение.</p>
54
<p>Чтобы просуммировать элементы из разных структур данных, сейчас достаточно написать всего одну функцию. Это стало возможным благодаря итераторам. Обычно итерацией в программировании называют отдельный шаг цикла. Но у слова есть и другое значение.</p>
95
<p><strong>Итератор</strong>- это объект, который одинаковым образом перебирает элементы коллекции, независимо от структуры данных. Скажем, мы можем написать функцию суммирования элементов любой коллекции и вызвать ее для списка:</p>
55
<p><strong>Итератор</strong>- это объект, который одинаковым образом перебирает элементы коллекции, независимо от структуры данных. Скажем, мы можем написать функцию суммирования элементов любой коллекции и вызвать ее для списка:</p>
96
-
<p><strong>Javascript</strong></p>
97
-
<p><strong>Python</strong></p>
98
-
<p><strong>PHP</strong></p>
99
-
<p><strong>Java</strong></p>
100
<p>Эта функция сможет работать и с нашим двусвязным списком, но для этого нам потребуется реализовать собственный итератор.</p>
56
<p>Эта функция сможет работать и с нашим двусвязным списком, но для этого нам потребуется реализовать собственный итератор.</p>
101
<p>Однако, здесь есть проблема. Массив и односвязный список имеют естественный порядок перебора - от начала к концу. В двусвязном списке поддерживаются два равноправных порядка:</p>
57
<p>Однако, здесь есть проблема. Массив и односвязный список имеют естественный порядок перебора - от начала к концу. В двусвязном списке поддерживаются два равноправных порядка:</p>
102
<ul><li>От начала к концу</li>
58
<ul><li>От начала к концу</li>
103
<li>От конца к началу</li>
59
<li>От конца к началу</li>
104
</ul><p>Поэтому двусвязный список должен иметь два итератора - прямой и обратный. Чтобы так сделать, можно возвращать итераторы из методов класса. Например, метод fore() может создавать и возвращать прямой итератор:</p>
60
</ul><p>Поэтому двусвязный список должен иметь два итератора - прямой и обратный. Чтобы так сделать, можно возвращать итераторы из методов класса. Например, метод fore() может создавать и возвращать прямой итератор:</p>
105
-
<p><strong>Javascript</strong></p>
106
-
<p><strong>Python</strong></p>
107
-
<p><strong>PHP</strong></p>
108
<p>Использовать итератор можно так:</p>
61
<p>Использовать итератор можно так:</p>
109
-
<p><strong>Javascript</strong></p>
110
-
<p><strong>Python</strong></p>
111
-
<p><strong>PHP</strong></p>
112
-
<p><strong>Java</strong></p>
113
<p>В JavaScript используется синтаксис function* и yield, который упрощает работу с итераторами. В нашем примере порядок действий такой:</p>
62
<p>В JavaScript используется синтаксис function* и yield, который упрощает работу с итераторами. В нашем примере порядок действий такой:</p>
114
<ul><li>Начинаем с первого узла, адрес которого хранится в поле head</li>
63
<ul><li>Начинаем с первого узла, адрес которого хранится в поле head</li>
115
<li>Пробегаем по всем узлам списка</li>
64
<li>Пробегаем по всем узлам списка</li>
116
<li>Передаем значения узлов в вызывающую функцию с помощью конструкции yield</li>
65
<li>Передаем значения узлов в вызывающую функцию с помощью конструкции yield</li>
117
</ul><h2>Выводы</h2>
66
</ul><h2>Выводы</h2>
118
<ul><li>Односвязный список подходит не для всех задач - он предоставляет только последовательный доступ к последующим элементам</li>
67
<ul><li>Односвязный список подходит не для всех задач - он предоставляет только последовательный доступ к последующим элементам</li>
119
<li>Чтобы справиться с этими трудностями, программисты используют такую структуру данных, как двусвязный список.</li>
68
<li>Чтобы справиться с этими трудностями, программисты используют такую структуру данных, как двусвязный список.</li>
120
<li>В каждом узле двусвязного списка хранится не только ссылка на следующий узел (как у односвязного), но и на предыдущий.</li>
69
<li>В каждом узле двусвязного списка хранится не только ссылка на следующий узел (как у односвязного), но и на предыдущий.</li>
121
<li>К плюсам двусвязного списка можно отнести возможность "пробегать" по списку как вперед, так и назад, а к минусам - сложный код и больший расход памяти.</li>
70
<li>К плюсам двусвязного списка можно отнести возможность "пробегать" по списку как вперед, так и назад, а к минусам - сложный код и больший расход памяти.</li>
122
<li>Итераторы позволяют писать один код для работы с коллекциями разных типов. Мы можем реализовать итераторы для тех структур данных, которые мы разрабатываем.</li>
71
<li>Итераторы позволяют писать один код для работы с коллекциями разных типов. Мы можем реализовать итераторы для тех структур данных, которые мы разрабатываем.</li>
123
</ul>
72
</ul>