Longest Consecutive Sequence
Дан массив целых чисел nums в произвольном порядке. Последовательность подряд идущих чисел — это группа значений x, x+1, x+2 и так далее, каждое из которых встречается в nums. Верните длину самой длинной последовательности подряд идущих чисел. Значение, встречающееся более одного раза, учитывается один раз.
Функция
- numsinteger-array
- целые числа в любом порядке; повторы разрешены
- Возвращаетinteger
- длина самой длинной последовательности подряд идущих значений в nums
Ограничения
1 ≤ nums.length ≤ 104-109 ≤ nums[i] ≤ 109- Значения могут повторяться. Позиции в массиве не имеют значения, важно только, какие значения присутствуют.
Примеры
- Ввод
- nums = [40, 4, 39, 1, 3, 2, 41]
- Вывод
- 4
- Пояснение
1,2,3и4присутствуют в массиве, образуя последовательность из 4 чисел, хотя они и разбросаны по массиву. В другой последовательности, от39до41, всего 3 значения.
- Ввод
- nums = [7, 3, 7, 5, 6, 5]
- Вывод
- 3
- Пояснение
5,6и7образуют последовательность из 3 чисел. Вторая7и вторая5ничего не добавляют, а3не может присоединиться, потому что отсутствует4.
- Ввод
- nums = [10, 30, 20]
- Вывод
- 1
- Пояснение
- Никакие два значения не отличаются на 1, поэтому каждый фрагмент содержит одно значение, и ответ — 1.
+17 скрытых тестов при отправке
Дополнительный вопрос
Предположим, значения поступают по одному, и после каждого из них нужно сообщать длину самой длинной последовательности на данный момент. Сможешь обновлять ответ в среднем за O(1) на каждое значение?
Подсказки
Открывайте по одной. Каждая подсказывает чуть больше.
Попробуйте каждое значение в качестве первого числа последовательности и считайте вверх. Какой вопрос вы задаёте снова и снова и сколько стоит каждый ответ, когда вы ищете его в массиве?
Вопрос звучит так: «есть ли
x+1в массиве?». Хеш-множество отвечает на него в среднем за постоянное время и также удаляет повторяющиеся элементы.Начинай подсчёт только со значения
x, для которогоx-1отсутствует в множестве. Затем переходи кx+1,x+2и далее, пока множество содержит эти значения, и сохраняй самый длинный проход. Таким образом, каждое значение будет пройдено только один раз.
Решение
Значения последовательности могут находиться где угодно в массиве, поэтому нельзя считывать элементы последовательности слева направо. Сортировка выстраивает их в нужном порядке за O(n log n). Хеш-множество работает эффективнее: оно отвечает на вопрос «есть ли здесь x+1?» за O(1), и если начинать подсчёт только со значений, для которых отсутствует x-1, каждый элемент будет пройден один раз, что обеспечивает сложность всего поиска O(n).
Считайте вверх от каждого значения, находя его в массиве
Верно, но не успевает на самых больших тестах
Идея
Считай каждое значение возможным началом последовательности. Начиная с x, ищи в массиве x+1; если оно там есть, ищи x+2 и продолжай, пока не обнаружится пропущенное значение. Количество найденных значений — это длина последовательности, начинающейся с x, а ответом будет наибольшая из таких длин.
Это правильно, потому что у каждой последовательности есть наименьшее значение, это значение содержится в nums, и цикл пробует его в качестве начала и проходит всю последовательность. Повторы не мешают: они лишь дважды пробуют одно и то же начало.
Это медленно по двум причинам. При каждой проверке «есть ли оно здесь?» просматривается до n значений, а длинная последовательность проходится заново от каждого из входящих в неё элементов. Возьмём 10^4 значений, образующих одну перемешанную последовательность: на проходы уйдёт в сумме около n²/2 = 5 × 10^7 шагов, и на каждом шаге просматривается в среднем половина массива. Это примерно 2.5 × 10^11 сравнений.
Алгоритм
- Присвойте
bestзначение 0. - Для каждого значения
startвnumsприсвойтеcurrentзначениеstart, аlength— значение 1. - Пока при просмотре
numsнаходитсяcurrent+1, увеличивайтеcurrentиlengthна 1. - Сохраните
lengthвbest, если оно больше. - Верните
best.
def longestConsecutive(nums):
best = 0
for start in nums:
current = start
length = 1
# "in" on a list reads it from the front until it finds the value.
while current + 1 in nums:
current += 1
length += 1
best = max(best, length)
return bestОтсортируйте, затем подсчитайте серии
Идея
Сортировка располагает значения каждого последовательного ряда рядом друг с другом. [40, 4, 39, 1, 3, 2, 41] превращается в [1, 2, 3, 4, 39, 40, 41], а ряды читаются слева направо: от 1 до 4, затем скачок к 39.
Пройдитесь по отсортированным значениям и отслеживайте длину текущего ряда. Значение, на единицу большее предыдущего, продлевает ряд. Значение, равное предыдущему, — это повтор: пропустите его, поскольку оно ни продлевает ряд, ни завершает его. Любое другое значение — это разрыв, и с него начинается новый ряд длины 1.
Сортировка требует O(n log n), а проход — O(n). Сортировка на месте не требует дополнительного массива, но меняет порядок элементов во входных данных вызывающего кода; языки, которые сортируют копию, используют O(n) памяти.
Алгоритм
- Отсортируй
numsпо возрастанию. - Задай
bestиrunравными 1, поскольку массив никогда не бывает пустым. - Для каждого индекса
i, начиная с 1, пропустиnums[i], если он равенnums[i-1]. - Если
nums[i]равенnums[i-1]+1, прибавь 1 кrun; иначе задайrunравным 1. Сохрани значениеrunвbest, если оно больше. - Верни
best.
def longestConsecutive(nums):
nums.sort()
best = 1
run = 1
for i in range(1, len(nums)):
if nums[i] == nums[i - 1]:
continue # a repeat neither extends nor breaks the run
if nums[i] == nums[i - 1] + 1:
run += 1
else:
run = 1
best = max(best, run)
return bestХеш-множество, подсчёт начинается только с начала каждого запуска
Идея
Поместите каждое значение в хеш-множество. Теперь проверка «есть ли x+1?» в среднем стоит O(1) вместо сканирования, а повторяющиеся значения сводятся к одной записи.
Если начинать обход с каждого значения, работа всё равно будет повторяться: в последовательности 1, 2, 3, 4 вы сделаете 3 шага от 1, 2 от 2 и 1 от 3. Поэтому начинайте обход только с первого значения последовательности. Значение x является первым, только если x-1 отсутствует в множестве. В [40, 4, 39, 1, 3, 2, 41] этому условию соответствуют только 1 и 39: от 1 вы дойдёте до 4, длина будет равна 4, а от 39 — до 41, длина будет равна 3.
Каждое значение принадлежит ровно одной последовательности, и только обход от первого значения этой последовательности проходит через него, поэтому суммарно все обходы займут не более n шагов. Добавьте по одной проверке принадлежности для каждого значения и построение множества — итоговая временная сложность составит O(n), а для множества потребуется O(n) памяти.
Перебирайте множество, а не nums. Если первое значение последовательности из 2,500 значений встречается в nums 2,000 раз, перебор nums приведёт к тому, что вы пройдёте эту последовательность 2,000 раз.
Алгоритм
- Помести каждое значение из
numsв хеш-множествоvaluesи установиbestв 0. - Для каждого значения
xв множестве пропусти его, еслиx-1есть в множестве: это не первое значение последовательности. - Иначе установи
endвxи увеличивай его на 1, покаend+1есть в множестве. - Сохрани
end-x+1вbest, если оно больше текущего значения. - Верни
best.
def longestConsecutive(nums):
values = set(nums)
best = 0
for value in values:
# Only a value with no left neighbour starts a run.
if value - 1 in values:
continue
end = value
while end + 1 in values:
end += 1
best = max(best, end - value + 1)
return best
Ловушки и крайние случаи
Большинство неправильных ответов связано с повторяющимися значениями, а большинство медленных ответов — с тем, что один и тот же участок обходят больше одного раза.
- Если считать повтор пропуском или шагом после сортировки. В
[1, 2, 2, 3]сброс последовательности на второй2даст 2, а подсчёт его как шага даст 4. Ответ — 3. - Если задать
bestзначение 0 перед обходом отсортированного массива и обновлять его только внутри цикла. Тогда для массива с одним значением вернётся 0 вместо 1. - Если начинать обход с каждого значения множества, а не только с начала последовательностей. Ответ будет правильным, но один участок из
10^4значений потребует5 × 10^7шагов — именно ту квадратичную работу, от которой множество должно было избавить. - Если перебирать
nums, а не множество, когда значения повторяются. Участок, начинающийся со значения, которое встречается тысячи раз, будет пройден тысячи раз. - Если отмечать значения в массиве, индексируемом по значению. Значения достигают
±10^9, поэтому массиву потребуется2 × 10^9элементов.
Частые вопросы4
Какова временная сложность задачи «Самая длинная последовательность подряд идущих чисел»?
Решение с хеш-множеством в среднем работает за время O(n) и использует O(n) дополнительной памяти. Сортировка и последующий подсчёт длин серий требуют времени O(n log n). Поиск в массиве каждого следующего значения без множества может потребовать до O(n³).
Почему решение с хеш-множеством имеет сложность O(n), если в нём есть цикл while внутри цикла for?
Внутренний цикл запускается только для значения, у которого отсутствует левый сосед x-1, то есть для первого значения в его последовательности. Каждое значение пропускается при обходе своей последовательности и не пропускается при других обходах, поэтому все внутренние циклы вместе выполняют не более n шагов. Внешний цикл добавляет одну проверку на каждое значение, всего получается O(n).
Можешь решить задачу «Самая длинная последовательность подряд идущих чисел» без использования дополнительной памяти?
Да, если входные данные можно переставить: отсортируйте их на месте и подсчитайте серии за один проход, пропуская повторы. Для этого требуется дополнительная память O(1), но время — O(n log n). Для решения за O(n) нужна хеш-множество.
Может ли структура union-find решить задачу о самой длинной последовательности последовательных чисел?
Да. Сделай каждое уникальное значение отдельным множеством, объединяй x с x+1, если присутствуют оба значения, и возвращай размер наибольшего множества. Алгоритм работает за время, близкое к O(n), но для него нужны отображение значений на индексы, связи с родителями и размеры, тогда как обход хеш-множества выполняет ту же задачу с помощью одного множества и двух циклов.
Похожие задачи
Задачи на те же идеи. Решите две или три, и приём запомнится.
Python
def longestConsecutive(nums):
# Напишите код здесьСлучай 1
Случай 2
Случай 3
Ввод
nums = [40, 4, 39, 1, 3, 2, 41]
Ожидается
4