C++初阶 | [九] list 及 其模拟实现

news2024/11/25 16:43:01

摘要:介绍 list 容器,list 模拟实现,list与vector的对比


list(带头双向循环列表)

导入:list 的成员函数基本上与 vector 类似,具体内容可以查看相关文档(cplusplus.com/reference/list/list/),这里不多赘述。以下对 list 的 Operations 部分的函数进行简单讲解。

Operations:

splice

Transfer elements from list to list (public member function)

remove

Remove elements with specific value (public member function)

remove_if

Remove elements fulfilling condition (public member function template)

unique

Remove duplicate values (public member function)

merge

Merge sorted lists (public member function)

sort

Sort elements in container (public member function)

reverse

Reverse the order of elements (public member function)

注意:list 没有扩容的概念,而是一份一份相对独立的节点串连起来的。

1)sort

  • #include<list> std::list::sort#include<algorithm> std::sort
    如上图,RandomAccessIterator 至少已经在名称上提示使用者,这个 sort 函数要求支持能够被随机访问的迭代器
    首先,list 的迭代器是双向迭代器;其次,从底层实现来看,std::sort 函数用到了迭代器相减,而 list 的地址是不连续的。所以 list 不支持使用 std::sort 函数。

  •  std::list::sort 的使用:该函数默认升序排列(底层是归并排序)
    如果要降序排序有如下代码以供参考:(std::greater<int>() 是一个 greater 类型的匿名对象,这种写法更常用)

    #include<functional>
    #include<list>
    
    int main()
    {
    	std::list<int> lt;
        //在 lt 中插入一些数据之后
    	std::greater<int> gt;
    	lt.sort(gt);//or:lt.sort(std::greater<int>());
    
    	return 0;
    }

  • std::list::sort 性能测试
    测试结果:
    ①在 Rlease 模式下, std::vector::sort 效率大约是 list 的 2 倍,并且数据量越大效率差距越大。(tip.性能测试要在 Rlease 模式下进行,Debug 模式下优化没有全开)
    ②通过 vector 给 list 排序:把 list 对象 → 拷贝数据到 vector 对象中 →对 vector 对象 sort → 把排序好的数据拷贝到 list 。这样对 list 排序,在数据量较大的情况下效率甚至比 list 直接排序要高。

sumlist 的 sort 在性能上没有什么优势,list 中的 sort 函数在对于数据量小的情况下可以使用,但平时能不用尽量不要频繁使用。

2)merge

归并两个 list 到一个 list(要先 sort 才可以 merge,实践中很少用)。

3)unique

去重,但也有要求——只能去除连续相同的,所以要先 sort 再 unique 才可以真正“去重”。

4)splice

转移(移动指针),如下图。

以上就是对 list 一些函数的简单介绍。 


list 的模拟实现

1)结构

如上图,list 中的每个节点是一个自定义类型 Node,对于双向链表,每个节点内包括自身储存的数据、前节点指针和后节点指针。
对于由一个一个节点组成的 list通过头节点来管理整个 list

代码示例

// List的节点类
	template<class T>
	struct ListNode
	{
		ListNode<T>* _pPre;
		ListNode<T>* _pNext;
		T _val;
	};

//List类
    template<class T>
	class list
	{
		PNode _pHead;//注意:这里是一个内置类型(指针)	
	};

2)初始化_Constructor

对 list 的初始化首先是对头节点的初始化。

// List的节点类
	template<class T>
	struct ListNode
	{
		ListNode(const T& val = T())
			: _val(val)
			, _pPre(nullptr)
			, _pNext(nullptr)
		{}

		ListNode<T>* _pPre;
		ListNode<T>* _pNext;
		T _val;
	};

//List类
    template<class T>
	class list
	{
		typedef ListNode<T> Node;
		typedef Node* PNode;
	public:
		///
		// List的构造
		list()
		{
			CreateHead();
		}
	
	private:
		void CreateHead()//对头结点进行初始化
		{
			_pHead = new Node;//这里会去调用struct ListNode的构造函数
			_pHead->_pNext = _pHead;
			_pHead->_pPre = _pHead;
		}
		PNode _pHead;//注意:这里是一个内置类型(指针)	
	};

3)Iterator

class Iterator——Iterator类

  1. 成员变量:Node* _pNode
  2. 成员函数:operator* 、operator++ 、operator-- 、operator!= 、operator==(模拟指针的行为)——这里体现了“封装”。封装屏蔽底层差异和实现细节,提供统一的访问修改遍历方式。

代码示例

	//List的迭代器类
	template<class T>
	class ListIterator
	{
		typedef ListNode<T>* PNode;
		typedef ListIterator<T> Self;
	public:
		//constructor
		ListIterator(PNode pNode = nullptr)
			:_pNode(pNode)
		{}
		ListIterator(const Self& l)//copy constructor
		{
			_pNode = l._pNode;
		}

		//operations
		T& operator*()
		{
			return _pNode->_val;
		}
		T* operator->()
		{
			return &_pNode->_val;
		}
		Self& operator++()
		{
			_pNode = _pNode->_pNext;
			return *this;
		}
		Self operator++(int)
		{
			Self tmp = _pNode;
			_pNode = _pNode->_pNext;
			return tmp;
		}
		Self& operator--()
		{
			_pNode = _pNode->_pPre;
			return *this;
		}
		Self operator--(int)
		{
			Self tmp = _pNode;
			_pNode = _pNode->_pPre;
			return tmp;
		}
		bool operator!=(const Self& l)
		{
			return _pNode != l._pNode;
		}
		bool operator==(const Self& l)
		{
			return _pNode == l._pNode;
		}
		PNode _pNode;
	};

对 operator-> 的补充说明

我们知道,对于自定义类型,可以通过对其指针解引用 " *(pointer). " 和 " (pointer)-> " 来访问其成员。而 iterator 实际上是在模拟指针的行为,对于 operator-> 的使用编译器做出了优化。如下图。

3)Const_Iterator 

注意!const_iterator 不是用 const 修饰 iterator,如上 iterator 中的模拟实现可以看出,iterator 底层是原生指针,用 const 修饰 iterator 是使得指针本身不可修改,const_iterator 本身是要能被进行 ++ 和 -- 操作的,否则无法实现遍历;而 const_iterator 针对的是被 const 修饰的 list 的对象,即 const 修饰的是 list 的实例化对象本身(ps. list 对象是 const 的,那储存在节点中的数据肯定也是 const 的,即为 const T)

如上图,实际上我们需要实现两个不同的 iterator —— class ListIteratorclass ListConst_Iterator ,而对于 const 对象,begin 和 end 函数将会返回 const_iterator。

优化:使用类模板实现 List 的 Iterator 类

代码示例

	//List的迭代器类
	template<class T, class Ref, class Ptr>
	class ListIterator
	{
		typedef ListNode<T>* PNode;
		typedef ListIterator<T, Ref, Ptr> Self;
	public:
		//constructor
		ListIterator(PNode pNode = nullptr)
			:_pNode(pNode)
		{}
		ListIterator(const Self& l)//copy constructor
		{
			_pNode = l._pNode;
		}

		//operations
		Ref operator*()
		{
			return _pNode->_val;
		}
		Ptr operator->()
		{
			return &_pNode->_val;
		}
		Self& operator++()
		{
			_pNode = _pNode->_pNext;
			return *this;
		}
		Self operator++(int)
		{
			Self tmp = _pNode;
			_pNode = _pNode->_pNext;
			return tmp;
		}
		Self& operator--()
		{
			_pNode = _pNode->_pPre;
			return *this;
		}
		Self operator--(int)
		{
			Self tmp = _pNode;
			_pNode = _pNode->_pPre;
			return tmp;
		}
		bool operator!=(const Self& l)
		{
			return _pNode != l._pNode;
		}
		bool operator==(const Self& l)
		{
			return _pNode == l._pNode;
		}
		PNode _pNode;
	};

	//list类
	template<class T>
	class list
	{
		typedef ListNode<T> Node;
		typedef Node* PNode;
	public:
		typedef ListIterator<T, T&, T*> iterator;
		typedef ListIterator<T, const T&, const T&> const_iterator;
	public:
		///
		// List的构造
		list()
		{
			CreateHead();
		}

		///
		// List Iterator
		iterator begin()
		{
			return _pHead->_pNext;
		}
		iterator end()
		{
			return _pHead;
		}
		const_iterator begin() const
		{
			return _pHead->_pNext;
		}
		const_iterator end()const
		{
			return _pHead;
		}
    }

注意:同一个类模板,实例化参数不同,就是完全不同的类型,即对于 ListIterator<T, T&, T*> 和 ListIterator<T, const T&, const T&> 是两个不同的类型。(ps. iterator 和 const_iterator 都实现之后才可以支持使用范围 for)

4)其他成员函数

这些成员函数实现起来思路很简单,有问题建议去看数据结构的文章回顾一下。以下简略说明。

①insert

insert 之后 iterator 不失效,因为没有扩容的影响。

		// 在pos位置前插入值为val的节点
		iterator insert(iterator pos, const T& val)
		{
			PNode cur = pos._pNode;
			PNode newnode = new Node(val);
			newnode->_pNext = cur;
			newnode->_pPre = cur->_pPre;

			cur->_pPre = newnode;
			newnode->_pPre->_pNext = newnode;

			return pos;
		}

②erase

erase 之后 iterator 失效,因为这个被 erase 的节点被释放了,那么指向它的 iterator 也就失效了。

		// 删除pos位置的节点,返回该节点的下一个位置
		iterator erase(iterator pos)
		{
			if (!empty())
			{
				PNode next = pos._pNode->_pNext;
				pos._pNode->_pPre->_pNext = next;
				next->_pPre = pos._pNode->_pPre;
				delete pos._pNode;
				--_size;

				return next;
			}

			return _pHead;
		}

③push_back and push_front

复用 insert。

		// List Modify
		void push_back(const T& val) { insert(end(), val); }
		
		void push_front(const T& val) { insert(begin(), val); }	

④pup_back and pop_front

复用 erase。

		// List Modify
		void pop_back() { erase(--end()); }

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

⑤clear

用 iterator 遍历,依次 erase 每个节点。

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

⑥Destructor

clear → delete → nullptr,即清理 list,释放头节点,头结点指针指针置空。

		//destructor
		~list()
		{
			clear();
			delete _pHead;
			_pHead = nullptr;
		}

⑦Copy Constructor

范围 for 循环 push_back。(注意:使用范围 for 需要把 const_iterator 也实现了才能用)

		list(const list<T>& l)//copy constructor
		{
			CreateHead();
			for (auto e : l)
			{
				push_back(e);
			}
		}

⑧赋值重载

		//assign
		list<T>& operator=(list<T> l)
		{
			if (_pHead != l._pHead)
			{
				swap(l);
				return *this;
			}

		}

		void swap(list<T>& l)
		{
			std::swap(_pHead, l._pHead);
			std::swap(_size, l._size);
		}

⑨其他构造函数重载

		list(int n, const T& value = T())
		{
			CreateHead();
			while (n--)
			{
				push_back(value);
			}
		}
		template <class Iterator>
		list(Iterator first, Iterator last)
		{
			CreateHead();
			Iterator it = first;
			while (it != last)
			{
				push_back(*it);
				++it;
			}
		}

补充:list 的成员变量中可以加一个 size_t 类型的变量来记录节点个数,因为如果没有这个成员变量就需要遍历来获取有效数据个数,效率比较低。(提醒:如果增加了 size_t 类型的成员变量记得在 insert 和 erase 的函数实现中相应地做出调整)

5)补充:Print

针对于 list<int> / list<char> 等类型的打印函数很好实现,以下我们尝试写出更通用的打印函数。

打印 list<T> 而不只是针对某个具体的 T 类型

因为语法编译之前要先对模板进行实例化,对于 Btl::list<T>::const_iterator 由于模板没有被实例化,所以编译器不知道 const_iterator  list<T> 中的一个内嵌类型还是静态成员变量,这样的行为对于编译器是未知的。

所以,Btl::list<T>::const_iterator 前加 typename 来声明这是一个内嵌类型。代码如下。

template<typename T>
void print_l(const Btl::list<T>& _list)
{
	typename Btl::list<T>::const_iterator it = _list.begin();
	while (it != _list.end())
	{
		std::cout << *it;
		++it;
	}
	std::cout << std::endl;
}

打印任意容器

提醒:下列代码中要求 *it 支持流插入。

template<typename Container>
void print_l(const Container& _con)
{
	typename Container::const_iterator it = _con.begin();
	while (it != _con.end())
	{
		std::cout << *it;
		++it;
	}
	std::cout << std::endl;
}

回顾:vector模拟实现中涉及的深浅拷贝的问题

对于类似 vector<string> 而出现的深浅拷贝问题,因为 list 不涉及扩容的概念,所以不会出现深浅拷贝的问题。


list与vector的对比

vectorlist
底层结构动态顺序表,一段连续空间带头结点的双向循环链表
随机访问支持随机访问,访问某个元素的效率为O(1)不支持随机访问,访问某个元素的效率为O(N)
插入和删除任意位置插入和删除效率低,需要搬移元素(挪动数据),时间复杂度为O(N),插入时有可能需要增容——开辟新空间,拷贝元素,释放就空间,导致效率更低任意位置插入和删除效率高,不需要搬移元素,时间复杂度为O(1)
空间利用率底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高底层结点动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低
迭代器原生态指针对原生态指针(节点指针)进行封装
迭代器失效

在插入元素时,要给所有迭代器重新赋值,因为插入元素有可能会导致重新扩容,致使原来迭代器失效;删除时,当前迭代器需要重新给赋值否则会失效

插入元素不会导致迭代器失效;删除元素时,只会导致当前迭代器失效,其他迭代器不受影响
使用场景需要高效存储,支持随机访问,不关系插入删除效率大量插入和删除操作,不关心随机访问

完整代码链接My_List/My_List/My_List.h · fantansy-13-07/Cpp - 码云 - 开源中国 (gitee.com)


END

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

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

相关文章

JavaScript中new操作符具体干了什么

文章目录 一、是什么二、流程三、手写new操作符 一、是什么 在JavaScript中&#xff0c;new操作符用于创建一个给定构造函数的实例对象 例子 function Person(name, age){this.name name;this.age age; } Person.prototype.sayName function () {console.log(this.name) …

【堆、位运算、数学】算法例题

目录 十九、堆 121. 数组中的第K个最大元素 ② 122. IPO ③ 123. 查找和最小的K对数字 ② 124. 数据流的中位数 ③ 二十、位运算 125. 二进制求和 ① 126. 颠倒二进制位 ① 127. 位1的个数 ① 128. 只出现一次的数字 ① 129. 只出现一次的数字 II ② 130. 数字范围…

Java项目:66 ssm实验室耗材管理系统设计与实现+jsp

作者主页&#xff1a;源码空间codegym 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文中获取源码 项目介绍 管理员管理实验材料&#xff0c;审核教师与学生对实验材料的申请信息&#xff0c;统计每学年实验材料的使用总数信息。 教师申请使用实验材料…

未来有无限可能,加油年轻人!

在浩渺的宇宙中&#xff0c;我们如同微尘般渺小&#xff0c;然而&#xff0c;正是这些微小的存在&#xff0c;构成了世界的五彩斑斓。而对于年轻人来说&#xff0c;他们就是这个世界的未来&#xff0c;是希望的象征&#xff0c;是无限可能的源泉。他们拥有青春的热情&#xff0…

什么是智能体(agent)

智能体&#xff08;Agent&#xff09;是人工智能领域中的一个核心概念。在最基本的层面上&#xff0c;智能体可以被定义为一个实体&#xff0c;它能够在其所处的环境中自主地感知信息&#xff0c;并根据这些信息做出决策&#xff0c;以实现特定的目标或任务。智能体的关键特性包…

idea将Java代码打成jar包

1.进入idea的项目管理界面编写好代码 2.点击Artifacts的“”号下的jar->“From modules with dependencies” 3.在Main Class选择主函数 4.点击OK后&#xff0c;选择菜单栏的“Build” -> “Build Artifacts” -> “Build” 之后我们就可以看到在左侧生成了一个out文件…

生成单一c段或者连续c段范围内的所有ip地址+生成范围内C段脚本

1. 背景 马上有电子政务外网攻防演练要处理ip 2. 脚本1 生成c段和连续c段所有ip地址.py 用处&#xff1a;生成单一c段或者连续c段范围内的所有ip地址。 用法&#xff1a;ipc.txt 放入 ip段或者两个ip段范围&#xff1a;如&#xff1a; 192.168.3.0/24 172.16.1.0/24-1…

罐头鱼AI视频混剪矩阵获客系统|批量剪辑定时发送

罐头鱼AI视频混剪矩阵获客系统 随着数字化时代的来临&#xff0c;视频内容已经成为各行业吸引用户关注和提升品牌形象的重要方式之一。为了帮助用户更便捷、高效地进行视频制作和推广&#xff0c;罐头鱼AI推出了全新的视频混剪矩阵获客系统&#xff0c;为您的视频营销增添智能利…

使用Python读取一个文本文件并计算其中每个单词出现的次数

import re import matplotlib.pyplot as plt from collections import Counter word_counts {} with open(E:\自然语言处理\Hamlet(2).txt) as f:for line in f:words line.strip().lower().split()for word in words:word re.sub(r[^a-zA-Z],,word)if word in word_counts:…

【链表】Leetcode 2. 两数相加【中等】

两数相加 给你两个 非空 的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的&#xff0c; 并且每个节点只能存储 一位 数字。请你将两个数相加&#xff0c;并以相同形式返回一个表示和的链表。你可以假设除了数字 0 之外&#xff0c;这两个数都不…

网速监控,实时网络速度监控

带宽与网速 现在&#xff0c;对高带宽的需求空前高涨&#xff0c;而且网络&#xff08;包括标准的内部部署&#xff09;以及公共、私有和混合环境都变得更加复杂。 虽然带宽和网速经常互换使用&#xff0c;但它们并不总是相同的。网速更多的是与延迟有关&#xff0c;而不是与…

文件上传基础篇

文件上传基础篇 文件上传漏洞原理 ​ 目标网站存在文件上传接口&#xff0c;但是对用户上传的文件没有做仔细甄别&#xff0c;导致黑客可以根据此功能点直接上传木马到网站服务器&#xff0c;造成危害 文件上传存在点 ​ 通常有头像上传&#xff0c;pdf上传 文件上传防护 …

Vue 计算属性和监视属性

Vue 计算属性和监视属性 computed computed 计算属性 规则&#xff1a; 用已有的属性计算不存在的属性默认调用一次get()只有值不发生改变的时候才可以使用简写&#xff08;函数&#xff09;&#xff1b;值发生改变 使用对象式写法&#xff0c;才可以配置set()方法底层原理使…

har的编译及引用

1.创建HAR 选择文件->新建->模块&#xff0c;然后再下一个页面选择static library,之后在接下来的页面设置模块名字&#xff0c;然后下一步直到完成。 2.创建成功后在新建的模块下编写自己的代码内容。 3.编译HAR 编译默认是从Index.ets文件下进行导出&#xff0c;如果…

Flink:使用 Faker 和 DataGen 生成测试数据

博主历时三年精心创作的《大数据平台架构与原型实现&#xff1a;数据中台建设实战》一书现已由知名IT图书品牌电子工业出版社博文视点出版发行&#xff0c;点击《重磅推荐&#xff1a;建大数据平台太难了&#xff01;给我发个工程原型吧&#xff01;》了解图书详情&#xff0c;…

用图解说明mysql 行锁加锁规则

加锁原则 原则 1&#xff1a;加锁的基本单位是 next-key lock。希望你还记得&#xff0c;next-key lock 是前开后闭区间。原则 2&#xff1a;查找过程中访问到的对象才会加锁。优化 1&#xff1a;索引上的等值查询&#xff0c;给唯一索引加锁的时候&#xff0c;next-key lock …

Linux---基本操作命令之用户管理命令

1.1useradd 添加新用户 root用户&#xff1a;/root 普通用户&#xff1a;/home/ 创建的用户还是david&#xff0c;只是在dave文件夹下 1.2 passwd 设置密码 给用户tony设置密码: 123456 1.3 id 查看用户是否存在 查看有没有这个用户&#xff1a;id 名字 gid&#xff1a;用…

当人工智能无处不在

以下文章来源&#xff1a;新华社 2024年度的“西南偏南”多元创新大会和艺术节8日至16日在得克萨斯州首府奥斯汀举行。在今年的数百场讲座或沙龙讨论中&#xff0c;热度最高、讨论最多的前沿话题不难猜——人工智能&#xff08;AI&#xff09;。 焦点高度发散。从太空探索到音乐…

【windows】Python文件打包成dll文件及遇见的问题

最近需要将py文件转为dll&#xff0c;特此记录。 操作步骤来自于&#xff1a;将Python文件发布成DLL并调用 写一个py文件 文件名&#xff1a;test_numpy.py import numpy as npdef func(my_list1, my_list2):list_np1 np.array(my_list1)list_np2 np.array(my_list2)retur…

Visual Studio 2013 - 输出窗口一闪而过问题解决

Visual Studio 2013 - 输出窗口一闪而过问题解决 1. Visual Studio Console 一闪而过问题解决1.1. set Debug1.2. set Release References 1. Visual Studio Console 一闪而过问题解决 工程 -> 属性 -> 配置属性 -> 链接器 -> 系统 -> 子系统 -> 下拉框 -&g…