前言
关联式容器与序列容器最大的区别在于,内部结构是平衡二叉树构成,而非线性表结构。这种结构决定了关联式容器与生俱来的天赋——具有独一无二的查找效率。而决定这一特性的重要前提,则是其所具有自动排序的能力。这种排序能力保证了二叉树的平衡性,从而保证了查找效率控制在
的水平。
关联式容器提供了定义排序规则的接口,默认使用仿函数less<typename T>
作为排序准则,less
函数通过operator<
对元素进行排序。与此同时,我们也可以通过这个接口自定义自己的排序准则,来适应更复杂的使用场景。
本文以Set为例,对一些自定义排序规则用法进行总结。当然,这些规则也同样适用于Multiset、Map和Multimap等。
以template参数定义排序规则
Set的第二个template接口,是用来控制排序方式的唯一接口,关联容器中,为了确保只有排序准则相同的容器才能被合并,第二参数排序准则,被认为是一种类型。
template<
typename Key,
typename Compare = std::less<Key>,
typename Allocator = std::allocator<Key>
> class set;
对于基本类型(int/float/double/long/…)与对象类型,使用该接口的方式有所不同,我们分别讨论:
greater和less函数
基本对象均满足可比较(Comparable)和可复制(Copyable)的特性,即可以直接通过<
比较大小,那么排序也就简单了许多,可以直接调用greater
与less
函数定义排序规则,比如——
#include<iostream>
#include<set>
using namespace std;
int main(){
set<int, greater<int>> s{ 1,2,3,4,5,6 };
for (auto it = s.begin(); it != s.end(); it++) {
cout << *it << endl;
}
return 0;
}
由于使用了greater函数,因此并没有升序输出,而是降序。
函数对象定义比较规则
对于用户自定义的类型,情况比较复杂,假设我们自定义一个类,用于记录人名——
#include<iostream>
#include<set>
using namespace std;
// 自定义类
class Person{
public:
Person(string str1, string str2):firstname(str1),lastname(str2) {}
string firstname;
string lastname;
friend ostream& operator<<(ostream&out, const Person & p);
};
int main(){
set<Person> ss{ Person("test1","test2") };
for (auto&elem : ss) {
cout << elem << endl;
}
return 0;
}
如果没有重载类的<
运算符,那么意味着该类所对应的实例化对象都是没有比较依据的,此时编译器会报错——
提示没有对应的<
操作符用于比较,我们需要手动添加一个比较规则。C++标准库广泛使用一种叫做函数对象(Function Object)的方法定义比较规则,修改如下:
// 这个类的唯一作用就是定义了Person的比较规则
class PersonSortCriterion {
public:
bool operator() (const Person&p1, const Person&p2) const {
return p1.firstname < p2.firstname ||
(p1.firstname == p2.firstname&&p1.lastname < p2.lastname);
}
};
将这个两段代码综合一下,我们再次使用Set记录人名——
#include<iostream>
#include<set>
using namespace std;
class Person{
public:
Person(string str1, string str2):firstname(str1),lastname(str2) {}
string firstname;
string lastname;
friend ostream& operator<<(ostream&out, const Person & p);
};
class PersonSortCriterion {
public:
bool operator() (const Person&p1, const Person&p2) const {
return p1.firstname < p2.firstname ||
(p1.firstname == p2.firstname&&p1.lastname < p2.lastname);
}
};
// 为了方便输出,我们重载了<<符号
ostream &operator<<(ostream&out,const Person & p)
{
// TODO: insert return statement here
cout << p.firstname << " " << p.lastname;
return out;
}
int main(){
set<Person,PersonSortCriterion> ss{
Person("Jason","Lee"),
Person("Jack","Chen"),
Person("Alpha","Lee") };
for (auto&elem : ss) {
cout << elem << endl;
}
return 0;
}
按照我们在PersonSortCriterion
中定义的规则一样,Set中的元素按序输出。
重载operator<函数
在Set中,默认使用仿函数less
进行排序,但实际上less底层也是调用operator<
进行比较——
// Visual Studio 2017
template<class _Ty = void>
struct less
{ // functor for operator<
_CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef _Ty first_argument_type;
_CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef _Ty second_argument_type;
_CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef bool result_type;
constexpr bool operator()(const _Ty& _Left, const _Ty& _Right) const
{ // apply operator< to operands
return (_Left < _Right);
}
};
既然如此,我们直接重载<
运算符,也能实现我们的排序规则(如果可以的话)——
#include<iostream>
#include<set>
using namespace std;
class Person{
public:
Person(string str1, string str2):firstname(str1),lastname(str2) {}
string firstname;
string lastname;
bool operator<(const Person&p) const;
friend ostream& operator<<(ostream&out, const Person & p);
};
// 重载<来定义比较规则
ostream &operator<<(ostream&out,const Person & p)
{
// TODO: insert return statement here
cout << p.firstname << " " << p.lastname;
return out;
}
// 为了方便输出,我们重载了<<符号
ostream &operator<<(ostream&out,const Person & p)
{
// TODO: insert return statement here
cout << p.firstname << " " << p.lastname;
return out;
}
int main(){
set<Person> ss{
Person("Jason","Lee"),
Person("Jack","Chen"),
Person("Alpha","Lee"),
Person("Alience","Steven"),
Person("Luffy","Lily")
};
for (auto&elem : ss) {
cout << elem << endl;
}
return 0;
}
效果如理想所愿。
使用构造函数参数——定义运行时排序规则
无论是使用容器的排序规则参数,还是重载<
运算符,通常都是将排序规则作为类型的一部分。但是有时候必须在运行时处理排序规则,或者有时候需要对同一种数据类型在不同的时段定义不同的排序规则,这个时候就需要一个运行时排序规则。
我们仿照PersonSortCriterion
类定义一个RT_CMP
(Run time compare function)类,该类的作用是通过构造函数传值来确定具体函数对象,从而根据这个函数对象的具体状态来确定排序规则。
#include<iostream>
#include<set>
using namespace std;
// 排序规则类
class RT_CMP {
public:
// normal表示升序,reverse表示降序
enum cmp_mode { normal, reverse };
private:
// 控制升序降序的枚举型变量
cmp_mode mode;
public:
// 构造函数确定初始状态
RT_CMP(cmp_mode m = normal) :mode(m) {
}
template<class T>
bool operator()(const T&t1, const T&t2) const {
return mode == normal ? t1<t2
: t1>t2;
}
bool operator==(const RT_CMP&rc) const {
return mode == rc.mode;
}
};
int main(){
// 使用升序的排序准则
set<int, RT_CMP> s1 = { 123,458,645,784,894 };
for (auto elem : s1) {
cout << elem << " ";
}
cout << endl;
// 通过RT_CMP构造函数,指定降序排序规则
RT_CMP reverse_order(RT_CMP::reverse);
// 通过set的构造函数来确定最终的排序规则
set<int, RT_CMP> s2(reverse_order);
s2 = { 123,458,645,784,894 };
for (auto elem : s2) {
cout << elem << " ";
}
cout << endl;
// 比较s1和s2的排序规则是否相同
if (s1.key_comp() == s2.key_comp()) {
cout << "s1 and s2 have the same sorting criterion." << endl;
}
else {
cout << "s1 and s2 have a different sorting criterion." << endl;
}
s1 = s2;
if (s1.key_comp() == s2.key_comp()) {
cout << "s1 and s2 have the same sorting criterion." << endl;
}
else {
cout << "s1 and s2 have a different sorting criterion." << endl;
}
return 0;
}
总结
对于基本类型,我们基本上仅使用greater
和less
函数就能满足几乎所有的使用需求;对于复合对象类型,我们需要使用函数对象或者重载<
运算符来定义比较规则。函数对象相比于重载运算符的方法略显麻烦,但是也具有更强大的功能,并能在运行时大显身手。
本文所提到的所有方法都能直接在其他关联容器中使用,这是STL中的通用标准。