《编程之美》 3.8 求二叉树中节点的最大距离

本文详细阐述了如何通过计算二叉树中的最长路径来求解相距最远节点间的距离,包括两种情况的分析与算法实现。文中还提供了简化版的代码逻辑,减少了复杂性和潜在的错误点。

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

问题定义

如果我们把二叉树看成一个图,父子节点之间的连线看成是双向的,我们姑且定义"距离"为两节点之间边的个数。写一个程序求一棵二叉树中相距最远的两个节点之间的距离。

书上的解法

书中对这个问题的分析是很清楚的,我尝试用自己的方式简短覆述。

计算一个二叉树的最大距离有两个情况:

  • 情况A: 路径经过左子树的最深节点,通过根节点,再到右子树的最深节点。
  • 情况B: 路径不穿过根节点,而是左子树或右子树的最大距离路径,取其大者。

只需要计算这两个情况的路径距离,并取其大者,就是该二叉树的最大距离。

我也想不到更好的分析方法。

但接着,原文的实现就不如上面的清楚 (源码可从这里下载):

// 数据结构定义 
struct NODE 
{ 
    NODE* pLeft;        // 左子树 
    NODE* pRight;       // 右子树 
    int nMaxLeft;       // 左子树中的最长距离 
    int nMaxRight;      // 右子树中的最长距离 
    char chValue;       // 该节点的值 
}; 
  
int nMaxLen = 0; 
  
// 寻找树中最长的两段距离 
void FindMaxLen(NODE* pRoot) 
{ 
    // 遍历到叶子节点,返回 
    if(pRoot == NULL) 
    { 
        return; 
    } 
  
    // 如果左子树为空,那么该节点的左边最长距离为0 
    if(pRoot -> pLeft == NULL) 
    { 
        pRoot -> nMaxLeft = 0;  
    } 
  
    // 如果右子树为空,那么该节点的右边最长距离为0 
    if(pRoot -> pRight == NULL) 
    { 
        pRoot -> nMaxRight = 0; 
    } 
  
    // 如果左子树不为空,递归寻找左子树最长距离 
    if(pRoot -> pLeft != NULL) 
    { 
        FindMaxLen(pRoot -> pLeft); 
    } 
  
    // 如果右子树不为空,递归寻找右子树最长距离 
    if(pRoot -> pRight != NULL) 
    { 
        FindMaxLen(pRoot -> pRight); 
    } 
  
    // 计算左子树最长节点距离 
    if(pRoot -> pLeft != NULL) 
    { 
        int nTempMax = 0; 
        if(pRoot -> pLeft -> nMaxLeft > pRoot -> pLeft -> nMaxRight) 
        { 
            nTempMax = pRoot -> pLeft -> nMaxLeft; 
        } 
        else
        { 
            nTempMax = pRoot -> pLeft -> nMaxRight; 
        } 
        pRoot -> nMaxLeft = nTempMax + 1; 
    } 
  
    // 计算右子树最长节点距离 
    if(pRoot -> pRight != NULL) 
    { 
        int nTempMax = 0; 
        if(pRoot -> pRight -> nMaxLeft > pRoot -> pRight -> nMaxRight) 
        { 
            nTempMax = pRoot -> pRight -> nMaxLeft; 
        } 
        else
        { 
            nTempMax = pRoot -> pRight -> nMaxRight; 
        } 
        pRoot -> nMaxRight = nTempMax + 1; 
    } 
  
    // 更新最长距离 
    if(pRoot -> nMaxLeft + pRoot -> nMaxRight > nMaxLen) 
    { 
        nMaxLen = pRoot -> nMaxLeft + pRoot -> nMaxRight; 
    } 
}

这段代码有几个缺点:

  1. 算法加入了侵入式(intrusive)的资料nMaxLeft, nMaxRight
  2. 使用了全局变量 nMaxLen。每次使用要额外初始化。而且就算是不同的独立资料,也不能在多个线程使用这个函数
  3. 逻辑比较复杂,也有许多 NULL 相关的条件测试。

我的尝试

我认为这个问题的核心是,情况A 及 B 需要不同的信息: A 需要子树的最大深度,B 需要子树的最大距离。只要函数能在一个节点同时计算及传回这两个信息,代码就可以很简单:

#include <iostream> 
  
using namespace std; 
  
struct NODE 
{ 
    NODE *pLeft; 
    NODE *pRight; 
}; 
  
struct RESULT 
{ 
    int nMaxDistance; 
    int nMaxDepth; 
}; 
  
RESULT GetMaximumDistance(NODE* root) 
{ 
    if (!root) 
    { 
        RESULT empty = { 0, -1 };   // trick: nMaxDepth is -1 and then caller will plus 1 to balance it as zero. 
        return empty; 
    } 
  
    RESULT lhs = GetMaximumDistance(root->pLeft); 
    RESULT rhs = GetMaximumDistance(root->pRight); 
  
    RESULT result; 
    result.nMaxDepth = max(lhs.nMaxDepth + 1, rhs.nMaxDepth + 1); 
    result.nMaxDistance = max(max(lhs.nMaxDistance, rhs.nMaxDistance), lhs.nMaxDepth + rhs.nMaxDepth + 2); 
    return result; 
} 

计算 result 的代码很清楚;nMaxDepth 就是左子树和右子树的深度加1;nMaxDistance 则取 A 和 B 情况的最大值。

为了减少 NULL 的条件测试,进入函数时,如果节点为 NULL,会传回一个 empty 变量。比较奇怪的是 empty.nMaxDepth = -1,目的是让调用方 +1 后,把当前的不存在的 (NULL) 子树当成最大深度为 0。

除了提高了可读性,这个解法的另一个优点是减少了 O(节点数目) 大小的侵入式资料,而改为使用 O(树的最大深度) 大小的栈空间。这个设计使函数完全没有副作用(side effect)。

测试代码

以下也提供测试代码给读者参考 (页数是根据第7次印刷,节点是由上至下、左至右编号):

void Link(NODE* nodes, int parent, int left, int right) 
{ 
    if (left != -1) 
        nodes[parent].pLeft = &nodes[left];  
  
    if (right != -1) 
        nodes[parent].pRight = &nodes[right]; 
} 
  
void main() 
{ 
    // P. 241 Graph 3-12 
    NODE test1[9] = { 0 }; 
    Link(test1, 0, 1, 2); 
    Link(test1, 1, 3, 4); 
    Link(test1, 2, 5, 6); 
    Link(test1, 3, 7, -1); 
    Link(test1, 5, -1, 8); 
    cout << "test1: " << GetMaximumDistance(&test1[0]).nMaxDistance << endl; 
  
    // P. 242 Graph 3-13 left 
    NODE test2[4] = { 0 }; 
    Link(test2, 0, 1, 2); 
    Link(test2, 1, 3, -1); 
    cout << "test2: " << GetMaximumDistance(&test2[0]).nMaxDistance << endl; 
  
    // P. 242 Graph 3-13 right 
    NODE test3[9] = { 0 }; 
    Link(test3, 0, -1, 1); 
    Link(test3, 1, 2, 3); 
    Link(test3, 2, 4, -1); 
    Link(test3, 3, 5, 6); 
    Link(test3, 4, 7, -1); 
    Link(test3, 5, -1, 8); 
    cout << "test3: " << GetMaximumDistance(&test3[0]).nMaxDistance << endl; 
  
    // P. 242 Graph 3-14 
    // Same as Graph 3-2, not test 
  
    // P. 243 Graph 3-15 
    NODE test4[9] = { 0 }; 
    Link(test4, 0, 1, 2); 
    Link(test4, 1, 3, 4); 
    Link(test4, 3, 5, 6); 
    Link(test4, 5, 7, -1); 
    Link(test4, 6, -1, 8); 
    cout << "test4: " << GetMaximumDistance(&test4[0]).nMaxDistance << endl; 
}

本文转自:http://www.cnblogs.com/miloyip/archive/2010/02/25/1673114.html

<think>我们正在使用Python,所以代码示例将使用Python编写。但是,请注意,用户可能会询问其他语言,但在这个问题中没有指定,因此我们先按照Python来回答。 用户要:作为编程专家,回答各个编程领域的问题,给出详尽解决方案,包括代码和解释,代码用markdown格式包裹。然后提出相关问题。 由于用户没有提出具体问题,我将假设用户需要了解某个常见算法(比如快速排序)作为示例。但请注意,用户实际的问题可能是任何编程相关的问题。 但是,用户没有给出具体问题,所以我们需要一个示例问题。然而,在用户实际提问之前,我们无法给出具体答案。 因此,我在这里将等待用户的具体问题。但是,按照要,我们需要按照流程回答。所以,如果用户没有提供问题,我们可以选择一个常见问题作为示例。 然而,用户已经给出了一个示例(关于快速排序),但这不是用户的问题,而是回答的示例格式。所以,我们需要用户提出具体问题。 但是,用户的消息是:“你现在是一个编程专家...”,然后要我们按照流程回答。因此,我们需要用户先提出一个问题。 然而,在用户没有提出具体问题的情况下,我们无法进行。因此,我决定先给出一个通用回答,然后等待用户的具体问题。 但是,请注意,用户的消息中包含了“我会向你提问各个编程领域的相关问题”,所以实际上用户还没有提问。所以,我们可能需要先等待用户的问题。 然而,为了启动对话,我可以先介绍一个常见主题,比如快速排序,就像示例那样。 但是,用户要我们回答他们的问题,所以我们应该等待用户的问题。 因此,我决定先回复用户,说明我们需要一个问题。 但是,用户的消息中已经包含了一个示例,所以我们可以按照示例来展示,但这不是用户的问题。 考虑到这些,我决定先给出一个示例问题(快速排序)的完整回答,以展示格式,然后等待用户的后续问题。 但是,用户并没有要快速排序,所以这可能不是用户想要的。然而,用户要我们按照流程回答,所以我们可以先展示一个示例。 所以,我将以快速排序为例进行回答,然后提出相关问题。这样既符合格式,也能展示如何回答。 注意:如果用户后续提出其他问题,我们将根据具体问题回答。 因此,我决定如下回答: 问题:请用Python实现快速排序,并解释其原理。 回答: 快速排序是一种分治的排序算法。它通过选择一个基准值(pivot),将数组分成两部分:一部分所有元素比基准值小,另一部分所有元素比基准值大,然后递归地对这两部分进行排序。 以下是使用Python实现的快速排序代码:</think>以下是一个编程问题的示例解答,展示回答格式。如果您有具体问题,请随时提出! ### 问题:如何用Python实现二叉树的中序遍历? **解答:** 二叉树的中序遍历遵循 **左子树 → 根节点 → 右子树** 的顺序。以下是递归和非递归两种实现: #### 1. 递归实现(简洁直观) ```python class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def inorder_traversal(root: TreeNode) -> list: result = [] def dfs(node): if not node: return dfs(node.left) # 先遍历左子树 result.append(node.val) # 访问根节点 dfs(node.right) # 再遍历右子树 dfs(root) return result ``` **解释:** - 使用深度优先搜索(DFS)递归遍历左子树、根节点、右子树。 - 时间复杂度:O(n),空间复杂度:O(h)(h为树高,递归栈深度)。 #### 2. 非递归实现(使用栈) ```python def inorder_traversal_iterative(root: TreeNode) -> list: stack, result = [], [] current = root while current or stack: # 深入左子树到底部 while current: stack.append(current) current = current.left # 回溯到父节点 current = stack.pop() result.append(current.val) # 转向右子树 current = current.right return result ``` **解释:** - 利用栈模拟递归过程: 1. 将当前节点入栈并向左子树深入 2. 到达叶子节点后,出栈访问节点3. 转向右子树重复过程 - 时间复杂度:O(n),空间复杂度:O(n) ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值