【C++】- 掌握STL List类:带你探索双向链表的魅力

news2025/2/24 18:54:32

在这里插入图片描述

文章目录

  • 前言:
  • 一.list的介绍及使用
    • 1. list的介绍
    • 2. list的使用
      • 2.1 list的构造
      • 2.2 list iterator的使用
      • 2.3 list capacity
      • 2.4 list element access
      • 2.5 list modifiers
      • 2.6 list的迭代器失效
  • 二.list的模拟实现
    • 1. list的节点
    • 2. list的成员变量
    • 3.list迭代器相关问题
      • 3.1 普通迭代器
      • 3.2 const迭代器
    • 4. list的成员函数
      • 4.1 list的空初始化
      • 4.2 push_back
      • 4.3 构造函数
      • 4.4 insert
      • 4.4 erase
      • 4.5 push_front
      • 4.6 pop_front
      • 4.7 pop_back
      • 4.8 clear
      • 4.8 析构函数
      • 4.9 swap
      • 4.10 赋值运算符重载
  • 最后想说:

前言:

 C++中的List容器是标准模板库(STL)中的一种序列容器,它实现了双向链表的功能。与数组(如vector)和单向链表相比,List容器提供了更加灵活的元素插入和删除操作,特别是在容器中间位置进行这些操作时。

一.list的介绍及使用

1. list的介绍

  • 双向链表结构: list容器使用双向链表来存储元素,每个元素(节点)都包含数据部分两个指针,分别指向前一个元素和后一个元素。这种结构使得在链表的任何位置进行插入和删除操作都非常高效,时间复杂度为O(1)
  • 动态大小: list容器的大小可以在运行时动态改变,即可以在程序运行过程中添加或移除元素。
  • 不支持随机访问:vectorarray等连续内存的容器不同,list不支持随机访问迭代器,不能直接通过索引获取元素,而需要通过迭代器遍历。
  • 迭代器稳定性: 在list中插入或删除元素不会导致其他迭代器失效(除了指向被删除元素的迭代器)。这是因为它通过调整相邻节点的指针来维护链表结构,而不需要移动元素或重新分配内存。

2. list的使用

list的使用参考文档:list的文档介绍
在这里插入图片描述

2.1 list的构造

构造函数接口说明
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

代码演示:

#include<list>
int main()
{
	list<int> l1;//构造空的l1;
	list<int> l2(4,100);//l2中存放4个值为100的元素
	list<int> l3(l2.begin(),l2.end());//用l2的[begin,end)左开右闭区间构造l3;
	list<int> l4(l3);//用l3拷贝构造l4


    // 以数组为迭代器区间构造l5
    int array[] = { 16,2,77,29 };
    list<int> l5(array, array + sizeof(array) / sizeof(int));

    // 列表格式初始化C++11
    list<int> l6{ 1,2,3,4,5 };

    // 用迭代器方式打印l5中的元素
    list<int>::iterator it = l5.begin();
    while (it != l5.end())
    {
        cout << *it << " ";
        ++it;
    }
    cout << endl;

    // C++11范围for的方式遍历
    for (auto& e : l5)
        cout << e << " ";

    cout << endl;

	return 0;
}

2.2 list iterator的使用

此处,大家可暂时将迭代器理解成一个指针,该指针指向list中的某个节点。

函数声明接口说明
begin返回第一个元素的迭代器
end返回最后一个元素下一个位置的迭代器
rbegin返回一个指向容器中最后一个元素的反向迭代器(即容器的反向起始)
rend返回一个反向迭代器,该迭代器指向列表容器中第一个元素之前的理论元素(该元素被认为是其反向结束)。

注意:

  1. begin与end为正向迭代器,对迭代器执行++操作,迭代器向后移动
  2. rbegin(end)与rend(begin)为反向迭代器,对迭代器执行++操作,迭代器向前移动

代码演示:

int main()
{
    int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
    list<int> l(array, array + sizeof(array) / sizeof(array[0]));
    // 使用正向迭代器正向list中的元素
    // list<int>::iterator it = l.begin();   // C++98中语法
    auto it = l.begin();                     // C++11之后推荐写法
    while (it != l.end())
    {
        cout << *it << " ";
        ++it;
    }
    cout << endl;

    // 使用反向迭代器逆向打印list中的元素
    // list<int>::reverse_iterator rit = l.rbegin();
    auto rit = l.rbegin();
    while (rit != l.rend())
    {
        cout << *rit << " ";
        ++rit;
    }
    cout << endl;

    return 0;
}

2.3 list capacity

函数声明接口说明
front检测list是否为空,是返回true,否则返回false
size返回list中有效节点的个数

2.4 list element access

函数声明接口说明
front返回list的第一个节点中值的引用
back返回list的最后一个节点中值的引用

2.5 list modifiers

函数声明接口说明
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中的有效元素

代码演示:

#include<iostream>
#include<vector>
using namespace std;

void PrintList(const list<int>& l)
{
    // 注意这里调用的是list的 begin() const,返回list的const_iterator对象
    for (list<int>::const_iterator it = l.begin(); it != l.end(); ++it)
    {
        cout << *it << " ";

    }

    cout << endl;
}

// list插入和删除
// push_back/pop_back/push_front/pop_front
void TestList1()
{
    int array[] = { 1, 2, 3 };
    list<int> L(array, array + sizeof(array) / sizeof(array[0]));

    // 在list的尾部插入4,头部插入0
    L.push_back(4);
    L.push_front(0);
    PrintList(L);

    // 删除list尾部节点和头部节点
    L.pop_back();
    L.pop_front();
    PrintList(L);
}

// insert /erase 
void TestList2()
{
    int array1[] = { 1, 2, 3 };
    list<int> L(array1, array1 + sizeof(array1) / sizeof(array1[0]));

    // 获取链表中第二个节点
    auto pos = ++L.begin();
    cout << *pos << endl;

    // 在pos前插入值为4的元素
    L.insert(pos, 4);
    PrintList(L);

    // 在pos前插入5个值为5的元素
    L.insert(pos, 5, 5);
    PrintList(L);

    // 在pos前插入[v.begin(), v.end)区间中的元素
    vector<int> v{ 7, 8, 9 };
    L.insert(pos, v.begin(), v.end());
    PrintList(L);

    // 删除pos位置上的元素
    L.erase(pos);
    PrintList(L);

    // 删除list中[begin, end)区间中的元素,即删除list中的所有元素
    L.erase(L.begin(), L.end());
    PrintList(L);
}

// resize/swap/clear
void TestList3()
{
    // 用数组来构造list
    int array1[] = { 1, 2, 3 };
    list<int> l1(array1, array1 + sizeof(array1) / sizeof(array1[0]));
    PrintList(l1);

    // 交换l1和l2中的元素
    list<int> l2;
    l1.swap(l2);
    PrintList(l1);
    PrintList(l2);

    // 将l2中的元素清空
    l2.clear();
    cout << l2.size() << endl;
}

int main()
{
    TestList1();
    TestList2();
    TestList3();


	return 0;
}

运行结果:
在这里插入图片描述

2.6 list的迭代器失效

 前面已经说过了,此处可以将迭代器理解为类似于指针的东西,迭代器失效即迭代器指向的节点失效了,即该节点被删除了。因为list的底层结构为带头结点的双向循环链表,因此在list进行插入操作时不会导致迭代器失效,只有删除时才会失效,并且失效的是被删除节点的迭代器,其他迭代器不会受到影响。

二.list的模拟实现

1. list的节点

template<class T>
struct list_node
{
	T _data;
	list_node<T>* _next;
	list_node<T>* _prev;

	list_node(const T& x = T())
		:_data(x)
		, _next(nullptr)
		, _prev(nullptr)
	{}
};

2. list的成员变量

template<class T>
class list
{
	typedef list_node<T> Node;
public:
    //成员函数
private:
	Node* _head; //哨兵位的头节点
};

 没有用访问限定符限制的成员class默认是私有的,struct默认是公有的,如果一个类既有公有也有私有就用class,全部为公有一般用struct。这不是规定,只是个惯例。

3.list迭代器相关问题

简单分析:
&emsp; 这里不能像以前一样给一个结点的指针作为迭代器,如果it是typedef的节点的指针,it解引用得到的是节点,不是里面的数据,但是我们期望it解引用是里面的数据,++it我们期望走到下一个节点去,而list中++走不到下一个数据,因为数组的空间是连续的,++可以走到下一个数据。但是链表达不到这样的目的。所以原身指针已经无法满足这样的行为,怎么办呢?这时候我们的类就登场了
用类封装一下节点的指针,然后重载运算符,模拟指针。
例如:

reference operator*()const
{
	return (*node).data;
}
self& opertor++()
{
	node = (link_type)((*node).next);
	return *this;
}

3.1 普通迭代器

template<class T>
struct list_iterator
{
	typedef list_node<T> Node;
	typedef list_iterator<T> Self;
	Node* _node;

	list_iterator(Node* node)
		:_node(node)
	{}

	T& operator*() //用引用返回可以读数据也可以修改数据
	{
		return _node->_data;
	}
	T* operator->()
	{
		return &_node->_data;
	}

	Self& operator++()
	{
		_node = _node->_next;
		return *this;
	}
	Self& operator--()
	{
		_node = _node->_prev;
		return *this;
	}
	Self operator++(int)
	{
		Self tmp(*this);
		_node = _node->_next;
		return tmp;
	}
	Self operator--(int)
	{
		Self tmp(*this);
		_node = _node->_prev;
		return tmp;
	}

	bool operator!=(const Self& s)
	{
		return _node != s._node;
	}
};

3.2 const迭代器

const迭代器在定义的时候不能直接定义成typedef const list_iterator<T> const_iterator,const迭代器的本质是限制迭代器指向的内容不能被修改,而前面的这种写法限制了迭代器本身不能被修改,所以迭代器就不能进行++操作。那该怎能办呢?答案是我们可以实现一个单独的类:

template<class T>
struct list_const_iterator
{
	typedef list_node<T> Node;
	typedef list_const_iterator<T> Self;
	Node* _node;

	list_const_iterator(Node* node)
		:_node(node)
	{}

	const T& operator*()
	{
		return _node->_data; //返回这个数据的别名,但是是const别名,所以不能被修改
	}
	const T* operator->()
	{
		return &_node->_data; //我是你的指针,const指针
	}

	Self& operator++()
	{
		_node = _node->_next;
		return *this;
	}
	Self& operator--()
	{
		_node = _node->_prev;
		return *this;
	}
	Self operator++(int)
	{
		Self tmp(*this);
		_node = _node->_next;
		return tmp;
	}
	Self operator--(int)
	{
		Self tmp(*this);
		_node = _node->_prev;
		return tmp;
	}

	bool operator!=(const Self& s)
	{
		return _node != s._node;
	}
};

普通法迭代器与const迭代器的区别就是:普通迭代器可读可写,const迭代器只能读
上面是我们自己实现的普通迭代器和const迭代器,用两个类,并且这两个类高度相似,下来就让我们一起看一看库里面是怎么实现的吧!

在这里插入图片描述
我们可以看到库里面是写了两个模板,让编译器去生成对应的类。其本质上也是写了两个类,只不过是让编译器去生成对应的类。

迭代器不需要我们自己写析构函数、拷贝构造函数、赋值运算符重载函数,因为这里要的是浅拷贝,例如我把一个迭代器赋值给另外一个迭代器,就是期望两个迭代器指向同一个节点,这里用浅拷贝即可,拷贝给你我们两个迭代器就指向同一个节点。

4. list的成员函数

4.1 list的空初始化

void empty_init() //空初始化
{
	_head = new Node();
	_head->_next = _head;
	_head->_prev = _head;
}

4.2 push_back

//普通版本
void push_back(const T& x)
{
	Node* new_node = new Node(x);
	Node* tail = _head->_prev;

	tail->_next = new_node;
	new_node->_prev = tail;

	new_node->_next = _head;
	_head->_prev = new_node;
}
//复用insert版本
	insert(end(),x);

4.3 构造函数

list_node(const T& x = T())
	:_data(x)
	, _next(nullptr)
	, _prev(nullptr)
{}

4.4 insert

iterator insert(iterator position; const T& val)
{
	Node* cur = pos._node;
	Node* newnode = new Node(val);
	Node* prev = cur->_prev;
	//prev newnode cur
	prev->_next = newnode;
	newnode->_prev = prev;

	newnode->_next = cur;
	cur->_prev = newnode;

	return iterator(newnode);
}

4.4 erase

iterator erase(iterator pos)
{
	assert(pos != end());
	Node* del = pos._node;
	Node* prev = del->_prev;
	Node* next = del->_next;

	prev->_next = next;
	next->_prev = prev;
	delete del;

	return iterator(next);
}

4.5 push_front

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

4.6 pop_front

void pop_front()
{
	erase(begin());
}

4.7 pop_back

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

4.8 clear

void clear()
{
	auto it = begin();
	while (it != end())
	{
		it = erase(it);
	}
}

4.8 析构函数

~list()
{
	clear();
	delete _head;

	_head = nullptr;
}

4.9 swap

void swap(list<T>& tmp)
{
	std::swap(_head, tmp._head);//交换哨兵位的头节点
}

4.10 赋值运算符重载

//现代写法
//lt2=lt3
//list<T>& operator=(list<T> lt)
list& operator=(list lt) //不加模板参数
{
	swap(lt);//交换就是交换哨兵位的头节点
	return *this;
}

//lt3传给lt去调用拷贝构造,所以lt就和lt3有一样大的空间一样大的值,lt2很想要,也就是/this想要,lt2之前的数据不想要了,交换给lt,此时lt2就和lt3有一样大的空间一样大的值,
//lt出了作用域就被销毁了

构造函数和赋值运算符重载函数的形参和返回值类型可以只写类名 list,不需要写模板参数,这种写法在类里面可以不加,只能在类里面可以这样写,类外面是不行的,一般情况下加上好一点。


最后想说:

本章我们STL的List就介绍到这里,下期我将介绍关于stackqueue的有关知识,如果这篇文章对你有帮助,记得点赞,评论+收藏 ,最后别忘了关注作者,作者将带领你探索更多关于C++方面的问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2262429.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

泷羽sec学习打卡-brupsuite8伪造IP和爬虫审计

声明 学习视频来自B站UP主 泷羽sec,如涉及侵权马上删除文章 笔记的只是方便各位师傅学习知识,以下网站只涉及学习内容,其他的都 与本人无关,切莫逾越法律红线,否则后果自负 关于brupsuite的那些事儿-Brup-FaskIP 伪造IP配置环境brupsuite导入配置1、扩展中先配置python环境2、安…

挑战一个月基本掌握C++(第五天)了解运算符,循环,判断

一 运算符 运算符是一种告诉编译器执行特定的数学或逻辑操作的符号。C 内置了丰富的运算符&#xff0c;并提供了以下类型的运算符&#xff1a; 算术运算符关系运算符逻辑运算符位运算符赋值运算符杂项运算符 1.1 算术运算符 假设变量 A 的值为 10&#xff0c;变量 B 的值为…

JAVA没有搞头了吗?

前言 今年的Java程序员群体似乎承受着前所未有的焦虑。投递简历无人问津&#xff0c;难得的面试机会也难以把握&#xff0c;即便成功入职&#xff0c;也往往难以长久。于是&#xff0c;不少程序员感叹&#xff1a;互联网的寒冬似乎又一次卷土重来&#xff0c;环境如此恶劣&…

Linux -- 线程控制相关的函数

目录 pthread_create -- 创建线程 参数 返回值 代码 -- 不传 args&#xff1a; 编译时带 -lpthread 运行结果 为什么输出混杂&#xff1f; 如何证明两个线程属于同一个进程&#xff1f; 如何证明是两个执行流&#xff1f; 什么是LWP&#xff1f; 代码 -- 传 args&a…

达梦查询表字段详细信息脚本(字段名称、描述、类型、长度及是否为空)

达梦查询表字段详细信息脚本&#xff08;字段名称、描述、类型、长度及是否为空&#xff09; 该SQL 脚本&#xff0c;用于查询表中字段的基本信息&#xff0c;包括字段名称、描述、数据类型、数据长度、是否为空及是否为主键等属性。 SQL 脚本 -- 输入变量 DECLAREp_owner VA…

学习笔记073——Java中的【Object】和【包装类】

文章目录 1、Object 类1.1、什么是 Object 类1.2、可能被重写的常用方法 2、包装类2.1、什么是包装类&#xff1f;2.2、装箱和拆箱 1、Object 类 1.1、什么是 Object 类 Java 通过类来构建代码的结构&#xff0c;类分为两种&#xff1a; 1、Java 提供的工具类&#xff0c;不…

面向预测性维护的TinyML技术栈全面综述

论文标题&#xff1a;A Holistic Review of the TinyML Stack for Predictive Maintenance&#xff08;面向预测性维护的TinyML技术栈全面综述&#xff09; 作者信息&#xff1a;Emil Njor, Mohammad Amin Hasanpour, Jan Madsen, Xenofon Fafoutis&#xff0c;均来自丹麦技术…

【MySQL】InnoDB引擎中的Compact行格式

目录 1、背景2、数据示例3、Compact解释【1】组成【2】头部信息【3】隐藏列【4】数据列 4、总结 1、背景 mysql中数据存储是存储引擎干的事&#xff0c;InnoDB存储引擎以页为单位存储数据&#xff0c;每个页的大小为16KB&#xff0c;平时我们操作数据库都是以行为单位进行增删…

【SpringAOP】深入浅出SpringAOP从原理到源码

AOP对象是如何创建的 对于熟悉Spring IOC流程源码的同学来说&#xff0c;一定了解bean的整个生命周期&#xff0c;也就是从实例化、属性填充、初始化三个过程。那么对于Bean 工厂来说&#xff0c;是如何保证需要创建代理的对象创建代理的呢。 从图中可以看到&#xff0c;本质…

VMware虚拟机Ubuntu 18.04版本 磁盘扩容

一、版本配置 虚拟机版本&#xff1a;VMware WORKSTATION 16 PRO Ubuntu版本&#xff1a;Ubuntu 18.04 二、磁盘大小介绍 目的&#xff1a;磁盘扩容&#xff08;20G----->100G&#xff09;&#xff0c;从20G扩到100G 查看磁盘大小命令&#xff1a;df -h 扩容前的磁盘大小 …

QT多线程(二):基于互斥锁与读写锁的线程同步

此处需要说明的是&#xff0c;这里的线程同步概念与操作系统中的线程同步并无区别&#xff0c;都是避免多个线程同时访问临界区数据可能产生的读写错误问题。在 Qt 中&#xff0c;有多个类可以实现线程同步的功能&#xff0c;这些类包括 QMutex、QMutexLocker、 QReadWriteLock…

【ChatGPT】解锁AI思维链:如何让机器像人类一样思考?

在人工智能领域&#xff0c;我们一直在追求让机器像人类一样思考。然而&#xff0c;即使是最先进的AI&#xff0c;也常常被诟病缺乏“常识”&#xff0c;难以理解复杂问题&#xff0c;更不用说像人类一样进行逻辑推理和解决问题了。最经常的表现就是遇到不会的地方&#xff0c;…

重新定义页签!Choerodon UI Tabs让管理更高效

01 引言 Tabs 组件通过提供平级区域&#xff0c;将大块内容进行有效的收纳和展现&#xff0c;从而保持界面整洁。但在企业应用的快速发展中&#xff0c;这样传统的页签组件已无法满足我们对界面布局和个性化展示的追求。Choerodon UI Tabs 组件通过支持多级分组、个性化配置、…

机器学习之偏差

机器学习中的偏差&#xff08;Bias&#xff09;是指模型的预测值与真实值之间的系统性误差&#xff0c;或者说模型无法准确捕捉数据中复杂模式的能力。偏差通常与模型的假设或学习能力有关&#xff0c;过高的偏差会导致模型的性能不佳&#xff0c;表现为欠拟合。 偏差的来源 模…

SSH连接监控以及新用户创建和系统资源访问限制

目录 监控连接数SSH连接数的限制和影响理论限制可能的影响 创建SSH新用户为每个ssh用户配置系统资源限制1. 使用 /etc/security/limits.conf 限制资源2. 使用 cgroups 控制资源3. 磁盘配额限制4. 限制 SSH 访问5. 使用 PAM 限制6. 监控脚本示例7. 设置定期任务清理8. 检查配置是…

测试工程师八股文04|计算机网络 和 其他

一、计算机网络 1、http和https的区别 HTTP和HTTPS是用于在互联网上传输数据的协议。它们都是应用层协议&#xff0c;建立在TCP/IP协议栈之上&#xff0c;用于客户端&#xff08;如浏览器&#xff09;和服务器之间的通信。 ①http和https的主要区别在于安全性。http是一种明…

单片机学习笔记——入门51单片机

一、单片机基础介绍 1.何为单片机 单片机&#xff0c;英文Micro Controller Unit&#xff0c;简称MCU 。内部集成了中央处理器CPU、随机存储器ROM、只读存储器RAM、定时器/计算器、中断系统和IO口等一系列电脑的常用硬件功能 单片机的任务是信息采集&#xff08;依靠传感器&a…

【青牛科技】D8563是低功耗的CMOS实时时钟/日历电路,它提供一个可编程时钟输出,一个中断输出和掉电检测器,所有的地址和数据通过IC总线接口串行传递。

概述&#xff1a; D8563是低功耗的CMOS实时时钟/日历电路,它提供一个可编程时钟输出&#xff0c;一个中断输出和掉电检测器&#xff0c;所有的地址和数据通过IC总线接口串行传递。最大总线速度为400Kbitss每次读写数据后&#xff0c;内嵌的字地址寄存器会自动产生增量。 主要特…

安卓获取所有可用摄像头并指定预览

在Android设备中&#xff0c;做预览拍照的需求的时候&#xff0c;我们会指定 CameraSelector DEFAULT_FRONT_CAMERA前置 或者后置CameraSelector DEFAULT_BACK_CAMERA 如果你使用的是平板或者工业平板&#xff0c;那么就会遇到多摄像头以及外置摄像头问题&#xff0c;简单的指…

R语言学习笔记-1

1. 基础操作和函数 清空环境&#xff1a;rm(list ls()) 用于清空当前的R环境。 打印输出&#xff1a;print("Hello, world") 用于输出文本到控制台。 查看已安装包和加载包&#xff1a; search()&#xff1a;查看当前加载的包。install.packages("package_na…