• [ C++ ] STL_list 使用及其模拟实现


    本篇博客学习有关STL库中list的使用及其重要接口的模拟实现。

    目录

    1.list的介绍及使用

    1.1 list的介绍

    1.2 list的使用

    2.list的迭代器

    3.list的构造

    4. list capacity

    5. list常用接口

    5.1 insert

    5.2 push_front 

    5.3  push_back

    5.4 erase

    5.5  pop_front

    5.6 pop_back

    5.7 swap

    5.8 clear

    6.list的迭代器失效

    7. list 和 vector 的对比


    1.list的介绍及使用

    1.1 list的介绍

    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中的接口比较多,我们只需要熟悉使用常用的接口以及深入研究其背后的原理即可。

     

    2.list的迭代器

    list的迭代器是一个自定义类型的指针,该指针指向list中的某个节点

    List 的迭代器
    迭代器有两种实现方式,具体应根据容器底层数据结构实现:
    1. 原生态指针,比如: vector
    2. 将原生态指针进行封装,因迭代器使用形式与指针完全相同,因此在自定义的类中必须实现以下 方法:
            1. 指针可以解引用,迭代器的类中必须重载 operator*()
            2. 指针可以通过 -> 访问其所指空间成员,迭代器类中必须重载 oprator->()
            3. 指针可以++向后移动,迭代器类中必须重载operator++()与operator++(int)             
                至于operator--()/operator--(int)释放需要重载,根据具体的结构来抉择,双向链表
                可以向前 移动,所以需要重载,如果是 forward_list 就不需要重载 --
            4. 迭代器需要进行是否相等的比较,因此还需要重载 operator==() operator!=()

    我们首先实现一个简单的list的iterator

    函数声明接口说明
    begin+ end
    返回第一个元素的迭代器 + 返回最后一个元素下一个位置的迭代器
    rbegin+ rend
    返回第一个元素的 reverse_iterator, end 位置 返回最后一个元素下一个位置的 reverse_iterator, begin 位置

    注意:

    1、begin与end为正向迭代器,对迭代器执行++操作,迭代器向后移动。

    2、rebegin(end)与rend(begin)为反向迭代器,对迭代器执行++操作,迭代器向前移动。

     

     

    1. // T T& T*
    2. template<class T, class Ref, class Ptr>
    3. struct __list_iterator
    4. {
    5. typedef list_node Node;
    6. typedef __list_iterator self;
    7. Node* _node;
    8. __list_iterator(Node* node)
    9. :_node(node)
    10. {}
    11. Ref operator*()
    12. {
    13. return _node->_data;
    14. }
    15. Ptr operator->()
    16. {
    17. //返回的是节点数据的地址 AA*
    18. return &_node->_data;
    19. }
    20. self& operator++()
    21. {
    22. _node = _node->_next;
    23. return *this;
    24. }
    25. //后置++
    26. self operator++(int)
    27. {
    28. self tmp(*this);
    29. _node = _node->_next;
    30. return tmp;
    31. }
    32. self& operator--()
    33. {
    34. _node = _node->_prev;
    35. return *this;
    36. }
    37. //后置--
    38. self operator--(int)
    39. {
    40. self tmp(*this);
    41. _node = _node->_prev;
    42. return tmp;
    43. }
    44. bool operator!=(const self& it)
    45. {
    46. return _node != it._node;
    47. }
    48. bool operator==(const self& it)
    49. {
    50. return _node == it._node;
    51. }
    52. };

    3.list的构造

    构造函数(constructor)接口说明
    list()
    构造空的 list
    list (size_type n, const value_type& val = value_type())
    构造的 list 中包含 n 个值为 val 的元素
    list (const list& x)
    拷贝构造函数
    list (InputIterator first, InputIterator last)
    [first, last) 区间中的元素构造 list

     

    构造空的list:

    1. list()
    2. {
    3. _head = new Node();
    4. _head->_next = _head;
    5. _head->_prev = _head;
    6. }

    拷贝构造函数:

    1. list(const list& lt)
    2. {
    3. _head = new Node();
    4. _head->_next = _head;
    5. _head->_prev = _head;
    6. for (auto e : lt)
    7. {
    8. push_back(e);
    9. }
    10. }

    用[first, last)区间中的元素构造list:

    1. template <class InputIterator>
    2. list(InputIterator first, InputIterator last)
    3. {
    4. _head = new Node();
    5. _head->_next = _head;
    6. _head->_prev = _head;
    7. while (first != last)
    8. {
    9. push_back(*first);
    10. ++first;
    11. }
    12. }

    4. list capacity

    函数声明接口说明
    empty
    检测 list 是否为空,是返回 true ,否则返回 false
    size
    返回 list 中有效节点的个数

     

    5. list常用接口

     

    函数声明
    接口说明
    push_front
    list 首元素前插入值为 val 的元素

    pop_front

    删除 list 中第一个元素
    push_back
    list 尾部插入值为 val 的元素
    pop_back
    删除 list 中最后一个元素
    insert
    list position 位置中插入值为 val 的元素
    erase
    删除 list position 位置的元素
    swap
    交换两个 list 中的元素
    clear
    清空 list 中的有效元素

    5.1 insert

    方法:

    1、首先创建一个新节点newnode,赋值为x。

    2、创建两个指针,cur指向pos位置的节点,prev指向pos位置之前的节点

    3、prev newnode cur 三个指针依次连接,返回newnode的迭代器。
     

     

    代码实现:

    1. //插入在pos位置之前
    2. iterator insert(iterator pos, const T& x)
    3. {
    4. Node* newNode = new Node(x);
    5. Node* cur = pos._node;
    6. Node* prev = cur->_prev;
    7. //prev newnode cur
    8. prev->_next = newNode;
    9. newNode->_prev = prev;
    10. newNode->_next = cur;
    11. cur->_prev = newNode;
    12. return iterator(newNode);
    13. }

    5.2 push_front 

    方法:

    1、我们复用insert即可,头插就是在第一个元素前插入一个元素,因此我们只需要insert(begin(),x)即可

    代码实现:

    1. void push_front(const T& x)
    2. {
    3. insert(begin(), x);
    4. }

    5.3  push_back

    方法:

    1、尾插,就是在最后一个节点后插入新节点,我们依然可以复用insert,由于我们实现的list是双向循环链表,因此我们只需要在end前插即可

    1. void push_back(const T& x)
    2. {
    3. //Node* tail = _head->_prev;
    4. //Node* newnode = new Node(x);
    5. _head tail newnode
    6. //tail->_next = newnode;
    7. //newnode->_prev = tail;
    8. //newnode->_next = _head;
    9. //_head->_prev = newnode;
    10. insert(end(), x);
    11. }

    5.4 erase

    方法:

    1、首先创建3个指针,cur指向pos位置的节点,prev指向pos位置的前一个节点,next指向pos位置的后一个节点。

    2、让prev和next相互连接

    3、delete掉cur,返回next指针指向节点的迭代器

    代码实现:

    1. //删除后指向erase(it)之后的节点
    2. iterator erase(iterator pos)
    3. {
    4. assert(pos != end());
    5. Node* cur = pos._node;
    6. Node* prev = cur->_prev;
    7. Node* next = cur->_next;
    8. //prev next
    9. prev->_next = next;
    10. next->_prev = prev;
    11. delete cur;
    12. return iterator(next);
    13. }

     

     

    5.5  pop_front

    头删,复用erase即可

    代码实现:

    1. void pop_front()
    2. {
    3. erase(begin());
    4. }

    5.6 pop_back

    尾删,复用erase即可

    1. void pop_back()
    2. {
    3. erase(--end());
    4. }

     

    5.7 swap

    list的swap交换,只要交换两个链表的head,即可讲两个链表相互交换

    1. void swap(list& lt)
    2. {
    3. std::swap(_head, lt._head);
    4. }

    5.8 clear

    方法:

    链表的clear:我们需要将链表的每一个节点释放掉,因此我们使用迭代器时erase即可。

    1. void clear()
    2. {
    3. iterator it = begin();
    4. while (it != end())
    5. {
    6. it = erase(it);
    7. }
    8. }

    6.list的迭代器失效

    前面说过,此处大家可将迭代器暂时理解成类似于指针, 迭代器失效即迭代器所指向的节点的无效,即该节 点被删除了 。因为 list 的底层结构为带头结点的双向循环链表 ,因此 list 中进行插入时是不会导致 list 的迭代 器失效的,只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响

    我们来看这段代码:

    1. void TestListIterator1()
    2. {
    3. int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
    4. list<int> l(array, array + sizeof(array) / sizeof(array[0]));
    5. auto it = l.begin();
    6. while (it != l.end())
    7. {
    8. // erase()函数执行后,it所指向的节点已被删除,因此it无效,在下一次使用it时,必须先给其赋值
    9. l.erase(it);
    10. ++it;
    11. }
    12. }
    erase()函数执行后,it所指向的节点已被删除,因此it无效,在下一次使用it时,必须先给
    其赋值。
    代码改正:
    我们在删除it的时候,让it++即可巧妙地解决这个问题。
    1. void TestListIterator()
    2. {
    3. int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
    4. list<int> l(array, array + sizeof(array) / sizeof(array[0]));
    5. auto it = l.begin();
    6. while (it != l.end())
    7. {
    8. l.erase(it++); // it = l.erase(it);
    9. }
    10. }

    7. list 和 vector 的对比

    vector list 都是 STL 中非常重要的序列式容器,由于两个容器的底层结构不同,导致其特性以及应用场景不同,其主要不同如下:
    vector
    list
    动态顺序表,一段连续空间
    带头结点的双向循环链表
    访
    支持随机访问,访问某个元素效率 O(1)
    不支持随机访问,访问某个元素
    效率 O(N)
    任意位置插入和删除效率低,需要搬移元素,时间复杂
    度为 O(N) ,插入时有可能需要增容,增容:开辟新空
    间,拷贝元素,释放旧空间,导致效率更低
    任意位置插入和删除效率高,不 需要搬移元素,时间复杂度为
    O(1)
    底层为连续空间,不容易造成内存碎片,空间利用率
    高,缓存利用率高
    底层节点动态开辟,小节点容易
    造成内存碎片,空间利用率低,缓存利用率低
    原生态指针
    对原生态指针 ( 节点指针 ) 进行封装
    在插入元素时,要给所有的迭代器重新赋值,因为插入
    元素有可能会导致重新扩容,致使原来迭代器失效,删
    除时,当前迭代器需要重新赋值否则会失效
    插入元素不会导致迭代器失效,
    删除元素时,只会导致当前迭代
    器失效,其他迭代器不受影响
    使
    需要高效存储,支持随机访问,不关心插入删除效率
    大量插入和删除操作,不关心随
    机访问

    (本篇完)
  • 相关阅读:
    PID与ADRC
    贝壳找房基于Flink+Paimon进行全量数据实时分组排序的实践
    平衡二叉树、红黑树、B树、B+树
    leetcode75 颜色分类,荷兰国旗问题
    机器学习之SGD, Batch, and Mini Batch的简单介绍
    Java 八股文能不背吗?Java 面试都只是背答案吗?
    C语言程序设计教程(第三版)李凤霞 第十章课后习题答案
    Kibana:为地图应用选择不同的语言 - Elastic Stack 8.3
    【JAVASE】final关键字
    学算法常用刷题网站
  • 原文地址:https://blog.csdn.net/qq_58325487/article/details/126606460