KK's blog

每天积累多一些

0%

LeetCode 112 Path Sum

Given a binary tree and a sum, determine if the tree has a root-to-leaf path such that adding up all the values along the path equals the given sum.

For example:
Given the below binary tree and sum = 22,

              
              5
             / \
            4   8
           /   / \
          11  13  4
         /  \      \
        7    2      1

return true, as there exist a root-to-leaf path 5->4->11->2 which sum is 22.

题目大意:

给定一个二叉树root和一个和sum,决定这个树是否存在一条从根到叶子的路径使得沿路所有节点的和等于给定的sum。

解题思路:

DFS搜索,二叉树的题。步骤主要是考虑

  1. 空指针
  2. 一个node情况或多个node情况(可合并)

注意事项:

  1. 叶子节点是不含左儿子和右儿子

Java代码:

1
2
3
4
5
6
7
public boolean hasPathSum(TreeNode root, int sum) {
if(root == null)
return false;
if(root.left==null && root.right==null && root.val==sum)
return true;
return hasPathSum(root.left, sum-root.val) || hasPathSum(root.right, sum-root.val);
}

算法分析:

两种方法时间复杂度为O(n),n为节点数。空间复杂度为O(h),h为二叉树高度。

相关题目:

LeetCode 112 Path Sum
LeetCode 124 Binary Tree Maximum Path Sum

LeetCode 309 Best Time to Buy and Sell Stock with Cooldown

Say you have an array for which the ith element is the price of a given stock on day i.

Design an algorithm to find the maximum profit. You may complete as many transactions as you like (ie, buy one and sell one share of the stock multiple times) with the following restrictions:

  • You may not engage in multiple transactions at the same time (ie, you must sell the stock before you buy again).
  • After you sell your stock, you cannot buy stock on next day. (ie, cooldown 1 day)

Example:

prices = [1, 2, 3, 0, 2]
maxProfit = 3
transactions = [buy, sell, cooldown, buy, sell]

题目大意:

给定一个数组,第i个元素代表某只股票在第i天的价格。 设计一个算法计算最大收益。你可以完成多次交易(多次买入、卖出同一只股票),需要满足下列限制:
你不可以在同一时间参与多个交易(在买入股票之前必须卖出)。
在卖出股票之后,你不可以在第二天马上买入。(需要一天的冷却(CD)时间)。也就是卖出后过两天才能买入。

解题思路:

因为有限制条件,所以没有特别方法,只能计算所有结果,也就是用动态规划。动态规划首先是写出递归式(数学归纳法)。

  1. 定义f(n)为第n日卖出股票(一定要卖出,不能持有)的利润,加强了命题。
  2. 递归式如下图,f(n)只能由f(n-1)卖出后立刻买入(相当于n-1时候不卖出)或者f(n-3)卖出n-1时候买入两种情况。

    现在可以写出递归式:

    F(x)=max{f(1),…,f(n)}求加强命题最大值即为本题解。
    这里考虑到负数,方便程序实现,否则,f(n)的前3个值计算就不能放入循环而要特别处理了。

注意事项:

  1. 数组为空或者1个
  2. 负数组的实现方法

Java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public int maxProfit(int[] prices){
int sell[] = new int[prices.length];
int max = 0;
for(int i=1;i<prices.length;i++){
sell[i] = Math.max(f(i-3, sell), f(i-1, sell))+ prices[i]-prices[i-1];
if(sell[i]>max)
max = sell[i];
}
return max;
}

public int f(int i, int[] r){
if(i<1)
return 0;
else
return r[i];

这个实现有个错误就是忽略了一种重要的情况:f(n-4),…,f(1)的情况。看以下例子:[6,1,6,4,3,0,2]

Index 0 1 2 3 4 5 6
price 6 1 6 4 3 0 2
f(n) 0 -5 5 3 2 2 5

按照上面算法结果为5,但是很容易看出来1->6, 0->2结果是7。问题出在最后一个f(6)=max{f(3),f(5)}+2=max{3,2}+2。很明显,第3天卖出获利为3并不是最佳,第二天卖出获利为5才是最佳,我们忽略了f(n-3)之前的所有情况。解决方案是再创建一个数组维护前n天最大获利值。
定义g(n)为第n日(包括第n日)前卖出股票(不一定要第n天卖出)的利润。修改递归式为,把f(n-3)改为g(n-3):

Java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public int maxProfit2(int[] prices){
int sell[] = new int[prices.length];
int preSell[] = new int[prices.length];
int max = 0;
for(int i=1;i<prices.length;i++){
sell[i] = Math.max(g(i-3, preSell), f(i-1, sell))+ prices[i]-prices[i-1];
if(sell[i]>max)
max = sell[i];

preSell[i] = Math.max(preSell[i-1], sell[i]);
}
return max;
}

public int f(int i, int[] r){
if(i<1)
return 0;
else
return r[i];
}

public int g(int i, int[] g){
if(i<1)
return 0;
else
return g[i];
}

算法分析:

时间复杂度为O(n),n为数组长度,空间复杂度O(n)

current, first分别通过f和g的计算公式计算,second是通过current获得

Python代码:

1
2
3
4
5
6
7
8
# g(n) = max{f(n), g(n-1)}
# f(n) = A[n] - A[n - 1] + max{f(n - 1), g(n-3)}
def maxProfit(self, prices: List[int]) -> int:
# g(n-3), f(n-2), f(n-1)
first, second, current = 0, 0, 0
for i in range(1, len(prices)):
current, second, first = prices[i] - prices[i - 1] + max(current, first), current, max(second, first)
return max(current, second, first)

空间优化:

由于此题目,f(n)只与前三个状态有关f(n-1), f(n-2)(虽然没直接关系,但程序实现需要记录),g(n-3)。四个状态可以用三个变量
推进,如sell=Math.max(preSell, sell),同一个变量旧状态更新到新状态,所以可以避免维护数组开销。
代入preSell=g(n-3), sell_1=f(n-2), sell=f(n-1)到公式即得
f(n) = sell = max{preSell, sell}+prices[n]-prices[i-1]
g(n-2) = preSell = max{g(n-3), f(n-2)} = max{preSell, sell_1}
f(n-1) = sell_1 = PreValue(sell)
本题解就是preSell, sell_1, sell的最大值。

Java代码:

1
2
3
4
5
6
7
8
9
10
11
public int maxProfit(int[] prices){
//g(n-3), f(n-2), f(n-1)
int preSell=0, sell_1=0, sell = 0;
for(int i=1;i<prices.length;i++){
int tmp = sell;
sell = Math.max(preSell, sell)+ prices[i]-prices[i-1];
preSell = Math.max(preSell, sell_1);
sell_1 = tmp;
}
return Math.max(Math.max(preSell, sell_1), sell);
}

算法分析:

时间复杂度为O(n),n为数组长度,空间复杂度O(1)。本题有更简单解法但比较难想出。

最后注意事项:

  1. 数组为空或者1个
  2. 三种情况f(n-1),f(n-3),g(n-3)可以得到f(n)。解就是preSell, sell_1, sell的最大值。
  3. DP流程,定义函数(是否加强)、递归式、空间优化。

相关题目:

LeetCode 121 Best Time to Buy and Sell Stock
LeetCode 122 Best Time to Buy and Sell Stock II
LeetCode 309 Best Time to Buy and Sell Stock with Cooldown
LeetCode 123 Best Time to Buy and Sell Stock III

LeetCode 310 Minimum Height Trees

For a undirected graph with tree characteristics, we can choose any node as the root. The result graph is then a rooted tree. Among all possible rooted trees, those with minimum height are called minimum height trees (MHTs). Given such a graph, write a function to find all the MHTs and return a list of their root labels.

Format
The graph contains n nodes which are labeled from 0 to n - 1. You will be given the number n and a list of undirected edges (each edge is a pair of labels).

You can assume that no duplicate edges will appear in edges. Since all edges are undirected, [0, 1] is the same as [1, 0] and thus will not appear together in edges.

Example 1:

Given n = 4, edges = [[1, 0], [1, 2], [1, 3]]

        0
        |
        1
       / \
      2   3

return [1]

Example 2:

Given n = 6, edges = [[0, 3], [1, 3], [2, 3], [4, 3], [5, 4]]

     0  1  2
      \ | /
        3
        |
        4
        |
        5

return [3, 4]

Note:

(1) According to the definition of tree on Wikipedia): “a tree is an undirected graph in which any two vertices are connected by exactly one path. In other words, any connected graph without simple cycles is a tree.”

(2) The height of a rooted tree is the number of edges on the longest downward path between the root and a leaf.

题目大意:

对于一棵无向树,我们可以选择它的任意节点作为根。得到的结果就是有根树。在所有可能的有根树中,高度最小的称为最小高度树(MHT)。
给定一个无向图,编写函数找出所有的最小高度树,并返回其根标号的列表。

解题思路:

此题本质上求最长路径上的中间1-2个节点。由于根节点不确定,从叶节点出发,层层剥离,这就是拓扑排序(inDegree数组)。而且需要知道最后一层的1-2个节点,所以考虑用按层遍历BFS(两数组)。见KB。

注意事项:

  1. 由于最后一层可能是1-2个节点,所以要用一个变量res把最后一层记录下来, res = list(queue)在开始和循环中。
  2. 还有一点要注意的是这是无向图,所以入度=1而不是0时候即入队列。
  3. 单一节点(没有边)返回空列表

Python代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def findMinHeightTrees(self, n: int, edges: List[List[int]]) -> List[int]:
if not edges:
return [0] # remember
graph = collections.defaultdict(list)
in_degree = [0] * n
for li in edges:
graph[li[0]].append(li[1])
graph[li[1]].append(li[0])
in_degree[li[0]] += 1
in_degree[li[1]] += 1
queue = collections.deque([i for i in range(len(in_degree)) if in_degree[i] == 1])
res = list(queue) # remember
while queue:
for _ in range(len(queue)):
node = queue.popleft()
for neighbor in graph[node]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 1:
queue.append(neighbor)
if queue: # remember
res = list(queue)
return res

注意事项:

  1. 由于最后一层可能是1-2个节点,所以要用一个变量把最后一层记录下来。
  2. 还有一点要注意的是这是无向图,所以入度=1而不是0时候即入队列。

Topological:

  1. 根据边统计每个节点的入度数记入in[i]
  2. 找出度数为0的节点加入到Queue
  3. 取出队首节点,把此节点邻接的节点度数减1,如果度数为0,加入到队列,循环直到队列为空

Java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public List<Integer> findMinHeightTrees(int n, int[][] edges) {
if(n==1 && edges.length==0){
return new ArrayList<Integer>(Arrays.asList(new Integer[]{0}));
}
ArrayList<ArrayList<Integer>> graph = new ArrayList<ArrayList<Integer>>();
int num = n;
for(int i=0;i<n;i++)
graph.add(new ArrayList<Integer>());
int[] inDegree = new int[n];
//populate inDegree & convert to graph
for(int i=0;i<edges.length;i++){
inDegree[edges[i][0]]++;
inDegree[edges[i][1]]++;
graph.get(edges[i][1]).add(edges[i][0]);
graph.get(edges[i][0]).add(edges[i][1]);
}
Queue<Integer> q = new LinkedList<Integer>();
Queue<Integer> q2 = new LinkedList<Integer>();

for(int i=0;i<inDegree.length;i++){
if(inDegree[i]==1)
q.offer(i);
}
Queue<Integer> lastLayerQ = new LinkedList<Integer>(q);
while(!q.isEmpty()){
Integer v = q.poll();
for(int neighbor : graph.get(v)){
if(--inDegree[neighbor]==1)
q2.offer(neighbor);
}
if(q.isEmpty() && !q2.isEmpty()){
q = q2;
q2 = new LinkedList<Integer>();
lastLayerQ = new LinkedList<Integer>(q);
}

}

return (List)lastLayerQ;
}

算法分析:

时间复杂度为O(n),w为树的所有层里面的最大长度,空间复杂度O(w)

算法思路:

对于拓扑排序来说, 我们的中心思想是要我们可以找到一个顺序,每一次我们可以进行的工序是现在没有先序依赖的工序,
按照这个顺序可以流畅的完成我们的任务。
思路基于BFS的队列实现。区别在于统计每个节点的入度数。此法也可用于无向图。

  1. 根据边统计每个节点的入度数记入in[i],其他节点(含无边节点)入度数为0
  2. 找出度数为0的节点加入到Queue
  3. 取出队首节点,把此节点邻接的节点度数减1,如果度数为0,加入到队列,循环直到队列为空
  4. 如果队列为空但仍有节点度数不为0,存在循环,否则不存在

应用:

  1. 求最长或最短路径(Leetcode 310)
  2. 判断拓扑顺序(Leetcode外星人字典)
  3. 判断循环(Python代码返回None)

注意事项:

  1. graph要含所有节点,包括没有边的节点。否则结果会有遗漏
  2. in_degree初始化要对所有节点赋0
  3. 第四步判断是否含循环必不可少,要根据题目要求来处理。除非L310 min height明确一定有解,而L269外星人字典就明确可能无解

Python代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def topological_sort(self, graph: List[List[int]], n: int) -> List[int]:
in_degree = [0] * n
for i in range(len(graph)):
for j in range(len(graph[i])):
in_degree[graph[i][j]] += 1

start_nodes = [i for i in range(len(in_degree)) if in_degree[i] == 0]
queue, res = deque(start_nodes), []

while queue:
root = queue.popleft()
res.append(root)
for i in graph[root]:
in_degree[i] -= 1
if in_degree[i] == 0:
queue.append(i)
return res if len(res) == n else None

Java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/*
* graph: 邻接表
* num: 节点个数
*/
public void topologicalSort(ArrayList<ArrayList<Integer>> graph, int num) {
int[] inDegree = new int[num];
//populate inDegree
for(ArrayList<Integer> adjacencyList : graph){
for(Integer node : adjacencyList){
inDegree[node]++;
}
}
Queue<Integer> q = new LinkedList<Integer>();
for(int i=0;i<inDegree.length;i++){
if(inDegree[i]==0)
q.offer(i);
}
int count = 0;
while(!q.isEmpty()){
Integer v = q.poll();
count++;
System.out.print(v + "->");
for(int neighbor : graph.get(v)){
if(--inDegree[neighbor]==0)
q.offer(neighbor);
}
}
/* check isCyclic or not
return count == num;;
*/
}

算法分析:

时间复杂度为O(n),w为树的所有层里面的最大长度,空间复杂度O(w)

算法思路:

图用邻接表为输入,思路用Queue实现, 还要一个机制记录节点访问过没有,可以用HashSet,同时它作为结果存储BFS访问结果。
BFS多用于找最短路径
DFS多用于快速发现底部节点和具体路劲问题(如路径和或打印路径)。

BFS优缺点:
同一层的所有节点都会加入队列,所以耗用大量空间
仅能非递归实现
相比DFS较快,空间换时间
适合广度大的图
找环的话需要用拓扑排序

DFS优缺点:
无论是系统栈还是用户栈保存的节点数都只是树的深度,所以空间耗用小
有递归和非递归实现
由于有大量栈操作(特别是递归实现时候的系统调用),执行速度较BFS慢
适合深度大的图
找环的话比较容易实现

如果BFS和DFS都可以用,建议用BFS,因为工业应用中,BFS不用有限的栈空间,可以利用到所有内存。
BFS题目问自己是否需要按层遍历,是否需要visited

注意事项:

  1. 坐标型BFS,注意输入为(i, j)时候,x = node[0] + _dx[0], 用x不要用输入的i
  2. deque([(start[0], start[1])]), set([(startx, starty)])
  3. board[x][y] == ‘+’ 注意矩阵的其他不能遍历的元素,如L200中的0

其他:

  1. 只要把节点放入队列立即标记其为访问,不要在出列的时候才标记。否则同一个节点会入列多次。
    比如下图, 入列顺序为1,2,3,4,4。4入列了两次。
  2. 矩阵的遍历用方向List: OFFSETS = [(1, 0), (-1, 0), (0, 1), (0, -1)]

图遍历

只有是对所有节点BFS题型才需要将visited作为参数传入

Python代码:

1
2
3
4
5
6
7
8
9
10
11
12
def bfs(self, graph, start) -> List[int]:
queue, res = deque([start]), []
visited = set([start])
while queue:
node = queue.popleft()
res.append(node)
for neighbor in graph[node]:
if neighbor in visited:
continue
queue.append(neighbor)
visited.add(neighbor)
return res

树模板只要把visited去掉,neighbor改成left和right

计算最短距离的图遍历(最常考的模板)

  • 只要加line 4和14
  • visited在函数内定义
  • 遇到target就返回最短距离,若离开循环返回-1,问清楚是否存在路径不存在的情况
  • 求距离公式不需要用min,因为这个遍历保证了最短距离了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def bfs_layer(self, graph, start) -> List[int]:
queue = deque([start])
visited = set([start])
distance = {start: 1}
while queue:
node = queue.popleft()
if node == target:
return distance[node]
for neighbor in graph[node]:
if neighbor in visited:
continue
queue.append(neighbor)
visited.add(neighbor)
distance[neighbor] = distance[node] + 1
return -1

按层遍历

核心是加这一行for _ in range(len(queue))
具体还要加line 5, 6和14, 15. 二叉树不需要visited。能用distance dict就尽量不用此法,因为多了一个循环。

注意事项:

  1. 关键行: 多这一行for循环
1
2
3
4
5
6
7
8
9
10
11
12
13
def bfs_layer2(self, graph, visited, start) -> List[List[int]]:
queue, res, layer = deque([start]), [], 0
visited.add(start)
while queue:
for _ in range(len(queue)):
node = queue.popleft()
for neighbor in graph[node]:
if neighbor in visited:
continue
queue.append(neighbor)
visited.add(neighbor)
layer += 1
return res

Java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
* graph: 邻接表
* visited: 记录已访问节点,避免重复访问
* start: BFS的根节点
*/
public void bfs(HashMap<Integer, LinkedList<Integer>> graph, HashSet<Integer> visited, int start){
int num=0;
Queue<Integer> q=new LinkedList<>();
q.offer(start);
visited.add(start);
while(!q.isEmpty()){
int node = q.poll();
LinkedList<Integer> children = graph.get(node);
for(int child : children){
if (!visited.contains(child)){
q.offer(child);
visited.add(child);
}
}
}
}

错误算法:

问题是加入到queue里面的child没有去重,这样容易queue爆炸式增长。

1
2
3
4
5
6
7
8
9
10
11
12
13
public void bfs(HashMap<Integer, LinkedList<Integer>> graph, HashSet<Integer> visited, int start){
Queue<Integer> q=new LinkedList<>();
q.offer(start);
while(!q.isEmpty()){
int node = q.poll();
visited.add(node);
LinkedList<Integer> children = graph.get(node);
for(int child : children){
if (!visited.contains(child))
q.offer(child);
}
}
}

按层次遍历:

第一个思路是三重循环,先queue,然后该层所有节点,最后该层每一个节点的儿子。关键在于size = q.size();

Java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void bfsByLayer3(HashMap<Integer, LinkedList<Integer>> graph, int start){
Queue<Integer> q=new LinkedList<>();
int layer = 1;
q.offer(start);
while(!q.isEmpty()){
int size = q.size();
for(int i = 0; i < size; i++) {// loop this layer
int node = q.poll();
System.out.println("node:"+node+" in layer "+ layer);
LinkedList<Integer> children = graph.get(node);
for(int child : children){// loop its children
q.add(child);
}
}
layer++;
}
}

上述代码三重循环看似可读性不够好,所有可以用hashMap来记录每个点的距离从而减少第二重循环。
nodeToHeight.put(start, 1);
nodeToHeight.put(child, nodeToHeight.get(node) + 1);

Java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void bfsByLayer4(HashMap<Integer, LinkedList<Integer>> graph, int start){
Queue<Integer> q = new LinkedList<>();
q.offer(start);
Map<Integer, Integer> nodeToHeight = new HashMap<>();
nodeToHeight.put(start, 1);
while(!q.isEmpty()){

int node = q.poll();
System.out.println("node:"+node+" in layer "+ nodeToHeight.get(node));
LinkedList<Integer> children = graph.get(node);
for(int child : children){// loop its children
q.add(child);
nodeToHeight.put(child, nodeToHeight.get(node) + 1);
}

}
}

  1. q.isEmpty() && !q2.isEmpty()

第三思路是用两个队列来实现:用第一个队列存储该层的节点,第二个队列存储第一个队列中节点的儿子节点,
也就是下一次的节点。此思路比较容易实现。

Java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 假设不循环
* graph: 邻接表
* start: BFS的根节点
*/
public void bfsByLayer(HashMap<Integer, LinkedList<Integer>> graph, int start){
Queue<Integer> q=new LinkedList<>();
Queue<Integer> q2=new LinkedList<>();
int layer = 1;
q.offer(start);
while(!q.isEmpty()){
int node = q.poll();
System.out.println("node:"+node+" in layer "+ layer);
LinkedList<Integer> children = graph.get(node);
for(int child : children){
q2.offer(child);
}
if(q.isEmpty() && !q2.isEmpty()){
q = q2;
q2 = new LinkedList<>();
layer++;
}
}
}

第四思路是只用一个队列来实现:层与层之间用结束符间隔,每遇到结束符,表示该层访问结束,下一层的节点也准备好
(不会再有新的节点加入到这一层),此时再往队列加入新的结束符。此思路对数据有一定限制,实现起来注意事项较多。

Java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void bfsByLayer2(HashMap<Integer, LinkedList<Integer>> graph, int start){
Queue<Integer> q=new LinkedList<>();
int layer = 1;
q.offer(start);
q.offer(-1);
while(!q.isEmpty()){
int node = q.poll();
if(node == -1){
//确保有非结束符节点
if(q.size()>0){
q.offer(-1);
layer++;
}
continue;
}
System.out.println("node:"+node+" in layer "+ layer);
LinkedList<Integer> children = graph.get(node);
for(int child : children){
q.offer(child);
}
}
}

算法分析:

时间复杂度为O(n),w为树的所有层里面的最大长度,空间复杂度O(w)

Free mock interview