Leetcode 中有哪些题的解法让你拍案叫绝?

377 阅读6分钟

小伙汁,要什么拍案叫绝的奇技淫巧,不如问问自己:

今天刷LeetCode了吗? 看到题目还觉得自己是个SB吗? 遇到hard题能有思路吗?

你看,与其纠结那些拍案叫绝的奇技淫巧,不如踏踏实实地把题目做好、做对、做透。

我在北美Facebook(脸书)当面试官,刷题超过2500道,面试超过300人。Facebook的coding考试是出了名的刁钻,在这过程中我总结了很多刷题技巧,目前已帮助数百名同学上岸。接下来,我会由浅入深,让你获得面试官都会“拍案叫绝”的算法题解法。

先上一道简单的克隆图问题,感受一下“一个微小的提升能如何让你的解题过程实现质的飞跃”。

这道题在Facebook面试中出现过100+次,大多数人给我的解法是

  • 使用的是 BFS 宽度优先搜索算法右边的代码
  • 一边做宽度优先搜索找到所有的点
  • 一边又复制所有的点
  • 一边又复制所有的边
  • 并且在复制边的时候又复制点

代码过程如下:

当然,这一做法整出来的代码也能跑,但是在实际工作中却可能会遇到很多问题,更别说是让你“拍案叫绝”了。

我给出一个更好的实现方法,将整个算法分解为三个步骤

  • 找到所有点
  • 复制所有点
  • 复制所有边

通过解耦合,让代码更易读、易维护。最后代码可以优化成这样:

如果感兴趣,我们还能继续往下看。

其实80%的题目都可以通过总结规律——coding代码模板——暴力破解——逐步优化来实现。

建议初学者刷题的时候可以先按照算法和数据结构的知识点分类来刷,刷多了以后慢慢就能找到这类题目的特征,以及相应的解题方法和解题思路,面试难题也就能迎刃而解了,我就来献个丑。

二叉搜索树非递归 BST Iterator

第一步,打开LintCode。筛选你想攻克的数据结构和算法:

我总结出的二叉搜索树非递归的使用条件包括:

  • 用非递归的方式(Non-recursion / Iteration)实现二叉树的中序遍历
  • 常用于 BST 但不仅仅可以用于 BST

复杂度

  • 时间复杂度 O(n)
  • 空间复杂度 O(n)

代码模板

Java版

List<TreeNode> inorderTraversal(TreeNode root) {  
List<TreeNode> inorder = new ArrayList<>();  
if (root == null) {  
return inorder;  
}  
// 创建⼀个 dummy node, 右指针指向 root  
// 放到 stack ⾥,此时栈顶 dummy 就是 iterator 的当前位置  
TreeNode dummy = new TreeNode(0);  
dummy.right = root;  
Stack<TreeNode> stack = new Stack<>();  
stack.push(dummy);  

// 每次将 iterator 挪到下⼀个点  
// 就是调整 stack 使得栈顶是下⼀个点  
while (!stack.isEmpty()) {  
TreeNode node = stack.pop();  
if (node.right != null) {  
node = node.right;  
while (node != null) {  
stack.push(node);  
node = node.left;  
}  
}  
if (!stack.isEmpty()) {  
inorder.add(stack.peek());  
}  
}  
return inorder;  
}  

Python版

def inorder_traversal(root):  
if root is None:  
return []  

# 创建⼀个 dummy node,右指针指向 root  
# 并放到 stack ⾥,此时 stack 的栈顶 dummy  
# 是 iterator 的当前位置  
dummy = TreeNode(0)  
dummy.right = root  
stack = [dummy]  

inorder = []  
# 每次将 iterator 挪到下⼀个点  
# 也就是调整 stack 使得栈顶到下⼀个点  
while stack:  
node = stack.pop()  
if node.right:  
node = node.right  
while node:  
stack.append(node)  
node = node.left  
if stack:  
inorder.append(stack[-1])  
return inorder 

看不懂的,建议拿这份模板去实操一下。我们再来:

宽度优先搜索 BFS

宽度优先搜索的运用更广一点,使用条件:

  • 拓扑排序(100%)
  • 出现连通块的关键词(100%)
  • 分层遍历(100%)
  • 简单图最短路径(100%)
  • 给定⼀个变换规则,从初始状态变到终⽌状态最少几步(100%)

复杂度

  • 时间复杂度:O(n + m)
  • n 是点数, m 是边数
  • 空间复杂度:O(n)

代码模板

Java版

ReturnType bfs(Node startNode) {  
// BFS 必须要⽤队列 queue,别⽤栈 stack!  
Queue<Node> queue = new ArrayDeque<>();  
// hashmap 有两个作⽤,⼀个是记录⼀个点是否被丢进过队列了,避免重复访问  
// 另外⼀个是记录 startNode 到其他所有节点的最短距离  
// 如果只求连通性的话,可以换成 HashSet 就⾏  
// node 做 key 的时候⽐较的是内存地址  
Map<Node, Integer> distance = new HashMap<>();  
// 把起点放进队列和哈希表⾥,如果有多个起点,都放进去  
queue.offer(startNode);  
distance.put(startNode, 0); // or 1 if necessary  
// while 队列不空,不停的从队列⾥拿出⼀个点,拓展邻居节点放到队列中  
while (!queue.isEmpty()) {  
Node node = queue.poll();  
// 如果有明确的终点可以在这⾥加终点的判断  
if (node 是终点) {  
break or return something;  
}  
for (Node neighbor : node.getNeighbors()) {  
if (distance.containsKey(neighbor)) {  
continue;  
}  
queue.offer(neighbor);  
distance.put(neighbor, distance.get(node) + 1);  
}  
}  
// 如果需要返回所有点离起点的距离,就 return hashmap  
return distance;  
// 如果需要返回所有连通的节点, 就 return HashMap ⾥的所有点  
return distance.keySet();  
// 如果需要返回离终点的最短距离  
return distance.get(endNode);  
}  

Python版

def bfs(start_node):  
# BFS 必须要⽤队列 queue,别⽤栈 stack# distance(dict) 有两个作⽤,⼀个是记录⼀个点是否被丢进过队列了,避免重复访问  
# 另外⼀个是记录 start_node 到其他所有节点的最短距离 
def bfs(start_node):  
# BFS 必须要⽤队列 queue,别⽤栈 stack# distance(dict) 有两个作⽤,⼀个是记录⼀个点是否被丢进过队列了,避免重复访问  
# 另外⼀个是记录 start_node 到其他所有节点的最短距离 
# 如果只求连通性的话,可以换成 set 就⾏  
# node 做 key 的时候⽐较的是内存地址  
queue = collections.deque([start_node])  
distance = {start_node: 0}  

# while 队列不空,不停的从队列⾥拿出⼀个点,拓展邻居节点放到队列中  
while queue:  
node = queue.popleft()  
# 如果有明确的终点可以在这⾥加终点的判断  
if node 是终点:  
break or return something  
for neighbor in node.get_neighbors():  
if neighor in distnace:  
continue  
queue.append(neighbor)  
distance[neighbor] = distance[node] + 1  

# 如果需要返回所有点离起点的距离,就 return hashmap  
return distance  
# 如果需要返回所有连通的节点, 就 return HashMap ⾥的所有点  
return distance.keys()  
# 如果需要返回离终点的最短距离  
return distance[end_node]  

意犹未尽的话,再来一个。

深度优先搜索 DFS

使用条件

①找满足某个条件的所有方案 (99%)

②二叉树 Binary Tree 的问题 (90%)

③组合问题(95%)

  • 问题模型:求出所有满足条件的“组合”
  • 判断条件:组合中的元素是顺序无关的

④排列问题 (95%)

  • 问题模型:求出所有满足条件的“排列”
  • 判断条件:组合中的元素是顺序“相关”的。

值得注意的是,还有不用DFS的场景:

  • 连通块问题(⼀定要用 BFS,否则 StackOverflow)
  • 拓扑排序(⼀定要用 BFS,否则 StackOverflow)
  • 一切 BFS 可以解决的问题

复杂度

时间复杂度:

O(方案个数 * 构造每个方案的时间)

  • 树的遍历 : O(n)
  • 排列问题 : O(n! * n)
  • 组合问题 : O(2^n * n)

代码模板

Java版

public ReturnType dfs(参数列表) {  
if (递归出⼝) {  
记录答案;  
return;  
}  
for (所有的拆解可能性) {  
修改所有的参数  
dfs(参数列表);  
还原所有被修改过的参数  
}  
return something 如果需要的话,很多时候不需要 return 值除了分治的写法  
}  

Python版

def dfs(参数列表):  
if 递归出⼝:  
记录答案  
return  
for 所有的拆解可能性:  
修改所有的参数  
dfs(参数列表)  
还原所有被修改过的参数  
return something 如果需要的话,很多时候不需要 return 值除了分治的写法 

看到这里,很多人会说:算法和数据结构的知识点这么多,难道我要全部掌握吗?别慌,针对今年春招算法面试考察的情况,我整理了算法和数据结构的高频考点,颜色越红考的越多,灰色的不考或者出现的概率低于千分之一。

时间紧迫的旁友,刷刷颜色最深的知识点就够临时抱佛脚了。

最后,我想说一句:想获得让其他人拍案叫绝的解法,只有勤勤恳恳的刷题、刷题、再刷题,找到刷题的乐趣也好,获得独特的解题思路也罢,在成功背后都是要付出比任何人都要艰辛的努力的。

当然,我必须十分很负责的说一句:面试中不是coding写得好就能过,就算用代码模板秒了算法题也能保证你100%通过面试,和面试官的沟通也同样重要。