Related
I'm working to update the SVG::Graph gem, and have made many improvements to my version, but have found a bottleneck with multiple array sorting.
There is a "sort_multiple" function built in, which keeps an array of arrays (all of equal size) sorted by the first array in the group.
The issue I have is that this sort works well on truly random data, and really badly on sorted, or almost sorted data:
def sort_multiple( arrys, lo=0, hi=arrys[0].length-1 )
if lo < hi
p = partition(arrys,lo,hi)
sort_multiple(arrys, lo, p-1)
sort_multiple(arrys, p+1, hi)
end
arrys
end
def partition( arrys, lo, hi )
p = arrys[0][lo]
l = lo
z = lo+1
while z <= hi
if arrys[0][z] < p
l += 1
arrys.each { |arry| arry[z], arry[l] = arry[l], arry[z] }
end
z += 1
end
arrys.each { |arry| arry[lo], arry[l] = arry[l], arry[lo] }
l
end
this routine appears to use a variant of the Lomuto partition scheme from wikipedia: https://en.wikipedia.org/wiki/Quicksort#Lomuto_partition_scheme
I have an array of 5000+ numbers, which is previously sorted, and this function adds about 1/2 second per chart.
I have modified the "sort_multiple" routine with the following:
def sort_multiple( arrys, lo=0, hi=arrys[0].length-1 )
first = arrys.first
return arrys if first == first.sort
if lo < hi
...
which has "fixed" the problem with sorted data, but I was wondering if there is any way to utilise the better sort functions built into ruby to get this sort to work much quicker. e.g. do you think I could utilise a Tsort to speed this up? https://ruby-doc.org/stdlib-2.6.1/libdoc/tsort/rdoc/TSort.html
looking at my benchmarking, the completely random first group appears to be very fast.
Current benchmarking:
def sort_multiple( arrys, lo=0, hi=arrys[0].length-1 )
if lo < hi
p = partition(arrys,lo,hi)
sort_multiple(arrys, lo, p-1)
sort_multiple(arrys, p+1, hi)
end
arrys
end
def partition( arrys, lo, hi )
p = arrys[0][lo]
l = lo
z = lo+1
while z <= hi
if arrys[0][z] < p
l += 1
arrys.each { |arry| arry[z], arry[l] = arry[l], arry[z] }
end
z += 1
end
arrys.each { |arry| arry[lo], arry[l] = arry[l], arry[lo] }
l
end
first = (1..5400).map { rand }
second = (1..5400).map { rand }
unsorted_arrys = [first.dup, second.dup, Array.new(5400), Array.new(5400), Array.new(5400)]
sorted_arrys = [first.sort, second.dup, Array.new(5400), Array.new(5400), Array.new(5400)]
require 'benchmark'
Benchmark.bmbm do |x|
x.report("unsorted") { sort_multiple( unsorted_arrys.map(&:dup) ) }
x.report("sorted") { sort_multiple( sorted_arrys.map(&:dup) ) }
end
results:
Rehearsal --------------------------------------------
unsorted 0.070699 0.000008 0.070707 ( 0.070710)
sorted 0.731734 0.000000 0.731734 ( 0.731742)
----------------------------------- total: 0.802441sec
user system total real
unsorted 0.051636 0.000000 0.051636 ( 0.051636)
sorted 0.715730 0.000000 0.715730 ( 0.715733)
#EDIT#
Final accepted solution:
def sort( *arrys )
new_arrys = arrys.transpose.sort_by(&:first).transpose
new_arrys.each_index { |k| arrys[k].replace(new_arrys[k]) }
end
I have an array of 5000+ numbers, which is previously sorted, and this function adds about 1/2 second per chart.
Unfortunately, algorithms implemented in Ruby can become quite slow. It's often much faster to delegate the work to the built-in methods that are implemented in C, even if it comes with an overhead.
To sort a nested array, you could transpose it, then sort_by its first element, and transpose again afterwards:
arrays.transpose.sort_by(&:first).transpose
It works like this:
arrays #=> [[3, 1, 2], [:c, :a, :b]]
.transpose #=> [[3, :c], [1, :a], [2, :b]]
.sort_by(&:first) #=> [[1, :a], [2, :b], [3, :c]]
.transpose #=> [[1, 2, 3], [:a, :b, :c]]
And although it creates several temporary arrays along the way, the result seems to be an order of magnitude faster than the "unsorted" variant:
unsorted 0.035297 0.000106 0.035403 ( 0.035458)
sorted 0.474134 0.003065 0.477199 ( 0.480667)
transpose 0.001572 0.000082 0.001654 ( 0.001655)
In the long run, you could try to implement your algorithm as a C extension.
I confess I don't fully understand the question and don't have the time to study the code at the link, but it seems that you have one sorted array that you are repeatedly mutating only slightly, and with each change you may mutate several other arrays, each a little or a lot. After each set of mutations you re-sort the first array and then rearrage each of the other arrays consistent with the changes in indices of elements in the first array.
If, for example, the first array were
arr = [2,4,6,8,10]
and the change to arr were to replace the element at index 1 (4) with 9 and the element at index 3 (8) with 3, arr would become [2,9,6,3,10], which, after re-sorting, would be [2,3,6,9,10]. We could do that as follows:
new_arr, indices = [2,9,6,3,10].each_with_index.sort.transpose
#=> [[2, 3, 6, 9, 10], [0, 3, 2, 1, 4]]
Therefore,
new_arr
#=> [2, 3, 6, 9, 10]
indices
#=> [0, 3, 2, 1, 4]
the intermediate calculation being
[2,9,6,3,10].each_with_index.sort
#=> [[2, 0], [3, 3], [6, 2], [9, 1], [10, 4]]
Considering that
new_array == [2,9,6,3,10].values_at(*indices)
#=> true
we see that each of the other arrays, after having been mutated, can be sorted to conform with the sorting of indices in the first array with the following method, which is quite fast.
def sort_like_first(a, indices)
a.values_at(*indices)
end
For example,
a = [5,4,3,1,2]
a.replace(sort_like_first a, indices)
a #=> [5, 1, 3, 4, 2]
a = %w|dog cat cow pig owl|
a.replace(sort_like_first a, indices)
a #=> ["dog", "pig", "cow", "cat", "owl"]
In fact, it's not necessary to sort each of the other arrays until they are required in the calculations.
I would now like to consider a special case, namely, when only a single element in the first array is to be changed.
Suppose (as before)
arr = [2,4,6,8,10]
and the element at index 3 (8) is to be replaced with 5, resulting in [2,4,6,5,10]. A fast sort can be done with the following method, which employs a binary search.
def new_indices(arr, replace_idx, replace_val)
new_loc = arr.bsearch_index { |n| n >= replace_val } || arr.size
indices = (0..arr.size-1).to_a
index_removed = indices.delete_at(replace_idx)
new_loc -= 1 if new_loc > replace_idx
indices.insert(new_loc, index_removed)
end
arr.bsearch_index { |n| n >= replace_val } returns nil if n >= replace_val #=> false for all n. It is for that reason I have tacked on || arr.size.
See Array#bsearch_index, Array#delete_at and Array#insert.
Let's try it. If
arr = [2,4,6,8,10]
replace_idx = 3
replace_val = 5
then
indices = new_indices(arr, replace_idx, replace_val)
#=> [0, 1, 3, 2, 4]
Only now can we replace the element of arr at index replace_idx.
arr[replace_idx] = replace_val
arr
#=> [2, 4, 6, 5, 10]
We see that the re-sorted array is as follows.
arr.values_at(*indices)
#=> [2, 4, 5, 6, 10]
The other arrays are sorted as before, using sort_like_first:
a = [5,4,3,1,2]
a.replace(sort_like_first(a, indices))
#=> [5, 4, 1, 3, 2]
a = %w|dog cat cow pig owl|
a.replace(sort_like_first(a, indices))
#=> ["dog", "cat", "pig", "cow", "owl"]
Here's a second example.
arr = [2,4,6,8,10]
replace_idx = 3
replace_val = 12
indices = new_indices(arr, replace_idx, replace_val)
#=> [0, 1, 2, 4, 3]
arr[replace_idx] = replace_val
arr
#=> [2, 4, 6, 12, 10]
The first array sorted is therefore
arr.values_at(*indices)
#=> [2, 4, 6, 10, 12]
The other arrays are sorted as follows.
a = [5,4,3,1,2]
a.replace(sort_like_first a, indices)
a #=> [5, 4, 3, 2, 1]
a = %w|dog cat cow pig owl|
a.replace(sort_like_first a, indices)
a #=> ["dog", "cat", "cow", "owl", "pig"]
I'm fairly new to learning Ruby so please bear with me. I am working on a 7 kyu Ruby coding challenge and I've been tasked with finding how many people are left on the bus (first value represents people on, second value, people off) please look at comments in code for more detail.
below is a test example:
([[10, 0], [3, 5], [5, 8]]), # => should return 5"
This is my solution so far:
def number(bus_stops)
bus_stops.each{ | on, off | on[0] -= off[1] }
end
bus_stops
# loop through the array
# for the first array in the nested array subtract second value from first
# add the sum of last nested array to first value of second array and repeat
# subtract value of last element in nested array and repeat
How can I approach this? any resources you would recommend?
There would be many ways to achieve this. Here is one with inject
arr.map { |inner_array| inner_array.inject(&:-) }.inject(&:+)
Iterate over the arrays and calculate the count at each position of how many people would have been left on the bus (this can return negative integers). This will return
[10, -2, -3]
[10 on, none off][3 on, 5 off][5 on, 8 off]
Then inject a + operator between each element to calculate the sum of people left on the bus. This only works if you count from 0 people on and 0 people off.
Here are two other ways to compute the desired result.
arr = [[10, 0], [3, 5], [5, 8]]
Use Array#transpose
arr.transpose.map(&:sum).reduce(:-)
#=> 5
The steps are as follows.
a = arr.transpose
#=> [[10, 3, 5], [0, 5, 8]]
b = a.map(&:sum)
#=> [18, 13] ([total ons, total offs])
b.reduce(:-)
#=> 5
Use Matrix methods
require 'matrix'
(Matrix.row_vector([1] * arr.size) * Matrix[*arr] * Matrix.column_vector([1,-1]))[0,0]
#=> 5
The steps are as follows.
a = [1] * arr.size
#=> [1, 1, 1]
b = Matrix.row_vector(a)
#=> Matrix[[1, 1, 1]]
c = Matrix[*arr]
#=> Matrix[[10, 0], [3, 5], [5, 8]]
d = b * c
#=> Matrix[[18, 13]]
e = Matrix.column_vector([1,-1])
#=> Matrix[[1], [-1]]
f = d * e
#=> Matrix[[5]]
f[0,0]
#=> 5
See Matrix::[], Matrix::row_vector, Matrix::column_vector and Matrix#[]. Notice that the instance method [] is documented in Object.
sum takes a block, which is really simple in this case:
arr = [[10, 0], [3, 5], [5, 8]]
p arr.sum{|on, off| on - off} # => 5
So you were very close.
I have a nested array in ruby, similar to the one below, but with quite a few more nested arrays.
arr=[[1,2,3],[4,5,6],[7,8,9]]
Is there a way to get every second element within the nested array, and add x to that element? I have used the code below, but if there is a quicker way I'd love to know.
x = 5
arr[0][1], arr[1][1], arr[2][1] = arr[0][1]+x, arr[1][1]+x, arr[2][1]+x
I have tried to use arr.map!{|f,l| [f, l + 1]}, but I get the result arr == [[1, 3], [4, 6], [7, 9]]
EDIT:
So the outcome should be arr == [[1,2+x,3],[4,5+x,6],[7,8+x,9]]
Any time you find yourself writing that kind of code over and over, you should look to use a loop! Ruby commonly uses iterators for performing looping. Array#each is an iterator which loops over an Array, and lets you operate on each element one at a time.
Since you know that you want to add x to the second element in each, this is trivially:
arr.each {|e| e[1] += x }
This mutates arr (and arr's subarrays) in place. If you want to return a mutated copy, you would use map, which is like each except the return value is the output of the block, rather than the input to it.
# First we clone each subarray with Array#dup, then we iterate the
# clones, adding x to the second element. Because we're using `map`
# the result will be a new array, rather than the original arr, leaving
# the original unmodified.
new_arr = arr.map(&:dup).each {|e| e[1] += x }
You're close! You can use map to loop through each sub-array and the ruby += operator to add x to the second element of each. The trick with map is that you'll need to return the entire sub-array in each loop, which would look like:
arr.map { |a| a[1] += x; a }
#=> [[1, 7, 3], [4, 10, 6], [7, 13, 9]]
Here's another one:
arr = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
x = 5
arr.map { |a, b, c| [a, b + x, c] }
#=> [[1, 7, 3], [4, 10, 6], [7, 13, 9]]
Note that map returns a new array. If you want to modify the existing array, there's map!.
I got this array:
array = [["R.M", 20], ["R.U-CS", 3], ["R.M-TIC", 3], ["R.J.CONF", 20]]
I want to sum the numeric values, so I have converted this into a single array by using flatten:
array = array.flatten
#=> ["R.M", 20, "R.U-CS", 3, "R.M-TIC", 3, "R.J.CONF", 20]
then:
a = []
array.each do |r|
a << r if r.class == Fixnum
end
a
This works, but I am searching for a simpler solution, maybe converting the array into an "only numeric" array first:
[20, 3, 3, 20]
Not that obvious, but you can pass a class or module to grep to select its instances:
array.grep(Integer)
#=> [20, 3, 3, 20]
or
array.grep(Numeric)
#=> [20, 3, 3, 20]
or to grep_v in order to exclude its instances:
array.grep_v(String)
#=> [20, 3, 3, 20]
This works because grep uses === for pattern matching. If you pass a class, it invokes Module#=== (each class is a module) which returns true if the object is an instance of the receiver:
Integer === 20
#=> true
Integer === 'foo'
#=> false
However, your actual problem can be solved much easier. Given this array:
array = [["R.M", 20], ["R.U-CS", 3], ["R.M-TIC", 3], ["R.J.CONF", 20]]
And assuming that the second value is always a number, you can use sum with a block:
array.sum { |string, number| number }
#=> 46
or to sum each sub-array's last value: (which is the same in this case)
array.sum(&:last)
#=> 46
One might use class’ case-equality Module#=== to determine a class of elements and to reject strings:
array = ["R.M", 20, "R.U-CS", 3, "R.M-TIC", 3, "R.J.CONF", 20]
array.reject &String.method(:===)
#⇒ [
# [0] 20,
# [1] 3,
# [2] 3,
# [3] 20
# ]
array.delete_if { |x| x.kind_of(String) }
array.map { |item| item if item.is_a? Integer }.compact
# [20, 3, 3, 20]
More compact:
array.select { |item| item.is_a? Integer }
Or even shorter:
array.select &Integer.method(:===)
Follow the below code
array.reject { |c| c.class.name.eql?('String') }
With is_a?
array.reject { |c| c.is_a?(String) }
If your arrays are formatted like your example then this will work:
array.select.with_index { |_,i| i.odd? } #=> [20, 3, 3, 20]
use select to select only integers from the array
array = ["R.M", 20, "R.U-CS", 3, "R.M-TIC", 3, "R.J.CONF", 20]
array.select { |tmp| tmp.is_a?(Integer) }
#=> [20, 3, 3, 20]
use select! if you want to permanently change the content of your variable array with the integer-only content
I'd like to split an array into sub arrays of a specified length.
I know that .each_slice will chunk an array into equal length subarrays with the remainder leftover like so:
a = [1,2,3,4,5,6,7,8,9,10]
a.each_slice(3).to_a => [[1,2,3],[4,5,6],[7,8,9],[10]]
However, say I want the output like this:
=> [[1],[2,3],[4,5,6],[7,8,9,10]]
Is there a method in ruby for slicing an array into different specified lengths depending on the arguments you give it?
Try this
a = [1,2,3,4,5,6,7,8,9,10]
slices = [1,2,3,4].map { |n| a.shift(n) }
This slices the array into pieces
NB, this mutates the original array.
I cannot see how to improve on #akuhn's answer, but here are a couple of other methods that could be used.
a = [1,2,3,4,5,6,7,8,9,10,11]
slice_sizes = [1,2,3,4]
#1 Stab out slices
def variable_slice(a, slice_sizes)
last = 0
slice_sizes.each_with_object([]) do |n,arr|
arr << a[last,n]
last += n
end
end
variable_slice(a, slice_sizes)
#=> [[1], [2, 3], [4, 5, 6], [7, 8, 9, 10]]
#2 Use recursion
def variable_slice(a, slice_sizes)
return [] if slice_sizes.empty?
i, *rest = slice_sizes
[a.first(i)].concat variable_slice(a[i..-1], rest)
end
variable_slice(a, slice_sizes)
#=> [[1], [2, 3], [4, 5, 6], [7, 8, 9, 10]]