简单的算法题目往往败于细节

273 阅读1分钟

问题

今天像往常一样,我随机写了一道算法题目。原题如下

给定一个二叉树,判断其是否是一个有效的二叉搜索树。

假设一个二叉搜索树具有如下特征:

节点的左子树只包含小于当前节点的数。 节点的右子树只包含大于当前节点的数。 所有左子树和右子树自身必须也是二叉搜索树。 示例 1:

输入:
    2
   / \
  1   3
输出: true
示例 2:

输入:
    5
   / \
  1   4
     / \
    3   6
输出: false
解释: 输入为: [5,1,4,null,null,3,6]。
     根节点的值为 5 ,但是其右子节点值为 4 。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/validate-binary-search-tree

这道题我能想到的最简单的方法就是递归(中序遍历)进行判断。问题在哪里尼,首先贴上我的第一版本的java代码。


class Solution {

    long last = Long.MIN_VALUE;

    public boolean isValidBST(TreeNode root) {
        
        if (root == null) {
            return true;
        }
        
        if (!isValidBST(root.left)) {
            return false;
        }
        
        if (root.val < last) {
            return false;
        }
        this.last = root.val;
        
        return isValidBST(root.right);
    }
}
    

一眼看上去,似乎判断都对,返回也是对的。在测试用例跑的时候,发现【1,1】这种输入的时候返回了true.哦,原来是边界条件搞错了,我大意了哈(哈哈哈哈),没有仔细考虑。

 if (root.val <= last) {
 	return false;
 }

在用golang再次写这个题目的时候,又发现自己犯了一个低级错误,照样先放原来的代码


func isValidBST(root *TreeNode) bool {
	return validBST(root, math.MinInt64)
}

func validBST(root *TreeNode, prev int)  bool {
	if root != nil {
		validBST(root.Left, prev)
		if root.Val <= prev {
			return false
		}
		prev = root.Val
		validBST(root.Right, prev)
	}
	return true
}

大家有发现代存在的问题么,这里可以思考一下。

如果没发现的话,那我来讲讲,献丑了。首先递归的时候,每次的递归栈都会保存一份单独的prev,所以prev在后面的调用更新之后,之后的迭代当中未必更新了。所以可想而知,这个代码结果错误了。

那么正确的代码怎么书写尼,代码放在下面:

func isValidBST(root *TreeNode) bool {
    prev := math.MinInt64
    return validate(root, &prev)
}

func validate(root *TreeNode, prev *int)  bool {
    if root == nil {
        return true
    }

    if validate(root.Left, prev) == false {
        return false;
    }

    if root.Val <= *prev {
        return false;
    }
    
    *prev = root.Val

    return validate(root.Right, prev)

总结

好了,总结的时刻到了,我们在书写递归代码的时候必须有三个注意。第一,返回条件的书写(很可能造成无限递归)。第二: 临界条件判断, 例如我在java版本的书写当中的少了 = 的判断。 第三,就是对于变量传递要注意,尤其是golang语言刷题必须注意,如必须传递指针。

顺便总结一下递归算法的模版吧~,来自于极客大学的谭朝大佬的总结

public void recur(int level, int param) { 

  // terminator 
  if (level > MAX_LEVEL) { 
    // process result 
    return; 
  } 

  // process current logic 
  process(level, param); 

  // drill down 
  recur( level: level + 1, newParam); 

  // restore current status 
 
}

第一步: 找出递归终止条件 (terminator)

第二步: 处理当前逻辑 (process current logic )

第三步: 下探到下一层(drill down )

第四步: 清理当前层(restore current status)

很多时候问题的代码只需要前三个步骤足以。