leetcode专题训练笔记

一、 动态规划专题

①.记忆化递归“搜索”(自顶向下):用一个数组存放(递归子树的)中间结果,并且在下次递归前判断该结果是否计算过,若计算过直接返回,否则就递归,从而实现剪枝的效果,即空间换时间。

②动态规划法(自底向上):找规律,求子问题的解从而得到最终解,写动态转移方程。(找不到动态转移方程就先尝试记忆化弄清问题结构,再反推自底向上)。

     动态规划 “三板斧”

  1. 分治,找到最优子结构 opt[n]=best_of(opt[n-1], opt[n-2], ...)

  2. 状态定义,i 条件时的状态 f[i]

  3. DP方程,也就是递推公式,例如一维的斐波那契递推公式 dp[i] = dp[i-1] + dp[i-2] ; 二维递推公式例如 dp[i][j] = max(dp[i-1][j], dp[i][j-1]),高级的DP公式可能会达到三维甚至三维以上。

     推理方法:

        对于一维情况,求 dp[i] 可假设 dp[i-1] 已经求出,需要如何判断才能求出下一项;

        对于二维情况,利用递推关系用“填表格”的方式顺序计算,每个 dp 项的值其实等于一个递归子调用的结果,即递归子问题的解。

1.爬楼梯  

思路: 同斐波拉契数列 

练习:120、64

// 递归树里面有重叠子结构  用记忆数组剪枝
var climbStairs = function(n) {
    const memo = Array(n+1).fill(-1)
    if(n==1) return 1
    if(n==2) return 2
    
    if(memo[n]==-1)
        memo[n] = climbStairs(n-1) + climbStairs(n-2)
    return memo[n]
};

// 有递归子问题 便可动态规划
// 时间、空间复杂度均为O(n)
var climbStairs = function(n) {
    const dp = []
    dp[1]=1
    dp[2]=2
    for(let i=3; i<=n; i++)
        dp[i]=dp[i-1]+dp[i-2]
    return dp[n]
};

// 结果只与前俩次有关可继续优化空间 滑动数组 
// 时间复杂度为O(n),时间复杂度为 O(1) 
var climbStairs = function(n) {   
    if (n <= 1) return n;   
    const dp =[]
    dp[1] = 1
    dp[2] = 2
    for (let i = 3; i <= n; i++) {
        let sum = dp[1] + dp[2]
        dp[1] = dp[2]
        dp[2] = sum
    }
    return dp[2]
}

2.整数拆分    

思路 :遍历分数,分为俩数还是多个数。        

练习:279、91、63

// 递归树里面有重叠子结构  存在重复 用记忆数组
var integerBreak = function(n) {
    const memo = Array(n+1).fill(-1)
    if(n == 2) return 1
    if(memo[n]!=-1)
        return memo[n]
     let res = 1
    // 将数字从1开始分 1*(n-1)...i*(n-i)
     for(let i=1;i<=n-1;i++)
        //分割为俩个数i*(n-i) 或分割为多个数
        res = Math.max(res,i*(n-i),i*integerBreak(n-i))
    memo[n]=res
    return res
};

//转为动态规划
// 时间复杂度:O(n^2) 空间复杂度:O(n)
var integerBreak = function(n) {
    const memo =Array(n+1).fill(-1)
    memo[2]=1
     //memo[i]:数字i分割(至少俩部分)后的最大成绩
    for(let i=3;i<=n;i++)
        //分割为 俩个数相乘 j*(i-j) 
        for(let j=1;j<i;j++)
        //或分割为多个数:则将i-j继续分割即memo[i-j](显然memo[i-j]已经算过)
            memo[i]=Math.max(memo[i], j*(i-j), j*memo[i-j])
    return memo[n]
};

 3.打家劫舍   

思路:不偷当前第i家,则结果为后面 i+1 家的结果;否则结果为nums[i] + 后 i+2家的价值

练习:213、337、309

// 记忆化搜索
var rob = function (nums) {
  if (nums == null || !nums.length) return 0
  const memo =Array(nums.length).fill(-1)

  // 考虑抢劫nums[i...length)范围的所有房子
  const tryRob = (nums, i) => {
    if (i >= nums.length)  return 0;
    if(memo[i]!=-1) return memo[i]
    let res = Math.max(
        //不偷第i间,结果为从i+1间开始
        //偷第i间,结果为第i间价值+从i+2间开始
       tryRob(nums, i + 1),
       tryRob(nums, i + 2) + nums[i])
    memo[i] = res
    return res
  };

  return  tryRob(nums, 0)
};

// 优化dp,空间复杂度O(n) 
var rob = function(nums) {
    const len = nums.length;
    if(len == 0) return 0
    const dp = new Array(len)
    dp[0] = 0
    dp[1] = nums[0]
// dp[i]:考虑前i间房子的价值 注意下标nums:[0...len-1] dp:[1...len]
    for(let i = 2; i <= len; i++)        //+nums[i-1]即偷第i间
        dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i-1] ,)
    return dp[len]
};

//时间复杂度:O(n)、空间复杂度O(1) 
var rob = function(nums) {
    if(!nums.length) { return 0; }
    let dp0 = 0
    let dp1 = nums[0]
    for(let i = 2; i <= nums.length; i++) {
        const dp2 = Math.max(dp1, dp0 + nums[i-1] )
        dp0 = dp1
        dp1 = dp2
    }
    return dp1
};

4.  01背包问题

思路:memo[i][j] 表示容量为 j 时,0-i 号物品能获得最大价值;对于第 i 号物品,当前容量若放不下则结果为之前的价值,若放得下则考虑放与不放的价值谁大(放该物品时的价值=不放时容量的价值+该物品的价值)

练习:完全背包问题

     // 时间空间复杂度:n*capacity
     const knapsack01 = (weight, value, capacity) => {
            const n = weight.length
            if (n == 0) return 0
            const memo = new Array(n).fill(Array(capacity + 1).fill(-1))

            // 初始化第一行(放第一个物品)
            for (let k = 0; k <= capacity; k++)
                memo[0][k] = (k >= weight[0] ? weight[0] : 0)

            for (let i = 1; i < n; i++)
                for (let j = 0; j <= capacity; j++) {
                    if (j < weight[i])
                        memo[i][j] = memo[i - 1][j]
                    else
                        memo[i][j] = Math.max(memo[i-1][j], value[i] + memo[i - 1][j - weight[i]])
                }
            return memo[n - 1][capacity]
        }
        console.log(knapsack01([1, 2, 3], [6, 10, 12], 5)) //22

// 实际上下一行的数据 只依靠上一行,优化后 空间复杂度为O(n)
        const knapsack01 = (weight, value, capacity) => {
            const n = weight.length
            if (n == 0) return 0
            const memo = new Array(2).fill(Array(capacity + 1).fill(-1))

            // 初始化第一行(放第一个物品)
            for (let k = 0; k <= capacity; k++)
                memo[0][k] = (k >= weight[0] ? weight[0] : 0)

            for (let i = 1; i < n; i++)
                for (let j = 0; j <= capacity; j++) {
                    if (j < weight[i])
                        memo[i % 2][j] = memo[(i - 1) % 2][j]
                    else
                        memo[i % 2][j] = Math.max(memo[(i - 1) % 2][j], value[i] + memo[(i - 1) % 2][j - weight[i]])
                }
            return memo[(n - 1) % 2][capacity]
        }
        console.log(knapsack01([1, 2, 3], [6, 10, 12], 5)) //22

//继续优化
        const knapsack01 = (weight, value, capacity) => {
            const n = weight.length
            if (n == 0) return 0
            const memo = new Array(capacity + 1).fill(-1)


            for (let k = 0; k <= capacity; k++)
                memo[k] = (k >= weight[0] ? weight[0] : 0)

            for (let i = 1; i < n; i++)
                for (let j = capacity; j >= weight[i]; j--) {
                    memo[j] = Math.max(memo[j - 1], value[i] + memo[j - weight[i]])
                }
            return memo[capacity]
        }

5.分割等和子集

思路:类似背包问题,若能分割,则说明数组里的部分和等于sum的一半,即在数组里找到n个数填满容量为sum/2的背包。对于第 i 个数,放或不放只要能填满背包即可(若放,则需要能放得下)

练习:377、474、139、494

        var canPartition = function(nums) {
            let sum = nums.reduce((pre, cur) => pre + cur);
            // 和为奇数时,不可能划分成两个和相等的集合
            if (sum % 2 != 0) return false
            let n = nums.length,
                c = sum / 2
            let dp = new Array(n + 1)
                .fill(false)
                .map(() => new Array(c + 1).fill(false));
            // dp[i][j] 表示前i个物品 能不能放满容量为j的背包
            // base case 背包空间c=0时候,就相当于装满了
            for (let i = 0; i <= n; i++) 
                dp[i][0] = true;
           
            for (let i = 1; i <= n; i++) {
                for (let j = 1; j <= c; j++) {
                    if (j < nums[i - 1]) {
                        // 背包容量不足,不能装入第 i 个物品
                        dp[i][j] = dp[i - 1][j];
                    } else {
                        // 不装入或装入背包
                        dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]];
                    }
                }
            }
            return dp[n][c];
        };
// 优化
        var canPartition = function(nums) {
            let sum = 0
            for (let n of nums)
                sum += n
            if (sum % 2) return false
            let n = nums.length,
                c = sum / 2
            const memo = Array(c + 1).fill(false)
            for(let i =0;i<=c;i++) // 看第一个数能不能放入被入背包
                memo[i] = (nums[0]==i)

            for (let i = 1; i <= n; i++) // 看后面物品中 能不能放满容量为c的背包
                for (let j = c; j >= nums[i]; j--)
                    // 第i个物品不放 或放
                    memo[j] = memo[j] || memo[j - nums[i]] // memo[j] |= memo[j-nums[i]]
            return memo[c]
        };

6. 最长递增子序列

思路:dp[i] = max(dp[i], dp[j] + 1) for j in [0, i) ,num[ j ]<num[ i ] ,dp[ i ] 表示[ 0...i ]内选择nums[ i ] 可以获得的最长上升子序列的长度。

练习: 376


 
        var lengthOfLIS = function(nums) {
            const dp = new Array(nums.length).fill(1);
            for (let i = 0; i < nums.length; i++) {
                for (let j = 0; j < i; j++) 
                    if (nums[i] > nums[j]) 
                        dp[i] = Math.max(dp[i], dp[j] + 1);
            }
            return Math.max(...dp);
        };

7.最长公共子序列

思路:

a. 分治 LCS[i] = LCS(最后一个字母相同)+ LCS(最后一个字母不相同)

b. 状态定义 f[ i ][ j ] 表示第一个字符串索引 0-i 构成的子串与第二个字符串索引 0-j 子串的最长公共序列 

c. DP方程:

if text1[i-1] == text2[i-1]:
        dp[i][j] = dp[i-1][j-1] + 1
    else:
        dp[i][j] = max(dp[i-1][j], dp[i][j-1])

时间、空间复杂度:O(mn) 

var longestCommonSubsequence = function(text1, text2) {
    let dp = Array.from(Array(text1.length+1),
         () => Array(text2.length+1).fill(0));

    for(let i = 1; i <= text1.length; i++) {
        for(let j = 1; j <= text2.length; j++) {
            if(text1[i-1] === text2[j-1]) {
                dp[i][j] = dp[i-1][j-1] +1;;
            } else {
                dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1])
            }
        }
    }

    return dp[text1.length][text2.length];
};

8. 零钱兑换

思路: dp[j] = min(dp[j - coins[i]] + 1, dp[j]) , dp[j]:凑足总额为 j 所需最少硬币个数为dp[j],对于第 i 个硬币拿与不拿,看最后总数谁少。

        var coinChange = function(coins, amount) {
            let dp = new Array(amount + 1).fill(Infinity);
            dp[0] = 0;
            for (let i = 1; i <= amount; i++)
                for (let coin of coins)
                    if (i >= coin )
                        dp[i] = Math.min(dp[i], dp[i - coin] + 1);
            return dp[amount] === Infinity ? -1 : dp[amount];
        }

9.不同路径

思路:要到达终点[m-1][n-1],有俩种方式从左边来即[i-1][j]或从上边来即[i][j-1]。由此可得动态转移方程:dp[i][j] = dp[i-1][j] + dp[i][j-1] (dp[i][j] 表示从起点到任意位置的路径数,d[i][0]为1理由是从[0, 0]的位置到[i, 0]的路径只有一条,同理d[0][j])

时间、空间复杂度:O(m * n)

a. 分治(子问题) path = path(top) + path(left)

b. 状态定义 f[i, j] 表示第i行第j列的不同路径数

c. DP方程 dp[i][j] = dp[i-1][j] + dp[i][j-1]

// dp[i][j]为每行每列任意点的不同路径数
var uniquePaths = function(m, n) {
    const dp=new Array(m).fill(1).map(()=>Array(n).fill(1))
    for(i=1;i<m;i++)
        for(j=1;j<n;j++)
            dp[i][j]=dp[i-1][j]+dp[i][j-1]
    return dp[m-1][n-1]
};

//优化空间(O(n)):只需要保存每一行任意点的路径数即可,DP方程可以更新为 dp[j] = dp[j] + dp[j-1]`
var uniquePaths = function(m, n) {
    const dp=new Array(n).fill(1) 
    for(i=1;i<m;i++)
        for(j=1;j<n;j++)
            dp[j] = dp[j] + dp[j-1]
    return dp[n-1]
};

附加、22年各厂面试题整理

tx 1:  移掉 K 位数字

 思路:贪心,遍历字符串维护一个栈,如果当前数字小于前一个,则移除前一个元素同时k--,并加入当前元素;若当前元素为0且栈为空则不入栈。

 时间、空间复杂度:O(n)

var removeKdigits = function(num, k) {
    const stk = []
    for (const digit of num) {
        while (k && stk.length && stk[stk.length-1] > digit) {
            stk.pop()
            k --
        }
        if (digit == '0' && stk.length == 0) //如果当前元素为0,且栈为空,则跳过
            continue
        stk.push(digit)
    }
     while (k-- > 0) stk.pop()
    return stk.length == 0 ? "0" : stk.join('')
}

2. 复杂数组去重

        function removeDuplicate(arr) {
            const newArr = []
            const obj = {}

            arr.forEach(item => {
                if (!obj[item]) {
                    newArr.push(item)
                    obj[item] = true
                }
            })

            return newArr
        }
        const arr = ['12', 'webank', '12', [1, 23], { a: 1 }, 23, { a: 1 }]
        const result = removeDuplicate(arr)
        console.log(result) 

        // 0: "12"
        // 1: "webank"
        // 2: (2)[1, 23]
        // 3: { a: 1 }
        // 4: 23

3. 一维数组转为树状结构

       const arrayToTree = (arr) => {
            if (!Array.isArray(arr) || arr.length < 1) return null;
            const [root] = arr.filter(item => item.id === 0);
            const addChildren = (node, dataList) => {
                const children = dataList
                    .filter(item => item.id === node.id)
                    .map(item => addChildren(item, dataList));
                return { ...node, children };
            };
            return addChildren(root, arr);
        };
        const list = [{ id: 1, parentId: null, name: 'wuhan' }, { id: 2, parentId: 2, name: 'changsha' }]
        console.log(arrayToTree(list))

4. 求俩个日期中间的有效日期

        function RealDate(start, end) {
            const dayTimes = 24 * 60 * 60 * 1000; // 换算成毫秒级别
            const range = end.getTime() - start.getTime();
            let total = 0;
            res = [];
            while (total <= range && range > 0) {
                res.push(new Date(start.getTime() + total).toLocaleDateString().replace(/\//g, '-'))
                total += dayTimes
            }
            return res;
        }

        var start = "2020-09-29"
        var end = "2020-10-04"
        //console.log(new Date(start).getTime())
        var arr1 = RealDate(new Date(start), new Date(end))
        console.log(arr1)
        // 0: "2020-9-29"
        // 1: "2020-9-30"
        // 2: "2020-10-1"
        // 3: "2020-10-2"
        // 4: "2020-10-3"
        // 5: "2020-10-4"

5.两个数组的交集 II

// 借用map 哈希映射
const intersect = (nums1, nums2) => {
    const map = {};
    const res = [];
    if (nums1.length < nums2.length) {
        [nums1, nums2] = [nums2, nums1]
    }
    for (const num1 of nums1) {//nums1中各个元素的频次
        if (map[num1]) {
            map[num1]++;
        } else {
            map[num1] = 1;
        }
    }
    for (const num2 of nums2) { //遍历nums2
        const val = map[num2];
        if (val > 0) {            //在nums1中
            res.push(num2);         //加入res数组
            map[num2]--;            //匹配掉一个,就减一个
        }
    }
    return res;
};

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小白目

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值