数据结构与算法学习⑨(B树 巧妙的位运算 布隆过滤器和LRU Cache)

数据结构与算法学习⑨

B树

概念

B树(B-tree,B-树):它是一种平衡的多路搜索树,多用于文件系统,数据库的实现
在这里插入图片描述
m阶B树的含义:B树中每个节点最多有m个子节点
B树的特点:
1、B树每个节点可以存储超过2个元素,可以拥有超过2个子节点,即多路,多叉
2、B树拥有二叉搜索树的一些性质
3、B树非常平衡,每个节点所有子树的高度均一样
4、B树的高度非常低,即B树很矮 m
5、B树的叶子节点都在同一层(有的定义中叶子节点指的是不包含任何信息的外部空节点,指向它们的指针为null)

m阶B树的性质(m≥2)
在这里插入图片描述
在这里插入图片描述

B树的搜索

搜索和二叉搜索树的搜索类似
1.先再节点内部从小到大搜索元素
2.如果命中,搜索结束
3.如果未命中,再去对应的子节点中搜索元素,回到步骤一

B树的添加

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

明确一点,新元素的添加必定再最后一层的叶子节点中

根节点:1≤x≤m-1
非根节点 :⌈ m/2 ⌉ - 1 ≤ x ≤ m-1
由于现在是4阶B树,每个节点内部元素个数最多为3个 35加入后会导致20右子节点内部元素为4,不符合B树 的定义

这种节点内元素个数超过上限的现象叫做:上溢(overflow)

添加-上溢的解决

假设现在是5阶B树,某个节点发生了上溢,那么该节点的元素个数必然是5个
在这里插入图片描述

解决方案
1、假设上溢节点最中间元素的位置为k,将k位置的元素向上与父节点合并
2、将[0,k-1]和[k+1,m-1]位置的元素分裂成2个子节点,两个子节点的元素个数也必然不会低于最低限制:⌈ m/2 ⌉ - 1
3、一次分裂完成后,可能导致父节点上溢,只需按照上述方法继续解决,最极端情况是一直分裂到根节点, 根节点上溢分裂会导致B树长高一层
在这里插入图片描述
上溢节点是溢出的节点

B树的删除

在这里插入图片描述
删除的元素再叶子节点中:直接删除即可

在这里插入图片描述
删除的元素在非叶子节中
1、找到前驱或后继元素,覆盖要被删除的元素
2、再把用以覆盖的前驱或后继元素删除 p 非叶子节点的前驱或后继,必定在叶子节点中 所以删除前驱或后继就回到了删除的元素在叶子中 的情况
40的前驱是35后继是50

在这里插入图片描述
由于是一棵5阶B树,非根节点中元素个数x的范围是:2 ≤ x ≤ 4,删除15之后破坏了这个性质

这种节点内元素个数低于下限的现象叫做:下溢(underflow)

删除-下溢的解决

明确一点:下溢节点的元素数量必然等于:⌈ m/2 ⌉ - 2
在这里插入图片描述
解决方案:看临近的兄弟节点是否能借一个元素,兄弟节点元素个数如果≥⌈ m/2 ⌉ ,则可以向外借一个元素
1、因此将父节点中的b元素插入到下溢节点的0位置(最小位置), 2、用兄弟节点中的元素a(最大元素)替代父节点中的元素b,如果a原本有右边的子节点则将它作为b左边的子节点 p
这种操作其实就是旋转
(因为b比a大,借的话需要把b下来,a上去,d放在原来的另一边)

在这里插入图片描述
解决方案:看临近的兄弟节点是否能借一个元素,兄弟节点元素个数如果为⌈ m/2 ⌉ -1,则无法向外借一个元素
1、将父节点中的元素b挪下来和左右两个子节点合并(此时父节点只是少了个元素)
2、合并后的节点元素个数等于:⌈ m/2 ⌉ -1 + 1 + ⌈ m/2 ⌉ -2 ,不超过m -1
3、这个操作可能会导致父节点下溢,此时依然按照上述方案进行操作,下溢现象可能会一直往上传播,极端情况 会传播到根节点,由于根节点既没有父节点也没有兄弟节点,所以最坏的情况原本的根节点下去跟它的子节点合并 成新的根节点,此时整个B树会变矮
在这里插入图片描述

红黑树和4阶B树的等价变换

在这里插入图片描述
把黑色当作一个层,链接红色,兄弟节点在同一层
在这里插入图片描述

位运算

入门

计算机中的数在内存中都是以二进制形式进行存储的,用位运算就是直接对整数在内存中的二进制位进行操作, 因此其执行效率非常高,在程序中尽量使用位运算进行操作,这会大大提高程序的性能。
在这里插入图片描述
在这里插入图片描述

在线进制转换

常见的位运算符

在这里插入图片描述

有符号数&无符号数

场景:假设用1字节8位二进制表示一个数,那么8可以表示为:0000 1000,请问-8应该如何表示?
日常书写表示:负数其实就是正数前面加了一个负号
计算机内部表示:将二进制数的最高位(第一位)规定为符号位,用0代表正数,1代表负数,就能够表示负数了 这样八位二进制数中就分成了第一位符号位和后七位数值位
结论:有符号数中最高位用于表示正负,无符号数中,所有的位都用于直接表示该值的大小
在这里插入图片描述

计算机如何存储负数

在这里插入图片描述

负数用补码表示:补码=反码+1
知识小贴士:已知一个数的补码,求原码的操作分两种情况:
(1)如果补码的符号位为“0”,表示是一个正数,所以补码就是该数的原码。
(2)如果补码的符号位为“1”,表示是一个负数,求原码的操作可以是:符号位为1, 其余各位取反,然后再整个数加1。

以-1为例,原码10000001,反码11111110,补码11111111。

在这里插入图片描述
在这里插入图片描述

常见位运算的骚操作

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

面试题

191. 位1的个数

191. 位1的个数

解法一:
位移+计数 n & 1 ==1:最低位是1,计数器+1
n>>1,继续判断
移位32次结束(java中int用4字节表示)

public class Solution {
    // you need to treat n as an unsigned value
    public int hammingWeight(int n) {
         int count=0;
       for(int i=0;i<32;i++){
           if((n&1)==1){
              count++;
           }
           n>>=1;
           
       }
        return count;
    }
}

解法二:
n & (n−1)消去最低位的1 n & (n−1),可消去n的二进制表示中最后一位1, 消去1次,计数器+1
当n==0时,所有的1都被置为了0,结束

public class Solution {
    // you need to treat n as an unsigned value
    public int hammingWeight(int n) {
         int count=0;
        while(n!=0){
            count++;
            n=n&(n-1);
        }
        return count;
    }
}
231. 2的幂

知识点:如果是2的幂次方则只会有一个二进制位为1,则可以借助 n & (n-1) 的结果是否为0
231. 2的幂

class Solution {
    public boolean isPowerOfTwo(int n) {
          if(n<=0){
              return false;
          }
          return (n&(n-1))==0;
    }
}
190. 颠倒二进制位

190. 颠倒二进制位
好题解

解法一:取模求和
十进制:ans = ans * 10 + n % 10; n = n / 10;
二进制:ans = ans * 2 + n % 2; n = n / 2;

public class Solution {
    // you need treat n as an unsigned value
    public int reverseBits(int n) {
        int ret=0;
         for(int i=0;i<32;i++){
             ret=(ret<<1)+(n&1);
             n=n>>1;
         }
         return ret;
    }
}

解法二:按位翻转
在这里插入图片描述

public class Solution {
    // you need treat n as an unsigned value
    public int reverseBits(int n) {
        int res=0;
        for(int i=0;i<32;i++){
           res^=(n&(1<<i))!=0?(1<<(31-i)):0;
        }
        return res;
    }
}
52. N皇后 II

N皇后 II
位运算解法:此类问题的终极解法
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

class Solution {
    int count=0;
    public int totalNQueens(int n) {
           dfs(n,0,0,0,0);
           return count;
    }

    public void dfs(int n,int row,int col,int pie,int na){
        if(n==row){
            count++;
            return;
        }
        //算出棋盘当前行还有哪些位置可以放置皇后由 0 变成 1,以便进行后续的位遍历 i
        int bits=~(col|pie|na)&((1<<n)-1);
        while(bits>0){
        //取第一个1
            int mark=bits&(-bits);
            dfs(n,row+1,col|mark,(pie|mark)>>1,(na|mark)<<1);
            bits&=bits-1;
        }
    }
}

布隆过滤器

为什么要使用布隆过滤器

1、在 FBI,如何判断一个嫌疑人的名字是否已经在嫌疑名单上?
2、在网络爬虫里,如何判断一个网址是否被访问过?
3、反垃圾邮件功能中,如何从数十亿个垃圾邮件列表中判断某邮件是否垃圾邮件?
4、…
共同的特点: 如何判断一个元素(关键字)是否存在一个集合中?

用什么数据结构存储关键字?

数组 链表 BST Trie map(红黑树) 哈希表 …
思考1:数组,链表,树等数据结构配合常见的排序、二分搜索 可以解决大部分需求 但是当集合里面的元素数量足够大,如果有500万条记录甚至1亿条记录呢?
思考2:哈希表效率很高,查询效率可以达到O(1),判断关键字是否在集合中非常适合 数组、链表、树等数据结构会存储元素的内容,一旦数据量过大,消耗的内存也会呈现线性增长,最终达到瓶颈
但是哈希表需要消耗的内存依然很高。使用哈希表判断一亿个垃圾邮件中某邮件是否垃圾邮件,它毕竟需要将这 一亿条数据存储起来,耗费的空间资源是比较多的。
总之:希望找一种判断效率既高且耗费存储少的方案

什么是布隆过滤器?

布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量一系列随机映射函数。 布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法, 缺点是有一定的误识别率删除困难
很长的二进制向量:二进制数组 不存储关键字数据,只用二进制位表明在还是不在集合中
在这里插入图片描述
优势:非常节省存储空间,而且基于数组访问效率高

哈希表中的哈希函数
一系列随机函数:哈希函数
在这里插入图片描述
布隆过滤器中的哈希函数
在这里插入图片描述
在这里插入图片描述

为什么布隆过滤器删除困难?

在这里插入图片描述

应用场景举例

1、比特币网络:它是分布式系统,比如要判断某地址是否在某节点
2、分布式系统:Map-Reduce,Hadoop,Search Engine;判断数据是否 在某节点中
3、Redis缓存:解决缓存穿透
4、垃圾邮件,评论过滤等

5.作为Redis的防弹衣,防止缓存穿透,先去布隆过滤器查没有直接返回,有去redis查,再没有才会去数据库查
在这里插入图片描述

java实现实例1
java实现实例2

Cache缓存

在这里插入图片描述

缓存三要素

缓存的三个要素
大小(体积) 什么数据放缓存:
空间大:可以将所需要的数据都放缓存
空间小:放一些热数据
替换策略 分级架构的缓存,某级空间不够存储了,用什么策略决定哪些数据被替换(降级)
缓存空间不够了,用什么策略决定应该淘汰哪些数据
工业级缓存替换策略:https://en.wikipedia.org/wiki/Cache_replacement_policies
复杂度要求快速高效,查询尽量是O(1)

LRU

LRU(Least Recently Used):该算法策略首先丢弃最近最少使用的数据

底层数据结构:Hash Table + Double LinkedList
1、双向链表按照被使用的顺序存储了这些键值对,靠近头部的是最近使用的,靠近尾部的是最久未使用的。
2、哈希表即为普通的哈希映射(比如Java中的HashMap),通过缓存数据的键映射到其在双向链表中的位置。
在这里插入图片描述

LRU-GET

在这里插入图片描述
通过哈希表定位到该节点在双向链表中的位置,并将其移动到双向链表的头部,最后返回该节点的值

LRT-PUT

在这里插入图片描述

LRU复杂度分析

时间复杂度
访问哈希表的时间复杂度为 O(1)
在双向链表的头部添加节点、在双向链表的尾部删除节点的复杂度也为 O(1)
将一个节点移到双向链表的头部,可以分成「删除该节点」和「在双向链表的头部添加节点」两步操作, 都可以在 O(1)时间内完成。
结论:查询,添加,修改等操作的时间复杂度均为O(1)

面试题

LRU 缓存机制

class LRUCache {
    HashMap<Integer,Node>map;
    int capacity;
    int size;

    Node head;
    Node tail;

    public LRUCache(int capacity) {
          map=new HashMap<Integer,Node>();
          this.capacity=capacity;
          this.size=0;

          head=new Node();
          tail=new Node();

          head.next=tail;
          tail.prev=head;


    }
    
    public int get(int key) {
        Node node=map.get(key);
        if(node==null){
            return -1;
        }
        //如果有的话,需要把节点放到开头
        removeToHead(node);
        return node.value;
    }

    public void removeToHead(Node node){
        removeNode(node);
        addToHead(node);
    }

    public void removeNode(Node node){
        Node p=node.prev;
        Node t=node.next;

        p.next=t;
        t.prev=p;
        node.prev=null;
        node.next=null;
    }

    public void addToHead(Node node){
        Node firstNode=head.next;
        
        head.next=node;
        node.next=firstNode;
        firstNode.prev=node;
        node.prev=head;
    }


    
    public void put(int key, int value) {
          Node node=map.get(key);
          if(node!=null){
              node.value=value;
              removeToHead(node);
              return;
          }else{
              node=new Node(key,value);
              map.put(key,node);
              addToHead(node);
              size++;
          }
          if(this.size>this.capacity){
              Node n=removeTail();
              map.remove(n.key);
              size--;
          }
    }

    public Node removeTail(){
        Node node=tail.prev;
        Node last=node.prev;

        last.next=tail;
        tail.prev=last;

        node.next=null;
        node.prev=null;
        return node;
    }


    class Node{
        int key;
        int value;
        Node prev;
        Node next;

        public Node(){};
        public Node(int key,int value){
            this.key=key;
            this.value=value;
        }
    }
}

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache obj = new LRUCache(capacity);
 * int param_1 = obj.get(key);
 * obj.put(key,value);
 */

460. LFU 缓存

题解

class LFUCache {
    Map<Integer, Node> cache;  // 存储缓存的内容
    Map<Integer, LinkedHashSet<Node>> freqMap; // 存储每个频次对应的双向链表
    int size;
    int capacity;
    int min; // 存储当前最小频次

    public LFUCache(int capacity) {
        cache = new HashMap<> (capacity);
        freqMap = new HashMap<>();
        this.capacity = capacity;
    }
    
    public int get(int key) {
        Node node = cache.get(key);
        if (node == null) {
            return -1;
        }
        freqInc(node);
        return node.value;
    }
    
    public void put(int key, int value) {
        if (capacity == 0) {
            return;
        }
        Node node = cache.get(key);
        if (node != null) {
            node.value = value;
            freqInc(node);
        } else {
            if (size == capacity) {
                Node deadNode = removeNode();
                cache.remove(deadNode.key);
                size--;
            }
            Node newNode = new Node(key, value);
            cache.put(key, newNode);
            addNode(newNode);
            size++;     
        }
    }

    void freqInc(Node node) {
        // 从原freq对应的链表里移除, 并更新min
        int freq = node.freq;
        LinkedHashSet<Node> set = freqMap.get(freq);
        set.remove(node);
        if (freq == min && set.size() == 0) { 
            min = freq + 1;
        }
        // 加入新freq对应的链表
        node.freq++;
        LinkedHashSet<Node> newSet = freqMap.get(freq + 1);
        if (newSet == null) {
            newSet = new LinkedHashSet<>();
            freqMap.put(freq + 1, newSet);
        }
        newSet.add(node);
    }

    void addNode(Node node) {
        LinkedHashSet<Node> set = freqMap.get(1);
        if (set == null) {
            set = new LinkedHashSet<>();
            freqMap.put(1, set);
        } 
        set.add(node); 
        min = 1;
    }

    Node removeNode() {
        LinkedHashSet<Node> set = freqMap.get(min);
        Node deadNode = set.iterator().next();
        set.remove(deadNode);
        return deadNode;
    }
}

class Node {
    int key;
    int value;
    int freq = 1;

    public Node() {}
    
    public Node(int key, int value) {
        this.key = key;
        this.value = value;
    }
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值