当前位置: 首页 > news >正文

【C++】list 常见使用和模拟实现

作者主页:lightqjx

本文专栏:C++

目录

一、list简介

二、list的使用

1. 常见接口

2. list的操作

3. list的迭代器问题

三、list的模拟实现

1. 定义成员变量

2. 迭代器的实现(重点)

(1)关键部分实现

(2)const迭代器

(3)完善list的迭代器

3. 插入

4. 删除

5. 构造&析构

6. 求list中的数据个数

四、总代码参考


一、list简介

        对list的详细介绍,在链接中的解释是比较详细的:list文档介绍  。

        简而言之,lis的结构就是一个带哨兵位的双向循环链表。它是标准模板库(STL)提供的双向链表容器,是一种可以在常数范围内在任意位置进行插入和删除的序列式容器。

它的优点是插入/删除高效(O(1)的时间),支持双向遍历。缺陷是不支持随机访问(O(n)的时间)。

        list和vector一样,都是通过模板定义的,所以定义时都是需要指明类型的。如图:

使用时要注意需要包含头文件list,如以下代码:

#include <list>
int main()
{std::list<int> ls1;std::list<char> ls2;std::list<double> ls3;return 0;
}

二、list的使用

1. 常见接口

        在list中的接口有许多,但它也是STL中的一个容器,大多数的接口的使用都比较类似,比如:构造,析构等等,都是非常容易掌握如何正确的使用的。如下图就是第一个list的一些部分接口:

可以发现,它们和string,vector都差不多,提供了迭代器,一些增删查改的操作等等操作。它们的使用其实都是差不多的,这里就不一一介绍了,若有还疑问,可以通过查阅本文章开头的list文档进行更多了解。这里我们来使用list的一些基本操作:

#include <list>
#include<iostream>
using namespace std;
int main()
{list<int> lt;// 尾插lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);lt.push_back(5);auto it = lt.begin();while (it != lt.end()){cout << *it << " ";it++;}cout << endl;// 头插lt.push_front(10);lt.push_front(20);lt.push_front(30);for (auto e : lt){cout << e << " ";}cout << endl;//注意list的迭代器没有重载+//在第3个位置后插入666auto it2 = lt.begin();for (int i = 0; i < 3; i++){++it2;}lt.insert(it2, 666);for (auto e : lt){cout << e << " ";}cout << endl;lt.erase(lt.begin()); //删除第一个位置for (auto e : lt){cout << e << " ";}cout << endl;//......return 0;
}

2. list的操作

        与其他容器不同的常见的几个接口,如下所示:

splice 功能: 将元素从一个列表转移到另一个列表或同一列表的另一个位置。

这里我们来示范第一个:

#include <list>
#include<iostream>
using namespace std;
int main()
{list<int> lt1;lt1.push_back(1);lt1.push_back(2);lt1.push_back(3);lt1.push_back(4);lt1.push_back(5);list<int> lt2;lt2.push_back(10);lt2.push_back(20);lt2.push_back(30);lt2.push_back(40);lt2.push_back(50);lt1.splice(lt1.begin(), lt2);//将lt2移动到lt1的开头,这是通过结点链接的,直接将结点连接在一起for (auto e : lt1){cout << e << " ";}cout << endl;return 0;
}

remove 功能: 从列表中移除所有等于特定值的元素,相当于 find+erase,没找到啥也不干。

示例:

#include <list>
#include<iostream>
using namespace std;
int main()
{list<int> lt1;lt1.push_back(1);lt1.push_back(2);lt1.push_back(3);lt1.push_back(4);lt1.push_back(5);for (auto e : lt1){cout << e << " ";}cout << endl;lt1.remove(3); //移除三这个元素for (auto e : lt1){cout << e << " ";}cout << endl;return 0;
}

remove_if 涉及仿函数,暂时不说。

unique 功能: 移除连续重复的元素,仅保留一组连续相同元素中的一个。

示例:

#include <list>
#include<iostream>
using namespace std;
int main()
{list<int> lt1;int arr[10] = { 1,1,1,3,5,5,6,1,1,3 };for (int i = 0; i < 10; i++){lt1.push_back(arr[i]);}for (auto e : lt1){cout << e << " ";}cout << endl;lt1.unique();for (auto e : lt1){cout << e << " ";}cout << endl; //输出:1 3 5 6 1 3return 0;
}

merge 功能: 合并两个已排序的列表,合并后原列表被清空,合并结果存储在当前列表中。

示例:

#include <list>
#include<iostream>
using namespace std;
int main()
{list<int> lt1;lt1.push_back(1);lt1.push_back(2);lt1.push_back(3);lt1.push_back(4);lt1.push_back(5);list<int> lt2;lt2.push_back(1);lt2.push_back(2);lt2.push_back(3);lt2.push_back(4);lt2.push_back(5);lt1.merge(lt2);for (auto e : lt1){cout << e << " "; //输出:1 1 2 2 3 3 4 4 5 5}cout << endl;for (auto e : lt2){cout << e << " ";//原列表lt2会被清空}cout << endl;return 0;
}

sort 功能: 对列表中的元素进行排序。

但要注意这里的list库里面的sort实际上没有什么意义,因为在我算法库里面已经实现了一个sort是用快排实现的,而在这里的底层实现是归并排序,效率比较低,不能频繁使用。这里list库里面的sort的实际意义就是使用起来比较方便。使用list中的sort如下所示:

#include <list>
#include<iostream>
using namespace std;
int main()
{list<int> lt1;list<int> lt2;int arr[10] = { 10,9,8,7,6,5,4,3,2,1 };for (int i = 0; i < 10; i++){lt1.push_back(arr[i]);lt2.push_back(arr[i]);}//lt1.sort();for (auto e : lt1){cout << e << " "; //输出:1,2,3,4,5,6,7,8,9,10}cout << endl;return 0;
}

reverse 功能: 反转列表中元素的顺序。

示例:

#include <list>
#include<iostream>
using namespace std;
int main()
{list<int> lt1;int arr[10] = { 10,5,6,7,6,5,4,3,2,1 };for (int i = 0; i < 10; i++){lt1.push_back(arr[i]);}lt1.reverse();for (auto e : lt1){cout << e << " "; //输出:1 2 3 4 5 6 7 6 5 10}cout << endl;return 0;
}

3. list的迭代器问题

        首先我们要知道在C++中的迭代器是有不同种类的,当下阶段,迭代器大致可以分为单向迭代器,双向迭代器,随机迭代器这三种。

迭代器在某些库里的函数中常常会出现,用来表示调用该函数的容器必须要含有对应的迭代器种类(在模板定义中)才能够调用,比如算法库里的sort,只有是随机迭代器容器(如vector,string等等)才能调用。

如果我们要确认一个容器到底属于哪一种迭代器,就可以查看各个容器文档的这一页:

还有一点需要注意:

以上就是我们使用迭代器需要注意的问题了。

所以为什么在list中就不能使用算法库的sort原因了。

        对于sort需要注意,在list中是使用归并排序实现的,而算法库里的sort1一般是使用快排实现的,所以list中的算法效率比较低,一般不频繁使用。所以我们常常时通过先将list中的数据拷贝到vector中去,再使用算法库里的sort进行排序,最后再将vector中的数据拷贝回来(这里拷贝的效率是非常高的,所以一般不会影响整个过程的效率)。使用示例如下:

#include <list>
#include<vector>
#include<iostream>
#include<algorithm>
using namespace std;
int main()
{list<int> lt;srand(time(0));const int N = 100000;vector<int> v;v.reserve(N);for (int i = 0; i < N; ++i){auto e = rand();lt.push_back(e);}int begin1 = clock();// 先拷贝到vectorfor (auto e : lt){v.push_back(e);}// 排序sort(v.begin(), v.end());// 拷贝回去size_t i = 0;for (auto& e : lt){e = v[i++];}// 完成排序return 0;
}

以上差不多就是list的一些基本使用情况了。


三、list的模拟实现

        对list的模拟实现是我们理解list的一个重要操作。

1. 定义成员变量

        我们知道list是一个带哨兵位的双向循环链表,也就是说,它是一个一个结点组成的。

        我们首先需要将结点的结构实现出来,由于结点需要存储不同的数据,所以这里就需要使用模板。在C++中,这里由于结点中的变量需要再外部进行访问,所以我们可以将其通过struct实现出来,也可以是使用class实现,但需要将其设为公有的变量或者使用友元。

        对于list中的变量直接是一个结点的指针,需要设为私有的。

即:

namespace MyCreat
{template<class T>struct list_node{list_node<T>* prev;list_node<T>* next;T val;};template<class T>class list{typedef list_node<T> Node; //重定义一下,更加方便public://......函数......private:Node* _head;};
}

2. 迭代器的实现(重点)

        如果我们直接来看库里面的list的实现,会发现它是有三个模板的,可能会看不懂,如图:

现在我们就来揭秘一下  为什么会有三个模板参数?以及list中的迭代器如何实现?

(1)关键部分实现

        首先,我们来实现一个迭代器的关键部分操作,能够用来完成遍历。

        和之前学习的string,vector的迭代器不同,list的迭代器是不能直接加加的,因为list是链表,所以它的空间是一种不连续的,所以我们在实现迭代器时就不能简单的将结点的指针作为迭代器。

        如果我们想要使用迭代器来遍历list,就必须要使用到迭代器的加加,既然list的看就看不连续,那么我们就需要实现一个list的迭代器类,来封装一下迭代器的运算,由于list对不同类型的数据的存储使用的是模板,那么实现一个迭代器类也是需要模板的。

//实现一个迭代器的类
template<class T>
struct_list_iterator
{typedef list_node<T> Node;Node* _node;_list_iterator(Node* node) // 构造:_node(node){ }T& operator*(){return _node->_val;}_list_iterator<T>& operator++()//重载前置++{_node = _node->_next;return *this;}bool operator!=(const _list_iterator<T>& it)//加const的原因是因为end是值返回,具有常性{return _node != it._node;}//通过上面就可以完成对list的遍历了// ....
};

在list类中,对于begin和end的函数,可以返回它们对应的结点指针:

//返回值说明:单参构造发生了隐式类型的转换
iterator begin()
{return _head->_next;
}
iterator end()
{return _head;
}

这里两个代码需要结合者看,在上面的  bool operator!=(const _list_iterater<T>& it)  的参数加const的原因就是在遍历访问时,对于以下这种代码:

MyCreat::_list_iterator<int> it = lt.begin();
while (it != lt.end())
{cout << *it << " ";++it;
}

因为end()是值返回,返回的是临时变量,具有常性,需要加上const修饰。

以上就是要遍历list的数据需要的迭代器的部分关键实现了

(2)const迭代器

        在这里我们会解释第二个模板参数的作用。

        list的const迭代器,是要求迭代器可以正常的加加,即自己可以被修改;但是迭代器指向的内容不能被修改。

所以我们的const迭代器就可以这么:将上面的操作*的返回值改为const T&。这样就可以实现一个const的迭代器了,这里我们将上面的代码拷贝一份,在将其 _list_iterater 改为_list_const_iterater ,得到下面的代码:

//实现一个const迭代器的类
template<class T>
struct_list_const_iterator
{typedef list_node<T> Node;Node* _node;_list_const_iterator(Node* node) // 构造:_node(node){}const T& operator*(){return _node->_val;}_list_const_iterator<T>& operator++()//重载前置++{_node = _node->_next;return *this;}bool operator!=(const _list_const_iterator<T>& it)//加const的原因是因为end是值返回,具有常性{return _node != it._node;}//通过上面就可以完成对const list的遍历了// ....
};

在list类中,对于begin和end的函数也需要重载一下:

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

如此对于const的迭代器也就搞定了。

        但是如果是这样写的话,就会有两段比较重复的代码,它们就是有操作*操作符时的返回值是不一样的,其他的都是一样的。所以我们这里可以简化一下,再使用一个模板参数,从而只使用一段迭代器代码来实现list的迭代器。看以下代码:

template<class T, class Ref>
struct _list_iterator
{typedef list_node<T> Node;typedef _list_iterator<T, Ref> self; //为了下面的函数写得方便些,我们可以将该类型重定义一下Node* _node;_list_iterator(Node* node) // 构造:_node(node){ }Ref operator*(){return _node->_val;}self& operator++()//重载前置++{_node = _node->_next;return *this;}bool operator!=(const self& it) //这里加const是因为读写权限不能放大{return _node != it._node;}//通过上面就可以完成对list的遍历了// ....
};

这就是第二个模板参数的使用。

(3)完善list的迭代器

        在上面操作中,我们实现了普通迭代器和const的迭代器的关键操作实现。比如:重载前置++,重载*,重载!= 。而除了这三种操作,list的迭代器还有几种操作,比如:后置++,前置后置--,重载==,重载-> 等。

        其中对于重载 -> ,一般重载的->操作符都是对自定义类型的数据使用的。它需要第三个模板参数。下面是对->重载的原始实现。

使用模板参数:

到这里,我们就知道了库里面的三个模板参数的由来了。

接下来我们再实现其他的重载操作,最后list的迭代器的一个类就实现完成了,如以下代码:

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->_val;}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) //这里加const是因为读写权限不能放大{return _node != it._node;}bool operator==(const self& it) {return _node == it._node;}Ptr operator->(){return &_node->_val;}
};

有了 list 的迭代器的认识之后,我们实现后面的list的基本操作就简单了。

3. 插入

list 的插入重载的实现,就是通过insert来实现的。

这里我们实现第一个就可以了,其他的都是类似的。

第一个的意思是在position位置之前插入一个val ,返回新插入结点的迭代器。

下面是insert的实现:

// pos位置之前插入val
iterator insert(iterator pos, const T& val)
{Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(val);newnode->_prev = prev;prev->_next = newnode;newnode->_next = cur;cur->_prev = newnode;return newnode;
}

有了insert之后,对于头插和尾插就都一样调用insert1就可以了:

void push_back(const T& x)
{insert(end(), x);
}void push_front(const T& x)
{insert(begin(), x);
}

4. 删除

list中的删除操作是erase。

库里面有两个删除的重载,我们这里只实现第一个,即删除position位置的结点,返回下一个位置的迭代器:

//删除pos位置的结点
iterator erase(iterator pos)
{assert(pos!=end());//检查pos是否有效Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;prev->_next = next;next->_prev = prev;delete cur;_size--;return next;
}

同样,通过erase也可以实现尾插和头删:

void pop_back()
{erase(--end());
}void pop_front()
{erase(begin());
}

5. 构造&析构

现在我们实现list的构造函数。我们实现两个常用的构造函数:默认构造和拷贝构造。即:

list() // 构造
{_head = new Node;_head->_prev = _head;_head->_next = _head;
}list(const list<T>& x) // 拷贝构造
{_head = new Node;_head->_prev = _head;_head->_next = _head;for (auto& e : x){push_back(e);}
}

析构函数需要遍历链表,释放各个结点的空间,清理内部的指针(prev和next设为空)。这里我们可以使用另一种方法:使用clear函数。

clear是用来清空链表中的所有元素的,所以析构函数的实现为:

void clear()
{iterator it = begin();while (it != end()){it=erase(it);//防止迭代器失效}
}
~list()
{clear();delete _head;_head = nullptr;
}

6. 求list中的数据个数

        要求list中的数据个数,一种方法是可以直接遍历,通过计数来求;一种方法是通过在list在成员变量里设置一个变量专门来计数。在遇到插入时就加一,遇到删除就减一。很明显第一种方法是有效率的问题的。所以我们下面使用的是第二种。

C++11之前的是O(n),即通过遍历来求size;C++11之后是O(1),在list中缓存一个size变量。

size_t size()
{//遍历求size//size_t sz = 0;//iterator it = begin();//while (it != end())//{//	++sz;//	++it;//}//return sz;return _size;
}

在上述的所有操作中,需要修改的插入或删除操作有:


四、总代码参考

#pragma once
#include <assert.h>
namespace MyCreat
{template<class T>struct list_node{list_node<T>* _prev;list_node<T>* _next;T _val;list_node(const T& val = T()): _prev(nullptr), _next(nullptr), _val(val){ }};/*//实现一个迭代器的类template<class T>struct _list_iterator{typedef list_node<T> Node;Node* _node;_list_iterator(Node* node) // 构造:_node(node){ }T& operator*(){return _node->_val;}_list_iterator<T>& operator++()//重载前置++{_node = _node->_next;return *this;}bool operator!=(const _list_iterator<T>& it)//加const的原因是因为end是值返回,具有常性{return _node != it._node;}//通过上面就可以完成对list的遍历了// ....};//实现一个const迭代器的类template<class T>struct _list_const_iterator{typedef list_node<T> Node;Node* _node;_list_const_iterator(Node* node) // 构造:_node(node){}const T& operator*(){return _node->_val;}_list_const_iterator<T>& operator++()//重载前置++{_node = _node->_next;return *this;}bool operator!=(const _list_const_iterator<T>& it)//加const的原因是因为end是值返回,具有常性{return _node != it._node;}//通过上面就可以完成对const list的遍历了// ....};*/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->_val;}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) //这里加const是因为读写权限不能放大{return _node != it._node;}bool operator==(const self& it) {return _node == it._node;}Ptr operator->(){return &_node->_val;}};template<class T>class list{typedef list_node<T> Node; //重定义一下,更加方便public:typedef _list_iterator<T,T&,T*> iterator;typedef _list_iterator<T,const T&,T*> const_iterator;//返回值说明:单参构造发生了隐式类型的转换iterator begin(){return _head->_next;}iterator end(){return _head;}const_iterator begin() const{return _head->_next;}const_iterator end() const{return _head;}// ------------------------------------------------------// pos位置之前插入valiterator insert(iterator pos, const T& val){Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(val);newnode->_prev = prev;prev->_next = newnode;newnode->_next = cur;cur->_prev = newnode;_size++;return newnode;}void push_back(const T& x){insert(end(), x);}void push_front(const T& x){insert(begin(), x);}// ------------------------------------------------------//删除pos位置的结点iterator erase(iterator pos){assert(pos!=end());//检查pos是否有效Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;prev->_next = next;next->_prev = prev;delete cur;_size--;return next;}void pop_back(){erase(--end());}void pop_front(){erase(begin());}// ------------------------------------------------------list() // 构造{_head = new Node;_head->_prev = _head;_head->_next = _head;_size = 0;}list(const list<T>& x) // 拷贝构造{_head = new Node;_head->_prev = _head;_head->_next = _head;for (auto& e : x){push_back(e);}}//清空元素void clear(){iterator it = begin();while (it != end()){it=erase(it);}_size = 0;}~list(){clear();delete _head;_head = nullptr;}// ------------------------------------------------------size_t size(){//遍历求size//size_t sz = 0;//iterator it = begin();//while (it != end())//{//	++sz;//	++it;//}//return sz;return _size;}private:Node* _head;size_t _size;};
}

总体而言,通过模拟实现list,我们那个更加清晰的认识list的底层结构,对list的运用能够更加得熟悉!

感谢各位观看!希望能多多支持!

http://www.dtcms.com/a/528234.html

相关文章:

  • HTML HTML5基础(2)
  • macOS安装使用Oh My Tmux教程
  • SpringBoot+FFmpeg+ZLMediaKit 实现本地视频推流
  • Mac-终端
  • 中科大分子生物学Ⅲ复习题2025年
  • 关于lvgl-for linxu的dockerfile
  • 做ppt的网站叫什么名字sem和seo是什么职位
  • 临沂网站建设有哪些公司网页制作模板
  • UDP多线程在线咨询
  • 微信小程序原生如何使用画布生成名片
  • Postman介绍和安装,发送带参数的GET请求
  • 海西州wap网站建设公司网站布局怎么用dw做
  • Python入门经典题目
  • 佛山网站建设乐云seo在线制作wordpress媒体库略缩图
  • 网站的服务器怎么做的网站模板软件
  • Go Web 编程快速入门 07.4 - 模板(4):组合模板与逻辑控制
  • 【Canvas与旗帜】标准加拿大枫叶旗
  • LwIP协议栈MPA多进程架构
  • 【JUnit实战3_12】第七章:用 Stub 模拟进行粗粒度测试
  • 东莞网络推广网站做静态网站软件
  • 想建网站做优化网站建设服务费 印花税
  • verilog阻塞赋值和非阻塞赋值的区别
  • 【Redis典型应用——缓存详解】
  • 阮一峰《TypeScript 教程》学习笔记——模块
  • 第 09 天:文件传输 (SCP, SFTP, rsync, FTP, NFS)
  • pandas 和 numpy相关函数详解
  • 酵母 cDNA 文库:解码基因表达与功能研究的核心工具
  • Win10使用WSL2安装ubuntu22.04
  • macos 下 docker使用方法 新手教程
  • t恤定制网站哪个网站是做红酒酒的