前言:
通過之前的學習,我們已經學會了紅黑樹和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):
核心機制:
模板的二次編譯機制:
模板會經歷?第一次語法檢查(看到模板定義時)和?第二次實例化檢查(具體調用時)。
在第一次檢查時,編譯器并不知道?
T::NestedType
?是什么(因為?T
?尚未確定),所以需要?typename
?明確告訴編譯器:"這是一個類型,你先別報錯,等實例化時再確認"。
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* const 和 const 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++特性,大家不要偷懶!