第03篇:字符串入门
概述:为什么字符串是算法题里的高频主角
学完数组之后,字符串通常就是下一站。
原因并不复杂:
- 很多题目的输入本质上就是一段文本
- 字符串底层往往和数组处理思路高度相似
- 双指针、哈希表、滑动窗口、KMP 等经典算法,很多都和字符串密切相关
- 面试中非常喜欢考察字符串,因为它能同时检验遍历能力、边界处理能力和代码细节能力
这篇文章的目标不是单纯讲String的语法,而是帮助你建立字符串题最基础的 4 种意识:
- 字符串本质上也是一种可遍历的数据序列
- 字符统计类问题通常围绕“计数”展开
- 字符串反转类问题通常围绕“双指针”展开
- 字符匹配类问题通常围绕“比较、扫描、定位”展开
学完这篇,你应该能看懂大多数基础字符串题到底在考什么。
核心概念:字符串到底是什么
字符串可以理解为:由多个字符按顺序组成的一段文本数据。
例如:
Strings="algorithm";这里的s就是一个字符串,它由多个字符依次组成:
a l g o r i t h m如果从算法角度看,字符串和数组非常像:
- 都可以按位置访问元素
- 都可以从头到尾遍历
- 都经常会用到下标
- 都很依赖边界判断
不同点在于,字符串处理的对象是字符,而不是普通整数。
在 Java 中,如果你想访问字符串中的某个字符,最常见的方式是:
Strings="hello";charch=s.charAt(1);// e字符串长度则通过length()获取:
intn=s.length();原理:字符串题为什么特别考验细节
数组题里,很多元素比较的是数值大小;而字符串题里,除了遍历,你还会频繁遇到这些问题:
- 当前字符是不是字母
- 是否和另一个字符相等
- 大小写是否要统一
- 空格和标点是否需要跳过
- 比较的是字符、子串,还是整个字符串
所以字符串题经常看起来简单,但特别容易在细节上丢分。
下面这些错误,新手非常常见:
- 把
==当作字符串内容比较 - 忘记处理空字符串
- 下标访问越界
- 反转时左右指针没有收缩好
- 子串匹配时边界条件写错
字符串题的难点,往往不在思路本身,而在于细节是否严谨。
基础操作:先把字符串遍历吃透
和数组一样,字符串题里最常见的第一步也是遍历。
1. 从前往后遍历字符串
publicstaticvoidprintChars(Strings){for(inti=0;i<s.length();i++){System.out.println(s.charAt(i));}}这类写法非常常见,适用于:
- 统计字符种类
- 判断字符是否合法
- 查找某个字符第一次出现的位置
- 比较相邻字符关系
时间复杂度通常是:
O(n)其中n表示字符串长度。
2. 转成字符数组再处理
有些题目需要频繁修改字符,这时会更适合先转成字符数组。
publicstaticvoidprintCharsByArray(Strings){char[]chars=s.toCharArray();for(charch:chars){System.out.println(ch);}}为什么这样做?
因为在 Java 中,String是不可变的。
如果你频繁拼接或修改,直接操作String往往不如操作char[]或StringBuilder更合适。
第一类高频题:字符统计
字符统计类问题是字符串入门里最常见的一种。
它的核心通常只有一句话:
遍历字符串,并记录每个字符出现了多少次。
示例 1:统计某个字符出现的次数
publicstaticintcountChar(Strings,chartarget){intcount=0;for(inti=0;i<s.length();i++){if(s.charAt(i)==target){count++;}}returncount;}例如:
intans=countChar("banana",'a');// 3这类题的关键很简单:
- 准备一个计数变量
- 遍历字符串
- 满足条件就累加
时间复杂度是:
O(n)空间复杂度是:
O(1)示例 2:统计每个字符出现的频率
如果题目要求统计所有字符出现次数,最常见会用哈希表。
importjava.util.HashMap;importjava.util.Map;publicstaticMap<Character,Integer>countFrequency(Strings){Map<Character,Integer>freq=newHashMap<>();for(inti=0;i<s.length();i++){charch=s.charAt(i);freq.put(ch,freq.getOrDefault(ch,0)+1);}returnfreq;}这种题非常经典,后面很多字符串题都会建立在“先统计频率”这个动作之上,比如:
- 判断两个字符串是否为字母异位词
- 找到第一个不重复字符
- 统计出现次数最多的字符
示例 3:只统计英文字母
有些题会要求你忽略数字、空格或标点,这时就要先做字符判断。
publicstaticintcountLetters(Strings){intcount=0;for(inti=0;i<s.length();i++){charch=s.charAt(i);if(Character.isLetter(ch)){count++;}}returncount;}这类题提醒你一件事:
字符串题经常不只是“遍历”,而是“遍历 + 条件筛选”。
第二类高频题:字符串反转
字符串反转是非常典型的入门题,但它背后对应的是非常重要的双指针思想。
示例 1:使用StringBuilder反转
如果只是快速得到反转结果,Java 中最直接的写法是:
publicstaticStringreverseString(Strings){returnnewStringBuilder(s).reverse().toString();}这个写法非常简洁,适合工程开发或快速处理。
但如果你正在学算法,重点不应该只停在“会调用 API”,而是要理解:
如果不用现成方法,如何自己实现反转?
示例 2:双指针手写反转
publicstaticStringreverseByTwoPointers(Strings){char[]chars=s.toCharArray();intleft=0;intright=chars.length-1;while(left<right){chartemp=chars[left];chars[left]=chars[right];chars[right]=temp;left++;right--;}returnnewString(chars);}这个过程的本质是:
- 左指针从头开始
- 右指针从尾开始
- 两边字符交换
- 指针不断向中间靠拢
例如字符串:
hello交换过程大致是:
h e l l o o e l l h o l l e h时间复杂度通常是:
O(n)空间复杂度通常是:
O(n)因为这里转成了字符数组。
反转类题的常见变形
后续你会遇到很多反转变形题:
- 只反转单词顺序
- 只反转元音字母
- 每
k个字符反转一次 - 判断一个字符串是否为回文
虽然题目外观不同,但核心仍然是:
双指针 + 边界处理。
第三类高频题:字符串匹配
所谓字符串匹配,最基础的理解就是:
判断某个字符、某个子串,是否出现在另一个字符串中。
这是字符串题里极高频的一类。
示例 1:查找字符第一次出现的位置
publicstaticintfirstIndexOfChar(Strings,chartarget){for(inti=0;i<s.length();i++){if(s.charAt(i)==target){returni;}}return-1;}这本质上就是线性扫描,复杂度通常为O(n)。
示例 2:判断一个字符串是否包含某个子串
如果使用 Java 自带方法,可以直接这样写:
publicstaticbooleancontainsSubString(Strings,Stringtarget){returns.contains(target);}但从算法学习角度,更重要的是知道最基础的暴力匹配思路。
示例 3:手写最基础的子串匹配
publicstaticintindexOfSubString(Stringtext,Stringpattern){intn=text.length();intm=pattern.length();for(inti=0;i<=n-m;i++){intj=0;while(j<m&&text.charAt(i+j)==pattern.charAt(j)){j++;}if(j==m){returni;}}return-1;}这个写法的含义是:
- 枚举主串中的每一个可能起点
- 从该起点开始逐个字符比较
- 如果全部匹配成功,就返回起始位置
例如:
intindex=indexOfSubString("algorithm","go");// 2这就是最原始的字符串匹配思想。
后面你学到 KMP 时,会发现 KMP 本质上也是在解决“匹配时如何少回退”这个问题。
一道综合基础题:判断回文字符串
字符串入门里有一类非常经典的综合题,就是回文判断。
所谓回文,就是正着读和反着读都一样,例如:
levelradarabba
示例:判断字符串是否为回文
publicstaticbooleanisPalindrome(Strings){intleft=0;intright=s.length()-1;while(left<right){if(s.charAt(left)!=s.charAt(right)){returnfalse;}left++;right--;}returntrue;}这道题非常重要,因为它同时考察了:
- 字符串访问
- 双指针移动
- 边界判断
- 提前返回
如果你能把这道题真正写熟,很多基础字符串题都会轻松很多。
字符串题的常见套路:先分类,再选方法
字符串题看起来很多,其实初学阶段大致可以先分成下面几类:
| 题型 | 常见方法 | 典型关键词 |
|---|---|---|
| 字符统计类 | 遍历、计数、哈希表 | 次数、频率、重复 |
| 反转类 | 双指针、字符数组 | 反转、回文、交换 |
| 匹配类 | 扫描、比较、定位 | 包含、查找、子串 |
| 过滤处理类 | 条件判断、跳过无效字符 | 空格、标点、大小写 |
| 构造类 | StringBuilder | 拼接、生成新字符串 |
字符串题不要上来就硬写,先判断题型,再决定用遍历、双指针、哈希表还是构造。
易错点:新手做字符串题最容易踩的坑
1. 用==比较字符串内容
在 Java 中:
==比较的是引用地址equals()比较的是字符串内容
例如:
Stringa="abc";Stringb=newString("abc");System.out.println(a.equals(b));// true如果你要比较内容,优先考虑equals()。
2. 忘记处理空字符串
例如:
Strings="";很多人写代码时默认字符串一定有内容,结果一访问charAt(0)就出错。
3.charAt(i)越界
字符串下标范围是:
0 ~ s.length() - 1任何访问超出这个范围,都会抛异常。
4. 反转时忘记移动指针
双指针题里,如果忘了写:
left++;right--;程序就可能陷入死循环。
5. 子串匹配时边界写错
例如暴力匹配中,外层循环常见写法应该是:
i<=n-m如果写错,就可能漏掉最后一个可能起点,或者直接越界。
6. 频繁用+拼接长字符串
在循环中不断使用字符串相加,性能通常不理想。
如果需要反复构造字符串,更推荐使用StringBuilder。
publicstaticStringbuildString(intn){StringBuildersb=newStringBuilder();for(inti=0;i<n;i++){sb.append(i);}returnsb.toString();}7. 没有先想清楚“题目比较的到底是什么”
字符串题里常见的比较对象有:
- 字符
- 整个字符串
- 子串
- 忽略大小写后的结果
- 去掉非字母数字后的结果
如果不先想清楚,后面代码很容易越写越乱。
复杂度总结:字符串基础操作要有直觉
下面这些复杂度,建议尽快形成条件反射:
| 操作 | 常见时间复杂度 | 说明 |
|---|---|---|
| 按下标访问字符 | O(1) | 可以直接通过位置读取 |
| 遍历整个字符串 | O(n) | 逐个字符处理 |
| 统计字符次数 | O(n) | 一次扫描即可 |
| 反转字符串 | O(n) | 每个字符最多处理一次 |
| 暴力子串匹配 | O(n * m) | 主串和模式串逐步比较 |
这里的:
n表示主串长度m表示模式串长度
总结:字符串入门,关键不是 API,而是套路意识
很多人学字符串时,会把注意力放在各种 Java 字符串方法上。
这些方法当然重要,但如果只停留在“背 API”,算法能力很难真正提升。
这篇文章你应该清楚的结论:
- 字符串本质上也是一种可遍历的数据序列
- 字符统计类问题,核心是“遍历 + 计数”
- 反转类问题,核心往往是“双指针”
- 匹配类问题,核心是“扫描 + 比较 + 边界”
- 很多字符串题的难点,不在思路,而在细节处理
字符串题表面看是在处理文字,实际上考的是遍历、比较、边界和套路识别能力。
