투 포인터(Two Pointers. 다중 포인터) 알고리즘 패턴
두 개 또는 그 이상의 포인터를 두고 값들을 비교하여 문제를 해결하는 알고리즘 패턴이다.
- 배열이나 문자열같은 선형 구조에서 각자 다른 원소를 가리키는 2개의 포인터를 조작해가면서 원하는 것을 얻어내는 개념이다.
- 정렬되어있는 두 리스트의 합집합에도 사용됨. 병합정렬(merge sort)의 counquer 영역의 기초가 되기도 한다.
프로세스
그냥 기본 방식인 탐색(반복문)을 쓰다 보면 시간이 오래 걸리거나 시간 초과가 걸리는 경우에 투 포인터를 사용하면 메모리와 시간 효율성을 높일 수 있다.
시간복잡도 : O(N). 기본 탐색 반복문을 사용한다면 O(N^2)
투 포인터는 2가지 방식이 있다.
- 앞에서 시작하는 포인터와 끝에서 시작하는 포인터
- 빠른(Fast runner)가 느린 (Slow runner)보다 앞서는 형식
형식 1. 앞에서 시작하는 포인터와 끝에서 시작하는 포인터
정렬된 배열에서 첫번째로 합이 0이 되는 쌍을 찾는 함수
두 포인터가 하나는 처음, 하나는 끝 인덱스에서 중앙으로 이동하는 예제
여기서 중요한 점은 배열의 중복 수가 없어야 하고, 수열이 정렬되어 있어야 한다.
출력 예시)
sumZero([-3, -2, -1, 0, 1, 2, 3]);// [-3, 3]
sumZero([-2, 0, 1, 3]);// undefined
sumZero([1, 2, 3]);// undefined
패턴을 적용하지않고 풀이 해보면
const arr = [-4, -3, -2, -1, 0, 1, 2, 5];
const solution = arr => {
for (let i = 0; i < arr.length; i++) {
for (let j = i + 1; j < arr.length; j++) {
if (arr[i] + arr[j] === 0) {
return [arr[i], arr[j]];
}
}
}
};
console.log(solution(arr)); // [-2, 2]
이중 루프를 이용해서 시간복잡도 O(N^2), 공간복잡도(1) 으로 해결할 수 있다.
이 방법으로도 해결은 가능하지만, 배열의 항목이 많아지면 많아질수록 반복횟수는 아주 많이 이루어지게 된다.
이때 시간 복잡도를 효율적으로 가져갈 수 있는 것이 투 포인터다.
투 포인터 패턴을 적용
const arr = [-4, -3, -2, -1, 0, 1, 2, 5];
//left(-4) right(5) ---- 1
const arr = [-4, -3, -2, -1, 0, 1, 2, 5];
//left(-4) right(2) ---- -2
const arr = [-4, -3, -2, -1, 0, 1, 2, 5];
//left(-3) right(2) ---- -1
const arr = [-4, -3, -2, -1, 0, 1, 2, 5];
//left(-2) right(2) ---- 0 [-2, 2] 출력
배열의 값이 오름차순인 것을 알고 있기 때문에 가장 작은값인 -4와 가장 큰값인 5를 포인터 left, right로 둔다
- -4와 5를 더하면 1 --> 양수 (더해서 0이 되는 쌍을 찾아야하기 때문에 right를 작은쪽으로 한칸 옮긴다.)
- -4와 2를 더하면 -2 --> 음수 (작은값 포인터 left를 큰쪽으로 한칸 옮긴다.)
- -3과 2를 더하면 -1 --> 음수 (작은값 포인터 left를 큰쪽으로 한칸 옮긴다.)
- -2와 2를 더하면 0 --> 쌍을 찾았으므로 출력한다.
코드로 작성해보면
const arr = [-4, -3, -2, -1, 0, 1, 2, 5];
const solution = arr => {
let left = 0; //제일 처음 인덱스
let right = arr.length - 1; // 마지막 인덱스
while (left < right) {//포인터가 같은 곳을 가리키거나 서로 교차되면 값을 못찾았으므로 loop 끝
let sum = arr[left] + arr[right];
if (sum === 0) {//합이 0이면
return [arr[left], arr[right]];
} else if (sum > 0) {// 양수면
right--;
} else {//음수면
left++;
}
}
//값을 못찾았다면 여기서 default로 undefined가 리턴된다
};
console.log(solution(arr)); // [-2, 2]
시간복잡도 O(N), 공간복잡도(1) 으로 해결할 수 있다.
형식 2. 빠른 포인터(Fast runner)가 느린 포인터(Slow runner)보다 앞서는 형식
정렬된 배열에서 요소의 가짓수를 구하는 함수
두 포인터가 첫 인덱스에서 끝으로 이동하는 예제
풀이
- 두 포인터 slow와 fast는 각각 인덱스0과 1에서 시작한다.
- fast가 인덱스 1부터 증가하게 되면서 조건(두 포인터의 숫자가 다르면)이 맞으면 slow를 앞으로 이동한 후, 그 숫자를 fast 포인터가 가리키는 숫자로 바꿔준다.
출력 예시)
console.log(solution([1, 1, 1, 1, 1, 2])); // 2
console.log(solution([1, 2, 3, 4, 4, 4, 7, 7, 12, 12, 13])); // 7
console.log(solution([])); // 0
console.log(solution([1])); // 1
console.log(solution([-2, -1, -1, 0, 1])); // 4
const solution = arr => {
let slow = 0;
let fast = 1;
if (arr.length === 0) return 0;
while (fast < arr.length) {
if (arr[slow] !== arr[fast]) {
slow++;
arr[slow] = arr[fast];
}
fast++;
}
return slow + 1;
};
solution([1, 1, 1, 1, 1, 2])의 동작순서를 보면
//p1은 i , p2는 j로 설명
const arr = [1, 1, 1, 1, 1, 2];
i
[1, 1, 1, 1, 1, 2]
j // arr[i]와 arr[j]는 같으므로 j++
i
[1, 1, 1, 1, 1, 2]
j // arr[i]와 arr[j]는 같으므로 j++
i
[1, 1, 1, 1, 1, 2]
j // arr[i]와 arr[j]는 같으므로 j++
i
[1, 1, 1, 1, 1, 2]
j // arr[i]와 arr[j]는 같으므로 j++
i
[1, 1, 1, 1, 1, 2]
j // arr[i]와 arr[j]는 같으므로 j++
i
[1, 1, 1, 1, 1, 2]
j // arr[i]와 arr[j]는 다르므로 i++ 후 arr[i]에 arr[j]를 대입 j++
i
[1, 2, 1, 1, 1, 2]
j //j < arr.length를 만족하지 않으므로 while문 탈출
//i의 i + 1(가짓수)을 리턴한다.
이 문제는 자바스크립트 Set과 spread 연산자를 활용하면 간단하게 해결할수도 있다.
const solution = arr => {
const uniqueArr = [...new Set(arr)];
return uniqueArr.length;
};
'[Basic] Data > Algorithm' 카테고리의 다른 글
[Algorithm] (알고리즘 패턴) Frequency Counters(빈도수 세기) (0) | 2023.03.25 |
---|---|
[Algorithm] 유클리드 호제법 (최대공약수 & 최소공배수 구하기) (0) | 2023.03.24 |
[Algorithm] (알고리즘 패턴) 슬라이딩 윈도우(Sliding Window) (0) | 2023.03.24 |
[Algorithm] Segment Tree (구간 트리) (0) | 2023.03.23 |
[Algorithm] KMP 알고리즘 (Knuth-Morris-Pratt Algorithm) (0) | 2023.03.23 |
댓글