I am currently doing a Ruby challenge and get the error Terminated due to timeout
for some testcases where the string input is very long (10.000+ characters).
How can I improve my code?
def alternatingCharacters(s)
counter = 0
s.chars.each_with_index { |char, idx| counter += 1 if s.chars[idx + 1] == char }
return counter
end
You don’t say what the challenge is, so I’m going to assume that the code shown is functionally correct - it counts the number of times two consecutive characters in the string have the same value. The each_with_index
executes the block once for each character in the input string. So in your long string example, that’s 10K times. So let’s look at what is happening inside the loop.
- We convert the input string to an array of characters, and we know it has 10K characters
- We look up the next item and compare it to the char passed into the block
- On the last item in the array,
idx + 1
is outside the bounds of the array, and in another less forgiving language it might have thrown an error
So as the string gets longer, the performance degrades exponentially, as we keep converting ever longer strings into arrays of characters, and we do it more times. With 10K, it takes about 7.4s on my ageing laptop. With 20K, it takes 33.1s, which probably explains your timeout issue.
I moved the conversion to the character array outside the loop so it only happens once:
def alternating_characters_better(sample)
counter = 0
sample_list = sample.chars
sample_list.each_with_index { |char, idx| counter += 1 if sample_list[idx + 1] == char }
counter
end
which brought the execution time down to 3.8ms for 10K and 7.3ms for 20K
I thought I could make it better by simplifying it further:
def alternating_characters_not_as_good(sample)
counter = 0
prev_char = ''
sample.chars.each do |char|
counter += 1 if char == prev_char
prev_char = char
end
counter
end
but this came in slightly worse at 11.1ms for 20K (although it doesn’t go out of bounds at the end of the array).
I did it like this
sample = SecureRandom.alphanumeric(10_000).split ''
counter = 0
while sample.present? do
matcher = sample.shift
counter += 1 if matcher == sample[0]
end
counter
Virtually instantaneous on my MacBook Pro
I ran this under ruby-prof
and it took 83ms on my machine. However, about 80% of that was consumed by the generation of the sample string for the test SecureRandom.alphanumeric()
is a bit of a CPU hog, it would appear…
Anyway, I switched to ('AA' * 10_000).chars
for the string generation and it dropped to 24ms. And although it’s slightly slower than the earlier attempts, it feels cleaner and more idiomatic, so if absolute performance isn’t the only goal I’d probably go with @itsterrys answer with a slight modification to avoid the dependency on present?
# frozen_string_literal: true
sample = ('AA' * 10_000).chars
counter = 0
while sample.length.positive?
matcher = sample.shift
counter += 1 if matcher == sample[0]
end
puts counter
1 Like