学习笔记——大厂真题训练与解读—微软真题
微软真题
一、最长回文子串问题
题目描述:给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例1:
输入:“babad”
输出:“bab”
注意:"aba"也是一个有效答案
示例2:
输入:“cbbd”
输出:“bb”
命题关键字:字符串、动态规划
解题思路
题干中给出“最长”,表明这是一道“求最值”的问题。基于之前的学习累积的经验以及修言大佬的总结——看到最值,就直接将动态规划拿出来。
由题可知,参数是一个字符串序列,它符合“序列型”动态规划的特征,对于序列型动态规划,我们总是需要以它的索引为线索去构造一维或二维的状态数组。对于本题来讲,由于定位任意子串需要的是两个索引,一个头部,一个尾部。因此我们的状态数组应该是一个二维数组:
// 初始化一个二维数组
let dp = []
// 给定字符串的长度
const len = s.length
for (let i = 0; i < len; i++) {
dp[i] = []
}
对于判定字符串是否为回文串:
我们直接从两边开始,i 和 j 表示子串的两个端点。因为只要确认了这两个值,我们便可得到子串的长度。
"babad"
b a b a d
i j
以以上为例:
如果 s[i] === s[j]
我们只需要判断 s[i+1] 和 s[j-1] 是否相等。
如果相等,说明该子串为回文子串
则 dp[i][j] = true
如果不相等,说明不是
则 dp[i][j] = false
如果 s[i] !== s[j],则 dp[i][j] = false
到这里,状态转移方程也就出来了,代码如下:
if(s[i] === s[j]) {
dp[i][j] = dp[i+1][j-1]
} else {
dp[i][j] = false
}
状态转移方程出来后,我们就只差边界了。
我们可以思考一下,上面说到 s[i] === s[j],则需要比较 s[i+1] 和 s[j-1], 如果一直相等,那么必定会使得 i === j 。这个时候的 s[i] 和 s[j] 代表着同一个字母,而一个单词必定是回文串,因此,dp[i][i] = true
编码实现
/**
* @param {string} s
* @return {string}
*/
var longestPalindrome = function (s) {
const dp = []
// 给定字符串的长度
const len = s.length
// 初始化二维数组
for (let i = 0; i < len; i++) {
dp[i] = []
}
// 初始化最长回文子串的两个端点值
let st = 0, end = 0
// 处理一个字母字符串
// 初始化单个字母的回文子串,也是边界
for (let i = 0; i < len; i++) {
dp[i][i] = true
}
// 处理两个字符的字符串
// 这里为了降低题目的复杂度,作者预先对悬念比较小的 s[i][i + 1] 也做了处理。将两个相连的相同的字母所对应的 dp[i][i + 1] 设为 true
for (let i = 0; i < len - 1; i++) {
if (s[i] === s[i + 1]) {
dp[i][i + 1] = true
st = i
end = i + 1
}
}
// n 代表子串的长度,从3开始递增
for (let n = 3; n <= len; n++) {
// 下面的两层循环,用来实现状态转移方程
for (let i = 0; i <= len - n; i++) {
// 字符串最后一个索引
let j = i + n - 1
if (dp[i + 1][j - 1]) {
// 若定位到更长的回文子串,则更新目标子串端点的索引值
if (s[i] === s[j]) {
dp[i][j] = true
st = i
end = j
}
}
}
}
return s.substring(st, end + 1)
};
小伙伴们可能会对最后一段代码有些疑惑
// n 代表子串的长度,从3开始递增
for (let n = 3; n <= len; n++) {
// 下面的两层循环,用来实现状态转移方程
for (let i = 0; i <= len - n; i++) {
// 当前字符串最后一个索引
let j = i + n - 1
if (dp[i + 1][j - 1]) {
// 若定位到更长的回文子串,则更新目标子串端点的索引值
if (s[i] === s[j]) {
dp[i][j] = true
st = i
end = j
}
}
}
}
我们用案例来模拟一下
"caacbabad"
c a c b a b a d
准备操作,初始化
对于单个字母 如 "c"
dp[i][i] = true
相连的两个相等的字母 如 "aa"
dp[i][i+1] = true
如果字符串s的长度不为1,也不为2
才会继续进入最后一段代码
否则直接输出答案即可,小伙伴们可以细细品味一下这句话
接下来进入正题
c a c b a b a d
len = 8
初始 n = 3 ,最终 n <= len,当前 n <= 8
i = 0,且 i <= len - n,当前 i <= 5
j = i + n - 1 ,这里 j = 2
0 1 2 3 4 5 6 7
c a c b a b a d
i j
记住这里 n = 3,以三个字符横移
开始 i 循环 i <= 5
第一次 i = 0,j = 2
c a c b a b a d
i j
i
j
因为 i+1 === j-1 => 所以 dp[i+1][j-1] = true
又因为 s[i] === s[j] => 于是 dp[i][j] = true
st = 0,end = 2
第二次 i = 1,j = 3
c a c b a b a d
i j
i
j
因为 i+1 === j-1 => 所以 dp[i+1][j-1] = true
可是 s[i] !== s[j] => 于是 dp[i][j] = false
st = 0,end = 2
第三次 i = 2,j = 4
c a c b a b a d
i j
i
j
第四次 i = 3,j = 5
c a c b a b a d
i j
i
j
第五次 i = 4,j = 6
c a c b a b a d
i j
i
j
第六次 i = 5,j = 7
c a c b a b a d
i j
i
j
我们可以发现,n = 3 的 i 循环结束后
形成了一个列表
对角线为 i = j
对角线下面第一条为 i = i+1
对角线下面第二条为 n = 3
|j\i| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 0 | 1 | | | | | | | |
| 1 | 0 | 1 | | | | | | |
| 2 | 1 | 0 | 1 | | | | | |
| 3 | | 0 | 0 | 1 | | | | |
| 4 | | | 0 | 0 | 1 | | | |
| 5 | | | | 1 | 0 | 1 | | |
| 6 | | | | | 1 | 0 | 1 | |
| 7 | | | | | | 0 | 0 | 1 |
经过全部循环后
表的左下半部分都会填满
越是靠左下部分,回文子串的长度越大
而 1 则代表着有效(true)
由 st 与 end 记录
最后返回答案 s.substring(st, end + 1)
二、从前序(先序)与中序遍历序列构造二叉树
待更…
三、复制带随即指针的链表
待更…
文章会持续更新,小伙伴们可以收藏一下
算法道路并不好走,希望各位能继续坚持。