算法+手撕


手撕

节流-防抖


const throttle1 = (fn,wait = 500) => { //必须返回函数否则是函数的直接调用
        return function(...args) {  // 不能是箭头函数,让this指向该标签,否则this指向windiw
          if(!this.timer){//检测是否开启了定时器
            this.timer =  setTimeout(() => {//没有开启则开启新的定时器,必须是箭头函数,否则this指向windiw
              fn.apply(this,args) //fn必须是普通函数,否则没法改变this指向
              this.timer = null//清除定时器
            },wait)
          }
        }
     }
const debounce = (func, wait = 50) => {
  // 缓存一个定时器id
  // 这里返回的函数是每次用户实际调用的防抖函数
  // 如果已经设定过定时器了就清空上一次的定时器
  // 开始一个新的定时器,延迟执行用户传入的方法
  return function(...args) {
    if (this.timer) clearTimeout(timer)
    this.timer = setTimeout(() => {
      func.apply(this, args)
    }, wait)
  }
}

call-apply-bind

1.call

Function.prototype.myCall = function (context = window, ...args) {
  // this-->func  context--> obj  args--> 传递过来的参数

  // 在context上加一个唯一值不影响context上的属性
  let key = Symbol("key")
  context[key] = this // context为调用的上下文,this此处为函数,将这个函数作为context的方法
  // let args = [...arguments].slice(1)   //第一个参数为obj所以删除,伪数组转为数组

  let result = context[key](...args)
  delete context[key] // 不删除会导致context属性越来越多
  return result
}

2.apply

Function.prototype.myApply = function (context = window, args) {
  // this-->func  context--> obj  args--> 传递过来的参数

  // 在context上加一个唯一值不影响context上的属性
  let key = Symbol("key")
  context[key] = this // context为调用的上下文,this此处为函数,将这个函数作为context的方法
  let result = context[key](...args) // 这里和call传参不一样
  delete context[key] // 不删除会导致context属性越来越多
  return result
}

3.bind

Function.prototype.myBind = function (context = window, ...args) {
  const temp = this
  return function F(...args2) {
    if (this instanceof F) {
      return new temp(...args, args2)
    }
    temp.myCall(context, ...args, ...args2)
  }
}

Promise

实现

class MyPromise {
  constructor(fn) {
    this.resolvedCallbacks = []
    this.rejectedCallbacks = []

    this.state = "PENDING"
    this.value = ""

    fn(this.resolve.bind(this), this.reject.bind(this))
  }

  resolve(value) {
    if (this.state === "PENDING") {
      this.state = "RESOLVED"
      this.value = value

      this.resolvedCallbacks.map((cb) => cb(value))
    }
  }

  reject(value) {
    if (this.state === "PENDING") {
      this.state = "REJECTED"
      this.value = value

      this.rejectedCallbacks.map((cb) => cb(value))
    }
  }

  then(onFulfilled, onRejected) {
    if (this.state === "PENDING") {
      this.resolvedCallbacks.push(onFulfilled)
      this.rejectedCallbacks.push(onRejected)
    }

    if (this.state === "RESOLVED") {
      onFulfilled(this.value)
    }

    if (this.state === "REJECTED") {
      onRejected(this.value)
    }
  }
}

异步函数

const time = (timer) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve()
    }, timer)
  })
}

const ajax1 = () =>
  time(2000).then(() => {
    console.log(1)
    return 1
  })

const ajax2 = () =>
  time(3000).then(() => {
    console.log(2)
    return 2
  })

const ajax3 = () => {
  time(1000).then(() => {
    console.log(3)
    return 3
  })
}

Promise.all

Promise.all = function (promises) {
  return new Promise((resolve, reject) => {
    let result = []
    let index = 0
    let len = promises.length
    if (len === 0) {
      resolve(result)
      return
    }

    for (let i = 0; i < len; i++) {
      // 为什么不直接 promise[i].then, 因为promise[i]可能不是一个promise
      Promise.resolve(promise[i])
        .then((data) => {
          result[i] = data
          index++
          if (index === len) resolve(result)
        })
        .catch((err) => {
          reject(err)
        })
    }
  })
}
Promise.all1([ajax1(), ajax2(), ajax3()]).then((res) => {
  console.log(res)
})

有顺序的执行 promose

function mergePromise(ajaxArr) {
  const data = [] //存放返回结果
  let promise = Promise.resolve()
  ajaxArr.forEach((ajax) => {
    promise = promise.then(ajax).then((res) => {
      data.push(res)
      return data
    })
  })
  return promise
}
mergePromise([ajax1, ajax2, ajax3]).then((res) => {
  console.log(res)
})

限制并发

function limitLoad(url, handler, limit) {
  let sequence = [...url]
  //初始化promise容器
  let promises = sequence.splice(0, limit).map((url, index) => {
    return handler(url).then(() => {
      return index
    })
  })
  return sequence
    .reduce((pCollect, url) => {
      return pCollect
        .then(() => {
          return Promise.race(promises) //返回已经完成的下标
        })
        .then((fastestIndex) => {
          promises[fastestIndex] = handler(url).then(() => {
            return fastestIndex
          })
        })
        .catch((err) => {
          console.log(err)
        })
    }, Promise.resolve())
    .then(() => {
      return Promise.all(promises)
    })
}

模拟红绿的灯

 async function fn() {
      await ajax1()
      await ajax2()
      await ajax3()
      await fn()
    }

-----------------------------------------------------
function fn() {
      Promise.resolve().then(ajax1).then(ajax2).then(ajax3).then(fn)
    }
  fn()

eventBus

class EventBus {
  constructor() {
    this._events = new Map()
  }
  on(type, fn) {
    const handler = this._events.get(type)
    if (!handler) {
      this._events.set(type, [fn])
    } else {
      handler.push(fn)
    }
  }
  emit(type, ...args) {
    let handler = this._events.get(type)
    if (handler) {
      for (let fn of handler) {
        fn.apply(this, args)
      }
    }
  }
  off(type, fn) {
    const handler = this._events.get(type)
    const index = handler.findIndex((e) => e === fn)
    if (index >= 0) {
      handler.splice(index, 1)
    }
    if (handler.length === 0) {
      this._events.delete(type)
    }
  }
  once(type, fn) {
    let _self = this
    function handler() {
      fn.apply(this, arguments)
      _self.off(type, handler)
    }
    this.on(type, handler)
  }
}

// 下面是 测试代码
function test1(...params) {
  console.log(11, params)
}

function test2(...params) {
  console.log(22, params)
}

function test3(...params) {
  console.log(33, params)
}

function test4(...params) {
  console.log(33, params)
}

//测试用例
let eb = new EventBus()
eb.on("event1", test1)
eb.on("event1", test2)
eb.on("event1", test3)
eb.emit("event1", "第一次")

eb.off("event1", test1)
eb.emit("event1", ["第二次1", "第二次2"])

eb.once("once", test4)
eb.emit("once", "执行一次", 1, 2, 3)
eb.emit("once", 134)

算法

伪数组转换

什么是伪数组

本身并不能调用数组方法,它是一个另外一种对象类型,只不过属性从 0 开始排,依次为 0,1,2…最后还有 callee 和 length 属性。我们也把这样的对象称为类数组

var arr = [..arguments]
let args = Array.prototype.slice.call(arguments);//(start,end)
let args = Array.from(arguments);
let args = Array.prototype.concat.apply([], arguments);

数组扁平化

  1. for 循环遍历判断是否数组 是的递归调用 concat 递归调用该数组 否则 push 新数组
var a = [1, [2, [3, 4, 5]]]
function flatten(arr) {
  let result = []

  for (let i = 0; i < arr.length; i++) {
    if (Array.isArray(arr[i])) {
      result = result.concat(flatten(arr[i]))
    } else {
      result.push(arr[i])
    }
  }
  return result
}
flatten(a) //  [1, 2, 3, 4,5]
  1. 利用 reduce 方法 +concat

    // 方法2
    var arr = [1, [2, [3, 4]]]
    function flatten(arr) {
      return arr.reduce(function (prev, next) {
        return prev.concat(Array.isArray(next) ? flatten(next) : next)
      }, [])
    }
    console.log(flatten(arr)) //  [1, 2, 3, 4,5]
  2. some+[…]

    // 方法3
    var arr = [1, [2, [3, 4]]]
    function flatten(arr) {
      while (arr.some((item) => Array.isArray(item))) {
        arr = [].concat(...arr)
      }
      return arr
    }
    console.log(flatten(arr)) //  [1, 2, 3, 4,5]
    1. arr.toString().split(‘,’);

    2. arr.flat([depth])

    3. 正则

      // 方法 6
      let arr = [1, ["2", [3, [4, 5]]], 6]
      function flatten(arr) {
        let str = JSON.stringify(arr) //[1,["2",[3,[4,5]]],6]
        str = str.replace(/(\[|\])/g, "")
        str = "[" + str + "]"
        return JSON.parse(str)
      }
      console.log(flatten(arr)) //  [1, 2, 3, 4,5]

深拷贝

  1. JSON.parse

    const newObj = JSON.parse(JSON.stringify(oldObj))

    局限性:

    • 会忽略 undefined

    • 不能序列化函数

    • 不能解决循环引用的对象

    • 他无法实现对函数 、RegExp 等特殊对象的克隆

    • 会抛弃对象的 constructor,所有的构造函数会指向 Object

    • 对象有循环引用,会报错

  2. (递归)

function deepClone(obj) {
  // 如果是 值类型 或 null,则直接return
  if (typeof obj !== "object" || obj === null) {
    return obj
  }
  let copy = {}
  if (obj.constructor === Array) {
    copy = []
  }
  // 遍历对象的key
  for (let key in obj) {
    // 如果key是对象的自有属性
    if (obj.hasOwnProperty(key)) {
      // 递归调用深拷贝方法
      copy[key] = deepClone(obj[key])
    }
  }
  return copy
}

数组去重

  1. new Set()

    ;[...new Set(arr)]
    Array.from(new Set(arr))

对象数组不能去重

  1. indexOf

    function unique(arr) {
      var array = []
      for (var i = 0; i < arr.length; i++) {
        if (array.indexOf(arr[i]) === -1) {
          array.push(arr[i])
        }
      }
      return array
    }

    对象数组 NaN 不能去重

  2. new Map()

function unique(arr) {
  let result = []
  let map = new Map()
  arr.forEach((item) => {
    if (!map.has(item)) {
      result.push(item)
      map.set(item, true)
    }
  })
  return result
}
  1. hasOwnProperty

简单版本

只保留一个对象

 typeof [1]+[1]  object1
  typeof {a:1}+{a:1}  object[object Object]

function unique(arr){
        let result = []
        let obj = {}
        arr.forEach(item => {
          if(!obj.hasOwnProperty(typeof item+item)){
            result.push(item)
            obj[typeof item+item] = true
          }
        });
        return result
    }

完整版本

function unique(arr) {
  let result = []
  let obj = {}
  arr.forEach((item) => {
    if (Object.prototype.toString.call(item) === "[object Object]") {
      // console.log(item);
      // console.log(JSON.stringify(item));
      if (!obj.hasOwnProperty(JSON.stringify(item))) {
        console.log(JSON.stringify(item))
        result.push(item)
        obj[JSON.stringify(item)] = true
      }
    }
    if (
      !obj.hasOwnProperty(typeof item + item) &&
      Object.prototype.toString.call(item) !== "[object Object]"
    ) {
      console.log("a", item)
      result.push(item)
      obj[typeof item + item] = true
    }
  })
  return result
}

排序

  1. 冒泡

    每一轮操作,都会将这一轮中最大的元素放置到数组的末尾

    function bubbleSort(arr) {
      // 缓存数组长度
      const len = arr.length
      // 外层循环用于控制从头到尾的比较+交换到底有多少轮
      for (let i = 0; i < len; i++) {
        // 区别在这里,我们加了一个标志位
        let flag = false
        // 内层循环用于完成每一轮遍历过程中的重复比较+交换
        for (let j = 0; j < len - 1 - i; j++) {
          // 若相邻元素前面的数比后面的大
          if (arr[j] > arr[j + 1]) {
            // 交换两者
            ;[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
            flag = true
          }
        }
        // 若一次交换也没发生,则说明数组有序,直接放过
        if (flag == false) return arr
      }
      // 返回数组
      return arr
    }
  2. 选择排序

    每次都找出当前范围内的最小值,把它放在当前范围的头部

    function selectSort(arr) {
      // 缓存数组长度
      const len = arr.length
      // 定义 minIndex,缓存当前区间最小值的索引,注意是索引
      let minIndex
      // i 是当前排序区间的起点
      for (let i = 0; i < len - 1; i++) {
        // 初始化 minIndex 为当前区间第一个元素
        minIndex = i
        // i、j分别定义当前区间的上下界,i是左边界,j是右边界
        for (let j = i; j < len; j++) {
          // 若 j 处的数据项比当前最小值还要小,则更新最小值索引为 j
          if (arr[j] < arr[minIndex]) {
            minIndex = j
          }
        }
        // 如果 minIndex 对应元素不是目前的头部元素,则交换两者
        if (minIndex !== i) {
          ;[arr[i], arr[minIndex]] = [arr[minIndex], arr[i]]
        }
      }
      return arr
    }
  3. 插入排序

    找到元素在它前面那个序列中的正确位置

function insertSort(arr) {
  // 缓存数组长度
  const len = arr.length
  // temp 用来保存当前需要插入的元素
  let temp
  // i用于标识每次被插入的元素的索引
  for (let i = 1; i < len; i++) {
    // j用于帮助 temp 寻找自己应该有的定位
    let j = i
    temp = arr[i]
    // 判断 j 前面一个元素是否比 temp 大
    while (j > 0 && arr[j - 1] > temp) {
      // 如果是,则将 j 前面的一个元素后移一位,为 temp 让出位置
      arr[j] = arr[j - 1]
      j--
    }
    // 循环让位,最后得到的 j 就是 temp 的正确索引
    arr[j] = temp
  }
  return arr
}

归并排序

归并排序是对分治思想的典型应用,它按照如下的思路对分治思想“三步走”的框架进行了填充:

  • 分解子问题:将需要被排序的数组从中间分割为两半,然后再将分割出来的每个子数组各分割为两半,重复以上操作,直到单个子数组只有一个元素为止。
  • 求解每个子问题:从粒度最小的子数组开始,两两合并、确保每次合并出来的数组都是有序的。(这里的“子问题”指的就是对每个子数组进行排序)。 合并子问题的解,得出大问题的解:当数组被合并至原有的规模时,就得到了一个完全排序的数组
function mergeSort(arr) {
  const len = arr.length
  // 处理边界情况
  if (len <= 1) {
    return arr
  }
  // 计算分割点
  const mid = Math.floor(len / 2)
  // 递归分割左子数组,然后合并为有序数组
  const leftArr = mergeSort(arr.slice(0, mid))
  // 递归分割右子数组,然后合并为有序数组
  const rightArr = mergeSort(arr.slice(mid, len))
  // 合并左右两个有序数组
  arr = mergeArr(leftArr, rightArr)
  // 返回合并后的结果
  return arr
}

function mergeArr(arr1, arr2) {
  // 初始化两个指针,分别指向 arr1 和 arr2
  let i = 0,
    j = 0
  // 初始化结果数组
  const res = []
  // 缓存arr1的长度
  const len1 = arr1.length
  // 缓存arr2的长度
  const len2 = arr2.length
  // 合并两个子数组
  while (i < len1 && j < len2) {
    if (arr1[i] < arr2[j]) {
      res.push(arr1[i])
      i++
    } else {
      res.push(arr2[j])
      j++
    }
  }
  // 若其中一个子数组首先被合并完全,则直接拼接另一个子数组的剩余部分
  if (i < len1) {
    return res.concat(arr1.slice(i))
  } else {
    return res.concat(arr2.slice(j))
  }
}

快排

function partition(arr, left = 0, right = arr.length - 1) {
  if (arr.length <= 1) {
    return arr
  }
  if (left > right) {
    return
  }

  let value = arr[left]
  let i = left,
    j = right
  while (i !== j) {
    while (arr[j] >= value && j > i) {
      j--
    }
    while (arr[i] <= value && i < j) {
      i++
    }
    if (i < j) {
      ;[arr[i], arr[j]] = [arr[j], arr[i]]
    }
  }
  ;[arr[i], arr[left]] = [arr[left], arr[i]]
  partition(arr, left, i - 1)
  partition(arr, i + 1, right)
  return arr
}

堆排序

字符串

转化为驼峰命名

var s1 = "get-element-by-id"

// 转化为 getElementById

var f = function (s) {
  return s.replace(/-\w/g, function (x) {
    return x.slice(1).toUpperCase()
  })
}

判断是否是回文串

str ==== str.split('').reverse().join('')
----------------------
function isPalindrome(str) {
    // 缓存字符串的长度
    const len = str.length
    // 遍历前半部分,判断和后半部分是否对称
    for(let i=0;i<len/2;i++) {
        if(str[i]!==str[len-i-1]) {
            return false
        }
    }
    return true
}
--------------------
 // 工具方法,用于判断字符串是否回文
    function isPalindrome(st, ed) {
        while(st<ed) {
            if(s[st] !== s[ed]) {
                return false
            }
            st++
            ed--
        }
        return true
    }

删除一个是否回文

const validPalindrome = function (s) {
  // 缓存字符串的长度
  const len = s.length

  // i、j分别为左右指针
  let i = 0,
    j = len - 1

  // 当左右指针均满足对称时,一起向中间前进
  while (i < j && s[i] === s[j]) {
    i++
    j--
  }

  // 尝试判断跳过左指针元素后字符串是否回文
  if (isPalindrome(i + 1, j)) {
    return true
  }
  // 尝试判断跳过右指针元素后字符串是否回文
  if (isPalindrome(i, j - 1)) {
    return true
  }

  // 工具方法,用于判断字符串是否回文
  function isPalindrome(st, ed) {
    while (st < ed) {
      if (s[st] !== s[ed]) {
        return false
      }
      st++
      ed--
    }
    return true
  }

  // 默认返回 false
  return false
}

无重复字符的最长子串

var lengthOfLongestSubstring = function (s) {
  const set = new Set() //判断滑动窗口内是否有重复元素
  let j = 0, //滑动窗口左边界
    maxLength = 0
  if (s.length === 0) {
    //极端情况
    return 0
  }
  for (let i = 0; i < s.length; i++) {
    //滑动窗口右边界
    if (!set.has(s[i])) {
      //当前元素不在set中 就加入set 然后更新最大长度,i++继续下一轮循环
      set.add(s[i])
      maxLength = Math.max(maxLength, set.size)
    } else {
      //set中有重复元素不断让j++ 并删除窗口之外的元素 直到滑动窗口内没有重复的元素
      while (set.has(s[i])) {
        set.delete(s[j])
        j++
      }
      set.add(s[i]) //放心将s[i]加入set中
    }
  }
  return maxLength
}

最长回文子串

var longestPalindrome = function (s) {
  if (s.length < 2) {
    return s
  }
  let l = 0
  let r = 0
  for (let i = 0; i < s.length; i++) {
    // 回文子串长度是奇数
    helper(i, i)
    // 回文子串长度是偶数
    helper(i, i + 1)
  }

  function helper(m, n) {
    while (m >= 0 && n < s.length && s[m] == s[n]) {
      m--
      n++
    }
    // 注意此处m,n的值循环完后  是恰好不满足循环条件的时刻 如果此轮询得到回文串长度大于之前记录, 记录此轮循边界
    if (n - m - 1 > r - l - 1) {
      r = n
      l = m
    }
  }
  return s.slice(l + 1, r)
}

最长上升子序列模型

// 入参是一个数字序列
const lengthOfLIS = function (nums) {
  // 缓存序列的长度
  const len = nums.length
  // 处理边界条件
  if (!len) {
    return 0
  }
  // 初始化数组里面每一个索引位的状态值
  const dp = new Array(len).fill(1)
  // 初始化最大上升子序列的长度为1
  let maxLen = 1
  // 从第2个元素开始,遍历整个数组
  for (let i = 1; i < len; i++) {
    // 每遍历一个新元素,都要“回头看”,看看能不能延长原有的上升子序列
    for (let j = 0; j < i; j++) {
      // 若遇到了一个比当前元素小的值,则意味着遇到了一个可以延长的上升子序列,故更新当前元素索引位对应的状态
      if (nums[j] < nums[i]) {
        dp[i] = Math.max(dp[i], dp[j] + 1)
      }
    }
    // 及时更新上升子序列长度的最大值
    if (dp[i] > maxLen) {
      maxLen = dp[i]
    }
  }
  // 遍历完毕,最后到手的就是最大上升子序列的长度
  return maxLen
}

数组

创建

const arr = new Array(7).fill(1)

注意:fill([])后面传递的是引用数据类型

两数求和问题

哈希表 map

const twoSum = function (nums, target) {
  // 这里我用对象来模拟 map 的能力
  const map = {}
  // 缓存数组长度
  const len = nums.length
  // 遍历数组
  for (let i = 0; i < len; i++) {
    // 判断当前值对应的 target 差值是否存在(是否已遍历过)
    if (map[target - nums[i]]) {
      // 若有对应差值,那么答案get!
      return [map[target - nums[i]], i]
    }
    // 若没有对应差值,则记录当前值
    map[nums[i]] = i
  }
}

合并升序数组

const merge = function (nums1, m, nums2, n) {
  // 初始化两个指针的指向,初始化 nums1 尾部索引k
  let i = m - 1,
    j = n - 1,
    k = m + n - 1
  // 当两个数组都没遍历完时,指针同步移动
  while (i >= 0 && j >= 0) {
    // 取较大的值,从末尾往前填补
    if (nums1[i] >= nums2[j]) {
      nums1[k] = nums1[i]
      i--
      k--
    } else {
      nums1[k] = nums2[j]
      j--
      k--
    }
  }

  // nums2 留下的情况,特殊处理一下
  while (j >= 0) {
    nums1[k] = nums2[j]
    k--
    j--
  }
}
function mergeArr(arr1, arr2) {
  // 初始化两个指针,分别指向 arr1 和 arr2
  let i = 0,
    j = 0
  // 初始化结果数组
  const res = []
  // 缓存arr1的长度
  const len1 = arr1.length
  // 缓存arr2的长度
  const len2 = arr2.length
  // 合并两个子数组
  while (i < len1 && j < len2) {
    if (arr1[i] < arr2[j]) {
      res.push(arr1[i])
      i++
    } else {
      res.push(arr2[j])
      j++
    }
  }
  // 若其中一个子数组首先被合并完全,则直接拼接另一个子数组的剩余部分
  if (i < len1) {
    return res.concat(arr1.slice(i))
  } else {
    return res.concat(arr2.slice(j))
  }
}

三数求和

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
const threeSum = function (nums) {
  // 用于存放结果数组
  let res = []
  // 给 nums 排序
  nums = nums.sort((a, b) => {
    return a - b
  })
  // 缓存数组长度
  const len = nums.length
  // 注意我们遍历到倒数第三个数就足够了,因为左右指针会遍历后面两个数
  for (let i = 0; i < len - 2; i++) {
    // 左指针 j
    let j = i + 1
    // 右指针k
    let k = len - 1
    // 如果遇到重复的数字,则跳过
    if (i > 0 && nums[i] === nums[i - 1]) {
      continue
    }
    while (j < k) {
      // 三数之和小于0,左指针前进
      if (nums[i] + nums[j] + nums[k] < 0) {
        j++
        // 处理左指针元素重复的情况
        while (j < k && nums[j] === nums[j - 1]) {
          j++
        }
      } else if (nums[i] + nums[j] + nums[k] > 0) {
        // 三数之和大于0,右指针后退
        k--

        // 处理右指针元素重复的情况
        while (j < k && nums[k] === nums[k + 1]) {
          k--
        }
      } else {
        // 得到目标数字组合,推入结果数组
        res.push([nums[i], nums[j], nums[k]])

        // 左右指针一起前进
        j++
        k--

        // 若左指针元素重复,跳过
        while (j < k && nums[j] === nums[j - 1]) {
          j++
        }

        // 若右指针元素重复,跳过
        while (j < k && nums[k] === nums[k + 1]) {
          k--
        }
      }
    }
  }

  // 返回结果数组
  return res
}

比较版本号

var compareVersion = function (version1, version2) {
  const arr1 = version1.split(".")
  const arr2 = version2.split(".")

  while (arr1.length && arr2.length) {
    const n1 = Number(arr1.shift())
    const n2 = Number(arr2.shift())

    if (n1 > n2) return 1
    if (n1 < n2) return -1
  }
  if (arr1.length) {
    // arr2 数组已经为空
    return arr1.every((item) => Number(item) === 0) ? 0 : 1
  }
  //if (temp2.length === 0) {
  //    for (const value of temp1) {
  //    if (Number(value) !== 0) return 1
  //   }
  //}
  if (arr2.length) {
    // arr1 数组已经为空
    return arr2.every((item) => Number(item) === 0) ? 0 : -1
  }

  return 0
}

千位分隔符

var thousandSeparator = function (n) {
  let str = n.toString()
  return str.replace(/\d{1,3}(?=(\d{3})+$)/g, (s) => `${s}.`)
}
var thousandSeparator = function (n) {
  let arr = n.toString().split("")
  for (let i = arr.length - 3; i >= 0; i -= 3) {
    arr.splice(i, 0, ".")
  }
  return arr.join("").replace(/^\./, "")
}

拆解 URL 参数中 queryString

function querySearch(url) {
  const query = url.split("?").pop().split("#").shift().split("&")
  const res = {}
  query.forEach((item) => {
    const [key, val] = item.split("=")
    res[key] = val
  })
  return res
}
const url = "www.alipay.com/index.html?user=anyone&tip=haha#first"
console.log(querySearch(url))

返回最接近输入值的数字

function findNext(n, arr) {
  const tempArr = [...arr]
  tempArr.push(n)
  tempArr.sort((a, b) => {
    return a - b
  })
  let index = tempArr.indexOf(n)
  if (index === 0) {
    return tempArr[index + 1]
  } else if (index === tempArr.length - 1) {
    return tempArr[index - 1]
  }

  return tempArr[index] - tempArr[index - 1] >=
    tempArr[index + 1] - tempArr[index]
    ? tempArr[index + 1]
    : tempArr[index - 1]
}

连续子数组的最大和

  1. 贪心算法
var maxSubArray = function (nums) {
  let ans = nums[0]
  let sum = 0
  for (const num of nums) {
    if (sum > 0) {
      // 继续加当前元素
      sum += num
    } else {
      // 加上当前元素只会对最终数组和起减少的作用,而不是增大数组和,所以就直接以当前元素为起点新起数组求最大数组和
      sum = num
    }
    ans = Math.max(ans, sum)
  }
  return ans
}
  1. 动态规划

    /**
     * @param {number[]} nums
     * @return {number}
     */
    var maxSubArray = function (nums) {
      let n = nums.length
      if (n == 0) return 0
      let dp = new Array(n).fill(0)
      // base case 第一个元素前面没有子数组
      dp[0] = nums[0]
      // 状态转移方程
      for (let i = 1; i < n; i++) {
        dp[i] = Math.max(
          // 自成一派
          nums[i],
          // 与前面的子数组合并
          nums[i] + dp[i - 1]
        )
      }
      return Math.max(...dp)
    }
    /**
     * @param {number[]} nums
     * @return {number}
     */
    var maxSubArray = function (nums) {
      let n = nums.length
      if (n == 0) return 0
      let cur = nums[0] //保存上一次的最大值
      let res = cur //保存结果
      for (let i = 1; i < n; i++) {
        cur = Math.max(nums[i], nums[i] + cur)
        res = Math.max(res, cur)
      }
      return res
    }

    全排列

    // 入参是一个数组
    const permute = function (nums) {
      // 缓存数组的长度
      const len = nums.length
      // curr 变量用来记录当前的排列内容
      const curr = []
      // res 用来记录所有的排列顺序
      const res = []
      // visited 用来避免重复使用同一个数字
      const visited = {}
      // 定义 dfs 函数,入参是坑位的索引(从 0 计数)
      function dfs(nth) {
        // 若遍历到了不存在的坑位(第 len+1 个),则触碰递归边界返回
        if (nth === len) {
          // 此时前 len 个坑位已经填满,将对应的排列记录下来
          res.push(curr.slice())
          return
        }
        // 检查手里剩下的数字有哪些
        for (let i = 0; i < len; i++) {
          // 若 nums[i] 之前没被其它坑位用过,则可以理解为“这个数字剩下了”
          if (!visited[nums[i]]) {
            // 给 nums[i] 打个“已用过”的标
            visited[nums[i]] = 1
            // 将nums[i]推入当前排列
            curr.push(nums[i])
            // 基于这个排列继续往下一个坑走去
            dfs(nth + 1)
            // nums[i]让出当前坑位
            curr.pop()
            // 下掉“已用过”标识
            visited[nums[i]] = 0
          }
        }
      }
      // 从索引为 0 的坑位(也就是第一个坑位)开始 dfs
      dfs(0)
      return res
    }

队列

如何用栈实现一个队列?

  • push(x) – 将一个元素放入队列的尾部。

  • pop() – 从队列首部移除元素。

  • peek() – 返回队列首部的元素。

  • empty() – 返回队列是否为空。

    /**
     * 初始化构造函数
     */
    const MyQueue = function () {
      // 初始化两个栈
      this.stack1 = []
      this.stack2 = []
    }
    
    /**
     * Push element x to the back of queue.
     * @param {number} x
     * @return {void}
     */
    MyQueue.prototype.push = function (x) {
      // 直接调度数组的 push 方法
      this.stack1.push(x)
    }
    
    /**
     * Removes the element from in front of queue and returns that element.
     * @return {number}
     */
    MyQueue.prototype.pop = function () {
      // 假如 stack2 为空,需要将 stack1 的元素转移进来
      if (this.stack2.length <= 0) {
        // 当 stack1 不为空时,出栈
        while (this.stack1.length !== 0) {
          // 将 stack1 出栈的元素推入 stack2
          this.stack2.push(this.stack1.pop())
        }
      }
      // 为了达到逆序的目的,我们只从 stack2 里出栈元素
      return this.stack2.pop()
    }
    
    /**
     * Get the front element.
     * @return {number}
     * 这个方法和 pop 唯一的区别就是没有将定位到的值出栈
     */
    MyQueue.prototype.peek = function () {
      if (this.stack2.length <= 0) {
        // 当 stack1 不为空时,出栈
        while (this.stack1.length != 0) {
          // 将 stack1 出栈的元素推入 stack2
          this.stack2.push(this.stack1.pop())
        }
      }
      // 缓存 stack2 的长度
      const stack2Len = this.stack2.length
      return stack2Len && this.stack2[stack2Len - 1]
    }
    
    /**
     * Returns whether the queue is empty.
     * @return {boolean}
     */
    MyQueue.prototype.empty = function () {
      // 若 stack1 和 stack2 均为空,那么队列空
      return !this.stack1.length && !this.stack2.length
    }

    找出所有滑动窗口里的最大值。

    const maxSlidingWindow = function (nums, k) {
      // 缓存数组的长度
      const len = nums.length
      // 初始化结果数组
      const res = []
      // 初始化双端队列
      const deque = []
      // 开始遍历数组
      for (let i = 0; i < len; i++) {
        // 当队尾元素小于当前元素时
        while (deque.length && nums[deque[deque.length - 1]] < nums[i]) {
          // 将队尾元素(索引)不断出队,直至队尾元素大于等于当前元素
          deque.pop()
        }
        // 入队当前元素索引(注意是索引)
        deque.push(i)
        // 当队头元素的索引已经被排除在滑动窗口之外时
        while (deque.length && deque[0] <= i - k) {
          // 将队头元素索引出队
          deque.shift()
        }
        // 判断滑动窗口的状态,只有在被遍历的元素个数大于 k 的时候,才更新结果数组
        if (i >= k - 1) {
          res.push(nums[deque[0]])
        }
      }
      // 返回结果数组
      return res
    }

栈的设计——“最小栈”问题

const MinStack = function () {
  this.stack = []
  // 定义辅助栈
  this.stack2 = []
}

/**
 * @param {number} x
 * @return {void}
 */
MinStack.prototype.push = function (x) {
  this.stack.push(x)
  // 若入栈的值小于当前最小值,则推入辅助栈栈顶
  if (this.stack2.length == 0 || this.stack2[this.stack2.length - 1] >= x) {
    this.stack2.push(x)
  }
}

/**
 * @return {void}
 */
MinStack.prototype.pop = function () {
  // 若出栈的值和当前最小值相等,那么辅助栈也要对栈顶元素进行出栈,确保最小值的有效性
  if (this.stack.pop() == this.stack2[this.stack2.length - 1]) {
    this.stack2.pop()
  }
}

/**
 * @return {number}
 */
MinStack.prototype.top = function () {
  return this.stack[this.stack.length - 1]
}

/**
 * @return {number}
 */
MinStack.prototype.getMin = function () {
  // 辅助栈的栈顶,存的就是目标中的最小值
  return this.stack2[this.stack2.length - 1]
}

有效括号

var isValid = function (s) {
  var map = new Map([
    ["(", ")"],
    ["[", "]"],
    ["{", "}"],
  ])
  var arr = []
  var i = 0
  for (const ch of s) {
    switch (ch) {
      case "(":
      case "[":
      case "{":
        arr.push(ch)
        i++
        break
      case ")":
      case "]":
      case "}":
        // arr.push()
        if (ch == map.get(arr[i - 1])) {
          arr.pop()
          i--
        } else {
          return false
        }
    }
  }
  return arr.length === 0
}

链表

链表的合并

const mergeTwoLists = function (l1, l2) {
  // 定义头结点,确保链表可以被访问到
  let head = new ListNode()
  // cur 这里就是咱们那根“针”
  let cur = head
  // “针”开始在 l1 和 l2 间穿梭了
  while (l1 && l2) {
    // 如果 l1 的结点值较小
    if (l1.val <= l2.val) {
      // 先串起 l1 的结点
      cur.next = l1
      // l1 指针向前一步
      l1 = l1.next
    } else {
      // l2 较小时,串起 l2 结点
      cur.next = l2
      // l2 向前一步
      l2 = l2.next
    }

    // “针”在串起一个结点后,也会往前一步
    cur = cur.next
  }

  // 处理链表不等长的情况
  cur.next = l1 !== null ? l1 : l2
  // 返回起始结点
  return head.next
}

链表结点的删除

给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。

const deleteDuplicates = function (head) {
  // 设定 cur 指针,初始位置为链表第一个结点
  let cur = head
  // 遍历链表
  while (cur != null && cur.next != null) {
    // 若当前结点和它后面一个结点值相等(重复)
    if (cur.val === cur.next.val) {
      // 删除靠后的那个结点(去重)
      cur.next = cur.next.next
    } else {
      // 若不重复,继续遍历
      cur = cur.next
    }
  }
  return head
}

给定一个排序链表,删除所有含有重复数字的结点,只保留原始链表中 没有重复出现的数字。

const deleteDuplicates = function (head) {
  // 极端情况:0个或1个结点,则不会重复,直接返回
  if (!head || !head.next) {
    return head
  }
  // dummy 登场
  let dummy = new ListNode()
  // dummy 永远指向头结点
  dummy.next = head
  // cur 从 dummy 开始遍历
  let cur = dummy
  // 当 cur 的后面有至少两个结点时
  while (cur.next && cur.next.next) {
    // 对 cur 后面的两个结点进行比较
    if (cur.next.val === cur.next.next.val) {
      // 若值重复,则记下这个值
      let val = cur.next.val
      // 反复地排查后面的元素是否存在多次重复该值的情况
      while (cur.next && cur.next.val === val) {
        // 若有,则删除
        cur.next = cur.next.next
      }
    } else {
      // 若不重复,则正常遍历
      cur = cur.next
    }
  }
  // 返回链表的起始结点
  return dummy.next
}

删除链表的倒数第 N 个结点

const removeNthFromEnd = function (head, n) {
  // 初始化 dummy 结点

  const dummy = new ListNode()
  // dummy指向头结点
  dummy.next = head
  // 初始化快慢指针,均指向dummy
  let fast = dummy
  let slow = dummy

  // 快指针闷头走 n 步
  while (n !== 0) {
    fast = fast.next
    n--
  }

  // 快慢指针一起走
  while (fast.next) {
    fast = fast.next
    slow = slow.next
  }

  // 慢指针删除自己的后继结点
  slow.next = slow.next.next
  // 返回头结点
  return dummy.next
}

链表的反转

const reverseList = function (head) {
  // 初始化前驱结点为 null
  let pre = null
  // 初始化目标结点为头结点
  let cur = head
  // 只要目标结点不为 null,遍历就得继续
  while (cur !== null) {
    // 记录一下 next 结点
    let next = cur.next
    // 反转指针
    cur.next = pre
    // pre 往前走一步
    pre = cur
    // cur往前走一步
    cur = next
  }
  // 反转结束后,pre 就会变成新链表的头结点
  return pre
}

局部反转一个链表

const reverseBetween = function (head, m, n) {
  // 定义pre、cur,用leftHead来承接整个区间的前驱结点
  let pre, cur, leftHead
  // 别忘了用 dummy 嗷
  const dummy = new ListNode()
  // dummy后继结点是头结点
  dummy.next = head
  // p是一个游标,用于遍历,最初指向 dummy
  let p = dummy
  // p往前走 m-1 步,走到整个区间的前驱结点处
  for (let i = 0; i < m - 1; i++) {
    p = p.next
  }
  // 缓存这个前驱结点到 leftHead 里
  leftHead = p
  // start 是反转区间的第一个结点
  let start = leftHead.next
  // pre 指向start
  pre = start
  // cur 指向 start 的下一个结点
  cur = pre.next
  // 开始重复反转动作
  for (let i = m; i < n; i++) {
    let next = cur.next
    cur.next = pre
    pre = cur
    cur = next
  }
  //  leftHead 的后继结点此时为反转后的区间的第一个结点
  leftHead.next = pre
  // 将区间内反转后的最后一个结点 next 指向 cur
  start.next = cur
  // dummy.next 永远指向链表头结点
  return dummy.next
}

判断链表是否成环

const hasCycle = function (head) {
  // 只要结点存在,那么就继续遍历
  while (head) {
    // 如果 flag 已经立过了,那么说明环存在
    if (head.flag) {
      return true
    } else {
      // 如果 flag 没立过,就立一个 flag 再往 下走
      head.flag = true
      head = head.next
    }
  }
  return false
}

定位环的起点

const detectCycle = function (head) {
  while (head) {
    if (head.flag) {
      return head
    } else {
      head.flag = true
      head = head.next
    }
  }
  return null
}

相交链表

  1. 哈希表
var getIntersectionNode = function (headA, headB) {
  if (!headA || !headB) return null
  const hashmap = new Map()

  let pA = headA
  while (pA) {
    hashmap.set(pA, 1)
    pA = pA.next
  }

  let pB = headB
  while (pB) {
    if (hashmap.has(pB)) return pB
    pB = pB.next
  }
  return null
}
  1. 双指针

    var getIntersectionNode = function (headA, headB) {
      if (!headA || !headB) return null
    
      let pA = headA,
        pB = headB
      while (pA !== pB) {
        pA = pA === null ? headB : pA.next
        pB = pB === null ? headA : pB.next
      }
      return pA
    }

二叉树

递归遍历

var preorderTraversal = function (root) {
  // 首先声明一个数组用来存放遍历得到的节点val值
  const result = []
  // 采用递归遍历
  function preorder(node) {
    // 如果节点为空直接返回
    if (!node) return
    // 先序遍历就是把当前节点输出 放在左右递归调用之前 将其数值放入结果数组
    result.push(node.val)
    // 然后递归遍历左孩子
    preorder(node.left)
    // 最后递归遍历右孩子
    preorder(node.right)
  }
  preorder(root)
  // 返回结果
  return result
}

迭代遍历(利用栈思想)

  1. 前序

    /**
     * @param {TreeNode} root
     * @return {number[]}
     */
    const preorderTraversal = function (root) {
      // 定义结果数组
      const res = []
      // 处理边界条件
      if (!root) {
        return res
      }
      // 初始化栈结构
      const stack = []
      // 首先将根结点入栈
      stack.push(root)
      // 若栈不为空,则重复出栈、入栈操作
      while (stack.length) {
        // 将栈顶结点记为当前结点
        const cur = stack.pop()
        // 当前结点就是当前子树的根结点,把这个结点放在结果数组的尾部
        res.push(cur.val)
        // 若当前子树根结点有右孩子,则将右孩子入栈
        if (cur.right) {
          stack.push(cur.right)
        }
        // 若当前子树根结点有左孩子,则将左孩子入栈
        if (cur.left) {
          stack.push(cur.left)
        }
      }
      // 返回结果数组
      return res
    }
  2. 后序

    /**
     * @param {TreeNode} root
     * @return {number[]}
     */
    const postorderTraversal = function (root) {
      // 定义结果数组
      const res = []
      // 处理边界条件
      if (!root) {
        return res
      }
      // 初始化栈结构
      const stack = []
      // 首先将根结点入栈
      stack.push(root)
      // 若栈不为空,则重复出栈、入栈操作
      while (stack.length) {
        // 将栈顶结点记为当前结点
        const cur = stack.pop()
        // 当前结点就是当前子树的根结点,把这个结点放在结果数组的头部
        res.unshift(cur.val)
        // 若当前子树根结点有左孩子,则将左孩子入栈
        if (cur.left) {
          stack.push(cur.left)
        }
        // 若当前子树根结点有右孩子,则将右孩子入栈
        if (cur.right) {
          stack.push(cur.right)
        }
      }
      // 返回结果数组
      return res
    }

中序

const inorderTraversal = function (root) {
  // 定义结果数组
  const res = []
  // 初始化栈结构
  const stack = []
  // 用一个 cur 结点充当游标
  let cur = root
  // 当 cur 不为空、或者 stack 不为空时,重复以下逻辑
  while (cur || stack.length) {
    // 这个 while 的作用是把寻找最左叶子结点的过程中,途径的所有结点都记录下来
    while (cur) {
      // 将途径的结点入栈
      stack.push(cur)
      // 继续搜索当前结点的左孩子
      cur = cur.left
    }
    // 取出栈顶元素
    cur = stack.pop()
    // 将栈顶元素入栈
    res.push(cur.val)
    // 尝试读取 cur 结点的右孩子
    cur = cur.right
  }
  // 返回结果数组
  return res
}

层序遍历

  1. 返回数组
function BFS(root) {
  const res = []
  const queue = [] // 初始化队列queue
  // 根结点首先入队
  queue.push(root)
  // 队列不为空,说明没有遍历完全
  while (queue.length) {
    const top = queue[0] // 取出队头元素
    // 访问 top
    res.push(top.val)
    // 如果左子树存在,左子树入队
    if (top.left) {
      queue.push(top.left)
    }
    // 如果右子树存在,右子树入队
    if (top.right) {
      queue.push(top.right)
    }
    queue.shift() // 访问完毕,队头元素出队
  }
}
  1. 返回二维数组
var levelOrder = function (root) {
  /* 非递归的实现方式 */
  let res = []
  if (root == null) return res
  let queue = [root]
  // while 循环控制从上向下一层层遍历
  while (queue.length) {
    let size = queue.length
    // 记录这一层的节点值
    let level = []
    // for 循环控制每一层从左向右遍历
    for (let i = 0; i < size; i++) {
      let cur = queue.shift()
      level.push(cur.val)
      if (cur.left != null) queue.push(cur.left)
      if (cur.right != null) queue.push(cur.right)
    }
    res.push(level)
  }
  return res
}

动态规划

爬楼梯

const climbStairs = function(n) {
    // 处理递归边界
    if(n === 1) {
        return 1
    }
    if(n === 2){
        return 2
    }
    // 递归计算
    return climbStairs(n-1) + climbStairs(n-2)
};
--------------------
// 定义记忆数组 f
const f = []
const climbStairs = function(n) {
  if(n==1) {
      return 1
  }
  if(n==2) {
      return 2
  }
  // 若f[n]不存在,则进行计算
  if(f[n]===undefined)  f[n] = climbStairs(n-1) + climbStairs(n-2)
  // 若f[n]已经求解过,直接返回
  return f[n]
};


-------------
  const climbStairs = function(n) {
    // 初始化状态数组
    const f = [];
    // 初始化已知值
    f[1] = 1;
    f[2] = 2;
    // 动态更新每一层楼梯对应的结果
    for(let i = 3;i <= n;i++){
        f[i] = f[i-2] + f[i-1];
    }
    // 返回目标值
    return f[n];
};

如何优雅地找硬币

const coinChange = function (coins, amount) {
  // 用于保存每个目标总额对应的最小硬币个数
  const f = []
  // 提前定义已知情况
  f[0] = 0
  // 遍历 [1, amount] 这个区间的硬币总额
  for (let i = 1; i <= amount; i++) {
    // 求的是最小值,因此我们预设为无穷大,确保它一定会被更小的数更新
    f[i] = Infinity
    // 循环遍历每个可用硬币的面额
    for (let j = 0; j < coins.length; j++) {
      // 若硬币面额小于目标总额,则问题成立
      if (i - coins[j] >= 0) {
        // 状态转移方程
        f[i] = Math.min(f[i], f[i - coins[j]] + 1)
      }
    }
  }
  // 若目标总额对应的解为无穷大,则意味着没有一个符合条件的硬币总数来更新它,本题无解,返回-1
  if (f[amount] === Infinity) {
    return -1
  }
  // 若有解,直接返回解的内容
  return f[amount]
}

背包模型

function knapsack(n, c, w, value) {
  // dp是动态规划的状态保存数组
  const dp = new Array(c + 1).fill(0)
  // res 用来记录所有组合方案中的最大值
  let res = -Infinity
  for (let i = 1; i <= n; i++) {
    for (let v = c; v >= w[i]; v--) {
      // 写出状态转移方程
      dp[v] = Math.max(dp[v], dp[v - w[i]] + value[i])
      // 即时更新最大值
      if (dp[v] > res) {
        res = dp[v]
      }
    }
  }
  return res
}

最长上升子序列模型

// 入参是一个数字序列
const lengthOfLIS = function (nums) {
  // 缓存序列的长度
  const len = nums.length
  // 处理边界条件
  if (!len) {
    return 0
  }
  // 初始化数组里面每一个索引位的状态值
  const dp = new Array(len).fill(1)
  // 初始化最大上升子序列的长度为1
  let maxLen = 1
  // 从第2个元素开始,遍历整个数组
  for (let i = 1; i < len; i++) {
    // 每遍历一个新元素,都要“回头看”,看看能不能延长原有的上升子序列
    for (let j = 0; j < i; j++) {
      // 若遇到了一个比当前元素小的值,则意味着遇到了一个可以延长的上升子序列,故更新当前元素索引位对应的状态
      if (nums[j] < nums[i]) {
        dp[i] = Math.max(dp[i], dp[j] + 1)
      }
    }
    // 及时更新上升子序列长度的最大值
    if (dp[i] > maxLen) {
      maxLen = dp[i]
    }
  }
  // 遍历完毕,最后到手的就是最大上升子序列的长度
  return maxLen
}

函数

实现 add(1)(2)()

function add(a) {
  return function (b) {
    if (!b) return a
    return add(a + b)
  }
}

![image-20220330143049151](/Users/didi/Library/Application Support/typora-user-images/image-20220330143049151.png)


文章作者: 高红翔
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 高红翔 !
  目录