list模拟实现
1)list介绍
结构,list的迭代器用类封装为了实现重载,<T,T&,T*>
template < class T, class Alloc = allocator<T> > class list;
- ①:list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代
- ②: list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素
- ③: list与forward_list非常相似:最主要的不同在于forward_list是单链表,只能朝前迭代,已让其更简单高效
- ④: 与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率更好
- ⑤:与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大list来说这可能是一个重要的因素)
- ⑥:
list 有一个重要性质:插入操作(insert)与接合操作(splice)都不会造成原有的list迭代器失效
。这在vector是不成立的,因为vactor的插入可能引起空间的重新配置
,导致原来的迭代器全部失效。list的迭代器失效,只会出现在删除的时候,指向删除元素的那个迭代器在删除后失效
2)list使用
详细请参考:cplusplus -list
①list构造函数
参见下面模拟实现部分
②list的访问及遍历
略
③list容量操作
略
③list修改
注意容器尽量只用自己的swap,std库中的实现如下
template <class T> void swap ( T& a, T& b ) { T c(a); a=b; b=c; }
对于list容器的swap,只用交换头指针就行了,
库中的T c(a);
语句是进行的深拷贝,自然不推荐使用
④list操作
①:list的成员函数sort不建议使用,因为是归并排序,效率低无意义(
链表的快排有缺陷
,有序时候是O(N^2))
②: 注意list不能使用库函数的sort,库函数是使用的快排
- list迭代器不是原生指针,++不是取到下一个数据,地址是随机的,要实现找到下一个位置的这个操作本身实现就需要O(N)的时间复杂度,所以无意义
- 库函数里的sort用了迭代器相减,变相只支持原生指针,list迭代器不能相减
3)list模拟实现(结构:三个类)
list底层是一个带头双向循环链表
Ⅰ.节点类
前面说过struct中的成员默认都是公有的(public)
//1. List的节点类 template<class T> struct ListNode { ListNode(const T& val = T())//匿名对象缺省值 :_prev(nullptr) ,_next(nullptr) ,_val(val) {} ListNode<T>* _prev; ListNode<T>* _next; T _val; };
Ⅱ. List的迭代器类
- 当我们要使用const_iterator时,只能重新定义一个const版的List的迭代器类
- 而const_iterator和iterator只有解引用的重载不一样,我们使用这种方法
- 类模板参数改为三个分别对应
T,T&,T*
template<class T, class Ref, class Ptr> struct ListIterator { typedef ListNode<T> Node; typedef ListIterator<T, Ref, Ptr> Self; Node* _pNode };
1.构造
①默认构造和拷贝构造
注意
: 在List的迭代器类里不需要自定义析构函数,默认析构函数已经够用了ListIterator(Node* pNode = nullptr) :_pNode(pNode) {} ListIterator(const Self& l) : _pNode(l._pNode) {}
2.运算符重载
①解引用*和->重载
解引用*
Ref operator*() { return _pNode->_val; } //Self* operator->();
->重载
Self* operator->() { return &_pNode->data; }
编译器对‘->’的省略
假如我们有一个结构体类型
struct TreeNode { struct TreeNode* _left; struct TreeNode* _right; int _val; TreeNode(int val = -1) :_left(nullptr) , _right(nullptr) , _val(val) {} };
构建一个树
list<TreeNode> lt; lt.push_back(TreeNode(1)); lt.push_back(TreeNode(2)); lt.push_back(TreeNode(3)); lt.push_back(TreeNode(4)); list<TreeNode>::iterator it = lt.begin(); while (it != lt.end()) { //cout << (*it)._val << " "; printf("val:%d,left:%p,right:%p\n", (*it)._val, (*it)._left, (*it)._right); printf("val:%d,left:%p,right:%p\n", it->_val, it->_left, it->_right); ++it; }
想要拿到树节点的_val值有三种方法
cout << *it
,参考上面的重载*可以得知这里的*it是TreeNode结构体而不是结构体里的_val值,所以我们可以在本命名空间重载一个<<运算符ostream& operator<<(ostream& out, TreeNode n);
- 直接
cout << (*it)._val
(*it
==it.operator*()
)- 利用->的重载:
it->_val
(it->
==it.operator->()
)
但是->的重载返回的是TreeNode*指针,不是应该这样调用吗it->->_val
?
!!注意!!
:所有编译器对->->做了优化,省略为一个->
②前置/后置++/ - -
注意
: 后置,临时变量出作用域销毁,不能引用返回Self& operator++()//注意 这是前置++ { _pNode = _pNode->_next; return *this; } Self operator++(int)//注意 后置++要加int //注意 临时变量出作用域销毁,不能引用返回 { Self tmp = _pNode; _pNode = _pNode->_next; return tmp; } Self& operator--()//注意 这是前置++ { _pNode = _pNode->_prev; return *this; } Self operator--(int)//注意 后置++要加int { Self tmp = _pNode; _pNode = _pNode->_prev; return tmp; }
③关系运算符!= ==重载
bool operator!=(const Self& it) const { return _pNode != it._pNode; } bool operator==(const Self& it) const { return (_pNode == it._pNode); }
3.list迭代器失效问题
仅在节点被删除的时候会造成迭代器失效,迭代器失效问题在C++初阶—vector的使用及模拟实现中详细探讨了,这里不再赘述,参考下面的erase实现
Ⅲ. list类
swap
用于赋值重载和拷贝构造的现代写法
当然使用库函数里的可以这样达到一样的效果std::swap(_head,l._head);
(只交换头节点)void swap(list<T>& l) { PNode tmp = _head; _head = l._head; l._head = tmp; }
1.构造
①默认构造
list() { _head = new Node; _head->_next = _head; _head->_prev = _head; }
②构造有n个节点值为val的list
list(int n, const T& value = T()) { _head = new Node; _head->_next = _head; _head->_prev = _head; while (n--) { push_back(value); } }
③拷贝构造
传统写法
// lt2(lt1) -- 传统写法 list(const list<T>& l) { _head = new Node; _head->_next = _head; _head->_prev = _head; for (const auto& e : l) { push_back(e); } }
现代写法(与传统写法实际上没有太大区别)
//lt2(lt1) -- 现代写法 list(const list<T>& l) { _head = new Node; _head->_next = _head; _head->_prev = _head; list<T> tmp(l.begin(), l.end()); this->swap(tmp); }
④迭代器区间
支持各种迭代器类型
template <class Iterator> list(Iterator first, Iterator last) { _head = new Node; _head->_next = _head; _head->_prev = _head; PNode cur = _head; while (first != last) { push_back(*first); first++; } }
2.赋值运算符重载
传统写法 (
注意范围for加&防止T是自定义类型时深拷贝
)//传统 list<T>& operator=(const list<T>& l) { if (this != &l) { this->clear(); for (const auto& e : l)//引用防止深拷贝 { push_back(e); } } return *this; }
现代写法(
对于深拷贝现代写法一定比传统写法更简单
)//现代 list<T>& operator=(list<T> l) { swap(l); return *this; }
3.析构
注意clear不会清理头节点
void clear() { list<T>::iterator cur = begin(); while (cur != end()) { cur = erase(cur);//注意迭代器失效问题 } }
析构直接复用clear+清理头节点即可
~list() { clear(); delete _head; _head = nullptr; }
4. 容量
①size
不包括头节点,节点个数
size_t size()const { size_t count = 0; PNode cur = _head->_next; while (cur!=_head) { cur = cur->_next; count++; } return count; }
②empty
判空
bool empty()const { return _head->_next == _head; }
5.访问
①front
分为
可写
和只读
两种T& front() { assert(!empty()); return _head->_next->_val; } const T& front()const//常 { assert(!empty()); return _head->_next->_val; }
②back
分为
可写
和只读
两种T& back() { assert(!empty()); return _head->_prev->_val; } const T& back()const { assert(!empty()); return _head->_prev->_val; }
6.修改操作
①insert
操作和之前c语言实现带头双向循环链表一样,不再赘述
// 在pos位置前插入值为val的节点 iterator insert(iterator pos, const T& val) { Node* cur = pos._pNode; Node* prev = cur->_prev; Node* newnode = new Node(val); newnode->_next = cur; cur->_prev = newnode; prev->_next = newnode; newnode->_prev = prev; //1. 原始 //iterator ret(newnode);// //return ret;// //2. 隐式类型转换 //return newnode; //3. 匿名对象 return iterator(newnode);//== return ListIterator<T>(newnode);
返回值有三种写法
:
- 原始
iterator ret(newnode);
return ret;
- 隐式类型转换
return newnode;
- 匿名对象
return iterator(newnode);
②erase
防止出现迭代器失效
,需要返回一个iterator指向删除位置的下一个位置的// 删除pos位置的节点,返回该节点的下一个位置 iterator erase(iterator pos) { assert(pos!=end()); Node* cur = pos._pNode; Node* prev = cur->_prev; prev->_next = cur->_next; cur->_next->_prev = prev; delete cur; return iterator(prev->_next); }
③pop_back,push_back等复用
服用
void push_back(const T& val) { insert(end(), val); //Node* tail = _head->_prev; //Node* newnode = new Node(val);//节点直接有构造函数,不需要自己给_val值了 //tail->_next = newnode; //newnode->_prev = tail; //newnode->_next = _head; //_head->_prev = newnode; } void pop_back() { erase(--end()); } void push_front(const T& val) { insert(begin(), val); } void pop_front() { erase(begin()); }
4)迭代器总结
①:迭代器从使用功能分类
:
- 正向/正向const
- 反向/反向const
②:迭代器从底层结构分类
:
单向
单链表/哈希表(只支持++)双向
双向链表/二叉树/map(支持++/ - -)随机
dequeue/vector/string/map(支持++ /- -/ +/ -)随机>双向>单向(包含关系)
迭代器在不暴露容器底层实现细节的情况下,提供统一的方式去修改容器中储存的数据,是算法和容器的胶合剂
思考
: 在模拟实现list第一感觉会直接进行遍历节点的操作,而不是利用实现好的迭代器进行操作,是对list底层体会不够深刻