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

问题背景

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

例如:

原商品名:

复制代码
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 替换性能最强,因为它只做了"替换"一件事,轻量、高效。
相关推荐
凤凰院凶涛QAQ8 小时前
《Java版数据结构 & 集合类剖析》Map & Set & 反射:“从哈希碰撞到红黑树转换,反射帮你扒开 HashMap 的桶数组看个究竟”
java·数据结构·算法
H.莓飛8 小时前
【数据结构】排序_OJ题(一):计数、桶与排序后的处理
java·开发语言·数据结构
wangchunyu1148 小时前
JAVA AQS与Lock实现原理(含CAS)
java·开发语言·word
w_zero_one10 小时前
算法--模拟
java·数据结构·算法
Javatutouhouduan10 小时前
阿里1658页Java面试题总结(含答案)
java·高并发·java面试·java面试题·后端开发·java程序员·java八股文
秋天的怀念_David10 小时前
算法一千零一夜 · 第十三夜 幻方,武侠世界里的数学迷宫
java·程序人生·算法
专业程序开发源10 小时前
springboot欧缇蔓月子中心管理系统82697-计算机课程设计、毕业设计
java·spring boot·后端·python·spring·django·课程设计
秋天的怀念_David11 小时前
算法一千零一夜 · 第十四夜 幻方与奇妙的二进制
java·程序人生·算法
thefool11226611 小时前
网络编程^
java
Thinker QAQ11 小时前
并发编程(十一):Shared Memory 与 Message Passing——关系与组合使用
java·python·go·并发编程·shared memory·message passing