本节我们将继续介绍 STL 中的 list 容器使用。
list 容器排序及合并元素
sort() 函数定义在头文件 algorithm 中,它要求使用随机访问迭代器,能在指定的迭代器范围内进行排序。
vector<int> data {1,5,0,3,2,4};
vector<int>::iterator begin_iter = begin(data);
auto end_iter = end(data);
sort(begin_iter,end_iter,compare);
compare 函数
bool compare(int a,int b){
return (a>b)?true:false;
}
通过自定义函数对象或 lambda 表达式,我们可以实现更多的操作。
而 list 容器并不提供随机访问迭代器,只提供双向迭代器,因此不能对 list 中的元素使用 sort() 算法。但是,list 容器还是可以进行元素排序,因为 list 模板定义了自己的 sort() 函数。
sort() 有两个版本:
第一个版本的无参 sort() 函数将所有元素升序排列。
第二个版本的 sort() 接受一个函数对象或 lambda 表达式作为参数,这两种参数都定义一个断言用来比较两个元素。
无参 sort() 实现升序:
list<int> data {1,5,0,3,2,4};
data.sort();
data 的内容为:
{0,1,2,3,4,5}
实现降序排序:
list<int> data {1,5,0,3,2,4};
data.sort(greater<int> ());
这里使用了定义在头文件 functional 中的模板 greater<T>。这个模板定义了一个用来比较 T 类型对象的函数对象。如果第一个参数大于第二个参数,那么成员函数 operator()() 返回 true。头文件 functional 中定义了一些定义了断言的模板,在后面,我们会看到更多的断言。
排序执行完毕后,list 中的元素如下:
{5,4,3,2,1,0}
现在 data 中的元素是降序排列的。有一个简洁版的 greater<T> 断言,可以如下所示使用:
list<int> data {1,5,0,3,2,4};
data.sort(greater<> ());
效果和上面的一样。
当然,在必要时可以将自定义的函数对象传给断言来对 list 排序。尽管对一般对象来说,并不需要这样。如果为自己的类定义了 operator()(),然后就可以继续使用 greater<>。 当我们需要比较非默认类型时,就需要一个函数对象。
例如,假设我们想对 data 中的元素进行排序。
如果使用字符串对象标准比较方式:
list<string> data {"Janet", "Jane", "Hugo", "Jules","Jim","Hannah", "Ann", "Alan"};
//升序排列
data.sort();
data 的内容为:
{"Alan","Ann","Hannah","Hugo","Jane","Janet","Jim","Jules"}
但是不想使用字符串对象标准的 greater<> 来比较,而是想将相同初始字符的字符串按长度排序。可以如下所示定义一个类:
class my_greater{
public:
bool operator () (const string s1, const string s2){
if (s1[0] == s2 [0])
return s1.length() > s2.length();
else
return s1 > s2;
}
};
对 data 中的元素进行排序:
list<string> data {"Janet", "Jane", "Hugo", "Jules","Jim","Hannah", "Ann", "Alan"};
data.sort(my_greater());
data 的内容为:
{"Jules","Janet", "Jane", "Jim","Hannah","Hugo","Alan","Ann"}
显然两种方式得到的结果是不同的。
如果不想重用 my_greater() 断言,这里也可以使用 lambda 表达式。
list<string> data {"Janet", "Jane", "Hugo", "Jules","Jim","Hannah", "Ann", "Alan"};
data.sort([](const string s1, const string s2){
if (s1[0] == s2[0])
return s1.length() > s2.length();
else
return s1 > s2;
});
data 的内容为:
{"Jules","Janet", "Jane", "Jim","Hannah","Hugo","Alan","Ann"}
list 的成员函数 merge() 以另一个具有相同类型元素的 list 容器作为参数。两个容器中的元素都必须是升序。参数 list 容器中的元素会被合并到当前的 list 容器中。
list<int> data {1,3,5,7,9};
list<int> data_ {0,2,4,6,8};
data.merge(data_);
data 的内容为:
{0,1,2,3,4,5,6,7,8,9}
此时,data_ 的内容为空了。
元素从 data_ 转移到 data,因此,在执行完这个操作后,data_ 中就没有元素了。改变每个 list 节点的指针,在适当的位置将它们和当前容器的元素链接起来,这样就实现了元素的转移。list 节点在内存中的位置不会改变;只有链接它们的指针变了。在合并的过程中,两个容器中的元素使用 operator()() 进行比较。
在另一个版本的 merge() 函数中,可以提供一个比较函数作为该函数的第二个参数,用来在合并过程中比较元素。
list<string> data { "three","six", "eight"};
list<string> data_ { "seven", "four", "nine"};
auto comp_str = [](const string s1, const string s2){
return s1[0]<s2[0];
};
对 data 排序后
data.sort (comp_str);
data 的内容为:
{"eight","six","three"}
对 data_ 排序后
data_.sort (comp_str);
data_ 的内容为:
{"four", "nine", "seven"}
data 和 data_ 合并后到 data :
data.merge (data_, comp_str);
data 的内容为:
{"eight", "four", "nine", "six", "seven", "three"}
这里的字符串对象比较函数是由 lambda 表达式定义的,这个表达式只比较第一个字符。比较的效果是,在合并的 list 容器中, "six" 在 "seven" 之前。在上面的代码中,也可以无参调用 merge(),这样 "seven" 会在 "six" 之前,这是一般的排序。
list 容器的成员函数 splice() 有几个重载版本。这个函数将参数 list 容器中的元素移动到当前容器中指定位置的前面。可以移动单个元素、一段元素或源容器的全部元素。
下面是一个剪切单个元素的示例:
list<string> data { "three","six", "eight"};
list<string> data_ { "seven", "four", "nine"};
auto begin_iter = begin(data);
list<string>::iterator begin_iter_ = begin(data_);
advance(begin_iter,1);
advance(begin_iter_,1);
data.splice(begin_iter,data_,begin_iter_);
splice() 的第一个参数是指向目的容器的迭代器。第二个参数是元素的来源。第三个参数是一个指向源list容器中被粘接元素的迭代器,它会被插入到第一个参数所指向位置之前。代码执行完中后,容器中的内容如下:
data_ : {"seven", "nine"}
data : {"three", "four", "six", "eight"}
当要粘接源 list 容器中的一段元素时,第 3 和第 4 个参数可以定义这段元素的范围。 例如:
list<string> data { "three","six", "eight"};
list<string> data_ { "seven", "four", "nine"};
auto begin_iter = begin(data);
list<string>::iterator begin_iter_ = begin(data_);
auto end_iter_ = end(data_);
advance(begin_iter,1);
advance(begin_iter_,1);
advance(end_iter_,0);
data.splice(begin_iter,data_,begin_iter_,end_iter_);
上面的代码会将 my_words 从第二个元素直到末尾的元素,粘接到 your_words 的第二个元素之前。
两个 list 容器的内容如下:
data_ : {"three"}
data : {"seven", "four", "six", "eight","nine"}
下面的语句可以将 data_ 的全部元素粘接到 data 中:
list<string> data { "three","six", "eight"};
list<string> data_ { "seven", "four", "nine"};
auto begin_iter = begin(data);
data.splice(begin_iter,data_);
两个 list 容器的内容如下:
data_ : {}
data : {"seven", "four","nine", "three", "six", "eight"}
现在即使 data_ 里面没有任何元素,依然可以执行合并操作。
list 容器访问元素
list 的成员函数 front() 和 back(),可以各自返回第一个和最后一个元素的引用。在空 list 中调用它们中的任意一个,结果是未知的,因此不要这样使用。可以通过迭代器的自增或自减来访问 list 的内部元素。
正如我们所了解的那样,begin() 和 end() 分别返回的是指向第一个和最后一个元素下一个位置的双向迭代器。rbegin() 和 rend() 函数返回的双向迭代器,可以让我们逆序遍历元素。
使用 begin() 和 end() 迭代器:
auto begin_iter = begin(data);
auto end_iter = end(data);
while(begin_iter!=end_iter){
cout<<*begin_iter<<" ";
begin_iter++;
}
结果如下:
one two three four
使用 rbegin() 和 rend() 迭代器:
auto begin_iter = rbegin(data);
auto end_iter = rend(data);
while(begin_iter!=end_iter){
cout<<*begin_iter<<" ";
begin_iter++;
}
结果如下:
four three two one
也可以不使用迭代器:
for(auto d : data){
cout<<d<<" ";
}
结果如下:
one two three four
结合 list 容器元素的插入操作,示例如下:
list<string> data {"one","two","three","four"};
string goal("zeros");
//在容器头部增加一个 "zeros"
data.emplace_front(goal);
goal = "five";
//在容器尾部增加一个 "five"
data.emplace_back(goal);
auto iter = begin(data);
// 移动迭代器
advance(iter,2);
goal = "one_two";
//在指定位置插入 ""one_two"
data.emplace(iter,goal);
iter = end(data);
//在容器最后处增加 "AAA"
data.emplace(iter,3,'A');
采用不使用迭代器的方式,每一步的操作,data 的内容为:
data : {"zeros","one","two","three","four"}
data : {"zeros","one","two","three","four","five"}
data : {"zeros","one","one_two","two","three","four","five"}
data : {"zeros","one","one_two","two","three","four","five","AAA"}
sort() 里面的断言是十分强大的,经常用到的断言,包括 greater_equal<>()、less<>() 和 less_equal<>()等,这里就不作展开了。
至此 list 容器的使用就暂告一段落了。