当前位置: 首页 > java >正文

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

问题背景

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

例如:

原商品名:

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) 方法,便于对不同替换方案进行比较。

public interface Replacer {String replaceKeywords(String text);
}

下面我们介绍四种方案。


1 最简单的 String.replace 方案

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

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());}@Overridepublic String replaceKeywords(String text) {String result = text;for (String keyword : keyWordList) {result = result.replace(keyword, "");}return result;}
}

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


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

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

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);}@Overridepublic String replaceKeywords(String text) {return pattern.matcher(text).replaceAll("");}
}

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


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

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

我们使用现成的库:

<dependency><groupId>org.ahocorasick</groupId><artifactId>ahocorasick</artifactId><version>0.6.3</version>
</dependency>

实现如下:

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();}@Overridepublic 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”

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

public class TrieKeywordReplacer implements Replacer {private final Trie trie;public TrieKeywordReplacer(String keyWords) {trie = new Trie();for (String s : keyWords.split(";")) {trie.insert(s);}}@Overridepublic 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;}
}

内存占用比较

替换类对象大小(单位:字节)
StrReplacer12,560
PatternReplacer21,592
TrieKeywordReplacer184,944
AhoCorasickReplacer253,896

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


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

场景StrReplacerPatternReplacerTrieKeywordReplacerAhoCorasickReplacer
单线程 1w 次21843 ns28846 ns532 ns727 ns
2线程,有1个命中关键词23444 ns39984 ns680 ns1157 ns
2线程,有20个命中关键词252738 ns114740 ns33900 ns113764 ns
2线程,无关键词匹配22248 ns9253 ns397 ns738 ns

总结

  • 如果关键词很少,用 String.replace 最方便。
  • 如果关键词较多,推荐使用 AC 自动机(如 AhoCorasick)或自己实现 Trie。
  • 自己实现的 Trie 替换性能最强,因为它只做了“替换”一件事,轻量、高效。
http://www.xdnf.cn/news/1906.html

相关文章:

  • 自学新标日第二十二课(复习)
  • ViewPager FragmentPagerAdapter在系统杀死应用后重建时UI不刷新的问题
  • Hadoop生态圈框架部署 - Windows上部署Hadoop
  • MySQL锁
  • redis客户端库redis++在嵌入式Linux下的交叉编译及使用
  • 从线性到非线性:简单聊聊神经网络的常见三大激活函数
  • 不吃【Numpy】版
  • Spring AI 快速入门:从环境搭建到核心组件集成
  • 高精度运算
  • 有关虚拟奢侈品
  • Java知识日常巩固(四)
  • Linux Ubuntu18.04下安装Qt Craeator 5.12.9(图文详解)
  • 【Qt】文件
  • 探秘LLM推理模型:hidden states中藏着的self verification的“钥匙”
  • PubMed PDF下载 cloudpmc-viewer-pow逆向
  • C#中实现XML解析器
  • YOLOX-PAI手部检测模型
  • RocketMQ 主题与队列的协同作用解析(既然队列存储在不同的集群中,那要主题有什么用呢?)---管理命令、配置安装
  • 二项分布详解:从基础到应用
  • Nginx---总结
  • 服务网格助力云原生后端系统升级:原理、实践与案例剖析
  • 第25周:DenseNet+SE-Net实战
  • 跟我学C++中级篇——处理对象的复制
  • Java实现加密(七)国密SM2算法的签名和验签(附商用密码检测相关国家标准/国密标准下载)
  • 深度解析 Java 排序中的 Null 值处理:Comparator.nullsLast 与 Comparator.nullsFirst 最佳实践
  • 酷狗音乐安卓版K歌功能与音效优化体验测评
  • 整合 CountVectorizer 和 TfidfVectorizer 绘制词云图
  • easyExcel导入导出convert
  • 算法训练营 Day1
  • 课程9. 机器翻译,Seq2Seq与Attention