【转载】二叉树中两个节点的最近公共祖先节点

本文详细介绍了求解二叉树中两个节点的最近公共祖先(LCA)问题的不同算法,包括针对二叉查找树的简单算法、适用于一般二叉树的离线算法(Tarjan算法)和在线算法(RMQ算法),以及后序遍历的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

【以下内容为转载,部分注释为本人补注。原文地址:二叉树中两个节点的最近公共祖先 - eriol - ITeye博客

求二叉树中任意两个节点的最近公共祖先也称为LCA问题(Lowest Common Ancestor)。

二叉查找树

如果该二叉树是二叉查找树,那么求解LCA十分简单。

基本思想为:从树根开始,该节点的值为t,如果t大于t1和t2,说明t1和t2都位于t的左侧,所以它们的共同祖先必定在t的左子树中,从t.left开始搜索;如果t小于t1和t2,说明t1和t2都位于t的右侧,那么从t.right开始搜索;如果t1<t< t2,说明t1和t2位于t的两侧,那么该节点t为公共祖先。

如果t1是t2的祖先,那么应该返回t1的父节点;同理,如果t2是t1的祖先,应该返回t2的父节点。

 这里其实没有太大必要,直接返回t1 或者 t2即可。。。

Java代码   收藏代码
  1. public int query(Node t1, Node t2, Node t) {  
  2.     int left = t1.value;  
  3.     int right = t2.value;  
  4.     Node parent = null;  
  5.           
  6.     if (left > right) {  
  7.         int temp = left;  
  8.         left = right;  
  9.         right = temp;  
  10.     }  
  11.           
  12.     while (true) {  
  13.         if (t.value < left) {  
  14.             parent = t;  
  15.             t = t.right;  
  16.         } else if (t.value > right) {  
  17.             parent = t;  
  18.             t = t.left;  
  19.         } else if (t.value == left || t.value == right) {  
  20.             return parent.value;  
  21.         } else {  
  22.             return t.value;  
  23.         }  
  24.     }  
  25. }  

    其中,parent用于处理t1是t2的祖先(或t2是t1的祖先)的情况。

一般的二叉树

如果二叉树不是二叉查找树该怎么办呢?

1. 离线算法(Tarjan)

利用并查集优越的时空复杂度,可以实现O(n+q)的算法,q是查询次数。

Tarjan算法基于深度优先搜索。对于新搜索到的一个结点u,先创建由u构成的集合,再对u的每颗子树进行搜索,每搜索完一棵子树,这时候子树中所有的结点的最近公共祖先就是u了。

#include <iostream>
#include <stdio.h>

#define N 100
class Solution {
  public:
  int id[N], lcs[N][N], g[N][N];
  void init() {
    memset(id, -1, sizeof(id));
    memset(g, 0, sizeof(g));
    memset(lcs,0,sizeof(lcs));
    g[1][2] = 1;
    g[1][3] = 1;
    g[2][4] = 1;
    g[2][5] = 1;
    g[3][6] = 1;
    g[3][7] = 1;
    g[7][8] = 1;
    
  }
  int find(int i) {
    return id[i] == i ? id[i] : (id[i] = find(id[i]));
  }
  void merge(int i, int j) {
    id[find(i)] = find(id[j]);
  }
  void dfs(int rt, int n) {
    int i;
    id[rt] = rt;
    for (i = 1; i <= n; ++i)
    if (g[rt][i] && id[i] == -1) {
      dfs(i,n);
      id[find(i)] = find(rt);
      //id[find(rt)] = find(id[i]); //Not right!!!!
    }
    for (i = 1; i <= n; ++i)
    if (id[i] != -1)
    lcs[rt][i] = lcs[i][rt] = find(i);
  }
};

int main() {
  Solution s;
  s.init();
  s.dfs(1, 8);
  return 0;
}

2. 在线算法(RMQ)

一个O(nlog2n)的预处理,O(1)的查询。

以下面一棵树为例:

           (1)
         /     \
       (2)     (7)
      /   \       \
    (3)  (4)     (8)
          /   \
         (5)  (6)

step1:


    按深度遍历树,记录访问顺序和相应的深度(2*n-1),及每个节点第一次出现的位置。


    结点的访问顺序为:1 2 3 2 4 5 4 6 4 2 1 7 8 7
    相应的深度为:    0 1 1 2 3 2 3 2 1 0 1 2 1 0
    结点1-8第一次出现的次序为:1 2 3 5 6 8 12 13

step2:

    查询3和6的公共祖先,考虑3和6第一次出现的位置为3和8,即寻找序列2 1 2 3 2 3中的最小值,最小值为1,对应的点位2,则3与6的最近公共祖先为2。

step3:

    则对于给出的任意两个结点,找出它们第一次出现的位置i,j(i<j),在深度序列中查询最小值的下标k,depth[k]即为所求。显然,查询多次深度序列中的最小值的下标,自然而然就想到了RMQ。

Java代码   收藏代码
  1. public class LCA {  
  2.       
  3.     private final int MAX = 10;  
  4.     private int[] dfs = new int[2*MAX];  
  5.     private int[] depth = new int[2*MAX];  
  6.     private int[][] f;  
  7.     private int[] call = new int[MAX];  
  8.     private int len = 0;  
  9.       
  10.     public void track(Node t, int d) {  
  11.         if (t == null)  
  12.             return;  
  13.         dfs[len] = t.value;  
  14.         depth[len] = d;  
  15.         call[t.value-1] = len;  
  16.         len++;  
  17.           
  18.         track2(t.left, d+1);  
  19.         if (t.left != null) {  
  20.             dfs[len] = t.value;  
  21.             depth[len] = d;  
  22.             len++;  
  23.         }  
  24.           
  25.         track2(t.right, d+1);  
  26.         if (t.right != null) {  
  27.             dfs[len] = t.value;  
  28.             depth[len] = d;  
  29.             len++;  
  30.         }  
  31.     }  
  32.       
  33.     public void rmq() {  
  34.         int count = 1;  
  35.         while ((1 << count) <= len)  
  36.             count++;  
  37.         f = new int[len][count];  
  38.         count--;  
  39.           
  40.         for (int i = 0; i < len; i++) {  
  41.             f[i][0] = i;  
  42.         }  
  43.           
  44.         for (int j = 1; (1 << j) <= len; j++) {  
  45.             for (int i = 0; i+(1<<j)-1 < len; i++) {  
  46.                 f[i][j] = depth[f[i][j-1]] < depth[f[i+(1<<j-1)][j-1]] ?   
  47.                         f[i][j-1] : f[i+(1<<j-1)][j-1];  
  48.             }  
  49.         }  
  50.     }  
  51.       
  52.     public int query(Node t1, Node t2) {  
  53.         int start = call[t1.value-1];  
  54.         int end = call[t2.value-1];  
  55.           
  56.         if(start > end) {  
  57.             int temp = start;  
  58.             start = end;  
  59.             end = temp;  
  60.         }  
  61.           
  62.         int count = 1;  
  63.         while ((1 << count) <= end - start + 1)   
  64.             count++;  
  65.         count--;  
  66.         int result = depth[f[start][count]] < depth[f[end-(1<<count)+1][count]] ?  
  67.                 f[start][count] : f[end-(1<<count)+1][count];  
  68.           
  69.         if (dfs[result] == t1.value || dfs[result] == t2.value) {  
  70.             int temp = depth[result];  
  71.             while (depth[result] >= temp)  
  72.                 result--;  
  73.         }  
  74.         return dfs[result];   
  75.     }  
  76.       
  77.     public static void main(String[] args) {  
  78.         Node n3 = new Node(3);  
  79.         Node n5 = new Node(5);  
  80.         Node n6 = new Node(6);  
  81.         Node n4 = new Node(4, n5, n6);  
  82.         Node n2 = new Node(2, n3, n4);  
  83.         Node n8 = new Node(8);  
  84.         Node n7 = new Node(7null, n8);  
  85.         Node n1 = new Node(1, n2, n7);  
  86.           
  87.         LCA l = new LCA();  
  88.         l.track(n1, 0);  
  89.         l.rmq();  
  90.   
  91.         System.out.println(l.query(n5, n4));      
  92.     }  
  93. }  
  94.   
  95. class Node {  
  96.     int value;  
  97.     Node left;  
  98.     Node right;  
  99.       
  100.     public Node(int value, Node left, Node right) {  
  101.         this.value = value;  
  102.         this.left = left;  
  103.         this.right = right;  
  104.     }  
  105.       
  106.     public Node(int value) {  
  107.         this.value = value;  
  108.         this.left = null;  
  109.         this.right = null;  
  110.     }  
  111. }  

3. 后序遍历

基本思想:如果这两个节点不在一条线上(即这两个节点不存在一个节点是另一个节点的祖先的情况),则它们必定分别在所求节点A的左子树和右子树上,后序遍历到第一个满足这个条件的节点就是所要求的节点A。否则,当这两个节点在一条线上,所求节点A则是这两个节点中深度最低的节点的父节点。

Cpp代码   收藏代码
  1. bool lca(Node *root, int va, int vb, Node *&result, Node* parent)  
  2. {  
  3.     // left/right 左/右子树是否含有要判断的两节点之一   
  4.     bool left = false, right = false;  
  5.     if (!result && root->left) left = lca(root->left,va,vb,result,root);  
  6.     if (!result && root->right) right = lca(root->right,va,vb,result,root);  
  7.   
  8.     // mid 当前节点是否是要判断的两节点之一   
  9.     bool mid = false;  
  10.     if (root->data == va || root->data == vb) mid=true;  
  11.     if (!result && int(left + right + mid) == 2) {  // 补注,如果left+right+mid等于2,说明,要么当前节点root就是要找的节点之一,并且另一节点在它的左或者右子树中;要么当前节点是的左右子树分别包含了要找的两个节点
  12.         if (mid) result = parent;  // 补注,如果当前节点是要找的节点之一,则其父节点就是公共节点
  13.         else result = root;  // 补注,否则当前节点就是公共节点
  14.     }  
  15.     return left | mid | right ;  // 补注,有一个满足就返回true
  16. }  
  17.   
  18. Node *query(Node *root,int va, int vb)  
  19. {  
  20.     if (root == NULL) return NULL;  
  21.     Node *result = NULL;  
  22.     lca(root, va, vb,result, NULL);  
  23.     return result;  
  24. }  
二叉树:来自leetcode:
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
}

N叉树:
class Node {
 public:
  int val;
  vector<Node*> child;
};
Node* commonAncestor(Node* root, Node* p1, Node* p2) {
  if (root == NULL || p1 == NULL || p2 == NULL)  return root;
  if (root == p1 || root == p2)  return root;
  int cnum = root->child.size(), count = 0;
  Node* tmp = NULL;
  
  for (int i = 0; i < cnum; ++i) {
    tmp = commonAncestor(root->child[i], p1, p2);
    if (tmp != NULL)
      ++count;
    if (count == 2)
      return root;
  }
  return tmp; 
}

这个题递归坑的点在于if (root == NULL || p1 == NULL || p2 == NULL)递归是查p1或p2在树中的节点,但最终返回的commonAncestor,所以递归过程中只用了一个返回值来实现,如果用2个返回值会被绕晕。但这一个返回值假设p1或者p2不在树中,没法给出正确的返回

如果p1或者p2不在树中,用个dict来非递归吧!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值