如何优化字符串替换:四种实现方案对比与性能分析

问题背景

我们在处理商品名称时,常常需要去掉一些不需要的关键词

例如:

原商品名:

复制代码
HUAWEI Pura 70 Pro 国家补贴500元 羽砂黑 12GB+512GB 超高速风驰闪拍 华为鸿蒙智能手机

希望替换后:

复制代码
HUAWEI Pura 70 Pro 羽砂黑 12GB+512GB 超高速风驰闪拍 华为鸿蒙智能手机

替换掉的关键词是:"国家补贴500元"。

你可能会说:"这很简单嘛,用 skuName.replace("国家补贴500元", "") 不就搞定了?"

确实,当关键词不多时这样完全可以

但如果我们要处理的是几百上千个关键词,就不那么简单了!

很多项目上线后,用 String.replace 处理大量关键词会瞬间把CPU打满,程序几乎卡死,根本无法用。

其实这和"敏感词过滤"是一个道理。为了解决这个问题,就需要用更高效的算法,比如下面介绍的 Aho-Corasick 自动机算法(简称 AC 自动机)


解决方案

我们设计了一个统一的接口 Replacer,定义了 replaceKeywords(String text) 方法,便于对不同替换方案进行比较。

java 复制代码
public interface Replacer {
    String replaceKeywords(String text);
}

下面我们介绍四种方案。


1 最简单的 String.replace 方案

这是最基础的办法,也就是把每个关键词依次替换掉。为了避免"短关键词"干扰"长关键词",我们先把关键词按长度从长到短排序

java 复制代码
public class StrReplacer implements Replacer {
    private final List<String> keyWordList;

    public StrReplacer(String keyWords) {
        this.keyWordList = Arrays.asList(keyWords.split(";"));
        keyWordList.sort((a, b) -> b.length() - a.length());
    }

    @Override
    public String replaceKeywords(String text) {
        String result = text;
        for (String keyword : keyWordList) {
            result = result.replace(keyword, "");
        }
        return result;
    }
}

这个方法适用于关键词不多的场景,简单有效。


2 使用正则表达式优化替换

其实 String.replace() 背后也是在用正则表达式。我们也可以自己构造一个正则表达式,一次性替换所有关键词。

java 复制代码
public class PatternReplacer implements Replacer {
    private final Pattern pattern;

    public PatternReplacer(String keyWords) {
        List<String> keywords = Arrays.asList(keyWords.split(";"));
        keywords.sort((a, b) -> b.length() - a.length());
        String regex = keywords.stream().map(Pattern::quote).collect(Collectors.joining("|"));
        this.pattern = Pattern.compile(regex);
    }

    @Override
    public String replaceKeywords(String text) {
        return pattern.matcher(text).replaceAll("");
    }
}

这个方法适用于关键词数量中等的情况 ,性能比循环调用 replace() 稍好。


3 使用 Aho-Corasick(AC 自动机)算法

这是一个经典的多关键词匹配算法,可以在一次遍历中找出所有匹配的关键词,效率非常高。

我们使用现成的库:

xml 复制代码
<dependency>
    <groupId>org.ahocorasick</groupId>
    <artifactId>ahocorasick</artifactId>
    <version>0.6.3</version>
</dependency>

实现如下:

java 复制代码
public class AhoCorasickReplacer implements Replacer {
    private final Trie trie;

    public AhoCorasickReplacer(String keyWords) {
        Trie.TrieBuilder builder = Trie.builder().ignoreOverlaps().onlyWholeWords();
        for (String keyword : keyWords.split(";")) {
            builder.addKeyword(keyword);
        }
        this.trie = builder.build();
    }

    @Override
    public String replaceKeywords(String text) {
        StringBuilder result = new StringBuilder();
        int lastEnd = 0;
        for (Emit emit : trie.parseText(text)) {
            result.append(text, lastEnd, emit.getStart());
            lastEnd = emit.getEnd() + 1;
        }
        result.append(text.substring(lastEnd));
        return result.toString();
    }
}

这适用于关键词很多的高性能场景,是实际项目中非常推荐的方式。


4 自己手写 Trie 树实现替换

Trie 树(字典树)是一种结构简单的前缀树,可以快速查找前缀匹配的字符串。

图片位置(插图说明 Trie 树结构)

比如:

  • root → c → a → t 代表单词 "cat"
  • root → d → o → g 代表单词 "dog"

我们实现的代码只做"替换"功能,性能非常好:

java 复制代码
public class TrieKeywordReplacer implements Replacer {
    private final Trie trie;

    public TrieKeywordReplacer(String keyWords) {
        trie = new Trie();
        for (String s : keyWords.split(";")) {
            trie.insert(s);
        }
    }

    @Override
    public String replaceKeywords(String text) {
        return trie.replaceKeywords(text, "");
    }

    static class Trie {
        private final TrieNode root = new TrieNode();

        public void insert(String word) {
            TrieNode node = root;
            for (char c : word.toCharArray()) {
                node.children.putIfAbsent(c, new TrieNode());
                node = node.children.get(c);
            }
            node.isEndOfWord = true;
        }

        public String replaceKeywords(String text, String replacement) {
            StringBuilder result = new StringBuilder();
            int i = 0;
            while (i < text.length()) {
                TrieNode node = root;
                int j = i, lastMatch = -1;
                while (j < text.length() && node.children.containsKey(text.charAt(j))) {
                    node = node.children.get(text.charAt(j));
                    if (node.isEndOfWord) lastMatch = j;
                    j++;
                }
                if (lastMatch >= 0) {
                    result.append(replacement);
                    i = lastMatch + 1;
                } else {
                    result.append(text.charAt(i++));
                }
            }
            return result.toString();
        }
    }

    static class TrieNode {
        Map<Character, TrieNode> children = new HashMap<>();
        boolean isEndOfWord = false;
    }
}

内存占用比较

替换类 对象大小(单位:字节)
StrReplacer 12,560
PatternReplacer 21,592
TrieKeywordReplacer 184,944
AhoCorasickReplacer 253,896

可以看出,自己实现的 Trie 占用较小,AC 自动机功能更强,占用更多内存


性能测试结果(关键词数量 400,JDK 1.8)

场景 StrReplacer PatternReplacer TrieKeywordReplacer AhoCorasickReplacer
单线程 1w 次 21843 ns 28846 ns 532 ns 727 ns
2线程,有1个命中关键词 23444 ns 39984 ns 680 ns 1157 ns
2线程,有20个命中关键词 252738 ns 114740 ns 33900 ns 113764 ns
2线程,无关键词匹配 22248 ns 9253 ns 397 ns 738 ns

总结

  • 如果关键词很少,用 String.replace 最方便。
  • 如果关键词较多,推荐使用 AC 自动机(如 AhoCorasick)或自己实现 Trie。
  • 自己实现的 Trie 替换性能最强,因为它只做了"替换"一件事,轻量、高效。
相关推荐
Ivanqhz16 小时前
Rust Lazy浅析
java·javascript·rust
bksczm17 小时前
Linux之日志和线程池、内存池
java·开发语言
笨蛋不要掉眼泪17 小时前
Java虚拟机:对象复活、引用强度与Stop-The-World
java·开发语言·jvm
凤山老林17 小时前
SpringBoot实战:构建优雅的全局异常处理机制
java·springboot·异常处理
小Ti客栈17 小时前
Spring Boot 整合 Swagger2 和 Knife4j实现接口文档与可视化调试
java·spring boot·后端
梅头脑17 小时前
ThreadLocalMap里几十万个死Entry——排查了半天OOM,根因就一行finally没写
java
2601_9638701718 小时前
【计算机毕业设计】基于Spring Boot的社区老年大学课程报名与学习系统的设计与实现
java·spring boot·学习
llwszx18 小时前
【Java/Go后端手撸原生Agent(第七篇):Token预算管理 + 滑动窗口上下文裁剪】
java·后端·python·agent开发·上下文工程·上下文裁剪·滑动窗口裁剪
weixin_BYSJ198719 小时前
springboot3家政平台小程序--附源码00904
java·javascript·spring boot·python·django·flask·php
梦幻通灵19 小时前
Java中finally失效的几种情况【持续更新】
java·开发语言