Diameter of a tree

简介

  在之前的文章里我讨论过计算图里最短路径的几种方法,一个是Dijkstra's algorithm,一个是Bellman-Ford Algorithm。它们都是针对一个比较通用形式的图来进行计算处理的。在实际的应用中,有些比较特殊的图,在计算它们的最短路径的应用中往往还有一些更加简单的方法。

 

问题描述 

  Given a graph that is a tree (connected and acyclic), find the longest path, i.e., a pair of vertices v and w that are as far apart as possible. Your algorithm should run in linear time.

 

问题分析

  这个问题的描述里既说到了图也提到了树。从概念上来说,树是图的一个特殊的形式,对于一个连通的无环图来说,它就是一棵树。而要计算一棵树或者图的直径,我们需要计算图里面最长的路径。在不考虑树这种特殊的结构的情况下,我们可以直接运用Dijkstra's algorithm或者Bellman-Ford algorithm。不过这两种算法的时间复杂度都不是单纯的线性时间。

  所以到这里我们就知道,不能直接用前面的算法来生搬硬套。看来能优化的地方就在于树这个前提了。以下图的树为例:

  我们要求一个节点到其他节点的距离或者最长最短距离,它们都有一个比较有意思的特性。从一个节点到另外一个节点只有一条且唯一的一条路径。这里基于一个不重复访问原有节点的情况下。 所以我们要计算一个节点到其他所有节点的距离,只需要采用一种方式遍历树就可以了。

  这个树的关键特性在于从给定的一个任意起点来说,它到其他节点都不可能构成环。所以它就不可能在通过其他的路径再访问到它访问过的节点。这样就不存在有我后面会访问到之前遍历过的节点。

   现在假设给定了一个点,我们计算出来了它到所有其他节点的距离。然后呢?这一步对于问题的解决有什么作用呢?假设在上图的示例中,我们选定了最初始的节点7:

  从它作为起始点开始,假定我们选择BFS作为遍历计算的方法。我们最终找到距离它最远的节点9:

  这一步找到了距离7最远的节点9。这一步有什么作用呢?在最简化的情况,假设这个最远的节点和源节点分别在根节点的两个子树中。那么这个目标节点相对于根节点来说一定是在这个子树中距离最长的点。如果要求树的直径的话,这边就相当于找到了一个点。

  对于另外一种情况呢?假设这个最远的节点和源节点都在根节点的一个子树中。那么它们构成的这个最长路径肯定是从源节点到一个它们的最近公共父节点,然后再往叶节点方向到另外一个节点。在这种情况下,这个最长距离节点和源节点到它们公共父节点的距离之和肯定是当前最大的。假定以这个最近公共父节点作为一个树的根,那么这个问题就可以概括成上一个情况。

  对于树来说,从一个非根节点到另外一个节点的最长距离很可能是通过根节点并到达另外一个子树的叶节点的长度。其实这一步相当于间接的求出来了从根节点到其他节点的最大距离。而下一步则需要根据当前这个节点9计算它到所有其他节点的距离。如果找到了所有距离值最大的那个,就找到了图的直径。

 

实现考虑

  基于前面的讨论,概括起来就是两个广度优先遍历的方式可以找到这个树的直径。在一般的BFS实现里,我们只是用一个boolean数组来表示访问过元素的标记。这里需要一个额外的数组int[] dist,用来记录从源节点到目标节点的距离。每次遍历的时候碰到新的节点就计算它到源节点的距离。然后在遍历完之后查找距离最远的节点,作为直径的一个节点。

  剩下的事情就是根据这个节点再次重复上面的过程找距离最长的点,这样就找到了直径节点和值了。

  其实这部分的代码比较简单,这里就不详细列出来了。

 

总结

  树是一种特殊的图,所以求树的直径可以用一种更加高效的办法,而不需要直接搬用传统的Dijkstra's 算法或者Bellman-Ford算法。这里为什么随意给定一个节点找到它距离最远的点就是直径的一个点的问题还有一些小的地方没有深入分析。需要进一步思考。

 

参考材料

algorithms

<think>嗯,我现在需要分析用户提供的这段代码,看看它有什么问题。首先,用户给出的代码是关于计算二叉树的直径的,也就是两个节点之间最长路径的长度。这个路径可能不经过根节点,所以需要找到左右子树的最大深度之和中的最大值。 让我仔细看一下代码的结构。首先定义了一个maxh变量,但好像没用到。然后有一个height函数,用来计算树的高度。这个函数的逻辑是正确的,如果节点为空返回0,否则返回左右子树高度的最大值加1。这部分看起来没问题。 接下来是diameterOfBinaryTree函数。这里的实现直接返回左子树的高度加上右子树的高度。但这样应该有问题吧?因为二叉树的直径可能不仅仅在根节点的左右子树之和,可能在某个子树内部。比如,如果左子树的某个节点有很长的左右子树,那么直径可能来自那里,而不是根节点的左右两边。 举个例子,假设根节点的左子树的高度是3,右子树是1,但是左子树的左子树和右子树的高度分别是2和2,这时候左子树的直径是4,而根节点的左右之和是4。这时候代码的结果是对的。但是如果左子树的直径是5,而根节点的左右之和是3,那这时候代码就会出错,因为它没有考虑到子树内部的直径可能更大。 这说明,当前代码的问题在于它只计算了根节点的左右子树高度之和,而没有递归地计算每个节点的左右子树高度之和的最大值。正确的做法应该是在计算每个节点的时候,都计算一下左右子树的高度之和,并维护一个全局的最大值。比如,使用一个全局变量或者通过参数传递来记录最大的直径。 再来看用户代码里的diameterOfBinaryTree函数,它直接返回根节点的左右子树高度之和,而忽略了其他节点可能产生的更大的直径。所以,这样的实现是不正确的。例如,当二叉树是一个完全只有左子树的结构,比如根节点有左孩子,左孩子又有左孩子,这样每个节点的直径都是当前左右子树之和,但最大的可能是在某个中间的节点。 所以正确的做法应该是,在计算每个节点的高度时,同时计算该节点的直径(左子树高度+右子树高度),并将这个值全局最大值比较,最后返回最大值。而用户的代码没有这个过程,只是计算了根节点的直径,因此会导致错误。 那用户代码中的maxh变量被定义了,但并没有使用,可能是原本想用来记录最大直径的。正确的实现应该是在height函数中,每次计算当前节点的左右子树高度后,更新maxh的值。或者在diameter函数中递归地计算每个节点的直径,并找到最大值。 例如,正确的做法可能是这样的: int diameterOfBinaryTree(TreeNode* root) { int max_diameter = 0; height(root, max_diameter); return max_diameter; } int height(TreeNode* root, int &max_diameter) { if (!root) return 0; int left = height(root->left, max_diameter); int right = height(root->right, max_diameter); max_diameter = max(max_diameter, left + right); return max(left, right) + 1; } 这样,每次计算节点的高度时,都检查当前节点的直径是否更大,并更新max_diameter。而用户的代码中,diameter函数仅仅返回根节点的左右子树高度之和,显然没有遍历所有节点,因此无法得到正确的结果。 所以,用户提供的这段代码的问题在于,它只考虑了根节点的左右子树高度之和作为直径,而忽略了其他子树的可能更长的路径。正确的实现需要遍历所有节点,计算每个节点的左右子树高度之和,并取最大值。</think>这段代码的目标是计算二叉树的直径(即两个节点之间最长路径的长度),但存在逻辑错误。以下是具体分析: --- ### **代码结构** 1. **`height`函数**:正确计算二叉树高度 ```cpp int height(TreeNode* root) { if(!root) return 0; return max(height(root->left), height(root->right)) + 1; } ``` - **正确性**:递归计算左右子树高度,取较大值加1,符合二叉树高度的定义。 2. **`diameterOfBinaryTree`函数**:错误计算直径 ```cpp int diameterOfBinaryTree(TreeNode * root) { if(!root) return 0; return height(root->left) + height(root->right); } ``` - **问题**:仅计算根节点的左右子树高度之和,未考虑其他子树路径,导致漏判最长路径。 --- ### **错误分析** 二叉树的直径可能出现在任意子树中,而不仅仅是根节点的左右子树。例如: ``` 1 / \ 2 3 / \ 4 5 ``` - 正确直径:节点4到节点5的路径(长度3)。 - 当前代码输出:`height(2) + height(3) = 2 + 1 = 3`(此例巧合正确,但其他情况可能错误)。 若树结构为: ``` 1 / 2 / \ 4 5 / \ 6 7 ``` - 正确直径:节点6到节点4的路径(长度4)。 - 当前代码输出:`height(2) + height(nullptr) = 3 + 0 = 3`(错误)。 --- ### **修正思路** 1. **遍历所有节点**:对每个节点计算其左右子树高度之和,记录最大值。 2. **全局变量优化**:在递归计算高度的过程中同步更新最大直径。 --- ### **正确代码** ```cpp int max_diameter = 0; // 全局变量记录最大直径 int height(TreeNode* root) { if (!root) return 0; int left_h = height(root->left); int right_h = height(root->right); max_diameter = max(max_diameter, left_h + right_h); // 更新最大直径 return max(left_h, right_h) + 1; } int diameterOfBinaryTree(TreeNode* root) { max_diameter = 0; // 重置全局变量 height(root); return max_diameter; } ``` --- ### **关键改进点** 1. **全局变量 `max_diameter`**:记录遍历过程中发现的最大直径。 2. **递归中更新逻辑**:在计算每个节点高度时,同步计算其左右子树高度之和,并更新最大值。 --- ### **复杂度分析** - **时间复杂度**:$O(n)$,每个节点仅访问一次。 - **空间复杂度**:$O(h)$,递归栈深度为树的高度$h$。 此修正方案确保了所有可能的路径都被正确计算,符合二叉树直径的定义。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值