Разделите массив Ruby на части в соответствии с его полосами

Резюме: Я обнаружил, что основной вопрос здесь заключался в том, можно ли передать блок кода в массив Ruby, который фактически уменьшит содержимое этого массива до другого массива, а не до одного значения (как это делает inject ). Короткий ответ - нет".

Я принимаю ответ, в котором говорится об этом. Спасибо Squeegy за отличную стратегию циклов для удаления полос из массива.

Задача: уменьшить количество элементов массива, не просматривая его явно.
Вход: все целые числа от -10 до 10 (кроме 0) упорядочены случайным образом.
Желаемый результат: массив, представляющий полосы положительных или отрицательных чисел. Например, -3 представляет три последовательных отрицательных числа. 2 представляет два последовательных положительных числа.

Пример сценария:

original_array = (-10..10).to_a.sort{rand(3)-1}
original_array.reject!{|i| i == 0} # remove zero

streaks = (-1..1).to_a # this is a placeholder.  
# The streaks array will contain the output.
# Your code goes here, hopefully without looping through the array

puts "Original Array:"
puts original_array.join(",")
puts "Streaks:"
puts streaks.join(",")
puts "Streaks Sum:"
puts streaks.inject{|sum,n| sum + n}

Примеры результатов:

Original Array:
3,-4,-6,1,-10,-5,7,-8,9,-3,-7,8,10,4,2,5,-2,6,-1,-9
Streaks:
1,-2,1,-2,1,-1,1,-2,5,-1,1,-2
Streaks Sum:
0


Original Array:
-10,-9,-8,-7,-6,-5,-4,-3,-2,-1,1,2,3,4,5,6,7,8,9,10
Streaks:
-10,10
Streaks Sum:
0

Обратите внимание на несколько моментов:

  • Массив полос имеет чередующиеся положительные и отрицательные значения.
  • Сумма элементов массива полос всегда равна 0 (как и сумма оригинала).
  • Сумма абсолютных значений массива штрихов всегда равна 20.

Надеюсь, это ясно!

Изменить: я понимаю, что такие конструкции, как отклонение! фактически проходят через массив в фоновом режиме. Я не исключаю зацикливания, потому что я злой человек. Просто хочу выучить язык. Если необходима явная итерация, ничего страшного.

Ответов (5)

Решение

Начиная с Ruby 1.9 существует гораздо более простой способ решить эту проблему:

original_array.chunk{|x| x <=> 0 }.map{|a,b| a * b.size }

Enumerable.chunk сгруппирует все последовательные элементы массива вместе по выходным данным блока:

>> original_array.chunk{|x| x <=> 0 }
=> [[1, [3]], [-1, [-4, -6]], [1, [1]], [-1, [-10, -5]], [1, [7]], [-1, [-8]], [1, [9]], [-1, [-3, -7]], [1, [8, 10, 4, 2, 5]], [-1, [-2]], [1, [6]], [-1, [-1, -9]]]

Это почти именно то, что запрашивает OP, за исключением того, что необходимо подсчитать результирующие группы, чтобы получить окончательный массив полос.

new_array = original_array.dup
<Squeegy's answer, using new_array>

Да да! Нет цикла по исходному массиву. Хотя внутри dup это MEMCPY, который, я полагаю, можно рассматривать как цикл на уровне ассемблера?

http://www.ruby-doc.org/doxygen/1.8.4/array_8c-source.html

РЕДАКТИРОВАТЬ:; )

original_array.each do |num|
  if streaks.size == 0
    streaks << num
  else
    if !((streaks[-1] > 0) ^ (num > 0))
      streaks[-1] += 1
    else
      streaks << (num > 0 ? 1 : -1)
    end
  end
end

Магия здесь - ^ оператор xor.

true ^ false  #=> true
true ^ true   #=> false
false ^ false #=> false

Поэтому, если последнее число в массиве находится на той же стороне от нуля, что и обрабатываемое число, добавьте его в полосу, в противном случае добавьте его в массив полос, чтобы начать новую полосу. Обратите внимание, что синус true ^ true возвращает, false мы должны отрицать все выражение.

Что ж, вот однострочная версия, если вам так больше нравится:

streaks = original_array.inject([]) {|a,x| (a.empty? || x * a[-1] < 0 ? a << 0 : a)[-1] += x <=> 0; a}

И если даже инъекция для вас слишком зацикливается, вот действительно глупый способ:

  streaks = eval "[#{original_array.join(",").gsub(/((\-\d+,?)+|(\d+,?)+)/) {($1[0..0] == "-" ? "-" : "") + $1.split(/,/).size.to_s + ","}}]"

Но я думаю, что совершенно очевидно, что вам будет лучше с чем-то более простым:

streaks = []
original_array.each do |x|
  xsign = (x <=> 0)
  if streaks.empty? || x * streaks[-1] < 0
    streaks << xsign
  else
    streaks[-1] += xsign
  end
end

В дополнение к тому, что «циклическая» версия намного проще для понимания и поддержки, она выполняется примерно на две трети времени инъекционной версии и примерно в шестой части времени выполнения eval / regexp.

PS: Вот еще одна потенциально интересная версия:

a = [[]]
original_array.each do |x|
  a << [] if x * (a[-1][-1] || 0) < 0
  a[-1] << x
end
streaks = a.map {|aa| (aa.first <=> 0) * aa.size}

Для этого используются два прохода: сначала создается массив массивов полос, а затем массив массивов преобразуется в массив размеров со знаком. В Ruby 1.8.5 это на самом деле немного быстрее, чем приведенная выше inject-версия (хотя в Ruby 1.9 она немного медленнее), но скучный цикл по-прежнему остается самым быстрым.

Больше злоупотреблений строкой, а-ля Гленн Макдональд, только другое:

runs = original_array.map do |e|
  if e < 0
    '-'
  else
    '+'
  end
end.join.scan(/-+|\++/).map do |t|
  "#{t[0..0]}#{t.length}".to_i
end

p original_array
p runs
# => [2, 6, -4, 9, -8, -3, 1, 10, 5, -7, -1, 8, 7, -2, 4, 3, -5, -9, -10, -6]
# => [2, -1, 1, -2, 3, -2, 2, -1, 2, -4]