C++ STL应用与实现7: 如何使用std::forward_list 单链表 (since C++11)

Posted elloop

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ STL应用与实现7: 如何使用std::forward_list 单链表 (since C++11)相关的知识,希望对你有一定的参考价值。

本系列文章的目录在这里:目录. 通过目录里可以对STL总体有个大概了解

前言

本文介绍了STL中的序列式容器std::forward_list, 从它的名字就能推测出来,它是单向的,只能前进(forward). 确实如此,它其实就是对C语言风格的单链表的封装,仅提供有限的接口,相对于std::list(双向链表,并且定义很多接口)来说它节省了内存,同时又有比list更好的运行时性能;相对于自己实现的C风格的单链表(hand-written c-style linked list)而言,forward_list也有与其不相上下的效率表现.

正如标准库所说的那样:

forward_list设计的目标是, 使用forward_list不会比使用自己手写的C风格单链表有更多的时间和空间的开销, 任何有悖于这个目标的接口和特性都会被标准拒之门外”

先看个例子吧

#include <forward_list>
#include <iostream>

using namespace std;

int main()

    forward_list<int> fl = 1, 2, 3, 4, 5;

    for (const auto &elem : fl)
    
        cout << elem;
    

    return 0;

constructor

构造方式说明
forward_list<T> c默认构造函数,构造一个空的列表。
forward_list<T> c(c2)使用c2来拷贝构造c,c2中所有元素都被拷贝
forward_list<T> c = c2使用c2来拷贝构造c,c2中所有元素都被拷贝
forward_list<T> c(rValue)移动构造函数,使用右值rValue的内容来构造c。
forward_list<T> c = rValue移动构造函数,使用右值rValue的内容来构造c。
forward_list<T> c(n)构造一个含有n个元素的列表,每个元素使用默认构造函数创建
forward_list<T> c(n, elem)构造一个含有n个elem元素的列表
forward_list<T> c(begin, end)使用[begin, end)之间的元素初始化c
forward_list<T> c(initiallist)使用初始化列表initiallist来初始化c
forward_list<T> c = initiallist使用初始化列表initiallist来初始化c

非修改类接口 nonmodifying operations.

API说明
c.empty()是否是空列表
c.max_size()最大可能的容量
c1 == c2是否相等,内部对所有elems调用==
c1 != c2!(c1 == c2)
c1 < c2小于
c1 > c2c2 < c1
c1 <= c2!(c2 < c1)
c1 >= c2!(c1 < c2)

注意:因为性能方面的考虑,forward_list不提供size()接口,这正是标准所说的“不比手写的C风格单链表有更多的时间和空间上的开销”的设计目标的结果·

赋值操作 assignments

API说明
c1 = c2c2所有元素赋值给c1
c1 = rValuemove rValue所有元素给c1
c1 = initiallistinitiallist所有元素赋值给c1
c1.assign(initiallist)分配initiallist给c1
c1.assign(n, elem)分配n个elem
c1.assign(begin, end)分配[begin, end)给c1
c1.swap(c2)交换c1, c2
swap(c1, c2)交换c1, c2

元素访问 element access

同样是出于简单高效的设计目标,forward_list只有一个直接访问元素的接口:c.front().

c.front()返回第一个元素,这个接口内部并不会检查是不是存在第一个元素。如果调用了,但是元素不存在,那么导致未定义行为。

既然只有一个接口返回第一个元素,那么怎么来遍历所有元素呢?

有两种方式:

  • iterators.

  • range-based for loops. (其本质依然是使用iterators)

迭代器函数 iterator functions.

API说明
c.begin()指向第一个元素的迭代器
c.end()最后一个元素的下一个位置
c.cbegin()const begin()
c.cend()const end()
c.before_begin()第一个元素前一个位置
c.cbefore_begin()const before_begin()

使用迭代器是遍历forward_list所有元素的方式之一, 这一点和其他类型的容器类似,不必细说,待会看代码的例子就明了了。

注意: 对end()和before_begin()迭代器的直接操作都是未定义的行为,有可能造成运行时错误:

*c.before_begin();      // undefined behaviour
*c.end();               // undefined behaviour

一般没人会像上面这样写,但是有时候连我自己都不知道我会对这两个位置进行解引用,比如我从一个函数的返回值得到了一个pos,然后我没有对pos进行合法性检查就直接*pos。

插入、移除元素 modifying operations

API说明
c.push_front()在开头插入元素
c.pop_front()删掉开头元素
c.insert_after(pos, elem)在pos之后插入元素, 返回新元素的位置
c.insert_after(pos, n, elem)在pos之后插入n个元素elem, 返回第一个新元素的位置
c.insert_after(pos, begin, end)在pos之后插入元素[begin, end), 返回第一个新元素的位置
c.insert_after(pos, initiallist)在pos之后插入initiallist, 返回第一个新元素的位置
c.emplace_after(pos, args...)在pos之后插入args…元素,返回第一个新元素的位置
c.emplace_front(args...)在开头插入元素args…, 无返回值
c.erase_after(pos)删掉pos后面那个元素
c.erase_after(begin, end)删掉(begin, end)之间的元素
c.remove(val)移除所有值为val的元素
c.remove_if(op)删掉所使得op(elem)为true的元素
c.resize(n)调整个数为n,如果变大了,那多出来的用默认构造函数来创建
c.resize(n, elem)调整个数为n,如果变大了,那多出来的用elem的来拷贝构造
c.clear()清除所有元素

手写过单链表这种数据结构的人都会知道,对单链表的插入和删除操作都需要指定一个位置,并且这个位置要是被改动元素之前的位置,这是因为单链表不能够“回头”,比如我要删除第N个元素,我不仅要删掉第N个,还需要修改第N-1个元素,使它的跟第N+1个连在一起。

这个道理在std::forward_list上的体现就是,插入和删除forward_list的操作跟其他的标准容器的接口不太一样:

  • 1. forward_list的接口总是需要一个“位置”参数pos, 要处理的元素就是在pos的后面;

  • 2. forward_list的接口总是以 _after结尾,意思是在某个参数之后进行操作。

在实际的应用中,我通常要先搜索要被处理的元素,比如,在一个列表里,我要删除第一个偶数, 如果我这样来写:

//----------------------- find and modify  ----------------------
RUN_GTEST(ForwardListTest, FindAndModify, @);

forward_list<int> fl =  1, 2, 3 ;
printContainer(fl, "fl: ");                 // fl: 1 2 3

// 找偶数用
auto isEven = [](int i) -> bool

    return (i % 2 == 0);
;

auto deletePos = find_if(fl.begin(), fl.end(), isEven);

// 接下来怎么写,不行了,我已经走过头了,deletePos虽然是我要删除元素的位置,
// 但是我要调用erase_after, 那么就要把deletePos前面的位置传给erase_after.

// 因此为了要记住deletePos之前的位置,我得这么写:

// pos是要删除的元素,posBefore是pos的前一个元素
forward_list<int>::iterator posBefore, pos;

posBefore = fl.before_begin();

for (pos = fl.begin(); pos != fl.end(); posBefore = pos, ++pos)

    if (*pos % 2 == 0)
    
        // 找到了
        break;
    


if (pos != fl.end())

    // 把pos的前一个位置传给erase
    fl.erase_after(posBefore);
    printContainer(fl, "fl: ");                 // fl: 1 3



// 这个过程是不是有些麻烦,要定义两个迭代器位置,
// 其实可以使用迭代器辅助函数: next(pos, n)来简化, 
// next的作用很简单,返回pos的下n个位置,n默认为1.

// 下面用next来完成查找过程:

// 把list恢复成 1 2 3
fl.insert_after(posBefore, 2);
printContainer(fl, "fl: ");                     // fl: 1 2 3

posBefore = fl.before_begin();
for (; next(posBefore) != fl.end(); ++posBefore)

    int i = *next(posBefore);
    if (isEven(i))
    
        // 找到了
        break;
    



if (next(posBefore) != fl.end()) 

    fl.erase_after(posBefore);
    printContainer(fl, "fl: ");              // fl: 1 3

使用next来找要删除或插入的位置的代码还勉强能够接受,如果要频繁重复这操作,那建议像标准库那本书里自己定义两个算法:

  • find_before(begin, end, val) : 返回[begin, end)之间,elem == val的元素的前一个位置

  • find_before_if(begin, end, op) : 返回[begin, end)之间,op(elem) == true 的元素的前一个位置

本文最后会给出这俩算法的实现。

连接(splice)和变序类操作

API说明
c.unique()删除连续相同的元素
c.unique(op)删除连续使得op(elem) == true的元素
dst.splice_after(dstPos, src)把src里的全部元素移动到dst中的dstPos之后
dst.splice_after(dstPos, src, srcPos)把src里的srcPos后面的一个元素移动到dst中的dstPos之后
dst.splice_after(dstPos, src, srcBegin, srcEnd)把src里(srcBegin, srcEnd)之间的元素移动到dst中的dstPos之后
c.sort()使用默认的<来给c中的元素排序
c.sort(op)使用op来给c中的元素排序
c.merge(c2)移动c2中的元素到c中,假定二者原来都已序,那么移动之后c仍然有序
c.merge(c2, op)移动c2中的元素到c中,假定二者原来都按op已序,那么移动之后c仍然按op有序
c.reverse()所有元素反序

注意:splice_after中涉及到的dst, src可以是同一个forward_list, 此时,这个操作就是在列表内部来移动元素。splice_after的位置参数不要使用end(), 这将导致未定义行为。

下面给出《c++标准库》上关于splice_after的一个例子,书中的代码有一句是有错误的,下面给出正确的代码:

//----------------------- splice ----------------------
RUN_GTEST(ForwardListTest, SpliceTest, @);

forward_list<int> fl1 =  1, 2, 3, 4, 5 ;
forward_list<int> fl2 =  97, 98, 99 ;

printContainer(fl1, "fl1: ");       // 1 2 3 4 5
printContainer(fl2, "fl2: ");       // 97 98 99

auto pos1 = fl1.before_begin();                 
for ( ; next(pos1) != fl1.end(); ++pos1 )

    if ( *next(pos1) == 3 )         // find 3, pos1 is position before 3.
    
        break;
    


auto pos2 = fl2.before_begin();
for ( ; next(pos2) != fl2.end(); ++pos2 )

    if ( *next(pos2) == 99 )        // find 99, pos2 is position before 99.
    
        break;
    



// 书中原始代码为:
// fl1.splice_after(pos2, fl2,         // destination
//                  pos1);             // source

// 可能是这个函数之前的作用和现在标准发布的用法不一致,
// 作者的代码用反了source和destination. 我运行这段代码程序报错了。
// 结合书中对splice_after函数的解释,正确的用法 应该是下面这样:

// 把fl1中pos1后面的一个元素,移动到fl2中pos2后面。
fl2.splice_after(pos2,              // 目的地
                 fl1, pos1);        // 源

// 要移动到fl2中,那么fl2是目标,所以要用fl2来调用splice_after.
// pos2是fl2中的位置,它是目的地,应该作为第一个参数;
// fl1是源,作为第二个参数;
// pos1则是源fl1中的具体位置,作为第三个参数。


printContainer(fl1, "fl1: ");       // 1 2 4 5
printContainer(fl2, "fl2: ");       // 97 98 3 99
END_TEST;

find_beforefind_before_if的实现

// 返回(begin, end)之间,值等于val的元素的前一个位置pos. 开区间,不包括beginend
template <typename ForwardIter, typename T>
ForwardIter find_before(ForwardIter beg, ForwardIter end, const T &val)

    auto posBefore = beg;
    while (beg != end)
    
        if (*beg == val) 
        
            return posBefore;
        
        posBefore = beg;
        ++beg;
    
    return end;


// 返回(begin, end)之间,满足op(elem) == true的元素的前一个位置pos. 注意是开区间,不包括beginend
template <typename ForwardIter, typename Pred>
ForwardIter find_before_if(ForwardIter beg, ForwardIter end, Pred op)

    auto posBefore = beg;
    ++beg;
    while (beg != end)
    
        if (op(*beg))
        
            return posBefore;
        
        posBefore = beg;
        ++beg;
    
    return end;

使用示例:

//----------------------- test find_before  ----------------------
RUN_GTEST(ForwardListTest, FindBefore, @);

forward_list<int> fl =  1, 2, 3 ;
printContainer(fl, "original fl: ");                 // 1 2 3

//------------------ find_before -----------------------
auto posBefore2 = find_before(fl.begin(), fl.end(), 2);
if (posBefore2 != fl.end()) 

    fl.erase_after(posBefore2);
    printContainer(fl, "delete 2: ");                 // 1 3



//------------------ find_before_if -----------------------
auto isEven = [](int i) -> bool

    return (i % 2 == 0);
;

// recover fl contents to 1 2 3.
fl.clear();
fl.insert_after(fl.before_begin(), 1, 2, 3);
printContainer(fl, "original fl: ");                 // 1 2 3


posBefore2 = find_before_if(fl.begin(), fl.end(), isEven);
if (posBefore2 != fl.end()) 

    fl.erase_after(posBefore2);
    printContainer(fl, "delete first even: ");      // 1 3


END_TEST;

源码和参考链接


作者水平有限,对相关知识的理解和总结难免有错误,还望给予指正,非常感谢!

欢迎访问github博客,与本站同步更新

以上是关于C++ STL应用与实现7: 如何使用std::forward_list 单链表 (since C++11)的主要内容,如果未能解决你的问题,请参考以下文章

C++ STL应用与实现95: 如何使用Allocator

C++ STL应用与实现72: 标准库里的堆--如何使用标准库的heap算法

C++ STL应用与实现86: 如何使用std::accumulate

C++ STL应用与实现17: 如何使用迭代器辅助函数

C++ STL应用与实现62: 如何使用std::next_permutation

C++ STL应用与实现62: 如何使用std::next_permutation