最近工作中,需要求两个单词最小编辑距离,用于判断两个字符串的相近程度。

最小编辑距离是什么

允许一个字符串 A ,通过以下三种操作,变换成另一个字符串 B
插入一个字符,例如:ab -> abc
删除一个字符,例如:abc -> ab
替换一个字符,例如:abc -> dbc
所需要的最短操作次数,称为最短编辑距离 D,又称 Levenshtein 距离
故两个字符串的相识度 S = D / Max(len(A), len(B))
使用场景:搜索引擎的单词纠错,扩召回等。

解决思路

我发现这个问题,其实也是LeetCode的一道题目, 以前有些枯燥的算法题,应用在工程领域上会显得如此可爱迷人。要解决这个问题,暴力枚举很快会让人绝望。直觉告诉我们要分而治之, 将复杂的问题分解为相似的子问题。

假设字符串 A 共 m 位,从 A[1] 到 A[m];字符串 B 共 n 位,从 B[1] 到 B[n];D[i][j] 表示字符串 A[1]-A[i] 转换为 B[1]-B[j] 的编辑距离。

  • 规律一: 当 A[i] == B[j] 时,D[i][j] = D[i-1][j-1],比如 hel -> hal 的编辑距离 = he -> ha 的编辑距离
  • 规律二: 当 A[i] 不等于 B[j] 时,D[i][j] 等于如下 3 项的最小值:
    • D[i-1][j] + 1(删除 A[i]),比如 xyz -> abc 的编辑距离 = xy -> abc 的编辑距离 + 1
    • D[i][j-1] + 1(插入 B[j]), 比如 xyz -> abc 的编辑距离 = xyzc -> abc 的编辑距离 + 1; 根据规律一, xyzc->abc 由于最后一个字符是一样的, 所以它的编辑距离等于 xyz->ab, 因此xyz -> abc 的编辑距离 = xyzc -> abc 的编辑距离 + 1 = xyz->ab 的编辑距离 + 1。
    • D[i-1][j-1] + 1(将 A[i] 替换为 B[j]), 比如 xyz -> abc 的编辑距离 = xyc -> abc 的编辑距离 + 1 = xy -> ab 的编辑距离 + 1 (根据规律一)
  • 边界:
    • A[i][0] = i, B 字符串为空,表示将 A[1]-A[i] 全部删除,所以编辑距离为 i
    • B[0][j] = j, A 字符串为空,表示 A 插入 B[1]-B[j],所以编辑距离为 j
      翻译成代码
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      int distance(char *a, char *b, int i, int j)
      {
      if (j == 0) return i;
      else if (i == 0) return j;
      else if (a[i-1] == b[j-1])
      return edit_distance(a, b, i - 1, j - 1);
      else
      return Min3(edit_distance(a, b, i - 1, j) + 1,
      edit_distance(a, b, i, j - 1) + 1,
      edit_distance(a, b, i - 1, j - 1) + 1);
      }

动态规划

递归被人诟病的就是性能不行,时间复杂度是指数增长的,很多相同的子问题其实是经过了多次求解,用动态规划可以解决这类问题。 而此问题需用一个二维数组来记忆状态图,俗称打表。
翻译成代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int distance(char *a, char *b)
{
int lena = strlen(a);
int lenb = strlen(b);
int d[lena+1][lenb+1];
int i, j;
for (i = 0; i <= lena; i++)
d[i][0] = i;
for (j = 0; j <= lenb; j++)
d[0][j] = j;
for (i = 1; i <= lena; i++)
{
for (j = 1; j <= lenb; j++)
{
if (a[i-1] == b[j-1])
{
d[i][j] = d[i-1][j-1];
}
else
{
d[i][j] = min_of_three(d[i-1][j]+1, d[i][j-1]+1, d[i-1][j-1]+1);
}
}
}
return d[lena][lenb];
}

Libco 是 微信出品的 C/C++ 的协程库, 通过仅有的几个函数接口 co_create/co_resume/co_yield 再配合 co_poll,可以支持同步或者异步的写法

让我们用官方的 example_cond.cpp 来开启学习之旅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <queue>
#include "co_routine.h"
using namespace std;
struct stTask_t
{
int id;
};
struct stEnv_t
{
stCoCond_t* cond;
queue<stTask_t*> task_queue;
};
void* Producer(void* args)
{
co_enable_hook_sys();
stEnv_t* env = (stEnv_t*)args;
int id = 0;
while (true)
{
stTask_t* task = (stTask_t*)calloc(1, sizeof(stTask_t));
task->id = id++;
env->task_queue.push(task);
printf("%s:%d produce task %d\n", __func__, __LINE__, task->id);
co_cond_signal(env->cond);
poll(NULL, 0, 1000);
}
return NULL;
}
void* Consumer(void* args)
{
co_enable_hook_sys();
stEnv_t* env = (stEnv_t*)args;
while (true)
{
if (env->task_queue.empty())
{
co_cond_timedwait(env->cond, -1);
continue;
}
stTask_t* task = env->task_queue.front();
env->task_queue.pop();
printf("%s:%d consume task %d\n", __func__, __LINE__, task->id);
free(task);
}
return NULL;
}
int main()
{
stEnv_t* env = new stEnv_t;
env->cond = co_cond_alloc();

stCoRoutine_t* consumer_routine;
co_create(&consumer_routine, NULL, Consumer, env);
co_resume(consumer_routine);

stCoRoutine_t* producer_routine;
co_create(&producer_routine, NULL, Producer, env);
co_resume(producer_routine);

co_eventloop(co_get_epoll_ct(), NULL, NULL);
return 0;
}

通过以下命令编译就可以执行

1
2
3
4
5
6
git clone https://github.com/Tencent/libco
cd libco
make
// 需要-ldl -lpthread
g++ example_cond.cpp -o main -L./lib -lcolib -lpthread -ldl
./main

重点函数如下

  • co_create 是协程创建函数, 原型如下
    co 是协程控制块,attr 创建协程属性,一般用来设置共享栈模式和栈空间大小,默认为 NULL,routine 是协程的入口函数,arg 是函数的参数
1
2
int co_create(stCoRoutine_t** co, const stCoRoutineAttr_t* attr,
void* (*routine)(void*), void* arg);
  • co_resume

切换到指定的协程 co,操作系统对协程是无感知的,切换调度都是由协程自己来完成

  • co_eventloop

co_eventloop 是主协程的调度函数,函数的主要作用就是通过 epoll 负责各个协程的时间监控,如果有网络事件到了或者等待时间超时了,就切换到相应的协程处理。ctx 是库函数 co_get_epoll_ct(),pfn 是一个钩子函数,用户自定义,在函数 co_eventloop 的最后会执行,arg 是 pfn的参数

1
void co_eventloop(stCoEpoll_t *ctx, pfn_co_eventloop_t pfn, void *arg)
  • co_enable_hook_sys()

co_enable_hook_sys 函数是用来打开 libco 的钩子标示,进行系统 io 函数的时候才会调用到 libco 的函数而不是原系统函数

  • poll 相当于我们平时用到的 sleep 函数。
    如果用 sleep 作用到的是整个线程,通过 poll 来实现协程的挂起,协程环境下实际调用的函数是 co_poll,主要是完成回调函数的设置,超时事件的挂入然后把自己切出去。
1
int poll(struct pollfd fds[], nfds_t nfds, int timeout)
  • 涉及到同步的接口有
1
2
3
4
co_cond_alloc
co_cond_signal
co_cond_broadcast
co_cond_timedwait

本文介绍各种 C++ 的调试方法

  • valgrind
  • sanitizer
  • gdb

valgrind 可用来检测 C/C++ 代码是否内存泄漏。Valgrind 使用起来不需修改目标程序源码。

用一个明显有内存泄漏的代码来演示

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <malloc.h>

int main(void) {
int *p = NULL;
p = malloc(10 * sizeof(int));
free(p);
*p = 3;
return 0;
}

编译成二进制, 然后用 valgrind 检测

1
2
g++ -g -o bug main.c
valgrind --tool=memcheck --leak-check=full --log-file=memchk.log ./bug

valgrind 会做一次 全身检查, 如果有内存泄漏, 我们要关注的是 LeakSummary:

1
2
3
4
5
LeakSummary
definitely lost: 确定有内存泄漏,表示在程序退出时,该内存无法回收.
indirectly lost: 间接内存泄漏,比如结构体中定义的指针指向的内存无法回收;
possibly lost: 可能出现内存泄漏,比如程序退出时,没有指针指向一块内存的首地址了
still reachable: 程序没主动释放内存,在退出时候该内存仍能访问到,比如全局 new 的对象没 delete,操作系统会回收

再推荐另一个工具 sanitizer, 同样是上面的示例代码,

-fsanitize=address 编译, 然后执行二进制,会打印出错的地址空间。

1
2
gcc -fsanitize=address -g -o bug main.c
./bug

显示出错的地址空间

1
2
3
4
5
==8035== ERROR: AddressSanitizer: heap-use-after-free on address 0x60080000bfd0 at pc 0x4007e1 bp 0x7ffeb17cf780 sp 0x7ffeb17cf778
WRITE of size 4 at 0x60080000bfd0 thread T0
#0 0x4007e0 (/data1/mm64/levizheng/QQMail/study/test_debug/a+0x4007e0)
#1 0x7f6808337c04 (/usr/lib64/libc-2.17.so+0x21c04)
#2 0x4006b8 (/data1/mm64/levizheng/QQMail/study/test_debug/a+0x4006b8)

然后用命令指定二进制来查看地址空间, address2line -e bug, 输入出错的地址空间,比如 0x4007e0,
就可以看到他对应的代码了。

如果用 addr2line
addr2line -C -f -e 0xffc3b7


说回 GDB,大多数时候用的还是最多的。

用 GDB 调试一个程序

1
gdb <execute_binary>

如需传入启动参数,使用 set args,例如

1
set args -i config.file

开始运行则输入 run

当发生异常时,输入 bt, 就可以看到堆栈信息了。
f #num 可以指定 具体frame 查看信息,p arg 用来打印变量值。
下面例子中, 就是查看变量的。

1
2
p __str
p *this

再次展现完整的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Program received signal SIGSEGV, Segmentation fault.
std::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string (this=0x7fffffffdd40, __str=...) at /data1/gcc-4.8.2_build/x86_64-unknown-linux-gnu/libstdc++-v3/include/bits/basic_string.tcc:173
173 /data1/gcc-4.8.2_build/x86_64-unknown-linux-gnu/libstdc++-v3/include/bits/basic_string.tcc: No such file or directory.
(gdb) bt
#0 std::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string (this=0x7fffffffdd40, __str=...) at /data1/gcc-4.8.2_build/x86_64-unknown-linux-gnu/libstdc++-v3/include/bits/basic_string.tcc:173
#1 0x00000000006476d6 in GetBinLogSynShmPath (this=<optimized out>) at mmux/mmuxstatbf/mmuxstatbfconfig.h:95
#2 BinLogSyncMgr::InitShmData (this=0x1fbee80) at mmux/mmuxstatbf/binlogsyncmgr.cpp:112
#3 0x0000000000641b80 in GetMetaPtr (this=0x1fbee80) at mmux/mmuxstatbf/binlogsyncmgr.h:68
#4 MMUxStatBFServer::BeforeMasterRun (this=0x7fffffffe1d0, pvProcArgs=<optimized out>) at mmux/mmuxstatbf/mmuxstatbfserver.h:75
(gdb) p __str
$1 = (const std::basic_string<char, std::char_traits<char>, std::allocator<char> > &) @0x20: <error reading variable>
(gdb) f 1
#1 0x00000000006476d6 in GetBinLogSynShmPath (this=<optimized out>) at mmux/mmuxstatbf/mmuxstatbfconfig.h:95
95 mmux/mmuxstatbf/mmuxstatbfconfig.h: No such file or directory.
(gdb) p *this
(gdb) f 2
#2 BinLogSyncMgr::InitShmData (this=0x1fbee80) at mmux/mmuxstatbf/binlogsyncmgr.cpp:112
112 mmux/mmuxstatbf/binlogsyncmgr.cpp: No such file or directory.
(gdb) info locals
is_new = 0
__func__ = "InitShmData"
(gdb) f 1

BOOST 库的很多标准已经被纳入 C++11, 这也说明了 BOOST 库设计得到标准的重视。工作中需要用到 BOOST 的特性来加速开发。以下举例常用到的 utility

  • BOOST_SCOPE_EXIT

BOOST_SCOPE_EXIT()里面可以传入多个参数, 其作用相当于回调函数, 在作用域结束之后程序会自动调用 BOOST_SCOPE_EXIT到BOOST_SCOPE_EXIT_END之间的代码。这个在异常捕捉,或者日志打印的时候会非常有用,节约代码。

举个例子

1
2
3
4
5
6
7
8
9
10
11
void func (int ans) 
{
bool flag = false;
BOOST_SCOPE_EXIT(&flag) {
if (!flag)
std::cout << "false" <<std::endl;
else
std::cout << "true" << std::endl;
} BOOST_SCOPE_EXIT_END
flag = ((ans == 1)? true:false);
}
  • boost::split

文本处理,切割字符串的利器

1
2
3
4
5
6
7
8
9
10
11
#include <boost/algorithm/string/split.hpp>
int main( int argc, char** argv )
{
string source = "how to live in the world!";
vector<string> destination;
boost::split( destination, source, boost::is_any_of( " ,!" ), boost::token_compress_on );
vector<string>::iterator it ;
for (it= destination.begin(); it != destination.end(); ++ it)
cout << *it << endl;
return 0;
}
  • boost::regex

正则匹配

1
boost::regex_match(str, boost::regex("^1[3|4|5|8][0-9]{9}$")) 
  • BOOST 读写锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include "boost/thread/shared_mutex.hpp"
boost::shared_mutex m_rwlock;
boost::shared_lock<boost::shared_mutex> guard(m_rwlock);读锁
boost::unique_lock<boost::shared_mutex> guard(m_rwlock);写锁

#include "boost/thread/locks.hpp"
#include "boost/thread/shared_mutex.hpp"
using namespace std;

//线程安全
class NameMap{

private:
boost::shared_mutex m_rwlock;
map<uint32_t, string> m_map;
public:
bool find(uint32_t uin, string & nickname) {
boost::shared_lock<boost::shared_mutex> guard(m_rwlock);
auto it = m_map.find(uin);
if(it == m_map.end()){
return false;
}
nickname = it->second;
return true;
}

void insert(uint32_t uin, const string & nickname) {
boost::unique_lock<boost::shared_mutex> guard(m_rwlock);
m_map[uin]=nickname;
}

size_t size(){
boost::shared_lock<boost::shared_mutex> guard(m_rwlock);
return m_map.size();
}
};

时光荏苒,这一次的博客更新,用了一年。 故事每天都在发生,不同时段的感悟也不尽相同。

过去四年接触的是小公司式的后端全栈工作。庆幸认识到人生中导师和伙伴。我从一个满腔热情,但啥都不懂的小毛孩, 到后面成长为独挡一面, 负责全公司的运维架构, 以及传承培育新人的老家伙。依然清晰记得每次凌晨无缝迁移服务器的喜悦, 以及将新技术运用到实践当中, 节约成本带来的精神富足。不得不说这是一个技术人最纯粹的感动。也有过战友离开的彷徨, 面对人事压力的焦虑。 点点滴滴占据了令人感动的回忆, 倏忽之间一晃而过。

新的征途几乎是一次完全的洗礼,进入微信, 从零开始接触 C++ 的后端开发, 以往熟悉的知识体系和经验都要翻开新的一页。开始硬磕指针和内存。

半年的开发时间

  • 写了一个通用的三机容灾布隆过滤器,支撑小程序的统计
  • 改造几个组里的基础组件
  • 承担搜索的排序模块
  • 负载客服 IM 系统的设计与开发

我也在慢慢接受和适应凶残的加班。 产品迭代速度快到有些窒息,这样的大背景下,我还在坚持记录文档的习惯,应该是微信的 “奇葩” 了, 我怕不靠谱的记忆力给自己,后人留坑。

生活的难题也一道道出现,想来也没什么好的解决方法,锻炼身心是我能把控当下的选择。偶尔我会在地铁读读古人的诗词,以前没觉得有什么, 如今觉得古人的诗词实在是豁达开朗。

说了挺多废话, 生活还在继续。相信认真生活的人运气不会太差, 愿我的朋友们也都有好运相伴。

0%