你好!作为一名经常与数据处理打交道的开发者,我发现在实际的编码工作或技术面试中,字符串处理是一个永恒的话题。今天,我们将深入探讨一个经典且非常实用的字符串算法问题:如何在给定的字符串中,找到连续出现次数最多的字符。
这个问题看似简单,但它非常能考察我们对循环控制、边界条件处理以及算法效率优化的理解。别担心,我们会像在日常代码审查(Code Review)中一样,从最基础的思路开始,一步步剖析并优化代码,直到找到最优解。
核心问题定义
首先,让我们明确一下我们的目标。给定一个字符串 s,我们需要找到其中连续重复次数最多的那个字符。
这里有一个非常关键的点需要注意:我们要找的是“单次连续重复”的最大长度,而不是字符在字符串中出现的总次数。
为了让你更直观地理解,让我们看两个例子:
示例 1:
> 输入: s = "geeekk"
> 输出: e
> 解释: 在这里,虽然 INLINECODE5991426e 也出现了多次,但字符 INLINECODE5c78436b 是连续出现了 3 次(eee),这是当前字符串中最大的连续重复长度。
示例 2:
> 输入: s = "aaaabbcbbb"
> 输出: a
> 解释: 字符 INLINECODE03ff9d78 连续出现了 4 次(INLINECODE91e96716),而 INLINECODE50fb1855 虽然在字符串中总共有 5 个,但它们被 INLINECODEe11a2c03 隔开了,最长的一段只有 2 个。因此,答案是 a。
理解了这一点后,我们就不会掉入“统计字符频率”的陷阱里了。接下来,让我们看看如何解决这个问题。
—
方法一:朴素思路(嵌套循环)
在寻找最高效的解决方案之前,让我们先尝试用最直观的方式解决这个问题。这通常被称为“暴力解法”或“朴素解法”。
#### 算法思路
我们的核心思路是:对于字符串中的每一个字符,都以此为起点,向后遍历,直到遇到一个不同的字符为止。
这就好比你在排队买票,你想知道当前最长的一队连续穿同样颜色衣服的人有多长。你可能会从头走到尾,每遇到一个人,就数数他和后面几个人穿的衣服颜色是一样的。
具体步骤如下:
- 初始化 INLINECODEb7f37384(记录最大连续次数)为 0,INLINECODE1990385d(记录结果字符)为字符串的第一个字符。
- 使用一个外层循环
i遍历字符串的每一个字符。 - 对于每个 INLINECODE02426597,使用内层循环 INLINECODEdc93745d 从 INLINECODEb39e51ed 开始向后移动,只要 INLINECODE1687a53d,计数器
cnt就加 1。 - 一旦内层循环遇到不同的字符或到达字符串末尾,我们就比较当前的 INLINECODE5c433d9b 是否大于 INLINECODE5057cd66。如果是,就更新 INLINECODE2fb713f9 和 INLINECODE3862b3b9。
#### 代码实现
让我们看看这段代码是如何用 C++、Java、Python 和 JavaScript 实现的。我特意保留了详细的注释,帮助你理解每一行的作用。
C++ 实现
// C++ program to find the maximum consecutive
// repeating character in given string
#include
using namespace std;
// 返回字符串中连续重复次数最多的字符
char maxRepeating(string str) {
int n = str.length();
// 记录最大连续数量
int maxCnt = 0;
// 记录对应的字符,默认设为第一个字符
char res = str[0];
// 外层循环:遍历字符串中的每一个字符
for (int i = 0; i < n; i++) {
// 当前字符的连续计数器
int cnt = 0;
// 内层循环:从当前字符开始,向后检查连续性
for (int j = i; j maxCnt) {
maxCnt = cnt;
res = str[i];
}
}
return res;
}
int main() {
string s = "aaaabbaaccde";
cout << "最大连续重复字符是: " << maxRepeating(s);
return 0;
}
Java 实现
// Java program to find the maximum consecutive
// repeating character in given string
class GfG {
// Function to find out the maximum repeating
// character in given string
static char maxRepeating(String str) {
int n = str.length();
int maxCnt = 0;
char res = str.charAt(0);
// Find the maximum repeating character
// starting from str[i]
for (int i = 0; i < n; i++) {
int cnt = 0;
for (int j = i; j maxCnt) {
maxCnt = cnt;
res = str.charAt(i);
}
}
return res;
}
public static void main(String[] args) {
String s = "aaaabbaaccde";
System.out.println("最大连续重复字符是: " + maxRepeating(s));
}
}
Python 实现
# Python program to find the maximum consecutive
# repeating character in given string
def maxRepeating(str):
n = len(str)
max_cnt = 0
res = str[0]
# 遍历字符串中的每个字符
for i in range(n):
cnt = 0
# 向后检查连续相同的字符
for j in range(i, n):
if str[i] != str[j]:
break
cnt += 1
# 更新最大值和结果字符
if cnt > max_cnt:
max_cnt = cnt
res = str[i]
return res
if __name__ == "__main__":
s = "aaaabbaaccde"
print(f"最大连续重复字符是: {maxRepeating(s)}")
JavaScript 实现
// JavaScript program to find the maximum consecutive
// repeating character in given string
function maxRepeating(str) {
let n = str.length;
let maxCnt = 0;
let res = str[0];
// 遍历字符串
for (let i = 0; i < n; i++) {
let cnt = 0;
// 内层循环检查连续性
for (let j = i; j maxCnt) {
maxCnt = cnt;
res = str[i];
}
}
return res;
}
// 测试代码
let s = "aaaabbaaccde";
console.log("最大连续重复字符是: " + maxRepeating(s));
#### 性能分析
虽然朴素方法逻辑清晰,但在处理大规模数据时可能并不高效。让我们分析一下它的复杂度:
- 时间复杂度:O(n²)。为什么是平方级?因为对于每一个字符(外层循环 n 次),我们可能都要遍历剩下的所有字符(内层循环最坏情况下也是 n 次)。当字符串非常长时,这会导致明显的性能瓶颈。
- 空间复杂度:O(1)。这点做得很好,我们只使用了几个变量来存储计数和结果,没有使用额外的存储空间。
—
方法二:优化的单次遍历
既然嵌套循环带来了 O(n²) 的开销,我们能不能想办法只遍历一次字符串就得到答案呢?答案是肯定的。
#### 优化思路
让我们换个角度思考。如果我们正在从头到尾阅读字符串,当我们看到一个字符时,我们只需要关心它跟前一个字符是否相同。
想象一下你在流水线上工作:
- 你手里拿着当前产品(字符)。
- 你看它跟上一个产品是不是一样的。
- 如果一样,你就把当前的计数器加 1。
- 如果不一样,说明上一个“连续批次”结束了。你需要结算一下:刚才那个批次的数量是不是创了新高?如果是,记录下来。然后,重置计数器,开始统计新产品的数量。
这种思路把“以 i 为中心向右扩散”改成了“线性扫描并记录状态”,效率大大提升。
#### 关键步骤
- 遍历:使用一个循环从头到尾遍历字符串(从索引 1 开始,而不是 0,因为我们总是比较 INLINECODEe5b184f9 和 INLINECODE23688a90)。
- 比较:如果 INLINECODE21589a94,说明当前字符是前一个字符的延续,INLINECODEb55dbad0 加 1。
- 重置与结算:如果
s[i] != s[i-1],说明连续性被打断。
* 首先,检查刚才的 INLINECODEc35da2b7 是否大于 INLINECODE44acdb23。如果是,更新结果。
* 然后,将 INLINECODE21f4e9f1 重置为 1(因为当前字符 INLINECODE1c7729f3 开始了一个新的潜在连续段,它自己占 1 次)。
#### 代码实现(优化版)
这里我只展示 C++ 代码,你可以尝试用同样的逻辑修改上面的 Java 或 Python 代码,这是一个很好的练习。
// 优化后的 C++ 程序:使用单次遍历查找最大连续重复字符
#include
#include
using namespace std;
char maxRepeating(string str) {
int n = str.length();
if (n == 0) return ‘\0‘; // 处理空字符串的边界情况
int maxCount = 0; // 全局最大连续计数
int count = 0; // 当前连续字符的计数
char res = str[0]; // 结果字符
for (int i = 0; i maxCount) {
maxCount = count;
res = str[i - 1]; // 更新结果为上一段的字符
}
// 重置计数器,开始计算新字符
count = 1;
}
}
// 循环结束后,不要忘记检查最后一段连续字符!
// 因为循环可能结束时恰好处于最长连续段中,没有遇到“不同字符”来触发更新
if (count > maxCount) {
res = str[n - 1];
}
return res;
}
int main() {
string s = "aaaabbaaccde";
cout << "最大连续重复字符是: " << maxRepeating(s) << endl;
return 0;
}
#### 优化后的性能分析
- 时间复杂度:O(n)。我们只遍历了字符串一次。无论字符串有多长,我们都只需要常数倍的遍历时间。这在处理海量数据(如读取日志文件分析)时至关重要。
- 空间复杂度:O(1)。依然只使用了有限的几个变量。
—
实战应用与最佳实践
在实战中,这个问题有很多应用场景。比如,在数据压缩(Run-Length Encoding)中,我们需要先找到连续重复的字符块才能进行编码。再比如,在基因序列分析中,科学家可能对最长的连续 DNA 片段感兴趣。
#### 常见错误提示
- 忽略循环后的最后一次检查:在方法二中,很多初学者容易犯的错误是只在内层 INLINECODE71aa8d65 更新结果。如果最长的一段连续字符恰好位于字符串的末尾,循环结束前没有遇到不同的字符来触发 INLINECODEc7274667 分支,这个结果就不会被记录下来。所以,循环结束后的额外检查是必须的。
- 索引越界:记得在比较 INLINECODE0ef0daf0 和 INLINECODE549c95b4 时,确保循环从 INLINECODE069a2646 开始,或者在逻辑中处理 INLINECODE61645ed5 的情况,否则会访问到
str[-1]导致未定义行为。 - 题目理解偏差:正如我在开头强调的,千万别把题目理解成“统计出现频率最高的字符”。例如在 "abbbbbba" 中,频率最高的是 ‘b‘(8次),但连续出现的最大长度也是 8。但如果题目是 "ababa",频率最高的还是 ‘a‘ 或 ‘b‘(3次),但它们的最大连续长度只有 1。
总结
通过这篇深度解析,我们从朴素的双重循环出发,将其优化为高效的线性扫描算法。在这个过程中,我们不仅学会了如何解决这个问题,更重要的是掌握了状态机思维——即如何在遍历过程中维护当前的“状态”(当前计数和最大计数)。
希望这次分享对你有帮助。下次当你遇到字符串处理问题时,不妨先思考一下:“我能不能只遍历一次就搞定?” 这种思维习惯往往能帮你写出更优雅、更高效的代码。
你可以尝试复制上面的代码,自己修改输入字符串,或者尝试实现不同语言的版本,加深对算法的理解。祝编码愉快!