go语言:实现Levenshtein(编辑)距离算法(附带源码)

一、项目背景详细介绍

在字符串处理、自然语言处理(NLP)、搜索引擎、拼写纠错、模糊匹配、DNA序列分析等领域,编辑距离(Edit Distance) 是一个极其重要的算法。其中最经典、最广泛使用的就是 Levenshtein 距离算法

Levenshtein 距离由俄罗斯数学家:

  • Vladimir Levenshtein

在1965年提出,用于衡量两个字符串之间的相似程度。


什么是编辑距离?

编辑距离表示:

将字符串A转换为字符串B所需的最少编辑操作次数。

允许的三种基本操作:

  1. 插入(Insert)
  2. 删除(Delete)
  3. 替换(Replace)

举例说明

例1:

kitten → sitting

操作过程:

kitten
sitten (k → s 替换)
sittin (e → i 替换)
sitting (g 插入)

编辑距离 = 3


例2:

flaw → lawn

操作:

flaw
law (删除 f)
lawn (插入 n)

编辑距离 = 2


二、项目需求详细介绍


2.1 基础需求

实现函数:

func Levenshtein(a, b string) int

功能:

  • 输入两个字符串
  • 返回它们的编辑距离
  • 支持Unicode
  • 时间复杂度 O(m*n)

2.2 输入输出示例

示例1:

输入: "kitten", "sitting"
输出: 3

示例2:

输入: "flaw", "lawn"
输出: 2

示例3(Unicode):

输入: "你好", "您好"
输出: 1


2.3 进阶需求

  • 支持Unicode
  • 提供空间优化版本
  • 提供企业级封装
  • 提供可扩展版本(支持自定义操作权重)

三、相关技术详细介绍


3.1 动态规划(Dynamic Programming)

Levenshtein算法本质是:

二维动态规划问题

定义:

dp[i][j] = a的前i个字符 与 b的前j个字符 的最小编辑距离

状态转移方程:

如果 a[i-1] == b[j-1]:
dp[i][j] = dp[i-1][j-1]
否则:
dp[i][j] = min(
dp[i-1][j] + 1, // 删除
dp[i][j-1] + 1, // 插入
dp[i-1][j-1] + 1 // 替换
)


3.2 时间复杂度

设:

m = len(a)
n = len(b)

时间复杂度:

O(m * n)

空间复杂度:

O(m * n)

可优化为:

O(min(m, n))


3.3 Unicode处理

Go字符串是UTF-8编码。

必须转换为:

[]rune

否则中文会出错。


四、实现思路详细介绍


第一步:转为rune数组

ra := []rune(a)
rb := []rune(b)


第二步:创建二维DP数组

dp := make([][]int, m+1)


第三步:初始化边界

dp[i][0] = i
dp[0][j] = j


第四步:填表

根据状态转移公式计算。


第五步:返回结果

dp[m][n]


五、完整实现代码

// ===================================== // file: distance/levenshtein.go // ===================================== package distance // DistanceCalculator 编辑距离计算器 type DistanceCalculator struct{} // NewDistanceCalculator 构造函数 func NewDistanceCalculator() *DistanceCalculator { return &DistanceCalculator{} } // -------------------------------------------------- // 标准二维动态规划版本(推荐教学版) // -------------------------------------------------- func (d *DistanceCalculator) Levenshtein(a, b string) int { ra := []rune(a) rb := []rune(b) m := len(ra) n := len(rb) // 创建二维DP数组 dp := make([][]int, m+1) for i := range dp { dp[i] = make([]int, n+1) } // 初始化边界 for i := 0; i <= m; i++ { dp[i][0] = i } for j := 0; j <= n; j++ { dp[0][j] = j } // 填充DP表 for i := 1; i <= m; i++ { for j := 1; j <= n; j++ { if ra[i-1] == rb[j-1] { dp[i][j] = dp[i-1][j-1] } else { dp[i][j] = min( dp[i-1][j]+1, // 删除 dp[i][j-1]+1, // 插入 dp[i-1][j-1]+1, // 替换 ) } } } return dp[m][n] } // -------------------------------------------------- // 空间优化版本(滚动数组) // 空间复杂度 O(min(m,n)) // -------------------------------------------------- func (d *DistanceCalculator) LevenshteinOptimized(a, b string) int { ra := []rune(a) rb := []rune(b) if len(ra) < len(rb) { ra, rb = rb, ra } m := len(ra) n := len(rb) prev := make([]int, n+1) curr := make([]int, n+1) for j := 0; j <= n; j++ { prev[j] = j } for i := 1; i <= m; i++ { curr[0] = i for j := 1; j <= n; j++ { if ra[i-1] == rb[j-1] { curr[j] = prev[j-1] } else { curr[j] = min( prev[j]+1, curr[j-1]+1, prev[j-1]+1, ) } } prev, curr = curr, prev } return prev[n] } // min 辅助函数 func min(a, b, c int) int { if a < b { if a < c { return a } return c } if b < c { return b } return c } // ===================================== // file: main.go // ===================================== package main import ( "fmt" "distance/distance" ) func main() { calculator := distance.NewDistanceCalculator() fmt.Println("标准版本:") fmt.Println(calculator.Levenshtein("kitten", "sitting")) fmt.Println("优化版本:") fmt.Println(calculator.LevenshteinOptimized("flaw", "lawn")) fmt.Println("Unicode测试:") fmt.Println(calculator.Levenshtein("你好", "您好")) }

六、代码详细解读(仅解读方法作用)


Levenshtein

作用:

  • 使用二维DP数组
  • 完整保存状态
  • 易于理解
  • 适合教学

LevenshteinOptimized

作用:

  • 使用滚动数组
  • 只保存上一行数据
  • 空间复杂度降低
  • 适合生产环境

min函数

作用:

  • 返回三个数的最小值
  • 用于状态转移

七、项目详细总结

本项目实现:

  • 标准二维DP版本
  • 空间优化版本
  • Unicode安全版本
  • 企业级封装结构

时间复杂度:

O(m * n)

空间复杂度:

标准版:

O(m * n)

优化版:

O(min(m, n))


八、项目常见问题及解答


Q1 为什么必须使用[]rune?

因为Go字符串是UTF-8。

中文字符占多个字节。

如果用byte会出错。


Q2 是否可以支持权重不同?

可以。

将 +1 替换为:

+ cost

即可实现加权编辑距离。


Q3 是否可以优化时间复杂度?

在一般情况下无法突破 O(m*n)。

但可以:

  • 提前剪枝
  • 使用带阈值的算法

九、扩展方向与性能优化


1 Damerau-Levenshtein距离

增加操作:

  • 相邻字符交换

2 构建拼写纠错系统

结合:

  • 字典
  • Trie树
  • 编辑距离阈值

3 模糊搜索系统

用于:

  • 搜索引擎
  • 商品匹配
  • 用户名检测

4 生物信息学应用

用于:

  • DNA序列匹配
  • 蛋白质序列分析

结语

你现在已经掌握:

  • 动态规划思想
  • 二维DP构造
  • 滚动数组优化
  • Unicode安全处理
  • O(m*n)复杂度设计

Read more

前端虚拟列表深度拆解

虚拟列表是为了解决什么问题 真实项目中的痛点: 想象一个后台系统:用户列表:10 万条;订单列表:20 万条;日志列表:百万级;表格里还有:多列、复杂 DOM、hover、操作按钮、状态标签 直接 map 渲染: data.map(item => <Row key={item.id} />) 会遇到:首次渲染卡死、滚动严重掉帧、内存暴涨和浏览器直接崩 根因只有一个:DOM 太多,浏览器不是怕 JS,浏览器最怕的是成千上万个 DOM 节点 总的来说虚拟列表就是只渲染可视区域内的列表项,而其余的用占位高度“假装存在” 虚拟列表的核心思想 我总结主要要理解这四点: 1.可视区域(

By Ne0inhk
Flutter for OpenHarmony: Flutter 三方库 jaspr 为鸿蒙端开启极速渲染的现代 Web 开发新范式(Dart Web 框架首选)

Flutter for OpenHarmony: Flutter 三方库 jaspr 为鸿蒙端开启极速渲染的现代 Web 开发新范式(Dart Web 框架首选)

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 在进行 OpenHarmony 开发时,我们偶尔需要跳出原生的 HAP 容器,寻找更轻量、更适合在移动端 Web 加载的方案。虽然 Flutter Web 极其强大,但其生成的 Canvas/Wasm 产物体积巨大,在鸿蒙系统加载较慢。是否存在一种方案,既能使用 Dart 的声明式开发体验,又能产出纯正、轻量的 HTML/CSS/JS 节点? jaspr 就是这个问题的终极答案。它是一个模仿 Flutter 语法、但专注于渲染原生 Web DOM 的现代框架。通过 Jaspr,鸿蒙开发者可以利用熟悉的 Widget、Component 和生命周期,

By Ne0inhk
Flutter 三方库 flutter_dropzone 的鸿蒙化适配指南 - 掌握万物皆可拖拽的资源流转技术、助力鸿蒙大屏与 Web 应用构建极致直观的文件导入与交互体系

Flutter 三方库 flutter_dropzone 的鸿蒙化适配指南 - 掌握万物皆可拖拽的资源流转技术、助力鸿蒙大屏与 Web 应用构建极致直观的文件导入与交互体系

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 flutter_dropzone 的鸿蒙化适配指南 - 掌握万物皆可拖拽的资源流转技术、助力鸿蒙大屏与 Web 应用构建极致直观的文件导入与交互体系 前言 在 OpenHarmony 鸿蒙应用全场景覆盖、特别是适配鸿蒙桌面模式(Desktop Mode)、折叠屏大屏交互及鸿蒙 Web 版推送的工程实战中,“文件拖拽(Drag and Drop)”已成为提升生产力效率的标配功能。用户希望能够像在 PC 上一样,直接将图片或文档拖入应用窗口即可完成上传。如何实现这种跨越边界的直观交互?flutter_dropzone 作为一个专注于“拖放区域感知与文件流提取”的库,旨在为鸿蒙开发者提供一套标准的拖放治理方案。本文将详述其在鸿蒙端的实战技法。 一、原原理分析 / 概念介绍 1.1 基础原理 flutter_dropzone

By Ne0inhk
【数据结构指南】深入二叉树遍历

【数据结构指南】深入二叉树遍历

前言:         在前文中,我们探讨了完全二叉树和满二叉树的概念与性质,并基于完全二叉树实现了堆这一数据结构。然而,对于普通二叉树的认识仍有待深入,本文将系统性地介绍普通二叉树的遍历相关内容。                   一、前置说明          一般而言,对于一棵普通二叉树是通过链式结构定义,即每个节点包含三个部分:          1.数据域(data):用于存储节点的值          2.左指针(left):用于指向左子节点          3.右指针(right):用于指向右子节点                  typedef int BTDataType;//此处将 (int)整形 作为节点存储的内容 typedef struct BinaryTree { BTDataType data; struct BinaryTree* left; struct BinaryTree* right; }BTNode;                  在学习二叉树的基本操作前,需先要先创建一棵二叉树,然后才能学习

By Ne0inhk