【问题标题】:How to make array distinct如何使数组不同
【发布时间】:2015-11-09 19:34:18
【问题描述】:

我有一个整数和字符串字段数组。为了使其与众不同,我目前逐行复制到新数组中,并且对于每条记录,我检查该记录是否已经存在于新的记录中,如果不存在,则复制它。最后,我将新数组复制回原始数组。

它可以工作,但速度很慢。有没有更快、更简单的方法来做到这一点?

TArrayMixed = record
    Field1: integer;
    Field2: integer;
    Field3: integer;
    Field4: string;
    Field5: string;
    Field6: string;
 end;

procedure TForm1.Button10Click(Sender: TObject);
var
  ArrayMixed, ArrayMixed_tmp: array of TArrayMixed;
  i, j, vIdx: integer;
  vExists: boolean;
begin
  SetLength(ArrayMixed, 100000);
  for i := 0 to 99999 do
  begin
    ArrayMixed[i].Field1 := 1 + Random(5);
    ArrayMixed[i].Field2 := 1 + Random(5);
    ArrayMixed[i].Field3 := 1 + Random(5);
    ArrayMixed[i].Field4 := 'String';
    ArrayMixed[i].Field5 := 'Another string';
    ArrayMixed[i].Field6 := 'New string';
  end;

  // Sort
  TArray.Sort<TArrayMixed > (ArrayMixed, TComparer<TArrayMixed > .Construct(function(const Left, Right: TArrayMixed): Integer
    begin
      Result := MyCompareAMixed(Left, Right);
    end
    ));

  // Distinct
  SetLength(ArrayMixed_tmp, Length(ArrayMixed));
  vIdx := 0;
  for i := Low(ArrayMixed) to High(ArrayMixed) do
  begin
    vExists := False;
    for j := Low(ArrayMixed_tmp) to vIdx - 1 do
      if (ArrayMixed_tmp[j].Field1 = ArrayMixed[i].Field1) and
        (ArrayMixed_tmp[j].Field2 = ArrayMixed[i].Field2) and
        (ArrayMixed_tmp[j].Field3 = ArrayMixed[i].Field3) and
        (ArrayMixed_tmp[j].Field4 = ArrayMixed[i].Field4) and
        (ArrayMixed_tmp[j].Field5 = ArrayMixed[i].Field5) and
        (ArrayMixed_tmp[j].Field6 = ArrayMixed[i].Field6) then
      begin
        vExists := True;
        Break;
      end;

    if not vExists then
    begin
      ArrayMixed_tmp[vIdx] := ArrayMixed[i];
      Inc(vIdx);
    end;
  end;
  SetLength(ArrayMixed_tmp, vIdx);

  // now copy back to original array
  SetLength(ArrayMixed, 0);
  SetLength(ArrayMixed, Length(ArrayMixed_tmp));
  for i := Low(ArrayMixed_tmp) to High(ArrayMixed_tmp) do
    ArrayMixed[i] := ArrayMixed_tmp[i];

  sleep(0);

end;

编辑:

由于在真实数据中字符串并不完全相同,当原始数组被这样填充时,它使不同数组的部分变慢:

编辑#2:(在编辑#1中复制了错误的代码)

for i := 0 to 999999 do
  begin
    ArrayMixed[i].Field1 := 1 + Random(5);
    ArrayMixed[i].Field2 := 1 + Random(5);
    ArrayMixed[i].Field3 := 1 + Random(5);
    ArrayMixed[i].Field4 := 'String'+IntToStr(i mod 5);
    ArrayMixed[i].Field5 := 'Another string'+IntToStr(i mod 5);
    ArrayMixed[i].Field6 := 'New string'+IntToStr( i mod 5);
  end;

编辑 #3:发布用于排序的代码 - 仅对前 3 个字段进行排序!

TMyArray3 = array[1..3] of Integer;

    function CompareIntegerArray3(const lhs, rhs: TMyArray3): Integer;
    var
      i: Integer;
    begin
      Assert(Length(lhs) = Length(rhs));
      for i := low(lhs) to high(lhs) do
        if lhs[i] < rhs[i] then
          exit(-1)
        else if lhs[i] > rhs[i] then
          exit(1);

      exit(0);
    end;

    function GetMyArrayAMixed(const Value: TArrayMixed): TMyArray3;
    begin
      Result[1] := Value.Field1;
      Result[2] := Value.Field2;
      Result[3] := Value.Field3;
    end;

    function MyCompareAMixed(const lhs, rhs: TArrayMixed): Integer;
    begin
      Result := CompareIntegerArray3(GetMyArrayAMixed(lhs), GetMyArrayAMixed(rhs));
    end;

【问题讨论】:

  • 加快重复搜索的一种方法是为您的记录类型编写自定义哈希并存储在 TDictionary 中(其中 key=value)。这增加了添加条目的开销(每次添加都需要计算一个昂贵的哈希),但它加快了搜索重复项(O(1) vs O(n))的速度。这也可以让您以相同的成本,仅在记录不存在时有条件地插入(也是 O(1))。这是否适合您的用例,只有您自己知道。
  • 或者,数据库可能更适合存储这些信息。
  • 谢谢,但数据库不是我的应用程序中的选项。从来没有使用过散列,所以我什至不知道从哪里开始。

标签: arrays delphi distinct delphi-xe7


【解决方案1】:
  • 为记录实现一些方法(平等检查、哈希码)以摆脱大量样板代码

type
  TArrayMixed = record
    Field1: integer;
    Field2: integer;
    Field3: integer;
    Field4: string;
    Field5: string;
    Field6: string;
    class operator Equal( const a, b: TArrayMixed ): Boolean;
    class function Compare( const L, R: TArrayMixed ): integer; overload; static;
    function Compare( const Other: TArrayMixed ): integer; overload;
    function GetHashCode( ): integer;
  end;

  { TArrayMixed }

class function TArrayMixed.Compare( const L, R: TArrayMixed ): integer;
begin
  Result := L.Compare( R );
end;

function TArrayMixed.Compare( const Other: TArrayMixed ): integer;
begin
  Result := Field1 - Other.Field1;
  if Result = 0
  then
    begin
      Result := Field2 - Other.Field2;
      if Result = 0
      then
        begin
          Result := Field3 - Other.Field3;
          if Result = 0
          then
            begin
              Result := CompareStr( Field4, Other.Field4 );
              if Result = 0
              then
                begin
                  Result := CompareStr( Field5, Other.Field5 );
                  if Result = 0
                  then
                    begin
                      Result := CompareStr( Field6, Other.Field6 );
                    end;
                end;
            end;
        end;
    end;
end;

class operator TArrayMixed.Equal( const a, b: TArrayMixed ): Boolean;
begin
  Result := true
  {} and ( a.Field1 = b.Field1 )
  {} and ( a.Field2 = b.Field2 )
  {} and ( a.Field3 = b.Field3 )
  {} and ( a.Field4 = b.Field4 )
  {} and ( a.Field5 = b.Field5 )
  {} and ( a.Field6 = b.Field6 );
end;

function TArrayMixed.GetHashCode: integer;
begin
{$IFOPT Q+}
{$Q-}
{$DEFINE SET_Q_ON}
{$ENDIF}
  Result := 17;
  Result := Result * 31 + Field1;
  Result := Result * 31 + Field2;
  Result := Result * 31 + Field3;
  Result := Result * 31 + Field4.GetHashCode;
  Result := Result * 31 + Field5.GetHashCode;
  Result := Result * 31 + Field6.GetHashCode;
{$IFDEF SET_Q_ON}
{$Q+}
{$UNDEF SET_Q_ON}
{$ENDIF}
end;
  • 使用 TDictionary 作为 HashSet 来检查重复项

procedure Test;
var
  arr1, arr2: TArray<TArrayMixed>;
  idx       : integer;
  lst       : TDictionary<TArrayMixed, integer>;
begin
  // fill the array
  SetLength( arr1, 100000 );
  for idx := low( arr1 ) to high( arr1 ) do
    begin
      arr1[ idx ].Field1 := 1 + Random( 5 );
      arr1[ idx ].Field2 := 1 + Random( 5 );
      arr1[ idx ].Field3 := 1 + Random( 5 );
      arr1[ idx ].Field4 := 'String' + IntToStr( idx mod 5 );
      arr1[ idx ].Field5 := 'Another string' + IntToStr( idx mod 5 );
      arr1[ idx ].Field6 := 'New string' + IntToStr( idx mod 5 );
    end;

  // distinct
  lst := TDictionary<TArrayMixed, integer>.Create( TEqualityComparer<TArrayMixed>.Construct(
    function( const L, R: TArrayMixed ): Boolean
    begin
      Result := ( L = R );
    end,
    function( const i: TArrayMixed ): integer
    begin
      Result := i.GetHashCode( );
    end ) );
  try
    for idx := low( arr1 ) to high( arr1 ) do
      begin
        lst.AddOrSetValue( arr1[ idx ], 0 );
      end;

    arr2 := lst.Keys.ToArray;

  finally
    lst.Free;
  end;
end;

【讨论】:

  • 这通常是我所暗示的那种想法 - 感谢您抽出时间来输入所有内容!这主要是出于我自己的好奇心,但在这里看到一些性能指标会很有趣。
  • 在比较器中提前退出可以避免嵌套缩进
  • 另外,减法的使用容易导致算术溢出。
【解决方案2】:

我会做这样的事情。您基本上可以即时创建结果并同时使用二进制搜索进行排序以删除重复项。

function RemoveDuplicates(aSourceArray: TArray<TArrayMixed>): TArray<TArrayMixed>;
var
  i: Integer;
  index: Integer;
  sortList: TList<TArrayMixed>;
begin
  sortList := TList<TArrayMixed>.Create;
  try
    for i := Low(aSourceArray) to High(aSourceArray) do
    begin
      if not sortList.BinarySearch(aSourceArray[i], index,
        TDelegatedComparer<TArrayMixed>.Construct(
        function(const L, R: TArrayMixed): integer
        begin
          Result := L.Field1 - R.Field1;
          if Result <> 0 then Exit;
          Result := L.Field2 - R.Field2;
          if Result <> 0 then Exit;
          Result := L.Field3 - R.Field3;
          if Result <> 0 then Exit;
          Result := CompareStr(L.Field4, R.Field4);
          if Result <> 0 then Exit;
          Result := CompareStr(L.Field5, R.Field5);
          if Result <> 0 then Exit;
          Result := CompareStr(L.Field6, R.Field6);
        end)) then
      begin
        sortList.Insert(index, aSourceArray[i]);
      end;
    end;
    Result := sortList.ToArray;
  finally
    sortList.Free;
  end;
end;

要使用此代码,您可以执行以下操作:

procedure TForm1.Button10Click(Sender: TObject);
var
  ArrayMixed, ArrayMixed_tmp: TArray<TArrayMixed>;
  i: Integer;
begin
  SetLength(ArrayMixed, 100000);
  for i := 0 to 999999 do
  begin
    ArrayMixed[i].Field1 := 1 + Random(5);
    ArrayMixed[i].Field2 := 1 + Random(5);
    ArrayMixed[i].Field3 := 1 + Random(5);
    ArrayMixed[i].Field4 := 'String'+IntToStr(i mod 5);
    ArrayMixed[i].Field5 := 'Another string'+IntToStr(i mod 5);
    ArrayMixed[i].Field6 := 'New string'+IntToStr( i mod 5);
  end;
  ArrayMixed_tmp := RemoveDuplicates(ArrayMixed);
end;

【讨论】:

    【解决方案3】:

    只需检查前一个索引旁边的重复项,因为数组已排序。这里也是重用的排序比较器。

    function RemoveDuplicates(const anArray: array of TArrayMixed): TArray<TArrayMixed>;
    var
      j, vIdx: integer;
    begin
      // Sort
      TArray.Sort<TArrayMixed > (anArray, TComparer<TArrayMixed >.Construct(function(const Left, Right: TArrayMixed): Integer
        begin
          Result := MyCompareAMixed(Left, Right);
        end
        ));
    
      // Distinct
      SetLength(Result, Length(anArray));
      vIdx := 0;
      j := 0;
      while (j <= High(anArray) do
      begin
        Result[vIdx] := anArray[j];
        Inc(j);
        While (j <= High(anArray)) and (MyCompareAMixed(Result[vIdx],anArray[j]) = 0) do
          Inc(j);
       Inc(vIdx);
      end;
      SetLength(Result, vIdx);
    end;
    

    更新

    在对问题的更新中指出该数组仅部分排序。减少重复删除重复次数的一种方法是:

    • 查找共享第一个排序标准的项目的开始和停止索引。
    • 在它们之间进行迭代以排除重复项。

    【讨论】:

    • 谢谢,我想我应该发布完整的代码,因为只有前 3 个字段被排序,而不是字符串。刚刚在我的问题中发布了排序代码。
    • 好的,那么您可以在共享前 3 个字段的每个项目中进行包含字符串的子排序。
    【解决方案4】:

    由于您已经对ArrayMixed 进行了排序,因此您无需将每个项目相互比较即可找到重复项。副本已经彼此相邻放置。因此,您只需遍历ArrayMixed 并将当前项与ArrayMixed_tmp 中的最后一项进行比较。 因此,复制不同的项目可以进行得更快,看起来像这样:

    vIdx := 0;
    for i := Low(ArrayMixed) to High(ArrayMixed) do
    begin
      if (vIdx = 0) or // the first item can never be a duplicate
         (ArrayMixed_tmp[vIdx].Field1 <> ArrayMixed[i].Field1) or
         (ArrayMixed_tmp[vIdx].Field2 <> ArrayMixed[i].Field2) or
         (ArrayMixed_tmp[vIdx].Field3 <> ArrayMixed[i].Field3) or
         (ArrayMixed_tmp[vIdx].Field4 <> ArrayMixed[i].Field4) or
         (ArrayMixed_tmp[vIdx].Field5 <> ArrayMixed[i].Field5) or
         (ArrayMixed_tmp[vIdx].Field6 <> ArrayMixed[i].Field6) then
      begin
        ArrayMixed_tmp[vIdx] := ArrayMixed[i];
        Inc(vIdx);
      end;
    end;
    

    【讨论】:

    • 谢谢。我刚刚发布了排序代码,只有前 3 个字段(整数)被排序。所以数组没有完全排序。
    【解决方案5】:

    您尚未发布 MyCompareAMixed() 函数的代码,因此无法测试包括此未定义函数在内的实际代码的性能,包括当前的排序性能。

    但是,由于您发布的重复检测方法不依赖于被排序的数组,我只是从代码中删除了排序。

    在没有任何进一步优化的情况下,生成的重复数据删除过程在 50 毫秒内完成,这在我的书中对 100,000 个复杂项目的重复数据删除并不“慢”。即不是单个值,而是多个值记录的项目。

    如果由于其他原因需要进行排序,那么您可以保留排序并根据其他人给出的答案优化重复数据删除过程,但我首先要质疑为什么您认为该过程很慢以及是否真的低于 50 毫秒很慢,你的目标是什么?

    有可能是排序增加了开销(正如我所说,没有你的比较功能,我们无法量化增加的开销)所以如果由于其他原因不需要这种排序并且如果低于 50 毫秒对这个数组进行重复数据删除是可以接受的,然后我将继续执行其他任务。

    【讨论】:

    • 这是一个简单的例子,方便测试。在我的应用程序中,平均每个数组大约有 100 到 10.000 条记录,大约 50 个字段(混合)并且每次运行调用几千次 - 在大约 50 种不同的数组结构中,因此有 50 种不同的程序可以使数组不同。排序是可选的,因为在某些情况下,使数组不同然后排序会更快。我猜是逐案的。
    • 刚刚编辑了问题,数组不仅填充了相同的字符串。制作不同的新数组要慢得多。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-06-11
    • 2011-05-08
    • 1970-01-01
    相关资源
    最近更新 更多