【问题标题】:A more efficient 'remove duplicates' function更有效的“删除重复”功能
【发布时间】:2018-07-03 21:03:03
【问题描述】:

我管理有时超过 10,000 行的 Google 表格列表。对于行数高达 5,000 左右的工作表,下面提到的删除重复项功能可以正常工作。但是对于超过 5,000 的任何内容,我都会收到“超过最大执行时间”错误。我将不胜感激有关如何使代码更高效的一些说明,即使对于 10k+ 行的工作表也能顺利运行。

function removeDuplicates() {
  var sheet = SpreadsheetApp.getActiveSheet();
  var data = sheet.getDataRange().getValues();
  var newData = new Array();
  for(i in data){
    var row = data[i];
    var duplicate = false;
    for(j in newData){
      if(row.join() == newData[j].join()){
        duplicate = true;
      }
    }
    if(!duplicate){
      newData.push(row);
    }
  }
  sheet.clearContents();
  sheet.getRange(1, 1, newData.length, newData[0].length).setValues(newData);
}

【问题讨论】:

  • 您可以在duplicate=true; 之后添加break; 我不确定哪个更有效,但您可以尝试使用 indexOf()。
  • @pnuts,事实上,我没有,因为我不知道任何存在。你能把我引到他们那里吗?
  • 你可以使用集合

标签: javascript google-apps-script google-sheets


【解决方案1】:

有几件事使您的代码变慢。让我们看看你的两个for 循环:

for (i in data) {
  var row = data[i];
  var duplicate = false;

  for (j in newData){
    if (row.join() == newData[j].join()) {
      duplicate = true;
    }
  }

  if (!duplicate) {
    newData.push(row);
  }
}

从表面上看,您做的是正确的事情:对于原始数据中的每一行,检查新数据是否已经有匹配的行。如果没有,则将该行添加到新数据中。但是,在此过程中,您需要做很多额外的工作。

例如,在任何给定时间,data 中的一行在newData 中将不超过一个匹配行。但是在您的内部for 循环中,在找到一个匹配项后,它仍会继续检查newData 中的其余行。解决方案是在duplicate = true; 之后添加break; 以停止迭代。

还请考虑对于任何给定的jnewData[j].join() 的值将始终相同。假设您在 data 中有 100 行,并且没有重复(最坏的情况)。到您的函数完成时,您将计算 newData[0].join() 99 次、newData[1].join() 98 次......总而言之,您将完成近 5,000 次计算以获得相同的 99 个值。对此的解决方案是memoization,您可以存储计算结果以避免以后再次进行相同的计算。

即使您进行了这两项更改,您的代码的time complexity 仍然是O(n²)。如果您有 100 行数据,在最坏的情况下,内部循环将运行 4,950 次。对于 10,000 行,该数字约为 5000 万。

但是,如果我们摆脱内循环并像这样重新构造外循环,我们可以这样做是 O(n) 时间:

var seen = {};

for (var i in data) {
  var row = data[i];
  var key = row.join();

  if (key in seen) {
    continue;
  }
  seen[key] = true;
  newData.push(row);
}

在这里,我们不是在每次迭代中检查newData 的每一行是否有匹配row 的行,而是将迄今为止我们看到的每一行作为键存储在对象seen 中。然后在每次迭代中,我们只需要检查 seen 是否有一个匹配 row 的键,我们可以在几乎恒定的时间内完成一个操作,或者 O(1) .1

作为一个完整的函数,如下所示:

function removeDuplicates_() {
  const startTime = new Date();
  const sheet = SpreadsheetApp.getActiveSheet();
  const data = sheet.getDataRange().getValues();
  const numRows = data.length;
  const newData = [];
  const seen = {};

  for (var i = 0, row, key; i < numRows && (row = data[i]); i++) {
    key = JSON.stringify(row);
    if (key in seen) {
      continue;
    }
    seen[key] = true;
    newData.push(row);
  }

  sheet.clearContents();
  sheet.getRange(1, 1, newData.length, newData[0].length).setValues(newData);

  // Show summary
  const secs = (new Date() - startTime) / 1000;
  SpreadsheetApp.getActiveSpreadsheet().toast(
    Utilities.formatString('Processed %d rows in %.2f seconds (%.1f rows/sec); %d deleted',
                           numRows, secs, numRows / secs, numRows - newData.length),
    'Remove duplicates', -1);
}

function onOpen() {
  SpreadsheetApp.getActive().addMenu('Scripts', [
    { name: 'Remove duplicates', functionName: 'removeDuplicates_' }
  ]);
}

您会看到,这段代码没有使用row.join(),而是使用JSON.stringify(row),因为row.join() 是脆弱的(例如['a,b', 'c'].join() == ['a', 'b,c'].join())。 JSON.stringify 不是免费的,但对于我们的目的来说这是一个很好的折衷方案。

在我的测试中,这会在 8 秒多一点的时间内处理一个包含 50,000 行和 2 列的简单电子表格,即每秒大约 6,000 行。

【讨论】:

  • 注意,“key-in-object”(key in see)需要 O(log(N)) 时间,而不是 O(1)。所以整个算法需要 O(N * log(N)),而不是 O(N)。如果我们能在恒定时间内做到这一点,那将是一场革命:D
  • @IvanKuckir 来源?
  • @JordanRunning 你应该显示源代码,它是 O(1)。如果您曾经学习过计算机科学,那么显然这是不可能的。当然,存在完美的散列函数和摊销结构,但它们仍然不是 O(1)。
  • @Jordan 运行它是一个哈希图(chrome 只能优化这种情况显然不是重复的模式)。在美好的一天将需要 O(1)。实际上略低,但仍然没有 O(log(n)) 糟糕(理论上最坏的情况是 O(n))。见stackoverflow.com/a/15469844
  • @JordanRunning 似乎 V8 为此使用了Splay Trees。我很惊讶,因为红黑树通常用于此目的(例如,在 .Net 中或用于 C++ 中的 std:: 结构)。无论如何,它们都是 log(N) 结构。
猜你喜欢
  • 2019-11-05
  • 1970-01-01
  • 2020-06-07
  • 1970-01-01
  • 2016-03-15
  • 2019-03-24
  • 2019-07-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多