布隆过滤器

什么是 BloomFilter

布隆过滤器(英语:Bloom Filter)是 1970 年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。主要用于判断一个元素是否在一个集合中。

通常我们会遇到很多要判断一个元素是否在某个集合中的业务场景,一般想到的是将集合中所有元素保存起来,然后通过比较确定。链表、树、散列表(又叫哈希表,Hash table)等等数据结构都是这种思路。但是随着集合中元素的增加,我们需要的存储空间也会呈现线性增长,最终达到瓶颈。同时检索速度也越来越慢,上述三种结构的检索时间复杂度分别为$O(n)$,$O(logn)$,$O(1)$。

这个时候,布隆过滤器(Bloom Filter)就应运而生。

布隆过滤器原理

了解布隆过滤器原理之前,先回顾下 Hash 函数原理。

哈希函数

哈希函数的概念是:将任意大小的输入数据转换成特定大小的输出数据的函数,转换后的数据称为哈希值或哈希编码,也叫散列值。下面是一幅示意图:

所有散列函数都有如下基本特性:

  • 如果两个散列值是不相同的(根据同一函数),那么这两个散列值的原始输入也是不相同的。这个特性是散列函数具有确定性的结果,具有这种性质的散列函数称为单向散列函数
  • 散列函数的输入和输出不是唯一对应关系的,如果两个散列值相同,两个输入值很可能是相同的,但也可能不同,这种情况称为“散列碰撞(collision)”。

但是用 hash表存储大数据量时,空间效率还是很低,当只有一个 hash 函数时,还很容易发生哈希碰撞。

布隆过滤器数据结构

BloomFilter 是由一个固定大小的二进制向量或者位图(bitmap)和一系列映射函数组成的。

在初始状态时,对于长度为 m 的位数组,它的所有位都被置为0,如下图所示:

当有变量被加入集合时,通过 K 个映射函数将这个变量映射成位图中的 K 个点,把它们置为 1(假定有两个变量都通过 3 个映射函数)。

查询某个变量的时候我们只要看看这些点是不是都是 1 就可以大概率知道集合中有没有它了

  • 如果这些点有任何一个 0,则被查询变量一定不在;
  • 如果都是 1,则被查询变量很可能存在

为什么说是可能存在,而不是一定存在呢?那是因为映射函数本身就是散列函数,散列函数是会有碰撞的。

误判率

布隆过滤器的误判是指多个输入经过哈希之后在相同的bit位置1了,这样就无法判断究竟是哪个输入产生的,因此误判的根源在于相同的 bit 位被多次映射且置 1。

这种情况也造成了布隆过滤器的删除问题,因为布隆过滤器的每一个 bit 并不是独占的,很有可能多个元素共享了某一位。如果我们直接删除这一位的话,会影响其他的元素。(比如上图中的第 3 位)

特性
  • 一个元素如果判断结果为存在的时候元素不一定存在,但是判断结果为不存在的时候则一定不存在
  • 布隆过滤器可以添加元素,但是不能删除元素。因为删掉元素会导致误判率增加。
添加与查询元素步骤
添加元素
  1. 将要添加的元素给 k 个哈希函数
  2. 得到对应于位数组上的 k 个位置
  3. 将这k个位置设为 1
查询元素
  1. 将要查询的元素给k个哈希函数
  2. 得到对应于位数组上的k个位置
  3. 如果k个位置有一个为 0,则肯定不在集合中
  4. 如果k个位置全部为 1,则可能在集合中
优点

相比于其它的数据结构,布隆过滤器在空间和时间方面都有巨大的优势。布隆过滤器存储空间和插入/查询时间都是常数 $O(K)$,另外,散列函数相互之间没有关系,方便由硬件并行实现。布隆过滤器不需要存储元素本身,在某些对保密要求非常严格的场合有优势。

布隆过滤器可以表示全集,其它任何数据结构都不能;

缺点

但是布隆过滤器的缺点和优点一样明显。误算率是其中之一。随着存入的元素数量增加,误算率随之增加。但是如果元素数量太少,则使用散列表足矣。

另外,一般情况下不能从布隆过滤器中删除元素。我们很容易想到把位数组变成整数数组,每插入一个元素相应的计数器加 1, 这样删除元素时将计数器减掉就可以了。然而要保证安全地删除元素并非如此简单。首先我们必须保证删除的元素的确在布隆过滤器里面。这一点单凭这个过滤器是无法保证的。另外计数器回绕也会造成问题。

在降低误算率方面,有不少工作,使得出现了很多布隆过滤器的变种。

布隆过滤器使用场景和实例

在程序的世界中,布隆过滤器是程序员的一把利器,利用它可以快速地解决项目中一些比较棘手的问题。

如网页 URL 去重、垃圾邮件识别、大集合中重复元素的判断和缓存穿透等问题。

布隆过滤器的典型应用有:

  • 数据库防止穿库。 Google Bigtable,HBase 和 Cassandra 以及 Postgresql 使用BloomFilter来减少不存在的行或列的磁盘查找。避免代价高昂的磁盘查找会大大提高数据库查询操作的性能。
  • 业务场景中判断用户是否阅读过某视频或文章,比如抖音或头条,当然会导致一定的误判,但不会让用户看到重复的内容。
  • 缓存宕机、缓存击穿场景,一般判断用户是否在缓存中,如果在则直接返回结果,不在则查询db,如果来一波冷数据,会导致缓存大量击穿,造成雪崩效应,这时候可以用布隆过滤器当缓存的索引,只有在布隆过滤器中,才去查询缓存,如果没查询到,则穿透到db。如果不在布隆器中,则直接返回。
  • WEB拦截器,如果相同请求则拦截,防止重复被攻击。用户第一次请求,将请求参数放入布隆过滤器中,当第二次请求时,先判断请求参数是否被布隆过滤器命中。可以提高缓存命中率。Squid 网页代理缓存服务器在 cache digests 中就使用了布隆过滤器。Google Chrome浏览器使用了布隆过滤器加速安全浏览服务
  • Venti 文档存储系统也采用布隆过滤器来检测先前存储的数据。
  • SPIN 模型检测器也使用布隆过滤器在大规模验证问题时跟踪可达状态空间。

Coding~

知道了布隆过滤去的原理和使用场景,我们可以自己实现一个简单的布隆过滤器

自定义的 BloomFilter
public class MyBloomFilter {

    /**
     * 一个长度为10 亿的比特位
     */
    private static final int DEFAULT_SIZE = 256 << 22;

    /**
     * 为了降低错误率,使用加法hash算法,所以定义一个8个元素的质数数组
     */
    private static final int[] seeds = {3, 5, 7, 11, 13, 31, 37, 61};

    /**
     * 相当于构建 8 个不同的hash算法
     */
    private static HashFunction[] functions = new HashFunction[seeds.length];

    /**
     * 初始化布隆过滤器的 bitmap
     */
    private static BitSet bitset = new BitSet(DEFAULT_SIZE);

    /**
     * 添加数据
     *
     * @param value 需要加入的值
     */
    public static void add(String value) {
        if (value != null) {
            for (HashFunction f : functions) {
                //计算 hash 值并修改 bitmap 中相应位置为 true
                bitset.set(f.hash(value), true);
            }
        }
    }

    /**
     * 判断相应元素是否存在
     * @param value 需要判断的元素
     * @return 结果
     */
    public static boolean contains(String value) {
        if (value == null) {
            return false;
        }
        boolean ret = true;
        for (HashFunction f : functions) {
            ret = bitset.get(f.hash(value));
            //一个 hash 函数返回 false 则跳出循环
            if (!ret) {
                break;
            }
        }
        return ret;
    }

    /**
     * 模拟用户是不是会员,或用户在不在线。。。
     */
    public static void main(String[] args) {

        for (int i = 0; i < seeds.length; i++) {
            functions[i] = new HashFunction(DEFAULT_SIZE, seeds[i]);
        }

        // 添加1亿数据
        for (int i = 0; i < 100000000; i++) {
            add(String.valueOf(i));
        }
        String id = "123456789";
        add(id);

        System.out.println(contains(id));   // true
        System.out.println("" + contains("234567890"));  //false
    }
}

class HashFunction {

    private int size;
    private int seed;

    public HashFunction(int size, int seed) {
        this.size = size;
        this.seed = seed;
    }

    public int hash(String value) {
        int result = 0;
        int len = value.length();
        for (int i = 0; i < len; i++) {
            result = seed * result + value.charAt(i);
        }
        int r = (size - 1) & result;
        return (size - 1) & result;
    }
}

What?我们写的这些早有大牛帮我们实现,还造轮子,真是浪费时间,No,No,No,我们学习过程中是可以造轮子的,造轮子本身就是我们自己对设计和实现的具体落地过程,不仅能提高我们的编程能力,在造轮子的过程中肯定会遇到很多我们没有思考过的问题,成长看的见~~

实际项目使用的时候,领导和我说项目一定要稳定运行,没自信的我放弃了自己的轮子。

Guava 中的 BloomFilter
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>23.0</version>
</dependency>
public class GuavaBloomFilterDemo {

    public static void main(String[] args) {
        //后边两个参数:预计包含的数据量,和允许的误差值
        BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 100000, 0.01);
        for (int i = 0; i < 100000; i++) {
            bloomFilter.put(i);
        }
        System.out.println(bloomFilter.mightContain(1));
        System.out.println(bloomFilter.mightContain(2));
        System.out.println(bloomFilter.mightContain(3));
        System.out.println(bloomFilter.mightContain(100001));

        //bloomFilter.writeTo();
    }
}

分布式环境中,布隆过滤器肯定还需要考虑是可以共享的资源,这时候我们会想到 Redis,是的,Redis 也实现了布隆过滤器。

当然我们也可以把布隆过滤器通过 bloomFilter.writeTo() 写入一个文件,放入OSS、S3这类对象存储中。

Redis 中的 BloomFilter

Redis 提供的 bitMap 可以实现布隆过滤器,但是需要自己设计映射函数和一些细节,这和我们自定义没啥区别。

Redis 官方提供的布隆过滤器到了 Redis 4.0 提供了插件功能之后才正式登场。布隆过滤器作为一个插件加载到 Redis Server 中,给 Redis 提供了强大的布隆去重功能。

在已安装 Redis 的前提下,安装 RedisBloom,有两种方式

直接编译进行安装

git clone https://github.com/RedisBloom/RedisBloom.git

cd RedisBloom

make     #编译 会生成一个rebloom.so文件

redis-server --loadmodule /path/to/rebloom.so   #运行redis时加载布隆过滤器模块

redis-cli    # 启动连接容器中的 redis 客户端验证

使用Docker进行安装

docker pull redislabs/rebloom:latest # 拉取镜像

docker run -p 6379:6379 --name redis-redisbloom redislabs/rebloom:latest #运行容器

docker exec -it redis-redisbloom bash

redis-cli     

使用

布隆过滤器基本指令:

  • bf.add 添加元素到布隆过滤器
  • bf.exists 判断元素是否在布隆过滤器
  • bf.madd 添加多个元素到布隆过滤器,bf.add 只能添加一个
  • bf.mexists 判断多个元素是否在布隆过滤器
127.0.0.1:6379> bf.add user Tom
(integer) 1
127.0.0.1:6379> bf.add user John
(integer) 1
127.0.0.1:6379> bf.exists user Tom
(integer) 1
127.0.0.1:6379> bf.exists user Linda
(integer) 0
127.0.0.1:6379> bf.madd user Barry Jerry Mars
1) (integer) 1
2) (integer) 1
3) (integer) 1
127.0.0.1:6379> bf.mexists user Barry Linda
1) (integer) 1
2) (integer) 0

我们只有这几个参数,肯定不会有误判,当元素逐渐增多时,就会有一定的误判了,这里就不做这个实验了。

上面使用的布隆过滤器只是默认参数的布隆过滤器,它在我们第一次 add 的时候自动创建。

Redis 还提供了自定义参数的布隆过滤器,bf.reserve 过滤器名 error_rate initial_size

  • error_rate:允许布隆过滤器的错误率,这个值越低过滤器的位数组的大小越大,占用空间也就越大
  • initial_size:布隆过滤器可以储存的元素个数,当实际存储的元素个数超过这个值之后,过滤器的准确率会下降

但是这个操作需要在 add 之前显式创建。如果对应的 key 已经存在,bf.reserve 会报错

127.0.0.1:6379> bf.reserve user 0.01 100
(error) ERR item exists
127.0.0.1:6379> bf.reserve topic 0.01 1000
OK

我是一名 Javaer,肯定还要用 Java 来实现的,Java 的 Redis 客户端比较多,有些还没有提供指令扩展机制,笔者已知的 Redisson 和 lettuce 是可以使用布隆过滤器的,我们这里用 Redisson

public class RedissonBloomFilterDemo {

    public static void main(String[] args) {

        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        RedissonClient redisson = Redisson.create(config);

        RBloomFilter<String> bloomFilter = redisson.getBloomFilter("user");
        // 初始化布隆过滤器,预计统计元素数量为55000000,期望误差率为0.03
        bloomFilter.tryInit(55000000L, 0.03);
        bloomFilter.add("Tom");
        bloomFilter.add("Jack");
        System.out.println(bloomFilter.count());   //2
        System.out.println(bloomFilter.contains("Tom"));  //true
        System.out.println(bloomFilter.contains("Linda"));  //false
    }
}

扩展

为了解决布隆过滤器不能删除元素的问题,布谷鸟过滤器横空出世。论文《Cuckoo Filter:Better Than Bloom》作者将布谷鸟过滤器和布隆过滤器进行了深入的对比。相比布谷鸟过滤器而言布隆过滤器有以下不足:查询性能弱、空间利用效率低、不支持反向操作(删除)以及不支持计数。

由于使用较少,暂不深入。

参考链接


布隆过滤器,这一篇给你讲的明明白白

贝塞尔曲线

贝赛尔曲线的前世今生

贝塞尔曲线,这个命名规则一眼看上去大概是一个叫贝塞尔的数学家发明的。但,贝塞尔曲线依据的最原始的数学公式,是在1912年在数学界广为人知的伯恩斯坦多项式。简单理解,伯恩斯坦多项式可以用来证明,在[ a, b ] 区间上所有的连续函数都可以用多项式来逼近,并且收敛性很强,也就是一致收敛。再简单点,就是一个连续函数,你可以将它写成若干个伯恩斯坦多项式相加的形式,并且,随着 n→∞,这个多项式将一致收敛到原函数,这个就是伯恩斯坦斯的逼近性质。

时光荏苒岁月如梭,镜头切换到了1959年。当时就职于雪铁龙的法国数学家 Paul de Casteljau 开始对伯恩斯坦多项式进行了图形化的尝试,并且提供了一种数值稳定的德卡斯特里奥(de Casteljau) 算法。(多数理论公式是建立在大量且系统的数学建模基础之上研究的规律性成果)根据这个算法,就可以实现 通过很少的控制点,去生成复杂的平滑曲线,也就是贝塞尔曲线

但贝塞尔曲线的声名大噪,不得不提到1962年就职于雷诺的法国工程师皮埃尔·贝塞尔(Pierre Bézier),他使用这种方法来辅助汽车的车体工业设计(最早计算机的诞生则是为了帮助美国海军绘制弹道图),并且广泛宣传(典型的理论联系实际并获得成功的示例),因此大家称为贝塞尔曲线 。

贝赛尔曲线的数学理论

既然贝赛尔曲线的本质是通过数学计算公式去绘制平滑的曲线,那就可以通过数学工具进行实际求证以及解释说明。当然对其进行数学求证就没必要了,因为这些伟大的数学家们已经做过了,这里只是解释说明:

  • 步骤一:在平面内选3个不同线的点并且依次用线段连接。

    3点连线
    3点连线
  • 步骤二:在AB和BC线段上找出点D和点E,使得 AD/AB = BE/BC

    AD/AB = BE/BC
    AD/AB = BE/BC
  • 步骤三:连接DE,在DE上寻找点F,F点需要满足:DF/DE = AD/AB = BE/BC

    DF/DE = AD/AB = BE/BC
    DF/DE = AD/AB = BE/BC
  • 步骤四:最最重要的!根据DE线段和计算公式找出所有的F点,记住是所有的F点,然后将其这些点连接起来。那,连接规则是什么?以上图为例,第一个连接点是A-F,第二连接点是A-F1(这个F1必须满足DF1/DE = AD/AB = BE/BC)以此类推,直到最后连接上C点,下面上一个动图加深理解:

    贝塞尔曲线
    贝塞尔曲线

    可能有些朋友还是不理解,那么这个GIF我截下其中的一张图说明,如下图:

    示例说明
    示例说明

    动图里的P0、P1、P2分别代表的是上图的:P0 == A;P1 == B;P2 == C。那么这个黑色点,代表的就是F点,绿色线段的2个端点(P0-P1线段上的绿色点,代表是就是D点,P0-P2线段上的绿色点,代表是就是E点)。线段上面点的获取,必须要满足等比关系。

关于贝赛尔曲线的基本数学理论大概就是上面的内容。两个线段根据等比关系找点的贝塞尔曲线,一般也称为二阶贝塞尔曲线。

贝赛尔曲线的N阶拓展(三阶贝塞尔与N阶贝塞尔曲线)

刚才说到,上面的贝赛尔曲线一般称为二阶贝塞尔曲线,既然是二阶贝塞尔曲线,那肯定有三阶贝塞尔曲线、四阶贝赛尔曲线等等。其实三阶贝塞尔与四阶贝赛尔曲线以及N阶贝赛尔曲线曲线的规则都是一样的,都是先在线段上找点,这个点必须要满足等比关系,然后依次连接,下面是三阶贝赛尔曲线的解释说明:

  • 步骤一:三阶贝赛尔曲线,简单理解就是在平面内选4个不同线的点并且依次用线段连接(也就是三条线)。如下所示

    四点三线
    四点三线
  • 步骤二:同二阶贝塞尔曲线一样首先需要在线段上找对应的点(E、F、G),对应的点必须要符合等比的计算规则,计算规则如下:AE/AB = BF/BC = CG/CD;找到对应的点以后接着依次链接EF、FG;接着在EF、FG线段上面继续找点H、I,对应的点依旧要符合等比的计算规则,也就是 EH/EF = FI/FG;最后连接H、I线段,在HI线段上面继续找点J、点J的计算规则需要符合:EH/EF = FI/FG = HJ/HI

    三阶贝赛尔曲线找点
    三阶贝赛尔曲线找点
  • 步骤三:重复步骤二的动作,找到所有的J点,依次将J点连接起来,这样最终完成了三阶贝赛尔曲线。

    J点依次连线
    J点依次连线

整一个三阶贝赛尔曲线的动作加起来就是下面的一张动图:

三阶贝塞尔
三阶贝塞尔

那么四阶贝赛尔曲线的实现步骤也是一样的,平面上先选取5个点(5点4线)、依次选点(满足等比关系)、依次连接、根据计算规则找到所有的点(逐个连接)。。。。。。

四阶贝赛尔曲线
四阶贝赛尔曲线

貌似都是从二阶贝塞尔曲线说起的,那么一阶贝赛尔又是怎么样的?一阶贝赛尔如图:

一阶贝赛尔
一阶贝赛尔

可以看到一阶贝赛尔是一条直线!

因此,N阶贝赛尔不仅可以画平滑的曲线也可以画直线,因此自定义控件画直线又多了一种可选择的方式,但是一般用贝赛尔主要是画曲线,这里只是提供了一种别的解决思路;另外,在Android属性动画,系统为我们提供了一个PathInterpolator插值器。这个PathInterpolator里面就有贝塞尔曲线的身影。有兴趣的小伙伴也可以去了解一下。

贝赛尔曲线的拟合

给定一段曲线,如何用贝塞尔曲线去拟合? 一般可以把曲线拆分成若干离散点的集合,然后要求拟合的曲线通过这些离散的数据点。

现在推导一下Bezier曲线控制点的计算过程。

曲线公式

曲 线 :$ C(u) = \displaystyle\sum_{i=0}^nB_{n,i}(u)P_i $

基 函 数 : $ B_{n,i} = \frac{ n ! }{ i !  (n−i)! } u^i ( 1 − u )^ {n − i} $

这里求解控制点,即C为已知信息,求解式中的P。

计算3次Bezier曲线控制点

曲线多项式:

$ C(u) = \displaystyle\sum_{i=0}^3 B_{3,i}(u)P_i = ( 1 − u )^3 P_0 + 3 ( 1 − u )^2 u P_1 + 3 ( 1 − u ) u^2 P_2 + u^3 P_3 , 0 ≤ u ≤ 1 $

写成矩阵方式:

$ C = B∗P $

式中:

$ B = \begin{bmatrix} 1 & 0 & 0 & 0 \\\\\frac{8}{27} & \frac{4}{9} & \frac{2}{9} & \frac{1} {27} \\\\ \frac{1} {27} & \frac{2}{9} & \frac{4}{9} & \frac{8}{27} \\\\ 0 & 0 & 0 & 1 \end{bmatrix}$

$ C = \begin{bmatrix} P_0 \\\\ P_1 \\\\P_2 \\\\P_3 \end{bmatrix}$ 

则得到;

$ B^{−1} C = B^{−1} B ∗ P $

$ P = B^{ − 1} C $

即可计算得到相应的样条曲线控制点。

Python验证

取点位 $ \begin{bmatrix} C_0(0,0) & C_1(0,2) & C_2(2,2) & C_3(2,0) \end{bmatrix}$

计算控制点P后,画出如下Bezier曲线:

黑色点为原始数据点;
红色点为计算得到的控制点;
蓝色曲线为由原始数据点直接拟合的Bezier曲线;
橘黄色为由控制点拟合的Bezier曲线;

参考链接


保持函数依赖的分解

大部分是对一个关系模式分解成两个模式的考察,分解为三个以上模式时无损分解和保持依赖的判断比较复杂,考的可能性不大,因此我们只对“一个关系模式分解 成两个模式”这种类型的题的相关判断做一个总结。

以下的论述都基 于这样一个前提:
R是具有函数依赖集F的关系模式,(R1 ,R2)是R的一个分解。

首先我们给出一 个看似无关却非常重要的概念:属性集的闭包 。
令α为一属性集。我们称在函数依赖集F下由α函数确定的所有属性的集合为F下α的闭包,记为α+ 。
下面给出一个计算α+的算法,该算法的输入是函数依赖集F和属性集α,输出存储在变量result中。
算法一:
result:=α;
while(result发生变化)do
    for each 函数依赖β→γ in F do
    begin
        if β⊆result then result:=result∪γ;
    end

属性集闭包的计 算有以下两个常用用途:
·判断α是否为超码,通过计算α+(α在F下的闭包),看α+ 是否包含了R中的所有属性。若是,则α为R的超码。
·通过检验是否β⊆α+,来验证函数依赖是否成立。也就是说,用属性闭包计算α+,看它是否包含β。

看一个例子 吧,2005年11月系分上午37题:

● 给定关系R(A1,A2,A3,A4)上的函数依赖集F={A1→A2,A3→A2,A2→A3,A2→A4},R的候选关键字为________。
(37)A. A1  B. A1A3  C. A1A3A4  D. A1A2A3

首先我们按照上 面的算法计算A1+ 。
result=A1,
由于A1→A2,A1⊆result,所以result=result∪A2=A1A2
由于A2→A3,A2⊆result,所以result=result∪A3=A1A2A3
由于A2→A4,A2⊆result,所以result=result∪A3=A1A2A3A4
由于A3→A2,A3⊆result,所以result=result∪A2=A1A2A3A4

通过计算我们看 到,A1+ =result={A1A2A3A4},所以A1是R的超码,理所当然是R的候选关键字。此题选A 。

好了,有了前面的铺垫,我们进入正题。

 

无损分解的判断 。
如果R1∩R2是R1或R2的超码,则R上的分解(R1,R2)是无损分解。这是一个充分条件,当所有的约束都是函数依赖时它才是必要条件(例如多值依赖 就是一种非函数依赖的约束),不过这已经足够了。

保持依赖的判断 。
如果F上的每一个函数依赖都在其分解后的某一个关系上成立,则这个分解是保持依赖的(这是一个充分条件)。
如果上述判断失败,并不能断言分解不是保持依赖的,还要使用下面的通用方法来做进一步判断。
该方法的表述如下:
算法二:
对F上的每一个α→β使用下面的过程:
result:=α;
while(result发生变化)do
    for each 分解后的Ri
        t=(result∩Ri)+ ∩Ri
        result=result∪t

这里的属性闭包是在函数依赖集F下计算出来的。如果result中包含了β的所有属性,则函数依赖α→β。分解是保持依赖的当且仅当上述过程中F的所有依 赖都被保持。

 

下面给出一个例题,2006年5月系分上午43题:

●设关系模式R<U, F>,其中U={A, B, C, D, E},F={A→BC,C→D,BC→E,E→A},则分解ρ={R1(ABCE),R2(CD)}满足 (43) 。
(43) A.具有无损连接性、保持函数依赖
              B.不具有无损连接性、保持函数依赖
              C.具有无损连接性、不保持函数依赖
              D.不具有无损连接性、不保持函数依赖

先做无损链接的判断。R1∩R2={C},计算C+。

Result=C
由于C→D,C∈result,所以result=result∪D=CD
可见C是R2的超码,该分解是一个无损分解。

再做保持依赖的 判断。
A→BC,BC→E, E→A都在R1上成立(也就是说每一个函数依赖左右两边的属性都在R1中),C→D在R2上成立,因此给分解是保持依赖的。

选A。

再看一个复杂点的例题。2007年5月数工40-41题。

●给定关系模式R<U, F>,U={A, B, C, D, E},F={B→A,D→A,A→E,AC→B},其候选关键字为 
(40) ,则分解ρ={R1(ABCE),R2(CD)}满足 (41) 。
(40) A.ABD
              B.ABE
              C.ACD
              D.CD
(41) A.具有无损连接性、保持函数依赖
              B.不具有无损连接性、保持函数依赖
              C.具有无损连接性、不保持函数依赖
              D.不具有无损连接性、不保持函数依赖

看见了吧,和前 面一题多么的相像!
对于第一问,分别计算ABCD四个选项的闭包,
(ABD)+ = { ABDE }
(ABE)+ = { ABE }
(ACD)+ = { ABCDE }
(CD)+ = { ABCDE }
选D。

再看第二问。
先做无损链接的判断。R1∩R2={C},计算C+。

result=C
因此C既不是R1也不是R2的超码,该分解不具有无损分解性。

再做保持依赖的 判断。
B→A,A→E,AC→B在R1上成立,D→A在R1和R2上都不成立,因此需做进一步判断。
由于B→A,A→E,AC→B都是被保持的(因为它们的元素都在R1中),因此我们要判断的是D→A是不是也被保持。

对于D→A应用 算法二:
result=D
对R1,result∩R1=ф(空集,找不到空集的符号,就用这个表示吧),t=ф,result=D
再对R2,result∩R2=D,D+ =ADE ,t=D+ ∩R2=D,result=D
一个循环后result未发生变化,因此最后result=D,并未包含A,所以D→A未被保持,该分解不是保持依赖的。

选D。

参考链接


系统分析师试题分析 索引式文件的索引节点

如果一个索引式文件的索引节点有10个直接块,1个一级间接块,1个二级间接块,1个三级间接块。假设每个数据块的大小是512个字节,一个索引指针占用4个字节。假设索引节点已经在内存中,那么访问该文件偏移地址在6000字节的数据需要再访问 ( ) 次磁盘。

A.1
B.2
C.3
D.4

正确答案

B

答案解析

[解析] 因为每个数据块的大小是512个字节,且前10块可以直接寻址,得出1~5120字节范围内可以直接寻址。对于间接索引块(索引块的大小也是512字节),一个索引指针占4字节,则一个索引块可以映射512/4=128个数据块,因为每个数据块的大小是512个字节,合计64KB。6000B-5120B=880B<64KB,所以只需一次映射就够了。因此,第1次,取索引指针,第2次读数据,一共需要两次访问。

继续阅读系统分析师试题分析 索引式文件的索引节点

霍纳法则

多项式计算

在计算机科学里,我们会经常遇到一些关于计算多项式的问题,例如计算当 ${x}=2$ 时 $2x^4 - 3x^3 + 5x^2 + x - 7$ 的值。我们首先能够想到的方法就是求出每一项的值,然后把它们全部加起来。如果多项式的阶数不高,这种方法完全可行,而且更容易理解,可是如果把这个问题推广到 $n$ 阶,即计算 $a_nx^n + a_{n-1}x^{n-1} + ··· + a_2x^2 + a_1x + a_0 $ 的值,而且当  $n$ 很大时,这种算法就显得力不从心了。

这里以 $2x^4 - 3x^3 + 5x^2 + x - 7$ 为例计算当 $x = 4$ 时的值。下面是直接求解的代码:

def poly_bf(coeffi_list, x):
    degree = len(coeffi_list) - 1  # 最高次项
    result = 0
    for i in range(degree+1):
        coeffi = coeffi_list[i]; poly = 1
        for j in range(degree-i-1, -1, -1):
            poly *= x  # 计算 x^i
        result += coeffi * poly
    return result

直接求解的方法的复杂度等于多少呢?我们知道,计算机在计算乘法的时候的时间开销要大于加减法的时间开销,所以这里的复杂度大致看做是执行乘法运算的次数。

$T(n)=\sum_{i=1}^{n}{i+1}=2+3+\cdots+n+1=\frac{n(n+3)}{2}\in\Theta(n^2) $

最后得到时间复杂度为 $Θ(n^2)$。

霍纳法则

霍纳法则Horner’s rule)可以将上面的多项式转化成下面的形式:

$p(x)=(\cdots(a_nx+a_{n-1})x+\cdots)x+a_0"$

假设还是计算当 $x = 4$ 时 $2x^4 - 3x^3 + 5x^2 + x - 7$ 的值,我们需要先将其转换为 $x(x(x(2x - 3) + 5) + 1) - 7$ 的形式,为了更好地呈现每一步的计算过程,我们可以构建出下面的表格:

实现霍纳法则的代码非常简单,只需要用一个循环即可。

def poly_horner(coeffi_list, x):
    degree = len(coeffi_list) - 1  # 最高次项
    result = coeffi_list[0]
    for i in range(1, degree+1):
        result = result * x + coeffi_list[i]
    return result

经过霍纳法则变换的多项式只需要执行 $n$ 次乘法运算便可以得到 $n$ 阶多项式的值,所以复杂度自然就为 $Θ(n)$ 。跟直接求解相比有了明显的提升,根本原因在于我们对问题做了一个变换,使其变得更容易求解。

参考链接


国密算法

算法分类

国密即国家密码局认定的国产密码算法。主要有SM1,SM2,SM3,SM4。密钥长度和分组长度均为128位。
SM1 为对称加密。其加密强度与AES相当。该算法不公开,调用该算法时,需要通过加密芯片的接口进行调用。
SM2为非对称加密,基于ECC。该算法已公开。由于该算法基于ECC,故其签名速度与秘钥生成速度都快于RSA。ECC 256位(SM2采用的就是ECC 256位的一种)安全强度比RSA 2048位高,但运算速度快于RSA。
SM3 消息摘要。可以用MD5作为对比理解。该算法已公开。校验结果为256位。
SM4 无线局域网标准的分组数据算法。对称加密,密钥长度和分组长度均为128位。

继续阅读国密算法

匈牙利算法

零、前言

匈牙利算法是一个经典的解决二部图最小权值匹配问题的算法。网上也有不少资料,但是看完之后总觉得有两个核心问题没有解决:算法为什么一定能得到最优匹配?算法复杂度为什么不再是指数级了?

最后读到了python的库函数scipy.optimize.linear_sum_assignment源代码里引用的文章,才算理解算法的实现,再花了一点时间弄清楚了上边两个问题。

继续阅读匈牙利算法

Munkres' Assignment Algorithm

Assignment Problem - Let C be an nxn matrix representing the costs of each of n workers to perform any of n jobs.  The assignment problem is to assign jobs to workers so as to minimize the total cost. Since each worker can perform only one job and each job can be assigned to only one worker the assignments constitute an independent set of the matrix C.

继续阅读Munkres' Assignment Algorithm

求最长公共子序列

求给出的两个序列的最长公共子序列是常见的一个问题。

需要注意的就是 最长公共子串(Longest Common Substring)与 最长公共子序列(Longest Common Subsequence)的区别: 子串要求在原字符串中是连续的,而子序列则只需保持相对顺序,并不要求连续。

解法如下:

参考链接


最长递增子序列(Longest Increasing Subsequence)

最长递增子序列的解法,可以转化为 求最长公共子序列,也就是生成从目标序列从小到大排序之后的新序列,然后计算原始序列与新序列的 最长公共子序列但是遇到重复数字的时候,会出现问题也就是这个解法只能解决无重复数字的最长递增子序列

另外需要注意的就是 最长公共子串(Longest Common Substring)与 最长公共子序列(Longest Common Subsequence)的区别: 子串要求在原字符串中是连续的,而子序列则只需保持相对顺序,并不要求连续。

整个的求解过程其实很好理解:

  • 假定要计算的序列为 K = {K1,K2,K3,……,Kn,Kn+1,……,Km}
  • 初始化计数数组 N = {N1,N2,N3,……,Nn,Nn+1,……,Nm} ,(计数数组跟序列的长度相同,Kn 的最长递增子序列长度计数对应于 Nn)用来记录最长递增子序列的长度,每个项的初始化值都设置为 1即使最小的项,项本身也算一个长度,因此最小的长度是 1
  • 必须从头部(K1)开始遍历
  • 当计算 Kn 结尾的最长递增子序列的时候,只要遍历序列 K 的前 [0 ~ n-1] 个项,找到数值 小于(或者小于等于Kn必须遍历,Kn 之前的最长递增子序列不一定是 Kn-1 对应的 Nn-1 ),并且在计数数组 N 中对应的计数最大的项 Ni ,然后 Nn = Ni + 1

本方法的时间复杂度是 O(n2)

例子:

设数组 K = { 2, 5, 1, 5, 4, 5 }  那么求最长递增子序列的代码如下:

#include <iostream>
#include <vector>

int calcMaxLISLength(const std::vector<int>& K) {
    std::vector<int> N(K.size(), 1);
    int maxL = 0;
    for(int i = 0; i < K.size(); i++) {
        int Ki = K[i];
        int maxNi = 0;
        for(int j = 0; j < i; j++) {
            int Kj = K[j]; 
            if(Kj < Ki) {
                int Nj = N[j];
                if(Nj > maxNi) {
                    maxNi = Nj;
                }
            }
        }
        int Ni = maxNi + 1;
        N[i] = Ni;
        if(maxL < Ni) {
            maxL = Ni;
        }
    }
    return maxL;    
}

int main(int argc, char** argv) {
    // 2 5 1 5 4 5
    // 3
    std::vector<int> K = {2, 5, 1, 5, 4, 5};
    int r = calcMaxLISLength(K);
    std::cout << r << std::endl;
    
    // 17
    K = {317, 211, 180, 187, 104, 285, 63, 117, 320, 35, 288, 299,
          235, 282, 105, 231, 253, 74, 143, 148, 77, 249, 310, 137,
          191, 184, 88, 8, 194, 12, 117, 108, 260, 313, 114, 261, 60,
          226, 133, 61, 146, 297, 291, 13, 198, 286, 254, 96, 135, 48, 
          135, 307, 23, 155, 203, 258, 168, 42, 301, 45, 164, 193, 26, 
          290, 280, 172, 94, 230, 156, 36, 250, 174, 47, 188, 148, 138,
          194, 89, 71, 119, 218, 325, 136, 63, 271, 210, 320, 309};
    
    r = calcMaxLISLength(K);
    std::cout << r << std::endl;
	
    return 0;
}

第二个测试用例的数据如下:

输入数据个数 
88

实际数据 
317 211 180 187 104 285 63 117 320 35 288 299 235 282 105 231 253 74 143 148 77 249 310 137 191 184 88 8 194 12 117 108 260 313 114 261 60 226 133 61 146 297 291 13 198 286 254 96 135 48 135 307 23 155 203 258 168 42 301 45 164 193 26 290 280 172 94 230 156 36 250 174 47 188 148 138 194 89 71 119 218 325 136 63 271 210 320 309

期望结果
17

涉及到的面试题目如下类型:

 
梅花桩问题

题目描述 : Redraiment是走梅花桩的高手。Redraiment总是起点不限,从前到后,往高的桩子走,但走的步数最多,不知道为什么?你能替Redraiment研究他最多走的步数吗? 
样例输入
6
2 5 1 5 4 5
样例输出
3
提示
Example: 
6个点的高度各为 2 5 1 5 4 5 
如从第1格开始走,最多为3步, 2 4 5 
从第2格开始走,最多只有1步,5 
而从第3格开始走最多有3步,1 4 5 
从第5格开始走最多有2步,4 5
所以这个结果是3。
输入例子:
6
2
5
1
5
4
5
输出例子:
3

合唱队问题

题目描述
计算最少出列多少位同学,使得剩下的同学排成合唱队形
说明:
N位同学站成一排,音乐老师要请其中的(N-K)位同学出列,使得剩下的K位同学排成合唱队形。
合唱队形是指这样的一种队形:设K位同学从左到右依次编号为1,2…,K,他们的身高分别为T1,T2,…,TK,   则他们的身高满足存在i(1<=i<=K)使得T1<T2<......<Ti-1<Ti>Ti+1>......>TK。
你的任务是,已知所有N位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。
输入描述:
整数N
输出描述:
最少需要几位同学出列
示例1
输入
8
186 186 150 200 160 130 197 200
输出
4

合唱队问题其实是 求最长递增子序列 与 最长递减子序列 的 和 最大。

最长递减子序列的求法其实就是把原始序列反序,然后 求最长递增子序列 然后把最后的结果反序即可。

代码如下:

#include <iostream>
#include <vector>

static void calcMaxLIS(const std::vector<int>& K, std::vector<int>& N) {
    for(int i = 0; i < K.size(); i++) {
        int Ki = K[i];
        int maxNi = 0;
        for(int j =0; j<i; j++) {
            int Kj = K[j];
            if(Ki > Kj) {
                int Nj = N[j];
                if(maxNi < Nj) {
                    maxNi = Nj;
                }
            }
        }
        N[i] = maxNi + 1;
    }
}

static void calcMaxLDS(const std::vector<int>& K, std::vector<int>& N) {
    std::vector<int> rK(K.rbegin(), K.rend());
    std::vector<int> rN(N);
    calcMaxLIS(rK, rN);
    for(int i = 0; i<rN.size(); i++) {
        N[i] = rN[rN.size() - 1 - i];
    }
}

static int calcMaxVal(const std::vector<int>& K) {
    std::vector<int> N(K.size(), 1);
    std::vector<int> rN(K.size(), 1);
    std::vector<int> KN(K.size(), 0);
    calcMaxLIS(K, N);
    calcMaxLDS(K, rN);
    //
    /*for(int i = 0; i<N.size(); i++) {
        std::cout << N[i];
    }
    std::cout << std::endl;
    for(int i = 0; i<rN.size(); i++) {
        std::cout << rN[i];
    }
    std::cout << std::endl;*/
    
    for(int i = 0; i<N.size(); i++) {
        KN[i] = N[i] + rN[i];
    }
    
   /* for(int i = 0; i<KN.size(); i++) {
        std::cout << KN[i];
    }
    std::cout << std::endl;*/
    
    int maxL = 0;
    for(int i = 0; i < KN.size(); i++) {
        if(maxL < KN[i]) {
            maxL = KN[i];
        }
    }
    return maxL - 1;
}

static void test_calcMaxVal() {
    std::vector<int> K = {186, 186, 150, 200, 160, 130, 197, 200};
    int r = calcMaxVal(K);
    std::cout << K.size() - r << std::endl;
}

static void main_test() {
    test_calcMaxVal();
}

static void main_task() {
    int n = 0;
    while(std::cin >> n) {
        std::vector<int> K;
        for(int i = 0; i<n; i++) {
            int v = 0;
            std::cin >> v;
            K.push_back(v);   
        }
        int r = calcMaxVal(K);
        std::cout << n - r << std::endl;         
    }
}

int main(int argc, char** argv) {
    //main_test();
    main_task();
    return 0;
}

解法介绍

首先计算每个数在最大递增子串中的位置

186  186  150  200  160  130  197  200   quene

1      1      1      2       2      1      3     4       递增计数


然后计算每个数在反向最大递减子串中的位置--->计算反向后每个数在最大递增子串中的位置

200  197  130  160  200  150  186  186   反向quene

1      1      1       2     3      2      3       3      递减计数


然后将每个数的递增计数和递减计数相加

186  186  150  200  160  130  197  200   quene

1      1      1      2       2     1      3      4       递增计数

3      3      2      3       2     1      1      1       递减计数

4      4      3      5       4     2      4      5       每个数在所在队列的人数+1(自己在递增和递减中被重复计算)


如160这个数

在递增队列中有2个人数

150  160

在递减队列中有2个人数

160  130

那么160所在队列中就有3个人

150  160  130


每个数的所在队列人数表达就是这个意思


总人数 - 该数所在队列人数 = 需要出队的人数

参考链接