← 返回手写题目录

🔤 字符串手写题 Top 10

反转 · 回文 · 驼峰 · 压缩 · 查找 · 统计 · 截断 · 模板 · 解析 · 随机

高频必练

1字符串反转(保留单词顺序 vs 整体反转)

场景:用户输入 "hello",输出 "olleh"。还要会反转句子里的单词顺序:"the sky is blue" → "blue is sky the"。

// ① 整体反转(最基础) function reverseStr(str) { return str.split('').reverse().join('') } reverseStr('hello') // 'olleh' // ② 反转单词顺序(空格分隔,单词内部不反转) function reverseWords(str) { return str.trim().split(/\s+/).reverse().join(' ') } reverseWords('the sky is blue') // 'blue is sky the' // ③ 不用 reverse 的手写版(双指针,体现底层思路) function reverseManual(str) { let arr = str.split('') let left = 0, right = arr.length - 1 while (left < right) { [arr[left], arr[right]] = [arr[right], arr[left]] // 交换 left++; right-- } return arr.join('') }
考点:split/join/reverse 三件套 + 双指针思路。面试官追问问"不用 reverse 怎么写"。
2判断回文字符串(忽略大小写和非字母数字)

场景:"A man, a plan, a canal: Panama" 是回文。常考题,要处理标点和空格。

function isPalindrome(str) { // ① 清洗:只保留字母数字,转小写 const cleaned = str.toLowerCase().replace(/[^a-z0-9]/g, '') // ② 双指针从两头往中间走 let left = 0, right = cleaned.length - 1 while (left < right) { if (cleaned[left] !== cleaned[right]) return false left++; right-- } return true } isPalindrome('A man, a plan, a canal: Panama') // true isPalindrome('race a car') // false // 简洁版(用反转对比) function isPalindrome2(str) { const s = str.toLowerCase().replace(/[^a-z0-9]/g, '') return s === s.split('').reverse().join('') }
陷阱:很多人忘了处理大小写、空格、标点。面试题干说"忽略大小写和非字母数字",正则 /[^a-z0-9]/g 是关键。
3驼峰与下划线/短横线互转

场景:后端返回 snake_case(user_name),前端要 camelCase(userName)。Vue/React 项目天天用。

// ① 下划线 → 驼峰 user_name → userName function toCamelCase(str) { return str.replace(/_[a-z]/g, match => match[1].toUpperCase()) } toCamelCase('user_name_age') // 'userNameAge' // ② 短横线 → 驼峰 border-color → borderColor function kebabToCamel(str) { return str.replace(/-(\w)/g, (_, c) => c.toUpperCase()) } // ③ 驼峰 → 下划线 userName → user_name function toSnakeCase(str) { return str.replace(/([A-Z])/g, '_$1').toLowerCase() } toSnakeCase('userName') // 'user_name' // ④ 驼峰 → 短横线 borderColor → border-color function camelToKebab(str) { return str.replace(/([A-Z])/g, '-$1').toLowerCase() }
正则技巧(\w) 捕获组配合 (_, c) => c.toUpperCase() 是替换大写的标准写法。$1 表示第一个捕获组。
4字符串压缩(aabcccccaaa → a2b1c5a3)

场景:连续相同字符压缩成"字符+次数"。LeetCode 经典题,考遍历和边界。

function compress(str) { if (!str) return '' let result = '' let count = 1 for (let i = 1; i <= str.length; i++) { // 当前字符和前一个不同,结算前一个 if (str[i] !== str[i - 1]) { result += str[i - 1] + count count = 1 // 重置计数 } else { count++ } } return result } compress('aabcccccaaa') // 'a2b1c5a3' // 进阶:压缩后更长就返回原串 function compressSmart(str) { const compressed = compress(str) return compressed.length < str.length ? compressed : str }
考点:遍历到 i <= length(注意等号,处理最后一组);不要忘记空字符串边界。
5最长无重复字符子串(滑动窗口经典)

场景:"abcabcbb" 最长无重复子串是 "abc",长度 3。LeetCode TOP3 高频,必须会。

// 思路:滑动窗口 + Set // 左指针 left,右指针 right 扩展;遇到重复就左指针收缩 function lengthOfLongestSubstring(str) { const set = new Set() let left = 0, max = 0 for (let right = 0; right < str.length; right++) { // 窗口里有重复字符,左指针右移直到移除重复 while (set.has(str[right])) { set.delete(str[left]) left++ } set.add(str[right]) max = Math.max(max, right - left + 1) } return max } lengthOfLongestSubstring('abcabcbb') // 3 lengthOfLongestSubstring('pwwkew') // 3 ('wke')
核心:滑动窗口双指针 O(n)。也可以用 Map 存字符最新位置,更优化。

业务实用

6统计字符串中每个字符出现次数

场景:"aabbbcc" → {a:2, b:3, c:2}。词频统计基础。

// 方法1:reduce(推荐,函数式) function charCount(str) { return str.split('').reduce((acc, char) => { acc[char] = (acc[char] || 0) + 1 return acc }, {}) } // 方法2:for 循环(直观) function charCount2(str) { const map = {} for (const char of str) { map[char] = (map[char] || 0) + 1 } return map } // 进阶:找出出现次数最多的字符 function topChar(str) { const count = charCount(str) return Object.entries(count) .sort((a, b) => b[1] - a[1])[0] } topChar('aabbbcc') // ['b', 3]
技巧acc[char] || 0 处理首次出现是 undefined 的情况,比 if 判断简洁。
7字符串截断 + 省略号(超出长度加...)

场景:列表卡片标题太长要截断显示。Vue 项目里 truncate(text, 20)

// 基础版:按字符数截断 function truncate(str, maxLen = 20) { if (str.length <= maxLen) return str return str.slice(0, maxLen) + '...' } truncate('这是一段很长很长的标题文字', 5) // '这是一段很...' // 进阶版:按字节截断(中文算2字节,避免半截中文) function truncateByBytes(str, maxBytes) { let bytes = 0, i = 0 for (; i < str.length; i++) { const code = str.charCodeAt(i) bytes += code > 127 ? 2 : 1 // 中文/全角算2 if (bytes > maxBytes) break } return str.slice(0, i) + (i < str.length ? '...' : '') }
注意:CSS 的 text-overflow: ellipsis 也能截断,但 JS 截断是为了后端返回数据时预处理好,或导出文件时用。
8简易模板字符串渲染({{name}} → 实际值)

场景:实现简化版 mustache 模板:"你好 {{name}},今年 {{age}} 岁" + 数据 → "你好小明,今年18岁"。

function render(template, data) { return template.replace(/\{\{(\w+)\}\}/g, (match, key) => { return data[key] !== undefined ? data[key] : '' }) } const tpl = '你好 {{name}},今年 {{age}} 岁,城市 {{city}}' render(tpl, { name: '小明', age: 18, city: '北京' }) // '你好 小明,今年 18 岁,城市 北京' // 进阶版:支持嵌套对象 {{user.name}} function renderDeep(template, data) { return template.replace(/\{\{(.+?)\}\}/g, (match, expr) => { return expr.trim().split('.').reduce((obj, key) => obj?.[key], data) ?? '' }) } renderDeep('{{user.name}}-{{user.address.city}}', { user: { name: '小红', address: { city: '上海' } } }) // '小红-上海'
考点:正则 + replace 第二参数为函数。(\w+) 只匹配单词,(.+?) 支持点号嵌套。
9金额格式化:1234567.89 → "1,234,567.89"

场景:支付页面、订单列表显示金额要千分位分隔。每 3 位加一个逗号。

// 方法1:正则(最简洁,面试推荐) function formatMoney(num) { return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') } formatMoney(1234567.89) // '1,234,567.89' // 方法2:原生 Intl.NumberFormat(生产推荐) const fmt = new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }) fmt.format(1234567.89) // '¥1,234,567.89' // 方法3:手写循环(体现思路) function formatManual(num) { const [intPart, decPart] = num.toString().split('.') let result = '' let intStr = intPart while (intStr.length > 3) { result = ',' + intStr.slice(-3) + result intStr = intStr.slice(0, -3) } return decPart ? intStr + result + '.' + decPart : intStr + result }
正则解析\B 非单词边界;(?=(\d{3})+) 正向预查,后面每 3 位数字的位置;(?!\d) 后面不再是数字(处理小数点后)。
10生成随机字符串(指定长度,可指定字符集)

场景:生成临时 ID、邀请码、验证码、文件名随机后缀。

// 方法1:Math.random + toString(36)(短小精悍) function randomStr(len = 8) { return Math.random().toString(36).slice(2, 2 + len) } randomStr(6) // 'a3f2k9' 左右 // 方法2:指定字符集(更可控,比如验证码只要数字) function randomFromCharset(len, charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') { let result = '' for (let i = 0; i < len; i++) { result += charset.charAt(Math.floor(Math.random() * charset.length)) } return result } // 方法3:纯数字验证码 const smsCode = () => Math.random().toString().slice(2, 8) // 6位数字 // 方法4:UUID v4(安全随机,适合做主键) const uuid = () => crypto.randomUUID() // 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
选型:临时 ID 用方法1;验证码用方法2/3;生产环境主键用方法4 (crypto.randomUUID,密码学安全)。
🎯 字符串题复习重点:① 滑动窗口(无重复子串)② 正则替换(驼峰/模板/金额)③ 双指针(回文/反转)④ reduce 统计。
正则相关题是字符串题里最高频的考点,把 replace 配合函数、捕获组、预查这几个语法搞透。