「C++」list的使用及模拟实现

目录

前言

list的使用

迭代器引申

Operations接口

emplace_back初识

sort及性能对比

sort延申less、greater的使用

list模拟实现

成员变量

构造函数

empty_init

默认构造

拷贝构造

迭代器区间构造

initializer_list简易介绍及构造

节点类型

迭代器

insert和erase

迭代器失效

后记

附完整代码


前言

欢迎大家再次来到海盗猫的博客,今天讲解cpp中的list容器

在数据结构中,我们知道链表有单链表和双向链表,而我们今天要学习的list就是一个带有哨兵位的双向链表。

list文档:list - C++ Reference


list的使用

讲解list相关内容使用时的注意点和相关知识点

迭代器引申

在之前的容器学习中,我们已经知道迭代器是一种用来遍历容器的类型,而在vector和string的模拟实现中,iterator迭代器是由原生指针typedef而来,所以可以直接对迭代器进行自增自减以及加减整数个数的距离,以此来让迭代器指向容器的下一个元素。

这是因为string和vector的底层空间是一个连续的结构,但list链表的每个节点的空间是随机的,迭代器不再能随意的加减,说明list与前两者的迭代器类型本质不同,这就引出了迭代器的分类,

迭代器的分类:

迭代器按照不同的功能,性质分类:

随机迭代器为双向和单向迭代器的父类,其使用时为包含关系:

迭代器的性质类型决定了该容器可以使用的算法库#include <algorithm>​中的算法

排序算法为例:

算法库中的sort就只能使用随机迭代器来访问,因为其底层需要对迭代器使用+-​操作;双向迭代器和单向迭代器没有实现这两个操作;

相应的:

元素去重(需要容器有序):

此处标为单向迭代器,实际是因为底层需要++​操作,而双向和随机迭代器都实现了++​操作,所以同样可以正常调用这个函数

查找:

而find中,参数为InputIterator,使用时理解为单向、双向、随机迭代器三种迭代器都可以使用

Operations接口

  1. reverse逆置与算法库中的逆置效果相同;

    cpp 复制代码
    	//ls为链表对象
    	ls.reverse();
    	reverse(ls.begin(), ls.end());
    	//效果相同
  2. sort是list自己实现的成员函数,因为算法库algorithm​中的sort只能使用随机迭代器作为参数调用(见下文sort详解);

  3. merge合并需要两个链表都为有序,且合并完成后,作为参数的链表会变为空。其底层逻辑为取小节点尾插到目标节点中;

    cpp 复制代码
    void test_list3() {
    	list<double> first, second;
    
    	first.push_back(3.1);
    	first.push_back(2.2);
    	first.push_back(2.9);
    
    	second.push_back(3.7);
    	second.push_back(7.1);
    	second.push_back(1.4);
    
    	//任意一个链表无序都将导致merge出错
    	first.sort();
    	second.sort();
    
    	first.merge(second);
    	for (auto i : first) {
    		//cout << i << ' ';
    		printf("%.1lf  ", i);
    	}
    }
  4. unique去重也同样需要链表有序,不然会出现删除不完全的情况(这是因为该函数底层默认认为链表为有序,若相同的数据没有挨在一起,就会识别成不重复)


  5. remove删除,传一个值,若找到就会删除,没有则不执行操作

  6. splice:可以将一个链表,一个链表节点,一个迭代器区间内的链表节点,转移(剪切)到调用对象的指定位置(被转移的位置数据消失);

    测试代码:

    cpp 复制代码
    void test_list4() {
    	list<double> first, second;
    
    	first.push_back(1.1);
    	first.push_back(2.2);
    	first.push_back(3.3);
    
    	second.push_back(1.0);
    	second.push_back(2.0);
    	second.push_back(3.0);
    	second.push_back(4.0);
    	second.push_back(5.0);
    	second.push_back(6.0);
    
    	cout << "first: ";
    	for (auto i : first) {
    		printf("%.1lf  ", i);
    	}
    	cout << endl;
    	cout << "second: ";
    	for (auto i : second) {
    		printf("%.1lf  ", i);
    	}
    	cout << endl;
    	//剪切整个list
    	first.splice(first.end(), second);
    
    	////剪切一个
    	//list<double>::iterator it = second.begin();
    	//int n = 1;
    	//while (n--) it++;
    	//first.splice(first.begin(), second, it);
    
    	////一个区间
    	//first.splice(first.begin(), second, it, second.end());
    
    
    	cout << "splice after: \n";
    	cout << "first: ";
    	for (auto i : first) {
    		printf("%.1lf  ", i);
    	}
    	cout << endl;
    	cout << "second: ";
    	for (auto i : second) {
    		printf("%.1lf  ", i);
    	}
    }

emplace_back初识

目前由于其底层涉及右值引用,所以目前只需要知道其 用法与push_back​类似即可,而相较于push_back​,多了一种用法

cpp 复制代码
class A {

	int _a1 = 0;
	int _a2 = 0;
public:
	A(const int& a1 = 0, const int& a2 = 0)
		:_a1(a1),
		_a2(a2)
	{
		cout << "调用:A(const int& a1 = 0, const int& a2 = 0)" << endl;
	}
	A(const A& a) {
		_a1 = a._a1;
		_a2 = a._a2;
		cout << "调用:A(const A& a)" << endl;
	}


};

void test_list1() {
	list<A> ls;
	A a1(1, 1);
	ls.push_back(a1);
	A a2(2, 2);
	ls.push_back(a2);
	//push_back只能用同类型对象来进行插入
	//ls.push_back(3,3);
	A a3(3, 3);
	ls.emplace_back(a3);
	//emplace_back就可以通过直接传入构造函数的参数来使用,会直接调用构造后插入
	ls.emplace_back(4, 4);
}

可以看到emplace_back​ 不仅可以像push_back​ 一样,传入同类型对象来插入;还可以直接将默认构造的参数传给emplace_back ,其会自动调用构造后插入

sort及性能对比

由于算法库<algorithm>​中sort不支持双向迭代器,所以list自己实现了一个sort成员函数来进行排序。

这样,即便list不支持调用库中的sort也可以进行排序了;但当数据较大时,直接调用list的成员函数sort却存在性能问题:

测试代码(VS2022 Release下):

cpp 复制代码
void test_sort() {
	srand(time(0));
	const int N = 1000000;

	list<int> ls1;
	list<int> ls2;

	for (int i = 0; i < N; i++) {
		auto e = rand() + i;
		ls1.push_back(e);
		ls2.push_back(e);
	}

	int begin1 = clock();

	//使用list迭代器区间构造vector
	vector<int> v(ls1.begin(), ls1.end());
	//利用vector迭代器为随机迭代器的特点,来调用算法库中的sort
	sort(v.begin(), v.end());
	//assign可以插入一个迭代器区间
	ls1.assign(v.begin(), v.end());

	int end1 = clock();
	int begin2 = clock();

	ls2.sort();

	int end2 = clock();

	cout << "list copt vector sort list: " << end1 - begin1 <<endl;
	cout << "list sort: " << end2 - begin2;
}

可以看到,当数据量较大时,即便是将list转换为vector排序再转换回来,用时依然比直接使用list的成员函数sort来的效率高

sort延申less、greater的使用

由于在排序算法中,都默认为升序,所以可以使用仿函数less和greater来实现降序排列

less和greater都是类,可以定义出该类型的对象,作为参数传给sort函数,达到升序和降序排列的效果

cpp 复制代码
void test_list2() {
	list<int> ls;
	ls.push_back(1);
	ls.push_back(5);
	ls.push_back(2);
	ls.push_back(3);
	ls.push_back(7);
	ls.push_back(4);
	ls.push_back(10);

	//algorithm库中sort算法需要随机调用,但此处还时不会提示错误
	//sort(ls.begin(), ls.end());//运行时出错

	//less<int> le;//降序
	//greater<int> ge;//升序
	//ls.sort(ge);
	ls.sort(greater<int>());//使用匿名对象
	//ls.sort(less<int>());//使用匿名对象
	for (auto i : ls) {
		cout << i << ' ';
	}
}

list模拟实现

由于库中的list实现为带有哨兵位的双向链表结构,且链表的空间结构不再是string和vector中的顺序结构,这将导致模拟实现时的许多不同。

成员变量

由于list为带有哨兵位的双向链表,所以对于list自身,只有一个哨兵位节点的指针Node* _head = nullptr;​,而list节点都是通过单独封装一个类来实现的;也因此,list的默认构造函数只需要使用_head成员创建一个哨兵位节点即可

构造函数

list类型中只含有一个哨兵位节点指针,所以不管哪种构造前提都要先构造出哨兵位节点,因此使用一个函数来构造哨兵位节点,这样其他类型的构造也可以调用

empty_init

cpp 复制代码
//只用来构造出哨兵位节点
void empty_init() {
	_head = new Node;
	_head->_next = _head;
	_head->_prev = _head;
}

默认构造

cpp 复制代码
list() {
	empty_init();
}

拷贝构造

cpp 复制代码
list(const list<T>& ls) {
	//构造一个哨兵位并将list中的节点尾插赋值到这个哨兵位后
	//使用空构造
	empty_init();
	for (auto n : ls) {
		push_back(n);
	}
}

迭代器区间构造

cpp 复制代码
//迭代器区间构造
template <class Iterator>
list(Iterator first, Iterator last) {
	empty_init();
	while (first != last) {
		push_back(*first);
		++first;
	}
}

initializer_list简易介绍及构造

c++11中引入了这个类型模板,用以表示大括号括起来的一组数据,这个类型也具有迭代器,而其他的容器都可以使用这种类型来进行初始化

cpp 复制代码
auto il1 = { 1,2,3,4,5 };
auto il2 = { 'a','b','c','d','e'};
cout << typeid(il1).name() << endl;
std::list<int> ls(il1);
print_container(ls);
std::vector<int> v(il1);
print_container(v);
std::string s(il2);
print_container(s);

所以我们模拟实现时,就可以直接使用initializer_list的迭代器来插入数据到链表

cpp 复制代码
	list(initializer_list<T> il) {
		empty_init();
		for (auto& i : il) {
			push_back(i);
		}
	}

节点类型

list中每个节点都是一个独立的空间,所以每次插入一个数据,都是创建一个新节点,再链入list中

cpp 复制代码
	template<class T>
	struct _list_node
	{
		typedef _list_node<T> Node;
		T _data;//存储数据
		Node* _next;//下一个节点的指针
		Node* _prev;//上一个节点的指针

		//初始化节点
		_list_node(const T& data = T())
			:_data(data)
			,_next(nullptr)
			,_prev(nullptr)
		{}
	}; 

迭代器

list中,由于物理结构上每个节点的存储空间不再连续,如果像string和vector中(迭代器底层为原生指针)一样,直接对迭代器++ --​等操作,将会导致结果错误;

所以此时list迭代器底层不再是简单的原生指针typedef而来;而是将一个节点的指针封装起来的类型,用以在其中用重载的方法,实现模拟原生指针的各种操作

模拟实现iterator​:

cpp 复制代码
template<class T>
struct _list_iterator {
	typedef _list_node<T> Node;
	typedef _list_iterator<T> Self;
	Node* _node;
	_list_iterator() = default;
	_list_iterator(Node* node)
		:_node(node)
	{}

	//operator*返回data数据
	T& operator*() {
		return _node->_data;
	}
	//operator->返回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& it) {
		return _node == it._node;
	}
	bool operator!=(const Self& it) {
		return _node != it._node;
	}
};

然后在list类中typedef _list_iterator iterator

此时若想实现const_iterator​可以直接复制iterator​后修改类型名,将operator*​和operator->​的返回值加上const,再在list类中typedef _list_const_iterator const_iterator​即可;

但这样的方法会,两个类的代码极其相似,仅仅是俩个重载函数的返回值有无const​和类型名不同,导致代码冗余。而我们又知道,模板函数模板类不就是用来解决类似这样的问题的吗?由此我们可以得出,或许模板可以帮我们解决这个问题。

我们++引入两个新的模板参数Ref、Ptr让他们来分别对应operator*和operator->的返回值++:

cpp 复制代码
	template<class T, class Ref, class Ptr>
	struct _list_iterator {
		typedef _list_node<T> Node;
		typedef _list_iterator<T, Ref, Ptr> Self;
		Node* _node;
		_list_iterator() = default;
		_list_iterator(Node* node)
			:_node(node)
		{}

		//operator*返回data数据
		Ref operator*() {
			return _node->_data;
		}
		//operator->返回data数据的指针
		Ptr 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& it) {
			return _node == it._node;
		}
		bool operator!=(const Self& it) {
			return _node != it._node;
		}
	};

同时,在list的typedef中,将后两个参数的类型对应为T&和T*、const T&和const T*

cpp 复制代码
typedef _list_iterator<T, T&, T*> iterator;
typedef _list_iterator<T, const T&, const T*> const_iterator;

这样,当外部调用iterator​的begin和end接口时,就会传入T&和T*从而使迭代器的*和->的结果可修改;调用const_iterator的begin和end时,则传入const T&和const T*,致使*和->的返回结果不能修改,从而实现普通迭代器和const迭代器的区分。

insert和erase

cpp 复制代码
//list的insert不会产生迭代器失效,但库中有返回值,返回新插入的节点的迭代器
iterator insert(iterator pos, const T& data) {
	//创建新节点
	Node* newNode = new Node(data);
	//记录pos位置前节点
	Node* prev_node = pos._node->_prev;
	//链入指定位置
	newNode->_next = pos._node;
	newNode->_prev = prev_node;
	prev_node->_next = newNode;
	pos._node->_prev = newNode;
	return newNode;
}
cpp 复制代码
//erase后指向当前节点的迭代器失效(野指针),返回下一个节点的迭代器
iterator erase(iterator pos) {
	assert(pos != end());
	//记录删除节点的前后节点地址
	Node* prev_node = pos._node->_prev;
	Node* next_node = pos._node->_next;
	delete pos._node;

	prev_node->_next = next_node;
	next_node->_prev = prev_node;

	return next_node;
}

迭代器失效

list中insert不再有迭代器失效的问题,因为每个节点的空间不再连续,即便在pos位置插入新节点,原本迭代器指向的空间位置也不会发生改变,而且list也没有扩容这一说,但基于库中的实现逻辑,insert会返回插入的新节点的地址;

而erase,因为会删除pos位置的节点,再链接前后节点,这导致指向删除节点的位置的迭代器变为一个野指针,导致迭代器失效,所以erase后需要更新指针指向,erase会返回删除节点的下一个节点地址

后记

关于list相关的内容我们就介绍到这里,上文中没有提及的接口,有些现阶段掌握不住,有些则于之前的容器相似,较为简单,需要请查阅附完整代码来理解其底层

本期专栏:C++_海盗猫鸥的博客-CSDN博客

个人主页:海盗猫鸥-CSDN博客

本篇博客就到这里,我们下期见~

附完整代码

++list.h++

cpp 复制代码
#pragma once
#include<iostream>
#include<assert.h>
using std::swap;
using std::initializer_list;
namespace hdmo{
	//list节点
	template<class T>
	struct _list_node
	{
		typedef _list_node<T> Node;
		T _data;//存储数据
		Node* _next;//下一个节点的指针
		Node* _prev;//上一个节点的指针

		//初始化节点
		_list_node(const T& data = T())
			:_data(data)
			,_next(nullptr)
			,_prev(nullptr)
		{}
	}; 

	//迭代器,一个被封装起来的节点指针
	//引入额外的模板参数,用于作为operator*和operator->的返回值
	template<class T, class Ref, class Ptr>
	struct _list_iterator {
		typedef _list_node<T> Node;
		typedef _list_iterator<T, Ref, Ptr> Self;
		Node* _node;
		_list_iterator() = default;
		_list_iterator(Node* node)
			:_node(node)
		{}

		//operator*返回data数据
		Ref operator*() {
			return _node->_data;
		}
		//operator->返回data数据的指针
		Ptr 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& it) {
			return _node == it._node;
		}
		bool operator!=(const Self& it) {
			return _node != it._node;
		}
	};

	//template<class T>
	//struct _list_const_iterator {
	//	typedef _list_node<T> Node;
	//	typedef _list_const_iterator<T> Self;
	//	Node* _node;
	//	_list_const_iterator() = default;
	//	_list_const_iterator(Node* node)
	//		:_node(node)
	//	{}

	//	//返回data数据
	//	const T& operator*(){
	//		return _node->_data;
	//	}
	//	const 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& it) {
	//		return _node == it._node;
	//	}
	//	bool operator!=(const Self& it) {
	//		return _node != it._node;
	//	}
	//};

	//list主体
	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;
		//空构造,用于创建哨兵位
		void empty_init() {
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
		}
		//默认构造
		list() {
			empty_init();
		}
		//拷贝构造
		list(const list<T>& ls) {
			//构造一个哨兵位并将list中的节点尾插赋值到这个哨兵位后
			//_head = new Node;
			//_head->_next = _head;
			//_head->_prev = _head;

			//使用空构造
			empty_init();
			for (auto& n : ls) {
				push_back(n);
			}
		}
		list(initializer_list<T> il) {
			empty_init();
			for (auto& i : il) {
				push_back(i);
			}
		}
		//迭代器区间构造
		template <class Iterator>
		list(Iterator first, Iterator last) {
			empty_init();
			while (first != last) {
				push_back(*first);
				++first;
			}
		}
		//n个value构造
		list(int n, const T& value = T()) {
			empty_init();
			while (n--) {
				push_back(value);
			}
		}
		//赋值
		list<T>& operator=(list<T> ls) {
			swap(ls);
			return *this;
		}
		//析构
		~list() {
			//清楚所有节点,最后销毁哨兵位
			clear();
			delete _head;
			_head = nullptr;
		}

		//交换,交换两个list的成员
		void swap(list<T>& ls) {
			std::swap(_head, ls._head);  
		}
		
		iterator begin() {
			return _head->_next;
		}
		iterator end() {
			return _head;
		}
		const_iterator begin() const {
			return _head->_next;
		}
		const_iterator end() const {
			return _head;
		}

		void push_back(const T& data) {
			////在哨兵位前插入
			//Node* newNode = new Node(data);

			//Node* tail = _head->_prev;//原本的尾节点
			//newNode->_next = _head;
			//newNode->_prev = tail;
			//tail->_next = newNode;
			//_head->_prev = newNode;
			insert(end(), data);
		}
		void push_front(const T& data) {
			insert(begin(), data);
		}
		void pop_back() {
			erase(--end());
		}
		void pop_front() {
			erase(begin());
		}

		//void insert(iterator pos, const T& data) {
		//	//创建新节点
		//	Node* newNode = new Node(data);
		//	//记录pos位置前节点
		//	Node* prev_node = pos._node->_prev;
		//	//链入指定位置
		//	newNode->_next = pos._node;
		//	newNode->_prev = prev_node;
		//	prev_node->_next = newNode;
		//	pos._node->_prev = newNode;
		//}
		
		//list的insert不会产生迭代器失效,但库中有返回值,返回新插入的节点的迭代器
		iterator insert(iterator pos, const T& data) {
			//创建新节点
			Node* newNode = new Node(data);
			//记录pos位置前节点
			Node* prev_node = pos._node->_prev;
			//链入指定位置
			newNode->_next = pos._node;
			newNode->_prev = prev_node;
			prev_node->_next = newNode;
			pos._node->_prev = newNode;
			return newNode;
		}
		//void erase(iterator pos) {
		//	assert(pos != end());
		//	//记录删除节点的前后节点地址
		//	Node* prev_node = pos._node->_prev;
		//	Node* next_node = pos._node->_next;
		//	delete pos._node;

		//	prev_node->_next = next_node;
		//	next_node->_prev = prev_node;
		//}
		
		//erase后指向当前节点的迭代器失效(野指针),返回下一个节点的迭代器
		iterator erase(iterator pos) {
			assert(pos != end());
			//记录删除节点的前后节点地址
			Node* prev_node = pos._node->_prev;
			Node* next_node = pos._node->_next;
			delete pos._node;

			prev_node->_next = next_node;
			next_node->_prev = prev_node;

			return next_node;
		}


		T& front() {
			return _head->_next->_data;
		}
		const T& front()const {
			return _head->_next->_data;
		}
		T& back() {
			return _head->_prev->_data;
		}
		const T& back()const {
			return _head->_prev->_data;
		}

		size_t size()const {
			auto it = begin();
			size_t n = 0;
			while (it != end()) {
				++n;
				++it;
			}
			return n;
		}
		//删除所有数据节点
		void clear() {
			auto it = begin();
			while (it != end()) {
				it = erase(it);//erase的返回值就是指向下一个节点的
			}
		}
		bool empty() {
			return _head->_next == _head;
		}

	private:
		Node* _head = nullptr;//哨兵位指针
	};
}
相关推荐
xlq223224 小时前
15.list(上)
数据结构·c++·list
云帆小二4 小时前
从开发语言出发如何选择学习考试系统
开发语言·学习
光泽雨5 小时前
python学习基础
开发语言·数据库·python
Elias不吃糖5 小时前
总结我的小项目里现在用到的Redis
c++·redis·学习
AA陈超5 小时前
使用UnrealEngine引擎,实现鼠标点击移动
c++·笔记·学习·ue5·虚幻引擎
百***06015 小时前
python爬虫——爬取全年天气数据并做可视化分析
开发语言·爬虫·python
jghhh016 小时前
基于幅度的和差测角程序
开发语言·matlab
fruge6 小时前
自制浏览器插件:实现网页内容高亮、自动整理收藏夹功能
开发语言·前端·javascript
No0d1es6 小时前
电子学会青少年软件编程(C/C++)六级等级考试真题试卷(2025年9月)
c语言·c++·算法·青少年编程·图形化编程·六级
曹牧6 小时前
Java中处理URL转义并下载PDF文件
java·开发语言·pdf