09.list

打印 上一主题 下一主题

主题 1029|帖子 1029|积分 3087

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
1. list的先容和利用

1.1 list的先容


  • list是可以在常数范围内涵任意位置进行插入和删除的序列式容器,而且该容器可从前后双向迭代。
  • list底层是双向链表布局,双向链表中每个元素存储在互不相干的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。
  • 与其他序列式容器相比,list通常在任意位置插入、移除元素的效率更好。
  • 与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问。好比:要访问list的第6个元素,必须从已知的位置(头和尾)一步步走到该位置;list还必要一些额外空间,以生存每个节点的相干联信息。
1.2 list的利用

1.2.1 list的构造

  1. #include <list>
  2. // 默认构造函数
  3. std::list<int> defaultList;
  4. // 通过值初始化列表
  5. std::list<int> valueList = {1, 2, 3, 4, 5};
  6. // 通过大小和默认值初始化列表
  7. std::list<int> sizeValueList(3, 1); // 包含三个元素,每个元素的值都是1
  8. // 通过范围初始化列表
  9. int arr[] = {10, 20, 30, 40, 50};
  10. std::list<int> rangeList(arr, arr + sizeof(arr) / sizeof(arr[0]));
  11. // 复制构造函数
  12. std::list<int> copyList = valueList;
  13. // 移动构造函数(C++11及以后)
  14. std::list<int> moveList = std::move(valueList);
复制代码
1.2.2 list 访问元素

1. front():返回对列表第一个元素的引用
  1. std::list<int> myList = {1, 2, 3, 4, 5};
  2. int firstElement = myList.front(); // firstElement is 1
复制代码
2. back():返回对列表最后一个元素的引用
  1. int lastElement = myList.back(); // lastElement is 5
复制代码
3. operator[]:通过索引访问元素,但请留意,std::list   不像   std::vector   或   std::array   那样提供随机访问。这个操纵实际上会从  begin()  开始遍历列表直到到达指定位置,因此效率较低。
  1. int elementAtTwo = myList[2]; // elementAtTwo is 3
复制代码
1.2.3 list增删查改

push_front(const T& value)  :在列表的开头添加一个新元素。
push_back(const T& value)  :在列表的末端添加一个新元素。
pop_front()  :移除列表的第一个元素。
pop_back()  :移除列表的最后一个元素。
insert(iterator pos, const T& value)  :在指定位置插入一个新元素。
  1. auto it = myList.begin();
  2. advance(it, 2); // 移动到第三个元素的位置
  3. myList.insert(it, 15); // 在第三个元素的位置插入 15
复制代码
留意:头插头删,尾插尾删的效率都很高。string以后插入开始利用迭代器位置,string用的是下标。list假如想在第3个位置插入元素,不能写成:
  1. myList.insert(myList.begin() + 3, 15);
复制代码
因为空间不是连续的。
erase(iterator pos)  :移除指定位置的元素。
  1. auto it = myList.begin();
  2. advance(it, 2); // 移动到第三个元素的位置
  3. myList.erase(it); // 移除第三个元素
复制代码
1.2.4 其他

好比说size(),capacity()等。


  • merge()
        在C++尺度库中,std::merge 是一个用于归并两个已排序范围的算法。它位于 <algorithm> 头文件中,而且可以用于各种容器(如 vector, list, deque 等)。std::merge 的主要作用是将两个已排序的序列归并成一个新的已排序序列。
        输入必须已排序的:std::merge 假设输入的两个范围已经按升序或降序分列。假如输入未排序,则结果将是未界说的。


  • remove
std::remove 和 std::remove_if 是用于从序列中移除元素的算法。它们并不直接删除元素,而是将不必要的元素移动到序列的末端,并返回一个指向新序列末端(即最后一个未被移除的元素之后的位置)的迭代器。实际的删除操纵通常必要结合容器的 erase 方法来完成。
  1. #include <iostream>
  2. #include <list>
  3. #include <algorithm> // 包含 std::remove
  4. using namespace std;
  5. void test_remove()
  6. {
  7.     int array[] = {1, 2, 3, 4, 5, 3, 6};
  8.     list<int> l(array, array + 7);
  9.     cout << "Original list: ";
  10.     for (const auto& elem : l)
  11.     {
  12.         cout << elem << " ";
  13.     }
  14.     cout << endl;
  15.     // 使用 std::remove 将值为 3 的元素移到末尾
  16.     l.erase(std::remove(l.begin(), l.end(), 3), l.end());
  17.     cout << "List after removing 3: ";
  18.     for (const auto& elem : l)
  19.     {
  20.         cout << elem << " ";
  21.     }
  22.     cout << endl;
  23. }
  24. int main()
  25. {
  26.     test2();
  27.     test_remove();
  28.     return 0;
  29. }
复制代码
利用 std::remove 将所有 3 移动到列表末端,并返回新的末端迭代器 new_end。
利用 erase 删除从 new_end 到列表末端的所有元素,即删除所有 3。
这样,只需调用一次 erase 就能删除所有 3。
1.2.5 迭代器失效

        因为list的底层布局为带头结点的双向循环链表,因此在list中进行插入时是不会导致list的迭代器失效的,只有在被删除时才会失效,而且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响。
  1. #include <iostream>
  2. #include <list>
  3. void test()
  4. {
  5.     int array[] = {1, 2, 3, 4, 5};
  6.     std::list<int> l(array, array + 5);
  7.     auto it = l.begin();
  8.     while (it != l.end())
  9.     {
  10.         std::cout << "Deleting element: " << *it << std::endl;
  11.         auto next_it = l.erase(it);  // erase 返回下一个有效的迭代器
  12.         std::cout << "Iterator after erase: " << (next_it == l.end() ? "end" : std::to_string(*next_it)) << std::endl;
  13.         it = next_it;
  14.     }
  15.     std::cout << "List after deletion: ";
  16.     for (const auto& elem : l)
  17.     {
  18.         std::cout << elem << " ";
  19.     }
  20.     std::cout << std::endl;
  21. }
  22. int main()
  23. {
  24.     test();
  25.     return 0;
  26. }
复制代码
  1. Deleting element: 1
  2. Iterator after erase: 2
  3. Deleting element: 2
  4. Iterator after erase: 3
  5. Deleting element: 3
  6. Iterator after erase: 4
  7. Deleting element: 4
  8. Iterator after erase: 5
  9. Deleting element: 5
  10. Iterator after erase: end
  11. List after deletion:
复制代码
通过上述代码和控制流图,可以清楚地验证删除时失效的只是被删除节点的迭代器,其他迭代器不会受到影响。
  1. void test()
  2. {
  3.     int array[] = {1, 2, 3, 4, 5};
  4.     list<int> l(array, array + 5);
  5.     auto it = l.begin();
  6.     while (it != l.end())
  7.     {
  8.         l.erase(it);  
  9.         ++it;
  10.     }
  11. }
复制代码
erase函数实行后,it所指向的节点已经被删除,因此it无效,在下一次利用it时,必须先给其赋值。
  1. void test()
  2. {
  3.     int array[] = {1, 2, 3, 4, 5};
  4.     list<int> l(array, array + 5);
  5.     auto it = l.begin();
  6.     while (it != l.end())
  7.     {
  8.         it = l.erase(it);  // erase 返回下一个有效的迭代器
  9.     }
  10. }
复制代码
1.3 迭代器的分类

迭代器按性子分类,由容器底层布局决定。


  • 前向迭代器(Forward Iterator)
可以读取和写入数据,支持多次遍历,但只能单向进步。
支持的操纵:输入迭代器和输出迭代器的所有操纵,以及多次遍历。


  • 双向迭代器(Bidirectional Iterator)
在前向迭代器的基础上增加了反向遍历的本领。
支持的操纵:前向迭代器的所有操纵,以及--(前缀和后缀)。


  • 随机访问迭代器(Random Access Iterator)
支持任意位置的访问,可以进行算术运算(如+, -),而且可以比力巨细。
支持的操纵:双向迭代器的所有操纵,以及[],+, -,<, >, <=, >=。
按功能从弱到强分列如下:

  • 输入迭代器(Input Iterator)
  • 输出迭代器(Output Iterator)
  • 前向迭代器(Forward Iterator)
  • 双向迭代器(Bidirectional Iterator)
  • 随机访问迭代器(Random Access Iterator)
        好比说 sort 函数利用的就是随机迭代器,我们这节讲的 list 就不能调用它,因为list利用的是双向迭代器。
在C++尺度库中,不同容器支持不同范例的迭代器:

  • std::list:利用双向迭代器,支持前向和后向遍历。
  • std::vector 和 std::deque、std::string:利用随机访问迭代器,支持任意位置的访问和高效的算术运算。
  • std::forward_list:利用前向迭代器,仅支持单向遍历。
  • std::set, std::map, std::multiset, std::multimap:利用双向迭代器。
  • std::unordered_set, std::unordered_map, std::unordered_multiset, std::unordered_multimap:利用前向迭代器。
层级低的容器不能访问层级高的函数。层级高的容器可以访问层级低的函数。好比:vector和string就可以调用reverse(双向)。
详细的函数属于什么层级可以在文档中查找。

2. list的模仿实现

2.1 开端实现

  1. namespace zzy
  2. {
  3.     template<class T>
  4.     struct list_node//为什么这里写它?
  5.     {
  6.         list_node<T>* _next;
  7.         list_node<T>* _prev;
  8.         T _val;
  9.         explicit list_node(const T& val = T())
  10.             :_next(nullptr)
  11.             ,_prev(nullptr)
  12.             ,_val(val)
  13.         {}
  14.     };
  15.    
  16.     template<class T>
  17.     class list
  18.     {
  19.         typedef list_node<T> Node;
  20.     public:
  21.         list()
  22.         {
  23.             _head = new Node;
  24.             _head->_prev = _head;
  25.             _head->_next = _head;
  26.             _size = 0;
  27.         }
  28.         void push_back(const T& x)
  29.         {
  30.             Node* tail = _head->_prev;
  31.             Node* newnode = new Node(x);
  32.             tail->_next = newnode;
  33.             newnode->_prev = tail;
  34.             
  35.             newnode->_next = _head;
  36.             _head->_prev = newnode;
  37.         }
  38.     private:
  39.         Node* _head;
  40.         size_t _size;
  41.     };
  42. }
复制代码


  • list_node,为什么这里写它?后面再  typedef  成 Node:
        因为在zzy的定名空间下,别的容器的结点也要用 Node 这个名字,假如这里列表的结点写成了Node,别的容器利用时会有定名冲突。以是在后面的list类中再typedef成Node,便于区分的同时也更方便。
2.2 list迭代器

在namespace中还要加上list迭代器:
  1.     template<class T>
  2.     struct _list_iterator
  3.     {
  4.         typedef list_node<T> Node;
  5.         Node* _node;
  6.         explicit _list_iterator(Node* node)
  7.             :_node(node)
  8.         {}
  9.         T& operator*()
  10.         {
  11.             return _node->_val;
  12.         }
  13.         _list_iterator<T>& operator++()
  14.         {
  15.             _node = _node->_next;
  16.             return *this;
  17.         }
  18.         bool operator!=(const _list_iterator<T>& it) const
  19.         {
  20.             return _node != it._node;
  21.         }
  22.     };
复制代码
这段C++代码界说了一个模板类 _list_iterator,用于实现双向链表的迭代器。主要功能如下:

  • 构造函数:初始化迭代器,指向给定的节点。
  • 解引用操纵符 operator*:返回当前节点存储的值。
  • 前缀自增操纵符 operator++:将迭代器移动到下一个节点,并返回自身。
  • 不等比力操纵符 operator!=:比力两个迭代器是否指向不同的节点。


  • 为什么 const _list_iterator<T>& it  要写const
不加const会报错,因为end() 函数通常返回一个表现链表末端的迭代器,这个迭代器是一个临时对象(具有常性)。假如你倒霉用 const 修饰符,编译器将不允许你将这个临时对象绑定到非 const 引用上。
与此同时,list类中public更新为:
  1.     public:
  2.         typedef _list_iterator<T> iterator;
  3.         iterator begin()
  4.         {
  5.             return iterator(_head->_next);
  6.         }
  7.         iterator end()
  8.         {
  9.             return iterator(_head);
  10.         }
复制代码
2.3 const迭代器

我们期望的是指向的内容不能修改,不是指针本身不能修改,指针本身还得++才能遍历列表。
  1. T& operator*() const
复制代码
上面这种是允许修改返回值的,很少用。
  1. const T& operator*()
复制代码
这个是以const T& 返回,返回值范例是 const T&,返回值不能修改。
假如我们只是复制一下普通迭代器的实现,只是修改成上面这种*重载,其余都稳固,未免太过冗余,于是我们想到了可以在模板中增加参数,一种只用来控制返回范例不同的参数
  1. public:
  2.         typedef _list_iterator<T, T&> iterator;
  3.         typedef _list_iterator<T, const T&> const_iterator;
复制代码
  1.     template<class T, class Ref>
  2.     struct _list_iterator
  3.     {
  4.         typedef list_node<T> Node;
  5.         typedef _list_iterator<T, Ref> self;
  6.         Node* _node;
  7.         explicit _list_iterator(Node* node)
  8.             :_node(node)
  9.         {}
  10.         Ref operator*()
  11.         {
  12.             return _node->_val;
  13.         }
  14.         self& operator++()//前置
  15.         {
  16.             _node = _node->_next;
  17.             return *this;
  18.         }
  19.         self operator++(int)//后置
  20.         {
  21.             self tmp = *this;
  22.             _node = _node->_next;
  23.             return tmp;
  24.         }
  25.         bool operator!=(const self& it) const
  26.         {
  27.             return _node != it._node;
  28.         }
  29.     };
复制代码
Ref 是 _list_iterator 模板类的第二个模板参数。
Ref 的作用:
Ref 用于界说解引用操纵符 operator*() 返回的范例。详细来说:


  • 假如 Ref 是 T&(即 T 的引用),那么 operator*() 将返回当前节点存储的值的引用。
  • 假如 Ref 是 T(即 T 的值),那么 operator*() 将返回当前节点存储的值的副本。
通过这种方式,_list_iterator 可以支持不同范例的迭代器,例如:


  • 普通迭代器:返回引用 (T&),允许修改元素。
  • 常量迭代器:返回常量引用 (const T&),不允许修改元素。
  1. void test_list()
  2. {
  3.     list<A> lt;
  4.     lt.push_back(A(1));
  5.     lt.push_back(A(2));
  6.     lt.push_back(A(3));
  7.     list<A>::iterator it = lt.begin();
  8.     while (it != lt.end())
  9.     {
  10.         cout << (*it)._a << endl;
  11.         cout << it->_val._a << endl;
  12.         ++it;
  13.     }
  14.     cout << endl;
  15. }
复制代码
C语言中的scanf和printf可以针对内置范例,但是自界说范例打印仍必要流插入重载。
由于普通和const迭代器各必要一个不同的返回范例的-> 的重载:
  1.     template<class T, class Ref = T&, class Ptr>
  2.     struct _list_iterator
  3.     {
  4.         typedef list_node<T> Node;
  5.         typedef _list_iterator<T, Ref, Ptr> self;
  6.         Node* _node;
  7.         
  8.         ...
  9.         Ptr operator->() const
  10.         {
  11.             return &_node->_val;
  12.         }
  13.     };
复制代码
  1. public:
  2.         typedef _list_iterator<T, T&, T*> iterator;
  3.         typedef _list_iterator<T, const T&, const T*> const_iterator;
复制代码
2.4 其他成员函数

  1.         iterator insert(iterator pos, const T& x)
  2.         {
  3.             Node* cur = pos._node;
  4.             Node* prev = cur->_prev;
  5.             Node* newnode = new Node(x);
  6.             prev->_next = newnode;
  7.             newnode->_next = cur;
  8.             cur->_prev = newnode;
  9.             newnode->_prev = prev;
  10.             ++size;
  11.             return newnode;
  12.         }
  13.         iterator erase(iterator pos)
  14.         {
  15.             assert(pos != end());
  16.             
  17.             Node* cur = pos._node;
  18.             Node* prev = cur->_prev;
  19.             Node* next = cur->_next;
  20.             
  21.             prev->_next = next;
  22.             next->_prev = prev;
  23.             
  24.             delete cur;
  25.             --size;
  26.             return next;
  27.         }
复制代码
  1.         void clear()
  2.         {
  3.             iterator it = begin();
  4.             while (it != end())
  5.             {
  6.                 it = erase(it);
  7.             }
  8.             _size = 0;
  9.         }
  10.         size_t size() const
  11.         {
  12.             return _size;
  13.         }
  14.         ~list()
  15.         {
  16.             clear();
  17.             delete _head;
  18.             _head = nullptr;
  19.         }
复制代码
  1.         list(const list<T>& lt)
  2.         {
  3.             _head = new Node;
  4.             _head->_prev = _head;
  5.             _head->_next = _head;
  6.             _size = 0;
  7.             for(auto& e : lt)
  8.             {
  9.                 push_back(e);
  10.             }
  11.         }
  12.         void swap(list<T>& lt)
  13.         {
  14.             std::swap(_head, lt._head);
  15.             std::swap(_size, lt._size);
  16.         }
  17.         list<T>& operator=(const list<T>& lt)
  18.         {
  19.             swap(lt);
  20.             return *this;
  21.         }
复制代码

3. list与vector的对比

vectorlist
底层布局动态次序表,一段连续空间带头结点的双向循环链表
随机访问支持,访问效率为O(1)不支持,访问某个效率为O(N)
插入删除任意位置插删效率低效率高,不必要搬移元素,O(1)
空间利用率不易造成内存碎片,空间利用率高小结点易造成内存碎片
迭代器原生指针对指针进行封装
迭代器失效 插入可能导致扩容,导致失效
删除时需重新赋值否则失效
插入时不会导致迭代器失效
删除时,只会导致当前迭代器失效
利用场景 必要高效存储,支持随机访问
不关心插入删除效率
大量删除和插入操纵
不关心随机访问
3.1 容器与迭代器

假如我们想在3前面插入一个值,但是我们发现list和vector都没有提供find,该怎么考虑呢?
首先我们应该意识到:C++通过迭代器将容器与算法连接起来了。迭代器提供了统一的方法访问容器,却不必要关注容器的底层实现。
        在vector中,begin指向第一个元素,end指向最后一个数据的下一个位置。在list中,由于list是带头双向循环链表,begin指向的是第一个数据(头节点的下一个节点),而end则指向头结点。在list中,不能用  it < c.end(),在遍历   std::list   时,应该利用   !=   来比力迭代器是否到达容器的末端,因为在list中地点巨细关系是不一定的。
        在 C++ 尺度库中,  std::list   和   std::vector   都没有直接提供   find   成员函数。原因在于:


  • 通用算法与容器分离:C++ 尺度库接纳了一种设计哲学,将算法与容器分离。算法被设计为独立于容器的函数,这样可以提高算法的复用性。例如,  std::find   是一个通用算法,可以应用于任何支持迭代器的容器,包罗   std::list、std::vector、std::array   等。
  • 克制重复:假如每个容器都提供自己的   find   方法,那么就会有很多重复的代码。通过将算法作为独立的函数,可以克制这种重复,同时保持代码的简洁性和划一性。
3.2 排序效率

        在C++中,选择合适的容器对于步伐的性能和效率至关重要。std::list 和 std::vector 是两种常用的容器,但它们在不同操纵上的表现差别很大。以下是为什么在必要排序时,std::list 不如 std::vector 合适的原因:
1. 排序算法的实现


  • std::vector:
std::vector 支持随机访问迭代器,这意味着可以高效地进行索引访问和算术运算。尺度库中的排序算法(如 std::sort)依靠于随机访问迭代器来实现高效的排序算法(通常是快速排序或归并排序),这些算法的时间复杂度为 O(n log n)。


  • std::list:
std::list 只支持双向迭代器,不支持随机访问迭代器。
因此,尺度库中的 std::sort 不能直接用于 std::list。虽然 std::list 提供了 std::list::sort 成员函数,但它的时间复杂度通常较高(O(n log n),但在某些环境下可能更差),而且由于 std::list 的链表布局,元素之间的比力和交换操纵也较为低效。
2. 内存布局与缓存友好性


  • std::vector:
std::vector 是连续内存分配的,这使得它在现代CPU上具有更好的缓存局部性,从而提高了访问速度。
连续内存布局也有助于减少页面错误和提高内存带宽利用率。


  • std::list:
std::list 是由节点组成的链表,每个节点都可能分散在不同的内存位置。
这种非连续的内存布局导致较差的缓存局部性,增加了内存访问的延迟。
4. 实际应用场景


  • std::vector:
假如你必要对大量数据进行排序,而且不必要频仍的插入和删除操纵,std::vector 是更好的选择。
它提供了更好的性能和更高的代码可读性。


  • std::list:
假如你必要频仍地在列表中心插入和删除元素,而且不必要高效的排序操纵,std::list 可能更得当。
但在必要排序的环境下,std::list 的性能劣势明显。


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

立山

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表