Java:两个列表之间的差异

时间:2011-06-01 13:02:57

标签: java algorithm list edit-distance

我公司的猫饲养应用程序跟踪一队猫。它需要定期将previousOrdercurrentOrder进行比较(每个都是ArrayList<Cat>),并将任何更改通知给cat-wranglers。

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

  1. 猫的顺序完全被扰乱
  2. 猫在列表中单独向上或向下移动
  3. 新猫加入,在车队的特定地点
  4. 猫离开车队
  5. 这对我来说就像edit distance problem。理想情况下,我正在寻找一种算法来确定使previousOrder匹配currentOrder所需的步骤:

    • Fluffy移至12
    • 位置
    • Snuggles
    • 位置插入37
    • 删除Mr. Chubbs

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

    最好的办法是什么?

    This postthat post提出了类似的问题,但他们都在处理已排序的列表。我的已订购,但未分类。)

    修改

    Levenshtein算法是一个很好的建议,但我关注创建矩阵的时间/空间要求。我的主要目标是尽快确定并传达变更。比找到添加和发送消息更快的事情是“这是新猫,这是当前的订单。”

5 个答案:

答案 0 :(得分:10)

这是我整理合并两个列表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)。这是必要的,因为我们不想在数据发生变化时重绘整个表格;列表之间的差异可能很小,只影响一行或两行。它可能不是您正在寻找的数据算法。如果没有,请告诉我,我会删除它。

可能会对此进行一些优化。但对于我和我正在使用的数据来说,它具有足够的性能和可预测性。

答案 1 :(得分:2)

Levenshtein距离度量。

http://www.levenshtein.net/

答案 2 :(得分:2)

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

跟踪实际操作并加入“争夺”操作应该不会太困难。

答案 3 :(得分:1)

我知道提问者正在寻求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;
    }
}

答案 4 :(得分:1)

我最近不得不这样做,除了项目可能存在多次。这是复杂的事情,但我能够使用先行计数器和其他一些疯狂来做到这一点。它看起来很像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;
}

让你的头脑走动有点棘手,但基本上你会做删除,以便你知道你要插入或移动的每个键。然后你再次浏览列表,如果有足够的,你从列表中尚未看到的部分移动一个,否则插入。当你走到尽头时,一切都排好了。