面试之必备字符串

271 阅读11分钟

字符串

字符串是最常用的一种数据类型,也是面试中经常必会问题的类型之一。

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)