#ruby #time-complexity #space-complexity
#ruby #временная сложность #сложность пространства
Вопрос:
Я пытаюсь выяснить, какова временная сложность этого кода, который решает проблему скользящего максимума
Я пробовал использовать 2 вложенных цикла, но это было бы сложнее O (n * k), и я думаю, что приведенный ниже код менее сложный
res=[]
for i in 0..(array.length-k) do
res << array.slice(i,k).sort[-1]
end
return res
Я хочу знать, какова сложность используемых методов по умолчанию (Ruby) и как они влияют на сложность этого цикла. Спасибо
Комментарии:
1.
for
на самом деле не используется в Ruby, потому что всегда есть лучший метод для выражения вещей. Рассмотрим:(array.length - k).times do |i|
в качестве альтернативы.2. Не все из нас знают, что такое «проблема скользящего максимума». Можете ли вы определить это (с помощью редактирования)? Я только что посмотрел это. Дано
K
: «Найдите максимальный элемент для каждых K последовательных элементов»..3. @CarySwoveland извините, я должен был объяснить это заранее
4. [3, 3 , 5, 5, 7] должно быть скользящим максимумом (по крайней мере, таково было мое намерение при создании алгоритма). если нет, пожалуйста, дайте мне знать, каким должен быть результат для вашего примера.
5. Мои извинения. Вы правы. Я неправильно понял, что должно было быть возвращено после того, как нашел определение проблемы ! 😳
Ответ №1:
Вот Enumerator
решение, которое кажется самым быстрым для больших наборов данных (k> ~ 65)
def sliding_max(arr,k)
a = arr.dup
b = a.shift(k)
max_n = n = b.max
Enumerator.new do |y|
y << max_n
loop do
break if a.empty?
b.<<(a.shift)
max_n = b.max if b.shift == max_n || b.last > max_n
y << max_n
end
end.to_a
end
Здесь мы вычисляем max только в том случае, если число, удаленное из массива, было равно max или добавляемое значение больше текущего максимума.
Комментарии:
1. Если
b.shift < max_n
иb.last < max_n
вам не нужно вычислятьb.max
. Скорее,max_n = b.shift == max_n ? b.max : [max_n, b.last].max
.2. @CarySwoveland, похоже, 6, полдюжины других, что касается скорости (возможно, из-за выделения памяти для нового
Array
), но вы правы, что это тоже сработает
Ответ №2:
Вот еще один тест, который показывает, как меняется относительная эффективность по мере k
увеличения размера каждого фрагмента.
require 'benchmark'
def test(arr, k)
puts "Testing for k = #{k}"
Benchmark.bm(11) do |x|
x.report("Wonder Boy") {
res=[]
for i in 0..(arr.length-k) do
res << arr.slice(i,k).max
end
res
}
x.report("Tadman") { arr.each_cons(k).map(amp;:max) }
x.report("Cary") {
(k..arr.size-1).each_with_object([arr.first(k).max]) do |i,a|
mx = a.last
a << (arr[i-k] < mx ? [mx, arr[i]] : arr[i-k 1, k]).max
end
}
x.report("Engineer 1") {
a = arr.dup
b = a.shift(k)
max_n = n = b.max
Enumerator.new do |y|
y << max_n
loop do
break if a.empty?
b.<<(a.shift)
max_n = b.max if b.shift == max_n || b.last > max_n
y << max_n
end
end.to_a
}
x.report("Engineer 2") {
a = arr.dup
b = a.shift(k)
max_n = n = b.max
Enumerator.new do |y|
y << max_n
loop do
break if a.empty?
b.<<(a.shift)
max_n = (b.shift == max_n) ? b.max : [max_n, b.last].max
y << max_n
end
end.to_a
}
end
end
arr = 10000.times.map { rand(100) }
arr.first(4)
#=> [61, 13, 41, 82]
test(arr, 3)
Testing for k = 3
user system total real
Wonder Boy 0.021185 0.004539 0.025724 ( 0.025695)
Tadman 0.004801 0.000000 0.004801 ( 0.004809)
Cary 0.004542 0.000000 0.004542 ( 0.004568)
Engineer 1 0.003998 0.000000 0.003998 ( 0.004005)
Engineer 2 0.003427 0.000000 0.003427 ( 0.003438)
test(arr, 10)
Testing for k = 10
user system total real
Wonder Boy 0.003102 0.000000 0.003102 ( 0.003105)
Tadman 0.003205 0.000012 0.003217 ( 0.003225)
Cary 0.003286 0.000000 0.003286 ( 0.003292)
Engineer 1 0.003387 0.000000 0.003387 ( 0.003397)
Engineer 2 0.003092 0.000000 0.003092 ( 0.003100)
test(arr, 30)
Testing for k = 30
user system total real
Wonder Boy 0.011111 0.000000 0.011111 ( 0.011139)
Tadman 0.010568 0.000000 0.010568 ( 0.010572)
Cary 0.004292 0.000000 0.004292 ( 0.004301)
Engineer 1 0.004197 0.000000 0.004197 ( 0.004203)
Engineer 2 0.003759 0.000000 0.003759 ( 0.003766)
test(arr, 100)
Testing for k = 100
user system total real
Wonder Boy 0.007409 0.000035 0.007444 ( 0.007437)
Tadman 0.005771 0.000914 0.006685 ( 0.006703)
Cary 0.002773 0.000000 0.002773 ( 0.002782)
Engineer 1 0.003213 0.000000 0.003213 ( 0.003222)
Engineer 2 0.003138 0.000005 0.003143 ( 0.003150)
test(arr, 1000)
Testing for k = 1000
user system total real
Wonder Boy 0.019694 0.000000 0.019694 ( 0.019696)
Tadman 0.031178 0.012383 0.043561 ( 0.043571)
Cary 0.005782 0.000000 0.005782 ( 0.005788)
Engineer 1 0.002446 0.000000 0.002446 ( 0.002431)
Engineer 2 0.002395 0.000000 0.002395 ( 0.002396)
Наиболее показательным результатом почти наверняка является результат для k = 100
.
Комментарии:
1. спасибо за доработку результатов. Я не настолько продвинут в ruby (по крайней мере, с точки зрения синтаксиса). Теперь я вижу, что у каждого предложения есть свои плюсы и минусы в зависимости от входных данных. Спасибо
2. Честно говоря, это было не предложенное @tadmans решение, а скорее мое. На самом деле он намного ближе к OP, просто более рубиновый. Кроме того, я бы все еще стоял за своим с точки зрения ruby 😊
3. После публикации моих оригинальных результатов тестирования @enginersmnky обнаружил недостаток в моем коде. После исправления этого я повторно запустил тест и обновил свой ответ выше. Теперь мой код работает несколько лучше, чем раньше.
4. @CarySwoveland опубликовал исправленную и более эффективную версию, которая, похоже, превосходит другие параметры для k> 65
5. Я пересмотрел тест, включив ответ @enginersmnky («Инженер 1»), а также вариант этого ответа («Инженер 2»).
Ответ №3:
Часто помогает начать с сокращения кода до самого необходимого:
(array.length-k).times.map |i|
array.slice(i,k).max
end
Где sort
здесь можно убрать, сделав линейное время для этой операции. Обычно sort
считается O(log n).
В конечном счете это O (n2), если только вы не сможете исключить внутренний или внешний цикл.
Если цель здесь состоит в том, чтобы найти все максимальные значения во всех возможных подмножествах массива, вероятно, существует алгоритм, который может это сделать.
Комментарии:
1. на самом деле их много. но я ищу оптимальное решение, которым является O (n). Я видел кого-то, кто использовал queue в своем алгоритме, но проблема в том, что он использует циклы while внутри цикла for, и кто-то сказал мне, что его решение оптимально, а я не разделял его мнение. youtube.com/watch?v=J6o_Wz-UGvc
2. Кажется,
array.each_cons(k).map(amp;:max)
сделал бы это довольно эффективно3. @engineersmnky о, это работает. Можете ли вы определить его сложность?
4. @WonderBoy Помните, что вы можете выяснить сложность подобного метода, проверив время, необходимое для случайных данных различной длины. Вы получите довольно точную картину, если выполните 100 итераций на каждой длиной 1 ..n.
Ответ №4:
require 'benchmark'
def sliding_maximum(k, array)
time=Benchmark.realtime do
=begin
my proposition >
res=[]
for i in 0..(array.length-k) do
res << array.slice(i,k).max
end
#return res
=end # time => 8.9185999968322e-05
=begin
@Cary's proposition >
(k..array.size-1).each_with_object([array.first(k).max]) do |i,a|
mx = a.last
a << (array[i-k] < mx ? [mx, array[i]] : array[i-k 1, i]).max
end
=end # time => 0.0001353049992758315
=begin
@tadman proposition
#array.each_cons(k).map(amp;:max)
=end time 7.903100049588829e-05
end
p time
end
sliding_maximum(3, [1, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9, 3, 5, 7, 9])
Это код, в котором я пытался вычислить выполнение в реальном времени каждого предложения, включая мое. Не стесняйтесь изменять переданный массив или K, чтобы увидеть разницу в выполнении. Что касается меня, я вижу, что предложение @tadman (третье) работает быстрее для больших массивов.
Комментарии:
1. Я опубликовал еще один тест, как по моей заявленной причине, так и потому, что я хотел показать вам, как они обычно пишутся. кстати, более похожий на Ruby способ написания вашего метода — это
(array.length-k 1).times.with_object([]) { |i,res| res << array.slice(i,k).max }
. Смотрите Перечислимый#each_with_object . По мере того, как вы будете лучше знакомиться с Ruby, вы будете использовать этот метод постоянно. О, еще одна вещь: это последний раз, когда вы будете использоватьfor
.
Ответ №5:
Как работает метод
Я могу объяснить основную идею подхода, который я использовал здесь, на примере. Предположим, что массивом были [1 , 3, 2, 4]
и k = 3
, и мы уже вычислили [1, 3, 2].max #=> 3
. Тогда, поскольку 1 < [1, 3, 2].max
мы знаем это [3, 2].max == [1, 3, 2].max #=> true
. Следовательно, мы можем вычислить [3, 2, 4].max
, сравнив два известных значения: [[3, 2].max, 4].max => [3, 4] => 4
. Это небольшая экономия вычислительного времени для k = 3
, но она будет увеличиваться со значением k
.
Вычислительная эффективность
Приведенный ниже метод имеет (в наихудшем случае) вычислительную сложность O( n*k
) ( n = arr.size
), но случайно сгенерированные массивы в среднем должны требовать чего-то вроде (n-k)*(1 2*(k-1)/k)
операций.
Методы грубой силы, которые вычисляют a.max
для каждого k
фрагмента a
массива, требуют (n-k)*k
операций. Следовательно, для случайно сгенерированных массивов отношение количества операций, требуемых моим методом, к количеству операций, используемых методами грубой силы, составляет приблизительно
(n-k)*(1 2*(k-1)/k)/(n-k)*k
#=> (1 2*(k-1)/k)/k
Для k = 3
это соотношение равно 0.77
, но этот метод все еще может быть в невыгодном положении из-за вычислительных затрат. По мере k
увеличения числитель приближается 3
, поэтому отношение приближается 3/k
. Поэтому, по сравнению с методами грубой силы, мой метод будет приносить все более значительные дивиденды по мере k
продвижения.
Код
def sliding_max(arr, k)
return nil if k > arr.size
(k..arr.size-1).each_with_object([arr.first(k).max]) do |i,a|
mx = a.last
a << (arr[i-k] < mx ? [mx, arr[i]] : arr[i-k 1, k]).max
end
end
Примеры
sliding_max([1, 4, 2, 3, 2, 1, 0], 3)
#=> [4, 4, 3, 3, 2]
sliding_max([1, 2, 3, 4, 5, 6, 7], 3)
#=> [3, 4, 5, 6, 7] (fastest)
sliding_max([7, 6, 5, 4, 3, 2, 1], 3)
#=> [7, 6, 5, 4, 3] (slowest)
Объяснение
Давайте пошагово рассмотрим метод для следующих аргументов.
arr = [1, 4, 2, 3, 2, 1, 4]
k = 3
return nil if k > arr.size
#=> return nil if 3 > 7 => false
a = [arr.first(k).max]
#=> [[1, 4, 2].max] => [4]
enum = (k..arr.size-1).each_with_object(a)
#=> #<Enumerator: 3..6:each_with_object([4])>
Генерируется первый элемент и передается в блок, а переменным блока присваиваются значения.
i, a = enum.next
#=> [3, [4]]
i #=> 3
a #=> [4]
Теперь выполняется вычисление блока.
mx = a.last
#=> 4
arr[i-k] < mx
#=> arr[3-3] < mx => 1 < 4 => true
итак, выполните
b = [mx, arr[i]].max
#=> [4, arr[3]].max => [4, 3].max => 4
a << b
#=> [4, 4]
Следующее значение генерируется enum
, передается в блок, переменным блока присваиваются значения и выполняется вычисление блока.
i, a = enum.next
#=> [4, [4, 4]]
i #=> 4
a #=> [4, 4]
mx = a.last
#=> 4
arr[i-k] < mx
#=> arr[4-3] < 4 => 4 < 4 => false
Итак, на этот раз мы должны выполнить более дорогостоящее вычисление.
b = arr[i-k 1, k].max
#=> arr[4-3 1, 4].max => [2, 3, 2].max => 3
a << b
#=> [4, 4, 3]
Остальные вычисления аналогичны.
Комментарии:
1. Я вычислил реальное время, необходимое для каждого из алгоритмов, включая ваш (используя бенчмарк). Оказывается, ваш работает быстрее, чем array.each_cons(k).map(amp;: max) , предложенный @tadman, но мой появился первым после того, как я удалил сортировку и просто поставил вместо нее .max. По-видимому, методы по умолчанию (например, sort) оказывают большое влияние на время выполнения, и мне нужно принимать их во внимание
2. ВБ, пожалуйста, увеличивайте
k
время тестирования, пока мой метод не станет самым быстрым. Возможно, вы захотите опубликовать свой тест в виде ответа.3. Кэри, похоже, в вашем алгоритме есть недостаток по сравнению с исходным предложением OP, Смотрите Здесь
4. Спасибо за предупреждение, @engineersmnky.
arr[i-k 1, i]
должно было бытьarr[i-k 1, k]
, что и есть сейчас. Я проверил это на repl.it вы предоставили ссылку и были вознаграждены синей лентой. О, я только что понял, что мне также нужно изменить свой бенчмарк. Хммм, это может подтвердить мое предыдущее недоумение относительно того, почему мой алгоритм не показал лучших результатов в тестировании. Сейчас я перезапущу бенчмарк. Я так взволнован!5. @CarySwoveland нет проблем. Обновил repl, чтобы отразить сравнительный анализ в соответствии с вашей новой методологией. С дополнительной реализацией перечислителя, который работает лучше, чем
each_cons
на большихk
наборах