高度なデータ構造 - バランス型バイナリ ツリー (AVL ツリー)

目次

1. 基礎となる構造

2. AVL番号の概念 

3. AVLツリーノードの定義

4. 基本的な枠組み

5. AVLツリーの挿入

6. AVLツリーの回転

6.1 単一左回転

6.2 右一回転

6.3 左右二重回転 

6.4 右左双旋

7. AVLツリーの検証

8. AVLツリーの検索

9. AVLツリーの削除

10. AVLツリーのパフォーマンス

11. 合計コード

11.1 AVLツリー

11.2 テスト.cpp

1. 基礎となる構造

Map、multimap、set、および multiset については以前に簡単に紹介しました。これらのコンテナには共通点が 1 つあります。それらの最下層は二分探索ツリーに従って実装されていますが、二分探索ツリーには独自の欠点があります。ツリーに挿入された要素が順序付けまたは順序付けに近い場合、二分探索木は単一の木に縮退し、時間計算量は O(N) に縮退します。したがって、マップやセットなどの連想コンテナーの基礎となる構造は、二分木はバランスが取れています。 、バランスのとれたツリーを使用して実装されます。

2. AVL番号の概念 

二分探索木は検索効率を低下させる可能性がありますが、データが順序付けされている、または順序付けに近い場合、二分探索木は単一の枝木に縮退します要素の検索は、順序表内の要素の検索と同等であり、非効率 。したがって、2 人のロシアの数学者 GMAdelson-Velskii と EMLandis は 1962 年に上記の問題を解決する方法を発明しました。二分探索木に新しいノードを挿入した後、各ノードの左右のサブツリーが保証できるかどうか、高さの差が 1 を超えない場合(ツリー内のノードを調整する必要がある)、ツリーの高さを低くすることができるため、平均検索長が短縮されます。

AVL ツリーは、空のツリー、または次のプロパティを持つ二分探索ツリーのいずれかです。

  1. その左と右のサブツリーは両方とも AVL ツリーです
  2. 左右のサブツリー間の高さの差の絶対値 (バランス係数と呼ばれます) は 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. 空ではないツリーから始めて、挿入に適した場所を見つけます。

ここでの考え方は、二分探索木で適切な挿入位置を見つける考え方と同じであり、次の手順に従う必要があります。

  1. 挿入された値 > ノード値、右サブツリー検索に更新
  2. 挿入値 < ノード値、左のサブツリー検索に更新
  3. 挿入された値 = ノード値。データ冗長性の挿入は失敗し、false を返します。

ループが終了すると、挿入に適した場所が見つかったことを意味し、リンクの次のステップを実行できるようになります。

  • 3. 適切な挿入位置を見つけたら、父親と子の間で双方向リンクを実行します。

ここのノードは 3 方向チェーンで構成されているため、バックエンドの子と父親の間の最終リンクは 2 方向リンクであることに注意してください。具体的な操作は次のとおりです。

  1. 挿入された値 > 親の値、挿入された値を親の右側にリンクします
  2. 挿入された値 < 親の値、挿入された値を親の左側にリンクします
  3. これは 3 方向リンクであるため、挿入後は必ず双方向リンクを作成してください (子は父親にリンクします)。

この点に到達すると、ノードは挿入されましたが、次にバランス係数を更新する必要があることを意味します。

  • 4. 新しく挿入されたノードの祖先のバランス係数を更新します。

新しいノードを挿入すると、サブツリーの高さが変更される可能性があります。この変更については、次の要件を与えます。

  1. サブツリーの高さが変更された場合は、引き続き更新する必要があります。
  2. サブツリーの高さが変わらなければ更新は完了です。
  3. サブツリーがバランス ルールに違反している場合 (バランス係数の絶対値 >= 2)、更新は停止し、調整のためにサブツリーを回転する必要があります。

具体的な更新ルールは次のとおりです。

  1. 新しいノードは親の右側にあり、親のバランス係数は ++ です
  2. 新しいノードは親の左側にあり、親のバランス係数は — — です

ノードのバランス係数を更新した後、次の判断を行う必要があります。

  1. 親のバランス係数が -1 または 1 に等しい場合 (元は 0 で、ノードの挿入後に左右が増加し、左側のサブツリーまたは右側のサブツリーが増加したことを意味します)。バランス係数を上方に更新し続ける必要があることを示します
  2. 親のバランス係数が 0 に等しい場合 (元々は 1 または -1 で、1 つは高、もう 1 つは低であり、ノードの挿入後に短い方が埋められます)、バランスを更新する必要がないことを意味します。要因
  3. 親のバランス係数が -2 または 2 に等しい場合 (これは、元々は 1 または -1 で、1 つは高、もう 1 つは低であり、高い方がノードの挿入後に埋められることを意味します)。この時点でルート ノードとしての親ノードはすでにアンバランスになっているため、回転する必要があります
  • 5. 非準拠のバランス係数の調整を回転します。

親のバランス係数が 2 または -2 の場合、回転調整が必要となり、回転を次の 4 つのカテゴリに分類する必要があります。

  1. 親のバランス係数が 2、cur のバランス係数が 1 の場合、左 1 回転が実行されます。
  2. 親のバランス係数が-2、curのバランス係数が-1の場合、右一回転を行います。
  3. 親のバランス係数が-2、curのバランス係数が1の場合、左右2重回転が行われます。
  4. 親のバランス係数が 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 つのタイプがあります。

  1. 左単回転
  2. 右一回転
  3. 左右二回転
  4. 右左双旋

AVL ツリーのローテーションは、次の 2 つの原則に従う必要があります。

  • 1. 検索ツリーのルールを守る
  • 2. サブツリーのバランスが取れた状態になる

6.1 単一左回転

  • 条件: 新しいノードが右上のサブツリーの右側に挿入されます。

  左回りの場合はたくさんありますが、ここでは抽象的な図を描いて説明します。

ここで、長方形のバー (a、b、c) はサブツリーを表し、h はサブツリーの高さ、30 と 60 は実際のノードです。上記の左の単一回転操作は主に 4 つのことを実現します。

  1. subRL を親の正しいサブツリーにし、subRL の親を親に更新します。
  2. subR をルート ノードにします。
  3. 親を subR の左側のサブツリーにし、親の父親を subR に更新します。
  4. バランス係数を更新します。

知らせ:

  1. 親はツリー全体のサブツリーである場合があり、親の親とサブR をリンクする必要があります。
  2. 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 つのことを実現します。

  1. subLR を親の左側のサブツリーとし、subLR の親を親に更新します。
  2. subL をルートノードにする
  3. 親を subL の右側のサブツリーにし、親の親を subL に更新します。
  4. バランス係数を更新する

知らせ:

  1. 親はツリー全体のサブツリーである場合があり、親の親とサブL をリンクする必要があります。
  2. 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. バランスの取れたツリーであることを確認します。

ルールは次のとおりです: (再帰的アイデア)

  1. 空の木もバランスの取れた木であり、最初から判断する必要があります。
  2. 特に高さを計算する関数 (再帰的に高さを計算) をカプセル化し、その後高さの差 (バランス係数の差) を計算するために使用されます。
  3. diff がルートのバランス係数 (root->_bf) と等しくない場合、またはルート バランス係数の絶対値が 1 を超える場合、AVL ツリーであってはなりません。
  4. 最後までサブツリーと右ツリーへの再帰を続けます

コードは以下のように表示されます。

//验证一棵树是否为平衡树
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 ポインタを定義します。

  1. キー値が現在のノードの値より小さい場合、検索はノードの左側のサブツリーで実行される必要があります。
  2. キーの値が現在のノードの値より大きい場合、検索はノードの右側のサブツリーで実行される必要があります。
  3. キーの値が現在のノードの値と等しい場合、検索は成功し、true が返されます。
  4. 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 つの主なアイデアがあります。

  1. 二分探索木のルールに従って削除する
  2. バランス係数を更新する
  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();
}

おすすめ

転載: blog.csdn.net/m0_49687898/article/details/131334137