STL序列式容器之list的使用及实现

std::list 和 std::vector 是两种不同的数据结构,std::vector 是基于数组的动态数组,而 std::list 是基于双向链表的数据结构。list适用于需要在序列中频繁执行插入和删除操作的场景。

1.list的特性

双向链表: list是一个双向链表,允许在序列的两端和中间执行高效的插入和删除操作。

不支持随机访问: 与vector不同,list不支持通过索引进行常量时间内的随机访问。要访问list中的元素,必须通过迭代器进行。

动态内存管理: list的内部实现使用节点,每个节点都包含一个元素和指向前后节点的指针。这种结构使得list在执行插入和删除操作时能够更好地管理内存。

保持迭代器有效性: list在进行插入和删除操作时,能够更好地保持迭代器的有效性。这意味着在进行这些操作后,不会导致所有迭代器失效。

高效的插入和删除操作: 由于list是双向链表,插入和删除操作在两端和中间都是常量时间的,使其成为处理这类操作的理想容器。

2.list的性能考虑

插入和删除操作: 如果主要进行频繁的插入和删除操作,并且不需要随机访问元素,list可能比vector更为高效。

随机访问: 如果需要通过索引进行随机访问元素,使用vector可能更为合适,因为它提供了常量时间的随机访问。

内存使用: 由于list使用了链表结构,可能引入一些额外的内存开销。在内存使用方面,vector可能更为紧凑。

3.C++标准库中list的基本用法

3.1 头文件

要使用list,首先需要包含相关的头文件:

cpp 复制代码
#include <list>

3.2 声明list对象

cpp 复制代码
std::list<int> myList;

3.3 list的构造函数

list的构造函数包括4种:

cpp 复制代码
//1、构造的list中包含n个值为val的元素
list (size_type n, const value_type& val = value_type())

//2、构造空的list
list()

//3、拷贝构造函数
list (const list& x)

//4、用[first, last)区间中的元素构造list
list (InputIterator first, InputIterator last)

除了上述4种构造函数外,还可以使用数组作为迭代器区间构造链表,具体用法如下述代码所示:

cpp 复制代码
//1、list的构造函数
void test01()
{
	list<int>l1;//构造一个空对象l1
	list<int>l2(4, 100);//链表l2中放入4个100
	list<int>l3(l2.begin(), l2.end());//用l2的[begin(),end() )左开右闭的区间构造l3
	list<int>l4(l3);//使用l3拷贝构造l4

	//使用数组作为迭代器区间构造链表l5
	int array[] = { 12,3,66,88 };
	list<int>l5(array, array + sizeof(array) / sizeof(int));

	//列表格式初始化C++11
	list<int>l6{ 2,4,6,8,10,12 };

	//用迭代器方式打印l5中的元素
	list<int>::iterator it = l5.begin();
	while (it != l5.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;

	//范围for遍历--C++11
	for (auto e : l5)
	{
		cout << e << " ";
	}
	cout << endl;
}

3.3 list迭代器的使用

这里介绍list的正向迭代器、反向迭代器和const迭代器的使用。

cpp 复制代码
//正向迭代器,对迭代器执行++操作,迭代器向后移动
iterator begin();//返回第一个元素的迭代器
iterator end();//返回最后一个元素下一个位置的迭代器

//反向迭代器,对迭代器执行++操作,迭代器向前移动
reverse_iterator rbegin();//返回第一个元素的reverse_iterator,即end位置
reverse_iterator rend();//,返回最后一个元素下一个位置的reverse_iterator,即begin位置

//const迭代器
const_iterator begin() const;//const对象只能调用const迭代器
 

具体用法如下:

cpp 复制代码
//2、list的迭代器
//注意:链表的遍历只能用迭代器和范围for
void PrintList(const list<int>& l)
{
	list<int>::const_iterator it = l.begin();
	while (it != l.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

void test02()
{
	//1、使用数组作为迭代器区间构造链表l
	int array[] = { 12,33,45,67 };
	list<int>l(array, array+sizeof(array) / sizeof(array[0]));

	//2、使用正向迭代器正向遍历链表list中的元素
	//list<int>::iterator it = l.begin(); //c++98中的语法
	auto it = l.begin();//c++11之后的推荐写法
	while (it != l.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;

	//3、使用反向迭代器逆向打印l中的数据
	//list<int>::reverse_iterator rit = l.rbegin();
	auto rit = l.rbegin();
	while (rit != l.rend())
	{
		cout << *rit << " ";
		++rit;
	}
	cout << endl;
}

3.4 list的插入和删除操作

list的插入和删除操作包括:尾插push_back、尾删pop_back、头插push_front、头删pop_front四个操作。

cpp 复制代码
//1、尾插
void push_back (const value_type& val);

//2、尾删
void pop_back();

//3、头插
void push_front (const value_type& val);

//4、头删
void pop_front();

具体用法如下代码所示:

cpp 复制代码
//3、list的插入和删除
//push_back/pop_back/push_front/pop_front
void test03()
{
	int array[] = { 12,13,14 };
	list<int>l(array, array + sizeof(array) / sizeof(array[0]));
	PrintList(l);
	
	//在list的尾部插入55,头部插入11
	l.push_back(55);
	l.push_front(11);
	PrintList(l);

	//删除list的尾结点和头结点
	l.pop_back();
	l.pop_front();
	PrintList(l);
}

3.5 在指定位置插入和删除结点

在指定位置插入和删除结点包括insert和erase两个操作。

cpp 复制代码
//1、在position位置前插入值val的结点
iterator insert (iterator position, const value_type& val);

//2、在position位置前插入n个值为val的结点
void insert (iterator position, size_type n, const value_type& val);

//3、在position位置前插入[first,last)区间中的元素
template <class InputIterator>    
void insert (iterator position, InputIterator first, InputIterator last);

//4、删除position位置上的元素
iterator erase (iterator position);

//5、删除list中[first,last)区间中的元素
iterator erase (iterator first, iterator last);

具体使用案例如以下代码所示:

cpp 复制代码
//4、在指定位置插入和删除结点
//insert/erase
void test04()
{
	int array[] = { 1,2,3 };
	list<int>l(array, array + sizeof(array) / sizeof(array[0]));

	//获取list中第二个结点
	auto pos = ++l.begin();
	cout << *pos << endl;

	//在pos前插入值为4的元素
	l.insert(pos,4);
	PrintList(l);

	//在pos前插入6个值为8的元素
	l.insert(pos, 6, 8);
	PrintList(l);

	//在pos前插入[v.begin(),v.end() )区间中的元素
	vector<int>v{ 10,11,12 };
	l.insert(pos, v.begin(), v.end());
	PrintList(l);

	//删除pos位置上的元素
	l.erase(pos);
	PrintList(l);

	//删除list中[begin(),end() )区间中的元素,这里即指删除list中的所有元素
	l.erase(l.begin(), l.end());
	PrintList(l);
}

3.6 list中size、swap、clear、empty的用法

cpp 复制代码
//1、返回list中有效结点的个数
size_type size() const;

//2、交换两个list中的元素
void swap (list& x);

//3、清空list中的有效元素
void clear();

//4、检测list是否为空,是返回true,否则返回false
bool empty() const;

使用案例如以下代码所示:

cpp 复制代码
//5、
//size:返回list中有效结点的个数
//swap:交换两个list中的元素
//clear:清空list中的有效元素
//empty:检测list是否为空,是返回true,否则返回false
void test05()
{
	int array[] = { 11,12,13,14,15 };
	list<int>l1(array, array + sizeof(array) / sizeof(array[0]));
	PrintList(l1);

	//交换l1和l2中的元素
	list<int>l2(2, 100);
	l1.swap(l2);
	PrintList(l1);
	PrintList(l2);

	//使用clear函数将l2中的元素清空
	//使用size函数返回l1和l2中有效结点的个数
	l2.clear();
	cout << l2.size() << endl;
	cout << l1.size() << endl;

	//使用empty函数返回链表是否为空
	list<int>l3;
	int sum(0);

	for (int i = 0; i <=10; ++i)
		l3.push_back(i);

	while (!l3.empty())
	{
		sum += l3.front();
		l3.pop_front();
	}
	cout << "total:" << sum << endl;
}

3.7 front和back的用法

cpp 复制代码
//1、返回list的第一个结点中值的引用
reference front();
const_reference front() const;

//2、返回list的最后一个结点中值的引用
reference back();
const_reference back() const;

具体使用案例如下:

cpp 复制代码
//6、front:返回list的第一个结点中值的引用
//   back:返回list的最后一个结点中值的引用
void test06()
{
	//front
	list<int>mylist;

	mylist.push_back(77);
	mylist.push_back(22);

	mylist.front() -= mylist.back();
	cout << "mylist.front() is now " << mylist.front() << endl;

	//back
	mylist.push_back(10);

	while (mylist.back() != 0)
	{
		mylist.push_back(mylist.back() - 1);
	}

	cout << "mylist contains: ";
	for (list<int>::iterator it = mylist.begin(); it != mylist.end(); ++it)
		cout << *it <<" ";
	cout << endl;
}

list的基本用法可参考:list的基本用法

4.list的模拟实现

4.1 list与vector的区别

如前文所述,list的模拟实现与vector相比,略复杂一点:

(1)list节点是一个结构体,包含数据域和指针域,将节点的相关操作放入节点类进行处理,逻辑更清晰;

(2)vector的元素在空间上是连续分布的,迭代器++就能指向下一个元素,但list的迭代器不行,它的每个元素在空间上都不连续,要访问下一个节点必须找到当前节点的next指针,因此list的迭代器必须重写。

4.2 list的具体框架

4.2 list节点类的实现

由上文中list的具体框架可知,list本身和list的节点是不同的结构,两者进行了分开设计,以下是list的节点(node)结构:

cpp 复制代码
template<class T>
struct __list_node
{
	__list_node<T>* _next;//指向下一个结点的指针
	__list_node<T>* _prev;//指向上一个结点的指针
	T _data;//数据

	//构造函数
	__list_node(const T& val = T())
		:_next(nullptr)
		, _prev(nullptr)
		, _data(val)
	{}
};

4.3 list的迭代器

如前文所述,list不像vector一样以普通指针作为迭代器,因为其节点不保证在储存空间中连续存在。list迭代器必须有能力指向list的节点,并有能力进行正确的递增、递减、取值、成员存取等操作。list的递增、递减、取值、成员存取操作是指,递增时指向下一个节点,递减时指向上一个节点,取值时取的是节点的数据值,成员取用时取用的是节点的成员。由于节点的指针原生行为不满足迭代器定义,这里的迭代器通过类来封装节点的指针重载运算符,如operator*、operator->、operator++等。

迭代器分为普通迭代器和const迭代器,对于__list_iterator类要实现两个版本,一个是普通的iterator,另一个是const版本的const_iterator。区别在于:对于两个类中的部分函数有普通函数和const函数之分(如begin( )和end( )),其他并无区别。因为这两个类的大部分代码相似,会造成代码冗余,如何解决代码冗余的问题呢?

对于T&,类模板实例化出两个类,一个是T&类,一个是const T&类,同理,T*也一样。使用类模板就会实例化出来两个类,一个是普通的不带const的T,T&, T*,另一个是带const的T,const T&, const T*,其中Ref是引用,Ptr是指针,该类模板实例化了以下这两个类模板:

cpp 复制代码
template<class T,class Ref,class Ptr>
__list_iterator<T,T&,T*>  对应的是一个普通的迭代器
__list_iterator<T, const T&, const T*> 对应的是一个const迭代器 const_iterator

4.3.1 __list_iterator类

迭代器操作list的节点,需要一个指向链表节点的指针。

cpp 复制代码
//list迭代器
//__list_iterator<T,T&,T*>  对应的是一个普通的迭代器
//__list_iterator<T, const T&, const T*> 对应的是一个const迭代器 const_iterator
template<class T,class Ref,class Ptr>
struct __list_iterator
{
	typedef __list_node<T> node;
	typedef __list_iterator<T,Ref, Ptr> Self;
	node* _node;//指向链表节点的指针
}

4.3.2 迭代器类的构造函数

构造函数初始化指向节点的指针。

cpp 复制代码
__list_iterator(node* node)
	:_node(node)
{}

4.3.3 operator*运算符重载

cpp 复制代码
//*it 解引用
Ref operator*() 
{
	return _node->_data;
}

4.3.4 operator-> 运算符重载

cpp 复制代码
//it->
Ptr operator->()
{
	return &_node->_data;
}

4.3.5 前后置++、--

cpp 复制代码
//++it  迭代器前置++,返回++后的迭代器
Self operator++()
{
	_node = _node->_next;
	return *this;
}

//it++ 后置++返回的是++之前的值
Self operator++(int)
{
	Self tmp(*this);
	//_node = _node->_next;
	++(*this);

	return tmp;
}

//--it  迭代器前置--,返回--后的迭代器
Self operator--()
{
	_node = _node->_prev;
	return *this;
}

//it-- 后置--返回的是--之前的值
Self operator--(int)
{
	Self tmp(*this);
	//_node = _node->_prev;
	--(*this);

	return tmp;
}

4.3.6 operator==、operator!=重载

cpp 复制代码
//it!=end() 当前迭代器it与迭代器end()比较
bool operator!=(const Self& it)
{
	return _node != it._node;
}

//it==end() 当前迭代器it与迭代器end()比较
bool operator==(const Self& it)
{
	return _node == it._node;
}

4.3.7 list迭代器失效

4.4 list类的实现

list的成员需要一个头节点,并通过迭代器访问其他节点元素。

cpp 复制代码
template<class T>
class MyList
{
	typedef __list_node<T> node;

public:
	//普通迭代器
	typedef __list_iterator<T, T&, T*> iterator;
	
	//const_iterator迭代器的实现
	typedef __list_iterator<T, const T&, const T*> const_iterator;

private:
    node* _head;
};

4.4.1 迭代器

cpp 复制代码
//迭代器
//begin()是双向循环链表头结点下一个位置的节点
iterator begin()
{
	return iterator(_head->_next);
}

//end()是链表最后一个节点的下一个位置,即头节点的位置
iterator end()
{
	return iterator(_head);
}

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

//const迭代器
const_iterator end()const
{
	return const_iterator(_head);
}

4.4.1 构造函数

cpp 复制代码
//1、带头双向循环链表,构造函数
MyList()
{
	_head = new node;
	_head->_next = _head;
	_head->_prev = _head;
}

4.4.2 拷贝构造函数

cpp 复制代码
//3、拷贝构造 lt2(lt1)
MyList(const MyList<T>& lt)
{
	//先创建一个新的只有头结点的链表
	_head = new node;
	_head->_next = _head;
	_head->_prev = _head;

	//将链表lt各结点中的数据插入新创建的链表中
	/*const_iterator it = lt.begin();
	while (it != lt.end())
	{
		push_back(*it);
		++it;
	}*/

	//也可将以上迭代器循环换成范围for循环
	for (auto e : lt)
	{
		push_back(e);
	}
}

4.4.3 operator=赋值运算符重载

cpp 复制代码
//4、赋值 lt1=lt3
//写法1
MyList<T>& operator=(const MyList<T>& lt)
{
	if (this != &lt)
	{
		for (auto e : lt)
		push_back(e);
	}

	return *this;
}

//5、写法2:赋值的常用写法 lt1=lt3
MyList<T>& operator=(MyList<T> lt)
{	
	swap(_head, lt._head);
	return *this;
}

4.4.4 clear()

clear函数只清除链表中所有节点内容,不删除头节点,如果删除头节点那么链表就不存在了,这是链表的析构函数完成的操作。

cpp 复制代码
//清理链表,保留头结点
void clear()
{
	iterator it = begin();
	while (it != end())
	{
		erase(it++);
	}
}

4.4.5 erase()

移除pos所指节点,并更新指针的指向,即指向pos所指节点的下一个节点。

cpp 复制代码
//4、erase() 在指定位置删除数据
iterator erase(iterator pos)
{
	//注意不能删除头结点
	assert(pos != end());

	node* cur = pos._node;
	node* curPrev = cur->_prev;
	node* curNext = cur->_next;
	delete cur;

	curPrev->_next = curNext;
	curNext->_prev = curPrev;

	return iterator(curNext);
}

4.4.6 insert()

insert函数,在pos之前插入节点newNode。

cpp 复制代码
//5、insert函数,在pos之前插入结点newNode
void insert(iterator pos, const T& val)
{
	node* cur = pos._node;
	node* prev = cur->_prev;
	node* newNode = new node(val);

	prev->_next = newNode;
	newNode->_prev = prev;
	newNode->_next = cur;
	cur->_prev = newNode;
}

4.4.7 push_back()

在链表的尾部插入一个节点。

cpp 复制代码
//6、尾插
void push_back(const T& val)
{
	node* tail = _head->_prev;
	node* newNode = new node(val);

	tail->_next = newNode;
	newNode->_prev = tail;

	_head->_prev = newNode;
	newNode->_next = _head;
}

也可以复用insert()函数来实现push_back()函数。

cpp 复制代码
void push_back(const T& val)
{	
	//end()是链表结尾的下一个位置,即头结点的位置
	insert(end(), val);
}

4.4.8 push_front()

在链表头部插入数据,可以直接复用insert()函数。

cpp 复制代码
//7、push_front头插
void push_front(const T& val)
{
	insert(begin(), val);
}

4.4.9 pop_back()

尾删

cpp 复制代码
//8、尾删
void pop_back()
{
	//两种写法
	//erase(iterator(_head->_prev));
	erase(--end());
}

4.4.10 pop_front()

头删

cpp 复制代码
//9、头删
void pop_front()
{
	erase(begin());
}

4.4.11 empty()

判空

cpp 复制代码
//10、判空empty()
bool empty()
{
	return begin() == end();
}

4.4.12 求链表节点的个数

cpp 复制代码
//11、求链表的节点个数
size_t size()
{
	size_t count = 0;
	iterator it = begin();
	while (it != end())
	{
		++it;
		++count;
	}

	return count;
}

完整代码可参考:list的模拟实现

相关推荐
时光の尘1 分钟前
C语言菜鸟入门·关键字·float以及double的用法
运维·服务器·c语言·开发语言·stm32·单片机·c
我们的五年5 分钟前
【Linux课程学习】:进程描述---PCB(Process Control Block)
linux·运维·c++
以后不吃煲仔饭15 分钟前
Java基础夯实——2.7 线程上下文切换
java·开发语言
进阶的架构师16 分钟前
2024年Java面试题及答案整理(1000+面试题附答案解析)
java·开发语言
前端拾光者20 分钟前
利用D3.js实现数据可视化的简单示例
开发语言·javascript·信息可视化
程序猿阿伟21 分钟前
《C++ 实现区块链:区块时间戳的存储与验证机制解析》
开发语言·c++·区块链
傻啦嘿哟39 分钟前
如何使用 Python 开发一个简单的文本数据转换为 Excel 工具
开发语言·python·excel
大数据编程之光43 分钟前
Flink Standalone集群模式安装部署全攻略
java·大数据·开发语言·面试·flink
初九之潜龙勿用44 分钟前
C#校验画布签名图片是否为空白
开发语言·ui·c#·.net
爱摸鱼的孔乙己1 小时前
【数据结构】链表(leetcode)
c语言·数据结构·c++·链表·csdn