目次
1. 基礎となる構造
Map、multimap、set、および multiset については以前に簡単に紹介しました。これらのコンテナには共通点が 1 つあります。それらの最下層は二分探索ツリーに従って実装されていますが、二分探索ツリーには独自の欠点があります。ツリーに挿入された要素が順序付けまたは順序付けに近い場合、二分探索木は単一の木に縮退し、時間計算量は O(N) に縮退します。したがって、マップやセットなどの連想コンテナーの基礎となる構造は、二分木はバランスが取れています。 、バランスのとれたツリーを使用して実装されます。
2. AVL番号の概念
二分探索木は検索効率を低下させる可能性がありますが、データが順序付けされている、または順序付けに近い場合、二分探索木は単一の枝木に縮退します。要素の検索は、順序表内の要素の検索と同等であり、非効率 。したがって、2 人のロシアの数学者 GMAdelson-Velskii と EMLandis は 1962 年に上記の問題を解決する方法を発明しました。二分探索木に新しいノードを挿入した後、各ノードの左右のサブツリーが保証できるかどうか、高さの差が 1 を超えない場合(ツリー内のノードを調整する必要がある)、ツリーの高さを低くすることができるため、平均検索長が短縮されます。
AVL ツリーは、空のツリー、または次のプロパティを持つ二分探索ツリーのいずれかです。
- その左と右のサブツリーは両方とも AVL ツリーです
- 左右のサブツリー間の高さの差の絶対値 (バランス係数と呼ばれます) は 1 (-1/0/1) を超えません。
- 0 は左右の高さが等しいことを意味します。
- 1 は、右側のサブツリーの高さが 1 であることを意味します。
- -1 は、左側のサブツリーの高さが 1 であることを意味します。
二分探索ツリーのバランスが高い (比較的バランスが取れている) 場合、それは AVL ツリーです。n 個のノードがある場合、その高さは O(logN) に保つことができ、検索時間の計算量は O(logN) になります。
3. AVLツリーノードの定義
ここで実装する AVL ツリーはKV モデルです。自然ノードには 2 つのテンプレート パラメーターがあり、ノードは三分岐接続構造 (左の子、右の子、父) として定義されます。バイナリ リンク リストに基づいて、親ノードを指すノードが追加され、ポインタ フィールドにより、子ノードと親ノードの両方を簡単に見つけることができます。次に、バランス係数 (右側のサブツリーと左側のサブツリーの高さの差) として変数 _bf を作成する必要があります。最後に、変数を初期化するコンストラクターを作成します。
//节点类 template<class K, class V> struct AVLTreeNode { //存储的键值对 pair<K, V> _kv; //三叉连结构 AVLTreeNode<K, V>* _left;//左孩子 AVLTreeNode<K, V>* _right;//右孩子 AVLTreeNode<K, V>* _parent;//父亲 //平衡因子_bf int _bf;//右子树 - 左子树的高度差 //构造函数 AVLTreeNode(const pair<K, V>& kv) :_kv(kv) , _left(nullptr) , _right(nullptr) , _bf(0) {} };
4. 基本的な枠組み
この部分の内容は AVL ツリーのクラスであり、その主な機能は、後続の挿入、回転、削除などの操作を完了することです。
//AVL树的类 template<class K, class V> class AVLTree { typedef AVLTreeNode<K, V> Node; public: //…… private: Node* _root; };
5. AVLツリーの挿入
挿入は主に次の主要な手順に分かれています。
- 1. ツリーは最初は空であり、新しいノードが直接追加されます。
- 2. 空ではないツリーから始めて、挿入に適した場所を見つけます。
- 3. 適切な挿入位置を見つけたら、父親と子供の間で双方向リンクを実行します。
- 4. 新しく挿入されたノードの祖先のバランス係数を更新します。
- 5. 準拠していないバランス係数に対して回転調整を行う
次に、それらを 1 つずつ分析します。
- 1. ツリーは最初は空であり、新しいノードが直接追加されます。
ツリーは空であるため、新しく挿入したノードを作成してルート _root として使用し、バランス係数 _bf を 0 に更新して、最後に true を返します。
- 2. 空ではないツリーから始めて、挿入に適した場所を見つけます。
ここでの考え方は、二分探索木で適切な挿入位置を見つける考え方と同じであり、次の手順に従う必要があります。
- 挿入された値 > ノード値、右サブツリー検索に更新
- 挿入値 < ノード値、左のサブツリー検索に更新
- 挿入された値 = ノード値。データ冗長性の挿入は失敗し、false を返します。
ループが終了すると、挿入に適した場所が見つかったことを意味し、リンクの次のステップを実行できるようになります。
- 3. 適切な挿入位置を見つけたら、父親と子の間で双方向リンクを実行します。
ここのノードは 3 方向チェーンで構成されているため、バックエンドの子と父親の間の最終リンクは 2 方向リンクであることに注意してください。具体的な操作は次のとおりです。
- 挿入された値 > 親の値、挿入された値を親の右側にリンクします
- 挿入された値 < 親の値、挿入された値を親の左側にリンクします
- これは 3 方向リンクであるため、挿入後は必ず双方向リンクを作成してください (子は父親にリンクします)。
この点に到達すると、ノードは挿入されましたが、次にバランス係数を更新する必要があることを意味します。
- 4. 新しく挿入されたノードの祖先のバランス係数を更新します。
新しいノードを挿入すると、サブツリーの高さが変更される可能性があります。この変更については、次の要件を与えます。
- サブツリーの高さが変更された場合は、引き続き更新する必要があります。
- サブツリーの高さが変わらなければ更新は完了です。
- サブツリーがバランス ルールに違反している場合 (バランス係数の絶対値 >= 2)、更新は停止し、調整のためにサブツリーを回転する必要があります。
具体的な更新ルールは次のとおりです。
- 新しいノードは親の右側にあり、親のバランス係数は ++ です。
- 新しいノードは親の左側にあり、親のバランス係数は — — です。
ノードのバランス係数を更新した後、次の判断を行う必要があります。
- 親のバランス係数が -1 または 1 に等しい場合 (元は 0 で、ノードの挿入後に左右が増加し、左側のサブツリーまたは右側のサブツリーが増加したことを意味します)。バランス係数を上方に更新し続ける必要があることを示します。
- 親のバランス係数が 0 に等しい場合 (元々は 1 または -1 で、1 つは高、もう 1 つは低であり、ノードの挿入後に短い方が埋められます)、バランスを更新する必要がないことを意味します。要因。
- 親のバランス係数が -2 または 2 に等しい場合 (これは、元々は 1 または -1 で、1 つは高、もう 1 つは低であり、高い方がノードの挿入後に埋められることを意味します)。この時点でルート ノードとしての親ノードはすでにアンバランスになっているため、回転する必要があります。
- 5. 非準拠のバランス係数の調整を回転します。
親のバランス係数が 2 または -2 の場合、回転調整が必要となり、回転を次の 4 つのカテゴリに分類する必要があります。
- 親のバランス係数が 2、cur のバランス係数が 1 の場合、左 1 回転が実行されます。
- 親のバランス係数が-2、curのバランス係数が-1の場合、右一回転を行います。
- 親のバランス係数が-2、curのバランス係数が1の場合、左右2重回転が行われます。
- 親のバランス係数が 2、cur のバランス係数が -1 の場合、左右 2 回転が実行されます。
コードは以下のように表示されます。
//Insert插入 bool Insert(const pair<K, V>& kv) { //1、一开始为空树,直接new新节点 if (_root == nullptr) { //如果_root一开始为空树,直接new一个kv的节点,更新_root和_bf _root = new Node(kv); _root->_bf = 0; return true; } //2、寻找插入的合适位置 Node* cur = _root;//记录插入的位置 Node* parent = nullptr;//保存parent为cur的父亲 while (cur) { if (cur->_kv.first < kv.first) { //插入的值 > 节点的值 parent = cur; cur = cur->_right;//更新到右子树查找 } else if (cur->_kv.first > kv.first) { //插入的值 < 节点的值 parent = cur; cur = cur->_left;//更新到左子树查找 } else { //插入的值 = 节点的值,数据冗余插入失败,返回false return false; } } //3、找到了插入的位置,进行父亲与插入节点的链接 cur = new Node(kv); if (parent->_kv.first < kv.first) { //插入的值 > 父亲的值,链接在父亲的右边 parent->_right = cur; } else { //插入的值 < 父亲的值,链接在父亲的左边 parent->_left = cur; } //因为是三叉连,插入后记得双向链接(孩子链接父亲) cur->_parent = parent; //4、更新新插入节点的祖先的平衡因子 while (parent)//最远要更新到根 { if (cur == parent->_right) { parent->_bf++;//新增结点在parent的右边,parent的平衡因子++ } else { parent->_bf--;//新增结点在parent的左边,parent的平衡因子 -- } //判断是否继续更新? if (parent->_bf == 0)// 1 or -1 -> 0 填上了矮的那一方 { //1 or -1 -》 0 填上了矮的那一方,此时正好,无需更新 break; } else if (parent->_bf == 1 || parent->_bf == -1) { //0 -》 1或-1 此时说明插入节点导致一边变高了,继续更新祖先 cur = cur->_parent; parent = parent->_parent; } else if (parent->_bf == 2 || parent->_bf == -2) { //1 or -1 -》2或-2 插入节点导致本来高的一边又变更高了 //此时子树不平衡,需要进行旋转 if (parent->_bf == 2 && cur->_bf == 1) { RotateL(parent);//右边高,左单旋 } else if (parent->_bf == -2 && cur->_bf == -1) { RotateR(parent);//左边高,右单旋 } else if (parent->_bf == -2 && cur->_bf == 1) { RotateLR(parent);//左右双旋 } else if (parent->_bf == 2 && cur->_bf == -1) { RotateRL(parent);//右左双旋 } break; } else { //插入之前AVL树就存在不平衡树,|平衡因子| >= 2的节点 //实际上根据前面的判断不可能走到这一步,不过这里其实是为了检测先前的插入是否存在问题 assert(false); } } return true; }
6. AVLツリーの回転
AVL ツリーの回転には 4 つのタイプがあります。
- 左単回転
- 右一回転
- 左右二回転
- 右左双旋
AVL ツリーのローテーションは、次の 2 つの原則に従う必要があります。
- 1. 検索ツリーのルールを守る
- 2. サブツリーのバランスが取れた状態になる
6.1 単一左回転
- 条件: 新しいノードが右上のサブツリーの右側に挿入されます。
左回りの場合はたくさんありますが、ここでは抽象的な図を描いて説明します。
ここで、長方形のバー (a、b、c) はサブツリーを表し、h はサブツリーの高さ、30 と 60 は実際のノードです。上記の左の単一回転操作は主に 4 つのことを実現します。
- subRL を親の正しいサブツリーにし、subRL の親を親に更新します。
- subR をルート ノードにします。
- 親を subR の左側のサブツリーにし、親の父親を subR に更新します。
- バランス係数を更新します。
知らせ:
- 親はツリー全体のサブツリーである場合があり、親の親とサブR をリンクする必要があります。
- subRL は空でも構いませんが、subRL の親から親への更新は、subRL が空ではないことを前提として完了します。
上記の左回りの回転が可能な理由を説明してください。
- まず第一に、基礎となる二分探索木の構造によれば、ノード b の値は 30 から 60 の間でなければなりません。 b が 30 の右側のサブツリーであることに問題はなく、ここでは 60 がルートに移動されます。そして 30 は 60 として使用されます。 の右側のサブツリーであるため、全体的な変化は左手の変化のようになり、二分探索木の特性も満たし、均等にバランスが取れています。
コードは以下のように表示されます。
void RotateL(Node* parent) { Node* subR = parent->_right; Node* subRL = subR->_left; Node* ppNode = parent->_parent;//提前保持parent的父亲 //1、建立parent和subRL之间的关系 parent->_right = subRL; if (subRL)//防止subRL为空 { subRL->_parent = parent; } //2、建立subR和parent之间的关系 subR->_left = parent; parent->_parent = subR; //3、建立ppNode和subR之间的关系 if (parent == _root) { _root = subR; _root->_parent = nullptr; } else { if (parent == ppNode->_left) { ppNode->_left = subR; } else { ppNode->_right = subR; } subR->_parent = ppNode;//三叉链双向链接关系 } //4、更新平衡因子 subR->_bf = parent->_bf = 0; }
6.2 右一回転
- 条件: 新しいノードが左上のサブツリーの左側に挿入されます。
図:
同様に、ここでの長方形のバー (a、b、c) はサブツリーを表し、h はサブツリーの高さ、30 と 60 は実際のノードです。上記の左の単一回転操作は主に 4 つのことを実現します。
- subLR を親の左側のサブツリーとし、subLR の親を親に更新します。
- subL をルートノードにする
- 親を subL の右側のサブツリーにし、親の親を subL に更新します。
- バランス係数を更新する
知らせ:
- 親はツリー全体のサブツリーである場合があり、親の親とサブL をリンクする必要があります。
- subLRは空でも構いませんが、subLRの親から親への更新はsubLRが空ではないことを前提として完了します。
コードは以下のように表示されます。
//2、右单旋 void RotateR(Node* parent) { Node* subL = parent->_left; Node* subLR = subL->_right; Node* ppNode = parent->_parent; //1、建立parent和subLR之间的关系 parent->_left = subLR; if (subLR) { subLR->_parent = parent; } //2、建立subL和parent之间的关系 subL->_right = parent; parent->_parent = subL; //3、建立ppNode和subL的关系 if (parent == _root) { _root = subL; _root->_parent = nullptr; } else { if (parent == ppNode->_left) { ppNode->_left = subL; } else { ppNode->_right = subL; } subL->_parent = ppNode;//三叉链双向关系 } //4、更新平衡因子 subL->_bf = parent->_bf = 0; }
6.3 左右二重回転
- 条件: 新しいノードが左上のサブツリーの右側に挿入されます。
次の図は、左右の二重回転の具体的な解決策を示しています。
- 1. 新しいノードを挿入します。
このタイプのモデルは、左一回転、右一回転の条件を満たしていませんが、これらを組み合わせて、左右二回転(最初は左一回転、次に右一回転)を行うことでバランスを取り戻すことができます。次に、左単一回転の次のステップを実行します。
- 2. ノード 30 (subL) を回転ポイントとして使用して、左に回転します。
このとき、もう一度この図を観察してください。これは正しい右側単回転モデルですか? 60 の左側の部分木全体を考えてください。このとき、新しく挿入されたノードは、左上の部分木の左側に挿入されます。まさに右手単回転の特性に従って、次に右手単回転を実行できます。
- 3. ノード 90 (親) を回転ポイントとして右単一回転を実行します。
この時点で左右の2重回転は完了し、最後にバランス係数を更新するのですが、更新されたバランス係数は以下の3つに分類されます。
- 1. subLR の元のバランス係数が -1 の場合、左右のダブルスピンの親、subL、subLR のバランス係数はそれぞれ 1、0、0 に更新されます。
- 2. subLR の元のバランス係数が 1 の場合、左右の二重回外親、subL、subLR のバランス係数はそれぞれ 0、-1、0 に更新されます。
- 3. 元の subLR のバランス係数が 0 の場合、左右のダブルスピンの親、subL、subLR のバランス係数はそれぞれ 0、0、0 に更新されます。
ここで、subLR バランス係数が 0 の場合にのみ、左右の回転後に 3 つのノードのバランス係数が 0 に更新されることがわかります。
- コードは以下のように表示されます。
//3、左右双旋 void RotateLR(Node* parent) { Node* subL = parent->_left; Node* subLR = subL->_right; int bf = subLR->_bf;//提前记录subLR的平衡因子 //1、以subL为根传入左单旋 RotateL(subL); //2、以parent为根传入右单旋 RotateR(parent); //3、重新更新平衡因子 if (bf == 0) { parent->_bf = 0; subL->_bf = 0; subLR->_bf = 0; } else if (bf == 1) { parent->_bf = 0; subL->_bf = -1; subLR->_bf = 0; } else if (bf == -1) { parent->_bf = 1; subL->_bf = 0; subLR->_bf = 0; } else { assert(false);//此时说明旋转前就有问题,检查 } }
6.4 右左双旋
- 条件: 新しいノードが右上のサブツリーの左側に挿入されます。
次の図は、左右の二重回転の具体的な解決策を示しています。
- 1. 新しいノードを挿入します。
ここでの新しいノードは、右上のサブツリーの左側に挿入されることに注意してください。上記の単一回転と左右の回転は使用できません。代わりに、左右の回転を使用する必要があります (最初は右の単一回転、次に左の単一回転)次へ 右回転の次のステップを実行します。
- 2. ノード 90 (subR) を回転点として使用して、右に回転します。
この図をもう一度観察してください。これは正しい左手モデルですか? 60 の右側の部分木全体を考えてください。このとき、新しく挿入されたノードは、右上の部分木の右側に挿入されます。左単回転の特性に従って、左単回転は次のように実行できます。
- 3. ノード 30 (親) を回転ポイントとして使用して、左に回転します。
この時点で左右の2重回転は完了し、最後にバランス係数を更新するのですが、更新されたバランス係数は以下の3つに分類されます。
- 1. subRL の元のバランス係数が -1 の場合、右と左の二重回外の親、subR、および subRL のバランス係数はそれぞれ 0、1、0 に更新されます。
- 2. subRL の元のバランス係数が 1 の場合、右と左の二重回外の親、subR、および subRL のバランス係数はそれぞれ -1、0、0 に更新されます。
- 3. subRL の元のバランス係数が 0 の場合、右と左の二重回外の親、subR、および subRL のバランス係数はそれぞれ 0、0、0 に更新されます。
ここで、subRL バランス係数が 0 の場合にのみ、左右の回転後に 3 つのノードのバランス係数が 0 に更新されることがわかります。
- コードは以下のように表示されます。
//4、右左双旋 void RotateRL(Node* parent) { Node* subR = parent->_right; Node* subRL = subR->_left; int bf = subRL->_bf;//提前记录subLR的平衡因子 //1、以subL为根传入左单旋 RotateR(subR); //2、以parent为根传入右单旋 RotateL(parent); //3、重新更新平衡因子 if (bf == 0) { parent->_bf = 0; subR->_bf = 0; subRL->_bf = 0; } else if (bf == 1) { parent->_bf = -1; subR->_bf = 0; subRL->_bf = 0; } else if (bf == -1) { parent->_bf = 0; subR->_bf = 1; subRL->_bf = 0; } else { assert(false);//此时说明旋转前就有问题,检查 } }
7. AVLツリーの検証
AVLツリーは二分探索木をベースに平衡制約を加えているため、AVLツリーを検証するには二分探索木であるか、平衡木であるかを確認することになります。次にそれぞれについて説明します。
- 1. 二分探索ツリーであることを確認します。
ここでは、順序トラバーサルを実行して、結果が順序付けられたシーケンスであるかどうかを確認するだけです。そうであれば、それは二分探索木であることが証明されます。順序トラバーサルの実装は非常に簡単です。バイナリの以前の実装検索ツリーが完了しました。コードはここに直接与えられます。
//中序遍历的子树 void _InOrder(Node* root) { if (root == nullptr) return; _InOrder(root->_left); cout << root->_kv.first << " "; _InOrder(root->_right); } //中序遍历 void InOrder() { _InOrder(_root); cout << endl; }
二分探索木であるかどうかを検出するコードが実装されたら、次のステップはそれが平衡木であるかどうかを検証することです。
- 2. バランスの取れたツリーであることを確認します。
ルールは次のとおりです: (再帰的アイデア)
- 空の木もバランスの取れた木であり、最初から判断する必要があります。
- 特に高さを計算する関数 (再帰的に高さを計算) をカプセル化し、その後高さの差 (バランス係数の差) を計算するために使用されます。
- diff がルートのバランス係数 (root->_bf) と等しくない場合、またはルート バランス係数の絶対値が 1 を超える場合、AVL ツリーであってはなりません。
- 最後までサブツリーと右ツリーへの再帰を続けます
コードは以下のように表示されます。
//验证一棵树是否为平衡树 bool IsBalanceTree() { return _IsBalanceTree(_root); } //判读是否平衡的子树 bool _IsBalanceTree(Node* root) { //空树也是AVL树 if (nullptr == root) return true; //计算root节点的平衡因子diff:即root左右子树的高度差 int leftHeight = _Height(root->_left); int rightHeight = _Height(root->_right); int diff = rightHeight - leftHeight; //如果计算出的平衡因子与root的平衡因子不相等,或root平衡因子的绝对值超过1,则一定不是AVL树 if ((abs(diff) > 1)) { cout << root->_kv.first << "节点平衡因子异常" << endl; return false; } if (diff != root->_bf) { cout << root->_kv.first << "节点平衡因子与root的平衡因子不等,不符合实际" << endl; return false; } //继续递归检测,直到结束 return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right); } //求高度的子树 int _Height(Node* root) { if (root == nullptr) return 0; int lh = _Height(root->_left); int rh = _Height(root->_right); return lh > rh ? lh + 1 : rh + 1; }
上記の 2 つの手順を組み合わせると、ツリーが AVL ツリーであるかどうかを完全に検証できます。
8. AVLツリーの検索
Find 関数の考え方は非常にシンプルで、次の規則に従ってルートからトラバースするように cur ポインタを定義します。
- キー値が現在のノードの値より小さい場合、検索はノードの左側のサブツリーで実行される必要があります。
- キーの値が現在のノードの値より大きい場合、検索はノードの右側のサブツリーで実行される必要があります。
- キーの値が現在のノードの値と等しい場合、検索は成功し、true が返されます。
- cur が円を横断して nullptr に到達した場合、そのようなノードが存在しないことを意味し、false を返します。
//Find查找 bool Find(const K& key) { Node* cur = _root; while (cur) { if (cur->_key < key) { cur = cur->_right;//若key值大于当前结点的值,则应该在该结点的右子树当中进行查找。 } else if (cur->_key > key) { cur = cur->_left;//若key值小于当前结点的值,则应该在该结点的左子树当中进行查找。 } else { return true;//若key值等于当前结点的值,则查找成功,返回true。 } } return false;//遍历一圈没找到返回false }
9. AVLツリーの削除
削除して調べてみてください。
AVL ツリーは二分探索ツリーでもあるため、次の 3 つの主なアイデアがあります。
- 二分探索木のルールに従って削除する
- バランス係数を更新する
- アンバランスがあるので回転調整が必要です。
ただし、探索二分木の削除とは異なり、ノード削除後のバランス係数は常に更新する必要があり、最悪の場合はルートノードの位置に合わせなければなりません。少し複雑になるため、ここでは具体的に実装しません。ただし、イン・レンクン著『アルゴリズム入門』または『データ構造 - オブジェクト指向手法とC++を使った記述』の2冊に詳しい解説があります。
10. AVLツリーのパフォーマンス
AVL ツリーは完全にバランスの取れた二分探索ツリーであり、各ノードの左右のサブツリー間の高さの差の絶対値が 1 を超えないことが必要です。これにより、クエリ時間の効率的な計算量、つまり O(logN ) が保証されます。ただし、AVL ツリーに構造的な変更を加えたい場合は、パフォーマンスが非常に低くなります。たとえば、挿入時には絶対的なバランスを維持する必要があり、回転数も比較的高くなります。さらに悪いことに、削除時はルート位置まで回転を続けることが可能です。したがって、効率的なクエリと順序を備えたデータ構造が必要で、データの数が静的である (つまり変化しない) 場合は、AVL ツリーを検討できますが、構造が頻繁に変更される場合には、AVL ツリーは適していません。 。
11. 合計コード
11.1 AVLツリー
#pragma once #include<queue> #include<vector> #include<iostream> using namespace std; //节点类 template<class K, class V> struct AVLTreeNode { //存储的键值对 pair<K, V> _kv; //三叉连结构 AVLTreeNode<K, V>* _left;//左孩子 AVLTreeNode<K, V>* _right;//右孩子 AVLTreeNode<K, V>* _parent;//父亲 //平衡因子_bf int _bf;//右子树 - 左子树的高度差 //构造函数 AVLTreeNode(const pair<K, V>& kv) :_kv(kv) , _left(nullptr) , _right(nullptr) , _parent(nullptr) , _bf(0) {} }; //AVL树的类 template<class K, class V> class AVLTree { typedef AVLTreeNode<K, V> Node; public: //1、按照搜索树的规则插入 //2、是否违反平衡规则,如果违反就需要处理:旋转 bool Insert(const pair<K, V>& kv) { //1、一开始为空树,直接new新节点 if (_root == nullptr) { //如果_root一开始为空树,直接new一个kv的节点,更新_root和_bf _root = new Node(kv); _root->_bf = 0; return true; } //2、寻找插入的合适位置 Node* cur = _root;//记录插入的位置 Node* parent = nullptr;//保存parent为cur的父亲 while (cur) { if (cur->_kv.first < kv.first) { //插入的值 > 节点的值 parent = cur; cur = cur->_right;//更新到右子树查找 } else if (cur->_kv.first > kv.first) { //插入的值 < 节点的值 parent = cur; cur = cur->_left;//更新到左子树查找 } else { //插入的值 = 节点的值,数据冗余插入失败,返回false return false; } } //3、找到了插入的位置,进行父亲与插入节点的链接 cur = new Node(kv); if (parent->_kv.first < kv.first) { //插入的值 > 父亲的值,链接在父亲的右边 parent->_right = cur; } else { //插入的值 < 父亲的值,链接在父亲的左边 parent->_left = cur; } //因为是三叉连,插入后记得双向链接(孩子链接父亲) cur->_parent = parent; //4、更新新插入节点的祖先的平衡因子 while (parent)//最远要更新到根 { if (cur == parent->_right) { parent->_bf++;//新增结点在parent的右边,parent的平衡因子++ } else { parent->_bf--;//新增结点在parent的左边,parent的平衡因子 -- } //判断是否继续更新? if (parent->_bf == 0)// 1 or -1 -> 0 填上了矮的那一方 { //1 or -1 -》 0 填上了矮的那一方,此时正好,无需更新 break; } else if (parent->_bf == 1 || parent->_bf == -1) { //0 -》 1或-1 此时说明插入节点导致一边变高了,继续更新祖先 cur = cur->_parent; parent = parent->_parent; } else if (parent->_bf == 2 || parent->_bf == -2) { //1 or -1 -》2或-2 插入节点导致本来高的一边又变更高了 //此时子树不平衡,需要进行旋转 if (parent->_bf == 2 && cur->_bf == 1) { RotateL(parent);//右边高,左单旋 } else if (parent->_bf == -2 && cur->_bf == -1) { RotateR(parent);//左边高,右单旋 } else if (parent->_bf == -2 && cur->_bf == 1) { RotateLR(parent);//左右双旋 } else if (parent->_bf == 2 && cur->_bf == -1) { RotateRL(parent);//右左双旋 } break; } else { //插入之前AVL树就存在不平衡树,|平衡因子| >= 2的节点 //实际上根据前面的判断不可能走到这一步,不过这里其实是为了检测先前的插入是否存在问题 assert(false); } } return true; } //求一棵树的高度 int Height() { return _Height(_root); } //验证是否为一颗搜索二叉树 void InOrder() { _InOrder(_root);//调用中序遍历子树 cout << endl; } //验证一棵树是否为平衡树 bool IsBalanceTree() { return _IsBalanceTree(_root); } private: //1、左单旋 void RotateL(Node* parent) { Node* subR = parent->_right; Node* subRL = subR->_left; Node* ppNode = parent->_parent;//提前保持parent的父亲 //1、建立parent和subRL之间的关系 parent->_right = subRL; if (subRL)//防止subRL为空 { subRL->_parent = parent; } //2、建立subR和parent之间的关系 subR->_left = parent; parent->_parent = subR; //3、建立ppNode和subR之间的关系 if (parent == _root) { _root = subR; _root->_parent = nullptr; } else { if (parent == ppNode->_left) { ppNode->_left = subR; } else { ppNode->_right = subR; } subR->_parent = ppNode;//三叉链双向链接关系 } //4、更新平衡因子 subR->_bf = parent->_bf = 0; } //2、右单旋 void RotateR(Node* parent) { Node* subL = parent->_left; Node* subLR = subL->_right; Node* ppNode = parent->_parent; //1、建立parent和subLR之间的关系 parent->_left = subLR; if (subLR) { subLR->_parent = parent; } //2、建立subL和parent之间的关系 subL->_right = parent; parent->_parent = subL; //3、建立ppNode和subL的关系 if (parent == _root) { _root = subL; _root->_parent = nullptr; } else { if (parent == ppNode->_left) { ppNode->_left = subL; } else { ppNode->_right = subL; } subL->_parent = ppNode;//三叉链双向关系 } //4、更新平衡因子 subL->_bf = parent->_bf = 0; } //3、左右双旋 void RotateLR(Node* parent) { Node* subL = parent->_left; Node* subLR = subL->_right; int bf = subLR->_bf;//提前记录subLR的平衡因子 //1、以subL为根传入左单旋 RotateL(subL); //2、以parent为根传入右单旋 RotateR(parent); //3、重新更新平衡因子 if (bf == 0) { parent->_bf = 0; subL->_bf = 0; subLR->_bf = 0; } else if (bf == 1) { parent->_bf = 0; subL->_bf = -1; subLR->_bf = 0; } else if (bf == -1) { parent->_bf = 1; subL->_bf = 0; subLR->_bf = 0; } else { assert(false);//此时说明旋转前就有问题,检查 } } //4、右左双旋 void RotateRL(Node* parent) { Node* subR = parent->_right; Node* subRL = subR->_left; int bf = subRL->_bf;//提前记录subLR的平衡因子 //1、以subL为根传入左单旋 RotateR(subR); //2、以parent为根传入右单旋 RotateL(parent); //3、重新更新平衡因子 if (bf == 0) { parent->_bf = 0; subR->_bf = 0; subRL->_bf = 0; } else if (bf == 1) { parent->_bf = -1; subR->_bf = 0; subRL->_bf = 0; } else if (bf == -1) { parent->_bf = 0; subR->_bf = 1; subRL->_bf = 0; } else { assert(false);//此时说明旋转前就有问题,检查 } } //中序遍历的子树 void _InOrder(Node* root) { if (root == nullptr) return; _InOrder(root->_left); cout << root->_kv.first <<" "; _InOrder(root->_right); } //求一棵树的高度的子树 int _Height(Node* root) { if (root == nullptr) return 0; int lh = _Height(root->_left); int rh = _Height(root->_right); return lh > rh ? lh + 1 : rh + 1; } //判读是否平衡的子树 bool _IsBalanceTree(Node* root) { //空树也是AVL树 if (nullptr == root) return true; //计算root节点的平衡因子diff:即root左右子树的高度差 int leftHeight = _Height(root->_left); int rightHeight = _Height(root->_right); int diff = rightHeight - leftHeight; //如果计算出的平衡因子与root的平衡因子不相等,或root平衡因子的绝对值超过1,则一定不是AVL树 if ((abs(diff) > 1)) { cout << root->_kv.first << "节点平衡因子异常" << endl; return false; } if (diff != root->_bf) { cout << root->_kv.first << "节点平衡因子与root的平衡因子不等,不符合实际" << endl; return false; } //继续递归检测,直到结束 return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right); } public: vector<vector<int>> levelOrder() { vector<vector<int>> vv; if (_root == nullptr) return vv; queue<Node*> q; int levelSize = 1; q.push(_root); while (!q.empty()) { // levelSize控制一层一层出 vector<int> levelV; while (levelSize--) { Node* front = q.front(); q.pop(); levelV.push_back(front->_kv.first); if (front->_left) q.push(front->_left); if (front->_right) q.push(front->_right); } vv.push_back(levelV); for (auto e : levelV) { cout << e << " "; } cout << endl; // 上一层出完,下一层就都进队列 levelSize = q.size(); } return vv; } private: Node* _root = nullptr; };
11.2 テスト.cpp
#define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> #include<vector> #include<time.h> #include<assert.h> using namespace std; #include"AVLTree.h" void TestAVLTree1() { int a[] = { 1,2,3,4,5,6,7,8 }; AVLTree<int, int> t; t.Insert(make_pair(1, 1)); t.Insert(make_pair(2, 2)); t.Insert(make_pair(3, 3)); } void TestAVLTree2() { //int a[] = { 1,2,3,4,5,6,7,8 }; int a[] = { 8,7,6,5,4,3,2,1 }; AVLTree<int, int> t; for (auto e : a) { t.Insert(make_pair(e, e)); } } void TestAVLTree3() { const size_t N = 1024; vector<int> v; srand(time(0)); v.reserve(N); for (size_t i = 0; i < N; i++) { //v.push_back(i); v.push_back(rand()); } AVLTree<int, int> t; for (auto e : v) { t.Insert(make_pair(e, e)); } t.levelOrder(); cout << endl; t.InOrder(); } void TestAVLTree4() { const size_t N = 1024 * 1024; vector<int> v; srand(time(0)); v.reserve(N); for (size_t i = 0; i < N; i++) { v.push_back(i);//有序插入 --》检测单旋是否正确 //v.push_back(rand());//乱序插入 --》检测双旋是否正确 } AVLTree<int, int> t; for (auto e : v) { t.Insert(make_pair(e, e)); } cout << "是否平衡?" << t.IsBalanceTree() << endl; cout << "高度:" << t.Height() << endl; //t.InOrder();//判断是否二叉搜索树 } int main() { //TestAVLTree1(); //TestAVLTree2(); //TestAVLTree3(); TestAVLTree4(); }