【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);
}
测试二: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);
}
可以看到,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。
但是此时这里又会和前面一样的问题,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 != <)// {// 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:触发器,用于设定触发任务的条件job stores:作业存储器,用于存放任务,可以存放在数据库或内存,默认内存executors:执行器,用于执行任务&#x…...

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

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

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

【蓝桥杯集训10】Tire树 字典树 最大异或对专题(3 / 3)
目录 字典树模板 1、插入操作 2、查询操作 143. 最大异或对 - trie 二进制 3485. 最大异或和 - 前缀和Trie滑动窗口 字典树模板 活动 - AcWing 字典树:高效存储和查找字符串集合的数据结构 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 概念介绍并发:同一个对象被多个线程同时操作📌 线程同步现实生活中,我们会遇到“同一个资源,多个人都想使用”的问题,比如,食堂排队打饭,每个人都想吃饭,最天然的解决办法就是,排队…...

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

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

【Python实战】激情澎湃,2023极品劲爆舞曲震撼全场,爬虫一键采集DJ大串烧,一曲醉人女声DJ舞曲,人人都听醉~(排行榜采集,妙啊~)
导语 哈喽!大家好。我是木木子吖~今天给大家带来爬虫的内容哈。 所有文章完整的素材源码都在👇👇 粉丝白嫖源码福利,请移步至CSDN社区或文末公众hao即可免费。 今天教大家Python爬虫实战一键采集大家喜欢的DJ舞曲哦! …...

[SSD综述 1.5] SSD固态硬盘参数图文解析_选购固态硬盘就像买衣服?
版权声明:付费作品,未经许可,不可转载前言SSD (Solid State Drive),即固态硬盘,通常是一种以半导体闪存(NAND Flash)作为介质的存储设备。SSD 以半导体作为介质存储数据&…...

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

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

05 封装
在对 context 的封装中,我们只是将 request、response 结构直接放入 context 结构体中,对应的方法并没有很好的封装。 函数封装并不是一件很简单、很随意的事情。相反,如何封装出易用、可读性高的函数是非常需要精心考量的,框架中…...
clean
clean code 记得以前写过这题,写的乱七八糟,分析来分析去。 后悔应该早点写代码,leetcode大一就该刷了。 https://leetcode.cn/problems/plus-one/submissions/ class Solution { public:vector<int> plusOne(vector<int>&…...

佛科院计算机软件技术基础——线性表
一、基础知识了解:结构体的理解:我们知道整型是由1位符号位和15位数值位组成,而就可以把结构体理解为我们定义的数据类型,如:typedef struct {int data[2]; //存储顺序表中的元素int len; …...
linux下终端操作mysql数据库
目录 一.检查mysql是否安装 1. 查看文件安装路径 2. 查询运行文件所在路径(文件夹地址) 二.登录mysql 三.列出mysql全部用户 四.常用指令 1.查看全部数据库 2.选择数据库 …...
MySQL参数优化之thread_cache_size
1.thread_cache_size简介 每建立一个连接,都需要一个线程来与之匹配,此参数用来缓存空闲的线程,以至不被销毁,如果线程缓存中有空闲线程,这时候如果建立新连接,MYSQL就会很快的响应连接请求。 show statu…...

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?
编辑:陈萍萍的公主一点人工一点智能 未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战,在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…...

日语AI面试高效通关秘籍:专业解读与青柚面试智能助攻
在如今就业市场竞争日益激烈的背景下,越来越多的求职者将目光投向了日本及中日双语岗位。但是,一场日语面试往往让许多人感到步履维艰。你是否也曾因为面试官抛出的“刁钻问题”而心生畏惧?面对生疏的日语交流环境,即便提前恶补了…...
【位运算】消失的两个数字(hard)
消失的两个数字(hard) 题⽬描述:解法(位运算):Java 算法代码:更简便代码 题⽬链接:⾯试题 17.19. 消失的两个数字 题⽬描述: 给定⼀个数组,包含从 1 到 N 所有…...
linux 下常用变更-8
1、删除普通用户 查询用户初始UID和GIDls -l /home/ ###家目录中查看UID cat /etc/group ###此文件查看GID删除用户1.编辑文件 /etc/passwd 找到对应的行,YW343:x:0:0::/home/YW343:/bin/bash 2.将标红的位置修改为用户对应初始UID和GID: YW3…...
【服务器压力测试】本地PC电脑作为服务器运行时出现卡顿和资源紧张(Windows/Linux)
要让本地PC电脑作为服务器运行时出现卡顿和资源紧张的情况,可以通过以下几种方式模拟或触发: 1. 增加CPU负载 运行大量计算密集型任务,例如: 使用多线程循环执行复杂计算(如数学运算、加密解密等)。运行图…...
工业自动化时代的精准装配革新:迁移科技3D视觉系统如何重塑机器人定位装配
AI3D视觉的工业赋能者 迁移科技成立于2017年,作为行业领先的3D工业相机及视觉系统供应商,累计完成数亿元融资。其核心技术覆盖硬件设计、算法优化及软件集成,通过稳定、易用、高回报的AI3D视觉系统,为汽车、新能源、金属制造等行…...

云原生玩法三问:构建自定义开发环境
云原生玩法三问:构建自定义开发环境 引言 临时运维一个古董项目,无文档,无环境,无交接人,俗称三无。 运行设备的环境老,本地环境版本高,ssh不过去。正好最近对 腾讯出品的云原生 cnb 感兴趣&…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...
在鸿蒙HarmonyOS 5中使用DevEco Studio实现企业微信功能
1. 开发环境准备 安装DevEco Studio 3.1: 从华为开发者官网下载最新版DevEco Studio安装HarmonyOS 5.0 SDK 项目配置: // module.json5 {"module": {"requestPermissions": [{"name": "ohos.permis…...
Bean 作用域有哪些?如何答出技术深度?
导语: Spring 面试绕不开 Bean 的作用域问题,这是面试官考察候选人对 Spring 框架理解深度的常见方式。本文将围绕“Spring 中的 Bean 作用域”展开,结合典型面试题及实战场景,帮你厘清重点,打破模板式回答,…...