文章目录
1. list
列表逻辑上是顺序容器,允许在容器的任何地方进行O(1)的插入和删除操作,并在两个方向上进行迭代。
list的使用对于现在的我们来说,已经没有什么学习成本了,大家自己看文档就看得懂。
1.1 list迭代器的思考
因为链表不是物理上连续的存储空间,所以不能用it < it.end()
来迭代。以前vector、string的迭代器就是原生指针,但是list不是了。
大家想一想list的迭代器解引用是怎么取到数据,++it怎么到下一个节点的位置?这才是我们学习list的重点。
1.2 list::sort
list是支持排序的,但是有大量数据(以10000个数据为分界)要排序的话,不建议用list。
list的sort不支持快排(std::sort),因为快排的源码有两个迭代器相减的操作,list的迭代器是不支持的。
list的迭代器并不能支持随机访问。
void TestOP()
{
srand(time(0));
const int N = 10000000;
vector<int> v;
v.reserve(N);
list<int> lt1;
list<int> lt2;
for (int i = 0; i < N; ++i)
{
//v.push_back(rand());
auto e = rand();
lt1.push_back(e);
lt2.push_back(e);
}
// 拷贝到vector排序,排完以后再拷贝回来
int begin1 = clock();
for (auto e : lt1)
{
v.push_back(e);
}
// 快排
sort(v.begin(), v.end());
size_t i = 0;
for (auto& e : lt1)
{
e = v[i++];
}
int end1 = clock();
int begin2 = clock();
lt2.sort();
int end2 = clock();
printf("vector Sort:%d\n", end1 - begin1);
printf("list Sort:%d\n", end2 - begin2);
}
用list排序还不如把list的数据拷贝给vector,用std::sort排好后再拷回来效率高。
list1
进行如上操作,list2
用list::sort排序。测试结果如下:
1.3 迭代器分类
迭代器分为三类:单向、双向、随机迭代器。InputIterator
表示只写迭代器,三种类型迭代器它都支持。
2. list模拟实现
2.1 快速搭建list框架
#pragma once
namespace Yuucho
{
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)
{}
};
template<class T>
class list
{
typedef list_node<T> Node;
public:
list()
{
_head = new Node();
_head->_next = _head;
_head->_prev = _head;
}
private:
Node* _head;
};
}
2.2 list的迭代器设计
假设我们认为list的迭代器就是节点的原生指针,那你解引用的话是不支持直接取数据的。节点的指针解引用还是节点,节点的指针++能到下一个节点吗?不能到,节点的物理空间并不连续。那怎么办?
节点的指针不支持,但是可以间接支持。节点的指针取data就是数据,节点的指针等于next就有了下一个节点的地址。
因此我们用自定义类型去封装,运算符重载就可以控制行为。OK,搞定。
我们自己实现就没必要像源码那样复杂,可以简化一下。
节点不属于迭代器,不需要迭代器释放,所以不需要写析构函数。
拷贝构造和赋值重载,默认生成的浅拷贝就可以。
template<class T>
struct __list_iterator
{
typedef list_node<T> Node;
typedef __list_iterator<T> self;
Node* _node;
// 通过节点的指针来构造迭代器
__list_iterator(Node* node)
:_node(node)
{}
// 析构函数 -- 节点不属于迭代器,不需要迭代器释放
// 拷贝构造和赋值重载 -- 默认生成的浅拷贝就可以
T& operator*()
{
return _node->_data;
}
// 支持list存储自定义类型时用->访问数据
// 做到像指针一样使用
T* operator->()
{
//return &(operator*());
return &_node->_data;
}
self& operator++()
{
_node = _node->_next;
return *this;
}
// 后置++
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;
}
bool operator!=(const self& it)
{
return _node != it._node;
}
bool operator==(const self& it)
{
return _node == it._node;
}
};
关于->重载的可读性优化:
至此,我们就可以在list类中使用迭代器了:
typedef __list_iterator<T> iterator;
iterator begin()
{
return iterator(_head->_next);
//return _head->_next;
}
iterator end()
{
return iterator(_head);// end就是哨兵位的头结点
}
测试:
源码中的迭代器是这样的:
template<class T, class Ref, class Ptr>
struct __list_iterator
{
typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> consut_iterator;
typedef __list_iterator<T, Ref, Ptr> self;
// ......
}
源码为什么要提供三个模板参数呢?
我们先来讨论一下consut_iterator
和iterator
有什么区别。
假设我们写了一个打印链表的函数:为了减少拷贝我们传引用,防止引用实体被修改我们有加了const
。
void print_list(const list<int>& lt)
{
list<int>::iterator it = lt.begin();
while (it != lt.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
那么此时我们的代码就有问题了,因为const
对象不能调用非const
的成员函数begin。
如果我们为const
对象提供以下迭代器接口,那会出大问题。
typedef __list_iterator<T> iterator;
iterator begin() const
{
return iterator(_head->_next);
//return _head->_next;
}
iterator end() const
{
return iterator(_head);// end就是哨兵位的头结点
}
const
对象返回普通迭代器,那么我的普通迭代器就可读可写了。
迭代器模仿的是指针的行为,为了让const
对象的迭代器在调用解引用*
和箭头->
时不修改原数据,我们应该这样写。
const T& operator*()
{
return _node->_data;
}
// 支持list存储自定义类型时用->访问数据
// 做到像指针一样使用
const T* operator->()
{
//return &(operator*());
return &_node->_data;
}
但是如果你直接在迭代器类里面这样改,又会有一个问题。普通对象用什么?
那为了解决这个问题,很多人的做法是再写一个__list_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;
// 通过节点的指针来构造迭代器
__list_iterator(Node* node)
:_node(node)
{}
// 析构函数 -- 节点不属于迭代器,不需要迭代器释放
// 拷贝构造和赋值重载 -- 默认生成的浅拷贝就可以
Ref operator*()
{
return _node->_data;
}
// 支持list存储自定义类型时用->访问数据
// 做到像指针一样使用
Ptr operator->()
{
//return &(operator*());
return &_node->_data;
}
self& operator++()
{
_node = _node->_next;
return *this;
}
// 后置++
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;
}
bool operator!=(const self& it)
{
return _node != it._node;
}
bool operator==(const self& it)
{
return _node == it._node;
}
};
我们就可以在list类中这样使用:
typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;
const_iterator begin()
{
return iconst_iterator(_head->_next);
//return _head->_next;
}
const_iterator end()
{
return const_iterator(_head);// end就是哨兵位的头结点
}
iterator begin()
{
return iterator(_head->_next);
//return _head->_next;
}
iterator end()
{
return iterator(_head);
}
至此,我们就很好地提供了list
的普通对象和const
对象的迭代器。
2.3 list迭代器使用流程梳理
紫色代表普通对象,蓝色代表const对象。
普通对象调用的是普通的begin,普通的begin返回的是普通迭代器,普通迭代器是参数没加const的迭代器类typedef来的,T&和T*传给迭代器类模板,分别应用到了Ref和Ptr的位置。
const对象调用的是const的begin,const的begin返回的是const迭代器,const迭代器是参数加const的迭代器类typedef来的,const T&和const T*传给迭代器类模板,分别应用到了Ref和Ptr的位置。
const_iterator的第一个模板参数不传const T的原因是:
因为lt调用begin构造迭代器传的是list_node<int>*
类型,如果第一个模板参数写成const T,那Node就变成了list_node<const int>*
类型。此时这两个就是不同的类型了,编译器无法用不同类型的参数来构造迭代器。
node是list_node<int>*
类型,_node是list_node<const int>*
类型,无法构造。
__list_iterator(Node* node)
:_node(node)
{}
2.4 push_back
这里和数据结构链表实现思路是一致的,不多赘述。
void push_back(const T& x)
{
传统写法
//Node* tail = _head->_prev;
//Node* newnode = new Node(x);
_head tail newnode
//tail->_next = newnode;
//newnode->_prev = tail;
//newnode->_next = _head;
//_head->_prev = newnode;
// 复用insert
insert(end(), x);
}
2.5 insert
这里和数据结构链表实现思路是一致的,不多赘述。只是多了新插入节点的返回值。
// 插入在pos位置之前
iterator insert(iterator pos, const T& x)
{
Node* newNode = new Node(x);
Node* cur = pos._node;
Node* prev = cur->_prev;
// prev newnode cur
prev->_next = newNode;
newNode->_prev = prev;
newNode->_next = cur;
cur->_prev = newNode;
return iterator(newNode);
}
2.6 erase
这里和数据结构链表实现思路是一致的,不多赘述。只是多了新释放节点的后一个节点的返回值。防止迭代器失效。
iterator erase(iterator pos)
{
assert(pos != end());
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* next = cur->_next;
// prev next
prev->_next = next;
next->_prev = prev;
delete cur;
return iterator(next);
}
2.7 push_front, pop_back,pop_front
复用insert和erase。
void push_front(const T& x)
{
insert(begin(), x);
}
void pop_back()
{
erase(--end());
}
void pop_front()
{
erase(begin());
}
2.8 析构和clear
我们先清理掉数据再析构。
~list()
{
clear();
delete _head;
_head = nullptr;
}
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it);
}
}
2.9 拷贝构造
链表的深拷贝得一个一个节点拷贝。不是所有的浅拷贝都不好,比如说迭代器类的默认拷贝函数就能让迭代器和源节点的指针一起指向同一块空间(但节点并不属于迭代器),这正是我们想要的。
void empty_init()
{
_head = new Node();
_head->_next = _head;
_head->_prev = _head;
}
template <class InputIterator>
list(InputIterator first, InputIterator last)
{
empty_init();
while (first != last)
{
push_back(*first);
++first;
}
}
// 两个链表交换,交换的是头指针
void swap(list<T>& lt)
{
std::swap(_head, lt._head);
}
// lt2(lt1) -- 现代写法
list(const list<T>& lt)
{
// 不能拿随机值交换,不然析构会出问题
empty_init();
list<T> tmp(lt.begin(), lt.end());
swap(tmp);
}
// lt2 = lt1
list<T>& operator=(list<T> lt)
{
swap(lt);
return *this;
}
3. list迭代器失效问题
list的insert迭代器不失效,不存在出现野指针或者意义变了的问题。因为它每一个节点相对独立,扩容也是一个一个扩。迭代器指向哪儿就是哪儿。但是为了保持设计统一和支持查找被插入的节点,所以还是用了返回值。
但是erase的迭代器失效就很明显了。因为迭代器指向的节点被释放了,野指针问题显而易见。所以用被erase的节点的下一个节点作返回值来解决这个问题