【万字总结】C++——list的基本使用和模拟实现(建议收藏)
创始人
2024-01-19 00:53:38

目录

一、list基本介绍

二、list的使用

1、list的初始化方式

2、list的增删查改

push_front和pop_front与push_back和pop_back

insert

erase

3、list迭代器的使用

正向迭代器

反向迭代器

4、list获取头尾元素

5、list容量操作

6、list的其他操作

sort

splice

remove

unique

merge

reverse

assign

swap

7、结点的构造函数

三、模拟迭代器类

迭代器类的模板参数

1、构造函数

2、运算符重载

++

==

!=

*

->

四、list的模拟实现

1、构造函数

2、拷贝构造函数

3、赋值运算符重载函数

4、析构函数

5、迭代器相关函数

begin和end

insert

erase

push_back和pop_back与push_front和pop_front

clear

五、反向迭代器的模拟实现

六、vector与list优劣


一、list基本介绍

1. list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。

2. list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向 其前一个元素和后一个元素。

3. list与forward_list非常相似:最主要的不同在于forward_list是单链表,只能朝前迭代,已让其更简单高 效。

4. 与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率 更好。

5. 与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list 的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间 开销;list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大list来说这 可能是一个重要的因素)。

二、list的使用

1、list的初始化方式

    list lt1;//构造空容器list lt2(20, 2);//构造有20个2的int型容器list lt3(lt2);//拷贝构造string str("hello world");//使用迭代器list lt4(str.begin(), str.end());//使用区间给容器赋值int myints[] = {16,2,77,29};std::list fifth (myints, myints + sizeof(myints) / sizeof(int) );//使用数组进行初始化

2、list的增删查改

push_front和pop_front与push_back和pop_back

void test_list2()
{list lt;//尾插lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);//1  2  3  4//尾删lt.pop_back();//1  2  3  //头插lt.push_front(0);//0  1  2  3//头删lt.pop_front();//1  2  3 //打印for (auto e : lt){cout << e << " ";}cout << endl;//1  2  3 
}

insert

查阅文档我们可以发现list有三种插入方式:

1、在指定迭代器位置插入一个数。

2、在指定迭代器位置插入n个值为val的数。

3、在指定迭代器位置插入一段迭代器区间。 

	list lt;//尾插lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);// 1  2  3  4//方式一//先确定2的位置list::iterator pos = find(lt.begin(), lt.end(), 2);//在2的前面插入一个0lt.insert(pos, 0);// 1  0  2  3  4 //方式二//在2的前面插入两个0lt.insert(pos, 2, 0);//1  0  0  0  2  3  4//方式三vector v(3, 0);lt.insert(pos, v.begin(), v.end());//在2的前面插入三个0//1  0  0  0  0  0  0  2  3  4

erase

 文档中记载有两种删除方式:

  1. 删除指定迭代器位置的元素。
  2. 删除指定迭代器区间(左闭右开)的所有元素。
    list lt;//尾插lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);// 1  2  3  4// //找到2的位置用迭代器指向它获得list::iterator pos1 = find(lt.begin(), lt.end(),2);lt.erase(pos1);// 1  3  4  list::iterator pos2 = find(lt.begin(), lt.end(), 3);lt.erase(pos2, lt.end());// 1

3、list迭代器的使用

正向迭代器

list lt(3, 7);list::iterator it = lt.begin();while (it != lt.end()){cout << *it << " ";it++;}//输出3个7

反向迭代器

    list lt;//尾插lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);list::reverse_iterator rit = lt.rbegin();while (rit != lt.rend()){cout << *rit << " ";rit++;}//4  3  2  1

4、list获取头尾元素

    list lt;//尾插lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);//输出最后一个元素的值cout << lt.back() << endl;//输出第一个元素的值cout << lt.front() << endl;

5、list容量操作

list lt;//尾插lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);//输出数据个数cout << lt.size() << endl;//当所给值大于当前的size时,将size扩大到该值,扩大的数据为第二个所给值//若未给出,则默认为容器所存储类型的默认构造函数所构造出来的值。//当所给值小于当前的size时,将size缩小到该值。//容量增加到5lt.resize(5);//判断容器内是否为空if (lt.empty()){cout << "容器为空" << endl;}else{cout << "容器不为空" << endl;}//清空容器的数据,不改变容量大小lt.clear();cout << lt.size() << endl;

6、list的其他操作

sort

将容器中的数据默认按照升序排列。

list lt;//尾插lt.push_back(3);lt.push_back(4);lt.push_back(1);lt.push_back(2);lt.sort();for (auto e : lt){cout << e << " ";}cout << endl;//输出 1  2  3  4

splice

作用是拼接两个list容器。

由文档可知有三种方式: 

1、将一个容器拼接到另一个容器迭代器指向的位置。

2、将容器中的某一个数据拼接到另一个容器迭代器指向的位置。

3、将要容器中迭代器指向的区间的数据拼接到另一个容器迭代器指向的位置。

//方式一list lt1;//尾插lt1.push_back(1);lt1.push_back(2);lt1.push_back(3);lt1.push_back(4);list lt2(3, 5);list::iterator pos1 = find(lt1.begin(), lt1.end(), 2);lt1.splice(pos1, lt2);for (auto e : lt1){cout << e << " ";}cout << endl;//1 5 5 5 2 3 4//方式二list lt1;//尾插lt1.push_back(1);lt1.push_back(2);lt1.push_back(3);lt1.push_back(4);list lt2(3, 5);list::iterator pos1 = find(lt1.begin(), lt1.end(), 2);lt1.splice(pos1, lt2, lt2.begin());//1  5  2  3  4//方式三list lt1;//尾插lt1.push_back(1);lt1.push_back(2);lt1.push_back(3);lt1.push_back(4);list lt2(3, 5);list::iterator pos1 = find(lt1.begin(), lt1.end(), 2);list::iterator it2 = lt2.begin();lt1.splice(pos1, lt2, lt2.begin(), lt2.end());//1 5 5 5 2 3 4

remove

删除容器中的某个值

list lt1;//尾插lt1.push_back(1);lt1.push_back(2);lt1.push_back(3);lt1.push_back(3);lt1.push_back(3);lt1.push_back(3);lt1.push_back(4);lt1.remove(3);for (auto e : lt1){cout << e << " ";}//1  2  4

unique

去除容器容器中连续重复的元素。

list lt;lt.push_back(1);lt.push_back(1);lt.push_back(1);lt.push_back(2);lt.push_back(2);lt.push_back(2);lt.push_back(3);lt.push_back(4);lt.push_back(4);lt.push_back(4);lt.push_back(4);lt.push_back(4);lt.unique();for (auto e : lt){cout << e << " ";}//1  2  3  4

merge

类似归并排序,将一个有序容器归并到另一个有序容器中,使其还是有序的。

list lt1;lt1.push_back(1);lt1.push_back(3);lt1.push_back(5);lt1.push_back(7);list lt2;lt2.push_back(2);lt2.push_back(4);lt2.push_back(6);lt2.push_back(8);lt1.merge(lt2);for (auto e : lt1){cout << e << " ";}//1  2  3  4  5  6  7  8

reverse

将容器中的元素进行逆置。

list lt1;lt1.push_back(1);lt1.push_back(3);lt1.push_back(5);lt1.push_back(7);list lt2;lt2.push_back(2);lt2.push_back(4);lt2.push_back(6);lt2.push_back(8);lt1.merge(lt2);//1  2  3  4  5  6  7  8lt1.reverse();for (auto e : lt1){cout << e << " ";}//8  7  6  5  4  3  2  1

assign

将新内容分配给容器,并且会覆盖原容器中的值。

 

由文档可知有两种方式:

1、将n个val值分配给容器

2、将迭代器区间中的内容分配给容器

//方式一list lt1;lt1.push_back(1);lt1.push_back(3);lt1.push_back(5);lt1.push_back(7);lt1.assign(3, 3);for (auto e : lt1){cout << e << " ";}// 3  3  3//方式二list lt1;lt1.push_back(1);lt1.push_back(3);lt1.push_back(5);lt1.push_back(7);list lt2;lt2.push_back(2);lt2.push_back(4);lt2.push_back(6);lt2.push_back(8);lt1.assign(lt2.begin(), lt2.end());for (auto e : lt1){cout << e << " ";}//2  4  6  8 

swap

交换两个容器的内容

list lt1;lt1.push_back(1);lt1.push_back(3);lt1.push_back(5);lt1.push_back(7);list lt2;lt2.push_back(2);lt2.push_back(4);lt2.push_back(6);lt2.push_back(8);lt1.swap(lt2);for (auto e : lt1){cout << e << " ";}//2  4  6  8 

7、结点的构造函数

构造一个结点,并且给结点的数据域he指针域初始化。

template//类模板struct ListNode{ListNode* _next;ListNode* _prev;T _data;
ListNode(const T& data = T()):_next(nullptr), _prev(nullptr), _data(data){}

三、模拟迭代器类

为什么在vector和string中不用实现一个迭代器类呢?

因为vector和string中的数据都是存储在一块连续的地址空间,我们直接使迭代器像原生指针那样解引用、自增、自减。就可以得到我们所要得到的效果。

可是list却不是如此,我们知道list的本质是一个双向带头循环链表。地址空间并不连续,我们就不能单纯的像原生指针那样进行操作了。

而迭代器的意义就是,让使用者可以不必关心容器的底层实现,可以用简单统一的方式对容器内的数据进行访问。

迭代器类的模板参数

//迭代器template//类模板

由以上代码我们可以看出来,迭代器类的模板参数列表有三个模板参数。

这也与list模拟实现中的两个迭代器相对应。

typedef _list_iterator iterator;
typedef _list_iterator const_iterator;

其具体作用其实就是为了让我们使用普通迭代器的时候,编译器实例化出一个普通迭代器对象。当我们使用const迭代器的时候就会实例化一个const迭代器对象。

1、构造函数

本质就是根据结点指针构造一个迭代器对象。

//构造函数
__list_iterator(Node* x):_node(x)
{}

2、运算符重载

 注意:

self是当前迭代器对象的类型

		typedef __list_iterator self;

++

本质是使结点指针指向后一个结点,并且返回下一个结点指针。

//++itself& operator++(){_node = _node->_next;return *this;}//it++self& operator++(int){self tmp(*this);//保存++之前的值以便于返回_node = _node->_next;return tmp;}

--

本质是使结点值真指向前一个结点,并且返回上一个结点指针。

//--itself& operator--(){_node = _node->_prev;return *this;}//it--self& operator--(int){self tmp(*this);//保存--之前的值以便于返回_node = _node->_prev;return tmp;}

==

本质就是比较此时两个迭代器当中的结点指针是否指向同一个位置

bool operator==(const self& it) const{return _node == it._node;}

!=

bool operator!=(const self& it) const{return _node != it._node;}

*

由于list是一个链表结构,*解引用实质就是返回此结点指针所指向的数据。

//需要T&传T& 需要const T& 就传 const T&Ref operator*()//由于用的引用,所以可读可写{return _node->_data;}

->

如果list容器中结点中存放的不是内置类型,而是自定义类型,那么我们就要用到->。

比如下面的日期类:

struct Date{int _year;int _month;int _day;Date(int year = 1, int month = 1, int day = 1):_year(year), _month(month), _day(day){}};void test_list2(){list lt;lt.push_back(Date(2022, 3, 12));lt.push_back(Date(2022, 3, 13));lt.push_back(Date(2022, 3, 14));//普通指针用解引用访问,结构体指针用箭头list::iterator it = lt.begin();while (it != lt.end()){//(*it)代表结点的数据也就是日期类的对象/*cout << (*it)._year << " ";cout << (*it)._month << " ";cout << (*it)._day << " ";cout << endl;*/cout << it->_year << " ";cout << it->_month << " ";cout << it->_day << " ";cout << endl;it++;}cout << endl;}

那么该如何重载呢?

在这里我们先写出重载的函数,再加以解释。

Ptr operator->(){return &_node->_data;}

如果你认真去推理你会发现这个重载函数是有问题的,it->找到的是自定义类型Date*,只有再->才是对应的Date的成员变量呀。所以按理说这里应该是it->->,才行呀。

因此,我们可以得出,如果两个->的话,程序的可读性太差了,所以这里编译器做了特殊的处理,所以省略了一个->。

四、list的模拟实现

1、构造函数

list()//构造函数{_head = new Node();//申请头结点_head->_next = _head;//头结点的后一个结点指向自己_head->_prev = _head;//头结点的前一个结点指向自己}

2、拷贝构造函数

传统写法:

首先申请一个头结点,使头结点前驱指针和后继指针都指向自己。然后将容器中的数据通过遍历的方式尾插到新容器后面。

现代写法:

首先申请一个头结点,使头结点前驱指针和后继指针都指向自己。然后创建一个名叫tmp的list容器并且使lt1容器通过迭代器区间的方式初始化tmp,随后交换tmp的头结点和lt2。

传统写法//lt2(lt1)list(const list& lt){_head = new Node();_head->_next = _head;_head->_prev = _head;for (auto e : lt){push_back(e);  }}
//现代写法//lt2(lt1)list(const list& lt){_head = new Node();_head->_next = _head;_head->_prev = _head;list tmp(lt.begin(), lt.end());std::swap(_head, tmp._head);}

3、赋值运算符重载函数

传统写法:

直接将lt中的值一个个遍历尾插到lt1中。

现代写法:

直接交换两个容器。

首先利用编译器机制,故意不使用引用接收参数,通过编译器自动调用list的拷贝构造函数构造出来一个list对象,然后调用swap函数将原容器与该list对象进行交换即可。

这样做相当于将应该用clear清理的数据,通过交换函数交给了容器lt,而当该赋值运算符重载函数调用结束时,容器lt会自动销毁,并调用其析构函数进行清理。

//传统写法
//lt2=lt1list& operator=(const list& lt){if (this != <){clear();for (auto e : lt){push_back(e);}}return *this;}
//现代写法
list& operator=(list lt){std::swap(_head, lt._head);return *this;}

4、析构函数

先用clear清理数据,然后释放头结点,再把头结点置空。

~list(){clear();delete _head;_head = nullptr;}

5、迭代器相关函数

begin和end

iterator begin(){return iterator(_head->_next);}iterator end(){return iterator(_head);}const_iterator begin() const{return const_iterator(_head->_next);}const_iterator end() const{return const_iterator(_head);}

insert

先根据所给迭代器得到该位置处的结点指针cur,然后通过cur指针找到前一个位置的结点指针prev,接着根据所给数据x构造一个待插入结点,之后再建立新结点与cur之间的双向关系,最后建立新结点与prev之间的双向关系即可。

iterator insert(iterator pos, const T& x){Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(x);prev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;return iterator(newnode);}

erase

首先使用断言assert来使所删除位置不能是头结点。先根据所给迭代器得到该位置处的结点指针cur,然后通过cur指针找到前一个位置的结点指针prev,以及后一个位置的结点指针next,紧接着释放cur结点,最后建立prev和next之间的双向关系即可。

iterator erase(iterator pos){assert(pos != end());Node* prev = pos._node->_prev;Node* next = pos._node->_next;delete pos._node;prev->_next = next;next->_prev = prev;return iterator(next);}

push_back和pop_back与push_front和pop_front

可以复用insert和erase进行处理。

        void push_back(const T& x){//Node* tail = _head->_prev;//记录最后的尾结点//Node* newnode = new Node(x);//创建新结点空节点也适用链接各个节点之间的关系//tail->_next = newnode;//newnode->_prev = tail;//newnode->_next = _head;//_head->_prev = newnode;insert(end(), x);}void push_front(const T& x){insert(begin(), x);}void pop_back(){erase(--end());}void pop_front(){erase(begin());}

clear

复用 erase,只保留头结点。

void clear(){/*iterator it = begin();while (it != end()){iterator del = it++;delete del._node;}_head->_next = _head;_head->_prev = _head;*/iterator it = begin();while (it != end()){iterator del = it++;erase(del);}}

五、反向迭代器的模拟实现

反向迭代器有一点非常重要,就是rbegin和rend的位置。

我们所知道的begin就是指向数据的第一个元素,然而end是指向最后一个元素的后一个位置。

那么反向迭代器呢,rbegin是指向数据的最后一个数据,rend是指向数据的第一个数据的前一个。

也就是如图所示的情况:

 

 可是,list的反向迭代器的源代码却不是这样实现的,而是如图所示:

 可是为什么这么做呢?,难道只是为了强迫症吗?

其实不然,我们可以发现list这样做的话begin与rend和end和rbegin有很好的的对称性,也印证了为什么我们在vector和string的时候不对反向迭代器进行模拟实现,因为当我们模拟实现了list这个反向迭代器之后,我们的vector和string可以直接使用,就是因为这个很好的对称性!

那么现在有一个问题,我们既然把rend和rbegin这样都向后移动了一位,那我们解引用的时候,也就是通过rend和rbegin拿到他们对应的值不就乱套了,所以我们此时对 *  运算符进行重载的时候很好的避免了这一点,这也就是为什么下边代码中 * 运算符重载返回的是 return *--prev;


namespace mwb
{//Iterator是哪个容器的迭代器,reverse_iterator就可以//适配出哪个容器的反向迭代器。复用的体现template class reverse_iterator{typedef reverse_iterator 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与list优劣

vector缺陷:

连续的物理空间,是优势也是劣势。

劣势:

1、空间不够要增容,增容代价比较大。

2、可能存在一定空间浪费。按需申请,会导致频繁增容,所以一般都会2倍左右扩容。

3、头部或者中部插入删除需要挪动数据,效率低下。

list优势:

1、按需申请释放空间

2、list任意位置支持O(1)插入删除。

综上所述:

本质vector和list是互补的两个数据结构。

上一篇:Centos7宝塔部署python

下一篇:秋夜赏月

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
苁蓉的食用方法 鲜苁蓉的吃法有... 肉苁蓉号称“温而不热,补而不峻,暖而不燥,滑而不泄,故有从容之名。”历来也是传统药食同源的滋补佳品。...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
阳澄湖在哪里哪个省的 阳澄湖是... 点击题目下方苏州生活指南有用 有趣 有态度近期的阳澄湖度很高,3月24日,央视二套《第一时间》直播连...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...