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

【C++】list的使用及底层逻辑实现

目录

一 list介绍及使用

1 list介绍

2 list使用

(1)list的构造

(2)list iterator的使用

迭代器从功能角度的分类:

(3)list的sort

二 list 和vector的核心区别

三 list的底层逻辑及部分源代码

四 自己实现List

(1)push_back

(2)iterator

(3)insert

(4)erase

(5)size

(6)头插头删 尾插尾删

(7)链表核心框架实现

 a 链表类及核心类型定义

b 迭代器接口

c 初始化函数

d 构造函数(多种初始化方式)

e 析构函数

f 拷贝控制 (深拷贝)

g 私有成员变量

(8)operator** 和operator->


一 list介绍及使用

1 list介绍

在 C++ 中,std::list 是标准模板库(STL)提供的双向链表容器,其底层实现基于双向链表数据结构,每个元素(节点)包含数据域和两个指针域(分别指向前后节点)。

可以将list理解为:带头双向循环链表

list文档介绍:http://www.cplusplus.com/reference/list/list/?kw=list

2 list使用

list中的接口比较多,和string,vector中的类似,只需要掌握如何正确的使用,然后再去深入研究背后的原理,已达到可扩展的能力。以下为list中一些常见的重要接口。

(1)list的构造
构造函数(constructor)接口说明
vector (size_type n, const value_type& val = value_type())构造的 vector 中包含 n 个值为 val 的元素
vector()构造空的 vector
vector (const vector& x)拷贝构造函数
vector (InputIterator first, InputIterator last)用 [first, last) 区间中的元素构造 vector
(2)list iterator的使用
迭代器从功能角度的分类:

迭代器分为:单向迭代器(+ + )  双向迭代器(++ --)  随机迭代器(++  -- + -)

是由容器的底层结构决定

迭代器类型核心功能限制典型容器无法支持的操作 / 算法示例
输入迭代器只读、单向移动istream_iterator写入操作(*it = val)、reverse
输出迭代器只写、单向移动、不可重复写ostream_iterator读取操作(val = *it)、find
前向迭代器不可反向移动(无 --forward_list反向遍历(--it)、rbegin()
双向迭代器不可随机访问(无 it + nlistmap下标访问([])、std::sort
随机访问迭代器无核心限制vectorarray无(兼容所有弱迭代器场景)

(3)list的sort

list不支持算法库里的sort(核心是快速排序),因为sort对迭代器的功能有要求,必须是随机迭代器,而list是双向迭代器

debug不能作为判断性能的标准,尤其是判断递归

虽然list自己实现了一个sort,核心是归并排序,但是不建议使用,因为效率太差

在STL中,vector的sort随机访问迭代器下的快速排序(平均时间复杂度O(nlogn)),而list的sort双向迭代器下的归并排序(稳定时间复杂度为O(nlogn))数据量大时不建议用list的sort


二 list 和vector的核心区别

list的核心缺陷是没有办法做下标随机访问、

对比维度vectorlist
底层数据结构动态数组(连续内存空间)双向链表(非连续内存,节点含前后指针)
随机访问支持支持(通过 [] 或 at(),时间复杂度 O(1))不支持(需迭代器顺序遍历,时间复杂度 O(n))
插入 / 删除效率尾部操作高效(O(1));中间 / 头部操作需移动元素(O(n))任意位置操作高效(仅修改指针,O(1))
内存分配容量不足时重新分配更大连续空间(可能触发元素复制)每个节点单独分配 / 释放内存,无整体复制开销
内存利用率连续空间,缓存友好,但可能存在预留空间浪费非连续空间,节点含指针额外开销(内存利用率较低)
迭代器稳定性插入 / 删除中间元素后,该位置后的迭代器失效插入 / 删除元素后,只有被删除节点的迭代器失效
适用场景频繁随机访问、尾部增删为主的场景频繁在任意位置插入 / 删除、对随机访问需求低的场景

但其实,vector和list是互补的关系,如果需要大量的存储数据,尽量选择用vector去存储数据,因为vector是连续的存储数据,在读取数据的的时候可以快速的读取。  如果需要头插,头删,建议使用list


三 list的底层逻辑及部分源代码

我们在学习源代码的时候:要学会抽丝剥茧,抓住核心,去掉不重要的部分

如果List的成员变量是vecor之类的,那么在销毁时,不仅要调用list的析构函数,还要调用vector的析构函数


四 自己实现List

(1)push_back

习惯上来说,如果类不想让访问限定符限制,就使用struct,例如下面的list_node就不想限制,因为list_node是作为链表的一个子结构,是存储每个数据的最小单元,而链表是需要大量访问数据的,就不需要访问限定符的限制,使用struct更好

这个时候就有人问了,那这样设定不就可以随便访问了吗?   但是我们有迭代器,只能从内部看出节点,外部无法看出。而且不同的平台list_node的名称也不同。

我们先来写定义和初始化部分:
 

#pragma oncenamespace bit
{template<class T>struct list_node{list_node<T>* _next;list_node<T>* _prev;T _data;list_node(const T& x = T()):_next(nullptr),_prev(nullptr),_data(x){}};
}

然后来写push_back

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;
}

tail是尾节点

(2)iterator

迭代器的核心使用就是解引用,找到指向的数据,在不暴露底层的情况下去访问你的数据,不管是链表,还是之后的树型结构,都是一样的访问方式,它是一种封装

迭代器的设计是一种封装,封装隐藏底层的结构差异,提供类似统一的方式访问容器

我们在写这部分的完整代码时,用了三个:类一个类封装链表,一个类封装节点。一个类封装迭代器。完整代码将会在结尾展出

在源代码的部分

类里封装了一个节点的指针,然后重载运算符,这个时候迭代器就是这个类。*it就会调用*operator,*operator中含有节点指针指向的数据,所以迭代器解引用就会访问这个指针。

++it就会调用operator++,指向当前节点的下一个地址。

为什么要封装呢?

通过类的成员函数(如重载的 operator*operator++ 等),可以为迭代器提供统一、简洁的接口。不管底层容器(如 list)的实现多么复杂,用户都可以用相同的方式(如 *it 访问元素、++it 移动迭代器)来操作不同容器的迭代器  通过统一的方式访问容器,不用管底层是怎么样的

那我们来自己实现一下:

​
//实现双向迭代器,支持前向和后向遍历
template<class T, class Ref>struct list_iterator{using Self = list_iterator<T, Ref>;using Node = list_node<T>;Node* _node;list_iterator(Node* node):_node(node){}// *it = 1Ref operator*(){return _node->_data;}// ++itSelf& operator++(){_node = _node->_next;return *this;}Self operator++(int){Self tmp(*this);_node = _node->_next;return tmp;}// --itSelf& operator--(){_node = _node->_prev;return *this;}Self operator--(int){Self tmp(*this);_node = _node->_prev;return tmp;}bool operator!=(const Self& s) const{return _node != s._node;}bool operator==(const Self& s) const{return _node == s._node;}};​

using Self = list_iterator<T, Ref>:简化自身类型的使用(避免重复写

void Print(const bit::list<int>& lt)
{bit::list<int>::const_iterator it = lt.begin();while (it != lt.end()){//*it = 1;cout << *it << " ";++it;}cout << endl;
}

void test_list1()
{bit::list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);bit::list<int>::iterator it = lt.begin();while (it != lt.end()){cout << *it << " ";++it;}cout << endl;for (auto e : lt){cout << e << " ";}cout << endl;
}

支持迭代器就支持范围for(范围for的底层就是迭代器)

我们来看右上角:

Node* it1和 bit::list<int>::iterator it2都保存了1这个节点,但是因为类型不一样,所以解引用后的使用也不一样。

迭代器是借助链表的指针,去访问链表的数据,但是迭代器销毁以后不会销毁节点,因为节点是归链表管,迭代器销毁了节点还在,所以迭代器不会实现析构函数

(3)insert

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

(4)erase

iterator erase(iterator 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 函数中返回 next(被删除节点的下一个节点对应的迭代器),是为了保证迭代器的有效性,避免用户使用已失效的迭代器

(5)size

size_t size() const{/*size_t n = 0;for (auto& e : *this){++n;}return n;*/return _size;}private:Node* _head;size_t _size = 0;};

这段代码实现了链表的 size 成员函数,用于获取链表中元素的个数,同时定义了链表类的私有成员变量。核心设计思路是通过维护一个 _size 变量,避免每次获取大小都遍历链表,从而提升效率

这段代码实现了两种方法:
 

实现方式核心逻辑时间复杂度优缺点
注释版(遍历计数)通过范围 for 循环遍历链表,每访问一个元素就将计数器 n 加 1,最终返回 nO (N)(N 为链表元素个数)优点:无需额外维护变量,逻辑直观;缺点:每次调用 size 都要遍历整个链表,元素越多效率越低。
保留版(直接返回 _size直接返回私有成员变量 _size 的值O (1)(常数时间)优点:无论链表有多少元素,都能瞬间返回结果,效率极高;缺点:需要在链表的增删操作(如 push_backinserterase)中手动维护 _size 的值(确保增删时同步 ++_size 或 --_size)。

因为遍历计数比较麻烦,所以我们可以直接在私有成员变量中添加一个size变量

(6)头插头删 尾插尾删

因为我们已经完成了insert的函数,所以我们可以利用函数的复用降低函数的代码长度

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

(7)链表核心框架实现

 a 链表类及核心类型定义
template<class T>
class list
{// 节点类型定义(链表的基本存储单元)using Node = list_node<T>;  // 假设list_node是已定义的节点结构体(含_data, _prev, _next)public:// 迭代器类型定义(通过模板参数控制读写权限)using iterator = list_iterator<T, T&>;         // 可读写迭代器(解引用返回T&)using const_iterator = list_iterator<T, const T&>;  // 只读迭代器(解引用返回const T&)// 注释:另一种迭代器实现思路(通过两个独立类)// using iterator = list_iterator<T>;// using const_iterator = list_const_iterator<T>;
b 迭代器接口
    // 获取非const迭代器(指向第一个元素)iterator begin(){// 头节点的_next是第一个数据节点return iterator(_head->_next);}// 获取非const迭代器(指向末尾标记,即头节点)iterator end(){// 尾后迭代器指向头节点(符合[begin, end)左闭右开区间)return iterator(_head);}// 获取const迭代器(指向第一个元素,只读)const_iterator begin() const{return const_iterator(_head->_next);}// 获取const迭代器(指向末尾标记,只读)const_iterator end() const{return const_iterator(_head);}

const迭代器不是迭代器不能修改,而是指向的内容不能修改,注意const的位置,不要写错

const迭代器和普通迭代器的区别是:const迭代器不能修改---->核心在于修改是通过解引用,*it调用operator*,而operator*返回const T&,就不能修改了

c 初始化函数
private:// 初始化空链表(创建哨兵头节点,形成双向循环)void empty_init(){_head = new Node;  // 创建头节点(不存储实际数据,仅作哨兵)_head->_next = _head;  // 头节点的_next指向自身_head->_prev = _head;  // 头节点的_prev指向自身(循环结构)}public:
d 构造函数(多种初始化方式)
    // 默认构造函数(初始化空链表)list(){empty_init();}// 初始化列表构造(支持list<int> l = {1,2,3})list(initializer_list<T> il){empty_init();  // 先初始化空链表// 遍历初始化列表,逐个插入元素for (auto& e : il){push_back(e);  // 假设push_back已实现(尾插)}}// 迭代器区间构造(从其他容器迭代器区间初始化)template <class InputIterator>list(InputIterator first, InputIterator last){empty_init();  // 先初始化空链表// 遍历[first, last)区间,逐个插入元素while (first != last){push_back(*first);  // 插入当前元素++first;  // 移动到下一个元素}}// 构造n个值为val的元素(size_t版本,避免类型歧义)list(size_t n, T val = T()){empty_init();for (size_t i = 0; i < n; ++i){push_back(val);}}// 构造n个值为val的元素(int版本,与size_t重载区分)list(int n, T val = T()){empty_init();for (int i = 0; i < n; ++i){push_back(val);}}
e 析构函数
~list(){clear();  // 清空所有数据节点(假设clear已实现)delete _head;  // 释放头节点_head = nullptr;  // 避免野指针_size = 0;  // 重置大小}
f 拷贝控制 (深拷贝)

第一个是现代写法,第二个是传统写法

    // 拷贝构造函数(深拷贝,从另一个list复制)list(const list<T>& lt){empty_init();  // 先初始化空链表// 遍历被拷贝链表,逐个复制元素for (auto& e : lt){push_back(e);}}// 赋值运算符重载(深拷贝,支持lt1 = lt2)list<T>& operator=(const list<T>& lt){if (this != &lt)  // 避免自我赋值(如lt1 = lt1){clear();  // 先清空当前链表的旧元素// 复制lt中的元素到当前链表for (auto& e : lt){push_back(e);}}return *this;  // 支持链式赋值(如lt1 = lt2 = lt3)}
g 私有成员变量
private:Node* _head;  // 头节点指针(哨兵节点,不存储数据)size_t _size = 0;  // 链表元素个数(需在增删操作中维护)
};

(8)operator** 和operator->

// 迭代器解引用操作
// *it = 1
// Ref 返回节点数据的引用(可读或可写)
Ref operator*() // 解引用, Ref就是reference,引用的意思
{return _node->_data;
}
// operator*()返回对应数据类型的引用Ptr operator->() // 返回对应数据类型的指针
{return &_node->_data;
}

这个时候我们会在模板参数里加入一个新的模板,class Ptr,这也就和源代码里的三个模板一样

template<class T, class Ref, class Ptr> // T 数据类型 <T> 提供 IntelliSense 的示例模板参数
// Ref 引用类型 (T& 或 const T&)
struct list_iterator
{// using还具有typedef没有的功能// 使用类型别名 (C++11新特性)using Self = list_iterator<T, Ref, Ptr>; // 自身类型using Node = list_node<T>; // 节点类型Node* _node; // 当前指向的节点
};// 迭代器类型定义
using iterator = list_iterator<T, T&, T*>; // 普通迭代器
using const_iterator = list_iterator<T, const T&, const T*>; // 常量迭代器
// const T* 只能读取数据,不能修改数据

operator->:返回当前节点数据的指针(Ptr 类型,若为 T* 则可通过指针操作元素,若为 const T* 则只能读),支持像指针一样用 it->member 访问元素的成员(比如当元素是自定义结构体或类时,访问其成员变量或成员函数)

operator->返回的是指针

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

相关文章:

  • 网站开发的整体职业规划购物网站多少钱
  • 【JVM】线上JVM堆内存报警,占用超90%
  • 【JVM系列】-第1章-JVM与Java体系结构
  • 鸿蒙NEXT Wear Engine穿戴侧应用开发完全指南
  • OpenHarmony 与 HarmonyOS 的 NAPI 开发实战对比:自上而下与自下而上的差异解析
  • openHarmony之DSoftBus分布式软总线智能链路切换算法
  • TensorFlow2 Python深度学习 - 循环神经网络(GRU)示例
  • TVM | Relay
  • 使用 Conda 安装 QGIS 也是很好的安装方式
  • 网站套餐到期什么意思抖音seo优化系统招商
  • 怎么看网站pr值衡水市住房和城乡建设局网站
  • 散点拟合圆:Matlab两种方法实现散点拟合圆
  • Kubernetes流量管理:从Ingress到GatewayAPI演进
  • 专做品牌网站西安做网站电话
  • “函数恒大于0”说明函数是可取各不同数值的变数(变量)——“函数是一种对应法则等”是非常明显的错误
  • Linux系统--信号(4--信号捕捉、信号递达)--重点--重点!!!
  • Blender后期合成特效资产预设插件 MP_Comp V2.0.2
  • 达梦8数据库常见故障分析与解决方案
  • 迁移服务器
  • 解决docker构建centos7时yum命令报错、镜像源失效问题
  • 密钥轮换:HashiCorp Vault自动续期,密钥生命周期?
  • 即时通讯系统核心模块实现
  • 【HarmonyOS】组件嵌套优化
  • 福州企业做网站催眠物语wordpress
  • 图文并茂:全面了解UART相关知识(TTL+RS232+RS484)
  • VMware Euler系统Ctrl+C/V共享剪贴板完全指南:从配置到彻底清理
  • IOT项目——STM32
  • 【物联网架构】
  • 【编程】IDEA自定义系统注解格式|自定义自定义注解格式
  • 定位网站关键词dw网页制作模板源代码