C++ STL库复习(6)list

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 容器内部添加元素。

  1. 在迭代器指定的位置插入一个新的元素:
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 容器的插入过程相比,这个要快很多,并且无论在哪里插入元素,花费的时间都相同。

  1. 可以在给定位置插入几个相同元素的副本:
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;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值