문제
Input : point 배열 points, 정수 k
Output : points에서 원점(0, 0)과 가까운 순으로 k개의 좌표 반환
※ $point = [x, y]$
시간 복잡도
※1초에 1억번을 대략적인 기준으로 삼는다.
$1 \le n \le 10'000$
- $n^2 = 100'000'000$ ❓
1초에 1억번을 대략적인 기준으로 삼고 있는데, 딱 1억이어서 통과 여부에 대해 위험하므로 일단은 후순위로 고려해둔다. - $n \log{n} = 10'000 * 14 = 140'000$ ✅
충분히 안전하기 때문에, $n \log{n}$이하의 접근을 목표로 하면 된다.
해결 계획
"~순으로". "순서 관계없이" 라는 단어를 보자마자 우선순위 큐가 생각났다.
배열을 한 번 순회하면서 큐의 거리를 계산해 저장하고, k만큼 꺼내는 방식이 진행할 수 있을 것 같았다.
이 경우 복잡도는 $O(n \log n)$ 으로 충분히 통과 가능하다.
아래는 풀이하기전 작성해 본 수도코드다.
vector<vector<int>> kClosest(vector<vector<int>>& points, int k) {
// 우선순위 큐 <pair<크기, 좌표>>
for (auto data : points)
{
// 벡터 크기 계산
/* 크기의 범위는? 최대 10'000'000 => int안에서 로 충분 */
// 크기+좌표 큐에 삽입 (오름차순으로)
}
// 결과 vector <vector<int>>
while (k--)
{
// 큐 꺼내서 결과 벡터에 삽입
/* k <= points.size 이므로 큐가 빈상태에서 꺼낼 일은 없음 */
}
return result;
}
구현
using piv = pair<int,vector<int>>;
vector<vector<int>> kClosest(vector<vector<int>>& points, int k) {
priority_queue<piv, vector<piv>, greater<piv>> q;
for (auto& data : points)
{
int size = (data[0] * data[0]) + (data[1] * data[1]);
q.push({size, data});
}
vector<vector<int>> result;
result.reserve(k);
while (k--)
{
result.push_back({q.top().second[0], q.top().second[1]});
q.pop();
}
return result;
}
되돌아보기
sort vs priority queue
다른 사람들의 코드를 보다가 정렬을 이용해 풀이한 사람도 있었는데,
우선순위 큐보다 더 빠른 속도로 실행됐는데, 같은 n log n 방식에서 왜 더 빠르게 실행되었는지 궁금하여 이에 대해 좀 알아보았다.
우선순위가 더 느렸던 주요인은,
연속된 메모리를 한 번에 정리해버리는 sort와 달리 , 우선순위 큐는 트리 구조로 원소 수만큼 정렬(swap)을 반복하기 때문에
이번 상황에서는 좀 더 불리했다.
또한 연속 메모리가 아니기 때문에 캐시 친화성이 떨어지는 것도 한 몫한다고 볼 수 있겠다.
"그럼 언제 sort를 사용하고, 언제 우선순위 큐를 사용해야할까?"에 대해 내린 결론은 다음과 같다.
- Sort
정렬 한 번으로 끝낼 수 있으면 sort가 유리. - Priority queue
데이터가 계속 변하며, 중간중간 top을 꺼내야할때는 우선순위 큐가 유리.
우선순위 큐는 정렬도구가 아니라 동적 우선순위 자료구조임을 명심하자~
개선 코드
vector 복사 회피
내가 작성한 코드에서 vector를 복사하는 대신에, index를 넣어 원본을 사용하는 방식이다.
// first : vec2 Size | second : points index
using pii = pair<int,int>;
class Solution {
public:
vector<vector<int>> kClosest(vector<vector<int>>& points, int k) {
priority_queue<pii, vector<pii>, greater<pii>> q;
for (int i = 0 ; i < points.size(); ++i)
{
int size = (points[i][0] * points[i][0]) + (points[i][1] * points[i][1]);
q.push({size, i});
}
vector<vector<int>> result;
result.reserve(k);
while (k--)
{
result.push_back(points[q.top().second]);
q.pop();
}
return result;
}
};
n log n ⇒ n log k
나는 좀 쓸모 없이 모든 원소를 계속 넣고 있었는데, 큐에서 k개만 필요하다면 k개만 유지해도 될 일이였다.
하지만 내 코드에서는 Min-heap을 사용하여 무작정 루트를 삭제 할 수 없었는데,
우선순위 큐의 내부에서는 정렬이 되지 않기 때문에 정답이 되는 노드가 잘릴 수도 있기 때문이다.
그래서 루트노드를 최순위로 꺼내야하는 노드가 아닌, 최후순위로 꺼내야하는 노드로 변경해주면
크기가 k개를 넘었을 때 루트를 삭제해줄 수 있다.
이는 기존 Min-heap에서 Max-heap으로 변경하면 해결 가능하다.
priority_queue<int> maxHeap;
if (k < maxHeap.size()) maxHeap.pop();
nth_element 사용_1
다른 사람의 코드를 보면서 처음 알게 된 함수인데, std::nth_element 라고 Quick sort의 편의성 버전이다.
nth_element는 특정 index를 지정하고, 해당 index의 값이 정렬이 끝나면 종료한다.
그래서 결과값으로 지정한 index는 정확하게 정렬이 되었지만, 앞과 뒤 부분은 정렬되지 않는다.
하지만 앞 부분은 지정한 index의 값보다 작은 값들, 뒷 부분은 지정한 index의 값보다 큰 값들로 구성되는 특성을 이용해 볼 수 있다.
이 특성은 이 문제에 굉장히 효율적며, 실제로 평균 O(n)을 가지기 때문에 빠른 편에 속한다.
vector<vector<int>> kClosest(vector<vector<int>>& points, int k) {
for (auto& data : points)
{
int size = (data[0] * data[0]) + (data[1] * data[1]);
data.push_back(size);
}
nth_element(points.begin(), points.begin() + k - 1, points.end(),
[](const vector<int>& v1, const vector<int>& v2){return v1[2] < v2[2];}
);
vector<vector<int>> result;
result.reserve(k);
for (int i = 0; i < k; ++i)
{
points[i].pop_back();
result.push_back(points[i]);
}
return result;
}
};
주의할 건, zero based문제로 nth_element를 사용할 때 points.begin() + k - 1을 사용해야한다.
여기서 push_back이나 pop_back 같은 메모리에 대한 연산을 조금 더 최적화 하면 아래와 같다.
vector<vector<int>> kClosest(vector<vector<int>>& points, int k)
{
nth_element(points.begin(), points.begin() + k, points.end(),
[](const vector<int>& v1, const vector<int>& v2)
{
return v1[0]*v1[0] + v1[1]*v1[1] < v2[0]*v2[0] + v2[1]*v2[1];
}
);
return vector<vector<int>>(points.begin(), points.begin()+k);
}
이 코드는 size를 정렬 과정에서 계속 사용하여 좀 회피하려고 헀었는데,
이런 몇 번의 정수 곱같은 산술 연산은 비용이 저렴한 축에 속하고,
push_back이나 pop_back같은 메모리 변경 연산은 보다 비싼축에 속한다.
그래서 지금은 아래의 코드가 더 효율적인 코드라고 볼 수 있고,
만약 sqrt처럼 비교 연산이 매우 비싼편이라면, 위의 코드처럼 미리 값을 저장해 이용하는 방식이 더 유리하다.
근데 nth_element는 C++98부터 포함되어 있다고 하는데, 왜 이제 알았을까..?!
참고로 람다는 C++11부터 같이 사용할 수 있다!
'코딩 테스트 > 알고리즘 풀이' 카테고리의 다른 글
| [Leet code - 64] Minimum Path Sum ✅ (0) | 2026.03.06 |
|---|---|
| [Leet code - 121] Best Time to Buy and Sell Stock ✅ (0) | 2026.03.05 |
| [Leet code - 62] Unique Paths ❌ (0) | 2026.03.04 |
| [Leet code - 198] House Robber ✅ (0) | 2026.03.03 |
| [Leet code - 739] Daily Temperatures ✅ (0) | 2026.02.26 |
| [Leet code - 20] Valid Parentheses ✅ (0) | 2026.02.26 |
| [Leet code - 75] Sort Colors ✅ (0) | 2026.02.13 |
| [Leet code - 347] Top K Frequent Elements ✅ (0) | 2026.02.12 |
