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

【C++】STL 模拟实现之 list

文章目录

  • 一、list 的常用接口及其使用
    • 1、list 一般接口
    • 2、list 特殊接口
    • 3、list 排序的性能分析
  • 二、list 迭代器的实现
    • 1、迭代器的分类
    • 2、list 迭代器失效问题
    • 3、list 迭代器源码分析
    • 4、list 迭代器模拟实现
      • 4.1 普通迭代器
      • 4.2 const 迭代器
      • 4.3 完整版迭代器
  • 三、list 的模拟实现
  • 四、vector 和 list 的区别

一、list 的常用接口及其使用

1、list 一般接口

list 是可以在常数范围内在任意位置进行插入和删除的序列式容器,其底层是带头双向循环链表;list 常用接口的使用和 string、vector 系列容器的接口使用一样,这里我就不再详细介绍,具体使用细节可以查看 list 使用文档 – cplusplus.com。

构造函数

-构造函数( constructor)-接口说明
list (size_type n, const value_type& val = value_type())构造的 list 中包含n个值为val的元素
list()构造空的 list
list (const list& x)拷贝构造函数
list (InputIterator first, InputIterator last)用 [first, last) 区间中的元素构造 list

增删查改

函数声明-接口说明
push_front在list首元素前插入值为val的元素
pop_front删除list中第一个元素
push_back在list尾部插入值为val的元素
pop_back删除list中最后一个元素
insert在list position 位置中插入值为val的元素
erase删除list position位置的元素
swap交换两个list中的元素
clear清空list中的有效元素

注意事项

1、由于 list 的物理结构是非连续的 – 前一个节点地址和后一个节点地址的位置关系是随机的,所以 list 不支持随机访问,自然也就不支持 [] 操作;

2、list 不支持 reserve 操作,因为 list 的节点是使用时开辟,使用完销毁,不能预留空间;

2、list 特殊接口

除了上述 STL 容器基本都有的一般接口外,list 还提供一些独有的特殊操作接口,如下:

-函数声明-接口说明
splice将 list1 中的元素转移到 list2 中
remove移除 list 中的指定元素
unique链表去重
merge合并两个链表
sort链表排序
reverse链表逆置

注意事项

1、链表排序只能使用 list 提供的 sort 接口,而不能使用 algorithm 提供的 sort 接口,因为链表物理地址不连续,迭代器为双向迭代器,不支持 + - 操作,而算法库中的 sort 函数需要支持 + - 的随机迭代器;

2、链表去重之前必须保证链表有序,否则去重不完全;

3、两个有序链表合并之后仍然保存有序;

最后,虽然 list 提供了这些具有特殊功能的接口,它们也确实有一定的作用,但是实际上这些特殊接口使用频率非常低,包括 sort 接口 (链表排序的效率太低)。

3、list 排序的性能分析

虽然链表排序只能使用 list 提供的 sort 接口,而不能使用 algorithm 提供的 sort 接口,但是其使用频率仍然非常低,这是由于链表排序的效率太低了,我们可以通过对比两组测试数据来直观的感受链表排序的效率。

测试一:vector 排序与 list 排序性能对比

//vector sort 和 list sort 性能对比 -- release 版本下
void test_op1() {srand((size_t)time(0));const int N = 1000000;  //100万个数据vector<int> v;v.reserve(N);list<int> lt;for (int i = 0; i < N; ++i){auto e = rand();v.push_back(e);lt.push_back(e);}//vector sortint begin1 = clock();sort(v.begin(), v.end());int end1 = clock();//list sortint begin2 = clock();lt.sort();int end2 = clock();printf("vector sort:%d\n", end1 - begin1);printf("list sort:%d\n", end2 - begin2);
}

image-20230227185402161

测试二:list 直接进行排序与将数据拷贝到 vector 中使用 vector 排序后再将数据拷回 list 中性能对比

//list sort 与 将数据转移到 vector 中进行排序后拷贝回来性能对比 -- release 版本下
void test_op2()
{srand(time(0));const int N = 1000000;  //100万个数据list<int> lt1;list<int> lt2;for (int i = 0; i < N; ++i){auto e = rand();lt1.push_back(e);lt2.push_back(e);}//list sort -- lt1int begin1 = clock();lt1.sort();int end1 = clock();// 将数据拷贝到vector中排序,排完以后再拷贝回来 -- lt2int begin2 = clock();vector<int> v;v.reserve(N);for (auto e : lt2)  //拷贝{v.push_back(e);}sort(v.begin(), v.end());  //排序lt2.assign(v.begin(), v.end());  //拷贝int end2 = clock();printf("list1 sort:%d\n", end1 - begin1);printf("list2 sort:%d\n", end2 - begin2);
}

image-20230227191002915

可以看到,list sort 的效率远低于 vector sort,甚至于说,直接使用 list sort 的效率都不如先将数据拷贝到 vector 中,然后使用 vector sort,排序之后再将数据拷贝回 list 中快;至此,我们也能明白为什么 list sort 接口使用的非常少了。

注:在工程中,在 release 版本下测试软件或算法性能得到的结果要比在 debug 版本下得到的结果具有参考意义。


二、list 迭代器的实现

1、迭代器的分类

按照迭代器的功能,迭代器一共可以分为以下三类:

  • 单向迭代器 – 迭代器仅仅支持 ++ 和解引用操作,单链表的迭代器是典型的单向迭代器;
  • 双向迭代器 – 迭代器支持 ++、-- 和解引用操作,但不支持 +、- 操作,list (双向带头循环链表) 是典型的双向迭代器;
  • 随机迭代器 – 迭代器不仅支持 ++、-- 和解引用操作,还支持 +、- 操作,即迭代器能够随机访问,我们前面学习的 string 和 vector 的迭代器是典型的随机迭代器。

2、list 迭代器失效问题

和 vector 不同,list 进行 insert 操作后并不会产生迭代器失效问题,因为 list 插入的新节点是动态开辟的,同时由于 list 每个节点的物理地址是不相关的,所以插入的新节点并不会影响原来其他节点的地址;

但是 list erase 之后会发生迭代器失效,因为 list 删除节点会直接将该节点释放掉,此时我们再访问该节点就会造成越界访问。

3、list 迭代器源码分析

我们知道,迭代器是类似于指针一样的东西,即迭代器要能够实现指针相关的全部或部分操作 – ++、–、*、+、-;对我们之前 string 和 vector 的迭代器来说,迭代器就是原生指针,所以它天然的就支持上述操作;

但是对于 list 来说,list 的节点是一个结构体,同时 list 每个节点的物理地址是不连续的,如果此时我们还简单将节点的指针 typedef 为迭代器的话,那么显然它是不能够实现解引用、++ 等操作的,所以我们需要用结构体/类来对迭代器进行封装,再配合运算符重载等操作让迭代器能够实现解引用、++、-- 等操作

SGI 版本 C++ 源码中对 list 迭代器实现框架如下:

//节点定义
template <class T>
struct __list_node {typedef void* void_pointer;void_pointer next;void_pointer prev;T data;
};//迭代器定义
typedef __list_iterator<T, T&, T*>   iterator;
typedef __list_iterator<T, const T&, const T*>  const_iterator;//迭代器类
template<class T, class Ref, class Ptr>
struct __list_iterator {typedef __list_iterator<T, Ref, Ptr>  self;typedef __list_node<T>* link_type;  //节点的指针link_type node; //类成员变量__list_iterator(link_type x) : node(x) {} //将节点指针构造为类对象//... 使用运算符重载支持迭代器的各种行为self& operator++() {...}self& operator--() {...}Ref operator*() const {...}
};

如上,我们为迭代器专门设计了一个 __list_iterator 类,然后在类内配合运算符重载、模板等操作使得迭代器支持指针的各种行为 (至于这里为什么 __list_iterator 会有三个模板参数,我们会在下面模拟实现中具体解释,当前我们只需要理解其第一个模板参数 T 即可)。

4、list 迭代器模拟实现

4.1 普通迭代器

list 普通迭代器的实现比较简单,只需要一个模板参数 T 来代表数据类型,然后通过运算符重载来实现迭代器的各种操作即可:

//typedef __list_iterator<T> iterator -- 迭代器template<class T>
struct __list_iterator {typedef list_node<T> node;  //将list节点重命名为nodenode* _pnode;  //节点指针作为类的唯一成员变量__list_iterator(node* p)  //默认构造:_pnode(p){}T& operator*()  //解引用{return _pnode->_data;}__list_iterator<T>& operator++()  //前置++{_pnode = _pnode->_next;return *this;}__list_iterator<T>& operator++(int)  //后置++{__list_iterator<T> it(*this);_pnode = _pnode->_next;return it;}__list_iterator<T>& operator--()  //前置--{_pnode = _pnode->_prev;return *this;}__list_iterator<T>& operator--(int)  //后置--{__list_iterator<T> it(*this);_pnode = _pnode->_prev;return it;}bool operator!=(const __list_iterator<T>& it)  //!={return _pnode != it._pnode;}
};

4.2 const 迭代器

我们上面的迭代器是普通迭代器,那么如何实现 const 迭代器呢?const 迭代器与普通迭代器的区别在于 – const 迭代器不能修改节点中的数据,即 operator*() 函数的返回值应该是 const T&;

那么部分同学可能会这样来实现 const 迭代器,即在 __list_iterator 类中重载一个返回值为 const T& 的 operator*() 函数,如下:

//typedef __list_iterator<T> iterator -- 迭代器
//typedef const __list_iterator<T> const_iterator  -- const 迭代器
template<class T>
struct __list_iterator {T& operator*()  //解引用{return _pnode->_data;}const T& operator*() const{return _pnode->_data;}
};

但是这样显然是不行的,因为 const __list_iterator<T> const_iterator 中 const 修饰的是 const_iterator 本身,即限制 const_iterator 不能改变,这样会导致 const_iterator 不能进行 ++ 等操作,而并不会限制迭代器解引用后对节点数据的改变。

所以,我们应该为 const 迭代器设计一个单独的类 __list_const_iterator:

//typedef __list_iterator<T> iterator -- 迭代器
template<class T>
struct __list_iterator {typedef list_node<T> node;  //将list节点重命名为nodenode* _pnode;  //节点指针作为类的唯一成员变量__list_iterator(node* p)  //默认构造:_pnode(p){}T& operator*()  //解引用{return _pnode->_data;}__list_iterator<T>& operator++()  //前置++{_pnode = _pnode->_next;return *this;}__list_iterator<T>& operator++(int)  //后置++{__list_iterator<T> it(*this);_pnode = _pnode->_next;return it;}__list_iterator<T>& operator--()  //前置--{_pnode = _pnode->_prev;return *this;}__list_iterator<T>& operator--(int)  //后置--{__list_iterator<T> it(*this);_pnode = _pnode->_prev;return it;}bool operator!=(const __list_iterator<T>& it)  //!={return _pnode != it._pnode;}
};//typedef __list_const_iterator<T> const_iterator -- 迭代器
template<class T>
struct __list_const_iterator {typedef list_node<T> node;  //将list节点重命名为nodenode* _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++(int)  //后置++{__list_iterator<T> it(*this);_pnode = _pnode->_next;return it;}__list_const_iterator<T>& operator--()  //前置--{_pnode = _pnode->_prev;return *this;}__list_const_iterator<T>& operator--(int)  //后置--{__list_const_iterator<T> it(*this);_pnode = _pnode->_prev;return it;}bool operator!=(const __list_const_iterator<T>& it)  //!={return _pnode != it._pnode;}
};

可以看到,__list_iterator 类和 __list_const_iterator 类除了类名和 operator*() 函数的返回值不同以外,其他地方完全相同,这就会造成严重的代码冗余。

大佬显然是不会运行这种冗余存在的,所以想到了另一种办法来解决 const 迭代器的问题,那就是向 __list_iterator 中增加一个模板参数,如下:

//const 迭代器 -- 增加模板参数,解决 operator*()返回值问题
//typedef __list_iterator<T, T&> iterator;
//typedef __list_iterator<T, const T&> const_iterator;//STL源码中大佬的写法,利用多个模板参数来避免副本造成的代码冗余问题
template<class T, class Ref>  
struct __list_iterator  //迭代器类
{typedef list_node<T> node;  //重命名list节点typedef __list_iterator<T, Ref> Self;  //这里进行重命名是为了后续再添加模板参数时只用修改这一个地方node* _pnode;  //节点指针作为类的唯一成员变量__list_iterator(node* p):_pnode(p){}Ref operator*()  //const迭代器看这个{return _pnode->_data;}Self& operator++() //前置{_pnode = _pnode->_next;return *this;}Self& operator--() //前置{_pnode = _pnode->_prev;return *this;}bool operator!=(const Self& it){return _pnode != it._pnode;}
};

注意事项:对于普通类来说,类名 = 类型;对于模板类来说,类名 != 类型,类型 = 类名 + 模板参数 。(注意:在类内,不管是否存在模板,类名都等于类型,不过为了混淆我们不建议这样使用)

所以我们可以通过传递不同的模板参数来让编译器实例化出两个不同的类,对于上面的类来说表现如下:

//库使用者
list<int, T&>::iterator it;  //普通迭代器
list<int, const T&>::iterator cit;  //const 迭代器//template<class T, class Ref>
//通过编译器实例化出的两个不同的 __list_iterator 类
//类1、T:int Ref:T&
struct __list_iterato {T& operator*() { return _pnode->_data }
};//类2、T:int Ref:const T&
struct __list_iterato {const T& operator*() { return _pnode->_data }
};

总结 const 迭代器实现的两种方法

  • 为 const 迭代器单独写一个类,此类与原来的类只有类名和 operator*() 函数返回值不同,造成代码冗余;
  • 给迭代器类增加一个模板参数 Ref,让编译器根据传入的 Ref 的不同来自动示例化出 const 迭代器类。

4.3 完整版迭代器

从最初的迭代器源码中我们可以看到,源码中迭代器类有三个模板参数,下面,我们来引出这第三个参数。

假设 list 第一个模板参数 T 是一个自定义类型的类 Pos,其类的定义如下:

struct Pos
{int _row;int _col;Pos(int row = 0, int col = 0)  //构造:_row(row),_col(col){}
};

那么我们可以通过 list 迭代器以这样的方式来访问 Pos 中的成员变量:

list<Pos> lt(1,2);
list<Pos>::iterator it = lt.begin();
while(it != lt.end())
{cout <<(*it)._row<<":"<<(*it)._col<<endl;++it;
}

可以看到,我们需要先解引用得到 list 的节点,但由于此节点 Pos 是结构体类型,所以我们还需要通过 . 的方式来获取结构体成员;但是这样用非常别扭,因为迭代器是模拟指针的行为,而结构体指针访问数据的方式是 类名->变量名,那么我们能否像下面这样用呢?

cout <<it->_row<<":"<<it->_col<<endl;

显然,这样是不行的,因为 it 是一个自定义类型的对象 (__list_iterator 类的对象),而只有结构体指针才能像上面那样访问成员,所以我们需要利用运算符重载让 it 支持 -> 操作,如下:

template<class T, class Ref>
struct __list_iterator {T* operator->() { return &_pnode->_data; }  
};
cout <<it->_row<<":"<<it->_col<<endl;

相信绝大部分同学看到 &_pnode->_data 和 it->_row 的组合是懵逼的,这其实是因为 C++ 为了代码的可读性,省略了一个 ->,而原本的调用方式应该是这样的:

cout <<it->->_row<<":"<<it->->_col<<endl;  
//编译器编译时转化为:cout <<it.operator->()->_row<<":"<<it.operator->()->_col<<endl;

如上,由于 __list_iterator 对 -> 运算符进行了重载,所以编译器会将 it-> 转化为 it.operator->(),它得到的是节点数据的地址,也就是 Pos*,所以实际上 Pos* 还需要通过 -> 操作符来得到 _row 和 _col,但是 it->->_row 可读性太差,所以 C++ 省略了一个 ->,而让编译器对其进行处理 – it.operator->()->_row。

image-20230228193735451

但是此时这里又会和前面一样的问题,const 迭代器需要 operator->() 的返回值为 const T*,所以这里我们采取和前面一样的做法 – 再增加一个模板参数,把第三个模板参数作为 operator->() 函数的返回值,使得编译器可以根据传入的参数的不同实例化出不同的 __list_iterator 类

//库使用者
list<int, T&, T*>::iterator it;  //普通迭代器
list<int, const T&, const T*>::iterator cit;  //const 迭代器//template<class T, class Ref, class Ptr>
//通过编译器实例化出的两个不同的 __list_iterator 类
//类1、T:int Ref:T& Ptr
struct __list_iterato {T* operator->() { return &_pnode->_data }
};//类2、T:int Ref:const T& Ptr:const T*
struct __list_iterato {const T* operator->() { return &_pnode->_data }
};

至此,完整的迭代器模拟实现如下

//const 迭代器 -- 增加模板参数,解决 operator*() 返回值与 operator->() 返回值问题
//typedef __list_iterator<T, T&, T*> iterator;
//typedef __list_iterator<T, const T&, const T*> const_iterator;
//STL源码中大佬的写法,利用多个模板参数来避免副本造成的代码冗余问题
template<class T, class Ref, class Ptr>  
struct __list_iterator  //迭代器类
{typedef list_node<T> node;  //重命名list节点typedef __list_iterator<T, Ref, Ptr> Self;  //这里进行重命名是为了后续再添加模板参数时只用修改这一个地方node* _pnode;  //节点指针作为类的唯一成员变量__list_iterator(node* p)  //构造:_pnode(p){}Ref operator*()  //解引用{return _pnode->_data;}Ptr operator->()  //->{return &_pnode->_data;}Self& operator++() //前置++{_pnode = _pnode->_next;return *this;}Self& operator++(int) //后置++{Self it(*this);_pnode = _pnode->_next;return it;}Self& operator--() //前置--{_pnode = _pnode->_prev;return *this;}Self& operator--(int) //后置--{Self it(*this);_pnode = _pnode->_prev;return it;}bool operator!=(const Self& it) const //!={return _pnode != it._pnode;}bool operator==(const Self& it) const  //=={return _pnode == it._pnode;}
};

三、list 的模拟实现

list 模拟实现的最大难点在于 list 迭代器类的模拟实现,至于其他的一些成员函数,比如构造、赋值重载、析构等都比较简单,这里我就直接给出最终代码了,大家可以对照着自己模拟实现的代码看一看。

list.h

#pragma once#include <iostream>
#include <assert.h>
#include <algorithm>namespace thj {template<class T>struct list_node  //list 节点结构定义{list_node<T>* _next;//不加<T>也没错,但是写上好一些list_node<T>* _prev;T _data;list_node(const T& x)//构造:_next(nullptr), _prev(nullptr), _data(x){}};//迭代器最终版//const 迭代器 -- 增加模板参数,解决 operator*() 返回值与 operator->() 返回值问题//typedef __list_iterator<T, T&, T*> iterator;//typedef __list_iterator<T, const T&, const T*> const_iterator;//STL源码中大佬的写法,利用多个模板参数来避免副本造成的代码冗余问题template<class T, class Ref, class Ptr>struct __list_iterator  //迭代器类{typedef list_node<T> node;  //重命名list节点typedef __list_iterator<T, Ref, Ptr> Self;  //这里进行重命名是为了后续再添加模板参数时只用修改这一个地方node* _pnode;  //节点指针作为类的唯一成员变量__list_iterator(node* p):_pnode(p){}Ref operator*()  //解引用{return _pnode->_data;}Ptr operator->()  //->{return &_pnode->_data;}Self& operator++() //前置++{_pnode = _pnode->_next;return *this;}Self& operator++(int) //后置++{Self it(*this);_pnode = _pnode->_next;return it;}Self& operator--() //前置--{_pnode = _pnode->_prev;return *this;}Self& operator--(int) //后置--{Self it(*this);_pnode = _pnode->_prev;return it;}bool operator!=(const Self& it) const //!={return _pnode != it._pnode;}bool operator==(const Self& it) const  //=={return _pnode == it._pnode;}};//list 类template<class T>class list{typedef list_node<T> node;  //list 的节点public:typedef __list_iterator<T, T&, T*> iterator;  //迭代器typedef __list_iterator<T, const T&, const T*> const_iterator; //const 迭代器//迭代器iterator begin() {return iterator(_head->_next);}iterator end() {//iterator it(_head);//return it;//直接利用匿名对象更为便捷return iterator(_head);}const_iterator begin() const {return const_iterator(_head->_next);}const_iterator end() const {return const_iterator(_head);}void empty_initialize() {  //初始化 -- 哨兵位头结点_head = new node(T());_head->_next = _head;_head->_prev = _head;_size = 0;  //空间换时间,用于标记节点个数}list() {  //构造,不是list<T>的原因:构造函数函数名和类名相同,而list<T>是类型empty_initialize();}//迭代器区间构造template <class InputIterator>list(InputIterator first, InputIterator last) {empty_initialize();while (first != last){push_back(*first);++first;//first++;}}//拷贝构造传统写法//list(const list<T>& lt) {//	empty_initialize();//	for (const auto& e : lt)//	{//		push_back(e);//	}//}// 拷贝构造的现代写法//list(const list& lt) 官方库是这样写的,这是由于在类内类名等价于类型,但不建议自己这样写list(const list<T>& lt) {empty_initialize();  //初始化头结点,防止交换后tmp野指针不能正常的调用析构list<T> tmp(lt.begin(), lt.end());swap(tmp);}//赋值重载传统写法//list<T>& operator=(const list<T>& lt) {//	if (this != &lt)//	{//		clear();//		for (const auto& e : lt)//		{//			push_back(e);//		}//	}//	return *this;//}//赋值重载现代写法//list& operator=(list lt)list<T>& operator=(list<T> lt) {  //不能加引用,lt是调用拷贝构造生成的swap(lt);return *this;}~list() {  //析构clear();delete _head;_head = nullptr;}void swap(list<T>& lt) {  //交换两个链表,本质上是交换两个链表的头结点std::swap(_head, lt._head);std::swap(_size, lt._size);}size_t size() const {  //增加一个计数的成员,以空间换时间return _size;}bool empty() {  //判空return _size == 0;}void clear() {iterator it = begin();while (it != end()) {it = erase(it);}_size = 0;}void push_back(const T& x) {//node* newnode = new node(x);//node* tail = _head->_prev;//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_front() {erase(begin());}void pop_back() {erase(--end());}iterator insert(iterator pos, const T& x) {node* newnode = new node(x);node* cur = pos._pnode;node* prev = cur->_prev;prev->_next = newnode;newnode->_prev = prev;cur->_prev = newnode;newnode->_next = cur;++_size;return iterator(pos);}iterator erase(iterator pos) {assert(pos != end());node* prev = pos._pnode->_prev;node* next = pos._pnode->_next;prev->_next = next;next->_prev = prev;delete pos._pnode;--_size;return iterator(next);}private:node* _head;size_t _size;};
}

test.cpp

#include "list.h"using namespace thj;
using std::cout;
using std::endl;void test_list1()
{list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);lt.insert(++lt.begin(), 10);//iterator 1、内嵌类型 2、像指针一样list<int>::iterator it = lt.begin();while (it != lt.end()){cout << *it << " ";++it;}cout << endl;for (auto e : lt){cout << e << " ";}cout << endl;
}void test_list2() {list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);lt.push_front(10);lt.push_front(20);lt.push_front(30);lt.pop_back();lt.pop_back();lt.pop_back();lt.pop_front();lt.pop_front();for (auto e : lt){cout << e << " ";}cout << endl;
}void test_list3() {list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);for (auto e : lt){cout << e << " ";}cout << endl;list<int> lt1(lt);//默认是浅拷贝,指向同一块lt.pop_back();lt.pop_back();for (auto e : lt1){cout << e << " ";}cout << endl;list<int> lt3 = lt1;for (auto e : lt3){cout << e << " ";}
}void test_list4() {list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);list<int>::iterator it = lt.begin();while (it != lt.end()){(*it) += 2;cout << *it << " ";++it;//it++;}cout << endl;//print_list(lt);list<int> lt1(lt);for (auto e : lt1){cout << e << " ";}cout << endl;list<int> lt2 = lt;for (auto e : lt2){cout << e << " ";}cout << endl;cout << lt.size() << endl;
}struct Pos {int _row;int _col;Pos(int row = 0, int col = 0):_row(row), _col(col){}
};void print_list(const list<Pos>& lt) {list<Pos>::const_iterator it = lt.begin(); //重载了,找的另一个类的迭代器while (it != lt.end()) {//it->_row++;  增加第三个模板参数cout << it->_row << ":" << it->_col << endl;++it;}cout << endl;
}void test_list5() {list<Pos> lt;Pos p1(1, 1);lt.push_back(p1);lt.push_back(p1);lt.push_back(p1);lt.push_back(Pos(2, 3));lt.push_back(Pos(2, 3));list<Pos>::iterator it = lt.begin();while (it != lt.end()) {//cout << (*it)._row << ":" << (*it)._col << endl;cout << it->_row << ":" << it->_col << endl;  //编译器为了可读性,做了特殊处理,省略了一个->,//那么省略之前应该是it->->_row//cout << it.operator->()->_col << ":" << it.operator->()->_row << endl;++it;}cout << endl;print_list(lt);
}int main() {//test_list1();//test_list2();//test_list3();//test_list4();test_list5();return 0;
}

四、vector 和 list 的区别

vector 和 list 的区别其实就是顺序表和链表的区别,但是由于相较于数据结构初阶我们又增添了 C++ 的相关知识,所以这里我还是重新列举一下二者的异同与优缺:

--vector-list
底层结构动态顺序表,一段连续空间带头结点的双向循环链表
随机访问支持随机访问,访问某个元素效率 O(1)不支持随机访问,访问某个元素效率 O(N)
插入和删除任意位置插入和删除效率低,需要搬移元素,时间复杂度为 O(N),插入时有可能需要增容,增容:开辟新空间,拷贝元素,释放旧空间,导致效率更低任意位置插入和删除效率高,不需要搬移元素,时间复杂度为 O(1)
空间利用率底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高底层节点动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低
迭代器原生态指针对原生态指针 (节点指针) 进行封装
迭代器失效在插入元素时,要给所有的迭代器重新赋值,因为插入元素有可能会导致重新扩容,致使原来迭代器失效,删除时,当前迭代器需要重新赋值否则会失效插入元素不会导致迭代器失效,删除元素时,只会导致当前迭代器失效,其他迭代器不受影响
使用场景需要高效存储,支持随机访问,不关心插入删除效率大量插入和删除操作,不关心随机访问

相关文章:

【C++】STL 模拟实现之 list

文章目录一、list 的常用接口及其使用1、list 一般接口2、list 特殊接口3、list 排序的性能分析二、list 迭代器的实现1、迭代器的分类2、list 迭代器失效问题3、list 迭代器源码分析4、list 迭代器模拟实现4.1 普通迭代器4.2 const 迭代器4.3 完整版迭代器三、list 的模拟实现…...

20230228----重返学习-数组-引用数据类型的转换-基础调试用方法-对象检测-各数据转布尔值及相等运算符-条件语句-循环语句

day-017-seventeen-20230228-数组-引用数据类型的转换-基础调试用方法-对象检测-各数据转布尔值及相等运算符-条件语句-循环语句 数组 字面量表示法 [数组成员0,数组成员1,数组成员2]用中括号语法来取值 var ary [5,6,7] console.log("ary[0]--->", ary[0])数组…...

apscheduler 定时任务框架

Apscheduler 介绍 四大组件 triggers&#xff1a;触发器&#xff0c;用于设定触发任务的条件job stores&#xff1a;作业存储器&#xff0c;用于存放任务&#xff0c;可以存放在数据库或内存&#xff0c;默认内存executors&#xff1a;执行器&#xff0c;用于执行任务&#x…...

Softing OPC Tunnel——绕过DCOM配置实现OPC Classic广域网通信

一 摘要 Softing OPC Tunnel是dataFEED OPC Suite的一个组件&#xff0c;可避免跨设备OPC Classic通信中出现的DCOM配置问题&#xff0c;同时可保证跨网络数据交换的高性能和可靠性。OPC Tunnel内部集成的存储转发功能&#xff0c;可在连接中断时缓存数据&#xff0c;并在重新…...

Java的运算操作

个人主页&#xff1a;平行线也会相交 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 平行线也会相交 原创 收录于专栏【JavaSE_primary】 文章目录算术运算符增量运算符注意自增自减运算符关系运算符逻辑运算符逻辑与&&逻辑或||逻辑非&#xff01;…...

基于OBD系统的量产车评估测试(PVE)

在轻型汽车污染物排放限值及测量方法&#xff08;中国第六阶段&#xff09;中&#xff0c;除了对汽车尾气排放等制定了更为严格的限制之外&#xff0c;也在OBD系统认证项目中增加了新的要求——量产车评估&#xff08;Production Vehicle Evaluation&#xff09;测试。该测试由…...

【蓝桥杯集训10】Tire树 字典树 最大异或对专题(3 / 3)

目录 字典树模板 1、插入操作 2、查询操作 143. 最大异或对 - trie 二进制 3485. 最大异或和 - 前缀和Trie滑动窗口 字典树模板 活动 - AcWing 字典树&#xff1a;高效存储和查找字符串集合的数据结构 son[节点1地址][值]节点2地址 —— 节点1的子节点为节点2cnt[节点地…...

docker部署zabbix6.2.7+grafana

目录 1、下载docker 2、下载相关镜像文件 3、创建一个供zabbix系统使用的网络环境 4、创建一个供mysql数据库存放文件的目录 5、启动mysql容器 6、为zabbix-server创建一个持久卷 7、启动zabbix-server容器 8、创建语言存放目录 9、启动zabbix-web容器 10、启动zabbix…...

【Java开发】JUC基础 04:Synchronized、死锁、Lock锁

1 概念介绍并发&#xff1a;同一个对象被多个线程同时操作&#x1f4cc; 线程同步现实生活中&#xff0c;我们会遇到“同一个资源&#xff0c;多个人都想使用”的问题&#xff0c;比如&#xff0c;食堂排队打饭,每个人都想吃饭&#xff0c;最天然的解决办法就是&#xff0c;排队…...

离散数学---期末复习知识点

一、 数理逻辑 [复习知识点] 1、命题与联结词&#xff08;否定&#xffe2;、析取∨、合取∧、蕴涵→、等价↔&#xff09;&#xff0c;命题(非真既假的陈述句),复合命题(由简单命题通过联结词联结而成的命题) 2、命题公式与赋值&#xff08;成真、成假&#xff09;&#x…...

在线安装ESP32和ESP8266 Arduino开发环境

esp32和esp8266都是乐鑫科技开发的单片机产品&#xff0c;esp8266价格便宜开发板只需要十多块钱就可以买到&#xff0c;而esp32是esp8266的升级版本&#xff0c;比esp8266的功能和性能更强大&#xff0c;开发板价格大约二十多元就可以买到。 使用Arduino开发esp32和esp8266需要…...

【Python实战】激情澎湃,2023极品劲爆舞曲震撼全场,爬虫一键采集DJ大串烧,一曲醉人女声DJ舞曲,人人都听醉~(排行榜采集,妙啊~)

导语 哈喽&#xff01;大家好。我是木木子吖~今天给大家带来爬虫的内容哈。 所有文章完整的素材源码都在&#x1f447;&#x1f447; 粉丝白嫖源码福利&#xff0c;请移步至CSDN社区或文末公众hao即可免费。 今天教大家Python爬虫实战一键采集大家喜欢的DJ舞曲哦&#xff01; …...

[SSD综述 1.5] SSD固态硬盘参数图文解析_选购固态硬盘就像买衣服?

版权声明&#xff1a;付费作品&#xff0c;未经许可&#xff0c;不可转载前言SSD &#xff08;Solid State Drive&#xff09;&#xff0c;即固态硬盘&#xff0c;通常是一种以半导体闪存&#xff08;NAND Flash&#xff09;作为介质的存储设备。SSD 以半导体作为介质存储数据&…...

SAP Insurance Analyzer

SAP Insurance Analyzer 是一款用于保险公司财务和风险管理的软件。SAP Insurance analyzer 支持基于 IFRS 17 或 Solvency II 的保险合同估值和计算要求。SAP Insurance Analyzer 于 2013 年 5 月推出&#xff0c;为源数据和结果数据集成了一个预配置的保险数据模型。 源数据…...

自动化测试 ——自动卸载软件

在平常的测试工作中&#xff0c;经常要安装软件&#xff0c;卸载软件, 即繁琐又累。 安装和卸载完全可以做成自动化。 安装软件我们可以通过自动化框架&#xff0c;自动点击Next,来自动安装。 卸载软件我们可以通过msiexec命令行工具自动化卸载软件 用msiexec 命令来卸载软件 …...

05 封装

在对 context 的封装中&#xff0c;我们只是将 request、response 结构直接放入 context 结构体中&#xff0c;对应的方法并没有很好的封装。 函数封装并不是一件很简单、很随意的事情。相反&#xff0c;如何封装出易用、可读性高的函数是非常需要精心考量的&#xff0c;框架中…...

clean

clean code 记得以前写过这题&#xff0c;写的乱七八糟&#xff0c;分析来分析去。 后悔应该早点写代码&#xff0c;leetcode大一就该刷了。 https://leetcode.cn/problems/plus-one/submissions/ class Solution { public:vector<int> plusOne(vector<int>&…...

佛科院计算机软件技术基础——线性表

一、基础知识了解&#xff1a;结构体的理解&#xff1a;我们知道整型是由1位符号位和15位数值位组成&#xff0c;而就可以把结构体理解为我们定义的数据类型&#xff0c;如&#xff1a;typedef struct {int data[2]; //存储顺序表中的元素int len; …...

linux下终端操作mysql数据库

目录 一&#xff0e;检查mysql是否安装 1. 查看文件安装路径 2. 查询运行文件所在路径(文件夹地址) 二&#xff0e;登录mysql 三&#xff0e;列出mysql全部用户 四&#xff0e;常用指令 &#xff11;&#xff0e;查看全部数据库 &#xff12;&#xff0e;选择数据库 …...

MySQL参数优化之thread_cache_size

1.thread_cache_size简介 每建立一个连接&#xff0c;都需要一个线程来与之匹配&#xff0c;此参数用来缓存空闲的线程&#xff0c;以至不被销毁&#xff0c;如果线程缓存中有空闲线程&#xff0c;这时候如果建立新连接&#xff0c;MYSQL就会很快的响应连接请求。 show statu…...

Leetcode 3576. Transform Array to All Equal Elements

Leetcode 3576. Transform Array to All Equal Elements 1. 解题思路2. 代码实现 题目链接&#xff1a;3576. Transform Array to All Equal Elements 1. 解题思路 这一题思路上就是分别考察一下是否能将其转化为全1或者全-1数组即可。 至于每一种情况是否可以达到&#xf…...

(十)学生端搭建

本次旨在将之前的已完成的部分功能进行拼装到学生端&#xff0c;同时完善学生端的构建。本次工作主要包括&#xff1a; 1.学生端整体界面布局 2.模拟考场与部分个人画像流程的串联 3.整体学生端逻辑 一、学生端 在主界面可以选择自己的用户角色 选择学生则进入学生登录界面…...

R语言AI模型部署方案:精准离线运行详解

R语言AI模型部署方案:精准离线运行详解 一、项目概述 本文将构建一个完整的R语言AI部署解决方案,实现鸢尾花分类模型的训练、保存、离线部署和预测功能。核心特点: 100%离线运行能力自包含环境依赖生产级错误处理跨平台兼容性模型版本管理# 文件结构说明 Iris_AI_Deployme…...

day52 ResNet18 CBAM

在深度学习的旅程中&#xff0c;我们不断探索如何提升模型的性能。今天&#xff0c;我将分享我在 ResNet18 模型中插入 CBAM&#xff08;Convolutional Block Attention Module&#xff09;模块&#xff0c;并采用分阶段微调策略的实践过程。通过这个过程&#xff0c;我不仅提升…...

页面渲染流程与性能优化

页面渲染流程与性能优化详解&#xff08;完整版&#xff09; 一、现代浏览器渲染流程&#xff08;详细说明&#xff09; 1. 构建DOM树 浏览器接收到HTML文档后&#xff0c;会逐步解析并构建DOM&#xff08;Document Object Model&#xff09;树。具体过程如下&#xff1a; (…...

ESP32 I2S音频总线学习笔记(四): INMP441采集音频并实时播放

简介 前面两期文章我们介绍了I2S的读取和写入&#xff0c;一个是通过INMP441麦克风模块采集音频&#xff0c;一个是通过PCM5102A模块播放音频&#xff0c;那如果我们将两者结合起来&#xff0c;将麦克风采集到的音频通过PCM5102A播放&#xff0c;是不是就可以做一个扩音器了呢…...

【git】把本地更改提交远程新分支feature_g

创建并切换新分支 git checkout -b feature_g 添加并提交更改 git add . git commit -m “实现图片上传功能” 推送到远程 git push -u origin feature_g...

SpringTask-03.入门案例

一.入门案例 启动类&#xff1a; package com.sky;import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCach…...

2025季度云服务器排行榜

在全球云服务器市场&#xff0c;各厂商的排名和地位并非一成不变&#xff0c;而是由其独特的优势、战略布局和市场适应性共同决定的。以下是根据2025年市场趋势&#xff0c;对主要云服务器厂商在排行榜中占据重要位置的原因和优势进行深度分析&#xff1a; 一、全球“三巨头”…...

uniapp 字符包含的相关方法

在uniapp中&#xff0c;如果你想检查一个字符串是否包含另一个子字符串&#xff0c;你可以使用JavaScript中的includes()方法或者indexOf()方法。这两种方法都可以达到目的&#xff0c;但它们在处理方式和返回值上有所不同。 使用includes()方法 includes()方法用于判断一个字…...