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

问题背景

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

例如:

原商品名:

复制代码
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 替换性能最强,因为它只做了"替换"一件事,轻量、高效。
相关推荐
2401_cf3 小时前
为什么hadoop不用Java的序列化?
java·hadoop·eclipse
帮帮志3 小时前
idea整合maven环境配置
java·maven·intellij-idea
LuckyTHP3 小时前
java 使用zxing生成条形码(可自定义文字位置、边框样式)
java·开发语言·python
无声旅者6 小时前
深度解析 IDEA 集成 Continue 插件:提升开发效率的全流程指南
java·ide·ai·intellij-idea·ai编程·continue·openapi
Ryan-Joee6 小时前
Spring Boot三层架构设计模式
java·spring boot
Hygge-star7 小时前
【数据结构】二分查找5.12
java·数据结构·程序人生·算法·学习方法
dkmilk7 小时前
Tomcat发布websocket
java·websocket·tomcat
工一木子7 小时前
【Java项目脚手架系列】第七篇:Spring Boot + Redis项目脚手架
java·spring boot·redis
哞哞不熬夜7 小时前
JavaEE--初识网络
java·网络·java-ee
等等5437 小时前
Java EE初阶——wait 和 notify
java·开发语言