【问题标题】:Java: Difference between two listsJava:两个列表之间的区别
【发布时间】:2011-09-06 07:00:51
【问题描述】:

我公司的猫放牧应用程序跟踪猫车队。它需要定期比较previousOrdercurrentOrder(每个都是ArrayList<Cat>)并通知猫牧马人任何变化。

每只猫都是独一无二的,在每个列表中只能出现一次(或根本不出现)。大多数情况下,previousOrdercurrentOrder 列表具有相同的内容,顺序相同,但以下任何一种情况都可能发生(从频繁到不频繁):

  1. 猫的顺序完全打乱了
  2. 猫在列表中单独向上或向下移动
  3. 新的猫加入,在车队的特定点
  4. 猫离开车队

在我看来,这就像edit distance problem。理想情况下,我正在寻找一种算法来确定使previousOrder 匹配currentOrder 所需的步骤:

  • Fluffy 移动到12 的位置
  • 37 位置插入Snuggles
  • 删除Mr. Chubbs

算法还应该识别场景 #1,在这种情况下,新订单会完整传达。

最好的方法是什么?

This postthat post 提出了类似的问题,但它们都在处理排序列表。我的是有序,但未排序.)

编辑

Levenshtein 算法 是一个很好的建议,但我担心创建矩阵的时间/空间要求。我的主要目标是尽快确定和传达更改。比“这是新的猫,这是当前的订单”这样的查找添加和发送消息更快的东西。

【问题讨论】:

  • 面试或作业问题?
  • 不——这是我面临的现实问题。至少,感觉就像放猫一样!
  • +1 仅用于放猫示例
  • 您是否只是想找到一组命令和/或“编辑距离”来重新排序列表,或者您实际上是在重新排序列表?我们最近做了一些类似的事情,涉及使用 javascript 操作有序的<table> 行。我们想出了一个算法来移动它们,但它不是最有效的,也不会产生命令列表;它只是在遍历列表时执行它们。
  • 这是一个类似的问题——我们必须在服务器进程和客户端进程之间同步有序列表。

标签: java algorithm list edit-distance


【解决方案1】:

这是我合并两个列表oldnew 的算法。它不是最优雅或最高效的,但对于我使用它的数据来说似乎可以正常工作。

new 是最新的数据列表,old 是需要转换为new 的过期列表。该算法对old 列表执行其操作 - 相应地删除、移动和插入项目。

for(item in old)
    if (new does not contain item)
        remove item from old

for(item in new)
    if (item exists in old)
        if (position(item, old) == position(item, new))
            continue // next loop iteration
        else
            move old item to position(item, new)
    else
        insert new item into old at position(item, new)

删除都是预先完成的,以使项目的位置在第二个循环中更可预测。

这背后的驱动力是将来自服务器的数据列表与浏览器 DOM 中的 <table> 行同步(使用 javascript)。之所以需要它,是因为我们不想在数据更改时重新绘制整个表格;列表之间的差异可能很小,只影响一两行。它可能不是您正在寻找数据的算法。如果没有,请告诉我,我会删除它。

可能对此可以进行一些优化。但它对我和我正在使用的数据来说足够高效和可预测。

【讨论】:

  • 我能想到的主要优化是在被匹配项的索引处开始列表比较,而不是使用通用的contains 方法(你可能已经这样做了)跨度>
  • 这是个好建议。我会记住这一点,以防循环开始执行的速度比我需要的慢。谢谢你。
  • @Jen - 为了清楚起见,我确实简化了算法,在这里。我的代码做得更多; position() 每次迭代只调用一次,并存储在迭代范围内。我想这就是你所指的?不过,这是一个很好的建议。
【解决方案2】:

Levenshtein 距离度量。

http://www.levenshtein.net/

【讨论】:

    【解决方案3】:

    解决此问题的一种有效方法是使用动态规划。维基百科有一个密切相关问题的伪代码:Computing Levenshtein distance

    跟踪实际操作并结合“打乱”操作应该不会太难。

    【讨论】:

      【解决方案4】:

      我知道提问者正在寻找 Java 解决方案,但我在寻找用 C# 实现的算法时遇到了这个问题。

      这是我的解决方案,它生成简单 IListDifference 值的枚举:ItemAddedDifference、ItemRemovedDifference 或 ItemMovedDifference。

      它使用源列表的工作副本逐项确定需要进行哪些修改才能将其转换为与目标列表匹配。

      public class ListComparer<T>
          {
              public IEnumerable<IListDifference> Compare(IEnumerable<T> source, IEnumerable<T> target)
              {
                  var copy = new List<T>(source);
      
                  for (var i = 0; i < target.Count(); i++)
                  {
                      var currentItemsMatch = false;
      
                      while (!currentItemsMatch)
                      {
                          if (i < copy.Count && copy[i].Equals(target.ElementAt(i)))
                          {
                              currentItemsMatch = true;
                          }
                          else if (i == copy.Count())
                          {
                              // the target item's index is at the end of the source list
                              copy.Add(target.ElementAt(i));
                              yield return new ItemAddedDifference { Index = i };
                          }
                          else if (!target.Skip(i).Contains(copy[i]))
                          {
                              // the source item cannot be found in the remainder of the target, therefore
                              // the item in the source has been removed 
                              copy.RemoveAt(i);
                              yield return new ItemRemovedDifference { Index = i };
                          }
                          else if (!copy.Skip(i).Contains(target.ElementAt(i)))
                          {
                              // the target item cannot be found in the remainder of the source, therefore
                              // the item in the source has been displaced by a new item
                              copy.Insert(i, target.ElementAt(i));
                              yield return new ItemAddedDifference { Index = i };
                          }
                          else
                          {
                              // the item in the source has been displaced by an existing item
                              var sourceIndex = i + copy.Skip(i).IndexOf(target.ElementAt(i));
                              copy.Insert(i, copy.ElementAt(sourceIndex));
                              copy.RemoveAt(sourceIndex + 1);
                              yield return new ItemMovedDifference { FromIndex = sourceIndex, ToIndex = i };
                          }
                      }
                  }
      
                  // Remove anything remaining in the source list
                  for (var i = target.Count(); i < copy.Count; i++)
                  {
                      copy.RemoveAt(i);
                      yield return new ItemRemovedDifference { Index = i };
                  }
              }
          }
      

      刚刚注意到这使用了 IEnumerable 上的自定义扩展方法 - 'IndexOf':

      public static class EnumerableExtensions
      {
          public static int IndexOf<T>(this IEnumerable<T> list, T item)
          {
              for (var i = 0; i < list.Count(); i++)
              {
                  if (list.ElementAt(i).Equals(item))
                  {
                      return i;
                  }
              }
      
              return -1;
          }
      }
      

      【讨论】:

        【解决方案5】:

        我最近不得不这样做,但项目可能存在多次。这很复杂,但我能够使用前瞻计数器和其他一些疯狂的方法来做到这一点。它看起来很像 Rob 的解决方案,所以感谢他让我开始!

        首先,假设我们要返回将第一个列表转换为第二个列表的操作列表:

        public interface Operation {
            /**
             * Apply the operation to the given list.
             */
            void apply(List<String> keys);
        }
        

        我们有一些辅助方法来构造操作。您实际上不需要“移动”操作,甚至还可以进行“交换”(或替代),但这就是我所采用的:

        Operation delete(int index) { ... }
        Operation insert(int index, String key) { ... }
        Operation move(int from, int to) { ... }
        

        现在我们将定义一个特殊的类来保存我们的前瞻计数:

        class Counter {
            private Map<String, Integer> counts;
        
            Counter(List<String> keys) {
                counts = new HashMap<>();
        
                for (String key : keys) {
                    if (counts.containsKey(key)) {
                        counts.put(key, counts.get(key) + 1);
                    } else {
                        counts.put(key, 1);
                    }
                }
            }
        
            public int get(String key) {
                if (!counts.containsKey(key)) {
                    return 0;
                }
        
                return counts.get(key);
            }
        
            public void dec(String key) {
                counts.put(key, counts.get(key) - 1);
            }
        }
        

        还有一个帮助方法来获取列表中下一个键的索引:

        int next(List<String> list, int start, String key) {
            for (int i = start; i < list.size(); i++) {
                if (list.get(i).equals(key)) {
                    return i;
                }
            }
        
            throw new RuntimeException("next index not found for " + key);
        }
        

        现在我们准备好进行转换了:

        List<Operation> transform(List<String> from, List<String> to) {
            List<Operation> operations = new ArrayList<>();
        
            // make our own copy of the first, that we can mutate
            from = new ArrayList<>(from);
        
            // maintain lookahead counts
            Counter fromCounts = new Counter(from);
            Counter toCounts = new Counter(to);
        
            // do all our deletes first
            for (int i = 0; i < from.size(); i++) {
                String current = from.get(i);
        
                if (fromCounts.get(current) > toCounts.get(current)) {
                    Operation op = delete(i);
                    operations.add(op);
                    op.apply(from);
                    fromCounts.dec(current);
                    i--;
                }
            }
        
            // then one more iteration for the inserts and moves
            for (int i = 0; i < to.size(); i++) {
                String current = to.get(i);
        
                if (from.size() > i && from.get(i).equals(current)) {
                    fromCounts.dec(current);
                    continue;
                }
        
                if (fromCounts.get(current) > 0) {
                    Operation op = move(next(from, i + 1, current), i);
                    operations.add(op);
                    op.apply(from);
        
                    fromCounts.dec(current);
                } else {
                    Operation op = insert(i, current);
                    operations.add(op);
                    op.apply(from);
                }
            }
        
            return operations;
        }
        

        搞清楚你的​​头脑有点棘手,但基本上你会执行删除操作,这样你就知道你插入或移动的每个键。然后你再次浏览列表,如果有足够的,你从列表中你还没有看到的部分移动一个,否则插入。当你走到尽头时,一切都排好了。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2014-11-04
          • 1970-01-01
          • 2012-06-25
          • 1970-01-01
          • 2023-03-08
          相关资源
          最近更新 更多