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 ) 对于刚接触的这题目的比较不好理解。