【算法10天:Day10】第四章字符串 LeetCode 重复的子字符串(459)

81 阅读2分钟

题目一:

image.png

解法一:(拼接)

解题思路: 如果一个字符串s是由其子串重复多次构成,那么s+s中间必定存在一个s,判断之前需要先将首尾字符去掉,避免匹配判断时,检测到原有字符串s。

var repeatedSubstringPattern = function(s) {
    let str = `${s}${s}`
    str = str.slice(1)
    str = str.slice(0, str.length - 1)
    if(str.indexOf(s) !== -1) {
        return true
    } else {
        return false
    }
};

// 或者

var repeatedSubstringPattern = function(s) {
    let s1 = (s + s).slice(1, -1);
    return s1.indexOf(s) != -1;
};

解法二:(KMP算法)

解题思路:

1. 先计算出字符串s的next数组
2. 题意中重复子串长度为 (字符串长度 - 最长相等前后缀长度),即 s.length - next[s.length - 1]
3. 如果s是由其子串重复构成,那么字符串s的长度对重复子串长度取余一定为0
4. 即为 s.length % (s.length - next[s.length - 1]) === 0,这是判断是否重复子串构成的关键判断条件
var repeatedSubstringPattern = function(s) {
    var getNext = function(s) {
        let j = 0
        let next = []
        next.push(j)
        
        for (let i = 1; i < s.length; i++) {
            while(j > 0 && s[j] !== s[i]) {
                j = next[j - 1]
            }
            if(s[j] === s[i]) {
                j++
            }
            next.push(j)
        }
        return next
    }
    let next = getNext(s)
    if(next[s.length - 1] !==0 && s.length % (s.length - next[s.length - 1]) === 0) {
        return true
    } else {
        return false
    }
};

题外话:整体思路我其实差不多,但是这个条件没有找准确,我已经打印出字符串的next数组,但是没有准确找出其中的关系。

解法四:(API repeat)

const repeatedSubstringPattern = (s) => {
  // 1. 设置 s 的长度 length
  const length = s.length

  // 2. 设置每次累加的长度
  let str = ''

  // 3. 遍历字符串
  for (let i = 0; i < Math.floor(s.length / 2); i++) {
    // 3.1 累加字符串
    str += s[i]
    // 3.2 判断是否为重复的长度
    if (s === str.repeat(Math.floor(length / str.length))) {
      return true
    }
  }

  // 4. 如果不存在,则返回 false
  return false
};

解法五: (正则匹配)

var repeatedSubstringPattern = function(s) {
    return /^([a-z]+)\1+$/.test(s)
};

总结:

JS字符串方法集合

1、转换为字符串类型 toString()

    (1)var num=110;
    var n=num.toString();    //"110"
    (2)var num=111;
    var n=String(num);       //"111"
    (3)var num=112;
    var n="" + num;          //"112"

2、字符串分割返回新的数组 split()

1)var str="qingchenghuwoguoxiansheng,woaishenghuo,woaiziji";
    var arr1=str.split(",");    //["qingchenghuwoguoxiansheng","woaishenghuo","woaiziji"];
    var arr2=str.split("");        //["q","i","n","g","c","h","e","n","g","h","u","w","o","g","u","o","x","i",
    "a","n","s","h","e","n","g",",","w","o","a","i","s","h","e","n","g","h","u",
    "o",",","w","o","a","i","z","i","j","i"];
(2split()的第二个参数,表示返回的字符串数组的最大长度
    var str="qingchenghuwoguoxiansheng,woaishenghuo,woaiziji";
    var arr1=str.split(",",2); //["qingchenghuwoguoxiansheng","woaishenghuo"];
    var arr2=str.split("",8); //["q","i","n","g","c","h","e","n"];

3、查询字符串 indexOf() lastIndexOf()

判断字符串内是否包含子串

  • (1)indexOf(),该Of() 方法对大小写敏感。返回字符串中一个子串第一处出现的索引(从左到右搜索)。如果没有匹配项,返回 -1
  • (2)lastIndexOf(),该方法对大小写敏感。返回字符串中一个子串最后一处出现的索引(从右到左搜索),如果没有匹配项,返回 -1 。

4、返回指定位置的字符或其字符编码值 charAt()、charCodeAt()

(1)var str="Hello World!";
     var index=str.charAt(7); //o
将整个字符串中第七位查找出来(查找的是下标)
(2)查找对应位置字符的编码值
    var str="Hello World!";
var charCode=str. charCodeAt(7);    //111

5、字符串匹配 match()

Match函数  match();
var str="hi,mynameisguoxiansheng6,33iswho?";
var n=str.match("guo");    //guo
var n1=str.match("Guo");    //null
var zz1=/\d+/g;//正则判断的是全局匹配0-9的数字
var zz2=/guo/g;//正则判断的是全局匹配guo
var zz3=/guo/;
var n2=str.match(zz1);    //["6","33"]
var n3=str.match(zz2);    //["guo"]
var n3=str.match(zz3);    //["guo",index:11,input:"hi,mynameisguoxiansheng6,33iswho?"]guo字母的下标处于第11位,
并返回原来的字符串;
n3.index    //11
n3.input    //hi,mynameisguoxiansheng6,33iswho?
match函数在字符串上调用
被匹配的字符串内包含要匹配的字符串时,返回所要匹配的字符串
如果使用正则匹配字符串时,如果正则表达式没有 g (全局标识)标志,返回与正则匹配相同的结果。而且返回的数组拥有一个额
外的 input 属性,该属性包含原始字符串。另外,还拥有一个 index 属性,该属性表示匹配结果在被字符串中的索引(以0开
始)。如果正则表达式包含 g 标志,则该方法返回匹配字符串的数组。
exec函数   exec()
var str="hi,mynameisguoxiansheng6,33iswho?";
var zz1=/guo/g;
var n=zz1.exec(str);  //["guo"]
var zz2=/guo/;
var n1=zz2.exec(str);    //["guo",index:11,input:"hi,mynameisguoxiansheng6,33iswho?"]
n1.index    //11
n1.input    //hi,mynameisguoxiansheng6,33iswho?
exec()函数是在正则上调用,传递字符串的参数
同上,返回第一个匹配成功的字符串,如果匹配失败则返回null。
  Search函数  search()
      var str = "hi,mynameisguoxiansheng6,33iswho?";
var zz1 = /guo/;
var n = str.search(zz1);    //11
进行正则匹配查找。如果查找成功,返回字符串中匹配的索引值。否则返回 -1=

6、字符串连接concat()

直接用加号拼接
Concat函数 concat()
var str1="Hello";
var str2=" world,";
var str3="Hello";
var str4="guoxiansheng";
var newStr=str1.concat(str2+str3+" "+str4);    //Hello world,Hello guoxiansheng
concat()函数可以有多个参数,传递多个字符串,拼接多个字符串。

7、字符串的切割和提取slice()

//第一种,slice()函数:
var str="hello world!";
var n1=str.slice(-3);    //ld!
var n2=str.slice(-3,-1);    //ld
var n3=str.slice(3);    //lo world!
var n4=str.slice(3,7);    //lo w
slice() 可以为负数,如果起始位置为负数,则从字符串最后一位向前找对应位数并且向后取结束位置,如果为正整数则从前
往后取起始位置到结束位置。

//第二种:substring()函数:
var str="hello world!";
var n1=str.substring(3);    //lo world!
var n2=str.substring(3,7);    //lo w
substring()只能非负整数,截取起始结束位置同slice()函数一致。

//第三种:substr()函数:
var str="hello world!";
var n1=str.substr(3);    //lo world!
var n2=str.substr(3,7);    //lo wo
substr()与第一、第二种函数不同,从起始位置开始截取,结束位置为第二个参数截取的字符串最大长度。
以上三种函数未填第二参数时,自动截取起始位置到字符串末尾。

8、字符串大小写转换toLowerCase()、toUpperCase()

var mystr="Hello World!";
var lowCaseStr=mystr.toLowerCase();    //hello world!
var upCaseStr=mystr. toUpperCase();    //HELLO WORLD!

9、字符串去空格trim()

trim方法用来删除字符串前后的空格
var mystr="     hello world      ";
var trimStr=mystr.trim();    //hello world

10、字符串去重复

var str="aahhgggsssjjj";
function removeRepeat(n){
var k=[];
var arr=n.split("");
for(var i=0;i<arr.length;i++){
    if(k.indexOf(arr[i])==-1){
        k.push(arr[i]);
    }
}
return k.join("");
}
removeRepeat(str);    //ahgsj

11、replace()

  • replace() 方法用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。
var str="abc Def!"
console.log(str.replace(/abc/, "CBA"))//CBA Def!

12.search()

search() 方法用于检索字符串中指定的子字符串,或检索与正则表达式相匹配的子字符串。要执行忽略大小写的检索,请追加标志 i。如果没有找到任何匹配的子串,则返回 -1。

var str="abc DEF!"
console.log(str.search(/DEF/))//4

13.slice()

提取字符串的片断,并在新的字符串中返回被提取的部分。

stringObject.slice(start,end);

start :要抽取的片断的起始下标。如果是负数,则该参数规定的是从字符串的尾部开始算起的位置。也就是说,-1 指字符串的最后一个字符,-2 指倒数第二个字符,以此类推。 end:紧接着要抽取的片段的结尾的下标。若未指定此参数,则要提取的子串包括 start 到原字符串结尾的字符串。如果该参数是负数,那么它规定的是从字符串的尾部开始算起的位置。

var str="abc def ghk"
console.log(str.slice(6))//f ghk

14.substr()

从起始索引号提取字符串中指定数目的字符。

stringObject.substr(start,length)。

start:必需。要抽取的子串的起始下标。必须是数值。如果是负数,那么该参数声明从字符串的尾部开始算起的位置。也就是说,-1 指字符串中最后一个字符,-2 指倒数第二个字符,以此类推。 length:可选。子串中的字符数。必须是数值。如果省略了该参数,那么返回从 stringObject 的开始位置到结尾的字串。

var str="abc def"
console.log(str.substr(2))//c def
console.log(str.substr(2,4))// c de

15.substring()

提取字符串中两个指定的索引号之间的字符。

stringObject.substring(start,stop)。

start :必需。一个非负的整数,规定要提取的子串的第一个字符在 stringObject 中的位置。 stop :可选。一个非负的整数,比要提取的子串的最后一个字符在 stringObject 中的位置多 1。如果省略该参数,那么返回的子串会一直到字符串的结尾。

var str="abc def"
console.log(str.substring(2))//c def
console.log(str.substring(2,4))// c

相同点:如果只是写一个参数,两者的作用都一样:都是是截取字符串从当前下标以后直到字符串最后的字符串片段。

substr(startIndex);
substring(startIndex);
var str = '123456789';
console.log(str.substr(2));    //  "3456789"
console.log(str.substring(2)) ;//  "3456789"

不同点:第二个参数 substr(startIndex,lenth): 第二个参数是截取字符串的长度(从起始点截取某个长度的字符串); substring(startIndex, endIndex): 第二个参数是截取字符串最终的下标 (截取2个位置之间的字符串,‘含头不含尾’)。

console.log("123456789".substr(2,5));    //  "34567"
console.log("123456789".substring(2,5)) ;//  "345"

16.codePointAt()

let s = '?a';
s.codePointAt(0) // 134071
s.codePointAt(1) // 57271
s.codePointAt(2) // 97

codePointAt方法的参数,是字符在字符串中的位置(从 0 开始)。上面代码中,JavaScript 将“?a”视为三个字符,codePointAt 方法在第一个字符上,正确地识别了“?”,返回了它的十进制码点 134071(即十六进制的20BB7)。在第二个字符(即“?”的后两个字节)和第三个字符“a”上,codePointAt方法的结果与charCodeAt方法相同。

17.String.fromCodePoint()

ES5 提供String.fromCharCode方法,用于从码点返回对应字符,但是这个方法不能识别 32 位的 UTF-16 字符(Unicode 编号大于0xFFFF)。

String.fromCharCode(0x20BB7)
// “ஷ”

上面代码中,String.fromCharCode不能识别大于0xFFFF的码点,所以0x20BB7就发生了溢出,最高位2被舍弃了,最后返回码点U+0BB7对应的字符,而不是码点U+20BB7对应的字符。 ES6 提供了String.fromCodePoint方法,可以识别大于0xFFFF的字符,弥补了String.fromCharCode方法的不足。在作用上,正好与codePointAt方法相反。

String.fromCodePoint(0x20BB7)
// "?"
String.fromCodePoint(0x78, 0x1f680, 0x79) === 'x\uD83D\uDE80y'
// true

18.字符串的遍历器接口 for of

for (let codePoint of 'abc') {
  console.log(codePoint)
}
// "a"
// "b"
// "c"

除了遍历字符串,这个遍历器最大的优点是可以识别大于0xFFFF的码点,传统的for循环无法识别这样的码点。

19.at()

at方法可以识别Unicode 编号大于0xFFFF的字符,返回正确的字符。

‘abc’.at(0)//"a"
'吉'.at(0)//"吉"

20.normalize()

许多欧洲语言有语调符号和重音符号。为了表示它们,Unicode 提供了两种方法。一种是直接提供带重音符号的字符,比如Ǒ(u01D1)。另一种是提供合成符号(combining character),即原字符与重音符号的合成,两个字符合成一个字符,比如O(u004F)和ˇ(u030C)合成Ǒ(u004Fu030C)。

这两种表示方法,在视觉和语义上都等价,但是 JavaScript 不能识别。

'\u01D1'==='\u004F\u030C' //false
'\u01D1'.length // 1
'\u004F\u030C'.length // 2

上面代码表示,JavaScript 将合成字符视为两个字符,导致两种表示方法不相等。 ES6 提供字符串实例的normalize()方法,用来将字符的不同表示方法统一为同样的形式,这称为 Unicode 正规化。

'\u01D1'.normalize() === '\u004F\u030C'.normalize()
// true

22.includes(), startsWith(), endsWith()

传统上,JavaScript 只有indexOf方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6 又提供了三种新方法。

includes():返回布尔值,表示是否找到了参数字符串。
startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
let s = 'Hello world!';
s.startsWith('Hello') // true
s.endsWith('!') // true
s.includes('o') // true

这三个方法都支持第二个参数,表示开始搜索的位置。

let s = 'Hello world!';
s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true
s.includes('Hello', 6) // false

上面代码表示,使用第二个参数n时,endsWith的行为与其他两个方法有所不同。它针对前n个字符,而其他两个方法针对从第n个位置直到字符串结束。

23.repeat()

repeat方法返回一个新字符串,表示将原字符串重复n次。

'x'.repeat(3) // "xxx"
'hello'.repeat(2) // "hellohello"
'na'.repeat(0) // ""

参数如果是小数,会被取整。

'na'.repeat(2.9) // "nana"

如果repeat的参数是负数或者Infinity,会报错。

'na'.repeat(Infinity)
// RangeError
'na'.repeat(-1)
// RangeError

24.padStart(),padEnd()

ES2017 引入了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。padStart()用于头部补全,padEnd()用于尾部补全。

'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'

'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'

上面代码中,padStart和padEnd一共接受两个参数,第一个参数用来指定字符串的最小长度,第二个参数是用来补全的字符串。 如果原字符串的长度,等于或大于指定的最小长度,则返回原字符串。

'xxx'.padStart(2, 'ab') // 'xxx'
'xxx'.padEnd(2, 'ab') // 'xxx'

如果用来补全的字符串与原字符串,两者的长度之和超过了指定的最小长度,则会截去超出位数的补全字符串。

'abc'.padStart(10, '0123456789')
// '0123456abc'

如果省略第二个参数,默认使用空格补全长度。

'x'.padStart(4) // '   x'
'x'.padEnd(4) // 'x   '

padStart的常见用途是为数值补全指定位数。下面代码生成 10 位的数值字符串。

'1'.padStart(10, '0') // "0000000001"
'12'.padStart(10, '0') // "0000000012"
'123456'.padStart(10, '0') // "0000123456"

另一个用途是提示字符串格式。

'12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"
'09-12'.padStart(10, 'YYYY-MM-DD') // "YYYY-09-12"

24.matchAll()

matchAll方法返回一个正则表达式在当前字符串的所有匹配。

25.模板字符串

模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。-----字符串模板,工作中用到比较多。

// 普通字符串
`In JavaScript '\n' is a line-feed.`

// 多行字符串
`In JavaScript this is
 not legal.`

console.log(`string text line 1
string text line 2`);

// 字符串中嵌入变量
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`

上面代码中的模板字符串,都是用反引号表示。如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。

let greeting = `\`Yo\` World!`;

如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。

$('#list').html(`
<ul>
  <li>first</li>
  <li>second</li>
</ul>
`);

上面代码中,所有模板字符串的空格和换行,都是被保留的,比如ul标签前面会有一个换行。如果你不想要这个换行,可以使用trim方法消除它。

$('#list').html(`
<ul>
  <li>first</li>
  <li>second</li>
</ul>
`.trim());

模板字符串中嵌入变量,需要将变量名写在${}之中。

function authorize(user, action) {
  if (!user.hasPrivilege(action)) {
    throw new Error(
      // 传统写法为
      // 'User '
      // + user.name
      // + ' is not authorized to do '
      // + action
      // + '.'
      `User ${user.name} is not authorized to do ${action}.`);
  }
}

大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性。

let x = 1;
let y = 2;

`${x} + ${y} = ${x + y}`
// "1 + 2 = 3"

`${x} + ${y * 2} = ${x + y * 2}`
// "1 + 4 = 5"

let obj = {x: 1, y: 2};
`${obj.x + obj.y}`
// "3"

模板字符串之中还能调用函数。

function fn() {
  return "Hello World";
}
`foo ${fn()} bar`
// foo Hello World bar

如果大括号中的值不是字符串,将按照一般的规则转为字符串。比如,大括号中是一个对象,将默认调用对象的toString方法。 如果模板字符串中的变量没有声明,将报错。

// 变量place没有声明
let msg = `Hello, ${place}`;
// 报错

由于模板字符串的大括号内部,就是执行 JavaScript 代码,因此如果大括号内部是一个字符串,将会原样输出。

`Hello ${'World'}`
// "Hello World"

模板字符串甚至还能嵌套。

const tmpl = addrs => `
  <table>
  ${addrs.map(addr => `
    <tr><td>${addr.first}</td></tr>
    <tr><td>${addr.last}</td></tr>
  `).join('')}
  </table>
`;