C++ STL 之 list(上)

本节我们将介绍 STL 中的 list 容器使用。



list<T> 是定义在 list 头文件中容器模板,是 T 类型对象的双向链表。
在数据结构中我们知道,
链表插入和删除元素的时间复杂度是 O\left( 1 \right) 级别的,数组中是 O\left( n \right)
链表访问的时间复杂度是 O\left( n \right),数组能随机访问,时间复杂度为 O\left( 1 \right)

这也就意味着,list 容器具有 vector 和 deque 容器所不具备的优势,它可以在常规时间内,在序列已知的任何位置插入或删除元素。
list 的缺点是无法使用位置来直接访问序列中的元素,即不能索引元素。为了访问 list 内部的一个元素,必须一个一个地遍历元素,通常从第一个元素或最后一个元素开始遍历。

双向链表的示意图如下:

image.png

list<T> 容器的每个 T 类型对象通常都被封装在一个内部节点中,该节点对象维护了两个指针,一个指向前一个节点,另一个指向下一个节点。这些指针将节点连接成一个链表。通过指针可以从任何位置的任一方向来遍历链表。第一个元素的前向指针总是为 null,因为它前面没有元素,尾部元素的后向指针也总为 null。这使我们可以检测到链表的尾部。list<T> 实例保存了头部和尾部的指针。这允许我们从两端访问链表,也允许从任一端开始按顺序检索列表中的元素。

同时我们可以使用和其他序列容器相同的方式,来获取 list 容器的迭代器。鉴于不能随机访问 list 中的元素,因此获取到的迭代器都是双向迭代器。以 list 为参数,调用 begin() 可以得到指向 list 中第一个元素的迭代器。通过调用 end(),可以得到一个指向最后一个元素下一个位置的迭代器,从而可以像其他序列容器一样,用它们来指定整个范围的元素。

也可以像其他容器一样,使用 rbegin()、rend()、crbegin()、crend()、cbegin()、cend() 来获取反向迭代器和 const 迭代器。

list 容器的初始化

list 容器初始化的用法类似于 vector 或 deque 容器。
1.使用默认的构造函数生成 list 容器,容器中没有任何元素,大小为0。

list<string> data;

2.可以生成给定元素个数的 list 容器,每一个元素都由构造函数生成。
容器内所有元素为空

list<string> data{7};

或者,容器内所有元素为 "null" 。

list<string> data{7,"null"};

3.可以用初始化列表来生成 list 容器。

list<string> data{"one","null","three","four","null","six","seven"};

4.也可以用由两个迭代器标识的一段元素来初始化它。

list<string> data {"one","null","three","four","null","six","seven"};
list<string> data_ {data};

或者

list<string> data {"one","null","three","four","null","six","seven"};
auto begin_iter = begin(data);
auto end_iter = end(data);
// 只能使用自增或自减运算符
begin_iter++;
end_iter--;
list<string> data_ {begin_iter,end_iter};

注意,list 容器的 begin() 和 end() 函数返回的都是双向迭代器,所以不能用它们加减整数。修改双向迭代器的唯一方式是使用自增或自减运算符。
当然,在上面的语句中,初始化列表中的迭代器可以代表任意容器的一段元素,而不仅仅只是 list 容器。
接下来,我们汇总下 list 容器的初始化。

#include <bits/stdc++.h>
using namespace std;

int main(){
    list<string> data_1;
    list<string> data_2(7);
    list<string> data_3(7,"null");
    list<string> data_4 {"one","null","three","four","null","six","seven"};
    //全部复制
    list<string> data_5{data_4};
    auto begin_iter = begin(data_4);
    list<string>::iterator end_iter = end(data_4);
    //注意迭代器只能自增或自减运算
    begin_iter++;
    begin_iter++;
    end_iter--;
    end_iter--;
    //部分复制
    list<string> data_6{begin_iter,end_iter};
    cout<<"data_1: "<<data_1.size()<<endl;
    for(auto d : data_1){
        cout<<d<<" ";
    }
    cout<<endl;
    cout<<"data_2: "<<data_2.size()<<endl;
    for(auto d : data_2){
        cout<<d<<" ";
    }
    cout<<endl;
    cout<<"data_3: "<<data_3.size()<<endl;
    for(auto d : data_3){
        cout<<d<<" ";
    }
    cout<<endl;
    cout<<"data_4: "<<data_4.size()<<endl;
    for(auto d : data_4){
        cout<<d<<" ";
    }
    cout<<endl;
    cout<<"data_5: "<<data_5.size()<<endl;
    for(auto d : data_5){
        cout<<d<<" ";
    }
    cout<<endl;
    cout<<"data_6: "<<data_6.size()<<endl;
    for(auto d : data_6){
        cout<<d<<" ";
    }
    cout<<endl;
    return 0;
}

结果如下:

image.png

list 容器添加元素

和 deque 容器类似,list 容器不仅能在容器尾部增加和删除元素,在头部也可以增加和删除元素。
示例如下:

list<string> data {"one","two","three","four"};
//在头部增加一个元素
data.push_front("zeros");
//删除头部的元素
data.pop_front();
//在尾部增加一个元素
data.push_back("five");
//删除尾部的元素
data.pop_back();

成员函数 emplace() ,emplace_front() 和 emplace_back() 也可以实现相同功能,而且其效率更高效。
1). emplace() 在迭代器指定的位置构造一个元素;
2). emplace_front() 在 list 的第一个元素之前构造元素;
3). emplace_back() 在 list 的尾部元素之后构造元素。
示例如下:
使用 emplace() 插入元素:

list<string> data {"one","two","three","four"};
auto iter = end(data);
//使用 emplace() 容器尾部添加一个 "AAA"
data.emplace(iter,3,'A');
iter = end(data);
//注意迭代器只能自增或自减运算
iter--;
//在 data 的倒数第一个元素前加上一个 "AAAA"
data.emplace(iter,4,'A');

使用 emplace_front() 和 emplace_back() 插入元素:

list<string> data{"one","two","three"};
//在头部插入元素 "zeros"
data.emplace_front("zeros");
//在尾部插入元素 "four"
data.emplace_back("four");

或者

list<string> data{"one","two","three"};
string word = "zeros four";
//从word中截取,从6开始的4个字符,在尾部加入
data.emplace_back(word,6,4);
//从word中截取,从0开始的5个字符,在头部加入
data.emplace_front(word,0,5);

同样地,注意迭代器只能自增或自减运算。

可以使用成员函数 insert() 在 list 容器内部添加元素。像其他序列容器一样,它有三种使用方法。

在迭代器指定的位置插入一个新的元素
list<string> data {"one","two","three","four"};
auto iter = begin(data);
iter++;
data.insert(iter,"one_two");

data 的内容为:

{"one","one_two","two","three","four"}

begin() 返回的双向迭代器自增后,指向下个元素。
list 容器插入元素不必移动现有的元素。生成新元素后,这个过程需要将 4 个指针重新设为适当的值,回忆数据结构中双向链表的插入和删除过程。
第一个元素的 next 指针指向新的元素,原来的第二个元素的 pre 指针也指向新的元素。新元素的 pre 指针指向第一个元素,next 指针指向序列之前的第二个元素。
和 vector、deque 容器的插入过程相比,这个要快很多,并且无论在哪里插入元素,花费的时间都相同。

可以在给定位置插入多个元素
list<string> data {"one","two","three","four"};
auto iter = begin(data);
//移动 iter,相对于 iter 自增三次
advance(iter,3);
//在 iter 处增加两个 "null"
data.insert(iter,2,"null");

data 的内容为:

{"one","two","three","null","null","four"}

iter 是 list<int>::iterator 类型。insert() 函数的第一个参数是用来指定插入位置的迭代器,第二个参数是被插入元素的个数,第三个参数是被重复插入的元素。
为了得到第 3 个元素,可以使用定义在 iterator 头文件中的全局函数 advance(),将迭代器增加 3。
因为 list 容器的迭代器只能自增和自减,所以迭代器不能直接加 3,advance() 函数会在循环中自增迭代器。

如果想减小迭代器的话,可以这样操作:

list<string> data {"one","two","three","four"};
auto iter = end(data);
//移动 iter,相对于 iter 自减三次
advance(iter,-3);
//在 iter 处增加两个 "null"
data.insert(iter,2,"null");

data 的内容为:

{"one","null","null","two","three","four"}
插入一段由迭代器指定的元素。
list<string> data {"one","two","three","seven"};
list<string> data_ {"null","null","four","five","six","null"};
auto begin_iter = begin(data_);
auto end_iter = end(data_);
auto iter = end(data);
//移动 iter,相当于 iter 自减 1 次
advance(iter,-1);
// 移动 begin_iter,相当于 iter 自增 2 次
advance(begin_iter,2);
// end_iter 自减 1 次
end_iter--;
data.insert(iter,begin_iter,end_iter);

insert() 的第一个参数是一个迭代器,它指向 data 的倒数第一个元素。第二和第三个参数指定了 data_ 中被插入元素的范围,因此从 data 中倒数第二个元素开始,依次插入 begin_iter 和 end_iter 两个迭代器指定范围的元素。
代码执行后,data 中的内容如下:

{"one","two","three","four","five","six","seven"}

在 insert() 中,第二和第三个参数两个迭代器若指定元素类型与 data 相同,也是可以插入的。

list<string> data {"one","two","three","seven"};
vector<string> data_ {"null","null","four","five","six","null"};
auto begin_iter = begin(data_);
auto end_iter = end(data_);
auto iter = end(data);
//移动 iter,相当于 iter 自减 1 次
advance(iter,-1);
//vector 的迭代器可以进行加减运算
begin_iter += 2;
end_iter--;
data.insert(iter,begin_iter,end_iter);

第二和第三个参数指定了 data_ 中被插入元素的范围,同时元素类似和 data 相同,因此从 data 中倒数第二个元素开始,依次插入 begin_iter 和 end_iter 两个迭代器指定范围的元素。
代码执行后,data 中的内容如下:

{"one","two","three","four","five","six","seven"}

注意,list 元素的迭代器只会在它所指向的元素被删除时才会失效。

list 容器删除元素

list 的成员函数有 clear() 和 erase(),它们的使用方式和效果和之前的序列容器相同。
使用 clear():

list<string> data {"one","two","three","seven"};
data.clear();

clear() 清除了容器内的所有元素。
使用 erase():

list<string> data {"one","two","three","four","five","null","seven"};
//移除一个元素
auto iter = end(data);
advance(iter,-2);
data.erase(iter);
//data = {"one","two","three","four","five","seven"}
//移除一段元素
list<string>::iterator begin_iter = begin(data);
auto end_iter = end(data);
advance(begin_iter,2);
advance(end_iter,-2);
data.erase(begin_iter,end_iter);

最后,data 的内容为:

{"one","two","five","seven"}

list 容器的成员函数 remove() 则移除和参数匹配的元素。例如:

list<string> data {"one","two","null","four","five","null","seven"};
// 移除所有为 "null" 的元素
data.remove("null");

data 的内容为:

{"one","two","four","five","seven"}

成员函数 remove_if() 传入一个一元断言作为参数。一元断言接受一个和元素同类型的参数或引用,返回一个布尔值。断言返回 true 的所有元素都会被移除。

list<string> data {"one","two","null","four","five","null","seven"};
// 移除所有为 "null" 的元素
data.remove_if([](string str){
    return str == "null";
});

这里的参数是一个 lambda 表达式,但也可以是一个函数对象。

成员函数 unique() 可以移除连续的重复元素,只留下其中的第一个。

list<string> data {"one","null","null","four","four","four","seven"};
data.unique();

data 的内容为:

{"one","null","four","seven"}

注意是连续的重复元素,如果是这样的

list<string> data {"one","null","null","four","null","four","four","seven"};
data.unique();

data 的内容为:

{"one","null","four", "null", "four","seven"}

因为它并不是去重,而是去重连续的重复元素,保留第一个。
如果想去重也是可以的,一种方法是先排序,后使用 unique()。

list<string> data {"one","null","null","four","null","four","four","seven"};
data.sort();
data.unique();

data 的内容为:

{"four","null","one","seven"}

unique() 的功能是很强大的,重载的 unique() 函数接受一个二元断言作为参数,有着更灵活的功能,有兴趣的可以查阅其他资料,这里就不展开了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容