我一直在尝试理解使用here比较两个列表的算法,这个列表在commit中实现。据我所知,目的是找到从dst
创建src
的最少量更改。稍后将这些更改列为patch
命令的序列。我不是python开发人员,并且学习generators
来理解流程以及如何完成递归。但是,现在我无法理解_split_by_common_seq
方法生成的输出。我提供了几个不同的列表,输出如下所示。你可以帮我理解为什么输出就像在这些情况下一样。
在参考案例中,
src [0, 1, 2, 3]
dst [1, 2, 4, 5]
[[(0, 1), None], [(3, 4), (2, 4)]]
我无法看到它与文档中的picture有何关联。为什么(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)))]
答案 0 :(得分:3)
这是一个可爱的算法,但我不认为这是一个“已知”的算法。这是比较列表的一种聪明方式,可能不是第一次有人想到它,但我以前从未见过它。
基本上,输出会告诉您src
和dst
中看起来不同的范围。
该函数始终返回包含2个列表的列表。第一个列表是指src
和dst
中位于src
和dst
之间最长公共子序列左侧的元素;第二个是指最长公共子序列右侧的元素。这些列表中的每一个都包含一对元组。元组表示列表中的范围 - (x, y)
表示执行lst[x:y]
时将获得的元素。从这对元组中,第一个元组是src
的范围,第二个元组是dst
的范围。
在每一步中,算法计算src
和dst
的范围,这些范围位于最长公共子序列的左侧和src
之间的最长公共子序列的右侧。 dst
。
让我们看看你的第一个例子来清理:
src [0, 1, 2, 3]
dst [1, 2, 4, 5]
src
和dst
之间最长的公共子序列是[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]
是(0, 1)
中src
范围内的唯一元素。这同样适用于其余部分。
一旦你得到这个,你发布的其他例子应该很容易理解。但请注意,如果存在多个公共子序列,则输出会变得更复杂:算法以非递增顺序查找每个公共子序列;由于每次调用都返回一个包含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)]]]
中的第二个列表显示最长公共子序列右侧列表中的差异。
最长的公共子序列是[1, 2, 3, 4, 5]
。在[1, 2, 3, 4, 5]
的左侧,两个列表在第一个元素中是不同的(范围相等且易于检查)。
现在,该过程以递归方式应用。对于右侧,有一个新的递归调用,src
和dst
成为:
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]
,现在没有最长的公共子序列,此侧的递归停止(只需按照图片)。
每个嵌套列表递归地表示调用过程的子列表上的差异,但请注意索引始终引用原始列表中的位置(由于bx
和{{的参数的方式1}}被传递 - 注意它们总是从开始就累积。)
这里的关键点是,您将获得与递归深度成线性比例的嵌套列表,事实上,您只需通过查看嵌套级别就可以实际知道原始列表中存在多少个常见子序列。