day2_greedy&Intervals&LRU/LFU

二、贪心算法之区间调度问题

0.计算一个区间集合中无重复的区间的最大数量(模板)
public int intervalSchedule(int[][] intvs) {
    if (intvs.length == 0) return 0;
    // 按 end 升序排序
    Arrays.sort(intvs, (a, b) -> Integer.compare(a[1], b[1]));
    // 至少有一个区间不相交
    int count = 1;
    // 排序后,第一个区间就是 x
    int x_end = intvs[0][1];
    for (int[] interval : intvs) {
        int start = interval[0];
        if (start >= x_end) {
            // 找到下一个选择的区间了
            count++;
            x_end = interval[1];
        }
    }
    return count;
}

1.435无重叠区间
class Solution {
    public int eraseOverlapIntervals(int[][] intervals) {
        int n = intervals.length;
        if (n == 0) return 0;
            // 按 end 升序排序
            Arrays.sort(intervals, (a, b) -> Integer.compare(a[1], b[1]));
            // 至少有一个区间不相交
            int count = 1;
            // 排序后,第一个区间就是 x
            int x_end = intervals[0][1];
            for (int[] interval : intervals) {
                int start = interval[0];
                if (start >= x_end) {
                    // 找到下一个选择的区间了
                    count++;
                    x_end = interval[1];
                }
            }
            //count为最多的互不相交的区间,n - count就为最少的需要去除的区间数量
            count = n - count;
            return count;
            }
}
2.452.用最少的箭射爆气球

与模板不同点是在 intervalSchedule 算法中,如果两个区间的边界触碰,不算重叠;而按照这道题目的描述,箭头如果碰到气球的边界气球也会爆炸,所以说相当于区间的边界触碰也算重叠

将不重叠区间的条件 从 >= 改为 > 即可

class Solution {
    public int findMinArrowShots(int[][] points) {
    if (points.length == 0) return 0;
    // 按 end 升序排序
    Arrays.sort(points, (a, b) -> Integer.compare(a[1], b[1]));
    // 至少有一个区间不相交
    int count = 1;
    // 排序后,第一个区间就是 x
    int x_end = points[0][1];
    for (int[] interval : points) {
        int start = interval[0];
        if (start > x_end) {
            // 找到下一个选择的区间了
            count++;
            x_end = interval[1];
        }
    }
    return count;
    }
}

三、一个方法解决三道区间题

在这里插入图片描述

1.区间覆盖问题

1288删除被覆盖区间,返回剩余区间的数量

int removeCoveredIntervals(int[][] intvs) {
    // 按照起点升序排列,起点相同时降序排列
    Arrays.sort(intvs, (a, b) -> {
        if (a[0] == b[0]) {
            return b[1] - a[1];
        }
        return a[0] - b[0]; 
    });

    // 记录合并区间的起点和终点
    int left = intvs[0][0];
    int right = intvs[0][1];
    
    int res = 0;
    for (int i = 1; i < intvs.length; i++) {
        int[] intv = intvs[i];
        // 情况一,找到覆盖区间
        if (left <= intv[0] && right >= intv[1]) {
            res++;
        }
        // 情况二,找到相交区间,合并
        if (right >= intv[0] && right <= intv[1]) {
            right = intv[1];
        }
        // 情况三,完全不相交,更新起点和终点
        if (right < intv[0]) {
            left = intv[0];
            right = intv[1];
        }
    }
    //总数量减去合并后剩余的区间数量就是被删除的区间数量
    return intvs.length - res;
}

2.区间合并问题
class Solution {
    public int[][] merge(int[][] intervals) {
    if(intervals == null || intervals.length == 0) return new int[][]{};
    // 按区间的 start 升序排列
    Arrays.sort(intervals, Comparator.comparingInt(a -> a[0]));
    List<int[]> res = new ArrayList<>();
    //将第一个元素加入到res中
    res.add(intervals[0]);
    
    // 注意从1开始
    for(int i=1; i<intervals.length; i++){
        int[] curr = intervals[i];
        // res 中最后一个元素的引用
        int[] last = res.get(res.size()-1);
        if(curr[0] <= last[1]){ //当前的起始 < 上一个结束,表示相交
            // 找到最大的 end
            last[1] = Math.max(last[1], curr[1]);
        } else{ //否则不相交
            // 处理下一个待合并区间
            res.add(curr);
        }
    }
    return res.toArray(new int[res.size()][]);
    }
}
3.区间交集问题

三个关键点

  1. 两区间相交的条件
  2. 获取相交区间的左端点和右端点
  3. 两个指针什么情况下移动
class Solution {
    public int[][] intervalIntersection(int[][] firstList, int[][] secondList) {
        int i = 0, j = 0;
        List<int[]> res = new ArrayList<>();
        while (i < firstList.length && j < secondList.length) {
        //a1,b1 是 两个区间列表的左端点,a2,b2是对应的右端点
            int a1 = firstList[i][0], a2 = firstList[i][1];
            int b1 = secondList[j][0], b2 = secondList[j][1];
            // 两个区间存在交集,使用没有交集的对立条件推出存在交集的条件
            //if b2 < a1 or a2 < b1:  [a1,a2] 和 [b1,b2] 无交集
            if (b2 >= a1 && a2 >= b1) {
                // 计算出交集,加入 res; 交集是左端点的最大值和右端点的最小值组成的
                res.add(new int[]{Math.max(a1, b1), Math.min(a2, b2)});
            }
            // 指针前进,取决于右端点的大小,谁小谁就向后遍历
            if (b2 < a2) {
                j++;
            } else {
                i++;
            }
        }
        return res.toArray(new int[res.size()][]);
    }
}

四、LRU缓存 && LFU缓存

这个算是我老朋友了,我窥觊它得一个月了,但实在太难,直到遇见labuladong,学的更轻松了,也算是激发了我学习热情,感谢dong哥

LRU 算法的核心数据结构是使用哈希链表 LinkedHashMap,首先借助链表的有序性使得链表元素维持插入顺序,同时借助哈希映射的快速访问能力使得我们可以在 O(1) 时间访问链表的任意元素

1.LRU缓存
  1. 使用哈希双端链表(Java中对应的是LinkedHashMap)

  2. 创建Node单个节点 -> 组成DoubleList双端链表

  3. 因为同时维护map的双端链表,需要抽象出来几个方法在对元素操作时同时操作map和DoubleList,防止漏掉操作,比如说删除某个 key 时,在 cache 中删除了对应的 Node,但是却忘记在 map 中删除 key

    解决这种问题的有效方法是:在这两种数据结构之上提供一层抽象 API

    说的有点玄幻,实际上很简单,就是尽量让 ==LRU 的主方法 getput 避免直接操作 mapcache==的细节。我们可以先实现下面几个函数:

  4. get逻辑比较简单,这里阐述一下put的逻辑

    1. 若key已经存在,那么修改key对应的value
    2. 若key不存在,需要插入key
      1. 当容量满的时候,淘汰最久未使用的key
      2. 当容量没有满,直接执行c
    3. 插入key和val为最近使用的数据
class LRUCache {

    // key -> Node(key, val)
    private HashMap<Integer, Node> map;
    // Node(k1, v1) <-> Node(k2, v2)...
    private DoubleList cache;
    // 最大容量
    private int cap;
    
    public LRUCache(int capacity) {
        this.cap = capacity;
        map = new HashMap<>();
        cache = new DoubleList();
    }


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

class DoubleList {  
    // 头尾虚节点
    private Node head, tail;  
    // 链表元素数
    private int size;
    
    public DoubleList() {
        // 初始化双向链表的数据
        head = new Node(0, 0);
        tail = new Node(0, 0);
        head.next = tail;
        tail.prev = head;
        size = 0;
    }

    // 在链表尾部添加节点 x,时间 O(1)
    public void addLast(Node x) {
        x.prev = tail.prev;
        x.next = tail;
        tail.prev.next = x;
        tail.prev = x;
        size++;
    }

    // 删除链表中的 x 节点(x 一定存在)
    // 由于是双链表且给的是目标 Node 节点,时间 O(1)
    public void remove(Node x) {
        x.prev.next = x.next;
        x.next.prev = x.prev;
        size--;
    }
    
    // 删除链表中第一个节点,并返回该节点,时间 O(1)
    public Node removeFirst() {
        if (head.next == tail)
            return null;
        Node first = head.next;
        remove(first);
        return first;
    }

    // 返回链表长度,时间 O(1)
    public int size() { return size; }

}

 /* 将某个 key 提升为最近使用的 */
    private void makeRecently(int key) {
        Node x = map.get(key);
        // 先从链表中删除这个节点
        cache.remove(x);
        // 重新插到队尾
        cache.addLast(x);
    }

    /* 添加最近使用的元素 */
    private void addRecently(int key, int val) {
        Node x = new Node(key, val);
        // 链表尾部就是最近使用的元素
        cache.addLast(x);
        // 别忘了在 map 中添加 key 的映射
        map.put(key, x);
    }

    /* 删除某一个 key */
    private void deleteKey(int key) {
        Node x = map.get(key);
        // 从链表中删除
        cache.remove(x);
        // 从 map 中删除
        map.remove(key);
    }

    /* 删除最久未使用的元素 */
    private void removeLeastRecently() {
        // 链表头部的第一个元素就是最久未使用的
        Node deletedNode = cache.removeFirst();
        // 同时别忘了从 map 中删除它的 key
        int deletedKey = deletedNode.key;
        map.remove(deletedKey);
    }

    public int get(int key) {
        if (!map.containsKey(key)) {
            return -1;
        }
        // 将该数据提升为最近使用的
        makeRecently(key);
        return map.get(key).val;
    }

    public void put(int key, int val) {
        if (map.containsKey(key)) {
            // 删除旧的数据
            deleteKey(key);
            // 新插入的数据为最近使用的数据
            addRecently(key, val);
            return;
        }
        
        if (cap == cache.size()) {
            // 删除最久未使用的元素
            removeLeastRecently();
        }
        // 添加为最近使用的元素
        addRecently(key, val);
    }

}

/**
 * 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);
 */
2.LFU缓存
class LFUCache {
    // key 到 val 的映射,我们后文称为 KV 表
    HashMap<Integer, Integer> keyToVal;
    // key 到 freq 的映射,我们后文称为 KF 表
    HashMap<Integer, Integer> keyToFreq;
    // freq 到 key 列表的映射,我们后文称为 FK 表
    HashMap<Integer, LinkedHashSet<Integer>> freqToKeys;
    // 记录最小的频次
    int minFreq;
    // 记录 LFU 缓存的最大容量
    int cap;

    public LFUCache(int capacity) {
        keyToVal = new HashMap<>();
        keyToFreq = new HashMap<>();
        freqToKeys = new HashMap<>();
        this.cap = capacity;
        this.minFreq = 0;
    }

    public int get(int key) {
        if (!keyToVal.containsKey(key)) {
            return -1;
        }
        // 增加 key 对应的 freq
        increaseFreq(key);
        return keyToVal.get(key);
    }

    public void put(int key, int val) {
        if (this.cap <= 0) return;

        /* 若 key 已存在,修改对应的 val 即可 */
        if (keyToVal.containsKey(key)) {
            keyToVal.put(key, val);
            // key 对应的 freq 加一
            increaseFreq(key);
            return;
        }

        /* key 不存在,需要插入 */
        /* 容量已满的话需要淘汰一个 freq 最小的 key */
        if (this.cap <= keyToVal.size()) {
            removeMinFreqKey();
        }

        /* 插入 key 和 val,对应的 freq 为 1 */
        // 插入 KV 表
        keyToVal.put(key, val);
        // 插入 KF 表
        keyToFreq.put(key, 1);
        // 插入 FK 表
        freqToKeys.putIfAbsent(1, new LinkedHashSet<>());
        freqToKeys.get(1).add(key);
        // 插入新 key 后最小的 freq 肯定是 1
        this.minFreq = 1;
    }
    
    private void removeMinFreqKey() {
        // freq 最小的 key 列表
        LinkedHashSet<Integer> keyList = freqToKeys.get(this.minFreq);
        // 其中最先被插入的那个 key 就是该被淘汰的 key
        int deletedKey = keyList.iterator().next();
        /* 更新 FK 表 */
        keyList.remove(deletedKey);
        if (keyList.isEmpty()) {
            freqToKeys.remove(this.minFreq);
            // 问:这里需要更新 minFreq 的值吗?
        }
        /* 更新 KV 表 */
        keyToVal.remove(deletedKey);
        /* 更新 KF 表 */
        keyToFreq.remove(deletedKey);
    }
    
    private void increaseFreq(int key) {
        int freq = keyToFreq.get(key);
        /* 更新 KF 表 */
        keyToFreq.put(key, freq + 1);
        /* 更新 FK 表 */
        // 将 key 从 freq 对应的列表中删除
        freqToKeys.get(freq).remove(key);
        // 将 key 加入 freq + 1 对应的列表中
        freqToKeys.putIfAbsent(freq + 1, new LinkedHashSet<>());
        freqToKeys.get(freq + 1).add(key);
        // 如果 freq 对应的列表空了,移除这个 freq
        if (freqToKeys.get(freq).isEmpty()) {
            freqToKeys.remove(freq);
            // 如果这个 freq 恰好是 minFreq,更新 minFreq
            if (freq == this.minFreq) {
                this.minFreq++;
            }
        }
    }

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值