首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >探索C/C++的奥秘之list

探索C/C++的奥秘之list

作者头像
用户11290648
发布2024-11-21 16:21:02
发布2024-11-21 16:21:02
1910
举报
文章被收录于专栏:学习学习

list和我们之前讲的东西都一样,list第二个参数是一个空间配置器,是一个内存池, 底层是一个带头双向循环列表。list可以重载[],但是效率太低了。

list的遍历不能使用下标+[],因为它的空间不是连续的,可以使用迭代器,也可以使用范围for。 

#include<iostream> #include<list> using namespace std; int main() {     list<int> lt;     lt.push_back(1);     lt.push_back(2);     lt.push_back(3);     lt.push_back(4);     list<int>::iterator it = lt.begin();     while (it != lt.end())     {         cout << *it << " ";         it++;     }     cout << endl;     for (auto e : lt)     {         cout << e << " ";     }     cout << endl;     return 0; }

1. list的介绍及使用

1.1 list的介绍

1. list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。

2. list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。

3. list与forward_list非常相似:最主要的不同在于forward_list是单链表,只能朝前迭代,已让其更简单高效。

4. 与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率 更好。

5. 与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list 的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间 开销;list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大list来说这 可能是一个重要的因素)

1.2 list的使用

list中的接口比较多,此处类似,只需要掌握如何正确的使用,然后再去深入研究背后的原理,已达到可扩展的能力。以下为list中一些常见的重要接口。list的头插和头删复杂度都为O(1)。

从string之后,position都变了,在string的部分都是用下标去插入,现在的位置都是迭代器,但是和vector还是有点区别的。比如下面的在第五个位置插入数据: 

int main() {     list<int> lt;     lt.push_back(1);     lt.push_back(2);     lt.push_back(3);     lt.push_back(4);     lt.push_front(10);     lt.push_front(20);     //如果要在第五个位置插入数据     //下面这种方法是不行的     //lt.insert(lt.begin() + 5, 10);     //因为vector是一个连续的物理空间,是支持+的,但是插入的代价比较大,数据要往后挪动。     //list和vector各有各的优缺点。     //list的物理空间不是连续的,可以支持+,但是代价比较大,库立面没有直接支持。     //只有一种方法     auto it = lt.begin();     for (size_t i = 0; i < 5; i++)     {         ++it;     }     lt.insert(it, 100);     for (auto e : lt)     {         cout << e << " ";     }     cout << endl;     return 0; }

这是vector和list的第一个差别,这种差别是迭代器的差别导致的,list的插入代价是很低的,把前后位置的指向关系改变一下就可以了,排序都写到算法里面去了。

还有一种插入的场景,就是找到一个值,在它的前面插入一个数据。list和vector都没有提供find接口。

对于vector来说用小于没有问题,但对于list来讲,迭代器中end()节点的地址不一定比begin()大, end()的节点是有效数据的下一个位置,也就是哨兵位。

list中的insert接口不存在迭代器失效的问题,比如我要在这个位置插入接点,没有扩容的问题,没有野指针的问题,没有位置改变的问题,因为不需要挪动数据。vector在这个位置插入之前有两大问题,第一大问题就是扩容,第二大问题就是插入这个数据之后,位置往后挪,它的位置意义都变了。

erase节点存在迭代器失效的问题,因为节点都没了。

算法中的find,找到就返回这个节点,找不到就返回end节点。

int main() {     list<int> lt;     lt.push_back(1);     lt.push_back(2);     lt.push_back(3);     lt.push_back(4);     lt.push_front(10);     lt.push_front(20);     auto it = lt.begin();     it = find(lt.begin(), lt.end(), 3);     if (it != lt.end())     {         lt.insert(it, 3);         *it *= 100;     }     for (auto e : lt)     {         cout << e << " ";     }     cout << endl;     it = find(lt.begin(), lt.end(), 3);     if (it != lt.end())     {         lt.insert(it, 30);         *it *= 100;     }     for (auto e : lt)     {         cout << e << " ";     }     cout << endl;     return 0; } 

如果是erase的话,程序就会崩溃:

int main() {     list<int> lt;     lt.push_back(1);     lt.push_back(2);     lt.push_back(3);     lt.push_back(4);     lt.push_front(10);     lt.push_front(20);     auto it = lt.begin();     it = find(lt.begin(), lt.end(), 3);     if (it != lt.end())     {         lt.insert(it, 3);         *it *= 100;     }     for (auto e : lt)     {         cout << e << " ";     }     cout << endl;     it = find(lt.begin(), lt.end(), 3);     if (it != lt.end())     {         lt.erase(it);         *it *= 100;     }     for (auto e : lt)     {         cout << e << " ";     }     cout << endl;     return 0; } 

如果要用erase删除list中的偶数:

int main() {     list<int> lt;     lt.push_back(1);     lt.push_back(2);     lt.push_back(3);     lt.push_back(4);     lt.push_front(10);     lt.push_front(20);     auto it = lt.begin();     //如果要用erase删除list中的偶数:     while (it != lt.end())     {         if (*it % 2 == 0)         {             it = lt.erase(it);         }         else         {             ++it;         }     }     for (auto e : lt)     {         cout << e << " ";     }     cout << endl;     return 0; }

 swap就是两个链表直接交换。

int main() {     list<int> lt1;     lt1.push_back(1);     lt1.push_back(2);     lt1.push_back(3);     lt1.push_back(4);     list<int> lt2;     lt2.push_back(10);     lt2.push_back(20);     lt2.push_back(30);     lt2.push_back(40);     for (auto e : lt1)     {         cout << e << " ";     }     cout << endl;     for (auto e : lt2)     {         cout << e << " ";     }     cout << endl;     lt1.swap(lt2);     for (auto e : lt1)     {         cout << e << " ";     }     cout << endl;     for (auto e : lt2)     {         cout << e << " ";     }     cout << endl;     return 0; }

容器都是通过迭代器区间去访问的,比如这有一个容器,想在这一段查找,给这个容器的区间查找这个val就可以了。

clear就是把我们的节点删掉等等这些情况。

list中sort的价值大不大,库里面也有一个sort,list中也提供了一个sort,有啥意义呢?编译报错了,有时候掉不调到库里面去了,就看这个库是谁的归属,那就是哪儿出错了,sort这个库用这个迭代器我们不能用,因为sort这个库里面用到一个

,就一个点我们就知道了,sort是快排,快排不能用这个东西,因为快排要做过参数取中,参数取中选其中一个值的时候,选中间那个值或者选另外一个值的时候,我直接要选那个位置,快排不适合,就是那个链表没办法适应这个场景。

所以大家仔细再看,如果我们单看算法,这些算法的名字有一些差异,他这个是函数模板,但是他在这个名字上面很有讲究,它的名字就暗示了应该传什么迭代器,这里就要迁移出一个知识讲一讲,然后再结合这这个说,迭代器从他的功能的角度是会分类的,从功能上来说,分为单向、双向、随机。功能上是什么呢?单向就只能++,双向可以++也可以--,随机是可以++、--、也可以+,也可以-。

 谁的迭代器就是典型的单向呢?单链表、哈希表。谁是双向迭代器呢?双向链表,其实也就是我们的list,还有map、set,也就是树。谁是随机迭代器呢?vector、string、双端队列deque。它这的名字就暗示了你用哪种算法,reverse就适合用双向,find就适合用单向,sort就适合用随机。这个地方对迭代器是一个隐式的分类,可以认为它是一个性质进行划分,这个性质跟容器的底层结构有关系。

 谁可以调用这个sort算法呢,这个sort底层是快排,决定了他要用这个随机迭代器,随机是可以吊的,链表就不可以调,链表这里调就报错了,用这个算法就要看容器的迭代器到底是哪一种,能不能适应,我怎么知道我的容器是哪一种呢?这里是有说明的,它的迭代器的内容是什么,vector和string可以用这个逆置,随机迭代器可以认为是一个特殊的双向迭代器,随机迭代器满足双向的要求,满足单向的要求,

,如果写这个,单向、双向、随机都可以用,写随机的就随机可以用,写双向的就双向、随机可以用,写单向的就单向、双向、随机都可以用。

set底层就是更复杂的迭代器,也就是树了,forward_list是单向迭代器,

 排序应该用vector,不应该用list,vector的效率远高于list,

 数据量越大,差距越大一些,list排序终究还是要慢不少。如果我们真的要数据排序,我们不应该用链表,链表访问数据相比vector毕竟还是慢,list底层用的是归并算法。所以说list的sort意义不大。

void test_op() {     srand(time(0));     const int N = 1000000;     vector<int> v;     v.reserve(N);     list<int> lt1;     //list<int> lt2;     for (int i = 0; i < N; ++i)     {         auto e = rand();         v.push_back(e);         lt1.push_back(e);     }     // 排序     int begin1 = clock();     sort(v.begin(), v.end());     int end1 = clock();     int begin2 = clock();     lt1.sort();     int end2 = clock();     printf("vector sort:%d\n", end1 - begin1);     printf("list sort:%d\n", end2 - begin2); } int main() {     test_op();     return 0; }

list的作用是当排序值比较小的时候,list排序才有价值。

void test_op() {     srand(time(0));     const int N = 100000;     vector<int> v;     v.reserve(N);     list<int> lt1;     list<int> lt2;     for (int i = 0; i < N; ++i)     {         auto e = rand();         lt2.push_back(e);         lt1.push_back(e);     }     // 拷贝到vector排序,排完以后再拷贝回来     int begin1 = clock();     // 先拷贝到vector     for (auto e : lt1)     {         v.push_back(e);     }     // 排序     sort(v.begin(), v.end());     // 拷贝回去     size_t i = 0;     for (auto& e : lt1)     {         e = v[i++];     }     int end1 = clock();     int begin2 = clock();     lt2.sort();     int end2 = clock();     printf("vector sort:%d\n", end1 - begin1);     printf("list sort:%d\n", end2 - begin2); } int main() {     test_op();     return 0; }

我们可以区分迭代器的类型,区分迭代器的类型有点复杂,叫做迭代器的萃取,就可以分辨出迭代器的类型。

merge就是连个链表可以直接进行归并,归并有一个前提就是先有序,两个链表要先sort再merge,这里还有另外一个东西叫仿函数,比较的仿函数的概念,仿函数在优先级队列再讲。

这个接口的本质是去重,去重也是有前提的,也是要先排序, 如果不排序去重的话效率是很低的。

remove就是find加erase,remove就是直接可以删。remove唯一要注意的是如果这个值不存在会不会报错?

void text_x() {     int myints[] = { 17,89,7,14 };     std::list<int> mylist(myints, myints + 4);     mylist.remove(890);     for (auto e : mylist)     {         cout << e << " ";     }     cout << endl; } int main() {     text_x();     return 0; }

相当于find,如果找到就删除了,没找到就啥也不干。 

remove_if 也涉及到一个仿函数的问题。

这个接口可以把一个链表的内容转移到另一个,它的转移是直接把节点拿走,就比如说有一个a链表,有一个b链表,直接把a链表的节点取下来直接插入到b链表,相当于a链表删除了数据,b链表插入了数据,严格来说就是发生了转移。也可以转移到自己身上,但是别把范围区间重叠了。

int main() {     int myints[] = { 17,89,7,14 };     std::list<int> mylist1, mylist2;     std::list<int>::iterator it;     for (int i = 1; i <= 4; ++i)         mylist1.push_back(i);      // mylist1: 1 2 3 4     for (int i = 1; i <= 3; ++i)         mylist2.push_back(i * 10);   // mylist2: 10 20 30     for (auto e : mylist1)     {         cout << e << " ";     }     cout << endl;     for (auto e : mylist2)     {         cout << e << " ";     }     cout << endl << endl;     it = mylist1.begin();     ++it;     // points to 2     //全部转移     //mylist1.splice(it, mylist2);     //转移一个     //mylist1.splice(it, mylist2, ++mylist2.begin());     //部分转移     //mylist1.splice(it, mylist2, ++mylist2.begin(), mylist2.end());     //把后面的数据转移到第一个的前面     mylist1.splice(mylist1.begin(), mylist1, ++mylist1.begin(), mylist1.end());     for (auto e : mylist1)     {         cout << e << " ";     }     cout << endl;     for (auto e : mylist2)     {         cout << e << " ";     }     cout << endl;     return 0; }

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-11-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. list的介绍及使用
    • 1.1 list的介绍
    • 1.2 list的使用
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档