Ethan's Blog


  • Home

  • Archives

  • Tags

  • Search

概率统计

Posted on 2021-01-21

如何利用朴素贝叶斯过滤垃圾短信

垃圾短信和骚扰电话,我想每个人都收到过吧?买房、贷款、投资理财、开发票,各种垃圾短信和骚扰电话,不胜其扰。如果你是一名手机应用开发工程师,让你实现一个简单的垃圾短信过滤功能以及骚扰电话拦截功能,该用什么样的数据结构和算法实现呢?

算法解析

实际上,解决这个问题并不会涉及很高深的算法。今天,我就带你一块看下,如何利用简单的数据结构和算法,解决这种看似非常复杂的问题。

基于黑名单的过滤器

我们可以维护一个骚扰电话号码和垃圾短信发送号码的黑名单。如果黑名单中的电话号码不多的话,我们可以使用散列表、二叉树等动态数据结构来存储,对内存的消耗并不会很大。如果我们把每个号码看作一个字符串,并且假设平均长度是 16 个字节,那存储 50 万个电话号码,大约需要 10MB 的内存空间。即便是对于手机这样的内存有限的设备来说,这点内存的消耗也是可以接受的。

但是,如果黑名单中的电话号码很多呢?比如有 500 万个。这个时候,如果再用散列表存储,就需要大约 100MB 的存储空间。为了实现一个拦截功能,耗费用户如此多的手机内存,这显然有点儿不合理。如果我们要存储 500 万个手机号码,我们把位图大小设置为 10 倍数据大小,也就是 5000 万,那也只需要使用 5000 万个二进制位(5000 万 bits),换算成字节,也就是不到 7MB 的存储空间。比起散列表的解决方案,内存的消耗减少了很多。

我们还可以把黑名单存储在服务器端上,把过滤和拦截的核心工作,交给服务器端来做。手机端只负责将要检查的号码发送给服务器端,服务器端通过查黑名单,判断这个号码是否应该被拦截,并将结果返回给手机端。用这个解决思路完全不需要占用手机内存。不过,有利就有弊。我们知道,网络通信是比较慢的,所以,网络延迟就会导致处理速度降低。而且,这个方案还有个硬性要求,那就是只有在联网的情况下,才能正常工作。

Read more »

位图

Posted on 2021-01-20

如何实现网页爬虫中的 URL 去重功能

网页爬虫是搜索引擎中的非常重要的系统,负责爬取几十亿、上百亿的网页。爬虫的工作原理是,通过解析已经爬取页面中的网页链接,然后再爬取这些链接对应的网页。而同一个网页链接有可能被包含在多个页面中,这就会导致爬虫在爬取的过程中,重复爬取相同的网页。如果你是一名负责爬虫的工程师,你会如何避免这些重复的爬取呢?

最容易想到的方法就是,我们记录已经爬取的网页链接(也就是 URL),在爬取一个新的网页之前,我们拿它的链接,在已经爬取的网页链接列表中搜索。如果存在,那就说明这个网页已经被爬取过了;如果不存在,那就说明这个网页还没有被爬取过,可以继续去爬取。等爬取到这个网页之后,我们将这个网页的链接添加到已经爬取的网页链接列表了。

算法解析

这个问题要处理的对象是网页链接,也就是 URL,需要支持的操作有两个,添加一个 URL 和查询一个 URL。除了这两个功能性的要求之外,在非功能性方面,我们还要求这两个操作的执行效率要尽可能高。除此之外,因为我们处理的是上亿的网页链接,内存消耗会非常大,所以在存储效率上,我们要尽可能地高效。

显然,散列表、红黑树、跳表这些动态数据结构,都能支持快速地插入、查找数据,但是在内存消耗方面,是否可以接受呢?我们拿散列表来举例,假设一个 URL 的平均长度是 64 字节,那单纯存储这 10 亿个 URL,需要大约 60GB 的内存空间。因为散列表必须维持较小的装载因子,才能保证不会出现过多的散列冲突,导致操作的性能下降。而且,用链表法解决冲突的散列表,还会存储链表指针。所以,如果将这 10 亿个 URL 构建成散列表,那需要的内存空间会远大于 60GB,有可能会超过 100GB。

Read more »

最短路径

Posted on 2021-01-19

地图软件是如何计算出最优出行路径的

像 Google 地图、百度地图、高德地图这样的地图软件,我想你应该经常使用吧?如果想从家开车到公司,你只需要输入起始、结束地址,地图就会给你规划一条最优出行路线。这里的最优,有很多种定义,比如最短路线、最少用时路线、最少红绿灯路线等等。作为一名软件开发工程师,你是否思考过,地图软件的最优路线是如何计算出来的吗?底层依赖了什么算法呢?

算法解析

解决软件开发中的实际问题,最重要的一点就是建模,也就是将复杂的场景抽象成具体的数据结构。我们把每个岔路口看作一个顶点,岔路口与岔路口之间的路看作一条边,路的长度就是边的权重。如果路是单行道,我们就在两个顶点之间画一条有向边;如果路是双行道,我们就在两个顶点之间画两条方向不同的边。这样,整个地图就被抽象成一个有向有权图。于是,我们要求解的问题就转化为,在一个有向有权图中,求两个顶点间的最短路径:

Read more »

拓扑排序

Posted on 2021-01-19

如何确定代码源文件的编译依赖关系

我们知道,一个完整的项目往往会包含很多代码源文件。编译器在编译整个项目的时候,需要按照依赖关系,依次编译每个源文件。比如,A.cpp 依赖 B.cpp,那在编译的时候,编译器需要先编译 B.cpp,才能编译 A.cpp。编译器通过分析源文件或者程序员事先写好的编译配置文件(比如 Makefile 文件),来获取这种局部的依赖关系。那编译器又该如何通过源文件两两之间的局部依赖关系,确定一个全局的编译顺序呢:

算法解析

我们在穿衣服的时候都有一定的顺序,我们可以把这种顺序想成,衣服与衣服之间有一定的依赖关系。比如说,你必须先穿袜子才能穿鞋,先穿内裤才能穿秋裤。假设我们现在有八件衣服要穿,它们之间的两两依赖关系我们已经很清楚了,那如何安排一个穿衣序列,能够满足所有的两两之间的依赖关系?这就是个拓扑排序(Topological Sorting)问题。从这个例子中,你应该能想到,在很多时候,拓扑排序的序列并不是唯一的:

Read more »

动态规划

Posted on 2021-01-17

动态规划学习路线

动态规划(Dynamic Programming)比较适合用来求解最优问题,比如求最大值、最小值等等。它可以非常显著地降低时间复杂度,提高代码的执行效率。不过,它也是出了名的难学。它的主要学习难点跟递归类似,那就是,求解问题的过程不太符合人类常规的思维方式。对于新手来说,要想入门确实不容易。不过,等你掌握了之后,你会发现,实际上并没有想象中那么难。

0-1 背包问题

对于一组不同重量、不可分割的物品,我们需要选择一些装入背包,在满足背包最大重量限制的前提下,背包中物品总重量的最大值是多少呢。关于这个问题,回溯的解决方法就是穷举搜索所有可能的装法,然后找出满足条件的最大值。不过,回溯算法的复杂度比较高,是指数级别的。那有没有什么规律,可以有效降低时间复杂度呢:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 回溯算法实现。注意:我把输入的变量都定义成了成员变量
private int maxW = Integer.MIN_VALUE; // 结果放到 maxW 中
private int[] weight = {2, 2, 4, 6, 3}; // 物品重量
private int n = 5; // 物品个数
private int w = 9; // 背包承受的最大重量
public void f(int i, int cw) {
// 调用 f(0, 0)
if (cw == w || i == n) {
// cw==w 表示装满了,i==n 表示物品都考察完了
if (cw > maxW) {
maxW = cw;
}
return;
}
f(i+1, cw); // 选择不装第 i 个物品
if (cw + weight[i] <= w) {
f(i+1, cw + weight[i]); // 选择装第 i 个物品
}
}
Read more »
1…262728…55
necusjz

necusjz

274 posts
16 tags
© 2016 - 2025 necusjz
Powered by Hexo
Theme - NexT.Mist