[C++ Grocery Store] 検索機能付きバイナリツリー

ここに画像の説明を挿入します

1. 二分探索木の概念

二分探索ツリーは二分挿入ソート ツリーとも呼ばれ、空のツリー、または次のプロパティを持つ二分ツリーのいずれかです。

  • 左のサブツリーが空でない場合、左のサブツリー上のすべてのノードの値はルート ノードの値より小さい (大きい) ことになります。

  • 右のサブツリーが空でない場合、右のサブツリー上のすべてのノードの値はルート ノードの値より大きくなります (小さくなります)。

  • その左側と右側のサブツリーもそれぞれ二分探索ツリーです。

ここに画像の説明を挿入します

2. 二分探索木の操作

2.1 二分探索木での検索

  • ルートから比較して検索し、ルートより大きい場合は右へ、ルートより小さい場合は左へ検索します。

  • 高さの検索の最大回数。空の場合でも見つからない場合は、この値が存在しないことを意味します。

ちょっとしたヒント: ここでの最大の高さを検索する時間計算量はO (log N) O(logN)ではありません。O ( l o g N )、これは比較的理想的な状況に基づいています。つまり、このバイナリ ツリーは完全なバイナリ ツリーまたは完全なバイナリ ツリーです。極端な場合、この二分木にはパスが 1 つだけあり、最大の高さでの検索の時間計算量はO ( N ) O(N)O ( N )

2.2 二分探索木への挿入

具体的な挿入手順は以下の通りです。

  • ツリーが空の場合: ノードを直接追加し、ルート ポインターに割り当てます。

  • ツリーは空ではありません。まず、二分探索ツリーのプロパティに従って挿入位置を見つけて、新しいノードを挿入します。

ここに画像の説明を挿入します

2.3 二分探索木の削除

まず、要素が二分探索木に存在するかどうかを確認し、存在しない場合は要素を返します。存在しない場合、削除されるノードは次の 4 つの状況に分けられます。

  1. 削除するノードには子ノードがありません。

  2. 削除するノードは左側の子ノードのみです。

  3. 削除するノードは右の子ノードのみです。

  4. 削除するノードには左右の子ノードがあります。

ノードの削除には 4 つのケースがあるように見えますが、実際にはケース 1 はケース 2 またはケース 3 と組み合わせることができるため、実際の削除プロセスは次のようになります。

  • 状況 1 (削除されるノードには左側の子ノードのみが存在します) : ノードを削除し、削除されたノードの親ノードが削除されたノードの左側の子ノードを指すようにします。つまり、直接削除します。

  • ケース 2 (削除するノードには右の子ノードしかない) : ノードを削除し、削除されたノードの親ノードが削除されたノードの右の子ノードを指すようにします。----- 直接削除します。

  • ケース 3 (削除されるノードには左右の子がある) : 左側のサブツリーで最大のキー コードを持つノードを見つけ、その値を削除されたノードに入力して、ノードの削除を処理します。 問題 ---・削除する置換方法。

ここに画像の説明を挿入します

3. 二分探索木の実装

2 挿入検索木は単なる構造です。本質的にはノードによってリンクされています。したがって、最初にデータを格納するために使用されるノード クラスを定義する必要があります。ノードクラスを作成したら、二分探索木クラスを定義する必要があります。このクラスは主に構造を維持し、追加、削除、変更などの機能を実装するために使用されます。構造を維持するため、このクラスのメンバー変数のみが使用されますルート ノードがあれば十分です。このルート ノードを使用すると、番号構造全体を維持および管理できます。

3.1 BinarySearchTreeNode(ノードクラス)

template <class K>
struct BinarySearchTreeNode
{
    
    
	typedef BinarySearchTreeNode<K> TNode;

	BinarySearchTreeNode(const K& val = K())
		:_val(val)
		,_left(nullptr)
		,_right(nullptr)
	{
    
    }

	K _val;
	TNode* _left;
	TNode* _right;
};

3.2 BinarySearchTree(二分探索ツリークラス)

3.2.1 フレームワーク

template <class K>
class BinarySearchTree
{
    
    
	typedef BinarySearchTreeNode<K> BSTNode;
	typedef BinarySearchTree<K> Self;
public:
	BinarySearckTree()
		:_root(nullptr)
	{
    
    }
private:
	BSTNode* _root;
};

3.2.2 挿入

非再帰バージョン:

bool insert(const K& val)
{
    
    
	if (_root == nullptr)
	{
    
    
		_root = new BSTNode(val);
		return true;
	}

	BSTNode* newnode = new BSTNode(val);
	BSTNode* cur = _root;
	BSTNode* parent = nullptr;
	while (cur)
	{
    
    
		if (val < cur->_val)
		{
    
    
			parent = cur;
			cur = cur->_left;
		}
		else if (val > cur->_val)
		{
    
    
			parent = cur;
			cur = cur->_right;
		}
		else
		{
    
    
			return false;//相等就说明树中已经有了,就应该插入失败
		}
	}

	//if (parent->_left == cur)//左右都是空,每次就走上面这个了
	if(val < parent->_val)
	{
    
    
		parent->_left = newnode;
	}
	else
	{
    
    
		parent->_right = newnode;
	}

	return true;
}

ちょっとしたヒント: ルート ノードが空の場合を個別に考慮する必要があります。を使用してcurノードを挿入する位置を見つけ、 を使用してparentその位置の親ノードをポイントし、リンク関係を実現します。最後に、親ノードの左側に挿入するか右側に挿入するかを決定する必要があります。val私たちが実装した二分探索ツリーでは、同じ値を格納するノードが二分探索ツリー内に 1 回だけ出現できる必要があるため、値を挿入するときに、ツリー内に を格納するノードがすでに存在することが検出された場合は、 を返す必要がありますvalfalse挿入が失敗したことを示します。

再帰バージョン:

//插入(递归---版本一)
private:
	bool _InsertR(BSTNode*& root, BSTNode* parent, const K& key)
	{
    
    
		if (root == nullptr)//为空说明就是在该位置插入
		{
    
    
			BSTNode* newnode = new BSTNode(key);
			if (parent != nullptr)
			{
    
    
				if (key < parent->_val)
				{
    
    
					parent->_left = newnode;
				}
				else
				{
    
    
					parent->_right = newnode;
				}
			}
			else
			{
    
    
				root = newnode;
			}
	
			return true;
		}
	
		//root不为空说明还没有找到待插入的位置,还得继续找
		if (key < root->_val)
		{
    
    
			return _InsertR(root->_left, root, key);
		}
		else if (key > root->_val)
		{
    
    
			return _InsertR(root->_right, root, key);
		}
		else
		{
    
    
			return false;
		}
	}
public:
	//插入(递归)
	bool InsertR(const K& key)
	{
    
    
		return _InsertR(_root, _root, key);
	}
//插入(递归---版本二)
private:
	bool _InsertR(BSTNode*& root, const K& key)
	{
    
    
		if (root == nullptr)//为空说明就是在该位置插入
		{
    
    
			root = new BSTNode(key);
			return true;
		}

		//root不为空说明还没有找到待插入的位置,还得继续找
		if (key < root->_val)
		{
    
    
			return _InsertR(root->_left, key);
		}
		else if (key > root->_val)
		{
    
    
			return _InsertR(root->_right, key);
		}
		else
		{
    
    
			return false;
		}
	}
public:
	//插入(递归)
	bool InsertR(const K& key)
	{
    
    
		return _InsertR(_root, key);
	}

ちょっとしたヒント: 空のツリーに挿入する場合、ルート ノードを変更する必要があります_root。つまり、ポインタを変更する必要があるため、ここでは参照ポインタまたはセカンダリ ポインタを使用する必要があります。

3.2.3 InOrder (インオーダートラバーサル)

private:
	void _InOrder(BSTNode* root) const
	{
    
    
		if (root == nullptr)
		{
    
    
			return;
		}

		_InOrder(root->_left);
		cout << root->_val << " ";
		_InOrder(root->_right);
	}
public:
	void InOrder()
	{
    
    
		_InOrder(_root);
		cout << endl;
	}

ヒント: ここでの順序トラバーサルは再帰的に実装されていますが、再帰関数にはパラメーターが必要です。バイナリ ツリー全体を順序でトラバースするには、ユーザーはルート ノードをこの関数に渡す必要がありますが、ルート_rootノード_rootはプライベートメンバー変数であり、ユーザーはアクセスできないため、ユーザーにインオーダートラバーサル関数を直接提供することはできません。正しいアプローチは、ユーザーはルート ノードにアクセスできませんが、クラス内ではアクセスできるため、クラス内に順序トラバーサルのサブ関数を実装し、この中に順序トラバーサルのロジックを実装できるということです_InOrder。サブ関数を作成し、それからInOrder呼び出すことができる順序トラバーサル用の関数インターフェイスをユーザーに提供します_InOrderこのようにして、ユーザーはインオーダートラバーサルを正常に使用できるようになります。

3.2.4 検索

非再帰バージョン:

bool find(const K& key)
{
    
    
	BSTNode* cur = _root;
	while (cur)
	{
    
    
		if (key < cur->_val)
		{
    
    
			cur = cur->_left;
		}
		else if (key > cur->_val)
		{
    
    
			cur = cur->_right;
		}
		else
		{
    
    
			return true;
		}
	}

	return false;
}

再帰バージョン:

private:
	bool _FindR(BSTNode* root, const K& key)
	{
    
    
		if (root == nullptr)
		{
    
    
			return false;
		}

		if (key < root->_val)
		{
    
    
			return _FindR(root->_left, key);
		}
		else if (key > root->_val)
		{
    
    
			return _FindR(root->_right, key);
		}
		else
		{
    
    
			return true;
		}
	}
public:
	bool FindR(const K& key)
	{
    
    
		return _FindR(_root, key);
	}

3.2.5 消去(削除)

非再帰バージョン:

bool erase(const K& key)
{
    
    
	BSTNode* cur = _root;
	BSTNode* parent = nullptr;

	//先找需要删除的结点
	while (cur)
	{
    
    
		if (key < cur->_val)
		{
    
    
			parent = cur;
			cur = cur->_left;
		}
		else if (key > cur->_val)
		{
    
    
			parent = cur;
			cur = cur->_right;
		}
		else
		{
    
    
			//到这里说明cur就是待删除的节点
			if (cur->_left == nullptr)//如果cur只有一个孩子(只有右孩子),直接托孤
			{
    
    
				if (parent == nullptr)//说明删除的是根结点
				{
    
    
					_root = _root->_right;
				}
				else if (cur == parent->_left)//判断cur是左孩子还是右孩子
				{
    
    
					parent->_left = cur->_right;
				}
				else if(cur == parent->_right)
				{
    
    
					parent->_right = cur->_right;
				}
			}
			else if(cur->_right == nullptr)//如果cur只有一个孩子(只有左孩子)
			{
    
    
				if (parent == nullptr)//说明删除的是根结点
				{
    
    
					_root = _root->_left;
				}
				else if (cur == parent->_left)//判断cur是左孩子还是右孩子
				{
    
    
					parent->_left = cur->_left;
				}
				else if (cur == parent->_right)
				{
    
    
					parent->_right = cur->_left;
				}
			}
			else//到这里说明cur有两个孩子
			{
    
    
				BSTNode* parent = cur;
				BSTNode* leftmax = cur->_left;//找到左孩子中最大的那个
				while (leftmax->_right)
				{
    
    
					parent = leftmax;
					leftmax = leftmax->_right;
				}

				swap(cur->_val, leftmax->_val);
				cur = leftmax;

				//有一种极端情况就是左边只有一条路径
				if (leftmax == parent->_left)
				{
    
    
					parent->_left = leftmax->_left;
				}
				else
				{
    
    
					parent->_right = leftmax->_left;
				}
			}

			delete cur;
			return true;
		}
	}

	return false;
}

ちょっとしたヒント: 上記のコードでは、常に、 cur削除するノードと、parent削除するノードの親、つまりcurの親を指します。削除は一般に、セクション 2.3 で説明した 3 つの状況に分類されます。ただし、ルート ノードを削除する状況、つまり がいつ削除されるかなど、注意が必要な詳細がまだいくつかありますparent == nullptrケース 1 と 2 では、削除するノードがcur親ノードの左側の子であるか右側の子であるかを判断して、その子が正しいリンク関係を確立しているparentことを確認する必要もあります。ケース 3: 削除するノードには 2 つの子があり、ここで行うことは、左側のサブツリーで最大のノードを見つけて、それを置き換えて「子」を育てやすくすることです。左側のサブツリーで最大の値を持つノードを見つけるのは簡単です。 の左側の子から開始して右端まで移動するだけです。に保存されている値を見つけた後交換します。交換後は削除対象ノードとなるため、この時点でこのノードにリダイレクトする必要があります。ノードを削除したいので、後でリンク関係を変更しやすくするために、ここでも のノードを見つける必要があります。内側で表される意味と外側で表される意味は異なります。前者は、その親ノードを表します。左側のツリーの最大のノードであり、後者はの親ノードを表します。最後に、これを実現するにはリンク関係を変更する必要があります。curparentcurleftmaxcurcurcurcurleftmaxleftmaxcurleftmaxleftmaxleftmaxparentparentparentparentcurcurcurノードを削除する場合、ここでのリンク関係には次の 2 つの状況があります。

シナリオ 1 :
ここに画像の説明を挿入します
ちょっとしたヒント: ステップ 2 の交換は、ノード内の値を交換するものであり、2 つのノードを交換するものではありません。最終的にleftmaxcur同じノードを指します。

シナリオ 2 :
ここに画像の説明を挿入します
ちょっとしたヒント: シナリオ 2 とシナリオ 1 の最大の違いは 2 か所に反映されています。まず、シナリオ 2 では、初期値を定義して代入するときにそれを手放すことはできないことを意味するparentだけです。手放すべきです。そうしないと、後でリンク関係が変更され、null ポインターにアクセスする際に問題が発生します。2 番目の違いは、リンク関係の変更にあります。ケース 2 では、の左の子が の左の子を指すようにし、ケース 1 では、右の子が の左の子を指すようにします。したがって、リンク関係を変更する場合には、状況を見て判断する必要があります。2 番目の違いにはもう 1 つの類似点があります。つまり、 の左の子であっても、の右の子であっても、最終的には の左の子を指すことになります。これはなぜですか? その理由は実は非常に単純で、の右の子は空でなければなりませんが、左の子は空ではない可能性があります。なぜ右側の子が最初に空であると確信できるのでしょうか? は左側のサブツリーの最大のノードだからです。右側の子が空でない場合は、現在のノードが最大のノードであってはいけないことを意味します。したがって、リンク関係を変更するときは、の左の子との接続を確立する必要があります。最後に、この関数は毎回ルートノードから検索を開始し、交換後のツリーは条件を満たさないため、交換後のノードは再帰呼び出しではなくリンク関係を変更することによってのみ削除できることに注意してください検索木の構造は、ケース 1 を例にとると、8 を格納しているノードを見つけることができません。curparentparent = nullptrparent = curparentleftmaxparentleftmaxparentparentleftmaxleftmaxleftmaxleftmaxparentleftmaxcur

再帰バージョン:

private:
//删除递归版
	bool _eraseR(BSTNode*& root, const K& key)
	{
    
    
		if (root == nullptr)
		{
    
    
			return false;
		}

		if (key < root->_val)
		{
    
    
			return _eraseR(root->_left, key);
		}
		else if (key > root->_val)
		{
    
    
			return _eraseR(root->_right, key);
		}
		else
		{
    
    
			//相等了,需要进行删除了
			BSTNode* del = root;

			if (root->_left == nullptr)//左为空
			{
    
    
				root = root->_right;
			}
			else if (root->_right == nullptr)//右为空
			{
    
    
				root = root->_left;
			}
			else//左右都不为空
			{
    
    
				BSTNode* parent = root;
				BSTNode* leftmax = root->_left;//找到左孩子中最大的那个
				while (leftmax->_right)
				{
    
    
					parent = leftmax;
					leftmax = leftmax->_right;
				}
				swap(root->_val, leftmax->_val);
				
				return _eraseR(root->_left, key);
			}
			
			delete del;
			del = nullptr;
			return true;
		}
	}
public:
	//删除递归版
	bool eraseR(const K& key)
	{
    
    
		return _eraseR(_root, key);
	}

ちょっとしたヒント: 交換後、ツリー全体は二分探索木の構造を満たさないかもしれませんが、root交換するのはrootノード_valと左のサブツリーであるため、ノードの左のサブツリーは二分探索木を満たす必要があります。_val, そしてrootノードは_val左のサブツリーの最大のものより_val大きくなければなりません, したがって交換後root, 左のサブツリーはまだ二分探索木の構造を満たしています. このとき、それを再帰的に呼び出すことができます. 削除するrootノードを見つけますの左側のサブツリーにあり、削除されるノードは交換後に状況 1 または状況 2 のいずれかになる必要があります。root再帰バージョンのケース 1 とケース 2 の処理は、が参照であるため、はるかに単純です。rootの子の 1 つが空であることが判明した場合は、 の別の子をそれに割り当てます。 をroot割り当てる前にroot値を保存することを忘れないでください。この値が指すノードは削除されるノードです。削除する前にこの値を保存してください。deleteそうしないと、割り当て後にノードを指すポインタがなくなり、このノードのスペース リソースを解放する方法がなくなります。メモリリークが発生します。非再帰で参照が使用された場合でも、これは実行できません。非再帰では、参照は常に関数スタック フレーム内にあり、参照はそのポインタを変更できないためです。しかし、再帰は異なり、各再帰呼び出しは新しい関数スタック フレームを開き、各関数スタック フレームはroot異なるノードのエイリアスになります。

3.2.6 ~BinarySearchTree (破壊)

private:
	//析构子函数
	void Destruction(BSTNode*& root)
	{
    
    
		if (root == nullptr)
		{
    
    
			return;
		}

		//先去释放左孩子和右孩子的空间资源
		Destruction(root->_left);
		Destruction(root->_right);

		//再去释放root自己的空间资源
		delete root;
		root = nullptr;//形参root如果不加引用,这里置空是没有任何意义的,因为不加引用这里仅仅是一份拷贝

		return;
	}
public:
	//析构函数
	~BinarySearckTree()
	{
    
    
		Destruction(_root);
	}

3.2.7 BinarySearchTree(const Self&tree) (コピー構築)

コピー コンストラクターは、insert を直接呼び出すことができないことに注意してください。データの挿入順序が異なるため、ツリーの最終的な構造も異なります。最終的にはバイナリ ツリーの構造に準拠しますが、コピーされたツリーとは依然として異なります。正しいアプローチは、事前順序トラバーサルを実行することです。ツリーのノードをトラバースするときは、新しいノードに移動して同じ値を保存します。

書き方1

//拷贝构造函数的子函数
private:
	void Construct(BSTNode*& root, BSTNode* copy)
	{
    
    
		if (copy == nullptr)
		{
    
    
			root = nullptr;
			return;
		}

		root = new BSTNode(copy->_val);//通过引用直接来实现链接关系
		Construct(root->_left, copy->_left);
		Construct(root->_right, copy->_right);
	}
public:
	//拷贝构造
	BinarySearchTree(const Self& tree)
		:_root(nullptr)
	{
    
    
		Construct(_root, tree._root);
	}

書き方2

private:
//拷贝构造子函数(写法二)
	BSTNode* Construct(BSTNode* root)
	{
    
    
		if (root == nullptr)
		{
    
    
			return nullptr;
		}

		BSTNode* newnode = new BSTNode(root->_val);
		newnode->_left = Construct(root->_left);//通过返回值来实现链接关系
		newnode->_right = Construct(root->_right);

		return newnode;
	}
public:
	//拷贝构造(写法二)
	BinarySearchTree(const Self& tree)
	{
    
    
		_root = Construct(tree._root);
	}

ヒント: 上記 2 つの書き方の違いは、1 つ目は参照によってリンク関係を実現する方法、2 つ目は戻り値によってリンク関係を実現する方法です。

3.2.8 Operator= (代入演算子のオーバーロード)

public:
//赋值运算符重载(现代写法)
	Self& operator=(Self tree)
	{
    
    
		swap(_root, tree._root);//交换两颗搜索二叉树就是交换它们里面维护的根节点

		return *this;
	}

4. 二分探索木の応用

4.1Kモデル

K モデルでは、キー コードとして Key が 1 つだけあり、構造体に格納する必要があるのは Key だけであり、検索する必要がある値はキー コードです。例: 単語 word が与えられた場合、その単語のスペルが正しいかどうかを判断します。具体的な方法は次のとおりです。

  • 語彙内のすべての単語のセット内の各単語をキーとして使用して、二分探索木を構築します。

  • 単語が二分探索木に存在するかどうかを取得します。存在する場合、そのスペルは正しいです。存在しない場合、そのスペルは間違っています。

上で作成した二分探索ツリーは Key モデルです。このツリーのノードには 1 つの値しか格納できず、この値が Key であるためです。

4.2KVモデル

KV モデルは、各キー コード Key に対応する値 Value、つまり <Key, Value> のキーと値のペアがあることを意味します。この方法は実生活でも非常に一般的です。

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

  • 別の例は、単語の数をカウントすることです。カウントが成功すると、特定の単語の出現数がすぐにわかります。単語とその出現数は、キーと値のペアを形成する <word, count> です。

4.2.1 KVモデルハンドティアリング

#pragma once

namespace K_V
{
    
    
	template <class K, class V>
	struct BinarySearchTreeNode
	{
    
    
		typedef BinarySearchTreeNode<K, V> TNode;

		BinarySearchTreeNode(const K& key = K(), const V& val = V())
			:_key(key)
			, _val(val)
			, _left(nullptr)
			, _right(nullptr)
		{
    
    }

		K _key;
		V _val;
		TNode* _left;
		TNode* _right;
	};

	template <class K, class V>
	class BinarySearchTree
	{
    
    
		typedef BinarySearchTreeNode<K, V> BSTNode;
		typedef BinarySearchTree<K, V> Self;
	private:
		void _InOrder(BSTNode* root) const
		{
    
    
			if (root == nullptr)
			{
    
    
				return;
			}

			_InOrder(root->_left);
			cout << root->_key << "--" << root->_val << endl;
			_InOrder(root->_right);
		}

		BSTNode* _FindR(BSTNode* root, const K& key)//KV模型中的Key不能被修改,但是Val可以被修改
		{
    
    
			if (root == nullptr)
			{
    
    
				return nullptr;
			}

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

		//插入(递归---版本一)
		//bool _InsertR(BSTNode*& root, BSTNode* parent, const K& key)
		//{
    
    
		//	if (root == nullptr)//为空说明就是在该位置插入
		//	{
    
    
		//		BSTNode* newnode = new BSTNode(key);
		//		if (parent != nullptr)
		//		{
    
    
		//			if (key < parent->_key)
		//			{
    
    
		//				parent->_left = newnode;
		//			}
		//			else
		//			{
    
    
		//				parent->_right = newnode;
		//			}
		//		}
		//		else
		//		{
    
    
		//			root = newnode;
		//		}

		//		return true;
		//	}

		//	//root不为空说明还没有找到待插入的位置,还得继续找
		//	if (key < root->_key)
		//	{
    
    
		//		return _InsertR(root->_left, root, key);
		//	}
		//	else if (key > root->_key)
		//	{
    
    
		//		return _InsertR(root->_right, root, key);
		//	}
		//	else
		//	{
    
    
		//		return false;
		//	}
		//}

		//插入(递归---版本二)
		bool _InsertR(BSTNode*& root, const K& key, const V& val)
		{
    
    
			if (root == nullptr)//为空说明就是在该位置插入
			{
    
    
				root = new BSTNode(key, val);
				return true;
			}

			//root不为空说明还没有找到待插入的位置,还得继续找
			if (key < root->_key)
			{
    
    
				return _InsertR(root->_left, key, val);
			}
			else if (key > root->_key)
			{
    
    
				return _InsertR(root->_right, key, val);
			}
			else
			{
    
    
				return false;
			}
		}

		//删除递归版
		bool _eraseR(BSTNode*& root, const K& key)
		{
    
    
			if (root == nullptr)
			{
    
    
				return false;
			}

			if (key < root->_key)
			{
    
    
				return _eraseR(root->_left, key);
			}
			else if (key > root->_key)
			{
    
    
				return _eraseR(root->_right, key);
			}
			else
			{
    
    
				//相等了,需要进行删除了
				BSTNode* del = root;

				if (root->_left == nullptr)//左为空
				{
    
    
					root = root->_right;
				}
				else if (root->_right == nullptr)//右为空
				{
    
    
					root = root->_left;
				}
				else//左右都不为空
				{
    
    
					BSTNode* parent = root;
					BSTNode* leftmax = root->_left;//找到左孩子中最大的那个
					while (leftmax->_right)
					{
    
    
						parent = leftmax;
						leftmax = leftmax->_right;
					}
					std::swap(root->_key, leftmax->_key);

					return _eraseR(root->_left, key);
				}

				delete del;
				del = nullptr;
				return true;
			}
		}

		//析构子函数
		void Destruction(BSTNode*& root)
		{
    
    
			if (root == nullptr)
			{
    
    
				return;
			}

			//先去释放左孩子和右孩子的空间资源
			Destruction(root->_left);
			Destruction(root->_right);

			//再去释放root自己的空间资源
			delete root;
			root = nullptr;//形参root如果不加引用,这里置空是没有任何意义的,因为不加引用这里仅仅是一份拷贝

			return;
		}

		//拷贝构造函数的子函数(写法一)
		void Construct(BSTNode*& root, BSTNode* copy)
		{
    
    
			if (copy == nullptr)
			{
    
    
				root = nullptr;
				return;
			}

			root = new BSTNode(copy->_key);
			Construct(root->_left, copy->_left);
			Construct(root->_right, copy->_right);
		}

		//拷贝构造子函数(写法二)
		BSTNode* Construct(BSTNode* root)
		{
    
    
			if (root == nullptr)
			{
    
    
				return nullptr;
			}

			BSTNode* newnode = new BSTNode(root->_key);
			newnode->_left = Construct(root->_left);
			newnode->_right = Construct(root->_right);

			return newnode;
		}

	public:
		BinarySearchTree()
			:_root(nullptr)
		{
    
    }

		//拷贝构造(写法一)
		/*BinarySearchTree(const Self& tree)
			:_root(nullptr)
		{
			Construct(_root, tree._root);
		}*/

		//拷贝构造(写法二)
		BinarySearchTree(const Self& tree)
		{
    
    
			_root = Construct(tree._root);
		}

		//赋值运算符重载(现代写法)
		Self& operator=(Self tree)
		{
    
    
			swap(_root, tree._root);//交换两颗搜索二叉树就是交换它们里面维护的根节点

			return *this;
		}

		//插入(非递归)
		bool insert(const K& key, const V& val)
		{
    
    
			if (_root == nullptr)
			{
    
    
				_root = new BSTNode(key, val);
				return true;
			}

			BSTNode* newnode = new BSTNode(key, val);
			BSTNode* cur = _root;
			BSTNode* parent = nullptr;
			while (cur)
			{
    
    
				if (key < cur->_key)
				{
    
    
					parent = cur;
					cur = cur->_left;
				}
				else if (key > cur->_key)
				{
    
    
					parent = cur;
					cur = cur->_right;
				}
				else
				{
    
    
					return false;//相等就说明树中已经有了,就应该插入失败
				}
			}

			//if (parent->_left == cur)//左右都是空,每次就走上面这个了
			if (key < parent->_key)
			{
    
    
				parent->_left = newnode;
			}
			else
			{
    
    
				parent->_right = newnode;
			}

			return true;
		}

		//插入(递归)
		bool InsertR(const K& key, const V& val)
		{
    
    
			return _InsertR(_root, key, val);
		}

		//中序遍历
		void InOrder()
		{
    
    
			_InOrder(_root);
			cout << endl;
		}

		//查找(非递归)
		BSTNode* find(const K& key)
		{
    
    
			BSTNode* cur = _root;
			while (cur)
			{
    
    
				if (key < cur->_key)
				{
    
    
					cur = cur->_left;
				}
				else if (key > cur->_key)
				{
    
    
					cur = cur->_right;
				}
				else
				{
    
    
					return cur;
				}
			}

			return nullptr;
		}

		//查找(递归)
		BSTNode* FindR(const K& key)
		{
    
    
			return _FindR(_root, key);
		}

		//删除(非递归)
		bool erase(const K& key)
		{
    
    
			BSTNode* cur = _root;
			BSTNode* parent = nullptr;

			//先找需要删除的结点
			while (cur)
			{
    
    
				if (key < cur->_key)
				{
    
    
					parent = cur;
					cur = cur->_left;
				}
				else if (key > cur->_key)
				{
    
    
					parent = cur;
					cur = cur->_right;
				}
				else
				{
    
    
					//到这里说明cur就是待删除的节点
					if (cur->_left == nullptr)//如果cur只有一个孩子(只有右孩子),直接托孤
					{
    
    
						if (parent == nullptr)
						{
    
    
							_root = _root->_right;
						}
						else if (cur == parent->_left)//判断cur是左孩子还是右孩子
						{
    
    
							parent->_left = cur->_right;
						}
						else if (cur == parent->_right)
						{
    
    
							parent->_right = cur->_right;
						}
					}
					else if (cur->_right == nullptr)//如果cur只有一个孩子(只有左孩子)
					{
    
    
						if (parent == nullptr)
						{
    
    
							_root = _root->_left;
						}
						else if (cur == parent->_left)//判断cur是左孩子还是右孩子
						{
    
    
							parent->_left = cur->_left;
						}
						else if (cur == parent->_right)
						{
    
    
							parent->_right = cur->_left;
						}
					}
					else//到这里说明cur有两个孩子
					{
    
    
						BSTNode* parent = cur;
						BSTNode* leftmax = cur->_left;//找到左孩子中最大的那个
						while (leftmax->_right)
						{
    
    
							parent = leftmax;
							leftmax = leftmax->_right;
						}

						std::swap(cur->_key, leftmax->_key);
						cur = leftmax;

						//有一种极端情况就是左边只有一条路径
						if (leftmax == parent->_left)
						{
    
    
							parent->_left = leftmax->_left;
						}
						else
						{
    
    
							parent->_right = leftmax->_left;
						}
					}

					delete cur;
					return true;
				}
			}

			return false;
		}

		//删除递归版
		bool eraseR(const K& key)
		{
    
    
			return _eraseR(_root, key);
		}

		//析构函数
		~BinarySearchTree()
		{
    
    
			Destruction(_root);
		}

	private:
		BSTNode* _root = nullptr;
	};
}

void TestBSTree4()
{
    
    
	// 统计水果出现的次数
	string arr[] = {
    
     "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜","苹果", "香蕉", "苹果", "香蕉" };
	K_V::BinarySearchTree<string, int> countTree;
	for (const auto& str : arr)
	{
    
    
		// 先查找水果在不在搜索树中
		// 1、不在,说明水果第一次出现,则插入<水果, 1>
		// 2、在,则查找到的节点中水果对应的次数++
		//BSTreeNode<string, int>* ret = countTree.Find(str);
		auto ret = countTree.FindR(str);
		if (ret == NULL)
		{
    
    
			countTree.insert(str, 1);
		}
		else
		{
    
    
			ret->_val++;
		}
	}
	countTree.InOrder();
}

ここに画像の説明を挿入します
ちょっとしたヒント: KV モデルになりましたが、依然として二分探索木であるため、木全体の構造は変わりません。唯一の変更はツリーのノードにあります。KV モデルの場合、ツリー内のノードはキーだけでなく値も格納する必要があります。これにより、キーだけでなく、キーに関連するキーも挿入されます。キーに対応する値。次に、KV モデルの場合、Key の変更は許可されていませんが、Value の変更は可能であるため、KV モデルの場合、後続の操作を容易にするために、Find 時にノードのポインタを返す必要があります。

5. 二分探索木の性能解析

挿入操作と削除操作の両方を最初に検索する必要があり、検索効率は二分探索ツリー内の各操作のパフォーマンスを表します。n 個のノードを持つ二分探索ツリーの場合、各要素の検索確率が等しい場合、二分探索ツリーの平均探索長は二分探索ツリー内のノードの深さの関数になります。つまり、ノードが深ければ深いほど、ノードが多ければ多いほど、回数も多くなります。ただし、同じキーセットでも、各キーを異なる順序で挿入すると、異なる構造の二分探索木が得られる場合があります。

ここに画像の説明を挿入します

  • 最適な状況下では、二分探索木は完全な二分木 (または完全な二分木に近い) であり、その平均比較数は次のとおりです: log 2 n log2^nl o g 2

  • 最悪の場合: 二分探索木は単一分岐ツリー (または単一分岐ツリーに類似) に縮退し、その平均比較数は次のようになります: N 2 \frac{N}{2}2N単一分岐木に縮退すると、二分探索木の性能が失われます。現時点では、今後リリースされる AVL ツリーと赤黒ツリーを使用する必要があります。

6. 結論

本日のシェアはここまでです!記事が良いと思ったら3回応援してください .春連のホームページには面白い記事がたくさんあります. お友達のコメントも大歓迎です. あなたの応援が春連の前進の原動力です!

ここに画像の説明を挿入します

おすすめ

転載: blog.csdn.net/weixin_63115236/article/details/133243323