一、字符串
“最长回文子串” 考察频率较高。
5. 最长回文子串:中心扩散法 + 临界情况
回文数就是无论正着排、反着排都相同,呈中心对称。
「中心扩散法总体思路」: 遍历每个下标,以这个下标为中心,利用 「回文串中心对称」 的特点,往两边扩散,看最多能扩散多远。
- 需要注意,回文串在长度为奇数和偶数时的 「回文中心」 不一样:长度奇数时,回文中心就是一个字符,偶数时的中心是中间的两个字符。
- 所以遍历每个下标时,会假设以该下标为回文中心的回文串分别是奇数和偶数时不同的处理情况,并在每次遍历结束后选择较长的回文串来更新 res。
const sliceString = (left, right, str) => { // 提取每个下标的回文串
while (left >= 0 && right < str.length && str[left] === str[right]) {
left--; // 循环终止时left应该加 1,right应该减 1
right++;
}
return str.slice(left + 1, right); // 注意范围 right - 1 + 1
};
const longestPalindrome = (s) => {
if (s.length < 2) return s; // 临界条件!!!
let res = ''; // 必须先赋值为空字符,而不是为空,因为要用到 length 属性
for (let i = 0; i < s.length - 1; i++) { // 考虑到偶数长度时的适用性,for 循环的长度减1
let str1 = silceString(i, i, s); // 当回文串是奇数长度
let str2 = silceString(i, i + 1, s); // 当回文串是偶数长度
res = (res.length > str1.length) ? res : str1;
res = (res.length > str2.length) ? res : str2;
}
return res;
}
时间复杂度:O(n^2)。 回文中心分别有 n、n - 1个,每个回文中心最多向外扩张O(n)次。
空间复杂度:O(1)。
补充:验证回文串
- 将字符串转小写
- 移除非字母、非数字的字符
- 双指针从两侧进行相等判断
const isPalindrome = function(s) {
// 准备工作
s = s.toLowerCase();
let str = '';
for (let i = 0; i < s.length; i++) {
if ((s[i] >= 'a' && s[i] <= 'z') || (s[i] >= '0' && s[i] <= '9') ) {
str += s[i];
}
}
let l = 0, r = str.length - 1;
while (l < r) {
if (str[l] !== str[r]) {
return false;
}
l++;
r--;
}
return true;
};
14. 最长公共前缀:sort + 比较第一个和最后一个
sort()对字符串数组排序是按 Unicode码点 来排,也就是 [ 'a', 'abc', 'b', 'reflower', 'z' ]。- 排完序后,直接比较第一个字符串和最后一个字符串,返回这两个相同的子串即可。
const longestCommonPrefix = (strs) => {
strs.sort();
let first = strs[0], last = strs[strs.length - 1];
let res = '';
for (let i = 0; i < last.length; i++) {
if (first[i] !== last[i]) break;
res = res + first[i];
}
return res;
}
时间复杂度:O(nlogn),取决于sort的复杂度,比纵向遍历的复杂度小。 尽管sort()由于不同浏览器、不同版本的JS引擎可能指定不同的排序算法,但一般来说,你可以期望对较大的数组进行排序时不会花费比O(nlogn)更长的时间,对于Timsort甚至还可以提高到O(n)。
空间复杂度:
1556. 千位分隔符:splice + i > 0
先将数字转为字符串数组,再从后往前遍历数组,每隔 3 位加点号。
注意,splice插入元素后,原位置的元素会往后移动。 也就是说,移动完成之后,splice 插入的元素位置和指定的插入位置有区别。
const thousandSeparator = (n) => {
const arr = n.toString().split(''); // 数字 -> 字符串 -> 数组
if (arr.length < 4) return n.toString();
// i > 0 可以避免开头的点号
for (let i = arr.length - 3; i > 0; i -= 3) {
arr.splice(i, 0, '.'); // splice() 会改变原数组
}
return arr.join('');
}
时间复杂度:O(n)
171. Excel表列序号:十进制码点 charCodeAt + 运算方式
不能直接用'B' -'A' + 1,得到的是NaN。
法一:从前向后遍历 + 迭代计算
const titleToNumber = (s) => {
let sum = 0;
for (let i = 0; i < s.length; i++) { // 注意
let val = s.charCodeAt(i) - 'A'.charCodeAt() + 1
sum = sum * 26 + val; // 代替了 Math.pow(26, weight),需要细品!
}
return sum;
};
法二 Math.pow():从后往前遍历 + 幂运算
const titleToNumber = (s) => {
let sum = 0;
for (let i = s.length - 1; i >= 0; i--) {
let val = s.charCodeAt(i) - 'A'.charCodeAt() + 1;
let weight = s.length - 1 - i;
sum += val * Math.pow(26, weight);
}
return sum;
};
时间复杂度:O(n)。
空间复杂度:O(1)。
二、正则表达式
151. 翻转字符串里的单词
如果挑战是常数空间,那么要用双指针(有难度,先不做)
trim():去除字符串两端的空格。split(/\s+/):split可以用 「正则表达式」 作参数,\s匹配空格,+表示某个模式出现1次或多次。也就是说, 「以1个或多个空格作为分隔符,返回数组」join():以指定参数作为分隔符,将所有数组成员连接作为一个字符串返回。
const reverseWords = (s) => {
return s.trim().split(/\s+/).reverse().join(' ');
}
时间、空间复杂度均为:O(n)
468. 验证IP地址
- 检查ipv4的合法性
- 字符串数组应该等于4
- 字符串长度应该在 1 ~ 3 之间
- 字符串如果不是0,不能以 0 开头
- 不能含有数字以外的字符
- 对应的数字不能超过 255
- 检查ipv6的合法性
- 数组长度应该等于8
- 字符串长度应该在 1 ~ 4 之间
- 不能含有非法字符
const validIPAddress = function(ip) {
const arr4 = ip.split('.');
const arr6 = ip.split(':');
if (arr4.length == 4) { // 判断IPv4
// 中间不要有空格
if (arr4.every(s => (s.match(/^0$|^([1-9]\d{0,2})$/) && s < 256) ))
return 'IPv4';
} else if (arr6.length == 8) { // 判断IPv6
if (arr6.every(s => s.match(/^[0-9a-fA-F]{1,4}$/)))
return 'IPv6';
}
return 'Neither';
}
时间复杂度:O(1)。
空间复杂度:O(1)。