告别烦人的“三连发”:我的智能评论系统过滤之旅(1957. 删除字符使字符串变好)

告别烦人的"三连发":我的智能评论系统过滤之旅 😎

嘿,各位码农兄弟姐妹们!我是你们的老朋友,一个在代码世界里摸爬滚打多年的开发者。今天,我想和大家分享一个在实际项目中遇到的"小需求"以及我是如何从一道看似简单的算法题------LeetCode 1957. 删除字符使字符串变好------中找到灵感,并打造出一个健壮又高效的解决方案的。准备好小板凳,故事开始啦!

我遇到了什么问题?

最近,我接手了一个内容社区的"智能评论系统"优化项目。产品经理提出了一个需求:为了提升社区内容的阅读体验,需要对用户的评论进行一些"净化"。有些用户为了刷屏或者表达强烈情感,会输入大量重复的字符,比如:"这篇文章写得太棒棒棒棒棒了!!!" 或者 "楼主在吗吗吗吗吗?"

这种评论不仅影响观感,还占用了宝贵的屏幕空间。我们的目标就是自动将这类评论"变好",规则很简单:不允许出现三个或以上连续的相同字符

所以,我的任务可以精确地描述为:给定一个字符串(用户评论),删除最少的字符,使它变成一个"好字符串"。比如,"leeetcode" 应该变成 "leetcode""aaabaaaa" 应该变成 "aabaa"

瞧,这不就是上面那道 LeetCode 题嘛!现实世界的问题,往往就能在这些经典的算法题中找到影子。

先瞅瞅"通关秘籍"(题目提示解读)

在动手之前,我们先看看题目的提示,这往往藏着解题的关键线索:

  • 1 <= s.length <= 10^5:字符串长度最长有十万,这意味着 O(N^2) 的算法(比如循环内嵌字符串拼接)会超时,我们必须追求 O(N) 的线性时间复杂度。
  • s 只包含小写英文字母:字符集很简单,不用考虑复杂的 Unicode 或多字节字符。
  • 题目数据保证答案总是唯一的:这是最重要的提示!它告诉我们,处理过程不需要瞻前顾后,不需要回溯。我们可以从左到右,用一种"贪心"的策略来处理每个字符,因为每一步的局部最优选择,最终会导向全局最优解。这让我可以放心大胆地往前冲!

我是如何用"线性扫描"搞定它的

有了上面的分析,我意识到这个问题的核心是一个线性序列处理任务。我脑海中立刻浮现出几种经典的解决方案。

解法一:StringBuilder 王道,边建边看

这是最符合直觉,也是 Java 开发中最常用的方法。我们从左到右遍历原始评论,同时用一个 StringBuilder 构建我们"净化后"的新评论。

每当遇到一个新字符,我们只需要"回头"看看新评论的末尾,判断加上这个新字符后,会不会构成"三连"。

我的第一个念头就是这个方法,但我也踩了一个小坑 😉。我最初想的是:"记录前一个字符和前前一个字符......" 这样做需要维护好几个变量,逻辑有点绕。

恍然大悟的瞬间 ⚡️:我为什么要自己维护状态?StringBuilder 本身就是状态的存储者啊!我只需要看它末尾的两个字符就行了,逻辑清晰又简单。

java 复制代码
/*
 * 思路:遍历和StringBuilder。这是处理字符串构建问题的标准高效方法。
 * 遍历原字符串的每个字符,根据规则决定是否将其添加到StringBuilder中。
 * 规则:只有当新字符不会与结果的末尾两个字符形成三连时,才添加。
 * 时间复杂度:O(N),只遍历一次字符串。
 * 空间复杂度:O(N),最坏情况需要一个与原字符串等长的StringBuilder。
 */
class Solution {
    public String makeFancyString(String s) {
        // 为什么用StringBuilder?因为它可变。
        // 用String做"+"拼接每次都会生成新对象,效率极低(O(N^2))。
        // StringBuilder的append操作平均是O(1),是我们的不二之选。
        StringBuilder sb = new StringBuilder();
      
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            int len = sb.length();
          
            // 核心逻辑:回头看,但只看最后两位
            // 如果结果长度还不足2,或者新字符与最后两位不构成三连...
            if (len < 2 || sb.charAt(len - 1) != c || sb.charAt(len - 2) != c) {
                // ...就放心地把它加进来!
                sb.append(c);
            }
            // 否则,啥也不干,相当于"删除"了这个多余的字符。
        }
      
        return sb.toString();
    }
}
  • 时间复杂度 :O(N)。N 是字符串长度。我们只遍历了一次 ssb.appendsb.lengthsb.charAt 平均都是 O(1) 的操作。
  • 空间复杂度 :O(N)。需要一个 StringBuilder 来存储结果,其最大长度可能等于 N。

解法二:双指针,经典的"原地"智慧

作为一个有追求的开发者,我总想着用更"炫酷"的方式解决问题。双指针就是处理序列问题的经典技巧之一。

我们可以想象把字符串变成一个字符数组,然后用两个指针:

  • 读指针 i:它负责勇往直前,扫描整个原始数组。
  • 写指针 j:它比较保守,只在确认当前字符是"好的"之后,才把它写到自己的位置,然后前进一步。

这样,j 就慢慢地在数组的前面部分构建出了我们想要的"好字符串"。虽然在 Java 中字符串不可变,我们不能真正地"原地"修改,但这个思想完全可以模拟出来,并且代码非常精炼。

java 复制代码
/*
 * 思路:双指针。这是一种模拟原地修改数组的经典技巧。
 * 读指针`i`遍历原字符串,写指针`j`指向结果字符串(在char数组中)的末尾。
 * 对于每个读到的字符,如果它"合格",就把它放到写指针`j`的位置,然后`j`前进。
 * 时间复杂度:O(N),一次遍历和一次新字符串创建。
 * 空间复杂度:O(N),需要一个char数组和最终的结果字符串。
 */
class Solution {
    public String makeFancyString(String s) {
        int n = s.length();
        if (n < 3) {
            return s; // 长度小于3,不可能有三连,直接返回
        }
      
        // Java中String不可变,我们用char数组模拟这个过程
        char[] chars = s.toCharArray();
        int j = 0; // 写指针,永远指向下一个可写入的"净土"

        for (int i = 0; i < n; i++) { // i是读指针,一路狂奔
            // 这里的判断逻辑和解法一本质相同,但操作的是chars数组
            // chars[j-1]和chars[j-2]就是已构建好的"好字符串"的末尾
            if (j < 2 || chars[i] != chars[j - 1] || chars[i] != chars[j - 2]) {
                chars[j] = chars[i];
                j++; // 写入成功,写指针前进,占领新位置
            }
        }
      
        // 最后,根据写指针`j`的位置,从char数组的有效部分创建新字符串
        // new String(char[] data, int offset, int count) 是一个非常高效的构造器
        return new String(chars, 0, j);
    }
}
  • 时间复杂度 :O(N)。toCharArray O(N),循环 O(N),new String O(N),整体还是 O(N)。
  • 空间复杂度 :O(N)。需要一个 O(N) 的 char[] 和一个 O(N) 的返回字符串。

解法三:计数器,轻装上阵

还有一种思路,就是完全不"回头看"。我们只带一个简单的状态上路:一个计数器 count

我们遍历原始字符串,维护当前连续字符 lastChar 和它的出现次数 count

  • 如果遇到的字符和 lastChar 不一样,说明新的一组字符开始了,我们就更新 lastChar 并把 count 重置为1。
  • 如果一样,就把 count 加1。

每次我们都检查 count,只要它还小于3,就把当前字符加入结果。这个方法特别适合处理流式数据,因为它不需要存储除了最后一个字符以外的任何历史信息。

java 复制代码
/*
 * 思路:遍历和计数器。通过维护状态变量来避免回看,适合流式处理场景。
 * 我们遍历原字符串,用count记录当前连续相同字符的数量。
 * 只要一个字符的连续计数值小于3,它就是"好的",可以加入结果中。
 * 时间复杂度:O(N)。
 * 空间复杂度:O(N)。
 */
class Solution {
    public String makeFancyString(String s) {
        StringBuilder sb = new StringBuilder();
        int count = 0;
        char lastChar = ' '; // 初始化一个不可能在输入中出现的字符作为哨兵

        for (char c : s.toCharArray()) {
            if (c == lastChar) {
                count++; // 还是老熟人,计数器加1
            } else {
                lastChar = c; // 遇见新面孔,更新记录,计数器重置
                count = 1;
            }

            // 只要连续次数还没到3,就欢迎加入
            if (count < 3) {
                sb.append(c);
            }
        }
        return sb.toString();
    }
}
  • 时间复杂度:O(N)。
  • 空间复杂度:O(N)。

举一反三,这个思想还能用在哪?

这个"基于局部规则的线性扫描"思想非常强大,是很多文本处理、数据清洗功能的核心:

  1. 敏感词过滤 :遍历文本,用一个指针或滑动窗口匹配敏感词库,匹配到就替换成 *
  2. 代码格式化/Linter :比如自动移除多余的空格,将 a = b 格式化为 a = b。也是从头到尾扫一遍,根据规则(如"非字符串内的多个空格合并为一个")构建新代码。
  3. 数据压缩算法 :经典的游程编码 (Run-Length Encoding)就是这个思想的直接应用。它将 "aaabbc" 扫描并压缩成 "a3b2c1",其核心就是遍历并计数连续字符。

类似好题推荐

如果你觉得这种序列处理题很有意思,可以挑战一下这些:

最终,我用第一种 StringBuilder 的方案快速上线了评论净化功能,效果拔群。一个看似简单的算法题,不仅帮我解决了实际问题,还让我重新梳理了多种序列处理的经典思路。这就是编程的乐趣所在吧!希望我的这次分享对你也有所启发!😉


三种解法对比表格

对比维度 解法1: StringBuilder 回看 解法2: 双指针 解法3: StringBuilder + 计数器
核心思想 边构建边回头看结果的最后两位。 模拟原地修改,用写指针构建新序列。 边构建边维护一个连续字符的计数器状态。
代码简洁度 。逻辑最直接。 中等。需要理解双指针的移动逻辑。 。状态变量清晰。
空间效率(Java) O(N) O(N) O(N)
推荐场景 通用首选。最符合Java习惯,清晰易懂。 面试时展示对经典算法模式的理解。 当处理的数据是流式时,此方法优势明显,因为它不需要存储大量历史结果。
相关推荐
CoovallyAIHub27 分钟前
避开算力坑!无人机桥梁检测场景下YOLO模型选型指南
深度学习·算法·计算机视觉
YouQian77231 分钟前
问题 C: 字符串匹配
c语言·数据结构·算法
yanxing.D37 分钟前
408——数据结构(第二章 线性表)
数据结构·算法
艾莉丝努力练剑1 小时前
【LeetCode&数据结构】二叉树的应用(二)——二叉树的前序遍历问题、二叉树的中序遍历问题、二叉树的后序遍历问题详解
c语言·开发语言·数据结构·学习·算法·leetcode·链表
YuTaoShao1 小时前
【LeetCode 热题 100】51. N 皇后——回溯
java·算法·leetcode·职场和发展
1 小时前
3D碰撞检测系统 基于SAT算法+Burst优化(Unity)
算法·3d·unity·c#·游戏引擎·sat
Tony沈哲2 小时前
OpenCV 图像调色优化实录:基于图像金字塔的 RAW / HEIC 文件加载与调色实践
opencv·算法
我就是全世界2 小时前
Faiss中L2欧式距离与余弦相似度:究竟该如何选择?
算法·faiss
boyedu2 小时前
比特币运行机制全解析:区块链、共识算法与数字黄金的未来挑战
算法·区块链·共识算法·数字货币·加密货币
KarrySmile3 小时前
Day04–链表–24. 两两交换链表中的节点,19. 删除链表的倒数第 N 个结点,面试题 02.07. 链表相交,142. 环形链表 II
算法·链表·面试·双指针法·虚拟头结点·环形链表