zoukankan      html  css  js  c++  java
  • 《编程之美: 求二叉树中节点的最大距离》的另一个解法

    昨天花了一个晚上为《编程之美》,在豆瓣写了一篇书评《迟来的书评和感想──给喜爱编程的朋友》。书评就不转载到这里了,取而代之,在这里介绍书里其中一条问题的另一个解法。这个解法比较简短易读及降低了空间复杂度,或者可以说觉得比较「美」吧。

    问题定义

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

    书上的解法

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

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

    • 情况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;
    }
    

    你想到更好的解法吗?

  • 相关阅读:
    【Javascript】javascript学习 二十二 JavaScript 对象简介
    【Javascript】javascript学习 二十六 JavaScript Boolean(逻辑)对象
    【Javascript】javascript学习 二十九 JavaScript HTML DOM 对象
    【Javascript】javascript学习 二十八 JavaScript RegExp 对象
    【Javascript】javascript学习 二十一 JavaScript 指导方针
    【Javascript】javascript学习 二十三 JavaScript 字符串(String)对象
    【Javascript】javascript学习 三十 JavaScript 浏览器检测
    【Javascript】javascript学习 二十五 JavaScript Array(数组)对象
    【Javascript】javascript学习 二十四 JavaScript Date(日期)对象
    【Javascript】javascript学习 二十七 JavaScript Math(算数)对象
  • 原文地址:https://www.cnblogs.com/miloyip/p/binary_tree_distance.html
Copyright © 2011-2022 走看看