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

问题背景

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

例如:

原商品名:

复制代码
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 替换性能最强,因为它只做了"替换"一件事,轻量、高效。
相关推荐
sss191s6 分钟前
Java 集合面试题从数据结构到 HashMap 源码剖析详解及常见考点梳理
java·开发语言·数据结构
LI JS@你猜啊14 分钟前
window安装docker
java·spring cloud·eureka
书中自有妍如玉24 分钟前
.net 使用MQTT订阅消息
java·前端·.net
风铃儿~1 小时前
Spring AI 入门:Java 开发者的生成式 AI 实践之路
java·人工智能·spring
斯普信专业组1 小时前
Tomcat全方位监控实施方案指南
java·tomcat
忆雾屿1 小时前
云原生时代 Kafka 深度实践:06原理剖析与源码解读
java·后端·云原生·kafka
武昌库里写JAVA1 小时前
iview Switch Tabs TabPane 使用提示Maximum call stack size exceeded堆栈溢出
java·开发语言·spring boot·学习·课程设计
gaoliheng0061 小时前
Redis看门狗机制
java·数据库·redis
我是唐青枫1 小时前
.NET AOT 详解
java·服务器·.net
Su米苏2 小时前
Axios请求超时重发机制
java