JS算法探险之字符串

大家好,我是「柒八九」。一个立志要成为「海贼王的男人」

今天,我们讲一讲,JS中针对 String类型的相关算法的解题技巧和一些注意事项。

我们之前,已经有3篇文章,从不同视角来探寻JS算法中可能遇到的「礁石」。如果有诸君需要的,「拿走不谢」,但是不要忘记回来,看下面的文章。

文章list

天不早了,我们干点正事哇。

字符串-打油诗

  • 字符串算法有很多,「变位词」「回文串」来报道
  • 变位词要「数数」「哈希表」来撑场面
    • 哈希表可变通,counts = new Array(x).fill(0)
    • 下标对应ascll字符,s.charAt(i).charCodeAt()
    • 值对应字符梳理, counts[x]++--
    • 反向双指针,第一指针,始终为i-s1l,第二指针i
  • 回文串有特点,前后字符都一样
    • 「反向双指针」花样多
    • 两边向中间,left=0/right= s.length-1
    • 中间向两边, i可为奇数中心,ii+1可为偶数中心

文章概要

  1. 双指针
  2. 回文字符串

知识点简讲

String本质

每个字符在 JS 内部都是以16位(即「2个字节」)的 UTF-16 格式储存,也就是说 「JS 的字符长度固定为16位长度,即2个字节」

ECMAScript 中的String「不可变的」即:「String一旦创建,他们的值就不能改变」

要改变某个变量保存的String,首先要「销毁原来的」 String,然后再用另一个「包含新值的」 String填充该变量。

let stringVal = '北宸';
stringVal = stringVal + '南蓁';

实现这个操作的过程如下:

  1. 创建一个能容纳8个字节的新String
  2. 在这个String中填充 "北宸"和"南蓁"
  3. 销毁原来的String "北宸"和"南蓁"

工具方法 & 语言特性

❝在JS中,「字符串可以被视为字符数组」

  • str.charAt(i) 用于获取stri位置的字符

在JS中,字符之间是无法之间「相减」

'b' - 'a' // NAN

其实,这里面的深层次的原因是,JS中针对 '-'操作符,没兼容字符。而-操作符要的预期就是返回数值,因为,字符没被兼容,所以结果返回了一个NAN

作为替换方案,str.charAt(i).charCodeAt()(获取stri位置的字符ASCLL码值 )就肩负起,字符之间相减的操作

str = 'ba';
str.charAt(1).charCodeAt() - str.charAt(0).charCodeAt()
// 结果为1  b的ascll 为98,a的ascll为97 即:98 -97

1. 双指针

在JS算法探险之数组中我们通过「双指针」的技巧,来处理一些比较有特点的数组数据。

「字符串可以被视为字符数组」,那么也可以用「双指针」的技巧来处理字符串的一些问题。

由于在处理字符串时,很多都与「统计字母出现的次数有关」,所以我们可以借用「哈希表」(map)来存储每个元素出现的次数。

❝Map 在信息存储方面还是很有用的。在讲「数组」算法中,在非正整数用Si时,就用 Map进行key 和value的信息存储 ❞

字符串中的变位词

题目描述:

❝输入字符串s1和s2,判断s2中是否包含s1的某个变位词 提示: 如果s2中包含s1的某个变位词,则s1至少有一个变位词是s2的「子字符串」 假设两个字符串中只包含英文小写字母 示例:s1 为“ac”, s2为“dgcaf” ,由于s2包含s1的变位词"ca", 结果为「true」

分析

  1. 「变位词」是指组成各个单词的「字母」及每个字母出现的「次数」完全相同,只是字母的排列顺序不同
  2. 变位词有几个特点
    • 一组变位词「长度相同」
    • 组成变位词的「字母集合相同」
    • 每个字母出现的「次数」也相同
  3. 变位词与「字母及字母出现的次数」有关,那么统计字符串中包含的字母及每个字母出现的次数。
    • 哈希表的「键」是字母
    • 「值」对应的是「字母出现的次数」
  4. 题中,说只含有「小写英文」,所以我们可以「用数组模拟一个哈希表」
    • 数组「下标」表示字母,即 下标为0 对应字母a, 下标为1对应字母b
    • 数组中的「值」表示对应字母出现的次数
  5. 「首先」,扫描s1,每扫描到一个字符,就找到它在哈希表中的位置,并把它对应+1
  6. 判断s2「子字符串」是否包含s1的变位词
    • 假设s1长度为n
    • 逐一判断s2「长度为n的子字符串」是不是s1的变位词
    • 扫描「子字符串」中的每个字母,把该字母在哈希表中对应的值-1
    • 如果哈希表中「所有」值都是0,那么该「子字符串」就是s1的变位词

代码实现

function checkInclusion(s1,s2){
  let s1l = s1.length,s2l = s2.length;
  
  if(s2l<s1l) return false;
  
  // 构建 字符 => 个数 数组
  let counts = new Array(26).fill(0);
  
  // 遍历s1,并对s1中字符进行数据收集 (++)
  // 针对已收集的s1数据信息,与s2进行匹配(--)
  for(let i =0;i<s1l;i++){
    counts[s1.charAt(i).charCodeAt() - 97]++;
    counts[s2.charAt(i).charCodeAt() - 97]--;
  }
  
  //判断,是否全为0,如果是,刚才已经满足情况了,直接返回true
  if(areaAllZero(counts)) return true;
  
  //从s1l的位置出发,先匹配,后收集(类似同向双指针)
  for(let i = s1l;i<s2l;i++){
    counts[s2.charAt(i).charCodeAt() - 97]--;
    counts[s2.charAt(i - s1l).charCodeAt() -97]++;
    if(areaAllZero(counts)) return true;
  }
  return false
}

辅助函数,用于判断,数值中值是否都为0

function areaAllZero(counts){
  for(let count  of counts) {
    if(count >0) return false
  }
  return true;
}

在上面的函数中,

  • 「第一个指针」指向下标为i-s1l的位置
  • 「第二个for」循环中的下标i相当于「第二个指针」,指向「子字符串」的最后一个字符
  • 两个指针之间的「子字符串」的长度一直是字符串s1的长度

字符串中所有变位词

题目描述:

❝输入字符串s1和s2,找出s1的「所有」变位词在s1中的「起始」下标 提示: 假设两个字符串中只包含英文小写字母 示例:s1 为“abc”, s2为“cbadabacg” ,s1的两个变位词"cba"/"bac"是s1中的子字符串,输出在s1中的起始下标为0和5 ❞

分析

和找「字符串中的变位词」的思路是一样的

  1. 变位词与「字母及字母出现的次数」有关,那么统计字符串中包含的字母及每个字母出现的次数。
    • 哈希表的「键」是字母
    • 「值」对应的是「字母出现的次数」
  2. 题中,说只含有「小写英文」,所以我们可以「用数组模拟一个哈希表」
    • 数组「下标」表示字母,即 下标为0 对应字母a, 下标为1对应字母b
    • 数组中的「值」表示对应字母出现的次数
  3. 「首先」,扫描s1,每扫描到一个字符,就找到它在哈希表中的位置,并把它对应+1
  4. 判断s2「子字符串」是否包含s1的变位词
    • 假设s1长度为n
    • 逐一判断s2「长度为n的子字符串」是不是s1的变位词
    • 扫描「子字符串」中的每个字母,把该字母在哈希表中对应的值-1
    • 如果哈希表中「所有」值都是0,那么该「子字符串」就是s1的变位词(进行下标的记录处理)

代码实现

function findAnagrams(s1,s2){
  let result = [];
  
  let s1l = s1.length,s2l = s2.length;
  if(s2l<s1l) return result;
  
  let counts = new Array(26).fill(0);
  
  for(let i= 0;i<s1l;i++){
    counts[s1.charAt(i).charCodeAt() - 97]++;
    counts[s2.charAt(i).charCodeAt() - 97]--;
  }
  
  if(areaAllZero(counts)) result.push(0);
  
  for(let i= s1l;i<s2l;i++){
    counts[s2.charAt(i).charCodeAt()-97]--;
    counts[s2.charAt(i-s1l).charCodeAt()-97]++;
    // 在满足情况下,对应的开始下标为`i-s1l+1`
    if(areaAllZero(counts)) result.push(i - s1l+1);
  }
  return result
}

辅助函数,用于判断,数值中值是否都为0

function areaAllZero(counts){
  for(let count  of counts) {
    if(count >0) return false
  }
  return true;
}

针对「字符串中的变位词」还是「字符串中所有变位词」中用到的思路,都是「利用数组来模拟哈希表」(map)然后,针对特定的场景进行数据的处理。然后,针对双指针的定义,在第二个for循环中,第一个指针为i-s1l对应的位置,第二个指针,为i对应的位置,而两者恰好相差(s1l)的长度。

不含重复字符的「最长子字符串」

题目描述:

❝输入一个字符串,求该字符串中不含重复字符的「最长子字符串」 示例: 输入"babcca",其最长的不含重复字符的子字符串为“abc”,长度为3 ❞

分析

  1. 此处用哈希表(map)统计子字符串中字符出现的次数
  2. 如果一个字符串中不含重复的字符,那么每个字符都是只出现一次,即哈希表中对应的值为1
  3. 我们还是采用用「数组来模拟哈希表」,由于题目中,没限制字符为小写英文字母,所以我们需要对字符做一个简单限制,只处理ascll的字符,即:new Array(256).fill(0)
  4. 仍用「两个指针」来定位一个「子字符串」
    • 第一个指针指向子字符串的第一个字符
    • 第二个指针指向子字符串的最后一个字符
  5. 如果两个指针之间的子字符串不包含重复的字符,为了找出最长的子字符串,「向右移动第二个」指针,然后判断是否出现重复字符
  6. 如果两个指针之间的子字符串中包含重复的字符,「向右移动第一个」指针

代码实现

function lengthOfLongestSubstring(s){
  let sl = s.length;
  if(sl==0) return 0;
  
  let counts = new Array(256).fill(0);
  let longest = 0;
  let j= -1; // 左指针
  // i 为右指针
  for(let i=0;i<sl;i++){
    counts[s.charAt(i).charCodeAt()]++;
    while(hasGreaterThan1(counts)){
      j++
      counts[s.charAt(j).charCodeAt()]--;
    }
    // 更新最长子串的长度
    longest = Math.max(i-j,longest);
  }
  return longest;
}

辅助函数,用于判断数组中是否有含有大于1的数

function hasGreaterThan1(counts){
  for(let count of counts){
    if(count>1) return true
  }
  return false;
}

在上面代码中,其实难点就是双指针的定义和赋值

  • 左指针 1. 默认值为-1 2. 在hasGreaterThan1为true时,j++,且counts指定位置counts[s.charAt(j).charCodeAt()]--
  • 右指针 1. 默认值为0 2. 通过循环控制右指针移动

回文字符串

回文是一类特殊的字符串。不管「从前往后」,还是「从后往前」,得到的字符信息都是一样的。

有效回文

题目描述:

❝输入一个字符串,判断它是不是回文 提示: 只考虑字母和数字字符,忽略大小写 示例: 输入字符串“abba”返回true, 输入“abc”返回false ❞

分析

  1. 判断字符串是否为回文,既定套路「反向双指针」
    • 一个指针从「第一个字符」开始,「从前往后」移动
    • 另一个指针从「最后一个字符」开始,「从后往前」移动
  2. 针对非数字和字母的字符,进行跳过处理
  3. 大小写需要转换

代码实现

function isPalindrome(s){
  let left =0,right = s.length -1;
  
  while(left<right){
    // 获取指定位置的字符
    let cl = s.charAt(left);
    let cr = s.charAt(right);
    
    // 跳过非数字和字母的字符 (!isLetterOrDigit(x))
    if(!isLetterOrDigit(cl)){
      left++;
    }else if(!isLetterOrDigit(cr)){
      right--;
    }else {
      // 大小写不敏感
      cl = cl.toLocaleLowerCase();
      cr = cr.toLocaleLowerCase();
      // 不一样,跳出循环
      if(cl!=cr) return false
      
      // 指针移动
      left++;
      right--;
    }
  }
  return true;
}

辅助函数

const isLetterOrDigit = str =>  /^[A-Za-z0-9]+$/.test(str)

最多删除一个字符得到回文

题目描述:

❝输入一个字符串,判断「最多」从字符串中删除一个字符能不能得到一个回文字符串 示例: 输入字符串“abca”, 删除字符b或者c能得到一个回文字符串,因此输出true ❞

分析

  1. 判断字符串是否为回文,既定套路「反向双指针」
    • 一个指针从「第一个字符」开始,「从前往后」移动
    • 另一个指针从「最后一个字符」开始,「从后往前」移动
  2. 题目中说,「最多」删除一个字符
    • 不删除:本身就是回文串
    • 删除:可能是前半部分,也可能是后半部分

代码实现

function validPalindrome(s){
  let left =0,right = s.length -1;
  
  let middlePosition = s.length>>1;
  
  // 移动指针,并比较字符是否相等
  for(;left<middlePosition;left++,right--){
    if(s.charAt(left)!=s.charAt(right)) break;
  }
  // 这里有两类情况 
  // 1: 字符串本身是回文 (left == middlePosition)
  // 2. 需要对字符串进行字符剔除 (isPalindrome)
  return left == middlePosition 
        || isPalindrome(s,left,right-1)
        || isPalindrome(s,left+1,right)
}

辅助函数,用于判断字符串是否是回文

function isPalindrome(s,left,right){
  while(left<right){
    if(s.charAt(left)!= s.charAt(right)) break;
    
    left++;
    right--;
  }
  return left>=right;
}

这里有一个比较重要的点,就是「最多」可以删除一个字符。放到代码中其实就是在validPalindromereturn那里体现

  • 「不删除字符」:本身就是回文,那就意味着在validPalindromefor循环没阻断,即:left == middlePositon
  • 「删除字符」:意味着在validPalindrome中的for发生了「阻断」(break)
    • 在阻断处,删除「后半部分」的字符isPalindrome(s,left,right-1)
    • 在阻断处,删除「前半部分」的字符isPalindrome(s,left+1,right)

回文子字符串的个数

题目描述:

❝输入一个字符串,求字符串中有多少个「回文连续子字符串」? 示例: 输入字符串“abc”有3个回文子字符串,分别是"a"/"b"/"c" ❞

分析

  1. 判断字符串是否为回文,既定套路「反向双指针」
    • 从两边向中间移动(比较常见)
    • 从中间向两边扩散
  2. 回文的长度既可以是奇数,也可以是偶数
    • 长度为奇数的回文的「对称中心只有一个字符」
    • 长度为偶数的回文的「对称中心有两个字符」

代码实现

function countSubstrings(s){
  if(s==null || s.length==0) return 0; //处理边界
  
  let count = 0;
  for(let i=0;i<s.length;i++){
    // 字符串下标为i。
    // 既作为奇数回文的中心
    // 又可以和i+1一同作为偶数回文的中心
    count+=countPalindrome(s,i,i);
    count+=countPalindrome(s,i,i+1);
  }
  return count;
}

辅助函数,

function countPalindrome(s,left,right){
  let count = 0;
  while(left>=0&&right<s.length
        && s.charAt(left)==s.charAt(right)){
          count++;
          left--;
          right++;
        }
  return count;
}

这个题,最主要的就是需要明白:

  • i个字符本身可以成为「长度为奇数」的回文字符串的对称中心
    • 所以,在下标i的位置 countPalindrome(s,i,i);
  • i个字符和第i+1个字符可以成为「长度为偶数」的回文字符串的对称中心
    • 所以,在下标i的位置 countPalindrome(s,i,i+1);

后记

「分享是一种态度」

参考资料:剑指offer

原文地址:https://cloud.tencent.com/developer/article/2081766

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习编程?其实不难,不过在学习编程之前你得先了解你的目的是什么?这个很重要,因为目的决定你的发展方向、决定你的发展速度。
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面设计类、前端与移动、开发与测试、营销推广类、数据运营类、运营维护类、游戏相关类等,根据不同的分类下面有细分了不同的岗位。
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生学习Java开发,但要结合自身的情况,先了解自己适不适合去学习Java,不要盲目的选择不适合自己的Java培训班进行学习。只要肯下功夫钻研,多看、多想、多练
Can’t connect to local MySQL server through socket \'/var/lib/mysql/mysql.sock问题 1.进入mysql路径
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 sqlplus / as sysdba 2.普通用户登录
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服务器有时候会断掉,所以写个shell脚本每五分钟去判断是否连接,于是就有下面的shell脚本。
BETWEEN 操作符选取介于两个值之间的数据范围内的值。这些值可以是数值、文本或者日期。
假如你已经使用过苹果开发者中心上架app,你肯定知道在苹果开发者中心的web界面,无法直接提交ipa文件,而是需要使用第三方工具,将ipa文件上传到构建版本,开...
下面的 SQL 语句指定了两个别名,一个是 name 列的别名,一个是 country 列的别名。**提示:**如果列名称包含空格,要求使用双引号或方括号:
在使用H5混合开发的app打包后,需要将ipa文件上传到appstore进行发布,就需要去苹果开发者中心进行发布。​
+----+--------------+---------------------------+-------+---------+
数组的声明并不是声明一个个单独的变量,比如 number0、number1、...、number99,而是声明一个数组变量,比如 numbers,然后使用 nu...
第一步:到appuploader官网下载辅助工具和iCloud驱动,使用前面创建的AppID登录。
如需删除表中的列,请使用下面的语法(请注意,某些数据库系统不允许这种在数据库表中删除列的方式):
前不久在制作win11pe,制作了一版,1.26GB,太大了,不满意,想再裁剪下,发现这次dism mount正常,commit或discard巨慢,以前都很快...
赛门铁克各个版本概览:https://knowledge.broadcom.com/external/article?legacyId=tech163829
实测Python 3.6.6用pip 21.3.1,再高就报错了,Python 3.10.7用pip 22.3.1是可以的
Broadcom Corporation (博通公司,股票代号AVGO)是全球领先的有线和无线通信半导体公司。其产品实现向家庭、 办公室和移动环境以及在这些环境...
发现个问题,server2016上安装了c4d这些版本,低版本的正常显示窗格,但红色圈出的高版本c4d打开后不显示窗格,
TAT:https://cloud.tencent.com/document/product/1340