二叉树两结点的最低共同父结点[数据结构]

本文介绍了二叉树中查找两个节点的最低共同父结点的方法,包括二叉查找树的特殊情况和一般情况。通过递归函数HasNode判断子树中是否包含指定节点,并采用两种方法实现:一种是基于二叉查找树的性质,另一种则是将问题转化为求两个链表的公共节点。最后,通过路径获取和公共节点查找实现了高效算法,改进了时间复杂度。

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


题目:二叉树的结点定义如下:
struct TreeNode
{
      int m_nvalue;
      TreeNode* m_pLeft;
      TreeNode* m_pRight;
};
输入二叉树中的两个结点,输出这两个结点在数中最低的共同父结点。


分析:求数中两个结点的最低共同结点是面试中经常出现的一个问题。这个问题至少有两个变种。

第一变种是二叉树是一种特殊的二叉树:查找二叉树。也就是树是排序过的,位于左子树上的结点都比父结点小,而位于右子树的结点都比父结点大。我们只需要从根结点开始和两个结点进行比较。如果当前结点的值比两个结点都大,则最低的共同父结点一定在当前结点的左子树中。如果当前结点的值比两个结点都小,则最低的共同父结点一定在当前结点的右子树中。

第二个变种是树不一定是二叉树,每个结点都有一个指针指向它的父结点。于是我们可以从任何一个结点出发,得到一个到达树根结点的单向链表。因此这个问题转换为两个单向链表的第一个公共结点。我们在本面试题系列的第35题讨论了这个问题。

现在我们回到这个问题本身。所谓共同的父结点,就是两个结点都出现在这个结点的子树中。因此我们可以定义一函数,来判断一个结点的子树中是不是包含了另外一个结点。这不是件很难的事,我们可以用递归的方法来实现:

/////////////////////////////////////////////////////////////////////////////////

// If the tree with head pHead has a node pNode, return true.

// Otherwise return false.

/////////////////////////////////////////////////////////////////////////////////

bool HasNode(TreeNode* pHead, TreeNode* pNode)

{

    if(pHead == pNode)

        return true;

 

    bool has = false;

 

    if(pHead->m_pLeft != NULL)

        has = HasNode(pHead->m_pLeft, pNode);

 

    if(!has && pHead->m_pRight != NULL)

        has = HasNode(pHead->m_pRight, pNode);

 

    return has;

}

我们可以从根结点开始,判断以当前结点为根的树中左右子树是不是包含我们要找的两个结点。如果两个结点都出现在它的左子树中,那最低的共同父结点也出现在它的左子树中。如果两个结点都出现在它的右子树中,那最低的共同父结点也出现在它的右子树中。如果两个结点一个出现在左子树中,一个出现在右子树中,那当前的结点就是最低的共同父结点。基于这个思路,我们可以写出如下代码:

/////////////////////////////////////////////////////////////////////////////////

// Find the last parent of pNode1 and pNode2 in a tree with head pHead

/////////////////////////////////////////////////////////////////////////////////

TreeNode* LastCommonParent_1(TreeNode* pHead, TreeNode* pNode1, TreeNode* pNode2)

{

    if(pHead == NULL || pNode1 == NULL || pNode2 == NULL)

        return NULL;

 

    // check whether left child has pNode1 and pNode2

    bool leftHasNode1 = false;

    bool leftHasNode2 = false;

    if(pHead->m_pLeft != NULL)

    {

        leftHasNode1 = HasNode(pHead->m_pLeft, pNode1);

        leftHasNode2 = HasNode(pHead->m_pLeft, pNode2);

    }

 

    if(leftHasNode1 && leftHasNode2)

    {

        if(pHead->m_pLeft == pNode1 || pHead->m_pLeft == pNode2)

            return pHead;

 

        return LastCommonParent_1(pHead->m_pLeft, pNode1, pNode2);

    }

 

    // check whether right child has pNode1 and pNode2

    bool rightHasNode1 = false;

    bool rightHasNode2 = false;

    if(pHead->m_pRight != NULL)

    {

        if(!leftHasNode1)

            rightHasNode1 = HasNode(pHead->m_pRight, pNode1);

        if(!leftHasNode2)

            rightHasNode2 = HasNode(pHead->m_pRight, pNode2);

    }

 

    if(rightHasNode1 && rightHasNode2)

    {

        if(pHead->m_pRight == pNode1 || pHead->m_pRight == pNode2)

            return pHead;

 

        return LastCommonParent_1(pHead->m_pRight, pNode1, pNode2);

    }

 

    if((leftHasNode1 && rightHasNode2)

        || (leftHasNode2 && rightHasNode1))

        return pHead;

 

    return NULL;

}

接着我们来分析一下这个方法的效率。函数HasNode的本质就是遍历一棵树,其时间复杂度是O(n)n是树中结点的数目)。由于我们根结点开始,要对每个结点调用函数HasNode。因此总的时间复杂度是O(n2)

我们仔细分析上述代码,不难发现我们判断以一个结点为根的树是否含有某个结点时,需要遍历树的每个结点。接下来我们判断左子结点或者右结点为根的树中是否含有要找结点,仍然需要遍历。第二次遍历的操作其实在前面的第一次遍历都做过了。由于存在重复的遍历,本方法在时间效率上肯定不是最好的。

前面我们提过如果结点中有一个指向父结点的指针,我们可以把问题转化为求两个链表的共同结点。现在我们可以想办法得到这个链表。我们在本面试题系列的第4题中分析过如何得到一条中根结点开始的路径。我们在这里稍作变化即可:

/////////////////////////////////////////////////////////////////////////////////

// Get the path form pHead and pNode in a tree with head pHead

/////////////////////////////////////////////////////////////////////////////////

bool GetNodePath(TreeNode* pHead, TreeNode* pNode, std::list<TreeNode*>& path)

{

    if(pHead == pNode)

        return true;

 

    path.push_back(pHead);

 

    bool found = false;

    if(pHead->m_pLeft != NULL)

        found = GetNodePath(pHead->m_pLeft, pNode, path);

    if(!found && pHead->m_pRight)

        found = GetNodePath(pHead->m_pRight, pNode, path);

 

    if(!found)

        path.pop_back();

 

    return found;

}

由于这个路径是从跟结点开始的。最低的共同父结点就是路径中的最后一个共同结点:

/////////////////////////////////////////////////////////////////////////////////

// Get the last common Node in two lists: path1 and path2

/////////////////////////////////////////////////////////////////////////////////

TreeNode* LastCommonNode

(

    const std::list<TreeNode*>& path1,

    const std::list<TreeNode*>& path2

)

{

    std::list<TreeNode*>::const_iterator iterator1 = path1.begin();

    std::list<TreeNode*>::const_iterator iterator2 = path2.begin();

   

    TreeNode* pLast = NULL;

 

    while(iterator1 != path1.end() && iterator2 != path2.end())

    {

        if(*iterator1 == *iterator2)

            pLast = *iterator1;

 

        iterator1++;

        iterator2++;

    }

 

    return pLast;

}

有了前面两个子函数之后,求两个结点的最低共同父结点就很容易了。我们先求出从根结点出发到两个结点的两条路径,再求出两条路径的最后一个共同结点。代码如下:

/////////////////////////////////////////////////////////////////////////////////

// Find the last parent of pNode1 and pNode2 in a tree with head pHead

/////////////////////////////////////////////////////////////////////////////////

TreeNode* LastCommonParent_2(TreeNode* pHead, TreeNode* pNode1, TreeNode* pNode2)

{

    if(pHead == NULL || pNode1 == NULL || pNode2 == NULL)

        return NULL;

 

    std::list<TreeNode*> path1;

    GetNodePath(pHead, pNode1, path1);

 

    std::list<TreeNode*> path2;

    GetNodePath(pHead, pNode2, path2);

 

    return LastCommonNode(path1, path2);

}

这种思路的时间复杂度是O(n),时间效率要比第一种方法好很多。但同时我们也要注意到,这种思路需要两个链表来保存路径,空间效率比不上第一个方法。


### 关于吉林大学数据结构课程中的二叉树结点及其父结点 在吉林大学的数据结构课程中,对于二叉树查找特定结点以及其父结点的操作是一个重要的知识点。此操作不仅涉及基本的遍历算法,还涉及到如何记录路径上的节点信息以便找到目标节点的同时也能够定位到它的直接前驱——即父节点。 当处理此类问题时,通常会采用递归方法来实现这一功能。下面给出了一种基于Python语言的具体实施方案: ```python class TreeNode: def __init__(self, value=0, left=None, right=None): self.value = value self.left = left self.right = right def find_node_and_parent(root, target_value, parent=None): """在一个二叉树中寻找指定值的目标节点及其父节点""" # 如果当前子树为空或者找到了目标节点则返回结果 if root is None or root.value == target_value: return (root, parent) # 首先尝试在左子树中查找 found_left = find_node_and_parent(root.left, target_value, root) if found_left[0]: return found_left # 若未果,则继续探索右子树 return find_node_and_parent(root.right, target_value, root) # 构建测试用例所描述的二叉树实例 tree_structure = [ 1, [5, [8], []], [[3], [], [6]] ] def build_tree_from_list(lst): """根据列表构建二叉树""" if not lst: return None val = lst.pop(0) node = TreeNode(val) if val != 0 else None children = [] while lst and isinstance(lst[0], list): child_lst = lst.pop(0) children.append(build_tree_from_list(child_lst)) if len(children)>0: node.left = children[0] if len(children)>1: node.right = children[-1] return node test_tree = build_tree_from_list(tree_structure.copy()) target_val = int(input("请输入想要查询的节点值:")) result = find_node_and_parent(test_tree, target_val)[1] if result is None: print(f"{target_val} 的父节点不存在或为根节点.") else: print(f"{target_val} 的父节点是 {result.value}.") ``` 上述代码实现了从给定的二叉树结构中查找特定数值对应的节点,并同时获取该节点的父亲节点的功能[^1]。通过这种方式可以有效地完成题目要求的任务,在实际应用过程中可以根据具体需求调整输入输出形式以适应不同的场景[^2]。 为了更好地理解这个过程,建议学习者关注以下几个方面: - 如何定义二叉树数据结构; - 掌握递归函数的设计思路; - 学习如何利用额外参数传递上下文信息(如这里的`parent`变量)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值