算法:有趣的差异比对算法

8

这是一个实际问题,我想分享一下,因为它可能会导致一些有趣的解决方案。基本上,算法需要对两个列表进行差异比较,但让我给您更严格的问题定义。

数学公式

假设您有两个列表LR,它们都包含来自某些底层字母表S的元素。此外,这些列表具有它们所拥有的共同元素按顺序出现的属性:也就是说,如果L[i] = R[i*]并且L[j] = R[j*],并且i < j,那么i* < j*。列表不必有任何共同的元素,并且一个或两个列表可能为空。[澄清:您可以假设没有重复的元素。]

问题是生成列表的“diff”,它可以被视为有序对(x,y)的新列表,其中x来自Ly来自R,具有以下属性:

  1. 如果x在两个列表中都出现,则(x,x)出现在结果中。
  2. 如果x出现在L中但未出现在R中,则(x,NULL)出现在结果中。
  3. 如果y出现在R中但未出现在L中,则(NULL,y)出现在结果中。

最后

  • 结果列表与每个输入列表具有“相同”的排序:它与每个列表分别共享大致相同的排序属性(请参见示例)。

示例

L = (d)
R = (a,b,c)
Result = ((NULL,d), (a,NULL), (b,NULL), (c,NULL))

L = (a,b,c,d,e)  
R = (b,q,c,d,g,e)
Result = ((a,NULL), (b,b), (NULL,q), (c,c), (d,d), (NULL,g), (e,e))

有没有好的算法来解决这个问题?复杂度是多少?


1
请告诉我您测试的结果。 我也想知道作业的正确答案。 - sblom
我猜NULL的相对顺序是任意的?也就是说,在你的第一个例子中,(NULL,d)可以出现在任何地方,对吗? - David Berger
1
你知道排序算法吗?如果是前者,那很简单,时间复杂度是O(n)。 - Jason S
@Jason,你并不知道排序算法。通过“有序列表”,我们指的是列表元素最初以某种顺序给出,其顺序很重要。这些字母代表任意元素。 - Jake
@Mike 再次检查规则,它指出如果 L(2)=R(1) 和 L(5)=R(6),且元素按照特定顺序出现在 L 中(即 2<5),那么它们也按照相同的顺序出现在 R 中(即 1<6),从而满足规则。 - Jake
显示剩余8条评论
8个回答

3
如果你愿意将列表中的一个副本存储在不同的数据结构中,那么就有一种O(n)的方法来完成这个任务。这是一个经典的时间/空间权衡。
创建列表R的哈希映射,将元素作为键,原始索引作为值;在C++中,可以使用tr1或boost中的unordered_map。
保持对未处理部分列表R的索引,初始化为第一个元素。
对于列表L中的每个元素,在哈希映射中检查是否存在匹配的列表R元素。如果没有找到,则输出(L值,NULL)。如果存在匹配项,则获取哈希映射中相应的索引。对于匹配索引之前的每个未处理元素,在输出(NULL,R值)之前都要输出出来。对于匹配项,输出(value,value)。
当你到达列表L的末尾时,遍历列表R的剩余元素并输出(NULL,R值)。 编辑:以下是Python代码解决方案。对于那些认为此解决方案依赖于存在良好的哈希函数的人 - 当然它是依赖的。如果这是一个问题,原始发布者可能会添加其他限制条件,但在那之前我会持乐观态度。
def FindMatches(listL, listR):
    result=[]
    lookupR={}
    for i in range(0, len(listR)):
        lookupR[listR[i]] = i
    unprocessedR = 0
    for left in listL:
        if left in lookupR:
            for right in listR[unprocessedR:lookupR[left]]:
                result.append((None,right))
            result.append((left,left))
            unprocessedR = lookupR[left] + 1
        else:
            result.append((left,None))
    for right in listR[unprocessedR:]:
        result.append((None,right))
    return result

>>> FindMatches(('d'),('a','b','c'))
[('d', None), (None, 'a'), (None, 'b'), (None, 'c')]
>>> FindMatches(('a','b','c','d','e'),('b','q','c','d','g','e'))
[('a', None), ('b', 'b'), (None, 'q'), ('c', 'c'), ('d', 'd'), (None, 'g'), ('e','e')]

哈希表的高效速度取决于好的哈希函数是否存在。Jake甚至没有承诺存在这样一个函数。如果确实存在这样一个函数,你可以通过它们的哈希码进行排序并进行标准有序比较,尽管这是O(nlogn)的。 - Brian
现在我很好奇 - 为什么添加一个有效的代码示例会被投票-1? - Mark Ransom

2
最坏情况下,仅使用等式定义,时间复杂度必须是O(n*m)。考虑以下两个列表:
A[] = {a,b,c,d,e,f,g}
B[] = {h,i,j,k,l,m,n}
假设这两个“有序”列表之间存在恰好一次匹配。由于不存在一个比较可以消除后续比较的需要,因此需要进行O(n*m)次比较。
因此,你想出的任何算法都将是O(n*m)或更糟糕的时间复杂度。

有没有一种方法可以在少于O(n ^ 2)的时间内对两个列表取交集? 如果是这样,我们就可以更快地完成这个操作。 - Jake
没有,没有这样的。如果你有两个列表,分别有n和m个项目,并且交集的大小为1。那么即使你事先知道交集的大小,你仍需要平均0.5nm次比较来找到交集。 - Brian
1
请查看Mark Ransom的帖子。 - Jake
-1. 独立地按照您喜欢的任何顺序,在O(log n)时间内对每个列表进行排序(这将破坏Jake第二段保证的“结构”,但这并不重要)。然后使用O(n)列表合并来查找交集--基本上,比较顶部两个元素,适当输出较小的元素,从其列表的顶部删除它,反复执行此操作。 - j_random_hacker
@Brian:他在哪里说的?我所看到的只是L和R共享的特定排序对我们来说不是已知的。我们并没有被禁止发明我们自己的排序方式(对于任何可以在计算机上表示的值集合,这总是可以做到的——例如,只需使用其二进制表示形式,解释为整数)。 - j_random_hacker
显示剩余7条评论

1
这就像序列比对一样,你可以使用Needleman-Wunsch算法来解决它。链接中包含了Python代码。只需确保设置得分,使不匹配为负,匹配为正,空白对齐时为0,以最大化得分。该算法的时间和空间复杂度均为O(n * m),但其空间复杂度可以改进。 得分函数
int score(char x, char y){
    if ((x == ' ') || (y == ' ')){
        return 0;
    }
    else if (x != y){
        return -1;
    }
    else if (x == y){
        return 1;
    }
    else{
        puts("Error!");
        exit(2);
    }
}

代码

#include <stdio.h>
#include <stdbool.h>

int max(int a, int b, int c){
    bool ab, ac, bc;
    ab = (a > b);
    ac = (a > c);
    bc = (b > c);
    if (ab && ac){
        return a;
    }
    if (!ab && bc){
        return b;
    }
    if (!ac && !bc){
        return c;
    }
}

int score(char x, char y){
    if ((x == ' ') || (y == ' ')){
        return 0;
    }
    else if (x != y){
        return -1;
    }
    else if (x == y){
        return 1;
    }
    else{
        puts("Error!");
        exit(2);
    }
}


void print_table(int **table, char str1[], char str2[]){
    unsigned int i, j, len1, len2;
    len1 = strlen(str1) + 1;
    len2 = strlen(str2) + 1;
    for (j = 0; j < len2; j++){
        if (j != 0){
            printf("%3c", str2[j - 1]);
        }
        else{
            printf("%3c%3c", ' ', ' ');
        }
    }
    putchar('\n');
    for (i = 0; i < len1; i++){
        if (i != 0){
            printf("%3c", str1[i - 1]);
        }
        else{
            printf("%3c", ' ');
        }
        for (j = 0; j < len2; j++){
            printf("%3d", table[i][j]);
        }
        putchar('\n');
    }
}

int **optimal_global_alignment_table(char str1[], char str2[]){
    unsigned int len1, len2, i, j;
    int **table;
    len1 = strlen(str1) + 1;
    len2 = strlen(str2) + 1;
    table = malloc(sizeof(int*) * len1);
    for (i = 0; i < len1; i++){
        table[i] = calloc(len2, sizeof(int));
    }
    for (i = 0; i < len1; i++){
        table[i][0] += i * score(str1[i], ' ');
    }
    for (j = 0; j < len1; j++){
        table[0][j] += j * score(str1[j], ' ');
    }
    for (i = 1; i < len1; i++){
        for (j = 1; j < len2; j++){
            table[i][j] = max(
                table[i - 1][j - 1] + score(str1[i - 1], str2[j - 1]),
                table[i - 1][j] + score(str1[i - 1], ' '),
                table[i][j - 1] + score(' ', str2[j - 1])
            );
        }
    }
    return table;
}

void prefix_char(char ch, char str[]){
    int i;
    for (i = strlen(str); i >= 0; i--){
        str[i+1] = str[i];
    }   
    str[0] = ch;
}

void optimal_global_alignment(int **table, char str1[], char str2[]){
    unsigned int i, j;
    char *align1, *align2;
    i = strlen(str1);
    j = strlen(str2);
    align1 = malloc(sizeof(char) * (i * j));
    align2 = malloc(sizeof(char) * (i * j));
    align1[0] = align2[0] = '\0';
    while((i > 0) && (j > 0)){
        if (table[i][j] == (table[i - 1][j - 1] + score(str1[i - 1], str2[j - 1]))){
            prefix_char(str1[i - 1], align1);
            prefix_char(str2[j - 1], align2);
            i--;
            j--;
        }
        else if (table[i][j] == (table[i - 1][j] + score(str1[i-1], ' '))){
            prefix_char(str1[i - 1], align1);
            prefix_char('_', align2);
            i--;
        }
        else if (table[i][j] == (table[i][j - 1] + score(' ', str2[j - 1]))){
            prefix_char('_', align1);
            prefix_char(str2[j - 1], align2);
            j--;
        }
    }
    while (i > 0){
        prefix_char(str1[i - 1], align1);
        prefix_char('_', align2);
        i--;
    }
    while(j > 0){
        prefix_char('_', align1);
        prefix_char(str2[j - 1], align2);
        j--;
    }
    puts(align1);
    puts(align2);
}

int main(int argc, char * argv[]){
    int **table;
    if (argc == 3){
        table = optimal_global_alignment_table(argv[1], argv[2]);
        print_table(table, argv[1], argv[2]);
        optimal_global_alignment(table, argv[1], argv[2]);
    }
    else{
        puts("Reqires to string arguments!");
    }
    return 0;
}

示例输入输出

$ cc dynamic_programming.c && ./a.out aab bba
__aab
bb_a_
$ cc dynamic_programming.c && ./a.out d abc
___d
abc_
$ cc dynamic_programming.c && ./a.out abcde bqcdge
ab_cd_e
_bqcdge

1

通过遍历两个列表并在匹配时进行比较,可以在线性时间内对有序列表进行差异比较。我将尝试在更新中发布一些伪Java代码。

由于我们不知道排序算法,并且无法根据小于或大于运算符确定任何排序,因此我们必须将列表视为无序。此外,考虑到结果的格式化方式,您需要扫描两个列表(至少直到找到匹配项,然后您可以进行标记并从那里重新开始)。它仍将具有O(n^2)的性能,或者更具体地说是O(nm)。


这需要您了解排序算法。第二个例子(b,q,c,d,g,e)有一个神秘的排序方式。(请注意“q”和“g”) - Jason S
是的,请注意,这些字母只是代表任意元素。 - Jake
1
好的,构建自定义小于和大于运算符,考虑到神秘排序。如果我们有一个有序列表,我必须假设我们知道如何对其进行排序,否则我们无法认为它已排序。 - Mike Pone
@Mike 我只是在数学意义上使用“有序”:将列表视为n元组。我不知道在底层字母表S上是否施加了任何部分或完全顺序。 - Jake
@Mike:既然你意识到可以构建自己的“定制”小于号,那么你为什么认为这仍然需要O(nm)时间呢?为什么不能根据你的“定制”小于号对两个列表进行排序,在O(n)时间内将它们合并,然后使用O(log n+m)排序根据L或R的神秘顺序重新排序输出列表? - j_random_hacker

0

这是一个相当简单的问题,因为您已经有了一个有序列表。

//this is very rough pseudocode
stack aList;
stack bList;
List resultList;
char aVal;
char bVal;

while(aList.Count > 0 || bList.Count > 0)
{
  aVal = aList.Peek; //grab the top item in A
  bVal = bList.Peek; //grab the top item in B

  if(aVal < bVal || bVal == null)
  {
     resultList.Add(new Tuple(aList.Pop(), null)));
  }
  if(bVal < aVal || aVal == null)
  {
     resultList.Add(new Tuple(null, bList.Pop()));
  }
  else //equal
  {
     resultList.Add(new Tuple(aList.Pop(), bList.Pop()));
  }
}

注意... 这段代码将无法编译。它只是作为指南。

编辑 基于OP的评论

如果排序算法未公开,则必须将列表视为无序。 如果列表是无序的,则算法的时间复杂度为O(n^2),具体而言是O(nm),其中n和m是每个列表中的项目数。

编辑 解决此问题的算法

L(a,b,c,d,e) R(b,q,c,d,g,e)

//pseudo code... will not compile
//Note, this modifies aList and bList, so make copies.
List aList;
List bList;
List resultList;
var aVal;
var bVal;

while(aList.Count > 0)
{
   aVal = aList.Pop();
   for(int bIndex = 0; bIndex < bList.Count; bIndex++)
   {
      bVal = bList.Peek();
      if(aVal.RelevantlyEquivalentTo(bVal)
      {
         //The bList items that come BEFORE the match, are definetly not in aList
         for(int tempIndex = 0; tempIndex < bIndex; tempIndex++)
         {
             resultList.Add(new Tuple(null, bList.Pop()));
         }
         //This 'popped' item is the same as bVal right now
         resultList.Add(new Tuple(aVal, bList.Pop()));

         //Set aVal to null so it doesn't get added to resultList again
         aVal = null;

         //Break because it's guaranteed not to be in the rest of the list
         break;
      }
   }
   //No Matches
   if(aVal != null)
   {
      resultList.Add(new Tuple(aVal, null));
   }
}
//aList is now empty, and all the items left in bList need to be added to result set
while(bList.Count > 0)
{
   resultList.Add(new Tuple(null, bList.Pop()));
}

结果集将会是

L(a,b,c,d,e) R(b,q,c,d,g,e)

结果为 ((a,null),(b,b),(null,q),(c,c),(d,d),(null,g),(e,e))


不行。与Mike Pone的答案相同;那需要你知道排序算法。第二个例子(b,q,c,d,g,e)有一个神秘的排序。(请注意"q"和"g") - Jason S
如果您不知道对象是如何排序的,那么从编程角度来看,它们就不能被视为“有序列表”。例如,给定“我最喜欢的电影”和“他最喜欢的电影”的两个有序列表,算法必须将它们视为无序列表。 - DevinB
问题再次出现在你尝试使用<和>时。你只能测试元素的相等性。 - Jake
1
如果你只能测试相等性,那么它的时间复杂度是O(m*n),虽然有一些方法可以加速算法以达到最坏情况,但复杂度始终如此。 - DevinB
如果您了解排序算法,您可以将“<”替换为“WouldAppearPriorTo”。 - DevinB
显示剩余2条评论

0

没有真正的具体答案,只有模糊的直觉。因为您不知道排序算法,只知道数据在每个列表中是有序的,这听起来有点像用于“diff”文件(例如在Beyond Compare中)并将行序列匹配在一起的算法。或者也类似于正则表达式算法。

也可能有多种解决方案。(算了吧,如果没有严格排序的重复元素,就没有必要考虑太多关于文件比较的思路了)


0

我认为你没有足够的信息。你所断言的只是匹配相同顺序的元素,但是找到第一个匹配对除非你有其他可以确定的排序,否则这是一个O(nm)操作。


-1

SELECT distinct l.element, r.element
FROM LeftList l
OUTER JOIN RightList r
ON l.element = r.element
ORDER BY l.id, r.id

假设每个元素的 ID 是其排序。当然,您的列表包含在关系数据库中 :)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接