如何用 MoonBit 实现 diff?

你使用过 Unix 下的小工具 diff 吗?

没有也没关系,简而言之,它是一个比对两个文本文件之间有什么不同之处的工具。它的作用不止于此,Unix 下还有一个叫 patch 的小工具。

时至今日,很少有人手动为某个软件包打补丁了,但 diff 在另一个地方仍然保留着它的作用:版本管理系统。能够看见某一次提交之后的源码文件发生了哪些变化(并且用不同颜色标出来)是个很有用的功能。我们以当今最流行的版本管理系统 git 为例,它可以:

diff --git a/main/main.mbt b/main/main.mbt
index 99f4c4c..52b1388 100644
--- a/main/main.mbt
+++ b/main/main.mbt
@@ -3,7 +3,7 @@fn main {let a = lines("A\nB\nC\nA\nB\nB\nA")
-  let b = lines("C\nB\nA\nB\nA\nC")
+  let b = lines("C\nB\nA\nB\nA\nA")let r = shortst_edit(a, b)println(r)}

但是,究竟怎样计算出两个文本文件的差别呢?

git 的默认 diff 算法是 Eugene W. Myers在他的论文An O(ND) Difference Algorithm and Its Variations 中所提出的,这篇论文的 pdf 可以在网上找到,但论文内容主要集中于证明该算法的正确性。

在下文中,我们将以不那么严谨的方式了解该算法的基本框架,并且使用 MoonBit 编写该算法的一个简单实现。

01 定义"差别"及其度量标准

当我们谈论两段文本的"差别"时,我们说的其实是一系列的编辑动作,通过执行这段动作,我们可以把文本 a 转写成文本 b。

假设文本 a 的内容是:

A
B
C
A
B
B
A

文本 b 的内容是:

C
B
A
B
A
C

要把文本 a 转写成文本 b,最简单的编辑序列是删除每一个 a 中的字符(用减号表示),然后插入每一个 b 中的字符(用加号表示)。

- A
- B
- C
- A
- B
- B
- A
+ C
+ B
+ A
+ B
+ A
+ C

但这样的结果对阅读代码的程序员可能没有什么帮助,而下面这个编辑序列就好很多,至少它比较短。

- A
- BC
+ BAB
- BA
+ C

实际上,它是最短的可以将文本 a 转写成文本 b 的编辑序列之一,总共有5个动作。如果仅仅以编辑序列长度作为衡量标准,这个结果足以让我们满意。但当我们审视现实中已经存在的各种编程语言,我们会发现在此之外还有一些对用户体验同样重要的指标,让我们看看下面这两个例子:

// 质量好struct RHSet[T] {set : RHTable[T, Unit]}
+
+ fn RHSet::new[T](capacity : Int) -> RHSet[T] {
+  let set : RHTable[T, Unit]= RHTable::new(capacity)
+  { set : set }
+ }// 质量不好struct RHSet[T] {set : RHTable[T, Unit]
+ }
+
+ fn RHSet::new[T](capacity : Int) -> RHSet[T] {
+  let set : RHTable[T, Unit]= RHTable::new(capacity)
+  { set : set }}

当我们在文件末尾处插入了一个新的函数定义,那计算出的编辑序列最好把更改都集中在后面。还有些类似的情况,当同时存在删除和插入时,最好不要计算出一个两种操作交织穿插的编辑序列,下面是另一个例子。

Good:   - one         Bad:    - one- two                 + four- three               - two+ four                + five+ five                + six+ six                 - three

myers 的 diff 算法能够满足我们在上面提到的这些需求,它是一种贪心算法,会尽可能地跳过相同的行(避免了在{前面插入文本的情况),同时它还会尽可能地把删除安排在插入前面,这又避免了后面一种情况。

02 算法概述

Myers 论文的基本想法是构建一张编辑序列构成的网格图,然后在这条图上搜索一条最短路径。我们沿用上面的例子 a = ABCABBA 和 b = CBABAC,建立一个 (x, y) 坐标网格。

    0     1     2     3     4     5     6     70   o-----o-----o-----o-----o-----o-----o-----o|     |     | \   |     |     |     |     ||     |     |  \  |     |     |     |     |   C|     |     |   \ |     |     |     |     |
1   o-----o-----o-----o-----o-----o-----o-----o|     | \   |     |     | \   | \   |     ||     |  \  |     |     |  \  |  \  |     |   B|     |   \ |     |     |   \ |   \ |     |
2   o-----o-----o-----o-----o-----o-----o-----o| \   |     |     | \   |     |     | \   ||  \  |     |     |  \  |     |     |  \  |   A|   \ |     |     |   \ |     |     |   \ |
3   o-----o-----o-----o-----o-----o-----o-----o|     | \   |     |     | \   | \   |     ||     |  \  |     |     |  \  |  \  |     |   B|     |   \ |     |     |   \ |   \ |     |
4   o-----o-----o-----o-----o-----o-----o-----o| \   |     |     | \   |     |     | \   ||  \  |     |     |  \  |     |     |  \  |   A|   \ |     |     |   \ |     |     |   \ |
5   o-----o-----o-----o-----o-----o-----o-----o|     |     | \   |     |     |     |     ||     |     |  \  |     |     |     |     |   C|     |     |   \ |     |     |     |     |
6   o-----o-----o-----o-----o-----o-----o-----oA     B     C     A     B     B     A

这张网格中左上方为起点(0, 0), 右下方为终点(7, 6)。沿着 x 轴向右前进一步为删除 a 中对应位置文本,沿 y 轴向下前进一步为插入 b 中对应位置文本,对角斜线标记的则是相同的文本,这些斜线可以直接跳过,它们不会触发任何编辑。

在编写实际执行搜索的代码之前,让我们先手动执行两轮搜索:

  • 第一轮搜索起点为(0, 0),移动一步可以到达(0,1)和(1,0)。
  • 第二轮搜索起点为(0,1)和(1,0),从(0,1)出发下移可以到达(0,2), 但是那里有一条通向(1,3)的斜线,所以最终落点为(1,3)。

整个myers算法的基础就是这样的广度优先搜索。

03 实现

虽然我们已经敲定了算法的基本思路,但仍有一些关键的设计需要考虑。算法的输入是两个字符串,但搜索需要在图上进行,如果真的把图构造出来再去搜索,这既非常浪费内存,也很费时间。

myers 算法的实现使用了一个聪明的想法,它定义了一个新的坐标 k = x - y。

  • 右移一步会让k加一
  • 左移一步会让k减一
  • 沿对角线向左下方移动k值不变

让我们再定义一个坐标 d 用于代表搜索的深度,以 d 为横轴 k 为纵轴画出搜索过程的树状图:

    |      0     1     2     3     4     5
----+--------------------------------------|4  |                             7,3|                           /3  |                       5,2|                     /2  |                 3,1         7,5|               /     \     /     \1  |           1,0         5,4         7,6|         /     \           \0  |     0,0         2,2         5,5|         \                       \
-1  |           0,1         4,5         5,6|               \     /     \
-2  |                 2,4         4,6|                     \
-3  |                       3,6

可以看出来,在每一轮搜索中,k都严格地处于[-d, d]区间中(因为一次移动中最多也就能在上一轮的基础上加一或者减一), 且各点之间的k值间隔为2。myers算法的基本思路便源于此:通过遍历d和k进行搜索。当然了,它还需要保存每轮搜索的x坐标供下一轮搜索使用。

让我们首先定义Line结构体,它表示文本中的一行。

struct Line {number : Int // 行号text : String // 不包含换行
} derive(Debug, Show)fn Line::new(number : Int, text : String) -> Line {Line::{ number : number, text : text }
}

然后定义一个辅助函数,它将一个字符串按照换行符分割成 Array[Line]。这里需要注意的是,行号是从1开始的。

fn lines(str : String) -> Array[Line] {let mut line_number = 0let buf = Buffer::make(50)let vec = []for i = 0; i < str.length(); i = i + 1 {let ch = str[i]buf.write_char(ch)if ch == '\n' {let text = buf.to_string()buf.reset()line_number = line_number + 1vec.push(Line::new(line_number, text))}} else {// 可能文本不以换行符为结尾let text = buf.to_string()if text != "" {line_number = line_number + 1vec.push(Line::new(line_number, text))}vec}
}

接下来我们需要包装一下数组,使其支持负数索引,原因是我们要用k的值做索引。

type BPArray[T] Array[T] // BiPolar Arrayfn BPArray::make[T](capacity : Int, default : T) -> BPArray[T] {let arr = Array::make(capacity, default)BPArray(arr)
}fn op_get[T](self : BPArray[T], idx : Int) -> T {let BPArray(arr) = selfif idx < 0 {arr[arr.length() + idx]} else {arr[idx]}
}fn op_set[T](self : BPArray[T], idx : Int, elem : T) -> Unit {let BPArray(arr) = selfif idx < 0 {arr[arr.length() + idx] = elem} else {arr[idx] = elem}
}

现在我们可以开始编写搜索函数了,不过,搜索出完整的路径是比较复杂的,我们的第一个目标是搜索出最短路径的长度(大小和搜索深度一样)。我们先展示它的基本框架:

fn shortst_edit(a : Array[Line], b : Array[Line]) -> Int {let n = a.length()let m = b.length()let max = n + mlet v = BPArray::make(2 * max + 1, 0)for d = 0; d < max + 1; d = d + 1 {for k = -d; k < d + 1; k = k + 2 {......}}
}

通过最极端的情况(两段文本没有相同的行)可以推出最多需要搜索n + m步,最少需要搜索0步。故设变量max = n + m。数组v是以k为索引保存x值的历史记录,因为k的范围是[-d, d],这个数组的大小被设为2 * max + 1。

但即使到了这一步,接下来该怎么做还是挺不好想,所以我们暂且只考虑d = 0; k = 0的情况。此时一定在(0, 0)点。同时,假如两段文本的开头相同,那就允许直接跳过。我们将这一轮的最终坐标写入数组v。

if d == 0 { // d等于0 k也一定等于0x = 0y = x - kwhile x < n && y < m && a[x].text == b[y].text {// 跳过所有相同的行x = x + 1y = y + 1}v[k] = x
}

在d > 0时,就需要用到上一轮存储的坐标信息了。当我们知道一个点的k值以及上一轮搜索中点的坐标时,v[k]的值其实很好推算。因为搜索每深入一步k的值只能加一或者减一,所以v[k]在搜索树中一定是从v[k - 1]或者v[k + 1]延伸出来的。接下来的问题是:以v[k - 1]为末端的和以v[k + 1]为末端的这两条路径,应该如何选择?

有两种边界情况:k == -d和k == d

  • k == -d时,只能选择v[k + 1]
  • k == d时,只能选择v[k - 1]

回顾一下我们之前提到的要求:尽可能地把删除安排在插入前面,这基本上意味着我们应该选择x值最大的前一个位置。

if k == -d {x = v[k + 1]
} else if k == d {x = v[k - 1] + 1 // 横向移动需要加一
} else if v[k - 1] < v[k + 1] {x = v[k + 1]
} else {x = v[k - 1] + 1
}

合并一下这四个分支,我们得到这样的代码:

if k == -d || (k != d && v[k - 1] < v[k + 1]) {x = v[k + 1]
} else {x = v[k - 1] + 1
}

综合上面的所有步骤,我们可以得到这样的代码:

fn shortst_edit(a : Array[Line], b : Array[Line]) -> Int {let n = a.length()let m = b.length()let max = n + mlet v = BPArray::make(2 * max + 1, 0)// v[1] = 0for d = 0; d < max + 1; d = d + 1 {for k = -d; k < d + 1; k = k + 2 {let mut x = 0let mut y = 0// if d == 0 {//   x = 0// }if k == -d || (k != d && v[k - 1] < v[k + 1]) {x = v[k + 1]} else {x = v[k - 1] + 1}y = x - kwhile x < n && y < m && a[x].text == b[y].text {x = x + 1y = y + 1}v[k] = xif x >= n && y >= m {return d}}} else {abort("impossible")}
}

由于数组的初始值为0,我们可以省略 d == 0 这个分支。

04 尾声

我们实现了一个不完整的myers算法,它完成了正向的路径搜索,在下一篇文章中,我们将实现回溯,还原出完整的编辑路径,并写一个可以输出彩色diff的打印函数。

本篇文章参考了:The Myers diff algorithm: part 2

感谢这篇博客的作者James Coglan。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/15480.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ros的pcl库中对于自己定义的消息,调用pcl库时总是报错 c++

首先定义自己的消息类型 struct CustomPoint { // 定义点类型结构PCL_ADD_POINT4D; // 该点类型有4个元素float intensity 0.0;uint32_t zone;uint32_t ring;uint32_t sector;EIGEN_MAKE_ALIGNED_OPERATOR_NEW // 确保new操作符对齐操作 } EIGEN_ALIGN16; // 强制SSE对齐POIN…

[BT]小迪安全2023学习笔记(第26天:Web攻防-sqlmap)

第26天 Oracle 测回显&#xff1a; and 12 union select 1, 2 from dualMongoDB 测回显&#xff1a; ?id1}); return ({title:1,content:2});用于闭合前面的({&#xff0c;后面就是自己构造的语句 sqlmap使用 基本操作 python sqlmap.py -u "网址url&#xff08…

使用C/C++ API接口操作 Zookeeper 数据

ZooKeeper 支持 Java 和 C 的API接口。本文将介绍使用 C/C 语言客户端库的编译安装和使用入门。 一、编译安装 PS&#xff1a;就在上一篇文章还觉得安装和配置 jdk 、maven 麻烦&#xff0c;所以当时选择 apache-zookeeper-[version]-bin.tar.gz 的版本。然而&#xff0c;本文…

Leetcode刷题笔记4

1658. 将 x 减到 0 的最小操作数 1658. 将 x 减到 0 的最小操作数 - 力扣&#xff08;LeetCode&#xff09; 示例 3&#xff1a; 输入&#xff1a;nums [3,2,20,1,1,3], x 10 输出&#xff1a;5 解释&#xff1a;最佳解决方案是移除后三个元素和前两个元素&#xff08;总共…

贪心part2 买卖股票的最佳时机 跳跃游戏

122.买卖股票的最佳时机II 代码随想录 分析&#xff1a;计算相邻两天的收益&#xff0c;将正收益相加即为总的最大收益 class Solution:def maxProfit(self, prices: List[int]) -> int:res 0for i in range(len(prices)-1):if prices[i1] - prices[i] > 0:res prices…

【CAN】STM32新能源汽车CAN通信实现过程

【CAN】STM32新能源汽车CAN通信实现过程 文章目录 前言一、软件1.PA11、PA12口配置2.PB8、PB9口配置 二、接线图三、硬件原理图四、上位机总结 前言 【电机控制】直流有刷电机、无刷电机汇总——持续更新 使用工具&#xff1a; 1.控制器——STM32F103C8T6 2.仿真器——STLINK …

人工智能方面顶会

人工智能 AAAI the National Conference on Artificial Intelligence 美国人工智能协会主办 IJCAJ the International Joint Conference on Artificial Intelligence每年举办 计算机视觉 CVPR IEEE Conference on Computer Vision and Pattern Recognition ECCV European Co…

【Python搞定车载自动化测试】系列文章目录汇总

系列文章目录汇总&#xff1a; 【Python搞定车载自动化测试】——PythonRobot Framework框架实现UDS诊断自动化&#xff08;含Python源码&#xff09; 【Python搞定车载自动化测试】——Python基于周立功ZCANPRO开发刷写脚本 【Python搞定车载自动化测试】——Python基于Pytest…

Opencompass模型评测教程

模型评测 模型评测非常关键&#xff0c;目前主流的方法主要可以概括为主观评测和客观评测&#xff0c;主观评测又可以分为两种形式&#xff1a;人工判断或者和模型竞技场。客观评测一般采用评测数据集的形式进行模型评测。本教程使用Opencompass工具进行对Internlm2-7b模型进行…

29. 相似矩阵,若尔当型

文章目录 1. 相似矩阵1.1 A T A A^TA ATA正定性证明 2. 相似矩阵2.1 举例2.2 证明相似矩阵具有相同特征值 1. 相似矩阵 假设矩阵A&#xff0c;B为正定矩阵&#xff0c;那么对于任意非零列向量x来说&#xff0c;二次型 x T A x , x T B x x^TAx,x^TBx xTAx,xTBx恒为正 x T A …

网络模型-策略路由配置

在实际网络应用中&#xff0c;策略路由也是一种重要的技术手段。尽管在考试并不注重策略路由&#xff0c;但是实际上应用较多建议考生除了掌握基本的静态路由协议IP route-static&#xff0c;动态路由协议RIP、还要掌握如何配置策略路由。策略路由的基本原理:根据ACL定义的不同…

深度学习之人脸性别年龄检测系统

欢迎大家点赞、收藏、关注、评论啦 &#xff0c;由于篇幅有限&#xff0c;只展示了部分核心代码。 文章目录 一项目简介 二、功能三、系统四. 总结 一项目简介 一、项目背景与意义 随着计算机视觉和深度学习技术的飞速发展&#xff0c;人脸性别年龄检测系统在多个领域展现出广…

字符串转ArrayBuffer

const arrayBuffer new ArrayBuffer(str.length)const bufferView new Uint8Array(ab) for (let i 0; i < str.length; i) {bufferView[i] str.charCodeAt(i) }【前端】失落的 ArrayBuffer - 知乎

SVG批量转为pdf超有效的方式!

最近在整理工作&#xff0c;发现ppt里面画的图智能导出svg格式无法导出pdf格式&#xff0c;由于在线的网站会把我的图片搞乱而且不想下载visio&#xff08;会把本地的word搞坏&#xff09;&#xff0c;因此琢磨出这种批量转换的方式。 1. 下载并安装Inkscape 下载链接&#xf…

使用HTTP客户端在Python中进行网页抓取——笔记

使用HTTP客户端在Python中进行网页抓取 在网页抓取的上下中&#xff0c;HTTP客户端向目标网站发送请求&#xff0c;并检索网站的HTML代码或JSON有效负载等信息。 HTTP客户端并不直接指向代码开发者&#xff0c;而是指在客户端-服务器模型中发送HTTP请求的部分。这个客户端是一…

电路仿真软件:点亮教学新篇章,十大便利助力高效学习

在信息化时代的浪潮中&#xff0c;电路仿真软件以其独特的优势&#xff0c;逐渐在教学领域崭露头角。它不仅能够帮助学生更好地理解电路知识&#xff0c;还能提升教师的教学效果。接下来&#xff0c;让我们一起探讨电路仿真软件对教学带来的十大便利。 一、直观展示电路原理 电…

Keras深度学习框架第二十四讲:KerasNLP概述

1、KerasNLP简介 KerasNLP是一个与TensorFlow深度集成的库&#xff0c;旨在简化NLP&#xff08;自然语言处理&#xff09;任务的建模过程。它提供了一系列高级API&#xff0c;用于预处理文本数据、构建序列模型和执行常见的NLP任务&#xff0c;如情感分析、命名实体识别和机器…

风控指南:国内车险欺诈呈现四大趋势

目录 车险欺诈呈现内外勾结的团伙化 防范车险欺诈需要多重合作 保险企业需要提升反欺诈能力 监管部门需要加强协同合作 2024年4月11日&#xff0c;国家金融监督管理总局官网发布国家金融监督管理总局关于《反保险欺诈工作办法&#xff08;征求意见稿&#xff09;》公开征求意见…

PCL平面多边形可视化

1、背景介绍 多边形是一个在二维平面上由直线段&#xff08;称为边或侧&#xff09;首尾顺次连接围成的封闭图形。这些直线段也称为多边形的边&#xff0c;而它们的交点称为多边形的顶点或角。在点云边缘点提取后&#xff0c;有时候需要将其进行可视化&#xff0c;如下图所示。…

【Mac】跑猫RunCat for mac(菜单栏Cpu可视化监测工具) v10.3免费版安装教程

软件介绍 RunCat是一款为菜单栏提供关键帧动画的软件。动画速度会根据Mac的CPU使用情况而变化。奔跑的小猫通过运行速度告诉您Mac的CPU使用率。 这是一款好玩的软件&#xff0c;可以为您的Mac使用添加一点小确幸。感兴趣的朋友可以试试哦。 安装步骤 1.打开安装包&#xff0…