Remove adjacent identical elements in a Ruby Array? - ruby

Ruby 1.8.6
I have an array containing numerical values. I want to reduce it such that sequences of the same value are reduced to a single instance of that value.
So I want
a = [1, 1, 1, 2, 2, 3, 3, 3, 3, 2, 2, 2, 3, 3, 3]
to reduce to
[1, 2, 3, 2, 3]
As you can see, Array#uniq won't work in this case.
I have the following, which works:
(a.size - 1).downto(1) { |i| a[i] = nil if a[i - 1] == a[i] }
Can anyone come up with something less ugly?

For the simplest, leanest solution, you could use the method Enumerable#chunk:
a.chunk(&:itself).map(&:first)
The itself method is Ruby 2.2+. Use {|n| n} if you are stuck in an older Ruby, or my backports gems.
It was introduced in Ruby 1.9.2. If you're unlucky enough to be using older rubies, you could use my backports gem and require 'backports/1.9.2/enumerable/chunk'.

a.inject([]){|acc,i| acc.last == i ? acc : acc << i }

I can think only of this
a.each_with_index{|item,i| a[i] = nil if a[i] == a[i+1] }.compact
but it is more or less the same.

Unless you are very concerned with the speed that block will calculate at, I would suggest you simply add this line to the end of your block to get the desired output:
a.compact!
That will just remove all the nil elements you introduced to the array earlier (the would-be duplicates), forming your desired output: [1, 2, 3, 2, 3]
If you want another algorithm, here is something far uglier than yours. :-)
require "pp"
a = [1, 1, 1, 2, 2, 3, 3, 3, 3, 2, 2, 2, 3, 3, 3]
i = 0
while i < a.size do
e = a[i]
j = i
begin
j += 1
end while e == a[j]
for k in i+1..j-1 do
a[k] = nil
end
i = j
end
pp a
a.compact!
pp a
Gives you the output:
[1, nil, nil, 2, nil, 3, nil, nil, nil, 2, nil, nil, 3, nil, nil]
[1, 2, 3, 2, 3]
In my opinion, your code is fine. Just add the a.compact! call and you are sorted.

another solution:
acc = [a[0]]
a.each_cons(2) {|x,y| acc << y if x != y}
or
a.each_cons(2).inject([a[0]]) {|acc, (x,y)| x == y ? acc : acc << y}

If the numbers are all single digits 0-9: a.join.squeeze('0-9').each_char.to_a should work.

Related

Ruby: bsearch returning nil

I have an sorted array of number, and wanted to check if number is in the array or not. I think I should use bsearch here but it return nil even if the number is present in the array, and only give positive result for the middle element.
Here is what I test.
>> [1,2,3,4,5].bsearch { |value| value <=> 1}
>> nil
>> [1,2,3,4,5].bsearch { |value| value <=> 3}
>> 3
I am using Ruby version 2.3.8
You are using Array#bsearch in Find-any mode.
As the documentation says, your block needs to satisfy the following requirements:
All positive-evaluating elements precede all zero-evaluating elements.
All positive-evaluating elements precede all negative-evaluating elements.
All zero-evaluating elements precede all negative-evaluating elements.
Your block violates those requirements.
Let's run through an example. In the first iteration, Array#bsearch will pick the middle element, which is 3 and pass it to your block. Your block returns 1, which means that the element you are searching for is greater than 3 and thus must be to the right of the current element. So, Array#bsearch throws away the left half of the array, and we are left with [4, 5]. Now again, Array#bsearch picks the "middle" element, which is 4 and passes it to your block. Your block returns 1, which is a positive number and thus means that the element you are looking for is greater than 4 and thus must be in the right half of the array. So again, Array#bsearch throws away the left half and we are left with [5]. Same thing, Array#bsearch passes 5 to the block, your block returns 1 which is a positive number and thus means we need to look to the right, except we are already at the end of the array, and thus we can conclude that the element you are looking for is not in the array.
So, basically, you are just telling Array#bsearch to look in the wrong place.
Let's look again at the documentation:
These make sense as blocks in find-any mode:
a = [0, 4, 7, 10, 12]
a.map {|element| 7 <=> element } # => [1, 1, 0, -1, -1]
a.map {|element| -1 <=> element } # => [-1, -1, -1, -1, -1]
a.map {|element| 5 <=> element } # => [1, 1, -1, -1, -1]
a.map {|element| 15 <=> element } # => [1, 1, 1, 1, 1]
This would not make sense:
a = [0, 4, 7, 10, 12]
a.map {|element| element <=> 7 } # => [-1, -1, 0, 1, 1]
So, essentially you wrote your block exactly like the block that the documentation explicitly says is wrong, and exactly the opposite way of what the documentation tells you to.
If you flip the two operands in the block, so that your block looks like the example in the documentation says it should look, it will work:
[1, 2, 3, 4, 5].bsearch { |value| 0 <=> value } #=> nil
[1, 2, 3, 4, 5].bsearch { |value| 1 <=> value } #=> 1
[1, 2, 3, 4, 5].bsearch { |value| 2 <=> value } #=> 2
[1, 2, 3, 4, 5].bsearch { |value| 3 <=> value } #=> 3
[1, 2, 3, 4, 5].bsearch { |value| 4 <=> value } #=> 4
[1, 2, 3, 4, 5].bsearch { |value| 5 <=> value } #=> 5
[1, 2, 3, 4, 5].bsearch { |value| 6 <=> value } #=> nil

How to improve algorithm efficiency for nested loop

Given a list of integers and a single sum value, return the first two values (from the left) that add up to form the sum.
For example, given:
sum_pairs([10, 5, 2, 3, 7, 5], 10)
[5, 5] (at indices [1, 5] of [10, 5, 2, 3, 7, 5]) add up to 10, and [3, 7] (at indices [3, 4]) add up to 10. Among them, the entire pair [3, 7] is earlier, and therefore is the correct answer.
Here is my code:
def sum_pairs(ints, s)
result = []
i = 0
while i < ints.length - 1
j = i+1
while j < ints.length
result << [ints[i],ints[j]] if ints[i] + ints[j] == s
j += 1
end
i += 1
end
puts result.to_s
result.min
end
It works, but is too inefficient, taking 12000 ms to run. The nested loop is the problem of inefficiency. How could I improve the algorithm?
Have a Set of numbers you have seen, starting empty
Look at each number in the input list
Calculate which number you would need to add to it to make up the sum
See if that number is in the set
If it is, return it, and the current element
If not, add the current element to the set, and continue the loop
When the loop ends, you are certain there is no such pair; the task does not specify, but returning nil is likely the best option
Should go superfast, as there is only a single loop. It also terminates as soon as it finds the first matching pair, so normally you wouldn't even go through every element before you get your answer.
As a style thing, using while in this way is very unRubyish. In implementing the above, I suggest you use ints.each do |int| ... end rather than while.
EDIT: As Cary Swoveland commented, for a weird reason I thought you needed indices, not the values.
require 'set'
def sum_pairs(arr, target)
s = Set.new
arr.each do |v|
return [target-v, v] if s.include?(target-v)
s << v
end
nil
end
sum_pairs [10, 5, 2, 3, 7, 5], 10
#=> [3, 7]
sum_pairs [10, 5, 2, 3, 7, 5], 99
#=> nil
I've used Set methods to speed include? lookups (and, less important, to save only unique values).
Try below, as it is much more readable.
def sum_pairs(ints, s)
ints.each_with_index.map do |ele, i|
if ele < s
rem_arr = ints.from(i + 1)
rem = s - ele
[ele, rem] if rem_arr.include?(rem)
end
end.compact.last
end
One liner (the fastest?)
ary = [10, 0, 8, 5, 2, 7, 3, 5, 5]
sum = 10
def sum_pairs(ary, sum)
ary.map.with_index { |e, i| [e, i] }.combination(2).to_a.keep_if { |a| a.first.first + a.last.first == sum }.map { |e| [e, e.max { |a, b| a.last <=> b.last }.last] }.min { |a, b| a.last <=> b.last }.first.map{ |e| e.first }
end
Yes, it's not really readable, but if you add methods step by step starting from ary.map.with_index { |e, i| [e, i] } it's easy to understand how it works.

How to group adjacent numbers that are the same

I need to pack if there are at least two adjacent numbers which are same in the format <number : number_of_occurrences >.
This is my input:
[2,2,2,3,4,3,3,2,4,4,5]
And the expected output:
"2:3,3,4,3:2,2,4:2,5"
So far I tried:
a = [1, 1, 1, 2, 2, 3, 2, 3, 4, 4, 5]
a.each_cons(2).any? do |s , t|
if s == t
If it's equal try a counter maybe, but thats not working.
You can use Enumerable#chunk_while (if you're on Ruby >= 2.3):
a.chunk_while { |a, b| a == b }
.flat_map { |chunk| chunk.one? ? chunk.first : "#{chunk.first}:#{chunk.size}" }
.join(',')
#=> "2:3,3,4,3:2,2,4:2,5"
You can also use Enumerable#chunk (Ruby ~1.9.3, maybe earlier):
a.chunk(&:itself)
.flat_map { |_, chunk| chunk.one? ? chunk.first : "#{chunk.first}:#{chunk.size}" }
.join(',')
#=> "2:3,3,4,3:2,2,4:2,5"
You could chunk elements together when they're equal, you could also slice the array between elements that are distinct (slice_when has been added in Ruby 2.2 ):
[2, 2, 2, 3, 4, 3, 3, 2, 4, 4, 5].slice_when { |a, b| a != b }.map do |ints|
if ints.size == 1
ints[0]
else
"#{ints[0]}:#{ints.size}"
end
end.join(',')
# "2:3,3,4,3:2,2,4:2,5"
It's mostly a matter of taste, both methods can achieve perfectly similar results, just like select and reject.
arr = [2, 2, 2, 3, 4, 3, 3, 2, 4, 4, 5]
arr.drop(1).each_with_object([[arr.first, 1]]) do |e,a|
a.last.first == e ? a[-1][-1] += 1 : a << [e, 1]
end.map { |a| a.join(':') }.join(',')
#=> "2:3,3:1,4:1,3:2,2:1,4:2,5:1"

How to select the first n elements from Ruby array that satisfy a predicate?

I want to get all items from an array, which satisfy a predicate. Once I see an element that doesn't satisfy, I should stop iterating. For example:
[1, 4, -9, 3, 6].select_only_first { |x| x > 0}
I'm expecting to get: [1, 4]
This is how you want :
arup#linux-wzza:~> pry
[1] pry(main)> [1, 4, -9, 3, 6].take_while { |x| x > 0}
=> [1, 4]
[2] pry(main)>
Here is the documentation :
arup#linux-wzza:~> ri Array#take_while
= Array#take_while
(from ruby site)
------------------------------------------------------------------------------
ary.take_while { |arr| block } -> new_ary
ary.take_while -> Enumerator
------------------------------------------------------------------------------
Passes elements to the block until the block returns nil or false, then stops
iterating and returns an array of all prior elements.
If no block is given, an Enumerator is returned instead.
See also Array#drop_while
a = [1, 2, 3, 4, 5, 0]
a.take_while { |i| i < 3 } #=> [1, 2]
lines 1-20/20 (END)
If you're exploring other solution, this works too:
[1, 4, -9, 3, 6].slice_before { |x| x <= 0}.to_a[0]
You have to change x > 0 to x <=0.
That was an excellent answer Arup. My method is slightly more complicated.
numbers = [1,4,-9,3,6]
i = 0
new_numbers = []
until numbers[i] < 0
new_numbers.push(numbers[i])
i+= 1
end
=> [1,4]

How to take one element out of an array and put in front?

a = [1,2,3]
b = [2,1,3]
What is the best way to get b from a?
My inelegant solution:
x = 2
y = a - [x]
b = y.unshift(x)
a.unshift a.delete(2)
This appends the recently deleted object (here 2).
Beware that, if the object in question appears more than once in the array, all occurences will be deleted.
In case you want only the first occurrence of an object to be moved, try this:
a = [1,2,3,2]
a.unshift a.delete_at(a.index(2))
# => [2, 1, 3, 2]
a.unshift a.slice!(a.index(2)||0)
# => [2, 1, 3]
If there are multiple instances, only the first instance is moved to the front.
If the element doesn't exist, then a is unchanged.
If you wanted to move elements of an array arbitrarily, you could do something like this:
Code
# Return a copy of the receiver array, with the receiver's element at
# offset i moved before the element at offset j, unless j == self.size,
# in which case the element at offset i is moved to the end of the array.
class Array
def move(i,j)
a = dup
case
when i < 0 || i >= size
raise ArgumentError, "From index is out-of-range"
when j < 0 || j > size
raise ArgumentError, "To index is out-of-range"
when j < i
a.insert(j, a.delete_at(i))
when j == size
a << a.delete_at(i)
when j > i+1
a.insert(j-1, a.delete_at(i))
else
a
end
end
end
With Ruby v2.1, you could optionally replace class Array with refine Array. (Module#refine was introduced experimentally in v2.0, but was changed substantially in v2.1.)
Demo
arr = [1,2,3,4,5] #=> [1, 2, 3, 4, 5]
arr.move(2,1) #=> [1, 3, 2, 4, 5]
arr.move(2,2) #=> [1, 2, 3, 4, 5]
arr.move(2,3) #=> [1, 2, 3, 4, 5]
arr.move(2,4) #=> [1, 2, 4, 3, 5]
arr.move(2,5) #=> [1, 2, 4, 5, 3]
arr.move(2,6) #=> ArgumentError: To index is out-of-range

Resources