【C++】list模拟实现

文章目录

    • 一. 基本框架
      • 1. 节点类的完整框架
      • 2. 迭代器类的基本框架
      • 3. list类的基本框架
    • 二. list类
      • 1. 和迭代器类有关的相关接口
        • 1.1 begin
        • 1.2 end
      • 2. list的修改操作接口
        • 2.1 insert
        • 2.2 erase
      • 3. 默认成员函数
        • 3.1 构造函数
        • 3.2 析构函数
        • 3.3 拷贝构造
        • 3.4 赋值重载
    • 三. 迭代器类
      • 1. 默认成员函数
        • 1.1 构造函数
        • 1.2 拷贝构造
      • 2. 指针操作接口
        • 2.1 解引用 (*)
        • 2.2 箭头接口(->)
        • 2.3 自增和自减
      • 3. 再次理解list迭代器
    • 四. vector和list区别

一. 基本框架

list的底层是带头双向循环链表,其基本功能的实现需要三个类模板(节点类,迭代器类,和list类)共同完成
【C++】list模拟实现_第1张图片

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;
}

模板参数的几点说明
【C++】list模拟实现_第2张图片

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类模板的成员变量都是指向节点的指针,不能直接搞成节点吗?

【C++】list模拟实现_第3张图片

二. list类

1. 和迭代器类有关的相关接口

1.1 begin

函数原型
iterator begin();
const_iterator begin() const;
作用
返回该list类对象第一个节点的迭代器

【C++】list模拟实现_第4张图片

// 非const对象就返回非const迭代器
iterator begin()
{
    //用第一个节点(即头结点的下一个节点)构造一个迭代器对象返回
	return iterator(_head->_next);
}

// const对象返回const迭代器
const_iterator begin() const
{
	return const_iterator(_head->_next);
}

begin的几点说明
【C++】list模拟实现_第5张图片

1.2 end

函数原型
iterator end();
const_iterator end() const;
作用
返回list类对象的头结点的迭代器

【C++】list模拟实现_第6张图片

iterator end()
{
	return iterator(_head);
}

const_iterator end() const
{
	return const_iterator(_head);
}

补充:being/rbeginend/rend的对比

【C++】list模拟实现_第7张图片

2. list的修改操作接口

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的几点说明
    【C++】list模拟实现_第8张图片
  • 复用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的迭代器失效分析
    【C++】list模拟实现_第9张图片
  • 复用erase实现pop_back
void pop_back()
{
	// end是获取到头结点,- -节获取到最后一个节点
	erase(--end());
}

3. 默认成员函数

3.1 构造函数

list()

作用:构造空的list,即只创建一个不存有效数据的头结点

list类的成员变量只有一个指向头结点的指针,创建一个list类对象,就是让它的成员变量_head指向一块我们手动用new开辟的节点类的空间。由于构造函数有好几种形式,都要让_head指向一块空间,我们让这个步骤在 GreatHeadNode()这个函数里实现了。

list()
{
	CreatHeadNode();
}

【C++】list模拟实现_第10张图片
list (InputIterator first, InputIterator last)

作用:这是一个函数模板,用其他list类的迭代器[first, last)区间(左闭右开)中的元素构造list。

template <class InputIterator>
list(InputIterator first, InputIterator last)
{
    // 先开头结点的空间
	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());
	// 利用std的swap交换头结点
	::swap(_head, tmp._head);
}

关于拷贝构造的几点说明
【C++】list模拟实现_第11张图片

3.4 赋值重载
list<T>& operator=(list<T> lt)
{
	::swap(_head, lt._head);
	return *this;
}

关于赋值重载的几点说明
【C++】list模拟实现_第12张图片

三. 迭代器类

这里我们再来看看迭代器类的基本框架

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)  //初始化列表初始化
{}

关于拷贝构造的几点说明
【C++】list模拟实现_第13张图片

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;
}

解引用的几点说明
【C++】list模拟实现_第14张图片

2.2 箭头接口(->)

就是返回节点中值的指针

//可读,可不可以写取决于迭代器类型(决定Ptr是 const T*还是 T*)
Ptr operator->()
{
	return &(operator*());
}

【C++】list模拟实现_第15张图片

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;
}

关于前置和后置的几点说明
【C++】list模拟实现_第16张图片

3. 再次理解list迭代器

区分这两个对象

  • Node* pnode
  • iterator it

当他们都指向同一个节点时,在物理内存上他们都存这个节点的地址,在物理上他们都是一样的。但是它们的类型不一样,那么它们的意义也就不一样,因为类型决定了对空间的使用权。
比如:
*pnode是一个指针的解引用,取到的值是这个节点本身。

*it是去调用这个迭代器的operator *(),返回的值是这个节点中值的引用。

四. vector和list区别

  • vector
    底层:可动态增长的数组
    增容:开新空间,拷贝数据,指向新空间

优点:

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

缺点:

  1. 头部或中间的插入删除需要挪动数据。
  2. 增容代价较大(开新空间,拷贝数据,释放旧空间,指向新空间)
  • list
    底层:带头双向循环链表
    增容:需要一个节点就开辟一个节点,在链接到链表上。

优点:

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

缺点:

  1. 不支持随机访问,访问节点的时间复杂度为O(N)

你可能感兴趣的:(C++)