1. 程式人生 > >自定義關聯容器的排序規則

自定義關聯容器的排序規則

前言

  關聯式容器與序列容器最大的區別在於,內部結構是平衡二叉樹構成,而非線性表結構。這種結構決定了關聯式容器與生俱來的天賦——具有獨一無二的查詢效率。而決定這一特性的重要前提,則是其所具有自動排序的能力。這種排序能力保證了二叉樹的平衡性,從而保證了查詢效率控制在 O ( l o g

2 n ) O(log_2n) 的水平。
  關聯式容器提供了定義排序規則的介面,預設使用仿函式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)的特性,即可以直接通過<比較大小,那麼排序也就簡單了許多,可以直接呼叫greaterless函式定義排序規則,比如——

#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;
}

在這裡插入圖片描述

總結

  對於基本型別,我們基本上僅使用greaterless函式就能滿足幾乎所有的使用需求;對於複合物件型別,我們需要使用函式物件或者過載<運算子來定義比較規則。函式物件相比於過載運算子的方法略顯麻煩,但是也具有更強大的功能,並能在執行時大顯身手。
  本文所提到的所有方法都能直接在其他關聯容器中使用,這是STL中的通用標準。