利用C++模拟实现STL容器:list
作者:蒋灵瑜的笔记本
一、list的介绍
列表是一种顺序容器,它允许在序列中的任何位置执行常量时间插入和删除操作,并允许在两个方向上进行迭代。
它的底层是一个带头双向循环链表。附一篇博主用C语言写的带头双向循环链表:【数据结构】带头双向循环链表的实现
二、list的排序
list不能用算法库的sort进行排序。算法库中的sort的底层是一个快排,需满足三数取中,需要传入随机访问迭代器,所以list并不适用。
当然list中提供了一个自己的sort,它的底层是一个归并排序。但是这个sort比vector使用算法库的sort还慢,甚至比list的数据先push_back到vector到再用算法库的sort还要慢。
三、迭代器
1、list的迭代器失效问题
insert,迭代器不失效
earse失效
2、迭代器的功能分类
1、单向迭代器:只能++,不能--。例如单链表,哈希表;
2、双向迭代器:既能++也能--。例如双向链表;
3、随机访问迭代器:能++--,也能+和-。例如vector和string。
迭代器是内嵌类型(内部类或定义在类里)
3、list迭代器的模拟实现
普通迭代器
迭代器的实现需要支持解引用(为了取数据),支持++--。
博主模拟实现string和vector时,直接将原生指针typedef成迭代器,是因为数组的结构正好满足迭代器的行为(注:string和vector可以用原生指针实现,但是vs中采用自定义类型封装的方式实现),但是list中的节点地址是不连续的,不能使用原生指针,需要使用类进行封装+运算符重载实现。
//用类封装迭代器 template <class T> struct __list_iterator { typedef list_node<T> node; //用节点的指针进行构造 __list_iterator(node* p) :_pnode(p) {} //迭代器运算符的重载 T& operator*() { return _pnode->_data; } __list_iterator<T>& operator++()//返回值不要写成node* operator++(),因为迭代器++肯定返回迭代器啊,你返回节点指针类型不对 { //return _pnode->_next; _pnode=_pnode->_next; return *this;//返回的是迭代器 } bool operator!=(const __list_iterator<T>& it) { return _pnode != it._pnode; } public: node* _pnode;//封装一个节点的指针 };
const迭代器
const迭代器的错误写法:
typedef __list_iterator<T> iterator; const list<T>::iterator it=lt.begin();
因为typedef后,const修饰的是迭代器it,只能调用operator*(),调不了operator++()。(重载operator++()为const operator++()也不行,因为const版本++还是改变不了)
正确写法:想实现const迭代器,不能在同一个类里面动脑筋,需要再写一个const版本迭代器的类。
//用类封装const迭代器 template <class T> struct __list_const_iterator { typedef list_node<T> node; //用节点的指针进行构造 __list_const_iterator(node* p) :_pnode(p) {} //迭代器运算符的重载 const T& operator*()const { return _pnode->_data; } __list_const_iterator<T>& operator++()//返回值不要写成node*,因为迭代器++肯定返回迭代器啊,你返回节点指针类型不对 { //return _pnode->_next;//返回类型错误的 _pnode = _pnode->_next; return *this;//返回的是迭代器 } __list_const_iterator<T>& operator--() { _pnode = _pnode->_prev; return *this; } bool operator!=(const __list_const_iterator<T>& it)const { return _pnode != it._pnode; } public: node* _pnode;//封装一个节点的指针 }; typedef __list_const_iterator<T> const_iterator;
当然,这样写__list_iterator和__list_const_iterator这两个类会出现代码重复。STL库中是通过类模板多给一个参数来实现,这样,同一份类模板就可以生成两种不同的类型的迭代器(以下为仿STL库的模拟实现):
//用类封装普通/const迭代器 template <class T,class Ref> struct __list_iterator { typedef list_node<T> node; typedef __list_iterator<T,Ref> Self; //用节点的指针进行构造 __list_iterator(node* p) :_pnode(p) {} //迭代器运算符的重载 Ref operator*() { return _pnode->_data; } Self& operator++()//返回值不要写成node*,因为迭代器++肯定返回迭代器啊,你返回节点指针类型不对 { //return _pnode->_next;//返回类型错误的 _pnode=_pnode->_next; return *this;//返回的是迭代器 } Self& operator--() { _pnode = _pnode->_prev; return *this; } bool operator!=(const Self& it) { return _pnode != it._pnode; } public: node* _pnode;//封装一个节点的指针 }; typedef __list_iterator<T, T&> iterator; typedef __list_iterator<T, const T&> const_iterator;
4、迭代器价值
1、封装底层实现,不暴露底层实现的细节;
2、多种容器提供统一的访问方式,降低使用成本;
C语言没有运算符重载和引用等语法,是实现不了迭代器的。
5、迭代器operator->的重载
迭代器的用法就是模拟指针的行为,如果现在有一个指向结构的指针,那么就需要用到->来解引用。
//*的重载:返回节点的数据 Ref operator*() { return _pnode->_data; } //->的重载:返回数据的指针 T* operator->() { return &_pnode->_data; }
但是operator->使用T*做返回值类型,这样无论是普通迭代器和const迭代器都能修改,所以operator->的返回值类型应该改为泛型:
template <class T, class Ref,class Ptr> Ptr operator->() { return &_pnode->_data; } typedef __list_iterator<T, T&, T*> iterator; typedef __list_iterator<T, const T&, const T*> const_iterator;
四、模拟实现时遇到的困惑及注意点
1、调用拷贝构造时,链表内节点数据为什么已经是深拷贝了?
2、类名和类型的区别
普通类:类名等于类型
类模板:类名不等价于类型,例如list类模板类名是list,类型list<int>等。
所以我们平常写函数形参和返回值时,总会带上形参和返回值的类型:
// 赋值运算符重载写法2(赋值运算符重载都可以这么干) list<T>& operator=(list<T> lt) { swap(lt); return *this; }
但是C++在类模板里面可以用类名代替类型:
// 赋值运算符重载写法2(赋值运算符重载都可以这么干) list& operator=(list lt) { swap(lt); return *this; }
建议写代码的时候严格区分类型和类名,让自己和别人能够看的很明白。(了解下C++有这种坑语法即可)
五、vector和list的优缺点
vector和list像左右手一样,是互补关系,缺一不可。vector的优点正是list的缺点,list的优点也是vector的缺点,实际选用容器时,按照需求择优选用。
1、vector
vector的优点(结构厉害):
1、支持下标的随机访问;
2、尾插尾删效率高(当然扩容的那一次尾插尾删会较慢);
3、CPU高速缓存命中高(数据从缓存加载至CPU中,会加载连续的一段数据,vector因为结构连续,高速缓存命中高)。
vector的缺点:
1、非尾插尾删效率低;
2、扩容有消耗,并存在一定的空间浪费。
vector迭代器失效问题:
insert/erase均失效。(如果string的insert和erase形参是迭代器,那么也会失效,但是大部分接口是下标传参,不考虑失效问题,只有几个接口是迭代器传参,需要注意迭代器失效问题)
2、list
list的优点:
1、按需申请释放,无需扩容;
2、任意位置插入删除时间O(1);(这里说的是插入删除,不要加上查找的时间)
list的缺点:
1、不支持下标的随机访问;
2、CPU高速缓存命中率低;
3、每一个节点除了存储数据外,还需要额外存储两个指针。
list迭代器失效问题:
insert不失效,erase失效。
六、模拟实现list整体代码
#pragma once #include <iostream> #include <algorithm> #include <assert.h> #include <vector> using std::cout; using std::endl; namespace jly { //链表节点的类 template <class T> struct list_node { list_node(const T& x = T())//给一个缺省值,变成默认构造函数 :_next(nullptr) , _prev(nullptr) , _data(x) {} list_node* _next; list_node* _prev; T _data; }; //用类封装普通/const迭代器 template <class T, class Ref,class Ptr> struct __list_iterator { typedef list_node<T> node; typedef __list_iterator<T, Ref,Ptr> Self; //用节点的指针进行构造 __list_iterator(node* p) :_pnode(p) {} //迭代器运算符的重载 Ref operator*() { return _pnode->_data; } Ptr operator->() { return &_pnode->_data; } Self& operator++()//返回值不要写成node*,因为迭代器++肯定返回迭代器啊,你返回节点指针类型不对 { //return _pnode->_next;//返回类型错误的 _pnode = _pnode->_next; return *this;//返回的是迭代器 } Self operator++(int)//后置++ { _pnode = _pnode->_next; return Self(_pnode->_next); } Self& operator--() { _pnode = _pnode->_prev; return *this; } Self operator--(int)//后置-- { _pnode = _pnode->_prev; return Self(_pnode->_prev); } bool operator!=(const Self& it)const { return _pnode != it._pnode; } bool operator==(const Self& it)const { return _pnode == it._pnode; } public: node* _pnode;//封装一个节点的指针 }; //用类封装const迭代器 //template <class T> //struct __list_const_iterator //{ // typedef list_node<T> node; // //用节点的指针进行构造 // __list_const_iterator(node* p) // :_pnode(p) // {} // //迭代器运算符的重载 // const T& operator*()const // { // return _pnode->_data; // } // __list_const_iterator<T>& operator++()//返回值不要写成node*,因为迭代器++肯定返回迭代器啊,你返回节点指针类型不对 // { // //return _pnode->_next;//返回类型错误的 // _pnode = _pnode->_next; // return *this;//返回的是迭代器 // } // __list_const_iterator<T>& operator--() // { // _pnode = _pnode->_prev; // return *this; // } // bool operator!=(const __list_const_iterator<T>& it)const // { // return _pnode != it._pnode; // } //public: // node* _pnode;//封装一个节点的指针 //}; //链表类(控制哨兵位) template <class T> class list { public: typedef list_node<T> node; typedef __list_iterator<T, T&,T*> iterator; typedef __list_iterator<T, const T&,const T*> const_iterator; //typedef __list_const_iterator<T> const_iterator; //构造函数 void empty_initialize()//用于初始化哨兵位 { _head = new node; _head->_next = _head; _head->_prev = _head; _size = 0; } list() { empty_initialize(); } template <class InputIterator> list(InputIterator first, InputIterator last)//迭代器区间构造 { empty_initialize(); while (first != last) { push_back(*first); ++first; } } //析构函数 ~list() { clear(); delete _head; _head = nullptr; } //拷贝构造 list(const list<T>& lt) { 先初始化*this //empty_initialize(); //for (const auto& e : lt)//取lt的数据给e //{ // push_back(e); //} //工具人写法 list<T> tmp(lt.begin(),lt.end()); empty_initialize(); swap(tmp); } void swap(list<T>& lt) { std::swap(_head, lt._head);//交换头指针 std::swap(_size,lt._size); } 赋值运算符重载写法1 //list<T>& operator=(const list<T>& lt) //{ // //防止自己给自己赋值 // if (this != <) // { // clear(); // for (const auto& e : lt) // { // push_back(e); // } // } // return *this; //} // 赋值运算符重载写法2(赋值运算符重载都可以这么干) list<T>& operator=(list<T> lt) { swap(lt);//直接交换 return *this; } //插入删除 void push_back(const T& x) { /*node* newNode = new node(x); node* tail = _head->_prev; newNode->_prev = tail; newNode->_next = _head; tail->_next = newNode; _head->_prev = newNode;*/ insert(end(), x); } void pop_back() { erase(--end()); } void push_front(const T& x)//头插 { insert(begin(), x); } void pop_front() { erase(begin()); } iterator insert(iterator pos, const T& x) { node* newNode = new node(x); node* prev = pos._pnode->_prev; node* cur = pos._pnode; newNode->_prev = prev; newNode->_next = cur; prev->_next = newNode; cur->_prev = newNode; //return iterator(newNode); pos._pnode = newNode; ++_size; return pos; } iterator erase(iterator pos) { assert(!empty()); node* prev = pos._pnode->_prev; node* next = pos._pnode->_next; prev->_next = next; next->_prev = prev; delete pos._pnode; --_size; //return iterator(next); pos._pnode = next; return pos; } //链表小接口 bool empty()const { return _head->_next == _head; } void clear() { /*assert(!empty); node* cur = _head->_next; while (cur != _head) { node* next = cur->_next; delete cur; cur = next; }*/ iterator it = begin(); while (it != end()) { it = erase(it);//erase返回删除元素的下一个 } } size_t size()const { return _size; } //普通begin(),end()迭代器 iterator begin() { //return iterator(_head->_next); return __list_iterator<T, T&,T*>(_head->_next); } iterator end() { return __list_iterator<T, T&,T*>(_head); } //const begin(),end()迭代器 const_iterator begin()const { //return const_iterator(_head->_next); return __list_iterator<T, const T&,const T*>(_head->_next); } const_iterator end()const { return __list_iterator<T, const T&,const T*>(_head); } private: node* _head;//哨兵位 size_t _size;//用于统计节点个数,空间换时间 //不加这个私有变量,统计节点个数时间O(N),有这个私有变量,时间O(1),但是每个节点的体积变大 }; //测试函数 struct Pos { int _row; int _col; Pos(int row = 0, int col = 0) :_row(row) , _col(col) {} }; void test() { list<Pos> i; i.push_back(Pos(1, 2)); i.push_back(Pos(2, 5)); i.push_back(Pos(4, 3)); list<Pos>::iterator it = i.begin(); while (it != i.end()) { cout << (&( * it))->_row;//*it取数据,再取地址、解引用得到_row,多此一举 cout << it->_row;//同第三种写法,编译器为了可读性,省略了一个-> cout << it.operator->()->_row;//it.operator->()是显示调用,->_row是解引用得到_row it++; } } void test1() { list<std::vector<int>> i; std::vector<int> v1(1, 2); std::vector<int> v2(2, 4); std::vector<int> v3(3, 5); i.push_back(v1); i.push_back(v2); i.push_back(v3); list<std::vector<int>> m(i); i = m; cout << m.size(); } }
到此这篇关于利用C++模拟实现STL容器:list的文章就介绍到这了,更多相关C++ STL容器list内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!