一、先决知识点1——认识list:
- list底层实现是双向链表,但是不是循环链表。
- list是否使用哨兵节点,是细节问题,C++标准并未规定。
- list是链表,他的优势在于对节点的操作会十分灵活,因此它在需要频繁插入和删除元素的情况下非常高效。
- list是链表的原因,他的元素分布不再是连续的空间,所以使用‘[ ]’来随机访问会使得性能消耗过大,所以C++标准不支持使用'[ ]'实现访问数据。
二、先决知识2——迭代器的分类:
- 根据迭代器的访问能力,可以将迭代器分为三类:单向迭代器、双向迭代器、随机访问迭代器。
- 单向迭代器:只支持++,例如单链表
- 双向迭代器:支持++和--,例如双向链表,红黑树
- 随机访问迭代器:支持++/--/+/- 等,例如vector,string
- 随机访问迭代器支持所有单向/双向迭代器的功能,因此可以向支持随机访问迭代器作为参数的函数,传递单向/双向迭代器作为参数。反之则不行。
三、简单的使用演示(vector/string中使用方法不变的不再赘述):
3.1排序——sort:
- list内部实现了sort方法,默认升序。
- 由于list是链表,他的sort在底层是归并排序而非快排。因此效率并不高,当数据量很大时,归并和快排的效率差距很大。数据量大时,先转换为vector排序后再转化为list可行。
3.2去重——unique:
- 要求list有序,可以先sort再unique。
3.3删除——remove:
- 删除所有指定值。
3.4转移——splice:
- 把一个list的节点摘下来插到另一个list
void splice (iterator position, list& x); void splice (iterator position, list& x, iterator i); void splice (iterator position, list& x, iterator first, iterator last);
四、底层功能实现(第一版,部分功能不是很完善,适合先了解逻辑):
4.1节点类:
- 节点类,每个链表节点包含三个成员,分别是节点数据、上一个节点地址、下一个节点地址。
- 创建哨兵节点时,哨兵节点不存储节点数据,所以可以使用缺省值;在C++中,内置类型也有默认构造,
T()
可以初始化一个类型为T
的对象,调用其默认构造函数template<class T>//模板 struct list_node//节点类 {T _data;list_node<T>* _next;list_node<T>* _prev;//一个指向上一个节点,一个指向下一个节点,一个存储节点值list_node<T>(const T& x = T())//内置类型也有匿名对象:_data(x), _prev(nullptr), _next(nullptr){} };
4.2迭代器类:
按照需求,实现的逻辑顺序:
1.重命名,简化书写:
typedef list_node<T> Node;//对节点对象重命名typedef __list_iterator<T> self;//对自己重命名
2.构造函数和成员:
- _node是一个指针,通过构造函数初始化,指向传过来的节点的地址。
Node* _node;//创建一个指针__list_iterator(Node* node):_node(node)//指针指向传递的节点对象{}
3.++/--的运算符重载:
- list是链表,要实现节点之间的迭代,就需要‘封装+运算符重载’。
- 后置++/--,加上一个参数int来和前置++/--构成函数重载;
- 由于后置++/--会创建临时对象,所以资源消耗会大于前置++/--,推荐使用前置代替后置。
- 后置++/--,返回的是临时对象,所以不能使用引用返回。
self& operator++()//向后挪动一个节点{_node = _node->_next;return *this;}self& operator--(){_node = _node->_prev;return *this;}self operator++(int){self tmp(*this);_node = _node->_next;return tmp;}self operator--(int){self tmp(*this);_node = _node->_prev;return tmp;}
4.*的运算符重载:
- 返回节点处的数据,如果使用引用返回,还可以修改节点数据。
T& operator*()//获取节点对象处值,引用返回{return _node->_data;}
5. ==和!=的运算符重载:
- 比较两个节点的地址是否相同。
bool operator!=(const self& s)//判断两节点地址是否相等{return _node != s._node;}bool operator==(const self& s)//判断两节点地址是否相等{return _node == s._node;}
6.完整代码:
template<class T>struct __list_iterator//迭代器对象{typedef list_node<T> Node;//对节点对象重命名typedef __list_iterator<T> self;//对自己重命名Node* _node;//创建一个指针__list_iterator(Node* node):_node(node)//指针指向传递的节点对象{}self& operator++()//向后挪动一个节点{_node = _node->_next;return *this;}self& operator--(){_node = _node->_prev;return *this;}self operator++(int){self tmp(*this);_node = _node->_next;return tmp;}self operator--(int){self tmp(*this);_node = _node->_prev;return tmp;}T& operator*()//获取节点对象处值,引用返回{return _node->_data;}bool operator!=(const self& s)//判断两节点地址是否相等{return _node != s._node;}bool operator==(const self& s)//判断两节点地址是否相等{return _node == s._node;}};
4.3链表类:
按照需求,实现的逻辑顺序:
1.私有成员:
- _head(哨兵节点) + _size(链表长度)
- 哨兵节点不存储数据。
- 链表长度‘_size’,在库中实现的list中并没有,加上这个私有成员,主要是方便返回链表的长度,无需再遍历一遍链表来计数链表长度。
private:Node* _head;//哨兵节点size_t _size;
2.重命名:
typedef list_node<T> Node;//重命名节点typedef __list_iterator<T> iterator;//重命名迭代器
3.无参构造函数:
- list() + empty_init()
- 库中的无参构造调用了一个empty_init()函数初始化哨兵节点。
- 初始化哨兵节点,将他的next和prev指针都指向自己即可,然后将链表长度初始化为0。
void empty_init()//无参构造初始化哨兵节点{_head = new Node;//开辟一个空节点_head->_next = _head;//头节点的next和prev都指向自己_head->_prev = _head;_size = 0;}list(){empty_init();}
4. 插入函数:
- 通过传过来的迭代器,在迭代器前面位置插入一个节点
- 成功插入节点后,链表长度加一
- 返回插入节点的迭代器,防止迭代器失效问题
iterator insert(iterator pos, const T& x){Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(x);newnode->_prev = prev;prev->_next = newnode;cur->_prev = newnode;newnode->_next = cur;++_size;return iterator(newnode);}
5.删除节点:
- 断开迭代器位置的节点,并将其前后两个节点相互连接。
- 删除节点后要将链表长度减一。
iterator erase(iterator pos){Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;delete cur;prev->_next = next;next->_prev = prev;--_size;return iterator(next);}
6.获取链表首尾迭代器:
- 首节点,就是哨兵节点的下一个节点。
- 尾节点,就是哨兵节点。
iterator begin(){return iterator(_head->_next);}iterator end(){return iterator(_head);}
7.头插,尾插:
- 头插,就是在哨兵节点的后一个节点插入节点,也就是begin()函数得到的迭代器的前一个位置插入节点。
- 尾插,就是在哨兵节点前插入节点。
- 复用insert即可。
void push_back(const T& x)//尾插,重点在于处理好头尾新节点的指针指向{//第一版,在没有insert的情况下实现的版本//Node* tail = _head->_prev;//Node* newnode = new Node(x);//newnode->_data = x;//newnode->_next = _head;//_head->_prev = newnode;//tail->_next = newnode;//newnode->_prev = tail;//第二版,复用insertinsert(end(), x);}void push_front(const T& x){insert(begin(), x);}
8.头删,尾删:
- 和头插,尾插操作的位置相同。
- 复用erase函数即可。
void pop_back(){erase(begin());}void pop_front(){erase(--end());}
9.清空list对象:
- 清理除了哨兵节点以外的所有节点。
- 创建一个迭代器指向第一个节点(哨兵节点后一个节点),当这个迭代器不和哨兵节点重合,就持续删除节点。
- 动态更新迭代器,由于erase会返回被删除节点的下一个节点,所以让迭代器每次都等于erase的返回值即可。
void clear(){iterator it = begin();while (it != end()){it = erase(it);}}
10.析构函数:
- 析构函数,是删除所有节点,包括哨兵节点。
- 复用clear函数后,再删除哨兵节点即可。
~list(){clear();delete _head;_head = nullptr;}
11.list对象节点数量:
size_t size(){return _size;}
12.拷贝构造:
- 先用empty_init函数初始化哨兵节点。
- 再将源对象的数据一个个插入目标对象即可。
list(list<T>& l){empty_init();for (auto a : l){push_back(a);}}
13.=的运算符重载:
- 两个版本,第一个版本先将目标对象的节点全部删除,然后将源对象每个节点的值插入目标对象即可。
- 第二个版本,实现一个swap函数,swap函数参数创建一个匿名对象,该匿名对象拷贝构造源对象;通过两个swap交换目标对象的哨兵节点和链表大小。交换完成后匿名对象被销毁。
list<T>& operator=(list<int> l){//if (*this != &l)//{// clear();// for (auto a : l)// {// push_back(a);// }//}//return *this;//版本二:调用swap函数swap(l);return *this;}void swap(list<T>& l){std::swap(_head, l._head);std::swap(_size, l._size);}
14.完整代码:
template<class T>class list//链表类{typedef list_node<T> Node;typedef __list_iterator<T> iterator;public:void empty_init()//无参构造初始化头节点{_head = new Node;//开辟一个空节点_head->_next = _head;//头节点的next和prev都指向自己_head->_prev = _head;_size = 0;}list(){empty_init();}list(list<T>& l){empty_init();for (auto a : l){push_back(a);}}~list(){clear();delete _head;_head = nullptr;}void clear(){iterator it = begin();while (it != end()){it = erase(it);}}list<T>& operator=(list<int> l){//if (*this != &l)//{// clear();// for (auto a : l)// {// push_back(a);// }//}//return *this;//版本二:调用swap函数swap(l);return *this;}void swap(list<T>& l){std::swap(_head, l._head);std::swap(_size, l._size);}void push_back(const T& x)//尾插,重点在于处理好头尾新节点的指针指向{//第一版,在没有insert的情况下实现的版本//Node* tail = _head->_prev;//Node* newnode = new Node(x);//newnode->_data = x;//newnode->_next = _head;//_head->_prev = newnode;//tail->_next = newnode;//newnode->_prev = tail;//第二版,复用insertinsert(end(), x);}void push_front(const T& x){insert(begin(), x);}void pop_back(){erase(begin());}void pop_front(){erase(--end());}iterator begin(){return iterator(_head->_next);}iterator end(){return iterator(_head);}iterator insert(iterator pos, const T& x){Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(x);newnode->_prev = prev;prev->_next = newnode;cur->_prev = newnode;newnode->_next = cur;++_size;return iterator(newnode);}iterator erase(iterator pos){Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;delete cur;prev->_next = next;next->_prev = prev;--_size;return iterator(next);}size_t size(){return _size;}private:Node* _head;//头节点size_t _size;};
五、const迭代器:
5.1const迭代器和普通迭代器区别:
- 常规迭代器允许遍历容器并修改容器中的元素。它的使用和指针类似,你可以解引用它来访问和修改元素。
- const迭代器又名常量迭代器,常量迭代器不允许修改容器中的元素。它只能用于读取元素。这种迭代器用于确保代码的安全性和可读性,防止意外修改元素。
- 两者的主要区别在于是否允许通过迭代器修改容器中的元素。
5.2const迭代器实现:
- 要适配const对象和非const对象,就需要写两个版本的迭代器分别对应const对象和非const对象。由于我们写的普通迭代器是一个模板,就需要再写一个const迭代器模板。但是实际上两个模板之间的许多是相同的。
- 我们可以通过添加模板参数,实现简化代码。
template<class T, class Ref, class Ptr>typedef __list_iterator<T, T&, T*> iterator; typedef __list_iterator<T, const T&, const T*> const_iterator;
- 通过ref和ptr就可以实现通过传过来的参数,实例化具体的模板种类。同一个类模板,会根据传过来的模板参数不同,实例化出不同的类。
- 就比如以上的两种传模板参数的方式,由于部分模板参数不同,实例化出的就是两个不同的类。
5.3修改*和[]运算符的重载:
- 由于我们不知道通过模板具体实例化出的是普通迭代器还是const版本的迭代器,所以我们通过模板参数来替代返回类型。
Ref operator*()//ref传过来的是T&或const T&{return _node->_data;}Ptr operator->()//ptr传过来的是T*或const T*{return &_node->_data;}
5.4添加begin()和end()的const版本:
const_iterator begin() const{return const_iterator(_head->_next);}const_iterator end() const{return const_iterator(_head);}
六、打印函数和他的模板:
- 我们如果要实现打印任意类型的list,就需要使用模板实现print_list()函数。
- 下面这样写,运行不通过。
template<typename T>void print_list(const list<T>& l){list<T>::const_iterator it = l.begin();while (it != l.end()){cout << *it << ' ';++it;}cout << endl;}
原因是:在C++中,当你在模板中使用依赖于模板参数的嵌套类型时,例如list<T>::const_iterator,编译器不知道这是一个静态成员或者一个静态函数还是一个类型。
因此需要使用
typename
关键字明确告诉编译器它是一个类型。template<typename T>void print_list(const list<T>& l){typename list<T>::const_iterator it = l.begin();while (it != l.end()){cout << *it << ' ';++it;}cout << endl;}
- 上面是只针对list的打印模板,下面我们升级以下,让这个打印模板可以打印任意类型。
template<typename Container>void print_container(const Container& con){typename Container::const_iterator it = con.begin();while (it != con.end()){cout << *it << ' ';++it;}cout << endl;}
七、第二版(添加了const迭代器),完整的头文件代码:
测试函数可以写在demo命名空间中,在测试文件的主函数调用,要注意在测试文件包含要调用到的库。
#pragma oncenamespace demo
{template<class T>//模板struct list_node//节点对象{T _data;list_node<T>* _next;list_node<T>* _prev;//一个指向上一个节点,一个指向下一个节点,一个存储节点值list_node<T>(const T& x = T())//内置类型也有匿名对象:_data(x), _prev(nullptr), _next(nullptr){}};template<class T, class Ref, class Ptr>//多加两个模板参数对应&和*的模板//template<class T>struct __list_iterator//迭代器对象{typedef list_node<T> Node;//对节点对象重命名typedef __list_iterator<T, Ref, Ptr> self;//typedef __list_iterator<T> self;//对自己重命名Node* _node;//创建一个指针__list_iterator(Node* node):_node(node)//指针指向传递的节点对象{}self& operator++()//向后挪动一个节点{_node = _node->_next;return *this;}self& operator--(){_node = _node->_prev;return *this;}self operator++(int){self tmp(*this);_node = _node->_next;return tmp;}self operator--(int){self tmp(*this);_node = _node->_prev;return tmp;}Ref operator*()//ref传过来的是T&或const T&{return _node->_data;}Ptr operator->()//ptr传过来的是T*或const T*{return &_node->_data;}bool operator!=(const self& s)//判断两节点地址是否相等{return _node != s._node;}bool operator==(const self& s)//判断两节点地址是否相等{return _node == s._node;}};template<class T>class list//链表对象{typedef list_node<T> Node;public:typedef __list_iterator<T, T&, T*> iterator;//普通迭代器的类typedef __list_iterator<T, const T&, const T*> const_iterator;//const迭代器的类//typedef __list_iterator<T> iterator;void empty_init()//无参构造初始化头节点{_head = new Node;//开辟一个空节点_head->_next = _head;//头节点的next和prev都指向自己_head->_prev = _head;_size = 0;}list(){empty_init();}list(const list<T>& l)//需要先实现const迭代器后,才能使用{empty_init();for (auto a : l){push_back(a);}}~list(){clear();delete _head;_head = nullptr;}void clear(){iterator it = begin();while (it != end()){it = erase(it);}}list<T>& operator=(list<int> l){//if (*this != &l)//{// clear();// for (auto a : l)// {// push_back(a);// }//}//return *this;//版本二:调用swap函数swap(l);return *this;}void swap(list<T>& l){std::swap(_head, l._head);std::swap(_size, l._size);}void push_back(const T& x)//尾插,重点在于处理好头尾新节点的指针指向{//第一版,在没有insert的情况下实现的版本//Node* tail = _head->_prev;//Node* newnode = new Node(x);//newnode->_data = x;//newnode->_next = _head;//_head->_prev = newnode;//tail->_next = newnode;//newnode->_prev = tail;//第二版,复用insertinsert(end(), x);}void push_front(const T& x){insert(begin(), x);}void pop_back(){erase(begin());}void pop_front(){erase(--end());}iterator begin(){return iterator(_head->_next);}iterator end(){return iterator(_head);}const_iterator begin() const//const迭代器{return const_iterator(_head->_next);}const_iterator end() const{return const_iterator(_head);}iterator insert(iterator pos, const T& x){Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(x);newnode->_prev = prev;prev->_next = newnode;cur->_prev = newnode;newnode->_next = cur;++_size;return iterator(newnode);}iterator erase(iterator pos){Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;delete cur;prev->_next = next;next->_prev = prev;--_size;return iterator(next);}size_t size(){return _size;}private:Node* _head;//头节点size_t _size;};template<typename Container>void print_container(const Container& con){typename Container::const_iterator it = con.begin();while (it != con.end()){cout << *it << ' ';++it;}cout << endl;}
}
八、list和vector的比较:
- list使用双向链表实现,节点存储不连续;vector使用动态数组实现,元素在内存中是连续存储的。
- vector支持随机访问,访问某个元素效率O(1);list不支持随机访问,访问某个元素
效率O(n)。- vector底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高 ;list底层节点动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低。
- list不会导致迭代器失效,vector删除、插入数据都会导致迭代器失效。