【问题标题】:I have to write a program that outputs the largest X number, given X number and a huge filesize给定 X 数和巨大的文件大小,我必须编写一个输出最大 X 数的程序
【发布时间】:2019-04-13 01:59:17
【问题描述】:

到目前为止,我有这段代码可以读取文件并使用 Ruby 对其进行排序。但这并不能正确地对数字进行排序,而且我认为这将是低效的,因为文件可以大到 200GB 并且每行都包含一个数字。你能建议其他做什么吗?

File.open("topN.txt", "w") do |file|
  File.readlines("N.txt").sort.reverse.each do |line|
    file.write(line.chomp<<"\n")
  end
End

在每个人都在这里帮助之后,这就是我的代码到目前为止的样子......

begin

  puts "What is the file name?"
  file = gets.chomp

  puts "Whats is the N number?"
  myN = Integer(gets.chomp)

rescue ArgumentError

  puts "That's not a number, try again"
  retry
end

topN = File.open(file).each_line.max(myN){|a,b| a.to_i <=> b.to_i}
puts topN

【问题讨论】:

  • 你能提供一个包含数字的行的例子吗?你真的需要重写文件吗?或者只是打印出最大的数字?
  • 基本上它的工作方式是每个like都会包含一个单独的数字,然后读取它并先输出较大的数字。
  • 你是什么意思最大的数字在前?之后是什么?您能否仅展示一个非常基本的输入文件和预期输出示例?
  • 该文件当然只包含字符串,但它们可能代表数字("123', "-43.78"`)。它们都是整数的表示形式还是浮点数?
  • 写一个程序 topN,给定一个数字 N 和一个任意大的文件,每行包含单独的数字(例如 200Gb 文件),将输出最大的 N 个数字,最高的在前

标签: ruby sorting numbers readfile large-files


【解决方案1】:

对内存中的 200GB 数据进行排序不会非常高效。我会写一个小助手类,它只记住迄今为止添加的 N 个最大的元素。

class SortedList
  attr_reader :list

  def initialize(size)
    @list = []
    @size = size
  end

  def add(element)
    return if @min && @min > element

    list.push(element)
    reorganize_list
  end

  private

  def reorganize_list
    @list = list.sort.reverse.first(@size)
    @min = list.last
  end
end

使用 require N 初始化一个实例,然后将每行解析的值添加到该实例中。

sorted_list = SortedList.new(n)

File.readlines("N.txt").each do |line|
  sorted_list.add(line.to_i)
end

puts sorted_list.list

【讨论】:

  • 有趣...当我提供此列表时... 15 7 25 1 9 3 45 100 65 即使我将 N 设置为 20 或其他任何值,该程序也只会对前 5 个进行排序。 ..
  • 是的,因为我用5 初始化了我的示例SortedList.new(5)。请为您的N 使用一个变量:SortedList.new(n)。我更新了我的答案。
【解决方案2】:

假设

str = File.read(in_filename)
  #=> "117\n106\n143\n147\n63\n118\n146\n93\n"

您可以将该字符串转换为枚举行的枚举器,使用Enumerable#sort_by 以降序对这些行进行排序,将结果行(以换行符结尾)连接起来形成一个可以写入文件的字符串:

str.each_line.sort_by { |line| -line.to_i }.join
  #=> "147\n146\n143\n118\n117\n106\n93\n63\n"

另一种方法是将字符串转换为整数数组,使用Array#sort对数组进行排序,反转生成的数组,然后将数组的元素连接回可以写入文件的字符串:

str.each_line.map(&:to_i).sort.reverse.join("\n") << "\n"
  #=> "147\n146\n143\n118\n117\n106\n93\n63\n"

让我们做一个快速的基准测试。

require 'benchmark/ips'

(str = 1_000_000.times.map { rand(10_000) }.join("\n") << "\n").size

Benchmark.ips do |x|
  x.report("sort_by") { str.each_line.sort_by { |line| -line.to_i }.join }
  x.report("sort")    { str.each_line.map(&:to_i).sort.reverse.join("\n") << "\n" }
  x.compare!
end

Comparison:
                sort:        0.4 i/s
             sort_by:        0.3 i/s - 1.30x  slower

强大的sort又赢了!

【讨论】:

  • 哇!即使有基准测试,您的答案也很棒。让我看看我能不能运行它。
  • 我想弄清楚如何在我更新的代码上使用基准测试,因为我需要知道运行时间/空间复杂度。
  • 你看我是如何使用benchmark的。如果您将代码放入方法中,例如francisco,只需添加一行x.report("sort_by") { francisco(str) }。但是,这并不能帮助您确定时间和空间复杂度。这些度量是通过对您的代码的分析获得的。
【解决方案3】:

您对您的问题发表了以下评论:

“编写一个程序 topN,给定一个数字 N 和一个任意大的文件,该文件在每一行包含单独的数字(例如 200Gb 文件),将输出最大的 N 个数字,最高的在前。”

在我看来,这个问题与问题中描述的问题有些不同,也构成了一个更有趣的问题。我已经在这个答案中解决了这个问题。

代码

def topN(fname, n, m=n)
  raise ArgumentError, "m cannot be smaller than n" if m < n
  f = File.open(fname)
  best = Array.new(n)
  n.times do |i|
    break best.replace(best[0,i]) if f.eof?
    best[i] = f.readline.to_i
  end
  best.sort!.reverse!
  return best if f.eof?
  new_best = Array.new(n)
  cand = Array.new(m)
  until f.eof?
    rd(f, cand)
    merge_arrays(best, new_best, cand)
  end
  f.close
  best
end

def rd(f, cand)
  cand.size.times { |i| cand[i] = (f.eof? ? -Float::INFINITY : f.readline.to_i) }
  cand.sort!.reverse!
end                

def merge_arrays(best, new_best, cand)
  cand_largest = cand.first
  best_idx = best.bsearch_index { |n| cand_largest > n }
  return if best_idx.nil?
  bi = best_idx
  cand_idx = 0
  nbr_to_compare = best.size-best_idx
  nbr_to_compare.times do |i|
    if cand[cand_idx] > best[bi]
      new_best[i] = cand[cand_idx]
      cand_idx += 1
    else 
      new_best[i] = best[bi]
      bi += 1
    end
  end
  best[best_idx..-1] = new_best[0, nbr_to_compare]
end 

示例

让我们创建一个包含 1000 万个整数表示的文件,每行一个。

require 'time'

FName = 'test'

(s = 10_000_000.times.with_object('') { |_,s| s << rand(100_000_000).to_s << "\n" }).size
s[0,27]
  #=> "86752031\n84524374\n29347072\n"
File.write(FName, s)
  #=> 88_888_701

接下来,创建一个简单的方法来使用不同的参数调用 topN 并显示执行时间。

def try_one(n, m=n)
  t = Time.now
  a = topN(FName, n, m)
  puts "#{(Time.new-t).round(2)} seconds"
  puts "top 5: #{a.first(5)}"
  puts "bot 5: #{a[n-5..n-1]}"
end

在测试中,我发现将m 设置为小于n 在计算时间方面是不可取的。要求m &gt;= n 允许对代码进行小幅简化和小幅提高效率。因此,我将m &gt;= n 设为了一项要求。

try_one 100, 100
9.44 seconds
top 5: [99999993, 99999993, 99999991, 99999971, 99999964]
bot 5: [99999136, 99999127, 99999125, 99999109, 99999078]

try_one 100, 1000
9.53 seconds
top 5: [99999993, 99999993, 99999991, 99999971, 99999964]
bot 5: [99999136, 99999127, 99999125, 99999109, 99999078]

try_one 100, 10_000
9.95 seconds
top 5: [99999993, 99999993, 99999991, 99999971, 99999964]
bot 5: [99999136, 99999127, 99999125, 99999109, 99999078]

在这里,我测试了生成 100 最大值的情况,其中文件的行数不同,一次读取 m。如所见,该方法对后一个值不敏感。正如预期的那样,最大的 5 个值和最小的 5 个值(返回的 100 个)在所有情况下都是相同的。

try_one 1_000
9.31 seconds
top 5: [99999993, 99999993, 99999991, 99999971, 99999964]
bot 5: [99990425, 99990423, 99990415, 99990406, 99990399]

try_one 1000, 10_000
9.24 seconds

实际上,返回 1,000 个最大值所需的时间略小于返回最大 100 个值所需的时间。我认为这是不可重现的。前 5 个当然与返回最大的 100 个值时相同。因此,我不会在下面显示该行。返回的 1000 个中最小的 5 个值当然比返回最大的 100 个值时要小。

try_one 10_000
12.15 seconds
bot 5: [99898951, 99898950, 99898946, 99898932, 99898922]

try_one 100_000
13.2 seconds
bot 5: [98995266, 98995259, 98995258, 98995254, 98995252]

try_one 1_000_000
14.34 seconds
bot 5: [89999305, 89999302, 89999301, 89999301, 89999287]

说明

注意重用三个数组,bestcandnew_best。具体来说,我多次替换这些数组的内容,而不是不断地创建新的(可能非常大的)数组,让孤立的数组被垃圾收集。一些测试表明这种方法提高了性能。

我们可以创建一个小例子,然后逐步计算。

fname = 'temp'

File.write(fname, 20.times.map { rand(100) }.join("\n") << "\n")
  #=> 58

此文件包含以下数组中整数的表示形式。

arr = File.read(fname).lines.map(&:to_i)
  #=> [9, 66, 80, 64, 67, 67, 89, 10, 62, 94, 41, 16, 0, 22, 68, 72, 41, 64, 87, 24]

排序,这是:

arr.sort_by! { |n| -n }
  #=> [94, 89, 87, 80, 72, 68, 67, 67, 66, 64, 64, 62, 41, 41, 24, 22, 16, 10, 9, 0]

假设我们想要 5 个最大值。

arr[0,5]
  #=> [94, 89, 87, 80, 72] 

首先,设置两个参数:n,要返回的最大值的数量,m,每次从文件中读取的行数。

n = 5
m = 5

计算如下。

m < n
  #=> false, so do not raise ArgumentError 
f = File.open(fname)
  #=> #<File:temp> 
best = Array.new(n)
  #=> [nil, nil, nil, nil, nil] 
n.times { |i| f.eof? ? (return best.replace(best[0,i])) : best[i] = f.readline.to_i }
best
  #=> [9, 66, 80, 64, 67]
best.sort!.reverse!
  #=> [80, 67, 66, 64, 9] 
f.eof?
  #=> false, so do not return 
new_best = Array.new(n)
  #=> [nil, nil, nil, nil, nil] 
cand = Array.new(m)
  #=> [nil, nil, nil, nil, nil]
puts "best=#{best}".rjust(52) 
until f.eof?
  rd(f, cand)
  merge_arrays(best, new_best, cand)
  puts "cand=#{cand}, best=#{best}"
end
f.close
best
  #=> [94, 89, 87, 80, 72] 

显示如下。

                           best=[80, 67, 66, 64,  9]
cand=[94, 89, 67, 62, 10], best=[94, 89, 80, 67, 67]
cand=[68, 41, 22, 16,  0], best=[94, 89, 80, 68, 67]
cand=[87, 72, 64, 41, 24], best=[94, 89, 87, 80, 72]

【讨论】:

    【解决方案4】:

    Enumerable.max 接受一个指定返回多少元素的参数和一个指定如何比较元素的块:

    N = 5
    p File.open("test.txt").each_line.max(N){|a,b| a.to_i <=> b.to_i}
    

    这不会读取内存中的整个文件;文件被逐行读取。

    【讨论】:

    • 感谢您的回答!此行会替换我的任何原始代码吗?我怎样才能用这一个班轮制作一个正在运行的程序?
    • @FranciscoCantu 现在是一个完整的程序,带有N 变量并输出到屏幕。
    猜你喜欢
    • 2020-02-23
    • 2017-05-30
    • 1970-01-01
    • 2014-05-06
    • 2019-08-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多