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 != <)
{
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的模拟实现