优化匹配两个字符串模加扰的代码

时间:2018-01-31 04:02:10

标签: ruby

我正在尝试编写一个函数scramble(str1, str2),如果true个字符的一部分可以重新排列以匹配str1,则返回str2,否则返回false 。仅使用小写字母(a-z)。不会包含标点符号或数字。例如:

  • str1 = 'rkqodlw'; str2 = 'world'应该返回true
  • str1 = 'cedewaraaossoqqyt'; str2 = 'codewars'应该返回true
  • str1 = 'katas'; str2 = 'steak'应该返回false

这是我的代码:

def scramble(s1, s2)
  #sorts strings into arrays
  first = s1.split("").sort
  second = s2.split("").sort
  correctLetters = 0
  for i in 0...first.length
    #check for occurrences of first letter
    occurrencesFirst = first.count(s1[i])
    for j in 0...second.length
      #scan through second string
      occurrencesSecond = second.count(s2[j])
      #if letter to be tested is correct and occurrences of first less than occurrences of second
      #meaning word cannot be formed
      if (s2[j] == s1[i]) && occurrencesFirst < occurrencesSecond
        return false
      elsif s2[j] == s1[i]
        correctLetters += 1
      elsif first.count(s1[s2[j]]) == 0
        return false
      end
    end
  end
  if correctLetters == 0
    return false
  end
  return true
end

我需要帮助优化此代码。请给我建议。

2 个答案:

答案 0 :(得分:3)

这是一种有效且类似Ruby的方式。

<强>代码

def scramble(str1, str2)
  h1 = char_counts(str1)
  h2 = char_counts(str2)
  h2.all? { |ch, nbr| nbr <= h1[ch] }
end

def char_counts(str)
  str.each_char.with_object(Hash.new(0)) { |ch, h| h[ch] += 1 }
end

<强>实施例

scramble('abecacdeba', 'abceae')
  #=> true
scramble('abecacdeba', 'abweae')
  #=> false

<强>解释

这三个步骤如下。

str1 = 'abecacdeba'
str2 = 'abceae'

h1 = char_counts(str1)
  #=> {"a"=>3, "b"=>2, "e"=>2, "c"=>2, "d"=>1}
h2 = char_counts(str2)
  #=> {"a"=>2, "b"=>1, "c"=>1, "e"=>2}
h2.all? { |ch, nbr| nbr <= h1[ch] }
  #=> true

最后一句话相当于

2 <= 3 && 1 <= 2 && 1 <= 2 && 2 <=2

方法char_counts构造有时称为“计数哈希”的方法。要了解char_counts的工作原理,请参阅Hash::new,尤其是提供默认值作为新参数的效果的说明。简而言之,如果定义了哈希h = Hash.new(0),那么如果h没有密钥k,则h[k]会返回默认值,此处为0(和哈希没有改变。)

假设,对于不同的数据,

h1 = { "a"=>2 }
h2 = { "a"=>1, "b"=>2 }

然后我们会找到1 <= 2 #=> true2 <= 0 #=> false,因此该方法会返回false。第二个比较是2 <= h1["b"]。由于h1没有密钥"b"h1["b"]会返回默认值0

方法char_counts实际上是编写如下表达方法的简短方法。

def char_counts(str)
  h = {}
  str.each_char do |ch|
    h[ch] = 0 unless h.key?(ch) # instead of Hash.new(0)
    h[ch] = h[c] + 1            # instead of h[c][ += 1
  end
  h                             # no need for this if use `each_with_object`
end

请参阅Enumerable#each_with_objectString#each_char(优先于String.chars,后者生成不需要的临时数组,而前者返回枚举数)和Hash#key?(或{{1 }},Hash#has_key?Hash#include?)。

另类

Hash#member?

我发现方法def scramble(str1, str2) str2.chars.difference(str1.chars).empty? end class Array def difference(other) h = other.each_with_object(Hash.new(0)) { |e,h| h[e] += 1 } reject { |e| h[e] > 0 && h[e] -= 1 } end end 非常有用我建议将它添加到Ruby Core(here)。呃,反应一直没有给人留下深刻的印象。

答案 1 :(得分:1)

一种方式:

def scramble(s1,s2)
  s2.chars.uniq.all? { |c| s1.count(c) >= s2.count(c) }
end

另一种方式:

def scramble(s1,s2)
  pool = s1.chars.group_by(&:itself)
  s2.chars.all? { |c| pool[c]&.pop }
end

又一个:

def scramble(s1,s2)
  ('a'..'z').all? { |c| s1.count(c) >= s2.count(c) }
end

由于这似乎是from codewars,我在那里提交了我的前两个。两者都被接受了,第一个更快了。然后我看到了其他人的解决方案,并看到有人使用('a'..'z')并且速度很快,所以我将其包括在内。

代码战“性能测试”没有明确显示,但它们总共长达45000个字母。因此,我对这些解决方案以及Cary(你的速度太慢而不能被包括在内)进行基准测试,重复进行长时间的重复(并且做了100次):

               user     system      total        real
Stefan 1   0.812000   0.000000   0.812000 (  0.811765)
Stefan 2   2.141000   0.000000   2.141000 (  2.127585)
Other      0.125000   0.000000   0.125000 (  0.122248)
Cary 1     2.562000   0.000000   2.562000 (  2.575366)
Cary 2     3.094000   0.000000   3.094000 (  3.106834)

故事的道德? String#count在这里很快。就像,快得离谱。几乎难以置信快(我实际上不得不运行额外的测试来相信它)。它大约每秒 19亿个字母(100次26个字母乘以2个字符串~45000个字母,全部在0.12秒内)。请注意,与我自己的第一个解决方案的不同之处仅在于我s2.chars.uniq,并且将时间从0.12秒增加到0.81秒。这意味着这个双遍一个字符串大约需要52次通过计数的六倍。计数大约快150倍。我确实期望它非常快,因为它可能只是使用C代码搜索字节数组中的一个字节(编辑:looks like it does),但这种速度仍让我感到惊讶。

代码:

require 'benchmark'

def scramble_stefan1(s1,s2)
  s2.chars.uniq.all? { |c| s1.count(c) >= s2.count(c) }
end

def scramble_stefan2(s1,s2)
  pool = s1.chars.group_by(&:itself)
  s2.chars.all? { |c| pool[c]&.pop }
end

def scramble_other(s1,s2)
  ('a'..'z').all? { |c| s1.count(c) >= s2.count(c) }
end

def scramble_cary1(str1, str2)
  h1 = char_counts(str1)
  h2 = char_counts(str2)
  h2.all? { |ch, nbr| nbr <= h1[ch] }
end
def char_counts(str)
  str.each_char.with_object(Hash.new(0)) { |ch, h| h[ch] += 1 }
end

def scramble_cary2(str1, str2)
  str2.chars.difference(str1.chars).empty?
end
class Array
  def difference(other)
    h = other.each_with_object(Hash.new(0)) { |e,h| h[e] += 1 }
    reject { |e| h[e] > 0 && h[e] -= 1 }
  end
end

Benchmark.bmbm do |x|
  n = 100
  s1 = (('a'..'z').to_a * (45000 / 26)).shuffle.join
  s2 = s1.chars.shuffle.join
  x.report('Stefan 1') { n.times { scramble_stefan1(s1, s2) } }
  x.report('Stefan 2') { n.times { scramble_stefan2(s1, s2) } }
  x.report('Other') { n.times { scramble_other(s1, s2) } }
  x.report('Cary 1') { n.times { scramble_cary1(s1, s2) } }
  x.report('Cary 2') { n.times { scramble_cary2(s1, s2) } }
end