c++STL-list的使用和迭代器
- 序列容器list
- 构造函数
- 析构函数
- 赋值重载
- 细说STL的迭代器(Iterator)
- list的迭代器
- 用模板简化const修饰的迭代器
- 容量(Capacity)
- 元素通道(Element access)
- 修改(Modifiers)
- assign 赋值
- 头插 push_front、emplace_front
- 尾插 push_back、emplace_back
- 头删pop_front和尾删pop_back
- 指定插入 emplace和insert
- 删除erase、交换swap、清空clear
- resize
- 操作Operations
- sort
- 转移splice、移除remove、remove_if
- 去重unique,归并merge,翻转reverse
- 其他
序列容器list
建议先看c++STL-string的使用-CSDN博客和c++STL-string的模拟实现-CSDN博客。
list
是序列式容器的一种,底层是带头双向循环链表。这个结构的链表也是很多链表的参考。
原型:
template < class T, class Alloc = allocator<T> >class list;
list
是可以在时间复杂度为 O ( 1 ) O(1) O(1)内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。
list
的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。
除了带头双向循环链表,STL还有单链表forward_list
。list
与forward_list
非常相似:最主要的不同在于forward_list
是单链表,只能朝前迭代,已让其更简单高效。
list
和forward_list
不支持任意位置的随机访问,比如:要访问list
的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list
还需要一些额外的空间,以保存每个节点的相关联信息。
和string
、vector
一样,简单看看list
的使用即可开始模拟实现。同样是c++98为主,c++11为辅。
构造函数
个人觉得比较好用的构造函数:
explicit list (const allocator_type& alloc = allocator_type());
explicit list (size_type n, const value_type& val = value_type(),const allocator_type& alloc = allocator_type());
template <class InputIterator>list (InputIterator first, InputIterator last,const allocator_type& alloc = allocator_type());
list (const list& x);//初始化列表需要编译器支持c++11
list (initializer_list<value_type> il,const allocator_type& alloc = allocator_type());
凡是alloc
都是和空间配置器有关,在初学使用的阶段可以不理会。
explicit
关键字会禁止构造函数(特别是单形参的构造函数)的隐式转换,也就是说带explicit
的构造函数要按它给定的形参列表来调用。
简单使用:
#include<iostream>
#include<list>
using namespace std;void print(list<int>& a) {for (auto& x : a)cout << x << ' ';cout << endl;
}void f1() {//explicit list (const allocator_type& alloc = allocator_type());//可以理解为什么参数都不用给list<int>a;
}void f2() {//explicit list (size_type n, const value_type& val = value_type(),//const allocator_type& alloc = allocator_type());//用n个数据val初始化listlist<int>b(3, 6);print(b);
}void f3() {//template <class InputIterator>//list(InputIterator first, InputIterator last,// const allocator_type & alloc = allocator_type());//迭代器初始化list<int>b(3, 6);list<int>c(b.begin(), b.end());print(c);
}void f4() {初始化列表需要编译器支持c++11//list(initializer_list<value_type> il,// const allocator_type & alloc = allocator_type());list<int>d{ 0,1,2,3,4,5 };list<int>e = { 0,1,2,3 };print(d);
}int main() {//f1();//f2();//f3();f4();return 0;
}
析构函数
list
的结点不连续,因此先释放非哨兵卫结点的内存,再释放哨兵卫结点。
赋值重载
list& operator= (const list& x);//初始化链表需要编译器支持c++11
list& operator= (initializer_list<value_type> il);
简单使用:
#include<iostream>
#include<list>
using namespace std;void print(list<int>& a) {for (auto& x : a)cout << x << ' ';cout << endl;
}void f1() {list<int>a = { 1,1,4,5,1,4 };list<int>b;b = a;print(b);
}void f2() {list<int>a;a = { 0,7,2,1 };print(a);
}int main() {//f1();f2();return 0;
}
细说STL的迭代器(Iterator)
list
还是那几个迭代器,但迭代器的内容和以往有很大的不同。
iterator begin() noexcept;
const_iterator begin() const noexcept;iterator end() noexcept;
const_iterator end() const noexcept;reverse_iterator rbegin() noexcept;
const_reverse_iterator rbegin() const noexcept;reverse_iterator rend() nothrow;
const_reverse_iterator rend() const nothrow;
//带c的迭代器加了const关键字进行修饰,需要编译器支持c++11
const_iterator cbegin() const noexcept;
const_iterator cend() const noexcept;
const_reverse_iterator crbegin() const noexcept;
const_reverse_iterator crend() const noexcept;
list
的迭代器不再是原生指针,而是和list
类伴生的类。
STL和string
的迭代器的分类:
- 迭代器在功能上有分类:
- 正向。
- 反向。
const
。
- 迭代器在性质上分类(由容器的底层实现决定):
- 单向:特点是只支持
++
。经典的有单链表(forward_list
)、哈希表(unordered_map
)的迭代器。 - 双向:特点是除了支持
++
,还支持--
。经典的有这次的双向链表(list
)、红黑树(map
和set
)。 - 随机:特点是支持
++
、--
、+
、-
,经典的有vector
,string
,双端队列deque
。
随机迭代器算一种双向迭代器。前者有的特征后者没有。
双向是一种特殊的单向。vector
支持算法库里的随机迭代器和双向迭代器,这些迭代器之间是类的继承关系。
库里有的函数,需要容器内的工具支持某种特定的迭代器才能使用。
例如list
自带的赋值函数assign
,InputIterator
指的就是单向迭代器。
template <class InputIterator>void assign (InputIterator first, InputIterator last);
例如算法库(algorithm
)的sort
,RandomAccessIterator
指的就是随机迭代器。
template <class RandomAccessIterator>void sort (RandomAccessIterator first, RandomAccessIterator last);
template <class RandomAccessIterator, class Compare>void sort (RandomAccessIterator first, RandomAccessIterator last, Compare comp);
再例如list
自带的成员函数unique
,BinaryPredicate
指的就是双向迭代器。
template <class BinaryPredicate>void unique (BinaryPredicate binary_pred);
list的迭代器
list
因为底层的数据结构是双向循环链表,因此迭代器必不可能用原生指针代替。因为结点的含义丰富,例如解引用*
能访问的数据有前驱、后继和数据,需要支持++
后跳转到下一结点、--
后跳转到前一个结点等功能。
因此list
的迭代器至少是个结构体(类),才能重载如此丰富的功能。
而且迭代器最重要的作用是封装,屏蔽底层差异和实现细节,提供了统一的访问修改方式。比如链表、数组、二叉树、哈希表等,无论底层的数据结构再怎么复杂,都能用统一的迭代器完整遍历。
list
的迭代器模拟指针的行为。但若使用自定义类型的链表,->
会有两个,编译器不做处理的话会有2个,处理后只有1个。这点在模拟实现时会特别说明。
用模板简化const修饰的迭代器
迭代器是模拟指针,通过对迭代器解引用可得到数据。
普通迭代器是可读可写,const
修饰的迭代器是只读。
且不能对迭代器对象本身加
const
修饰,这样迭代器不能修改,而迭代器还要完成遍历的功能。即迭代器对象的类型须是const T*
,不能是T* const
。所以只读迭代器是
typedef const T* const_iterator;
。这样,只读迭代器和原始迭代器又是不同的类型,需要加单独的类。且
*
和->
两个解引用都有可能修改迭代器指向的内容,因此对只读迭代器,需要对this
加const
修饰,并将原结点的指针强制转化成const T*
再返回。
但若是加const
和不加const
的迭代器都来一份,则会显得代码很冗余。例如这里的两个迭代器类,它们之间的成员函数除了返回值几乎都是一样的。
同一个类模板,只要实例化的参数不同,就是完全不同的类型。
因此可以通过两个不同的模板参数{T,T&,T*}
和{T,const T&,const T*}
来区分iterator
和const_iterator
。list
底层的迭代器就是这样设计的:
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*> const_iterator;typedef __list_iterator<T, Ref, Ptr> self;
//...这里截取库中的一部分typedef Ptr pointer;typedef Ref reference;reference operator*() const { return (*node).data; }
//...pointer operator->() const { return &(operator*()); }//...
};template <class T, class Alloc = alloc>
class list {
public://...typedef __list_iterator<T, T&, T*> iterator;typedef __list_iterator<T, const T&, const T*> const_iterator;//...iterator begin() { return (link_type)((*node).next); }const_iterator begin() const { return (link_type)((*node).next); }iterator end() { return node; }const_iterator end() const { return node; }
};
Ref
是reference
也就是引用的缩写,另一个Ptr
是point
也就是指针的缩写。
iterator
和const_iterator
的模板参数不同,不是同一类型。在使用迭代器时,list
会上传不同的模板参数,生成两个不同的迭代器类,这样就实现了同一个迭代器类根据模板参数演变成两种不同的类。
容量(Capacity)
list
的和容量有关的成员函数只有2个。
bool empty() const noexcept;
size_type max_size () const noexcept;
根据string
的经验,很容易了解到它们的功能:empty
用于表示list
是否为空,max_size
用于表示能申请的结点的最大值。
简单使用:
#include<iostream>
#include<list>
using namespace std;int main() {list<int>a;cout << a.empty() << endl;//生成匿名对象调用成员函数cout << list<int>().max_size() << endl;cout << list<char>().max_size() << endl;return 0;
}
在x86(32位)环境下输出(测试环境:Devcpp5.11、MSVC):
1
357913941
357913941
在x64(64位)环境下输出:
1
768614336404564650
768614336404564650
说明list
能申请的最大结点数和类型无关,和编译环境有关。
元素通道(Element access)
reference front();
const_reference front() const;reference back();
const_reference back() const;
reference
在某个版本的STL中的定义:
template <class T, class Alloc = alloc>
class list {//...typedef T value_type;typedef value_type& reference;typedef const value_type& const_reference;//...
}
所以reference
就是结点的数据域的引用。
简单使用front
和back
:
#include<iostream>
#include<list>
using namespace std;int main() {list<int>a = { 1,1,4,5,1,4,0,7,2 };cout << a.front()<<endl;//输出1cout << a.back()<<endl;//输出2return 0;
}
链表的遍历可以支持下标访问,但效率会特别慢,这个是由链表的性质决定。
修改(Modifiers)
可以根据string
的迭代器来猜测它们的作用:
assign 赋值
assign
是赋值,需要编译器支持c++11。
#include<iostream>
#include<list>
#include<string>
using namespace std;void f1() {list<int>a;string st = "Railgun";//template <class InputIterator>//void assign(InputIterator first, InputIterator last);a.assign(st.begin(), st.end());for (auto& x : a)cout << x << ' ';
}void f2() {list<int>a;//void assign (size_type n, const value_type& val);a.assign(3,6);for (auto& x : a)cout << x << ' ';
}void f3() {list<int>a;//赋值初始化列表//void assign (initializer_list<value_type> il);a.assign({2,0,2,5,5,6});for (auto& x : a)cout << x << ' ';
}int main() {//f1();//f2();f3();return 0;
}
头插 push_front、emplace_front
template <class... Args>void emplace_front (Args&&... args);
void push_front (const value_type& val);
void push_front (value_type&& val);
在初学阶段暂时将它们当成单结点的头插使用。
emplace_front
需要编译器支持c++11。
#include<iostream>
#include<list>
using namespace std;void f1() {list<int>a;a.push_front(1);a.push_front(2);a.emplace_front(3);for (auto& x : a)cout << x << ' ';cout << endl;
}int main() {f1();return 0;
}
尾插 push_back、emplace_back
void push_back (const value_type& val);
//以下函数需要编译器支持c++11
void push_back (value_type&& val);
template <class... Args>void emplace_back (Args&&... args);
在初学阶段暂时将它们当成单结点的尾插使用。
emplace_back
需要编译器支持c++11。
#include<iostream>
#include<list>
using namespace std;void f1() {list<int>a;a.emplace_back(1);a.push_back(2);a.push_back(3);for (auto& x : a)cout << x << ' ';cout << endl;
}int main() {f1();return 0;
}
头删pop_front和尾删pop_back
void pop_front();
void pop_back();
双向链表的头删、尾删。
#include<iostream>
#include<list>
using namespace std;void f1() {list<int>a = { 0,1,2,3,4,5,6 };a.pop_back();for (auto& x : a)cout << x << ' ';cout << endl;a.pop_front();for (auto& x : a)cout << x << ' ';cout << endl;
}int main() {f1();return 0;
}
指定插入 emplace和insert
iterator insert (iterator position, const value_type& val);void insert (iterator position, size_type n, const value_type& val);
template <class InputIterator>void insert (iterator position, InputIterator first, InputIterator last);//以下函数需要编译器支持c++11
iterator insert (const_iterator position, initializer_list<value_type> il);
template <class... Args>iterator emplace (const_iterator position, Args&&... args);
使用:
#include<iostream>
#include<list>
#include<vector>
using namespace std;void print(list<int>& a) {for (auto& x : a)cout << x << ' ';cout << endl;
}void f1() {list<int>a = { 0,1,2,3,4,5,6 };//iterator insert (iterator position, const value_type& val);//list的迭代器不支持 + 整数auto it = a.insert(++a.begin(), 99);cout << *it << endl;print(a);
}void f2() {list<int>a = { 0,1,2,3,4,5,6 };//iterator insert (iterator position, const value_type& val);//list的迭代器不支持 + 整数auto it = a.insert(++a.begin(), 3,99);cout << *it << endl;//insert返回被插入的位置的迭代器print(a);
}void f3() {list<int>a = { 0,1,2,3,4,5,6 };vector<int>x = { 0,1,1,4,5,1,4 };int y[] = { 0,7,2,1 };//template <class InputIterator>//void insert(iterator position, InputIterator first, InputIterator last);auto it = a.begin();for (int i = 0; i < 3; i++)++it;a.insert(it, x.begin(), x.end());print(a);//静态数组的指针也能当迭代器使用a.insert(it, y, y + 4);print(a);
}void f4() {list<int>a = { 0,1,2,3,4,5,6 };//iterator insert(const_iterator position, initializer_list<value_type> il);auto it = a.begin();for (int i = 0; i < 5; i++)++it;a.insert(it, {3,1,4,1,5,9});print(a);
}void f5() {list<int>a = { 0,1,2,3,4,5,6 };//template <class... Args>//iterator emplace(const_iterator position, Args&&... args);auto it = a.begin();for (int i = 0; i < 4; i++)++it;a.emplace(it, 9);print(a);
}int main() {//f1();//f2();//f3();//f4();f5();return 0;
}
删除erase、交换swap、清空clear
这些无论哪个标准,使用方式都是一样的。
iterator erase (const_iterator position);
iterator erase (const_iterator first, const_iterator last);void swap (list& x);void clear() noexcept;
使用:
#include<iostream>
#include<list>
#include<vector>
using namespace std;void print(list<int>& a) {for (auto& x : a)cout << x << ' ';cout << endl;
}void f1() {list<int>a = { 0,1,2,3,4,5,6 };auto it = ++a.begin();for (int i = 0; i < 3; i++)++it;//iterator erase (const_iterator position);//删除指定迭代器表示的结点//erase会导致迭代器失效,需要重新赋值it=a.erase(it);print(a);//iterator erase (const_iterator first, const_iterator last);//删除指定迭代器表示的结点it=a.erase(a.begin(),it);print(a);
}void f2() {list<int>a = { 0,1,2,3 };list<int>b = { 0,7,2,1 };//void swap (list& x);print(a);a.swap(b);print(a);//void clear() noexcept;b.clear();print(b);//打印不出东西,因为已经被清空的只剩哨兵卫结点
}int main() {//f1();f2();return 0;
}
resize
void resize (size_type n, const value_type& val);//这个需要编译器支持c++11
void resize (size_type n);
相比于string
和vector
的resize
。list
的resize
是再次插入结点,因此放在了modified
。
使用:
#include<iostream>
#include<list>
#include<vector>
using namespace std;void print(list<int>& a) {for (auto& x : a)cout << x << ' ';cout << endl;
}void f1() {list<int>a = { 0,1,2,3,4,5,6 };//void resize (size_type n, const value_type& val);//相当于扩容,用val填充print(a);a.resize(9, 9);print(a);//缩容时第2个形参无用a.resize(5, 6);print(a);//这个需要编译器支持c++11// 啥都不填默认填充0//void resize(size_type n);a.resize(15);print(a);
}int main() {f1();return 0;
}
操作Operations
这些函数相比于vector
和string
多了很多新东西。
sort
最适合链表的排序算法是归并排序。因此list
的sort
的底层是归并排序。
void sort();
template <class Compare>void sort (Compare comp);
链表的sort
默认支持升序。内部有默认升序的仿函数(STL的六大组件)。比如升序用的仿函数是less
,降序是greater
。
但这个sort
因为链表的空间访问的原因(指结点交换方式),使得效率很低,完全可以将链表中的数据提取到数组中,对数组排序后再插入回链表,比直接调用这个成员函数要快。
而且算法库的sort
是随机迭代器,list
的迭代器不支持,因此不能用算法库里的sort
对list
进行排序。
但无论是什么原因,都使得这个成员函数很少用。
debug
性能下,递归、循环会加很多调试信息,很多优化也没开。因此测试性能最好还是在release
下。
转移splice、移除remove、remove_if
void splice (const_iterator position, list& x);void splice (const_iterator position, list& x, const_iterator i);void splice (const_iterator position, list& x,const_iterator first, const_iterator last);
void remove (const value_type& val);
template <class Predicate>void remove_if (Predicate pred);//以下函数需要编译器支持c++11
void splice (const_iterator position, list&& x);
void splice (const_iterator position, list&& x, const_iterator i);
void splice (const_iterator position, list&& x,const_iterator first, const_iterator last);
splice
有结合、粘贴的意思,但在list
中的应用却是转移,即将另一个链表的一部分转移到当前链表之前。
list
自带remove
是去除指定值的结点。涉及仿函数的问题。remove
不上传形参,啥都不做。
remove_if
则是满足pred
的结点会被移除。pred
可以是仿函数,可以是回调函数。
使用:
#include<iostream>
#include<list>
#include<vector>
using namespace std;void print(list<int>& a) {for (auto& x : a)cout << x << ' ';cout << endl;
}void f1() {list<int>a = { 0,1,1,2,3,4,3,5,6,7,7,8,8,8,8,8,9 };list<int>b = { 3,14,1,5 };//void splice (const_iterator position, list& x);//将x内嵌到a中指定位置a.splice(++a.begin(), b);print(a);print(b);//b内的结点被转移b = { 2,71,8,2,8 };//void splice (const_iterator position, list& x, const_iterator i);//将x中指定迭代器表示的结点移动到主链中a.splice(++a.begin(), b, ++b.begin());print(a);print(b);//void splice (const_iterator position, list& x,//const_iterator first, const_iterator last);//将指定区域迭代器内的结点转移b.splice(++b.begin(), a, ++a.begin(), --a.end());print(a);print(b);
}void f2() {list<int>a = { 0,1,2,3,3,3,4,5,6,7,8,9,3,10 };//void remove (const value_type& val);a.remove(3);print(a);
}bool f3_1(int a) {return a % 2 == 0;
}void f3() {list<int>a = { 0,1,2,3,4,5,6,7,8,9,10 };//template <class Predicate>//void remove_if(Predicate pred);a.remove_if(f3_1);//移除偶数print(a);
}int main() {//f1();//f2();f3();return 0;
}
去重unique,归并merge,翻转reverse
void unique();
template <class BinaryPredicate>void unique (BinaryPredicate binary_pred);void merge (list& x);
template <class Compare>void merge (list& x, Compare comp);void reverse() noexcept;//以下函数需要编译器支持c++11
void merge (list&& x);
template <class Compare>void merge (list&& x, Compare comp);
unique
会针对连续位置上的链表进行去重。例如{1,1,2,3,2}
,unique
会将链表变成{1,2,3,2}
。
而且还能上传比较器,这个比较器可以是回调函数,也可以是仿函数。
merge
能按指定比较器将两条链表合并在一起,不上传比较器的话默认按照升序的方式归并。
reverse
能将链表整个翻转。
使用:
#include<iostream>
#include<list>
#include<vector>
using namespace std;void print(list<int>& a) {for (auto& x : a)cout << x << ' ';cout << endl;
}void f1() {list<int>a = { 0,1,1,2,3,4,3,5,6,7,7,8,8,8,8,8,9 };print(a);//void unique();a.unique();print(a);
}bool f2_1(int a, int b) {return a == b - 1;
}void f2() {list<int>a = { 0,1,2,3,4,5,6,7,8,9,10 };print(a);//template <class BinaryPredicate>//void unique(BinaryPredicate binary_pred);a.unique(f2_1);print(a);
}void f3() {list<int>a = { 0,1,2,3,4,5,6,7,8,9,10 };list<int>b = { 1,1,4,5 };print(a);//void merge(list & x);a.merge(b);print(a);//void reverse() noexcept;a.reverse();print(a);
}int main() {//f1();//f2();f3();return 0;
}
其他
get_allocate
是分配空间配置器,后期再谈。
relational operator
是比较操作符,对比string
即可。
swap
和类的成员函数swap
基本相同。
这些不过多描述。