본문 바로가기

JavaScript/Algorithm

[Algorithm] 알고리즘 기본 패턴 2 - Multiple Pointers (다중 포인터)

 

1. 개념

 

배열이나 문자열에서 한 쌍의 값이나 조건을 찾을 때 사용

인덱스 또는 위치에 해당하는 포인터 또는 값을 생성하고 특정 조건에 따라 시작, 끝 또는 중간으로 이동합니다.

공간의 복잡성을 최소화하면서 문제를 해결하는 데 매우 효율적입니다.


정렬된 배열에서 사용합니다

 

2. 특징


시간 복잡도: 대부분 O(n)
공간 복잡도: O(1) (추가 공간이 거의 필요 없음)
주로 정렬된 배열에서 사용됨
두 개의 값을 비교하거나 특정 조건을 찾을 때 효과적


3. 사용하는 경우


중복 값 찾기
특정 합을 가진 쌍 찾기
팰린드롬 확인
배열에서 고유한 값 개수 세기

 

 

4. 예제

 

1) 정렬된 배열에서 합이 0인 쌍 찾기

/**
 * 배열에서 합이 0이 되는 두 숫자를 찾는 함수
 * @param {number[]} arr - 정렬된 숫자 배열
 * @returns {number[]|undefined} - 합이 0이 되는 두 숫자의 배열 또는 undefined
 */
function findZeroSum(arr) {
    // 왼쪽 포인터 초기화
    let left = 0;
    // 오른쪽 포인터 초기화
    let right = arr.length - 1;
    
    // 두 포인터가 교차할 때까지 반복
    while(left < right) {
        // 현재 왼쪽과 오른쪽 포인터가 가리키는 값의 합 계산
        let sum = arr[left] + arr[right];
        
        if(sum === 0) {
            // 합이 0이면 해당 두 숫자 반환
            return [arr[left], arr[right]];
        } else if(sum > 0) {
            // 합이 0보다 크면 오른쪽 포인터를 왼쪽으로 이동
            right--;
        } else {
            // 합이 0보다 작으면 왼쪽 포인터를 오른쪽으로 이동
            left++;
        }
    }
    // 합이 0이 되는 쌍을 찾지 못한 경우 undefined 반환
    return undefined;
}

 

 

2) 중복 값 제거하기

/**
 * 정렬된 배열에서 중복 요소를 제거하고 고유한 요소의 개수를 반환하는 함수
 * @param {number[]} arr - 정렬된 숫자 배열
 * @returns {number} - 중복이 제거된 후의 배열 길이
 */
function removeDuplicates(arr) {
    // 빈 배열인 경우 0 반환
    if(arr.length === 0) return 0;
    
    // i는 고유한 요소를 저장할 위치를 가리키는 포인터
    let i = 0;
    
    // j는 배열을 순회하면서 새로운 요소를 찾는 포인터
    for(let j = 1; j < arr.length; j++) {
        // 현재 요소가 이전 고유 요소와 다른 경우
        if(arr[i] !== arr[j]) {
            // 다음 위치로 이동
            i++;
            // 새로운 고유 요소를 저장
            arr[i] = arr[j];
        }
        // 같은 요소인 경우 j만 증가하고 넘어감
    }
    
    // 고유한 요소의 개수 반환 (i는 0부터 시작했으므로 +1)
    return i + 1;
}

// 사용 예시
let arr = [1, 1, 2, 2, 3, 4, 4, 5];
console.log(removeDuplicates(arr)); // 5 출력 (고유한 요소: 1, 2, 3, 4, 5)

 

 

3) 세 수의 합이 0인 경우 찾기

/**
 * 배열에서 합이 0이 되는 세 숫자의 조합을 모두 찾는 함수
 * @param {number[]} arr - 숫자 배열
 * @returns {number[][]} - 합이 0이 되는 세 숫자의 조합들을 담은 2차원 배열
 */
function findTripleSum(arr) {
    // 배열을 오름차순으로 정렬
    arr.sort((a, b) => a - b);
    // 결과를 저장할 배열
    let result = [];
    
    // 첫 번째 숫자를 고정하고 나머지 두 숫자를 찾기
    for(let i = 0; i < arr.length - 2; i++) {
        // 중복된 값은 건너뛰어 중복 조합 방지
        if(i > 0 && arr[i] === arr[i-1]) continue;
        
        // 두 번째, 세 번째 숫자를 찾기 위한 투 포인터 설정
        let left = i + 1;
        let right = arr.length - 1;
        
        while(left < right) {
            // 세 숫자의 합 계산
            let sum = arr[i] + arr[left] + arr[right];
            
            if(sum === 0) {
                // 합이 0이면 결과 배열에 추가
                result.push([arr[i], arr[left], arr[right]]);
                
                // 중복된 값 건너뛰기 (left)
                while(left < right && arr[left] === arr[left+1]) left++;
                // 중복된 값 건너뛰기 (right)
                while(left < right && arr[right] === arr[right-1]) right--;
                
                // 다음 조합을 찾기 위해 포인터 이동
                left++;
                right--;
            } else if(sum < 0) {
                // 합이 0보다 작으면 left를 증가
                left++;
            } else {
                // 합이 0보다 크면 right를 감소
                right--;
            }
        }
    }
    return result;
}

// 사용 예시
console.log(findTripleSum([-1, 0, 1, 2, -1, -4]));
// [[-1, -1, 2], [-1, 0, 1]] 출력

 

 

5. 장점


메모리 사용이 효율적
선형 시간 복잡도로 문제 해결 가능
중첩 루프를 피할 수 있음


6. 주의사항


대부분의 경우 정렬된 데이터에서 더 효과적
포인터의 이동 조건을 정확히 설정해야 함
경계 조건(edge cases) 처리에 주의

 

 

기존 알아본 패턴

1. Frequency Counter (빈도수 세기) https://daunje0.tistory.com/190

 

다음 알아볼 패턴들

 

Sliding Window https://daunje0.tistory.com/262
Divide and Conquer https://daunje0.tistory.com/263
Dynamic Programming
Greedy Algorithms
Backtracking