小五的算法系列 - 深度优先遍历与广度优先遍历

·  阅读 676
小五的算法系列 - 深度优先遍历与广度优先遍历

Hello, 各位勇敢的小伙伴, 大家好, 我是你们的嘴强王者小五, 身体健康, 脑子没病.

本人有丰富的脱发技巧, 能让你一跃成为资深大咖.

一看就会一写就废是本人的主旨, 菜到抠脚是本人的特点, 卑微中透着一丝丝刚强, 傻人有傻福是对我最大的安慰.

欢迎来到小五算法系列深度优先遍历与广度优先遍历.

前言

此系列文章以《算法图解》和《学习JavaScript算法》两书为核心,其余资料为辅助,并佐以笔者愚见所成。力求以简单、趣味的语言带大家领略这算法世界的奇妙。

前置位总结:

  • 深度优先遍历,对应数据结构为栈,核心思路为递归,找准递归条件及边界则大获全胜

  • 广度优先遍历,对应数据结构为队列,每层循环处理一层数据

深度优先遍历(DFS)

对应数据结构:栈

dfs1.png

深度优先遍历就像走迷宫一样,常常会一条路走到黑;若此路不通,则返回最近的岔路继续尝试,直到走出迷宫,这个反复的过程即在尝试 “穷举” 所有可能;

其核心思想为穷举所有路径,凡遇到有关穷举的问题,应第一时间想到递归,

我们来简化一下迷宫,假定其如下:

dfs2.png

其深度优先遍历结果为:

  • 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进栈 --- 深度优先遍历本质就是栈

拟定上述数据结构如下:

dfs3.png

代码如下:

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)

对应数据结构:队列

bfs.png

广度优先遍历就像关系网一样,比如你想找一位水果经销商,你先问了你的朋友们,发现没有后你和朋友们说,希望能帮忙问问他们的朋友中有没有水果经销商,以此类推,直到有人将水果经销商推荐给他;

广度优先遍历的核心就是就近原则,逐层扩散的过程

我们来简化一下人际关系网,假定其如下:

bfs2.png

其广度优先遍历结果为:

  • 第一层: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为经销商,终止

🤔 总结:其就是一个入列出列的过程,广度优先遍历的本质是队列

拟定上述数据结构如下:

bfs3.png

代码如下:

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]]

👺 题目分析

求子集,那肯定要穷举,而看到穷举,我们就应该想到递归,也该考虑是否可以深度优先搜索;

我们不妨换个思路,将此题转换成如下形式:

dfs4.png

从上到下分别如下:

  • []

  • [3]

  • [2]

  • [2, 3]

  • [1]

  • [1, 3]

  • [1, 2]

  • [1, 2, 3]

题目转变成了对上图的深度优先遍历,当走到第四层,即 arr.length 层时,触碰边界,推入结果;空与不空则可用 i < 2for 循环处理,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]]

👺 题目分析

层序遍历 -- 广度优先遍历,每次循环即一层,出列的数据即当前层数据

上述二叉树如下:

bfs4.jpeg

👺 代码实现

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;
};
复制代码

后记

🔗 本系列其它文章链接:

🔗 参考链接:修言 - 前端算法与数据结构面试:底层逻辑解读与大厂真题训练

1.gif

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改