【问题标题】:Sort arrays by multiple fields按多个字段对数组进行排序
【发布时间】:2015-11-08 13:22:25
【问题描述】:

我有多个数组,它们都以整数字段开头,从 1 到 5 个字段,这些就像需要排序的索引,从最小值到最大值

    TArrayA = record
          Field1:integer;
          Field2:integer;
          Field3:integer;
          Field4:integer;
          Field5:integer;
          ... //other fields, strings, integers... up to 50 fields
        end;

    ArrayA:=array of TArrrayA;

目前我使用这种方法进行排序:

    // sort by Field1
    top:=Length(ArrayA);
      for counter := 0 to top do
        begin
          min := counter;
          for look := counter + 1 to top do
            if ArrayA[look].Field1 < ArrayA[min].Field1 then
              min := look;
          vTmpRecord := ArrayA[min];
          ArrayA[min] := ArrayA[counter];
          ArrayA[counter] := vTmpRecord;
        end;

   // now sort by Field2
    top:=Length(ArrayA);
      for counter := 0 to top do
        begin
          min := counter;
          for look := counter + 1 to top do
            if (ArrayA[look].Field1 = ArrayA[min].Field1) And 
               (ArrayA[look].Field2 < ArrayA[min].Field2) then
              min := look;
          vTmpRecord := ArrayA[min];
          ArrayA[min] := ArrayA[counter];
          ArrayA[counter] := vTmpRecord;
        end;

这可以完成工作。虽然当我需要对所有 5 个字段进行排序时有点慢, 这就是我的做法,逐个字段,所以我对数组进行了 5 次排序。 有没有更好更快的方法?

示例如下:

procedure TForm1.Button8Click(Sender: TObject);
type
  TArrayA = record
    Field1: integer;
    Field2: integer;
    Field3: integer;
    Field4: integer;
    Field5: integer;
  end;
var
  ArrayA: array of TArrayA;
  vTmpRecord: TArrayA;
  top, counter, min, max, look: integer;
  i,t1,t2:integer;
begin

  SetLength(ArrayA,100000);
  for i := 0 to 99999 do
  begin
    ArrayA[i].Field1:=1+Random(100);
    ArrayA[i].Field2:=1+Random(100);
    ArrayA[i].Field3:=1+Random(100);
    ArrayA[i].Field4:=1+Random(100);
    ArrayA[i].Field5:=1+Random(100);
  end;


  t1:=GetTickCount;
  // sort by Field1
  top := Length(ArrayA);
  for counter := 0 to top do
  begin
    min := counter;
    for look := counter + 1 to top do
      if ArrayA[look].Field1 < ArrayA[min].Field1 then
        min := look;
    vTmpRecord := ArrayA[min];
    ArrayA[min] := ArrayA[counter];
    ArrayA[counter] := vTmpRecord;
  end;

  // sort by Field2
  top := Length(ArrayA);
  for counter := 0 to top do
  begin
    min := counter;
    for look := counter + 1 to top do
      if (ArrayA[look].Field1 = ArrayA[min].Field1) and
        (ArrayA[look].Field2 < ArrayA[min].Field2) then
        min := look;
    vTmpRecord := ArrayA[min];
    ArrayA[min] := ArrayA[counter];
    ArrayA[counter] := vTmpRecord;
  end;

  // sort by Field3
  top := Length(ArrayA);
  for counter := 0 to top do
  begin
    min := counter;
    for look := counter + 1 to top do
      if (ArrayA[look].Field1 = ArrayA[min].Field1) and (ArrayA[look].Field2 = ArrayA[min].Field2) and
        (ArrayA[look].Field3 < ArrayA[min].Field3) then
        min := look;
    vTmpRecord := ArrayA[min];
    ArrayA[min] := ArrayA[counter];
    ArrayA[counter] := vTmpRecord;
  end;

  // sort by Field4
  top := Length(ArrayA);
  for counter := 0 to top do
  begin
    min := counter;
    for look := counter + 1 to top do
      if (ArrayA[look].Field1 = ArrayA[min].Field1) and (ArrayA[look].Field2 = ArrayA[min].Field2) and (ArrayA[look].Field3 = ArrayA[min].Field3) and
        (ArrayA[look].Field4 < ArrayA[min].Field4) then
        min := look;
    vTmpRecord := ArrayA[min];
    ArrayA[min] := ArrayA[counter];
    ArrayA[counter] := vTmpRecord;
  end;

  // sort by Field5
  top := Length(ArrayA);
  for counter := 0 to top do
  begin
    min := counter;
    for look := counter + 1 to top do
      if (ArrayA[look].Field1 = ArrayA[min].Field1) and (ArrayA[look].Field2 = ArrayA[min].Field2) and (ArrayA[look].Field3 = ArrayA[min].Field3) and (ArrayA[look].Field4 = ArrayA[min].Field4) and
        (ArrayA[look].Field5 < ArrayA[min].Field5) then
        min := look;
    vTmpRecord := ArrayA[min];
    ArrayA[min] := ArrayA[counter];
    ArrayA[counter] := vTmpRecord;
  end;

  t2:=GetTickCount;
  Button8.Caption:=IntToStr(t2-t1);
end;

【问题讨论】:

标签: arrays delphi sorting delphi-xe7


【解决方案1】:

您可以使用内置的快速排序方法通过自定义比较器对数组进行排序:

uses
  System.Math,
  System.Generics.Defaults,
  System.Generics.Collections;

  TArray.Sort<TArrayA>(ArrayA, TComparer<TArrayA>.Construct( function(const Left, Right: TArrayA): Integer
  begin
    if Left.Field1 = Right.Field1 then
      begin
        if Left.Field2 = Right.Field2 then
          begin
            Result := CompareValue(Left.Field3, Right.Field3);
          end
        else Result := CompareValue(Left.Field2, Right.Field2);
      end
    else Result := CompareValue(Left.Field1, Right.Field1);
  end
  ));

我只为前三个字段添加了代码,但您将了解如何为更多字段构建自己的比较器。

【讨论】:

  • 这看起来很简单,而且似乎非常快,从 60 秒到 110 毫秒……我在这里遗漏了什么吗?为什么速度这么快,有没有这种方法行不通的情况?
  • 了解快速排序和冒泡排序,以及它们的渐近行为。尽量避免使用这里实现的比较器,因为它充满了重复。选择使用迭代的比较器。
  • @DavidHeffernan 同意,这里的比较器是重复的。但是,我认为它描绘了如何构建自己的比较器。特别是,如果您必须对不同的字段使用不同的比较算法(当然,在此示例代码中不是这种情况)
  • 这样写的比较器很难阅读。我不推荐诸如您答案中的代码之类的方法。
  • 至少你应该使用early return来避免深度嵌套缩进,并避免使用容易引起算术溢出的减法。但是迭代要好得多。不要重复自己。
【解决方案2】:

您要做的最重要的事情是将排序算法与数据分开。这样你就可以用不同的数据一次又一次地编写或使用一个排序算法

执行此操作的经典方法是使用比较排序。它们是排序算法,需要一个比较函数来比较两个项目,小于返回负整数,大于返回正整数,相等时返回零。

所以,让我们首先为您的数据演示这种比较功能。存储多个字段使编写通用比较器变得困难。最好将字段放在数组中。完成后,您可以像这样使用迭代来比较 lexicographically

function CompareIntegerArray(const lhs, rhs: array of Integer): 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;

使用字典顺序,我们首先比较主要字段。如果它们不同,我们有我们的答案,否则我们就进入次要领域。等等。这种算法非常适合迭代,如上所示。

通过只对数组进行一次排序,这克服了您方法中的一个重大弱点。

一旦你有了这个比较函数,你需要将它包装在一个外部比较函数中,该函数从记录字段中提取数据并填充数组。也许沿着这些思路:

type
  TMyArray = array [1..5] of Integer;

function GetMyArray(const Value: TArrayA): TMyArray;
begin
  Result[1] := Value.Field1;
  Result[2] := Value.Field2;
  ....
end;

function MyCompare(const lhs, rhs: TArrayA): Integer;
begin
  Result := CompareIntegerArray(
    GetMyArray(lhs),
    GetMyArray(rhs)
  );
end;

现在,正如所承诺的,您可以将此比较函数与TArray.Sort&lt;T&gt; 之类的通用排序一起使用,来自Generics.Collections。这是Quicksort 的一个实现,一个平均复杂度为 O(n log n) 的比较排序。这通常会比您的 O(n2) bubble sort 产生巨大的好处。

如果你可以用一个实际的数组替换记录,生活会更简单。另一个可能有用的选项是向记录添加一个方法,该方法返回一个整数数组,准备好在字典比较函数中使用。

回顾一下:

  1. 分离数据、比较和排序,以方便重用和清晰。
  2. 使用数组来启用通过循环实现字典比较。
  3. 使用高效的排序算法,例如快速排序。

【讨论】:

  • 您的建议很有趣,而且看起来很有效。所以,我只是这样设置它并且它可以工作:TArray.Sort&lt;TArrayA&gt;(ArrayA, TComparer&lt;TArrayA&gt;.Construct( function(const Left, Right: TArrayA): Integer begin Result:=MyCompare(Left,Right); end ));。如果我想对不同的数组进行排序,比如 TArrayA2 的数组,我只需复制函数 GetMyArrayA2(const Value: TArrayA2): TMyArray;function MyCompareA2(const lhs, rhs: TArrayA2): Integer; 并调用这个新的 MyCompareA2 in TArray.Sort,对吗?你的例子比 Dalija 的慢一点,大约 170 毫秒对 120 毫秒。
  • 你在比较器中多了一层不必要的间接层。您可以将MyCompare 直接传递给Construct。或者您可以将MyCompare 的主体移动到您传递给Construct 的匿名方法中。我更感兴趣的是跨越概念而不是优化性能。
  • 谢谢,这似乎是我可以在整个应用程序中应用的解决方案 - 大约 150 个排序部分。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-10-24
  • 2011-10-18
相关资源
最近更新 更多