STL的list读书笔记

来源:互联网 发布:不可思议的植物 知乎 编辑:程序博客网 时间:2024/06/05 16:25

前言

循循渐进,今天来记录一下对于list的读书笔记。一般我们学生期间碰到使用链表的场景其实比较少的,因为有了vector后链表list相对于数组的主要优势可以说只有O(1)时间复杂度的插入和删除了,但如果是要求空间尽可能利用无多余的空间场景的话,还是会使用到链表结构。

而实际使用中往往采取是把链表和其他结构相结合组成一种混杂的数据结构来使用的方法,如:前面的STL的空间配置器中的自由链表,哈希表中解决冲突的链地址法中用到的结构。因为得益于STL的空间配置器的机制,使用list引起系统内存碎片化的几率很小。

STL中的list的设计其实并没有很多特别的(令人感到精彩的)技术(但也不是没有),和常规的设计一样,list和list的node是分开成两种结构来设计的,一个list对象中只需要包含一个node作为链表的头就可以有效的抽象出一个链表结构了。

STL中的链表结构使用的是环状双向链表,显然易见这样设计的好处是如果我们要实现单向链表,无环链表这些简单的结构,只需要封装多一层,限制下访问方法,就可以重用list的代码了。而且本身双向链表使用起来灵活性也比普通的链表要高。

list的内存模型


节点结构
这里写图片描述

因为要双向访问,所以需要多一个指针来指向前驱。

链表的结构
书中有一图是存在错误的,见下图

这里写图片描述

节点的next指针应该是指向下一节点的prev指针,而不是下一节点的next,这个要注意,下图的才是正确的结构,当然也是来自书中的图。

结构

这里写图片描述

源码摘录

这里写图片描述

list实现的链表是带头指针的链表,一条链表带头的意思是指有一个不存放(可以存储,只是不用)元素的节点做头,头的下一个节点才是链表真正的开始;无头链表则代表的是,链表中的所有节点都存放元素。

巧妙之处

而STL里面使用带头节点的链表是有原因的,如果让尾端指向一个空节点(此处不是指向NULL),就能够符合STL区间访问中左闭右开的规范,可以发这个空节点恰好可以使用头节点node来担当,所以STL使用环状的链表是一个很巧妙的设计。

下面来代码验证一下

list<int>  tlist;    for(int i=0;i<10;i++)//尾插0~9  0 1 2 3 4 5 6 7 8 9       {        tlist.push_back(i);    }    for(int i=10;i<20;i++)//头插10~19  19 18 17 16 15 14 13 12 11 10     {        tlist.push_front(i);    }    list<int>::iterator itl;    for(itl=tlist.begin();itl!=tlist.end();itl++)    {        cout<<*itl<<" ";    //list输出:  19 18 17 16 15 14 13 12 11 10 0 1 2 3 4 5 6 7 8 9       }   //------------------分割---------------------    list<int>::iterator it_begin,it_end;     it_begin = tlist.begin();    it_end = tlist.end();    cout<<endl;    cout<<"it_begin地址:"<<&(*it_begin)<<"  值:"<<*it_begin<<endl;    cout<<"it_end地址: "<<&(*it_end)<<" 值:"<<*it_end<<endl;    //末尾地址往前进1     ++it_end;    cout<<"it_end的后继地址:"<<&(*it_end)<<" 值:"<<*it_end<<endl;

这里写图片描述

end不仅可以访问,而且后继就是begin。和书上的说法一样

按这样的设计初始的时候,node节点的情况就如下图


这里写图片描述

需要顺便说明一点,list不同于vector,因为是对节点间的存放地址没有连续的要求,所以,删除一个节点erase()的时候,不仅会析构对象还会回收空间,同时返回下一个节点的迭代器。而且对list进行操作不会出现空间重新配置,移动数据的操作,因此迭代器不会出现操作过后失效的问题。

list里面实现了一个transfer()方法可以将某个范围内的元素迁移到某个特定位置,实现上就是各种指针操作,《STL源码剖析》中给出的代码阅读性不高,相比之下SGI_STL的源码看起来比较人性化,下面附上源码截图和书上的示意图:

这里写图片描述

这里写图片描述

在这个方法的基础上进一步封装了以下方法

  • merge(list&x) 合并两个递增链表,看过剑指offer的人是不是很熟悉呢?
  • splice()结合链表,这个方法重载的数量比较多,这里不详述。
  • sort()没错,就是排序,而且而且是基于归并的排序实现,这里挖个坑,list的sort()实现比较牛逼(难懂),有时间在开一篇分析。STL内置的sort()

要求输入迭代器具有随机访问的性质,而list的迭代器不支持随机访问,所以list有自己特有的sort()方法。这里需要深挖一下sort()是要求对象之间的比较关系是符合严格弱序的(strict weak ordering)。
关于严格弱序可以参考这篇博文:C++STL中的strict weak ordering

然后我在引用一下C++primer中对严格弱序的讲解,后面在map中我会在提及一次
对于一个集合S中的元素

  • 如果 k1 < k2,则不存在k2 > k1
  • 如果 k1是属于S的,则不存在k1 < k1
  • 如果有 k1 < k2,k2 < k3,则一定k1 < k3,存在传递性
  • 如果有 k1 !< k2,k2 !< k1,则k1一定等于k2
  • 如果k1=k2,k2=k3,则k1一定等于k3

  • 符合上述性质的<关系,才是“行为正常的”。

list中需要注意的点

很多人习惯判断一个容器是否为空都喜欢用size()方法,而STL的各大容器都提供了size()方法,list中的size()则需要注意了,这一个复杂度为O(n)的操作,如果需要频繁的判断一个list是否为空的话建议还是使用O(1)时间返回的empty()方法。
下面是源码,最终是通过调用一个__distance()方法来统计的。
这里写图片描述

至于为什么要这样设计,下面链接的博主做了说明了。
list的size为何要设计成O(N)?

好了list篇就记录到此了。

原创粉丝点击