list可以看作是双向循环链表,list跟前面的vector和string使用方法大多相同,这里主要是模拟实现。
因为底层为链表的数据结构,是非线性的,所以其迭代器不能使用原生指针,要重新定义迭代器的操作,这里非常重要,因为后面大多数写法跟这里差不多。
一、list 部分函数
转移数据,把后一个链表数据转移到前另一个链表


删除指定数据


数据去重

用list操作去重操作,效率并不高,后面又更高效的去重类型。

排序

list 底层使用的是归并排序,不用消耗额外的空间。默认是升序,可以改。
二、模拟实现 list
list 包含了类和对象的很多基本要点,要是类和对象不熟悉的话,学和写都会比较困难,难以理解。
首先这里把节点、链表、迭代器,分成了三个部分(三个类)来写。
节点:
cpp
//节点
template<class T>
class list_node
{
public:
T _data;
list_node<T>* _next;
list_node<T>* _pre;
//拷贝构造
list_node(const T& x = T())
:_data(x) //自定义类型去调用自己的拷贝构造
, _next(nullptr)
, _pre(nullptr)
{
}
};
这里链表节点的写法没什么要点,跟 前面C语言链表节点的写法大同小异。因为不知道节点具体存储的什么数据,所以这里使用了模板,使其变的更为灵活。
迭代器:
cpp
//迭代器
template<class T>
struct __list_iterator //!注意这因为这里要经常使用迭代器的成员,所以定义成了结构体,当然也可以使用class
{
typedef list_node<T> Node;
//缩减一下长度
typedef __list_iterator<T> iterator;
Node* _node;
}
PS: Node、iterator 共用同一个模板参数 T 。
跟vector不同,vector的迭代器是原生指针,其迭代器的操作可以直接当指针用。
这里不能使用原生指针,list 是一个非线性的容器。且 node* 解引用得到的是节点,但是这里迭代器解引用要得到是节点中的数据(_data),所以这里迭代器的操作要重新写。
这里拷贝构造函数可写可不写,直接用默认的浅拷贝就行,把一个迭代器赋值给另一个迭代器,这里希望的就是另一个迭代器也指向同一个节点。使用深拷贝反而不符合其本意。
迭代器类不用写析构函数,因为这里节点是属于链表的,这里只是封装起来进行操作。
构造函数:
cpp
//构造函数 使用节点的指针
__list_iterator(Node* node)
:_node(node)
{
}
迭代器的精华,用一个节点的指针就能定义迭代器。
迭代器操作:
cpp
iterator begin()
{
return iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}

PS:这里迭代器是开区间(左闭右开),end() 是链表尾部的下一个位置,所以end()指向哨兵头节点的位置,begin()指向哨兵头节点的下一个位置。
操作符重载:
cpp
bool operator!=(const iterator& it)const
{
return _node != it._node;
}
bool operator==(const iterator& it)const
{
return _node == it._node;
}
PS:因为 list 是一个非线性的表,其节点指针位置是不确定的,有可能不再同一段内存空间,所以并不能使用 大于(>)、小于(<)等比较符号,所以也不用重载它们。
cpp
//++it 前置
iterator& operator++()
{
_node = _node->_next;
return *this;
}
//it++ 后置
iterator operator++(int)
{
iterator temp(*this);
_node = _node->_next;
return temp;
}
iterator& operator--()
{
_node = _node->_pre;
return *this;
}
iterator operator--(int)
{
iterator temp(*this);
_node = _node->_pre;
return temp;
}
解引用、箭头重载:
cpp
//重载解引用
//*it 转换成 it.operator*(),然后再返回节点里面数据的引用,可以读也可以写
T& operator*()
{
return _node->_data;
}

迭代器是像指针一样的对象。一般结构体指针才会使用箭头,用箭头来访问结构体成员。

这里迭代器 it 解引用,返回节点中 T (Pos)类型数据(_data)的引用(可读可写)。
cpp
//重载箭头
T* operator->()
{
return &(operator*());
}
这种写法比较难以理解,其实 operator*() 可以直接替换为 _node->_data。
cpp
T* operator->()
{
return &(_node->_data);
}
这里会发现有两个箭头,但是下面用一个箭头就能调用到节点的数据。


这里相当于隐藏了一个->。这里重载->,返回了一个结构体的指针,结构体的指针再使用->寻找成员。
cpp
it->_node->data。
PS:这里隐藏的是后面那个箭头,第一个箭头是迭代器的运算符重载。
这里返回的是 T 的指针,这里T是Pos。Pos* 结构体的指针就可以使用箭头访问成员。
cpp
it->Pos->_a1。
const迭代器:

这里无法遍历,要用 const 迭代器接收。普通迭代器可读可写,但是 const 迭代器是只读的。
const list<int>
中的const
修饰的是整个list<int>
对象本身,而不是其元素类型int
。具体来说:
- 列表的常量性 :
const
表示列表对象本身不可修改。例如,不能调用push_back()
、pop_back()
等修改列表结构的成员函数。- 元素的访问权限 :通过
const list<int>
访问元素时,迭代器会变为const_iterator
,解引用得到的元素是const int&
(视为只读)。但这不改变模板参数int
的本质,而是由列表的常量性隐式限制了对元素的修改权限。总结 :
const
修饰的是list
本身,而非int
。列表的结构和元素都不可通过该常量对象修改,但元素类型仍是int
(非const int
)。
cpp
const T& operator*()
{
return _node->_data;
}
核心问题在于如何通过返回值的 const
属性来控制数据的读写权限 。operator*()
的返回类型决定了通过迭代器解引用后能否修改数据。
通过控制 operator*()
的返回类型(T&
或 const T&
),可以决定迭代器的读写权限。这种设计是 C++ const 正确性的核心思想,也是 STL 迭代器实现的基石。
这里怎么改很关键,需要为迭代器类提供两种版本的 operator*(),
但是没参数所以这里没法写重载函数。
cpp
//list 迭代器
template<class T,class Ref,class Ptr>
struct __list_iterator
{
typedef list_node<T> Node;
typedef __list_iterator<T, Ref, Ptr> iterator;
Node* _node;
}
迭代器类这里多加两个模板参数 Ref 和 Ptr 用于统一管理迭代器的引用和指针类型 ,使得同一个迭代器模板可以同时支持普通迭代器和常量迭代器
cpp
//链表
template<class T>
class list
{
typedef list_node<T> Node;
public:
typedef __list_iterator<T, T&, T*> iterator; //普通迭代器
typedef __list_iterator<T, const T&, const T*> const_iterator; //const 迭代器
private:
Node* _head ;
}
避免代码重复:通过模板参数控制引用和指针类型,可以复用同一份迭代器代码,无需为普通迭代器和常量迭代器分别编写类。
这里普通迭代器和 const 迭代器是两个类型,根据模板参数的不同,实例化了两个对象。
cpp
//const T& operator*()
Ref operator*()
{
return _node->_data;
}
//const T* operator->()
Ptr operator->()
{
return &(operator*());
}
Ref
:控制解引用操作符(operator*
)的返回类型,决定数据是否可修改。- 若为普通迭代器:
Ref = T&
,允许通过迭代器修改数据。 - 若为常量迭代器:
Ref = const T&
,禁止通过迭代器修改数据。
- 若为普通迭代器:
- **
Ptr
** :控制箭头操作符(operator->
)的返回类型,决定指针的读写权限。- 若为普通迭代器:
Ptr = T*
,允许通过指针修改数据。 - 若为常量迭代器:
Ptr = const T*
,禁止通过指针修改数据。·
- 若为普通迭代器:
cpp
const_iterator begin()const
{
return const_iterator(_head->_next);
}
const_iterator end()const
{
return const_iterator(_head);
}
反向迭代器:
链表:
cpp
链表
template<class T>
class list
{
typedef list_node<T> Node;
public:
//typedef __list_iterator<T> iterator; //迭代器
typedef __list_iterator<T,T&,T*> iterator;
typedef __list_iterator<T,const T&,const T*> const_iterator;
private:
Node* _head ; //哨兵位的头节点
}
这里要注意的是 typeddef 在 private 下是只能在类的内部使用,所以这里iterator这里要定义在 public 下方便在外部使用。
构造函数:
cpp
list()
{
_head = new Node;
_head->_next = _head;
_head->_pre = _head;
}
插入节点、删除节点:
cpp
//尾部插入数据
void push_back(const T& x)
{
Node* tail = _head->_pre;
Node* newnode = new Node(x);
tail->_next = newnode;
newnode->_pre = tail;
newnode->_next = _head;
_head->_pre = newnode;
}

操作跟前来链表的操作一样,注意一下节点连接的代码顺序就行。

只有一个节点(又当头又当尾 )时插入数据也没有问题。
cpp
//删除 这里使用了迭代器
iterator erase(iterator pos)
{
assert(pos != end()); //断言 不能删除哨兵位的头节点
Node* cur = pos._node; //!注意pos不是指针,使用的是'.',访问结构体成员
Node* pre = cur->_pre;
Node* next = cur->_next;
pre->_next = next;
next->_pre = pre;
delete cur;
return iterator(next);
}


注意这里pos节点被删除了,pos如果再使用就是野指针了,发生了迭代器失效,如果要再继续使用pos可以使其接受erase()函数的返回值,其返回的是pos节点下一个节点位置,这里的话就是 4 的位置。
cpp
pos = lt_1.erase(pos);
cpp
//任意位置插入节点
iterator insert(iterator pos,const T& x)
{
Node* cur = pos._node;
Node* pre = cur->_pre;
Node* newnode = new Node(x);
newnode->_next = cur;
newnode->_pre = pre;
pre->_next = newnode;
cur->_pre = newnode;
return iterator(newnode);
}


insert()这里插入数据后,pos并没有失效,还可以继续使用,但是指向的位置发生了改变。原来指向容器的第二个位置,插入数据过后指向了第三个位置。如果想让其继续指向第二个位置,让其接收insert()函数的返回值。
cpp
pos = lt_1.insert(pos, 30);
函数复用:
cpp
//尾插
void push_back(const T& x)
{
insert(end(),x);
}
//头插
void push_front(const T& x)
{
insert(begin(),x);
}
//尾删
void pop_back()
{
erase(--end());
}
//头删
void pop_front()
{
erase(begin());
}
析构函数:
cpp
void clear()
{
iterator it = begin();
while (it!=end())
{
it = erase(it);
}
}
~list()
{
clear();
delete _head;
_head = nullptr;
}
clear()清除数据,不清除头节点。
拷贝构造函数:
cpp
void empty_init()
{
//创建并初始化哨兵位的头节点
_head = new Node;
_head->_next = _head;
_head->_pre = _head;
}
//构造函数模板
//通过输入迭代器范围[first, last)来构造一个包含相同元素的新链表
template<class InputIterator>
list(InputIterator first, InputIterator last)
{
empty_init();
while (first!=last)
{
push_back(*first);
++first;
}
}
//前面构造函数可以改写成这样
list()
{
empty_init();
}
void swap(list<T>& x)
{
std::swap(_head,x._head);
}
//拷贝构造
list(const list<T>& lt)
{
empty_init();
list<T> tmp(lt.begin(), lt.end());
swap(tmp);
}
现代写法,使用迭代器区间构造 list 。