Можно ли побить свою игру в своей игре?

Я ищу подходящий алгоритм для сравнения двух файлов. Я думаю, что я могу сделать лучше, чем diff из-за некоторых добавленных ограничений.

У меня есть два текстовых файла, каждый из которых содержит список файлов. Это снимки всех файлов в системе, сделанные в два разных раза. Я хочу выяснить, какие файлы были добавлены или удалены между двумя моментальными снимками.

Я мог бы использовать diff для сравнения этих файлов, но я не хочу, потому что:

  • diff пытается сгруппировать изменения вместе, обнаружив, какие фрагменты в файле изменились. Я ищу только список строк, которые изменились, и это должно быть гораздо более простой проблемой, чем поиск самой длинной-общей подпоследовательности или некоторой такой вещи.

  • Обобщенные алгоритмы diff - это O (mn) во время выполнения или в пространстве. Я ищу нечто вроде O (m + n) во времени и O (1) в пространстве.

Вот ограничения на проблему:

  • Списки файлов находятся в одном порядке в обоих файлах. Они не обязательно в алфавитном порядке, но они находятся в одном и том же относительном порядке.

  • В большинстве случаев различий между списками не будет. Если есть различия, обычно будет только несколько новых/удаленных файлов.

  • Мне не нужно группировать результаты вместе, например, говоря "весь каталог удален" или "строки 100-200 являются новыми". Я могу индивидуально перечислить каждую строку, которая отличается.

Я думаю, что это эквивалентно проблеме наличия двух отсортированных списков и попытке выяснить различия между этими двумя списками. Сцепка - это элементы списка, которые не обязательно сортируются в алфавитном порядке, поэтому вы не знаете, является ли один элемент "большим", чем другой. Вы просто знаете, что файлы, которые присутствуют в обоих списках, будут в том же порядке.

Для чего это стоит, я ранее опубликовал этот вопрос на Спросить Metafilter несколько лет назад. Позвольте мне заранее ответить на несколько потенциальных ответов.

Ответ: Эта проблема называется Самая длинная общая подпоследовательность.

Ответ: Я пытаюсь избежать самой длинной общей подпоследовательности, потому что простые алгоритмы, выполняемые в O (mn) время/пространство, и лучшие из них сложны и более "эвристичны". Моя интуиция говорит мне, что существует алгоритм линейного времени из-за добавленных ограничений.

Ответ: Сортировка в алфавитном порядке, а затем сравнение.

Ответ: Это будет O (m log m + n log n), что хуже, чем O (m + n).

Ответ 1

Это не совсем O(1) память, потребность в памяти в порядке количества изменений, но это O(m+n) время выполнения.

Это по существу буферизованный алгоритм потоковой передачи, который в любой заданной строке знает разницу всех предыдущих строк.

// Pseudo-code:
initialize HashMap<Line, SourceFile> changes = new empty HashMap
while (lines left in A and B) {
    read in lineA from file A
    read in lineB from file B

    if (lineA.equals(lineB)) continue

    if (changes.contains(lineA) && changes.get(lineA).SourceFile != A) {
         changes.remove(lineA)
    } else {
         changes.add(lineA, A)
    }

    if (changes.contains(lineB) && changes.get(lineB).SourceFile != B) {
         changes.remove(lineB)
    } else {
         changes.add(lineB, B)
    }
}

for each (line in longerFile) {
    if (changes.contains(line) && changes.get(line).SourceFile != longerFile) {
         changes.remove(line)
    } else {
         changes.add(line, longerFile)
    }
}

Lines in the HashMap from SourceFile == A have been removed
Lines in the HashMap from SourceFile == B have been added

Это сильно зависит от того, что файлы указаны в одном и том же относительном порядке. В противном случае потребность в памяти будет намного больше, чем количество изменений. Однако из-за этого упорядочения этот алгоритм не должен использовать гораздо больше памяти, чем 2 * numChanges.

Ответ 2

Прочитайте один файл, поместив каждое имя файла в структуру HashSet, с O(1) add и O(1) содержит реализации.

Затем прочитайте файл секунд, проверяя каждое имя файла на HashSet.

Общий алгоритм, если файл имеет длину m, а второй файл имеет длину n O(m+n) по мере необходимости.

Примечание. Этот алгоритм предполагает, что набор данных удобно помещается в физической памяти, чтобы быть быстрым.

Если набор данных не может быть легко помещен в память, поиск может быть реализован с использованием некоторого изменения B-Tree с пейджингом на диске. Тогда сложность была бы O(mlog m) для первоначальной настройки и O(n log m) для сравнения друг с другом.

Ответ 3

С теоретической точки зрения, сравнивая расстояние редактирования между двумя строками (потому что здесь у вас есть строки на смешном языке, где "символ" - это имя файла) не может быть сделано O (m + n). Но здесь мы имеем упрощения.

Реализация алгоритма в вашем случае (должна содержать ошибки):

# i[0], i[1] are undoable iterables; at the end they both return Null

while (a = i[0].next()) && (b = i[1].next()) :    # read one item from each stream
    if a != b:                 # skip if they are identical
        c = [[a],[b]]          # otherwise, prepare two fast arrays to store difference
        for (w = 1; ; w = 1-w) # and read from one stream at a time
             nxi = Null        
             if (nx = i[1-w].next()) in c[w]:  # if we read a new character that matches
                  nxi = c[w].index(nx)          
             if nx is Null: nxi = -1           # or if we read end of stream
             if nxi is not Null:               # then output that we found some diff
                 for cc in c[1-w]: yield cc              # the ones stored 
                 for cc in c[w][0:nxi-1]: yield cc       # and the ones stored before nx
                 for cc in c[w][nxi+1:]: i[w].undo(cc)   # about the remainder - put it back
                 break                         # and return back to normal cycle
 # one of them finished
 if a: yield a
 if b: yield b
 for ci in i: 
     while (cc = ci.next()): yield cc

Существуют структуры данных, которые я называю быстрыми массивами - это, вероятно, HashSet вещи, но те, которые запоминают порядок. Добавление и поиск в них должны быть O(log N), но использование памяти O(N).

Это не использует память или циклы за пределами O(m+n) за пределами поиска различий. Для каждого "разностного блока" - операции, которая может быть описана как отнимающая M коннективных элементов и добавление N единиц - это принимает O(m+n) память и O(MN) O(Mlog N+Nlog M) инструкции. Память освобождается после того, как блок сделан, так что это не большая вещь, если у вас действительно есть небольшие изменения. Конечно, производительность худшего случая не так плоха, как при использовании общего метода.

Ответ 4

На практике разница в логарифмическом коэффициенте во времена сортировки, вероятно, незначителен - sort может сортировать сотни тысяч строк за несколько секунд. Поэтому на самом деле вам не нужно писать код:

sort filelist1 > filelist1.sorted
sort filelist2 > filelist2.sorted
comm -3 filelist1.sorted filelist2.sorted > changes

Я не утверждаю, что это обязательно самое быстрое решение - я думаю, принятый ответ Ben S будет, по крайней мере, выше некоторого значения N. Но это определенно самый простой, он будет масштабироваться до любого количества файлов, и (если вы не являетесь парнем, ответственным за операцию резервного копирования Google), он будет более чем достаточно быстрым для количества файлов, которые у вас есть.

Ответ 5

Если вы принимаете, что словари (хэш-карты) - это O (n) пространство и O (1) insert/lookup, это решение должно быть O (m + n) как во времени, так и в пространстве.

from collections import defaultdict
def diff(left, right):
    left_map, right_map = defaultdict(list), defaultdict(list)
    for index, object in enumerate(left): left_map[object] += [index]
    for index, object in enumerate(right): right_map[object] += [index]
    i, j = 0, 0
    while i < len(left) and j < len(right):
        if left_map[right[j]]:
            i2 = left_map[right[j]].pop(0)
            if i2 < i: continue
            del right_map[right[j]][0]
            for i in range(i, i2): print '<', left[i]
            print '=', left[i2], right[j]
            i, j = i2 + 1, j + 1
        elif right_map[left[i]]:
            j2 = right_map[left[i]].pop(0)
            if j2 < j: continue
            del left_map[left[i]][0]
            for j in range(j, j2): print '>', right[j]
            print '=', left[i], right[j2]
            i, j = i + 1, j2 + 1
        else:
            print '<', left[i]
            i = i + 1
    for j in range(j, len(right)): print '>', right[j]
>>> diff([1, 2, 1, 1, 3,    5, 2,    9],
...      [   2, 1,    3, 6, 5, 2, 8, 9])
< 1
= 2 2
= 1 1
< 1
= 3 3
> 6
= 5 5
= 2 2
> 8
= 9 9

Хорошо, небольшое обманывание как list.append и list.__delitem__ - это только O (1), если они связаны списками, что на самом деле не так... но что идея во всяком случае.

Ответ 6

Уточнение эфемерного ответа, при использовании изменений используется дополнительная память.

def diff(left, right):
    i, j = 0, 0

    while i < len(left) and j < len(right):
        if left[i] == right[j]:
            print '=', left[i], right[j]
            i, j = i+1, j+1
            continue

        old_i, old_j = i, j
        left_set, right_set = set(), set()

        while i < len(left) or j < len(right):
            if i < len(left) and left[i] in right_set:
                for i2 in range(old_i, i): print '<', left[i2]
                j = old_j
                break

            elif j < len(right) and right[j] in left_set:
                for j2 in range(old_j, j): print '>', right[j2]
                i = old_i
                break

            else:
                left_set .add(left [i])
                right_set.add(right[j])
                i, j = i+1, j+1

    while i < len(left):
        print '<', left[i]
        i = i+1

    while j < len(right):
        print '>', right[j]
        j = j+1

Комментарии? Улучшения?

Ответ 7

Я был после программы для разграничения больших файлов без исчерпания памяти, но не нашел ничего подходящего для моих целей. Мне не интересно использовать diff для исправления (тогда я бы, вероятно, использовал rdiff от librdiff), но для визуального контроля различий, возможно, превратив их в word-diffs с помощью dwdiff --diff-input (который читает унифицированный формат diff ) и, возможно, собирая слово-diffs как-то.

(Мой типичный прецедент: у меня есть инструмент NLP, который я использую для обработки большого текстового содержимого. Я запускаю его один раз, получаю файл длиной 122760246 строк, я делаю изменения в своем инструменте, запускаю его снова, получаю файл, который отличается как каждый миллион строк, может быть, две вставки и удаление, или только одна строка отличается от такого рода.)

Поскольку я ничего не мог найти, я просто сделал немного script https://github.com/unhammer/diff-large-files - он работает (dwdiff принимает его как входной), он достаточно быстро (быстрее, чем xz-процесс, который часто запускается после него в конвейере), и, самое главное, он не исчерпывается.

Ответ 8

Я бы прочитал списки файлов на два набора и нашел те имена файлов, которые являются уникальными для любого списка.

В Python что-то вроде:

files1 = set(line.strip() for line in open('list1.txt'))
files2 = set(line.strip() for line in open('list2.txt'))
print('\n'.join(files1.symmetric_difference(files2)))