目录
list介绍
- list是一种可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。
- list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立结点当中,在结点中通过指针指向其前一个元素和后一个元素。
- list与forward_list非常相似,最主要的不同在于forward_list是单链表,只能进行单方向迭代。
- 与其他容器相比,list通常在任意位置进行插入、删除元素的执行效率更高。
- list和forward_list最大的缺陷是不支持在任意位置的随机访问,其次,list还需要一些额外的空间,以保存每个结点之间的关联信息(对于存储的类型较小元素来说这可能是一个重要的因素)。
接口总览
namespace XM
{
//1.模拟实现list当中的结点类
template<class T>
struct _list_node
{
//成员函数
_list_node(const T& val = T()); //构造函数
//成员变量
T _val; //数据域
_list_node<T>* _next; //后继指针
_list_node<T>* _prev; //前驱指针
};
//2.模拟实现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--();
self operator++(int);
self operator--(int);
bool operator==(const self& s) const;
bool operator!=(const self& s) const;
Ref operator*();
Ptr operator->();
//成员变量
node* _pnode; //一个指向结点的指针
};
//3.模拟实现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; //指向链表头结点的指针
};
}
1.结点类的模拟实现
- list底层本质是一个带头双向循环链表
- 该结点类只需要根据数据来构造一个结点,而结点的释放则由list的析构函数来完成;只需要一个构造函数
构造函数
-
若构造结点时未传入数据,则默认以list容器所存储类型的默认构造函数所构造出来的值为传入数据。
-构造函数
_list_node(const T& val = T())
:_val(val)
, _prev(nullptr)
, _next(nullptr)
{}
2.迭代器的模拟实现
(1)迭代器类存在的意义
- string和vector对象都将其数据存储在了一块连续的内存空间,我们通过指针进行自增、自减以及解引用等操作,就可以对相应位置的数据进行一系列操作,因此string和vector当中的迭代器就是原生指针。
- list的各个结点在内存当中的位置是随机的,并不是连续的,我们不能仅通过结点指针的自增、自减以及解引用等操作对相应结点的数据进行操作。
- 而迭代器的意义就是: 让使用者可以不必关心容器的底层实现,可以用简单统一的方式对容器内的数据进行访问。
- 既然list的结点指针的行为不满足迭代器定义,那么我们可以对这个结点指针进行封装,对结点指针的各种运算符操作进行重载,使得我们可以用和string和vector当中的迭代器一样的方式使用list当中的迭代器。例如,当你使用list当中的迭代器进行自增操作时,实际上执行了p = p->next语句,底层自动实现,上层不知道底层细节。
- 总结: list迭代器类,实际上就是对结点指针进行了封装,对其各种运算符进行了重载,使得结点指针的各种行为看起来和普通指针一样。
(2)迭代器类的模板参数说明
template<class T, class Ref, class Ptr>
typedef _list_iterator<T, T&, T*> iterator;
typedef _list_iterator<T, const T&, const T*> const_iterator;
- 迭代器类的模板参数列表当中的Ref和Ptr分别代表的是引用类型和指针类型。
- 当我们使用普通迭代器时,编译器就会实例化出一个普通迭代器对象;当我们使用const迭代器时,编译器就会实例化出一个const迭代器对象。
- 若该迭代器类不设计三个模板参数,那么就不能很好的区分普通迭代器和const迭代器。
(3)构造函数
- 迭代器类实际上就是对结点指针进行了封装,其成员变量就只有一个,那就是结点指针,其构造函数直接根据所给结点指针构造一个迭代器对象即可
_list_iterator(node* pnode)
:_pnode(pnode)
{}
(4)函数重载
①++ 运算符
- 前置++,前置++原本的作用是将数据自增,然后返回自增后的数据。我们的目的是让结点指针的行为看起来更像普通指针,那么对于结点指针的前置++,我们就应该先让结点指针指向后一个结点,然后再返回“自增”后的结点指针
- 后置++,我们则应该先记录当前结点指针的指向,然后让结点指针指向后一个结点,最后返回“自增”前的结点指针(int)
-前置++
self operator++()
{
_pnode = _pnode->_next; //让结点指针指向后一个结点
return *this; //返回自增后的结点指针
}
-后置++
typedef _list_iterator<T, Ref, Ptr> self;
self operator++(int)
{
self tmp(*this); //记录当前结点指针的指向
_pnode = _pnode->_next; //让结点指针指向后一个结点
return tmp; //返回自增前的结点指针
}
② - - 运算符
- 前置- -,我们应该先让结点指针指向前一个结点,然后再返回“自减”后的结点指针
- 后置- -,我们则应该先记录当前结点指针的指向,然后让结点指针指向前一个结点,最后返回“自减”前的结点指针 (int)
-前置--
self operator--()
{
_pnode = _pnode->_prev; //让结点指针指向前一个结点
return *this; //返回自减后的结点指针
}
-后置--
self operator--(int)
{
self tmp(*this); //记录当前结点指针的指向
_pnode = _pnode->_prev; //让结点指针指向前一个结点
return tmp; //返回自减前的结点指针
}
③ == 和 != 运算符
- 使用==运算符比较两个迭代器时,实际上想知道的是这两个迭代器是否是同一个位置的迭代器,即判断这两个迭代器当中的结点指针的指向是否相同
bool operator==(const self& s) const
{
return _pnode == s._pnode; -判断两个结点指针指向是否相同
}
bool operator!=(const self& s) const
{
return _pnode != s._pnode; -判断两个结点指针指向是否不同
}
④ * 运算符
- 使用解引用操作符时,是想得到该位置的数据内容。因此,我们直接返回当前结点指针所指结点的数据即可,但是这里需要使用引用返回,因为解引用后可能需要对数据进行修改。
Ref operator*()
{
return _pnode->_val; //返回结点指针所指结点的数据
}
⑤ -> 运算符
- 当list容器当中的每个结点存储的不是内置类型,而是自定义类型,如日期类(year,month,day),那么当我们拿到一个位置的迭代器时,我们可能会使用->运算符访问Date的成员
- 使用pos->_year这种访问方式时,需要将日期类的成员变量设置为公有。
list<Date> lt;
Date d1(2022,1,1);
Date d2(2022,2,2);
Date d3(2022,3,3);
lt.push_back(d1);
lt.push_back(d2);
lt.push_back(d3);
list<Date>::iterator pos = lt.begin();
cout << pos->_year << endl; //输出第一个日期的年份
- 对于->运算符的重载,我们直接返回结点当中所存储数据的地址
Ptr operator->()
{
return &_pnode->_val; -返回结点指针所指结点的数据的地址
}
补充:
- 有一个细节,这里使用迭代器访问日期类当中的成员变量时不是应该用两个->吗?
- 这里本来是应该有两个->的,第一个箭头是pos ->去调用重载的operator->返回Date* 的指针,第二个箭头是Date* 的指针去访问对象当中的成员变量_year。
- 但是一个地方出现两个箭头,程序的可读性太差了,所以编译器做了特殊识别处理,为了增加程序的可读性,省略了一个箭头。
3.list的模拟实现
(1)构造函数
-
list是一个带头双向循环链表,在构造一个list对象时,直接申请一个头结点,并让其前驱指针和后继指针都指向自己
-构造函数
list()
{
_head = new node; //申请一个头结点
_head->_next = _head; //头结点的后继指针指向自己
_head->_prev = _head; //头结点的前驱指针指向自己
}
(2)拷贝构造
-
拷贝构造函数就是根据所给list容器,拷贝构造出一个对象。
-
我们先申请一个头结点,然后将所给容器当中的数据,通过遍历的方式一个个尾插到新构造的容器后面即可。
-拷贝构造
list(const list<T>& lt)
{
_head = new node; //申请一个头结点
_head->_next = _head; //头结点的后继指针指向自己
_head->_prev = _head; //头结点的前驱指针指向自己
for (const auto& e : lt)
{
push_back(e); //将容器lt当中的数据一个个尾插到新构造的容器后面
}
}
(3)赋值重载
①传统写法
- 先调用clear函数将原容器清空,然后将容器lt当中的数据,通过遍历的方式一个个尾插到清空后的容器当中即可。
-传统写法
list<T>& operator=(const list<T>& lt)
{
if (this != <) -避免自己给自己赋值
{
clear(); //清空list
for (const auto& e : lt)
{
push_back(e); //将容器lt当中的数据一个个尾插到链表后面
}
}
return *this; -支持连续赋值
}
②现代写法
- 现代写法的代码量较少,首先利用编译器机制,故意不使用引用接收参数,通过编译器自动调用list的拷贝构造函数构造出来一个list对象,然后调用swap函数将原容器与该list对象进行交换。
-现代写法
list<T>& operator=(list<T> lt) //编译器接收值的时候自动调用其拷贝构造函数
{
swap(lt); -交换这两个对象
return *this; -支持连续赋值
}
(4)析构函数
-
首先调用clear函数清理容器当中的数据,然后将头结点释放,最后将头指针置空
-析构函数
~list()
{
clear(); //清理容器
delete _head; //释放头结点
_head = nullptr; //头指针置空
}
(5)迭代器相关函数 begin和end
- 明确的是: begin函数返回的是第一个有效数据的迭代器,end函数返回的是最后一个有效数据的下一个位置的迭代器。
- 对于list来说,其第一个有效数据的迭代器就是使用头结点后一个结点的地址构造出来的迭代器,而其最后一个有效数据的下一个位置的迭代器就是使用头结点的地址构造出来的迭代器。(最后一个结点的下一个结点就是头结点)
iterator begin()
{
-返回使用头结点后一个结点的地址构造出来的普通迭代器
return iterator(_head->_next);
}
iterator end()
{
-返回使用头结点的地址构造出来的普通迭代器
return iterator(_head);
}
-Const迭代器
const_iterator begin() const
{
-返回使用头结点后一个结点的地址构造出来的const迭代器
return const_iterator(_head->_next);
}
const_iterator end() const
{
-返回使用头结点的地址构造出来的普通const迭代器
return const_iterator(_head);
}
(6)访问容器相关函数 front 和 back
- front和back函数分别用于获取第一个有效数据和最后一个有效数据,因此,直接返回第一个有效数据和最后一个有效数据的引用即可
- 也需要重载一对用于const对象的front函数和back函数,因为const对象调用front和back函数后所得到的数据不能被修改。
T& front()
{
return *begin(); -返回第一个有效数据的引用
}
T& back()
{
return *(--end()); -返回最后一个有效数据的引用
}
Const
const T& front() const
{
return *begin(); -返回第一个有效数据的const引用
}
const T& back() const
{
return *(--end()); -返回最后一个有效数据的const引用
}
(7)插入、删除函数
①insert
- 先根据所给迭代器得到该位置处的结点指针cur,然后通过cur指针找到前一个位置的结点指针prev,接着根据所给数据x构造一个待插入结点,之后再建立新结点与cur之间的双向关系,最后建立新结点与prev之间的双向关系
-插入insert
void insert(iterator pos, const T& x)
{
assert(pos._pnode); //检测pos的合法性
node* cur = pos._pnode; //迭代器pos处的结点指针
node* prev = cur->_prev; //cur节点前一个位置的结点指针
node* newnode = new node(x); //根据所给数据x构造一个待插入结点
-建立newnode与cur之间的双向关系
newnode->_next = cur;
cur->_prev = newnode;
-建立newnode与prev之间的双向关系
newnode->_prev = prev;
prev->_next = newnode;
}
②erase
- 根据所给迭代器得到该位置处的结点指针cur,然后通过cur指针找到前一个位置的结点指针prev,以及后一个位置的结点指针next,紧接着释放cur结点,最后建立prev和next之间的双向关系
-
迭代器失效即迭代器所指向的节点的无效,即该节点被删除了。因为list的底层结构为带头结点的双向循环链表,因此在list中进行插入时是不会导致list的迭代器失效的,只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响
-删除
iterator erase(iterator pos)
{
assert(pos._pnode); -检测pos的合法性
assert(pos != end()); -删除的结点不能是头结点
node* cur = pos._pnode; //迭代器pos处的结点指针
node* prev = cur->_prev; //迭代器pos前一个位置的结点指针
node* next = cur->_next; //迭代器pos后一个位置的结点指针
delete cur; //释放cur结点
//建立prev与next之间的双向关系
prev->_next = next;
next->_prev = prev;
return iterator(next); -返回所给迭代器pos的下一个迭代器
}
③push_back 和 push_front
- 尾插和头插
-尾插
void push_back(const T& x)
{
/* 老方法
Node* newNode = new Node(x);
Node* tail = _head->_prev;
tail->_next = newNode;
newNode->_next = _head;
_head->_prev = newNode;
newNode->_prev = tail;
*/
insert(end(), x);
}
-头插
void push_front(const T& x)
{
insert(begin(), x);
}
④ pop_back 和 pop_front
- 尾删和头删
-头删
void pop_front()
{
erase(begin()); //删除第一个有效结点
}
-尾删
void pop_back()
{
erase(--end());
}
(8)其他函数
①size
- size函数用于获取当前容器当中的有效数据个数,因为list是链表,所以只能通过遍历的方式逐个统计有效数据的个数
- 可以给list多设置一个成员变量size,用于记录当前容器内的有效数据个数
size_t size() const
{
size_t num = 0; //统计有效数据个数
const_iterator it = begin(); //获取第一个有效数据的迭代器
while (it != end()) //通过遍历统计有效数据个数
{
num++;
it++;
}
return num; //返回有效数据个数
}
②resize
- 若当前容器的size小于所给n,则尾插结点,直到size等于n为止。
- 若当前容器的size大于所给n,则只保留前n个有效数据。
- 实现resize函数时,不要直接调用size函数获取当前容器的有效数据个数,因为当你调用size函数后就已经遍历了一次容器了,而如果结果是size大于n,那么还需要遍历容器,找到第n个有效结点并释放之后的结果
这里实现resize的方法是,设置一个变量len,用于记录当前所遍历的数据个数,然后开始变量容器,在遍历过程中:
- 当len大于或是等于n时遍历结束,此时说明该结点后的结点都应该被释放,将之后的结点释放即可。
- 当容器遍历完毕时遍历结束,此时说明容器当中的有效数据个数小于n,则需要尾插结点,直到容器当中的有效数据个数为n时停止尾插即可。
void resize(size_t n, const T& val = T())
{
iterator it = begin(); //获取第一个有效数据的迭代器
size_t len = 0; -记录当前所遍历的数据个数
while (len < n && it != end())
{
len++;
it++;
}
if (len == n) -说明容器当中的有效数据个数大于或是等于n
{
while (it != end()) //只保留前n个有效数据
{
it = erase(it); //每次删除后接收下一个数据的迭代器
}
}
else -说明容器当中的有效数据个数小于n
{
while (len < n) -尾插数据为val的结点,直到容器当中的有效数据个数为n
{
push_back(val);
len++;
}
}
}
③clear
- 清空容器,我们通过遍历的方式,逐个删除结点,只保留头结点
void clear()
{
iterator it = begin();
while (it != end()) -逐个删除结点,只保留头结点
{
it = erase(it);
}
}
④empty
- 直接判断该容器的begin函数和end函数所返回的迭代器,是否是同一个位置的迭代器即可。(此时说明容器当中只有一个头结点)
bool empty() const
{
return begin() == end(); //判断是否只有头结点
}
⑤swap
- swap函数用于交换两个容器,list容器当中存储的实际上就只有链表的头指针,我们将这两个容器当中的头指针交换即可
- 在此处调用库当中的swap函数需要在swap之前加上“::”(作用域限定符),告诉编译器这里优先在全局范围寻找swap函数,否则编译器会认为你调用的就是你正在实现的swap函数(就近原则)。
void swap(list<T>& lt)
{
::swap(_head, lt._head); -交换两个容器当中的头指针即可 ::使用全局的swap
}
4.list 和 Vector对比