list模拟实现

Posted DR5200

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了list模拟实现相关的知识,希望对你有一定的参考价值。

一. list接口函数总览

namespace lyp
{
	//模拟实现list当中的结点类
	template<class T>
	struct _list_node
	{
		//成员函数
		_list_node(const T& val = T()); //构造函数

		//成员变量
		T _val;                 //数据域
		_list_node<T>* _next;   //后继指针
		_list_node<T>* _prev;   //前驱指针
	};

	//模拟实现list迭代器
	template<class T, class Ref, class Ptr>
	struct _list_iterator
	{
		typedef _list_node<T> node;
		typedef _list_iterator<T, Ref, Ptr> self;

		_list_iterator(node* pnode);  //构造函数

		//各种运算符重载函数
		self operator++();
		self operator++(int);
		self operator--();
		self operator--(int);
		bool operator==(const self& s) const;
		bool operator!=(const self& s) const;
		Ref operator*();
		Ptr operator->();

		//成员变量
		node* _pnode; //一个指向结点的指针
	};

	//模拟实现list
	template<class T>
	class list
	{
	public:
		typedef _list_node<T> node;
		typedef _list_iterator<T, T&, T*> iterator;
		typedef _list_iterator<T, const T&, const T*> const_iterator;

		//默认成员函数
		list();
		list(const list<T>& lt);
		list<T>& operator=(const list<T>& lt);
		~list();

		//迭代器相关函数
		iterator begin();
		iterator end();
		const_iterator begin() const;
		const_iterator end() const;

		//访问容器相关函数
		T& front();
		T& back();
		const T& front() const;
		const T& back() const;

		//插入、删除函数
		void insert(iterator pos, const T& x);
		iterator erase(iterator pos);
		void push_back(const T& x);
		void pop_back();
		void push_front(const T& x);
		void pop_front();

		//其他函数
		size_t size() const;
		void resize(size_t n, const T& val = T());
		void clear();
		bool empty() const;
		void swap(list<T>& lt);

	private:
		node* _head; //指向链表头结点的指针
	};
}

二.结点类的模拟实现

list 的底层结构为带头双向循环链表,所以结点类里的成员变量有 T _val(数据),_list_node< T >* _prev(前驱指针),_list_node< T >* _next(后继指针),成员函数只需要构造函数即可(初始化数据,初始化指针为nullptr)

template<class T>
struct _list_node
{
  _list_node(const T& val = T())
  	:_val(val)
  	,_prev(nullptr)
  	,_next(nullptr)
  {}
	T _val;                // 数据
	_list_node<T>* _prev;  // 前驱指针
	_list_node<T>* _next; // 后继指针
}

三. 迭代器类模拟实现

前面的博客已经介绍过 vector 和 string 类的模拟实现,在 vector 和 string 中,迭代器均为原生指针,是因为vector和string底层实现均为数组,在使用迭代器进行遍历时可以支持 != 判断是否到末尾,++ 移动到下一数据,但list为带头双向循环链表,若迭代器采用原生指针,不支持 != 操作,且++后不能移动到下一数据(底层是链表,空间不连续),因此需要用一个类来封装指针,对上述操作的运算符进行重载,使list能够像vector/string一样使用同样的方式去进行遍历

迭代器的成员变量 : node* _pnode; //一个指向结点的指针
迭代器的成员函数 : 运算符的重载

可能有读者对模板参数比较疑惑,为什么会有三个模板参数呢?

template<class T,class Ref,class Ptr>

因为我们在list类中定义了两个迭代器类,普通迭代器类,const迭代器类(Ref为 T&/const T& 类型,Ptr为 T*/const T* 类型)

typedef _list_iterator<T,T&,T*> iterator;
typedef _list_iterator<T,const T&,const T*> const_iterator;

当我们使用普通迭代器对象时,实例化出普通迭代器类(iterator),使用const迭代器对象时,实例化出const迭代器类(const_iterator)

构造函数

对封装的指针进行初始化即可

_list_iterator(node* pnode)
	:_pnode(pnode)
{}

前置++和后置++

前置++/后置++都是将指针指向下一个数据,即 _pnode = _pnode->_next,前置++返回++之后的值,后置++返回原先的值,为了区分前置++和后置++,后置++比前置++多一个int参数

self 为当前迭代器类型

typedef _list_iterator<T,Ref,Ptr> self
// 前置++
self& operator++()
{
	_pnode = _pnode->_next;
	return *this;
}
// 后置++
self operator++(int)
{
	self tmp(*this);	
	_pnode = _pnode->_next;
	return tmp;
}

前置- -和后置- -

前置- -/后置- -都是将指针指向前一个数据,即 _pnode = _pnode->_prev,前置- -返回- -之后的值,后置- -返回原先的值,为了区分前置- -和后置- -,后置- -比前置- -多一个int参数

self 为当前迭代器类型

// 前置- -
self& operator--()
{
	_pnode = _pnode->_prev;
	return *this;
}
// 后置- -
self& operator--(int)
{
	self tmp(*this);
	_pnode = _pnode->_prev;
	return tmp;
}

==/!=运算符重载

==运算符重载
比较两个迭代器对象的_pnode指针指向是否相同

// ==运算符重载
bool operator==(const self& s)const
{
	return _pnode == s._pnode;
}

!=运算符重载
比较两个迭代器对象的_pnode指针指向是否不同

// !=运算符重载
bool operator!=(const self& s)const
{
	return _pnode != s._pnode;
}

* 运算符重载

重载 * 运算符的目的是为了得到迭代器对象的_pnode指针所指向的数据

// * 运算符重载
Ref operator*()
{
	return _pnode->_val;
}

-> 运算符重载

重载 -> 运算符的目的是当list中的数据是自定义类型时,我们可以通过 -> 来访问自定义类型的成员变量

// -> 运算符重载
Ptr operator->()
{
	return &_pnode->_val;
}

使用案例

class Date
{
public:
	int _year = 0;
	int _month = 1;
	int _day = 1;
}
int main()
{
	lt<Date> lt;
	lt.push_back(Date());
	list<Date>::iterator it = lt.begin();
	cout<<it->_year<<" "<<it->_month<<" "<<it->_day<<endl;
}

在这里编译器做了一些优化
it->_year 本来应该是 it->->_year ,第一个->去调用operator->重载函数返回T*的指针,第二个->用来去访问自定义类型的成员变量,但是两个箭头程序的可读性较差,所以编译器做了优化,省略了一个箭头

list模拟实现

构造函数

list是一个带头双向循环链表,构造函数的目的是创建一个头结点,_next 和 _prev都指向自己

list()
{
	_head = new node; // 申请一个头结点
	_head->_next = _head; // 后继指针指向自己
	_head->_prev = _head; // 前驱指针指向自己
}

拷贝构造函数

1). 申请一个头结点,_next 和 _prev都指向自己
2). 将 lt 中的数据拷贝到新构造的容器中

list(const list<T>& lt)
{
	_head = new node; // 申请一个头结点
	_head->_next = _head; // 后继指针指向自己
	_head->_prev = _head; // 前驱指针指向自己
	for(const auto& e : lt) // 拷贝到新构造的容器中
	{
		push_back(e);
	}
}

赋值运算符重载

传统写法 :
1). 释放除了头结点以外的结点
2). 将 lt 中的数据拷贝到新构造的容器中

list<T>& operator=(const list<T>& lt)
{
  // 防止自己给自己赋值
	if(this != &lt)
	{
		clear(); // 清空数据
		for(const auto& e : lt) // 拷贝到新构造的容器中
		{
			push_back(e);
		}
	}
	return *this; // 支持连续赋值
}

现代写法 :
1). 拷贝构造出 lt 对象
2). 交换 this 和 lt 的 _head 指针,出了作用域,lt 调用析构函数,释放掉原this的结点

list<T>& operator=(list<T> lt) // 拷贝构造
{
	std::swap(_head,lt._head); // 交换指针
	return *this; // 支持连续赋值
}

析构函数

1). 使用 clear() 释放除了头结点以外的结点
2). 释放掉头结点

~list()
{
	clear(); // 释放除了头结点以外的结点
	delete _head; // 释放掉头结点
	_head = nullptr; // 置空头指针
}

begin和end

begin() :
构造出指针指向第一个结点的迭代器对象
end() :
构造出指针指向头结点的迭代器对象

iterator begin()
{
	return iterator(_head->_next);
}
const_iterator begin()const
{
	return const_iterator(_head->_next);
}
iterator end()
{
	return iterator(_head);
}
const_iterator end()const
{
	return const_iterator(_head);
}

front/back

front() :
返回第一个结点数据的引用
end() :
返回最后一个结点数据的引用

T& front()
{
	return *begin(); 
}
const T& front()const
{
	return *begin(); 
}
T& end()
{
	return *(--end()); 
}
const T& end()const
{
	return *(--end()); 
}

insert

1). 新开辟一个结点newnode(值为val),得到当前结点的指针,前驱结点的指针
2). 前驱结点的_next 指向 newnode,newnode的_prev指向前驱结点
3). newnode的_next 指向当前结点,当前结点的_prev指向newnode

void insert(iterator pos,const T& val)
{
		assert(pos._pnode); // 断言指针不为空
		node* cur = pos._pnode; // 当前结点指针
		node* prev = pos._pnode->_prev; // 前驱结点指针
		node* newnode = new node(val); // 新开辟结点

		prev->_next = newnode; // 前驱结点的_next 指向 newnode
		newnode->_prev = prev; // newnode的_prev指向前驱结点
		newnode->_next = cur;  // newnode的_next 指向当前结点
		cur->_prev = newnode;  // 当前结点的_prev指向newnode
}

erase

1). 得到前驱结点和后继结点的指针
2). 前驱结点的_next 指向后继结点
3). 后继结点的_prev指向前驱结点
4). 删除当前结点,返回删除位置的下一个位置

iterator erase(iterator pos)
{
	assert(!empty()); // 链表不为空
	assert(pos._pnode != _head); // 不能删头结点
	node* prev = pos._pnode->_prev; // 前驱结点指针
	node* next = pos._pnode->_next; // 后继结点指针
	prev->_next = next; // 前驱结点的_next 指向后继结点
	next->_prev = prev; // 后继结点的_prev指向前驱结点
	delete pos._pnode;  // 删除当前结点
	return iterator(next); // 返回删除位置的下一个位置
}

push_back/pop_back/push_front/pop_front

这四个函数都可以复用 insert 和 erase 函数

push_back :
尾插即在头结点前插入一个结点
pop_back :
尾删,删除最后一个结点
push_front :
头插即在第一个结点前插入一个结点
pop_front :
头删,删除第一个结点

void push_back(const T& val)
{
	insert(end(),val);
}
void pop_back()
{
	erase(--end());
}
void push_front(const T& val)
{
	insert(begin(),val);
}
void pop_front()
{
	erase(begin());
}

size

获取链表中有效结点的个数,遍历一次链表即可得到,但这是O(n)的时间复杂度,如果要频繁的使用size接口,可以在list类中加入_size成员变量

size_t size() const
{
	size_t sz = 0; //统计有效数据个数
	const_iterator it = begin(); //获取第一个有效数据的迭代器
	while (it != end()) //通过遍历统计有效数据个数
	{
		sz++;
		it++;
	}
	return sz; //返回有效数据个数
}

clear

只保留头结点,遍历链表调用erase接口进行删除,注意调用erase后 it 迭代器就已经失效了

void clear()
{
	iterator it = begin();
	while(it != end())
	{
		it = erase(it);
	}
}

empty

判断容器是否为空

bool empty()const
{
	return begin() == end();
}

swap

交换容器的头指针即可

void swap(list<T>& lt)
{
	std::swap(_head,lt._head);
}

resize

1). 若n大于当前容器的size,则尾插结点,直到size等于n为止。
2). 若n小于当前容器的size,则只保留前n个结点。

void resize(size_t n, const T& val = T())
{
		size_t len = size();
		if (n < len)
		{
			size_t count = len - n;
			while (count--)
			{
				pop_back();
			}
		}
		// n > len
		else
		{
			size_t count = n - len;
			while (count--)
			{
				push_back(val);
			}
		}
}

以上是关于list模拟实现的主要内容,如果未能解决你的问题,请参考以下文章

c++:stl中list的模拟实现

C++ 初阶List底层框架模拟实现

C++ 初阶List底层框架模拟实现

C++初阶第十一篇——list(list常见接口的用法与介绍+list的模拟实现+list迭代器原理)

模拟实现STL库

Java模拟数据量过大时批量处理数据实现