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

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


list(带头双向循环列表)

导入:list 的成员函数基本上与 vector 类似,具体内容可以查看相关文档(cplusplus.com/reference/list/list/),这里不多赘述。以下对 list 的 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) |
[Operations:]

注意: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 类型的匿名对象,这种写法更常用)

    cpp 复制代码
    #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 直接排序要高。

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

2)merge

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

3)unique

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

4)splice

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

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


list 的模拟实现

1)结构

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

对于由一个一个节点组成的 list通过头节点来管理整个 list

代码示例

cpp 复制代码
// 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 的初始化首先是对头节点的初始化。

cpp 复制代码
// 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==(模拟指针的行为)------ 这里体现了"封装"。封装屏蔽底层差异和实现细节,提供统一的访问修改遍历方式。

代码示例

cpp 复制代码
	//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 类

代码示例

cpp 复制代码
	//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 不失效,因为没有扩容的影响。

cpp 复制代码
		// 在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 也就失效了。

cpp 复制代码
		// 删除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。

cpp 复制代码
		// 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。

cpp 复制代码
		// List Modify
		void pop_back() { erase(--end()); }

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

⑤clear

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

cpp 复制代码
		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				it = erase(it);
			}
		}

⑥Destructor

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

cpp 复制代码
		//destructor
		~list()
		{
			clear();
			delete _pHead;
			_pHead = nullptr;
		}

⑦Copy Constructor

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

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

⑧赋值重载

cpp 复制代码
		//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);
		}

⑨其他构造函数重载

cpp 复制代码
		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来声明这是一个内嵌类型。代码如下。

cpp 复制代码
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 支持流插入。

cpp 复制代码
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的对比

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


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


END

相关推荐
为啥不吃肉捏1 分钟前
C++/Qt 集成 AutoHotkey
开发语言·c++·qt
代码吐槽菌4 分钟前
基于SpringBoot的在线点餐系统【附源码】
java·开发语言·spring boot·后端·mysql·计算机专业
努力学习的小廉4 分钟前
【C++】—— string模拟实现
开发语言·c++
凌晨五点的星6 分钟前
网络安全-webshell绕过,hash碰撞,webshell绕过原理
开发语言·前端·javascript
qq_1728055923 分钟前
Kafka-Go学习
开发语言·学习·golang·kafka·go
天心天地生24 分钟前
【bugfix】-洽谈回填的图片消息无法显示
开发语言·前端·javascript
小灰灰爱代码24 分钟前
C++——求3*3矩阵主对角元素之和。
c++·算法·矩阵
计算机学姐28 分钟前
基于协同过滤算法+PHP的新闻推荐系统
开发语言·vue.js·vscode·mysql·php·phpstorm
银氨溶液28 分钟前
IO模型---BIO、NIO、IO多路复用、AIO详解
java·开发语言·java基础·io模型
hjxxlsx1 小时前
插入与冒泡排序(C++)
c++·算法·排序算法