文章目录
一. 基本框架
list 容器的底层是带头双向循环链表:
其基本功能的实现需要三个类模板(节点类,迭代器类,和 list 类)共同完成。
1. 节点类的完整框架
template<class T>
struct ListNode
{
// 默认的构造函数
ListNode(const T& x = T())
:_val(x)
, _prev(nullptr)
, _next(nullptr)
{}
T _val; // 存储的数据
ListNode<T>* _prev; // 指向前一个节点
ListNode<T>* _next; // 指向后一个节点
};
2. 迭代器类的基本框架
list 容器的迭代器不再是像 string 或者 vector 那样的原生指针了,首先因为 list 的各个节点在物理空间上不是连续的,我们不能直接对节点的地址进行 ++ / - - 得到其前、后位置的迭代器
并且我们的数据是保存在节点之中的,不能把节点的指针解引用直接得到里面数据。这些操作我们可以通过封装节点的地址形成一个迭代器类,然后重载这个类的 operator* 和 operator++ 等运算符及其他一系列方法,最终可以向操作指针一样去操作迭代器。
迭代器类的基本框架如下
template<class T, class Ref, class Ptr>
struct ListIterator
{
// 在迭代器类中还会用到如下模板类
// 为了方便写,我们给这些模板类重命名
typedef ListNode<T> ListNode;
typedef ListIterator<T, Ref, Ptr> self;
// 迭代器类的构造函数
ListIterator(ListNode* pnode)
:_node(pnode) // 初始化列表进行初始化
{}
ListNode* _node; // 指向一个节点
}
模板参数的几点说明:
3. list 类的基本框架
因为 list 类的底层是带头双向循环链表,所以我们只要知道头结点,就可以通过它的 _next 得到第一个节点,通过它的 _prev 得到最后一个节点,对实现链表的遍历和插入操作很方便。
对于 list 类,其成员变量就是一个头结点的指针,后面对链表的操作都通过这个头结点来完成。
template<class T>
class list
{
public:
typedef ListNode<T> ListNode;
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T*> const_iterator;
private:
ListNode* _head;// 指向哨兵位头节点
}
为什么迭代器模板类和 list 模板类的成员变量都是一个指向节点的指针,不能直接搞成节点吗?
二. list 类实现
1. 和迭代器相关的接口
1.1 begin
函数原型
iterator begin();
const_iterator begin() const;
作用
返回该 list 类对象第一个节点的迭代器
代码实现
// 非const对象就返回非const迭代器
iterator begin()
{
//用第一个节点(即头结点的下一个节点)构造一个迭代器对象返回
return iterator(_head->_next);
}
// const对象返回const迭代器
const_iterator begin() const
{
return const_iterator(_head->_next);
}
begin 补充说明
1.2 end
函数原型
iterator end();
const_iterator end() const;
作用
返回 list 类对象的头结点的迭代器
代码实现
iterator end()
{
return iterator(_head);
}
const_iterator end() const
{
return const_iterator(_head);
}
补充:being/rbegin 和 end/rend的对比
2. 修改操作接口
2.1 insert
函数原型
iterator insert (iterator position, const value_type& val);
作用
在 pos 这个迭代器位置插入一个值为 val 的节点,并返回这个新插入节点的迭代器。
代码实现
iterator insert(iterator pos, const T& x)
{
// 创建一个新的节点
ListNode* pnewnode = new ListNode(x);
// 获取该迭代器位置的节点的地址
ListNode* cur = pos._node;
// 获取前一个节点的地址
ListNode* prev = cur->_prev;
// 插入新节点
prev->_next = pnewnode;
pnewnode->_prev = prev;
pnewnode->_next = cur;
cur->_prev = pnewnode;
// 构造一个该新节点的迭代器,拷贝构造返回给外部
return iterator(pnewnode);
}
insert 补充说明
补充:复用 insert 实现 push_back 接口
void push_back(const T& x)
{
// 写法一(复用 insert 实现尾插)
insert(end(), x);
// 写法二(常规写法)
ListNode* pnewnode = new ListNode(x);
ListNode* tail = _head->_prev;
tail->_next = pnewnode;
pnewnode->_prev = tail;
_head->_prev = pnewnode;
pnewnode->_next = _head;
}
2.2 erase
函数原型
iterator erase (iterator position);
作用
删除指定位置节点,并返回其下一个节点的迭代器
iterator erase(iterator pos)
{
// 通过迭代器获取节点的地址
ListNode* cur = pos._node;
// 记录要删除节点的前后节点
ListNode* prev = cur->_prev;
ListNode* next = cur->_next;
// 删除节点
delete cur;
// 连接原来的前后节点
prev->_next = next;
next->_prev = prev;
// 构造下一个节点的迭代器对象并返回
return iterator(next);
}
erase 和 insert 迭代器失效情况分析
复用 erase 实现 pop_back
void pop_back()
{
// end 是获取到头结点,需要自减才是最后一个位置的节点
erase(--end());
}
3. 默认成员函数
3.1 构造函数
list()
作用:构造空的 list,即只创建一个不存有效数据的哨兵位头结点
list 类的成员变量只有一个指向头结点的指针,创建一个 list 类对象,就是让它的成员变量 _head 指向一块我们手动用 new 开辟的节点类的空间。由于构造函数有好几种形式,它们无疑都要让 _head 指向一块空间,我们把这个实现单独封装在 GreatHeadNode() 函数中。
list()
:_head(nullptr)
{
CreatHeadNode();
}
list (InputIterator first, InputIterator last)
作用:这是一个函数模板,用其他 list 类的迭代器 [first, last) 区间(左闭右开)中的元素来构造新的 list。
template <class InputIterator>
list(InputIterator first, InputIterator last)
:_head(nullptr)
{
// 先开头结点的空间
CreatHeadNode();
// 在头结点后尾插元素
while (first != last)
{
push_back(*first);
++first;
}
}
3.2 析构函数
// clear() 也一个成员函数,用来清理 list 对象中所有有效节点
void clear()
{
iterator it = begin();
// 遍历并删除每一个有效节点
while (it != end())
{
delete (it++)._node;
}
// 清理完所有有效节点后,更新头结点
_head->_prev = _head;
_head->_next = _head;
}
// 析构函数,复用 clear()
~list()
{
clear();
// 释放头结点
delete _head;
// 让 list 对象的 _head 指向 nullptr
_head = nullptr;
}
3.3 拷贝构造
list(const list& lt)
:_head(nullptr)
{
// 先开头结点空间
CreatHeadNode();
// 构造一个临时对象
list<T> tmp(lt.begin(), lt.end());
// 利用标准库的 swap 交换头结点
std::swap(_head, tmp._head);
}
关于拷贝构造的几点说明
3.4 赋值重载
list<T>& operator=(list<T> lt)
{
std::swap(_head, lt._head);
return *this;
}
关于赋值重载的几点说明
三. 迭代器类实现
下面是迭代器类的基本框架:
template<class T, class Ref, class Ptr>
struct ListIterator
{
// 类里还会用到这些模板类,为了简洁,我们这里给类名重定义
typedef ListNode<T> ListNode;
typedef ListIterator<T, Ref, Ptr> self;
ListNode* _node;
}
1. 默认成员函数
1.1 构造函数
迭代器类的成员变量只有一个指向节点地址的指针变量 _node,构造一个迭代器对象就传节点的地址,然后让 _node 指向这个地址。
ListIterator(ListNode* pnode)
:_node(pnode) // 初始化列表进行初始化
{}
1.2 拷贝构造
ListIterator(const self& it)
:_node(it._node) //初始化列表初始化
{}
关于拷贝构造的几点说明
2. 指针操作接口
因为物理空间上的不连续,迭代器就不是原生指针,所以不能拿到节点的地址直接进行解引用、自增、自减等操作。迭代器是对节点的地址进行封装,上述这些功能本生就是迭代器所需要完成的。
在这之前我们再来看看节点类的框架
template<class T>
struct ListNode
{
// 默认的构造函数
ListNode(const T& x = T())
:_val(x)
, _prev(nullptr)
, _next(nullptr)
{}
T _val; // 存储的数据
ListNode<T>* _prev; // 指向前一个节点
ListNode<T>* _next; // 指向后一个节点
};
2.1 解引用 (*)
返回节点中值的引用
Ref operator*()
{
// 直接返回节点的数据
return _node->_val;
}
解引用的几点说明
2.2 箭头接口(->)
就是返回节点中值的指针
//可读,但可不可以写取决于迭代器类型(看 Ptr 是 const T* 还是 T*)
Ptr operator->()
{
return &(operator*());
}
2.3 自增和自减
// 前置++(让迭代器对象的成员变量指向下一个节点,并返回它自己)
self& operator++()
{
_node = _node->_next;
return *this;
}
// 后置++(先构造一个它的迭代器,它自己指向下一个节点)
self operator++(int)
{
self tmp(_node);
_node = _node->_next;
return tmp;
}
// 前置- -
self& operator--()
{
_node = _node->_prev;
return *this;
}
// 后置- -
self operator--(int)
{
self tmp(_node);
_node = _node->_prev;
return tmp;
}
关于前置和后置的几点说明
3. 再次理解 list 迭代器
区分这两个对象
- Node* pnode
- iterator it
前者是一个节点的地址,后者是对前者这个地址进行了封装,其内部包含除了前者外,还有对前者进行一系列操作的方法,比如:*、++、-- 等。
它们的类型不一样,那么它们的意义也就不一样,因为类型决定了对这块空间的解释和使用权。
比如:
-
*pnode 是在对一个节点的地址进行解引用,返回的值是这个节点对象。
-
*it 是去调用这个迭代器的 operator *(),返回的值是这个节点对象中 _val 的引用。
四. vector 和 list 区别
vector
底层:可动态增长的数组
增容:开新空间,拷贝数据,指向新空间
优点:
- 支持随机访问(因为它的每个元素在物理空间上是连续的)
缺点:
- 头部或中间的插入删除需要挪动数据。
- 增容代价较大(开新空间,拷贝数据,释放旧空间)
list
底层:带头双向循环链表
增容:需要一个节点就开辟一个节点,然后再把它链接到链表上。
优点:
- 任意位置插入删除的时间复杂度为 O(1)
- 增容代价较小
缺点:
- 不支持随机访问,访问节点的时间复杂度为 O(n)