我的编程知识大部分都是自学的,算法并不好。但是作为程序员的自尊心告诉我,算法很重要,所以后面看了《Data Structures and Algorithm Analysis in C:Second Edition》、《Algorithms》这几本书入门了算法。
非常幸运的是这两本书都有国内的英文版,价格低廉,尤其《Data Structures and Algorithm Analysis in C》售价仅仅 30 元。
简洁优雅的算法搭配精妙绝伦的数据结构,时常给人以一种美的享受。
但是在业务中,算法的应用并不多,就算遇到了,通常也有了比较好的封装。但是之前我曾遇到一个问题,就很巧的利用了我学到的算法知识,最后也取得了不错的效果。
不久前公司准备优化性能,经过分析发现翻译模块的性能差强人意,所以准备从这个地方入手,而我正好被分配来处理这个问题。下面是这个问题的简化版本:
翻译模块的功能
翻译模块是将数据库表的字段在展示给用户时进行翻译。假设有如下表结构(表名、字段右侧为对应翻译,表结构纯为示例):
user: 用户
car: 汽车
owner: Foreign Key of 「user」: 拥有者,也是指向 「user」表的外键
翻译模块的接口函数 translate 需要实现的功能为(省略无关细节):
输入表名、字段名,根据预设翻译输出对应的翻译数组,最后通过指定分隔符连接,比如中文用”的“。
支持外键无限级联扩展,包括反向。
支持循环引用
支持自定义翻译
[car, owner] => ”车辆所有人",[father, father] => ”爷爷"
,则 [car, owner, father, father, name] => 车辆所有人的爷爷的姓名
,相比原来简短很多。其他
因为翻译一部分来源是机器翻译,字段有数十万而且请求频率非常高,所以对翻译速度和内存有一定要求。
简单分析后发现这是一个算法问题,问题描述如下:
翻译模块之算法
给定两个字符串数组 arr1 以及 arr 2,arr1 为待翻译数组,arr2 为自定义翻译数组。待翻译数组中的字符可用自定义翻译中的匹配字符串替代,给出使得翻译结果片段最少的输出结果,有多个时输出任一即可。示例如下:
给出 arr1 = A, B, C, D, E, F,arr2 = B-C,C-D-E, E-F-G
输入 [A, B]
,因为 arr2 无匹配,输出 [A, B]
,最少为 2。
输入[A, B, C]
,arr2 中的 [B-C]
可取代匹配子数组 [B-C]
,输出 [A, B-C]
,最少为 2。
输入 [A, B, C, D, E]
,用 [C-D-E]
取代比使用 [B-C]
取代更短,所以输出 [A, B, C-D-E]
,最少为 2。
输入 [B, C, D, E, F, G]
,用 [B-C, E-F-G]
取代比使用 [C-D-E]
取代更短,所以输出 [B-C, D, E-F-G]
,最少为 3。
解法
典型的动态规划,对应方程为:
F_min(i) = min(F_min(i - 1) + 1, F_min(j) + 1) (j 满足 arr1[j + 1],...,arr[i] 的片段在 arr2 中存在)
arr1[j + 1: i + 1]
的可替代片段存在于 arr2,即可以基于此加 1比如翻译如上的 A, B, C ,初始条件 F_min(-1) = 0; F_min(0) = [A] = 1
F_min(1) = min(F_min(0) + 1, F_min(j) + 1)
,因为 arr2 中不存在 A-B,所以 j 不存在,F_min(1) = F_min(0) + 1 = 2
F_min(2) = min(F_min(1) + 1, F_min(j) + 1)
,此时存在 j = 0 时,arr1[1 : 3] = [B, C]
的片段 B-C 在 arr2 中存在,所以 F_min(2) = min(F_min(1) + 1, F_min(0) + 1) = min(2 + 1, 1 + 1) = 2
最少片段数为 2,实际的翻译结果为 A, B-C
代码
完整见 Github
def find(fields, seqs):
cost = [0] * (len(fields) + 1)
path = [None] * len(fields)
for i in range(len(fields)):
cost[i] = cost[i - 1] + 1
for j in range(i - 1, -1, -1):
if tuple(fields[j: i + 1]) not in seqs:
continue
if cost[j - 1] + 1 < cost[i]:
cost[i] = cost[j - 1] + 1
path[i] = j
return cost, path
fields = ('A', 'B', 'C', 'D', 'E', 'F')
custom = {
('A', 'B'),
('B', 'C'),
('C', 'D', 'E'),
('E', 'F'),
}
find(fields, custom)
翻译模块之数据结构
但是上面的算法中,自定义翻译的存储方式比较浪费空间,('A', 'B')
和 ('A', 'B', 'C')
里的 A, B 重复存储了,而且查找操作也比较低效,这里自然想到用树来保存自定义翻译。同时在实际场景中,为了实现共享翻译功能,选择基于图构建表结构以及字段之间的关系。
一开始会读取表结构以及翻译数据在内存中构建对应的翻译图,最后数据结构如图所示:
结果
上线后结果不错,性能大幅提升,翻译速度从原来的平均每次 1ms,下降到 10us 级别,而内存因为使用图避免了很多不必要的重复创建,下降为原来的四分之一。
平常更多的是处理业务问题,最常用算法的地方就是在 LeetCode 了,难得在实际场景中应用一番,而且体验还不错,希望之后在实践中能遇到更多有意思的算法问题。