Hello, 各位勇敢的小伙伴, 大家好, 我是你们的嘴强王者小五, 身体健康, 脑子没病.
本人有丰富的脱发技巧, 能让你一跃成为资深大咖.
一看就会一写就废是本人的主旨, 菜到抠脚是本人的特点, 卑微中透着一丝丝刚强, 傻人有傻福是对我最大的安慰.
欢迎来到
小五的算法系列之深度优先遍历与广度优先遍历.
前言
此系列文章以《算法图解》和《学习JavaScript算法》两书为核心,其余资料为辅助,并佐以笔者愚见所成。力求以简单、趣味的语言带大家领略这算法世界的奇妙。
前置位总结:
-
深度优先遍历,对应数据结构为栈,核心思路为递归,找准递归条件及边界则大获全胜
-
广度优先遍历,对应数据结构为队列,每层循环处理一层数据
深度优先遍历(DFS)
对应数据结构:栈
深度优先遍历就像走迷宫一样,常常会一条路走到黑;若此路不通,则返回最近的岔路继续尝试,直到走出迷宫,这个反复的过程即在尝试 “穷举” 所有可能;
其核心思想为穷举所有路径,凡遇到有关穷举的问题,应第一时间想到递归,
我们来简化一下迷宫,假定其如下:
其深度优先遍历结果为:
-
A -> B -> C -> G
-
A -> B -> D -> E
-
A -> B -> D -> H
-
A -> B -> D -> F
过程如下:
-
从起点A出发,到达拐点B,经过C点后最终到达G
-
G不是终点,回退到C,无其余路线,继续回退到B,此时出现另一条路D,选择D后继续选择E
-
E不是终点,回退到D,选择H
-
H不是终点,回退到D,选择F,F为终点
🤔 总结:上述过程就是一个进出栈的过程,A进栈,B进栈,C进栈,G进栈,G出栈,C出栈,D进栈,E进栈,E出栈,H进栈,H出栈,F进栈 --- 深度优先遍历本质就是栈
拟定上述数据结构如下:
代码如下:
let path = ''; // 最终路径
const stack = []; // 路径栈
const dfs = (root) => {
stack.push(root); // 根节点入栈
if (root.children && root.children.length) { // 未到边界,继续入栈
for (let i = 0; i < root.children.length; i++) { // 循环所有子元素
dfs(root.children[i]); // 递归子元素
}
stack.pop(); // 子元素全部执行后,当前元素出栈
} else { // 到达边界,处理边界
const top = stack.pop(); // 出栈边界值
if (top.val === 'F') { // 若为F,则为终点,拼接路径
stack.forEach(item => path += `${item.val} -> `);
path += 'F';
}
}
}
bfs(root); // path ~ A -> B -> D -> F
广度优先遍历(BFS)
对应数据结构:队列
广度优先遍历就像关系网一样,比如你想找一位水果经销商,你先问了你的朋友们,发现没有后你和朋友们说,希望能帮忙问问他们的朋友中有没有水果经销商,以此类推,直到有人将水果经销商推荐给他;
广度优先遍历的核心就是就近原则,逐层扩散的过程
我们来简化一下人际关系网,假定其如下:
其广度优先遍历结果为:
-
第一层:A
-
第二层:B、C
-
第三层:D、I
-
第四层:E、H、F
-
第五层:G
其过程如下:
-
A入列,A出列,B、C分别入列
-
B出列,D、I入列,C出列
-
D出列,E、H、F分别入列,I出列
-
E出列,H出列,G入列,F出列,F为经销商,终止
🤔 总结:其就是一个入列出列的过程,广度优先遍历的本质是队列
拟定上述数据结构如下:
代码如下:
const bfs = (root) => {
let path = ''; // 最终路径
const queue = []; // 查找队列
queue.push(root); // 入列根节点
while (queue.length) { // 循环直到找到目标值或者队列为空
const head = queue.shift(); // 队首出列
if (head.val === 'F') { // 找到目标值,处理相关路径
head.pre && head.pre.forEach(item => path += `${item} -> `);
path += 'F';
break;
}
if (head.children) { // 将该元素下的子元素分别入列
for (let i = 0; i < head.children.length; i++) {
const pre = head.pre ? [...head.pre, head.val] : [head.val];
head.children[i].pre = pre; // 记录前置元素,用于生成路径
queue.push(head.children[i]);
}
}
}
return path;
}
const path = bfs(root); // A -> B -> D -> F
小试牛刀
LeetCode 78. 子集
👺 题目简述
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
示例:
输入: nums = [1,2,3]
输出: [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
👺 题目分析
求子集,那肯定要穷举,而看到穷举,我们就应该想到递归,也该考虑是否可以深度优先搜索;
我们不妨换个思路,将此题转换成如下形式:
从上到下分别如下:
-
[]
-
[3]
-
[2]
-
[2, 3]
-
[1]
-
[1, 3]
-
[1, 2]
-
[1, 2, 3]
题目转变成了对上图的深度优先遍历,当走到第四层,即 arr.length 层时,触碰边界,推入结果;空与不空则可用 i < 2 的 for 循环处理,1代表非空,0代表空,当为1时进行入栈出栈操作;
👺 代码实现
const subsets = (nums) => {
const stack = [];
const result = [];
const dfs = (nth) => {
if (nth === nums.length) { // 触碰边界值,推入结果数组
result.push([...stack]);
return;
}
for (let i = 0; i < 2; i++) { // 为空或不为空两种可能,不为空进行入栈出栈操作
if (i !== 0) stack.push(nums[nth]);
dfs(nth + 1);
if (i !== 0) stack.pop();
}
}
dfs(0);
return result;
};
LeetCode 102. 二叉树的层序遍历
👺 题目简述
给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
示例:
输入: root = [3,9,20,null,null,15,7]
输出: [[3],[9,20],[15,7]]
👺 题目分析
层序遍历 -- 广度优先遍历,每次循环即一层,出列的数据即当前层数据
上述二叉树如下:
👺 代码实现
const levelOrder = (root) => {
if (!root) return [];
const queue = [];
const result = [];
queue.push(root);
while (queue.length) { // 每次循环代表一层
let cur = [];
const len = queue.length;
for (let i = 0; i < len; i++) { //当前层元素顺序出列,并入列下一层元素
const head = queue.shift();
cur.push(head.val);
head.left && queue.push(head.left);
head.right && queue.push(head.right);
}
result.push(cur);
}
return result;
};
后记
🔗 本系列其它文章链接:
🔗 参考链接:修言 - 前端算法与数据结构面试:底层逻辑解读与大厂真题训练