【问题标题】:What is the best algorithm to sort a list on the basis of two criteria?根据两个标准对列表进行排序的最佳算法是什么?
【发布时间】:2015-10-29 11:44:26
【问题描述】:

我有一个列表,我需要根据两个标准对其进行排序。 第一个标准是Boolean,比如说isBig。第二个是Long,它代表一个时间戳。

我需要以这种方式对列表的元素进行排序:在isBig = true 之前,然后是isBig = false。在这些组中,单个元素应根据其时间戳降序排列。

基本上,我希望结果是这样的:

isBig - 2015/10/29
isBig - 2015/10/28
isBig - 2015/10/27
!isBig - 2015/10/30
!isBig - 2015/10/27
!isBig - 2015/10/26

假设对象是这样的:

public class Item {
    Boolean isBig;
    Long timestamp;
    // ...
}

列表只是List<Item> list

我发现一种方法是创建三个 for 循环:第一个组成两个组:isBig!isBig。第二个和第三个用于对其中的元素进行排序。最后我合并了两个列表。

有没有更有效的算法根据两个标准对列表进行排序?

【问题讨论】:

  • 您可以简单地使用自定义比较器并使用Collections.sort
  • 定义“最佳”。你的名单有多大?您的环境内存是否受到限制?速度要求是什么?
  • 关于效率:最好使用booleanlong - 原始类型,因为它们更快并且节省内存。
  • 几百实际上并不是很多...
  • @Gianluca 100s 在毫秒内完成,你的速度没问题

标签: java algorithm list sorting


【解决方案1】:

您可以使用同时检查两个标准的自定义比较方法直接对列表进行排序。

使用Collections.sort 方法并将自定义比较器与compare 方法覆盖到:

 int compare(Item o1, Item o2) {
   if (o1.isBig && !o2.isBig)
     return -1;
   if (!o1.isBig && o2.isBig)
     return 1;
   if (o1.timestamp < o2.timestamp)
     return -1;
   if (o1.timestamp > o2.timestamp)
     return 1;
   return 0;
 }

如果您痴迷于性能,您可以使用更复杂的方法将性能提高几个百分点,但对于数百个元素的列表,收益将微不足道。

一种优化的比较方法:

int compare(Item o1, Item o2) {
   int bigness = (o2.isBig ? 2 : 0) - (o1.isBig ? 2 : 0);
   long diff = o1.timestamp - o2.timestamp;
   return bigness + (int) Long.signum(diff);
}

它没有条件分支,这意味着它可能会比上面的幼稚版本更快。

这可能是性能所能做的一切。如果我们对您的数据有更多了解(例如,大对象总是比小对象多,或者所有时间戳都是唯一的,或者所有时间戳都来自某个狭窄范围等),我们可能会提出一些更好的解决方案。但是,当我们假设您的数据是任意的并且没有特定模式时,最好的解决方案是使用我上面展示的标准排序实用程序。

将列表拆分为两个子列表并分别排序肯定会慢一些。实际上排序算法很可能会将数据分成两组,然后递归地分成四组,依此类推。但是,除法不会遵循isBig 标准。如果您想了解更多信息,请阅读 quick sortmerge sort 的工作原理。

【讨论】:

  • 三元运算符确实具有条件分支。所以你将分支从 4 个减少到 2 个。第一个版本很可能会被 JVM 优化为更少的分支。
  • 三元运算符没有条件分支。该代码不依赖于三元运算符的评估。实际上,如果您检查机器代码,则此例程中根本不会发生任何跳转。
  • 所以我确实检查了比较方法的 Java 字节码,并且肯定我看到了代码 ifeq 两次。它与 if 语句基本相同。
  • 让我检查一下。 x86(和大多数其他处理器架构)具有称为cmov 的特殊指令,它相当于编程语言中的三元运算符表达式。我很确定这段代码应该(最终)编译成cmov 指令。
  • 没错。但是由于三元和如果产生几乎相同的 Java 字节码,当这个字节码被编译成汇编时,优化器可以使用 cmov 来实现。
【解决方案2】:

为了让两个可比较的对象对两个参数进行排序,您需要执行以下操作。

  1. 您需要为您拥有的两个可比较对象实现 Comparator,一个 Boolean 和一个 Timestamp。
  2. 您需要将这些比较器传递给 Collections.sort(),因为它们是比较两个键的对象,并且数据结构不是它们需要 Collections.sort() 的原语。

    /**
     * Comparator to sort employees list or array in order of Salary
     */
    public static Comparator<BooleanComaprator> booleanComparator= new Comparator<BooleanComaprator>() {
    
        @Override
        public int compare(BooleanComaprator e1, BooleanComaprator e2) {
            if (e1.isBig && !e2.isBig)
                return -1;
            if (!e1.isBig && e2.isBig)
                return 1;
            else 
                return 0;
        }
    }
    

    Collections.sort(booleanComparator);中使用这个对象

【讨论】:

    【解决方案3】:

    理论上,使用两个单独列表的方法应该比使用两步的方法Comparator,因为基于一个字段的比较显然比基于两个字段的比较快.通过使用两个列表,您可以加速具有O(n log n) 时间复杂度(排序)的算法部分,但代价是增加了具有时间复杂度O(n) 的额外初始阶段(分成两部分)。由于n log n &gt; n,对于非常非常大的n 值,两个列表方法应该更快。

    然而,在实践中,我们谈论的时间差异如此之小,以至于在两个列表方法胜出之前,您必须拥有非常长的列表,因此在您开始遇到诸如此类的问题之前,使用列表来证明差异是非常困难的作为OutOfMemoryError

    但是,如果您使用数组而不是列表,并且使用巧妙的技巧来完成它而不是使用单独的数据结构,则可以击败两步 Comparator 方法,如下面的代码所示。在任何人抱怨之前:是的,我知道这不是一个合适的基准!

    尽管sort2sort1 快,但我可能不会在生产代码中使用它。最好使用熟悉的习语和明显有效的代码,而不是使用更难理解和维护的代码,即使它稍微快一些。

    public class Main {
    
        static Random rand = new Random();
    
        static Compound rand() {
            return new Compound(rand.nextBoolean(), rand.nextLong());
        }
    
        static Compound[] randArray() {
            int length = 100_000;
            Compound[] temp = new Compound[length];
            for (int i = 0; i < length; i++)
                temp[i] = rand();
            return temp;
        }
    
        static class Compound {
            boolean bool;
            long time;
    
            Compound(boolean bool, long time) {
                this.bool = bool;
                this.time = time;
            }
    
            @Override
            public boolean equals(Object o) {
                if (this == o) 
                    return true;
                if (o == null || getClass() != o.getClass()) 
                    return false;
                Compound compound = (Compound) o;
                return bool == compound.bool && time == compound.time;
            }   
    
            @Override
            public int hashCode() {
                int result = (bool ? 1 : 0);
                result = 31 * result + (int) (time ^ (time >>> 32));
                return result;
            }
        }
    
        static final Comparator<Compound> COMPARATOR = new Comparator<Compound>() {
            @Override
            public int compare(Compound o1, Compound o2) {
                int result = (o1.bool ? 0 : 1) - (o2.bool ? 0 : 1);
                return result != 0 ? result : Long.compare(o1.time, o2.time);
            }
        };
    
        static final Comparator<Compound> LONG_ONLY_COMPARATOR = new Comparator<Compound>() {
            @Override
            public int compare(Compound o1, Compound o2) {
                return Long.compare(o1.time, o2.time);
            }
        };
    
        static void sort1(Compound[] array) {
            Arrays.sort(array, COMPARATOR);
        }
    
        static void sort2(Compound[] array) {
            int secondIndex = array.length;
            if (secondIndex == 0)
                return;
            int firstIndex = 0;
            for (Compound c = array[0];;) {
                if (c.bool) {
                    array[firstIndex++] = c;
                    if (firstIndex == secondIndex)
                        break;
                    c = array[firstIndex];
                } else {
                    Compound c2 = array[--secondIndex];
                    array[secondIndex] = c;
                    if (firstIndex == secondIndex)
                        break;
                    c = c2;
                }
            }
            Arrays.sort(array, 0, firstIndex, LONG_ONLY_COMPARATOR);
            Arrays.sort(array, secondIndex, array.length, LONG_ONLY_COMPARATOR);
        }
    
        public static void main(String... args) {
    
            // Warm up the JVM and check the algorithm actually works.
            for (int i = 0; i < 20; i++) {
                Compound[] arr1 = randArray();
                Compound[] arr2 = arr1.clone();
                sort1(arr1);
                sort2(arr2);
                if (!Arrays.equals(arr1, arr2))
                    throw new IllegalStateException();
                System.out.println(i);
            }
    
            // Begin the test proper.
            long normal = 0;
            long split = 0;
            for (int i = 0; i < 100; i++) {
                Compound[] array1 = randArray();
                Compound[] array2 = array1.clone();
    
                long time = System.nanoTime();
                sort1(array1);
                normal += System.nanoTime() - time;
    
                time = System.nanoTime();
                sort2(array2);
                split += System.nanoTime() - time;
    
                System.out.println(i);
                System.out.println("COMPARATOR:           " + normal);
                System.out.println("LONG_ONLY_COMPARATOR: " + split);
            }
        }
    }
    

    【讨论】:

    • 实际上,使用 `Collections.sort' 会更快。通常,它在内部使用合并排序,这意味着列表将分为两组,然后无论如何都会合并。但是,mergesort 会以更公平的方式划分列表。而且,它会在原地分裂和合并。
    • @ciamej 我认为你错了,但我不是 100% 确定。我正在编写测试代码。
    • 请检查您的解决方案的性能是否取决于isBig 为真而isBig 为假的对象数量是否大致相等。
    【解决方案4】:

    这称为多键排序,很容易做到。如果您使用的排序库函数采用比较器回调函数来决定两个元素的相对顺序,请定义比较器函数,以便它首先检查两个输入值 a 和 b 是否具有相等的 isBig 值,并且,如果不是,则立即返回a.isBig &gt; b.isBig(我在这里假设&gt; 是为布尔值定义的;如果不是,则替换明显的测试)。但如果isBig 值相等,则应返回a.timestamp &gt; b.timestamp

    【讨论】:

      【解决方案5】:

      您可以定义一个自定义比较器并使用它对List 进行排序。例如

      class ItemComparator implements Comparator {
          @Override
          public int compare (Item a, Item b) {
              int bc = Boolean.compare(a.isBig, b.isBig);
              if (bc != 0)
                  return bc;
              return Long.compare(a.timestamp, b.timestamp);
          }
      }
      

      并像这样使用它

      Collections.sort(list, ItemComparator);
      

      【讨论】:

      • 您在 return 语句中将 long 转换为 int - 这可能会出错!
      • @ciamej 反对意见成立。我改用Long.compare
      猜你喜欢
      • 2019-08-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-09-28
      相关资源
      最近更新 更多