目次
前文
この記事は主に、二分探索木の実装と使用法を理解することを目的としています。
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;
};
}
要約する
この記事では主にバイナリツリーの基本的な概念、実装、および応用シナリオについて説明します。鉄人の方に商品を受け取っていただければ幸いです。