HTML Diff
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>