簡體   English   中英

查找列表之間差異最小的算法

[英]Algorithm to find the least difference between lists

我一直試圖理解這里使用的算法來比較在這個提交中實現的兩個列表。 據我所知,目的是找到從src創建dst的最少量的更改。 稍后將這些更改列為patch命令序列。 我不是python開發人員,並且學習了generators來理解流程以及如何完成遞歸。 但是,現在我無法理解_split_by_common_seq方法生成的輸出。 我提供了幾個不同的列表,輸出如下所示。 你可以幫我理解為什么輸出就像在這些情況下一樣。

在參考案例中,

src [0, 1, 2, 3]
dst [1, 2, 4, 5]
[[(0, 1), None], [(3, 4), (2, 4)]]

我看不出它與文檔中的圖片有什么關系。 為什么(3,4)(2,4)在右邊? 它是標准算法嗎?

測試用例

src [1, 2, 3]
dst [1, 2, 3, 4, 5, 6, 7, 8]
[[None, None], [None, (3, 8)]]   

src [1, 2, 3, 4, 5]
dst [1, 2, 3, 4, 5, 6, 7, 8]
[[None, None], [None, (5, 8)]]

src [4, 5]
dst [1, 2, 3, 4, 5, 6, 7, 8]
[[None, (0, 3)], [None, (5, 8)]]

src [0, 1, 2, 3]
dst [1, 2, 4, 5]
[[(0, 1), None], [(3, 4), (2, 4)]]

src [0, 1, 2, 3]
dst [1, 2, 3, 4, 5]
[[(0, 1), None], [None, (3, 5)]]

src [0, 1, 3]
dst [1, 2, 4, 5]
[[(0, 1), None], [(2, 3), (1, 4)]] 

為了將來參考,這里是代碼(取自前面提到的存儲庫):

import itertools

def _longest_common_subseq(src, dst):
    """Returns pair of ranges of longest common subsequence for the `src`
and `dst` lists.

>>> src = [1, 2, 3, 4]
>>> dst = [0, 1, 2, 3, 5]
>>> # The longest common subsequence for these lists is [1, 2, 3]
... # which is located at (0, 3) index range for src list and (1, 4) for
... # dst one. Tuple of these ranges we should get back.
... assert ((0, 3), (1, 4)) == _longest_common_subseq(src, dst)
"""
    lsrc, ldst = len(src), len(dst)
    drange = list(range(ldst))
    matrix = [[0] * ldst for _ in range(lsrc)]
    z = 0 # length of the longest subsequence
    range_src, range_dst = None, None
    for i, j in itertools.product(range(lsrc), drange):
        if src[i] == dst[j]:
            if i == 0 or j == 0:
                matrix[i][j] = 1
            else:
                matrix[i][j] = matrix[i-1][j-1] + 1
            if matrix[i][j] > z:
                z = matrix[i][j]
            if matrix[i][j] == z:
                range_src = (i-z+1, i+1)
                range_dst = (j-z+1, j+1)
        else:
            matrix[i][j] = 0
    return range_src, range_dst

def split_by_common_seq(src, dst, bx=(0, -1), by=(0, -1)):
    """Recursively splits the `dst` list onto two parts: left and right.
The left part contains differences on left from common subsequence,
same as the right part by for other side.

To easily understand the process let's take two lists: [0, 1, 2, 3] as
`src` and [1, 2, 4, 5] for `dst`. If we've tried to generate the binary tree
where nodes are common subsequence for both lists, leaves on the left
side are subsequence for `src` list and leaves on the right one for `dst`,
our tree would looks like::

[1, 2]
/ \
[0] []
/ \
[3] [4, 5]

This function generate the similar structure as flat tree, but without
nodes with common subsequences - since we're don't need them - only with
left and right leaves::

[]
/ \
[0] []
/ \
[3] [4, 5]

The `bx` is the absolute range for currently processed subsequence of
`src` list. The `by` means the same, but for the `dst` list.
"""
    # Prevent useless comparisons in future
    bx = bx if bx[0] != bx[1] else None
    by = by if by[0] != by[1] else None

    if not src:
        return [None, by]
    elif not dst:
        return [bx, None]

    # note that these ranges are relative for processed sublists
    x, y = _longest_common_subseq(src, dst)

    if x is None or y is None: # no more any common subsequence
        return [bx, by]

    return [split_by_common_seq(src[:x[0]], dst[:y[0]],
                                 (bx[0], bx[0] + x[0]),
                                 (by[0], by[0] + y[0])),
            split_by_common_seq(src[x[1]:], dst[y[1]:],
                                 (bx[0] + x[1], bx[0] + len(src)),
                                 (bx[0] + y[1], bx[0] + len(dst)))]

這是一個可愛的算法,但我不認為這是一個“已知”的算法。 這是比較列表的一種聰明方式,可能不是第一次有人想到它,但我以前從未見過它。

基本上,輸出告訴你srcdst中看起來不同的范圍。

該函數始終返回包含2個列表的列表。 第一個列表是指srcdst中位於srcdst之間最長公共子序列左側的元素; 第二個是指最長公共子序列右側的元素。 這些列表中的每一個都包含一對元組。 元組表示列表中的范圍 - (x, y)表示執行lst[x:y]將獲得的元素。 從這對元組中,第一個元組是src的范圍,第二個元組是dst的范圍。

在每個步驟中,算法計算srcdst的范圍,這些范圍位於最長公共子序列的左側和srcdst之間最長公共子序列的右側。

讓我們看看你的第一個例子來清理:

src [0, 1, 2, 3]
dst [1, 2, 4, 5]

srcdst之間最長的公共子序列是[1, 2] src ,范圍(0, 1)定義緊鄰[1, 2]左邊的元素; dst ,該范圍是空的,因為在[1, 2]之前沒有任何內容。 因此,第一個列表將是[(0, 1), None]

[1, 2]的右邊,在src ,我們有范圍(3, 4)的元素,而在dst我們有4和5,它們由范圍(2, 4) 所以第二個列表將是[(3, 4), (2, 4)]

你去了:

[[(0, 1), None], [(3, 4), (2, 4)]]

這與評論中的樹有何關系?

樹中的葉子使用不同的表示法:而不是描述范圍的元組,顯示該范圍內的實際元素。 實際上, [0]src范圍(0, 1)中唯一的元素。 這同樣適用於其余部分。

一旦你得到這個,你發布的其他例子應該很容易理解。 但請注意,如果存在多個公共子序列,則輸出會變得更復雜:算法以非遞增順序查找每個公共子序列; 由於每次調用都返回一個包含2個元素的列表,這意味着您將在這些情況下獲得嵌套列表。 考慮:

src = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
dst = [46, 1, 2, 3, 4, 5, 99, 98, 97, 5, 6, 7, 30, 31, 32, 11, 12, 956]

這輸出:

[[(0, 1), (0, 1)], [[[None, (6, 10)], [(8, 11), (12, 15)]], [(13, 14), (17, 18)]]]

第二個列表是嵌套的,因為有多個遞歸級別(您之前的示例立即落在基本案例上)。

之前顯示的解釋遞歸地應用於每個列表: [[(0, 1), (0, 1)], [[[None, (6, 10)], [(8, 11), (12, 15)]], [(13, 14), (17, 18)]]]的第二個列表[[(0, 1), (0, 1)], [[[None, (6, 10)], [(8, 11), (12, 15)]], [(13, 14), (17, 18)]]]顯示最長公共子序列右側列表中的差異。

最長的共同子序列是[1, 2, 3, 4, 5] [1, 2, 3, 4, 5]的左側,兩個列表在第一個元素中是不同的(范圍相等且易於檢查)。

現在,該過程以遞歸方式應用。 對於右側,有一個新的遞歸調用, srcdst變為:

src = [6, 7, 8, 9, 10, 11, 12, 13]
dst = [99, 98, 97, 5, 6, 7, 30, 31, 32, 11, 12, 956]

    # LCS = [6, 7]; Call on the left
        src = []
        dst = [99, 98, 97, 5]
    # LCS = [6, 7]; Call on the right
        src = [8, 9, 10, 11, 12, 13]
        dst = [30, 31, 32, 11, 12, 956]
        # LCS = [11, 12]; Call on the left
            src = [8, 9, 10]
            dst = [30, 31, 32]
        # LCS = [11, 12]; Call on the right
            src = [13]
            dst = [956]

最長的共同子序列是[6, 7] 那么你將在左邊有另一個遞歸調用,對於src = []dst = [99, 98, 97, 5] ,現在沒有最長的公共子序列,並且這一側的遞歸停止(只需按照圖片)。

每個嵌套列表遞歸地表示調用過程的子列表上的差異,但請注意索引始終引用原始列表中的位置(由於bxby參數傳遞方式 - 注意它們總是累積從一開始)。

這里的關鍵點是,您將獲得與遞歸深度成線性比例的嵌套列表,事實上,只需查看嵌套級別,您就可以實際了解原始列表中存在多少個常見子序列。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM