• 【c++_containers】10分钟带你学会list


    前言

            链表作为一个像是用“链子”链接起来的容器,在数据的存储等方面极为便捷。虽然单链表单独在实际的应用中没用什么作用,但是当他可以结合其他结构,比如哈希桶之类的。不过今天学习的list其实是一个带头双向链表。

    言归正传,让我们看一下list的特性。

    一、list的特性

    这里我还是推荐去cplusplus上阅读英文原文档。这里我总结了几条,

    1. list 是可以在常数范围内 在任意位置进行插入和删除的序列式容器 ,并且该容器 可以前后双向迭代。
    2. list 的底层是 双向链表结构 ,双向链表中每个 元素存储在互不相关的独立节点 中,在节点中通过指针指向其前一个元素和后一个元素。
    3. list forward_list 非常相似:最主要的不同在于 forward_list是单链表 ,只能朝前迭代,已让其更简单高效。
    4. 与其他的序列式容器相比 (array vector deque) list 通常 在任意位置进行插入、移除元素的执行效率更好。
    5. 与其他序列式容器相比, list forward_list 最大的缺陷是 不支持任意位置的随机访问 ,比如:要访问 list的第6 个元素,必须从已知的位置 ( 比如头部或者尾部 ) 迭代到该位置,在这段位置上迭代需要线性的时间开销;list 还需要一些 额外的空间, 以保存每个节点的相关联信息 ( 对于存储类型较小元素的大 list 来说这可能是一个重要的因素)。

    其物理模型简化后如下图:

    二、list的基本结构

    前面我们提到list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。那么他每个小结点的结构就变得清楚明了了。

    1. template<class T>
    2. struct list_node
    3. {
    4. list_node* _prev;
    5. list_node* _next;
    6. T _val;
    7. list_node(const T& val = T())
    8. : _prev(nullptr)
    9. , _next(nullptr)
    10. , _val(val)
    11. {}
    12. };

    随后我们就可以写list的本体了

    1. template<class T>
    2. class list
    3. {
    4. typedef list_node Node;
    5. private:
    6. Node* _head;//哨兵位
    7. size_t _size;
    8. };

    加一个表示size的数据是因为list的空间是不连续的。

    三、list的基础构造

    1. void empty_init()
    2. {
    3. _head = new Node;
    4. _head->_prev = _head;
    5. _head->_next = _head;
    6. _size = 0;
    7. }
    8. list()
    9. {
    10. empty_init();
    11. }

    将empty经行再封装是因为这样的构造函数设计可以方便地创建空的list对象,并且避免了在创建list对象时必须显式地指定初始大小。同时,通过将初始化代码封装在empty_init()函数中,可以简化list类的实现,提高代码的可读性和可维护性。

    四、list的插入与删除

    与vector不同的是list的其他几种构造或多或少的依赖插入,而且哨兵位的初始化就可以继续后面的操作。当然插入和删除是list的重要点。

    我们先从尾插写起,如图当插入一个节点时我们可以先新建一个新的节点,将值存入其中。然后将末尾(_head->_prev)的_next与newnode链接,然后将new的_prev与末尾链接,最后将头节点的_prev指向newnode,newnode的_next与_head链接。

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

    而对于任意位置插入,其实和上面的逻辑相似。

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

    与次对立的是list任意位置的删除。逻辑就是他反过来。

    1. iterator erase(iterator pos)
    2. {
    3. assert(pos != end());
    4. Node* cur = pos._node;
    5. Node* prev = cur->_prev;
    6. Node* next = cur->_next;
    7. prev->_next = next;
    8. next->_prev = prev;
    9. delete cur;
    10. --_size;
    11. return next;
    12. }

    但是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. l.erase(it);
    9. ++it;
    10. }
    11. // erase()函数执行后,it所指向的节点已被删除,因此it无效,在下一次使用it时,必须先给
    12. 其赋值
    13. 改正后:
    14. void TestListIterator()
    15. {
    16. int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
    17. list<int> l(array, array+sizeof(array)/sizeof(array[0]));
    18. auto it = l.begin();
    19. while (it != l.end())
    20. {
    21. l.erase(it++); // it = l.erase(it);
    22. }
    23. }

    当这两个比较泛型的插入与删除实现后其余代码就变得简单了。

    1. //拷贝构造
    2. list(const list& lt)
    3. //list(const list& lt)
    4. {
    5. empty_init();
    6. for (auto& e : lt)
    7. {
    8. push_back(e);
    9. }
    10. }
    11. //析构函数
    12. ~list()
    13. {
    14. clear();
    15. delete _head;
    16. _head = nullptr;
    17. }
    18. //头插
    19. void push_front(const T& x)
    20. {
    21. insert(begin(), x);
    22. }
    23. //尾删
    24. void pop_back()
    25. {
    26. erase(--end());
    27. }
    28. //头删
    29. void pop_front()
    30. {
    31. erase(begin());
    32. }
    33. //清空
    34. void clear()
    35. {
    36. iterator it = begin();
    37. while (it != end())
    38. {
    39. it = erase(it);
    40. }
    41. _size = 0;
    42. }

    五、list的迭代器

    list的迭代器我们要实现的主要就是他的++与--问题,而++就是返回当前位置的_next, --就是返回当前位置的_prev。

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

    _list_iterator类模板的三个类型参数分别为T(元素类型)、Ref(引用类型)和Ptr(指针类型)。这个类包含一个成员变量_node,它是一个指向list_node对象的指针,用于存储当前迭代器所指向的节点。这里的Ref与Ptr主要用于分辨const与非const.

    六、list与vector的对比

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

  • 相关阅读:
    页面功能并不是所有用户都能执行 点击判断用户权限路由是否进行跳转
    python三高问题分析
    Linux中目录的概述以及 查看 切换 创建和删除目录
    k线图分析法
    常见问题汇总
    解码eBPF可观测性:eBPF如何改变我们所知的观测性
    搭建智能桥梁,Amazon CodeWhisperer助您轻松编程
    C++ 多态(2)
    [数据结构与算法]全网最详细的二叉树详解,一文刷遍Leetcode
    使用pycharm or vscode来编写python代码?
  • 原文地址:https://blog.csdn.net/m0_73495584/article/details/133620371