数据结构之进阶二叉树(二叉搜索树和AVL树、红黑树的实现)超详细解析,附实操图和搜索二叉树的实现过程图
绪论
“生命有如铁砧,愈被敲打,愈能发出火花。——伽利略”;本章主要是数据结构 二叉树的进阶知识,若之前没学过二叉树建议看看这篇文章一篇掌握二叉树,本章的知识从浅到深的对搜索二叉树的使用进行了介绍和对其底层逻辑的实现进行了讲解,希望能对你有所帮助。话不多说安全带系好,发车啦(建议电脑观看)。
1.二叉搜索树
1.1二叉搜索树的概念:
二叉搜索树又称二叉排序树/二叉查找树**,它或者是一棵空树。二叉搜索树还有二叉树的性质不同的是其性质有:
1. 大于子树根节点的值存在根节点的右子树
2. 小于子树根节点的值存在根节点的左子树
3. 左右子树都是二叉搜索树
换种说法:若它的左子树不为空,则左子树上所有节点的值都小于根节点的值、若它的右子树不为空,则右子树上所有节点的值都大于根节点的值。
1.2二叉树搜索树一些常用功能(基本功能)
1.2.1 插入数据
有了前面的二叉搜索树的性质后,能很好的想到其插入的实现,只不过其中会有一些小的细节需要注意!
主要是判断当前子树根节点的值和传进来的值进行比较,然后当走到为空时,表示就是为要插入的位置
注意细节:
- 当根节点为空时,修改根节点即可。
- 我们需要记录父节点然后链接父子节点。
具体见下面代码:
bool Insert(const K& key, const V& val)
{if (_root == nullptr){_root = new Node(key);return true;}Node* cur = _root;Node* parent = nullptr;while (cur){if (cur->_key == key){return false;//不插入相同的值}else if (cur->_key > key){parent = cur;cur = cur->_left;}else{parent = cur;cur = cur->_right;}}//当循环结束表示已经到了所要插入节点的位置!cur = new Node(key);if (parent->_key > key){parent->_left = cur;}else{parent->_right = cur;}return true;
}
1.2.2 删除数据
对于删除数据来说有几个不同的情况:
- 当删除节点没有左右节点时,我们直接将其节点删除即可
- 当删除节点只有左节点/右节点时
将这个左节点/右节点链接到删除节点的父节点- 当删除节点用同时有两个节点
使用替换删除法(找到删除节点左子树的最右节点(最大节点)/删除节点右子树的最左节点(最小节点)后替换到删除的节点后删除,不过删除的时候要注意链接(2)!)
注意:假如交换的节点有子树那么需要注意将其链接到交换节点的父节点
此处可以把1和2情况并到一起写,即当有左为空/右为空时不管右/左为不为空都连接到其父节点
具体代码实现:
bool Erase(const K& key)
{Node* parent = nullptr, * child = nullptr;//删除节点的父亲Node* cur = _root;//所要删除的节点while (cur){if (cur->_key == key) //找到删除节点{//可以把左右为空和 左或右不为空写在一起if (cur->_left == nullptr)//当其左边为空时{if (cur == _root){_root = cur->_right;}else{//即当有左为空时不管删除节点的右为不为空都直接连接到其父节点if (parent->_left == cur)//判断在父节点的左边还是右边{parent->_left = cur->_right;}else// if (parent->_right = cur){parent->_right = cur->_right;}}delete cur;}else if (cur->_right == nullptr)//当其右边为空时{if (cur == _root){_root = cur->_left;}else{if (parent->_left == cur){parent->_left = cur;}else// if (parent->_right = cur){parent->_right = cur;}}delete cur;}else//当左右同时不为空时{//找到删除节点左子树的最大值/右子树的最小值、与删除节点交换//找到左子树的最右节点Node* tmp = cur->_left;//最右节点Node* p = cur;//最右节点的父节点while (tmp->_right){p = tmp;tmp = tmp->_right;}//父节点链接 最右节点的左节点if (p->_right == tmp) {p->_right = tmp->_left;}else {p->_left = tmp->_left;}swap(tmp->_key, cur->_key);//交换后删除即可delete tmp;}return true;}else if (cur->_key > key)//找不到继续循环{parent = cur;cur = cur->_left;}else{parent = cur;cur = cur->_right;}}return false;
}
1.2.3查找数据
直接利用二叉搜索树的性质即可很好的写出查找
通过大的在右小的在左的性质,就能最多树的层数次就能找到
若找不到则就会找到空的位置处
Node* Find(const K& key)
{Node* cur = _root;while (cur){if (cur->_key == key){return cur;//找到返回该节点}else if (cur->_key > key){cur = cur->_left;}else{cur = cur->_right;}}return nullptr;//找不到则返回nullptr
}
1.3二叉树搜索树基本功能用递归式实现
从代码直接分析,若有啥问题可以评论提出喔!
1.3.1插入
bool _InsertR(Node*& root, const K& key)//递归式插入 Recursion
{if (root == nullptr){//此处为引用也就是别名,当修改root就其实就是修改到了树(就不用链接了)root = new Node(key);return true;}if (root->_key == key)//若相等则表示已经插入过了那么返回错误{return false;}else if (root->_key > key)//当root值大于key时往左走{return _InsertR(root->_left, key);}else//当root值小于key时往右走{return _InsertR(root->_right, key);}
}
1.3.2删除
bool _EraseR(Node*& root, const K& key)
{//同样当左右无节点时直接删除//当只有左或者只有右时,链接其右或左到祖先即可//当同时有左右子树时交换删除if (root == nullptr) {//为空表示不存在返回空return false;}if (root->_key > key)//当root值大于key时往左走{return _EraseR(root->_left, key);}else if (root->_key < key)//当root值小于key时往右走{return _EraseR(root->_right, key);}else//找到后{if (root->_left == nullptr){Node* tmp = root;root = root->_right;//因为root是引用,所以当修改root后会影响到整棵树delete tmp;//再将原本的root给释放了return true;}else if (root->_right == nullptr){Node* tmp = root;root = root->_left;//因为root是引用,所以当修改root后会影响到整棵树delete tmp;//再将原本的root给释放了return true;}else{Node* tmp = root->_left;//找到左子树的最大值(最右值)while (tmp->_right){tmp = tmp->_right;}swap(tmp->_key, root->_key);//交换return _EraseR(root->_left, key);//通过递归删除把key删除,里面就包含了链接}}
}
1.3.3查找
此处如果你已经理解了上面的那么这里就非常简单了就不过诉了
Node* _FindR(Node* root, const K& key)
{if (root == nullptr){return nullptr;}if (root->_key < key){return _FindR(root->_right, key);}else if (root->_key > key){return _FindR(root->_left, key);}else{return root;}
}
1.4二叉搜索树的源码
如果是要学习的话,建议一定要将整体的结构框架理清楚了,下面是二叉搜索树的整体代码,包括了构造函数、析构函数、以及一些还没交代的函数。
template<class K>
struct BSTreeNode
{BSTreeNode(const K& key):_key(key), _left(nullptr), _right(nullptr){}K _key;BSTreeNode<K>* _left;BSTreeNode<K>* _right;
};template<class K>
class BSTree
{typedef BSTreeNode<K> Node;
public:BSTree() = default;~BSTree(){_Destory(_root);}//BSTree(const BSTree<K>& t)//{// _Copy(t._root);//}BSTree(const BSTree<K>& t){_root = _Copy(t._root);}BSTree<K>& operator=(BSTree<K> t){//_Destory(_root);//_Copy(t._root);swap(_root, t._root);//现代写法return *this;}void InOrder(){_InOrder(_root);}bool Insert(const K& key){if (_root == nullptr){_root = new Node(key);return true;}Node* cur = _root;Node* parent = nullptr;while (cur){if (cur->_key == key){return false;//不插入相同的值}else if (cur->_key > key){parent = cur;cur = cur->_left;}else{parent = cur;cur = cur->_right;}}//当循环结束表示已经到了所要插入节点的位置!cur = new Node(key);if (parent->_key > key){parent->_left = cur;}else{parent->_right = cur;}return true;}Node* Find(const K& key){Node* cur = _root;while (cur){if (cur->_key == key){return cur;//不插入相同的值}else if (cur->_key > key){cur = cur->_left;}else{cur = cur->_right;}}return nullptr;}//替换法 删除//找到删除节点的左子树的最大值(最左子树的右节点)/右子树的最小值(右子树的最左节点)后与删除的位置进行交换bool Erase(const K& key){Node* parent = nullptr, * child = nullptr;//删除节点的父亲Node* cur = _root;//所要删除的节点while (cur){if (cur->_key == key){//if (cur->_left == nullptr &&// cur->_right == nullptr)//{// delete cur;//}//else if (del->_left && del->_right)//{// //MaxNode(del->_left);//左子树的最大值// Node* min = MinNode(del->_right); //右子树的最小值// swap(del, min);// delete del;//}//可以把左右为空和 左或右不为空写在一起if (cur->_left == nullptr){if (cur == _root){_root = cur->_right;}else{if (parent->_left == cur){parent->_left = cur->_right;}else// if (parent->_right = cur){parent->_right = cur->_right;}}delete cur;}else if (cur->_right == nullptr){if (cur == _root){_root = cur->_left;}else{if (parent->_left == cur){parent->_left = cur;}else// if (parent->_right = cur){parent->_right = cur;}}delete cur;}else{//找到删除节点左子树的最大值/右子树的最小值、与删除节点交换//交换后删除即可Node* tmp = cur->_left;Node* p = cur;while (tmp->_right){p = tmp;tmp = tmp->_right;}if (p->_right == tmp) {p->_right = tmp->_left;}else {p->_left = tmp->_left;}swap(tmp->_key, cur->_key);delete tmp;}return true;}else if (cur->_key > key){parent = cur;cur = cur->_left;}else{parent = cur;cur = cur->_right;}}return false;}bool InsertR(const K& key)//递归式插入 Recursion{return _InsertR(_root, key);}Node* FindR(const K& key){return _FindR(_root, key);}bool EraseR(const K& key){return _EraseR(_root, key);}private:void _Destory(Node* root){if (root == nullptr) return;_Destory(root->_left);_Destory(root->_right);delete root;}//void _Copy(Node* root)//{// if (root == nullptr) return;// _InsertR(_root, root->_key);// _Copy(root->_left);// _Copy(root->_right);//}Node* _Copy(Node* root){if (root == nullptr)return nullptr;Node* newRoot = new Node(root->_key);newRoot->_left = _Copy(root->_left);newRoot->_right = _Copy(root->_right);return newRoot;}void _InOrder(Node* root){if (root == nullptr) return;_InOrder(root->_left);cout << root->_key << " ";_InOrder(root->_right);}//判断root->_key 与 keybool _InsertR(Node*& root, const K& key)//递归式插入 Recursion{if (root == nullptr){root = new Node(key);//此处为引用也就是别名,当修改root就其实就是修改到了树return true;}if (root->_key == key){return false;}else if (root->_key > key){return _InsertR(root->_left, key);}else{return _InsertR(root->_right, key);}}Node* _FindR(Node* root, const K& key){if (root == nullptr){return nullptr;}if (root->_key < key){return _FindR(root->_right, key);}else if (root->_key > key){return _FindR(root->_left, key);}else{return root;}}bool _EraseR(Node*& root, const K& key){//同样当左右无节点时直接删除//当只有左或者只有右时,链接其右或左到祖先即可//当同时有左右子树时交换删除if (root == nullptr) {return false;}if (root->_key > key){return _EraseR(root->_left, key);}else if (root->_key < key){return _EraseR(root->_right, key);}else{if (root->_left == nullptr){Node* tmp = root;root = root->_right;//因为root是引用,所以当修改root后会影响到整棵树delete tmp;//再将原本的root给释放了return true;}else if (root->_right == nullptr){Node* tmp = root;root = root->_left;//因为root是引用,所以当修改root后会影响到整棵树delete tmp;//再将原本的root给释放了return true;}else{Node* tmp = root->_left;//找到左子树的最大值(最右值)while (tmp->_right){tmp = tmp->_right;}swap(tmp->_key, root->_key);//交换return _EraseR(root->_left, key);//通过递归删除把key删除,里面就包含了链接}}}Node* _root;
};
1.5二叉搜索树的应用
- k模型,K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。如:给一个单词word,判断该单词是否拼写正确。
- KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方式在现实生活中非常常见:英汉词典就是英文与中文对应关系<English,meaning>、或者统计个数对应的就是元素和个数的关系<ele,count>。
在上面我们写的是就是k模型,下面我们将其修改为kv模型(对于k模型来说大概的都不需要修改只有一部分需要简单修改)
具体代码为:
template<class K, class V>
struct BSTreeNode
{BSTreeNode(const K& key, const V& val):_key(key), _val(val), _left(nullptr), _right(nullptr){}K _key;V _val;BSTreeNode<K, V>* _left;BSTreeNode<K, V>* _right;
};template<class K, class V>
class BSTree
{typedef BSTreeNode<K, V> Node;
public:BSTree():_root(nullptr){}void InOrder(){_InOrder(_root);}bool Insert(const K& key, const V& val){//1. 当根节点为空时,修改根节点即可。if (_root == nullptr){_root = new Node(key, val);return true;}//2. 我们需要记录父节点然后链接父子节点。Node* cur = _root;Node* parent = nullptr;while (cur){if (cur->_key == key){return false;//不插入相同的值}else if (cur->_key > key){parent = cur;cur = cur->_left;}else{parent = cur;cur = cur->_right;}}//当循环结束表示已经到了所要插入节点的位置!cur = new Node(key, val);if (parent->_key > key){parent->_left = cur;}else{parent->_right = cur;}return true;}Node* Find(const K& key){Node* cur = _root;while (cur){if (cur->_key == key){return cur;//不插入相同的值}else if (cur->_key > key){cur = cur->_left;}else{cur = cur->_right;}}return nullptr;}//替换法 删除//找到删除节点的左子树的最大值(最左子树的右节点)/右子树的最小值(右子树的最左节点)后与删除的位置进行交换bool Erase(const K& key){Node* parent = nullptr, * child = nullptr;//删除节点的父亲Node* cur = _root;//所要删除的节点while (cur){if (cur->_key == key) //找到删除节点{if (cur->_left == nullptr)//当其左边为空时{if (cur == _root){_root = cur->_right;}else{//即当有左为空时不管删除节点的右为不为空都直接连接到其父节点if (parent->_left == cur)//判断在父节点的左边还是右边{parent->_left = cur->_right;}else// if (parent->_right = cur){parent->_right = cur->_right;}}delete cur;}else if (cur->_right == nullptr)//当其右边为空时{if (cur == _root){_root = cur->_left;}else{if (parent->_left == cur){parent->_left = cur;}else// if (parent->_right = cur){parent->_right = cur;}}delete cur;}else//当左右同时不为空时{//找到删除节点左子树的最大值/右子树的最小值、与删除节点交换//找到左子树的最右节点Node* tmp = cur->_left;//最右节点Node* p = cur;//最右节点的父节点while (tmp->_right){p = tmp;tmp = tmp->_right;}//父节点链接 最右节点的左节点if (p->_right == tmp) {p->_right = tmp->_left;}else {p->_left = tmp->_left;}swap(tmp->_key, cur->_key);//交换后删除即可delete tmp;}return true;}else if (cur->_key > key){parent = cur;cur = cur->_left;}else{parent = cur;cur = cur->_right;}}return false;}private:void _InOrder(Node* root){if (root == nullptr) return;_InOrder(root->_left);cout << root->_key << '-' << root->_val << " ";_InOrder(root->_right);}Node* _root = nullptr;
};
英汉词典实现:
int main()
{kv::BSTree<string, string> dict;//插入单词dict.Insert("sort", "种类");dict.Insert("left", "左边");dict.Insert("right", "右边");dict.Insert("insert", "插入");dict.Insert("key", "钥匙");string str;while (cin>>str)//输入所要查找的单词{kv::BSTreeNode<string, string>* ret = dict.Find(str);if (ret){cout << ret->_val << endl;}else{cout << "不存在" << endl;}}return 0;
}
计数实现:
int main()
{string arr[] = { "苹果", "苹果","梨子", "香蕉", "苹果", "桃子", "哈密瓜", "梨子", "苹果", "西瓜", "哈密瓜", "苹果", "桃子" };kv::BSTree<string, int> countTree;for (auto& e : arr){kv::BSTreeNode<string, int>* ret = countTree.Find(e);if (ret == nullptr){countTree.Insert(e, 1);}else{ret->_val++;}}countTree.InOrder();return 0;
}
2.键值对、关联式容器、序列式容器
在初阶阶段,我们已经接触过STL中的部分容器,比如:vector、list、deque、forward_list(C++11)等,这些容器统称为序列式容器。因为其底层为线性序列的数据结构,里面存储的是元素本身。关联式容器也是用来存储数据的,与序列式容器不同的是,其里面存储的是<key, value>结构的键值对,在数据检索时比序列式容器效率更高。
键值对用来表示具有一 一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代表键值,value表示与key对应的信息。
键值对可以用一个新的变量 pair<K,V> 表示
其结构可以看成:
template <class T1, class T2>
struct pair
{T1 first;T2 second;pair(): first(T1()), second(T2()){}pair(const T1& a, const T2& b): first(a), second(b){}
};
有了上面这个类,我们可以定义一个变量为 pair<int,int> _kv
这样就能调用其内部的元素:_kv.first代表第一个元素K、_kv.second表示第二个元素
或者为pair<int,int>* _kv,_kv->first同样代表第一个元素、_kv->second表示第二个元素
3.AVL树
3.1AVL树的概念
当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
- 它的左右子树都是AVL树
- 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在
O ( l o g 2 n ) O(log_2 n) O(log2n),搜索时间复杂度O( l o g 2 n log_2 n log2n)。
3.2AVL树的构造
AVL树是一个三叉树,他的节点有三个指针分别指向左孩子、右孩子、父节点,还有一个值(这个值是kv模型的)和一个平衡因子。
具体如下:
template<class K, class V>
struct AVLTreeNode
{AVLTreeNode(const pair<K,V>& kv): _left(nullptr), _right(nullptr), _parent(nullptr), _kv(kv), _bf(0){}AVLTreeNode<K,V>* _left;AVLTreeNode<K, V>* _right;AVLTreeNode<K, V>* _parent;pair<K, V> _kv;int _bf; // 节点的平衡因子 balance factor
};
3.3AVL树的基本功能
3.3.1平衡因子
在某个根上其左右子树的高度差(左子树的高度- 右子树的高度)
例子:直接通过下面的图来理解:
上图中在原图上的左子树的子叶增加元素(可增加该左子树的左边或者右边)
一开始可以看到所有的根的平衡因子都为0因为左右子树的高度一样。
- 当在最左边加上一个节点后,其平衡被打破为-1(根的左子树增加一个(所以根的右子树-左子树 = -1),对于该左子树来说也是其左子树增加所以同样的平衡变成 -1)
- 当在左子树的最右加上一个节点后,对于根来说仍然是左子树有变高(所以还是-1),而对于该左子树来说那就是其右子树增加了(那么右子树-左子树=1)
在上面插入新节点的例子中仍然满足平衡二叉树的平衡条件(左右孩子的高度差不超过1),如果出现平衡因子出现问题的话那就需要去旋转!(具体请继续往下看)
3.3.2插入(以及插入中的左旋、右旋、左右双旋、右左双旋的问题)
AVL树的插入本质上还是搜索二叉树的插入,但因为加入了平衡因子的所就可能出现一下问题:当插入一个节点后其平衡因子只可能变成 1,0,-1,-2,2
那么这几个平衡因子产生的情况又能分为几种情况:
附:下面的例子中的长方框表示着子树,而小框就表示新增的节点
-
当平衡因子变成了0 : 那么表示新增节点后使平衡,那么其实是不用做什么的,因为该子树的层数并没有增加,仅仅只需把该子树的平衡因子改成0即可。
-
当平衡因子变成了1/-1:那么表示该子树的层数发生了改变,那么连锁的也会导致上方的祖先的平衡因子发生改变。
如何向上调整:改变cur和parent的关系即可(cur = parent,parent = parent->parent) -
当平衡因子变成-2/2:此时表示在原本高的一方又加了一个节点,那么此时因为平衡因子的错误,那么需要去旋转来解决这个问题。
旋转又分为多种情况:
附:在下面多以cur和parent的关系为一棵子树来看。
1.右旋:当在左子树的左边新增元素(左左),此时因为原本为-1的p再在左边加元素后就变成了-2,此时就需要对cur进行右旋,方法为:将把cur的右子树链接成parent,再把parent的左子树换成cur的右子树(此时就断开了链接)(具体如下图)
旋转后cur = p = 0
代码:
void RotateR(Node* parent)
{Node* SubL = parent->_left;//此处就为 curNode* SubLR = SubL->_right;//parent的左换成cur的右parent->_left = SubLR;//把cur的右孩子换成parentSubL->_right = parent;//注意还要修改其父指针Node* Ppnode = parent->_parent;parent->_parent = SubL;if (SubLR)//cur的右边可能为空SubLR->_parent = parent;if (_root == parent)//如果parent为根节点,则需要把subR改变成根节点并且其父亲为nullptr{_root = SubL;SubL->_parent = nullptr;}else{//同时还要考虑父亲 是祖先的左或右if (Ppnode->_left == parent){Ppnode->_left = SubL;}else{Ppnode->_right = SubL;}SubL->_parent = Ppnode;}SubL->_bf = 0;parent->_bf = 0;
}
2.左旋:当在右子树的右边新增节点(右右),同样的就是 p就将变成2,此时要用对cur左旋,方法为:将cur的左子树断开链接为p,再把p的右子树断开链接成cur的左子树
旋转后cur = p = 0
代码:
void RotateL(Node* parent)
{Node* SubR = parent->_right;//此处就为 curNode* SubRL = SubR->_left;//parent的右换成cur的左parent->_right = SubRL;//把cur的左孩子换成parentSubR->_left = parent;Node* Ppnode = parent->_parent;//注意 还要修改其父指针parent->_parent = SubR;if (SubRL)//右边可能为空SubRL->_parent = parent;if (_root == parent)//如果parent为根节点,则需要把subR改变成根节点并且其父亲为nullptr{_root = SubR;SubR->_parent = nullptr;}else{//同时还要考虑父亲 是祖先的左或右if (Ppnode->_left == parent){Ppnode->_left = SubR;}else{Ppnode->_right = SubR;}SubR->_parent = Ppnode;}SubR->_bf = 0;parent->_bf = 0;
}
3.左右双旋:此处我们先定义p(parent)、subL(parent的左子树)、subLR(subL的右子树),当在左子树的右边新增节点(左右),此时我们先对 subL和subLR 组成的子树进行左旋后,再对p和subLR组成的子树进行右旋。其中有两种可能,当插入到subLR的左边/subLR的右边时他们对平衡因子的影响是不一样的我们需要分开讨论,具体见下图:
旋转后当在subLR的左边插入的话 subLR = subL = 0, p = 1,若在subLR的右边插入的话则subLR = p = 0 , subL = -1
代码:
void RotateLR(Node* parent)
{Node* subL = parent->_left;//此处就为 curNode* subLR = subL->_right;int bf = subLR->_bf; //先对cur 进行左旋 再对 parent进行右旋RotateL(subL);RotateR(parent);if (bf == 0)//自己为新增{parent->_bf = subL->_bf = subLR->_bf = 0;}else if (bf == -1){// subRL的左子树新增parent->_bf = 1;subLR->_bf = 0;subL->_bf = 0;}else if (bf == 1){// subRL的右子树新增parent->_bf = 0;subLR->_bf = 0;subL->_bf = -1;}else{assert(false);}
}
4.右左双旋:此处我们先定义p(parent)、subR(parent的右子树)、subRL(subR的左子树),当在右子树的左边新增节点(右左),此时我们先对 subR和subRL 组成的子树进行右旋后,再对p和subRL组成的子树进行左旋。其中有两种可能,当插入到subRL的左边/subRL的右边时他们只是对平衡因子的影响是不一样的我们需要分开讨论,具体见下图:
旋转后当在subLR的左边插入的话 subRL = p = 0, subR = 1,若在subLR的右边插入的话则subRL = subR = 0, p = 1
代码:
void RotateRL(Node* parent)
{Node* subR = parent->_right;//此处就为 curNode* subRL = subR->_left;int bf = subRL->_bf;//先对cur 进行左旋 再对 parent进行右旋RotateR(subR);RotateL(parent);if (bf == 0)//自己为新增{parent->_bf = subR->_bf = subRL->_bf = 0;}else if (bf == -1){// subRL的左子树新增subR->_bf = 1;subRL->_bf = 0;parent->_bf = 0;}else if (bf == 1){// subRL的右子树新增subR->_bf = 0;subRL->_bf = 0;parent->_bf = -1;}else{assert(false);}
}
对于插入函数代码:
bool Insert(const pair<K,V>& kv)
{//平衡二叉树 : 左子树的值都小于根的值 、 右子树的值都大于根的值Node* cur = _root;Node* parent = nullptr;if (!_root) {_root = new Node(kv);return true;}//1.找到所要插入的位置while (cur){if (cur->_kv.first > kv.first){parent = cur;cur = cur->_left;}else if (cur->_kv.first < kv.first){parent = cur;cur = cur->_right;}else//若已经存在则返回插入失败{return false;}}//2. 确立位置后 , new出新节点在cur处,然后在建立 链接关系//关系:// cur 没有左右为null// 主要是改变其_parent 和 父亲的left/right, 而要去判断cur 在 _parent的左还是右 通过比较kv.first 和 父亲的kv.first的值//此处parent 为其 父亲cur = new Node(kv);if (parent->_kv.first > kv.first){parent->_left = cur;//注意此处也要改变 cur 的 _parentcur->_parent = parent;}else{parent->_right = cur;cur->_parent = parent;}while (parent){//建立链接后,因为有了新插入的节点 故需要 先修改平衡因子//某个根节点的平衡因子为 : 右孩子的_bf(平衡因子) - 左孩子的_bf//插入后 父亲可能的情况为://-2: 左边高插到了左//-1: 平衡到 插左边 //0 : 1 / -1 -> 插到左或右 // //1 : 平衡插右边//2 : 右 右if (cur == parent->_left){parent->_bf--;}else if (cur == parent->_right){parent->_bf++;}//分析不同的情况对应的修改平衡因子if (parent->_bf == 0){break;}else if (parent->_bf == 1 || parent->_bf == -1)//当父亲的平衡因子为1,表示该子树的层数有了增加,但任然是满足AVL树的条件{ //因为子树层数的增加,则对应的会影响到祖先,故需要向上修改祖先的平衡因子cur = parent;parent = parent->_parent;}else if(parent->_bf == 2 || parent->_bf == -2)//当父亲的bf 为 2/-2 时则已经影响到了AVL树的平衡,故需要去通过旋转来保证AVL树{if (parent->_bf == -2 && cur->_bf == -1)//左左{//进行对父节点的右旋RotateR(parent);}if (parent->_bf == -2 && cur->_bf == 1)//左右{//先右旋再左旋RotateLR(parent);}if (parent->_bf == 2 && cur->_bf == 1)//右右{//左旋RotateL(parent);}if (parent->_bf == 2 && cur->_bf == -1)//右左{//先左旋再右旋RotateRL(parent);}//注意此处当旋转完后就直接break跳出循环了,因为其在内部已经将平衡因子调整好了,不用再考虑祖先平衡因子问题// 1、旋转让这颗子树平衡了// 2、旋转降低了这颗子树的高度,恢复到跟插入前一样的高度,所以对上一层没有影响,不用继续更新break;}else {assert(0);}}return true;
}
3.4AVL树的源码
在其中还包括了一些扩展功能
- 求树的高度(_Height)。
- 判断而AVL的每个节点是否满足平衡条件(IsBalance),这个函数是用来测试该AVL树是否满足条件的,实现原理为:遍历整个二叉树,并且在过程中判断其左右子树的差是否满足平衡条件。
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;template<class K, class V>
struct AVLTreeNode
{AVLTreeNode(const pair<K,V>& kv): _left(nullptr), _right(nullptr), _parent(nullptr), _kv(kv), _bf(0){}AVLTreeNode<K,V>* _left;AVLTreeNode<K, V>* _right;AVLTreeNode<K, V>* _parent;pair<K, V> _kv;int _bf; // 节点的平衡因子 balance factor
};// AVL: 二叉搜索树 + 平衡因子的限制
template<class K, class V>
class AVLTree
{typedef AVLTreeNode<K,V> Node;//通过模板将node确定类型
public:AVLTree():_root(nullptr){}// 在AVL树中插入值为data的节点bool Insert(const pair<K,V>& kv){//平衡二叉树 : 左子树的值都小于根的值 、 右子树的值都大于根的值Node* cur = _root;Node* parent = nullptr;if (!_root) {_root = new Node(kv);return true;}//1.找到所要插入的位置while (cur){if (cur->_kv.first > kv.first){parent = cur;cur = cur->_left;}else if (cur->_kv.first < kv.first){parent = cur;cur = cur->_right;}else//若已经存在则返回插入失败{return false;}}//2. 确立位置后 , new出新节点在cur处,然后在建立 链接关系//关系:// cur 没有左右为null// 主要是改变其_parent 和 父亲的left/right, 而要去判断cur 在 _parent的左还是右 通过比较kv.first 和 父亲的kv.first的值//此处parent 为其 父亲cur = new Node(kv);if (parent->_kv.first > kv.first){parent->_left = cur;//注意此处也要改变 cur 的 _parentcur->_parent = parent;}else{parent->_right = cur;cur->_parent = parent;}while (parent){//建立链接后,因为有了新插入的节点 故需要 先修改平衡因子//某个根节点的平衡因子为 : 右孩子的_bf(平衡因子) - 左孩子的_bf//插入后 父亲可能的情况为://-2: 左边高插到了左//-1: 平衡到 插左边 //0 : 1 / -1 -> 插到左或右 // //1 : 平衡插右边//2 : 右 右if (cur == parent->_left){parent->_bf--;}else if (cur == parent->_right){parent->_bf++;}//分析不同的情况对应的修改平衡因子if (parent->_bf == 0){break;}else if (parent->_bf == 1 || parent->_bf == -1)//当父亲的平衡因子为1,表示该子树的层数有了增加,但任然是满足AVL树的条件{ //因为子树层数的增加,则对应的会影响到祖先,故需要向上修改祖先的平衡因子cur = parent;parent = parent->_parent;}else if(parent->_bf == 2 || parent->_bf == -2)//当父亲的bf 为 2/-2 时则已经影响到了AVL树的平衡,故需要去通过旋转来保证AVL树{if (parent->_bf == -2 && cur->_bf == -1)//左左{//进行对父节点的右旋RotateR(parent);}if (parent->_bf == -2 && cur->_bf == 1)//左右{//先右旋再左旋RotateLR(parent);}if (parent->_bf == 2 && cur->_bf == 1)//右右{//左旋RotateL(parent);}if (parent->_bf == 2 && cur->_bf == -1)//右左{//先左旋再右旋RotateRL(parent);}//注意此处当旋转完后就直接break跳出循环了,因为其在内部已经将平衡因子调整好了,不用再考虑祖先平衡因子问题// 1、旋转让这颗子树平衡了// 2、旋转降低了这颗子树的高度,恢复到跟插入前一样的高度,所以对上一层没有影响,不用继续更新break;}else {assert(0);}}return true;}// AVL树的验证bool IsBalance(){return _IsBalance(_root);}private:// 根据AVL树的概念验证pRoot是否为有效的AVL树bool _IsBalance(Node* root){//主要是平衡因子的判断//此处不同用bf , 此处应该用height来确定bf是否正确if (!root) return true;int leftbf = _Height(root->_left);int rightbf = _Height(root->_right);if (rightbf - leftbf != root->_bf) {cout << "平衡因子出问题" << endl;}if (abs(rightbf - leftbf) > 1)return false;return _IsBalance(root->_left)&& _IsBalance(root->_right);}size_t _Height(Node* root){if (!root) return 0;int LeftchildTreeHeight = _Height(root->_left);int RightchildTreeHeight = _Height(root->_right);return LeftchildTreeHeight > RightchildTreeHeight ? LeftchildTreeHeight + 1 : RightchildTreeHeight + 1;}// 右单旋// 把cur的右孩子换成parent,parent的左换成cur的右void RotateR(Node* parent){Node* SubL = parent->_left;//此处就为 curNode* SubLR = SubL->_right;//parent的左换成cur的右parent->_left = SubLR;//把cur的右孩子换成parentSubL->_right = parent;//注意还要修改其父指针Node* Ppnode = parent->_parent;parent->_parent = SubL;if (SubLR)//cur的右边可能为空SubLR->_parent = parent;if (_root == parent)//如果parent为根节点,则需要把subR改变成根节点并且其父亲为nullptr{_root = SubL;SubL->_parent = nullptr;}else{//同时还要考虑父亲 是祖先的左或右if (Ppnode->_left == parent){Ppnode->_left = SubL;}else{Ppnode->_right = SubL;}SubL->_parent = Ppnode;}SubL->_bf = 0;parent->_bf = 0;}// 左单旋// 同理void RotateL(Node* parent){Node* SubR = parent->_right;//此处就为 curNode* SubRL = SubR->_left;//parent的右换成cur的左parent->_right = SubRL;//把cur的左孩子换成parentSubR->_left = parent;Node* Ppnode = parent->_parent;//注意 还要修改其父指针parent->_parent = SubR;if (SubRL)//右边可能为空SubRL->_parent = parent;if (_root == parent)//如果parent为根节点,则需要把subR改变成根节点并且其父亲为nullptr{_root = SubR;SubR->_parent = nullptr;}else{//同时还要考虑父亲 是祖先的左或右if (Ppnode->_left == parent){Ppnode->_left = SubR;}else{Ppnode->_right = SubR;}SubR->_parent = Ppnode;}SubR->_bf = 0;parent->_bf = 0;}// 右左双旋void RotateRL(Node* parent){Node* subR = parent->_right;//此处就为 curNode* subRL = subR->_left;int bf = subRL->_bf;//先对cur 进行左旋 再对 parent进行右旋RotateR(subR);RotateL(parent);if (bf == 0)//自己为新增{parent->_bf = subR->_bf = subRL->_bf = 0;}else if (bf == -1){// subRL的左子树新增subR->_bf = 1;subRL->_bf = 0;parent->_bf = 0;}else if (bf == 1){// subRL的右子树新增subR->_bf = 0;subRL->_bf = 0;parent->_bf = -1;}else{assert(false);}}// 对于在 左子树的 右边加元素的情况 , 用左右双旋void RotateLR(Node* parent){Node* subL = parent->_left;//此处就为 curNode* subLR = subL->_right;int bf = subLR->_bf; //先对cur 进行左旋 再对 parent进行右旋RotateL(subL);RotateR(parent);if (bf == 0)//自己为新增{parent->_bf = subL->_bf = subLR->_bf = 0;}else if (bf == -1){// subRL的左子树新增parent->_bf = 1;subLR->_bf = 0;subL->_bf = 0;}else if (bf == 1){// subRL的右子树新增parent->_bf = 0;subLR->_bf = 0;subL->_bf = -1;}else{assert(false);}}
private:Node* _root;
};
4.红黑树
4.1红黑树的概念
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。
4.2红黑树的性质
红黑树有的性质:
- 每个节点不是黑色就是红色
- 树的根节点必须是黑色
- 如果根节点为红色那么其左右子树必须为黑色节点(不能出现连续的红色节点)
- 每条路径的黑色节点个数是一样的
注意:其中路径要包括到null节点处的路径
红黑树确保没有一条路径会比其他路径长出俩倍:
最短路径:是一条全黑节点的路径
最长路径:是在有相同个黑色节点的前提下穿插着红色节点
具体如下图:
4.3红黑树结构的定义
结构是由三叉链(包括了左右父链接)、pair<K,V>值,颜色组成。
enum Color
{BLACK,RED
};template<class K , class V>
struct RBTreeNode {RBTreeNode<K,V>* _left = nullptr;RBTreeNode<K,V>* _right = nullptr;RBTreeNode<K,V>* _parent = nullptr;pair<K, V> _kv;Color _col = RED;//默认生成的节点颜色是红色RBTreeNode(const pair<K,V>& kv):_kv(kv){}
};
4.3红黑树结构的基本功能
4.3.1插入数据
插入数据(此处的红黑树是在cur、parent、grandfather、uncle中来看)后面在这个颗树中主要分为三种情况:
在g的左边插入(其中默认插入的节点是红色节点、旋转和AVL树是一样的)时:
- 当p为红,g、u为黑时插入节点cur。此时把p、u改成黑色将g改成红色,再向上调整把(cur = g , p = g->p)
- 当p为红,g为黑、u存在且为黑时插入新节点。
当在左边插入时,就先对局部左边进行变色(情况一),变色后向上调整c、p、g、u,再进行右旋(在向上调整后的树上)后再变色(将p变成黑、g变成黑)。
当在右边插入时,同样先变色(情况一),后向上调整,再进行先左旋和右旋后再进行变色(将g变成红色、c变成黑色)。
3. 当g为黑,p、c也为红、u为空时
此时先左旋+变色(p变成黑色、g变成红色)
在g的右边插入时:是一样的就不过诉了
下面通过代码来看:
// 在红黑树中插入值为data的节点,插入成功返回true,否则返回false
// 注意:为了简单起见,本次实现红黑树不存储重复性元素
bool Insert(const pair<K,V>& kv)
{//此处和AVL平衡二叉树的性质一样找到所要插入节点的位置 大的在右 、 小的在左Node* parent = nullptr;Node* cur = _root;if (cur == nullptr){_root = new Node(kv);_root->_col = BLACK;return true;}//找到插入的位置!while (cur)//当为null时表示此处就是要插入的位置!{if (cur->_kv.first > kv.first){parent = cur;cur = cur->_left;}else if (cur->_kv.first < kv.first){parent = cur;cur = cur->_right;}else {return false;}}//找到位置后,插入cur = new Node(kv);//建立新节点//建立链接if (parent->_kv.first > kv.first){parent->_left = cur;}else{parent->_right = cur;}cur->_parent = parent;//插入时要判断插入后是否会导致不平衡!对于红黑树来说主要问题有//1. 不能出现连续的红节点//2. 最长路径不超过最短路径的两倍//判断是否需要变色/旋转// //1.当父亲节点为黑色时,当新增了一个红色节点时就结束插入了// //2.当父为红时:// 情况一(仅变色即可):当parent为红 grandfather为黑 uncle存在且为黑 插入一个新节点// //while (parent && parent->_col == RED){Node* g = parent->_parent;//grandfatherif (g->_left == parent){Node* u = g->_right;//uncleif (u && u->_col == RED)//u存在且为红{//变色即可u->_col = parent->_col = BLACK;g->_col = RED;//向上调整cur = g;parent = g->_parent;//当g 的 父亲为黑时或者为null时停止调整}else //u不存在或者为黑{if (cur == parent->_left)//此处u不存在和当插入节点在左边时的情况一样直接右旋加变色即可{//旋转加变色RotateR(g);parent->_col = BLACK;g->_col = RED;}else{//旋转加变色RotateL(parent);RotateR(g);cur->_col = BLACK;g->_col = RED;}}}else{Node* u = g->_left;//uncleif (u && u->_col == RED)//u存在且为红{//变色即可u->_col = parent->_col = BLACK;g->_col = RED;//向上调整cur = g;parent = g->_parent;//当g 的 父亲为黑时或者为null时停止调整}else //u不存在或者为黑{if (cur == parent->_right)//此处u不存在和当插入节点在左边时的情况一样直接右旋加变色即可{RotateL(g);parent->_col = BLACK;g->_col = RED;}else{RotateR(parent);RotateL(g);cur->_col = BLACK;g->_col = RED;}}}}_root->_col = BLACK;return true;
}
4.3.判断红黑树的正确性
判断红黑树的正确性,主要还是在于其性质是否满足
主要判断下:
- 根节点是否为黑
- 是否出现连续的红色节点
- 最长路径有没有超过最短路径的两倍
- 每条路径的黑色节点是否一样
有了这些目标后设计代码(通过注释来解释!):
bool IsValidRBTRee()
{if (_root == nullptr) return true;//此时无节点为真if (_root->_col == RED) return false;//根节点只能为黑,若为红返回假Node* cur = _root;int blackCount = 0;//记录一条路径的黑色节点个数!while (cur){if (cur->_col == BLACK){blackCount++;}cur = cur->_left;}return _IsValidRBTRee(_root,blackCount,0);
}bool _IsValidRBTRee(Node* root, size_t blackCount, size_t pathBlack)
{if (root == nullptr){if (blackCount != pathBlack)//当为null时表示该路径已经结束,那么判断改路径的黑色节点(pathblack) 和其他路径的黑色节点(blacCount)是否相同{return false;}return true;}if (root->_col == RED){if (root->_left && root->_right && (root->_left->_col == RED || root->_right->_col == RED)){cout << "有连续的红色节点" << endl;return false;}}if (root->_col == BLACK){pathBlack++;//某条路径的黑节点个数}//前序遍历return _IsValidRBTRee(root->_left, blackCount, pathBlack) &&_IsValidRBTRee(root->_right, blackCount, pathBlack);
}
4.4红黑树的源码
#pragma once
#include<iostream>
using namespace std;
// 请模拟实现红黑树的插入--注意:为了后序封装map和set,本文在实现时给红黑树多增加了一个头结点enum Color
{BLACK,RED
};template<class K , class V>
struct RBTreeNode {RBTreeNode<K,V>* _left = nullptr;RBTreeNode<K,V>* _right = nullptr;RBTreeNode<K,V>* _parent = nullptr;pair<K, V> _kv;Color _col = RED;//默认生成的节点颜色是红色RBTreeNode(const pair<K,V>& kv):_kv(kv){}
};template<class K,class V>
class RBTree
{typedef typename RBTreeNode<K,V> Node;
public:// 在红黑树中插入值为data的节点,插入成功返回true,否则返回false// 注意:为了简单起见,本次实现红黑树不存储重复性元素bool Insert(const pair<K,V>& kv){//此处和AVL平衡二叉树的性质一样找到所要插入节点的位置 大的在右 、 小的在左Node* parent = nullptr;Node* cur = _root;if (cur == nullptr){_root = new Node(kv);_root->_col = BLACK;return true;}//找到插入的位置!while (cur)//当为null时表示此处就是要插入的位置!{if (cur->_kv.first > kv.first){parent = cur;cur = cur->_left;}else if (cur->_kv.first < kv.first){parent = cur;cur = cur->_right;}else {return false;}}//找到位置后,插入cur = new Node(kv);//建立新节点//建立链接if (parent->_kv.first > kv.first){parent->_left = cur;}else{parent->_right = cur;}cur->_parent = parent;//插入时要判断插入后是否会导致不平衡!对于红黑树来说主要问题有//1. 不能出现连续的红节点//2. 最长路径不超过最短路径的两倍//判断是否需要变色/旋转// //1.当父亲节点为黑色时,当新增了一个红色节点时就结束插入了// //2.当父为红时:// 情况一(仅变色即可):当parent为红 grandfather为黑 uncle存在且为黑 插入一个新节点// //while (parent && parent->_col == RED){Node* g = parent->_parent;//grandfatherif (g->_left == parent){Node* u = g->_right;//uncleif (u && u->_col == RED)//u存在且为红{//变色即可u->_col = parent->_col = BLACK;g->_col = RED;//向上调整cur = g;parent = g->_parent;//当g 的 父亲为黑时或者为null时停止调整}else //u不存在或者为黑{if (cur == parent->_left)//此处u不存在和当插入节点在左边时的情况一样直接右旋加变色即可{//旋转加变色RotateR(g);parent->_col = BLACK;g->_col = RED;}else{//旋转加变色RotateL(parent);RotateR(g);cur->_col = BLACK;g->_col = RED;}}}else{Node* u = g->_left;//uncleif (u && u->_col == RED)//u存在且为红{//变色即可u->_col = parent->_col = BLACK;g->_col = RED;//向上调整cur = g;parent = g->_parent;//当g 的 父亲为黑时或者为null时停止调整}else //u不存在或者为黑{if (cur == parent->_right)//此处u不存在和当插入节点在左边时的情况一样直接右旋加变色即可{RotateL(g);parent->_col = BLACK;g->_col = RED;}else{RotateR(parent);RotateL(g);cur->_col = BLACK;g->_col = RED;}}}}_root->_col = BLACK;return true;}void Inorder(){_Inorder(_root);cout << endl;}//
// // 获取红黑树最左侧节点Node* LeftMost(){Node* cur = _root;while (cur){if(cur->_left == nullptr){return cur;}cur = cur->_left;}return nullptr;}
//
// // 获取红黑树最右侧节点Node* RightMost(){Node* cur = _root;while (cur){if (cur->_right == nullptr){return cur;}cur = cur->_right;}return nullptr;}
//
// // 检测红黑树是否为有效的红黑树,注意:其内部主要依靠_IsValidRBTRee函数检测// 1.每条路径中的黑色节点个数是否一样// 2.最长路径不超过最短路径的两倍// 3.不能出现连续的红色节点// 4.根节点为黑色//bool IsValidRBTRee(){if (_root == nullptr) return true;//此时无节点为真if (_root->_col == RED) return false;//根节点只能为黑,若为红返回假Node* cur = _root;int blackCount = 0;//记录一条路径的黑色节点个数!while (cur){if (cur->_col == BLACK){blackCount++;}cur = cur->_left;}return _IsValidRBTRee(_root,blackCount,0);}int Height(){if (_root == nullptr) return 0;return _Height(_root);}int Size(){if (_root == nullptr) return 0;return _Size(_root);}//检测红黑树中是否存在值为data的节点,存在返回该节点的地址,否则返回nullptrNode* Find(const K val){Node* cur = _root;while (cur){if (cur->_kv.first == val){return cur;}else if (cur->_kv.first > val){cur = cur->_left;}else {cur = cur->_right;}}return nullptr;}private:int _Size(Node* root){if (root == nullptr)return 0;return _Size(root->_left) +_Size(root->_right) + 1;}int _Height(Node* root){if (root == nullptr)return 0;int lefthight = _Height(root->_left);int righthight = _Height(root->_right);return lefthight > righthight ? lefthight + 1 : righthight + 1;}void _Inorder(Node* root){if (root == nullptr)return;_Inorder(root->_left);cout << root->_kv.first << " " ;_Inorder(root->_right);}bool _IsValidRBTRee(Node* root, size_t blackCount, size_t pathBlack){if (root == nullptr){if (blackCount != pathBlack)//当为null时表示该路径已经结束,那么判断改路径的黑色节点(pathblack) 和其他路径的黑色节点(blacCount)是否相同{return false;}return true;}if (root->_col == RED){if (root->_left && root->_right && (root->_left->_col == RED || root->_right->_col == RED)){cout << "有连续的红色节点" << endl;return false;}}if (root->_col == BLACK){pathBlack++;//某条路径的黑节点个数}//前序遍历return _IsValidRBTRee(root->_left, blackCount, pathBlack) &&_IsValidRBTRee(root->_right, blackCount, pathBlack);}// // 为了操作树简单起见:获取根节点//Node*& GetRoot();void RotateR(Node* parent){Node* SubL = parent->_left;//此处就为 curNode* SubLR = SubL->_right;//parent的左换成cur的右parent->_left = SubLR;//把cur的右孩子换成parentSubL->_right = parent;//注意还要修改其父指针Node* Ppnode = parent->_parent;parent->_parent = SubL;if (SubLR)//cur的右边可能为空SubLR->_parent = parent;if (_root == parent)//如果parent为根节点,则需要把subR改变成根节点并且其父亲为nullptr{_root = SubL;SubL->_parent = nullptr;}else{//同时还要考虑父亲 是祖先的左或右if (Ppnode->_left == parent){Ppnode->_left = SubL;}else{Ppnode->_right = SubL;}SubL->_parent = Ppnode;}}// 左单旋// 同理void RotateL(Node* parent){Node* SubR = parent->_right;//此处就为 curNode* SubRL = SubR->_left;//parent的右换成cur的左parent->_right = SubRL;//把cur的左孩子换成parentSubR->_left = parent;Node* Ppnode = parent->_parent;//注意 还要修改其父指针parent->_parent = SubR;if (SubRL)//右边可能为空SubRL->_parent = parent;if (_root == parent)//如果parent为根节点,则需要把subR改变成根节点并且其父亲为nullptr{_root = SubR;SubR->_parent = nullptr;}else{//同时还要考虑父亲 是祖先的左或右if (Ppnode->_left == parent){Ppnode->_left = SubR;}else{Ppnode->_right = SubR;}SubR->_parent = Ppnode;}}private:Node* _root = nullptr;
};
本章完。预知后事如何,暂听下回分解。
如果有任何问题欢迎讨论哈!
如果觉得这篇文章对你有所帮助的话点点赞吧!
持续更新大量C++细致内容,早关注不迷路。
相关文章:
数据结构之进阶二叉树(二叉搜索树和AVL树、红黑树的实现)超详细解析,附实操图和搜索二叉树的实现过程图
绪论 “生命有如铁砧,愈被敲打,愈能发出火花。——伽利略”;本章主要是数据结构 二叉树的进阶知识,若之前没学过二叉树建议看看这篇文章一篇掌握二叉树,本章的知识从浅到深的对搜索二叉树的使用进行了介绍和对其底层…...
SpringIOC之LocaleContext
博主介绍:✌全网粉丝5W+,全栈开发工程师,从事多年软件开发,在大厂呆过。持有软件中级、六级等证书。可提供微服务项目搭建与毕业项目实战,博主也曾写过优秀论文,查重率极低,在这方面有丰富的经验✌ 博主作品:《Java项目案例》主要基于SpringBoot+MyBatis/MyBatis-plus+…...
前端案例—antdDesign的Select多选框组件加上全选功能
前端案例—antdDesign的Select多选框组件加上全选功能。 实现效果如下: Select 组件里有这个属性,可以利用这个对下拉菜单进行自定义。 const handleChange (e, value) > {setSelectState(e.target.checked)let arr productOptions?productOption…...
个人财务工具、密钥管理平台、在线会计软件、稍后阅读方案 | 开源专题 No.51
gethomepage/homepage Stars: 10.1k License: GPL-3.0 这个项目是一个现代化、完全静态的、快速且安全的应用程序仪表盘,具有超过 100 种服务和多语言翻译的集成。 快速:网站在构建时以静态方式生成,加载时间飞快。安全:所有对后…...
HBase基础知识(二):HBase集群部署、HBaseShell操作
1. HBase安装部署 1.1 Zookeeper正常部署 首先保证Zookeeper集群的正常部署,并启动之: 创建集群启动脚本: #!/bin/bash case $1 in "start"){ for i in hadoop100 hadoop101 hadoop102 do echo----------zookeeper $i 启动----…...
C 标准库 - <time.h>
简介 time.h 头文件定义了四个变量类型、两个宏和各种操作日期和时间的函数。 库变量 下面是头文件 time.h 中定义的变量类型: 序号变量 & 描述1size_t是无符号整数类型,它是 sizeof 关键字的结果。2clock_t这是一个适合存储处理器时间的类型。3…...
养老院自助饮水机(字符设备驱动)
目录 1、项目背景 2、驱动程序 2.1 三层架构 2.2 驱动三要素 2.3 字符设备驱动 2.3.1 驱动模块 2.3.2 应用层 3、设计实现 3.1 项目设计 3.2 项目实现 3.2.1 驱动模块代码 3.2.2 用户层代码 4、功能特性 5、技术分析 6. 总结与未来展望 1、项目背景 养老院的老人…...
Jenkins 构建触发器指南
目录 触发远程构建 (例如,使用脚本) 描述 配置步骤 安全令牌 在其他项目构建完成后触发构建 描述 配置步骤 定时触发构建 描述 配置步骤 GitHub钩子触发GITScm轮询 描述 配置步骤 Poll SCM - 轮询版本控制系统 描述 触发远程构建 (例如,使…...
通用的java中部分方式实现List<自定义对象>转为List<Map>
自定义类 /*** date 2023/12/19 11:20*/ public class Person {private String name;private String sex;public Person() {}public Person(String name, String sex) {this.name name;this.sex sex;}public String getName() {return name;}public String getSex() {return…...
Python---静态Web服务器-返回固定页面数据
1. 开发自己的静态Web服务器 实现步骤: 编写一个TCP服务端程序获取浏览器发送的http请求报文数据读取固定页面数据,把页面数据组装成HTTP响应报文数据发送给浏览器。HTTP响应报文数据发送完成以后,关闭服务于客户端的套接字。 2. 静态Web服务器-返回固…...
react v-18父组件调用子组件的方法和数据
版本 "react": "^18.1.0", "react-dom": "^18.1.0", 父组件 import React, { useState, useRef, memo, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { Card } from &q…...
Linux——缓冲区
我在上篇博客留下了一个问题,那个问题就是关于缓冲区的问题,我们发现 文件有缓冲区,语言有用户级缓冲区,那么缓冲区到底是什么?,或者该怎 么认识缓冲区?这篇文章或许会让你有所认识,…...
Mac 生成Android签名证书 .keystore文件
工具下载地址 https://www.oracle.com/java/technologies/downloads/#jdk21-mac1. 找到安装jdk的路径,并进入bin目录下 1.1 查找JDK命令 /usr/libexec/java_home -v结果为: java_home: option requires an argument -- v /Library/Java/JavaVirtualMachines/jdk…...
电商数仓项目----笔记六(数仓ODS层)
ODS层的设计要点如下: (1)ODS层的表结构设计依托于从业务系统同步过来的数据结构。 (2)ODS层要保存全部历史数据,故其压缩格式应选择压缩比较高的,此处选择gzip。 (3)…...
rtsp视频在使用unity三维融合播放后的修正
1 rtsp 接入 我们使用unity UE 等三维渲染引擎中使用c编写插件来接入rtsp 视频。同时做融合的时候,和背景的三维颜色要一致,这就要使用视频融合修正技术。包括亮度,对比度,饱和度的修正。在单纯颜色上的修正可以简单使用rgb->…...
【已解决】解决Springboot项目访问本地图片等静态资源无法访问的问题
今天在开发一个招聘系统的时候,有投递简历功能,有投递就会有随之而来的查看简历对吧,我投递过的简历,另存为一个文件夹,就是说本地磁盘(或者服务器)有一个专门存放投递过的简历的文件夹,用于存放PDF&#x…...
运维笔记之centos部署Go-FastDfs
安装Go-FastDfs 当前最新版本为1.4.5,但发布的最新版本为1.4.4 # 下载文件 wget --no-check-certificate https://github.com/sjqzhang/go-fastdfs/releases/download/v1.4.4/fileserver -O fileserver # 赋权限 chmod x fileserver # 运行 ./fileserver server服…...
C#基础——线程(线程池、线程锁、线程抢占、多线程)
线程 进程(Process)是由操作系统分配资源并执行的一个独立的程序实,属于Windows的概念,进程结束就表示程序关闭了。 线程(Thread)是程序中执行的最小单位。一个线程代表了一个独立的执行流,可…...
C# WPF上位机开发(QT vs WPF)
【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing 163.com】 最近经常收到朋友们的私信,他们对C# WPF开发很感兴趣,但是呢,正当准备学习的时候,又有人告诉他们应…...
2-高可用-负载均衡、反向代理
负载均衡、反向代理 upstream server即上游服务器,指Nginx负载均衡到的处理业务的服务器,也可以称之为real server,即真实处理业务的服务器。 对于负载均衡我们要关心的几个方面如下: 上游服务器配置:使用upstream server配置上…...
STM32 使用ARM仿真器设置
STM32单片机程序下载到单片机芯片中有两种方式,①编译生成HEX,使用程序烧录软件刷到单片机芯片里。②使用ARM仿真器下载程序。使用ARM仿真器的优势是,在工程编译没问题直接在Keil软件里就可以将程序下载到单片机里,并且程序可以在…...
【Java】spring
一、spring spring是一个很大的生态圈,里面有很多技术。 其中最基础的是spring framework,主要的技术 是springboot以及springcloud。 1、spring framework spring framework是spring生态圈中最基础的项目,是其他项目的基础。 1.1、核心…...
C语言中关于操作符的理解
本篇文章只会列出大家在生活中经常使用的操作符 算术操作符 在算数操作符中常用的有,,-,*,/,% ,我们重点讲一讲 / (除) 和 % (模) " / "运算 #include <stdio.h>int main() {int a5/2;fl…...
Flutter本地化(国际化)之App名称
文章目录 Android国际化IOS国际化 Flutter开发的App,如果名称想要跟随着系统的语言自动改变,则必须同时配置Android和IOS原生。 Android国际化 打开android\app\src\main\res\values 创建strings.xml 在values上右键,选择New>Values Res…...
Redis哨兵源码分析
在Redis server启动过程中,实现了实例化和初始化 1、哨兵实例化过程,采用redis sentinel指令实例化还是redis server下的参数实例化--sentinel。 // 检查服务器是否以 Sentinel 模式启动 server.sentinel_mode checkForSentinelMode(argc,argv);/* Re…...
安装Neo4j
jdk1.8对应的neo4j的版本是3.5 自行下载3.5版本的zip文件 地址 解压添加环境变量 变量名:NEO4J_HOME 变量值:D:\neo4j-community-3.5.0 (你自己的地址) PATH添加: %NEO4J_HOME%\bin (如果是挨着的注意前后英…...
华为鸿蒙开发适合哪些人学习?
随着鸿蒙系统的崛起,越来越多的人开始关注鸿蒙开发,并希望成为鸿蒙开发者。然而,鸿蒙开发并不适合所有人,那么哪些人最适合学习鸿蒙开发呢?本文将为您总结鸿蒙开发适合的人群。 一、具备编程基础的人 学习鸿蒙开发需要…...
深信服技术认证“SCSA-S”划重点:命令执行漏洞
为帮助大家更加系统化地学习网络安全知识,以及更高效地通过深信服安全服务认证工程师考核,深信服特别推出“SCSA-S认证备考秘笈”共十期内容,“考试重点”内容框架,帮助大家快速get重点知识~ 划重点来啦 *点击图片放大展示 深信服…...
Flink系列之:Savepoints
Flink系列之:Savepoints 一、Savepoints二、分配算子ID三、Savepoint 状态四、算子五、触发Savepoint六、Savepoint 格式七、触发 Savepoint八、使用 YARN 触发 Savepoint九、使用 Savepoint 停止作业十、从 Savepoint 恢复十一、跳过无法映射的状态恢复十二、Resto…...
使用宝塔面板部署前端项目到服务器
目录 文章目录 前言 一、第一步:创建文件夹 二、第二步:部署前端项目 三、第三步:打开防火墙 文章目录 前言第一步:创建文件夹第二步:部署前端项目第三步:打开防火墙总结 前言 在此之前,我…...
学校网站建设的安全策略/天津seo选天津旗舰科技a
其实,两者之间是没有多大差别的,只是为了提高查找效率而区分的。当你包含一个头文件时,编译时,需要找到那个头文件,使用<>这种方式,编译器查找的时候,会在编译器的安装目录的标准库中开始…...
wordpress创建企业邮箱/网页优化方案
Geospatial 地理位置 朋友的定位,附近的人,打车距离计算? Redis 的 Geo 在Redis3.2 版本就推出了! 这个功能可以推算地理位置的信息,两地之间的距离,方圆 几里的人! 可以查询一些测试数据&…...
php网站插件/怎么创建网站的快捷方式
通常修改网卡物理MAC地址的方法是通过软件信息的方法来实现,当然也可直接修改网卡ROM信息来实现修改地址的方法。在此学习啦小编就与大家分享一下修改笔记本MAC地址的方法。修改笔记本的物理地址的方法首先,我们需要了解当前的物理MAC地址。点击“开始”…...
介绍化工项目建设和招聘的网站/河南企业网站建设
传送门 给你串A和串B,|B| < |A| 已知串B有两种可能含义,求串A的总可能含义数 dp[i]表示串A的从头开始长度为i的串的可能意义的数目 若该长度为i的串的后缀与模式串B匹配,则该后缀可以选择替换或者不替换 dp[i] dp[i - 1] dp[i - Bl] 否…...
做做网站2023下载/深圳市龙华区
一、体验环境使用设备:iPhone 6S操作系统:iOS 10.1.1产品版本:3.1.1操作时间:2018.1.13二、产品体验(一)战略层1.产品定位Slogan:得到——和你一起终身学习得到是一个知识付费型的线上知识学习平台:通过提供…...
那个网站是响应式的/企业营销策划
博主说:在本篇文章中,马化腾先生亲口讲述了自己的创业史,回顾了早期创业之艰辛、公司生存之法则,从“创业维艰、自我颠覆、产品思维和最大担忧”四个方面分别进行了阐述,值得每一个互联网人为之深思。 正文 创业维艰 我…...