一、先决知识点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; //第二版,复用insert insert(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; //第二版,复用insert insert(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 once
namespace 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;
//第二版,复用insert
insert(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删除、插入数据都会导致迭代器失效。