유튜브 영상에서 보던 방식과는 조금 달라서 헤맸다. 실제 코드를 보면 two pointer개념을 사용하기는 하지만 pivot을 기점으로 물리적으로 분리된 공간에 실제로 데이터를 swap하는 것이 보였으나, 여기 방식에 따르면 일단 한쪽 끝 (아래 코드는 end)으로 피벗을 배치시킨 뒤에 pivot보다 작은 원소의 개수를 카운트 하고 incremental 하게 위치를 잡는 것을 볼 수 있다.
맨 마지막 줄이 중요한데, 피벗의 위치는 아직 끝이므로 본래 있어야 할 자리로 옮기는 것을 알 수 있다.
#include <random>
#include <utility>
static std::mt19937 engine{std::random_device{}()};
static std::uniform_int_distribution<unsigned int> generator{};
template <typename Iter, class Less>
Iter partition(Iter begin, Iter end, Less const &less) {
auto pivot = begin + generator(engine) % (end - begin);
std::swap(*pivot, *(end - 1));
pivot = end - 1;
size_t count = 0; // pivot보다 작은 원소의 개수를 카운트
for (auto cur = begin; cur != pivot; ++cur) {
if (less(*cur, *pivot)) {
// move element to left
std::swap(*cur, *(begin + count));
count += 1;
}
}
// move pivot to right place
std::swap(*pivot, *(begin + count));
return begin + count;
}
재귀호출 시 중요한 건 역시 종료조건이다. 나의 경우에는 end
가 STL의 이터레이터와 마찬가지로 미포함이기 때문에 범위가 비어있을 때 무조건 begin과 end 사이 1 차이가 난다.
partition을 마친 뒤에 리턴값인 피벗은 이미 확실하게 정렬이 되어있기 때문에 굳이 다음 이터레이션에 포함시킬 필요가 없다.
template <typename Iter, class Less>
void sort(Iter begin, Iter end, Less const &less) {
if (end - begin <= 1) {
return;
}
auto mid = partition(begin, end, less);
sort(begin, mid, less);
sort(mid + 1, end, less);
}
TEST(Test, 1) {
vector<string> ls = {"hello", "my", "name", "is", "choi", "wheatley"};
vector<string> ls2{ls};
std::sort(ls.begin(), ls.end());
sol1::sort(ls2.begin(), ls2.end(), std::less<string>{});
ASSERT_EQ(ls, ls2);
}
확실히 nlogn 속도를 보여주기는 하지만 역시 하이브리드 방식의 표준정렬의 속도에 미치지는 못하는 것 같다.
constexpr size_t MAX_N = 200000;
TEST(Timeout, 1) {
vector<int> ls;
vector<int> ls2;
std::generate_n(std::back_inserter(ls), MAX_N, std::bind(generator, engine));
std::generate_n(std::back_inserter(ls2), MAX_N, std::bind(generator, engine));
std::sort(ls.begin(), ls.end());
sol1::sort(ls2.begin(), ls2.end(), std::less<int>{});
ASSERT_EQ(ls, ls2);
}
아니 ai가 관련문제까지 설명해줬네…