【C++】通过红黑树封装map和set

前言:

通过之前的学习,我们已经学会了红黑树和map、set。这次我们要实现自己的map和set,对,使用红黑树进行封装!

当然,红黑树内容这里就不在赘述,我们会复用红黑树的代码,所以先将红黑树头文件代码拷贝过来。当然,也需要创建两个新的头文件,我将它们命名为"mymap.h"和"myset.h"。

这里把红黑树中的前中序遍历删除了,因为用不上。

提前声明,RBTree.h中的方法都是大写的,因为要和map和set类区分,这里map和set是按照STL标准实现的。

一:创建头文件

创建"mymap.h"和"myset.h",并包含"RBTree.h"头文件(这个就是我们之前实现的红黑树头文件,大家随意命名),之后为了方便后续操作,我们将它们都封装到一个bit命名空间中去。

这里set只需要K,一个模板参数;而map需要 K,V两个模板参数。其中的私有成员变量是红黑树。

二:修改模板参数 

此时我们面临第一个问题,红黑树在其他类中声明,需要传入模板参数,但是set需要一个模板参数;map需要两个模板参数,我们该怎么想RBTree传入参数呢?

此时我们就需要将RBTree的模板参数修改了,其实C++源代码也是这么干的。

但是在此之前,还有一个问题,我们定义树节点的时候也应该修正。我们先来看看RBTree是如何修改的:

template<class K, class T>
class RBTree
{//......
}

它将第二个参数修改为了T。

此时你慌了,WTF!这?这后面的代码不都要修改吗?那map和set又是怎么传入参数的?

因为set只存K,所以可以传入两个K;而map存键值对,我们第一个传入K,第二个传入pair<K, V>即可。 

map头文件:

#pragma once
#include"RBTree.h"namespace bit
{template<class K, class V>class map{public:private:RBTree<K, pair<K, V>> _t;};
}

set头文件:

#pragma once
#include"RBTree.h"namespace bit
{template<class K>class set{public:private:RBTree<K, K> _t;};
}

当然,树节点里面的也不再是只针对K, V存储了,而是存的T。

//定义树节点
template<class T>
struct RBTreeNode
{//让编译器生成默认构造RBTreeNode() = default;//写一个构造RBTreeNode(const T& data): _data(data){}T _data;RBTreeNode* _left = nullptr;RBTreeNode* _right = nullptr;RBTreeNode* _parent = nullptr;  //父节点Colour _col = RED;     //默认为红色
};

看到这里你可能一头雾水,没事,我们画个图你就懂了: 

此时你就会想:为啥底层要这样设计?一个参数看起来也可以完成。

对,可以。但是你可曾想过,如果设计为一个参数,是不是每次比较(只和键比较)是不是每次都要再套一层,增加了代码复杂度。而多写一个K作为参数,我们比较的时候就会少些操作,比从pair中提取first更高效。

而且多写一个Key会使查找和比较更加轻松。也更方便解耦。

所以我们每次插入的就不再是pair,而是T类型的data,所以这里修改Insert代码:

//插入
bool Insert(const T& data)
{
}

如果是set的insert插入的就是K;map的insert插入的就是pair<K, V>。

三:每个类增加仿函数

此时你兴致勃勃的去修改Insert代码(以下只展示修改部分):

bool Insert(const T& data)
{if (_root == nullptr){Node* newNode = new Node(data);  //修改部分_root = newNode;_root->_col = BLACK;  //更新根节点 并置为黑色return true;}//...    //新建节点cur = new Node(data);  //修改部分
}

但聪明的你发现,这里的比较会出现问题:

Node* parent = nullptr;
Node* cur = _root;
while (cur)
{if (kv.first > cur->_kv.first){parent = cur;cur = cur->_right;}else if (kv.first < cur->_kv.first){parent = cur;cur = cur->_left;}else{//已经存在该值了 不处理return false;}
}

我怎知道到底该比较哪个呢?我又不知道上面传入的到底是set还是map,这可咋办?完犊子了。后面的比较不都歇菜了?

对,你当然不知道,这是由谁传给你你才知道的,也就是谁封装得你,谁知道(就像qsort函数一样)。所以我们应该先让set和map对RBTree这个类传入一个能转换的,之后才能比较。

此时就需要用到仿函数了,大家都知道,仿函数是一个类,里面实质上就是对“()”的重载,所以我们在set和map类中写一个内部类,map的仿函数传入pair<K, V>放回first;set传入K,返回K。

先对map添加仿函数: 

template<class K, class V>
class map
{//template<K, V>   此时不需要给内部类加模板参数 因为它知道外部的模板参数struct MapOfK{const K& operator()(const pair<K, V>& kv){return kv.first;}};
public:
private:RBTree<K, pair<K, V>, MapOfK> _t; //传入仿函数
};

在对set添加仿函数:

template<class K>
class set
{struct SetOfK{const K& operator()(const K& key){return key;}};public:
private:RBTree<K, K, SetOfK> _t;
};

此时注意RBTree的模板参数也需要多一个参数,我们称作:KeyOfT。

//新增模板参数 KeyOfT
template<class K, class T, class KeyOfT>
class RBTree
{//...
};

之后我们就可以通过KeyOfT来实现具体比较的是哪个操作对象了,为了方便,因为仿函数是一个类,我们在RBTree类中实例化一个KeyOfT对象命名为kot,只要对T进行比较就转换一下:

template<class K, class T, class KeyOfT>
class RBTree
{
public://对RBTreeNode进行重命名using Node = RBTreeNode<T>;//实例化一个KeyOfT对象KeyOfT kot;//插入bool Insert(const T& data){if (_root == nullptr){Node* newNode = new Node(data);_root = newNode;_root->_col = BLACK;  //更新根节点 并置为黑色return true;}Node* parent = nullptr;Node* cur = _root;while (cur){//if (kv.first > cur->_kv.first)if (kot(data) > kot(cur->_data))  //使用kot{parent = cur;cur = cur->_right;}//else if (kv.first < cur->_kv.first)else if (kot(data) < kot(cur->_data)) //使用kot{parent = cur;cur = cur->_left;}else{//已经存在该值了 不处理return false;}}//新建节点cur = new Node(data);//if (kv.first > parent->_kv.first)if (kot(data) > kot(parent->_data)) //使用kot{parent->_right = cur;}else{parent->_left = cur;}cur->_parent = parent; //记得更新父节点//调整树...
}

对,后面的调整不会和data比较,无需修改,修改的部分只有这么点。

之后就是修改Find函数了: 

bool Find(const K& key)
{Node* cur = _root;while (cur){//if (key > cur->_kv.first)if (kot(data) > kot(cur->_data))  //kot替换{cur = cur->_right;}else if (kot(data) < kot(cur->_data)) //kot替换{cur = cur->_left;}else{return true;}}return false;
}

之后不要忘记在set和map中添加对应的insert方法,本质也就是调用红黑树的Insert方法。

map中insert方法:

bool insert(const pair<K, V>& kv)
{return _t.Insert(kv);
}

set中insert方法:

bool insert(const K& key)
{return _t.Insert(key);
}

这时候不要着急,我们要调试一下代码了。这里给出test.cpp代码:

#include"myset.h"
#include"mymap.h"namespace bit
{void test_set(){set<int> s;int a[] = { 17, 18, 23, 34, 27, 15, 9, 6, 8, 5, 25 };for (auto e : a){cout << e << endl;s.insert(e);}}void test_map(){map<int, int> m;int a[] = { 17, 18, 23, 34, 27, 15, 9, 6, 8, 5, 25 };for (auto e : a){cout << e << endl;m.insert({ e, e });}}
}int main()
{//bit::test_set();bit::test_map();return 0;
}

小编这里没有问题,相信大家也是这样(哈哈)。 

四:实现迭代器

上面其实都不算很难,本篇最难的是迭代器的实现,这是本篇的核心知识。

我们知道迭代器的本质就是指针,难道说我们直接在红黑树中定义一个名为iterator的节点指针,之后再实现其他方法吗?但是大家都用过迭代器,这里以list举例,一般用法为(当然你可能会说我一般是使用范围for,但是我们一般想控制具体一点会使用如下方法):

int main()
{list<int> l = { 1, 2, 3 };list<int>::iterator it = l.begin();while (it != l.end()){cout << *it << " ";++it;}return 0;
}

我们对it做了解引用,可以发现它就是指针,而且可以++。这里我们想对map和set也实现迭代器该怎么办呢?对了,还是对红黑树实现迭代器,而map和set调用红黑树迭代器。

这里我们如何对红黑树实现迭代器呢?在里面定义指针之后实现类似的方法吗?

不,这样做并不好,我们要将其解耦,也就是说,再定义一个类来专门实现迭代器。

问题接踵而至,这个模板参数该是什么呢?既然是一个指针,指针里存放的是树节点,所以T即可。里面的成员变量就是RBTreeNode的指针。为了方便操作,我们将其声明为结构体。

//定义迭代器
template<class T>
struct RBTreeIterator
{//或者使用 typedef RBTreeNode<T> Node;  看个人习惯using Node = RBTreeNode<T>;//为了减少代码量 重命名RBTreeIteratorusing Self = RBTreeIterator;Node* _node = nullptr;
};

这里我们迭代器的模型就构建好了,之后我们就要实现++、!=、*、->、--等运算符的重载了。

但是现行暂停,我们思考一下,我们平时是这样赋值迭代器的:

//拿刚才的list举例   l 是list对象
list<int>::iterator it = l.begin();

这就说明begin是一个方法,并且在list中已经被定义了。所以这里我们先在红黑树中定义这个begin方法。我们知道,begin返回的就是这个数据结构中第一个数据的位置,此时我们数据结构是红黑树,它的第一个节点位置在哪里?

中序遍历的第一个位置,也就是最小节点;但是我们要用中序递归方式找吗?仔细思考发现它也是树中最左边的节点,这个就很好找,所以我们现在RBTree中将RBTreeIterator重命名为Iterator,之后实现Begin方法。

template<class K, class T, class KeyOfT>
class RBTree
{
public://对RBTreeNode进行重命名using Node = RBTreeNode<T>;//重命名RBTreeIterator 为 Iteratortypedef RBTreeIterator<T> Iterator;//实现Begin方法Iterator Begin(){//找最左边节点Node* leftMost = _root;while (leftMost->_left){leftMost = leftMost->_left;}//返回的是迭代器 需要调用其构造方法return Iterator(leftMost);}//...
};

而对于End方法,也非常简单,我们知道是最后一个节点的下一个位置,其实也就是空指针,所以我们补充End方法。

Iterator End()
{return Iterator(nullptr);
}

tips:为了将其和set、map区分,这里使用大写来定义该方法。

返回的类型当然是迭代器类型,因为我们一会还有对这个迭代器进行++等操作。但是我们刚才在迭代器中没有写构造方法,所以补充上去:

//定义迭代器
template<class T>
struct RBTreeIterator
{//或者使用 typedef RBTreeNode<T> Node;  看个人习惯using Node = RBTreeNode<T>;//为了减少代码量 重命名RBTreeIteratorusing Self = RBTreeIterator;//构造方法RBTreeIterator(Node* node): _node(node){}Node* _node = nullptr;
};

五:实现迭代器运算符的重载 

1.++运算符重载

我们是中序遍历,下一个节点时什么?先来考虑最简单的情况,就是当前遍历节点就是中间节点,下一个节点就应该是右子树的最左边节点:

对,上面的情况就这么简单。但是如果右子树为空呢?下图我们只关心如何++,不关心是否为红黑树。

可以发现这其实是一个循环,假设当前it处于cur位置,我们定义一个parent节点(也就是parent = cur->_parent),之后当cur为parent->_left时跳出循环。当然要考虑parent为空的情况。

Self& operator++()
{if (_node->_right){//此时右子树不为空 找右子树最左边节点_node = _node->_right;while (_node->_left){_node = _node->_left;}}else{//右子树为空Node* cur = _node;Node* parent = cur->_parent;//parent可能为空while (parent && cur != parent->_left){cur = parent;parent = cur->_parent;}_node = parent; //记得更改_node 赋值为parent}return *this;
}

2.==和!=运算符重载

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

3.实现->和*运算符重载 

这个也能简单,解引用返回存储类型的引用;->返回类型的地址,也就是指针。 

T& operator*()
{return _node->_data;
}T* operator->()
{return &_node->_data;
}

到这里先暂停,我们要测试一下代码了,所以去set和map中实现迭代器。 

这里先完善set中的迭代器并测试set是否正确。所以在set中添加代码:

typedef RBTree<K, K, SetOfK>::Iterator iterator;//实现begin
iterator begin()
{return _t.Begin();
}//实现end
iterator end()
{return _t.End();
}

之后测试代码中测试范围for:

namespace bit
{void test_set(){set<int> s;int a[] = { 17, 18, 23, 34, 27, 15, 9, 6, 8, 5, 25 };for (auto e : a){cout << e << endl;s.insert(e);}for (auto e : s){cout << e << " ";}}
}int main()
{bit::test_set();//bit::test_map();return 0;
}

我们避免程序直接崩溃,可以现重新生成解决方案,发现程序崩溃……哈哈,这里确实有一个坑。当我们在一个类中声明其他类时,编译器不知道这个类是静态成员还是类型,所以用typename明确指出这是一个类型。

当然还有另外一个解释:因为此时没有实例化RBTree 不知道是什么类型,编译无法通过。所以加上typename在编译的时候确定类型。

补充(来自AI)

核心机制:

  1. 模板的二次编译机制

    • 模板会经历 第一次语法检查(看到模板定义时)和 第二次实例化检查(具体调用时)。

    • 在第一次检查时,编译器并不知道 T::NestedType 是什么(因为 T 尚未确定),所以需要 typename 明确告诉编译器:"这是一个类型,你先别报错,等实例化时再确认"。

  2. typename 的本质作用

    • 不是让编译器"先不填充类型",而是解决 语法歧义

    • 在没有 typename 时,T::NestedType 可能被误认为是静态成员变量(例如 T::value),导致语法解析错误。typename 强制声明这是一个类型。

所以此时我们在set中定义迭代器时加上typename即可通过编译:

typedef typename RBTree<K, K, SetOfK>::Iterator iterator;

此时运行代码输出结果为:

之后就是map的迭代器这里不再赘述。

4.--运算符重载

这里我们给自己上上强度,多加一个--运算符重载,说白了就是和++哪里镜像的。但是最开始传入的是End,所以我们要先找最右边节点,也就是判断是否为空,之后赋值。

其他情况,我们先看是否有左子树,有左子树就是左子树最右节点:

当没有左子树的时候,和++一样,找第一个cur == parent->_right:

OK,当你开始写--的时候,第一步判空,此时新的问题出现了,我们要找树的根节点,但是我们不知道树的根节点!(没想到吧,我们又要修改参数了,呜呜~~我都要哭了……啊啊啊!)

此时我们应该怎么解决?也就是对RBTreeIterator多增加一个参数,也就是要记录根节点。

//定义迭代器
template<class T>
struct RBTreeIterator
{//...//多定义一个成员 专门记录根节点Node* _root = nullptr;
};

所以此时RBTreeIterator的构造方法也要修改。

//定义迭代器
template<class T>
struct RBTreeIterator
{//构造方法 //向构造方法多增加root赋值RBTreeIterator(Node* node, Node* root): _node(node), _root(root){}//...//多定义一个成员 专门记录根节点Node* _root = nullptr;
};

那么之前在红黑树中的Begin和End都需要多传入_root参数。

//实现Begin方法
Iterator Begin()
{//找最左边节点Node* leftMost = _root;while (leftMost->_left){leftMost = leftMost->_left;}//返回的是迭代器 需要调用其构造方法return Iterator(leftMost, _root);
}Iterator End()
{return Iterator(nullptr, _root);
}

此时我们就可以拿到根节点了,同时也可以开始完善--运算符重载了。根据之前的图像,可以写出以下代码:

Self& operator--()
{if (_node == nullptr){//找最右边节点Node* rightMost = _root;while (rightMost->_right){rightMost = rightMost->_right;}_node = rightMost;}else if (_node->_left)  //此时有左子树{//找左子树最右节点Node* rightMost = _node->_left;while (rightMost->_right){rightMost = rightMost->_right;}_node = rightMost;}else{//此时没有右子树Node* cur = _node;Node* parent = cur->_parent; //此时要找第一个cur == parent->_right 且parent存在while (parent && cur != parent->_right){cur = parent;parent = cur->_parent;}_node = parent;}return *this;
}

之后我们还拿set来做实验,这次我们倒序遍历,但是注意应该先--it迭代器,因为最开始指向空。

void test_set()
{set<int> s;int a[] = { 17, 18, 23, 34, 27, 15, 9, 6, 8, 5, 25 };for (auto e : a){//cout << e << endl;s.insert(e);}for (auto e : s){cout << e << " ";}cout << endl;//倒着遍历set<int>::iterator it = s.end();while (it != s.begin()){//最开始是 nullptr 所以先----it;cout << *it << " ";}cout << endl;
}

测试结果:

此时我们就几乎实现了所有关于迭代器的基本操作。

六:实现包括const版本的迭代器

注意我上面说的是迭代器基本操作,但是我们知道,底层还存在const_iterator。我们上面实现的迭代器是可以通过解引用来改变里面指向的内容的。但是底层有const_iterator是不允许修改其指向的内容的。

这里我们先区分 int* constconst int* 的区别。 

  • int* const : 指的是指针不能修改,指向的值可以修改。
  • const int* : 指的是指针可以修改,指向的值不能修改。

所以 const_iterator 本质上就是 const int*。

这时聪明的你想到了,那么我们在写一个ConstRBTreeIterator类,把里面的模板参数都写成const类型可不可以呢?当然可以,比如:

template<class T>
struct ConstRBTreeIterator
{//... 与RBTreeIterator代码全部相同//唯一不同的是 * -> 的重载const T& operator*(){return _node->_data;}const T* operator->(){return &_node->_data;}
};

不对劲,你发现只有两个运算符重载不一样,难道为了碟醋,包一盘饺子?这太冗余了。

我们知道模板参数可以像函数传参一样使用,既然只有这两个参数不一样,那是否可以考虑当传入的时候就告知应该生成谁。这里一个是引用,一个是指针。所以我们可以修改RBTreeIterator参数,多传入两个参数,一个是T的应用,一个是T的指针,这样我们就可以减少代码量了。

//定义迭代器           引用        指针
template<class T, class Ref, class Ptr>
struct RBTreeIterator
{//修改位置Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}
};

所以说RBTree中传入的模板参数也要修改,并且多定义一个ConstIterator类型:

template<class K, class T, class KeyOfT>
class RBTree
{
public://对RBTreeNode进行重命名using Node = RBTreeNode<T>;//重命名RBTreeIterator 为 Iteratortypedef RBTreeIterator<T, T&, T*> Iterator;typedef RBTreeIterator<T, const T&, const T*> ConstIterator;//...
};

这里你会很迷,什么钩八?看图:

所以我们通过修改传入参数从而实现对const版本和非const版本的操作。此时我们依旧是先实现set中的const_iterator。

template<class K>
class set
{//...
public:typedef typename RBTree<K, K, SetOfK>::Iterator iterator;typedef typename RBTree<K, K, SetOfK>::ConstIterator const_iterator;//实现beginiterator begin(){return _t.Begin();}//实现enditerator end(){return _t.End();}//实现begin const版本const_iterator begin() const{return _t.Begin();}//实现end const版本const_iterator end() const{return _t.End();}//...
};

如何测试呢?我们在bit命名空间中,添加FuncSet函数,它的参数是const set<int>& s即可,这就是常量类型,不可修改。所以如下:

void FuncSet(const set<int>& s)
{for (auto e : s){cout << e << " ";}cout << endl;
}void test_set()
{set<int> s;int a[] = { 17, 18, 23, 34, 27, 15, 9, 6, 8, 5, 25 };for (auto e : a){//cout << e << endl;s.insert(e);}for (auto e : s){cout << e << " ";}cout << endl;//倒着遍历set<int>::iterator it = s.end();while (it != s.begin()){//最开始是 nullptr 所以先----it;cout << *it << " ";}cout << endl;FuncSet(s);
}

结果如下:

之后补充map的代码:

template<class K, class V>
class map
{//...
public:typedef typename RBTree<K, pair<K, V>, MapOfK>::Iterator iterator;typedef typename RBTree<K, pair<K, V>, MapOfK>::ConstIterator const_iterator;//实现beginiterator begin(){return _t.Begin();}//实现enditerator end(){return _t.End();}//实现begin const版本const_iterator begin() const{return _t.Begin();}//实现end const版本const_iterator end() const{return _t.End();}//...
};

 七:修改Insert

之前我们在set和map篇中讲到过,他们的insert方法并不是简单的返回bool。而是一个迭代器,插入节点不存在返回该插入节点的迭代器;存在返回这个位置的迭代器。返回的类型时pair类型(具体细节可以看我的上一篇文章) 。所以我们要修改红色树中的Insert方法。 

//插入
pair<Iterator, bool> Insert(const T& data)
{if (_root == nullptr){Node* newNode = new Node(data);_root = newNode;_root->_col = BLACK;  //更新根节点 并置为黑色// return true;return make_pair(Iterator(newNode, _root), true);}Node* parent = nullptr;Node* cur = _root;while (cur){//if (kv.first > cur->_kv.first)if (kot(data) > kot(cur->_data)){parent = cur;cur = cur->_right;}//else if (kv.first < cur->_kv.first)else if (kot(data) < kot(cur->_data)){parent = cur;cur = cur->_left;}else{//已经存在该值了 不处理//return false;return make_pair(Iterator(cur, _root), false);}}//新建节点cur = new Node(data);//if (kv.first > parent->_kv.first)if (kot(data) > kot(parent->_data)){parent->_right = cur;}else{parent->_left = cur;}cur->_parent = parent; //记得更新父节点if (parent->_col == BLACK){//return true;return make_pair(Iterator(cur, _root), true);}else{//...}//最后都时 _root->_col = BLACK 即可_root->_col = BLACK;//return true;return make_pair(Iterator(cur, _root), true);
}

所以我们也要修改map和set中的insert方法返回值(大家自行修改吧)。 

之后再test_map函数中增加以下代码,测试insert函数: 

//第一次插入90这个键
cout << "第一次插入90这个键" << endl;
pair<map<int, int>::iterator, bool> p = m.insert({90, 100});
cout << p.first->first << ":" << p.first->second << endl;
cout << p.second << endl;cout << "第二次插入90这个键" << endl;
p = m.insert({ 90, 30 });
cout << p.first->first << ":" << p.first->second << endl;
cout << p.second << endl;

运行结果:

八:实现map中[ ]的重载

我们在上一篇讲到了[ ],但是那里没有讲的很清楚,[ ]可以充当insert,也可以查找键对应的值;更可以修改键对应的值,这和它的实现有关,我们先上代码:

V& operator[](const K& key)
{iterator it = insert(make_pair(key, V())).first;return it->second;
}

新增test_map1函数:

void test_map1()
{map<std::string, std::string> dict;dict.insert({ "sort", "排序" });dict.insert({ "left", "左边" });dict.insert({ "right", "右边" });dict["left"] = "左边,剩余";  //修改dict["insert"] = "插入";      //插入dict["value"];                //插入 string使用默认构造map<string, string>::iterator it = dict.begin();while (it != dict.end()){cout << it->first << ":" << it->second << endl;++it;}cout << endl;
}

运行结果:

你肯定好奇,为什么能修改对应的值?为什么我们能直接插入。首先我们封装的[ ]里面复用的是insert,我们在insert中传入的参数对应的值是调用的默认构造,之后我们返回的first拿iterator接收,之后通过iterator重载的->拿到对应的值(second),可以看到返回的是引用。以至于在外部使用=可以修改对应的值!

总结:

这一篇内容其实很难,这里算法是基础,语言比算法还要难。大家不要眼高手低,实现了才知道有多难,多有成就感。最初小编实现的时候全是BUG,这个比红黑树好一点,算法的BUG可能一找要找半个小时,而这个也全是BUG,找得快,但是多。多敲两边你会发现你的C++代码能力突飞猛进,这一章包含了很多基础语法和C++特性,大家不要偷懒!

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

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

相关文章

非凸科技受邀出席AI SPARK活动,共探生成式AI驱动金融新生态

4月19日&#xff0c;由AI SPARK社区主办的“生成式AI创新与应用构建”主题沙龙在北京举行。活动聚焦生成式AI的技术突破与产业融合&#xff0c;围绕大模型优化、多模态应用、存内计算等前沿议题展开深度探讨。非凸科技受邀出席并发表主题演讲&#xff0c;深入解析金融垂直大模型…

【Java IO流】IO流详解

参考笔记&#xff1a;【Java基础-3】吃透Java IO&#xff1a;字节流、字符流、缓冲流_javaio-CSDN博客 目录 1.IO流简介 1.1 什么是IO流&#xff1f; 1.2 IO流的分类 1.3 字符流和字节流的其他区别 1.4 Java IO流体系图 2.字符编码详解 3. Java的char类型与 Unicode、U…

驱动开发系列56 - Linux Graphics QXL显卡驱动代码分析(三)显示模式设置

一:概述 如之前介绍,在qxl_pci_probe 中会调用 qxl_modeset_init 来初始化屏幕分辨率和刷新率,本文详细看下 qxl_modeset_init 的实现过程。即QXL设备的显示模式设置,是如何配置CRTC,Encoder,Connector 的以及创建和更新帧缓冲区的。 二:qxl_modeset_init 分析 in…

Vue3开发常见性能问题知多少

文章目录 1 常见性能优化瓶颈及原因1.1 响应式数据的过度使用1.2 虚拟 DOM 的频繁更新1.3 组件渲染的冗余1.4 大列表渲染的性能问题1.5 计算属性和侦听器的滥用1.6 事件处理函数的频繁绑定1.7 异步组件的加载性能2 解决方案与优化技巧2.1 合理使用响应式数据2.2 优化虚拟 DOM 更…

Rust Ubuntu下编译生成环境win程序踩坑指南

前言&#xff1a; 1&#xff0c;公司要给一线搞一个升级程序&#xff0c;需要在win下跑。 之前都是找开发总监帮忙&#xff0c;但是他最近比较忙。就让我自己搞。有了下文.。说来惭愧&#xff0c;之前写过一篇ubuntu下编译windows的文章。里面的demo就一句话 fuck world。依赖…

openharmony 4.1 运行busybox工具包(保姆教程)

1.下载 链接&#xff1a;Index of /downloads/binaries 进入其中后&#xff0c;找到 挑选适合你系统架构的版本&#xff0c;例如我这边是 https://busybox.net/downloads/binaries/1.31.0-defconfig-multiarch-musl/busybox-armv7r 右键复制链接 打开迅雷&#xff0c;直接粘…

算法四 习题 1.3

数组实现栈 #include <iostream> #include <vector> #include <stdexcept> using namespace std;class MyStack { private:vector<int> data; // 用于存储栈元素的数组public:// 构造函数MyStack() {}// 入栈操作void push(int val) {data.push_back…

GD32F407单片机开发入门(十七)内部RTC实时时钟及实战含源码

文章目录 一.概要二.RTC基本特点三.GD32单片机RTC内部结构图四.配置一个RTC走秒例程五.工程源代码下载六.小结 一.概要 RTC&#xff08;Real-Time Clock&#xff09;是一种用于追踪和记录实际时间的时钟系统。RTC模块提供了一个包含日期&#xff08;年/月/日&#xff09;和时间…

新能源汽车运动控制器核心芯片选型与优化:MCU、DCDC与CANFD协同设计

摘要&#xff1a;随着新能源汽车产业的迅猛发展&#xff0c;汽车运动控制器的性能和可靠性面临着更高的要求。本文深入探讨了新能源汽车运动控制器中MCU&#xff08;微控制单元&#xff09;、DCDC电源管理芯片和CANFD总线通信芯片的选型要点、优化策略及其协同设计方案。通过综…

2.maven 手动安装 jar包

1.背景 有的时候&#xff0c;maven仓库无法下载&#xff0c;可以手动安装。本文以pentaho-aggdesigner-algorithm-5.1.5-jhyde.jar为例。 2.预先准备 下载文件到本地指定位置。 2.1.安装pom mvn install:install-file \-Dfile/home/wind/tmp/pentaho-aggdesigner-5.1.5-jh…

OpenCV 图形API(75)图像与通道拼接函数-----将 4 个单通道图像矩阵 (GMat) 合并为一个 4 通道的多通道图像矩阵函数merge4()

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 由4个单通道矩阵创建一个4通道矩阵。 该函数将多个矩阵合并为一个单一的多通道矩阵。也就是说&#xff0c;输出矩阵的每一个元素都是输入矩阵对…

AI日报 · 2025年05月02日 | 再见GPT-4!OpenAI CEO 确认 GPT-4 已从 ChatGPT 界面正式移除

1、OpenAI CEO 确认 GPT-4 已从 ChatGPT 界面正式移除 在处理 GPT-4o 更新问题的同时&#xff0c;OpenAI CEO Sam Altman 于 5 月 1 日在 X 平台发文&#xff0c;正式确认初代 GPT-4 模型已从 ChatGPT 主用户界面中移除。此举遵循了 OpenAI 此前公布的计划&#xff0c;即在 4 …

patch命令在代码管理中的应用

patch 是一个用于将差异文件&#xff08;补丁&#xff09;应用到源代码的工具&#xff0c;常用于修复 bug、添加功能或调整代码结构。在您提供的代码中&#xff0c;patch 命令通过一系列补丁文件&#xff08;.patch&#xff09;修改了 open-amp 库的源代码。 patch 命令的核心作…

spring-ai集成langfuse

1、pom文件 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apache.org/POM/4.…

PyTorch 与 TensorFlow:深度学习框架的深度剖析与实战对比

PyTorch 与 TensorFlow&#xff1a;深度学习框架的深度剖析与实战对比 摘要 &#xff1a;本文深入对比 PyTorch 与 TensorFlow 两大深度学习框架&#xff0c;从核心架构、优缺点、适用场景等多维度剖析&#xff0c;结合实例讲解&#xff0c;帮助开发者清晰理解两者特性&#x…

如何配置NGINX作为反向代理服务器来缓存后端服务的响应?

大家好&#xff0c;我是锋哥。今天分享关于【如何配置NGINX作为反向代理服务器来缓存后端服务的响应&#xff1f;】面试题。希望对大家有帮助&#xff1b; 如何配置NGINX作为反向代理服务器来缓存后端服务的响应&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源…

DiT:文档图像Transformer 的自监督预训练

摘要 图像transformer&#xff08;Image Transformer&#xff09;最近在自然图像理解方面取得了显著进展&#xff0c; 无论是使用监督&#xff08;ViT、DeiT等&#xff09;还是自监督&#xff08;BEiT、MAE等&#xff09;预训练技术。在本文中&#xff0c;我们提出了DiT&#…

51c嵌入式~电路~合集4

我自己的原文哦~ https://blog.51cto.com/whaosoft/11888986 一、电流检测电路 电流检测的应用 电路检测电路常用于&#xff1a;高压短路保护、电机控制、DC/DC换流器、系统功耗管理、二次电池的电流管理、蓄电池管理等电流检测等场景。电路专辑 对于大部分应用&#xff…

【Git】万字详解 Git 的原理与使用(上)

&#x1f970;&#x1f970;&#x1f970;来都来了&#xff0c;不妨点个关注叭&#xff01; &#x1f449;博客主页&#xff1a;欢迎各位大佬!&#x1f448; 文章目录 1. 初识 Git1.1 Git 是什么&#xff1f;1.2 为什么要有 Git 2. 安装 Git2.1 Linux-Ubuntu 安装 Git2.2 Windo…

【原创开发】无印去水印[特殊字符]短视频去水印工具[特殊字符]支持一键批量解析

支持&#xff1a;快手&#xff0c;抖音&#xff0c;小红书&#xff0c;哔哩哔哩&#xff0c;等多款应用去水印&#xff0c;支持图集解析下载 【应用名称】&#xff1a;无印 【应用版本】&#xff1a;1.3 【应用大小】&#xff1a;17M 【测试机型】&#xff1a;小米14 【下载链…