在上一篇篇博客中,我们了解了与string类和vector类中相通的函数,而在这篇博客中,我们来学习一下那些string类和vector类中没有的、list新引入的内容。
4.list的元素访问
C++ list 是一个双向链表,因此不像 vector 或 string 那样可以通过随机访问来获取元素。list 的元素访问需要通过迭代器逐个遍历。由于其双向链表结构,它没有 operator[] 或 at() 这样常见的随机访问函数。
4.1访问首尾元素
- front(): 返回链表中的第一个元素。
- back(): 返回链表中的最后一个元素。
std::list<int> mylist = {1, 2, 3};
int first = mylist.front(); // first == 1
int last = mylist.back(); // last == 3
4.2通过迭代器访问
由于 list 不支持随机访问,你必须通过迭代器来访问中间元素。你可以使用 begin() 和 end() 来遍历元素:
list<int> mylist = {1, 2, 3, 4, 5};
list<int>::iterator it = mylist.begin()
while (it != mylist.end())
{
cout << *it << " ";
++it;
}
如果要访问特定位置的元素,必须从 begin() 开始通过迭代器逐个移动,而无法直接访问某个位置。
4.3list 与 vector 和 string 的元素访问区别
1. 访问机制
- list:不能随机访问,只能通过迭代器逐步遍历。由于链表结构,`list` 的元素并不连续存储,因此获取中间元素时需要从头或尾遍历,时间复杂度是 O(n)。
- vector 和 `string:这两者都支持随机访问。由于 vector 和 string 是基于连续内存块的实现,元素之间按序排列,因此可以通过下标 operator[] 或 at() 以 O(1) 时间复杂度直接访问任意位置的元素。
//vector
std::vector<int> vec = {1, 2, 3, 4};
int second = vec[1]; // 直接访问第二个元素
//string
std::string str = "hello";
char secondChar = str[1]; // 直接访问第二个字符
2.适用场景
- list:更适合需要频频繁在中间插入或删除元素的场景,虽然遍历性能不如 vector,但插入和删除操作的效率较高,特别是在中间位置操作时。
- vector 和 string:适合频繁访问元素、需要随机访问的场景,且内存连续分配,局部性好。vector 尤其适合需要批量操作(如排序、搜索)且元素数量稳定的场景。
3.总结
list 由于链表结构不支持随机访问,访问元素的方式只能通过迭代器逐步遍历。
vector 和 string 则是连续存储,可以通过下标或 at() 进行 O(1) 时间复杂度的快速访问。
在选择容器时,如果访问频繁且随机,vector 和 string 更合适;如果插入和删除操作多,list 更合适。
5. list的操作函数
5.1splice(拼接)
std::list 提供了一种特殊的操作函数 `splice()`,用于将一个链表的元素移动到另一个链表中,而不涉及拷贝或内存重新分配。`splice()` 直接操作链表节点,因此效率非常高,尤其适合需要在不同链表之间移动元素的场景。
splice() 的三种形式
entire list (1)
void splice (const_iterator position, list& x);
single element (2)
void splice (const_iterator position, list& x, const_iterator i);
element range (3)
void splice (const_iterator position, list& x,
1. 移动整个链表
- 功能:将链表 `x` 的所有元素移动到当前链表的 `position` 位置,`x` 变为空链表。
std::list<int> list1 = {1, 2, 3};
std::list<int> list2 = {4, 5, 6};
auto it = list1.begin();
std::advance(it, 2); // 移动迭代器到 list1 的第三个元素
list1.splice(it, list2); // 将 list2 的所有元素移动到 list1 中的第三个元素前
结果:`list1 = {1, 2, 4, 5, 6, 3}`,`list2` 变为空。
2. 移动单个元素
- 功能:将链表 `x` 中的单个元素 `i` 移动到当前链表的 `position` 位置。
void splice (const_iterator position, list& x, const_iterator i);
std::list<int> list1 = {1, 2, 3};
std::list<int> list2 = {4, 5, 6};
auto it = list1.begin();
std::advance(it, 2); // 移动到 list1 的第三个元素
auto it2 = list2.begin();
std::advance(it2, 1); // 移动到 list2 的第二个元素
list1.splice(it, list2, it2); // 将 list2 中的 5 移动到 list1 的第三个元素前
结果:‘list1 = {1, 2, 5, 3}`,`list2 = {4, 6}`。
3. 移动一个范围的元素
void splice(iterator position, list& x, iterator first, iterator last);
std::list<int> list1 = {1, 2, 3};
std::list<int> list2 = {4, 5, 6, 7};
auto it = list1.begin();
std::advance(it, 2); // 移动到 list1 的第三个元素
auto first = list2.begin();
auto last = list2.end();
std::advance(last, -2); // 移动到 list2 的倒数第二个元素
list1.splice(it, list2, first, last); //将list2中的4和5移动到list1的第三个元素前
结果:`list1 = {1, 2, 4, 5, 3}`,`list2 = {6, 7}`。
5.2 remove
std::list 中的 remove() 函数用于删除所有等于给定值的元素。该操作遍历整个链表,移除匹配的元素,并保持链表的其它元素顺序不变。remove() 不需要传入迭代器,因为它是直接基于值来进行匹配的。
- remove():根据传入的值删除链表中所有等于该值的元素。
- remove_if():根据自定义的条件删除链表中满足该条件的元素。
(1)remove
void remove(const T& value);
#include <iostream>
#include <list>
int main() {
// 初始化一个 list
list<int> mylist = {10, 25, 30, 20, 40, 50, 20, 5};
// 使用 remove() 删除所有值为 20 的元素
mylist.remove(20);
// 输出调用 remove() 后的列表
cout << "List after removing 20: ";
for (int elem : mylist) {
cout << elem << " ";// 10 25 30 40 50 5
}
cout << endl;
return 0;
}
(2)remove_if
template <class Predicate>
void remove_if(Predicate pred);
- 参数:`pred` 是谓词(即一个返回 `bool` 值的函数或 lambda 表达式)。它会传入每个元素,若返回 `true`,该元素将被移除。
pred 的特点:
- 参数类型:`pred` 接收链表中的元素作为参数。
- 返回类型:`pred` 返回一个 `bool` 值。如果返回 `true`,则 `remove_if()` 函数会删除该元素;如果返回 `false`,则保留该元素。
- 可以是:普通函数、函数对象、lambda 表达式,或者函数指针。
示例:使用 `lambda` 表达式作为谓词删除偶数元素
#include <iostream>
#include <list>
int main() {
// 初始化一个 list
list<int> mylist = {10, 15, 20, 25, 30, 35, 40};
// 使用 remove_if() 删除偶数元素,谓词是一个 lambda 表达式
mylist.remove_if([](int x) { return x % 2 == 0; });
return 0;
}
删除后的结果:15 25 35
在这个例子中,`pred` 是 lambda 表达式 `[](int x) { return x % 2 == 0; }`,该表达式接收每个元素 `x`,并检查它是否是偶数。如果是偶数,则返回 `true`,表示要删除这个元素。
使用普通函数作为谓词删除偶数元素
#include <iostream>
#include <list>
bool isOdd(int x) {
return x % 2 != 0; // 如果元素是奇数,返回 true
}
int main() {
// 初始化一个 list
list<int> mylist = {10, 15, 20, 25, 30, 35, 40};
// 使用 remove_if() 删除奇数元素,谓词是一个普通函数
mylist.remove_if(isOdd);
return 0;
}
删除后的结果:15 25 35
在这个例子中,`pred` 是普通函数 `isOdd()`,它接收一个整数 `x` 并返回 `true` 或 `false`。如果元素是奇数,它返回 `true`,从而删除这个元素。
总结:
- pred 是谓词:它是一个接收元素并返回布尔值的函数。
- remove_if() 的工作原理:每次迭代链表元素时,`remove_if()` 都会将当前元素传递给 `pred`。如果 `pred` 返回 `true`,则该元素会被删除,否则保留。删除元素后,链表的顺序不变,其他元素将保持不动。
- 灵活性:由于可以使用 lambda 表达式、普通函数或函数对象,你可以定义任意条件来删除链表中的元素。
(3)Lambda表达式简要介绍
Lambda 表达式是 C++11 引入的一种匿名函数,允许在代码中定义轻量级的、简洁的函数。它通常用于临时需要一个函数或传递一个简单的行为,例如在算法中作为谓词(predicate),或在线程库中作为回调函数。
1、Lambda 表达式的基本语法:
[capture](parameters) -> return_type { body }
- capture:用于捕获外部变量,可以按值或按引用捕获,也可以捕获所有外部变量。
- parameters:函数的参数列表,类似普通函数的参数。
- return_type(可选):返回值类型,可以省略,如果省略,编译器会根据 `body` 推导返回类型。
- body:函数的执行代码块,和普通函数的函数体一样。
2、捕获外部变量
在 lambda 表达式的 `capture` 部分,可以捕获函数外部的变量,这使得它比普通函数更加灵活。
捕获方式:
[=]:按值捕获外部变量(外部变量的副本,不能修改)。
[&`:按引用捕获外部变量(可以修改外部变量)。
[x]:按值捕获变量 `x`。
[&x]:按引用捕获变量 `x`。
[=, &y]:按值捕获所有外部变量,但按引用捕获 `y`。
[&, x]:按引用捕获所有外部变量,但按值捕获 `x`。
5.3 unique
`std::list::unique` 函数用于移除链表中相邻的重复元素,确保每个相邻的元素都不相等。如果链表是排序的,那么 `unique()` 可以移除所有重复的元素。如果链表没有排序,它只能移除相邻的相同元素。
语法:
(1) void unique();
(2) template <class BinaryPredicate>
void unique (BinaryPredicate binary_pred);
(1)无参版本
该版本删除相邻的重复元素,比较是基于 `operator==`。
#include <iostream>
#include <list>
int main() {
// 初始化一个 list,包含一些相邻的重复元素
std::list<int> mylist = {1, 2, 2, 3, 3, 3, 4, 5, 5, 6};
// 使用 unique() 移除相邻的重复元素
mylist.unique();//1 2 3 4 5 6
return 0;
}
(2)带谓词版本:
该版本根据用户提供的二元谓词 `pred` 来判断相邻元素是否应该被认为是重复的。
#include <iostream>
#include <list>
#include <cctype>
bool caseInsensitiveCompare(char a, char b) {
return std::tolower(a) == std::tolower(b); // 忽略大小写
}
int main() {
// 初始化一个 list,包含相邻的大写和小写字符
std::list<char> mylist = {'a', 'A', 'b', 'B', 'b', 'B', 'c', 'C'};
// 使用 unique() 和自定义谓词,忽略大小写
mylist.unique(caseInsensitiveCompare);//a b c
return 0;
}
解释:
- 使用了自定义的谓词 `caseInsensitiveCompare()`,这个谓词比较两个字符的大小写是否相同,如果相同则认为它们是重复的。
- 在调用 `unique()` 后,相邻的大写和小写字符被视为重复,并且删除相邻重复的部分。
(3)总结:
- unique() 用于移除相邻的重复元素,只保留每组相同元素中的第一个。
- 该函数不需要链表是有序的,但只能删除相邻的重复项。
- 通过提供一个自定义的谓词,可以用自定义的比较方式来定义“重复”的标准,比如忽略大小写的字符比较。
5.4 merge
`std::list::merge` 函数用于将两个有序链表合并为一个有序链表。这个函数假定两个链表中的元素已经按升序(或其他排序顺序)排列,因此合并后的链表也是有序的。
`merge` 函数有两种版本:
1. 默认版本,使用 `<` 操作符进行比较。
2. 自定义谓词版本,使用用户提供的比较函数或谓词。
(1)
void merge (list& x);
(2)
template <class Compare>
void merge (list& x, Compare comp);
(1)默认版本
将 `x` 链表合并到当前链表中,合并后 `other` 链表会被清空。
#include <iostream>
#include <list>
int main() {
// 初始化两个有序的 list
list<int> list1 = {1, 3, 5, 7};
list<int> list2 = {2, 4, 6, 8};
// 合并两个有序链表
list1.merge(list2);
// 输出合并后的链表
for (int elem : list1) {
std::cout << elem << " ";//1 2 3 4 5 6 7 8
}
return 0;
}
解释:
- `list1.merge(list2)` 将 `list2` 合并到 `list1` 中,并保持升序排列。
- 合并之后,`list2` 会被清空,所有元素都转移到了 `list1` 中。
- 由于 `merge()` 假定链表已经有序,因此两个链表必须是升序排列的,否则代码运行可能会报错。
(2)带谓词版本:
使用自定义的比较函数 `Compare` 来合并链表。
#include <iostream>
#include <list>
// 自定义比较函数,用于降序排列
bool compareDescending(int first, int second) {
return first > second;
}
int main() {
// 初始化两个降序排列的 list
std::list<int> list1 = {7, 5, 3, 1};
std::list<int> list2 = {8, 6, 4, 2};
// 使用自定义比较函数进行降序合并
list1.merge(list2, compareDescending);
// 输出合并后的链表
for (int elem : list1) {
std::cout << elem << " ";//8 7 6 5 4 3 2 1
}
return 0;
}
(3)merge() 的几点注意事项:
1. 链表必须有序:无论是升序还是自定义顺序,传递给 `merge()` 的链表都应该按照某种顺序排列。
2. 合并后清空:`merge()` 会将 `other` 链表的元素移到当前链表中,合并后 `other` 会变为空链表。
3. 稳定性:`merge()` 是稳定的,它不会改变相同元素的相对顺序。
5.5 sort
`std::list::sort` 函数用于对 `std::list` 中的元素进行排序。与 `std::vector` 不同的是,`std::list` 由于是双向链表,无法通过随机访问进行排序,而是使用链表内部的指针操作,因此它的排序效率比对数组或 `vector` 进行排序更低一些,但仍然是稳定的排序算法。
`std::list::sort` 有两种常用形式:
1. 默认使用小于运算符 (`<`) 进行排序。
2. 使用自定义的比较函数或谓词进行排序。
(1)无参版本
(1) void sort();
(2) template <class Compare>
void sort (Compare comp);
按升序排列列表中的元素,使用 `<` 运算符进行比较。
#include <iostream>
#include <list>
int main() {
// 初始化一个乱序的 list
std::list<int> mylist = {5, 2, 9, 1, 5, 6};
// 调用 sort() 进行升序排序
mylist.sort();//1 2 5 5 6 9
return 0;
}
解释:
- 调用 `mylist.sort()` 使用默认的 `<` 比较运算符对列表进行排序,结果为升序。
- `list::sort()` 是一个稳定的排序,即如果有相等的元素(例如 `5`),它们在排序后仍然保持相对顺序不变
对字符串排序
#include <iostream>
#include <list>
#include <string>
int main() {
// 初始化一个包含字符串的 list
std::list<std::string> mylist = {"apple", "orange", "banana", "grape"};
// 对字符串列表进行升序排序
mylist.sort();// apple banana grape orange
return 0;
}
解释:
- 调用 `mylist.sort()` 按照字符串的字母顺序进行排序,结果为升序的字典顺序。
(2)带谓词版本
#include <iostream>
#include <list>
// 自定义的比较函数,用于降序排序
bool compareDescending(int first, int second) {
return first > second;
}
int main() {
// 初始化一个乱序的 list
std::list<int> mylist = {5, 2, 9, 1, 5, 6};
// 使用自定义比较函数进行降序排序
mylist.sort(compareDescending);9 6 5 5 2 1
return 0;
}
解释:
- `compareDescending()` 是一个自定义的比较函数,它比较两个元素的大小,并返回 `true` 如果第一个元素大于第二个元素。这使得排序结果为降序。
- `mylist.sort(compareDescending)` 使用这个自定义函数对列表进行降序排序。
(3)sort() 的几点注意事项:
1. 链表必须可比较:默认的 `sort()` 使用 `<` 运算符进行比较,因此链表中的元素类型必须支持 `<` 运算符。如果使用自定义的比较函数,则只需该函数定义比较逻辑即可。
2. 链表大小:虽然 `std::list` 是一个双向链表,但 `sort()` 的时间复杂度仍然是 `O(N log N)`,与其他容器的排序一样,因为它使用的是高效的归并排序。
3. 稳定性:`list::sort()` 是稳定的,这意味着如果两个元素在排序前相等,它们的相对顺序在排序后仍然保持不变。
5.6 reverse
`std::list::reverse` 函数用于将链表中的元素顺序完全颠倒。它直接对链表进行原地修改,而不是创建一个新的链表,时间复杂度为 `O(N)`。
语法:
void reverse();
- 效果:它会将链表中的元素顺序逆转,使得第一个元素变为最后一个元素,最后一个元素变为第一个元素。
#include <iostream>
#include <list>
int main() {
// 初始化一个 list
std::list<int> mylist = {1, 2, 3, 4, 5};
// 调用 reverse() 颠倒列表中的元素顺序
mylist.reverse();// 5 4 3 2 1
return 0;
}
特点:
1. 原地操作:`reverse()` 不创建新的链表,而是直接对现有链表进行修改,不需要额外的空间。
2. 链表顺序反转:它直接颠倒整个链表的顺序,包括头部和尾部。
3. 时间复杂度:`O(N)`,因为每个元素的顺序需要调整一次。
list的特性与注意事项
- 双向链表:`std::list` 是双向链表,所以它有 `begin()` 和 `end()` 迭代器,也有 `rbegin()` 和 `rend()` 迭代器用于反向遍历。
- 性能优化:适用于大量插入、删除操作的场景。但在频繁访问的情况下,链表的缓存局部性不如 `vector` 好。
- 优点:在需要频繁插入/删除操作的场景下,`list` 提供了更好的性能。
- *缺点:不支持随机访问,遍历性能相对较低。
通过对比 C++ `list` 与 `vector` 和 `string`,我们可以看到不同容器在操作特性和应用场景上的差异。`vector` 提供了随机访问和连续存储的优势,适合需要频繁访问特定元素的场景,而 `string` 则是专门为处理字符序列而设计的,它提供了丰富的字符串操作函数。
相比之下,`list` 的双向链表结构更适合频繁的插入和删除操作,特别是在中间位置进行操作时效率更高。虽然 `list` 无法像 `vector` 和 `string` 那样进行高效的随机访问,但在需要高效调整容器元素时,它提供了独特的优势。