C++【二分探索木】

1. 二分探索木

(1) コンセプト

二分探索木別名二分ソート木はBSTに組み込まれており、空の木である場合もあれば、次のプロパティを持つ二分木である場合もあります。左の部分木が空でない場合、左の部分木のすべてのノードの値すべてがルート ノードの値より小さく、右のサブツリーが空でない場合、右のサブツリーのすべてのノードの値がルート ノードの値よりも大きく、その左と右のサブツリーもバイナリです。ツリーを検索します。写真に示すように:
ここに画像の説明を挿入

(2) 操作

二分木には、検索、挿入、削除の 3 つの操作があります。
検索: ルートから比較検索を開始. ルートよりも大きい場合は右に移動して検索し, ルートよりも小さい場合は左に移動して検索します. 最大で検索の高さの倍.空のスペースに行くと、まだ見つかっていないため、この値は存在しません。
挿入: 木が空ならノードを直接追加する. 木が空でないなら, 二分探索木の性質に合わせて挿入位置を見つけて新しいノードを挿入する. 削除 :
まず要素
が二分探索木、存在しない場合は返す、存在しない場合は返す ノードを削除するケースは 3 つあります。
リーフ ノードは、1 つ目と 2 つ目のケースに分類されます。
1. 削除されたノードの右側が空の場合、ノードを削除し、削除されたノードの親ノードが削除されたノードの左側の子ノードを指すようにします。
2. 削除されたノードの左側が空の場合は、ノードを削除し、削除されたノードの親ノードが削除されたノードの右側の子ノードを指すようにします。これも最初のノードと同様に直接削除されます。
3. 左のサブツリーの最大のノード (右端のノード) を見つけ、右のサブツリーの最小のノード (左端のノード) を見つけ、その値を使用して削除されたノードを埋め、ノードの削除を処理します。置換法による削除、間接削除に相当します。

(3) 申請

2 つのモデルがあります。
K モデル: K モデルは、キーのみがキー コードとして使用され、キーのみが構造体に格納される必要があり、キー コードは検索される値であることを意味します

例: 単語 hello が与えられた場合、その単語のスペルが正しいかどうかを判断する. 具体的な方法は次のとおりです:
シソーラスのすべての単語セットの各単語をキーとして使用し、二分探索木を構築し、二分法で単語を取得します。検索ツリー 存在するかどうか、存在する場合はスペルが正しく、存在しない場合はスペルが間違っています。
2. KV モデル: 各キー コードには、対応する値 Value、つまり、<Key, Value> のキーと値のペアがあります。
例: 英中辞書は英語と中国語の対応関係です. 対応する中国語を英語ですぐに見つけることができます. 英語の単語とそれに対応する中国語 <word, Chinese> はキーと値のペアを構成します. 別の例はcount the number of words
. 、単語が与えられた場合、その単語が出現する回数をすばやく見つけることができ、その単語とその出現回数は <word, count> で、キーと値のペアを形成します。

2. BSTシミュレーションの実装と機能解析

(1) BST ノード構造の構築

template<class K>
	struct BSTNode
	{
    
    
		BSTNode<K>* left;
		BSTNode<K>* right;
		K _key;
		BSTNode(const K& t)
			:_key(t)
			,left(nullptr)
			,right(nullptr)
		{
    
    }
		
	};

ノードはツリーを構成するので、構造を構築してそこに属性を追加します. 左の子と右の子をそれぞれ指す左ポインタ左と右ポインタが必要です. ノードの値キーを格納する必要があります. node. ここでは初期化する必要があるので、内部にコンストラクターも記述します。

(2) BSTデフォルト構築とコピー構築

        BST() = default;
        BST(const BST<K>& t)
		{
    
    
			_root = copy(t._root);
		}
		Node* copy(Node* root)
			{
    
    
				if(!root)
				{
    
    
					return nullptr;
				}
				Node* newroot = new Node(root->_key);
				newroot->left = copy(root->left);
				newroot->right = copy(root->right);
				return newroot;
			}

ここで default を使用してデフォルト構造の生成を強制します. コピー構造にはディープコピーが含まれます. 次に, コピー関数をカプセル化します. 空の場合, nullptrを直接返します. そうでない場合, new はノードを作成します.最後にルートノードに戻ります。

(3) BST代入オーバーロードとデストラクタ

BST<K>& operator=(const BST<K>& t)
		{
    
    
			swap(_root, t._root);
			return *this;
		}
		~BST()
		{
    
    
			destroy(_root);
		}
			void destroy(Node*& root)
			{
    
    
				if (!root)
					return;
				destroy(root->left);
				destroy(root->right);
				delete root;
				root = nullptr;
			}

割り当てのオーバーロードの場合、ディープ コピーも必要で、2 つのツリーのルート ノードのポインターまたはアドレスを交換するだけで済みます。デストラクタは 1 つずつ解放し、最後に空にする必要があります。

(4) BST 3 操作の非再帰的実装

(1) 検索

       bool find(const K& key)
		{
    
    
			Node* cur = _root;
			while (cur)
			{
    
    
				if (cur->_key < key)
				{
    
    
					cur = cur->right;
				}
				else if (cur->_key > key)
				{
    
    
					cur = cur->left;
				}
				else
				{
    
    
					return true;
				}
			}
			return false;
		}

非再帰的な方法で検索を記述するには、while ループと BST の性質のみを使用する必要があります: ループ内: ルート ノードから開始し、私があなたより古い場合は、右に移動して現在のノードを更新します。私が若い場合は、左に移動して現在の位置を更新します。そうでない場合は、見つかった場合は true を返し、空の端に移動し、探索の最後に見つからなかった場合は false を返します。

(2)削除

        bool erase(const K& key)
		{
    
    
			//叶子结点,左为空或右为空,可以把前面归到后面,实际上是两类。托孤,要记录父亲(删1和6)
			//第三种情况左右都不为空,请保姆,找左子树的最大结点(最右结点)找右子树的最小结点(最左节点),间接删,替代法。
			Node* parent = nullptr;
			Node* cur = _root;
			while (cur)
			{
    
    
				if (cur->_key < key)
				{
    
    
					parent = cur;
					cur = cur->right;
				}
				else if (cur->_key > key)
				{
    
    
					parent = cur;
					cur = cur->left;
				}
				else
				{
    
    
					if (cur->left == nullptr)//左为空
					{
    
    
						if (cur == _root)
						{
    
    
							_root = cur->right;
						}
						else
						{
    
    
							if (parent->right == cur)
							{
    
    
								parent->right = cur->right;
							}
							else
							{
    
    
								parent->left = cur->right;
							}
						}
						delete cur;

					}

					else if (cur->right == nullptr)//右为空
					{
    
    
						if (cur == _root)
						{
    
    
							_root = cur->left;
						}
						else
						{
    
    
							if (parent->right == cur)
							{
    
    
								parent->right = cur->left;
							}
							else
							{
    
    
								parent->left =cur->left;
							}
						}
						delete cur;
					}
					else//左右不为空,这里去找右子树的最左节点
					{
    
    
						Node* pmiR = cur;
						Node* miR = cur->right;
						while (miR->left)
						{
    
    
							pmiR = miR;
							miR = miR->left;
						}

						cur->_key = miR->_key;
						if (pmiR->right == miR)
						{
    
    
							pmiR->right = miR->right;
						}
						else
						{
    
    
							pmiR->left = miR->right;
						}
						delete miR;
					}
					return true;
				}
			}
			return false;
		}

削除の考え方は、まずBSTプロパティを使って削除するノードを探し、自分より年上なら右へ、年下なら左へ、こちらも記録する親ノードを以降の削除に使用し、最終的に上記の 3 つを使用して削除される状況です。
1. 削除されたノードの左側が空の場合は、まずそのノードがルート ノードであるかどうかを判断し、ルート ノードである場合は、ルート ノードにノードの右側のサブツリーを与え、ルート ノードとして機能させてから、そのノードを解放します。ノード; ルート ノード ノードでない場合は、その中で 2 つの判断を行います。親ノードの右の子が現在のノードである場合、現在のノードの右の子を親ノードの右のポインタにリンクする場合、そうでない場合は、現在のノードの右の子を父の左のポインター リンクにリンクします。最後にノードを解放します。前の図のように、1 と 6 を削除します。
2. 削除されたノードの右側が空の場合、そのノードがルート ノードであるかどうかも最初に判断する必要があります. そうであれば、ルート ノードにノードの左側のサブツリーを与え、それをルート ノードとして機能させます。ルートノードでない場合は内部で2つの判断が必要 親ノードの右子がカレントノードの場合はカレントノードの左子をカレントノードの右ポインタにリンクそれ以外の場合は、現在のノードの左の子と親の左のポインター リンク。前の図のように: 14 を削除します。
3. 削除されたノードの左側と右側が空の場合、置換メソッドを使用して、既存のノードとして機能する適切なノードを見つける必要があります。適切なノード値は、左端のサブツリーの最大ノード (右端のノード)、または右サブツリーの最小ノード (左端のノード) を見つけることです。これが一番右側のノードです. 検索をトラバースするとき, 親ノード pmiR と最小ノードの最小ノード miR を記録する必要があります. 見つけたら上書きし, 最小ノードの値を の値に与えます.ここで、親ノードの左子が最小ノードの場合、最小ノードの右子を親ノードの左ポインタにリンクする必要がある、という2つの判断が必要です。それ以外の場合は、最小ノードの右の子を親の右のポインターにリンクする必要があります。

(3)挿入

        bool insert(const K& key)
		{
    
    
			if (_root ==nullptr)
			{
    
    
				_root = new Node(key);
				return true;
			}
			Node* cur = _root;
			Node* parent = nullptr;
			while (cur)
			{
    
    
				if (key > cur->_key)
				{
    
    
					parent = cur;
					cur = cur->right;
				}
				else if (key < cur->_key)
				{
    
    
					parent = cur;
					cur = cur->left;
				}
				else
				{
    
    
					return false;
				}
			}

			cur = new Node(key);
			if (parent->_key < key)
			{
    
    
				parent->right = cur;
			}
			else
			{
    
    
				parent->left = cur;
			}
			return true;
		}

挿入の考え方は、まず挿入する空きスペースを見つけてBSTプロパティを使い、左右のサブツリーをたどり、同時に親ノードを記録してから、新しい新しいノードを挿入するという2つの判断が必要です。 . それより大きい場合は親ノードの右ポインタをリンクし、そうでない場合は親ノードの左ポインタをリンクします。

(5) BST の 3 つの操作を再帰的に実現する

(1) 検索

        bool _Rfind(Node* root, const K& key)
		{
    
    
			if (!root)
				return false;
			if (root->_key = key)
				return true;
			if (root->_key < key)
				return _Rfind(root->right);
			else 
				return _Rfind(root->left);
		}

再帰的検索方法は引き続き BST プロパティを使用します. それよりも大きい場合は右側の部分木を再帰し, それより小さい場合は左側の部分木を再帰します. 見つかった場合は true を返します.空です。false を返します。

(2)挿入

bool _Rinsert(Node*& root, const K& key)
		{
    
    
			if (!root)
			{
    
    
				root = new Node(key);
			}
			if (root->_key < key)
				return _Rinsert(root->right, key);
			if (root->_key > key)
				return _Rinsert(root->left, key);
		}

再帰挿入もBSTの性質を利用しており、最初に左右のサブツリーを再帰し、空になったら挿入するが、挿入は親とリンクしなければならない。パラメータを追加せずに参照を渡すことができます, これは最適なソリューションです, 前回はそれが機能したためです. 実はこの時のルートは前のルートの左ポインタまたは右ポインタのエイリアスであり, 新しいものはここで作成されたノードは root に与えられ、間接的に接続されます。

(3)削除

bool _Rerase(Node*& root, const K& key)
		{
    
    
			if (!root)
				return false;
			if (root->_key < key)
				return _Rerase(root->right, key);
			else if (root->_key > key)
				return _Rerase(root->left, key);
			else
			{
    
    
				Node* del = root;
				if (root->right == nullptr)
				{
    
    
					root = root->left;
				}
				else if (root->left == nullptr)
				{
    
    
					root = root->right;
				}
				else
				{
    
    
					Node* mal = root;
					mal = mal->left;
					
					while (mal->right)
					{
    
    
						mal = mal->right;
					}
					swap(root->_key, mal->_key);
					return _Rerase(root->left, mal->_key);

				}
				delete del;
				return true;
			}

		}

再帰的な削除、BST プロパティを使用して削除するノードを見つける、再帰的な左右のサブツリー、ここで親ノードを記録する必要はなく、3 つのケースもあります。
1. 削除されたノードの左側は空であり、del ポインター変数を定義し、現在削除されているノードを指定してから、リンクし、参照を使用します。親ノードがどのポインターにリンクされているかを気にする必要はなく、直接リンクし、リリースデル。
2. 1と同じ。
3. 削除されたノードの左側と右側は空ではなく、それを置き換える適切なノードが見つかります. ここで、左のサブツリーの最大のノード、つまり最も右のノードが見つかります. それを見つけた後、削除されたノードノードと最大のノードは値交換です。再帰を再度実行して、左のサブツリーの最大のノードを削除します。この時点で、mal によって保存されたキーは 3 番目の条件に入りません。そのため、最初のケースか2番目のケースか、どちらの場合でも参照を使用して、父親を見つけてリンクすることができます.
ここで、2 番目の再帰ポインターを直接 mal に渡すことはできないことに注意してください。これが渡されると、参照が役に立たなくなり、リンクが機能しなくなります。これは、最初または 2 番目の条件に入るためです。必要なのは前の位置です。参照は、この場所での参照ではありません。

3. BST 実装ソースコード

(1)BST.h

#pragma once


namespace nza
{
    
    
	template<class K>
	struct BSTNode
	{
    
    
		BSTNode<K>* left;
		BSTNode<K>* right;
		K _key;
		BSTNode(const K& t)
			:_key(t)
			,left(nullptr)
			,right(nullptr)
		{
    
    }
		
	};
	template<class K>
	class BST
	{
    
    
		typedef BSTNode<K> Node;
	public:
		BST() = default;
		BST(const BST<K>& t)
		{
    
    
			_root = copy(t._root);

		}
		BST<K>& operator=(const BST<K>& t)
		{
    
    
			swap(_root, t._root);
			return *this;
		}
		~BST()
		{
    
    
			destroy(_root);
		}

		//非递归如下:
		bool insert(const K& key)
		{
    
    
			if (_root ==nullptr)
			{
    
    
				_root = new Node(key);
				return true;
			}
			Node* cur = _root;
			Node* parent = nullptr;
			while (cur)
			{
    
    
				if (key > cur->_key)
				{
    
    
					parent = cur;
					cur = cur->right;
				}
				else if (key < cur->_key)
				{
    
    
					parent = cur;
					cur = cur->left;
				}
				else
				{
    
    
					return false;
				}
			}

			cur = new Node(key);
			if (parent->_key < key)
			{
    
    
				parent->right = cur;
			}
			else
			{
    
    
				parent->left = cur;
			}
			return true;
		}

		bool find(const K& key)
		{
    
    
			Node* cur = _root;
			while (cur)
			{
    
    
				if (cur->_key < key)
				{
    
    
					cur = cur->right;
				}
				else if (cur->_key > key)
				{
    
    
					cur = cur->left;
				}
				else
				{
    
    
					return true;
				}
			}
			return false;
		}

		bool erase(const K& key)
		{
    
    
			//叶子结点,左为空或右为空,可以把前面归到后面,实际上是两类。托孤,要记录父亲(删1和6)
			//第三种情况左右都不为空,请保姆,找左子树的最大结点(最右结点)找右子树的最小结点(最左节点),间接删,替代法。
			Node* parent = nullptr;
			Node* cur = _root;
			while (cur)
			{
    
    
				if (cur->_key < key)
				{
    
    
					parent = cur;
					cur = cur->right;
				}
				else if (cur->_key > key)
				{
    
    
					parent = cur;
					cur = cur->left;
				}
				else
				{
    
    
					if (cur->left == nullptr)//左为空
					{
    
    
						if (cur == _root)
						{
    
    
							_root = cur->right;
						}
						else
						{
    
    
							if (parent->right == cur)
							{
    
    
								parent->right = cur->right;
							}
							else
							{
    
    
								parent->left = cur->right;
							}
						}
						delete cur;

					}

					else if (cur->right == nullptr)//右为空
					{
    
    
						if (cur == _root)
						{
    
    
							_root = cur->left;
						}
						else
						{
    
    
							if (parent->right == cur)
							{
    
    
								parent->right = cur->left;
							}
							else
							{
    
    
								parent->left =cur->left;
							}
						}
						delete cur;
					}
					else//左右不为空,这里去找右子树的最左节点
					{
    
    
						Node* pmiR = cur;
						Node* miR = cur->right;
						while (miR->left)
						{
    
    
							pmiR = miR;
							miR = miR->left;
						}

						cur->_key = miR->_key;
						if (pmiR->right == miR)
						{
    
    
							pmiR->right = miR->right;
						}
						else
						{
    
    
							pmiR->left = miR->right;
						}
						delete miR;
					}
					return true;
				}
			}
			return false;
		}


		void inorder()
		{
    
    
			_inoder(_root);
			cout << endl;
		}

		//递归如下:
		bool Rfind(const K& key)
		{
    
    
			return _Rfind(_root, key);
		}
		bool Rinsert(const K& key)
		{
    
    
			return _Rinsert(_root, key);
		}
		bool Rerase(const K& key)
		{
    
    
			return _Rerase(_root, key);
		}

	protected:
		bool _Rfind(Node* root, const K& key)
		{
    
    
			if (!root)
				return false;
			if (root->_key = key)
				return true;
			if (root->_key < key)
				return _Rfind(root->right);
			else 
				return _Rfind(root->left);
		}
		bool _Rinsert(Node*& root, const K& key)
		{
    
    
			if (!root)//插入要和父亲链接起来,在不增加参数的情况下我们可以传引用,是最优方案,因为就最后一次起了作用,实际上就是
				//此时的root是上一层root指向左指针或右指针的别名,在这里new了一个结点给给root,间接地链接上了。
			{
    
    
				root = new Node(key);
			}
			if (root->_key < key)
				return _Rinsert(root->right, key);
			if (root->_key > key)
				return _Rinsert(root->left, key);
		}
		bool _Rerase(Node*& root, const K& key)
		{
    
    
			if (!root)
				return false;
			if (root->_key < key)
				return _Rerase(root->right, key);
			else if (root->_key > key)
				return _Rerase(root->left, key);
			else
			{
    
    
				Node* del = root;
				if (root->right == nullptr)
				{
    
    
					root = root->left;
				}
				else if (root->left == nullptr)
				{
    
    
					root = root->right;
				}
				else
				{
    
    
					Node* mal = root;
					mal = mal->left;
					
					while (mal->right)
					{
    
    
						mal = mal->right;
					}
					swap(root->_key, mal->_key);
					return _Rerase(root->left, mal->_key);//引用不能改指向,所以把它们的值交换,重新再做一次递归转化为删掉左子树的
					//最大结点,此时的mal存的key不会进入第三个条件,因为此时mal的孩子要么有一个要么没有。这里注意的这里第二次递归指针
					// 不能直接传mal,因为如果传了,我们的引用就没有用了就链接不了,因为它会进入第一个或第二条件,我们要的是上个位置的引用
					//不是这个位置的引用

				}
				delete del;
				return true;
			}

		}
			Node* copy(Node* root)
			{
    
    
				if(!root)
				{
    
    
					return nullptr;
				}
				Node* newroot = new Node(root->_key);
				newroot->left = copy(root->left);
				newroot->right = copy(root->right);
				return newroot;
			}
			void destroy(Node*& root)
			{
    
    
				if (!root)
					return;
				destroy(root->left);
				destroy(root->right);
				delete root;
				root = nullptr;
			}
			void _inoder(Node* root)
			{
    
    
				if (!root)
					return;
				_inoder(root->left);
				cout << root->_key << " ";
				_inoder(root->right);

			}

	private:
		Node* _root = nullptr;
		
	};
}

(2)Test.cpp

#include<iostream>
using namespace std;
#include"BST.h"
int main()
{
    
    
	nza::BST<int> t;
	int a[] = {
    
     5,2,9,6,1,0,3 };
	for (auto n : a)
	{
    
    
		t.insert(n);
	}
	t.inorder();

	t.erase(9);
	t.inorder();

	t.erase(5);
	t.inorder();

	t.erase(1);
	t.inorder();


	t.Rerase(2);
	t.inorder();
	 
	t.Rerase(0);
	t.inorder();

	t.Rinsert(100);
	t.inorder();


}

4.BSTの走行結果

ここに画像の説明を挿入

五、二分探索木の性能

挿入操作と削除操作の両方を最初に検索する必要があり、検索効率は二分探索ツリーの各操作のパフォーマンスを表します。
二分探索木は完全な二分木 (または完全な二分木に近い) であり、回数は O(logN) です。
ここに画像の説明を挿入
二分探索木は単分木であり、その回数は O(N^2) であり、
ここに画像の説明を挿入
単分木に退化すると二分探索木の性能が失われます。キーコードがどの順序で挿入されても、二分探索木のパフォーマンスは最高に達し、AVL 木と赤黒木はこの問題を解決できます。

おすすめ

転載: blog.csdn.net/m0_59292239/article/details/130300614