跳到主要内容

41 篇博文 含有标签「深度优先搜索」

查看所有标签

· 阅读需 3 分钟

1、题干

给你一棵二叉搜索树,请 按中序遍历 将其重新排列为一棵递增顺序搜索树,使树中最左边的节点成为树的根节点,并且每个节点没有左子节点,只有一个右子节点。

 

示例 1:

输入:root = [5,3,6,2,4,null,8,1,null,null,null,7,9]
输出:[1,null,2,null,3,null,4,null,5,null,6,null,7,null,8,null,9]

示例 2:

输入:root = [5,1,7]
输出:[1,null,5,null,7]

 

提示:

  • 树中节点数的取值范围是 [1, 100]
  • 0 <= Node.val <= 1000

 

注意:本题与主站 897 题相同: https://leetcode-cn.com/problems/increasing-order-search-tree/

2、解法1-DFS+改变指针

深度遍历所有节点,在递归左子节点后构造新的二叉树,newRoot用于记录新树的根节点,若newRoot为空则赋值为当前节点,lastNode用于记录新树的最后一个节点,若lastNode有值则将其右子节点置为当前节点,然后将lastNode置为当前节点,当前节点的左子节点置为空,最后返回newRoot

3、代码

var increasingBST = function (root) {
let newRoot = null, lastNode = null;
function dfs(node) {
if (!node) return;
dfs(node.left);
if (!newRoot) newRoot = node;
if (lastNode) lastNode.right = node;
lastNode = node;
node.left = null;
dfs(node.right);
}
dfs(root);

return newRoot;
};

4、执行结果

1.png

5、解法2-DFS+数组

深度遍历将节点按中序存入数组中,结束后对节点数组进行遍历,将当前节点的左子节点置空右子节点置为下一个数组元素,最后返回数组首个元素。

6、代码

var increasingBST = function (root) {
const arr = [];
function dfs(node) {
if (!node) return;
dfs(node.left);
arr.push(node);
dfs(node.right);
}
dfs(root);

for (let i = 0; i < arr.length; i++) {
arr[i].left = null;
arr[i].right = arr[i + 1] || null;
}

return arr[0];
};

7、执行结果

执行用时: 68 ms 内存消耗: 39.2 MB

· 阅读需 4 分钟

1、题干

路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。

路径和 是路径中各节点值的总和。

给定一个二叉树的根节点 root ,返回其 最大路径和,即所有路径上节点值之和的最大值。

 

示例 1:

输入:root = [1,2,3]
输出:6
解释:最优路径是 2 -> 1 -> 3 ,路径和为 2 + 1 + 3 = 6

示例 2:

输入:root = [-10,9,20,null,null,15,7]
输出:42
解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42

 

提示:

  • 树中节点数目范围是 [1, 3 * 104]
  • -1000 <= Node.val <= 1000

 

注意:本题与主站 124 题相同: https://leetcode-cn.com/problems/binary-tree-maximum-path-sum/

2、解题思路

深度遍历树上所有节点,对每个节点求最大路径和即可。

3、代码

var maxPathSum = function (root) {
let res = root.val;
function dfs(node) {
if (!node) return -Infinity;
const maxSum1 = dfs(node.left), maxSum2 = dfs(node.right);
const maxSum = Math.max(node.val, maxSum1 + node.val, maxSum2 + node.val);
res = Math.max(res, maxSum, maxSum1, maxSum2, maxSum1 + maxSum2 + node.val);
return maxSum;
}
return dfs(root), res;
};

关键代码说明

  • 求每个节点的最大路径和,且路径不重复的关键
    • const maxSum = Math.max(node.val, maxSum1 + node.val, maxSum2 + node.val);
    • 这行代码用于求当前节点下的最大路径和maxSum,并将其返回给父节点用于计算父节点下的最大路径和
    • Math.max取最大值时包含当前节点的值、当前节点值+左子节点最大路径和、当前节点值+右子节点最大路径和
    • 即最大路径和一定要包含当前节点的值,还可能叠加某个子节点最大路径和,也可能不叠加子节点最大路径和
    • 因此dfs函数逐级返回时,由下至上形成了一条不分叉的路径,并能逐级向上返回子节点的最大路径和
  • 求整棵树最大路径和res的关键
    • res = Math.max(res, maxSum, maxSum1, maxSum2, maxSum1 + maxSum2 + node.val);
    • 在每个节点求最大路径和时,res取其自身以及maxSum1maxSum2node.val三者任意组合的路径和中的最大值
    • maxSum1maxSum2node.val三者任意组合求和时,至少取1个,且取2个时不能是maxSum1 + maxSum2,因为这个组合不构成路径

4、执行结果

1.png

· 阅读需 4 分钟

1、题干

路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。

路径和 是路径中各节点值的总和。

给定一个二叉树的根节点 root ,返回其 最大路径和,即所有路径上节点值之和的最大值。

 

示例 1:

输入:root = [1,2,3]
输出:6
解释:最优路径是 2 -> 1 -> 3 ,路径和为 2 + 1 + 3 = 6

示例 2:

输入:root = [-10,9,20,null,null,15,7]
输出:42
解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42

 

提示:

  • 树中节点数目范围是 [1, 3 * 104]
  • -1000 <= Node.val <= 1000

 

注意:本题与主站 124 题相同: https://leetcode-cn.com/problems/binary-tree-maximum-path-sum/

2、解题思路

深度遍历树上所有节点,对每个节点求最大路径和即可。

3、代码

var maxPathSum = function (root) {
let res = root.val;
function dfs(node) {
if (!node) return -Infinity;
const maxSum1 = dfs(node.left), maxSum2 = dfs(node.right);
const maxSum = Math.max(node.val, maxSum1 + node.val, maxSum2 + node.val);
res = Math.max(res, maxSum, maxSum1, maxSum2, maxSum1 + maxSum2 + node.val);
return maxSum;
}
return dfs(root), res;
};

关键代码说明

  • 求每个节点的最大路径和,且路径不重复的关键
    • const maxSum = Math.max(node.val, maxSum1 + node.val, maxSum2 + node.val);
    • 这行代码用于求当前节点下的最大路径和maxSum,并将其返回给父节点用于计算父节点下的最大路径和
    • Math.max取最大值时包含当前节点的值、当前节点值+左子节点最大路径和、当前节点值+右子节点最大路径和
    • 即最大路径和一定要包含当前节点的值,还可能叠加某个子节点最大路径和,也可能不叠加子节点最大路径和
    • 因此dfs函数逐级返回时,由下至上形成了一条不分叉的路径,并能逐级向上返回子节点的最大路径和
  • 求整棵树最大路径和res的关键
    • res = Math.max(res, maxSum, maxSum1, maxSum2, maxSum1 + maxSum2 + node.val);
    • 在每个节点求最大路径和时,res取其自身以及maxSum1maxSum2node.val三者任意组合的路径和中的最大值
    • maxSum1maxSum2node.val三者任意组合求和时,至少取1个,且取2个时不能是maxSum1 + maxSum2,因为这个组合不构成路径

4、执行结果

1.png

· 阅读需 4 分钟

1、题干

给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum路径 的数目。

路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。

 

示例 1:

输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
输出:3
解释:和等于 8 的路径有 3 条,如图所示。

示例 2:

输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:3

 

提示:

  • 二叉树的节点个数的范围是 [0,1000]
  • -109 <= Node.val <= 109 
  • -1000 <= targetSum <= 1000 

 

注意:本题与主站 437 题相同:https://leetcode-cn.com/problems/path-sum-iii/

2、解法1

使用DFS递归遍历树的所有节点,递归函数接收上层传递下来的路径上所有前缀和,遍历所有前缀和并计算是否存在和等于targetSum的路径(计算公式node.val + sums[sums.length - 1] - (sums[i] || 0) === targetSum),存在则最终结果+1,时间复杂度为O(nlogn)O(nlogn)

上述时间复杂度是估算值,因为每个节点需要访问一次,因此至少有O(n)O(n)的复杂度,另外每个节点内部需要再遍历前缀和数组,而数组长度最大为树的最大高度lognlogn,因此综合估算时间复杂度为O(nlogn)O(nlogn)

3、代码

var pathSum = function (root, targetSum) {
if (!root) return 0;
let res = root.val === targetSum ? 1 : 0;

function dfs(node, sums) {
if (!node) return;
for (let i = -1; i < sums.length; i++) {
if (node.val + sums[sums.length - 1] - (sums[i] || 0) === targetSum) res++;
}

dfs(node.left, [...sums, (sums[sums.length - 1] || 0) + node.val]);
dfs(node.right, [...sums, (sums[sums.length - 1] || 0) + node.val]);
}

return dfs(root, []), res;
};

4、执行结果

执行用时: 148 ms 内存消耗: 57.8 MB

5、解法2

该解法是解法1的优化版本,利用了哈希表存储前缀和,另外使用了回溯的思想重置前缀和状态,优化后的时间复杂度为O(n)O(n)

  • 同样使用DFS递归遍历树的所有节点,但使用Map存储前缀和及其数量,递归时在Map中查找是否已存在等于sum - targetSum的路径前缀和,存在则最终结果累加其数量
  • 将当前路径总和对应的数量累加1
  • 然后进入下层递归,同样执行上述逻辑
  • 下层递归结束时,将当前路径总和对应的数量累减1

6、代码

var pathSum = function (root, targetSum) {
if (!root) return 0;
let res = 0, map = new Map();
map.set(0, 1);

function dfs(node, sum) {
if (!node) return;
sum = sum + node.val;
res += map.get(sum - targetSum) || 0;
map.set(sum, (map.get(sum) || 0) + 1);
dfs(node.left, sum);
dfs(node.right, sum);
map.set(sum, map.get(sum) - 1);
}

return dfs(root, 0), res;
};

7、执行结果

1.png

· 阅读需 4 分钟

1、题干

给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum路径 的数目。

路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。

 

示例 1:

输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
输出:3
解释:和等于 8 的路径有 3 条,如图所示。

示例 2:

输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:3

 

提示:

  • 二叉树的节点个数的范围是 [0,1000]
  • -109 <= Node.val <= 109 
  • -1000 <= targetSum <= 1000 

 

注意:本题与主站 437 题相同:https://leetcode-cn.com/problems/path-sum-iii/

2、解法1

使用DFS递归遍历树的所有节点,递归函数接收上层传递下来的路径上所有前缀和,遍历所有前缀和并计算是否存在和等于targetSum的路径(计算公式node.val + sums[sums.length - 1] - (sums[i] || 0) === targetSum),存在则最终结果+1,时间复杂度为O(nlogn)O(nlogn)

上述时间复杂度是估算值,因为每个节点需要访问一次,因此至少有O(n)O(n)的复杂度,另外每个节点内部需要再遍历前缀和数组,而数组长度最大为树的最大高度lognlogn,因此综合估算时间复杂度为O(nlogn)O(nlogn)

3、代码

var pathSum = function (root, targetSum) {
if (!root) return 0;
let res = root.val === targetSum ? 1 : 0;

function dfs(node, sums) {
if (!node) return;
for (let i = -1; i < sums.length; i++) {
if (node.val + sums[sums.length - 1] - (sums[i] || 0) === targetSum) res++;
}

dfs(node.left, [...sums, (sums[sums.length - 1] || 0) + node.val]);
dfs(node.right, [...sums, (sums[sums.length - 1] || 0) + node.val]);
}

return dfs(root, []), res;
};

4、执行结果

执行用时: 148 ms 内存消耗: 57.8 MB

5、解法2

该解法是解法1的优化版本,利用了哈希表存储前缀和,另外使用了回溯的思想重置前缀和状态,优化后的时间复杂度为O(n)O(n)

  • 同样使用DFS递归遍历树的所有节点,但使用Map存储前缀和及其数量,递归时在Map中查找是否已存在等于sum - targetSum的路径前缀和,存在则最终结果累加其数量
  • 将当前路径总和对应的数量累加1
  • 然后进入下层递归,同样执行上述逻辑
  • 下层递归结束时,将当前路径总和对应的数量累减1

6、代码

var pathSum = function (root, targetSum) {
if (!root) return 0;
let res = 0, map = new Map();
map.set(0, 1);

function dfs(node, sum) {
if (!node) return;
sum = sum + node.val;
res += map.get(sum - targetSum) || 0;
map.set(sum, (map.get(sum) || 0) + 1);
dfs(node.left, sum);
dfs(node.right, sum);
map.set(sum, map.get(sum) - 1);
}

return dfs(root, 0), res;
};

7、执行结果

1.png

· 阅读需 4 分钟

1、题干

给你一个 不含重复 单词的字符串数组 words ,请你找出并返回 words 中的所有 连接词

连接词 定义为:一个完全由给定数组中的至少两个较短单词(不一定是不同的两个单词)组成的字符串。

 

示例 1:

输入:words = ["cat","cats","catsdogcats","dog","dogcatsdog","hippopotamuses","rat","ratcatdogcat"]
输出:["catsdogcats","dogcatsdog","ratcatdogcat"]
解释:"catsdogcats" 由 "cats", "dog" 和 "cats" 组成;
"dogcatsdog" 由 "dog", "cats" 和 "dog" 组成;
"ratcatdogcat" 由 "rat", "cat", "dog" 和 "cat" 组成。

示例 2:

输入:words = ["cat","dog","catdog"]
输出:["catdog"]

 

提示:

  • 1 <= words.length <= 104
  • 1 <= words[i].length <= 30
  • words[i] 仅由小写英文字母组成。 
  • words 中的所有字符串都是 唯一 的。
  • 1 <= sum(words[i].length) <= 105

常规解法用字典树处理,详情可以看官解。 补充:这个解法有漏洞,调整用例肯定还会超时

2、解题思路

这里采用一种非常规解法,创建哈希集合set并加入所有单词,然后遍历所有单词并判断该单词是否由set中的元素组成,判断过程是利用递归对每个单词进行分段,如果set包含所有分段后的子串则说明其是连接词。

然后写出了下面这段代码,结果通过用例43/44,最后一个用例超时。主要是因为这个用例的特殊性,导致递归检查函数的执行次数暴涨。

var findAllConcatenatedWordsInADict = function (words) {
const set = new Set(words);
if (set.has("")) set.delete("");

function dfs(word, start) {
let s = '';
for (let i = start; i < word.length; i++) {
s += word[i];
if (set.has(s) && dfs(word, i + 1)) return true;
}
return set.has(s) && start;
}

return words.reduce((acc, cur) => (dfs(cur, 0) && acc.push(cur), acc), []);
};

由于该用例的特殊性,尝试添加预检策略绕过一些特殊用例:

  • 1、字符串数组按长度升序排序,由于长单词肯定由短单词组成,意味着如果前面的短单词无法组合出当前单词的特征,就说明其不是连接词
  • 2、预检函数,取出单词中所有连续的两个字符(最短连接子串),检查更短的单词是否包含或能组合出这个最短连接子串,如果为否则说明其不是连接词

3、完整代码

var findAllConcatenatedWordsInADict = function (words) {
words.sort((a, b) => a.length - b.length);
const set = new Set(words);
if (set.has('')) set.delete('');

function dfs(word, start) {
let s = '';
for (let i = start; i < word.length; i++) {
s += word[i];
if (set.has(s) && dfs(word, i + 1)) return true;
}
return set.has(s) && start;
}

const headSet = new Set(), tailSet = new Set(), compSet = new Set();
function preCheck(word) {
let found = true, comps = [];
for (let i = 0; i < word.length - 1; i++) {
const s = word[i] + word[i + 1];
if (!compSet.has(s) && !(tailSet.has(s[0]) && headSet.has(s[1]))) found = false;
comps.push(s);
}
headSet.add(word[0]), tailSet.add(word[word.length - 1]);
for (const c of comps) compSet.add(c);
return found;
}

return words.reduce((acc, cur) => {
if (!preCheck(cur)) return acc;
if (dfs(cur, 0)) acc.push(cur);
return acc;
}, []);
};

4、执行结果

1.png

· 阅读需 3 分钟

1、题干

给定一棵二叉树的根节点 root ,请找出该二叉树中每一层的最大值。

 

示例1:

输入: root = [1,3,2,5,3,null,9]
输出: [1,3,9]
解释:
1
/ \
3 2
/ \ \
5 3 9

示例2:

输入: root = [1,2,3]
输出: [1,3]
解释:
1
/ \
2 3

示例3:

输入: root = [1]
输出: [1]

示例4:

输入: root = [1,null,2]
输出: [1,2]
解释:
  1
  \
  2

示例5:

输入: root = []
输出: []

 

提示:

  • 二叉树的节点个数的范围是 [0,104]
  • -231 <= Node.val <= 231 - 1

 

注意:本题与主站 515 题相同: https://leetcode-cn.com/problems/find-largest-value-in-each-tree-row/

解法1:DFS递归实现

DFS递归遍历树的每个节点,递归函数的形参中添加1个层级参数,便于计算每层节点最大值。

执行结果

1.png

代码

var largestValues = function (root) {
const res = [];
function dfs(node, level) {
if (!node) return;
if (res[level] === undefined || node.val > res[level]) res[level] = node.val;
dfs(node.left, level + 1);
dfs(node.right, level + 1);
}
dfs(root, 0);
return res;
};

解法2:BFS非递归实现

BFS非递归遍历树的每个节点,需要借助队列实现。与常规BFS非递归实现不同,此处队列是一个矩阵,队列中的元素是每层节点的集合。

执行结果

2.png

代码

var largestValues = function (root) {
const queue = root ? [[root]] : [], res = [];
for (const curNodes of queue) {
let max = -Infinity, nodes = [];
for (const node of curNodes) {
if (node.val > max) max = node.val;
if (node.left) nodes.push(node.left);
if (node.right) nodes.push(node.right);
}
if (nodes.length) queue.push(nodes);
res.push(max);
}
return res;
};

由于非递归实现需要额外借助数组实现,且存在大量Array.push()等耗时操作,所以时间和空间上会消耗得更多

· 阅读需 2 分钟

1、题干

给定一个二叉树的 根节点 root,请找出该二叉树的 最底层 最左边 节点的值。

假设二叉树中至少有一个节点。

 

示例 1:

输入: root = [2,1,3]
输出: 1

示例 2:

输入: [1,2,3,4,null,5,6,null,null,7]
输出: 7

 

提示:

  • 二叉树的节点个数的范围是 [1,104]
  • -231 <= Node.val <= 231 - 1 

 

注意:本题与主站 513 题相同: https://leetcode-cn.com/problems/find-bottom-left-tree-value/

执行结果

2.png

解题思路

总体思路:使用DFS递归遍历树的所有节点,当第一次遍历到层级大于所有已遍历节点的节点时,该节点就是当前层级的最左子节点;随着遍历层级递增,最终能找到最底层的最左子节点。

  • 使用DFS递归遍历树的所有节点
  • 记录当前节点的层级level与已遍历节点的最大层级maxLevel
  • 每当level超过maxLevel时,将当前节点赋值给res,另外更新maxLevel
  • 遍历完成后,res就是要找的节点,返回该节点的值即可

代码

var findBottomLeftValue = function (root) {
let res, maxLevel = 0;
function dfs(node, level) {
if (!node) return;
if (level > maxLevel) res = node, maxLevel = level;
dfs(node.left, level + 1);
dfs(node.right, level + 1);
}
dfs(root, 1);
return res && res.val;
};

· 阅读需 2 分钟

1、题干

给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。

 

示例 1:

输入: [1,2,3,null,5,null,4]
输出: [1,3,4]

示例 2:

输入: [1,null,3]
输出: [1,3]

示例 3:

输入: []
输出: []

 

提示:

  • 二叉树的节点个数的范围是 [0,100]
  • -100 <= Node.val <= 100 

 

注意:本题与主站 199 题相同:https://leetcode-cn.com/problems/binary-tree-right-side-view/

执行结果

0.png

解题思路

总体思路:使用DFS递归遍历树的所有节点,创建结果数组res,将当前节点的层级当做res的下标、val当做res的值,以此更新res,右边节点的值会不断覆盖左边节点,得到的数组就是从右侧所能看到的节点值。

代码

var rightSideView = function (root) {
const res = [];
function dfs(node, level) {
if (!node) return;
res[level] = node.val;
dfs(node.left, level + 1);
dfs(node.right, level + 1);
}
dfs(root, 0);
return res;
};

· 阅读需 2 分钟

1、题干

给定一个二叉树 根节点 root ,树的每个节点的值要么是 0,要么是 1。请剪除该二叉树中所有节点的值为 0 的子树。

节点 node 的子树为 node 本身,以及所有 node 的后代。

 

示例 1:

输入: [1,null,0,0,1]
输出: [1,null,0,null,1]
解释:
只有红色节点满足条件“所有不包含 1 的子树”。
右图为返回的答案。


示例 2:

输入: [1,0,1,0,0,0,1]
输出: [1,null,1,null,1]
解释:


示例 3:

输入: [1,1,0,1,1,0,1,0]
输出: [1,1,0,1,1,null,1]
解释:


 

提示:

  • 二叉树的节点个数的范围是 [1,200]
  • 二叉树节点的值只会是 01

 

注意:本题与主站 814 题相同:https://leetcode-cn.com/problems/binary-tree-pruning/

执行结果

1.png

解题思路

总体思路:使用DFS递归遍历树的节点,递归过程中把值为0的叶子节点逐个剪除,就能得到剪枝后的二叉树。

代码

var pruneTree = function (root) {
function dfs(node) {
if (!node) return;
if (dfs(node.left)) node.left = null;
if (dfs(node.right)) node.right = null;
if (node.val === 0 && !node.left && !node.right) return true;
}
return dfs(root) ? null : root;
};