【问题标题】:What's the complexity of this loop这个循环的复杂度是多少
【发布时间】:2019-04-05 18:26:31
【问题描述】:

我试图弄清楚解决滑动最大值问题的这段代码的时间复杂度是多少

我尝试了 2 个嵌套循环,但复杂度为 O(n*k),我认为下面列出的代码不太复杂

  res=[]
  for i in 0..(array.length-k) do 
   res <<  array.slice(i,k).sort[-1]
  end
  return res 

我想知道使用的默认方法 (Ruby) 的复杂性是什么,以及它们如何影响这个循环的复杂性。谢谢

【问题讨论】:

  • for 并没有真正在 Ruby 中使用,因为总有更好的方法来表达事物。考虑:(array.length - k).times do |i| 作为替代方案。
  • 并非所有人都知道“最大滑动问题”是什么。你能定义它(通过编辑)吗?我刚查了一下。给定K,“找到每K个连续元素的最大元素。”。
  • @CarySwoveland 抱歉,我应该提前解释一下
  • [3, 3 , 5, 5, 7] 应该是滑动最大值(至少这是我在制作算法时的意图)。如果不是,请让我知道您的示例的结果应该如何。
  • 我很抱歉。你说的对。 在找到问题的定义后,我误解了要返回的内容! ??????

标签: ruby time-complexity space-complexity


【解决方案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

这里我们仅在从数组中删除的数字等于最大值或添加的值大于当前最大值时才计算最大值。

【讨论】:

  • 如果b.shift &lt; max_nb.last &lt; max_n 你不需要计算b.max。相反,max_n = b.shift == max_n ? b.max : [max_n, b.last].max.
  • @CarySwoveland 似乎是 6 个,就速度而言,另一个是 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(&: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

【讨论】:

  • 感谢您完善结果。我对 ruby​​ 并没有那么先进(至少从语法角度来看)。现在我可以根据输入看到每个命题都有自己的优缺点。谢谢
  • 公平地说,这不是@tadmans 提出的解决方案,而是我的解决方案。他实际上更接近于 OP 只是更红宝石。从红宝石的角度来看,我仍然会站在我的身后?
  • 在发布我的原始基准测试结果后,@engineersmnky 在我的代码中发现了一个缺陷。修复后,我重新运行了基准测试并更新了上面的答案。我的代码现在比以前好一些。
  • @CarySwoveland 发布了一个修改后的更高效的版本,似乎比 k > 65 的其他选项更好
  • 我修改了基准,加入了@engineersmnky 的答案(“Engineer 1”)以及该答案的一个变体(“Engineer 2”)。
【解决方案3】:

从简化代码开始通常会有所帮助:

(array.length-k).times.map |i|
  array.slice(i,k).max
end

sort 可以在此处删除,使其成为该操作的线性时间。通常sort 被认为是O(log n)

这最终是 O(n2) 除非您可以消除内部或外部循环。

如果这里的目标是在数组的所有可能子集中找到所有最大值,那么可能有一种算法可以做到这一点。

【讨论】:

  • 其实有很多。但是,我正在寻找 O(n) 的最佳解决方案。我看到有人在他的算法中使用了队列,但问题是他在 for 循环中使用了一个 while 循环,有人告诉我他的解决方案是最优的,我不同意他的意见。 youtube.com/watch?v=J6o_Wz-UGvc
  • 似乎array.each_cons(k).map(&amp;:max) 会相当有效地做到这一点
  • @engineersmnky 哦,它有效。你能说出它的复杂性吗?
  • @WonderBoy 请记住,您可以通过测试不同长度的随机数据所需的时间来找出此类方法的复杂性。如果您在每个长度为 1..n 的情况下运行 100 次迭代,您将获得相当准确的图片。
【解决方案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(&: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 命题(第三个)对于更大的数组来说更快。

【讨论】:

  • 我发布了另一个基准,既是出于我陈述的原因,也是因为我想向您展示这些通常是如何编写的。顺便说一句,编写方法的更类似于 Ruby 的方式是 (array.length-k+1).times.with_object([]) { |i,res| res &lt;&lt; array.slice(i,k).max }。见Enumerable#each_with_object。随着您对 Ruby 越来越熟悉,您将一直使用该方法。哦,还有一件事:这是你最后一次使用for
【解决方案5】:

方法的工作原理

我可以用一个例子来解释我在这里使用的方法的基本思想。假设数组是[1 , 3, 2, 4]k = 3,我们已经计算了[1, 3, 2].max #=&gt; 3。然后因为1 &lt; [1, 3, 2].max,我们知道[3, 2].max == [1, 3, 2].max #=&gt; true。因此,我们可以通过比较两个已知值来计算[3, 2, 4].max[[3, 2].max, 4].max =&gt; [3, 4] =&gt; 4。对于k = 3,这节省了一点计算时间,但它会随着k 的值而增加。

计算效率

以下方法的(最坏情况)计算复杂度为 O(n*k) (n = arr.size),但随机生成的数组平均需要类似 (n-k)*(1+2*(k-1)/k) 的操作。

为数组的每个 k-slice a 计算 a.max 的蛮力方法需要 (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生成,传递给block,block变量赋值并进行block计算。

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]

其余的计算类似。

【讨论】:

  • 我计算了包括你的算法在内的每个算法所需的实时性(使用基准)。事实证明,你的比@tadman 提出的 array.each_cons(k).map(&:max) 更快,但我的在我删除排序后排在第一位,只是把 .max 代替。显然默认方法(如排序)对运行时有很大影响,我需要考虑它们
  • WB,请在您的测试中增加k,直到我的方法最快。您可能希望以答案的形式发布您的基准。
  • Cary 与 OP See Here 的原始命题相比,您的算法似乎存在缺陷
  • 感谢@engineersmnky 的提醒。 arr[i-k+1, i] 应该是 arr[i-k+1, k],这就是现在的样子。我在您提供的 repl.it 链接上检查了它,并获得了一条蓝丝带作为奖励。哦,我刚刚意识到我也需要改变我的基准。嗯,这可能证实了我之前对为什么我的算法在基准测试中没有做得更好的困惑。我现在重新运行基准测试。我太兴奋了!
  • @CarySwoveland 没问题。更新了 repl 以反映新方法下的基准测试。在更大的k 集合上,额外实现一个比each_cons 效果更好的枚举器
猜你喜欢
  • 1970-01-01
  • 2015-06-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多