前端算法 | 字符串篇

151 阅读10分钟

本文是作者刷算法题之余,将刷题的经验分享出来,欢迎和我交流探讨。

(Easy) —— 验证回文串 II

给定一个非空字符串 s,请判断如果 最多 从字符串中删除一个字符能否得到一个回文字符串。

 

示例 1:

输入: s = "aba"
输出: true

示例 2:

输入: s = "abca"
输出: true
解释: 可以删除 "c" 字符 或者 "b" 字符

示例 3:

输入: s = "abc"
输出: false

分析

  首先给题目进行关键词分类,这可以帮助我们以后遇到其他算法题时快速检索这类题的解题思路。

  这道题目的关键词是:

  • 字符串
  • 回文
  • 删除

  判断字符串是否回文有固定的套路。难点在于要删除某个字符,删除字符这个操作可以使用 指针 跳过来模拟。

  我们先使用 对撞双指针 从两端不断地比较两端字符是否相等,目的就是要找到那两个不相等的字符。一旦出现不相等的,分别尝试从 左指针右指针 跳过一个字符,之后套用验证回文串的通用方法验证是否是回文串。

指针跳过某个字符判断是否回文

/**
 * @param {string} s
 * @return {boolean}
 */
var validPalindrome = function (s) {
  // 初始化指针
  let j = 0
  let k = s.length - 1

  while (j < k && s[j] === s[k]) {
    j++
    k--
  }

  // 跳过j
  if (isPalindrome(s, j + 1, k)) {
    return true
  }

  // 跳过k
  if (isPalindrome(s, j, k - 1)) {
    return true
  }
    
  // 判断是否回文串的固定套路  
  function isPalindrome(str, start, end) {
    while (start < end) {
      if (str[start] !== str[end]) {
        return false
      }
      start++
      end--
    }
    return true
  }

  return false
}
  • 时间复杂度:O(n),n为字符串长度,最多把字符完全遍历一次
  • 空间复杂度:O(1),变量数跟遍历无关

总结

  • 判断回文串有固定的 套路,一定记住
  • 删除某个字符的操作,想到用 指针 模拟

(Easy) —— 有效的括号

给定一个只包括 '('')''{''}''['']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。
  3. 每个右括号都有一个对应的相同类型的左括号。

 

示例 1:

输入: s = "()"
输出: true

示例 2:

输入: s = "()[]{}"
输出: true

示例 3:

输入: s = "(]"
输出: false

分析

  首先给题目进行关键词分类,这可以帮助我们以后遇到其他算法题时快速检索这类题的解题思路。

  这道题目的关键词是:

  • 字符串
  • 对称性

  根据题意,假设第一个字符是 (, 那么最后一个字符也要是 ),有点类似于先入后出?可以联想到 的数据结构。

  我们将左右括号的对应关系维护到一个字典对象中,一旦匹配到左括号 (,就将对应的右括号 ) push 中,随着遍历,越来越多左括号对应的右括号被 push 到了 中。这时候,先被 push 的字符在栈底,后被 push 的字符在栈顶,如果要出栈的话,后被 push 的字符先出栈,和左右符号的匹配规则相符(最后出现的左括号将会第一个匹配到他的右括号)。

根据出栈先后验证对称性

const leftToRight = {
  '(': ')',
  '{': '}',
  '[': ']',
}
/**
 * @param {string} s
 * @return {boolean}
 */
var isValid = function (s) {
  // 极端情况,空字符串,一定不符合要求
  if (!s.length) {
    return false
  }
  const stack = [] // 初始化栈
  const len = s.length
  for (let i = 0; i < len; i++) {
    const cur = s[i]
    // 如果匹配到左括号,将对应的右括号入栈
    if (cur === '(' || cur === '{' || cur === '[') {
      stack.push(leftToRight[cur])
    }
    // 如果匹配到的是右括号,如果栈为空 || 栈顶不是当前右括号,即为匹配失败
    else {
      if (!stack.length || stack.pop() !== cur) {
        return false
      }
    }
  }
  // 如果字符串是有效的,最终的栈一定是空的
  if (stack.length) {
    return false
  } else {
    return true
  }
}

总结

  • 字符串的 对称性 问题,联想到 栈的出入栈

(Medium) —— 添加与搜索单词 - 数据结构设计

请你设计一个数据结构,支持 添加新单词 和 查找字符串是否与任何先前添加的字符串匹配 。

实现词典类 WordDictionary :

  • WordDictionary() 初始化词典对象
  • void addWord(word) 将 word 添加到数据结构中,之后可以对它进行匹配
  • bool search(word) 如果数据结构中存在字符串与 word 匹配,则返回 true ;否则,返回  false 。word 中可能包含一些 '.' ,每个 . 都可以表示任何一个字母。

 

示例:

输入:
["WordDictionary","addWord","addWord","addWord","search","search","search","search"]
[[],["bad"],["dad"],["mad"],["pad"],["bad"],[".ad"],["b.."]]
输出:
[null,null,null,null,false,true,true,true]

解释:
WordDictionary wordDictionary = new WordDictionary();
wordDictionary.addWord("bad");
wordDictionary.addWord("dad");
wordDictionary.addWord("mad");
wordDictionary.search("pad"); // 返回 False
wordDictionary.search("bad"); // 返回 True
wordDictionary.search(".ad"); // 返回 True
wordDictionary.search("b.."); // 返回 True

请你设计一个数据结构,支持 添加新单词 和 查找字符串是否与任何先前添加的字符串匹配 。

实现词典类 WordDictionary :

  • WordDictionary() 初始化词典对象
  • void addWord(word) 将 word 添加到数据结构中,之后可以对它进行匹配
  • bool search(word) 如果数据结构中存在字符串与 word 匹配,则返回 true ;否则,返回  false 。word 中可能包含一些 '.' ,每个 . 都可以表示任何一个字母。

 

示例:

输入:
["WordDictionary","addWord","addWord","addWord","search","search","search","search"]
[[],["bad"],["dad"],["mad"],["pad"],["bad"],[".ad"],["b.."]]
输出:
[null,null,null,null,false,true,true,true]

解释:
WordDictionary wordDictionary = new WordDictionary();
wordDictionary.addWord("bad");
wordDictionary.addWord("dad");
wordDictionary.addWord("mad");
wordDictionary.search("pad"); // 返回 False
wordDictionary.search("bad"); // 返回 True
wordDictionary.search(".ad"); // 返回 True
wordDictionary.search("b.."); // 返回 True

分析

  首先给题目进行关键词分类,这可以帮助我们以后遇到其他算法题时快速检索这类题的解题思路。

  这道题目的关键词是:

  • 字符串
  • 设计数据结构
  • 字符串查询

  设计数据结构 这类题,没有太好的方法,只能多做题,拓宽思路,记住各种的设计方式。

  对于查询,效率最高的当然是对象(时间复杂度是 O(1)),为了降低查找的复杂度,我们可以考虑以字符串的长度为 key,相同长度的字符串存在于一个数组中。

  现在数据结构定下了,addWord 方法也不是问题。现在来解决 search 支持查找 字符串 和 正则 的问题。根据题意分析:

  • 如果不包含 .,根据字符串 精确 查询
  • 如果包含.,套用正则查询

对象 + 数组 + 正则 设计字符串数据结构

/*
 * @lc app=leetcode.cn id=211 lang=javascript
 *
 * [211] 添加与搜索单词 - 数据结构设计
 */

// @lc code=start

var WordDictionary = function () {
  this.words = new Map()
}

/**
 * @param {string} word
 * @return {void}
 */
WordDictionary.prototype.addWord = function (word) {
  const len = word.length
  if (this.words.has(len)) {
    this.words.get(len).push(word)
  } else {
    this.words.set(len, [word])
  }
  return true
}

/**
 * @param {string} word
 * @return {boolean}
 */
WordDictionary.prototype.search = function (word) {
  const len = word.length
  if (!this.words.has(len)) {
    return false
  }
  // 普通字符串
  if (!word.includes('.')) {
    return this.words.get(len).includes(word)
  }
  // 正则
  const reg = new RegExp('^' + word + '$')
  return this.words.get(len).some((item) => {
    return reg.test(item)
  })
}

/**
 * Your WordDictionary object will be instantiated and called as such:
 * var obj = new WordDictionary()
 * obj.addWord(word)
 * var param_2 = obj.search(word)
 */
// @lc code=end


总结

  • 字符串的存储结构,可能是对象+数组
  • 字符串的题目很大概率会涉及正则表达式,正则表达式的基本用法要清楚

(Medium) —— 字符串转换整数 (atoi)

请你来实现一个 myAtoi(string s) 函数,使其能将字符串转换成一个 32 位有符号整数(类似 C/C++ 中的 atoi 函数)。

函数 myAtoi(string s) 的算法如下:

  1. 读入字符串并丢弃无用的前导空格
  2. 检查下一个字符(假设还未到字符末尾)为正还是负号,读取该字符(如果有)。 确定最终结果是负数还是正数。 如果两者都不存在,则假定结果为正。
  3. 读入下一个字符,直到到达下一个非数字字符或到达输入的结尾。字符串的其余部分将被忽略。
  4. 将前面步骤读入的这些数字转换为整数(即,"123" -> 123, "0032" -> 32)。如果没有读入数字,则整数为 0 。必要时更改符号(从步骤 2 开始)。
  5. 如果整数数超过 32 位有符号整数范围 [−2^31,  2^31 − 1] ,需要截断这个整数,使其保持在这个范围内。具体来说,小于 −2^31 的整数应该被固定为 −2^31 ,大于 2^31 − 1 的整数应该被固定为 2^31 − 1 。
  6. 返回整数作为最终结果。

注意:

  • 本题中的空白字符只包括空格字符 ' ' 。
  • 除前导空格或数字后的其余字符串外,请勿忽略 任何其他字符。

 

示例 1:

输入: s = "42"
输出: 42
解释: 加粗的字符串为已经读入的字符,插入符号是当前读取的字符。
第 1 步:"42"(当前没有读入字符,因为没有前导空格)
         ^
第 2 步:"42"(当前没有读入字符,因为这里不存在 '-' 或者 '+')
         ^
第 3 步:"42"(读入 "42")
           ^
解析得到整数 42 。
由于 "42" 在范围 [-231, 231 - 1] 内,最终结果为 42

示例 2:

输入: s = "   -42"
输出: -42
解释:
第 1 步:" -42"(读入前导空格,但忽视掉)
            ^
第 2 步:"   -42"(读入 '-' 字符,所以结果应该是负数)
             ^
第 3 步:"   -42"(读入 "42")
               ^
解析得到整数 -42 。
由于 "-42" 在范围 [-231, 231 - 1] 内,最终结果为 -42

示例 3:

输入: s = "4193 with words"
输出: 4193
解释:
第 1 步:"4193 with words"(当前没有读入字符,因为没有前导空格)
         ^
第 2 步:"4193 with words"(当前没有读入字符,因为这里不存在 '-' 或者 '+')
         ^
第 3 步:"4193 with words"(读入 "4193";由于下一个字符不是一个数字,所以读入停止)
             ^
解析得到整数 4193 。
由于 "4193" 在范围 [-231, 231 - 1] 内,最终结果为 4193

分析

  首先给题目进行关键词分类,这可以帮助我们以后遇到其他算法题时快速检索这类题的解题思路。

  这道题目的关键词是:

  • 字符串
  • 边界计算
  • 字符的匹配:这道题是空格、符号、非数字字符的匹配

  边界计算较为简单,先来实现这个。

// 边界限制
const max = Math.pow(2, 31) - 1 // 计算最大值
const min = -Math.pow(2, 31) // 计算最小值
if (value > max) {
    value = max
} else if (value < min) {
    value = min
} else {
    // do nothing
}

  再来看字符的匹配,优先想到正则表达式,看下这道题能不能套用正则表达式。

  1. 读入字符串并丢弃无用的前导空格 => \s
  2. 正负号 => [\+-]?
  3. 非数字字符或到达输入的结尾 => .*

  OK,正则表达式完全符合题目要求,再来看剩下的要求怎么满足。

  1. 数字转换为整数 => +。如果没有读入数字,则整数为 0 => isNaN()判断
  2. 边界要求 => 已实现

现在,尝试写一下代码实现

字符串转整数,包含边界要求

/**
 * @param {string} s
 * @return {number}
 */
var myAtoi = function (s) {
  // 初始化正则规则,注意要用捕获组包裹你想要的部分
  // const reg = new RegExp(/\s*([\+-]?[0-9]*).*/)
  const reg = /\s*([\+-]?[0-9]*).*/
  // 得到捕获组
  const groups = s.match(reg)
  /* 
  groups的内容如下:
  example input: "4193 with words"
  匹配成功:
  [
    '4193 with words',
    '4193',
    index: 0,
    input: '4193 with words',
    groups: undefined
  ] 
  匹配失败:
  [ 'words', '', index: 0, input: 'words', groups: undefined ]
  */
  const max = Math.pow(2, 31) - 1 // 计算最大值
  const min = -Math.pow(2, 31) // 计算最小值
  let res = 0 // 转化后的整数值

  // 假设匹配成功
  if (groups[1]) {
    // 转化为整数
    res = +groups[1]
    // 兼容转化后的结果是NaN的情况,此时赋值为0
    if (isNaN(res)) {
      res = 0
    }
  }

  // 判断是否超出边界范围,如果超过,进行转换
  if (res > max) {
    return max
  } else if (res < min) {
    return min
  }

  // 不超过,正常返回
  return res
}

总结

  • 字符串的题目很大概率会涉及正则表达式,正则表达式的基本用法要清楚。这道题主要使用了正则中匹配组的写法 + 获取匹配组的方法