투 포인터 알고리즘은 이름에서 알 수 있듯이, 두 개의 포인터를 사용하여 문제를 해결하는 방법이다.
이 알고리즘의 핵심은 두 개의 포인터를 사용하여 배열이나 데이터 구조를 순회하면서, 특정 조건에 따라 포인터들을 움직이고 연산을 수행하는 것이다. 두 포인터가 서로 다른 방식으로 움직이거나 같은 방식으로 움직일 수 있으며, 이는 문제의 특성에 따라 달라진다.
문제 설명
오름차순으로 정렬이 된 두 배열이 주어지면 두 배열을 오름차순으로 합쳐 출력하는 프로그램 을 작성하세요.
▣ 입력설명
첫 번째 줄에 첫 번째 배열의 크기 N(1<=N<=100)이 주어집니다. 두 번째 줄에 N개의 배열 원소가 오름차순으로 주어집니다. 세 번째 줄에 두 번째 배열의 크기 M(1<=M<=100)이 주어집니다. 네 번째 줄에 M개의 배열 원소가 오름차순으로 주어집니다. 각 리스트의 원소는 int형 변수의 크기를 넘지 않습니다.
▣ 출력설명
오름차순으로 정렬된 배열을 출력합니다.
input | output |
---|---|
3 / [1, 3, 5] / 5 / [2, 3, 6, 7, 9] | [1, 2, 3, 3, 5, 6, 7, 9] |
4 / [1, 2, 2, 9] / 5 / [2, 3, 4, 8, 9] | [1, 2, 2, 2, 3, 4, 8, 9, 9] |
풀이
const combineTwoArray = (a, b) => {
const combinedArray = [...a, ...b];
console.log(a, b); // [1, 3, 5] [2, 3, 6, 7, 9]
return combinedArray.sort();
};
let a = [1, 3, 5];
let b = [2, 3, 6, 7, 9];
console.log(combineTwoArray(a, b));
👉 두 배열 a와 b를 합친 후, sort()
메서드를 사용하여 배열을 정렬하는 방법으로 풀이
❗️ 이 코드는 작동하지만, sort()
메서드의 시간 복잡도가 O(n log n)이므로 두 배열의 길이가 길어질수록 덜 효율적이다. 🤨
Two Pointers Algorithm을 사용한 접근 방식
이 경우, 각 배열에 하나씩 포인터를 두고, 두 배열을 동시에 순회하면서 더 작은 요소를 결과 배열에 추가한다. 이미 배열이 정렬되어 있기 때문에, 전체 배열을 한 번만 순회하면 된다. 이 방법의 시간 복잡도는 O(n+m)이다.
✍️ solution
function mergeArrays(arr1, arr2) {
let result = [];
let i = 0,
j = 0;
while (i < arr1.length && j < arr2.length) {
if (arr1[i] < arr2[j]) {
result.push(arr1[i++]); // 1)
} else {
result.push(arr2[j++]);
}
}
// 나머지 요소 추가
while (i < arr1.length) {
result.push(arr1[i++]);
}
while (j < arr2.length) {
result.push(arr2[j++]);
}
return result;
}
let a = [1, 3, 5];
let b = [2, 3, 6, 7, 9];
console.log(mergeArrays(a, b));
arr1[i++]
구문은 JS에서 post-increment 연산자를 사용하는 방식이다. 이것은 두가지 작업을 수행한다.i
의 값을 증가시킴i
의 값을 1만큼 증가시킴arr1[i++]
가 실행될 때, arr1[i]
의 현재 값을 result 배열에 추가한 후에 i
를 1만큼 증가시키는 것이다. 이는 arr1
배열의 다음 원소로 이동하고자 할 때 유용하다. 😀이미 정렬된 배열을 사용하기 때문에, 전체 배열을 한 번만 순회하면 된다. 이는 큰 배열에서 특히 유리하다.
새로운 배열 외에 추가적인 메모리를 거의 사용하지 않는다.
👉 즉, 첫 번째 방법으로도 문제 해결은 가능하지만, Two Pointer Algorithm을 사용하면 훨씬 효율적이다. 투 포인터 알고리즘은 이미 정렬된 배열을 사용하여 추가적인 정렬 과정 없이 요소들을 합치는 데에 집중한다. 이것은 특히 큰 데이터 세트를 다룰 때 매우 유리한 접근 방식이라고 한다. 🧐
투포인터 알고리즘의 핵심은 두 개의 포인터를 사용하여 두 배열을 동시에 순회하면서, 각 시점에서 더 작은 값ㅇ르 결과 배열에 추가하는 것이다.
✍️ solution2
function solution(arr1, arr2) {
let answer = [];
let n = arr1.length;
let m = arr2.length;
let p1 = (p2 = 0);
while (p1 < n && p2 < m) {
if (arr1[p1] <= arr2[p2]) answer.push(arr1[p1++]);
else answer.push(arr2[p2++]);
}
while (p1 < n) answer.push(arr1[p1++]);
while (p2 < m) answer.push(arr2[p2++]);
return answer;
}
console.log(solution(a, b));
두 배열 arr1과 arr2에 대한 포인터 p1과 p2를 0으로 초기화
while (p1 < n && p2 < m) 루프를 사용하여 두 배열을 동시에 순회한다. 이때, 각 배열의 현재 포인터 위치의 원소를 비교하여 더 작은 값을 결과 배열 answer에 추가한다.
p1++ 또는 p2++을 사용하여 해당 배열의 다음 원소로 이동한다.
두 번째와 세 번째 while 루프는 한 배열의 모든 원소가 결과 배열에 추가된 후, 다른 배열에 남아있는 원소들을 결과 배열에 추가한다.
solution1과 비슷한 풀이이지만 if (arr1[i] < arr2[j])
와 (arr1[p1] <= arr2[p2])
에서의 차이는 뭘까? 🧐
→ 두 배열에 같은 값이 있을 때 어떤 배열의 원소를 먼저 추가할 지에 대한 차이가 있다!
전자의 경우 arr2의 배열의 원소가 먼저, 후자의 경우 arr1의 배열의 원소가 먼저 추가된다. 😀