疫情在家,希望给大家提供一个更有效率刷题的方法。很多刷题的博文是根据难度来总结,个人认为根据考察的算法和数据结构来总结,可以实现事半功倍。所以lz根据自身经验和别人的讨论总结出各个类别下最有代表性以及面试频繁出现的题,提供给大家简洁没有废话的讲解和代码,争取让大家读完就基本会了这个类别。先做一期很基本的二叉树,之后会根据反响更新。注意本篇只讲解题目,不解释数据结构本身,如果数据结构不会的请谷歌一下。
首先看一下这个Easy难度的题
是否是有效的平衡二叉树:
给定一个二叉树,判断它是否是高度平衡的二叉树。
本题中,一棵高度平衡二叉树定义为:
一个二叉树每个节点的左右两个子树的高度差的绝对值不超过1。
这题的描述就给了我们明确思路,首先左右两个子树的高度差的绝对值不超过1。所以我们肯定要有用abs()
和递归来进行两个子树的高度差的绝对值的计算。考点在于我们要想到每一个子树也要求是一个有效的平衡二叉树,不然就会出现虽然两个子树的高度差的绝对值不超过1,但是子树的子树的绝对值差超过1。
Python 3 代码:
class Tree:
def __init__(self, value, left=None, right=None):
self.val = value
self.left = left
self.right = right
class Solution:
def isBalanced(self,root):
if not root:
return True
return self.isBalanced(root.left) and self.isBalanced(root.right) and (abs(self.depth(root.left)-self.depth(root.right))<=1)
def depth(self,node):
if not node:
return 0
return 1 + max(self.depth(node.left), self.depth(node.right))
知道如何用class的思想去测试也很重要,示例测试代码如下:
n3 = Tree(3)
n2 = Tree(2, n3)
root = Tree(1, n2, None)
# 1
# /
# 2
# /
# 3
print(Solution().isBalanced(root))
False
当然面试中时间紧张我们也可以直接 root.left = 2, root.right = None
和第一道题相似地,我们有这道中等难度题:
是否是有效的二叉搜索树:
给定一个二叉树,判断其是否是一个有效的二叉搜索树。
假设一个二叉搜索树具有如下特征:
节点的左子树只包含小于当前节点的数。
节点的右子树只包含大于当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
说是中等难度我觉得难度和上面那道题差不多,可以说都是中等偏简单。
看题描述就可以知道思路:我们当前节点的值需要小于左边子树的最小值,大于右边的最大值。然后子树也都要满足这个条件。难点是对于左子树来说,因为当前节点的值需要小于左边子树的最小值,也就是说当前节点的值是左子树的“最大值”;对于右子树来说则相反。
class Tree:
def __init__(self, value, left=None, right=None):
self.val = value
self.left = left
self.right = right
class Solution:
def isValid(self,root):
def helper(node,lower,upper):
# base case:如果是空树的话,判定有效
if not node:
return True
curr_val = node.val
# 对我们当前的node来说,它的值必须要小于左边subtree的最小值,大于右边subtree的最大值。这就是lower和upper的意思。
if curr_val <= lower or curr_val >= upper:
return False
# 对于右侧sub tree来说,它的lower bound就是你当前parent node的值
if not helper(node.right, curr_val, upper):
return False
# 同理于左侧sub tree来说,它的upper bound就是你当前parent node的值
if not helper(node.left, lower, curr_val):
return False
return True
return helper(root, float('-inf'), float('inf'))
如果上面两道你觉得有些难度的话也不要紧,我们下面来看一道真.简单的题目叫做:
反转二叉树
这道题灵感来源于明星程序员Max Howell不会反转二叉树而被Google拒绝,所以做出这道题你就可以超越大牛(不是。
做法就是当前node的左子树是经过反转的右子树,右子树是经过反转的左子树。简单粗暴:
class Solution:
def invertTree(self, root):
if root != None:
right = self.invertTree(root.right)
left = self.invertTree(root.left)
root.right = left
root.left = right
return root
做对这道题可以说是二叉树的基本啦。如果这道题不会的可以先去复习一下二叉树的遍历和搜索怎么写。比如我面过国内的大厂就有问过最基本的前中后遍历。
再来看一道基础题:
二叉树的最大深度
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
和第一题有些相似,我们运用一个DFS和递归的解法:
class Solution:
def maxDepth(self, root):
if root is None:
return 0
else:
left_height = self.maxDepth(root.left)
right_height = self.maxDepth(root.right)
return max(left_height, right_height) + 1
这道题还可以用迭代的解法,原理是运用一个栈,迭代时将当前结点弹出栈将子结点推入。每一步都会更新深度。应该之后会写一个针对栈和队列等数据结构的专题。
这里对二叉树的解题方法做个总结:其实大部分Tree的问题都可以使用递归和迭代两种解法。lz比较喜欢递归,因为好想,代码又简洁。迭代的好处是不容易出错,而且没有recursion stack,有时候可以省空间。
复杂度
可能有人注意到这上面的题我都没有提时间空间复杂度。是因为二叉树的问题复杂度都是有规律可循的,如果是遍历那么时间复杂度就是:O(N),因为你需要把树里每个Node都造访一遍。如果是搜索那么时间复杂度就是:O(log(n)),因为你需要去找某一个Node,时间复杂度和二分法是一样的(除非不是搜索二叉树)。
空间复杂度,用递归的解法一定会有一个调用栈,他的存储一般最少是O(log(n))。因为你调用几次recursion function他的存储就是几。像上面那道题,如果树是完全平衡(perfectly balanced)的,空间复杂度就是O(log(n))。在最坏的情况下,比如每个节点只有左节点没有右节点,你就需要调用N次,所以这样空间复杂度就是O(N)。
下面来看一道稍微难一点的题(中等难度)
不同的二叉搜索树
这道题呢个人认为非常有意思,看似很难,融合了排列组合和二叉树。但是实际上只要我们用一个清晰的递归思路来解,就并不太难:
def gen_tree(tree_range):
# base cases
if len(tree_range) == 0:
return [None]
if len(tree_range) == 1:
return [Node(tree_range[0])]
result = []
# 循环所有的node,生成所有可能的子树
for n in tree_range:
lefts = gen_tree(range(tree_range[0], n))
rights = gen_tree(range(n + 1, tree_range[-1] + 1))
# 让当下选定node做root,循环所有的子树
for left in lefts:
for right in rights:
tree = Node(n, left, right)
result.append(tree)
return result
def generate_tree(n):
# 将给定的整数n变成一个范围
return gen_tree(range(1, n + 1))
这道题的复杂度是什么呢?循环所有可能的子树,是O(2^(n)),最外层的循环是O(n)。所以复杂度就是两者相乘。
这道题还有一个DP的解法。可能在后面写DP专题的时候会提到。