目录
前言
一. list概述
1.1 介绍
1.2 list功能介绍
1.2.1 list的构造
1.2.2 list的迭代器
1.2.3 capacity
1.2.4 list member access
1.2.5 增删查改
1.2.6 list迭代器失效问题
1.2.7 sort
二、list的模拟实现
2.1 list的节点
2.2 list迭代器
2.2.1 关于 -> 的重载
2.3 list常用接口
2.3 const迭代器
2.4 反向迭代器
C语言学习中有一个数据结构叫做链表 , 当时引入这个数据结构是因为对于数组我们可以随机访问 , 但是如果进行插入的话就要对插入位置往后进行挪动,效率较低,所以我们创造了一个能够在任意位置插入时间复杂度都是O(1)的链表 , 而链表我们学过基础的单向链表 , 双向链表 , 循环链表等等, stl中的list容器就是一个双向链表,它的接口基本上跟vector相似度很高 , 只不过不支持随机访问罢了 , 在学习list过程中我们会认识到实现迭代器的新的封装方式 ,我们要了解其底层逻辑,特性以及实现方式。
相较于vector的线性空间,list显得复杂得多,它的好处是每次插入或者删除一个元素,就分配或者释放一个空间。因此,list对于空间的运用有着绝对的精准,一点也不浪费。而且,对于任何位置的元素安插或移除,永远是O(1)的时间复杂度
通过下面示范代码,我们发现其用法其实与vector很类似
// constructors used in the same order as described above:
std::list first; // int的空链
std::list second (4,100); // 4个int类型的100
std::list third (second.begin(),second.end()); // 通过second的迭代器进行构造
std::list fourth (third); // third的拷贝构造
// 迭代器构造也能通过数组进行
int myints[] = {16,2,77,29};
std::list fifth (myints, myints + sizeof(myints) / sizeof(int) );
std::cout << "The contents of fifth are: ";
for (std::list::iterator it = fifth.begin(); it != fifth.end(); it++)
std::cout << *it << ' ';
std::cout << '\n';
正向迭代器
iterator begin();
const_iterator begin() const;
iterator end();
const_iterator end() const;
反向迭代器
reverse_iterator rbegin();
const_reverse_iterator rbegin() const;
reverse_iterator rend();
const_reverse_iterator rend() const;
size_type size() const;//节点个数
bool empty() const;//是否非空
reference front();
const_reference front() const;
reference back();
const_reference back() const;
这里我们注意,front和back都是引用返回
void push_front (const value_type& val); 在list首元素前插入值为val的元素
void pop_front(); 删除list中第一个元素
void push_back (const value_type& val); 在list尾部插入值为val的元素
void pop_back(); 删除list中最后一个元素
在pos处插入val
iterator insert (iterator position, const value_type& val);
pos处插入n个val
void insert (iterator position, size_type n, const value_type& val);
pos处插入迭代器first到last的内容
template void insert (iterator position, InputIterator first, InputIterator last);
iterator erase (iterator position);删除list position位置的元素
iterator erase (iterator first, iterator last);
删除从first到last的元素
void swap (list& x); 交换两个list中的元素
void clear(); 清空list中的有效元素
对于insert的使用可以参考如下代码
std::list mylist;
std::list::iterator it;
// set some initial values:
for (int i = 1; i <= 5; ++i) mylist.push_back(i); // 1 2 3 4 5
it = mylist.begin();
++it; // it points now to number 2 ^
mylist.insert(it, 10); // 1 10 2 3 4 5
// "it" still points to number 2 ^
mylist.insert(it, 2, 20); // 1 10 20 20 2 3 4 5
--it; // it points now to the second 20 ^
std::vector myvector(2, 30);
mylist.insert(it, myvector.begin(), myvector.end());
// 1 10 20 30 30 20 2 3 4 5
// ^
std::cout << "mylist contains:";
for (it = mylist.begin(); it != mylist.end(); ++it)
std::cout << ' ' << *it;
std::cout << '\n';
在vector和string的使用中我们遇到了迭代器失效问题, 同样的假如我们想要删除list的中的奇数,
我们用如下逻辑就会出问题
it所指向的节点已经删除,这时再进行操作的话显然是非法的,正确的代码如下
如果我们进行insert的话 ,那便不会失效, 因为对于list它的空间是非连续的 , 不像vector那样进行插入就要把一连串数据进行更改 ,不会对原迭代器指向内容进行更改,这里就不赘述了
list中的sort是单独实现的 , 因为它的排序涉及到了节点间的连接关系
void sort();
template void sort (Compare comp);
我们可以指定compare
注意:
list的sort底层其实是一个mergesort
list的实现是要比前面学的string和vector复杂一些的 , 运用到了很多新的思想,比如节点和迭代器的封装
与我们C语言双向链表的实现相似,都是用结构体对于节点进行封装,不过C++中我们使用了模板来达到我们泛型编程的目的,同时我们结构体中多了一个构造函数
template
struct __list_node
{
__list_node(const T& x = T()):data(x),prev(nullptr),next(nullptr)
{
}
__list_node* prev;
__list_node* next;
T data;
};
对于string和vector的迭代器都是通过指针来进行实现 , 但并不是所有迭代器都是要通过指针来实现,我们迭代器主要是为了实现对于容器中元素的外部访问,我们使迭代器具备移动,解引用,箭头访问等功能
对于list的迭代器的学习对于我们理解迭代器这一抽象概念以及我们对于泛型编程的思想有了更深的了解
template
struct __list_iterator
{
typedef __list_node Node;
typedef __list_iterator Self;
Node* _node;
__list_iterator(Node* node) :_node(node)
{
}
Ref operator*()
{
return _node->data;
}
//++it
Self& operator++()
{
_node = _node->next;
return *this;
}
//it++
Self operator++(int)
{
Self newit(*this);
++* this;
return newit;
}
//--it
Self& operator--()
{
_node = _node->prev;
return *this;
}
//it--
Self operator--(int)
{
__list_iterator newit(*this);
--* this;
return newit;
}
Ptr operator->()
{
return &_node->data;
}
bool operator!=(const __list_iterator& it)const
{
return _node != it._node;
}
bool operator==(const __list_iterator& it)const
{
return _node == it._node;
}
};
观察上述代码我们发现,模板参数里面除了数据类型T之外,又多了Ref(引用),Ptr(指针)两个参数,那么为什么要引入这两个参数呢?
对于这个迭代器的实现,我们不难看懂
其实我们分析后发现似乎只需要T一个参数就可以实现list的迭代器
但是我们知道,我们还要实现const_iterator,那么我们只要将代码复制一份,把指针换成常量指针,引用换成常量引用就可以了,这样就造成了代码冗余,不符合我们泛型编程的思想
如果我们换成三个参数呢?下面的代码将会让你赞不绝口
typedef __list_iterator iterator;
typedef __list_iterator const_iterator;
通过这样的处理,我们只需要实现一个迭代器就能实现普通迭代器和常迭代器两种迭代器,大大减少了代码量
我们发现list迭代器里面重载了一个->,如果我们list中元素类型为int这种内置类型,显然这个重载是没有作用的,但是对于下述代码就不一样了
struct Person
{
Person():_name("张三"),_age(20),_ID(20230628)
{
}
char _name[20];
int _age;
int _ID;
};
void Test()
{
Person p1;
list l1;
l1.push_back(p1);
l1.push_back(p1);
l1.push_back(p1);
l1.push_back(p1);
l1.push_back(p1);
list::iterator it = l1.begin();
while (it != l1.end())
{
cout << it->_name << endl;
++it;
}
}
但是如果我们追究起来的话,->返回的应该是data的指针,我们如果想访问data的内容应该再增加一个->才对,这其实就是编译器对于这个操作的 优化
typedef __list_node Node;
public:
typedef __list_iterator iterator;
typedef __list_iterator const_iterator;
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);
}
list():_head(new Node())
{
_head->next = _head;
_head->prev = _head;
}
list(const list& l1):_head(new Node())
{
_head->next = _head;
_head->prev = _head;
for (auto x : l1)
push_back(x);
}
//list& operator=(const list& l1)
//{
// if (this != &l1)
// {
// clear();
// for (auto x : l1)
// push_back(x);
// return *this;
// }
//}
//modern operator=
list& operator=(list l1)
{
swap(_head, l1._head);
return *this;
}
void push_back(const T& x)
{
/*Node* newnode = new Node(x);
_head->prev->next = newnode;
newnode->prev = _head->prev;
_head->prev = newnode;
newnode->next = _head;*/
insert(end(), x);
}
void push_front(const T& x)
{
/* Node* newnode = new Node(x);
newnode->next = _head->next;
_head->next->prev = newnode;
_head->next = newnode;
newnode->prev = _head;*/
insert(begin(), x);
}
void pop_back()
{
if (!empty())
{
/*Node* tar = _head->prev;
_head->prev->prev->next = _head;
_head->prev = _head->prev->prev;
delete tar;
tar = nullptr;*/
erase(--(end()));
}
}
bool empty()const
{
return _head->next == _head;
}
void insert(iterator it , const T& x)
{
Node* cur = it._node;
Node* newnode = new Node(x);
newnode->prev = cur;
newnode->next = cur->next;
cur->next->prev = newnode;
cur->next = newnode;
}
iterator& erase(iterator it)
{
assert(it != end());
Node* cur = it._node;
cur->prev->next = cur->next;
Node* newcur = cur->next;
newcur->prev = cur->prev;
delete cur;
it = iterator(newcur);
return it;
}
void clear()
{
//Node* pos = _head->next;
//while (pos != _head)
//{
// pos = pos->next;
// delete pos->prev;
//}
//_head->prev = _head->next = nullptr;
iterator it = begin();
while (it != end())
erase(it++);
}
~list()
{
clear();
delete _head;
_head = nullptr;
}
我们发现其实只要实现了insert,对于头插尾插都可以进行复用
其实前面讲为什么迭代器有三个模板参数时已经介绍过了
list反向迭代器的实现其实借助了适配器reverse_iterator , 通过将正向迭代器作为接口传给适配器,而适配器里面实现了反向++,--的操作从而我们就得到了反向迭代器,这其实也是复用的思想
typedef reverse_iterator const_reverse_iterator;
typedef reverse_iterator reverse_iterator;