当前位置: 首页 > article >正文

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

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;
}


http://www.kler.cn/a/402728.html

相关文章:

  • spring源码解析-@Autowired
  • C++ std::unique_ptr的使用及源码分析
  • python如何解压缩文件或文件夹
  • IP转发流程
  • Spring |(四)IoC/DI配置管理第三方bean
  • Linux各种并发服务器优缺点
  • 实验室资源调度系统:基于Spring Boot的创新
  • 技术总结(三十三)
  • Flink使用详解
  • 【5】GD32H7xx CAN发送及FIFO接收
  • React 远程仓库拉取项目部署,无法部署问题
  • 【VTK】MFC中使用VTK9.3
  • 1+X应急响应(网络)威胁情报分析:
  • 百度遭初创企业指控抄袭,维权还是碰瓷?
  • Github 2024-11-20C开源项目日报 Top9
  • 【Python项目】基于Python的医疗知识图谱问答系统
  • 低代码开发平台搭建思考与实战
  • ftdi_sio应用学习笔记 2 - 操作串口
  • Linux系统之lsblk命令的基本使用
  • 音视频入门基础:MPEG2-TS专题(7)——FFmpeg源码中,读取出一个transport packet数据的实现
  • 芯原科技嵌入式面试题及参考答案
  • React的诞生与发展
  • AI 大模型:重塑软件开发的魔法力量
  • 【AIGC半月报】AIGC大模型启元:2024.11(下)
  • js utils 封装
  • 快速理解python中的yield关键字