<二分探索木>を20分で徹底解説!!!

目次

前文

まず、二分探索木とは何でしょうか?

1.1 二分探索木の概念

2. 二分探索木の共通操作と実装

2.1 検索

2.2 インサート

 2.3 削除

三、二分探索木の応用

3.1 K モデル

3.2KVモデル

第四に、二分探索木の性能分析

5、コード

要約する


前文

この記事は主に、二分探索木の実装と使用法を理解することを目的としています。

ps: 二分探索木のすべてのコードは記事の最後に掲載されます (構築、破壊、代入などを含む)。

まず、二分探索木とは何でしょうか?

1.1 二分探索木の概念

二分探索ツリー (二分ソート ツリーとも呼ばれます) は、空のツリーまたは次の特性を持つ二分木です。

1.左のサブツリーが空でない場合、その左のサブツリーのすべての値はルートよりも小さくなります。

2.右のサブツリーが空でない場合、その右のサブツリーのすべての値はルートよりも大きくなります

3.その左右のサブツリーも二分探索木に準拠します。

二分探索木は古典的なデータ構造として、連結リストの挿入・削除操作が高速であるという特徴だけでなく、配列の検索も高速であるという利点があるため、広く使用されている。ファイル システムとデータベース システム 効率的な並べ替えと検索操作のためのデータ構造。 

2. 二分探索木の共通操作と実装

上図は検索二分木のノード構造とメンバ変数を示しています。

2.1 検索

実装アイデア:

1.ルートから比較を開始し、ルートより大きい場合は右へ、ルートより小さい場合は左へ進みます。

2.ほとんどの場合、ツリーの高さを比較し、空で見つからない場合は false を返します。

 上図のように、左側が非再帰実装、右側が再帰実装ですが、ここで注意が必要なのは再帰実装です、クラス内の*thisポインタを通じてメンバ関数を呼び出しているため、 *this ポインタは隠蔽されており、再帰条件を制御できないため、クラス内で再帰を記述する場合は、サブ関数を介して再帰呼び出しを完了する必要があります。


2.2 インサート

実装アイデア:

1.ツリーが空の場合は、ルートに新しいノードを直接作成します

2.ツリーは空ではありません。二分木検索の規則に従って空の位置を見つけて、それを挿入します。

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

 2.3 削除

実装アイデア:

二分木検索のルールを使用して目的のノードを検索し、存在しない場合は false を返しますが、削除する場合は次の 4 つの状況を考慮する必要があります。

1.ターゲット ノードには左右の子がありません。

2.ターゲット ノードには左の子はありますが、右の子はありません。

3.ターゲット ノードには右の子はありますが、左の子はありません

4.ターゲットノードの左右の子が存在します。

 左右の子が存在しない状況は、左の子または右の子のみで処理できるため、左右の子が存在しない処理メソッドと、左の子のみが存在する処理メソッドをマージします。

次のように処理されます。

2.ノードを削除します。削除されたノードの親ノードは、削除されたノードの左側の子を指します。直接削除します。

 3. ノードを削除します。削除されたノードの親ノードは、削除されたノードの右側の子を指します。直接削除します。

 4. その位置を置き換えることができる値を見つけます。つまり、左側のサブツリーがこの値より小さく、右側のサブツリーがこの値より大きいことを満たすために、左側のサブツリーの最大値または最小値を見つけることができます。右側のサブツリーを選択し、見つかった最大値または最小値に削除されたノードの値を代入し、最大値または最小値ノードを削除します。

 

 上の図のように、左側が非再帰的書き込み、右側が再帰的書き込みです。理解できない場合は、プライベートメッセージを送ってください。

三、二分探索木の応用

3.1 K モデル

Kモデルはキーコードとしてキーのみを持ち、キーのみを構造体に格納する必要があり、キーコードは検索される値です

実用:

与えられた単語が正しいスペルであるかどうかを、次の具体的な方法で判断します。

具体的な方法は以下の通りです。

1. シソーラス内のすべての単語コレクションの各単語をキーとして使用して、二分検索ツリーを構築します。
2. 単語が二分検索ツリーに存在するかどうかを検索します。存在する場合はスペルが正しいか、存在しない場合は、スペルが間違っています

3.2KVモデル

各キー コード キーには、対応する値 Value、つまり <Key, Value> のキーと値のペアがありますこのアプローチは実生活でも非常に一般的です。

1. たとえば、英語-中国語辞書は英語と中国語の対応関係です。英語を通じて対応する中国語をすぐに見つけることができます。英語の単語とそれに対応する中国語 <word, Chinese> はキーと値のペアを構成します。

2. 別の例は、単語の数をカウントすることです。統計が成功すると、特定の単語の出現数をすぐに見つけることができます。単語と出現数は <word, count> でキーと値を形成します。ペア

第四に、二分探索木の性能分析

二分探索木における検索は避けられない機能であり、挿入でも削除でも最初に検索する必要があるため、検索の効率は二分探索木の性能に直結します

 同じ配列の集合ではありますが、挿入順序の違いによって構築される探索二分木も異なり、上図に示すように、左側は比較的正常な木、右側は極端な場合には曲がった首の木になります。 。

左側のケースと同様の最適なケースでは、複雑さはツリーの高さ、つまり logN になります。

最悪の場合、右の首が曲がった木のようになりますが、このとき木の高さは N に近いため、複雑度は N、

では、右側の曲がった首のツリーの最適化方法は何でしょうか?

きっとあるはず、フォローアップで学習したAVL木と赤黒木は大きな効果を発揮します

5、コード

//K模型
namespace key
{
	template<class K>
	struct BSTreeNode
	{
		BSTreeNode(K key)
			:_left(nullptr)
			, _right(nullptr)
			, _key(key)
		{}

		BSTreeNode<K>* _left;
		BSTreeNode<K>* _right;
		K _key;

	};

	template<class K>
	class BSTree
	{
		typedef BSTreeNode<K> Node;
	public:
		//构造函数
		BSTree()
			:_root(nullptr)
		{}
		//拷贝构造函数,用copy函数前序遍历拷贝
		BSTree(BSTree<K>& t)
		{
			_root = copy(t._root);
		}
		Node* copy(Node* root)
		{
			if (root == nullptr)
				return nullptr;

			Node* ret = new Node(root->_key);
			copy(root->_right);
			copy(root->_left);
			return ret;

		}

		//赋值重载
		BSTree<K>& operator=(BSTree<K> t)
		{
			swap(_root, t._root);
			return *this;
		}

		//析构函数,调用Destory后续遍历销毁即可
		~BSTree()
		{
			Destory(_root);
		}
		void Destory(Node*& root)
		{
			if (root == nullptr)
				return;

			Destory(root->_right);
			Destory(root->_left);
			delete root;
			root = nullptr;

		}
		 
		//查找
		bool Find(const K& key)
		{
			if (_root == nullptr)
				return false;

			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 Findr(const K& key)
		{
			_Findr(_root, key);
		}
		bool _Findr(Node*& root, const K& key)
		{
			if (root == nullptr)
			{
				return false;
			}
			if (root->_key < key)
			{
				return _Findr(root->_right, key);
			}
			else if (root->_key > key)
			{
				return _Findr(root->_left, key);
			}
			else//找到了
			{
				return true;
			}
			return false;
		}

		//删除(erase)
		bool Erase(const K& key)
		{
			if (_root == nullptr)
				return false;

			Node* cur = _root;
			Node* parent = nullptr;
			//找val的位置
			while (cur)
			{
				if (cur->_key < key)
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (cur->_key > key)
				{
					parent = cur;
					cur = cur->_left;
				}
				else
				{
					if (cur->_right == nullptr)//目标位置没有右孩子
					{
						if (cur == _root)//删除根的情况
						{
							_root = cur->_left;
							delete cur;

						}
						else
						{
							if (parent->_left == cur)
							{
								parent->_left = cur->_left;
								delete cur;
							}
							else
							{
								parent->_right = cur->_left;
								delete cur;
							}
						}

					}
					else if (cur->_left == nullptr)
					{

						if (cur == _root)//删除根的情况
						{
							_root = cur->_right;
							delete cur;

						}
						else
						{
							if (parent->_left == cur)
							{
								parent->_left = cur->_right;
								delete cur;
							}
							else
							{
								parent->_right = cur->_right;
								delete cur;
							}
						}

					}
					else
					{
						//我们这里寻找左子树的最大值
						Node* maxLeft = cur->_left;
						Node* pmaxLeft = cur;
						while (maxLeft->_right)
						{
							pmaxLeft = maxLeft;
							maxLeft = maxLeft->_right;
						}
						cur->_key = maxLeft->_key;
						if (pmaxLeft->_left == maxLeft)
						{
							pmaxLeft->_left = maxLeft->_left;
							delete maxLeft;
						}
						else if (pmaxLeft->_right == maxLeft)
						{
							pmaxLeft->_right = maxLeft->_left;
							delete maxLeft;
						}
					}
					return true;
				}
			}
			return false;
		}

		//递归删除
		bool Eraser(const K& key)
		{
			return _Eraser(_root, key);
		}
		bool _Eraser(Node*& root, const K& key)
		{
			if (root == nullptr)
			{
				return false;
			}
			if (root->_key < key)
			{
				return _Eraser(root->_right, key);
			}
			else if (root->_key > key)
			{
				return _Eraser(root->_left, key);
			}
			else
			{
				//找到要删除的位置了
				Node* cur = root;
				if (root->_right == nullptr)
				{
					/*Node* cur = root;*/
					root = root->_left;
					/*delete cur;*/
				}
				else if (root->_left == nullptr)
				{
					/*Node* cur = root;*/
					root = root->_right;
					/*delete cur;*/

				}
				else//左右子树都存在
				{
					Node* pcur = cur;
					cur = cur->_left;

					while (cur->_right)//找左树最大值
					{
						pcur = cur;
						cur = cur->_right;
					}
					root->_key = cur->_key;

					/*if (pcur->_left == cur)
					{
						pcur->_left = cur->_left;
					}
					else if (pcur->_right==cur)
					{
						pcur->_right = cur->_left;
					}*/
					return _Eraser(root->_left, root->_key);

				}
				delete cur;
				return true;
			}
			return false;
		}

		//插入,成功插入返回true,失败也就是已有所要插入的数字则返回false
		bool Insert(const K& key)
		{
			if (_root == nullptr)
			{
				_root = new Node(key);
			}
			else
			{
				Node* cur = _root;
				Node* parent = nullptr;
				while (cur)
				{
					//大于,则往右边走
					if (cur->_key < key)
					{
						parent = cur;
						cur = cur->_right;
					}
					else if (cur->_key > 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 Insertr(const K& key)
		{
			return _Insertr(_root, key);
		}
		bool _Insertr(Node*& root, const K& key)
		{
			if (root == nullptr)
			{
				root = new Node(key);
				return true;
			}

			if (root->_key < key)
			{
				return _Insertr(root->_right, key);
			}
			else if (root->_key > key)
			{
				return _Insertr(root->_left, key);
			}
			return false;
		}


		//中层遍历
		void InOrder()
		{
			_Inorder(_root);
			cout << endl;
		}
		void _Inorder(Node* root)
		{
			if (root == nullptr)
				return;

			_Inorder(root->_left);
			cout << root->_key << " ";
			_Inorder(root->_right);
		}
	private:
		Node* _root = nullptr;
	};
}

要約する

この記事では主にバイナリツリーの基本的な概念、実装、および応用シナリオについて説明します。鉄人の方に商品を受け取っていただければ幸いです。

おすすめ

転載: blog.csdn.net/zcxmjw/article/details/130249378