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

问题背景

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

例如:

原商品名:

复制代码
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 替换性能最强,因为它只做了"替换"一件事,轻量、高效。
相关推荐
Full Stack Developme1 小时前
Java LRU 与 LFU 算法及应用
java·开发语言·算法
程序员老油条3 小时前
WSL2 + Docker Desktop:Windows 下的完美 Java 开发环境
java·windows·docker·wsl2
shushangyun_4 小时前
2026智能采购商城系统选型指南:如何引领企业数字化采购升级
java·大数据·数据库·人工智能·机器学习
她说..5 小时前
Java 默认值设置方式
java·开发语言·后端·springboot
学渣超5 小时前
记一次分布式事务数据不一致的排查之旅:从超时到索引,层层剥茧
java·后端·架构
掉鱼的猫5 小时前
Agent Harness 实战指南:构建生产级 AI Agent 的"马具"框架
java·llm·aigc
带刺的坐椅5 小时前
Agent Harness 实战指南:构建生产级 AI Agent 的"马具"框架
java·ai·llm·agent·solon-ai
c238565 小时前
互斥锁高频面试题全解:从基础概念到底层实现,一文通关
java·c++·面试·职场和发展
error:(6 小时前
【系统与实战双精通】VS Code 调试 ROS2 Python 节点与 Launch 系统指南
android·java·python
Tian_Hang7 小时前
Eclipse Ditto 的权限策略
java·服务器·前端·网络·ide·ubuntu·eclipse