List
List接口使用
- List:双向带头循环的链表,不支持随机访问,排序就是一个大问题
当大量的插入数据的时候就体现出了优势。
在任意位置以O(1)的时间复杂度插入数据.
- 只有一种遍历方式就是迭代器,因为他的物理结构是不连续的无法用下标的方式进行访问
list<int>::iterator it=lt.begin();
while(it!=lt.end())
{
cout<<*it<<" ";
++it;
}
cout<<endl;
-
lt.sort();
自身携带的排序方式,但是效率不太高。 -
lt.unique();
去重数据,先进行排序才能够去除干净。 -
lt.splice()
;接合,将链表的节点直接转移到另一个里面去.
#include<iostream>
#include<list>
using namespace std;
int main()
{
//list<int> l1;
//l1.push_back(1);
//l1.push_back(5);
//l1.push_back(3);
//l1.push_back(4);
//l1.push_back(2);
//l1.push_back(2);
//l1.push_back(2);
//l1.push_back(2);
//l1.sort();
//l1.unique();//先排序再去重效率更高
//list<int>::iterator it = l1.begin();
//while (it != l1.end())
//{
// cout << *it << " ";
// ++it;
//}
//cout << endl;
list<int> l1;
list<int> l2;
for (int i = 1; i <= 4; i++)
{
l1.push_back(i);
l2.push_back(i * 10);
}
auto it = l1.begin();
++it;
//将l2拼接到l1的指定位置处
l1.splice(it,l2);
l1.splice(it,l2,l2.begin(),l2.end());
//advance 让it指向指定下标位置的数,必须先从头开始
it = l1.begin();
advance(it,2);
//相当于将指定部分长度元素组,头插
l1.splice(l1.begin(),l1,it,l1.end());
list<int>::iterator it1 = l1.begin();
while (it1 != l1.end())
{
cout << *it1 << " ";
++it1;
}
cout << endl;
return 0;
}
list的模拟实现
迭代器
自定义类型进行封装节点指针,自定义类型的运算符重载实现++
,和operator*
。
end()是最后一个数据的下一个位置,就是头结点
迭代器不只是原生指针有可能是。
类型的意义:
链表节点的原生指针和一个迭代器对象,他们占用的空间时一样大的,都是4byte,
并且存的值也是一样的。但是对他们使用运算符的意义和结果都是不一样的。
void f()
{
Node*pnode=_head->next;
iterator it=_head->next;
*pnode;
*it;
}
迭代器的拷贝构造和赋值重载以及析构函数都不需要我们自己实现,默认生成的就行。因为,迭代器是借助节点的指针访问和修改链表。节点属于链表不属于迭代器,所以他不用管释放的过程。默认生成的拷贝就是浅拷贝足以满足拷贝构造和赋值的需求,两个指针指向同一个迭代器同一个位置.
通过自己模拟实现迭代器的功能,使用迭代器去访问和修改链表的相关函数。
const迭代器
普通迭代器可读可写,const迭代器只能读不能修改.就是实现一个const iterator
让容器的内容不可修改.这是两个迭代器.
所以要么实现一个不能修改的迭代器一般适应容器是const的,对于const类型和普通类型本身需要声明两个类的迭代器,除了类型名和返回值不同其他都相同,但是为了避免代码的冗余和重复.
可以通过控制模板参数不同的返回值来区别两个类的迭代器。加了一个Ref
这样返回值是const类型就去调用const_iterator
的begin和end,普通类型就去调用普通类型的内个。
通过模板参数实现代码的复用,避免了代码的冗余。
如果迭代器指针指向的是一个结构,那我们还是要访问他本身的成员变量。所以要重载一个 operator->
使用场景:日期类。迭代器像Date*
指针一样使用,结构体里面用->访问成员变量it->_year;
- 使用场景(1->2)
void test2()
{
list<Date> lt;
lt.push_back(Date(2022, 1, 1));
lt.push_back(Date(2022, 2, 2));
lt.push_back(Date(2022,3,3));
list<Date>::iterator it = lt.begin();
while (it != lt.end())
{
//1. std::cout << (*it)._year << "." << (*it)._month << "." << (*it)._day << std::endl;
//2. it->:Date
std::cout << it->_year<<"."<< it->_month << "."<< it->_day<< std::endl;
++it;
}
std::cout << std::endl;
}
it.operator->(),返回来是Date*``,Date*
访问成员又是一个->
所以本来应该是it->->year
,这样运算符的可读性变差。编译器进行优化为一个->
,所有类型想要重载都会是这样的。
//迭代器中重载这样的函数
T *operator->()
{
return &_node->_data;
}
如果是const迭代器,所以类似于Ref的添加一个模板参数Ptr.
增删查改
string
和vector
是连续的物理空间,模拟指针的行为,像指针一样去访问容器,原生指针就是天然的迭代器。支持随机访问,但是增容可能造成浪费空间,插入删除要挪动数据.
list
按需申请和释放空间,在任意位置支持O(1)
的插入删除。所以vector和list是互补的两个数据结构。
iterator 类去封装节点指针,重载运算符,让他像指针一样。STL的所有容器都不能保证线程安全。
泛型,复用,封装。
insert()
在既定pos的前一个位置进行插入新的节点,list 的迭代器,insert之后不会发生失效。vector会野指针(异地扩容)和意义改变(数据移动覆盖导致)
插入之后,STL里面的会返回插入节点的迭代器指针。
erase()
不能将头结点删除,断言处理.返回删除之后下一个节点.
拷贝构造的实现:深浅拷贝的问题
push_back()的前提是这个链表被初始化了,就需要他已经存在哨兵位的头节点,所以在调用之前要创建一个头结点,并且自己指向自己。
list(const list<int>& lt)
{
_head = new Node();
_head->_next = _head;
_head->_prev = _head;
for (auto e : lt)
{
push_back(e);
}
}
赋值构造
list& operator=(const list<int>& lt)
{
if (this != <)
{
clear();
_head = new Node();
_head->_next = _head;
_head->_prev = _head;
for (auto e : lt)
{
push_back(e);
}
}
return *this;
}
//升级版本
template<class InputIterator >
list(InputIterator first, InputIterator last)
{
_head = new Node();
_head->_next = _head;
_head->_prev = _head;
while (first != last)
{
push_back(*first);
++first;
}
};
list<T>(const list<T>& lt)
{
//避免交换的时候_head为随机值
_head = new Node();
_head->_next = _head;
_head->_prev = _head;
list<T>tmp(lt.begin(),lt.end());
std::swap(_head, tmp._head);
}
list<T>& operator=(list<T> lt)
{
std::swap(_head, lt._head);
return *this;
}
非法间接寻址
当迭代器区间初始化list(InputIterator first, InputIterator last)
和list(size_t n, const T& val = T())
同时存在时,
void test4()
{
list<Date>lt1(5,Date(2022,1,1));//类型不同,走得list()
list<int>lt2(5,1);//两个都是int,走得迭代器区间那个,造成间接非法寻址
//匹配函数的时候,有现成的是不会去推演模板的
}
- 可以将size_t重载一个int类型就可以解决因为参数类型不同造成的匹配函数错误.
反向迭代器
本身包含正向迭代器,对于正向迭代器的一种封装,重载++--
的方向。区别就是++和–的方向是相反的。
正向迭代器的开始就是反向迭代器的结束。
operator*
取前一个位置主要就是为了让反向迭代器开始和结束跟正向迭代器对称。
reference operator* const()
{
Iterator tmp=current;
return *--tmp;
}
所以以后别的不是链表的反向迭代器都可以像这个一样进行复用。
iterator
是哪个容器的迭代器,reverse_iterator
就可以适配哪个容器的反向迭代器.
template <class Iterator, class Ref, class Ptr>
class reverse_iterator
{
typedef reverse_iterator<Iterator, Ref, Ptr> self;
public:
reverse_iterator(Iterator it)
:_it(it)
{}
Ref operator*()
{
//return *_it;
Iterator prev = _it;
return *--prev;
}
Ptr operator->()
{
return &operator*();
}
self& operator++()
{
--_it;
return *this;
}
self& operator--()
{
++_it;
return *this;
}
bool operator!= (const self & rit) const
{
return _it != rit._it;
}
private:
Iterator _it;
};
删除节点后,只有指向当前节点的迭代器失效了,其前后的迭代器仍然有效,
因为底层为不连续空间,只有被删除的 节点才会失效
vector由于在头部插入数据效率很低,所以没有提供push_front方法,但是如果需要有insert可以使用.