本文主要是介绍【STL深入浅出】list探秘与实战,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
📃博客主页: 小镇敲码人
💚代码仓库,欢迎访问
🚀 欢迎关注:👍点赞 👂🏽留言 😍收藏
🌏 任尔江湖满血骨,我自踏雪寻梅香。 万千浮云遮碧月,独傲天下百坚强。 男儿应有龙腾志,盖世一意转洪荒。 莫使此生无痕度,终归人间一捧黄。🍎🍎🍎
❤️ 什么?你问我答案,少年你看,下一个十年又来了 💞 💞 💞
【STL深入浅出】list探秘与实战
- 🌆 list容器简单介绍
- 🌆 list的容器部分方法的使用介绍
- ❇️ list对象的构造以及访问
- ❇️ 头插和头删
- ❇️ 尾插和尾删
- ❇️ 反向迭代器访问元素
- ❇️ list的插入和删除
- ❇️ list的迭代器失效问题
- ❇️ clear函数和size函数
- ❇️ 其它的操作
- 🌆 list容器的模拟实现
- ❇️ list容器的成员变量
- ❇️ list容器中的迭代器模拟实现
- ✴️ 普通的迭代器和const迭代器
- ✴️ 反向迭代器的实现
- ❇️ list容器方法的模拟实现
- ✴️ list类中的类型
- ✴️ list的构造函数
- ✴️ 迭代器的方法函数
- ✴️ 尾插和尾删
- ✴️ 头插和头删
- ✴️ 插入和删除
- ✴️ clear和size以及析构函数
- ✴️ 赋值运算符重载函数
- ✴️ 交换函数
- 🌆 list方法的测试
- ❇️ 测试构造函数
- ❇️ 测试头插和头删
- ❇️ 测试尾插和尾删
- ❇️ 反向迭代器测试
- ❇️ 插入和删除函数测试
- ❇️ clear和size函数测试
- ❇️ swap函数测试
- ❇️ list排序效率测试
🌆 list容器简单介绍
list
容器的底层是带头双向循环链表,和前面两个物理空间连续的容器string
、vector
不同的是,list
容器的物理空间不再连续,在实现迭代器的时候就要更加麻烦了,并且这次我们还将模拟实现反向迭代器。
和其他容器相比list
容器的优势在于,插入、删除很方便,不需要挪动数据。
但是也有劣势,就是list
容器不支持[]
随机访问,只能通过迭代器来一个个的遍历访问。
C++11中还增加了forward_list
容器,forward_list
容器和list
容器的区别在于forward_list
容器的底层是单链表。
🌆 list的容器部分方法的使用介绍
❇️ list对象的构造以及访问
c++98提供了以上四种构造函数,alloc
是内存池,我们现在可以不用管。
构造以及访问演示:
#include<iostream>
#include<list>using namespace std;void Test1()//测试构造函数
{list<int> list1;//空的构造list<int> list2(4, -1);//3个元素每个元素为-1list<int> list3(list2.begin(), list2.end());//迭代器区间构造list<int> list4(list2);//拷贝构造//普通迭代器访问list2list<int>::iterator it = list2.begin();while (it != list2.end()){cout << *it << " ";it++;}cout << endl;//也可以使用范围for来访问for (auto& i : list3){cout << i << " ";}cout << endl;//也可以使用范围for来访问for (auto& i : list4){cout << i << " ";}cout << endl;
}int main()
{Test1();return 0;
}
运行结果:
这里重新理解一下迭代器:
1.具有像指针一样的行为,通过运算符重载来实现,有*
,++
,--
等行为。
2. 是一个类型,可能直接就是原生指针类型的typedef
,也可能是自己自定义的类型。因为原生指针无法满足我们访问的需求,原生指针++的地址是紧接着的下一个地址,但是不是所有的容器物理空间都是连续存储的,迭代器++的意义在于访问元素,如果你++之后没有指向下一个元素的地址,就无法满足我们的要求,我们需要自定义一个类型,以此在自己实现++
操作,经过这个操作之后,我们的迭代器将指向下一个元素的地址。至于细节我们稍后便会模拟实现。
❇️ 头插和头删
和vector
容器不同的是,list
容器还单独实现了头插和头删,可能是这两个操作在链表这个数据结构里用的比较多,使用起来很简单:
#include<iostream>
#include<list>using namespace std;void Test2()//头插和头删
{list<int> list1;for (int i = 0; i < 10; ++i)list1.push_front(i);//依次头插cout << "list1: ";for (auto& i : list1){cout << i << " ";}cout << endl;list1.pop_front();//头删list1.pop_front();//头删cout << "list1: ";for (auto& i : list1){cout << i << " ";}cout << endl;
}int main()
{Test2();return 0;
}
运行结果:
❇️ 尾插和尾删
除了头插和头删,list
的容器也实现尾插和尾删的方法。
代码演示:
#include<iostream>
#include<list>using namespace std;void Test3()//尾插和尾删
{list<int> list1;for (int i = 0; i < 10; ++i)list1.push_back(i);//依次尾插cout << "list1: ";for (auto& i : list1){cout << i << " ";}cout << endl;list1.pop_back();//尾删list1.pop_back();//尾删cout << "list1: ";for (auto& i : list1){cout << i << " ";}cout << endl;
}int main()
{Test3();return 0;
}
运行结果:
❇️ 反向迭代器访问元素
反向迭代器和它的名字一样,是反着来访问元素的,我们来看具体的例子:
#include<iostream>
#include<list>using namespace std;void Test4()//反向迭代器
{list<int> list1;for (int i = 0; i < 10; ++i)list1.push_back(i);//依次尾插元素进入list1容器中//先正向迭代器正常访问list<int>::iterator it1 = list1.begin();//调用正向迭代器的begin函数,得到访问的开始迭代器cout << "list1: ";while (it1 != list1.end()){cout << *it1 << " ";it1++;}cout << endl;//反向迭代器访问list<int>::reverse_iterator it2 = list1.rbegin();//调用反向迭代器的rbegin函数,得到访问的开始迭代器cout << "list1 r: ";while (it2 != list1.rend()){cout << *it2 << " ";it2++;}cout << endl;}
int main()
{Test4();return 0;
}
运行结果:
反向迭代器和普通迭代器的类型是不同的,所以它们的起始迭代器的类型也不同,注意不要访问错了,否则类型不同会报错的。
❇️ list的插入和删除
接口和前面的顺序容器基本没有区别,我们只演示最简单的接口:
演示代码:
#include<iostream>
#include<list>using namespace std;void Test5()//insert and erase display
{list<int> list1;for (int i = 0; i < 10; ++i)list1.push_back(i);//依次尾插元素进入list1容器中my_list::print_list(list1);//打印list1.insert(list1.begin(),1000);//头插一个1000my_list::print_list(list1);//打印list1.insert(list1.end(), 1000);//尾插一个1000my_list::print_list(list1);//打印auto it = std::find(list1.begin(), list1.end(), 8);//在list1对象中找一个值为8的元素,并返回它的迭代器if (it != list1.end())list1.erase(it);//删除元素8my_list::print_list(list1);//打印it = std::find(list1.begin(), list1.end(), 10);//在list1对象中找一个值为10的元素,并返回它的迭代器if(it != list1.end())list1.erase(it);//删除元素10my_list::print_list(list1);//打印//头插一个999,再尾插一个999,看find函数会返回谁的迭代器list1.push_front(999);list1.push_back(999);my_list::print_list(list1);//打印it = std::find(list1.begin(), list1.end(), 999);//在list1对象中找一个值为10的元素,并返回它的迭代器if (it != list1.end())list1.erase(it);//删除元素10my_list::print_list(list1);//打印}
int main()
{Test5();return 0;
}
运行结果:
因为打印链表这个代码会重复很多,所以我们在我们自己的命名空间里面实现了一个打印函数模板,如果想打印链表,直接调用这个函数就可以了:
另外我们还用到了find
函数来找某个元素的位置,如果这个元素不存在,find
函数会返回last
迭代器,也就是我们传的区间的末尾,如果这个元素存在,就返回第一次找到的迭代器。
❇️ list的迭代器失效问题
和string
、vector
容器一样list
容器也有迭代器失效的问题,我们具体来看它们是否有所不同。
list
的迭代器失效在插入的时候不会出现,因为插入时,迭代器it
的节点指针不受影响,插入只会新增节点,不会对原先节点的地址产生任何影响。
所以迭代器失效只会出现在删除的时候,因为删除后,这个迭代器内部节点指针被释放了,一般是为空,这个时候你对它使用任何操作都是非法的。
下面是具体迭代器失效的例子:
#include<iostream>
#include<list>using namespace std;void Test6()//删除导致list迭代器失效
{list<int> list1;for (int i = 0; i < 10; ++i)list1.push_back(i);my_list::print_list(list1); auto it = list1.begin();while (it != list1.end()){list1.erase(it);it++;//错误,此时it迭代器内部的节点已经被释放,相当于it已经失效}my_list::print_list(list1);
}
int main()
{Test6();return 0;
}
运行结果:
改进办法就是在这个迭代器失效之前,就更新即可:
#include<iostream>
#include<list>using namespace std;void Test6()//删除导致list迭代器失效
{list<int> list1;for (int i = 0; i < 10; ++i)list1.push_back(i);my_list::print_list(list1); auto it = list1.begin();while (it != list1.end()){list1.erase(it++);//后置++,it++的返回值是it,但是自己的值已经改变了,erase传进去的是operator++的返回值,}my_list::print_list(list1);
}
int main()
{Test6();return 0;
}
运行结果:
程序正常运行,迭代器失效的问题已经解决。
❇️ clear函数和size函数
clear
函数会把除头节点外的所有节点都pop
掉
代码演示:
#include<iostream>
#include<list>using namespace std;void Test7()//clear函数,size
{list<int> list1;for (int i = 0; i < 10; ++i)list1.push_back(i);cout << list1.size() << endl;list1.clear();cout << list1.size() << endl;
}
int main()
{Test7();return 0;
}
运行结果:
❇️ 其它的操作
链表还有一些其它的操作,像排序链表(sort)、去重(unique),逆序(把链表中元素的存储位置反过来)不是较为常用,我们不再介绍,如果有兴趣,自行查阅文档
🌆 list容器的模拟实现
❇️ list容器的成员变量
通过看源码,我们发现,list
容器中就只有一个节点指针类型的变量。
link_type
是一个指针类型:
下面是_list_node
的定义:
它是一个模板类:
这里节点类型里面next
和pre
使用的是void *
万能指针我们不用管,我们使用正常类型就行。
我们自己实现的list_node
:
template<class T>struct List_node{struct List_node<T>* pre_;struct List_node<T>* next_;T val_;List_node(const T& x = T()):val_(x),pre_(nullptr),next_(nullptr)//构造函数{}};
❇️ list容器中的迭代器模拟实现
迭代器我们实现了三个,但准确来讲只有两个。
c++中struct
可以不要。名称<模板参数>就代表类型。
✴️ 普通的迭代器和const迭代器
前面已经阐述了迭代器的基本概念,下面我们来看下一下,普通迭代器的模拟实现,首先我们要先明确这个迭代器需要什么成员变量,要一个节点指针就够我们完成所有操作了,其次我们的迭代器还应该设计为模板类型的,因为节点的指针是一个模板类型。
然后我们需要考虑,我们的普通迭代器和const
迭代器的区别是什么,不就在解引用和->
(对于list中保存的是结构体成员来说)吗,因为如果解引用返回的是一个const
类型的值,它就不可修改呀,解引用返回的是值的引用,而operator ->
重载函数返回的是那个值的地址。我们给这两个返回值弄成模板的形式不就行了吗?const
迭代器就传const 引用
,这样避免了代码的冗余。
迭代器不需要析构函数,因为节点的空间不是我们开的,我们不需要负责释放。
代码实现:
template<class T, class Ref, class Ptr>struct list_iterator{typedef List_node<T> Link_type;//给模板节点类型起别名typedef list_iterator<T, Ref, Ptr> self;//给这个比较长的类型起别名,等会要做返回值使用typedef list_iterator<T, T&, T*> iterator;typedef Ref Ref;//把这个模板类型变成我们类里面的类型,之后反向迭代器要使用typedef Ptr Ptr;//把这个模板类型变成我们类里面的类型,之后反向迭代器要使用list_iterator() {};//空的构造函数list_iterator(Link_type* x) :node(x)//用节点指针构造的构造函数{}list_iterator(const iterator& x) : node(x.node)//拷贝构造函数{}Ref operator*() const//返回这个值的引用{return node->val_;}Ptr operator->() const//返回这个节点指针,让迭代器可以使用箭头访问这个节点的值(主要用于list保存的结构体类型的情况){return &node->val_;}self operator++(int)//后置++{self tmp(node);node = node->next_;return tmp;}self& operator++()//前置++{node = node->next_;return *this;}self operator--()//前置--{node = node->pre_;return *this;}self operator--(int)//后置--{self tmp(node);//先构造现在的迭代器node = node->pre_;//再更新当前迭代器的nodereturn tmp;//返回的是没--前的迭代器}bool operator!=(const self& x) const //判断迭代器是否相等,看他们保存的节点指针是否一样即可{return node != x.node;}bool operator==(const self& x) const{return node == x.node;}Link_type* node;};
迭代器这个类型的目的就在于模拟指针的行为,所以运算符重载是它的核心语法。
注释已经说的很清楚了,我们对一些核心部分做一下阐述:
-
前置++(-- )和后置++(- -),我们之前已经阐述过,这里代码的实现部分,相信不难理解,++需要迭代器保存这个节点的下一个节点的指针,直接把
next_
赋值给它即可,但是要注意,后置++的返回值还是原先那个迭代器,没有发生改变。 -
迭代器比较部分的实现逻辑是比较这些迭代器保存的节点指针是否相等,不能直接比较迭代器是否一样,这样会导致无限递归下去,也不能比较迭代器的地址,因为迭代器的地址是不同的,两个不同的迭代器变量,它们的内存地址可能不同,但是保存的节点指针可能相同。
-
迭代器构造部分,直接把节点指针赋值给当前迭代器的节点指针变量即可。
-
Ref
是引用类型的模板参数,Ptr
是指针类型的模板参数,Ref
做operator*
函数的返回值(节点指针值的引用),Ptr
做operator->
函数的返回值(节点返回值的地址)。
const
迭代器和普通迭代器我们只需要在模板传参上传不同的类型,就可以利用这一个迭代器代码,实现两个出来。
✴️ 反向迭代器的实现
我们可以思考一下反向迭代器和正向普通的迭代器的区别和共同点:
- 共同点:都是迭代器,利用运算符重载函数模仿指针的行为,以完成容器的遍历。
- 区别:反向迭代器是反着来遍历的,它的
++
和正向迭代器的--
一样,它的--
和正向迭代器的++
一样。
所以其实区别很小,我们可以利用模板传正向迭代器的类型给反向迭代器,然后创建一个正向迭代器的类型作为它的成员变量,通过这个正向迭代器的操作来实现反向迭代器的一系列操作,这样我们的反向迭代器就是一个类模板,它不仅适用于list
的反向迭代器实现,也适用于其它容器的反向迭代器的实现。
typename
的作用是告诉编译器我们起别名的是一个类型而不是静态变量,因为在类里面,静态变量也是这样来访问的。
namespace my_reverse_iterator
{template<class Iterator>struct reverse_iterator{typedef typename Iterator::Ref Ref;typedef typename Iterator::Ptr Ptr;typedef typename reverse_iterator<Iterator> Self;reverse_iterator() {};//空的构造reverse_iterator(Iterator it) : it1(it)//用Iterator构造{}Ref operator*() const//解引用操作,去调用it1的解引用{return *it1;}Ptr operator->() const//->操作,去调用it1的operator->操作{return &(*it1);}Self& operator--()//前置-- 前置--,去调用it1的前置或者后置++均可{++it1;return *this;}Self operator--(int)//后置-- 后置--,去调用it1的前置或者后置++均可,要提前保存没有++的it1,然后返回{Self tmp(it1);++it1;return tmp;}Self& operator++()//前置++ 和前置--类似{--it1;return *this;}Self& operator++(int)//后置++ 和后置--类似{Self tmp(it1);--it1;return tmp;}bool operator!=(const Self& x)//调用it1 的!= 操作{return it1 != x.it1;}bool operator==(const Self& x)//调用it1 的 == 操作{return it1 == x.it1;}private:Iterator it1;//创建Iterator类型的it1对象,复用这个类型的操作};
}
我们上面的代码相当于是一个复用,像一个模具一样,适用于所有的容器的反向迭代器,复用的是这个容器的迭代器操作,因为每个容器的迭代器的操作是不同的,所以我们要写成类模板的形式。
❇️ list容器方法的模拟实现
完整的代码:
template<class T>class list{public:typedef struct List_node<T> list_type;//给类模板起别名,因为后面我们用的比较多,这个类模板的类型名字太长了typedef struct list_iterator<T,T&,T*> iterator;//普通给迭代器类型起别名typedef struct list_iterator<T,const T&,const T*> const_iterator;//给const迭代器类型起别名typedef struct my_reverse_iterator::reverse_iterator<iterator> reverse_iterator;//给反向迭代器类型起别名void Init_empty()//初始化list的容器,因为list的底层是带头的,所以就算是空构造也要给这个对象创建一个头节点{Head_ = new list_type;//开空间//初始化Head_->val_ = T();Head_->pre_ = Head_;Head_->next_ = Head_;}list()//空的构造{Init_empty();}list(const list& x)//拷贝构造{Init_empty();for (auto i: x)//复用尾插和迭代器{push_back(i);}}list(size_t n, const T& val = T())//普通构造{Init_empty();//初始化头节点for (int i = 0; i < n; ++i)//尾插数据push_back(val);}list(int n, const T& val = T())//普通构造{Init_empty();for (int i = 0; i < n; ++i)push_back(val);}template <class InputIterator>list(InputIterator first, InputIterator last)//迭代器区间构造{Init_empty();//初始化头节点while (first != last)//迭代器遍历并初始化{push_back(*first);first++;}}list<T>& operator=(const list<T>& x)//赋值运算符重载{list<T> tmp(x);//复用拷贝构造std::swap(tmp.Head_,Head_);//交换它们的Head_节点return *this;}iterator begin()//普通迭代器的开始访问函数,从头节点的下一个节点开始访问{return Head_->next_;}iterator end()//普通迭代器的结束访问函数,访问到头节点就不能访问了,因为是双向循环链表{return Head_;}const_iterator begin() const{return Head_->next_;}const_iterator end() const{return Head_;}reverse_iterator rbegin()//反向迭代器的开始访问函数,从普通迭代器的end()前面的那个节点开始访问。{return --end();}reverse_iterator rend()//和end()一样{return end();}void push_back(const T& val)//尾插{list_type* newnode = new list_type(val);//创建新的节点list_type* tail = Head_->pre_;//保存尾节点newnode->pre_ = tail;//先把新节点的前驱节点指向尾节点newnode->next_ = Head_;//再把新节点的后继节点指向头节点Head_->pre_ = newnode;//更新头节点的前驱节点tail->next_ = newnode;//更新尾节点的后继节点}void pop_back()//尾删{assert(Head_->next_ != Head_);//检查list中是否还有数据list_type* tail = Head_->pre_;//先保存尾节点tail->pre_->next_ = Head_;//把尾节点的前驱节点的后继节点指向head_Head_->pre_ = tail->pre_;//再把头节点的前驱节点指向尾节点的前驱节点delete tail;//释放当前尾节点的空间}void push_front(const T& val)//头插{list_type* newnode = new list_type(val);//创建新的节点newnode->next_ = Head_->next_;//新节点的next先指向头节点的nextnewnode->pre_ = Head_;//新节点的前驱节点指向Head_Head_->next_ = newnode;//头节点的后继节点指向新节点}void pop_front()//头删{assert(Head_->next_ != Head_);//如果只剩下头节点,就不能继续删除了list_type* node = Head_->next_;//先保存要删除的节点,防止重新链接之后找不到,造成内存泄漏Head_->next_ = node->next_;//先把头节点的后继节点指向待删除节点的后继节点node->next_ ->pre_ = Head_;//再把待删除节点的后继节点的前驱节点指向头节点delete node;//释放node节点}iterator insert(iterator position, const T& val)//在position迭代器指向的地址之前,插入元素val{list_type* newnode = new list_type(val);//创建新的节点list_type* pre = position.node->pre_;//链接起来pre->next_ = newnode;newnode->pre_ = pre;newnode->next_ = position.node;position.node->pre_ = newnode;return --position;//返回第一个新插入元素的迭代器}iterator erase(iterator position){assert(position.node != Head_);//检查是否还有节点list_type* pre = position.node->pre_;//保存前驱节点//链接pre->next_ = position.node->next_;position.node->next_->pre_ = pre;//删除delete position.node;//返回被删除的最后一个位置的下一个元素的迭代器return iterator(pre->next_);}size_t size() const{list_type* cur = Head_->next_;//保存头节点的后继节点size_t size = 0;//保存节点的个数while (cur != Head_){cur = cur->next_;//继续遍历size++;}return size;}void clear() {list_type* cur = Head_->next_;//保存头节点的后继节点while (cur != Head_)//如果cur不等于Head_节点就继续{list_type* next = cur->next_;//先保存后继节点delete cur;//释放当前节点cur = next;}//不要忘记更新头节点的链接,不然就会指向野指针了Head_->next_ = Head_;Head_->pre_ = Head_;}void swap(list<T>& x){std::swap(Head_, x.Head_);}~list(){clear();//调用析构函数delete Head_;//释放头节点Head_ = nullptr;}private:list_type* Head_;};
✴️ list类中的类型
有一个节点类型,其余是迭代器类型,我们没有实现const的反向迭代器的rbegin
、rend()
函数,其实加个const
即可(也不麻烦),所以迭代器类型只有3个。
✴️ list的构造函数
void Init_empty()//初始化list的容器,因为list的底层是带头的,所以就算是空构造也要给这个对象创建一个头节点{Head_ = new list_type;//开空间//初始化Head_->val_ = T();Head_->pre_ = Head_;Head_->next_ = Head_;}list()//空的构造{Init_empty();}list(const list& x)//拷贝构造{Init_empty();for (auto i: x)//复用尾插和迭代器{push_back(i);}}list(size_t n, const T& val = T())//普通构造{Init_empty();//初始化头节点for (int i = 0; i < n; ++i)//尾插数据push_back(val);}list(int n, const T& val = T())//普通构造{Init_empty();for (int i = 0; i < n; ++i)push_back(val);}template <class InputIterator>list(InputIterator first, InputIterator last)//迭代器区间构造{Init_empty();//初始化头节点while (first != last)//迭代器遍历并初始化{push_back(*first);first++;}}
构造函数部分较为简单,主要复用了push_back
的操作,和之前的string
和vector
容器没有太大的区别。
✴️ 迭代器的方法函数
iterator begin()//普通迭代器的开始访问函数,从头节点的下一个节点开始访问{return Head_->next_;}iterator end()//普通迭代器的结束访问函数,访问到头节点就不能访问了,因为是双向循环链表{return Head_;}const_iterator begin() const{return Head_->next_;}const_iterator end() const{return Head_;}reverse_iterator rbegin()//反向迭代器的开始访问函数,从普通迭代器的end()前面的那个节点开始访问。{return --end();}reverse_iterator rend()//和end()一样{return end();}
这里迭代器的类型和节点指针的类型不同,为什么能通过呢?因为编译器会去调用对应的迭代器的构造函数,相当于隐式的构造了一个临时变量出来。我们在测试迭代器的时候可以顺便来看一下是否是这样。
✴️ 尾插和尾删
void push_back(const T& val)//尾插{list_type* newnode = new list_type(val);//创建新的节点list_type* tail = Head_->pre_;//保存尾节点newnode->pre_ = tail;//先把新节点的前驱节点指向尾节点newnode->next_ = Head_;//再把新节点的后继节点指向头节点Head_->pre_ = newnode;//更新头节点的前驱节点tail->next_ = newnode;//更新尾节点的后继节点}void pop_back()//尾删{assert(Head_->next_ != Head_);//检查list中是否还有数据list_type* tail = Head_->pre_;//先保存尾节点tail->pre_->next_ = Head_;//把尾节点的前驱节点的后继节点指向head_Head_->pre_ = tail->pre_;//再把头节点的前驱节点指向尾节点的前驱节点delete tail;//释放当前尾节点的空间}
带头双向循环链表的尾插和尾删操作和单链表的尾插、尾删操作类似,需要注意的是不要让本来要删除和指向的节点找不到了。需要删除的节点要提前保存。如果不能理解可以画出逻辑图,自己对照着看,熟练之后就不需要画了。
✴️ 头插和头删
类似的逻辑。不再重复赘述。
void push_front(const T& val)//头插{list_type* newnode = new list_type(val);//创建新的节点newnode->next_ = Head_->next_;//新节点的next先指向头节点的nextnewnode->pre_ = Head_;//新节点的前驱节点指向Head_Head_->next_ = newnode;//头节点的后继节点指向新节点}void pop_front()//头删{assert(Head_->next_ != Head_);//如果只剩下头节点,就不能继续删除了list_type* node = Head_->next_;//先保存要删除的节点,防止重新链接之后找不到,造成内存泄漏Head_->next_ = node->next_;//先把头节点的后继节点指向待删除节点的后继节点node->next_ ->pre_ = Head_;//再把待删除节点的后继节点的前驱节点指向头节点delete node;//释放node节点}
✴️ 插入和删除
我们还需要一个支持删除任意迭代器的节点的erase
函数,和插入元素到任意迭代器之前的insert
函数:
实际编程中,我们可以先写这个插入和删除函数,然后上述的尾插、尾删、头插和头删就可以复用了。
iterator insert(iterator position, const T& val)//在position迭代器指向的地址之前,插入元素val{list_type* newnode = new list_type(val);//创建新的节点list_type* pre = position.node->pre_;//链接起来pre->next_ = newnode;newnode->pre_ = pre;newnode->next_ = position.node;position.node->pre_ = newnode;return --position;//返回第一个新插入元素的迭代器}iterator erase(iterator position){assert(position.node != Head_);//检查是否还有节点list_type* pre = position.node->pre_;//保存前驱节点//链接pre->next_ = position.node->next_;position.node->next_->pre_ = pre;//删除delete position.node;//返回被删除的最后一个位置的下一个元素的迭代器return iterator(pre->next_);}
✴️ clear和size以及析构函数
clear
函数会释放除头节点外的所有节点,size则是统计除头节点外的节点的数量,析构函数我们会复用clear
函数:
size_t size() const{list_type* cur = Head_->next_;//保存头节点的后继节点size_t size = 0;//保存节点的个数while (cur != Head_){cur = cur->next_;//继续遍历size++;}return size;}void clear() {list_type* cur = Head_->next_;//保存头节点的后继节点while (cur != Head_)//如果cur不等于Head_节点就继续{list_type* next = cur->next_;//先保存后继节点delete cur;//释放当前节点cur = next;}//不要忘记更新头节点的链接,不然就会指向野指针了Head_->next_ = Head_;Head_->pre_ = Head_;}~list(){clear();//调用析构函数delete Head_;//释放头节点Head_ = nullptr;}
✴️ 赋值运算符重载函数
复用拷贝构造函数。
list& operator= (const list& x)//赋值运算符重载{list<T> tmp(x);//复用拷贝构造std::swap(x.Head_, Head_);//交换它们的Head_节点return *this;}
✴️ 交换函数
void swap(list<T>& x)//在类里面实现{std::swap(Head_, x.Head_);}template<class T>void swap(list<T>& x, list<T>& y)//在类外面实现{x.swap(y);}
🌆 list方法的测试
❇️ 测试构造函数
#include"list.h"void Test1()//测试构造函数
{my_list::list<int> list1;//空的构造my_list::list<int> list2(4, -1);//3个元素每个元素为-1my_list::list<int> list3(list2.begin(), list2.end());//迭代器区间构造my_list::list<int> list4(list2);//拷贝构造my_list::list<int> list5(5, -1);list5 = list2;//赋值运算符重载//普通迭代器访问list2my_list::list<int>::iterator it = list2.begin();while (it != list2.end()){cout << *it << " ";it++;}cout << endl;//也可以使用范围for来访问for (auto& i : list3){cout << i << " ";}cout << endl;//也可以使用范围for来访问for (auto& i : list4){cout << i << " ";}cout << endl;my_list::print_list(list5);
}int main()
{Test1();return 0;
}
运行结果:
❇️ 测试头插和头删
#include"list.h"void Test2()//头插和头删
{my_list::list<int> list1;for (int i = 0; i < 10; ++i)list1.push_front(i);//依次头插cout << "list1: ";for (auto& i : list1){cout << i << " ";}cout << endl;list1.pop_front();//头删list1.pop_front();//头删cout << "list1: ";for (auto& i : list1){cout << i << " ";}cout << endl;
}int main()
{Test2();return 0;
}
运行结果:
❇️ 测试尾插和尾删
#include"list.h"void Test3()//尾插和尾删
{my_list::list<int> list1;for (int i = 0; i < 10; ++i)list1.push_back(i);//依次尾插cout << "list1: ";for (auto& i : list1){cout << i << " ";}cout << endl;list1.pop_back();//尾删list1.pop_back();//尾删cout << "list1: ";for (auto& i : list1){cout << i << " ";}cout << endl;
}int main()
{Test3();return 0;
}
运行结果:
❇️ 反向迭代器测试
#include"list.h"void Test4()//反向迭代器
{my_list::list<int> list1;for (int i = 0; i < 10; ++i)list1.push_back(i);//依次尾插元素进入list1容器中//先正向迭代器正常访问my_list::list<int>::iterator it1 = list1.begin();//调用正向迭代器的begin函数,得到访问的开始迭代器cout << "list1: ";while (it1 != list1.end()){cout << *it1 << " ";it1++;}cout << endl;//反向迭代器访问my_list::list<int>::reverse_iterator it2 = list1.rbegin();//调用反向迭代器的rbegin函数,得到访问的开始迭代器cout << "list1 r: ";while (it2 != list1.rend()){cout << *it2 << " ";it2++;}cout << endl;
}int main()
{Test4();return 0;
}
运行结果:
我们可以通过调试看一下,begin()
和rbegin
返回节点指针类型然后去调用构造函数的过程:
begin()
:
rbegin()
:
会先去构造Iterator
类型,再使用这个Iterator
去构造reverse_iterator
类型:
❇️ 插入和删除函数测试
#include"list.h"void Test5()//insert and erase display
{my_list::list<int> list1;for (int i = 0; i < 10; ++i)list1.push_back(i);//依次尾插元素进入list1容器中my_list::print_list(list1);//打印list1.insert(list1.begin(),1000);//头插一个1000my_list::print_list(list1);//打印list1.insert(list1.end(), 1000);//尾插一个1000my_list::print_list(list1);//打印list1.erase(list1.begin());//头删my_list::print_list(list1);//打印list1.erase(--list1.end());//尾删my_list::print_list(list1);//打印
}int main()
{Test5();return 0;
}
运行结果:
❇️ clear和size函数测试
#include"list.h"void Test7()//clear函数,size
{my_list::list<int> list1;for (int i = 0; i < 10; ++i)list1.push_back(i);cout << list1.size() << endl;list1.clear();cout << list1.size() << endl;
}int main()
{Test7();return 0;
}
运行结果:
❇️ swap函数测试
#include"list.h"void Test8()//swap函数
{list<int> list1;for (int i = 0; i < 10; ++i)list1.push_back(i);list<int> list2;for (int i = 15; i < 25; ++i)list2.push_back(i);my_list::print_list(list1);my_list::print_list(list2);std::swap(list1, list2);my_list::print_list(list1);my_list::print_list(list2);
}int main()
{Test7();return 0;
}
运行结果:
❇️ list排序效率测试
list
容器不支持下标访问,但是它也有排序函数,我们不需要关心它的底层是如何来实现的,就只是简单来比较一下它和vector
容器排序的效率。
我们准备了两个函数:
- 第一个函数单纯比较
list
和vector
容器的排序的效率。 - 第二个函数比较
list
容器排序和把list
容器中的数据拷贝到vector
,排完序后再拷贝回list
容器的效率。
#include"list.h"void Test9()
{srand(time(0));const int N = 1000000;list<int> lt1;vector<int> v;for (int i = 0; i < N; ++i){auto e = rand();lt1.push_back(e);v.push_back(e);}int begin1 = clock();//vector中排序// sort(v.begin(), v.end());int end1 = clock();int begin2 = clock();//list排序lt1.sort();int end2 = clock();printf("vector sort:%d\n", end1 - begin1);printf("list sort:%d\n", end2 - begin2);
}void Test10()
{srand(time(0));const int N = 1000000;list<int> lt1;list<int> lt2;for (int i = 0; i < N; ++i){auto e = rand();lt1.push_back(e);lt2.push_back(e);}int begin1 = clock();//先把数据拷贝到vector,排完序后再拷贝回list的时间// vectorvector<int> v(lt2.begin(), lt2.end());// sort(v.begin(), v.end());// lt2lt2.assign(v.begin(), v.end());int end1 = clock();int begin2 = clock();//单纯的list的排序lt1.sort();int end2 = clock();printf("list copy vector sort copy list sort:%d\n", end1 - begin1);printf("list sort:%d\n", end2 - begin2);
}int main()
{Test9();Test10();return 0;
}
运行结果(100w个数据有大量重复数据,随机数只有3w多个,Release下测试对递归有优化):
可以看到即使是拷贝了两次,在vector
排序都比直接在list
中快。
少量重复数据测试结果(给随机数+i
可以减少重复):
少量重复数据都比之前快了。
但是list
容器排序的效率依旧比vector
慢了不少,所以这个实际中这个list
的排序基本没什么用,都尽量在vector
中排序。
这篇关于【STL深入浅出】list探秘与实战的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!