1. list的介绍及使用
2. list的深度剖析及模拟实现
3. list与vector的对比
1. list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。
2. list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向 其前一个元素和后一个元素。
3. list与forward_list非常相似:最主要的不同在于forward_list是单链表,只能朝前迭代,已让其更简单高 效。
4. 与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率 更好。
5. 与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list 的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间 开销;list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大list来说这 可能是一个重要的因素)
list中的接口比较多,此处类似,只需要掌握如何正确的使用,然后再去深入研究背后的原理,已达到可扩展 的能力。以下为list中一些常见的重要接口。
构造函数( (constructor)) | 接口说明 |
list (size_type n, const value_type& val = value_type()) | 构造的list中包含n个值为val的元素 |
list() | 构造空的list |
list (const list& x) | 拷贝构造函数 |
list (InputIterator first, InputIterator last) | 用[first, last)区间中的元素构造list |
// list的构造
void TestList1()
{
list l1; // 构造空的l1
list l2(4, 100); // l2中放4个值为100的元素
list l3(l2.begin(), l2.end()); // 用l2的[begin(), end())左闭右开的区间构造l3
list l4(l3); // 用l3拷贝构造l4
// 以数组为迭代器区间构造l5
int array[] = { 16,2,77,29 };
list l5(array, array + sizeof(array) / sizeof(int));
// 列表格式初始化C++11
list l6{ 1,2,3,4,5 };
// 用迭代器方式打印l5中的元素
list::iterator it = l5.begin();
while (it != l5.end())
{
cout << *it << " ";
++it;
}
cout << endl;
// C++11范围for的方式遍历
for (auto& e : l5)
cout << e << " ";
cout << endl;
}
此处,可暂时将迭代器理解成一个指针,该指针指向list中的某个节点。
函数声明 | 接口说明 |
begin + end | 返回第一个元素的迭代器+返回最后一个元素下一个位置的迭代器 |
rbegin + rend | 返回第一个元素的reverse_iterator,即end位置,返回最后一个元素下一个位置的 reverse_iterator,即begin位置 |
【注意】
- 1. begin与end为正向迭代器,对迭代器执行++操作,迭代器向后移动
- 2. rbegin(end)与rend(begin)为反向迭代器,对迭代器执行++操作,迭代器向前移动
// list迭代器的使用
// 注意:遍历链表只能用迭代器和范围for
void PrintList(const list& l)
{
// 注意这里调用的是list的 begin() const,返回list的const_iterator对象
for (list::const_iterator it = l.begin(); it != l.end(); ++it)
{
cout << *it << " ";
// *it = 10; 编译不通过
}
cout << endl;
}
void TestList2()
{
int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
list l(array, array + sizeof(array) / sizeof(array[0]));
// 使用正向迭代器正向list中的元素
// list::iterator it = l.begin(); // C++98中语法
auto it = l.begin(); // C++11之后推荐写法
while (it != l.end())
{
cout << *it << " ";
++it;
}
cout << endl;
// 使用反向迭代器逆向打印list中的元素
// list::reverse_iterator rit = l.rbegin();
auto rit = l.rbegin();
while (rit != l.rend())
{
cout << *rit << " ";
++rit;
}
cout << endl;
}
函数声明 | 接口说明 |
empty | 检测list是否为空,是返回true,否则返回false |
size | 返回list中有效节点的个数 |
函数声明 | 接口说明 |
front | 返回list的第一个节点中值的引用 |
back | 返回list的最后一个节点中值的引用 |
函数声明 | 接口说明 |
push_front | 在list首元素前插入值为val的元素 |
pop_front | 删除list中第一个元素 |
push_back | 在list尾部插入值为val的元素 |
pop_back | 删除list中最后一个元素 |
insert | 在list position 位置中插入值为val的元素 |
erase | 删除list position位置的元素 |
swap | 交换两个list中的元素 |
clear | 清空list中的有效元素 |
// list插入和删除
// push_back/pop_back/push_front/pop_front
void TestList3()
{
int array[] = { 1, 2, 3 };
list L(array, array + sizeof(array) / sizeof(array[0]));
// 在list的尾部插入4,头部插入0
L.push_back(4);
L.push_front(0);
PrintList(L);
// 删除list尾部节点和头部节点
L.pop_back();
L.pop_front();
PrintList(L);
}
// insert /erase
void TestList4()
{
int array1[] = { 1, 2, 3 };
list L(array1, array1 + sizeof(array1) / sizeof(array1[0]));
// 获取链表中第二个节点
auto pos = ++L.begin();
cout << *pos << endl;
// 在pos前插入值为4的元素
L.insert(pos, 4);
PrintList(L);
// 在pos前插入5个值为5的元素
L.insert(pos, 5, 5);
PrintList(L);
// 在pos前插入[v.begin(), v.end)区间中的元素
vector v{ 7, 8, 9 };
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);
}
// resize/swap/clear
void TestList5()
{
// 用数组来构造list
int array1[] = { 1, 2, 3 };
list l1(array1, array1 + sizeof(array1) / sizeof(array1[0]));
PrintList(l1);
// 交换l1和l2中的元素
list l2;
l1.swap(l2);
PrintList(l1);
PrintList(l2);
// 将l2中的元素清空
l2.clear();
cout << l2.size() << endl;
}
函数声明 | 接口说明 |
splice |
从一个列表转移元素到另一个列表 |
remove |
移除具有特定值的元素 |
unique |
移除重复的数值 |
merge |
合并排序过的列表 |
sort |
对容器中的元素进行排序 |
reverse |
颠倒元素的顺序 |
int main()
{
list lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
list::iterator it = lt.begin();
//while (it < lt.begin())
//这里不能使用 < ,因为不能保证前一个节点比后一个大节点
//链表间没有大小关系
// < 只适用于底层物理空间连续:string,vector
while (it != lt.end())
{
cout << *it << " ";
++it;
}
cout << endl;
//逆置链表
lt.reverse();
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
//归并链表
//归并的前提:要求两个链表都有序
list lt1;
lt1.push_back(1);
lt1.push_back(2);
lt1.push_front(1);
// lt和lt1链表排序
lt.sort();
lt1.sort();
lt.merge(lt1);
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
//链表去重 - 要求 - 链表有序/重复元素相邻
lt.unique();
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
//链表排序
lt.sort();
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
//这里提一个问题,能不能使用算法库的sort
//这里为什么要单独提供一个链表的sort
/*sort(lt.begin(), lt.end());
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;*/
//error:_Sort_unchecked(_UFirst, _ULast, _ULast - _UFirst, _Pass_fn(_Pred));
//算法库实现的sort是两个迭代器相减,list物理空间不连续,不支持相减
//erase:删除pos位置的值
//remove:根据值找pos位置+删除pos位置的值
lt.remove(4);
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
//链表转移 - 节点转移
list mylist1, mylist2;
list::iterator it;
// set some initial values:
for (int i = 1; i <= 4; ++i)
mylist1.push_back(i); // mylist1: 1 2 3 4
for (int i = 1; i <= 3; ++i)
mylist2.push_back(i * 10);// mylist2: 10 20 30
it = mylist1.begin();
++it; // points to 2
mylist1.splice(it, mylist2); // mylist1: 1 10 20 30 2 3 4
// mylist2 (empty)
//LRU,2最近被使用到 - 最少使用次数
lt.splice(lt.end(), lt, find(lt.begin(), lt.end(), 2));
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
return 0;
}
- 1. list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。
- 2. list的底层是带头双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。
- 3. 与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率更好。
- 4. 与其他序列式容器相比,list最大的缺陷是不支持任意位置的随机访问,比如:要访问list的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大list来说这可能是一个重要的因素)
强调一下head是哨兵结点,本身不存储数据,作用只是指向头和尾!
template
struct ListNode
{
public:
//类模板
//ListNode是一个基本类型或者自定义类型
//ListNode就是int类型
//ListNode就是string类型
//实例化什么就是什么类型
ListNode* _next;
ListNode* _prev;
T _data;
public:
//记住不能这里写,构造函数的命名与类名相同
//ListNode (const T& x)
ListNode(const T& x)
{
_next = nullptr;
_prev = nullptr;
_data = x;
}
};
这里我们可以解释一下,由于struct是类名,所以我们这里可以直接定义_next和_prev,不需要再加上struct,然后typedef了,同时这里也是一个类模板参数,当用户实例化类模板的时候,实例化什么类型该结点就是什么类型的结点。我们来看一下上面的结点有没有什么问题,还记得我们上面的提示吗?
强调一下head是哨兵结点,本身不存储数据,作用只是指向头和尾!
如果我们申请的是一个头节点,此时这个头结点就是一个哨兵位,本身不需要传入参数,但是我们上面的构造函数是传入了参数,所以我们需要修改我们的代码,这里可以写一个缺省值。
template
struct ListNode
{
public:
//类模板
//ListNode是一个基本类型或者自定义类型
//ListNode就是int类型
//ListNode就是string类型
//实例化什么就是什么类型
ListNode* _next;
ListNode* _prev;
T _data;
public:
ListNode(const T& x = 0)
{
_next = nullptr;
_prev = nullptr;
_data = x;
}
};
我们这样写对吗?这就犯了一个错误,如果我们实例化类模板是LIstNode
template
struct ListNode
{
public:
//类模板
//ListNode是一个内置类型或者自定义类型
//ListNode就是int类型
//ListNode就是string类型
//实例化什么就是什么类型
ListNode* _next;
ListNode* _prev;
T _data;
public:
ListNode(const T& x = T())
{
_next = nullptr;
_prev = nullptr;
_data = x;
}
};
这里我们不传参的时候,匿名类它就会去调用默认的构造函数去初始化x,内置类型和自定义类型都会调用自己的默认构造去初始化x。
template
class list
{
typedef ListNode Node;
public:
list()
{
//带哨兵位的链表,第一个节点不存数据
_head = new Node;//申请一个Node空间
// new -> 开空间 + 调用构造函数
_head->_next = _head;
_head->_prev = _head;
}
void push_back(const T& x)
{
Node* newnode = new Node(x);//开空间 + 调用构造函数
//先找到tail
Node* tail = _head->_prev;
tail->_next = newnode;
newnode->_prev = tail;
newnode->_next = _head;
_head->_prev = newnode;
}
private:
Node* _head;
};
这里我们也稍微解释一下,使用new申请空间它首先会开辟我们想要的空间,然后再调用构造函数去初始化我们申请的空间,这也就是为什么上面的结点类我们需要写一个构造函数,然后我们来看一下我们的尾插函数,首先需要申请结点,然后再将申请的结点链接到我们的双向带头循环链表中。
然后我们来测试一下,由于我们这里还没有实现迭代器,我们就写一个打印链表的接口。
void print()
{
Node* cur = _head->_next;
while (cur != _head)
{
printf("%d ", cur->_data);
cur = cur->_next;
}
}
然后来测试一下push_back的功能。
void test_list1()
{
// 类模板实例化
list lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
lt.print();
}
#include "list.h"
int main()
{
yu::test_list1();
return 0;
}
运行结果:
但是我们这里print存在限制,首先它的输出格式是写死的,其次它对数据只能读,不能写,所以我们这里就需要使用迭代器,我们在上面list使用的时候就可以++迭代器,但是链表底层的物理空间不连续,如何做到++呢?在这里我们可以重载++
强调一下Node*是指针类型,内置类型不能重载,所以我们要单独对Node*封装成一个类。
template
struct __list_iterator
{
typedef ListNode Node;
typedef __list_iterator self;
public:
Node* _node;
public:
__list_iterator(Node* node)
{
_node = node;//一般传入的是begin()
}
self& operator++()//使用++it
{
_node = _node->_next;
return *this;//这里的*this类型是__list_iterator
}
self operator++(int)//后置++
{
self tmp(*this);
_node = _node->_next;
return tmp;//返回局部对象,出了作用域就被释放,不能销毁
}
self& operator--()
{
_node = _node->_prev;
return *this;
}
self operator--(int)//后置--
{
self tmp(*this);
_node = _node->_prev;
return tmp;
}
T& operator*()//使用*it
{
return _node->_data;
}
bool operator!=(const self& s)//使用it != end()
{
return _node != s._node;
}
bool operator==(const self& s)
{
return _node == s._node;
}
};
首先我们需要将封装的迭代器需要实现的功能函数写好!
❓❓首先我们要问自己一个问题,链表的迭代器能不能像vector string 那样直接单纯地用解引用和++?
我们不妨回顾一下vector string 的迭代器,其底层是指针数组的原生指针!而它们容器的存储空间是连续的,解引用可以直接拿到数组数据,++可以直接加上类型的步长!
而list的呢?list是不连续的!它们通过前后指针链接!直接++不能找到后一个结点!解引用不是直接拿到存储数据,而是一个结点结构体!我们需要的是里面的data!
怎么办? 系统的【*】和【++】不能满足我们需要的结果!
必须重载运算符!所以我们得将迭代器封装成一个类!
现在再问自己一个问题! 他的类成员变量是谁? 联系一下我们的目的和链表容器结构!
我们想到一个指向node 的指针就可以解决了,++不就是next , --不就是prev , *不就是data!
!这里单独将->符号拿出来说明一下! 想用->访问数据,->左边是什么? 是结构体地址!
也就是说如果我们list 存储的数据是结构体或者类!里面有多个变量,那么我们访问数据就不能单纯的解引用!必须使用->来访问数据!
我们这里再介绍一个重点。
void func()
{
//内置类型
//yu::ListNode* it1 = _head->_next;
Node* it1 = _head->_next;
//对Node*进行封装成类 - 自定义类型
//yu::__list_iterator it2 = _head->_next;
iterator it2 = _head->_next;
*it1;
++it1;
*it2;
++it2;
cout << sizeof(it1) << endl;
cout << sizeof(it2) << endl;
}
我们封装Node*为一个单独的类,那么它和Node*一样吗?*it1;++it1;*it2;++it2;它们之间有区别吗?各自求sizeof大小是多少呢?
虽然我们是用Node*为一个单独的类,但是就不再是基本类型了,而是自定义类型,对于Node*d的*it1;++it1;它此时就是一个原生指针,其解引用就是通过地址去找元素,++通过原生指针向后跳过一个Node*字节。而封装成的类Node*的*it2;++it2;此时就不再是原生指针,其解引用是调用操作符重载*去返回当前值的引用,++也是调用操作符重载++去指向当前指针的下一个结点。那它们底层物理上一样吗?
但是它们的底层物理是一样的,都是Node*,都是4个字节,只不过一个是基本类型,一个是自定义类型。然后我么来看一下反向迭代器的特点。
反向迭代器就是将list数据倒序遍历。
根据上面的图,我们如果要实现反向迭代器的话,迭代的时候++rit就是让指针指向前面的位置。那我们就可以把之前写的普通迭代器改吧改吧,名字改成reverse_iterator,然后只需要将里面的操作符重载++让其指向的前一个位置。但是这种实现方式存在缺陷,这样会存在大量的代码重复,类似于普通迭代器和const迭代器单独实现的代码冗余,所以我们这里换一种写法,使用一下适配器的思路,传入正向迭代器我给你适配出反向迭代器,我们先来看一下库里面的实现方式。
按照库里面的实现方法,指针的位置确实是向前走的,但是在解引用哪里,我们发现解引用是解引用的前一个位置,所以按照我们上面的逻辑是和库不一样,但是我们的也能实现,但是为了跟库保持一致,根据上面的reverse迭代器传参应该是我们下面的逻辑,end()位置是rbegin(),而begin()的位置是rend()位置
直接看代码
//适配器
//给我list的iterator,适配出list的reverse_iterator
//给我vector的iterator,适配出vector的reverse_iterator
template
struct ReverseIterator
{
typedef ReverseIterator Self;
public:
Iterator cur;
public:
ReverseIterator(Iterator it)
:cur(it)
{}
Self& operator++()
{
--cur;
return *this;
}
Ref operator*()
{
Iterator tmp = cur;
--tmp;
return *tmp;
}
Ptr operator->()
{
return &(this->operator*());
}
bool operator!=(const Self& s)
{
return cur != s.cur;
}
};
typedef ReverseIterator reverse_iterator;
typedef ReverseIterator const_reverse_iterator;
reverse_iterator rbegin()
{
return reverse_iterator(end());
}
reverse_iterator rend()
{
return reverse_iterator(begin());
}
我们来测试一下:
int main()
{
yu::list lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
yu::list::reverse_iterator rit = lt.rbegin();
while (rit != lt.rend())
{
cout << *rit << " ";
++rit;
}
cout << endl;
return 0;
}
运行结果:
我们再来看一下vector容器的反向迭代器。
typedef ReverseIterator reverse_iterator;
typedef ReverseIterator const_reverse_iterator;
reverse_iterator rbegin()
{
return reverse_iterator(end());
}
reverse_iterator rend()
{
return reverse_iterator(begin());
}
我们来测试一下:
int main()
{
yu::vector v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
yu::vector::reverse_iterator rit = v.rbegin();
while (rit != v.rend())
{
cout << *rit << " ";
++rit;
}
cout << endl;
return 0;
}
运行结果:
图解:
//再强调一下,迭代器被封装成类!!!
typedef __list_iterator iterator;//标准化
iterator begin()
{
//匿名对象返回!这里调用了迭代器的构造函数
return iterator(_head->_next);
}
iterator end()
{
//这里千万记住,返回的是哨兵结点!不是尾结点!
return _head;//单参数隐式类型转化 -> 构造
}
//测试:
void test_list2()
{
// 类模板实例化
list lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
list::iterator it = lt.begin();
while (it != lt.end())
{
*it += 10;
cout << *it << " ";
++it;
}
cout << endl;
}
#include "list.h"
int main()
{
yu::test_list2();
return 0;
}
运行结果:
迭代器封装的价值
迭代器是一种可以遍历的抽象接口!
- 1.封装底层实现,不暴露底层实现原理
- 2.有了迭代器,可以不关心底层实现原理,而是注重使用!
我们上面用的*符号,看似简单几个字母,可底层却是返回data!我们不需要知道它返回的具体是什么,我们只要知道*符号代表着就是获取容器数据的方式!
类封装价值是什么?
- 1.因为不同容器实现原理不同,vector,list同样是*符号,对于vector,*号是原生指针!解引用可以直接获取数据,但是对于list,解引用实现不了我们直接获取数据的方法!(前面说过)有了类封装,我们就可以重载*号,让它的意义改变!有了封装,让我们使得同样的符号,对于不同实现原理的容器,可以有同样的效果!
- 2.我们可以注意到,即使是封装了迭代器,它的物理消耗内存仍只有一个指针大小!也就是说即使是类,他没有过多消耗内存!
根据之前的理论知识,普通的迭代器使用*it是可以修改的,但是有时候我们不期望进行修改,所以我们也要实现一下const版本的迭代器,那什么时候会使用const版本的迭代器,print_list的时候,我们希望传入的链表不要拷贝,所以带上引用,同时呢我们又期望打印的时候不要对元素进行修改,所以此时我们就要传入const版本的迭代器。
这里我们能不能在普通的迭代器前面加一个const就变成const版本的迭代器呢?
这里肯定是不能的,因为此时const修饰的是it,意味着此时的it不能改变,而我们的const迭代器是*it不能被修改,这两个指向的内容是不同的,我们的const迭代器是自己可以修改,但是指向的内容不能被修改。
template
struct __list_cosnt_iterator
{
typedef ListNode Node;
typedef __list_cosnt_iterator self;
public:
Node* _node;
public:
__list_cosnt_iterator(Node* node)
{
_node = node;//一般传入的是begin()
}
self& operator++()//使用++it
{
_node = _node->_next;
return *this;//这里的*this类型是__list_cosnt_iterator
}
self operator++(int)//后置++
{
self tmp(*this);
_node = _node->_next;
return tmp;//返回局部对象,出了作用域就被释放,不能销毁
}
self& operator--()
{
_node = _node->_prev;
return *this;
}
self operator--(int)//后置--
{
self tmp(*this);
_node = _node->_prev;
return tmp;
}
const T& operator*()//不能使用*it修改元素
{
return _node->_data;
}
bool operator!=(const self& s)//使用it != end()
{
return _node != s._node;
}
bool operator==(const self& s)
{
return _node == s._node;
}
};
//再强调一下,迭代器被封装成类!!!
typedef __list_cosnt_iterator const_iterator;//标准化
const_iterator begin() const { return iterator(_head->_next); }//调用构造函数
const_iterator end() const { return _head; } //单参数隐式类型转化 -> 构造
运行结果:
我们发现我们的代码只修改重载*的地方,代码其余的地方都没有变化,这样代码就有点冗余,所以我们这里可以使用模板解决,模板参数可以传任意类型。
template
struct __list_iterator
{
typedef ListNode Node;
typedef __list_iterator self;
public:
Node* _node;
public:
__list_iterator(Node* node)
{
_node = node;//一般传入的是begin()
}
self& operator++()//使用++it
{
_node = _node->_next;
return *this;//这里的*this类型是__list_iterator
}
self operator++(int)//后置++
{
self tmp(*this);
_node = _node->_next;
return tmp;//返回局部对象,出了作用域就被释放,不能销毁
}
self& operator--()
{
_node = _node->_prev;
return *this;
}
self operator--(int)//后置--
{
self tmp(*this);
_node = _node->_prev;
return tmp;
}
Ref operator*()//使用*it
{
return _node->_data;
}
bool operator!=(const self& s)//使用it != end()
{
return _node != s._node;
}
bool operator==(const self& s)
{
return _node == s._node;
}
};
typedef __list_iterator iterator;//标准化
typedef __list_iterator const_iterator;//标准化
迭代器除了重载上面的operator*,同时还要重载operator->,我们先来看一下箭头会在结构体指针访问内部元素的场景下会用到。
void test_list3()
{
struct AA
{
int _a;
int _b;
AA(int a = 0, int b = 0)
{
_a = a;
_b = b;
}
};
list lt;
//匿名对象
lt.push_back(AA());
lt.push_back(AA(1,1));
//有名对象
AA aa1;
lt.push_back(aa1);
AA aa2(1, 1);
lt.push_back(aa2);
list::iterator it = lt.begin();
while (it != lt.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
此时我们想遍历就会出现问题,因为我们*it访问的就是AA,但是呢我们有没有实现AA的流插入的重载,所以我们这里就会出错,如果我们不想使用流插入的重载,我们这里就需要实现迭代器的->重载。
由于->也可以修改数据,所以我们也要实现const版本的,所以上面的模板参数咱接着使用呗!
template
struct __list_iterator
{
typedef ListNode Node;
typedef __list_iterator self;
public:
Node* _node;
public:
__list_iterator(Node* node)
{
_node = node;//一般传入的是begin()
}
self& operator++()//使用++it
{
_node = _node->_next;
return *this;//这里的*this类型是__list_iterator
}
self operator++(int)//后置++
{
self tmp(*this);
_node = _node->_next;
return tmp;//返回局部对象,出了作用域就被释放,不能销毁
}
self& operator--()
{
_node = _node->_prev;
return *this;
}
self operator--(int)//后置--
{
self tmp(*this);
_node = _node->_prev;
return tmp;
}
Ref operator*()//使用*it
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
bool operator!=(const self& s)//使用it != end()
{
return _node != s._node;
}
bool operator==(const self& s)
{
return _node == s._node;
}
};
typedef __list_iterator iterator;//标准化
typedef __list_iterator const_iterator;//标准化
iterator insert(iterator pos, const T& x)
{
Node* cur = pos._node;//访问__list_iterator类成员 - Node*
Node* prev = cur->_prev;
Node* newnode = new Node(x);
//链接上面三个结点prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return newnode;
}
iterator erase(iterator pos)
{
assert(pos != end());
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* next = cur->_next;
//prev cur next //删除cur
prev->_next = next;
next->_prev = prev;
delete cur;
return next;
}
前面说过,此处大家可将迭代器暂时理解成类似于指针,迭代器失效即迭代器所指向的节点的无效,即该节点被删除了。因为list的底层结构为带头结点的双向循环链表,因此在list中进行插入时是不会导致list的迭代器失效的,只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响。
void TestListIterator1()
{
int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
list l(array, array + sizeof(array) / sizeof(array[0]));
auto it = l.begin();
while (it != l.end())
{
// erase()函数执行后,it所指向的节点已被删除,因此it无效,在下一次使用it时,必须先给其赋值
l.erase(it);
++it;
}
}
// 改正
void TestListIterator()
{
int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
list l(array, array + sizeof(array) / sizeof(array[0]));
auto it = l.begin();
while (it != l.end())
{
l.erase(it++); // it = l.erase(it);
}
}
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());
}
这里提一嘴,注意前后两者区别,前面不--end() 因为插入是插在结点前一个!end()返回哨兵结点,插在它的前一个就是尾结点!这里尾删,删的是尾结点,所以--迭代器重载返回前一个结点,也就是尾结点!
※:析构函数是整个链表释放了,但是clear() 释放list非哨兵结点其余结点,链表依然存在!
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it);//!!!注意考虑迭代器失效
}
}
~list()
{
clear();
delete _head;
_head = nullptr;
}
这里再引入一个问题!为什么之前的迭代器类,结点类我们没有写析构函数?现在需要写呢?因为迭代器是获得结点的指针,如果写了析构函数,那么该节点被我们访问后就被释放了,下次就不能访问了,同时内置类型编译器会自动生成析构函数,只不过它什么都不做。
同时这里我们也可以不要写拷贝构造函数,这里不需要深拷贝,只需要浅拷贝即可,因为我们迭代器返回那个位置的指针,返回赋值给it,需要的就是浅拷贝!!!
如果list我们这里不写,默认析构函数只会将我们的头结点释放,那些剩余结点并没有被释放,也就是说list析构任务不仅仅是单个结点,而是所有结点!所以这里我们必须手动写!
1.先构造临时链表tmp(深拷贝!)->> 2. 临时链表与拷贝构造链表交换!
那么如何交换两个链表? 很简单就是交换两个链表的哨兵结点!
//list lt2(lt1)
void swap(list& lt)
{
std::swap(_head, lt._head);//交换地址
}
//lt是lt1的别名
list(const list& lt)
{
_head = new Node;//申请一个Node空间
// new -> 开空间 + 调用构造函数
_head->_next = _head;
_head->_prev = _head;
//注意我们必须初始化lt2的哨兵结点!这样tmp析构的时候析构的不是野指针!
list tmp(lt.begin(), lt.end());
swap(tmp);
}
或者我们也可以这样写
//list lt2(lt1)
list(const list& lt)
{
_head = new Node;//申请一个Node空间
// new -> 开空间 + 调用构造函数
_head->_next = _head;
_head->_prev = _head;
//注意我们必须初始化lt2的哨兵结点!这样tmp析构的时候析构的不是野指针!
for (const auto& e : lt)
{
push_back(e);
}
}
//lt1 = lt2
//*this = lt
list& operator=(const list& lt) //传统写法
{
if (this != <)
{
clear();//清除lt1数据
for (const auto& e : lt)
{
push_back(e);
}
}
return *this;
}
1.构造lt--> 2.交换--> 3.出作用域,临时变量调用析构函数,释放this原空间
//连等可能,所以返回链表!
//出作用域,被替换的lt会调用它的析构函数,析构this的原空间
void swap(list& lt)
{
std::swap(_head, lt._head);//交换地址
}
//lt1 = lt2
//*this = lt
list& operator=(const list lt) //现代写法
{
swap(lt);
return *this;
}
vector与list都是STL中非常重要的序列式容器,由于两个容器的底层结构不同,导致其特性以及应用场景不同,其主要不同如下:
vector | list | |
底 层 结 构 | 动态顺序表,一段连续空间 | 带头结点的双向循环链表 |
随 机 访 问 | 支持随机访问,访问某个元素效率O(1) | 不支持随机访问,访问某个元素 效率O(N) |
插 入 和 删 除 | 任意位置插入和删除效率低,需要搬移元素,时间复杂 度为O(N),插入时有可能需要增容,增容:开辟新空 间,拷贝元素,释放旧空间,导致效率更低 | 任意位置插入和删除效率高,不 需要搬移元素,时间复杂度为 O(1) |
空 间 利 用 率 | 底层为连续空间,不容易造成内存碎片,空间利用率 高,缓存利用率高 | 底层节点动态开辟,小节点容易 造成内存碎片,空间利用率低, 缓存利用率低 |
迭 代 器 | 原生态指针 | 对原生态指针(节点指针)进行封装 |
迭 代 器 失 效 | 在插入元素时,要给所有的迭代器重新赋值,因为插入 元素有可能会导致重新扩容,致使原来迭代器失效,删 除时,当前迭代器需要重新赋值否则会失效 | 插入元素不会导致迭代器失效, 删除元素时,只会导致当前迭代 器失效,其他迭代器不受影响 |
使 用 场 景 | 需要高效存储,支持随机访问,不关心插入删除效率 | 大量插入和删除操作,不关心随机访问 |
我们再来看一下list和vector在排序方面的效率,首先我们要知道算法库中的sort(快排)是不能给链表排序使用的,链表之间的地址是不确定的,而算法库中的排序要求两个迭代器相减,此时就会出现问题,所以我们这里使用链表单独的排序方法(归并)。
#include
#include
#include
#include
using namespace std;
int main()
{
srand(time(0));
const int N = 1000000;
list lt;
vector v;
for (int i = 0; i < N; ++i)
{
auto e = rand();//产生一百万个随机数
lt.push_back(e);//保存到list中
v.push_back(e);//保存到vector中
}
int begin1 = clock();
sort(v.begin(), v.end());
int end1 = clock();
int begin2 = clock();
lt.sort();
int end2 = clock();
printf("vector sort:%d\n", end1 - begin1);
printf("list sort:%d\n", end2 - begin2);
return 0;
}
运行结果:
现在我们再来看一下release版本下的运行结果
此时相差就非常大了,一般我们都知道一款软件发布的形式是release版本,所以此时谁效率更高就不言而说了。此时我们再来改一下代码,将list的数据拷贝到vector,利用vector的排序之后再拷贝回list,看看和list单独排序的效率谁高谁低。
void test_op2()
{
srand(time(0));
const int N = 1000000;
list lt1;
list lt2;
for (int i = 0; i < N; ++i)
{
auto e = rand();
lt1.push_back(e);
lt2.push_back(e);
}
int begin1 = clock();
vector v(lt2.begin(), lt2.end());
sort(v.begin(), v.end());
lt2.assign(v.begin(), v.end());
int end1 = clock();
int begin2 = clock();
lt1.sort();
int end2 = clock();
printf("list copy vector sort copy list sort:%d\n", end1 - begin1);
printf("list sort:%d\n", end2 - begin2);
}
运行结果:
我们发现此时的效率居然比list排序的效率还高,所以list排序的意义不大,如果要使用容器的时候排序,建议使用vector。