[Leetcode] 1. Two Sum

bradley·2022년 5월 31일
1

Algorithm

목록 보기
2/12

Problem

Solution

1) 브루트 포스로 계산

브루트 포스(Brute-Force) : 배열을 2번 반복하면서 모든 조합을 더해서 일일히 확인해보는 무차별 대입 방식

class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        for i in range(len(nums)): # 리스트 iteration
            for j in range(i + 1, len(nums)): # 리스트의 두 번째 요소부터 iteration
                if nums[i] + nums[j] == target: # 리스트의 각 요소 중 target과 같은 값이면
                    return [i, j] # 해당하는 index return

지나치게 느린 비효율적인 풀이로 이 경우 풀이 시간 복잡도는 O(n2)O(n^2).

2) in을 이용한 탐색

모든 조합을 비교하지 않고 정답, 즉 타겟에서 첫 번째 값을 뺀 값 target - n이 존재하는지 탐색하는 방식으로 접근

class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        for i, n in enumerate(nums):
            complement = target - n
            
            if complement in nums[i + 1:]: # 현재 반복 이후 값들로 이루어진 리스트에서 비교
                return [nums.index(n), nums[i + 1:].index(complement) + (i + 1)] # 현재 반복 이후 리스트에서 비교했으니 (i + 1)을 더해 입력 리스트의 인덱스를 추출한다. 

in의 시간 복잡도는 O(n)O(n), 따라서 전체 시간 복잡도는 이전과 동일한 O(n2)O(n^2).
하지만 같은 시간 복잡도라도 in 연산 쪽이 훨씬 더 가볍고 빠르다.
상수항이 생략되어 표기된 시간 복잡도에서 생략된 상수항은 1) 풀이에 비해 훨씬 더 작은 값이라고 할 수 있다.

3) 첫 번째 수를 뺀 결과 key 조회

비교나 탐색 대신 한 번에 정답을 찾는 방법

target에서 첫 번째 수를 빼면 두 번째 수를 바로 알아낼 수 있다. 그렇다면 두 번째 수를 key로 하고 기존의 index를 value로 바꿔서 dictionary에 저장해두면, 나중에 두 번째 수를 key로 조회해서 정답을 즉시 찾아낼 수 있다.

class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        nums_map = {}
        
        # key와 value를 바꿔서 dictionary로 저장
        for i, num in enumerate(nums):
            nums_map[num] = i
        
        # target에서 첫 번째 수를 뺀 결과를 key로 조회
        for i, num in enumerate(nums):
            if target - num in nums_map and i != nums_map[target - num]:
                return [i, nums_map[target - num]]

dictionary는 해시 테이블로 구현되어 있고, 이 경우 조회는 평균 O(1)O(1)에 가능하다. 최악의 경우에는 O(n)O(n)이 될 수도 있다.

4) 조회 구조 개선

3) 풀이에서 dictionary 저장과 조회를 2개의 for문으로 처리하였는데 이것을 하나로 통합

class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        nums_map = {}
        
        # 하나의 for 문으로 통합
        for i, num in enumerate(nums):
            if target - num in nums_map:
                return [nums_map[target - num], i]
            nums_map[num] = i

이 경우 전체를 모두 저장할 필요없이 정답을 찾게되면 함수를 바로 빠져나온다.
그러나 두 번째 값을 찾기 위해 어차피 매번 비교해야 하기 때문에 3) 풀이에 비해 성능상의 큰 이점은 없을 것 같다.

5) 투 포인터 이용 (잘못된 접근방식!)

투 포인터로 해당 문제를 접근하면 어떻게 될까?
투 포인터란 왼쪽 포인터와 오른쪽 포인터의 합이 타겟보다 크다면 오른쪽 포인터를 왼쪽으로, 작다면 왼쪽 포인터를 오른쪽으로 옮기면서 값을 조정하는 방식이다.

class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        nums.sort() # 입력 리스트 정렬
        left, right = 0, len(nums) - 1
        
        while not left == right:
        	# 합이 target보다 작으면 왼쪽 포인터를 오른쪽으로
            if nums[left] + nums[right] < target:
                left += 1
            # 합이 target보다 크면 오른쪽 포인터를 왼쪽으로
            elif nums[left] + nums[right] > target:
                right -= 1
            else:
                return [left, right]

이처럼 투 포인터로 구현하면 코드도 간결하고 이해하기 쉽다. 투 포인터의 시간 복잡도도 O(n)O(n)이고, 불필요한 추가 계산도 필요없어 매우 빠른 속도가 기대된다.

다만 이 문제는 투 포인터로 풀 수 없다!!
왜냐면 입력값 nums가 정렬된 상태가 아니기 때문이다. nums.sort()로 정렬한다고 하더라두 정렬한 index와 입력 리스트의 index가 다르기 때문에 index가 엉망이 된다.
만약 이 문제가 index가 아니라 값을 찾아내는 문제라면, 얼마든지 정렬하고 투 포인터로 풀이할 수 있다. 하지만 이처럼 index를 찾아내는 문제에서는 이렇게 정렬을 통해 index를 섞어 버리면 곤란하다. 원래 index를 찾을 수가 없기 때문이다.

profile
데이터 엔지니어링에 관심이 많은 홀로 삽질하는 느림보

0개의 댓글

관련 채용 정보