【问题标题】:Sorting Ruby array of array items by length evenly按长度均匀地对数组项的 Ruby 数组进行排序
【发布时间】:2019-11-14 06:45:13
【问题描述】:

在 Ruby 中,如何对数组进行排序,使其项目(也包括数组)按长度大小排列,而不是简单地按长度升序/降序排序。

我想让数组项均匀分布,以便有些项包含大量对象与较小的数组混合。

例如,我有一个包含数组项的数组,其中包含comment 中显示的对象数。为了清楚起见,我将它们分成几块并计算了它们的总大小(请参阅下面的动机)。

[
  # chunk 1, inner total length 5
  [{...}], # 2
  [{...}], # 1
  [{...}], # 1
  [{...}], # 1
  # chunk 2, inner total length 11
  [{...}], # 2
  [{...}], # 2
  [{...}], # 3
  [{...}], # 4
  # chunk 3, inner total length 9
  [{...}], # 3
  [{...}], # 3
  [{...}], # 1
  [{...}], # 2
  # chunk 4, inner total length 15
  [{...}], # 4
  [{...}], # 3
  [{...}], # 4
  [{...}], # 4
]

我想排列数组,使其看起来更像下面。注意:此示例将它们从最小到最大 (1..4) 排序,但这不是必需的。我只想将它们分块,以便内部数组累积长度具有可比性。

[
  # chunk 1, inner total length 10
  [{...}], # 1
  [{...}], # 2
  [{...}], # 3
  [{...}], # 4
  # chunk 2, inner total length 10
  [{...}], # 1
  [{...}], # 2
  [{...}], # 3
  [{...}], # 4
  # chunk 3, inner total length 10
  [{...}], # 1
  [{...}], # 2
  [{...}], # 3
  [{...}], # 4
  # chunk 4, inner total length 10
  [{...}], # 1
  [{...}], # 2
  [{...}], # 3
  [{...}], # 4
]

我这样做的动机是分割外部数组,这样我就可以并行处理内部数组。我不希望其中一个并行进程获得一小块,而另一个进程获得一块非常大的块。

注意:我知道我将有 4 个并行进程,这可能有助于告知如何排列数组中的块。谢谢!

【问题讨论】:

  • 非常有趣的问题,尽管我担心您可能从并行处理每个分布式块中获得的任何收益都会被分配所需的初始排序算法所权衡。
  • 不能均匀排序的数据怎么办?假设我有 4 个长度为 1、2、4 和 4 的数组。它们不能均匀分组。
  • 另外,根据您的参数,(根据我上面的评论,假设存在解决方案)这个问题是 NP-Complete。考虑您希望将大型数据集组织成仅两个块的情况。 Bam,你有partition problem。也就是说,对于某些输入,计算它可能需要很长时间。
  • 作为建议,启发式方法可能会更好。您可以对所有数组进行分块并按数组长度排序。然后遍历块,将带有index % 4 == 0 的项目分配给第一个进程,index % 4 == 1 分配给第二个进程,index % 4 == 2 分配给第三个进程,index % 4 == 3 分配给第四个进程。这不会提供完美的解决方案,但会大致正确。它的好处是更简单,并且初始排序更快。
  • 你也可以通过设置四个worker,然后让master给每个worker分配一份工作来避免预分块;当一个工人完成工作时,它会得到下一个可用的工作。这意味着一些工人可能会做几份大工作,而另一些工人可能会做更多的小工作,最终还是有点平(直到一份工作的差异)。当作业长度无法从输入中预测时,这也适用。例如,parallel gem 可以自动为您完成。

标签: arrays ruby sorting parallel-processing


【解决方案1】:

根据我对 OP 的评论,我将用来获得大致均匀分布的算法:

unchunked_data = [
  [{...}],
  [{...}],
  [{...}],
  [{...}],
  [{...}],
  [{...}],
  [{...}],
  [{...}]
]

sorted_data = unchunked_data.sort_by(&:size)
grouped_data = sorted_data.each_with_index.group_by { |_, index| index % 4 }

grouped_data.each do |process_index, data|
  # each_with_index would put data in an array with its index in sorted_data. Calling map(&:first) removes that index.
  data_without_index = data.map(&:first)
  send_data_to_process(process_index, data_without_index)
end

如果数据与 OP 的示例中显示的一样,这将导致完美的分布。


根据 cmets 中的讨论,您可以通过以下方式取回单个数组中的所有数据,按照原始格式但使用此方法分组:

grouped_data.values.flatten(1)

【讨论】:

  • 谢谢@Glyoko,我倾向于这个解决方案。一件事是我不希望我的数组必须分组(或嵌套在第三个数组中),您可以将它返回到您的unchunked_data (一个数组数组)的形式吗?此外,我倾向于使用grouped_data.map 而不是grouped_data.each,因为我将在删除那些讨厌的索引号的块之外处理它(根据你的 each_with_index 评论)。
  • 这段代码没有改变unchunked_data,所以你仍然可以在事后按照它开始的顺序操作它。
  • 至于grouped_data,我想你可以使用map,但请注意grouped_data 是哈希而不是数组。它的键是process_indexs, (0, 1, 2, 3),值是它们各自的排序和分组块。 (不要忘记关于在值上使用 map(&:first) 的评论仍然适用于此。)
  • 请注意,这种方法不一定比我的答案更均匀;它完全取决于输入...例如,假设原始数组大小为:[1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4]。我的回答会将这些分块为:[[1, 1, 1, 2, 2], [2, 2, 3], [3, 3], [3, 4]] - 即[7, 7, 6, 7] 的总大小;而这个答案会将它们分块为:[[1, 2, 3], [1, 2, 3], [1, 2, 3], [2, 3, 4]] - 即 [6, 6, 6, 9] 的总大小。我不确定哪种算法最有可能提供最佳传播。
  • @mfink 在这种情况下,您可以执行grouped_data.values.flatten(1) 之类的操作。 @TomLord 没错。这个问题实际上是 NP 完全的,所以我们的两个答案都只是“最佳猜测”。根据数据,一种可能比另一种更好。
【解决方案2】:

这不是一个“完美”的解决方案,但这是一种计算量不太大/复杂的方法:

  1. 总结所有内部数组的长度:
total_count = original_list.map(&:count).inject(:+)
  1. 确定您希望在每个并行流程中放入多少项目(在您的情况下为 4 个流程):
chunk_size = total_count / 4
  1. 现在,更难的部分是:算法。我将保持这个非常简单,只遍历数组中的每个项目,然后 "chunk" 直到到达 chunk_size
current_chunk_size = 0

original_list.chunk_while do |inner_array|
  current_chunk_size += inner_array.count
  current_chunk_size = 0 if current_chunk_size >= chunk_size
  current_chunk_size > 0
end

如果您愿意,可以使用 slice_after 等方法实现类似的逻辑。

针对您的原始示例使用此算法:

[
  # chunk 1, inner total length 5
  [{...}], # 2
  [{...}], # 1
  [{...}], # 1
  [{...}], # 1
  # chunk 2, inner total length 11
  [{...}], # 2
  [{...}], # 2
  [{...}], # 3
  [{...}], # 4
  # chunk 3, inner total length 9
  [{...}], # 3
  [{...}], # 3
  [{...}], # 1
  [{...}], # 2
  # chunk 4, inner total length 15
  [{...}], # 4
  [{...}], # 3
  [{...}], # 4
  [{...}], # 4
]

产生结果:

[
  # chunk 1, inner total length 12
  [{...}], # 2
  [{...}], # 1
  [{...}], # 1
  [{...}], # 1
  [{...}], # 2
  [{...}], # 2
  [{...}], # 3

  # chunk 2, inner total length 10
  [{...}], # 4
  [{...}], # 3
  [{...}], # 3

  # chunk 3, inner total length 10
  [{...}], # 1
  [{...}], # 2
  [{...}], # 4
  [{...}], # 3

  # chunk 4, inner total length 8
  [{...}], # 4
  [{...}], # 4
]

...非常接近。

【讨论】:

  • 谢谢,我喜欢这种方法并将对其进行测试。足够接近就好了:)
  • 汤姆,我无法按照你的计算。一方面,original_list.chunk_while 不应该是original_list.flatten(1).chunk_while 吗?您可能希望针对我的答案中的示例运行您的代码。
  • @CarySwoveland 您的数据假设输入可能嵌套很深,但最初的问题(诚然是无效的语法,所以有点模棱两可!)没有说明这一点。您使用的是:[[[0,1], [2], [3], [4]], ...],但我假设它是:[[1, 2, 3, 4], ...]
  • 我明白了。恐怕我还是不跟。如果您可以包含一个简单的示例,将会很有帮助。注意chunk_while 返回一个枚举器,所以我希望你需要end.to_a
【解决方案3】:

这是另一个启发式方法。1我将很快解释该过程。我们得到:

arr = [[[0,1],         [2],        [3],           [4]],
       [[5,6],         [7,8],      [9,10,11],     [12,13,14,15]],
       [[16,17,18],    [19,20,21], [22],          [23,24]],
       [[25,26,27,28], [29,30,31], [32,33,34,35], [36,37,38,39]]
      ]

nbr_groups = 4

让我们先展平一层,然后按大小对结果数组进行排序。

sorted = arr.flatten(1).sort_by(&:size)
  #=> [[2], [3], [4], [22], [0, 1], [5, 6], [7, 8], [23, 24], [9, 10, 11],
  #    [16, 17, 18], [19, 20, 21], [29, 30, 31], [12, 13, 14, 15],
  #    [25, 26, 27, 28], [32, 33, 34, 35], [36, 37, 38, 39]] 

我们需要将sorted 的元素组合成一个数组result,其中包含nbr_groups 数组。这将通过将sorted 的元素“扫描”到result 中来完成。扫描由nbr_groups 正向分配与相同数量的反向分配交替组成。

现在创建一个枚举器。

a = nbr_groups.times.to_a
  #=> [0, 1, 2, 3] 
idx = [*a, *a.reverse].cycle
  #=> #<Enumerator: [0, 1, 2, 3, 3, 2, 1, 0]:cycle>

我建议的启发式首先将sorted的第一个nbr_groups元素分配给result,这样sorted的第一个元素分配给result的第一个元素,@的第二个元素987654335@ 被分配给result 的第二个元素,依此类推。 sorted 的下一个 nbr_group 元素类似地分配给 result,但这次以相反的顺序:sorted 的第 nbr_groups+1 元素分配给 result 的最后一个元素,@987654343 sorted 的第 @'th 元素分配给 result 的倒数第二个元素,以此类推。这些交替分配一直持续到sorted 的所有元素都已分配。

result = sorted.each_with_object(Array.new(nbr_groups) { [] }) do |a,arr| 
  arr[idx.next] << a
end
  #=> [[[2], [23, 24], [9, 10, 11], [36, 37, 38, 39]],
  #    [[3], [7, 8], [16, 17, 18], [32, 33, 34, 35]],
  #    [[4], [5, 6], [19, 20, 21], [25, 26, 27, 28]],
  #    [[22], [0, 1], [29, 30, 31], [12, 13, 14, 15]]]

现在让我们看看这些分配是多么均匀:

result.map { |a| a.sum(&:size) }
  #=> [10, 10, 10, 10] 

这个结果给我带来了微笑。 result 的所有元素大小相同当然纯属巧合。

1.正如@glyoko 在评论中指出的那样,这个问题是 NP 完全的,所以除了最小的问题之外,必须使用启发式算法。

【讨论】:

  • 非常简洁,但我不确定我是否理解“扫描”的帮助。仅使用 idx = a.cycle 似乎也可以。
  • @Glyko,假设sorted = [[0], [1,2], [3,4,5], [6,7,8,9]]nbr_groups = 2。通过扫一扫,result #=&gt; [[[0], [6,7,8,9]], [[1,2], [3,4,5]]result.map { |a| a.sum(&amp;:size) } #=&gt; [5,5]。与idx.cycleresult #=&gt; [[[0], [3,4,5]], [[1,2], [6,7,8,9]]result.map { |a| a.sum(&amp;:size) } #=&gt; [4,6]。对于idx.cycleresult.map { |a| a.sum(&amp;:size) } 将趋于不增加,如果sorted % nbr_groups #=&gt; 0 将不增加。
  • 酷,有道理。每隔一个向后传递一遍往往会使分布均匀。
猜你喜欢
  • 2010-10-02
  • 2011-03-11
  • 1970-01-01
  • 1970-01-01
  • 2021-04-23
  • 2013-08-18
  • 1970-01-01
  • 1970-01-01
  • 2010-10-24
相关资源
最近更新 更多