list模拟实现

news/2024/11/30 2:50:47/

image-20221213014908853

文章目录

  • list的介绍
  • list和vector的对比
    • **list和vector对于排序算法速度的比较**
    • **list和vector对于迭代器的比较**
  • **list的模拟实现**
    • **框架**
      • **节点**
      • **迭代器**
        • **普通迭代器-普通写法**
        • **const 迭代器-普通写法**
        • **迭代器-高级写法**
        • **链表结构**
        • **关于节点的析构**
        • **关于迭代器没有实现拷贝构造和赋值为什么可以用?**

list的介绍

1、list是可以在常数范围内在任意位置进行插入和删除(相比其他容器如vector、string等等效率更高)

2、list底层是带头双向链表结构,每个元素存储在互不相干的独立节点,节点之间通过指针相互联系

3、与其他容器相比,list存在的最大缺陷就是不能随机访问即不能通过【】访问(list的内存空间的不连续的)

list和vector的对比

vector

优点缺点
下标随机访问
尾插尾删效率高前面部分插入删除数据效率低O(N)
cpu高速缓存命中高扩容有损耗,还有一定的空间浪费

list

优点缺点
按需申请释放空间,无需扩容不支持下标随机访问
任意位置插入删除O(1)cpu高速缓存命中低

可以看到vector和list是互补配合的!

list和vector对于排序算法速度的比较

这里我创建了一个list和vector,对两者同时插入100w个和1000w个随机数,插入后记一次时间,然后进行排序,排完序再记一次时间,两次时间相差即排序的时间, 然后对两者时间比较;理论存在实践开始:

//要包含的头文件
#include<iostream>
#include<list>
#include<vector>
#include<time.h>
#include<algorithm>
using namespace std;void test_sort_time()
{srand(time(0));const int N = 1000000;//100w个随机数vector<int> v;v.reserve(N);//扩容list<int> lt1;for (int i = 0; i < N; ++i){auto e = rand();lt1.push_back(e);}int begin1 = clock();lt1.sort();int end1 = clock();//记的list排序时间for (int i = 0; i < N; ++i){auto e = rand();v.push_back(e);}int begin2 = clock();
sort(v.begin(), v.end());int end2 = clock();//记的vector排序时间printf("list sort:%d\n", end1 - begin1);//打印list排序时间
printf("vector sort:%d\n", end2 - begin2);//打印vector排序时间

debug版本下,得出结论vector排序比list块,但两者差距不是很明显【可能是我这破电脑的缘故(doge】

image-20221210195518840

release版本下,可以看到两者差距还是非常大的,甚至把list里面的数据导入vector里排序完再导回list的时间都比本身list排序的时间短(各位彦祖们可以去尝试噢)

image-20221210195633988

list和vector对于迭代器的比较

在STL中,迭代器一般有两个特点:1、内嵌类型;2、行为像指针一样

内嵌类型:1、内部类-在类里面定义的类;2、typedef出来的(typedef内置类型)

1、迭代器类型: vector的迭代器本质上是内置类型 ,但实现方式不一定是(可以用内置类型实现)—在g++底层是用内置类型实现;vs下不是内置类型实现-迭代器通过封装已经不是原生指针了。 list的迭代器底层实现是内部类

2、迭代器解引用: vector的迭代器解引用是对指针的解引用 (可以取到数据);而 list的迭代器解引用是对节点的指针解引用但这种方式取不到数据所以要用函数调用 (用的operator()函数重载)*

3、对于迭代器的判断: vector迭代器可以用>或<进行判断 —vector内存空间是连续的,连续的地址可以大小比较; list迭代器不能用>或<进行判断,只能用!=判断 —list内存空间不是连续的且是随机的

这里的实验直接或间接证明了第1点和第3点:我创建了一个vector和list,同样插入数据1234,通过对迭代器的++一个遍历打印;通过内存观察看出这里的两者迭代器的不同点

这里是vector迭代器++,++后前后地址相差为一个数据类型大小,说明vector迭代器本质是原生指针

image-20221210192829661

这里是list迭代器++,加加后前后地址相差并不是一个数据类型大小,说明vector迭代器本质不是原生指针,而且通过调试可知这是函数调用

image-20221210193007183

迭代器的优势: 1、封装底层实现,不暴露底层实现细节 2、提供了统一的访问方式,降低了使用成本

list的模拟实现

框架

我们要用命名空间围起来

namespace listrealize()
{ //节点template<typename T>struct list_node(){list_node<T>* _next;list_node<T>* _prev;T _data;list_node(const T& val)//节点初始化:_next(nullptr),_prev(nullptr),_data(val){}};//迭代器template<typedef T>struct list_iterator{............    };//链表结构template<typename T>class list{typedef list_node<T> node;public:typedef list_iterator<T> iterator;............
};}

节点

	template<typename T>struct list_node//节点初始化{list_node <T>* _next;list_node <T>* _prev;T _data;list_node(const T& val):_next(nullptr),_prev(nullptr),_data(val){}}

迭代器

由于list迭代器不是像vector那样的原生指针,所以得用类中类来写迭代器,而不是直接typedef出来,而且普通迭代器和const迭代器都要写

普通迭代器-普通写法

template<typename T>struct list_iterator{typedef list_node<T> node;node* _pnode;//成员变量---类指针list_iterator(node* p)//构造函数-迭代器:_pnode(p){}T& operator*()//引用返回{return _pnode->_data;//返回数据}list_iterator& operator++()//返回下一个节点{_pnode = _pnode->_next;return *this;}list_iterator& operator--()//返回上一个节点{_pnode = _pnode->_prev;return *this;}bool operator!=(const list_iterator& it ){return _pnode != it._pnode;}};

const 迭代器-普通写法

template<typename T>struct list_const_iterator{typedef list_node<T> node;node* _pnode;//成员变量---类指针list_const_iterator(node* p)//构造函数-迭代器:_pnode(p){}const T& operator*(){return _pnode->_data;}list_const_iterator<T>& operator++(){_pnode = _pnode->_next;return *this;}list_const_iterator<T>& operator--(){_pnode = _pnode->_prev;return *this;}bool operator!=(const list_const_iterator<T>& it){return _pnode != it._pnode;}};

迭代器-高级写法

如果我们按照上面普通写法实现迭代器的话,相当于写了两份迭代器(普通迭代器和const迭代器),而大佬是不允许发生这种情况。大佬选择给类模板和函数模板增加参数关键字来减少代码膨胀。为什么不能直接在迭代器前加const以构造const迭代器的原因是迭代器指向的内容可读可写,而const迭代器指向的内容只能读不能写。在迭代器前面加const是指迭代器这个像指针的的东西不能改变!(指针指定的方向不能改变)

image-20221212205708443

而list这里的迭代器不是原生指针,迭代器和const迭代器其本质区别问题所在就是对数据的输出即解引用,所以我们通过给类模板和函数模板加参数就能实现。

image-20221212211244830

template<typename T,typename ref>struct list_iterator{typedef list_node<T> node;typedef list_iterator<T, ref> self;node* _pnode;//成员变量---类指针list_iterator(node* p)//构造函数-迭代器:_pnode(p){}ref& operator*(){return _pnode->_data;}self& operator++()//前置++{_pnode = _pnode->_next;return *this;}self operator++(int)//后置++{self tmp(*this);_pnode = _pnode->_next;return tmp;//返回迭代器-不需要用引用}self& operator--()//前置--{_pnode = _pnode->_prev;return *this;}self operator--(int)//后置--{self tmp(*this);//用地址构造迭代器_pnode = _pnode->_prev;return tmp;}bool operator!=(const self& it ){return _pnode != it._pnode;}bool operator==(const self& it){return _pnode == it._pnode;}};

链表结构

	template <typename T>class list//链表结构{typedef list_node<T>  node;public:typedef list_iterator<T,T&>  iterator;typedef list_iterator<T,const T&> const_iterator;void empty_initialize()//节点初始化{_head = new node(T());_head->_next = _head;_head->_prev = _head;_size=0;}list()//构造函数{empty_initialize();}template <class InputIterator>list(InputIterator first, InputIterator last)//迭代器区间构造{empty_initialize();while (first != last){push_back(*first);++first;}}//lt1(lt)list( const list<T>& lt)//旧时代写法{//自己初始化一份empty_initialize();for (const auto& e : lt)//这里要用引用{push_back(e);}}list(const list<T>& lt)//拷贝构造-现代写法{//自己初始化一份empty_initialize();list<T> tmp(lt.begin(), lt.end());swap(tmp);}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);}size_t size() const{return _size;}bool empty()const{return _size == 0;}~list()//析构{clear();delete[] _head;_head = nullptr;}void clear(){iterator pos = begin();while (pos != end()){pos=erase(pos);}}void swap( list<T>& lt){std::swap(_head, lt._head);std::swap(_size, lt._size);}void push_back(const T& val)//传的是数据-T&{insert(end(), val);/*	node* cur = new node(val);node* tail = _head->_prev;tail->_next = cur;cur->_prev = tail;cur->_next = _head;_head->_prev = cur;*/}void push_front(const T& val){insert(begin(), val);}iterator insert(iterator pos, const T& val)//在pos节点前面插入{node* newnode = new node(val);node* cur = pos._pnode;node* prev = cur->_prev;//链接newnode->_next = cur;cur->_prev = newnode;newnode->_prev = prev;prev->_next = newnode;++_size;return iterator(newnode);}iterator erase(iterator pos)//删除节点{assert(end());node* cur = pos._pnode;node* next = cur->_next;node* prev = cur->_prev;//链接next->_prev = prev;prev->_next = next;delete[] cur;--_size;return iterator(next);}void pop_back(){assert(++end());erase(_head->_next);}void pop_front(){assert(--end());erase(_head->_prev);}list<T>& operator=( const list<T>&lt)//这里加const就报错{if (this != &lt){clear();for (const auto& e : lt){push_back(e);}}return *this;}private:node* _head;};

写到这,我们创建一个POS,类似坐标的类,成员变量有_row和 _col,那我们用POS来初始化list,该怎么访问到POS 里的 _row 和 _col呢?

	struct POS{int _row;int _col;POS(int row = 0, int col = 0):_row(row),_col(col){}};

我们知道,在类外面访问类的公有成员可以用三种访问方式:访问限定符:: 、.、->

所以我们可以用->指针的方式访问,那么还需要在写一个->的迭代器运算符重载

		ptr operator->(){return &_pnode->_data;//返回节点数据的地址,用指针接收}

image-20221213012709356

加上之后就能访问了

下面是完善后的代码

	//节点的实现template<typename T>struct list_node//节点初始化{list_node <T>* _next;list_node <T>* _prev;T _data;list_node(const T& val):_next(nullptr),_prev(nullptr),_data(val){}}//迭代器的实现template<typename T,typename ref,typename ptr>struct list_iterator{typedef list_node<T> node;typedef list_iterator<T, ref,ptr> self;node* _pnode;//成员变量---类指针list_iterator(node* p)//构造函数-迭代器:_pnode(p){}ptr operator->(){return &_pnode->_data;//返回节点数据的地址,用指针接收}ref& operator*(){return _pnode->_data;}self& operator++()//前置++{_pnode = _pnode->_next;return *this;}self operator++(int)//后置++{self tmp(*this);_pnode = _pnode->_next;return tmp;//返回迭代器-不需要用引用}self& operator--()//前置--{_pnode = _pnode->_prev;return *this;}self operator--(int)//后置--{self tmp(*this);//用地址构造迭代器_pnode = _pnode->_prev;return tmp;}bool operator!=(const self& it ){return _pnode != it._pnode;}bool operator==(const self& it){return _pnode == it._pnode;}};
//链表的实现
template <typename T>class list//链表结构{typedef list_node<T>  node;public:typedef list_iterator<T,T&,T*>  iterator;typedef list_iterator<T,const T&,const T*> const_iterator;void empty_initialize(){_head = new node(T());_head->_next = _head;_head->_prev = _head;_size = 0;}list()//构造函数{empty_initialize();}template <class InputIterator>list(InputIterator first, InputIterator last)//迭代器区间构造{empty_initialize();while (first != last){push_back(*first);++first;}}list(const list<T>& lt)//拷贝构造-现代写法{//自己初始化一份empty_initialize();list<T> tmp(lt.begin(), lt.end());swap(tmp);}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);}size_t size() const{return _size;}bool empty()const{return _size == 0;}~list()//析构{clear();delete[] _head;_head = nullptr;_size = 0;}void clear(){iterator pos = begin();while (pos != end()){pos=erase(pos);}}void swap( list<T>& lt){std::swap(_head, lt._head);std::swap(_size, lt._size);}void push_back(const T& val)//传的是数据-T&{insert(end(), val);}void push_front(const T& val){insert(begin(), val);}iterator insert(iterator pos, const T& val)//在pos节点前面插入{node* newnode = new node(val);node* cur = pos._pnode;node* prev = cur->_prev;//链接newnode->_next = cur;cur->_prev = newnode;newnode->_prev = prev;prev->_next = newnode;++_size;return iterator(newnode);}iterator erase(iterator pos){assert(end());node* cur = pos._pnode;node* next = cur->_next;node* prev = cur->_prev;//链接next->_prev = prev;prev->_next = next;delete[] cur;--_size;return iterator(next);}void pop_back(){assert(++end());erase(_head->_next);}void pop_front(){assert(--end());erase(_head->_prev);}list<T>& operator=(list<T> lt){swap(lt);return *this;}private:node* _head;size_t _size;//复用-提高代码的可维护性};

关于节点的析构

我们知道,c++类的自定义类型会调用自定义类型的析构函数,没有析构函数则调用默认析构,而写了析构函数的类一般都要写拷贝构造和赋值运算符重载,那么编译器敢析构节点吗?又或者说编译器会析构头节点吗?

首先答案很明确是不会的!

1、在程序结束时会调用自定义类型的析构函数,而内置类型则不会-指针也不会,在这里若把这个头节点析构掉了则找不到后面的节点了

2、迭代器也不会把指针析构掉,这个指针是链表的,迭代器只是借用它指向数据;

关于迭代器没有实现拷贝构造和赋值为什么可以用?

首先迭代器没有实现析构函数,那么就不是必须实现拷贝构造和赋值运算符重载;其次,迭代器拷贝构造和赋值运算符重载都属于浅拷贝地址—指针指向同一块空间,因为迭代器不会把指向的数据析构,所以浅拷贝就够用!

好啦,关于list的实现和相关问题就分析到这里,制作不易,给个点赞支持支持吧~~~


http://www.ppmy.cn/news/3273.html

相关文章

12.13

AJAX 视频 1-11 1.1 AJAX 简介 AJAX 全称为 Asynchronous JavaScript And XML&#xff0c;就是异步的 JS 和 XML。 通过 AJAX 可以在浏览器中向服务器发送异步请求&#xff0c;最大的优势&#xff1a;无刷新获取数据。 AJAX 不是新的编程语言&#xff0c;而是一种将现有…

通过数据分析提升客流量和销售额

只有把每一位来光临的顾客都当作最后一位顾客&#xff0c;我们才可能真正实现营销和服务管理的精细化&#xff0c;正如只有把自己的每一天都当作生命的最后一天来对待的人才可能真正珍惜时间和生命一样。 如何提高门店的销售额可以说是大部分店长都十分困惑的问题&#xff0c;…

Sklearn机器学习与Plotly可视化强强联合

在学习sklearn(机器学习)过程中&#xff0c;模型原理可谓是枯燥无味&#xff0c;加上大多数模型训练过程也是不可见的&#xff0c;这使得很多小伙伴们望而却步&#xff0c;当然也有很多学者试图通过各种方式以可视化模型学习及预测过程&#xff0c;但大多数是复杂且不美观的。 …

解决visual studio对不安全函数的警告

解决visual studio 对scanf &#xff0c;strcpy&#xff0c;strcmp等函数的不安全警告报错 可以看到&#xff0c;编译器对scanf进行了报错&#xff0c;原因是说它不安全 编译器自己给了一种解决方案&#xff1a; 使用vs自带的 scanf_s&#xff0c;但是用这个函数&#xff0c;仅…

Django demo项目搭建

安装 Django 在应用程序开发中&#xff0c;分别创建env文件夹和wordspace文件夹。 env文件夹用于存放创建的虚拟环境&#xff0c;wordspace用于存放项目代码&#xff0c;至此实现虚拟环境和应用程序代码的分隔。 步骤1&#xff1a;创建文件夹&#xff0c;创建命令为mkdir en…

Day49.动规:买卖股票I、II

Day49.动规&#xff1a;买卖股票I、II 0121.买卖股票的最佳时机 链接&#xff1a;0121.买卖股票的最佳时机 dp[i][0]&#xff0c;第i天持有股票的最大现金&#xff1b;dp[i][1]第i天不持有股票的最大现金。持有不一定是买当天股票&#xff0c;只是保持着持有状态&#xff0c…

[附源码]计算机毕业设计的项目管理系统Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; Springboot mybatis MavenVue等等组成&#xff0c;B/S模式…

Tomcat的Maven插件使用方法(在idea里面运行Tomcat)

目录 一、概述 二、下载和导入插件 三、测试使用方式 四、总结 一、概述 使用这个插件可以快速的运行Tomcat&#xff0c;比在本地配置快得多。 二、下载和导入插件 1.下载插件Maven Helper ps&#xff1a;已经有下载过这个插件的可以跳过此步骤 &#xff08;1&#xff…