在一个字符串中查找子字符串的最佳方法

9

我有一个问题,我想在字符串中查找子字符串。该子字符串可能存在于字符串中,也可能不存在。

str = "hello how are you?"
substr = "how are"

我知道的两种方法是:

  1. string.indexOf("how are")
  2. 正则表达式

但是,还有没有其他更“优化”的方式?你会怎么做?

使用Ruby能提供更好的答案吗?由于我们使用jRuby,答案可以是Ruby或Java。


1
知道这已经过时了:但是“最好的方法”很大程度上取决于目标是什么。如果是开发的便捷性,str.include?(substring)非常好,但如果是速度,则可能有其他更好的方法。 - Automatico
7个回答

11
在 Ruby 中,使用 String#include? 方法:
str = "hello how are you?"
substr = "how are"
str.include? substr 

该函数返回true


我知道我来晚了,但请记住,“include?”是区分大小写的。 - HomeworkHopper

5
“你会怎么做?”
我会进行基准测试并尝试比较完成同一任务的不同方法,以了解哪种方法最快。在早期版本的Ruby中,我们会看到基于正则表达式的搜索运行得更慢。我正在使用1.9.2中的新引擎进行基准测试,这对结果有很大影响。特别是,未锚定的搜索以前比固定字符串搜索慢得多。现在,对于大多数情况下,使用正则表达式或固定字符串搜索都差不多。如果您需要在循环中执行相同的模式,则使用未预编译正则表达式的match()会影响速度,因此将模式分配给变量并引用变量是有意义的。
显示的时间是每个测试执行“n”(750,000)次迭代所需的时间,因此较低的数字更好。
require 'benchmark'

LOREM = %q{Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et convallis purus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Cras interdum nibh et nunc pellentesque vestibulum. Donec elementum felis malesuada urna vehicula consectetur commodo urna accumsan. Phasellus bibendum euismod tincidunt. Sed pellentesque cursus faucibus. Etiam bibendum tincidunt nibh eget ultrices. Fusce imperdiet, felis id consequat imperdiet, justo est ultrices elit, sed vestibulum dui nibh vel felis. Sed feugiat, libero quis consequat semper, magna tellus facilisis enim, rutrum adipiscing eros mauris commodo metus. Sed lobortis aliquet augue ac sodales. Quisque pharetra odio vel augue tempus porttitor.}

REGEX1 = %r{/porttitor\.$/}
REGEX2 = %r{/porttitor\./}
REGEX3 = %r{/porttitor\.\Z/}

n = 750_000
puts "word in string"
Benchmark.bm(15) do |x|
  x.report('string[""]:')   { n.times { LOREM['porttitor.']          } }
  x.report('string[//]:')   { n.times { LOREM[/porttitor\./]         } } # unanchored regex
  x.report('string[/$/]:')  { n.times { LOREM[/porttitor\.$/]        } } # anchored regex
  x.report('string[/\Z/]:') { n.times { LOREM[/porttitor\.\Z/]       } } # anchored regex
  x.report('index():')      { n.times { LOREM.index('porttitor.')    } }
  x.report('include?():')   { n.times { LOREM.include?('porttitor.') } }
  x.report('match($):')     { n.times { LOREM.match(/porttitor\.$/)  } }
  x.report('match(\Z):')    { n.times { LOREM.match(/porttitor\.\Z/) } }
  x.report('match():')      { n.times { LOREM.match(/porttitor\./)   } }
  x.report('match2($):')    { n.times { LOREM.match(REGEX1)          } } # compiled regex w/ anchor
  x.report('match2():')     { n.times { LOREM.match(REGEX2)          } } # compiled report w/out anchor
  x.report('match2(\Z):')   { n.times { LOREM.match(REGEX3)          } } # compiled regex w/ anchor
end

puts
puts "word not in string"
Benchmark.bm(15) do |x|
  x.report('string[""]:')   { n.times { LOREM['porttit0r.']          } }
  x.report('string[//]:')   { n.times { LOREM[/porttit0r\./]         } } # unanchored regex
  x.report('string[/$/]:')  { n.times { LOREM[/porttit0r\.$/]        } } # anchored regex
  x.report('string[/\Z/]:') { n.times { LOREM[/porttit0r\.\Z/]       } } # anchored regex
  x.report('index():')      { n.times { LOREM.index('porttit0r.')    } }
  x.report('include?():')   { n.times { LOREM.include?('porttit0r.') } }
  x.report('match($):')     { n.times { LOREM.match(/porttit0r\.$/)  } }
  x.report('match(\Z):')    { n.times { LOREM.match(/porttit0r\.\Z/) } }
  x.report('match():')      { n.times { LOREM.match(/porttit0r\./)   } }
end

带有输出:
word in string
                      user     system      total        real
string[""]:       0.670000   0.000000   0.670000 (  0.675319)
string[//]:       0.700000   0.000000   0.700000 (  0.706148)
string[/$/]:      0.720000   0.000000   0.720000 (  0.716853)
string[/\Z/]:     0.530000   0.000000   0.530000 (  0.527568)
index():          0.630000   0.000000   0.630000 (  0.638562)
include?():       0.610000   0.000000   0.610000 (  0.603223)
match($):         1.690000   0.000000   1.690000 (  1.696045)
match(\Z):        1.520000   0.010000   1.530000 (  1.532107)
match():          1.700000   0.000000   1.700000 (  1.698748)
match2($):        0.840000   0.000000   0.840000 (  0.847590)
match2():         0.840000   0.000000   0.840000 (  0.840969)
match2(\Z):       0.840000   0.000000   0.840000 (  0.835557)

word not in string
                      user     system      total        real
string[""]:       0.570000   0.000000   0.570000 (  0.578120)
string[//]:       0.740000   0.000000   0.740000 (  0.734751)
string[/$/]:      0.730000   0.000000   0.730000 (  0.735599)
string[/\Z/]:     0.560000   0.000000   0.560000 (  0.563673)
index():          0.620000   0.000000   0.620000 (  0.619451)
include?():       0.570000   0.000000   0.570000 (  0.574413)
match($):         0.910000   0.010000   0.920000 (  0.910059)
match(\Z):        0.730000   0.000000   0.730000 (  0.726533)
match():          0.950000   0.000000   0.950000 (  0.960865)

供参考,以下是一些使用 Ruby 1.8.7 的数字,这是 Snow Leopard 的默认设置:
word in string
                     user     system      total        real
string[""]:      1.130000   0.000000   1.130000 (  1.130687)
string[//]:      1.170000   0.000000   1.170000 (  1.165692)
string[/$/]:     1.180000   0.000000   1.180000 (  1.184954)
string[/\Z/]:    1.180000   0.000000   1.180000 (  1.179168)
index():         1.070000   0.000000   1.070000 (  1.077791)
include?():      1.060000   0.000000   1.060000 (  1.056430)
match($):        1.470000   0.010000   1.480000 (  1.472797)
match(\Z):       1.480000   0.000000   1.480000 (  1.490172)
match():         1.480000   0.000000   1.480000 (  1.478146)
match2($):       0.650000   0.000000   0.650000 (  0.653029)
match2():        0.570000   0.000000   0.570000 (  0.574384)
match2(\Z):      0.640000   0.000000   0.640000 (  0.646688)

word not in string
                     user     system      total        real
string[""]:      1.040000   0.000000   1.040000 (  1.038885)
string[//]:      0.510000   0.000000   0.510000 (  0.507031)
string[/$/]:     0.510000   0.000000   0.510000 (  0.508425)
string[/\Z/]:    0.500000   0.000000   0.500000 (  0.507316)
index():         1.060000   0.000000   1.060000 (  1.055157)
include?():      1.030000   0.000000   1.030000 (  1.037060)
match($):        0.630000   0.000000   0.630000 (  0.623627)
match(\Z):       0.620000   0.000000   0.620000 (  0.624737)
match():         0.620000   0.000000   0.620000 (  0.623049)

我添加了额外的测试来展示仅使用未固定和固定锚定正则表达式的效果。
require 'fruity'

LOREM = %{Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et convallis purus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Cras interdum nibh et nunc pellentesque vestibulum. Donec elementum felis malesuada urna vehicula consectetur commodo urna accumsan. Phasellus bibendum euismod tincidunt. Sed pellentesque cursus faucibus. Etiam bibendum tincidunt nibh eget ultrices. Fusce imperdiet, felis id consequat imperdiet, justo est ultrices elit, sed vestibulum dui nibh vel felis. Sed feugiat, libero quis consequat semper, magna tellus facilisis enim, rutrum adipiscing eros mauris commodo metus. Sed lobortis aliquet augue ac sodales. Quisque pharetra odio vel augue tempus porttitor.}

compare do
  str_slice_regex  { LOREM[/porttitor\./]         } # unanchored regex
  str_slice_dollar { LOREM[/porttitor\.$/]        } # anchored regex
  str_slice_ctrlZ  { LOREM[/porttitor\.\Z/]       } # anchored regex
  str_slice_ctrlz  { LOREM[/porttitor\.\z/]       } # anchored regex
end

# >> Running each test 8192 times. Test will take about 1 second.
# >> str_slice_ctrlz is similar to str_slice_ctrlZ
# >> str_slice_ctrlZ is faster than str_slice_regex by 2x ± 0.1
# >> str_slice_regex is similar to str_slice_dollar

这是使用Fruity,因此结果与上面的信息没有直接关联,但仍然有用。
以下是一些更新的信息:
# >> Running on Ruby v.2.7.0
# >> word in string
# >>                       user     system      total        real
# >> string[""]:       0.368283   0.000147   0.368430 (  0.368468)
# >> string[//]:       0.329253   0.000080   0.329333 (  0.329466)
# >> string[/$/]:      0.330270   0.000172   0.330442 (  0.330594)
# >> string[/\Z/]:     0.183119   0.000048   0.183167 (  0.183209)
# >> index():          0.358397   0.000289   0.358686 (  0.360185)
# >> include?():       0.352700   0.000196   0.352896 (  0.353056)
# >> match($):         0.761605   0.001502   0.763107 (  0.763297)
# >> match(\Z):        0.631132   0.000507   0.631639 (  0.631767)
# >> match():          0.765219   0.000634   0.765853 (  0.766199)
# >> match2($):        0.394938   0.000128   0.395066 (  0.395173)
# >> match2():         0.391687   0.000080   0.391767 (  0.391879)
# >> match2(\Z):       0.389440   0.000089   0.389529 (  0.389678)
# >> 
# >> word not in string
# >>                       user     system      total        real
# >> string[""]:       0.365097   0.000117   0.365214 (  0.365262)
# >> string[//]:       0.388117   0.000361   0.388478 (  0.389008)
# >> string[/$/]:      0.381933   0.000091   0.382024 (  0.382061)
# >> string[/\Z/]:     0.236101   0.000086   0.236187 (  0.236307)
# >> index():          0.369898   0.000131   0.370029 (  0.370267)
# >> include?():       0.361057   0.000065   0.361122 (  0.361202)
# >> match($):         0.409296   0.000390   0.409686 (  0.410103)
# >> match(\Z):        0.258903   0.000074   0.258977 (  0.259095)
# >> match():          0.399220   0.000070   0.399290 (  0.399386)
# >> --------------------
# >> Running each test 16384 times. Test will take about 1 second.
# >> str_slice_ctrlz is similar to str_slice_ctrlZ
# >> str_slice_ctrlZ is faster than str_slice_dollar by 2x ± 0.1
# >> str_slice_dollar is similar to str_slice_regex

"Finding if a sentence contains a specific phrase in Ruby" 是相关的。

干得好!但是对我来说仍然很奇怪,str [substr] 比正则表达式慢。理论上,str [substr] 是实现良好性能的最简单方法。我希望日本人能够优化它。 - Nakilon
我还没有查看源代码,但我怀疑str[substr]实际上将括号内的每个子字符串都视为正则表达式,强制重新编译模式,或跳转到执行固定字符串搜索的其他代码。在C/C++中进行固定字符串搜索很容易,因此必须进行其他处理。或者,我做错了基准测试,但我不这么认为。 - the Tin Man

5

如果想了解“其他方式”的概述,可以从维基百科上的“字符串搜索算法”文章开始。

使用“子串索引”对字符串进行索引是一种明显的加速方法,正如Martin所提到的那样,只有在对同一字符串进行多次搜索时才适用:


2
如果你只想检查一个子字符串是否在字符串中,你可以使用:str[substr]。它会返回子字符串或者nil。

0
据我所知,除非你准备事先构建某种搜索元数据(例如索引),否则没有什么“神奇”的方法可以快速搜索子字符串。这样做很可能会浪费更多的时间,除非你经常搜索相同的字符串。
由于搜索模式很简单,我建议避免使用正则表达式。

0
如果你确信运行时系统中的功能(例如字符串内搜索等)不够快,无法满足你的应用需求,那么可以尝试实现 KMP 算法。
但是,现代运行时系统的实现者可能已经为你完成了这个工作。

-2

最好的方法是使用indexOf,正则表达式较慢。


展示你的断言数据。 - the Tin Man

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接