算法题2

259 阅读9分钟

限制请求顺序Scheduler

// 使用一个队列 queue 来存储当前执行的函数
// 使用一个标识 running 表示当前并发执行的数量
// 判断当前队列是否为空或者 当前执行的并发个数为2,则直接return
// 取出
class Scheduler {
    constructor() {
       this.queue = [];
       this.running = 0;
    }
    

    run() {
        if(this.queue.length === 0 || this.running === 2) {
            return;
        }
        const p = this.queue.shift();
        this.running++;
        p().then((result) => {
            this.running--;
            this.run();
            return result;
        })
    }
    add(promise) {
        this.queue.push(promise);
        this.run();
    }
}
const timout = (time) => new Promise(resolve => {
    setTimeout(resolve, time)
})
const scheduler = new Scheduler();
const addTask = (time, order) => {
    scheduler.add(() => timout(time).then(() => {
        console.log(order);
    }))
}
addTask(1000, '1');
addTask(500, '2');
addTask(300, '3');
addTask(400, '4');

// output: 2 3 1 4
// 一开始,1, 2两个任务进入队列
// 500ms时,2完成,输入2;任务3进队
// 800ms时,3完成,输出 3;任务4进队
// 1000ms时,1完成,输出1
// 1200ms时,4完成,输出4
class Scheduler {
  constructor(limit = 2) {
    this.limit = limit
    this.concurrent = 0
    this.stack = []
  }
  add(promiseCreator) {
    if (this.concurrent < this.limit) {
      this.concurrent++
      return promiseCreator().then(() => {
        this.concurrent--        
        this.next() 
      })
    } else {
      let resolve
      let p = new Promise(r => { resolve = r })
      this.stack.push(() => {
        promiseCreator().then(() => {
          resolve()
          this.concurrent--          
          this.next()
        })
      })
      return p
    }
  }
  next() {
    if (this.stack.length > 0 && this.concurrent < this.limit) {
      let p = this.stack.shift()      
      this.concurrent++      
      p()
    }
  }
}


const timout = (time) => new Promise(resolve => {
    setTimeout(resolve, time)
})
const scheduler = new Scheduler();
const addTask = (time, order) => {
    scheduler.add(() => timout(time).then(() => {
        console.log(order);
    }))
}
addTask(1000, '1');
addTask(500, '2');
addTask(300, '3');
addTask(400, '4');

// 2 3 1 4

347. 前 K 个高频元素

let topKFrequent = function(nums, k) {
    let map = new Map(), arr = [...new Set(nums)]
    nums.map((num) => {
        if(map.has(num)) map.set(num, map.get(num)+1)
        else map.set(num, 1)
    })
    

    return arr.sort((a, b) => map.get(b) - map.get(a)).slice(0, k);
};
时间复杂度:O(nlogn)
空间复杂度:O(n)

. 数组中的第K个最大元素

let findKthLargest = function(nums, k) {
    nums.sort((a, b) => b - a).slice(0, k);
    return nums[k-1]
};

快速排序来取第 k 个最大值,其实没必要那么麻烦,我们仅仅需要在每执行一次的时候,比较基准值位置是否在 n-k 位置上,如果小于 n-k ,则第 k 个最大值在基准值的右边,我们只需递归基准值右边的子序列即可;如果大于 n-k ,则第 k 个最大值在基准值的做边,我们只需递归***基准值左边的子序列即可;如果等于 n-k ,则第 k 个最大值就是基准值

let findKthLargest = function(nums, k) {
    return quickSelect(nums, nums.length - k)
};


let quickSelect = (arr, k) => {
  return quick(arr, 0 , arr.length - 1, k)
}

let quick = (arr, left, right, k) => {
  let index
  if(left < right) {
    // 划分数组
    index = partition(arr, left, right)
    // Top k
    if(k === index) {
        return arr[index]
    } else if(k < index) {
        // Top k 在左边
        return quick(arr, left, index-1, k)
    } else {
        // Top k 在右边
        return quick(arr, index+1, right, k)
    }
  }
  return arr[left]
}

let partition = (arr, left, right) => {
  // 取中间项为基准
  var datum = arr[Math.floor(Math.random() * (right - left + 1)) + left],
      i = left,
      j = right
  // 开始调整
  while(i < j) {
    
    // 左指针右移
    while(arr[i] < datum) {
      i++
    }
    
    // 右指针左移
    while(arr[j] > datum) {
      j--
    }
    
    // 交换
    if(i < j) swap(arr, i, j)

    // 当数组中存在重复数据时,即都为datum,但位置不同
    // 继续递增i,防止死循环
    if(arr[i] === arr[j] && i !== j) {
        i++
    }
  }
  return i
}

// 交换
let swap = (arr, i , j) => {
    let temp = arr[i]
    arr[i] = arr[j]
    arr[j] = temp
}


//test
findKthLargest([3,2,1,5,6,4],2) // 5

只出现一次的数字

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

// 进行排序后,相同的数字必定是连续的,所以只要判断是否连续
var singleNumber = function(nums) {
    nums = nums.sort((a, b) => {
        return a-b
    })
    for (let i = 0; i<nums.length;i+=2){
        if (nums[i] != nums[i+1]) {
            return nums[i]
        }
    }
};




// map 
var singleNumber = function(nums) {
    let map = new Map()
    for (let i=0; i<nums.length; i++) {
       if (map.has(nums[i])) {
            map.set(nums[i], map.get(nums[i])+1)
        } else {
            map.set(nums[i], 1)
        } 
    }
    for (let item of map.entires()) {
        if (item[1] == 1) {
         	return item[0]   
         }
    }
};

// 哈希
var singleNumber = function(nums) {
  let numsObj = {};
  for (let i = 0; i < nums.length; i++) {
    if (numsObj[nums[i]]) delete numsObj[nums[i]];
    else numsObj[nums[i]] = 1;
  }
  return Object.keys(numsObj)[0];
};

// 
var singleNumber = function(nums) {
  for (let i = 0; i < nums.length; i++) {
    if (nums.lastIndexOf(nums[i]) === nums.indexOf(nums[i])) return nums[i];
  }
};

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现三次。找出那个只出现了一次的元素。

// 数学思路 3(a+b)-(a+b+b+b)=2a
var singleNumber = function(nums) {
    let arr=[...new Set(nums)]; // 去重
    return (3*arr.reduce((a,b)=>a+b)-nums.reduce((a,b)=>a+b))/2
};

// 排序对比 
var singleNumber = function(nums) {
    nums.sort((a,b)=>a-b);
    for(let i=0;i<nums.length;i+=3){
        if(nums[i] != nums[i+1]) return nums[i];
    }
};

// 
var singleNumber = function(nums) {
    let obj = {}
    for( let i = 0 ; i < nums.length ; i++ ){
        if( obj.hasOwnProperty(nums[i]) ){
            obj[nums[i]]++
        } else{
            obj[nums[i]] = 1
        }
    }
    for( let key in obj ){
        if( obj[key] == 1 ) return key
    }
};

判断回文数

// 1
var isPalindrome = function(x) {
    if ( x < 0 ) return false
    let str = '' + x
    return Array.from(str).reverse().join('') === str
};
// 2 从后往前循环字符串数组
var isPalindrome = function(x) {
    let str = x + ''
    let newStr = ''
    for(let len = str.length, i = len - 1; i >= 0 ; i--) {
        newStr += str[i]
    }}
    return newStr === str
};
// 3 以中间数为节点,判断左右两边首尾是否相等
var isPalindrome = function(x) {
    if ( x < 0 || (x !== 0 && x % 10 === 0)) {
        return false
    } else if ( 0 <= x && x < 10) {
        return true
    }
    x = '' + x
    for(let i = 0 ; i < x.length/2; i++) {
        if (x[i] !== x[x.length - i - 1]) {
            return false
        }
    }
    return true
};
// 4 将整数反转,反转前后两个整数是否相等来判断是否为回文整数。求模得尾数,除10得整数,将整数求模得到尾数,之后每求一次模,都再原数上添加一位(通过*10来得到),这样就能得到一个反转的数。
var isPalindrome = function(x) {
    if ( x < 0 || (x !== 0 && x % 10 === 0)) {
        return false
    } else if ( 0 <= x && x < 10) {
        return true
    }
    let y = x
    let num = 0
    while(x !== 0) {
        num = x % 10 + num * 10
        x = Math.floor(x / 10)
    }
    return y === num
};

reduce 实现map

var maps = function(arr, callback){
  return arr.reduce((acc, cur, i) => {
        acc.push(callback(cur, i, arr));
        return acc
    }, []);
}

sum(1)(2)

function sum(...initialArgs) {
  let total = initialArgs.reduce((a, b) => a + b, 0);

  function inner(...args) {
    total += args.reduce((a, b) => a + b, 0);
    return inner;
  }

  inner.valueOf = function() {
    return total;
  };

  return inner;
}

网络七层模型

OSI中的层 功能 TCP/IP协议族

  • 应用层 文件传输,电子邮件,文件服务,虚拟终端 TFTP,HTTP,SNMP,FTP,SMTP,DNS,RIP,Telnet
  • 表示层 数据格式化,代码转换,数据加密 ASCII、ASN.1、JPEG、MPEG
  • 会话层 解除或建立与别的接点的联系 NetBIOS、ZIP
  • 传输层 向高层提供可靠的端到端的网络数据流服务 TCP,UDP,SPX
  • 网络层 为数据包选择路由,拥塞控制、网际互连 IP,ICMP,OSPF,BGP,IGMP,ARP,RARP,IPX、RIP、OSPF
  • 数据链路层 传输有地址的帧以及错误检测功能 SLIP,CSLIP,PPP,MTU,ARP,RARP,SDLC、HDLC、PPP、STP、帧中继
  • 物理层 以二进制数据形式在物理媒体上传输数据 ISO2110,IEEE802,IEEE802.2,EIA/TIA RS-232、EIA/TIA RS-449、V.35、RJ-45

二层(数据链路层)的数据叫「Frame」,第三层(网络层)上的数据叫「Packet」,第四层(传输层)的数据叫「Segment」

所有的URL地址在 urls 参数中,同时可以通过 max 参数控制请求的并发数,当所有的请求结束之后,需要执行 callback 回调函数。发请求的函数可以直接使用 fetch 即可

function sendRequest(urls, max, callback) {
  const results = []; // 存储结果
  const errors = []; // 存储错误

  function _request() {
    while (urls.length > 0 && max > 0) {
      const url = urls.shift(); // 从数组中取出一个URL
      max--; // 占用通道
      fetch(url)
        .then(response => {
          if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
          }
          return response.json();
        })
        .then(data => {
          results.push(data); // 存储成功的结果
        })
        .catch(error => {
          errors.push(error); // 存储错误
        })
        .finally(() => {
          max++; // 释放通道
          if (urls.length === 0 && max === 0) {
            callback(errors, results); // 所有请求完成后执行回调函数
          } else {
            _request(); // 继续请求
          }
        });
    }
  }

  _request(); // 开始请求
}
const mapUrlList = (urls) => urls.map(url => fetch(url));

function sendRequest(urls, max, callback) {
  if (urls.length === 0) {
    callback();
    return;
  }
  
  const reqList = urls.splice(0, max);
  Promise.all(mapUrlList(reqList)).then((respList) => {
    sendRequest(urls, max, callback);
  });
}

给定一个整数数组 a,其中有些元素出现两次而其他元素出现一次。

遍历数组,将遍历到的数组value作为index去找value所对应的index里的值,并将其乘负一,如果当前值已经是-1了,说明已经遇到value了,就找到重复的了

var findDuplicates = function(nums) {
    const res = []

    for (const num of nums) {
        const absNum = Math.abs(num)
        if (nums[absNum - 1] < 0) {
            res.push(absNum)
        } else {
            nums[absNum - 1] *= -1
        }
    }
    
    return res
};

最短回文串

给定一个字符串 s,你可以通过在字符串前面添加字符将其转换为回文串。找到并返回可以用这种方式转换的最短回文串

只能从前面添加字符来转换回文串。而有的字符串前面一部分可能是回文。
所以我们要从头开始先找到字符串中回文的一段。
获得字符串中回文的字符串后,再获得剩下需要添加到前头的字符串。
然后拼接起来,就是答案了。

var shortestPalindrome = function(s) {
    let str = ''; // 用于判断回文字符串的正向存储
    let restr = ''; // 用于判断回文字符串的反向存储
    let cylen = 0; // 回文字符串的长度
    for(let i=0;i<s.length;i++){ // 判断回文字符串
        str += s[i];
        restr = s[i] + restr;
        if(str == restr) cylen = cylen < str.length ? str.length : cylen;
    }
    let reverse = s;
    // 拼接字符串,从cylen开始
    for(let i=cylen;i<s.length;i++){
        reverse = s[i] + reverse;
    }
    return reverse
};


function shortestPalindrome(s: string): string {
    let reverse = s.split("").reverse().join("");
    for (let i = 0; i <= s.length; i++) {
        if (s.startsWith(reverse.slice(i))) return reverse.slice(0, i) + s;
    }
};

两个链表的第一个公共节点

var getIntersectionNode = function(headA, headB) {
    var h1 = headA;
    var h2 = headB;

    while(h1 !== h2){ // 如果相交、或者没有相交
        h1 = h1 === null ? headB: h1.next; // h1结束 接入对方
        h2 = h2 === null ? headA: h2.next;  // h2结束 接入对方
    }

    return h1;
};

汉明距离

两个整数之间的汉明距离指的是这两个数字对应二进制位不同的位置的数目。

var hammingDistance = function(x, y) {
    var ans = 0;
    x = x.toString(2); //11
    y = y.toString(2); //1

    if(x.length > y.length){
         while(y.length !== x.length){
            y = '0' + y;
         }
    }
    else if(x.length < y.length){
         while(y.length !== x.length){
            x = '0' + x;
        }
    }
     for(var i = 0; i < x.length;i++){

        if(x[i] !== y[i] ){
            ans++;
        } 
    }
     return ans;

};

617.合并二叉树

给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。

你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。

const mergeTrees = (t1, t2) => {
  if (t1 == null && t2) {
    return t2;
  }
  if ((t1 && t2 == null) || (t1 == null && t2 == null)) {
    return t1;
  }
  t1.val += t2.val;

  t1.left = mergeTrees(t1.left, t2.left);
  t1.right = mergeTrees(t1.right, t2.right);

  return t1;
};

回溯的套路

回溯的套路(可硬记):

遍历枚举出所有可能的选择。 依次尝试这些选择:作出一种选择,并往下递归。 如果这个选择产生不出正确的解,要撤销这个选择(将当前的 "Q" 恢复为 "."),回到之前的状态,并作出下一个可用的选择。 是一个选择、探索、撤销选择的过程。识别出死胡同,就回溯,尝试下一个点,不做无效的搜索

77. 组合,给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。

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

回溯剪枝

const combine = (n, k) => {
  const res = [];

  const helper = (start, path) => { // start是枚举选择的起点 path是当前构建的路径(组合)
    if (path.length == k) {
      res.push(path.slice());       // 拷贝一份path,推入res
      return;                       // 结束当前递归
    }
    for (let i = start; i <= n; i++) { // 枚举出所有选择
      path.push(i);                    // 选择
      helper(i + 1, path);             // 向下继续选择
      path.pop();                      // 撤销选择
    }
  };

  helper(1, []); // 递归的入口,从数字1开始选
  return res;
}

79. 单词搜索

给定一个二维网格和一个单词,找出该单词是否存在于网格中。

示例:

board =
[
  ['A','B','C','E'],
  ['S','F','C','S'],
  ['A','D','E','E']
]

给定 word = "ABCCED", 返回 true
给定 word = "SEE", 返回 true
给定 word = "ABCB", 返回 false
const exist = (board, word) => {
    const m = board.length;
    const n = board[0].length;
    const used = new Array(m);    // 二维矩阵used,存放bool值
    for (let i = 0; i < m; i++) {
        used[i] = new Array(n);
    }
    // canFind 判断当前点是否是目标路径上的点
    const canFind = (row, col, i) => { // row col 当前点的坐标,i当前考察的word字符索引
        if (i == word.length) {        // 递归的出口 i越界了就返回true
            return true;
        }
        if (row < 0 || row >= m || col < 0 || col >= n) { // 当前点越界 返回false
            return false;
        }
        if (used[row][col] || board[row][col] != word[i]) { // 当前点已经访问过,或,非目标点
            return false;
        }
        // 排除掉所有false的情况,当前点暂时没毛病,可以继续递归考察
        used[row][col] = true;  // 记录一下当前点被访问了
        // canFindRest:基于当前选择的点[row,col],能否找到剩余字符的路径。
        const canFindRest = canFind(row + 1, col, i + 1) || canFind(row - 1, col, i + 1) ||
            canFind(row, col + 1, i + 1) || canFind(row, col - 1, i + 1); 

        if (canFindRest) { // 基于当前点[row,col],可以为剩下的字符找到路径
            return true;    
        }
        used[row][col] = false; // 不能为剩下字符找到路径,返回false,撤销当前点的访问状态
        return false;
    };

    for (let i = 0; i < m; i++) { // 遍历找起点,作为递归入口
      for (let j = 0; j < n; j++) {
        if (board[i][j] == word[0] && canFind(i, j, 0)) { // 找到起点且递归结果为真,找到目标路径
          return true; 
        }
      }
    }
    return false; // 怎么样都没有返回true,则返回false
};

全排列,给出集合 [1,2,3,...,n],其所有元素共有 n! 种排列。按大小顺序列出所有排列情况,并一一标记,当 n = 3 时, 所有排列如下:

"123" "132" "213" "231" "312" "321"

给定 n 和 k,返回第 k 个排列。 输入:n = 3, k = 3 输出:"213"

DFS回溯
n 个数字的排列一共有 n!n! 个,以 1 开头的排列有 (n-1)!(n−1)! 个,以 2、3…… n 为开头的也是。

可以看成不同的组。第 k 个排列落在哪个组,直接空降进去找,
如上图,n = 4 ,k = 10,要找的是第 10 个。我们把数字都放到 nums 数组[1,2,3,4],索引 0 对应数字 1,有一个错位,所以我把 k 减 1 处理。

(10-1) / 3! 的取整,等于索引 1,对应 nums 数组的 2,因此,确定了第一个数字 2。
现在,剩下 3 个数字,[1, 3, 4],继续分组,如下图,更新一下 k,看看落入哪一组,重复上述步骤,确定出下一个数字,直到确定完 n 个数字。

const getPermutation = (n, k) => { // 以 n=4 k=10 为例
  const nums = [];
  let factorial = 1;               // 阶乘  

  for (let i = 1; i <= n; i++) {
    nums.push(i);                  // [1, 2, 3, 4]
    factorial = factorial * i;     // 4!   24
  }

  k--;     // nums中数字们的索引是从0开始,k要先减去1
  let resStr = '';

  while (nums.length > 0) {              // 选了一个数字就删掉,直到为空
    factorial = factorial / nums.length; //  3! .. 2! .. 1!
    const index = k / factorial | 0;     // 当前选择的数字的索引
    resStr += nums[index];               // 加上当前选的数字
    nums.splice(index, 1);               // nums删去选的这个数字
    k = k % factorial;                   // 更新 k,
  }
  return resStr;
};

N 皇后

n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。

const solveNQueens = (n) => {
  const board = new Array(n);
  for (let i = 0; i < n; i++) {   // 棋盘的初始化
    board[i] = new Array(n).fill('.');
  }

  const cols = new Set();  // 列集,记录出现过皇后的列
  const diag1 = new Set(); // 正对角线集
  const diag2 = new Set(); // 反对角线集
  const res = [];

  const helper = (row) => {   // 递归的出口,超出了最后一行
    if (row == n) {        // 递归的出口,超出了最后一行
      const stringsBoard = board.slice();  // 拷贝一份board
      for (let i = 0; i < n; i++) {
        stringsBoard[i] = stringsBoard[i].join('');  // 将每一行拼成字符串
      }
      res.push(stringsBoard);
      return;
    }
    for (let col = 0; col < n; col++) { // 枚举出所有选择
      // 如果当前点的所在的列,所在的对角线都没有皇后,即可选择,否则,跳过
      if (!cols.has(col) && !diag1.has(row + col) && !diag2.has(row - col)) { 
        board[row][col] = 'Q';  // 放置皇后
        cols.add(col);          // 记录放了皇后的列
        diag1.add(row + col);   // 记录放了皇后的正对角线
        diag2.add(row - col);   // 记录放了皇后的负对角线
        helper(row + 1);
        board[row][col] = '.';  // 撤销该点的皇后
        cols.delete(col);       // 对应的记录也删一下
        diag1.delete(row + col);
        diag2.delete(row - col);
      }
    }
  };
  helper(0);
  return res;
};

332. 重新安排行程

给定一个机票的字符串二维数组 [from, to],子数组中的两个成员分别表示飞机出发和降落的机场地点,对该行程进行重新规划排序。

输入:[["MUC", "LHR"], ["JFK", "MUC"], ["SFO", "SJC"], ["LHR", "SFO"]]
输出:["JFK", "MUC", "LHR", "SFO", "SJC"]

输入:[["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"]]
输出:["JFK","ATL","JFK","SFO","ATL","SFO"]
解释:另一种有效的行程是 ["JFK","SFO","ATL","JFK","ATL","SFO"]。但是它自然排序更大更靠后。
用 dfs 遍历,从 JFK 开始,尝试所有可能的选择,这需要知道当前可以飞哪些城市,需要构建出邻接表。根据当前选择,往下递归,尝试找出第一个用完机票的路径,如果找不出来,返回false,否则,返回true。

为什么要返回真假,因为要用它判断要不要提前回溯,在该分支走不下去,就要离开。

访问过的边要删掉——用掉的机票不能再用。北京飞广州,到了广州,北京的邻居list中删掉广州。

我们选择飞入的城市,如果发现困住了,得不到解,就要回溯,将北京的邻居list中删除的广州恢复回来,不飞广州了,飞别的试试,离开当前分支,切入别的分支,继续探索路径。


const findItinerary = (tickets) => {
  const res = ['JFK']; // 初始放入起点'JFK'
  const map = {};      // 邻接表

  for (const ticket of tickets) { // 遍历tickets,建表
    const [from, to] = ticket;    // 每一张机票,读出起点和终点
    if (!map[from]) {
      map[from] = []; // 初始化
    }
    map[from].push(to); // 建立映射
  }

  for (const city in map) { // 按照字母顺序,小的在前
    map[city].sort();
  }

  const dfs = (city, used) => { // city是当前访问的城市、used是已用掉的机票数
    if (used == tickets.length) { // 用光了所有机票,路径找到了
      return true;
    };
    const nextCities = map[city]; // 获取下一站能去的城市list
    if (!nextCities || nextCities.length == 0) { // 没有邻接城市了
      return false; // 还没用光机票,就没有下一站了,返回false
    }
    for (let i = 0; i < nextCities.length; i++) { // 设置出各种选项(递归分支)
      const next = nextCities[i]; // 当前选择的下一站
      nextCities.splice(i, 1);    // 飞出地的list中删掉这一站
      res.push(next);             // 将该选择推入res
      if (dfs(next, used + 1)) {  // 在该递归分支中能找到一个用完所有机票的路径
        return true;
      } else {
        nextCities.splice(i, 0, next); // 将删掉的这一站重新插回去
        res.pop();                     // 推入res的选择,也撤销
      }
    }
  };

  dfs('JFK', 0); // 从'JFK'城市开始遍历,当前用掉0张机票
  return res;
};
欧拉路径
如果在一张图中,从一个点出发可以走完所有的边,则这个遍历走过的路径就叫欧拉路径。
可以理解为:一张图可以一笔画出来。

题意已知图中存在欧拉路径,你要找出一个欧拉路径,可以用 hierholzer 算法。

任选一个点为起点(题目把起点告诉你了),遍历它所有邻接的边(设置不同的分支)。
DFS 搜索,访问邻接的点,并且将走过的边(邻接关系)删除。
如果走到的当前点,已经没有相邻边了,则将当前点推入 res。
随着递归的出栈,点不断推入 res 的开头,最后就得到一个从起点出发的欧拉路径。

const findItinerary = (tickets) => {
  const res = [];
  const map = {};
  
  for (const ticket of tickets) { // 建表
    const [from, to] = ticket;
    if (!map[from]) {
      map[from] = [];
    }
    map[from].push(to);
  }
  for (const city in map) {
    map[city].sort();
  }

  const dfs = (node) => { // 当前城市
    const nextNodes = map[node]; // 当前城市的邻接城市
    while (nextNodes && nextNodes.length) { // 遍历,一次迭代设置一个递归分支
      const next = nextNodes.shift(); // 获取并移除第一项,字母小的城市
      dfs(next);                      // 向下递归
    }                 
    // 当前城市没有下一站,就把他加到res里,递归开始向上返回,选过的城市一个个推入res 
    res.unshift(node); 
  };

  dfs('JFK'); // 起点城市
  return res;
};

494. 目标和

给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。

返回可以使最终数组和为目标数 S 的所有添加符号的方法数。

dfs

/**
 * @param {number[]} nums
 * @param {number} S
 * @return {number}
 */
var findTargetSumWays = function(nums, S) {
    // 方法1:深度遍历
    let count = 0;
    function dfs(i, res) {
        if(i === nums.length) {
            if(res === S) {
                count++;
            }
            return;
        };
        dfs(i+1, res+nums[i]);
        dfs(i+1, res-nums[i]);
    }
    dfs(1, nums[0]);
    dfs(1, -nums[0]);
    return count;
};
所以状态转移方程是:
dp[n][s] = dp[n - 1][s - num] + dp[n- 1][s + num]

/**
 * @param {number[]} nums
 * @param {number} S
 * @return {number}
 */
let findTargetSumWays = function (nums, S) {
  let ns = nums.length
  if (!ns) {
    return 0
  }
  let min = nums.reduce((sum, cur) => sum - cur, 0)
  let max = nums.reduce((sum, cur) => sum + cur, 0)

  let dp = []
  for (let n = 0; n < ns; n++) {
    dp[n] = []
  }

  // 基础状态
  for (let s = min; s <= max; s++) {
    let num = nums[0]
    let pickPositive = s === num ? 1 : 0
    // 选负数形态
    let pickNegative = -s === num ? 1 : 0
    dp[0][s] = pickPositive + pickNegative
  }

  for (let n = 1; n < ns; n++) {
    for (let s = min; s <= max; s++) {
      let num = nums[n]
      // 选正数形态
      let pickPositive = dp[n - 1][s - num] || 0
      // 选负数形态
      let pickNegative = dp[n - 1][s + num] || 0
      dp[n][s] = pickNegative + pickPositive
    }
  }
  return dp[ns - 1][S] || 0
}

动态规划问题的一般形式就是求最值。动态规划其实是运筹学的一种最优化方法

72. 编辑距离

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。

如果我们 word1[i] 与 word2[j] 相等,这个时候不需要进行任何操作,显然有 dp[i][j] = dp[i-1][j-1]

如果我们 word1[i] 与 word2[j] 不相等,这个时候我们就必须进行调整,而调整的操作有 3 种,我们要选择一种。三种操作对应的关系试如下(注意字符串与字符的区别):

如果在字符串 word1 末尾插入一个与 word2[j] 相等的字符,则有 dp[i][j] = dp[i][j-1] + 1
如果把字符 word1[i] 删除,则有 dp[i][j] = dp[i-1][j] + 1
如果把字符 word1[i] 替换成与 word2[j] 相等,则有 dp[i][j] = dp[i-1][j-1] + 1
那么我们应该选择哪一种操作,可以使得 dp[i][j] 的值最小?显然有:
dp[i][j] = min(dp[i-1][j-1],dp[i][j-1],dp[[i-1][j]]) + 1


let minDistance = (word1, word2)=> {
    //1.初始化
    let n = word1.length, m = word2.length
    let dp = new Array(n+1).fill(0).map(() => new Array(m+1).fill(0))
    for (let i = 0; i <= n; i++) {
        dp[i][0] = i
    }
    for (let j = 0; j <= m; j++) {
        dp[0][j] = j
    }
    //2.dp
    for(let i = 0;i <= n;i++){
        for(let j = 0;j <= m;j++){
            if(i*j){
                dp[i][j] = word1[i-1] == word2[j-1]? dp[i-1][j-1]: (Math.min(dp[i-1][j],dp[i][j-1],dp[i-1][j-1]) + 1)
            }else{
                dp[i][j] = i + j
            }
        }
    }
    return dp[n][m]
};

53. 最大子序和

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
var maxSubArray = function(nums) {
    let pre = 0, maxAns = nums[0];
    nums.forEach((x) => {
        pre = Math.max(pre + x, x);
        maxAns = Math.max(maxAns, pre);
    });
    return maxAns;
};

1143. 最长公共子序列

给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。

输入:text1 = "abcde", text2 = "ace" 
输出:3  
解释:最长公共子序列是 "ace",它的长度为 3。

当我们对比 text1[m] 和 text2[n] 的时候,存在两种情况:
当text1[m] === text2[n]时,说明 text1[m] 或者 text2[n] 对应的字符是最长公共子序列的一部分,所以有 dp[m][n] = 1 + dp[m-1][n-1]
当text1[m] !== text2[n]时,此时我们要看两个字符串分别单独往回撤一个字符串的对比情况,获取两者的最大值即可,所以有 dp[m][n] = Math.max(dp[m - 1][n], dp[m][n - 1], dp[m-1][n-1])
/**
 * @param {string} text1
 * @param {string} text2
 * @return {number}
 * dp(m,n)表示:S1[0...m] 和 S2[0...n] 的最长公共子序列的长度
 * S1[m] == S[n]:dp(m,n) = 1 + dp(m - 1,n - 1)
 * dp(m,n) = max(dp(m - 1,n), dp(m,n - 1))
 */
var longestCommonSubsequence = function(text1, text2) {
  const m = text1.length;
  const n = text2.length;

  // 初始化二维 dp 数组
  const dp = new Array(m)
  for (let i = 0; i < m; i++) {
    dp[i] = new Array(n).fill(0)
  }

  // 从前往后遍历设置 dp[i][j],根据 dp[0][0..n] 和 dp[0..m][0] 都为 0,推导出 dp[m-1][n-1]
  for (let i = 0; i < m; i++) {
      for (let j = 0; j < n; j++) {
          // 第一种情况,两者相等,则 dp(m,n) = 1 + dp(m - 1,n - 1)
          if (text1[i] === text2[j]) {
            if (i - 1 < 0 || j - 1 < 0) { // 越界处理
              dp[i][j] = 1 + 0
            } else {
              dp[i][j] = 1 + dp[i - 1][j - 1];
            }  
          } else { // 第二种情况,两者不相等,则 dp(m,n) = max(dp(m - 1,n), dp(m,n - 1))
              if (i - 1 < 0 || j - 1 < 0) { // 越界处理
                if (i - 1 < 0 && j - 1 < 0) {
                  dp[i][j] = 0
                } else if (i - 1 < 0) {
                  dp[i][j] = dp[i][j - 1]
                } else if (j - 1 < 0) {
                  dp[i][j] = dp[i - 1][j]
                } 
              } else {
                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
              }
             
          }
      }
  }

  // dp[m - 1][n - 1] 即为结果
  return dp[m - 1][n - 1];
};

两个字符串的删除操作

给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。

/**
 * @param {string} word1
 * @param {string} word2
 * @return {number}
 */
var minDistance = function(word1, word2) {
    // 递归解法
    // return calcByRecur(word1, word2)

    // 循环解法:正向线性计算
    return calcByLoop(word1, word2)
};

function calcByLoop(str1, str2) {
    // 建立dp数组
    const dp = new Array(str1.length + 1)
    let i = 0
    while (i < dp.length) {
        dp[i] = new Array(str2.length + 1)
        i++
    }

    // dp默认值
    i = 0
    while (i < dp[0].length) {
        dp[0][i] = i
        i++
    }
    i = 0
    while (i < dp.length) {
        dp[i][0] = i
        i++
    }

    // 计算开始:线性
    for (let i = 1; i < dp.length; i++) {
        for (let j = 1; j <dp[0].length; j++) {
            if (str1[i - 1] === str2[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1]
            }
            else {
                dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + 1
            }
        }
    }

    return dp[str1.length][str2.length]
}

function calcByRecur(str1, str2) {

    function calc(pos1, pos2) {
        if (pos1 === -1) {
            return pos2 + 1
        }
        if (pos2 ===  -1) {
            return pos1 + 1
        }

        if (str1[pos1] === str2[pos2]) {
            // 相等:无需增加
            return calc(pos1 - 1, pos2 - 1)
        }
        else {
            // 不相等:递归求解并+1
            return Math.min(calc(pos1 - 1, pos2), calc(pos1, pos2 - 1)) + 1
        }
    }

    return calc(str1.length - 1, str2.length - 1)
}

712. 两个字符串的最小ASCII删除和

给定两个字符串s1, s2,找到使两个字符串相等所需删除字符的ASCII值的最小和。

输入: s1 = "sea", s2 = "eat"
输出: 231
解释: 在 "sea" 中删除 "s" 并将 "s" 的值(115)加入总和。
在 "eat" 中删除 "t" 并将 116 加入总和。
结束时,两个字符串相等,115 + 116 = 231 就是符合条件的最小和。
dp[i][j]是两个字符串相等的最小和两种状态
当前元素不删除:
dp[i][j] = dp[i-1][j-1]
当前元素删除
dp[i][j] = Math.min(dp[i-1][j] + s1[i-1].charCodeAt(),dp[i][j-1]+s2[j-1].charCodeAt())
/**
 * @param {string} s1
 * @param {string} s2
 * @return {number}
 */
var minimumDeleteSum = function(s1, s2) {
    let dp = Array.from({length:s1.length+1},()=> new Array(s2.length+1));
    dp[0][0] = 0;
    for(let i = 1; i <= s2.length;i++) {
        dp[0][i] = dp[0][i-1] + s2[i-1].charCodeAt();
    }
    for(let i = 1; i <= s1.length; i++) {
        dp[i][0] = dp[i-1][0] + s1[i-1].charCodeAt();
    }
    for(let i = 1; i <= s1.length; i++) {
        for(let j = 1; j<= s2.length; j++) {
            if(s1[i-1] === s2[j-1]) {
                dp[i][j] = dp[i-1][j-1]
            } else {
                dp[i][j] = Math.min(dp[i-1][j] + s1[i-1].charCodeAt(),dp[i][j-1]+s2[j-1].charCodeAt())
            }
        }
    }
    return dp[s1.length][s2.length];
};

516. 最长回文子序列

给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。

/**
 * @param {string} s
 * @return {number}
 */
var longestPalindromeSubseq = function(s) {
    let length = s.length;
    
    // dp[i][j]表示的是从s[i]至s[j]之间的最长回文子序列的长度
    let dp = new Array(length);
    for (let i = 0; i < length; i++) {
        dp[i] = new Array(length).fill(0);
    }
    
    for (let i = length - 1; i >= 0; i--) {
        // 每一个字符都是一个回文字符串,因此对于dp[i][i]设置为1
        dp[i][i] = 1;
        for (let j = i+1; j < length; j++) {
            // 状态转移方程为:
            // 当s[i]等于s[j]时,dp[i][j] = dp[i-1][j+1] + 2;
            // 当s[i]不等于s[j]时,dp[i][j] = max(dp[i-1][j], dp[i][j+1])
            if (s[i] === s[j]) {
                dp[i][j] = dp[i+1][j-1] + 2;
            } else {
                dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1])
            }
        }
    }
    return dp[0][length-1];
};

5. 最长回文子串

给你一个字符串 s,找到 s 中最长的回文子串。

输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
状态定义
dp[i,j]:字符串s从索引i到j的子串是否是回文串
true: s[i,j] 是回文串
false:s[i,j] 不是回文串
转移方程
dp[i][j] = dp[i+1][j-1] && s[i] == s[j]
s[i] == s[j]:说明当前中心可以继续扩张,进而有可能扩大回文串的长度
dp[i+1][j-1]:true
说明s[i,j]的**子串s[i+1][j-1]**也是回文串
说明,i是从最大值开始遍历的,j是从最小值开始遍历的
特殊情况
j - i < 2:意即子串是一个长度为01的回文串
总结
dp[i][j] = s[i] == s[j] && ( dp[i+1][j-1] || j - i < 2)
var longestPalindrome = function(s) {
    let n = s.length;
    let res = '';
    let dp = Array.from(new Array(n),() => new Array(n).fill(0));
    for(let i = n-1;i >= 0;i--){
        for(let j = i;j < n;j++){
            dp[i][j] = s[i] == s[j] && (j - i < 2 || dp[i+1][j-1]);
            if(dp[i][j] && j - i +1 > res.length){
                res = s.substring(i,j+1);
            }
        }
    }
    return res;
};
中心扩展法
思路
回文串一定是对称的
每次选择一个中心,进行中心向两边扩展比较左右字符是否相等
中心点的选取有两种
aba,中心点是b
aa,中心点是两个a之间
所以共有两种组合可能
left:i,right:i
left:i,right:i+1
图解


/**
 * @param {string} s
 * @return {string}
 */
var longestPalindrome = function(s) {
    if(!s || s.length < 2){
        return s;
    }
    let start = 0,end = 0;
    let n = s.length;
    // 中心扩展法
    let centerExpend = (left,right) => {
        while(left >= 0 && right < n && s[left] == s[right]){
            left--;
            right++;
        }
        return right - left - 1;
    }
    for(let i = 0;i < n;i++){
        let len1 = centerExpend(i,i);
        let len2 = centerExpend(i,i+1);
        // 两种组合取最大回文串的长度
        let maxLen = Math.max(len1,len2);
        if(maxLen > end - start){
            // 更新最大回文串的首尾字符索引
            start = i - ((maxLen - 1) >> 1);
            end = i + (maxLen >> 1);
        }
    }
    return s.substring(start,end+1);
};

887. 鸡蛋掉落

给你 k 枚相同的鸡蛋,并可以使用一栋从第 1 层到第 n 层共有 n 层楼的建筑。

var superEggDrop = function (K, N) {
  // 不选择dp[K][M]的原因是dp[M][K]可以简化操作
  const dp = Array(N + 1)
    .fill(0)
    .map((_) => Array(K + 1).fill(0));

  let m = 0;
  while (dp[m][K] < N) {
    m++;
    for (let k = 1; k <= K; ++k) dp[m][k] = dp[m - 1][k - 1] + 1 + dp[m - 1][k];
  }
  return m;
};
状态:dp[i][j] 有i个鸡蛋,j次扔鸡蛋的测得的最多楼层
转移方程:dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j] + 1
一维优化版:dp[i] = dp[i-1] + dp[i] + 1
dp[i] 表示当前次数下使用i个鸡蛋可以测出的最高楼层

let superEggDrop = (K, N)=> {
    let dp = Array(K+1).fill(0)
    let cnt = 0
    while (dp[K] < N){
        cnt++
        for (let i=K; i>0; i--){
            dp[i] = dp[i-1] + dp[i] + 1
        }
    }
    return cnt
};

分割等和子集

给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

输入: [1, 5, 11, 5]

输出: true

解释: 数组可以分割成 [1, 5, 5][11].
const canPartition = (nums) => {
    let sum = 0;
    for (const n of nums) { // 求数组和
        sum += n;
    }
    if (sum % 2 != 0) return false; // 如果 sum 为奇数,直接返回 false
    const memo = new Map();
    const target = sum / 2; // 目标和

    const dfs = (curSum, i) => {    // curSum是当前累加和,i是指针
        if (i == nums.length || curSum > target) { // 递归的出口
            return false;
        }
        if (curSum == target) {                    // 递归的出口
            return true;
        }
        const key = curSum + '&' + i;   // 描述一个问题的key
        if (memo.has(key)) {            // 如果memo中有对应的缓存值,直接使用
            return memo.get(key);
        }
        const res = dfs(curSum + nums[i], i + 1) || dfs(curSum, i + 1);
        memo.set(key, res);  // 计算的结果存入memo
        return res;
    };

    return dfs(0, 0); // 递归的入口,当前和为0,指针为0
};

322. 零钱兑换

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

输入:coins = [1, 2, 5], amount = 11
输出:3 
解释:11 = 5 + 5 + 1

/**
 *
 * 时间复杂度O(n*m),n为coins的长度,m为amount
 * 空间复杂度O(1)
 * 
 * @param {*} coins 
 * @param {*} amount 
 */
function coinChange3(coins, amount) {
    // dp 数组的定义:当目标金额为 i 时,至少需要 dp[i] 枚硬币凑出
    // dp 数组初始化为 amount + 1 , 因为凑成 amount 金额的硬币数最多只可能等于 amount(全用 1 元面值的硬币),
    // 所以初始化为 amount + 1 就相当于初始化为正无穷,便于后续取最小值
    let dp = new Array(amount + 1).fill(amount+1);
    dp[0] = 0;
    // 外层 for 循环在遍历所有状态的所有取值
    for (let i = 0; i < dp.length; i++) {
        // 内层 for 循环在求所有选择的最小值
        for (let coin of coins) {
            // 子问题无解
            if (i - coin < 0) {
                continue;
            }
            dp[i] = Math.min(dp[i], 1 + dp[i - coin]);
        }
    }
    // 等于初始值
    return dp[amount] === amount + 1 ? -1 : dp[amount];
}

518. 零钱兑换 II

给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。

输入: amount = 5, coins = [1, 2, 5]
输出: 4
解释: 有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
/**
 * @param {number} amount
 * @param {number[]} coins
 * @return {number}
 */
var change = function (amount, coins) {
  // 不断使用硬币组合,从0元一只组合到amount
  // 数组保存的是每个金额可用的硬币组合数量
  // 初始状态因为都没有递推,都为0
  let dp = new Array(amount + 1).fill(0);
  // 0位置设置为1,因为第一次递推都是从金额为coin开始
  // 状态转移方程为dp[i] = dp[i] + dp[i - coin],这样dp[coin]的初始值就为dp[0]
  // 由于第一次要投一个硬币,因此dp[0] = 1,能保证正常递推
  dp[0] = 1;

  // 分别计算每个硬币可以组合成amount的方法数量
  for (const coin of coins) {
    // 小于coin的金额是无法通过coin组合而成
    // 一直递推到amount,就知道当前硬币组合成amount有多少种方法
    for (let i = coin; i <= amount; i++) {
      dp[i] =
        // 保留之前硬币可以组合出当前金额的方法数量
        dp[i] +
        // 当前金额,必然是从i-coin,添加coin金额硬币之后组合而成
        // 也就是当前金额是在i-coin的基础上变化而来,因此需要带上i-coin的组合数量
        dp[i - coin];
    }
  }

  // 最后组合成amount的方法数,就在amount位置
  return dp[amount];
};

213. 打家劫舍 II

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。

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

输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
所以此题可以分为两种情况
偷第一家,不能偷最后一家
不偷第一家,能偷最后一家
因此在代码中,直接截取掉第一个和最后一个数字分解成两个子问题求解即可。

/**
 * @param {number[]} nums
 * @return {number}
 */
var rob = function(nums) {
    var n = nums.length;
    if(n == 1){
        return nums[0];
    }else if(n == 0){
        return 0;
    }
    function dpGO(nums){
       var n = nums.length;
       var dp = Array.from(new Array(n),() => new Array(n));
       dp[0][0] = 0;
       dp[0][1] = nums[0];
       for(var i = 1;i < nums.length;i++){
           dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]);
           dp[i][1] = nums[i]+dp[i-1][0];
       }
       return Math.max(dp[n-1][0],dp[n-1][1]);
    }
    var need1 = dpGO(nums.slice(1));
    var need2 = dpGO(nums.slice(0,nums.length-1));
    return Math.max(need1,need2);
};


/**
 * @param {number[]} nums
 * @return {number}
 */
var rob = function (nums) {
  // 用dp1递推从0偷到nums.length-2,初始值是第0户偷与不偷的状态
  let dp1 = [[nums[0] || 0, 0]];

  // 从1开始递推到nums.length-2
  for (let i = 1; i < nums.length - 1; i++) {
    dp1[i] = [
      // 如果偷当前人家,那么上一户人家就不能偷,只能取不偷的值
      dp1[i - 1][1] + nums[i],
      // 如果不偷当前人家,上一户人家偷不偷都可以,只需要取一个最大值
      Math.max(dp1[i - 1][0], dp1[i - 1][1]),
    ];
  }

  // 用dp2递推从1偷到nums.length-1,初始值是第1户偷与不偷的状态
  // 为保证dp2索引和nums能一一对应,dp2[0]存储一个空数组占位
  let dp2 = [[], [nums[1] || 0, 0]];

  // 从0开始递推到nums.length-1
  for (let i = 2; i < nums.length; i++) {
    dp2[i] = [
      // 如果偷当前人家,那么上一户人家就不能偷,只能取不偷的值
      dp2[i - 1][1] + nums[i],
      // 如果不偷当前人家,上一户人家偷不偷都可以,只需要取一个最大值
      Math.max(dp2[i - 1][0], dp2[i - 1][1]),
    ];
  }

  // 两次递推结果的最大值,就是最终结果
  return Math.max(...dp1[dp1.length - 1], ...dp2[dp2.length - 1]);
};

337. 打家劫舍 III

在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。

打不打劫根节点,会影响打劫一棵树的收益:

打劫根节点,则不能打劫左右子节点,但是能打劫左右子节点的四个子树(如果有)。
不打劫根节点,则能打劫左子节点和右子节点,收益是打劫左右子树的收益之和。

const rob = (root) => { // 打劫以root为根节点的子树的最大收益
    if (root == null) {
        return 0;
    }
    // 打劫包括根节点的收益,保底是root.val
    let robIncludeRoot = root.val; 
    if (root.left) {
        robIncludeRoot += rob(root.left.left) + rob(root.left.right);
    }
    if (root.right) {
        robIncludeRoot += rob(root.right.left) + rob(root.right.right);
    }
    // 打劫不包括根节点的收益
    const robExcludeRoot = rob(root.left) + rob(root.right); 
    // 二者取其大
    return Math.max(robIncludeRoot, robExcludeRoot); 
};
打劫一个树的最大收益,是 robIncludeRoot 和 robExcludeRoot 中的较大者。

即每个子树都有两个状态下的最优解:没打劫 root、和有打劫 root 下的最优解。

有两个变量共同决定一个状态:1、代表不同子树的 root 节点、2、是否打劫了 root。

可以维护一个二维数组 dp,但对象不能作为数组索引,改用 map。key 是当前子树的 root 节点,value 是存放两个状态的 res 数组。

没打劫根节点,则左右子树的根节点可打劫可不打劫:
res[0] = 左子树的两个状态的较大值 + 右子树的两个状态的较大值。

打劫了根节点,则左右子树的根节点不能打劫:
res[1] = root.val + 左子树的 [0] 状态 + 右子树的 [0] 状态。


const rob = (root) => {
  const dp = new Map();
  // 辅助函数返回打劫以root为根节点的子树的收益
  const helper = (root) => {
    // 递归的出口,遍历到null节点,两种状态下收益都是0
    if (root == null) return [0, 0];
    // 递归调用左右子树
    const left = helper(root.left);
    const right = helper(root.right);
    // 之前没遍历过当前节点
    if (!dp.has(root)) {
      // 在map中添加当前节点,和对应的res数组
      dp.set(root, [0, 0]); 
    }
    // 获取当前节点对应的res数组
    const res = dp.get(root);
    // 将当前子树的两个状态记录到map中
    res[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
    res[1] = root.val + left[0] + right[0];
    // 返回出这两个状态
    return res;                     
  };
  // 递归的入口
  const res = helper(root);          
  // 两种状态下的最优解,取其大
  return Math.max(res[0], res[1]);
};

309. 最佳买卖股票时机含冷冻期

给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。​

设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。 示例:

输入: [1,2,3,0,2]
输出: 3 
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
DP 状态的选择
可以用一个三维数组,i 表示天,j 表示是否持有股票,k表示是否是冷冻期
也可以用一个二维数组,dp[i][j]:i 表示天,j为 0,1,2:0表示持股,1表示不持股,2表示处于冷冻天
也可以用三个一维数组,分别代表第i天,3种选择:卖出、买进、休息,对应的最大收益。
也可以按手中是否持有股票,用两个一维数组,分别代表第 i 天,持有和没持有的最大收益
也就是说,在选择DP状态的定义时,可以尝试着降维。

状态转移方程
hold[i] : 第 i 天,手中持有股票,这时的最大收益。
有两种可能:
昨天就持有股票,今天休息。
前天卖了股票,今天买了股票。
hold[i] = Math.max(hold[i - 1], unhold[i - 2] - prices[i])
unhold[i] : 第 i 天,手中没有股票,此时的最大收益。
有两种可能:今天休息、或卖了股票
昨天也没有持有,今天休息。
昨天持有股票,今天卖了股票。
unhold[i] = Math.max(unhold[i -1], hold[i - 1] + prices[i])
目标是求 unhold[n-1] ( n:0 1 2 3 ... )
base case
hold[0] = -prices[0] 第0天买股票,收益-prices[0]元
hold[1] = Math.max(-prices[0], -prices[1]) 第1天持有着股票,可能是昨天买的,今天休息,也可能是昨天休息,今天买的
unhold[0] = 0 第0天没有持有股票,就是休息,收益 0 元
const maxProfit = (prices) => {
  const n = prices.length;   // n天
  if (n == 0) return 0
  let hold = new Array(n);   // 第i天持有股票的最大收益
  let unhold = new Array(n); // 第i天不持有股票的最大收益
  hold[0] = -prices[0];      // 第0天 买了股票的收益
  unhold[0] = 0
  for (let i = 1; i < n; i++) {
    if (i == 1) {            // base case
      hold[i] = Math.max(hold[i - 1], -prices[1]);
    } else {
      hold[i] = Math.max(hold[i - 1], unhold[i - 2] - prices[i]);
    }
    unhold[i] = Math.max(unhold[i - 1], hold[i - 1] + prices[i]); 
  }
  return unhold[n - 1];
};

小D去旅游

小D要在某个时间点要从A点到B点,A到B有可能有多种不同的路径方案,计算小D走花费时间最小路径到达B点后的时间。

输入第一行 N:节点个数,M:边的个数

输入之后的M行都是 节点begin 节点end 需要花费的时间(小时)

最后一行再输入 小D要出发的点 到达的点 起始时间(起始时间格式:month.day/hour)

输出 小D抵达的时间month.day/hour

输入:
4 4
1 2 5
1 3 6
2 4 8
3 4 6
1 4 7.9/8

输出:
7.9/20 
注:1->3->4 = 12小时


let read_line = (function (){
    let i = 0
    let data = [        '4 4',        '1 2 25',        '1 3 18',        '2 4 28',        '3 4 22',        '1 4 7.9/8',        ]
    // let data = [
    //     '4 4',
    //     '1 2 5',
    //     '1 3 6',
    //     '2 4 8',
    //     '3 4 6',
    //     '1 4 7.9/8',
    //     ]
    return function (){
        return data[i++]
    }
})()

function main(){
    let args = read_line().split(' ')
    let N = args[0]
    let M = args[1]

    // 默认为最大time
    let result = 1000000000000000
    let TREE = {}
    for(let i=0;i<M;i++){
        let p = read_line().split(' ').map(e=>parseInt(e))
        let begin = p[0],end = p[1],time = p[2]
        if(TREE[begin])TREE[begin][end] = time
        else{
            TREE[begin] = {
                [end]:time
            }
        }
    }
    
    // 获取起点、终点和时间
    let p = read_line().split(' ')
    let Begin = p[0],End = p[1],BeginTime = p[2]
    // 构建回溯函数
    function resolve(begin,end,tree,price){
        if(!tree[begin])return 
        for (const key in tree[begin]) {
            if (tree[begin].hasOwnProperty(key)) {
                const element = tree[begin][key];
                // 找到终点
                if(key == end && price+element<result){
                    result = price+element
                    continue
                }
                if(key != end && price+element < result){
                    resolve(key,end,tree,price+element)
                }
            }
        }
    }
    // 获取最小需要花费的时间 => result
    resolve(Begin,End,TREE,0)

    // 返回值为加了result时间后的时间字符串
    function addHour(time,hours){
        let [month,day,hour] = time.split(/[\.\/]/)
        let resHour = parseInt(hour)+parseInt(hours)
        let a = new Date(2020,month-1,parseInt(day)+parseInt(resHour/24),resHour%24)
        return `${a.getMonth()+1}.${a.getDate()}/${a.getHours()}`

    }
    console.log(addHour(BeginTime,result))

}
main()

可选链的实现 a.?b.?c

function get (source, path, defaultValue = undefined) {
  // a[3].b -> a.3.b
  const paths = path.replace(/\[(\d+)\]/g, '.$1').split('.')
  let result = source
  for (const p of paths) {
    result = Object(result)[p]
    if (result === undefined) {
      return defaultValue
    }
  }
  return result
}

存在重复元素 II

给定一个整数数组和一个整数 k,判断数组中是否存在两个不同的索引 i 和 j,使得 nums [i] = nums [j],并且 i 和 j 的差的 绝对值 至多为 k。

输入: nums = [1,2,3,1], k = 3
输出: true

维护一个哈希表,里面始终最多包含 k 个元素,当出现重复值时则说明在 k 距离内存在重复元素 每次遍历一个元素则将其加入哈希表中,如果哈希表的大小大于 k,则移除最前面的数字 时间复杂度:O(n),n 为数组长度

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {boolean}
 */
var containsNearbyDuplicate = function(nums, k) {
    const set = new Set();
    for(let i = 0; i < nums.length; i++) {
        if(set.has(nums[i])) {
            return true;
        }
        set.add(nums[i]);
        if(set.size > k) {
            set.delete(nums[i - k]);
        }
    }
    return false;
};
// o(n) o(1)
var containsNearbyDuplicate = function(nums, k) {
    let i = 0,j=1;
    while(j<=nums.length) {
        if(j-i>k || j==nums.length) {
            i++
            j=i+1
            continue
        }
        if(nums[i]==nums[j]) return true
        j++
    }
    return false
};
/**
 * @param {number[]} nums
 * @param {number} k
 * @return {boolean}
 */
var containsNearbyDuplicate = function(nums, k) {
    const map = new Map();
    for(let i = 0,n = nums.length;i<n;i++){
        if(map.has(nums[i]) && i-map.get(nums[i])<=k){
            return true
        }
        map.set(nums[i],i)
    }
    return false;
};

220.存在重复元素 III

在整数数组 nums 中,是否存在两个下标 i 和 j,使得 nums [i] 和 nums [j] 的差的绝对值小于等于 t ,且满足 i 和 j 的差的绝对值也小于等于 ķ 。如果存在则返回 true,不存在返回 false。

输入: nums = [1,2,3,1], k = 3, t = 0
输出: true

输入: nums = [1,5,9,1,5,9], k = 2, t = 3
输出: false
/**
 * @param {number[]} nums
 * @param {number} k
 * @param {number} t
 * @return {boolean}
 */
var containsNearbyAlmostDuplicate = function (nums, k, t) {
    // 解法一:快慢指针
    let s = 0, f = 1
    // 循环数组
    while (f <= nums.length) {
        // 快指针和慢指针的差要小于等于k,当f的值和数组长度一样的时候还没返回,证明慢指针对应的值和快指针扫过的值都没有符合条件的值,那就要移动慢指针,重置快指针,继续寻找
        if (f - s > k || f === nums.length) {
            s++
            f = s + 1
            continue
        }
        // 计算快慢指针对应值的绝对值是否小于等于t
        if (Math.abs(nums[s] - nums[f]) <= t) return true
        // 大于t, f++,继续完后寻找
        else f++
    }
    return false

    // 解法二:hash表
    // 哈希表的方式,进行窗口的滑动,窗口的宽度受K限制,即哈希表中至多只能放K个元素,如果超过,就把第一个删除然后继续完后遍历,直到滑动窗口中至多有k个元素,遍历hash表,在这个k个元素的窗口中寻找符合条件的,有就直接返回true,遍历完都没有返回值,就直接返回false
    let set = new Set()
    for (let i = 0; i < nums.length; i++) {
        for (let val of set) {
            if (Math.abs(nums[i] - val) <= t) return true
        }
        set.add(nums[i])
        if (set.size > k) set.delete(nums[i - k])
    }
    return false

    // 解法三:暴力解法,第一层for循环固定一个值,第二次for循环从第i+1之后遍历,并且j - i <= k,如果在这个去区间内有一个数和i对应的数的差值小于等于t,符合条件直接返回true
    for (let i = 0; i < nums.length - 1; i++) {
        for (let j = i + 1; j - i <= k; j++) {
            if (Math.abs(nums[i] - nums[j]) <= t) {
                return true
            }
        }
    }
    return false
};

228. 汇总区间

输入:nums = [0,1,2,4,5,7]
输出:["0->2","4->5","7"]
解释:区间范围是:
[0,2] --> "0->2"
[4,5] --> "4->5"
[7,7] --> "7"
var summaryRanges = function(nums) {
    const ret = [];
    let i = 0;
    const n = nums.length;
    while (i < n) {
        const low = i;
        i++;
        while (i < n && nums[i] === nums[i - 1] + 1) {
            i++;
        }
        const high = i - 1;
        const temp = ['' + nums[low]];
        if (low < high) {
            temp.push('->');
            temp.push('' + nums[high]);
        }
        ret.push(temp.join(''));
    }
    return ret;
};

986  区间列表的交集

var intervalIntersection = function(firstList, secondList) {
    let i = 0, j = 0;
    let res = [];
    while(i<firstList.length && j<secondList.length) {
        let start = Math.max(firstList[i][0], secondList[j][0]);
        let end = Math.min(firstList[i][1], secondList[j][1]);
        if (start <= end) res.push([start, end])
        if (firstList[i][1]>secondList[j][1]) {
            j++
        } else {
            i++
        }
    }
    return res
};