Слияние двух отсортированных связанных списков — распространенная проблема на собеседованиях по алгоритмическому кодированию. Задача состоит в том, чтобы объединить два списка в один отсортированный список, сохранив порядок элементов. Хотя на первый взгляд проблема может показаться простой, для ее решения можно использовать несколько подходов, каждый из которых имеет свои компромиссы с точки зрения временной и пространственной сложности.
В этой статье мы рассмотрим различные подходы к решению проблемы слияния двух отсортированных связанных списков. На протяжении всей статьи мы будем давать подробное объяснение каждого подхода, а также анализ его временной и пространственной сложности.
Итак, без лишних слов приступим.
Постановка проблемы:
Вам даны главы двух отсортированных связанных списков
list1иlist2.
Объедините два списка в один отсортированный список. Список должен быть составлен путем соединения узлов первых двух списков.
Возвращает заголовок объединенного связанного списка.
Подход 1: итеративное решение
- Создайте фиктивный узел и указатель, который будет действовать как объединенный список.
- Выполните итерацию по обоим входным спискам одновременно, сравнивая значения текущих узлов.
- Добавьте меньшее значение в объединенный список, переместив соответствующий указатель вперед.
- Продолжайте этот процесс, пока один из списков не достигнет своего конца (т. е. его значение не станет равным нулю).
- Достигнув конца одного списка, добавьте все оставшиеся узлы из другого списка в объединенный список.
- Наконец, верните объединенный список, обратившись к
dummy.next, который указывает на заголовок объединенного списка.
// ES6 Arrow Function
const mergeTwoLists = (list1, list2) => {
// dummy node and pointer
let dummy = new ListNode(0);
let pointer = dummy;
// compare values and add to the merged list
while(list1 !== null && list2 !== null) {
if(list1.val < list2.val) {
pointer.next = list1;
list1 = list1.next;
} else {
pointer.next = list2;
list2 = list2.next;
}
pointer = pointer.next;
}
// Add remaining nodes
pointer.next = list1 !== null ? list1 : list2;
return dummy.next;
}
Временная сложность:O(N + M)
Мы перебираем оба списка один раз, и длина списков равна n и m соответственно. Таким образом, временная сложность линейно пропорциональна общему количеству узлов, присутствующих в списках.
Пространственная сложность:O(1)
Мы используем постоянное количество дополнительного пространства, создавая фиктивный узел и указатель. Слияние выполняется на месте, не требуя дополнительной структуры данных.
Подход 2: рекурсивный
- Базовый случай: если какой-либо из связанных списков пуст, вернуть другой список.
- Сравните значения заголовков двух списков.
- Установите меньшее значение в качестве заголовка объединенного списка.
- Рекурсивно вызовите функцию со следующим узлом списка, имеющим меньшее значение.
- Установите следующий указатель объединенного списка на результат рекурсивного вызова.
- Вернуть объединенный список.
// ES6 Arrow Function
const mergeTwoLists = (list1, list2) => {
if(list1 === null) return list2;
if(list2 === null) return list1;
if(list1.val < list2.val) {
list1.next = mergeTwoLists(list1.next, list2);
return list1;
} else {
list2.next = mergeTwoLists(list1, list2.next);
return list2;
}
}
Временная сложность:O(N + M), где N и M — длины двух входных связанных списков.
Рекурсивная функция вызывается для каждого узла в обоих списках, и каждый вызов занимает постоянное время. Следовательно, временная сложность линейно пропорциональна общему количеству узлов во входных списках.
Пространственная сложность:O(N + M)
Рекурсивные вызовы используют стек вызовов, который увеличивается с количеством рекурсивных вызовов. В худшем случае, когда списки имеют одинаковую длину, максимальная глубина стека вызовов равна N (или M).
И у вас есть это, ребята! Мы изучили два разных подхода, увидели их реализацию в JavaScript и, надеюсь, повеселились. Я надеюсь, что эта статья предоставила вам ценную информацию и помогла лучше понять различные подходы к решению этой проблемы. Удачного кодирования!
Задача - Leetcode 21