私は最近、さまざまなバイナリ検索ツリーの実装(AVL、splay、treap)をコーディングしており、これらの構造をトラバースするイテレータを作成する特に「良い」方法があるかどうかに興味があります。現在使用しているソリューションは、BSTの各ノードにツリー内の次の要素と前の要素へのポインターを格納させることです。これにより、反復が標準のリンクリスト反復に削減されます。しかし、私はこの答えに本当に満足していません。 2つのポインター(次と前)によって各ノードのスペース使用量が増加し、ある意味ではごまかしにすぎません。
スタックを使用してフロンティアを追跡することにより、O(h)補助記憶スペース(hはツリーの高さ))を使用するバイナリ検索ツリーイテレータを構築する方法を知っています。ノードは後で調査するために使用しますが、メモリ使用量のためにこれをコーディングすることには抵抗しました。一定のスペースのみを使用するイテレータを構築する方法があることを望んでいました。
私の質問はこれです-次のプロパティを持つバイナリ検索ツリーの反復子を設計する方法はありますか?
next()
およびhasNext()
クエリは、O(1)時間で実行されます。簡単にするために、反復中にツリー構造の形状が変化しない(つまり、挿入、削除、または回転がない)と仮定すれば問題ありませんが、実際にこれを処理できるソリューションがあれば本当にクールです。
最も単純なイテレータは、最後に見たキーを保存し、次の繰り返しで、そのキーの最小の上限をツリーで検索します。反復はO(log n)です。これには、非常に単純であるという利点があります。キーが小さい場合、反復子も小さくなります。もちろん、ツリーを反復処理する方法が比較的遅いという欠点があります。また、一意でないシーケンスでは機能しません。
一部のツリーでは、スキャンが非常に高速であることが特定の用途にとって重要であるため、既に使用している実装をそのまま使用します。各ノードのキーの数が多い場合、兄弟ポインターを格納することのペナルティはそれほど面倒ではありません。ほとんどのBツリーはこのメソッドを使用します。
多くの検索ツリー実装は、他の操作を簡素化するために各ノードに親ポインターを保持します。それがある場合は、最後に見たノードへの単純なポインターをイテレーターの状態として使用できます。各反復で、最後に表示されたノードの親で次の子を探します。兄弟がいない場合は、さらに1レベル上に移動します。
これらの手法のいずれも適切でない場合は、イテレーターに保管されているノードのスタックを使用できます。これは、通常どおり検索ツリーを反復処理するときの関数呼び出しスタックと同じ機能を提供しますが、兄弟をループして子で再帰する代わりに、子をスタックにプッシュし、連続する各兄弟を返します。
TokenMacGuyが述べたように、イテレータに保存されたスタックを使用できます。これは、Javaでこれを簡単にテストした実装です。
/**
* An iterator that iterates through a tree using in-order tree traversal
* allowing a sorted sequence.
*
*/
public class Iterator {
private Stack<Node> stack = new Stack<>();
private Node current;
private Iterator(Node argRoot) {
current = argRoot;
}
public Node next() {
while (current != null) {
stack.Push(current);
current = current.left;
}
current = stack.pop();
Node node = current;
current = current.right;
return node;
}
public boolean hasNext() {
return (!stack.isEmpty() || current != null);
}
public static Iterator iterator(Node root) {
return new Iterator(root);
}
}
他のバリエーションは、構築時にツリーをトラバースし、トラバースをリストに保存することです。後でリストイテレータを使用できます。
これは古いことはわかっていますが、しばらく前にマイクロソフトとのインタビューでこれを尋ねられたので、少し作業することにしました。私はこれをテストしましたが、非常にうまく機能します。
template <typename E>
class BSTIterator
{
BSTNode<E> * m_curNode;
std::stack<BSTNode<E>*> m_recurseIter;
public:
BSTIterator( BSTNode<E> * binTree )
{
BSTNode<E>* root = binTree;
while(root != NULL)
{
m_recurseIter.Push(root);
root = root->GetLeft();
}
if(m_recurseIter.size() > 0)
{
m_curNode = m_recurseIter.top();
m_recurseIter.pop();
}
else
m_curNode = NULL;
}
BSTNode<E> & operator*() { return *m_curNode; }
bool operator==(const BSTIterator<E>& other)
{
return m_curNode == other.m_curNode;
}
bool operator!=(const BSTIterator<E>& other)
{
return !(*this == other);
}
BSTIterator<E> & operator++()
{
if(m_curNode->GetRight())
{
m_recurseIter.Push(m_curNode->GetRight());
if(m_curNode->GetRight()->GetLeft())
m_recurseIter.Push(m_curNode->GetRight()->GetLeft());
}
if( m_recurseIter.size() == 0)
{
m_curNode = NULL;
return *this;
}
m_curNode = m_recurseIter.top();
m_recurseIter.pop();
return *this;
}
BSTIterator<E> operator++ ( int )
{
BSTIterator<E> cpy = *this;
if(m_curNode->GetRight())
{
m_recurseIter.Push(m_curNode->GetRight());
if(m_curNode->GetRight()->GetLeft())
m_recurseIter.Push(m_curNode->GetRight()->GetLeft());
}
if( m_recurseIter.size() == 0)
{
m_curNode = NULL;
return *this;
}
m_curNode = m_recurseIter.top();
m_recurseIter.pop();
return cpy;
}
};
ツリートラバーサル 、ウィキペディアから:
すべてのサンプル実装には、ツリーの高さに比例したコールスタックスペースが必要です。バランスの悪いツリーでは、これは非常に重要です。
各ノードで親ポインターを維持するか、ツリーをスレッド化することにより、スタック要件を削除できます。スレッドを使用する場合、これは大幅に改善された順序トラバーサルを可能にしますが、前順序および後順序トラバーサルに必要な親ノードの取得は、単純なスタックベースのアルゴリズムよりも遅くなります。
この記事には、O(1)状態)の反復用の疑似コードがあり、これは反復子に簡単に適用できます。
深さ優先検索手法の使用についてはどうですか。イテレータオブジェクトには、既にアクセスしたノードのスタックが必要です。
O(1)スペースを使用します。つまり、O(h)スタックを使用しません。
始める:
hasNext()? current.val <= endNode.valは、ツリーが完全にトラバースされているかどうかを確認します。
左端から最小値を見つける:常に左端を探して次の最小値を見つけることができます。
左端の最小値がチェックされたら(名前はcurrent
)。次の分は2つのケースになります:current.right!= nullの場合、次の分としてcurrent.rightの左端の子を探し続けることができます。または、親を後方に見る必要があります。バイナリ検索ツリーを使用して、現在の親ノードを見つけます。
注:親をバイナリ検索するときは、parent.left = currentを満たしていることを確認してください。
理由:parent.right == currentの場合、その親は前にアクセスされている必要があります。バイナリ検索ツリーでは、parent.val <parent.right.valであることがわかります。この特別なケースはスキップする必要があります。無限ループにつながるからです。
public class BSTIterator {
public TreeNode root;
public TreeNode current;
public TreeNode endNode;
//@param root: The root of binary tree.
public BSTIterator(TreeNode root) {
if (root == null) {
return;
}
this.root = root;
this.current = root;
this.endNode = root;
while (endNode != null && endNode.right != null) {
endNode = endNode.right;
}
while (current != null && current.left != null) {
current = current.left;
}
}
//@return: True if there has next node, or false
public boolean hasNext() {
return current != null && current.val <= endNode.val;
}
//@return: return next node
public TreeNode next() {
TreeNode rst = current;
//current node has right child
if (current.right != null) {
current = current.right;
while (current.left != null) {
current = current.left;
}
} else {//Current node does not have right child.
current = findParent();
}
return rst;
}
//Find current's parent, where parent.left == current.
public TreeNode findParent(){
TreeNode node = root;
TreeNode parent = null;
int val = current.val;
if (val == endNode.val) {
return null;
}
while (node != null) {
if (val < node.val) {
parent = node;
node = node.left;
} else if (val > node.val) {
node = node.right;
} else {//node.val == current.val
break;
}
}
return parent;
}
}
定義上、next()およびhasNext()をO(1)時間で実行することはできません。BSTの特定のノードを見ると、他のノードの高さと構造は、したがって、正しい次のノードに「ジャンプ」することはできません。
ただし、スペースの複雑さはO(1)(BST自体のメモリを除く)に減らすことができます。Cで行う方法は次のとおりです。
struct node{
int value;
struct node *left, *right, *parent;
int visited;
};
struct node* iter_next(struct node* node){
struct node* rightResult = NULL;
if(node==NULL)
return NULL;
while(node->left && !(node->left->visited))
node = node->left;
if(!(node->visited))
return node;
//move right
rightResult = iter_next(node->right);
if(rightResult)
return rightResult;
while(node && node->visited)
node = node->parent;
return node;
}
秘Theは、親リンクと各ノードの訪問済みフラグの両方を持つことです。私の意見では、これは追加のスペース使用量ではなく、単にノード構造の一部であると言えます。そして明らかに、iter_next()は、ツリー構造の状態を変更せずに(もちろん)呼び出す必要がありますが、「訪問済み」フラグは値を変更しません。
Iter_next()を呼び出し、このツリーのたびに値を出力するテスター関数は次のとおりです。
27
/ \
20 62
/ \ / \
15 25 40 71
\ /
16 21
int main(){
//right root subtree
struct node node40 = {40, NULL, NULL, NULL, 0};
struct node node71 = {71, NULL, NULL, NULL, 0};
struct node node62 = {62, &node40, &node71, NULL, 0};
//left root subtree
struct node node16 = {16, NULL, NULL, NULL, 0};
struct node node21 = {21, NULL, NULL, NULL, 0};
struct node node15 = {15, NULL, &node16, NULL, 0};
struct node node25 = {25, &node21, NULL, NULL, 0};
struct node node20 = {20, &node15, &node25, NULL, 0};
//root
struct node node27 = {27, &node20, &node62, NULL, 0};
//set parents
node16.parent = &node15;
node21.parent = &node25;
node15.parent = &node20;
node25.parent = &node20;
node20.parent = &node27;
node40.parent = &node62;
node71.parent = &node62;
node62.parent = &node27;
struct node *iter_node = &node27;
while((iter_node = iter_next(iter_node)) != NULL){
printf("%d ", iter_node->value);
iter_node->visited = 1;
}
printf("\n");
return 1;
}
ソートされた順序で値を出力します:
15 16 20 21 25 27 40 62 71
スタックを使用する場合、「余分なメモリ使用量O(h)、hはツリーの高さ」のみを達成します。ただし、O(1)余分なメモリのみを使用する場合は、次の分析を記録する必要があります。現在のノードには正しい子がありません。ルートから検索し、最下位の先祖である次の最下位ノードを更新し続ける必要があります
public class Solution {
//@param root: The root of binary tree.
TreeNode current;
TreeNode root;
TreeNode rightMost;
public Solution(TreeNode root) {
if(root==null) return;
this.root = root;
current = findMin(root);
rightMost = findMax(root);
}
//@return: True if there has next node, or false
public boolean hasNext() {
if(current!=null && rightMost!=null && current.val<=rightMost.val) return true;
else return false;
}
//O(1) memory.
public TreeNode next() {
//1. if current has right child: find min of right sub tree
TreeNode tep = current;
current = updateNext();
return tep;
}
public TreeNode updateNext(){
if(!hasNext()) return null;
if(current.right!=null) return findMin(current.right);
//2. current has no right child
//if cur < root , go left; otherwise, go right
int curVal = current.val;
TreeNode post = null;
TreeNode tepRoot = root;
while(tepRoot!=null){
if(curVal<tepRoot.val){
post = tepRoot;
tepRoot = tepRoot.left;
}else if(curVal>tepRoot.val){
tepRoot = tepRoot.right;
}else {
current = post;
break;
}
}
return post;
}
public TreeNode findMin(TreeNode node){
while(node.left!=null){
node = node.left;
}
return node;
}
public TreeNode findMax(TreeNode node){
while(node.right!=null){
node = node.right;
}
return node;
}
}