深度优先遍历(DFS)-栈和广度优先遍历(BFS)-队列的理解

JS版本的,概念什么的就不赘述了。参考了这位大佬的文章(JS算法之深度优先遍历(DFS)和广度优先遍历(BFS))
在这里记录下自己的理解,为啥一个是栈,一个是队列。看码:

<div id="box">
  <ul>
    <li><div><p></p></div></li>
    <li><img /></li>
    <li><a></a></li>
  </ul>
  <div>
    <p>1</p>
    <p><span><strong></strong></span></p>
    <p>3</p>
  </div>
  <button><strong></strong></button>
</div>
// 深度优先,用的是栈,同一边进出,后入先出
function deepFirstSearch(node) {
  var nodes = []
  if (node != null) {
    var stack = []
    stack.push(node) // 第一个元素无论 push 还是 unshift 都一样,这里只是为了相呼应。
    while(stack.length != 0) {
      // 最核心的差别在这里了
      // 第一次进来:是[$box],弹出来后都是为空
      // 第二次进来: [button, div, ul],然后 ul 被 pop 出去,收进结果数组 nodes 里
      var item = stack.pop()
      nodes.push(item)
      var children = item.children
      for (var i = children.length - 1; i >= 0; i--) {
        // 这一步也是一样,把每个子节点放进去待遍历数组
        // 把 $box 的子节点放进待选数组,此时是 [button, div, ul]
        // 第一次 while 进来都是一样
        // 这里先收集左边还是右边的子级意义都是一样的
        // 这里是从先把 button 放进来,ul 最后,等下 pop 出去就是 ul 最先
        stack.push(children[i])
        // 点出 ul 的子级,收进 stack 的最后,下次 while 进来,pop 就会是 ul 的子级,也即是 li
        // 然后再继续下去,还是会把 ul 的子级的子级,也就是 div,收进 stack 最后
        // 也就是不断的子级优先,达成深挖
      }
    }
  }
  return nodes
}

// 广度优先,用的是队列,一边进另一边出,先入先出
function breadthFirstSearch(node) {
  var nodes = []
  if (node != null) {
    var queue = []
    // 第一次进来:是[$box],弹出来后都是为空
    // 第二次进来:[button, div, ul],然后 button 被 unshift 出去,收进结果数组 nodes 里
    // 第三次进来:[div, ul, strong],然后 div 被 unshift 出去,收进结果数组 nodes 里
    // 第四次进来:[ul, strong, p, p, p],然后 ul 被 unshift 出去,收进结果数组 nodes 里
    // 可以看出跟深度的差别,点出子级都是排进待选数组的最后
    // 但是深度是栈pop,加塞在数组最后的子级优先
    // 广度是队列unshift,一起放进数组的兄弟级优先
    queue.unshift(node)
    while(queue.length != 0) {
      var item = queue.shift()
      nodes.push(item)
      var children = item.children
      for (var i = children.length - 1; i >= 0; i--) {
        queue.push(children[i])
        // 对应上面第二次进来:点出 button 的子级,也就是 strong,收进 queue 的最后
        // 但对 unshift 的结果没影响,下次 while 进来,unshift 依然是 div
        // 对应上面第三次进来:点出 div 的子级,也就是 p,收进 queue 的最后
      }
    }
  }
  return nodes
}

可以看出,就是在遍历时候暂存的数据结构不一样。
深度优先,就是一直深挖,孩子的孩子的孩子…。
广度优先,就是兄弟兄弟兄弟孩子兄弟…。需要注意的是,同级的就算兄弟。意思是相对于根级为孙子辈,就算不是同个父级,也算是兄弟级。

2022-10-18 21:31:18
更新了一篇用 js 做遍历的《深广度优先遍历》

记录一些基础算法

开始补充一些算法和数据结构的知识

冒泡排序

const arr = [162, 6, 3, 8, 90, 234, 5, 7, 31, 45, 12, 1, 98, 23, 87, 40]
function sort(arr) {
  const len = arr.length - 1
  for (let x = 0, y = len; x < y; x++) {
    // 把第一个跟后面那个对比,如果比后面的数大,就交换位置,到最后就可以确定最大的数在最后
    // 也就是在 length 的位置
    // 在第一轮的时候,x=1,也就是遍历到倒数第二个就好,因为取到 i+1 的数
    for (let i = 0, l = len - x; i < l; i++) {
      const first = arr[i], second = arr[i+1]
      if (first > second) {
        [arr[i], arr[i+1]] = [second, first]
      }
    }
    // 假设没有第一层,最大的数已经在最后,那么第二轮就是要在剩下的数找到最大,并放到倒数第二位置
    // 也就是在 length-1 的位置
    // 此时 x=2,因为依然是当轮最后数(第二轮为length-1)的前一个
    // for (var i = 0, l = arr.length - 2; i < l; i++) {
    //  if (arr[i] > arr[i+1]) {
    //    [arr[i], arr[i+1]] = [arr[i+1], arr[i]]
    //  }
    // }
    // 第三轮 x=3
    // for (var i = 0, l = arr.length - 3; i < l; i++) {
    //  if (arr[i] > arr[i+1]) {
    //    [arr[i], arr[i+1]] = [arr[i+1], arr[i]]
    //  }
    // }
    // 第四轮 x=4
    // for (var i = 0, l = arr.length - 4; i < l; i++) {
    //  if (arr[i] > arr[i+1]) {
    //    [arr[i], arr[i+1]] = [arr[i+1], arr[i]]
    //  }
    // }
    // 所以循环个遍,就成了外面一层的循环
  }
  return arr
}
console.log(sort(arr))

二分法

就是在一个有序的序列中迅速找到要找的那个数的索引。一个一个遍历当然也可以找到,但是不确定性导致复杂度。
二分法就是中间砍一刀,这样数列就成了两半。看看中间值是不是,是那就刚好了。不是的话,判断目标数是在前一半还是后一半,如此这般的循环即可。

这种思想应该在现实生活中还是经常用到的,比如git bisect看看大佬的介绍)。
以及,让我想起以前 TVB 的《超级无敌掌门人》里面有个游戏叫“超级无敌开口中”,轮流在在100里面瞎说一个数,中了就受罚。特别是轮到说最后一个必中的数字时那种视死如归的神情,超搞笑。

function getRandomNum(numMin, numMax) {
  return Math.round( Math.random()*(numMax-numMin) + numMin )
}  
function getArr() {
  const arr = []
  for(let i = 1, l = 100; i <= l; i++) {
    arr.push(i)
  }
  return arr
}
// 随机生成 1 ~ 100 中的某数
const num = getRandomNum(1, 100)
// 生成 1 - 100 的数组
const arr = getArr()
function binarySearch(num, arr) {
  let start = arr.length
  let end = 0
  const getHalf = () => {
    let length = start + end
    return (length % 2 === 0) ? (length / 2) : (length + 1) / 2
  }
  // 取中值
  let half = getHalf()
  let current = arr[half]
  while (current !== num) {
    if (current > num) {
      start = half
    } else if (current < num) {
      end = half
    }
    half = getHalf()
    current = arr[half]
  }
  return half
}
const idx = binarySearch(num, arr)
console.log(idx, arr[idx], num)

斐波那契数列

具体的数学意义就不说了,我也不懂,总而言之是满足这样的一个数列:F(0)=0,F(1)=1,F(n)=F(n - 1)+F(n - 2)(n ≥ 3,n ∈ N*)

// 第一反应就是递归,最简单最明晰。但效率最低,其实就是每次算出来的值没存起来,导致每个 n 位的值传进去都要重新递归的算
function fibonacci(n) {
  if(n === 0 || n === 1) {
    return n
  }
  return fibonacci(n-1) + fibonacci(n-2)
}
// 改进一些,是个数列自然就可以用个数组来存
// 把之前获得 n 位的值存起来,下次要拿就直接取就好
function fibonacci2(n) {
  if(n === 0 || n === 1) {
    return n
  }
  const arr = [0, 1]
  for(let i = 2; i <= n; i++) {
    arr[i] = arr[i-1] + arr[i-2]
  }
  return arr[n]
}
// 改进一些,其实每次都是需要上两位就可以了,不需要用整个数组来存全部
function fibonacci3(n) {
  if(n === 0 || n === 1) {
    return n
  }
  let lastOne = 1 // n-1
  let lastTwo = 0 // n-2
  for(let i = 2; i <= n; i++) {
    var current = lastOne + lastTwo
    // n-2 和 n-1 分别获取后一位的值
    lastTwo = lastOne
    lastOne = current
  }
  return current
}
// 第一种递归的方法,在浏览器 40 以后基本快跑不出来了
// 第三种还是比第二种快那么些许
console.time('fibonacci')
console.log(fibonacci(40))
console.timeEnd('fibonacci')

玩一玩puppeteer

puppeteer官网上写着:

Puppeteer 是一个 Node 库,它提供了一个高级 API 来通过 DevTools 协议控制 Chromium 或 Chrome。Puppeteer 默认以 headless 模式运行,但是可以通过修改配置文件运行“有头”模式。

其实就是个浏览器,给了许多方法让我们可以控制这个浏览器,比如打开某某网页,输入什么字符,点击哪个按钮。看起来就像是用程序来模拟人工的行为。
这样就表示了它可以用来自动化测试。搭配 jest 或 mocha 之类的测试框架,进行e2e测试,模拟一个人去访问网站,进行一些操作。
当然不止这一个方案,还有 selenium 和在这之上的 night-watch,也是干 web 自动化测试的活。

我们项目上使用它来跑自动化测试的,我自己用它来做爬虫,奸笑~
可能也不叫爬虫,就是上新闻网站扫一下热搜,到图片库搜点图片之类的。

安装:

安装是个蛋疼的事,因为这个用到chromium,这玩意在谷歌仓库。所以如果不能科学上网,那就只能修改为淘宝源。

env puppeteer_download_host=https://npm.taobao.org/mirrors npm i puppeteer

npm config set puppeteer_download_host  https://npm.taobao.org/mirrors
npm i puppeteer

年代久远,记得都是可以的。不保证时效性…

我们项目一开始使用它来截图

const puppeteer = require('puppeteer')
;(async () => {
  const browser = await puppeteer.launch({
    headless: true // 无头就是不要弹出浏览器
  })
  const page = await browser.newPage()
  await page.goto('https://www.baidu.com')
  await page.setViewport({
    width: 1200,
    height: 800
  })
  await page.screenshot({
    path: 'puppeteer.png',
    fullPage: true
  })
  await browser.close()
})()

来看下用它来搜百度热搜,之所以对 puppeteer 感兴趣,就是同事写了一篇这个教程,里面就是演示了爬热搜。

const puppeteer = require('puppeteer')
;(async () => {
  const browser = await puppeteer.launch({
    headless: true // 无头就是不要弹出浏览器
  })
  const page = await browser.newPage()
  await page.goto('http://top.baidu.com/buzz?b=1&fr=topindex', {
    timeout: 0, // 设置超时时间为0
    waitUntil: 'domcontentloaded' // 有多个状态可设置,这里设置为 dom 渲染完成
  })
  // 有时候网速卡,加载慢,容易超时报错,所以上面把超时时间去掉
  // 同时网页渲染也需要时间,可以等一下,
  // 比如 await page.waitFor(1000) 等待1000ms
  // 比如我之前爬图片的时候,都知道图片是相当耗时的
  // 所以我写成等图片的 dom 可见即可,可见包括了它已经被赋予 src,不需要等到图片下载并渲染完成
  // 然后获取其 src,再通过别的方法下载
  // await page.waitForSelector(dom, { visible: true, timeout: 0 })
  const list = await page.evaluate((dom) => {  // 爬取内容
    // 这个 dom 需要去网站上看实际结构,很好找,跟 jQuery 一样
    const $doms = document.querySelectorAll('.mainBody .keyword a')
    const list = Array.from($doms).map(($dom) => {
      return {
        href: $dom.href,
        text: $dom.innerText
      }
    })
    return list
  })
  // 这个 list 就记录了热搜的标题和网址
  await page.close()
  await browser.close()
})()

对网页的抓取还有这个cheerio,在 node 里面像 jQuery 一样获取dom。
不过好像对单页面的网站比较麻烦。因为是抓了整个网页内容下来,再来分析页面。那对于 SPA 抓下来也是一个容器一个js,没有实体dom。所以具体看用途啦。

puppeteer还有很多牛的功能,这里只是用到了冰山一角,还能做更多有趣的事。

在海边

最近去了一趟深圳大鹏官湖村。典型的海边小镇,地方不大,沙滩挺大。有一个路口在滤镜加持下,居然有点像灌篮高手片头里,樱木在车站前的经典场景。







Promise的几个api用法

Promise 应该大家都很熟悉了。大部分的库也都用上了Promise。
这文章主要记录下 Promise.all、Promise.allSettled、Promise.race 这几个的用法。

Promise.all:
参数是一个数组,数组元素是各个Promise。要数组里面所有的 Promise 都返回成功,那这个 Promise.all 的结果才会是成功。成功返回数组,就是元素成功的结果集合。失败的话返回单值,是元素里第一个失败的结果。

Promise.allSettled:
跟 Promise.all 参数一样,是一个数组,数组元素是各个Promise。不同的是这个不会因为数组元素的成败来返回成功失败。返回的是个数组,记录着元素 Promise 的各个结果,包括成败。

先准备四个小函数,然后再混起来用:

function PromiseSuccess0() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('PromiseSuccess0')
    }, 0)
  })
}
function PromiseSuccess1000() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('PromiseSuccess1000')
    }, 1000)
  })
}
function PromiseError2000() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('PromiseError2000')
    }, 2000)
  })
}
function PromiseError3000() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('PromiseError3000')
    }, 3000)
  })
}

Promise.all([PromiseSuccess0(), PromiseError3000(), PromiseError2000()])
 .then((data) => {
   console.log('success all-1: ', data)
 })
 .catch((error) => {
   console.error('error all-1: ', error)
    // error all-1:  PromiseError2000
 })

Promise.all([PromiseSuccess0(), PromiseSuccess1000()])
 .then((data) => {
   console.log('success all-2: ', data)
    // success all-2:  (2) ["PromiseSuccess0", "PromiseSuccess1000"]
 })
 .catch((error) => {
   console.error('error all-2: ', error)
 })

Promise.allSettled([PromiseError2000(), PromiseSuccess1000()])
  .then((data) => {
    console.log('success allSettled: ', data)
    // success allSettled:  
    // (2) [{…}, {…}]
    // 0: {status: "rejected", reason: "PromiseError2000"}
    // 1: {status: "fulfilled", value: "PromiseSuccess1000"}
  })
  .catch((error) => {
    console.error('error allSettled: ', error)
  })

Promise.race
顾名思义,竞赛。参数依然是个元素为 Promise 的数组。会返回第一个跑完的元素结果。话不多说,上码:

function load1000() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('load1000')
    }, 1000)
  })
}
function load3000() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('load3000')
    }, 3000)
  })
}
function errorTimer() {
 return new Promise((resolve, reject) => {
   setTimeout(() => {
     reject(new Error('timeout'))
   }, 2000)
 })
}

Promise.race([load1000(), errorTimer()])
 .then((data) => {
   console.log('success race-1: ', data)
   // success race-1:  load1000
 })
 .catch((error) => {
   console.error('error race-1: ', error)
 })

Promise.race([load3000(), errorTimer()])
 .then((data) => {
   console.log('success race-2: ', data)
 })
 .catch((error) => {
   console.error('error race-2: ', error)
   // error race-2:  Error: timeout
 })

具体的使用场景可以是,比如下载个东西,给其设定个时间限定,超过这个时间就报错。稍微封装一下:

function addTimer(fn, time) {
 const errorTimer = new Promise((resolve, reject) => {
   setTimeout(() => {
     reject(new Error('timeout'))
   }, time)
 })
 return Promise.race([fn(), errorTimer])
}

addTimer(load1000, 2000)
 .then((data) => {
   console.log('success addTimer: ', data)
 })
 .catch((error) => {
   console.error('error addTimer: ', error)
 })