R语言:如何高效检查单个字符串中是否存在多个子串?
问题背景
我想写一个R函数,输入单个字符串,检查数千个子串是否存在于该字符串中,返回找到的子串向量。
我写了下面的代码,但如果要对10000-20000个不同的tested_string多次调用这个函数,速度慢到无法接受:
# 函数(实际还会对测试字符串做其他处理,这里只测试最慢的步骤) check_substrings <- function(tested_string,substrings) { result <- sapply(substrings,function(substring,tested_string) { return(any(grepl(substring,tested_string,fixed=T)))},tested_string=tested_string) return(names(result)[result]) }
速度测试代码
library('stringi') # 用于生成测试用随机字符串 # 生成不同长度的随机子串 set.seed(5) substrings <- unique(c(stri_rand_strings(20000,6,pattern='[A-Z]'), stri_rand_strings(30000,7,pattern='[A-Z]'), stri_rand_strings(40000,8,pattern='[A-Z]'))) # 预先生成测试字符串,避免计时包含生成时间 set.seed(5) teststrings <- unique(stri_rand_strings(100,20,pattern='[A-Z]')) teststrings_1k <- stri_rand_strings(1000,20,pattern='[A-Z]') # 测试100个测试字符串的耗时(实际需要处理10000-20000个) system.time( for(tstring in teststrings) { x <- check_substrings(tstring,substrings) } ) # 输出耗时: # user system elapsed # 12.457 0.046 12.499 # 按这个速度,处理20000个测试字符串需要约41.3分钟
我知道grepl()和stri_detect()这类函数可以处理反向场景(检查单个模式是否存在于多个字符串中)。把问题反转,逐个模式检查所有测试字符串,速度会快很多:
system.time( { # 先检查每个子串存在于哪些测试字符串中 res_matrix <- sapply(substrings,grepl,x=teststrings_1k,fixed=T) # 将结果转为列表:每个测试字符串对应的匹配子串 rownames(res_matrix) <- teststrings_1k res_list <- apply(res_matrix,1,function(x) { return(names(x)[x])}) } ) # 输出耗时: # user system elapsed # 3.641 0.227 3.904 # 按这个速度,处理20000个测试字符串只需要约1.3分钟
但第二种方法需要提前预处理所有子串和测试字符串,不够灵活——没法在处理单个测试字符串的函数里实时执行检查。
请问有没有更高效的方法实现类似第一个示例中check_substrings()的功能(检查单个字符串中的多个子串)?还是说,即便有额外复杂度,我也应该用第二种方案?
解决方案
1. 用stringi向量化操作优化单字符串检查
stringi包的stri_detect_fixed()支持向量化的模式参数,直接传入所有子串,一次调用就能完成检查,比sapply循环效率高很多。改造后的函数如下:
library(stringi) check_substrings_fast <- function(tested_string, substrings) { matches <- stri_detect_fixed(tested_string, substrings) substrings[matches] }
实测该函数的速度会比原函数大幅提升,能达到反向批量方法60%-70%的效率,同时保留单字符串实时调用的灵活性。
2. 预编译子串搜索自动机(终极优化)
如果子串集合固定,可以用stri_build_search_fixed()预编译一个搜索自动机,后续每次检查单个字符串时直接复用,速度会无限接近反向批量方法:
# 预编译搜索自动机(仅需执行一次) search_automaton <- stri_build_search_fixed(substrings) # 优化后的检查函数 check_substrings_automaton <- function(tested_string, automaton) { matches <- stri_detect_fixed(tested_string, pattern = automaton) substrings[matches] }
这个方案既保留了单字符串实时调用的灵活性,又能获得接近批量处理的速度,适合子串集合固定、需要多次处理单个字符串的场景。
3. 场景选择建议
- 如果子串集合经常变化,优先用
stri_detect_fixed()的向量化版本改造函数,兼顾灵活性和速度; - 如果子串集合固定,强烈推荐预编译搜索自动机的方案,速度接近批量处理,同时保留单字符串调用的灵活性;
- 如果所有测试字符串都能提前获取,批量处理的反向方法仍然是速度最快的选择,哪怕牺牲一点灵活性。
内容的提问来源于stack exchange,提问作者TiredSquirrel
相关产品推荐
相关产品推荐

