【问题标题】:How can I search a generic TList for a record with a certain field value?如何在通用 TList 中搜索具有特定字段值的记录?
【发布时间】:2011-11-08 13:43:46
【问题描述】:

关于通用TList 的一切。我有这个结构:

Type
  TExtract = record
    Wheel: string;
    Extract: array [1..5] of Byte;
  end;

  TExtractList = TList<TExtract>

  TEstr = record
    Date: TDate;
    Extract: TExtractList;
  end;

  TEstrList = TList<TEstr>;

主列表是TExtrList,在这个列表中我有所有日期,并且所有日期都与该日期一致。我想搜索日期是否存在。如果不存在,我添加子列表TExtractListTEstr 中提取信息。当我从TExtrList 搜索时,Delphi 询问我有关TEstr 的类型。我只需要搜索Date。那么如何在通用TList 中搜索单个字段?

PS:我已经删除了上一篇文章,因为在这里我试图更好地解释。

【问题讨论】:

  • 是的,在我编辑我的答案以向您展示如何使用BinarySearch 搜索Date 之后,您删除了您的上一篇文章。不酷。
  • 你的名单有多大?搜索的方法将取决于您谈论的是一个小列表(几十个项目)还是一个大列表。如果您的列表很小,线性搜索将是最易读且可能是最好的解决方案。
  • 补充 Smasher 的评论:如果您只搜索列表几次,线性搜索方法是首选选项。使用智能搜索(二分搜索)需要相当多的工作才能对列表进行排序。另一方面,如果您将进行数千次列表查找,BinarySearch 将具有优势,并且排序将很有用。如果您进行更多搜索,您可能希望使用TDictionary&lt;Date, Integer&gt; 来加快搜索速度。
  • @Marcello 我试图稍微改进一下这个问题,但没有改变类型名称。我认为您对 TEstr TExtr TExtrListTEstrList 有一些混淆(它们的代码和解释不同)。如果你能编辑这些就好了。

标签: delphi generics search


【解决方案1】:

我们又来了。

您应该使用内置的TList&lt;T&gt;.BinarySearch() 函数,即使它正确地要求TEstr 记录作为参数。您首先需要使用TList&lt;T&gt;.Sort() 使用与搜索相同的条件对列表进行排序,然后调用BinarySearch() 来查找您的记录。

这是一个同时进行(排序和搜索)的函数:

uses Generics.Defaults; // this provides TDelegatedComparer
uses Math; // this provides Sign()

function SearchList(Date:TDate; Sort:Boolean; List:TList<TEstr>): Integer;
var Comparer: IComparer<TEstr>;
    Dummy: TEstr;
begin
  // Prepare a custom comparer that'll be used to sort the list
  // based on Date alone, and later to BinarySearch the list using
  // date alone.
  Comparer := TDelegatedComparer<TEstr>.Construct(
    function (const L, R: TEstr): Integer
    begin
      Result := Sign(L.Date - R.Date);
    end
  );

  // If the list is not sorted, sort it. We don't know if it's sorted or not,
  // so we rely on the "Sort" parameter
  if Sort then List.Sort(Comparer);

  // Prepare a Dummy TEstr record we'll use for searching
  Dummy.Date := Date;

  // Call BinarySearch() to look up the record based on Date alone
  if not List.BinarySearch(Dummy, Result, Comparer) then
    Result := -1;
end;

BinarySearch 假定列表已排序(这是二进制搜索的本质!)。在您第一次通话时,您需要设置Sort=True,以便正确排序列表。在随后的调用中,Sort 应该是 False。当然,在实际使用中,您可能有单独的搜索和排序例程,并且您可能会将它们作为从TList&lt;TEstr&gt; 下降的类的方法(以使事情更容易)。出于演示目的,我将两者放在同一个例程中。

【讨论】:

    【解决方案2】:

    您也可以像这样声明一个辅助类,以避免IComparer 要求比较的左侧和右侧都必须是专用类型:

    type
      TLeftComparison<T> = reference to function(const Left: T; var Value): Integer;
    
      TListHelper<T> = class
      public
        class function BinarySearch(Instance: TList<T>; var Value; out FoundIndex: Integer;
          Comparison: TLeftComparison<T>; Index, Count: Integer): Boolean; overload;
        class function BinarySearch(Instance: TList<T>; var Value; out FoundIndex: Integer;
          Comparison: TLeftComparison<T>): Boolean; overload;
        class function Contains(Instance: TList<T>; var Value; Comparison: TLeftComparison<T>): Boolean;
        class function IndexOf(Instance: TList<T>; var Value; Comparison: TLeftComparison<T>): Integer;
        class function LastIndexOf(Instance: TList<T>; var Value; Comparison: TLeftComparison<T>): Integer;
      end;
    
    class function TListHelper<T>.BinarySearch(Instance: TList<T>; var Value; out FoundIndex: Integer;
      Comparison: TLeftComparison<T>; Index, Count: Integer): Boolean;
    var
      L, H: Integer;
      mid, cmp: Integer;
    begin
      Result := False;
      L := Index;
      H := Index + Count - 1;
      while L <= H do
      begin
        mid := L + (H - L) shr 1;
        cmp := Comparison(Instance[mid], Value);
        if cmp < 0 then
          L := mid + 1
        else
        begin
          H := mid - 1;
          if cmp = 0 then
            Result := True;
        end;
      end;
      FoundIndex := L;
    end;
    
    class function TListHelper<T>.BinarySearch(Instance: TList<T>; var Value; out FoundIndex: Integer;
      Comparison: TLeftComparison<T>): Boolean;
    begin
      Result := BinarySearch(Instance, Value, FoundIndex, Comparison, 0, Instance.Count);
    end;
    
    class function TListHelper<T>.Contains(Instance: TList<T>; var Value; Comparison: TLeftComparison<T>): Boolean;
    begin
      Result := IndexOf(Instance, Value, Comparison) >= 0;
    end;
    
    class function TListHelper<T>.IndexOf(Instance: TList<T>; var Value; Comparison: TLeftComparison<T>): Integer;
    var
      I: Integer;
    begin
      for I := 0 to Instance.Count - 1 do
        if Comparison(Instance[I], Value) = 0 then
          Exit(I);
      Result := -1;
    end;
    
    class function TListHelper<T>.LastIndexOf(Instance: TList<T>; var Value; Comparison: TLeftComparison<T>): Integer;
    var
      I: Integer;
    begin
      for I := Instance.Count - 1 downto 0 do
        if Comparison(Instance[I], Value) = 0 then
          Exit(I);
      Result := -1;
    end;
    

    那么你可以这样使用它:

    // TComparison (requires instances on both sides)
    function CompareEstr(const Left, Right: TEstr): Integer;
    begin
      if Left.Date < Right.Date then
        Exit(-1);
      if Left.Date > Right.Date then
        Exit(1);
      Result := 0;
    end;
    
    // TLeftComparison: requires instance only on the left side    
    function CompareEstr2(const Left: TEstr; var Value): Integer;
    begin
      if Left.Date < TDateTime(Value) then
        Exit(-1);
      if Left.Date > TDateTime(Value) then
        Exit(1);
      Result := 0;
    end;
    
    procedure Main;
    var
      Date: TDate;
      Comparer: IComparer<TEstr>;
      List: TEstrList;
      Item: TEstr;
      Index: Integer;
      I: Integer;
    begin
      Comparer := nil;
      List := nil;
      try
        // create a list with a comparer
        Comparer := TComparer<TEstr>.Construct(CompareEstr);
        List := TEstrList.Create(Comparer);
        // fill with some data
        Date := EncodeDate(2011, 1, 1);
        for I := 0 to 35 do
        begin
          Item.Date := IncMonth(Date, I);
          List.Add(Item);
        end;
        // sort (using our comparer)
        List.Sort;
    
        Date := EncodeDate(2011, 11, 1);
        Item.Date := Date;
    
        // classic approach, needs Item on both sides   
        Index := List.IndexOf(Item);
        Writeln(Format('TList.IndexOf(%s): %d', [DateToStr(Date), Index]));
        List.BinarySearch(Item, Index);
        Writeln(Format('TList.BinarySearch(%s): %d', [DateToStr(Date), Index]));
        Writeln;
    
        // here we can pass Date directly
        Index := TListHelper<TEstr>.IndexOf(List, Date, CompareEstr2);
        Writeln(Format('TListHelper.IndexOf(%s): %d', [DateToStr(Date), Index]));
        TListHelper<TEstr>.BinarySearch(List, Date, Index, CompareEstr2);
        Writeln(Format('TListHelper.BinarySearch(%s): %d', [DateToStr(Date), Index]));
        Readln;
      finally
        List.Free;
      end;
    end;
    

    这当然不是类型安全的(由于无类型的右侧比较参数),但需要允许一般比较不同类型的值。稍加小心,这应该不是问题。否则,您还可以为需要比较的大多数使用的类型编写重载版本。

    【讨论】:

    • 我不喜欢这种方法。 TCompare&lt;T&gt; = function(const L, R: T): Integer; 更有效。没有任何有趣的界面。
    • 在 XE6 中至少使用 reference to 对性能不利。原始回调是最快的。
    【解决方案3】:

    我发现只有一种方法可以搜索具有特定值的列表。

    我将重用 Cosmin Prund 示例:

    uses Generics.Defaults; // this provides TDelegatedComparer
    uses Math; // this provides Sign()
    
    function SearchList(Date:TDate; Sort:Boolean; List:TList<TEstr>): Integer;
    var Dummy : TEstr;
    begin
      // If the list is not sorted, sort it. We don't know if it's sorted or not,
      // so we rely on the "Sort" parameter
      if Sort then List.Sort(TDelegatedComparer<TEstr>.Construct(
        function (const L, R: TEstr): Integer
        begin
          Result := Sign(L.Date - R.Date);
        end
      );
    
      // Call BinarySearch() to look up the record based on Date alone
      if not List.BinarySearch(Dummy, Result, TDelegatedComparer<TEstr>.Construct(
          function (const L, R: TEstr): Integer
          begin
             //By implementation, the binarySearch Dummy parameter is passed in the "R" parameter of the Comparer function. (In delphi 2010 at least)
            Result := Sign(L.Date - Date); //Use the Date parameter instead of R.Date
          end) then
        Result := -1;
    end;
    

    但是,这种方法仅在“通过实现”而不是“通过设计”有效(据我所知)。换句话说,它很容易在 Delphi 的版本之间中断。因此,仅建议将这种方法用于创建可能“性能昂贵”的项目。如果你这样做,我强烈建议在你的代码中添加这样的内容。

    {$IF RTLVersion > *YourCurrentVersion*}
       {$MESSAGE WARNING 'Verify if BinarySearch implementation changed'}    
    {$IFEND}
    

    p 这样,下次您在较新版本的 Delphi 中构建此代码时,您将自动收到警告,告诉您确保您的代码仍能按预期工作。但是,如果您的代码需要同时支持多个版本的 Delphi,这仍然会导致问题。

    【讨论】:

      【解决方案4】:

      我认为你在TDynArrayHashed 中有这个功能,这个Post中有一个例子

      【讨论】:

        【解决方案5】:

        真的需要是一个 TList 吗? Imo,二进制搜索对此太复杂了。也许您可以简单地使用TDictionary

        type
          TEstrCollection = TDictionary<TDate, TEstr>;
        
        var
          EstrCollection: TEstrCollection;
        begin
          EstrCollection := TEstrCollection.Create;
        
          // Add an item
          EstrCollection.Add(Date, TExtractList.Create)
        
          // Search
          ExtractList := EstrCollection[Date];
        end;
        

        现在,这要求日期字段是唯一的,因为它是字典的键。此外,这些项目没有特定的顺序。

        如果顺序很重要,我会结合数据结构。例如,您可以有一个 TList 来按顺序保存项目,加上一个 TDictionary 来执行搜索,诸如此类。

        唯一的问题是records 不是指针。要在两个不同的数据结构中添加相同的record,您需要为它们创建指针

        PEstr = ^TEstr
        

        或者只使用对象,因为它们已经是指针。您可以使用TObjectListTObjectDictionary 让项目的生命周期由集合自动管理(只要记住只有一个集合管理对象的生命周期,如果它在多个集合中)。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2014-01-25
          • 1970-01-01
          • 1970-01-01
          • 2021-07-25
          • 2013-06-26
          • 1970-01-01
          • 2022-07-12
          • 1970-01-01
          相关资源
          最近更新 更多