字符串
字符串是最常用的一种数据类型,也是面试中经常必会问题的类型之一。
242. 有效的字母异位词
题目描述
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
找到所有在 [1, n] 范围之间没有出现在数组中的数字。
例子1
Input: s = "anagram", t = "nagaram"
output: true
例子2
Input: s = "rat", t = "car"
output: false
提示:
你可以假设字符串只包含小写字母。
进阶:
如果输入字符串包含 unicode 字符怎么办?你能否调整你的解法来应对这种情况?
思考
1 直接使用一个数组记录字符出现在s中的次数,然后遍历t每个字符相减就可以了。
至于如果存在unicode字符,可以先把unicode转换成字符就可以了
参考实现1
实现1
/**
* @param {string} s
* @param {string} t
* @return {boolean}
*/
// Runtime: 88 ms, faster than 90.79% of JavaScript online submissions for Valid Anagram.
// Memory Usage: 39.8 MB, less than 93.22% of JavaScript online submissions for Valid Anagram.
export default (s, t) => {
const arr = new Array(26).fill(0);
for (let i = 0; i < s.length; i++) {
arr[s.charCodeAt(i) - 97]++;
}
for (let i = 0; i < t.length; i++) {
if (arr[t.charCodeAt(i) - 97] >= 1) {
arr[t.charCodeAt(i) - 97]--;
} else {
return false;
}
}
return arr.reduce((a, b) => a + b) === 0;
};
205. 同构字符串
题目描述
给定两个字符串 s 和 t,判断它们是否是同构的。
如果 s 中的字符可以按某种映射关系替换得到 t ,那么这两个字符串是同构的。
每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。
例子1
Input: s = "egg", t = "add"
output: true
例子2
Input: s = "foo", t = "bar"
output: false
例子3
Input: s = "paper", t = "title"
output: true
提示:
假设s的长度和t的长度相同
思考
1 这里可以直接转换一下,依次遍历字符串,如果s和t中相同位置的字符在前面出现的位置是一样的,那字符串s和字符串t就是同构的。
参考实现1
实现1
/**
* @param {string} s
* @param {string} t
* @return {boolean}
*/
// Runtime: 88 ms, faster than 73.09% of JavaScript online submissions for Isomorphic Strings.
// Memory Usage: 39.8 MB, less than 69.51% of JavaScript online submissions for Isomorphic Strings.
export default (s, t) => {
const s_first_index = new Map();
const t_first_index = new Map();
for (let i = 0; i < s.length; i++) {
if (s_first_index.get(s.charAt(i)) !== t_first_index.get(t.charAt(i))) {
return false;
}
s_first_index.set(s.charAt(i), i + 1);
t_first_index.set(t.charAt(i), i + 1);
}
return true;
};
647. 回文子串
题目描述
给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
例子1
Input: "abc"
output: 3
解释:三个回文子串: "a", "b", "c"
例子2
Input: "aaa"
output: 6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"
提示:
输入的字符串长度不会超过 1000 。
思考
1 这里很明显可以使用暴力求解,分别求出不同长度的回文字符串长度,从1到s.length
参考实现1
2 发现时间复杂度太高了,想使用空间换时间,但是发现好像时间更高
参考实现2
3 后来发现回文字符串的特点,可以使用从中间从两边扩散的方法,但是刚开始使用这种方法的时候,忽略了偶数情况下向外扩散的情况,只是考虑到了奇数扩散的情况。
所以当使用这种从中间向外扩散的方法的时候,必须同时考略使用奇数和偶数两种情况向外扩散
参考实现3
4 方法3还可以写的比较简洁一些
参考实现4
实现1
/**
* @param {string} s
* @return {number}
*/
// Runtime: 312 ms, faster than 27.56% of JavaScript online submissions for Palindromic Substrings.
// Memory Usage: 37.6 MB, less than 100.00% of JavaScript online submissions for Palindromic Substrings.
export default (s) => {
let count = s.length;
for (let i = 2; i <= s.length; i++) {
for (let j = 0; j < s.length - i + 1; j++) {
let k = j + i - 1;
let m = j;
while (m < k) {
if (s.charAt(m) === s.charAt(k)) {
m++;
k--;
} else {
break;
}
}
if (m >= k) {
count++;
}
}
}
return count;
};
实现2
/**
* @param {string} s
* @return {number}
*/
// Runtime: 600 ms, faster than 16.13% of JavaScript online submissions for Palindromic Substrings.
// Memory Usage: 79.4 MB, less than 5.04% of JavaScript online submissions for Palindromic Substrings.
export default (s) => {
let count = s.length;
const map = new Map();
for (let i = 0; i < s.length; i++) {
map.set(`${i}_${i}`, 1);
}
for (let i = 2; i <= s.length; i++) {
for (let j = 0; j < s.length - i + 1; j++) {
let k = j + i - 1;
let m = j;
const begin = j;
const end = k;
if (begin >= 1 && end >= 1 && map.get(`${begin - 1}_${end - 1}`) === 1 && s[begin] === s[end]) {
count++;
map.set(`${begin}_${end}`, 1);
} else {
while (m < k) {
if (s.charAt(m) === s.charAt(k)) {
m++;
k--;
} else {
break;
}
}
if (m >= k) {
count++;
map.set(`${begin}${end}`, 1);
}
}
}
}
return count;
};
实现3
/**
* @param {string} s
* @return {number}
*/
// Runtime: 100 ms, faster than 58.99% of JavaScript online submissions for Palindromic Substrings.
// Memory Usage: 40.3 MB, less than 48.07% of JavaScript online submissions for Palindromic Substrings.
export default (s) => {
let count = 0;
for (let i = 0; i < s.length; i++) {
let begin = i;
let end = i;
while (begin >= 0 && end <= s.length && s.charAt(begin) === s.charAt(end)) {
count++;
begin--;
end++;
}
begin = i;
end = i + 1;
while (begin >= 0 && end <= s.length && s.charAt(begin) === s.charAt(end)) {
count++;
begin--;
end++;
}
}
return count;
};
实现4
/**
* @param {string} s
* @return {number}
*/
// Runtime: 80 ms, faster than 95.97% of JavaScript online submissions for Palindromic Substrings.
// Memory Usage: 39.3 MB, less than 80.17% of JavaScript online submissions for Palindromic Substrings.
// "aaaaa"
// 0 2 00 01
// 1 6 11 02 12 03
// 2 11 22 13 04 23 14
// 3 14 33 24 34
// 4 15 44 45
const extendSubstrings = (s, begin, end) => {
let count = 0;
while (begin >= 0 && end < s.length && s[begin] === s[end]) {
--begin;
++end;
++count;
}
return count;
};
export default (s) => {
let count = 0;
for (let i = 0; i < s.length; i++) {
count += extendSubstrings(s, i, i); // 奇数长度
count += extendSubstrings(s, i, i + 1); // 偶数长度
}
return count;
};
n 时间复杂度O(n * n),空间复杂度O(1)
696. 计数二进制子串
题目描述
给定一个字符串 s,计算具有相同数量0和1的非空(连续)子字符串的数量,并且这些子字符串中的所有0和所有1都是组合在一起的。
重复出现的子串要计算它们出现的次数。
例子1
Input: "00110011"
output: 6
解释:有6个子串具有相同数量的连续1和0:“0011”,“01”,“1100”,“10”,“0011” 和 “01”。
请注意,一些重复出现的子串要计算它们出现的次数。
另外,“00110011”不是有效的子串,因为所有的0(和1)没有组合在一起。
例子2
Input: "10101"
output: 4
解释:有4个子串:“10”,“01”,“10”,“01”,它们具有相同数量的连续1和0。
提示:
1 s.length 在1到50,000之间。
2 s 只包含“0”或“1”字符。
思考
1 这里很明显可以使用暴力求解,但是超时了
参考实现1
2 通过观察可以发现,当前面相同的字符数量大于等于后面的字符数量的时刻,那肯定存在一个子字符串符合要求,所以可以利用此思想遍历一遍就可以了
参考实现2
实现1
/**
* @param {string} s
* @return {number}
*/
export default (s) => {
let count = 0;
for (let i = 0; i < s.length; i++) {
let count0 = 0;
let j = i;
let flag = s.charAt(j);
while (s.charAt(j) === flag) {
j++;
count0++;
}
// console.log(i,count0)
flag = flag === "0" ? "1" : "0";
while (j + count0 < s.length && s.charAt(j) === flag) {
j++;
count0--;
if (count0 === 0) {
count++;
break;
}
}
}
return count;
};
实现2
/**
* @param {string} s
* @return {number}
*/
// Runtime: 84 ms, faster than 92.50% of JavaScript online submissions for Count Binary Substrings.
// Memory Usage: 42.2 MB, less than 63.75% of JavaScript online submissions for Count Binary Substrings.
export default (s) => {
let pre = 0;
let cur = 1;
let count = 0;
for (let i = 1; i < s.length; ++i) {
if (s[i] === s[i - 1]) {
++cur;
} else {
pre = cur;
cur = 1;
}
if (pre >= cur) {
++count;
}
}
return count;
};
时间复杂度O(n),空间复杂度O(1)
227. 基本计算器 II
题目描述
实现一个基本的计算器来计算一个简单的字符串表达式的值。
字符串表达式仅包含非负整数,+, - ,*, / 四种运算符和空格 。 整数除法仅保留整数部分。
例子1
Input: "3+2*2"
output: 7
解释:
例子2
Input: " 3/2 "
output: 1
解释:
例子3
Input: " 3+5 / 2 "
output: 5
解释:
提示:
1 你可以假设所给定的表达式都是有效的。
2 请不要使用内置的库函数 eval。
思考
1 这里如果不是使用一些小技巧,其实还是非常复杂的
有几个问题需要考虑如何处理
1.1 如何处理输入字符串中的空格?
1.2 如何处理输入字符串中的连续的数字,比如输入"234/3+2"的时候,应该如何得到234?
1.3 如何处理输入的顺序,题解中是在输入字符串中前面添加了一个+号,这样输入字符串“23+1”就变成了“+23+1”,可以想下这样有什么好处,如果不这样搞,应该如何处理?
参考实现1
实现1
/**
* @param {string} si
* @return {number}
*/
// Runtime: 100 ms, faster than 78.72% of JavaScript online submissions for Basic Calculator II.
// Memory Usage: 46.9 MB, less than 32.83% of JavaScript online submissions for Basic Calculator II.
export default (s) => {
s = s.replace(/\s+/g, "");
let len = s.length;
if (!s || len === 0) return 0;
const stack = [];
let num = 0;
let opr = "+";
for (let i = 0; i < len; i++) {
if (/^[0-9]+.?[0-9]*$/.test(s.charAt(i))) {
num = num * 10 + s.charCodeAt(i) - 48;
}
if (!/^[0-9]+.?[0-9]*$/.test(s.charAt(i)) || i === len - 1) {
if (opr === "-") {
stack.push(-num);
}
if (opr === "+") {
stack.push(num);
}
if (opr === "*") {
stack.push(stack.pop() * num);
}
if (opr === "/") {
const tempNum = stack.pop();
stack.push(tempNum > 0 ? Math.floor(tempNum / num) : Math.ceil(tempNum / num));
}
opr = s.charAt(i);
num = 0;
}
}
// let res = 0;
// console.log(stack);
return stack.reduce((a, b) => a + b);
};
时间复杂度O(n),空间复杂度O(1)
227. 基本计算器 III
题目描述
实现一个基本的计算器来计算一个简单的字符串表达式的值。
字符串表达式仅包含非负整数,+, - ,*, / , ( , ) 等各种运算符和空格 。 整数除法仅保留整数部分。
例子1
Input: " 6-4 / 2 "
output: 4
解释:
例子2
Input: "2*(5+5*2)/3+(6/2+8)"
output: 21
解释:
例子3
Input: "(2+6* 3+5- (3*14/7+2)*5)+3"
output: -12
解释:
提示:
1 你可以假设所给定的表达式都是有效的。
2 请不要使用内置的库函数 eval。
思考
1 这里相比227,主要区别就是如何想办法处理掉左右括号?
1.1 如果找到办法处理左右括号,那么就可以很容易解决该问题了,可是如何处理这里的左右括号呢?
参考实现1
实现1
const calculate = (s) => {
s = s.replace(/\s+/g, "");
const sLen = s.length;
// 当前的操作数
let num = 0;
const stack = [];
// 操作符,第一个加上"+
let opr = "+";
for (let i = 0; i < sLen; ++i) {
const c = s.charAt(i);
if (/^[0-9]+.?[0-9]*$/.test(s.charAt(i))) {
num = num * 10 + s.charCodeAt(i) - 48;
} else if (c === "(") {
let j = i;
let matchCount = 0;
for (; i < sLen; ++i) {
if (s.charAt(i) === "(") ++matchCount;
if (s.charAt(i) === ")") --matchCount;
if (matchCount === 0) break;
}
num = calculate(s.substring(j + 1, i));
}
if (c === "+" || c === "-" || c === "*" || c === "/" || i === sLen - 1) {
if (opr === "-") {
stack.push(-num);
}
if (opr === "+") {
stack.push(num);
}
if (opr === "*") {
stack.push(stack.pop() * num);
}
if (opr === "/") {
const tempNum = stack.pop();
stack.push(tempNum > 0 ? Math.floor(tempNum / num) : Math.ceil(tempNum / num));
}
opr = c;
num = 0;
}
}
return stack.reduce((a, b) => a + b);
};
export default calculate;
28. 实现 strStr()
题目描述
实现 strStr() 函数。
给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。
例子1
Input: haystack = "hello", needle = "ll"
output: 2
解释:
例子2
Input: haystack = "aaaaa", needle = "bba"
output: -1
解释:
提示:
1 当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。
2 对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与C语言的 strstr() 以及 Java的 indexOf() 定义相符。
思考
1 第一种很容易想到,直接使用暴力解法
参考实现1
2 这是特别的典型使用kmp算法来解决的问题。
kmp算法其实也很简单,就是当发现不匹配的时候,如何更快的把子串移动到最大距离
kmp算法最难的就是next数组,next数组存储的就是当前字符前面的字符串中前后互相相等的长度减一。
求next数组就是使用dp来求
只要得出next数组,其他就简单了
实现1
/**
* @param {string} haystack
* @param {string} needle
* @return {number}
*/
// Runtime: 4132 ms, faster than 5.02% of JavaScript online submissions for Implement strStr().
// Memory Usage: 40 MB, less than 27.63% of JavaScript online submissions for Implement strStr().
export default (haystack, needle) => {
if (!needle) return 0;
const len = needle.length;
for (let i = 0; i < haystack.length; i++) {
if (haystack.charAt(i) === needle.charAt(0)) {
let begin = i;
let needleBegin = 0;
while (haystack.charAt(begin) === needle.charAt(needleBegin)) {
begin++;
needleBegin++;
if (needleBegin === len) {
return i;
}
}
}
}
return -1;
};
时间复杂度O(m+n),空间复杂度O(m)
409. 最长回文串
题目描述
给定一个包含大写字母和小写字母的字符串,找到通过这些字母构造成的最长的回文串。
在构造过程中,请注意区分大小写。比如 "Aa" 不能当做一个回文字符串。
例子1
Input: haystack = "hello", needle = "ll"
output: 2
解释:
例子2
Input: "abccccdd"
output: 7
解释:我们可以构造的最长的回文串是"dccaccd", 它的长度是 7。
提示:
1 假设字符串的长度不会超过 1010。
思考
1 直接使用hash,根据回文字符串中只有一个字符出现一次,其他字符都是偶数次进行构造
参考实现1
实现1
/**
* @param {string} s
* @return {number}
*/
// Runtime: 76 ms, faster than 96.49% of JavaScript online submissions for Longest Palindrome.
// Memory Usage: 40 MB, less than 62.11% of JavaScript online submissions for Longest Palindrome.
export default (s) => {
const map = new Map();
let count = 0;
for (let i = 0; i < s.length; i++) {
if (map.has(s.charAt(i))) {
const tempCount = map.get(s.charAt(i)) + 1;
map.set(s.charAt(i), tempCount);
} else {
map.set(s.charAt(i), 1);
}
}
// console.log(map)
for (let [key, val] of map) {
if (val % 2 === 0) {
count += val;
} else if (val % 2 !== 0) {
count += val - 1;
}
}
if (count < s.length) {
return count + 1;
} else {
return count;
}
};
时间复杂度O(n),空间复杂度O(n)
3. 无重复字符的最长子串
题目描述
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
例子1
Input: s = "abcabcbb"
output: 3
解释:因为无重复字符的最长子串是 "abc",所以其长度为 3。
例子2
Input: s = "bbbbb"
output: 1
解释:因为无重复字符的最长子串是 "b",所以其长度为 1。
例子3
Input: s = "pwwkew"
output: 3
解释:因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
例子4
Input: s = ""
output: 0
解释:
提示:
1 0 <= s.length <= 5 * 104
2 s 由英文字母、数字、符号和空格组成
思考
1 使用类似dp的方法,dp[i]表示以s[i]结尾的最大的含有不重复字符的最大长度,那么dp[i+1]就等于从s[i+1]往前移动dp[i]个字符,发现是否有和s[i+1]相等的字符,如果没有,,则dp[i+1]=dp[i]+1,否则等于dp[i+1] = i-j(j是和s[i+1]相等的字符的位置)
参考实现1
2 可以使用hash存储每个字符的位置,然后使用类似双指针的思想来解决。
2.1 这里需要解决的是什么时候更新begin指针,如何更新?
参考实现2
实现1
/**
* @param {string} s
* @return {number}
*/
// Runtime: 112 ms, faster than 76.90% of JavaScript online submissions for Longest Substring Without Repeating Characters.
// Memory Usage: 41.3 MB, less than 90.39% of JavaScript online submissions for Longest Substring Without Repeating Characters.
export default (s) => {
if (!s) return 0;
const dp = [];
let max = 1;
dp[0] = 1;
for (let i = 1; i < s.length; i++) {
let count = 1;
for (let j = i - 1; j >= i - dp[i - 1]; j--) {
if (s.charAt(j) === s.charAt(i)) {
dp[i] = i - j;
max = Math.max(i - j, max);
break;
}
if (j === i - dp[i - 1]) {
dp[i] = dp[i - 1] + 1;
max = Math.max(dp[i], max);
}
}
}
return max;
};
实现2
// Runtime: 108 ms, faster than 83.81% of JavaScript online submissions for Longest Substring Without Repeating Characters.
// Memory Usage: 41.9 MB, less than 86.43% of JavaScript online submissions for Longest Substring Without Repeating Characters.
export default (s) => {
if (!s) return 0;
const map = new Map();
let max = 1;
let begin = 0;
for (let i = 0; i < s.length; i++) {
if (map.has(s.charAt(i))) {
const iIndex = map.get(s.charAt(i));
if (begin <= iIndex) {
max = Math.max(i - map.get(s.charAt(i)), max);
begin = map.get(s.charAt(i)) + 1;
map.set(s.charAt(i), i);
} else {
max = Math.max(i - begin + 1, max);
map.set(s.charAt(i), i);
}
} else {
max = Math.max(i - begin + 1, max);
map.set(s.charAt(i), i);
}
}
// console.log(max);
return max;
};
时间复杂度O(n)空间复杂度O(n)
5. 最长回文字符串
题目描述
给定一个字符串,找出最长的回文字符串
例子1
Input: s = "babad"
output: "bab"
解释:因为无重复字符的最长子串是 "abc",所以其长度为 3。
例子2
Input: s = "cbbd"
output: “bb”
解释:
例子3
Input: s = "a"
output: “a”
解释:
例子4
Input: s = "ac"
output: “a”
解释:
提示:
1 1 <= s.length <= 1000
2 s 由英文小写字母或者英文大写字母组成
思考
1 使用dp很好解决,只不过这里的dp是从长度等于1一直到长度等于字符串长度开始
参考实现1
2 可以使用马拉车算法。
马拉车算法的关键点是什么呢?
就是我们想利用前面已经匹配到的信息,关键点就是center到左右maxRight是回文字符串,充分利用回文字符串的特性,什么特性呢,就是左右对称。
参考实现2
实现1
/**
* @param {string} s
* @return {string}
*/
// "babad";
// "cbbd";
// aacabdkacaa;
export default (s) => {
const len = s.length;
const dp = [];
if (s.length <= 1) {
return s;
}
for (let i = 0; i < len; i++) {
dp[i] = new Array(len).fill(false);
dp[i][i] = true;
}
let begin = 0;
let end = 1;
for (let len1 = 2; len1 <= len; len1++) {
for (let j = 0; j < len + 1 - len1; j++) {
if (s.charAt(j) === s.charAt(j + len1 - 1)) {
if (len1 === 2 || (dp[j + 1][j + len1 - 2] && j <= j + len1 - 3)) {
dp[j][j + len1 - 1] = true;
if (len1 > end - begin) {
begin = j;
end = j + len1;
}
}
}
}
}
return s.substring(begin, end);
};
实现2
/**
* @param {string} s
* @return {string}
*/
export default (s) => {
const len = s.length;
const dp = [];
if (s.length <= 1) {
return s;
}
for (let i = 0; i < len; i++) {
dp[i] = new Array(len).fill(false);
dp[i][i] = true;
}
let begin = 0;
let end = 1;
for (let len1 = 2; len1 <= len; len1++) {
for (let j = 0; j < len + 1 - len1; j++) {
if (s.charAt(j) === s.charAt(j + len1 - 1)) {
if (len1 === 2 || (dp[j + 1][j + len1 - 2] && j <= j + len1 - 3)) {
dp[j][j + len1 - 1] = true;
if (len1 > end - begin) {
begin = j;
end = j + len1;
}
}
}
}
}
return s.substring(begin, end);
};
时间复杂度O(n)空间复杂度O(n)