【问题标题】:Really fast function to compare the name (full path) of two files比较两个文件的名称(完整路径)的真正快速的功能
【发布时间】:2023-04-06 13:59:01
【问题描述】:

我必须检查我在 FileListBox 中是否有重复的路径(FileListBox 具有某种工作列表或播放列表的作用)。 使用 Delphi 的 SameText,CompareStr,CompareText,需要 6 秒。所以我带来了我自己的比较功能,它(只是)快一点但不够快。任何想法如何改进它?

function SameFile(CONST Path1, Path2: string): Boolean;
VAR i: Integer;
begin
 Result:= Length(Path1)= Length(Path2);                                         { if they have different lenghts then obviously are not the same file }
 if Result then
  for i:= Length(Path1) downto 1 DO                                             { start from the end because it is more likely to find the difference there }
   if Path1[i]<> Path2[i] then
    begin
     Result:= FALSE;
     Break;
    end;
end;

我是这样使用的:

 for x:= JList.Count-1 downto 1 DO
  begin
   sMaster:= JList.Items[x];
   for y:= x-1 downto 0 DO
    if SameFile(sMaster, JList.Items[y]) then
     begin
      JList.Items.Delete (x); { REMOVE DUPLICATES }
      Break;
     end;
  end;

注意:重复的可能性很小,因此不经常调用 Delete。此外,列表无法排序,因为项目是由用户添加的,有时顺序可能很重要。

更新:
问题是我失去了代码的优势,因为它是 Pascal。 如果比较循环( Path1[i] Path2[i] )可以优化为使用 Borland 的 ASM 代码,那就太好了。


Delphi 7,Win XP 32 位,测试完成了列表中的 577 个项目。从列表中删除项目不是问题,因为这种情况很少发生。


结论

正如 Svein Bringsli 所指出的,我的代码很慢不是因为比较算法,而是因为 TListBox。最佳解决方案由 Marcelo Cantos 提供。非常感谢马塞洛。
我接受了 Svein 的回答,因为它直接回答了我的问题“如何让我的比较函数更快”和“没有必要让它更快”。
目前我实现了肮脏和 快速实施 解决方案:当我的文件少于 200 个时,我使用我的慢代码来检查重复项。如果有超过 200 个文件,我会使用 dwrbudr 的解决方案(这该死的快),考虑到如果用户有这么多文件,那么顺序无论如何都无关紧要(人脑无法跟踪这么多项目)。

我要感谢大家的想法,特别是 Svein 揭露真相:(Borland 的)视觉控制太慢了!

【问题讨论】:

  • 哈希表是最快的方法。然后,您有 O(n) 用于列表遍历和 O(1) 用于哈希搜索。所以你有 O(n) 复杂度。只需遍历列表并检查该项目是否已在哈希表中。

标签: delphi


【解决方案1】:

不要浪费时间优化汇编程序。您可以从 O(n2) 到 O(n log(n)) — 将时间缩短到毫秒 — 通过对列表进行排序然后对重复项进行线性扫描。

当您使用它时,忘记 SameFile 函数。算法的改进将使您在那里可以实现的任何目标都相形见绌。

编辑:根据 cmets 中的反馈...

您可以按如下方式执行保序 O(n log(n)) 重复数据删除:

  1. 对列表的副本进行排序。
  2. 识别重复条目并将其复制到第三个列表中,并将其重复计数减一。
  3. 按照您的原始版本向后遍历原始列表。
  4. 在内部 (for y := ...) 循环中,改为遍历重复列表。如果外部项匹配,则删除它,减少重复计数,如果计数达到零,则删除重复条目。

这显然更复杂,但它仍然会快几个数量级,即使你做了可怕的肮脏事情,比如将重复计数存储为字符串,C:\path1\file1=2,并使用如下代码:

y := dupes.IndexOfName(sMaster);
if y <> -1 then
begin
    JList.Items.Delete(x);
    c := StrToInt(dupes.ValueFromIndex(y));
    if c > 1 then
        dupes.Values[sMaster] = IntToStr(c - 1);
    else
        dupes.Delete(y);
end;

旁注:二分切比for y := ... 循环更有效,但鉴于重复很少,差异应该可以忽略不计。

【讨论】:

  • 对不起。这些技巧对我没有帮助。 1. 项目由用户添加(某种播放列表)。我可以对列表进行排序,但用户会生我的气。 2.由于列表是用户自己建的,所以很少使用Delete(在我的帖子里已经说明过了)。这就像一个安全网(很少使用:)
  • 为您的帖子+1。它可能对其他可以使用 Sort 的人有用。
  • @Altar,我想你误会了。您不必对 FileListBoxes 进行排序。正如 Marcelo 指出的那样,复制列表,对副本进行排序,...
  • @Altar:我已经修改了我的答案以适应订单保留。
  • @Altar:真的,你需要排序。如果您需要保留顺序,则使用某种结构,以便您可以恢复原始顺序。记录数组,其中每条记录保存路径和原始排序位置。然后你在路径上对整个事情进行排序。在将其放回 FileListBox 之前,请按原始排序键排序。 StringList 有一个自定义排序选项,在这里可能很有用。
【解决方案2】:

以您的代码为起点,我对其进行了修改,以便在搜索重复项之前获取列表的副本。时间从 5.5 秒缩短到 0.5 秒左右。

vSL := TStringList.Create;
try
  vSL.Assign(jList.Items);
  vSL.Sorted := true;
  for x:= vSL.Count-1 downto 1 DO
  begin
   sMaster:= vSL[x];
   for y:= x-1 downto 0 DO
    if SameFile(sMaster, vSL[y]) then
     begin
      vSL.Delete (x); { REMOVE DUPLICATES }
      jList.Items.Delete (x);
      Break;
     end;
  end;
finally
  vSL.Free;
end;

显然,这不是一个好方法,但它表明 TFileListBox 本身非常慢。我不相信你可以通过优化你的比较功能获得太多。

为了证明这一点,我用以下代码替换了您的 SameFile 函数,但保留了您的其余代码:

function SameFile(CONST Path1, Path2: string): Boolean;
VAR i: Integer;
begin
  Result := false; //Pretty darn fast code!!!
end;

时间从 5.6 秒变为 5.5 秒。我认为那里没有更多的收获:-)

【讨论】:

  • +1 表示不盲目回答问题而是解决问题。
【解决方案3】:

使用 sortedList.Duplicates := dupIgnore 创建另一个排序列表并将您的字符串添加到该列表,然后返回。


vSL := TStringList.Create;
try
  vSL.Sorted := true;
  vSL.Duplicates := dupIgnore;
  for x:= 0 to jList.Count - 1 do
    vSL.Add(jList[x]);

  jList.Clear;
  for x:= 0 to vSL.Count - 1 do
    jList.Add(vSL[x]);
finally
  vSL.Free;
end;

【讨论】:

    【解决方案4】:

    绝对最快的方法是使用一个例程,为字符串生成唯一的 64/128/256 位哈希码(我使用 C# 中的 SHA256Managed 类),没有任何方法(如前所述)。运行字符串列表,生成字符串的哈希码,在排序的哈希码列表中检查它,如果找到则该字符串是重复的。否则将哈希码添加到排序的哈希码列表中。

    这适用于字符串、文件名、图像(您可以获得图像的唯一哈希码)等,我保证这将与任何其他实现一样快或更快。

    PS 您可以通过将哈希码表示为字符串来为哈希码使用字符串列表。我过去使用过十六进制表示(256 位 -> 64 个字符),但理论上您可以按照自己喜欢的方式进行操作。

    【讨论】:

    • 其实我已经发布了一个更好的选项,不需要哈希码。
    【解决方案5】:

    4 秒通话多少次?如果您将其调用十亿次,则表现出色...

    不管怎样,Length(Path1) 每次循环都会被评估吗?如果是这样,请在循环之前将其存储在 Integer 变量中。

    指针可能会在字符串上产生一些速度。

    尝试内联函数: 函数 SameFile(blah blah): 布尔值;内联;

    如果每秒调用数千次,这将节省一些时间。我会从它开始,看看它是否能节省任何东西。

    编辑:我没有意识到您的列表没有排序。显然,你应该先这样做!这样您就不必与列表中的所有其他项目进行比较 - 只需与前一项或下一项进行比较。

    【讨论】:

    • 我有 Delphi 7。内联在 D7 中不可用。 “每次循环都会评估 Length(Path1) 吗?” - 我已经这样做了,但是在外部循环中并没有太大帮助。
    【解决方案6】:

    我使用修改后的三元搜索树 (TST) 对列表进行重复数据删除。您只需将项目加载到树中,使用整个字符串作为键,并且在每个项目上,如果键已经存在(并删除可见条目),您可以返回指示。然后你把树扔掉。我们的 TST 加载函数通常可以在一秒钟内加载 100000 个 80 字节的项目。并且只要正确使用开始和结束更新,就可以重新绘制您的列表。 TST 需要大量内存,但如果您只有大约 500 个项目,您根本不会注意到它。而且比排序、比较和汇编简单得多(当然,如果你有合适的 TST 实现)。

    【讨论】:

      【解决方案7】:

      不需要使用哈希表,单个排序列表给我 10 毫秒的结果,即 0.01 秒,大约快 500 倍!这是我使用 TListBox 的测试代码:

      procedure TForm1.Button1Click(Sender: TObject);
      var
        lIndex1: Integer;
        lString: string;
        lIndex2: Integer;
        lStrings: TStringList;
        lCount: Integer;
        lItems: TStrings;
      begin
        ListBox1.Clear;
        for lIndex1 := 1 to 577 do begin
          lString := '';
          for lIndex2 := 1 to 100 do
            if (lIndex2 mod 6) = 0 then
             lString := lString + Chr(Ord('a') + Random(2))
            else
              lString := lString + 'a';
          ListBox1.Items.Add(lString);
        end;
      
        CsiGlobals.AddLogMsg('Start', 'Test', llBrief);
      
        lStrings := TStringList.Create;
        try
          lStrings.Sorted := True;
          lCount := 0;
          lItems := ListBox1.Items;
          with lItems do begin
            BeginUpdate;
            try
              for lIndex1 := Count - 1 downto 0 do begin
                lStrings.Add(Strings[lIndex1]);
                if lStrings.Count = lCount then
                  Delete(lIndex1)
                else
                  Inc(lCount);
              end;
            finally
              EndUpdate;
            end;
          end;
        finally
          lStrings.Free;
        end;
      
        CsiGlobals.AddLogMsg('Stop', 'Test', llBrief);
      end;
      

      【讨论】:

      • 查看 Svein Bringsli 提供的答案。
      • 如果您使用 BeginUpdate 和 EndUpdate 并“批量”更新,TListBox 并不慢。如果您只更新一次列表,您可以在几毫秒内完成 500 次删除,如上面的代码所示。我会为你的问题添加评论,我被允许添加评论!!!
      • 你正在做两个循环,而只需要一个。慢得令人难以置信的是双循环,而不是 TListBox。
      • 它是一个 TListBox。如上所述,“删除项目”不是问题。当我删除项目时,它可能很慢。但我不在乎。我很少删除项目,可能永远不会。
      【解决方案8】:

      我还想指出,如果将您的解决方案应用于庞大的列表(例如包含 100,000,000 个或更多项目),您的解决方案将花费大量时间。即使是构建哈希表或排序列表也会花费太多时间。

      在这种情况下,您可以尝试另一种方法:散列每个成员,但不是填充完整的散列表,而是创建一个位集(大到足以包含与输入项一样多的插槽的接近因子)并且只需将每个位设置为哈希函数指示的偏移量。如果该位为 0,则将其更改为 1。如果它已经为 1,请在单独的列表中记下有问题的字符串索引并继续。这会产生一个在哈希中发生冲突的字符串索引列表,因此您必须再次运行它才能找到这些冲突的第一个原因。之后,您应该对该列表中的字符串索引进行排序和重复数据删除(因为除第一个索引之外的所有索引都将出现两次)。完成后,您应该再次对列表进行排序,但这次根据字符串内容对其进行排序,以便在随后的单次扫描中轻松发现重复项。

      当然,这么长的篇幅可能有点极端,但至少对于非常大的卷来说,这是一个可行的解决方案! (哦,如果重复的数量非常多,当散列函数的分布很差或“散列表”位集中的槽数选择得太小时,这仍然不起作用 - 这会产生很多冲突这并不是真正的重复。)

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-11-09
        • 1970-01-01
        • 2011-04-13
        • 1970-01-01
        • 2019-08-08
        • 1970-01-01
        相关资源
        最近更新 更多