1

Trying to make a function that counts the number of letters that appear more than once anywhere in a string (not necessarily together, and not the the number of times they repeat). This is what I have:

def num_repeats(string)

    repeat = []

    i1 = 0
    i2 = 1
    while i1 < string.length 
        while  i2 < string.length
            if (string[i1] == string[i2]) && (!repeat.include? string[i1]) 
                repeat << string[i1]
            end
            i2 +=1
        end 
        i1+=1
    end

    return repeat.length
end

puts(num_repeats('sldhelanlaskjkajksda'))

For some reason, it only pushes the first letter of the string if that first letter has been used in the rest of the string, but after that, it seems like the method stops looping through the rest of the string.

I'd like to know first why the current code is not working and if there is a way to fix it, and I also welcome other better solutions.

jgozal
  • 1,480
  • 6
  • 22
  • 43
  • It is not clear what you mean by "the number of letters that repeat in a string." Do you mean letters that appear more than once? Or, do you mean letters that appear consecutively more than once? – sawa Aug 16 '15 at 13:54
  • I mean letters that appear more than once anywhere in the string, not necessarily together. I'll edit the question to make it clearer. – jgozal Aug 16 '15 at 13:55
  • 1
    You forgot to initialize `i2` back to 1 after inner while loop ends, and hence the condition `i2 < string.length` remains true for rest of the outer while loop after one iteration. – Wand Maker Aug 16 '15 at 13:59
  • thanks @WandMaker. So I should put i2 =1 inside the first loop? im not sure if i follow you – jgozal Aug 16 '15 at 14:08
  • @jgozal Yes do that. And after that, you can debug further. Also, note that you can `puts` or `p` to print values for debugging. Its more fun to debug and fix the issues. Also, look at suggested answers later here in this thread as Rubyists will do things bit differently due to richness of Ruby API – Wand Maker Aug 16 '15 at 14:11
  • hi @WandMaker. This is what the method returns if I just return the array: ["l", "d", "h", "e", "a", "n", "k", "j"]. So it looks like it adds all characters from the string without repeating them, but it still adds those that are not being repeated in the original string. – jgozal Aug 16 '15 at 14:17
  • @jgozal There is some wrong with your logic - your logic seems to pick unique characters instead of repeat ones - look at amaiaeskisabel's answer which fixes your program for the desired output – Wand Maker Aug 16 '15 at 16:19
  • @WandMaker it wasn't a problem about my logic. My logic, as I described it in sawa's answer was correct. It just wasn't being implemented correctly in the code because everytime the i2 loop returned false, the loop never ran again. Both sawa and amaiaeskisabel pointed out how I could fix this. – jgozal Aug 16 '15 at 16:26
  • nonetheless, what I will admit is that although my logic was correct, it is very dirty and probably not the most efficient logic. I have yet to learn algorithms and improve the way I go about solving programming problems. – jgozal Aug 16 '15 at 16:29

6 Answers6

7

Here is an orthodox way to do it:

'sldhelanlaskjkajksda'.each_char.group_by(&:itself).count{|_, v| v.length > 1}
# => 6


The reason your code does not work is because, (i) once the i2 loop terminates, you increment i1, and try for another i2 loop in the next i1 iteration, but since i2 hasn't been touched after failed to satisfy the loop condition, it will not satisfy the condition again, and the i2 loop will never run again, and (ii) you are initializing i2 to a constant.

To fix it, initialize i2 within i1 loop at the beginning, and initialize it to i2 = i1 + 1, not 1.

sawa
  • 165,429
  • 45
  • 277
  • 381
  • Thank you sawa. Let me see if I can sort of explain what I'm trying to do in my code. In the first while loop I am getting the first letter of the string, then I want to loop again through the whole string to see if this first letter exists elsewhere in the string. If it does and if it doesn't already exist in the array of 'repeated letters' then add it to the array. Do that for every letter in the string, then return the length of the repeat array. Does this make sense? – jgozal Aug 16 '15 at 14:07
  • @jgozal Sort of. But the expression is un-Rubyistic. You usually do not need to refer to index to do this kind of thing. – sawa Aug 16 '15 at 14:10
  • I am coming from other languages - hence why my code doesn't look like ruby. Is there any way to implement exactly what I described in ruby? This is pure curiosity – jgozal Aug 16 '15 at 14:11
  • 1
    @jgozal Yes. Wand Maker's comment to your question will work. The problem you have is not about Ruby. Your algorithm is wrong. – sawa Aug 16 '15 at 14:14
  • i just noticed your edits. Thanks for the explanation – jgozal Aug 16 '15 at 14:41
2

Another way:

s = 'sldhelanlaskjkajksda'

a = s.chars
  #=> ["s", "l", "d", "h", "e", "l", "a", "n", "l", "a",
  #    "s", "k", "j", "k", "a", "j", "k", "s", "d", "a"] 
a.difference(a.uniq).uniq.size
  #=> 6

where Array#difference is defined in my answer here.

We have:

b = a.uniq
  #=> ["s", "l", "d", "h", "e", "a", "n", "k", "j"] 
c = a.difference(b)
  #=> ["l", "l", "a", "s", "k", "a", "j", "k", "s", "d", "a"] 
d = c.uniq
  #=> ["l", "a", "s", "k", "j", "d"] 
d.size
  #=> 6 
Community
  • 1
  • 1
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
2

None of these answers consider that OP asked for repeating letters

But this does:

'sldhe-lanlas-kjkajksda'.scan(/([a-z])(?=.*\1)/i).uniq.size
#=> 6
pguardiario
  • 53,827
  • 19
  • 119
  • 159
  • Nice use of a positive lookahead. You are certainly correct that the rest of us read "letters" as "characters", in part, I expect, because of the OP's example, which contains only letters. The other answers could be fixed by writing `s.gsub(/[^a-z]/i,'')...`. We must assume what the OP had in mind with regard to case. – Cary Swoveland Aug 17 '15 at 01:24
  • @CarySwoveland is right. For the exercise I had to assume that the string contained only lowercase letters - I did not specify this in my question. Thank you for thinking about the question thoughtfully :-) – jgozal Aug 17 '15 at 14:25
1

This is the solution for your problem

    def num_repeats(string)
        repeat = []
        i1 = 0
        i2 = 1

        while i1 < string.length
            while  i2 < string.length
                if (string[i1] == string[i2]) && !(repeat.include? string[i1]) 
                    repeat << string[i1]
                end
               i2 +=1
            end 
            i1+=1
            i2 = i1 + 1
        end
        return repeat.length
    end 
    puts(num_repeats('sldhelanlaskjkajksda'))
amaiaeskisabel
  • 334
  • 1
  • 5
0

Here is bit simpler (hopefully) and little Ruby-ish, solution:

def num_repeats(string)

    # chars in string
    chars = string.split('')

    # initialize map - for each char, count is initialized to 0
    hash = chars.uniq.inject({}) { |h, c| h[c] = 0; h}

    # for each char in string, lets count its occurrences
    chars.each do |c| 
        hash[c] += 1
    end

    # now lets pick those entries from the map where the count is > 1
    hash_with_repeated_chars = hash.select {|k, v| v > 1 }

    # now lets pick the chars that are repeated by picking keys of hash
    repeated_chars = hash_with_repeated_chars.select { |k, v| k}

    # return the count of repeated chars
    return repeated_chars.count
end

p num_repeats('abc')    # Prints 0
p num_repeats('abbc')   # Prints 1
p num_repeats('abbcc')  # Prints 2
p num_repeats('aabbcc') # Prints 3

I also have a Ruby version that is different from all other answers (and hence, bit inefficient due to multiple iterations it does internally)

s = 'sldhelanlaskjkajksda'
p s.chars.combination(2).to_a.uniq.map(&:sort).map(&:uniq).select{|a| a.size.eql?(1)}.count
Wand Maker
  • 18,476
  • 8
  • 53
  • 87
0

Create a hash and set the default value to 0. Use the gsub method to remove all white space from the string. Convert the string into an array of string characters using the split method. Iterate over each letter in the array and store the key as each letter and the number of times each letter occurs as the key's value. Finally Remove any key from the hash with a value that is less than 2. Return the hash's length since this corresponds to the number of letters in the hash that have occurred more than once in your original string. Hope this explanation helps and that it answers your question. The code below could be more compact but it is in its current form in the hopes of being more explanatory and informative.

  def counter(string)
  counts = Hash.new(0)
  result = string.gsub(" ","")
  result = result.split('')

  result.each do |letter|
    counts[letter] += 1
  end

  counts.delete_if { |key,value| value < 2}

  return counts.length

end
bdbasinger
  • 175
  • 2
  • 11