【C++】list 链表的使用+模拟实现
目录
文章目录
前言
一、list的简介
二、list的使用方法
三、list的模拟实现
1.基本框架:
2.迭代器实现
3.常用接口实现
四、完整代码
总结
前言
本文主要介绍C++【STL】容器中的 list,包括接口说明和模拟实现。其中讲解了迭代器功能上的分类,让你了解为何 list 要实现这个接口。
一、list的简介
- list就是链表,对应我们在C语言数据结构中学的带头双向循环链表。
- 既然是链表,那么它在内存中就是不连续的,数据由节点组成。
- list为模版类,创建时需要传数据类型类型。
我们接着前面 string 和 vector 继续讲解,因为STL中容器的使用方法很相似,因此我写的比较简洁:
二、list的使用方法
1.list 的构造
常用构造:
构造函数 | 接口说明 |
list (size_type n, const value_type& val = value_type()) | 构造的list中包含n个值为val的 元素 |
list() | 构造空的list |
list (const list& x) | 拷贝构造函数 |
list (InputIterator first, InputIterator last) | 用迭代器[first, last)区间中的元素构造 list |
list (initializer_list<value_type> il) | 用initializer_list构造 |
简单演示:
#include <iostream>
#include <list>
#include <vector>
using namespace std;int main()
{//(1)list<int> l1(8, 6);for (auto e : l1){cout << e << " ";}cout << endl;//(2)vector<int> v({ 1,2,3,4,5,6,7,8 });list<int>l2(v.begin(), v.end());for (auto e : l2){cout << e << " ";}cout << endl;//(3)list<int> l3({ 1,2,3,4,5,6 });for (auto e : l3){cout << e << " ";}cout << endl;return 0;
}
运行结果:
2.list的迭代器、list遍历
迭代器:
- 用法和 string、vector 一样,不赘述。
遍历方式:
(1)迭代器和范围for
#include <iostream>
#include <list>
using namespace std;int main()
{list<int> l1({ 9, 8, 7, 6, 5, 4, 12 });//迭代器遍历auto it = l1.begin();while (it != l1.end()){cout << *it << " ";++it;}cout << endl;//范围for,支持迭代器就支持范围forfor (auto e : l1){cout << e << " ";}cout << endl;return 0;
}
运行结果:
注意:因为 [ ] 对于链式结构的数据来说,效率不高,因此 list 并没有重载 [ ]
3.常见方法
函数声明 | 接口说明 |
size() | 返回list中有效节点的个数 |
empty() | 检测list是否为空,是返回true,否则返回false |
front() | 返回list的第一个节点中值的引用 |
back() | 返回list的最后一个节点中值的引用 |
push_front (const value_type& val) | 在list首元素前插入值为val的元素 |
pop_front() | 删除list中第一个元素 |
push_back (const value_type& val) | 在list尾部插入值为val的元素 |
pop_back() | 删除list中最后一个元素 |
emplace_back (Args&&... args) | 模版函数,与push_back大致相同,具体区别下面在讲 |
insert | 在list position 位置中插入值为val的元素 |
swap | 交换两个list中的元素 |
clear | 清空list中的有效元素 |
简单演示:
(1)size、empyt、front、back、push_front、pop_front、pop_back
#include <iostream>
#include <list>
using namespace std;int main()
{list<int> l1({ 1,2,3 });l1.push_front(88);for (auto e : l1){cout << e << " ";}cout << endl;cout << "size: " << l1.size() << endl;cout << "empty: " << l1.empty() << endl;cout << "front: " << l1.front() << endl;cout << "back: " << l1.back() << endl;l1.pop_back();l1.pop_front();for (auto e : l1){cout << e << " ";}cout << endl;return 0;
}
运行结果:
- 因为list是链式结构,所以头插头删是没有效率消耗的。
(2)push_back 和 emplace_back 的区别
- 首先 emplace_back 是一个模版函数
用法异同:
#include <iostream>
#include <list>
using namespace std;struct Pos
{int _row;int _col;Pos(int row = 0, int col = 0):_row(row),_col(col){}
};int main()
{list<Pos> l1;list<Pos> l2;Pos p;//push_back支持:l1.push_back(p);l1.push_back(Pos(1, 2));//匿名对象l1.push_back({ 1,2 });//大括号//emplace_back支持:l2.emplace_back(p);l2.emplace_back(Pos(1, 2));l2.emplace_back(1, 2);//支持直接传参l2.emplace_back(1);//支持直接传参return 0;
}
- 简单说:push_back 支持大括号传参
- emplace_back 支持直接传参
(3)insert、erase、迭代器失效问题
在 vector 中,我们接触了迭代器失效的问题,而在 list 中,迭代器失效只会出现在 erase 中
- list中,insert 插入数据迭代器不失效的原因就是,pos 指向的节点没有改变,因为 list 是链式结构,不存在扩容以及原空间销毁等问题。insert(1)的返回值是新节点的迭代器。
- 而在 erase 中,删除节点就等于将迭代器指定的空间销毁,因此 erase 的返回值是被删除节点的下一个节点。
演示:
#include <iostream>
#include <list>
using namespace std;int main()
{list<int> l1;//插入auto it = l1.insert(l1.begin(), 22);cout << *it << endl;//迭代器没有失效l1.insert(l1.end(), 33);l1.insert(l1.end(), 44);l1.insert(l1.end(), 55);for (auto e : l1){cout << e << " ";}cout << endl;cout << endl;//删除,我们可以借助 find 函数来找到需要删除的节点auto del = find(l1.begin(), l1.end(), 44);del = l1.erase(del);//迭代器失效,需要更新迭代器cout << *del << endl;for (auto e : l1){cout << e << " ";}cout << endl;return 0;
}
运行结果:
4.特有方法
函数声明 | 接口说明 |
splice | 将另一个list对象中的元素拿出来插入到自己元素中。 |
remove | 删除指定元素 |
unique | 删除 list 中重复值,需先排序 |
merge | 合并两个链表,被合并的链表会变为空,合并前两个链表需先排序 |
sort | 链表特供的排序 |
reverse | 逆置 |
(1)sort排序 和 迭代器分类
通过前面 string 和 vector 的学习,我们知道它们两个都没有提供 sort 接口,而是直接使用算法库中的 sort。那么为什么 list 要提供 sort 接口?这里就涉及迭代器分类的问题了。
- 首先,我们可以发现算法库中的 sort 的参数列表,迭代器的名字带有随机的字样。这里的命名其实就提示了使用者该函数需要的迭代器类型了。
- 还有一个原因是 list 需要自己实现 sort 的:
- 我们在 sort 的定义中大致可以看出,算法库中的 sort 为了计算排序元素的个数其实通过地址相减得到的,这对于连续型存储空间的容器来说确实可以,但是 list 是链式结构,因此 list 不能使用算法库中的 sort 来进行排序。
<1.1>迭代器分类
按使用上分类:
- 使用上分类就是我在 string 中讲到的 普通迭代器、const迭代器、反向迭代器等。这是我们按照使用上进行分类的。
按功能性分类:
- 单向迭代器:只支持++,比如 forward_list(单链表)
- 双向迭代器:支持++、--,比如 list(双链表)
- 随机迭代器:支持++、--、+、-,比如 string,vector
从官方文档中也能看到每个容器的迭代器类型:
例如:vector(随机迭代器)
例如:list(双向迭代器)
例如:forward_list(单向迭代器)
- 所以算法库中 sort 的参数名其实就暗示了只有随机迭代器才能使用。
- 当然这里还要注意一点,就是如果一个函数只支持双向迭代器,那么其实随机迭代器也可以使用,我们记住: 随机迭代器 > 双向迭代器 > 单向迭代器,类似包含的关系,如果一个接口支持单向迭代器,那么双向和随机迭代器也可以使用,但如果只支持随机,那么单向和双向都不能使用
例如1:算法库中的 reverse 逆置方法
- 算法库中的 reverse 显示支持双向迭代器,那么 list 其实是可以直接使用算法库中的 reverse,那么为什么还要自己实现一个 reverse,关于这一点应该是效率问题。
例如2:算法库中的 find 接口
- 这里显示的迭代器名称其实是可读迭代器,这涉及另外一种分类,但是其实 find 按功能是支持单向迭代器的,也就是 单向、双向、随机迭代器都可以使用该接口进行查找元素的。
<1.2>对比sort排序效率:
#include <iostream>
#include <vector>
#include <list>
#include <algorithm>
using namespace std;int main()
{srand((unsigned int)time(0));const int N = 1000000;vector<int> v1;list<int> l1;for (size_t i = 0; i < N; i++){v1.push_back(rand());l1.push_back(v1[i]);}int begin1 = clock();sort(v1.begin(), v1.end());int end1 = clock();int begin2 = clock();l1.sort();int end2 = clock();cout << "vector_sort: " << end1 - begin1 << endl;cout << "list_sort: " << end2 - begin2 << endl;return 0;
}
运行结果:
- 我们可以看到,排一百万个数据时,list 比 vector 多花了3倍多的时间。
- 所以效率上,list 的 sort 是比不上算法库的 sort 的
我们可以试着将 list 的数据拷贝到 vector 进行排序,然后再拷回 list,看看效率如何:
#include <iostream>
#include <vector>
#include <list>
#include <algorithm>
using namespace std;int main()
{srand((unsigned int)time(0));const int N = 1000000;vector<int> v;list<int> l1;list<int> l2;for (size_t i = 0; i < N; i++){l1.push_back(rand());}l2.assign(l1.begin(), l1.end());//assign用于重新赋值int begin1 = clock();v.assign(l1.begin(), l1.end());sort(v.begin(), v.end());l1.assign(v.begin(), v.end());int end1 = clock();int begin2 = clock();l2.sort();int end2 = clock();cout << "list_vector_sort_list: " << end1 - begin1 << endl;cout << "list_sort: " << end2 - begin2 << endl;return 0;
}
运行结果:
- 我们可以很直观的发现,list 的 sort 效率甚至比不上,list 拷贝给vector排再拷回来的效率。所以我们一般不直接使用 list 的 sort 进行排序,因为效率不高,除非数据量少,一般我们还是拷贝给 vector 进行排序,毕竟连续型空间排序优势很大。
(2)splice、remove、unique、merge 和 reverse 演示
splice:
- (1)将 x 的全部数据插入到自己的 pos 位置,x 中的数据将会清除
- (2)将 x 中 i 位置的数据插入到自己 pos 位置,x 中该数据会删除
- (3)将 x 中的一段迭代器区间的内容插入到 pos 位置,x 中这些数据会删除
简单演示:
#include <iostream>
#include <list>
#include <algorithm>
using namespace std;int main()
{list<int> l1{ 1,2,3,4,5 };list<int> l2{ 6,7,8,9,10 };auto it = find(l2.begin(), l2.end(), 8);l1.splice(l1.begin(), l2, it);for (auto e : l1){cout << e << " ";}cout << endl;for (auto e : l2){cout << e << " ";}cout << endl;return 0;
}
运行结果:
uinque、merge:
- 这两个的第二个重载都涉及仿函数,仿函数下篇再讲
- 注意这两个接口使用前,需要先将数据排成有序的。
简单演示:
#include <iostream>
#include <list>
#include <algorithm>
using namespace std;int main()
{list<int> l1{ 5,8,3,3,1,2,2,8 };list<int> l2({ 4, 4, 9, 9, 9, 6, 6, 5 });l1.sort();l2.sort();l1.unique();//删除重复项for (auto e : l1){cout << e << " ";}cout << endl;l2.unique();//删除重复项for (auto e : l2){cout << e << " ";}cout << endl;l1.merge(l2);//合并for (auto e : l1){cout << e << " ";}cout << endl;for (auto e : l2){cout << e << " ";}cout << "没了" << endl;return 0;
}
运行结果:
reverse、remove:
- remove 只要知道元素内容就能删除
演示:
#include <iostream>
#include <list>
#include <algorithm>
using namespace std;int main()
{list<int> l1({ 1,2,3,4,5 });l1.remove(2);//删除for (auto e : l1){cout << e << " ";}cout << endl;l1.reverse();//逆置for (auto e : l1){cout << e << " ";}cout << endl;return 0;
}
运行结果:
小结:
- list常用方法当然还有一些,比如关系运算等,这些比较简单就不一一演示了
- 接下来我们就模拟实现 list。
三、list的模拟实现
我们不完全模拟,主要实现出 list 常用的接口,加强印象和使用。
1.基本框架:
namespace txp
{//定义节点template<class T>struct list_node{T _data;list_node<T>* _next;list_node<T>* _prev;list_node(const T& x = T()):_data(x),_next(nullptr),_prev(nullptr){}};//链表template<class T>class list{typedef list_node<T> Node;//节点public://初始化void empty_init(){_head = new Node();_head->_next = _head;_head->_prev = _head;_size = 0;}//默认构造list(){empty_init();}private:Node* _head;size_t _size;};
}
节点:
- 首先,依然需要命名空间来与库里的list区分
- 然后,定义节点,我们之前学过链表,链表是由一个个节点连接组成的,既然是双向带头循环链表,那么就有三个成员变量,存储数据的_data,存储下一个节点地址的_next,存储上一个节点的地址_prev。
- 节点 list_node 需要写成模版类,模版参数即是存储数据的类型,然后我们需要手动写一个默认构造,将成员初始化。
- 注意:你可能想知道为啥 list_node 要用 struct 进行定义,这里简单说一下。
使用 struct 定义节点的原因:
- 在学习c++类时,我们知道 c++ 中 struct 和 class 的区别,其实大致上没有区别,c++中 struct 也能定义类。
- 唯一的区别就是如果不加访问限定符,struct 的成员默认是公有的,而 class 不加访问限定符则成员默认是私有的。
- 然后行业内就有一个不成文的规定:假如类中的每个成员都需要写成公有的,那么就用 struct 进行定义,反之则使用 class 进行定义。
- 因为 list_node 的每个成员都需要被频繁的访问,所以 list_node 成员全部为公有成员,故而不加访问限定符,直接使用 struct 进行定义。
链表:
- 链表有两个成员变量,一个是头结点 _head,另一个是记录节点个数的 _size。
- 头结点的类型由节点类决定,因此对节点类进行重命名,这里重命名到链表类中,其实相当于将节点类设置为链表的内部类了,并且重命名在 public 的上面,这样节点就是私有的,对外部来说无法访问。
- 链表的初始化先写到一个函数 empty_init 中,不直接写在构造函数中,是因为构造有很多,为了避免重复写就直接写成一个函数给构造函数调用。
- 初始化:因为是双向带头循环链表,因此头结点是不储存数据的,也就是哨兵位。然后_next 和 _prev 指针初始化时需要指向自己,这样才算是循环链表。
2.迭代器实现
在 string 和 vector 的模拟实现中,迭代器是直接对指针进行重命名的,这样迭代器++、--就能遍历数据了,但是在 list 中,这种方法行不通,因为链表的存储结构是不连续的,在这种情况下,我们该怎么实现迭代器呢?
- 其实之前我们就能发现迭代器很像指针,但我们不能说它就是指针,其实它是对指针的一种封装,面向对象的三大特征之一就是封装,为什么要封装,就是为了通用性,在STL中,迭代器是通用的功能,都叫一个名字,但其实底层实现不一样。
- 对于 list 来说,想要实现迭代器就需要对指针进行封装,封装成一个类,在类中重载 ++、--等操作符,对于迭代器来说,++就是走到下一个数据,那么链表要实现这样的效果,就是将指针指向下一个节点。
- 按照这样的思路,我们实现出 list 的迭代器
//定义迭代器
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(Node* node):_node(node){}Ref operator*(){return _node->_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& s){return _node != s._node;}//重载==bool operator==(const Self& s){return _node == s._node;}
};
- 首先,迭代器的全部功能都需要公开,因此直接写成 struct 类。
- 迭代器只要一个成员变量 _node,也就是节点指针。
- 然后你会发现迭代器有3个模版参数,这其实是一种巧妙的设计,我们模拟迭代器时一般只模拟两个迭代器:普通迭代器和const迭代器。这两个迭代器在解引用返回值上有差别,const 迭代器解引用返回的是 const 修饰的数据,不能修改。因此,如果只有一个模版参数用来替换数据类型时,那么对于这两个迭代器就需要手动写两个迭代器类了。
- 为了避免再手动写一个高度相似的迭代器类,因此另加两个参数 Ref 和 Ptr。
然后我们在链表中传递不同参数就能得到两种迭代器了:
//链表
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迭代器
(注意:迭代器重命名在 public 下方,迭代器需要被外部访问)
其实这样重命名后,编译器会根据模版自动生成两份迭代器类,只是减少了我们的代码量,将本来应该我们写两份迭代器类的工作交给了编译器。
Ref 和 Ptr 只用于解引用操作符* 和 ->中:
- *的重载函数就是返回对应节点的 _data 即可。
- ->的重载函数就需要返回对应节点 _data 的地址了,写法就是前面加个取地址符
关于其他的运算符重载:
- 如前置++、--,其实就是将节点指针移动到下一个节点或者上一个节点即可,后置同理只不过返回的是修改前的地址。
- 另外需要重载的就是 != 和 == 了,!= 就是为了支持我们迭代器遍历,==有时候需要用到。
3.常用接口实现
(1)迭代器
//链表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迭代器iterator begin(){return iterator(_head->_next);}iterator end(){return iterator(_head);}const_iterator begin() const{return const_iterator(_head->_next);}const_iterator end() const{return const_iterator(_head);}
- begin() 就是头结点的下一个节点
- end() 就是头结点了
(2)insert
//insert
iterator insert(iterator pos, const T& val)
{Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(val);cur->_prev = newnode;newnode->_next = cur;prev->_next = newnode;newnode->_prev = prev;++_size;return iterator(newnode);
}
- 直接看示意图:
- 最后需要返回新节点的指针。
(3)push_back、push_front
- 我们先实现了insert,那么这两个就可以直接复用insert了
//尾插
void push_back(const T& x)
{//复用写法insert(end(), x);
}//头插
void push_front(const T& val = T())
{insert(begin(), val);
}
(4)erase
//erase
iterator erase(iterator pos)
{assert(pos != end());Node* del = pos._node;Node* next = del->_next;Node* prev = del->_prev;next->_prev = prev;prev->_next = next;delete del;del = nullptr;--_size;return iterator(next);
}
- 先改变待删节点的前后节点指向,最后删除。
- 注意,不能删除头结点,使用assert进行断言
- 最后需要返回被删节点的下一个节点的迭代器
(5)pop_back,pop_front
同样,当我们实现了erase后,就可以直接复用
//尾删
void pop_back()
{erase(--end());
}//头删
void pop_front()
{erase(begin());
}
- 注意尾删需要--end(),因为end()返回的是头结点。
(6)size、swap,clear、析构和更多的构造
//size
size_t size()
{return _size;
}//交换
void swap(list<T>& tmp)
{std::swap(_head, tmp._head);std::swap(_size, tmp._size);
}//赋值运算符重载
list<T>& operator=(list<T> lt)
{swap(lt);return *this;
}//clear
void clear()
{auto it = begin();while (it != end()){it = erase(it);}
}//析构
~list()
{clear();delete _head;_head = nullptr;
}//n个val构造
list(size_t n, const T& val = T())
{empty_init();for (size_t i = 0; i < n; i++){push_back(val);}
}//initializer_list构造
list(std::initializer_list<T> il)
{empty_init();for (auto& e : il){push_back(e);}
}//拷贝构造
list(const list<T>& lt)
{empty_init();for (auto& e : lt){push_back(e);}
}
- 这里面需要注意的就是 swap 的操作,swap 本身是通过调用算法库中的swap交换两个list对象的指针和 _size 实现的。
- swap 配合赋值运算符重载,能极大简化代码,这里就是函数参数使用传值传参,这样参数相当完成了拷贝,将参数与自己调换就完成了赋值。
- clear 删除元素,配合析构也能简化代码。
- 其他的就很好理解了,不再赘述
全局的 swap:
//全局的swap
template<class T>
void swap(list<T>l1, list<T>l2)
{l1.swap(l2);
}
- 全局的 swap 存在主要是怕调用到算法库中的 swap,官方的文档中也有实现两个swap。
- 这样一来,当我们不用 对象. 去调用swap,而是直接调用 swap 也能高效的调换两个list对象了
- 关于 list 模拟就实现这么多,当然剩下的你如果有想法可以自己试着实现。
四、完整代码
#pragma once
#include <assert.h>namespace txp
{//定义节点template<class T>struct list_node{T _data;list_node<T>* _next;list_node<T>* _prev;list_node(const T& x = T()):_data(x),_next(nullptr),_prev(nullptr){}};//定义迭代器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(Node* node):_node(node){}Ref operator*(){return _node->_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& 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迭代器iterator begin(){return iterator(_head->_next);}iterator end(){return iterator(_head);}const_iterator begin() const{return const_iterator(_head->_next);}const_iterator end() const{return const_iterator(_head);}//初始化void empty_init(){_head = new Node();_head->_next = _head;_head->_prev = _head;_size = 0;}//默认构造list(){empty_init();}//n个val构造list(size_t n, const T& val = T()){empty_init();for (size_t i = 0; i < n; i++){push_back(val);}}//initializer_list构造list(std::initializer_list<T> il){empty_init();for (auto& e : il){push_back(e);}}//拷贝构造list(const list<T>& lt){empty_init();for (auto& e : lt){push_back(e);}}//析构~list(){clear();delete _head;_head = nullptr;}//sizesize_t size(){return _size;}//赋值运算符重载list<T>& operator=(list<T> lt){swap(lt);return *this;}//交换void swap(list<T>& tmp){std::swap(_head, tmp._head);std::swap(_size, tmp._size);}//clearvoid clear(){auto it = begin();while (it != end()){it = erase(it);}}//尾插void push_back(const T& x){//原始写法/*Node* new_node = new Node(x);Node* tail = _head->_prev;tail->_next = new_node;new_node->_prev = tail;_head->_prev = new_node;new_node->_next = _head;*///复用写法insert(end(), x);}//头插void push_front(const T& val = T()){insert(begin(), val);}//尾删void pop_back(){erase(--end());}//头删void pop_front(){erase(begin());}//insertiterator insert(iterator pos, const T& val){Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(val);cur->_prev = newnode;newnode->_next = cur;prev->_next = newnode;newnode->_prev = prev;++_size;return iterator(newnode);}//eraseiterator erase(iterator pos){assert(pos != end());Node* del = pos._node;Node* next = del->_next;Node* prev = del->_prev;next->_prev = prev;prev->_next = next;delete del;del = nullptr;--_size;return iterator(next);}private:Node* _head;size_t _size;};//全局的swaptemplate<class T>void swap(list<T>l1, list<T>l2){l1.swap(l2);}
}
总结
以上就是本文的全部内容了,感谢支持!
相关文章:
【C++】list 链表的使用+模拟实现
目录 文章目录 前言 一、list的简介 二、list的使用方法 三、list的模拟实现 1.基本框架: 2.迭代器实现 3.常用接口实现 四、完整代码 总结 前言 本文主要介绍C【STL】容器中的 list,包括接口说明和模拟实现。其中讲解了迭代器功能上的分类&am…...
@Configuration与 @Component的差异
继承关系 Configuration确实可以视为Component的派生注解。从源码层面来看,Configuration本身通过元注解方式标记了Component,这意味着所有被Configuration注解的类本质上也会被Spring识别为组件(Component)。这种设计使得Config…...
c++第一课(基础c)
目录 1.开场白 2.char(字符) 3.字符数组 4.ASCII码 1.开场白 OK,咱们也是亿(不是作者故意的)天没见,话不多说,直接开始! 2.char(字符) 众所不周知&…...
element ui的time时间和table表格
<el-date-picker v-model"value1" align"right" type"date" placeholder"选择日期" value-format"yyyy-MM-dd" change"changeDate"></el-date-picker><el-date-picker v-model"datetime"…...
安装 tensorflow 遇到的问题
Q1: 没发现满足需求的版本 ERROR: Could not find a version that satisfies the requirement keras-nightly~2.5.0.dev (from tensorflow) (from versions: none) ERROR: No matching distribution found for keras-nightly~2.5.0.dev 按照官方文档Installation — TensorFl…...
音视频封装格式:多媒体世界的“容器”与“桥梁”
一、音视频封装格式的核心概念 音视频封装格式(容器)是一种将编码后的视频、音频、字幕等数据按规则整合的文件格式,其本质是多媒体数据容器,核心作用包含: 同步多轨道数据:通过时间戳(PTS/DTS)实现音画同步。组织数据流:统一管理视频流、音频流、字幕流等,并存储元…...
【学习资料】嵌入式人工智能Embedded AI
图片来源: Embedded Artificial Intelligence for Business Purposes | DAC.digital 随着AI在设备端的应用,我们看到越来越多的可穿戴设备出现以及自动驾驶汽车的发展,可以看到嵌入式人工智能是新的发展方向。我为大家介绍嵌入式人工智能的…...
Linux命令大全完整版
1. linux 系统管理命令 adduser 功能说明:新增用户帐号。语 法:adduser补充说明:在 Slackware 中,adduser 指令是个 script 程序,利用交谈的方式取得输入的用户帐号资料,然后再交由真正建立帐号的 use…...
红队内网攻防渗透:内网渗透之内网对抗:实战项目VPC2打靶父子域三层路由某绒免杀下载突破约束委派域控提权
红队内网攻防渗透 实战网络攻防靶场记录1.靶机配置信息讲解2.靶场渗透完整流程2.1 入口点:192.168.139.130(win2008 R2)2.1.1 tomcat后台war包获取权限2.1.2 tomcat使用后门上线CS平台2.1.3 信息收集获取数据库密码2.2 入口点横向:192.168.10.11 (win2012 SQL)2.2.1 SQLs…...
always和assign语法区别
always语句可以带时钟, 也可以不带时钟。 一,assign和always区别 assign 语句使用时不能带时钟。 assign a=1; assign b=2; 在always不带时钟时,逻辑功能和assign完全一致,都是只产生组合逻辑。比较简单的组合逻辑推荐使用assign语句,比较复杂的组合逻辑推荐使用 al…...
深入了解ThreadLocal底层原理-高并发架构
目录 什么是ThreadLocal应用场景需求实现 ThreadLocal核心源码解读Thread 、ThreadLocal、ThreadLocalMap 三者的关系 四大引用-强软弱虚类型ThreadLocal内存泄漏ThreadLocal为什么需要设计成弱引用?并且ThreadLocal用完需要remove呢?原因 什么是ThreadL…...
《AI与NLP:开启元宇宙社交互动新纪元》
在科技飞速发展的当下,元宇宙正从概念逐步走向现实,成为人们关注的焦点。而在元宇宙诸多令人瞩目的特性中,社交互动体验是其核心魅力之一。人工智能(AI)与自然语言处理(NLP)技术的迅猛发展&…...
基于vue和微信小程序的校园自助打印系统(springboot论文源码调试讲解)
第3章 系统设计 3.1系统功能结构设计 本系统的结构分为管理员和用户、店长。本系统的功能结构图如下图3.1所示: 图3.1系统功能结构图 3.2数据库设计 本系统为小程序类的预约平台,所以对信息的安全和稳定要求非常高。为了解决本问题,采用前端…...
电脑开机一段时间就断网,只有重启才能恢复网络(就算插网线都不行),本篇文章直接解决,不要再看别人的垃圾方法啦
下面的是我解决问题的心路历程,不想看的可以直接跳到解决方法上面! 内心思路: w11电脑更新过系统后,我的电脑是常年不关机的,但是一天突然断网,试了很多方法都连不上,重启电脑就会好࿰…...
go-zero学习笔记(五)
api自定义中间件 1. 修改.api文件 syntax"v1"type (GetInfoReq {IDs []string json:"IDs"}GetInfoData {ID string json:"ID"Name string json:"Name"MD5 string json:"md5"Size int64 json:"Size"Up…...
DeepSeek技术全景解析:架构创新与行业差异化竞争力
一、DeepSeek技术体系的核心突破 架构设计:效率与性能的双重革新 Multi-head Latent Attention (MLA):通过将注意力头维度与隐藏层解耦,实现显存占用降低30%的同时支持4096超长上下文窗口。深度优化的MoE架构:结合256个路由专家…...
函数中的形参和实参(吐槽)
def greet_user(user_name):print(f"Hello,{user_name.title()}!")greet_user("zhangsan") 在以上函数中,user_name是形参, 在greet_user("zhangsan")中,值“zhangsan”是实参。这本身没什么大问题。 但是这…...
使用 Promptic 进行对话管理需要具备python技术中的那些编程能力?
使用 Promptic 进行对话管理时,需要掌握一些基础的编程知识和技能,以下是详细说明: 1. Python 编程基础 Promptic 是一个基于 Python 的开发框架,因此需要具备一定的 Python 编程能力,包括: 函数定义与使用:了解如何定义函数、使用参数和返回值。类型注解:熟悉 Python…...
【模块】 ASFF 模块
ASFF (Adaptively Spatial Feature Fusion) 方法针对单次射击物体检测器的特征金字塔中存在的不同特征尺度之间的不一致性问题,提出了一种新颖的数据驱动策略进行金字塔特征融合。通过学习空间上筛选冲突信息的方法,减少了特征之间的不一致性,…...
第二十四周:OpenPose:使用部分亲和场的实时多人2D姿态估计
OpenPose 摘要Abstract文章信息引言方法同时进行检测和关联关键部位检测的置信图PAF使用PAF进行多人解析 关键代码实验结果创新与不足总结 摘要 本篇博客介绍了一种实时多人2D姿态估计框架——OpenPose,其核心思想是通过自底向上的全局关联策略,解决传统…...
ReACT agent和FC agent
rag系列文章目录 文章目录 rag系列文章目录前言一、简介二、示例说明三、对比总结 前言 大模型时代llm正在改变我们的生活,它可以帮助人类写作,写代码,翻译等等,但是llm的知识在训练时被冻结,无法直接使用api接入外部…...
大数据之常用Linux操作
一、 修改文件夹的所有者和所属组均为test用户 chown test:test /opt/文件夹名称二、使用rsync同步文件 rsync主要用于备份和镜像。具有速度快、避免复制相同内容和支持符号链接的优点。 rsync -av $pdir/$fname $user$host:$pdir/$fname三、配置环境变…...
计算机视觉行业洞察--影像行业系列第一期
计算机视觉行业产业链的上下游构成相对清晰,从基础技术研发到具体应用场景的多个环节相对成熟。 以下是我结合VisionChina经历和行业龙头企业对计算机视觉行业产业链上下游的拆解总结。 上下游总结 上游产业链分为软硬件两类,视觉的硬件主要指芯片、…...
自定义实现简版状态机
状态机(State Machine)是一种用于描述系统行为的数学模型,广泛应用于计算机科学、工程和自动化等领域。它通过定义系统的状态、事件和转移来模拟系统的动态行为。 基本概念 状态(State):系统在某一时刻的特…...
【Deepseek】Linux 本地部署 Deepseek
前言 本文介绍在 Linux 系统上部署 Deepseek AI。本文教程是面向所有想体验 AI 玩家的一个简易教程,因此即使是小白也可以轻松完成体验,话不多说立马着手去干。 [注]:笔者使用的系统为 Ubuntu 24.10 1. 关于 ollama Ollama 是一款开源应用…...
JavaScript系列(83)--正则表达式高级详解
JavaScript 正则表达式高级详解 🎯 正则表达式是处理文本的强大工具,掌握其高级特性可以让我们更高效地处理复杂的文本匹配和处理任务。让我们深入探讨JavaScript中正则表达式的高级应用。 正则表达式基础回顾 🌟 💡 小知识&…...
【行业解决方案篇九】【DeepSeek能源勘探:地震波数据智能解释】
第一章 先导课:给地球做CT的百年难题 各位老铁,今天咱们要聊的这个话题绝对硬核——给地球做CT还要用人工智能,这事儿到底有多刺激?想象一下你拿着医院CT报告单,但扫描对象换成深埋地下5000米的油气层,扫描仪换成总长300公里的地震波阵列,这操作难度直接飙升到地狱级。…...
密度提升30%!Intel 18A工艺正式开放代工
快科技2月23日消息,Intel官方网站悄然更新了对于18A(1.8nm级)工艺节点的描述,称已经做好了迎接客户项目的准备,将在今年上半年开始流片,有需求的客户可以随时联系。 Intel宣称,这是在北美地区率先量产的2nm以下工艺节…...
ESP32S3:参考官方提供的led_strip组件使用 SPI + DMA 方式驱动WS2812 RGB灯的实现思路 (实现各个平台移植使用该方式)
目录 引言使用SPI + DMA 方式实现思路分析1. 查看WS2812的datasheet手册2. 根据官方的led_strip组件的方式,自己手把手实现一遍3.完整的程序(实现霓虹灯效果)引言 参考官方提供的led_strip组件使用 SPI + DMA 方式驱动WS2812 RGB灯的实现思路,只有明白实现的思路,方能将其…...
java实现多图合成mp4和视频附件下载
java实现多图合成mp4和视频附件下载 在wutool中,封装了视频处理工具类,基于javacv和ffmpeg库,实现多图合成mp4、视频http附件下载等。 关于wutool wutool是一个java代码片段收集库,针对特定场景提供轻量解决方案,只…...
VulnOSv2 靶机渗透测试
春秋蝉鸣少年归~ arp发现靶机ip地址 发现开放80端口那先去访问一下 问题不大,没有什么有用的提示那就上dirb跑一下 这里给了一个版本号 通过searchsploit搜索了一下没有这个版本的poc/exp去网上搜搜看 这个也试了一下也利用不了回到页面上发现有个website可以点 然后…...
【STM32】内存管理
【STM32】内存管理 文章目录 【STM32】内存管理1、内存管理简介疑问:为啥不用标准的 C 库自带的内存管理算法?2、分块式内存管理(掌握)分配方向分配原理释放原理分块内存管理 管理内存情况 3、内存管理使用(掌握&#…...
【环境配置】maven,mysql,node.js,vue的快速配置与上手
【环境配置】maven,mysql,node.js,vue的快速配置与上手 我们在利用springbootvue来进行全栈项目开发时,需要做很多的准备工作,其中maven,mysql,node,js和vue的配置是必不可少的。 本期我们尽可能精简地介绍它们的配置以及快速上手。 1.maven 1.1.下载…...
前端实现socket 中断重连
前端代码 let ws;let reconnectAttempts 0;const maxReconnectAttempts 5;let reconnectTimer null;// 初始化连接function connect() {ws new WebSocket(ws://localhost:3001);ws.onopen () > {console.log(✅ 连接成功);reconnectAttempts 0; // 重置重连计数器docu…...
【深度学习】Transformer 的常见的位置编码有哪些
Transformer 位置编码(Positional Encoding)主要用于弥补 自注意力机制(Self-Attention) 对位置信息的忽略,常见的方案有以下几种: 1. 绝对位置编码(Absolute Positional Encoding) …...
HybridCLR+Adressable+Springboot热更
本文章会手把手教大家如何搭建HybridCLRAdressableSpringboot热更。 创作不易,动动发财的小手点个赞。 安装华佗 首先我们按照官网的快速上手指南搭建一个简易的项目: 快速上手 | HybridCLR 注意在热更的代码里添加程序集。把用到的工具放到程序集里…...
收到线上服务器出现cpu告警一般怎么排查?
当线上服务器出现CPU告警时,可以按照以下步骤进行系统性排查,逐步定位问题根源: 1. 快速确认CPU使用情况 命令工具:top # 实时查看CPU占用(按P排序进程) htop …...
买股票的最佳时机 - 2
买卖股票的最佳时机 III 题目描述: 提示: 1 < prices.length < 1050 < prices[i] < 105 分析过程: 写动态规划,我们需要考虑一下问题: 定义状态状态转移方程初始条件 遍历顺序 4种状态: …...
pytorch入门级项目--基于卷积神经网络的数字识别
文章目录 前言1.数据集的介绍2.数据集的准备3.数据集的加载4.自定义网络模型4.1卷积操作4.2池化操作4.3模型搭建 5.模型训练5.1选择损失函数和优化器5.2训练 6.模型的保存7.模型的验证结语 前言 本篇博客主要针对pytorch入门级的教程,实现了一个基于卷积神经网络&a…...
【Java】求绝对值
目录 引言基础方法Math.abs()适用类型与语法代码示例 特殊数值处理复数绝对值(模)大整数与高精度小数 底层实现与性能优化位运算技巧(仅限int类型)最小值溢出与 Math.absExact()解决方案1:手动判断解决方案2࿰…...
简单爬虫:东方财富网股票数据爬取(20231230)
目标网站:https://quote.eastmoney.com/center/gridlist.html#hs_a_board 需求:将东方财富网行情中心不同板块的股票数据爬取下来 目标是将各个选项卡的股票数据全部爬取并以excel文件保存在本地。 查看网页源代码发现并没有目标数据,因此需…...
Vue学习教程-14内置指令
文章目录 前言一、v-text指令二、v-html指令三、v-cloak指令四、v-once指令五、v-pre指令六、其他指令 前言 Vue.js 提供了许多内置指令(Directives),这些指令用于在模板中添加特殊功能。内置指令以 v- 前缀开始。 v-text : 更新元素的 tex…...
Java——抽象类
在Java中,抽象类(Abstract Class) 是一种特殊的类,用于定义部分实现的类结构,同时允许子类提供具体的实现。抽象类通常用于定义通用的行为或属性,而将具体的实现细节留给子类。 1. 抽象类的定义 语法&…...
js数据类型检测
JavaScript的数据类型检测 typeof操作符 适用场景 基本数据类型快速判断:适用于快速判断变量是否为number、string、boolean、undefined、function等基本数据类型。比如在函数参数检查中,若要求传入数字参数,可用typeof来初步判断。函数类型…...
Maven+SSM+SpringBoot+Mybatis-Plus
SSM技术栈:spring6、springmvc、mybatis、springboot3、mybatis-plus、druid; 前端: node、npm、vue 快速掌握:全新SSMSpring BootMyBatis-Plus实战精讲...
【爬虫】request库
文章目录 发送请求响应对象响应数据的方式中文乱码问题响应对象的其他属性或方法 发送带参数的请求headers和查询参数 Requests——发送http请求,获取响应数据 首先,请确保: 已安装 RequestsRequests 是最新的 让我们从一些简单的示例开始…...
Docker内存芭蕾:优雅调整容器内存的极限艺术
title: “💾 Docker内存芭蕾:优雅调整容器内存的极限艺术” author: “Cjs” date: “2025-2-23” emoji: “🩰💥📊” 当你的容器变成内存吸血鬼时… 🚀 完美内存编排示范 📜 智能内存管家脚本…...
云手机如何进行经纬度修改
云手机如何进行经纬度修改 云手机修改经纬度的方法因不同服务商和操作方式有所差异,以下是综合多个来源的常用方法及注意事项: 通过ADB命令注入GPS数据(适用于技术用户) 1.连接云手机 使用ADB工具连接云手机服务器,…...
力扣-贪心-53 最大子数组和
思路 先把每一个值都加到当前集合中,记录当前的和,直到当前记录和小于0了,再重置改记录,再次尝试累加 代码 class Solution { public:int maxSubArray(vector<int>& nums) {int res INT32_MIN;int curSum 0;for(in…...
【c语言】函数_作业详解
前言: 对应鹏哥专升本c语言,51集 内容: 找出10个数值中的最大值, #include <stdio.h> //求10个整数中的最大值 int main() {//准备10个整数 //int arr[10] {1,2,3,4,13,6,7,8,9,-2};//用于循环10次int i 0;//也可以自…...