views:

292

answers:

4

I recently had to write an anagram solver for a job interview. Here is my code.

class Anagram

  def self.resolves?(first_word, second_word)
    @letter_counts = {}

    # Assuming that anagrams aren't case sensitive.
    first_word.downcase!
    second_word.downcase!

    count_letters(first_word, 1)
    count_letters(second_word, -1)

    (sum_of_counts == 0)
  end

  private
    def self.sum_of_counts
      @letter_counts.inject(0){|sum, n| sum + n[1]}
    end

    def self.count_letters(aword, incrementer)
      aword.each_byte {|c| @letter_counts[c].nil? ? @letter_counts[c] = 1 : @letter_counts[c]+=incrementer }
    end
end

puts Anagram.resolves?('dog','cat')
puts Anagram.resolves?('dog','god')
puts Anagram.resolves?('Jim','Jones')
puts Anagram.resolves?('Hello','olleH')

I particularly don't like the count_letters method and the checking of the existence of the hash value. Where can I improve this code so that's it's more idiomatic Ruby?

Thanks in advance.

+3  A: 

I like this approach:

  1. Take both strings as an arrays of letters.
  2. Sort them.
  3. Compare them.
  4. If they are the same, the words are anagrams.

The code in ruby looks extremely easy:

def self.resolves?(first_word, second_word)
  first_word.downcase!
  second_word.downcase!
  first_word.split(//).sort == second_word.split(//).sort
end

The algorithm is not optimal, but I think is a good solution for a job interview.

pgb
+8  A: 

What about this?:

def anagrams?(w1, w2)
  w1.downcase.split('').sort == w2.downcase.split('').sort
end

Anagrams have the same letters, so the array of letters for each word should be equal.

Alex Reisner
Nice, I figured I was making it harder than it had to be.1) In terms of computation complexity,does my solution win out? I'm not sure how expensive your sorts are vs my individual passes of each string of bytes.2) Looking back at my solution, how would you rewrite the count_letters method? I hate that within the each_byte block I have to check to see if the hash value exists and if so, initialize it, etc.
If you can't analyze the complexity of your code, you prob. won't get the job. Yours is O(n), sorting is typically nlog(n). You can sort alphabetically in o(n) but not with ruby Array::sort
klochner
You could avoid checking `@letter_counts[c].nil?` by setting `@letter_counts.default = 1`, but I agree with klochner that, even in the abstract (big O), this is going to be slower than sorting. Plus, method invocation in Ruby is not cheap...
Alex Reisner
I meant the OP's was O(n), sorting is O(n*log(n)), i.e., slower.
klochner
+3  A: 

Refactoring of your code

Not purely a refactoring, since I just realized yours gives bad output on some string pairs.

Like yours, this is linear in word length, i.e, O(n), n==length of longer word, so it's faster than the sort-based solutions.

I use an array to count each letter frequency (+1 for first word, -1 for second word), then just check for non-zero final values.

This assumes letter-input, so you would have to add a check if you wanted robustness to bad input.

class Anagram

  def self.resolves?(first_word, second_word)
     counts = Array.new(26,0)
     offset = "a"[0]
     first_word.downcase.each_byte {|b| counts[b-offset]+=1}
     second_word.downcase.each_byte{|b| counts[b-offset]-=1}
     counts.detect{|i| i!=0}.nil?
  end
end

klochner
A: 

Regarding you question about the Hash initialisation: Hash#new takes a block in which you can define code to initialize the value for an unset key:

h = Hash.new { 0 }
h[:a] += 1
h.inspect # => {:a=>1}

so you could rewrite your code as follows:

@letter_counts = Hash.new { 0 }
...
aword.each_byte { |c| @letter_counts[c] += incrementer }
severin