Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

算法基础07-位运算、布隆过滤器、LRU Cache #7

Open
venaissance opened this issue May 10, 2020 · 0 comments
Open

算法基础07-位运算、布隆过滤器、LRU Cache #7

venaissance opened this issue May 10, 2020 · 0 comments

Comments

@venaissance
Copy link
Owner

位运算

核心要领

想要熟练掌握位运算,记住下面几个公式即可:

  • 清除 n 最低位的 1:n & (n - 1)
  • 获取 n 最低位的 1:n & -n
  • 整除 2:n >> 1
  • 判断奇偶: n & 1 == 1 | 0
  • n & ~n = 0

经典例题

52. N-Queens II

思路:

  1. 回溯,时间复杂度 O(n!),空间复杂度 O(n)

  2. 位运算,时间复杂度 O(n!),空间复杂度 O(n),由于位运算更接近计算机底层,所以运行速度会更快,位运算也是N-Queens问题的终极解决方案,算法的核心要点如下:

    • 0 - 不能放皇后,1 - 能放
    • availPos = (~(cols | pie | na)) & ((1 << n) - 1) 考虑整行、两条对角线,得到当前能放皇后的位置,用最低的 n 位表示
    • p = availPos & -availPos 取得 availPos 最低位的1,用来放皇后
    • availPos &= (availPos - 1) 清除最低位的 1,表示皇后已放
    • dfs(n, row + 1, cols | p, (pie | p) << 1, (na | p) >> 1) 对 cols, pie, na 可放皇后的位置做相应的更新,下探下一层 ( Drill Down )
Java 实现
class Solution {
    private int count = 0;

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

    private void dfs(int n, int row, int cols, int pie, int na) {
        if (row == n) {
            count++;
            return;
        }
        int availPos = (~(cols | pie | na)) & ((1 << n) - 1);
        while (availPos != 0) {
            int p = availPos & -availPos;
            availPos &= (availPos - 1);
            dfs(n, row + 1, cols | p, (pie | p) << 1, (na | p) >> 1);
        }
    }
}
Python 实现
class Solution:
    def totalNQueens(self, n: int) -> int:
        self.count = 0
        self.dfs(n, 0, 0, 0, 0)
        return self.count
    
    def dfs(self, n, row, cols, pie, na):
        if row == n:
            self.count += 1
            return
        
        availPos = (~(cols | pie | na)) & ((1 << n) - 1)

        while availPos:
            p = availPos & -availPos
            availPos &= (availPos - 1)
            self.dfs(n, row + 1, cols | p, (pie | p) << 1, (na | p) >> 1)

布隆过滤器

本质

布隆过滤器由一个很长的二进制向量一系列随机映射函数组成。用于检索一个元素是否在一个集合中。

  • 优点是空间效率和查询时间远超一般算法
  • 缺点是有一定的错误识别率和删除困难

要点

布隆过滤器是一个快速判断元素是否存在集合的算法,特点是:

  1. 不需要像哈希表一样存额外的信息
  2. 只能判断肯定不存在可能存在
  3. 适合用作高速缓存,如判断为可能存在,再到数据库中查询
  4. 每个元素的存在用几个二进制位置 1 来表示
  5. 多用于大型分布式系统如比特币网络、Redis缓存、垃圾邮件过滤器、评论过滤器等

LRU Cache

最近最少使用缓存替换策略,是一种 缓存替换策略,其他缓存策略还有 FIFO、LFU、RR 等等。

LRU:Latest Recently Used 最近最少使用。

硬核实现 LRU Cache

接下来让我们自己造轮子,硬核实现一个 LRU Cache,它应该支持以下两个操作:

  • 获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
  • 写入数据 put(key, value) - 如果密钥已经存在,则变更其数据值;如果密钥不存在,则插入该组「密钥/数据值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。

还有两个需求:

  • 增加删除数据的时间复杂度为O(1)
  • 随机访问数据的时间复杂度为O(1)

增删 O(1),我们第一个想到要用 LinkedList,访问 O(1),我们第一个想到要用 HashTable,那么怎么把两者结合起来呢?让我们从思考这两个问题开始:

  1. 链表有特殊要求吗?单链表还是双向链表?
  2. 链表中的节点存什么,只存 value 够吗?

先回答第一个问题,因为我们需要删除数据,链表删除节点需要找到当前节点的前驱,单链表想要找到节点前驱需要从头开始遍历,存在 O(n) 的时间复杂度;而双向链表本身就保存了节点的前驱,删除时间复杂度就是 O(1),所以我们使用双向链表

接下来回答第二个问题,由于缓存空间有限,当缓存存满时,根据 LRU 策略,我们需要移除缓存里保存最久的未被访问节点,比如上图中的 A 和 B,与此同时,我们还需要移除 HashMap 中的该节点(已不在缓存中,下次无法查到),如果节点只存 value,那么 HashMap 就无法移除该节点(哈希表删除节点需要 key, map.remove(key)),所以链表节点需要同时存 key 和 value,就像下图这样。

解答了这两个问题,我们就可以开始动手了,先实现一个同时保存 key 和 value 的双向链表。

class Node {
    public int key, val;
    public Node next, prev;

    public Node(int k, int v) {
        this.key = k;
        this.val = v;
    }
}

// 双向链表
class DoubleLinkedList {
    private Node head, tail; // 头尾虚节点
    private int size; // 链表元素数

    public DoubleLinkedList() {
        head = new Node(0, 0);
        tail = new Node(0, 0);
        head.next = tail;
        tail.prev = head;
        size = 0;
    }

    // 在链表头部添加节点
    public void addFirst(Node node) {
        node.next = head.next;
        node.prev = head;
        head.next.prev = node;
        head.next = node;
        size++;
    }
    
    // 删除链表中的节点(该节点一定存在)
    public void remove(Node node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
        size--;
    }
    
    // 删除链表中最后一个节点,并返回该节点
    public Node removeLast() {
        if (tail.prev == head)
            return null;
        Node last = tail.prev;
        remove(last);
        return last;
    }
    
    // 返回链表长度
    public int size() {
        return size;
    }
}

接下来就是正式实现 LRU Cache 了,核心逻辑写在注释上了。

class LRUCache {
    private HashMap<Integer, Node> map;
    private DoubleLinkedList cache;
    private int cap; // 最大容量

    public LRUCache(int capacity) {
        this.cap = capacity;
        map = new HashMap<>();
        cache = new DoubleLinkedList();
    }

    // 访问节点
    public int get(int key) {
        if (!map.containsKey(key)) {
            return -1;
        }
        int val = map.get(key).val;
        // 使用put方法把最近访问的节点提前
        put(key, val);
        return val;
    }

    public void put(int key, int value) {
        // 生成新的节点
        Node node = new Node(key, value);
        if (map.containsKey(key)) {
            // 删除旧的节点,新的插到头部
            cache.remove(map.get(key));
            cache.addFirst(node);
            // 更新 map 中对应的数据
            map.put(key, node);
        } else {
            // 如果缓存满了
            if (cap == cache.size()) {
                // 删除链表最后一个数据
                Node last = cache.removeLast();
                // 哈希表也要删
                map.remove(last.key);
            }
            // 新节点添加到头部
            cache.addFirst(node);
            map.put(key, node);
        }
    }
}

最后多说一句,如果我们把这两部分合起来,其实就得到了 LeetCode 146最佳题解,我想这也是面试题中对 LRU Cache 实现的最高要求了。

参考链接

  1. Bloomfilter in Python
  2. Orestes-Bloomfilter in Java
  3. 布隆过滤器的原理和实现
  4. Cache Replacement Policies - Wiki
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant