算法题

247 阅读43分钟

树的右侧视图:

var rightSideView = function(root) {

if(!root) return []

  let queue = [root]                        *//* 队列 **把树顶加入队列

  let arr = []                              *//* 用来存储每层最后个元素值

  while(queue.length > 0){

    console.log(queue)

    let len = queue.length

    while (len) {

      console.log(len)

      let node = queue.shift()               *//* 取出队列第一个元素

      if(len === 1) arr.push(node.val)       *//* 当是 **当前一层的最后一个元素时,把值加入*arr*

      if(node.left) queue.push(node.left)    *//* 继续往队列添加元素

      if(node.right) queue.push(node.right)

      len--

    }

  }

  return arr

};

求最大公共前缀,如 ['aaafsd', 'aawwewer', 'aaddfff'] => 'aa'

var longestCommonPrefix = function(strs) {
    if(strs.length == 0) 
        return "";
    let ans = strs[0];
    for(let i =1;i<strs.length;i++) {
        let j=0;
        for(;j<ans.length && j < strs[i].length;j++) {
            if(ans[j] != strs[i][j])
                break;
        }
        ans = ans.substr(0, j);
        if(ans === "")
            return ans;
    }
    return ans;
};

字符串中出现最多的字符和个数

function A(str) {
var obj={}
for (var i = 0; i < str.length; i++) { //   循环字符串中每一项
    var k = str[i]; // 把每一项保存给变量 k (对这一步不理解可先熟悉 for循环机制);

    if (obj[k]) {

        obj[k]++;
    } else {
        obj[k] = 1;
    }
}
let num = 0;
let value = null;
for (var j in obj) {
    if (obj[j] > num) {
        num = obj[j];            // 这一步是找出出现最多的那个字母, 也就是最大的那个数
        value = j;
    }
}

console.log(value, num); // 输出 "h" 4

}

字符串中连续出现最多的字符和个数

function getMax(str){
   let res = {}
   for(let i = 0; i < str.length; i++){
       let j = i
    while(str[i] == str[i + 1]) i++
    res[str[j]]=i - j + 1
   }
  console.log(res)
  let max = 0,maxStr = '';
  Object.keys(res).forEach(function(key){
     if(res[key] >max ) {max = res[key];maxStr=key;}
  });
}

反转字符串

var reverseString = function(s) {
    const n = s.length;
    for (let left = 0, right = n - 1; left < right; ++left, --right) {
        [s[left], s[right]] = [s[right], s[left]];
    }
};
时间复杂度:O(N)O(N),其中 NN 为字符数组的长度。一共执行了 N/2N/2 次的交换。
空间复杂度:O(1)O(1)。只使用了常数空间来存放若干变量。

repeat 函数实现

// 1 数组
function repeat(src, n) {
    return (new Array(n + 1)).join(src);
}
// 2 二分法
function repeat(src, n) {
    var s = src, total = "";
    while (n > 0) {
        if (n % 2 == 1) {
            total += s;
        }
        if (n == 1) {
            break;
        }
        s += s;
        n = Math.floor(n/2);
    }
    return total;
}
// 3 递归
function repeat(src, n) {
    return (n > 0) ? src.concat(repeat(src, --n)):"";
}

字符串中的第一个唯一字符

var firstUniqChar = function(s) {
    const frequency = _.countBy(s);
    for (const [i, ch] of Array.from(s).entries()) {
        if (frequency[ch] === 1) {
            return i;
        }
    }
    return -1;
};
时间复杂度:O(n),其中 n 是字符串 s 的长度。我们需要进行两次遍历。

空间复杂度:OO(∣Σ∣),其中 Σ 是字符集,在本题中 s 只包含小写字母,因此 ∣Σ∣≤26。我们需要 O(∣Σ∣) 的空间存储哈希映射。

var firstUniqChar = function(s) {
    const position = new Map();
    const n = s.length;
    for (let [i, ch] of Array.from(s).entries()) {
        if (position.has(ch)) {
            position.set(ch, -1);
        } else {
            position.set(ch, i);
        }
    }
    let first = n;
    for (let pos of position.values()) {
        if (pos !== -1 && pos < first) {
            first = pos;
        }
    }
    if (first === n) {
        first = -1;
    }
    return first;
};

计算这个字符串中有多少个回文子串。

用双循环来遍历,利用两个字符串来存储字符串组合结果,一个正向存储,一个反向存储,每次比较它们是否相等。相等就+1。

var countSubstrings = function(s) {
    let res = 0; //记录结果
    for(let i=0;i<s.length;i++){
        let str = ''; //正向组合字符串
        let restr = ''; //反向组合字符串
        for(let j=i;j<s.length;j++){
            str += s[j];
            restr = s[j] + restr;
            if(str == restr) res++; 
        }
    }
    return res
};

parse url ,包括重复的Key转化为数组,未指定值约定为1等等要求?

var obj = url.match(/([^=?&]+)(=[^=?&]+)?/g)
  .reduce(function (acc, match) {
    var splits = match.split('='),
        key = splits[0],
        value = decodeURIComponent(splits[1] || '') || true
    if (acc[key] === undefined) acc[key] = value
    else acc[key] = [].concat(acc[key], value)
    return acc
  }, {})


// 或者如果使用 ES6 语法
const obj = url.match(/([^=?&]+)(=[^=?&]+)?/g)
  .map(match => match.split('='))
  .reduce((acc, [key, valueStr]) => {
    const value = decodeURIComponent(valueStr || '') || true
    return {
      ...acc,
      [key]: acc[key] === undefined ? value : [].concat(acc[key], value)
    }
  }, {})

判断素数

function is_pri(m) {
    // var result=[]
    if (m===1){return false;};
    
    for (let i =2;i<=parseInt(Math.sqrt(m));i++){
        if (m%i===0){return false}
    };

    return true;
    
}

字符串全排列 (无重复字符)

*全排列 'abc' -> ['abc', 'acb', 'bac', 'bca', 'cab', 'cba']

function permutation(str) {
    let ans = []
    function Search(str = '',result = ''){
        if(str.length === 0)return ans.push(result)
        let arr = str.split('')
        for(let i = 0;i < arr.length;i++){
            let t = arr[i]
            arr[i] = ''
            Search(arr.join(''),result+t)
            arr[i] = t
        }
    }
    Search(str,"")
    return ans
}
输入: nums = [1,2,3]
输出: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
var permute = function(nums) {
    let ans = []
    function search(arr=[], res=[]) {
        if(arr.length === 0) {
            ans.push([...res])
            return
        }
        for (let i = 0;i<arr.length;i++) {
            const t = arr.slice(0, i).concat(arr.slice(i+1))
            const sec = res.concat(arr[i])
            search(t, sec)
        }
    }
    search(nums, [])
    return ans
};

字符串全排列 (有重复字符)

var permuteUnique = function(nums) {
    const ans = []
    nums = nums.sort((a, b) => a-b)
    function premute (arr, res) {
        if (arr.length === 0) {
            ans.push([...res])
            return
        }
        for(let i = 0;i<arr.length;i++) {
            if (i>0 && arr[i] === arr[i-1]) {
                continue
            }
            const t = arr.slice(0, i).concat(arr.slice(i+1))
            const r = res.concat(arr[i])
            premute(t, r)
        }
    }
    premute(nums, [])
    return ans
};

字符串 下一个排列

function nextPermutation(nums) {
  const n = nums.length;
  let i = n - 2;
  while (i >= 0 && nums[i] >= nums[i + 1]) {
    i--;
  }

  if (i >= 0) {
    let j = n - 1;
    while (nums[j] <= nums[i]) {
      j--;
    }
    [nums[i], nums[j]] = [nums[j], nums[i]]; // 交换 nums[i] 和 nums[j]
  }

  let left = i + 1;
  let right = n - 1;
  while (left < right) {
    [nums[left], nums[right]] = [nums[right], nums[left]]; // 反转子数组
    left++;
    right--;
  }
}

// 测试用例
const nums = [1, 2, 3];
nextPermutation(nums);
console.log(nums); // 输出: [1, 3, 2]

const nums2 = [3, 2, 1];
nextPermutation(nums2);
console.log(nums2); // 输出: [1, 2, 3]

数组中重复的数字

var findRepeatNumber = function (nums) {
    var map = {};

    for(var i = 0 ; i< nums.length; i++){
        if(!map[nums[i]]){
            map[nums[i]] = 1; 
        }else {
            return nums[i]
        }
    }
};

var findRepeatNumber = function(nums) {
    /*
    /* 法一:
            数组排完序后,相等的数就会相邻。
            于是循环直接比较就好,遇到相等的就返回该值。
    */
    nums.sort();
    for(var i=0;i<nums.length-1;i++){
        if(nums[i]==nums[i+1]) return nums[i];
    }
    
};

旋转数组

给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。

//1
用 n 表示数组的长度,我们遍历原数组,将原数组下标为 i 的元素放至新数组下标为  (i+k) % n 的位置,最后将新数组拷贝至原数组即可。

var rotate = function(nums, k) {
    const n = nums.length;
    const newArr = new Array(n);
    for (let i = 0; i < n; ++i) {
        newArr[(i + k) % n] = nums[i];
    }
    for (let i = 0; i < n; ++i) {
        nums[i] = newArr[i];
    }
};

时间复杂度: O(n),其中 nn 为数组的长度。

空间复杂度: O(n)。

//2
倒序遍历,数组增加k位整体后移。指针到k后,把[n, n + k]位移回[0, k]。截断到n
取余:当k>数组长度,测试用例[1, 2] 3等同于[1, 2] 1 3 / 2取余即可
var rotate = function(nums, k) {
    let n = i = nums.length
    if ((k %= n) === 0) return
    while (i--) {
        nums[i + k] = nums[i]
        if (i <= k) nums[i] = nums[n + i]
    }
    nums.length = n
};

//3
var rotate = function(nums, k) {
    let n = nums.length
    let _k = k % n
    while (_k-- > 0) {
        nums.unshift(nums.pop())
    }
};

//4
将数组中的元素向右移动 k 个位置
nums = [1,2,3,4,5,6,7], k = 3

[1,2,3,4,5,6,7,1,2,3,4,5,6,7]
相当于视图左移k步
[1,2,3,4, 5,6,7,1,2,3,4, 5,6,7]

var rotate = function(nums, k) {
    let n = nums.length
    let _k = k % n
    nums.push(...nums)
    nums.splice(0, n - _k)
    nums.splice(n)
};

//5
如 nums = [1,2,3,4,5,6,7], k = 3

翻转整个数组
[7,6,5,4,3,2,1]

翻转前k个
[5,6,7,4,3,2,1]

翻转后面的
[5,6,7,1,2,3,4]

var rotate = function(nums, k) {
    let n = nums.length
    let _k = k % n
    rev(0, n - 1)
    rev(0, _k - 1)
    rev(_k, n - 1)
    // 指定位置翻转数组
    function rev (i, j) {
        while (i < j) {
            [nums[i], nums[j]] = [nums[j], nums[i]]
            i++, j--
        }
    }
};

一个整数数组 nums 和一个目标值 target,在该数组中找出和为目标值的那两个整数

function twoSum(arr, t) { 
    for (let i = 0; i < Math.floor(arr.length / 2); i++) { 
        if (arr.indexOf(t - arr[i]) > 0) {
        	console.log(i, arr.indexOf(t - arr[i]))
        }
    } 
}


//2
/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var twoSum = function (nums, target) {
  let result = [];
  let map = new Map();
  // 遍历一遍数组, 将数组中每个值和对应的索引 做一个映射
  for (let i = 0; i < nums.length; i++) {
    map.set(nums[i], i);
  }
  // 再遍历一遍数组
  for (let i = 0; i < nums.length; i++) {
    // 循环每一个元素的时候 都将目标值算出来
    let anotherOne = target - nums[i];
    // 检查 map 中是否包含这个元素,且对应的索引不能是当前的这个索引
    if (map.has(anotherOne) && map.get(anotherOne) !== i) {
      // 找到则放进数组
      result.push(i);
      result.push(map.get(anotherOne))
      break;
    }
  }
  // 返回结果
  return result
};

一个整数数组 nums 和一个目标值 target,在该数组中找出和为目标值的三个整数

const threeSum = (nums, target) => {
  nums.sort((a, b) => a - b); // 排序

  const res = [];

  for (let i = 0; i < nums.length - 2; i++) { // 外层遍历
    let n1 = nums[i];
    if (n1 > target) break; // 如果已经>target,不用做了,break
    if (i - 1 >= 0 && n1 == nums[i - 1]) continue; // 遍历到重复的数,跳过

    let left = i + 1;            // 左指针
    let right = nums.length - 1; // 右指针

    while (left < right) {
      let n2 = nums[left], n3 = nums[right];

      if (n1 + n2 + n3 === target) {  // 三数和=target,加入解集res
        res.push([n1, n2, n3]);
        while (left < right && nums[left] == n2) left++; // 直到指向不一样的数
        while (left < right && nums[right] == n3) right--; // 直到指向不一样的数
      } else if (n1 + n2 + n3 < target) { // 三数和<target,则左指针右移
        left++;
      } else {      // 三数和>target,则右指针左移
        right--;
      }
    }
  }
  return res;
};

给定数组,从数组中取出n个不重复的数的和为sum

function find(arr, n, sum) {
    let res = []
    function findGroup(arr,n,sum,onRes) {
      if (n> arr.length) return false
      if (sum==0 && n==0){
          res.push(onRes)
          return true
      } else if(n<=0) {
          return false
      }
      if(n>0){
        let temp = arr.slice(1, arr.length)
        findGroup(temp, n-1, sum-arr[0], [...onRes, arr[0]])
        findGroup(temp, n, sum, [...onRes])
      }
    }
    findGroup(arr, n, sum, [])
    return res
}

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。可以重复

输入:candidates = [2,3,6,7], target = 7, 所求解集为: [ [7], [2,2,3] ]

var combinationSum = function(candidates, target) {
    if (candidates.length <= 0) {
        return [];
    }

    // 从小到大排序,是剪枝的关键
    candidates.sort((a, b) => parseInt(a) - parseInt(b));

    /**
     * 
     * @param {*} candidates 原数组
     * @param {*} target 此时要组合成的目标值
     * @param {*} begin 数组开始遍历的索引
     * @param {*} track 每次选择的结果数组
     */
    const backtrack = function(candidates, target, begin, track) {
        // 终止条件,找到符合的数组则保存数组结果
        if (target == 0) {
            // 注意这里要存复制的数组,因为存引用会改变
            res.push(track.slice()); 
            return;
        }

        // 循环做选择 
        for (let i = begin; i < candidates.length; i++) {
            // 剪枝,选择的数大于目标值则直接退出
            if (target - candidates[i] < 0) {
                break;
            }
            // 选择
            track.push(candidates[i]);
            
            // 回溯
            backtrack(candidates, target - candidates[i], i, track);
            
            // 撤销选择
            track.pop();
        }
    }

    let res = [];
    // 存每次选择的结果数组
    let track = [];
    backtrack(candidates, target, 0, track);

    return res;
};

40. 组合总和 II

给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。 candidates 中的每个数字在每个组合中只能使用一次。

输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
  [1, 7],
  [1, 2, 5],
  [2, 6],
  [1, 1, 6]
]
const combinationSum2 = (candidates, target) => {
  candidates.sort((a,b) => a - b ); // 升序排序
  const res = [];

  const dfs = (start, temp, sum) => { // start是索引 当前选择范围的第一个
    if (sum >= target) {        // 爆掉了,不用继续选了
      if (sum == target) {      // 满足条件,加入解集
        res.push(temp.slice()); // temp是引用,所指向的内存后续还要操作,所以拷贝一份
      }
      return;                   // 结束当前递归
    }
    for (let i = start; i < candidates.length; i++) {             // 枚举出当前的选择
      if (i - 1 >= start && candidates[i - 1] == candidates[i]) { // 当前选项和左邻选项一样,跳过
        continue;
      }
      temp.push(candidates[i]);              // 作出选择
      dfs(i + 1, temp, sum + candidates[i]); // 基于该选择,继续选择,递归
      temp.pop();               // 上面的递归结束,撤销选择,回到选择前的状态,切入另一分支
    }
  };

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

216. 组合总和 III

找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。

输入: k = 3, n = 7
输出: [[1,2,4]]

回溯

基本思路就是从1开始找,然后在第i个循环中从i开始,如果这个数字已经比需要的大了,直接退循环; 如果小了就是 入组->继续->复原->下一个

const combinationSum3 = (k, n) => {
  const res = []; 
  // 基于当前已选的comb数组(和为sum),在数start到数9中继续选
  const dfs = (start, comb, sum) => { 
    if (comb.length == k) {     // 选够k个数 结束递归
      if (sum == n) {           // 组合中数之和等于n
        res.push(comb.slice()); // 将它的拷贝加入解集
      }
      return;
    }
    for (let i = start; i <= 9; i++) { // 枚举出所有的选择(选项)
      comb.push(i);                    // 作出一个选择i
      dfs(i + 1, comb, sum + i);       // 基于该选择i,往下递归
      comb.pop();                      // 撤销这个选择
    }
  };

  dfs(1, [], 0);  // 入口
  return res;
};

合并两个有序数组

const merge = (nums1, m, nums2, n) => {
    let index1 = m - 1;
    let index2 = n - 1;
    let tail = m + n - 1;

    while (index1 >= 0 && index2 >= 0) {
        if (nums1[index1] > nums2[index2]) {
            nums1[tail] = nums1[index1];
            index1--;
        } else {
            nums1[tail] = nums2[index2];
            index2--;
        }
        tail--;
    }
    while (tail >= 0 && index1 >= 0) {
        nums1[tail] = nums1[index1];
        index1--;
        tail--;
    }
    while (tail >= 0 && index2 >= 0) {
        nums1[tail] = nums2[index2];
        index2--;
        tail--;
    }
};

两个数组的交集

intersection([1, 3, 2, 2, 2, 4], [3, 5, 2, 2, 10]) // [3, 2]

// 1
const set_intersection = (set1, set2) => {
    if (set1.size > set2.size) {
        return set_intersection(set2, set1);
    }
    const intersection = new Set();
    for (const num of set1) {
        if (set2.has(num)) {
            intersection.add(num);
        }
    }
    return [...intersection];
}

var intersection = function(nums1, nums2) {
    const set1 = new Set(nums1);
    const set2 = new Set(nums2);
    return set_intersection(set1, set2);
};

// 2 
const intersection = (nums1, nums2) => {
  const map = {};
  const res = [];

  for (const num1 of nums1) {
    map[num1] = true; // 记录nums1出现过的数字
  }
  for (const num2 of nums2) {
    if (map[num2]) {  // 如果nums2的这个数字在nums1出现过
      map[num2] = false; // 置为false,避免重复推入res
      res.push(num2);  // 交集数字推入res
    }
  }
  return res;
};

// 3
var intersection = function(nums1, nums2) {
    nums1.sort((x, y) => x - y);
    nums2.sort((x, y) => x - y);
    const length1 = nums1.length, length2 = nums2.length;
    let index1 = 0, index2 = 0;
    const intersection = [];
    while (index1 < length1 && index2 < length2) {
        const num1 = nums1[index1], num2 = nums2[index2];
        if (num1 === num2) {
            // 保证加入元素的唯一性
            if (!intersection.length || num1 !== intersection[intersection.length - 1]) {
                intersection.push(num1);
            }
            index1++;
            index2++;
        } else if (num1 < num2) {
            index1++;
        } else {
            index2++;
        }
    }
    return intersection;
};

取2个数组的交集并且不去重

case2: const arr0 = [1, 3, 2, 2, 2, 4]

const arr1 = [3, 5, 2, 2, 10]

// result [3, 2, 2]

// 1
function helper(arr){
  return arr.reduce((acc,cur)=>{
      if(acc[cur] !== undefined)acc[cur] += 1
      else acc[cur] = 1
      return acc
  },{})
}
function func2(arr1,arr2){
  let obj1 = helper(arr1);
  return arr2.filter(key=>{
      if(obj1[key]>0){
          obj1[key]-=1;
          return true
      }
      return false
  })
}
func2(arr0, arr1)

// 2
const intersect = (nums1, nums2) => {
  const map = {};
  const res = [];
  for (const num1 of nums1) { // 记录nums1各个数字的出现次数
    if (map[num1]) {
      map[num1]++;  
    } else {         
      map[num1] = 1; 
    }
  }
  for (const num2 of nums2) { // 遍历nums2,看看有没有数字在nums1出现过
    const val = map[num2];
    if (val > 0) {            // 有出现过
      res.push(num2);         // 推入res数组
      map[num2]--;            // 匹配掉一个,就减一个
    }
  }
  return res;
};

// 3
const intersect = (nums1, nums2) => {
  nums1.sort((a, b) => a - b);
  nums2.sort((a, b) => a - b); // 为了使相同元素相邻出现
  const res = [];
  let p1 = 0;                  // p1扫描nums1
  let p2 = 0;                  // p2扫描nums2
  while (p1 < nums1.length && p2 < nums2.length) { // 用&& 因为有一个越界就不能找交集
    if (nums1[p1] > nums2[p2]) { // p1指向的数大,移动p2,期待扫到一样大的
      p2++;
    } else if (nums1[p1] < nums2[p2]) { // p2指向的数大,移动p1,期待扫到一样大的
      p1++;
    } else {                   // 遇到相同的,推入res数组,两指针均右移考察下一个
      res.push(nums1[p1]);
      p1++;
      p2++;
    }
  }
  return res;
};

合并区间

请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。

输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3][2,6] 重叠, 将它们合并为 [1,6].

先将区间排序,然后逐个遍历进行区间合并

var merge = function (intervals) {
  // 如果传递进来的数组长度为0 返回一个空数组
  if (intervals.length === 0) {
    return []
  }
  // 创建合并数组
  var res = []
  // 将数组进行升序排序
  intervals.sort(function (a, b) {
    return a[0] - b[0]
  })
  // 结果数组放进第一个数组
  res.push(intervals[0])
  // 从原数组的第一个元素进行遍历
  for (var i = 1; i < intervals.length; i++) {
    // 如果当前区间的左端点 大于 merge数组最后一个元素的右端点
    if (intervals[i][0] > res[res.length - 1][1]) {
      // 说明这个数组可以直接放进merge数组中
      res.push(intervals[i])
    } else { // 说明有区间有交集 当前区间的左端点小于等于最后一个元素的右端点
      // 如果当前区间的右端点 大于 merge 最后一个右端点
      if (intervals[i][1] > res[res.length - 1][1]) {
        // 更新右端点为最大值
        res[res.length - 1][1] = intervals[i][1]
      }
    }
  }
  return res
};

全排列

给定一个 没有重复 数字的序列,返回其所有可能的全排列。 输入: [1,2,3] 输出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]

const permute = (nums) => {
  const res = [];
  const used = {};

  function dfs(path) {
    if (path.length == nums.length) { // 个数选够了
      res.push(path.slice()); // 拷贝一份path,加入解集res
      return;                 // 结束当前递归分支
    }
    for (const num of nums) { // for枚举出每个可选的选项
      // if (path.includes(num)) continue; // 查找的时间是O(n),别这么写!增加时间复杂度
      if (used[num]) continue; // 使用过的,跳过
      path.push(num);         // 选择当前的数,加入path
      used[num] = true;       // 记录一下 使用了
      dfs(path);              // 基于选了当前的数,递归
      path.pop();             // 上一句的递归结束,回溯,将最后选的数pop出来
      used[num] = false;      // 撤销这个记录
    }
  }

  dfs([]); // 递归的入口,空path传进去
  return res;
};

给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。

const permuteUnique = (nums) => {
  const res = [];
  const len = nums.length;
  const used = new Array(len);
  nums.sort((a, b) => a - b); // 升序排序

  const helper = (path) => {
    if (path.length == len) { // 个数选够了
      res.push(path.slice()); // path的拷贝 加入解集
      return;                 // 结束当前递归 结束当前分支
    }

    for (let i = 0; i < len; i++) { // 枚举出所有的选择
      if (nums[i - 1] == nums[i] && i - 1 >= 0 && !used[i - 1]) { // 避免产生重复的排列
        continue;
      }
      if (used[i]) {      // 这个数使用过了,跳过。
        continue;
      }
      path.push(nums[i]); // make a choice
      used[i] = true;     // 记录路径上做过的选择
      helper(path);       // explore,基于它继续选,递归
      path.pop();         // undo the choice
      used[i] = false;    // 也要撤销一下对它的记录
    }
  };

  helper([]);
  return res;
};

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

  • 单看每个元素,都有两种选择:选入子集,或不选入子集。
  • 比如[1,2,3],先看1,选1或不选1,都会再看2,选2或不选2,以此类推。
  • 考察当前枚举的数,基于选它而继续,是一个递归分支;基于不选它而继续,又是一个分支。
  • 用索引index代表当前递归考察的数字nums[index]。
  • 当index越界时,所有数字考察完,得到一个解,位于递归树的底部,把它加入解集,结束当前递归分支。
  • 找到一个子集,结束递归,要撤销当前的选择,回到选择前的状态,做另一个选择——不选当前的数,基于不选,往下递归,继续生成子集。
  • 回退到上一步,才能在包含解的空间树中把路走全,回溯出所有的解。 输入:nums = [1,2,3] 输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
const subsets = (nums) => {
  const res = [];

  const dfs = (index, list) => {
    if (index == nums.length) { // 指针越界
      res.push(list.slice());   // 加入解集
      return;                   // 结束当前的递归
    }
    list.push(nums[index]); // 选择这个数
    dfs(index + 1, list);   // 基于该选择,继续往下递归,考察下一个数
    list.pop();             // 上面的递归结束,撤销该选择
    dfs(index + 1, list);   // 不选这个数,继续往下递归,考察下一个数
  };

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

数组去重

const arr = [1,2,3,4,4,3,2,1];
// 方法一:new Set ES6
return [...new Set(arr)]; // 这里又问到我...的用法

// 方法二:双层for循环 (然后说这样性能不好,让我只用一层for循环的方法)
function unique(arr){
  var res=[];
  for (var i = 0; i < arr.length; i++) {
    for (var j = i+1; j < arr.length; j++) {
      if (arr[i] === arr[j]) {
        ++ i;
        j = i;
      }
    }
    res.push(arr[i]);
  }
  return res;
}

// 方法三:单层for循环 + indexOf
function unique(array){
    var res = [];
    for(var i = 0; i < array.length; i++) {
        //如果当前数组的第i项在当前数组中第一次出现的位置是i,才存入数组;否则代表是重复的
        if (array.indexOf(array[i]) === i) {
            res.push(array[i])
        }
    }
    return res;
}
// 方法三点三:或者这样
function unique(array){
    let res = [];
    for(var i = 0; i < array.length; i++) {
        if (res.indexOf(array[i]) === -1) {
            res.push(array[i]);
        }
    }
    return res;
}

// 方法四:面试官说如果可以容忍改变原有数组的情况下,怎么改进性能更好
function unique(array){
    // 注意这里一定要倒叙for循环,否则会出现问题
    for(var i = array.length - 1; i > 0; i--) { 
        if (array.indexOf(array[i]) !== i) {
            array.splice(i, 1);
        }
    }
    // 因为少声明一个变量,节省了内存空间(虽然可以忽略不计,但是面试嘛~)
    return array;
}
//
function unique (arr) {
  return Array.from(new Set(arr))
}
//
function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    var array =[];
    for(var i = 0; i < arr.length; i++) {
            if( !array.includes( arr[i]) ) {//includes 检测数组是否有某个值
                    array.push(arr[i]);
              }
    }
    return array
}
// 
function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return;
    }
    arr = arr.sort()
    var arrry= [arr[0]];
    for (var i = 1; i < arr.length; i++) {
        if (arr[i] !== arr[i-1]) {
            arrry.push(arr[i]);
        }
    }
    return arrry;
}

数组转树

var list = [
  { id: 1, name: '部门A', parentId: 0 },
  { id: 3, name: '部门C', parentId: 1 },
  { id: 4, name: '部门D', parentId: 1 },
  { id: 5, name: '部门E', parentId: 2 },
  { id: 6, name: '部门F', parentId: 3 },
  { id: 8, name: '部门H', parentId: 4 }
];
function convert(list) {
  const map = list.reduce((acc, item) => {
    acc[item.id] = item
    return acc
  }, {})
  const result = []
  for (const key in map) {
    const item = map[key]
    if (item.parentId === 0) {
      result.push(item)
    } else {
      const parent = map[item.parentId]
      if (parent) {
        parent.children = parent.children || []
        parent.children.push(item)
      }
    }
  }
  return result
}
var result = convert(list)

反转二叉树

// dfs
const invertTree = (root) => {
  if (root == null) { // 遍历到null节点时,不用翻转,直接返回它本身
    return root;
  }
  invertTree(root.left);
  invertTree(root.right);

  const temp = root.left;
  root.left = root.right;
  root.right = temp;

  return root;
};
// bfs
const invertTree = (root) => {
  if (root == null) {
    return root;
  }
  const queue = [root];   // 维护一个队列,初始推入第一层的root
  
  while (queue.length) {
    const cur = queue.shift(); // 出列的节点
    [cur.left, cur.right] = [cur.right, cur.left]; // 交换左右子树

    if (cur.left) {            // 作为下一层节点入列考察
      queue.push(cur.left);
    }
    if (cur.right) {
      queue.push(cur.right);
    }
  }
  return root;
};

二叉树的最大深度

// dfs
const maxDepth = (root) => {
  if (root == null) return 0;
  const leftMaxDepth = maxDepth(root.left);
  const rightMaxDepth = maxDepth(root.right);
  return 1 + Math.max(leftMaxDepth, rightMaxDepth);
};
// bfs
const maxDepth = (root) => {
  if (root == null) return 0;
  const queue = [root];
  let depth = 1;
  while (queue.length) {
    // 当前层的节点个数
    const levelSize = queue.length;          
    // 逐个让当前层的节点出列
    for (let i = 0; i < levelSize; i++) {    
      // 当前出列的节点
      const cur = queue.shift();            
      // 左右子节点入列
      if (cur.left) queue.push(cur.left);
      if (cur.right) queue.push(cur.right); 
    }
    // 当前层所有节点已经出列,如果队列不为空,说明有下一层节点,depth+1
    if (queue.length) depth++;
  }
  return depth;
};

二叉树深度遍历

  • 先序
// 递归
let result = [];
let dfs = function (node) {
    if(node) {
        result.push(node.value);
        dfs(node.left);
        dfs(node.right);
    }
}
dfs(tree);
console.log(result); 
// 非递归
let dfs = function (nodes) {
    let result = [];
    let stack = [];
    stack.push(nodes);
    while(stack.length) { // 等同于 while(stack.length !== 0) 直到栈中的数据为空
        let node = stack.pop(); // 取的是栈中最后一个j
        result.push(node.value);
        if(node.right) stack.push(node.right); // 先压入右子树
        if(node.left) stack.push(node.left); // 后压入左子树
    }
    return result;
}
dfs(tree);
  • 中序遍历
//递归遍
let result = [];
let dfs = function (node) {
     if(node) {
        dfs(node.left);
        result.push(node.value); // 直到该结点无左子树 将该结点存入结果数组 接下来并开始遍历右子树
        dfs(node.right);
    }
}

dfs(tree);
console.log(result);
// 非递归
function dfs(node) {
    let result = [];
    let stack = [];
    while(stack.length || node) { // 是 || 不是 &&
        if(node) {
            stack.push(node);
            node = node.left;
        } else {
            node = stack.pop();
            result.push(node.value);
            //node.right && stack.push(node.right);
            node = node.right; // 如果没有右子树 会再次向栈中取一个结点即双亲结点
        }
    }
    return result;
}

dfs(tree);
  • 后序
// 递归
result = [];
function dfs(node) {
    if(node) {
        dfs(node.left);
        dfs(node.right);
        result.push(node.value);
    }
}
dfs(tree);
console.log(result);
// 非递归
function dfs(node) {
    let result = [];
    let stack = [];
    stack.push(node);
    while(stack.length) {
        // 不能用node.touched !== 'left' 标记‘left’做判断,
        // 因为回溯到该结点时,遍历右子树已经完成,该结点标记被更改为‘right’ 若用标记‘left’判断该if语句会一直生效导致死循环
        if(node.left && !node.touched) { // 不要写成if(node.left && node.touched !== 'left')
            // 遍历结点左子树时,对该结点做 ‘left’标记;为了子结点回溯到该(双亲)结点时,便不再访问左子树
            node.touched = 'left';
            node = node.left;
            stack.push(node);
            continue;
        }
        if(node.right && node.touched !== 'right') { // 右子树同上
            node.touched = 'right';
            node = node.right;
            stack.push(node);
            continue;
        }
        node = stack.pop(); // 该结点无左右子树时,从栈中取出一个结点,访问(并清理标记)
        node.touched && delete node.touched; // 可以不清理不影响结果 只是第二次对同一颗树再执行该后序遍历方法时,结果就会出错啦因为你对这棵树做的标记还留在这棵树上
        result.push(node.value);
        node = stack.length ? stack[stack.length - 1] : null;
        //node = stack.pop(); 这时当前结点不再从栈中取(弹出),而是不改变栈数据直接访问栈中最后一个结点
        //如果这时当前结点去栈中取(弹出)会导致回溯时当该结点左右子树都被标记过时 当前结点又变成从栈中取会漏掉对结点的访问(存入结果数组中)
    }
    return result; // 返回值
}

dfs(tree);

二叉树广度遍历

//递归
let result = [];
let stack = [tree]; // 先将要遍历的树压入栈
let count = 0; // 用来记录执行到第一层
let bfs = function () {
    let node = stack[count];
    if(node) {
        result.push(node.value);
        if(node.left) stack.push(node.left);
        if(node.right) stack.push(node.right);
        count++;
        bfs();
    }
}
dfc();
console.log(result);
// 非递归
function bfs(node) {
    let result = [];
    let queue = [];
    queue.push(node);
    let pointer = 0;
    while(pointer < queue.length) {
        let node = queue[pointer++]; // // 这里不使用 shift 方法(复杂度高),用一个指针代替
        result.push(node.value);
        node.left && queue.push(node.left);
        node.right && queue.push(node.right);
    }
    return result;
}

bfs(tree);

另一个树的子树,给定两个非空二叉树 s 和 t,检验 s 中是否包含和 t 具有相同结构和节点值的子树。

var isSubtree = function (s, t) {
  if (s == null) {
    return false;
  }
  if (isSameTree(s, t)) {
    return true;
  }
  return isSubtree(s.left, t) || isSubtree(s.right, t); // 有一个true就true
};
/*
  isSameTree
    两树同为 null 则相同
    一个 null 一个不是,则不同;
    两个树都不为 null,则递归判断左右子树是否相同
*/
function isSameTree(s, t) { // 100题
  if (s == null && t == null) {
    return true;
  };
  if (s == null || t == null) {
    return false;
  }
  return s.val == t.val && isSameTree(s.left, t.left) && isSameTree(s.right, t.right);
}

树的叶子结点

function TreeNode(val){
    this.val = val;
    this.children = []
}
 
const obj ={
    val:1,
    children:[
        {
        val:2,
            children:[{val:5},{val:6}]
        },
        {
        val:3,
        children:[{val:7},{val:8}]
        },
        {
        val:4,
        children:[{val:9},{val:10}]
        },
             ]
}
 
function func(root){
    let queue = [root];
    let res = [];let q=[]
    while(queue.length){
        let node = queue.shift(); 
        res.push(node.val);
        if(node.children !== undefined){
            node.children.forEach(v=>queue.push(v));
        }
        if(node.children === undefined){
            q.push(node.val);
        }
    }
    return q;
}

二叉搜索树

class BinaryTreeNode {
  constructor(key, value) {
    // 指向父节点
    this.p = null;

    // 左节点
    this.left = null;

    // 右节点
    this.right = null;

    // 键
    this.key = key;

    // 值
    this.value = value;
  }
}

class BinaryTree {
  constructor() {
    this.root = null;
  }

  static createNode(key, value) {
    return new BinaryTreeNode(key, value);
  }

  search(key) {
    let p = this.root;
    if (!p) {
      return;
    }

    while (p && p.key !== key) {
      if (p.key < key) {
        p = p.right;
      } else {
        p = p.left;
      }
    }

    return p;
  }

  insert(node) {
    // 尾指针的父节点指针
    let p = this.root;

    // 尾指针
    let tail = this.root;

    while (tail) {
      p = tail;
      if (node.key < tail.key) {
        tail = tail.left;
      } else {
        tail = tail.right;
      }
    }

    if (!p) {
      this.root = node;
      return;
    }

    // 插入
    if (p.key < node.key) {
      p.right = node;
    } else {
      p.left = node;
    }

    node.p = p;
  }

  transverse() {
    this.result=[]
    this.__transverse(this.root);
  }

  __transverse(node) {
    if (!node) {
      return;
    }
    this.__transverse(node.left);
    this.result.push(node);
    this.__transverse(node.right);
  }
}

判断它是否是高度平衡的二叉树。

一棵高度平衡二叉树定义为:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。

var isBalanced = function(root) {
    // 遍历到底还没有发现高度差超过 1 的左右子树,那么这个子树肯定符合平衡二叉树的规范
    if (!root) {
        return true
    }
    // 判断左右子树的高度差,如果超过 1 那么立即返回 false
    if (Math.abs(getHeight(root.left) - getHeight(root.right)) > 1) {
        return false
    }
    // 分别递归左右子树
    return isBalanced(root.left) && isBalanced(root.right)
    // 获取某个子树的高度
    function getHeight (root) {
        if (!root) {
            return 0
        }
        return Math.max(getHeight(root.left), getHeight(root.right)) + 1
    }
};

大数相加

function bigNumAdd(num1, num2) {
    // 首先检查传来的大数是否是字符串类型,如果传Number类型的大数,在传入的时候已经丢失精度了,
    // 就如 如果传入11111111111111111,处理的时候已经是丢失精度的11111111111111112了,则需要传入
    // 字符串类型的数字 '11111111111111111'
    const checkNum = num => typeof num === 'string' && !isNaN(Number(num))
    if (checkNum(num1) && checkNum(num2)) {
        // 将传入的数据进行反转,从前向后依次加和,模拟个,十,百依次向上加和
        const tmp1 = num1.split('').reverse()
        const tmp2 =  num2.split('').reverse()
        const result = []
        // 格式化函数,主要针对两个大数长度不一致时,超长的数字的格式化为0
        const format = val => {
          if( typeof val === 'number') return val
          if(!isNaN(Number(val))) return Number(val)
          return 0
        }
        let temp = 0
        // 以较长的数字为基准进行从前往后逐个加和,为避免两个数相加最高位进位后,导
        // 致结果长度大于两个数字中的长度,for循环加和长度为最长数字长度加一
        for (let i = 0; i <= Math.max(tmp1.length, tmp2.length); i++) {
          const addTmp = format(tmp1[i]) + format(tmp2[i]) + temp
          // 当加和的数字大于10的情况下,进行进位操作,将要进位的数字赋值给temp,在下一轮使用
          result[i] = addTmp % 10
          temp = addTmp > 9 ? 1 : 0;
        }
        // 计算完成,反转回来
        result.reverse()
        // 将数组for中多加的一位进行处理,如果最高位没有进位则结果第一个数位0,
        // 结果第一个数位1,则发生了进位。 如99+3,最大数字长度位2,结果数长度位3
        // 此时结果的第一位为1,发生了进位,第一位保留,如果是2+94,第一位为0,则不保留第一位
    const resultNum = result[0] > 0
        ? result.join('')
        : result.join('').slice(1)
        console.log('result', resultNum)
    } else {
      return 'big number type error'
    }
}

bigNumAdd('9007199254740992', '4')

合并两个有序链表

如果 l1 或者 l2 一开始就是空链表 ,那么没有任何操作需要合并,所以我们只需要返回非空链表。否则,我们要判断 l1 和 l2 哪一个链表的头节点的值更小,然后递归地决定下一个添加到结果里的节点。如果两个链表有一个为空,递归结束。

var mergeTwoLists = function(l1, l2) {
    if (l1 === null) {
        return l2;
    } else if (l2 === null) {
        return l1;
    } else if (l1.val < l2.val) {
        l1.next = mergeTwoLists(l1.next, l2);
        return l1;
    } else {
        l2.next = mergeTwoLists(l1, l2.next);
        return l2;
    }
};
时间复杂度:O(n + m)
空间O(n + m)

//2
var mergeTwoLists = function(l1, l2) {
    const prehead = new ListNode(-1);

    let prev = prehead;
    while (l1 != null && l2 != null) {
        if (l1.val <= l2.val) {
            prev.next = l1;
            l1 = l1.next;
        } else {
            prev.next = l2;
            l2 = l2.next;
        }
        prev = prev.next;
    }

    // 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可
    prev.next = l1 === null ? l2 : l1;

    return prehead.next;
};
时间复杂度:O(n + m)
空间O(1)

判断链表中是否有环。

var hasCycle = function (head) {
  while (head) {
    if (head.flag) return true;
    head.flag = true;
    head = head.next;
  }
  return false;
};
// 双指针
var hasCycle = function (head) {
  if (!head || !head.next) {
    return false;
  }
  let slow = head,
    fast = head.next;
  while (slow !== fast) {
    if (!fast || !fast.next) return false;
    fast = fast.next.next;
    slow = slow.next;
  }
  return true;
};

反转链表

var reverseList = function(head) {
    let prev = null;
    let curr = head;
    while (curr) {
        const next = curr.next;
        curr.next = prev;
        prev = curr;
        curr = next;
    }
    return prev;
};

var reverseList = function(head) {
    if (head == null || head.next == null) {
        return head;
    }
    const newHead = reverseList(head.next);
    head.next.next = head;
    head.next = null;
    return newHead;
};

反转前N个

let last = null
const reverseList = function(head, n) {
    if (n === 1) {
        last = head.next
        return head
    }
    let newhead = reverseList(head.next, n-1)
    head.next.next = head
    head.next = last
    rturn newhead
}

反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。

var reverseBetween = function(head, m, n) {
    let dummy = new ListNode(0);
    dummy.next = head;
    let tmpHead = dummy;
    // 找到第m-1个链表节点
    for(let i = 0;i < m - 1;i++){
        tmpHead = tmpHead.next;
    }
    // 206题解法一
    let prev = null;
    let curr = tmpHead.next;
    for(let i = 0;i <= n - m;i++){
        let next = curr.next;
        curr.next = prev;
        prev = curr;
        curr = next;
    }
    // 将翻转的部分链表 和 原链表拼接
    tmpHead.next.next = curr;
    tmpHead.next = prev;
    return dummy.next;
};

//2
var reverseBetween = function(head, m, n) {
    let nextTail = null;
    let reverseN = (head,n) => {
        if(n == 1){
            nextTail = head.next;
            return head; 
        }
        let last = reverseN(head.next,n-1);
        head.next.next = head;
        head.next = nextTail;
        return last;
    }
    if(m == 1){
        return reverseN(head,n);
    }
    head.next = reverseBetween(head.next,m-1,n-1);
    return head;
};

删除链表节点

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/**
 * @param {ListNode} head
 * @param {number} val
 * @return {ListNode}
 */

var removeElements = function(head, val) {
  let newHead = new ListNode( null ),
      prev = newHead,
      curr = head;
  newHead.next = head;
  
  while (curr) {
    if (curr.val === val) {
      prev.next = curr.next;
      curr = prev.next;
    }
    else {
      prev = curr;
      curr = curr.next;
    }
  }
  
  return newHead.next;
};

二分查找

function search(arr,key) {
    var low=0;
    var height=arr.length-1;
    var mid;
    while(low<=height){
        mid=Math.floor((low+height)/2);
        if(arr[mid]==key){
            return mid;
        }else if(arr[mid]<key){
            low=mid+1;
        }else{
            height=mid-1;
        }
    }
    return -1;
}

//2
    function binary_search(arr,low, high, key) {
        if (low > high){
            return -1;
        }
        var mid = parseInt((high + low) / 2);
        if(arr[mid] == key){
            return mid;
        }else if (arr[mid] > key){
            high = mid - 1;
            return binary_search(arr, low, high, key);
        }else if (arr[mid] < key){
            low = mid + 1;
            return binary_search(arr, low, high, key);
        }
    };

猴子吃香蕉

珂珂喜欢吃香蕉。这里有 N 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 H 小时后回来。

珂珂可以决定她吃香蕉的速度 K (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 K 根。如果这堆香蕉少于 K 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。  

珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。

返回她可以在 H 小时内吃掉所有香蕉的最小速度 K(K 为整数)

输入: piles = [3,6,7,11], H = 8 输出: 4

// 二分查找
const minEatingSpeed = (piles, H) => {
    // 计算堆中香蕉最大值
    let maxVal = Math.max(...piles)

    // 速度最小的时候,耗时最长
    let low = 1
    // 速度最大的时候,耗时最短
    let high = maxVal

    while (low < high) {
        let mid = Math.floor((low+high)/2)
        if (calculateTime(piles, mid) > H) {
            // 耗时太多,说明速度太慢了,进入下一轮搜索
            low = mid + 1
        } else {
            high = mid
        }
    }
    return low
}

// 计算吃掉香蕉所需的总耗时
const calculateTime = (piles, speed) => {
    let times = 0
    for (let pile of piles) {
        // 向上取整
        times += Math.ceil(pile / speed)

    }
    return times
}

LRU 算法

LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存 int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。 void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。

输入
["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]

解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1);    // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2);    // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1);    // 返回 -1 (未找到)
lRUCache.get(3);    // 返回 3
lRUCache.get(4);    // 返回 4
class LRUCache {
    constructor(capacity) {
        this.capacity = capacity
        this.map = new Map();
    }

    get(key) {
        let val = this.map.get(key);
        if (val === undefined) return -1;

        this.map.delete(key); // 因为被用过一次,原有位置删除
        this.map.set(key, val); // 放入最下面表示最新使用
        return val;
    }

    put(key, val) {
        if (this.map.has(key)) this.map.delete(key); // 如果有,删除

        this.map.set(key, val); // 放到最下面表示最新使用

        if (this.map.size > this.capacity) {
            // 这里有个知识点
            // map的entries方法,还有keys方法(可以看mdn)),会返回一个迭代器
            // 迭代器调用next也是顺序返回,所以返回第一个的值就是最老的,找到并删除即可
            this.map.delete(this.map.entries().next().value[0])
        }
    }
}

页面上有一个输入框,两个按钮,A按钮和B按钮,点击A或者B分别会发送一个异步请求,请求完成后,结果会显示在输入框中。用户随机点击A和B多次,要求输出显示结果时,按照用户点击的顺序显示

//dom元素
    var a = document.querySelector("#a")
    var b = document.querySelector("#b")
    var i = document.querySelector("#ipt");
    //全局变量p保存promie实例
    var P = Promise.resolve();
    a.onclick  = function(){
        //将事件过程包装成一个promise并通过then链连接到
        //全局的Promise实例上,并更新全局变量,这样其他点击
        //就可以拿到最新的Promies执行链
        P = P.then(function(){
            //then链里面的函数返回一个新的promise实例
            return new Promise(function(resolve,reject){
                setTimeout(function(){
                    resolve()
                    i.value = "a";
                },1000)
            })
        })
    }
    b.onclick  = function(){
        P = P.then(function(){
            return new Promise(function(resolve,reject){
                setTimeout(function(){
                    resolve()
                    console.log("b")
                    i.value = "b"
                },2000)
            })
        })
    }

最长递增子序列

输入:nums = [10,9,2,5,3,7,101,18] 输出:4 解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

/**
 * 
 * 时间复杂度O(n^2)
 * 空间复杂度O(n)
 * 
 * @param {number[]} nums
 * @return {number}
 */
var lengthOfLIS = function(nums) {
    const n = nums.length;
    if (n <= 0) {
        return n; 
    }

    // 初始化为1,因为子序列最少包含自己,即1
    let dp = Array(n).fill(1);
    // dp数组的最大值
    let maxResult = 0;
    // 做选择
    for (let i = 0; i < n; i++) {
        for (let j = 0; j < i; j++) {
            // 因为求的是递增子序列,所以前面的数nums[j]必须小于nums[i]才算递增子序列,才可以计算最大值
            // 加1为在nums[j]的最长递增子序列dp[j]基础上加上当前元素nums[i]所得的最长递增子序列
            if (nums[i] > nums[j]) {
                dp[i] = Math.max(dp[i], dp[j] + 1);
            }
        }
        maxResult = Math.max(maxResult, dp[i]);
    }

    return maxResult;
};

三角形最小路径和

输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]] 输出:11 解释:如下面简图所示: 2 3 4 6 5 7 4 1 8 3 自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。

DP
重复性(分治)
problem(i,j) = min(sub(i+1,j) , sub(i+1,j+1)) + a[i,j]
problem(i,j):当前行当前列(二维数组)的向下面走的路径总数
sub(i+1,j):下一行当前列(即向下并向左边走)的路径总数
sub(i+1,j+1):下一行下一列(即向下并向右边走)的路径总数
路径总数包括自己所在位置a[i,j],即到达当前位置所需的步数
定义状态数组
dp[i,j]
DP方程
dp[i,j] = min(dp[i+1,j],dp[i+1][j+1])+dp[i,j]
初始化数据
一般是第一行n列和第一列n行或者最后一行n列最后一列n行
但题中本意就是为了比较相邻数字和的大小,直接用原题的数据,最后一行n列即可对推到起点。

var minimumTotal = function(triangle) {
    var dp = triangle;
    for(var i = dp.length-2;i >= 0;i--){
        for(var j = 0;j < dp[i].length;j++){
            dp[i][j] = Math.min(dp[i+1][j],dp[i+1][j+1]) + dp[i][j];
        }
    }
    return dp[0][0];
};

//2
var minimumTotal = function(triangle) {
    var dp = new Array(triangle.length+1).fill(0);
    for(var i = triangle.length-1;i >= 0;i--){
        for(var j = 0;j < triangle[i].length;j++){
            dp[j] = Math.min(dp[j],dp[j+1]) + triangle[i][j];
        }
    }
    return dp[0];
};

打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

动态规划方程:dp[n] = MAX( dp[n-1], dp[n-2] + num )
由于不可以在相邻的房屋闯入,所以在当前位置 n 房屋可盗窃的最大值,要么就是 n-1 房屋可盗窃的最大值,要么就是 n-2 房屋可盗窃的最大值加上当前房屋的值,二者之间取最大值
举例来说:1 号房间可盗窃最大值为 3 即为 dp[1]=3,2 号房间可盗窃最大值为 4 即为 dp[2]=4,3号房间自身的值为 2 即为 num=2,那么 dp[3] = MAX( dp[2], dp[1] + num ) = MAX(4, 3+2) = 53 号房间可盗窃最大值为 5
时间复杂度:O(n),n为数组长度

var rob = function(nums) {
    const len = nums.length;
    if(len == 0)
        return 0;
    const dp = new Array(len + 1);
    dp[0] = 0;
    dp[1] = nums[0];
    for(let i = 2; i <= len; i++) {
        dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i-1]);
    }
    return dp[len];
};

柠檬水找零

顾客只可能给你三个面值的钞票,而且我们一开始没有任何钞票,因此我们拥有的钞票面值只可能是 5 美元,10 美元和 20 美元三种。

输入:[5,5,5,10,20]
输出:true
解释:
前 3 位顾客那里,我们按顺序收取 35 美元的钞票。
第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。
第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。
由于所有客户都得到了正确的找零,所以我们输出 true
var lemonadeChange = function(bills) {
    let five = 0, ten = 0;
    for (const bill of bills) {
        if (bill === 5) {
            five += 1;
        } else if (bill === 10) {
            if (five === 0) {
                return false;
            }
            five -= 1;
            ten += 1;
        } else {
            if (five > 0 && ten > 0) {
                five -= 1;
                ten -= 1;
            } else if (five >= 3) {
                five -= 3;
            } else {
                return false;
            }
        }
    }
    return true;
};

createStore 实现

createStore 创建一个 Redux store 来以存放应用中所有的 state,应用中应有且仅有一个 store。其中暴露 dispatch, subscribe, getState, replaceReducer 方法

// 首先定义了一个 ActionTypes 对象,它是一个 action,是一个 Redux 的私有 action,不允许外界触发,用来初始化 Store 的状态树和改变 reducers 后初始化 Store 的状态树。
export const ActionTypes = {
  INIT: "@@redux/INIT"
};
/**
 * 创建 store, 参数 reducer, state 以及中间件
 */

export default function createStore(reducer, preloadedState, enhancer) {
  // 在平常的使用中,我们一般会省略第二个参数。比如,当我们需要使用redux中间件的时候,就会像第三个参数传递一个applyMiddleware()[返回值是一个function]。
  // 如果,我们没有初始状态,则会省略第二个参数。这个时候,我们的函数调用形式为:
  // const store = createStore(reducer, applyMiddleware(...))
  if (typeof preloadedState === "function" && typeof enhancer === "undefined") {
    enhancer = preloadedState;
    preloadedState = undefined;
  }


  // 如果我们指定了reducer增强器enhancer
  if (typeof enhancer !== "undefined") {
    // enhancer必须是一个函数
    if (typeof enhancer !== "function") {
      throw new Error("Expected the enhancer to be a function.");
    }
    // 这个函数接收createStore作为参数,并且返回一个函数,这个函数接收的参数是reducer,preloadedState
    // 直接返回经过enhancer包装的对象
    return enhancer(createStore)(reducer, preloadedState);
  }

  
  // 要求传递给createStore的第一个参数必须是一个函数
  if (typeof reducer !== "function") {
    throw new Error("Expected the reducer to be a function.");
  }


  // 保存初始的reducer
  let currentReducer = reducer
  // 保存初始的state
  let currentState = preloadedState
  // 保存所有的事件监听器
  let currentListeners = []
  // 获取当前监听器的一个副本(相同的引用)
  let nextListeners = currentListeners
  // 是否正在派发action
  let isDispatching = false

  // 这个函数可以根据当前监听函数的列表生成新的下一个监听函数列表引用
  // 如果nextListeners和currentListeners具有相同的引用,则获取一份当前事件监听器集合的一个副本保存到nextListeners中
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice();
    }
  }

  // 为什么要维护两份事件监听器列表(nextListeners,currentListeners)??
  // 下面我们来解释

  // 直接返回当前store的state
  function getState() {
    return currentState;
  }

  // 订阅事件,返回移除订阅函数
  function subscribe(listener) {
    // 事件监听器必须是函数,否则会抛出异常
    if (typeof listener !== "function") {
      throw new Error("Expected listener to be a function.");
    }

    // 这个事件监听器是否已经被取消的标志
    let isSubscribed = true

    // 调用这个函数的结果就是生成一份当前事件监听器的一个副本保存到nextListeners中
    ensureCanMutateNextListeners()
    
    // 将新的事件监听器添加到nextListeners中
    nextListeners.push(listener)
    
    // 返回一个取消监听的函数
    return function unsubscribe() {
      // 如果这个监听器已经被取消了,则直接return
      if (!isSubscribed) {
        return;
      }
      // 将监听器是否取消的标志设置为false
      isSubscribed = false
      // 再次生成一份事件监听器集合的副本
      ensureCanMutateNextListeners()
      // 获取到需要取消的事件监听器的索引
      const index = nextListeners.indexOf(listener)
      // 从事件监听器集合中删除这个事件监听器
      nextListeners.splice(index, 1)
    };
  }

  // 从subscribe方法的源码中可以看出,每次在进行监听器的添加/删除之前,都会基于当前的监听器集合生成一个副本保存到nextListeners中。
  // 下面我们继续看dispatch的源码:

  // 执行 reducer,并触发订阅事件
  function dispatch(action) {
    // https://lodash.com/docs#isPlainObject
    // dispatch的参数就是我们需要派发的action,一定要保证这个action是一个纯粹的对象
    // 如果不是一个纯粹的对象,则会抛出异常。
    if (!isPlainObject(action)) {
      throw new Error(
        "Actions must be plain objects. " +
          "Use custom middleware for async actions."
      );
    }

    // 判断 action 是否有 type{必须} 属性
    // 派发的action必须有一个type属性(我们可以将这个属性认为就是action的身份证,这样redux才知道你派发的是哪个action,你需要做什么,该怎么为你做)
    // 如果没有这个属性则会抛出异常
    if (typeof action.type === "undefined") {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
          "Have you misspelled a constant?"
      );
    }

    // 如果正在 dispatch 则抛出错误
    if (isDispatching) {
      throw new Error("Reducers may not dispatch actions.");
    }
    // 对抛出 error 的兼容,但无论如何都会继续执行 isDispatching = false 的操作
    try {
      isDispatching = true;
      // 派发action
      // 实质就是将当前的state和你需要派发的action传递给reducer函数并返回一个新的state
      currentState = currentReducer(currentState, action);
    } finally {
      isDispatching = false;
    }

    // 又多了一份事件监听器的列表,简单的说一下这三份列表的作用
    // nextListeners: 保存这次dispatch后,需要触发的所有事件监听器的列表
    // currentListeners: 保存一份nextListeners列表的副本
    // listeners: 需要执行的列表
    const listeners = currentListeners = nextListeners
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      // 调用所有的事件监听器
      listener()
    }
    //  dispatch的返回值也是十分重要的,如果没有这个返回值,就不可能引入强大的中间件机制。
    return action;
  }
  /**
   * 动态替换 reducer
   */
  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== "function") {
      throw new Error("Expected the nextReducer to be a function.");
    }
    currentReducer = nextReducer;
    dispatch({ type: ActionTypes.INIT });
  }
  dispatch({ type: ActionTypes.INIT });
  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer
  };
}

三门问题

有个综艺节目,节目里设置有三个门,其中一个门后面是汽车,两扇门后面是羊,参加的嘉宾选择门,如果门后面是车子,就可以赢得车子; 环节1:参加节目的嘉宾, 在三个门里选一个门; 环节2:主持人必定会开其中一个门 (且为了节目效果,必定是只羊) ; 环节3: 这时主持人会问嘉宾,换不换一开始选择的门; 问题:嘉宾换门好还是不换门好?

 function getRes(doors, reSelect) {
      const len = doors.length
      const x = Math.ceil(Math.random() * len) // 汽车所在的第x扇门
      const car = doors[x - 1] // 汽车所在的门 - 对应的门牌号
      
      const y = Math.ceil(Math.random() * len) // 玩家选择第y扇门
      let player = doors[y - 1] // 玩家选择的门 - 对应的门牌号
      
      const empty_doors = [...doors] // 空的门
      empty_doors.splice(x - 1, 1)
      
      const z = Math.ceil(Math.random() * (len - 1)) // 主持人选择第z扇门
      const presenter = empty_doors[z - 1] // 主持人选择的门 - 对应的门牌号
      
      const resDoors = [...empty_doors] // 剩下的门
      resDoors.splice(z - 1, 1, car)
      
      if (reSelect) { // 换门
        return player !== car // 玩家当前选择不等于车所在门,换门后一定中奖
      } else {
        return player === car // 玩家不换门,只有当等于车所在门才中奖
      }
    }
    
    /**
     * 选数字游戏执行函数
     * @param {Array} doors 门的数组
     * @param {Number} times 玩游戏的次数
     * @param {boolean} reSelect 是否重新选择
     */
    function playGame(doors, times, reSelect) {
      if (typeof times !== 'number' || parseInt(times) !== times) {
        console.warn("请输入整数局的对局次数");
        return false;
      }
      
      let count = 0
      
      for (let i = 0; i < times; i++) {
        if (getRes(doors, reSelect)) {
          count ++
        }
      }
      
      return count / times
    }
    
    const doors = ['A', 'B', 'C']
    const times = 10000
    
    playGame(doors, times, false)
 	playGame(doors, times, true)

手写状态管理库: Mobx

实现observable方法:
// 这里后面的两个参数: key 和 descriptor主要用于之后的装饰器实现
export defualt function observable(target, key, descriptor){
	// 这里支持装饰器模式的observable写法:
	if(typeof key === "string"){
		// 如果是作为装饰器装饰属性进行监听,先将装饰的对象进行深度代理
		let v = descriptor.initializer();
		v = createObservable(v);
		// 这里执行依赖搜集: 使用的Reaction类会在之后实现
		let reaction = new Reaction();
		// 返回描述器
		return {
			enumerable: true,
			configurable: true,
			get(){
				reaction.collect();  // 再获取target属性时进行autorun中的handler的依赖搜集
				return v;
			},
			set(value){
				v = value;
				reaction.run();  // 在每次更新target中的属性时执行autorun中的依赖
			}
		}
	}
	// 如果不是装饰器写法,则创建Proxy代理
	return createObservable(target);
}

// 创建代理对象
function createObservable(val){
	// 用于生成代理对象的控制器:
	const handler = () => {
		// 实例化Reaction在autorun获取属性的时候进行依赖搜集
		let reaction = new Reaction();
		return {
			set(target, key, value){
				// 对于数组的值设置处理: 当对数组进行观察监听时,由于对数组的操作会有两步执行:
				// 更新数组元素值
				// 更改数组的length属性,所以需要将更改length属性的操作给拦截,避免一次操作数组,多次触发handler
				if(key === "length"){
					return true;
				}
				// 执行搜集绑定, 此时修改值需要先执行,这样在autorun中的handler中才能拿到最新的值
				let r = Reflect.set(target, key, value)
				reaction.run();
				return r;
			},
			get(target, key){
				// 在获取属性值的时候进行依赖搜集
				reaction.collect()
				return Reflect.get(target, key);
			}
		}
	}
	// 进行深层Proxy代理返回: 针对如: {name: "chensir", age: {num: 21}}这样的对象
	return deepProxy(val, handler)
}

// 深度设置Proxy对象代理
function deepProxy(val, handler){
	if(typeof val !== "object"){
		return val;
	}
	// 深度递归进行Proxy代理,此时的递归树相当于是后序遍历进行代理
	for(let key in val){
		val[key] = deepProxy(val[key], handler);
	}
	return new Proxy(val, handler);
}
实现Reaction类进行状态搜集,作为abservable和autorun之间的桥梁:
// 定义两个全局变量,这里是简单实现,所以和实际的源码实现有一定的区别
let nowFn = null;  // 这个表示当前的autorun中的handler方法
let counter = 0;  // 这里使用counter记录一个计数器值作为每个observable属性的id值进行和nowFn进行绑定

class Reaction {
	constructor(){
		// 标识每一个proxy对象
		this.id = ++counter;  // 这里采用一个比较low的方法简易实现的,在每次对observable属性进行Proxy的时候,对Proxy进行标记
		this.store = {};  // 存储当前可观察对象对应的nowFn, 写入的形式如: {id: [nowFn]}
	}
	collect(){
		// 进行依赖搜集,只当当前有autorun绑定了相关属性观察后才会进行绑定
		if(nowFn){   // 通过这个判断主要是因为只有在调用autorun绑定的时候才会设置这里的nowFn
			this.store[this.id] = this.store[this.id] || [];
			this.store[this.id].push(nowFn);	
		}
	}
	run(){
		// 运行依赖函数
		if(this.store[this.id]){
			this.store[this.id].forEach(fn => {
				fn()
			})
		}
	}
	// 定义两个静态方法,用于在调用autorun方法时候对nowFn进行设置和消除
	static start(handler){
		nowFn = handler;
	}
	// 在注册绑定这个就要清空当前的nowFn,用于之后进行进行搜集绑定
	static end(){
		nowFn = null;
	}
}
实现autorun方法,进行简单的依赖搜集
export default function autorun(handler){
	if(typeof handler !== "function"){
		throw new TypeError(`autorun function expect a function but get a ${typeof handler}`)
	}
	// 开始搜集依赖,设置Reaction中的nowFn
	Reaction.start(handler)
	// 执行一次handler,在handler中有对于相应属性的getter获取,此时就可以设置改属性的Proxy的Reaction状态依赖
	handler()
	// 清除nowFn
	Reaction.end()
}

Mobx-React这个库在React Component组件中的state发生变动时手动调起组件的render方法和forceUpdate()对外部mobx更新后的状态在Component中进行强制刷新。

异步并发任务调度器

class TaskScheduler {
  constructor(concurrency) {
    this.concurrency = concurrency;  // 最大并发数
    this.queue = [];  // 存储待执行的低优先级任务队列
    this.highPriorityQueue = [];  // 存储待执行的高优先级任务队列
    this.running = 0;  // 当前运行的任务数
    this.pausedTasks = [];  // 存储被暂停的任务
    this.isPaused = false;  // 标记是否有任务被暂停
  }

  // 添加任务到队列
  addTask(task, isHighPriority = false) {
    if (isHighPriority) {
      this.highPriorityQueue.push(task);  // 高优先级任务插入高优先级队列
    } else {
      this.queue.push(task);  // 低优先级任务插入普通队列
    }

    // 如果有高优先级任务,立即执行
    if (isHighPriority) {
      this._next();  // 检查是否需要暂停当前任务并执行高优先级任务
    }
  }

  // 执行队列中的任务,按照并发数控制
  async run() {
    // 启动并发任务的执行
    for (let i = 0; i < this.concurrency; i++) {
      this._next(); // 通过 _next 方法调度任务执行
    }

    // 如果队列有剩余任务,等待所有任务完成
    await new Promise((resolve) => {
      const checkCompletion = setInterval(() => {
        if (this.running === 0 && this.queue.length === 0 && this.highPriorityQueue.length === 0) {
          clearInterval(checkCompletion);
          resolve();
        }
      }, 100);
    });
  }

  // 启动下一个任务
  async _next() {
    if (this.isPaused) return; // 如果处于暂停状态,不执行新任务

    if (this.highPriorityQueue.length > 0) {
      // 当有高优先级任务时,暂停当前任务,执行高优先级任务
      const task = this.highPriorityQueue.shift();  // 从高优先级队列中取出任务
      await this.pauseAndExecuteHighPriorityTask(task); // 调用暂停并执行高优先级任务
    } else if (this.queue.length > 0) {
      const task = this.queue.shift();  // 从低优先级队列中取出任务
      this.running++;  // 正在执行的任务数 +1
      await this.runTask(task);  // 执行任务
    }
  }

  // 执行任务
  async runTask(task) {
    try {
      await task();
    } catch (err) {
      console.error('Task failed:', err);
    } finally {
      this.running--;  // 任务完成后减少正在执行的任务数
      this._next();  // 继续执行下一个任务
    }
  }

  // 暂停当前任务并执行高优先级任务
  async pauseAndExecuteHighPriorityTask(task) {
    this.isPaused = true; // 标记为暂停状态

    // 暂停当前执行的任务
    if (this.running > 0) {
      // 存储当前正在执行的任务
      this.pausedTasks.push(...this.queue.splice(0));  // 将低优先级任务移至暂停队列
    }

    // 执行高优先级任务
    await task();

    // 执行完高优先级任务后,恢复低优先级任务的执行
    this.isPaused = false; // 恢复执行状态
    // 恢复所有暂停的任务
    this.queue.push(...this.pausedTasks);
    this.pausedTasks = [];  // 清空暂停任务列表

    this._next();  // 恢复执行队列中的任务
  }
}

异步控制

function asyncAdd(a, b, callback) {
    setTimeout(() => callback(null, a + b), 0);
}

function sum(list = []){
    return new Promise((resolve,reject)=>{
        if(list.length > 2){
            sum(list.slice(1)).then(data=>{
                asyncAdd(list[0],data,(err,data)=>{
                    resolve(data)
                })
            })
        }else{
            asyncAdd(list[0],list[1],(err,data)=>{
                resolve(data)
            })
        }
    })
}

sum([1,2,3,4]).then(console.log)
let tasks = [task1, task2, ..., taskN];

function seq(tasks) {

}

promise.then()

task1().then(() => task2()).then(() => task3())

// 模拟tasks
function asyncPrint(str){
    return function(){
        return new Promise((resolve,reject)=>{
            setTimeout(()=>{
                console.log(str)
                resolve()
            },Math.random()*1000)
        })
    }
} 

let tasks = [ asyncPrint("task1"), asyncPrint("task2"), asyncPrint("task3")]

//seq
function seq(tasks) {
    return tasks.reduce((pre,cur)=>{
        return pre.then(cur)
    },Promise.resolve())
}
// 或
function seq(tasks) {
    if(tasks.length == 0)return
    tasks[0]().then(()=>{
        seq(tasks.slice(1))
    })
}

顺序执行一组异步代码函数,并输出最后的结果

const applyAsync = (acc,val) => acc.then(val);
const composeAsync = (...dd) => x => dd.reduce(applyAsync, Promise.resolve(x));
const transformData = composeAsync(funca, funcb, funcc, funcd);
transformData().then(result => console.log(result,'last result')).catch(e => console.log(e));

有 8 个图片资源的 url,已经存储在数组 urls 中(即urls = ['1.jpg', ...., '8.jpg']),而且已经有一个函数 function loadImg,输入一个 url 链接,返回一个 Promise,该 Promise 在图片下载完成的时候 resolve,下载失败则 reject。

但是我们要求,任意时刻,同时下载的链接数量不可以超过 3 个。

var urls = ['data.jpg', 'gray.gif', 'Particle.gif', 'arithmetic.png', 'arithmetic2.gif', 'getImgDataError.jpg', 'arithmetic.gif', 'wxQrCode2.png'];
function loadImg(url) {
    return new Promise((resolve, reject) => {
        const img = new Image()
        img.onload = function () {
            console.log('一张图片加载完成');
            resolve();
        }
        img.onerror = reject
        img.src = url
    })
};

function limitLoad(urls, handler, limit) {
    // 对数组做一个拷贝
    const sequence = [].concat(urls)
    let promises = [];

    //并发请求到最大数
    promises = sequence.splice(0, limit).map((url, index) => {
        // 这里返回的 index 是任务在 promises 的脚标,用于在 Promise.race 之后找到完成的任务脚标
        return handler(url).then(() => {
            return index
        }); 
    });

    // 利用数组的 reduce 方法来以队列的形式执行
    return sequence.reduce((last, url, currentIndex) => {
        return last.then(() => {
            // 返回最快改变状态的 Promise
            return Promise.race(promises)
        }).catch(err => {
            // 这里的 catch 不仅用来捕获 前面 then 方法抛出的错误
            // 更重要的是防止中断整个链式调用
            console.error(err)
        }).then((res) => {
            // 用新的 Promise 替换掉最快改变状态的 Promise
            promises[res] = handler(sequence[currentIndex]).then(() => { return res });
        })
    }, Promise.resolve()).then(() => {
        return Promise.all(promises)
    })

}
limitLoad(urls, loadImg, 3)

// 因为 limitLoad 函数也返回一个 Promise,所以当 所有图片加载完成后,可以继续链式调用
limitLoad(urls, loadImg, 3).then(() => {
    console.log('所有图片加载完成');
}).catch(err => {
    console.error(err);
})

带权图的最大路径

// 路径遍历
let read_line = (function (){
    let i = 0
    data = [        'A B 100',        'B D 100',        'D E 50',        'A C 200',        'C G 300',        'A F 500',    ]
    return function (){
        return data[i++]
    }
})()

function main(){
    // 定义路径树,最终将会生成如下:
    /*
     { A: { B: 100, C: 200, F: 100 },
       B: { D: 100 },
       D: { E: 50 },
      C: { G: 300 } }*/
     
    let TREE = {}

    let MaxValue = 0 , MaxRoads = []

    let read  = read_line()

    while(read){
        let p = read.split(' ')
        let begin = p[0],end = p[1],value = parseInt(p[2])
        // 生成路径树
        TREE[begin] = {...TREE[begin],[end]:value}
        read = read_line()
    }
    /**
     * 回溯函数
     * @param {String} begin 起点键值
     * @param {Object} tree 路径树
     * @param {Number} value 当前路径权值 
     * @param {String} road 已走过的路径
     */
    function resolve(begin,tree,value,road){
        if(!tree[begin])return
        
        for (const end in tree[begin]) {
            if (tree[begin].hasOwnProperty(end)) {
                const element = tree[begin][end];
               
                let currentValue = value+element,currentRoad = road+'->'+end
               
                if(!TREE[end] && currentValue > MaxValue){
                    // 抵达终点,当前值比之前的最大值还要大,更新MaxValue和路径数组
                    MaxValue = currentValue
                    MaxRoads = [currentRoad]
                    continue
                }else if(!TREE[end] && currentValue == MaxValue){
                    // 抵达终点,当前值等于之前的最大值,路径数组新增路径
                    MaxRoads.push(currentRoad)
                }else if(TREE[end]){
                    // 还没有抵达终点,继续向下走
                    resolve(end,tree,currentValue,currentRoad)
                }
            }
        }
    }
    for (const begin in TREE) {
        if (TREE.hasOwnProperty(begin)) {
            // 遍历路径树下的各个起点,开始路径计算
            resolve(begin,TREE,0,begin)
        }
    }
    MaxRoads.map(road=>console.log(road,MaxValue))
}

main()