第四章:C++之list(逻辑梳理、简单使用演示、部分源码实现)

一、先决知识点1------认识list:

  • list底层实现是双向链表,但是不是循环链表。
  • list是否使用哨兵节点,是细节问题,C++标准并未规定。
  • list是链表,他的优势在于对节点的操作会十分灵活,因此它在需要频繁插入和删除元素的情况下非常高效。
  • list是链表的原因,他的元素分布不再是连续的空间,所以使用'[ ]'来随机访问会使得性能消耗过大,所以C++标准不支持使用'[ ]'实现访问数据。

二、先决知识2------迭代器的分类:

  • 根据迭代器的访问能力,可以将迭代器分为三类:单向迭代器、双向迭代器、随机访问迭代器。
  • 单向迭代器:只支持++,例如单链表
  • 双向迭代器:支持++和--,例如双向链表,红黑树
  • 随机访问迭代器:支持++/--/+/- 等,例如vector,string
  • 随机访问迭代器支持所有单向/双向迭代器的功能,因此可以向支持随机访问迭代器作为参数的函数,传递单向/双向迭代器作为参数。反之则不行。

三、简单的使用演示(vector/string中使用方法不变的不再赘述):

3.1排序------sort:

  • list内部实现了sort方法,默认升序。
  • 由于list是链表,他的sort在底层是归并排序而非快排。因此效率并不高,当数据量很大时,归并和快排的效率差距很大。数据量大时,先转换为vector排序后再转化为list可行。

3.2去重------unique:

  • 要求list有序,可以先sort再unique。

3.3删除------remove:

  • 删除所有指定值。

3.4转移------splice:

  • 把一个list的节点摘下来插到另一个list

    void splice (iterator position, list& x);
    void splice (iterator position, list& x, iterator i);
    void splice (iterator position, list& x, iterator first, iterator last);

四、底层功能实现(第一版,部分功能不是很完善,适合先了解逻辑):

4.1节点类:

  • 节点类,每个链表节点包含三个成员,分别是节点数据、上一个节点地址、下一个节点地址。

  • 创建哨兵节点时,哨兵节点不存储节点数据,所以可以使用缺省值;在C++中,内置类型也有默认构造,T()可以初始化一个类型为T的对象,调用其默认构造函数

    template<class T>//模板
    struct list_node//节点类
    {
    T _data;
    list_node<T>* _next;
    list_node<T>* _prev;//一个指向上一个节点,一个指向下一个节点,一个存储节点值

    list_node<T>(const T& x = T())//内置类型也有匿名对象
    	:_data(x)
    	, _prev(nullptr)
    	, _next(nullptr)
    {}
    

    };

4.2迭代器类:

按照需求,实现的逻辑顺序:

1.重命名,简化书写:

		typedef list_node<T> Node;//对节点对象重命名
		typedef __list_iterator<T> self;//对自己重命名

2.构造函数和成员:

  • _node是一个指针,通过构造函数初始化,指向传过来的节点的地址。

    	Node* _node;//创建一个指针
    
    	__list_iterator(Node* node)
    		:_node(node)//指针指向传递的节点对象
    	{}
    

3.++/--的运算符重载:

  • list是链表,要实现节点之间的迭代,就需要'封装+运算符重载'。

  • 后置++/--,加上一个参数int来和前置++/--构成函数重载;

  • 由于后置++/--会创建临时对象,所以资源消耗会大于前置++/--,推荐使用前置代替后置。

  • 后置++/--,返回的是临时对象,所以不能使用引用返回。

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

4.*的运算符重载:

  • 返回节点处的数据,如果使用引用返回,还可以修改节点数据。

    	T& operator*()//获取节点对象处值,引用返回
    	{
    		return _node->_data;
    	}
    
  1. ==和!=的运算符重载:
  • 比较两个节点的地址是否相同。

    	bool operator!=(const self& s)//判断两节点地址是否相等
    	{
    		return _node != s._node;
    	}
    	bool operator==(const self& s)//判断两节点地址是否相等
    	{
    		return _node == s._node;
    	}
    

6.完整代码:

	template<class T>
	struct __list_iterator//迭代器对象
	{
		typedef list_node<T> Node;//对节点对象重命名
		typedef __list_iterator<T> self;//对自己重命名

		Node* _node;//创建一个指针

		__list_iterator(Node* node)
			:_node(node)//指针指向传递的节点对象
		{}

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

		T& operator*()//获取节点对象处值,引用返回
		{
			return _node->_data;
		}

		bool operator!=(const self& s)//判断两节点地址是否相等
		{
			return _node != s._node;
		}
		bool operator==(const self& s)//判断两节点地址是否相等
		{
			return _node == s._node;
		}
	};

4.3链表类:

按照需求,实现的逻辑顺序:

1.私有成员:

  • _head(哨兵节点) + _size(链表长度)

  • 哨兵节点不存储数据。

  • 链表长度'_size',在库中实现的list中并没有,加上这个私有成员,主要是方便返回链表的长度,无需再遍历一遍链表来计数链表长度。

    private:
    	Node* _head;//哨兵节点
    	size_t _size;
    

2.重命名:

		typedef list_node<T> Node;//重命名节点
		typedef __list_iterator<T> iterator;//重命名迭代器

3.无参构造函数:

  • list() + empty_init()

  • 库中的无参构造调用了一个empty_init()函数初始化哨兵节点。

  • 初始化哨兵节点,将他的next和prev指针都指向自己即可,然后将链表长度初始化为0。

    	void empty_init()//无参构造初始化哨兵节点
    	{
    		_head = new Node;//开辟一个空节点
    		_head->_next = _head;//头节点的next和prev都指向自己
    		_head->_prev = _head;
    		_size = 0;
    	}
    
    	list()
    	{
    		empty_init();
    	}
    
  1. 插入函数:
  • 通过传过来的迭代器,在迭代器前面位置插入一个节点

  • 成功插入节点后,链表长度加一

  • 返回插入节点的迭代器,防止迭代器失效问题

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

5.删除节点:

  • 断开迭代器位置的节点,并将其前后两个节点相互连接。

  • 删除节点后要将链表长度减一。

    	iterator erase(iterator pos)
    	{
    		Node* cur = pos._node;
    		Node* prev = cur->_prev;
    		Node* next = cur->_next;
    
    		delete cur;
    		prev->_next = next;
    		next->_prev = prev;
    		--_size;
    		return iterator(next);
    	}
    

6.获取链表首尾迭代器:

  • 首节点,就是哨兵节点的下一个节点。

  • 尾节点,就是哨兵节点。

    	iterator begin()
    	{
    		return iterator(_head->_next);
    	}
    	iterator end()
    	{
    		return iterator(_head);
    	}
    

7.头插,尾插:

  • 头插,就是在哨兵节点的后一个节点插入节点,也就是begin()函数得到的迭代器的前一个位置插入节点。

  • 尾插,就是在哨兵节点前插入节点。

  • 复用insert即可。

    	void push_back(const T& x)//尾插,重点在于处理好头尾新节点的指针指向
    	{
    		//第一版,在没有insert的情况下实现的版本
    		//Node* tail = _head->_prev;
    		//Node* newnode = new Node(x);
    
    		//newnode->_data = x;
    		//newnode->_next = _head;
    		//_head->_prev = newnode;
    
    		//tail->_next = newnode;
    		//newnode->_prev = tail;
    
    		//第二版,复用insert
    		insert(end(), x);
    	}
    
    	void push_front(const T& x)
    	{
    		insert(begin(), x);
    	}
    

8.头删,尾删:

  • 和头插,尾插操作的位置相同。

  • 复用erase函数即可。

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

9.清空list对象:

  • 清理除了哨兵节点以外的所有节点。

  • 创建一个迭代器指向第一个节点(哨兵节点后一个节点),当这个迭代器不和哨兵节点重合,就持续删除节点。

  • 动态更新迭代器,由于erase会返回被删除节点的下一个节点,所以让迭代器每次都等于erase的返回值即可。

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

10.析构函数:

  • 析构函数,是删除所有节点,包括哨兵节点。

  • 复用clear函数后,再删除哨兵节点即可。

    	~list()
    	{
    		clear();
    		delete _head;
    		_head = nullptr;
    	}
    

11.list对象节点数量:

		size_t size()
		{
			return _size;
		}

12.拷贝构造:

  • 先用empty_init函数初始化哨兵节点。

  • 再将源对象的数据一个个插入目标对象即可。

    	list(list<T>& l)
    	{
    		empty_init();
    		for (auto a : l)
    		{
    			push_back(a);
    		}
    	}
    

13.=的运算符重载:

  • 两个版本,第一个版本先将目标对象的节点全部删除,然后将源对象每个节点的值插入目标对象即可。

  • 第二个版本,实现一个swap函数,swap函数参数创建一个匿名对象,该匿名对象拷贝构造源对象;通过两个swap交换目标对象的哨兵节点和链表大小。交换完成后匿名对象被销毁。

    	list<T>& operator=(list<int> l)
    	{
    		//if (*this != &l)
    		//{
    		//	clear();
    		//	for (auto a : l)
    		//	{
    		//		push_back(a);
    		//	}
    		//}
    		//return *this;
    
    		//版本二:调用swap函数
    		swap(l);
    		return *this;
    	}
    
    	void swap(list<T>& l)
    	{
    		std::swap(_head, l._head);
    		std::swap(_size, l._size);
    	}
    

14.完整代码:

	template<class T>
	class list//链表类
	{
		typedef list_node<T> Node;
		typedef __list_iterator<T> iterator;
	public:
		void empty_init()//无参构造初始化头节点
		{
			_head = new Node;//开辟一个空节点
			_head->_next = _head;//头节点的next和prev都指向自己
			_head->_prev = _head;
			_size = 0;
		}

		list()
		{
			empty_init();
		}

		list(list<T>& l)
		{
			empty_init();
			for (auto a : l)
			{
				push_back(a);
			}
		}

		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}

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

		list<T>& operator=(list<int> l)
		{
			//if (*this != &l)
			//{
			//	clear();
			//	for (auto a : l)
			//	{
			//		push_back(a);
			//	}
			//}
			//return *this;

			//版本二:调用swap函数
			swap(l);
			return *this;
		}

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


		void push_back(const T& x)//尾插,重点在于处理好头尾新节点的指针指向
		{
			//第一版,在没有insert的情况下实现的版本
			//Node* tail = _head->_prev;
			//Node* newnode = new Node(x);

			//newnode->_data = x;
			//newnode->_next = _head;
			//_head->_prev = newnode;

			//tail->_next = newnode;
			//newnode->_prev = tail;

			//第二版,复用insert
			insert(end(), x);
		}

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

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

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

		iterator begin()
		{
			return iterator(_head->_next);
		}
		iterator end()
		{
			return iterator(_head);
		}

		iterator insert(iterator pos, const T& x)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(x);

			newnode->_prev = prev;
			prev->_next = newnode;

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

			++_size;
			return iterator(newnode);
		}

		iterator erase(iterator pos)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* next = cur->_next;

			delete cur;
			prev->_next = next;
			next->_prev = prev;
			--_size;
			return iterator(next);
		}

		size_t size()
		{
			return _size;
		}

	private:
		Node* _head;//头节点
		size_t _size;
	};

五、const迭代器:

5.1const迭代器和普通迭代器区别:

  • 常规迭代器允许遍历容器并修改容器中的元素。它的使用和指针类似,你可以解引用它来访问和修改元素。
  • const迭代器又名常量迭代器,常量迭代器不允许修改容器中的元素。它只能用于读取元素。这种迭代器用于确保代码的安全性和可读性,防止意外修改元素。
  • 两者的主要区别在于是否允许通过迭代器修改容器中的元素。

5.2const迭代器实现:

  • 要适配const对象和非const对象,就需要写两个版本的迭代器分别对应const对象和非const对象。由于我们写的普通迭代器是一个模板,就需要再写一个const迭代器模板。但是实际上两个模板之间的许多是相同的。

  • 我们可以通过添加模板参数,实现简化代码。

    template<class T, class Ref, class Ptr>

    typedef __list_iterator<T, T&, T*> iterator;
    typedef __list_iterator<T, const T&, const T*> const_iterator;

  • 通过ref和ptr就可以实现通过传过来的参数,实例化具体的模板种类。同一个类模板,会根据传过来的模板参数不同,实例化出不同的类。

  • 就比如以上的两种传模板参数的方式,由于部分模板参数不同,实例化出的就是两个不同的类。

5.3修改*和[]运算符的重载:

  • 由于我们不知道通过模板具体实例化出的是普通迭代器还是const版本的迭代器,所以我们通过模板参数来替代返回类型。

    	Ref operator*()//ref传过来的是T&或const T&
    	{
    		return _node->_data;
    	}
    
    	Ptr operator->()//ptr传过来的是T*或const T*
    	{
    		return &_node->_data;
    	}
    

5.4添加begin()和end()的const版本:

		const_iterator begin() const
		{
			return const_iterator(_head->_next);
		}
		const_iterator end() const
		{
			return const_iterator(_head);
		}

六、打印函数和他的模板:

  • 我们如果要实现打印任意类型的list,就需要使用模板实现print_list()函数。

  • 下面这样写,运行不通过。

    template<typename T>
    void print_list(const list<T>& l)
    {
    	list<T>::const_iterator it = l.begin();
    	while (it != l.end())
    	{
    		cout << *it << ' ';
    		++it;
    	}
    	cout << endl;
    }
    
  • 原因是:在C++中,当你在模板中使用依赖于模板参数的嵌套类型时,例如list<T>::const_iterator,编译器不知道这是一个静态成员或者一个静态函数还是一个类型。

  • 因此需要使用 typename 关键字明确告诉编译器它是一个类型。

    template<typename T>
    void print_list(const list<T>& l)
    {
    	typename list<T>::const_iterator it = l.begin();
    	while (it != l.end())
    	{
    		cout << *it << ' ';
    		++it;
    	}
    	cout << endl;
    }
    
  • 上面是只针对list的打印模板,下面我们升级以下,让这个打印模板可以打印任意类型。

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

七、第二版(添加了const迭代器),完整的头文件代码:

测试函数可以写在demo命名空间中,在测试文件的主函数调用,要注意在测试文件包含要调用到的库。

#pragma once

namespace demo
{
	template<class T>//模板
	struct list_node//节点对象
	{
		T _data;
		list_node<T>* _next;
		list_node<T>* _prev;//一个指向上一个节点,一个指向下一个节点,一个存储节点值

		list_node<T>(const T& x = T())//内置类型也有匿名对象
			:_data(x)
			, _prev(nullptr)
			, _next(nullptr)
		{}
	};

	template<class T, class Ref, class Ptr>//多加两个模板参数对应&和*的模板
	//template<class T>
	struct __list_iterator//迭代器对象
	{
		typedef list_node<T> Node;//对节点对象重命名
		typedef __list_iterator<T, Ref, Ptr> self;
		//typedef __list_iterator<T> self;//对自己重命名

		Node* _node;//创建一个指针

		__list_iterator(Node* node)
			:_node(node)//指针指向传递的节点对象
		{}

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

		Ref operator*()//ref传过来的是T&或const T&
		{
			return _node->_data;
		}

		Ptr operator->()//ptr传过来的是T*或const T*
		{
			return &_node->_data;
		}

		bool operator!=(const self& s)//判断两节点地址是否相等
		{
			return _node != s._node;
		}
		bool operator==(const self& s)//判断两节点地址是否相等
		{
			return _node == s._node;
		}
	};

	template<class T>
	class list//链表对象
	{
		typedef list_node<T> Node;
	public:
		typedef __list_iterator<T, T&, T*> iterator;//普通迭代器的类
		typedef __list_iterator<T, const T&, const T*> const_iterator;//const迭代器的类
		//typedef __list_iterator<T> iterator;
		void empty_init()//无参构造初始化头节点
		{
			_head = new Node;//开辟一个空节点
			_head->_next = _head;//头节点的next和prev都指向自己
			_head->_prev = _head;
			_size = 0;
		}

		list()
		{
			empty_init();
		}

		list(const list<T>& l)//需要先实现const迭代器后,才能使用
		{
			empty_init();
			for (auto a : l)
			{
				push_back(a);
			}
		}

		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}

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

		list<T>& operator=(list<int> l)
		{
			//if (*this != &l)
			//{
			//	clear();
			//	for (auto a : l)
			//	{
			//		push_back(a);
			//	}
			//}
			//return *this;

			//版本二:调用swap函数
			swap(l);
			return *this;
		}

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


		void push_back(const T& x)//尾插,重点在于处理好头尾新节点的指针指向
		{
			//第一版,在没有insert的情况下实现的版本
			//Node* tail = _head->_prev;
			//Node* newnode = new Node(x);

			//newnode->_data = x;
			//newnode->_next = _head;
			//_head->_prev = newnode;

			//tail->_next = newnode;
			//newnode->_prev = tail;

			//第二版,复用insert
			insert(end(), x);
		}

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

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

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

		iterator begin()
		{
			return iterator(_head->_next);
		}
		iterator end()
		{
			return iterator(_head);
		}
		const_iterator begin() const//const迭代器
		{
			return const_iterator(_head->_next);
		}
		const_iterator end() const
		{
			return const_iterator(_head);
		}

		iterator insert(iterator pos, const T& x)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(x);

			newnode->_prev = prev;
			prev->_next = newnode;

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

			++_size;
			return iterator(newnode);
		}

		iterator erase(iterator pos)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* next = cur->_next;

			delete cur;
			prev->_next = next;
			next->_prev = prev;
			--_size;
			return iterator(next);
		}

		size_t size()
		{
			return _size;
		}

	private:
		Node* _head;//头节点
		size_t _size;
	};

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

八、list和vector的比较:

  • list使用双向链表实现,节点存储不连续;vector使用动态数组实现,元素在内存中是连续存储的。
  • vector支持随机访问,访问某个元素效率O(1);list不支持随机访问,访问某个元素
    效率O(n)。
  • vector底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高 ;list底层节点动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低。
  • list不会导致迭代器失效,vector删除、插入数据都会导致迭代器失效。
相关推荐
niu_sama2 分钟前
基于muduo库函数实现protobuf协议的通信
开发语言·qt
不写八个4 分钟前
Qt教程(001):Qt概述与安装
开发语言·qt
quaer10 分钟前
Open-Sora全面开源?
开发语言·算法·机器学习·matlab·矩阵
hakesashou14 分钟前
python如何比较字符串
linux·开发语言·python
coduck_S12004zbj25 分钟前
csp-j模拟五补题报告
c++·算法·图论
Death20030 分钟前
Qt 3D、QtQuick、QtQuick 3D 和 QML 的关系
c语言·c++·qt·3d·c#
yufei-coder39 分钟前
C#基础语法
开发语言·c#·.net
长天一色39 分钟前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
sukalot1 小时前
windows C++-windows C++-使用任务和 XML HTTP 请求进行连接(二)
c++·windows
_.Switch1 小时前
Python机器学习模型的部署与维护:版本管理、监控与更新策略
开发语言·人工智能·python·算法·机器学习