对于“排序”这个计算机科学里的经典问题,你是绝对没有必要自己写 for 循环的,必须坚决地选择标准算法。
在求职面试的时候,你也许手写过不少排序算法吧,像选择排序、插入排序、冒泡排序,等等,但标准库里的算法绝对要比你所能写出的任何实现都要好。说到排序,你脑海里跳出的第一个词可能就是 sort(),它是经典的快排算法,通常用它准没错。
使用方法可以参考我的另一篇文章stl算法之sort 01
不过,排序也有多种不同的应用场景,sort() 虽然快,但它是不稳定的,而且是全排所有元素。很多时候,这样做的成本比较高,比如 TopN、中位数、最大最小值等,我们只关心一部分数据,如果你用 sort(),就相当于“杀鸡用牛刀”,是一种浪费。
C++ 为此准备了多种不同的算法,不过它们的名字不全叫 sort,所以你要认真理解它们的含义。
我来介绍一些常见问题对应的算法:
1.要求排序后仍然保持元素的相对顺序,应该用 stable_sort,它是稳定的;
2.选出前几名(TopN),应该用 partial_sort;
3.选出前几名,但不要求再排出名次(BestN),应该用 nth_element;
4.中位数(Median)、百分位数(Percentile),还是用 nth_element;
5.按照某种规则把元素划分成两组,用 partition;
6.第一名和最后一名,用 minmax_element。
假设这样一种情境,有一个存有 100 万个元素的容器,但我们只想从中提取出值最小的 10 个元素,该如何实现呢?
通过我之前的文章,可能会想到使用 sort() 或者 stable_sort() 排序函数,即通过对容器中存储的 100 万个元素进行排序,就可以成功筛选出最小的 10 个元素。但仅仅为了提取 10 个元素,却要先对 100 万个元素进行排序,可想而知这种实现方式的效率是非常低的。
值得一提的是,stable_sort() 函数完全可以看作是 sort() 函数在功能方面的升级版。换句话说,stable_sort() 和 sort() 具有相同的使用场景,就连语法格式也是相同的(后续会讲),只不过前者在功能上除了可以实现排序,还可以保证不改变相等元素的相对位置。
要知道,一个函数的功能往往可以从它的函数名中体现出来,以 partial_sort() 函数为例,partial sort 可直译为“部分排序”。partial_sort() 函数的功能确是如此,即该函数可以从指定区域中提取出部分数据,并对它们进行排序。
但“部分排序”仅仅是对 partial_sort() 函数功能的一个概括,如果想彻底搞清楚它的功能,需要结合该函数的语法格式。partial_sort() 函数有 2 种用法,其语法格式分别为:
//按照默认的升序排序规则,对 [first, last) 范围的数据进行筛选并排序
void partial_sort (RandomAccessIterator first,
RandomAccessIterator middle,
RandomAccessIterator last);
//按照 comp 排序规则,对 [first, last) 范围的数据进行筛选并排序
void partial_sort (RandomAccessIterator first,
RandomAccessIterator middle,
RandomAccessIterator last,
Compare comp);
其中,first、middle 和 last 都是随机访问迭代器,comp 参数用于自定义排序规则。
partial_sort() 函数会以交换元素存储位置的方式实现部分排序的。具体来说,partial_sort() 会将 [first, last) 范围内最小(或最大)的 middle-first 个元素移动到 [first, middle) 区域中,并对这部分元素做升序(或降序)排序。
需要注意的是,partial_sort() 函数受到底层实现方式的限制,它仅适用于普通数组和部分类型的容器。换句话说,只有普通数组和具备以下条件的容器,才能使用 partial_sort() 函数:
1.容器支持的迭代器类型必须为随机访问迭代器。这意味着,partial_sort() 函数只适用于 array、vector、deque 这 3 个容器。
2.当选用默认的升序排序规则时,容器中存储的元素类型必须支持 <小于运算符;同样,如果选用标准库提供的其它排序规则,元素类型也必须支持该规则底层实现所用的比较运算符;
3.partial_sort() 函数在实现过程中,需要交换某些元素的存储位置。因此,如果容器中存储的是自定义的类对象,则该类的内部必须提供移动构造函数和移动赋值运算符。
接下来看看对10000个元素所花费的时间
void main()
{
//sort函数是不稳定的,因为它有可能会改变原来元素的顺序,但stable_sort是稳定的,不会更改原有元素的顺序
std::vector<int> m_vc{ 1, 2, 3, 2, 4, 6, 5 };
m_vc.resize(10000);
clock_t t1 = clock();
std::stable_sort(m_vc.begin(), m_vc.end(), [](int x, int y){return x < y; });
clock_t t2 = clock();
cout << "\nstable_sort need time:" << t2 - t1 << endl;
clock_t t3 = clock();
//从小到大排序,将前四个放在最前面的四个位置
std::partial_sort(m_vc.begin(), m_vc.begin() + 4, m_vc.end());
clock_t t4 = clock();
cout << "partial_sort need time:" << t4 - t3 << endl << endl;
system("pause");
}
结果:
从运行结果可以看出部分排序确实比全部排完花费的时间更短。接下来我将元素放少一些,看看具体的使用。
void main()
{
//sort函数是不稳定的,因为它有可能会改变原来元素的顺序,但stable_sort是稳定的,不会更改原有元素的顺序
std::vector<int> m_vc{ 1, 2, 3, 2, 100,4, 6, 5 ,700,-1};
cout << "原数组:";
for (auto node :m_vc)
{
cout << node << " ";
}
cout << endl;
std::partial_sort(m_vc.begin(), m_vc.begin() + 4, m_vc.end(), [](int x, int y){return x < y; });
cout << "partial_sort(前面最小的四个元素) 后:";
for (auto node :m_vc)
{
cout << node << " ";
}
cout << endl;
std::vector<int> m_vcx{ 1, 2, 3, 2, 100, 4, 6, 5, 700, -1 };
cout << "原数组:";
for (auto node : m_vcx)
{
cout << node << " ";
}
cout << endl;
std::partial_sort(m_vcx.begin(), m_vcx.begin() + 4, m_vcx.end(), [](int x, int y){return x > y; });
cout << "partial_sort(前面最大的四个元素) 后:";
for (auto node : m_vcx)
{
cout << node << " ";
}
cout << endl;
system("pause");
}
结果:
那么你如果需要前面四个,你就可以遍历到第四个停止就OK了!
值得一提的是,partial_sort() 函数实现排序的平均时间复杂度为N*log(M),其中 N 指的是 [first, last) 范围的长度,M 指的是 [first, middle) 范围的长度。
partial_sort_copy() 函数的功能和 partial_sort() 类似,唯一的区别在于,前者不会对原有数据做任何变动,而是先将选定的部分元素拷贝到另外指定的数组或容器中,然后再对这部分元素进行排序。
partial_sort_copy() 函数也有 2 种语法格式,分别为:
//默认以升序规则进行部分排序
RandomAccessIterator partial_sort_copy (
InputIterator first,
InputIterator last,
RandomAccessIterator result_first,
RandomAccessIterator result_last);
//以 comp 规则进行部分排序
RandomAccessIterator partial_sort_copy (
InputIterator first,
InputIterator last,
RandomAccessIterator result_first,
RandomAccessIterator result_last,
Compare comp);
其中,first 和 last 为输入迭代器;result_first 和 result_last 为随机访问迭代器;comp 用于自定义排序规则。
partial_sort_copy() 函数会将 [first, last) 范围内最小(或最大)的 result_last-result_first 个元素复制到 [result_first, result_last) 区域中,并对该区域的元素做升序(或降序)排序。
值得一提的是,[first, last] 中的这 2 个迭代器类型仅限定为输入迭代器,这意味着相比 partial_sort() 函数,partial_sort_copy() 函数放宽了对存储原有数据的容器类型的限制。换句话说,partial_sort_copy() 函数还支持对 list 容器或者 forward_list 容器中存储的元素进行“部分排序”,而 partial_sort() 函数不行。
但是,介于 result_first 和 result_last 仍为随机访问迭代器,因此 [result_first, result_last) 指定的区域仍仅限于普通数组和部分类型的容器,这和 partial_sort() 函数对容器的要求是一样的。
看看怎么使用:
void main()
{
//sort函数是不稳定的,因为它有可能会改变原来元素的顺序,但stable_sort是稳定的,不会更改原有元素的顺序
std::vector<int> m_vc{ 1, 2, 3, 2, 100,4, 6, 5 ,700,-1};
cout << "原数组:";
for (auto node :m_vc)
{
cout << node << " ";
}
cout << endl;
int a1[4] = { 0 };
std::partial_sort_copy(m_vc.begin(), m_vc.end(), a1,a1+4);//默认是从小到大
cout << "partial_sort_copy后得到的前面最小的四个元素:";
for (auto node :a1)
{
cout << node << " ";
}
cout << endl;
int a2[4] = { 0 };
std::vector<int> m_vcx{ 1, 2, 3, 2, 100, 4, 6, 5, 700, -1 };
cout << "原数组:";
for (auto node : m_vcx)
{
cout << node << " ";
}
cout << endl;
std::partial_sort_copy(m_vcx.begin(), m_vcx.end(), a2,a2+4, [](int x, int y){return x > y; });
cout << "partial_sort_copy前面最大的四个元素:";
for (auto node : a2)
{
cout << node << " ";
}
cout << endl;
system("pause");
}
结果:
值得一提的是,partial_sort_copy() 函数实现排序的平均时间复杂度为N*log(min(N,M)),其中 N 指的是 [first, last) 范围的长度,M 指的是 [result_first, result_last) 范围的长度。