list 容器模板定义在 list 头文件中,是 T 类型对象的双向链表。因此,使用 list 模板时,要
#include <list>
list 容器具有一些 vector 和 deque 容器所不具备的优势,它可以在常规时间内,在序列已知的任何位置插入或删除元素。这是我们使用 list,而不使用 vector 或 deque 容器的主要原因。
list 的缺点是无法通过位置来直接访问序列中的元素,也就是说,不能索引元素。为了访问 list 内部的一个元素,必须一个一个地遍历元素,通常从第一个元素或最后一个元素开始遍历。
list 容器的每个 T 对象通常都被包装在一个内部节点对象中,节点对象维护了两个指针,一个指向前一个节点,另一个指向下一个节点。这些指针将节点连接成一个链表。通过指针可以从任何位置的任一方向来遍历链表中的元素。第一个元素的前向指针总是为 null,因为它前面没有元素,尾部元素的后向指针也总为 null。这使我们可以检测到链表的尾部。list 实例保存了头部和尾部的指针。这允许我们从两端访问链表,也允许从任一端开始按顺序检索列表中的元素。
以 list 为参数,调用 begin() 可以得到指向 list 中第一个元素的迭代器。通过调用 end(),可以得到一个指向最后一个元素下一个位置的迭代器,因此像其他序列容器一样,可以用它们来指定整个范围的元素。
也可以像其他容器一样,使用 rbegin()、rend()、crbegin()、crend()、cbegin()、cend() 来获取反向迭代器和 const 迭代器。
这里加上一点自己的个人理解,list之所以不能通过位置来访问来直接访问序列中的元素,是因为list与array、vector不同,list不是从内存中划取中一块连续的内存来进行数据的存储,list的数据存储是随机的,它包含有两个指针,头指针和尾指针,这样就很方便于数据的插入和删除。
list容器的使用、创建和初始化
list 容器的构造函数的用法类似于 vector 或 deque 容器。下面这条语句生成了一个空的 list 容器:
std::list<std::string> words;
可以创建一个带有给定数量的默认元素的列表:
std::list<std::string> sayings{20}; // A list of 20 empty strings
元素的个数由构造函数的参数指定,每一个元素都由默认的构造函数生成,因此这里调用 string() 来生成元素。
下面展示如何生成一个包含给定数量的相同元素的列表:
std::list<double> values(50, 3.14159265);
这里生成了一个具有 50 个 double 型值的列表,并且每一个值都等于 π。注意在圆括号中,不能使用初始化列表 {50,3.14159265},这样列表将仅仅包含两个元素。
list 容器有一个拷贝构造函数,因此可以生成一个现有 list 容器的副本:
std::list<double> save_values{values}; // Duplicate of values
可以用另一个序列的开始和结束迭代器所指定的一段元素,来构造 list 容器的初始化列表:
std::list<double> samples{++cbegin(values), --cend(values)};
除了 values 中的第一个和最后一个元素,其他元素都被用来生成列表。因为 list 容器的 begin() 和 end() 函数返回的都是双向迭代器,所以不能用它们加减整数。修改双向迭代器的唯一方式是使用自增或自减运算符。当然,在上面的语句中,初始化列表中的迭代器可以代表任意容器的一段元素,而不仅仅只是 list 容器。
可以通过调用 list 容器的成员函数 size() 来获取它的元素个数。也可以使用它的 resize() 函数来改变元素个数。如果 resize() 的参数小于当前元素个数,会从尾部开始删除多余的元素。如果参数比当前元素个数大,会使用所保存元素类型的默认构造函数来添加元素。
list增加和插入元素
1. push_front(),push_back()
可以使用 list 容器的成员函数 push_front() 在它的头部添加一个元素。调用 push_back() 可以在 list 容器的末尾添加一个元素。在下面这两个示例中,参数作为对象被添加:
std::list<std::string> names { "Jane", "Jim", "Jules", "Janet"};
names.push_front("Ian"); // Add string ("Ian") to the front of the list
names.push_back("Kitty"); // Append string ("Kitty") to the end of the list
这两个函数都有右值引用参数的版本,这种版本的函数会移动参数而不是从参数复制新的元素。它们显然要比其他使用左值引用参数的版本高效。
2.emplace(),emplace_front(),emplace_back()
然而,成员函数 emplace_front() 和 emplace_back() 可以做得更好:
names.emplace_front("Ian"); // Create string ("Ian") in place at the front of the list
names.emplace_back("Kitty"); // Create string ("Kitty") in place at the end of the list
这些成员函数的参数是调用以被生成元素的构造函数的参数。它们消除了 push_front() 和 push_back() 不得不执行的右值移动运算。
emplace() 在迭代器指定的位置构造一个元素;
emplace_front() 在 list 的第一个元素之前构造元素;
emplace_back() 在 list 的尾部元素之后构造元素。
下面是一些用法示例:
std::list<std:: string> names {"Jane", "Jim", "Jules", "Janet"};
names.emplace_back("Ann");
std:: string name ("Alan");
names.emplace_back(std::move(name));
names.emplace_front("Hugo");
names.emplace(++begin(names), "Hannah");
第 4 行代码用 std::move() 函数将 name 的右值引用传入 emplace_back() 函数。这个操作执行后,name 变为空,因为它的内容已经被移到 list 中。
执行完这些语句后,names 中的内容如下:
"Hugo” "Hannah" "Jane" "Jim" "Jules" "Janet" "Ann" "Alan"
3.insert()
可以使用成员函数 insert() 在 list 容器内部添加元素。
- 在迭代器指定的位置插入一个新的元素:
std::list<int> data(10, 55); // List of 10 elements with value 55
data.insert(++begin(data), 66); // Insert 66 as the second element
insert() 的第一个参数是一个指定插入点的迭代器,第二个参数是被插入的元素。begin() 返回的双向迭代器自增后,指向第二个元素。
上面的语句执行完毕后,list 容器的内容如下:
55 66 55 55 55 55 55 55 55 55 55
list 容器现在包含 11 个元素。插入元素不必移动现有的元素。生成新元素后,这个过程需要将插入点前后两个元素的 4 个指针重新设为适当的值,第一个元素的 next 指针指向新的元素,原来的第二个元素的 pre 指针也指向新的元素,新元素的 pre 指针指向第一个元素,next 指针指向序列之前的第二个元素。
和 vector、deque 容器的插入过程相比,这个要快很多,并且无论在哪里插入元素,花费的时间都相同。
- 可以在给定位置插入几个相同元素的副本:
auto iter = begin(data);
std::advance(iter, 9); // Increase iter by 9
data.insert(iter, 3, 88); // Insert 3 copies of 88 starting at the 10th
iter 是 list::iterator 类型。insert() 函数的第一个参数是用来指定插入位置的迭代器,第二个参数是被插入元素的个数,第三个参数是被重复插入的元素。
为了得到第 10 个元素,可以使用定义在 iterator 头文件中的全局函数 advance(),将迭代器增加 9。只能增加或减小双向迭代器。因为迭代器不能直接加 9,所以 advance() 会在循环中自增迭代器。
执行完上面的代码后,list 容器的内容如下:
55 66 55 55 55 55 55 55 55 88 88 88 55 55
现在 list 容器包含 14 个元素。下面展示如何将一段元素插入到data列表中:
std::vector<int> numbers(10, 5); // Vector of 10 elements with value 5
data.insert(--(--end(data)), cbegin(numbers), cend(numbers));
insert() 的第一个参数是一个迭代器,它指向 data 的倒数第二个元素。第二和第三个参数指定了 number 中被插入元素的范围,因此从 data 中倒数第二个元素开始,依次插入 vector 的全部元素。代码执行后,data 中的内容如下:
55 66 55 55 55 55 55 55 55 88 88 88 5 5 5 5 5 5 5 5 5 5 55 55
list 容器现在有 24 个元素。从 numbers 中倒数第二个元素的位置开始插入元素,list 最右边两个元素的位置被替换了。尽管如此,指向最后两个元素的任何迭代器或结束迭代器都没有失效。list 元素的迭代器只会在它所指向的元素被删除时才会失效。
list删除元素
对于 list 的成员函数 clear() 和 erase(),它们的工作方式及效果,和前面的序列容器相同。
list 容器的成员函数 remove() 则移除和参数匹配的元素。例如:
std::list<int> numbers{2, 5, 2, 3, 6, 7, 8, 2, 9};
numbers.remove(2); // List is now 5 3 6 7 8 9
第二条语句移除了 numbers 中出现的所有值等于 2 的元素。
成员函数 remove_if() 期望传入一个一元断言作为参数。一元断言接受一个和元素同类型的参数或引用,返回一个布尔值。断言返回 true 的所有元素都会被移除。例如:
numbers.remove_if([](int n){return n%2 == 0;}); // Remove even numbers. Result 5 3 7 9
这里的参数是一个 lambda 表达式,但也可以是一个函数对象。
成员函数 unique() 可以移除连续的重复元素,只留下其中的第一个。例如:
std::list<std::string> words{"one", "two", "two", "two","three", "four", "four"};
words.unique(); // Now contains "one" "two" "three" "four"
这个版本的 unique() 函数使用 == 运算符比较连续元素。可以在对元素进行排序后,再使用 unique(),这样可以保证移除序列中全部的重复元素。
list排序及合并元素
排序
sort() 函数模板定义在头文件 algorithm 中,要求使用随机访问迭代器,但 list 容器并不提供随机访问迭代器,只提供双向迭代器,因此不能对 list 中的元素使用 sort() 算法。
但是,还是可以进行元素排序,因为 list 模板定义了自己的 sort() 函数。
下面是一个调用 list 成员函数 sort() 的示例:
names.sort(std::greater<std::string>()); // Names in descending sequence
排序执行完毕后,list 中的元素如下:
"Jules" "Jim" "Janet" "Jane" "Hugo" "Hannah" "Ann" "Alan"
有一个简洁版的 greater 断言,可以如下所示使用:
names.sort(std::greater<>()); // Function object uses perfect forwarding
简洁版的函数对象可以接受任何类型的参数。
但是,当我们需要按照自己实际项目需要的对元素进行非标准库的操作,就需要自己定义一个函数来实现,
例如,假设我们想对 names 中的元素进行排序,但是不想使用字符串对象标准的 std::greater<> 来比较,而是想将相同初始字符的字符串按长度排序。可以如下所示定义一个类:
// Order strings by length when the initial letters are the same
class my_greater
{
public:
bool operator () (const std::strings s1, const std::strings s2)
{
if (s1[0] == s2 [0])
return s1.length() > s2.length();
else
return s1 > s2;
}
};
可以用这个来对 names 中的元素排序:
names.sort(my_greater()); // Sort using my_greater
代码执行完毕后,list 中包含的元素如下:
"Jules" "Janet" "Jane” "Jim" "Hannah" "Hugo" "Alan" "Ann"
这个结果和前面使用字符串对象标准比较方式的结果明显不同。names 中初始字符相同的元素按长度降序排列。当然,如果不需要重用 my_greater() 断言,这里也可以使用 lambda 表达式。
下面是一个用 lambda 表达式实现的示例:
names.sort([](const std::strings s1, const std::strings s2)
{
if (s1[0] == s2[0])
return s1.length() > s2.length();
else
return s1 > s2;
});
这和前面代码的作用相同。
合并
list 的成员函数 merge() 以另一个具有相同类型元素的 list 容器作为参数。两个容器中的元素都必须是升序。参数 list 容器中的元素会被合并到当前的 list 容器中。
std::list<int> my_values {2, 4, 6, 14};
std::list<int> your_values{ -2, 1, 7, 10};
my_values.merge (your_values); // my_values contains: -2 1 2 4 6 7 10 14
your_values.empty(); // Returns true
元素从 your_values 转移到 my_values,因此,在执行完这个操作后,your_values 中就没有元素了。
改变每个 list 节点的指针,在适当的位置将它们和当前容器的元素链接起来,这样就实现了元素的转移。list 节点在内存中的位置不会改变;只有链接它们的指针变了。
在另一个版本的 merge() 函数中,可以提供一个比较函数作为该函数的第二个参数,用来在合并过程中比较元素。例如:
std::list<std::string> my_words { "three","six", "eight"};
std::list<std::string> your_words { "seven", "four", "nine"};
auto comp_str = [](const std::strings s1, const std::strings s2){ return s1[0]<s2[0];};
my_words.sort (comp_str); // "eight" "six" "three"
your_words.sort (comp_str) ; // "four" "nine" "seven"
my_words.merge (your_words, comp_str) ; // "eight" "four" "nine" "six" "seven" "three"
这里的字符串对象比较函数是由 lambda 表达式定义的,这个表达式只比较第一个字符。
比较的效果是,在合并的 list 容器中,"six”在”seven”之前。在上面的代码中,也可以无参调用 merge(),这样"seven"会在"six"之前,这是一般的排序。
剪切
list 容器的成员函数 splice() 有几个重载版本。
这个函数将参数 list 容器中的元素移动到当前容器中指定位置的前面。可以移动单个元素、一段元素或源容器的全部元素。
下面是一个剪切单个元素的示例:
std::list<std::string> my_words{"three", "six", "eight"};
std::list<std::string> your_words{"seven", "four", "nine"};
my_words.splice(++std::begin(my_words), your_words, ++std::begin(your_words));
splice() 的第一个参数是指向目的容器的迭代器,第二个参数是元素的来源,第三个参数是一个指向源list容器中被粘接元素的迭代器,它会被插入到第一个参数所指向位置之前。
代码执行完中后,容器中的内容如下:
your_words: "seven", "nine"
my_words : "three", "four", "six", "eight"
当要粘接源 list 容器中的一段元素时,第 3 和第 4 个参数可以定义这段元素的范围。 例如:
your_words.splice(++std::begin(your_words),my_words,++std::begin(my_words), std::end(my_words));
上面的代码会将 my_words 从第二个元素直到末尾的元素,粘接到 your_words 的第二个元素之前。上面两个 list 容器的内容如下:
your_words:"seven", "four", "six", "eight","nine"
my_words: "three"
下面的语句可以将 your_words 的全部元素粘接到 my_words 中:
my_words.splice(std::begin(my_words), your_words);
your_words 的所有元素被移到了 my_words 的第一个元素"three”之前。然后,your_words 会变为空。
即使 your_words 为空,也仍然可以向它粘接元素:
your_words.splice(std::end(your_words), my_words);
现在,my_words 变为空,your_words 拥有全部元素。第一个参数也可以是 std::begin (your_words),因为当容器为空时,它也会返回一个结束迭代器。
list访问(获取)元素
list 的成员函数 front() 和 back(),可以各自返回第一个和最后一个元素的引用。在空 list 中调用它们中的任意一个,结果是未知的,因此不要这样使用。
以通过迭代器的自增或自减来访问 list 的内部元素,begin() 和 end() 分别返回的是指向第一个和最后一个元素下一个位置的双向迭代器。rbegin() 和 rend() 函数返回的双向迭代器,可以让我们逆序遍历元素。
可以对 list 使用基于范围的循环,所以当我们想要处理所有元素时,可以不使用迭代器:
std::list<std::string> names {"Jane","Jim", "Jules", "Janet"};
names.emplace_back("Ann");
std::string name("Alan");
names.emplace_back(std::move(name)); names.emplace_front("Hugo");
names.emplace(++begin(names), "Hannah");
for(const auto& name:names)
std::cout << name << std::endl;
循环变量 name 是依次指向每个 list 元素的引用,使得循环能够逐行输出各个字符串。
下面上个栗子:
#include <iostream>
#include <list>
#include <string>
#include <functional>
using std::list;
using std::string;
// List a range of elements
template<typename Iter>
void list_elements(Iter begin, Iter end)
{
while (begin != end)
std::cout << *begin++ << std::endl;
}
int main()
{
string proverb;
std::list<string> proverbs;
// Read the proverbs
std::cout << "Enter a few proverbs and enter an empty line to end:" << std::endl;
while (getline(std::cin, proverb, '\n'), !proverb.empty())
proverbs.push_front(proverb);
std::cout << "Go on, just one more:" << std::endl;
getline(std::cin, proverb, '\n');
proverbs.emplace_back(proverb);
std::cout << "The elements in the list in reverse order are:" << std::endl;
list_elements(std::rbegin(proverbs), std::rend(proverbs));
proverbs.sort(); // Sort the proverbs in ascending sequence
std::cout << "\nYour proverbs in ascending sequence are:" << std::endl;
list_elements(std::begin(proverbs), std::end(proverbs));
proverbs.sort(std::greater<>()); // Sort the proverbs in descending sequence
std::cout << "\nYour proverbs in descending sequence:" << std::endl;
list_elements(std::begin(proverbs), std::end(proverbs));
return 0;
}