ホームページ >バックエンド開発 >Python チュートリアル >Python の二分探索ツリーの詳細な紹介 (コード例)
この記事では、Python の二分探索木について詳しく紹介 (コード例) しています。一定の参考価値があります。困っている友人は参考にしてください。お役に立てれば幸いです。
1. 二分探索ツリー
**二分探索ツリー (BST 二分探索) Tree)** は特殊なバイナリ ツリーです。任意のノードの値は、左の子の値より大きく、右の子の値以下です。BST (バイナリ) が順番に走査されます。 Search Tree) はソートされた要素のセットを取得でき、挿入と削除の消費時間も比較的妥当ですが、メモリのオーバーヘッドが少し大きいことが欠点です。
二分探索ツリーのプロパティ
1. 任意のノード x について、その左側のサブツリーのキーは x.key より大きくなく、右側のサブツリーのキーは x より小さくありません。 。鍵。 ###2、異なる二分探索ツリーが同じ値のセットを表すことができます。
3. 二分探索木の基本演算は木の高さに比例するため、完全な二分木であれば最悪の実行時間は Θ(lgn) ですが、次の線形木であればn ノードの場合、最悪の実行時間は Θ(n) になります。 ###4、ルートノードは、親ポインタがNILノードを指す唯一のノードである。
5. 各ノードには少なくとも 4 つの属性 (キー、左、右、親) が含まれています。二分探索木を構築するときは、キーの比較アルゴリズムが必要です。
1.
class TreeNode: def __init__(self,key,val,left=None,right=None,parent=None): self.key = key self.payload = val self.leftChild = left self.rightChild = right self.parent = parent class BinarySearchTree: def __init__(self): self.root = None self.size = 0
2. 挿入
挿入する場合、常にリーフ ノードとしてツリーに挿入されます。
二分探索木を順番に構築するシーケンスが与えられると、二分探索木は一意になります。このツリーのすべてのキーが一致する場合、二分探索木は一意になります。シーケンスが使用される 単語は、一般に一意ではない、可能なバイナリ ツリー (任意の順序で配置) を構築するために使用されます。
def put(self,key,val): if self.root: self._put(key,val,self.root) else: self.root = TreeNode(key,val) self.size = self.size + 1 def _put(self,key,val,currentNode): if key < currentNode.key: if currentNode.hasLeftChild(): self._put(key,val,currentNode.leftChild) else: currentNode.leftChild = TreeNode(key,val,parent=currentNode) else: if currentNode.hasRightChild(): self._put(key,val,currentNode.rightChild) else: currentNode.rightChild = TreeNode(key,val,parent=currentNode)
3. Find
二分探索木が空でない場合:
def get(self,key): if self.root: res = self._get(key,self.root) if res: return res.payload else: return None else: return None def _get(self,key,currentNode): if not currentNode: return None elif currentNode.key == key: return currentNode elif key < currentNode.key: return self._get(key,currentNode.leftChild) else: return self._get(key,currentNode.rightChild) def __getitem__(self,key): return self.get(key)4。削除
def delete(self,key): if self.size > 1: nodeToRemove = self._get(key,self.root) if nodeToRemove: self.remove(nodeToRemove) self.size = self.size-1 else: raise KeyError('Error, key not in tree') elif self.size == 1 and self.root.key == key: self.root = None self.size = self.size - 1 else: raise KeyError('Error, key not in tree') def __delitem__(self,key): self.delete(key)
#削除するノードには子ノードが 1 つだけあります。
第一种情况:
如果当前节点没有子节点,我们需要做的是删除节点并删除对父节点中该节点的引用。
第二种情况:
如果一个节点只有一个孩子,那么我们可以简单地促进孩子取代其父。此案例的代码展示在下一个列表中。当你看这个代码,你会看到有六种情况要考虑。由于这些情况相对于左孩子或右孩子对称,我们将仅讨论当前节点具有左孩子的情况。决策如下:
如果当前节点是左子节点,则我们只需要更新左子节点的父引用以指向当前节点的父节点,然后更新父节点的左子节点引用以指向当前节点的左子节点。
如果当前节点是右子节点,则我们只需要更新左子节点的父引用以指向当前节点的父节点,然后更新父节点的右子节点引用以指向当前节点的左子节点。
如果当前节点没有父级,则它是根。在这种情况下,我们将通过在根上调用replaceNodeData 方法来替换 key,payload,leftChild 和 rightChild 数据。
第三种情况:
最难处理的情况。 如果一个节点有两个孩子,那么我们不太可能简单地提升其中一个节点来占据节点的位置。 然而,我们可以在树中搜索可用于替换被调度删除的节点的节点。 我们需要的是一个节点,它将保留现有的左和右子树的二叉搜索树关系。 执行此操作的节点是树中具有次最大键的节点。 我们将这个节点称为后继节点,我们将看一种方法来很快找到后继节点。 继承节点保证没有多于一个孩子,所以我们知道使用已经实现的两种情况删除它。 一旦删除了后继,我们只需将它放在树中,代替要删除的节点。
找到后继的代码如下所示,是 TreeNode 类的一个方法。此代码利用二叉搜索树的相同属性,采用中序遍历从最小到最大打印树中的节点。在寻找接班人时,有三种情况需要考虑:
如果节点有右子节点,则后继节点是右子树中的最小的键。
如果节点没有右子节点并且是父节点的左子节点,则父节点是后继节点。
如果节点是其父节点的右子节点,并且它本身没有右子节点,则此节点的后继节点是其父节点的后继节点,不包括此节点。
def findSuccessor(self): succ = None if self.hasRightChild(): succ = self.rightChild.findMin() else: if self.parent: if self.isLeftChild(): succ = self.parent else: self.parent.rightChild = None succ = self.parent.findSuccessor() self.parent.rightChild = self return succ def findMin(self): current = self while current.hasLeftChild(): current = current.leftChild return current def spliceOut(self): if self.isLeaf(): if self.isLeftChild(): self.parent.leftChild = None else: self.parent.rightChild = None elif self.hasAnyChildren(): if self.hasLeftChild(): if self.isLeftChild(): self.parent.leftChild = self.leftChild else: self.parent.rightChild = self.leftChild self.leftChild.parent = self.parent else: if self.isLeftChild(): self.parent.leftChild = self.rightChild else: self.parent.rightChild = self.rightChild self.rightChild.parent = self.parent
二叉搜索树的深度越小,那么搜索所需要的运算时间越小。一个深度为log(n)的二叉搜索树,搜索算法的时间复杂度也是log(n)。然而,我们在二叉搜索树中已经实现的插入和删除操作并不能让保持log(n)的深度。如果我们按照8,7,6,5,4,3,2,1的顺序插入节点,那么就是一个深度为n的二叉树。那么,搜索算法的时间复杂度为n。
在上一节中我们讨论了建立一个二叉搜索树。我们知道,当树变得不平衡时get和put操作会使二叉搜索树的性能降低到O(n)。
如何减少树的深度呢?
一种想法是先填满一层,再去填充下一层,这样就是一个完全二叉树(complete binary tree)。这样的二叉树实现插入算法会比较复杂。另一种比较容易实现的树状数据结构——AVL树,其搜索算法复杂度为log(n)。
在这一节中我们将看到一种特殊的二叉搜索树,它可以自动进行调整,以确保树随时都保持平衡。这种树被称为AVL树,命名源于其发明者:G.M. Adelson-Velskii 和 E.M. Landis。
AVL ツリーの実装 Map 抽象データ型は通常の二分探索ツリーとまったく同じで、唯一の違いはツリーの実行方法です。 AVL ツリーを実装するには、ツリー内の各ノードの バランス係数 を追跡する必要があります。これは、各ノードの左右のサブツリーの高さを調べることによって行われます。より正式には、ノードのバランス係数を、左側のサブツリーの高さと右側のサブツリーの高さの差 として定義します。
balanceFacto#r##=##height(leftSubTree)##−height(rightSub# ##木######)################## ###上記のバランス係数の定義を使用すると、バランス係数がゼロより大きい場合、サブツリーは左に重いと言えます。バランス係数がゼロより小さい場合、サブツリーは右重みになります。バランス係数がゼロの場合、ツリーは完全にバランスが取れています。 AVL ツリーを実装し、バランスの取れたツリーの利点を得るには、バランス係数が -1、0、または 1 の場合にツリーのバランスを定義します。ツリー内のノードのバランス係数がこの範囲外になると、ツリーのバランスを戻す手順が必要になります。 高さ h の木のノード数 (Nh) は次のとおりです: Nh=1 Nh−1 Nh−2 この式はフィボナッチ数列に非常に似ています。この式を使用して、ツリー内のノードの数から AVL ツリーの高さを推定できます。フィボナッチ数列の数がどんどん大きくなるにつれて、Fi / Fi−1 は黄金比 Φ にどんどん近づきます。いつでも、AVL ツリーの高さは、AVL ツリーの高さの対数である定数に等しくなります。ツリー内のノード数 (1.44) 倍。検索が O(logN) に制限されるため、これは AVL ツリーの検索にとって朗報です。 2. 実装新しいノードを追加すると二分木のバランスが崩れるため、回転により修正する必要があります。 単一回転正しい右回転を実行するには、次のことを行う必要があります: 左ノードを作成します。サブツリーのルートになります。
注: 新しいルートは古いルートの左ノードであるため、移動後の古いルートの左ノードは空でなければなりません。この時点で、移動した古いルートに左側のノードを直接追加できます。
同様に、左回転を実行するには、次のことを行う必要があります:
右のノード (B) をサブツリーのルートにします。
古いルート ノード (A) を新しいルートの左側のノードに移動します。
新しいルート (B) にもともと左ノードがある場合、元の B の左ノードを新しいルートの左ノード (A) の右ノードにします。
図に示すように、子ノードが 2 ではなく 4 の場合、単一回転を試してみると、問題が解決できないことがわかります。
この問題を解決するには、次のルールを使用する必要があります:
バランスをとるためにサブツリーを左に回転する必要がある場合は、まず右側のノードのバランス係数を確認します。右のノードが左に多い場合、右のノードが右に回転されてから、元のノードが左に回転されます。
サブツリーのバランスをとるためにサブツリーを右に回転する必要がある場合は、まず左側のノードのバランス係数を確認します。左側のノードが右寄りの場合、左側のノードが左に回転されてから、元のノードが右に回転されます。
現時点での解決策は二重回転です。
二重回転は実際には 2 回の単一回転です。ルート ノード 4 を持つサブツリーは最初に左への 1 回転を実行し、次にルート ノード 5 を持つサブツリーは右方向への 1 回転を実行します。これにより、ツリーの ACL プロパティが復元されます。
AVL ツリーの場合、新しいノードが追加されると、AVL ツリーのプロパティは 1 回の回転を通じて常に復元できることが証明できます。
新しいノードを挿入すると、どこが回転するのでしょうか?単一回転または二重回転を使用する必要がありますか?
次の基本手順に従います。
二分探索ツリー法に従ってノードを追加し、新しいノード名はリーフノードです。
新しいノードから開始して、最初の不平衡ノード (5) まで遡ります。
(ルート ノードまで遡って不均衡なノードが存在しない場合は、ツリーがすでに AVL プロパティに準拠していることを意味します。)
(2 つのサブツリーの深さを等しくすることはできません。深さが深いサブツリーには新しいノードが含まれます。)
赤黒ツリーは、非常に興味深いバランスの取れた検索ツリーです。赤黒ツリーはバランス二分木 (AVL-tree) よりも統計的なパフォーマンスが優れているため、多くの場所で使用されます。 C STL では、多くの部分 (現在
set、multiset、map、multimap を含む) で赤黒ツリーのバリエーションが適用されます (SGI STL の赤黒ツリーにはいくつかの変更があり、これらの変更により、パフォーマンスが向上するだけでなく、セット操作もサポートされます)。 赤黒ツリーの主なルール:
赤黒ツリーの主なルールは次のとおりです:
赤黒ツリーに挿入されたノードはすべて赤です。赤いノードを挿入する方が黒のノードを挿入するよりも赤黒ルールに違反する可能性が低いため、これは偶然ではありません。その理由は、黒のノードを挿入すると常に黒の高さが変更される (ルール 4 に違反) が、赤のノードを挿入するとルール 3 に違反する可能性は半分しかないためです。さらに、ルール 3 の違反は、ルール 4 の違反よりも修正が簡単です。
新規ノードを挿入するとこのバランスが崩れる場合がありますが、赤黒ツリーでは主にノードの色の変更、左回転、右回転の3つの方法でバランスを修正します。 (具体的なルールは省略)
各赤黒ツリーも特殊な二分探索木であるため、赤黒ツリーに対する読み取り専用操作は通常の二分探索木に対する読み取り専用操作と同じです。ただし、赤黒ツリーに対する挿入および削除操作は、赤黒ツリーのプロパティに準拠しなくなります。赤黒ツリーのプロパティを復元するには、わずかな (O(logn)) 色の変更 (実際には非常に高速) と 3 回以内のツリー回転 (挿入操作の場合は 2 回) が必要です。挿入と削除は複雑ですが、操作時間は O(logn) 回に抑えることができます。
n 個の内部ノードを持つ赤黒ツリーの高さは、最大 2lg(n 1) です。
以上がPython の二分探索ツリーの詳細な紹介 (コード例)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。