【C++】list 模拟实现

一. 基本框架

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/rbeginend/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
底层:可动态增长的数组
增容:开新空间,拷贝数据,指向新空间

优点:

  1. 支持随机访问(因为它的每个元素在物理空间上是连续的)

缺点:

  1. 头部或中间的插入删除需要挪动数据。
  2. 增容代价较大(开新空间,拷贝数据,释放旧空间)

list
底层:带头双向循环链表
增容:需要一个节点就开辟一个节点,然后再把它链接到链表上。

优点:

  1. 任意位置插入删除的时间复杂度为 O(1)
  2. 增容代价较小

缺点:

  1. 不支持随机访问,访问节点的时间复杂度为 O(n)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值