【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.pswp.cn/diannao/81414.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/81414.shtml
英文地址,請注明出處:http://en.pswp.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 【下載鏈…