有效地合并和重新排序已排序的列表

时间:2016-06-11 22:06:33

标签: java algorithm sorting merge time-complexity

这不是经典的"合并两个排序的"列出了在线性时间内fairly trivial要做的问题。

我尝试做的是合并两个已经按(key, value)排序的value对列表,其中两个列表中都有相同key的对象:对象应该合并(添加)value,这可能会改变它们的排序顺序。我主要关注如何使用已排序列表中的信息有效地执行排序,因为排序是此算法中最慢的部分。

让我们举一个具体的例子。想象一下ListStudent个对象:

class Student {
  final String name;
  final int score;
  ...
}

根据List<Student>排序的两个score作为输入,我想创建新的合并学生列表,其中任何学生(由Student.name标识)出现在两个列表中在最终列表中出现一次,得分等于两个列表中得分的总和。原始列表应保持不变。

,例如,

List 1:
{"bob", 20}
{"john", 15}
{"mark", 14}

List 2:
{"bill", 11}
{"mark", 9}
{"john", 1}

Result:
{"mark", 23}
{"bob", 20}
{"john", 16}
{"bill", 11}

合并本身(识别出现在两个列表中的学生)可以使用任何O(1)查找/插入结构(例如HashMap)在预期的O(1)时间内完成。我最感兴趣的是排序步骤(尽管我不排除同时进行合并和排序的解决方案)。

但问题是,我如何有效地重新排序这样的列表?现有列表的排序明显地对合并列表中元素的最终位置施加了一些约束。例如,如果学生在第一个列表中位于i而在第二个列表中位于j,则他必须通过简单的参数分析显示在合并列表中的第一个i + j学生中可以获得更高分数的最大学生人数。但是,如果这些信息对列表排序有用,则不能立即清楚。

您可以假设在许多情况下,在一个列表中获得高分的学生在另一个列表中获得高分。该算法应该在不是这种情况时起作用,但除了列表已经排序之外,它还为您提供了一些可能有用的分布信息。

对于任何类型的分布式查询+排序实现,这种类型的操作似乎都很常见。例如,想象一下&#34;选择状态,按状态计数(*)组&#34;针对分布式系统的查询问题类型(计算每个状态中的记录数) - 当然,您可以从每个节点获得(状态,计数)对象的排序列表,然后您需要在reduce操作期间合并和重新排序。抛弃已经在分布式节点上完成的所有工作似乎很愚蠢。

定量笔记

我对要合并和重新排序的列表很小的情况很感兴趣:通常大约有256个条目。分数范围在一些情况下从0到100变化,在其他情况下变化到大约0到10,000,000。当然,鉴于元素数量很少,即使使用天真的算法,每个操作在绝对时间内也会很快 - 但是执行了数十亿次,它会加起来。

事实上,下面的答案中有一个proven,一般情况下,这不会比增加列表大小的普通排序更好(即, n 是组合列表大小) - 但是对于固定大小的列表,我实际上对这样做更感兴趣,具有良好的经验性能。

7 个答案:

答案 0 :(得分:7)

听起来你需要使用adaptive sort算法。

  

“排序算法如果利用其输入中的现有顺序,则属于自适应排序族。它受益于输入序列中的预分类 - 或对于各种无序度量定义的有限量的无序 - 并且排序通常通过修改现有的排序算法来执行自适应排序。“ - 上面链接的维基百科文章。

示例包括插入排序和Timsort;请参阅上面的文章了解更多。请注意,在Java 8中,Arrays.sort(Object[])库方法使用修改后的Timsort。

我不知道任何已发布的算法可以处理您的示例的特定要求,但这是一个想法:

  1. 在两个输入列表L1和L2上执行经典合并:

    • 合并一对对象并更改确定排序的键时,将合并的对象放入临时列表A中。
    • 否则将对象放入临时列表B ......将保持有序。
  2. 对临时列表A进行排序。

  3. 合并列表A和B.

  4. 假设:

    • 原始列表的长度L1&amp; L2是M&amp;分别为N和
    • 其键更改的合并对象数为R(小于max(M,N)),

    然后总体复杂度为O(M + N + RlogR)。如果R相对于M + N较小,那么这应该是一种改进。

    在您的示例中,输入列表中的元素之间存在匹配的每种情况都可能以按顺序移动元素。如果它移动元素,它将按顺序移动到以后(从不更早)。所以另一个想法是在原始2个列表和优先级队列之间进行三向合并。获得匹配后,合并计数并将结果添加到优先级队列。

    复杂性与前一个类似,但您可以避免额外传递以合并列表。此外,RlogR变为RlogA,其中A是优先级队列的平均大小。

      

    请记住,我对R大约等于max(M,N),以及M == N的情况特别感兴趣。

    (你没有在你的问题中说明这一点!事实上,R对于&gt; min(M,N)没有任何意义!)

    在这种情况下,可能只使用优先级队列作为增量分拣机。抛出所有合并的记录和所有无法合并到队列中的记录,并在他们的密钥/分数小于两个列表的当前头部时拉出我们的记录。假设M和N是列表长度,并且A是平均优先级队列大小,则复杂度是max(M,N)* log A)。这是否是对简单重新排序的改进将取决于平均值A是否显着(以大O值表示)小于最大值(M,N)。这取决于输入......和合并功能。

      

    数字(N)变化,但通常为256到1,000。也许多达10,000。

    对于那个典型大小的列表,您处于复杂性分析没有帮助的水平。而且,你处于一个优化变得毫无意义的水平......除非你在很多次,或者在紧张的“时间预算”中进行操作。

    这一切都非常近似,我的数学充其量只是“粗略”。

    正确的调查将需要数百小时来研究,编码,测试,基准测试,分析各种替代方案......我们可能仍然得到它取决于输入数据集大小和分布的答案。

答案 1 :(得分:5)

看起来你想要像合并排序一样进行O(n)合并。我想我可能有一些坏消息。我将(希望)证明你不能比O(nlog(n))更好地解决广义问题:(因此,你应该使用其他人提出的任何最优O(nlog(n))解决方案)。首先,我将从直觉开始,为什么会这样,然后我会写一个非正式的证据。

直觉

我们的想法是将列表排序问题转化为问题并显示如果你能比O(nlog(n))更快地解决问题,那么我可以比O更快地排序任何列表(nlog(n) ),我们知道这是假的。我们只需使用整数来保持简单。

假设您要排序一些奇怪的序列:X = 1, 3, 2, -10, 5, 4, 7, 25。我现在将构建两个列表Dec和Inc.我从1 = 1 + 0开始(即x_1 = x_1 + 0)。然后,如果x_{i-1} -> x_i增加,我会从Dec中的值中减去1并计算Inc中的必要值,以求和x_i。如果x_{i-1} -> x_i是减少的,那么我在Inc中将我的值加1并计算Dec中的必要值以求和x_i。我们将此算法应用于下表中的序列:

idx   x     Dec    Inc      
----------------------
 1 |  1  =  1   +  0
 2 |  3  =  0   +  3
 3 |  2  =  -2  +  4
 4 | -10 =  -15 +  5
 5 |  5  =  -16 +  21
 6 |  4  =  -18 +  22
 7 |  7  =  -19 +  23
 8 |  25 =  -20 +  45

请注意,我可以在O(n)中从排序转换为您的问题 - 注意:在O(n)时间内反转Inc以获得两个递减序列。然后我们可以输入您的问题

A = {(1, 1), (2, 0), (3, -2), (4, -15), (5, -16), (6, -18), (7, -19), (8, -20)}
B = {(8, 45), (7, 23), (6, 22), (5, 21), (4, 5), (3, 4), (2, 3), (1, 0)}

现在,如果您可以将A和B按其值的总和(有序对中的第二个元素)组合成排序顺序,并获得类似

的内容
C = {(8, 25), (7, 7), (5, 5), (6, 4), (2, 3), (3, 2), (1, 1), (4, -10)

然后你基本上完成了初始序列x_i的argsort(按索引排序)。因此,如果您比O(nlog(n))更快地解决问题,那么我可以通过首先解决您的问题然后将解决方案转换为我的排序列表问题来比O(nlog(n))排序更快。特别是,我将使用复杂度O(n)+ O(解决问题的复杂性)进行排序

要证明的陈述

让你的两个键值列表

A = [(ka_i, va_i) | i = 1..n]
B = [(kb_i, vb_i) | i = 1..m] 

按值的降序排序。您找不到合并列表

C = [(ka_i, va_i + va_j) | ka_i = kb_j]

比O(nlog(n))时间快。

校样大纲

此证明的唯一假设是您不能比O(nlog(n))时间更快地对列表进行排序,并且此证明将通过提供从O(n)时间运行的减少来从任意列表排序到您的问题

实质上,我们将展示如果我们比O(nlog(n))更快地解决您的问题,那么我们也可以比O(nlog(n))更快地对任意列表进行排序。而且我们已经知道不可能比nlog(n)更快地对列表进行排序,因此您所需的解决方案也必须是不可能的。

证明细节

为简单起见,我们将对整数列表进行排序。设S = x_1, x_2, ..., x_n为任意整数序列。我们现在将构建两个列表,Dec和Inc。

我们有三个限制因素:

  1. Inc严格增加
  2. 12月严格减少
  3. 在算法的迭代i上Inc[j] + Dec[j] = x_j for all j = 1..i-1
  4. 正如他们的名字所暗示的那样,Dec将严格减少,Inc将严格增加。我们将保持x_i = Dec[i] + Inc[i] for i = 1..n

    的不变量

    这是减少:

    # (Assume 1-indexed lists)
    1. Initialize Inc = [x_1] and Dec = [0]
    2. For i = 2..n:
        a. if x[i] > x[i-1] then
              Dec.append(Dec[i-1] - 1)
              Inc.append(x_i - Dec[i])
           else   # We must have x[i] <= x[i-1]
              Inc.append(Inc[i-1] + 1)
              Dec.append(x_i - Inc[i])
    
    3. Create list A and B:
        A = [(i, Dec[i]) | i = 1..n]
        B = [(i, Inc[i]) | i = 1..n]
    4. B = reverse(B) # Reverse B because B was in increasing order and we
                      # need both lists to be in decreasing order
    5. A and B are inputs to your algorithm.
      If your algorithm can combine A and B into sorted order,
      then we have also sorted S (via argsort on the keys).
    

    你可能也渴望得到一个证据,证明我选择将Inc增加1或减少Dec减1的特殊方法。那么这里是一个非正式的“证明”(你可以通过归纳法将其形式化):

    案例x_ {i}&gt; X_ {I-1}

    回想一下,在这种情况下,我们选择将Dec递减1.我们得到x_{i} > x_{i-1},我们知道Dec_{i-1} + Inc_{i-1} = x_{i-1}。我们也可以说(Dec_{i-1} - 1) + (Inc_{i+1} + 1) = x_{i-1}

    x_{i} > x_{i-1}起,我们必须x_{i} >= x_{i-1} + 1。因此,x_{i} >= (Dec_{i-1} - 1) + (Inc_{i+1} + 1)。因此,如果我们只将Dec递减1,我们将被迫向Inc添加至少1,因此Inc仍然严格增加。

    案例x_ {i}≤x_{i-1}

    回想一下,在这种情况下,我们选择将Inc增加1.我们得到x_{i} <= x_{i-1},我们知道Dec_{i-1} + Inc_{i-1} = x_{i-1}。我们也可以说(Dec_{i-1} - 1) + (Inc_{i+1} + 1) = x_{i-1}x_{i} <= x_{i-1}(Dec_{i-1} - 1) + (Inc_{i+1} + 1) <= x_{i}就是这种情况。因此,如果我们向Inc添加1,我们确信必须从12月减去至少1。

    结论

    你的问题不能比O(nlog(n))更快地完成。你最好只是组合成一个HashMap,然后在O(nlog(n))中对它的元素进行排序,因为找不到更快的解决方案。

    如果您发现减少问题或有疑问,请随意发表评论。我很确定这是正确的。当然,如果我错误地认为排序不比O(nlog(n))快,那么整个证据就会崩溃,但最后我检查过,有人已经证明O(nlog(n))是排序最快的复杂性。评论,如果您更喜欢正式减少。对我来说现在已经很晚了,我跳过了一些“正式化”,但是当我有机会时我可以编辑它们。

    如果您编写用于创建缩减的算法,您可能会更好地理解。

    另外:如果您想要对排序What are the rules for the "Ω(n log n) barrier" for sorting algorithms?上绑定的O(nlog(n))进行解释,请参阅此帖子

答案 2 :(得分:4)

(解除首次合并然后重新排序),我的第一个尝试是声明排序的输入列表(半静态)优先级队列并继续两个阶段。为了避免术语 merge 中的歧义,我将调用创建/更改对象来表示“常见对象”的值 combine / 组合 ;为了减少混乱,我将表示优先级队列 PQ。

  1. 识别出现在两个/多个“输入队列”中的对象
    (在这里以次要的方式)
    • 组合(可能使列表中的位置无效),
    • 将它们放入另一个(动态)PQ(如有必要)
    • 从(输入)队列中删除/ invalidate,它们将不再存在。
  2. 以通常的方式合并PQ
  3. 这应该在对象的数量 n 的线性时间内工作,加上 c “常见”对象的 O(c log c)其中组合对象将不按顺序代替任何组合对象。 (...给予(识别和)组合一个(一组共同的)对象的预期的恒定时间(请参阅关于期望的O(1)的评论))
    然后,我担心这不能正确解决要点:

    有没有办法利用最终的关键词(线性,单调)
    组合至少有一个有序序列和“其他值”?
    (有很多常见的条目 - 想着所有。)

    如果组合单调减少优先级(在该示例中,添加(正)分数值增加优先级),没有组合阶段并在合并PQ时组合对象,可能会减少记忆以及所需的时间 否则,选择一个 PQ从中获取对象(优先级降低),以便与其他对象结合使用。
    “最坏情况”似乎是组合对象的优先级,显示没有相关性:我担心答案是 一般情况下,没有。 (参见user2570465's answer明确的论点)
    (作为BeeOnRope points out,如果可以检测和利用那些被挑选出来的对象(序列)被组合支配(不利选择)实际上可能变成一个好的情况。)
    然后,(线性,单调)组合可以预期倾斜键的分布,即使没有(正)相关(在问题中假设):一定要使用(动态)PQ实现,其中按顺序插入是最好的情况而不是最差情况:
    首先,取一个implicit heap in an array(索引 i 的元素的子元素位于 2i 2i + 1 (或 2i + 1 &amp; 2i + 2 “不浪费元素0”,但更多的索引操作):
    只需将项目(分布偏向降低优先级)附加到最后:
    与父母交换的预期交换数量低于1(几乎没有偏差)。

答案 3 :(得分:0)

  1. 维护一张地图,该地图将某些内容映射到实际的学生信息。

    Map<String, Student> scores = new HashMap<>();
    
  2. 遍历所有列表并将其放入分数图

    for (Student s : list1) {
        if (scores.containsKey(s.name)) {
            scores.put(s.name, s.score + scores.get(s.name));
        } else {
            scores.put(s.name, s.score); 
        } 
    }
    
  3. 使用Java 8流

    对entrySet进行排序
    scores.entrySet()
      .stream()
      .sorted((s1, s2) -> (s2.getValue().score - s1.getValue().score)
      .map(s1 -> s1.getValue())
      .collect(Collectos.toList());
    
  4. 这仍然是O(N Log N)

    您无法使用标准合并算法对其进行排序,因为列表包含位置不同的名称。标准合并算法不会两次处理相同的元素。找到副本并添加学生分数后,您需要重新排序。您打破了合并排序的前提条件,即两个列表始终按其值排序。

答案 4 :(得分:0)

在我看来,任何解决方案通常应属于O(n * log(n))复杂度的类别(n =长度(L1)+长度(L2),或n = max(长度(L1)) ,长度(L2)))。

我的基本算法如下

  Let's use two intermediate structures:
  - a TreeSet R, which guarantees ordering by rank, 
  - an HashMap M, which guarantees constant time insertion and retrieve 
  Call R's size n

  1 for each student in each list
      1.1 find the student in M by name (O(1)).
      1.2 if the student is found          
         1.2.1 find the student in R by its rank (O(log(n)).  
         1.2.2 remove the student from R (O(log(n))
         1.2.3 update the student rank 
      1.3 else 
        1.3.1. put the student in M O(1)
      1.4 put the student in R (O(log(n))
  2 At the end (if needed) transform the TreeSet in a list

整体O复杂度为O(n * log(n)),

假设L1是2个列表中最长的,那么小的优化将避免在遍历L1时找到学生,在这种情况下,O复杂度是相同的,但是你的绝对操作会少一些。 最好的情况当然是Len(L1)&gt;&gt; Len(L2)。

可能有更复杂的解决方案或更好的数据结构来减少操作次数,但我认为可能没有更好的O复杂性,因为基本上你有两种可能性

1-保持订购的结果列表,因此每次扫描列表,查找匹配和重新计算位置

2-使用中间地图降低匹配发现的复杂性,然后对结果进行排序

两种可能性通常以O(n * log(n))

计算

答案 5 :(得分:0)

在我看来,列表已经按分数排序的事实没有帮助,因为首先我们需要合并分数。

同样,虽然使用hash-map似乎可以提供O(1)搜索,但根据我的理解,底层实现意味着在包括创建hashmap的吞吐量方面,效率仍然不会那么好(与下面的相比)。

方法如下:

  1. 在List-1和List-2上合并inplace-binary-most-significant-bit-radix-sort
  2. 分数出现两次的学生将相邻,合并这些条目。
  3. 最后对合并列表中的学生分数使用inplace-binary-most-higher-bit-radix-sort(如上所述)(以便根据需要重新安排分数和学生对)。
  4. 更新#1: 第1步中的排序取决于学生姓名。

答案 6 :(得分:0)

试一试:

//班级学生修改。

public class Student {

        String name = "";
        int score = 0;

        public Student(String name, int score) {
            this.name = name;
            this.score = score;
        }

        @Override
        public boolean equals(Object v) {
            if (v instanceof Student) {
                return this.name.equals(((Student) v).name);
            } else if (v instanceof String) {
                return this.name.equals(String.valueOf(v));
            } else {
                return false;
            }
        }

        @Override
        public int hashCode() {
            int hash = 7;
            hash = 67 * hash + Objects.hashCode(this.name);
            return hash;
        }
    }

//类CustomComparator按对象或stri

对列表进行排序
public class CustomComparator implements Comparator<Object> {

        public int orderby = 0;

        @Override
        public int compare(Object o1, Object o2) {
            Student st1 = (Student)o1;
            Student st2 = (Student)o2;
            if (orderby==0){
                //order by name.
                return st1.name.compareTo(st2.name);
            }else{
                //order by score.
                Integer a=st1.score;
                Integer b = st2.score;
                return a.compareTo(b);
            }

        }
    }

//例

List<Student> A = new ArrayList<Student>();
A.add(new Student("bob", 20));
A.add(new Student("john", 15));
A.add(new Student("mark", 14));

List<Student> B = new ArrayList<Student>();
B.add(new Student("bill", 11));
B.add(new Student("mark", 9));
B.add(new Student("john", 1));

List<Student> merge = new ArrayList<Student>();
merge.addAll(A);
merge.addAll(B);

//Copy.
List<Student> result = new ArrayList<Student>();
for (Student st : merge) {
    if (result.contains(st)) {
        for (Student r : result) {
            if (r.equals(st)) {
                System.out.println(st.score + " > " +r.score);
                //Se the best score
                if (st.score > r.score) {
                    r.score = st.score;
                    break;
                }
            }
        }
    } else {
        result.add(st);
    }
}

//Sort result by name.
CustomComparator comparator = new CustomComparator();
comparator.orderby=0; //1 sort by score.
Collections.sort(result, comparator);
for (Student r : result) {
    System.out.println(r.name + " = " + r.score);
}

//结果示例:

  

bill = 11 | bob = 20 |约翰= 15 | mark = 14

相关问题