Day37-数据结构与算法-串、其他


title: Day37-数据结构与算法-串、其他
date: 2021-02-24 17:57:30
author:Liu_zimo


常用的经典数据结构


布隆过滤器(Bloom Filter)

  • 1970年由布隆提出
    • 它是一个空间效率高的概率型数据结构,可以用来告诉你:一个元素一定不存在或者可能存在
  • 优缺点
    • 优点:空间效率和查询时间都远远超过一般的算法
    • 缺点:有一定的误判率、删除困难
  • 它实质上是一个很长的二进制向量和一系列随机映射函数(Hash函数)
  • 常见应用
    • 网页黑名单系统、垃圾邮件过滤系统、爬虫的网址判重系统、解决缓存穿透问题
布隆过滤器的原理
  • 假设布隆过滤器由20位二进制、3个哈希函数组成,每个元素经过哈希函数处理都能生成一个索引位置
    • 添加元素:将每一个哈希函数生成的索引位置都设为1
    • 查询元素是否存在
      • 如果有一个哈希函数生成的索引位置不为1,就代表不存在(100%准确)
      • 如果每一个哈希函数生成的索引位置都为1,就代表存在(存在一定的误判率)
  • 添加、查询的时间复杂度都是:O(k),k是哈希函数的个数。空间复杂度是:O(m),m是二进制位的个数
  • 误判率p受3个因素影响:二进制位的个数m、哈希函数的个数k、数据规模n
    • p = (1 - e- ( [k(n + 0.5)] / (m - 1) ))k == (1 - e- (kn / m))k
  • 已知误判率p、数据规模n,求二进制位的个数m、哈希函数的个数k
    • m = - (nlnp) / (ln2)2 lnp = logep
    • k = (m / n) ln2 k = - (lnp / ln2) = -log2p
布隆过滤器的实现
  • Guava:Google Core Libraries For Java
    https://mvnrepository.com/artifact/com.google.guava/guava
package com.zimo.算法.串_其他;

/**
 * 布隆过滤器
 *
 * @author Liu_zimo
 * @version v0.1 by 2021/2/25 10:14
 */
public class BloomFilter<T> {

    private int bitSize;    // 二进制向量的长度(一共有多少个二进制位)
    private long[] bits;    // 二进制向量  [[0-63], [64-127], [128 - 191]]  [64,63,62,61,...2,1,0]
    private int hashSize;   // 哈希函数的个数
    /**
     * 构造一个布隆过滤器
     * @param n 数据规模
     * @param p 误判率,取值范围(0,1)
     */
    public BloomFilter(int n, double p) {

        double ln2 = Math.log(2);
        // 求出二进制向量的长度
        this.bitSize = (int)(- (n * Math.log(p)) / (ln2 * ln2));
        // 求出哈希函数的个数
        this.hashSize = (int) (bitSize * ln2 / n);

        this.bits = new long[(bitSize + Long.SIZE - 1)/ Long.SIZE];
    }

    /**
     * 添加元素
     * @param value
     */
    public void put(T value){
        nullCheck(value);
        int hash1 = value.hashCode();
        int hash2 = hash1 >>> 16;
        for (int i = 0; i <= hashSize; i++) {
            int combinedHash = hash1 + (i * hash2);
            if (combinedHash < 0){
                combinedHash = ~combinedHash;
            }
            // 生成一个二进制位的索引
            int index = combinedHash % this.bitSize;
            // 设置index位置的二进制位为1
            set(index);
        }
    }

    /**
     * 判断是否存在一个元素
     * @param value
     * @return
     */
    public boolean contains(T value){
        nullCheck(value);
        int hash1 = value.hashCode();
        int hash2 = hash1 >>> 16;
        for (int i = 0; i <= hashSize; i++) {
            int combinedHash = hash1 + (i * hash2);
            if (combinedHash < 0){
                combinedHash = ~combinedHash;
            }
            // 生成一个二进制位的索引
            int index = combinedHash % this.bitSize;
            // 查询index位置的二进制位是否为0
            if (!get(index)) return false;
        }
        return true;
    }

    private void set(int index){
        // 找到对应的long位置
        long value = this.bits[index / Long.SIZE];
        // 找到二进制位在long内部的索引
        this.bits[index / Long.SIZE] = value | (1 << (index % Long.SIZE));
    }
    private boolean get(int index){
        long value = this.bits[index / Long.SIZE];
        value = value & (1 << (index % Long.SIZE));
        return value != 0;
    }

    private void nullCheck(T value){
        if (value == null){
            throw new IllegalArgumentException("value must not be null");
        }
    }

    public static void main(String[] args) {
        BloomFilter<Integer> integerBloomFilter = new BloomFilter<>(1_00_0000, 0.01);

        for (int i = 0; i < 500; i++) {
            integerBloomFilter.put(i);
        }
        for (int i = 0; i < 500; i++) {
            System.out.println(integerBloomFilter.contains(i));
        }
        int error = 0;
        for (int i = 500; i < 1000; i++) {
            if (integerBloomFilter.contains(i)){
                error++;
            }
        }
        System.out.println(error);
    }
}

跳表(SkipList)

  • 一个有序链表搜索、添加、删除的平均时间复杂度是多少?

    • O(n)
  • 能否利用二分搜索优化有序链表,将搜索、添加、删除的平均时间复杂度降低至O(logn)?

    • 链表没有像数组那样的高效随机访问(O(1)时间复杂度),所以不能像有序数组那样直接进行二分搜索进行优化
  • 那有没有其他办法让有序链表搜索、添加、删除的平均时间复杂度降低至O(logn)?

    • 使用跳表(SkipList)
  • 跳表,又叫做跳跃表、跳跃列表,在有序链表的基础上增加了“跳跃”的功能

    • 由William Pugh于1990年发布,设计的初衷是为了取代平衡树(比如红黑树、AVL树)
  • Redis中的SortedSet、LevelDB中的MemTable都用到了跳表

    • Redis、LevelDB都是著名的Key-Value数据库
  • 对比平衡树

    • 跳表的实现和维护会更加简单
    • 跳表的搜索、删除、添加的平均时间复杂度是O(logn)

跳表

跳表的搜索
  1. 从顶层链表的首元素开始,从左往右搜索,直至找到一个大于或等于目标的元素,或者到达当前层链表的尾部
  2. 如果该元素等于目标元素,则表明该元素已被找到
  3. 如果该元素大于目标元素或已到达链表的尾部,则退回到当前层的前一个元素,然后转入下一层进行搜索
跳表的添加、删除
  • 添加的细节
    • 随机决定新添加元素的层数
  • 删除的细节
    • 删除一个元素后,整个跳表的层数可能会降低
跳表的层数
  • 跳表是按层构造的,底层是一个普通的有序链表,高层相当于是低层的“快速通道”
    • 在第i层中的元素按某个固定的概率p(通常为1/2或1/4)出现在第i+1层中,产生越高的层数,概率越低
      • 元素层数恰好等于1的概率为1 - p
      • 元素层数大于等于2的概率为p,而元素层数恰好等于2的概率为p * (1 - p)
      • 元素层数大于等于3的概率为p ^ 2,而元素层数恰好等于3的概率为p ^ 2 * (1 - p)
      • 元素层数大于等于4的概率为p ^ 3,而元素层数恰好等于4的概率为p ^ 3 * (1 - p)
      • 一个元素的平均层数是1 / (1 - p)

跳表层数公式推导

  • 当p = 1/2时,每个元素所包含的平均指针数量是2
  • 当p= 1/4时,每个元素所包含的平均指针数量是1.33
跳表的复杂度
  • 每一层的元素数量
    • 第1层链表固定有n个元素
    • 第2层链表平均有n * p个元素
    • 第3层链表平均有n * p ^ 2个元素
    • 第k层链表平均有n * p ^ k个元素
  • 另外
    • 最高层的层数是log1/pn,平均有个1/p元素
    • 在搜索时,每一层链表的预期查找步数最多是1/p,所以总的查找步数是-(logpn/p),时间复杂度是O(logn)

B+树

  • B+树是B树的变体,常用于数据库和操作系统的文件系统中

    • MySQL数据库的索引就是基于B+树实现的
  • B+树的特点

    • 分为内部节点(非叶子)、叶子节点2种节点
      内部节点只存储key,不存储具体数据
      叶子节点存储key和具体数据

    • 所有的叶子节点形成一条有序链表

    • m阶B+树非根节点的元素数量x

      ceil(m/2) ≤ x ≤ m

MySQL的索引底层为何使用B+树
  • 为了减小IO操作数量,一般把一个节点的大小设计成最小读写单位的大小
    • MySQL的存储引擎lnnoDB的最小读写单位是16K
  • 对比B树,B+树的优势是
    • 每个节点存储的key数量更多,树的高度更低
    • 所有的具体数据都存在叶子节点上,所以每次查询都要查到叶子节点,查询速度比较稳定
    • 所有的叶子节点构成了一个有序链表,做区间查询时更方便

B*树

  • B* 树是B+树的变体:给内部节点增加了指向兄弟节点的指针
  • m阶B*树非根节点的元素数量x
    ceil(2m/3) ≤ x ≤ m

串(Sequence)

  • 本课程研究的串是开发中非常熟悉的字符串,是由若干个字符组成的有限序列

    String Text = "thank";

  • 字符串thank的前缀(prefix)、真前缀(proper prefix)、后缀(suffix)、真后缀(proper suffix)

    前缀t、th、tha、than、thank
    真前缀t、th、tha、than
    后缀thank、hank、ank、nk、k
    真后缀hank、ank、nk、k
串匹配算法
  • 本课程主要研究串的匹配问题,比如

    • 查找一个模式串(Pattern)在文本串(Text)中的位置

      String text = "Hello World";
      String pattern = "or";
      text.indexOf(pattern);	// 7
      text.indexOf("other");  // -1
      
  • 几个经典的串匹配算法

    • 蛮力(Brute Force)
    • KMP
    • Boyer-Moore
    • Rabin-Karp
    • Sunday
  • 本课程用tlen代表文本串Text的长度,plen 代表模式串Pattern的长度

蛮力(Brute Force)
  • 以字符为单位,从左到右移动模式串,直到匹配成功

蛮力算法

  • 蛮力算法有两种常见实现思路
蛮力1

蛮力1执行过程

package com.zimo.算法.串_其他..蛮力;

/**
 * 蛮力算法1
 *
 * @author Liu_zimo
 * @version v0.1 by 2021/2/26 14:46
 */
public class BruteForce_1 {
    private static int indexOf(String text, String pattern){
        if (text == null || pattern == null) return -1;
        char[] textChars = text.toCharArray();
        int tlen = textChars.length;
        if (tlen == 0) return -1;
        char[] patternChars = pattern.toCharArray();
        int plen = patternChars.length;
        if (plen == 0) return -1;
        if (tlen < plen) return -1;

        int pi = 0, ti = 0;
        while (pi < plen && ti <tlen){
            if (textChars[ti] == patternChars[pi]){
                ti++;
                pi++;
            }else{
                ti -= pi - 1;
                pi = 0;
            }
        }
        return pi == plen ? (ti - pi) : -1;
    }

    private static int indexOf2(String text, String pattern){
        if (text == null || pattern == null) return -1;
        char[] textChars = text.toCharArray();
        int tlen = textChars.length;
        if (tlen == 0) return -1;
        char[] patternChars = pattern.toCharArray();
        int plen = patternChars.length;
        if (plen == 0) return -1;
        if (tlen < plen) return -1;

        int pi = 0, ti = 0;
        while (pi < plen && ti - pi <= tlen - plen){    // 优化
            if (textChars[ti] == patternChars[pi]){
                ti++;
                pi++;
            }else{
                ti -= pi - 1;
                pi = 0;
            }
        }
        return pi == plen ? (ti - pi) : -1;
    }
}
  • 因此,ti的退出条件可以从ti < tlen 改为
    • ti - pi <= tlen - plen
    • ti - pi是指每一轮比较中Text首个比较字符的位置
蛮力2

蛮力2执行过程

package com.zimo.算法.串_其他..蛮力;

/**
 * 蛮力算法2
 *
 * @author Liu_zimo
 * @version v0.1 by 2021/2/26 14:46
 */
public class BruteForce_2 {
    private static int indexOf(String text, String pattern){
        if (text == null || pattern == null) return -1;
        char[] textChars = text.toCharArray();
        int tlen = textChars.length;
        if (tlen == 0) return -1;
        char[] patternChars = pattern.toCharArray();
        int plen = patternChars.length;
        if (plen == 0) return -1;
        if (tlen < plen) return -1;

        for (int ti = 0; ti <= tlen - plen; ti++) {
            int pi = 0;
            for (;pi < plen; pi++) {
                if (textChars[ti + pi] != patternChars[pi])break;
            }
            if (pi == plen) return ti;
        }
        return -1;
    }
}
蛮力 - 性能分析
  • n是文本串长度,m是模式串长度
  • 最好情况
    • 只需一轮比较就完全匹配成功,比较m次( m是模式串的长度)
    • 时间复杂度为O(m)
  • 最坏情况(字符集越大,出现概率越低)
    • 执行了n - m + 1轮比较(n是文本串的长度)
    • 每轮都比较至模式串的末字符后失败(m - 1次成功,1次失败)
    • 时间复杂度为O(m * (n - m + 1)),由于一般m远小于n,所以为O(nm)

KMP

  • KMP是 Knuth-Morris-Pratt 的简称(取名自3位发明人的名字),于1977年发布

蛮力VSKMP

  • KMP会预先根据模式串的内容生成一张next表(一般是个数组)
模式串“ABCDABCE”的next表
模式串字符ABCDABCE
索引01234567
元素-10000123
KMP - 核心原理

KMP-核心原理

  • 当d、e失配时,如果希望Pattern能够一次性向右移动一大段距离,然后直接比较d、c字符
    • 前提条件是A必须等于B
  • 所以KMP必须在失配字符e左边的子串中找出符合条件的A、B,从而得知向右移动的距离
  • 向右移动的距离:e左边子串的长度-A的长度,等价于:e的索-c的索引
  • 且c的索引 == next[e的索引],所以向右移动的距离:e的索引 - next[e的索]
  • 总结
    • 如果在pi位置失配,向右移动的距离是pi - next[pi],所以next[pi]越小,移动距离越大
    • next[pi]是pi左边子串的真前缀后缀的最大公共子串长度
KMP - 得到next表
模式串字符ABCDABCE
元素00001230
  • 将最大公共子串长度都向后移动1位,首字符设置为负1,就得到了next表
模式串“ABCDABCE”的next表
模式串字符ABCDABCE
索引01234567
元素-10000123
KMP - 为什么是“最大”公共子串长度
  • 假设文本串是AAAAABCDEF,模式串是AAAAB
模式串真前缀真后缀公共子串长度
AAAAA,AA,AAAA,AA,AAA1,2,3
AAAA,AAA,AA1,2
AAAA1

KMP“最大”公共子串

KMP - next表的构造思路

next表的构造思路

KMP - next表的不足之处

KMP-不足之处

  • 在这种情况下,KMP显得比较笨拙
KMP - next表的优化思路

KMP-优化思路

模式串“AAAAB”的next表
模式串字符AAAAB
索引01234
优化前-10123
优化后-1-1-1-13
package com.zimo.算法.串_其他..KMP;

/**
 * KMP算法
 *
 * @author Liu_zimo
 * @version v0.1 by 2021/2/26 17:21
 */
public class KMP {

    private static int indexOf(String text, String pattern){
        if (text == null || pattern == null) return -1;
        char[] textChars = text.toCharArray();
        int tlen = textChars.length;
        if (tlen == 0) return -1;
        char[] patternChars = pattern.toCharArray();
        int plen = patternChars.length;
        if (plen == 0) return -1;
        if (tlen < plen) return -1;

        // next表
        int[] next = next1(pattern);

        int pi = 0, ti = 0;
        while (pi < plen && ti - pi <= tlen - plen){    // 优化
            if (pi < 0 || textChars[ti] == patternChars[pi]){
                ti++;
                pi++;
            }else{
                // 失配情况
                pi = next[pi];
            }
        }
        return pi == plen ? (ti - pi) : -1;
    }

    private static int[] next(String pattern){
        char[] chars = pattern.toCharArray();
        int[] next = new int[chars.length];

        int i = 0;
        int n = next[0] = -1;   // next表头设置为-1 , n == next[i]
        int iMax = chars.length -1;
        while (i < iMax){
            if (n < 0 || chars[i] == chars[n]){
                next[++i] = n + 1;
            }else {
                n = next[n];
            }
        }
        return next;
    }

    // 优化next表
    private static int[] next1(String pattern){
        char[] chars = pattern.toCharArray();
        int[] next = new int[chars.length];

        int i = 0;
        int n = next[0] = -1;   // next表头设置为-1 , n == next[i]
        int iMax = chars.length -1;
        while (i < iMax){
            if (n < 0 || chars[i] == chars[n]){
                ++i;
                ++n;
                if (chars[i] == chars[n]){
                    next[i] = next[n];
                }else {
                    next[i] = n;
                }
            }else {
                n = next[n];
            }
        }
        return next;
    }

    public static void main(String[] args) {
        System.out.println(KMP.indexOf("AAABCD", "BC"));
    }
}
KMP - 性能分析
  • KMP主逻辑
    • 最好时间复杂度:O(m)
    • 最坏时间复杂度:O(n),不超过O(2n)
  • next表的构造过程跟KMP主体逻辑类似
    • 时间复杂度:O(m)
  • KMP整体
    • 最好时间复杂度:O(m)
    • 最坏时间复杂度:O(n + m)
    • 空间复杂度:O(m)

蛮力 vs KMP

  • 蛮力算法为何低效?
  • 当字符失配时蛮力算法
    • ti回溯到左边位置
    • pi回溯到0
  • KMP算法
    • ti不必回溯
    • pi不一定要回溯到0

Boyer - Moore

  • Boyer-Moore算法,简称BM算法,由Robert S.Boyer和J Strother Moore于1977年发明
    • 最好时间复杂度:O(n/m),最坏时间复杂度:O(n + m)
    • 该算法从模式串的尾部开始匹配(自后向前)
  • BM算法的移动字符数是通过2条规则计算出的最大值
    • 坏字符规则(Bad Character,简称BC)
    • 好后缀规则(Good Suffix,简称GS)
坏字符(Bad Character)

BM算法-坏字符规则BC

  • 当Pattern中的字符E和Text中的S失配时,称S为“坏字符”
    • 如果 Pattern 的未匹配子串中不存在坏字符,直接将 Pattern 移动到坏字符的下一位
    • 否则,让 Pattern 的未匹配子串中最靠右的坏字符与Text中的坏字符对齐
好后缀(Good Suffix)

BM算法-好后缀规则GS

  • “MPLE”是一个成功匹配的后缀,“E”、“LE” 、“PLE”、“MPLE”都是“好后缀”
    • 如果 Pattern 中找不到与好后缀对齐的子串,直接将Pattern移动到好后缀的下一位
    • 否则,从 Pattern 中找出子串与Text 中的好后缀对齐
BM的最好情况

BM的最好情况

  • 时间复杂度:O(n/m)
BM的最坏情况

BM的最坏情况

  • 时间复杂度:O(n + m)
    • 其中的O(m)是构造 BC、GS表

Rabin-Karp

  • Rabin-Karp算法(或Karp-Rabin算法),简称RK算法,是一种基于hash的字符串匹配算法
    • 由Richard M. Karp和Michael O. Rabin于1987年发明
  • 大致原理
    • 将Pattern的hash值与Text中每个子串的hash值进行比较
    • 某一子串的hash 值可以根据上一子串的 hash 值在O(1)时间内计算出来

Sunday

  • Sunday算法由 Daniel M.Sunday 在1990年提出,它的思想跟BM算法很相似
    • 从前向后匹配
    • 当匹配失败时,关注的是Text中参与匹配的子串的下一位字符A
      • 如果A没有在Pattern 中出现,则直接跳过,即移动位数 = Pattern长度+1
      • 否则,让 Pattern中最靠右的A与Text 中的A对齐

Sunday算法

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柳子陌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值