如何在Delphi中合并两个字符串数组

9

我有两个或更多动态字符串数组,其中填充了一些庞大的数据。我想将这两个数组合并成一个数组,我知道可以用for循环来实现,代码如下:

var
  Arr1, Arr2, MergedArr: Array of string;
  I: Integer;
begin
  // Arr1:= 5000000 records
  // Arr2:= 5000000 records

  // Fill MergedArr by Arr1
  MergedArr:= Arr1;

  // Set length of MergedArr to length of ( Arra1 + Arr2 )+ 2
  SetLength(MergedArr, High(Arr1)+ High(Arr2)+2);

  // Add Arr2 to MergedArr
  for I := Low(Arr2)+1 to High(Arr2)+1 do
    MergedArr[High(Arr1)+ i]:= Arr2[i-1];
end;

但是在处理大量数据时速度很慢,是否有更快的方法,比如复制数组内存数据?


也许你不知道:SetLength(MergedArr, High(Arr1) + High(Arr2) + 2); 等同于 SetLength(MergedArr, Length(Arr1) + Length(Arr2));High(Arr) 返回 Arr 的最后一个索引,而 Length(Arr) 返回其元素数量。 - Andriy M
你应该使用本地变量来存储长度(例如 L1 := length(Arr1)):这样代码会更易读且更快。你使用的是哪个 Delphi 版本?旧版本使用较慢的内存管理器。如果你使用的是 Delphi 7 等版本,请尝试将 FastMM4 添加到你的 use 子句中。 - Arnaud Bouchez
对于更通用的处理动态数组的方法,请参见https://dev59.com/C2w05IYBdhLWcg3wqzms。 - Arnaud Bouchez
3个回答

8
首先,string 是特殊的,因此应该特别对待:不要试图愚弄编译器,请保持代码不变。 String 是特殊的,因为它是引用计数的。每次从一个地方复制字符串到另一个地方时,它的引用计数都会增加。当引用计数达到 0 时,字符串被销毁。您的代码表现良好,因为它让编译器知道您在做什么,反过来编译器有机会正确地增加所有引用计数。
当然,你可以像 gabr 的回答中建议的那样玩各种技巧,比如用零填充旧数组,这样新数组中的引用计数仍然有效,但如果您实际上也需要旧数组,则无法这样做。而这是一个小技巧(尽管这可能在可预见的未来是有效的)。 (值得注意的是,我实际上喜欢这个技巧)。
无论如何,这是我的答案中最重要的部分,您的代码很可能在将字符串从一个数组复制到另一个数组时并不慢,它很可能在其他地方缓慢。这里是一个短控制台应用程序,它创建两个数组,每个数组都有 5M 个随机字符串,然后将两个数组合并到第三个数组中,并显示创建合并所需的时间。 在我的机器上,合并只需要大约 300 毫秒。填充数组需要更长时间,但我没有计时:
程序 Project26;
{$APPTYPE CONSOLE}

uses SysUtils, Windows;

var a, b, c: array of string;
    i: Integer;

    Freq: Int64;
    Start, Stop: Int64;
    Ticks: Cardinal;

const count = 5000000;

begin
  SetLength(a,count);
  SetLength(b,count);
  for i:=0 to count-1 do
  begin
    a[i] := IntToStr(Random(1));
    b[i] := IntToStr(Random(1));
  end;

  WriteLn('Moving');

  QueryPerformanceFrequency(Freq);
  QueryPerformanceCounter(Start);

  SetLength(c, Length(a) + Length(b));
  for i:=0 to High(a) do
    c[i] := a[i];
  for i:=0 to High(b) do
    c[i+Length(a)] := b[i];

  QueryPerformanceCounter(Stop);
  WriteLn((Stop - Start) div (Freq div 1000), ' milliseconds');
  ReadLn;

end.

7
您可以使用内置的Move函数来将一块内存移动到另一个位置。参数是源内存块、目标内存块和要移动的数据大小。
由于您正在复制字符串,因此在合并后,必须通过用零填充它们来销毁源数组。否则,字符串的引用计数将全部错误,从而在程序后期造成混乱和破坏。
var
  Arr1, Arr2, MergedArr: Array of string;
  I: Integer;
begin
  SetLength(Arr1, 5000000);
  for I := Low(Arr1) to High(Arr1) do
    Arr1[I] := IntToStr(I);

  SetLength(Arr2, 5000000);
  for I := Low(Arr2) to High(Arr2) do
    Arr2[I] := IntToStr(I);

  // Set length of MergedArr to length of ( Arra1 + Arr2 )+ 2
  SetLength(MergedArr, High(Arr1)+ High(Arr2)+2);

  // Add Arr1 to MergedArr
  Move(Arr1[Low(Arr1)], MergedArr[Low(MergedArr)], Length(Arr1)*SizeOf(Arr1[0]));

  // Add Arr2 to MergedArr
  Move(Arr2[Low(Arr2)], MergedArr[High(Arr1)+1], Length(Arr2)*SizeOf(Arr2[0]));

  // Cleanup Arr1 and Arr2 without touching string refcount.
  FillChar(Arr1[Low(Arr1)], Length(Arr1)*SizeOf(Arr1[0]), 0);
  FillChar(Arr2[Low(Arr2)], Length(Arr2)*SizeOf(Arr2[0]), 0);

  // Test
  for I := Low(Arr1) to High(Arr1) do begin
    Assert(MergedArr[I] = IntToStr(I));
    Assert(MergedArr[I] = MergedArr[Length(Arr1) + I]);
  end;

  // Clear the array to see if something is wrong with refcounts
  for I := Low(MergedArr) to High(MergedArr) do
    MergedArr[I] := '';
end;

1
Andriy: 如果你使用 fillchar 函数将原始数组填充为零,它会起作用。这意味着引用计数没有实际变化。但是,这个概念的线程安全性会暂时受到损害(在移动和填充字符之间,存在多个引用而没有更新的引用计数)。因此,我不建议这种方法作为一般策略。 - Marco van de Voort
@大家:哎呀,我完全忘记了“字符串”这一部分。现在我的答案已经改进和修复了。谢谢! - gabr
@Cosmin,你说得对。SetLength 隐式地将 Arr1 复制到 MergedArr 中,这又会导致速度变慢。我已经按照你的建议更新了代码。谢谢! - gabr
@gabr,+1。我也会将代码打包到一个procedure MergeStringArrays(var Arr1, Arr2, MergedArray: array of string);中,但这主要是为了美观而已。 - Cosmin Prund
在我看来,这个基于移动的解决方案不是线程安全的。如果另一个线程访问动态数组内容,你会得到引用计数不匹配的错误。一个更通用的解决方案可以在https://dev59.com/C2w05IYBdhLWcg3wqzms找到。 - Arnaud Bouchez
显示剩余3条评论

3
优秀的格言是:最快的代码是永远不运行的代码。由于复制很昂贵,因此应该尽量避免复制的成本。
您可以使用虚拟数组来实现。创建一个类,其中包含字符串数组的数组。在您的示例中,外部数组将包含两个字符串数组。
  • 添加一个Count属性,返回所有数组中字符串的总数。
  • 添加一个默认索引属性,通过计算索引所指向的外部数组,然后从内部数组返回相应的值来操作。
  • 为额外的加分,实现一个枚举器,使得for循环起作用。

这里的问题是 - 与使用普通数组相比,使用这个类会有多快?如果存在大量的数组索引和很少的数组合并,那么代码可能会运行得更慢。 - gabr
@gabr 这是一个很好的观点,显然只有 OP 才知道权衡如何工作。随机访问会更慢,但使用虚拟数组遍历整个集合的 for 循环会很快。 - David Heffernan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接