C++基础 [八] - list的使用与模拟实现

news2025/3/25 21:44:13

目录

list的介绍

List的迭代器失效问题

List中sort的效率测试

list 容器的模拟实现思想

模块分析

作用分析

list_node类设计 

list 的迭代器类设计

迭代器类--存在的意义

迭代器类--模拟实现

模板参数 和 成员变量

构造函数 

* 运算符的重载

++运算符的重载  

 -- 运算符的重载

 重载 != 和 ==

 -> 运算符的重载 

list 结构的完善

默认成员函数

构造函数

拷贝构造 

迭代器区间构造  

n个相同元素构造  

赋值重载 

析构函数 

迭代器相关函数

begin 和 end  

访问容器相关函数

fron 和 back 

增删改查相关函数

insert

earse 

push_back 和 pop_back

push_front 和 pop_front

容量相关函数

size

resize 

empty

clear

list 容器的模拟实现整体代码

list.h 

list.cpp


list的介绍

1. list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。

2. list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。

3. list与forward_list非常相似:最主要的不同在于forward_list是单链表,只能朝前迭代,已让其更简单高效。

4. 与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率更好。

5. 与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大list来说这可能是一个重要的因素

List的迭代器失效问题

我们之前学习vector的时候,知道了insert和erase都有可能存在迭代器失效的问题,那list会出现这种情况吗??下面来进行分析

insert插入新节点的迭代器,因此insert不可能会出现失效的问题。

 而earse必然会失效,因为该迭代器对应的节点被删除了。如果我们想继续用的话,就得利用返回值去更新迭代器,返回值是被删除元素的下一个位置的迭代器。

List中sort的效率测试

void test_op()
{
	srand((unsigned int)time(NULL));
	const int N = 1000000;
	vector<int> v;
	v.reserve(N);
	list<int> lt1;
	list<int> lt2;
	for (int i = 0; i < N; ++i)
	{
		int e = rand();
		lt1.push_back(e);
		lt2.push_back(e);
	}
	// 拷贝到vector排序,排完以后再拷贝回来
	int begin1 = clock();
	for (auto e : lt1)
	{
		v.push_back(e);
	}
	sort(v.begin(), v.end());
	size_t i = 0;
	for (auto& e : lt1)
	{
		e = v[i++];
	}
	int end1 = clock();
	//list调用自己的sort
	int begin2 = clock();
	lt2.sort();
	int end2 = clock();
	printf("vector sort:%d\n", end1 - begin1);
	printf("list sort:%d\n", end2 - begin2);
}

会发现哪怕我先拷贝到vector排完再拷贝回去效率都比list的sort效率高,所以list的sort实际中意义不是很大!!

list 容器的模拟实现思想

模块分析

根据 list 容器图可以分析一下 模拟实现 list容器 都要准备什么?

  • 存储元素需要结点--->结点类
  • 使用迭代器访问结点--->迭代器类
  • 总体--->list类

作用分析

节点类

作用:存储 list容器 的元素,因为list里面要存储各种类型的元素,所以结点类需要定义成模版。结点类中的成员变量则是有三个,分别是:指向前一个结点的_prev指针,指向后一个结点的_next指针,存储结点元素_data变量。

迭代器类 

  • 此时大家可以思考这样一个问题,在模拟实现vector类时,我们是直接用结点指针作为迭代器来使用的,并没有自己实现迭代器类。list中为什么需要单独实现迭代器类?

原因:如上图所示。vector容器是数组,它的空间是连续的,所以结点指针完全可以通过自增的方式来指向下一个结点。可是list容器是链表,它的空间并不连续,自然不可能直接通过结点指针的自增来指向下一个链表结点,所以我们才需要自己实现迭代器类,并且重载自增与自减运算符,这样就可以通过迭代器的自增或自减来指向前后结点了。 

list类

 

 作用:实现链表各项功能的类,为主要部分 

list_node类设计 

list本身 和 list的结点 是两个不同的结构,需要分开设计。以下是list的节点结构

首先,我们在自己的命名空间内模拟实现 list(为了防止与库冲突),上面的代码就是list节点的结构在这里并没有使用 class,因为 struct 默认访问权限是 public,又因为节点是需要经常访问的,所以使用struct更好。但是此结构体非C语言的结构体,已经是类了

list 的迭代器类设计

迭代器类--存在的意义

之前 模拟实现 string 和 vector 时都没有说要实现一个迭代器类为什么实现list的时候就需要实现一个迭代器类了呢?

因为 string 和 vector 对象都将其数据存储在了一块连续的内存空间,我们通过指针进行自增、自减以及解引用等操作,就可以对相应位置的数据进行一系列操作,因此string和vector当中的迭代器就是原生指针。

但是对于 list 来说,其各个结点在内存当中的位置是随机的,并不是连续的我们不能仅通过结点指针的自增、自减以及解引用等操作对相应结点的数据进行操作。

  •  而迭代器的意义就是,让使用者可以不必关心容器的底层实现,可以用简单统一的方式对容器内的数据进行访问。
  • 既然 list 结点指针的行为不满足迭代器定义,那么我们可以对这个结点指针进行封装对结点指针的各种运算符操作进行重载,使得我们可以用和string和vector当中的迭代器一样的方式使用list当中的迭代器。例如,当你使用 list 当中的迭代器进行自增操作时,实际上执行了p = p->next语句,只是你不知道而已。

迭代器类--模拟实现

模板参数 和 成员变量

我们知道指针有两种const形势,一种是const T* ptr1;另一种是T* const  ptr2,那我们的const的迭代器是哪一种呢?

我们知道const迭代器是可以++的,它只是限制内容不能修改,但是它指针本身是可以++的。所以肯定是const T* prt1这种形式。T* const  ptr2的const本身修饰的是指针不能修改的

typedef const  _list_iterator<T> const_iterator;那我们这样写能行吗?

不行的,这样就是迭代器本身不能修改了。我们要的是它指向的内容不能修改的,那该怎么办呢?

我们只需要让它重载的*加上const就可以了 const T& operator*() 这样当调用*的时候,就会提示不能修改内容了。

但是这样设计太臃肿了。我们发现const_iterator和普通的iterator只有operator*()的返回值不一样

那我们能不能通过一个类型去控制这个返回值呢?肯定是可以的,我们可以增加个模板参数

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 分别代表的是 引用类型(T&) 和 指针类型(T *)。 
  • 当我们使用 普通迭代器 时,编译器就会实例化出一个 普通迭代器 对象;当我们使用 const迭代器时,编译器就会实例化出一个 const迭代器对象。 
  • 若该迭代器类不设计三个模板参数,那么就不能很好的区分-- 普通迭代器 和-- const迭代器。 

 因为 结点类 和 迭代器类 自身的类型名太长,写起来太麻烦,所以我们用 typedef关键字 给这两个类型取了别名

我们为 结点类 的类型取的别名是 Node,为 迭代器类 取的别名是 Self

template<typename T, typename Ref, typename Ptr>
struct _list_iterator 
{
	typedef ListNode<T> Node;                //为结点类取别名
	typedef _list_iterator<T, Ref, Ptr> self;  //为正向迭代器类取别名
 
    //成员变量
	Node* _node;                             //指向结点的指针
}

看这个代码 list<int>::iterator it = lt.begin(); 这里会发生拷贝构造,因为我们没有自己写,所以编译器会调用默认拷贝构造。对于自定义类型的默认拷贝构造是值拷贝(浅拷贝)。那会有问题吗?

答案是否定的我们要的就是浅拷贝。对于 迭代器 来说,浅拷贝通常是完全合适的,因为:

  • 迭代器本身只是容器中的一个元素的访问者,它并不拥有实际的容器数据。它只是持有容器中元素的位置(指针或类似机制),而不直接管理数据。
  • 所以拷贝一个迭代器通常不会影响容器的所有权或内存的生命周期,因为它只是一个指向容器的指针,拷贝它只是创建一个新的指针来访问同样的容器数据。
  • 容器本身会负责管理数据,迭代器只需要能够访问和遍历这些数据,所以浅拷贝是安全且有效的。

那为什么it和begin()都指向了第一个节点,不崩溃呢?

这个时候我们就要想想值拷贝崩溃的原因是什么了。以前我们了解到值拷贝崩溃的原因是进行了两次析构。但是迭代器我们没写析构函数呀。因为这个节点不是属于迭代器的,迭代器不是要做管理节点的工作,它只是访问节点 

构造函数 

迭代器类 实际上就是对 结点指针进行了封装,其成员变量就只有一个,那就是结点指针,其构造函数直接根据所给结点指针构造一个迭代器对象即可。

//正向迭代器构造函数
_list_iterator(Node* node = nullptr)       // 默认构造函数
			:_node(node)
		{}

* 运算符的重载

当我们使用 解引用操作符 时,是想得到该位置的数据内容。因此,我们直接返回当前结点指针所指结点的数据即可,但是这里需要使用引用返回因为解引用后可能需要对数据进行修改。

//重载*
//返回迭代器指向的结点的值域
 
// T& operator*()
 
Ref operator*() 
{
	return _node->_data;
}

++运算符的重载  

自增运算符的 重载 是迭代器类的核心前置++重载中,要让当前迭代器指向下一个结点后,再把迭代器返回后置++中是把当前迭代器用临时变量保存一份,再把迭代器指向下一个结点,然后返回临时变量。注意:重载后置++或后置--时,必须在函数参数列表加一个int变量,这是语法规定。

  • 重载前置++ 和 后置++ 时的返回值有所不同前置++返回值类型是--------迭代器类型的引用,而后置++返回值类型是------ 迭代器类型。
  • 前置++中,返回的是对 this 的解引用this并不是局部变量函数结束后依然存在,所以可以返回它的引用,减少值拷贝次数。
  • 后置++中,返回的 temp 是函数中创建的局部对象,在函数结束后会被销毁所以返回值类型不可以是引用。这里就必须通过值拷贝来返回值。
//重载前置++
//返回迭代器对象自身的引用
//因为对象自身并不是该函数中的局部对象
 
self& operator++() 
{
	_node = _node->_next;
	return *this;
}
//重载后置++
//此时需要返回temp对象,而不是引用
//因为temp对象是局部的对象
//函数结束后就被释放
 
self operator++(int a) 
{
	self temp(*this);
	_node = _node->_next;
	return temp;
}

 -- 运算符的重载

前置--和后置--关于函数的返回类型跟重载++类似,这里就不再赘述。

 //重载前置--
self& operator--() 
{
	_node = _node->_prev;
	return *this;
}
//重载后置--
self operator--(int a) 
{
	self temp(*this);
	_node = _node->_prev;
	return temp;
}

 重载 != 和 ==

这里只需要比较_node是否相同即可,因为_node本身就是指向结点的指针,保存着结点的地址,只要地址相同,那自然就是同一个结点了

//重载!=
bool operator!=(const self& s)const 
{
	return _node != s._node;
}
 
//重载==
bool operator==(const self& s)const 
{
	return _node == s._node;
}

 -> 运算符的重载 

   有时候,实例化的模板参数是自定义类型,我们想要像 指针 一样访问访问自定义类型力的成员变量,这样显得更通俗易懂,所以就要重载 -> 运算符它的返回值是 T* 

想想如下场景: 

当 list容器 当中的每个结点存储的不是内置类型,而是自定义类型,例如数据存储类,那么当我们拿到一个位置的迭代器时,我们可能会使用 ->运算符访问 Data 的成员: 

struct Data
{
	Data(int a = int(), double b = double(), char c = char())
		:_a(a)
		, _b(b)
		, _c(c)
	{}
 
	int _a;
	double _b;
	char _c;
};
 
void TestList()
{
	list<Data> lt;
	lt.push_back(Data(1, 2.2, 'A'));
 
	auto it = lt.begin();
	cout << (*it)._a << endl;	//不使用 operator->() 比较别扭
	cout << it.operator->()->_b << endl;	//这种写法是真实调用情况
	cout << it->_c << endl;	//编译器直接优化为 it->
}
 
int main()
{
	TestList();
	return 0;
}

list 结构的完善

成员变量和模板参数

  • 因为 list 可以存储各种类型的元素,因此 list 类要设置为模板,T就是存储的元素的类型
  • 因为 结点类 和 迭代器类 的类名太长,所以用 typedef 关键为它们取了别名。这里迭代器的三个参数之所以设置为<T , T& , T*>,是因为list类只给出了一个模板参数,而迭代器类应该有三个,因此用 T& 和 T* 作为另外两个参数。
//带头结点的双向链表
template<class T>
class list 
{
	typedef ListNode<T> Node;
public:
	typedef _list_iterator<T, T&, T*> Iterator;                   //正向迭代器
    typedef _list_iterator<T, const T&, const T*> const_iterator;
private:
	Node* _head;              //指向头结点的指针
}

默认成员函数

构造函数

list 的成员变量是 一个节点类,在构造头节点时,需要将这单个头节点构造成一个双向循环链表;

//构造函数
list()
{
	_head = new Node;     //new一个节点出来
	_head->_prev = _head; 
	_head->_next = _head; //_prev 和 _next 同时指向了头结点,形成了双向循链表
}

拷贝构造 

拷贝构造是用一个已有对象去构造出另一个对象,首先将待构造对象进行初始化,然后利用迭代器区间去构造一个和 lt1 一样的临时的 tmp 对象,再进行数据的交换,达到深拷贝的目的。  

//拷贝构造 --- 现代写法 lt2(lt1)
list(const list<T>& lt)
{
	_head = new Node;
	_head->_prev = _head;
	_head->_next = _head;
	list<T> tmp(lt.begin(), lt.end());
	std::swap(_head, tmp._head);
}

迭代器区间构造  

由于list可以存储各种类型的元素,所以区间构造时自然也会用到各种类型的迭代器,因此区间构造也应该定义为模版,需要给出模版参数列表。具体实现和上一个函数是差不多的。

//迭代器区间构造
template<class iterator>
list(iterator first, iterator last)
{
	_head = new Node;
	_head->_prev = _head;
	_head->_next = _head;
 
	while (first != last)
	{
		push_back(*first);//尾插数据,会根据不同类型的迭代器进行调用
		++first;
	}
}

n个相同元素构造  

通过用 n 个 val 来对对象进行初始化,需要注意这里的 T( )是一个匿名对象,作为 val 的缺省参数,因为我们并不知道传给val的是一个对象还是一个整形(或其他),给缺省参数的好处在于,对于自定义类型编译器会去调用自定义类型的构造函数来对val进行初始化,如果是内置类型,它也是有自己的构造函数

//用n个val个构造      			   
list(int n, const T& val = T())        			   
{												   
	_head = new Node;
	_head->_prev = _head;
	_head->_next = _head;
	for (int i = 0; i < n; i++)
	{
		push_back(val);
	}
}

赋值重载 

将赋值运算符重载的参数定义为 list 类型的对象而不是对象的引用,传参时会发生值拷贝。因此我们可以把 list对象 的 this指针 和 拷贝出来的参数 L 指向头结点的指针交换,这样 this指针 就直接指向了拷贝出来的L的头结点。L则指向了list对象的头结点在函数结束后,作为局部对象的L将被销毁,它指向的空间也会被释放。

//传统写法
list<T>& operator=(const list<T>& lt)
{
	if (this != &lt) //避免自己给自己赋值
	{
		clear(); //清空容器
		for (const auto& e : lt)
		{
			push_back(e); //将容器lt当中的数据一个个尾插到链表后面
		}
	}
	return *this; //支持连续赋值
}

析构函数 

对对象进行析构时,首先调用clear函数清理容器当中的数据,然后将头结点释放,最后将头指针置空即可。

//析构函数
~list()
{
	clear(); //清理容器
	delete _head; //释放头结点
	_head = nullptr; //头指针置空
}

迭代器相关函数

begin 和 end  

首先我们应该明确的是:begin 函数返回的是第一个有效数据的迭代器,end函数返回的是最后一个有效数据的下一个位置的迭代器。

对于 list 这个 带头双向循环链 来说,其第一个有效数据的迭代器就是使用头结点后一个结点的地址构造出来的迭代器,而其最后一个有效数据的下一个位置的迭代器就是使用头结点的地址构造出来的迭代器。(最后一个结点的下一个结点就是头结点)

iterator begin()
{
	//返回使用头结点后一个结点的地址构造出来的普通迭代器
	return iterator(_head->_next); //这里也可以是隐式类型转换
}
iterator end()
{
	//返回使用头结点的地址构造出来的普通迭代器
	return iterator(_head);
}
  • 当然,还需要重载一对用于 const对象 的 begin函数 和 end函数。
const_iterator begin() const
{
	//返回使用头结点后一个结点的地址构造出来的const迭代器
	return const_iterator(_head->_next);
}
const_iterator end() const
{
	//返回使用头结点的地址构造出来的普通const迭代器
	return const_iterator(_head);
}

访问容器相关函数

fron 和 back 

front 和 back 函数分别用于获取第一个有效数据和最后一个有效数据,因此,实现front和back函数时,直接返回第一个有效数据和最后一个有效数据的引用即可。 

T& front()
{
	return *begin(); //返回第一个有效数据的引用
}
T& back()
{
	return *(--end()); //返回最后一个有效数据的引用
}
  • 当然,这也需要重载一对用于const对象 的front函数 和 back函数,因为 const对象 调用front和back函数后所得到的数据不能被修改。
const T& front() const
{
	return *begin(); //返回第一个有效数据的const引用
}
 
const T& back() const
{
	return *(--end()); //返回最后一个有效数据的const引用
}

增删改查相关函数

insert

insert函数可以在所给迭代器之前插入一个新结点。 

  • 先根据所给迭代器得到该位置处的结点指针cur,然后通过cur指针找到前一个位置的结点指针prev,接着根据所给数据x构造一个待插入结点,之后再建立新结点与cur之间的双向关系,最后建立新结点与prev之间的双向关系即可。
//插入函数
void insert(iterator pos, const T& x)
{
	assert(pos._node); //检测pos的合法性
 
	node* cur = pos._node; //迭代器pos处的结点指针
	node* prev = cur->_prev; //迭代器pos前一个位置的结点指针
	node* newnode = new Node(x); //根据所给数据x构造一个待插入结点
 
	//建立newnode与cur之间的双向关系
	newnode->_next = cur;
	cur->_prev = newnode;
	//建立newnode与prev之间的双向关系
	newnode->_prev = prev;
	prev->_next = newnode;
}

earse 

先根据所给迭代器得到该位置处的结点指针cur,然后通过cur指针找到前一个位置的结点指针prev,以及后一个位置的结点指针next,紧接着释放cur结点,最后建立prevnext之间的双向关系即可。

//删除函数
iterator erase(iterator pos)
{
	assert(pos._node); //检测pos的合法性
	assert(pos != end()); //删除的结点不能是头结点
 
	node* cur = pos._node; //迭代器pos处的结点指针
	node* prev = cur->_prev; //迭代器pos前一个位置的结点指针
	node* next = cur->_next; //迭代器pos后一个位置的结点指针
 
	delete cur; //释放cur结点
 
	//建立prev与next之间的双向关系
	prev->_next = next;
	next->_prev = prev;
	
	return iterator(next); //返回所给迭代器pos的下一个迭代器
}

这里如果返回当前的位置,也会有迭代器失效问题 

push_back 和 pop_back

void push_back(const T& x)
{
    Node* tail = _head->_prev;
    Node* newnode = new Node(x);

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

    newnode->_next = _head;
    _head->_prev = newnode;
}

push_front 和 pop_front

 push_front函数就是在第一个有效结点前插入结点,而pop_front就是删除第一个有效结点。

//头插
void push_front(const T& x)
{
	insert(begin(), x); //在第一个有效结点前插入结点
}
//头删
void pop_front()
{
	erase(begin()); //删除第一个有效结点
}

容量相关函数

size

size函数用于获取当前容器当中的有效数据个数,因为list是链表,所以只能通过遍历的方式逐个统计有效数据的个数。 

size_t size() const
{
	size_t sz = 0; //统计有效数据个数
	const_iterator it = begin(); //获取第一个有效数据的迭代器
	while (it != end()) //通过遍历统计有效数据个数
	{
		sz++;
		it++;
	}
	return sz; //返回有效数据个数
}

resize 

resize函数的规则:

  1. 若当前容器的size小于所给n,则尾插结点,直到size等于n为止。
  2. 若当前容器的size大于所给n,则只保留前n个有效数据。

实现resize函数时,不要直接调用size函数获取当前容器的有效数据个数,因为当你调用size函数后就已经遍历了一次容器了,而如果结果是size大于n,那么还需要遍历容器,找到第n个有效结点并释放之后的结点。

void resize(size_t n, const T& val = T())
{
	iterator i = begin(); //获取第一个有效数据的迭代器
	size_t len = 0; //记录当前所遍历的数据个数
	while (len < n&&i != end())
	{
		len++;
		i++;
	}
	if (len == n) //说明容器当中的有效数据个数大于或是等于n
	{
		while (i != end()) //只保留前n个有效数据
		{
			i = erase(i); //每次删除后接收下一个数据的迭代器
		}
	}
	else //说明容器当中的有效数据个数小于n
	{
		while (len < n) //尾插数据为val的结点,直到容器当中的有效数据个数为n
		{
			push_back(val);
			len++;
		}
	}
}

empty

empty函数用于判断容器是否为空,我们直接判断该容器的begin函数和end函数所返回的迭代器,是否是同一个位置的迭代器即可。(此时说明容器当中只有一个头结点)

bool empty() const
{
	return begin() == end(); //判断是否只有头结点
}

clear

clear函数用于清空容器,我们通过遍历的方式,逐个删除结点,只保留头结点即可。

void clear()
{
	iterator it = begin();
	while (it != end()) //逐个删除结点,只保留头结点
	{
		it = erase(it);
	}
}

可能会有同学问:这个是清除全部操作 为啥没有对it++进行遍历删除呢

这里 erase(it) 会删除当前迭代器 it 指向的元素,并返回一个指向下一个元素的迭代器。所以就相当于++了

list 容器的模拟实现整体代码

list.h 

#pragma once
#include <iostream>
#include <string>
#include <assert.h>
using std::ostream;
using std::istream;
using std::cin;
using std::cout;
using std::endl;
 
// 为了避免和库里的 list 产生冲突,在自己的命名空间内实现 list
// 带头---双向---循环---链表
namespace xas_list
{
	// 通过模板能够实现不同类型的数据存储
	template<class T>
	// 链表节点的构造
	struct ListNode
	{
		ListNode<T>* _next;   // 指向后面节点的指针
		ListNode<T>* _prev;   // 指向前面节点的指针
		T _data;              // 一个节点中的数据
 
		// 构造函数
		ListNode(const T& x = T())
			:_next(nullptr)
			,_prev(nullptr)
			,_data(x)
		{}
	};
 
	// 模拟实现迭代器
	template<class T, class Ref, class Ptr>
	// 模式一个迭代器 类型
	struct _list_iterator
	{
		typedef ListNode<T> Node;       // 为节点类 取别名
 
		//只要用自己的类型,就对其typedef成self,方便后续使用
		typedef _list_iterator<T, Ref, Ptr> self;   // 为正向迭代器类 取别名
 
		// 成员变量
		Node* _node;                     // _node 表示一个节点
		
 
		_list_iterator(Node* node = nullptr)       // 默认构造函数
			:_node(node)
		{}
 
		// ++it 重载前置++ —— 让链表能够像数组一样去++操作,访问元素
		// 注意:这里的 this 不是局部变量,函数结束不会被销毁,可以使用引用返回,减少拷贝次数
		self& operator++()
		{
			//前置++返回的是++之后的值,直接让当前位置的结点指向下一个节点
			_node = _node->_next;
			return *this;
		}
 
		//重载后置++
		//此时需要返回temp对象,而不是引用
		//因为temp对象是局部的对象
		//函数结束后就被释放
 
		//it++ 重载后置++ —— (这里需要加上int作为一个站位符,与前置++区分)
		self operator++(int a)
		{
			self temp(*this);
			_node = _node->_next; //后置++返回的是++之前的值,需要保存当前节点,再指向下一个节点
			return temp;
		}
 
 
		//重载前置--
		self& operator--() 
		{
			_node = _node->_prev;
			return *this;
		}
		//重载后置--
		self operator--(int a) 
		{
			self temp(*this);
			_node = _node->_prev;
			return temp;
		}
 
		// 赋值重载重载  * 
		//返回迭代器指向的结点的值域
		Ref operator*()
		{
			return _node->_data;
		}
 
		// 重载 -> 操作符 ---实现指针访问元素
		Ptr operator->()
		{
			return &_node->date;
		}
 
		// 赋值重载 !=
		bool operator!=(const self& s)  const
		{
			return _node != s._node;
		}
 
		// 赋值重载 ==
		bool operator==(const self& s)  const
		{
			return _node == s._node;
		}
	};
 
 
	template<class T>
	// 创建一个 list 类
	class list
	{
		typedef ListNode<T> Node;            // 重新命名节点(结构体)的名称
	public:
 
		typedef _list_iterator<T, T&, T*> iterator;  // 为 迭代器类型取 别名
		typedef _list_iterator<T, const T&, const T*> const_iterator;
 
		// 正向迭代器 和 正向 const 迭代器
		iterator begin();
		iterator end();
		const_iterator begin()const;
		const_iterator end()const;
 
		// 默认成员函数
		list();                             // 构造函数   --- 无参构造
		list(int n, const T& val = T());    // 用 n个val 构造
		template<class iterator>
		list(iterator first, iterator last) // 迭代器区间构造
		{
			empty_init();
			while (first != last)
			{
				push_back(*first);
				++first;
			}
 
		}
		~list();                            // 析构函数
		list(list<T>& lt);                  // 拷贝构造
		void empty_init();                  // 初始化一个循环链表
		list<T>& operator=(list<T> lt);     // 赋值运算符符重载
 
 
		// 容量相关的函数
		size_t size() const;                // 计算节点的有效个数
		bool Empty()const;                  // 判空 不为空时,返回 true
		void clear();                       // 清空数据
		void resize(size_t n, const T& val = T());    // 设置list对象的有效元素个数
 
 
		// 访问容器相关函数
		T& front();                         // 返回第一个有效数据
		T& back();                          // 返回最后一个有效数据
		const T& front() const;                   
		const T& back() const;
 
 
		// 修改容器内容的相关函数
		void push_back(const T& x);                 // 尾插
		iterator insert(iterator pos, const T& x);  // 插入
		void push_front(const T& x);                // 头插
		iterator erase(iterator pos);               // 删除
		void pop_back();                            // 尾删
		void pop_front();                           // 头删
		void swap(list<T>& temp);                   // 交换函数
 
	private:
		Node* _head;
	};
 
	// 打印函数
	void printf_list(const list<int>& lt);
}

list.cpp

#include "list.h"
 
template<class T>
xas_list::list<T>::list()    // 构造函数   --- 无参构造
{
	_head = new Node;     //申请创建一个新的节点  --- 双向循环
	_head->_next = _head;  
	_head->_prev = _head;
}
 
template<class T>       // 用 n个val 进行构造
xas_list::list<T>::list(int n, const T& val)
{
	// 创建一个空节点
	_head = new Node;
	// 形成一个带头双向循环链表
	_head->_prev = _head;
	_head->_next = _head;
 
	for (int i = 0; i < n; i++)
	{
		push_back(val);
	}
}
 
 
 
template<class T>
xas_list::list<T>::~list()                     // 析构函数
{
	clear();
 
	delete _head;
 
	_head = nullptr;
 
}
 
// 初始化一个循环链表
template<class T>
void xas_list::list<T>::empty_init()
{
	//申请创建一个新的节点  --- 双向循环
	_head = new Node;
	_head->_next = _head;
	_head->_prev = _head;
}
 
template<class T>
xas_list::list<T>::list( list<T>& lt)
{
	//申请创建一个新的节点  --- 双向循环
	empty_init();
 
	for (auto& e : lt)
	{
		push_back(e);
	}
}
 
template<class T>
void xas_list::list<T>::swap(list<T>& temp)
{
	std::swap(_head, temp._head);
}
 
 
// lt1 = lt2;  --- 赋值重载
template<class T>
xas_list::list<T>& xas_list::list<T>::operator=(list<T> lt)
{
	//if (this != &lt)  // 判断一下是否有给自己赋值
	//{
	//	// 将lt1 先清空,再将lt2 插入到 lt1中
	//	// 注意 this 指针
	//	clear();
	//	for (const auto& e : lt)
	//	{
	//		push_back(e);
	//	}
	//}
	swap(lt);
	return *this;
}
 
 
template<class T>
void xas_list::list<T>::clear()        // 清空数据
{
	//复用erase
	iterator it = begin();
	while (it != end())
	{
		it = erase(it);//用it接收删除后的下一个结点的位置
	}
}
 
 
// 正向迭代器 
// begin迭代器指向的是正方向第一个有效结点,也就是头结点的下一个结点。
// 因为有 哨兵位的存在
template<class T>
typename xas_list::list<T>::iterator xas_list::list<T>::begin()
{
	return iterator(_head->_next);
}
 
 
// end迭代器指向的是正方向最后一个有效结点的下一个结点,也就是头结点。
template<class T>
typename xas_list::list<T>::iterator xas_list::list<T>::end()
{
	return iterator(_head);
}
 
template<class T>
typename xas_list::list<T>::const_iterator xas_list::list<T>::begin()const
{
	return const_iterator(_head->_next);
}
 
template<class T>
typename xas_list::list<T>::const_iterator xas_list::list<T>::end()const
{
	return const_iterator(_head);
}
 
template<class T>
// ListNode(const T& x = T())  这里的 T() 的给一个缺省值
void xas_list::list<T>::push_back(const T& x)   // 尾插
{
	// 创建一个新的节点  ----  Node
	//Node* newnode = new Node(x);
	//Node* tail = _head->_prev;     // 找尾节点--------头节点的前一个节点
 
	 进行尾插
	//tail->_next = newnode;
	//newnode->_prev = tail;
	//newnode->_next = _head;
	//_head->_prev = newnode;
 
	insert(end(), x);
}
 
// 头插
template<class T>
void xas_list::list<T>::push_front(const T& x)
{
	insert(begin(), x);
}
 
 
// 插入
template<class T>
typename xas_list::list<T>::iterator xas_list::list<T>::insert(iterator pos, const T& x)
{
	// 保存当前节点
	Node* cur = pos._node;
	Node* prev = cur->_prev;
 
	// 创建一个新的节点
	Node* newnode = new Node(x);
 
	prev->_next = newnode;
	newnode->_prev = prev;
	newnode->_next = cur;
	cur->_prev = newnode;
 
	return iterator(newnode);
}
 
// 删除
template<class T>
typename xas_list::list<T>::iterator xas_list::list<T>::erase(iterator pos)
{
	assert(pos != end());
 
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* next = cur->_next;
 
	prev->_next = next;
	next->_prev = prev;
	// 删除当前位置
	delete cur;
	return iterator(next);
}
 
// 尾删
template<class T>
void xas_list::list<T>::pop_back()
{
	erase(--end());
}
 
// 头删
template<class T>
void  xas_list::list<T>::pop_front()
{
	erase(begin());
}
 
// 判断 list 的有效链表个数
template<class T>
size_t xas_list::list<T>::size() const
{
	size_t count = 0;
	list<T>::const_iterator it = begin();
	while (it != end())
	{
		count++;
		it++;
	}
	return count;
}
 
// 判断 list 是否为空
template<class T>
bool xas_list::list<T>::Empty() const
{
	return _head->_next == _head;
}
 
template<class T>
void xas_list::list<T>::resize(size_t n, const T& val)
{
	list<T>::iterator it = begin(); //获取第一个有效数据的迭代器
	size_t len = 0; //记录当前所遍历的数据个数
	while (len < n && it != end())
	{
		len++;
		it++;
	}
	if (len == n) //说明容器当中的有效数据个数大于或是等于n
	{
		while (it != end()) //只保留前n个有效数据
		{
			it = erase(it); //每次删除后接收下一个数据的迭代器
		}
	}
	else //说明容器当中的有效数据个数小于n
	{
		while (len < n) //尾插数据为val的结点,直到容器当中的有效数据个数为n
		{
			push_back(val);
			len++;
		}
	}
}
 
template<class T>
T& xas_list::list<T>::front()
{
	return *begin();
}
 
template<class T>
T& xas_list::list<T>::back()
{
	return *(--end());
}
 
template<class T>
const T& xas_list::list<T>::front() const
{
	return *begin();
}
 
template<class T>
const T& xas_list::list<T>::back() const
{
	return *(--end());
}
 
 
// 打印对应的链表
void xas_list::printf_list(const list<int>& lt)
{
	list<int>::const_iterator it = lt.begin();
 
	while (it != lt.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;
}
 
 
// 遍历的测试
void test1()
{
	xas_list::list<int> lt(5,6);
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	lt.push_back(5);
 
	lt.push_front(0);
	// 头删
	lt.pop_back();
	// 尾删
	lt.pop_front();
 
	// -------------迭代器测试------------------// 
	cout << "迭代器的测试" << endl;
	xas_list::list<int>::iterator it = lt.begin();
	while (it != lt.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
 
	cout << "范围for的测试" << endl;
	// -------------范围 for 测试------------------// 
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;
	
 
	cout << "清空数据" << endl;
	// 清空数据
	lt.clear();
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;
 
}
 
 
void test2()
{
	xas_list::list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	lt.push_back(5);
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;
 
	cout << "拷贝构造" << endl;
	xas_list::list<int> copy(lt);
 
	for (auto e : copy)
	{
		cout << e << " ";
	}
	cout << endl;
 
	cout << "赋值重载" << endl;
	xas_list::list<int> lt1;
	lt1.push_back(10);
	lt1.push_back(20);
	lt1.push_back(33);
 
	lt = lt1;
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;
 
}
 
 
// const 迭代器
void test3()
{
	xas_list::list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
 
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	xas_list::list<int> l3(arr, arr + 10); //迭代器区间构造
	cout << l3.size() << endl;
	cout << l3.Empty() << endl;
	l3.resize(12,2);
	cout << l3.back() << endl;
	cout << l3.front() << endl;
	printf_list(l3);
}
 
 
 
int main()
{
	test3();
	return 0;
	
}

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

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

相关文章

使用excel.EasyExcel实现导出有自定义样式模板的excel数据文件,粘贴即用!!!

客户要求导出的excel文件是有好看格式的&#xff0c;当然本文举例模板文件比较简单&#xff0c;内容丰富的模板可以自行设置&#xff0c;话不多说&#xff0c;第一步设置一个"好看"的excel文件模板 上面要注意的地方是{.变量名} &#xff0c;这里的变量名对应的就是…

Spring Boot 集成 Elasticsearch怎样在不启动es的情况下正常启动服务

解释 在spingboot 集成es客户端后&#xff0c;每当服务启动时&#xff0c;服务默认都会查看es中是否已经创建了对应的索引&#xff0c;如果没有索引则创建。基于上面的规则我们可以通过配置不自动创建索引来达到在没有es服务的情况下正常启动服务。 解决办法 在entity类的Docu…

JVM常见概念之条件移动

问题 当我们有分支频率数据时&#xff0c;有什么有趣的技巧可以做吗&#xff1f;什么是条件移动&#xff1f; 基础知识 如果您需要在来自一个分支的两个结果之间进行选择&#xff0c;那么您可以在 ISA 级别做两件不同的事情。 首先&#xff0c;你可以创建一个分支&#xff…

Android AI ChatBot-v1.6.3-28-开心版[免登录使用GPT-4o和DeepSeek]

Android AI ChatBot- 链接&#xff1a;https://pan.xunlei.com/s/VOLi1Ua071S6QZBGixcVL5eeA1?pwdp3tt# 免登录使用GPT-4o和DeepSeek

集成学习(上):Bagging集成方法

一、什么是集成学习&#xff1f; 在机器学习的世界里&#xff0c;没有哪个模型是完美无缺的。就像古希腊神话中的"盲人摸象"&#xff0c;单个模型往往只能捕捉到数据特征的某个侧面。但当我们把多个模型的智慧集合起来&#xff0c;就能像拼图一样还原出完整的真相&a…

DeepSeek R1 本地部署指南 (3) - 更换本地部署模型 Windows/macOS 通用

0.准备 完成 Windows 或 macOS 安装&#xff1a; DeepSeek R1 本地部署指南 (1) - Windows 本地部署-CSDN博客 DeepSeek R1 本地部署指南 (2) - macOS 本地部署-CSDN博客 以下内容 Windows 和 macOS 命令执行相同&#xff1a; Windows 管理员启动&#xff1a;命令提示符 CMD ma…

【TI MSPM0】Timer学习

一、计数器 加法计数器&#xff1a;每进入一个脉冲&#xff0c;就加一减法计算器&#xff1a;每进入一个脉冲&#xff0c;就减一 当计数器减到0&#xff0c;触发中断 1.最短计时时间 当时钟周期为1khz时&#xff0c;最短计时时间为1ms&#xff0c;最长计时时间为65535ms 当时…

Windows部署deepseek R1训练数据后通过AnythingLLM当服务器创建问答页面

如果要了解Windows部署Ollama 、deepseek R1请看我上一篇内容。 这是接上一篇的。 AnythingLLM是一个开源的全栈AI客户端&#xff0c;支持本地部署和API集成。它可以将任何文档或内容转化为上下文&#xff0c;供各种语言模型&#xff08;LLM&#xff09;在对话中使用。以下是…

信奥赛CSP-J复赛集训(模拟算法专题)(27):P5016 [NOIP 2018 普及组] 龙虎斗

信奥赛CSP-J复赛集训(模拟算法专题)(27):P5016 [NOIP 2018 普及组] 龙虎斗 题目背景 NOIP2018 普及组 T2 题目描述 轩轩和凯凯正在玩一款叫《龙虎斗》的游戏,游戏的棋盘是一条线段,线段上有 n n n 个兵营(自左至右编号 1 ∼ n 1 \sim n 1∼n),相邻编号的兵营之间…

多模态大模型常见问题

1.视觉编码器和 LLM 连接时&#xff0c;使用 BLIP2中 Q-Former那种复杂的 Adaptor 好还是 LLaVA中简单的 MLP 好&#xff0c;说说各自的优缺点&#xff1f; Q-Former&#xff08;BLIP2&#xff09;&#xff1a; 优点&#xff1a;Q-Former 通过查询机制有效融合了视觉和语言特征…

SpringBoot项目实战(初级)

目录 一、数据库搭建 二、代码开发 1.pom.xml 2.thymeleaf模块处理的配置类 3.application配置文件 4.配置&#xff08;在启动类中&#xff09; 5.编写数据层 ②编写dao层 ③编写service层 接口 实现类 注意 补充&#xff08;注入的3个注解&#xff09; 1.AutoWir…

计算机网络——总结

01. 网络的发展及体系结构 网络演进历程 从1969年ARPANET的4个节点发展到如今覆盖全球的互联网&#xff0c;网络技术经历了电路交换到分组交换、有线连接到无线覆盖的革命性变革。5G时代的到来使得网络传输速度突破10Gbps&#xff0c;物联网设备数量突破百亿级别。 网络体系…

Umi-OCR- OCR 文字识别工具,支持截图、批量图片排版解析

Umi-OCR 是免费开源的离线 OCR 文字识别软件。无需联网&#xff0c;解压即用&#xff0c;支持截图、批量图片、PDF 扫描件的文字识别&#xff0c;能识别数学公式、二维码&#xff0c;可生成双层可搜索 PDF。内置多语言识别库&#xff0c;界面支持多语言切换&#xff0c;提供命令…

高速网络包处理,基础网络协议上内核态直接处理数据包,XDP技术的原理

文章目录 预备知识TCP/IP 网络模型&#xff08;4层、7层&#xff09;iptables/netfilterlinux网络为什么慢 DPDKXDPBFPeBPFXDPXDP 程序典型执行流通过网络协议栈的入包XDP 组成 使用 GO 编写 XDP 程序明确流程选择eBPF库编写eBPF代码编写Go代码动态更新黑名单 预备知识 TCP/IP…

C++:背包问题习题

1. 货币系统 1371. 货币系统 - AcWing题库 给定 V 种货币&#xff08;单位&#xff1a;元&#xff09;&#xff0c;每种货币使用的次数不限。 不同种类的货币&#xff0c;面值可能是相同的。 现在&#xff0c;要你用这 V 种货币凑出 N 元钱&#xff0c;请问共有多少种不同的…

数据可信安全流通实战,隐语开源社区Meetup武汉站开放报名

隐语开源社区 Meetup 系列再出发&#xff01;2025 年将以武汉为始发站&#xff0c;聚焦"技术赋能场景驱动"&#xff0c;希望将先进技术深度融入数据要素流转的各个环节&#xff0c;推动其在实际应用场景中落地生根&#xff0c;助力释放数据要素的最大潜能&#xff01…

java使用Apache POI 操作word文档

项目背景&#xff1a; 当我们对一些word文档&#xff08;该文档包含很多的标题比如 1.1 &#xff0c;1.2 &#xff0c; 1.2.1.1&#xff0c; 1.2.2.3&#xff09;当我们删除其中一项或者几项时&#xff0c;需要手动的对后续的进行补充。该功能主要是对标题进行自动的补充。 具…

免费开源的NAS解决方案:TrueNAS

TrueNAS是业内知名的FreeNAS系统的升级版&#xff0c;是一款开源的网络存储系统&#xff0c;具有高性能、稳定性和易用性等优点。 TrueNAS目前有三个版本&#xff0c;分别是TrueNAS CORE、TrueNAS ENTERPRISE、TrueNAS SCALE。其中&#xff0c;TrueNAS CORE基于FreeBSD开发&…

LeetCode热题100精讲——Top1:两数之和【哈希】

你好&#xff0c;我是安然无虞。 文章目录 题目背景两数之和C解法Python解法 题目背景 如果大家对于 哈希 类型的概念并不熟悉, 可以先看我之前为此专门写的算法详解: 蓝桥杯算法竞赛系列第九章巧解哈希题&#xff0c;用这3种数据类型足矣 两数之和 题目链接&#xff1a;两数…

Rocky9.5基于sealos快速部署k8s集群

首先需要下载 Sealos 命令行工具&#xff0c;sealos 是一个简单的 Golang 二进制文件&#xff0c;可以安装在大多数 Linux 操作系统中。 以下是一些基本的安装要求&#xff1a; 每个集群节点应该有不同的主机名。主机名不要带下划线。 所有节点的时间需要同步。 需要在 K8s …