Code › algorithm-study

LeetCode 128 - Longest Consecutive Sequence

set을 사용해 연속 수열의 시작점에서만 길이를 계산하는 풀이

정렬 없이 가장 긴 연속 수열의 길이를 구하는 Medium 문제다. 모든 숫자에서 수열을 세기 시작하면 같은 구간을 반복해서 보게 되므로, 이전 숫자가 없는 값만 시작점으로 삼는 방식으로 정리했다.


문제 링크 & 설명

  • 문제 링크: 128. Longest Consecutive Sequence
  • 요약: 정수 배열 nums가 주어졌을 때, 연속된 정수로 만들 수 있는 가장 긴 수열의 길이를 반환하는 문제.

예를 들어 입력이 이렇게 주어진다고 해보자.

nums = [100, 4, 200, 1, 3, 2]

이 배열에서 가장 긴 연속 수열은 [1, 2, 3, 4]이고, 길이는 4다. 배열 안에서 원소들이 정렬되어 있지는 않지만, 값 기준으로 연속된 숫자를 찾아야 한다.


접근 방법

정렬을 하면 인접한 값을 비교하면서 연속 구간을 셀 수 있다. 다만 정렬 비용이 O(n log n)이기 때문에, set으로 값의 존재 여부를 평균 상수시간에 확인해서 정렬 비용을 피한다.

먼저 nums를 set으로 바꾼다. 이렇게 하면 어떤 값이 배열에 있는지 바로 알아낼 수 있다.

num_set = set(nums)

그 다음 모든 숫자를 순회하되, 아무 숫자에서나 수열을 시작하지 않는다. 현재 값이 num일 때 num - 1이 set 안에 있다면, num은 어떤 연속 수열의 중간에 있는 값이다. 예를 들어 3을 보고 있는데 2가 이미 있다면, 3에서 새로 시작할 필요가 없고 1이나 2 쪽에서 시작했을 때 같은 수열을 한 번에 셀 수 있다.

if num - 1 in num_set:
    continue

num - 1이 없다면 그 숫자는 연속 수열의 시작점이다. 그때부터 cur_num을 하나씩 늘리면서 set 안에 값이 존재하는 동안 streak을 증가시킨다.

streak = 0
cur_num = num

while cur_num in num_set:
    streak += 1
    cur_num += 1

for loop 안에 while loop가 있어서 nested loop 구조이지만, 실제로는 시작점에서만 while loop를 돈다. 중간 값들은 num - 1 조건에서 건너뛰기 때문에 같은 연속 구간을 여러 번 다시 세지 않는다.


트러블 슈팅

처음에는 바깥쪽 for loop에서 num_set이 아니라 nums를 그대로 순회했다. 값의 존재 여부는 set으로 확인하니까 괜찮다고 생각했지만, nums에는 중복 값이 남아 있어서 같은 시작점을 여러 번 다시 처리할 수 있었다.

예를 들어 시작점에 해당하는 값이 입력 배열에 여러 번 들어 있으면, 그 값이 나올 때마다 while loop가 같은 연속 구간을 다시 센다. 그래서 중복을 제거한 num_set을 순회하도록 바꾸었고, 각 시작점이 한 번만 처리되면서 시간초과를 피할 수 있었다.


복잡도 분석

  • 시간 복잡도: O(n)

    • nums를 set으로 만드는 데 O(n)이 들고, num_set을 순회하는 for loop와 시작점에서만 도는 while loop를 합쳐도 최대 O(2n) 수준이다. 상수는 제거하므로 최종 시간복잡도는 O(n)이다.
  • 공간 복잡도: O(n)

    • num_set이 입력 크기에 비례하는 공간을 사용한다.

이 풀이에서 중요한 부분은 set 자체보다 시작점을 고르는 조건이다. num - 1이 없는 값에서만 길이를 세면 정렬 없이도 전체 반복 횟수를 선형으로 묶을 수 있다.


최적화 코드

from typing import List, Set

class Solution:
    def longestConsecutive(self, nums: List[int]) -> int:
        num_set: Set[int] = set(nums)
        max_streak = 0

        for num in num_set:
            if num - 1 in num_set:
                continue

            streak = 0
            cur_num = num

            while cur_num in num_set:
                streak += 1
                cur_num += 1

            max_streak = max(streak, max_streak)

        return max_streak

요약 및 회고

이 문제는 set으로 존재 여부를 빠르게 확인하는 것에서 시작하지만, 모든 숫자를 출발점으로 삼지 않는 조건이 더 중요했다. 현재 숫자보다 하나 작은 값이 있으면 그 숫자는 이미 어떤 수열의 중간에 있으므로 건너뛰고, 시작점에서만 길이를 세면 정렬 없이 시간복잡도 O(n)으로 처리할 수 있다.