我有一个包含文本添加和删除位置的列表,如下所示:
Type Position Text/Length
1. + 2 ab // 'ab' was added at position 2
2. + 1 cde // 'cde' was added at position 1
3. - 4 1 // a character was deleted at position 4
为了更清楚,这就是这些操作的作用:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
---------------------------------
t | e | x | t | | | | |
1. t | a | b | e | x | t | | |
2. c | d | e | t | a | b | e | x | t
3. c | d | e | a | b | e | x | t |
行动次数可以减少为:
Type Position Text/Length
1. - 1 1 // 't' was deleted at position 1
2. + 1 cdeab // 'cdeab' was added at position 1
或者:
Type Position Text/Length
1. + 1 cdeab // 'cdeab' was added at position 1
2. - 6 1 // 't' was deleted at position 6
这些操作将保存在我的数据库中,以便对此进行优化:如何减少为获得相同结果而要执行的操作数量?有没有比O(n * n)更快的方法?
请注意,这些操作是按时间顺序排列的,更改操作的顺序会产生另一种结果。
答案 0 :(得分:3)
不是解决方案,只是一些想法:
我没有看到最短解决方案的直接算法。但是,使用规则1 + 2的启发式方法可能是:
应用于样本,这意味着:
+ 2 ab
+ 1 cde
- 4 1
规则1(2x):
+ 2 ab
- 1 1 // position adjusted by -3
+ 1 cde
- 1 1
+ 1 ab // position adjusted
+ 1 cde
规则2:
- 1 1
+ 1 cdeab // watch correct order!
原始实现将是O(N * N) - 基本上是具有附加停止条件的冒泡排序。我不确定是否打算降低这种复杂性,因为由于必须调整位置,标准算法在这里没用。
然而,您可能能够显着改善 - 例如你不需要“完全排序”
答案 1 :(得分:2)
在应用所有更改之前和之后创建表示文档的二叉树。每个节点代表原始文本或插入/删除的文本;后一种节点包括要删除的原始文本量(可能为0)和要插入的文本字符串(可能为空)。
最初树只有一个节点,“0到结尾:原始文本”。应用所有更改,尽可能合并更改。然后从头到尾走树,发出最后一组编辑。这可以保证产生最佳效果。
应用插入:在树中查找适当的点。如果它位于插入文本的中间或附近,只需更改该节点的文本到插入字符串即可。否则添加一个节点。
应用删除:在树中查找起点和终点 - 与插入不同,删除可以覆盖整个范围的现有节点。相应地修改起始节点和结束节点,并终止其间的所有节点。完成后,检查是否有相邻的“插入/删除文本”节点。如果是,请加入他们。
唯一棘手的问题是确保您可以在树中找到点,而无需在每次更改时更新整个树。这是通过在每个节点缓存该子树表示的文本总量来完成的。然后,当您进行更改时,您只需直接在节点上更新这些缓存的值上面您更改的节点。
如果您打算实现平衡树并使用绳索插入文本,那么对于所有输入,这看起来严格为O(n log n)。如果你放弃了整个树的想法并使用向量和字符串,它就是O(n 2 ),但在实践中可能会正常工作。
工作示例。以下是此算法将如何逐步应用于您的示例。我不是做复杂的ascii艺术,而是将树转向侧面,按顺序显示节点,并通过缩进显示树形结构。我希望它很清楚。
初始状态:
*: orig
我上面说过,我们会缓存每个子树中的文本量。这里我只给了一个*作为字节数,因为这个节点包含整个文档,我们不知道它有多长。您可以使用任何足够大的数字,比如0x4000000000000000L。
在位置2插入“ab”后
2: orig, 2 bytes
*: insert "ab", delete nothing
*: orig, all the rest
在位置1插入“cde”后:
1: orig, 1 byte
5: insert "cde", delete nothing
1: orig, 1 byte
*: insert "ab", delete nothing
*: orig, all the rest
下一步是删除位置4处的字符。暂停此处以查看我们如何在树中找到位置4.
从根开始。查看第一个子节点:该子树包含5个字符。所以位置4必须在那里。移动到该节点。看看它的第一个子节点。这次它只包含1个字符。不在那里。这个编辑包含3个字符,所以它不在这里;它紧接着。移动到第二个子节点。 (该算法约为12行代码。)
在第4位删除1个字符后,你得到了这个......
4: orig, 1 byte
3: insert "cde", delete nothing
*: insert "ab", delete nothing
*: orig, all the rest
...然后,注意两个相邻的插入节点,将它们合并。 (请注意,给定两个相邻节点,一个在树层次结构中始终位于另一个节点之上。将数据合并到该较高节点;然后删除较低节点并更新其间的缓存子树大小。)
1: orig, 1 byte
*: insert "cdeab", delete nothing
*: orig, all the rest
答案 2 :(得分:1)
源代码控制系统中使用的“diff”工具使用算法来生成将一个源代码转换为另一个源代码所需的最小编辑 - 可能值得研究它们。我认为他们中的大多数是基于this algorithm的(最终),但是我已经有一段时间阅读了这个主题。
答案 3 :(得分:1)
我相信这可以比O(n²)平均快得多(很可能设计输入不允许快速分析)。您可以将连续添加或删除视为集合。您可以一次分析一个操作,并且必须进行一些条件转换:
这只是第一次草稿。有些事情可能必须以不同的方式完成,例如,总是应用所有删除可能更容易或更有效,因此结果总是只有一组删除后跟一组添加。
答案 4 :(得分:0)
为简单起见,我们假设只有字母a-z出现在你的文本中。
为i = 1到N初始化一个值为a [i] = i的列表A(你将自己弄清楚N应该有多大)。
在A上执行(模拟)所有操作。在此分析A之后,找到所需的操作:
Fist通过在A中找到缺失的数字来找到所需的删除操作(它们将形成连续值的组,一个组代表一个删除操作)。
在此之后,您可以通过查找连续序列来找到所需的插入操作 字母(一个序列是一个插入操作)。
在你的例子中:
init A:
1 2 3 4 5 6 7 8 9 10
步骤1(+:2:ab):
1 a b 2 3 4 5 6 7 8 9 10
Step2(+:1:cde):
c d e 1 a b 2 3 4 5 6 7 8 9 10
Step3( - :4:1):
c d e a b 2 3 4 5 6 7 8 9 10
现在我们搜索缺失的数字以查找删除。在我们的例子中,只缺少一个数字(即数字1), 所以只需要1次删除,所以我们有一个删除操作: - :1:1 (通常可能会丢失更多数字,每个缺失数字序列都是一个删除操作。 例如,如果1,2,3,5,6,10都是缺失的数字,则有3个删除操作: - :1:3, - :2:2, - :5:1。请记住,每次删除操作后,所有索引都会减少,您必须存储以前删除操作的总和,以计算当前删除操作的索引。)
现在我们搜索字符序列以查找插入操作。在我们的示例中,只有一个序列: 索引为1的cdeab,所以我们有一个插入操作:+:1:cdeab
希望这很清楚。
答案 5 :(得分:0)
如何减少操作次数:算法方法可以尝试对操作进行排序。我想,排序之后:
以下“position-number”代表文本插入或删除位置。
假设可以交换两个相邻的动作(通过调整位置数和 这两个动作的text / length属性),我们可以将动作列表带到我们的任何命令 喜欢。我建议使用升序将删除操作带到操作列表的前面 位置编号。删除操作后,添加操作将按升序排序 位置编号。
以下示例应说明,为什么我认为可以交换任何相邻的操作。
交换以下行动:
1. + 2 aaa -> taaaext
2. - 3 1 -> taaext
产生一个动作:
1. + 2 aa -> taaext
交换以下行动:
1. + 3 aaa -> teaaaxt
2. + 1 bb -> bbteaaaxt
收益率:
1. + 1 bb -> bbtext
2. + 5 aaa -> bbteaaaxt
交换以下行动:
1. + 1 bb -> bbtext
2. - 2 2 -> bext
收益率:
1. - 1 1 -> ext
2. + 1 b -> bext
如第一个示例所示,在某些情况下,交换会导致删除删除。这是一个 有益于副作用。这也是我建议将所有删除移动到的原因 前面。
我希望我不会忘记某些事情并考虑所有情况。