高さバランス二分探索ツリー (AVL ツリー)

目次

序文

1. AVL ツリー - 高さバランスのとれた二分探索ツリー

1.1 AVLツリーの概念

1.2 AVLツリーノードの定義 - トリプルチェーン

2. AVLツリーの挿入

2.1 AVLツリーのローテーション

        2.1.1 新しいノードが右上のサブツリーの右側に挿入されます --- 右 右: 左単一回転

        2.1.2 新しいノードが左上のサブツリーの左側に挿入されます --- left left: 右単一回転 (左単一回転と同じロジック)

        2.1.3 新しいノードが左上のサブツリーの右側に挿入されます --- 左と右: 最初に左の単一​​回転、次に右の単一回転

        2.1.4 新しいノードが右上のサブツリーの左側に挿入されます --- 右左: 最初に右への単一回転、次に左への単一回転

 要約:


世界の大きな出来事は詳細に行わなければなりません!

序文

        AVLtree は、「追加のバランス条件」を備えた二分探索木です。そのバランス条件の確立は、ツリー全体の深さが O(logN) であることを保証することです。直感的には、各ノードの左右のサブツリーの高さが同じであることが最適なバランス条件となりますが、これでは厳しすぎて、このようなバランス条件を維持しながら新しい要素を挿入することは困難です。次に、AVLtree は次善の方法を採用し、任意のノードの左右のサブツリー間の高さの差が最大 1 であることを要求します。これは弱い条件ですが、それでも「対数深さ」の平衡状態が保証されます。

1. AVL ツリー - 高さバランスのとれた二分探索ツリー

1.1 AVLツリーの概念

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

バランス係数: 右のサブツリーの高さ - 左のサブツリーの高さ

AVL ツリーは、空のツリー、または次のプロパティを持つ二分探索ツリーのいずれかです。
  • その左と右のサブツリーは両方とも AVL ツリーです
  • 左右のサブツリーの高さの差の絶対値 (バランス係数と呼ばれます) は 1 (-1/0/1) を超えません。
二分探索ツリーが高さのバランスが取れている場合、それは AVL ツリーです。n 個のノードがある場合、高さを維持できます。
O(log n)、検索時間の複雑さは O(log n)。

1.2 AVLツリーノードの定義 - トリプルチェーン

template<class K,class V>
struct AVLTreeNode
{
	AVLTreeNode<K,V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;

	pair<K, V> _kv;
	int _bf;

	AVLTreeNode(const pair<K, V>& kv)
		:_left(nullptr),
		_right (nullptr),
		_parent (nullptr),
		_kv (kv),
		_bf (0)
	{}
};

2. AVLツリーの挿入

AVL ツリーは二分探索木に基づいてバランス係数を導入するため、AVL ツリーは二分探索木とみなすこともできます。それで
AVL ツリーの挿入プロセスは 2 つのステップに分けることができます。
  • 二分探索木の途中に新しいノードを挿入します
  • ノードのバランス係数を調整します

挿入は祖先 (親パス) に影響し、左側のサブツリーの高さが変更されました (parent->_bf--)、右側のサブツリーの高さが変更されました (parent->_bf++)

挿入操作の先頭は二分探索ツリーと同じですが、親操作を指す cur->_parent が追加されています。

バランスを再制御: バランス係数を更新します。

バランス係数を更新するためのルール:

1.右側に新規追加、parent->_bf++、左側に新規追加、parent->_bf--

2.更新後、parent->_bf == 1 または -1 は、挿入前の親のバランス係数が 0 であることを示し、左右のサブツリーの高さが等しく、挿入後に一方の側が高くなったことを示します。挿入し、親の高さが変更されたため、引き続き更新する必要があります

3. 更新後、parent->_bf == 0 は、挿入前の親のバランス係数が 1 または -1 であることを示し、左右のサブツリーの一方がより高く、もう一方がより低いことを示します。挿入、両側が同じ高さであり、挿入によって短辺が埋められます。親が配置されているサブツリーの高さは変更されず、更新を続ける必要はありません。

4. 更新後、parent->_bf == 2 または -2 は、挿入前の親のバランス係数が 1 または -1 であり、既にクリティカル値のバランスが取れていることを示します。挿入後は 2 または -2 になります。これによりバランスが崩れ、親が配置されている子はツリーを回転する必要があります

 以下にいくつかの例を示します (すべて回転なし): ルート ノードに対する最悪のケース (つまり、parent==nullptr)

コード:

        //控制平衡因子
		while (parent)
		{
			if (cur == parent->_left)
			{
				parent->_bf++;
			}
			else
			{
				parent->_bf--;
			}
			//插入后为0,说明插入前为1/-1,说明高度不一致,但是插入必定向矮的那端插入,插入后高度一致
			if (parent->_bf == 0)
			{
				break;
			}
			//插入后为1/-1,说明插入前为0,说明高度一致,现在高度改变,向上更新
			else if (abs(parent->_bf) == 1)
			{
				parent = parent->_parent;
				cur = cur->_parent;
			}
			//插入后为2,打破平衡,需要旋转处理
			else if (abs(parent->_bf == 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);
				}
				break;
			}
			else
			{
				assert(false);
			}
		}
	}

2.1 AVLツリーのローテーション

AVL ツリーのバランス条件: 任意のノードの左右のサブツリーの高さの差が最大 1

更新後に parnt->_bf==2 または -2 になった場合、バランス限界値が壊れており、この時点で回転操作が必要です。

回転原理:

        a. バランスのとれたツリーに回転する b. 検索ツリーのルールを維持する

回転には次の 4 種類があります。

        1. 左単回転

        2. 右一回転

        3. 左右2回転

        4.右左双旋

このうち(1,2)は対称になっており、外挿(外挿)と呼ばれます。(3, 4) は互いに対称であり、内側挿入と呼ばれます。

        2.1.1 新しいノードが右上のサブツリーの右側に挿入されます --- 右 右: 左単一回転

抽象的なグラフ:

上の図では、挿入前に AVLツリーのバランスがとられ、新しいノードが60の右側のサブツリーに挿入され(注: これは右側の子ではありません)、レイヤーが 60 の右側のサブツリーに追加されます。30 をルートとする不平衡二分木では、 30 を平衡にするには、30の右側のサブツリーの高さを1 レベルだけ下げ、左側のサブツリーの高さを 1 レベル増やすことができます。

  つまり、右側のサブツリーが持ち上げられ、その結果 30 が下に下がります 。30 は 6 0 より小さいため、それは 6 0の左側のサブツリー にのみ配置できます。6 0に左側のサブツリーがある場合、左側のサブツリーのルートの値は 30 より大きく、 60未満でなければなりません。30の右側のサブツリーにのみ配置できます。回転が完了した後は、ノードのバランス係数を更新するだけです。ローテーション中には、考慮すべき状況がいくつかあります。
  1. ノード 60 の左側の子は存在する場合と存在しない場合があります。
  2. 30 は ルート ノードまたはサブツリーである可能性があります
    ルート ノードの場合は、ローテーションの完了後にルート ノードを更新する必要があります。
    サブツリーの場合、ノードの左側のサブツリーまたは右側のサブツリーである可能性があります。

比喩的なイメージ:

コード:

void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		//根左旋,压下去,连接subRL
		parent->_right = subRL;
		if (subRL)
			subRL->_parent = parent;

		//记录下parent的父节点
		Node* ppNode = parent->_parent;

		subR->_left = parent;
		parent->_parent = subR;

		//如果根结点是父节点
		if (_root == parent)
		{
			_root = subR;
			subR->_parent = nullptr;
		}
		else
		{
			if (ppNode->_left == parent)
			{
				ppNode->_left = subR;
			}
			else
			{
				ppNode->_right = subR;
			}

			subR->_parent = ppNode;
		}
		subR->_bf = parent->_bf = 0;

	}

        2.1.2 新しいノードが左上のサブツリーの左側に挿入されます --- left left: 右単一回転 (左単一回転と同じロジック)

上の図では、挿入前に AVLツリーのバランスが取れています。新しいノードが30の左側のサブツリーに挿入され(注: これは左側の子ではありません) 30の左側のサブツリーにレイヤーが追加されるため、バランスが取れていません。 60をルートとする二分木です。60 のバランスを保つには、60の左側のサブツリーの高さを 1 レベル減らし、右側のサブツリーの高さを 1 レベル増やす、つまり左側のサブツリーを持ち上げるだけです。 60 は下に回転できます。60 は30より大きいため、下に置くことしかできません。30の右サブツリーでは、 30に右サブツリーがある場合、右サブツリーのルートの値は 30 より大きく、それ以下でなければなりません60より大きく、60 の左側のサブツリーにのみ配置できます。ローテーションが完了すると、更新ノードのバランス係数は Can になります。ローテーション中には、考慮すべき状況がいくつかあります。

1. ノード 30 の右側の子は存在する場合と存在しない場合があります。
2. 60 は ルート ノードまたはサブツリーである可能性があります
    ルート ノードの場合は、ローテーションの完了後にルート ノードを更新する必要があります。
    サブツリーの場合、ノードの左側のサブツリーまたは右側のサブツリーである可能性があります。

例:

        横挿入状態では、K2のみ「挿入前バランス、挿入後アンバランス」の場合を図の左側に示します。A サブツリーは 1 レベル成長し、C サブツリーよりも 2 深くなります。B サブツリーが A サブツリーと同じレベルになることは不可能です。そうでない場合、k2 は挿入前にアンバランスな状態になります。また、B サブツリーが C サブツリーと同じレベルになることも不可能です。そうでない場合、バランス条件の最初の違反は k2 ではなく k1 になります。

         バランス状態を調整するには、A サブツリーを 1 レベル上げ、C サブツリーを 1 レベル下げることを考えています。これは、AVL ツリーで要求されるバランス状態よりもすでに一歩進んでいます。図の右側が調整後の状態です。このように想像して、k1 を持ち上げ、k2 を自然に下にスライドさせ、B サブツリーを k2 の左側に吊るすことができます。これが行われるのは、二分探索ツリーのルールにより k2>k1 であるため、新しいツリーでは k2 が k1 の右側の子になる必要があるためです。二分探索木の規則によれば、B サブツリーのすべてのノードのキー値は k1 と k2 の間にあるため、新しいツリー形状の B サブツリーは k2 の左側に位置する必要があります。

 コード:

void RotateR(Node* parent)
	{
		Node* subL = parent->_right;
		Node* subLR = subL->_right;

		parent->_left = subLR;
		if (subLR)
			subLR->_parent = parent;

		Node* ppNode = parent->_parent;

		subL->_right = parent;
		parent->_parent = subL;
		
		if (_root == parent)
		{
			_root = subL;
			subL->_parent = nullptr;
		}
		else
		{
			if (ppNode->_left == parent)
			{
				ppNode->_left = subL;
			}
			else
			{
				ppNode->_right = subL;
			}
			subL->_parent = ppNode;
		}
		subL->_bf = parent->_bf = 0;
	}

        2.1.3 新しいノードが左上のサブツリーの右側に挿入されます --- 左と右: 最初に左の単一​​回転、次に右の単一回転

2 回転を 1 回転に変換してから回転します。つまり、まず 30 で左 1 回転を実行し、次に 90 で右 1 回転を実行し、回転が完了した後でバランス係数を更新することを検討します (バランス係数は固定されていません。これは、b 挿入にある可能性もあれば、c にも挿入される可能性もあるし、または h=0 (30 の右側に挿入される) である可能性もあります。回転前のバランスファクター60を判定基準とし、判定を記録する。

比喩的なイメージ:

 コード:

void RotateLR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		int bf = subLR->_bf;

		RotateL(parent->_left);
		RotateR(parent);

		if (bf == 1)
		{
			subL->_bf = -1;
			parent->_bf = 0;
		}
		else if (bf == -1)
		{
			subL->_bf = 0;
			parent->_bf = 1;
		}
		else if (bf == 0)
		{
			subL->_bf = parent->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

        2.1.4 新しいノードが右上のサブツリーの左側に挿入されます --- 右左: 最初に右への単一回転、次に左への単一回転

 要約:

pParent をルートとするサブツリーがアンバランスである場合、つまり、pParent のバランス係数が 2 または -2 である場合は、次の状況を考慮してください。
1. pParent のバランス係数は 2 で、pParent の右サブツリーの高さを示し、pParent の右サブツリーのルートは pSubR です。
        pSubR のバランス係数が 1 の場合、左 1 回転が実行されます。
        pSubR のバランス係数が -1 の場合、左右 2 回転します。
2. pParent のバランス係数は -2 で、pParent の左サブツリーの高さを示し、pParent の左サブツリーのルートは pSubL です。
        pSubL のバランス係数が -1 の場合、右 1 回転を実行します
        pSubL のバランス係数が 1 の場合、左右の 2 回転が実行されます。

回転が完了すると、元の pParent をルートとするサブツリーの高さが減り、バランスが保たれるため、上方への更新は必要ありません。

回転の価値と重要性:

1.バランス

2. 高さを低くする(挿入前の外観に戻す)

おすすめ

転載: blog.csdn.net/bang___bang_/article/details/131793941