一、list介绍
list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。
list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。
list与forward_list非常相似:最主要的不同在于forward_list是单链表,只能朝前迭代,已让其更简单高效。
与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率更好。
与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大list来说这可能是一个重要的因素)
二、list的使用
list中的接口比较多,此处类似,只需要掌握如何正确的使用,然后再去深入研究背后的原理,已达到可扩展的能力。下面为大家介绍list中一些常见的重要接口。
list的构造
构造函数( (constructor)) 接口说明
list (size_type n, const value_type& val = value_type()) 构造的list中包含n个值为val的元素
list() 构造空的list
list (const list& x) 拷贝构造函数
list (InputIterator first, InputIterator last) 用[first, last)区间中的元素构造list
void ListTest1() { //构造n个val的list list<int> lt1(10, 1); for (auto e : lt1) { cout << e << " "; } cout << endl; //构造一个空的list list<int> lt2; for (auto e : lt2) { cout << e << " "; } cout << endl; //拷贝构造一个list list<int> lt3(lt1); for (auto e : lt3) { cout << e << " "; } cout << endl; //使用迭代器区间构造一个list list<int> lt4(lt1.begin(), lt1.end()); for (auto e : lt4) { cout << e << " "; } cout << endl; }
运行结果>
迭代器
这里我们可暂时将迭代器理解成一个指针,该指针指向list中的某个节点。
函数声明 接口说明
begin +end 返回第一个元素的迭代器+返回最后一个元素下一个位置的迭代器
rbegin +rend 返回第一个元素的reverse_iterator,即end位置,返回最后一个元素下一个位置的reverse_iterator,即begin位置
1.begin与end为正向迭代器,对迭代器执行++操作,迭代器向后移动.
2.rbegin(end)与rend(begin)为反向迭代器,对迭代器执行++操作,迭代器向前移动.
empty和size
函数声明- - 接口说明
empty 检测list是否为空,是返回true,否则返回false
size 返回list中有效节点的个数
front和back
函数声明- - 接口说明
front 返回list的第一个节点中值的引用
back 返回list的最后一个节点中值的引用
list modifiers
函数声明- - 接口说明
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中的有效元素
list迭代器失效问题
前面说过,此处大家可将迭代器暂时理解成类似于指针,迭代器失效即迭代器所指向的节点的无效,即该节点被删除了。因为list的底层结构为带头结点的双向循环链表,因此在list中进行插入时是不会导致list的迭代器失效的,只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响。
void TestListIterator1() { int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }; list<int> l(array, array + sizeof(array) / sizeof(array[0])); auto it = l.begin(); while (it != l.end()) { // erase()函数执行后,it所指向的节点已被删除,因此it无效,在下一次使用it时,必须先给其赋值 l.erase(it); ++it; } }
运行报错>
改正过的代码:
void TestListIterator() { int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }; list<int> l(array, array + sizeof(array) / sizeof(array[0])); auto it = l.begin(); while (it != l.end()) { l.erase(it++); // it = l.erase(it); } }
这样运行就不会出问题了。
三、list的模拟实现
list的结构模型
成员变量
//自定义新的命名空间 namespace csdn { //定义模板参数 template<class T> //定义结构体 struct list_node { list_node<T>* _next;//指向下一个结构体的指针变量 list_node<T>* _prev;//指向上一个结构体的指针变量 T _val;//定义val值 //构造函数 list_node(const T& val = T()) :_next(nullptr) , _prev(nullptr) , _val(val) {} }; //定义模板参数 template<class T> class list//定义list类 { typedef list_node<T> Node;//重定义为Node private: Node* _head;//定义头节点 size_t _size;//定义_size保存元素个数 }; }
初始化函数
void empty_init() { //new一个新的节点 _head = new Node; _head->_prev = _head;//初始化指针变量 _head->_next = _head;//初始化指针变量 _size = 0;//初始化_size }
这里定义这个函数的意义是为了降低代码的冗余度,申请节点是比较常用的,这里封装成函数也方便我们直接调用。
构造函数
list() { //直接调用初始化函数 empty_init(); }
拷贝构造
list(const list<T>& lt) { //调用初始化函数 empty_init(); //使用范围for对数据进行逐个尾插 for (auto& e : lt) { push_back(e); } }
clear()
void clear() { //通过迭代器对每一个数据进行删除 iterator it = begin(); while (it != end()) { it = erase(it);//调用erase对该节点进行删除释放 } _size = 0;//将_size置为0 }
析构函数
~list() { //调用clear清空数据 clear(); delete _head;//释放头节点空间 _head = nullptr;//将头节点置为空 }
迭代器
STL中迭代器的设计思想:
STL中list的迭代器设计思想是将list中的节点作为迭代器,而不是像vector那样使用指针作为迭代器。这是因为list是一个双向链表,每个节点都包含指向前一个节点和后一个节点的指针,因此可以很方便地实现双向迭代器。
在STL中,list的迭代器包括正向迭代器和反向迭代器。正向迭代器可以从头部开始遍历到尾部,而反向迭代器则可以从尾部开始遍历到头部。通过这种设计,可以方便地实现list的各种操作,如插入、删除、排序等。
此外,在STL中,list的迭代器还支持一些常用的操作符重载,如*、->、++、–等,使得使用起来更加方便。
根据STL中的设计思想我们需要新定义一个结构体重新封装我们的链表节点然后再重载各种操作符,来达到我们实现迭代器的目的。
封装的结构体:
// typedef __list_iterator<T, T&, T*> iterator; // typedef __list_iterator<T, const T&, const T*> const_iterator; template<class T, class Ref, class Ptr> struct __list_iterator { typedef list_node<T> Node; // 定义类型别名 Node,代表指向 list_node 的指针类型 typedef __list_iterator<T, Ref, Ptr> self; // 定义类型别名 self,代表当前迭代器类型 Node* _node; // 指向 list_node 的指针,表示当前迭代器指向的节点 __list_iterator(Node* node) :_node(node) // 构造函数,接受一个指向 list_node 的指针,并将其赋值给 _node {} Ref operator*() { return _node->_val; // 重载 * 运算符,返回当前节点的值(通过指针访问节点的值) } Ptr operator->() { return &_node->_val; // 重载 -> 运算符,返回指向当前节点值的指针 } self& operator++() { _node = _node->_next; // 重载前置 ++ 运算符,使迭代器指向下一个节点,并返回自身引用 return *this; } self operator++(int) { self tmp(*this); // 重载后置 ++ 运算符,先用当前迭代器创建一个临时副本 _node = _node->_next; // 然后将当前迭代器指向下一个节点 return tmp; // 最后返回保存原始位置的临时副本 } self& operator--() { _node = _node->_prev; // 重载前置 -- 运算符,使迭代器指向前一个节点,并返回自身引用 return *this; } self operator--(int) { self tmp(*this); // 重载后置 -- 运算符,先用当前迭代器创建一个临时副本 _node = _node->_prev; // 然后将当前迭代器指向前一个节点 return tmp; // 最后返回保存原始位置的临时副本 } bool operator!=(const self& it) const { return _node != it._node; // 重载 != 运算符,判断两个迭代器是否不相等 } bool operator==(const self& it) const { return _node == it._node; // 重载 == 运算符,判断两个迭代器是否相等 } };
类内部:
typedef __list_iterator<T, T&, T*> iterator; typedef __list_iterator<T, const T&, const T*> const_iterator; iterator begin() { return _head->_next;//这里会发生隐式类型转换自动转换为iterator类型 //return iterator(_head->_next); } iterator end() { return _head; //return iterator(_head); } const_iterator begin() const { return _head->_next; //return const_iterator(_head->_next); } const_iterator end() const { return _head; //return const_iterator(_head); }
swap()
void swap(list<T>& lt) { std::swap(_head, lt._head);//调用std库里面的swap函数来交换_head std::swap(_size, lt._size);//调用std库里面的swap函数来交换_size }
重载=赋值运算符
list<T>& operator=(list<T> lt) { //调用swap函数交换*this和lt swap(lt); return *this; }
lt
的作用域在这个函数内,出了函数就会自动析构,我们把*this
和lt
的内容进行了交换,完了出了作用域lt
也会帮我们析构掉交换之后lt
的空间。
insert()
// 在迭代器 pos 指向的节点之前插入一个值为 x 的节点 iterator insert(iterator pos, const T& x) { // 获取 pos 指向的节点 Node* cur = pos._node; // 获取 pos 前面的节点 Node* prev = cur->_prev; // 创建一个新的节点,值为 x Node* newnode = new Node(x); // 将 prev 的 next 指针指向新节点 prev->_next = newnode; // 将新节点的 next 指针指向 cur newnode->_next = cur; // 将 cur 的 prev 指针指向新节点 cur->_prev = newnode; // 将新节点的 prev 指针指向 prev newnode->_prev = prev; // 链表大小加 1 ++_size; // 返回新节点的迭代器 return newnode; }
erase()
// 删除迭代器 pos 指向的节点 iterator erase(iterator pos) { // 断言 pos 不等于 end() assert(pos != end()); // 获取 pos 指向的节点 Node* cur = pos._node; // 获取 cur 前面的节点 Node* prev = cur->_prev; // 获取 cur 后面的节点 Node* next = cur->_next; // 将 prev 的 next 指针指向 next prev->_next = next; // 将 next 的 prev 指针指向 prev next->_prev = prev; // 删除 cur 指向的节点 delete cur; // 链表大小减 1 --_size; // 返回下一个节点的迭代器 return next; }
size()
size_t size() { return _size;//直接返回_size即可 }
头插/删、尾插/删
// 在链表尾部添加一个元素 void push_back(const T& x) { insert(end(), x); // 调用 insert 函数,在迭代器 end() 处插入元素 x } // 在链表头部添加一个元素 void push_front(const T& x) { insert(begin(), x); // 调用 insert 函数,在迭代器 begin() 处插入元素 x } // 移除链表尾部的元素 void pop_back() { erase(--end()); // 调用 erase 函数,移除迭代器 end() 前一个位置的元素 } // 移除链表头部的元素 void pop_front() { erase(begin()); // 调用 erase 函数,移除迭代器 begin() 处的元素 }
这里的代码是在一个链表容器中实现了四个成员函数,它们分别是 push_back、push_front、pop_back 和 pop_front。这些函数对链表进行尾部添加、头部添加、尾部移除和头部移除的操作。
push_back(const T& x):在链表的尾部添加一个元素 x,使用 insert 函数,在迭代器 end() 处插入元素 x。
push_front(const T& x):在链表的头部添加一个元素 x,使用 insert 函数,在迭代器 begin() 处插入元素 x。
pop_back():移除链表的尾部元素,使用 erase 函数,移除迭代器 end() 前一个位置的元素。
pop_front():移除链表的头部元素,使用 erase 函数,移除迭代器 begin() 处的元素。
这些函数利用了之前定义的链表迭代器,通过合适的迭代器位置,实现了对链表容器的尾部添加、头部添加、尾部移除和头部移除的功能。
总结
当谈到STL(标准模板库)中的 list 容器时,它是一个双向链表实现的容器。list 提供了许多有用的方法,使其成为处理插入和删除元素效率高的容器。下面是对 list 容器的一些总结:
双向链表结构:
list 是一个双向链表容器,每个节点包含一个值以及指向前一个节点和后一个节点的指针。这种结构使得在任何位置插入和删除元素的代价都是常数时间 O(1)。
不支持随机访问:
由于 list 是一个链表,不像 vector 那样支持随机访问,即不能通过索引直接访问元素。要访问 list 的元素,只能通过迭代器进行顺序访问。
插入和删除高效:
在 list 中插入和删除元素的效率非常高。由于链表的特性,插入和删除操作只涉及相邻节点指针的重新连接,而不需要像动态数组那样移动大量元素。
不连续内存存储:
list 的元素在内存中不是连续存储的,而是通过指针链接在一起的。这意味着 list 对内存的利用较低,但在元素插入和删除时效率很高。
不支持快速随机访问:
由于不支持常数时间的随机访问,使用 list 迭代器进行元素遍历和访问时的时间复杂度是 O(n),其中 n 是链表中元素的数量。
不支持数组操作:
与 vector 不同,list 不能通过 [] 运算符访问元素。如果需要通过索引访问元素,list 不是最佳选择。
没有预留空间:
list 不支持预留内存空间,因为其元素并非连续存储。因此,与 vector 不同,list 不提供 capacity() 方法。
迭代器稳定性:
list 在插入或删除元素后,除了当前元素的迭代器,其他迭代器仍然保持有效。这是由于链表的性质,与动态数组 vector 不同。
使用时的注意事项:
list 适用于需要频繁插入和删除元素的场景,但不需要随机访问。
如果需要频繁随机访问元素或预留大量内存空间,更适合使用 vector。
list 不支持与排序相关的随机访问算法,例如 std::sort(),因为它没有随机访问能力。
总的来说,list 是一个高效的双向链表容器,对于频繁插入和删除元素的场景非常有用。但是,如果需要随机访问元素或需要预留大量内存空间,应该选择其他支持这些操作的容器,如 vector。选择合适的容器取决于特定的使用情况和性能要求。