C++ 链表List使用与实现:拷贝交换与高效迭代器细致讲解

目录

list的使用:

构造与赋值

元素访问

修改操作

容量查询

链表特有操作

拼接(Splice)

C++11 新增方法

注意:

stl_list的模拟实现:

一、链表节点设计的艺术

1.1 结构体 vs 类的选择

二、迭代器实现的精髓

2.1 为什么需要自定义迭代器?(迭代器在本文章后面还会更新)

2.2 运算符重载的细节实现

三、链表类的完整实现

3.1 构造函数与哨兵节点

3.2 迭代器范围获取

3.3 核心操作实现解析

插入操作:insert()

尾插:push_back()

头插:push_front()

删除操作:erase()

尾删:pop_back()

头删:pop_front()

辅助函数实现size()、empty()

析构函数销毁链表x~list()、清除数据clear()

拷贝构造list(const list& lt)

赋值list& operator==()、swap()

为什么参数要「按值传递」?

运作原理:

优势:

四、关键问题深度探讨

 自定义类型(更新原iterator类)​编辑

3.方法三:重载   operator->()

内存布局可视化

PrintList()、const_iterator

方法二:方法一使代码冗余,我们需要学习一种方法能够解决这个问题。

完整代码:


本文章主要简要讲解list容器的使用与详细讲解list模拟实现以及相关问题上,所用到完整代码在文章末尾

list的使用:

构造与赋值

  1. 构造函数

    list<int> lt;                // 空链表
    list<int> lt2(5, 10);        // 5 个元素,每个初始化为 10
    list<int> lt3(lt2.begin(), lt2.end()); // 通过迭代器范围构造
    list<int> lt4 = {1, 2, 3};   // 初始化列表(C++11)
  2. 赋值操作

    lt = lt2;                    // 深拷贝赋值
    lt.assign(3, 100);           // 替换内容为 3 个 100
    lt.assign(lt2.begin(), lt2.end()); // 通过迭代器赋值


元素访问

  1. 首尾元素front()、back()

    int front = lt.front();      // 首元素(不可修改空链表)
    int back = lt.back();        // 尾元素(不可修改空链表)

  2. 迭代器begin()、rbegin()

    list<int>::iterator it = lt.begin(); // 正向迭代器
    list<int>::reverse_iterator rit = lt.rbegin(); // 反向迭代器


修改操作

  1. 插入

    lt.push_front(0);            // 头部插入
    lt.push_back(4);             // 尾部插入
    
    auto pos = lt.begin();
    lt.insert(pos, 99);          // 在迭代器位置前插入
    lt.insert(pos, 3, 88);       // 插入 3 个 88
  2. 删除

    lt.pop_front();              // 删除头部元素
    lt.pop_back();               // 删除尾部元素
    
    lt.erase(pos);               // 删除迭代器位置元素
    lt.erase(pos, lt.end());     // 删除迭代器范围
    
    lt.remove(4);               // 删除所有值为 4 的节点
  3. 清空链表(clear)

    lt.clear();                  // 清空所有元素

容量查询

bool isEmpty = lt.empty();      // 是否为空
size_t size = lt.size();        // 元素个数(O(n) 时间复杂度!)
lt.resize(10);                 // 调整链表大小

链表特有操作

  1. 拼接(Splice)

    // 将 lt2 的全部内容移动到 lt 的 pos 位置前
    lt.splice(pos, lt2);         // lt2 会被清空
    
    // 移动单个元素
    auto it2 = lt2.begin();
    //pos为目标位置,it2为需要移动的单个元素,拿走it2,再尾差pos,改变节点的指向
    lt.splice(pos, lt2, it2);
    
    // 移动范围
    lt.splice(pos, lt2, lt2.begin(), lt2.end());

    注意:被转移的节点会被清空

  2. 合并(Merge)

    // 前提:lt 和 lt2 必须已按相同顺序排序(升序或降序)
    lt.sort();                   // 默认升序
    lt2.sort();
    lt.merge(lt2);               // 合并后 lt2 为空
  3. 排序(Sort)

    lt.sort();                   // 默认升序
    lt.sort(greater<int>());     // 降序(需包含 <functional>)
  4. 去重(Unique)

    lt.sort();                   // 必须先排序!
    lt.unique();                 // 删除连续重复元素
  5. 反转(Reverse)

    lt.reverse();                // 反转链表顺序

C++11 新增方法

  1. 原地构造(Emplace)

    lt.emplace_front(10);        // 在头部直接构造对象(避免拷贝)
    lt.emplace_back(20);         // 尾部构造
    lt.emplace(pos, 30);         // 指定位置构造

  2. 移动语义

    list<int> lt5 = std::move(lt); // 移动构造(高效转移资源)


注意:

  1. merge 前必须排序

    // 错误示例:lt 是降序,lt1 是升序
    lt.sort(greater<int>());    // 降序
    lt1.sort();                 // 升序
    lt.merge(lt1);             // 未定义行为!
    
    // 正确做法:统一顺序
    lt.sort();                 // 升序
    lt1.sort();
    lt.merge(lt1);
  2. unique 必须配合 sort

    // 错误用法:未排序时去重
    lt.unique();               // 只能删除连续重复
    
    // 正确用法
    lt.sort();
    lt.unique();               // 删除所有重复
  3. 迭代器失效

    • 在插入/删除操作后,指向被修改位置的迭代器会失效,需重新获取。


stl_list的模拟实现:

一、链表节点设计的艺术

1.1 结构体 vs 类的选择

template<class T>
struct ListNode {
    ListNode<T>* _next;  // 后继指针
    ListNode<T>* _prev;  // 前驱指针
    T _data;             // 存储数据

    // 默认构造函数初始化三要素
    ListNode(const T& x = T())
        :_next(nullptr),
        _prev(nullptr),
        _data(x)
    {}
};

设计要点分析

  1. 使用struct而非class的深层考量:

    • 默认public访问权限,便于链表类直接操作节点指针

    • 符合C++标准库实现惯例,增强代码可读性

    • 节点本身无需封装,属于链表内部实现细节

  2. 三指针设计哲学:

    • _next_prev构成双向链接的基石

    • _data采用模板类型,支持任意数据类型存储

    • 默认构造函数的巧妙设计:同时支持无参构造和值初始化

  3. 内存布局可视化:

    +------+    +------+    +------+
    | prev |<-->| prev |<-->| prev |
    | data |    | data |    | data |
    | next |--->| next |--->| next |
    +------+    +------+    +------+

二、迭代器实现的精髓

2.1 为什么需要自定义迭代器?(迭代器在本文章后面还会更新)

template<class T>
struct ListIterator {
    typedef ListNode<T> Node;
    typedef ListIterator<T> Self;

    Node* _node;  // 核心:封装节点指针

    // 构造函数实现指针到迭代器的转换
    ListIterator(Node* node):_node(node){}
};

关键问题解答

  • 原生指针的局限性:

    • 无法直接通过++操作跳转到下一个节点

    • 解引用操作不符合链表数据访问需求

    • 无法正确比较链表节点的位置关系

  • 迭代器设计模式的优势:

    • 统一容器访问接口

    • 隐藏底层数据结构差异

    • 支持算法泛型编程

2.2 运算符重载的细节实现

// 前置++:直接修改自身
Self& operator++() {
    _node = _node->_next;  // 关键跳转逻辑
    return *this;
}

// 后置++:返回临时对象
Self operator++(int) {
    Self tmp(*this);      // 拷贝当前状态
    _node = _node->_next;
    return tmp;           // 返回旧值
}

//1. --it
Self& operator--()
{
	_node = _node->_prev;
	return *this;
}
//2. it--
Self operator--(int)
{
	Self tmp(*this);
	_node = _node->_prev;
	return tmp;
}

// 解引用:访问节点数据
T& operator*() {
    return _node->_data;  // 返回引用支持修改
}

//两个迭代器是否相等?
bool operator!=(const Self& it)
{
	//即比较节点的指针,节点的指针相同,他们就相同
	return _node != it._node;
}


// 相等性比较的本质
bool operator==(const Self& it) {
    return _node == it._node;  // 指针地址比较
}

实现细节剖析

  1. 前置与后置++的差异:

    1. 性能考量:避免不必要的拷贝

    2. 语法区分:int参数占位符

  2. 解引用操作的注意事项:

    • 返回引用以实现左值操作

    • 与const迭代器的兼容性设计

  3. 比较大小是否需要重载?不需要,一个类是否要重载一个运算符,需要看他是否有意义,节点后面的地址不能保证比前面的地址大,所以没什么意义。不需要重载

三、链表类的完整实现

3.1 构造函数与哨兵节点

重新定义类型名称便于阅读:


	template<class T>
	class list 
	{
		typedef ListNode<T> Node;

	public:
		//在这里定义好类型
		typedef ListIterator<T> iterator;
    };
list() {
    _head = new Node;       // 创建哨兵节点
    _head->_next = _head;   // 自环初始化
    _head->_prev = _head;
    _size = 0;              // 尺寸计数器
}

哨兵节点的精妙之处

  • 统一空链表和非空链表的操作

  • 简化边界条件处理

  • 保证迭代器end()的有效性

  • 内存布局示意图:

    [HEAD] <-> [NODE1] <-> [NODE2] <-> [HEAD]

3.2 迭代器范围获取

iterator begin() 
{
    // 1.有名对象
    //iterator it(_head->_next);
    //return it;
    // 2.隐式类型转换(单参数的构造函数支持隐式类型的转换):
    //return _head->_next;
    // 3.匿名对象 
    return iterator(_head->_next);  // 隐式转换
}

iterator end() {
    return iterator(_head);         // 哨兵即终点
}
  • 统一使用匿名对象构造迭代器

  • begin()指向首元节点,end()指向哨兵节点

  • 支持C++11范围for循环的关键

3.3 核心操作实现解析

插入操作:insert()

 选定位置插入:insert()

//在pos位置插入data
void insert(iterator pos, const T& data) {
    Node* current = pos._node;
    Node* newnode = new Node(data);  // 内存申请
    
    // 四步链接法
    Node* prev = current->_prev;
    prev->_next = newnode;
    newnode->_prev = prev;
    newnode->_next = current;
    current->_prev = newnode;

    ++_size;  // 维护尺寸
}

尾插:push_back()

void push_back(const T& x)
{
	//1.还未写insert()时写的push_back()
	//Node* newnode = new Node(x);

	//Node* tail = _head->_prev; //指定被插的节点

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

	//2.写了insert()后写的push_back()
	insert(end(), x);
}

头插:push_front()

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

四步链接法示意图

Before: [PREV] <-> [CURRENT]
After:  [PREV] <-> [NEWNODE] <-> [CURRENT]

删除操作:erase()

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

    // 重新链接相邻节点
    prev->_next = next;
    next->_prev = prev;
    
    delete cur;     // 释放内存
    --_size;        // 更新尺寸
    
    return iterator(next);  // 返回有效迭代器
}

 迭代器失效问题

  • 删除操作使当前迭代器失效

  • 返回新迭代器的必要性

  • 正确使用模式示例:

    auto it = lst.begin();
    while(it != lst.end()) {
        if(condition) {
            it = lst.erase(it);  // 接收返回值
        } else {
            ++it;
        }
    }

尾删:pop_back()

//尾删  ,要--,不然删的是head(哨兵位)
void pop_back()
{
	erase(--end());  
}

头删:pop_front()

//头删
void pop_front()
{
	erase(begin());
}

辅助函数实现size()、empty()

size_t size() const { return _size; }  // O(1)时间复杂度

bool empty() { return _size == 0; }    // 高效判空

性能优化点

  • 使用_size变量避免遍历计数

  • empty()的常数时间复杂度

  • clear()的通用实现方式

析构函数销毁链表x~list()、清除数据clear()

//清除掉所有数据,有没有清除掉头结点?并没有(在析构函数中清除)
void clear()
{
	iterator it = begin();
	while (it != end())
	{
		it = erase(it);
	}
}

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

拷贝构造list(const list<T>& lt)

为了拷贝构造的实现方便我们将空链表的制作做了一个成员函数,因此默认的无参构造函数可以直接使用这个成员函数

	void empty_init()
	{
		//创建一个头结点
		_head = new Node;
		_head->_next = _head;
		_head->_prev = _head;

		_size = 0;x
	}

	//构造函数  
	list()
	{
		empty_init();
	}

拷贝构造,默认拷贝构造是浅拷贝,所以需要我们自己实现一个深拷贝(防止指向同一个空间,析构两次同一空间导致错误)

//lt2(lt1)
list(const list<T>& lt)
{
	//首先:在这里我们先创建空链表
	//也就是:lt2.empty_init()直接给lt2创建一个空链表即创建一个哨兵位头结点
	empty_init();
	//直接遍历一遍lt1,尾插
	for (auto& e : lt)//这里一定要加引用,因为如果T是string,就浅拷贝了
	{
		push_back(e);
	}
}

赋值list<T>& operator==()、swap()

调用库里的swap(<algorithm>)

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

//赋值
//lt1 = lt3
list<T>& operator=(list<T> lt) //lt  = lt3;
{
	swap(lt);
}

为什么参数要「按值传递」?

我的代码中赋值运算符的实现如下:

list<T>& operator=(list<T> lt) { // 参数按值传递
    swap(lt);
    return *this;
}

运作原理

  1. 按值传递参数:调用 operator= 时,lt 是传入对象的副本(触发拷贝构造函数)。

  2. 交换资源:通过 swap 将当前对象的 _head 和 _size 与副本 lt 交换。

  3. 自动析构副本:函数结束时,临时副本 lt 被析构,释放原对象的旧资源。

优势

  • 天然处理自我赋值:即使 lt1 = lt1;,参数 lt 是原对象的副本,交换后旧资源由副本释放。

  • 异常安全:拷贝过程(生成 lt)在交换前完成,若拷贝失败不会影响原对象。

小技巧:需要析构,就需要自己写深拷贝,不需要析构,默认浅拷贝

四、关键问题深度探讨

 自定义类型(更新原iterator类)

出现报错的原因是:内置类型才支持流插入提取,而A是自定义类型,所以在这里不支持,需要自己写运算符重载。

1.方法一:改变提取方式

			cout << (*it)._a1 << ":" << (*it)._a2 << endl;

2.方法二:写流插入

省略

3.方法三:重载   operator->()

		//it->
		T* operator->()
		{
			return &_node->_data;//返回的是_data的地址
		}

实际上用箭头会方便很多。

编译器为了可读性,省略了一个→

			cout << it->_a1 << ":" << it->_a2 << endl;

实现原理:

编译器隐藏了一个箭头实际上是:

it->->_a1

(it.operator->())->_a1,   (it.operator->())返回的是data的地址即T*,在这里就是我们的自定义类型 A*,有了A*此时就可以访问直接访问结构体成员_a1,_a2。从而得到他们的值。这里将it的行为当成A*  。

内存布局可视化

+---------------------+
| ListIterator对象     |
|   _node: 0x1000    |
+---------------------+
           |
           v
+---------------------+    +---------------------+
| ListNode<A>节点      |    | A结构体实例          |
|   _prev: 0x2000     |    |   _a1: 1           |
|   _next: 0x3000     |    |   _a2: 5           |
|   _data: 0x4000     |--->+---------------------+
+---------------------+    

当调用 it->_a1 时:

  1. 通过 _node 找到数据节点

  2. 获取 _data 的内存地址(0x4000)

  3. 通过指针访问结构体成员 _a1

PrintList()、const_iterator

因为现在的clt是一个const成员变量,而begin()、end()还未加const

加完const后编译通过:权限可以缩小但是不能扩大,非const成员可以调用const成员,const成员不能调用非const成员

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

		iterator end() const
		{
			return iterator(_head);
		}

这样真的就没有问题了吗?

按理来说,普通迭代器是能够修改的,但这里是const,是不能修改的:

实际上我们测试了一下,被修改了,说明代码有问题

原因出在这里,由于这里可以让非const对象访问,访问后返回的是普通迭代器,普通迭代器是支持被修改的

所以我们需要再重载const版本的,返回值也要记得修改

//const版本
const_iterator begin() const
{
	return const_iterator(_head->_next);
}

const_iterator end() const
{
	return const_iterator(_head);
}

const迭代器,需要的不是是迭代器不能被修改,而是迭代器指向的的内容不能被修改,所以不能用const iterator,因为它是避免迭代器被修改。

方法一:需要重新定义一个ListConstIterator的类,且在链表前面定义类型的时候加上const_iterator:

对于ListConstIterator类,实际上只有的这里的逻辑和ListIterator不同:*it += 10会显示报错了

图示:

方法二:方法一使代码冗余,我们需要学习一种方法能够解决这个问题。

而我们知道,凡是类型不同就可以用模版来解决 

Ref-->reference,引用  Ptr -->Pointer 

如图:本质相当于我们写了一个类模版,编译器实例化生成了两个类

最后达到目的。

完整代码:

#pragma once
#include<assert.h>
#include<iostream>
//#include<vector>
//#include<list>
#include<algorithm>

using namespace std;
namespace bit
{

	template<class T>

	//注意一下这里用的struct,数据想要全部公有的时候用struct
	struct ListNode
	{
		ListNode<T>* _next;
		ListNode<T>* _prev;
		T _data;
		ListNode(const T& x = T())
			:_next(nullptr),
			_prev(nullptr),
			_data(x)
		{}
	private:
		
	};


	//普通迭代器
	template<class T ,class Ref , class Ptr>
	struct ListIterator
	{
		typedef ListNode<T> Node;
		typedef ListIterator<T, Ref, Ptr> Self;

		Node* _node;

		//从本源来说迭代器还是一个节点的指针
		ListIterator(Node* node)
			:_node(node)
		{}

		//我们最想实现的是对于指针的++,即运算符的重载:
		//1.++it
		Self& operator++()
		{
			//++是怎么+到下一个节点的,即交换值
			_node = _node->_next;
			return *this;
		}
		//2.it++
		Self operator++(int)
		{
			//1.调用拷贝构造,这里是一个浅拷贝,把一个迭代器给另外一个迭代器,it1,it2都指向一个节点
			Self tmp(*this);

			_node = _node->_next;
			//2.迭代器是否需要写析构?不需要,因为资源是属于链表的
			return tmp;
		}
		//1. --it
		Self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}
		//2. it--
		Self operator--(int)
		{
			Self tmp(*this);
			_node = _node->_prev;
			return tmp;
		}

		//解引用:*it  返回的是_data
		//T& operator*()
		Ref operator*()
		{
			return _node->_data;
		}


		//it->
		//T* operator->()
		Ptr operator->()
		{
			return &_node->_data;//返回的是_data的地址
		}

		//两个迭代器是否相等?
		bool operator!=(const Self& it)
		{
			//即比较节点的指针,节点的指针相同,他们就相同
			return _node != it._node;
		}

		bool operator==(const Self& it)
		{
			return _node == it._node;
		}

		//比较大小是否需要重载?不需要,一个类是否要重载一个运算符,需要看他是否有意义
		//节点后面的地址不能保证比前面的地址大,所以没什么意义。不需要重载
		//迭代器的重点:指针,用类似指针的方式来访问容器

		//begin()和end()谁能提供,链表

	};



	//迭代器
	//template<class T>
	//struct ListConstIterator
	//{
	//	typedef ListNode<T> Node;
	//	typedef ListConstIterator<T> Self;

	//	Node* _node;

	//	//从本源来说迭代器还是一个节点的指针
	//	ListConstIterator(Node* node)
	//		:_node(node)
	//	{
	//	}

	//	//我们最想实现的是对于指针的++,即运算符的重载:
	//	//1.++it
	//	Self& operator++()
	//	{
	//		//++是怎么+到下一个节点的,即交换值
	//		_node = _node->_next;
	//		return *this;
	//	}
	//	//2.it++
	//	Self operator++(int)
	//	{
	//		//1.调用拷贝构造,这里是一个浅拷贝,把一个迭代器给另外一个迭代器,it1,it2都指向一个节点
	//		Self tmp(*this);

	//		_node = _node->_next;
	//		//2.迭代器是否需要写析构?不需要,因为资源是属于链表的
	//		return tmp;
	//	}
	//	//1. --it
	//	Self& operator--()
	//	{
	//		_node = _node->_prev;
	//		return *this;
	//	}
	//	//2. it--
	//	Self operator--(int)
	//	{
	//		Self tmp(*this);
	//		_node = _node->_prev;
	//		return tmp;
	//	}

	//	//如何控制*it不能被修改,即在这里加const
	//	const T& operator*()
	//	{
	//		return _node->_data;
	//	}


	//	//如何控制it->不能被修改,即在这里加const 
	//	const T* operator->()
	//	{
	//		return &_node->_data;//返回的是_data的地址
	//	}

	//	//两个迭代器是否相等?
	//	bool operator!=(const Self& it)
	//	{
	//		//即比较节点的指针,节点的指针相同,他们就相同
	//		return _node != it._node;
	//	}

	//	bool operator==(const Self& it)
	//	{
	//		return _node == it._node;
	//	}

	//	//比较大小是否需要重载?不需要,一个类是否要重载一个运算符,需要看他是否有意义
	//	//节点后面的地址不能保证比前面的地址大,所以没什么意义。不需要重载
	//	//迭代器的重点:指针,用类似指针的方式来访问容器

	//	//begin()和end()谁能提供,链表

	//};



	template<class T>
	class list 
	{
		typedef ListNode<T> Node;

	public:
		//在这里定义好类型
		//typedef ListIterator<T> iterator;
		//typedef ListConstIterator<T> const_iterator;
		typedef ListIterator<T, T&, T*> iterator;
		typedef ListIterator<T,const T&,const T*> const_iterator;


		void empty_init()
		{
			//创建空链表
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;

			_size = 0;
		}

		//构造函数  
		list()
		{
			empty_init();
		}

		//清除掉所有数据,有没有清除掉头结点?并没有(在析构函数中清除)
		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				it = erase(it);
			}
		}
		//析构函数
		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}

		//lt2(lt1)
		//拷贝构造,默认拷贝构造是浅拷贝,所以需要我们自己实现一个
		list(const list<T>& lt)
		{
			//首先:在这里我们先创建空链表
			//也就是:lt2.empty_init()直接给lt2创建一个空链表
			empty_init();
			//直接遍历一遍lt1,尾插
			for (auto& e : lt)//这里一定要加引用,因为如果T是string,就浅拷贝了
			{
				push_back(e);
			}
		}
		void swap(list<T>& lt)
		{
			std::swap(_head, lt._head);
			std::swap(_size, lt._size);
		}

		//赋值,按值传递
		//lt1 = lt3
		list<T>& operator=(list<T> lt) //lt  = lt3;
		{
			swap(lt);
			return *this;
		}

		//迭代器
		//1.原生指针能不能充当迭代器?不可以,这是建立在物理空间连续的情况下(画图解释)
		//2.++Node*能不能加到下一个节点,节点的解引用能取到当前节点的数据吗?不能
		//并且新new出来的地址和原来的地址也不会相同,头插和中间插入怎么保证在物理空间连续?从功能上未解决
		//因此我们在此重新封装一个类(内嵌类、typedef(在类域)),想一想日期类,可以日期相加,做各种操作的是合理的,使用封装的类
		//来重载运算符,重载运算符后我就可以自定义这些运算符的行为

		//普通版本
		iterator begin() 
		{
			// 1.有名对象
			//iterator it(_head->_next);
			//return it;
			// 2.隐式类型转换(单参数的构造函数支持隐式类型的转换):
			//return _head->_next;
			// 3.匿名对象
			return iterator(_head->_next);
		}
		//end()是最后一个数据的下一个位置
		iterator end() 
		{
			return iterator(_head);
		}

		 //const版本
		const_iterator begin() const
		{
			// 1.有名对象
			//iterator it(_head->_next);
			//return it;
			// 2.隐式类型转换(单参数的构造函数支持隐式类型的转换):
			//return _head->_next;
			// 3.匿名对象
			return const_iterator(_head->_next);
		}

		//end()是最后一个数据的下一个位置
		const_iterator end() const
		{
			return const_iterator(_head);
		}


		//注意这里我们都应该通过begin()、end()控制迭代器而不是控制节点了
		//尾插
		void push_back(const T& x)
		{
			//1.还未写insert()时写的push_back()
			//Node* newnode = new Node(x);

			//Node* tail = _head->_prev; //指定被插的节点

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

			//2.写了insert()后写的push_back()
			insert(end(), x);
		}
		//头插
		void push_front(const T& x)
		{
			insert(begin(), x);
		}

		//尾删  ,要--,不然删的是head(哨兵位)
		void pop_back()
		{
			erase(--end());  
		}
		//头删
		void pop_front()
		{
			erase(begin());
		}

		//在pos位置插入data
		void insert(iterator pos, const T& data)
		{
			Node* current = pos._node; 
			Node* newnode = new Node(data);
			Node* prev = current->_prev;

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

			++_size;
		}
		//删除pos位置

		iterator erase(iterator pos) //请注意这里的pos位置迭代器会失效,为什么?可以看我的上篇vector中目录的更新erase()
		{
			//拿到当前位置
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* next = cur->_next;

			//prev cur next
			
			prev->_next = next;
			next->_prev = prev;
			delete cur;
			--_size;

			return iterator(next);//解决办法就是返回下一个位置的迭代器
		}


		size_t size() const
		{
			库里面:1. 遍历一遍计数
			//for ()
			//{

			//}
			//2. 我们通过构造函数来初始化,给insert、erase加上_size便于计数
			return _size;
		}

		bool empty()
		{
			return _size == 0;
		}
		
		//resize()不常用,我们这里不做模拟,没有扩容的概念,比我当前的_size大就尾插入,比当前的_size小就尾删

	private:
		Node* _head;
		size_t _size;
	};

	void test_list1()
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		lt.push_back(5);
		//迭代器测试代码
		//这里需要遍历数据,所以要用到迭代器,于是现在我们写迭代器代码:
		list<int>::iterator it = lt.begin();
		cout << *it << endl;
		while (it != lt.end())
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;

		lt.push_front(10);
		lt.push_front(20);
		lt.push_front(30);
		for (auto e : lt)
		{
			cout << e << " ";

		}
		cout << endl;
		lt.pop_back();
		lt.pop_front();
		for (auto e : lt)
		{
			cout << e << " ";

		}
		cout << endl;
	}

	//迭代器进一步优化
	struct A
	{
		int _a1;
		int _a2;

		A(int a1 = 0, int a2 = 0)
			:_a1(a1)
			,_a2(a2)
		{

		}
	};
	void test_list2()
	{
		list<A> lt;
		//如果这里有自定义类型A
		A aa1(1, 5);
		A aa2 = { 1,1 };
		lt.push_back(aa1);
		lt.push_back(A(2, 3));
		lt.push_back({ 4,5 });
		lt.push_back(aa2);

		list<A>::iterator it = lt.begin();
		while (it != lt.end())
		{
			cout << it->_a1 << ":" << it->_a2 << endl;
			++it;
		}

	}  

	void PrintList(const list<int>& clt)
	{

		list<int>::const_iterator it = clt.begin();
		while (it != clt.end())
		{
			//*it += 10;
			cout << *it << " ";
			++it;
		}
	}

	void test_list3()
	{
		list<int> lt1;
		lt1.push_back(1);
		lt1.push_back(2);
		lt1.push_back(3);
		lt1.push_back(4);
		lt1.push_back(5);
		PrintList(lt1);
		cout << endl;

		//拷贝构造
		list<int> lt2(lt1);
		PrintList(lt2);
		cout << endl;

		list<int> lt3 = lt1;
		PrintList(lt3);

	}



}

结语:

       随着这篇关于题目解析的博客接近尾声,我衷心希望我所分享的内容能为你带来一些启发和帮助。学习和理解的过程往往充满挑战,但正是这些挑战让我们不断成长和进步。我在准备这篇文章时,也深刻体会到了学习与分享的乐趣。

       在此,我要特别感谢每一位阅读到这里的你。是你的关注和支持,给予了我持续写作和分享的动力。我深知,无论我在某个领域有多少见解,都离不开大家的鼓励与指正。因此,如果你在阅读过程中有任何疑问、建议或是发现了文章中的不足之处,都欢迎你慷慨赐教。

        你的每一条反馈都是我前进路上的宝贵财富。同时,我也非常期待能够得到你的点赞、收藏,关注,这将是对我莫大的支持和鼓励。当然,我更期待的是能够持续为你带来有价值的内容,让我们在知识的道路上共同前行。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【C++】C++入门基础

C&#xff08;C plus plus&#xff09; 是一种计算机高级程序设计语言&#xff0c;既可以进行 C语言 的过程化程序设计&#xff0c;又可以进行以抽象数据类型为特点的基于对象的程序设计&#xff0c;还可以进行以继承和多态为特点的面向对象的程序设计。 文章目录 前言一、C 的…

探索AI对冲基金:开源自动化交易系统的革新之路

在量化交易领域,人工智能技术的应用正悄然改变传统对冲基金的运作模式。GitHub上的开源项目ai-hedge-fund为开发者和金融从业者提供了一个独特的实践平台。该项目通过多智能体系统架构,整合市场数据分析、量化策略生成、风险管理和投资组合优化等核心功能,实现了从数据采集到…

C语言每日一练——day_3(快速上手C语言)

引言 针对初学者&#xff0c;每日练习几个题&#xff0c;快速上手C语言。第三天。&#xff08;会连续更新&#xff09; 采用在线OJ的形式 什么是在线OJ&#xff1f; 在线判题系统&#xff08;英语&#xff1a;Online Judge&#xff0c;缩写OJ&#xff09;是一种在编程竞赛中用…

SpringCloud系列教程(十三):Sentinel流量控制

SpringCloud中的注册、发现、网关、服务调用都已经完成了&#xff0c;现在就剩下最后一部分&#xff0c;就是关于网络控制。SpringCloud Alibaba这一套中间件做的非常好&#xff0c;把平时常用的功能都集成进来了&#xff0c;而且非常简单高效。我们下一步就完成最后一块拼图Se…

VMware安装欧拉操作系统(openEuler)第二节

摘要&#xff1a; 本篇文章接上篇《VMware安装欧拉操作系统&#xff08;openEuler&#xff09;第一节》&#xff0c;上一篇写到vmware workstation 17中创建openEuler虚拟机&#xff0c;本篇将详细介绍openEuler操作系统初始化以及相关配置的详细内容。 VMware安装欧拉操作系统…

[数据结构]并查集--C++版本的实现代码

目录 并查集的基本框架 查找一个元素在哪一个集合 判断两个元素是否在同一个集合 将两个集合进行合并 查询有多少组 测试 大学班级的同学会来自于五湖四海&#xff0c;每个人的家乡可能都不相同&#xff0c;那么如何将相同省份的同学连接到一块&#xff0c;也就是按省份进…

基于SpringBoot+Vue的瑜伽课体验课预约系统【附源码】

基于SpringBootVue的瑜伽课体验课预约系统 一、系统技术说明二、运行说明三、系统的演示四、系统的核心代码演示 一、系统技术说明 框架&#xff1a;SpringbootVue 数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09; 数据库工具&#xff1a;Navicat11 开发软…

【编译器】VSCODE烧录ESP32-C3——xiaozhi智能聊天机器人固件

【编译器】VSCODE烧录ESP32-C3——xiaozhi智能聊天机器人固件 文章目录 [TOC](文章目录) 前言一、方法一&#xff1a;使用固件烧录工具1. 安装CH340驱动2. 打开FLASH_DOWNLOAD文件3. 选择芯片类型和烧录方式4. 选择烧录文件5. 参数配置 二、方法二&#xff1a;VSCODE导入工程1.…

【C++】 —— 笔试刷题day_1

为了锻炼自己写代码的思路&#xff0c;开始每日刷题&#xff0c;加油&#xff01;&#xff01;&#xff01; 第一题 数字统计 题目要求&#xff1a; ​ 给定一个范围 [L , R] 求出数字L在该区间内出现的次数。&#xff08;其中1<L<R<10000&#xff09; 算法思路&…

R语言和RStudio安装

整体还是比较简单的&#xff0c;主要是记录个流程。 官方镜像站列表R语言官网 1 安装R&#xff08;2025/3/6&#xff09; R语言官网&#xff1a;The R Project for Statistical Computing 打开之后就Hello world一下吧 配置环境变量 2 安装RStudio 下载地址&#xff1a;htt…

计算机视觉应用|自动驾驶的感知革命:多传感器融合架构的技术演进与落地实践

一、引言 自动驾驶的终极目标是实现比人类驾驶更安全、更高效的交通系统。其核心挑战在于如何让机器像人类一样感知和理解复杂环境。然而&#xff0c;人类驾驶员依赖视觉、听觉和触觉的多模态信息&#xff0c;而自动驾驶系统则需要通过传感器和算法模拟这一过程。当前&#xf…

高效自动化测试:打造Python+Requests+Pytest+Allure+YAML的接口测试框架

一、背景 在快节奏的开发周期中&#xff0c;如何确保接口质量&#xff1f;自动化测试是关键。通过构建标准化、可复用的测试框架&#xff0c;能显著提升测试效率与准确性&#xff0c;为项目质量保驾护航[1][7]。 二、目标 ✅ 核心目标&#xff1a; ● 实现快速、高效的接口测试…

速算迷你世界脚本UI

--[[ --数学速算主界面 local UI"6996144362677448610" local v"6996144362677448610_" --自定义玩家数据界面 --显示界面分类 -- --称号积分幼儿园0学前班50小学生200初中生500高中生1000大学生2000研究生5000博士生10000教授50000 local A {["主屏幕…

『PostgreSQL』 Ubuntu 系统下PG15的安装与 PGVector 配置指南

&#x1f4e3;读完这篇文章里你能收获到 &#x1f4e6; 学会如何在 Ubuntu 上安装最新的 PostgreSQL 15 数据库。&#x1f511; 掌握修改 PostgreSQL 管理员密码和配置远程连接的方法。&#x1f310; 了解如何启用 PGVector 插件&#xff0c;实现向量存储和多种距离计算。&…

关于在electron(Nodejs)中使用 Napi 的简单记录

当我们使用electron想要集成一个C SDK实现很底层的算法逻辑就有可能与C SDK进行数据通信。 Napi 应该是比较好的选择&#xff0c;因为C本身的运行速度很快&#xff0c;使用Napi也能很大程度上保证软件的兼容性、又不会阻塞C线程、还可以很简单的与C 实现数据传递。 开始使用 安…

vscode(cursor)配置python环境,含远程调试

一、本地配置 1.1 安装python插件 1.2 配置python环境 在右下角就可以切换python环境&#xff0c;太简单了&#xff01; 1.3 Debug说明 打断点直接开启&#xff01; 在debug的过程中&#xff0c;还可以输入打印中间变量或者做一些测试 二、远程连接 2.1 下载远程工具 2.2 连…

S19文件格式详解:汽车ECU软件升级中的核心镜像格式

文章目录 引言一、S19文件格式的起源与概述二、S19文件的核心结构三、S19在汽车ECU升级中的应用场景四、S19与其他格式的对比五、S19文件实例解析六、工具链支持与安全考量七、未来趋势与挑战结语引言 在汽车电子控制单元(ECU)的软件升级过程中,S19文件(也称为Motorola S-…

怎么使用数据集微调大模型LLM

怎么使用数据集微调大模型LLM 目录 怎么使用数据集微调大模型LLM项目运行后目录结构1. 导入必要的库2. 准备训练数据3. 加载模型与分词器4. 数据预处理5. 配置训练参数(CPU 专用)6. 训练与保存完整可运行代码,调试了2天,保证可用项目运行后目录结构 1. 导入必要的库 from …

深度评测DeepSeek、ChatGPT O1和谷歌Gemini AI应用开发场景 - DeepSeek性能完胜!

下面我会展示我为期一周的实验结果&#xff0c;创作不宜&#xff0c;希望大家关注我&#xff0c;以后多多互3&#xff01;前一阵我在互联网上看到很多关于DeepSeek R1的讨论&#xff0c;这个开源模型据说可以媲美&#xff0c;甚至优于像OpenAI o1这样的付费模型。 由于我在日常…

ubuntu局域网部署stable-diffusion-webui记录

需要局域网访问&#xff0c;如下设置&#xff1a; 过程记录查看源码&#xff1a; 查看源码&#xff0c;原来修改参数&#xff1a;--server-name 故启动&#xff1a; ./webui.sh --server-name0.0.0.0 安装下载记录&#xff1a; 快速下载可设置&#xff1a; export HF_ENDPOI…