(立下flag)每日10道前端面试题-21 关于【动态规划】算法题

本文主要介绍了动态规划的概念,并通过六个经典的面试题:最长回文子串、最小路径和、爬楼梯、买卖股票的最佳时机、不同的二叉搜索树、最大子序和,详细讲解了动态规划的解题思路和实现方法。每道题目都包含了暴力枚举、动态规划和特殊优化等解法,帮助读者深入理解动态规划的应用。
摘要由CSDN通过智能技术生成

1. 最长回文子串

给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。

示例 1:

输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。
示例 2:

输入: "cbbd"
输出: "bb"

解法一:暴力枚举法

时间复杂度O(n^3)

空间复杂度O(1)

思路 很简单:一个函数isPalindrome来判断当前字符串是不是回文串。

然后在两重循环里调用 这个函数。

function isPalindrome(str) {
    var len  = str.length
    var middle = parseInt(len/2)
    for(var i = 0;i<middle;i++){
        if(str[i]!=str[len-i-1]){
            return false
        }
    }
    return true
}
var ans = '';
var max = 0;
var len = s.length
for(var i = 0;i<len;i++){
    for(var r = i+1;r<=len;r++){
        var tmpStr = s.substring(i,r)
        if(isPalindrome(tmpStr) && tmpStr.length > max){
            ans = s.substring(i,r)
            max = tmpStr.length;
        }
    }
}
return ans;

解法二:动态规划

  • 状态定义

    • dp[i,j]:字符串s从索引i到j的子串是否是回文串

    • true:s[i,j] 是回文串

    • false:s[i,j] 不是回文串

  • 转移方程

    • dp [i] [j] = s [i] == s [j] &&(dp [i + 1] [j-1] || j-i <2)

    • j - i < 2:意即子串是一个长度为0或1的回文串

    • dp [i] [j] = dp [i + 1] [j-1] && s [i] == s [j]

    • s[i] == s[j]:说明当前中心可以继续扩张,进而有可能扩大回文串的长度

    • dp [i + 1] [j-1]:是

    • 说明s[i,j]的**子串s[i+1][j-1]**也是回文串

说明,i是从最大值开始遍历的,j是从最小值开始遍历的 特殊情况 总结

/**
 * @param {string} s
 * @return {string}
 */
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之间
所以共有两种组合可能
左:i,右:i
左:i,右: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);
};

2. 最小路径和

/**
 * @param {number[][]} grid
 * @return {number}
 */
var minPathSum = function(grid) {
    var n = grid.length;
    var m = grid[0].length;
    var dp = Array.from(new Array(n),() => new Array(m));
    for(var i = 0;i < n;i++){
        for(var j = 0;j < m;j++){
            if( i != 0 && j!= 0){
                dp[i][j] = Math.min(dp[i-1][j],dp[i][j-1])+grid[i][j];
            }else if(i == 0 && j!=0){
                dp[i][j] = dp[i][j-1]+grid[i][j];
            }else if(i != 0 && j==0){
                dp[i][j] = dp[i-1][j]+grid[i][j];
            }else if(i == 0 && j==0){
                dp[i][j] = grid[i][j];
            }
        }
    }
    return dp[n-1][m-1];
};

空间复杂度优化版+2

时间复杂度:O(M*N)

空间复杂度:O(1)

直接修改原数组即可

/**
 * @param {number[][]} grid
 * @return {number}
 */
var minPathSum = function(grid) {
    for(var i = 0;i < grid.length;i++){
        for(var j = 0;j < grid[0].length;j++){
            if( i != 0 && j!= 0){
                grid[i][j] = Math.min(grid[i-1][j],grid[i][j-1])+grid[i][j];
            }else if(i == 0 && j!=0){
                grid[i][j] = grid[i][j-1]+grid[i][j];
            }else if(i != 0 && j==0){
                grid[i][j] = grid[i-1][j]+grid[i][j];
            }else if(i == 0 && j==0){
                continue;
            }
        }
    }
    return grid[grid.length-1][grid[0].length-1];
};

3. 爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。

示例 1:

输入:2
输出:2
解释:有两种方法可以爬到楼顶。

1 阶 + 1 阶

2 阶

示例 2:

输入:3
输出:3
解释:有三种方法可以爬到楼顶。

1 阶 + 1 阶 + 1 阶

1 阶 + 2 阶

2 阶 + 1 阶

解法:动态规划

本问题其实常规解法可以分成多个子问题,爬第n阶楼梯的方法数量,等于 2 部分之和

爬上 n-1阶楼梯的方法数量。因为再爬1阶就能到第n阶
爬上 n-2阶楼梯的方法数量,因为再爬2阶就能到第n阶
所以我们得到公式 dp[n] = dp[n-1] + dp[n-2]
同时需要初始化 dp[0]=1和 dp[1]=1
时间复杂度:O(n)

4. 买卖股票的最佳时机

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。

注意:你不能在买入股票前卖出股票。

示例 1:

输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
     注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2:

输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0

贪心算法

解题思路

使用贪心算法, 遍历prices每日价格, 保存价格最低的那个, 每天比较和最低价的差值, 保存起来, 如果明天差值更大, 覆盖掉旧差值, 最后拿到的就是最高的盈利. 简单来说, 就是捡麦子, 记住最小麦子的重量,和现在捡到的麦子比较, 相差得最大的, 就是最后的结果

function maxProfit(prices) {
  let min = prices[0];
  let max = 0;
  for (let i in prices) {
    i = Number(i)
    if (i === 0) continue;
    const price = prices[i]
    if (price - min > max) max = price - min
    if (price < min) min = price
  }
  return max
}

5.不同的二叉搜索树

给定一个整数 n,求以 1 ... n 为节点组成的二叉搜索树有多少种?

示例:

输入: 3
输出: 5
解释:
给定 n = 3, 一共有 5 种不同结构的二叉搜索树:

   1         3     3      2      1
    \       /     /      / \      \
     3     2     1      1   3      2
    /     /       \                 \
   2     1         2                 3

动态规划解法

每一个树的构成都可以分为左子树和右子树,所以以i为根节点的二叉搜索树种类的个数可以分解为子问题:左子树的种类 乘 右子树的种类。

左子树的节点范围为:1...i-1。右子树的节点范围为:i+1...n。

那么就可以把同样的问题转接到左子树和有字数上。

时间复杂度:O(n²)

空间复杂度:O(n)

/**
 * @param {number} n
 * @return {number}
 */
var numTrees = function(n) {
    // res[i]表示以1...i为节点组成的二叉搜索树的种类
    let res = new Array(n+1).fill(0);
    res[0] = 1;
    res[1] = 1;
    
    // res[1]已经确定,因此从2开始
    for (let i = 2; i <= n; i++) {
        // j表示分别从1为根节点至i为根节点
        for (let j = 1; j <= i; j++) {
            res[i] += res[j-1] * res[i-j];
        }
    }
    return res[n];
};

6.最大子序和

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

示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
进阶:
如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。

解题思路:

示例: [a, b , c, d , e]

解答这类题目, 省略不掉遍历, 因此我们先从遍历方式说起

通常我们遍历子串或者子序列有三种遍历方式

以某个节点为开头的所有子序列: 如 [a],[a, b],[ a, b, c] ... 再从以 b 为开头的子序列开始遍历 [b] [b, c]。

根据子序列的长度为标杆,如先遍历出子序列长度为 1 的子序列,在遍历出长度为 2 的 等等。以子序列的结束节点为基准,先遍历出以某个节点为结束的所有子序列,因为每个节点都可能会是子序列的结束节点,因此要遍历下整个序列,如: 以 b 为结束点的所有子序列: [a , b] [b] 以 c 为结束点的所有子序列: [a, b, c] [b, c] [ c ]。

第一种遍历方式通常用于暴力解法, 第二种遍历方式 leetcode (5. 最长回文子串 ) 中的解法就用到了。

第三种遍历方式 因为可以产生递推关系, 采用动态规划时, 经常通过此种遍历方式, 如 背包问题, 最大公共子串 , 这里的动态规划解法也是以 先遍历出 以某个节点为结束节点的所有子序列 的思路

对于刚接触动态规划的, 我感觉熟悉第三种遍历方式是需要抓住的核心

因为我们通常的惯性思维是以子序列的开头为基准,先遍历出以 a 为开头的所有子序列,再遍历出以 b 为开头的...但是动态规划为了找到不同子序列之间的递推关系,恰恰是以子序列的结束点为基准的,这点开阔了我们的思路。

我在网上看不少解答时,直接阅读其代码,总是感觉很理解很吃力,因为好多没有写清楚,一些遍历到底代表什么意思,看了许久仍不知所以然,下面的代码中摘录了 维基中的解释,感觉比较清楚,供大家理解参考。

代码:

第二块代码和 第一块代码 思路实现是完全一样的,但是如果第一次看到这类题目,直接阅读 第二块代码,理解起来很难,尤其是 如果改成 if (sum > 0 ) 对于刚接触的这题目的比较不好理解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值