Всем привет!
Недавно я решил начать решать задачки на LeetCode. К этому я пришел, чтобы в будущем на собеседованиях не ударить в грязь лицом и уверенно справляться хотя бы с базовыми задачами на сортировку, работу со строками и тому подобное.
Сначала все шло неплохо: я успешно решал легкие задачки, используя обычные циклы (for, while). Редко когда надо было прям зависать и задумываться над решениями. Чаще, если даже решение было неверным, можно было в процессе искать ошибки и исправлять их. Но тут я наткнулся на задачу с пометкой "Easy" и названием "Merge Two Sorted Lists".
Прочитав название, я не обратил внимания на слово List и подумал, что это очередное задание на сортировку/конкатенацию массивов и в принципе обрадовался: “Щас решу в одну строку”, - подумал я. Однако компилятор остудил мой пыл.
Тогда я понял, что задача не так проста, как казалась и начал подробнее узнавать что же такое связанный список.
Что же такое связанный список?
Связанный список или же Linked List - это структура данных, в которой каждый элемент (узел) хранит два значения: данные и указатель-ссылку на следующий узел. Вот как примерно он выглядит на схеме:

Каждый узел в объекте содержит два параметра val и next. val возвращает данные хранящиеся в этом узле, а next содержит указатель-ссылку на следующий узел. Последний узел всегда указывает на null. На схеме я сделал третий узел ниже остальных, чтобы показать что объект LinkedList не располагается линейно в памяти, а разбросан по ней, и доступ к последующим элементам открывается только лишь по указателям.
Разбираемся с задачей
И так, поняв что же это такое пришло время снова перечитать условие задачи:
Вам даны заголовки двух отсортированных связанных списков list1 и list2.Объедините два списка в один отсортированный список. Список должен быть создан путем сращивания узлов первых двух списков. Верните заголовок объединенного связанного списка.
В задаче сказано вернуть “заголовок” связанного списка. Но что же это такое?
Заголовок (head) - это первый узел связанного списка, с которого начинается доступ ко всем остальным. Через него можно пройтись по всей цепочке узлов.
Сначала для работы с Связанным Списком нам понадобится написать функцию-конструктор которая содержит два параметра val
, next
. Этот код находился прямо в комментариях к задаче. По умолчанию val = 0, next = null
. Что логично когда у нас пустой список у нас не может быть значения, так же как и указателя на следующий узел.
function ListNode(val, next) {
this.val = (val===undefined ? 0 : val)
this.next = (next===undefined ? null : next)
}
Далее мы создаём два связанных списка. (Если вы пишете решение прямо на LeetCode, создавать их вручную не нужно — как и конструктор ListNode
, он уже задан системой. Но я предпочитаю сначала писать и тестировать код в Visual Studio Code, поэтому добавил эти части вручную.)
let list1 = new ListNode(1, new ListNode(2, new ListNode(4)))
let list2 = new ListNode(1, new ListNode(3, new ListNode(4)))
Как можно увидеть тут есть вложенные вызовы, то есть начинаем с первого значения (тут это 1) и вторым параметром передаем новый узел, который в себе содержит значение и ссылку на следующий новый узел и так далее. Таким способом мы получаем два связанных списка:
list1: 1→2→4
list2: 1→3→4
В результате мы должны получить один новый связанный список:
output: 1→1→2→3→4→4
Понимая, как устроены связанные списки, можем приступить к решению
Алгоритм:
создаем пустой узел - dummy, который будеv использовать как начало/заголовок.
создаем переменную current, которая будет "прицелом" и указывать, куда добавлять следующий элемент.
создаем цикл который будет работать пока один из списков не закончится т.е не станет null.
while(list1 && list2)
.в каждой итерации будет сравнивать текущий элемент первого узла с текущим элементом второго.
если элемент первого/второго списка окажется меньше мы записываем всю цепочку в наш “прицел” и смещаем первый/второй список на один вперед.
после каждого сравнения смещаем “прицел” на один вперед.
возвращаем dummy.next, т.е. реальное начало нового списка (первый узел после фиктивного).
Вот как это будет выглядеть на практике:
const mergeTwoLists = function(list1,list2){
let dummy = new ListNode() // новый пустой узел
let current = dummy // указатель на пустой узел
while(list1 && list2){ // цикл выполняется пока не закончится один из списков
if(list1.val < list2.val){ // проверка каждого элемента в списке
current.next = list1 // записываем во второй элемент вcю цепочку узлов list1
list1 = list1.next // смещаем узел list1 на один элемент правее.
} else {
current.next = list2
list2 = list2.next
}
current = current.next // после каждого сравнения не забываем передвинуть указатель
}
current.next = list1 || list2 // добавляем то что осталось от второго списка
return dummy.next // возвращаем наш исходный пустой узел в который сохранялись значения кроме первого значения
}

Как мы видим скорость не самая высокая, но решение вполне понятно и работает.)
Заключение
Решив эту задачу я понял сразу две вещи:
Не все задачи с пометкой "easy" легко решить за несколько минут - иногда это вопрос знаний, а не сложности.
Понять структуру - 70% пути к решению
После того как задача наконец решилась, я почувствовал радость и настоящее ощущение прогресса. Именно такие моменты и питают стремление двигаться дальше, учиться новому и не сдаваться. Я рад, что LeetCode помогает мне — как и тысячам других людей — развиваться в программировании, шаг за шагом приближаясь к новым вершинам и целям.
wataru
Что комментарии в коде, что текст статьи - просто попугайное переложение кода. Ну какой в этом смысл? Это не несет никакой пользы никому. Вы или рассказывайте "почему" и "как", а не "что", или хотя бы приведите рассуждения по шагам, как можно до решения додуматься. Без этого статья не имеет никакой ценности.