100道前端面试题(七):获取字符串中连续最多的字符以及次数(TS+单元测试)

84 阅读1分钟

前言

1-1.webp

今天我们来研究一下如何获取字符串中连续最多的字符以及次数


任意输入一个字符串,统计字符串中连续最多的字符以及次数

请看示例1:

示例 1 :


输入:

str = 'aabbcccddeeee11223'

输出:一个包含所得字母和长度的对象

res = {
char: '',
length: 0
}

示例 2 :

输入:
str = 'abc'

输出:

res = {
   char: 'a',
   length: 1
   }

示例 3 :

输入:

str = 'aaa'

输出:

res = {
char: 'a',
length: 3
}


解题思路(方法一:嵌套循环)

  • 嵌套循环,遍历字符串,计算当前字符出现次数,依次使用一个对象统计字符出现个数

  • 当前一个字符出现次数,小于后一个字符出现次数,更新对象

  • 数组遍历完毕,返回一个对象

代码实现

export function findContinuousChar1(str: string) : IRes {
     const res = {
        char: "",
        length: 0
     }
     
     const length = str.length
     let tempLength = 0 
     if (length === 0) return res

     for (let i = 0; i < length; i++) {
        // 为什么要初始化tempLength?
        // 因为如果不初始化tempLength,就会累计数值越来越多
        // 自然,出现最多的字符串肯定是最后一位字符
        tempLength = 0
        for (let j = i; j < length; j++) {
            if (str[i] === str[j]) {
               tempLength++
            } 
            // 为什么此处不能使用else if?
            // 因为当字符为连续字符时,str[i] === str[j]一定满足
            // 如果使用else if ,那么永远不会进入else if这个循环,每次进入了if
            // 因此两个都要使用if,才能使得res里面的数据更新
             if (str[i] !== str[j] || j === (length - 1)) {
               if (tempLength > res.length) {
                
                   res.length = tempLength
                   res.char = str[i]
                   console.info(res)
               }  
               if (i < str.length - 1) {
                   i = j - 1
               }
               break
            }
       }
     }

    return res
}


解题思路(方法二:双指针)

  • 定义指针i和j,j不动,i保持移动

  • 如果i和j的值一直相等,则i保持移动

  • 如果i和j的值不等,记录次数,使得j追上i,重复第一步

代码实现

export function findContinuousChar2(str: string): IRes {
    const res: IRes = {
        char: '',
        length: 0
    }

    const length = str.length
    if (length === 0) return res

    let tempLength = 0 // 临时记录当前连续字符的长度
    let i = 0
    let j = 0

    // O(n)
    for (; i < length; i++) {
        
        if (str[i] === str[j]) {
            tempLength++
        }

        if (str[i] !== str[j] || i === length - 1) {
            // 不相等,或者 i 到了字符串的末尾
            if (tempLength > res.length) {
                res.char = str[j]
                res.length = tempLength
            }
            tempLength = 0 // reset

            if (i < length - 1) {
                j = i // 让 j “追上” i
                // 为什么i 要减一
                // 因为不减一的话,最后计算出来的次数就会少一次
                i-- // 细节
            }
        }
    }

    return res
 }

算法时间度分析

仍然先给出我们的结论:方法一和方法二的时间复杂度基本接近,都为O(n),我们再来逐步分析原因

方法一中,我们以字符串aaabbbccc为例,开始i不动,指向第一个变量a,只有变量j一直在遍历字符串,直到遍历到第一个b时,由于str[i] !== str[j],此时i = j - 1,发生了跳步,i直接指向最后一个a,如果字符连续的长度足够长,那么相当于,变量i始终没有参与到复杂度为O(n)的遍历中,因此,总的时间复杂度为O(n),虽然表面上是一个嵌套结构

方法二中,我们仍以字符串aaabbbccc为例,在双指针的结构中,时间复杂度就相当明显了,拢共就只有一个for循环遍历,初始,指针j定位在第一个a处,指针i一直向前移动,直到双方指向的变量不一致或者指针i指向字符串最后一位字符

当双方指向的变量不一致时,i直接与j指针相等,跳过遍历,因此在双指针中,相当于只有指针i参与了复杂度为O(n)的遍历,因此,总的时间复杂度为O(n)

我们依旧以实际性能测试的结果说话

测试环节

性能测试

// let str = ''
// for (let i = 0; i < 100 * 10000; i++) {
//     str += i.toString()
// }

// console.time('findContinuousChar1')
// findContinuousChar1(str)
// console.timeEnd('findContinuousChar1') // 219ms

// console.time('findContinuousChar2')
// findContinuousChar2(str)
// console.timeEnd('findContinuousChar2') // 228ms

可见,当数据量足够大时,方法一、二的性能接近

单元测试

import { findContinuousChar1, findContinuousChar2 } from './continuous-char'

describe('连续字符和长度', () => {
    it('正常情况', () => {
        const str = 'aabbcccddeeee11223'
        const res = findContinuousChar1(str)
        expect(res).toEqual({ char: 'e', length: 4 })
    })
    it('空字符串', () => {
        const res = findContinuousChar1('')
        expect(res).toEqual({ char: '', length: 0 })
    })
    it('无连续字符', () => {
        const str = 'abc'
        const res = findContinuousChar1(str)
        expect(res).toEqual({ char: 'a', length: 1 })
    })
    it('全部都是连续字符', () => {
        const str = 'aaa'
        const res = findContinuousChar1(str)
        expect(res).toEqual({ char: 'a', length: 3 })
    })
})

测试所需要的环境变量:

{
          "name": "interview-js-code",
          "version": "1.0.0",
          "description": "",
          "main": "index.js",
          "scripts": {
            "test": "jest --detectOpenHandles",
            "dev": "cross-env NODE_ENV=development webpack serve --config build/webpack.dev.js",
            "build": "cross-env NODE_ENV=production webpack --config build/webpack.prod.js",
            "build:analyzer": "cross-env NODE_ENV=production_analyzer webpack --config build/webpack.prod.js"
          },
          "keywords": [],
          "author": "",
          "license": "ISC",
          "devDependencies": {
            "@babel/core": "^7.13.14",
            "@babel/preset-env": "^7.13.12",
            "@types/jest": "^27.0.2",
            "autoprefixer": "^10.2.5",
            "babel-jest": "^27.3.0",
            "babel-loader": "^8.2.2",
            "clean-webpack-plugin": "^3.0.0",
            "cross-env": "^7.0.3",
            "css-loader": "^5.2.0",
            "html-webpack-plugin": "^5.3.1",
            "jest": "^27.3.0",
            "less": "^4.1.1",
            "less-loader": "^8.0.0",
            "postcss-loader": "^5.2.0",
            "style-loader": "^2.0.0",
            "ts-loader": "^8.1.0",
            "typescript": "^4.2.3",
            "url-loader": "^4.1.1",
            "webpack": "^5.30.0",
            "webpack-bundle-analyzer": "^4.4.0",
            "webpack-cli": "^4.6.0",
            "webpack-dev-server": "^3.11.2",
            "webpack-merge": "^5.7.3",
            "ts-jest": "^27.0.7"
          }
     }