1. 字符串处理基础与双指针技巧
字符串处理是编程中最基础也最常遇到的问题之一。在算法训练中,字符串题目往往能考察程序员对基础数据结构的掌握程度,以及对边界条件的处理能力。今天我们就来深入探讨字符串处理中的核心技巧——双指针法。
字符串在内存中的存储方式决定了它的许多特性。在C/C++中,字符串是以'\0'结尾的字符数组;在Java中,String是不可变对象;在Python中,字符串同样是不可变序列。理解这些底层特性对于高效处理字符串至关重要。
提示:虽然不同语言中字符串的实现方式不同,但大多数算法题的解法思路是通用的,关键在于掌握核心算法思想而非特定语言的API。
双指针技巧在字符串处理中尤为实用。它通过维护两个指针(通常一个快指针、一个慢指针)来高效地完成字符串的遍历和修改。这种方法可以将许多O(n²)的暴力解法优化到O(n)的时间复杂度。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 字符串反转的多种实现方式
2.1 经典的双指针反转法
字符串反转是最基础的字符串操作,也是理解双指针法的绝佳起点。我们来看一个标准的实现:
cpp复制void reverseString(vector<char>& s) {
int left = 0, right = s.size() - 1;
while (left < right) {
swap(s[left++], s[right--]);
}
}
这个实现中,left指针从头部开始,right指针从尾部开始,每次交换它们指向的字符,然后向中间移动,直到相遇。这种方法的时间复杂度是O(n),空间复杂度是O(1),是最优解。
2.2 处理特定条件下的反转问题
在实际问题中,我们常常需要处理更复杂的反转场景。例如,只反转字符串中的元音字母:
python复制def reverseVowels(s: str) -> str:
vowels = set('aeiouAEIOU')
s = list(s)
left, right = 0, len(s) - 1
while left < right:
while left < right and s[left] not in vowels:
left += 1
while left < right and s[right] not in vowels:
right -= 1
s[left], s[right] = s[right], s[left]
left += 1
right -= 1
return ''.join(s)
这个例子展示了双指针法的灵活性——我们可以在移动指针时加入额外的条件判断,实现更复杂的逻辑。
3. 字符串中的单词反转问题
3.1 整体思路与常见陷阱
给定一个字符串,要求反转字符串中每个单词的顺序,但保留单词间的空格和初始单词顺序。例如:"the sky is blue" → "blue is sky the"。
这个问题看似简单,但隐藏着几个常见的陷阱:
- 字符串首尾可能有空格
- 单词间可能有多个连续空格
- 需要处理标点符号的情况
3.2 分步解决方案
我们可以采用三步走策略:
- 去除多余空格
- 反转整个字符串
- 反转每个单词
具体实现如下:
cpp复制string reverseWords(string s) {
// 去除多余空格
int slow = 0, fast = 0;
// 去掉字符串前面的空格
while (s[fast] == ' ' && fast < s.size()) fast++;
// 去掉单词间多余空格
for (; fast < s.size(); fast++) {
if (fast > 0 && s[fast] == s[fast-1] && s[fast] == ' ') continue;
s[slow++] = s[fast];
}
// 去掉字符串末尾空格
if (slow > 0 && s[slow-1] == ' ') slow--;
s.resize(slow);
// 反转整个字符串
reverse(s.begin(), s.end());
// 反转每个单词
int start = 0;
for (int i = 0; i <= s.size(); i++) {
if (i == s.size() || s[i] == ' ') {
reverse(s.begin() + start, s.begin() + i);
start = i + 1;
}
}
return s;
}
注意:在处理字符串空格时,快慢指针法是最高效的方式,避免了频繁的字符串拼接操作。
4. KMP算法在字符串匹配中的应用
4.1 为什么需要KMP算法
当我们需要在一个字符串(主串)中查找另一个字符串(模式串)是否出现时,暴力解法的时间复杂度是O(m×n)。KMP算法通过预处理模式串,构建next数组,将时间复杂度优化到O(m+n)。
4.2 next数组的构建原理
next数组表示模式串中"前缀"和"后缀"的最长公共元素长度。构建过程如下:
cpp复制void getNext(int* next, const string& s) {
int j = 0;
next[0] = 0;
for (int i = 1; i < s.size(); i++) {
while (j > 0 && s[i] != s[j]) {
j = next[j - 1];
}
if (s[i] == s[j]) {
j++;
}
next[i] = j;
}
}
4.3 实际应用示例
实现strStr()函数(查找子串位置):
cpp复制int strStr(string haystack, string needle) {
if (needle.size() == 0) return 0;
int next[needle.size()];
getNext(next, needle);
int j = 0;
for (int i = 0; i < haystack.size(); i++) {
while (j > 0 && haystack[i] != needle[j]) {
j = next[j - 1];
}
if (haystack[i] == needle[j]) {
j++;
}
if (j == needle.size()) {
return i - needle.size() + 1;
}
}
return -1;
}
5. 字符串的替换与删除操作
5.1 原地修改字符串的技巧
很多字符串问题要求我们在原字符串上进行修改,而不是创建新的字符串。这时,双指针法再次显示出其优势。
例如,将字符串中的空格替换为"%20":
java复制public String replaceSpace(String s) {
StringBuilder sb = new StringBuilder();
for (char c : s.toCharArray()) {
if (c == ' ') {
sb.append("%20");
} else {
sb.append(c);
}
}
return sb.toString();
}
虽然这个实现创建了新字符串,但我们可以通过预先计算新长度,然后从后向前填充来实现原地修改(在C++等语言中)。
5.2 删除字符串中的特定字符
删除字符串中所有相邻重复项是一个经典问题:
python复制def removeDuplicates(s: str) -> str:
res = []
for char in s:
if res and res[-1] == char:
res.pop()
else:
res.append(char)
return ''.join(res)
这个实现使用了栈的思想,但也可以用双指针法实现类似效果。
6. 字符串的排列与组合问题
6.1 判断字符串排列
给定两个字符串s1和s2,判断s2是否包含s1的排列:
cpp复制bool checkInclusion(string s1, string s2) {
vector<int> need(26, 0), window(26, 0);
for (char c : s1) need[c - 'a']++;
int left = 0, right = 0, count = 0;
while (right < s2.size()) {
char c = s2[right];
window[c - 'a']++;
if (window[c - 'a'] <= need[c - 'a']) count++;
right++;
while (right - left >= s1.size()) {
if (count == s1.size()) return true;
char d = s2[left];
if (window[d - 'a'] <= need[d - 'a']) count--;
window[d - 'a']--;
left++;
}
}
return false;
}
这个解法使用了滑动窗口技巧,是双指针法的变种。
6.2 无重复字符的最长子串
另一个经典问题是找到不包含重复字符的最长子串:
python复制def lengthOfLongestSubstring(s: str) -> int:
used = {}
max_length = start = 0
for i, c in enumerate(s):
if c in used and start <= used[c]:
start = used[c] + 1
else:
max_length = max(max_length, i - start + 1)
used[c] = i
return max_length
这个解法通过哈希表记录字符最后出现的位置,实现了O(n)时间复杂度的最优解。
7. 字符串的编码与解码
7.1 字符串压缩
实现字符串的简单压缩算法:
cpp复制string compressString(string S) {
if (S.empty()) return S;
string res;
int count = 1;
char prev = S[0];
for (int i = 1; i < S.size(); i++) {
if (S[i] == prev) {
count++;
} else {
res += prev + to_string(count);
prev = S[i];
count = 1;
}
}
res += prev + to_string(count);
return res.size() < S.size() ? res : S;
}
7.2 字符串编码解码
设计一个算法来编码/解码字符串列表:
python复制def encode(strs):
return ''.join(f'{len(s)}#{s}' for s in strs)
def decode(s):
res = []
i = 0
while i < len(s):
j = i
while s[j] != '#':
j += 1
length = int(s[i:j])
res.append(s[j+1:j+1+length])
i = j + 1 + length
return res
这种编码方式使用"长度#字符串"的格式,可以正确处理包含各种特殊字符(包括数字和#)的字符串。
8. 字符串处理中的边界条件与调试技巧
8.1 常见边界条件
处理字符串问题时,以下边界条件需要特别注意:
- 空字符串输入
- 字符串全部由相同字符组成
- 字符串包含空格、标点等特殊字符
- 字符串长度达到上限(特别是对于原地修改问题)
- 大小写敏感性问题
8.2 调试技巧
- 打印指针位置和关键变量:在处理复杂字符串算法时,在循环中打印指针位置和相关变量值可以帮助快速定位问题。
- 单元测试:为各种边界情况编写测试用例,特别是空字符串、单字符字符串、全相同字符字符串等特殊情况。
- 可视化跟踪:对于双指针问题,可以在纸上画出指针移动过程,帮助理解算法逻辑。
经验分享:我在处理字符串问题时,通常会先手动模拟几个简单例子,确保理解了算法逻辑后再开始编码。这比直接写代码然后调试要高效得多。
