一道二叉树题的n步优化——LeetCode98validate binary search tree(草稿)
树的题目,往往可以用到三种遍历、以及递归,因为其结构上天然地可以往深处递归,且判断条件也往往不复杂(左右子树都是空的)。
LeetCode 98题讲的是,判断一棵树是不是二叉搜索树。
题目中给的是标准定义:即一个二叉搜索树(binary search tree,简称BST)的每个节点都满足:1.其左侧的整个子树的值都比它小;2.其右侧的整个子树的值都比他大;3.它的左右子树依旧保持二叉搜索树的结构。
第0步
一开始理解错了条件,容易理解成,每个节点只需要比它的左子树大,比它的右子树小,于是得到了这样的递归代码(关于递归,可参加本人第一篇博文:https://www.cnblogs.com/mingyu-li/p/12161704.html):
1 class Solution { 2 public boolean isValidBST(TreeNode root) { 3 // it is a typical in-order traverse 4 // exit: if null, return true 5 if(root == null) return true; //结束递归条件 6 7 boolean leftValid = true; 8 boolean rightValid = true; 9 if(root.left != null && root.left.val >= root.val) leftValid = true; //检查左子节点 10 if(root.right != null && root.right.val <= root.val) rightValid = true; //检查右子节点 11 12 return leftValid && rightValid && isValidBST(root.left) && isValidBST(root.right); 13 } 14 }
问题是很明显的,实际上1、2两个条件未能满足,如果一个节点的左子节点的右子节点又大于这个节点,不违反上述的判断,但是又不符合定义:“整个”左子树全部小于节点。
第1步:正确的左右子树递归
既然问题就出在没有合理进行判断,那么对于一个节点,就深入递归判断其全部的子树就是,但是平均来看,一个节点都需要再遍历与总结点数成正比的节点来判断大小了。
1 class Solution { 2 public boolean isValidBST(TreeNode root) { 3 // exit: if null, return true 4 if(root == null) return true; 5 6 // the recursion of recursion: the root should be larger than any element in left 7 // the root should be smaller than any element in right 8 if(!checkLeft(root.left, root.val) || !checkRight(root.right, root.val)) return false; 9 10 return isValidBST(root.left) && isValidBST(root.right); 11 } 12 13 public boolean checkLeft(TreeNode node, int value){ 14 if(node == null) return true; 15 if(node.val >= value) return false; 16 return checkLeft(node.left, value) && checkLeft(node.right, value); 17 } 18 19 public boolean checkRight(TreeNode node, int value){ 20 if(node == null) return true; 21 if(node.val <= value) return false; 22 return checkRight(node.right, value) && checkRight(node.left, value); 23 } 24 }
这样,时间复杂度大约是O(n^2),空间复杂度是O(n)。
第2.1步
一个BST其实有一个性质,就是其中序遍历的结果应该是有序的,此处是升序,也就是递增的。很自然且容易写的办法就是用一个全局变量链表把节点的值先保存进去,再一一进行比较是否是升序。
从这步开始,时间复杂度就已经一步优化到了O(n)了,因为其合理利用了BST的中序遍历的性质,如果想不到,也就很难做这样的优化了。不过,在空间上,虽然同样是O(n),这步却略高于第一步,因为还新开辟了一个ArrayList(虽然递归的栈也是主要来源)。
1 class Solution { 2 ArrayList<Integer> inorderset = new ArrayList(); 3 public boolean isValidBST(TreeNode root) { 4 if(root == null) return true; 5 inorderTraverse(root); 6 for(int i = 1; i < inorderset.size(); i++){ 7 if(inorderset.get(i) <= inorderset.get(i - 1)) return false; 8 } 9 return true; 10 } 11 12 public void inorderTraverse(TreeNode root){ 13 if(root == null) return; 14 inorderTraverse(root.left); 15 inorderset.add(root.val); 16 inorderTraverse(root.right); 17 } 18 }
第2.2步
上述方法较大的问题就是太过暴力,先把全部数据遍历保存了才比较,虽然易于理解与实现,但是对这个题目的要求有点舍近求远了。所以比较合理的想法就是依照官方题解,确认每个点的上下界。
左节点的范围:(根/父节点的左边界,父节点的值);右节点的范围:(父节点的值,父节点的右边界)
细节:最大最小值的表示方法,一种是利用Integer.MAX_VALUE,若输入是长整形long,则只能考虑使用null以及java的自动拆包装包特性了。
因为多做了以上的数学分析,代码显得更加简洁,但是不那么intutive了,时间复杂度O(n),空间复杂度O(n)。这一方法也就是树的DFS,每一轮向树的左右孩子移动,然后找不到时结束。(从结果来看,时间上的最优解似乎出自这个方法)
1 class Solution { 2 public boolean isValidBST(TreeNode root) { 3 boolean res = helper(root, null, null); 4 return res; 5 } 6 7 public boolean helper(TreeNode node, Integer min, Integer max){ 8 if(node == null) return true; 9 if(min != null && min >= node.val) return false; 10 if(max != null && max <= node.val) return false; 11 12 return helper(node.left, min, node.val) && helper(node.right, node.val, max); 13 } 14 }
第2.3步
对于上述遍历方法,也可以考虑使用栈/队列来实现,但是如果使用栈,按深度优先方法,保存的变量则太多,不如使用BFS,遍历一层只保存上层的必要信息。实现BFS的必要操作是使用queue。
空间、时间都是O(n),但是时间上略慢。
1 class Solution { 2 public boolean isValidBST(TreeNode root) { 3 if(root == null) return true; 4 5 Queue<TreeNode> queue = new LinkedList(); 6 Queue<Integer> maxList = new LinkedList(); 7 Queue<Integer> minList = new LinkedList(); 8 9 queue.add(root); 10 maxList.add(null); 11 minList.add(null); 12 13 while(!queue.isEmpty()){ 14 TreeNode node = queue.poll(); 15 Integer max = maxList.poll(); 16 Integer min = minList.poll(); 17 18 if(max != null && max <= node.val) return false; 19 if(min != null && min >= node.val) return false; 20 21 if(node.left != null){ 22 queue.add(node.left); 23 maxList.add(node.val); 24 minList.add(min); 25 } 26 27 if(node.right != null){ 28 queue.add(node.right); 29 maxList.add(max); 30 minList.add(node.val); 31 } 32 } 33 34 return true; 35 } 36 }
第2.4步
e. 使用DFS+stack实现:(参考:https://leetcode.wang/leetCode-98-Validate-Binary-Search-Tree.html)
1 public boolean isValidBST(TreeNode root) { 2 if (root == null || root.left == null && root.right == null) { 3 return true; 4 } 5 //利用三个栈来保存对应的节点和区间 6 LinkedList<TreeNode> stack = new LinkedList<>(); 7 LinkedList<Integer> minValues = new LinkedList<>(); 8 LinkedList<Integer> maxValues = new LinkedList<>(); 9 //头结点入栈 10 TreeNode pNode = root; 11 stack.push(pNode); 12 minValues.push(null); 13 maxValues.push(null); 14 while (pNode != null || !stack.isEmpty()) { 15 if (pNode != null) { 16 //判断栈顶元素是否符合 17 Integer minValue = minValues.peek(); 18 Integer maxValue = maxValues.peek(); 19 TreeNode node = stack.peek(); 20 if (minValue != null && node.val <= minValue) { 21 return false; 22 } 23 if (maxValue != null && node.val >= maxValue) { 24 return false; 25 } 26 //将左孩子加入到栈 27 if(pNode.left!=null){ 28 stack.push(pNode.left); 29 minValues.push(minValue); 30 maxValues.push(pNode.val); 31 } 32 pNode = pNode.left; 33 } else { // pNode == null && !stack.isEmpty() 34 //出栈,将右孩子加入栈中 35 TreeNode node = stack.pop(); 36 minValues.pop(); 37 Integer maxValue = maxValues.pop(); 38 if(node.right!=null){ 39 stack.push(node.right); 40 minValues.push(node.val); 41 maxValues.push(maxValue); 42 } 43 pNode = node.right; 44 } 45 } 46 return true; 47 }
第2.5步
既然b中已经考虑了中序遍历,但是实际上中序遍历又不需要专门保存往一个数组中,因为题目并不需要这样,多浪费了空间,因此可以考虑使用iteration的方式实现中序遍历,用一个stack来做。
此时,时间复杂度是O(n),空间复杂度降低到了O(h),当二叉树平衡时,实际上最好可以达到O(logn),因此在LeetCode上结果也达到了80.93%。
这一思路的本质是利用二叉树的中序遍历的iteration的化归,如果能看出这一点,实际上不难联想到这里了。(到这步,也已几近于空间上的最优了,当然,最好还是希望树型接近平衡二叉树)
1 class Solution { 2 public boolean isValidBST(TreeNode root) { 3 // this is using in-order traverse iteration version 4 if(root == null) return true; 5 Stack<TreeNode> stack = new Stack(); 6 TreeNode pre = null; // denote the node before current node 7 8 while(root != null || !stack.isEmpty()){ 9 while(root != null){ 10 // push all left nodes in the stack 11 stack.push(root); 12 root = root.left; 13 } 14 root = stack.pop(); 15 if(pre != null && pre.val >= root.val) return false; 16 pre = root; 17 root = root.right; 18 } 19 20 return true; 21 } 22 }
第3步
由于在中序遍历中,还有一个Morris算法,可以再降低中序遍历到O(1),如果使用这一结构解决这个问题,可以达到空间最优。(算法之后补上)