バイナリツリーで2つのノードの最も低い共通祖先を見つける方法は?


186

ここでのバイナリツリーは、必ずしもバイナリ検索ツリーであるとは限りません。
構造は次のように解釈できます-

struct node {
    int data;
    struct node *left;
    struct node *right;
};

友人と一緒に解決できる最大の解決策は、このようなものでした- この二分木を
考えてみましょう:

二分木

順序トラバーサルの利回り-8、4、9、2、5、1、6、3、7

そして、ポストオーダートラバーサルイールド-8、9、4、5、2、6、7、3、1

したがって、たとえば、ノード8と5の共通の祖先を見つけたい場合は、順序木走査で8と5の間のすべてのノードのリストを作成します。この場合、[4、9 、2]。次に、このリストのどのノードがポストオーダートラバーサルの最後に表示されるか(2)を確認します。したがって、8と5の共通の祖先は2です。

このアルゴリズムの複雑さは、O(n)(インオーダー/ポストオーダートラバーサルではO(n)、残りのステップは配列の単純な反復にすぎないため、残りのステップもO(n)である)と私は信じています。しかし、これが間違っている可能性が高いです。:-)

しかし、これは非常に大雑把なアプローチであり、いくつかのケースで失敗するかどうかはわかりません。この問題に対する他の(おそらくより最適な)解決策はありますか?


6
好奇心から、これの実用的な使い方は何ですか?
David Brunelle

19
@David:LCAクエリ応答はかなり便利です。LCA +サフィックスツリー=強力な文字列関連アルゴリズム。

44
そして、私が同様の質問をしたところ、インタビューの質問のようなコメントで不評になりました。SOの双対性?:(
some_other_guy

5
質問の詳細については、@ Siddant +1。:)
amod、2013年

5
@DavidBrunelle LCAを計算する1つの実用的なアプリケーション:Webページをレンダリングするとき、特に特定のDOM要素に適用できるカスケードスタイルシート(CSS)を計算するときは、これは不可欠な計算です。
zc22 2014年

回答:


73

Nick Johnsonは正しいです。親ポインターがない場合は、O(n)時間の複雑度アルゴリズムが最適です。)そのアルゴリズムの単純な再帰バージョンについては、O(n)時間で実行されるKindingの投稿のコードを参照してください。。

ただし、ノードに親ポインターがある場合は、アルゴリズムを改善できることに注意してください。問題の両方のノードについて、ルートからノードへのパスを含むリストを作成し、ノードから開始して、親を前面に挿入します。

したがって、例の8の場合、(ステップを示す)次のようになります:{4}、{2、4}、{1、2、4}

問題の他のノードについても同じことを行い、結果(ステップは表示されていません):{1、2}

次に、作成した2つのリストを比較して、リストが異なる最初の要素、またはリストのいずれかの最後の要素のどちらか早い方を探します。

このアルゴリズムはO(h)時間を必要とします。hは木の高さです。最悪の場合、O(h)はO(n)と同等ですが、ツリーのバランスが取れている場合、それはO(log(n))だけです。また、O(h)スペースも必要です。CEGRDの投稿にコードが示されている、一定のスペースのみを使用する改良バージョンが可能です


ツリーの構築方法に関係なく、これを途中で変更せずにツリーで何度も実行する操作である場合、O(n)[線形]時間の準備を必要とする他のアルゴリズムを使用できますが、ペアはO(1)[一定]時間しかかかりません。これらのアルゴリズムのリファレンスについては、ウィキペディアの最も低い一般的な祖先の問題のページを参照してください。(最初にこのリンクを投稿したジェイソンへのクレジット)


1
親ポインターが指定されている場合は、これでうまくいきます。ツリー内のノードは、質問で指定した構造と同じです-左/右の子ポインターだけで、親ポインターはありません。利用可能な親ポインタがなく、ツリーがバイナリ検索ツリーではなく、単なるバイナリツリーである場合、O(log(n))ソリューションはありますか?
Siddhant

2
親と特定のノードの間のパスを見つける特定の方法がない場合、それを見つけるのに平均でO(n)時間かかります。これにより、O(log(n))時間を持つことが不可能になります。ただし、途中でツリーを変更せずにこの操作を何度も実行する場合は、O(1)のペアを見つけるO(n)の1回のコストが最善の策になる可能性があります。それ以外の場合、可能であれば、親ポインターを追加する必要があります。かなりの数の潜在的なアルゴリズムを高速化できますが、既存のアルゴリズムの順序を変更しないと確信しています。お役に立てれば。
ケビン・キャスカート、

1
でArteliusの(その他)ソリューションを参照してください-このアプローチはO(1)メモリを使用して行うことができstackoverflow.com/questions/1594061/...
トムSirgedas

@Tom:確かに、これはリストベースのアルゴリズムのメモリの複雑さをO(1)に制限するのに役立ちます。明らかに、それはノード自体の深さを取得するために両側で1回ツリー自体を反復し、次に共通の祖先を見つけるために(部分的に)2回目を繰り返すことを意味します。O(h)時間とO(1)スペースは、親ポインターがあり、O(n)事前計算を行わない場合に明らかに最適です。
Kevin Cathcart、2011年

1
@ALBI O(h)O(log(n))、ツリーのバランスが取れている場合のみです。どんなツリーでも、それがバイナリであるかどうかに関係なく、親ポインターがある場合はO(h)、親ポインターをh何回か追跡するだけで、葉からルートへのパスを時間で決定できます。これで、葉から根へのパスがわかります。パスがスタックとして保存されている場合、スタックを反復するとルートからリーフへのパスが得られます。親ポインターがなく、ツリーへの特別な構造がない場合、ルートからリーフへのパスを見つけるにはO(n)時間がかかります。
Kevin Cathcart

107

開始rootノードとあなたはどちらか持っている任意のノードを見つける場合は下方に移動するpか、qその直接の子としてそれはLCAです。(編集-これは、ノードの値であるか、pまたはそれqである必要があります。それを返します。それ以外の場合は、一方pまたはq他方の直接の子の場合は失敗します。)

そうでなければp、右(または左)サブツリーとq左(または右)サブツリーにノードがある場合、それはLCAです。

修正されたコードは次のようになります。

treeNodePtr findLCA(treeNodePtr root, treeNodePtr p, treeNodePtr q) {

        // no root no LCA.
        if(!root) {
                return NULL;
        }

        // if either p or q is the root then root is LCA.
        if(root==p || root==q) {
                return root;
        } else {
                // get LCA of p and q in left subtree.
                treeNodePtr l=findLCA(root->left , p , q);

                // get LCA of p and q in right subtree.
                treeNodePtr r=findLCA(root->right , p, q);

                // if one of p or q is in leftsubtree and other is in right
                // then root it the LCA.
                if(l && r) {
                        return root;
                }
                // else if l is not null, l is LCA.
                else if(l) {
                        return l;
                } else {
                        return r;
                }
        }
}

以下のコードは、どちらかが他の直接の子である場合に失敗します。

treeNodePtr findLCA(treeNodePtr root, treeNodePtr p, treeNodePtr q) {

        // no root no LCA.
        if(!root) {
                return NULL;
        }

        // if either p or q is direct child of root then root is LCA.
        if(root->left==p || root->left==q || 
           root->right ==p || root->right ==q) {
                return root;
        } else {
                // get LCA of p and q in left subtree.
                treeNodePtr l=findLCA(root->left , p , q);

                // get LCA of p and q in right subtree.
                treeNodePtr r=findLCA(root->right , p, q);

                // if one of p or q is in leftsubtree and other is in right
                // then root it the LCA.
                if(l && r) {
                        return root;
                }
                // else if l is not null, l is LCA.
                else if(l) {
                        return l;
                } else {
                        return r;
                }
        }
}

実行中のコード


2
エレガントなソリューションですが、root == p || root == q =>戻りルートビットは楽観的すぎるようです。ルートがp / qであることが判明したが、他の求められているノードが実際にはツリーにない場合はどうなりますか?
Ian Durkan、2011年

15
このコードは、pまたはqがバイナリツリーにない値の場合に失敗すると思います。私は正しいですか?たとえば、LCA(8,20)。urコードは8を返しますが、20はバイナリツリーに存在しません
javaMan

3
このソリューションのコストはいくらですか?効率的ですか?pとqの両方が見つかった後も検索を続けているようです。これは、BSTではなく、重複が含まれている可能性があるため、pとqがツリー内で一意でない可能性があるためですか?
MikeB 2013年

3
@MikeB、このソリューションは間違いなくO(n)です。これは、最悪の場合、各ノードを1回だけトラバースするためです。ピーター・リー、これは親ポインターを使用せずに作成できる最も効率的な方法です。より良い解決策はありますか?
gsingh2011 2014年

8
最初の不完全なソリューションは、邪魔にならないように削除する必要があります
Zinan Xing

50

これがJAVAの作業コードです

public static Node LCA(Node root, Node a, Node b) {
   if (root == null) {
       return null;
   }

   // If the root is one of a or b, then it is the LCA
   if (root == a || root == b) {
       return root;
   }

   Node left = LCA(root.left, a, b);
   Node right = LCA(root.right, a, b);

   // If both nodes lie in left or right then their LCA is in left or right,
   // Otherwise root is their LCA
   if (left != null && right != null) {
      return root;
   }

   return (left != null) ? left : right; 
}

4
これは、ノードがツリーに存在しない場合は機能しません。
Pratik Khadloya

指定されたツリーがBSTである場合、コードを最適化しますか?
Mona Jalal 2016年

1
「ルートがaまたはbのいずれかである場合、それはLCAです。」これは本当ではないかもしれません。この時点で知っているのは、LCAを見つけるためにその子をチェックする必要がないことです。これは、後でルートの親について、両方のブランチ(LCAが親)またはそれらの1つだけ(この場合はLCAであるか、さらに上位の祖先がLCAである可能性がある)で一致があったかどうかを確認できるために発生します)。
andresp

28

これまでの回答では、再帰を使用するか、たとえばメモリ内のパスを格納しています。

非常に深いツリーがある場合、これらのアプローチはどちらも失敗する可能性があります。

これがこの質問に対する私の見解です。両方のノードの深さ(ルートからの距離)をチェックするときに、それらが等しい場合、両方のノードから共通の祖先に安全に上に移動できます。深さの1つが大きい場合は、他のノードにとどまったまま、より深いノードから上に移動する必要があります。

これがコードです:

findLowestCommonAncestor(v,w):
  depth_vv = depth(v);
  depth_ww = depth(w);

  vv = v; 
  ww = w;

  while( depth_vv != depth_ww ) {
    if ( depth_vv > depth_ww ) {
      vv = parent(vv);
      depth_vv--;
    else {
      ww = parent(ww);
      depth_ww--;
    }
  }

  while( vv != ww ) {
    vv = parent(vv);
    ww = parent(ww);
  }

  return vv;    

このアルゴリズムの時間の複雑さは、O(n)です。このアルゴリズムの空間の複雑さは、O(1)です。

深さの計算に関しては、最初に定義を思い出すことができます。vがルートの場合、depth(v)= 0; それ以外の場合、depth(v)= depth(parent(v))+1。次のように深さを計算できます。

depth(v):
  int d = 0;
  vv = v;
  while ( vv is not root ) {
    vv = parent(vv);
    d++;
  }
  return d;

6
通常、バイナリツリーには親要素への参照がありません。親参照の追加は問題なく実行できますが、O(n)補助スペースを考慮します。
John Kurlak 2013

このソリューションには微妙な前提があります。一方のノードが他方の直接または間接の親である(つまり、より深いノードがより浅いノードをルートとするツリー内にある)場合、このソリューションは結果としてより浅いノードの親を返します。共通の最下位の祖先をどのように定義するかによっては、これが希望どおりにならない場合があります。一部の定義では、より浅いノード自体を親にする必要があります。この場合、どちらがより浅いノードであるかを追跡し、それを返す必要があります。
Srikanth、2015年

8

まあ、この種類は、バイナリツリーの構造によって異なります。おそらく、ツリーのルートを指定して目的のリーフノードを見つける方法がいくつかあるでしょう。選択したブランチが分岐するまで、両方の値に適用するだけです。

ルートを指定して目的のリーフを見つける方法がない場合、通常の操作と最後の共通ノードの両方を見つける唯一の解決策は、ツリーの総当たり検索です。


8

これは次の場所にあります:-http : //goursaha.freeoda.com/DataStructure/LowestCommonAncestor.html

 tree_node_type *LowestCommonAncestor(
 tree_node_type *root , tree_node_type *p , tree_node_type *q)
 {
     tree_node_type *l , *r , *temp;
     if(root==NULL)
     {
        return NULL;
     }

    if(root->left==p || root->left==q || root->right ==p || root->right ==q)
    {
        return root;
    }
    else
    {
        l=LowestCommonAncestor(root->left , p , q);
        r=LowestCommonAncestor(root->right , p, q);

        if(l!=NULL && r!=NULL)
        {
            return root;
        }
        else
        {
        temp = (l!=NULL)?l:r;
        return temp;
        }
    }
}

ツリーにpが存在するがqがまったく存在しない場合のコードの動作を教えてください。同様に、pとqの両方が存在しません。ありがとう!!!
試行

時間的に大きなOは何ですか?O(n * log(n))、2つ遅いと思います。
ピーターリー


6

2つのノードの共通の祖先を見つけるには:-

  • バイナリ検索を使用してツリー内の指定されたノードNode1を見つけ、このプロセスでアクセスされたすべてのノードを配列A1に保存します。時間-O(logn)、スペース-O(logn)
  • バイナリ検索を使用してツリー内の指定されたNode2を見つけ、このプロセスでアクセスされたすべてのノードを配列A2に保存します。時間-O(logn)、スペース-O(logn)
  • A1リストまたはA2リストが空の場合、ノードが存在しないため、共通の祖先はありません。
  • A1リストとA2リストが空でない場合は、一致しないノードが見つかるまでリストを調べます。そのようなノードを見つけるとすぐに、その前のノードが共通の祖先になります。

これは、バイナリ検索ツリーで機能します。


2
彼はツリーが必ずしもBSTではないことを明確に述べました。
ピーターリー

@Peter Lee-上記のロジックは、単純な変更を加えたバイナリツリーでも機能します。指定されたノードのバイナリ検索の代わりに、線形検索を適用します(つまり、任意のトラバーサルですが、両方のケースで同じである必要があります)。もちろん、ランタイムはO(logn)ではなくO(n)になります。実際、このアルゴは、親ポインターが利用できない場合に最も堅牢なものです。多くの(viz。 'codaddict')によって与えられた再帰的アルゴリズムは、与えられたノードの1つがツリーに属していない場合は機能しません)
KGhatak


3

以下の再帰アルゴリズムは、バランスのとれたバイナリツリーのO(log N)で実行されます。getLCA()関数に渡されたノードのいずれかがルートと同じである場合、ルートはLCAであり、再帰を実行する必要はありません。

テストケース。 [1]ノードn1とn2の両方がツリー内にあり、親ノードのいずれかの側にあります。 [2]ノードn1またはn2のいずれかがルートであり、LCAがルートです。 [3]ツリーにはn1またはn2のみがあり、LCAはツリールートの左側のサブツリーのルートノードか、LCAはツリールートの右側のサブツリーのルートノードになります。

[4] n1もn2もツリー内になく、LCAはありません。 [5] n1とn2はどちらも直線上に並んでいます。LCAはn1またはn2のどちらかで、ツリーのルートに最も近くなります。

//find the search node below root
bool findNode(node* root, node* search)
{
    //base case
    if(root == NULL)
        return false;

    if(root->val == search->val)
        return true;

    //search for the node in the left and right subtrees, if found in either return true
    return (findNode(root->left, search) || findNode(root->right, search));
}

//returns the LCA, n1 & n2 are the 2 nodes for which we are
//establishing the LCA for
node* getLCA(node* root, node* n1, node* n2)
{
    //base case
    if(root == NULL)
        return NULL;

    //If 1 of the nodes is the root then the root is the LCA
    //no need to recurse.
    if(n1 == root || n2 == root)
        return root;

    //check on which side of the root n1 and n2 reside
    bool n1OnLeft = findNode(root->left, n1);
    bool n2OnLeft = findNode(root->left, n2);

    //n1 & n2 are on different sides of the root, so root is the LCA
    if(n1OnLeft != n2OnLeft)
        return root;

    //if both n1 & n2 are on the left of the root traverse left sub tree only
    //to find the node where n1 & n2 diverge otherwise traverse right subtree
    if(n1OnLeft)
        return getLCA(root->left, n1, n2);
    else
        return getLCA(root->right, n1, n2);
}

3

祖先を検索する必要のあるroot指定されたノード、たとえばpとの両方qが同じサブツリーにある限り、ツリー全体から歩いていくだけです(つまり、値が両方ともルートのルートよりも小さいか、どちらもルートのルートよりも大きい)。

これは、ルートから最少の共通祖先までまっすぐ進み、ツリーの残りの部分は見ていないので、できるだけ速くなります。それを行うためのいくつかの方法。

反復、O(1)スペース

パイソン

def lowestCommonAncestor(self, root, p, q):
    while (root.val - p.val) * (root.val - q.val) > 0:
        root = (root.left, root.right)[p.val > root.val]
    return root

ジャワ

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    while ((root.val - p.val) * (root.val - q.val) > 0)
        root = p.val < root.val ? root.left : root.right;
    return root;
}

オーバーフローの場合、私は(root.val-(long)p.val)*(root.val-(long)q.val)を実行します

再帰的

パイソン

def lowestCommonAncestor(self, root, p, q):
    next = p.val < root.val > q.val and root.left or \
           p.val > root.val < q.val and root.right
    return self.lowestCommonAncestor(next, p, q) if next else root

ジャワ

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    return (root.val - p.val) * (root.val - q.val) < 1 ? root :
           lowestCommonAncestor(p.val < root.val ? root.left : root.right, p, q);
}

2
Node *LCA(Node *root, Node *p, Node *q) {
  if (!root) return NULL;
  if (root == p || root == q) return root;
  Node *L = LCA(root->left, p, q);
  Node *R = LCA(root->right, p, q);
  if (L && R) return root;  // if p and q are on both sides
  return L ? L : R;  // either one of p,q is on one side OR p,q is not in L&R subtrees
}

2

この木を考えて ここに画像の説明を入力してください

ポストオーダーおよびプレオーダートラバーサルを実行し、最初に発生する共通の先行および後続を検出すると、共通の祖先が取得されます。

postorder => 0,2,1,5,4,6,3,8,10,11,9,14,15,13,​​12,7 preorder => 7,3,1,0,2,6,4 、5、12、9、8、11、10、13、15、14

  • 例:1

8,11の最小共通祖先

ポストオーダーでは、8&11の後に=> 9,14,15,13,​​12,7があります。プレオーダーでは、8&11の前に=> 7,3,1,0,2,6,4,5,12,9があります。

9は、ポストオーダーの8&11の後で、プレオーダーの8&11の前に発生する最初の一般的な数なので、9が答えです

  • 例:2

最少共通祖先5,10

11,9,14,15,13,​​12,7ポストオーダー7,3,1,0,2,6,4プレオーダー

7は、ポストオーダーで5,10の後で、プレオーダーで5,10の前に発生する最初の数値であるため、7が答えです


2

ノードxの子が2 * xおよび2 * x + 1である完全なバイナリツリーの場合、より高速な方法よりも

int get_bits(unsigned int x) {
  int high = 31;
  int low = 0,mid;
  while(high>=low) {
    mid = (high+low)/2;
    if(1<<mid==x)
      return mid+1;
    if(1<<mid<x) {
      low = mid+1;
    }
    else {
      high = mid-1;
    }
  }
  if(1<<mid>x)
    return mid;
  return mid+1;
}

unsigned int Common_Ancestor(unsigned int x,unsigned int y) {

  int xbits = get_bits(x);
  int ybits = get_bits(y);
  int diff,kbits;
  unsigned int k;
  if(xbits>ybits) {
    diff = xbits-ybits;
    x = x >> diff;
  }
  else if(xbits<ybits) {
    diff = ybits-xbits;
    y = y >> diff;
  }
  k = x^y;
  kbits = get_bits(k);
  return y>>kbits;  
}

それはどのように機能しますか

  1. xとyを表すために必要なビットを取得します。バイナリ検索を使用するとO(log(32))になります。
  2. xとyのバイナリ表記の共通の接頭辞は共通の祖先です
  3. より大きなビット数で表されている方は、k >> diffによって同じビットになります
  4. k = x ^ yは、xおよびyの共通のプレフィックスを消去します
  5. 残りのサフィックスを表すビットを見つける
  6. xまたはyを接尾辞ビットでシフトして、共通の祖先である共通の接頭辞を取得します。

これは基本的に、両方の数値が等しくなるまで、大きい方の数値を再帰的に2で除算するためです。その数は共通の祖先です。分割は事実上、右シフト操作です。したがって、最も近い祖先を見つけるには、2つの数値の共通のプレフィックスを見つける必要があります。


2

Scalaでは、次のことができます。

  abstract class Tree
  case class Node(a:Int, left:Tree, right:Tree) extends Tree
  case class Leaf(a:Int) extends Tree

  def lca(tree:Tree, a:Int, b:Int):Tree = {
    tree match {
      case Node(ab,l,r) => {
        if(ab==a || ab ==b) tree else {
          val temp = lca(l,a,b);
          val temp2 = lca(r,a,b);
          if(temp!=null && temp2 !=null)
            tree
          else if (temp==null && temp2==null)
            null
          else if (temp==null) r else l
        }

      }
      case Leaf(ab) => if(ab==a || ab ==b) tree else null
    }
  }

1
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if(root==null || root == p || root == q){
            return root;
        }
        TreeNode left = lowestCommonAncestor(root.left,p,q);
        TreeNode right = lowestCommonAncestor(root.right,p,q);
        return left == null ? right : right == null ? left : root;
    }

0

これを行うC ++の方法を次に示します。アルゴリズムを可能な限り理解しやすいように維持しようとしました:

// Assuming that `BinaryNode_t` has `getData()`, `getLeft()` and `getRight()`
class LowestCommonAncestor
{
  typedef char type;    
  // Data members which would behave as place holders
  const BinaryNode_t* m_pLCA;
  type m_Node1, m_Node2;

  static const unsigned int TOTAL_NODES = 2;

  // The core function which actually finds the LCA; It returns the number of nodes found
  // At any point of time if the number of nodes found are 2, then it updates the `m_pLCA` and once updated, we have found it!
  unsigned int Search (const BinaryNode_t* const pNode)
  {
    if(pNode == 0)
      return 0;

    unsigned int found = 0;

    found += (pNode->getData() == m_Node1);
    found += (pNode->getData() == m_Node2);

    found += Search(pNode->getLeft()); // below condition can be after this as well
    found += Search(pNode->getRight());

    if(found == TOTAL_NODES && m_pLCA == 0)
      m_pLCA = pNode;  // found !

    return found;
  }

public:
  // Interface method which will be called externally by the client
  const BinaryNode_t* Search (const BinaryNode_t* const pHead,
                              const type node1,
                              const type node2)
  {
    // Initialize the data members of the class
    m_Node1 = node1;
    m_Node2 = node2;
    m_pLCA = 0;

    // Find the LCA, populate to `m_pLCANode` and return
    (void) Search(pHead);
    return m_pLCA;
  }
};

どうやって使うのですか:

LowestCommonAncestor lca;
BinaryNode_t* pNode = lca.Search(pWhateverBinaryTreeNodeToBeginWith);
if(pNode != 0)
  ...

0

最も低い共通の祖先を見つける最も簡単な方法は、次のアルゴリズムを使用することです。

ルートノードを調べる

value1とvalue2がルートノードの値よりも厳密に小さい場合 
    左側のサブツリーを調べる
それ以外の場合、value1とvalue2がルートノードの値よりも厳密に大きい場合 
    正しいサブツリーを調べる
そうしないと
    ルートを返す
public int LCA(TreeNode root, int value 1, int value 2) {
    while (root != null) {
       if (value1 < root.data && value2 < root.data)
           return LCA(root.left, value1, value2);
       else if (value2 > root.data && value2 2 root.data)
           return LCA(root.right, value1, value2);
       else
           return root
    }

    return null;
} 

6
それはBSTではありません!
ピーターリー

0

解決策を見つけた

  1. 順番に
  2. 予約購入
  3. ポストオーダーを取る

3つのトラバーサルに応じて、誰がLCAであるかを決定できます。LCAから、両方のノードの距離を見つけます。これらの2つの距離を加算することが答えです。


0

これが私が思うことです、

  1. 最初のノードのルートを見つけ、arr1に保存します。
  2. ルートからarr1までのすべての値を確認しながら、2ノードのルートの検索を開始します。
  3. 値が異なるとき、終了します。古い一致値はLCAです。

複雑さ:ステップ1:O(n)、ステップ2 =〜O(n)、合計=〜O(n)。


0

参照のために、c#(.net)での2つのアプローチ(上記で説明)を次に示します。

  1. バイナリツリーでのLCAの再帰バージョン(O(N)-最大で各ノードにアクセス)(ソリューションの主なポイントはLCAです)(a)両方の要素がサブツリーのいずれかの側にあるバイナリツリーの唯一のノード(左と右)はLCAです(b)また、どちらのノードがどちらの側に存在するかは関係ありません。最初はその情報を保持しようとしましたが、明らかに再帰関数は非常に混乱します。

  2. 両方のノードを検索し(O(N))、パスを追跡します(余分なスペースを使用します。そのため、バイナリツリーのバランスが取れていればスペースが無視できるとしても、#1はおそらく優れています。 O(log(N))。

    パスが比較されるように(実質的に受け入れられた回答に似ていますが、パスは、ポインターノードがバイナリツリーノードに存在しないと想定して計算されます)

  3. 完了のため(質問とは関係ありません)、BSTのLCA(O(log(N))

  4. テスト

再帰的:

private BinaryTreeNode LeastCommonAncestorUsingRecursion(BinaryTreeNode treeNode, 
            int e1, int e2)
        {
            Debug.Assert(e1 != e2);
            
            if(treeNode == null)
            {
                return null;
            }
            if((treeNode.Element == e1)
                || (treeNode.Element == e2))
            {
                //we don't care which element is present (e1 or e2), we just need to check 
                //if one of them is there
                return treeNode;
            }
            var nLeft = this.LeastCommonAncestorUsingRecursion(treeNode.Left, e1, e2);
            var nRight = this.LeastCommonAncestorUsingRecursion(treeNode.Right, e1, e2);
            if(nLeft != null && nRight != null)
            {
                //note that this condition will be true only at least common ancestor
                return treeNode;
            }
            else if(nLeft != null)
            {
                return nLeft;
            }
            else if(nRight != null)
            {
                return nRight;
            }
            return null;
        }

上記のプライベート再帰バージョンは、次のパブリックメソッドによって呼び出されます。

public BinaryTreeNode LeastCommonAncestorUsingRecursion(int e1, int e2)
        {
            var n = this.FindNode(this._root, e1);
            if(null == n)
            {
                throw new Exception("Element not found: " + e1);
            }
            if (e1 == e2)
            {   
                return n;
            }
            n = this.FindNode(this._root, e2);
            if (null == n)
            {
                throw new Exception("Element not found: " + e2);
            }
            var node = this.LeastCommonAncestorUsingRecursion(this._root, e1, e2);
            if (null == node)
            {
                throw new Exception(string.Format("Least common ancenstor not found for the given elements: {0},{1}", e1, e2));
            }
            return node;
        }

両方のノードのパスを追跡することによる解決策:

public BinaryTreeNode LeastCommonAncestorUsingPaths(int e1, int e2)
        {
            var path1 = new List<BinaryTreeNode>();
            var node1 = this.FindNodeAndPath(this._root, e1, path1);
            if(node1 == null)
            {
                throw new Exception(string.Format("Element {0} is not found", e1));
            }
            if(e1 == e2)
            {
                return node1;
            }
            List<BinaryTreeNode> path2 = new List<BinaryTreeNode>();
            var node2 = this.FindNodeAndPath(this._root, e2, path2);
            if (node1 == null)
            {
                throw new Exception(string.Format("Element {0} is not found", e2));
            }
            BinaryTreeNode lca = null;
            Debug.Assert(path1[0] == this._root);
            Debug.Assert(path2[0] == this._root);
            int i = 0;
            while((i < path1.Count)
                && (i < path2.Count)
                && (path2[i] == path1[i]))
            {
                lca = path1[i];
                i++;
            }
            Debug.Assert(null != lca);
            return lca;
        }

FindNodeAndPathは次のように定義されています

private BinaryTreeNode FindNodeAndPath(BinaryTreeNode node, int e, List<BinaryTreeNode> path)
        {
            if(node == null)
            {
                return null;
            }
            if(node.Element == e)
            {
                path.Add(node);
                return node;
            }
            var n = this.FindNodeAndPath(node.Left, e, path);
            if(n == null)
            {
                n = this.FindNodeAndPath(node.Right, e, path);
            }
            if(n != null)
            {
                path.Insert(0, node);
                return n;
            }
            return null;
        }

BST(LCA)-関連なし(参考のために記入してください)

public BinaryTreeNode BstLeastCommonAncestor(int e1, int e2)
        {
            //ensure both elements are there in the bst
            var n1 = this.BstFind(e1, throwIfNotFound: true);
            if(e1 == e2)
            {
                return n1;
            }
            this.BstFind(e2, throwIfNotFound: true);
            BinaryTreeNode leastCommonAcncestor = this._root;
            var iterativeNode = this._root;
            while(iterativeNode != null)
            {
                if((iterativeNode.Element > e1 ) && (iterativeNode.Element > e2))
                {
                    iterativeNode = iterativeNode.Left;
                }
                else if((iterativeNode.Element < e1) && (iterativeNode.Element < e2))
                {
                    iterativeNode = iterativeNode.Right;
                }
                else
                {
                    //i.e; either iterative node is equal to e1 or e2 or in between e1 and e2
                    return iterativeNode;
                }
            }
            //control will never come here
            return leastCommonAcncestor;
        }

ユニットテスト

[TestMethod]
        public void LeastCommonAncestorTests()
        {
            int[] a = { 13, 2, 18, 1, 5, 17, 20, 3, 6, 16, 21, 4, 14, 15, 25, 22, 24 };
            int[] b = { 13, 13, 13, 2, 13, 18, 13, 5, 13, 18, 13, 13, 14, 18, 25, 22};
            BinarySearchTree bst = new BinarySearchTree();
            foreach (int e in a)
            {
                bst.Add(e);
                bst.Delete(e);
                bst.Add(e);
            }
            for(int i = 0; i < b.Length; i++)
            {
                var n = bst.BstLeastCommonAncestor(a[i], a[i + 1]);
                Assert.IsTrue(n.Element == b[i]);
                var n1 = bst.LeastCommonAncestorUsingPaths(a[i], a[i + 1]);
                Assert.IsTrue(n1.Element == b[i]);
                Assert.IsTrue(n == n1);
                var n2 = bst.LeastCommonAncestorUsingRecursion(a[i], a[i + 1]);
                Assert.IsTrue(n2.Element == b[i]);
                Assert.IsTrue(n2 == n1);
                Assert.IsTrue(n2 == n);
            }
        }

0

擬似コード(大学のホームワーク用)に興味がある人がいれば、ここにあります。

GETLCA(BINARYTREE BT, NODE A, NODE  B)
IF Root==NIL
    return NIL
ENDIF

IF Root==A OR root==B
    return Root
ENDIF

Left = GETLCA (Root.Left, A, B)
Right = GETLCA (Root.Right, A, B)

IF Left! = NIL AND Right! = NIL
    return root
ELSEIF Left! = NIL
    Return Left
ELSE
    Return Right
ENDIF

0

これはすでに回答されていますが、これはCプログラミング言語を使用したこの問題への私のアプローチです。コードは(insert()に関する限り)バイナリ検索ツリーを示していますが、アルゴリズムはバイナリツリーでも機能します。アイデアは、順序トラバーサルでノードAからノードBにあるすべてのノードを調べ、ポスト順序トラバーサルでこれらのインデックスを検索することです。後順走査で最大のインデックスを持つノードは、最も低い共通の祖先です。

これは、バイナリツリーで最も低い共通の祖先を見つける関数を実装するための実用的なCコードです。すべてのユーティリティ関数なども提供していますが、すぐに理解できるようにCommonAncestor()にジャンプします。

#include <stdio.h>
#include <malloc.h>
#include <stdlib.h>
#include <math.h>

static inline int min (int a, int b)
{
    return ((a < b) ? a : b);
}
static inline int max (int a, int b)
{
    return ((a > b) ? a : b);
}

typedef struct node_ {
    int value;
    struct node_ * left;
    struct node_ * right;
} node;

#define MAX 12

int IN_ORDER[MAX] = {0};
int POST_ORDER[MAX] = {0};

createNode(int value) 
{
    node * temp_node = (node *)malloc(sizeof(node));
    temp_node->left = temp_node->right = NULL;
    temp_node->value = value;
    return temp_node;
}

node *
insert(node * root, int value)
{
    if (!root) {
        return createNode(value);
    }

    if (root->value > value) {
        root->left = insert(root->left, value);
    } else {
        root->right = insert(root->right, value);
    }

    return root;
}


/* Builds inorder traversal path in the IN array */
void
inorder(node * root, int * IN)
{
    static int i = 0;

    if (!root) return;

    inorder(root->left, IN);
    IN[i] = root->value;
    i++;
    inorder(root->right, IN);
}

/* Builds post traversal path in the POST array */

void
postorder (node * root, int * POST)
{
    static int i = 0;

    if (!root) return;

    postorder(root->left, POST);
    postorder(root->right, POST);
    POST[i] = root->value;
    i++;
}


int
findIndex(int * A, int value)
{
    int i = 0;
    for(i = 0; i< MAX; i++) {
        if(A[i] == value) return i;
    }
}
int
CommonAncestor(int val1, int val2)
{
    int in_val1, in_val2;
    int post_val1, post_val2;
    int j=0, i = 0; int max_index = -1;

    in_val1 = findIndex(IN_ORDER, val1);
    in_val2 = findIndex(IN_ORDER, val2);
    post_val1 = findIndex(POST_ORDER, val1);
    post_val2 = findIndex(POST_ORDER, val2);

    for (i = min(in_val1, in_val2); i<= max(in_val1, in_val2); i++) {
        for(j = 0; j < MAX; j++) {
            if (IN_ORDER[i] == POST_ORDER[j]) {
                if (j > max_index) {
                    max_index = j;
                }
            }
        }
    }
    printf("\ncommon ancestor of %d and %d is %d\n", val1, val2, POST_ORDER[max_index]);
    return max_index;
}
int main()
{
    node * root = NULL; 

    /* Build a tree with following values */
    //40, 20, 10, 30, 5, 15, 25, 35, 1, 80, 60, 100
    root = insert(root, 40);
    insert(root, 20);
    insert(root, 10);
    insert(root, 30);
    insert(root, 5);
    insert(root, 15);
    insert(root, 25);
    insert(root, 35);
    insert(root, 1);
    insert(root, 80);
    insert(root, 60);
    insert(root, 100);

    /* Get IN_ORDER traversal in the array */
    inorder(root, IN_ORDER);

    /* Get post order traversal in the array */
    postorder(root, POST_ORDER);

    CommonAncestor(1, 100);


}

0

もう1つの方法があります。ただし、回答ですでに提案されているものほど効率的ではありません。

  • ノードn1のパスベクトルを作成します。

  • ノードn2の2番目のパスベクトルを作成します。

  • 問題のノードに到達するためにトラバースするノードのセットノードを意味するパスベクトル。

  • 両方のパスベクトルを比較します。それらが一致しないインデックスは、そのインデックス-1のノードを返します。これにより、LCAが得られます。

このアプローチの短所:

パスベクトルを計算するには、ツリーを2回トラバースする必要があります。パスベクトルを格納するには、追加のO(h)スペースが必要です。

ただし、これは実装も理解も簡単です。

パスベクトルを計算するコード:

private boolean findPathVector (TreeNode treeNode, int key, int pathVector[], int index) {

        if (treeNode == null) {
            return false;
        }

        pathVector [index++] = treeNode.getKey ();

        if (treeNode.getKey () == key) {
            return true;
        }
        if (findPathVector (treeNode.getLeftChild (), key, pathVector, index) || 
            findPathVector (treeNode.getRightChild(), key, pathVector, index)) {

            return true;        
        }

        pathVector [--index] = 0;
        return false;       
    }

0

このようにしてみてください

node * lca(node * root, int v1,int v2)
{

if(!root) {
            return NULL;
    }
    if(root->data == v1 || root->data == v2) {
        return root;}
    else
    {
        if((v1 > root->data && v2 < root->data) || (v1 < root->data && v2 > root->data))
        {
            return root;
        }

        if(v1 < root->data && v2 < root->data)
        {
            root = lca(root->left, v1, v2);
        }

        if(v1 > root->data && v2 > root->data)
        {
            root = lca(root->right, v1, v2);
        }
    }
return root;
}

0

粗野な方法:

  • すべてのノードで
    • X = n1、n2のいずれかがノードの左側に存在するかどうかを調べる
    • Y = n1、n2のいずれかがノードの右側に存在するかどうかを調べる
      • ノード自体がn1の場合|| n2、一般化の目的で、左または右にあると呼ぶことができます。
    • XとYの両方がtrueの場合、ノードはCAです

上記の方法の問題は、「検索」を複数回実行することです。つまり、各ノードが複数回トラバースされる可能性があります。再度処理しないように情報を記録できれば、この問題を克服できます(動的プログラミングを考えてみてください)。

したがって、すべてのノードを検索するのではなく、すでに見つかったものについての記録を保持します。

もっといい方法:

  • left_set(n1 | n2が左側のサブツリーで見つかったことを意味します)またはright_setのいずれかが深さ優先であるかどうかを確認します。(注:n1 | n2のいずれかである場合、ルート自体にleft_setのプロパティを指定します)
  • left_setとright_setの両方の場合、ノードはLCAです。

コード:

struct Node *
findCA(struct Node *root, struct Node *n1, struct Node *n2, int *set) {
   int left_set, right_set;
   left_set = right_set = 0;
   struct Node *leftCA, *rightCA;
   leftCA = rightCA = NULL;

   if (root == NULL) {
      return NULL;
   }
   if (root == n1 || root == n2) {
      left_set = 1;
      if (n1 == n2) {
         right_set = 1;
      }
   }

   if(!left_set) {
      leftCA = findCA(root->left, n1, n2, &left_set);
      if (leftCA) {
         return leftCA;
      }
   }
   if (!right_set) {
      rightCA= findCA(root->right, n1, n2, &right_set);
      if(rightCA) {
         return rightCA;
      }
   }

   if (left_set && right_set) {
      return root;
   } else {
      *set = (left_set || right_set);
      return NULL;
   }
}

0

幅優先検索のコードで、両方のノードがツリーにあることを確認します。その後、LCA検索を続行します。改善するための提案があればコメントしてください。私たちはおそらくそれらを訪問済みとしてマークし、2番目のノードの改善のために中断した特定のポイントで検索を再開できると思います(VISITEDが見つからない場合)。

public class searchTree {
    static boolean v1=false,v2=false;
    public static boolean bfs(Treenode root, int value){
         if(root==null){
           return false;
     }
    Queue<Treenode> q1 = new LinkedList<Treenode>();

    q1.add(root);
    while(!q1.isEmpty())
    {
        Treenode temp = q1.peek();

        if(temp!=null) {
            q1.remove();
            if (temp.value == value) return true;
            if (temp.left != null) q1.add(temp.left);
            if (temp.right != null) q1.add(temp.right);
        }
    }
    return false;

}
public static Treenode lcaHelper(Treenode head, int x,int y){

    if(head==null){
        return null;
    }

    if(head.value == x || head.value ==y){
        if (head.value == y){
            v2 = true;
            return head;
        }
        else {
            v1 = true;
            return head;
        }
    }

    Treenode left = lcaHelper(head.left, x, y);
    Treenode right = lcaHelper(head.right,x,y);

    if(left!=null && right!=null){
        return head;
    }
    return (left!=null) ? left:right;
}

public static int lca(Treenode head, int h1, int h2) {
    v1 = bfs(head,h1);
    v2 = bfs(head,h2);
    if(v1 && v2){
        Treenode lca = lcaHelper(head,h1,h2);
        return lca.value;
    }
    return -1;
}
}

0

親ノードがない場合、トラバーサルを使用したソリューションはO(n)時間の複雑さを与えることは間違いありません。

トラバーサルアプローチ ノードAとBのLCAを見つける場合、最も簡単なアプローチは、最初にルートからAへのパスを取得し、次にルートからBへのパスを取得することです。これら2つのパスを取得したら、簡単に反復できます最後の共通ノードを見つけます。これは、AとBの共通の最下位の祖先です。

再帰的な解決策 別のアプローチは、再帰を使用することです。まず、左側のツリーと右側のツリー(存在する場合)の両方からLCAを取得できます。AまたはBのいずれかがルートノードである場合、ルートはLCAであり、再帰の終点であるルートを返すだけです。ツリーをサブツリーに分割し続けると、最終的にはAとBのどちらかがヒットします。

サブ問題のソリューションを組み合わせるために、LCA(左ツリー)がノードを返す場合、AとBの両方が左ツリーに配置され、返されたノードが最終結果であることがわかります。LCA(左)とLCA(右)の両方が空でないノードを返す場合、それはAとBがそれぞれ左と右のツリーにあることを意味します。この場合、ルートノードは最も低い共通ノードです。

詳細な分析と解決策については、最下位の共通祖先を確認してください。


0

ここでのソリューションの中には、ルートノードへの参照があると想定しているものや、ツリーがBSTであると想定しているものがあります。rootノードとツリーを参照せずにハッシュマップを使用して私のソリューションを共有すると、BSTまたは非BSTになります。

    var leftParent : Node? = left
    var rightParent : Node? = right
    var map = [data : Node?]()

    while leftParent != nil {
        map[(leftParent?.data)!] = leftParent
        leftParent = leftParent?.parent
    }

    while rightParent != nil {
        if let common = map[(rightParent?.data)!] {
            return common
        }
        rightParent = rightParent?.parent
    }

0

解決策1:再帰-より高速

  • ルートからツリーをトラバースするという考え方です。指定されたキーpおよびqのいずれかがrootと一致する場合、両方のキーが存在すると想定して、rootはLCAです。ルートがどのキーとも一致しない場合は、左と右のサブツリーを再帰します。
  • 左側のサブツリーに1つのキーがあり、右側のサブツリーにもう1つのキーがあるノードは、LCAです。両方のキーが左側のサブツリーにある場合、左側のサブツリーにもLCAがあり、それ以外の場合、LCAは右側のサブツリーにあります。
  • 時間の複雑さ:O(n)
  • スペースの複雑さ:O(h)-再帰呼び出しスタック用
class Solution 
{
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q)
    {
        if(root == null || root == p  || root == q)
            return root;

        TreeNode left = lowestCommonAncestor(root.left, p, q);
        TreeNode right = lowestCommonAncestor(root.right, p, q);

        if(left == null)
            return right;
        else if(right == null)
            return left;
        else
            return root;    // If(left != null && right != null)
    }
}

解決策2:反復-親ポインターの使用-遅い

  • 空のハッシュテーブルを作成します。
  • pとそのすべての祖先をハッシュテーブルに挿入します。
  • qまたはその祖先がハッシュテーブルに存在するかどうかを確認します。存在する場合は、最初の既存の祖先を返します。
  • 時間の複雑さ:O(n)-最悪の場合、バイナリツリーのすべてのノードにアクセスする可能性があります。
  • スペースの複雑さ:O(n)-親ポインターハッシュテーブル、ancestor_set、およびキューを使用するスペースは、それぞれO(n)になります。
class Solution
{
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q)
    {
        HashMap<TreeNode, TreeNode> parent_map = new HashMap<>();
        HashSet<TreeNode> ancestors_set = new HashSet<>();
        Queue<TreeNode> queue = new LinkedList<>();

        parent_map.put(root, null);
        queue.add(root);

        while(!parent_map.containsKey(p) || !parent_map.containsKey(q))
        {
            TreeNode node = queue.poll();

            if(node.left != null)
            {
                parent_map.put(node.left, node);
                queue.add(node.left);
            }
            if(node.right != null)
            {
                parent_map.put(node.right, node);
                queue.add(node.right);
            }
        }

        while(p != null)
        {
            ancestors_set.add(p);
            p = parent_map.get(p);
        }

        while(!ancestors_set.contains(q))
            q = parent_map.get(q);

        return q;
    }
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.