머신러닝 및 딥러닝/알고리즘

[알고리즘] 투 포인터 (Two pointers) 알고리즘

밴B 2021. 4. 16. 10:42
728x90
반응형



투 포인터란?

이번 글에서는 Two pointers technique (algorithm)을 설명해보도록 하겠습니다.
일단 이름 그대로 두 가지 포인터를 사용하여 문자열이나 배열(또는 리스트)에서 원하는 값을 찾거나 반복문을 써야 할 때 쓰기 좋은 방식입니다.

그냥 naive 방식인 그냥 탐색 (반복문)을 쓰다보면 시간 초과가 걸리는 경우가에 투 포인터를 사용하면 메모리와 시간 효율성을 높일 수 있습니다. 코딩 테스트를 보면 시간 복잡도를 낮출 수 있는 경우에는 일부로 테스트 케이스에 n이 정말 큰 (엄청 긴 배열이나 문자열)을 사용해서 Time out을 걸리게 하는 케이스가 많습니다.

포인터는 크게 두가지 방식으로 쓰입니다.

  1. 앞에서 시작하는 포인터와 끝에서 시작하는 포인터가 만나는 형식
  2. 또는 빠른 포인터 (fast runner)가 느린 포인터 (slow runner) 보다 앞서는 형식

밑에서 각 두 형식에서 유명한 연습 문제를 풀며 설명해보겠습니다.

 


형식 1: 중간에서 만나는 포인터


리트코드의 Two sum (sorted array) 문제를 써서 설명해보겠습니다. (문제 링크 난이도: ★)

작은 수부터 큰 수로 정렬된 배열 numbers와 숫자 target가 주어집니다.
이 배열 내에서 숫자 두 수의 합이 target인 그 두 수의 인덱스(1으로 시작)를 출력하는 문제입니다.

테스트 케이스 예)
Input: numbers = [2,7,11,15], target = 9
Output: [1,2]

기본 탐색 알고: 이중 for문 사용

반복문 (loop) 두 개로 한 숫자와 모든 숫자를 차례대로 더해보는 방식입니다.

시간 복잡도 O(n^2)

첫 숫자(i=0)인 3을 다음 숫자 (j=1) 24랑 더해보고 합이 target이 아니면
다음 숫자 (j=2) 50이랑 더해보고 또 아니네
다음 숫자 (j=3)랑 더해보고... 이렇게 배열에 있는 모든 숫자랑 더해보고 target이 나오면 인덱스를 출력하는 기본적인 알고입니다.

테스트 케이스에서 배열이 짧거나 배열이 길어도 찾으려는 두 수가 배열의 앞쪽에 있다면 타임 아웃이 안 걸리겠지만..

이런 코드를 아웃시키려는 테스트 코드를 보면: (target=5)

월리를 찾아라

이렇게 13010개의 0과 13008개의 9 사이에 있는 2와 3이 숨어있는 걸 볼 수 있습니다.
이 알고리즘을 사용한다면 outer loop를 13010번 돌고 13011번째 루프에서 target을 찾을 수 있었겠지만 각 루프에서 26020번의 더하기를 실행했기에 오래 걸립니다.

투 포인터 방식

그럼 어떻게 하면 더 효율적이게 배열에서 두 수를 찾을까요?
위에서 말했던 앞에서 시작, 뒤에서 시작하는 두 포인터를 사용하면 시간 복잡도를 낮출 수 있습니다.

시간 복잡도 O(n)

앞에서 시작하는 포인터 i와 뒤에서 시작하는 포인터 j
두 포인터가 만날 때까지 또는 원하는 값 target을 찾을 때까지 밑 과정을 반복합니다.

  • 두 수의 합이 target 보다 작으면 i 포인터를 앞으로 하나 이동 (i += 1)
  • 두 수의 합이 target 보다 크면 j 포인터를 앞으로 하나 이동 (j -= 1)

이렇게 배열이 정돈되어 있다는 점을 이용하여 기본 탐색 방식보다 훨씬 빠르고 효율적이게 원하는 값을 찾을 수 있습니다.

위 코드는 긱스포긱스에서 참고했으며 다른 투 포인터 문제도 있으니 궁금하다면 확인해보세요! (링크)


형식 2: 토끼와 거북이

투 포인터의 다른 형식은 앞과 끝에서 시작하는 포인터가 아니라 두 포인터가 앞에서 시작하는데 속도가 다르게 움직이는 형식입니다 (이름하여 fast runner와 slow runner).
주로 fast runner는 각 반복문마다 커지고 slow runner는 커지는 데에 조건이 있는 편으로 쓰입니다.

이 방식을 쓰기 좋은 리트코드의 Remove Duplicates 문제를 한번 보겠습니다. (문제 링크 난이도: ★)

숫자가 들어있는 정렬된 배열에서 중복되는 숫자를 제거하는 문제입니다. 중복이 없는 새로운 배열의 길이를 출력하면 됩니다. 새로운 리스트를 만드는 게 아니라 리스트를 바꾸는 형식으로 해야 합니다.

테스트 케이스 예)
Input: nums = [0,0,1,1,1,2,2,3,3,4]
Output: 5

풀이 코드

def removeDuplicates(self, nums: List[int]) -> int: 
	slow = 0 
    for fast in range(1, len(nums)): 
    	if nums[slow] != nums[fast]: 
        	slow += 1 
            nums[slow] = nums[fast] 
    return slow + 1

코드 출처는 여기

 


이 코드에서 두 포인터 slow와 fast는 각각 인덱스 0과 1에서 시작합니다.


Outer loop에서 fast가 인덱스 1부터 시작에 쭉 움직이게 되어있습니다. 이때 slow 포인터는 조건이 맞아야지만 앞으로 이동 (slow += 1) 합니다.

두 포인터가 가리키는 숫자가 다르다면 slow는 앞으로 이동을 한 후, 그 숫자를 fast 포인터가 가르키는 숫자로 바꿔줍니다.

fast 포인터가 먼저 '달리면서' 다른 숫자를 찾으면 slow포인터에게 알려줘서 slow가 바꿔주는 식으로 생각하면 될 거 같습니다.

이 loop를 반복하다 보면 배열이 nums = [0,0,1,1,1,2,2,3,3,4]에서 nums = [0, 1, 2, 3, 4, 2, 2, 3, 3, 4]로 바뀌고 slow+1이 중복된 숫자가 없는 길이로 출력하면 됩니다.





투 포인터 연습 문제

  • 백준 투포인터 연습 문제 대략 130개 정도 (링크)
  • 리트코드 투포인터 연습 문제 74개 (링크)

 


728x90
반응형